For Zoho Services only:


I'm actually part of something bigger at Ascent Business Solutions recognized as the top Zoho Premium Solutions Partner in the United Kingdom.

Ascent Business Solutions offer support for smaller technical fixes and projects for larger developments, such as migrating to a ZohoCRM.  A team rather than a one-man-band is always available to ensure seamless progress and address any concerns. You'll find our competitive support rates with flexible, no-expiration bundles at https://ascentbusiness.co.uk/zoho-services/uk-zoho-support.  For larger projects, talk to our experts and receive dedicated support from our hands-on project consultants at https://ascentbusiness.co.uk/zoho-services/zoho-crm-implementation.

The team I manage specializes in coding API integrations between Zoho and third-party finance/commerce suites such as Xero, Shopify, WooCommerce, and eBay; to name but a few.  Our passion lies in creating innovative solutions where others have fallen short as well as working with new businesses, new sectors, and new ideas.  Our success is measured by the growth and ROI we deliver for clients, such as transforming a garden shed hobby into a 250k monthly turnover operation or generating a +60% return in just three days after launch through online payments and a streamlined e-commerce solution, replacing a paper-based system.

If you're looking for a partner who can help you drive growth and success, we'd love to work with you.  You can reach out to us on 0121 392 8140 (UK) or info@ascentbusiness.co.uk.  You can also visit our website at https://ascentbusiness.co.uk.
PHP Script: Make your own Thumbnail Generator via API

PHP Script: Make your own Thumbnail Generator via API

What?
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
  1. 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.
  2. Create Storage Folders
    In the same directory, create two writable subfolders:
    • _origs/ — stores the original downloaded images
    • _thmbs/ — stores the 75×75 thumbnail images
    Ensure both folders are writable by your web-server user (e.g. www-data).
  3. 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.
  4. 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
    Requests missing or failing any parameter will be rejected with a JSON error.
  5. Example Request
    copyraw
    https://your-domain.com/api/thumbnailer.php?
     			url=https://picsum.photos/400
     			&name=test_photo
     			&client=joellipman
     			&auth=YOUR_DAILY_KEY
    1.  https://your-domain.com/api/thumbnailer.php? 
    2.               url=https://picsum.photos/400 
    3.               &name=test_photo 
    4.               &client=joellipman 
    5.               &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"
    }
    1.  { 
    2.    "original": "https://your-domain.com/api/_origs/joellipman/test_photo.jpg", 
    3.    "thumbnail": "https://your-domain.com/api/_thmbs/joellipman/test_photo_75x75.jpg" 
    4.  } 
  6. 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"
]);

