Image permalinks, e.g. /download/itemid/page/cover.jpg
[bookreader.git] / BookReaderIA / datanode / BookReaderImages.inc.php
index b324481..d0e2c79 100644 (file)
@@ -24,6 +24,8 @@ the MIME type is "image/jpeg".
     along with BookReader.  If not, see <http://www.gnu.org/licenses/>.
 */
 
+require_once("BookReaderMeta.inc.php");
+
 class BookReaderImages
 {
     public $MIMES = array('gif' => 'image/gif',
@@ -41,12 +43,138 @@ class BookReaderImages
                         'png' => 'png',
                         'tif' => 'tiff',
                         'tiff' => 'tiff');
-                   
+    
+    // Width when generating thumbnails
+    public $imageSizes = array(
+        'thumb' => 100,
+        'small' => 240,
+        'medium' => 500,
+        'large' => 1024,
+    );
+    
     // Paths to command-line tools
     var $exiftool = '/petabox/sw/books/exiftool/exiftool';
     var $kduExpand = '/petabox/sw/bin/kdu_expand';
     
     /*
+     * Serve an image request that requires looking up the book metadata
+     *
+     * Code path:
+     *   - Get book metadata
+     *   - Parse the requested page (e.g. cover_t.jpg, n5_r4.jpg) to determine which page type,
+     *       size and format (etc) is being requested
+     *   - Determine the leaf number corresponding to the page
+     *   - Determine scaling values
+     *   - Serve image request now that all information has been gathered
+     */
+
+    function serveLookupRequest($requestEnv) {
+        $brm = new BookReaderMeta();
+        try {
+            $metadata = $brm->buildMetadata($_REQUEST['id'], $_REQUEST['itemPath'], $_REQUEST['subPrefix'], $_REQUEST['server']);
+        } catch (Exception $e) {
+            $this->BRfatal($e->getMessage);
+        }
+        
+        $page = $_REQUEST['page'];
+
+        // Index of image to return
+        $imageIndex = null;
+
+        // deal with subPrefix
+        if ($_REQUEST['subPrefix']) {
+            $parts = split('/', $_REQUEST['subPrefix']);
+            $bookId = $parts[count($parts) - 1 ];
+        } else {
+            $bookId = $_REQUEST['id'];
+        }
+        
+        $pageInfo = $this->parsePageRequest($page, $bookId);
+
+        $basePage = $pageInfo['type'];
+        
+        switch ($basePage) {
+            case 'title':
+                if (! array_key_exists('titleIndex', $metadata)) {
+                    $this->BRfatal("No title page asserted in book");
+                }
+                $imageIndex = $metadata['titleIndex'];
+                break;
+                
+            case 'cover':
+                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':
+                // Preference is:
+                //   Cover page if book was published >= 1950
+                //   Title page
+                //   Cover page
+                //   Page 0
+                         
+                if ( array_key_exists('date', $metadata) && array_key_exists('coverIndices', $metadata) ) {
+                    if ($brm->parseYear($metadata['date']) >= 1950) {
+                        $imageIndex = $metadata['coverIndices'][0];                
+                        break;
+                    }
+                }
+                if (array_key_exists('titleIndex', $metadata)) {
+                    $imageIndex = $metadata['titleIndex'];
+                    break;
+                }
+                if (array_key_exists('coverIndices', $metadata)) {
+                    $imageIndex = $metadata['coverIndices'][0];
+                    break;
+                }
+                
+                // First page
+                $imageIndex = 0;
+                break;
+                
+            case 'n':
+                // Accessible index page
+                $imageIndex = intval($pageInfo['value']);
+                break;
+                
+            case 'page':
+                // Named page
+                $index = array_search($pageInfo['value'], $metadata['pageNums']);
+                if ($index === FALSE) {
+                    // Not found
+                    $this->BRfatal("Page not found");
+                    break;
+                }
+                
+                $imageIndex = $index;
+                break;
+                
+            default:
+                // Shouldn't be possible
+                $this->BRfatal("Unrecognized page type requested");
+                break;
+                
+        }
+        
+        $leaf = $brm->leafForIndex($imageIndex, $metadata['leafNums']);
+        
+        $requestEnv = array(
+            'zip' => $metadata['zip'],
+            'file' => $brm->imageFilePath($leaf, $metadata['subPrefix'], $metadata['imageFormat']),
+            'ext' => 'jpg',
+        );
+
+        // Return image data - will check privs        
+        $this->serveRequest($requestEnv);
+    
+    }
+    
+    /*
+     * Returns a page image when all parameters such as the image stack location are
+     * passed in.
+     * 
      * Approach:
      * 
      * Get info about requested image (input)
@@ -56,7 +184,6 @@ class BookReaderImages
      * Return image data
      * Clean up temporary files
      */
-     
      function serveRequest($requestEnv) {
         // Process some of the request parameters
         $zipPath  = $requestEnv['zip'];
@@ -77,7 +204,10 @@ class BookReaderImages
         } else {
             $callback = null;
         }
-        
+
+        if ( !file_exists($zipPath) ) {
+            $this->BRfatal('Image stack does not exist at ' . $zipPath);
+        }
         // Make sure the image stack is readable - return 403 if not
         $this->checkPrivs($zipPath);
         
@@ -120,42 +250,31 @@ class BookReaderImages
         
         // 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($requestEnv['height'])) {
-            $ratio = floatval($requestEnv['origHeight']) / floatval($requestEnv['height']);
-            if ($ratio <= 2) {
-                $scale = 2;
-                $powReduce = 1;    
-            } else if ($ratio <= 4) {
-                $scale = 4;
-                $powReduce = 2;
-            } else {
-                //$powReduce = 3; //too blurry!
-                $scale = 2;
-                $powReduce = 1;
-            }
-        
+            $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);
+
         } else {
             // $$$ could be cleaner
             // Provide next smaller power of two reduction
-            $scale = intval($requestEnv['scale']);
-            if (1 >= $scale) {
-                $powReduce = 0;
-            } else if (2 > $scale) {
-                $powReduce = 0;
-            } else if (4 > $scale) {
-                $powReduce = 1;
-            } else if (8 > $scale) {
-                $powReduce = 2;
-            } else if (16 > $scale) {
-                $powReduce = 3;
-            } else if (32 > $scale) {
-                $powReduce = 4;
-            } else if (64 > $scale) {
-                $powReduce = 5;
+            $scale = $requestEnv['scale'];
+            if (!$scale) {
+                $scale = 1;
+            }
+            if (array_key_exists($scale, $this->imageSizes)) {
+                $srcRatio = floatval($imageInfo['width']) / floatval($imageInfo['height']);
+                if ($srcRatio > 1) {
+                    // wide
+                    $dimension = 'width';
+                } else {
+                    $dimension = 'height';
+                }
+                $powReduce = $this->nearestPow2Reduce($this->imageSizes[$scale], $imageInfo[$dimension]);
             } else {
-                // $$$ Leaving this in as default though I'm not sure why it is...
-                $powReduce = 3;
+                $powReduce = $this->nearestPow2ForScale($scale);
             }
             $scale = pow(2, $powReduce);
         }
@@ -181,7 +300,6 @@ class BookReaderImages
           system('ln -s /dev/stdout ' . $stdoutLink);  
         }
         
