author | Ryan VanderMeulen <ryanvm@gmail.com> |
Mon, 10 Jun 2013 20:11:41 -0400 | |
changeset 134594 | b7637656cc54446ee646a43df732281ea91d239f |
parent 134593 | 63386b71d1b50cc11dd82f1966153e954c9af169 |
child 134595 | 172b542f51bae22359cb5c4444f5ceedf0f66590 |
push id | 24805 |
push user | emorley@mozilla.com |
push date | Tue, 11 Jun 2013 08:32:39 +0000 |
treeherder | mozilla-central@81b227f1a522 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | bdahl |
bugs | 878897 |
milestone | 24.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/extensions/pdfjs/README.mozilla +++ b/browser/extensions/pdfjs/README.mozilla @@ -1,4 +1,4 @@ This is the pdf.js project output, https://github.com/mozilla/pdf.js -Current extension version is: 0.8.169 +Current extension version is: 0.8.229
--- a/browser/extensions/pdfjs/components/PdfStreamConverter.js +++ b/browser/extensions/pdfjs/components/PdfStreamConverter.js @@ -193,38 +193,17 @@ PdfDataListener.prototype = { // All the priviledged actions. function ChromeActions(domWindow, contentDispositionFilename) { this.domWindow = domWindow; this.contentDispositionFilename = contentDispositionFilename; } ChromeActions.prototype = { isInPrivateBrowsing: function() { - var docIsPrivate, privateBrowsing; - try { - docIsPrivate = PrivateBrowsingUtils.isWindowPrivate(this.domWindow); - } catch (x) { - // unable to use PrivateBrowsingUtils, e.g. FF15 - } - if (typeof docIsPrivate === 'undefined') { - // per-window Private Browsing is not supported, trying global service - try { - privateBrowsing = Cc['@mozilla.org/privatebrowsing;1'] - .getService(Ci.nsIPrivateBrowsingService); - docIsPrivate = privateBrowsing.privateBrowsingEnabled; - } catch (x) { - // unable to get nsIPrivateBrowsingService (e.g. not Firefox) - docIsPrivate = false; - } - } - // caching the result - this.isInPrivateBrowsing = function isInPrivateBrowsingCached() { - return docIsPrivate; - }; - return docIsPrivate; + return PrivateBrowsingUtils.isWindowPrivate(this.domWindow); }, download: function(data, sendResponse) { var self = this; var originalUrl = data.originalUrl; // The data may not be downloaded so we need just retry getting the pdf with // the original url. var originalUri = NetUtil.newURI(data.originalUrl); var blobUri = data.blobUrl ? NetUtil.newURI(data.blobUrl) : originalUri; @@ -407,17 +386,17 @@ var RangedChromeActions = (function Rang /** * This is for range requests */ function RangedChromeActions( domWindow, contentDispositionFilename, originalRequest) { ChromeActions.call(this, domWindow, contentDispositionFilename); - this.pdfUrl = originalRequest.URI.resolve(''); + this.pdfUrl = originalRequest.URI.spec; this.contentLength = originalRequest.contentLength; // Pass all the headers from the original request through var httpHeaderVisitor = { headers: {}, visitHeader: function(aHeader, aValue) { if (aHeader === 'Range') { // When loading the PDF from cache, firefox seems to set the Range
--- a/browser/extensions/pdfjs/content/build/pdf.js +++ b/browser/extensions/pdfjs/content/build/pdf.js @@ -11,18 +11,18 @@ * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var PDFJS = {}; -PDFJS.version = '0.8.169'; -PDFJS.build = '869b878'; +PDFJS.version = '0.8.229'; +PDFJS.build = 'b996e1b'; (function pdfjsWrapper() { // Use strict in our context only - users might not want it 'use strict'; /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* Copyright 2012 Mozilla Foundation @@ -40,25 +40,26 @@ PDFJS.build = '869b878'; * limitations under the License. */ /* globals assert, MissingDataException, isInt, NetworkManager, Promise, isEmptyObj */ 'use strict'; var ChunkedStream = (function ChunkedStreamClosure() { - function ChunkedStream(length, chunkSize) { + function ChunkedStream(length, chunkSize, manager) { this.bytes = new Uint8Array(length); this.start = 0; this.pos = 0; this.end = length; this.chunkSize = chunkSize; this.loadedChunks = []; this.numChunksLoaded = 0; this.numChunks = Math.ceil(length / chunkSize); + this.manager = manager; } // required methods for a stream. if a particular stream does not // implement these, an error should be thrown ChunkedStream.prototype = { getMissingChunks: function ChunkedStream_getMissingChunks() { var chunks = []; @@ -198,16 +199,28 @@ var ChunkedStream = (function ChunkedStr moveStart: function ChunkedStream_moveStart() { this.start = this.pos; }, makeSubStream: function ChunkedStream_makeSubStream(start, length, dict) { function ChunkedStreamSubstream() {} ChunkedStreamSubstream.prototype = Object.create(this); + ChunkedStreamSubstream.prototype.getMissingChunks = function() { + var chunkSize = this.chunkSize; + var beginChunk = Math.floor(this.start / chunkSize); + var endChunk = Math.floor((this.end - 1) / chunkSize) + 1; + var missingChunks = []; + for (var chunk = beginChunk; chunk < endChunk; ++chunk) { + if (!(chunk in this.loadedChunks)) { + missingChunks.push(chunk); + } + } + return missingChunks; + }; var subStream = new ChunkedStreamSubstream(); subStream.pos = subStream.start = start; subStream.end = start + length || this.end; subStream.dict = dict; return subStream; }, isStream: true @@ -215,17 +228,17 @@ var ChunkedStream = (function ChunkedStr return ChunkedStream; })(); var ChunkedStreamManager = (function ChunkedStreamManagerClosure() { function ChunkedStreamManager(length, chunkSize, url, args) { var self = this; - this.stream = new ChunkedStream(length, chunkSize); + this.stream = new ChunkedStream(length, chunkSize, this); this.length = length; this.chunkSize = chunkSize; this.url = url; this.disableAutoFetch = args.disableAutoFetch; var msgHandler = this.msgHandler = args.msgHandler; if (args.chunkedViewerLoading) { msgHandler.on('OnDataRange', this.onReceiveData.bind(this)); @@ -264,60 +277,36 @@ var ChunkedStreamManager = (function Chu onLoadedStream: function ChunkedStreamManager_getLoadedStream() { return this.loadedStream; }, // Get all the chunks that are not yet loaded and groups them into // contiguous ranges to load in as few requests as possible requestAllChunks: function ChunkedStreamManager_requestAllChunks() { var missingChunks = this.stream.getMissingChunks(); - var chunksToRequest = []; - for (var i = 0, n = missingChunks.length; i < n; ++i) { - var chunk = missingChunks[i]; - if (!(chunk in this.requestsByChunk)) { - this.requestsByChunk[chunk] = []; - chunksToRequest.push(chunk); - } - } - var groupedChunks = this.groupChunks(chunksToRequest); - for (var i = 0, n = groupedChunks.length; i < n; ++i) { - var groupedChunk = groupedChunks[i]; - var begin = groupedChunk.beginChunk * this.chunkSize; - var end = Math.min(groupedChunk.endChunk * this.chunkSize, this.length); - this.sendRequest(begin, end); - } - + this.requestChunks(missingChunks); return this.loadedStream; }, - getStream: function ChunkedStreamManager_getStream() { - return this.stream; - }, - - // Loads any chunks in the requested range that are not yet loaded - requestRange: function ChunkedStreamManager_requestRange( - begin, end, callback) { - - end = Math.min(end, this.length); - - var beginChunk = this.getBeginChunk(begin); - var endChunk = this.getEndChunk(end); - + requestChunks: function ChunkedStreamManager_requestChunks(chunks, + callback) { var requestId = this.currRequestId++; var chunksNeeded; this.chunksNeededByRequest[requestId] = chunksNeeded = {}; - for (var chunk = beginChunk; chunk < endChunk; ++chunk) { - if (!this.stream.hasChunk(chunk)) { - chunksNeeded[chunk] = true; + for (var i = 0, ii = chunks.length; i < ii; i++) { + if (!this.stream.hasChunk(chunks[i])) { + chunksNeeded[chunks[i]] = true; } } if (isEmptyObj(chunksNeeded)) { - callback(); + if (callback) { + callback(); + } return; } this.callbacksByRequest[requestId] = callback; var chunksToRequest = []; for (var chunk in chunksNeeded) { chunk = chunk | 0; @@ -337,16 +326,56 @@ var ChunkedStreamManager = (function Chu for (var i = 0; i < groupedChunksToRequest.length; ++i) { var groupedChunk = groupedChunksToRequest[i]; var begin = groupedChunk.beginChunk * this.chunkSize; var end = Math.min(groupedChunk.endChunk * this.chunkSize, this.length); this.sendRequest(begin, end); } }, + getStream: function ChunkedStreamManager_getStream() { + return this.stream; + }, + + // Loads any chunks in the requested range that are not yet loaded + requestRange: function ChunkedStreamManager_requestRange( + begin, end, callback) { + + end = Math.min(end, this.length); + + var beginChunk = this.getBeginChunk(begin); + var endChunk = this.getEndChunk(end); + + var chunks = []; + for (var chunk = beginChunk; chunk < endChunk; ++chunk) { + chunks.push(chunk); + } + + this.requestChunks(chunks, callback); + }, + + requestRanges: function ChunkedStreamManager_requestRanges(ranges, + callback) { + ranges = ranges || []; + var chunksToRequest = []; + + for (var i = 0; i < ranges.length; i++) { + var beginChunk = this.getBeginChunk(ranges[i].begin); + var endChunk = this.getEndChunk(ranges[i].end); + for (var chunk = beginChunk; chunk < endChunk; ++chunk) { + if (chunksToRequest.indexOf(chunk) < 0) { + chunksToRequest.push(chunk); + } + } + } + + chunksToRequest.sort(function(a, b) { return a - b; }); + this.requestChunks(chunksToRequest, callback); + }, + // Groups a sorted array of chunks into as few continguous larger // chunks as possible groupChunks: function ChunkedStreamManager_groupChunks(chunks) { var groupedChunks = []; var beginChunk; var prevChunk; for (var i = 0; i < chunks.length; ++i) { var chunk = chunks[i]; @@ -425,27 +454,27 @@ var ChunkedStreamManager = (function Chu var lastChunk = this.stream.numChunks - 1; if (!this.stream.hasChunk(lastChunk)) { nextEmptyChunk = lastChunk; } } else { nextEmptyChunk = this.stream.nextEmptyChunk(endChunk); } if (isInt(nextEmptyChunk)) { - var nextEmptyByte = nextEmptyChunk * this.chunkSize; - this.requestRange(nextEmptyByte, nextEmptyByte + this.chunkSize, - function() {}); + this.requestChunks([nextEmptyChunk]); } } for (var i = 0; i < loadedRequests.length; ++i) { var requestId = loadedRequests[i]; var callback = this.callbacksByRequest[requestId]; delete this.callbacksByRequest[requestId]; - callback(); + if (callback) { + callback(); + } } this.msgHandler.send('DocProgress', { loaded: this.stream.numChunksLoaded * this.chunkSize, total: this.length }); }, @@ -676,16 +705,17 @@ var Page = (function PageClosure() { this.pageIndex = pageIndex; this.pageDict = pageDict; this.xref = xref; this.ref = ref; this.idCounters = { font: 0, obj: 0 }; + this.resourcesPromise = null; } Page.prototype = { getPageProp: function Page_getPageProp(key) { return this.pageDict.get(key); }, inheritPageProp: function Page_inheritPageProp(key) { var dict = this.pageDict; @@ -758,84 +788,93 @@ var Page = (function PageClosure() { } else if (isStream(content)) { stream = content; } else { // replacing non-existent page content with empty one stream = new NullStream(); } return stream; }, + loadResources: function(keys) { + if (!this.resourcesPromise) { + // TODO: add async inheritPageProp and remove this. + this.resourcesPromise = this.pdfManager.ensure(this, 'resources'); + } + var promise = new Promise(); + this.resourcesPromise.then(function resourceSuccess() { + var objectLoader = new ObjectLoader(this.resources.map, + keys, + this.xref); + objectLoader.load().then(function objectLoaderSuccess() { + promise.resolve(); + }); + }.bind(this)); + return promise; + }, getOperatorList: function Page_getOperatorList(handler) { var self = this; var promise = new Promise(); function reject(e) { promise.reject(e); } var pageListPromise = new Promise(); var pdfManager = this.pdfManager; var contentStreamPromise = pdfManager.ensure(this, 'getContentStream', []); - var resourcesPromise = pdfManager.ensure(this, 'resources'); + var resourcesPromise = this.loadResources([ + 'ExtGState', + 'ColorSpace', + 'Pattern', + 'Shading', + 'XObject', + 'Font', + // ProcSet + // Properties + ]); var partialEvaluator = new PartialEvaluator( pdfManager, this.xref, handler, this.pageIndex, 'p' + this.pageIndex + '_', this.idCounters); var dataPromises = Promise.all( [contentStreamPromise, resourcesPromise], reject); dataPromises.then(function(data) { var contentStream = data[0]; - var resources = data[1]; - - pdfManager.ensure(partialEvaluator, 'getOperatorList', - [contentStream, resources]).then( - function(opListPromise) { - opListPromise.then(function(data) { - pageListPromise.resolve(data); - }); + + partialEvaluator.getOperatorList(contentStream, self.resources).then( + function(data) { + pageListPromise.resolve(data); }, reject ); }); var annotationsPromise = pdfManager.ensure(this, 'annotations'); Promise.all([pageListPromise, annotationsPromise]).then(function(datas) { var pageData = datas[0]; var pageQueue = pageData.queue; var annotations = datas[1]; - var ensurePromises = []; - for (var i = 0, n = annotations.length; i < n; ++i) { - var ensurePromise = pdfManager.ensure(annotations[i], - 'getOperatorList', - [partialEvaluator]); - ensurePromises.push(ensurePromise); - } - - Promise.all(ensurePromises).then(function(listPromises) { - Promise.all(listPromises).then(function(datas) { - for (var i = 0, n = datas.length; i < n; ++i) { - var annotationData = datas[i]; - var annotationQueue = annotationData.queue; - Util.concatenateToArray(pageQueue.fnArray, - annotationQueue.fnArray); - Util.concatenateToArray(pageQueue.argsArray, - annotationQueue.argsArray); - Util.extendObj(pageData.dependencies, - annotationData.dependencies); - } - - PartialEvaluator.optimizeQueue(pageQueue); - - promise.resolve(pageData); - }, reject); + if (annotations.length === 0) { + PartialEvaluator.optimizeQueue(pageQueue); + promise.resolve(pageData); + return; + } + + var dependencies = pageData.dependencies; + var annotationsReadyPromise = Annotation.appendToOperatorList( + annotations, pageQueue, pdfManager, dependencies, partialEvaluator); + annotationsReadyPromise.then(function () { + PartialEvaluator.optimizeQueue(pageQueue); + + promise.resolve(pageData); }, reject); }, reject); return promise; }, extractTextContent: function Page_extractTextContent() { var handler = { on: function nullHandlerOn() {}, @@ -844,37 +883,34 @@ var Page = (function PageClosure() { var self = this; var textContentPromise = new Promise(); var pdfManager = this.pdfManager; var contentStreamPromise = pdfManager.ensure(this, 'getContentStream', []); - var resourcesPromise = new Promise(); - pdfManager.ensure(this, 'resources').then(function(resources) { - pdfManager.ensure(self.xref, 'fetchIfRef', [resources]).then( - function(resources) { - resourcesPromise.resolve(resources); - } - ); - }); + + var resourcesPromise = this.loadResources([ + 'ExtGState', + 'XObject', + 'Font' + ]); var dataPromises = Promise.all([contentStreamPromise, resourcesPromise]); dataPromises.then(function(data) { var contentStream = data[0]; - var resources = data[1]; var partialEvaluator = new PartialEvaluator( pdfManager, self.xref, handler, self.pageIndex, 'p' + self.pageIndex + '_', self.idCounters); partialEvaluator.getTextContent( - contentStream, resources).then(function(bidiTexts) { + contentStream, self.resources).then(function(bidiTexts) { textContentPromise.resolve({ bidiTexts: bidiTexts }); }); }); return textContentPromise; }, @@ -921,17 +957,17 @@ var PDFDocument = (function PDFDocumentC else error('PDFDocument: Unknown argument type'); } function init(pdfManager, stream, password) { assertWellFormed(stream.length > 0, 'stream must have data'); this.pdfManager = pdfManager; this.stream = stream; - var xref = new XRef(this.stream, password); + var xref = new XRef(this.stream, password, pdfManager); this.xref = xref; } function find(stream, needle, limit, backwards) { var pos = stream.pos; var end = stream.end; var str = ''; if (pos + limit > end) @@ -979,18 +1015,18 @@ var PDFDocument = (function PDFDocumentC if (linearization.length != length) { linearization = false; } } catch (err) { if (err instanceof MissingDataException) { throw err; } - warn('The linearization data is not available ' + - 'or unreadable pdf data is found'); + info('The linearization data is not available ' + + 'or unreadable PDF data is found'); linearization = false; } } // shadow the prototype getter with a data property return shadow(this, 'linearization', linearization); }, get startXRef() { var stream = this.stream; @@ -2807,22 +2843,25 @@ var TextRenderingMode = { FILL: 0, STROKE: 1, FILL_STROKE: 2, INVISIBLE: 3, FILL_ADD_TO_PATH: 4, STROKE_ADD_TO_PATH: 5, FILL_STROKE_ADD_TO_PATH: 6, ADD_TO_PATH: 7, + FILL_STROKE_MASK: 3, ADD_TO_PATH_FLAG: 4 }; // Minimal font size that would be used during canvas fillText operations. var MIN_FONT_SIZE = 16; +var COMPILE_TYPE3_GLYPHS = true; + function createScratchCanvas(width, height) { var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; return canvas; } function addContextCurrentTransform(ctx) { @@ -2939,16 +2978,190 @@ function addContextCurrentTransform(ctx) m[5] ]; this._originalRotate(angle); }; } } +var CachedCanvases = (function CachedCanvasesClosure() { + var cache = {}; + return { + getCanvas: function CachedCanvases_getCanvas(id, width, height) { + var canvas; + if (id in cache) { + canvas = cache[id]; + canvas.width = width; + canvas.height = height; + // reset canvas transform for emulated mozCurrentTransform, if needed + canvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0); + } else { + canvas = createScratchCanvas(width, height); + cache[id] = canvas; + } + return canvas; + }, + clear: function () { + cache = {}; + } + }; +})(); + +function compileType3Glyph(imgData) { + var POINT_TO_PROCESS_LIMIT = 1000; + + var width = imgData.width, height = imgData.height; + var i, j; + // we need sparse arrays + var points = []; + for (i = 0; i <= height; i++) { + points.push([]); + } + + // finding iteresting points: every point is located between mask pixels, + // so there will be points of the (width + 1)x(height + 1) grid. Every point + // will have flags assigned based on neighboring mask pixels: + // 4 | 8 + // --P-- + // 2 | 1 + // We are interested only in points with the flags: + // - outside corners: 1, 2, 4, 8; + // - inside corners: 7, 11, 13, 14; + // - and, intersections: 5, 10. + var pos = 3, data = imgData.data, lineSize = width * 4, count = 0; + if (data[3] !== 0) { + points[0][0] = 1; + ++count; + } + for (j = 1; j < width; j++) { + if (data[pos] !== data[pos + 4]) { + points[0][j] = data[pos] ? 2 : 1; + ++count; + } + pos += 4; + } + if (data[pos] !== 0) { + points[0][j] = 2; + ++count; + } + pos += 4; + for (i = 1; i < height; i++) { + if (data[pos - lineSize] !== data[pos]) { + points[i][0] = data[pos] ? 1 : 8; + ++count; + } + for (j = 1; j < width; j++) { + var f1 = data[pos + 4] ? 1 : 0; + var f2 = data[pos] ? 1 : 0; + var f4 = data[pos - lineSize] ? 1 : 0; + var f8 = data[pos - lineSize + 4] ? 1 : 0; + var fSum = f1 + f2 + f4 + f8; + if (fSum === 1 || fSum === 3 || (fSum === 2 && f1 === f4)) { + points[i][j] = f1 | (f2 << 1) | (f4 << 2) | (f8 << 3); + ++count; + } + pos += 4; + } + if (data[pos - lineSize] !== data[pos]) { + points[i][j] = data[pos] ? 2 : 4; + ++count; + } + pos += 4; + + if (count > POINT_TO_PROCESS_LIMIT) { + return null; + } + } + pos -= lineSize; + if (data[pos] !== 0) { + points[i][0] = 8; + ++count; + } + for (j = 1; j < width; j++) { + if (data[pos] !== data[pos + 4]) { + points[i][j] = data[pos] ? 4 : 8; + ++count; + } + pos += 4; + } + if (data[pos] !== 0) { + points[i][j] = 4; + ++count; + } + if (count > POINT_TO_PROCESS_LIMIT) { + return null; + } + + // building outlines + var outline = []; + outline.push('c.save();'); + // the path shall be painted in [0..1]x[0..1] space + outline.push('c.scale(' + (1 / width) + ',' + (-1 / height) + ');'); + outline.push('c.translate(0,-' + height + ');'); + outline.push('c.beginPath();'); + for (i = 0; i <= height; i++) { + if (points[i].length === 0) { + continue; + } + var js = null; + for (js in points[i]) { + break; + } + if (js === null) { + continue; + } + var i0 = i, j0 = (j = +js); + + outline.push('c.moveTo(' + j + ',' + i + ');'); + var type = points[i][j], d = 0; + do { + if (type === 5 || type === 10) { + // line crossed: following dirrection we followed + points[i0][j0] = type | (15 ^ d); // changing direction for "future hit" + type |= d; + } + + switch (type) { + case 1: + case 13: + do { i0++; } while (!points[i0][j0]); + d = 9; + break; + case 4: + case 7: + do { i0--; } while (!points[i0][j0]); + d = 6; + break; + case 8: + case 14: + do { j0++; } while (!points[i0][j0]); + d = 12; + break; + case 2: + case 11: + do { j0--; } while (!points[i0][j0]); + d = 3; + break; + } + outline.push('c.lineTo(' + j0 + ',' + i0 + ');'); + + type = points[i0][j0]; + delete points[i0][j0]; + } while (j0 !== j || i0 !== i); + --i; + } + outline.push('c.fill();'); + outline.push('c.beginPath();'); + outline.push('c.restore();'); + + /*jshint -W054 */ + return new Function('c', outline.join('\n')); +} + var CanvasExtraState = (function CanvasExtraStateClosure() { function CanvasExtraState(old) { // Are soft masks and alpha values shapes or opacities? this.alphaIsShape = false; this.fontSize = 0; this.fontSizeScale = 1; this.textMatrix = IDENTITY_MATRIX; this.fontMatrix = FONT_IDENTITY_MATRIX; @@ -3009,148 +3222,43 @@ var CanvasGraphics = (function CanvasGra this.pendingEOFill = false; this.res = null; this.xobjs = null; this.commonObjs = commonObjs; this.objs = objs; this.textLayer = textLayer; this.imageLayer = imageLayer; this.groupStack = []; + this.processingType3 = null; if (canvasCtx) { addContextCurrentTransform(canvasCtx); } } - function applyStencilMask(imgArray, width, height, inverseDecode, buffer) { - var imgArrayPos = 0; - var i, j, mask, buf; - // removing making non-masked pixels transparent - var bufferPos = 3; // alpha component offset - for (i = 0; i < height; i++) { - mask = 0; - for (j = 0; j < width; j++) { - if (!mask) { - buf = imgArray[imgArrayPos++]; - mask = 128; - } - if (!(buf & mask) === inverseDecode) { - buffer[bufferPos] = 0; - } - bufferPos += 4; - mask >>= 1; - } - } - } - - function putBinaryImageData(ctx, data, w, h) { - var tmpImgData = 'createImageData' in ctx ? ctx.createImageData(w, h) : - ctx.getImageData(0, 0, w, h); - + function putBinaryImageData(ctx, imgData) { + if (typeof ImageData !== 'undefined' && imgData instanceof ImageData) { + ctx.putImageData(imgData, 0, 0); + return; + } + + var tmpImgData = ctx.createImageData(imgData.width, imgData.height); + + var data = imgData.data; var tmpImgDataPixels = tmpImgData.data; if ('set' in tmpImgDataPixels) tmpImgDataPixels.set(data); else { // Copy over the imageData pixel by pixel. for (var i = 0, ii = tmpImgDataPixels.length; i < ii; i++) tmpImgDataPixels[i] = data[i]; } ctx.putImageData(tmpImgData, 0, 0); } - function prescaleImage(pixels, width, height, widthScale, heightScale) { - pixels = new Uint8Array(pixels); // creating a copy - while (widthScale > 2 || heightScale > 2) { - if (heightScale > 2) { - // scaling image twice vertically - var rowSize = width * 4; - var k = 0, l = 0; - for (var i = 0; i < height - 1; i += 2) { - for (var j = 0; j < width; j++) { - var alpha1 = pixels[k + 3], alpha2 = pixels[k + 3 + rowSize]; - if (alpha1 === alpha2) { - pixels[l] = (pixels[k] + pixels[k + rowSize]) >> 1; - pixels[l + 1] = (pixels[k + 1] + pixels[k + 1 + rowSize]) >> 1; - pixels[l + 2] = (pixels[k + 2] + pixels[k + 2 + rowSize]) >> 1; - pixels[l + 3] = alpha1; - } else if (alpha1 < alpha2) { - var d = 256 - alpha2 + alpha1; - pixels[l] = (pixels[k] * d + (pixels[k + rowSize] << 8)) >> 9; - pixels[l + 1] = (pixels[k + 1] * d + - (pixels[k + 1 + rowSize] << 8)) >> 9; - pixels[l + 2] = (pixels[k + 2] * d + - (pixels[k + 2 + rowSize] << 8)) >> 9; - pixels[l + 3] = alpha2; - } else { - var d = 256 - alpha1 + alpha2; - pixels[l] = ((pixels[k] << 8) + pixels[k + rowSize] * d) >> 9; - pixels[l + 1] = ((pixels[k + 1] << 8) + - pixels[k + 1 + rowSize] * d) >> 9; - pixels[l + 2] = ((pixels[k + 2] << 8) + - pixels[k + 2 + rowSize] * d) >> 9; - pixels[l + 3] = alpha1; - } - k += 4; l += 4; - } - k += rowSize; - } - if (height & 1) { - for (var i = 0; i < rowSize; i++) { - pixels[l++] = pixels[k++]; - } - } - height = (height + 1) >> 1; - heightScale /= 2; - } - if (widthScale > 2) { - // scaling image twice horizontally - var k = 0, l = 0; - for (var i = 0; i < height; i++) { - for (var j = 0; j < width - 1; j += 2) { - var alpha1 = pixels[k + 3], alpha2 = pixels[k + 7]; - if (alpha1 === alpha2) { - pixels[l] = (pixels[k] + pixels[k + 4]) >> 1; - pixels[l + 1] = (pixels[k + 1] + pixels[k + 5]) >> 1; - pixels[l + 2] = (pixels[k + 2] + pixels[k + 6]) >> 1; - pixels[l + 3] = alpha1; - } else if (alpha1 < alpha2) { - var d = 256 - alpha2 + alpha1; - pixels[l] = (pixels[k] * d + (pixels[k + 4] << 8)) >> 9; - pixels[l + 1] = (pixels[k + 1] * d + (pixels[k + 5] << 8)) >> 9; - pixels[l + 2] = (pixels[k + 2] * d + (pixels[k + 6] << 8)) >> 9; - pixels[l + 3] = alpha2; - } else { - var d = 256 - alpha1 + alpha2; - pixels[l] = ((pixels[k] << 8) + pixels[k + 4] * d) >> 9; - pixels[l + 1] = ((pixels[k + 1] << 8) + pixels[k + 5] * d) >> 9; - pixels[l + 2] = ((pixels[k + 2] << 8) + pixels[k + 6] * d) >> 9; - pixels[l + 3] = alpha1; - } - k += 8; l += 4; - } - if (width & 1) { - pixels[l++] = pixels[k++]; - pixels[l++] = pixels[k++]; - pixels[l++] = pixels[k++]; - pixels[l++] = pixels[k++]; - } - } - width = (width + 1) >> 1; - widthScale /= 2; - } - } - - var tmpCanvas = createScratchCanvas(width, height); - var tmpCtx = tmpCanvas.getContext('2d'); - putBinaryImageData(tmpCtx, pixels.subarray(0, width * height * 4), - width, height); - - return tmpCanvas; - } - function copyCtxState(sourceCtx, destCtx) { var properties = ['strokeStyle', 'fillStyle', 'fillRule', 'globalAlpha', 'lineWidth', 'lineCap', 'lineJoin', 'miterLimit', 'globalCompositeOperation', 'font']; for (var i = 0, ii = properties.length; i < ii; i++) { var property = properties[i]; if (property in sourceCtx) { destCtx[property] = sourceCtx[property]; @@ -3301,16 +3409,17 @@ var CanvasGraphics = (function CanvasGra // If the operatorList isn't executed completely yet OR the execution // time was short enough, do another execution round. } }, endDrawing: function CanvasGraphics_endDrawing() { this.ctx.restore(); + CachedCanvases.clear(); if (this.textLayer) { this.textLayer.endLayout(); } if (this.imageLayer) { this.imageLayer.endLayout(); } }, @@ -3407,20 +3516,16 @@ var CanvasGraphics = (function CanvasGra }, save: function CanvasGraphics_save() { this.ctx.save(); var old = this.current; this.stateStack.push(old); this.current = old.clone(); }, restore: function CanvasGraphics_restore() { - if ('textClipLayers' in this) { - this.completeTextClipping(); - } - var prev = this.stateStack.pop(); if (prev) { this.current = prev; this.ctx.restore(); } }, transform: function CanvasGraphics_transform(a, b, c, d, e, f) { this.ctx.transform(a, b, c, d, e, f); @@ -3558,74 +3663,35 @@ var CanvasGraphics = (function CanvasGra // Text beginText: function CanvasGraphics_beginText() { this.current.textMatrix = IDENTITY_MATRIX; this.current.x = this.current.lineX = 0; this.current.y = this.current.lineY = 0; }, endText: function CanvasGraphics_endText() { - if ('textClipLayers' in this) { - this.swapImageForTextClipping(); - } - }, - getCurrentTextClipping: function CanvasGraphics_getCurrentTextClipping() { + if (!('pendingTextPaths' in this)) { + this.ctx.beginPath(); + return; + } + var paths = this.pendingTextPaths; var ctx = this.ctx; - var transform = ctx.mozCurrentTransform; - if ('textClipLayers' in this) { - // we need to reset only font and transform - var maskCtx = this.textClipLayers.maskCtx; - maskCtx.setTransform.apply(maskCtx, transform); - maskCtx.font = ctx.font; - return maskCtx; - } - - var canvasWidth = ctx.canvas.width; - var canvasHeight = ctx.canvas.height; - // keeping track of the text clipping of the separate canvas - var maskCanvas = createScratchCanvas(canvasWidth, canvasHeight); - var maskCtx = maskCanvas.getContext('2d'); - maskCtx.setTransform.apply(maskCtx, transform); - maskCtx.font = ctx.font; - var textClipLayers = { - maskCanvas: maskCanvas, - maskCtx: maskCtx - }; - this.textClipLayers = textClipLayers; - return maskCtx; - }, - swapImageForTextClipping: - function CanvasGraphics_swapImageForTextClipping() { - var ctx = this.ctx; - var canvasWidth = ctx.canvas.width; - var canvasHeight = ctx.canvas.height; - // saving current image content and clearing whole canvas + ctx.save(); - ctx.setTransform(1, 0, 0, 1, 0, 0); - var data = ctx.getImageData(0, 0, canvasWidth, canvasHeight); - this.textClipLayers.imageData = data; - ctx.clearRect(0, 0, canvasWidth, canvasHeight); + ctx.beginPath(); + for (var i = 0; i < paths.length; i++) { + var path = paths[i]; + ctx.setTransform.apply(ctx, path.transform); + ctx.translate(path.x, path.y); + path.addToPath(ctx, path.fontSize); + } ctx.restore(); - }, - completeTextClipping: function CanvasGraphics_completeTextClipping() { - var ctx = this.ctx; - // applying mask to the image (result is saved in maskCanvas) - var maskCtx = this.textClipLayers.maskCtx; - maskCtx.setTransform(1, 0, 0, 1, 0, 0); - maskCtx.globalCompositeOperation = 'source-in'; - maskCtx.drawImage(ctx.canvas, 0, 0); - - // restoring image data and applying the result of masked drawing - ctx.save(); - ctx.setTransform(1, 0, 0, 1, 0, 0); - ctx.putImageData(this.textClipLayers.imageData, 0, 0); - ctx.drawImage(this.textClipLayers.maskCanvas, 0, 0); - ctx.restore(); - - delete this.textClipLayers; + ctx.clip(); + ctx.beginPath(); + delete this.pendingTextPaths; }, setCharSpacing: function CanvasGraphics_setCharSpacing(spacing) { this.current.charSpacing = spacing; }, setWordSpacing: function CanvasGraphics_setWordSpacing(spacing) { this.current.wordSpacing = spacing; }, setHScale: function CanvasGraphics_setHScale(scale) { @@ -3733,32 +3799,84 @@ var CanvasGraphics = (function CanvasGra } geometry.spaceWidth = font.spaceWidth; geometry.fontName = font.loadedName; geometry.fontFamily = font.fallbackName; geometry.fontSize = this.current.fontSize; return geometry; }, + paintChar: function (character, x, y) { + var ctx = this.ctx; + var current = this.current; + var font = current.font; + var fontSize = current.fontSize / current.fontSizeScale; + var textRenderingMode = current.textRenderingMode; + var fillStrokeMode = textRenderingMode & + TextRenderingMode.FILL_STROKE_MASK; + var isAddToPathSet = !!(textRenderingMode & + TextRenderingMode.ADD_TO_PATH_FLAG); + + var addToPath; + if (font.disableFontFace || isAddToPathSet) { + addToPath = font.renderer.getPathGenerator(character); + } + + if (font.disableFontFace) { + ctx.save(); + ctx.translate(x, y); + ctx.beginPath(); + addToPath(ctx, fontSize); + if (fillStrokeMode === TextRenderingMode.FILL || + fillStrokeMode === TextRenderingMode.FILL_STROKE) { + ctx.fill(); + } + if (fillStrokeMode === TextRenderingMode.STROKE || + fillStrokeMode === TextRenderingMode.FILL_STROKE) { + ctx.stroke(); + } + ctx.restore(); + } else { + if (fillStrokeMode === TextRenderingMode.FILL || + fillStrokeMode === TextRenderingMode.FILL_STROKE) { + ctx.fillText(character, x, y); + } + if (fillStrokeMode === TextRenderingMode.STROKE || + fillStrokeMode === TextRenderingMode.FILL_STROKE) { + ctx.strokeText(character, x, y); + } + } + + if (isAddToPathSet) { + var paths = this.pendingTextPaths || (this.pendingTextPaths = []); + paths.push({ + transform: ctx.mozCurrentTransform, + x: x, + y: y, + fontSize: fontSize, + addToPath: addToPath + }); + } + }, + showText: function CanvasGraphics_showText(str, skipTextSelection) { var ctx = this.ctx; var current = this.current; var font = current.font; var glyphs = font.charsToGlyphs(str); var fontSize = current.fontSize; var fontSizeScale = current.fontSizeScale; var charSpacing = current.charSpacing; var wordSpacing = current.wordSpacing; var textHScale = current.textHScale * current.fontDirection; var fontMatrix = current.fontMatrix || FONT_IDENTITY_MATRIX; var glyphsLength = glyphs.length; var textLayer = this.textLayer; var geom; var textSelection = textLayer && !skipTextSelection ? true : false; - var textRenderingMode = current.textRenderingMode; var canvasWidth = 0.0; var vertical = font.vertical; var defaultVMetrics = font.defaultVMetrics; // Type3 fonts - each glyph is a "mini-PDF" if (font.coded) { ctx.save(); ctx.transform.apply(ctx, current.textMatrix); @@ -3777,32 +3895,34 @@ var CanvasGraphics = (function CanvasGra var glyph = glyphs[i]; if (glyph === null) { // word break this.ctx.translate(wordSpacing, 0); current.x += wordSpacing * textHScale; continue; } + this.processingType3 = glyph; this.save(); ctx.scale(fontSize, fontSize); ctx.transform.apply(ctx, fontMatrix); this.executeOperatorList(glyph.operatorList); this.restore(); var transformed = Util.applyTransform([glyph.width, 0], fontMatrix); var width = (transformed[0] * fontSize + charSpacing) * current.fontDirection; ctx.translate(width, 0); current.x += width * textHScale; canvasWidth += width; } ctx.restore(); + this.processingType3 = null; } else { ctx.save(); this.applyTextTransforms(); var lineWidth = current.lineWidth; var a1 = current.textMatrix[0], b1 = current.textMatrix[1]; var scale = Math.sqrt(a1 * a1 + b1 * b1); if (scale === 0 || lineWidth === 0) @@ -3846,71 +3966,37 @@ var CanvasGraphics = (function CanvasGra if (!glyph.disabled) { if (vertical) { scaledX = vx / fontSizeScale; scaledY = (x + vy) / fontSizeScale; } else { scaledX = x / fontSizeScale; scaledY = 0; } - if (accent) { - scaledAccentX = scaledX + accent.offset.x / fontSizeScale; - scaledAccentY = scaledY - accent.offset.y / fontSizeScale; - } if (font.remeasure && width > 0) { // some standard fonts may not have the exact width, trying to // rescale per character var measuredWidth = ctx.measureText(character).width * 1000 / current.fontSize * current.fontSizeScale; var characterScaleX = width / measuredWidth; restoreNeeded = true; ctx.save(); ctx.scale(characterScaleX, 1); scaledX /= characterScaleX; if (accent) { scaledAccentX /= characterScaleX; } } - switch (textRenderingMode) { - default: // other unsupported rendering modes - case TextRenderingMode.FILL: - case TextRenderingMode.FILL_ADD_TO_PATH: - ctx.fillText(character, scaledX, scaledY); - if (accent) { - ctx.fillText(accent.fontChar, scaledAccentX, scaledAccentY); - } - break; - case TextRenderingMode.STROKE: - case TextRenderingMode.STROKE_ADD_TO_PATH: - ctx.strokeText(character, scaledX, scaledY); - if (accent) { - ctx.strokeText(accent.fontChar, scaledAccentX, scaledAccentY); - } - break; - case TextRenderingMode.FILL_STROKE: - case TextRenderingMode.FILL_STROKE_ADD_TO_PATH: - ctx.fillText(character, scaledX, scaledY); - ctx.strokeText(character, scaledX, scaledY); - if (accent) { - ctx.fillText(accent.fontChar, scaledAccentX, scaledAccentY); - ctx.strokeText(accent.fontChar, scaledAccentX, scaledAccentY); - } - break; - case TextRenderingMode.INVISIBLE: - case TextRenderingMode.ADD_TO_PATH: - break; - } - if (textRenderingMode & TextRenderingMode.ADD_TO_PATH_FLAG) { - var clipCtx = this.getCurrentTextClipping(); - clipCtx.fillText(character, scaledX, scaledY); - if (accent) { - clipCtx.fillText(accent.fontChar, scaledAccentX, scaledAccentY); - } + this.paintChar(character, scaledX, scaledY); + if (accent) { + scaledAccentX = scaledX + accent.offset.x / fontSizeScale; + scaledAccentY = scaledY - accent.offset.y / fontSizeScale; + this.paintChar(accent.fontChar, scaledAccentX, scaledAccentY); } } x += charWidth; canvasWidth += charWidth; if (restoreNeeded) { @@ -4290,16 +4376,25 @@ var CanvasGraphics = (function CanvasGra this.ctx.imageSmoothingEnabled = false; } else { this.ctx.mozImageSmoothingEnabled = false; } this.ctx.drawImage(groupCtx.canvas, 0, 0); this.restore(); }, + beginAnnotations: function CanvasGraphics_beginAnnotations() { + this.save(); + this.current = new CanvasExtraState(); + }, + + endAnnotations: function CanvasGraphics_endAnnotations() { + this.restore(); + }, + beginAnnotation: function CanvasGraphics_beginAnnotation(rect, transform, matrix) { this.save(); if (rect && isArray(rect) && 4 == rect.length) { var width = rect[2] - rect[0]; var height = rect[3] - rect[1]; this.rectangle(rect[0], rect[1], width, height); @@ -4338,67 +4433,84 @@ var CanvasGraphics = (function CanvasGra top: position[1], width: w / currentTransform[0], height: h / currentTransform[3] }); } this.restore(); }, - paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject( - imgArray, inverseDecode, width, height) { + paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject(img) { var ctx = this.ctx; - var tmpCanvas = createScratchCanvas(width, height); - var tmpCtx = tmpCanvas.getContext('2d'); + var width = img.width, height = img.height; + + var glyph = this.processingType3; + + if (COMPILE_TYPE3_GLYPHS && glyph && !('compiled' in glyph)) { + var MAX_SIZE_TO_COMPILE = 1000; + if (width <= MAX_SIZE_TO_COMPILE && height <= MAX_SIZE_TO_COMPILE) { + glyph.compiled = + compileType3Glyph({data: img.data, width: width, height: height}); + } else { + glyph.compiled = null; + } + } + + if (glyph && glyph.compiled) { + glyph.compiled(ctx); + return; + } + + var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height); + var maskCtx = maskCanvas.getContext('2d'); + maskCtx.save(); + + putBinaryImageData(maskCtx, img); + + maskCtx.globalCompositeOperation = 'source-in'; var fillColor = this.current.fillColor; - tmpCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && + maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && fillColor.type === 'Pattern') ? - fillColor.getPattern(tmpCtx) : fillColor; - tmpCtx.fillRect(0, 0, width, height); - - var imgData = tmpCtx.getImageData(0, 0, width, height); - var pixels = imgData.data; - - applyStencilMask(imgArray, width, height, inverseDecode, pixels); - - this.paintInlineImageXObject(imgData); + fillColor.getPattern(maskCtx) : fillColor; + maskCtx.fillRect(0, 0, width, height); + + maskCtx.restore(); + + this.paintInlineImageXObject(maskCanvas); }, paintImageMaskXObjectGroup: function CanvasGraphics_paintImageMaskXObjectGroup(images) { var ctx = this.ctx; - var tmpCanvasWidth = 0, tmpCanvasHeight = 0, tmpCanvas, tmpCtx; + for (var i = 0, ii = images.length; i < ii; i++) { var image = images[i]; - var w = image.width, h = image.height; - if (w > tmpCanvasWidth || h > tmpCanvasHeight) { - tmpCanvasWidth = Math.max(w, tmpCanvasWidth); - tmpCanvasHeight = Math.max(h, tmpCanvasHeight); - tmpCanvas = createScratchCanvas(tmpCanvasWidth, tmpCanvasHeight); - tmpCtx = tmpCanvas.getContext('2d'); - - var fillColor = this.current.fillColor; - tmpCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && - fillColor.type === 'Pattern') ? - fillColor.getPattern(tmpCtx) : fillColor; - } - tmpCtx.fillRect(0, 0, w, h); - - var imgData = tmpCtx.getImageData(0, 0, w, h); - var pixels = imgData.data; - - applyStencilMask(image.data, w, h, image.inverseDecode, pixels); - - tmpCtx.putImageData(imgData, 0, 0); + var width = image.width, height = image.height; + + var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height); + var maskCtx = maskCanvas.getContext('2d'); + maskCtx.save(); + + putBinaryImageData(maskCtx, image); + + maskCtx.globalCompositeOperation = 'source-in'; + + var fillColor = this.current.fillColor; + maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && + fillColor.type === 'Pattern') ? + fillColor.getPattern(maskCtx) : fillColor; + maskCtx.fillRect(0, 0, width, height); + + maskCtx.restore(); ctx.save(); ctx.transform.apply(ctx, image.transform); ctx.scale(1, -1); - ctx.drawImage(tmpCanvas, 0, 0, w, h, + ctx.drawImage(maskCanvas, 0, 0, width, height, 0, -1, 1, 1); ctx.restore(); } }, paintImageXObject: function CanvasGraphics_paintImageXObject(objId) { var imgData = this.objs.get(objId); if (!imgData) @@ -4407,42 +4519,66 @@ var CanvasGraphics = (function CanvasGra this.paintInlineImageXObject(imgData); }, paintInlineImageXObject: function CanvasGraphics_paintInlineImageXObject(imgData) { var width = imgData.width; var height = imgData.height; var ctx = this.ctx; + this.save(); // scale the image to the unit square ctx.scale(1 / width, -1 / height); var currentTransform = ctx.mozCurrentTransformInverse; - var widthScale = Math.max(Math.abs(currentTransform[0]), 1); - var heightScale = Math.max(Math.abs(currentTransform[3]), 1); - var tmpCanvas = createScratchCanvas(width, height); - var tmpCtx = tmpCanvas.getContext('2d'); - - if (widthScale > 2 || heightScale > 2) { - // canvas does not resize well large images to small -- using simple - // algorithm to perform pre-scaling - tmpCanvas = prescaleImage(imgData.data, - width, height, - widthScale, heightScale); - ctx.drawImage(tmpCanvas, 0, 0, tmpCanvas.width, tmpCanvas.height, - 0, -height, width, height); - } else { - if (typeof ImageData !== 'undefined' && imgData instanceof ImageData) { - tmpCtx.putImageData(imgData, 0, 0); - } else { - putBinaryImageData(tmpCtx, imgData.data, width, height); - } - ctx.drawImage(tmpCanvas, 0, -height); - } + var a = currentTransform[0], b = currentTransform[1]; + var widthScale = Math.max(Math.sqrt(a * a + b * b), 1); + var c = currentTransform[2], d = currentTransform[3]; + var heightScale = Math.max(Math.sqrt(c * c + d * d), 1); + + var imgToPaint; + if (imgData instanceof HTMLElement) { + imgToPaint = imgData; + } else { + var tmpCanvas = CachedCanvases.getCanvas('inlineImage', width, height); + var tmpCtx = tmpCanvas.getContext('2d'); + putBinaryImageData(tmpCtx, imgData); + imgToPaint = tmpCanvas; + } + + var paintWidth = width, paintHeight = height; + var tmpCanvasId = 'prescale1'; + // Vertial or horizontal scaling shall not be more than 2 to not loose the + // pixels during drawImage operation, painting on the temporary canvas(es) + // that are twice smaller in size + while ((widthScale > 2 && paintWidth > 1) || + (heightScale > 2 && paintHeight > 1)) { + var newWidth = paintWidth, newHeight = paintHeight; + if (widthScale > 2 && paintWidth > 1) { + newWidth = Math.ceil(paintWidth / 2); + widthScale /= paintWidth / newWidth; + } + if (heightScale > 2 && paintHeight > 1) { + newHeight = Math.ceil(paintHeight / 2); + heightScale /= paintHeight / newHeight; + } + var tmpCanvas = CachedCanvases.getCanvas(tmpCanvasId, + newWidth, newHeight); + tmpCtx = tmpCanvas.getContext('2d'); + tmpCtx.clearRect(0, 0, newWidth, newHeight); + tmpCtx.drawImage(imgToPaint, 0, 0, paintWidth, paintHeight, + 0, 0, newWidth, newHeight); + imgToPaint = tmpCanvas; + paintWidth = newWidth; + paintHeight = newHeight; + tmpCanvasId = tmpCanvasId === 'prescale1' ? 'prescale2' : 'prescale1'; + } + ctx.drawImage(imgToPaint, 0, 0, paintWidth, paintHeight, + 0, -height, width, height); if (this.imageLayer) { var position = this.getCanvasPosition(0, -height); this.imageLayer.appendImage({ imgData: imgData, left: position[0], top: position[1], width: width / currentTransform[0], @@ -4453,19 +4589,19 @@ var CanvasGraphics = (function CanvasGra }, paintInlineImageXObjectGroup: function CanvasGraphics_paintInlineImageXObjectGroup(imgData, map) { var ctx = this.ctx; var w = imgData.width; var h = imgData.height; - var tmpCanvas = createScratchCanvas(w, h); + var tmpCanvas = CachedCanvases.getCanvas('inlineImage', w, h); var tmpCtx = tmpCanvas.getContext('2d'); - putBinaryImageData(tmpCtx, imgData.data, w, h); + putBinaryImageData(tmpCtx, imgData); for (var i = 0, ii = map.length; i < ii; i++) { var entry = map[i]; ctx.save(); ctx.transform.apply(ctx, entry.transform); ctx.scale(1, -1); ctx.drawImage(tmpCanvas, entry.x, entry.y, entry.w, entry.h, 0, -1, 1, 1); @@ -4615,16 +4751,48 @@ var Dict = (function DictClosure() { if (typeof (value = this.map[key2]) != 'undefined' || key2 in this.map || typeof key3 == 'undefined') { return xref ? xref.fetchIfRef(value) : value; } value = this.map[key3] || null; return xref ? xref.fetchIfRef(value) : value; }, + // Same as get(), but returns a promise and uses fetchIfRefAsync(). + getAsync: function Dict_getAsync(key1, key2, key3) { + var value; + var promise; + var xref = this.xref; + if (typeof (value = this.map[key1]) !== undefined || key1 in this.map || + typeof key2 === undefined) { + if (xref) { + return xref.fetchIfRefAsync(value); + } + promise = new Promise(); + promise.resolve(value); + return promise; + } + if (typeof (value = this.map[key2]) !== undefined || key2 in this.map || + typeof key3 === undefined) { + if (xref) { + return xref.fetchIfRefAsync(value); + } + promise = new Promise(); + promise.resolve(value); + return promise; + } + value = this.map[key3] || null; + if (xref) { + return xref.fetchIfRefAsync(value); + } + promise = new Promise(); + promise.resolve(value); + return promise; + }, + // no dereferencing getRaw: function Dict_getRaw(key) { return this.map[key]; }, // creates new map and dereferences all Refs getAll: function Dict_getAll() { var all = {}; @@ -4668,21 +4836,25 @@ var Ref = (function RefClosure() { // this structure stores only one instance of the reference. var RefSet = (function RefSetClosure() { function RefSet() { this.dict = {}; } RefSet.prototype = { has: function RefSet_has(ref) { - return !!this.dict['R' + ref.num + '.' + ref.gen]; + return ('R' + ref.num + '.' + ref.gen) in this.dict; }, put: function RefSet_put(ref) { - this.dict['R' + ref.num + '.' + ref.gen] = ref; + this.dict['R' + ref.num + '.' + ref.gen] = true; + }, + + remove: function RefSet_remove(ref) { + delete this.dict['R' + ref.num + '.' + ref.gen]; } }; return RefSet; })(); var Catalog = (function CatalogClosure() { function Catalog(pdfManager, xref) { @@ -5340,17 +5512,16 @@ var XRef = (function XRefClosure() { this.startXRefQueue.shift(); } return this.topDict; } catch (e) { if (e instanceof MissingDataException) { throw e; } - log('(while reading XRef): ' + e); } if (recoveryMode) return; throw new XRefParseException(); }, @@ -5467,16 +5638,40 @@ var XRef = (function XRefClosure() { } } e = entries[e.gen]; if (!e) { error('bad XRef entry for compressed object'); } return e; }, + fetchIfRefAsync: function XRef_fetchIfRefAsync(obj) { + if (!isRef(obj)) { + var promise = new Promise(); + promise.resolve(obj); + return promise; + } + return this.fetchAsync(obj); + }, + fetchAsync: function XRef_fetchAsync(ref, suppressEncryption) { + var promise = new Promise(); + var tryFetch = function (promise) { + try { + promise.resolve(this.fetch(ref, suppressEncryption)); + } catch (e) { + if (e instanceof MissingDataException) { + this.stream.manager.requestRange(e.begin, e.end, tryFetch); + return; + } + promise.reject(e); + } + }.bind(this, promise); + tryFetch(); + return promise; + }, getCatalogObj: function XRef_getCatalogObj() { return this.root; } }; return XRef; })(); @@ -5643,16 +5838,151 @@ var PDFObjects = (function PDFObjectsClo clear: function PDFObjects_clear() { this.objs = {}; } }; return PDFObjects; })(); +/** + * A helper for loading missing data in object graphs. It traverses the graph + * depth first and queues up any objects that have missing data. Once it has + * has traversed as many objects that are available it attempts to bundle the + * missing data requests and then resume from the nodes that weren't ready. + * + * NOTE: It provides protection from circular references by keeping track of + * of loaded references. However, you must be careful not to load any graphs + * that have references to the catalog or other pages since that will cause the + * entire PDF document object graph to be traversed. + */ +var ObjectLoader = (function() { + + function mayHaveChildren(value) { + return isRef(value) || isDict(value) || isArray(value) || isStream(value); + } + + function addChildren(node, nodesToVisit) { + if (isDict(node) || isStream(node)) { + var map; + if (isDict(node)) { + map = node.map; + } else { + map = node.dict.map; + } + for (var key in map) { + var value = map[key]; + if (mayHaveChildren(value)) { + nodesToVisit.push(value); + } + } + } else if (isArray(node)) { + for (var i = 0, ii = node.length; i < ii; i++) { + var value = node[i]; + if (mayHaveChildren(value)) { + nodesToVisit.push(value); + } + } + } + } + + function ObjectLoader(obj, keys, xref) { + this.obj = obj; + this.keys = keys; + this.xref = xref; + this.refSet = null; + } + + ObjectLoader.prototype = { + + load: function ObjectLoader_load() { + var keys = this.keys; + this.promise = new Promise(); + // Don't walk the graph if all the data is already loaded. + if (!(this.xref.stream instanceof ChunkedStream) || + this.xref.stream.getMissingChunks().length === 0) { + this.promise.resolve(); + return this.promise; + } + + this.refSet = new RefSet(); + // Setup the initial nodes to visit. + var nodesToVisit = []; + for (var i = 0; i < keys.length; i++) { + nodesToVisit.push(this.obj[keys[i]]); + } + + this.walk(nodesToVisit); + return this.promise; + }, + + walk: function ObjectLoader_walk(nodesToVisit) { + var nodesToRevisit = []; + var pendingRequests = []; + // DFS walk of the object graph. + while (nodesToVisit.length) { + var currentNode = nodesToVisit.pop(); + + // Only references or chunked streams can cause missing data exceptions. + if (isRef(currentNode)) { + // Skip nodes that have already been visited. + if (this.refSet.has(currentNode)) { + continue; + } + try { + var ref = currentNode; + this.refSet.put(ref); + currentNode = this.xref.fetch(currentNode); + } catch (e) { + if (!(e instanceof MissingDataException)) { + throw e; + } + nodesToRevisit.push(currentNode); + pendingRequests.push({ begin: e.begin, end: e.end }); + } + } + if (currentNode instanceof ChunkedStream && + currentNode.getMissingChunks().length) { + nodesToRevisit.push(currentNode); + pendingRequests.push({ + begin: currentNode.start, + end: currentNode.end + }); + } + + addChildren(currentNode, nodesToVisit); + } + + if (pendingRequests.length) { + this.xref.stream.manager.requestRanges(pendingRequests, + function pendingRequestCallback() { + nodesToVisit = nodesToRevisit; + for (var i = 0; i < nodesToRevisit.length; i++) { + var node = nodesToRevisit[i]; + // Remove any reference nodes from the currrent refset so they + // aren't skipped when we revist them. + if (isRef(node)) { + this.refSet.remove(node); + } + } + this.walk(nodesToVisit); + }.bind(this)); + return; + } + // Everything is loaded. + this.refSet = null; + this.promise.resolve(); + } + + }; + + return ObjectLoader; +})(); + + var Annotation = (function AnnotationClosure() { // 12.5.5: Algorithm: Appearance streams function getTransformMatrix(rect, bbox, matrix) { var bounds = Util.getAxialAlignedBoundingBox(bbox, matrix); var minX = bounds[0]; var minY = bounds[1]; @@ -5705,24 +6035,33 @@ var Annotation = (function AnnotationClo var dict = params.dict; var data = this.data = {}; data.subtype = dict.get('Subtype').name; var rect = dict.get('Rect'); data.rect = Util.normalizeRect(rect); data.annotationFlags = dict.get('F'); - var border = dict.get('BS'); - if (isDict(border)) { - var borderWidth = border.has('W') ? border.get('W') : 1; - data.border = { - width: borderWidth, - type: border.get('S') || 'S', - rgb: dict.get('C') || [0, 0, 1] - }; + var color = dict.get('C'); + if (isArray(color) && color.length === 3) { + // TODO(mack): currently only supporting rgb; need support different + // colorspaces + data.color = color; + } else { + data.color = [0, 0, 0]; + } + + // Some types of annotations have border style dict which has more + // info than the border array + if (dict.has('BS')) { + var borderStyle = dict.get('BS'); + data.borderWidth = borderStyle.has('W') ? borderStyle.get('W') : 1; + } else { + var borderArray = dict.get('Border') || [0, 0, 1]; + data.borderWidth = borderArray[2]; } this.appearance = getDefaultAppearance(dict); } Annotation.prototype = { getData: function Annotation_getData() { @@ -5733,17 +6072,18 @@ var Annotation = (function AnnotationClo return false; }, getHtmlElement: function Annotation_getHtmlElement(commonObjs) { throw new NotImplementedException( 'getHtmlElement() should be implemented in subclass'); }, - getEmptyContainer: function Annotaiton_getEmptyContainer(tagName, rect) { + // TODO(mack): Remove this, it's not really that helpful. + getEmptyContainer: function Annotation_getEmptyContainer(tagName, rect) { assert(!isWorker, 'getEmptyContainer() should be called from main thread'); rect = rect || this.data.rect; var element = document.createElement(tagName); element.style.width = Math.ceil(rect[2] - rect[0]) + 'px'; element.style.height = Math.ceil(rect[3] - rect[1]) + 'px'; return element; @@ -5754,17 +6094,31 @@ var Annotation = (function AnnotationClo return !!( data && (!data.annotationFlags || !(data.annotationFlags & 0x22)) && // Hidden or NoView data.rect // rectangle is nessessary ); }, - getOperatorList: function Annotation_appendToOperatorList(evaluator) { + loadResources: function(keys) { + var promise = new Promise(); + this.appearance.dict.getAsync('Resources').then(function(resources) { + var objectLoader = new ObjectLoader(resources.map, + keys, + resources.xref); + objectLoader.load().then(function() { + promise.resolve(resources); + }); + }.bind(this)); + + return promise; + }, + + getOperatorList: function Annotation_getToOperatorList(evaluator) { var promise = new Promise(); if (!this.appearance) { promise.resolve({ queue: { fnArray: [], argsArray: [] @@ -5772,36 +6126,47 @@ var Annotation = (function AnnotationClo dependency: {} }); return promise; } var data = this.data; var appearanceDict = this.appearance.dict; - var resources = appearanceDict.get('Resources'); + var resourcesPromise = this.loadResources([ + 'ExtGState', + 'ColorSpace', + 'Pattern', + 'Shading', + 'XObject', + 'Font' + // ProcSet + // Properties + ]); var bbox = appearanceDict.get('BBox') || [0, 0, 1, 1]; var matrix = appearanceDict.get('Matrix') || [1, 0, 0, 1, 0 ,0]; var transform = getTransformMatrix(data.rect, bbox, matrix); var border = data.border; - var listPromise = evaluator.getOperatorList(this.appearance, resources); - listPromise.then(function(appearanceStreamData) { - var fnArray = appearanceStreamData.queue.fnArray; - var argsArray = appearanceStreamData.queue.argsArray; - - fnArray.unshift('beginAnnotation'); - argsArray.unshift([data.rect, transform, matrix]); - - fnArray.push('endAnnotation'); - argsArray.push([]); - - promise.resolve(appearanceStreamData); - }); + resourcesPromise.then(function(resources) { + var listPromise = evaluator.getOperatorList(this.appearance, resources); + listPromise.then(function(appearanceStreamData) { + var fnArray = appearanceStreamData.queue.fnArray; + var argsArray = appearanceStreamData.queue.argsArray; + + fnArray.unshift('beginAnnotation'); + argsArray.unshift([data.rect, transform, matrix]); + + fnArray.push('endAnnotation'); + argsArray.push([]); + + promise.resolve(appearanceStreamData); + }); + }.bind(this)); return promise; } }; Annotation.getConstructor = function Annotation_getConstructor(subtype, fieldType) { @@ -5814,17 +6179,21 @@ var Annotation = (function AnnotationClo return LinkAnnotation; } else if (subtype === 'Text') { return TextAnnotation; } else if (subtype === 'Widget') { if (!fieldType) { return; } - return WidgetAnnotation; + if (fieldType === 'Tx') { + return TextWidgetAnnotation; + } else { + return WidgetAnnotation; + } } else { return Annotation; } }; // TODO(mack): Support loading annotation from data Annotation.fromData = function Annotation_fromData(data) { var subtype = data.subtype; @@ -5865,16 +6234,51 @@ var Annotation = (function AnnotationClo if (annotation.isViewable()) { return annotation; } else { TODO('unimplemented annotation type: ' + subtype); } }; + Annotation.appendToOperatorList = function Annotation_appendToOperatorList( + annotations, pageQueue, pdfManager, dependencies, partialEvaluator) { + + function reject(e) { + annotationsReadyPromise.reject(e); + } + + var annotationsReadyPromise = new Promise(); + + var annotationPromises = []; + for (var i = 0, n = annotations.length; i < n; ++i) { + annotationPromises.push(annotations[i].getOperatorList(partialEvaluator)); + } + + Promise.all(annotationPromises).then(function(datas) { + var fnArray = pageQueue.fnArray; + var argsArray = pageQueue.argsArray; + fnArray.push('beginAnnotations'); + argsArray.push([]); + for (var i = 0, n = datas.length; i < n; ++i) { + var annotationData = datas[i]; + var annotationQueue = annotationData.queue; + Util.concatenateToArray(fnArray, annotationQueue.fnArray); + Util.concatenateToArray(argsArray, annotationQueue.argsArray); + Util.extendObj(dependencies, annotationData.dependencies); + } + fnArray.push('endAnnotations'); + argsArray.push([]); + + annotationsReadyPromise.resolve(); + }, reject); + + return annotationsReadyPromise; + }; + return Annotation; })(); PDFJS.Annotation = Annotation; var WidgetAnnotation = (function WidgetAnnotationClosure() { function WidgetAnnotation(params) { @@ -5938,16 +6342,158 @@ var WidgetAnnotation = (function WidgetA return parent.isViewable.call(this); } }); return WidgetAnnotation; })(); +var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() { + function TextWidgetAnnotation(params) { + WidgetAnnotation.call(this, params); + + if (params.data) { + return; + } + + this.data.textAlignment = Util.getInheritableProperty(params.dict, 'Q'); + } + + // TODO(mack): This dupes some of the logic in CanvasGraphics.setFont() + function setTextStyles(element, item, fontObj) { + + var style = element.style; + style.fontSize = item.fontSize + 'px'; + style.direction = item.fontDirection < 0 ? 'rtl': 'ltr'; + + if (!fontObj) { + return; + } + + style.fontWeight = fontObj.black ? + (fontObj.bold ? 'bolder' : 'bold') : + (fontObj.bold ? 'bold' : 'normal'); + style.fontStyle = fontObj.italic ? 'italic' : 'normal'; + + var fontName = fontObj.loadedName; + var fontFamily = fontName ? '"' + fontName + '", ' : ''; + // Use a reasonable default font if the font doesn't specify a fallback + var fallbackName = fontObj.fallbackName || 'Helvetica, sans-serif'; + style.fontFamily = fontFamily + fallbackName; + } + + + var parent = WidgetAnnotation.prototype; + Util.inherit(TextWidgetAnnotation, WidgetAnnotation, { + hasHtml: function TextWidgetAnnotation_hasHtml() { + return !!this.data.fieldValue; + }, + + getHtmlElement: function TextWidgetAnnotation_getHtmlElement(commonObjs) { + assert(!isWorker, 'getHtmlElement() shall be called from main thread'); + + var item = this.data; + + var element = this.getEmptyContainer('div'); + element.style.display = 'table'; + + var content = document.createElement('div'); + content.textContent = item.fieldValue; + var textAlignment = item.textAlignment; + content.style.textAlign = ['left', 'center', 'right'][textAlignment]; + content.style.verticalAlign = 'middle'; + content.style.display = 'table-cell'; + + var fontObj = item.fontRefName ? + commonObjs.getData(item.fontRefName) : null; + var cssRules = setTextStyles(content, item, fontObj); + + element.appendChild(content); + + return element; + }, + + getOperatorList: function TextWidgetAnnotation_getOperatorList(evaluator) { + + + var promise = new Promise(); + var data = this.data; + + // Even if there is an appearance stream, ignore it. This is the + // behaviour used by Adobe Reader. + + var defaultAppearance = data.defaultAppearance; + if (!defaultAppearance) { + promise.resolve({ + queue: { + fnArray: [], + argsArray: [] + }, + dependency: {} + }); + return promise; + } + + // Include any font resources found in the default appearance + + var stream = new Stream(stringToBytes(defaultAppearance)); + var listPromise = evaluator.getOperatorList(stream, this.fieldResources); + listPromise.then(function(appearanceStreamData) { + var appearanceFnArray = appearanceStreamData.queue.fnArray; + var appearanceArgsArray = appearanceStreamData.queue.argsArray; + var fnArray = []; + var argsArray = []; + + // TODO(mack): Add support for stroke color + data.rgb = [0, 0, 0]; + for (var i = 0, n = fnArray.length; i < n; ++i) { + var fnName = appearanceFnArray[i]; + var args = appearanceArgsArray[i]; + if (fnName === 'dependency') { + var dependency = args[i]; + if (dependency.indexOf('g_font_') === 0) { + data.fontRefName = dependency; + } + fnArray.push(fnName); + argsArray.push(args); + } else if (fnName === 'setFont') { + data.fontRefName = args[0]; + var size = args[1]; + if (size < 0) { + data.fontDirection = -1; + data.fontSize = -size; + } else { + data.fontDirection = 1; + data.fontSize = size; + } + } else if (fnName === 'setFillRGBColor') { + data.rgb = args; + } else if (fnName === 'setFillGray') { + var rgbValue = args[0] * 255; + data.rgb = [rgbValue, rgbValue, rgbValue]; + } + } + promise.resolve({ + queue: { + fnArray: fnArray, + argsArray: argsArray + }, + dependency: {} + }); + + }); + + return promise; + } + }); + + return TextWidgetAnnotation; +})(); + var TextAnnotation = (function TextAnnotationClosure() { function TextAnnotation(params) { Annotation.call(this, params); if (params.data) { return; } @@ -6123,17 +6669,34 @@ var LinkAnnotation = (function LinkAnnot return false; }, hasHtml: function LinkAnnotation_hasHtml() { return true; }, getHtmlElement: function LinkAnnotation_getHtmlElement(commonObjs) { - var element = this.getEmptyContainer('a'); + var rect = this.data.rect; + var element = document.createElement('a'); + var borderWidth = this.data.borderWidth; + + element.style.borderWidth = borderWidth + 'px'; + var color = this.data.color; + var rgb = []; + for (var i = 0; i < 3; ++i) { + rgb[i] = Math.round(color[i] * 255); + } + element.style.borderColor = Util.makeCssRgb(rgb); + element.style.borderStyle = 'solid'; + + var width = rect[2] - rect[0] - 2 * borderWidth; + var height = rect[3] - rect[1] - 2 * borderWidth; + element.style.width = width + 'px'; + element.style.height = height + 'px'; + element.href = this.data.url || ''; return element; } }); return LinkAnnotation; })(); @@ -14415,17 +14978,17 @@ var IndexedCS = (function IndexedCSClosu if (isStream(lookup)) { lookupArray = new Uint8Array(length); var bytes = lookup.getBytes(length); lookupArray.set(bytes); } else if (isString(lookup)) { lookupArray = new Uint8Array(length); for (var i = 0; i < length; ++i) lookupArray[i] = lookup.charCodeAt(i); - } else if (lookup instanceof Uint8Array) { + } else if (lookup instanceof Uint8Array || lookup instanceof Array) { lookupArray = lookup; } else { error('Unrecognized lookup table: ' + lookup); } this.lookup = lookupArray; } IndexedCS.prototype = { @@ -15658,17 +16221,18 @@ var PartialEvaluator = (function Partial var width = dict.get('Width', 'W'); var height = dict.get('Height', 'H'); var bitStrideLength = (width + 7) >> 3; var imgArray = image.getBytes(bitStrideLength * height); var decode = dict.get('Decode', 'D'); var inverseDecode = !!decode && decode[0] > 0; retData.fn = 'paintImageMaskXObject'; - retData.args = [imgArray, inverseDecode, width, height]; + retData.args = [PDFImage.createMask(imgArray, width, height, + inverseDecode)]; return retData; } var softMask = dict.get('SMask', 'SM') || false; var mask = dict.get('Mask') || false; var SMALL_IMAGE_DIMENSIONS = 200; // Inlining small images into the queue as RGB data @@ -15944,19 +16508,16 @@ var PartialEvaluator = (function Partial font.loadedName = 'g_font_' + this.uniquePrefix + (this.idCounters.font + 1); if (!font.translated) { var translated; try { translated = this.translateFont(font, xref); } catch (e) { - if (e instanceof MissingDataException) { - throw e; - } translated = new ErrorFont(e instanceof Error ? e.message : e); } font.translated = translated; } if (font.translated.loadCharProcs) { var charProcs = font.get('CharProcs').getAll(); var fontResources = font.get('Resources') || resources; @@ -16015,231 +16576,218 @@ var PartialEvaluator = (function Partial resources = resources || new Dict(); var xobjs = resources.get('XObject') || new Dict(); var patterns = resources.get('Pattern') || new Dict(); // TODO(mduan): pass array of knownCommands rather than OP_MAP // dictionary var parser = new Parser(new Lexer(stream, OP_MAP), false, xref); var promise = new Promise(); - function parseCommands() { - try { - parser.restoreState(); - var args = []; - while (true) { - - var obj = parser.getObj(); - - if (isEOF(obj)) { - break; - } - - if (isCmd(obj)) { - var cmd = obj.cmd; - - // Check that the command is valid - var opSpec = OP_MAP[cmd]; - if (!opSpec) { - warn('Unknown command "' + cmd + '"'); - continue; - } - - var fn = opSpec.fnName; - - // Validate the number of arguments for the command - if (opSpec.variableArgs) { - if (args.length > opSpec.numArgs) { - info('Command ' + fn + ': expected [0,' + opSpec.numArgs + - '] args, but received ' + args.length + ' args'); - } + var args = []; + while (true) { + + var obj = parser.getObj(); + + if (isEOF(obj)) { + break; + } + + if (isCmd(obj)) { + var cmd = obj.cmd; + + // Check that the command is valid + var opSpec = OP_MAP[cmd]; + if (!opSpec) { + warn('Unknown command "' + cmd + '"'); + continue; + } + + var fn = opSpec.fnName; + + // Validate the number of arguments for the command + if (opSpec.variableArgs) { + if (args.length > opSpec.numArgs) { + info('Command ' + fn + ': expected [0,' + opSpec.numArgs + + '] args, but received ' + args.length + ' args'); + } + } else { + if (args.length < opSpec.numArgs) { + // If we receive too few args, it's not possible to possible + // to execute the command, so skip the command + info('Command ' + fn + ': because expected ' + + opSpec.numArgs + ' args, but received ' + args.length + + ' args; skipping'); + args = []; + continue; + } else if (args.length > opSpec.numArgs) { + info('Command ' + fn + ': expected ' + opSpec.numArgs + + ' args, but received ' + args.length + ' args'); + } + } + + // TODO figure out how to type-check vararg functions + + if ((cmd == 'SCN' || cmd == 'scn') && + !args[args.length - 1].code) { + // compile tiling patterns + var patternName = args[args.length - 1]; + // SCN/scn applies patterns along with normal colors + var pattern; + if (isName(patternName) && + (pattern = patterns.get(patternName.name))) { + + var dict = isStream(pattern) ? pattern.dict : pattern; + var typeNum = dict.get('PatternType'); + + if (typeNum == TILING_PATTERN) { + var patternPromise = self.handleTilingType( + fn, args, resources, pattern, dict); + fn = 'promise'; + args = [patternPromise]; + } else if (typeNum == SHADING_PATTERN) { + var shading = dict.get('Shading'); + var matrix = dict.get('Matrix'); + var pattern = Pattern.parseShading(shading, matrix, xref, + resources); + args = pattern.getIR(); } else { - if (args.length < opSpec.numArgs) { - // If we receive too few args, it's not possible to possible - // to execute the command, so skip the command - info('Command ' + fn + ': because expected ' + - opSpec.numArgs + ' args, but received ' + args.length + - ' args; skipping'); - args = []; - continue; - } else if (args.length > opSpec.numArgs) { - info('Command ' + fn + ': expected ' + opSpec.numArgs + - ' args, but received ' + args.length + ' args'); - } - } - - // TODO figure out how to type-check vararg functions - - if ((cmd == 'SCN' || cmd == 'scn') && - !args[args.length - 1].code) { - // compile tiling patterns - var patternName = args[args.length - 1]; - // SCN/scn applies patterns along with normal colors - var pattern; - if (isName(patternName) && - (pattern = patterns.get(patternName.name))) { - - var dict = isStream(pattern) ? pattern.dict : pattern; - var typeNum = dict.get('PatternType'); - - if (typeNum == TILING_PATTERN) { - var patternPromise = self.handleTilingType( - fn, args, resources, pattern, dict); - fn = 'promise'; - args = [patternPromise]; - } else if (typeNum == SHADING_PATTERN) { - var shading = dict.get('Shading'); - var matrix = dict.get('Matrix'); - var pattern = Pattern.parseShading(shading, matrix, xref, - resources); - args = pattern.getIR(); - } else { - error('Unkown PatternType ' + typeNum); - } - } - } else if (cmd == 'Do' && !args[0].code) { - // eagerly compile XForm objects - var name = args[0].name; - var xobj = xobjs.get(name); - if (xobj) { - assertWellFormed( - isStream(xobj), 'XObject should be a stream'); - - var type = xobj.dict.get('Subtype'); - assertWellFormed( - isName(type), - 'XObject should have a Name subtype' - ); - - if ('Form' == type.name) { - fn = 'promise'; - args = [self.buildFormXObject(resources, xobj)]; - } else if ('Image' == type.name) { - var data = self.buildPaintImageXObject( - resources, xobj, false); - Util.extendObj(dependencies, data.dependencies); - self.insertDependencies(queue, data.dependencies); - fn = data.fn; - args = data.args; - } else { - error('Unhandled XObject subtype ' + type.name); - } - } - } else if (cmd == 'Tf') { // eagerly collect all fonts + error('Unkown PatternType ' + typeNum); + } + } + } else if (cmd == 'Do' && !args[0].code) { + // eagerly compile XForm objects + var name = args[0].name; + var xobj = xobjs.get(name); + if (xobj) { + assertWellFormed( + isStream(xobj), 'XObject should be a stream'); + + var type = xobj.dict.get('Subtype'); + assertWellFormed( + isName(type), + 'XObject should have a Name subtype' + ); + + if ('Form' == type.name) { fn = 'promise'; - args = [self.handleSetFont(resources, args)]; - } else if (cmd == 'EI') { + args = [self.buildFormXObject(resources, xobj)]; + } else if ('Image' == type.name) { var data = self.buildPaintImageXObject( - resources, args[0], true); + resources, xobj, false); Util.extendObj(dependencies, data.dependencies); self.insertDependencies(queue, data.dependencies); fn = data.fn; args = data.args; - } - - switch (fn) { - // Parse the ColorSpace data to a raw format. - case 'setFillColorSpace': - case 'setStrokeColorSpace': - args = [ColorSpace.parseToIR(args[0], xref, resources)]; - break; - case 'shadingFill': - var shadingRes = resources.get('Shading'); - if (!shadingRes) - error('No shading resource found'); - - var shading = shadingRes.get(args[0].name); - if (!shading) - error('No shading object found'); - - var shadingFill = Pattern.parseShading( - shading, null, xref, resources); - var patternIR = shadingFill.getIR(); - args = [patternIR]; - fn = 'shadingFill'; - break; - case 'setGState': - var dictName = args[0]; - var extGState = resources.get('ExtGState'); - - if (!isDict(extGState) || !extGState.has(dictName.name)) - break; - - var gState = extGState.get(dictName.name); - fn = 'promise'; - args = [self.setGState(resources, gState)]; - } // switch - - fnArray.push(fn); - argsArray.push(args); - args = []; - parser.saveState(); - } else if (obj !== null && obj !== undefined) { - args.push(obj instanceof Dict ? obj.getAll() : obj); - assertWellFormed(args.length <= 33, 'Too many arguments'); - } - } - - var subQueuePromises = []; - for (var i = 0; i < fnArray.length; ++i) { - if (fnArray[i] === 'promise') { - subQueuePromises.push(argsArray[i][0]); - } - } - Promise.all(subQueuePromises).then(function(datas) { - // TODO(mack): Optimize by using repositioning elements - // in original queue rather than creating new queue - - for (var i = 0, n = datas.length; i < n; ++i) { - var data = datas[i]; - var subQueue = data.queue; - queue.transparency = subQueue.transparency || queue.transparency; - Util.extendObj(dependencies, data.dependencies); - } - - var newFnArray = []; - var newArgsArray = []; - var currOffset = 0; - var subQueueIdx = 0; - for (var i = 0, n = fnArray.length; i < n; ++i) { - var offset = i + currOffset; - if (fnArray[i] === 'promise') { - var data = datas[subQueueIdx++]; - var subQueue = data.queue; - var subQueueFnArray = subQueue.fnArray; - var subQueueArgsArray = subQueue.argsArray; - for (var j = 0, nn = subQueueFnArray.length; j < nn; ++j) { - newFnArray[offset + j] = subQueueFnArray[j]; - newArgsArray[offset + j] = subQueueArgsArray[j]; - } - currOffset += subQueueFnArray.length - 1; } else { - newFnArray[offset] = fnArray[i]; - newArgsArray[offset] = argsArray[i]; - } - } - - promise.resolve({ - queue: { - fnArray: newFnArray, - argsArray: newArgsArray, - transparency: queue.transparency - }, - dependencies: dependencies - }); - }); - } catch (e) { - if (!(e instanceof MissingDataException)) { - throw e; - } - - self.pdfManager.requestRange(e.begin, e.end).then(parseCommands); - } - } - parser.saveState(); - parseCommands(); + error('Unhandled XObject subtype ' + type.name); + } + } + } else if (cmd == 'Tf') { // eagerly collect all fonts + fn = 'promise'; + args = [self.handleSetFont(resources, args)]; + } else if (cmd == 'EI') { + var data = self.buildPaintImageXObject( + resources, args[0], true); + Util.extendObj(dependencies, data.dependencies); + self.insertDependencies(queue, data.dependencies); + fn = data.fn; + args = data.args; + } + + switch (fn) { + // Parse the ColorSpace data to a raw format. + case 'setFillColorSpace': + case 'setStrokeColorSpace': + args = [ColorSpace.parseToIR(args[0], xref, resources)]; + break; + case 'shadingFill': + var shadingRes = resources.get('Shading'); + if (!shadingRes) + error('No shading resource found'); + + var shading = shadingRes.get(args[0].name); + if (!shading) + error('No shading object found'); + + var shadingFill = Pattern.parseShading( + shading, null, xref, resources); + var patternIR = shadingFill.getIR(); + args = [patternIR]; + fn = 'shadingFill'; + break; + case 'setGState': + var dictName = args[0]; + var extGState = resources.get('ExtGState'); + + if (!isDict(extGState) || !extGState.has(dictName.name)) + break; + + var gState = extGState.get(dictName.name); + fn = 'promise'; + args = [self.setGState(resources, gState)]; + } // switch + + fnArray.push(fn); + argsArray.push(args); + args = []; + parser.saveState(); + } else if (obj !== null && obj !== undefined) { + args.push(obj instanceof Dict ? obj.getAll() : obj); + assertWellFormed(args.length <= 33, 'Too many arguments'); + } + } + + var subQueuePromises = []; + for (var i = 0; i < fnArray.length; ++i) { + if (fnArray[i] === 'promise') { + subQueuePromises.push(argsArray[i][0]); + } + } + Promise.all(subQueuePromises).then(function(datas) { + // TODO(mack): Optimize by using repositioning elements + // in original queue rather than creating new queue + + for (var i = 0, n = datas.length; i < n; ++i) { + var data = datas[i]; + var subQueue = data.queue; + queue.transparency = subQueue.transparency || queue.transparency; + Util.extendObj(dependencies, data.dependencies); + } + + var newFnArray = []; + var newArgsArray = []; + var currOffset = 0; + var subQueueIdx = 0; + for (var i = 0, n = fnArray.length; i < n; ++i) { + var offset = i + currOffset; + if (fnArray[i] === 'promise') { + var data = datas[subQueueIdx++]; + var subQueue = data.queue; + var subQueueFnArray = subQueue.fnArray; + var subQueueArgsArray = subQueue.argsArray; + for (var j = 0, nn = subQueueFnArray.length; j < nn; ++j) { + newFnArray[offset + j] = subQueueFnArray[j]; + newArgsArray[offset + j] = subQueueArgsArray[j]; + } + currOffset += subQueueFnArray.length - 1; + } else { + newFnArray[offset] = fnArray[i]; + newArgsArray[offset] = argsArray[i]; + } + } + + promise.resolve({ + queue: { + fnArray: newFnArray, + argsArray: newArgsArray, + transparency: queue.transparency + }, + dependencies: dependencies + }); + }); return promise; }, getTextContent: function PartialEvaluator_getTextContent( stream, resources) { var SPACE_FACTOR = 0.35; @@ -16267,171 +16815,158 @@ var PartialEvaluator = (function Partial resources = this.xref.fetchIfRef(resources) || new Dict(); // The xobj is parsed iff it's needed, e.g. if there is a `DO` cmd. var xobjs = null; var parser = new Parser(new Lexer(stream), false); var chunkPromises = []; var fontPromise; - function parseCommands() { - try { - parser.restoreState(); - var args = []; - - while (true) { - var obj = parser.getObj(); - if (isEOF(obj)) { - break; - } - - if (isCmd(obj)) { - var cmd = obj.cmd; - switch (cmd) { - // TODO: Add support for SAVE/RESTORE and XFORM here. - case 'Tf': - fontPromise = handleSetFont(args[0].name, null, resources); - //.translated; - break; - case 'TJ': - var chunkPromise = new Promise(); - chunkPromises.push(chunkPromise); - fontPromise.then(function(items, chunkPromise, font) { - var chunk = ''; - for (var j = 0, jj = items.length; j < jj; j++) { - if (typeof items[j] === 'string') { - chunk += fontCharsToUnicode(items[j], font); - } else if (items[j] < 0 && font.spaceWidth > 0) { - var fakeSpaces = -items[j] / font.spaceWidth; - if (fakeSpaces > MULTI_SPACE_FACTOR) { - fakeSpaces = Math.round(fakeSpaces); - while (fakeSpaces--) { - chunk += ' '; - } - } else if (fakeSpaces > SPACE_FACTOR) { - chunk += ' '; - } + var args = []; + + while (true) { + var obj = parser.getObj(); + if (isEOF(obj)) { + break; + } + + if (isCmd(obj)) { + var cmd = obj.cmd; + switch (cmd) { + // TODO: Add support for SAVE/RESTORE and XFORM here. + case 'Tf': + fontPromise = handleSetFont(args[0].name, null, resources); + //.translated; + break; + case 'TJ': + var chunkPromise = new Promise(); + chunkPromises.push(chunkPromise); + fontPromise.then(function(items, chunkPromise, font) { + var chunk = ''; + for (var j = 0, jj = items.length; j < jj; j++) { + if (typeof items[j] === 'string') { + chunk += fontCharsToUnicode(items[j], font); + } else if (items[j] < 0 && font.spaceWidth > 0) { + var fakeSpaces = -items[j] / font.spaceWidth; + if (fakeSpaces > MULTI_SPACE_FACTOR) { + fakeSpaces = Math.round(fakeSpaces); + while (fakeSpaces--) { + chunk += ' '; } - } - chunkPromise.resolve( - getBidiText(chunk, -1, font.vertical)); - }.bind(null, args[0], chunkPromise)); - break; - case 'Tj': - var chunkPromise = new Promise(); - chunkPromises.push(chunkPromise); - fontPromise.then(function(charCodes, chunkPromise, font) { - var chunk = fontCharsToUnicode(charCodes, font); - chunkPromise.resolve( - getBidiText(chunk, -1, font.vertical)); - }.bind(null, args[0], chunkPromise)); - break; - case '\'': - // For search, adding a extra white space for line breaks - // would be better here, but that causes too much spaces in - // the text-selection divs. - var chunkPromise = new Promise(); - chunkPromises.push(chunkPromise); - fontPromise.then(function(charCodes, chunkPromise, font) { - var chunk = fontCharsToUnicode(charCodes, font); - chunkPromise.resolve( - getBidiText(chunk, -1, font.vertical)); - }.bind(null, args[0], chunkPromise)); - break; - case '"': - // Note comment in "'" - var chunkPromise = new Promise(); - chunkPromises.push(chunkPromise); - fontPromise.then(function(charCodes, chunkPromise, font) { - var chunk = fontCharsToUnicode(charCodes, font); - chunkPromise.resolve( - getBidiText(chunk, -1, font.vertical)); - }.bind(null, args[2], chunkPromise)); - break; - case 'Do': - if (args[0].code) { - break; - } - - if (!xobjs) { - xobjs = resources.get('XObject') || new Dict(); - } - - var name = args[0].name; - var xobj = xobjs.get(name); - if (!xobj) - break; - assertWellFormed(isStream(xobj), - 'XObject should be a stream'); - - var type = xobj.dict.get('Subtype'); - assertWellFormed( - isName(type), - 'XObject should have a Name subtype' - ); - - if ('Form' !== type.name) - break; - - var chunkPromise = self.getTextContent( - xobj, - xobj.dict.get('Resources') || resources - ); - chunkPromises.push(chunkPromise); - break; - case 'gs': - var dictName = args[0]; - var extGState = resources.get('ExtGState'); - - if (!isDict(extGState) || !extGState.has(dictName.name)) - break; - - var gsState = extGState.get(dictName.name); - - for (var i = 0; i < gsState.length; i++) { - if (gsState[i] === 'Font') { - fontPromise = handleSetFont( - args[0].name, null, resources); + } else if (fakeSpaces > SPACE_FACTOR) { + chunk += ' '; } } - break; - } // switch - - args = []; - parser.saveState(); - } else if (obj !== null && obj !== undefined) { - assertWellFormed(args.length <= 33, 'Too many arguments'); - args.push(obj); - } - } // while - - Promise.all(chunkPromises).then(function(datas) { - var bidiTexts = []; - for (var i = 0, n = datas.length; i < n; ++i) { - var bidiText = datas[i]; - if (!bidiText) { - continue; - } else if (isArray(bidiText)) { - Util.concatenateToArray(bidiTexts, bidiText); - } else { - bidiTexts.push(bidiText); - } - } - statePromise.resolve(bidiTexts); - }); - } catch (e) { - if (!(e instanceof MissingDataException)) { - throw e; - } - - self.pdfManager.requestRange(e.begin, e.end).then(parseCommands); - } - } - parser.saveState(); - parseCommands(); + } + chunkPromise.resolve( + getBidiText(chunk, -1, font.vertical)); + }.bind(null, args[0], chunkPromise)); + break; + case 'Tj': + var chunkPromise = new Promise(); + chunkPromises.push(chunkPromise); + fontPromise.then(function(charCodes, chunkPromise, font) { + var chunk = fontCharsToUnicode(charCodes, font); + chunkPromise.resolve( + getBidiText(chunk, -1, font.vertical)); + }.bind(null, args[0], chunkPromise)); + break; + case '\'': + // For search, adding a extra white space for line breaks + // would be better here, but that causes too much spaces in + // the text-selection divs. + var chunkPromise = new Promise(); + chunkPromises.push(chunkPromise); + fontPromise.then(function(charCodes, chunkPromise, font) { + var chunk = fontCharsToUnicode(charCodes, font); + chunkPromise.resolve( + getBidiText(chunk, -1, font.vertical)); + }.bind(null, args[0], chunkPromise)); + break; + case '"': + // Note comment in "'" + var chunkPromise = new Promise(); + chunkPromises.push(chunkPromise); + fontPromise.then(function(charCodes, chunkPromise, font) { + var chunk = fontCharsToUnicode(charCodes, font); + chunkPromise.resolve( + getBidiText(chunk, -1, font.vertical)); + }.bind(null, args[2], chunkPromise)); + break; + case 'Do': + if (args[0].code) { + break; + } + + if (!xobjs) { + xobjs = resources.get('XObject') || new Dict(); + } + + var name = args[0].name; + var xobj = xobjs.get(name); + if (!xobj) + break; + assertWellFormed(isStream(xobj), + 'XObject should be a stream'); + + var type = xobj.dict.get('Subtype'); + assertWellFormed( + isName(type), + 'XObject should have a Name subtype' + ); + + if ('Form' !== type.name) + break; + + var chunkPromise = self.getTextContent( + xobj, + xobj.dict.get('Resources') || resources + ); + chunkPromises.push(chunkPromise); + break; + case 'gs': + var dictName = args[0]; + var extGState = resources.get('ExtGState'); + + if (!isDict(extGState) || !extGState.has(dictName.name)) + break; + + var gsState = extGState.get(dictName.name); + + for (var i = 0; i < gsState.length; i++) { + if (gsState[i] === 'Font') { + fontPromise = handleSetFont( + args[0].name, null, resources); + } + } + break; + } // switch + + args = []; + parser.saveState(); + } else if (obj !== null && obj !== undefined) { + assertWellFormed(args.length <= 33, 'Too many arguments'); + args.push(obj); + } + } // while + + Promise.all(chunkPromises).then(function(datas) { + var bidiTexts = []; + for (var i = 0, n = datas.length; i < n; ++i) { + var bidiText = datas[i]; + if (!bidiText) { + continue; + } else if (isArray(bidiText)) { + Util.concatenateToArray(bidiTexts, bidiText); + } else { + bidiTexts.push(bidiText); + } + } + statePromise.resolve(bidiTexts); + }); return statePromise; }, extractDataStructures: function partialEvaluatorExtractDataStructures(dict, baseDict, xref, properties) { // 9.10.2 @@ -17036,19 +17571,18 @@ var PartialEvaluator = (function Partial MAX_IMAGES_IN_MASKS_BLOCK); if (count < MIN_IMAGES_IN_MASKS_BLOCK) { continue; } var images = []; for (var q = 0; q < count; q++) { var transform = argsArray[j + (q << 2) + 1]; var maskParams = argsArray[j + (q << 2) + 2]; - images.push({data: maskParams[0], width: maskParams[2], - height: maskParams[3], transform: transform, - inverseDecode: maskParams[1]}); + images.push({data: maskParams.data, width: maskParams.width, + height: maskParams.height, transform: transform}); } // replacing queue items fnArray.splice(j, count * 4, ['paintImageMaskXObjectGroup']); argsArray.splice(j, count * 4, [images]); i = j; ii = fnArray.length; } } @@ -17097,16 +17631,18 @@ var PDF_GLYPH_SPACE_UNITS = 1000; var HINTING_ENABLED = false; // Accented charactars are not displayed properly on windows, using this flag // to control analysis of seac charstrings. var SEAC_ANALYSIS_ENABLED = false; var FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0]; +PDFJS.disableFontFace = false; + var FontFlags = { FixedPitch: 1, Serif: 2, Symbolic: 4, Script: 8, Nonsymbolic: 32, Italic: 64, AllCap: 65536, @@ -19877,16 +20413,21 @@ var Font = (function FontClosure() { Font.prototype = { name: null, font: null, mimetype: null, encoding: null, + get renderer() { + var renderer = FontRendererFactory.create(this); + return shadow(this, 'renderer', renderer); + }, + exportData: function Font_exportData() { var data = {}; for (var i in this) { if (this.hasOwnProperty(i)) data[i] = this[i]; } return data; }, @@ -20728,16 +21269,19 @@ var Font = (function FontClosure() { if (prep) { sanitizeTTProgram(prep, ttContext); } if (fpgm) { addTTDummyFunctions(fpgm, ttContext, maxFunctionDefs); } } + // The following steps modify the original font data, making copy + font = new Stream(new Uint8Array(font.getBytes())); + // Check that required tables are present var requiredTables = ['OS/2', 'cmap', 'head', 'hhea', 'hmtx', 'maxp', 'name', 'post']; var header = readOpenTypeHeader(font); var numTables = header.numTables; var cmap, post, maxp, hhea, hmtx, head, os2; @@ -21423,16 +21967,21 @@ var Font = (function FontClosure() { this.unicodeToCID = []; } }, bindDOM: function Font_bindDOM() { if (!this.data) return null; + if (PDFJS.disableFontFace) { + this.disableFontFace = true; + return null; + } + var data = bytesToString(this.data); var fontName = this.loadedName; // Add the font-face rule to the document var url = ('url(data:' + this.mimetype + ';base64,' + window.btoa(data) + ');'); var rule = '@font-face { font-family:"' + fontName + '";src:' + url + '}'; @@ -22173,21 +22722,18 @@ var Type1Parser = (function Type1ParserC glyph: glyph, encoded: encoded }); } break; case 'Subrs': var num = this.readInt(); this.getToken(); // read in 'array' - for (var j = 0; j < num; ++j) { - token = this.getToken(); // read in 'dup' + while ((token = this.getToken()) === 'dup') { var index = this.readInt(); - if (index > j) - j = index; var length = this.readInt(); this.getToken(); // read in 'RD' or '-|' var data = stream.makeSubStream(stream.pos + 1, length); var lenIV = program.properties.privateData['lenIV']; var encoded = decrypt(data.getBytes(), CHAR_STRS_ENCRYPT_KEY, lenIV); // Skip past the required space and binary data. stream.skip(1 + length); @@ -24060,16 +24606,709 @@ var CFFCompiler = (function CFFCompilerC // https://github.com/mozilla/pdf.js/issues/1689 (function checkChromeWindows() { if (/Windows.*Chrome/.test(navigator.userAgent)) { SYMBOLIC_FONT_GLYPH_OFFSET = 0xF100; } })(); +var FontRendererFactory = (function FontRendererFactoryClosure() { + function getLong(data, offset) { + return (data[offset] << 24) | (data[offset + 1] << 16) | + (data[offset + 2] << 8) | data[offset + 3]; + } + + function getUshort(data, offset) { + return (data[offset] << 8) | data[offset + 1]; + } + + function parseCmap(data, start, end) { + var offset = getUshort(data, start + 2) === 1 ? getLong(data, start + 8) : + getLong(data, start + 16); + var format = getUshort(data, start + offset); + if (format === 4) { + var length = getUshort(data, start + offset + 2); + var segCount = getUshort(data, start + offset + 6) >> 1; + var p = start + offset + 14; + var ranges = []; + for (var i = 0; i < segCount; i++, p += 2) { + ranges[i] = {end: getUshort(data, p)}; + } + p += 2; + for (var i = 0; i < segCount; i++, p += 2) { + ranges[i].start = getUshort(data, p); + } + for (var i = 0; i < segCount; i++, p += 2) { + ranges[i].idDelta = getUshort(data, p); + } + for (var i = 0; i < segCount; i++, p += 2) { + var idOffset = getUshort(data, p); + if (idOffset === 0) { + continue; + } + ranges[i].ids = []; + for (var j = 0, jj = ranges[i].end - ranges[i].start + 1; j < jj; j++) { + ranges[i].ids[j] = getUshort(data, p + idOffset); + idOffset += 2; + } + } + return ranges; + } else if (format === 12) { + var length = getLong(data, start + offset + 4); + var groups = getLong(data, start + offset + 12); + var p = start + offset + 16; + var ranges = []; + for (var i = 0; i < groups; i++) { + ranges.push({ + start: getLong(data, p), + end: getLong(data, p + 4), + idDelta: getLong(data, p + 8) - getLong(data, p) + }); + p += 12; + } + return ranges; + } + error('not supported cmap: ' + format); + } + + function parseCff(data, start, end) { + var properties = {}; + var parser = new CFFParser( + new Stream(data, start, end - start), properties); + var cff = parser.parse(); + return { + glyphs: cff.charStrings.objects, + subrs: cff.topDict.privateDict && cff.topDict.privateDict.subrsIndex && + cff.topDict.privateDict.subrsIndex.objects, + gsubrs: cff.globalSubrIndex && cff.globalSubrIndex.objects + }; + } + + function parseGlyfTable(glyf, loca, isGlyphLocationsLong) { + var itemSize, itemDecode; + if (isGlyphLocationsLong) { + itemSize = 4; + itemDecode = function fontItemDecodeLong(data, offset) { + return (data[offset] << 24) | (data[offset + 1] << 16) | + (data[offset + 2] << 8) | data[offset + 3]; + }; + } else { + itemSize = 2; + itemDecode = function fontItemDecode(data, offset) { + return (data[offset] << 9) | (data[offset + 1] << 1); + }; + } + var glyphs = []; + var startOffset = itemDecode(loca, 0); + for (var j = itemSize; j < loca.length; j += itemSize) { + var endOffset = itemDecode(loca, j); + glyphs.push(glyf.subarray(startOffset, endOffset)); + startOffset = endOffset; + } + return glyphs; + } + + function lookupCmap(ranges, unicode) { + var code = unicode.charCodeAt(0); + var l = 0, r = ranges.length - 1; + while (l < r) { + var c = (l + r + 1) >> 1; + if (code < ranges[c].start) { + r = c - 1; + } else { + l = c; + } + } + if (ranges[l].start <= code && code <= ranges[l].end) { + return (ranges[l].idDelta + (ranges[l].ids ? + ranges[l].ids[code - ranges[l].start] : code)) & 0xFFFF; + } + return 0; + } + + function compileGlyf(code, js, font) { + function moveTo(x, y) { + js.push('c.moveTo(' + x + ',' + y + ');'); + } + function lineTo(x, y) { + js.push('c.lineTo(' + x + ',' + y + ');'); + } + function quadraticCurveTo(xa, ya, x, y) { + js.push('c.quadraticCurveTo(' + xa + ',' + ya + ',' + + x + ',' + y + ');'); + } + + var i = 0; + var numberOfContours = ((code[i] << 24) | (code[i + 1] << 16)) >> 16; + var xMin = ((code[i + 2] << 24) | (code[i + 3] << 16)) >> 16; + var yMin = ((code[i + 4] << 24) | (code[i + 5] << 16)) >> 16; + var xMax = ((code[i + 6] << 24) | (code[i + 7] << 16)) >> 16; + var yMax = ((code[i + 8] << 24) | (code[i + 9] << 16)) >> 16; + i += 10; + if (numberOfContours < 0) { + // composite glyph + var x = 0, y = 0; + do { + var flags = (code[i] << 8) | code[i + 1]; + var glyphIndex = (code[i + 2] << 8) | code[i + 3]; + i += 4; + var arg1, arg2; + if ((flags & 0x01)) { + arg1 = ((code[i] << 24) | (code[i + 1] << 16)) >> 16; + arg2 = ((code[i + 2] << 24) | (code[i + 3] << 16)) >> 16; + i += 4; + } else { + arg1 = code[i++]; arg2 = code[i++]; + } + if ((flags & 0x02)) { + x = arg1; + y = arg2; + } else { + x = 0; y = 0; // TODO "they are points" ? + } + var scaleX = 1, scaleY = 1, scale01 = 0, scale10 = 0; + if ((flags & 0x08)) { + scaleX = + scaleY = ((code[i] << 24) | (code[i + 1] << 16)) / 1073741824; + i += 2; + } else if ((flags & 0x40)) { + scaleX = ((code[i] << 24) | (code[i + 1] << 16)) / 1073741824; + scaleY = ((code[i + 2] << 24) | (code[i + 3] << 16)) / 1073741824; + i += 4; + } else if ((flags & 0x80)) { + scaleX = ((code[i] << 24) | (code[i + 1] << 16)) / 1073741824; + scale01 = ((code[i + 2] << 24) | (code[i + 3] << 16)) / 1073741824; + scale10 = ((code[i + 4] << 24) | (code[i + 5] << 16)) / 1073741824; + scaleY = ((code[i + 6] << 24) | (code[i + 7] << 16)) / 1073741824; + i += 8; + } + var subglyph = font.glyphs[glyphIndex]; + if (subglyph) { + js.push('c.save();'); + js.push('c.transform(' + scaleX + ',' + scale01 + ',' + + scale10 + ',' + scaleY + ',' + x + ',' + y + ');'); + compileGlyf(subglyph, js, font); + js.push('c.restore();'); + } + } while ((flags & 0x20)); + } else { + // simple glyph + var endPtsOfContours = []; + for (var j = 0; j < numberOfContours; j++) { + endPtsOfContours.push((code[i] << 8) | code[i + 1]); + i += 2; + } + var instructionLength = (code[i] << 8) | code[i + 1]; + i += 2 + instructionLength; // skipping the instructions + var numberOfPoints = endPtsOfContours[endPtsOfContours.length - 1] + 1; + var points = []; + while (points.length < numberOfPoints) { + var flags = code[i++], repeat = 1; + if ((flags & 0x08)) { + repeat += code[i++]; + } + while (repeat-- > 0) { + points.push({flags: flags}); + } + } + var x = 0, y = 0; + for (var j = 0; j < numberOfPoints; j++) { + switch (points[j].flags & 0x12) { + case 0x00: + x += ((code[i] << 24) | (code[i + 1] << 16)) >> 16; + i += 2; + break; + case 0x02: + x -= code[i++]; + break; + case 0x12: + x += code[i++]; + break; + } + points[j].x = x; + } + for (var j = 0; j < numberOfPoints; j++) { + switch (points[j].flags & 0x24) { + case 0x00: + y += ((code[i] << 24) | (code[i + 1] << 16)) >> 16; + i += 2; + break; + case 0x04: + y -= code[i++]; + break; + case 0x24: + y += code[i++]; + break; + } + points[j].y = y; + } + + var startPoint = 0; + for (var i = 0; i < numberOfContours; i++) { + var endPoint = endPtsOfContours[i]; + // contours might have implicit points, which is located in the middle + // between two neighboring off-curve points + var contour = points.slice(startPoint, endPoint + 1); + if ((contour[0].flags & 1)) { + contour.push(contour[0]); // using start point at the contour end + } else if ((contour[contour.length - 1].flags & 1)) { + // first is off-curve point, trying to use one from the end + contour.unshift(contour[contour.length - 1]); + } else { + // start and end are off-curve points, creating implicit one + var p = { + flags: 1, + x: (contour[0].x + contour[contour.length - 1].x) / 2, + y: (contour[0].y + contour[contour.length - 1].y) / 2 + }; + contour.unshift(p); + contour.push(p); + } + moveTo(contour[0].x, contour[0].y); + for (var j = 1, jj = contour.length; j < jj; j++) { + if ((contour[j].flags & 1)) { + lineTo(contour[j].x, contour[j].y); + } else if ((contour[j + 1].flags & 1)){ + quadraticCurveTo(contour[j].x, contour[j].y, + contour[j + 1].x, contour[j + 1].y); + j++; + } else { + quadraticCurveTo(contour[j].x, contour[j].y, + (contour[j].x + contour[j + 1].x) / 2, + (contour[j].y + contour[j + 1].y) / 2); + } + } + startPoint = endPoint + 1; + } + } + } + + function compileCharString(code, js, font) { + var stack = []; + var x = 0, y = 0; + var stems = 0; + + function moveTo(x, y) { + js.push('c.moveTo(' + x + ',' + y + ');'); + } + function lineTo(x, y) { + js.push('c.lineTo(' + x + ',' + y + ');'); + } + function bezierCurveTo(x1, y1, x2, y2, x, y) { + js.push('c.bezierCurveTo(' + x1 + ',' + y1 + ',' + x2 + ',' + y2 + ',' + + x + ',' + y + ');'); + } + + function parse(code) { + var i = 0; + while (i < code.length) { + var stackClean = false; + var v = code[i++]; + switch (v) { + case 1: // hstem + stems += stack.length >> 1; + stackClean = true; + break; + case 3: // vstem + stems += stack.length >> 1; + stackClean = true; + break; + case 4: // vmoveto + y += stack.pop(); + moveTo(x, y); + stackClean = true; + break; + case 5: // rlineto + while (stack.length > 0) { + x += stack.shift(); + y += stack.shift(); + lineTo(x, y); + } + break; + case 6: // hlineto + while (stack.length > 0) { + x += stack.shift(); + lineTo(x, y); + if (stack.length === 0) { + break; + } + y += stack.shift(); + lineTo(x, y); + } + break; + case 7: // vlineto + while (stack.length > 0) { + y += stack.shift(); + lineTo(x, y); + if (stack.length === 0) { + break; + } + x += stack.shift(); + lineTo(x, y); + } + break; + case 8: // rrcurveto + while (stack.length > 0) { + var xa = x + stack.shift(), ya = y + stack.shift(); + var xb = xa + stack.shift(), yb = ya + stack.shift(); + x = xb + stack.shift(); y = yb + stack.shift(); + bezierCurveTo(xa, ya, xb, yb, x, y); + } + break; + case 10: // callsubr + var n = stack.pop() + font.subrsBias; + var subrCode = font.subrs[n]; + if (subrCode) { + parse(subrCode); + } + break; + case 11: // return + return; + case 12: + v = code[i++]; + switch (v) { + case 34: // flex + var xa = x + stack.shift(); + var xb = xa + stack.shift(), y1 = y + stack.shift(); + x = xb + stack.shift(); + bezierCurveTo(xa, y, xb, y1, x, y1); + var xa = x + stack.shift(); + var xb = xa + stack.shift(); + x = xb + stack.shift(); + bezierCurveTo(xa, y1, xb, y, x, y); + break; + case 35: // flex + var xa = x + stack.shift(), ya = y + stack.shift(); + var xb = xa + stack.shift(), yb = ya + stack.shift(); + x = xb + stack.shift(); y = yb + stack.shift(); + bezierCurveTo(xa, ya, xb, yb, x, y); + var xa = x + stack.shift(), ya = y + stack.shift(); + var xb = xa + stack.shift(), yb = ya + stack.shift(); + x = xb + stack.shift(); y = yb + stack.shift(); + bezierCurveTo(xa, ya, xb, yb, x, y); + stack.pop(); // fd + break; + case 36: // hflex1 + var xa = x + stack.shift(), y1 = y + stack.shift(); + var xb = xa + stack.shift(), y2 = y1 + stack.shift(); + x = xb + stack.shift(); + bezierCurveTo(xa, y1, xb, y2, x, y2); + var xa = x + stack.shift(); + var xb = xa + stack.shift(), y3 = y2 + stack.shift(); + x = xb + stack.shift(); + bezierCurveTo(xa, y2, xb, y3, x, y); + break; + case 37: // flex1 + var x0 = x, y0 = y; + var xa = x + stack.shift(), ya = y + stack.shift(); + var xb = xa + stack.shift(), yb = ya + stack.shift(); + x = xb + stack.shift(); y = yb + stack.shift(); + bezierCurveTo(xa, ya, xb, yb, x, y); + var xa = x + stack.shift(), ya = y + stack.shift(); + var xb = xa + stack.shift(), yb = ya + stack.shift(); + x = xb; y = yb; + if (Math.abs(x - x0) > Math.abs(y - y0)) + x += stack.shift(); + else + y += stack.shift(); + bezierCurveTo(xa, ya, xb, yb, x, y); + break; + default: + error('unknown operator: 12 ' + v); + } + break; + case 14: // endchar + if (stack.length >= 4) { + var achar = stack.pop(); + var bchar = stack.pop(); + y = stack.pop(); + x = stack.pop(); + js.push('c.save();'); + js.push('c.translate('+ x + ',' + y + ');'); + var gid = lookupCmap(font.cmap, String.fromCharCode( + font.glyphNameMap[Encodings.StandardEncoding[achar]])); + compileCharString(font.glyphs[gid], js, font); + js.push('c.restore();'); + + gid = lookupCmap(font.cmap, String.fromCharCode( + font.glyphNameMap[Encodings.StandardEncoding[bchar]])); + compileCharString(font.glyphs[gid], js, font); + } + return; + case 18: // hstemhm + stems += stack.length >> 1; + stackClean = true; + break; + case 19: // hintmask + stems += stack.length >> 1; + i += (stems + 7) >> 3; + stackClean = true; + break; + case 20: // cntrmask + stems += stack.length >> 1; + i += (stems + 7) >> 3; + stackClean = true; + break; + case 21: // rmoveto + y += stack.pop(); + x += stack.pop(); + moveTo(x, y); + stackClean = true; + break; + case 22: // hmoveto + x += stack.pop(); + moveTo(x, y); + stackClean = true; + break; + case 23: // vstemhm + stems += stack.length >> 1; + stackClean = true; + break; + case 24: // rcurveline + while (stack.length > 2) { + var xa = x + stack.shift(), ya = y + stack.shift(); + var xb = xa + stack.shift(), yb = ya + stack.shift(); + x = xb + stack.shift(); y = yb + stack.shift(); + bezierCurveTo(xa, ya, xb, yb, x, y); + } + x += stack.shift(); + y += stack.shift(); + lineTo(x, y); + break; + case 25: // rlinecurve + while (stack.length > 6) { + x += stack.shift(); + y += stack.shift(); + lineTo(x, y); + } + var xa = x + stack.shift(), ya = y + stack.shift(); + var xb = xa + stack.shift(), yb = ya + stack.shift(); + x = xb + stack.shift(); y = yb + stack.shift(); + bezierCurveTo(xa, ya, xb, yb, x, y); + break; + case 26: // vvcurveto + if (stack.length % 2) { + x += stack.shift(); + } + while (stack.length > 0) { + var xa = x, ya = y + stack.shift(); + var xb = xa + stack.shift(), yb = ya + stack.shift(); + x = xb; y = yb + stack.shift(); + bezierCurveTo(xa, ya, xb, yb, x, y); + } + break; + case 27: // hhcurveto + if (stack.length % 2) { + y += stack.shift(); + } + while (stack.length > 0) { + var xa = x + stack.shift(), ya = y; + var xb = xa + stack.shift(), yb = ya + stack.shift(); + x = xb + stack.shift(); y = yb; + bezierCurveTo(xa, ya, xb, yb, x, y); + } + break; + case 28: + stack.push(((code[i] << 24) | (code[i + 1] << 16)) >> 16); + i += 2; + break; + case 29: // callgsubr + var n = stack.pop() + font.gsubrsBias; + var subrCode = font.gsubrs[n]; + if (subrCode) { + parse(subrCode); + } + break; + case 30: // vhcurveto + while (stack.length > 0) { + var xa = x, ya = y + stack.shift(); + var xb = xa + stack.shift(), yb = ya + stack.shift(); + x = xb + stack.shift(); + y = yb + (stack.length === 1 ? stack.shift() : 0); + bezierCurveTo(xa, ya, xb, yb, x, y); + if (stack.length === 0) { + break; + } + + var xa = x + stack.shift(), ya = y; + var xb = xa + stack.shift(), yb = ya + stack.shift(); + y = yb + stack.shift(); + x = xb + (stack.length === 1 ? stack.shift() : 0); + bezierCurveTo(xa, ya, xb, yb, x, y); + } + break; + case 31: // hvcurveto + while (stack.length > 0) { + var xa = x + stack.shift(), ya = y; + var xb = xa + stack.shift(), yb = ya + stack.shift(); + y = yb + stack.shift(); + x = xb + (stack.length === 1 ? stack.shift() : 0); + bezierCurveTo(xa, ya, xb, yb, x, y); + if (stack.length === 0) { + break; + } + + var xa = x, ya = y + stack.shift(); + var xb = xa + stack.shift(), yb = ya + stack.shift(); + x = xb + stack.shift(); + y = yb + (stack.length === 1 ? stack.shift() : 0); + bezierCurveTo(xa, ya, xb, yb, x, y); + } + break; + default: + if (v < 32) + error('unknown operator: ' + v); + if (v < 247) + stack.push(v - 139); + else if (v < 251) + stack.push((v - 247) * 256 + code[i++] + 108); + else if (v < 255) + stack.push(-(v - 251) * 256 - code[i++] - 108); + else { + stack.push(((code[i] << 24) | (code[i + 1] << 16) | + (code[i + 2] << 8) | code[i + 3]) / 65536); + i += 4; + } + break; + } + if (stackClean) { + stack.length = 0; + } + } + } + parse(code); + } + + function TrueTypeCompiled(glyphs, cmap, fontMatrix) { + this.glyphs = glyphs; + this.cmap = cmap; + this.fontMatrix = fontMatrix || [0.000488, 0, 0, 0.000488, 0, 0]; + + this.compiledGlyphs = []; + } + + var noop = function () {}; + + TrueTypeCompiled.prototype = { + getPathGenerator: function (unicode) { + var gid = lookupCmap(this.cmap, unicode); + var fn = this.compiledGlyphs[gid]; + if (!fn) { + this.compiledGlyphs[gid] = fn = this.compileGlyph(this.glyphs[gid]); + } + return fn; + }, + compileGlyph: function (code) { + if (!code || code.length === 0 || code[0] === 14) { + return noop; + } + + var js = []; + js.push('c.save();'); + js.push('c.transform(' + this.fontMatrix.join(',') + ');'); + js.push('c.scale(size, -size);'); + + var stack = [], x = 0, y = 0; + compileGlyf(code, js, this); + + js.push('c.restore();'); + + /*jshint -W054 */ + return new Function('c', 'size', js.join('\n')); + } + }; + + function Type2Compiled(cffInfo, cmap, fontMatrix, glyphNameMap) { + this.glyphs = cffInfo.glyphs; + this.gsubrs = cffInfo.gsubrs || []; + this.subrs = cffInfo.subrs || []; + this.cmap = cmap; + this.glyphNameMap = glyphNameMap || GlyphsUnicode; + this.fontMatrix = fontMatrix || [0.001, 0, 0, 0.001, 0, 0]; + + this.compiledGlyphs = []; + this.gsubrsBias = this.gsubrs.length < 1240 ? 107 : + this.gsubrs.length < 33900 ? 1131 : 32768; + this.subrsBias = this.subrs.length < 1240 ? 107 : + this.subrs.length < 33900 ? 1131 : 32768; + } + + Type2Compiled.prototype = { + getPathGenerator: function (unicode) { + var gid = lookupCmap(this.cmap, unicode); + var fn = this.compiledGlyphs[gid]; + if (!fn) { + this.compiledGlyphs[gid] = fn = this.compileGlyph(this.glyphs[gid]); + } + return fn; + }, + compileGlyph: function (code) { + if (!code || code.length === 0 || code[0] === 14) { + return noop; + } + + var js = []; + js.push('c.save();'); + js.push('c.transform(' + this.fontMatrix.join(',') + ');'); + js.push('c.scale(size, -size);'); + + var stack = [], x = 0, y = 0; + compileCharString(code, js, this); + + js.push('c.restore();'); + + /*jshint -W054 */ + return new Function('c', 'size', js.join('\n')); + } + }; + + return { + create: function FontRendererFactory_create(font) { + var data = new Uint8Array(font.data); + var cmap, glyf, loca, cff, indexToLocFormat, unitsPerEm; + var numTables = getUshort(data, 4); + for (var i = 0, p = 12; i < numTables; i++, p += 16) { + var tag = String.fromCharCode.apply(null, data.subarray(p, p + 4)); + var offset = getLong(data, p + 8); + var length = getLong(data, p + 12); + switch (tag) { + case 'cmap': + cmap = parseCmap(data, offset, offset + length); + break; + case 'glyf': + glyf = data.subarray(offset, offset + length); + break; + case 'loca': + loca = data.subarray(offset, offset + length); + break; + case 'head': + unitsPerEm = getUshort(data, offset + 18); + indexToLocFormat = getUshort(data, offset + 50); + break; + case 'CFF ': + cff = parseCff(data, offset, offset + length); + break; + } + } + + if (glyf) { + var fontMatrix = !unitsPerEm ? font.fontMatrix : + [1 / unitsPerEm, 0, 0, 1 / unitsPerEm, 0, 0]; + return new TrueTypeCompiled( + parseGlyfTable(glyf, loca, indexToLocFormat), cmap, fontMatrix); + } else { + return new Type2Compiled(cff, cmap, font.fontMatrix, font.glyphNameMap); + } + } + }; +})(); + + + var GlyphsUnicode = { A: 0x0041, AE: 0x00C6, AEacute: 0x01FC, AEmacron: 0x01E2, AEsmall: 0xF7E6, Aacute: 0x00C1, Aacutesmall: 0xF7E1, @@ -28456,16 +29695,40 @@ var PDFImage = (function PDFImageClosure temp[newIndex + 1] = pixels[oldIndex + 1]; temp[newIndex + 2] = pixels[oldIndex + 2]; } } } return temp; }; + PDFImage.createMask = function PDFImage_createMask(imgArray, width, height, + inverseDecode) { + var buffer = new Uint8Array(width * height * 4); + var imgArrayPos = 0; + var i, j, mask, buf; + // removing making non-masked pixels transparent + var bufferPos = 3; // alpha component offset + for (i = 0; i < height; i++) { + mask = 0; + for (j = 0; j < width; j++) { + if (!mask) { + buf = imgArray[imgArrayPos++]; + mask = 128; + } + if (!(buf & mask) !== inverseDecode) { + buffer[bufferPos] = 255; + } + bufferPos += 4; + mask >>= 1; + } + } + return {data: buffer, width: width, height: height}; + }; + PDFImage.prototype = { get drawWidth() { if (!this.smask) return this.width; return Math.max(this.width, this.smask.width); }, get drawHeight() { if (!this.smask) @@ -28612,40 +29875,16 @@ var PDFImage = (function PDFImageClosure } } else { buf = new Uint8Array(width * height); for (var i = 0, ii = width * height; i < ii; ++i) buf[i] = 255; } return buf; }, - applyStencilMask: function PDFImage_applyStencilMask(buffer, - inverseDecode) { - var width = this.width, height = this.height; - var bitStrideLength = (width + 7) >> 3; - var imgArray = this.getImageBytes(bitStrideLength * height); - var imgArrayPos = 0; - var i, j, mask, buf; - // removing making non-masked pixels transparent - var bufferPos = 3; // alpha component offset - for (i = 0; i < height; i++) { - mask = 0; - for (j = 0; j < width; j++) { - if (!mask) { - buf = imgArray[imgArrayPos++]; - mask = 128; - } - if (!(buf & mask) === inverseDecode) { - buffer[bufferPos] = 0; - } - bufferPos += 4; - mask >>= 1; - } - } - }, fillRgbaBuffer: function PDFImage_fillRgbaBuffer(buffer, width, height) { var numComps = this.numComps; var originalWidth = this.width; var originalHeight = this.height; var bpc = this.bpc; // rows start at byte boundary; var rowBytes = (originalWidth * numComps * bpc + 7) >> 3; @@ -32346,20 +33585,16 @@ var Linearization = (function Linearizat } }; return Linearization; })(); -// This global variable is used to minimize the memory usage when patterns are -// used. -var temporaryPatternCanvas = null; - var PatternType = { AXIAL: 2, RADIAL: 3 }; var Pattern = (function PatternClosure() { // Constructor should define this.getPattern function Pattern() { @@ -32677,19 +33912,16 @@ var TilingPattern = (function TilingPatt height = Math.min(Math.ceil(Math.abs(height * combinedScale[1])), MAX_PATTERN_SIZE); tmpCanvas.width = width; tmpCanvas.height = height; // set the new canvas element context as the graphics context var tmpCtx = tmpCanvas.getContext('2d'); - // for simulated mozCurrentTransform canvas (normaly setting width/height - // will reset the matrix) - tmpCtx.setTransform(1, 0, 0, 1, 0, 0); var graphics = new CanvasGraphics(tmpCtx, commonObjs, objs); this.setFillAndStrokeStyleToContext(tmpCtx, paintType, color); this.setScale(width, height, xstep, ystep); this.transformToScale(graphics); // transform coordinates to pattern space @@ -32741,21 +33973,17 @@ var TilingPattern = (function TilingPatt context.strokeStyle = cssColor; break; default: error('Unsupported paint type: ' + paintType); } }, getPattern: function TilingPattern_getPattern() { - // The temporary canvas is created only because the memory is released - // more quickly than creating multiple temporary canvases. - if (temporaryPatternCanvas === null) { - temporaryPatternCanvas = createScratchCanvas(1, 1); - } + var temporaryPatternCanvas = CachedCanvases.getCanvas('pattern'); this.createPatternCanvas(temporaryPatternCanvas); var ctx = this.ctx; ctx.setTransform.apply(ctx, this.curMatrix); ctx.transform.apply(ctx, this.matrix); this.scaleToContext(); return ctx.createPattern(temporaryPatternCanvas, 'repeat'); @@ -35257,22 +36485,30 @@ var WorkerMessageHandler = { } function getPdfManager(data) { var pdfManagerPromise = new Promise(); var source = data.source; var disableRange = data.disableRange; if (source.data) { - pdfManager = new LocalPdfManager(source.data, source.password); - pdfManagerPromise.resolve(); + try { + pdfManager = new LocalPdfManager(source.data, source.password); + pdfManagerPromise.resolve(); + } catch (ex) { + pdfManagerPromise.reject(ex); + } return pdfManagerPromise; } else if (source.chunkedViewerLoading) { - pdfManager = new NetworkPdfManager(source, handler); - pdfManagerPromise.resolve(); + try { + pdfManager = new NetworkPdfManager(source, handler); + pdfManagerPromise.resolve(); + } catch (ex) { + pdfManagerPromise.reject(ex); + } return pdfManagerPromise; } var networkManager = new NetworkManager(source.url, { httpHeaders: source.httpHeaders }); var fullRequestXhrId = networkManager.requestFull({ onHeadersReceived: function onHeadersReceived() { @@ -35299,24 +36535,32 @@ var WorkerMessageHandler = { // NOTE: by cancelling the full request, and then issuing range // requests, there will be an issue for sites where you can only // request the pdf once. However, if this is the case, then the // server should not be returning that it can support range requests. networkManager.abortRequest(fullRequestXhrId); source.length = length; - pdfManager = new NetworkPdfManager(source, handler); - pdfManagerPromise.resolve(pdfManager); + try { + pdfManager = new NetworkPdfManager(source, handler); + pdfManagerPromise.resolve(pdfManager); + } catch (ex) { + pdfManagerPromise.reject(ex); + } }, onDone: function onDone(args) { // the data is array, instantiating directly from it - pdfManager = new LocalPdfManager(args.chunk, source.password); - pdfManagerPromise.resolve(); + try { + pdfManager = new LocalPdfManager(args.chunk, source.password); + pdfManagerPromise.resolve(); + } catch (ex) { + pdfManagerPromise.reject(ex); + } }, onError: function onError(status) { if (status == 404) { var exception = new MissingPDFException( 'Missing PDF "' + source.url + '".'); handler.send('MissingPDF', { exception: exception }); } else { @@ -35406,18 +36650,18 @@ var WorkerMessageHandler = { onFailure(ex); return; } pdfManager.requestLoadedStream(); pdfManager.onLoadedStream().then(function() { loadDocument(true).then(onSuccess, onFailure); }); - }); - }); + }, onFailure); + }, onFailure); }); handler.on('GetPageRequest', function wphSetupGetPage(data) { var pageIndex = data.pageIndex; pdfManager.getPage(pageIndex).then(function(page) { var rotatePromise = pdfManager.ensure(page, 'rotate'); var refPromise = pdfManager.ensure(page, 'ref'); var viewPromise = pdfManager.ensure(page, 'view');
--- a/browser/extensions/pdfjs/content/network.js +++ b/browser/extensions/pdfjs/content/network.js @@ -146,42 +146,44 @@ var NetworkManager = (function NetworkMa if (!(xhrId in this.pendingRequests)) { // The XHR request might have been aborted in onHeadersReceived() // callback, in which case we should abort request return; } delete this.pendingRequests[xhrId]; - if (xhr.status === 0) { + // success status == 0 can be on ftp, file and other protocols + if (xhr.status === 0 && /^https?:/i.test(this.url)) { if (pendingRequest.onError) { pendingRequest.onError(xhr.status); } return; } + var xhrStatus = xhr.status || OK_RESPONSE; // From http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.2: // "A server MAY ignore the Range header". This means it's possible to // get a 200 rather than a 206 response from a range request. var ok_response_on_range_request = - xhr.status === OK_RESPONSE && + xhrStatus === OK_RESPONSE && pendingRequest.expectedStatus === PARTIAL_CONTENT_RESPONSE; if (!ok_response_on_range_request && - xhr.status !== pendingRequest.expectedStatus) { + xhrStatus !== pendingRequest.expectedStatus) { if (pendingRequest.onError) { pendingRequest.onError(xhr.status); } return; } this.loadedRequests[xhrId] = true; var chunk = getArrayBuffer(xhr); - if (xhr.status === PARTIAL_CONTENT_RESPONSE) { + if (xhrStatus === PARTIAL_CONTENT_RESPONSE) { var rangeHeader = xhr.getResponseHeader('Content-Range'); var matches = /bytes (\d+)-(\d+)\/(\d+)/.exec(rangeHeader); var begin = parseInt(matches[1], 10); pendingRequest.onDone({ begin: begin, chunk: chunk }); } else {
--- a/browser/extensions/pdfjs/content/web/viewer.css +++ b/browser/extensions/pdfjs/content/web/viewer.css @@ -1124,16 +1124,20 @@ html[dir='rtl'] .outlineItem > a { #sidebarControls { position:absolute; width: 180px; height: 32px; left: 15px; bottom: 35px; } +.canvasWrapper { + overflow: hidden; +} + canvas { margin: auto; display: block; } .page { direction: ltr; width: 816px;
--- a/browser/extensions/pdfjs/content/web/viewer.js +++ b/browser/extensions/pdfjs/content/web/viewer.js @@ -24,16 +24,19 @@ var DEFAULT_SCALE_DELTA = 1.1; var UNKNOWN_SCALE = 0; var CACHE_SIZE = 20; var CSS_UNITS = 96.0 / 72.0; var SCROLLBAR_PADDING = 40; var VERTICAL_PADDING = 5; var MIN_SCALE = 0.25; var MAX_SCALE = 4.0; var SETTINGS_MEMORY = 20; +var HISTORY_DISABLED = false; +var SCALE_SELECT_CONTAINER_PADDING = 8; +var SCALE_SELECT_PADDING = 22; var RenderingStates = { INITIAL: 0, RUNNING: 1, PAUSED: 2, FINISHED: 3 }; var FindStates = { FIND_FOUND: 0, @@ -715,16 +718,304 @@ var PDFFindBar = { if (this.opened) { this.close(); } else { this.open(); } } }; +var PDFHistory = { + initialized: false, + initialDestination: null, + + initialize: function pdfHistoryInitialize(fingerprint) { + if (HISTORY_DISABLED || window.parent !== window) { + // The browsing history is only enabled when the viewer is standalone, + // i.e. not when it is embedded in a page. + return; + } + this.initialized = true; + this.reInitialized = false; + this.allowHashChange = true; + this.historyUnlocked = true; + + this.previousHash = window.location.hash.substring(1); + this.currentBookmark = ''; + this.currentPage = 0; + this.updatePreviousBookmark = false; + this.previousBookmark = ''; + this.nextHashParam = ''; + + this.fingerprint = fingerprint; + this.currentUid = this.uid = 0; + this.current = {}; + + var state = window.history.state; + if (this._isStateObjectDefined(state)) { + // This case corresponds to navigating back to the document + // from another page in the browser history. + if (state.target.dest) { + this.initialDestination = state.target.dest; + } else { + PDFView.initialBookmark = state.target.hash; + } + this.currentUid = state.uid; + this.uid = state.uid + 1; + this.current = state.target; + } else { + // This case corresponds to the loading of a new document. + if (state && state.fingerprint && + this.fingerprint !== state.fingerprint) { + // Reinitialize the browsing history when a new document + // is opened in the web viewer. + this.reInitialized = true; + } + window.history.replaceState({ fingerprint: this.fingerprint }, '', ''); + } + + var self = this; + window.addEventListener('popstate', function pdfHistoryPopstate(evt) { + evt.preventDefault(); + evt.stopPropagation(); + + if (!self.historyUnlocked) { + return; + } + if (evt.state) { + // Move back/forward in the history. + self._goTo(evt.state); + } else { + // Handle the user modifying the hash of a loaded document. + self.previousHash = window.location.hash.substring(1); + if (self.uid === 0) { + var previousParams = (self.previousHash && self.currentBookmark && + self.previousHash !== self.currentBookmark) ? + { hash: self.currentBookmark } : { page: 1 }; + self.historyUnlocked = false; + self.allowHashChange = false; + window.history.back(); + self._pushToHistory(previousParams, false, true); + window.history.forward(); + self.historyUnlocked = true; + } + self._pushToHistory({ hash: self.previousHash }, false, true); + if (self.currentBookmark) { + self.previousBookmark = self.currentBookmark; + } + } + }, false); + + window.addEventListener('beforeunload', + function pdfHistoryBeforeunload(evt) { + var previousParams = self._getPreviousParams(null, true); + if (previousParams) { + self._pushToHistory(previousParams, false); + } + if (PDFView.isPresentationMode) { + // Prevent the user from accidentally navigating away from + // the document when presentation mode is active. + evt.preventDefault(); + } + }, false); + }, + + _isStateObjectDefined: function pdfHistory_isStateObjectDefined(state) { + return (state && state.uid >= 0 && + state.fingerprint && this.fingerprint === state.fingerprint && + state.target && state.target.hash) ? true : false; + }, + + get isHashChangeUnlocked() { + if (!this.initialized) { + return true; + } + // If the current hash changes when moving back/forward in the history, + // this will trigger a 'popstate' event *as well* as a 'hashchange' event. + // Since the hash generally won't correspond to the exact the position + // stored in the history's state object, triggering the 'hashchange' event + // can thus corrupt the browser history. + // + // When the hash changes during a 'popstate' event, we *only* prevent the + // first 'hashchange' event and immediately reset allowHashChange. + // If it is not reset, the user would not be able to change the hash. + + var temp = this.allowHashChange; + this.allowHashChange = true; + return temp; + }, + + updateCurrentBookmark: function pdfHistoryUpdateCurrentBookmark(bookmark, + pageNum) { + if (this.initialized) { + this.currentBookmark = bookmark.substring(1); + this.currentPage = pageNum | 0; + if (this.updatePreviousBookmark) { + this.previousBookmark = this.currentBookmark; + this.updatePreviousBookmark = false; + } + } + }, + + updateNextHashParam: function pdfHistoryUpdateNextHashParam(param) { + if (this.initialized) { + this.nextHashParam = param; + } + }, + + push: function pdfHistoryPush(params, isInitialBookmark) { + if (!(this.initialized && this.historyUnlocked)) { + return; + } + if (params.dest && !params.hash) { + params.hash = (this.current.hash && this.current.dest && + this.current.dest === params.dest) ? + this.current.hash : + PDFView.getDestinationHash(params.dest).split('#')[1]; + } + if (params.page) { + params.page |= 0; + } + if (isInitialBookmark && this.uid === 0) { + this._pushToHistory(params, false); + this.previousHash = window.location.hash.substring(1); + this.updatePreviousBookmark = this.nextHashParam ? false : true; + return; + } + if (this.nextHashParam && this.nextHashParam === params.hash) { + this.nextHashParam = null; + this.updatePreviousBookmark = true; + return; + } + + if (params.hash) { + if (this.current.hash) { + if (this.current.hash !== params.hash) { + this._pushToHistory(params, true); + } else if (!this.current.page && params.page) { + this._pushToHistory(params, false, true); + } + } else { + this._pushToHistory(params, true); + } + } else if (this.current.page && params.page && + this.current.page !== params.page) { + this._pushToHistory(params, true); + } + }, + + _getPreviousParams: function pdfHistory_getPreviousParams(onlyCheckPage, + beforeUnload) { + if (!(this.currentBookmark && this.currentPage)) { + return null; + } + if ((!this.current.dest && !onlyCheckPage) || beforeUnload) { + if (this.previousBookmark === this.currentBookmark) { + return null; + } + } else if (this.current.page) { + if (this.current.page === this.currentPage) { + return null; + } + } else { + return null; + } + var params = { hash: this.currentBookmark, page: this.currentPage }; + if (PDFView.isPresentationMode) { + params.hash = null; + } + return params; + }, + + _stateObj: function pdfHistory_stateObj(params) { + return { fingerprint: this.fingerprint, uid: this.uid, target: params }; + }, + + _pushToHistory: function pdfHistory_pushToHistory(params, + addPrevious, overwrite) { + if (!this.initialized) { + return; + } + if (!params.hash && params.page) { + params.hash = ('page=' + params.page); + } + if (addPrevious && !overwrite) { + var previousParams = this._getPreviousParams(); + if (previousParams) { + this._pushToHistory(previousParams, false); + } + } + if (overwrite || this.uid === 0) { + window.history.replaceState(this._stateObj(params), '', ''); + } else { + window.history.pushState(this._stateObj(params), '', ''); + } + this.currentUid = this.uid++; + this.current = params; + this.updatePreviousBookmark = true; + }, + + _goTo: function pdfHistory_goTo(state) { + if (!(this.initialized && this.historyUnlocked && + this._isStateObjectDefined(state))) { + return; + } + if (!this.reInitialized && state.uid < this.currentUid) { + var previousParams = this._getPreviousParams(true); + if (previousParams) { + this._pushToHistory(this.current, false); + this._pushToHistory(previousParams, false); + this.currentUid = state.uid; + window.history.back(); + return; + } + } + this.historyUnlocked = false; + + if (state.target.dest) { + PDFView.navigateTo(state.target.dest); + } else { + PDFView.setHash(state.target.hash); + } + this.currentUid = state.uid; + if (state.uid > this.uid) { + this.uid = state.uid; + } + this.current = state.target; + this.updatePreviousBookmark = true; + + var currentHash = window.location.hash.substring(1); + if (this.previousHash !== currentHash) { + this.allowHashChange = false; + } + this.previousHash = currentHash; + + this.historyUnlocked = true; + }, + + back: function pdfHistoryBack() { + this.go(-1); + }, + + forward: function pdfHistoryForward() { + this.go(1); + }, + + go: function pdfHistoryGo(direction) { + if (this.initialized && this.historyUnlocked) { + var state = window.history.state; + if (direction === -1 && state && state.uid > 0) { + window.history.back(); + } else if (direction === 1 && state && state.uid < (this.uid - 1)) { + window.history.forward(); + } + } + } +}; + var PDFView = { pages: [], thumbnails: [], currentScale: UNKNOWN_SCALE, currentScaleValue: null, initialBookmark: document.location.hash.substring(1), startedTextExtraction: false, pageText: [], @@ -910,18 +1201,19 @@ var PDFView = { return value; }, get supportsFullscreen() { var doc = document.documentElement; var support = doc.requestFullscreen || doc.mozRequestFullScreen || doc.webkitRequestFullScreen; - // Disable presentation mode button if we're in an iframe - if (window.parent !== window) { + if (document.fullscreenEnabled === false || + document.mozFullScreenEnabled === false || + document.webkitFullscreenEnabled === false ) { support = false; } Object.defineProperty(this, 'supportsFullscreen', { value: support, enumerable: true, configurable: true, writable: false }); return support; @@ -1164,36 +1456,38 @@ var PDFView = { PDFView.download(); }); }, navigateTo: function pdfViewNavigateTo(dest) { var self = this; PDFJS.Promise.all([this.pagesPromise, this.destinationsPromise]).then(function() { + var destString = ''; if (typeof dest === 'string') { + destString = dest; dest = self.destinations[dest]; } if (!(dest instanceof Array)) { return; // invalid destination } // dest array looks like that: <page-ref> </XYZ|FitXXX> <args..> var destRef = dest[0]; var pageNumber = destRef instanceof Object ? self.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] : (destRef + 1); if (pageNumber) { if (pageNumber > self.pages.length) { pageNumber = self.pages.length; } - self.page = pageNumber; - if (!self.isPresentationMode) { // Avoid breaking presentation mode. - var currentPage = self.pages[pageNumber - 1]; - currentPage.scrollIntoView(dest); - } + var currentPage = self.pages[pageNumber - 1]; + currentPage.scrollIntoView(dest); + + // Update the browsing history. + PDFHistory.push({ dest: dest, hash: destString, page: pageNumber }); } }); }, getDestinationHash: function pdfViewGetDestinationHash(dest) { if (typeof dest === 'string') return PDFView.getAnchorUrl('#' + escape(dest)); if (dest instanceof Array) { @@ -1201,18 +1495,22 @@ var PDFView = { var pageNumber = destRef instanceof Object ? this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] : (destRef + 1); if (pageNumber) { var pdfOpenParams = PDFView.getAnchorUrl('#page=' + pageNumber); var destKind = dest[1]; if (typeof destKind === 'object' && 'name' in destKind && destKind.name == 'XYZ') { - var scale = (dest[4] || this.currentScale); - pdfOpenParams += '&zoom=' + (scale * 100); + var scale = (dest[4] || this.currentScaleValue); + var scaleNumber = parseFloat(scale); + if (scaleNumber) { + scale = scaleNumber * 100; + } + pdfOpenParams += '&zoom=' + scale; if (dest[2] || dest[3]) { pdfOpenParams += ',' + (dest[2] || 0) + ',' + (dest[3] || 0); } } return pdfOpenParams; } } return ''; @@ -1398,18 +1696,21 @@ var PDFView = { var storedHash = null; if (store.get('exists', false)) { var pageNum = store.get('page', '1'); var zoom = store.get('zoom', PDFView.currentScale); var left = store.get('scrollLeft', '0'); var top = store.get('scrollTop', '0'); storedHash = 'page=' + pageNum + '&zoom=' + zoom + ',' + - left + ',' + top; + left + ',' + top; } + // Initialize the browsing history. + PDFHistory.initialize(self.documentFingerprint); + self.setInitialView(storedHash, scale); // Make all navigation keys work on document load, // unless the viewer is embedded in another page. if (window.parent === window) { PDFView.container.focus(); PDFView.container.blur(); } @@ -1483,23 +1784,26 @@ var PDFView = { }); }, setInitialView: function pdfViewSetInitialView(storedHash, scale) { // Reset the current scale, as otherwise the page's scale might not get // updated if the zoom level stayed the same. this.currentScale = 0; this.currentScaleValue = null; - if (this.initialBookmark) { + if (PDFHistory.initialDestination) { + this.navigateTo(PDFHistory.initialDestination); + PDFHistory.initialDestination = null; + } else if (this.initialBookmark) { this.setHash(this.initialBookmark); + PDFHistory.push({ hash: this.initialBookmark }, !!this.initialBookmark); this.initialBookmark = null; - } - else if (storedHash) + } else if (storedHash) { this.setHash(storedHash); - else if (scale) { + } else if (scale) { this.parseScale(scale, true); this.page = 1; } if (PDFView.currentScale === UNKNOWN_SCALE) { // Scale was not initialized: invalid bookmark or scale was not specified. // Setting the default one. this.parseScale(DEFAULT_SCALE, true); @@ -1591,16 +1895,17 @@ var PDFView = { setHash: function pdfViewSetHash(hash) { if (!hash) return; if (hash.indexOf('=') >= 0) { var params = PDFView.parseQueryString(hash); // borrowing syntax from "Parameters for Opening PDF Files" if ('nameddest' in params) { + PDFHistory.updateNextHashParam(params.nameddest); PDFView.navigateTo(params.nameddest); return; } if ('page' in params) { var pageNumber = (params.page | 0) || 1; if ('zoom' in params) { var zoomArgs = params.zoom.split(','); // scale,left,top // building destination array @@ -1629,20 +1934,22 @@ var PDFView = { toggle.click(); } this.switchSidebarView(params.pagemode === 'thumbs' ? 'thumbs' : 'outline'); } else if (params.pagemode === 'none' && this.sidebarOpen) { toggle.click(); } } - } else if (/^\d+$/.test(hash)) // page number + } else if (/^\d+$/.test(hash)) { // page number this.page = hash; - else // named destination + } else { // named destination + PDFHistory.updateNextHashParam(unescape(hash)); PDFView.navigateTo(unescape(hash)); + } }, switchSidebarView: function pdfViewSwitchSidebarView(view) { var thumbsView = document.getElementById('thumbnailView'); var outlineView = document.getElementById('outlineView'); var thumbsButton = document.getElementById('viewThumbnail'); var outlineButton = document.getElementById('viewOutline'); @@ -1808,29 +2115,31 @@ var PDFView = { wrapper.requestFullscreen(); } else if (document.documentElement.mozRequestFullScreen) { wrapper.mozRequestFullScreen(); } else if (document.documentElement.webkitRequestFullScreen) { wrapper.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); } else { return false; } - + return true; + }, + + enterPresentationMode: function pdfViewEnterPresentationMode() { this.isPresentationMode = true; var currentPage = this.pages[this.page - 1]; this.previousScale = this.currentScaleValue; this.parseScale('page-fit', true); // Wait for presentation mode to take effect setTimeout(function() { currentPage.scrollIntoView(); }, 0); this.showPresentationControls(); - return true; }, exitPresentationMode: function pdfViewExitPresentationMode() { this.isPresentationMode = false; this.parseScale(this.previousScale); this.page = this.page; this.clearMouseScrollState(); this.hidePresentationControls(); @@ -2102,16 +2411,20 @@ var PageView = function pageView(contain return this.viewport.convertToPdfPoint(x, y); }; this.scrollIntoView = function pageViewScrollIntoView(dest) { if (!dest) { scrollIntoView(div); return; } + if (PDFView.isPresentationMode) { // Avoid breaking presentation mode. + PDFView.page = id; + return; + } var x = 0, y = 0; var width = 0, height = 0, widthScale, heightScale; var scale = 0; switch (dest[1].name) { case 'XYZ': x = dest[2]; y = dest[3]; @@ -2199,22 +2512,31 @@ var PageView = function pageView(contain } if (this.renderingState !== RenderingStates.INITIAL) { console.error('Must be in new state before drawing'); } this.renderingState = RenderingStates.RUNNING; + var viewport = this.viewport; + // Wrap the canvas so if it has a css transform for highdpi the overflow + // will be hidden in FF. + var canvasWrapper = document.createElement('div'); + canvasWrapper.style.width = div.style.width; + canvasWrapper.style.height = div.style.height; + canvasWrapper.classList.add('canvasWrapper'); + var canvas = document.createElement('canvas'); canvas.id = 'page' + this.id; - div.appendChild(canvas); + canvasWrapper.appendChild(canvas); + div.appendChild(canvasWrapper); this.canvas = canvas; - var scale = this.scale, viewport = this.viewport; + var scale = this.scale; var outputScale = PDFView.getOutputScale(); canvas.width = Math.floor(viewport.width) * outputScale.sx; canvas.height = Math.floor(viewport.height) * outputScale.sy; var textLayerDiv = null; if (!PDFJS.disableTextLayer) { textLayerDiv = document.createElement('div'); textLayerDiv.className = 'textLayer'; @@ -2258,17 +2580,17 @@ var PageView = function pageView(contain self.renderingState = RenderingStates.FINISHED; if (self.loadingIconDiv) { div.removeChild(self.loadingIconDiv); delete self.loadingIconDiv; } if (checkIfDocumentFontsUsed && PDFView.pdfDocument.embeddedFontsUsed && - !PDFView.supportsDocumentFonts) { + PDFJS.disableFontFace) { console.error(mozL10n.get('web_fonts_disabled', null, 'Web fonts are disabled: unable to use embedded PDF fonts.')); PDFView.fallback(); } if (self.textLayer && self.textLayer.textDivs && self.textLayer.textDivs.length > 0 && !PDFView.supportsDocumentColors) { console.error(mozL10n.get('document_colors_disabled', null, @@ -3022,16 +3344,23 @@ document.addEventListener('DOMContentLoa if ('disableRange' in hashParams) { PDFJS.disableRange = (hashParams['disableRange'] === 'true'); } if ('disableAutoFetch' in hashParams) { PDFJS.disableAutoFetch = (hashParams['disableAutoFetch'] === 'true'); } + if ('disableFontFace' in hashParams) { + PDFJS.disableFontFace = (hashParams['disableFontFace'] === 'true'); + } + + if (!PDFView.supportsDocumentFonts) { + PDFJS.disableFontFace = true; + } if ('textLayer' in hashParams) { switch (hashParams['textLayer']) { case 'off': PDFJS.disableTextLayer = true; break; case 'visible': case 'shadow': @@ -3243,29 +3572,34 @@ function updateViewarea() { store.set('exists', true); store.set('page', pageNumber); store.set('zoom', normalizedScaleValue); store.set('scrollLeft', Math.round(topLeft[0])); store.set('scrollTop', Math.round(topLeft[1])); }); var href = PDFView.getAnchorUrl(pdfOpenParams); document.getElementById('viewBookmark').href = href; + + // Update the current bookmark in the browsing history. + PDFHistory.updateCurrentBookmark(pdfOpenParams, pageNumber); } window.addEventListener('resize', function webViewerResize(evt) { if (PDFView.initialized && (document.getElementById('pageWidthOption').selected || document.getElementById('pageFitOption').selected || document.getElementById('pageAutoOption').selected)) PDFView.parseScale(document.getElementById('scaleSelect').value); updateViewarea(); }); window.addEventListener('hashchange', function webViewerHashchange(evt) { - PDFView.setHash(document.location.hash.substring(1)); + if (PDFHistory.isHashChangeUnlocked) { + PDFView.setHash(document.location.hash.substring(1)); + } }); window.addEventListener('change', function webViewerChange(evt) { var files = evt.target.files; if (!files || files.length === 0) return; // Read the local file into a Uint8Array. @@ -3299,25 +3633,29 @@ function selectScaleOption(value) { } return predefinedValueFound; } window.addEventListener('localized', function localized(evt) { document.getElementsByTagName('html')[0].dir = mozL10n.getDirection(); // Adjust the width of the zoom box to fit the content. - PDFView.animationStartedPromise.then( - function() { - var container = document.getElementById('scaleSelectContainer'); + // Note: This is only done if the zoom box is actually visible, + // since otherwise element.clientWidth will return 0. + PDFView.animationStartedPromise.then(function() { + var container = document.getElementById('scaleSelectContainer'); + if (container.clientWidth > 0) { var select = document.getElementById('scaleSelect'); select.setAttribute('style', 'min-width: inherit;'); - var width = select.clientWidth + 8; - select.setAttribute('style', 'min-width: ' + (width + 20) + 'px;'); + var width = select.clientWidth + SCALE_SELECT_CONTAINER_PADDING; + select.setAttribute('style', 'min-width: ' + + (width + SCALE_SELECT_PADDING) + 'px;'); container.setAttribute('style', 'min-width: ' + width + 'px; ' + 'max-width: ' + width + 'px;'); + } }); }, true); window.addEventListener('scalechange', function scalechange(evt) { document.getElementById('zoomOut').disabled = (evt.scale === MIN_SCALE); document.getElementById('zoomIn').disabled = (evt.scale === MAX_SCALE); var customScaleOption = document.getElementById('customScaleOption'); @@ -3542,24 +3880,41 @@ window.addEventListener('keydown', funct break; case 82: // 'r' PDFView.rotatePages(90); break; } } - if (cmd == 4) { // shift-key + if (cmd === 4) { // shift-key switch (evt.keyCode) { case 82: // 'r' PDFView.rotatePages(-90); break; } } + if (cmd === 2) { // alt-key + switch (evt.keyCode) { + case 37: // left arrow + if (PDFView.isPresentationMode) { + PDFHistory.back(); + handled = true; + } + break; + case 39: // right arrow + if (PDFView.isPresentationMode) { + PDFHistory.forward(); + handled = true; + } + break; + } + } + if (handled) { evt.preventDefault(); PDFView.clearMouseScrollState(); } }); window.addEventListener('beforeprint', function beforePrint(evt) { PDFView.beforePrint(); @@ -3570,17 +3925,19 @@ window.addEventListener('afterprint', fu }); (function presentationModeClosure() { function presentationModeChange(e) { var isPresentationMode = document.fullscreenElement || document.mozFullScreen || document.webkitIsFullScreen; - if (!isPresentationMode) { + if (isPresentationMode) { + PDFView.enterPresentationMode(); + } else { PDFView.exitPresentationMode(); } } window.addEventListener('fullscreenchange', presentationModeChange, false); window.addEventListener('mozfullscreenchange', presentationModeChange, false); window.addEventListener('webkitfullscreenchange', presentationModeChange, false);