Looking for an online tool that will take an image and make a thumbnail copy? This article is for me if I ever want to recreate an online tool capable of receiving an image URL and which both generates and outputs a thumbnail image and makes it downloadable via URL.
Why?
Performance. I have a client with about 10k images of products that they want to appear in a dropdown menu of a JavaScript widget. When the user first loads up the webpage containing the widget, 10k images are downloaded. I have put in a lazy loading process where it will load the first few images and as the user scrolls down the dropdown, more are loaded. This still gets buggy on certain mobile devices as some images are over 1Mb.
So I needed a tool that every time a new product is added, a thumbnail gets generated and stored in the same application. Trawling through the first few pages of Google, all the online tools that did the same had a pricing page. I can understand why you would need to limit users so I don't blame them. But if you can, why not make your own?
How?
So you will need a webserver of your own running PHP 8. The following PHP script was only tested using PHP version 8.x so I can't say whether it will work for previous versions.
I cannot take credit for this script, as I asked OpenAI's ChatGPT to write it initially, and to build upon it as I kept moving the goal posts, figuratively speaking, and changing the requirements. I am impressed however that every version it iterated, worked exactly as I asked with only 1 error where it mixed a method with a comment but thereafter, a working script every time.
The PHP Script: I'm calling it thumbnailer.php
-
Deploy the Script
Place thumbnailer.php into a web-accessible directory on your server (for example, public_html/api/thumbnailer.php). It will be called as a JSON API over GET and does not render a webpage. -
Create Storage Folders
In the same directory, create two writable subfolders:- _origs/ — stores the original downloaded images
- _thmbs/ — stores the 75×75 thumbnail images
-
Client-Specific Subfolders
The script automatically creates per-client subdirectories inside both _origs and _thmbs when you supply a client key in the request. You do not need to pre-create those. -
Secure Access via GET
All calls must include:- name — the output file name without the extension
- url — the source image URL
- auth — your daily MD5 API key
- client — your lowercase alphanumeric client identifier
-
Example Request
copyrawhttps://your-domain.com/api/thumbnailer.php? url=https://picsum.photos/400 &name=test_photo &client=joellipman &auth=YOUR_DAILY_KEY
- https://your-domain.com/api/thumbnailer.php?
- url=https://picsum.photos/400
- &name=test_photo
- &client=joellipman
- &auth=YOUR_DAILY_KEY
On success, you’ll receive:copyraw{ "original": "https://your-domain.com/api/_origs/joellipman/test_photo.jpg", "thumbnail": "https://your-domain.com/api/_thmbs/joellipman/test_photo_75x75.jpg" }
- {
- "original": "https://your-domain.com/api/_origs/joellipman/test_photo.jpg",
- "thumbnail": "https://your-domain.com/api/_thmbs/joellipman/test_photo_75x75.jpg"
- }
-
Permissions & Logging
• Ensure PHP can write to _origs and _thmbs.
• Optionally monitor or rotate logs for download and processing errors.
copyraw
<?php /** * PHP Thumbnail Generator with Dynamic Auth, Extended Format Support, * URL Parameter, Custom Output Name, JSON Response, Original Image Storage, * and Client-based Subfolders * * Accepts via GET: * - 'url' (image URL, required) * - 'name' (custom base filename, optional) * - 'auth' (API key, required; must match md5(internal_auth_key_1 . internal_auth_key_2 . the_client_name . date('d-M-Y'))) * - 'client' (client identifier, required; lowercase, alphanumeric, hyphens/underscores) * * If 'auth' is invalid, returns HTTP 403 with JSON {"error":"invalid key"}. * If 'client' is invalid or missing, returns HTTP 400 with JSON {"error":"invalid client"}. * Otherwise, downloads the image, stores the original in '_origs/{client}', * creates a 75x75 thumbnail in '_thmbs/{client}', and returns JSON with both URLs. * Supports JPEG, PNG, GIF, WebP, BMP (GD), HEIC/others (Imagick). * * Changelog: * v1.0.0 2025-05-28: - Initial Release * - Basic script to download an image from a hard-coded URL. * - Resizes to 75x75 pixels using GD. * - Saves thumbnail in a Thumbnails directory. * * v1.1.0 2025-05-28: - Added Format Support * - Added support for PNG and GIF formats (with transparency preservation). * - Detects image type via getimagesize() and calls the appropriate GD loader. * * v1.2.0 2025-05-28: - WebP Support * - Added detection and handling for WebP images. * - Uses imagecreatefromwebp()/imagewebp() when available. * - Error message if WebP support is missing in PHP. * * v1.3.0 2025-05-28: - BMP and HEIC Support * - Added BMP support via imagecreatefrombmp()/imagebmp(). * - Added HEIC (and other formats) support via Imagick fallback. * - HEIC handled by Imagick::cropThumbnailImage() when GD cannot process. * * v1.4.0 2025-05-28: - Basic Authentication * - Added private key basic authentication including date. * - Returns a JSON payload with both original and thumbnail URLs. * * v1.5.0 2025-05-28: - Custom Folders and Client Subfolders * - Changed destination folder from Thumbnails to _thmbs and originals to _origs. * - Automatically creates per-client subdirectories when &client= is passed. * - Included client name in authentication * * v1.6.0 2025-05-29: - JSON Response & Original URL * - Ensures originals are saved with their original file extensions. * * v1.7.0 2025-05-29: - Filename Suffix Change * - Changed thumbnail suffix from _thumb to _75x75 to reflect dimensions. * * v1.8.0 2025-05-29: - Aspect Ratio & Background Fill * - Retains source aspect ratio when resizing. * - Outputs a square 75x75 thumbnail by centering on a white canvas. * - Implements both GD and Imagick branches for background fill. * * v1.9.0 2025-05-29: - Force JPEG Output for Thumbnails * - Force thumbnail to always be JPG (compression at 90) * * v1.10.0 2025-06-18: - AVIF compatibility * - Handle AVIF image format * - Upload and convert original to JPG format * */ header('Content-Type: application/json'); // Validate client parameter $clientRaw = filter_input(INPUT_GET, 'client', FILTER_SANITIZE_STRING); $client = preg_match('/^[a-z0-9_-]+$/', $clientRaw) ? $clientRaw : ''; if (!$client) { header('HTTP/1.1 400 Bad Request'); echo json_encode(['error' => 'invalid client']); exit; } // Validate API auth parameter (change the $auth1 and $auth2 values and match these to the requesting script) $auth = filter_input(INPUT_GET, 'auth', FILTER_SANITIZE_STRING); $auth1 = "just_some_random_key_about_64_characters_long_of_alphanumeric_and_symbols"; $auth2 = "another_random_key_about_64_characters_long_of_alphanumeric_and_symbols"; $today = date('d-M-Y'); $expected = md5($auth1 . $auth2 . $client . $today); if ($auth !== $expected) { header('HTTP/1.1 403 Forbidden'); echo json_encode(['error' => 'invalid key']); exit; } // *********** NO NEED TO CHANGE ANYTHING BELOW THIS COMMENT LINE *********** // Fetch and validate URL $url = filter_input(INPUT_GET, 'url', FILTER_VALIDATE_URL); if (!$url) { header('HTTP/1.1 400 Bad Request'); echo json_encode(['error' => "Provide a valid 'url' parameter."]); exit; } // Determine base filename $customName = filter_input(INPUT_GET, 'name', FILTER_SANITIZE_STRING); if ($customName) { $baseName = preg_replace('/[^A-Za-z0-9_-]/', '', $customName); if ($baseName === '') { header('HTTP/1.1 400 Bad Request'); echo json_encode(['error' => "'name' invalid—use letters, numbers, hyphens, underscores."]); exit; } } else { $baseName = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_FILENAME); } // Settings: client-specific directories $originalFolder = __DIR__ . '/_origs/' . $client; $thumbnailFolder = __DIR__ . '/_thmbs/' . $client; $thumbWidth = 75; $thumbHeight = 75; // Ensure directories exist (creates parent paths automatically) foreach ([$originalFolder, $thumbnailFolder] as $dir) { if (!is_dir($dir) && !mkdir($dir, 0755, true)) { header('HTTP/1.1 500 Internal Server Error'); exit(json_encode(['error' => "Failed to create directory: $dir"])); } } // Base URL paths $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS']!=='off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST']; $scriptDir = rtrim(dirname($_SERVER['PHP_SELF']), '/'); $origBaseURL = "$protocol://$host$scriptDir/_origs/$client"; $thumbBaseURL = "$protocol://$host$scriptDir/_thmbs/$client"; // Download source to temp file $tempFile = tempnam(sys_get_temp_dir(), 'img_'); $imgData = @file_get_contents($url); if (!$imgData || file_put_contents($tempFile, $imgData) === false) { header('HTTP/1.1 502 Bad Gateway'); exit(json_encode(['error' => 'Failed to download image.'])); } // Validate image $info = @getimagesize($tempFile); if (!$info) { unlink($tempFile); header('HTTP/1.1 415 Unsupported Media Type'); exit(json_encode(['error' => 'Downloaded file is not a valid image.'])); } list($origW, $origH, $type) = $info; // Ensure AVIF constant if (!defined('IMAGETYPE_AVIF')) define('IMAGETYPE_AVIF', 19); // Convert and save original as JPEG if (class_exists('Imagick')) { try { $imOrig = new Imagick($tempFile); $imOrig->setImageFormat('jpeg'); $imOrig->writeImage($origPath); $imOrig->destroy(); } catch (Exception $e) { // fallback to GD } } // Determine base filename $path = parse_url($url, PHP_URL_PATH); $urlBase = pathinfo($path, PATHINFO_FILENAME); $base = $customName ? preg_replace('/[^a-zA-Z0-9_-]/', '', $customName) : $urlBase; $mimeExtMap = [ IMAGETYPE_JPEG=>'jpg',IMAGETYPE_PNG=>'png',IMAGETYPE_GIF=>'gif', IMAGETYPE_WEBP=>'webp',IMAGETYPE_BMP=>'bmp', IMAGETYPE_AVIF => 'avif' ]; $origExt = $mimeExtMap[$type] ?? 'img'; $origFilename = "$base.$origExt"; // Save original in client folder copy($tempFile, "$originalFolder/$origFilename"); // Prepare thumbnail filename (JPEG) $thumbFilename = $base . "_{$thumbWidth}x{$thumbHeight}.jpg"; $thumbPath = "$thumbnailFolder/$thumbFilename"; // JSON error helper function errorJson($msg, $code=500) { http_response_code($code); exit(json_encode(['error'=>$msg])); } // Generate thumbnail (Imagick if available) if (class_exists('Imagick')) { try { $im = new Imagick($tempFile); $im->thumbnailImage($thumbWidth, $thumbHeight, true); $w = $im->getImageWidth(); $h = $im->getImageHeight(); $canvas = new Imagick(); $canvas->newImage($thumbWidth, $thumbHeight, new ImagickPixel('white')); $canvas->compositeImage($im, Imagick::COMPOSITE_OVER, ($thumbWidth-$w)/2, ($thumbHeight-$h)/2); $canvas->setImageFormat('jpeg'); $canvas->writeImage($thumbPath); $im->destroy(); $canvas->destroy(); unlink($tempFile); header('Content-Type:application/json'); exit(json_encode(['original'=>"$origBaseURL/$origFilename", 'thumbnail'=>"$thumbBaseURL/$thumbFilename"])); } catch (Exception $e) { unlink($tempFile); errorJson('Imagick error: '.$e->getMessage()); } } // Fallback to GD switch ($type) { case IMAGETYPE_JPEG: $src=imagecreatefromjpeg($tempFile); break; case IMAGETYPE_PNG: $src=imagecreatefrompng($tempFile); break; case IMAGETYPE_GIF: $src=imagecreatefromgif($tempFile); break; case IMAGETYPE_WEBP: if (!function_exists('imagecreatefromwebp')) errorJson('WebP not supported'); $src=imagecreatefromwebp($tempFile); break; case IMAGETYPE_BMP: if (!function_exists('imagecreatefrombmp')) errorJson('BMP not supported'); $src=imagecreatefrombmp($tempFile); break; case IMAGETYPE_AVIF: if (!function_exists('imagecreatefromavif')) errorJson('AVIF not supported'); $src=imagecreatefromavif($tempFile); break; default: unlink($tempFile); errorJson('Unsupported image format', 415); } // Create white canvas and prepare thumbnail // Initialize a white background canvas of size {$thumbWidth}×{$thumbHeight} $thumb = imagecreatetruecolor($thumbWidth, $thumbHeight); $whiteColor = imagecolorallocate($thumb, 255, 255, 255); imagefilledrectangle( $thumb, // destination image 0, 0, // top-left corner $thumbWidth, // width $thumbHeight,// height $whiteColor // fill color ); // Calculate aspect ratio to fit the original image into the square canvas $ratio = min($thumbWidth / $origW, $thumbHeight / $origH); $newW = (int) ($origW * $ratio); $newH = (int) ($origH * $ratio); // Calculate centering offsets $offsetX = (int) (($thumbWidth - $newW) / 2); $offsetY = (int) (($thumbHeight - $newH) / 2); // Copy and resize original onto the canvas imagecopyresampled( $thumb, // destination $src, // source $offsetX, // dest x $offsetY, // dest y 0, // src x 0, // src y $newW, // dest width $newH, // dest height $origW, // src width $origH // src height ); // Save JPEG thumbnail imagejpeg($thumb,$thumbPath,90); // Cleanup imagedestroy($src); imagedestroy($thumb); unlink($tempFile); // Return JSON header('Content-Type:application/json'); echo json_encode([ 'original'=>"$origBaseURL/$origFilename", 'thumbnail'=>"$thumbBaseURL/$thumbFilename" ]); ?>
- <?php
- /**
- * PHP Thumbnail Generator with Dynamic Auth, Extended Format Support,
- * URL Parameter, Custom Output Name, JSON Response, Original Image Storage,
- * and Client-based Subfolders
- *
- * Accepts via GET:
- * - 'url' (image URL, required)
- * - 'name' (custom base filename, optional)
- * - 'auth' (API key, required; must match md5(internal_auth_key_1 . internal_auth_key_2 . the_client_name . date('d-M-Y')))
- * - 'client' (client identifier, required; lowercase, alphanumeric, hyphens/underscores)
- *
- * If 'auth' is invalid, returns HTTP 403 with JSON {"error":"invalid key"}.
- * If 'client' is invalid or missing, returns HTTP 400 with JSON {"error":"invalid client"}.
- * Otherwise, downloads the image, stores the original in '_origs/{client}',
- * creates a 75x75 thumbnail in '_thmbs/{client}', and returns JSON with both URLs.
- * Supports JPEG, PNG, GIF, WebP, BMP (GD), HEIC/others (Imagick).
- *
- * Changelog:
- * v1.0.0 2025-05-28: - Initial Release
- * - Basic script to download an image from a hard-coded URL.
- * - Resizes to 75x75 pixels using GD.
- * - Saves thumbnail in a Thumbnails directory.
- *
- * v1.1.0 2025-05-28: - Added Format Support
- * - Added support for PNG and GIF formats (with transparency preservation).
- * - Detects image type via getimagesize() and calls the appropriate GD loader.
- *
- * v1.2.0 2025-05-28: - WebP Support
- * - Added detection and handling for WebP images.
- * - Uses imagecreatefromwebp()/imagewebp() when available.
- * - Error message if WebP support is missing in PHP.
- *
- * v1.3.0 2025-05-28: - BMP and HEIC Support
- * - Added BMP support via imagecreatefrombmp()/imagebmp().
- * - Added HEIC (and other formats) support via Imagick fallback.
- * - HEIC handled by Imagick::cropThumbnailImage() when GD cannot process.
- *
- * v1.4.0 2025-05-28: - Basic Authentication
- * - Added private key basic authentication including date.
- * - Returns a JSON payload with both original and thumbnail URLs.
- *
- * v1.5.0 2025-05-28: - Custom Folders and Client Subfolders
- * - Changed destination folder from Thumbnails to _thmbs and originals to _origs.
- * - Automatically creates per-client subdirectories when &client= is passed.
- * - Included client name in authentication
- *
- * v1.6.0 2025-05-29: - JSON Response & Original URL
- * - Ensures originals are saved with their original file extensions.
- *
- * v1.7.0 2025-05-29: - Filename Suffix Change
- * - Changed thumbnail suffix from _thumb to _75x75 to reflect dimensions.
- *
- * v1.8.0 2025-05-29: - Aspect Ratio & Background Fill
- * - Retains source aspect ratio when resizing.
- * - Outputs a square 75x75 thumbnail by centering on a white canvas.
- * - Implements both GD and Imagick branches for background fill.
- *
- * v1.9.0 2025-05-29: - Force JPEG Output for Thumbnails
- * - Force thumbnail to always be JPG (compression at 90)
- *
- * v1.10.0 2025-06-18: - AVIF compatibility
- * - Handle AVIF image format
- * - Upload and convert original to JPG format
- * */
- header('Content-Type: application/json');
- // Validate client parameter
- $clientRaw = filter_input(INPUT_GET, 'client', FILTER_SANITIZE_STRING);
- $client = preg_match('/^[a-z0-9_-]+$/', $clientRaw) ? $clientRaw : '';
- if (!$client) {
- header('HTTP/1.1 400 Bad Request');
- echo json_encode(['error' => 'invalid client']);
- exit;
- }
- // Validate API auth parameter (change the $auth1 and $auth2 values and match these to the requesting script)
- $auth = filter_input(INPUT_GET, 'auth', FILTER_SANITIZE_STRING);
- $auth1 = "just_some_random_key_about_64_characters_long_of_alphanumeric_and_symbols";
- $auth2 = "another_random_key_about_64_characters_long_of_alphanumeric_and_symbols";
- $today = date('d-M-Y');
- $expected = md5($auth1 . $auth2 . $client . $today);
- if ($auth !== $expected) {
- header('HTTP/1.1 403 Forbidden');
- echo json_encode(['error' => 'invalid key']);
- exit;
- }
- // *********** NO NEED TO CHANGE ANYTHING BELOW THIS COMMENT LINE ***********
- // Fetch and validate URL
- $url = filter_input(INPUT_GET, 'url', FILTER_VALIDATE_URL);
- if (!$url) {
- header('HTTP/1.1 400 Bad Request');
- echo json_encode(['error' => "Provide a valid 'url' parameter."]);
- exit;
- }
- // Determine base filename
- $customName = filter_input(INPUT_GET, 'name', FILTER_SANITIZE_STRING);
- if ($customName) {
- $baseName = preg_replace('/[^A-Za-z0-9_-]/', '', $customName);
- if ($baseName === '') {
- header('HTTP/1.1 400 Bad Request');
- echo json_encode(['error' => "'name' invalid--use letters, numbers, hyphens, underscores."]);
- exit;
- }
- } else {
- $baseName = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_FILENAME);
- }
- // Settings: client-specific directories
- $originalFolder = __DIR__ . '/_origs/' . $client;
- $thumbnailFolder = __DIR__ . '/_thmbs/' . $client;
- $thumbWidth = 75;
- $thumbHeight = 75;
- // Ensure directories exist (creates parent paths automatically)
- foreach ([$originalFolder, $thumbnailFolder] as $dir) {
- if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
- header('HTTP/1.1 500 Internal Server Error');
- exit(json_encode(['error' => "Failed to create directory: $dir"]));
- }
- }
- // Base URL paths
- $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS']!=='off') ? 'https' : 'http';
- $host = $_SERVER['HTTP_HOST'];
- $scriptDir = rtrim(dirname($_SERVER['PHP_SELF']), '/');
- $origBaseURL = "$protocol://$host$scriptDir/_origs/$client";
- $thumbBaseURL = "$protocol://$host$scriptDir/_thmbs/$client";
- // Download source to temp file
- $tempFile = tempnam(sys_get_temp_dir(), 'img_');
- $imgData = @file_get_contents($url);
- if (!$imgData || file_put_contents($tempFile, $imgData) === false) {
- header('HTTP/1.1 502 Bad Gateway');
- exit(json_encode(['error' => 'Failed to download image.']));
- }
- // Validate image
- $info = @getimagesize($tempFile);
- if (!$info) {
- unlink($tempFile);
- header('HTTP/1.1 415 Unsupported Media Type');
- exit(json_encode(['error' => 'Downloaded file is not a valid image.']));
- }
- list($origW, $origH, $type) = $info;
- // Ensure AVIF constant
- if (!defined('IMAGETYPE_AVIF')) define('IMAGETYPE_AVIF', 19);
- // Convert and save original as JPEG
- if (class_exists('Imagick')) {
- try {
- $imOrig = new Imagick($tempFile);
- $imOrig->setImageFormat('jpeg');
- $imOrig->writeImage($origPath);
- $imOrig->destroy();
- } catch (Exception $e) {
- // fallback to GD
- }
- }
- // Determine base filename
- $path = parse_url($url, PHP_URL_PATH);
- $urlBase = pathinfo($path, PATHINFO_FILENAME);
- $base = $customName
- ? preg_replace('/[^a-zA-Z0-9_-]/', '', $customName)
- : $urlBase;
- $mimeExtMap = [
- IMAGETYPE_JPEG=>'jpg',IMAGETYPE_PNG=>'png',IMAGETYPE_GIF=>'gif',
- IMAGETYPE_WEBP=>'webp',IMAGETYPE_BMP=>'bmp', IMAGETYPE_AVIF => 'avif'
- ];
- $origExt = $mimeExtMap[$type] ?? 'img';
- $origFilename = "$base.$origExt";
- // Save original in client folder
- copy($tempFile, "$originalFolder/$origFilename");
- // Prepare thumbnail filename (JPEG)
- $thumbFilename = $base . "_{$thumbWidth}x{$thumbHeight}.jpg";
- $thumbPath = "$thumbnailFolder/$thumbFilename";
- // JSON error helper
- function errorJson($msg, $code=500) {
- http_response_code($code);
- exit(json_encode(['error'=>$msg]));
- }
- // Generate thumbnail (Imagick if available)
- if (class_exists('Imagick')) {
- try {
- $im = new Imagick($tempFile);
- $im->thumbnailImage($thumbWidth, $thumbHeight, true);
- $w = $im->getImageWidth();
- $h = $im->getImageHeight();
- $canvas = new Imagick();
- $canvas->newImage($thumbWidth, $thumbHeight, new ImagickPixel('white'));
- $canvas->compositeImage($im, Imagick::COMPOSITE_OVER, ($thumbWidth-$w)/2, ($thumbHeight-$h)/2);
- $canvas->setImageFormat('jpeg');
- $canvas->writeImage($thumbPath);
- $im->destroy(); $canvas->destroy(); unlink($tempFile);
- header('Content-Type:application/json');
- exit(json_encode(['original'=>"$origBaseURL/$origFilename", 'thumbnail'=>"$thumbBaseURL/$thumbFilename"]));
- } catch (Exception $e) {
- unlink($tempFile);
- errorJson('Imagick error: '.$e->getMessage());
- }
- }
- // Fallback to GD
- switch ($type) {
- case IMAGETYPE_JPEG: $src=imagecreatefromjpeg($tempFile); break;
- case IMAGETYPE_PNG: $src=imagecreatefrompng($tempFile); break;
- case IMAGETYPE_GIF: $src=imagecreatefromgif($tempFile); break;
- case IMAGETYPE_WEBP:
- if (!function_exists('imagecreatefromwebp')) errorJson('WebP not supported');
- $src=imagecreatefromwebp($tempFile); break;
- case IMAGETYPE_BMP:
- if (!function_exists('imagecreatefrombmp')) errorJson('BMP not supported');
- $src=imagecreatefrombmp($tempFile); break;
- case IMAGETYPE_AVIF:
- if (!function_exists('imagecreatefromavif')) errorJson('AVIF not supported');
- $src=imagecreatefromavif($tempFile); break;
- default:
- unlink($tempFile);
- errorJson('Unsupported image format', 415);
- }
- // Create white canvas and prepare thumbnail
- // Initialize a white background canvas of size {$thumbWidth}Ã--{$thumbHeight}
- $thumb = imagecreatetruecolor($thumbWidth, $thumbHeight);
- $whiteColor = imagecolorallocate($thumb, 255, 255, 255);
- imagefilledrectangle(
- $thumb,  // destination image
- 0, 0,  // top-left corner
- $thumbWidth, // width
- $thumbHeight,// height
- $whiteColor  // fill color
- );
- // Calculate aspect ratio to fit the original image into the square canvas
- $ratio = min($thumbWidth / $origW, $thumbHeight / $origH);
- $newW = (int) ($origW * $ratio);
- $newH = (int) ($origH * $ratio);
- // Calculate centering offsets
- $offsetX = (int) (($thumbWidth - $newW) / 2);
- $offsetY = (int) (($thumbHeight - $newH) / 2);
- // Copy and resize original onto the canvas
- imagecopyresampled(
- $thumb,  // destination
- $src,  // source
- $offsetX,  // dest x
- $offsetY,  // dest y
- 0,  // src x
- 0,  // src y
- $newW,  // dest width
- $newH,  // dest height
- $origW,  // src width
- $origH  // src height
- );
- // Save JPEG thumbnail
- imagejpeg($thumb,$thumbPath,90);
- // Cleanup
- imagedestroy($src);
- imagedestroy($thumb);
- unlink($tempFile);
- // return JSON
- header('Content-Type:application/json');
- echo json_encode([
- 'original'=>"$origBaseURL/$origFilename",
- 'thumbnail'=>"$thumbBaseURL/$thumbFilename"
- ]);
- ?>
Usage: Zoho Deluge
Awesome right?!? OpenAI ChatGPT made this for me in under an hour. I haven't properly tested whether it can manage *.webp and *.heic files but if I don't update this article after my further testing today then all good. I now needed to use a Deluge script to loop through all the products (in this case a "Models" table), download the product image, generate a thumbnail, and upload the thumbnail to the same record but to a field called "Thumbnail_Photo". Unfortunately, ChatGPT couldn't generate the ZohoDeluge script for me, so this is my script to do this. You will need to modify it as per your requirements. Here are 2 code snippets: the first is a function which will make a request to my online 'Thumbnailer' PHP script and the second is a function which will loop through the products table to generate and upload thumbnails:
copyraw
string API.fn_GenerateThumbnail(string p_ImageURL, string p_OutputName) { /* ******************************************************************************* Function: string API.fn_GenerateThumbnail(string p_ImageURL, string p_OutputName) Label: fn_GenerateThumbnail Trigger: used primarily in workflow when product image is uploaded to generate a thumbnail Purpose: Given an image on a public URL, converts the image to a thumbnail Inputs: string p_ImageURL, string p_OutputName Outputs: JSON of uploaded image URL and thumbnail URL Date Created: 2025-05-28 (Joel Lipman) - Initial release Date Modified: ??? - ??? More Information: - Accepts via GET: - - 'url' (image URL, required) - - 'name' (custom base filename, optional) - - 'auth' (API key, required; must match md5(internal_auth_key_1 . internal_auth_key_2 . the_client_name . date('d-M-Y'))) - - 'client' (client identifier, required; lowercase, alphanumeric, hyphens/underscores) - I use any online tool that generates random strings of 64 characters with symbols and numbers and enter these as the 2 keys used for authentication. Make sure these match the ones within the PHP script or build an authentication process. I don't have to go over the top with this one as it's a closed API used only by myself and colleagues so client name in the authentication with an MD5 hash will make this unique per client. ******************************************************************************* */ // // TEST IMAGE URL: https://picsum.photos/400 v_Client = "lowercase_url_safe_string_of_the_client_name"; v_Key1 = "just_some_random_key_about_64_characters_long_of_alphanumeric_and_symbols"; v_Key2 = "another_random_key_about_64_characters_long_of_alphanumeric_and_symbols"; v_Key = zoho.encryption.md5(v_Key1 + v_Key2 + v_Client + zoho.currentdate.toString("dd-MMM-yyyy")); // m_Params = Map(); m_Params.put("url",p_ImageURL); m_Params.put("name",p_OutputName); m_Params.put("auth",v_Key); m_Params.put("client",v_Client); // v_ThumbnailURL = invokeurl [ url :"<URL_to_the_PHP_script_made_above>.php" type :GET parameters:m_Params ]; return v_ThumbnailURL; }
- string API.fn_GenerateThumbnail(string p_ImageURL, string p_OutputName)
- {
- /* *******************************************************************************
- Function: string API.fn_GenerateThumbnail(string p_ImageURL, string p_OutputName)
- Label: fn_GenerateThumbnail
- Trigger: used primarily in workflow when product image is uploaded to generate a thumbnail
- Purpose: Given an image on a public URL, converts the image to a thumbnail
- Inputs: string p_ImageURL, string p_OutputName
- Outputs: JSON of uploaded image URL and thumbnail URL
- Date Created: 2025-05-28 (Joel Lipman)
- - Initial release
- Date Modified: ???
- - ???
- More Information:
- - Accepts via GET:
- - - 'url' (image URL, required)
- - - 'name' (custom base filename, optional)
- - - 'auth' (API key, required; must match md5(internal_auth_key_1 . internal_auth_key_2 . the_client_name . date('d-M-Y')))
- - - 'client' (client identifier, required; lowercase, alphanumeric, hyphens/underscores)
- - I use any online tool that generates random strings of 64 characters with symbols
- and numbers and enter these as the 2 keys used for authentication. Make sure these
- match the ones within the PHP script or build an authentication process. I don't
- have to go over the top with this one as it's a closed API used only by myself and
- colleagues so client name in the authentication with an MD5 hash will make this
- unique per client.
- ******************************************************************************* */
- //
- // TEST IMAGE url: https://picsum.photos/400
- v_Client = "lowercase_url_safe_string_of_the_client_name";
- v_Key1 = "just_some_random_key_about_64_characters_long_of_alphanumeric_and_symbols";
- v_Key2 = "another_random_key_about_64_characters_long_of_alphanumeric_and_symbols";
- v_Key = zoho.encryption.md5(v_Key1 + v_Key2 + v_Client + zoho.currentdate.toString("dd-MMM-yyyy"));
- //
- m_Params = Map();
- m_Params.put("url",p_ImageURL);
- m_Params.put("name",p_OutputName);
- m_Params.put("auth",v_Key);
- m_Params.put("client",v_Client);
- //
- v_ThumbnailURL = invokeUrl
- [
- url :"<URL_to_the_PHP_script_made_above>.php"
- type :GET
- parameters:m_Params
- ];
- return v_ThumbnailURL;
- }
Now I can reuse this function whenever I want a JSON returned containing the URL of the thumbnail:
copyraw
void API.fn_GenerateModelThumbnails() { /* ******************************************************************************* Function: void API.fn_GenerateModelThumbnails() Label: fn_GenerateModelThumbnails Trigger: standalone / on-demand Purpose: Generate a thumbnail photo for all model/product records Inputs: NONE Outputs: Uploads thumbnail image to each record Date Created: 2025-05-28 (Joel Lipman) - Initial release Date Modified: 2025-06-05 (Joel Lipman) - Downloads and uploads the image not as a link URL to the thumbnailer but image stored in ZohoCreator Date Modified: 2025-06-11 (Joel Lipman) - Tweaks following production release ******************************************************************************* */ // init v_CountTotal = 0; v_CountSuccessful = 0; // l_Pages = {1,2}; v_PerPage = 1; for each v_Page in l_Pages { // // calculate pagination and index ranges v_StartIndex = (v_Page-1) * v_PerPage; v_EndIndex = v_StartIndex + v_PerPage - 1; info "Page #" + v_Page + ": from " + v_StartIndex + " to " + v_EndIndex; // // select models/product records to loop through (modified ascending for the first pass, then modified descending going forwards) l_Models = Models[Photo != null && Thumbnail_Photo == null] sort by Modified_Time asc range from v_StartIndex to v_EndIndex; for each c_Model in l_Models { // // increment counter v_CountTotal = v_CountTotal + 1; // // build up filename (unique?) l_ItemNameParts = List(); if(!isBlank(c_Model.Model)) { l_ItemNameParts.add(c_Model.Model); } if(!isBlank(c_Model.Brand)) { l_ItemNameParts.add(c_Model.Brand); } if(!isBlank(c_Model.Club_Type)) { l_ItemNameParts.add(c_Model.Club_Type); } v_ItemName = l_ItemNameParts.toString("-"); info v_ItemName; // // this will be the filename of the image v_ItemNameSafe = v_ItemName.toLowerCase().replaceAll("[^a-z0-9]","-",false); // // removing consecutive hyphens/dashes v_ItemNameSafe = v_ItemNameSafe.replaceAll("--","-",true); v_ItemNameSafe = v_ItemNameSafe.replaceAll("--","-",true); // // check that there is an image file to begin with (field: Photo) v_ImgFileField = ifnull(c_Model.Photo,""); if(v_ImgFileField.contains("\"")) { // // parse out the file name v_ImageSrc = v_ImgFileField.getSuffix("\""); v_ImageSrc = v_ImageSrc.getPrefix("\""); l_ImageSrcParts = v_ImageSrc.toList("/"); v_ImageFilename = l_ImageSrcParts.get(l_ImageSrcParts.size() - 1); info v_ImageFilename; // // parse out the extension from the filename l_ImageFilenameParts = v_ImageFilename.toList("."); v_ImageExtension = l_ImageFilenameParts.get(l_ImageFilenameParts.size() - 1); info v_ImageExtension; // // now get public image url of this Photo field v_PublishKey = "<your_own_key_when_you_publish_the_report>"; v_ModelPhotoURL = "https://creatorexport.zoho.com/file" + zoho.appuri + "All_Models_Public/" + c_Model.ID + "/Photo/image-download/" + v_PublishKey + "?filepath=" + v_ImageFilename; // // go go thumbnailer try { r_NewThumbnail = thisapp.API.fn_GenerateThumbnail(v_ModelPhotoURL,v_ItemNameSafe); } catch (e) { r_NewThumbnail = Map(); info "Thumbnailer Service not available"; // // don't want to keep hammering the service if it's down return; } // // convert JSON to a Zoho Map Variable m_NewThumbnail = r_NewThumbnail.toMap(); // // check that thumbnail and not error was received if(!isNull(m_NewThumbnail.get("thumbnail"))) { // // output to console just for debugging v_NewThumbnailUrl = r_NewThumbnail.toMap().get("thumbnail"); info v_NewThumbnailUrl; // // upload to Zoho Creator image field on form (this only adds link to image field) //v_ImageUpload = "<img src='" + v_NewThumbnailUrl + "' />"; //c_Model.Thumbnail_Photo=v_ImageUpload; // // download actual file to use as file object f_Download = invokeUrl [ url: v_NewThumbnailUrl type: GET ]; // // IMPORTANT! set param name to file f_Download.setParamName("file"); // // upload actual file to Zoho Creator image field (now hosted by ZohoCreator) v_Endpoint = "https://www.zohoapis.com/creator/v2.1/data"+zoho.appuri+"report/All_Models/"+c_Model.ID+"/Thumbnail_Photo/upload?skip_workflow=[\"all\"]"; r_Upload = invokeUrl [ url: v_Endpoint type: POST connection: "zcreator" files: f_Download ]; m_Data = ifnull(r_Upload.get("data"), Map()); v_Message = ifnull(m_Data.get("message"),"ERROR"); // // increment successful if(v_Message.containsIgnoreCase("File Uploaded Successfully")) { v_CountSuccessful = v_CountSuccessful + 1; } } else { info "ERROR: Could not generate thumbnail."; } info "------------------------------"; } } } info "Successfully processed " + v_CountSuccessful + " of " + v_CountTotal; }
- void API.fn_GenerateModelThumbnails()
- {
- /* *******************************************************************************
- Function: void API.fn_GenerateModelThumbnails()
- Label: fn_GenerateModelThumbnails
- Trigger: standalone / on-demand
- Purpose: Generate a thumbnail photo for all model/product records
- Inputs: NONE
- Outputs: Uploads thumbnail image to each record
- Date Created: 2025-05-28 (Joel Lipman)
- - Initial release
- Date Modified: 2025-06-05 (Joel Lipman)
- - Downloads and uploads the image not as a link URL to the thumbnailer but image stored in ZohoCreator
- Date Modified: 2025-06-11 (Joel Lipman)
- - Tweaks following production release
- ******************************************************************************* */
- // init
- v_CountTotal = 0;
- v_CountSuccessful = 0;
- //
- l_Pages = {1,2};
- v_PerPage = 1;
- for each v_Page in l_Pages
- {
- //
- // calculate pagination and index ranges
- v_StartIndex = (v_Page-1) * v_PerPage;
- v_EndIndex = v_StartIndex + v_PerPage - 1;
- info "Page #" + v_Page + ": from " + v_StartIndex + " to " + v_EndIndex;
- //
- // select models/product records to loop through (modified ascending for the first pass, then modified descending going forwards)
- l_Models = Models[Photo != null && Thumbnail_Photo == null] sort by Modified_Time asc range from v_StartIndex to v_EndIndex;
- for each c_Model in l_Models
- {
- //
- // increment counter
- v_CountTotal = v_CountTotal + 1;
- //
- // build up filename (unique?)
- l_ItemNameParts = List();
- if(!isBlank(c_Model.Model))
- {
- l_ItemNameParts.add(c_Model.Model);
- }
- if(!isBlank(c_Model.Brand))
- {
- l_ItemNameParts.add(c_Model.Brand);
- }
- if(!isBlank(c_Model.Club_Type))
- {
- l_ItemNameParts.add(c_Model.Club_Type);
- }
- v_ItemName = l_ItemNameParts.toString("-");
- info v_ItemName;
- //
- // this will be the filename of the image
- v_ItemNameSafe = v_ItemName.toLowerCase().replaceAll("[^a-z0-9]","-",false);
- //
- // removing consecutive hyphens/dashes
- v_ItemNameSafe = v_ItemNameSafe.replaceAll("--","-",true);
- v_ItemNameSafe = v_ItemNameSafe.replaceAll("--","-",true);
- //
- // check that there is an image file to begin with (field: Photo)
- v_ImgFileField = ifnull(c_Model.Photo,"");
- if(v_ImgFileField.contains("\""))
- {
- //
- // parse out the file name
- v_ImageSrc = v_ImgFileField.getSuffix("\"");
- v_ImageSrc = v_ImageSrc.getPrefix("\"");
- l_ImageSrcParts = v_ImageSrc.toList("/");
- v_ImageFilename = l_ImageSrcParts.get(l_ImageSrcParts.size() - 1);
- info v_ImageFilename;
- //
- // parse out the extension from the filename
- l_ImageFilenameParts = v_ImageFilename.toList(".");
- v_ImageExtension = l_ImageFilenameParts.get(l_ImageFilenameParts.size() - 1);
- info v_ImageExtension;
- //
- // now get public image url of this Photo field
- v_PublishKey = "<your_own_key_when_you_publish_the_report>";
- v_ModelPhotoURL = "https://creatorexport.zoho.com/file" + zoho.appuri + "All_Models_Public/" + c_Model.ID + "/Photo/image-download/" + v_PublishKey + "?filepath=" + v_ImageFilename;
- //
- // go go thumbnailer
- try
- {
- r_NewThumbnail = thisapp.API.fn_GenerateThumbnail(v_ModelPhotoURL,v_ItemNameSafe);
- }
- catch (e)
- {
- r_NewThumbnail = Map();
- info "Thumbnailer Service not available";
- //
- // don't want to keep hammering the service if it's down
- return;
- }
- //
- // convert JSON to a Zoho Map Variable
- m_NewThumbnail = r_NewThumbnail.toMap();
- //
- // check that thumbnail and not error was received
- if(!isNull(m_NewThumbnail.get("thumbnail")))
- {
- //
- // output to console just for debugging
- v_NewThumbnailUrl = r_NewThumbnail.toMap().get("thumbnail");
- info v_NewThumbnailUrl;
- //
- // upload to Zoho Creator image field on form (this only adds link to image field)
- //v_ImageUpload = "<img src='" + v_NewThumbnailUrl + "' />";
- //c_Model.Thumbnail_Photo=v_ImageUpload;
- //
- // download actual file to use as file object
- f_Download = invokeUrl
- [
- url: v_NewThumbnailUrl
- type: GET
- ];
- //
- // IMPORTANT! set param name to file
- f_Download.setParamName("file");
- //
- // upload actual file to Zoho Creator image field (now hosted by ZohoCreator)
- v_Endpoint = "https://www.zohoapis.com/creator/v2.1/data"+zoho.appuri+"report/All_Models/"+c_Model.ID+"/Thumbnail_Photo/upload?skip_workflow=[\"all\"]";
- r_Upload = invokeUrl
- [
- url: v_Endpoint
- type: POST
- connection: "zcreator"
- files: f_Download
- ];
- m_Data = ifnull(r_Upload.get("data"), Map());
- v_Message = ifnull(m_Data.get("message"),"ERROR");
- //
- // increment successful
- if(v_Message.containsIgnoreCase("File Uploaded Successfully"))
- {
- v_CountSuccessful = v_CountSuccessful + 1;
- }
- }
- else
- {
- info "ERROR: Could not generate thumbnail.";
- }
- info "------------------------------";
- }
- }
- }
- info "Successfully processed " + v_CountSuccessful + " of " + v_CountTotal;
- }
Source(s):
- OpenAI ChatGPT for the PHP script.
Category: Zoho :: Article: 905
Add comment