Image #
Hampir semua aplikasi web yang menerima upload gambar membutuhkan kemampuan untuk memproses gambar — resize thumbnail agar tidak menyimpan gambar 10MB dari kamera ponsel, menambahkan watermark brand, mengkonversi format WebP untuk performa, atau generate gambar OG (Open Graph) secara dinamis untuk media sosial. PHP menyediakan ekstensi GD (Graphics Draw) yang sudah terinstal di hampir semua hosting dan tidak membutuhkan software tambahan. Ekstensi Imagick (binding PHP untuk ImageMagick) memberikan kualitas lebih tinggi tapi membutuhkan instalasi terpisah. Artikel ini fokus pada GD karena portabilitasnya — semua kode langsung berjalan di hampir semua server PHP.
Memastikan GD Tersedia #
<?php
// Cek apakah GD aktif
if (!extension_loaded('gd')) {
throw new \RuntimeException("Ekstensi GD tidak aktif");
}
// Info GD — format yang didukung
$info = gd_info();
echo "GD versi: " . $info['GD Version'] . "\n";
echo "JPEG: " . ($info['JPEG Support'] ? 'Ya' : 'Tidak') . "\n";
echo "PNG: " . ($info['PNG Support'] ? 'Ya' : 'Tidak') . "\n";
echo "WebP: " . ($info['WebP Support'] ? 'Ya' : 'Tidak') . "\n";
echo "GIF: " . ($info['GIF Create Support'] ? 'Ya' : 'Tidak') . "\n";
echo "AVIF: " . ($info['AVIF Support'] ? 'Ya' : 'Tidak') . "\n";
// Konstanta format yang berguna
// IMG_JPEG, IMG_PNG, IMG_GIF, IMG_WEBP, IMG_AVIF
Membuka dan Membuat Gambar #
<?php
// Buka gambar yang sudah ada — deteksi format otomatis
$gambar = imagecreatefromjpeg('foto.jpg');
$gambar = imagecreatefrompng('logo.png');
$gambar = imagecreatefromgif('animasi.gif');
$gambar = imagecreatefromwebp('modern.webp');
// Cara universal — deteksi dari MIME type
function bukaGambar(string $path): \GdImage
{
$info = getimagesize($path);
if ($info === false) {
throw new \InvalidArgumentException("Bukan file gambar: $path");
}
$gambar = match($info[2]) {
IMAGETYPE_JPEG => imagecreatefromjpeg($path),
IMAGETYPE_PNG => imagecreatefrompng($path),
IMAGETYPE_GIF => imagecreatefromgif($path),
IMAGETYPE_WEBP => imagecreatefromwebp($path),
IMAGETYPE_AVIF => imagecreatefromavif($path),
default => throw new \InvalidArgumentException(
"Format tidak didukung: " . image_type_to_mime_type($info[2])
),
};
if ($gambar === false) {
throw new \RuntimeException("Gagal membuka gambar: $path");
}
return $gambar;
}
// Buat gambar baru (canvas kosong)
$lebar = 800;
$tinggi = 600;
$canvas = imagecreatetruecolor($lebar, $tinggi);
// imagecreate — 8-bit (256 warna), hindari kecuali untuk GIF
// imagecreatetruecolor — 24-bit (16 juta warna), gunakan ini
// Warna — imagecolorallocate() untuk setiap warna
$putih = imagecolorallocate($canvas, 255, 255, 255); // RGB
$hitam = imagecolorallocate($canvas, 0, 0, 0);
$merah = imagecolorallocate($canvas, 255, 0, 0);
$biru = imagecolorallocate($canvas, 0, 100, 200);
$abuAbu = imagecolorallocate($canvas, 128, 128, 128);
// Warna dengan alpha (transparansi) — 0=opak, 127=transparan
$putihSemiTransparan = imagecolorallocatealpha($canvas, 255, 255, 255, 64);
// Isi background dengan warna
imagefill($canvas, 0, 0, $putih);
// Jangan lupa destroy gambar setelah selesai untuk bebaskan memori
imagedestroy($canvas);
Resize — Mengubah Ukuran #
<?php
function resize(
string $inputPath,
string $outputPath,
int $lebarBaru,
int $tinggiBaru = 0, // 0 = hitung otomatis pertahankan rasio
int $kualitas = 85, // untuk JPEG: 0-100
): void {
$sumber = bukaGambar($inputPath);
$lebarAsli = imagesx($sumber);
$tinggiAsli = imagesy($sumber);
// Hitung dimensi baru dengan mempertahankan rasio aspek
if ($tinggiBaru === 0) {
$rasio = $lebarBaru / $lebarAsli;
$tinggiBaru = (int) round($tinggiAsli * $rasio);
} elseif ($lebarBaru === 0) {
$rasio = $tinggiBaru / $tinggiAsli;
$lebarBaru = (int) round($lebarAsli * $rasio);
}
// Buat canvas baru
$hasil = imagecreatetruecolor($lebarBaru, $tinggiBaru);
// Pertahankan transparansi untuk PNG dan GIF
$ekstensi = strtolower(pathinfo($outputPath, PATHINFO_EXTENSION));
if (in_array($ekstensi, ['png', 'gif'])) {
imagealphablending($hasil, false);
imagesavealpha($hasil, true);
$transparan = imagecolorallocatealpha($hasil, 0, 0, 0, 127);
imagefill($hasil, 0, 0, $transparan);
}
// Resize dengan resampling berkualitas tinggi
imagecopyresampled(
$hasil, $sumber,
0, 0, 0, 0, // posisi tujuan, posisi sumber
$lebarBaru, $tinggiBaru, // ukuran tujuan
$lebarAsli, $tinggiAsli // ukuran sumber
);
// Simpan sesuai format output
simpanGambar($hasil, $outputPath, $kualitas);
imagedestroy($sumber);
imagedestroy($hasil);
}
function simpanGambar(\GdImage $gambar, string $path, int $kualitas = 85): void
{
$ekstensi = strtolower(pathinfo($path, PATHINFO_EXTENSION));
match($ekstensi) {
'jpg', 'jpeg' => imagejpeg($gambar, $path, $kualitas),
'png' => imagepng($gambar, $path, (int) round((100 - $kualitas) / 10)),
'gif' => imagegif($gambar, $path),
'webp' => imagewebp($gambar, $path, $kualitas),
'avif' => imageavif($gambar, $path, $kualitas),
default => throw new \InvalidArgumentException("Format tidak didukung: $ekstensi"),
};
}
// Penggunaan
resize('foto_besar.jpg', 'thumbnail.jpg', lebarBaru: 300);
resize('foto_besar.jpg', 'medium.webp', lebarBaru: 800, tinggiBaru: 600);
Crop — Memotong Gambar #
<?php
// Crop area tertentu
function crop(
string $inputPath,
string $outputPath,
int $x, int $y,
int $lebar, int $tinggi,
int $kualitas = 85,
): void {
$sumber = bukaGambar($inputPath);
$hasil = imagecreatetruecolor($lebar, $tinggi);
imagecopyresampled(
$hasil, $sumber,
0, 0, // posisi di canvas output
$x, $y, // posisi mulai crop di sumber
$lebar, $tinggi, $lebar, $tinggi
);
simpanGambar($hasil, $outputPath, $kualitas);
imagedestroy($sumber);
imagedestroy($hasil);
}
// Smart crop — crop ke tengah (center crop)
function cropTengah(
string $inputPath,
string $outputPath,
int $lebarTarget,
int $tinggiTarget,
int $kualitas = 85,
): void {
$sumber = bukaGambar($inputPath);
$lebarAsli = imagesx($sumber);
$tinggiAsli = imagesy($sumber);
// Hitung rasio untuk fit ke target tanpa distorsi
$rasioX = $lebarTarget / $lebarAsli;
$rasioY = $tinggiTarget / $tinggiAsli;
$rasio = max($rasioX, $rasioY); // ambil yang lebih besar agar gambar memenuhi target
$lebarResize = (int) round($lebarAsli * $rasio);
$tinggiResize = (int) round($tinggiAsli * $rasio);
// Resize dulu
$resize = imagecreatetruecolor($lebarResize, $tinggiResize);
imagecopyresampled($resize, $sumber, 0, 0, 0, 0, $lebarResize, $tinggiResize, $lebarAsli, $tinggiAsli);
imagedestroy($sumber);
// Lalu crop dari tengah
$offsetX = (int) round(($lebarResize - $lebarTarget) / 2);
$offsetY = (int) round(($tinggiResize - $tinggiTarget) / 2);
$hasil = imagecreatetruecolor($lebarTarget, $tinggiTarget);
imagecopyresampled($hasil, $resize, 0, 0, $offsetX, $offsetY, $lebarTarget, $tinggiTarget, $lebarTarget, $tinggiTarget);
imagedestroy($resize);
simpanGambar($hasil, $outputPath, $kualitas);
imagedestroy($hasil);
}
// Crop menjadi persegi (untuk thumbnail sosmed)
cropTengah('foto_landscape.jpg', 'thumbnail_square.jpg', 400, 400);
Watermark #
<?php
// Watermark teks
function watermarkTeks(
string $inputPath,
string $outputPath,
string $teks,
int $ukuranFont = 20,
int $opacity = 70, // 0-100
string $posisi = 'bawah-kanan', // bawah-kanan, tengah, dll.
int $kualitas = 85,
): void {
$gambar = bukaGambar($inputPath);
$lebar = imagesx($gambar);
$tinggi = imagesy($gambar);
// Font built-in GD (harus pakai TTF untuk font kustom)
// Font bawaan: 1-5 (ukuran tetap, tidak bisa diatur besar)
$font = 5; // font terbesar bawaan
$lebarTeks = imagefontwidth($font) * strlen($teks);
$tinggiTeks = imagefontheight($font);
// Tentukan posisi
$padding = 15;
[$x, $y] = match($posisi) {
'bawah-kanan' => [$lebar - $lebarTeks - $padding, $tinggi - $tinggiTeks - $padding],
'bawah-kiri' => [$padding, $tinggi - $tinggiTeks - $padding],
'atas-kanan' => [$lebar - $lebarTeks - $padding, $padding],
'atas-kiri' => [$padding, $padding],
'tengah' => [(int)(($lebar - $lebarTeks) / 2), (int)(($tinggi - $tinggiTeks) / 2)],
default => [$padding, $tinggi - $tinggiTeks - $padding],
};
// Warna dengan alpha untuk transparansi
$alpha = (int) round(127 * (1 - $opacity / 100));
$putih = imagecolorallocatealpha($gambar, 255, 255, 255, $alpha);
$bayangan = imagecolorallocatealpha($gambar, 0, 0, 0, $alpha);
// Tulis bayangan dulu (offset 1px) lalu teks utama
imagestring($gambar, $font, $x + 1, $y + 1, $teks, $bayangan);
imagestring($gambar, $font, $x, $y, $teks, $putih);
simpanGambar($gambar, $outputPath, $kualitas);
imagedestroy($gambar);
}
// Watermark dengan TTF font (kualitas lebih baik)
function watermarkTeksTTF(
string $inputPath,
string $outputPath,
string $teks,
string $fontPath, // path ke file .ttf
int $ukuranFont = 24,
int $kualitas = 85,
): void {
$gambar = bukaGambar($inputPath);
$lebar = imagesx($gambar);
$tinggi = imagesy($gambar);
// Hitung bounding box teks
$bbox = imagettfbbox($ukuranFont, 0, $fontPath, $teks);
$lebarTeks = abs($bbox[2] - $bbox[0]);
$tinggiTeks = abs($bbox[7] - $bbox[1]);
$x = $lebar - $lebarTeks - 20;
$y = $tinggi - 20;
// Bayangan
$bayangan = imagecolorallocatealpha($gambar, 0, 0, 0, 64);
imagettftext($gambar, $ukuranFont, 0, $x + 2, $y + 2, $bayangan, $fontPath, $teks);
// Teks utama
$putih = imagecolorallocatealpha($gambar, 255, 255, 255, 32);
imagettftext($gambar, $ukuranFont, 0, $x, $y, $putih, $fontPath, $teks);
simpanGambar($gambar, $outputPath, $kualitas);
imagedestroy($gambar);
}
// Watermark gambar (logo overlay)
function watermarkGambar(
string $inputPath,
string $logoPath,
string $outputPath,
int $opacity = 50,
string $posisi = 'bawah-kanan',
int $kualitas = 85,
): void {
$gambar = bukaGambar($inputPath);
$logo = bukaGambar($logoPath);
$lebarGambar = imagesx($gambar);
$tinggiGambar = imagesy($gambar);
$lebarLogo = imagesx($logo);
$tinggiLogo = imagesy($logo);
$padding = 15;
[$x, $y] = match($posisi) {
'bawah-kanan' => [$lebarGambar - $lebarLogo - $padding, $tinggiGambar - $tinggiLogo - $padding],
'bawah-kiri' => [$padding, $tinggiGambar - $tinggiLogo - $padding],
'tengah' => [(int)(($lebarGambar - $lebarLogo) / 2), (int)(($tinggiGambar - $tinggiLogo) / 2)],
default => [$lebarGambar - $lebarLogo - $padding, $tinggiGambar - $tinggiLogo - $padding],
};
// Merge dengan opacity
imagecopymerge($gambar, $logo, $x, $y, 0, 0, $lebarLogo, $tinggiLogo, $opacity);
simpanGambar($gambar, $outputPath, $kualitas);
imagedestroy($gambar);
imagedestroy($logo);
}
Output ke Browser #
<?php
// Kirim gambar langsung ke browser tanpa simpan ke file
function outputGambarKeBrowser(\GdImage $gambar, string $format = 'jpeg', int $kualitas = 85): void
{
$mimeType = match($format) {
'jpeg', 'jpg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
default => 'image/jpeg',
};
header("Content-Type: $mimeType");
header('Cache-Control: public, max-age=86400'); // cache 24 jam
match($format) {
'jpeg', 'jpg' => imagejpeg($gambar, null, $kualitas), // null = output ke browser
'png' => imagepng($gambar),
'gif' => imagegif($gambar),
'webp' => imagewebp($gambar, null, $kualitas),
};
}
// Contoh: generate thumbnail on-demand
// URL: /thumbnail.php?id=42&w=300&h=200
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$lebar = filter_input(INPUT_GET, 'w', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 2000]]);
$tinggi = filter_input(INPUT_GET, 'h', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 2000]]);
if (!$id || !$lebar) {
http_response_code(400);
exit;
}
$pathAsli = "/uploads/produk/$id.jpg";
$pathCache = "/cache/thumb/{$id}_{$lebar}x{$tinggi}.webp";
if (!file_exists($pathAsli)) {
http_response_code(404);
exit;
}
// Serve dari cache jika ada
if (file_exists($pathCache)) {
header('Content-Type: image/webp');
header('X-Cache: HIT');
readfile($pathCache);
exit;
}
// Generate dan cache
$sumber = bukaGambar($pathAsli);
$thumb = imagecreatetruecolor($lebar, $tinggi ?: imagesx($sumber));
// ... resize logic ...
imagewebp($thumb, $pathCache, 80);
imagedestroy($sumber);
// Serve hasil
header('Content-Type: image/webp');
header('X-Cache: MISS');
imagewebp($thumb, null, 80);
imagedestroy($thumb);
exit;
Pola Upload Gambar yang Aman #
<?php
class ImageUploader
{
private const MAX_SIZE_BYTES = 10 * 1024 * 1024; // 10MB
private const ALLOWED_TYPES = [IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_WEBP, IMAGETYPE_GIF];
private const MAX_LEBAR = 5000;
private const MAX_TINGGI = 5000;
public function __construct(
private string $uploadDir,
private int $thumbLebar = 400,
private int $outputKualitas = 82,
) {
if (!is_dir($this->uploadDir)) {
mkdir($this->uploadDir, 0755, true);
}
}
public function upload(array $file): array
{
// 1. Validasi error upload PHP
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new \RuntimeException("Upload error: " . $this->pesanError($file['error']));
}
// 2. Validasi ukuran file
if ($file['size'] > self::MAX_SIZE_BYTES) {
throw new \InvalidArgumentException("File terlalu besar (maks 10MB)");
}
// 3. Validasi tipe gambar — JANGAN percaya $_FILES['type']!
// Gunakan getimagesize() pada file yang sudah diupload
$imageInfo = @getimagesize($file['tmp_name']);
if ($imageInfo === false) {
throw new \InvalidArgumentException("File bukan gambar yang valid");
}
if (!in_array($imageInfo[2], self::ALLOWED_TYPES, strict: true)) {
throw new \InvalidArgumentException(
"Format tidak didukung: " . image_type_to_mime_type($imageInfo[2])
);
}
// 4. Validasi dimensi
if ($imageInfo[0] > self::MAX_LEBAR || $imageInfo[1] > self::MAX_TINGGI) {
throw new \InvalidArgumentException("Gambar terlalu besar (maks 5000x5000px)");
}
// 5. Generate nama file yang aman (jangan gunakan nama asli!)
$namaFile = bin2hex(random_bytes(16)) . '.webp';
$namaThumbnail = 'thumb_' . $namaFile;
$pathFull = $this->uploadDir . '/' . $namaFile;
$pathThumb = $this->uploadDir . '/thumbnails/' . $namaThumbnail;
if (!is_dir(dirname($pathThumb))) {
mkdir(dirname($pathThumb), 0755, true);
}
// 6. Proses gambar (konversi ke WebP, resize thumbnail)
$sumber = bukaGambar($file['tmp_name']);
// Simpan full size sebagai WebP
imagewebp($sumber, $pathFull, $this->outputKualitas);
// Buat thumbnail
$lebarAsli = imagesx($sumber);
$tinggiAsli = imagesy($sumber);
$rasio = $this->thumbLebar / $lebarAsli;
$tinggiThumb = (int) round($tinggiAsli * $rasio);
$thumb = imagecreatetruecolor($this->thumbLebar, $tinggiThumb);
imagecopyresampled($thumb, $sumber, 0, 0, 0, 0, $this->thumbLebar, $tinggiThumb, $lebarAsli, $tinggiAsli);
imagewebp($thumb, $pathThumb, $this->outputKualitas);
imagedestroy($sumber);
imagedestroy($thumb);
return [
'nama_file' => $namaFile,
'nama_thumb' => $namaThumbnail,
'lebar' => $imageInfo[0],
'tinggi' => $imageInfo[1],
'ukuran' => $file['size'],
'mime' => 'image/webp',
];
}
private function pesanError(int $kode): string
{
return match($kode) {
UPLOAD_ERR_INI_SIZE => "File melebihi upload_max_filesize di php.ini",
UPLOAD_ERR_FORM_SIZE => "File melebihi MAX_FILE_SIZE di form HTML",
UPLOAD_ERR_PARTIAL => "File hanya terupload sebagian",
UPLOAD_ERR_NO_FILE => "Tidak ada file yang diupload",
UPLOAD_ERR_NO_TMP_DIR => "Direktori temp tidak ada",
UPLOAD_ERR_CANT_WRITE => "Gagal menulis file ke disk",
UPLOAD_ERR_EXTENSION => "Upload dihentikan oleh ekstensi PHP",
default => "Error tidak dikenal: $kode",
};
}
}
// Penggunaan
$uploader = new ImageUploader('/var/www/uploads', thumbLebar: 300);
try {
$hasil = $uploader->upload($_FILES['foto']);
echo json_encode(['sukses' => true, 'data' => $hasil]);
} catch (\InvalidArgumentException $e) {
http_response_code(422);
echo json_encode(['error' => $e->getMessage()]);
} catch (\RuntimeException $e) {
http_response_code(500);
echo json_encode(['error' => 'Gagal memproses gambar']);
error_log($e->getMessage());
}
Ringkasan #
imagecreatefromjpeg/png/webp()untuk membuka,imagecreatetruecolor()untuk membuat canvas baru. Selalu gunakanimagedestroy()setelah selesai untuk membebaskan memori.imagecopyresampled()(bukanimagecopyresized()) untuk resize berkualitas tinggi — ia melakukan interpolasi bilinear yang menghasilkan gambar yang jauh lebih halus.- Pertahankan transparansi PNG/GIF dengan
imagealphablending(false)+imagesavealpha(true)+imagecolorallocatealpha()sebelum operasi resize atau crop.- Output ke browser: kirim header
Content-Typeyang benar lalu panggil fungsi output dengan argumen keduanull(misalnyaimagejpeg($img, null, 85)).- Validasi upload gambar dengan
getimagesize()pada file tmp — jangan percaya$_FILES['type']karena bisa dipalsukan. GunakanIMAGETYPE_JPEG,IMAGETYPE_PNG, dll. untuk cek tipe.- Gunakan nama file acak (
bin2hex(random_bytes(16))) untuk file yang diupload — jangan gunakan nama asli dari user karena bisa mengandung karakter berbahaya atau menimpa file penting.- Konversi ke WebP untuk semua gambar yang diupload — ukuran file 25-35% lebih kecil dari JPEG dengan kualitas yang sama. PHP 7.0+ mendukung
imagewebp().- Cache thumbnail yang sudah di-generate — jangan generate ulang setiap request. Simpan ke disk dan serve langsung di request berikutnya.