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