?>
  1.  <?php 
  2.  /** 
  3.   * PHP Thumbnail Generator with Dynamic Auth, Extended Format Support, 
  4.   * URL Parameter, Custom Output Name, JSON Response, Original Image Storage, 
  5.   * and Client-based Subfolders 
  6.   * 
  7.   * Accepts via GET: 
  8.   * - 'url'    (image URL, required) 
  9.   * - 'name'   (custom base filename, optional) 
  10.   * - 'auth'   (API key, required; must match md5(internal_auth_key_1 . internal_auth_key_2 . the_client_name . date('d-M-Y'))) 
  11.   * - 'client' (client identifier, required; lowercase, alphanumeric, hyphens/underscores) 
  12.   * 
  13.   * If 'auth' is invalid, returns HTTP 403 with JSON {"error":"invalid key"}
  14.   * If 'client' is invalid or missing, returns HTTP 400 with JSON {"error":"invalid client"}
  15.   * Otherwise, downloads the image, stores the original in '_origs/{client}', 
  16.   * creates a 75x75 thumbnail in '_thmbs/{client}', and returns JSON with both URLs. 
  17.   * Supports JPEG, PNG, GIF, WebP, BMP (GD), HEIC/others (Imagick)
  18.   * 
  19.   * Changelog: 
  20.    * v1.0.0 2025-05-28: - Initial Release 
  21.   *  - Basic script to download an image from a hard-coded URL. 
  22.   *  - Resizes to 75x75 pixels using GD. 
  23.   *  - Saves thumbnail in a Thumbnails directory. 
  24.   * 
  25.   * v1.1.0 2025-05-28: - Added Format Support 
  26.   *  - Added support for PNG and GIF formats (with transparency preservation)
  27.   *  - Detects image type via getimagesize() and calls the appropriate GD loader. 
  28.   * 
  29.   * v1.2.0 2025-05-28: - WebP Support 
  30.   *  - Added detection and handling for WebP images. 
  31.   *  - Uses imagecreatefromwebp()/imagewebp() when available. 
  32.   *  - Error message if WebP support is missing in PHP. 
  33.   * 
  34.   * v1.3.0 2025-05-28: - BMP and HEIC Support 
  35.   *  - Added BMP support via imagecreatefrombmp()/imagebmp()
  36.   *  - Added HEIC (and other formats) support via Imagick fallback. 
  37.   *  - HEIC handled by Imagick::cropThumbnailImage() when GD cannot process. 
  38.   * 
  39.   * v1.4.0 2025-05-28: - Basic Authentication 
  40.   *  - Added private key basic authentication including date. 
  41.   *  - Returns a JSON payload with both original and thumbnail URLs. 
  42.   * 
  43.   * v1.5.0 2025-05-28: - Custom Folders and Client Subfolders 
  44.   *  - Changed destination folder from Thumbnails to _thmbs and originals to _origs. 
  45.   *  - Automatically creates per-client subdirectories when &client= is passed. 
  46.   *  - Included client name in authentication 
  47.   * 
  48.   * v1.6.0 2025-05-29: - JSON Response & Original URL 
  49.   *  - Ensures originals are saved with their original file extensions. 
  50.   * 
  51.   * v1.7.0 2025-05-29: - Filename Suffix Change 
  52.   *  - Changed thumbnail suffix from _thumb to _75x75 to reflect dimensions. 
  53.   * 
  54.   * v1.8.0 2025-05-29: - Aspect Ratio & Background Fill 
  55.   *  - Retains source aspect ratio when resizing. 
  56.   *  - Outputs a square 75x75 thumbnail by centering on a white canvas. 
  57.   *  - Implements both GD and Imagick branches for background fill. 
  58.   * 
  59.   * v1.9.0 2025-05-29: - Force JPEG Output for Thumbnails 
  60.   *  - Force thumbnail to always be JPG (compression at 90) 
  61.   * 
  62.   * v1.10.0 2025-06-18: - AVIF compatibility 
  63.   *  - Handle AVIF image format 
  64.   *  - Upload and convert original to JPG format 
  65.   *  */ 
  66.   
  67.  header('Content-Type: application/json')
  68.   
  69.  // Validate client parameter 
  70.  $clientRaw = filter_input(INPUT_GET, 'client', FILTER_SANITIZE_STRING)
  71.  $client    = preg_match('/^[a-z0-9_-]+$/', $clientRaw) ? $clientRaw : ''
  72.  if (!$client) { 
  73.      header('HTTP/1.1 400 Bad Request')
  74.      echo json_encode(['error' => 'invalid client'])
  75.      exit; 
  76.  } 
  77.   
  78.  // Validate API auth parameter (change the $auth1 and $auth2 values and match these to the requesting script) 
  79.  $auth     = filter_input(INPUT_GET, 'auth', FILTER_SANITIZE_STRING)
  80.  $auth1    = "just_some_random_key_about_64_characters_long_of_alphanumeric_and_symbols"
  81.  $auth2    = "another_random_key_about_64_characters_long_of_alphanumeric_and_symbols"
  82.  $today    = date('d-M-Y')
  83.  $expected = md5($auth1 . $auth2 . $client . $today)
  84.  if ($auth !== $expected) { 
  85.      header('HTTP/1.1 403 Forbidden')
  86.      echo json_encode(['error' => 'invalid key'])
  87.      exit; 
  88.  } 
  89.   
  90.  // *********** NO NEED TO CHANGE ANYTHING BELOW THIS COMMENT LINE *********** 
  91.   
  92.  // Fetch and validate URL 
  93.  $url    = filter_input(INPUT_GET, 'url', FILTER_VALIDATE_URL)
  94.  if (!$url) { 
  95.      header('HTTP/1.1 400 Bad Request')
  96.      echo json_encode(['error' => "Provide a valid 'url' parameter."])
  97.      exit; 
  98.  } 
  99.   
  100.  // Determine base filename 
  101.  $customName = filter_input(INPUT_GET, 'name', FILTER_SANITIZE_STRING)
  102.  if ($customName) { 
  103.      $baseName = preg_replace('/[^A-Za-z0-9_-]/', '', $customName)
  104.      if ($baseName === '') { 
  105.          header('HTTP/1.1 400 Bad Request')
  106.          echo json_encode(['error' => "'name' invalid--use letters, numbers, hyphens, underscores."])
  107.          exit; 
  108.      } 
  109.  } else { 
  110.      $baseName = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_FILENAME)
  111.  } 
  112.   
  113.  // Settings: client-specific directories 
  114.  $originalFolder  = __DIR__ . '/_origs/' . $client
  115.  $thumbnailFolder = __DIR__ . '/_thmbs/' . $client
  116.  $thumbWidth      = 75
  117.  $thumbHeight     = 75
  118.   
  119.  // Ensure directories exist (creates parent paths automatically) 
  120.  foreach ([$originalFolder, $thumbnailFolder] as $dir) { 
  121.      if (!is_dir($dir) && !mkdir($dir, 0755, true)) { 
  122.          header('HTTP/1.1 500 Internal Server Error')
  123.          exit(json_encode(['error' => "Failed to create directory: $dir"]))
  124.      } 
  125.  } 
  126.   
  127.  // Base URL paths 
  128.  $protocol     = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS']!=='off') ? 'https' : 'http'
  129.  $host         = $_SERVER['HTTP_HOST']
  130.  $scriptDir    = rtrim(dirname($_SERVER['PHP_SELF']), '/')
  131.  $origBaseURL  = "$protocol://$host$scriptDir/_origs/$client"
  132.  $thumbBaseURL = "$protocol://$host$scriptDir/_thmbs/$client"
  133.   
  134.  // Download source to temp file 
  135.  $tempFile = tempnam(sys_get_temp_dir(), 'img_')
  136.  $imgData  = @file_get_contents($url)
  137.  if (!$imgData || file_put_contents($tempFile, $imgData) === false) { 
  138.      header('HTTP/1.1 502 Bad Gateway')
  139.      exit(json_encode(['error' => 'Failed to download image.']))
  140.  } 
  141.   
  142.  // Validate image 
  143.  $info = @getimagesize($tempFile)
  144.  if (!$info) { 
  145.      unlink($tempFile)
  146.      header('HTTP/1.1 415 Unsupported Media Type')
  147.      exit(json_encode(['error' => 'Downloaded file is not a valid image.']))
  148.  } 
  149.  list($origW, $origH, $type) = $info
  150.   
  151.  // Ensure AVIF constant 
  152.  if (!defined('IMAGETYPE_AVIF')) define('IMAGETYPE_AVIF', 19)
  153.   
  154.  // Convert and save original as JPEG 
  155.  if (class_exists('Imagick')) { 
  156.      try { 
  157.          $imOrig = new Imagick($tempFile)
  158.          $imOrig->setImageFormat('jpeg')
  159.          $imOrig->writeImage($origPath)
  160.          $imOrig->destroy()
  161.      } catch (Exception $e) { 
  162.          // fallback to GD 
  163.      } 
  164.  } 
  165.   
  166.  // Determine base filename 
  167.  $path    = parse_url($url, PHP_URL_PATH)
  168.  $urlBase = pathinfo($path, PATHINFO_FILENAME)
  169.  $base    = $customName 
  170.      ? preg_replace('/[^a-zA-Z0-9_-]/', '', $customName) 
  171.      : $urlBase
  172.  $mimeExtMap = [ 
  173.      IMAGETYPE_JPEG=>'jpg',IMAGETYPE_PNG=>'png',IMAGETYPE_GIF=>'gif', 
  174.      IMAGETYPE_WEBP=>'webp',IMAGETYPE_BMP=>'bmp', IMAGETYPE_AVIF => 'avif' 
  175.  ]
  176.  $origExt      = $mimeExtMap[$type] ?? 'img'
  177.  $origFilename = "$base.$origExt"
  178.   
  179.  // Save original in client folder 
  180.  copy($tempFile, "$originalFolder/$origFilename")
  181.   
  182.  // Prepare thumbnail filename (JPEG) 
  183.  $thumbFilename = $base . "_{$thumbWidth}x{$thumbHeight}.jpg"
  184.  $thumbPath     = "$thumbnailFolder/$thumbFilename"
  185.   
  186.  // JSON error helper 
  187.  function errorJson($msg, $code=500) { 
  188.      http_response_code($code)
  189.      exit(json_encode(['error'=>$msg]))
  190.  } 
  191.   
  192.  // Generate thumbnail (Imagick if available) 
  193.  if (class_exists('Imagick')) { 
  194.      try { 
  195.          $im     = new Imagick($tempFile)
  196.          $im->thumbnailImage($thumbWidth, $thumbHeight, true)
  197.          $w      = $im->getImageWidth()
  198.          $h      = $im->getImageHeight()
  199.          $canvas = new Imagick()
  200.          $canvas->newImage($thumbWidth, $thumbHeight, new ImagickPixel('white'))
  201.          $canvas->compositeImage($im, Imagick::COMPOSITE_OVER, ($thumbWidth-$w)/2, ($thumbHeight-$h)/2)
  202.          $canvas->setImageFormat('jpeg')
  203.          $canvas->writeImage($thumbPath)
  204.          $im->destroy()$canvas->destroy()unlink($tempFile)
  205.          header('Content-Type:application/json')
  206.          exit(json_encode(['original'=>"$origBaseURL/$origFilename", 'thumbnail'=>"$thumbBaseURL/$thumbFilename"]))
  207.      } catch (Exception $e) { 
  208.          unlink($tempFile)
  209.          errorJson('Imagick error: '.$e->getMessage())
  210.      } 
  211.  } 
  212.   
  213.  // Fallback to GD 
  214.  switch ($type) { 
  215.      case IMAGETYPE_JPEG: $src=imagecreatefromjpeg($tempFile); break
  216.      case IMAGETYPE_PNG:  $src=imagecreatefrompng($tempFile) break
  217.      case IMAGETYPE_GIF:  $src=imagecreatefromgif($tempFile) break
  218.      case IMAGETYPE_WEBP: 
  219.          if (!function_exists('imagecreatefromwebp')) errorJson('WebP not supported')
  220.          $src=imagecreatefromwebp($tempFile); break
  221.      case IMAGETYPE_BMP: 
  222.          if (!function_exists('imagecreatefrombmp')) errorJson('BMP not supported')
  223.          $src=imagecreatefrombmp($tempFile); break
  224.      case IMAGETYPE_AVIF: 
  225.          if (!function_exists('imagecreatefromavif')) errorJson('AVIF not supported')
  226.          $src=imagecreatefromavif($tempFile); break
  227.      default: 
  228.          unlink($tempFile)
  229.          errorJson('Unsupported image format', 415)
  230.  } 
  231.  // Create white canvas and prepare thumbnail 
  232.   
  233.      // Initialize a white background canvas of size {$thumbWidth}Ã--{$thumbHeight} 
  234.      $thumb = imagecreatetruecolor($thumbWidth, $thumbHeight)
  235.      $whiteColor = imagecolorallocate($thumb, 255, 255, 255)
  236.      imagefilledrectangle( 
  237.          $thumb,      // destination image 
  238.          0, 0,        // top-left corner 
  239.          $thumbWidth, // width 
  240.          $thumbHeight,// height 
  241.          $whiteColor  // fill color 
  242.      )
  243.   
  244.      // Calculate aspect ratio to fit the original image into the square canvas 
  245.      $ratio    = min($thumbWidth / $origW, $thumbHeight / $origH)
  246.      $newW     = (int) ($origW * $ratio)
  247.      $newH     = (int) ($origH * $ratio)
  248.   
  249.      // Calculate centering offsets 
  250.      $offsetX  = (int) (($thumbWidth  - $newW) / 2)
  251.      $offsetY  = (int) (($thumbHeight - $newH) / 2)
  252.   
  253.      // Copy and resize original onto the canvas 
  254.      imagecopyresampled( 
  255.          $thumb,     // destination 
  256.          $src,       // source 
  257.          $offsetX,   // dest x 
  258.          $offsetY,   // dest y 
  259.          0,          // src x 
  260.          0,          // src y 
  261.          $newW,      // dest width 
  262.          $newH,      // dest height 
  263.          $origW,     // src width 
  264.          $origH      // src height 
  265.      )
  266.   
  267.  // Save JPEG thumbnail 
  268.  imagejpeg($thumb,$thumbPath,90)
  269.   
  270.  // Cleanup 
  271.  imagedestroy($src)
  272.  imagedestroy($thumb)
  273.  unlink($tempFile)
  274.   
  275.  // return JSON 
  276.  header('Content-Type:application/json')
  277.  echo json_encode([ 
  278.      'original'=>"$origBaseURL/$origFilename", 
  279.      'thumbnail'=>"$thumbBaseURL/$thumbFilename" 
  280.  ])
  281.   
  282.  ?> 

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;
}
  1.  string API.fn_GenerateThumbnail(string p_ImageURL, string p_OutputName) 
  2.  { 
  3.      /* ******************************************************************************* 
  4.      Function:       string API.fn_GenerateThumbnail(string p_ImageURL, string p_OutputName) 
  5.      Label:          fn_GenerateThumbnail 
  6.      Trigger:        used primarily in workflow when product image is uploaded to generate a thumbnail 
  7.      Purpose:        Given an image on a public URL, converts the image to a thumbnail 
  8.      Inputs:         string p_ImageURL, string p_OutputName 
  9.      Outputs:        JSON of uploaded image URL and thumbnail URL 
  10.   
  11.      Date Created:   2025-05-28 (Joel Lipman) 
  12.                      - Initial release 
  13.      Date Modified:    ??? 
  14.                      - ??? 
  15.   
  16.      More Information: 
  17.                      - Accepts via GET: 
  18.                      - - 'url'    (image URL, required) 
  19.                      - - 'name'   (custom base filename, optional) 
  20.                       - - 'auth'   (API key, required; must match md5(internal_auth_key_1 . internal_auth_key_2 . the_client_name . date('d-M-Y'))) 
  21.                      - - 'client' (client identifier, required; lowercase, alphanumeric, hyphens/underscores) 
  22.   
  23.                      - I use any online tool that generates random strings of 64 characters with symbols 
  24.                        and numbers and enter these as the 2 keys used for authentication.  Make sure these 
  25.                        match the ones within the PHP script or build an authentication process.  I don't 
  26.                        have to go over the top with this one as it's a closed API used only by myself and 
  27.                        colleagues so client name in the authentication with an MD5 hash will make this 
  28.                        unique per client. 
  29.   
  30.      ******************************************************************************* */ 
  31.      // 
  32.      // TEST IMAGE url: https://picsum.photos/400 
  33.      v_Client = "lowercase_url_safe_string_of_the_client_name"
  34.      v_Key1 = "just_some_random_key_about_64_characters_long_of_alphanumeric_and_symbols"
  35.      v_Key2 = "another_random_key_about_64_characters_long_of_alphanumeric_and_symbols"
  36.      v_Key = zoho.encryption.md5(v_Key1 + v_Key2 + v_Client + zoho.currentdate.toString("dd-MMM-yyyy"))
  37.      // 
  38.      m_Params = Map()
  39.      m_Params.put("url",p_ImageURL)
  40.      m_Params.put("name",p_OutputName)
  41.      m_Params.put("auth",v_Key)
  42.      m_Params.put("client",v_Client)
  43.      // 
  44.      v_ThumbnailURL = invokeUrl 
  45.      [ 
  46.          url :"<URL_to_the_PHP_script_made_above>.php" 
  47.          type :GET 
  48.          parameters:m_Params 
  49.      ]
  50.      return v_ThumbnailURL; 
  51.  } 

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;
}
  1.  void API.fn_GenerateModelThumbnails() 
  2.  { 
  3.      /* ******************************************************************************* 
  4.      Function:       void API.fn_GenerateModelThumbnails() 
  5.      Label:          fn_GenerateModelThumbnails 
  6.      Trigger:        standalone / on-demand 
  7.      Purpose:        Generate a thumbnail photo for all model/product records 
  8.      Inputs:         NONE 
  9.      Outputs:        Uploads thumbnail image to each record 
  10.   
  11.      Date Created:   2025-05-28 (Joel Lipman) 
  12.                      - Initial release 
  13.      Date Modified:    2025-06-05 (Joel Lipman) 
  14.                      - Downloads and uploads the image not as a link URL to the thumbnailer but image stored in ZohoCreator 
  15.      Date Modified:    2025-06-11 (Joel Lipman) 
  16.                      - Tweaks following production release 
  17.      ******************************************************************************* */ 
  18.      // init 
  19.      v_CountTotal = 0
  20.      v_CountSuccessful = 0
  21.      // 
  22.      l_Pages = {1,2}
  23.      v_PerPage = 1
  24.      for each v_Page in l_Pages 
  25.      { 
  26.          // 
  27.          // calculate pagination and index ranges 
  28.          v_StartIndex = (v_Page-1) * v_PerPage; 
  29.          v_EndIndex = v_StartIndex + v_PerPage - 1
  30.          info "Page #" + v_Page + ": from " + v_StartIndex + " to " + v_EndIndex; 
  31.          // 
  32.          // select models/product records to loop through (modified ascending for the first pass, then modified descending going forwards) 
  33.          l_Models = Models[Photo != null && Thumbnail_Photo == null] sort by Modified_Time asc range from v_StartIndex to v_EndIndex; 
  34.          for each  c_Model in l_Models 
  35.          { 
  36.              // 
  37.              // increment counter 
  38.              v_CountTotal = v_CountTotal + 1
  39.              // 
  40.              // build up filename (unique?) 
  41.              l_ItemNameParts = List()
  42.              if(!isBlank(c_Model.Model)) 
  43.              { 
  44.                  l_ItemNameParts.add(c_Model.Model)
  45.              } 
  46.              if(!isBlank(c_Model.Brand)) 
  47.              { 
  48.                  l_ItemNameParts.add(c_Model.Brand)
  49.              } 
  50.              if(!isBlank(c_Model.Club_Type)) 
  51.              { 
  52.                  l_ItemNameParts.add(c_Model.Club_Type)
  53.              } 
  54.              v_ItemName = l_ItemNameParts.toString("-")
  55.              info v_ItemName; 
  56.              // 
  57.              // this will be the filename of the image 
  58.              v_ItemNameSafe = v_ItemName.toLowerCase().replaceAll("[^a-z0-9]","-",false)
  59.              // 
  60.              // removing consecutive hyphens/dashes 
  61.              v_ItemNameSafe = v_ItemNameSafe.replaceAll("--","-",true)
  62.              v_ItemNameSafe = v_ItemNameSafe.replaceAll("--","-",true)
  63.              // 
  64.              // check that there is an image file to begin with (field: Photo) 
  65.              v_ImgFileField = ifnull(c_Model.Photo,"")
  66.              if(v_ImgFileField.contains("\"")) 
  67.              { 
  68.                  // 
  69.                  // parse out the file name 
  70.                  v_ImageSrc = v_ImgFileField.getSuffix("\"")
  71.                  v_ImageSrc = v_ImageSrc.getPrefix("\"")
  72.                  l_ImageSrcParts = v_ImageSrc.toList("/")
  73.                  v_ImageFilename = l_ImageSrcParts.get(l_ImageSrcParts.size() - 1)
  74.                  info v_ImageFilename; 
  75.                  // 
  76.                  // parse out the extension from the filename 
  77.                  l_ImageFilenameParts = v_ImageFilename.toList(".")
  78.                  v_ImageExtension = l_ImageFilenameParts.get(l_ImageFilenameParts.size() - 1)
  79.                  info v_ImageExtension; 
  80.                  // 
  81.                  // now get public image url of this Photo field 
  82.                  v_PublishKey = "<your_own_key_when_you_publish_the_report>"
  83.                  v_ModelPhotoURL = "https://creatorexport.zoho.com/file" + zoho.appuri + "All_Models_Public/" + c_Model.ID + "/Photo/image-download/" + v_PublishKey + "?filepath=" + v_ImageFilename; 
  84.                  // 
  85.                  // go go thumbnailer 
  86.                  try 
  87.                  { 
  88.                      r_NewThumbnail = thisapp.API.fn_GenerateThumbnail(v_ModelPhotoURL,v_ItemNameSafe)
  89.                  } 
  90.                  catch (e) 
  91.                  { 
  92.                      r_NewThumbnail = Map()
  93.                      info "Thumbnailer Service not available"
  94.                      // 
  95.                      // don't want to keep hammering the service if it's down 
  96.                      return; 
  97.                  } 
  98.                  // 
  99.                  // convert JSON to a Zoho Map Variable 
  100.                  m_NewThumbnail = r_NewThumbnail.toMap()
  101.                  // 
  102.                  // check that thumbnail and not error was received 
  103.                  if(!isNull(m_NewThumbnail.get("thumbnail"))) 
  104.                  { 
  105.                      // 
  106.                      // output to console just for debugging 
  107.                      v_NewThumbnailUrl = r_NewThumbnail.toMap().get("thumbnail")
  108.                      info v_NewThumbnailUrl; 
  109.                      // 
  110.                      // upload to Zoho Creator image field on form (this only adds link to image field) 
  111.                      //v_ImageUpload = "<img src='" + v_NewThumbnailUrl + "' />"; 
  112.                      //c_Model.Thumbnail_Photo=v_ImageUpload; 
  113.                      // 
  114.                      // download actual file to use as file object 
  115.                      f_Download = invokeUrl 
  116.                      [ 
  117.                          url: v_NewThumbnailUrl 
  118.                          type: GET 
  119.                      ]
  120.                      // 
  121.                      // IMPORTANT! set param name to file 
  122.                      f_Download.setParamName("file")
  123.                      // 
  124.                      // upload actual file to Zoho Creator image field (now hosted by ZohoCreator) 
  125.                      v_Endpoint = "https://www.zohoapis.com/creator/v2.1/data"+zoho.appuri+"report/All_Models/"+c_Model.ID+"/Thumbnail_Photo/upload?skip_workflow=[\"all\"]"
  126.                      r_Upload = invokeUrl 
  127.                      [ 
  128.                          url: v_Endpoint 
  129.                          type: POST 
  130.                          connection: "zcreator" 
  131.                          files: f_Download 
  132.                      ]
  133.                      m_Data = ifnull(r_Upload.get("data"), Map())
  134.                      v_Message = ifnull(m_Data.get("message"),"ERROR")
  135.                      // 
  136.                      // increment successful 
  137.                      if(v_Message.containsIgnoreCase("File Uploaded Successfully")) 
  138.                      { 
  139.                          v_CountSuccessful = v_CountSuccessful + 1
  140.                      } 
  141.                  } 
  142.                  else 
  143.                  { 
  144.                      info "ERROR: Could not generate thumbnail."
  145.                  } 
  146.                  info "------------------------------"; 
  147.              } 
  148.          } 
  149.      } 
  150.      info "Successfully processed " + v_CountSuccessful + " of " + v_CountTotal; 
  151.  } 

