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