Function to retrieve jp2 info records
[bookreader.git] / BookReaderIA / datanode / BookReaderImages.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 $MIMES = array('gif' => 'image/gif',
28                'jp2' => 'image/jp2',
29                'jpg' => 'image/jpeg',
30                'jpeg' => 'image/jpeg',
31                'png' => 'image/png',
32                'tif' => 'image/tiff',
33                'tiff' => 'image/tiff');
34                
35 $EXTENSIONS = array('gif' => 'gif',
36                     'jp2' => 'jp2',
37                     'jpeg' => 'jpeg',
38                     'jpg' => 'jpeg',
39                     'png' => 'png',
40                     'tif' => 'tiff',
41                     'tiff' => 'tiff');
42                
43 // Paths to command-line tools
44 $exiftool = '/petabox/sw/books/exiftool/exiftool';
45 $kduExpand = '/petabox/sw/bin/kdu_expand';
46
47 // Process some of the request parameters
48 $zipPath  = $_REQUEST['zip'];
49 $file     = $_REQUEST['file'];
50 if (isset($_REQUEST['ext'])) {
51     $ext = $_REQUEST['ext'];
52 } else {
53     // Default to jpg
54     $ext = 'jpeg';
55 }
56 if (isset($_REQUEST['callback'])) {
57     // validate callback is valid JS identifier (only)
58     $callback = $_REQUEST['callback'];
59     $identifierPatt = '/^[[:alpha:]$_]([[:alnum:]$_])*$/';
60     if (! preg_match($identifierPatt, $callback)) {
61         BRfatal('Invalid callback');
62     }
63 } else {
64     $callback = null;
65 }
66
67 /*
68  * Approach:
69  * 
70  * Get info about requested image (input)
71  * Get info about requested output format
72  * Determine processing parameters
73  * Process image
74  * Return image data
75  * Clean up temporary files
76  */
77  
78 function getUnarchiveCommand($archivePath, $file)
79 {
80     $lowerPath = strtolower($archivePath);
81     if (preg_match('/\.([^\.]+)$/', $lowerPath, $matches)) {
82         $suffix = $matches[1];
83         
84         if ($suffix == 'zip') {
85             return 'unzip -p '
86                 . escapeshellarg($archivePath)
87                 . ' ' . escapeshellarg($file);
88         } else if ($suffix == 'tar') {
89             return '7z e -so '
90                 . escapeshellarg($archivePath)
91                 . ' ' . escapeshellarg($file);
92         } else {
93             BRfatal('Incompatible archive format');
94         }
95
96     } else {
97         BRfatal('Bad image stack path');
98     }
99     
100     BRfatal('Bad image stack path or archive format');
101     
102 }
103
104 /*
105  * Returns the image type associated with the file extension.
106  */
107 function imageExtensionToType($extension)
108 {
109     global $EXTENSIONS;
110     
111     if (array_key_exists($extension, $EXTENSIONS)) {
112         return $EXTENSIONS[$extension];
113     } else {
114         BRfatal('Unknown image extension');
115     }            
116 }
117
118 /*
119  * Get the image information.  The returned associative array fields will
120  * vary depending on the image type.  The basic keys are width, height, type
121  * and bits.
122  */
123 function getImageInfo($zipPath, $file)
124 {
125     return getImageInfoFromExif($zipPath, $file); // this is fast
126     
127     /*
128     $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
129     $type = imageExtensionToType($fileExt);
130     
131     switch ($type) {
132         case "jp2":
133             return getImageInfoFromJp2($zipPath, $file);
134             
135         default:
136             return getImageInfoFromExif($zipPath, $file);
137     }
138     */
139 }
140
141 // Get the records of of JP2 as returned by kdu_expand
142 function getJp2Records($zipPath, $file)
143 {
144     global $kduExpand;
145     
146     $cmd = getUnarchiveCommand($zipPath, $file)
147              . ' | ' . $kduExpand
148              . ' -no_seek -quiet -i /dev/stdin -record /dev/stdout';
149     exec($cmd, $output);
150     
151     $records = Array();
152     foreach ($output as $line) {
153         $elems = explode("=", $line, 2);
154         if (1 == count($elems)) {
155             // delimiter not found
156             continue;
157         }
158         $records[$elems[0]] = $elems[1];
159     }
160     
161     return $records;
162 }
163
164 /*
165  * Get the image width, height and depth using the EXIF information.
166  */
167 function getImageInfoFromExif($zipPath, $file)
168 {
169     global $exiftool;
170     
171     // We look for all the possible tags of interest then act on the
172     // ones presumed present based on the file type
173     $tagsToGet = ' -ImageWidth -ImageHeight -FileType'        // all formats
174                  . ' -BitsPerComponent -ColorSpace'          // jp2
175                  . ' -BitDepth'                              // png
176                  . ' -BitsPerSample';                        // tiff
177                         
178     $cmd = getUnarchiveCommand($zipPath, $file)
179         . ' | '. $exiftool . ' -S -fast' . $tagsToGet . ' -';
180     exec($cmd, $output);
181     
182     $tags = Array();
183     foreach ($output as $line) {
184         $keyValue = explode(": ", $line);
185         $tags[$keyValue[0]] = $keyValue[1];
186     }
187     
188     $width = intval($tags["ImageWidth"]);
189     $height = intval($tags["ImageHeight"]);
190     $type = strtolower($tags["FileType"]);
191     
192     switch ($type) {
193         case "jp2":
194             $bits = intval($tags["BitsPerComponent"]);
195             break;
196         case "tiff":
197             $bits = intval($tags["BitsPerSample"]);
198             break;
199         case "jpeg":
200             $bits = 8;
201             break;
202         case "png":
203             $bits = intval($tags["BitDepth"]);
204             break;
205         default:
206             BRfatal("Unsupported image type");
207             break;
208     }
209    
210    
211     $retval = Array('width' => $width, 'height' => $height,
212         'bits' => $bits, 'type' => $type);
213     
214     return $retval;
215 }
216
217 /*
218  * Output JSON given the imageInfo associative array
219  */
220 function outputJSON($imageInfo, $callback)
221 {
222     header('Content-type: text/plain');
223     $jsonOutput = json_encode($imageInfo);
224     if ($callback) {
225         $jsonOutput = $callback . '(' . $jsonOutput . ');';
226     }
227     echo $jsonOutput;
228 }
229
230 // Get the image size and depth
231 $imageInfo = getImageInfo($zipPath, $file);
232
233 // Output json if requested
234 if ('json' == $ext) {
235     // $$$ we should determine the output size first based on requested scale
236     outputJSON($imageInfo, $callback);
237     exit;
238 }
239
240 // Unfortunately kakadu requires us to know a priori if the
241 // output file should be .ppm or .pgm.  By decompressing to
242 // .bmp kakadu will write a file we can consistently turn into
243 // .pnm.  Really kakadu should support .pnm as the file output
244 // extension and automatically write ppm or pgm format as
245 // appropriate.
246 $decompressToBmp = true;
247 if ($decompressToBmp) {
248   $stdoutLink = '/tmp/stdout.bmp';
249 } else {
250   $stdoutLink = '/tmp/stdout.ppm';
251 }
252
253 $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
254
255 // Rotate is currently only supported for jp2 since it does not add server load
256 $allowedRotations = array("0", "90", "180", "270");
257 $rotate = $_REQUEST['rotate'];
258 if ( !in_array($rotate, $allowedRotations) ) {
259     $rotate = "0";
260 }
261
262 // Image conversion options
263 $pngOptions = '';
264 $jpegOptions = '-quality 75';
265
266 // The pbmreduce reduction factor produces an image with dimension 1/n
267 // The kakadu reduction factor produceds an image with dimension 1/(2^n)
268 // $$$ handle continuous values for scale
269 if (isset($_REQUEST['height'])) {
270     $ratio = floatval($_REQUEST['origHeight']) / floatval($_REQUEST['height']);
271     if ($ratio <= 2) {
272         $scale = 2;
273         $powReduce = 1;    
274     } else if ($ratio <= 4) {
275         $scale = 4;
276         $powReduce = 2;
277     } else {
278         //$powReduce = 3; //too blurry!
279         $scale = 2;
280         $powReduce = 1;
281     }
282
283 } else {
284     // $$$ could be cleaner
285     $scale = intval($_REQUEST['scale']);
286     if (1 >= $scale) {
287         $scale = 1;
288         $powReduce = 0;
289     } else if (2 > $scale) {
290         $powReduce = 0;
291         $scale = 1;
292     } else if (4 > $scale) {
293         $powReduce = 1;
294         $scale = 2;
295     } else if (8 > $scale) {
296         $powReduce = 2;
297         $scale = 4;
298     } else if (16 > $scale) {
299         $powReduce = 3;
300         $scale = 8;
301     } else if (32 > $scale) {
302         $powReduce = 4;
303         $scale = 16;
304     } else if (64 > $scale) {
305         $powReduce = 5;
306         $scale = 32;
307     } else {
308         // $$$ Leaving this in as default though I'm not sure why it is...
309         $scale = 8;
310         $powReduce = 3;
311     }
312 }
313
314 // Override depending on source image format
315 // $$$ consider doing a 302 here instead, to make better use of the browser cache
316 // Limit scaling for 1-bit images.  See https://bugs.edge.launchpad.net/bookreader/+bug/486011
317 if (1 == $imageInfo['bits']) {
318     if ($scale > 1) {
319         $scale /= 2;
320         $powReduce -= 1;
321         
322         // Hard limit so there are some black pixels to use!
323         if ($scale > 4) {
324             $scale = 4;
325             $powReduce = 2;
326         }
327     }
328 }
329
330 if (!file_exists($stdoutLink)) 
331 {  
332   system('ln -s /dev/stdout ' . $stdoutLink);  
333 }
334
335
336 putenv('LD_LIBRARY_PATH=/petabox/sw/lib/kakadu');
337
338 $unzipCmd  = getUnarchiveCommand($zipPath, $file);
339
340 switch ($imageInfo['type']) {
341     case 'jp2':
342         $decompressCmd = 
343             " | " . $kduExpand . " -no_seek -quiet -reduce $powReduce -rotate $rotate -i /dev/stdin -o " . $stdoutLink;
344         if ($decompressToBmp) {
345             $decompressCmd .= ' | bmptopnm ';
346         }
347         break;
348
349     case 'tiff':
350         // We need to create a temporary file for tifftopnm since it cannot
351         // work on a pipe (the file must be seekable).
352         // We use the BookReaderTiff prefix to give a hint in case things don't
353         // get cleaned up.
354         $tempFile = tempnam("/tmp", "BookReaderTiff");
355     
356         // $$$ look at bit depth when reducing
357         $decompressCmd = 
358             ' > ' . $tempFile . ' ; tifftopnm ' . $tempFile . ' 2>/dev/null' . reduceCommand($scale);
359         break;
360  
361     case 'jpeg':
362         $decompressCmd = ' | jpegtopnm ' . reduceCommand($scale);
363         break;
364
365     case 'png':
366         $decompressCmd = ' | pngtopnm ' . reduceCommand($scale);
367         break;
368         
369     default:
370         BRfatal('Unknown source file extension: ' . $fileExt);
371         break;
372 }
373        
374 // Non-integer scaling is currently disabled on the cluster
375 // if (isset($_REQUEST['height'])) {
376 //     $cmd .= " | pnmscale -height {$_REQUEST['height']} ";
377 // }
378
379 switch ($ext) {
380     case 'png':
381         $compressCmd = ' | pnmtopng ' . $pngOptions;
382         break;
383         
384     case 'jpeg':
385     case 'jpg':
386     default:
387         $compressCmd = ' | pnmtojpeg ' . $jpegOptions;
388         $ext = 'jpeg'; // for matching below
389         break;
390
391 }
392
393 if (($ext == $fileExt) && ($scale == 1) && ($rotate === "0")) {
394     // Just pass through original data if same format and size
395     $cmd = $unzipCmd;
396 } else {
397     $cmd = $unzipCmd . $decompressCmd . $compressCmd;
398 }
399
400 # print $cmd;
401
402
403 // $$$ investigate how to flush cache when this file is changed
404 header('Content-type: ' . $MIMES[$ext]);
405 header('Cache-Control: max-age=15552000');
406 passthru ($cmd); # cmd returns image data
407
408 if (isset($tempFile)) {
409     unlink($tempFile);
410 }
411
412 function BRFatal($string) {
413     echo "alert('$string');\n";
414     die(-1);
415 }
416
417 // Returns true if using a power node
418 function onPowerNode() {
419     exec("lspci | fgrep -c Realtek", $output, $return);
420     if ("0" != $output[0]) {
421         return true;
422     } else {
423         exec("egrep -q AMD /proc/cpuinfo", $output, $return);
424         if ($return == 0) {
425             return true;
426         }
427     }
428     return false;
429 }
430
431 function reduceCommand($scale) {
432     if (1 != $scale) {
433         if (onPowerNode()) {
434             return ' | pnmscale -reduce ' . $scale;
435         } else {
436             return ' | pnmscale -nomix -reduce ' . $scale;
437         }
438     } else {
439         return '';
440     }
441 }
442
443
444 ?>
445