PageRenderTime 56ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/BookReaderIA/datanode/BookReaderImages.inc.php

https://github.com/gootdude/bookreader
PHP | 887 lines | 544 code | 143 blank | 200 comment | 103 complexity | 614081309a7aa6828de741837c62f531 MD5 | raw file
Possible License(s): AGPL-3.0
  1. <?php
  2. /*
  3. Copyright(c) 2008-2010 Internet Archive. Software license AGPL version 3.
  4. This file is part of BookReader. The full source code can be found at GitHub:
  5. http://github.com/openlibrary/bookreader
  6. The canonical short name of an image type is the same as in the MIME type.
  7. For example both .jpeg and .jpg are considered to have type "jpeg" since
  8. the MIME type is "image/jpeg".
  9. BookReader is free software: you can redistribute it and/or modify
  10. it under the terms of the GNU Affero General Public License as published by
  11. the Free Software Foundation, either version 3 of the License, or
  12. (at your option) any later version.
  13. BookReader is distributed in the hope that it will be useful,
  14. but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. GNU Affero General Public License for more details.
  17. You should have received a copy of the GNU Affero General Public License
  18. along with BookReader. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. require_once("BookReaderMeta.inc.php");
  21. class BookReaderImages
  22. {
  23. public static $MIMES = array('gif' => 'image/gif',
  24. 'jp2' => 'image/jp2',
  25. 'jpg' => 'image/jpeg',
  26. 'jpeg' => 'image/jpeg',
  27. 'png' => 'image/png',
  28. 'tif' => 'image/tiff',
  29. 'tiff' => 'image/tiff');
  30. public static $EXTENSIONS = array('gif' => 'gif',
  31. 'jp2' => 'jp2',
  32. 'jpeg' => 'jpeg',
  33. 'jpg' => 'jpeg',
  34. 'png' => 'png',
  35. 'tif' => 'tiff',
  36. 'tiff' => 'tiff');
  37. // Width when generating thumbnails
  38. public static $imageSizes = array(
  39. 'thumb' => 100,
  40. 'small' => 256,
  41. 'medium' => 512,
  42. 'large' => 2048,
  43. );
  44. // Keys in the image permalink urls, e.g. http://www.archive.org/download/itemid/page/cover_{keyval}_{keyval}.jpg
  45. public static $imageUrlKeys = array(
  46. //'r' => 'reduce', // pow of 2 reduction
  47. 's' => 'scale', // $$$ scale is downscaling factor in BookReaderImages but most people call this "reduce"
  48. 'region' => 'region',
  49. 'tile' => 'tile',
  50. 'w' => 'width',
  51. 'h' => 'height',
  52. 'rotate' => 'rotate'
  53. );
  54. // Paths to command-line tools
  55. var $exiftool = '/petabox/sw/books/exiftool/exiftool';
  56. var $kduExpand = '/petabox/sw/bin/kdu_expand';
  57. // Name of temporary files, to be cleaned at exit
  58. var $tempFiles = array();
  59. /*
  60. * Serve an image request that requires looking up the book metadata
  61. *
  62. * Code path:
  63. * - Get book metadata
  64. * - Parse the requested page (e.g. cover_t.jpg, n5_r4.jpg) to determine which page type,
  65. * size and format (etc) is being requested
  66. * - Determine the leaf number corresponding to the page
  67. * - Determine scaling values
  68. * - Serve image request now that all information has been gathered
  69. */
  70. function serveLookupRequest($requestEnv) {
  71. $brm = new BookReaderMeta();
  72. try {
  73. $metadata = $brm->buildMetadata($_REQUEST['id'], $_REQUEST['itemPath'], $_REQUEST['subPrefix'], $_REQUEST['server']);
  74. } catch (Exception $e) {
  75. $this->BRfatal($e->getMessage());
  76. }
  77. $page = $_REQUEST['page'];
  78. // Index of image to return
  79. $imageIndex = null;
  80. // deal with subPrefix
  81. if ($_REQUEST['subPrefix']) {
  82. $parts = split('/', $_REQUEST['subPrefix']);
  83. $bookId = $parts[count($parts) - 1 ];
  84. } else {
  85. $bookId = $_REQUEST['id'];
  86. }
  87. $pageInfo = $this->parsePageRequest($page, $bookId);
  88. $basePage = $pageInfo['type'];
  89. $leaf = null;
  90. switch ($basePage) {
  91. case 'title':
  92. if (! array_key_exists('titleIndex', $metadata)) {
  93. $this->BRfatal("No title page asserted in book");
  94. }
  95. $imageIndex = $metadata['titleIndex'];
  96. break;
  97. /* Old 'cover' behaviour where it would show cover 0 if it exists or return 404.
  98. Could be re-added as cover0, cover1, etc
  99. case 'cover':
  100. if (! array_key_exists('coverIndices', $metadata)) {
  101. $this->BRfatal("No cover asserted in book");
  102. }
  103. $imageIndex = $metadata['coverIndices'][0]; // $$$ TODO add support for other covers
  104. break;
  105. */
  106. case 'preview':
  107. case 'cover': // Show our best guess if cover is requested
  108. // Preference is:
  109. // Cover page if book was published >= 1950
  110. // Title page
  111. // Cover page
  112. // Page 0
  113. if ( array_key_exists('date', $metadata) && array_key_exists('coverIndices', $metadata) ) {
  114. if ($brm->parseYear($metadata['date']) >= 1950) {
  115. $imageIndex = $metadata['coverIndices'][0];
  116. break;
  117. }
  118. }
  119. if (array_key_exists('titleIndex', $metadata)) {
  120. $imageIndex = $metadata['titleIndex'];
  121. break;
  122. }
  123. if (array_key_exists('coverIndices', $metadata)) {
  124. $imageIndex = $metadata['coverIndices'][0];
  125. break;
  126. }
  127. // First page
  128. $imageIndex = 0;
  129. break;
  130. case 'n':
  131. // Accessible index page
  132. $imageIndex = intval($pageInfo['value']);
  133. break;
  134. case 'page':
  135. // Named page
  136. $index = array_search($pageInfo['value'], $metadata['pageNums']);
  137. if ($index === FALSE) {
  138. // Not found
  139. $this->BRfatal("Page not found");
  140. break;
  141. }
  142. $imageIndex = $index;
  143. break;
  144. case 'leaf':
  145. // Leaf explicitly specified
  146. $leaf = $pageInfo['value'];
  147. break;
  148. default:
  149. // Shouldn't be possible
  150. $this->BRfatal("Unrecognized page type requested");
  151. break;
  152. }
  153. if (is_null($leaf)) {
  154. // Leaf was not explicitly set -- look it up
  155. $leaf = $brm->leafForIndex($imageIndex, $metadata['leafNums']);
  156. }
  157. $requestEnv = array(
  158. 'zip' => $metadata['zip'],
  159. 'file' => $brm->imageFilePath($leaf, $metadata['subPrefix'], $metadata['imageFormat']),
  160. 'ext' => 'jpg', // XXX should pass through ext
  161. );
  162. // remove non-passthrough keys from pageInfo
  163. unset($pageInfo['type']);
  164. unset($pageInfo['value']);
  165. // add pageinfo to request
  166. $requestEnv = array_merge($pageInfo, $requestEnv);
  167. // Return image data - will check privs
  168. $this->serveRequest($requestEnv);
  169. }
  170. /*
  171. * Returns a page image when all parameters such as the image stack location are
  172. * passed in.
  173. *
  174. * Approach:
  175. *
  176. * Get info about requested image (input)
  177. * Get info about requested output format
  178. * Determine processing parameters
  179. * Process image
  180. * Return image data
  181. * Clean up temporary files
  182. */
  183. function serveRequest($requestEnv) {
  184. // Process some of the request parameters
  185. $zipPath = $requestEnv['zip'];
  186. $file = $requestEnv['file'];
  187. if (! $ext) {
  188. $ext = $requestEnv['ext'];
  189. } else {
  190. // Default to jpg
  191. $ext = 'jpeg';
  192. }
  193. if (isset($requestEnv['callback'])) {
  194. // validate callback is valid JS identifier (only)
  195. $callback = $requestEnv['callback'];
  196. $identifierPatt = '/^[[:alpha:]$_]([[:alnum:]$_])*$/';
  197. if (! preg_match($identifierPatt, $callback)) {
  198. $this->BRfatal('Invalid callback');
  199. }
  200. } else {
  201. $callback = null;
  202. }
  203. if ( !file_exists($zipPath) ) {
  204. $this->BRfatal('Image stack does not exist at ' . $zipPath);
  205. }
  206. // Make sure the image stack is readable - return 403 if not
  207. $this->checkPrivs($zipPath);
  208. // Get the image size and depth
  209. $imageInfo = $this->getImageInfo($zipPath, $file);
  210. // Output json if requested
  211. if ('json' == $ext) {
  212. // $$$ we should determine the output size first based on requested scale
  213. $this->outputJSON($imageInfo, $callback); // $$$ move to BookReaderRequest
  214. exit;
  215. }
  216. // Unfortunately kakadu requires us to know a priori if the
  217. // output file should be .ppm or .pgm. By decompressing to
  218. // .bmp kakadu will write a file we can consistently turn into
  219. // .pnm. Really kakadu should support .pnm as the file output
  220. // extension and automatically write ppm or pgm format as
  221. // appropriate.
  222. $this->decompressToBmp = true; // $$$ shouldn't be necessary if we use file info to determine output format
  223. if ($this->decompressToBmp) {
  224. $stdoutLink = '/tmp/stdout.bmp';
  225. } else {
  226. $stdoutLink = '/tmp/stdout.ppm';
  227. }
  228. $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
  229. // Rotate is currently only supported for jp2 since it does not add server load
  230. $allowedRotations = array("0", "90", "180", "270");
  231. $rotate = $requestEnv['rotate'];
  232. if ( !in_array($rotate, $allowedRotations) ) {
  233. $rotate = "0";
  234. }
  235. // Image conversion options
  236. $pngOptions = '';
  237. $jpegOptions = '-quality 75';
  238. // The pbmreduce reduction factor produces an image with dimension 1/n
  239. // The kakadu reduction factor produces an image with dimension 1/(2^n)
  240. // We interpret the requested size and scale, look at image format, and determine the
  241. // actual scaling to be returned to the client. We generally return the largest
  242. // power of 2 reduction that is larger than the requested size in order to reduce
  243. // image processing load on our cluster. The client should then scale to their final
  244. // needed size.
  245. // Set scale from height or width if set
  246. if (isset($requestEnv['height'])) {
  247. $powReduce = $this->nearestPow2Reduce($requestEnv['height'], $imageInfo['height']);
  248. $scale = pow(2, $powReduce);
  249. } else if (isset($requestEnv['width'])) {
  250. $powReduce = $this->nearestPow2Reduce($requestEnv['width'], $imageInfo['width']);
  251. $scale = pow(2, $powReduce);
  252. } else {
  253. // Set scale from named size (e.g. 'large') if set
  254. $size = $requestEnv['size'];
  255. if ( $size && array_key_exists($size, self::$imageSizes)) {
  256. $srcRatio = floatval($imageInfo['width']) / floatval($imageInfo['height']);
  257. if ($srcRatio > 1) {
  258. // wide
  259. $dimension = 'width';
  260. } else {
  261. $dimension = 'height';
  262. }
  263. $powReduce = $this->nearestPow2Reduce(self::$imageSizes[$size], $imageInfo[$dimension]);
  264. $scale = pow(2, $powReduce);
  265. } else {
  266. // No named size - use explicit scale, if given
  267. $scale = $requestEnv['scale'];
  268. if (!$scale) {
  269. $scale = 1;
  270. }
  271. $powReduce = $this->nearestPow2ForScale($scale);
  272. // ensure integer scale
  273. $scale = pow(2, $powReduce);
  274. }
  275. }
  276. // Override depending on source image format
  277. // $$$ consider doing a 302 here instead, to make better use of the browser cache
  278. // Limit scaling for 1-bit images. See https://bugs.edge.launchpad.net/bookreader/+bug/486011
  279. if (1 == $imageInfo['bits']) {
  280. if ($scale > 1) {
  281. $scale /= 2;
  282. $powReduce -= 1;
  283. // Hard limit so there are some black pixels to use!
  284. if ($scale > 4) {
  285. $scale = 4;
  286. $powReduce = 2;
  287. }
  288. }
  289. }
  290. if (!file_exists($stdoutLink))
  291. {
  292. system('ln -s /dev/stdout ' . $stdoutLink);
  293. }
  294. putenv('LD_LIBRARY_PATH=/petabox/sw/lib/kakadu');
  295. $unzipCmd = $this->getUnarchiveCommand($zipPath, $file);
  296. $decompressCmd = $this->getDecompressCmd($imageInfo['type'], $powReduce, $rotate, $scale, $stdoutLink);
  297. // Non-integer scaling is currently disabled on the cluster
  298. // if (isset($_REQUEST['height'])) {
  299. // $cmd .= " | pnmscale -height {$_REQUEST['height']} ";
  300. // }
  301. switch ($ext) {
  302. case 'png':
  303. $compressCmd = ' | pnmtopng ' . $pngOptions;
  304. break;
  305. case 'jpeg':
  306. case 'jpg':
  307. default:
  308. $compressCmd = ' | pnmtojpeg ' . $jpegOptions;
  309. $ext = 'jpeg'; // for matching below
  310. break;
  311. }
  312. if (($ext == $fileExt) && ($scale == 1) && ($rotate === "0")) {
  313. // Just pass through original data if same format and size
  314. $cmd = $unzipCmd;
  315. } else {
  316. $cmd = $unzipCmd . $decompressCmd . $compressCmd;
  317. }
  318. // print $cmd;
  319. $filenameForClient = $this->filenameForClient($file, $ext);
  320. $headers = array('Content-type: '. self::$MIMES[$ext],
  321. 'Cache-Control: max-age=15552000',
  322. 'Content-disposition: inline; filename=' . $filenameForClient);
  323. $errorMessage = '';
  324. if (! $this->passthruIfSuccessful($headers, $cmd, $errorMessage)) { // $$$ move to BookReaderRequest
  325. // $$$ automated reporting
  326. trigger_error('BookReader Processing Error: ' . $cmd . ' -- ' . $errorMessage, E_USER_WARNING);
  327. // Try some content-specific recovery
  328. $recovered = false;
  329. if ($imageInfo['type'] == 'jp2') {
  330. $records = $this->getJp2Records($zipPath, $file);
  331. if (array_key_exists('Clevels', $records)) {
  332. $maxReduce = intval($records['Clevels']);
  333. trigger_error("BookReader using max reduce $maxReduce from jp2 records");
  334. } else {
  335. $maxReduce = 0;
  336. }
  337. $powReduce = min($powReduce, $maxReduce);
  338. $reduce = pow(2, $powReduce);
  339. $cmd = $unzipCmd . $this->getDecompressCmd($imageInfo['type'], $powReduce, $rotate, $scale, $stdoutLink) . $compressCmd;
  340. trigger_error('BookReader rerunning with new cmd: ' . $cmd, E_USER_WARNING);
  341. if ($this->passthruIfSuccessful($headers, $cmd, $errorMessage)) { // $$$ move to BookReaderRequest
  342. $recovered = true;
  343. } else {
  344. $this->cleanup();
  345. trigger_error('BookReader fallback image processing also failed: ' . $errorMessage, E_USER_WARNING);
  346. }
  347. }
  348. if (! $recovered) {
  349. $this->BRfatal('Problem processing image - command failed');
  350. }
  351. }
  352. $this->cleanup();
  353. }
  354. function getUnarchiveCommand($archivePath, $file)
  355. {
  356. $lowerPath = strtolower($archivePath);
  357. if (preg_match('/\.([^\.]+)$/', $lowerPath, $matches)) {
  358. $suffix = $matches[1];
  359. if ($suffix == 'zip') {
  360. return 'unzip -p '
  361. . escapeshellarg($archivePath)
  362. . ' ' . escapeshellarg($file);
  363. } else if ($suffix == 'tar') {
  364. return ' ( 7z e -so '
  365. . escapeshellarg($archivePath)
  366. . ' ' . escapeshellarg($file) . ' 2>/dev/null ) ';
  367. } else {
  368. $this->BRfatal('Incompatible archive format');
  369. }
  370. } else {
  371. $this->BRfatal('Bad image stack path');
  372. }
  373. $this->BRfatal('Bad image stack path or archive format');
  374. }
  375. /*
  376. * Returns the image type associated with the file extension.
  377. */
  378. function imageExtensionToType($extension)
  379. {
  380. if (array_key_exists($extension, self::$EXTENSIONS)) {
  381. return self::$EXTENSIONS[$extension];
  382. } else {
  383. $this->BRfatal('Unknown image extension');
  384. }
  385. }
  386. /*
  387. * Get the image information. The returned associative array fields will
  388. * vary depending on the image type. The basic keys are width, height, type
  389. * and bits.
  390. */
  391. function getImageInfo($zipPath, $file)
  392. {
  393. return $this->getImageInfoFromExif($zipPath, $file); // this is fast
  394. /*
  395. $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
  396. $type = imageExtensionToType($fileExt);
  397. switch ($type) {
  398. case "jp2":
  399. return getImageInfoFromJp2($zipPath, $file);
  400. default:
  401. return getImageInfoFromExif($zipPath, $file);
  402. }
  403. */
  404. }
  405. // Get the records of of JP2 as returned by kdu_expand
  406. function getJp2Records($zipPath, $file)
  407. {
  408. $cmd = $this->getUnarchiveCommand($zipPath, $file)
  409. . ' | ' . $this->kduExpand
  410. . ' -no_seek -quiet -i /dev/stdin -record /dev/stdout';
  411. exec($cmd, $output);
  412. $records = Array();
  413. foreach ($output as $line) {
  414. $elems = explode("=", $line, 2);
  415. if (1 == count($elems)) {
  416. // delimiter not found
  417. continue;
  418. }
  419. $records[$elems[0]] = $elems[1];
  420. }
  421. return $records;
  422. }
  423. /*
  424. * Get the image width, height and depth using the EXIF information.
  425. */
  426. function getImageInfoFromExif($zipPath, $file)
  427. {
  428. // We look for all the possible tags of interest then act on the
  429. // ones presumed present based on the file type
  430. $tagsToGet = ' -ImageWidth -ImageHeight -FileType' // all formats
  431. . ' -BitsPerComponent -ColorSpace' // jp2
  432. . ' -BitDepth' // png
  433. . ' -BitsPerSample'; // tiff
  434. $cmd = $this->getUnarchiveCommand($zipPath, $file)
  435. . ' | '. $this->exiftool . ' -S -fast' . $tagsToGet . ' -';
  436. exec($cmd, $output);
  437. $tags = Array();
  438. foreach ($output as $line) {
  439. $keyValue = explode(": ", $line);
  440. $tags[$keyValue[0]] = $keyValue[1];
  441. }
  442. $width = intval($tags["ImageWidth"]);
  443. $height = intval($tags["ImageHeight"]);
  444. $type = strtolower($tags["FileType"]);
  445. switch ($type) {
  446. case "jp2":
  447. $bits = intval($tags["BitsPerComponent"]);
  448. break;
  449. case "tiff":
  450. $bits = intval($tags["BitsPerSample"]);
  451. break;
  452. case "jpeg":
  453. $bits = 8;
  454. break;
  455. case "png":
  456. $bits = intval($tags["BitDepth"]);
  457. break;
  458. default:
  459. $this->BRfatal("Unsupported image type $type for file $file in $zipPath");
  460. break;
  461. }
  462. $retval = Array('width' => $width, 'height' => $height,
  463. 'bits' => $bits, 'type' => $type);
  464. return $retval;
  465. }
  466. /*
  467. * Output JSON given the imageInfo associative array
  468. */
  469. function outputJSON($imageInfo, $callback)
  470. {
  471. header('Content-type: text/plain');
  472. $jsonOutput = json_encode($imageInfo);
  473. if ($callback) {
  474. $jsonOutput = $callback . '(' . $jsonOutput . ');';
  475. }
  476. echo $jsonOutput;
  477. }
  478. function getDecompressCmd($imageType, $powReduce, $rotate, $scale, $stdoutLink) {
  479. switch ($imageType) {
  480. case 'jp2':
  481. $decompressCmd =
  482. " | " . $this->kduExpand . " -no_seek -quiet -reduce $powReduce -rotate $rotate -i /dev/stdin -o " . $stdoutLink;
  483. if ($this->decompressToBmp) {
  484. // We suppress output since bmptopnm always outputs on stderr
  485. $decompressCmd .= ' | (bmptopnm 2>/dev/null)';
  486. }
  487. break;
  488. case 'tiff':
  489. // We need to create a temporary file for tifftopnm since it cannot
  490. // work on a pipe (the file must be seekable).
  491. // We use the BookReaderTiff prefix to give a hint in case things don't
  492. // get cleaned up.
  493. $tempFile = tempnam("/tmp", "BookReaderTiff");
  494. array_push($this->tempFiles, $tempFile);
  495. // $$$ look at bit depth when reducing
  496. $decompressCmd =
  497. ' > ' . $tempFile . ' ; tifftopnm ' . $tempFile . ' 2>/dev/null' . $this->reduceCommand($scale);
  498. break;
  499. case 'jpeg':
  500. $decompressCmd = ' | ( jpegtopnm 2>/dev/null ) ' . $this->reduceCommand($scale);
  501. break;
  502. case 'png':
  503. $decompressCmd = ' | ( pngtopnm 2>/dev/null ) ' . $this->reduceCommand($scale);
  504. break;
  505. default:
  506. $this->BRfatal('Unknown image type: ' . $imageType);
  507. break;
  508. }
  509. return $decompressCmd;
  510. }
  511. // If the command has its initial output on stdout the headers will be emitted followed
  512. // by the stdout output. If initial output is on stderr an error message will be
  513. // returned.
  514. //
  515. // Returns:
  516. // true - if command emits stdout and has zero exit code
  517. // false - command has initial output on stderr or non-zero exit code
  518. // &$errorMessage - error string if there was an error
  519. //
  520. // $$$ Tested with our command-line image processing. May be deadlocks for
  521. // other cases.
  522. function passthruIfSuccessful($headers, $cmd, &$errorMessage)
  523. {
  524. $retVal = false;
  525. $errorMessage = '';
  526. $descriptorspec = array(
  527. 0 => array("pipe", "r"), // stdin is a pipe that the child will read from
  528. 1 => array("pipe", "w"), // stdout is a pipe that the child will write to
  529. 2 => array("pipe", "w"), // stderr is a pipe to write to
  530. );
  531. $cwd = NULL;
  532. $env = NULL;
  533. $process = proc_open($cmd, $descriptorspec, $pipes, $cwd, $env);
  534. if (is_resource($process)) {
  535. // $pipes now looks like this:
  536. // 0 => writeable handle connected to child stdin
  537. // 1 => readable handle connected to child stdout
  538. // 2 => readable handle connected to child stderr
  539. $stdin = $pipes[0];
  540. $stdout = $pipes[1];
  541. $stderr = $pipes[2];
  542. // check whether we get input first on stdout or stderr
  543. $read = array($stdout, $stderr);
  544. $write = NULL;
  545. $except = NULL;
  546. $numChanged = stream_select($read, $write, $except, NULL); // $$$ no timeout
  547. if (false === $numChanged) {
  548. // select failed
  549. $errorMessage = 'Select failed';
  550. $retVal = false;
  551. error_log('BookReader select failed!');
  552. } else {
  553. if (in_array($stderr, $read)) {
  554. // Either content in stderr, or stderr is closed (could read 0 bytes)
  555. $error = stream_get_contents($stderr);
  556. if ($error) {
  557. $errorMessage = $error;
  558. $retVal = false;
  559. fclose($stderr);
  560. fclose($stdout);
  561. fclose($stdin);
  562. // It is important that you close any pipes before calling
  563. // proc_close in order to avoid a deadlock
  564. proc_close($process);
  565. return $retVal;
  566. }
  567. }
  568. $output = fopen('php://output', 'w');
  569. foreach($headers as $header) {
  570. header($header);
  571. }
  572. stream_copy_to_stream($pipes[1], $output);
  573. fclose($output); // okay since tied to special php://output
  574. $retVal = true;
  575. }
  576. fclose($stderr);
  577. fclose($stdout);
  578. fclose($stdin);
  579. // It is important that you close any pipes before calling
  580. // proc_close in order to avoid a deadlock
  581. $cmdRet = proc_close($process);
  582. if (0 != $cmdRet) {
  583. $retVal = false;
  584. $errorMessage .= "Command failed with result code " . $cmdRet;
  585. }
  586. }
  587. return $retVal;
  588. }
  589. function BRfatal($string) {
  590. $this->cleanup();
  591. throw new Exception("Image error: $string");
  592. }
  593. // Returns true if using a power node
  594. // XXX change to "on red box" - not working for new Xeon
  595. function onPowerNode() {
  596. exec("lspci | fgrep -c Realtek", $output, $return);
  597. if ("0" != $output[0]) {
  598. return true;
  599. } else {
  600. exec("egrep -q AMD /proc/cpuinfo", $output, $return);
  601. if ($return == 0) {
  602. return true;
  603. }
  604. }
  605. return false;
  606. }
  607. function reduceCommand($scale) {
  608. if (1 != $scale) {
  609. if ($this->onPowerNode()) {
  610. return ' | pnmscale -reduce ' . $scale . ' 2>/dev/null ';
  611. } else {
  612. return ' | pnmscale -nomix -reduce ' . $scale . ' 2>/dev/null ';
  613. }
  614. } else {
  615. return '';
  616. }
  617. }
  618. function checkPrivs($filename) {
  619. // $$$ we assume here that requests for the title, cover or preview
  620. // come in via BookReaderPreview.php which will be re-run with
  621. // privileges after we return the 403
  622. if (!is_readable($filename)) {
  623. header('HTTP/1.1 403 Forbidden');
  624. exit(0);
  625. }
  626. }
  627. // Given file path (inside archive) and output file extension, return a filename
  628. // suitable for Content-disposition header
  629. function filenameForClient($filePath, $ext) {
  630. $pathParts = pathinfo($filePath);
  631. if ('jpeg' == $ext) {
  632. $ext = 'jpg';
  633. }
  634. return $pathParts['filename'] . '.' . $ext;
  635. }
  636. // Returns the nearest power of 2 reduction factor that results in a larger image
  637. function nearestPow2Reduce($desiredDimension, $sourceDimension) {
  638. $ratio = floatval($sourceDimension) / floatval($desiredDimension);
  639. return $this->nearestPow2ForScale($ratio);
  640. }
  641. // Returns nearest power of 2 reduction factor that results in a larger image
  642. function nearestPow2ForScale($scale) {
  643. $scale = intval($scale);
  644. if ($scale <= 1) {
  645. return 0;
  646. }
  647. $binStr = decbin($scale); // convert to binary string. e.g. 5 -> '101'
  648. return strlen($binStr) - 1;
  649. }
  650. /*
  651. * Parses a page request like "page5_r2.jpg" or "cover_t.jpg" to corresponding
  652. * page type, size, reduce, and format
  653. */
  654. function parsePageRequest($pageRequest, $bookPrefix) {
  655. // Will hold parsed results
  656. $pageInfo = array();
  657. // Normalize
  658. $pageRequest = strtolower($pageRequest);
  659. // Pull off extension
  660. if (preg_match('#(.*)\.([^.]+)$#', $pageRequest, $matches) === 1) {
  661. $pageRequest = $matches[1];
  662. $extension = $matches[2];
  663. if ($extension == 'jpeg') {
  664. $extension = 'jpg';
  665. }
  666. } else {
  667. $extension = 'jpg';
  668. }
  669. $pageInfo['extension'] = $extension;
  670. // Split parts out
  671. $parts = explode('_', $pageRequest);
  672. // Remove book prefix if it was included (historical)
  673. if ($parts[0] == $bookPrefix) {
  674. array_shift($parts);
  675. }
  676. if (count($parts) === 0) {
  677. $this->BRfatal('No page type specified');
  678. }
  679. $page = array_shift($parts);
  680. $pageTypes = array(
  681. 'page' => 'str',
  682. 'n' => 'num',
  683. 'cover' => 'single',
  684. 'preview' => 'single',
  685. 'title' => 'single',
  686. 'leaf' => 'num'
  687. );
  688. // Look for known page types
  689. foreach ( $pageTypes as $pageName => $kind ) {
  690. if ( preg_match('#^(' . $pageName . ')(.*)#', $page, $matches) === 1 ) {
  691. $pageInfo['type'] = $matches[1];
  692. switch ($kind) {
  693. case 'str':
  694. $pageInfo['value'] = $matches[2];
  695. break;
  696. case 'num':
  697. $pageInfo['value'] = intval($matches[2]);
  698. break;
  699. case 'single':
  700. break;
  701. }
  702. }
  703. }
  704. if ( !array_key_exists('type', $pageInfo) ) {
  705. $this->BRfatal('Unrecognized page type');
  706. }
  707. // Look for other known parts
  708. foreach ($parts as $part) {
  709. if ( array_key_exists($part, self::$imageSizes) ) {
  710. $pageInfo['size'] = $part;
  711. continue;
  712. }
  713. // Key must be alpha, value must start with digit and contain digits, alpha, ',' or '.'
  714. // Should prevent injection of strange values into the redirect to datanode
  715. if ( preg_match('#^([a-z]+)(\d[a-z0-9,.]*)#', $part, $matches) === 0) {
  716. // Not recognized
  717. continue;
  718. }
  719. $key = $matches[1];
  720. $value = $matches[2];
  721. if ( array_key_exists($key, self::$imageUrlKeys) ) {
  722. $pageInfo[self::$imageUrlKeys[$key]] = $value;
  723. continue;
  724. }
  725. // If we hit here, was unrecognized (no action)
  726. }
  727. return $pageInfo;
  728. }
  729. // Clean up temporary files and resources
  730. function cleanup() {
  731. foreach($this->tempFiles as $tempFile) {
  732. unlink($tempFile);
  733. }
  734. $this->tempFiles = array();
  735. }
  736. }
  737. ?>