Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic image creation/Apache headers using Imagick

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.

like image 737
fred2 Avatar asked Jul 23 '13 17:07

fred2


2 Answers

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 ?

like image 155
exussum Avatar answered Oct 04 '22 12:10

exussum


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.

like image 31
Travis Hegner Avatar answered Oct 04 '22 14:10

Travis Hegner