6

While transferring an existing, stable, website to a new server I've run into an intermittent problem with a bit of code that creates images dynamically using Imagick.

The code parses a GET query (eg example.com/image.php?ipid=750123&r=0&w=750&h=1000) and then scales and rotates an image stored on the server and serves it to the client.

ipid = id for an image stored on server
r = degrees of rotation
w = width to display
h = height to display.

The code has probably been used for at least 5 years with no problems.

On transferring to a new, much faster, server (from Debian Squeeze to Ubuntu 12.04), I encounter a problem where about 50% of the time the image does not display, and instead the server sends a 'png file' of 0 bytes. There are no PHP errors or server errors.

Depending on whether the images is sent successfully or not, different headers are sent:

Successful image headers:

Connection: Keep-Alive
Content-Type:   image/png
Date:   Tue, 23 Jul 2013 17:03:32 GMT
Keep-Alive: timeout=5, max=76
Server: Apache/2.2.22 (Ubuntu)
Transfer-Encoding:  chunked
X-Powered-By:   PHP/5.3.10-1ubuntu3.7

Failed image headers:

Connection  Keep-Alive
Content-Length  0
Content-Type    image/png
Date    Tue, 23 Jul 2013 17:03:31 GMT
Keep-Alive  timeout=5, max=78
Server  Apache/2.2.22 (Ubuntu)
X-Powered-By    PHP/5.3.10-1ubuntu3.7

Does anyone have any ideas why this is happening?

Is there a way to 'force' the png images to be sent chunked, as I wonder if that is at the root of the problem. I've tried various workarounds where I send the image size, or 'Transfer-Encoding: chunked' as a header via PHP's header() function, but did not work, and in these cases the browser states the image is corrupted.

<?php

//Class used to connect to Imagick and do image manipulation:
class Images
{
    public $image = null;

    public function loadImage($imagePath){

        $this->image = new Imagick();
        return $this->image->readImage($imagePath);
    }

    public function getImage(){

        $this->image->setImageFormat("png8");
        $this->image->setImageDepth(5);
        $this->image->setCompressionQuality(90);
        return $this->image;
    }

    //      Resize an image by given percentage.
    //      percentage must be set as float between 0.01 and 1
    public function resizeImage ($percentage = 1, $maxWidth = false, $maxHeight = false)
    {
        if(!$this->image){return false;}
        if($percentage==1 && $maxWidth==false && $maxHeight == false){return true;}

        $width = $this->image->getImageWidth();
        $height = $this->image->getImageHeight();

        $newWidth = $width;
        $newHeight = $height;

        if($maxHeight && $maxWidth){
            if($height > $maxHeight || $width > $maxWidth){

                $scale = ($height/$maxHeight > $width/$maxWidth) ? ($height/$maxHeight) : ($width/$maxWidth) ;
                $newWidth = (int) ($width / $scale);
                $newHeight = (int) ($height / $scale);
            }
        }else{

            $newWidth = $width * $percentage;
            $newHeight = $height * $percentage;
        }
        return $this->image->resizeImage($newWidth,$newHeight,Imagick::FILTER_LANCZOS,1);

    }

    public function resizeImageByWidth ($newWidth)
    {
        if ($newWidth > 3000){
            $newWidth = 3000; //Safety measure - don't allow crazy sizes to break server.
        }

        if(!$this->image){return false;}

        return $this->image->resizeImage($newWidth,0,Imagick::FILTER_LANCZOS,1);

    }

    public function rotateImage($degrees=0)
    {
        if(!$this->image){return false;}
        return $this->image->rotateImage(new ImagickPixel(), $degrees);
    }

}


//(simplified version of) procedural code that outputs the image to browser:

$img = new Images();

$imagePath = '/some/path/returned/by/DB/image.png';

if($imagePath){
    $img->loadImage($imagePath);

    $width = $img->image->getImageWidth();
    $height = $img->image->getImageHeight();

    if (!$img->resizeImageByWidth($newWidth))
    {
        die ("image_error: resizeImage() could not create image.");
    }

    if($rotation > 0){
        if (!$img->rotateImage($rotation))
        {
            die ("image_error: rotateImage() could not create image.");
        }
    }

}else{

    die("image_error: no image path specified");
}

header('Content-type:image/png');
echo $img->getImage();

exit(0);
?>

UPDATE: In case it helps identify the location of the problem:

