PHP’de Base64 görsel yükleme, görüntünün ikili verisini metne çevirip bir JSON isteğinin içinde sunucuya göndermek anlamına gelir. Canvas çıktısı, kırpma aracı veya imza alanı gibi tarayıcıda üretilen küçük görüntülerde pratiktir; karşılığında istek yaklaşık üçte bir büyür, sunucu aynı veriyi bellekte birkaç kez tutar ve doğrulama sorumluluğu tamamen uygulamaya kalır.
Aşağıda data URI biçimi, istemci tarafı, PHP’de güvenli doğrulama sırası, dosyanın saklanması ve multipart/form-data yönteminin hangi durumlarda daha iyi olduğu ele alınıyor. PHP örnekleri PHP 8.1+ ve GD eklentisi gerektirir.
Data URI biçimi
Tarayıcının ürettiği Base64 görüntü bir data URI’dir:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...
Dört parçadan oluşur:
- Şema:
data: - MIME türü:
image/png,image/jpeggibi. Bu değeri istemci yazar, sunucu için bir iddiadan ibarettir. - Kodlama işareti:
;base64 - Veri: virgülden sonraki Base64 metni.
Base64 her 3 baytı 4 karakterle gösterir. Bu nedenle 3 MB’lık bir görüntü yaklaşık 4 MB’lık bir metne dönüşür.
İstemci tarafı
Bir canvas’ın içeriği toDataURL() ile, seçilen bir dosya ise FileReader ile data URI’ye çevrilir. İstek JSON olarak gönderilir ve HTTP durum kodu kontrol edilir:
// Seçilen dosyayı data URI'ye çevirir
function fileToDataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
}
async function uploadImage(dataUrl) {
const response = await fetch("/upload.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ image: dataUrl }),
});
if (!response.ok) {
throw new Error(`Yükleme başarısız: ${response.status}`);
}
return response.json();
}
// Sayfadaki canvas'ın çıktısını %85 kaliteli JPEG olarak gönderir
const canvas = document.querySelector("canvas");
uploadImage(canvas.toDataURL("image/jpeg", 0.85)).then(console.log);
Gövde JSON olduğu için PHP bu veriyi $_POST dizisine koymaz; $_POST yalnız application/x-www-form-urlencoded ve multipart/form-data isteklerinde dolar. Ham gövde php://input akışından okunur.
Sunucuda doğrulama sırası
Sıralama güvenliğin parçasıdır. Ucuz kontroller pahalı işlemlerden önce gelir; görüntü, boyutu doğrulanmadan belleğe açılmaz:
- Gövde sınırı:
php://inputen fazla belirli bir uzunlukta okunur, ardındanjson_decodeçalışır. - Önek: data URI öneki ayrılır. İstemcinin bildirdiği MIME türü kullanılmaz.
- Kodlu uzunluk: Base64 metni çözülmeden önce izin verilen boyuta karşılık gelen uzunlukla karşılaştırılır.
- Katı çözme:
base64_decode($s, true)alfabe dışı karakter görürsefalsedöndürür. Katı mod olmadan geçersiz karakterler sessizce atılır. - Tür tespiti: Gerçek tür içerikten,
finfoile belirlenir ve izin listesiyle karşılaştırılır. - Piksel sınırı:
getimagesizefromstring()genişlik ve yüksekliği dosya başlığından okur, piksel verisini açmaz. - Çözme ve yeniden kodlama: Görüntü GD ile açılır ve yeni bir dosyaya yeniden yazılır.
- Kayıt: Rastgele bir adla, web kökünün dışındaki bir dizine kaydedilir.
Piksel sınırının dosya boyutundan ayrı kontrol edilmesinin nedeni şudur: çözülmüş bir görüntünün bellek ihtiyacı dosya boyutuna değil, genişlik ile yüksekliğin çarpımına bağlıdır. Birkaç kilobaytlık bir PNG, başlığında on binlerce piksel genişlik ve yükseklik bildirebilir ve açıldığında memory_limit değerini aşabilir. Bu tür dosyalara piksel bombası (decompression bomb) denir.
PHP’de doğrulama ve kayıt
Doğrulama mantığı HTTP katmanından ayrı iki fonksiyonda durur; böylece aynı kod multipart yüklemelerde de kullanılabilir:
<?php
// src/image_upload.php
declare(strict_types=1);
const MAX_BYTES = 5 * 1024 * 1024; // çözülmüş dosya için üst sınır (5 MiB)
const MAX_SIDE = 4096; // genişlik ve yükseklik için üst sınır
const ALLOWED_TYPES = [ // içerikten tespit edilen MIME => uzantı
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
];
function decodeDataUri(string $dataUri): string
{
// İstemcinin bildirdiği MIME türüne güvenilmez; önek yalnızca ayrılır
if (!preg_match('#^data:image/[a-z0-9.+-]+;base64,#i', $dataUri, $m)) {
throw new InvalidArgumentException('Geçersiz data URI');
}
$encoded = substr($dataUri, strlen($m[0]));
// Çözmeden önce kodlu uzunluk: her 4 karakter en fazla 3 bayta karşılık gelir
if (strlen($encoded) > intdiv(MAX_BYTES + 2, 3) * 4) {
throw new LengthException('Dosya çok büyük');
}
// Katı mod: Base64 alfabesi dışında karakter varsa false döner
$binary = base64_decode($encoded, true);
if ($binary === false || $binary === '') {
throw new InvalidArgumentException('Geçersiz Base64');
}
return $binary;
}
function storeImage(string $binary, string $dir): string
{
if (strlen($binary) > MAX_BYTES) {
throw new LengthException('Dosya çok büyük');
}
// Tür, istemcinin beyanından değil içerikten belirlenir
$mime = (new finfo(FILEINFO_MIME_TYPE))->buffer($binary);
$ext = ALLOWED_TYPES[$mime] ?? null;
if ($ext === null) {
throw new InvalidArgumentException('Desteklenmeyen dosya türü');
}
// Boyut başlıktan okunur; piksel verisi henüz belleğe açılmaz
$size = getimagesizefromstring($binary);
if ($size === false || $size['mime'] !== $mime
|| $size[0] < 1 || $size[1] < 1
|| $size[0] > MAX_SIDE || $size[1] > MAX_SIDE) {
throw new InvalidArgumentException('Geçersiz görüntü boyutu');
}
$image = imagecreatefromstring($binary);
if ($image === false) {
throw new InvalidArgumentException('Görüntü çözülemedi');
}
// Rastgele ad; dizin web kökünün dışında olmalı
$name = bin2hex(random_bytes(16)) . '.' . $ext;
$path = $dir . DIRECTORY_SEPARATOR . $name;
// Yeniden kodlama: yalnız piksel verisi yazılır, ek baytlar ve meta veri atılır
imagesavealpha($image, true);
$saved = match ($ext) {
'jpg' => imagejpeg($image, $path, 85),
'png' => imagepng($image, $path),
'webp' => imagewebp($image, $path, 85),
};
if (!$saved) {
throw new RuntimeException('Dosya kaydedilemedi');
}
return $name;
}
HTTP katmanı yalnızca isteği okur, fonksiyonları çağırır ve hatayı durum koduna çevirir:
<?php
// public/upload.php
declare(strict_types=1);
require __DIR__ . '/../src/image_upload.php';
const UPLOAD_DIR = '/var/app-data/uploads'; // web kökünün dışında
header('Content-Type: application/json');
function respond(int $status, array $body): never
{
http_response_code($status);
echo json_encode($body);
exit;
}
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST'
|| !str_starts_with($_SERVER['CONTENT_TYPE'] ?? '', 'application/json')) {
respond(415, ['error' => 'JSON gövdesi bekleniyor']);
}
// Gövdeyi sınırlı oku: Base64 metni, data URI öneki ve JSON için küçük bir pay
$maxBody = intdiv(MAX_BYTES + 2, 3) * 4 + 1024;
$raw = file_get_contents('php://input', false, null, 0, $maxBody + 1);
if ($raw === false || strlen($raw) > $maxBody) {
respond(413, ['error' => 'İstek çok büyük']);
}
try {
$data = json_decode($raw, true, 8, JSON_THROW_ON_ERROR);
if (!is_array($data) || !is_string($data['image'] ?? null)) {
respond(400, ['error' => 'image alanı eksik']);
}
$name = storeImage(decodeDataUri($data['image']), UPLOAD_DIR);
respond(201, ['id' => $name]);
} catch (JsonException) {
respond(400, ['error' => 'Geçersiz JSON']);
} catch (LengthException) {
respond(413, ['error' => 'Dosya çok büyük']);
} catch (InvalidArgumentException $e) {
respond(422, ['error' => $e->getMessage()]);
} catch (Throwable $e) {
error_log((string) $e);
respond(500, ['error' => 'Sunucu hatası']);
}
Kodla ilgili birkaç not:
- Tür tespiti: PHP kılavuzu
getimagesize()fonksiyonunu tek başına geçerlilik kontrolü olarak kullanmamayı, bunun yerine Fileinfo gibi amaca yönelik bir araç önerir. Bu yüzden türfinfoile belirlenir,getimagesizefromstring()yalnızca boyut için kullanılır ve iki sonucun aynı türü göstermesi beklenir. - SVG: SVG bir XML belgesidir ve betik içerebilir. İzin listesinde olmadığı için bu akışta reddedilir; kabul edilmesi gerekiyorsa ayrı bir temizleme adımı gerekir.
- Yeniden kodlama: GD dosyayı piksellerden yeniden yazar. Görüntünün sonuna eklenmiş ek baytlar ve EXIF meta verisi (konum bilgisi dahil) yeni dosyaya taşınmaz. Telefon fotoğraflarındaki yön (Orientation) etiketi de kaybolduğu için döndürmeyi gerekiyorsa ayrıca uygulayın. Kayıpsız biçimlerde piksellerin içine gizlenmiş veri ise korunur; bu konu Steghide ve steganografi yazısında anlatılıyor.
- Kaynak serbest bırakma: PHP 8.0’dan beri GD görüntüleri nesnedir ve kapsam dışına çıkınca serbest kalır.
imagedestroy()artık etkisizdir ve PHP 8.5’te kullanımdan kaldırılmıştır (deprecated). - Sunucu sınırı: Gövde sınırını web sunucusunda da (örneğin nginx
client_max_body_size) uygulamak, büyük isteklerin PHP’ye hiç ulaşmamasını sağlar.
Dosyayı saklamak ve sunmak
Yüklenen dosya web kökünün dışında tutulduğunda, uzantısı ne olursa olsun web sunucusu onu betik olarak çalıştıramaz. Dosyayı sunmak için kimliği ^[a-f0-9]{32}\.(jpg|png|webp)$ gibi katı bir desenle doğrulayan bir uç nokta yazılır; yanıt, kayıt sırasında belirlenen Content-Type ve X-Content-Type-Options: nosniff başlığıyla gönderilir. Bulut nesne depolama kullanılıyorsa aynı ilke geçerlidir: dosya adını istemci değil sunucu belirler.
Küçük resim üretmek veya görüntüyü ImageMagick gibi harici bir araçla işlemek gibi ağır işler istek içinde yapılmak zorunda değildir. Bu işler bir kuyruğa bırakılıp RabbitMQ gibi bir mesaj aracısıyla ayrı bir arka plan işçisine (worker) devredilebilir. Harici araç çağırırken komutu kabuk olmadan çalıştırmak için PHP exec() ve proc_open() yazısındaki yöntem kullanılır.
Ne zaman multipart daha iyidir?
Base64 bir zorunluluk değildir. Canvas çıktısı toBlob() ile doğrudan ikili veri olarak alınabilir, panodan yapıştırılan bir görsel de paste olayında File nesnesi olarak gelir. Her ikisi FormData ile gönderilebilir. Aşağıdaki canvas, önceki örnekte seçilen aynı öğedir:
canvas.toBlob(
async blob => {
const form = new FormData();
form.append("image", blob, "image.jpg");
// Content-Type başlığını elle yazmayın; tarayıcı sınır (boundary) değerini ekler
const response = await fetch("/upload-multipart.php", {
method: "POST",
body: form,
});
if (!response.ok) {
throw new Error(`Yükleme başarısız: ${response.status}`);
}
},
"image/jpeg",
0.85
);
PHP tarafında dosya $_FILES['image'] altında gelir. error alanı UPLOAD_ERR_OK ise geçici dosyanın içeriği storeImage(file_get_contents($_FILES['image']['tmp_name']), UPLOAD_DIR) ile aynı doğrulama zincirinden geçirilir. Multipart bu durumlarda daha uygundur:
- Büyük dosyalar: Base64’teki yaklaşık %33 büyüme ve JSON ile çözülmüş verinin aynı anda bellekte durması ortadan kalkar; PHP yüklenen dosyayı geçici bir dosyaya yazar. Sınırlar
upload_max_filesizevepost_max_sizeile yönetilir. - Özgün dosyalar: Kullanıcının seçtiği fotoğraf veya belge zaten ikili bir dosyadır; metne çevirmek yalnızca maliyet ekler.
- Doğrudan nesne depolama: Sunucunun ürettiği imzalı bir URL ile istemci dosyayı depolama servisine doğrudan yükleyebilir. Uygulama sunucusu baytları hiç taşımaz, doğrulama yükleme tamamlandıktan sonra yapılır.
Base64 ise API’nin yalnız JSON kabul ettiği, görüntünün başka alanlarla tek bir JSON nesnesinde gitmesi gereken ve görüntünün küçük olduğu (avatar, imza, kırpılmış önizleme) durumlarda makul bir seçimdir.
Sonuç
PHP ile Base64 görsel yükleme, tarayıcıda üretilen küçük görüntüler ve yalnız JSON kabul eden API’ler için uygundur. Güvenli kalması için gövde ve kodlu uzunluk çözmeden önce sınırlanmalı, base64_decode katı modda çalışmalı, tür içerikten belirlenmeli, piksel sınırı görüntü açılmadan uygulanmalı ve dosya yeniden kodlanıp rastgele bir adla web kökünün dışına kaydedilmelidir. Büyük veya kullanıcının seçtiği özgün dosyalarda multipart ya da imzalı URL ile doğrudan yükleme daha az bellek ve bant genişliği harcar.