false, 'ERROR' => 'missing_itemid', 'MESSAGE' => 'ItemID is required']); } if (!isset($_FILES['photo']) || $_FILES['photo']['error'] !== UPLOAD_ERR_OK) { jsonResponse(['OK' => false, 'ERROR' => 'no_file', 'MESSAGE' => 'No file was uploaded']); } $allowedExtensions = ['jpg', 'jpeg', 'gif', 'png', 'webp', 'heic', 'heif']; $ext = strtolower(pathinfo($_FILES['photo']['name'], PATHINFO_EXTENSION)); if (!in_array($ext, $allowedExtensions)) { jsonResponse(['OK' => false, 'ERROR' => 'invalid_type', 'MESSAGE' => "Only image files are accepted (jpg, jpeg, gif, png, webp, heic). Got: $ext"]); } // Determine uploads directory (must be in Lucee webroot, not PHP docroot) $itemsDir = luceeWebroot() . '/uploads/items'; if (!is_dir($itemsDir)) { mkdir($itemsDir, 0755, true); } try { $tmpFile = $_FILES['photo']['tmp_name']; // Delete old photos and thumbnails foreach (['jpg', 'jpeg', 'gif', 'png', 'webp'] as $oldExt) { foreach (['', '_thumb', '_medium'] as $suffix) { $oldFile = "$itemsDir/{$itemId}{$suffix}.{$oldExt}"; if (file_exists($oldFile)) { @unlink($oldFile); } } } // Load image with GD $srcImage = null; $mime = mime_content_type($tmpFile); switch ($mime) { case 'image/jpeg': $srcImage = imagecreatefromjpeg($tmpFile); break; case 'image/png': $srcImage = imagecreatefrompng($tmpFile); break; case 'image/gif': $srcImage = imagecreatefromgif($tmpFile); break; case 'image/webp': $srcImage = imagecreatefromwebp($tmpFile); break; default: // Try JPEG as fallback (HEIC converted by server) $srcImage = @imagecreatefromjpeg($tmpFile); } if (!$srcImage) { jsonResponse(['OK' => false, 'ERROR' => 'invalid_image', 'MESSAGE' => 'Could not process image file']); } // Fix EXIF orientation for JPEG if (function_exists('exif_read_data') && in_array($mime, ['image/jpeg', 'image/tiff'])) { $exif = @exif_read_data($tmpFile); if ($exif && isset($exif['Orientation'])) { switch ($exif['Orientation']) { case 3: $srcImage = imagerotate($srcImage, 180, 0); break; case 6: $srcImage = imagerotate($srcImage, -90, 0); break; case 8: $srcImage = imagerotate($srcImage, 90, 0); break; } } } $origW = imagesx($srcImage); $origH = imagesy($srcImage); // Helper: resize to fit within maxSize box $resizeToFit = function($img, int $maxSize) { $w = imagesx($img); $h = imagesy($img); if ($w <= $maxSize && $h <= $maxSize) return $img; if ($w > $h) { $newW = $maxSize; $newH = (int) ($h * ($maxSize / $w)); } else { $newH = $maxSize; $newW = (int) ($w * ($maxSize / $h)); } $resized = imagecreatetruecolor($newW, $newH); imagecopyresampled($resized, $img, 0, 0, 0, 0, $newW, $newH, $w, $h); return $resized; }; // Helper: create square center-crop thumbnail $createSquareThumb = function($img, int $size) { $w = imagesx($img); $h = imagesy($img); // Resize so smallest dimension equals size if ($w > $h) { $newH = $size; $newW = (int) ($w * ($size / $h)); } else { $newW = $size; $newH = (int) ($h * ($size / $w)); } $resized = imagecreatetruecolor($newW, $newH); imagecopyresampled($resized, $img, 0, 0, 0, 0, $newW, $newH, $w, $h); // Center crop to square $x = (int) (($newW - $size) / 2); $y = (int) (($newH - $size) / 2); $thumb = imagecreatetruecolor($size, $size); imagecopy($thumb, $resized, 0, 0, $x, $y, $size, $size); imagedestroy($resized); return $thumb; }; // Create thumbnail (128x128 square for retina) $thumb = $createSquareThumb($srcImage, 128); imagejpeg($thumb, "$itemsDir/{$itemId}_thumb.jpg", 85); imagedestroy($thumb); // Create medium (400px max) $medium = $resizeToFit($srcImage, 400); imagejpeg($medium, "$itemsDir/{$itemId}_medium.jpg", 85); if ($medium !== $srcImage) imagedestroy($medium); // Save full size (1200px max) $full = $resizeToFit($srcImage, 1200); imagejpeg($full, "$itemsDir/{$itemId}.jpg", 90); if ($full !== $srcImage) imagedestroy($full); imagedestroy($srcImage); $cacheBuster = date('YmdHis'); jsonResponse([ 'OK' => true, 'ERROR' => '', 'MESSAGE' => 'Photo uploaded successfully', 'IMAGEURL' => "/uploads/items/{$itemId}.jpg?v={$cacheBuster}", 'THUMBURL' => "/uploads/items/{$itemId}_thumb.jpg?v={$cacheBuster}", 'MEDIUMURL' => "/uploads/items/{$itemId}_medium.jpg?v={$cacheBuster}", ]); } catch (Exception $e) { jsonResponse(['OK' => false, 'ERROR' => 'server_error', 'MESSAGE' => $e->getMessage(), 'DETAIL' => '']); }