5

A couple of years ago I authored a PHP (ZEND) module that I still use today in some of my projects. This module was built with a fairly rudimentary (i.e. copypasta) understanding of PHP image manipulation but works beautiful except in one case.

The module pulls blob data from a table, parses it into an image, uses imagcopyresampled() to resize it, and then sends the resulting .jpg to the browser, it is called as a standard controller action.

It seems to work in all cases except when the original image was saved by a user from facebook (i.e. right click on the facebook image viewer and download to desktop and then upload to client site). I've tested this a number of times myself and been able to replicate it. I've also been able to upload the same image when re-saved via photoshop without encountering the problem.

I suspect the facebook image display adds some sort of additional metadata inside the file that causes my system to break.

Is there a solution for this?

The code for the PHP image module is as follows:

private function _buildImage($mode) {
    //Prepare the output image
    //Currently CROP and RESIZE are using different output onstruction calls
    //So $finalImage is initialized prior to entering the mode blocks.

    $finalImage = imagecreatetruecolor($this->_width, $this->_height);
    $backgroundFillColor = imagecolorallocate($finalImage, RED, BLUE, GREEN);

    imageFill($finalImage, 0, 0, $backgroundFillColor);

    $this->_table = $this->_getTable($mode);

    $image = $this->_request->image;
    $this->_imageData = $this->_table->fetchEntryAsRow($image);

    //Set top and left to 0 to capture the top/left corner of the orignal image.
    $top = 0;
    $left = 0;


    $inputImage = imagecreatefromstring(    $this->_imageData->image);
    list($inputWidth, $inputHeight) = $this->_getImageSize($this->_imageData->image);   

    //Ratio is the target ratio of $this->_width divided by $this->_height, as set in actions.
    //For index thumbnails this ratio is .7
    //For index large images this ratio is 2
    $ratio = $this->_width / $this->_height;


    //define offset width and offset height as being equal to input image width and height
    $offsetWidth = $inputWidth;
    $offsetHeight = $inputHeight;

    //Define Original Ratio as found in the image in the table.
    $inputRatio = $inputWidth / $inputHeight;

    //Rendering maths for RESIZE and CROP modes.
    //RESIZE forces the whole input image to appear within the frame of the output image.
    //CROP forces the output image to contain only the relevantly sized piece of the input image, measured from the middle.

    if($this->_mode == CROP) {
        if($inputRatio > $ratio) {
            //Original image is too wide, use $height as is. Modify $width;
            //define $scale: input is scaled to output along height.
            $scale = $inputHeight / $this->_height;
            //Calculate $left: an integer calculated based on 1/2 of the input width * half of the difference in the rations.
            $left = round(($inputWidth/2)*(($inputRatio-$ratio)/2), 0);
            $inputWidth = round(($inputWidth - ($left*2)), 0);
            $offset = $offsetWidth - $inputWidth;
        } else {
            //Original image is too high, use $width as is.  Modify $height;
            $scale = $inputWidth / $this->_width;
            $inputHeight = round(($this->_height * $scale),0);
            $offset = $offsetHeight - $inputHeight;
            $top = $offset / 2;
        }

        imagecopyresampled($finalImage, //Destination Image 
            $inputImage, //Original Image 
            0, 0, //Destination top left Coord 
            $left, $top, //Source top left coord
            $this->_width, $this->_height,  //Final location Bottom Right Coord
            $inputWidth, $inputHeight //Source bottom right coord.
        );

    } else {

        if($inputRatio < $ratio) {
            //Original image is too wide, use $height as is. Modify $width;

            $scale = $inputHeight / $this->_height;


            $calculatedWidth = round(($inputWidth / $scale), 0);
            $calculatedHeight = $this->_height;

            $offset = $this->_width - $calculatedWidth;
            $left = round(($offset / 2), 0);
            $top = 0;

        } else {
            //Original image is too high, use $width as is.  Modify $height;
            $scale = $inputWidth / $this->_width;
            $calculatedHeight = round(($inputHeight / $scale),0);
            $calculatedWidth = $this->_width;
            $offset = $this->_height - $calculatedHeight;
            $top = round(($offset / 2), 2);
        }

        imagecopyresampled($finalImage, //Destination Image 
            $inputImage, //Original Image 
            $left, $top, //Destination top left Coord 
            0, 0, //Source top left coord
            $calculatedWidth, $calculatedHeight,  //Final location Bottom Right Coord
            $inputWidth, $inputHeight //Source bottom right coord.
        );
    }



    imagejpeg($finalImage, null, 100);
    imagedestroy($inputImage);
    imagedestroy($finalImage);

}

