Limit scaling of 1-bit images for more legible text!
[bookreader.git] / BookReaderIA / datanode / BookReaderImages.php
index d009557..35a67e0 100644 (file)
@@ -5,6 +5,10 @@ Copyright(c)2008 Internet Archive. Software license AGPL version 3.
 
 This file is part of BookReader.
 
+The canonical short name of an image type is the same as in the MIME type.
+For example both .jpeg and .jpg are considered to have type "jpeg" since
+the MIME type is "image/jpeg".
+
     BookReader is free software: you can redistribute it and/or modify
     it under the terms of the GNU Affero General Public License as published by
     the Free Software Foundation, either version 3 of the License, or
@@ -19,13 +23,39 @@ This file is part of BookReader.
     along with BookReader.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-$MIMES = array('jpg' => 'image/jpeg',
-               'png' => 'image/png');
+$MIMES = array('gif' => 'image/gif',
+               'jp2' => 'image/jp2',
+               'jpg' => 'image/jpeg',
+               'jpeg' => 'image/jpeg',
+               'png' => 'image/png',
+               'tif' => 'image/tiff',
+               'tiff' => 'image/tiff');
+               
+$EXTENSIONS = array('gif' => 'gif',
+                    'jp2' => 'jp2',
+                    'jpeg' => 'jpeg',
+                    'jpg' => 'jpeg',
+                    'png' => 'png',
+                    'tif' => 'tiff',
+                    'tiff' => 'tiff');
                
 $exiftool = '/petabox/sw/books/exiftool/exiftool';
 
+// Process some of the request parameters
 $zipPath  = $_REQUEST['zip'];
 $file     = $_REQUEST['file'];
+if (isset($_REQUEST['ext'])) {
+  $ext = $_REQUEST['ext'];
+} else {
+  // Default to jpg
+  $ext = 'jpg';
+}
+if (isset($_REQUEST['callback'])) {
+  // XXX sanitize
+  $callback = $_REQUEST['callback'];
+} else {
+  $callback = null;
+}
 
 /*
  * Approach:
@@ -52,34 +82,111 @@ function getUnarchiveCommand($archivePath, $file)
             return '7z e -so '
                 . escapeshellarg($archivePath)
                 . ' ' . escapeshellarg($file);
+        } else {
+            BRfatal('Incompatible archive format');
         }
 
+    } else {
+        BRfatal('Bad image stack path');
     }
     
-    BRfatal('Incompatible image stack path');
+    BRfatal('Bad image stack path or archive format');
     
 }
+
+/*
+ * Returns the image type associated with the file extension.
+ */
+function imageExtensionToType($extension)
+{
+    global $EXTENSIONS;
+    
+    if (array_key_exists($extension, $EXTENSIONS)) {
+        return $EXTENSIONS[$extension];
+    } else {
+        BRfatal('Unknown image extension');
+    }            
+}
+
 /*
  * Get the image width, height and depth from a jp2 file in zip.
  */
-function getImageSizeAndDepth($zipPath, $file)
+function getImageInfo($zipPath, $file)
 {
     global $exiftool;
     
-    # $$$ will exiftool work for *all* of our images?
+    $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
+    $type = imageExtensionToType($fileExt);
+     
+    // We look for all the possible tags of interest then act on the
+    // ones presumed present based on the file type
+    $tagsToGet = ' -ImageWidth -ImageHeight -FileType'        // all formats
+                 . ' -BitsPerComponent -ColorSpace'          // jp2
+                 . ' -BitDepth'                              // png
+                 . ' -BitsPerSample';                        // tiff
+                        
     $cmd = getUnarchiveCommand($zipPath, $file)
-        . ' | '. $exiftool . ' -s -s -s -ImageWidth -ImageHeight -BitsPerComponent -';
+        . ' | '. $exiftool . ' -S -fast' . $tagsToGet . ' -';
     exec($cmd, $output);
     
-    preg_match('/^(\d+)/', $output[2], $groups);
-    $bits = intval($groups[1]);
+    $tags = Array();
+    foreach ($output as $line) {
+        $keyValue = explode(": ", $line);
+        $tags[$keyValue[0]] = $keyValue[1];
+    }
+    
+    $width = intval($tags["ImageWidth"]);
+    $height = intval($tags["ImageHeight"]);
+    $type = strtolower($tags["FileType"]);
+    
+    switch ($type) {
+        case "jp2":
+            $bits = intval($tags["BitsPerComponent"]);
+            break;
+        case "tiff":
+            $bits = intval($tags["BitsPerSample"]);
+            break;
+        case "jpeg":
+            $bits = 8;
+            break;
+        case "png":
+            $bits = intval($tags["BitDepth"]);
+            break;
+        default:
+            BRfatal("Unsupported image type");
+            break;
+    }
+   
+   
+    $retval = Array('width' => $width, 'height' => $height,
+        'bits' => $bits, 'type' => $type);
     
-    $retval = Array('width' => intval($output[0]), 'height' => intval($output[1]),
-        'bits' => $bits);    
     return $retval;
 }
 
