Merge branch 'leaf_url' into image_region
authorMichael Ang <mang@archive.org>
Wed, 25 May 2011 01:30:38 +0000 (01:30 +0000)
committerMichael Ang <mang@archive.org>
Wed, 25 May 2011 01:30:38 +0000 (01:30 +0000)
BookReaderIA/datanode/BookReaderImages.inc.php
BookReaderIA/datanode/BookReaderJSIA.php
BookReaderIA/test/unit/Images.js

index cd02d06..7811942 100644 (file)
@@ -60,7 +60,9 @@ class BookReaderImages
         'tile' => 'tile',
         'w' => 'width',
         'h' => 'height',
-        'rotate' => 'rotate'
+        'x' => 'x',
+        'y' => 'y',
+        'rot' => 'rotate'
     );
     
     // Paths to command-line tools
@@ -108,6 +110,7 @@ class BookReaderImages
         $basePage = $pageInfo['type'];
         
         $leaf = null;
+        $region = null;
         switch ($basePage) {
         
             case 'title':
@@ -175,7 +178,7 @@ class BookReaderImages
                 // Leaf explicitly specified
                 $leaf = $pageInfo['value'];
                 break;
-                
+                                
             default:
                 // Shouldn't be possible
                 $this->BRfatal("Unrecognized page type requested");
@@ -250,7 +253,7 @@ class BookReaderImages
         
         // Get the image size and depth
         $imageInfo = $this->getImageInfo($zipPath, $file);
-        
+                
         // Output json if requested
         if ('json' == $ext) {
             // $$$ we should determine the output size first based on requested scale
@@ -293,11 +296,13 @@ 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'])) {
+        // Set scale from height or width if set and no x or y specified
+        if ( isset($requestEnv['height']) && !isset($requestEnv['x']) && !isset($requestEnv['y']) ) {
+            // No x or y specified, use height for scaling
             $powReduce = $this->nearestPow2Reduce($requestEnv['height'], $imageInfo['height']);
             $scale = pow(2, $powReduce);
-        } else if (isset($requestEnv['width'])) {
+        } else if ( isset($requestEnv['width']) && !isset($requestEnv['x']) && !isset($requestEnv['y']) ) {
+            // No x or y specified, use width for scaling
             $powReduce = $this->nearestPow2Reduce($requestEnv['width'], $imageInfo['width']);
             $scale = pow(2, $powReduce);
 
@@ -327,6 +332,30 @@ class BookReaderImages
             }            
         }
         
+        // Only extract a specific region if x or y were set
+        $region = array();
+        if (isset($reqeuestEnv['x']) || isset($requestEnv['y'])) {
+            foreach (array('x', 'y', 'width', 'height') as $key) {
+                if (array_key_exists($key, $requestEnv)) {
+                    $region[$key] = $requestEnv[$key];
+                }
+            }
+        }
+        $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
         // Limit scaling for 1-bit images.  See https://bugs.edge.launchpad.net/bookreader/+bug/486011
@@ -353,8 +382,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']} ";
@@ -410,7 +439,7 @@ 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
                     $recovered = true;
@@ -577,18 +606,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).
@@ -609,6 +640,25 @@ 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 -' . $regionString . $scaleString . $rotateString . ' pnm:-';
+                break;
                 
             default:
                 $this->BRfatal('Unknown image type: ' . $imageType);
@@ -617,6 +667,7 @@ class BookReaderImages
         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.
@@ -874,6 +925,103 @@ class BookReaderImages
         return $pageInfo;
     }
     
