1-bit detection routines for jp2, tiff, jpeg and png
[bookreader.git] / BookReaderIA / datanode / BookReaderImages.php
1 <?php
2
3 /*
4 Copyright(c)2008 Internet Archive. Software license AGPL version 3.
5
6 This file is part of BookReader.
7
8 The canonical short name of an image type is the same as in the MIME type.
9 For example both .jpeg and .jpg are considered to have type "jpeg" since
10 the MIME type is "image/jpeg".
11
12     BookReader is free software: you can redistribute it and/or modify
13     it under the terms of the GNU Affero General Public License as published by
14     the Free Software Foundation, either version 3 of the License, or
15     (at your option) any later version.
16
17     BookReader is distributed in the hope that it will be useful,
18     but WITHOUT ANY WARRANTY; without even the implied warranty of
19     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20     GNU Affero General Public License for more details.
21
22     You should have received a copy of the GNU Affero General Public License
23     along with BookReader.  If not, see <http://www.gnu.org/licenses/>.
24 */
25
26 $MIMES = array('gif' => 'image/gif',
27                'jp2' => 'image/jp2',
28                'jpg' => 'image/jpeg',
29                'jpeg' => 'image/jpeg',
30                'png' => 'image/png',
31                'tif' => 'image/tiff',
32                'tiff' => 'image/tiff');
33                
34 $EXTENSIONS = array('gif' => 'gif',
35                     'jp2' => 'jp2',
36                     'jpeg' => 'jpeg',
37                     'jpg' => 'jpeg',
38                     'png' => 'png',
39                     'tif' => 'tiff',
40                     'tiff' => 'tiff');
41                
42 $exiftool = '/petabox/sw/books/exiftool/exiftool';
43
44 // Process some of the request parameters
45 $zipPath  = $_REQUEST['zip'];
46 $file     = $_REQUEST['file'];
47 if (isset($_REQUEST['ext'])) {
48   $ext = $_REQUEST['ext'];
49 } else {
50   // Default to jpg
51   $ext = 'jpg';
52 }
53 if (isset($_REQUEST['callback'])) {
54   // XXX sanitize
55   $callback = $_REQUEST['callback'];
56 } else {
57   $callback = null;
58 }
59
60 /*
61  * Approach:
62  * 
63  * Get info about requested image (input)
64  * Get info about requested output format
65  * Determine processing parameters
66  * Process image
67  * Return image data
68  * Clean up temporary files
69  */
70  
71 function getUnarchiveCommand($archivePath, $file)
72 {
73     $lowerPath = strtolower($archivePath);
74     if (preg_match('/\.([^\.]+)$/', $lowerPath, $matches)) {
75         $suffix = $matches[1];
76         
77         if ($suffix == 'zip') {
78             return 'unzip -p '
79                 . escapeshellarg($archivePath)
80                 . ' ' . escapeshellarg($file);
81         } else if ($suffix == 'tar') {
82             return '7z e -so '
83                 . escapeshellarg($archivePath)
84                 . ' ' . escapeshellarg($file);
85         } else {
86             BRfatal('Incompatible archive format');
87         }
88
89     } else {
90         BRfatal('Bad image stack path');
91     }
92     
93     BRfatal('Bad image stack path or archive format');
94     
95 }
96
97 /*
98  * Returns the image type associated with the file extension.
99  */
100 function imageExtensionToType($extension)
101 {
102     global $EXTENSIONS;
103     
104     if (array_key_exists($extension, $EXTENSIONS)) {
105         return $EXTENSIONS[$extension];
106     } else {
107         BRfatal('Unknown image extension');
108     }            
109 }
110
111 /*
112  * Get the image width, height and depth from a jp2 file in zip.
113  */
114 function getImageInfo($zipPath, $file)
115 {
116     global $exiftool;
117     
118     $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
119     $type = imageExtensionToType($fileExt);
120      
121     // We look for all the possible tags of interest then act on the
122     // ones presumed present based on the file type
123     $tagsToGet = ' -ImageWidth -ImageHeight -FileType'        // all formats
124                  . ' -BitsPerComponent -ColorSpace'          // jp2
125                  . ' -BitDepth'                              // png
126                  . ' -BitsPerSample';                        // tiff
127                         
128     $cmd = getUnarchiveCommand($zipPath, $file)
129         . ' | '. $exiftool . ' -S -fast' . $tagsToGet . ' -';
130     exec($cmd, $output);
131     
132     $tags = Array();
133     foreach ($output as $line) {
134         $keyValue = explode(": ", $line);
135         $tags[$keyValue[0]] = $keyValue[1];
136     }
137     
138     $width = intval($tags["ImageWidth"]);
139     $height = intval($tags["ImageHeight"]);
140     $type = strtolower($tags["FileType"]);
141     
142     switch ($tags["FileType"]) {
143         case "JP2":
144             $bits = intval($tags["BitsPerComponent"]);
145             break;
146         case "TIFF":
147             $bits = intval($tags["BitsPerSample"]);
148             break;
149         case "JPEG":
150             $bits = 8;
151             break;
152         case "PNG":
153             $bits = intval($tags["BitDepth"]);
154             break;
155         default:
156             BRfatal("Unsupported image type");
157             break;
158     }
159    
160    
161     $retval = Array('width' => $width, 'height' => $height,
162         'bits' => $bits, 'type' => $type);
163     
164     return $retval;
165 }
166
167 /*
168  * Output JSON given the imageInfo associative array
169  */
170 function outputJSON($imageInfo, $callback)
171 {
172     header('Content-type: text/plain');
173     $jsonOutput = json_encode($imageInfo);
174     if ($callback) {
175         $jsonOutput = $callback . '(' . $jsonOutput . ');';
176     }
177     echo $jsonOutput;
178 }
179
180 // Get the image size and depth
181 $imageInfo = getImageInfo($zipPath, $file);
182
183 // Output json if requested
184 if ('json' == $ext) {
185     outputJSON($imageInfo, $callback);
186     exit;
187 }
188
189 // Unfortunately kakadu requires us to know a priori if the
190 // output file should be .ppm or .pgm.  By decompressing to
191 // .bmp kakadu will write a file we can consistently turn into
192 // .pnm.  Really kakadu should support .pnm as the file output
193 // extension and automatically write ppm or pgm format as
194 // appropriate.
195 $decompressToBmp = true;
196 if ($decompressToBmp) {
197   $stdoutLink = '/tmp/stdout.bmp';
198 } else {
199   $stdoutLink = '/tmp/stdout.ppm';
200 }
201
202 $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
203
204 // Rotate is currently only supported for jp2 since it does not add server load
205 $allowedRotations = array("0", "90", "180", "270");
206 $rotate = $_REQUEST['rotate'];
207 if ( !in_array($rotate, $allowedRotations) ) {
208     $rotate = "0";
209 }
210
211 // Image conversion options
212 $pngOptions = '';
213 $jpegOptions = '-quality 75';
214
215 // The pbmreduce reduction factor produces an image with dimension 1/n
216 // The kakadu reduction factor produceds an image with dimension 1/(2^n)
217
218 if (isset($_REQUEST['height'])) {
219     $ratio = floatval($_REQUEST['origHeight']) / floatval($_REQUEST['height']);
220     if ($ratio <= 2) {
221         $scale = 2;
222         $powReduce = 1;    
223     } else if ($ratio <= 4) {
224         $scale = 4;
225         $powReduce = 2;
226     } else {
227         //$powReduce = 3; //too blurry!
228         $scale = 2;
229         $powReduce = 1;
230     }
231
232 } else {
233     $scale = $_REQUEST['scale'];
234     if (1 >= $scale) {
235         $scale = 1;
236         $powReduce = 0;
237     } else if (2 == $scale) {
238         $powReduce = 1;
239     } else if (4 == $scale) {
240         $powReduce = 2;
241     } else if (8 == $scale) {
242         $powReduce = 3;
243     } else if (16 == $scale) {
244         $powReduce = 4;
245     } else if (32 == $scale) {
246         $powReduce = 5;
247     } else {
248         // $$$ Leaving this in as default though I'm not sure why it is...
249         $scale = 8;
250         $powReduce = 3;
251     }
252 }
253
254 if (!file_exists($stdoutLink)) 
255 {  
256   system('ln -s /dev/stdout ' . $stdoutLink);  
257 }
258
259
260 putenv('LD_LIBRARY_PATH=/petabox/sw/lib/kakadu');
261
262 $unzipCmd  = getUnarchiveCommand($zipPath, $file);
263         
264 if ('jp2' == $fileExt) {
265     $decompressCmd = 
266         " | /petabox/sw/bin/kdu_expand -no_seek -quiet -reduce $powReduce -rotate $rotate -i /dev/stdin -o " . $stdoutLink;
267     if ($decompressToBmp) {
268         $decompressCmd .= ' | bmptopnm ';
269     }
270     
271 } else if ('tif' == $fileExt) {
272     // We need to create a temporary file for tifftopnm since it cannot
273     // work on a pipe (the file must be seekable).
274     // We use the BookReaderTiff prefix to give a hint in case things don't
275     // get cleaned up.
276     $tempFile = tempnam("/tmp", "BookReaderTiff");
277
278     $pbmReduce = reduceCommand($scale);
279     
280     $decompressCmd = 
281         ' > ' . $tempFile . ' ; tifftopnm ' . $tempFile . ' 2>/dev/null' . $pbmReduce;
282         
283 } else if ('jpg' == $fileExt) {
284     $decompressCmd = ' | jpegtopnm ' . reduceCommand($scale);
285
286 } else if ('png' == $fileExt) {
287     $decompressCmd = ' | pngtopnm ' . reduceCommand($scale);
288     
289 } else {
290     BRfatal('Unknown source file extension: ' . $fileExt);
291 }
292        
293 // Non-integer scaling is currently disabled on the cluster
294 // if (isset($_REQUEST['height'])) {
295 //     $cmd .= " | pnmscale -height {$_REQUEST['height']} ";
296 // }
297
298 if ('jpg' == $ext) {
299     $compressCmd = ' | pnmtojpeg ' . $jpegOptions;
300 } else if ('png' == $ext) {
301     $compressCmd = ' | pnmtopng ' . $pngOptions;
302 }
303
304 if (($ext == $fileExt) && ($scale == 1) && ($rotate === "0")) {
305     // Just pass through original data if same format and size
306     $cmd = $unzipCmd;
307 } else {
308     $cmd = $unzipCmd . $decompressCmd . $compressCmd;
309 }
310
311 # print $cmd;
312
313
314 header('Content-type: ' . $MIMES[$ext]);
315 header('Cache-Control: max-age=15552000');
316 passthru ($cmd); # cmd returns image data
317
318 if (isset($tempFile)) {
319   unlink($tempFile);
320 }
321
322 function BRFatal($string) {
323     echo "alert('$string');\n";
324     die(-1);
325 }
326
327 // Returns true if using a power node
328 function onPowerNode() {
329     exec("lspci | fgrep -c Realtek", $output, $return);
330     if ("0" != $output[0]) {
331         return true;
332     } else {
333         exec("egrep -q AMD /proc/cpuinfo", $output, $return);
334         if ($return == 0) {
335             return true;
336         }
337     }
338     return false;
339 }
340
341 function reduceCommand($scale) {
342     if (1 != $scale) {
343         if (onPowerNode()) {
344             return ' | pnmscale -reduce ' . $scale;
345         } else {
346             return ' | pnmscale -nomix -reduce ' . $scale;
347         }
348     } else {
349         return '';
350     }
351 }
352
353
354 ?>
355