-        
         putenv('LD_LIBRARY_PATH=/petabox/sw/lib/kakadu');
         
         $unzipCmd  = $this->getUnarchiveCommand($zipPath, $file);
@@ -218,7 +336,7 @@ class BookReaderImages
         
         $filenameForClient = $this->filenameForClient($file, $ext);
         
-        $headers = array('Content-type: '. $MIMES[$ext],
+        $headers = array('Content-type: '. $MIMES[$ext], // XXX is nginx swallowing this?
                          'Cache-Control: max-age=15552000',
                          'Content-disposition: inline; filename=' . $filenameForClient);
                           
@@ -383,7 +501,7 @@ class BookReaderImages
                 $bits = intval($tags["BitDepth"]);
                 break;
             default:
-                $this->BRfatal("Unsupported image type");
+                $this->BRfatal("Unsupported image type $type for file $file in $zipPath");
                 break;
         }
        
@@ -527,8 +645,7 @@ class BookReaderImages
     }
     
     function BRfatal($string) {
-        echo "alert('$string');\n";
-        die(-1);
+        throw new Exception("Image error: $string");
     }
     
     // Returns true if using a power node
@@ -573,6 +690,105 @@ class BookReaderImages
         }
         return $pathParts['filename'] . '.' . $ext;
     }
