added jstore path
[bookreader.git] / BookReaderIA / datanode / BookReaderImages.inc.php
index 9957b40..ad56f20 100644 (file)
@@ -62,7 +62,7 @@ class BookReaderImages
         'h' => 'height',
         'x' => 'x',
         'y' => 'y',
-        'rotate' => 'rotate'
+        'rot' => 'rotate'
     );
     
     // Paths to command-line tools
@@ -98,7 +98,7 @@ class BookReaderImages
         $imageIndex = null;
 
         // deal with subPrefix
-        if ($_REQUEST['subPrefix']) {
+        if (array_key_exists($_REQUEST, 'subPrefix') && $_REQUEST['subPrefix']) {
             $parts = explode('/', $_REQUEST['subPrefix']);
             $bookId = $parts[count($parts) - 1 ];
         } else {
@@ -120,26 +120,24 @@ class BookReaderImages
                 $imageIndex = $metadata['titleIndex'];
                 break;
             
-            /* Old 'cover' behaviour where it would show cover 0 if it exists or return 404.
-               Could be re-added as cover0, cover1, etc
-            case 'cover':
+            /* Old 'cover' behaviour where it would show cover 0 if it exists or return 404. */
+            case 'cover0':
                 if (! array_key_exists('coverIndices', $metadata)) {
                     $this->BRfatal("No cover asserted in book");
                 }
                 $imageIndex = $metadata['coverIndices'][0]; // $$$ TODO add support for other covers
                 break;
-            */
             
             case 'preview':
             case 'cover': // Show our best guess if cover is requested
                 // Preference is:
-                //   Cover page if book was published >= 1950
+                //   Cover page if book was published >= 1923
                 //   Title page
                 //   Cover page
                 //   Page 0
                          
                 if ( array_key_exists('date', $metadata) && array_key_exists('coverIndices', $metadata) ) {
-                    if ($brm->parseYear($metadata['date']) >= 1950) {
+                    if ($brm->parseYear($metadata['date']) >= 1923) {
                         $imageIndex = $metadata['coverIndices'][0];                
                         break;
                     }
@@ -224,11 +222,14 @@ class BookReaderImages
      */
      function serveRequest($requestEnv) {
      
+        // Make sure cleanup happens
+        register_shutdown_function ( array( $this, 'cleanup') );
+     
         // Process some of the request parameters
         $zipPath  = $requestEnv['zip'];
         $file     = $requestEnv['file'];
-        if (! $ext) {
-            $ext = $requestEnv['ext'];
+        if (array_key_exists('ext', $requestEnv)) {
+            $ext = $requestEnv['ext']; // Will get santized below
         } else {
             // Default to jpg
             $ext = 'jpeg';
@@ -253,21 +254,7 @@ class BookReaderImages
         
         // Get the image size and depth
         $imageInfo = $this->getImageInfo($zipPath, $file);
-        
-        $region = array();
-        foreach (array('x', 'y', 'w', 'h') as $key) {
-            if (array_key_exists($key, $requestEnv)) {
-                $region[$key] = $requestEnv[$key];
-            }
-        }
-        $regionDimensions = $this->getRegionDimensions($imageInfo, $region);    
-        
-        /* $$$ remove
-        print_r($imageInfo);
-        print_r($region);
-        print_r($regionDimensions);
-        */
-        
+                
         // Output json if requested
         if ('json' == $ext) {
             // $$$ we should determine the output size first based on requested scale
@@ -310,15 +297,26 @@ class BookReaderImages
         // image processing load on our cluster.  The client should then scale to their final
         // needed size.
         
-        // Set scale from height or width if set
-        if (isset($requestEnv['height'])) {
-            $powReduce = $this->nearestPow2Reduce($requestEnv['height'], $imageInfo['height']);
-            $scale = pow(2, $powReduce);
-        } else if (isset($requestEnv['width'])) {
-            $powReduce = $this->nearestPow2Reduce($requestEnv['width'], $imageInfo['width']);
-            $scale = pow(2, $powReduce);
+        // Sizing logic:
+        //   If a named size is provided, we size the full image to that size
+        //   If x or y is set, we interpret the supplied width/height as the size of image region to crop to
+        //   If x and y are not set and both width and height are set, we size the full image "within" the width/height
+        //   If x and y are not set and only one of width and height are set, we size the full image to that width or height
+        //   If none of the above apply, we use the whole image
+        
+        // Crop region, if empty whole image is used
+        $region = array();
 
-        } else {
+        // Initialize scale        
+        $scale = 1;
+        if (isset($requestEnv['scale'])) {
+            $scale = $requestEnv['scale'];
+        }
+        $powReduce = $this->nearestPow2ForScale($scale);
+        // ensure integer scale
+        $scale = pow(2, $powReduce);
+        
+        if ( isset($requestEnv['size']) ) {
             // Set scale from named size (e.g. 'large') if set
             $size = $requestEnv['size'];
             if ( $size && array_key_exists($size, self::$imageSizes)) {
@@ -331,18 +329,60 @@ class BookReaderImages
                 }
                 $powReduce = $this->nearestPow2Reduce(self::$imageSizes[$size], $imageInfo[$dimension]);
                 $scale = pow(2, $powReduce);
-                
-            } else {
-                // No named size - use explicit scale, if given
-                $scale = $requestEnv['scale'];
-                if (!$scale) {
-                    $scale = 1;
+            }
+            
+        } else if ( isset($requestEnv['x']) || isset($requestEnv['y']) ) {
+            // x,y is crop region origin, width,height is size of crop region
+            foreach (array('x', 'y', 'width', 'height') as $key) {
+                if (array_key_exists($key, $requestEnv)) {
+                    $region[$key] = $requestEnv[$key];
                 }
-                $powReduce = $this->nearestPow2ForScale($scale);
-                // ensure integer scale
-                $scale = pow(2, $powReduce);
-            }            
+            }
+            
+        } else if ( isset($requestEnv['width']) && isset($requestEnv['height']) ) {
+            // proportional scaling within requested width/height
+            
+            $width = $this->intAmount($requestEnv['width'], $imageInfo['width']);
+            $height = $this->intAmount($requestEnv['height'], $imageInfo['height']);
+            
+            $srcAspect = floatval($imageInfo['width']) / floatval($imageInfo['height']);
+            $fitAspect = floatval($width) / floatval($height);
+            
+            if ($srcAspect > $fitAspect) {
+                // Source image is wide compared to fit
+                $powReduce = $this->nearestPow2Reduce($width, $imageInfo['width']);
+            } else {
+                $powReduce = $this->nearestPow2Reduce($height, $imageInfo['height']);
+            }
+            $scale = pow(2, $poweReduce);
+            
+        } else if ( isset($requestEnv['width']) ) {
+            // Fit within width
+            $width = $this->intAmount($requestEnv['width'], $imageInfo['width']);
+            $powReduce = $this->nearestPow2Reduce($width, $imageInfo['width']);
+            $scale = pow(2, $powReduce);        
+            
+        }   else if ( isset($requestEnv['height'])) {
+            // Fit within height
+            $height = $this->intAmount($requestEnv['height'], $imageInfo['height']);
+            $powReduce = $this->nearestPow2Reduce($height, $imageInfo['height']);
+            $scale = pow(2, $powReduce);
         }
+                
+        $regionDimensions = $this->getRegionDimensions($imageInfo, $region);    
+        
+        /*
+        print('imageInfo');
+        print_r($imageInfo);
+        print('region');
+        print_r($region);
+        print('regionDimensions');
+        print_r($regionDimensions);
+        print('asFloat');
+        print_r($this->getRegionDimensionsAsFloat($imageInfo, $region));
+        die(-1);
+        */
+
         
         // Override depending on source image format
         // $$$ consider doing a 302 here instead, to make better use of the browser cache
@@ -370,8 +410,8 @@ class BookReaderImages
         
         $unzipCmd  = $this->getUnarchiveCommand($zipPath, $file);
         
-        $decompressCmd = $this->getDecompressCmd($imageInfo['type'], $powReduce, $rotate, $scale, $stdoutLink);
-               
+        $decompressCmd = $this->getDecompressCmd($imageInfo, $powReduce, $rotate, $scale, $region, $stdoutLink);
+        
         // Non-integer scaling is currently disabled on the cluster
         // if (isset($_REQUEST['height'])) {
         //     $cmd .= " | pnmscale -height {$_REQUEST['height']} ";
@@ -401,15 +441,17 @@ class BookReaderImages
         // print $cmd;
         
         $filenameForClient = $this->filenameForClient($file, $ext);
-        
-        $headers = array('Content-type: '. self::$MIMES[$ext],
-                         'Cache-Control: max-age=15552000',
-                         'Content-disposition: inline; filename=' . $filenameForClient);
-                          
-        
+
         $errorMessage = '';
         
-        if (! $this->passthruIfSuccessful($headers, $cmd, $errorMessage)) { // $$$ move to BookReaderRequest
+        //if (! $this->passthruIfSuccessful($headers, $cmd, $errorMessage)) { // $$$ move to BookReaderRequest
+        
+        $tempFile = $this->getTempFilename($ext);
+        array_push($this->tempFiles, $tempFile);
+
+        // error_log("bookreader running " . $cmd);
+        $imageCreated = $this->createOutputImage($cmd, $tempFile, $errorMessage);
+        if (! $imageCreated) {
             // $$$ automated reporting
             trigger_error('BookReader Processing Error: ' . $cmd . ' -- ' . $errorMessage, E_USER_WARNING);
             
@@ -427,9 +469,13 @@ class BookReaderImages
                 $powReduce = min($powReduce, $maxReduce);
                 $reduce = pow(2, $powReduce);
                 
-                $cmd = $unzipCmd . $this->getDecompressCmd($imageInfo['type'], $powReduce, $rotate, $scale, $stdoutLink) . $compressCmd;
+                $cmd = $unzipCmd . $this->getDecompressCmd($imageInfo, $powReduce, $rotate, $scale, $region, $stdoutLink) . $compressCmd;
                 trigger_error('BookReader rerunning with new cmd: ' . $cmd, E_USER_WARNING);
-                if ($this->passthruIfSuccessful($headers, $cmd, $errorMessage)) { // $$$ move to BookReaderRequest
+                
+                $tempFile = $this->getTempFilename($ext);
+                array_push($this->tempFiles, $tempFile);
+                $imageCreated = $this->createOutputImage($cmd, $tempFile, $errorMessage);
+                if ($imageCreated) {
                     $recovered = true;
                 } else {
                     $this->cleanup();
@@ -438,10 +484,26 @@ class BookReaderImages
             }
             
             if (! $recovered) {
-                $this->BRfatal('Problem processing image - command failed');
+                $this->BRfatal("Problem processing image - command failed:\n " . $cmd);
             }
         }
         
+        if ($imageCreated) {
+            // Send the image
+                    
+            $headers = array('Content-type: '. self::$MIMES[$ext],
+                             'Cache-Control: max-age=15552000',
+                             'Content-disposition: inline; filename=' . $filenameForClient,
+                             'Content-Length: ' . filesize($tempFile));
+                             
+            foreach($headers as $header) {
+                header($header);
+            }
+            ob_clean();
+            flush(); // attempt to send header to client
+            readfile($tempFile);
+        }
+        
         $this->cleanup();
     }    
     
@@ -555,6 +617,11 @@ class BookReaderImages
         $width = intval($tags["ImageWidth"]);
         $height = intval($tags["ImageHeight"]);
         $type = strtolower($tags["FileType"]);
+
+        // Treat jpx as jp2
+        if (strcmp($type,'jpx') == 0) {
+            $type = 'jp2';
+        }
         
         switch ($type) {
             case "jp2":
@@ -594,18 +661,20 @@ class BookReaderImages
         echo $jsonOutput;
     }
     
-    function getDecompressCmd($imageType, $powReduce, $rotate, $scale, $stdoutLink) {
+    function getDecompressCmd($srcInfo, $powReduce, $rotate, $scale, $region, $stdoutLink) {
         
-        switch ($imageType) {
+        switch ($srcInfo['type']) {
             case 'jp2':
+                $regionAsFloat = $this->getRegionDimensionsAsFloat($srcInfo, $region);
+                $regionString = sprintf("{%f,%f},{%f,%f}", $regionAsFloat['y'], $regionAsFloat['x'], $regionAsFloat['h'], $regionAsFloat['w']);
                 $decompressCmd = 
-                    " | " . $this->kduExpand . " -no_seek -quiet -reduce $powReduce -rotate $rotate -i /dev/stdin -o " . $stdoutLink;
+                    " | " . $this->kduExpand . " -no_seek -quiet -reduce $powReduce -rotate $rotate -region $regionString -i /dev/stdin -o " . $stdoutLink;
                 if ($this->decompressToBmp) {
                     // We suppress output since bmptopnm always outputs on stderr
                     $decompressCmd .= ' | (bmptopnm 2>/dev/null)';
                 }
-                break;
-        
+                break;        
+/*
             case 'tiff':
                 // We need to create a temporary file for tifftopnm since it cannot
                 // work on a pipe (the file must be seekable).
@@ -626,14 +695,35 @@ class BookReaderImages
             case 'png':
                 $decompressCmd = ' | ( pngtopnm 2>/dev/null ) ' . $this->reduceCommand($scale);
                 break;
+*/
+
+            // Formats handled by ImageMagick
+            case 'tiff':
+            case 'jpeg':
+            case 'png':
+                $region = $this->getRegionDimensions($srcInfo, $region);
+                $regionString = sprintf('[%dx%d+%d+%d]', $region['w'], $region['h'], $region['x'], $region['y']);
+
+                // The argument to ImageMagick's scale command is a "geometry". We pass in the new width/height
+                $scaleString = ' -scale ' . sprintf("%dx%d", $region['w'] / $scale, $region['h'] / $scale);
+                
+                $rotateString = '';
+                if ($rotate && $rotate != '0') {
+                    $rotateString = ' -rotate ' . $rotate; // was previously checked to be a known value
+                }
+                
+                $decompressCmd = ' | convert -quiet -' . $regionString . $scaleString . $rotateString . ' pnm:-';
+                break;
                 
             default:
                 $this->BRfatal('Unknown image type: ' . $imageType);
                 break;
         }
+        
         return $decompressCmd;
     }
     
+    
     // If the command has its initial output on stdout the headers will be emitted followed
     // by the stdout output.  If initial output is on stderr an error message will be
     // returned.
@@ -644,9 +734,10 @@ class BookReaderImages
     //   &$errorMessage - error string if there was an error
     //
     // $$$ Tested with our command-line image processing.  May be deadlocks for
-    //     other cases.
+    //     other cases, e.g. if there are warnings on stderr
     function passthruIfSuccessful($headers, $cmd, &$errorMessage)
     {
+        
         $retVal = false;
         $errorMessage = '';
         
@@ -728,6 +819,12 @@ class BookReaderImages
         return $retVal;
     }
     
+    function createOutputImage($cmd, $tempFile, &$errorMessage) {
+        $fullCmd = $cmd . " > " . $tempFile;
+        system($fullCmd); // $$$ better error handling
+        return file_exists($tempFile) && filesize($tempFile) > 0;
+    }
+    
     function BRfatal($string) {
         $this->cleanup();
         throw new Exception("Image error: $string");
@@ -793,7 +890,9 @@ class BookReaderImages
             return 0;
         }
         $binStr = decbin($scale); // convert to binary string. e.g. 5 -> '101'
-        return strlen($binStr) - 1;
+        $largerPow2 = strlen($binStr) - 1;
+        
+        return $largerPow2;
     }
     
     /*
@@ -898,30 +997,62 @@ class BookReaderImages
 
         $sourceX = 0;
         if (array_key_exists('x', $regionDimensions)) {
-            $sourceX = intAmount($regionDimensions['x'], $sourceDimensions['width']);
+            $sourceX = $this->intAmount($regionDimensions['x'], $sourceDimensions['width']);
         }
         $sourceX = $this->clamp(0, $sourceDimensions['width'] - 2, $sourceX); // Allow at least one pixel
         
         $sourceY = 0;
         if (array_key_exists('y', $regionDimensions)) {
-            $sourceY = intAmount($regionDimensions['y'], $sourceDimensions['height']);
+            $sourceY = $this->intAmount($regionDimensions['y'], $sourceDimensions['height']);
         }
         $sourceY = $this->clamp(0, $sourceDimensions['height'] - 2, $sourceY); // Allow at least one pixel
         
         $sourceWidth = $sourceDimensions['width'] - $sourceX;
-        if (array_key_exists('w', $regionDimensions)) {
-            $sourceWidth = intAmount($regionDimensions['w'], $sourceDimensions['width']);
+        if (array_key_exists('width', $regionDimensions)) {
+            $sourceWidth = $this->intAmount($regionDimensions['width'], $sourceDimensions['width']);
         }
         $sourceWidth = $this->clamp(1, max(1, $sourceDimensions['width'] - $sourceX), $sourceWidth);
         
         $sourceHeight = $sourceDimensions['height'] - $sourceY;
-        if (array_key_exists('h', $regionDimensions)) {
-            $sourceHeight = intAmount($regionDimensions['h'], $sourceDimensions['height']);
+        if (array_key_exists('height', $regionDimensions)) {
+            $sourceHeight = $this->intAmount($regionDimensions['height'], $sourceDimensions['height']);
         }
         $sourceHeight = $this->clamp(1, max(1, $sourceDimensions['height'] - $sourceY), $sourceHeight);
         
         return array('x' => $sourceX, 'y' => $sourceY, 'w' => $sourceWidth, 'h' => $sourceHeight);
     }
+
+    function getRegionDimensionsAsFloat($sourceDimensions, $regionDimensions) {
+        // Return region dimensions as { 'x' => xOffset, 'y' => yOffset, 'w' => width, 'h' => height }
+        // in terms of full resolution image.
+        // Note: this will clip the returned dimensions to fit within the source image
+    
+        $sourceX = 0;
+        if (array_key_exists('x', $regionDimensions)) {
+            $sourceX = $this->floatAmount($regionDimensions['x'], $sourceDimensions['width']);
+        }
+        $sourceX = $this->clamp(0.0, 1.0, $sourceX);
+        
+        $sourceY = 0;
+        if (array_key_exists('y', $regionDimensions)) {
+            $sourceY = $this->floatAmount($regionDimensions['y'], $sourceDimensions['height']);
+        }
+        $sourceY = $this->clamp(0.0, 1.0, $sourceY);
+        
+        $sourceWidth = 1 - $sourceX;
+        if (array_key_exists('width', $regionDimensions)) {
+            $sourceWidth = $this->floatAmount($regionDimensions['width'], $sourceDimensions['width']);
+        }
+        $sourceWidth = $this->clamp(0.0, 1.0, $sourceWidth);
+        
+        $sourceHeight = 1 - $sourceY;
+        if (array_key_exists('height', $regionDimensions)) {
+            $sourceHeight = $this->floatAmount($regionDimensions['height'], $sourceDimensions['height']);
+        }
+        $sourceHeight = $this->clamp(0.0, 1.0, $sourceHeight);
+        
+        return array('x' => $sourceX, 'y' => $sourceY, 'w' => $sourceWidth, 'h' => $sourceHeight);
+    }
     
     function intAmount($stringValue, $maximum) {
         // Returns integer amount for string like "5" (5 units) or "0.5" (50%)
@@ -933,6 +1064,17 @@ class BookReaderImages
         return floatval($stringValue) * $maximum + 0.5;
     }
     
+    function floatAmount($stringValue, $maximum) {
+        // Returns float amount (0.0 to 1.0) for string like "0.4" (40%) or "4" (40% if max is 10)
+        if (strpos($stringValue, ".") === false) {
+            // No decimal, assume int value out of maximum
+            return floatval($stringValue) / $maximum;
+        }
+        
+        // Given float - just pass through
+        return floatval($stringValue);
+    }
+    
     function clamp($minValue, $maxValue, $observedValue) {
         if ($observedValue < $minValue) {
             return $minValue;
@@ -945,6 +1087,33 @@ class BookReaderImages
         return $observedValue;
     }
     
+    // Get the directory for temporary files. Use the fast in-RAM tmp if available.
+    function getTempDir() {
+        $fastbr = '/var/tmp/fast/bookreader';
+        
+        if (is_writeable($fastbr)) {
+            // Our directory in fast tmp already exists
+            return $fastbr;    
+        } else {
+            // Check for fast tmp and make our directory
+            $fast = '/var/tmp/fast';
+            if (is_writeable($fast)) {
+                if (mkdir($fastbr)) {
+                    return $fastbr;
+                } else {
+                    return $fast;
+                }
+            }
+        }
+        
+        // All else failed -- system tmp that should get cleaned on reboot
+        return '/tmp';
+    }
+    
+    function getTempFilename($ext) {
+        return tempnam($this->getTempDir(), "BookReaderImages");
+    }
+    
     // Clean up temporary files and resources
     function cleanup() {
         foreach($this->tempFiles as $tempFile) {
@@ -952,7 +1121,14 @@ class BookReaderImages
         }
         $this->tempFiles = array();
     }
+
+    /*    
+    function cleanTmp() {
+        system('find /var/tmp/fast -name "BookReaderImages*" -cmin +10 -exec rm {} \;');
+        system('find /var/tmp/fast/bookreader -name "BookReaderImages*" -cmin +10 -exec rm {} \;');
+    }
+    */
     
 }
 
-?>
\ No newline at end of file
+?>