3 Copyright(c)2008 Internet Archive. Software license AGPL version 3.
5 This file is part of BookReader.
7 BookReader is free software: you can redistribute it and/or modify
8 it under the terms of the GNU Affero General Public License as published by
9 the Free Software Foundation, either version 3 of the License, or
10 (at your option) any later version.
12 BookReader is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 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/>.
21 header('Content-Type: application/javascript');
23 $id = $_REQUEST['id'];
24 $itemPath = $_REQUEST['itemPath'];
25 $subPrefix = $_REQUEST['subPrefix'];
26 $server = $_REQUEST['server'];
28 // $$$mang this code has been refactored into BookReaderMeta.inc.php for use e.g. by
29 // BookReaderPreview.php and BookReaderImages.php. The code below should be
30 // taken out and replaced by calls into BookReaderMeta
32 // Check if we're on a dev vhost and point to JSIA in the user's public_html on the datanode
34 // $$$ TODO consolidate this logic
35 if (strpos($_SERVER["REQUEST_URI"], "/~mang") === 0) { // Serving out of home dir
36 $server .= ':80/~mang';
37 } else if (strpos($_SERVER["REQUEST_URI"], "/~rkumar") === 0) { // Serving out of home dir
38 $server .= ':80/~rkumar';
39 } else if (strpos($_SERVER["REQUEST_URI"], "/~testflip") === 0) { // Serving out of home dir
40 $server .= ':80/~testflip';
46 $subItemPath = $itemPath . '/' . $subPrefix;
49 BRFatal("No identifier specified!");
52 if ("" == $itemPath) {
53 BRFatal("No itemPath specified!");
57 BRFatal("No server specified!");
60 if (!preg_match("|^/\d+/items/{$id}$|", $itemPath)) {
64 // XXX check here that subitem is okay
66 $filesDataFile = "$itemPath/${id}_files.xml";
68 if (file_exists($filesDataFile)) {
69 $filesData = simplexml_load_file("$itemPath/${id}_files.xml");
71 BRfatal("File metadata not found!");
74 $imageStackInfo = findImageStack($subPrefix, $filesData);
75 if ($imageStackInfo['imageFormat'] == 'unknown') {
76 BRfatal('Couldn\'t find image stack');
79 $imageFormat = $imageStackInfo['imageFormat'];
80 $archiveFormat = $imageStackInfo['archiveFormat'];
81 $imageStackFile = $itemPath . "/" . $imageStackInfo['imageStackFile'];
83 if ("unknown" == $imageFormat) {
84 BRfatal("Unknown image format");
87 if ("unknown" == $archiveFormat) {
88 BRfatal("Unknown archive format");
92 $scanDataFile = "${subItemPath}_scandata.xml";
93 $scanDataZip = "$itemPath/scandata.zip";
94 if (file_exists($scanDataFile)) {
95 $scanData = simplexml_load_file($scanDataFile);
96 } else if (file_exists($scanDataZip)) {
97 $cmd = 'unzip -p ' . escapeshellarg($scanDataZip) . ' scandata.xml';
98 exec($cmd, $output, $retval);
99 if ($retval != 0) BRFatal("Could not unzip ScanData!");
101 $dump = join("\n", $output);
102 $scanData = simplexml_load_string($dump);
103 } else if (file_exists("$itemPath/scandata.xml")) {
104 // For e.g. Scribe v.0 books!
105 $scanData = simplexml_load_file("$itemPath/scandata.xml");
107 BRFatal("ScanData file not found!");
110 $metaDataFile = "$itemPath/{$id}_meta.xml";
111 if (!file_exists($metaDataFile)) {
112 BRFatal("MetaData file not found!");
116 $metaData = simplexml_load_file($metaDataFile);
118 //$firstLeaf = $scanData->pageData->page[0]['leafNum'];
121 // Error reporting - this helps us fix errors quickly
122 function logError(description,page,line) {
123 if (typeof(archive_analytics) != 'undefined') {
125 'bookreader': 'error',
126 'description': description,
129 'itemid': '<?echo $id;?>',
130 'subPrefix': '<?echo $subPrefix;?>',
131 'server': '<?echo $server;?>',
132 'bookPath': '<?echo $subItemPath;?>'
135 // if no referrer set '-' as referrer
136 if (document.referrer == '') {
137 values['referrer'] = '-';
139 values['referrer'] = document.referrer;
142 if (typeof(br) != 'undefined') {
143 values['itemid'] = br.bookId;
144 values['subPrefix'] = br.subPrefix;
145 values['server'] = br.server;
146 values['bookPath'] = br.bookPath;
149 var qs = archive_analytics.format_bug(values);
151 var error_img = new Image(100,25);
152 error_img.src = archive_analytics.img_src + "?" + qs;
155 return false; // allow browser error handling so user sees there was a problem
157 window.onerror=logError;
159 br = new BookReader();
162 /* Output title leaf if marked */
164 foreach ($scanData->pageData->page as $page) {
165 if (("Title Page" == $page->pageType) || ("Title" == $page->pageType)) {
166 $titleLeaf = "{$page['leafNum']}";
171 if ('' != $titleLeaf) {
172 printf("br.titleLeaf = %d;\n", $titleLeaf);
176 br.getPageWidth = function(index) {
177 return this.pageW[index];
180 br.getPageHeight = function(index) {
181 return this.pageH[index];
184 // Returns true if page image is available rotated
185 br.canRotatePage = function(index) {
186 return 'jp2' == this.imageFormat; // Assume single format for now
189 // reduce defaults to 1 (no reduction)
190 // rotate defaults to 0 (no rotation)
191 br.getPageURI = function(index, reduce, rotate) {
195 if ('undefined' == typeof(reduce)) {
200 if ('undefined' == typeof(rotate)) {
206 var file = this._getPageFile(index);
208 // $$$ add more image stack formats here
209 return 'http://'+this.server+'/BookReader/BookReaderImages.php?zip='+this.zip+'&file='+file+'&scale='+_reduce+'&rotate='+_rotate;
212 // Get a rectangular region out of a page
213 br.getRegionURI = function(index, reduce, rotate, sourceX, sourceY, sourceWidth, sourceHeight) {
215 // Map function arguments to the url keys
216 var urlKeys = ['n', 'r', 'rot', 'x', 'y', 'w', 'h'];
218 for (var i = 0; i < arguments.length; i++) {
219 if ('undefined' != typeof(arguments[i])) {
223 page += urlKeys[i] + argument[i];
227 return 'http://'+this.server+'/BookReader/BookReaderImages.php?zip='+this.zip+'&page='+page;
230 br._getPageFile = function(index) {
231 var leafStr = '0000';
232 var imgStr = this.leafMap[index].toString();
233 var re = new RegExp("0{"+imgStr.length+"}$");
235 var insideZipPrefix = this.subPrefix.match('[^/]+$');
236 var file = insideZipPrefix + '_' + this.imageFormat + '/' + insideZipPrefix + '_' + leafStr.replace(re, imgStr) + '.' + this.imageFormat;
241 br.getPageSide = function(index) {
242 //assume the book starts with a cover (right-hand leaf)
243 //we should really get handside from scandata.xml
245 <? // Use special function if we should infer the page sides based off the title page index
246 if (preg_match('/goog$/', $id) && ('' != $titleLeaf)) {
248 // assume page side based on title pagex
249 var titleIndex = br.leafNumToIndex(br.titleLeaf);
250 // assume title page is RHS
251 var delta = titleIndex - index;
252 if (0 == (delta & 0x1)) {
262 // $$$ we should get this from scandata instead of assuming the accessible
263 // leafs are contiguous
264 if ('rl' != this.pageProgression) {
265 // If pageProgression is not set RTL we assume it is LTR
266 if (0 == (index & 0x1)) {
267 // Even-numbered page
275 if (0 == (index & 0x1)) {
283 br.getPageNum = function(index) {
284 var pageNum = this.pageNums[index];
292 // Single images in the Internet Archive scandata.xml metadata are (somewhat incorrectly)
293 // given a "leaf" number. Some of these images from the scanning process should not
294 // be displayed in the BookReader (for example colour calibration cards). Since some
295 // of the scanned images will not be displayed in the BookReader (those marked with
296 // addToAccessFormats false in the scandata.xml) leaf numbers and BookReader page
297 // indexes are generally not the same. This function returns the BookReader page
298 // index given a scanned leaf number.
300 // This function is used, for example, to map between search results (that use the
301 // leaf numbers) and the displayed pages in the BookReader.
302 br.leafNumToIndex = function(leafNum) {
303 for (var index = 0; index < this.leafMap.length; index++) {
304 if (this.leafMap[index] == leafNum) {
312 // This function returns the left and right indices for the user-visible
313 // spread that contains the given index. The return values may be
314 // null if there is no facing page or the index is invalid.
315 br.getSpreadIndices = function(pindex) {
316 // $$$ we could make a separate function for the RTL case and
317 // only bind it if necessary instead of always checking
318 // $$$ we currently assume there are no gaps
320 var spreadIndices = [null, null];
321 if ('rl' == this.pageProgression) {
323 if (this.getPageSide(pindex) == 'R') {
324 spreadIndices[1] = pindex;
325 spreadIndices[0] = pindex + 1;
327 // Given index was LHS
328 spreadIndices[0] = pindex;
329 spreadIndices[1] = pindex - 1;
333 if (this.getPageSide(pindex) == 'L') {
334 spreadIndices[0] = pindex;
335 spreadIndices[1] = pindex + 1;
337 // Given index was RHS
338 spreadIndices[1] = pindex;
339 spreadIndices[0] = pindex - 1;
343 //console.log(" index %d mapped to spread %d,%d", pindex, spreadIndices[0], spreadIndices[1]);
345 return spreadIndices;
348 // Remove the page number assertions for all but the highest index page with
349 // a given assertion. Ensures there is only a single page "{pagenum}"
350 // e.g. the last page asserted as page 5 retains that assertion.
351 br.uniquifyPageNums = function() {
354 for (var i = br.pageNums.length - 1; i--; i >= 0) {
355 var pageNum = br.pageNums[i];
356 if ( !seen[pageNum] ) {
357 seen[pageNum] = true;
359 br.pageNums[i] = null;
365 br.cleanupMetadata = function() {
366 br.uniquifyPageNums();
371 // Returns a URL for an embedded version of the current book
372 br.getEmbedURL = function(viewParams) {
373 // We could generate a URL hash fragment here but for now we just leave at defaults
374 var url = 'http://' + window.location.host + '/stream/'+this.bookId;
375 if (this.subPrefix != this.bookId) { // Only include if needed
376 url += '/' + this.subPrefix;
379 if (typeof(viewParams) != 'undefined') {
380 url += '#' + this.fragmentFromParams(viewParams);
387 // Returns the embed code HTML fragment suitable for copy and paste
388 br.getEmbedCode = function(frameWidth, frameHeight, viewParams) {
389 return "<iframe src='" + this.getEmbedURL(viewParams) + "' width='" + frameWidth + "' height='" + frameHeight + "' frameborder='0' ></iframe>";
392 // getOpenLibraryRecord
393 br.getOpenLibraryRecord = function(callback) {
394 // Try looking up by ocaid first, then by source_record
396 var self = this; // closure
398 var jsonURL = self.olHost + '/query.json?type=/type/edition&*=&ocaid=' + self.bookId;
401 success: function(data) {
402 if (data && data.length > 0) {
403 callback(self, data[0]);
406 jsonURL = self.olHost + '/query.json?type=/type/edition&*=&source_records=ia:' + self.bookId;
409 success: function(data) {
410 if (data && data.length > 0) {
411 callback(self, data[0]);
422 br.buildInfoDiv = function(jInfoDiv) {
423 // $$$ it might make more sense to have a URL on openlibrary.org that returns this info
425 var escapedTitle = BookReader.util.escapeHTML(this.bookTitle);
426 var domainRe = /(\w+\.(com|org))/;
427 var domainMatch = domainRe.exec(this.bookUrl);
428 var domain = this.bookUrl;
430 domain = domainMatch[1];
433 // $$$ cover looks weird before it loads
434 jInfoDiv.find('.BRfloatCover').append([
435 '<div style="height: 140px; min-width: 80px; padding: 0; margin: 0;"><a href="', this.bookUrl, '"><img src="http://www.archive.org/download/', this.bookId, '/page/cover_t.jpg" alt="' + escapedTitle + '" height="140px" /></a></div>'].join('')
438 jInfoDiv.find('.BRfloatMeta').append([
440 //'<p>Published ', this.bookPublished,
441 //, <a href="Open Library Publisher Page">Publisher name</a>',
443 //'<p>Written in <a href="Open Library Language page">Language</a></p>',
444 '<h3>Other Formats</h3>',
445 '<ul class="links">',
446 '<li><a href="http://www.archive.org/download/', this.bookId, '/', this.subPrefix, '.pdf">PDF</a><span>|</span></li>',
447 '<li><a href="http://www.archive.org/download/', this.bookId, '/', this.subPrefix, '_djvu.txt">Plain Text</a><span>|</span></li>',
448 '<li><a href="http://www.archive.org/download/', this.bookId, '/', this.subPrefix, '_daisy.zip">DAISY</a><span>|</span></li>',
449 '<li><a href="http://www.archive.org/download/', this.bookId, '/', this.subPrefix, '.epub">ePub</a><span>|</span></li>',
450 '<li><a href="https://www.amazon.com/gp/digital/fiona/web-to-kindle?clientid=IA&itemid=', this.bookId, '&docid=', this.subPrefix, '">Send to Kindle</a></li>',
452 '<p class="moreInfo"><span></span>More information on <a href="'+ this.bookUrl + '">' + domain + '</a> </p>'].join('\n'));
454 jInfoDiv.find('.BRfloatFoot').append([
456 '<a href="http://openlibrary.org/contact" class="problem">Report a problem</a>',
459 if (domain == 'archive.org') {
460 jInfoDiv.find('.BRfloatMeta p.moreInfo span').css(
461 {'background': 'url(http://www.archive.org/favicon.ico) no-repeat', 'width': 22, 'height': 18 }
465 jInfoDiv.find('.BRfloatTitle a').attr({'href': this.bookUrl, 'alt': this.bookTitle}).text(this.bookTitle);
466 var bookPath = (window.location + '').replace('#','%23');
467 jInfoDiv.find('a.problem').attr('href','http://openlibrary.org/contact?path=' + bookPath);
474 foreach ($scanData->pageData->page as $page) {
475 if (shouldAddPage($page)) {
476 if(0 != $i) echo ","; //stupid IE
477 echo "{$page->cropBox->w}";
488 foreach ($scanData->pageData->page as $page) {
489 if (shouldAddPage($page)) {
490 if(0 != $i) echo ","; //stupid IE
491 echo "{$page->cropBox->h}";
492 $totalHeight += intval($page->cropBox->h/4) + 10;
501 foreach ($scanData->pageData->page as $page) {
502 if (shouldAddPage($page)) {
503 if(0 != $i) echo ","; //stupid IE
504 echo "{$page['leafNum']}";
514 foreach ($scanData->pageData->page as $page) {
515 if (shouldAddPage($page)) {
516 if(0 != $i) echo ","; //stupid IE
517 if (array_key_exists('pageNumber', $page) && ('' != $page->pageNumber)) {
518 echo "'{$page->pageNumber}'";
529 br.numLeafs = br.pageW.length;
531 br.bookId = '<?echo $id;?>';
532 br.zip = '<?echo $imageStackFile;?>';
533 br.subPrefix = '<?echo $subPrefix;?>';
534 br.server = '<?echo $server;?>';
535 br.bookTitle= '<?echo preg_replace("/\'/", "\\'", $metaData->title);?>';
536 br.bookPath = '<?echo $subItemPath;?>';
537 br.bookUrl = '<?echo "http://www.archive.org/details/$id";?>';
538 br.imageFormat = '<?echo $imageFormat;?>';
539 br.archiveFormat = '<?echo $archiveFormat;?>';
543 # Load some values from meta.xml
544 if ('' != $metaData->{'page-progression'}) {
545 echo "br.pageProgression = '" . $metaData->{"page-progression"} . "';\n";
547 // Assume page progression is Left To Right
548 echo "br.pageProgression = 'lr';\n";
553 foreach ($metaData->xpath('//collection') as $collection) {
554 if('browserlending' == $collection) {
560 echo "br.olHost = 'http://openlibrary.org';\n";
561 #echo "br.olHost = 'http://mang-dev.us.archive.org:8080';\n";
564 echo "br.olAuth = true;\n";
566 echo "br.olAuth = false;\n";
570 echo "br.protected = true;\n";
573 # Default options for BookReader
574 if ('' != $metaData->{'bookreader-defaults'}) {
575 echo "br.defaults = '" . $metaData->{'bookreader-defaults'} . "';\n";
580 // Check for config object
581 // $$$ change this to use the newer params object
582 if (typeof(brConfig) != 'undefined') {
583 if (typeof(brConfig["ui"]) != 'undefined') {
584 br.ui = brConfig["ui"];
587 if (brConfig['mode'] == 1) {
589 if (typeof(brConfig['reduce'] != 'undefined')) {
590 br.reduce = brConfig['reduce'];
592 } else if (brConfig['mode'] == 2) {
596 if (typeof(brConfig["isAdmin"]) != 'undefined') {
597 br.isAdmin = brConfig["isAdmin"];
605 this.authUrl = br.olHost + '/ia_auth/' + br.bookId;
606 this.olConnect = false;
607 this.loanUUID = false;
608 this.permsToken = false;
610 var cookieRe = /;\s*/;
611 var cookies = document.cookie.split(cookieRe);
612 var length = cookies.length;
614 for (i=0; i<length; i++) {
615 if (0 == cookies[i].indexOf('br-loan-' + br.bookId)) {
616 this.loanUUID = cookies[i].split('=')[1];
618 if (0 == cookies[i].indexOf('loan-' + br.bookId)) {
619 this.permsToken = cookies[i].split('=')[1];
626 OLAuth.prototype.init = function() {
627 var htmlStr = 'Checking loan status with Open Library';
629 this.showPopup("#F0EEE2", "#000", htmlStr, 'Please wait as we check the status of this book...');
630 var authUrl = this.authUrl+'?rand='+Math.random();
631 if (false !== this.loanUUID) {
632 authUrl += '&loan='+this.loanUUID
634 if (false !== this.permsToken) {
635 authUrl += '&token='+this.permsToken
637 $.ajax({url:authUrl, dataType:'jsonp', jsonpCallback:'olAuth.initCallback'});
640 OLAuth.prototype.showPopup = function(bgColor, textColor, msg, resolution) {
641 this.popup = document.createElement("div");
643 position: 'absolute',
645 left: ($('#BookReader').attr('clientWidth')-400)/2 + 'px',
648 border: "3px double #999999",
651 backgroundColor: bgColor,
653 }).appendTo('#BookReader');
655 this.setPopupMsg(msg, resolution);
659 OLAuth.prototype.setPopupMsg = function(msg, resolution) {
660 this.popup.innerHTML = ['<p><strong>', msg, '</strong></p><p>', resolution, '</p>'].join('\n');
663 OLAuth.prototype.showError = function(msg, resolution) {
665 backgroundColor: "#fff",
669 this.setPopupMsg(msg, resolution);
672 OLAuth.prototype.initCallback = function(obj) {
673 if (false == obj.success) {
675 ret = confirm("We couldn't authenticate your loan with Open Library, but since you are an administrator or uploader of this book, you can access this book for QA purposes. Would you like to QA this book?");
677 this.showError(obj.msg, obj.resolution)
682 this.showError(obj.msg, obj.resolution)
685 //user is authenticated
686 this.setCookie(obj.token);
687 this.olConnect = true;
693 OLAuth.prototype.callback = function(obj) {
694 if (false == obj.success) {
695 this.showPopup("#F0EEE2", "#000", obj.msg, obj.resolution);
696 clearInterval(this.poller);
697 this.ttsPoller = null;
699 this.olConnect = true;
700 this.setCookie(obj.token);
704 OLAuth.prototype.setCookie = function(value) {
705 var date = new Date();
706 date.setTime(date.getTime()+(10*60*1000)); //10 min expiry
707 var expiry = date.toGMTString();
708 var cookie = 'loan-'+br.bookId+'='+value;
709 cookie += '; expires='+expiry;
710 cookie += '; path=/; domain=.archive.org;';
711 document.cookie = cookie;
712 this.permsToken = value;
714 //refresh the br-loan uuid cookie with current expiry, if needed
715 if (false !== this.loanUUID) {
716 cookie = 'br-loan-'+br.bookId+'='+this.loanUUID;
717 cookie += '; expires='+expiry;
718 cookie += '; path=/; domain=.archive.org;';
719 document.cookie = cookie;
723 OLAuth.prototype.deleteCookies = function() {
724 var date = new Date();
725 date.setTime(date.getTime()-(24*60*60*1000)); //one day ago
726 var expiry = date.toGMTString();
727 var cookie = 'loan-'+br.bookId+'=""';
728 cookie += '; expires='+expiry;
729 cookie += '; path=/; domain=.archive.org;';
730 document.cookie = cookie;
732 cookie = 'br-loan-'+br.bookId+'=""';
733 cookie += '; expires='+expiry;
734 cookie += '; path=/; domain=.archive.org;';
735 document.cookie = cookie;
738 OLAuth.prototype.startPolling = function () {
740 this.poller=setInterval(function(){
741 if (!self.olConnect) {
742 self.showPopup("#F0EEE2", "#000", 'Connection error', 'The BookReader cannot reach Open Library. This might mean that you are offline or that Open Library is down. Please check your Internet connection and refresh this page or try again later.');
743 clearInterval(self.poller);
744 self.ttsPoller = null;
746 self.olConnect = false;
747 //be sure to add random param to authUrl to avoid stale cache
748 var authUrl = self.authUrl+'?rand='+Math.random();
749 if (false !== self.loanUUID) {
750 authUrl += '&loan='+self.loanUUID
752 if (false !== self.permsToken) {
753 authUrl += '&token='+self.permsToken
756 $.ajax({url:authUrl, dataType:'jsonp', jsonpCallback:'olAuth.callback'});
758 },300000); //five minute interval
761 br.cleanupMetadata();
763 var olAuth = new OLAuth();
771 function BRFatal($string) {
775 if (typeof(archive_analytics) != 'undefined') {
777 'bookreader': 'fatal',
778 'description': "<? echo $string; ?>",
779 'itemid': "<? echo $_REQUEST['id']; ?>",
780 'server': "<? echo $_REQUEST['server']; ?>",
781 'request_uri': "<? echo $_SERVER["REQUEST_URI"]; ?>"
784 if (document.referrer == '') {
785 values['referrer'] = '-';
787 values['referrer'] = document.referrer;
790 var qs = archive_analytics.format_bug(values);
792 var error_img = new Image(100,25);
793 error_img.src = archive_analytics.img_src + "?" + qs;
796 alert("<? echo $string;?>");
803 // Returns true if a page should be added based on it's information in
805 function shouldAddPage($page) {
806 // Return false only if the page is marked addToAccessFormats false.
807 // If there is no assertion we assume it should be added.
808 if (isset($page->addToAccessFormats)) {
809 if ("false" == strtolower(trim($page->addToAccessFormats))) {
817 // Returns { 'imageFormat' => , 'archiveFormat' => '} given a sub-item prefix and loaded xml data
818 function findImageStack($subPrefix, $filesData) {
820 // $$$ The order of the image formats determines which will be returned first
821 $imageFormats = array('JP2' => 'jp2', 'TIFF' => 'tif', 'JPEG' => 'jpg');
822 $archiveFormats = array('ZIP' => 'zip', 'Tar' => 'tar');
823 $imageGroup = implode('|', array_keys($imageFormats));
824 $archiveGroup = implode('|', array_keys($archiveFormats));
825 // $$$ Currently only return processed images
826 $imageStackRegex = "/Single Page (Processed) (${imageGroup}) (${archiveGroup})/";
828 foreach ($filesData->file as $file) {
829 if (strpos($file['name'], $subPrefix) === 0) { // subprefix matches beginning
830 if (preg_match($imageStackRegex, $file->format, $matches)) {
832 // Make sure we have a regular image stack
833 $imageFormat = $imageFormats[$matches[2]];
834 if (strpos($file['name'], $subPrefix . '_' . $imageFormat) === 0) {
835 return array('imageFormat' => $imageFormat,
836 'archiveFormat' => $archiveFormats[$matches[3]],
837 'imageStackFile' => $file['name']);
843 return array('imageFormat' => 'unknown', 'archiveFormat' => 'unknown', 'imageStackFile' => 'unknown');