Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

php allowed memory size exhausted on image upload rotation

I have a script that uploads images and rotates them depending on the orientation and I'm experiencing the problem that when an image that has EXIF tags is uploaded, I get an error saying:

Allowed memory size of 33554432 bytes exhausted (tried to allocate 10368 bytes.

And then the line it is referring to in the error log.

I did notice that it's only happening with images that have EXIF tags. If normal images, generated by Photoshop or something are uploaded, it works without problems.

The actual image orientation code is the following:

function correctImageOrientation($fullpath) {
  if (function_exists('exif_read_data')) {
    $exif = exif_read_data($fullpath);
    if($exif && isset($exif['Orientation'])) {
      $orientation = $exif['Orientation'];
      if($orientation != 1){
        $img = imagecreatefromjpeg($fullpath);
        $deg = 0;
        switch ($orientation) {
          case 3:
            $deg = 180;
            break;
          case 6:
            $deg = 270;
            break;
          case 8:
            $deg = 90;
            break;
        }
        if ($deg) {
          $img = imagerotate($img, $deg, 0);        
        }
        // then rewrite the rotated image back to the disk as $filename 
        imagejpeg($img, $fullpath, 100);
      } // if there is some rotation necessary
    } // if have the exif orientation info
  } // if function exists      
}

The exact line in the error_log where the memory problem happens is actually the one where it says:

$img = imagerotate($img, $deg, 0);

The way I am calling it in the script is the following:

$dirname = session::value('user_id');
$rotatedfile = '/home/myfolder/public_html/'.$dirname.'/'.$file_name;  
$rotatedfile = $this->correctImageOrientation($rotatedfile);

What I am basically trying to achieve is that the rotated image gets saved in the same place as the original file, basically replacing it.

Again, this is ONLY happening with images that contain EXIF information. All others are uploaded without problems.

What could be causing this memory allocation problem?

like image 356
user1227914 Avatar asked Jun 21 '14 01:06

user1227914


2 Answers

Your error is this:

Allowed memory size of 33554432 bytes exhausted (tried to allocate 10368 bytes).

33554432 bytes converts to 32 megabytes. So this all means that PHP ran out of memory while trying to do some work.

You claim that the images that fail have EXIF info, but that doesn’t ring true to me as the cause of this. Irregardless, the quick solution to your issue is to increase PHP memory for your function by adding and ini_set line connected to memory_limit to your function

For example, add it here after you do the if (function_exists('exif_read_data')) { check. I am setting it to 64M since that will effectively double your scripts memory capacity when it runs this function. Code here:

function correctImageOrientation($fullpath) {
  if (function_exists('exif_read_data')) {
    ini_set('memory_limit', '64M');
    $exif = exif_read_data($fullpath);
    if($exif && isset($exif['Orientation'])) {
      $orientation = $exif['Orientation'];
      if($orientation != 1){
        $img = imagecreatefromjpeg($fullpath);
        $deg = 0;
        switch ($orientation) {
          case 3:
            $deg = 180;
            break;
          case 6:
            $deg = 270;
            break;
          case 8:
            $deg = 90;
            break;
        }
        if ($deg) {
          $img = imagerotate($img, $deg, 0);        
        }
        // then rewrite the rotated image back to the disk as $filename 
        imagejpeg($img, $fullpath, 100);
      } // if there is some rotation necessary
    } // if have the exif orientation info
  } // if function exists      
}

What I am basically trying to achieve is that the rotated image gets saved in the same place as the original file, basically replacing it.

The problem is you are using the GD library in PHP which will eat up memory when PHP loads the file into the system and eat up even more memory when it is attempting to rotate the image.

It could be that the images that have EXIF info actually have a higher DPI than the standard 72dpi. So while their dimensions might seem superficially the same as another image without EXIF info, a 300dpi image will effectively be about 4 times larger in size than a 72dpi image. Which is most likely why those images are failing; not the EXIF info but overall DPI.

Now you can also change the memory limit in php.ini by changing the line that would read as memory_limit = 32M. And technically this would work. But I don’t consider it good practice for one script of function that is failing.

That’s because when you change the setting in the php.ini it increases the RAM for all PHP interactions; not just the problem issue. So your server is suddenly eating up more RAM for Apache (which runs PHP) for basic functions as well as the oddball function that eats up more RAM. Meaning if this code is only accessed a handful of times a day, then why burden the larger server that is more than happy with 32M of RAM per PHP process? Better use ini_set('memory_limit', '64M'); to isolate RAM increase needs like this.

like image 134
Giacomo1968 Avatar answered Sep 28 '22 05:09

Giacomo1968


imagecreatefromjpeg uncompresses the image and puts the result into the memory. That is the reason why a 3 MB JPEG needs sometimes 32 MB of memory (The exact amount depends on the resolution, color depth, etc.).

You need this result, so you assign it to a variable:

$img = imagecreatefromjpeg($fullpath);

And now the problem. imagerotate uses the $img resource, rotates it and puts the result into a new region of the memory as it returns by design a new image resource instead of overwriting the given $img resource. So finally you need 64 MB of memory:

$img = imagerotate($img, $deg, 0);

Proof:

// our image
$url = 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/Pluto-01_Stern_03_Pluto_Color_TXT.jpg/1024px-Pluto-01_Stern_03_Pluto_Color_TXT.jpg';
file_put_contents('../cache4/' . basename($url), fopen($url, 'r'));
$filename = '../cache4/' . basename($url);

echo 'Before imagecreate: ' . round(memory_get_usage() / pow(1024, 2)) . ' MB (Max: ' . round(memory_get_peak_usage() / pow(1024, 2)) . ' MB)<br>' . PHP_EOL;

// read image into RAM for further usage
$image = imagecreatefromjpeg($filename);

echo 'After imagecreate: ' . round(memory_get_usage() / pow(1024, 2)) . ' MB (Max: ' . round(memory_get_peak_usage() / pow(1024, 2)) . ' MB)<br>' . PHP_EOL;

// rotate image
$result = imagerotate($image, 180, 0);

echo 'After imagerotate: ' . round(memory_get_usage() / pow(1024, 2)) . ' MB (Max: ' . round(memory_get_peak_usage() / pow(1024, 2)) . ' MB)<br>' . PHP_EOL;

// flip image
imageflip($result, IMG_FLIP_VERTICAL);

echo 'After imageflip: ' . round(memory_get_usage() / pow(1024, 2)) . ' MB (Max: ' . round(memory_get_peak_usage() / pow(1024, 2)) . ' MB)<br>' . PHP_EOL;

Returns:

Before imagecreate: 0 MB (Max: 0 MB)
After imagecreate: 5 MB (Max: 5 MB)
After imagerotate: 10 MB (Max: 10 MB)
After imageflip: 10 MB (Max: 10 MB)

As you can see the peak raises to 10 MB after imagerotate has been called, but a function like imageflip does not show this behaviour as it internally overwrites your variable.

Maybe you think you can solve that by overwriting the variable:

$image = imagerotate($image, 180, 0);

But that will only reduce the current usage:

After imagerotate: 5 MB (Max: 10 MB)

It's sad that we can not pass the variable by reference since PHP 5.4:

imagerotate(&$image, 180, 0);

I opened a bug report so it will be hopefully optimized in the future.

like image 41
mgutt Avatar answered Sep 28 '22 04:09

mgutt