X-Git-Url: http://git.rot13.org/?a=blobdiff_plain;f=BookReaderIA%2Fdatanode%2FBookReaderImages.inc.php;h=512cd6e67fdacf3724b0ca98b584604950d7356f;hb=2b57cdeb7b3fdbe80cb9277ba698f2fe0ad6556a;hp=74b7b13e1cecc4012d66f1d5b185bbfef59ffff4;hpb=4a23ceebba1157176aa23c2aeca675b821d2b906;p=bookreader.git diff --git a/BookReaderIA/datanode/BookReaderImages.inc.php b/BookReaderIA/datanode/BookReaderImages.inc.php index 74b7b13..512cd6e 100644 --- a/BookReaderIA/datanode/BookReaderImages.inc.php +++ b/BookReaderIA/datanode/BookReaderImages.inc.php @@ -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 @@ -97,7 +99,7 @@ class BookReaderImages // deal with subPrefix if ($_REQUEST['subPrefix']) { - $parts = split('/', $_REQUEST['subPrefix']); + $parts = explode('/', $_REQUEST['subPrefix']); $bookId = $parts[count($parts) - 1 ]; } else { $bookId = $_REQUEST['id']; @@ -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,15 +296,26 @@ class BookReaderImages // image processing load on our cluster. The client should then scale to their final // needed size. - // Set scale from height or width if set - if (isset($requestEnv['height'])) { - $powReduce = $this->nearestPow2Reduce($requestEnv['height'], $imageInfo['height']); - $scale = pow(2, $powReduce); - } else if (isset($requestEnv['width'])) { - $powReduce = $this->nearestPow2Reduce($requestEnv['width'], $imageInfo['width']); - $scale = pow(2, $powReduce); + // Sizing logic: + // If a named size is provided, we size the full image to that size + // If x or y is set, we interpret the supplied width/height as the size of image region to crop to + // If x and y are not set and both width and height are set, we size the full image "within" the width/height + // If x and y are not set and only one of width and height are set, we size the full image to that width or height + // If none of the above apply, we use the whole image + + // Crop region, if empty whole image is used + $region = array(); - } else { + // Initialize scale + $scale = 1; + if (isset($requestEnv['scale'])) { + $scale = $requestEnv['scale']; + } + $powReduce = $this->nearestPow2ForScale($scale); + // ensure integer scale + $scale = pow(2, $powReduce); + + if ( isset($requestEnv['size']) ) { // Set scale from named size (e.g. 'large') if set $size = $requestEnv['size']; if ( $size && array_key_exists($size, self::$imageSizes)) { @@ -314,18 +328,54 @@ class BookReaderImages } $powReduce = $this->nearestPow2Reduce(self::$imageSizes[$size], $imageInfo[$dimension]); $scale = pow(2, $powReduce); - - } else { - // No named size - use explicit scale, if given - $scale = $requestEnv['scale']; - if (!$scale) { - $scale = 1; + } + + } else if ( isset($requestEnv['x']) || isset($requestEnv['y']) ) { + // x,y is crop region origin, width,height is size of crop region + foreach (array('x', 'y', 'width', 'height') as $key) { + if (array_key_exists($key, $requestEnv)) { + $region[$key] = $requestEnv[$key]; } - $powReduce = $this->nearestPow2ForScale($scale); - // ensure integer scale - $scale = pow(2, $powReduce); - } + } + + } else if ( isset($requestEnv['width']) && isset($requestEnv['height']) ) { + // proportional scaling within requested width/height + $srcAspect = floatval($imageInfo['width']) / floatval($imageInfo['height']); + $fitAspect = floatval($requestEnv['width']) / floatval($requestEnv['height']); + + if ($srcAspect > $fitAspect) { + // Source image is wide compared to fit + $powReduce = $this->nearestPow2Reduce($requestEnv['width'], $imageInfo['width']); + } else { + $powReduce = $this->nearestPow2Reduce($requestEnv['height'], $imageInfo['height']); + } + $scale = pow(2, $poweReduce); + + } else if ( isset($requestEnv['width']) ) { + // Fit within width + $powReduce = $this->nearestPow2Reduce($requestEnv['width'], $imageInfo['width']); + $scale = pow(2, $powReduce); + + } else if ( isset($requestEnv['height'])) { + // Fit within height + $powReduce = $this->nearestPow2Reduce($requestEnv['height'], $imageInfo['height']); + $scale = pow(2, $powReduce); } + + $regionDimensions = $this->getRegionDimensions($imageInfo, $region); + + /* + print('imageInfo'); + print_r($imageInfo); + print('region'); + print_r($region); + print('regionDimensions'); + print_r($regionDimensions); + print('asFloat'); + print_r($this->getRegionDimensionsAsFloat($imageInfo, $region)); + die(-1); + */ + // Override depending on source image format // $$$ consider doing a 302 here instead, to make better use of the browser cache @@ -353,8 +403,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 +460,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 +627,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 +661,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 +688,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 +946,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) {