Source(s):
Category: Zoho :: Article: 905

Add comment

Your rating:

Submit

Credit where Credit is Due:


Feel free to copy, redistribute and share this information. All that we ask is that you attribute credit and possibly even a link back to this website as it really helps in our search engine rankings.

Disclaimer: Please note that the information provided on this website is intended for informational purposes only and does not represent a warranty. The opinions expressed are those of the author only. We recommend testing any solutions in a development environment before implementing them in production. The articles are based on our good faith efforts and were current at the time of writing, reflecting our practical experience in a commercial setting.

Thank you for visiting and, as always, we hope this website was of some use to you!

Kind Regards,

Joel Lipman
www.joellipman.com

Accreditation

Badge - Zoho Creator Certified Developer Associate
Badge - Zoho Deluge Certified Developer
Badge - Certified Zoho CRM Developer

Donate & Support

If you like my content, and would like to support this sharing site, feel free to donate using a method below:

Paypal:
Donate to Joel Lipman via PayPal

Bitcoin:
Donate to Joel Lipman with Bitcoin bc1qf6elrdxc968h0k673l2djc9wrpazhqtxw8qqp4

Ethereum:
Donate to Joel Lipman with Ethereum 0xb038962F3809b425D661EF5D22294Cf45E02FebF

Please publish modules in offcanvas position.