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 @ 2315

Revision 2315, 27.0 KB (checked in by tboutell, 3 years ago)

Apostrophe now recognizes JPEGs with an EXIF orientation hint, like those produced by iPhones and many other cameras when rotated, and automatically rotates when rendering images or reporting image dimensions

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