'h' => 'height',
'x' => 'x',
'y' => 'y',
- 'rotate' => 'rotate'
+ 'rot' => 'rotate'
);
// Paths to command-line tools
$imageIndex = null;
// deal with subPrefix
- if ($_REQUEST['subPrefix']) {
+ if (array_key_exists($_REQUEST, 'subPrefix') && $_REQUEST['subPrefix']) {
$parts = explode('/', $_REQUEST['subPrefix']);
$bookId = $parts[count($parts) - 1 ];
} else {
$imageIndex = $metadata['titleIndex'];
break;
- /* Old 'cover' behaviour where it would show cover 0 if it exists or return 404.
- Could be re-added as cover0, cover1, etc
- case 'cover':
+ /* Old 'cover' behaviour where it would show cover 0 if it exists or return 404. */
+ case 'cover0':
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':
case 'cover': // Show our best guess if cover is requested
// Preference is:
- // Cover page if book was published >= 1950
+ // Cover page if book was published >= 1923
// Title page
// Cover page
// Page 0
if ( array_key_exists('date', $metadata) && array_key_exists('coverIndices', $metadata) ) {
- if ($brm->parseYear($metadata['date']) >= 1950) {
+ if ($brm->parseYear($metadata['date']) >= 1923) {
$imageIndex = $metadata['coverIndices'][0];
break;
}
*/
function serveRequest($requestEnv) {
+ // Make sure cleanup happens
+ register_shutdown_function ( array( $this, 'cleanup') );
+
// Process some of the request parameters
$zipPath = $requestEnv['zip'];
$file = $requestEnv['file'];
- if (! $ext) {
- $ext = $requestEnv['ext'];
+ if (array_key_exists('ext', $requestEnv)) {
+ $ext = $requestEnv['ext']; // Will get santized below
} else {
// Default to jpg
$ext = 'jpeg';
// Get the image size and depth
$imageInfo = $this->getImageInfo($zipPath, $file);
-
- $region = array();
- foreach (array('x', 'y', 'w', 'h') as $key) {
- if (array_key_exists($key, $requestEnv)) {
- $region[$key] = $requestEnv[$key];
- }
- }
- $regionDimensions = $this->getRegionDimensions($imageInfo, $region);
-
- /* $$$ remove
- print_r($imageInfo);
- print_r($region);
- print_r($regionDimensions);
- */
-
+
// Output json if requested
if ('json' == $ext) {
// $$$ we should determine the output size first based on requested scale
// 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)) {
}
$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
+
+ $width = $this->intAmount($requestEnv['width'], $imageInfo['width']);
+ $height = $this->intAmount($requestEnv['height'], $imageInfo['height']);
+
+ $srcAspect = floatval($imageInfo['width']) / floatval($imageInfo['height']);
+ $fitAspect = floatval($width) / floatval($height);
+
+ if ($srcAspect > $fitAspect) {
+ // Source image is wide compared to fit
+ $powReduce = $this->nearestPow2Reduce($width, $imageInfo['width']);
+ } else {
+ $powReduce = $this->nearestPow2Reduce($height, $imageInfo['height']);
+ }
+ $scale = pow(2, $poweReduce);
+
+ } else if ( isset($requestEnv['width']) ) {
+ // Fit within width
+ $width = $this->intAmount($requestEnv['width'], $imageInfo['width']);
+ $powReduce = $this->nearestPow2Reduce($width, $imageInfo['width']);
+ $scale = pow(2, $powReduce);
+
+ } else if ( isset($requestEnv['height'])) {
+ // Fit within height
+ $height = $this->intAmount($requestEnv['height'], $imageInfo['height']);
+ $powReduce = $this->nearestPow2Reduce($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
$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']} ";
// print $cmd;
$filenameForClient = $this->filenameForClient($file, $ext);
-
- $headers = array('Content-type: '. self::$MIMES[$ext],
- 'Cache-Control: max-age=15552000',
- 'Content-disposition: inline; filename=' . $filenameForClient);
-
-
+
$errorMessage = '';
- if (! $this->passthruIfSuccessful($headers, $cmd, $errorMessage)) { // $$$ move to BookReaderRequest
+ //if (! $this->passthruIfSuccessful($headers, $cmd, $errorMessage)) { // $$$ move to BookReaderRequest
+
+ $tempFile = $this->getTempFilename($ext);
+ array_push($this->tempFiles, $tempFile);
+
+ // error_log("bookreader running " . $cmd);
+ $imageCreated = $this->createOutputImage($cmd, $tempFile, $errorMessage);
+ if (! $imageCreated) {
// $$$ automated reporting
trigger_error('BookReader Processing Error: ' . $cmd . ' -- ' . $errorMessage, E_USER_WARNING);
$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
+
+ $tempFile = $this->getTempFilename($ext);
+ array_push($this->tempFiles, $tempFile);
+ $imageCreated = $this->createOutputImage($cmd, $tempFile, $errorMessage);
+ if ($imageCreated) {
$recovered = true;
} else {
$this->cleanup();
}
if (! $recovered) {
- $this->BRfatal('Problem processing image - command failed');
+ $this->BRfatal("Problem processing image - command failed:\n " . $cmd);
}
}
+ if ($imageCreated) {
+ // Send the image
+
+ $headers = array('Content-type: '. self::$MIMES[$ext],
+ 'Cache-Control: max-age=15552000',
+ 'Content-disposition: inline; filename=' . $filenameForClient,
+ 'Content-Length: ' . filesize($tempFile));
+
+ foreach($headers as $header) {
+ header($header);
+ }
+ ob_clean();
+ flush(); // attempt to send header to client
+ readfile($tempFile);
+ }
+
$this->cleanup();
}
$width = intval($tags["ImageWidth"]);
$height = intval($tags["ImageHeight"]);
$type = strtolower($tags["FileType"]);
+
+ // Treat jpx as jp2
+ if (strcmp($type,'jpx') == 0) {
+ $type = 'jp2';
+ }
switch ($type) {
case "jp2":
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).
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 -quiet -' . $regionString . $scaleString . $rotateString . ' pnm:-';
+ break;
default:
$this->BRfatal('Unknown image type: ' . $imageType);
break;
}
+
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.
// &$errorMessage - error string if there was an error
//
// $$$ Tested with our command-line image processing. May be deadlocks for
- // other cases.
+ // other cases, e.g. if there are warnings on stderr
function passthruIfSuccessful($headers, $cmd, &$errorMessage)
{
+
$retVal = false;
$errorMessage = '';
return $retVal;
}
+ function createOutputImage($cmd, $tempFile, &$errorMessage) {
+ $fullCmd = $cmd . " > " . $tempFile;
+ system($fullCmd); // $$$ better error handling
+ return file_exists($tempFile) && filesize($tempFile) > 0;
+ }
+
function BRfatal($string) {
$this->cleanup();
throw new Exception("Image error: $string");
return 0;
}
$binStr = decbin($scale); // convert to binary string. e.g. 5 -> '101'
- return strlen($binStr) - 1;
+ $largerPow2 = strlen($binStr) - 1;
+
+ return $largerPow2;
}
/*
$sourceX = 0;
if (array_key_exists('x', $regionDimensions)) {
- $sourceX = intAmount($regionDimensions['x'], $sourceDimensions['width']);
+ $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 = intAmount($regionDimensions['y'], $sourceDimensions['height']);
+ $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('w', $regionDimensions)) {
- $sourceWidth = intAmount($regionDimensions['w'], $sourceDimensions['width']);
+ 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('h', $regionDimensions)) {
- $sourceHeight = intAmount($regionDimensions['h'], $sourceDimensions['height']);
+ 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%)
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;
return $observedValue;
}
+ // Get the directory for temporary files. Use the fast in-RAM tmp if available.
+ function getTempDir() {
+ $fastbr = '/var/tmp/fast/bookreader';
+
+ if (is_writeable($fastbr)) {
+ // Our directory in fast tmp already exists
+ return $fastbr;
+ } else {
+ // Check for fast tmp and make our directory
+ $fast = '/var/tmp/fast';
+ if (is_writeable($fast)) {
+ if (mkdir($fastbr)) {
+ return $fastbr;
+ } else {
+ return $fast;
+ }
+ }
+ }
+
+ // All else failed -- system tmp that should get cleaned on reboot
+ return '/tmp';
+ }
+
+ function getTempFilename($ext) {
+ return tempnam($this->getTempDir(), "BookReaderImages");
+ }
+
// Clean up temporary files and resources
function cleanup() {
foreach($this->tempFiles as $tempFile) {
}
$this->tempFiles = array();
}
+
+ /*
+ function cleanTmp() {
+ system('find /var/tmp/fast -name "BookReaderImages*" -cmin +10 -exec rm {} \;');
+ system('find /var/tmp/fast/bookreader -name "BookReaderImages*" -cmin +10 -exec rm {} \;');
+ }
+ */
}
-?>
\ No newline at end of file
+?>