1

This app is written by me, I've tested it on my local machine, and the image generated by my app is great without bug on my local development server machine. My app is an API, the user use it to generate tiled image. It mostly uses PHP Image GD library.

The issue: Generated image has one-pixel-width right and bottom black borders, but it only happen on production server, it doesn't on my local server. The border is only generated when the image is a transparent one (in my case: 'outline', 'invert', 'black' image type. Look at the code below). But sometimes the border is there mostly and sometimes it's not.

I'm very sure that there is nothing wrong with my code and my app is working flawlessly. I have tested on both environments with the same image type, same dimension, supplied with the same configuration for my app... and still, the production server generates image that has the border.

Here is a piece of code of my app to look suspiciously at:

$src = $img->filePath;
$src_outline = $img->filePathComplements['outline'];
$src_invert = $img->filePathComplements['invert'];
$src_black = $img->filePathComplements['black'];
$info_text = is_array($img->info) ? join($img->info, ', ') : (is_string($img->info) ? $img->info : '');

$w = $img->widthOriginal;
$h = $img->heightOriginal;
$x = $img->fit->x + $this->packer->getPageMarginLeft() + $this->packer->getMarginLeft() +
        $this->packer->getVerticalBorderWidth() + $this->packer->getVerticalBordefOffset();
$y = $img->fit->y + $this->packer->getPageMarginTop() + $this->packer->getMarginTop();
$info_y = $y + $h + $this->packer->getImageInfoMargin();

// Create main and complement images
$image_main = imagecreatefrompng($src);
$image_outline = imagecreatefrompng($src_outline);
$image_invert = imagecreatefrompng($src_invert);
$image_black = imagecreatefrompng($src_black);

list($w_px_original, $h_px_original) = getimagesize($src);

$image_main_resampled = Image::imageCreateTrueColorTransparent($w, $h);
$image_outline_resampled = Image::imageCreateTrueColorTransparent($w, $h);
$image_invert_resampled = Image::imageCreateTrueColorTransparent($w, $h);
$image_black_resampled = Image::imageCreateTrueColorTransparent($w, $h);

// Resample images from original dimension to DPI-based dimension
imagecopyresampled($image_main_resampled, $image_main, 0, 0, 0, 0, $w, $h, $w_px_original, $h_px_original);
imagecopyresampled($image_outline_resampled, $image_outline, 0, 0, 0, 0, $w, $h, $w_px_original, $h_px_original);
imagecopyresampled($image_invert_resampled, $image_invert, 0, 0, 0, 0, $w, $h, $w_px_original, $h_px_original);
imagecopyresampled($image_black_resampled, $image_black, 0, 0, 0, 0, $w, $h, $w_px_original, $h_px_original);

// Add image to all containers
// Parameters are: Destination image, source image, destination starting coordinates (x, y),
// source starting coordinates (x, y), source dimension (width, height).
imagecopy($container_main, $image_main_resampled, $x, $y, 0, 0, $w, $h);
imagecopy($container_outline, $image_outline_resampled, $x, $y, 0, 0, $w, $h);
imagecopy($container_invert, $image_invert_resampled, $x, $y, 0, 0, $w, $h);
imagecopy($container_black, $image_black_resampled, $x, $y, 0, 0, $w, $h);

// Add info to main and outline images
$info = Image::imageDrawTextBordered($w, $info_h, INFO_FONT_SIZE, INFO_BORDER_SIZE, $info_text);
imagecopy($container_main, $info, $x, $info_y, 0, 0, $w, $info_h);
imagecopy($container_outline, $info, $x, $info_y, 0, 0, $w, $info_h);

And Image::imageCreateTrueColorTransparent() is:

/**
 * Creates and returns image resource of a true color transparent image.
 * @param $width
 * @param $height
 * @return resource
 */
public static function imageCreateTrueColorTransparent($width, $height) {
    $im = imagecreatetruecolor($width, $height);

    imagealphablending($im, false);
    imagesavealpha($im, true);

    $transparent = imagecolorallocatealpha($im, 0, 0, 0, 127);
    imagefill($im, 0, 0, $transparent);

    return $im;
}

The example of result from my local machine (click to view in original size): The example of result from my local machine

The example of result from the production server (click to view in original size): The example of result from the production server

I've been doing some research here on Stackoverflow, and I got this two threads who said that the issue was generated by imagecopyresampled() function. Still, I'm not so sure about this, since my app is working flawlessly on my local machine. This is the list of the discussion threads:

Any help would be appreciated, please elaborate if you know what's causing this and/or you've ever experienced this. Thank you in advance.