+/*
+ * Output JSON given the imageInfo associative array
+ */
+function outputJSON($imageInfo, $callback)
+{
+    header('Content-type: text/plain');
+    $jsonOutput = json_encode($imageInfo);
+    if ($callback) {
+        $jsonOutput = $callback . '(' . $jsonOutput . ');';
+    }
+    echo $jsonOutput;
+}
+
+// Get the image size and depth
+$imageInfo = getImageInfo($zipPath, $file);
+
+// Output json if requested
+if ('json' == $ext) {
+    // $$$ we should determine the output size first based on requested scale
+    outputJSON($imageInfo, $callback);
+    exit;
+}
+
 // Unfortunately kakadu requires us to know a priori if the
 // output file should be .ppm or .pgm.  By decompressing to
 // .bmp kakadu will write a file we can consistently turn into
@@ -93,13 +200,6 @@ if ($decompressToBmp) {
   $stdoutLink = '/tmp/stdout.ppm';
 }
 
-if (isset($_REQUEST['ext'])) {
-  $ext = $_REQUEST['ext'];
-} else {
-  // Default to jpg
-  $ext = 'jpg';
-}
-
 $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
 
 // Rotate is currently only supported for jp2 since it does not add server load
@@ -115,7 +215,7 @@ $jpegOptions = '-quality 75';
 
 // The pbmreduce reduction factor produces an image with dimension 1/n
 // The kakadu reduction factor produceds an image with dimension 1/(2^n)
-
+// $$$ handle continuous values for scale
 if (isset($_REQUEST['height'])) {
     $ratio = floatval($_REQUEST['origHeight']) / floatval($_REQUEST['height']);
     if ($ratio <= 2) {
@@ -152,6 +252,22 @@ if (isset($_REQUEST['height'])) {
     }
 }
 
+// Override depending on source image format
+// $$$ consider doing a 302 here instead, to make better use of the browser cache
+// Limit scaling for 1-bit images.  See https://bugs.edge.launchpad.net/bookreader/+bug/486011
+if (1 == $imageInfo['bits']) {
+    if ($scale > 1) {
+        $scale /= 2;
+        $powReduce -= 1;
+        
+        // Hard limit so there are some black pixels to use!
+        if ($scale > 4) {
+            $scale = 4;
+            $powReduce = 2;
+        }
+    }
+}
+
 if (!file_exists($stdoutLink)) 
 {  
   system('ln -s /dev/stdout ' . $stdoutLink);  
@@ -162,6 +278,7 @@ putenv('LD_LIBRARY_PATH=/petabox/sw/lib/kakadu');
 
 $unzipCmd  = getUnarchiveCommand($zipPath, $file);
         
+// XXX look at normalized type in imageinfo
 if ('jp2' == $fileExt) {
     $decompressCmd = 
         " | /petabox/sw/bin/kdu_expand -no_seek -quiet -reduce $powReduce -rotate $rotate -i /dev/stdin -o " . $stdoutLink;
@@ -175,20 +292,17 @@ if ('jp2' == $fileExt) {
     // We use the BookReaderTiff prefix to give a hint in case things don't
     // get cleaned up.
     $tempFile = tempnam("/tmp", "BookReaderTiff");
-    
-    if (1 != $scale) {
-        if (onPowerNode()) {
-            $pbmReduce = ' | pnmscale -reduce ' . $scale;
-        } else {
-            $pbmReduce = ' | pnmscale -nomix -reduce ' . $scale;
-        }
-    } else {
-        $pbmReduce = '';
-    }
-    
+
+    // $$$ look at bit depth when reducing
     $decompressCmd = 
-        ' > ' . $tempFile . ' ; tifftopnm ' . $tempFile . ' 2>/dev/null' . $pbmReduce;
+        ' > ' . $tempFile . ' ; tifftopnm ' . $tempFile . ' 2>/dev/null' . reduceCommand($scale);
+        
+} else if ('jpg' == $fileExt) {
+    $decompressCmd = ' | jpegtopnm ' . reduceCommand($scale);
 
+} else if ('png' == $fileExt) {
+    $decompressCmd = ' | pngtopnm ' . reduceCommand($scale);
+    
 } else {
     BRfatal('Unknown source file extension: ' . $fileExt);
 }
@@ -204,11 +318,17 @@ if ('jpg' == $ext) {
     $compressCmd = ' | pnmtopng ' . $pngOptions;
 }
 
-$cmd = $unzipCmd . $decompressCmd . $compressCmd;
+if (($ext == $fileExt) && ($scale == 1) && ($rotate === "0")) {
+    // Just pass through original data if same format and size
+    $cmd = $unzipCmd;
+} else {
+    $cmd = $unzipCmd . $decompressCmd . $compressCmd;
+}
 
 # print $cmd;
 
 
+// $$$ investigate how to flush cache when this file is changed
 header('Content-type: ' . $MIMES[$ext]);
 header('Cache-Control: max-age=15552000');
 passthru ($cmd); # cmd returns image data
@@ -218,7 +338,7 @@ if (isset($tempFile)) {
 }
 
 function BRFatal($string) {
-    echo "alert('$string')\n";
+    echo "alert('$string');\n";
     die(-1);
 }
 
@@ -236,6 +356,18 @@ function onPowerNode() {
     return false;
 }
 
+function reduceCommand($scale) {
+    if (1 != $scale) {
+        if (onPowerNode()) {
+            return ' | pnmscale -reduce ' . $scale;
+        } else {
+            return ' | pnmscale -nomix -reduce ' . $scale;
+        }
+    } else {
+        return '';
+    }
+}
+
 
 ?>