4 Copyright(c) 2008-2010 Internet Archive. Software license AGPL version 3.
6 This file is part of BookReader. The full source code can be found at GitHub:
7 http://github.com/openlibrary/bookreader
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".
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.
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.
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/>.
27 $MIMES = array('gif' => 'image/gif',
29 'jpg' => 'image/jpeg',
30 'jpeg' => 'image/jpeg',
32 'tif' => 'image/tiff',
33 'tiff' => 'image/tiff');
35 $EXTENSIONS = array('gif' => 'gif',
43 $exiftool = '/petabox/sw/books/exiftool/exiftool';
45 // Process some of the request parameters
46 $zipPath = $_REQUEST['zip'];
47 $file = $_REQUEST['file'];
48 if (isset($_REQUEST['ext'])) {
49 $ext = $_REQUEST['ext'];
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');
68 * Get info about requested image (input)
69 * Get info about requested output format
70 * Determine processing parameters
73 * Clean up temporary files
76 function getUnarchiveCommand($archivePath, $file)
78 $lowerPath = strtolower($archivePath);
79 if (preg_match('/\.([^\.]+)$/', $lowerPath, $matches)) {
80 $suffix = $matches[1];
82 if ($suffix == 'zip') {
84 . escapeshellarg($archivePath)
85 . ' ' . escapeshellarg($file);
86 } else if ($suffix == 'tar') {
88 . escapeshellarg($archivePath)
89 . ' ' . escapeshellarg($file);
91 BRfatal('Incompatible archive format');
95 BRfatal('Bad image stack path');
98 BRfatal('Bad image stack path or archive format');
103 * Returns the image type associated with the file extension.
105 function imageExtensionToType($extension)
109 if (array_key_exists($extension, $EXTENSIONS)) {
110 return $EXTENSIONS[$extension];
112 BRfatal('Unknown image extension');
117 * Get the image width, height and depth from a jp2 file in zip.
119 function getImageInfo($zipPath, $file)
123 $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
124 $type = imageExtensionToType($fileExt);
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
133 $cmd = getUnarchiveCommand($zipPath, $file)
134 . ' | '. $exiftool . ' -S -fast' . $tagsToGet . ' -';
138 foreach ($output as $line) {
139 $keyValue = explode(": ", $line);
140 $tags[$keyValue[0]] = $keyValue[1];
143 $width = intval($tags["ImageWidth"]);
144 $height = intval($tags["ImageHeight"]);
145 $type = strtolower($tags["FileType"]);
149 $bits = intval($tags["BitsPerComponent"]);
152 $bits = intval($tags["BitsPerSample"]);
158 $bits = intval($tags["BitDepth"]);
161 BRfatal("Unsupported image type");
166 $retval = Array('width' => $width, 'height' => $height,
167 'bits' => $bits, 'type' => $type);
173 * Output JSON given the imageInfo associative array
175 function outputJSON($imageInfo, $callback)
177 header('Content-type: text/plain');
178 $jsonOutput = json_encode($imageInfo);
180 $jsonOutput = $callback . '(' . $jsonOutput . ');';
185 // Get the image size and depth
186 $imageInfo = getImageInfo($zipPath, $file);
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);
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
201 $decompressToBmp = true;
202 if ($decompressToBmp) {
203 $stdoutLink = '/tmp/stdout.bmp';
205 $stdoutLink = '/tmp/stdout.ppm';
208 $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
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) ) {
217 // Image conversion options
219 $jpegOptions = '-quality 75';
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']);
229 } else if ($ratio <= 4) {
233 //$powReduce = 3; //too blurry!
239 // $$$ could be cleaner
240 $scale = intval($_REQUEST['scale']);
244 } else if (2 > $scale) {
247 } else if (4 > $scale) {
250 } else if (8 > $scale) {
253 } else if (16 > $scale) {
256 } else if (32 > $scale) {
259 } else if (64 > $scale) {
263 // $$$ Leaving this in as default though I'm not sure why it is...
269 // Override depending on source image format
270 // $$$ consider doing a 302 here instead, to make better use of the browser cache
271 // Limit scaling for 1-bit images. See https://bugs.edge.launchpad.net/bookreader/+bug/486011
272 if (1 == $imageInfo['bits']) {
277 // Hard limit so there are some black pixels to use!
285 if (!file_exists($stdoutLink))
287 system('ln -s /dev/stdout ' . $stdoutLink);
291 putenv('LD_LIBRARY_PATH=/petabox/sw/lib/kakadu');
293 $unzipCmd = getUnarchiveCommand($zipPath, $file);
295 switch ($imageInfo['type']) {
298 " | /petabox/sw/bin/kdu_expand -no_seek -quiet -reduce $powReduce -rotate $rotate -i /dev/stdin -o " . $stdoutLink;
299 if ($decompressToBmp) {
300 $decompressCmd .= ' | bmptopnm ';
305 // We need to create a temporary file for tifftopnm since it cannot
306 // work on a pipe (the file must be seekable).
307 // We use the BookReaderTiff prefix to give a hint in case things don't
309 $tempFile = tempnam("/tmp", "BookReaderTiff");
311 // $$$ look at bit depth when reducing
313 ' > ' . $tempFile . ' ; tifftopnm ' . $tempFile . ' 2>/dev/null' . reduceCommand($scale);
317 $decompressCmd = ' | jpegtopnm ' . reduceCommand($scale);
321 $decompressCmd = ' | pngtopnm ' . reduceCommand($scale);
325 BRfatal('Unknown source file extension: ' . $fileExt);
329 // Non-integer scaling is currently disabled on the cluster
330 // if (isset($_REQUEST['height'])) {
331 // $cmd .= " | pnmscale -height {$_REQUEST['height']} ";
336 $compressCmd = ' | pnmtopng ' . $pngOptions;
342 $compressCmd = ' | pnmtojpeg ' . $jpegOptions;
343 $ext = 'jpeg'; // for matching below
348 if (($ext == $fileExt) && ($scale == 1) && ($rotate === "0")) {
349 // Just pass through original data if same format and size
352 $cmd = $unzipCmd . $decompressCmd . $compressCmd;
358 // $$$ investigate how to flush cache when this file is changed
359 header('Content-type: ' . $MIMES[$ext]);
360 header('Cache-Control: max-age=15552000');
361 passthru ($cmd); # cmd returns image data
363 if (isset($tempFile)) {
367 function BRFatal($string) {
368 echo "alert('$string');\n";
372 // Returns true if using a power node
373 function onPowerNode() {
374 exec("lspci | fgrep -c Realtek", $output, $return);
375 if ("0" != $output[0]) {
378 exec("egrep -q AMD /proc/cpuinfo", $output, $return);
386 function reduceCommand($scale) {
389 return ' | pnmscale -reduce ' . $scale;
391 return ' | pnmscale -nomix -reduce ' . $scale;