+    
+    // Returns the nearest power of 2 reduction factor that results in a larger image
+    function nearestPow2Reduce($desiredDimension, $sourceDimension) {
+        $ratio = floatval($sourceDimension) / floatval($desiredDimension);
+        return $this->nearestPow2ForScale($ratio);
+    }
+    
+    // Returns nearest power of 2 reduction factor that results in a larger image
+    function nearestPow2ForScale($scale) {
+        $scale = intval($scale);
+        if ($scale <= 1) {
+            return 0;
+        }
+        $binStr = decbin($scale); // convert to binary string. e.g. 5 -> '101'
+        return strlen($binStr) - 1;
+    }
+    
+    /*
+     * Parses a page request like "page5_r2.jpg" or "cover_t.jpg" to corresponding
+     * page type, size, reduce, and format
+     */
+    function parsePageRequest($pageRequest, $bookPrefix) {
+    
+        $pageInfo = array();
+        
+        // Pull off extension
+        if (preg_match('#(.*)\.([^.]+)$#', $pageRequest, $matches) === 1) {
+            $pageRequest = $matches[1];
+            $extension = $matches[2];
+            if ($extension == 'jpeg') {
+                $extension = 'jpg';
+            }
+        } else {
+            $extension = 'jpg';
+        }
+        $pageInfo['extension'] = $extension;
+        
+        // Split parts out
+        $parts = explode('_', $pageRequest);
+
+        // Remove book prefix if it was included (historical)
+        if ($parts[0] == $bookPrefix) {
+            array_shift($parts);
+        }
+        
+        if (count($parts) === 0) {
+            $this->BRfatal('No page type specified');
+        }
+        $page = array_shift($parts);
+        
+        $pageTypes = array(
+            'page' => 'str',
+            'n' => 'num',
+            'cover' => 'single',
+            'preview' => 'single',
+            'title' => 'single'
+        );
+        
+        // Look for known page types
+        foreach ( $pageTypes as $pageName => $kind ) {
+            if ( preg_match('#^(' . $pageName . ')(.*)#', $page, $matches) === 1 ) {
+                $pageInfo['type'] = $matches[1];
+                switch ($kind) {
+                    case 'str':
+                        $pageInfo['value'] = $matches[2];
+                        break;
+                    case 'num':
+                        $pageInfo['value'] = intval($matches[2]);
+                        break;
+                    case 'single':
+                        break;
+                }
+            }
+        }
+        
+        if ( !array_key_exists('type', $pageInfo) ) {
+            $this->BRfatal('Unrecognized page type');
+        }
+        
+        // Look for other known parts
+        foreach ($parts as $part) {
+            $start = substr($part, 0, 1);
+            
+            switch ($start) {
+                case 't':
+                    $pageInfo['size'] = $start;
+                    break;
+                case 'r':
+                    $pageInfo['reduce'] = substr($part, 0);
+                    break;
+                default:
+                    // Unrecognized... just let it pass
+                    break;
+            }
+        }
+        
+        return $pageInfo;
+    }
+    
 }
 
 ?>
\ No newline at end of file