+    function getRegionDimensions($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->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 = $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('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('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%)
+        if (strpos($stringValue, '.') === false) {
+            // No decimal, assume int
+            return intval($stringValue);
+        }
+        
+        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;
+        }
+        
+        if ($observedValue > $maxValue) {
+            return $maxValue;
+        }
+        
+        return $observedValue;
+    }
+    
     // Clean up temporary files and resources
     function cleanup() {
         foreach($this->tempFiles as $tempFile) {
index e4da14a..8b7a4f9 100644 (file)
@@ -209,6 +209,26 @@ br.getPageURI = function(index, reduce, rotate) {
     return 'http://'+this.server+'/BookReader/BookReaderImages.php?zip='+this.zip+'&file='+file+'&scale='+_reduce+'&rotate='+_rotate;
 }
 
+// Get a rectangular region out of a page
+br.getRegionURI = function(index, reduce, rotate, sourceX, sourceY, sourceWidth, sourceHeight) {
+
+    // Map function arguments to the url keys
+    var urlKeys = ['n', 'r', 'rot', 'x', 'y', 'w', 'h'];
+    var page = '';
+    for (var i = 0; i < arguments.length; i++) {
+        if ('undefined' != typeof(arguments[i])) {
+            if (i > 0 ) {
+                page += '_';
+            }
+            page += urlKeys[i] + arguments[i];
+        }
+    }
+    
+    var itemPath = this.bookPath.replace(new RegExp('/'+this.subPrefix+'$'), ''); // remove trailing subPrefix
+    
+    return 'http://'+this.server+'/BookReader/BookReaderImages.php?id=' + this.bookId + '&itemPath=' + itemPath + '&server=' + this.server + '&subPrefix=' + this.subPrefix + '&page=' +page + '.jpg';
+}
+
 br._getPageFile = function(index) {
     var leafStr = '0000';
     var imgStr = this.leafMap[index].toString();
index 8334cdc..0b42a85 100644 (file)
@@ -157,7 +157,7 @@ asyncTest("Load tiff image from zip", function() {
     var img = new Image();
     $(img).bind( 'load error', function(eventObj) {
         equals(eventObj.type, 'load', 'Load image (' + pageURI + '). Event handler called');
-        equals(this.width, 702, 'Image width');
+        equals(this.width, 701, 'Image width');
         start();
     })
     .attr('src', pageURI);
@@ -179,8 +179,94 @@ asyncTest('Load jpg image from tar file - https://bugs.launchpad.net/bookreader/
     var img = new Image();
     $(img).bind( 'load error', function(eventObj) {
         equals(eventObj.type, 'load', 'Load image (' + pageURI + '). Event handler called');
-        equals(this.width, 244, 'Image width');
+        equals(this.width, 243, 'Image width');
         start();
     })
     .attr('src', pageURI);
 });
+
+asyncTest('Load image region using /download URL - populationsc18400378unit/page/n800_x1544_y4144_w1192_h848_s4.jpg', function() {
+    expect(3);
+    var pageURI = testHost() + '/download/populationsc18400378unit/page/n800_x1544_y4144_w1192_h848_s4.jpg';
+    
+    var img = new Image();
+    $(img).bind( 'load error', function(eventObj) {
+        equals(eventObj.type, 'load', 'Load image (' + pageURI + '). Event handler called');
+        equals(this.width, 299, 'Image width');
+        equals(this.height, 212, 'Image height');
+        start();
+    })
+    .attr('src', pageURI);    
+
+});
+
+
+asyncTest('Load image region using /download URL with decimal coordinates - populationsc18400378unit/page/n800_x0.75_y0.75_w0.25_h0.25_s4.jpg', function() {
+    expect(3);
+    var pageURI = testHost() + '/download/populationsc18400378unit/page/n800_x0.75_y0.75_w0.25_h0.25_s4.jpg';
+    
+    var img = new Image();
+    $(img).bind( 'load error', function(eventObj) {
+        equals(eventObj.type, 'load', 'Load image (' + pageURI + '). Event handler called');
+        equals(this.width, 337, 'Image width');
+        equals(this.height, 342, 'Image height');
+        start();
+    })
+    .attr('src', pageURI);    
+
+});
+
+
+asyncTest('Load image region - tomslademotorcyc00fitz/page/page3_x256_y96_w1720_h152_s4.jpg', function() {
+    expect(3);
+    var pageURI = testHost() + '/download/tomslademotorcyc00fitz/page/page3_x256_y96_w1720_h152_s4.jpg';
+    
+    var img = new Image();
+    $(img).bind( 'load error', function(eventObj) {
+        equals(eventObj.type, 'load', 'Load image (' + pageURI + '). Event handler called');
+        equals(this.width, 430, 'Image width');
+        equals(this.height, 38, 'Image height');
+        start();
+    })
+    .attr('src', pageURI);    
+
+});
+
+asyncTest('Load image region from tiff, via br.getRegionURI - fightingflyingc00rickgoog - n17_x1944_y1708_w668_h584', function() {
+
+    $.getScript( jsLocateURL('fightingflyingc00rickgoog'), function() {
+
+        expect(3);
+        var pageURI = br.getRegionURI(17, undefined, undefined, 1944, 1708, 668, 584);
+        
+        var img = new Image();
+        $(img).bind( 'load error', function(eventObj) {
+            equals(eventObj.type, 'load', 'Load image (' + pageURI + '). Event handler called');
+            equals(this.width, 668, 'Image width');
+            equals(this.height, 584, 'Image height');
+            start();
+        })
+        .attr('src', pageURI);
+        
+    });
+});
+
+asyncTest('Same image rotated 90 degrees, br.getRegionURI - fightingflyingc00rickgoog - n17_x1944_y1708_w668_h584_rot90', function() {
+
+    $.getScript( jsLocateURL('fightingflyingc00rickgoog'), function() {
+
+        expect(3);
+        var pageURI = br.getRegionURI(17, undefined, 90, 1944, 1708, 668, 584);
+        
+        var img = new Image();
+        $(img).bind( 'load error', function(eventObj) {
+            equals(eventObj.type, 'load', 'Load image (' + pageURI + '). Event handler called');
+            equals(this.width, 584, 'Image width');
+            equals(this.height, 668, 'Image height');
+            start();
+        })
+        .attr('src', pageURI);
+        
+    });
+});
+