I've created a cludgy workaround which works in all cases, as a stopgap measure. What I do is create the image, save it to disk as a temporary file. Open the file and send it to the client using passthru() and then delete the file from disk. Cumbersome, and I'd rather do it the 'tidy' way, but it suggests to me the problem is somehow associated with these two lines: header('Content-type:image/png'); echo $img->getImage(); and a failure by Apache, PHP or Imagick to handle the resource.

fred2
  • 1,015
  • 2
  • 9
  • 29
  • content-length of `0`? Chunked encoding should not be necessary. Show the actual code you're using to generate the image. The headers by themselves are useless. – Marc B Jul 23 '13 at 17:30
  • The current code: http://phpfiddle.org/main/code/mkd-tjr – fred2 Jul 23 '13 at 17:45
  • To clarify, the same image will appear one time with content-Length of 0, and the next time with Transfer Encoding: chunked. It appears to be random behaviour. – fred2 Jul 24 '13 at 01:14
  • Also ... the problem is particularly triggered by (eg) quickly hitting a button to reload an image, or to change to a different one. – fred2 Jul 24 '13 at 01:30
  • 1
    Do you perhaps have different versions of ImageMagick installed on the two servers? – James Holderness Jul 27 '13 at 17:25
  • Very likely ... the older server probably has a prehistoric version which I'd rather not roll back to unless I really have to. But in that case, could be a software bug I guess. – fred2 Jul 28 '13 at 16:15
  • 2
    Test the return of `$img->loadImage($imagePath);` maybe you're not getting the image in the first place so there is nothing to output. – Orangepill Jul 28 '13 at 17:26
  • pls, show how you specify variable $newWidth passing to $img->resizeImageByWidth($newWidth); – Electronick Aug 01 '13 at 07:55
  • @Orangepill - The image is loaded by Imagick successfully each time. I've actually created a workaround which works in all cases as a stopgap measure. What I do is create the image, save it to disk as a temporary file. Open the file and send it to the client using passthru() and then delete the file. Cumbersome, and I'd rather do it the 'tidy' way, but it suggests the problem is somehow associated with these two lines: header('Content-type:image/png'); echo $img->getImage(); – fred2 Aug 01 '13 at 14:23
  • @Electronick The width is passed to the image generation script via a $_GET variable. – fred2 Aug 01 '13 at 14:24

2 Answers2

3

I've had an issue very similar to this before and it was related to the second request having a header forward with a 301 or 302 status code. Some browsers don't follow

Are both images returning 200 or is the failed one returning a redirect ?

exussum
  • 18,275
  • 8
  • 32
  • 65
  • Hi Thanks for reply. Nope, sorry. Both images are returning 200. – fred2 Aug 01 '13 at 14:11
  • Can you send me a link to the image ? That may help – exussum Aug 01 '13 at 14:23
  • Bit difficult, as it's on a password protected site, however I'll see what I can do if I don't get any other suggestions in the next hour or so. In essence you'd only see the information in the original post I believe. – fred2 Aug 01 '13 at 14:31
  • Thats not a full header. Also have you checked your error log when it doesnt work ? – exussum Aug 01 '13 at 14:46
  • Yep - no errors thrown unfortunately. Will check out fuller headers. – fred2 Aug 01 '13 at 14:48
2

Maybe a long shot, but perhaps there is some unintended output before the echo $img->getImage() call? This would corrupt the output image. I've run into that before with a trailing new line character after the closeing ?> tag in some random include().

A quick test before scouring your code would be to use output buffering to trash anything before the image data itself is output.

<?php
    ob_start(); //call this before executing ANY other php
?>

Some time later...

<?php
    ob_clean(); //trash invalid data in the output buffer
    //set proper headers for image output and browser caching if desired
    echo $img->getImage();
    ob_end_flush(); //send the buffered image data to the browser
?>

Granted, you do mention a stable code base, but differing web server or php versions may treat that unintended white space differently.

EDIT: Another thought

Is it possible that the new server is running some kind of php output caching mechanism. Perhaps it is trying to reload the recently generated image from a cache somewhere, and that part is failing, which may be a better explanation for a content length of 0 bytes. Maybe the new server is simply missing a library... compare the output of phpinfo(); on each server.

Travis Hegner
  • 2,465
  • 1
  • 12
  • 11
  • Nope sorry, no whitespace.The caching was a good idea - I thought that might be it before remembering that the new servers don't have any caching on them yet! – fred2 Aug 03 '13 at 16:00
  • Is there any dependence on caching in the code base? Perhaps that is the root of the issue. – Travis Hegner Aug 03 '13 at 21:44