diff --git a/Makefile b/Makefile index 34a7930a3..c1062d49d 100644 --- a/Makefile +++ b/Makefile @@ -209,11 +209,11 @@ pages-repo: | $(BUILD_DIR) # copy of the pdf.js source. CONTENT_DIR := content BUILD_NUMBER := `git log --format=oneline $(EXTENSION_BASE_VERSION).. | wc -l | awk '{print $$1}'` -PDF_WEB_FILES = \ +EXTENSION_WEB_FILES = \ web/images \ - web/compatibility.js \ web/viewer.css \ web/viewer.js \ + web/viewer.html \ web/viewer-production.html \ $(NULL) @@ -251,8 +251,19 @@ extension: | production @cd extensions/firefox; cp -r $(FIREFOX_EXTENSION_FILES_TO_COPY) ../../$(FIREFOX_BUILD_DIR)/ # Copy a standalone version of pdf.js inside the content directory @cp $(BUILD_TARGET) $(FIREFOX_BUILD_CONTENT)/$(BUILD_DIR)/ - @cp -r $(PDF_WEB_FILES) $(FIREFOX_BUILD_CONTENT)/web/ - @mv -f $(FIREFOX_BUILD_CONTENT)/web/viewer-production.html $(FIREFOX_BUILD_CONTENT)/web/viewer.html + @cp -r $(EXTENSION_WEB_FILES) $(FIREFOX_BUILD_CONTENT)/web/ + @rm $(FIREFOX_BUILD_CONTENT)/web/viewer-production.html + # Copy over the firefox extension snippet so we can inline pdf.js in it + @cp web/viewer-snippet-firefox-extension.html $(FIREFOX_BUILD_CONTENT)/web/ + # Modify the viewer so it does all the extension only stuff. + @cd $(FIREFOX_BUILD_CONTENT)/web; \ + sed -i.bak '/PDFJSSCRIPT_INCLUDE_BUNDLE/ r ../build/pdf.js' viewer-snippet-firefox-extension.html; \ + sed -i.bak '/PDFJSSCRIPT_REMOVE/d' viewer.html; \ + sed -i.bak '/PDFJSSCRIPT_REMOVE_FIREFOX_EXTENSION/d' viewer.html; \ + sed -i.bak '/PDFJSSCRIPT_INCLUDE_FIREFOX_EXTENSION/ r viewer-snippet-firefox-extension.html' viewer.html; \ + rm -f *.bak; + # We don't need pdf.js anymore since its inlined + @rm -Rf $(FIREFOX_BUILD_CONTENT)/$(BUILD_DIR)/; # Update the build version number @sed -i.bak "s/PDFJSSCRIPT_BUILD/$(BUILD_NUMBER)/" $(FIREFOX_BUILD_DIR)/install.rdf @sed -i.bak "s/PDFJSSCRIPT_BUILD/$(BUILD_NUMBER)/" $(FIREFOX_BUILD_DIR)/update.rdf @@ -274,7 +285,7 @@ extension: | production @cp -R $(CHROME_EXTENSION_FILES) $(CHROME_BUILD_DIR)/ # Copy a standalone version of pdf.js inside the content directory @cp $(BUILD_TARGET) $(CHROME_BUILD_CONTENT)/$(BUILD_DIR)/ - @cp -r $(PDF_WEB_FILES) $(CHROME_BUILD_CONTENT)/web/ + @cp -r $(EXTENSION_WEB_FILES) $(CHROME_BUILD_CONTENT)/web/ @mv -f $(CHROME_BUILD_CONTENT)/web/viewer-production.html $(CHROME_BUILD_CONTENT)/web/viewer.html # Create the crx diff --git a/README.md b/README.md index f12fce934..09cc95039 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ rendering PDFs, and eventually release a PDF reader extension powered by pdf.js. Integration with Firefox is a possibility if the experiment proves successful. - + # Getting started diff --git a/extensions/firefox/bootstrap.js b/extensions/firefox/bootstrap.js index e51df28f8..f1a712c0c 100644 --- a/extensions/firefox/bootstrap.js +++ b/extensions/firefox/bootstrap.js @@ -3,6 +3,8 @@ 'use strict'; +const EXT_PREFIX = 'extensions.uriloader@pdf.js'; +const PDFJS_EVENT_ID = 'pdf.js.message'; let Cc = Components.classes; let Ci = Components.interfaces; let Cm = Components.manager; @@ -14,6 +16,7 @@ function log(str) { dump(str + '\n'); } + function startup(aData, aReason) { let manifestPath = 'chrome.manifest'; let manifest = Cc['@mozilla.org/file/local;1'] @@ -34,13 +37,11 @@ function shutdown(aData, aReason) { } function install(aData, aReason) { - let url = 'chrome://pdf.js/content/web/viewer.html?file=%s'; - Services.prefs.setCharPref('extensions.pdf.js.url', url); Services.prefs.setBoolPref('extensions.pdf.js.active', false); } function uninstall(aData, aReason) { - Services.prefs.clearUserPref('extensions.pdf.js.url'); Services.prefs.clearUserPref('extensions.pdf.js.active'); + application.prefs.setValue(EXT_PREFIX + '.database', '{}'); } diff --git a/extensions/firefox/chrome.manifest b/extensions/firefox/chrome.manifest index d7db20b38..5351257e7 100644 --- a/extensions/firefox/chrome.manifest +++ b/extensions/firefox/chrome.manifest @@ -1,5 +1,5 @@ -content pdf.js content/ +resource pdf.js content/ -component {2278dfd0-b75c-11e0-8257-1ba3d93c9f1a} components/pdfContentHandler.js -contract @mozilla.org/uriloader/content-handler;1?type=application/pdf {2278dfd0-b75c-11e0-8257-1ba3d93c9f1a} +component {6457a96b-2d68-439a-bcfa-44465fbcdbb1} components/PdfStreamConverter.js +contract @mozilla.org/streamconv;1?from=application/pdf&to=*/* {6457a96b-2d68-439a-bcfa-44465fbcdbb1} diff --git a/extensions/firefox/components/PdfStreamConverter.js b/extensions/firefox/components/PdfStreamConverter.js new file mode 100644 index 000000000..984915d23 --- /dev/null +++ b/extensions/firefox/components/PdfStreamConverter.js @@ -0,0 +1,159 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +'use strict'; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; +const PDFJS_EVENT_ID = 'pdf.js.message'; +const PDF_CONTENT_TYPE = 'application/pdf'; +const NS_ERROR_NOT_IMPLEMENTED = 0x80004001; +const EXT_PREFIX = 'extensions.uriloader@pdf.js'; + +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import('resource://gre/modules/Services.jsm'); + +function log(aMsg) { + let msg = 'PdfStreamConverter.js: ' + (aMsg.join ? aMsg.join('') : aMsg); + Cc['@mozilla.org/consoleservice;1'].getService(Ci.nsIConsoleService) + .logStringMessage(msg); + dump(msg + '\n'); +} +let application = Cc['@mozilla.org/fuel/application;1'] + .getService(Ci.fuelIApplication); +let privateBrowsing = Cc['@mozilla.org/privatebrowsing;1'] + .getService(Ci.nsIPrivateBrowsingService); +let inPrivateBrowswing = privateBrowsing.privateBrowsingEnabled; + +// All the priviledged actions. +function ChromeActions() { + this.inPrivateBrowswing = privateBrowsing.privateBrowsingEnabled; +} +ChromeActions.prototype = { + download: function(data) { + Services.wm.getMostRecentWindow('navigator:browser').saveURL(data); + }, + setDatabase: function(data) { + if (this.inPrivateBrowswing) + return; + application.prefs.setValue(EXT_PREFIX + '.database', data); + }, + getDatabase: function() { + if (this.inPrivateBrowswing) + return '{}'; + return application.prefs.getValue(EXT_PREFIX + '.database', '{}'); + } +}; + +// Event listener to trigger chrome privedged code. +function RequestListener(actions) { + this.actions = actions; +} +// Receive an event and synchronously responds. +RequestListener.prototype.receive = function(event) { + var message = event.target; + var action = message.getUserData('action'); + var data = message.getUserData('data'); + var actions = this.actions; + if (!(action in actions)) { + log('Unknown action: ' + action); + return; + } + var response = actions[action].call(this.actions, data); + message.setUserData('response', response, null); +}; + + +function PdfStreamConverter() { +} + +PdfStreamConverter.prototype = { + + // properties required for XPCOM registration: + classID: Components.ID('{6457a96b-2d68-439a-bcfa-44465fbcdbb1}'), + classDescription: 'pdf.js Component', + contractID: '@mozilla.org/streamconv;1?from=application/pdf&to=*/*', + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsISupports, + Ci.nsIStreamConverter, + Ci.nsIStreamListener, + Ci.nsIRequestObserver + ]), + + /* + * This component works as such: + * 1. asyncConvertData stores the listener + * 2. onStartRequest creates a new channel, streams the viewer and cancels + * the request so pdf.js can do the request + * Since the request is cancelled onDataAvailable should not be called. The + * onStopRequest does nothing. The convert function just returns the stream, + * it's just the synchronous version of asyncConvertData. + */ + + // nsIStreamConverter::convert + convert: function(aFromStream, aFromType, aToType, aCtxt) { + return aFromStream; + }, + + // nsIStreamConverter::asyncConvertData + asyncConvertData: function(aFromType, aToType, aListener, aCtxt) { + if (!Services.prefs.getBoolPref('extensions.pdf.js.active')) + throw NS_ERROR_NOT_IMPLEMENTED; + // Store the listener passed to us + this.listener = aListener; + }, + + // nsIStreamListener::onDataAvailable + onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) { + // Do nothing since all the data loading is handled by the viewer. + log('SANITY CHECK: onDataAvailable SHOULD NOT BE CALLED!'); + }, + + // nsIRequestObserver::onStartRequest + onStartRequest: function(aRequest, aContext) { + + // Setup the request so we can use it below. + aRequest.QueryInterface(Ci.nsIChannel); + // Cancel the request so the viewer can handle it. + aRequest.cancel(Cr.NS_BINDING_ABORTED); + + // Create a new channel that is viewer loaded as a resource. + var ioService = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + var channel = ioService.newChannel( + 'resource://pdf.js/web/viewer.html', null, null); + + // Keep the URL the same so the browser sees it as the same. + channel.originalURI = aRequest.originalURI; + channel.asyncOpen(this.listener, aContext); + + // Setup a global listener waiting for the next DOM to be created and verfiy + // that its the one we want by its URL. When the correct DOM is found create + // an event listener on that window for the pdf.js events that require + // chrome priviledges. + var url = aRequest.originalURI.spec; + var gb = Services.wm.getMostRecentWindow('navigator:browser'); + var domListener = function domListener(event) { + var doc = event.originalTarget; + var win = doc.defaultView; + if (doc.location.href === url) { + gb.removeEventListener('DOMContentLoaded', domListener); + var requestListener = new RequestListener(new ChromeActions()); + win.addEventListener(PDFJS_EVENT_ID, function(event) { + requestListener.receive(event); + }, false, true); + } + }; + gb.addEventListener('DOMContentLoaded', domListener, false); + }, + + // nsIRequestObserver::onStopRequest + onStopRequest: function(aRequest, aContext, aStatusCode) { + // Do nothing. + } +}; + +var NSGetFactory = XPCOMUtils.generateNSGetFactory([PdfStreamConverter]); diff --git a/extensions/firefox/components/pdfContentHandler.js b/extensions/firefox/components/pdfContentHandler.js deleted file mode 100644 index 67459b759..000000000 --- a/extensions/firefox/components/pdfContentHandler.js +++ /dev/null @@ -1,67 +0,0 @@ -/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ - -'use strict'; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -const PDF_CONTENT_TYPE = 'application/pdf'; - -Cu.import('resource://gre/modules/XPCOMUtils.jsm'); -Cu.import('resource://gre/modules/Services.jsm'); - -function log(aMsg) { - let msg = 'pdfContentHandler.js: ' + (aMsg.join ? aMsg.join('') : aMsg); - Cc['@mozilla.org/consoleservice;1'].getService(Ci.nsIConsoleService) - .logStringMessage(msg); - dump(msg + '\n'); -} - -const NS_ERROR_WONT_HANDLE_CONTENT = 0x805d0001; -function pdfContentHandler() { -} - -pdfContentHandler.prototype = { - handleContent: function handleContent(aMimetype, aContext, aRequest) { - if (aMimetype != PDF_CONTENT_TYPE) - throw NS_ERROR_WONT_HANDLE_CONTENT; - - if (!(aRequest instanceof Ci.nsIChannel)) - throw NS_ERROR_WONT_HANDLE_CONTENT; - - if (!Services.prefs.getBoolPref('extensions.pdf.js.active')) - throw NS_ERROR_WONT_HANDLE_CONTENT; - - let window = null; - let callbacks = aRequest.notificationCallbacks || - aRequest.loadGroup.notificationCallbacks; - if (!callbacks) - return; - - window = callbacks.getInterface(Ci.nsIDOMWindow); - - let url = null; - try { - url = Services.prefs.getCharPref('extensions.pdf.js.url'); - } catch (e) { - log('Error retrieving the pdf.js base url - ' + e); - throw NS_ERROR_WONT_HANDLE_CONTENT; - } - - let targetUrl = aRequest.URI.spec; - if (targetUrl.indexOf('#pdfjs.action=download') >= 0) - throw NS_ERROR_WONT_HANDLE_CONTENT; - - aRequest.cancel(Cr.NS_BINDING_ABORTED); - window.location = url.replace('%s', encodeURIComponent(targetUrl)); - }, - - classID: Components.ID('{2278dfd0-b75c-11e0-8257-1ba3d93c9f1a}'), - QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentHandler]) -}; - -var NSGetFactory = XPCOMUtils.generateNSGetFactory([pdfContentHandler]); - diff --git a/src/canvas.js b/src/canvas.js index 5ef900861..d0b0064f6 100644 --- a/src/canvas.js +++ b/src/canvas.js @@ -548,7 +548,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { var fontObj = this.objs.get(fontRefName).fontObj; if (!fontObj) { - throw 'Can\'t find font for ' + fontRefName; + error('Can\'t find font for ' + fontRefName); } var name = fontObj.loadedName || 'sans-serif'; @@ -866,7 +866,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } else if (IR[0] == 'RadialAxial' || IR[0] == 'Dummy') { var pattern = Pattern.shadingFromIR(this.ctx, IR); } else { - throw 'Unkown IR type'; + error('Unkown IR type ' + IR[0]); } return pattern; }, diff --git a/src/colorspace.js b/src/colorspace.js index 827fd2e19..d67d928b1 100644 --- a/src/colorspace.js +++ b/src/colorspace.js @@ -369,55 +369,16 @@ var DeviceCmykCS = (function DeviceCmykCSClosure() { DeviceCmykCS.prototype = { getRgb: function cmykcs_getRgb(color) { var c = color[0], m = color[1], y = color[2], k = color[3]; - var c1 = 1 - c, m1 = 1 - m, y1 = 1 - y, k1 = 1 - k; - var x, r, g, b; - // this is a matrix multiplication, unrolled for performance - // code is taken from the poppler implementation - x = c1 * m1 * y1 * k1; // 0 0 0 0 - r = g = b = x; - x = c1 * m1 * y1 * k; // 0 0 0 1 - r += 0.1373 * x; - g += 0.1216 * x; - b += 0.1255 * x; - x = c1 * m1 * y * k1; // 0 0 1 0 - r += x; - g += 0.9490 * x; - x = c1 * m1 * y * k; // 0 0 1 1 - r += 0.1098 * x; - g += 0.1020 * x; - x = c1 * m * y1 * k1; // 0 1 0 0 - r += 0.9255 * x; - b += 0.5490 * x; - x = c1 * m * y1 * k; // 0 1 0 1 - r += 0.1412 * x; - x = c1 * m * y * k1; // 0 1 1 0 - r += 0.9294 * x; - g += 0.1098 * x; - b += 0.1412 * x; - x = c1 * m * y * k; // 0 1 1 1 - r += 0.1333 * x; - x = c * m1 * y1 * k1; // 1 0 0 0 - g += 0.6784 * x; - b += 0.9373 * x; - x = c * m1 * y1 * k; // 1 0 0 1 - g += 0.0588 * x; - b += 0.1412 * x; - x = c * m1 * y * k1; // 1 0 1 0 - g += 0.6510 * x; - b += 0.3137 * x; - x = c * m1 * y * k; // 1 0 1 1 - g += 0.0745 * x; - x = c * m * y1 * k1; // 1 1 0 0 - r += 0.1804 * x; - g += 0.1922 * x; - b += 0.5725 * x; - x = c * m * y1 * k; // 1 1 0 1 - b += 0.0078 * x; - x = c * m * y * k1; // 1 1 1 0 - r += 0.2118 * x; - g += 0.2119 * x; - b += 0.2235 * x; + // CMYK -> CMY: http://www.easyrgb.com/index.php?X=MATH&H=14#text14 + c = (c * (1 - k) + k); + m = (m * (1 - k) + k); + y = (y * (1 - k) + k); + + // CMY -> RGB: http://www.easyrgb.com/index.php?X=MATH&H=12#text12 + var r = (1 - c); + var g = (1 - m); + var b = (1 - y); return [r, g, b]; }, diff --git a/src/core.js b/src/core.js index 7a9f3ee03..cb601398e 100644 --- a/src/core.js +++ b/src/core.js @@ -410,14 +410,14 @@ var Page = (function PageClosure() { if (callback) callback(e); else - throw e; + error(e); } }.bind(this), function pageDisplayReadPromiseError(reason) { if (callback) callback(reason); else - throw reason; + error(reason); } ); } @@ -620,13 +620,23 @@ var PDFDoc = (function PDFDocClosure() { if (!globalScope.PDFJS.disableWorker && typeof Worker !== 'undefined') { var workerSrc = PDFJS.workerSrc; if (typeof workerSrc === 'undefined') { - throw 'No PDFJS.workerSrc specified'; + error('No PDFJS.workerSrc specified'); } try { - // Some versions of FF can't create a worker on localhost, see: - // https://bugzilla.mozilla.org/show_bug.cgi?id=683280 - var worker = new Worker(workerSrc); + var worker; + if (PDFJS.isFirefoxExtension) { + // The firefox extension can't load the worker from the resource:// + // url so we have to inline the script and then use the blob loader. + var bb = new MozBlobBuilder(); + bb.append(document.querySelector('#PDFJS_SCRIPT_TAG').textContent); + var blobUrl = window.URL.createObjectURL(bb.getBlob()); + worker = new Worker(blobUrl); + } else { + // Some versions of FF can't create a worker on localhost, see: + // https://bugzilla.mozilla.org/show_bug.cgi?id=683280 + worker = new Worker(workerSrc); + } var messageHandler = new MessageHandler('main', worker); @@ -645,7 +655,9 @@ var PDFDoc = (function PDFDocClosure() { // serializing the typed array. messageHandler.send('test', testObj); return; - } catch (e) {} + } catch (e) { + warn('The worker has been disabled.'); + } } // Either workers are disabled, not supported or have thrown an exception. // Thus, we fallback to a faked worker. @@ -716,7 +728,7 @@ var PDFDoc = (function PDFDocClosure() { }); break; default: - throw 'Got unkown object type ' + type; + error('Got unkown object type ' + type); } }, this); @@ -737,7 +749,7 @@ var PDFDoc = (function PDFDocClosure() { if (page.displayReadyPromise) page.displayReadyPromise.reject(data.error); else - throw data.error; + error(data.error); }, this); messageHandler.on('jpeg_decode', function(data, promise) { diff --git a/src/evaluator.js b/src/evaluator.js index 21530f42f..c70013d25 100644 --- a/src/evaluator.js +++ b/src/evaluator.js @@ -481,8 +481,10 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { properties.cidToGidMap = this.readCidToGidMap(cidToGidMap); } + var flags = properties.flags; var differences = []; - var baseEncoding = Encodings.StandardEncoding; + var baseEncoding = !!(flags & FontFlags.Symbolic) ? + Encodings.symbolsEncoding : Encodings.StandardEncoding; var hasEncoding = dict.has('Encoding'); if (hasEncoding) { var encoding = xref.fetchIfRef(dict.get('Encoding')); @@ -761,8 +763,9 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { // Simulating descriptor flags attribute var fontNameWoStyle = baseFontName.split('-')[0]; var flags = (serifFonts[fontNameWoStyle] || - (fontNameWoStyle.search(/serif/gi) != -1) ? 2 : 0) | - (symbolsFonts[fontNameWoStyle] ? 4 : 32); + (fontNameWoStyle.search(/serif/gi) != -1) ? FontFlags.Serif : 0) | + (symbolsFonts[fontNameWoStyle] ? FontFlags.Symbolic : + FontFlags.Nonsymbolic); var properties = { type: type.name, diff --git a/src/fonts.js b/src/fonts.js index f96c15458..3f618b82a 100644 --- a/src/fonts.js +++ b/src/fonts.js @@ -19,6 +19,18 @@ var kPDFGlyphSpaceUnits = 1000; // Until hinting is fully supported this constant can be used var kHintingEnabled = false; +var FontFlags = { + FixedPitch: 1, + Serif: 2, + Symbolic: 4, + Script: 8, + Nonsymbolic: 32, + Italic: 64, + AllCap: 65536, + SmallCap: 131072, + ForceBold: 262144 +}; + var Encodings = { get ExpertEncoding() { return shadow(this, 'ExpertEncoding', ['', '', '', '', '', '', '', '', '', @@ -160,19 +172,20 @@ var Encodings = { 'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore', 'quoteleft', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - 'braceleft', 'bar', 'braceright', 'asciitilde', '', '', 'exclamdown', - 'cent', 'sterling', 'fraction', 'yen', 'florin', 'section', 'currency', - 'quotesingle', 'quotedblleft', 'guillemotleft', 'guilsinglleft', - 'guilsinglright', 'fi', 'fl', '', 'endash', 'dagger', 'daggerdbl', - 'periodcentered', '', 'paragraph', 'bullet', 'quotesinglbase', - 'quotedblbase', 'quotedblright', 'guillemotright', 'ellipsis', - 'perthousand', '', 'questiondown', '', 'grave', 'acute', 'circumflex', - 'tilde', 'macron', 'breve', 'dotaccent', 'dieresis', '', 'ring', - 'cedilla', '', 'hungarumlaut', 'ogonek', 'caron', 'emdash', '', '', '', - '', '', '', '', '', '', '', '', '', '', '', '', '', 'AE', '', - 'ordfeminine', '', '', '', '', 'Lslash', 'Oslash', 'OE', 'ordmasculine', - '', '', '', '', '', 'ae', '', '', '', 'dotlessi', '', '', 'lslash', - 'oslash', 'oe', 'germandbls' + 'braceleft', 'bar', 'braceright', 'asciitilde', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', 'exclamdown', 'cent', 'sterling', + 'fraction', 'yen', 'florin', 'section', 'currency', 'quotesingle', + 'quotedblleft', 'guillemotleft', 'guilsinglleft', 'guilsinglright', 'fi', + 'fl', '', 'endash', 'dagger', 'daggerdbl', 'periodcentered', '', + 'paragraph', 'bullet', 'quotesinglbase', 'quotedblbase', 'quotedblright', + 'guillemotright', 'ellipsis', 'perthousand', '', 'questiondown', '', + 'grave', 'acute', 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', + 'dieresis', '', 'ring', 'cedilla', '', 'hungarumlaut', 'ogonek', 'caron', + 'emdash', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + 'AE', '', 'ordfeminine', '', '', '', '', 'Lslash', 'Oslash', 'OE', + 'ordmasculine', '', '', '', '', '', 'ae', '', '', '', 'dotlessi', '', '', + 'lslash', 'oslash', 'oe', 'germandbls' ]); }, get WinAnsiEncoding() { @@ -405,6 +418,19 @@ var symbolsFonts = { 'Dingbats': true, 'Symbol': true, 'ZapfDingbats': true }; +// Some characters, e.g. copyrightserif, mapped to the private use area and +// might not be displayed using standard fonts. Mapping/hacking well-known chars +// to the similar equivalents in the normal characters range. +function mapPrivateUseChars(code) { + switch (code) { + case 0xF8E9: // copyrightsans + case 0xF6D9: // copyrightserif + return 0x00A9; // copyright + default: + return code; + } +} + var FontLoader = { listeningForFontLoad: false, @@ -761,8 +787,8 @@ var Font = (function FontClosure() { var names = name.split('+'); names = names.length > 1 ? names[1] : names[0]; names = names.split(/[-,_]/g)[0]; - this.isSerifFont = !!(properties.flags & 2); - this.isSymbolicFont = !!(properties.flags & 4); + this.isSerifFont = !!(properties.flags & FontFlags.Serif); + this.isSymbolicFont = !!(properties.flags & FontFlags.Symbolic); var type = properties.type; this.type = type; @@ -2186,7 +2212,7 @@ var Font = (function FontClosure() { case 'CIDFontType0': if (this.noUnicodeAdaptation) { width = this.widths[this.unicodeToCID[charcode] || charcode]; - unicode = charcode; + unicode = mapPrivateUseChars(charcode); break; } unicode = this.toUnicode[charcode] || charcode; @@ -2194,7 +2220,7 @@ var Font = (function FontClosure() { case 'CIDFontType2': if (this.noUnicodeAdaptation) { width = this.widths[this.unicodeToCID[charcode] || charcode]; - unicode = charcode; + unicode = mapPrivateUseChars(charcode); break; } unicode = this.toUnicode[charcode] || charcode; @@ -2204,7 +2230,7 @@ var Font = (function FontClosure() { if (!isNum(width)) width = this.widths[glyphName]; if (this.noUnicodeAdaptation) { - unicode = GlyphsUnicode[glyphName] || charcode; + unicode = mapPrivateUseChars(GlyphsUnicode[glyphName] || charcode); break; } unicode = this.glyphNameMap[glyphName] || @@ -2235,9 +2261,8 @@ var Font = (function FontClosure() { } // MacRoman encoding address by re-encoding the cmap table - unicode = glyphName in GlyphsUnicode ? - GlyphsUnicode[glyphName] : - this.glyphNameMap[glyphName]; + unicode = glyphName in this.glyphNameMap ? + this.glyphNameMap[glyphName] : GlyphsUnicode[glyphName]; break; default: warn('Unsupported font type: ' + this.type); diff --git a/src/function.js b/src/function.js index 26b8fe679..4f81158f0 100644 --- a/src/function.js +++ b/src/function.js @@ -125,109 +125,99 @@ var PDFFunction = (function PDFFunctionClosure() { else decode = toMultiArray(decode); - // Precalc the multipliers - var inputMul = new Float64Array(inputSize); - for (var i = 0; i < inputSize; ++i) { - inputMul[i] = (encode[i][1] - encode[i][0]) / - (domain[i][1] - domain[i][0]); - } - - var idxMul = new Int32Array(inputSize); - idxMul[0] = outputSize; - for (i = 1; i < inputSize; ++i) { - idxMul[i] = idxMul[i - 1] * size[i - 1]; - } - - var nSamples = outputSize; - for (i = 0; i < inputSize; ++i) - nSamples *= size[i]; - var samples = this.getSampleArray(size, outputSize, bps, str); return [ CONSTRUCT_SAMPLED, inputSize, domain, encode, decode, samples, size, - outputSize, bps, range, inputMul, idxMul, nSamples + outputSize, Math.pow(2, bps) - 1, range ]; }, constructSampledFromIR: function pdfFunctionConstructSampledFromIR(IR) { - var inputSize = IR[1]; - var domain = IR[2]; - var encode = IR[3]; - var decode = IR[4]; - var samples = IR[5]; - var size = IR[6]; - var outputSize = IR[7]; - var bps = IR[8]; - var range = IR[9]; - var inputMul = IR[10]; - var idxMul = IR[11]; - var nSamples = IR[12]; + // See chapter 3, page 109 of the PDF reference + function interpolate(x, xmin, xmax, ymin, ymax) { + return ymin + ((x - xmin) * ((ymax - ymin) / (xmax - xmin))); + } return function constructSampledFromIRResult(args) { - if (inputSize != args.length) + // See chapter 3, page 110 of the PDF reference. + var m = IR[1]; + var domain = IR[2]; + var encode = IR[3]; + var decode = IR[4]; + var samples = IR[5]; + var size = IR[6]; + var n = IR[7]; + var mask = IR[8]; + var range = IR[9]; + + if (m != args.length) error('Incorrect number of arguments: ' + inputSize + ' != ' + args.length); - // Most of the below is a port of Poppler's implementation. - // TODO: There's a few other ways to do multilinear interpolation such - // as piecewise, which is much faster but an approximation. - var out = new Float64Array(outputSize); - var x; - var e = new Array(inputSize); - var efrac0 = new Float64Array(inputSize); - var efrac1 = new Float64Array(inputSize); - var sBuf = new Float64Array(1 << inputSize); - var i, j, k, idx, t; - // map input values into sample array - for (i = 0; i < inputSize; ++i) { - x = (args[i] - domain[i][0]) * inputMul[i] + encode[i][0]; - if (x < 0) { - x = 0; - } else if (x > size[i] - 1) { - x = size[i] - 1; - } - e[i] = [Math.floor(x), 0]; - if ((e[i][1] = e[i][0] + 1) >= size[i]) { - // this happens if in[i] = domain[i][1] - e[i][1] = e[i][0]; - } - efrac1[i] = x - e[i][0]; - efrac0[i] = 1 - efrac1[i]; - } + var x = args; - // for each output, do m-linear interpolation - for (i = 0; i < outputSize; ++i) { + // Building the cube vertices: its part and sample index + // http://rjwagner49.com/Mathematics/Interpolation.pdf + var cubeVertices = 1 << m; + var cubeN = new Float64Array(cubeVertices); + var cubeVertex = new Uint32Array(cubeVertices); + for (var j = 0; j < cubeVertices; j++) + cubeN[j] = 1; - // pull 2^m values out of the sample array - for (j = 0; j < (1 << inputSize); ++j) { - idx = i; - for (k = 0, t = j; k < inputSize; ++k, t >>= 1) { - idx += idxMul[k] * (e[k][t & 1]); - } - if (idx >= 0 && idx < nSamples) { - sBuf[j] = samples[idx]; + var k = n, pos = 1; + // Map x_i to y_j for 0 <= i < m using the sampled function. + for (var i = 0; i < m; ++i) { + // x_i' = min(max(x_i, Domain_2i), Domain_2i+1) + var domain_2i = domain[i][0]; + var domain_2i_1 = domain[i][1]; + var xi = Math.min(Math.max(x[i], domain_2i), domain_2i_1); + + // e_i = Interpolate(x_i', Domain_2i, Domain_2i+1, + // Encode_2i, Encode_2i+1) + var e = interpolate(xi, domain_2i, domain_2i_1, + encode[i][0], encode[i][1]); + + // e_i' = min(max(e_i, 0), Size_i - 1) + var size_i = size[i]; + e = Math.min(Math.max(e, 0), size_i - 1); + + // Adjusting the cube: N and vertex sample index + var e0 = e < size_i - 1 ? Math.floor(e) : e - 1; // e1 = e0 + 1; + var n0 = e0 + 1 - e; // (e1 - e) / (e1 - e0); + var n1 = e - e0; // (e - e0) / (e1 - e0); + var offset0 = e0 * k; + var offset1 = offset0 + k; // e1 * k + for (var j = 0; j < cubeVertices; j++) { + if (j & pos) { + cubeN[j] *= n1; + cubeVertex[j] += offset1; } else { - sBuf[j] = 0; // TODO Investigate if this is what Adobe does + cubeN[j] *= n0; + cubeVertex[j] += offset0; } } - // do m sets of interpolations - for (j = 0, t = (1 << inputSize); j < inputSize; ++j, t >>= 1) { - for (k = 0; k < t; k += 2) { - sBuf[k >> 1] = efrac0[j] * sBuf[k] + efrac1[j] * sBuf[k + 1]; - } - } - - // map output value to range - out[i] = (sBuf[0] * (decode[i][1] - decode[i][0]) + decode[i][0]); - if (out[i] < range[i][0]) { - out[i] = range[i][0]; - } else if (out[i] > range[i][1]) { - out[i] = range[i][1]; - } + k *= size_i; + pos <<= 1; } - return out; + + var y = new Float64Array(n); + for (var j = 0; j < n; ++j) { + // Sum all cube vertices' samples portions + var rj = 0; + for (var i = 0; i < cubeVertices; i++) + rj += samples[cubeVertex[i] + j] * cubeN[i]; + + // r_j' = Interpolate(r_j, 0, 2^BitsPerSample - 1, + // Decode_2j, Decode_2j+1) + rj = interpolate(rj, 0, 1, decode[j][0], decode[j][1]); + + // y_j = min(max(r_j, range_2j), range_2j+1) + y[j] = Math.min(Math.max(rj, range[j][0]), range[j][1]); + } + + return y; } }, diff --git a/src/jpx.js b/src/jpx.js index 61a8f4487..a15c3db54 100644 --- a/src/jpx.js +++ b/src/jpx.js @@ -1052,7 +1052,7 @@ var JpxImage = (function JpxImageClosure() { } r = 0; } - throw 'Out of packets'; + error('JPX error: Out of packets'); }; } function ResolutionLayerComponentPositionIterator(context) { @@ -1091,7 +1091,7 @@ var JpxImage = (function JpxImageClosure() { } l = 0; } - throw 'Out of packets'; + error('JPX error: Out of packets'); }; } function buildPackets(context) { @@ -1187,7 +1187,7 @@ var JpxImage = (function JpxImageClosure() { new ResolutionLayerComponentPositionIterator(context); break; default: - throw 'Unsupported progression order'; + error('JPX error: Unsupported progression order ' + progressionOrder); } } function parseTilePackets(context, data, offset, dataLength) { @@ -1589,7 +1589,7 @@ var JpxImage = (function JpxImageClosure() { if (lbox == 0) lbox = length - position + headerSize; if (lbox < headerSize) - throw 'Invalid box field size'; + error('JPX error: Invalid box field size'); var dataLength = lbox - headerSize; var jumpDataLength = true; switch (tbox) { @@ -1675,7 +1675,7 @@ var JpxImage = (function JpxImageClosure() { scalarExpounded = true; break; default: - throw 'Invalid SQcd value'; + error('JPX error: Invalid SQcd value ' + sqcd); } qcd.noQuantization = spqcdSize == 8; qcd.scalarExpounded = scalarExpounded; @@ -1728,7 +1728,7 @@ var JpxImage = (function JpxImageClosure() { scalarExpounded = true; break; default: - throw 'Invalid SQcd value'; + error('JPX error: Invalid SQcd value ' + sqcd); } qcc.noQuantization = spqcdSize == 8; qcc.scalarExpounded = scalarExpounded; @@ -1795,7 +1795,7 @@ var JpxImage = (function JpxImageClosure() { cod.terminationOnEachCodingPass || cod.verticalyStripe || cod.predictableTermination || cod.segmentationSymbolUsed) - throw 'Unsupported COD options: ' + uneval(cod); + error('JPX error: Unsupported COD options: ' + uneval(cod)); if (context.mainHeader) context.COD = cod; @@ -1840,7 +1840,7 @@ var JpxImage = (function JpxImageClosure() { // skipping content break; default: - throw 'Unknown codestream code: ' + code.toString(16); + error('JPX error: Unknown codestream code: ' + code.toString(16)); } position += length; } diff --git a/src/obj.js b/src/obj.js index ef7932546..3cdee8778 100644 --- a/src/obj.js +++ b/src/obj.js @@ -287,74 +287,69 @@ var XRef = (function XRefClosure() { XRef.prototype = { readXRefTable: function readXRefTable(parser) { + // Example of cross-reference table: + // xref + // 0 1 <-- subsection header (first obj #, obj count) + // 0000000000 65535 f <-- actual object (offset, generation #, f/n) + // 23 2 <-- subsection header ... and so on ... + // 0000025518 00002 n + // 0000025635 00000 n + // trailer + // ... + + // Outer loop is over subsection headers var obj; - while (true) { - if (isCmd(obj = parser.getObj(), 'trailer')) - break; - if (!isInt(obj)) - error('Invalid XRef table'); - var first = obj; - if (!isInt(obj = parser.getObj())) - error('Invalid XRef table'); - var n = obj; - if (first < 0 || n < 0 || (first + n) != ((first + n) | 0)) - error('Invalid XRef table: ' + first + ', ' + n); - for (var i = first; i < first + n; ++i) { + while (!isCmd(obj = parser.getObj(), 'trailer')) { + var first = obj, + count = parser.getObj(); + + if (!isInt(first) || !isInt(count)) + error('Invalid XRef table: wrong types in subsection header'); + + // Inner loop is over objects themselves + for (var i = 0; i < count; i++) { var entry = {}; - if (!isInt(obj = parser.getObj())) - error('Invalid XRef table: ' + first + ', ' + n); - entry.offset = obj; - if (!isInt(obj = parser.getObj())) - error('Invalid XRef table: ' + first + ', ' + n); - entry.gen = obj; - obj = parser.getObj(); - if (isCmd(obj, 'n')) { - entry.uncompressed = true; - } else if (isCmd(obj, 'f')) { + entry.offset = parser.getObj(); + entry.gen = parser.getObj(); + var type = parser.getObj(); + + if (isCmd(type, 'f')) entry.free = true; - } else { - error('Invalid XRef table: ' + first + ', ' + n); - } - if (!this.entries[i]) { - // In some buggy PDF files the xref table claims to start at 1 - // instead of 0. - if (i == 1 && first == 1 && - entry.offset == 0 && entry.gen == 65535 && entry.free) { - i = first = 0; - } - this.entries[i] = entry; + else if (isCmd(type, 'n')) + entry.uncompressed = true; + + // Validate entry obj + if (!isInt(entry.offset) || !isInt(entry.gen) || + !(entry.free || entry.uncompressed)) { + error('Invalid entry in XRef subsection: ' + first + ', ' + count); } + + if (!this.entries[i + first]) + this.entries[i + first] = entry; } } - // read the trailer dictionary - var dict; - if (!isDict(dict = parser.getObj())) - error('Invalid XRef table'); + // Sanity check: as per spec, first object must have these properties + if (this.entries[0] && + !(this.entries[0].gen === 65535 && this.entries[0].free)) + error('Invalid XRef table: unexpected first object'); - // get the 'Prev' pointer - var prev; - obj = dict.get('Prev'); - if (isInt(obj)) { - prev = obj; - } else if (isRef(obj)) { - // certain buggy PDF generators generate "/Prev NNN 0 R" instead - // of "/Prev NNN" - prev = obj.num; - } - if (prev) { - this.readXRef(prev); - } + // Sanity check + if (!isCmd(obj, 'trailer')) + error('Invalid XRef table: could not find trailer dictionary'); - // check for 'XRefStm' key - if (isInt(obj = dict.get('XRefStm'))) { - var pos = obj; - // ignore previously loaded xref streams (possible infinite recursion) - if (!(pos in this.xrefstms)) { - this.xrefstms[pos] = 1; - this.readXRef(pos); - } - } + // Read trailer dictionary, e.g. + // trailer + // << /Size 22 + // /Root 20R + // /Info 10R + // /ID [ <81b14aafa313db63dbd6f981e49f94f4> ] + // >> + // The parser goes through the entire stream << ... >> and provides + // a getter interface for the key-value table + var dict = parser.getObj(); + if (!isDict(dict)) + error('Invalid XRef table: could not parse trailer dictionary'); return dict; }, @@ -407,9 +402,6 @@ var XRef = (function XRefClosure() { } range.splice(0, 2); } - var prev = streamParameters.get('Prev'); - if (isInt(prev)) - this.readXRef(prev); return streamParameters; }, indexObjects: function indexObjects() { @@ -529,22 +521,47 @@ var XRef = (function XRefClosure() { try { var parser = new Parser(new Lexer(stream), true); var obj = parser.getObj(); + var dict; - // parse an old-style xref table - if (isCmd(obj, 'xref')) - return this.readXRefTable(parser); + // Get dictionary + if (isCmd(obj, 'xref')) { + // Parse end-of-file XRef + dict = this.readXRefTable(parser); - // parse an xref stream - if (isInt(obj)) { + // Recursively get other XRefs 'XRefStm', if any + obj = dict.get('XRefStm'); + if (isInt(obj)) { + var pos = obj; + // ignore previously loaded xref streams + // (possible infinite recursion) + if (!(pos in this.xrefstms)) { + this.xrefstms[pos] = 1; + this.readXRef(pos); + } + } + } else if (isInt(obj)) { + // Parse in-stream XRef if (!isInt(parser.getObj()) || !isCmd(parser.getObj(), 'obj') || !isStream(obj = parser.getObj())) { error('Invalid XRef stream'); } - return this.readXRefStream(obj); + dict = this.readXRefStream(obj); } + + // Recursively get previous dictionary, if any + obj = dict.get('Prev'); + if (isInt(obj)) + this.readXRef(obj); + else if (isRef(obj)) { + // The spec says Prev must not be a reference, i.e. "/Prev NNN" + // This is a fallback for non-compliant PDFs, i.e. "/Prev NNN 0 R" + this.readXRef(obj.num); + } + + return dict; } catch (e) { - log('Reading of the xref table/stream failed: ' + e); + log('(while reading XRef): ' + e); } warn('Indexing all PDF objects'); @@ -574,7 +591,7 @@ var XRef = (function XRefClosure() { var stream, parser; if (e.uncompressed) { if (e.gen != gen) - throw ('inconsistent generation in XRef'); + error('inconsistent generation in XRef'); stream = this.stream.makeSubStream(e.offset); parser = new Parser(new Lexer(stream), true, this); var obj1 = parser.getObj(); @@ -703,7 +720,7 @@ var PDFObjects = (function PDFObjectsClosure() { // If there isn't an object yet or the object isn't resolved, then the // data isn't ready yet! if (!obj || !obj.isResolved) { - throw 'Requesting object that isn\'t resolved yet ' + objId; + error('Requesting object that isn\'t resolved yet ' + objId); return null; } else { return obj.data; diff --git a/src/stream.js b/src/stream.js index 610a54d38..fc163171f 100644 --- a/src/stream.js +++ b/src/stream.js @@ -821,15 +821,19 @@ var JpegStream = (function JpegStreamClosure() { JpegStream.prototype.ensureBuffer = function jpegStreamEnsureBuffer(req) { if (this.bufferLength) return; - var jpegImage = new JpegImage(); - if (this.colorTransform != -1) - jpegImage.colorTransform = this.colorTransform; - jpegImage.parse(this.bytes); - var width = jpegImage.width; - var height = jpegImage.height; - var data = jpegImage.getData(width, height); - this.buffer = data; - this.bufferLength = data.length; + try { + var jpegImage = new JpegImage(); + if (this.colorTransform != -1) + jpegImage.colorTransform = this.colorTransform; + jpegImage.parse(this.bytes); + var width = jpegImage.width; + var height = jpegImage.height; + var data = jpegImage.getData(width, height); + this.buffer = data; + this.bufferLength = data.length; + } catch (e) { + error('JPEG error: ' + e); + } }; JpegStream.prototype.getIR = function jpegStreamGetIR() { return bytesToString(this.bytes); diff --git a/src/util.js b/src/util.js index 99b422296..759908e9e 100644 --- a/src/util.js +++ b/src/util.js @@ -255,8 +255,8 @@ var Promise = (function PromiseClosure() { return; } if (this._data !== EMPTY_PROMISE) { - throw 'Promise ' + this.name + - ': Cannot set the data of a promise twice'; + error('Promise ' + this.name + + ': Cannot set the data of a promise twice'); } this._data = value; this.hasData = true; @@ -268,7 +268,7 @@ var Promise = (function PromiseClosure() { get data() { if (this._data === EMPTY_PROMISE) { - throw 'Promise ' + this.name + ': Cannot get data that isn\'t set'; + error('Promise ' + this.name + ': Cannot get data that isn\'t set'); } return this._data; }, @@ -283,10 +283,10 @@ var Promise = (function PromiseClosure() { resolve: function promiseResolve(data) { if (this.isResolved) { - throw 'A Promise can be resolved only once ' + this.name; + error('A Promise can be resolved only once ' + this.name); } if (this.isRejected) { - throw 'The Promise was already rejected ' + this.name; + error('The Promise was already rejected ' + this.name); } this.isResolved = true; @@ -300,10 +300,10 @@ var Promise = (function PromiseClosure() { reject: function proimseReject(reason) { if (this.isRejected) { - throw 'A Promise can be rejected only once ' + this.name; + error('A Promise can be rejected only once ' + this.name); } if (this.isResolved) { - throw 'The Promise was already resolved ' + this.name; + error('The Promise was already resolved ' + this.name); } this.isRejected = true; @@ -317,7 +317,7 @@ var Promise = (function PromiseClosure() { then: function promiseThen(callback, errback) { if (!callback) { - throw 'Requiring callback' + this.name; + error('Requiring callback' + this.name); } // If the promise is already resolved, call the callback directly. diff --git a/src/worker.js b/src/worker.js index 4d9dd1bb6..b81ff0540 100644 --- a/src/worker.js +++ b/src/worker.js @@ -26,7 +26,7 @@ function MessageHandler(name, comObj) { delete callbacks[callbackId]; callback(data.data); } else { - throw 'Cannot resolve callback ' + callbackId; + error('Cannot resolve callback ' + callbackId); } } else if (data.action in ah) { var action = ah[data.action]; @@ -44,7 +44,7 @@ function MessageHandler(name, comObj) { action[0].call(action[1], data.data); } } else { - throw 'Unkown action from worker: ' + data.action; + error('Unkown action from worker: ' + data.action); } }; } @@ -53,7 +53,7 @@ MessageHandler.prototype = { on: function messageHandlerOn(actionName, handler, scope) { var ah = this.actionHandler; if (ah[actionName]) { - throw 'There is already an actionName called "' + actionName + '"'; + error('There is already an actionName called "' + actionName + '"'); } ah[actionName] = [handler, scope]; }, @@ -208,6 +208,7 @@ var workerConsole = { action: 'console_error', data: args }); + throw 'pdf.js execution error'; }, time: function time(name) { @@ -217,7 +218,7 @@ var workerConsole = { timeEnd: function timeEnd(name) { var time = consoleTimer[name]; if (time == null) { - throw 'Unkown timer name ' + name; + error('Unkown timer name ' + name); } this.log('Timer:', name, Date.now() - time); } diff --git a/test/pdfs/issue1096.pdf.link b/test/pdfs/issue1096.pdf.link new file mode 100644 index 000000000..aa07f14dd --- /dev/null +++ b/test/pdfs/issue1096.pdf.link @@ -0,0 +1 @@ +http://www.faithaliveresources.org/Content/Site135/FilesSamples/105315400440pdf_00000009843.pdf diff --git a/test/pdfs/issue1127.pdf.link b/test/pdfs/issue1127.pdf.link new file mode 100644 index 000000000..2df2304ba --- /dev/null +++ b/test/pdfs/issue1127.pdf.link @@ -0,0 +1 @@ +https://vmp.ethz.ch/pdfs/diplome/vordiplome/Block%201/Algorithmen_%26_Komplexitaet/AlgoKo_f08_Aufg.pdf diff --git a/test/test_manifest.json b/test/test_manifest.json index 5bc344abf..c6fed0a35 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -402,6 +402,21 @@ "link": true, "type": "eq" }, + { "id": "issue1096", + "file": "pdfs/issue1096.pdf", + "md5": "7f75d2b4b93c78d401ff39e8c1b00612", + "rounds": 1, + "pageLimit": 10, + "link": true, + "type": "eq" + }, + { "id": "issue1127", + "file": "pdfs/issue1127.pdf", + "md5": "4fb2be5ffefeafda4ba977de2a1bb4d8", + "rounds": 1, + "link": true, + "type": "eq" + }, { "id": "liveprogramming", "file": "pdfs/liveprogramming.pdf", "md5": "7bd4dad1188232ef597d36fd72c33e52", diff --git a/web/compatibility.js b/web/compatibility.js index 26405ad8f..b22153516 100644 --- a/web/compatibility.js +++ b/web/compatibility.js @@ -224,3 +224,10 @@ } }); })(); + +// Check console compatability +(function checkConsoleCompatibility() { + if (typeof console == 'undefined') { + console = {log: function() {}}; + } +})(); diff --git a/web/viewer-snippet-firefox-extension.html b/web/viewer-snippet-firefox-extension.html new file mode 100644 index 000000000..a3d3502a8 --- /dev/null +++ b/web/viewer-snippet-firefox-extension.html @@ -0,0 +1,14 @@ + + + + diff --git a/web/viewer.css b/web/viewer.css index e355f7fc2..b9fd3e9e4 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -9,7 +9,7 @@ body { } [hidden] { - display: none; + display: none !important; } /* === Toolbar === */ diff --git a/web/viewer.html b/web/viewer.html index 221ef7abf..7c55ec735 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -2,9 +2,11 @@ Simple pdf.js page viewer + + - + @@ -91,7 +93,7 @@ -
+
Bookmark diff --git a/web/viewer.js b/web/viewer.js index 11c0769a3..cc279cf67 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -6,6 +6,7 @@ var kDefaultURL = 'compressed.tracemonkey-pldi-09.pdf'; var kDefaultScale = 'auto'; var kDefaultScaleDelta = 1.1; +var kUnknownScale = 0; var kCacheSize = 20; var kCssUnits = 96.0 / 72.0; var kScrollbarPadding = 40; @@ -61,6 +62,31 @@ var RenderingQueue = (function RenderingQueueClosure() { return RenderingQueue; })(); +var FirefoxCom = (function FirefoxComClosure() { + return { + /** + * Creates an event that hopefully the extension is listening for and will + * synchronously respond to. + * @param {String} action The action to trigger. + * @param {String} data Optional data to send. + * @return {*} The response. + */ + request: function(action, data) { + var request = document.createTextNode(''); + request.setUserData('action', action, null); + request.setUserData('data', data, null); + document.documentElement.appendChild(request); + + var sender = document.createEvent('Events'); + sender.initEvent('pdf.js.message', true, false); + request.dispatchEvent(sender); + var response = request.getUserData('response'); + document.documentElement.removeChild(request); + return response; + } + }; +})(); + // Settings Manager - This is a utility for saving settings // First we see if localStorage is available // If not, we use FUEL in FF @@ -76,22 +102,14 @@ var Settings = (function SettingsClosure() { return false; } })(); - var extPrefix = 'extensions.uriloader@pdf.js'; - var isExtension = location.protocol == 'chrome:' && !isLocalStorageEnabled; - var inPrivateBrowsing = false; - if (isExtension) { - var pbs = Components.classes['@mozilla.org/privatebrowsing;1'] - .getService(Components.interfaces.nsIPrivateBrowsingService); - inPrivateBrowsing = pbs.privateBrowsingEnabled; - } + + var isFirefoxExtension = PDFJS.isFirefoxExtension; function Settings(fingerprint) { var database = null; var index; - if (inPrivateBrowsing) - return false; - else if (isExtension) - database = Application.prefs.getValue(extPrefix + '.database', '{}'); + if (isFirefoxExtension) + database = FirefoxCom.request('getDatabase', null); else if (isLocalStorageEnabled) database = localStorage.getItem('database') || '{}'; else @@ -113,29 +131,26 @@ var Settings = (function SettingsClosure() { index = database.files.push({fingerprint: fingerprint}) - 1; this.file = database.files[index]; this.database = database; - if (isExtension) - Application.prefs.setValue(extPrefix + '.database', - JSON.stringify(database)); - else if (isLocalStorageEnabled) + if (isLocalStorageEnabled) localStorage.setItem('database', JSON.stringify(database)); } Settings.prototype = { set: function settingsSet(name, val) { - if (inPrivateBrowsing || !('file' in this)) + if (!('file' in this)) return false; var file = this.file; file[name] = val; - if (isExtension) - Application.prefs.setValue(extPrefix + '.database', - JSON.stringify(this.database)); + var database = JSON.stringify(this.database); + if (isFirefoxExtension) + FirefoxCom.request('setDatabase', database); else if (isLocalStorageEnabled) - localStorage.setItem('database', JSON.stringify(this.database)); + localStorage.setItem('database', database); }, get: function settingsGet(name, defaultValue) { - if (inPrivateBrowsing || !('file' in this)) + if (!('file' in this)) return defaultValue; return this.file[name] || defaultValue; @@ -152,7 +167,7 @@ var currentPageNumber = 1; var PDFView = { pages: [], thumbnails: [], - currentScale: 0, + currentScale: kUnknownScale, currentScaleValue: null, initialBookmark: document.location.hash.substring(1), @@ -207,12 +222,12 @@ var PDFView = { zoomIn: function pdfViewZoomIn() { var newScale = Math.min(kMaxScale, this.currentScale * kDefaultScaleDelta); - this.setScale(newScale, true); + this.parseScale(newScale, true); }, zoomOut: function pdfViewZoomOut() { var newScale = Math.max(kMinScale, this.currentScale / kDefaultScaleDelta); - this.setScale(newScale, true); + this.parseScale(newScale, true); }, set page(val) { @@ -261,7 +276,7 @@ var PDFView = { }, error: function getPdfError(e) { var loadingIndicator = document.getElementById('loading'); - loadingIndicator.innerHTML = 'Error'; + loadingIndicator.textContent = 'Error'; var moreInfo = { message: 'Unexpected server response of ' + e.target.status + '.' }; @@ -276,7 +291,13 @@ var PDFView = { }, download: function pdfViewDownload() { - window.open(this.url + '#pdfjs.action=download', '_parent'); + var url = this.url.split('#')[0]; + if (PDFJS.isFirefoxExtension) { + FirefoxCom.request('download', url); + } else { + url += '#pdfjs.action=download', '_parent'; + window.open(url, '_parent'); + } }, navigateTo: function pdfViewNavigateTo(dest) { @@ -297,14 +318,14 @@ var PDFView = { getDestinationHash: function pdfViewGetDestinationHash(dest) { if (typeof dest === 'string') - return '#' + escape(dest); + return PDFView.getAnchorUrl('#' + escape(dest)); if (dest instanceof Array) { var destRef = dest[0]; // see navigateTo method for dest format var pageNumber = destRef instanceof Object ? this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] : (destRef + 1); if (pageNumber) { - var pdfOpenParams = '#page=' + pageNumber; + var pdfOpenParams = PDFView.getAnchorUrl('#page=' + pageNumber); var destKind = dest[1]; if ('name' in destKind && destKind.name == 'XYZ') { var scale = (dest[4] || this.currentScale); @@ -319,6 +340,17 @@ var PDFView = { return ''; }, + /** + * For the firefox extension we prefix the full url on anchor links so they + * don't come up as resource:// urls and so open in new tab/window works. + * @param {String} anchor The anchor hash include the #. + */ + getAnchorUrl: function getAnchorUrl(anchor) { + if (PDFJS.isFirefoxExtension) + return this.url.split('#')[0] + anchor; + return anchor; + }, + /** * Show the error box. * @param {String} message A message that is human readable. @@ -331,7 +363,7 @@ var PDFView = { errorWrapper.removeAttribute('hidden'); var errorMessage = document.getElementById('errorMessage'); - errorMessage.innerHTML = message; + errorMessage.textContent = message; var closeButton = document.getElementById('errorClose'); closeButton.onclick = function() { @@ -366,7 +398,7 @@ var PDFView = { progress: function pdfViewProgress(level) { var percent = Math.round(level * 100); var loadingIndicator = document.getElementById('loading'); - loadingIndicator.innerHTML = 'Loading... ' + percent + '%'; + loadingIndicator.textContent = 'Loading... ' + percent + '%'; }, load: function pdfViewLoad(data, scale) { @@ -406,7 +438,7 @@ var PDFView = { var pagesCount = pdf.numPages; var id = pdf.fingerprint; var storedHash = null; - document.getElementById('numPages').innerHTML = pagesCount; + document.getElementById('numPages').textContent = pagesCount; document.getElementById('pageNumber').max = pagesCount; PDFView.documentFingerprint = id; var store = PDFView.store = new Settings(id); @@ -456,10 +488,16 @@ var PDFView = { } else if (storedHash) this.setHash(storedHash); - else { - this.parseScale(scale || kDefaultScale, true); + else if (scale) { + this.parseScale(scale, true); this.page = 1; } + + if (PDFView.currentScale === kUnknownScale) { + // Scale was not initialized: invalid bookmark or scale was not specified. + // Setting the default one. + this.parseScale(kDefaultScale, true); + } }, setHash: function pdfViewSetHash(hash) { @@ -652,7 +690,15 @@ var PageView = function pageView(container, content, id, pageWidth, pageHeight, if (!item.content) { content.setAttribute('hidden', true); } else { - text.innerHTML = item.content.replace('\n', '
'); + var e = document.createElement('span'); + var lines = item.content.split('\n'); + for (var i = 0, ii = lines.length; i < ii; ++i) { + var line = lines[i]; + e.appendChild(document.createTextNode(line)); + if (i < (ii - 1)) + e.appendChild(document.createElement('br')); + } + text.appendChild(e); image.addEventListener('mouseover', function annotationImageOver() { this.nextSibling.removeAttribute('hidden'); }, false); @@ -746,6 +792,8 @@ var PageView = function pageView(container, content, id, pageWidth, pageHeight, if (scale && scale !== PDFView.currentScale) PDFView.parseScale(scale, true); + else if (PDFView.currentScale === kUnknownScale) + PDFView.parseScale(kDefaultScale, true); setTimeout(function pageViewScrollIntoViewRelayout() { // letting page to re-layout before scrolling @@ -826,13 +874,13 @@ var PageView = function pageView(container, content, id, pageWidth, pageHeight, var t1 = stats.compile, t2 = stats.fonts, t3 = stats.render; var str = 'Time to compile/fonts/render: ' + (t1 - stats.begin) + '/' + (t2 - t1) + '/' + (t3 - t2) + ' ms'; - document.getElementById('info').innerHTML = str; + document.getElementById('info').textContent = str; }; }; var ThumbnailView = function thumbnailView(container, page, id, pageRatio) { var anchor = document.createElement('a'); - anchor.href = '#' + id; + anchor.href = PDFView.getAnchorUrl('#page=' + id); anchor.onclick = function stopNivigation() { PDFView.page = id; return false; @@ -1040,12 +1088,18 @@ window.addEventListener('load', function webViewerLoad(evt) { } var scale = ('scale' in params) ? params.scale : 0; - PDFView.open(params.file || kDefaultURL, parseFloat(scale)); + var file = PDFJS.isFirefoxExtension ? + window.location.toString() : params.file || kDefaultURL; + PDFView.open(file, parseFloat(scale)); - if (!window.File || !window.FileReader || !window.FileList || !window.Blob) + if (PDFJS.isFirefoxExtension || !window.File || !window.FileReader || + !window.FileList || !window.Blob) { document.getElementById('fileInput').setAttribute('hidden', 'true'); - else + document.getElementById('fileInputSeperator') + .setAttribute('hidden', 'true'); + } else { document.getElementById('fileInput').value = null; + } if ('disableWorker' in params) PDFJS.disableWorker = (params['disableWorker'] === 'true'); @@ -1130,8 +1184,8 @@ function updateViewarea() { store.set('zoom', normalizedScaleValue); store.set('scrollLeft', Math.round(topLeft.x)); store.set('scrollTop', Math.round(topLeft.y)); - - document.getElementById('viewBookmark').href = pdfOpenParams; + var href = PDFView.getAnchorUrl(pdfOpenParams); + document.getElementById('viewBookmark').href = href; } window.addEventListener('scroll', function webViewerScroll(evt) { @@ -1271,7 +1325,7 @@ window.addEventListener('keydown', function keydown(evt) { handled = true; break; case 48: // '0' - PDFView.setScale(kDefaultScale, true); + PDFView.parseScale(kDefaultScale, true); handled = true; break; case 37: // left arrow