diff --git a/Makefile b/Makefile index 3cc423350..d4833a561 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ DEFAULT_TESTS := test_manifest.json DEFAULT_PYTHON := python2.7 EXTENSION_SRC := ./extensions/ -EXTENSION_BASE_VERSION := 4bb289ec499013de66eb421737a4dbb4a9273eda +EXTENSION_BASE_VERSION := f0f0418a9c6637981fe1182b9212c2d592774c7d FIREFOX_EXTENSION_NAME := pdf.js.xpi FIREFOX_AMO_EXTENSION_NAME := pdf.js.amo.xpi CHROME_EXTENSION_NAME := pdf.js.crx @@ -20,6 +20,7 @@ all: bundle PDF_JS_FILES = \ core.js \ util.js \ + api.js \ canvas.js \ obj.js \ function.js \ @@ -74,7 +75,8 @@ bundle: | $(BUILD_DIR) @cd src; \ cat $(PDF_JS_FILES) > all_files.tmp; \ sed '/PDFJSSCRIPT_INCLUDE_ALL/ r all_files.tmp' pdf.js > ../$(BUILD_TARGET); \ - sed -i.bak "s/PDFJSSCRIPT_BUNDLE_VER/`git log --format="%h" -n 1`/" ../$(BUILD_TARGET); \ + cp ../$(BUILD_TARGET) ../$(BUILD_TARGET).bak; \ + sed "s/PDFJSSCRIPT_BUNDLE_VER/`git log --format="%h" -n 1`/" ../$(BUILD_TARGET).bak > ../$(BUILD_TARGET); \ rm -f ../$(BUILD_TARGET).bak; \ rm -f *.tmp; \ cd .. @@ -184,7 +186,7 @@ web: | production extension compiler pages-repo # and deletions. pages-repo: | $(BUILD_DIR) @if [ ! -d "$(GH_PAGES)" ]; then \ - git clone -b gh-pages $(REPO) $(GH_PAGES); \ + git clone --depth 1 -b gh-pages $(REPO) $(GH_PAGES); \ rm -rf $(GH_PAGES)/*; \ fi; @mkdir -p $(GH_PAGES)/web; @@ -211,7 +213,7 @@ 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}'` -PDFJSSCRIPT_VERSION := 0.2.$(BUILD_NUMBER) +PDFJSSCRIPT_VERSION := 0.3.$(BUILD_NUMBER) EXTENSION_WEB_FILES = \ web/images \ web/viewer.css \ @@ -273,25 +275,34 @@ extension: | production @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_CORE/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; \ + cp viewer-snippet-firefox-extension.html viewer-snippet-firefox-extension.html.bak; \ + sed '/PDFJSSCRIPT_INCLUDE_BUNDLE/ r ../build/pdf.js' viewer-snippet-firefox-extension.html.bak > viewer-snippet-firefox-extension.html; \ + cp viewer.html viewer.html.bak; \ + sed '/PDFJSSCRIPT_REMOVE_CORE/d' viewer.html.bak > viewer.html; \ + cp viewer.html viewer.html.bak; \ + sed '/PDFJSSCRIPT_REMOVE_FIREFOX_EXTENSION/d' viewer.html.bak > viewer.html; \ + cp viewer.html viewer.html.bak; \ + sed '/PDFJSSCRIPT_INCLUDE_FIREFOX_EXTENSION/ r viewer-snippet-firefox-extension.html' viewer.html.bak > 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_VERSION/$(PDFJSSCRIPT_VERSION)/" $(FIREFOX_BUILD_DIR)/install.rdf - @sed -i.bak "s/PDFJSSCRIPT_VERSION/$(PDFJSSCRIPT_VERSION)/" $(FIREFOX_BUILD_DIR)/install.rdf.in - @sed -i.bak "s/PDFJSSCRIPT_VERSION/$(PDFJSSCRIPT_VERSION)/" $(FIREFOX_BUILD_DIR)/update.rdf - @sed -i.bak "s/PDFJSSCRIPT_VERSION/$(PDFJSSCRIPT_VERSION)/" $(FIREFOX_BUILD_DIR)/README.mozilla + cp $(FIREFOX_BUILD_DIR)/install.rdf $(FIREFOX_BUILD_DIR)/install.rdf.bak + @sed "s/PDFJSSCRIPT_VERSION/$(PDFJSSCRIPT_VERSION)/" $(FIREFOX_BUILD_DIR)/install.rdf.bak > $(FIREFOX_BUILD_DIR)/install.rdf + cp $(FIREFOX_BUILD_DIR)/install.rdf.in $(FIREFOX_BUILD_DIR)/install.rdf.in.bak + @sed "s/PDFJSSCRIPT_VERSION/$(PDFJSSCRIPT_VERSION)/" $(FIREFOX_BUILD_DIR)/install.rdf.in.bak > $(FIREFOX_BUILD_DIR)/install.rdf.in + cp $(FIREFOX_BUILD_DIR)/update.rdf $(FIREFOX_BUILD_DIR)/update.rdf.bak + @sed "s/PDFJSSCRIPT_VERSION/$(PDFJSSCRIPT_VERSION)/" $(FIREFOX_BUILD_DIR)/update.rdf.bak > $(FIREFOX_BUILD_DIR)/update.rdf + cp $(FIREFOX_BUILD_DIR)/README.mozilla $(FIREFOX_BUILD_DIR)/README.mozilla.bak + @sed "s/PDFJSSCRIPT_VERSION/$(PDFJSSCRIPT_VERSION)/" $(FIREFOX_BUILD_DIR)/README.mozilla.bak > $(FIREFOX_BUILD_DIR)/README.mozilla @rm -f $(FIREFOX_BUILD_DIR)/*.bak @find $(FIREFOX_BUILD_DIR) -name ".*" -delete # Create the xpi @cd $(FIREFOX_BUILD_DIR); zip -r $(FIREFOX_EXTENSION_NAME) $(FIREFOX_EXTENSION_FILES) @echo "extension created: " $(FIREFOX_EXTENSION_NAME) # Build the amo extension too (remove the updateUrl) - @sed -i.bak "/updateURL/d" $(FIREFOX_BUILD_DIR)/install.rdf + cp $(FIREFOX_BUILD_DIR)/install.rdf $(FIREFOX_BUILD_DIR)/install.rdf.bak + @sed "/updateURL/d" $(FIREFOX_BUILD_DIR)/install.rdf.bak > $(FIREFOX_BUILD_DIR)/install.rdf @rm -f $(FIREFOX_BUILD_DIR)/*.bak @cd $(FIREFOX_BUILD_DIR); zip -r $(FIREFOX_AMO_EXTENSION_NAME) $(FIREFOX_EXTENSION_FILES) @echo "AMO extension created: " $(FIREFOX_AMO_EXTENSION_NAME) diff --git a/examples/acroforms/forms.js b/examples/acroforms/forms.js index 6ec92766d..868825fc7 100644 --- a/examples/acroforms/forms.js +++ b/examples/acroforms/forms.js @@ -9,7 +9,7 @@ var formFields = {}; -function setupForm(div, content, scale) { +function setupForm(div, content, viewport) { function bindInputItem(input, item) { if (input.name in formFields) { var value = formFields[input.name]; @@ -27,16 +27,20 @@ function setupForm(div, content, scale) { } function createElementWithStyle(tagName, item) { var element = document.createElement(tagName); - element.style.left = (item.x * scale) + 'px'; - element.style.top = (item.y * scale) + 'px'; - element.style.width = Math.ceil(item.width * scale) + 'px'; - element.style.height = Math.ceil(item.height * scale) + 'px'; + var rect = Util.normalizeRect( + viewport.convertToViewportRectangle(item.rect)); + element.style.left = Math.floor(rect[0]) + 'px'; + element.style.top = Math.floor(rect[1]) + 'px'; + element.style.width = Math.ceil(rect[2] - rect[0]) + 'px'; + element.style.height = Math.ceil(rect[3] - rect[1]) + 'px'; return element; } function assignFontStyle(element, item) { var fontStyles = ''; - if ('fontSize' in item) - fontStyles += 'font-size: ' + Math.round(item.fontSize * scale) + 'px;'; + if ('fontSize' in item) { + fontStyles += 'font-size: ' + Math.round(item.fontSize * + viewport.fontScale) + 'px;'; + } switch (item.textAlignment) { case 0: fontStyles += 'text-align: left;'; @@ -51,83 +55,88 @@ function setupForm(div, content, scale) { element.setAttribute('style', element.getAttribute('style') + fontStyles); } - var items = content.getAnnotations(); - for (var i = 0; i < items.length; i++) { - var item = items[i]; - switch (item.type) { - case 'Widget': - if (item.fieldType != 'Tx' && item.fieldType != 'Btn' && - item.fieldType != 'Ch') - break; - var inputDiv = createElementWithStyle('div', item); - inputDiv.className = 'inputHint'; - div.appendChild(inputDiv); - var input; - if (item.fieldType == 'Tx') { - input = createElementWithStyle('input', item); - } - if (item.fieldType == 'Btn') { - input = createElementWithStyle('input', item); - if (item.flags & 32768) { - input.type = 'radio'; - // radio button is not supported - } else if (item.flags & 65536) { - input.type = 'button'; - // pushbutton is not supported - } else { - input.type = 'checkbox'; + content.getAnnotations().then(function(items) { + for (var i = 0; i < items.length; i++) { + var item = items[i]; + switch (item.type) { + case 'Widget': + if (item.fieldType != 'Tx' && item.fieldType != 'Btn' && + item.fieldType != 'Ch') + break; + var inputDiv = createElementWithStyle('div', item); + inputDiv.className = 'inputHint'; + div.appendChild(inputDiv); + var input; + if (item.fieldType == 'Tx') { + input = createElementWithStyle('input', item); } - } - if (item.fieldType == 'Ch') { - input = createElementWithStyle('select', item); - // select box is not supported - } - input.className = 'inputControl'; - input.name = item.fullName; - input.title = item.alternativeText; - assignFontStyle(input, item); - bindInputItem(input, item); - div.appendChild(input); - break; + if (item.fieldType == 'Btn') { + input = createElementWithStyle('input', item); + if (item.flags & 32768) { + input.type = 'radio'; + // radio button is not supported + } else if (item.flags & 65536) { + input.type = 'button'; + // pushbutton is not supported + } else { + input.type = 'checkbox'; + } + } + if (item.fieldType == 'Ch') { + input = createElementWithStyle('select', item); + // select box is not supported + } + input.className = 'inputControl'; + input.name = item.fullName; + input.title = item.alternativeText; + assignFontStyle(input, item); + bindInputItem(input, item); + div.appendChild(input); + break; + } } - } + }); } function renderPage(div, pdf, pageNumber, callback) { - var page = pdf.getPage(pageNumber); - var scale = 1.5; + pdf.getPage(pageNumber).then(function(page) { + var scale = 1.5; + var viewport = page.getViewport(scale); - var pageDisplayWidth = page.width * scale; - var pageDisplayHeight = page.height * scale; + var pageDisplayWidth = viewport.width; + var pageDisplayHeight = viewport.height; - var pageDivHolder = document.createElement('div'); - pageDivHolder.className = 'pdfpage'; - pageDivHolder.style.width = pageDisplayWidth + 'px'; - pageDivHolder.style.height = pageDisplayHeight + 'px'; - div.appendChild(pageDivHolder); + var pageDivHolder = document.createElement('div'); + pageDivHolder.className = 'pdfpage'; + pageDivHolder.style.width = pageDisplayWidth + 'px'; + pageDivHolder.style.height = pageDisplayHeight + 'px'; + div.appendChild(pageDivHolder); - // Prepare canvas using PDF page dimensions - var canvas = document.createElement('canvas'); - var context = canvas.getContext('2d'); - canvas.width = pageDisplayWidth; - canvas.height = pageDisplayHeight; - pageDivHolder.appendChild(canvas); + // Prepare canvas using PDF page dimensions + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + canvas.width = pageDisplayWidth; + canvas.height = pageDisplayHeight; + pageDivHolder.appendChild(canvas); - // Render PDF page into canvas context - page.startRendering(context, callback); + // Render PDF page into canvas context + var renderContext = { + canvasContext: context, + viewport: viewport + }; + page.render(renderContext).then(callback); - // Prepare and populate form elements layer - var formDiv = document.createElement('div'); - pageDivHolder.appendChild(formDiv); + // Prepare and populate form elements layer + var formDiv = document.createElement('div'); + pageDivHolder.appendChild(formDiv); - setupForm(formDiv, page, scale); + setupForm(formDiv, page, viewport); + }); } -PDFJS.getPdf(pdfWithFormsPath, function getPdfForm(data) { - // Instantiate PDFDoc with PDF data - var pdf = new PDFJS.PDFDoc(data); - +// Fetch the PDF document from the URL using promices +PDFJS.getDocument(pdfWithFormsPath).then(function getPdfForm(pdf) { // Rendering all pages starting from first var viewer = document.getElementById('viewer'); var pageNumber = 1; diff --git a/examples/acroforms/index.html b/examples/acroforms/index.html index 8a9053f78..858ad649f 100644 --- a/examples/acroforms/index.html +++ b/examples/acroforms/index.html @@ -6,6 +6,7 @@ + diff --git a/examples/helloworld/hello.js b/examples/helloworld/hello.js index 45e61eb6f..7bf18d65b 100644 --- a/examples/helloworld/hello.js +++ b/examples/helloworld/hello.js @@ -7,25 +7,31 @@ 'use strict'; -PDFJS.getPdf('helloworld.pdf', function getPdfHelloWorld(data) { - // - // Instantiate PDFDoc with PDF data - // - var pdf = new PDFJS.PDFDoc(data); - var page = pdf.getPage(1); - var scale = 1.5; +// +// Fetch the PDF document from the URL using promices +// +PDFJS.getDocument('helloworld.pdf').then(function(pdf) { + // Using promise to fetch the page + pdf.getPage(1).then(function(page) { + var scale = 1.5; + var viewport = page.getViewport(scale); - // - // Prepare canvas using PDF page dimensions - // - var canvas = document.getElementById('the-canvas'); - var context = canvas.getContext('2d'); - canvas.height = page.height * scale; - canvas.width = page.width * scale; + // + // Prepare canvas using PDF page dimensions + // + var canvas = document.getElementById('the-canvas'); + var context = canvas.getContext('2d'); + canvas.height = viewport.height; + canvas.width = viewport.width; - // - // Render PDF page into canvas context - // - page.startRendering(context); + // + // Render PDF page into canvas context + // + var renderContext = { + canvasContext: context, + viewport: viewport + }; + page.render(renderContext); + }); }); diff --git a/examples/helloworld/index.html b/examples/helloworld/index.html index c6af616e6..c9df98809 100644 --- a/examples/helloworld/index.html +++ b/examples/helloworld/index.html @@ -6,6 +6,7 @@ + diff --git a/extensions/firefox/components/PdfStreamConverter.js b/extensions/firefox/components/PdfStreamConverter.js index 4467abc6b..f4b5b7712 100644 --- a/extensions/firefox/components/PdfStreamConverter.js +++ b/extensions/firefox/components/PdfStreamConverter.js @@ -153,7 +153,7 @@ PdfStreamConverter.prototype = { 'resource://pdf.js/web/viewer.html', null, null); var listener = this.listener; - // Proxy all the requst observer calls, when it gets to onStopRequst + // Proxy all the request observer calls, when it gets to onStopRequest // we can get the dom window. var proxy = { onStartRequest: function() { diff --git a/make.js b/make.js index e0975fec8..bcd7f433d 100755 --- a/make.js +++ b/make.js @@ -79,6 +79,7 @@ target.bundle = function() { var SRC_FILES = ['core.js', 'util.js', + 'api.js', 'canvas.js', 'obj.js', 'function.js', @@ -175,8 +176,8 @@ var EXTENSION_WEB_FILES = 'web/viewer.js', 'web/viewer.html', 'web/viewer-production.html'], - EXTENSION_BASE_VERSION = '4bb289ec499013de66eb421737a4dbb4a9273eda', - EXTENSION_VERSION_PREFIX = '0.2.', + EXTENSION_BASE_VERSION = 'f0f0418a9c6637981fe1182b9212c2d592774c7d', + EXTENSION_VERSION_PREFIX = '0.3.', EXTENSION_BUILD_NUMBER, EXTENSION_VERSION; diff --git a/src/api.js b/src/api.js new file mode 100644 index 000000000..3d97dacd2 --- /dev/null +++ b/src/api.js @@ -0,0 +1,590 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +/** + * This is the main entry point for loading a PDF and interacting with it. + * NOTE: If a URL is used to fetch the PDF data a standard XMLHttpRequest(XHR) + * is used, which means it must follow the same origin rules that any XHR does + * e.g. No cross domain requests without CORS. + * + * @param {string|TypedAray} source Either a url to a PDF is located or a + * typed array already populated with data. + * @return {Promise} A promise that is resolved with {PDFDocumentProxy} object. + */ +PDFJS.getDocument = function getDocument(source) { + var promise = new PDFJS.Promise(); + var transport = new WorkerTransport(promise); + if (typeof source === 'string') { + // fetch url + PDFJS.getPdf( + { + url: source, + progress: function getPDFProgress(evt) { + if (evt.lengthComputable) + promise.progress({ + loaded: evt.loaded, + total: evt.total + }); + }, + error: function getPDFError(e) { + promise.reject('Unexpected server response of ' + + e.target.status + '.'); + } + }, + function getPDFLoad(data) { + transport.sendData(data); + }); + } else { + // assuming the source is array, instantiating directly from it + transport.sendData(source); + } + return promise; +}; + +/** + * Proxy to a PDFDocument in the worker thread. Also, contains commonly used + * properties that can be read synchronously. + */ +var PDFDocumentProxy = (function() { + function PDFDocumentProxy(pdfInfo, transport) { + this.pdfInfo = pdfInfo; + this.transport = transport; + } + PDFDocumentProxy.prototype = { + /** + * @return {number} Total number of pages the PDF contains. + */ + get numPages() { + return this.pdfInfo.numPages; + }, + /** + * @return {string} A unique ID to identify a PDF. Not guaranteed to be + * unique. + */ + get fingerprint() { + return this.pdfInfo.fingerprint; + }, + /** + * @param {number} The page number to get. The first page is 1. + * @return {Promise} A promise that is resolved with a {PDFPageProxy} + * object. + */ + getPage: function(number) { + return this.transport.getPage(number); + }, + /** + * @return {Promise} A promise that is resolved with a lookup table for + * mapping named destinations to reference numbers. + */ + getDestinations: function() { + var promise = new PDFJS.Promise(); + var destinations = this.pdfInfo.destinations; + promise.resolve(destinations); + return promise; + }, + /** + * @return {Promise} A promise that is resolved with an {array} that is a + * tree outline (if it has one) of the PDF. The tree is in the format of: + * [ + * { + * title: string, + * bold: boolean, + * italic: boolean, + * color: rgb array, + * dest: dest obj, + * items: array of more items like this + * }, + * ... + * ]. + */ + getOutline: function() { + var promise = new PDFJS.Promise(); + var outline = this.pdfInfo.outline; + promise.resolve(outline); + return promise; + }, + /** + * @return {Promise} A promise that is resolved with an {object} that has + * info and metadata properties. Info is an {object} filled with anything + * available in the information dictionary and similarly metadata is a + * {Metadata} object with information from the metadata section of the PDF. + */ + getMetadata: function() { + var promise = new PDFJS.Promise(); + var info = this.pdfInfo.info; + var metadata = this.pdfInfo.metadata; + promise.resolve({ + info: info, + metadata: metadata ? new PDFJS.Metadata(metadata) : null + }); + return promise; + }, + destroy: function() { + this.transport.destroy(); + } + }; + return PDFDocumentProxy; +})(); + +var PDFPageProxy = (function PDFPageProxyClosure() { + function PDFPageProxy(pageInfo, transport) { + this.pageInfo = pageInfo; + this.transport = transport; + this.stats = new StatTimer(); + this.stats.enabled = !!globalScope.PDFJS.enableStats; + this.objs = transport.objs; + this.renderInProgress = false; + } + PDFPageProxy.prototype = { + /** + * @return {number} Page number of the page. First page is 1. + */ + get pageNumber() { + return this.pageInfo.pageIndex + 1; + }, + /** + * @return {number} The number of degrees the page is rotated clockwise. + */ + get rotate() { + return this.pageInfo.rotate; + }, + /** + * @return {object} The reference that points to this page. It has 'num' and + * 'gen' properties. + */ + get ref() { + return this.pageInfo.ref; + }, + /** + * @return {array} An array of the visible portion of the PDF page in the + * user space units - [x1, y1, x2, y2]. + */ + get view() { + return this.pageInfo.view; + }, + /** + * @param {number} scale The desired scale of the viewport. + * @param {number} rotate Degrees to rotate the viewport. If omitted this + * defaults to the page rotation. + * @return {PageViewport} Contains 'width' and 'height' properties along + * with transforms required for rendering. + */ + getViewport: function(scale, rotate) { + if (arguments.length < 2) + rotate = this.rotate; + return new PDFJS.PageViewport(this.view, scale, rotate, 0, 0); + }, + /** + * @return {Promise} A promise that is resolved with an {array} of the + * annotation objects. + */ + getAnnotations: function() { + if (this.annotationsPromise) + return this.annotationsPromise; + + var promise = new PDFJS.Promise(); + this.annotationsPromise = promise; + this.transport.getAnnotations(this.pageInfo.pageIndex); + return promise; + }, + /** + * Begins the process of rendering a page to the desired context. + * @param {object} params A parameter object that supports: + * { + * canvasContext(required): A 2D context of a DOM Canvas object., + * textLayer(optional): An object that has beginLayout, endLayout, and + * appendText functions. + * }. + * @return {Promise} A promise that is resolved when the page finishes + * rendering. + */ + render: function(params) { + this.renderInProgress = true; + + var promise = new Promise(); + var stats = this.stats; + stats.time('Overall'); + // If there is no displayReadyPromise yet, then the operatorList was never + // requested before. Make the request and create the promise. + if (!this.displayReadyPromise) { + this.displayReadyPromise = new Promise(); + this.destroyed = false; + + this.stats.time('Page Request'); + this.transport.messageHandler.send('RenderPageRequest', { + pageIndex: this.pageNumber - 1 + }); + } + + var self = this; + function complete(error) { + self.renderInProgress = false; + if (self.destroyed) { + delete self.operatorList; + delete self.displayReadyPromise; + } + + if (error) + promise.reject(error); + else + promise.resolve(); + }; + + // Once the operatorList and fonts are loaded, do the actual rendering. + this.displayReadyPromise.then( + function pageDisplayReadyPromise() { + if (self.destroyed) { + complete(); + return; + } + + var gfx = new CanvasGraphics(params.canvasContext, + this.objs, params.textLayer); + try { + this.display(gfx, params.viewport, complete); + } catch (e) { + complete(e); + } + }.bind(this), + function pageDisplayReadPromiseError(reason) { + complete(reason); + } + ); + + return promise; + }, + /** + * For internal use only. + */ + startRenderingFromOperatorList: + function PDFPageWrapper_startRenderingFromOperatorList(operatorList, + fonts) { + var self = this; + this.operatorList = operatorList; + + var displayContinuation = function pageDisplayContinuation() { + // Always defer call to display() to work around bug in + // Firefox error reporting from XHR callbacks. + setTimeout(function pageSetTimeout() { + self.displayReadyPromise.resolve(); + }); + }; + + this.ensureFonts(fonts, + function pageStartRenderingFromOperatorListEnsureFonts() { + displayContinuation(); + } + ); + }, + /** + * For internal use only. + */ + ensureFonts: function PDFPageWrapper_ensureFonts(fonts, callback) { + this.stats.time('Font Loading'); + // Convert the font names to the corresponding font obj. + for (var i = 0, ii = fonts.length; i < ii; i++) { + fonts[i] = this.objs.objs[fonts[i]].data; + } + + // Load all the fonts + FontLoader.bind( + fonts, + function pageEnsureFontsFontObjs(fontObjs) { + this.stats.timeEnd('Font Loading'); + + callback.call(this); + }.bind(this) + ); + }, + /** + * For internal use only. + */ + display: function PDFPageWrapper_display(gfx, viewport, callback) { + var stats = this.stats; + stats.time('Rendering'); + + gfx.beginDrawing(viewport); + + var startIdx = 0; + var length = this.operatorList.fnArray.length; + var operatorList = this.operatorList; + var stepper = null; + if (PDFJS.pdfBug && StepperManager.enabled) { + stepper = StepperManager.create(this.pageNumber - 1); + stepper.init(operatorList); + stepper.nextBreakPoint = stepper.getNextBreakPoint(); + } + + var self = this; + function next() { + startIdx = + gfx.executeOperatorList(operatorList, startIdx, next, stepper); + if (startIdx == length) { + gfx.endDrawing(); + stats.timeEnd('Rendering'); + stats.timeEnd('Overall'); + if (callback) callback(); + } + } + next(); + }, + /** + * Stub for future feature. + */ + getTextContent: function() { + var promise = new PDFJS.Promise(); + var textContent = 'page text'; // not implemented + promise.resolve(textContent); + return promise; + }, + /** + * Stub for future feature. + */ + getOperationList: function() { + var promise = new PDFJS.Promise(); + var operationList = { // not implemented + dependencyFontsID: null, + operatorList: null + }; + promise.resolve(operationList); + return promise; + }, + /** + * Destroys resources allocated by the page. + */ + destroy: function() { + this.destroyed = true; + + if (!this.renderInProgress) { + delete this.operatorList; + delete this.displayReadyPromise; + } + } + }; + return PDFPageProxy; +})(); +/** + * For internal use only. + */ +var WorkerTransport = (function WorkerTransportClosure() { + function WorkerTransport(promise) { + this.workerReadyPromise = promise; + this.objs = new PDFObjects(); + + this.pageCache = []; + this.pagePromises = []; + this.fontsLoading = {}; + + // If worker support isn't disabled explicit and the browser has worker + // support, create a new web worker and test if it/the browser fullfills + // all requirements to run parts of pdf.js in a web worker. + // Right now, the requirement is, that an Uint8Array is still an Uint8Array + // as it arrives on the worker. Chrome added this with version 15. + if (!globalScope.PDFJS.disableWorker && typeof Worker !== 'undefined') { + var workerSrc = PDFJS.workerSrc; + if (typeof workerSrc === 'undefined') { + error('No PDFJS.workerSrc specified'); + } + + try { + 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); + this.messageHandler = messageHandler; + + messageHandler.on('test', function transportTest(supportTypedArray) { + if (supportTypedArray) { + this.worker = worker; + this.setupMessageHandler(messageHandler); + } else { + globalScope.PDFJS.disableWorker = true; + this.setupFakeWorker(); + } + }.bind(this)); + + var testObj = new Uint8Array(1); + // Some versions of Opera throw a DATA_CLONE_ERR on + // serializing the typed array. + messageHandler.send('test', testObj); + return; + } 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. + globalScope.PDFJS.disableWorker = true; + this.setupFakeWorker(); + } + WorkerTransport.prototype = { + destroy: function WorkerTransport_destroy() { + if (this.worker) + this.worker.terminate(); + + this.pageCache = []; + this.pagePromises = []; + }, + setupFakeWorker: function WorkerTransport_setupFakeWorker() { + // If we don't use a worker, just post/sendMessage to the main thread. + var fakeWorker = { + postMessage: function WorkerTransport_postMessage(obj) { + fakeWorker.onmessage({data: obj}); + }, + terminate: function WorkerTransport_terminate() {} + }; + + var messageHandler = new MessageHandler('main', fakeWorker); + this.setupMessageHandler(messageHandler); + + // If the main thread is our worker, setup the handling for the messages + // the main thread sends to it self. + WorkerMessageHandler.setup(messageHandler); + }, + + setupMessageHandler: + function WorkerTransport_setupMessageHandler(messageHandler) { + this.messageHandler = messageHandler; + + messageHandler.on('GetDoc', function transportDoc(data) { + var pdfInfo = data.pdfInfo; + var pdfDocument = new PDFDocumentProxy(pdfInfo, this); + this.pdfDocument = pdfDocument; + this.workerReadyPromise.resolve(pdfDocument); + }, this); + + messageHandler.on('GetPage', function transportPage(data) { + var pageInfo = data.pageInfo; + var page = new PDFPageProxy(pageInfo, this); + this.pageCache[pageInfo.pageIndex] = page; + var promise = this.pagePromises[pageInfo.pageIndex]; + promise.resolve(page); + }, this); + + messageHandler.on('GetAnnotations', function transportAnnotations(data) { + var annotations = data.annotations; + var promise = this.pageCache[data.pageIndex].annotationsPromise; + promise.resolve(annotations); + }, this); + + messageHandler.on('RenderPage', function transportRender(data) { + var page = this.pageCache[data.pageIndex]; + var depFonts = data.depFonts; + + page.stats.timeEnd('Page Request'); + page.startRenderingFromOperatorList(data.operatorList, depFonts); + }, this); + + messageHandler.on('obj', function transportObj(data) { + var id = data[0]; + var type = data[1]; + if (this.objs.hasData(id)) + return; + + switch (type) { + case 'JpegStream': + var imageData = data[2]; + loadJpegStream(id, imageData, this.objs); + break; + case 'Image': + var imageData = data[2]; + this.objs.resolve(id, imageData); + break; + case 'Font': + var name = data[2]; + var file = data[3]; + var properties = data[4]; + + if (file) { + // Rewrap the ArrayBuffer in a stream. + var fontFileDict = new Dict(); + file = new Stream(file, 0, file.length, fontFileDict); + } + + // At this point, only the font object is created but the font is + // not yet attached to the DOM. This is done in `FontLoader.bind`. + var font = new Font(name, file, properties); + this.objs.resolve(id, font); + break; + default: + error('Got unkown object type ' + type); + } + }, this); + + messageHandler.on('PageError', function transportError(data) { + var page = this.pageCache[data.pageNum - 1]; + if (page.displayReadyPromise) + page.displayReadyPromise.reject(data.error); + else + error(data.error); + }, this); + + messageHandler.on('JpegDecode', function(data, promise) { + var imageData = data[0]; + var components = data[1]; + if (components != 3 && components != 1) + error('Only 3 component or 1 component can be returned'); + + var img = new Image(); + img.onload = (function messageHandler_onloadClosure() { + var width = img.width; + var height = img.height; + var size = width * height; + var rgbaLength = size * 4; + var buf = new Uint8Array(size * components); + var tmpCanvas = createScratchCanvas(width, height); + var tmpCtx = tmpCanvas.getContext('2d'); + tmpCtx.drawImage(img, 0, 0); + var data = tmpCtx.getImageData(0, 0, width, height).data; + + if (components == 3) { + for (var i = 0, j = 0; i < rgbaLength; i += 4, j += 3) { + buf[j] = data[i]; + buf[j + 1] = data[i + 1]; + buf[j + 2] = data[i + 2]; + } + } else if (components == 1) { + for (var i = 0, j = 0; i < rgbaLength; i += 4, j++) { + buf[j] = data[i]; + } + } + promise.resolve({ data: buf, width: width, height: height}); + }).bind(this); + var src = 'data:image/jpeg;base64,' + window.btoa(imageData); + img.src = src; + }); + }, + + sendData: function WorkerTransport_sendData(data) { + this.messageHandler.send('GetDocRequest', data); + }, + + getPage: function WorkerTransport_getPage(pageNumber, promise) { + var pageIndex = pageNumber - 1; + if (pageIndex in this.pagePromises) + return this.pagePromises[pageIndex]; + var promise = new PDFJS.Promise('Page ' + pageNumber); + this.pagePromises[pageIndex] = promise; + this.messageHandler.send('GetPageRequest', { pageIndex: pageIndex }); + return promise; + }, + + getAnnotations: function WorkerTransport_getAnnotations(pageIndex) { + this.messageHandler.send('GetAnnotationsRequest', + { pageIndex: pageIndex }); + } + }; + return WorkerTransport; + +})(); diff --git a/src/canvas.js b/src/canvas.js index 8f29051fd..9d470fbec 100644 --- a/src/canvas.js +++ b/src/canvas.js @@ -241,27 +241,10 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { 'shadingFill': true }, - beginDrawing: function CanvasGraphics_beginDrawing(mediaBox) { - var cw = this.ctx.canvas.width, ch = this.ctx.canvas.height; + beginDrawing: function CanvasGraphics_beginDrawing(viewport) { + var transform = viewport.transform; this.ctx.save(); - switch (mediaBox.rotate) { - case 0: - this.ctx.transform(1, 0, 0, -1, 0, ch); - break; - case 90: - this.ctx.transform(0, 1, 1, 0, 0, 0); - break; - case 180: - this.ctx.transform(-1, 0, 0, 1, cw, 0); - break; - case 270: - this.ctx.transform(0, -1, -1, 0, cw, ch); - break; - } - // Scale so that canvas units are the same as PDF user space units - this.ctx.scale(cw / mediaBox.width, ch / mediaBox.height); - // Move the media left-top corner to the (0,0) canvas position - this.ctx.translate(-mediaBox.x, -mediaBox.y); + this.ctx.transform.apply(this.ctx, transform); if (this.textLayer) this.textLayer.beginLayout(); diff --git a/src/core.js b/src/core.js index 15cd147e2..90a2c9733 100644 --- a/src/core.js +++ b/src/core.js @@ -63,8 +63,6 @@ var Page = (function PageClosure() { function Page(xref, pageNumber, pageDict, ref) { this.pageNumber = pageNumber; this.pageDict = pageDict; - this.stats = new StatTimer(); - this.stats.enabled = !!globalScope.PDFJS.enableStats; this.xref = xref; this.ref = ref; @@ -100,18 +98,10 @@ var Page = (function PageClosure() { return shadow(this, 'mediaBox', obj); }, get view() { - var cropBox = this.inheritPageProp('CropBox'); - var view = { - x: 0, - y: 0, - width: this.width, - height: this.height - }; - if (!isArray(cropBox) || cropBox.length !== 4) - return shadow(this, 'view', view); - var mediaBox = this.mediaBox; - var offsetX = mediaBox[0], offsetY = mediaBox[1]; + var cropBox = this.inheritPageProp('CropBox'); + if (!isArray(cropBox) || cropBox.length !== 4) + return shadow(this, 'view', mediaBox); // From the spec, 6th ed., p.963: // "The crop, bleed, trim, and art boxes should not ordinarily @@ -119,42 +109,13 @@ var Page = (function PageClosure() { // effectively reduced to their intersection with the media box." cropBox = Util.intersect(cropBox, mediaBox); if (!cropBox) - return shadow(this, 'view', view); + return shadow(this, 'view', mediaBox); - var tl = this.rotatePoint(cropBox[0] - offsetX, cropBox[1] - offsetY); - var br = this.rotatePoint(cropBox[2] - offsetX, cropBox[3] - offsetY); - view.x = Math.min(tl.x, br.x); - view.y = Math.min(tl.y, br.y); - view.width = Math.abs(tl.x - br.x); - view.height = Math.abs(tl.y - br.y); - - return shadow(this, 'view', view); + return shadow(this, 'view', cropBox); }, get annotations() { return shadow(this, 'annotations', this.inheritPageProp('Annots')); }, - get width() { - var mediaBox = this.mediaBox; - var rotate = this.rotate; - var width; - if (rotate == 0 || rotate == 180) { - width = (mediaBox[2] - mediaBox[0]); - } else { - width = (mediaBox[3] - mediaBox[1]); - } - return shadow(this, 'width', width); - }, - get height() { - var mediaBox = this.mediaBox; - var rotate = this.rotate; - var height; - if (rotate == 0 || rotate == 180) { - height = (mediaBox[3] - mediaBox[1]); - } else { - height = (mediaBox[2] - mediaBox[0]); - } - return shadow(this, 'height', height); - }, get rotate() { var rotate = this.inheritPageProp('Rotate') || 0; // Normalize rotation so it's a multiple of 90 and between 0 and 270 @@ -170,43 +131,19 @@ var Page = (function PageClosure() { return shadow(this, 'rotate', rotate); }, - startRenderingFromOperatorList: - function Page_startRenderingFromOperatorList(operatorList, fonts) { - var self = this; - this.operatorList = operatorList; - - var displayContinuation = function pageDisplayContinuation() { - // Always defer call to display() to work around bug in - // Firefox error reporting from XHR callbacks. - setTimeout(function pageSetTimeout() { - self.displayReadyPromise.resolve(); - }); - }; - - this.ensureFonts(fonts, - function pageStartRenderingFromOperatorListEnsureFonts() { - displayContinuation(); - } - ); - }, - getOperatorList: function Page_getOperatorList(handler, dependency) { - if (this.operatorList) { - // content was compiled - return this.operatorList; - } - - this.stats.time('Build IR Queue'); - var xref = this.xref; var content = this.content; var resources = this.resources; if (isArray(content)) { // fetching items + var streams = []; var i, n = content.length; for (i = 0; i < n; ++i) - content[i] = xref.fetchIfRef(content[i]); - content = new StreamsSequenceStream(content); + streams.push(xref.fetchIfRef(content[i])); + content = new StreamsSequenceStream(streams); + } else if (isStream(content)) { + content.reset(); } else if (!content) { // replacing non-existent page content with empty one content = new Stream(new Uint8Array(0)); @@ -215,82 +152,9 @@ var Page = (function PageClosure() { var pe = this.pe = new PartialEvaluator( xref, handler, 'p' + this.pageNumber + '_'); - this.operatorList = pe.getOperatorList(content, resources, dependency); - this.stats.timeEnd('Build IR Queue'); - return this.operatorList; + return pe.getOperatorList(content, resources, dependency); }, - ensureFonts: function Page_ensureFonts(fonts, callback) { - this.stats.time('Font Loading'); - // Convert the font names to the corresponding font obj. - for (var i = 0, ii = fonts.length; i < ii; i++) { - fonts[i] = this.objs.objs[fonts[i]].data; - } - - // Load all the fonts - FontLoader.bind( - fonts, - function pageEnsureFontsFontObjs(fontObjs) { - this.stats.timeEnd('Font Loading'); - - callback.call(this); - }.bind(this) - ); - }, - - display: function Page_display(gfx, callback) { - var stats = this.stats; - stats.time('Rendering'); - var xref = this.xref; - var resources = this.resources; - var mediaBox = this.mediaBox; - assertWellFormed(isDict(resources), 'invalid page resources'); - - gfx.xref = xref; - gfx.res = resources; - gfx.beginDrawing({ x: mediaBox[0], y: mediaBox[1], - width: this.width, - height: this.height, - rotate: this.rotate }); - - var startIdx = 0; - var length = this.operatorList.fnArray.length; - var operatorList = this.operatorList; - var stepper = null; - if (PDFJS.pdfBug && StepperManager.enabled) { - stepper = StepperManager.create(this.pageNumber); - stepper.init(operatorList); - stepper.nextBreakPoint = stepper.getNextBreakPoint(); - } - - var self = this; - function next() { - startIdx = - gfx.executeOperatorList(operatorList, startIdx, next, stepper); - if (startIdx == length) { - gfx.endDrawing(); - stats.timeEnd('Rendering'); - stats.timeEnd('Overall'); - if (callback) callback(); - } - } - next(); - }, - rotatePoint: function Page_rotatePoint(x, y, reverse) { - var rotate = reverse ? (360 - this.rotate) : this.rotate; - switch (rotate) { - case 180: - return {x: this.width - x, y: y}; - case 90: - return {x: this.width - y, y: this.height - x}; - case 270: - return {x: y, y: x}; - case 360: - case 0: - default: - return {x: x, y: this.height - y}; - } - }, getLinks: function Page_getLinks() { var links = []; var annotations = pageGetAnnotations(); @@ -342,15 +206,10 @@ var Page = (function PageClosure() { if (!isName(subtype)) continue; var rect = annotation.get('Rect'); - var topLeftCorner = this.rotatePoint(rect[0], rect[1]); - var bottomRightCorner = this.rotatePoint(rect[2], rect[3]); var item = {}; item.type = subtype.name; - item.x = Math.min(topLeftCorner.x, bottomRightCorner.x); - item.y = Math.min(topLeftCorner.y, bottomRightCorner.y); - item.width = Math.abs(topLeftCorner.x - bottomRightCorner.x); - item.height = Math.abs(topLeftCorner.y - bottomRightCorner.y); + item.rect = rect; switch (subtype.name) { case 'Link': var a = annotation.get('A'); @@ -424,7 +283,8 @@ var Page = (function PageClosure() { var title = annotation.get('T'); item.content = stringToPDFString(content || ''); item.title = stringToPDFString(title || ''); - item.name = annotation.get('Name').name; + item.name = !annotation.has('Name') ? 'Note' : + annotation.get('Name').name; break; default: TODO('unimplemented annotation type: ' + subtype.name); @@ -433,37 +293,6 @@ var Page = (function PageClosure() { items.push(item); } return items; - }, - startRendering: function Page_startRendering(ctx, callback, textLayer) { - var stats = this.stats; - stats.time('Overall'); - // If there is no displayReadyPromise yet, then the operatorList was never - // requested before. Make the request and create the promise. - if (!this.displayReadyPromise) { - this.pdf.startRendering(this); - this.displayReadyPromise = new Promise(); - } - - // Once the operatorList and fonts are loaded, do the actual rendering. - this.displayReadyPromise.then( - function pageDisplayReadyPromise() { - var gfx = new CanvasGraphics(ctx, this.objs, textLayer); - try { - this.display(gfx, callback); - } catch (e) { - if (callback) - callback(e); - else - error(e); - } - }.bind(this), - function pageDisplayReadPromiseError(reason) { - if (callback) - callback(reason); - else - error(reason); - } - ); } }; @@ -471,20 +300,20 @@ var Page = (function PageClosure() { })(); /** - * The `PDFDocModel` holds all the data of the PDF file. Compared to the + * The `PDFDocument` holds all the data of the PDF file. Compared to the * `PDFDoc`, this one doesn't have any job management code. - * Right now there exists one PDFDocModel on the main thread + one object + * Right now there exists one PDFDocument on the main thread + one object * for each worker. If there is no worker support enabled, there are two - * `PDFDocModel` objects on the main thread created. + * `PDFDocument` objects on the main thread created. */ -var PDFDocModel = (function PDFDocModelClosure() { - function PDFDocModel(arg, callback) { +var PDFDocument = (function PDFDocumentClosure() { + function PDFDocument(arg, callback) { if (isStream(arg)) init.call(this, arg); else if (isArrayBuffer(arg)) init.call(this, new Stream(arg)); else - error('PDFDocModel: Unknown argument type'); + error('PDFDocument: Unknown argument type'); } function init(stream) { @@ -510,7 +339,7 @@ var PDFDocModel = (function PDFDocModelClosure() { return true; /* found */ } - PDFDocModel.prototype = { + PDFDocument.prototype = { get linearization() { var length = this.stream.length; var linearization = false; @@ -571,7 +400,7 @@ var PDFDocModel = (function PDFDocModelClosure() { }, // Find the header, remove leading garbage and setup the stream // starting from the header. - checkHeader: function PDFDocModel_checkHeader() { + checkHeader: function PDFDocument_checkHeader() { var stream = this.stream; stream.reset(); if (find(stream, '%PDF-', 1024)) { @@ -581,7 +410,7 @@ var PDFDocModel = (function PDFDocModelClosure() { } // May not be a PDF file, continue anyway. }, - setup: function PDFDocModel_setup(ownerPassword, userPassword) { + setup: function PDFDocument_setup(ownerPassword, userPassword) { this.checkHeader(); var xref = new XRef(this.stream, this.startXRef, @@ -595,7 +424,7 @@ var PDFDocModel = (function PDFDocModelClosure() { // shadow the prototype getter return shadow(this, 'numPages', num); }, - getDocumentInfo: function PDFDocModel_getDocumentInfo() { + getDocumentInfo: function PDFDocument_getDocumentInfo() { var info; if (this.xref.trailer.has('Info')) { var infoDict = this.xref.trailer.get('Info'); @@ -609,7 +438,7 @@ var PDFDocModel = (function PDFDocModelClosure() { return shadow(this, 'getDocumentInfo', info); }, - getFingerprint: function PDFDocModel_getFingerprint() { + getFingerprint: function PDFDocument_getFingerprint() { var xref = this.xref, fileID; if (xref.trailer.has('ID')) { fileID = ''; @@ -630,251 +459,10 @@ var PDFDocModel = (function PDFDocModelClosure() { return shadow(this, 'getFingerprint', fileID); }, - getPage: function PDFDocModel_getPage(n) { + getPage: function PDFDocument_getPage(n) { return this.catalog.getPage(n); } }; - return PDFDocModel; + return PDFDocument; })(); - -var PDFDoc = (function PDFDocClosure() { - function PDFDoc(arg, callback) { - var stream = null; - var data = null; - - if (isStream(arg)) { - stream = arg; - data = arg.bytes; - } else if (isArrayBuffer(arg)) { - stream = new Stream(arg); - data = arg; - } else { - error('PDFDoc: Unknown argument type'); - } - - this.data = data; - this.stream = stream; - this.pdfModel = new PDFDocModel(stream); - this.fingerprint = this.pdfModel.getFingerprint(); - this.info = this.pdfModel.getDocumentInfo(); - this.catalog = this.pdfModel.catalog; - this.objs = new PDFObjects(); - - this.pageCache = []; - this.fontsLoading = {}; - this.workerReadyPromise = new Promise('workerReady'); - - // If worker support isn't disabled explicit and the browser has worker - // support, create a new web worker and test if it/the browser fullfills - // all requirements to run parts of pdf.js in a web worker. - // Right now, the requirement is, that an Uint8Array is still an Uint8Array - // as it arrives on the worker. Chrome added this with version 15. - if (!globalScope.PDFJS.disableWorker && typeof Worker !== 'undefined') { - var workerSrc = PDFJS.workerSrc; - if (typeof workerSrc === 'undefined') { - error('No PDFJS.workerSrc specified'); - } - - try { - 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); - - messageHandler.on('test', function pdfDocTest(supportTypedArray) { - if (supportTypedArray) { - this.worker = worker; - this.setupMessageHandler(messageHandler); - } else { - globalScope.PDFJS.disableWorker = true; - this.setupFakeWorker(); - } - }.bind(this)); - - var testObj = new Uint8Array(1); - // Some versions of Opera throw a DATA_CLONE_ERR on - // serializing the typed array. - messageHandler.send('test', testObj); - return; - } 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. - globalScope.PDFJS.disableWorker = true; - this.setupFakeWorker(); - } - - PDFDoc.prototype = { - setupFakeWorker: function PDFDoc_setupFakeWorker() { - // If we don't use a worker, just post/sendMessage to the main thread. - var fakeWorker = { - postMessage: function PDFDoc_postMessage(obj) { - fakeWorker.onmessage({data: obj}); - }, - terminate: function PDFDoc_terminate() {} - }; - - var messageHandler = new MessageHandler('main', fakeWorker); - this.setupMessageHandler(messageHandler); - - // If the main thread is our worker, setup the handling for the messages - // the main thread sends to it self. - WorkerMessageHandler.setup(messageHandler); - }, - - - setupMessageHandler: function PDFDoc_setupMessageHandler(messageHandler) { - this.messageHandler = messageHandler; - - messageHandler.on('page', function pdfDocPage(data) { - var pageNum = data.pageNum; - var page = this.pageCache[pageNum]; - var depFonts = data.depFonts; - - page.stats.timeEnd('Page Request'); - page.startRenderingFromOperatorList(data.operatorList, depFonts); - }, this); - - messageHandler.on('obj', function pdfDocObj(data) { - var id = data[0]; - var type = data[1]; - - switch (type) { - case 'JpegStream': - var imageData = data[2]; - loadJpegStream(id, imageData, this.objs); - break; - case 'Image': - var imageData = data[2]; - this.objs.resolve(id, imageData); - break; - case 'Font': - var name = data[2]; - var file = data[3]; - var properties = data[4]; - - if (file) { - // Rewrap the ArrayBuffer in a stream. - var fontFileDict = new Dict(); - file = new Stream(file, 0, file.length, fontFileDict); - } - - // At this point, only the font object is created but the font is - // not yet attached to the DOM. This is done in `FontLoader.bind`. - var font = new Font(name, file, properties); - this.objs.resolve(id, font); - break; - default: - error('Got unkown object type ' + type); - } - }, this); - - messageHandler.on('page_error', function pdfDocError(data) { - var page = this.pageCache[data.pageNum]; - if (page.displayReadyPromise) - page.displayReadyPromise.reject(data.error); - else - error(data.error); - }, this); - - messageHandler.on('jpeg_decode', function(data, promise) { - var imageData = data[0]; - var components = data[1]; - if (components != 3 && components != 1) - error('Only 3 component or 1 component can be returned'); - - var img = new Image(); - img.onload = (function messageHandler_onloadClosure() { - var width = img.width; - var height = img.height; - var size = width * height; - var rgbaLength = size * 4; - var buf = new Uint8Array(size * components); - var tmpCanvas = createScratchCanvas(width, height); - var tmpCtx = tmpCanvas.getContext('2d'); - tmpCtx.drawImage(img, 0, 0); - var data = tmpCtx.getImageData(0, 0, width, height).data; - - if (components == 3) { - for (var i = 0, j = 0; i < rgbaLength; i += 4, j += 3) { - buf[j] = data[i]; - buf[j + 1] = data[i + 1]; - buf[j + 2] = data[i + 2]; - } - } else if (components == 1) { - for (var i = 0, j = 0; i < rgbaLength; i += 4, j++) { - buf[j] = data[i]; - } - } - promise.resolve({ data: buf, width: width, height: height}); - }).bind(this); - var src = 'data:image/jpeg;base64,' + window.btoa(imageData); - img.src = src; - }); - - setTimeout(function pdfDocFontReadySetTimeout() { - messageHandler.send('doc', this.data); - this.workerReadyPromise.resolve(true); - }.bind(this)); - }, - - get numPages() { - return this.pdfModel.numPages; - }, - - startRendering: function PDFDoc_startRendering(page) { - // The worker might not be ready to receive the page request yet. - this.workerReadyPromise.then(function pdfDocStartRenderingThen() { - page.stats.time('Page Request'); - this.messageHandler.send('page_request', page.pageNumber + 1); - }.bind(this)); - }, - - getPage: function PDFDoc_getPage(n) { - if (this.pageCache[n]) - return this.pageCache[n]; - - var page = this.pdfModel.getPage(n); - // Add a reference to the objects such that Page can forward the reference - // to the CanvasGraphics and so on. - page.objs = this.objs; - page.pdf = this; - return (this.pageCache[n] = page); - }, - - destroy: function PDFDoc_destroy() { - if (this.worker) - this.worker.terminate(); - - if (this.fontWorker) - this.fontWorker.terminate(); - - for (var n in this.pageCache) - delete this.pageCache[n]; - - delete this.data; - delete this.stream; - delete this.pdf; - delete this.catalog; - } - }; - - return PDFDoc; -})(); - -globalScope.PDFJS.PDFDoc = PDFDoc; - diff --git a/src/evaluator.js b/src/evaluator.js index 350ab20b2..e07394201 100644 --- a/src/evaluator.js +++ b/src/evaluator.js @@ -153,13 +153,14 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { font = xref.fetchIfRef(font) || fontRes.get(fontName); assertWellFormed(isDict(font)); + ++self.objIdCounter; if (!font.translated) { font.translated = self.translateFont(font, xref, resources, dependency); if (font.translated) { // keep track of each font we translated so the caller can // load them asynchronously before calling display on a page - loadedName = 'font_' + uniquePrefix + (++self.objIdCounter); + loadedName = 'font_' + uniquePrefix + self.objIdCounter; font.translated.properties.loadedName = loadedName; font.loadedName = loadedName; @@ -466,7 +467,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { args = []; } else if (obj != null) { assertWellFormed(args.length <= 33, 'Too many arguments'); - args.push(obj); + args.push(obj instanceof Dict ? obj.getAll() : obj); } } @@ -862,7 +863,6 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { properties.coded = true; var charProcs = dict.get('CharProcs').getAll(); var fontResources = dict.get('Resources') || resources; - properties.resources = fontResources; properties.charProcOperatorList = {}; for (var key in charProcs) { var glyphStream = charProcs[key]; diff --git a/src/fonts.js b/src/fonts.js index 7fdab8fbb..7bd3ddd06 100644 --- a/src/fonts.js +++ b/src/fonts.js @@ -766,7 +766,6 @@ var Font = (function FontClosure() { this.name = name; this.coded = properties.coded; this.charProcOperatorList = properties.charProcOperatorList; - this.resources = properties.resources; this.sizes = []; var names = name.split('+'); @@ -1727,6 +1726,16 @@ var Font = (function FontClosure() { properties.glyphNames = glyphNames; } + function isOS2Valid(os2Table) { + var data = os2Table.data; + // usWinAscent == 0 makes font unreadable by windows + var usWinAscent = (data[74] << 8) | data[75]; + if (usWinAscent == 0) + return false; + + return true; + } + // Check that required tables are present var requiredTables = ['OS/2', 'cmap', 'head', 'hhea', 'hmtx', 'maxp', 'name', 'post']; @@ -1734,7 +1743,7 @@ var Font = (function FontClosure() { var header = readOpenTypeHeader(font); var numTables = header.numTables; - var cmap, post, maxp, hhea, hmtx, vhea, vmtx, head, loca, glyf; + var cmap, post, maxp, hhea, hmtx, vhea, vmtx, head, loca, glyf, os2; var tables = []; for (var i = 0; i < numTables; i++) { var table = readTableEntry(font); @@ -1752,6 +1761,8 @@ var Font = (function FontClosure() { hmtx = table; else if (table.tag == 'head') head = table; + else if (table.tag == 'OS/2') + os2 = table; requiredTables.splice(index, 1); } else { @@ -1767,7 +1778,7 @@ var Font = (function FontClosure() { tables.push(table); } - var numTables = header.numTables + requiredTables.length; + var numTables = tables.length + requiredTables.length; // header and new offsets. Table entry information is appended to the // end of file. The virtualOffset represents where to put the actual @@ -1781,21 +1792,10 @@ var Font = (function FontClosure() { // of missing tables createOpenTypeHeader(header.version, ttf, numTables); - if (requiredTables.indexOf('OS/2') != -1) { - // extract some more font properties from the OpenType head and - // hhea tables; yMin and descent value are always negative - var override = { - unitsPerEm: int16([head.data[18], head.data[19]]), - yMax: int16([head.data[42], head.data[43]]), - yMin: int16([head.data[38], head.data[39]]) - 0x10000, - ascent: int16([hhea.data[4], hhea.data[5]]), - descent: int16([hhea.data[6], hhea.data[7]]) - 0x10000 - }; - - tables.push({ - tag: 'OS/2', - data: stringToArray(createOS2Table(properties, null, override)) - }); + // Invalid OS/2 can break the font for the Windows + if (os2 && !isOS2Valid(os2)) { + tables.splice(tables.indexOf(os2), 1); + os2 = null; } // Ensure the [h/v]mtx tables contains the advance width and @@ -2076,6 +2076,23 @@ var Font = (function FontClosure() { } this.unicodeIsEnabled = unicodeIsEnabled; + if (!os2) { + // extract some more font properties from the OpenType head and + // hhea tables; yMin and descent value are always negative + var override = { + unitsPerEm: int16([head.data[18], head.data[19]]), + yMax: int16([head.data[42], head.data[43]]), + yMin: int16([head.data[38], head.data[39]]) - 0x10000, + ascent: int16([hhea.data[4], hhea.data[5]]), + descent: int16([hhea.data[6], hhea.data[7]]) - 0x10000 + }; + + tables.push({ + tag: 'OS/2', + data: stringToArray(createOS2Table(properties, glyphs, override)) + }); + } + // Rewrite the 'post' table if needed if (requiredTables.indexOf('post') != -1) { tables.push({ diff --git a/src/image.js b/src/image.js index 035e2f754..c8c19f9e5 100644 --- a/src/image.js +++ b/src/image.js @@ -15,7 +15,7 @@ var PDFImage = (function PDFImageClosure() { var colorSpace = dict.get('ColorSpace', 'CS'); colorSpace = ColorSpace.parse(colorSpace, xref, res); var numComps = colorSpace.numComps; - handler.send('jpeg_decode', [image.getIR(), numComps], function(message) { + handler.send('JpegDecode', [image.getIR(), numComps], function(message) { var data = message.data; var stream = new Stream(data, 0, data.length, image.dict); promise.resolve(stream); diff --git a/src/obj.js b/src/obj.js index 200b40a7f..c905a7dc5 100644 --- a/src/obj.js +++ b/src/obj.js @@ -37,51 +37,55 @@ var Dict = (function DictClosure() { // xref is optional function Dict(xref) { // Map should only be used internally, use functions below to access. - this.map = Object.create(null); - this.xref = xref; - } + var map = Object.create(null); + + this.assignXref = function Dict_assingXref(newXref) { + xref = newXref; + }; - Dict.prototype = { // automatically dereferences Ref objects - get: function Dict_get(key1, key2, key3) { + this.get = function Dict_get(key1, key2, key3) { var value; - var xref = this.xref; - if (typeof (value = this.map[key1]) != 'undefined' || key1 in this.map || + if (typeof (value = map[key1]) != 'undefined' || key1 in map || typeof key2 == 'undefined') { - return xref ? this.xref.fetchIfRef(value) : value; + return xref ? xref.fetchIfRef(value) : value; } - if (typeof (value = this.map[key2]) != 'undefined' || key2 in this.map || + if (typeof (value = map[key2]) != 'undefined' || key2 in map || typeof key3 == 'undefined') { - return xref ? this.xref.fetchIfRef(value) : value; + return xref ? xref.fetchIfRef(value) : value; } - value = this.map[key3] || null; - return xref ? this.xref.fetchIfRef(value) : value; - }, + value = map[key3] || null; + return xref ? xref.fetchIfRef(value) : value; + }; + // no dereferencing - getRaw: function Dict_getRaw(key) { - return this.map[key]; - }, + this.getRaw = function Dict_getRaw(key) { + return map[key]; + }; + // creates new map and dereferences all Refs - getAll: function Dict_getAll() { + this.getAll = function Dict_getAll() { var all = {}; - for (var key in this.map) - all[key] = this.get(key); + for (var key in map) { + var obj = this.get(key); + all[key] = obj instanceof Dict ? obj.getAll() : obj; + } return all; - }, + }; - set: function Dict_set(key, value) { - this.map[key] = value; - }, + this.set = function Dict_set(key, value) { + map[key] = value; + }; - has: function Dict_has(key) { - return key in this.map; - }, + this.has = function Dict_has(key) { + return key in map; + }; - forEach: function Dict_forEach(callback) { - for (var key in this.map) { + this.forEach = function Dict_forEach(callback) { + for (var key in map) { callback(key, this.get(key)); } - } + }; }; return Dict; @@ -299,7 +303,7 @@ var XRef = (function XRefClosure() { this.entries = []; this.xrefstms = {}; var trailerDict = this.readXRef(startXRef); - trailerDict.xref = this; + trailerDict.assignXref(this); this.trailer = trailerDict; // prepare the XRef cache this.cache = []; diff --git a/src/parser.js b/src/parser.js index 1c50d0f5f..2855018a6 100644 --- a/src/parser.js +++ b/src/parser.js @@ -249,9 +249,12 @@ var Parser = (function ParserClosure() { if (name == 'CCITTFaxDecode' || name == 'CCF') { return new CCITTFaxStream(stream, params); } - if (name == 'RunLengthDecode') { + if (name == 'RunLengthDecode' || name == 'RL') { return new RunLengthStream(stream); } + if (name == 'JBIG2Decode') { + error('JBIG2 image format is not currently supprted.'); + } warn('filter "' + name + '" not supported yet'); return stream; } diff --git a/src/util.js b/src/util.js index de7f3c1d5..63f6115a7 100644 --- a/src/util.js +++ b/src/util.js @@ -76,7 +76,7 @@ function stringToBytes(str) { var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0]; -var Util = (function UtilClosure() { +var Util = PDFJS.Util = (function UtilClosure() { function Util() {} Util.makeCssRgb = function Util_makeCssRgb(r, g, b) { @@ -97,6 +97,19 @@ var Util = (function UtilClosure() { return [xt, yt]; }; + Util.applyInverseTransform = function Util_applyTransform(p, m) { + var d = m[0] * m[3] - m[1] * m[2]; + var xt = (p[0] * m[3] - p[1] * m[2] + m[2] * m[5] - m[4] * m[3]) / d; + var yt = (-p[0] * m[1] + p[1] * m[0] + m[4] * m[1] - m[5] * m[0]) / d; + return [xt, yt]; + }; + + Util.inverseTransform = function Util_inverseTransform(m) { + var d = m[0] * m[3] - m[1] * m[2]; + return [m[3] / d, -m[1] / d, -m[2] / d, m[0] / d, + (m[2] * m[5] - m[4] * m[3]) / d, (m[4] * m[1] - m[5] * m[0]) / d]; + }; + // Apply a generic 3d matrix M on a 3-vector v: // | a b c | | X | // | d e f | x | Y | @@ -165,7 +178,7 @@ var Util = (function UtilClosure() { } return result; - } + }; Util.sign = function Util_sign(num) { return num < 0 ? -1 : 1; @@ -174,6 +187,80 @@ var Util = (function UtilClosure() { return Util; })(); +var PageViewport = PDFJS.PageViewport = (function PageViewportClosure() { + function PageViewport(viewBox, scale, rotate, offsetX, offsetY) { + // creating transform to convert pdf coordinate system to the normal + // canvas like coordinates taking in account scale and rotation + var centerX = (viewBox[2] + viewBox[0]) / 2; + var centerY = (viewBox[3] + viewBox[1]) / 2; + var rotateA, rotateB, rotateC, rotateD; + switch (rotate) { + case -180: + case 180: + rotateA = -1; rotateB = 0; rotateC = 0; rotateD = 1; + break; + case -270: + case 90: + rotateA = 0; rotateB = 1; rotateC = 1; rotateD = 0; + break; + case -90: + case 270: + rotateA = 0; rotateB = -1; rotateC = -1; rotateD = 0; + break; + case 360: + case 0: + default: + rotateA = 1; rotateB = 0; rotateC = 0; rotateD = -1; + break; + } + var offsetCanvasX, offsetCanvasY; + var width, height; + if (rotateA == 0) { + offsetCanvasX = Math.abs(centerY - viewBox[1]) * scale + offsetX; + offsetCanvasY = Math.abs(centerX - viewBox[0]) * scale + offsetY; + width = Math.abs(viewBox[3] - viewBox[1]) * scale; + height = Math.abs(viewBox[2] - viewBox[0]) * scale; + } else { + offsetCanvasX = Math.abs(centerX - viewBox[0]) * scale + offsetX; + offsetCanvasY = Math.abs(centerY - viewBox[1]) * scale + offsetY; + width = Math.abs(viewBox[2] - viewBox[0]) * scale; + height = Math.abs(viewBox[3] - viewBox[1]) * scale; + } + // creating transform for the following operations: + // translate(-centerX, -centerY), rotate and flip vertically, + // scale, and translate(offsetCanvasX, offsetCanvasY) + this.transform = [ + rotateA * scale, + rotateB * scale, + rotateC * scale, + rotateD * scale, + offsetCanvasX - rotateA * scale * centerX - rotateC * scale * centerY, + offsetCanvasY - rotateB * scale * centerX - rotateD * scale * centerY + ]; + + this.offsetX = offsetX; + this.offsetY = offsetY; + this.width = width; + this.height = height; + this.fontScale = scale; + } + PageViewport.prototype = { + convertToViewportPoint: function PageViewport_convertToViewportPoint(x, y) { + return Util.applyTransform([x, y], this.transform); + }, + convertToViewportRectangle: + function PageViewport_convertToViewportRectangle(rect) { + var tl = Util.applyTransform([rect[0], rect[1]], this.transform); + var br = Util.applyTransform([rect[2], rect[3]], this.transform); + return [tl[0], tl[1], br[0], br[1]]; + }, + convertToPdfPoint: function PageViewport_convertToPdfPoint(x, y) { + return Util.applyInverseTransform([x, y], this.transform); + } + }; + return PageViewport; +})(); + var PDFStringTranslateTable = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2D8, 0x2C7, 0x2C6, 0x2D9, 0x2DD, 0x2DB, 0x2DA, 0x2DC, 0, 0, 0, 0, 0, 0, 0, @@ -275,7 +362,7 @@ function isPDFFunction(v) { * can be set. If any of these happens twice or the data is required before * it was set, an exception is throw. */ -var Promise = (function PromiseClosure() { +var Promise = PDFJS.Promise = (function PromiseClosure() { var EMPTY_PROMISE = {}; /** @@ -297,6 +384,7 @@ var Promise = (function PromiseClosure() { } this.callbacks = []; this.errbacks = []; + this.progressbacks = []; }; /** * Builds a promise that is resolved when all the passed in promises are @@ -312,7 +400,7 @@ var Promise = (function PromiseClosure() { deferred.resolve(results); return deferred; } - for (var i = 0; i < unresolved; ++i) { + for (var i = 0, ii = promises.length; i < ii; ++i) { var promise = promises[i]; promise.then((function(i) { return function(value) { @@ -376,6 +464,13 @@ var Promise = (function PromiseClosure() { } }, + progress: function Promise_progress(data) { + var callbacks = this.progressbacks; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + callbacks[i].call(null, data); + } + }, + reject: function Promise_reject(reason) { if (this.isRejected) { error('A Promise can be rejected only once ' + this.name); @@ -393,7 +488,7 @@ var Promise = (function PromiseClosure() { } }, - then: function Promise_then(callback, errback) { + then: function Promise_then(callback, errback, progressback) { if (!callback) { error('Requiring callback' + this.name); } @@ -410,6 +505,9 @@ var Promise = (function PromiseClosure() { if (errback) this.errbacks.push(errback); } + + if (progressback) + this.progressbacks.push(progressback); } }; diff --git a/src/worker.js b/src/worker.js index 42bd61050..25f3f52cd 100644 --- a/src/worker.js +++ b/src/worker.js @@ -85,14 +85,43 @@ var WorkerMessageHandler = { handler.send('test', data instanceof Uint8Array); }); - handler.on('doc', function wphSetupDoc(data) { + handler.on('GetDocRequest', function wphSetupDoc(data) { // Create only the model of the PDFDoc, which is enough for // processing the content of the pdf. - pdfModel = new PDFDocModel(new Stream(data)); + pdfModel = new PDFDocument(new Stream(data)); + var doc = { + numPages: pdfModel.numPages, + fingerprint: pdfModel.getFingerprint(), + destinations: pdfModel.catalog.destinations, + outline: pdfModel.catalog.documentOutline, + info: pdfModel.getDocumentInfo(), + metadata: pdfModel.catalog.metadata + }; + handler.send('GetDoc', {pdfInfo: doc}); }); - handler.on('page_request', function wphSetupPageRequest(pageNum) { - pageNum = parseInt(pageNum); + handler.on('GetPageRequest', function wphSetupGetPage(data) { + var pageNumber = data.pageIndex + 1; + var pdfPage = pdfModel.getPage(pageNumber); + var page = { + pageIndex: data.pageIndex, + rotate: pdfPage.rotate, + ref: pdfPage.ref, + view: pdfPage.view + }; + handler.send('GetPage', {pageInfo: page}); + }); + + handler.on('GetAnnotationsRequest', function wphSetupGetAnnotations(data) { + var pdfPage = pdfModel.getPage(data.pageIndex + 1); + handler.send('GetAnnotations', { + pageIndex: data.pageIndex, + annotations: pdfPage.getAnnotations() + }); + }); + + handler.on('RenderPageRequest', function wphSetupRenderPage(data) { + var pageNum = data.pageIndex + 1; // The following code does quite the same as @@ -130,7 +159,7 @@ var WorkerMessageHandler = { }; } - handler.send('page_error', { + handler.send('PageError', { pageNum: pageNum, error: e }); @@ -148,9 +177,8 @@ var WorkerMessageHandler = { fonts[dep] = true; } } - - handler.send('page', { - pageNum: pageNum, + handler.send('RenderPage', { + pageIndex: data.pageIndex, operatorList: operatorList, depFonts: Object.keys(fonts) }); diff --git a/test/driver.js b/test/driver.js index a1dc4b242..cd5ea49e7 100644 --- a/test/driver.js +++ b/test/driver.js @@ -10,7 +10,7 @@ // Disable worker support for running test as // https://github.com/mozilla/pdf.js/pull/764#issuecomment-2638944 // "firefox-bin: Fatal IO error 12 (Cannot allocate memory) on X server :1." -PDFJS.disableWorker = true; +// PDFJS.disableWorker = true; var appPath, browser, canvas, currentTaskIdx, manifest, stdout; var inFlightRequests = 0; @@ -100,13 +100,24 @@ function nextTask() { getPdf(task.file, function nextTaskGetPdf(data) { var failure; + function continuation() { + task.pageNum = task.firstPage || 1; + nextPage(task, failure); + } try { - task.pdfDoc = new PDFJS.PDFDoc(data); + var promise = PDFJS.getDocument(data); + promise.then(function(doc) { + task.pdfDoc = doc; + continuation(); + }, function(e) { + failure = 'load PDF doc : ' + e; + continuation(); + }); + return; } catch (e) { failure = 'load PDF doc : ' + exceptionToString(e); } - task.pageNum = task.firstPage || 1; - nextPage(task, failure); + continuation(); }); } @@ -163,45 +174,45 @@ function nextPage(task, loadError) { log(' loading page ' + task.pageNum + '/' + task.pdfDoc.numPages + '... '); var ctx = canvas.getContext('2d'); - page = task.pdfDoc.getPage(task.pageNum); + task.pdfDoc.getPage(task.pageNum).then(function(page) { + var pdfToCssUnitsCoef = 96.0 / 72.0; + var viewport = page.getViewport(pdfToCssUnitsCoef); + canvas.width = viewport.width; + canvas.height = viewport.height; + clear(ctx); - var pdfToCssUnitsCoef = 96.0 / 72.0; - // using mediaBox for the canvas size - var pageWidth = page.width; - var pageHeight = page.height; - canvas.width = pageWidth * pdfToCssUnitsCoef; - canvas.height = pageHeight * pdfToCssUnitsCoef; - clear(ctx); - - // using the text layer builder that does nothing to test - // text layer creation operations - var textLayerBuilder = { - beginLayout: function nullTextLayerBuilderBeginLayout() {}, - endLayout: function nullTextLayerBuilderEndLayout() {}, - appendText: function nullTextLayerBuilderAppendText(text, fontName, - fontSize) {} - }; - - page.startRendering( - ctx, - function nextPageStartRendering(error) { - var failureMessage = false; - if (error) - failureMessage = 'render : ' + error.message; - snapshotCurrentPage(task, failureMessage); + // using the text layer builder that does nothing to test + // text layer creation operations + var textLayerBuilder = { + beginLayout: function nullTextLayerBuilderBeginLayout() {}, + endLayout: function nullTextLayerBuilderEndLayout() {}, + appendText: function nullTextLayerBuilderAppendText(text, fontName, + fontSize) {} + }; + var renderContext = { + canvasContext: ctx, + textLayer: textLayerBuilder, + viewport: viewport + }; + var completeRender = (function(error) { + page.destroy(); + snapshotCurrentPage(task, error); + }); + page.render(renderContext).then(function() { + completeRender(false); }, - textLayerBuilder - ); + function(error) { + completeRender('render : ' + error); + }); + }, + function(error) { + snapshotCurrentPage(task, 'render : ' + error); + }); } catch (e) { failure = 'page setup : ' + exceptionToString(e); + snapshotCurrentPage(task, failure); } } - - if (failure) { - // Skip right to snapshotting if there was a failure, since the - // fonts might be in an inconsistent state. - snapshotCurrentPage(task, failure); - } } function snapshotCurrentPage(task, failure) { diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 2a7c27875..d93dae609 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -31,4 +31,5 @@ !issue1002.pdf !issue925.pdf !gradientfill.pdf - +!basicapi.pdf +!mixedfonts.pdf diff --git a/test/pdfs/basicapi.pdf b/test/pdfs/basicapi.pdf new file mode 100644 index 000000000..31ffcfe9f Binary files /dev/null and b/test/pdfs/basicapi.pdf differ diff --git a/test/pdfs/mixedfonts.pdf b/test/pdfs/mixedfonts.pdf new file mode 100644 index 000000000..82bdbec34 Binary files /dev/null and b/test/pdfs/mixedfonts.pdf differ diff --git a/test/test.py b/test/test.py index 4dc6ca8a9..368069aff 100644 --- a/test/test.py +++ b/test/test.py @@ -16,6 +16,7 @@ BROWSERLOG_FILE = 'browser.log' REFDIR = 'ref' TMPDIR = 'tmp' VERBOSE = False +BROWSER_TIMEOUT = 60 SERVER_HOST = "localhost" @@ -74,7 +75,7 @@ class State: browsers = [ ] manifest = { } taskResults = { } - remaining = 0 + remaining = { } results = { } done = False numErrors = 0 @@ -83,6 +84,7 @@ class State: numFBFFailures = 0 numLoadFailures = 0 eqLog = None + lastPost = { } class Result: def __init__(self, snapshot, failure, page): @@ -180,6 +182,7 @@ class PDFTestHandler(BaseHTTPRequestHandler): result = json.loads(self.rfile.read(numBytes)) browser, id, failure, round, page, snapshot = result['browser'], result['id'], result['failure'], result['round'], result['page'], result['snapshot'] + State.lastPost[browser] = int(time.time()) taskResults = State.taskResults[browser][id] taskResults[round].append(Result(snapshot, failure, page)) @@ -199,9 +202,16 @@ class PDFTestHandler(BaseHTTPRequestHandler): self.server.masterMode) # Please oh please GC this ... del State.taskResults[browser][id] - State.remaining -= 1 + State.remaining[browser] -= 1 - State.done = (0 == State.remaining) + checkIfDone() + +def checkIfDone(): + State.done = True + for key in State.remaining: + if State.remaining[key] != 0: + State.done = False + return # Applescript hack to quit Chrome on Mac def tellAppToQuit(path, query): @@ -376,6 +386,8 @@ def setUp(options): for b in testBrowsers: State.taskResults[b.name] = { } + State.remaining[b.name] = len(manifestList) + State.lastPost[b.name] = int(time.time()) for item in manifestList: id, rounds = item['id'], int(item['rounds']) State.manifest[id] = item @@ -384,8 +396,6 @@ def setUp(options): taskResults.append([ ]) State.taskResults[b.name][id] = taskResults - State.remaining = len(testBrowsers) * len(manifestList) - return testBrowsers def startBrowsers(browsers, options): @@ -568,6 +578,12 @@ def runTests(options, browsers): try: startBrowsers(browsers, options) while not State.done: + for b in State.lastPost: + if State.remaining[b] > 0 and int(time.time()) - State.lastPost[b] > BROWSER_TIMEOUT: + print 'TEST-UNEXPECTED-FAIL | test failed', b, "has not responded in", BROWSER_TIMEOUT, "s" + State.numErrors += State.remaining[b] + State.remaining[b] = 0 + checkIfDone() time.sleep(1) processResults() finally: diff --git a/test/test_manifest.json b/test/test_manifest.json index 6a083bdf7..b6879e1ec 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -19,7 +19,7 @@ }, { "id": "intelisa-eq", "file": "pdfs/intelisa.pdf", - "md5": "83032ccbfdc5a66269b1403971110abe", + "md5": "c1444b7ccd935c0577d094297e1a6448", "link": true, "pageLimit": 100, "rounds": 1, @@ -29,12 +29,13 @@ "file": "pdfs/pdf.pdf", "md5": "dbdb23c939d2be09b43126c3c56060c7", "link": true, + "pageLimit": 500, "rounds": 1, "type": "load" }, { "id": "shavian-load", "file": "pdfs/shavian.pdf", - "md5": "4fabf0a03e82693007435020bc446f9b", + "md5": "79253352f48b55b7fa28a2586875d8b7", "link": true, "rounds": 1, "type": "load" @@ -241,10 +242,10 @@ "skipPages": [ 16 ], "type": "load" }, - { "id": "tcpdf_033", - "file": "pdfs/tcpdf_033.pdf", - "md5": "861294df58d185aae80919173f2732ff", - "link": true, + { "id": "mixedfonts", + "file": "pdfs/mixedfonts.pdf", + "md5": "a582b83fa1b3a25a6f13803a367c71ec", + "link": false, "rounds": 1, "type": "eq" }, diff --git a/test/test_slave.html b/test/test_slave.html index 50bb067e1..7c2097110 100644 --- a/test/test_slave.html +++ b/test/test_slave.html @@ -5,6 +5,7 @@ + diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js new file mode 100644 index 000000000..318dbb42a --- /dev/null +++ b/test/unit/api_spec.js @@ -0,0 +1,109 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +'use strict'; + +describe('api', function() { + // TODO run with worker enabled + PDFJS.disableWorker = true; + var basicApiUrl = '/basicapi.pdf'; + function waitsForPromise(promise) { + waitsFor(function() { + return promise.isResolved || promise.isRejected; + }, 250); + } + function expectAfterPromise(promise, successCallback) { + waitsForPromise(promise); + runs(function() { + promise.then(successCallback, + function(error, e) { + // Shouldn't get here. + expect(false).toEqual(true); + }); + }); + } + describe('PDFJS', function() { + describe('getDocument', function() { + it('creates pdf doc from URL', function() { + console.log('what is'); + debugger; + var promise = PDFJS.getDocument(basicApiUrl); + expectAfterPromise(promise, function(data) { + expect(true).toEqual(true); + }); + }); + /* + it('creates pdf doc from typed array', function() { + // TODO + }); + */ + }); + }); + describe('PDFDocument', function() { + var promise = PDFJS.getDocument(basicApiUrl); + waitsForPromise(promise); + var doc; + runs(function() { + promise.then(function(data) { doc = data; }); + }); + it('gets number of pages', function() { + expect(doc.numPages).toEqual(3); + }); + it('gets fingerprint', function() { + expect(typeof doc.fingerprint).toEqual('string'); + }); + it('gets page', function() { + var promise = doc.getPage(1); + expectAfterPromise(promise, function(data) { + expect(true).toEqual(true); + }); + }); + it('gets destinations', function() { + var promise = doc.getDestinations(); + expectAfterPromise(promise, function(data) { + // TODO this seems to be broken for the test pdf + }); + }); + it('gets outline', function() { + var promise = doc.getOutline(); + expectAfterPromise(promise, function(outline) { + // Two top level entries. + expect(outline.length).toEqual(2); + // Make sure some basic attributes are set. + expect(outline[1].title).toEqual('Chapter 1'); + expect(outline[1].items.length).toEqual(1); + expect(outline[1].items[0].title).toEqual('Paragraph 1.1'); + }); + }); + it('gets metadata', function() { + var promise = doc.getMetadata(); + expectAfterPromise(promise, function(metadata) { + expect(metadata.info['Title']).toEqual('Basic API Test'); + expect(metadata.metadata.get('dc:title')).toEqual('Basic API Test'); + }); + }); + }); + describe('Page', function() { + var promise = new Promise(); + PDFJS.getDocument(basicApiUrl).then(function(doc) { + doc.getPage(1).then(function(data) { + promise.resolve(data); + }); + }); + waitsForPromise(promise); + var page; + runs(function() { + promise.then(function(data) { + page = data; + }); + }); + it('gets ref', function() { + expect(page.ref).toEqual({num: 15, gen: 0}); + }); + // TODO rotate + // TODO viewport + // TODO annotaions + // TOOD text content + // TODO operation list + }); +}); diff --git a/test/unit/jsTestDriver.conf b/test/unit/jsTestDriver.conf index 9a26df6a4..b0f917b66 100644 --- a/test/unit/jsTestDriver.conf +++ b/test/unit/jsTestDriver.conf @@ -1,32 +1,39 @@ server: http://localhost:4224 -load: - - ../../external/jasmine/jasmine.js - - ../../external/jasmineAdapter/JasmineAdapter.js - - ../../src/obj.js - - ../../src/core.js - - ../../src/util.js - - ../../src/canvas.js - - ../../src/obj.js - - ../../src/function.js - - ../../src/charsets.js - - ../../src/cidmaps.js - - ../../src/colorspace.js - - ../../src/crypto.js - - ../../src/evaluator.js - - ../../src/fonts.js - - ../../src/glyphlist.js - - ../../src/image.js - - ../../src/metrics.js - - ../../src/parser.js - - ../../src/pattern.js - - ../../src/stream.js - - ../../src/worker.js - - ../../src/bidi.js - - ../../external/jpgjs/jpg.js - - ../unit/obj_spec.js - - ../unit/font_spec.js - - ../unit/function_spec.js - - ../unit/crypto_spec.js - - ../unit/stream_spec.js +basepath: .. +load: + - ../external/jasmine/jasmine.js + - ../external/jasmineAdapter/JasmineAdapter.js + - ../src/obj.js + - ../src/core.js + - ../src/util.js + - ../src/api.js + - ../src/canvas.js + - ../src/obj.js + - ../src/function.js + - ../src/charsets.js + - ../src/cidmaps.js + - ../src/colorspace.js + - ../src/crypto.js + - ../src/evaluator.js + - ../src/fonts.js + - ../src/glyphlist.js + - ../src/image.js + - ../src/metrics.js + - ../src/parser.js + - ../src/pattern.js + - ../src/stream.js + - ../src/worker.js + - ../src/bidi.js + - ../src/metadata.js + - ../external/jpgjs/jpg.js + - unit/obj_spec.js + - unit/font_spec.js + - unit/function_spec.js + - unit/crypto_spec.js + - unit/stream_spec.js + - unit/api_spec.js + +gateway: + - {matcher: "*.pdf", server: "http://localhost:8888/test/pdfs/"} diff --git a/web/compatibility.js b/web/compatibility.js index 9f139b7dd..5c192c9a9 100644 --- a/web/compatibility.js +++ b/web/compatibility.js @@ -234,3 +234,21 @@ console = {log: function() {}}; } })(); + +// Check onclick compatibility in Opera +(function checkOnClickCompatibility() { + // workaround for reported Opera bug DSK-354448: + // onclick fires on disabled buttons with opaque content + function ignoreIfTargetDisabled(event) { + if (isDisabled(event.target)) { + event.stopPropagation(); + } + } + function isDisabled(node) { + return node.disabled || (node.parentNode && isDisabled(node.parentNode)); + } + if (navigator.userAgent.indexOf('Opera') != -1) { + // use browser detection since we cannot feature-check this bug + document.addEventListener('click', ignoreIfTargetDisabled, true); + } +})(); diff --git a/web/debugger.js b/web/debugger.js index 00f5f6fd4..610a63854 100644 --- a/web/debugger.js +++ b/web/debugger.js @@ -163,29 +163,29 @@ var StepperManager = (function StepperManagerClosure() { enabled: false, active: false, // Stepper specific functions. - create: function create(pageNumber) { + create: function create(pageIndex) { var debug = document.createElement('div'); - debug.id = 'stepper' + pageNumber; + debug.id = 'stepper' + pageIndex; debug.setAttribute('hidden', true); debug.className = 'stepper'; stepperDiv.appendChild(debug); var b = document.createElement('option'); - b.textContent = 'Page ' + (pageNumber + 1); - b.value = pageNumber; + b.textContent = 'Page ' + (pageIndex + 1); + b.value = pageIndex; stepperChooser.appendChild(b); - var initBreakPoints = breakPoints[pageNumber] || []; - var stepper = new Stepper(debug, pageNumber, initBreakPoints); + var initBreakPoints = breakPoints[pageIndex] || []; + var stepper = new Stepper(debug, pageIndex, initBreakPoints); steppers.push(stepper); if (steppers.length === 1) - this.selectStepper(pageNumber, false); + this.selectStepper(pageIndex, false); return stepper; }, - selectStepper: function selectStepper(pageNumber, selectPanel) { + selectStepper: function selectStepper(pageIndex, selectPanel) { if (selectPanel) this.manager.selectPanel(1); for (var i = 0; i < steppers.length; ++i) { var stepper = steppers[i]; - if (stepper.pageNumber == pageNumber) + if (stepper.pageIndex == pageIndex) stepper.panel.removeAttribute('hidden'); else stepper.panel.setAttribute('hidden', true); @@ -193,11 +193,11 @@ var StepperManager = (function StepperManagerClosure() { var options = stepperChooser.options; for (var i = 0; i < options.length; ++i) { var option = options[i]; - option.selected = option.value == pageNumber; + option.selected = option.value == pageIndex; } }, - saveBreakPoints: function saveBreakPoints(pageNumber, bps) { - breakPoints[pageNumber] = bps; + saveBreakPoints: function saveBreakPoints(pageIndex, bps) { + breakPoints[pageIndex] = bps; sessionStorage.setItem('pdfjsBreakPoints', JSON.stringify(breakPoints)); } }; @@ -205,12 +205,12 @@ var StepperManager = (function StepperManagerClosure() { // The stepper for each page's IRQueue. var Stepper = (function StepperClosure() { - function Stepper(panel, pageNumber, initialBreakPoints) { + function Stepper(panel, pageIndex, initialBreakPoints) { this.panel = panel; this.len; this.breakPoint = 0; this.nextBreakPoint = null; - this.pageNumber = pageNumber; + this.pageIndex = pageIndex; this.breakPoints = initialBreakPoints; this.currentIdx = -1; } @@ -256,7 +256,7 @@ var Stepper = (function StepperClosure() { self.breakPoints.push(x); else self.breakPoints.splice(self.breakPoints.indexOf(x), 1); - StepperManager.saveBreakPoints(self.pageNumber, self.breakPoints); + StepperManager.saveBreakPoints(self.pageIndex, self.breakPoints); } })(i); @@ -278,7 +278,7 @@ var Stepper = (function StepperClosure() { return null; }, breakIt: function breakIt(idx, callback) { - StepperManager.selectStepper(this.pageNumber, true); + StepperManager.selectStepper(this.pageIndex, true); var self = this; var dom = document; self.currentIdx = idx; diff --git a/web/images/text.svg b/web/images/text.svg new file mode 100644 index 000000000..25df8f466 --- /dev/null +++ b/web/images/text.svg @@ -0,0 +1,46 @@ + + diff --git a/web/viewer.html b/web/viewer.html index ff9c01ae2..4fd0bded4 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -1,6 +1,6 @@
-