I suspect that the problem may actually lay with the implementation of _getImageSize.

private function _getImageSize($data)
{
    $soi = unpack('nmagic/nmarker', $data);
    if ($soi['magic'] != 0xFFD8) return false;
    $marker = $soi['marker'];
    $data   = substr($data, 4);
    $done   = false;

    while(1) {
            if (strlen($data) === 0) return false;
            switch($marker) {
                    case 0xFFC0:
                            $info = unpack('nlength/Cprecision/nY/nX', $data);
                            return array($info['X'], $info['Y']);
                            break;

                    default:
                            $info   = unpack('nlength', $data);
                            $data   = substr($data, $info['length']);
                            $info   = unpack('nmarker', $data);
                            $marker = $info['marker'];
                            $data   = substr($data, 2);
                            break;
            }
     }
}

You can see another example of this problem at http://www.angelaryan.com/gallery/Image/22 which displays a blue square, rather than the image stored in the database.

Crispen Smith
  • 513
  • 4
  • 20
  • Could you provide a sample image from Facebook that fails? – DCoder Jul 28 '13 at 07:00
  • Sure! how would you like the sample provided? – Crispen Smith Jul 28 '13 at 14:31
  • You can either post a link to the original image, or upload it to some FTP/file sharing site (not a specialized image sharing service, as that would likely recompress the image). – DCoder Jul 28 '13 at 14:34
  • Try this link, I think the content should be public for this page. I've just tested it over on my one client's website using this engine and it does fail as expected. https://www.facebook.com/photo.php?fbid=10153035086235414&set=pb.250413470413.-2207520000.1375023381.&type=3&theater When saving, I've found that I have to set the extension to .jpg to get it recognized as an image file. – Crispen Smith Jul 28 '13 at 14:57
  • Right-click and saving that image produces a normal JPG file for me, which `getimagesize` has no problems with. – DCoder Jul 28 '13 at 15:14
  • What environment are you using? Users have reported this problem with Chrome and Firefox on windows machines pre windows 8. I tested the problem with that image this morning in Chrome on a windows box. – Crispen Smith Jul 28 '13 at 15:18
  • Google Chrome 28.0.1500.72 m (Incognito mode), Windows 7 Pro x64. – DCoder Jul 28 '13 at 15:19
  • Weird, same format I'm using, and yet for the image in question, here's the page I get from uploading it without a resave. http://www.cinchedtight.com/image/product/thumbnail/n/image/301 – Crispen Smith Jul 28 '13 at 15:31
  • Why are you storing images in your database? – cmorrissey Jul 28 '13 at 22:52
  • I'm not doing it on new projects, and understand why it's not the right call in general but at the time I didn't have that understanding and at this point the amount of work involved in changing the pattern isn't justified for this one corner case, assuming it can be resolved. These days I'm used cloud based CDNs for image hosting. – Crispen Smith Jul 29 '13 at 15:01
  • @CrispenSmith Have you tried automatically "re-saving" the image after the upload with `imagejpeg(imagecreatefromjpeg($filename),$filename,9)` ? – MDEV Jul 31 '13 at 16:22
  • No, I haven't tried that. Let me review it and push a version to the server. I'll come back to you with my findings. – Crispen Smith Jul 31 '13 at 17:04
  • @SmokeyPHP, worked like a charm. Post this as an answer and I'll accept. – Crispen Smith Jul 31 '13 at 17:31
  • @CrispenSmith Good to know! Have done – MDEV Jul 31 '13 at 18:06
  • Why are you using `imagecreatefromstring` but not `getimagesizefromstring` ? Why do you want to parse this yourself? Is there a bug in `getimagesizefromstring` you're trying to work around? – huysentruitw Jul 31 '13 at 18:21
  • @WouterHuysentruit, this is part of the legacy of this being a copy-pasta solution. My knowledge of php's image functions is fairly basic. It's entirely better that this would have been a better solution, but I wasn't aware of it at the time of the original development. – Crispen Smith Jul 31 '13 at 19:29

2 Answers2

3

Try automatically "re-saving" the image after the upload with

imagejpeg(imagecreatefromjpeg($filename),$filename,9);

This should re-create any malformed or unrecognised headers from the original Facebook image.

MDEV
  • 10,730
  • 2
  • 33
  • 49
0

I think you should use the getimagesizefromstring function instead of parsing the raw data yourself:

list($width, $height, $type) = getimagesizefromstring($data);

Resaving the image will only degrade image quality.

huysentruitw
  • 27,376
  • 9
  • 90
  • 133