Allow GET or POST
[bookreader.git] / BookReaderIA / datanode / BookReaderImages.inc.php
1 <?php
2
3 /*
4 Copyright(c) 2008-2010 Internet Archive. Software license AGPL version 3.
5
6 This file is part of BookReader.  The full source code can be found at GitHub:
7 http://github.com/openlibrary/bookreader
8
9 The canonical short name of an image type is the same as in the MIME type.
10 For example both .jpeg and .jpg are considered to have type "jpeg" since
11 the MIME type is "image/jpeg".
12
13     BookReader is free software: you can redistribute it and/or modify
14     it under the terms of the GNU Affero General Public License as published by
15     the Free Software Foundation, either version 3 of the License, or
16     (at your option) any later version.
17
18     BookReader is distributed in the hope that it will be useful,
19     but WITHOUT ANY WARRANTY; without even the implied warranty of
20     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21     GNU Affero General Public License for more details.
22
23     You should have received a copy of the GNU Affero General Public License
24     along with BookReader.  If not, see <http://www.gnu.org/licenses/>.
25 */
26
27 require_once("BookReaderMeta.inc.php");
28
29 class BookReaderImages
30 {
31     public static $MIMES = array('gif' => 'image/gif',
32                    'jp2' => 'image/jp2',
33                    'jpg' => 'image/jpeg',
34                    'jpeg' => 'image/jpeg',
35                    'png' => 'image/png',
36                    'tif' => 'image/tiff',
37                    'tiff' => 'image/tiff');
38                    
39     public static $EXTENSIONS = array('gif' => 'gif',
40                         'jp2' => 'jp2',
41                         'jpeg' => 'jpeg',
42                         'jpg' => 'jpeg',
43                         'png' => 'png',
44                         'tif' => 'tiff',
45                         'tiff' => 'tiff');
46     
47     // Width when generating thumbnails
48     public static $imageSizes = array(
49         'thumb' => 100,
50         'small' => 256,
51         'medium' => 512,
52         'large' => 2048,
53     );
54
55     // Keys in the image permalink urls, e.g. http://www.archive.org/download/itemid/page/cover_{keyval}_{keyval}.jpg
56     public static $imageUrlKeys = array(
57         //'r' => 'reduce', // pow of 2 reduction
58         's' => 'scale', // $$$ scale is downscaling factor in BookReaderImages but most people call this "reduce"
59         'region' => 'region',
60         'tile' => 'tile',
61         'w' => 'width',
62         'h' => 'height',
63         'rotate' => 'rotate'
64     );
65     
66     // Paths to command-line tools
67     var $exiftool = '/petabox/sw/books/exiftool/exiftool';
68     var $kduExpand = '/petabox/sw/bin/kdu_expand';
69     
70     // Name of temporary files, to be cleaned at exit
71     var $tempFiles = array();
72     
73     /*
74      * Serve an image request that requires looking up the book metadata
75      *
76      * Code path:
77      *   - Get book metadata
78      *   - Parse the requested page (e.g. cover_t.jpg, n5_r4.jpg) to determine which page type,
79      *       size and format (etc) is being requested
80      *   - Determine the leaf number corresponding to the page
81      *   - Determine scaling values
82      *   - Serve image request now that all information has been gathered
83      */
84
85     function serveLookupRequest($requestEnv) {
86         $brm = new BookReaderMeta();
87         try {
88             $metadata = $brm->buildMetadata($_REQUEST['id'], $_REQUEST['itemPath'], $_REQUEST['subPrefix'], $_REQUEST['server']);
89         } catch (Exception $e) {
90             $this->BRfatal($e->getMessage());
91         }
92         
93         $page = $_REQUEST['page'];
94
95         // Index of image to return
96         $imageIndex = null;
97
98         // deal with subPrefix
99         if ($_REQUEST['subPrefix']) {
100             $parts = split('/', $_REQUEST['subPrefix']);
101             $bookId = $parts[count($parts) - 1 ];
102         } else {
103             $bookId = $_REQUEST['id'];
104         }
105         
106         $pageInfo = $this->parsePageRequest($page, $bookId);
107
108         $basePage = $pageInfo['type'];
109         
110         $leaf = null;
111         switch ($basePage) {
112         
113             case 'title':
114                 if (! array_key_exists('titleIndex', $metadata)) {
115                     $this->BRfatal("No title page asserted in book");
116                 }
117                 $imageIndex = $metadata['titleIndex'];
118                 break;
119             
120             /* Old 'cover' behaviour where it would show cover 0 if it exists or return 404.
121                Could be re-added as cover0, cover1, etc
122             case 'cover':
123                 if (! array_key_exists('coverIndices', $metadata)) {
124                     $this->BRfatal("No cover asserted in book");
125                 }
126                 $imageIndex = $metadata['coverIndices'][0]; // $$$ TODO add support for other covers
127                 break;
128             */
129             
130             case 'preview':
131             case 'cover': // Show our best guess if cover is requested
132                 // Preference is:
133                 //   Cover page if book was published >= 1950
134                 //   Title page
135                 //   Cover page
136                 //   Page 0
137                          
138                 if ( array_key_exists('date', $metadata) && array_key_exists('coverIndices', $metadata) ) {
139                     if ($brm->parseYear($metadata['date']) >= 1950) {
140                         $imageIndex = $metadata['coverIndices'][0];                
141                         break;
142                     }
143                 }
144                 if (array_key_exists('titleIndex', $metadata)) {
145                     $imageIndex = $metadata['titleIndex'];
146                     break;
147                 }
148                 if (array_key_exists('coverIndices', $metadata)) {
149                     $imageIndex = $metadata['coverIndices'][0];
150                     break;
151                 }
152                 
153                 // First page
154                 $imageIndex = 0;
155                 break;
156                 
157             case 'n':
158                 // Accessible index page
159                 $imageIndex = intval($pageInfo['value']);
160                 break;
161                 
162             case 'page':
163                 // Named page
164                 $index = array_search($pageInfo['value'], $metadata['pageNums']);
165                 if ($index === FALSE) {
166                     // Not found
167                     $this->BRfatal("Page not found");
168                     break;
169                 }
170                 
171                 $imageIndex = $index;
172                 break;
173                 
174             case 'leaf':
175                 // Leaf explicitly specified
176                 $leaf = $pageInfo['value'];
177                 break;
178                 
179             default:
180                 // Shouldn't be possible
181                 $this->BRfatal("Unrecognized page type requested");
182                 break;
183                 
184         }
185         
186         if (is_null($leaf)) {
187             // Leaf was not explicitly set -- look it up
188             $leaf = $brm->leafForIndex($imageIndex, $metadata['leafNums']);
189         }
190         
191         $requestEnv = array(
192             'zip' => $metadata['zip'],
193             'file' => $brm->imageFilePath($leaf, $metadata['subPrefix'], $metadata['imageFormat']),
194             'ext' => 'jpg', // XXX should pass through ext
195         );
196         
197         // remove non-passthrough keys from pageInfo
198         unset($pageInfo['type']);
199         unset($pageInfo['value']);
200         
201         // add pageinfo to request
202         $requestEnv = array_merge($pageInfo, $requestEnv);
203
204         // Return image data - will check privs        
205         $this->serveRequest($requestEnv);
206     
207     }
208     
209     /*
210      * Returns a page image when all parameters such as the image stack location are
211      * passed in.
212      * 
213      * Approach:
214      * 
215      * Get info about requested image (input)
216      * Get info about requested output format
217      * Determine processing parameters
218      * Process image
219      * Return image data
220      * Clean up temporary files
221      */
222      function serveRequest($requestEnv) {
223      
224         // Process some of the request parameters
225         $zipPath  = $requestEnv['zip'];
226         $file     = $requestEnv['file'];
227         if (! $ext) {
228             $ext = $requestEnv['ext'];
229         } else {
230             // Default to jpg
231             $ext = 'jpeg';
232         }
233         if (isset($requestEnv['callback'])) {
234             // validate callback is valid JS identifier (only)
235             $callback = $requestEnv['callback'];
236             $identifierPatt = '/^[[:alpha:]$_]([[:alnum:]$_])*$/';
237             if (! preg_match($identifierPatt, $callback)) {
238                 $this->BRfatal('Invalid callback');
239             }
240         } else {
241             $callback = null;
242         }
243
244         if ( !file_exists($zipPath) ) {
245             $this->BRfatal('Image stack does not exist at ' . $zipPath);
246         }
247         // Make sure the image stack is readable - return 403 if not
248         $this->checkPrivs($zipPath);
249         
250         
251         // Get the image size and depth
252         $imageInfo = $this->getImageInfo($zipPath, $file);
253         
254         // Output json if requested
255         if ('json' == $ext) {
256             // $$$ we should determine the output size first based on requested scale
257             $this->outputJSON($imageInfo, $callback); // $$$ move to BookReaderRequest
258             exit;
259         }
260         
261         // Unfortunately kakadu requires us to know a priori if the
262         // output file should be .ppm or .pgm.  By decompressing to
263         // .bmp kakadu will write a file we can consistently turn into
264         // .pnm.  Really kakadu should support .pnm as the file output
265         // extension and automatically write ppm or pgm format as
266         // appropriate.
267         $this->decompressToBmp = true; // $$$ shouldn't be necessary if we use file info to determine output format
268         if ($this->decompressToBmp) {
269           $stdoutLink = '/tmp/stdout.bmp';
270         } else {
271           $stdoutLink = '/tmp/stdout.ppm';
272         }
273         
274         $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
275         
276         // Rotate is currently only supported for jp2 since it does not add server load
277         $allowedRotations = array("0", "90", "180", "270");
278         $rotate = $requestEnv['rotate'];
279         if ( !in_array($rotate, $allowedRotations) ) {
280             $rotate = "0";
281         }
282         
283         // Image conversion options
284         $pngOptions = '';
285         $jpegOptions = '-quality 75';
286         
287         // The pbmreduce reduction factor produces an image with dimension 1/n
288         // The kakadu reduction factor produces an image with dimension 1/(2^n)
289         
290         // We interpret the requested size and scale, look at image format, and determine the
291         // actual scaling to be returned to the client.  We generally return the largest
292         // power of 2 reduction that is larger than the requested size in order to reduce
293         // image processing load on our cluster.  The client should then scale to their final
294         // needed size.
295         
296         // Set scale from height or width if set
297         if (isset($requestEnv['height'])) {
298             $powReduce = $this->nearestPow2Reduce($requestEnv['height'], $imageInfo['height']);
299             $scale = pow(2, $powReduce);
300         } else if (isset($requestEnv['width'])) {
301             $powReduce = $this->nearestPow2Reduce($requestEnv['width'], $imageInfo['width']);
302             $scale = pow(2, $powReduce);
303
304         } else {
305             // Set scale from named size (e.g. 'large') if set
306             $size = $requestEnv['size'];
307             if ( $size && array_key_exists($size, self::$imageSizes)) {
308                 $srcRatio = floatval($imageInfo['width']) / floatval($imageInfo['height']);
309                 if ($srcRatio > 1) {
310                     // wide
311                     $dimension = 'width';
312                 } else {
313                     $dimension = 'height';
314                 }
315                 $powReduce = $this->nearestPow2Reduce(self::$imageSizes[$size], $imageInfo[$dimension]);
316                 $scale = pow(2, $powReduce);
317                 
318             } else {
319                 // No named size - use explicit scale, if given
320                 $scale = $requestEnv['scale'];
321                 if (!$scale) {
322                     $scale = 1;
323                 }
324                 $powReduce = $this->nearestPow2ForScale($scale);
325                 // ensure integer scale
326                 $scale = pow(2, $powReduce);
327             }            
328         }
329         
330         // Override depending on source image format
331         // $$$ consider doing a 302 here instead, to make better use of the browser cache
332         // Limit scaling for 1-bit images.  See https://bugs.edge.launchpad.net/bookreader/+bug/486011
333         if (1 == $imageInfo['bits']) {
334             
335             if ($scale > 1) {
336                 $scale /= 2;
337                 $powReduce -= 1;
338                 
339                 // Hard limit so there are some black pixels to use!
340                 if ($scale > 4) {
341                     $scale = 4;
342                     $powReduce = 2;
343                 }
344             }
345         }
346         
347         if (!file_exists($stdoutLink)) 
348         {  
349           system('ln -s /dev/stdout ' . $stdoutLink);  
350         }
351         
352         putenv('LD_LIBRARY_PATH=/petabox/sw/lib/kakadu');
353         
354         $unzipCmd  = $this->getUnarchiveCommand($zipPath, $file);
355         
356         $decompressCmd = $this->getDecompressCmd($imageInfo['type'], $powReduce, $rotate, $scale, $stdoutLink);
357                
358         // Non-integer scaling is currently disabled on the cluster
359         // if (isset($_REQUEST['height'])) {
360         //     $cmd .= " | pnmscale -height {$_REQUEST['height']} ";
361         // }
362         
363         switch ($ext) {
364             case 'png':
365                 $compressCmd = ' | pnmtopng ' . $pngOptions;
366                 break;
367                 
368             case 'jpeg':
369             case 'jpg':
370             default:
371                 $compressCmd = ' | pnmtojpeg ' . $jpegOptions;
372                 $ext = 'jpeg'; // for matching below
373                 break;
374         
375         }
376         
377         if (($ext == $fileExt) && ($scale == 1) && ($rotate === "0")) {
378             // Just pass through original data if same format and size
379             $cmd = $unzipCmd;
380         } else {
381             $cmd = $unzipCmd . $decompressCmd . $compressCmd;
382         }
383         
384         // print $cmd;
385         
386         $filenameForClient = $this->filenameForClient($file, $ext);
387         
388         $headers = array('Content-type: '. self::$MIMES[$ext],
389                          'Cache-Control: max-age=15552000',
390                          'Content-disposition: inline; filename=' . $filenameForClient);
391                           
392         
393         $errorMessage = '';
394         
395         if (! $this->passthruIfSuccessful($headers, $cmd, $errorMessage)) { // $$$ move to BookReaderRequest
396             // $$$ automated reporting
397             trigger_error('BookReader Processing Error: ' . $cmd . ' -- ' . $errorMessage, E_USER_WARNING);
398             
399             // Try some content-specific recovery
400             $recovered = false;
401             if ($imageInfo['type'] == 'jp2') {
402                 $records = $this->getJp2Records($zipPath, $file);
403                 if (array_key_exists('Clevels', $records)) {
404                     $maxReduce = intval($records['Clevels']);
405                     trigger_error("BookReader using max reduce $maxReduce from jp2 records");
406                 } else {
407                     $maxReduce = 0;
408                 }
409                 
410                 $powReduce = min($powReduce, $maxReduce);
411                 $reduce = pow(2, $powReduce);
412                 
413                 $cmd = $unzipCmd . $this->getDecompressCmd($imageInfo['type'], $powReduce, $rotate, $scale, $stdoutLink) . $compressCmd;
414                 trigger_error('BookReader rerunning with new cmd: ' . $cmd, E_USER_WARNING);
415                 if ($this->passthruIfSuccessful($headers, $cmd, $errorMessage)) { // $$$ move to BookReaderRequest
416                     $recovered = true;
417                 } else {
418                     $this->cleanup();
419                     trigger_error('BookReader fallback image processing also failed: ' . $errorMessage, E_USER_WARNING);
420                 }
421             }
422             
423             if (! $recovered) {
424                 $this->BRfatal('Problem processing image - command failed');
425             }
426         }
427         
428         $this->cleanup();
429     }    
430     
431     function getUnarchiveCommand($archivePath, $file)
432     {
433         $lowerPath = strtolower($archivePath);
434         if (preg_match('/\.([^\.]+)$/', $lowerPath, $matches)) {
435             $suffix = $matches[1];
436             
437             if ($suffix == 'zip') {
438                 return 'unzip -p '
439                     . escapeshellarg($archivePath)
440                     . ' ' . escapeshellarg($file);
441             } else if ($suffix == 'tar') {
442                 return ' ( 7z e -so '
443                     . escapeshellarg($archivePath)
444                     . ' ' . escapeshellarg($file) . ' 2>/dev/null ) ';
445             } else {
446                 $this->BRfatal('Incompatible archive format');
447             }
448     
449         } else {
450             $this->BRfatal('Bad image stack path');
451         }
452         
453         $this->BRfatal('Bad image stack path or archive format');
454         
455     }
456     
457     /*
458      * Returns the image type associated with the file extension.
459      */
460     function imageExtensionToType($extension)
461     {
462         
463         if (array_key_exists($extension, self::$EXTENSIONS)) {
464             return self::$EXTENSIONS[$extension];
465         } else {
466             $this->BRfatal('Unknown image extension');
467         }            
468     }
469     
470     /*
471      * Get the image information.  The returned associative array fields will
472      * vary depending on the image type.  The basic keys are width, height, type
473      * and bits.
474      */
475     function getImageInfo($zipPath, $file)
476     {
477         return $this->getImageInfoFromExif($zipPath, $file); // this is fast
478         
479         /*
480         $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
481         $type = imageExtensionToType($fileExt);
482         
483         switch ($type) {
484             case "jp2":
485                 return getImageInfoFromJp2($zipPath, $file);
486                 
487             default:
488                 return getImageInfoFromExif($zipPath, $file);
489         }
490         */
491     }
492     
493     // Get the records of of JP2 as returned by kdu_expand
494     function getJp2Records($zipPath, $file)
495     {
496         
497         $cmd = $this->getUnarchiveCommand($zipPath, $file)
498                  . ' | ' . $this->kduExpand
499                  . ' -no_seek -quiet -i /dev/stdin -record /dev/stdout';
500         exec($cmd, $output);
501         
502         $records = Array();
503         foreach ($output as $line) {
504             $elems = explode("=", $line, 2);
505             if (1 == count($elems)) {
506                 // delimiter not found
507                 continue;
508             }
509             $records[$elems[0]] = $elems[1];
510         }
511         
512         return $records;
513     }
514     
515     /*
516      * Get the image width, height and depth using the EXIF information.
517      */
518     function getImageInfoFromExif($zipPath, $file)
519     {
520         
521         // We look for all the possible tags of interest then act on the
522         // ones presumed present based on the file type
523         $tagsToGet = ' -ImageWidth -ImageHeight -FileType'        // all formats
524                      . ' -BitsPerComponent -ColorSpace'          // jp2
525                      . ' -BitDepth'                              // png
526                      . ' -BitsPerSample';                        // tiff
527                             
528         $cmd = $this->getUnarchiveCommand($zipPath, $file)
529             . ' | '. $this->exiftool . ' -S -fast' . $tagsToGet . ' -';
530         exec($cmd, $output);
531         
532         $tags = Array();
533         foreach ($output as $line) {
534             $keyValue = explode(": ", $line);
535             $tags[$keyValue[0]] = $keyValue[1];
536         }
537         
538         $width = intval($tags["ImageWidth"]);
539         $height = intval($tags["ImageHeight"]);
540         $type = strtolower($tags["FileType"]);
541         
542         switch ($type) {
543             case "jp2":
544                 $bits = intval($tags["BitsPerComponent"]);
545                 break;
546             case "tiff":
547                 $bits = intval($tags["BitsPerSample"]);
548                 break;
549             case "jpeg":
550                 $bits = 8;
551                 break;
552             case "png":
553                 $bits = intval($tags["BitDepth"]);
554                 break;
555             default:
556                 $this->BRfatal("Unsupported image type $type for file $file in $zipPath");
557                 break;
558         }
559        
560        
561         $retval = Array('width' => $width, 'height' => $height,
562             'bits' => $bits, 'type' => $type);
563         
564         return $retval;
565     }
566     
567     /*
568      * Output JSON given the imageInfo associative array
569      */
570     function outputJSON($imageInfo, $callback)
571     {
572         header('Content-type: text/plain');
573         $jsonOutput = json_encode($imageInfo);
574         if ($callback) {
575             $jsonOutput = $callback . '(' . $jsonOutput . ');';
576         }
577         echo $jsonOutput;
578     }
579     
580     function getDecompressCmd($imageType, $powReduce, $rotate, $scale, $stdoutLink) {
581         
582         switch ($imageType) {
583             case 'jp2':
584                 $decompressCmd = 
585                     " | " . $this->kduExpand . " -no_seek -quiet -reduce $powReduce -rotate $rotate -i /dev/stdin -o " . $stdoutLink;
586                 if ($this->decompressToBmp) {
587                     // We suppress output since bmptopnm always outputs on stderr
588                     $decompressCmd .= ' | (bmptopnm 2>/dev/null)';
589                 }
590                 break;
591         
592             case 'tiff':
593                 // We need to create a temporary file for tifftopnm since it cannot
594                 // work on a pipe (the file must be seekable).
595                 // We use the BookReaderTiff prefix to give a hint in case things don't
596                 // get cleaned up.
597                 $tempFile = tempnam("/tmp", "BookReaderTiff");
598                 array_push($this->tempFiles, $tempFile);
599             
600                 // $$$ look at bit depth when reducing
601                 $decompressCmd = 
602                     ' > ' . $tempFile . ' ; tifftopnm ' . $tempFile . ' 2>/dev/null' . $this->reduceCommand($scale);
603                 break;
604          
605             case 'jpeg':
606                 $decompressCmd = ' | ( jpegtopnm 2>/dev/null ) ' . $this->reduceCommand($scale);
607                 break;
608         
609             case 'png':
610                 $decompressCmd = ' | ( pngtopnm 2>/dev/null ) ' . $this->reduceCommand($scale);
611                 break;
612                 
613             default:
614                 $this->BRfatal('Unknown image type: ' . $imageType);
615                 break;
616         }
617         return $decompressCmd;
618     }
619     
620     // If the command has its initial output on stdout the headers will be emitted followed
621     // by the stdout output.  If initial output is on stderr an error message will be
622     // returned.
623     // 
624     // Returns:
625     //   true - if command emits stdout and has zero exit code
626     //   false - command has initial output on stderr or non-zero exit code
627     //   &$errorMessage - error string if there was an error
628     //
629     // $$$ Tested with our command-line image processing.  May be deadlocks for
630     //     other cases.
631     function passthruIfSuccessful($headers, $cmd, &$errorMessage)
632     {
633         $retVal = false;
634         $errorMessage = '';
635         
636         $descriptorspec = array(
637            0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
638            1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
639            2 => array("pipe", "w"),   // stderr is a pipe to write to
640         );
641         
642         $cwd = NULL;
643         $env = NULL;
644         
645         $process = proc_open($cmd, $descriptorspec, $pipes, $cwd, $env);
646         
647         if (is_resource($process)) {
648             // $pipes now looks like this:
649             // 0 => writeable handle connected to child stdin
650             // 1 => readable handle connected to child stdout
651             // 2 => readable handle connected to child stderr
652         
653             $stdin = $pipes[0];        
654             $stdout = $pipes[1];
655             $stderr = $pipes[2];
656             
657             // check whether we get input first on stdout or stderr
658             $read = array($stdout, $stderr);
659             $write = NULL;
660             $except = NULL;
661             
662             $numChanged = stream_select($read, $write, $except, NULL); // $$$ no timeout
663             if (false === $numChanged) {
664                 // select failed
665                 $errorMessage = 'Select failed';
666                 $retVal = false;
667                 error_log('BookReader select failed!');
668             } else {            
669                 if (in_array($stderr, $read)) {
670                     // Either content in stderr, or stderr is closed (could read 0 bytes)
671                     $error = stream_get_contents($stderr);
672                     if ($error) {
673                     
674                         $errorMessage = $error;
675                         $retVal = false;
676                         
677                         fclose($stderr);
678                         fclose($stdout);
679                         fclose($stdin);
680                         
681                         // It is important that you close any pipes before calling
682                         // proc_close in order to avoid a deadlock
683                         proc_close($process);
684                         return $retVal;             
685  
686                     }
687                 }
688                 
689                 $output = fopen('php://output', 'w');
690                 foreach($headers as $header) {
691                     header($header);
692                 }
693                 stream_copy_to_stream($pipes[1], $output);
694                 fclose($output); // okay since tied to special php://output
695                 $retVal = true;
696             }
697     
698             fclose($stderr);
699             fclose($stdout);
700             fclose($stdin);
701     
702             
703             // It is important that you close any pipes before calling
704             // proc_close in order to avoid a deadlock
705             $cmdRet = proc_close($process);
706             if (0 != $cmdRet) {
707                 $retVal = false;
708                 $errorMessage .= "Command failed with result code " . $cmdRet;
709             }
710         }
711         return $retVal;
712     }
713     
714     function BRfatal($string) {
715         $this->cleanup();
716         throw new Exception("Image error: $string");
717     }
718     
719     // Returns true if using a power node
720     // XXX change to "on red box" - not working for new Xeon
721     function onPowerNode() {
722         exec("lspci | fgrep -c Realtek", $output, $return);
723         if ("0" != $output[0]) {
724             return true;
725         } else {
726             exec("egrep -q AMD /proc/cpuinfo", $output, $return);
727             if ($return == 0) {
728                 return true;
729             }
730         }
731         return false;
732     }
733     
734     function reduceCommand($scale) {
735         if (1 != $scale) {
736             if ($this->onPowerNode()) {
737                 return ' | pnmscale -reduce ' . $scale . ' 2>/dev/null ';
738             } else {
739                 return ' | pnmscale -nomix -reduce ' . $scale . ' 2>/dev/null ';
740             }
741         } else {
742             return '';
743         }
744     }
745     
746     function checkPrivs($filename) {
747         // $$$ we assume here that requests for the title, cover or preview
748         //     come in via BookReaderPreview.php which will be re-run with
749         //     privileges after we return the 403
750         if (!is_readable($filename)) {
751             header('HTTP/1.1 403 Forbidden');
752             exit(0);
753         }
754     }
755     
756     // Given file path (inside archive) and output file extension, return a filename
757     // suitable for Content-disposition header
758     function filenameForClient($filePath, $ext) {
759         $pathParts = pathinfo($filePath);
760         if ('jpeg' == $ext) {
761             $ext = 'jpg';
762         }
763         return $pathParts['filename'] . '.' . $ext;
764     }
765     
766     // Returns the nearest power of 2 reduction factor that results in a larger image
767     function nearestPow2Reduce($desiredDimension, $sourceDimension) {
768         $ratio = floatval($sourceDimension) / floatval($desiredDimension);
769         return $this->nearestPow2ForScale($ratio);
770     }
771     
772     // Returns nearest power of 2 reduction factor that results in a larger image
773     function nearestPow2ForScale($scale) {
774         $scale = intval($scale);
775         if ($scale <= 1) {
776             return 0;
777         }
778         $binStr = decbin($scale); // convert to binary string. e.g. 5 -> '101'
779         return strlen($binStr) - 1;
780     }
781     
782     /*
783      * Parses a page request like "page5_r2.jpg" or "cover_t.jpg" to corresponding
784      * page type, size, reduce, and format
785      */
786     function parsePageRequest($pageRequest, $bookPrefix) {
787     
788         // Will hold parsed results
789         $pageInfo = array();
790         
791         // Normalize
792         $pageRequest = strtolower($pageRequest);
793         
794         // Pull off extension
795         if (preg_match('#(.*)\.([^.]+)$#', $pageRequest, $matches) === 1) {
796             $pageRequest = $matches[1];
797             $extension = $matches[2];
798             if ($extension == 'jpeg') {
799                 $extension = 'jpg';
800             }
801         } else {
802             $extension = 'jpg';
803         }
804         $pageInfo['extension'] = $extension;
805         
806         // Split parts out
807         $parts = explode('_', $pageRequest);
808
809         // Remove book prefix if it was included (historical)
810         if ($parts[0] == $bookPrefix) {
811             array_shift($parts);
812         }
813         
814         if (count($parts) === 0) {
815             $this->BRfatal('No page type specified');
816         }
817         $page = array_shift($parts);
818         
819         $pageTypes = array(
820             'page' => 'str',
821             'n' => 'num',
822             'cover' => 'single',
823             'preview' => 'single',
824             'title' => 'single',
825             'leaf' => 'num'
826         );
827         
828         // Look for known page types
829         foreach ( $pageTypes as $pageName => $kind ) {
830             if ( preg_match('#^(' . $pageName . ')(.*)#', $page, $matches) === 1 ) {
831                 $pageInfo['type'] = $matches[1];
832                 switch ($kind) {
833                     case 'str':
834                         $pageInfo['value'] = $matches[2];
835                         break;
836                     case 'num':
837                         $pageInfo['value'] = intval($matches[2]);
838                         break;
839                     case 'single':
840                         break;
841                 }
842             }
843         }
844         
845         if ( !array_key_exists('type', $pageInfo) ) {
846             $this->BRfatal('Unrecognized page type');
847         }
848         
849         // Look for other known parts
850         foreach ($parts as $part) {
851             if ( array_key_exists($part, self::$imageSizes) ) {
852                 $pageInfo['size'] = $part;
853                 continue;
854             }
855         
856             // Key must be alpha, value must start with digit and contain digits, alpha, ',' or '.'
857             // Should prevent injection of strange values into the redirect to datanode
858             if ( preg_match('#^([a-z]+)(\d[a-z0-9,.]*)#', $part, $matches) === 0) {
859                 // Not recognized
860                 continue;
861             }
862             
863             $key = $matches[1];
864             $value = $matches[2];
865             
866             if ( array_key_exists($key, self::$imageUrlKeys) ) {
867                 $pageInfo[self::$imageUrlKeys[$key]] = $value;
868                 continue;
869             }
870             
871             // If we hit here, was unrecognized (no action)
872         }
873         
874         return $pageInfo;
875     }
876     
877     // Clean up temporary files and resources
878     function cleanup() {
879         foreach($this->tempFiles as $tempFile) {
880             unlink($tempFile);
881         }
882         $this->tempFiles = array();
883     }
884     
885 }
886
887 ?>