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 // Paths to command-line tools
44 $exiftool = '/petabox/sw/books/exiftool/exiftool';
45 $kduExpand = '/petabox/sw/bin/kdu_expand';
50 * Get info about requested image (input)
51 * Get info about requested output format
52 * Determine processing parameters
55 * Clean up temporary files
59 // Process some of the request parameters
60 $zipPath = $_REQUEST['zip'];
61 $file = $_REQUEST['file'];
62 if (isset($_REQUEST['ext'])) {
63 $ext = $_REQUEST['ext'];
68 if (isset($_REQUEST['callback'])) {
69 // validate callback is valid JS identifier (only)
70 $callback = $_REQUEST['callback'];
71 $identifierPatt = '/^[[:alpha:]$_]([[:alnum:]$_])*$/';
72 if (! preg_match($identifierPatt, $callback)) {
73 BRfatal('Invalid callback');
79 // Make sure the image stack is readable - return 403 if not
84 // Get the image size and depth
85 $imageInfo = getImageInfo($zipPath, $file);
87 // Output json if requested
89 // $$$ we should determine the output size first based on requested scale
90 outputJSON($imageInfo, $callback);
94 // Unfortunately kakadu requires us to know a priori if the
95 // output file should be .ppm or .pgm. By decompressing to
96 // .bmp kakadu will write a file we can consistently turn into
97 // .pnm. Really kakadu should support .pnm as the file output
98 // extension and automatically write ppm or pgm format as
100 $decompressToBmp = true;
101 if ($decompressToBmp) {
102 $stdoutLink = '/tmp/stdout.bmp';
104 $stdoutLink = '/tmp/stdout.ppm';
107 $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
109 // Rotate is currently only supported for jp2 since it does not add server load
110 $allowedRotations = array("0", "90", "180", "270");
111 $rotate = $_REQUEST['rotate'];
112 if ( !in_array($rotate, $allowedRotations) ) {
116 // Image conversion options
118 $jpegOptions = '-quality 75';
120 // The pbmreduce reduction factor produces an image with dimension 1/n
121 // The kakadu reduction factor produceds an image with dimension 1/(2^n)
122 // $$$ handle continuous values for scale
123 if (isset($_REQUEST['height'])) {
124 $ratio = floatval($_REQUEST['origHeight']) / floatval($_REQUEST['height']);
128 } else if ($ratio <= 4) {
132 //$powReduce = 3; //too blurry!
138 // $$$ could be cleaner
139 // Provide next smaller power of two reduction
140 $scale = intval($_REQUEST['scale']);
143 } else if (2 > $scale) {
145 } else if (4 > $scale) {
147 } else if (8 > $scale) {
149 } else if (16 > $scale) {
151 } else if (32 > $scale) {
153 } else if (64 > $scale) {
156 // $$$ Leaving this in as default though I'm not sure why it is...
159 $scale = pow(2, $powReduce);
162 // Override depending on source image format
163 // $$$ consider doing a 302 here instead, to make better use of the browser cache
164 // Limit scaling for 1-bit images. See https://bugs.edge.launchpad.net/bookreader/+bug/486011
165 if (1 == $imageInfo['bits']) {
170 // Hard limit so there are some black pixels to use!
178 if (!file_exists($stdoutLink))
180 system('ln -s /dev/stdout ' . $stdoutLink);
184 putenv('LD_LIBRARY_PATH=/petabox/sw/lib/kakadu');
186 $unzipCmd = getUnarchiveCommand($zipPath, $file);
188 $decompressCmd = getDecompressCmd($imageInfo['type']);
190 // Non-integer scaling is currently disabled on the cluster
191 // if (isset($_REQUEST['height'])) {
192 // $cmd .= " | pnmscale -height {$_REQUEST['height']} ";
197 $compressCmd = ' | pnmtopng ' . $pngOptions;
203 $compressCmd = ' | pnmtojpeg ' . $jpegOptions;
204 $ext = 'jpeg'; // for matching below
209 if (($ext == $fileExt) && ($scale == 1) && ($rotate === "0")) {
210 // Just pass through original data if same format and size
213 $cmd = $unzipCmd . $decompressCmd . $compressCmd;
218 $headers = array('Content-type: '. $MIMES[$ext],
219 'Cache-Control: max-age=15552000');
222 if (! passthruIfSuccessful($headers, $cmd, $errorMessage)) {
223 // $$$ automated reporting
224 trigger_error('BookReader Processing Error: ' . $cmd . ' -- ' . $errorMessage, E_USER_WARNING);
226 // Try some content-specific recovery
228 if ($imageInfo['type'] == 'jp2') {
229 $records = getJp2Records($zipPath, $file);
230 if ($powReduce > intval($records['Clevels'])) {
231 $powReduce = $records['Clevels'];
232 $reduce = pow(2, $powReduce);
238 $cmd = $unzipCmd . getDecompressCmd($imageInfo['type']) . $compressCmd;
239 if (passthruIfSuccessful($headers, $cmd, $errorMessage)) {
242 trigger_error('BookReader fallback image processing also failed: ' . $errorMessage, E_USER_WARNING);
247 BRfatal('Problem processing image - command failed');
251 if (isset($tempFile)) {
257 ////////////////////////////////////////////////
260 function getUnarchiveCommand($archivePath, $file)
262 $lowerPath = strtolower($archivePath);
263 if (preg_match('/\.([^\.]+)$/', $lowerPath, $matches)) {
264 $suffix = $matches[1];
266 if ($suffix == 'zip') {
268 . escapeshellarg($archivePath)
269 . ' ' . escapeshellarg($file);
270 } else if ($suffix == 'tar') {
271 return ' ( 7z e -so '
272 . escapeshellarg($archivePath)
273 . ' ' . escapeshellarg($file) . ' 2>/dev/null ) ';
275 BRfatal('Incompatible archive format');
279 BRfatal('Bad image stack path');
282 BRfatal('Bad image stack path or archive format');
287 * Returns the image type associated with the file extension.
289 function imageExtensionToType($extension)
293 if (array_key_exists($extension, $EXTENSIONS)) {
294 return $EXTENSIONS[$extension];
296 BRfatal('Unknown image extension');
301 * Get the image information. The returned associative array fields will
302 * vary depending on the image type. The basic keys are width, height, type
305 function getImageInfo($zipPath, $file)
307 return getImageInfoFromExif($zipPath, $file); // this is fast
310 $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
311 $type = imageExtensionToType($fileExt);
315 return getImageInfoFromJp2($zipPath, $file);
318 return getImageInfoFromExif($zipPath, $file);
323 // Get the records of of JP2 as returned by kdu_expand
324 function getJp2Records($zipPath, $file)
328 $cmd = getUnarchiveCommand($zipPath, $file)
330 . ' -no_seek -quiet -i /dev/stdin -record /dev/stdout';
334 foreach ($output as $line) {
335 $elems = explode("=", $line, 2);
336 if (1 == count($elems)) {
337 // delimiter not found
340 $records[$elems[0]] = $elems[1];
347 * Get the image width, height and depth using the EXIF information.
349 function getImageInfoFromExif($zipPath, $file)
353 // We look for all the possible tags of interest then act on the
354 // ones presumed present based on the file type
355 $tagsToGet = ' -ImageWidth -ImageHeight -FileType' // all formats
356 . ' -BitsPerComponent -ColorSpace' // jp2
357 . ' -BitDepth' // png
358 . ' -BitsPerSample'; // tiff
360 $cmd = getUnarchiveCommand($zipPath, $file)
361 . ' | '. $exiftool . ' -S -fast' . $tagsToGet . ' -';
365 foreach ($output as $line) {
366 $keyValue = explode(": ", $line);
367 $tags[$keyValue[0]] = $keyValue[1];
370 $width = intval($tags["ImageWidth"]);
371 $height = intval($tags["ImageHeight"]);
372 $type = strtolower($tags["FileType"]);
376 $bits = intval($tags["BitsPerComponent"]);
379 $bits = intval($tags["BitsPerSample"]);
385 $bits = intval($tags["BitDepth"]);
388 BRfatal("Unsupported image type");
393 $retval = Array('width' => $width, 'height' => $height,
394 'bits' => $bits, 'type' => $type);
400 * Output JSON given the imageInfo associative array
402 function outputJSON($imageInfo, $callback)
404 header('Content-type: text/plain');
405 $jsonOutput = json_encode($imageInfo);
407 $jsonOutput = $callback . '(' . $jsonOutput . ');';
412 function getDecompressCmd($imageType) {
414 global $powReduce, $rotate, $scale; // $$$ clean up
415 global $decompressToBmp; // $$$ TODO remove now that we have bit depth info
418 switch ($imageType) {
421 " | " . $kduExpand . " -no_seek -quiet -reduce $powReduce -rotate $rotate -i /dev/stdin -o " . $stdoutLink;
422 if ($decompressToBmp) {
423 // We suppress output since bmptopnm always outputs on stderr
424 $decompressCmd .= ' | (bmptopnm 2>/dev/null)';
429 // We need to create a temporary file for tifftopnm since it cannot
430 // work on a pipe (the file must be seekable).
431 // We use the BookReaderTiff prefix to give a hint in case things don't
433 $tempFile = tempnam("/tmp", "BookReaderTiff");
435 // $$$ look at bit depth when reducing
437 ' > ' . $tempFile . ' ; tifftopnm ' . $tempFile . ' 2>/dev/null' . reduceCommand($scale);
441 $decompressCmd = ' | ( jpegtopnm 2>/dev/null ) ' . reduceCommand($scale);
445 $decompressCmd = ' | ( pngtopnm 2>/dev/null ) ' . reduceCommand($scale);
449 BRfatal('Unknown image type: ' . $imageType);
452 return $decompressCmd;
455 // If the command has its initial output on stdout the headers will be emitted followed
456 // by the stdout output. If initial output is on stderr an error message will be
460 // true - if command emits stdout and has zero exit code
461 // false - command has initial output on stderr or non-zero exit code
462 // &$errorMessage - error string if there was an error
464 // $$$ Tested with our command-line image processing. May be deadlocks for
466 function passthruIfSuccessful($headers, $cmd, &$errorMessage)
471 $descriptorspec = array(
472 0 => array("pipe", "r"), // stdin is a pipe that the child will read from
473 1 => array("pipe", "w"), // stdout is a pipe that the child will write to
474 2 => array("pipe", "w"), // stderr is a pipe to write to
480 $process = proc_open($cmd, $descriptorspec, $pipes, $cwd, $env);
482 if (is_resource($process)) {
483 // $pipes now looks like this:
484 // 0 => writeable handle connected to child stdin
485 // 1 => readable handle connected to child stdout
486 // 2 => readable handle connected to child stderr
492 // check whether we get input first on stdout or stderr
493 $read = array($stdout, $stderr);
496 $numChanged = stream_select($read, $write, $except, NULL); // $$$ no timeout
497 if (false === $numChanged) {
499 $errorMessage = 'Select failed';
502 if ($read[0] == $stdout && (1 == $numChanged)) {
503 // Got output first on stdout (only)
504 // $$$ make sure we get all stdout
505 $output = fopen('php://output', 'w');
506 foreach($headers as $header) {
509 stream_copy_to_stream($pipes[1], $output);
510 fclose($output); // okay since tied to special php://output
513 // Got output on stderr
514 // $$$ make sure we get all stderr
515 $errorMessage = stream_get_contents($stderr);
524 // It is important that you close any pipes before calling
525 // proc_close in order to avoid a deadlock
526 $cmdRet = proc_close($process);
529 $errorMessage .= "Command failed with result code " . $cmdRet;
535 function BRFatal($string) {
536 echo "alert('$string');\n";
540 // Returns true if using a power node
541 function onPowerNode() {
542 exec("lspci | fgrep -c Realtek", $output, $return);
543 if ("0" != $output[0]) {
546 exec("egrep -q AMD /proc/cpuinfo", $output, $return);
554 function reduceCommand($scale) {
557 return ' | pnmscale -reduce ' . $scale . ' 2>/dev/null ';
559 return ' | pnmscale -nomix -reduce ' . $scale . ' 2>/dev/null ';
566 function checkPrivs($filename) {
567 if (!is_readable($filename)) {
568 header('HTTP/1.1 403 Forbidden');