Rizki Pratama
  • 551
  • 4
  • 23
  • This is known issue in image resampling. When image is resampling, new pixel must be calculated based on neigbour pixels on original image. Artifact mostly happen on edge because of less neighbour pixels available to calculate new pixels color. Even photoshop sometime behave like this. Workaround that you can try is to resample image bit larger for example 2pixels larger than actual dimension and crop it or resample as usual but then you remove black border by drawing color that matched background color – Zamrony P. Juhara Mar 01 '19 at 06:53
  • @ZamronyP.Juhara is this issue a general one? If yes, why it doesn't happen on my computer? Does library version or machine specs affect the resampling result especially for this case? – Rizki Pratama Mar 05 '19 at 08:29
  • It depends on input image and image resampling algorithm. – Zamrony P. Juhara Mar 05 '19 at 09:07

1 Answers1

0

This function resizes an image regardless of its format or the presence of alpha channel/transparency.

To avoid the problem due to resampling, a padded version of the original image is created so that the column of pixels farthest to the right and the row of pixels below have sufficient data to display the colors correctly.

The size of the padding is calculated independently for the two axes based on the difference in size between the original image and the resized image.

For example, an image of 256x128 pixels which must be resized to 16x16 pixels requires the following padding:

256 / 16 = 16   columns on the right
128 / 16 = 8    rows on the bottom

this is because the color of each pixel of the resized image will be calculated on a rectangle of 16x8 pixels of the original image (in the simplest case of a bilinear filtering).

Padded image

/** Resize an image resource.
 *
 * @param resource $src_image Original image resource.
 * @param int $dest_x Destination image x position.
 * @param int $dest_y  Destination image y position.
 * @param int $dest_width Destination width.
 * @param int $dest_height Destination height.
 * @param int $src_width Source width (can be less than full-width to get a subregion).
 * @param int $src_height Source height (can be less than full-height to get a subregion).
 * @return false|resource Resized image as resource, false on error.
 */
function resize_resource($src_image, $dest_x, $dest_y, $dest_width, $dest_height, $src_width, $src_height) {

    $img_width = imagesx($src_image);
    $img_height = imagesy($src_image);

    // Create a padded version of source image cloning rows/columns of pixels from last row/column
    // to ensure full coverage of right and bottom borders after rescaling.

    // Compute padding sizes.
    $pad_width = (int)ceil($img_width / $dest_width);
    $pad_height = (int)ceil($img_height / $dest_height);

    $padded = imagecreatetruecolor($img_width + $pad_width, $img_height + $pad_height);
    if ($padded === false) return false;

    imagealphablending($padded, false);

    $transparent = imagecolorallocatealpha($padded, 0, 0, 0, 127);
    imagefill($padded, 0, 0, $transparent);
    imagecopy($padded, $src_image, 0, 0, 0, 0, $img_width, $img_height);

    // Clone last column.
    for ($i = 0; $i < $pad_width; ++$i)
        imagecopy($padded, $src_image, $i + $img_width, 0, $img_width - 1, 0, 1, $img_height);

    // Clone last row.
    for ($i = 0; $i < $pad_height; ++$i)
        imagecopy($padded, $src_image, 0, $i + $img_height, 0, $img_height - 1, $img_width, 1);

    // Fill remaining padding area on bottom-right with color of bottom-right original image pixel.
    $pad_pixel = imagecolorat($padded, $img_width - 1, $img_height - 1);
    $pad_color = imagecolorallocatealpha($padded, ($pad_pixel >> 16) & 0xFF,
        ($pad_pixel >> 8) & 0xFF, $pad_pixel & 0xFF, ($pad_pixel >> 24) & 0x7F);
    imagefilledrectangle($padded, $img_width, $img_height,
        $img_width + $pad_width - 1, $img_height + $pad_height - 1, $pad_color);

    // Create new rescaled image.

    $new = imagecreatetruecolor($dest_width, $dest_height);
    if ($new === false) return false;

    imagealphablending($new, false);
    $transparent = imagecolorallocatealpha($new, 0, 0, 0, 127);
    imagefill($new, 0, 0, $transparent);

    imagecopyresampled($new, $padded, 0, 0, $dest_x, $dest_y, $dest_width, $dest_height, $src_width, $src_height);

    return $new;
}

NOTE: for a correct transparency display in the final image it is necessary to correctly use the following code just before writing it to disk:

$transparent = imagecolorallocatealpha($img, 0, 0, 0, 127);
imagecolortransparent ($img, $transparent);

in the case of indexed-color images, or the following code:

imagesavealpha($img, true);

in the case of images with alpha channel. Where $img is the resized image resource returned by the function above.

This method allows you to resample even small images without creating an offset with respect to the original image.

Marco Sacchi
  • 712
  • 6
  • 21