Refactor BookReaderImages into class. Thank goodness for unit tests!
authorMichael Ang <mang@archive.org>
Tue, 27 Apr 2010 00:59:10 +0000 (00:59 +0000)
committerMichael Ang <mang@archive.org>
Tue, 27 Apr 2010 00:59:10 +0000 (00:59 +0000)
BookReaderIA/datanode/BookReaderImages.php

index c239def..5d3c99d 100644 (file)
@@ -24,551 +24,545 @@ the MIME type is "image/jpeg".
     along with BookReader.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-$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');
-               
-// Paths to command-line tools
-$exiftool = '/petabox/sw/books/exiftool/exiftool';
-$kduExpand = '/petabox/sw/bin/kdu_expand';
-
-/*
- * Approach:
- * 
- * Get info about requested image (input)
- * Get info about requested output format
- * Determine processing parameters
- * Process image
- * Return image data
- * Clean up temporary files
- */
-
-// Process some of the request parameters
-$zipPath  = $_REQUEST['zip'];
-$file     = $_REQUEST['file'];
-if (isset($_REQUEST['ext'])) {
-    $ext = $_REQUEST['ext'];
-} else {
-    // Default to jpg
-    $ext = 'jpeg';
-}
-if (isset($_REQUEST['callback'])) {
-    // validate callback is valid JS identifier (only)
-    $callback = $_REQUEST['callback'];
-    $identifierPatt = '/^[[:alpha:]$_]([[:alnum:]$_])*$/';
-    if (! preg_match($identifierPatt, $callback)) {
-        BRfatal('Invalid callback');
-    }
-} else {
-    $callback = null;
-}
-
-// Make sure the image stack is readable - return 403 if not
-checkPrivs($zipPath);
-
-
-
-// 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
-// .pnm.  Really kakadu should support .pnm as the file output
-// extension and automatically write ppm or pgm format as
-// appropriate.
-$decompressToBmp = true;
-if ($decompressToBmp) {
-  $stdoutLink = '/tmp/stdout.bmp';
-} else {
-  $stdoutLink = '/tmp/stdout.ppm';
-}
-
-$fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
-
-// Rotate is currently only supported for jp2 since it does not add server load
-$allowedRotations = array("0", "90", "180", "270");
-$rotate = $_REQUEST['rotate'];
-if ( !in_array($rotate, $allowedRotations) ) {
-    $rotate = "0";
-}
-
-// Image conversion options
-$pngOptions = '';
-$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) {
-        $scale = 2;
-        $powReduce = 1;    
-    } else if ($ratio <= 4) {
-        $scale = 4;
-        $powReduce = 2;
-    } else {
-        //$powReduce = 3; //too blurry!
-        $scale = 2;
-        $powReduce = 1;
-    }
-
-} else {
-    // $$$ could be cleaner
-    // Provide next smaller power of two reduction
-    $scale = intval($_REQUEST['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;
-    } else {
-        // $$$ Leaving this in as default though I'm not sure why it is...
-        $powReduce = 3;
-    }
-    $scale = pow(2, $powReduce);
-}
-
-// 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);  
-}
-
-
-putenv('LD_LIBRARY_PATH=/petabox/sw/lib/kakadu');
-
-$unzipCmd  = getUnarchiveCommand($zipPath, $file);
-
-$decompressCmd = getDecompressCmd($imageInfo['type']);
-       
-// Non-integer scaling is currently disabled on the cluster
-// if (isset($_REQUEST['height'])) {
-//     $cmd .= " | pnmscale -height {$_REQUEST['height']} ";
-// }
-
-switch ($ext) {
-    case 'png':
-        $compressCmd = ' | pnmtopng ' . $pngOptions;
-        break;
-        
-    case 'jpeg':
-    case 'jpg':
-    default:
-        $compressCmd = ' | pnmtojpeg ' . $jpegOptions;
-        $ext = 'jpeg'; // for matching below
-        break;
-
-}
-
-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;
-
-$headers = array('Content-type: '. $MIMES[$ext],
-                  'Cache-Control: max-age=15552000');
-
-$errorMessage = '';
-if (! passthruIfSuccessful($headers, $cmd, $errorMessage)) {
-    // $$$ automated reporting
-    trigger_error('BookReader Processing Error: ' . $cmd . ' -- ' . $errorMessage, E_USER_WARNING);
+class BookReaderImages
+{
+    public $MIMES = array('gif' => 'image/gif',
+                   'jp2' => 'image/jp2',
+                   'jpg' => 'image/jpeg',
+                   'jpeg' => 'image/jpeg',
+                   'png' => 'image/png',
+                   'tif' => 'image/tiff',
+                   'tiff' => 'image/tiff');
+                   
+    public $EXTENSIONS = array('gif' => 'gif',
+                        'jp2' => 'jp2',
+                        'jpeg' => 'jpeg',
+                        'jpg' => 'jpeg',
+                        'png' => 'png',
+                        'tif' => 'tiff',
+                        'tiff' => 'tiff');
+                   
+    // Paths to command-line tools
+    var $exiftool = '/petabox/sw/books/exiftool/exiftool';
+    var $kduExpand = '/petabox/sw/bin/kdu_expand';
     
-    // Try some content-specific recovery
-    $recovered = false;    
-    if ($imageInfo['type'] == 'jp2') {
-        $records = getJp2Records($zipPath, $file);
-        if ($powReduce > intval($records['Clevels'])) {
-            $powReduce = $records['Clevels'];
-            $reduce = pow(2, $powReduce);
+    /*
+     * Approach:
+     * 
+     * Get info about requested image (input)
+     * Get info about requested output format
+     * Determine processing parameters
+     * Process image
+     * Return image data
+     * Clean up temporary files
+     */
+     
+     function serveRequest($requestEnv) {
+        // Process some of the request parameters
+        $zipPath  = $requestEnv['zip'];
+        $file     = $requestEnv['file'];
+        if (! $ext) {
+            $ext = $requestEnv['ext'];
         } else {
-            $reduce = 1;
-            $powReduce = 0;
+            // Default to jpg
+            $ext = 'jpeg';
         }
-         
-        $cmd = $unzipCmd . getDecompressCmd($imageInfo['type']) . $compressCmd;
-        if (passthruIfSuccessful($headers, $cmd, $errorMessage)) {
-            $recovered = true;
+        if (isset($requestEnv['callback'])) {
+            // validate callback is valid JS identifier (only)
+            $callback = $requestEnv['callback'];
+            $identifierPatt = '/^[[:alpha:]$_]([[:alnum:]$_])*$/';
+            if (! preg_match($identifierPatt, $callback)) {
+                $this->BRfatal('Invalid callback');
+            }
         } else {
-            trigger_error('BookReader fallback image processing also failed: ' . $errorMessage, E_USER_WARNING);
+            $callback = null;
         }
-    }
-    
-    if (! $recovered) {
-        BRfatal('Problem processing image - command failed');
-    }
-}
-
-if (isset($tempFile)) {
-    unlink($tempFile);
-}
-
-
-
-////////////////////////////////////////////////
-
-
-function getUnarchiveCommand($archivePath, $file)
-{
-    $lowerPath = strtolower($archivePath);
-    if (preg_match('/\.([^\.]+)$/', $lowerPath, $matches)) {
-        $suffix = $matches[1];
-        
-        if ($suffix == 'zip') {
-            return 'unzip -p '
-                . escapeshellarg($archivePath)
-                . ' ' . escapeshellarg($file);
-        } else if ($suffix == 'tar') {
-            return ' ( 7z e -so '
-                . escapeshellarg($archivePath)
-                . ' ' . escapeshellarg($file) . ' 2>/dev/null ) ';
+        
+        // Make sure the image stack is readable - return 403 if not
+        $this->checkPrivs($zipPath);
+        
+        
+        // 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
+            $this->outputJSON($imageInfo, $callback); // $$$ move to BookReaderRequest
+            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
+        // .pnm.  Really kakadu should support .pnm as the file output
+        // extension and automatically write ppm or pgm format as
+        // appropriate.
+        $this->decompressToBmp = true; // $$$ shouldn't be necessary if we use file info to determine output format
+        if ($this->decompressToBmp) {
+          $stdoutLink = '/tmp/stdout.bmp';
         } else {
-            BRfatal('Incompatible archive format');
+          $stdoutLink = '/tmp/stdout.ppm';
         }
-
-    } else {
-        BRfatal('Bad image stack path');
-    }
-    
-    BRfatal('Bad image stack path or archive format');
+        
+        $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
+        
+        // Rotate is currently only supported for jp2 since it does not add server load
+        $allowedRotations = array("0", "90", "180", "270");
+        $rotate = $requestEnv['rotate'];
+        if ( !in_array($rotate, $allowedRotations) ) {
+            $rotate = "0";
+        }
+        
+        // Image conversion options
+        $pngOptions = '';
+        $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($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;
+            }
+        
+        } 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;
+            } else {
+                // $$$ Leaving this in as default though I'm not sure why it is...
+                $powReduce = 3;
+            }
+            $scale = pow(2, $powReduce);
+        }
+        
+        // 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);  
+        }
+        
+        
+        putenv('LD_LIBRARY_PATH=/petabox/sw/lib/kakadu');
+        
+        $unzipCmd  = $this->getUnarchiveCommand($zipPath, $file);
+        
+        $decompressCmd = $this->getDecompressCmd($imageInfo['type'], $powReduce, $rotate, $scale, $stdoutLink);
+               
+        // Non-integer scaling is currently disabled on the cluster
+        // if (isset($_REQUEST['height'])) {
+        //     $cmd .= " | pnmscale -height {$_REQUEST['height']} ";
+        // }
+        
+        switch ($ext) {
+            case 'png':
+                $compressCmd = ' | pnmtopng ' . $pngOptions;
+                break;
+                
+            case 'jpeg':
+            case 'jpg':
+            default:
+                $compressCmd = ' | pnmtojpeg ' . $jpegOptions;
+                $ext = 'jpeg'; // for matching below
+                break;
+        
+        }
+        
+        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;
+        
+        $headers = array('Content-type: '. $MIMES[$ext],
+                          'Cache-Control: max-age=15552000');
+        
+        $errorMessage = '';
+        if (! $this->passthruIfSuccessful($headers, $cmd, $errorMessage)) { // $$$ move to BookReaderRequest
+            // $$$ automated reporting
+            trigger_error('BookReader Processing Error: ' . $cmd . ' -- ' . $errorMessage, E_USER_WARNING);
+            
+            // Try some content-specific recovery
+            $recovered = false;    
+            if ($imageInfo['type'] == 'jp2') {
+                $records = $this->getJp2Records($zipPath, $file);
+                if ($powReduce > intval($records['Clevels'])) {
+                    $powReduce = $records['Clevels'];
+                    $reduce = pow(2, $powReduce);
+                } else {
+                    $reduce = 1;
+                    $powReduce = 0;
+                }
+                 
+                $cmd = $unzipCmd . $this->getDecompressCmd($imageInfo['type'], $powReduce, $rotate, $scale, $stdoutLink) . $compressCmd;
+                if ($this->passthruIfSuccessful($headers, $cmd, $errorMessage)) { // $$$ move to BookReaderRequest
+                    $recovered = true;
+                } else {
+                    trigger_error('BookReader fallback image processing also failed: ' . $errorMessage, E_USER_WARNING);
+                }
+            }
+            
+            if (! $recovered) {
+                $this->BRfatal('Problem processing image - command failed');
+            }
+        }
+        
+        if (isset($tempFile)) {
+            unlink($tempFile);
+        }
+    }    
     
-}
-
-/*
- * Returns the image type associated with the file extension.
- */
-function imageExtensionToType($extension)
-{
-    global $EXTENSIONS;
+    function getUnarchiveCommand($archivePath, $file)
+    {
+        $lowerPath = strtolower($archivePath);
+        if (preg_match('/\.([^\.]+)$/', $lowerPath, $matches)) {
+            $suffix = $matches[1];
+            
+            if ($suffix == 'zip') {
+                return 'unzip -p '
+                    . escapeshellarg($archivePath)
+                    . ' ' . escapeshellarg($file);
+            } else if ($suffix == 'tar') {
+                return ' ( 7z e -so '
+                    . escapeshellarg($archivePath)
+                    . ' ' . escapeshellarg($file) . ' 2>/dev/null ) ';
+            } else {
+                $this->BRfatal('Incompatible archive format');
+            }
     
-    if (array_key_exists($extension, $EXTENSIONS)) {
-        return $EXTENSIONS[$extension];
-    } else {
-        BRfatal('Unknown image extension');
-    }            
-}
-
-/*
- * Get the image information.  The returned associative array fields will
- * vary depending on the image type.  The basic keys are width, height, type
- * and bits.
- */
-function getImageInfo($zipPath, $file)
-{
-    return getImageInfoFromExif($zipPath, $file); // this is fast
+        } else {
+            $this->BRfatal('Bad image stack path');
+        }
+        
+        $this->BRfatal('Bad image stack path or archive format');
+        
+    }
     
     /*
-    $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
-    $type = imageExtensionToType($fileExt);
-    
-    switch ($type) {
-        case "jp2":
-            return getImageInfoFromJp2($zipPath, $file);
-            
-        default:
-            return getImageInfoFromExif($zipPath, $file);
+     * Returns the image type associated with the file extension.
+     */
+    function imageExtensionToType($extension)
+    {
+        
+        if (array_key_exists($extension, $this->EXTENSIONS)) {
+            return $this->EXTENSIONS[$extension];
+        } else {
+            $this->BRfatal('Unknown image extension');
+        }            
     }
-    */
-}
-
-// Get the records of of JP2 as returned by kdu_expand
-function getJp2Records($zipPath, $file)
-{
-    global $kduExpand;
-    
-    $cmd = getUnarchiveCommand($zipPath, $file)
-             . ' | ' . $kduExpand
-             . ' -no_seek -quiet -i /dev/stdin -record /dev/stdout';
-    exec($cmd, $output);
     
-    $records = Array();
-    foreach ($output as $line) {
-        $elems = explode("=", $line, 2);
-        if (1 == count($elems)) {
-            // delimiter not found
-            continue;
+    /*
+     * Get the image information.  The returned associative array fields will
+     * vary depending on the image type.  The basic keys are width, height, type
+     * and bits.
+     */
+    function getImageInfo($zipPath, $file)
+    {
+        return $this->getImageInfoFromExif($zipPath, $file); // this is fast
+        
+        /*
+        $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
+        $type = imageExtensionToType($fileExt);
+        
+        switch ($type) {
+            case "jp2":
+                return getImageInfoFromJp2($zipPath, $file);
+                
+            default:
+                return getImageInfoFromExif($zipPath, $file);
         }
-        $records[$elems[0]] = $elems[1];
+        */
     }
     
-    return $records;
-}
-
-/*
- * Get the image width, height and depth using the EXIF information.
- */
-function getImageInfoFromExif($zipPath, $file)
-{
-    global $exiftool;
-    
-    // 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 -fast' . $tagsToGet . ' -';
-    exec($cmd, $output);
-    
-    $tags = Array();
-    foreach ($output as $line) {
-        $keyValue = explode(": ", $line);
-        $tags[$keyValue[0]] = $keyValue[1];
+    // Get the records of of JP2 as returned by kdu_expand
+    function getJp2Records($zipPath, $file)
+    {
+        
+        $cmd = $this->getUnarchiveCommand($zipPath, $file)
+                 . ' | ' . $this->kduExpand
+                 . ' -no_seek -quiet -i /dev/stdin -record /dev/stdout';
+        exec($cmd, $output);
+        
+        $records = Array();
+        foreach ($output as $line) {
+            $elems = explode("=", $line, 2);
+            if (1 == count($elems)) {
+                // delimiter not found
+                continue;
+            }
+            $records[$elems[0]] = $elems[1];
+        }
+        
+        return $records;
     }
     
-    $width = intval($tags["ImageWidth"]);
-    $height = intval($tags["ImageHeight"]);
-    $type = strtolower($tags["FileType"]);
+    /*
+     * Get the image width, height and depth using the EXIF information.
+     */
+    function getImageInfoFromExif($zipPath, $file)
+    {
+        
+        // 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 = $this->getUnarchiveCommand($zipPath, $file)
+            . ' | '. $this->exiftool . ' -S -fast' . $tagsToGet . ' -';
+        exec($cmd, $output);
+        
+        $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:
+                $this->BRfatal("Unsupported image type");
+                break;
+        }
+       
+       
+        $retval = Array('width' => $width, 'height' => $height,
+            'bits' => $bits, 'type' => $type);
+        
+        return $retval;
+    }
     
-    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;
+    /*
+     * 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;
     }
-   
-   
-    $retval = Array('width' => $width, 'height' => $height,
-        'bits' => $bits, 'type' => $type);
     
-    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 . ');';
+    function getDecompressCmd($imageType, $powReduce, $rotate, $scale, $stdoutLink) {
+        
+        switch ($imageType) {
+            case 'jp2':
+                $decompressCmd = 
+                    " | " . $this->kduExpand . " -no_seek -quiet -reduce $powReduce -rotate $rotate -i /dev/stdin -o " . $stdoutLink;
+                if ($this->decompressToBmp) {
+                    // We suppress output since bmptopnm always outputs on stderr
+                    $decompressCmd .= ' | (bmptopnm 2>/dev/null)';
+                }
+                break;
+        
+            case 'tiff':
+                // We need to create a temporary file for tifftopnm since it cannot
+                // work on a pipe (the file must be seekable).
+                // We use the BookReaderTiff prefix to give a hint in case things don't
+                // get cleaned up.
+                $tempFile = tempnam("/tmp", "BookReaderTiff");
+            
+                // $$$ look at bit depth when reducing
+                $decompressCmd = 
+                    ' > ' . $tempFile . ' ; tifftopnm ' . $tempFile . ' 2>/dev/null' . $this->reduceCommand($scale);
+                break;
+         
+            case 'jpeg':
+                $decompressCmd = ' | ( jpegtopnm 2>/dev/null ) ' . $this->reduceCommand($scale);
+                break;
+        
+            case 'png':
+                $decompressCmd = ' | ( pngtopnm 2>/dev/null ) ' . $this->reduceCommand($scale);
+                break;
+                
+            default:
+                $this->BRfatal('Unknown image type: ' . $imageType);
+                break;
+        }
+        return $decompressCmd;
     }
-    echo $jsonOutput;
-}
-
-function getDecompressCmd($imageType) {
-    global $kduExpand;
-    global $powReduce, $rotate, $scale; // $$$ clean up
-    global $decompressToBmp; // $$$ TODO remove now that we have bit depth info
-    global $stdoutLink;
     
-    switch ($imageType) {
-        case 'jp2':
-            $decompressCmd = 
-                " | " . $kduExpand . " -no_seek -quiet -reduce $powReduce -rotate $rotate -i /dev/stdin -o " . $stdoutLink;
-            if ($decompressToBmp) {
-                // We suppress output since bmptopnm always outputs on stderr
-                $decompressCmd .= ' | (bmptopnm 2>/dev/null)';
+    // 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.
+    // 
+    // Returns:
+    //   true - if command emits stdout and has zero exit code
+    //   false - command has initial output on stderr or non-zero exit code
+    //   &$errorMessage - error string if there was an error
+    //
+    // $$$ Tested with our command-line image processing.  May be deadlocks for
+    //     other cases.
+    function passthruIfSuccessful($headers, $cmd, &$errorMessage)
+    {
+        $retVal = false;
+        $errorMessage = '';
+        
+        $descriptorspec = array(
+           0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
+           1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
+           2 => array("pipe", "w"),   // stderr is a pipe to write to
+        );
+        
+        $cwd = NULL;
+        $env = NULL;
+        
+        $process = proc_open($cmd, $descriptorspec, $pipes, $cwd, $env);
+        
+        if (is_resource($process)) {
+            // $pipes now looks like this:
+            // 0 => writeable handle connected to child stdin
+            // 1 => readable handle connected to child stdout
+            // 2 => readable handle connected to child stderr
+        
+            $stdin = $pipes[0];        
+            $stdout = $pipes[1];
+            $stderr = $pipes[2];
+            
+            // check whether we get input first on stdout or stderr
+            $read = array($stdout, $stderr);
+            $write = NULL;
+            $except = NULL;
+            $numChanged = stream_select($read, $write, $except, NULL); // $$$ no timeout
+            if (false === $numChanged) {
+                // select failed
+                $errorMessage = 'Select failed';
+                $retVal = false;
+            }
+            if ($read[0] == $stdout && (1 == $numChanged)) {
+                // Got output first on stdout (only)
+                // $$$ make sure we get all stdout
+                $output = fopen('php://output', 'w');
+                foreach($headers as $header) {
+                    header($header);
+                }
+                stream_copy_to_stream($pipes[1], $output);
+                fclose($output); // okay since tied to special php://output
+                $retVal = true;
+            } else {
+                // Got output on stderr
+                // $$$ make sure we get all stderr
+                $errorMessage = stream_get_contents($stderr);
+                $retVal = false;
             }
-            break;
     
-        case 'tiff':
-            // We need to create a temporary file for tifftopnm since it cannot
-            // work on a pipe (the file must be seekable).
-            // We use the BookReaderTiff prefix to give a hint in case things don't
-            // get cleaned up.
-            $tempFile = tempnam("/tmp", "BookReaderTiff");
-        
-            // $$$ look at bit depth when reducing
-            $decompressCmd = 
-                ' > ' . $tempFile . ' ; tifftopnm ' . $tempFile . ' 2>/dev/null' . reduceCommand($scale);
-            break;
-     
-        case 'jpeg':
-            $decompressCmd = ' | ( jpegtopnm 2>/dev/null ) ' . reduceCommand($scale);
-            break;
+            fclose($stderr);
+            fclose($stdout);
+            fclose($stdin);
     
-        case 'png':
-            $decompressCmd = ' | ( pngtopnm 2>/dev/null ) ' . reduceCommand($scale);
-            break;
             
-        default:
-            BRfatal('Unknown image type: ' . $imageType);
-            break;
+            // It is important that you close any pipes before calling
+            // proc_close in order to avoid a deadlock
+            $cmdRet = proc_close($process);
+            if (0 != $cmdRet) {
+                $retVal = false;
+                $errorMessage .= "Command failed with result code " . $cmdRet;
+            }
+        }
+        return $retVal;
     }
-    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.
-// 
-// Returns:
-//   true - if command emits stdout and has zero exit code
-//   false - command has initial output on stderr or non-zero exit code
-//   &$errorMessage - error string if there was an error
-//
-// $$$ Tested with our command-line image processing.  May be deadlocks for
-//     other cases.
-function passthruIfSuccessful($headers, $cmd, &$errorMessage)
-{
-    $retVal = false;
-    $errorMessage = '';
-    
-    $descriptorspec = array(
-       0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
-       1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
-       2 => array("pipe", "w"),   // stderr is a pipe to write to
-    );
     
-    $cwd = NULL;
-    $env = NULL;
-    
-    $process = proc_open($cmd, $descriptorspec, $pipes, $cwd, $env);
-    
-    if (is_resource($process)) {
-        // $pipes now looks like this:
-        // 0 => writeable handle connected to child stdin
-        // 1 => readable handle connected to child stdout
-        // 2 => readable handle connected to child stderr
+    function BRfatal($string) {
+        echo "alert('$string');\n";
+        die(-1);
+    }
     
-        $stdin = $pipes[0];        
-        $stdout = $pipes[1];
-        $stderr = $pipes[2];
-        
-        // check whether we get input first on stdout or stderr
-        $read = array($stdout, $stderr);
-        $write = NULL;
-        $except = NULL;
-        $numChanged = stream_select($read, $write, $except, NULL); // $$$ no timeout
-        if (false === $numChanged) {
-            // select failed
-            $errorMessage = 'Select failed';
-            $retVal = false;
-        }
-        if ($read[0] == $stdout && (1 == $numChanged)) {
-            // Got output first on stdout (only)
-            // $$$ make sure we get all stdout
-            $output = fopen('php://output', 'w');
-            foreach($headers as $header) {
-                header($header);
-            }
-            stream_copy_to_stream($pipes[1], $output);
-            fclose($output); // okay since tied to special php://output
-            $retVal = true;
+    // Returns true if using a power node
+    function onPowerNode() {
+        exec("lspci | fgrep -c Realtek", $output, $return);
+        if ("0" != $output[0]) {
+            return true;
         } else {
-            // Got output on stderr
-            // $$$ make sure we get all stderr
-            $errorMessage = stream_get_contents($stderr);
-            $retVal = false;
-        }
-
-        fclose($stderr);
-        fclose($stdout);
-        fclose($stdin);
-
-        
-        // It is important that you close any pipes before calling
-        // proc_close in order to avoid a deadlock
-        $cmdRet = proc_close($process);
-        if (0 != $cmdRet) {
-            $retVal = false;
-            $errorMessage .= "Command failed with result code " . $cmdRet;
+            exec("egrep -q AMD /proc/cpuinfo", $output, $return);
+            if ($return == 0) {
+                return true;
+            }
         }
+        return false;
     }
-    return $retVal;
-}
-
-function BRFatal($string) {
-    echo "alert('$string');\n";
-    die(-1);
-}
-
-// Returns true if using a power node
-function onPowerNode() {
-    exec("lspci | fgrep -c Realtek", $output, $return);
-    if ("0" != $output[0]) {
-        return true;
-    } else {
-        exec("egrep -q AMD /proc/cpuinfo", $output, $return);
-        if ($return == 0) {
-            return true;
+    
+    function reduceCommand($scale) {
+        if (1 != $scale) {
+            if ($this->onPowerNode()) {
+                return ' | pnmscale -reduce ' . $scale . ' 2>/dev/null ';
+            } else {
+                return ' | pnmscale -nomix -reduce ' . $scale . ' 2>/dev/null ';
+            }
+        } else {
+            return '';
         }
     }
-    return false;
-}
-
-function reduceCommand($scale) {
-    if (1 != $scale) {
-        if (onPowerNode()) {
-            return ' | pnmscale -reduce ' . $scale . ' 2>/dev/null ';
-        } else {
-            return ' | pnmscale -nomix -reduce ' . $scale . ' 2>/dev/null ';
+    
+    function checkPrivs($filename) {
+        if (!is_readable($filename)) {
+            header('HTTP/1.1 403 Forbidden');
+            exit(0);
         }
-    } else {
-        return '';
     }
 }
 
-function checkPrivs($filename) {
-    if (!is_readable($filename)) {
-        header('HTTP/1.1 403 Forbidden');
-        exit(0);
-    }
-}
+$bri = new BookReaderImages();
+$bri->serveRequest($_REQUEST);
 
 ?>