To participate you must create an account on apostrophenow.org. If you have already done so, click Login.

root/plugins/apostrophePlugin/trunk/lib/toolkit/aImageConverter.class.php @ 2329

Revision 2329, 27.1 KB (checked in by tboutell, 3 years ago)

Fixed #626 (don't play with EXIF if the file is not a JPEG)

Line 
1<?php
2
3/*
4 *
5 * Efficient image conversions using netpbm or (if netpbm is not available) gd.
6 * For more information see the README file.
7 *
8 */ 
9
10class aImageConverter 
11{
12  // Produces images suitable for intentional cropping by CSS.
13  // Either the width or the height will match the request; the other
14  // will EXCEED the request. Looks nicer than letterboxing in cases
15  // where keeping the entire picture is not essential.
16
17  static public function scaleToNarrowerAxis($fileIn, $fileOut, $width, $height, $quality = 75)
18  {
19    $width = ceil($width);
20    $height = ceil($height);
21    $quality = ceil($quality);
22    list($iwidth, $iheight) = getimagesize($fileIn); 
23    if (!$iwidth) {
24      return false;
25    }
26    $iratio = $iwidth / $iheight;
27    $ratio = $width / $height;
28    if ($iratio > $ratio) {
29      $width = false;
30    } else {
31      $height = false;
32    }
33    return self::scaleToFit($fileIn, $fileOut, $width, $height, $quality);
34  }
35
36  static public function scaleToFit($fileIn, $fileOut, $width, $height, $quality = 75)
37  {
38    if ($width === false) {
39      $scaleParameters = array('ysize' => $height + 0);
40    } elseif ($height === false) {
41      $scaleParameters = array('xsize' => $width + 0);
42    } else {
43      $scaleParameters = array('xysize' => array($width + 0, $height + 0));
44    }
45    $result = self::scaleBody($fileIn, $fileOut, $scaleParameters, array(), $quality);
46    return $result;
47  }
48
49  static public function scaleByFactor($fileIn, $fileOut, $factor, 
50    $quality = 75)
51  {
52    $quality = ceil($quality);
53    $scaleParameters = array('scale' => $factor + 0); 
54    return self::scaleBody($fileIn, $fileOut, $scaleParameters, array(), $quality);
55  }
56
57  // $width and $height are the dimensions of the final rendered image. $quality is the JPEG quality setting (where needed).
58  // The $crop parameters, when not null (all four must be null or not null), are used to crop the original before scaling/distorting
59  // to the specified width and height and are always in the original image's coordinates.
60 
61  // If cropping coordinates are not specified, the largest possible portion of the center of the original image is scaled to fit into the
62  // destination image without distortion
63 
64  static public function cropOriginal($fileIn, $fileOut, $width, $height, $quality = 75, $cropLeft = null, $cropTop = null, $cropWidth = null,  $cropHeight = null)
65  {
66    // Allow skipping of parameters
67    if (is_null($quality))
68    {
69      $quality = 75;
70    }
71    $width = ceil($width);
72    $height = ceil($height);
73    $quality = ceil($quality);
74    list($iwidth, $iheight) = getimagesize($fileIn); 
75    if (!$iwidth) 
76    {
77      return false;
78    }
79    $iratio = $iwidth / $iheight;
80    $ratio = $width / $height;
81
82     // Spike's contribution: arbitrary cropping
83     if (!is_null($cropWidth) && !is_null($cropHeight) && !is_null($cropLeft) && !is_null($cropTop))
84     {
85       $cropTop = ceil($cropTop + 0);
86       $cropLeft = ceil($cropLeft + 0);
87       $cropWidth = ceil($cropWidth + 0);
88       $cropHeight = ceil($cropHeight + 0);
89       
90       $scale = array('xysize' => array($width + 0, $height + 0));
91       $crop = array('left' => $cropLeft, 'top' => $cropTop, 'width' => $cropWidth, 'height' => $cropHeight);
92       return self::scaleBody($fileIn, $fileOut, $scale, $crop, $quality);
93     }
94
95    $scale = array('xysize' => array($width + 0, $height + 0));
96    if ($iratio < $ratio)
97    {
98      $cropHeight = floor($iwidth * ($height / $width));
99      $cropTop = floor(($iheight - $cropHeight) / 2);
100      $cropLeft = 0;
101      $cropWidth = $iwidth;
102    }
103    else
104    {
105      $cropWidth = floor($iheight * $ratio);
106      $cropLeft = floor(($iwidth - $cropWidth) / 2);
107      $cropTop = 0;
108      $cropHeight = $iheight;
109    }
110    $scale = array('xysize' => array($width + 0, $height + 0));
111    $crop = array('left' => $cropLeft, 'top' => $cropTop, 'width' => $cropWidth, 'height' => $cropHeight);
112    return self::scaleBody($fileIn, $fileOut, $scale, $crop, $quality);
113  }
114
115  // Change the format without cropping or scaling
116  static public function convertFormat($fileIn, $fileOut, $quality = 75)
117  {
118    $quality = ceil($quality);
119    return self::scaleBody($fileIn, $fileOut, false, false, $quality);
120  }
121
122  static private function scaleBody($fileIn, $fileOut, $scaleParameters = array(), $cropParameters = array(), $quality = 75) 
123  {   
124    if (sfConfig::get('app_aimageconverter_netpbm', true))
125    {
126      // Auto fallback to gd, but only if it's not a small image gd can handle better (1.4). This means we get
127      // full alpha channel for manageably-sized PNGs and good performance for huge PNGs
128      $info = getimagesize($fileIn);
129      $mapTypes = array(IMAGETYPE_GIF => IMG_GIF, IMAGETYPE_PNG => IMG_PNG, IMAGETYPE_JPEG => IMG_JPG);
130      // Usually the 1024x768 rule is better, but this is useful for testing
131      if (sfConfig::get('app_aimageconverter_netpbm', true) === 'always')
132      {
133        return self::scaleNetpbm($fileIn, $fileOut, $scaleParameters, $cropParameters, $quality);
134      }
135      // If we got valid image info, the image size is less than 1024x768, gd is enabled, and gd supports
136      // the image type... *then* we skip to gd.
137      if (($info !== false) && (($info[0] <= 1024) && ($info[1] <= 768)) && function_exists('imagetypes') && isset($mapTypes[$info[2]]) && (imagetypes() & $mapTypes[$info[2]]))
138      {
139        return self::scaleGd($fileIn, $fileOut, $scaleParameters, $cropParameters, $quality);
140      }
141      $result = self::scaleNetpbm($fileIn, $fileOut, $scaleParameters, $cropParameters, $quality);
142      if (!$result)
143      {
144        return self::scaleGd($fileIn, $fileOut, $scaleParameters, $cropParameters, $quality);
145      }
146    }
147    else
148    {
149      return self::scaleGd($fileIn, $fileOut, $scaleParameters, $cropParameters, $quality);
150    }
151  }
152 
153  // Get the JPEG EXIF rotation. Always returns 1 (no rotation) for other formats.
154  // Other values:
155  // case 2: // horizontal flip
156  // case 3: // 180 rotate left
157  // case 4: // vertical flip
158  // case 5: // vertical flip + 90 rotate right
159  // case 6: // 90 rotate right
160  // case 7: // horizontal flip + 90 rotate right
161  // case 8:    // 90 rotate left
162
163  static public function getRotation($file, $getimagesize = null)
164  {
165    if (is_null($getimagesize))
166    {
167      $getimagesize = getimagesize($file);
168    }
169    if ($getimagesize[2] !== IMAGETYPE_JPEG)
170    {
171      return 1;
172    }
173    if (!extension_loaded("exif"))
174    {
175      // We can't tell
176      return 1;
177    }
178    $exif = exif_read_data($file);
179    if (!$exif)
180    {
181      return 1;
182    }
183    if (isset($exif['IFD0']['Orientation']))
184    {
185      // Code I'm seeing does this
186      $ort = $exif['IFD0']['Orientation'];
187    } elseif (isset($exif['Orientation']))
188    {
189      // Files I'm seeing do this
190      $ort = $exif['Orientation'];
191    }
192    else
193    {
194      $ort = 1;
195    }
196    return $ort;
197  }
198 
199  static private function scaleNetpbm($fileIn, $fileOut, $scaleParameters = array(), $cropParameters = array(), $quality = 75)
200  {
201    $outputFilters = array(
202      "jpg" => "pnmtojpeg --quality %d",
203      "jpeg" => "pnmtojpeg --quality %d",
204      "ppm" => "cat",
205      "pbm" => "cat",
206      "pgm" => "cat",
207      "tiff" => "pnmtotiff",
208      "png" => "pnmtopng",
209      "gif" => "ppmquant 256 | ppmtogif",
210      "bmp" => "ppmtobmp"
211    );
212    if (preg_match("/\.(\w+)$/", $fileOut, $matches)) {
213      $extension = $matches[1];
214      $extension = strtolower($extension);
215      if (!isset($outputFilters[$extension])) {
216        return false;
217      }
218      $filter = sprintf($outputFilters[$extension], $quality);
219    } else {
220      return false;
221    }
222    $path = sfConfig::get("app_aimageconverter_path", "");
223    if (strlen($path)) {
224      if (!preg_match("/\/$/", $path)) {
225        $path .= "/";
226      }
227    }
228       
229    // AUGH: some versions of anytopnm don't have
230    // the brains to look at the file signature. We need
231    // to be compatible with this brain damage, so pick
232    // the right filter based on the results of getimagesize()
233    // and punt to anytopnm only if we can't figure it out.
234   
235    // While we're at it: detect PDF by magic number too,
236    // not by extension, that's tacky
237
238    $input = 'anytopnm';
239   
240    $in = fopen($fileIn, 'r');
241    $bytes = fread($in, 4);
242    if ($bytes === '%PDF')
243    {
244      $input = 'gs -sDEVICE=ppm -sOutputFile=- ' .
245        ' -dNOPAUSE -dFirstPage=1 -dLastPage=1 -r100 -q -';
246    }
247    fclose($in);
248   
249    $info = getimagesize($fileIn);
250    if ($info !== false)
251    {
252      $type = $info[2];
253      if ($type === IMAGETYPE_GIF)
254      {
255        $input = 'giftopnm';
256      } 
257      elseif ($type === IMAGETYPE_PNG)
258      {
259        $input = 'pngtopnm';
260      }
261      elseif ($type === IMAGETYPE_JPEG)
262      {
263        $input = 'jpegtopnm';
264      }
265    }
266   
267    $rotate = '';
268   
269    $rotation = aImageConverter::getRotation($fileIn, $info);
270    switch ($rotation)
271    {
272        case 1: // nothing
273        $rotate = '';
274        break;
275
276        case 2: // horizontal flip
277        $rotate = '| pamflip -leftright ';
278        break;
279
280        case 3: // 180 rotate left
281        $rotate = '| pamflip -rotate180 ';
282        break;
283
284        case 4: // vertical flip
285        $rotate = '| pamflip -topbottom ';
286        break;
287
288        case 5: // vertical flip + 90 rotate right
289        $rotate = '| pamflip -topbottom | pamflip -cw ';
290        break;
291
292        case 6: // 90 rotate right
293        $rotate = '| pamflip -cw ';
294        break;
295
296        case 7: // horizontal flip + 90 rotate right
297        $rotate = '| pamflip -leftright | pamflip -cw ';
298        break;
299
300        case 8:    // 90 rotate left
301        $rotate = '| pamflip -ccw ';
302        break;
303    }
304   
305 
306    $scaleString = '';
307    $extraInputFilters = '';
308    foreach ($scaleParameters as $key => $values)
309    {
310      $scaleString .= " -$key ";
311      if (is_array($values))
312      {
313        foreach ($values as $value)
314        {
315          $value = ceil($value);
316          $scaleString .= " $value";
317        }
318      }
319      else
320      {
321        $values = ceil($values);
322        $scaleString .= " $values";
323      }
324    }
325    if (count($cropParameters))
326    {
327      $extraInputFilters = 'pnmcut ';
328      foreach ($cropParameters as $ckey => $cvalue)
329      {
330        $cvalue = ceil($cvalue);
331        $extraInputFilters .= " -$ckey $cvalue";
332      }
333    }
334   
335    $cmd = "(PATH=$path:\$PATH; export PATH; $input < " . escapeshellarg($fileIn) . ' ' . $rotate . ' ' . ($extraInputFilters ? "| $extraInputFilters" : "") . " " . ($scaleParameters ? "| pnmscale $scaleString " : "") . "| $filter " .
336      "> " . escapeshellarg($fileOut) . " " .
337      ") 2> /dev/null";
338    // sfContext::getInstance()->getLogger()->info("$cmd");
339    system($cmd, $result);
340    if ($result != 0) 
341    {
342      return false;
343    }
344    return true;
345  }
346 
347  static private function scaleGd($fileIn, $fileOut, $scaleParameters = array(), $cropParameters = array(), $quality = 75)
348  {
349   
350    // gd version for those who can't install netpbm, poor buggers
351    // "handles" PDF by rendering a blank white image. We already superimpose a PDF icon,
352    // so this should work well
353   
354    // (if you can install ghostview, you can install netpbm too, so there's no middle case)
355   
356    // Special case to emit the original. This preserves transparency in GIFs and is faster for everything. (PNGs can always preserve
357    // alpha channel in anything under 1024x768 or when gd is the only backend enabled.) WARNING: keep this up to date if new
358    // capabilities are added - we need to make sure they are not active etc. before using this trick. TODO: check for this in
359    // netpbm land too, right now in a typical configuration it's not checked over 1024x768   
360   
361    $imageInfo = getimagesize($fileIn);
362    // Don't panic on a PDF, fall through to the fake handler for that.
363    if ($imageInfo)
364    {
365      $width = $imageInfo[0];
366      $height = $imageInfo[1];
367      $orientation = aImageConverter::getRotation($fileIn, $imageInfo);
368      if ($imageInfo[2] === IMAGETYPE_JPEG)
369      {
370        // Some EXIF orientations swap width and height
371        switch ($orientation)
372        {
373          case 5: // vertical flip + 90 rotate right
374          case 6: // 90 rotate right
375          case 7: // horizontal flip + 90 rotate right
376          case 8:    // 90 rotate left
377          $tmp = $width;
378          $width = $height;
379          $height = $tmp;
380          break;
381        }
382      }
383     
384      $infoIn = pathinfo($fileIn);   
385      $infoOut = pathinfo($fileOut);   
386     
387      // Try not to do any work if we are not changing anything
388      if ($orientation == 1)
389      {
390        if (((!count($scaleParameters)) || (isset($scaleParameters['xysize']) && $scaleParameters['xysize'][0] == $width && $scaleParameters['xysize'][1] == $height)) && (strtolower($infoIn['extension']) === strtolower($infoOut['extension'])) && (!count($cropParameters)))
391        {
392          copy($fileIn, $fileOut);
393          return true;
394        }
395      }
396    }
397   
398    if (preg_match('/\.pdf$/i', $fileIn))
399    {
400      $in = self::createTrueColorAlpha(100, 100);
401      imagefilledrectangle($in, 0, 0, 100, 100, imagecolorallocate($in, 255, 255, 255));
402    } 
403    else
404    {
405      $in = self::imagecreatefromany($fileIn);
406      if ($orientation != 1)
407      {
408        // Note that gd rotation is CCL
409       
410        switch ($orientation)
411        {
412          case 2: // horizontal flip
413          aImageConverter::horizontalFlip($in);
414          break;
415
416          case 3: // 180 rotate left
417          $in2 = imagerotate($in, 180, imagecolorallocate($in, 255, 255, 255));
418          imagedestroy($in);
419          $in = $in2;
420          break;
421
422          case 4: // vertical flip
423          aImageConverter::verticalFlip($in);
424          break;
425
426          case 5: // vertical flip + 90 rotate right
427          aImageConverter::verticalFlip($in);
428          $in2 = imagerotate($in, 270, imagecolorallocate($in, 255, 255, 255));
429          imagedestroy($in);
430          $in = $in2;
431          break;
432
433          case 6: // 90 rotate right
434          $in2 = imagerotate($in, 270, imagecolorallocate($in, 255, 255, 255));
435          imagedestroy($in);
436          $in = $in2;
437          break;
438
439          case 7: // horizontal flip + 90 rotate right
440          aImageConverter::horizontalFlip($in);
441          $in2 = imagerotate($in, 270, imagecolorallocate($in, 255, 255, 255));
442          imagedestroy($in);
443          $in = $in2;
444          break;
445
446          case 8:    // 90 rotate left
447          $in2 = imagerotate($in, 90, imagecolorallocate($in, 255, 255, 255));
448          imagedestroy($in);
449          $in = $in2;
450          break;
451        }
452      }
453    }
454   
455    if (!$in)
456    {
457      return false;
458    }
459   
460    if (preg_match("/\.(\w+)$/i", $fileOut, $matches))
461    {
462      $extension = $matches[1];
463      $extension = strtolower($extension);
464    }
465    else
466    {
467      imagedestroy($in);
468      return false;
469    }
470   
471    $top = 0;
472    $left = 0;
473    $width = imagesx($in);
474    $height = imagesy($in);
475    if (count($cropParameters))
476    {
477      if (isset($cropParameters['top']))
478      {
479        $top = $cropParameters['top'];
480      }
481      if (isset($cropParameters['left']))
482      {
483        $left = $cropParameters['left'];
484      }
485      if (isset($cropParameters['width']))
486      {
487        $width = $cropParameters['width'];
488      }
489      if (isset($cropParameters['height']))
490      {
491        $height = $cropParameters['height'];
492      }
493      $cropped = self::createTrueColorAlpha($width, $height);
494      imagealphablending($cropped, false);
495      imagesavealpha($cropped, true);
496      imagecopy($cropped, $in, 0, 0, $left, $top, $width, $height);
497      imagedestroy($in);
498      $in = null;
499    }
500    else
501    {
502      // No cropping, so don't waste time and memory
503      $cropped = $in;
504      $in = null;
505    }
506 
507    if (count($scaleParameters))
508    {
509      $width = imagesx($cropped);
510      $height = imagesy($cropped);
511      $swidth = $width;
512      $sheight = $height;
513      if (isset($scaleParameters['xsize']))
514      {
515        $height = $scaleParameters['xsize'] * imagesy($cropped) / imagesx($cropped);
516        $width = $scaleParameters['xsize'];
517        $out = self::createTrueColorAlpha($width, $height);
518        imagecopyresampled($out, $cropped, 0, 0, 0, 0, $width, $height, imagesx($cropped), imagesy($cropped));
519        imagedestroy($cropped);
520        $cropped = null;
521      }
522      elseif (isset($scaleParameters['ysize']))
523      {
524        $width = $scaleParameters['ysize'] * imagesx($cropped) / imagesy($cropped);
525        $height = $scaleParameters['ysize'];
526        $out = self::createTrueColorAlpha($width, $height);
527        imagecopyresampled($out, $cropped, 0, 0, 0, 0, $width, $height, imagesx($cropped), imagesy($cropped));
528        imagedestroy($cropped);
529        $cropped = null;
530      }
531      elseif (isset($scaleParameters['scale']))
532      {
533        $width = imagesx($cropped) * $scaleParameters['scale'];
534        $height = imagesy($cropped)* $scaleParameters['scale'];
535        $out = self::createTrueColorAlpha($width, $height);
536        imagecopyresampled($out, $cropped, 0, 0, 0, 0, $width, $height, imagesx($cropped), imagesy($cropped));
537        imagedestroy($cropped);
538        $cropped = null;
539      }
540      elseif (isset($scaleParameters['xysize']))
541      {
542        $width = $scaleParameters['xysize'][0];
543        $height = $scaleParameters['xysize'][1];
544        // This was backwards until 05/31/2010, making things bigger rather than smaller if their
545        // aspect ratios differed from the original. Be consistent with netpbm which makes things
546        // smaller not bigger
547        if (($width / $height) > ($swidth / $sheight))
548        {
549          // Wider than the original. So it will be narrower than requested
550          $width = ceil($height * ($swidth / $sheight));
551        }
552        else
553        {
554          // Taller than the original. So it will be shorter than requested
555          $height = ceil($width * ($sheight / $swidth));
556        }
557        $out = self::createTrueColorAlpha($width, $height);
558        imagecopyresampled($out, $cropped, 0, 0, 0, 0, $width, $height, $swidth, $sheight);
559        imagedestroy($cropped);
560        $cropped = null;
561      }
562    }
563    else
564    {
565      // No scaling, don't waste time and memory
566      $out = $cropped;
567      $cropped = null;
568    }
569   
570    $extension = strtolower($infoOut['extension']);
571    if ($extension === 'gif')
572    {
573      imagegif($out, $fileOut);
574    }
575    elseif (($extension === 'jpg') || ($extension === 'jpeg'))
576    {
577      imagejpeg($out, $fileOut, $quality);
578    }
579    elseif ($extension === 'png')
580    {
581      imagepng($out, $fileOut);
582    }
583    else
584    {
585      return false;
586    }
587     
588    imagedestroy($out);
589    $out = null;
590    return true;
591  }
592 
593  // Flips the image in place
594  static protected function horizontalFlip($in)
595  {
596    $tmp = self::imageCreateTrueColor(1, $height);
597    for ($x = 0; ($x < ($width >> 1)); $x++)
598    {
599      imagecopy($tmp, $in, 0, 0, $x, 0, 1, $height);
600      imagecopy($in, $in, $x, 0, ($width - $x) - 1, 0, 1, $height);
601      imagecopy($in, $tmp, ($width - $x) - 1, 0, 0, 0, 1, $height);
602    }
603    imagedestroy($tmp);
604  }
605 
606  // Flips the image in place
607  static protected function verticalFlip($in)
608  {
609    $tmp = self::imageCreateTrueColor($width, 1);
610    for ($y = 0; ($y < ($height >> 1)); $y++)
611    {
612      imagecopy($tmp, $in, 0, 0, 0, $y, $width, 1);
613      imagecopy($in, $in, 0, $y, 0, ($height - $y) - 1, $width, 1);
614      imagecopy($in, $tmp, 0, ($height - $y) - 1, 0, 0, $width, 1);
615    }
616    imagedestroy($tmp);
617  }
618 
619  // Make sure the new image is capable of being saved with intact alpha channel;
620  // don't composite alpha channel in gd. If a designer uploads an alpha channel image
621  // they must have a reason for doing so
622  static public function createTrueColorAlpha($width, $height)
623  {
624    $im = imagecreatetruecolor($width, $height);
625    imagealphablending($im, false);
626    imagesavealpha($im, true);
627    return $im;
628  }
629 
630  // Retrieves what you really want to know about an image file, PDFs included,
631  // before making calls such as the above based on good information.
632 
633  // Returns as follows:
634 
635  // array('format' => 'file extension: gif, jpg, png or pdf', 'width' => width in pixels, 'height' => height in pixels);
636
637  // $format is the recommended file extension based on the actual file type, not the user's (possibly totally false or absent)
638  // claimed file extension.
639 
640  // If the file does not have a valid header identifying it as one of these types, false is returned.
641 
642  // If the 'format-only' option is true, only the format field is returned. This is much faster if the
643  // file is a PDF.
644 
645  static public function getInfo($file, $options = array())
646  {
647    $formatOnly = (isset($options['format-only']) && $options['format-only']);
648    $result = array();
649    $in = fopen($file, "rb");
650    $data = fread($in, 4);
651    fclose($in);
652   
653   
654    if ($data === '%PDF')
655    {
656      // format-only
657      if ((!aImageConverter::supportsInput('pdf')) || $formatOnly)
658      {
659        // All we can do is confirm the format and allow
660        // download of the original (which, for PDF, is
661        // usually fine)
662        return array('format' => 'pdf');
663      }
664      $result['format'] = 'pdf';
665      $path = sfConfig::get("app_aimageconverter_path", "");
666      if (strlen($path)) {
667        if (!preg_match("/\/$/", $path)) {
668          $path .= "/";
669        }
670      }
671      // Bounding box goes to stderr, not stdout! Charming
672      // 5 second timeout for reading dimensions. Keeps us from getting stuck on
673      // PDFs that just barely work in Adobe but are noncompliant and hang ghostscript.
674      // Read the output one line at a time so we can catch the happy
675      // bounding box message without hanging
676     
677      // Problem: this doesn't work. We regain control but the process won't die for some reason. It helps
678      // with import but for now go with the simpler standard invocation and hope they fix gs
679
680      // $cmd = "(PATH=$path:\$PATH; export PATH; gs -sDEVICE=bbox -dNOPAUSE -dFirstPage=1 -dLastPage=1 -r100 -q " . escapeshellarg($file) . " -c quit ) 2>&1";
681     
682      $cmd = "( PATH=$path:\$PATH; export PATH; gs -sDEVICE=bbox -dNOPAUSE -dFirstPage=1 -dLastPage=1 -r100 -q " . escapeshellarg($file) . " -c quit & GS=$!; ( sleep 5; kill \$GS ) & TIMEOUT=\$!; wait \$GS; kill \$TIMEOUT ) 2>&1";
683
684      // For some reason system() does not get the same result when killing subshells as I get when executing
685      // $cmd directly. I don't know why this is this the case but it's easily reproduced
686     
687      $script = aFiles::getTemporaryFilename() . '.sh';
688      file_put_contents($script, $cmd);
689      $cmd = "/bin/sh " . escapeshellarg($script);
690      $in = popen($cmd, "r");
691      $data = stream_get_contents($in);
692      pclose($in);
693      // Actual nonfatal errors in the bbox output mean it's not safe to just
694      // read this naively with fscanf, look for the good part
695      if (preg_match("/%%BoundingBox: \d+ \d+ (\d+) (\d+)/", $data, $matches))
696      {
697        $result['width'] = $matches[1];
698        $result['height'] = $matches[2];
699      }
700      if (!isset($result['width']))
701      {
702        // Bad PDF
703        return false;
704      }
705      return $result;
706    }
707    else
708    {
709      $formats = array(
710        IMAGETYPE_JPEG => "jpg",
711        IMAGETYPE_PNG => "png",
712        IMAGETYPE_GIF => "gif"
713      );
714      $data = getimagesize($file);
715      if (count($data) < 3)
716      {
717        return false;
718      }
719      if (!isset($formats[$data[2]]))
720      {
721        return false;
722      }
723      $format = $formats[$data[2]];
724      $result['format'] = $format;
725      if ($formatOnly)
726      {
727        return $result;
728      }
729      $result['width'] = $data[0];
730      $result['height'] = $data[1];
731      if ($format === 'jpg')
732      {
733        // Some EXIF orientations swap width and height
734        switch (aImageConverter::getRotation($file, $data))
735        {
736          case 5: // vertical flip + 90 rotate right
737          case 6: // 90 rotate right
738          case 7: // horizontal flip + 90 rotate right
739          case 8:    // 90 rotate left
740          $result['width'] = $data[1];
741          $result['height'] = $data[0];
742          break;
743        }
744      }
745      return $result;
746    }
747  }
748
749  // Odds and ends missing from gd
750 
751  // As commonly found on the Internets
752
753  static private function imagecreatefromany($filename) 
754  {
755    foreach (array('png', 'jpeg', 'gif', 'bmp', 'ico') as $type) 
756    {
757      $func = 'imagecreatefrom' . $type;
758      if (is_callable($func)) 
759      {
760        $image = @call_user_func($func, $filename);
761        if ($image) return $image;
762      }
763    }
764    return false;
765  }
766 
767  // Can this box handle pdf, png, jpeg (also acdepts jpg), gif, bmp, ico...
768
769  // Mainly used to check for PDF support.
770 
771  // NOTE: this call is a performance hit, especially with netpbm and ghostscript available.
772  // So we cache the result for 5 minutes. Keep that in mind if you make configuration changes, install
773  // ghostscript, etc. and don't see an immediate difference.
774
775  static public function supportsInput($extension)
776  {
777    $hint = aImageConverter::getHint("input:$extension");
778    if (!is_null($hint))
779    {
780      return $hint;
781    }
782   
783    $result = false;
784    if (sfConfig::get('app_aimageconverter_netpbm', true))
785    {
786      if (aImageConverter::supportsInputNetpbm($extension))
787      {
788        $result = true;
789      }
790    }
791    if (!$result)
792    {
793      $result = aImageConverter::supportsInputGd($extension);
794    }
795    aImageConverter::setHint("input:$extension", $result);
796    return $result;
797  }
798
799  static public function supportsInputNetpbm($extension)
800  {
801    $types = array('gif' => 'gif', 'png' => 'png', 'jpg' => 'jpeg', 'jpeg' => 'jpeg', 'bmp' => 'bmp', 'ico' => 'ico');
802    $path = sfConfig::get("app_aimageconverter_path", "");
803    if (strlen($path)) {
804      if (!preg_match("/\/$/", $path)) {
805        $path .= "/";
806      }
807    }
808    if ($extension === 'pdf')
809    {
810      $cmd = 'gs';
811    }
812    elseif (!isset($types[$extension]))
813    {
814      if (!preg_match('/^\w+$/', $extension))
815      {
816        return false;
817      }
818      $cmd = $extension . 'topnm';
819    }
820    else
821    {
822      $cmd = $types[$extension] . 'topnm';
823    }
824    $in = popen("(PATH=$path:\$PATH; export PATH; which $cmd)", "r");
825    $result = stream_get_contents($in);
826    pclose($in);
827    if (strlen($result))
828    {
829      return true;
830    }
831    return false;
832  }
833 
834  static public function supportsInputGd($extension)
835  {
836    $types = array('gif' => 'gif', 'png' => 'png', 'jpg' => 'jpeg', 'jpeg' => 'jpeg', 'bmp' => 'bmp', 'ico' => 'ico');
837    if (!isset($types[$extension]))
838    {
839      return false;
840    }
841    $f = 'imagecreatefrom' . $types[$extension];
842    return is_callable($f);
843  }
844 
845  static public function getHint($hint)
846  {
847    $cache = aImageConverter::getHintCache();
848    $key = 'apostrophe:imageconverter:' . $hint;
849    return $cache->get($key, null);
850  }
851 
852  static public function setHint($hint, $value)
853  {
854    $cache = aImageConverter::getHintCache();
855    // The lifetime should be short to avoid annoying developers who are
856    // trying to fix their configuration and test with new possibilities
857    $key = 'apostrophe:imageconverter:' . $hint;
858    $cache->set($key, $value, 300);
859  }
860  static public function getHintCache()
861  {
862    $cacheClass = sfConfig::get('app_a_hint_cache_class', 'sfFileCache');
863    $cache = new $cacheClass(sfConfig::get('app_a_hint_cache_options', array('cache_dir' => aFiles::getWritableDataFolder(array('a_hint_cache')))));
864    return $cache;
865  }
866}
Note: See TracBrowser for help on using the browser.