4 Copyright(c)2008 Internet Archive. Software license AGPL version 3.
6 This file is part of BookReader.
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".
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.
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.
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/>.
26 $MIMES = array('gif' => 'image/gif',
28 'jpg' => 'image/jpeg',
29 'jpeg' => 'image/jpeg',
31 'tif' => 'image/tiff',
32 'tiff' => 'image/tiff');
34 $EXTENSIONS = array('gif' => 'gif',
42 $exiftool = '/petabox/sw/books/exiftool/exiftool';
44 // Process some of the request parameters
45 $zipPath = $_REQUEST['zip'];
46 $file = $_REQUEST['file'];
47 if (isset($_REQUEST['ext'])) {
48 $ext = $_REQUEST['ext'];
53 if (isset($_REQUEST['callback'])) {
54 // validate callback is valid JS identifier (only)
55 $callback = $_REQUEST['callback'];
56 $identifierPatt = '/^[[:alpha:]$_]([[:alnum:]$_])*$/';
57 if (! preg_match($identifierPatt, $callback)) {
58 BRfatal('Invalid callback');
67 * Get info about requested image (input)
68 * Get info about requested output format
69 * Determine processing parameters
72 * Clean up temporary files
75 function getUnarchiveCommand($archivePath, $file)
77 $lowerPath = strtolower($archivePath);
78 if (preg_match('/\.([^\.]+)$/', $lowerPath, $matches)) {
79 $suffix = $matches[1];
81 if ($suffix == 'zip') {
83 . escapeshellarg($archivePath)
84 . ' ' . escapeshellarg($file);
85 } else if ($suffix == 'tar') {
87 . escapeshellarg($archivePath)
88 . ' ' . escapeshellarg($file);
90 BRfatal('Incompatible archive format');
94 BRfatal('Bad image stack path');
97 BRfatal('Bad image stack path or archive format');
102 * Returns the image type associated with the file extension.
104 function imageExtensionToType($extension)
108 if (array_key_exists($extension, $EXTENSIONS)) {
109 return $EXTENSIONS[$extension];
111 BRfatal('Unknown image extension');
116 * Get the image width, height and depth from a jp2 file in zip.
118 function getImageInfo($zipPath, $file)
122 $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
123 $type = imageExtensionToType($fileExt);
125 // We look for all the possible tags of interest then act on the
126 // ones presumed present based on the file type
127 $tagsToGet = ' -ImageWidth -ImageHeight -FileType' // all formats
128 . ' -BitsPerComponent -ColorSpace' // jp2
129 . ' -BitDepth' // png
130 . ' -BitsPerSample'; // tiff
132 $cmd = getUnarchiveCommand($zipPath, $file)
133 . ' | '. $exiftool . ' -S -fast' . $tagsToGet . ' -';
137 foreach ($output as $line) {
138 $keyValue = explode(": ", $line);
139 $tags[$keyValue[0]] = $keyValue[1];
142 $width = intval($tags["ImageWidth"]);
143 $height = intval($tags["ImageHeight"]);
144 $type = strtolower($tags["FileType"]);
148 $bits = intval($tags["BitsPerComponent"]);
151 $bits = intval($tags["BitsPerSample"]);
157 $bits = intval($tags["BitDepth"]);
160 BRfatal("Unsupported image type");
165 $retval = Array('width' => $width, 'height' => $height,
166 'bits' => $bits, 'type' => $type);
172 * Output JSON given the imageInfo associative array
174 function outputJSON($imageInfo, $callback)
176 header('Content-type: text/plain');
177 $jsonOutput = json_encode($imageInfo);
179 $jsonOutput = $callback . '(' . $jsonOutput . ');';
184 // Get the image size and depth
185 $imageInfo = getImageInfo($zipPath, $file);
187 // Output json if requested
188 if ('json' == $ext) {
189 // $$$ we should determine the output size first based on requested scale
190 outputJSON($imageInfo, $callback);
194 // Unfortunately kakadu requires us to know a priori if the
195 // output file should be .ppm or .pgm. By decompressing to
196 // .bmp kakadu will write a file we can consistently turn into
197 // .pnm. Really kakadu should support .pnm as the file output
198 // extension and automatically write ppm or pgm format as
200 $decompressToBmp = true;
201 if ($decompressToBmp) {
202 $stdoutLink = '/tmp/stdout.bmp';
204 $stdoutLink = '/tmp/stdout.ppm';
207 $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
209 // Rotate is currently only supported for jp2 since it does not add server load
210 $allowedRotations = array("0", "90", "180", "270");
211 $rotate = $_REQUEST['rotate'];
212 if ( !in_array($rotate, $allowedRotations) ) {
216 // Image conversion options
218 $jpegOptions = '-quality 75';
220 // The pbmreduce reduction factor produces an image with dimension 1/n
221 // The kakadu reduction factor produceds an image with dimension 1/(2^n)
222 // $$$ handle continuous values for scale
223 if (isset($_REQUEST['height'])) {
224 $ratio = floatval($_REQUEST['origHeight']) / floatval($_REQUEST['height']);
228 } else if ($ratio <= 4) {
232 //$powReduce = 3; //too blurry!
238 $scale = $_REQUEST['scale'];
242 } else if (2 == $scale) {
244 } else if (4 == $scale) {
246 } else if (8 == $scale) {
248 } else if (16 == $scale) {
250 } else if (32 == $scale) {
253 // $$$ Leaving this in as default though I'm not sure why it is...
259 // Override depending on source image format
260 // $$$ consider doing a 302 here instead, to make better use of the browser cache
261 // Limit scaling for 1-bit images. See https://bugs.edge.launchpad.net/bookreader/+bug/486011
262 if (1 == $imageInfo['bits']) {
267 // Hard limit so there are some black pixels to use!
275 if (!file_exists($stdoutLink))
277 system('ln -s /dev/stdout ' . $stdoutLink);
281 putenv('LD_LIBRARY_PATH=/petabox/sw/lib/kakadu');
283 $unzipCmd = getUnarchiveCommand($zipPath, $file);
285 switch ($imageInfo['type']) {
288 " | /petabox/sw/bin/kdu_expand -no_seek -quiet -reduce $powReduce -rotate $rotate -i /dev/stdin -o " . $stdoutLink;
289 if ($decompressToBmp) {
290 $decompressCmd .= ' | bmptopnm ';
295 // We need to create a temporary file for tifftopnm since it cannot
296 // work on a pipe (the file must be seekable).
297 // We use the BookReaderTiff prefix to give a hint in case things don't
299 $tempFile = tempnam("/tmp", "BookReaderTiff");
301 // $$$ look at bit depth when reducing
303 ' > ' . $tempFile . ' ; tifftopnm ' . $tempFile . ' 2>/dev/null' . reduceCommand($scale);
307 $decompressCmd = ' | jpegtopnm ' . reduceCommand($scale);
311 $decompressCmd = ' | pngtopnm ' . reduceCommand($scale);
315 BRfatal('Unknown source file extension: ' . $fileExt);
319 // Non-integer scaling is currently disabled on the cluster
320 // if (isset($_REQUEST['height'])) {
321 // $cmd .= " | pnmscale -height {$_REQUEST['height']} ";
326 $compressCmd = ' | pnmtopng ' . $pngOptions;
332 $compressCmd = ' | pnmtojpeg ' . $jpegOptions;
333 $ext = 'jpeg'; // for matching below
338 if (($ext == $fileExt) && ($scale == 1) && ($rotate === "0")) {
339 // Just pass through original data if same format and size
342 $cmd = $unzipCmd . $decompressCmd . $compressCmd;
348 // $$$ investigate how to flush cache when this file is changed
349 header('Content-type: ' . $MIMES[$ext]);
350 header('Cache-Control: max-age=15552000');
351 passthru ($cmd); # cmd returns image data
353 if (isset($tempFile)) {
357 function BRFatal($string) {
358 echo "alert('$string');\n";
362 // Returns true if using a power node
363 function onPowerNode() {
364 exec("lspci | fgrep -c Realtek", $output, $return);
365 if ("0" != $output[0]) {
368 exec("egrep -q AMD /proc/cpuinfo", $output, $return);
376 function reduceCommand($scale) {
379 return ' | pnmscale -reduce ' . $scale;
381 return ' | pnmscale -nomix -reduce ' . $scale;