4 Builds metadata about a book on the Internet Archive in json(p) format so that the book
5 can be accessed by other software including the Internet Archive BookReader.
7 Michael Ang <http://github.com/mangtronix>
9 Copyright (c) 2008-2010 Internet Archive. Software license AGPL version 3.
11 This file is part of BookReader.
13 BookReader is free software: you can redistribute it and/or modify
14 it under the terms of the GNU Affero General Public License as published by
15 the Free Software Foundation, either version 3 of the License, or
16 (at your option) any later version.
18 BookReader is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU Affero General Public License for more details.
23 You should have received a copy of the GNU Affero General Public License
24 along with BookReader. If not, see <http://www.gnu.org/licenses/>.
27 class BookReaderMeta {
29 // Fields from _meta.xml to add to response (if present)
30 var $metaFields = array(
33 'publisher' => 'publisher',
35 'language' => 'language',
36 'contributor' => 'contributor',
37 'collection' => 'collection',
38 'page-progression' => 'pageProgression',
41 var $metaDefaults = array(
42 'pageProgression' => 'lr',
45 // Stash spot for callback data... where are closures when we need them?
46 static $cbData = NULL;
48 // Builds metadata object (to be encoded as JSON)
49 function buildMetadata($id, $itemPath, $subPrefix, $server) {
54 $this->BRFatal("No identifier specified!");
57 if ("" == $itemPath) {
58 $this->BRFatal("No itemPath specified!");
62 $this->BRFatal("No server specified!");
65 if (!preg_match("|^/\d+/items/{$id}$|", $itemPath)) {
66 $this->BRFatal("Bad id!");
69 $filesDataFile = "$itemPath/${id}_files.xml";
71 if (file_exists($filesDataFile)) {
72 $filesData = simplexml_load_file("$itemPath/${id}_files.xml");
74 $this->BRfatal("File metadata not found!");
77 $imageStackInfo = $this->findImageStack($subPrefix, $filesData);
78 if ($imageStackInfo['imageFormat'] == 'unknown') {
79 $this->BRfatal('Couldn\'t find image stack');
81 // Update subPrefix -> may have been autodetected
82 $subPrefix = $imageStackInfo['subPrefix'];
83 $subItemPath = $itemPath . '/' . $subPrefix;
86 $imageFormat = $imageStackInfo['imageFormat'];
87 $archiveFormat = $imageStackInfo['archiveFormat'];
88 $imageStackFile = $itemPath . "/" . $imageStackInfo['imageStackFile'];
90 if ("unknown" == $imageFormat) {
91 $this->BRfatal("Unknown image format");
94 if ("unknown" == $archiveFormat) {
95 $this->BRfatal("Unknown archive format");
99 $scanDataFile = "${subItemPath}_scandata.xml";
100 $scanDataZip = "$itemPath/scandata.zip";
101 if (file_exists($scanDataFile)) {
102 $this->checkPrivs($scanDataFile);
103 $scanData = simplexml_load_file($scanDataFile);
104 } else if (file_exists($scanDataZip)) {
105 $this->checkPrivs($scanDataZip);
106 $cmd = 'unzip -p ' . escapeshellarg($scanDataZip) . ' scandata.xml';
107 exec($cmd, $output, $retval);
109 $this->BRFatal("Could not unzip ScanData!");
112 $dump = join("\n", $output);
113 $scanData = simplexml_load_string($dump);
114 } else if (file_exists("$itemPath/scandata.xml")) {
115 // For e.g. Scribe v.0 books!
116 $scanData = simplexml_load_file("$itemPath/scandata.xml");
118 $this->BRFatal("ScanData file not found!");
121 $metaDataFile = "$itemPath/{$id}_meta.xml";
122 if (!file_exists($metaDataFile)) {
123 $this->BRFatal("MetaData file not found!");
127 $metaData = simplexml_load_file($metaDataFile);
129 /* Find pages by type */
131 $coverLeafs = array();
132 foreach ($scanData->pageData->page as $page) {
133 if (("Title Page" == $page->pageType) || ("Title" == $page->pageType)) {
134 if ('' == $titleLeaf) {
136 $titleLeaf = "{$page['leafNum']}";
140 if (('Cover' == $page->pageType) || ('Cover Page' == $page->pageType)) {
141 array_push($coverLeafs, $page['leafNum']);
145 // These arrays map accessible page index numbers to width, height, scanned leaf numbers
146 // and page number strings (NB: these may not be unique)
147 $pageWidths = array();
148 $pageHeights = array();
152 foreach ($scanData->pageData->page as $page) {
153 if ($this->shouldAddPage($page)) {
154 $pageWidths[$i] = intval($page->cropBox->w);
155 $pageHeights[$i] = intval($page->cropBox->h);
156 $totalHeight += intval($page->cropBox->h/4) + 10;
157 $leafNums[$i] = intval($page['leafNum']);
158 $pageNums[$i] = $page->pageNumber . '';
163 # Load some values from meta.xml
164 foreach ($this->metaFields as $srcName => $destName) {
165 if ($metaData->{$srcName}) {
166 $response[$destName] = $metaData->{$srcName} . '';
168 if (array_key_exists($destName, $this->metaDefaults)) {
169 $response[$destName] = $this->metaDefaults[$destName];
175 $response['numPages'] = count($pageNums); // $$$ renamed
176 if ('' != $titleLeaf) {
177 $response['titleLeaf'] = $titleLeaf; // $$$ change to titleIndex - do leaf mapping here
178 $titleIndex = $this->indexForLeaf($titleLeaf, $leafNums);
179 if ($titleIndex !== NULL) {
180 $response['titleIndex'] = intval($titleIndex);
183 $response['url'] = "http://www.archive.org/details/$id";
184 $response['pageWidths'] = $pageWidths;
185 $response['pageHeights'] = $pageHeights;
186 $response['pageNums'] = $pageNums;
188 // Internet Archive specific
189 $response['itemId'] = $id; // $$$ renamed
190 $response['subPrefix'] = $subPrefix; // $$$ renamed
191 $response['itemPath'] = $itemPath;
192 $response['zip'] = $imageStackFile;
193 $response['server'] = $server;
194 $response['imageFormat'] = $imageFormat;
195 $response['archiveFormat'] = $archiveFormat;
196 $response['leafNums'] = $leafNums;
197 $response['previewImage'] = $this->previewURL('preview', $response);
199 // URL to title image
200 if ('' != $titleLeaf) {
201 $response['titleImage'] = $this->previewURL('title', $response);
204 if (count($coverLeafs) > 0) {
205 $coverIndices = array();
206 $coverImages = array();
207 foreach ($coverLeafs as $key => $leafNum) {
208 array_push($coverIndices, $this->indexForLeaf($leafNum, $leafNums));
209 // $$$ TODO use preview API once it supports multiple covers
210 array_push($coverImages, $this->imageUrl($leafNum, $response));
213 $response['coverIndices'] = $coverIndices;
214 $response['coverImages'] = $coverImages;
220 function emitResponse($metadata) {
221 $callback = $_REQUEST['callback'];
223 $contentType = 'application/json'; // default
225 if (! $this->isValidCallback($callback) ) {
226 $this->BRfatal("Invalid callback");
228 $contentType = 'text/javascript'; // JSONP is not JSON
231 header('Content-type: ' . $contentType . ';charset=UTF-8');
232 header('Access-Control-Allow-Origin: *'); // allow cross-origin requests
235 print $callback . '( ';
237 print json_encode($metadata);
243 function BRFatal($string) {
244 // $$$ TODO log error
245 throw new Exception("Metadata error: $string");
246 //echo "alert('$string');\n";
250 // Returns true if a page should be added based on it's information in
252 function shouldAddPage($page) {
253 // Return false only if the page is marked addToAccessFormats false.
254 // If there is no assertion we assume it should be added.
255 if (isset($page->addToAccessFormats)) {
256 if ("false" == strtolower(trim($page->addToAccessFormats))) {
264 // Returns { 'imageFormat' => , 'archiveFormat' => '} given a sub-item prefix and loaded xml data
265 function findImageStack($subPrefix, $filesData) {
267 // The order of the image formats determines which will be returned first
268 $imageFormats = array('JP2' => 'jp2', 'TIFF' => 'tif', 'JPEG' => 'jpg');
269 $imageFormatOrder = array_values($imageFormats);
270 $archiveFormats = array('ZIP' => 'zip', 'Tar' => 'tar');
271 $imageGroup = implode('|', array_keys($imageFormats));
272 $archiveGroup = implode('|', array_keys($archiveFormats));
273 // $$$ Currently only return processed images
274 $imageStackRegex = "/Single Page (Processed) (${imageGroup}) (${archiveGroup})/";
277 // - Find potential image stacks, regardless of subPrefix
278 // - If not given subPrefix sort based on potential subPrefix and assign based on asciibetical first
279 // - Filter results by subPrefix
280 // - Sort based on image format
283 $imageStacks = array();
284 foreach ($filesData->file as $file) {
285 if ( preg_match($imageStackRegex, $file->format, $matches) === 1 ) {
286 $imageFormat = $imageFormats[$matches[2]];
287 $archiveFormat = $archiveFormats[$matches[3]];
288 $imageStackFile = $file['name'] . '';
290 if ( preg_match("#(.*)_${imageFormat}\.${archiveFormat}#", $imageStackFile, $matches) === 0) {
291 // stack filename not regular
294 array_push($imageStacks, array(
295 'imageFormat' => $imageFormat,
296 'archiveFormat' => $archiveFormat,
297 'imageStackFile' => $imageStackFile,
298 'subPrefix' => $matches[1])
306 // print("found subPrefix $subPrefix\n");
307 // print_r($imageStacks);
310 function subPrefixSort($imageStackA, $imageStackB) {
311 return strcmp($imageStackA['subPrefix'], $imageStackB['subPrefix']);
314 usort($imageStacks, 'subPrefixSort');
315 $subPrefix = $imageStacks[0]['subPrefix'];
318 self::$cbData = $subPrefix;
319 function subPrefixFilter($imageStack) {
320 return $imageStack['subPrefix'] == BookReaderMeta::$cbData;
322 $imageStacks = array_filter($imageStacks, 'subPrefixFilter');
324 function formatSort($imageStackA, $imageStackB) {
325 $formatA = $imageStackA['imageFormat'];
326 $formatB = $imageStackB['imageFormat'];
327 if ($formatA == $formatB) {
331 $indexA = array_search($formatA, $imageFormatOrder);
332 $indexB = array_search($formatB, $imageFormatOrder);
333 // We already matched base on format, so both indices should be set
334 if ($indexA == $indexB) {
337 return ($indexA < $indexB) ? 1 : -1;
339 usort($imageStacks, 'formatSort'); // necessary to remap keys
341 if ( count($imageStacks) > 0 ) {
342 return $imageStacks[0];
344 return array('imageFormat' => 'unknown', 'archiveFormat' => 'unknown', 'imageStackFile' => 'unknown');
348 function isValidCallback($identifier) {
349 $pattern = '/^[a-zA-Z_$][a-zA-Z0-9_$]*$/';
350 return preg_match($pattern, $identifier) == 1;
353 function indexForLeaf($leafNum, $leafNums) {
354 $key = array_search($leafNum, $leafNums);
355 if ($key === FALSE) {
362 function leafForIndex($index, $leafNums) {
363 return $leafNums[$index]; // $$$ todo change to instance variables
366 function imageURL($leafNum, $metadata, $scale = null, $rotate = null) {
367 // "Under the hood", non-public, dynamically changing (achtung!) image URLs currently look like:
368 // http://{server}/BookReader/BookReaderImages.php?zip={zipPath}&file={filePath}&scale={scale}&rotate={rotate}
369 // e.g. http://ia311213.us.archive.org/BookReader/BookReaderImages.php?zip=/0/items/coloritsapplicat00andriala/coloritsapplicat00andriala_jp2.zip&file=coloritsapplicat00andriala_jp2/coloritsapplicat00andriala_0009.jp2&scale=8&rotate=0
372 $filePath = $this->imageFilePath($leafNum, $metadata['subPrefix'], $metadata['imageFormat']);
373 $url = 'http://' . $metadata['server'] . '/BookReader/BookReaderImages.php?zip=' . $metadata['zip'] . '&file=' . $filePath;
375 if ($scale !== null) {
376 $url .= '&scale=' . $scale;
378 if ($rotate !== null) {
379 $url .= '&rotate=' . $rotate;
385 // $$$ move inside BookReaderPreview
386 function previewURL($page, $metadata) {
388 'id' => $metadata['itemId'],
389 'subPrefix' => $metadata['subPrefix'],
390 'itemPath' => $metadata['itemPath'],
391 'server' => $metadata['server'],
395 return 'http://' . $metadata['server'] . '/BookReader/BookReaderPreview.php?' . http_build_query($query, '', '&');
398 function imageFilePath($leafNum, $subPrefix, $format) {
399 $pathParts = pathinfo($subPrefix);
400 $almostIdentifier = $pathParts['filename'];
401 return sprintf("%s_%s/%s_%04d.%s", $almostIdentifier, $format, $almostIdentifier, intval($leafNum), $format);
404 // Parse date from _meta.xml to integer
405 function parseYear($dateFromMetaXML) {
406 // grab the first run of digits
407 if (preg_match('|(\d+)|', $dateFromMetaXML, $matches)) {
408 return (int)$matches[1];
413 function processRequest($requestEnv) {
414 $id = $requestEnv['itemId']; // $$$ renamed
415 $itemPath = $requestEnv['itemPath'];
416 $subPrefix = $requestEnv['subPrefix']; // $$$ renamed
417 $server = $requestEnv['server'];
419 // Check if we're on a dev vhost and point to JSIA in the user's public_html on the datanode
420 // $$$ TODO consolidate this logic
421 $devHosts = array('testflip', 'rkumar', 'mang');
422 foreach ($devHosts as $host) {
423 if (strpos($_SERVER["REQUEST_URI"], '/~' . $host) === 0) { // Serving out of home dir
424 $server .= ':80/' . $host;
428 $this->emitResponse( $this->buildMetadata($id, $itemPath, $subPrefix, $server) );
431 function checkPrivs($filename) {
432 if (!is_readable($filename)) {
433 header('HTTP/1.1 403 Forbidden');