Merge branch 'master' into jp2levels
authorMichael Ang <mang@archive.org>
Thu, 18 Mar 2010 22:07:02 +0000 (22:07 +0000)
committerMichael Ang <mang@archive.org>
Thu, 18 Mar 2010 22:07:02 +0000 (22:07 +0000)
BookReader/BookReader.css
BookReader/BookReader.js
BookReader/images/thumbnail_mode_icon.png [new file with mode: 0644]
BookReaderIA/datanode/BookReaderImages.php

index 425006c..c81dac9 100644 (file)
     cursor: move;
 }
 
+.BRpagedivthumb {
+    background-color: #FFFFEE;
+    overflow:hidden;
+    border: 1px solid #909090;
+}
+
+.BRpagedivthumb_highlight {
+    background-color: #FFFFEE;
+    overflow:hidden;
+    border: 1px solid #000000;
+}
+
 .BRpagediv2up {
     background-color: rgb(234, 226, 205);
     overflow:hidden;
 .BRicon.zoom_in { background: url(images/zoom_in_icon.png) no-repeat; }
 .BRicon.one_page_mode { background: url(images/one_page_mode_icon.png) no-repeat; }
 .BRicon.two_page_mode { background: url(images/two_page_mode_icon.png) no-repeat; }
+.BRicon.thumbnail_mode { background: url(images/thumbnail_mode_icon.png) no-repeat; }
 .BRicon.embed { background: url(images/embed_icon.png) no-repeat; }
 .BRicon.print { background: url(images/print_icon.png) no-repeat; }
 .BRicon.book_left { background: url(images/book_left_icon.png) no-repeat; }
index 5d5585a..1b92f29 100644 (file)
@@ -38,8 +38,13 @@ This file is part of BookReader.
 function BookReader() {
     this.reduce  = 4;
     this.padding = 10;
-    this.mode    = 1; //1 or 2
+    this.mode    = 1; //1, 2, 3
     this.ui = 'full'; // UI mode
+
+    // thumbnail mode
+    this.thumbWidth = 100;
+    this.thumbRowBuffer = 3; // number of rows to pre-cache out a view
+    this.displayedRows=[];
     
     this.displayedIndices = [];
     //this.indicesToDisplay = [];
@@ -73,6 +78,7 @@ function BookReader() {
     // Mode constants
     this.constMode1up = 1;
     this.constMode2up = 2;
+    this.constModeThumb = 3;
     
     // Zoom levels
     this.reductionFactors = [0.5, 1, 2, 4, 8, 16];
@@ -95,6 +101,7 @@ function BookReader() {
 BookReader.prototype.init = function() {
 
     var startIndex = undefined;
+    this.pageScale = this.reduce; // preserve current reduce
     
     // Find start index and mode if set in location hash
     var params = this.paramsFromFragment(window.location.hash);
@@ -149,6 +156,8 @@ BookReader.prototype.init = function() {
             e.data.displayedIndices = [];
             e.data.updateSearchHilites(); //deletes hilights but does not call remove()            
             e.data.loadLeafs();
+        } else if (3 == e.data.mode){
+            e.data.prepareThumbnailView();
         } else {
             //console.log('drawing 2 page view');
             
@@ -183,6 +192,10 @@ BookReader.prototype.init = function() {
         this.resizePageView();
         this.firstIndex = startIndex;
         this.jumpToIndex(startIndex);
+    } else if (3 == this.mode) {
+        this.firstIndex = startIndex;
+        this.prepareThumbnailView();
+        this.jumpToIndex(startIndex);
     } else {
         //this.resizePageView();
         
@@ -262,6 +275,8 @@ BookReader.prototype.setupKeyListeners = function() {
 BookReader.prototype.drawLeafs = function() {
     if (1 == this.mode) {
         this.drawLeafsOnePage();
+    } else if(3 == this.mode) {
+        this.drawLeafsThumbnail();
     } else {
         this.drawLeafsTwoPage();
     }
@@ -549,6 +564,182 @@ BookReader.prototype.drawLeafsOnePage = function() {
     
 }
 
+// drawLeafsThumbnail()
+//______________________________________________________________________________
+BookReader.prototype.drawLeafsThumbnail = function() {
+    //alert('drawing leafs!');
+    this.timer = null;
+
+    var viewWidth = $('#BRcontainer').attr('scrollWidth') - 20; // width minus buffer
+
+    //console.log('top=' + scrollTop + ' bottom='+scrollBottom);
+
+    var i;
+    var leafWidth;
+    var leafHeight;
+    var rightPos = 0;
+    var bottomPos = 0;
+    var maxRight = 0;
+    var currentRow = 0;
+    var leafIndex = 0;
+    var leafMap = [];
+
+    for (i=0; i<this.numLeafs; i++) {
+        leafWidth = this.thumbWidth;
+        if (rightPos + (leafWidth + this.padding) > viewWidth){
+            currentRow++;
+            rightPos = 0;
+            leafIndex = 0;
+        }
+
+        if (leafMap[currentRow]===undefined) { leafMap[currentRow] = {}; }
+        if (leafMap[currentRow].leafs===undefined) {
+            leafMap[currentRow].leafs = [];
+            leafMap[currentRow].height = 0;
+            leafMap[currentRow].top = 0;
+        }
+        leafMap[currentRow].leafs[leafIndex] = {};
+        leafMap[currentRow].leafs[leafIndex].num = i;
+        leafMap[currentRow].leafs[leafIndex].left = rightPos;
+
+        leafHeight = parseInt((this.getPageHeight(leafMap[currentRow].leafs[leafIndex].num)*this.thumbWidth)/this.getPageWidth(leafMap[currentRow].leafs[leafIndex].num), 10);
+        if (leafHeight > leafMap[currentRow].height) { leafMap[currentRow].height = leafHeight; }
+        if (leafIndex===0) { bottomPos += this.padding + leafMap[currentRow].height; }
+        rightPos += leafWidth + this.padding;
+        if (rightPos > maxRight) { maxRight = rightPos; }
+        leafIndex++;
+    }
+
+    // reset the bottom position based on thumbnails
+    $('#BRpageview').height(bottomPos);
+
+    var pageViewBuffer = Math.floor(($('#BRcontainer').attr('scrollWidth') - maxRight) / 2) - 14;
+    var scrollTop = $('#BRcontainer').attr('scrollTop');
+    var scrollBottom = scrollTop + $('#BRcontainer').height();
+
+    var leafTop = 0;
+    var leafBottom = 0;
+    var rowsToDisplay = [];
+
+    for (i=0; i<leafMap.length; i++) {
+        leafBottom += this.padding + leafMap[i].height;
+        var topInView    = (leafTop >= scrollTop) && (leafTop <= scrollBottom);
+        var bottomInView = (leafBottom >= scrollTop) && (leafBottom <= scrollBottom);
+        var middleInView = (leafTop <=scrollTop) && (leafBottom>=scrollBottom);
+        if (topInView | bottomInView | middleInView) {
+            //console.log('row to display: ' + j);
+            rowsToDisplay.push(i);
+        }
+        if(leafTop > leafMap[i].top) { leafMap[i].top = leafTop; }
+        leafTop = leafBottom;
+    }
+
+    // create a buffer of preloaded rows before and after the visible rows
+    var firstRow = rowsToDisplay[0];
+    var lastRow = rowsToDisplay[rowsToDisplay.length-1];
+    for (i=1; i<this.thumbRowBuffer+1; i++) {
+        if (lastRow+i < leafMap.length) { rowsToDisplay.push(lastRow+i); }
+    }
+    for (i=1; i<this.thumbRowBuffer+1; i++) {
+        if (firstRow-i >= 0) { rowsToDisplay.push(firstRow-i); }
+    }
+
+    // Update hash, but only if we're currently displaying a leaf
+    // Hack that fixes #365790
+    if (this.displayedRows.length > 0) {
+        this.updateLocationHash();
+    }
+
+    var j;
+    var row;
+    var left;
+    var index;
+    var div;
+    var link;
+    var img;
+    var page;
+    for (i=0; i<rowsToDisplay.length; i++) {
+        if (-1 == jQuery.inArray(rowsToDisplay[i], this.displayedRows)) {    
+            row = rowsToDisplay[i];
+
+            for (j=0; j<leafMap[row].leafs.length; j++) {
+                index = j;
+                leaf = leafMap[row].leafs[j].num;
+
+                leafWidth = this.thumbWidth;
+                leafHeight = parseInt((this.getPageHeight(leaf)*this.thumbWidth)/this.getPageWidth(leaf), 10);
+                leafTop = leafMap[row].top;
+                left = leafMap[row].leafs[index].left + pageViewBuffer;
+                if ('rl' == this.pageProgression){
+                    left = viewWidth - leafWidth - left;
+                }
+
+                div = document.createElement("div");
+                div.id = 'pagediv'+leaf;
+                div.style.position = "absolute";
+                div.className = "BRpagedivthumb";
+
+                left += this.padding;
+                $(div).css('top', leafTop + 'px');
+                $(div).css('left', left+'px');
+                $(div).css('width', leafWidth+'px');
+                $(div).css('height', leafHeight+'px');
+                //$(div).text('loading...');
+
+                // link to page in single page mode
+                // $$$ direct JS calls instead should reduce visual disruption
+                link = document.createElement("a");
+                link.href = '#page/' + (this.getPageNum(leaf)) +'/mode/1up' ;
+                $(div).append(link);
+
+                $('#BRpageview').append(div);
+
+                img = document.createElement("img");
+                var thumbReduce = Math.floor(this.getPageWidth(leaf) / this.thumbWidth);
+                img.src = this._getPageURI(leaf, thumbReduce);
+                $(img).css('width', leafWidth+'px');
+                $(img).css('height', leafHeight+'px');
+                img.style.border = "0";
+                $(link).append(img);
+                //console.log('displaying thumbnail: ' + leafMap[j]);
+            }   
+        }
+    }
+
+    // remove previous highlights
+    if ($('.BRpagedivthumb_highlight').length>0) {
+        div = $('.BRpagedivthumb_highlight')
+        div.attr({className: 'BRpagedivthumb' });
+    }
+    // highlight current page
+    $('#pagediv'+this.currentIndex()).attr({className: 'BRpagedivthumb_highlight' });
+
+    var k;
+    for (i=0; i<this.displayedRows.length; i++) {
+        if (-1 == jQuery.inArray(this.displayedRows[i], rowsToDisplay)) {
+            row = this.displayedRows[i];
+            for (k=0; k<leafMap[row].leafs.length; k++) {
+                index = leafMap[row].leafs[k].num;
+                //console.log('Removing leaf ' + index);
+                $('#pagediv'+index).remove();
+            }
+        } else {
+
+            //console.log('NOT Removing leaf ' + this.displayedIndices[i]);
+        }
+    }
+
+    this.displayedRows = rowsToDisplay.slice();
+
+    if (null !== this.getPageNum(this.currentIndex()))  {
+        $("#BRpagenum").val(this.getPageNum(this.currentIndex()));
+    } else {
+        $("#BRpagenum").val('');
+    }
+
+    this.updateToolbarZoom(this.reduce); 
+}
+
 // drawLeafsTwoPage()
 //______________________________________________________________________________
 BookReader.prototype.drawLeafsTwoPage = function() {
@@ -676,7 +867,8 @@ BookReader.prototype.zoom1up = function(dir) {
         if (this.reduce >= 8) return;
         this.reduce*=2;             //zoom out
     }
-    
+
+    this.pageScale = this.reduce; // preserve current reduce
     this.resizePageView();
 
     $('#BRpageview').empty()
@@ -789,6 +981,7 @@ BookReader.prototype.zoom2up = function(direction) {
     }
     this.twoPage.autofit = newZoom.autofit;
     this.reduce = newZoom.reduce;
+    this.pageScale = this.reduce; // preserve current reduce
 
     // Preserve view center position
     var oldCenter = this.twoPageGetViewCenter();
@@ -909,7 +1102,38 @@ BookReader.prototype.jumpToIndex = function(index, pageX, pageY) {
             this.flipFwdToIndex(index);
         }
 
-    } else {        
+    } else if (3 == this.mode) {
+        var viewWidth = $('#BRcontainer').attr('scrollWidth') - 20; // width minus buffer
+        var i;
+        var leafWidth = 0;
+        var leafHeight = 0;
+        var rightPos = 0;
+        var bottomPos = 0;
+        var rowHeight = 0;
+        var leafTop = 0;
+        var leafIndex = 0;
+
+        for (i=0; i<(index+1); i++) {
+            leafWidth = this.thumbWidth;
+            if (rightPos + (leafWidth + this.padding) > viewWidth){
+                rightPos = 0;
+                rowHeight = 0;
+                leafIndex = 0;
+            }
+            leafHeight = parseInt((this.getPageHeight(leaf)*this.thumbWidth)/this.getPageWidth(leaf), 10);
+            if(leafHeight > rowHeight) { rowHeight = leafHeight; }
+            if (leafIndex==0) { leafTop = bottomPos; }
+            if (leafIndex==0) { bottomPos += this.padding + rowHeight; }
+            rightPos += leafWidth + this.padding;
+            leafIndex++;
+        }
+        this.firstIndex=index;
+        if ($('#BRcontainer').attr('scrollTop') == leafTop) {
+            this.loadLeafs();
+        } else {
+            $('#BRcontainer').animate({scrollTop: leafTop },'fast');
+        }
+    } else {
         var i;
         var leafTop = 0;
         var leafLeft = 0;
@@ -918,7 +1142,7 @@ BookReader.prototype.jumpToIndex = function(index, pageX, pageY) {
             h = parseInt(this._getPageHeight(i)/this.reduce); 
             leafTop += h + this.padding;
         }
-        
+
         if (pageY) {
             //console.log('pageY ' + pageY);
             var offset = parseInt( (pageY) / this.reduce);
@@ -926,12 +1150,12 @@ BookReader.prototype.jumpToIndex = function(index, pageX, pageY) {
             //console.log( 'jumping to ' + leafTop + ' ' + offset);
             leafTop += offset;
         }
-        
+
         if (pageX) {
             var offset = parseInt( (pageX) / this.reduce);
             offset -= $('#BRcontainer').attr('clientWidth') >> 1;
             leafLeft += offset;
-        }   
+        }
 
         //$('#BRcontainer').attr('scrollTop', leafTop);
         $('#BRcontainer').animate({scrollTop: leafTop, scrollLeft: leafLeft },'fast');
@@ -955,15 +1179,21 @@ BookReader.prototype.switchMode = function(mode) {
     this.removeSearchHilites();
 
     this.mode = mode;
-    
     this.switchToolbarMode(mode);
+
+    // reinstate scale if moving from thumbnail view
+    if (this.pageScale != this.reduce) this.reduce = this.pageScale;
     
     // $$$ TODO preserve center of view when switching between mode
     //     See https://bugs.edge.launchpad.net/gnubook/+bug/416682
-    
+
     if (1 == mode) {
         this.reduce = this.quantizeReduce(this.reduce);
         this.prepareOnePageView();
+    } else if (3 == mode) {
+        this.reduce = this.quantizeReduce(this.reduce);
+        this.prepareThumbnailView();
+        this.jumpToIndex(this.currentIndex());
     } else {
         this.twoPage.autofit = false; // Take zoom level from other mode
         this.reduce = this.quantizeReduce(this.reduce);
@@ -1006,6 +1236,38 @@ BookReader.prototype.prepareOnePageView = function() {
     brPageView[0].onselectstart = function(e) { return false; };
 }
 
+//prepareThumbnailView()
+//______________________________________________________________________________
+BookReader.prototype.prepareThumbnailView = function() {
+
+    // var startLeaf = this.displayedIndices[0];
+    var startLeaf = this.currentIndex();
+    this.reduce = this.getPageWidth(0)/this.thumbWidth;
+    
+    $('#BRcontainer').empty();
+    $('#BRcontainer').css({
+        overflowY: 'scroll',
+        overflowX: 'auto'
+    });
+    
+    var brPageView = $("#BRcontainer").append("<div id='BRpageview'></div>");
+    
+    this.resizePageView();
+    
+    this.displayedRows = [];
+    this.drawLeafsThumbnail();
+        
+    // Bind mouse handlers
+    // Disable mouse click to avoid selected/highlighted page images - bug 354239
+    brPageView.bind('mousedown', function(e) {
+        // $$$ check here for right-click and don't disable.  Also use jQuery style
+        //     for stopping propagation. See https://bugs.edge.launchpad.net/gnubook/+bug/362626
+        return false;
+    })
+    // Special hack for IE7
+    brPageView[0].onselectstart = function(e) { return false; };
+}
+
 // prepareTwoPageView()
 //______________________________________________________________________________
 // Some decisions about two page view:
@@ -1435,7 +1697,7 @@ BookReader.prototype.twoPageSetCursor = function() {
 // Returns the currently active index.
 BookReader.prototype.currentIndex = function() {
     // $$$ we should be cleaner with our idea of which index is active in 1up/2up
-    if (this.mode == this.constMode1up) {
+    if (this.mode == this.constMode1up || this.mode == this.constModeThumb) {
         return this.firstIndex; // $$$ TODO page in center of view would be better
     } else if (this.mode == this.constMode2up) {
         // Only allow indices that are actually present in book
@@ -2770,6 +3032,7 @@ BookReader.prototype.initToolbar = function(mode, ui) {
         +   " <span class='label'>Zoom: <span id='BRzoom'>"+parseInt(100/this.reduce)+"</span></span>"
         +   " <button class='BRicon rollover one_page_mode' onclick='br.switchMode(1); return false;'/>"
         +   " <button class='BRicon rollover two_page_mode' onclick='br.switchMode(2); return false;'/>"
+        +   " <button class='BRicon rollover thumbnail_mode' onclick='br.switchMode(3); return false;'/>"
         + "</span>"
         
         + "<span id='#BRbooktitle'>"
@@ -2797,6 +3060,7 @@ BookReader.prototype.initToolbar = function(mode, ui) {
                    '.zoom_out': 'Zoom out',
                    '.one_page_mode': 'One-page view',
                    '.two_page_mode': 'Two-page view',
+                   '.thumbnail_mode': 'Thumbnail view',
                    '.print': 'Print this page',
                    '.embed': 'Embed bookreader',
                    '.book_left': 'Flip left',
@@ -2836,15 +3100,25 @@ BookReader.prototype.initToolbar = function(mode, ui) {
 //______________________________________________________________________________
 // Update the toolbar for the given mode (changes navigation buttons)
 // $$$ we should soon split the toolbar out into its own module
-BookReader.prototype.switchToolbarMode = function(mode) {
+BookReader.prototype.switchToolbarMode = function(mode) { 
     if (1 == mode) {
-        // 1-up     
+        // 1-up
+        $('#BRtoolbar .BRtoolbarzoom').show().css('display', 'inline');
         $('#BRtoolbar .BRtoolbarmode2').hide();
+        $('#BRtoolbar .BRtoolbarmode3').hide();
         $('#BRtoolbar .BRtoolbarmode1').show().css('display', 'inline');
-    } else {
+    } else if (2 == mode) {
         // 2-up
+        $('#BRtoolbar .BRtoolbarzoom').show().css('display', 'inline');
         $('#BRtoolbar .BRtoolbarmode1').hide();
+        $('#BRtoolbar .BRtoolbarmode3').hide();
         $('#BRtoolbar .BRtoolbarmode2').show().css('display', 'inline');
+    } else {
+        // 3-up    
+        $('#BRtoolbar .BRtoolbarzoom').hide();
+        $('#BRtoolbar .BRtoolbarmode2').hide();
+        $('#BRtoolbar .BRtoolbarmode1').hide();
+        $('#BRtoolbar .BRtoolbarmode3').show().css('display', 'inline');
     }
 }
 
@@ -3047,7 +3321,7 @@ BookReader.prototype.updateFromParams = function(params) {
 // E.g paramsFromFragment(window.location.hash)
 BookReader.prototype.paramsFromFragment = function(urlFragment) {
     // URL fragment syntax specification: http://openlibrary.org/dev/docs/bookurls
-    
+
     var params = {};
     
     // For convenience we allow an initial # character (as from window.location.hash)
@@ -3077,6 +3351,8 @@ BookReader.prototype.paramsFromFragment = function(urlFragment) {
         params.mode = this.constMode1up;
     } else if ('2up' == urlHash['mode']) {
         params.mode = this.constMode2up;
+    } else if ('thumb' == urlHash['mode']) {
+        params.mode = this.constModeThumb;
     }
     
     // Index and page
@@ -3130,7 +3406,7 @@ BookReader.prototype.paramsFromCurrent = function() {
 // See http://openlibrary.org/dev/docs/bookurls for an explanation of the fragment syntax.
 BookReader.prototype.fragmentFromParams = function(params) {
     var separator = '/';
-    
+
     var fragments = [];
     
     if ('undefined' != typeof(params.page)) {
@@ -3149,6 +3425,8 @@ BookReader.prototype.fragmentFromParams = function(params) {
             fragments.push('mode', '1up');
         } else if (params.mode == this.constMode2up) {
             fragments.push('mode', '2up');
+        } else if (params.mode == this.constModeThumb) {
+            fragments.push('mode', 'thumb');
         } else {
             throw 'fragmentFromParams called with unknown mode ' + params.mode;
         }
@@ -3331,6 +3609,7 @@ BookReader.prototype._getPageURI = function(index, reduce, rotate) {
     
     if ('undefined' == typeof(reduce)) {
         // reduce not passed in
+        // $$$ this probably won't work for thumbnail mode
         var ratio = this.getPageHeight(index) / this.twoPage.height;
         var scale;
         // $$$ we make an assumption here that the scales are available pow2 (like kakadu)
diff --git a/BookReader/images/thumbnail_mode_icon.png b/BookReader/images/thumbnail_mode_icon.png
new file mode 100644 (file)
index 0000000..bc62318
Binary files /dev/null and b/BookReader/images/thumbnail_mode_icon.png differ
index 2895724..cb1ca65 100644 (file)
@@ -40,7 +40,9 @@ $EXTENSIONS = array('gif' => 'gif',
                     'tif' => 'tiff',
                     'tiff' => 'tiff');
                
+// Paths to command-line tools
 $exiftool = '/petabox/sw/books/exiftool/exiftool';
+$kduExpand = '/petabox/sw/bin/kdu_expand';
 
 // Process some of the request parameters
 $zipPath  = $_REQUEST['zip'];
@@ -114,15 +116,58 @@ function imageExtensionToType($extension)
 }
 
 /*
- * Get the image width, height and depth from a jp2 file in zip.
+ * 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)
 {
-    global $exiftool;
+    return 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);
+    }
+    */
+}
+
+// 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;
+        }
+        $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
@@ -236,20 +281,29 @@ if (isset($_REQUEST['height'])) {
     }
 
 } else {
-    $scale = $_REQUEST['scale'];
+    // $$$ could be cleaner
+    $scale = intval($_REQUEST['scale']);
     if (1 >= $scale) {
         $scale = 1;
         $powReduce = 0;
-    } else if (2 == $scale) {
+    } else if (2 > $scale) {
+        $powReduce = 0;
+        $scale = 1;
+    } else if (4 > $scale) {
         $powReduce = 1;
-    } else if (4 == $scale) {
+        $scale = 2;
+    } else if (8 > $scale) {
         $powReduce = 2;
-    } else if (8 == $scale) {
+        $scale = 4;
+    } else if (16 > $scale) {
         $powReduce = 3;
-    } else if (16 == $scale) {
+        $scale = 8;
+    } else if (32 > $scale) {
         $powReduce = 4;
-    } else if (32 == $scale) {
+        $scale = 16;
+    } else if (64 > $scale) {
         $powReduce = 5;
+        $scale = 32;
     } else {
         // $$$ Leaving this in as default though I'm not sure why it is...
         $scale = 8;
@@ -283,39 +337,50 @@ putenv('LD_LIBRARY_PATH=/petabox/sw/lib/kakadu');
 
 $unzipCmd  = getUnarchiveCommand($zipPath, $file);
 
-switch ($imageInfo['type']) {
-    case 'jp2':
-        $decompressCmd = 
-            " | /petabox/sw/bin/kdu_expand -no_seek -quiet -reduce $powReduce -rotate $rotate -i /dev/stdin -o " . $stdoutLink;
-        if ($decompressToBmp) {
-            $decompressCmd .= ' | bmptopnm ';
-        }
-        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");
+function getDecompressCmd($imageType) {
+    global $kduExpand;
+    global $powReduce, $rotate, $scale; // $$$ clean up
+    global $decompressToBmp; // $$$ TODO remove now that we have bit depth info
+    global $stdoutLink;
     
-        // $$$ look at bit depth when reducing
-        $decompressCmd = 
-            ' > ' . $tempFile . ' ; tifftopnm ' . $tempFile . ' 2>/dev/null' . reduceCommand($scale);
-        break;
-    case 'jpeg':
-        $decompressCmd = ' | jpegtopnm ' . reduceCommand($scale);
-        break;
-
-    case 'png':
-        $decompressCmd = ' | pngtopnm ' . reduceCommand($scale);
-        break;
+    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)';
+            }
+            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");
         
-    default:
-        BRfatal('Unknown source file extension: ' . $fileExt);
-        break;
+            // $$$ look at bit depth when reducing
+            $decompressCmd = 
+                ' > ' . $tempFile . ' ; tifftopnm ' . $tempFile . ' 2>/dev/null' . reduceCommand($scale);
+            break;
+     
+        case 'jpeg':
+            $decompressCmd = ' | jpegtopnm ' . reduceCommand($scale);
+            break;
+    
+        case 'png':
+            $decompressCmd = ' | pngtopnm ' . reduceCommand($scale);
+            break;
+            
+        default:
+            BRfatal('Unknown image type: ' . $imageType);
+            break;
+    }
+    return $decompressCmd;
 }
+
+$decompressCmd = getDecompressCmd($imageInfo['type']);
        
 // Non-integer scaling is currently disabled on the cluster
 // if (isset($_REQUEST['height'])) {
@@ -343,13 +408,122 @@ if (($ext == $fileExt) && ($scale == 1) && ($rotate === "0")) {
     $cmd = $unzipCmd . $decompressCmd . $compressCmd;
 }
 
-# print $cmd;
+// print $cmd;
+
+// 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;
+        }
+
+        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;
+        }
+    }
+    return $retVal;
+}
+
+$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);
+    
+    // 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);
+        } else {
+            $reduce = 1;
+            $powReduce = 0;
+        }
+         
+        $cmd = $unzipCmd . getDecompressCmd($imageInfo['type']) . $compressCmd;
+        if (passthruIfSuccessful($headers, $cmd, $errorMessage)) {
+            $recovered = true;
+        } else {
+            trigger_error('BookReader fallback image processing also failed: ' . $errorMessage, E_USER_WARNING);
+        }
+    }
+    
+    if (! $recovered) {
+        BRfatal('Problem processing image - command failed');
+    }
+}
 
-// $$$ 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
+// passthru ($cmd); # cmd returns image data
 
 if (isset($tempFile)) {
     unlink($tempFile);