app/assets/javascripts/pdfjs_viewer/pdfjs/pdf.combined.js in pdfjs_viewer-rails-0.0.6 vs app/assets/javascripts/pdfjs_viewer/pdfjs/pdf.combined.js in pdfjs_viewer-rails-0.0.7

- old
+ new

@@ -1,7 +1,5 @@ -/* -*- 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 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -20,37 +18,18 @@ // Initializing PDFJS global object (if still undefined) if (typeof PDFJS === 'undefined') { (typeof window !== 'undefined' ? window : this).PDFJS = {}; } -PDFJS.version = '1.0.907'; -PDFJS.build = 'e9072ac'; +PDFJS.version = '1.3.91'; +PDFJS.build = 'd1e83b5'; (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 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * 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. - */ -/* globals Cmd, ColorSpace, Dict, MozBlobBuilder, Name, PDFJS, Ref, URL, - Promise */ -'use strict'; var globalScope = (typeof window === 'undefined') ? this : window; var isWorker = (typeof window === 'undefined'); @@ -74,15 +53,59 @@ RGB_24BPP: 2, RGBA_32BPP: 3 }; var AnnotationType = { - WIDGET: 1, - TEXT: 2, - LINK: 3 + TEXT: 1, + LINK: 2, + FREETEXT: 3, + LINE: 4, + SQUARE: 5, + CIRCLE: 6, + POLYGON: 7, + POLYLINE: 8, + HIGHLIGHT: 9, + UNDERLINE: 10, + SQUIGGLY: 11, + STRIKEOUT: 12, + STAMP: 13, + CARET: 14, + INK: 15, + POPUP: 16, + FILEATTACHMENT: 17, + SOUND: 18, + MOVIE: 19, + WIDGET: 20, + SCREEN: 21, + PRINTERMARK: 22, + TRAPNET: 23, + WATERMARK: 24, + THREED: 25, + REDACT: 26 }; +var AnnotationFlag = { + INVISIBLE: 0x01, + HIDDEN: 0x02, + PRINT: 0x04, + NOZOOM: 0x08, + NOROTATE: 0x10, + NOVIEW: 0x20, + READONLY: 0x40, + LOCKED: 0x80, + TOGGLENOVIEW: 0x100, + LOCKEDCONTENTS: 0x200 +}; + +var AnnotationBorderStyleType = { + SOLID: 1, + DASHED: 2, + BEVELED: 3, + INSET: 4, + UNDERLINE: 5 +}; + var StreamType = { UNKNOWN: 0, FLATE: 1, LZW: 2, DCT: 3, @@ -234,25 +257,22 @@ if (PDFJS.verbosity >= PDFJS.VERBOSITY_LEVELS.warnings) { console.log('Warning: ' + msg); } } +// Deprecated API function -- treated as warnings. +function deprecated(details) { + warn('Deprecated API usage: ' + details); +} + // Fatal errors that should trigger the fallback UI and halt execution by // throwing an exception. function error(msg) { - // If multiple arguments were passed, pass them all to the log function. - if (arguments.length > 1) { - var logArguments = ['Error:']; - logArguments.push.apply(logArguments, arguments); - console.log.apply(console, logArguments); - // Join the arguments into a single string for the lines below. - msg = [].join.call(arguments, ' '); - } else { + if (PDFJS.verbosity >= PDFJS.VERBOSITY_LEVELS.errors) { console.log('Error: ' + msg); + console.log(backtrace()); } - console.log(backtrace()); - UnsupportedManager.notify(UNSUPPORTED_FEATURES.unknown); throw new Error(msg); } function backtrace() { try { @@ -275,55 +295,17 @@ smask: 'smask', shadingPattern: 'shadingPattern', font: 'font' }; -var UnsupportedManager = PDFJS.UnsupportedManager = - (function UnsupportedManagerClosure() { - var listeners = []; - return { - listen: function (cb) { - listeners.push(cb); - }, - notify: function (featureId) { - warn('Unsupported feature "' + featureId + '"'); - for (var i = 0, ii = listeners.length; i < ii; i++) { - listeners[i](featureId); - } - } - }; -})(); - // Combines two URLs. The baseUrl shall be absolute URL. If the url is an // absolute URL, it will be returned as is. function combineUrl(baseUrl, url) { if (!url) { return baseUrl; } - if (/^[a-z][a-z0-9+\-.]*:/i.test(url)) { - return url; - } - var i; - if (url.charAt(0) === '/') { - // absolute path - i = baseUrl.indexOf('://'); - if (url.charAt(1) === '/') { - ++i; - } else { - i = baseUrl.indexOf('/', i + 3); - } - return baseUrl.substring(0, i) + url; - } else { - // relative path - var pathLength = baseUrl.length; - i = baseUrl.lastIndexOf('#'); - pathLength = i >= 0 ? i : pathLength; - i = baseUrl.lastIndexOf('?', pathLength); - pathLength = i >= 0 ? i : pathLength; - var prefixLength = baseUrl.lastIndexOf('/', pathLength); - return baseUrl.substring(0, prefixLength + 1) + url; - } + return new URL(url, baseUrl).href; } // Validates if URL is safe and allowed, e.g. to avoid XSS. function isValidUrl(url, allowRelative) { if (!url) { @@ -339,10 +321,11 @@ switch (protocol) { case 'http': case 'https': case 'ftp': case 'mailto': + case 'tel': return true; default: return false; } } @@ -353,11 +336,53 @@ enumerable: true, configurable: true, writable: false }); return value; } +PDFJS.shadow = shadow; +var LinkTarget = PDFJS.LinkTarget = { + NONE: 0, // Default value. + SELF: 1, + BLANK: 2, + PARENT: 3, + TOP: 4, +}; +var LinkTargetStringMap = [ + '', + '_self', + '_blank', + '_parent', + '_top' +]; + +function isExternalLinkTargetSet() { + if (PDFJS.openExternalLinksInNewWindow) { + deprecated('PDFJS.openExternalLinksInNewWindow, please use ' + + '"PDFJS.externalLinkTarget = PDFJS.LinkTarget.BLANK" instead.'); + if (PDFJS.externalLinkTarget === LinkTarget.NONE) { + PDFJS.externalLinkTarget = LinkTarget.BLANK; + } + // Reset the deprecated parameter, to suppress further warnings. + PDFJS.openExternalLinksInNewWindow = false; + } + switch (PDFJS.externalLinkTarget) { + case LinkTarget.NONE: + return false; + case LinkTarget.SELF: + case LinkTarget.BLANK: + case LinkTarget.PARENT: + case LinkTarget.TOP: + return true; + } + warn('PDFJS.externalLinkTarget is invalid: ' + PDFJS.externalLinkTarget); + // Reset the external link target, to suppress further warnings. + PDFJS.externalLinkTarget = LinkTarget.NONE; + return false; +} +PDFJS.isExternalLinkTargetSet = isExternalLinkTargetSet; + var PasswordResponses = PDFJS.PasswordResponses = { NEED_PASSWORD: 1, INCORRECT_PASSWORD: 2 }; @@ -468,10 +493,12 @@ return XRefParseException; })(); function bytesToString(bytes) { + assert(bytes !== null && typeof bytes === 'object' && + bytes.length !== undefined, 'Invalid argument for bytesToString'); var length = bytes.length; var MAX_ARGUMENT_COUNT = 8192; if (length < MAX_ARGUMENT_COUNT) { return String.fromCharCode.apply(null, bytes); } @@ -483,10 +510,11 @@ } return strBuf.join(''); } function stringToBytes(str) { + assert(typeof str === 'string', 'Invalid argument for stringToBytes'); var length = str.length; var bytes = new Uint8Array(length); for (var i = 0; i < length; ++i) { bytes[i] = str.charCodeAt(i) & 0xFF; } @@ -534,11 +562,11 @@ get: function PDFJS_isLittleEndian() { return shadow(PDFJS, 'isLittleEndian', isLittleEndian()); } }); - // Lazy test if the userAgant support CanvasTypedArrays + // Lazy test if the userAgent support CanvasTypedArrays function hasCanvasTypedArrays() { var canvas = document.createElement('canvas'); canvas.width = canvas.height = 1; var ctx = canvas.getContext('2d'); var imageData = ctx.createImageData(1, 1); @@ -599,14 +627,14 @@ var rgbBuf = ['rgb(', 0, ',', 0, ',', 0, ')']; // makeCssRgb() can be called thousands of times. Using |rgbBuf| avoids // creating many intermediate strings. - Util.makeCssRgb = function Util_makeCssRgb(rgb) { - rgbBuf[1] = rgb[0]; - rgbBuf[3] = rgb[1]; - rgbBuf[5] = rgb[2]; + Util.makeCssRgb = function Util_makeCssRgb(r, g, b) { + rgbBuf[1] = r; + rgbBuf[3] = g; + rgbBuf[5] = b; return rgbBuf.join(''); }; // Concatenates two transformation matrices together and returns the result. Util.transform = function Util_transform(m1, m2) { @@ -973,10 +1001,14 @@ function stringToUTF8String(str) { return decodeURIComponent(escape(str)); } +function utf8StringToString(str) { + return unescape(encodeURIComponent(str)); +} + function isEmptyObj(obj) { for (var key in obj) { return false; } return true; @@ -996,14 +1028,10 @@ function isString(v) { return typeof v === 'string'; } -function isNull(v) { - return v === null; -} - function isName(v) { return v instanceof Name; } function isCmd(v, cmd) { @@ -1474,30 +1502,24 @@ } return buffer; }; })(); -function MessageHandler(name, comObj) { - this.name = name; +function MessageHandler(sourceName, targetName, comObj) { + this.sourceName = sourceName; + this.targetName = targetName; this.comObj = comObj; this.callbackIndex = 1; this.postMessageTransfers = true; var callbacksCapabilities = this.callbacksCapabilities = {}; var ah = this.actionHandler = {}; - ah['console_log'] = [function ahConsoleLog(data) { - console.log.apply(console, data); - }]; - ah['console_error'] = [function ahConsoleError(data) { - console.error.apply(console, data); - }]; - ah['_unsupported_feature'] = [function ah_unsupportedFeature(data) { - UnsupportedManager.notify(data); - }]; - - comObj.onmessage = function messageHandlerComObjOnMessage(event) { + this._onComObjOnMessage = function messageHandlerComObjOnMessage(event) { var data = event.data; + if (data.targetName !== this.sourceName) { + return; + } if (data.isReply) { var callbackId = data.callbackId; if (data.callbackId in callbacksCapabilities) { var callback = callbacksCapabilities[callbackId]; delete callbacksCapabilities[callbackId]; @@ -1510,20 +1532,30 @@ error('Cannot resolve callback ' + callbackId); } } else if (data.action in ah) { var action = ah[data.action]; if (data.callbackId) { + var sourceName = this.sourceName; + var targetName = data.sourceName; Promise.resolve().then(function () { return action[0].call(action[1], data.data); }).then(function (result) { comObj.postMessage({ + sourceName: sourceName, + targetName: targetName, isReply: true, callbackId: data.callbackId, data: result }); }, function (reason) { + if (reason instanceof Error) { + // Serialize error to avoid "DataCloneError" + reason = reason + ''; + } comObj.postMessage({ + sourceName: sourceName, + targetName: targetName, isReply: true, callbackId: data.callbackId, error: reason }); }); @@ -1531,11 +1563,12 @@ action[0].call(action[1], data.data); } } else { error('Unknown action from worker: ' + data.action); } - }; + }.bind(this); + comObj.addEventListener('message', this._onComObjOnMessage); } MessageHandler.prototype = { on: function messageHandlerOn(actionName, handler, scope) { var ah = this.actionHandler; @@ -1550,10 +1583,12 @@ * @param {JSON} data JSON data to send. * @param {Array} [transfers] Optional list of transfers/ArrayBuffers */ send: function messageHandlerSend(actionName, data, transfers) { var message = { + sourceName: this.sourceName, + targetName: this.targetName, action: actionName, data: data }; this.postMessage(message, transfers); }, @@ -1567,10 +1602,12 @@ */ sendWithPromise: function messageHandlerSendWithPromise(actionName, data, transfers) { var callbackId = this.callbackIndex++; var message = { + sourceName: this.sourceName, + targetName: this.targetName, action: actionName, data: data, callbackId: callbackId }; var capability = createPromiseCapability(); @@ -1592,10 +1629,14 @@ if (transfers && this.postMessageTransfers) { this.comObj.postMessage(message, transfers); } else { this.comObj.postMessage(message); } + }, + + destroy: function () { + this.comObj.removeEventListener('message', this._onComObjOnMessage); } }; function loadJpegStream(id, imageUrl, objs) { var img = new Image(); @@ -1607,11 +1648,630 @@ warn('Error during JPEG image loading'); }); img.src = imageUrl; } + // Polyfill from https://github.com/Polymer/URL +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +(function checkURLConstructor(scope) { + /* jshint ignore:start */ + // feature detect for URL constructor + var hasWorkingUrl = false; + if (typeof URL === 'function' && ('origin' in URL.prototype)) { + try { + var u = new URL('b', 'http://a'); + u.pathname = 'c%20d'; + hasWorkingUrl = u.href === 'http://a/c%20d'; + } catch(e) {} + } + + if (hasWorkingUrl) + return; + + var relative = Object.create(null); + relative['ftp'] = 21; + relative['file'] = 0; + relative['gopher'] = 70; + relative['http'] = 80; + relative['https'] = 443; + relative['ws'] = 80; + relative['wss'] = 443; + + var relativePathDotMapping = Object.create(null); + relativePathDotMapping['%2e'] = '.'; + relativePathDotMapping['.%2e'] = '..'; + relativePathDotMapping['%2e.'] = '..'; + relativePathDotMapping['%2e%2e'] = '..'; + + function isRelativeScheme(scheme) { + return relative[scheme] !== undefined; + } + + function invalid() { + clear.call(this); + this._isInvalid = true; + } + + function IDNAToASCII(h) { + if ('' == h) { + invalid.call(this) + } + // XXX + return h.toLowerCase() + } + + function percentEscape(c) { + var unicode = c.charCodeAt(0); + if (unicode > 0x20 && + unicode < 0x7F && + // " # < > ? ` + [0x22, 0x23, 0x3C, 0x3E, 0x3F, 0x60].indexOf(unicode) == -1 + ) { + return c; + } + return encodeURIComponent(c); + } + + function percentEscapeQuery(c) { + // XXX This actually needs to encode c using encoding and then + // convert the bytes one-by-one. + + var unicode = c.charCodeAt(0); + if (unicode > 0x20 && + unicode < 0x7F && + // " # < > ` (do not escape '?') + [0x22, 0x23, 0x3C, 0x3E, 0x60].indexOf(unicode) == -1 + ) { + return c; + } + return encodeURIComponent(c); + } + + var EOF = undefined, + ALPHA = /[a-zA-Z]/, + ALPHANUMERIC = /[a-zA-Z0-9\+\-\.]/; + + function parse(input, stateOverride, base) { + function err(message) { + errors.push(message) + } + + var state = stateOverride || 'scheme start', + cursor = 0, + buffer = '', + seenAt = false, + seenBracket = false, + errors = []; + + loop: while ((input[cursor - 1] != EOF || cursor == 0) && !this._isInvalid) { + var c = input[cursor]; + switch (state) { + case 'scheme start': + if (c && ALPHA.test(c)) { + buffer += c.toLowerCase(); // ASCII-safe + state = 'scheme'; + } else if (!stateOverride) { + buffer = ''; + state = 'no scheme'; + continue; + } else { + err('Invalid scheme.'); + break loop; + } + break; + + case 'scheme': + if (c && ALPHANUMERIC.test(c)) { + buffer += c.toLowerCase(); // ASCII-safe + } else if (':' == c) { + this._scheme = buffer; + buffer = ''; + if (stateOverride) { + break loop; + } + if (isRelativeScheme(this._scheme)) { + this._isRelative = true; + } + if ('file' == this._scheme) { + state = 'relative'; + } else if (this._isRelative && base && base._scheme == this._scheme) { + state = 'relative or authority'; + } else if (this._isRelative) { + state = 'authority first slash'; + } else { + state = 'scheme data'; + } + } else if (!stateOverride) { + buffer = ''; + cursor = 0; + state = 'no scheme'; + continue; + } else if (EOF == c) { + break loop; + } else { + err('Code point not allowed in scheme: ' + c) + break loop; + } + break; + + case 'scheme data': + if ('?' == c) { + this._query = '?'; + state = 'query'; + } else if ('#' == c) { + this._fragment = '#'; + state = 'fragment'; + } else { + // XXX error handling + if (EOF != c && '\t' != c && '\n' != c && '\r' != c) { + this._schemeData += percentEscape(c); + } + } + break; + + case 'no scheme': + if (!base || !(isRelativeScheme(base._scheme))) { + err('Missing scheme.'); + invalid.call(this); + } else { + state = 'relative'; + continue; + } + break; + + case 'relative or authority': + if ('/' == c && '/' == input[cursor+1]) { + state = 'authority ignore slashes'; + } else { + err('Expected /, got: ' + c); + state = 'relative'; + continue + } + break; + + case 'relative': + this._isRelative = true; + if ('file' != this._scheme) + this._scheme = base._scheme; + if (EOF == c) { + this._host = base._host; + this._port = base._port; + this._path = base._path.slice(); + this._query = base._query; + this._username = base._username; + this._password = base._password; + break loop; + } else if ('/' == c || '\\' == c) { + if ('\\' == c) + err('\\ is an invalid code point.'); + state = 'relative slash'; + } else if ('?' == c) { + this._host = base._host; + this._port = base._port; + this._path = base._path.slice(); + this._query = '?'; + this._username = base._username; + this._password = base._password; + state = 'query'; + } else if ('#' == c) { + this._host = base._host; + this._port = base._port; + this._path = base._path.slice(); + this._query = base._query; + this._fragment = '#'; + this._username = base._username; + this._password = base._password; + state = 'fragment'; + } else { + var nextC = input[cursor+1] + var nextNextC = input[cursor+2] + if ( + 'file' != this._scheme || !ALPHA.test(c) || + (nextC != ':' && nextC != '|') || + (EOF != nextNextC && '/' != nextNextC && '\\' != nextNextC && '?' != nextNextC && '#' != nextNextC)) { + this._host = base._host; + this._port = base._port; + this._username = base._username; + this._password = base._password; + this._path = base._path.slice(); + this._path.pop(); + } + state = 'relative path'; + continue; + } + break; + + case 'relative slash': + if ('/' == c || '\\' == c) { + if ('\\' == c) { + err('\\ is an invalid code point.'); + } + if ('file' == this._scheme) { + state = 'file host'; + } else { + state = 'authority ignore slashes'; + } + } else { + if ('file' != this._scheme) { + this._host = base._host; + this._port = base._port; + this._username = base._username; + this._password = base._password; + } + state = 'relative path'; + continue; + } + break; + + case 'authority first slash': + if ('/' == c) { + state = 'authority second slash'; + } else { + err("Expected '/', got: " + c); + state = 'authority ignore slashes'; + continue; + } + break; + + case 'authority second slash': + state = 'authority ignore slashes'; + if ('/' != c) { + err("Expected '/', got: " + c); + continue; + } + break; + + case 'authority ignore slashes': + if ('/' != c && '\\' != c) { + state = 'authority'; + continue; + } else { + err('Expected authority, got: ' + c); + } + break; + + case 'authority': + if ('@' == c) { + if (seenAt) { + err('@ already seen.'); + buffer += '%40'; + } + seenAt = true; + for (var i = 0; i < buffer.length; i++) { + var cp = buffer[i]; + if ('\t' == cp || '\n' == cp || '\r' == cp) { + err('Invalid whitespace in authority.'); + continue; + } + // XXX check URL code points + if (':' == cp && null === this._password) { + this._password = ''; + continue; + } + var tempC = percentEscape(cp); + (null !== this._password) ? this._password += tempC : this._username += tempC; + } + buffer = ''; + } else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) { + cursor -= buffer.length; + buffer = ''; + state = 'host'; + continue; + } else { + buffer += c; + } + break; + + case 'file host': + if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) { + if (buffer.length == 2 && ALPHA.test(buffer[0]) && (buffer[1] == ':' || buffer[1] == '|')) { + state = 'relative path'; + } else if (buffer.length == 0) { + state = 'relative path start'; + } else { + this._host = IDNAToASCII.call(this, buffer); + buffer = ''; + state = 'relative path start'; + } + continue; + } else if ('\t' == c || '\n' == c || '\r' == c) { + err('Invalid whitespace in file host.'); + } else { + buffer += c; + } + break; + + case 'host': + case 'hostname': + if (':' == c && !seenBracket) { + // XXX host parsing + this._host = IDNAToASCII.call(this, buffer); + buffer = ''; + state = 'port'; + if ('hostname' == stateOverride) { + break loop; + } + } else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) { + this._host = IDNAToASCII.call(this, buffer); + buffer = ''; + state = 'relative path start'; + if (stateOverride) { + break loop; + } + continue; + } else if ('\t' != c && '\n' != c && '\r' != c) { + if ('[' == c) { + seenBracket = true; + } else if (']' == c) { + seenBracket = false; + } + buffer += c; + } else { + err('Invalid code point in host/hostname: ' + c); + } + break; + + case 'port': + if (/[0-9]/.test(c)) { + buffer += c; + } else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c || stateOverride) { + if ('' != buffer) { + var temp = parseInt(buffer, 10); + if (temp != relative[this._scheme]) { + this._port = temp + ''; + } + buffer = ''; + } + if (stateOverride) { + break loop; + } + state = 'relative path start'; + continue; + } else if ('\t' == c || '\n' == c || '\r' == c) { + err('Invalid code point in port: ' + c); + } else { + invalid.call(this); + } + break; + + case 'relative path start': + if ('\\' == c) + err("'\\' not allowed in path."); + state = 'relative path'; + if ('/' != c && '\\' != c) { + continue; + } + break; + + case 'relative path': + if (EOF == c || '/' == c || '\\' == c || (!stateOverride && ('?' == c || '#' == c))) { + if ('\\' == c) { + err('\\ not allowed in relative path.'); + } + var tmp; + if (tmp = relativePathDotMapping[buffer.toLowerCase()]) { + buffer = tmp; + } + if ('..' == buffer) { + this._path.pop(); + if ('/' != c && '\\' != c) { + this._path.push(''); + } + } else if ('.' == buffer && '/' != c && '\\' != c) { + this._path.push(''); + } else if ('.' != buffer) { + if ('file' == this._scheme && this._path.length == 0 && buffer.length == 2 && ALPHA.test(buffer[0]) && buffer[1] == '|') { + buffer = buffer[0] + ':'; + } + this._path.push(buffer); + } + buffer = ''; + if ('?' == c) { + this._query = '?'; + state = 'query'; + } else if ('#' == c) { + this._fragment = '#'; + state = 'fragment'; + } + } else if ('\t' != c && '\n' != c && '\r' != c) { + buffer += percentEscape(c); + } + break; + + case 'query': + if (!stateOverride && '#' == c) { + this._fragment = '#'; + state = 'fragment'; + } else if (EOF != c && '\t' != c && '\n' != c && '\r' != c) { + this._query += percentEscapeQuery(c); + } + break; + + case 'fragment': + if (EOF != c && '\t' != c && '\n' != c && '\r' != c) { + this._fragment += c; + } + break; + } + + cursor++; + } + } + + function clear() { + this._scheme = ''; + this._schemeData = ''; + this._username = ''; + this._password = null; + this._host = ''; + this._port = ''; + this._path = []; + this._query = ''; + this._fragment = ''; + this._isInvalid = false; + this._isRelative = false; + } + + // Does not process domain names or IP addresses. + // Does not handle encoding for the query parameter. + function jURL(url, base /* , encoding */) { + if (base !== undefined && !(base instanceof jURL)) + base = new jURL(String(base)); + + this._url = url; + clear.call(this); + + var input = url.replace(/^[ \t\r\n\f]+|[ \t\r\n\f]+$/g, ''); + // encoding = encoding || 'utf-8' + + parse.call(this, input, null, base); + } + + jURL.prototype = { + toString: function() { + return this.href; + }, + get href() { + if (this._isInvalid) + return this._url; + + var authority = ''; + if ('' != this._username || null != this._password) { + authority = this._username + + (null != this._password ? ':' + this._password : '') + '@'; + } + + return this.protocol + + (this._isRelative ? '//' + authority + this.host : '') + + this.pathname + this._query + this._fragment; + }, + set href(href) { + clear.call(this); + parse.call(this, href); + }, + + get protocol() { + return this._scheme + ':'; + }, + set protocol(protocol) { + if (this._isInvalid) + return; + parse.call(this, protocol + ':', 'scheme start'); + }, + + get host() { + return this._isInvalid ? '' : this._port ? + this._host + ':' + this._port : this._host; + }, + set host(host) { + if (this._isInvalid || !this._isRelative) + return; + parse.call(this, host, 'host'); + }, + + get hostname() { + return this._host; + }, + set hostname(hostname) { + if (this._isInvalid || !this._isRelative) + return; + parse.call(this, hostname, 'hostname'); + }, + + get port() { + return this._port; + }, + set port(port) { + if (this._isInvalid || !this._isRelative) + return; + parse.call(this, port, 'port'); + }, + + get pathname() { + return this._isInvalid ? '' : this._isRelative ? + '/' + this._path.join('/') : this._schemeData; + }, + set pathname(pathname) { + if (this._isInvalid || !this._isRelative) + return; + this._path = []; + parse.call(this, pathname, 'relative path start'); + }, + + get search() { + return this._isInvalid || !this._query || '?' == this._query ? + '' : this._query; + }, + set search(search) { + if (this._isInvalid || !this._isRelative) + return; + this._query = '?'; + if ('?' == search[0]) + search = search.slice(1); + parse.call(this, search, 'query'); + }, + + get hash() { + return this._isInvalid || !this._fragment || '#' == this._fragment ? + '' : this._fragment; + }, + set hash(hash) { + if (this._isInvalid) + return; + this._fragment = '#'; + if ('#' == hash[0]) + hash = hash.slice(1); + parse.call(this, hash, 'fragment'); + }, + + get origin() { + var host; + if (this._isInvalid || !this._scheme) { + return ''; + } + // javascript: Gecko returns String(""), WebKit/Blink String("null") + // Gecko throws error for "data://" + // data: Gecko returns "", Blink returns "data://", WebKit returns "null" + // Gecko returns String("") for file: mailto: + // WebKit/Blink returns String("SCHEME://") for file: mailto: + switch (this._scheme) { + case 'data': + case 'file': + case 'javascript': + case 'mailto': + return 'null'; + } + host = this.host; + if (!host) { + return ''; + } + return this._scheme + '://' + host; + } + }; + + // Copy over the static methods + var OriginalURL = scope.URL; + if (OriginalURL) { + jURL.createObjectURL = function(blob) { + // IE extension allows a second optional options argument. + // http://msdn.microsoft.com/en-us/library/ie/hh772302(v=vs.85).aspx + return OriginalURL.createObjectURL.apply(OriginalURL, arguments); + }; + jURL.revokeObjectURL = function(url) { + OriginalURL.revokeObjectURL(url); + }; + } + + scope.URL = jURL; + /* jshint ignore:end */ +})(globalScope); + + +var DEFAULT_RANGE_CHUNK_SIZE = 65536; // 2^16 = 65536 + /** * The maximum allowed image size in total pixels e.g. width * height. Images * above this value will not be drawn. Use -1 for no limit. * @var {number} */ @@ -1629,11 +2289,11 @@ * Specifies if CMaps are binary packed. * @var {boolean} */ PDFJS.cMapPacked = PDFJS.cMapPacked === undefined ? false : PDFJS.cMapPacked; -/* +/** * By default fonts are converted to OpenType fonts and loaded via font face * rules. If disabled, the font will be rendered using a built in font renderer * that constructs the glyphs with primitive path commands. * @var {boolean} */ @@ -1658,11 +2318,13 @@ false : PDFJS.disableWorker); /** * Path and filename of the worker file. Required when the worker is enabled in * development mode. If unspecified in the production build, the worker will be - * loaded based on the location of the pdf.js file. + * loaded based on the location of the pdf.js file. It is recommended that + * the workerSrc is set in a custom application to prevent issues caused by + * third-party frameworks and libraries. * @var {string} */ PDFJS.workerSrc = (PDFJS.workerSrc === undefined ? null : PDFJS.workerSrc); /** @@ -1684,10 +2346,13 @@ /** * Disable pre-fetching of PDF file data. When range requests are enabled PDF.js * will automatically keep fetching more data even if it isn't needed to display * the current page. This default behavior can be disabled. + * + * NOTE: It is also necessary to disable streaming, see above, + * in order for disabling of pre-fetching to work correctly. * @var {boolean} */ PDFJS.disableAutoFetch = (PDFJS.disableAutoFetch === undefined ? false : PDFJS.disableAutoFetch); @@ -1717,10 +2382,18 @@ */ PDFJS.disableWebGL = (PDFJS.disableWebGL === undefined ? true : PDFJS.disableWebGL); /** + * Disables fullscreen support, and by extension Presentation Mode, + * in browsers which support the fullscreen API. + * @var {boolean} + */ +PDFJS.disableFullscreen = (PDFJS.disableFullscreen === undefined ? + false : PDFJS.disableFullscreen); + +/** * Enables CSS only zooming. * @var {boolean} */ PDFJS.useOnlyCssZoom = (PDFJS.useOnlyCssZoom === undefined ? false : PDFJS.useOnlyCssZoom); @@ -1735,31 +2408,74 @@ */ PDFJS.verbosity = (PDFJS.verbosity === undefined ? PDFJS.VERBOSITY_LEVELS.warnings : PDFJS.verbosity); /** - * The maximum supported canvas size in total pixels e.g. width * height. + * The maximum supported canvas size in total pixels e.g. width * height. * The default value is 4096 * 4096. Use -1 for no limit. * @var {number} */ PDFJS.maxCanvasPixels = (PDFJS.maxCanvasPixels === undefined ? 16777216 : PDFJS.maxCanvasPixels); /** + * (Deprecated) Opens external links in a new window if enabled. + * The default behavior opens external links in the PDF.js window. + * + * NOTE: This property has been deprecated, please use + * `PDFJS.externalLinkTarget = PDFJS.LinkTarget.BLANK` instead. + * @var {boolean} + */ +PDFJS.openExternalLinksInNewWindow = ( + PDFJS.openExternalLinksInNewWindow === undefined ? + false : PDFJS.openExternalLinksInNewWindow); + +/** + * Specifies the |target| attribute for external links. + * The constants from PDFJS.LinkTarget should be used: + * - NONE [default] + * - SELF + * - BLANK + * - PARENT + * - TOP + * @var {number} + */ +PDFJS.externalLinkTarget = (PDFJS.externalLinkTarget === undefined ? + PDFJS.LinkTarget.NONE : PDFJS.externalLinkTarget); + +/** + * Determines if we can eval strings as JS. Primarily used to improve + * performance for font rendering. + * @var {boolean} + */ +PDFJS.isEvalSupported = (PDFJS.isEvalSupported === undefined ? + true : PDFJS.isEvalSupported); + +/** * Document initialization / loading parameters object. * * @typedef {Object} DocumentInitParameters * @property {string} url - The URL of the PDF. - * @property {TypedArray} data - A typed array with PDF data. + * @property {TypedArray|Array|string} data - Binary PDF data. Use typed arrays + * (Uint8Array) to improve the memory usage. If PDF data is BASE64-encoded, + * use atob() to convert it to a binary string first. * @property {Object} httpHeaders - Basic authentication headers. * @property {boolean} withCredentials - Indicates whether or not cross-site * Access-Control requests should be made using credentials such as cookies * or authorization headers. The default is false. * @property {string} password - For decrypting password-protected PDFs. * @property {TypedArray} initialData - A typed array with the first portion or * all of the pdf data. Used by the extension since some data is already * loaded before the switch to range requests. + * @property {number} length - The PDF file length. It's used for progress + * reports and range requests operations. + * @property {PDFDataRangeTransport} range + * @property {number} rangeChunkSize - Optional parameter to specify + * maximum number of bytes fetched per range request. The default value is + * 2^16 = 65536. + * @property {PDFWorker} worker - The worker that will be used for the loading + * and parsing of the PDF data. */ /** * @typedef {Object} PDFDocumentStats * @property {Array} streamTypes - Used stream types in the document (an item @@ -1772,81 +2488,354 @@ * This is the main entry point for loading a PDF and interacting with it. * NOTE: If a URL is used to fetch the PDF data a standard XMLHttpRequest(XHR) * is used, which means it must follow the same origin rules that any XHR does * e.g. No cross domain requests without CORS. * - * @param {string|TypedArray|DocumentInitParameters} source Can be a url to - * where a PDF is located, a typed array (Uint8Array) already populated with - * data or parameter object. + * @param {string|TypedArray|DocumentInitParameters|PDFDataRangeTransport} src + * Can be a url to where a PDF is located, a typed array (Uint8Array) + * already populated with data or parameter object. * - * @param {Object} pdfDataRangeTransport is optional. It is used if you want - * to manually serve range requests for data in the PDF. See viewer.js for - * an example of pdfDataRangeTransport's interface. + * @param {PDFDataRangeTransport} pdfDataRangeTransport (deprecated) It is used + * if you want to manually serve range requests for data in the PDF. * - * @param {function} passwordCallback is optional. It is used to request a + * @param {function} passwordCallback (deprecated) It is used to request a * password if wrong or no password was provided. The callback receives two * parameters: function that needs to be called with new password and reason * (see {PasswordResponses}). * - * @param {function} progressCallback is optional. It is used to be able to + * @param {function} progressCallback (deprecated) It is used to be able to * monitor the loading progress of the PDF file (necessary to implement e.g. * a loading bar). The callback receives an {Object} with the properties: * {number} loaded and {number} total. * - * @return {Promise} A promise that is resolved with {@link PDFDocumentProxy} - * object. + * @return {PDFDocumentLoadingTask} */ -PDFJS.getDocument = function getDocument(source, +PDFJS.getDocument = function getDocument(src, pdfDataRangeTransport, passwordCallback, progressCallback) { - var workerInitializedCapability, workerReadyCapability, transport; + var task = new PDFDocumentLoadingTask(); - if (typeof source === 'string') { - source = { url: source }; - } else if (isArrayBuffer(source)) { - source = { data: source }; - } else if (typeof source !== 'object') { - error('Invalid parameter in getDocument, need either Uint8Array, ' + - 'string or a parameter object'); + // Support of the obsolete arguments (for compatibility with API v1.0) + if (arguments.length > 1) { + deprecated('getDocument is called with pdfDataRangeTransport, ' + + 'passwordCallback or progressCallback argument'); } + if (pdfDataRangeTransport) { + if (!(pdfDataRangeTransport instanceof PDFDataRangeTransport)) { + // Not a PDFDataRangeTransport instance, trying to add missing properties. + pdfDataRangeTransport = Object.create(pdfDataRangeTransport); + pdfDataRangeTransport.length = src.length; + pdfDataRangeTransport.initialData = src.initialData; + if (!pdfDataRangeTransport.abort) { + pdfDataRangeTransport.abort = function () {}; + } + } + src = Object.create(src); + src.range = pdfDataRangeTransport; + } + task.onPassword = passwordCallback || null; + task.onProgress = progressCallback || null; - if (!source.url && !source.data) { - error('Invalid parameter array, need either .data or .url'); + var source; + if (typeof src === 'string') { + source = { url: src }; + } else if (isArrayBuffer(src)) { + source = { data: src }; + } else if (src instanceof PDFDataRangeTransport) { + source = { range: src }; + } else { + if (typeof src !== 'object') { + error('Invalid parameter in getDocument, need either Uint8Array, ' + + 'string or a parameter object'); + } + if (!src.url && !src.data && !src.range) { + error('Invalid parameter object: need either .data, .range or .url'); + } + + source = src; } - // copy/use all keys as is except 'url' -- full path is required var params = {}; + var rangeTransport = null; + var worker = null; for (var key in source) { if (key === 'url' && typeof window !== 'undefined') { + // The full path is required in the 'url' field. params[key] = combineUrl(window.location.href, source[key]); continue; + } else if (key === 'range') { + rangeTransport = source[key]; + continue; + } else if (key === 'worker') { + worker = source[key]; + continue; + } else if (key === 'data' && !(source[key] instanceof Uint8Array)) { + // Converting string or array-like data to Uint8Array. + var pdfBytes = source[key]; + if (typeof pdfBytes === 'string') { + params[key] = stringToBytes(pdfBytes); + } else if (typeof pdfBytes === 'object' && pdfBytes !== null && + !isNaN(pdfBytes.length)) { + params[key] = new Uint8Array(pdfBytes); + } else if (isArrayBuffer(pdfBytes)) { + params[key] = new Uint8Array(pdfBytes); + } else { + error('Invalid PDF binary data: either typed array, string or ' + + 'array-like object is expected in the data property.'); + } + continue; } params[key] = source[key]; } - workerInitializedCapability = createPromiseCapability(); - workerReadyCapability = createPromiseCapability(); - transport = new WorkerTransport(workerInitializedCapability, - workerReadyCapability, pdfDataRangeTransport, - progressCallback); - workerInitializedCapability.promise.then(function transportInitialized() { - transport.passwordCallback = passwordCallback; - transport.fetchDocument(params); - }); - return workerReadyCapability.promise; + params.rangeChunkSize = params.rangeChunkSize || DEFAULT_RANGE_CHUNK_SIZE; + + if (!worker) { + // Worker was not provided -- creating and owning our own. + worker = new PDFWorker(); + task._worker = worker; + } + var docId = task.docId; + worker.promise.then(function () { + if (task.destroyed) { + throw new Error('Loading aborted'); + } + return _fetchDocument(worker, params, rangeTransport, docId).then( + function (workerId) { + if (task.destroyed) { + throw new Error('Loading aborted'); + } + var messageHandler = new MessageHandler(docId, workerId, worker.port); + messageHandler.send('Ready', null); + var transport = new WorkerTransport(messageHandler, task, rangeTransport); + task._transport = transport; + }); + }, task._capability.reject); + + return task; }; /** + * Starts fetching of specified PDF document/data. + * @param {PDFWorker} worker + * @param {Object} source + * @param {PDFDataRangeTransport} pdfDataRangeTransport + * @param {string} docId Unique document id, used as MessageHandler id. + * @returns {Promise} The promise, which is resolved when worker id of + * MessageHandler is known. + * @private + */ +function _fetchDocument(worker, source, pdfDataRangeTransport, docId) { + if (worker.destroyed) { + return Promise.reject(new Error('Worker was destroyed')); + } + + source.disableAutoFetch = PDFJS.disableAutoFetch; + source.disableStream = PDFJS.disableStream; + source.chunkedViewerLoading = !!pdfDataRangeTransport; + if (pdfDataRangeTransport) { + source.length = pdfDataRangeTransport.length; + source.initialData = pdfDataRangeTransport.initialData; + } + return worker.messageHandler.sendWithPromise('GetDocRequest', { + docId: docId, + source: source, + disableRange: PDFJS.disableRange, + maxImageSize: PDFJS.maxImageSize, + cMapUrl: PDFJS.cMapUrl, + cMapPacked: PDFJS.cMapPacked, + disableFontFace: PDFJS.disableFontFace, + disableCreateObjectURL: PDFJS.disableCreateObjectURL, + verbosity: PDFJS.verbosity + }).then(function (workerId) { + if (worker.destroyed) { + throw new Error('Worker was destroyed'); + } + return workerId; + }); +} + +/** + * PDF document loading operation. + * @class + * @alias PDFDocumentLoadingTask + */ +var PDFDocumentLoadingTask = (function PDFDocumentLoadingTaskClosure() { + var nextDocumentId = 0; + + /** @constructs PDFDocumentLoadingTask */ + function PDFDocumentLoadingTask() { + this._capability = createPromiseCapability(); + this._transport = null; + this._worker = null; + + /** + * Unique document loading task id -- used in MessageHandlers. + * @type {string} + */ + this.docId = 'd' + (nextDocumentId++); + + /** + * Shows if loading task is destroyed. + * @type {boolean} + */ + this.destroyed = false; + + /** + * Callback to request a password if wrong or no password was provided. + * The callback receives two parameters: function that needs to be called + * with new password and reason (see {PasswordResponses}). + */ + this.onPassword = null; + + /** + * Callback to be able to monitor the loading progress of the PDF file + * (necessary to implement e.g. a loading bar). The callback receives + * an {Object} with the properties: {number} loaded and {number} total. + */ + this.onProgress = null; + + /** + * Callback to when unsupported feature is used. The callback receives + * an {PDFJS.UNSUPPORTED_FEATURES} argument. + */ + this.onUnsupportedFeature = null; + } + + PDFDocumentLoadingTask.prototype = + /** @lends PDFDocumentLoadingTask.prototype */ { + /** + * @return {Promise} + */ + get promise() { + return this._capability.promise; + }, + + /** + * Aborts all network requests and destroys worker. + * @return {Promise} A promise that is resolved after destruction activity + * is completed. + */ + destroy: function () { + this.destroyed = true; + + var transportDestroyed = !this._transport ? Promise.resolve() : + this._transport.destroy(); + return transportDestroyed.then(function () { + this._transport = null; + if (this._worker) { + this._worker.destroy(); + this._worker = null; + } + }.bind(this)); + }, + + /** + * Registers callbacks to indicate the document loading completion. + * + * @param {function} onFulfilled The callback for the loading completion. + * @param {function} onRejected The callback for the loading failure. + * @return {Promise} A promise that is resolved after the onFulfilled or + * onRejected callback. + */ + then: function PDFDocumentLoadingTask_then(onFulfilled, onRejected) { + return this.promise.then.apply(this.promise, arguments); + } + }; + + return PDFDocumentLoadingTask; +})(); + +/** + * Abstract class to support range requests file loading. + * @class + * @alias PDFJS.PDFDataRangeTransport + * @param {number} length + * @param {Uint8Array} initialData + */ +var PDFDataRangeTransport = (function pdfDataRangeTransportClosure() { + function PDFDataRangeTransport(length, initialData) { + this.length = length; + this.initialData = initialData; + + this._rangeListeners = []; + this._progressListeners = []; + this._progressiveReadListeners = []; + this._readyCapability = createPromiseCapability(); + } + PDFDataRangeTransport.prototype = + /** @lends PDFDataRangeTransport.prototype */ { + addRangeListener: + function PDFDataRangeTransport_addRangeListener(listener) { + this._rangeListeners.push(listener); + }, + + addProgressListener: + function PDFDataRangeTransport_addProgressListener(listener) { + this._progressListeners.push(listener); + }, + + addProgressiveReadListener: + function PDFDataRangeTransport_addProgressiveReadListener(listener) { + this._progressiveReadListeners.push(listener); + }, + + onDataRange: function PDFDataRangeTransport_onDataRange(begin, chunk) { + var listeners = this._rangeListeners; + for (var i = 0, n = listeners.length; i < n; ++i) { + listeners[i](begin, chunk); + } + }, + + onDataProgress: function PDFDataRangeTransport_onDataProgress(loaded) { + this._readyCapability.promise.then(function () { + var listeners = this._progressListeners; + for (var i = 0, n = listeners.length; i < n; ++i) { + listeners[i](loaded); + } + }.bind(this)); + }, + + onDataProgressiveRead: + function PDFDataRangeTransport_onDataProgress(chunk) { + this._readyCapability.promise.then(function () { + var listeners = this._progressiveReadListeners; + for (var i = 0, n = listeners.length; i < n; ++i) { + listeners[i](chunk); + } + }.bind(this)); + }, + + transportReady: function PDFDataRangeTransport_transportReady() { + this._readyCapability.resolve(); + }, + + requestDataRange: + function PDFDataRangeTransport_requestDataRange(begin, end) { + throw new Error('Abstract method PDFDataRangeTransport.requestDataRange'); + }, + + abort: function PDFDataRangeTransport_abort() { + } + }; + return PDFDataRangeTransport; +})(); + +PDFJS.PDFDataRangeTransport = PDFDataRangeTransport; + +/** * Proxy to a PDFDocument in the worker thread. Also, contains commonly used * properties that can be read synchronously. * @class + * @alias PDFDocumentProxy */ var PDFDocumentProxy = (function PDFDocumentProxyClosure() { - function PDFDocumentProxy(pdfInfo, transport) { + function PDFDocumentProxy(pdfInfo, transport, loadingTask) { this.pdfInfo = pdfInfo; this.transport = transport; + this.loadingTask = loadingTask; } PDFDocumentProxy.prototype = /** @lends PDFDocumentProxy.prototype */ { /** * @return {number} Total number of pages the PDF contains. */ @@ -1949,11 +2938,11 @@ */ getDownloadInfo: function PDFDocumentProxy_getDownloadInfo() { return this.transport.downloadInfoCapability.promise; }, /** - * @returns {Promise} A promise this is resolved with current stats about + * @return {Promise} A promise this is resolved with current stats about * document structures (see {@link PDFDocumentStats}). */ getStats: function PDFDocumentProxy_getStats() { return this.transport.getStats(); }, @@ -1965,17 +2954,25 @@ }, /** * Destroys current document instance and terminates worker. */ destroy: function PDFDocumentProxy_destroy() { - this.transport.destroy(); + return this.loadingTask.destroy(); } }; return PDFDocumentProxy; })(); /** + * Page getTextContent parameters. + * + * @typedef {Object} getTextContentParameters + * @param {boolean} normalizeWhitespace - replaces all occurrences of + * whitespace with standard spaces (0x20). The default value is `false`. + */ + +/** * Page text content. * * @typedef {Object} TextContent * @property {array} items - array of {@link TextItem} * @property {Object} styles - {@link TextStyles} objects, indexed by font @@ -2003,26 +3000,38 @@ * @property {boolean} vertical - text is in vertical mode. * @property {string} fontFamily - possible font family */ /** + * Page annotation parameters. + * + * @typedef {Object} GetAnnotationsParameters + * @param {string} intent - Determines the annotations that will be fetched, + * can be either 'display' (viewable annotations) or 'print' + * (printable annotations). + * If the parameter is omitted, all annotations are fetched. + */ + +/** * Page render parameters. * * @typedef {Object} RenderParameters * @property {Object} canvasContext - A 2D context of a DOM Canvas object. * @property {PDFJS.PageViewport} viewport - Rendering viewport obtained by * calling of PDFPage.getViewport method. * @property {string} intent - Rendering intent, can be 'display' or 'print' * (default value is 'display'). + * @property {Array} transform - (optional) Additional transform, applied + * just before viewport transform. * @property {Object} imageLayer - (optional) An object that has beginLayout, * endLayout and appendImage functions. - * @property {function} continueCallback - (optional) A function that will be + * @property {function} continueCallback - (deprecated) A function that will be * called each time the rendering is paused. To continue * rendering call the function that is the first argument * to the callback. */ - + /** * PDF page operator list. * * @typedef {Object} PDFOperatorList * @property {Array} fnArray - Array containing the operator functions. @@ -2031,10 +3040,11 @@ */ /** * Proxy to a PDFPage in the worker thread. * @class + * @alias PDFPageProxy */ var PDFPageProxy = (function PDFPageProxyClosure() { function PDFPageProxy(pageIndex, pageInfo, transport) { this.pageIndex = pageIndex; this.pageInfo = pageInfo; @@ -2042,12 +3052,13 @@ this.stats = new StatTimer(); this.stats.enabled = !!globalScope.PDFJS.enableStats; this.commonObjs = transport.commonObjs; this.objs = new PDFObjects(); this.cleanupAfterRender = false; - this.pendingDestroy = false; + this.pendingCleanup = false; this.intentStates = {}; + this.destroyed = false; } PDFPageProxy.prototype = /** @lends PDFPageProxy.prototype */ { /** * @return {number} Page number of the page. First page is 1. */ @@ -2086,21 +3097,23 @@ rotate = this.rotate; } return new PDFJS.PageViewport(this.view, scale, rotate, 0, 0); }, /** + * @param {GetAnnotationsParameters} params - Annotation parameters. * @return {Promise} A promise that is resolved with an {Array} of the * annotation objects. */ - getAnnotations: function PDFPageProxy_getAnnotations() { - if (this.annotationsPromise) { - return this.annotationsPromise; - } + getAnnotations: function PDFPageProxy_getAnnotations(params) { + var intent = (params && params.intent) || null; - var promise = this.transport.getAnnotations(this.pageIndex); - this.annotationsPromise = promise; - return promise; + if (!this.annotationsPromise || this.annotationsIntent !== intent) { + this.annotationsPromise = this.transport.getAnnotations(this.pageIndex, + intent); + this.annotationsIntent = intent; + } + return this.annotationsPromise; }, /** * Begins the process of rendering a page to the desired context. * @param {RenderParameters} params Page render parameters. * @return {RenderTask} An object that contains the promise, which @@ -2110,11 +3123,11 @@ var stats = this.stats; stats.time('Overall'); // If there was a pending destroy cancel it so no cleanup happens during // this call to render. - this.pendingDestroy = false; + this.pendingCleanup = false; var renderingIntent = (params.intent === 'print' ? 'print' : 'display'); if (!this.intentStates[renderingIntent]) { this.intentStates[renderingIntent] = {}; @@ -2142,20 +3155,27 @@ var internalRenderTask = new InternalRenderTask(complete, params, this.objs, this.commonObjs, intentState.operatorList, this.pageNumber); + internalRenderTask.useRequestAnimationFrame = renderingIntent !== 'print'; if (!intentState.renderTasks) { intentState.renderTasks = []; } intentState.renderTasks.push(internalRenderTask); - var renderTask = new RenderTask(internalRenderTask); + var renderTask = internalRenderTask.task; + // Obsolete parameter support + if (params.continueCallback) { + deprecated('render is used with continueCallback parameter'); + renderTask.onContinue = params.continueCallback; + } + var self = this; intentState.displayReadyCapability.promise.then( function pageDisplayReadyPromise(transparency) { - if (self.pendingDestroy) { + if (self.pendingCleanup) { complete(); return; } stats.time('Rendering'); internalRenderTask.initalizeGraphics(transparency); @@ -2171,13 +3191,13 @@ if (i >= 0) { intentState.renderTasks.splice(i, 1); } if (self.cleanupAfterRender) { - self.pendingDestroy = true; + self.pendingCleanup = true; } - self._tryDestroy(); + self._tryCleanup(); if (error) { internalRenderTask.capability.reject(error); } else { internalRenderTask.capability.resolve(); @@ -2226,32 +3246,68 @@ } return intentState.opListReadCapability.promise; }, /** + * @param {getTextContentParameters} params - getTextContent parameters. * @return {Promise} That is resolved a {@link TextContent} * object that represent the page text content. */ - getTextContent: function PDFPageProxy_getTextContent() { + getTextContent: function PDFPageProxy_getTextContent(params) { + var normalizeWhitespace = (params && params.normalizeWhitespace) || false; + return this.transport.messageHandler.sendWithPromise('GetTextContent', { - pageIndex: this.pageNumber - 1 + pageIndex: this.pageNumber - 1, + normalizeWhitespace: normalizeWhitespace, }); }, + /** - * Destroys resources allocated by the page. + * Destroys page object. */ - destroy: function PDFPageProxy_destroy() { - this.pendingDestroy = true; - this._tryDestroy(); + _destroy: function PDFPageProxy_destroy() { + this.destroyed = true; + this.transport.pageCache[this.pageIndex] = null; + + var waitOn = []; + Object.keys(this.intentStates).forEach(function(intent) { + var intentState = this.intentStates[intent]; + intentState.renderTasks.forEach(function(renderTask) { + var renderCompleted = renderTask.capability.promise. + catch(function () {}); // ignoring failures + waitOn.push(renderCompleted); + renderTask.cancel(); + }); + }, this); + this.objs.clear(); + this.annotationsPromise = null; + this.pendingCleanup = false; + return Promise.all(waitOn); }, + /** + * Cleans up resources allocated by the page. (deprecated) + */ + destroy: function() { + deprecated('page destroy method, use cleanup() instead'); + this.cleanup(); + }, + + /** + * Cleans up resources allocated by the page. + */ + cleanup: function PDFPageProxy_cleanup() { + this.pendingCleanup = true; + this._tryCleanup(); + }, + /** * For internal use only. Attempts to clean up if rendering is in a state * where that's possible. * @ignore */ - _tryDestroy: function PDFPageProxy__destroy() { - if (!this.pendingDestroy || + _tryCleanup: function PDFPageProxy_tryCleanup() { + if (!this.pendingCleanup || Object.keys(this.intentStates).some(function(intent) { var intentState = this.intentStates[intent]; return (intentState.renderTasks.length !== 0 || intentState.receivingOperatorList); }, this)) { @@ -2261,11 +3317,11 @@ Object.keys(this.intentStates).forEach(function(intent) { delete this.intentStates[intent]; }, this); this.objs.clear(); this.annotationsPromise = null; - this.pendingDestroy = false; + this.pendingCleanup = false; }, /** * For internal use only. * @ignore */ @@ -2299,92 +3355,201 @@ intentState.renderTasks[i].operatorListChanged(); } if (operatorListChunk.lastChunk) { intentState.receivingOperatorList = false; - this._tryDestroy(); + this._tryCleanup(); } } }; return PDFPageProxy; })(); /** + * PDF.js web worker abstraction, it controls instantiation of PDF documents and + * WorkerTransport for them. If creation of a web worker is not possible, + * a "fake" worker will be used instead. + * @class + */ +var PDFWorker = (function PDFWorkerClosure() { + var nextFakeWorkerId = 0; + + // Loads worker code into main thread. + function setupFakeWorkerGlobal() { + if (!PDFJS.fakeWorkerFilesLoadedCapability) { + PDFJS.fakeWorkerFilesLoadedCapability = createPromiseCapability(); + // In the developer build load worker_loader which in turn loads all the + // other files and resolves the promise. In production only the + // pdf.worker.js file is needed. + PDFJS.fakeWorkerFilesLoadedCapability.resolve(); + } + return PDFJS.fakeWorkerFilesLoadedCapability.promise; + } + + function PDFWorker(name) { + this.name = name; + this.destroyed = false; + + this._readyCapability = createPromiseCapability(); + this._port = null; + this._webWorker = null; + this._messageHandler = null; + this._initialize(); + } + + PDFWorker.prototype = /** @lends PDFWorker.prototype */ { + get promise() { + return this._readyCapability.promise; + }, + + get port() { + return this._port; + }, + + get messageHandler() { + return this._messageHandler; + }, + + _initialize: function PDFWorker_initialize() { + // If worker support isn't disabled explicit and the browser has worker + // support, create a new web worker and test if it/the browser fullfills + // all requirements to run parts of pdf.js in a web worker. + // Right now, the requirement is, that an Uint8Array is still an + // Uint8Array as it arrives on the worker. (Chrome added this with v.15.) + // Either workers are disabled, not supported or have thrown an exception. + // Thus, we fallback to a faked worker. + this._setupFakeWorker(); + }, + + _setupFakeWorker: function PDFWorker_setupFakeWorker() { + warn('Setting up fake worker.'); + globalScope.PDFJS.disableWorker = true; + + setupFakeWorkerGlobal().then(function () { + if (this.destroyed) { + this._readyCapability.reject(new Error('Worker was destroyed')); + return; + } + + // If we don't use a worker, just post/sendMessage to the main thread. + var port = { + _listeners: [], + postMessage: function (obj) { + var e = {data: obj}; + this._listeners.forEach(function (listener) { + listener.call(this, e); + }, this); + }, + addEventListener: function (name, listener) { + this._listeners.push(listener); + }, + removeEventListener: function (name, listener) { + var i = this._listeners.indexOf(listener); + this._listeners.splice(i, 1); + }, + terminate: function () {} + }; + this._port = port; + + // All fake workers use the same port, making id unique. + var id = 'fake' + (nextFakeWorkerId++); + + // If the main thread is our worker, setup the handling for the + // messages -- the main thread sends to it self. + var workerHandler = new MessageHandler(id + '_worker', id, port); + PDFJS.WorkerMessageHandler.setup(workerHandler, port); + + var messageHandler = new MessageHandler(id, id + '_worker', port); + this._messageHandler = messageHandler; + this._readyCapability.resolve(); + }.bind(this)); + }, + + /** + * Destroys the worker instance. + */ + destroy: function PDFWorker_destroy() { + this.destroyed = true; + if (this._webWorker) { + // We need to terminate only web worker created resource. + this._webWorker.terminate(); + this._webWorker = null; + } + this._port = null; + if (this._messageHandler) { + this._messageHandler.destroy(); + this._messageHandler = null; + } + } + }; + + return PDFWorker; +})(); +PDFJS.PDFWorker = PDFWorker; + +/** * For internal use only. * @ignore */ var WorkerTransport = (function WorkerTransportClosure() { - function WorkerTransport(workerInitializedCapability, workerReadyCapability, - pdfDataRangeTransport, progressCallback) { + function WorkerTransport(messageHandler, loadingTask, pdfDataRangeTransport) { + this.messageHandler = messageHandler; + this.loadingTask = loadingTask; this.pdfDataRangeTransport = pdfDataRangeTransport; - - this.workerInitializedCapability = workerInitializedCapability; - this.workerReadyCapability = workerReadyCapability; - this.progressCallback = progressCallback; this.commonObjs = new PDFObjects(); + this.fontLoader = new FontLoader(loadingTask.docId); + this.destroyed = false; + this.destroyCapability = null; + this.pageCache = []; this.pagePromises = []; this.downloadInfoCapability = createPromiseCapability(); - this.passwordCallback = null; - // If worker support isn't disabled explicit and the browser has worker - // support, create a new web worker and test if it/the browser fullfills - // all requirements to run parts of pdf.js in a web worker. - // Right now, the requirement is, that an Uint8Array is still an Uint8Array - // as it arrives on the worker. Chrome added this with version 15. - // Either workers are disabled, not supported or have thrown an exception. - // Thus, we fallback to a faked worker. - this.setupFakeWorker(); + this.setupMessageHandler(); } WorkerTransport.prototype = { destroy: function WorkerTransport_destroy() { + if (this.destroyCapability) { + return this.destroyCapability.promise; + } + + this.destroyed = true; + this.destroyCapability = createPromiseCapability(); + + var waitOn = []; + // We need to wait for all renderings to be completed, e.g. + // timeout/rAF can take a long time. + this.pageCache.forEach(function (page) { + if (page) { + waitOn.push(page._destroy()); + } + }); this.pageCache = []; this.pagePromises = []; var self = this; - this.messageHandler.sendWithPromise('Terminate', null).then(function () { - FontLoader.clear(); - if (self.worker) { - self.worker.terminate(); + // We also need to wait for the worker to finish its long running tasks. + var terminated = this.messageHandler.sendWithPromise('Terminate', null); + waitOn.push(terminated); + Promise.all(waitOn).then(function () { + self.fontLoader.clear(); + if (self.pdfDataRangeTransport) { + self.pdfDataRangeTransport.abort(); + self.pdfDataRangeTransport = null; } - }); + if (self.messageHandler) { + self.messageHandler.destroy(); + self.messageHandler = null; + } + self.destroyCapability.resolve(); + }, this.destroyCapability.reject); + return this.destroyCapability.promise; }, - setupFakeWorker: function WorkerTransport_setupFakeWorker() { - globalScope.PDFJS.disableWorker = true; - - if (!PDFJS.fakeWorkerFilesLoadedCapability) { - PDFJS.fakeWorkerFilesLoadedCapability = createPromiseCapability(); - // In the developer build load worker_loader which in turn loads all the - // other files and resolves the promise. In production only the - // pdf.worker.js file is needed. - PDFJS.fakeWorkerFilesLoadedCapability.resolve(); - } - PDFJS.fakeWorkerFilesLoadedCapability.promise.then(function () { - warn('Setting up fake worker.'); - // If we don't use a worker, just post/sendMessage to the main thread. - var fakeWorker = { - postMessage: function WorkerTransport_postMessage(obj) { - fakeWorker.onmessage({data: obj}); - }, - terminate: function WorkerTransport_terminate() {} - }; - - var messageHandler = new MessageHandler('main', fakeWorker); - this.setupMessageHandler(messageHandler); - - // If the main thread is our worker, setup the handling for the messages - // the main thread sends to it self. - PDFJS.WorkerMessageHandler.setup(messageHandler); - - this.workerInitializedCapability.resolve(); - }.bind(this)); - }, - setupMessageHandler: - function WorkerTransport_setupMessageHandler(messageHandler) { - this.messageHandler = messageHandler; + function WorkerTransport_setupMessageHandler() { + var messageHandler = this.messageHandler; function updatePassword(password) { messageHandler.send('UpdatePassword', password); } @@ -2416,54 +3581,57 @@ } messageHandler.on('GetDoc', function transportDoc(data) { var pdfInfo = data.pdfInfo; this.numPages = data.pdfInfo.numPages; - var pdfDocument = new PDFDocumentProxy(pdfInfo, this); + var loadingTask = this.loadingTask; + var pdfDocument = new PDFDocumentProxy(pdfInfo, this, loadingTask); this.pdfDocument = pdfDocument; - this.workerReadyCapability.resolve(pdfDocument); + loadingTask._capability.resolve(pdfDocument); }, this); messageHandler.on('NeedPassword', function transportNeedPassword(exception) { - if (this.passwordCallback) { - return this.passwordCallback(updatePassword, - PasswordResponses.NEED_PASSWORD); + var loadingTask = this.loadingTask; + if (loadingTask.onPassword) { + return loadingTask.onPassword(updatePassword, + PasswordResponses.NEED_PASSWORD); } - this.workerReadyCapability.reject( + loadingTask._capability.reject( new PasswordException(exception.message, exception.code)); }, this); messageHandler.on('IncorrectPassword', function transportIncorrectPassword(exception) { - if (this.passwordCallback) { - return this.passwordCallback(updatePassword, - PasswordResponses.INCORRECT_PASSWORD); + var loadingTask = this.loadingTask; + if (loadingTask.onPassword) { + return loadingTask.onPassword(updatePassword, + PasswordResponses.INCORRECT_PASSWORD); } - this.workerReadyCapability.reject( + loadingTask._capability.reject( new PasswordException(exception.message, exception.code)); }, this); messageHandler.on('InvalidPDF', function transportInvalidPDF(exception) { - this.workerReadyCapability.reject( + this.loadingTask._capability.reject( new InvalidPDFException(exception.message)); }, this); messageHandler.on('MissingPDF', function transportMissingPDF(exception) { - this.workerReadyCapability.reject( + this.loadingTask._capability.reject( new MissingPDFException(exception.message)); }, this); messageHandler.on('UnexpectedResponse', function transportUnexpectedResponse(exception) { - this.workerReadyCapability.reject( + this.loadingTask._capability.reject( new UnexpectedResponseException(exception.message, exception.status)); }, this); messageHandler.on('UnknownError', function transportUnknownError(exception) { - this.workerReadyCapability.reject( + this.loadingTask._capability.reject( new UnknownErrorException(exception.message, exception.details)); }, this); messageHandler.on('DataLoaded', function transportPage(data) { this.downloadInfoCapability.resolve(data); @@ -2474,23 +3642,33 @@ this.pdfDataRangeTransport.transportReady(); } }, this); messageHandler.on('StartRenderPage', function transportRender(data) { + if (this.destroyed) { + return; // Ignore any pending requests if the worker was terminated. + } var page = this.pageCache[data.pageIndex]; page.stats.timeEnd('Page Request'); page._startRenderPage(data.transparency, data.intent); }, this); messageHandler.on('RenderPageChunk', function transportRender(data) { + if (this.destroyed) { + return; // Ignore any pending requests if the worker was terminated. + } var page = this.pageCache[data.pageIndex]; page._renderPageChunk(data.operatorList, data.intent); }, this); messageHandler.on('commonobj', function transportObj(data) { + if (this.destroyed) { + return; // Ignore any pending requests if the worker was terminated. + } + var id = data[0]; var type = data[1]; if (this.commonObjs.hasData(id)) { return; } @@ -2507,11 +3685,11 @@ break; } else { font = new FontFaceObject(exportedData); } - FontLoader.bind( + this.fontLoader.bind( [font], function fontReady(fontObjs) { this.commonObjs.resolve(id, font); }.bind(this) ); @@ -2523,10 +3701,14 @@ error('Got unknown common object type ' + type); } }, this); messageHandler.on('obj', function transportObj(data) { + if (this.destroyed) { + return; // Ignore any pending requests if the worker was terminated. + } + var id = data[0]; var pageIndex = data[1]; var type = data[2]; var pageProxy = this.pageCache[pageIndex]; var imageData; @@ -2554,29 +3736,55 @@ error('Got unknown object type ' + type); } }, this); messageHandler.on('DocProgress', function transportDocProgress(data) { - if (this.progressCallback) { - this.progressCallback({ + if (this.destroyed) { + return; // Ignore any pending requests if the worker was terminated. + } + + var loadingTask = this.loadingTask; + if (loadingTask.onProgress) { + loadingTask.onProgress({ loaded: data.loaded, total: data.total }); } }, this); messageHandler.on('PageError', function transportError(data) { + if (this.destroyed) { + return; // Ignore any pending requests if the worker was terminated. + } + var page = this.pageCache[data.pageNum - 1]; var intentState = page.intentStates[data.intent]; if (intentState.displayReadyCapability) { intentState.displayReadyCapability.reject(data.error); } else { error(data.error); } }, this); + messageHandler.on('UnsupportedFeature', + function transportUnsupportedFeature(data) { + if (this.destroyed) { + return; // Ignore any pending requests if the worker was terminated. + } + var featureId = data.featureId; + var loadingTask = this.loadingTask; + if (loadingTask.onUnsupportedFeature) { + loadingTask.onUnsupportedFeature(featureId); + } + PDFJS.UnsupportedManager.notify(featureId); + }, this); + messageHandler.on('JpegDecode', function(data) { + if (this.destroyed) { + return Promise.reject('Worker was terminated'); + } + var imageUrl = data[0]; var components = data[1]; if (components !== 3 && components !== 1) { return Promise.reject( new Error('Only 3 components or 1 component can be returned')); @@ -2612,29 +3820,13 @@ img.onerror = function () { reject(new Error('JpegDecode failed to load image')); }; img.src = imageUrl; }); - }); + }, this); }, - fetchDocument: function WorkerTransport_fetchDocument(source) { - source.disableAutoFetch = PDFJS.disableAutoFetch; - source.disableStream = PDFJS.disableStream; - source.chunkedViewerLoading = !!this.pdfDataRangeTransport; - this.messageHandler.send('GetDocRequest', { - source: source, - disableRange: PDFJS.disableRange, - maxImageSize: PDFJS.maxImageSize, - cMapUrl: PDFJS.cMapUrl, - cMapPacked: PDFJS.cMapPacked, - disableFontFace: PDFJS.disableFontFace, - disableCreateObjectURL: PDFJS.disableCreateObjectURL, - verbosity: PDFJS.verbosity - }); - }, - getData: function WorkerTransport_getData() { return this.messageHandler.sendWithPromise('GetData', null); }, getPage: function WorkerTransport_getPage(pageNumber, capability) { @@ -2648,10 +3840,13 @@ return this.pagePromises[pageIndex]; } var promise = this.messageHandler.sendWithPromise('GetPage', { pageIndex: pageIndex }).then(function (pageInfo) { + if (this.destroyed) { + throw new Error('Transport destroyed'); + } var page = new PDFPageProxy(pageIndex, pageInfo, this); this.pageCache[pageIndex] = page; return page; }.bind(this)); this.pagePromises[pageIndex] = promise; @@ -2660,21 +3855,23 @@ getPageIndex: function WorkerTransport_getPageIndexByRef(ref) { return this.messageHandler.sendWithPromise('GetPageIndex', { ref: ref }); }, - getAnnotations: function WorkerTransport_getAnnotations(pageIndex) { - return this.messageHandler.sendWithPromise('GetAnnotations', - { pageIndex: pageIndex }); + getAnnotations: function WorkerTransport_getAnnotations(pageIndex, intent) { + return this.messageHandler.sendWithPromise('GetAnnotations', { + pageIndex: pageIndex, + intent: intent, + }); }, getDestinations: function WorkerTransport_getDestinations() { return this.messageHandler.sendWithPromise('GetDestinations', null); }, getDestination: function WorkerTransport_getDestination(id) { - return this.messageHandler.sendWithPromise('GetDestination', { id: id } ); + return this.messageHandler.sendWithPromise('GetDestination', { id: id }); }, getAttachments: function WorkerTransport_getAttachments() { return this.messageHandler.sendWithPromise('GetAttachments', null); }, @@ -2705,15 +3902,15 @@ this.messageHandler.sendWithPromise('Cleanup', null). then(function endCleanup() { for (var i = 0, ii = this.pageCache.length; i < ii; i++) { var page = this.pageCache[i]; if (page) { - page.destroy(); + page.cleanup(); } } this.commonObjs.clear(); - FontLoader.clear(); + this.fontLoader.clear(); }.bind(this)); } }; return WorkerTransport; @@ -2826,41 +4023,53 @@ })(); /** * Allows controlling of the rendering tasks. * @class + * @alias RenderTask */ var RenderTask = (function RenderTaskClosure() { function RenderTask(internalRenderTask) { - this.internalRenderTask = internalRenderTask; + this._internalRenderTask = internalRenderTask; + /** - * Promise for rendering task completion. - * @type {Promise} + * Callback for incremental rendering -- a function that will be called + * each time the rendering is paused. To continue rendering call the + * function that is the first argument to the callback. + * @type {function} */ - this.promise = this.internalRenderTask.capability.promise; + this.onContinue = null; } RenderTask.prototype = /** @lends RenderTask.prototype */ { /** + * Promise for rendering task completion. + * @return {Promise} + */ + get promise() { + return this._internalRenderTask.capability.promise; + }, + + /** * Cancels the rendering task. If the task is currently rendering it will * not be cancelled until graphics pauses with a timeout. The promise that * this object extends will resolved when cancelled. */ cancel: function RenderTask_cancel() { - this.internalRenderTask.cancel(); + this._internalRenderTask.cancel(); }, /** - * Registers callback to indicate the rendering task completion. + * Registers callbacks to indicate the rendering task completion. * * @param {function} onFulfilled The callback for the rendering completion. * @param {function} onRejected The callback for the rendering failure. * @return {Promise} A promise that is resolved after the onFulfilled or * onRejected callback. */ then: function RenderTask_then(onFulfilled, onRejected) { - return this.promise.then(onFulfilled, onRejected); + return this.promise.then.apply(this.promise, arguments); } }; return RenderTask; })(); @@ -2881,12 +4090,14 @@ this.operatorList = operatorList; this.pageNumber = pageNumber; this.running = false; this.graphicsReadyCallback = null; this.graphicsReady = false; + this.useRequestAnimationFrame = false; this.cancelled = false; this.capability = createPromiseCapability(); + this.task = new RenderTask(this); // caching this-bound methods this._continueBound = this._continue.bind(this); this._scheduleNextBound = this._scheduleNext.bind(this); this._nextBound = this._next.bind(this); } @@ -2908,11 +4119,11 @@ var params = this.params; this.gfx = new CanvasGraphics(params.canvasContext, this.commonObjs, this.objs, params.imageLayer); - this.gfx.beginDrawing(params.viewport, transparency); + this.gfx.beginDrawing(params.transform, params.viewport, transparency); this.operatorListIdx = 0; this.graphicsReady = true; if (this.graphicsReadyCallback) { this.graphicsReadyCallback(); } @@ -2945,19 +4156,23 @@ _continue: function InternalRenderTask__continue() { this.running = true; if (this.cancelled) { return; } - if (this.params.continueCallback) { - this.params.continueCallback(this._scheduleNextBound); + if (this.task.onContinue) { + this.task.onContinue.call(this.task, this._scheduleNextBound); } else { this._scheduleNext(); } }, _scheduleNext: function InternalRenderTask__scheduleNext() { - window.requestAnimationFrame(this._nextBound); + if (this.useRequestAnimationFrame) { + window.requestAnimationFrame(this._nextBound); + } else { + Promise.resolve(undefined).then(this._nextBound); + } }, _next: function InternalRenderTask__next() { if (this.cancelled) { return; @@ -2978,11 +4193,31 @@ }; return InternalRenderTask; })(); +/** + * (Deprecated) Global observer of unsupported feature usages. Use + * onUnsupportedFeature callback of the {PDFDocumentLoadingTask} instance. + */ +PDFJS.UnsupportedManager = (function UnsupportedManagerClosure() { + var listeners = []; + return { + listen: function (cb) { + deprecated('Global UnsupportedManager.listen is used: ' + + ' use PDFDocumentLoadingTask.onUnsupportedFeature instead'); + listeners.push(cb); + }, + notify: function (featureId) { + for (var i = 0, ii = listeners.length; i < ii; i++) { + listeners[i](featureId); + } + } + }; +})(); + var Metadata = PDFJS.Metadata = (function MetadataClosure() { function fixMetadata(meta) { return meta.replace(/>\\376\\377([^<]+)/g, function(all, codes) { var bytes = codes.replace(/\\([0-3])([0-7])([0-7])/g, function(code, d1, d2, d3) { @@ -3065,36 +4300,41 @@ // <canvas> contexts store most of the state we need natively. // However, PDF needs a bit more state, which we store here. // Minimal font size that would be used during canvas fillText operations. var MIN_FONT_SIZE = 16; +// Maximum font size that would be used during canvas fillText operations. +var MAX_FONT_SIZE = 100; var MAX_GROUP_SIZE = 4096; +// Heuristic value used when enforcing minimum line widths. +var MIN_WIDTH_FACTOR = 0.65; + var COMPILE_TYPE3_GLYPHS = true; +var MAX_SIZE_TO_COMPILE = 1000; +var FULL_CHUNK_HEIGHT = 16; + function createScratchCanvas(width, height) { var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; return canvas; } function addContextCurrentTransform(ctx) { - // If the context doesn't expose a `mozCurrentTransform`, add a JS based on. + // If the context doesn't expose a `mozCurrentTransform`, add a JS based one. if (!ctx.mozCurrentTransform) { - // Store the original context - ctx._scaleX = ctx._scaleX || 1.0; - ctx._scaleY = ctx._scaleY || 1.0; ctx._originalSave = ctx.save; ctx._originalRestore = ctx.restore; ctx._originalRotate = ctx.rotate; ctx._originalScale = ctx.scale; ctx._originalTranslate = ctx.translate; ctx._originalTransform = ctx.transform; ctx._originalSetTransform = ctx.setTransform; - ctx._transformMatrix = [ctx._scaleX, 0, 0, ctx._scaleY, 0, 0]; + ctx._transformMatrix = ctx._transformMatrix || [1, 0, 0, 1, 0, 0]; ctx._transformStack = []; Object.defineProperty(ctx, 'mozCurrentTransform', { get: function getCurrentTransform() { return this._transformMatrix; @@ -3196,42 +4436,45 @@ }; } } var CachedCanvases = (function CachedCanvasesClosure() { - var cache = {}; - return { + function CachedCanvases() { + this.cache = Object.create(null); + } + CachedCanvases.prototype = { getCanvas: function CachedCanvases_getCanvas(id, width, height, trackTransform) { var canvasEntry; - if (id in cache) { - canvasEntry = cache[id]; + if (this.cache[id] !== undefined) { + canvasEntry = this.cache[id]; canvasEntry.canvas.width = width; canvasEntry.canvas.height = height; // reset canvas transform for emulated mozCurrentTransform, if needed canvasEntry.context.setTransform(1, 0, 0, 1, 0, 0); } else { var canvas = createScratchCanvas(width, height); var ctx = canvas.getContext('2d'); if (trackTransform) { addContextCurrentTransform(ctx); } - cache[id] = canvasEntry = {canvas: canvas, context: ctx}; + this.cache[id] = canvasEntry = {canvas: canvas, context: ctx}; } return canvasEntry; }, clear: function () { - for (var id in cache) { - var canvasEntry = cache[id]; + for (var id in this.cache) { + var canvasEntry = this.cache[id]; // Zeroing the width and height causes Firefox to release graphics // resources immediately, which can greatly reduce memory consumption. canvasEntry.canvas.width = 0; canvasEntry.canvas.height = 0; - delete cache[id]; + delete this.cache[id]; } } }; + return CachedCanvases; })(); function compileType3Glyph(imgData) { var POINT_TO_PROCESS_LIMIT = 1000; @@ -3415,10 +4658,11 @@ this.textRenderingMode = TextRenderingMode.FILL; this.textRise = 0; // Default fore and background colors this.fillColor = '#000000'; this.strokeColor = '#000000'; + this.patternFill = false; // Note: fill alpha applies to all non-stroking operations this.fillAlpha = 1; this.strokeAlpha = 1; this.lineWidth = 1; this.activeSMask = null; // nonclonable field (see the save method below) @@ -3464,13 +4708,17 @@ this.baseTransformStack = []; this.groupLevel = 0; this.smaskStack = []; this.smaskCounter = 0; this.tempSMask = null; + this.cachedCanvases = new CachedCanvases(); if (canvasCtx) { + // NOTE: if mozCurrentTransform is polyfilled, then the current state of + // the transformation must already be set in canvasCtx._transformMatrix. addContextCurrentTransform(canvasCtx); } + this.cachedGetSinglePixelWidth = null; } function putBinaryImageData(ctx, imgData) { if (typeof ImageData !== 'undefined' && imgData instanceof ImageData) { ctx.putImageData(imgData, 0, 0); @@ -3487,17 +4735,15 @@ // Note: as written, if the last chunk is partial, the putImageData() call // will (conceptually) put pixels past the bounds of the canvas. But // that's ok; any such pixels are ignored. var height = imgData.height, width = imgData.width; - var fullChunkHeight = 16; - var fracChunks = height / fullChunkHeight; - var fullChunks = Math.floor(fracChunks); - var totalChunks = Math.ceil(fracChunks); - var partialChunkHeight = height - fullChunks * fullChunkHeight; + var partialChunkHeight = height % FULL_CHUNK_HEIGHT; + var fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT; + var totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1; - var chunkImgData = ctx.createImageData(width, fullChunkHeight); + var chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT); var srcPos = 0, destPos; var src = imgData.data; var dest = chunkImgData.data; var i, j, thisChunkHeight, elemsInThisChunk; @@ -3513,11 +4759,11 @@ var white = 0xFFFFFFFF; var black = (PDFJS.isLittleEndian || !PDFJS.hasCanvasTypedArrays) ? 0xFF000000 : 0x000000FF; for (i = 0; i < totalChunks; i++) { thisChunkHeight = - (i < fullChunks) ? fullChunkHeight : partialChunkHeight; + (i < fullChunks) ? FULL_CHUNK_HEIGHT : partialChunkHeight; destPos = 0; for (j = 0; j < thisChunkHeight; j++) { var srcDiff = srcLength - srcPos; var k = 0; var kEnd = (srcDiff > fullSrcDiff) ? width : srcDiff * 8 - 7; @@ -3548,70 +4794,68 @@ // We ran out of input. Make all remaining pixels transparent. while (destPos < dest32DataLength) { dest32[destPos++] = 0; } - ctx.putImageData(chunkImgData, 0, i * fullChunkHeight); + ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT); } } else if (imgData.kind === ImageKind.RGBA_32BPP) { // RGBA, 32-bits per pixel. j = 0; - elemsInThisChunk = width * fullChunkHeight * 4; + elemsInThisChunk = width * FULL_CHUNK_HEIGHT * 4; for (i = 0; i < fullChunks; i++) { dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk)); srcPos += elemsInThisChunk; ctx.putImageData(chunkImgData, 0, j); - j += fullChunkHeight; + j += FULL_CHUNK_HEIGHT; } if (i < totalChunks) { elemsInThisChunk = width * partialChunkHeight * 4; dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk)); ctx.putImageData(chunkImgData, 0, j); } } else if (imgData.kind === ImageKind.RGB_24BPP) { // RGB, 24-bits per pixel. - thisChunkHeight = fullChunkHeight; + thisChunkHeight = FULL_CHUNK_HEIGHT; elemsInThisChunk = width * thisChunkHeight; for (i = 0; i < totalChunks; i++) { if (i >= fullChunks) { - thisChunkHeight =partialChunkHeight; + thisChunkHeight = partialChunkHeight; elemsInThisChunk = width * thisChunkHeight; } destPos = 0; for (j = elemsInThisChunk; j--;) { dest[destPos++] = src[srcPos++]; dest[destPos++] = src[srcPos++]; dest[destPos++] = src[srcPos++]; dest[destPos++] = 255; } - ctx.putImageData(chunkImgData, 0, i * fullChunkHeight); + ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT); } } else { error('bad image kind: ' + imgData.kind); } } function putBinaryImageMask(ctx, imgData) { var height = imgData.height, width = imgData.width; - var fullChunkHeight = 16; - var fracChunks = height / fullChunkHeight; - var fullChunks = Math.floor(fracChunks); - var totalChunks = Math.ceil(fracChunks); - var partialChunkHeight = height - fullChunks * fullChunkHeight; + var partialChunkHeight = height % FULL_CHUNK_HEIGHT; + var fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT; + var totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1; - var chunkImgData = ctx.createImageData(width, fullChunkHeight); + var chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT); var srcPos = 0; var src = imgData.data; var dest = chunkImgData.data; for (var i = 0; i < totalChunks; i++) { var thisChunkHeight = - (i < fullChunks) ? fullChunkHeight : partialChunkHeight; + (i < fullChunks) ? FULL_CHUNK_HEIGHT : partialChunkHeight; // Expand the mask so it can be used by the canvas. Any required // inversion has already been handled. var destPos = 3; // alpha component offset for (var j = 0; j < thisChunkHeight; j++) { @@ -3624,28 +4868,28 @@ dest[destPos] = (elem & mask) ? 0 : 255; destPos += 4; mask >>= 1; } } - ctx.putImageData(chunkImgData, 0, i * fullChunkHeight); + ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT); } } 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) { + if (sourceCtx[property] !== undefined) { destCtx[property] = sourceCtx[property]; } } - if ('setLineDash' in sourceCtx) { + if (sourceCtx.setLineDash !== undefined) { destCtx.setLineDash(sourceCtx.getLineDash()); destCtx.lineDashOffset = sourceCtx.lineDashOffset; - } else if ('mozDash' in sourceCtx) { + } else if (sourceCtx.mozDashOffset !== undefined) { destCtx.mozDash = sourceCtx.mozDash; destCtx.mozDashOffset = sourceCtx.mozDashOffset; } } @@ -3664,31 +4908,33 @@ bytes[i - 1] = (bytes[i - 1] * alpha + b0 * alpha_) >> 8; } } } - function composeSMaskAlpha(maskData, layerData) { + function composeSMaskAlpha(maskData, layerData, transferMap) { var length = maskData.length; var scale = 1 / 255; for (var i = 3; i < length; i += 4) { - var alpha = maskData[i]; + var alpha = transferMap ? transferMap[maskData[i]] : maskData[i]; layerData[i] = (layerData[i] * alpha * scale) | 0; } } - function composeSMaskLuminosity(maskData, layerData) { + function composeSMaskLuminosity(maskData, layerData, transferMap) { var length = maskData.length; for (var i = 3; i < length; i += 4) { - var y = ((maskData[i - 3] * 77) + // * 0.3 / 255 * 0x10000 - (maskData[i - 2] * 152) + // * 0.59 .... - (maskData[i - 1] * 28)) | 0; // * 0.11 .... - layerData[i] = (layerData[i] * y) >> 16; + var y = (maskData[i - 3] * 77) + // * 0.3 / 255 * 0x10000 + (maskData[i - 2] * 152) + // * 0.59 .... + (maskData[i - 1] * 28); // * 0.11 .... + layerData[i] = transferMap ? + (layerData[i] * transferMap[y >> 8]) >> 8 : + (layerData[i] * y) >> 16; } } function genericComposeSMask(maskCtx, layerCtx, width, height, - subtype, backdrop) { + subtype, backdrop, transferMap) { var hasBackdrop = !!backdrop; var r0 = hasBackdrop ? backdrop[0] : 0; var g0 = hasBackdrop ? backdrop[1] : 0; var b0 = hasBackdrop ? backdrop[2] : 0; @@ -3698,21 +4944,21 @@ } else { composeFn = composeSMaskAlpha; } // processing image in chunks to save memory - var PIXELS_TO_PROCESS = 65536; + var PIXELS_TO_PROCESS = 1048576; var chunkSize = Math.min(height, Math.ceil(PIXELS_TO_PROCESS / width)); for (var row = 0; row < height; row += chunkSize) { var chunkHeight = Math.min(chunkSize, height - row); var maskData = maskCtx.getImageData(0, row, width, chunkHeight); var layerData = layerCtx.getImageData(0, row, width, chunkHeight); if (hasBackdrop) { composeSMaskBackdrop(maskData.data, r0, g0, b0); } - composeFn(maskData.data, layerData.data); + composeFn(maskData.data, layerData.data, transferMap); maskCtx.putImageData(layerData, 0, row); } } @@ -3722,51 +4968,62 @@ ctx.setTransform(smask.scaleX, 0, 0, smask.scaleY, smask.offsetX, smask.offsetY); var backdrop = smask.backdrop || null; - if (WebGLUtils.isEnabled) { + if (!smask.transferMap && WebGLUtils.isEnabled) { var composed = WebGLUtils.composeSMask(layerCtx.canvas, mask, {subtype: smask.subtype, backdrop: backdrop}); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.drawImage(composed, smask.offsetX, smask.offsetY); return; } genericComposeSMask(maskCtx, layerCtx, mask.width, mask.height, - smask.subtype, backdrop); + smask.subtype, backdrop, smask.transferMap); ctx.drawImage(mask, 0, 0); } var LINE_CAP_STYLES = ['butt', 'round', 'square']; var LINE_JOIN_STYLES = ['miter', 'round', 'bevel']; var NORMAL_CLIP = {}; var EO_CLIP = {}; CanvasGraphics.prototype = { - beginDrawing: function CanvasGraphics_beginDrawing(viewport, transparency) { + beginDrawing: function CanvasGraphics_beginDrawing(transform, viewport, + transparency) { // For pdfs that use blend modes we have to clear the canvas else certain // blend modes can look wrong since we'd be blending with a white // backdrop. The problem with a transparent backdrop though is we then - // don't get sub pixel anti aliasing on text, so we fill with white if - // we can. + // don't get sub pixel anti aliasing on text, creating temporary + // transparent canvas when we have blend modes. var width = this.ctx.canvas.width; var height = this.ctx.canvas.height; + + this.ctx.save(); + this.ctx.fillStyle = 'rgb(255, 255, 255)'; + this.ctx.fillRect(0, 0, width, height); + this.ctx.restore(); + if (transparency) { - this.ctx.clearRect(0, 0, width, height); - } else { - this.ctx.mozOpaque = true; + var transparentCanvas = this.cachedCanvases.getCanvas( + 'transparent', width, height, true); + this.compositeCtx = this.ctx; + this.transparentCanvas = transparentCanvas.canvas; + this.ctx = transparentCanvas.context; this.ctx.save(); - this.ctx.fillStyle = 'rgb(255, 255, 255)'; - this.ctx.fillRect(0, 0, width, height); - this.ctx.restore(); + // The transform can be applied before rendering, transferring it to + // the new canvas. + this.ctx.transform.apply(this.ctx, + this.compositeCtx.mozCurrentTransform); } - var transform = viewport.transform; - this.ctx.save(); - this.ctx.transform.apply(this.ctx, transform); + if (transform) { + this.ctx.transform.apply(this.ctx, transform); + } + this.ctx.transform.apply(this.ctx, viewport.transform); this.baseTransform = this.ctx.mozCurrentTransform.slice(); if (this.imageLayer) { this.imageLayer.beginLayout(); @@ -3844,11 +5101,18 @@ } }, endDrawing: function CanvasGraphics_endDrawing() { this.ctx.restore(); - CachedCanvases.clear(); + + if (this.transparentCanvas) { + this.ctx = this.compositeCtx; + this.ctx.drawImage(this.transparentCanvas, 0, 0); + this.transparentCanvas = null; + } + + this.cachedCanvases.clear(); WebGLUtils.clear(); if (this.imageLayer) { this.imageLayer.endLayout(); } @@ -3868,11 +5132,11 @@ setMiterLimit: function CanvasGraphics_setMiterLimit(limit) { this.ctx.miterLimit = limit; }, setDash: function CanvasGraphics_setDash(dashArray, dashPhase) { var ctx = this.ctx; - if ('setLineDash' in ctx) { + if (ctx.setLineDash !== undefined) { ctx.setLineDash(dashArray); ctx.lineDashOffset = dashPhase; } else { ctx.mozDash = dashArray; ctx.mozDashOffset = dashPhase; @@ -3958,11 +5222,11 @@ var activeSMask = this.current.activeSMask; var drawnWidth = activeSMask.canvas.width; var drawnHeight = activeSMask.canvas.height; var cacheId = 'smaskGroupAt' + this.groupLevel; - var scratchCanvas = CachedCanvases.getCanvas( + var scratchCanvas = this.cachedCanvases.getCanvas( cacheId, drawnWidth, drawnHeight, true); var currentCtx = this.ctx; var currentTransform = currentCtx.mozCurrentTransform; this.ctx.save(); @@ -3987,10 +5251,11 @@ this.groupLevel--; this.ctx = this.groupStack.pop(); composeSMask(this.ctx, this.current.activeSMask, groupCtx); this.ctx.restore(); + copyCtxState(groupCtx, this.ctx); }, save: function CanvasGraphics_save() { this.ctx.save(); var old = this.current; this.stateStack.push(old); @@ -4003,14 +5268,21 @@ this.endSMaskGroup(); } this.current = this.stateStack.pop(); this.ctx.restore(); + + // Ensure that the clipping path is reset (fixes issue6413.pdf). + this.pendingClip = null; + + this.cachedGetSinglePixelWidth = null; } }, transform: function CanvasGraphics_transform(a, b, c, d, e, f) { this.ctx.transform(a, b, c, d, e, f); + + this.cachedGetSinglePixelWidth = null; }, // Path constructPath: function CanvasGraphics_constructPath(ops, args) { var ctx = this.ctx; @@ -4080,13 +5352,13 @@ }, stroke: function CanvasGraphics_stroke(consumePath) { consumePath = typeof consumePath !== 'undefined' ? consumePath : true; var ctx = this.ctx; var strokeColor = this.current.strokeColor; - if (this.current.lineWidth === 0) { - ctx.lineWidth = this.getSinglePixelWidth(); - } + // Prevent drawing too thin lines by enforcing a minimum line width. + ctx.lineWidth = Math.max(this.getSinglePixelWidth() * MIN_WIDTH_FACTOR, + this.current.lineWidth); // For stroke we want to temporarily change the global alpha to the // stroking alpha. ctx.globalAlpha = this.current.strokeAlpha; if (strokeColor && strokeColor.hasOwnProperty('type') && strokeColor.type === 'Pattern') { @@ -4111,31 +5383,29 @@ }, fill: function CanvasGraphics_fill(consumePath) { consumePath = typeof consumePath !== 'undefined' ? consumePath : true; var ctx = this.ctx; var fillColor = this.current.fillColor; + var isPatternFill = this.current.patternFill; var needRestore = false; - if (fillColor && fillColor.hasOwnProperty('type') && - fillColor.type === 'Pattern') { + if (isPatternFill) { ctx.save(); + if (this.baseTransform) { + ctx.setTransform.apply(ctx, this.baseTransform); + } ctx.fillStyle = fillColor.getPattern(ctx, this); needRestore = true; } if (this.pendingEOFill) { if (ctx.mozFillRule !== undefined) { ctx.mozFillRule = 'evenodd'; ctx.fill(); ctx.mozFillRule = 'nonzero'; } else { - try { - ctx.fill('evenodd'); - } catch (ex) { - // shouldn't really happen, but browsers might think differently - ctx.fill(); - } + ctx.fill('evenodd'); } this.pendingEOFill = false; } else { ctx.fill(); } @@ -4255,23 +5525,23 @@ if (fontObj.isType3Font) { return; // we don't need ctx.font for Type3 fonts } var name = fontObj.loadedName || 'sans-serif'; - var bold = fontObj.black ? (fontObj.bold ? 'bolder' : 'bold') : + var bold = fontObj.black ? (fontObj.bold ? '900' : 'bold') : (fontObj.bold ? 'bold' : 'normal'); var italic = fontObj.italic ? 'italic' : 'normal'; var typeface = '"' + name + '", ' + fontObj.fallbackName; // Some font backends cannot handle fonts below certain size. // Keeping the font at minimal size and using the fontSizeScale to change // the current transformation matrix before the fillText/strokeText. // See https://bugzilla.mozilla.org/show_bug.cgi?id=726227 - var browserFontSize = size >= MIN_FONT_SIZE ? size : MIN_FONT_SIZE; - this.current.fontSizeScale = browserFontSize !== MIN_FONT_SIZE ? 1.0 : - size / MIN_FONT_SIZE; + var browserFontSize = size < MIN_FONT_SIZE ? MIN_FONT_SIZE : + size > MAX_FONT_SIZE ? MAX_FONT_SIZE : size; + this.current.fontSizeScale = size / browserFontSize; var rule = italic + ' ' + bold + ' ' + browserFontSize + 'px ' + typeface; this.ctx.font = rule; }, setTextRenderingMode: function CanvasGraphics_setTextRenderingMode(mode) { @@ -4387,10 +5657,11 @@ var wordSpacing = current.wordSpacing; var fontDirection = current.fontDirection; var textHScale = current.textHScale * fontDirection; var glyphsLength = glyphs.length; var vertical = font.vertical; + var spacingDir = vertical ? 1 : -1; var defaultVMetrics = font.defaultVMetrics; var widthAdvanceScale = fontSize * current.fontMatrix[0]; var simpleFillText = current.textRenderingMode === TextRenderingMode.FILL && @@ -4407,11 +5678,17 @@ } var lineWidth = current.lineWidth; var scale = current.textMatrixScale; if (scale === 0 || lineWidth === 0) { - lineWidth = this.getSinglePixelWidth(); + var fillStrokeMode = current.textRenderingMode & + TextRenderingMode.FILL_STROKE_MASK; + if (fillStrokeMode === TextRenderingMode.STROKE || + fillStrokeMode === TextRenderingMode.FILL_STROKE) { + this.cachedGetSinglePixelWidth = null; + lineWidth = this.getSinglePixelWidth() * MIN_WIDTH_FACTOR; + } } else { lineWidth /= scale; } if (fontSizeScale !== 1.0) { @@ -4422,20 +5699,17 @@ ctx.lineWidth = lineWidth; var x = 0, i; for (i = 0; i < glyphsLength; ++i) { var glyph = glyphs[i]; - if (glyph === null) { - // word break - x += fontDirection * wordSpacing; + if (isNum(glyph)) { + x += spacingDir * glyph * fontSize / 1000; continue; - } else if (isNum(glyph)) { - x += -glyph * fontSize * 0.001; - continue; } var restoreNeeded = false; + var spacing = (glyph.isSpace ? wordSpacing : 0) + charSpacing; var character = glyph.fontChar; var accent = glyph.accent; var scaledX, scaledY, scaledAccentX, scaledAccentY; var width = glyph.width; if (vertical) { @@ -4451,20 +5725,26 @@ } else { scaledX = x / fontSizeScale; scaledY = 0; } - if (font.remeasure && width > 0 && this.isFontSubpixelAAEnabled) { - // some standard fonts may not have the exact width, trying to - // rescale per character + if (font.remeasure && width > 0) { + // Some standard fonts may not have the exact width: rescale per + // character if measured width is greater than expected glyph width + // and subpixel-aa is enabled, otherwise just center the glyph. var measuredWidth = ctx.measureText(character).width * 1000 / fontSize * fontSizeScale; - var characterScaleX = width / measuredWidth; - restoreNeeded = true; - ctx.save(); - ctx.scale(characterScaleX, 1); - scaledX /= characterScaleX; + if (width < measuredWidth && this.isFontSubpixelAAEnabled) { + var characterScaleX = width / measuredWidth; + restoreNeeded = true; + ctx.save(); + ctx.scale(characterScaleX, 1); + scaledX /= characterScaleX; + } else if (width !== measuredWidth) { + scaledX += (width - measuredWidth) / 2000 * + fontSize / fontSizeScale; + } } if (simpleFillText && !accent) { // common case ctx.fillText(character, scaledX, scaledY); @@ -4475,11 +5755,11 @@ scaledAccentY = scaledY - accent.offset.y / fontSizeScale; this.paintChar(accent.fontChar, scaledAccentX, scaledAccentY); } } - var charWidth = width * widthAdvanceScale + charSpacing * fontDirection; + var charWidth = width * widthAdvanceScale + spacing * fontDirection; x += charWidth; if (restoreNeeded) { ctx.restore(); } @@ -4497,41 +5777,41 @@ var ctx = this.ctx; var current = this.current; var font = current.font; var fontSize = current.fontSize; var fontDirection = current.fontDirection; + var spacingDir = font.vertical ? 1 : -1; var charSpacing = current.charSpacing; var wordSpacing = current.wordSpacing; var textHScale = current.textHScale * fontDirection; var fontMatrix = current.fontMatrix || FONT_IDENTITY_MATRIX; var glyphsLength = glyphs.length; - var i, glyph, width; + var isTextInvisible = + current.textRenderingMode === TextRenderingMode.INVISIBLE; + var i, glyph, width, spacingLength; - if (fontSize === 0) { + if (isTextInvisible || fontSize === 0) { return; } + this.cachedGetSinglePixelWidth = null; ctx.save(); ctx.transform.apply(ctx, current.textMatrix); ctx.translate(current.x, current.y); ctx.scale(textHScale, fontDirection); for (i = 0; i < glyphsLength; ++i) { glyph = glyphs[i]; - if (glyph === null) { - // word break - this.ctx.translate(wordSpacing, 0); - current.x += wordSpacing * textHScale; - continue; - } else if (isNum(glyph)) { - var spacingLength = -glyph * 0.001 * fontSize; + if (isNum(glyph)) { + spacingLength = spacingDir * glyph * fontSize / 1000; this.ctx.translate(spacingLength, 0); current.x += spacingLength * textHScale; continue; } + var spacing = (glyph.isSpace ? wordSpacing : 0) + charSpacing; var operatorList = font.charProcOperatorList[glyph.operatorListId]; if (!operatorList) { warn('Type3 character \"' + glyph.operatorListId + '\" is not available'); continue; @@ -4542,11 +5822,11 @@ ctx.transform.apply(ctx, fontMatrix); this.executeOperatorList(operatorList); this.restore(); var transformed = Util.applyTransform([glyph.width, 0], fontMatrix); - width = transformed[0] * fontSize + charSpacing; + width = transformed[0] * fontSize + spacing; ctx.translate(width, 0); current.x += width * textHScale; } ctx.restore(); @@ -4574,32 +5854,36 @@ // Color getColorN_Pattern: function CanvasGraphics_getColorN_Pattern(IR) { var pattern; if (IR[0] === 'TilingPattern') { var color = IR[1]; + var baseTransform = this.baseTransform || + this.ctx.mozCurrentTransform.slice(); pattern = new TilingPattern(IR, color, this.ctx, this.objs, - this.commonObjs, this.baseTransform); + this.commonObjs, baseTransform); } else { pattern = getShadingPatternFromIR(IR); } return pattern; }, setStrokeColorN: function CanvasGraphics_setStrokeColorN(/*...*/) { this.current.strokeColor = this.getColorN_Pattern(arguments); }, setFillColorN: function CanvasGraphics_setFillColorN(/*...*/) { this.current.fillColor = this.getColorN_Pattern(arguments); + this.current.patternFill = true; }, setStrokeRGBColor: function CanvasGraphics_setStrokeRGBColor(r, g, b) { - var color = Util.makeCssRgb(arguments); + var color = Util.makeCssRgb(r, g, b); this.ctx.strokeStyle = color; this.current.strokeColor = color; }, setFillRGBColor: function CanvasGraphics_setFillRGBColor(r, g, b) { - var color = Util.makeCssRgb(arguments); + var color = Util.makeCssRgb(r, g, b); this.ctx.fillStyle = color; this.current.fillColor = color; + this.current.patternFill = false; }, shadingFill: function CanvasGraphics_shadingFill(patternIR) { var ctx = this.ctx; @@ -4732,11 +6016,11 @@ var cacheId = 'groupAt' + this.groupLevel; if (group.smask) { // Using two cache entries is case if masks are used one after another. cacheId += '_smask_' + ((this.smaskCounter++) % 2); } - var scratchCanvas = CachedCanvases.getCanvas( + var scratchCanvas = this.cachedCanvases.getCanvas( cacheId, drawnWidth, drawnHeight, true); var groupCtx = scratchCanvas.context; // Since we created a new canvas that is just the size of the bounding box // we have to translate the group ctx. @@ -4752,11 +6036,12 @@ offsetX: offsetX, offsetY: offsetY, scaleX: scaleX, scaleY: scaleY, subtype: group.smask.subtype, - backdrop: group.smask.backdrop + backdrop: group.smask.backdrop, + transferMap: group.smask.transferMap || null }); } else { // Setup the current ctx so when the group is popped we draw it at the // right location. currentCtx.setTransform(1, 0, 0, 1, 0, 0); @@ -4854,15 +6139,16 @@ }, paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject(img) { var ctx = this.ctx; var width = img.width, height = img.height; + var fillColor = this.current.fillColor; + var isPatternFill = this.current.patternFill; var glyph = this.processingType3; - if (COMPILE_TYPE3_GLYPHS && glyph && !('compiled' in glyph)) { - var MAX_SIZE_TO_COMPILE = 1000; + if (COMPILE_TYPE3_GLYPHS && glyph && glyph.compiled === undefined) { 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; @@ -4872,21 +6158,20 @@ if (glyph && glyph.compiled) { glyph.compiled(ctx); return; } - var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height); + var maskCanvas = this.cachedCanvases.getCanvas('maskCanvas', + width, height); var maskCtx = maskCanvas.context; maskCtx.save(); putBinaryImageMask(maskCtx, img); maskCtx.globalCompositeOperation = 'source-in'; - var fillColor = this.current.fillColor; - maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && - fillColor.type === 'Pattern') ? + maskCtx.fillStyle = isPatternFill ? fillColor.getPattern(maskCtx, this) : fillColor; maskCtx.fillRect(0, 0, width, height); maskCtx.restore(); @@ -4896,28 +6181,29 @@ paintImageMaskXObjectRepeat: function CanvasGraphics_paintImageMaskXObjectRepeat(imgData, scaleX, scaleY, positions) { var width = imgData.width; var height = imgData.height; - var ctx = this.ctx; + var fillColor = this.current.fillColor; + var isPatternFill = this.current.patternFill; - var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height); + var maskCanvas = this.cachedCanvases.getCanvas('maskCanvas', + width, height); var maskCtx = maskCanvas.context; maskCtx.save(); putBinaryImageMask(maskCtx, imgData); maskCtx.globalCompositeOperation = 'source-in'; - var fillColor = this.current.fillColor; - maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && - fillColor.type === 'Pattern') ? - fillColor.getPattern(maskCtx, this) : fillColor; + maskCtx.fillStyle = isPatternFill ? + fillColor.getPattern(maskCtx, this) : fillColor; maskCtx.fillRect(0, 0, width, height); maskCtx.restore(); + var ctx = this.ctx; for (var i = 0, ii = positions.length; i < ii; i += 2) { ctx.save(); ctx.transform(scaleX, 0, 0, scaleY, positions[i], positions[i + 1]); ctx.scale(1, -1); ctx.drawImage(maskCanvas.canvas, 0, 0, width, height, @@ -4928,25 +6214,26 @@ paintImageMaskXObjectGroup: function CanvasGraphics_paintImageMaskXObjectGroup(images) { var ctx = this.ctx; + var fillColor = this.current.fillColor; + var isPatternFill = this.current.patternFill; for (var i = 0, ii = images.length; i < ii; i++) { var image = images[i]; var width = image.width, height = image.height; - var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height); + var maskCanvas = this.cachedCanvases.getCanvas('maskCanvas', + width, height); var maskCtx = maskCanvas.context; maskCtx.save(); putBinaryImageMask(maskCtx, image); maskCtx.globalCompositeOperation = 'source-in'; - var fillColor = this.current.fillColor; - maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && - fillColor.type === 'Pattern') ? + maskCtx.fillStyle = isPatternFill ? fillColor.getPattern(maskCtx, this) : fillColor; maskCtx.fillRect(0, 0, width, height); maskCtx.restore(); @@ -5007,11 +6294,12 @@ var imgToPaint, tmpCanvas; // instanceof HTMLElement does not work in jsdom node.js module if (imgData instanceof HTMLElement || !imgData.data) { imgToPaint = imgData; } else { - tmpCanvas = CachedCanvases.getCanvas('inlineImage', width, height); + tmpCanvas = this.cachedCanvases.getCanvas('inlineImage', + width, height); var tmpCtx = tmpCanvas.context; putBinaryImageData(tmpCtx, imgData); imgToPaint = tmpCanvas.canvas; } @@ -5029,11 +6317,12 @@ } if (heightScale > 2 && paintHeight > 1) { newHeight = Math.ceil(paintHeight / 2); heightScale /= paintHeight / newHeight; } - tmpCanvas = CachedCanvases.getCanvas(tmpCanvasId, newWidth, newHeight); + tmpCanvas = this.cachedCanvases.getCanvas(tmpCanvasId, + newWidth, newHeight); tmpCtx = tmpCanvas.context; tmpCtx.clearRect(0, 0, newWidth, newHeight); tmpCtx.drawImage(imgToPaint, 0, 0, paintWidth, paintHeight, 0, 0, newWidth, newHeight); imgToPaint = tmpCanvas.canvas; @@ -5061,11 +6350,11 @@ function CanvasGraphics_paintInlineImageXObjectGroup(imgData, map) { var ctx = this.ctx; var w = imgData.width; var h = imgData.height; - var tmpCanvas = CachedCanvases.getCanvas('inlineImage', w, h); + var tmpCanvas = this.cachedCanvases.getCanvas('inlineImage', w, h); var tmpCtx = tmpCanvas.context; putBinaryImageData(tmpCtx, imgData); for (var i = 0, ii = map.length; i < ii; i++) { var entry = map[i]; @@ -5091,10 +6380,14 @@ paintSolidColorImageMask: function CanvasGraphics_paintSolidColorImageMask() { this.ctx.fillRect(0, 0, 1, 1); }, + paintXObject: function CanvasGraphics_paintXObject() { + warn('Unsupported \'paintXObject\' command.'); + }, + // Marked content markPoint: function CanvasGraphics_markPoint(tag) { // TODO Marked content. }, @@ -5130,30 +6423,28 @@ if (ctx.mozFillRule !== undefined) { ctx.mozFillRule = 'evenodd'; ctx.clip(); ctx.mozFillRule = 'nonzero'; } else { - try { - ctx.clip('evenodd'); - } catch (ex) { - // shouldn't really happen, but browsers might think differently - ctx.clip(); - } + ctx.clip('evenodd'); } } else { ctx.clip(); } this.pendingClip = null; } ctx.beginPath(); }, getSinglePixelWidth: function CanvasGraphics_getSinglePixelWidth(scale) { - var inverse = this.ctx.mozCurrentTransformInverse; - // max of the current horizontal and vertical scale - return Math.sqrt(Math.max( - (inverse[0] * inverse[0] + inverse[1] * inverse[1]), - (inverse[2] * inverse[2] + inverse[3] * inverse[3]))); + if (this.cachedGetSinglePixelWidth === null) { + var inverse = this.ctx.mozCurrentTransformInverse; + // max of the current horizontal and vertical scale + this.cachedGetSinglePixelWidth = Math.sqrt(Math.max( + (inverse[0] * inverse[0] + inverse[1] * inverse[1]), + (inverse[2] * inverse[2] + inverse[3] * inverse[3]))); + } + return this.cachedGetSinglePixelWidth; }, getCanvasPosition: function CanvasGraphics_getCanvasPosition(x, y) { var transform = this.ctx.mozCurrentTransform; return [ transform[0] * x + transform[2] * y + transform[4], @@ -5168,11 +6459,10 @@ return CanvasGraphics; })(); - var WebGLUtils = (function WebGLUtilsClosure() { function loadShader(gl, code, shaderType) { var shader = gl.createShader(shaderType); gl.shaderSource(shader, code); gl.compileShader(shader); @@ -5217,11 +6507,11 @@ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); return texture; } var currentGL, currentCanvas; - function generageGL() { + function generateGL() { if (currentGL) { return; } currentCanvas = document.createElement('canvas'); currentGL = currentCanvas.getContext('webgl', @@ -5275,11 +6565,11 @@ var smaskCache = null; function initSmaskGL() { var canvas, gl; - generageGL(); + generateGL(); canvas = currentCanvas; currentCanvas = null; gl = currentGL; currentGL = null; @@ -5407,11 +6697,11 @@ var figuresCache = null; function initFiguresGL() { var canvas, gl; - generageGL(); + generateGL(); canvas = currentCanvas; currentCanvas = null; gl = currentGL; currentGL = null; @@ -5510,11 +6800,11 @@ break; case 'triangles': for (var j = 0, jj = ps.length; j < jj; j++) { coords[pIndex] = coordsMap[ps[j]]; coords[pIndex + 1] = coordsMap[ps[j] + 1]; - colors[cIndex] = colorsMap[cs[i]]; + colors[cIndex] = colorsMap[cs[j]]; colors[cIndex + 1] = colorsMap[cs[j] + 1]; colors[cIndex + 2] = colorsMap[cs[j] + 2]; pIndex += 2; cIndex += 3; } @@ -5575,11 +6865,11 @@ if (PDFJS.disableWebGL) { return false; } var enabled = false; try { - generageGL(); + generateGL(); enabled = !!currentGL; } catch (e) { } return shadow(this, 'isEnabled', enabled); }, composeSMask: composeSMask, @@ -5717,11 +7007,11 @@ break; } } function createMeshCanvas(bounds, combinesScale, coords, colors, figures, - backgroundColor) { + backgroundColor, cachedCanvases) { // we will increase scale on some weird factor to let antialiasing take // care of "rough" edges var EXPECTED_SCALE = 1.1; // MAX_PATTERN_SIZE is used to avoid OOM situation. var MAX_PATTERN_SIZE = 3000; // 10in @ 300dpi shall be enough @@ -5751,15 +7041,15 @@ if (WebGLUtils.isEnabled) { canvas = WebGLUtils.drawFigures(width, height, backgroundColor, figures, context); // https://bugzilla.mozilla.org/show_bug.cgi?id=972126 - tmpCanvas = CachedCanvases.getCanvas('mesh', width, height, false); + tmpCanvas = cachedCanvases.getCanvas('mesh', width, height, false); tmpCanvas.context.drawImage(canvas, 0, 0); canvas = tmpCanvas.canvas; } else { - tmpCanvas = CachedCanvases.getCanvas('mesh', width, height, false); + tmpCanvas = cachedCanvases.getCanvas('mesh', width, height, false); var tmpCtx = tmpCanvas.context; var data = tmpCtx.createImageData(width, height); if (backgroundColor) { var bytes = data.data; @@ -5811,11 +7101,12 @@ // Rasterizing on the main thread since sending/queue large canvases // might cause OOM. var temporaryPatternCanvas = createMeshCanvas(bounds, scale, coords, - colors, figures, shadingFill ? null : background); + colors, figures, shadingFill ? null : background, + owner.cachedCanvases); if (!shadingFill) { ctx.setTransform.apply(ctx, owner.baseTransform); if (matrix) { ctx.transform.apply(ctx, matrix); @@ -5914,11 +7205,12 @@ MAX_PATTERN_SIZE); height = Math.min(Math.ceil(Math.abs(height * combinedScale[1])), MAX_PATTERN_SIZE); - var tmpCanvas = CachedCanvases.getCanvas('pattern', width, height, true); + var tmpCanvas = owner.cachedCanvases.getCanvas('pattern', + width, height, true); var tmpCtx = tmpCanvas.context; var graphics = new CanvasGraphics(tmpCtx, commonObjs, objs); graphics.groupLevel = owner.groupLevel; this.setFillAndStrokeStyleToContext(tmpCtx, paintType, color); @@ -5968,11 +7260,11 @@ var ctx = this.ctx; context.fillStyle = ctx.fillStyle; context.strokeStyle = ctx.strokeStyle; break; case PaintType.UNCOLORED: - var cssColor = Util.makeCssRgb(color); + var cssColor = Util.makeCssRgb(color[0], color[1], color[2]); context.fillStyle = cssColor; context.strokeStyle = cssColor; break; default: error('Unsupported paint type: ' + paintType); @@ -5993,30 +7285,39 @@ return TilingPattern; })(); -PDFJS.disableFontFace = false; - -var FontLoader = { +function FontLoader(docId) { + this.docId = docId; + this.styleElement = null; + this.nativeFontFaces = []; + this.loadTestFontId = 0; + this.loadingContext = { + requests: [], + nextRequestId: 0 + }; +} +FontLoader.prototype = { insertRule: function fontLoaderInsertRule(rule) { - var styleElement = document.getElementById('PDFJS_FONT_STYLE_TAG'); + var styleElement = this.styleElement; if (!styleElement) { - styleElement = document.createElement('style'); - styleElement.id = 'PDFJS_FONT_STYLE_TAG'; + styleElement = this.styleElement = document.createElement('style'); + styleElement.id = 'PDFJS_FONT_STYLE_TAG_' + this.docId; document.documentElement.getElementsByTagName('head')[0].appendChild( styleElement); } var styleSheet = styleElement.sheet; styleSheet.insertRule(rule, styleSheet.cssRules.length); }, clear: function fontLoaderClear() { - var styleElement = document.getElementById('PDFJS_FONT_STYLE_TAG'); + var styleElement = this.styleElement; if (styleElement) { styleElement.parentNode.removeChild(styleElement); + styleElement = this.styleElement = null; } this.nativeFontFaces.forEach(function(nativeFontFace) { document.fonts.delete(nativeFontFace); }); this.nativeFontFaces.length = 0; @@ -6048,40 +7349,10 @@ 'AAAAAAABAAAAAMmJbzEAAAAAzgTjFQAAAADOBOQpAAEAAAAAAAAADAAUAAQAAAABAAAAAg' + 'ABAAAAAAAAAAAD6AAAAAAAAA==' )); }, - loadTestFontId: 0, - - loadingContext: { - requests: [], - nextRequestId: 0 - }, - - isSyncFontLoadingSupported: (function detectSyncFontLoadingSupport() { - if (isWorker) { - return false; - } - - // User agent string sniffing is bad, but there is no reliable way to tell - // if font is fully loaded and ready to be used with canvas. - var userAgent = window.navigator.userAgent; - var m = /Mozilla\/5.0.*?rv:(\d+).*? Gecko/.exec(userAgent); - if (m && m[1] >= 14) { - return true; - } - // TODO other browsers - if (userAgent === 'node') { - return true; - } - return false; - })(), - - nativeFontFaces: [], - - isFontLoadingAPISupported: !isWorker && !!document.fonts, - addNativeFontFace: function fontLoader_addNativeFontFace(nativeFontFace) { this.nativeFontFaces.push(nativeFontFace); document.fonts.add(nativeFontFace); }, @@ -6089,41 +7360,50 @@ assert(!isWorker, 'bind() shall be called from main thread'); var rules = []; var fontsToLoad = []; var fontLoadPromises = []; + var getNativeFontPromise = function(nativeFontFace) { + // Return a promise that is always fulfilled, even when the font fails to + // load. + return nativeFontFace.loaded.catch(function(e) { + warn('Failed to load font "' + nativeFontFace.family + '": ' + e); + }); + }; for (var i = 0, ii = fonts.length; i < ii; i++) { var font = fonts[i]; // Add the font to the DOM only once or skip if the font // is already loaded. if (font.attached || font.loading === false) { continue; } font.attached = true; - if (this.isFontLoadingAPISupported) { + if (FontLoader.isFontLoadingAPISupported) { var nativeFontFace = font.createNativeFontFace(); if (nativeFontFace) { - fontLoadPromises.push(nativeFontFace.loaded); + this.addNativeFontFace(nativeFontFace); + fontLoadPromises.push(getNativeFontPromise(nativeFontFace)); } } else { - var rule = font.bindDOM(); + var rule = font.createFontFaceRule(); if (rule) { + this.insertRule(rule); rules.push(rule); fontsToLoad.push(font); } } } - var request = FontLoader.queueLoadingCallback(callback); - if (this.isFontLoadingAPISupported) { - Promise.all(fontsToLoad).then(function() { + var request = this.queueLoadingCallback(callback); + if (FontLoader.isFontLoadingAPISupported) { + Promise.all(fontLoadPromises).then(function() { request.complete(); }); - } else if (rules.length > 0 && !this.isSyncFontLoadingSupported) { - FontLoader.prepareFontLoadEvent(rules, fontsToLoad, request); + } else if (rules.length > 0 && !FontLoader.isSyncFontLoadingSupported) { + this.prepareFontLoadEvent(rules, fontsToLoad, request); } else { request.complete(); } }, @@ -6137,11 +7417,11 @@ var otherRequest = context.requests.shift(); setTimeout(otherRequest.callback, 0); } } - var context = FontLoader.loadingContext; + var context = this.loadingContext; var requestId = 'pdfjs-font-loading-' + (context.nextRequestId++); var request = { id: requestId, complete: LoadLoader_completeRequest, callback: callback, @@ -6224,11 +7504,11 @@ data = spliceString(data, CFF_CHECKSUM_OFFSET, 4, string32(checksum)); var url = 'url(data:font/opentype;base64,' + btoa(data) + ');'; var rule = '@font-face { font-family:"' + loadTestFontId + '";src:' + url + '}'; - FontLoader.insertRule(rule); + this.insertRule(rule); var names = []; for (i = 0, ii = fonts.length; i < ii; i++) { names.push(fonts[i].loadedName); } @@ -6252,23 +7532,56 @@ request.complete(); }); /** Hack end */ } }; +FontLoader.isFontLoadingAPISupported = (!isWorker && + typeof document !== 'undefined' && !!document.fonts); +Object.defineProperty(FontLoader, 'isSyncFontLoadingSupported', { + get: function () { + var supported = false; + // User agent string sniffing is bad, but there is no reliable way to tell + // if font is fully loaded and ready to be used with canvas. + var userAgent = window.navigator.userAgent; + var m = /Mozilla\/5.0.*?rv:(\d+).*? Gecko/.exec(userAgent); + if (m && m[1] >= 14) { + supported = true; + } + // TODO other browsers + if (userAgent === 'node') { + supported = true; + } + return shadow(FontLoader, 'isSyncFontLoadingSupported', supported); + }, + enumerable: true, + configurable: true +}); + var FontFaceObject = (function FontFaceObjectClosure() { - function FontFaceObject(name, file, properties) { + function FontFaceObject(translatedData) { this.compiledGlyphs = {}; - if (arguments.length === 1) { - // importing translated data - var data = arguments[0]; - for (var i in data) { - this[i] = data[i]; - } - return; + // importing translated data + for (var i in translatedData) { + this[i] = translatedData[i]; } } + Object.defineProperty(FontFaceObject, 'isEvalSupported', { + get: function () { + var evalSupport = false; + if (PDFJS.isEvalSupported) { + try { + /* jshint evil: true */ + new Function(''); + evalSupport = true; + } catch (e) {} + } + return shadow(this, 'isEvalSupported', evalSupport); + }, + enumerable: true, + configurable: true + }); FontFaceObject.prototype = { createNativeFontFace: function FontFaceObject_createNativeFontFace() { if (!this.data) { return null; } @@ -6278,20 +7591,18 @@ return null; } var nativeFontFace = new FontFace(this.loadedName, this.data, {}); - FontLoader.addNativeFontFace(nativeFontFace); - if (PDFJS.pdfBug && 'FontInspector' in globalScope && globalScope['FontInspector'].enabled) { globalScope['FontInspector'].fontAdded(this); } return nativeFontFace; }, - bindDOM: function FontFaceObject_bindDOM() { + createFontFaceRule: function FontFaceObject_createFontFaceRule() { if (!this.data) { return null; } if (PDFJS.disableFontFace) { @@ -6304,40 +7615,126 @@ // 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 + '}'; - FontLoader.insertRule(rule); if (PDFJS.pdfBug && 'FontInspector' in globalScope && globalScope['FontInspector'].enabled) { globalScope['FontInspector'].fontAdded(this, url); } return rule; }, - getPathGenerator: function FontLoader_getPathGenerator(objs, character) { + getPathGenerator: + function FontFaceObject_getPathGenerator(objs, character) { if (!(character in this.compiledGlyphs)) { - var js = objs.get(this.loadedName + '_path_' + character); - /*jshint -W054 */ - this.compiledGlyphs[character] = new Function('c', 'size', js); + var cmds = objs.get(this.loadedName + '_path_' + character); + var current, i, len; + + // If we can, compile cmds into JS for MAXIMUM SPEED + if (FontFaceObject.isEvalSupported) { + var args, js = ''; + for (i = 0, len = cmds.length; i < len; i++) { + current = cmds[i]; + + if (current.args !== undefined) { + args = current.args.join(','); + } else { + args = ''; + } + + js += 'c.' + current.cmd + '(' + args + ');\n'; + } + /* jshint -W054 */ + this.compiledGlyphs[character] = new Function('c', 'size', js); + } else { + // But fall back on using Function.prototype.apply() if we're + // blocked from using eval() for whatever reason (like CSP policies) + this.compiledGlyphs[character] = function(c, size) { + for (i = 0, len = cmds.length; i < len; i++) { + current = cmds[i]; + + if (current.cmd === 'scale') { + current.args = [size, -size]; + } + + c[current.cmd].apply(c, current.args); + } + }; + } } return this.compiledGlyphs[character]; } }; return FontFaceObject; })(); -var HIGHLIGHT_OFFSET = 4; // px +/** + * Optimised CSS custom property getter/setter. + * @class + */ +var CustomStyle = (function CustomStyleClosure() { + + // As noted on: http://www.zachstronaut.com/posts/2009/02/17/ + // animate-css-transforms-firefox-webkit.html + // in some versions of IE9 it is critical that ms appear in this list + // before Moz + var prefixes = ['ms', 'Moz', 'Webkit', 'O']; + var _cache = {}; + + function CustomStyle() {} + + CustomStyle.getProp = function get(propName, element) { + // check cache only when no element is given + if (arguments.length === 1 && typeof _cache[propName] === 'string') { + return _cache[propName]; + } + + element = element || document.documentElement; + var style = element.style, prefixed, uPropName; + + // test standard property first + if (typeof style[propName] === 'string') { + return (_cache[propName] = propName); + } + + // capitalize + uPropName = propName.charAt(0).toUpperCase() + propName.slice(1); + + // test vendor specific properties + for (var i = 0, l = prefixes.length; i < l; i++) { + prefixed = prefixes[i] + uPropName; + if (typeof style[prefixed] === 'string') { + return (_cache[propName] = prefixed); + } + } + + //if all fails then set to undefined + return (_cache[propName] = 'undefined'); + }; + + CustomStyle.setProp = function set(propName, element, str) { + var prop = this.getProp(propName); + if (prop !== 'undefined') { + element.style[prop] = str; + } + }; + + return CustomStyle; +})(); + +PDFJS.CustomStyle = CustomStyle; + + var ANNOT_MIN_SIZE = 10; // px -var AnnotationUtils = (function AnnotationUtilsClosure() { +var AnnotationLayer = (function AnnotationLayerClosure() { // 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) { @@ -6354,77 +7751,127 @@ // Use a reasonable default font if the font doesn't specify a fallback var fallbackName = fontObj.fallbackName || 'Helvetica, sans-serif'; style.fontFamily = fontFamily + fallbackName; } - // TODO(mack): Remove this, it's not really that helpful. - function getEmptyContainer(tagName, rect, borderWidth) { - var bWidth = borderWidth || 0; - var element = document.createElement(tagName); - element.style.borderWidth = bWidth + 'px'; - var width = rect[2] - rect[0] - 2 * bWidth; - var height = rect[3] - rect[1] - 2 * bWidth; - element.style.width = width + 'px'; - element.style.height = height + 'px'; - return element; - } + function getContainer(data, page, viewport) { + var container = document.createElement('section'); + var width = data.rect[2] - data.rect[0]; + var height = data.rect[3] - data.rect[1]; - function initContainer(item) { - var container = getEmptyContainer('section', item.rect, item.borderWidth); - container.style.backgroundColor = item.color; + container.setAttribute('data-annotation-id', data.id); - var color = item.color; - var rgb = []; - for (var i = 0; i < 3; ++i) { - rgb[i] = Math.round(color[i] * 255); + data.rect = Util.normalizeRect([ + data.rect[0], + page.view[3] - data.rect[1] + page.view[1], + data.rect[2], + page.view[3] - data.rect[3] + page.view[1] + ]); + + CustomStyle.setProp('transform', container, + 'matrix(' + viewport.transform.join(',') + ')'); + CustomStyle.setProp('transformOrigin', container, + -data.rect[0] + 'px ' + -data.rect[1] + 'px'); + + if (data.borderStyle.width > 0) { + container.style.borderWidth = data.borderStyle.width + 'px'; + if (data.borderStyle.style !== AnnotationBorderStyleType.UNDERLINE) { + // Underline styles only have a bottom border, so we do not need + // to adjust for all borders. This yields a similar result as + // Adobe Acrobat/Reader. + width = width - 2 * data.borderStyle.width; + height = height - 2 * data.borderStyle.width; + } + + var horizontalRadius = data.borderStyle.horizontalCornerRadius; + var verticalRadius = data.borderStyle.verticalCornerRadius; + if (horizontalRadius > 0 || verticalRadius > 0) { + var radius = horizontalRadius + 'px / ' + verticalRadius + 'px'; + CustomStyle.setProp('borderRadius', container, radius); + } + + switch (data.borderStyle.style) { + case AnnotationBorderStyleType.SOLID: + container.style.borderStyle = 'solid'; + break; + + case AnnotationBorderStyleType.DASHED: + container.style.borderStyle = 'dashed'; + break; + + case AnnotationBorderStyleType.BEVELED: + warn('Unimplemented border style: beveled'); + break; + + case AnnotationBorderStyleType.INSET: + warn('Unimplemented border style: inset'); + break; + + case AnnotationBorderStyleType.UNDERLINE: + container.style.borderBottomStyle = 'solid'; + break; + + default: + break; + } + + if (data.color) { + container.style.borderColor = + Util.makeCssRgb(data.color[0] | 0, + data.color[1] | 0, + data.color[2] | 0); + } else { + // Transparent (invisible) border, so do not draw it at all. + container.style.borderWidth = 0; + } } - item.colorCssRgb = Util.makeCssRgb(rgb); - var highlight = document.createElement('div'); - highlight.className = 'annotationHighlight'; - highlight.style.left = highlight.style.top = -HIGHLIGHT_OFFSET + 'px'; - highlight.style.right = highlight.style.bottom = -HIGHLIGHT_OFFSET + 'px'; - highlight.setAttribute('hidden', true); + container.style.left = data.rect[0] + 'px'; + container.style.top = data.rect[1] + 'px'; - item.highlightElement = highlight; - container.appendChild(item.highlightElement); + container.style.width = width + 'px'; + container.style.height = height + 'px'; return container; } - function getHtmlElementForTextWidgetAnnotation(item, commonObjs) { - var element = getEmptyContainer('div', item.rect, 0); + function getHtmlElementForTextWidgetAnnotation(item, page) { + var element = document.createElement('div'); + var width = item.rect[2] - item.rect[0]; + var height = item.rect[3] - item.rect[1]; + element.style.width = width + 'px'; + element.style.height = height + 'px'; 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; + page.commonObjs.getData(item.fontRefName) : null; setTextStyles(content, item, fontObj); element.appendChild(content); return element; } - function getHtmlElementForTextAnnotation(item) { + function getHtmlElementForTextAnnotation(item, page, viewport) { var rect = item.rect; // sanity check because of OOo-generated PDFs if ((rect[3] - rect[1]) < ANNOT_MIN_SIZE) { rect[3] = rect[1] + ANNOT_MIN_SIZE; } if ((rect[2] - rect[0]) < ANNOT_MIN_SIZE) { rect[2] = rect[0] + (rect[3] - rect[1]); // make it square } - var container = initContainer(item); + var container = getContainer(item, page, viewport); container.className = 'annotText'; var image = document.createElement('img'); image.style.height = container.style.height; image.style.width = container.style.width; @@ -6443,19 +7890,19 @@ var content = document.createElement('div'); content.className = 'annotTextContent'; content.setAttribute('hidden', true); var i, ii; - if (item.hasBgColor) { + if (item.hasBgColor && item.color) { var color = item.color; - var rgb = []; - for (i = 0; i < 3; ++i) { - // Enlighten the color (70%) - var c = Math.round(color[i] * 255); - rgb[i] = Math.round((255 - c) * 0.7) + c; - } - content.style.backgroundColor = Util.makeCssRgb(rgb); + + // Enlighten the color (70%) + var BACKGROUND_ENLIGHT = 0.7; + var r = BACKGROUND_ENLIGHT * (255 - color[0]) + color[0]; + var g = BACKGROUND_ENLIGHT * (255 - color[1]) + color[1]; + var b = BACKGROUND_ENLIGHT * (255 - color[2]) + color[2]; + content.style.backgroundColor = Util.makeCssRgb(r | 0, g | 0, b | 0); } var title = document.createElement('h1'); var text = document.createElement('p'); title.textContent = item.title; @@ -6526,48 +7973,322 @@ container.appendChild(contentWrapper); return container; } - function getHtmlElementForLinkAnnotation(item) { - var container = initContainer(item); - container.className = 'annotLink'; + function getHtmlElementForLinkAnnotation(item, page, viewport, linkService) { + function bindLink(link, dest) { + link.href = linkService.getDestinationHash(dest); + link.onclick = function annotationsLayerBuilderLinksOnclick() { + if (dest) { + linkService.navigateTo(dest); + } + return false; + }; + if (dest) { + link.className = 'internalLink'; + } + } - container.style.borderColor = item.colorCssRgb; - container.style.borderStyle = 'solid'; + function bindNamedAction(link, action) { + link.href = linkService.getAnchorUrl(''); + link.onclick = function annotationsLayerBuilderNamedActionOnClick() { + linkService.executeNamedAction(action); + return false; + }; + link.className = 'internalLink'; + } + var container = getContainer(item, page, viewport); + container.className = 'annotLink'; + var link = document.createElement('a'); link.href = link.title = item.url || ''; - if (link.href.indexOf("mailto:") !== 0) { - link.target = "_blank"; + + if (item.url && isExternalLinkTargetSet()) { + link.target = LinkTargetStringMap[PDFJS.externalLinkTarget]; } + if (!item.url) { + if (item.action) { + bindNamedAction(link, item.action); + } else { + bindLink(link, ('dest' in item) ? item.dest : null); + } + } + container.appendChild(link); return container; } - function getHtmlElement(data, objs) { + function getHtmlElement(data, page, viewport, linkService) { switch (data.annotationType) { case AnnotationType.WIDGET: - return getHtmlElementForTextWidgetAnnotation(data, objs); + return getHtmlElementForTextWidgetAnnotation(data, page); case AnnotationType.TEXT: - return getHtmlElementForTextAnnotation(data); + return getHtmlElementForTextAnnotation(data, page, viewport); case AnnotationType.LINK: - return getHtmlElementForLinkAnnotation(data); + return getHtmlElementForLinkAnnotation(data, page, viewport, + linkService); default: throw new Error('Unsupported annotationType: ' + data.annotationType); } } + function render(viewport, div, annotations, page, linkService) { + for (var i = 0, ii = annotations.length; i < ii; i++) { + var data = annotations[i]; + if (!data || !data.hasHtml) { + continue; + } + + var element = getHtmlElement(data, page, viewport, linkService); + div.appendChild(element); + } + } + + function update(viewport, div, annotations) { + for (var i = 0, ii = annotations.length; i < ii; i++) { + var data = annotations[i]; + var element = div.querySelector( + '[data-annotation-id="' + data.id + '"]'); + if (element) { + CustomStyle.setProp('transform', element, + 'matrix(' + viewport.transform.join(',') + ')'); + } + } + div.removeAttribute('hidden'); + } + return { - getHtmlElement: getHtmlElement + render: render, + update: update }; })(); -PDFJS.AnnotationUtils = AnnotationUtils; +PDFJS.AnnotationLayer = AnnotationLayer; + +/** + * Text layer render parameters. + * + * @typedef {Object} TextLayerRenderParameters + * @property {TextContent} textContent - Text content to render (the object is + * returned by the page's getTextContent() method). + * @property {HTMLElement} container - HTML element that will contain text runs. + * @property {PDFJS.PageViewport} viewport - The target viewport to properly + * layout the text runs. + * @property {Array} textDivs - (optional) HTML elements that are correspond + * the text items of the textContent input. This is output and shall be + * initially be set to empty array. + * @property {number} timeout - (optional) Delay in milliseconds before + * rendering of the text runs occurs. + */ +var renderTextLayer = (function renderTextLayerClosure() { + var MAX_TEXT_DIVS_TO_RENDER = 100000; + + var NonWhitespaceRegexp = /\S/; + + function isAllWhitespace(str) { + return !NonWhitespaceRegexp.test(str); + } + + function appendText(textDivs, viewport, geom, styles) { + var style = styles[geom.fontName]; + var textDiv = document.createElement('div'); + textDivs.push(textDiv); + if (isAllWhitespace(geom.str)) { + textDiv.dataset.isWhitespace = true; + return; + } + var tx = PDFJS.Util.transform(viewport.transform, geom.transform); + var angle = Math.atan2(tx[1], tx[0]); + if (style.vertical) { + angle += Math.PI / 2; + } + var fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3])); + var fontAscent = fontHeight; + if (style.ascent) { + fontAscent = style.ascent * fontAscent; + } else if (style.descent) { + fontAscent = (1 + style.descent) * fontAscent; + } + + var left; + var top; + if (angle === 0) { + left = tx[4]; + top = tx[5] - fontAscent; + } else { + left = tx[4] + (fontAscent * Math.sin(angle)); + top = tx[5] - (fontAscent * Math.cos(angle)); + } + textDiv.style.left = left + 'px'; + textDiv.style.top = top + 'px'; + textDiv.style.fontSize = fontHeight + 'px'; + textDiv.style.fontFamily = style.fontFamily; + + textDiv.textContent = geom.str; + // |fontName| is only used by the Font Inspector. This test will succeed + // when e.g. the Font Inspector is off but the Stepper is on, but it's + // not worth the effort to do a more accurate test. + if (PDFJS.pdfBug) { + textDiv.dataset.fontName = geom.fontName; + } + // Storing into dataset will convert number into string. + if (angle !== 0) { + textDiv.dataset.angle = angle * (180 / Math.PI); + } + // We don't bother scaling single-char text divs, because it has very + // little effect on text highlighting. This makes scrolling on docs with + // lots of such divs a lot faster. + if (geom.str.length > 1) { + if (style.vertical) { + textDiv.dataset.canvasWidth = geom.height * viewport.scale; + } else { + textDiv.dataset.canvasWidth = geom.width * viewport.scale; + } + } + } + + function render(task) { + if (task._canceled) { + return; + } + var textLayerFrag = task._container; + var textDivs = task._textDivs; + var capability = task._capability; + var textDivsLength = textDivs.length; + + // No point in rendering many divs as it would make the browser + // unusable even after the divs are rendered. + if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) { + capability.resolve(); + return; + } + + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d', {alpha: false}); + + var lastFontSize; + var lastFontFamily; + for (var i = 0; i < textDivsLength; i++) { + var textDiv = textDivs[i]; + if (textDiv.dataset.isWhitespace !== undefined) { + continue; + } + + var fontSize = textDiv.style.fontSize; + var fontFamily = textDiv.style.fontFamily; + + // Only build font string and set to context if different from last. + if (fontSize !== lastFontSize || fontFamily !== lastFontFamily) { + ctx.font = fontSize + ' ' + fontFamily; + lastFontSize = fontSize; + lastFontFamily = fontFamily; + } + + var width = ctx.measureText(textDiv.textContent).width; + if (width > 0) { + textLayerFrag.appendChild(textDiv); + var transform; + if (textDiv.dataset.canvasWidth !== undefined) { + // Dataset values come of type string. + var textScale = textDiv.dataset.canvasWidth / width; + transform = 'scaleX(' + textScale + ')'; + } else { + transform = ''; + } + var rotation = textDiv.dataset.angle; + if (rotation) { + transform = 'rotate(' + rotation + 'deg) ' + transform; + } + if (transform) { + PDFJS.CustomStyle.setProp('transform' , textDiv, transform); + } + } + } + capability.resolve(); + } + + /** + * Text layer rendering task. + * + * @param {TextContent} textContent + * @param {HTMLElement} container + * @param {PDFJS.PageViewport} viewport + * @param {Array} textDivs + * @private + */ + function TextLayerRenderTask(textContent, container, viewport, textDivs) { + this._textContent = textContent; + this._container = container; + this._viewport = viewport; + textDivs = textDivs || []; + this._textDivs = textDivs; + this._canceled = false; + this._capability = createPromiseCapability(); + this._renderTimer = null; + } + TextLayerRenderTask.prototype = { + get promise() { + return this._capability.promise; + }, + + cancel: function TextLayer_cancel() { + this._canceled = true; + if (this._renderTimer !== null) { + clearTimeout(this._renderTimer); + this._renderTimer = null; + } + this._capability.reject('canceled'); + }, + + _render: function TextLayer_render(timeout) { + var textItems = this._textContent.items; + var styles = this._textContent.styles; + var textDivs = this._textDivs; + var viewport = this._viewport; + for (var i = 0, len = textItems.length; i < len; i++) { + appendText(textDivs, viewport, textItems[i], styles); + } + + if (!timeout) { // Render right away + render(this); + } else { // Schedule + var self = this; + this._renderTimer = setTimeout(function() { + render(self); + self._renderTimer = null; + }, timeout); + } + } + }; + + + /** + * Starts rendering of the text layer. + * + * @param {TextLayerRenderParameters} renderParameters + * @returns {TextLayerRenderTask} + */ + function renderTextLayer(renderParameters) { + var task = new TextLayerRenderTask(renderParameters.textContent, + renderParameters.container, + renderParameters.viewport, + renderParameters.textDivs); + task._render(renderParameters.timeout); + return task; + } + + return renderTextLayer; +})(); + +PDFJS.renderTextLayer = renderTextLayer; + + var SVG_DEFAULTS = { fontStyle: 'normal', fontWeight: 'normal', fillColor: '#000000' }; @@ -6796,11 +8517,11 @@ this.strokeAlpha = 1; this.lineWidth = 1; this.lineJoin = ''; this.lineCap = ''; this.miterLimit = 0; - + this.dashArray = []; this.dashPhase = 0; this.dependencies = []; @@ -7023,11 +8744,11 @@ var fnId = fnArray[x]; opList.push({'fnId' : fnId, 'fn': REVOPS[fnId], 'args': argsArray[x]}); } return opListToTree(opList); }, - + executeOpTree: function SVGGraphics_executeOpTree(opTree) { var opTreeLen = opTree.length; for(var x = 0; x < opTreeLen; x++) { var fn = opTree[x].fn; var fnId = opTree[x].fnId; @@ -7062,10 +8783,13 @@ this.setCharSpacing(args[0]); break; case OPS.setWordSpacing: this.setWordSpacing(args[0]); break; + case OPS.setHScale: + this.setHScale(args[0]); + break; case OPS.setTextMatrix: this.setTextMatrix(args[0], args[1], args[2], args[3], args[4], args[5]); break; case OPS.setLineWidth: @@ -7366,15 +9090,15 @@ }, setMiterLimit: function SVGGraphics_setMiterLimit(limit) { this.current.miterLimit = limit; }, setStrokeRGBColor: function SVGGraphics_setStrokeRGBColor(r, g, b) { - var color = Util.makeCssRgb(arguments); + var color = Util.makeCssRgb(r, g, b); this.current.strokeColor = color; }, setFillRGBColor: function SVGGraphics_setFillRGBColor(r, g, b) { - var color = Util.makeCssRgb(arguments); + var color = Util.makeCssRgb(r, g, b); this.current.fillColor = color; this.current.tspan = document.createElementNS(NS, 'svg:tspan'); this.current.xcoords = []; }, setDash: function SVGGraphics_setDash(dashArray, dashPhase) { @@ -7770,10 +9494,27 @@ array[i] = data.charCodeAt(i) & 0xFF; } return array.buffer; } + var supportsMozChunked = (function supportsMozChunkedClosure() { + try { + var x = new XMLHttpRequest(); + // Firefox 37- required .open() to be called before setting responseType. + // https://bugzilla.mozilla.org/show_bug.cgi?id=707484 + // Even though the URL is not visited, .open() could fail if the URL is + // blocked, e.g. via the connect-src CSP directive or the NoScript addon. + // When this error occurs, this feature detection method will mistakenly + // report that moz-chunked-arraybuffer is not supported in Firefox 37-. + x.open('GET', 'https://example.com'); + x.responseType = 'moz-chunked-arraybuffer'; + return x.responseType === 'moz-chunked-arraybuffer'; + } catch (e) { + return false; + } + })(); + NetworkManager.prototype = { requestRange: function NetworkManager_requestRange(begin, end, listeners) { var args = { begin: begin, end: end @@ -7810,18 +9551,15 @@ pendingRequest.expectedStatus = 206; } else { pendingRequest.expectedStatus = 200; } - if (args.onProgressiveData) { + var useMozChunkedLoading = supportsMozChunked && !!args.onProgressiveData; + if (useMozChunkedLoading) { xhr.responseType = 'moz-chunked-arraybuffer'; - if (xhr.responseType === 'moz-chunked-arraybuffer') { - pendingRequest.onProgressiveData = args.onProgressiveData; - pendingRequest.mozChunked = true; - } else { - xhr.responseType = 'arraybuffer'; - } + pendingRequest.onProgressiveData = args.onProgressiveData; + pendingRequest.mozChunked = true; } else { xhr.responseType = 'arraybuffer'; } if (args.onError) { @@ -7920,15 +9658,17 @@ begin: begin, chunk: chunk }); } else if (pendingRequest.onProgressiveData) { pendingRequest.onDone(null); - } else { + } else if (chunk) { pendingRequest.onDone({ begin: 0, chunk: chunk }); + } else if (pendingRequest.onError) { + pendingRequest.onError(xhr.status); } }, hasPendingRequests: function NetworkManager_hasPendingRequests() { for (var xhrId in this.pendingRequests) { @@ -7968,11 +9708,10 @@ return NetworkManager; })(); - var ChunkedStream = (function ChunkedStreamClosure() { function ChunkedStream(length, chunkSize, manager) { this.bytes = new Uint8Array(length); this.start = 0; this.pos = 0; @@ -8081,22 +9820,17 @@ } } }, nextEmptyChunk: function ChunkedStream_nextEmptyChunk(beginChunk) { - var chunk, n; - for (chunk = beginChunk, n = this.numChunks; chunk < n; ++chunk) { + var chunk, numChunks = this.numChunks; + for (var i = 0; i < numChunks; ++i) { + chunk = (beginChunk + i) % numChunks; // Wrap around to beginning if (!this.loadedChunks[chunk]) { return chunk; } } - // Wrap around to beginning - for (chunk = 0; chunk < beginChunk; ++chunk) { - if (!this.loadedChunks[chunk]) { - return chunk; - } - } return null; }, hasChunk: function ChunkedStream_hasChunk(chunk) { return !!this.loadedChunks[chunk]; @@ -8120,10 +9854,13 @@ }, getUint16: function ChunkedStream_getUint16() { var b0 = this.getByte(); var b1 = this.getByte(); + if (b0 === -1 || b1 === -1) { + return -1; + } return (b0 << 8) + b1; }, getInt32: function ChunkedStream_getInt32() { var b0 = this.getByte(); @@ -8253,11 +9990,11 @@ this.currRequestId = 0; this.chunksNeededByRequest = {}; this.requestsByChunk = {}; - this.callbacksByRequest = {}; + this.promisesByRequest = {}; this.progressiveDataLength = 0; this._loadedStreamCapability = createPromiseCapability(); if (args.initialData) { @@ -8272,16 +10009,15 @@ // 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(); - this.requestChunks(missingChunks); + this._requestChunks(missingChunks); return this._loadedStreamCapability.promise; }, - requestChunks: function ChunkedStreamManager_requestChunks(chunks, - callback) { + _requestChunks: function ChunkedStreamManager_requestChunks(chunks) { var requestId = this.currRequestId++; var chunksNeeded; var i, ii; this.chunksNeededByRequest[requestId] = chunksNeeded = {}; @@ -8290,17 +10026,15 @@ chunksNeeded[chunks[i]] = true; } } if (isEmptyObj(chunksNeeded)) { - if (callback) { - callback(); - } - return; + return Promise.resolve(); } - this.callbacksByRequest[requestId] = callback; + var capability = createPromiseCapability(); + this.promisesByRequest[requestId] = capability; var chunksToRequest = []; for (var chunk in chunksNeeded) { chunk = chunk | 0; if (!(chunk in this.requestsByChunk)) { @@ -8309,30 +10043,31 @@ } this.requestsByChunk[chunk].push(requestId); } if (!chunksToRequest.length) { - return; + return capability.promise; } var groupedChunksToRequest = this.groupChunks(chunksToRequest); for (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); } + + return capability.promise; }, 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) { + requestRange: function ChunkedStreamManager_requestRange(begin, end) { end = Math.min(end, this.length); var beginChunk = this.getBeginChunk(begin); var endChunk = this.getEndChunk(end); @@ -8340,15 +10075,14 @@ var chunks = []; for (var chunk = beginChunk; chunk < endChunk; ++chunk) { chunks.push(chunk); } - this.requestChunks(chunks, callback); + return this._requestChunks(chunks); }, - requestRanges: function ChunkedStreamManager_requestRanges(ranges, - callback) { + requestRanges: function ChunkedStreamManager_requestRanges(ranges) { ranges = ranges || []; var chunksToRequest = []; for (var i = 0; i < ranges.length; i++) { var beginChunk = this.getBeginChunk(ranges[i].begin); @@ -8359,14 +10093,14 @@ } } } chunksToRequest.sort(function(a, b) { return a - b; }); - this.requestChunks(chunksToRequest, callback); + return this._requestChunks(chunksToRequest); }, - // Groups a sorted array of chunks into as few continguous larger + // Groups a sorted array of chunks into as few contiguous larger // chunks as possible groupChunks: function ChunkedStreamManager_groupChunks(chunks) { var groupedChunks = []; var beginChunk = -1; var prevChunk = -1; @@ -8458,21 +10192,19 @@ } } else { nextEmptyChunk = this.stream.nextEmptyChunk(endChunk); } if (isInt(nextEmptyChunk)) { - this.requestChunks([nextEmptyChunk]); + this._requestChunks([nextEmptyChunk]); } } for (i = 0; i < loadedRequests.length; ++i) { requestId = loadedRequests[i]; - var callback = this.callbacksByRequest[requestId]; - delete this.callbacksByRequest[requestId]; - if (callback) { - callback(); - } + var capability = this.promisesByRequest[requestId]; + delete this.promisesByRequest[requestId]; + capability.resolve(); } this.msgHandler.send('DocProgress', { loaded: this.stream.numChunksLoaded * this.chunkSize, total: this.length @@ -8487,39 +10219,39 @@ var chunk = Math.floor(begin / this.chunkSize); return chunk; }, getEndChunk: function ChunkedStreamManager_getEndChunk(end) { - if (end % this.chunkSize === 0) { - return end / this.chunkSize; - } - - // 0 -> 0 - // 1 -> 1 - // 99 -> 1 - // 100 -> 1 - // 101 -> 2 var chunk = Math.floor((end - 1) / this.chunkSize) + 1; return chunk; + }, + + abort: function ChunkedStreamManager_abort() { + if (this.networkManager) { + this.networkManager.abortAllRequests(); + } + for(var requestId in this.promisesByRequest) { + var capability = this.promisesByRequest[requestId]; + capability.reject(new Error('Request was aborted')); + } } }; return ChunkedStreamManager; })(); - -// The maximum number of bytes fetched per range request -var RANGE_CHUNK_SIZE = 65536; - -// TODO(mack): Make use of PDFJS.Util.inherit() when it becomes available var BasePdfManager = (function BasePdfManagerClosure() { function BasePdfManager() { throw new Error('Cannot initialize BaseManagerManager'); } BasePdfManager.prototype = { + get docId() { + return this._docId; + }, + onLoadedStream: function BasePdfManager_onLoadedStream() { throw new NotImplementedException(); }, ensureDoc: function BasePdfManager_ensureDoc(prop, args) { @@ -8532,11 +10264,11 @@ ensureCatalog: function BasePdfManager_ensureCatalog(prop, args) { return this.ensure(this.pdfDocument.catalog, prop, args); }, - getPage: function BasePdfManager_pagePage(pageIndex) { + getPage: function BasePdfManager_getPage(pageIndex) { return this.pdfDocument.getPage(pageIndex); }, cleanup: function BasePdfManager_cleanup() { return this.pdfDocument.cleanup(); @@ -8544,11 +10276,11 @@ ensure: function BasePdfManager_ensure(obj, prop, args) { return new NotImplementedException(); }, - requestRange: function BasePdfManager_ensure(begin, end) { + requestRange: function BasePdfManager_requestRange(begin, end) { return new NotImplementedException(); }, requestLoadedStream: function BasePdfManager_requestLoadedStream() { return new NotImplementedException(); @@ -8577,145 +10309,131 @@ return BasePdfManager; })(); var LocalPdfManager = (function LocalPdfManagerClosure() { - function LocalPdfManager(data, password) { + function LocalPdfManager(docId, data, password) { + this._docId = docId; var stream = new Stream(data); this.pdfDocument = new PDFDocument(this, stream, password); this._loadedStreamCapability = createPromiseCapability(); this._loadedStreamCapability.resolve(stream); } - LocalPdfManager.prototype = Object.create(BasePdfManager.prototype); - LocalPdfManager.prototype.constructor = LocalPdfManager; - - LocalPdfManager.prototype.ensure = - function LocalPdfManager_ensure(obj, prop, args) { - return new Promise(function (resolve, reject) { - try { - var value = obj[prop]; - var result; - if (typeof value === 'function') { - result = value.apply(obj, args); - } else { - result = value; + Util.inherit(LocalPdfManager, BasePdfManager, { + ensure: function LocalPdfManager_ensure(obj, prop, args) { + return new Promise(function (resolve, reject) { + try { + var value = obj[prop]; + var result; + if (typeof value === 'function') { + result = value.apply(obj, args); + } else { + result = value; + } + resolve(result); + } catch (e) { + reject(e); } - resolve(result); - } catch (e) { - reject(e); - } - }); - }; + }); + }, - LocalPdfManager.prototype.requestRange = - function LocalPdfManager_requestRange(begin, end) { - return Promise.resolve(); - }; + requestRange: function LocalPdfManager_requestRange(begin, end) { + return Promise.resolve(); + }, - LocalPdfManager.prototype.requestLoadedStream = - function LocalPdfManager_requestLoadedStream() { - }; + requestLoadedStream: function LocalPdfManager_requestLoadedStream() { + return; + }, - LocalPdfManager.prototype.onLoadedStream = - function LocalPdfManager_getLoadedStream() { - return this._loadedStreamCapability.promise; - }; + onLoadedStream: function LocalPdfManager_onLoadedStream() { + return this._loadedStreamCapability.promise; + }, - LocalPdfManager.prototype.terminate = - function LocalPdfManager_terminate() { - return; - }; + terminate: function LocalPdfManager_terminate() { + return; + } + }); return LocalPdfManager; })(); var NetworkPdfManager = (function NetworkPdfManagerClosure() { - function NetworkPdfManager(args, msgHandler) { - + function NetworkPdfManager(docId, args, msgHandler) { + this._docId = docId; this.msgHandler = msgHandler; var params = { msgHandler: msgHandler, httpHeaders: args.httpHeaders, withCredentials: args.withCredentials, chunkedViewerLoading: args.chunkedViewerLoading, disableAutoFetch: args.disableAutoFetch, initialData: args.initialData }; - this.streamManager = new ChunkedStreamManager(args.length, RANGE_CHUNK_SIZE, + this.streamManager = new ChunkedStreamManager(args.length, + args.rangeChunkSize, args.url, params); - this.pdfDocument = new PDFDocument(this, this.streamManager.getStream(), - args.password); + args.password); } - NetworkPdfManager.prototype = Object.create(BasePdfManager.prototype); - NetworkPdfManager.prototype.constructor = NetworkPdfManager; + Util.inherit(NetworkPdfManager, BasePdfManager, { + ensure: function NetworkPdfManager_ensure(obj, prop, args) { + var pdfManager = this; - NetworkPdfManager.prototype.ensure = - function NetworkPdfManager_ensure(obj, prop, args) { - var pdfManager = this; - - return new Promise(function (resolve, reject) { - function ensureHelper() { - try { - var result; - var value = obj[prop]; - if (typeof value === 'function') { - result = value.apply(obj, args); - } else { - result = value; + return new Promise(function (resolve, reject) { + function ensureHelper() { + try { + var result; + var value = obj[prop]; + if (typeof value === 'function') { + result = value.apply(obj, args); + } else { + result = value; + } + resolve(result); + } catch(e) { + if (!(e instanceof MissingDataException)) { + reject(e); + return; + } + pdfManager.streamManager.requestRange(e.begin, e.end). + then(ensureHelper, reject); } - resolve(result); - } catch(e) { - if (!(e instanceof MissingDataException)) { - reject(e); - return; - } - pdfManager.streamManager.requestRange(e.begin, e.end, ensureHelper); } - } - ensureHelper(); - }); - }; - - NetworkPdfManager.prototype.requestRange = - function NetworkPdfManager_requestRange(begin, end) { - return new Promise(function (resolve) { - this.streamManager.requestRange(begin, end, function() { - resolve(); + ensureHelper(); }); - }.bind(this)); - }; + }, - NetworkPdfManager.prototype.requestLoadedStream = - function NetworkPdfManager_requestLoadedStream() { - this.streamManager.requestAllChunks(); - }; + requestRange: function NetworkPdfManager_requestRange(begin, end) { + return this.streamManager.requestRange(begin, end); + }, - NetworkPdfManager.prototype.sendProgressiveData = - function NetworkPdfManager_sendProgressiveData(chunk) { - this.streamManager.onReceiveData({ chunk: chunk }); - }; + requestLoadedStream: function NetworkPdfManager_requestLoadedStream() { + this.streamManager.requestAllChunks(); + }, - NetworkPdfManager.prototype.onLoadedStream = - function NetworkPdfManager_getLoadedStream() { - return this.streamManager.onLoadedStream(); - }; + sendProgressiveData: + function NetworkPdfManager_sendProgressiveData(chunk) { + this.streamManager.onReceiveData({ chunk: chunk }); + }, - NetworkPdfManager.prototype.terminate = - function NetworkPdfManager_terminate() { - this.streamManager.networkManager.abortAllRequests(); - }; + onLoadedStream: function NetworkPdfManager_onLoadedStream() { + return this.streamManager.onLoadedStream(); + }, + terminate: function NetworkPdfManager_terminate() { + this.streamManager.abort(); + } + }); + return NetworkPdfManager; })(); - var Page = (function PageClosure() { var LETTER_SIZE_MEDIABOX = [0, 0, 612, 792]; function Page(pdfManager, xref, pageIndex, pageDict, ref, fontCache) { @@ -8734,36 +10452,48 @@ Page.prototype = { getPageProp: function Page_getPageProp(key) { return this.pageDict.get(key); }, - getInheritedPageProp: function Page_inheritPageProp(key) { - var dict = this.pageDict; - var value = dict.get(key); - while (value === undefined) { - dict = dict.get('Parent'); - if (!dict) { + getInheritedPageProp: function Page_getInheritedPageProp(key) { + var dict = this.pageDict, valueArray = null, loopCount = 0; + var MAX_LOOP_COUNT = 100; + // Always walk up the entire parent chain, to be able to find + // e.g. \Resources placed on multiple levels of the tree. + while (dict) { + var value = dict.get(key); + if (value) { + if (!valueArray) { + valueArray = []; + } + valueArray.push(value); + } + if (++loopCount > MAX_LOOP_COUNT) { + warn('Page_getInheritedPageProp: maximum loop count exceeded.'); break; } - value = dict.get(key); + dict = dict.get('Parent'); } - return value; + if (!valueArray) { + return Dict.empty; + } + if (valueArray.length === 1 || !isDict(valueArray[0]) || + loopCount > MAX_LOOP_COUNT) { + return valueArray[0]; + } + return Dict.merge(this.xref, valueArray); }, get content() { return this.getPageProp('Contents'); }, get resources() { - var value = this.getInheritedPageProp('Resources'); // For robustness: The spec states that a \Resources entry has to be - // present, but can be empty. Some document omit it still. In this case - // return an empty dictionary: - if (value === undefined) { - value = Dict.empty; - } - return shadow(this, 'resources', value); + // present, but can be empty. Some document omit it still, in this case + // we return an empty dictionary. + return shadow(this, 'resources', this.getInheritedPageProp('Resources')); }, get mediaBox() { var obj = this.getInheritedPageProp('MediaBox'); // Reset invalid media box to letter size. @@ -8789,15 +10519,10 @@ return shadow(this, 'view', mediaBox); } return shadow(this, 'view', cropBox); }, - get annotationRefs() { - return shadow(this, 'annotationRefs', - this.getInheritedPageProp('Annots')); - }, - get rotate() { var rotate = this.getInheritedPageProp('Rotate') || 0; // Normalize rotation so it's a multiple of 90 and between 0 and 270 if (rotate % 90 !== 0) { rotate = 0; @@ -8843,11 +10568,11 @@ this.xref); return objectLoader.load(); }.bind(this)); }, - getOperatorList: function Page_getOperatorList(handler, intent) { + getOperatorList: function Page_getOperatorList(handler, task, intent) { var self = this; var pdfManager = this.pdfManager; var contentStreamPromise = pdfManager.ensure(this, 'getContentStream', []); @@ -8876,12 +10601,12 @@ handler.send('StartRenderPage', { transparency: partialEvaluator.hasBlendModes(self.resources), pageIndex: self.pageIndex, intent: intent }); - return partialEvaluator.getOperatorList(contentStream, self.resources, - opList).then(function () { + return partialEvaluator.getOperatorList(contentStream, task, + self.resources, opList).then(function () { return opList; }); }); var annotationsPromise = pdfManager.ensure(this, 'annotations'); @@ -8894,19 +10619,20 @@ pageOpList.flush(true); return pageOpList; } var annotationsReadyPromise = Annotation.appendToOperatorList( - annotations, pageOpList, pdfManager, partialEvaluator, intent); + annotations, pageOpList, partialEvaluator, task, intent); return annotationsReadyPromise.then(function () { pageOpList.flush(true); return pageOpList; }); }); }, - extractTextContent: function Page_extractTextContent() { + extractTextContent: function Page_extractTextContent(task, + normalizeWhitespace) { var handler = { on: function nullHandlerOn() {}, send: function nullHandlerSend() {} }; @@ -8931,29 +10657,39 @@ 'p' + self.pageIndex + '_', self.idCounters, self.fontCache); return partialEvaluator.getTextContent(contentStream, - self.resources); + task, + self.resources, + /* stateManager = */ null, + normalizeWhitespace); }); }, - getAnnotationsData: function Page_getAnnotationsData() { + getAnnotationsData: function Page_getAnnotationsData(intent) { var annotations = this.annotations; var annotationsData = []; for (var i = 0, n = annotations.length; i < n; ++i) { - annotationsData.push(annotations[i].getData()); + if (intent) { + if (!(intent === 'display' && annotations[i].viewable) && + !(intent === 'print' && annotations[i].printable)) { + continue; + } + } + annotationsData.push(annotations[i].data); } return annotationsData; }, get annotations() { var annotations = []; - var annotationRefs = (this.annotationRefs || []); + var annotationRefs = this.getInheritedPageProp('Annots') || []; + var annotationFactory = new AnnotationFactory(); for (var i = 0, n = annotationRefs.length; i < n; ++i) { var annotationRef = annotationRefs[i]; - var annotation = Annotation.fromRef(this.xref, annotationRef); + var annotation = annotationFactory.create(this.xref, annotationRef); if (annotation) { annotations.push(annotation); } } return shadow(this, 'annotations', annotations); @@ -8969,10 +10705,14 @@ * Right now there exists one PDFDocument on the main thread + one object * for each worker. If there is no worker support enabled, there are two * `PDFDocument` objects on the main thread created. */ var PDFDocument = (function PDFDocumentClosure() { + var FINGERPRINT_FIRST_BYTES = 1024; + var EMPTY_FINGERPRINT = '\x00\x00\x00\x00\x00\x00\x00' + + '\x00\x00\x00\x00\x00\x00\x00\x00\x00'; + function PDFDocument(pdfManager, arg, password) { if (isStream(arg)) { init.call(this, pdfManager, arg, password); } else if (isArrayBuffer(arg)) { init.call(this, pdfManager, new Stream(arg), password); @@ -9028,10 +10768,14 @@ }; PDFDocument.prototype = { parse: function PDFDocument_parse(recoveryMode) { this.setup(recoveryMode); + var version = this.catalog.catDict.get('Version'); + if (isName(version)) { + this.pdfFormatVersion = version.name; + } try { // checking if AcroForm is present this.acroForm = this.catalog.catDict.get('AcroForm'); if (this.acroForm) { this.xfa = this.acroForm.get('XFA'); @@ -9129,12 +10873,14 @@ if (version.length >= MAX_VERSION_LENGTH) { break; } version += String.fromCharCode(ch); } - // removing "%PDF-"-prefix - this.pdfFormatVersion = version.substring(5); + if (!this.pdfFormatVersion) { + // removing "%PDF-"-prefix + this.pdfFormatVersion = version.substring(5); + } return; } // May not be a PDF file, continue anyway. }, parseStartXRef: function PDFDocument_parseStartXRef() { @@ -9181,19 +10927,27 @@ } return shadow(this, 'documentInfo', docInfo); }, get fingerprint() { var xref = this.xref, hash, fileID = ''; + var idArray = xref.trailer.get('ID'); - if (xref.trailer.has('ID')) { - hash = stringToBytes(xref.trailer.get('ID')[0]); + if (idArray && isArray(idArray) && idArray[0] && isString(idArray[0]) && + idArray[0] !== EMPTY_FINGERPRINT) { + hash = stringToBytes(idArray[0]); } else { - hash = calculateMD5(this.stream.bytes.subarray(0, 100), 0, 100); + if (this.stream.ensureRange) { + this.stream.ensureRange(0, + Math.min(FINGERPRINT_FIRST_BYTES, this.stream.end)); + } + hash = calculateMD5(this.stream.bytes.subarray(0, + FINGERPRINT_FIRST_BYTES), 0, FINGERPRINT_FIRST_BYTES); } for (var i = 0, n = hash.length; i < n; i++) { - fileID += hash[i].toString(16); + var hex = hash[i].toString(16); + fileID += hex.length === 1 ? '0' + hex : hex; } return shadow(this, 'fingerprint', fileID); }, @@ -9208,11 +10962,10 @@ return PDFDocument; })(); - var Name = (function NameClosure() { function Name(name) { this.name = name; } @@ -9321,10 +11074,27 @@ return xref.fetchIfRefAsync(value); } return Promise.resolve(value); }, + // Same as get(), but dereferences all elements if the result is an Array. + getArray: function Dict_getArray(key1, key2, key3) { + var value = this.get(key1, key2, key3); + var xref = this.xref; + if (!isArray(value) || !xref) { + return value; + } + value = value.slice(); // Ensure that we don't modify the Dict data. + for (var i = 0, ii = value.length; i < ii; i++) { + if (!isRef(value[i])) { + continue; + } + value[i] = xref.fetch(value[i]); + } + return value; + }, + // no dereferencing getRaw: function Dict_getRaw(key) { return this.map[key]; }, @@ -9378,10 +11148,14 @@ item.target[item.key] = dereferenced; } return all; }, + getKeys: function Dict_getKeys() { + return Object.keys(this.map); + }, + set: function Dict_set(key, value) { this.map[key] = value; }, has: function Dict_has(key) { @@ -9395,10 +11169,28 @@ } }; Dict.empty = new Dict(null); + Dict.merge = function Dict_merge(xref, dictArray) { + var mergedDict = new Dict(xref); + + for (var i = 0, ii = dictArray.length; i < ii; i++) { + var dict = dictArray[i]; + if (!isDict(dict)) { + continue; + } + for (var keyName in dict.map) { + if (mergedDict.map[keyName]) { + continue; + } + mergedDict.map[keyName] = dict.map[keyName]; + } + } + return mergedDict; + }; + return Dict; })(); var Ref = (function RefClosure() { function Ref(num, gen) { @@ -9649,29 +11441,23 @@ function fetchDestination(dest) { return isDict(dest) ? dest.get('D') : dest; } var xref = this.xref; - var dest, nameTreeRef, nameDictionaryRef; + var dest = null, nameTreeRef, nameDictionaryRef; var obj = this.catDict.get('Names'); if (obj && obj.has('Dests')) { nameTreeRef = obj.getRaw('Dests'); } else if (this.catDict.has('Dests')) { nameDictionaryRef = this.catDict.get('Dests'); } - if (nameDictionaryRef) { - // reading simple destination dictionary - obj = nameDictionaryRef; - obj.forEach(function catalogForEach(key, value) { - if (!value) { - return; - } - if (key === destinationId) { - dest = fetchDestination(value); - } - }); + if (nameDictionaryRef) { // Simple destination dictionary. + var value = nameDictionaryRef.get(destinationId); + if (value) { + dest = fetchDestination(value); + } } if (nameTreeRef) { var nameTree = new NameTree(nameTreeRef, xref); dest = fetchDestination(nameTree.get(destinationId)); } @@ -9704,50 +11490,52 @@ get javaScript() { var xref = this.xref; var obj = this.catDict.get('Names'); var javaScript = []; + function appendIfJavaScriptDict(jsDict) { + var type = jsDict.get('S'); + if (!isName(type) || type.name !== 'JavaScript') { + return; + } + var js = jsDict.get('JS'); + if (isStream(js)) { + js = bytesToString(js.getBytes()); + } else if (!isString(js)) { + return; + } + javaScript.push(stringToPDFString(js)); + } if (obj && obj.has('JavaScript')) { var nameTree = new NameTree(obj.getRaw('JavaScript'), xref); var names = nameTree.getAll(); for (var name in names) { if (!names.hasOwnProperty(name)) { continue; } // We don't really use the JavaScript right now. This code is // defensive so we don't cause errors on document load. var jsDict = names[name]; - if (!isDict(jsDict)) { - continue; + if (isDict(jsDict)) { + appendIfJavaScriptDict(jsDict); } - var type = jsDict.get('S'); - if (!isName(type) || type.name !== 'JavaScript') { - continue; - } - var js = jsDict.get('JS'); - if (!isString(js) && !isStream(js)) { - continue; - } - if (isStream(js)) { - js = bytesToString(js.getBytes()); - } - javaScript.push(stringToPDFString(js)); } } // Append OpenAction actions to javaScript array var openactionDict = this.catDict.get('OpenAction'); - if (isDict(openactionDict)) { - var objType = openactionDict.get('Type'); + if (isDict(openactionDict, 'Action')) { var actionType = openactionDict.get('S'); - var action = openactionDict.get('N'); - var isPrintAction = (isName(objType) && objType.name === 'Action' && - isName(actionType) && actionType.name === 'Named' && - isName(action) && action.name === 'Print'); - - if (isPrintAction) { - javaScript.push('print(true);'); + if (isName(actionType) && actionType.name === 'Named') { + // The named Print action is not a part of the PDF 1.7 specification, + // but is supported by many PDF readers/writers (including Adobe's). + var action = openactionDict.get('N'); + if (isName(action) && action.name === 'Print') { + javaScript.push('print({});'); + } + } else { + appendIfJavaScriptDict(openactionDict); } } return shadow(this, 'javaScript', javaScript); }, @@ -9783,18 +11571,19 @@ getPageDict: function Catalog_getPageDict(pageIndex) { var capability = createPromiseCapability(); var nodesToVisit = [this.catDict.getRaw('Pages')]; var currentPageIndex = 0; var xref = this.xref; + var checkAllKids = false; function next() { while (nodesToVisit.length) { var currentNode = nodesToVisit.pop(); if (isRef(currentNode)) { xref.fetchAsync(currentNode).then(function (obj) { - if ((isDict(obj, 'Page') || (isDict(obj) && !obj.has('Kids')))) { + if (isDict(obj, 'Page') || (isDict(obj) && !obj.has('Kids'))) { if (pageIndex === currentPageIndex) { capability.resolve([obj, currentNode]); } else { currentPageIndex++; next(); @@ -9805,25 +11594,30 @@ next(); }, capability.reject); return; } - // must be a child page dictionary + // Must be a child page dictionary. assert( isDict(currentNode), 'page dictionary kid reference points to wrong type of object' ); var count = currentNode.get('Count'); + // If the current node doesn't have any children, avoid getting stuck + // in an empty node further down in the tree (see issue5644.pdf). + if (count === 0) { + checkAllKids = true; + } // Skip nodes where the page can't be. if (currentPageIndex + count <= pageIndex) { currentPageIndex += count; continue; } var kids = currentNode.get('Kids'); assert(isArray(kids), 'page dictionary kids object is not an array'); - if (count === kids.length) { + if (!checkAllKids && count === kids.length) { // Nodes that don't have the page have been skipped and this is the // bottom of the tree which means the page requested must be a // descendant of this pages node. Ideally we would just resolve the // promise with the page ref here, but there is the case where more // pages nodes could link to single a page (see issue 3666 pdf). To @@ -10175,13 +11969,16 @@ }, indexObjects: function XRef_indexObjects() { // Simple scan through the PDF content to find objects, // trailers and XRef streams. + var TAB = 0x9, LF = 0xA, CR = 0xD, SPACE = 0x20; + var PERCENT = 0x25, LT = 0x3C; + function readToken(data, offset) { var token = '', ch = data[offset]; - while (ch !== 13 && ch !== 10) { + while (ch !== LF && ch !== CR && ch !== LT) { if (++offset >= data.length) { break; } token += String.fromCharCode(ch); ch = data[offset]; @@ -10203,63 +12000,73 @@ offset++; skipped++; } return skipped; } + var objRegExp = /^(\d+)\s+(\d+)\s+obj\b/; var trailerBytes = new Uint8Array([116, 114, 97, 105, 108, 101, 114]); var startxrefBytes = new Uint8Array([115, 116, 97, 114, 116, 120, 114, 101, 102]); var endobjBytes = new Uint8Array([101, 110, 100, 111, 98, 106]); var xrefBytes = new Uint8Array([47, 88, 82, 101, 102]); + // Clear out any existing entries, since they may be bogus. + this.entries.length = 0; + var stream = this.stream; stream.pos = 0; var buffer = stream.getBytes(); var position = stream.start, length = buffer.length; var trailers = [], xrefStms = []; while (position < length) { var ch = buffer[position]; - if (ch === 32 || ch === 9 || ch === 13 || ch === 10) { + if (ch === TAB || ch === LF || ch === CR || ch === SPACE) { ++position; continue; } - if (ch === 37) { // %-comment + if (ch === PERCENT) { // %-comment do { ++position; if (position >= length) { break; } ch = buffer[position]; - } while (ch !== 13 && ch !== 10); + } while (ch !== LF && ch !== CR); continue; } var token = readToken(buffer, position); var m; - if (token === 'xref') { + if (token.indexOf('xref') === 0 && + (token.length === 4 || /\s/.test(token[4]))) { position += skipUntil(buffer, position, trailerBytes); trailers.push(position); position += skipUntil(buffer, position, startxrefBytes); - } else if ((m = /^(\d+)\s+(\d+)\s+obj\b/.exec(token))) { - this.entries[m[1]] = { - offset: position, - gen: m[2] | 0, - uncompressed: true - }; - + } else if ((m = objRegExp.exec(token))) { + if (typeof this.entries[m[1]] === 'undefined') { + this.entries[m[1]] = { + offset: position - stream.start, + gen: m[2] | 0, + uncompressed: true + }; + } var contentLength = skipUntil(buffer, position, endobjBytes) + 7; var content = buffer.subarray(position, position + contentLength); // checking XRef stream suspect // (it shall have '/XRef' and next char is not a letter) var xrefTagOffset = skipUntil(content, 0, xrefBytes); if (xrefTagOffset < contentLength && content[xrefTagOffset + 5] < 64) { - xrefStms.push(position); - this.xrefstms[position] = 1; // don't read it recursively + xrefStms.push(position - stream.start); + this.xrefstms[position - stream.start] = 1; // Avoid recursion } position += contentLength; + } else if (token.indexOf('trailer') === 0 && + (token.length === 7 || /\s/.test(token[7]))) { + trailers.push(position); + position += skipUntil(buffer, position, startxrefBytes); } else { position += token.length + 1; } } // reading XRef streams @@ -10510,13 +12317,13 @@ return new Promise(function tryFetch(resolve, reject) { try { resolve(xref.fetch(ref, suppressEncryption)); } catch (e) { if (e instanceof MissingDataException) { - streamManager.requestRange(e.begin, e.end, function () { + streamManager.requestRange(e.begin, e.end).then(function () { tryFetch(resolve, reject); - }); + }, reject); return; } reject(e); } }); @@ -10571,11 +12378,11 @@ continue; } var names = obj.get('Names'); if (names) { for (i = 0, n = names.length; i < n; i += 2) { - dict[names[i]] = xref.fetchIfRef(names[i + 1]); + dict[xref.fetchIfRef(names[i])] = xref.fetchIfRef(names[i + 1]); } } } return dict; }, @@ -10597,11 +12404,11 @@ loopCount++; if (loopCount > MAX_NAMES_LEVELS) { warn('Search depth limit for named destionations has been reached.'); return null; } - + var kids = kidsOrNames.get('Kids'); if (!isArray(kids)) { return null; } @@ -10610,13 +12417,13 @@ while (l <= r) { m = (l + r) >> 1; var kid = xref.fetchIfRef(kids[m]); var limits = kid.get('Limits'); - if (destinationId < limits[0]) { + if (destinationId < xref.fetchIfRef(limits[0])) { r = m - 1; - } else if (destinationId > limits[1]) { + } else if (destinationId > xref.fetchIfRef(limits[1])) { l = m + 1; } else { kidsOrNames = xref.fetchIfRef(kids[m]); break; } @@ -10636,13 +12443,13 @@ r = names.length - 2; while (l <= r) { // Check only even indices (0, 2, 4, ...) because the // odd indices contain the actual D array. m = (l + r) & ~1; - if (destinationId < names[m]) { + if (destinationId < xref.fetchIfRef(names[m])) { r = m - 2; - } else if (destinationId > names[m]) { + } else if (destinationId > xref.fetchIfRef(names[m])) { l = m + 2; } else { return xref.fetchIfRef(names[m + 1]); } } @@ -10652,14 +12459,14 @@ }; return NameTree; })(); /** - * "A PDF file can refer to the contents of another file by using a File + * "A PDF file can refer to the contents of another file by using a File * Specification (PDF 1.1)", see the spec (7.11) for more details. * NOTE: Only embedded files are supported (as part of the attachments support) - * TODO: support the 'URL' file system (with caching if !/V), portable + * TODO: support the 'URL' file system (with caching if !/V), portable * collections attributes and related files (/RF) */ var FileSpec = (function FileSpecClosure() { function FileSpec(root, xref) { if (!root || !isDict(root)) { @@ -10788,10 +12595,11 @@ function ObjectLoader(obj, keys, xref) { this.obj = obj; this.keys = keys; this.xref = xref; this.refSet = null; + this.capability = null; } ObjectLoader.prototype = { load: function ObjectLoader_load() { var keys = this.keys; @@ -10808,15 +12616,15 @@ var nodesToVisit = []; for (var i = 0; i < keys.length; i++) { nodesToVisit.push(this.obj[keys[i]]); } - this.walk(nodesToVisit); + this._walk(nodesToVisit); return this.capability.promise; }, - walk: function ObjectLoader_walk(nodesToVisit) { + _walk: function ObjectLoader_walk(nodesToVisit) { var nodesToRevisit = []; var pendingRequests = []; // DFS walk of the object graph. while (nodesToVisit.length) { var currentNode = nodesToVisit.pop(); @@ -10859,23 +12667,23 @@ addChildren(currentNode, nodesToVisit); } if (pendingRequests.length) { - this.xref.stream.manager.requestRanges(pendingRequests, + this.xref.stream.manager.requestRanges(pendingRequests).then( 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)); + this._walk(nodesToVisit); + }.bind(this), this.capability.reject); return; } // Everything is loaded. this.refSet = null; this.capability.resolve(); @@ -10985,14 +12793,61 @@ 'eightinferior', 'nineinferior', 'centinferior', 'dollarinferior', 'periodinferior', 'commainferior' ]; - var DEFAULT_ICON_SIZE = 22; // px -var SUPPORTED_TYPES = ['Link', 'Text', 'Widget']; +/** + * @class + * @alias AnnotationFactory + */ +function AnnotationFactory() {} +AnnotationFactory.prototype = /** @lends AnnotationFactory.prototype */ { + /** + * @param {XRef} xref + * @param {Object} ref + * @returns {Annotation} + */ + create: function AnnotationFactory_create(xref, ref) { + var dict = xref.fetchIfRef(ref); + if (!isDict(dict)) { + return; + } + + // Determine the annotation's subtype. + var subtype = dict.get('Subtype'); + subtype = isName(subtype) ? subtype.name : ''; + + // Return the right annotation object based on the subtype and field type. + var parameters = { + dict: dict, + ref: ref + }; + + switch (subtype) { + case 'Link': + return new LinkAnnotation(parameters); + + case 'Text': + return new TextAnnotation(parameters); + + case 'Widget': + var fieldType = Util.getInheritableProperty(dict, 'FT'); + if (isName(fieldType) && fieldType.name === 'Tx') { + return new TextWidgetAnnotation(parameters); + } + return new WidgetAnnotation(parameters); + + default: + warn('Unimplemented annotation type "' + subtype + '", ' + + 'falling back to base annotation'); + return new Annotation(parameters); + } + } +}; + 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]; @@ -11037,105 +12892,185 @@ return appearance; } function Annotation(params) { var dict = params.dict; - var data = this.data = {}; - data.subtype = dict.get('Subtype').name; - var rect = dict.get('Rect') || [0, 0, 0, 0]; - data.rect = Util.normalizeRect(rect); - data.annotationFlags = dict.get('F'); + this.setFlags(dict.get('F')); + this.setRectangle(dict.get('Rect')); + this.setColor(dict.get('C')); + this.setBorderStyle(dict); + this.appearance = getDefaultAppearance(dict); - 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]; - } + // Expose public properties using a data object. + this.data = {}; + this.data.id = params.ref.num; + this.data.subtype = dict.get('Subtype').name; + this.data.annotationFlags = this.flags; + this.data.rect = this.rectangle; + this.data.color = this.color; + this.data.borderStyle = this.borderStyle; + this.data.hasAppearance = !!this.appearance; + } - // 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] || 0; + Annotation.prototype = { + /** + * @return {boolean} + */ + get viewable() { + if (this.flags) { + return !this.hasFlag(AnnotationFlag.INVISIBLE) && + !this.hasFlag(AnnotationFlag.HIDDEN) && + !this.hasFlag(AnnotationFlag.NOVIEW); + } + return true; + }, - // TODO: implement proper support for annotations with line dash patterns. - var dashArray = borderArray[3]; - if (data.borderWidth > 0 && dashArray) { - if (!isArray(dashArray)) { - // Ignore the border if dashArray is not actually an array, - // this is consistent with the behaviour in Adobe Reader. - data.borderWidth = 0; - } else { - var dashArrayLength = dashArray.length; - if (dashArrayLength > 0) { - // According to the PDF specification: the elements in a dashArray - // shall be numbers that are nonnegative and not all equal to zero. - var isInvalid = false; - var numPositive = 0; - for (var i = 0; i < dashArrayLength; i++) { - var validNumber = (+dashArray[i] >= 0); - if (!validNumber) { - isInvalid = true; - break; - } else if (dashArray[i] > 0) { - numPositive++; - } - } - if (isInvalid || numPositive === 0) { - data.borderWidth = 0; - } - } - } + /** + * @return {boolean} + */ + get printable() { + if (this.flags) { + return this.hasFlag(AnnotationFlag.PRINT) && + !this.hasFlag(AnnotationFlag.INVISIBLE) && + !this.hasFlag(AnnotationFlag.HIDDEN); } - } + return false; + }, - this.appearance = getDefaultAppearance(dict); - data.hasAppearance = !!this.appearance; - data.id = params.ref.num; - } + /** + * Set the flags. + * + * @public + * @memberof Annotation + * @param {number} flags - Unsigned 32-bit integer specifying annotation + * characteristics + * @see {@link shared/util.js} + */ + setFlags: function Annotation_setFlags(flags) { + if (isInt(flags)) { + this.flags = flags; + } else { + this.flags = 0; + } + }, - Annotation.prototype = { - - getData: function Annotation_getData() { - return this.data; + /** + * Check if a provided flag is set. + * + * @public + * @memberof Annotation + * @param {number} flag - Hexadecimal representation for an annotation + * characteristic + * @return {boolean} + * @see {@link shared/util.js} + */ + hasFlag: function Annotation_hasFlag(flag) { + if (this.flags) { + return (this.flags & flag) > 0; + } + return false; }, - isInvisible: function Annotation_isInvisible() { - var data = this.data; - if (data && SUPPORTED_TYPES.indexOf(data.subtype) !== -1) { - return false; + /** + * Set the rectangle. + * + * @public + * @memberof Annotation + * @param {Array} rectangle - The rectangle array with exactly four entries + */ + setRectangle: function Annotation_setRectangle(rectangle) { + if (isArray(rectangle) && rectangle.length === 4) { + this.rectangle = Util.normalizeRect(rectangle); } else { - return !!(data && - data.annotationFlags && // Default: not invisible - data.annotationFlags & 0x1); // Invisible + this.rectangle = [0, 0, 0, 0]; } }, - isViewable: function Annotation_isViewable() { - var data = this.data; - return !!(!this.isInvisible() && - data && - (!data.annotationFlags || - !(data.annotationFlags & 0x22)) && // Hidden or NoView - data.rect); // rectangle is necessary + /** + * Set the color and take care of color space conversion. + * + * @public + * @memberof Annotation + * @param {Array} color - The color array containing either 0 + * (transparent), 1 (grayscale), 3 (RGB) or + * 4 (CMYK) elements + */ + setColor: function Annotation_setColor(color) { + var rgbColor = new Uint8Array(3); // Black in RGB color space (default) + if (!isArray(color)) { + this.color = rgbColor; + return; + } + + switch (color.length) { + case 0: // Transparent, which we indicate with a null value + this.color = null; + break; + + case 1: // Convert grayscale to RGB + ColorSpace.singletons.gray.getRgbItem(color, 0, rgbColor, 0); + this.color = rgbColor; + break; + + case 3: // Convert RGB percentages to RGB + ColorSpace.singletons.rgb.getRgbItem(color, 0, rgbColor, 0); + this.color = rgbColor; + break; + + case 4: // Convert CMYK to RGB + ColorSpace.singletons.cmyk.getRgbItem(color, 0, rgbColor, 0); + this.color = rgbColor; + break; + + default: + this.color = rgbColor; + break; + } }, - isPrintable: function Annotation_isPrintable() { - var data = this.data; - return !!(!this.isInvisible() && - data && - data.annotationFlags && // Default: not printable - data.annotationFlags & 0x4 && // Print - !(data.annotationFlags & 0x2) && // Hidden - data.rect); // rectangle is necessary + /** + * Set the border style (as AnnotationBorderStyle object). + * + * @public + * @memberof Annotation + * @param {Dict} borderStyle - The border style dictionary + */ + setBorderStyle: function Annotation_setBorderStyle(borderStyle) { + this.borderStyle = new AnnotationBorderStyle(); + if (!isDict(borderStyle)) { + return; + } + if (borderStyle.has('BS')) { + var dict = borderStyle.get('BS'); + var dictType; + + if (!dict.has('Type') || (isName(dictType = dict.get('Type')) && + dictType.name === 'Border')) { + this.borderStyle.setWidth(dict.get('W')); + this.borderStyle.setStyle(dict.get('S')); + this.borderStyle.setDashArray(dict.get('D')); + } + } else if (borderStyle.has('Border')) { + var array = borderStyle.get('Border'); + if (isArray(array) && array.length >= 3) { + this.borderStyle.setHorizontalCornerRadius(array[0]); + this.borderStyle.setVerticalCornerRadius(array[1]); + this.borderStyle.setWidth(array[2]); + + if (array.length === 4) { // Dash array available + this.borderStyle.setDashArray(array[3]); + } + } + } else { + // There are no border entries in the dictionary. According to the + // specification, we should draw a solid border of width 1 in that + // case, but Adobe Reader did not implement that part of the + // specification and instead draws no border at all, so we do the same. + // See also https://github.com/mozilla/pdf.js/issues/6179. + this.borderStyle.setWidth(0); + } }, loadResources: function Annotation_loadResources(keys) { return new Promise(function (resolve, reject) { this.appearance.dict.getAsync('Resources').then(function (resources) { @@ -11151,18 +13086,16 @@ }, reject); }, reject); }.bind(this)); }, - getOperatorList: function Annotation_getOperatorList(evaluator) { - + getOperatorList: function Annotation_getOperatorList(evaluator, task) { if (!this.appearance) { return Promise.resolve(new OperatorList()); } var data = this.data; - var appearanceDict = this.appearance.dict; var resourcesPromise = this.loadResources([ 'ExtGState', 'ColorSpace', 'Pattern', @@ -11178,134 +13111,204 @@ var self = this; return resourcesPromise.then(function(resources) { var opList = new OperatorList(); opList.addOp(OPS.beginAnnotation, [data.rect, transform, matrix]); - return evaluator.getOperatorList(self.appearance, resources, opList). + return evaluator.getOperatorList(self.appearance, task, + resources, opList). then(function () { opList.addOp(OPS.endAnnotation, []); self.appearance.reset(); return opList; }); }); } }; - Annotation.getConstructor = - function Annotation_getConstructor(subtype, fieldType) { - - if (!subtype) { - return; - } - - // TODO(mack): Implement FreeText annotations - if (subtype === 'Link') { - return LinkAnnotation; - } else if (subtype === 'Text') { - return TextAnnotation; - } else if (subtype === 'Widget') { - if (!fieldType) { - return; + Annotation.appendToOperatorList = function Annotation_appendToOperatorList( + annotations, opList, partialEvaluator, task, intent) { + var annotationPromises = []; + for (var i = 0, n = annotations.length; i < n; ++i) { + if ((intent === 'display' && annotations[i].viewable) || + (intent === 'print' && annotations[i].printable)) { + annotationPromises.push( + annotations[i].getOperatorList(partialEvaluator, task)); } - - if (fieldType === 'Tx') { - return TextWidgetAnnotation; - } else { - return WidgetAnnotation; - } - } else { - return Annotation; } + return Promise.all(annotationPromises).then(function(operatorLists) { + opList.addOp(OPS.beginAnnotations, []); + for (var i = 0, n = operatorLists.length; i < n; ++i) { + opList.addOpList(operatorLists[i]); + } + opList.addOp(OPS.endAnnotations, []); + }); }; - Annotation.fromRef = function Annotation_fromRef(xref, ref) { + return Annotation; +})(); - var dict = xref.fetchIfRef(ref); - if (!isDict(dict)) { - return; - } +/** + * Contains all data regarding an annotation's border style. + * + * @class + */ +var AnnotationBorderStyle = (function AnnotationBorderStyleClosure() { + /** + * @constructor + * @private + */ + function AnnotationBorderStyle() { + this.width = 1; + this.style = AnnotationBorderStyleType.SOLID; + this.dashArray = [3]; + this.horizontalCornerRadius = 0; + this.verticalCornerRadius = 0; + } - var subtype = dict.get('Subtype'); - subtype = isName(subtype) ? subtype.name : ''; - if (!subtype) { - return; - } + AnnotationBorderStyle.prototype = { + /** + * Set the width. + * + * @public + * @memberof AnnotationBorderStyle + * @param {integer} width - The width + */ + setWidth: function AnnotationBorderStyle_setWidth(width) { + if (width === (width | 0)) { + this.width = width; + } + }, - var fieldType = Util.getInheritableProperty(dict, 'FT'); - fieldType = isName(fieldType) ? fieldType.name : ''; + /** + * Set the style. + * + * @public + * @memberof AnnotationBorderStyle + * @param {Object} style - The style object + * @see {@link shared/util.js} + */ + setStyle: function AnnotationBorderStyle_setStyle(style) { + if (!style) { + return; + } + switch (style.name) { + case 'S': + this.style = AnnotationBorderStyleType.SOLID; + break; - var Constructor = Annotation.getConstructor(subtype, fieldType); - if (!Constructor) { - return; - } + case 'D': + this.style = AnnotationBorderStyleType.DASHED; + break; - var params = { - dict: dict, - ref: ref, - }; + case 'B': + this.style = AnnotationBorderStyleType.BEVELED; + break; - var annotation = new Constructor(params); + case 'I': + this.style = AnnotationBorderStyleType.INSET; + break; - if (annotation.isViewable() || annotation.isPrintable()) { - return annotation; - } else { - if (SUPPORTED_TYPES.indexOf(subtype) === -1) { - warn('unimplemented annotation type: ' + subtype); + case 'U': + this.style = AnnotationBorderStyleType.UNDERLINE; + break; + + default: + break; } - } - }; + }, - Annotation.appendToOperatorList = function Annotation_appendToOperatorList( - annotations, opList, pdfManager, partialEvaluator, intent) { + /** + * Set the dash array. + * + * @public + * @memberof AnnotationBorderStyle + * @param {Array} dashArray - The dash array with at least one element + */ + setDashArray: function AnnotationBorderStyle_setDashArray(dashArray) { + // We validate the dash array, but we do not use it because CSS does not + // allow us to change spacing of dashes. For more information, visit + // http://www.w3.org/TR/css3-background/#the-border-style. + if (isArray(dashArray) && dashArray.length > 0) { + // According to the PDF specification: the elements in a dashArray + // shall be numbers that are nonnegative and not all equal to zero. + var isValid = true; + var allZeros = true; + for (var i = 0, len = dashArray.length; i < len; i++) { + var element = dashArray[i]; + var validNumber = (+element >= 0); + if (!validNumber) { + isValid = false; + break; + } else if (element > 0) { + allZeros = false; + } + } + if (isValid && !allZeros) { + this.dashArray = dashArray; + } else { + this.width = 0; // Adobe behavior when the array is invalid. + } + } else if (dashArray) { + this.width = 0; // Adobe behavior when the array is invalid. + } + }, - function reject(e) { - annotationsReadyCapability.reject(e); - } + /** + * Set the horizontal corner radius (from a Border dictionary). + * + * @public + * @memberof AnnotationBorderStyle + * @param {integer} radius - The horizontal corner radius + */ + setHorizontalCornerRadius: + function AnnotationBorderStyle_setHorizontalCornerRadius(radius) { + if (radius === (radius | 0)) { + this.horizontalCornerRadius = radius; + } + }, - var annotationsReadyCapability = createPromiseCapability(); - - var annotationPromises = []; - for (var i = 0, n = annotations.length; i < n; ++i) { - if (intent === 'display' && annotations[i].isViewable() || - intent === 'print' && annotations[i].isPrintable()) { - annotationPromises.push( - annotations[i].getOperatorList(partialEvaluator)); + /** + * Set the vertical corner radius (from a Border dictionary). + * + * @public + * @memberof AnnotationBorderStyle + * @param {integer} radius - The vertical corner radius + */ + setVerticalCornerRadius: + function AnnotationBorderStyle_setVerticalCornerRadius(radius) { + if (radius === (radius | 0)) { + this.verticalCornerRadius = radius; } } - Promise.all(annotationPromises).then(function(datas) { - opList.addOp(OPS.beginAnnotations, []); - for (var i = 0, n = datas.length; i < n; ++i) { - var annotOpList = datas[i]; - opList.addOpList(annotOpList); - } - opList.addOp(OPS.endAnnotations, []); - annotationsReadyCapability.resolve(); - }, reject); - - return annotationsReadyCapability.promise; }; - return Annotation; + return AnnotationBorderStyle; })(); var WidgetAnnotation = (function WidgetAnnotationClosure() { - function WidgetAnnotation(params) { Annotation.call(this, params); var dict = params.dict; var data = this.data; + data.annotationType = AnnotationType.WIDGET; data.fieldValue = stringToPDFString( Util.getInheritableProperty(dict, 'V') || ''); data.alternativeText = stringToPDFString(dict.get('TU') || ''); data.defaultAppearance = Util.getInheritableProperty(dict, 'DA') || ''; var fieldType = Util.getInheritableProperty(dict, 'FT'); data.fieldType = isName(fieldType) ? fieldType.name : ''; data.fieldFlags = Util.getInheritableProperty(dict, 'Ff') || 0; this.fieldResources = Util.getInheritableProperty(dict, 'DR') || Dict.empty; + // Hide unsupported Widget signatures. + if (data.fieldType === 'Sig') { + warn('unimplemented annotation type: Widget signature'); + this.setFlags(AnnotationFlag.HIDDEN); + } + // Building the full field name by collecting the field and // its ancestors 'T' data and joining them using '.'. var fieldName = []; var namedItem = dict; var ref = params.ref; @@ -11313,11 +13316,11 @@ var parent = namedItem.get('Parent'); var parentRef = namedItem.getRaw('Parent'); var name = namedItem.get('T'); if (name) { fieldName.unshift(stringToPDFString(name)); - } else { + } else if (parent && ref) { // The field name is absent, that means more than one field // with the same name may exist. Replacing the empty name // with the '`' plus index in the parent's 'Kids' array. // This is not in the PDF spec but necessary to id the // the input controls. @@ -11335,38 +13338,28 @@ ref = parentRef; } data.fullName = fieldName.join('.'); } - var parent = Annotation.prototype; - Util.inherit(WidgetAnnotation, Annotation, { - isViewable: function WidgetAnnotation_isViewable() { - if (this.data.fieldType === 'Sig') { - warn('unimplemented annotation type: Widget signature'); - return false; - } + Util.inherit(WidgetAnnotation, Annotation, {}); - return parent.isViewable.call(this); - } - }); - return WidgetAnnotation; })(); var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() { function TextWidgetAnnotation(params) { WidgetAnnotation.call(this, params); this.data.textAlignment = Util.getInheritableProperty(params.dict, 'Q'); - this.data.annotationType = AnnotationType.WIDGET; this.data.hasHtml = !this.data.hasAppearance && !!this.data.fieldValue; } Util.inherit(TextWidgetAnnotation, WidgetAnnotation, { - getOperatorList: function TextWidgetAnnotation_getOperatorList(evaluator) { + getOperatorList: function TextWidgetAnnotation_getOperatorList(evaluator, + task) { if (this.appearance) { - return Annotation.prototype.getOperatorList.call(this, evaluator); + return Annotation.prototype.getOperatorList.call(this, evaluator, task); } var opList = new OperatorList(); var data = this.data; @@ -11375,44 +13368,34 @@ if (!data.defaultAppearance) { return Promise.resolve(opList); } var stream = new Stream(stringToBytes(data.defaultAppearance)); - return evaluator.getOperatorList(stream, this.fieldResources, opList). + return evaluator.getOperatorList(stream, task, + this.fieldResources, opList). then(function () { return opList; }); } }); return TextWidgetAnnotation; })(); -var InteractiveAnnotation = (function InteractiveAnnotationClosure() { - function InteractiveAnnotation(params) { - Annotation.call(this, params); - - this.data.hasHtml = true; - } - - Util.inherit(InteractiveAnnotation, Annotation, { }); - - return InteractiveAnnotation; -})(); - var TextAnnotation = (function TextAnnotationClosure() { function TextAnnotation(params) { - InteractiveAnnotation.call(this, params); + Annotation.call(this, params); var dict = params.dict; var data = this.data; var content = dict.get('Contents'); var title = dict.get('T'); data.annotationType = AnnotationType.TEXT; data.content = stringToPDFString(content || ''); data.title = stringToPDFString(title || ''); + data.hasHtml = true; if (data.hasAppearance) { data.name = 'NoIcon'; } else { data.rect[1] = data.rect[3] - DEFAULT_ICON_SIZE; @@ -11423,25 +13406,26 @@ if (dict.has('C')) { data.hasBgColor = true; } } - Util.inherit(TextAnnotation, InteractiveAnnotation, { }); + Util.inherit(TextAnnotation, Annotation, {}); return TextAnnotation; })(); var LinkAnnotation = (function LinkAnnotationClosure() { function LinkAnnotation(params) { - InteractiveAnnotation.call(this, params); + Annotation.call(this, params); var dict = params.dict; var data = this.data; data.annotationType = AnnotationType.LINK; + data.hasHtml = true; var action = dict.get('A'); - if (action) { + if (action && isDict(action)) { var linkType = action.get('S').name; if (linkType === 'URI') { var url = action.get('URI'); if (isName(url)) { // Some bad PDFs do not put parentheses around relative URLs. @@ -11452,11 +13436,19 @@ // TODO: pdf spec mentions urls can be relative to a Base // entry in the dictionary. if (!isValidUrl(url, false)) { url = ''; } - data.url = url; + // According to ISO 32000-1:2008, section 12.6.4.7, + // URI should to be encoded in 7-bit ASCII. + // Some bad PDFs may have URIs in UTF-8 encoding, see Bugzilla 1122280. + try { + data.url = stringToUTF8String(url); + } catch (e) { + // Fall back to a simple copy. + data.url = url; + } } else if (linkType === 'GoTo') { data.dest = action.get('D'); } else if (linkType === 'GoToR') { var urlDict = action.get('F'); if (isDict(urlDict)) { @@ -11490,15 +13482,11 @@ return ('http://' + url); } return url; } - Util.inherit(LinkAnnotation, InteractiveAnnotation, { - hasOperatorList: function LinkAnnotation_hasOperatorList() { - return false; - } - }); + Util.inherit(LinkAnnotation, Annotation, {}); return LinkAnnotation; })(); @@ -11845,11 +13833,14 @@ } var rmin = encode[2 * i]; var rmax = encode[2 * i + 1]; - tmpBuf[0] = rmin + (v - dmin) * (rmax - rmin) / (dmax - dmin); + // Prevent the value from becoming NaN as a result + // of division by zero (fixes issue6113.pdf). + tmpBuf[0] = dmin === dmax ? rmin : + rmin + (v - dmin) * (rmax - rmin) / (dmax - dmin); // call the appropriate function fns[i](tmpBuf, 0, dest, destOffset); }; }, @@ -11914,11 +13905,11 @@ key += value + '_'; } var cachedValue = cache[key]; if (cachedValue !== undefined) { - cachedValue.set(dest, destOffset); + dest.set(cachedValue, destOffset); return; } var output = new Float32Array(numOutputs); var stack = evaluator.execute(input); @@ -11938,11 +13929,11 @@ } if (cache_available > 0) { cache_available--; cache[key] = output; } - output.set(dest, destOffset); + dest.set(output, destOffset); }; } }; })(); @@ -12854,13 +14845,13 @@ return ['PatternCS', null]; default: error('unrecognized colorspace ' + mode); } } else if (isArray(cs)) { - mode = cs[0].name; + mode = xref.fetchIfRef(cs[0]).name; this.mode = mode; - var numComps, params; + var numComps, params, alt; switch (mode) { case 'DeviceGray': case 'G': return 'DeviceGrayCS'; @@ -12869,56 +14860,67 @@ return 'DeviceRgbCS'; case 'DeviceCMYK': case 'CMYK': return 'DeviceCmykCS'; case 'CalGray': - params = cs[1].getAll(); + params = xref.fetchIfRef(cs[1]).getAll(); return ['CalGrayCS', params]; case 'CalRGB': - params = cs[1].getAll(); + params = xref.fetchIfRef(cs[1]).getAll(); return ['CalRGBCS', params]; case 'ICCBased': var stream = xref.fetchIfRef(cs[1]); var dict = stream.dict; numComps = dict.get('N'); + alt = dict.get('Alternate'); + if (alt) { + var altIR = ColorSpace.parseToIR(alt, xref, res); + // Parse the /Alternate CS to ensure that the number of components + // are correct, and also (indirectly) that it is not a PatternCS. + var altCS = ColorSpace.fromIR(altIR); + if (altCS.numComps === numComps) { + return altIR; + } + warn('ICCBased color space: Ignoring incorrect /Alternate entry.'); + } if (numComps === 1) { return 'DeviceGrayCS'; } else if (numComps === 3) { return 'DeviceRgbCS'; } else if (numComps === 4) { return 'DeviceCmykCS'; } break; case 'Pattern': - var basePatternCS = cs[1]; + var basePatternCS = cs[1] || null; if (basePatternCS) { basePatternCS = ColorSpace.parseToIR(basePatternCS, xref, res); } return ['PatternCS', basePatternCS]; case 'Indexed': case 'I': var baseIndexedCS = ColorSpace.parseToIR(cs[1], xref, res); - var hiVal = cs[2] + 1; + var hiVal = xref.fetchIfRef(cs[2]) + 1; var lookup = xref.fetchIfRef(cs[3]); if (isStream(lookup)) { lookup = lookup.getBytes(); } return ['IndexedCS', baseIndexedCS, hiVal, lookup]; case 'Separation': case 'DeviceN': - var name = cs[1]; + var name = xref.fetchIfRef(cs[1]); numComps = 1; if (isName(name)) { numComps = 1; } else if (isArray(name)) { numComps = name.length; } - var alt = ColorSpace.parseToIR(cs[2], xref, res); + alt = ColorSpace.parseToIR(cs[2], xref, res); var tintFnIR = PDFFunction.getIR(xref, xref.fetchIfRef(cs[3])); return ['AlternateCS', numComps, alt, tintFnIR]; case 'Lab': - params = cs[1].getAll(); + params = xref.fetchIfRef(cs[1]).getAll(); return ['LabCS', params]; default: error('unimplemented color space object "' + mode + '"'); } } else { @@ -12934,11 +14936,11 @@ * slightly different. * @param {Array} decode Decode map (usually from an image). * @param {Number} n Number of components the color space has. */ ColorSpace.isDefaultDecode = function ColorSpace_isDefaultDecode(decode, n) { - if (!decode) { + if (!isArray(decode)) { return true; } if (n * 2 !== decode.length) { warn('The decode map is not the correct length'); @@ -15582,13 +17584,14 @@ if (pdfAlgorithm) { if (pdfAlgorithm.checkUserPassword(password, userValidationSalt, userPassword)) { return pdfAlgorithm.getUserKey(password, userKeySalt, userEncryption); - } else if (pdfAlgorithm.checkOwnerPassword(password, ownerValidationSalt, - uBytes, - ownerPassword)) { + } else if (password.length && pdfAlgorithm.checkOwnerPassword(password, + ownerValidationSalt, + uBytes, + ownerPassword)) { return pdfAlgorithm.getOwnerKey(password, ownerKeySalt, uBytes, ownerEncryption); } } @@ -15739,10 +17742,18 @@ this.encryptMetadata = encryptMetadata; var fileIdBytes = stringToBytes(fileId); var passwordBytes; if (password) { + if (revision === 6) { + try { + password = utf8StringToString(password); + } catch (ex) { + warn('CipherTransformFactory: ' + + 'Unable to convert UTF8 encoded password.'); + } + } passwordBytes = stringToBytes(password); } var encryptionKey; if (algorithm !== 5) { @@ -15788,11 +17799,11 @@ if (algorithm >= 4) { this.cf = dict.get('CF'); this.stmf = dict.get('StmF') || identityName; this.strf = dict.get('StrF') || identityName; - this.eff = dict.get('EFF') || this.strf; + this.eff = dict.get('EFF') || this.stmf; } } function buildObjectKey(num, gen, encryptionKey, isAes) { var key = new Uint8Array(encryptionKey.length + 9), i, n; @@ -15843,11 +17854,11 @@ error('Unknown crypto method'); } CipherTransformFactory.prototype = { createCipherTransform: - function CipherTransformFactory_createCipherTransform(num, gen) { + function CipherTransformFactory_createCipherTransform(num, gen) { if (this.algorithm === 4 || this.algorithm === 5) { return new CipherTransform( buildCipherConstructor(this.cf, this.stmf, num, gen, this.encryptionKey), buildCipherConstructor(this.cf, this.strf, @@ -15863,11 +17874,12 @@ }; return CipherTransformFactory; })(); -var PatternType = { + +var ShadingType = { FUNCTION_BASED: 1, AXIAL: 2, RADIAL: 3, FREE_FORM_MESH: 4, LATTICE_FORM_MESH: 5, @@ -15888,28 +17900,37 @@ error('Should not call Pattern.getStyle: ' + ctx); } }; Pattern.parseShading = function Pattern_parseShading(shading, matrix, xref, - res) { + res, handler) { var dict = isStream(shading) ? shading.dict : shading; var type = dict.get('ShadingType'); - switch (type) { - case PatternType.AXIAL: - case PatternType.RADIAL: - // Both radial and axial shadings are handled by RadialAxial shading. - return new Shadings.RadialAxial(dict, matrix, xref, res); - case PatternType.FREE_FORM_MESH: - case PatternType.LATTICE_FORM_MESH: - case PatternType.COONS_PATCH_MESH: - case PatternType.TENSOR_PATCH_MESH: - return new Shadings.Mesh(shading, matrix, xref, res); - default: - UnsupportedManager.notify(UNSUPPORTED_FEATURES.shadingPattern); - return new Shadings.Dummy(); + try { + switch (type) { + case ShadingType.AXIAL: + case ShadingType.RADIAL: + // Both radial and axial shadings are handled by RadialAxial shading. + return new Shadings.RadialAxial(dict, matrix, xref, res); + case ShadingType.FREE_FORM_MESH: + case ShadingType.LATTICE_FORM_MESH: + case ShadingType.COONS_PATCH_MESH: + case ShadingType.TENSOR_PATCH_MESH: + return new Shadings.Mesh(shading, matrix, xref, res); + default: + throw new Error('Unsupported ShadingType: ' + type); + } + } catch (ex) { + if (ex instanceof MissingDataException) { + throw ex; + } + handler.send('UnsupportedFeature', + {featureId: UNSUPPORTED_FEATURES.shadingPattern}); + warn(ex); + return new Shadings.Dummy(); } }; return Pattern; })(); @@ -15945,11 +17966,11 @@ var extendArr = dict.get('Extend'); extendStart = extendArr[0]; extendEnd = extendArr[1]; } - if (this.shadingType === PatternType.RADIAL && + if (this.shadingType === ShadingType.RADIAL && (!extendStart || !extendEnd)) { // Radial gradient only currently works if either circle is fully within // the other circle. var x1 = this.coordsArr[0]; var y1 = this.coordsArr[1]; @@ -15990,18 +18011,18 @@ var rgbColor; for (var i = t0; i <= t1; i += step) { ratio[0] = i; fn(ratio, 0, color, 0); rgbColor = cs.getRgb(color, 0); - var cssColor = Util.makeCssRgb(rgbColor); + var cssColor = Util.makeCssRgb(rgbColor[0], rgbColor[1], rgbColor[2]); colorStops.push([(i - t0) / diff, cssColor]); } var background = 'transparent'; if (dict.has('Background')) { rgbColor = cs.getRgb(dict.get('Background'), 0); - background = Util.makeCssRgb(rgbColor); + background = Util.makeCssRgb(rgbColor[0], rgbColor[1], rgbColor[2]); } if (!extendStart) { // Insert a color stop at the front and offset the first real color stop // so it doesn't conflict with the one we insert. @@ -16020,17 +18041,17 @@ RadialAxial.prototype = { getIR: function RadialAxial_getIR() { var coordsArr = this.coordsArr; var shadingType = this.shadingType; var type, p0, p1, r0, r1; - if (shadingType === PatternType.AXIAL) { + if (shadingType === ShadingType.AXIAL) { p0 = [coordsArr[0], coordsArr[1]]; p1 = [coordsArr[2], coordsArr[3]]; r0 = null; r1 = null; type = 'axial'; - } else if (shadingType === PatternType.RADIAL) { + } else if (shadingType === ShadingType.RADIAL) { p0 = [coordsArr[0], coordsArr[1]]; p1 = [coordsArr[3], coordsArr[4]]; r0 = coordsArr[2]; r1 = coordsArr[5]; type = 'radial'; @@ -16040,10 +18061,15 @@ var matrix = this.matrix; if (matrix) { p0 = Util.applyTransform(p0, matrix); p1 = Util.applyTransform(p1, matrix); + if (shadingType === ShadingType.RADIAL) { + var scale = Util.singularValueDecompose2dScale(matrix); + r0 *= scale[0]; + r1 *= scale[1]; + } } return ['RadialAxial', type, this.colorStops, p0, p1, r0, r1]; } }; @@ -16060,11 +18086,11 @@ this.buffer = 0; this.bufferLength = 0; var numComps = context.numComps; this.tmpCompsBuf = new Float32Array(numComps); - var csNumComps = context.colorSpace; + var csNumComps = context.colorSpace.numComps; this.tmpCsCompsBuf = context.colorFn ? new Float32Array(csNumComps) : this.tmpCompsBuf; } MeshStreamReader.prototype = { get hasData() { @@ -16180,17 +18206,14 @@ colors.push(color); verticesLeft--; reader.align(); } - - var psPacked = new Int32Array(ps); - mesh.figures.push({ type: 'triangles', - coords: psPacked, - colors: psPacked + coords: new Int32Array(ps), + colors: new Int32Array(ps), }); } function decodeType5Shading(mesh, reader, verticesPerRow) { var coords = mesh.coords; @@ -16201,17 +18224,14 @@ var color = reader.readComponents(); ps.push(coords.length); coords.push(coord); colors.push(color); } - - var psPacked = new Int32Array(ps); - mesh.figures.push({ type: 'lattice', - coords: psPacked, - colors: psPacked, + coords: new Int32Array(ps), + colors: new Int32Array(ps), verticesPerRow: verticesPerRow }); } var MIN_SPLIT_PATCH_CHUNKS_AMOUNT = 3; @@ -16349,33 +18369,36 @@ cs[2] = ci + 1; cs[3] = ci + 2; cs[0] = ci; cs[1] = ci + 3; break; case 1: tmp1 = ps[12]; tmp2 = ps[13]; tmp3 = ps[14]; tmp4 = ps[15]; - ps[12] = pi + 5; ps[13] = pi + 4; ps[14] = pi + 3; ps[15] = pi + 2; - ps[ 8] = pi + 6; /* values for 5, 6, 9, 10 are */ ps[11] = pi + 1; - ps[ 4] = pi + 7; /* calculated below */ ps[ 7] = pi; - ps[ 0] = tmp1; ps[ 1] = tmp2; ps[ 2] = tmp3; ps[ 3] = tmp4; + ps[12] = tmp4; ps[13] = pi + 0; ps[14] = pi + 1; ps[15] = pi + 2; + ps[ 8] = tmp3; /* values for 5, 6, 9, 10 are */ ps[11] = pi + 3; + ps[ 4] = tmp2; /* calculated below */ ps[ 7] = pi + 4; + ps[ 0] = tmp1; ps[ 1] = pi + 7; ps[ 2] = pi + 6; ps[ 3] = pi + 5; tmp1 = cs[2]; tmp2 = cs[3]; - cs[2] = ci + 1; cs[3] = ci; - cs[0] = tmp1; cs[1] = tmp2; + cs[2] = tmp2; cs[3] = ci; + cs[0] = tmp1; cs[1] = ci + 1; break; case 2: - ps[12] = ps[15]; ps[13] = pi + 7; ps[14] = pi + 6; ps[15] = pi + 5; - ps[ 8] = ps[11]; /* values for 5, 6, 9, 10 are */ ps[11] = pi + 4; - ps[ 4] = ps[7]; /* calculated below */ ps[ 7] = pi + 3; - ps[ 0] = ps[3]; ps[ 1] = pi; ps[ 2] = pi + 1; ps[ 3] = pi + 2; - cs[2] = cs[3]; cs[3] = ci + 1; - cs[0] = cs[1]; cs[1] = ci; + tmp1 = ps[15]; + tmp2 = ps[11]; + ps[12] = ps[3]; ps[13] = pi + 0; ps[14] = pi + 1; ps[15] = pi + 2; + ps[ 8] = ps[7]; /* values for 5, 6, 9, 10 are */ ps[11] = pi + 3; + ps[ 4] = tmp2; /* calculated below */ ps[ 7] = pi + 4; + ps[ 0] = tmp1; ps[ 1] = pi + 7; ps[ 2] = pi + 6; ps[ 3] = pi + 5; + tmp1 = cs[3]; + cs[2] = cs[1]; cs[3] = ci; + cs[0] = tmp1; cs[1] = ci + 1; break; case 3: - ps[12] = ps[0]; ps[13] = ps[1]; ps[14] = ps[2]; ps[15] = ps[3]; - ps[ 8] = pi; /* values for 5, 6, 9, 10 are */ ps[11] = pi + 7; - ps[ 4] = pi + 1; /* calculated below */ ps[ 7] = pi + 6; - ps[ 0] = pi + 2; ps[ 1] = pi + 3; ps[ 2] = pi + 4; ps[ 3] = pi + 5; - cs[2] = cs[0]; cs[3] = cs[1]; - cs[0] = ci; cs[1] = ci + 1; + ps[12] = ps[0]; ps[13] = pi + 0; ps[14] = pi + 1; ps[15] = pi + 2; + ps[ 8] = ps[1]; /* values for 5, 6, 9, 10 are */ ps[11] = pi + 3; + ps[ 4] = ps[2]; /* calculated below */ ps[ 7] = pi + 4; + ps[ 0] = ps[3]; ps[ 1] = pi + 7; ps[ 2] = pi + 6; ps[ 3] = pi + 5; + cs[2] = cs[0]; cs[3] = ci; + cs[0] = cs[1]; cs[1] = ci + 1; break; } // set p11, p12, p21, p22 ps[5] = coords.length; coords.push([ @@ -16456,33 +18479,36 @@ cs[2] = ci + 1; cs[3] = ci + 2; cs[0] = ci; cs[1] = ci + 3; break; case 1: tmp1 = ps[12]; tmp2 = ps[13]; tmp3 = ps[14]; tmp4 = ps[15]; - ps[12] = pi + 5; ps[13] = pi + 4; ps[14] = pi + 3; ps[15] = pi + 2; - ps[ 8] = pi + 6; ps[ 9] = pi + 11; ps[10] = pi + 10; ps[11] = pi + 1; - ps[ 4] = pi + 7; ps[ 5] = pi + 8; ps[ 6] = pi + 9; ps[ 7] = pi; - ps[ 0] = tmp1; ps[ 1] = tmp2; ps[ 2] = tmp3; ps[ 3] = tmp4; + ps[12] = tmp4; ps[13] = pi + 0; ps[14] = pi + 1; ps[15] = pi + 2; + ps[ 8] = tmp3; ps[ 9] = pi + 9; ps[10] = pi + 10; ps[11] = pi + 3; + ps[ 4] = tmp2; ps[ 5] = pi + 8; ps[ 6] = pi + 11; ps[ 7] = pi + 4; + ps[ 0] = tmp1; ps[ 1] = pi + 7; ps[ 2] = pi + 6; ps[ 3] = pi + 5; tmp1 = cs[2]; tmp2 = cs[3]; - cs[2] = ci + 1; cs[3] = ci; - cs[0] = tmp1; cs[1] = tmp2; + cs[2] = tmp2; cs[3] = ci; + cs[0] = tmp1; cs[1] = ci + 1; break; case 2: - ps[12] = ps[15]; ps[13] = pi + 7; ps[14] = pi + 6; ps[15] = pi + 5; - ps[ 8] = ps[11]; ps[ 9] = pi + 8; ps[10] = pi + 11; ps[11] = pi + 4; - ps[ 4] = ps[7]; ps[ 5] = pi + 9; ps[ 6] = pi + 10; ps[ 7] = pi + 3; - ps[ 0] = ps[3]; ps[ 1] = pi; ps[ 2] = pi + 1; ps[ 3] = pi + 2; - cs[2] = cs[3]; cs[3] = ci + 1; - cs[0] = cs[1]; cs[1] = ci; + tmp1 = ps[15]; + tmp2 = ps[11]; + ps[12] = ps[3]; ps[13] = pi + 0; ps[14] = pi + 1; ps[15] = pi + 2; + ps[ 8] = ps[7]; ps[ 9] = pi + 9; ps[10] = pi + 10; ps[11] = pi + 3; + ps[ 4] = tmp2; ps[ 5] = pi + 8; ps[ 6] = pi + 11; ps[ 7] = pi + 4; + ps[ 0] = tmp1; ps[ 1] = pi + 7; ps[ 2] = pi + 6; ps[ 3] = pi + 5; + tmp1 = cs[3]; + cs[2] = cs[1]; cs[3] = ci; + cs[0] = tmp1; cs[1] = ci + 1; break; case 3: - ps[12] = ps[0]; ps[13] = ps[1]; ps[14] = ps[2]; ps[15] = ps[3]; - ps[ 8] = pi; ps[ 9] = pi + 9; ps[10] = pi + 8; ps[11] = pi + 7; - ps[ 4] = pi + 1; ps[ 5] = pi + 10; ps[ 6] = pi + 11; ps[ 7] = pi + 6; - ps[ 0] = pi + 2; ps[ 1] = pi + 3; ps[ 2] = pi + 4; ps[ 3] = pi + 5; - cs[2] = cs[0]; cs[3] = cs[1]; - cs[0] = ci; cs[1] = ci + 1; + ps[12] = ps[0]; ps[13] = pi + 0; ps[14] = pi + 1; ps[15] = pi + 2; + ps[ 8] = ps[1]; ps[ 9] = pi + 9; ps[10] = pi + 10; ps[11] = pi + 3; + ps[ 4] = ps[2]; ps[ 5] = pi + 8; ps[ 6] = pi + 11; ps[ 7] = pi + 4; + ps[ 0] = ps[3]; ps[ 1] = pi + 7; ps[ 2] = pi + 6; ps[ 3] = pi + 5; + cs[2] = cs[0]; cs[3] = ci; + cs[0] = cs[1]; cs[1] = ci + 1; break; } mesh.figures.push({ type: 'patch', coords: new Int32Array(ps), // making copies of ps and cs @@ -16567,23 +18593,23 @@ }; var reader = new MeshStreamReader(stream, decodeContext); var patchMesh = false; switch (this.shadingType) { - case PatternType.FREE_FORM_MESH: + case ShadingType.FREE_FORM_MESH: decodeType4Shading(this, reader); break; - case PatternType.LATTICE_FORM_MESH: + case ShadingType.LATTICE_FORM_MESH: var verticesPerRow = dict.get('VerticesPerRow') | 0; assert(verticesPerRow >= 2, 'Invalid VerticesPerRow'); decodeType5Shading(this, reader, verticesPerRow); break; - case PatternType.COONS_PATCH_MESH: + case ShadingType.COONS_PATCH_MESH: decodeType6Shading(this, reader); patchMesh = true; break; - case PatternType.TENSOR_PATCH_MESH: + case ShadingType.TENSOR_PATCH_MESH: decodeType7Shading(this, reader); patchMesh = true; break; default: error('Unsupported mesh type.'); @@ -16737,13 +18763,14 @@ }, buildFormXObject: function PartialEvaluator_buildFormXObject(resources, xobj, smask, operatorList, + task, initialState) { - var matrix = xobj.dict.get('Matrix'); - var bbox = xobj.dict.get('BBox'); + var matrix = xobj.dict.getArray('Matrix'); + var bbox = xobj.dict.getArray('BBox'); var group = xobj.dict.get('Group'); if (group) { var groupOptions = { matrix: matrix, bbox: bbox, @@ -16769,11 +18796,11 @@ operatorList.addOp(OPS.beginGroup, [groupOptions]); } operatorList.addOp(OPS.paintFormXObjectBegin, [matrix, bbox]); - return this.getOperatorList(xobj, + return this.getOperatorList(xobj, task, (xobj.dict.get('Resources') || resources), operatorList, initialState). then(function () { operatorList.addOp(OPS.paintFormXObjectEnd, []); if (group) { @@ -16783,11 +18810,11 @@ }, buildPaintImageXObject: function PartialEvaluator_buildPaintImageXObject(resources, image, inline, operatorList, - cacheKey, cache) { + cacheKey, imageCache) { var self = this; var dict = image.dict; var w = dict.get('Width', 'W'); var h = dict.get('Height', 'H'); @@ -16821,13 +18848,14 @@ inverseDecode); imgData.cached = true; args = [imgData]; operatorList.addOp(OPS.paintImageMaskXObject, args); if (cacheKey) { - cache.key = cacheKey; - cache.fn = OPS.paintImageMaskXObject; - cache.args = args; + imageCache[cacheKey] = { + fn: OPS.paintImageMaskXObject, + args: args + }; } return; } var softMask = (dict.get('SMask', 'SM') || false); @@ -16865,44 +18893,65 @@ PDFImage.buildImage(self.handler, self.xref, resources, image, inline). then(function(imageObj) { var imgData = imageObj.createImageData(/* forceRGBA = */ false); self.handler.send('obj', [objId, self.pageIndex, 'Image', imgData], [imgData.data.buffer]); - }).then(null, function (reason) { + }).then(undefined, function (reason) { warn('Unable to decode image: ' + reason); self.handler.send('obj', [objId, self.pageIndex, 'Image', null]); }); operatorList.addOp(OPS.paintImageXObject, args); if (cacheKey) { - cache.key = cacheKey; - cache.fn = OPS.paintImageXObject; - cache.args = args; + imageCache[cacheKey] = { + fn: OPS.paintImageXObject, + args: args + }; } }, handleSMask: function PartialEvaluator_handleSmask(smask, resources, - operatorList, + operatorList, task, stateManager) { var smaskContent = smask.get('G'); var smaskOptions = { subtype: smask.get('S').name, backdrop: smask.get('BC') }; + + // The SMask might have a alpha/luminosity value transfer function -- + // we will build a map of integer values in range 0..255 to be fast. + var transferObj = smask.get('TR'); + if (isPDFFunction(transferObj)) { + var transferFn = PDFFunction.parse(this.xref, transferObj); + var transferMap = new Uint8Array(256); + var tmp = new Float32Array(1); + for (var i = 0; i < 255; i++) { + tmp[0] = i / 255; + transferFn(tmp, 0, tmp, 0); + transferMap[i] = (tmp[0] * 255) | 0; + } + smaskOptions.transferMap = transferMap; + } + return this.buildFormXObject(resources, smaskContent, smaskOptions, - operatorList, stateManager.state.clone()); + operatorList, task, stateManager.state.clone()); }, handleTilingType: function PartialEvaluator_handleTilingType(fn, args, resources, pattern, patternDict, - operatorList) { + operatorList, task) { // Create an IR of the pattern code. var tilingOpList = new OperatorList(); - return this.getOperatorList(pattern, - (patternDict.get('Resources') || resources), tilingOpList). - then(function () { + // Merge the available resources, to prevent issues when the patternDict + // is missing some /Resources entries (fixes issue6541.pdf). + var resourcesArray = [patternDict.get('Resources'), resources]; + var patternResources = Dict.merge(this.xref, resourcesArray); + + return this.getOperatorList(pattern, task, patternResources, + tilingOpList).then(function () { // Add the dependencies to the parent operator list so they are // resolved before sub operator list is executed synchronously. operatorList.addDependencies(tilingOpList.dependencies); operatorList.addOp(fn, getTilingPatternIR({ fnArray: tilingOpList.fnArray, @@ -16911,11 +18960,11 @@ }); }, handleSetFont: function PartialEvaluator_handleSetFont(resources, fontArgs, fontRef, - operatorList, state) { + operatorList, task, state) { // TODO(mack): Not needed? var fontName; if (fontArgs) { fontArgs = fontArgs.slice(); fontName = fontArgs[0].name; @@ -16925,13 +18974,19 @@ return this.loadFont(fontName, fontRef, this.xref, resources).then( function (translated) { if (!translated.font.isType3Font) { return translated; } - return translated.loadType3Data(self, resources, operatorList).then( - function () { + return translated.loadType3Data(self, resources, operatorList, task). + then(function () { return translated; + }, function (reason) { + // Error in the font data -- sending unsupported feature notification. + self.handler.send('UnsupportedFeature', + {featureId: UNSUPPORTED_FEATURES.font}); + return new TranslatedFont('g_font_error', + new ErrorFont('Type3 font load error: ' + reason), translated.font); }); }).then(function (translated) { state.font = translated.font; translated.send(self.handler); return translated.loadedName; @@ -16955,13 +19010,10 @@ } }.bind(this); for (var i = 0, ii = glyphs.length; i < ii; i++) { var glyph = glyphs[i]; - if (glyph === null) { - continue; - } buildPath(glyph.fontChar); // If the glyph has an accent we need to build a path for its // fontChar too, otherwise CanvasGraphics_paintChar will fail. var accent = glyph.accent; @@ -16973,12 +19025,12 @@ return glyphs; }, setGState: function PartialEvaluator_setGState(resources, gState, - operatorList, xref, - stateManager) { + operatorList, task, + xref, stateManager) { // This array holds the converted/processed state data. var gStateObj = []; var gStateMap = gState.map; var self = this; var promise = Promise.resolve(); @@ -16998,12 +19050,12 @@ case 'ca': gStateObj.push([key, value]); break; case 'Font': promise = promise.then(function () { - return self.handleSetFont(resources, null, value[0], - operatorList, stateManager.state). + return self.handleSetFont(resources, null, value[0], operatorList, + task, stateManager.state). then(function (loadedName) { operatorList.addDependency(loadedName); gStateObj.push([key, [loadedName, value[1]]]); }); }); @@ -17018,11 +19070,11 @@ } var dict = xref.fetchIfRef(value); if (isDict(dict)) { promise = promise.then(function () { return self.handleSMask(dict, resources, operatorList, - stateManager); + task, stateManager); }); gStateObj.push([key, true]); } else { warn('Unsupported SMask type'); } @@ -17139,11 +19191,11 @@ this.fontCache.put(fontRef, fontCapability.promise); } // Keep track of each font we translated so the caller can // load them asynchronously before calling display on a page. - font.loadedName = 'g_font_' + (fontRefIsDict ? + font.loadedName = 'g_' + this.pdfManager.docId + '_f' + (fontRefIsDict ? fontName.replace(/\W/g, '') : fontID); font.translated = fontCapability.promise; // TODO move promises into translate font @@ -17153,21 +19205,24 @@ this.translateFont(preEvaluatedFont, xref)); } catch (e) { translatedPromise = Promise.reject(e); } + var self = this; translatedPromise.then(function (translatedFont) { if (translatedFont.fontType !== undefined) { var xrefFontStats = xref.stats.fontTypes; xrefFontStats[translatedFont.fontType] = true; } fontCapability.resolve(new TranslatedFont(font.loadedName, translatedFont, font)); }, function (reason) { // TODO fontCapability.reject? - UnsupportedManager.notify(UNSUPPORTED_FEATURES.font); + // Error in the font data -- sending unsupported feature notification. + self.handler.send('UnsupportedFeature', + {featureId: UNSUPPORTED_FEATURES.font}); try { // error, but it's still nice to have font type reported var descriptor = preEvaluatedFont.descriptor; var fontFile3 = descriptor && descriptor.get('FontFile3'); @@ -17199,11 +19254,11 @@ Array.prototype.push.apply(opArgs[1], args); } }, handleColorN: function PartialEvaluator_handleColorN(operatorList, fn, args, - cs, patterns, resources, xref) { + cs, patterns, resources, task, xref) { // compile tiling patterns var patternName = args[args.length - 1]; // SCN/scn applies patterns along with normal colors var pattern; if (isName(patternName) && @@ -17212,15 +19267,16 @@ var typeNum = dict.get('PatternType'); if (typeNum === TILING_PATTERN) { var color = cs.base ? cs.base.getRgb(args, 0) : null; return this.handleTilingType(fn, color, resources, pattern, - dict, operatorList); + dict, operatorList, task); } else if (typeNum === SHADING_PATTERN) { var shading = dict.get('Shading'); var matrix = dict.get('Matrix'); - pattern = Pattern.parseShading(shading, matrix, xref, resources); + pattern = Pattern.parseShading(shading, matrix, xref, resources, + this.handler); operatorList.addOp(fn, pattern.getIR()); return Promise.resolve(); } else { return Promise.reject('Unknown PatternType: ' + typeNum); } @@ -17229,10 +19285,11 @@ operatorList.addOp(fn, args); return Promise.resolve(); }, getOperatorList: function PartialEvaluator_getOperatorList(stream, + task, resources, operatorList, initialState) { var self = this; @@ -17247,10 +19304,11 @@ var stateManager = new StateManager(initialState || new EvalState()); var preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager); var timeSlotManager = new TimeSlotManager(); return new Promise(function next(resolve, reject) { + task.ensureNotTerminated(); timeSlotManager.reset(); var stop, operation = {}, i, ii, cs; while (!(stop = timeSlotManager.check())) { // The arguments parsed by read() are used beyond this loop, so we // cannot reuse the same array on each iteration. Therefore we pass @@ -17268,12 +19326,16 @@ if (args[0].code) { break; } // eagerly compile XForm objects var name = args[0].name; - if (imageCache.key === name) { - operatorList.addOp(imageCache.fn, imageCache.args); + if (!name) { + warn('XObject must be referred to by name.'); + continue; + } + if (imageCache[name] !== undefined) { + operatorList.addOp(imageCache[name].fn, imageCache[name].args); args = null; continue; } var xobj = xobjs.get(name); @@ -17285,11 +19347,11 @@ 'XObject should have a Name subtype'); if (type.name === 'Form') { stateManager.save(); return self.buildFormXObject(resources, xobj, null, - operatorList, + operatorList, task, stateManager.state.clone()). then(function () { stateManager.restore(); next(resolve, reject); }, reject); @@ -17309,23 +19371,26 @@ } break; case OPS.setFont: var fontSize = args[1]; // eagerly collect all fonts - return self.handleSetFont(resources, args, null, - operatorList, stateManager.state). + return self.handleSetFont(resources, args, null, operatorList, + task, stateManager.state). then(function (loadedName) { operatorList.addDependency(loadedName); operatorList.addOp(OPS.setFont, [loadedName, fontSize]); next(resolve, reject); }, reject); case OPS.endInlineImage: var cacheKey = args[0].cacheKey; - if (cacheKey && imageCache.key === cacheKey) { - operatorList.addOp(imageCache.fn, imageCache.args); - args = null; - continue; + if (cacheKey) { + var cacheEntry = imageCache[cacheKey]; + if (cacheEntry !== undefined) { + operatorList.addOp(cacheEntry.fn, cacheEntry.args); + args = null; + continue; + } } self.buildPaintImageXObject(resources, args[0], true, operatorList, cacheKey, imageCache); args = null; continue; @@ -17334,15 +19399,16 @@ break; case OPS.showSpacedText: var arr = args[0]; var combinedGlyphs = []; var arrLength = arr.length; + var state = stateManager.state; for (i = 0; i < arrLength; ++i) { var arrItem = arr[i]; if (isString(arrItem)) { Array.prototype.push.apply(combinedGlyphs, - self.handleText(arrItem, stateManager.state)); + self.handleText(arrItem, state)); } else if (isNum(arrItem)) { combinedGlyphs.push(arrItem); } } args[0] = combinedGlyphs; @@ -17412,22 +19478,22 @@ break; case OPS.setFillColorN: cs = stateManager.state.fillColorSpace; if (cs.name === 'Pattern') { return self.handleColorN(operatorList, OPS.setFillColorN, - args, cs, patterns, resources, xref).then(function() { + args, cs, patterns, resources, task, xref).then(function() { next(resolve, reject); }, reject); } args = cs.getRgb(args, 0); fn = OPS.setFillRGBColor; break; case OPS.setStrokeColorN: cs = stateManager.state.strokeColorSpace; if (cs.name === 'Pattern') { return self.handleColorN(operatorList, OPS.setStrokeColorN, - args, cs, patterns, resources, xref).then(function() { + args, cs, patterns, resources, task, xref).then(function() { next(resolve, reject); }, reject); } args = cs.getRgb(args, 0); fn = OPS.setStrokeRGBColor; @@ -17443,11 +19509,11 @@ if (!shading) { error('No shading object found'); } var shadingFill = Pattern.parseShading(shading, null, xref, - resources); + resources, self.handler); var patternIR = shadingFill.getIR(); args = [patternIR]; fn = OPS.shadingFill; break; case OPS.setGState: @@ -17457,12 +19523,12 @@ if (!isDict(extGState) || !extGState.has(dictName.name)) { break; } var gState = extGState.get(dictName.name); - return self.setGState(resources, gState, operatorList, xref, - stateManager).then(function() { + return self.setGState(resources, gState, operatorList, task, + xref, stateManager).then(function() { next(resolve, reject); }, reject); case OPS.moveTo: case OPS.lineTo: case OPS.curveTo: @@ -17472,17 +19538,35 @@ self.buildPath(operatorList, fn, args); continue; case OPS.rectangle: self.buildPath(operatorList, fn, args); continue; + case OPS.markPoint: + case OPS.markPointProps: + case OPS.beginMarkedContent: + case OPS.beginMarkedContentProps: + case OPS.endMarkedContent: + case OPS.beginCompat: + case OPS.endCompat: + // Ignore operators where the corresponding handlers are known to + // be no-op in CanvasGraphics (display/canvas.js). This prevents + // serialization errors and is also a bit more efficient. + // We could also try to serialize all objects in a general way, + // e.g. as done in https://github.com/mozilla/pdf.js/pull/6266, + // but doing so is meaningless without knowing the semantics. + continue; + default: + // Note: Let's hope that the ignored operator does not have any + // non-serializable arguments, otherwise postMessage will throw + // "An object could not be cloned.". } operatorList.addOp(fn, args); } if (stop) { deferred.then(function () { next(resolve, reject); - }); + }, reject); return; } // Some PDFs don't close all restores inside object/form. // Closing those for them. for (i = 0, ii = preprocessor.savedStatesDepth; i < ii; i++) { @@ -17490,22 +19574,43 @@ } resolve(); }); }, - getTextContent: function PartialEvaluator_getTextContent(stream, resources, - stateManager) { + getTextContent: + function PartialEvaluator_getTextContent(stream, task, resources, + stateManager, + normalizeWhitespace) { stateManager = (stateManager || new StateManager(new TextState())); + var WhitespaceRegexp = /\s/g; + var textContent = { items: [], styles: Object.create(null) }; - var bidiTexts = textContent.items; - var SPACE_FACTOR = 0.35; + var textContentItem = { + initialized: false, + str: [], + width: 0, + height: 0, + vertical: false, + lastAdvanceWidth: 0, + lastAdvanceHeight: 0, + textAdvanceScale: 0, + spaceWidth: 0, + fakeSpaceMin: Infinity, + fakeMultiSpaceMin: Infinity, + fakeMultiSpaceMax: -0, + textRunBreakAllowed: false, + transform: null, + fontName: null + }; + var SPACE_FACTOR = 0.3; var MULTI_SPACE_FACTOR = 1.5; + var MULTI_SPACE_FACTOR_MAX = 4; var self = this; var xref = this.xref; resources = (xref.fetchIfRef(resources) || Dict.empty); @@ -17516,38 +19621,108 @@ var preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager); var textState; - function newTextChunk() { + function ensureTextContentItem() { + if (textContentItem.initialized) { + return textContentItem; + } var font = textState.font; if (!(font.loadedName in textContent.styles)) { textContent.styles[font.loadedName] = { fontFamily: font.fallbackName, ascent: font.ascent, descent: font.descent, vertical: font.vertical }; } - return { - // |str| is initially an array which we push individual chars to, and - // then runBidi() overwrites it with the final string. - str: [], - dir: null, - width: 0, - height: 0, - transform: null, - fontName: font.loadedName - }; + textContentItem.fontName = font.loadedName; + + // 9.4.4 Text Space Details + var tsm = [textState.fontSize * textState.textHScale, 0, + 0, textState.fontSize, + 0, textState.textRise]; + + if (font.isType3Font && + textState.fontMatrix !== FONT_IDENTITY_MATRIX && + textState.fontSize === 1) { + var glyphHeight = font.bbox[3] - font.bbox[1]; + if (glyphHeight > 0) { + glyphHeight = glyphHeight * textState.fontMatrix[3]; + tsm[3] *= glyphHeight; + } + } + + var trm = Util.transform(textState.ctm, + Util.transform(textState.textMatrix, tsm)); + textContentItem.transform = trm; + if (!font.vertical) { + textContentItem.width = 0; + textContentItem.height = Math.sqrt(trm[2] * trm[2] + trm[3] * trm[3]); + textContentItem.vertical = false; + } else { + textContentItem.width = Math.sqrt(trm[0] * trm[0] + trm[1] * trm[1]); + textContentItem.height = 0; + textContentItem.vertical = true; + } + + var a = textState.textLineMatrix[0]; + var b = textState.textLineMatrix[1]; + var scaleLineX = Math.sqrt(a * a + b * b); + a = textState.ctm[0]; + b = textState.ctm[1]; + var scaleCtmX = Math.sqrt(a * a + b * b); + textContentItem.textAdvanceScale = scaleCtmX * scaleLineX; + textContentItem.lastAdvanceWidth = 0; + textContentItem.lastAdvanceHeight = 0; + + var spaceWidth = font.spaceWidth / 1000 * textState.fontSize; + if (spaceWidth) { + textContentItem.spaceWidth = spaceWidth; + textContentItem.fakeSpaceMin = spaceWidth * SPACE_FACTOR; + textContentItem.fakeMultiSpaceMin = spaceWidth * MULTI_SPACE_FACTOR; + textContentItem.fakeMultiSpaceMax = + spaceWidth * MULTI_SPACE_FACTOR_MAX; + // It's okay for monospace fonts to fake as much space as needed. + textContentItem.textRunBreakAllowed = !font.isMonospace; + } else { + textContentItem.spaceWidth = 0; + textContentItem.fakeSpaceMin = Infinity; + textContentItem.fakeMultiSpaceMin = Infinity; + textContentItem.fakeMultiSpaceMax = 0; + textContentItem.textRunBreakAllowed = false; + } + + + textContentItem.initialized = true; + return textContentItem; } - function runBidi(textChunk) { + function replaceWhitespace(str) { + // Replaces all whitespaces with standard spaces (0x20), to avoid + // alignment issues between the textLayer and the canvas if the text + // contains e.g. tabs (fixes issue6612.pdf). + var i = 0, ii = str.length, code; + while (i < ii && (code = str.charCodeAt(i)) >= 0x20 && code <= 0x7F) { + i++; + } + return (i < ii ? str.replace(WhitespaceRegexp, ' ') : str); + } + + function runBidiTransform(textChunk) { var str = textChunk.str.join(''); - var bidiResult = PDFJS.bidi(str, -1, textState.font.vertical); - textChunk.str = bidiResult.str; - textChunk.dir = bidiResult.dir; - return textChunk; + var bidiResult = PDFJS.bidi(str, -1, textChunk.vertical); + return { + str: (normalizeWhitespace ? replaceWhitespace(bidiResult.str) : + bidiResult.str), + dir: bidiResult.dir, + width: textChunk.width, + height: textChunk.height, + transform: textChunk.transform, + fontName: textChunk.fontName + }; } function handleSetFont(fontName, fontRef) { return self.loadFont(fontName, fontRef, xref, resources). then(function (translated) { @@ -17555,36 +19730,19 @@ textState.fontMatrix = translated.font.fontMatrix || FONT_IDENTITY_MATRIX; }); } - function buildTextGeometry(chars, textChunk) { + function buildTextContentItem(chars) { var font = textState.font; - textChunk = textChunk || newTextChunk(); - if (!textChunk.transform) { - // 9.4.4 Text Space Details - var tsm = [textState.fontSize * textState.textHScale, 0, - 0, textState.fontSize, - 0, textState.textRise]; - var trm = textChunk.transform = Util.transform(textState.ctm, - Util.transform(textState.textMatrix, tsm)); - if (!font.vertical) { - textChunk.height = Math.sqrt(trm[2] * trm[2] + trm[3] * trm[3]); - } else { - textChunk.width = Math.sqrt(trm[0] * trm[0] + trm[1] * trm[1]); - } - } + var textChunk = ensureTextContentItem(); var width = 0; var height = 0; var glyphs = font.charsToGlyphs(chars); var defaultVMetrics = font.defaultVMetrics; for (var i = 0; i < glyphs.length; i++) { var glyph = glyphs[i]; - if (!glyph) { // Previous glyph was a space. - width += textState.wordSpacing * textState.textHScale; - continue; - } var vMetricX = null; var vMetricY = null; var glyphWidth = null; if (font.vertical) { if (glyph.vmetric) { @@ -17616,44 +19774,75 @@ // var trm = Util.transform(textState.textMatrix, tsm); // var pt = Util.applyTransform([trm[4], trm[5]], textState.ctm); // var x = pt[0]; // var y = pt[1]; + var charSpacing = textState.charSpacing; + if (glyph.isSpace) { + var wordSpacing = textState.wordSpacing; + charSpacing += wordSpacing; + if (wordSpacing > 0) { + addFakeSpaces(wordSpacing, textChunk.str); + } + } + var tx = 0; var ty = 0; if (!font.vertical) { var w0 = glyphWidth * textState.fontMatrix[0]; - tx = (w0 * textState.fontSize + textState.charSpacing) * + tx = (w0 * textState.fontSize + charSpacing) * textState.textHScale; width += tx; } else { var w1 = glyphWidth * textState.fontMatrix[0]; - ty = w1 * textState.fontSize + textState.charSpacing; + ty = w1 * textState.fontSize + charSpacing; height += ty; } textState.translateTextMatrix(tx, ty); textChunk.str.push(glyphUnicode); } - var a = textState.textLineMatrix[0]; - var b = textState.textLineMatrix[1]; - var scaleLineX = Math.sqrt(a * a + b * b); - a = textState.ctm[0]; - b = textState.ctm[1]; - var scaleCtmX = Math.sqrt(a * a + b * b); if (!font.vertical) { - textChunk.width += width * scaleCtmX * scaleLineX; + textChunk.lastAdvanceWidth = width; + textChunk.width += width * textChunk.textAdvanceScale; } else { - textChunk.height += Math.abs(height * scaleCtmX * scaleLineX); + textChunk.lastAdvanceHeight = height; + textChunk.height += Math.abs(height * textChunk.textAdvanceScale); } + return textChunk; } + function addFakeSpaces(width, strBuf) { + if (width < textContentItem.fakeSpaceMin) { + return; + } + if (width < textContentItem.fakeMultiSpaceMin) { + strBuf.push(' '); + return; + } + var fakeSpaces = Math.round(width / textContentItem.spaceWidth); + while (fakeSpaces-- > 0) { + strBuf.push(' '); + } + } + + function flushTextContentItem() { + if (!textContentItem.initialized) { + return; + } + textContent.items.push(runBidiTransform(textContentItem)); + + textContentItem.initialized = false; + textContentItem.str.length = 0; + } + var timeSlotManager = new TimeSlotManager(); return new Promise(function next(resolve, reject) { + task.ensureNotTerminated(); timeSlotManager.reset(); var stop, operation = {}, args = []; while (!(stop = timeSlotManager.check())) { // The arguments parsed by read() are not used beyond this loop, so // we can reuse the same array on every iteration, thus avoiding @@ -17664,39 +19853,66 @@ break; } textState = stateManager.state; var fn = operation.fn; args = operation.args; + var advance; switch (fn | 0) { case OPS.setFont: + flushTextContentItem(); textState.fontSize = args[1]; return handleSetFont(args[0].name).then(function() { next(resolve, reject); }, reject); case OPS.setTextRise: + flushTextContentItem(); textState.textRise = args[0]; break; case OPS.setHScale: + flushTextContentItem(); textState.textHScale = args[0] / 100; break; case OPS.setLeading: + flushTextContentItem(); textState.leading = args[0]; break; case OPS.moveText: + // Optimization to treat same line movement as advance + var isSameTextLine = !textState.font ? false : + ((textState.font.vertical ? args[0] : args[1]) === 0); + advance = args[0] - args[1]; + if (isSameTextLine && textContentItem.initialized && + advance > 0 && + advance <= textContentItem.fakeMultiSpaceMax) { + textState.translateTextLineMatrix(args[0], args[1]); + textContentItem.width += + (args[0] - textContentItem.lastAdvanceWidth); + textContentItem.height += + (args[1] - textContentItem.lastAdvanceHeight); + var diff = (args[0] - textContentItem.lastAdvanceWidth) - + (args[1] - textContentItem.lastAdvanceHeight); + addFakeSpaces(diff, textContentItem.str); + break; + } + + flushTextContentItem(); textState.translateTextLineMatrix(args[0], args[1]); textState.textMatrix = textState.textLineMatrix.slice(); break; case OPS.setLeadingMoveText: + flushTextContentItem(); textState.leading = -args[1]; textState.translateTextLineMatrix(args[0], args[1]); textState.textMatrix = textState.textLineMatrix.slice(); break; case OPS.nextLine: + flushTextContentItem(); textState.carriageReturn(); break; case OPS.setTextMatrix: + flushTextContentItem(); textState.setTextMatrix(args[0], args[1], args[2], args[3], args[4], args[5]); textState.setTextLineMatrix(args[0], args[1], args[2], args[3], args[4], args[5]); break; @@ -17705,62 +19921,82 @@ break; case OPS.setWordSpacing: textState.wordSpacing = args[0]; break; case OPS.beginText: + flushTextContentItem(); textState.textMatrix = IDENTITY_MATRIX.slice(); textState.textLineMatrix = IDENTITY_MATRIX.slice(); break; case OPS.showSpacedText: var items = args[0]; - var textChunk = newTextChunk(); var offset; for (var j = 0, jj = items.length; j < jj; j++) { if (typeof items[j] === 'string') { - buildTextGeometry(items[j], textChunk); + buildTextContentItem(items[j]); } else { - var val = items[j] / 1000; - if (!textState.font.vertical) { - offset = -val * textState.fontSize * textState.textHScale * - textState.textMatrix[0]; - textState.translateTextMatrix(offset, 0); - textChunk.width += offset; + ensureTextContentItem(); + + // PDF Specification 5.3.2 states: + // The number is expressed in thousandths of a unit of text + // space. + // This amount is subtracted from the current horizontal or + // vertical coordinate, depending on the writing mode. + // In the default coordinate system, a positive adjustment + // has the effect of moving the next glyph painted either to + // the left or down by the given amount. + advance = items[j] * textState.fontSize / 1000; + var breakTextRun = false; + if (textState.font.vertical) { + offset = advance * + (textState.textHScale * textState.textMatrix[2] + + textState.textMatrix[3]); + textState.translateTextMatrix(0, advance); + breakTextRun = textContentItem.textRunBreakAllowed && + advance > textContentItem.fakeMultiSpaceMax; + if (!breakTextRun) { + // Value needs to be added to height to paint down. + textContentItem.height += offset; + } } else { - offset = -val * textState.fontSize * - textState.textMatrix[3]; - textState.translateTextMatrix(0, offset); - textChunk.height += offset; - } - if (items[j] < 0 && textState.font.spaceWidth > 0) { - var fakeSpaces = -items[j] / textState.font.spaceWidth; - if (fakeSpaces > MULTI_SPACE_FACTOR) { - fakeSpaces = Math.round(fakeSpaces); - while (fakeSpaces--) { - textChunk.str.push(' '); - } - } else if (fakeSpaces > SPACE_FACTOR) { - textChunk.str.push(' '); + advance = -advance; + offset = advance * ( + textState.textHScale * textState.textMatrix[0] + + textState.textMatrix[1]); + textState.translateTextMatrix(advance, 0); + breakTextRun = textContentItem.textRunBreakAllowed && + advance > textContentItem.fakeMultiSpaceMax; + if (!breakTextRun) { + // Value needs to be subtracted from width to paint left. + textContentItem.width += offset; } } + if (breakTextRun) { + flushTextContentItem(); + } else if (advance > 0) { + addFakeSpaces(advance, textContentItem.str); + } } } - bidiTexts.push(runBidi(textChunk)); break; case OPS.showText: - bidiTexts.push(runBidi(buildTextGeometry(args[0]))); + buildTextContentItem(args[0]); break; case OPS.nextLineShowText: + flushTextContentItem(); textState.carriageReturn(); - bidiTexts.push(runBidi(buildTextGeometry(args[0]))); + buildTextContentItem(args[0]); break; case OPS.nextLineSetSpacingShowText: + flushTextContentItem(); textState.wordSpacing = args[0]; textState.charSpacing = args[1]; textState.carriageReturn(); - bidiTexts.push(runBidi(buildTextGeometry(args[2]))); + buildTextContentItem(args[2]); break; case OPS.paintXObject: + flushTextContentItem(); if (args[0].code) { break; } if (!xobjs) { @@ -17768,11 +20004,11 @@ } var name = args[0].name; if (xobjsCache.key === name) { if (xobjsCache.texts) { - Util.appendToArray(bidiTexts, xobjsCache.texts.items); + Util.appendToArray(textContent.items, xobjsCache.texts.items); Util.extendObj(textContent.styles, xobjsCache.texts.styles); } break; } @@ -17796,23 +20032,24 @@ var matrix = xobj.dict.get('Matrix'); if (isArray(matrix) && matrix.length === 6) { stateManager.transform(matrix); } - return self.getTextContent(xobj, - xobj.dict.get('Resources') || resources, stateManager). - then(function (formTextContent) { - Util.appendToArray(bidiTexts, formTextContent.items); + return self.getTextContent(xobj, task, + xobj.dict.get('Resources') || resources, stateManager, + normalizeWhitespace).then(function (formTextContent) { + Util.appendToArray(textContent.items, formTextContent.items); Util.extendObj(textContent.styles, formTextContent.styles); stateManager.restore(); xobjsCache.key = name; xobjsCache.texts = formTextContent; next(resolve, reject); }, reject); case OPS.setGState: + flushTextContentItem(); var dictName = args[0]; var extGState = resources.get('ExtGState'); if (!isDict(extGState) || !extGState.has(dictName.name)) { break; @@ -17836,13 +20073,14 @@ } // switch } // while if (stop) { deferred.then(function () { next(resolve, reject); - }); + }, reject); return; } + flushTextContentItem(); resolve(textContent); }); }, extractDataStructures: function @@ -17891,12 +20129,17 @@ var index = 0; for (var j = 0, jj = diffEncoding.length; j < jj; j++) { var data = diffEncoding[j]; if (isNum(data)) { index = data; - } else { + } else if (isName(data)) { differences[index++] = data.name; + } else if (isRef(data)) { + diffEncoding[j--] = xref.fetch(data); + continue; + } else { + error('Invalid entry in \'Differences\' array: ' + data); } } } } else if (isName(encoding)) { baseEncodingName = encoding.name; @@ -17939,19 +20182,26 @@ readToUnicode: function PartialEvaluator_readToUnicode(toUnicode) { var cmap, cmapObj = toUnicode; if (isName(cmapObj)) { cmap = CMapFactory.create(cmapObj, - { url: PDFJS.cMapUrl, packed: PDFJS.cMapPacked }, null).getMap(); - return new ToUnicodeMap(cmap); + { url: PDFJS.cMapUrl, packed: PDFJS.cMapPacked }, null); + if (cmap instanceof IdentityCMap) { + return new IdentityToUnicodeMap(0, 0xFFFF); + } + return new ToUnicodeMap(cmap.getMap()); } else if (isStream(cmapObj)) { cmap = CMapFactory.create(cmapObj, - { url: PDFJS.cMapUrl, packed: PDFJS.cMapPacked }, null).getMap(); + { url: PDFJS.cMapUrl, packed: PDFJS.cMapPacked }, null); + if (cmap instanceof IdentityCMap) { + return new IdentityToUnicodeMap(0, 0xFFFF); + } + var map = new Array(cmap.length); // Convert UTF-16BE // NOTE: cmap can be a sparse array, so use forEach instead of for(;;) // to iterate over all keys. - cmap.forEach(function(token, i) { + cmap.forEach(function(charCode, token) { var str = []; for (var k = 0; k < token.length; k += 2) { var w1 = (token.charCodeAt(k) << 8) | token.charCodeAt(k + 1); if ((w1 & 0xF800) !== 0xD800) { // w1 < 0xD800 || w1 > 0xDFFF str.push(w1); @@ -17959,13 +20209,13 @@ } k += 2; var w2 = (token.charCodeAt(k) << 8) | token.charCodeAt(k + 1); str.push(((w1 & 0x3ff) << 10) + (w2 & 0x3ff) + 0x10000); } - cmap[i] = String.fromCharCode.apply(String, str); + map[charCode] = String.fromCharCode.apply(String, str); }); - return new ToUnicodeMap(cmap); + return new ToUnicodeMap(map); } return null; }, readCidToGidMap: function PartialEvaluator_readCidToGidMap(cidToGidStream) { @@ -18171,10 +20421,25 @@ var encoding = baseDict.getRaw('Encoding'); if (isName(encoding)) { hash.update(encoding.name); } else if (isRef(encoding)) { hash.update(encoding.num + '_' + encoding.gen); + } else if (isDict(encoding)) { + var keys = encoding.getKeys(); + for (var i = 0, ii = keys.length; i < ii; i++) { + var entry = encoding.getRaw(keys[i]); + if (isName(entry)) { + hash.update(entry.name); + } else if (isRef(entry)) { + hash.update(entry.num + '_' + entry.gen); + } else if (isArray(entry)) { // 'Differences' entry. + // Ideally we should check the contents of the array, but to avoid + // parsing it here and then again in |extractDataStructures|, + // we only use the array length for now (fixes bug1157493.pdf). + hash.update(entry.length.toString()); + } + } } var toUnicode = dict.get('ToUnicode') || baseDict.get('ToUnicode'); if (isStream(toUnicode)) { var stream = toUnicode.str || toUnicode; @@ -18219,10 +20484,11 @@ if (type === 'Type3') { // FontDescriptor is only required for Type3 fonts when the document // is a tagged pdf. Create a barbebones one to get by. descriptor = new Dict(null); descriptor.set('FontName', Name.get(type)); + descriptor.set('FontBBox', dict.get('FontBBox')); } else { // Before PDF 1.5 if the font was one of the base 14 fonts, having a // FontDescriptor was not required. // This case is here for compatibility. var baseFontName = dict.get('BaseFont'); @@ -18373,11 +20639,11 @@ 'Font', fontData ]); this.sent = true; }, - loadType3Data: function (evaluator, resources, parentOperatorList) { + loadType3Data: function (evaluator, resources, parentOperatorList, task) { assert(this.font.isType3Font); if (this.type3Loaded) { return this.type3Loaded; } @@ -18390,11 +20656,11 @@ var charProcOperatorList = {}; for (var i = 0, n = charProcKeys.length; i < n; ++i) { loadCharProcsPromise = loadCharProcsPromise.then(function (key) { var glyphStream = charProcs[key]; var operatorList = new OperatorList(); - return evaluator.getOperatorList(glyphStream, fontResources, + return evaluator.getOperatorList(glyphStream, task, fontResources, operatorList).then(function () { charProcOperatorList[key] = operatorList.getIR(); // Add the dependencies to the parent operator list so they are // resolved before sub operator list is executed synchronously. @@ -18440,19 +20706,28 @@ function OperatorList(intent, messageHandler, pageIndex) { this.messageHandler = messageHandler; this.fnArray = []; this.argsArray = []; this.dependencies = {}; + this._totalLength = 0; this.pageIndex = pageIndex; this.intent = intent; } OperatorList.prototype = { get length() { return this.argsArray.length; }, + /** + * @returns {number} The total length of the entire operator list, + * since `this.length === 0` after flushing. + */ + get totalLength() { + return (this._totalLength + this.length); + }, + addOp: function(fn, args) { this.fnArray.push(fn); this.argsArray.push(args); if (this.messageHandler) { if (this.fnArray.length >= CHUNK_SIZE) { @@ -18497,16 +20772,19 @@ flush: function(lastChunk) { if (this.intent !== 'oplist') { new QueueOptimizer().optimize(this); } var transfers = getTransfers(this); + var length = this.length; + this._totalLength += length; + this.messageHandler.send('RenderPageChunk', { operatorList: { fnArray: this.fnArray, argsArray: this.argsArray, lastChunk: lastChunk, - length: this.length + length: length }, pageIndex: this.pageIndex, intent: this.intent }, transfers); this.dependencies = {}; @@ -19438,10 +21716,11 @@ // Map entries have one of two forms. // - cid chars are 16-bit unsigned integers, stored as integers. // - bf chars are variable-length byte sequences, stored as strings, with // one byte per character. this._map = []; + this.name = ''; this.vertical = false; this.useCMap = null; this.builtInCMap = builtInCMap; } CMap.prototype = { @@ -19537,17 +21816,36 @@ } } } out.charcode = 0; out.length = 1; + }, + + get length() { + return this._map.length; + }, + + get isIdentityCMap() { + if (!(this.name === 'Identity-H' || this.name === 'Identity-V')) { + return false; + } + if (this._map.length !== 0x10000) { + return false; + } + for (var i = 0; i < 0x10000; i++) { + if (this._map[i] !== i) { + return false; + } + } + return true; } }; return CMap; })(); // A special case of CMap, where the _map array implicitly has a length of -// 65535 and each element is equal to its index. +// 65536 and each element is equal to its index. var IdentityCMap = (function IdentityCMapClosure() { function IdentityCMap(vertical, n) { CMap.call(this); this.vertical = vertical; this.addCodespaceRange(n, 0, 0xffff); @@ -19598,11 +21896,19 @@ map[i] = i; } return map; }, - readCharCode: CMap.prototype.readCharCode + readCharCode: CMap.prototype.readCharCode, + + get length() { + return 0x10000; + }, + + get isIdentityCMap() { + error('should not access .isIdentityCMap'); + } }; return IdentityCMap; })(); @@ -20063,20 +22369,29 @@ if (isInt(obj)) { cMap.vertical = !!obj; } } + function parseCMapName(cMap, lexer) { + var obj = lexer.getObj(); + if (isName(obj) && isString(obj.name)) { + cMap.name = obj.name; + } + } + function parseCMap(cMap, lexer, builtInCMapParams, useCMap) { var previous; var embededUseCMap; objLoop: while (true) { var obj = lexer.getObj(); if (isEOF(obj)) { break; } else if (isName(obj)) { if (obj.name === 'WMode') { parseWMode(cMap, lexer); + } else if (obj.name === 'CMapName') { + parseCMapName(cMap, lexer); } previous = obj; } else if (isCmd(obj)) { switch (obj.cmd) { case 'endcmap': @@ -20182,10 +22497,13 @@ try { parseCMap(cMap, lexer, builtInCMapParams, useCMap); } catch (e) { warn('Invalid CMap data. ' + e); } + if (cMap.isIdentityCMap) { + return createBuiltInCMap(cMap.name, builtInCMapParams); + } return cMap; } error('Encoding required.'); } }; @@ -20488,13 +22806,16 @@ 'CourierNew-Italic': 'Courier-Oblique', 'CourierNewPS-BoldItalicMT': 'Courier-BoldOblique', 'CourierNewPS-BoldMT': 'Courier-Bold', 'CourierNewPS-ItalicMT': 'Courier-Oblique', 'CourierNewPSMT': 'Courier', + 'Helvetica': 'Helvetica', 'Helvetica-Bold': 'Helvetica-Bold', 'Helvetica-BoldItalic': 'Helvetica-BoldOblique', + 'Helvetica-BoldOblique': 'Helvetica-BoldOblique', 'Helvetica-Italic': 'Helvetica-Oblique', + 'Helvetica-Oblique':'Helvetica-Oblique', 'Symbol-Bold': 'Symbol', 'Symbol-BoldItalic': 'Symbol', 'Symbol-Italic': 'Symbol', 'TimesNewRoman': 'Times-Roman', 'TimesNewRoman-Bold': 'Times-Bold', @@ -20516,10 +22837,14 @@ /** * Holds the map of the non-standard fonts that might be included as a standard * fonts without glyph data. */ var nonStdFontMap = { + 'CenturyGothic': 'Helvetica', + 'CenturyGothic-Bold': 'Helvetica-Bold', + 'CenturyGothic-BoldItalic': 'Helvetica-BoldOblique', + 'CenturyGothic-Italic': 'Helvetica-Oblique', 'ComicSansMS': 'Comic Sans MS', 'ComicSansMS-Bold': 'Comic Sans MS-Bold', 'ComicSansMS-BoldItalic': 'Comic Sans MS-BoldItalic', 'ComicSansMS-Italic': 'Comic Sans MS-Italic', 'LucidaConsole': 'Courier', @@ -20539,11 +22864,12 @@ 'MS-PGothic-BoldItalic': 'MS PGothic-BoldItalic', 'MS-PGothic-Italic': 'MS PGothic-Italic', 'MS-PMincho': 'MS PMincho', 'MS-PMincho-Bold': 'MS PMincho-Bold', 'MS-PMincho-BoldItalic': 'MS PMincho-BoldItalic', - 'MS-PMincho-Italic': 'MS PMincho-Italic' + 'MS-PMincho-Italic': 'MS PMincho-Italic', + 'Wingdings': 'ZapfDingbats' }; var serifFonts = { 'Adobe Jenson': true, 'Adobe Text': true, 'Albertus': true, 'Aldus': true, 'Alexandria': true, 'Algerian': true, @@ -20668,10 +22994,17 @@ '3162': 589, '3165': 891, '3166': 892, '3169': 1274, '3170': 1275, '3173': 1278, '3174': 1279, '3181': 7622, '3182': 7623, '3282': 11799, '3316': 578, '3379': 42785, '3393': 1159, '3416': 8377 }; +// The glyph map for ArialBlack differs slightly from the glyph map used for +// other well-known standard fonts. Hence we use this (incomplete) CID to GID +// mapping to adjust the glyph map for non-embedded ArialBlack fonts. +var SupplementalGlyphMapForArialBlack = { + '227': 322, '264': 261, '291': 346, +}; + // Some characters, e.g. copyrightserif, are mapped to the private use area and // might not be displayed using standard fonts. Mapping/hacking well-known chars // to the similar equivalents in the normal characters range. var SpecialPUASymbols = { '63721': 0x00A9, // copyrightsans (0xF8E9) => copyright @@ -20683,11 +23016,11 @@ '63729': 0x23A7, // bracelefttp (0xF8F1) '63730': 0x23A8, // braceleftmid (0xF8F2) '63731': 0x23A9, // braceleftbt (0xF8F3) '63740': 0x23AB, // bracerighttp (0xF8FC) '63741': 0x23AC, // bracerightmid (0xF8FD) - '63742': 0x23AD, // bracerightmid (0xF8FE) + '63742': 0x23AD, // bracerightbt (0xF8FE) '63726': 0x23A1, // bracketlefttp (0xF8EE) '63727': 0x23A2, // bracketleftex (0xF8EF) '63728': 0x23A3, // bracketleftbt (0xF8F0) '63737': 0x23A4, // bracketrighttp (0xF8F9) '63738': 0x23A5, // bracketrightex (0xF8FA) @@ -22288,10 +24621,13 @@ } return s; } function adjustWidths(properties) { + if (!properties.fontMatrix) { + return; + } if (properties.fontMatrix[0] === FONT_IDENTITY_MATRIX[0]) { return; } // adjusting width to fontMatrix scale var scale = 0.001 / properties.fontMatrix[0]; @@ -22323,27 +24659,30 @@ return FontType.UNKNOWN; } } var Glyph = (function GlyphClosure() { - function Glyph(fontChar, unicode, accent, width, vmetric, operatorListId) { + function Glyph(fontChar, unicode, accent, width, vmetric, operatorListId, + isSpace) { this.fontChar = fontChar; this.unicode = unicode; this.accent = accent; this.width = width; this.vmetric = vmetric; this.operatorListId = operatorListId; + this.isSpace = isSpace; } - Glyph.prototype.matchesForCache = - function(fontChar, unicode, accent, width, vmetric, operatorListId) { + Glyph.prototype.matchesForCache = function(fontChar, unicode, accent, width, + vmetric, operatorListId, isSpace) { return this.fontChar === fontChar && this.unicode === unicode && this.accent === accent && this.width === width && this.vmetric === vmetric && - this.operatorListId === operatorListId; + this.operatorListId === operatorListId && + this.isSpace === isSpace; }; return Glyph; })(); @@ -22363,10 +24702,14 @@ for (var charCode in this._map) { callback(charCode, this._map[charCode].charCodeAt(0)); } }, + has: function(i) { + return this._map[i] !== undefined; + }, + get: function(i) { return this._map[i]; }, charCodeOf: function(v) { @@ -22383,28 +24726,32 @@ this.lastChar = lastChar; } IdentityToUnicodeMap.prototype = { get length() { - error('should not access .length'); + return (this.lastChar + 1) - this.firstChar; }, forEach: function (callback) { for (var i = this.firstChar, ii = this.lastChar; i <= ii; i++) { callback(i, i); } }, + has: function (i) { + return this.firstChar <= i && i <= this.lastChar; + }, + get: function (i) { if (this.firstChar <= i && i <= this.lastChar) { return String.fromCharCode(i); } return undefined; }, charCodeOf: function (v) { - error('should not call .charCodeOf'); + return (isInt(v) && v >= this.firstChar && v <= this.lastChar) ? v : -1; } }; return IdentityToUnicodeMap; })(); @@ -22548,10 +24895,36 @@ }; return OpenTypeFileBuilder; })(); +// Problematic Unicode characters in the fonts that needs to be moved to avoid +// issues when they are painted on the canvas, e.g. complex-script shaping or +// control/whitespace characters. The ranges are listed in pairs: the first item +// is a code of the first problematic code, the second one is the next +// non-problematic code. The ranges must be in sorted order. +var ProblematicCharRanges = new Int32Array([ + // Control characters. + 0x0000, 0x0020, + 0x007F, 0x00A1, + 0x00AD, 0x00AE, + // Chars that is used in complex-script shaping. + 0x0600, 0x0780, + 0x08A0, 0x10A0, + 0x1780, 0x1800, + // General punctuation chars. + 0x2000, 0x2010, + 0x2011, 0x2012, + 0x2028, 0x2030, + 0x205F, 0x2070, + 0x25CC, 0x25CD, + // Chars that is used in complex-script shaping. + 0xAA60, 0xAA80, + // Specials Unicode block. + 0xFFF0, 0x10000 +]); + /** * 'Font' is the class the outside world should use, it encapsulate all the font * decoding logics whatever type it is (assuming the font type is supported). * * For example to read a Type1 font and to attach it to the document: @@ -22590,10 +24963,11 @@ this.wideChars = properties.wideChars; this.cMap = properties.cMap; this.ascent = properties.ascent / PDF_GLYPH_SPACE_UNITS; this.descent = properties.descent / PDF_GLYPH_SPACE_UNITS; this.fontMatrix = properties.fontMatrix; + this.bbox = properties.bbox; this.toUnicode = properties.toUnicode = this.buildToUnicode(properties); this.toFontChar = []; @@ -22622,11 +24996,12 @@ this.missingFile = true; // The file data is not specified. Trying to fix the font name // to be used with the canvas.font. var fontName = name.replace(/[,_]/g, '-'); - var isStandardFont = fontName in stdFontMap; + var isStandardFont = !!stdFontMap[fontName] || + !!(nonStdFontMap[fontName] && stdFontMap[nonStdFontMap[fontName]]); fontName = stdFontMap[fontName] || nonStdFontMap[fontName] || fontName; this.bold = (fontName.search(/bold/gi) !== -1); this.italic = ((fontName.search(/oblique/gi) !== -1) || (fontName.search(/italic/gi) !== -1)); @@ -22640,13 +25015,18 @@ if (isStandardFont && type === 'CIDFontType2' && properties.cidEncoding.indexOf('Identity-') === 0) { // Standard fonts might be embedded as CID font without glyph mapping. // Building one based on GlyphMapForStandardFonts. var map = []; - for (var code in GlyphMapForStandardFonts) { - map[+code] = GlyphMapForStandardFonts[code]; + for (charCode in GlyphMapForStandardFonts) { + map[+charCode] = GlyphMapForStandardFonts[charCode]; } + if (/ArialBlack/i.test(name)) { + for (charCode in SupplementalGlyphMapForArialBlack) { + map[+charCode] = SupplementalGlyphMapForArialBlack[charCode]; + } + } var isIdentityUnicode = this.toUnicode instanceof IdentityToUnicodeMap; if (!isIdentityUnicode) { this.toUnicode.forEach(function(charCode, unicodeCharCode) { map[+charCode] = unicodeCharCode; }); @@ -22668,10 +25048,14 @@ continue; } this.toFontChar[charCode] = fontChar; } } else if (/Dingbats/i.test(fontName)) { + if (/Wingdings/i.test(name)) { + warn('Wingdings font without embedded font file, ' + + 'falling back to the ZapfDingbats encoding.'); + } var dingbats = Encodings.ZapfDingbatsEncoding; for (charCode in dingbats) { fontChar = DingbatsGlyphsUnicode[dingbats[charCode]]; if (!fontChar) { continue; @@ -22719,15 +25103,17 @@ } } if (subtype === 'CIDFontType0C' && type !== 'CIDFontType0') { type = 'CIDFontType0'; } - // XXX: Temporarily change the type for open type so we trigger a warning. - // This should be removed when we add support for open type. if (subtype === 'OpenType') { type = 'OpenType'; } + // Some CIDFontType0C fonts by mistake claim CIDFontType0. + if (type === 'CIDFontType0') { + subtype = isType1File(file) ? 'CIDFontType0' : 'CIDFontType0C'; + } var data; switch (type) { case 'MMType1': info('MMType1 font (' + name + '), falling back to Type1.'); @@ -22752,10 +25138,12 @@ // Repair the TrueType file. It is can be damaged in the point of // view of the sanitizer data = this.checkAndRepair(name, file, properties); if (this.isOpenType) { + adjustWidths(properties); + type = 'OpenType'; } break; default: @@ -22804,11 +25192,44 @@ function isTrueTypeFile(file) { var header = file.peekBytes(4); return readUint32(header, 0) === 0x00010000; } + function isType1File(file) { + var header = file.peekBytes(2); + // All Type1 font programs must begin with the comment '%!' (0x25 + 0x21). + if (header[0] === 0x25 && header[1] === 0x21) { + return true; + } + // ... obviously some fonts violate that part of the specification, + // please refer to the comment in |Type1Font| below. + if (header[0] === 0x80 && header[1] === 0x01) { // pfb file header. + return true; + } + return false; + } + /** + * Helper function for |adjustMapping|. + * @return {boolean} + */ + function isProblematicUnicodeLocation(code) { + // Using binary search to find a range start. + var i = 0, j = ProblematicCharRanges.length - 1; + while (i < j) { + var c = (i + j + 1) >> 1; + if (code < ProblematicCharRanges[c]) { + j = c - 1; + } else { + i = c; + } + } + // Even index means code in problematic range. + return !(i & 1); + } + + /** * Rebuilds the char code to glyph ID map by trying to replace the char codes * with their unicode value. It also moves char codes that are in known * problematic locations. * @return {Object} Two properties: * 'toFontChar' - maps original char codes(the value that will be read @@ -22819,47 +25240,35 @@ function adjustMapping(charCodeToGlyphId, properties) { var toUnicode = properties.toUnicode; var isSymbolic = !!(properties.flags & FontFlags.Symbolic); var isIdentityUnicode = properties.toUnicode instanceof IdentityToUnicodeMap; - var isCidFontType2 = (properties.type === 'CIDFontType2'); var newMap = Object.create(null); var toFontChar = []; var usedFontCharCodes = []; var nextAvailableFontCharCode = PRIVATE_USE_OFFSET_START; for (var originalCharCode in charCodeToGlyphId) { originalCharCode |= 0; var glyphId = charCodeToGlyphId[originalCharCode]; var fontCharCode = originalCharCode; // First try to map the value to a unicode position if a non identity map // was created. - if (!isIdentityUnicode) { - if (toUnicode.get(originalCharCode) !== undefined) { - var unicode = toUnicode.get(fontCharCode); - // TODO: Try to map ligatures to the correct spot. - if (unicode.length === 1) { - fontCharCode = unicode.charCodeAt(0); - } - } else if (isCidFontType2) { - // For CIDFontType2, move characters not present in toUnicode - // to the private use area (fixes bug 1028735 and issue 4881). - fontCharCode = nextAvailableFontCharCode; + if (!isIdentityUnicode && toUnicode.has(originalCharCode)) { + var unicode = toUnicode.get(fontCharCode); + // TODO: Try to map ligatures to the correct spot. + if (unicode.length === 1) { + fontCharCode = unicode.charCodeAt(0); } } // Try to move control characters, special characters and already mapped // characters to the private use area since they will not be drawn by // canvas if left in their current position. Also, move characters if the // font was symbolic and there is only an identity unicode map since the // characters probably aren't in the correct position (fixes an issue // with firefox and thuluthfont). if ((usedFontCharCodes[fontCharCode] !== undefined || - fontCharCode <= 0x1f || // Control chars - fontCharCode === 0x7F || // Control char - fontCharCode === 0xAD || // Soft hyphen - (fontCharCode >= 0x80 && fontCharCode <= 0x9F) || // Control chars - // Prevent drawing characters in the specials unicode block. - (fontCharCode >= 0xFFF0 && fontCharCode <= 0xFFFF) || + isProblematicUnicodeLocation(fontCharCode) || (isSymbolic && isIdentityUnicode)) && nextAvailableFontCharCode <= PRIVATE_USE_OFFSET_END) { // Room left. // Loop to try and find a free spot in the private use area. do { fontCharCode = nextAvailableFontCharCode++; @@ -22882,15 +25291,19 @@ charCodeToGlyphId: newMap, nextAvailableFontCharCode: nextAvailableFontCharCode }; } - function getRanges(glyphs) { + function getRanges(glyphs, numGlyphs) { // Array.sort() sorts by characters, not numerically, so convert to an // array of characters. var codes = []; for (var charCode in glyphs) { + // Remove an invalid glyph ID mappings to make OTS happy. + if (glyphs[charCode] >= numGlyphs) { + continue; + } codes.push({ fontCharCode: charCode | 0, glyphId: glyphs[charCode] }); } codes.sort(function fontGetRangesSort(a, b) { return a.fontCharCode - b.fontCharCode; }); @@ -22915,12 +25328,12 @@ } return ranges; } - function createCmapTable(glyphs) { - var ranges = getRanges(glyphs); + function createCmapTable(glyphs, numGlyphs) { + var ranges = getRanges(glyphs, numGlyphs); var numTables = ranges[ranges.length - 1][1] > 0xFFFF ? 2 : 1; var cmap = '\x00\x00' + // version string16(numTables) + // numTables '\x00\x03' + // platformID '\x00\x01' + // encodingID @@ -23307,11 +25720,20 @@ /** * Read the appropriate subtable from the cmap according to 9.6.6.4 from * PDF spec */ - function readCmapTable(cmap, font, isSymbolicFont) { + function readCmapTable(cmap, font, isSymbolicFont, hasEncoding) { + if (!cmap) { + warn('No cmap table available.'); + return { + platformId: -1, + encodingId: -1, + mappings: [], + hasShortCmap: false + }; + } var segment; var start = (font.start ? font.start : 0) + cmap.offset; font.pos = start; var version = font.getUint16(); @@ -23329,17 +25751,24 @@ var platformId = font.getUint16(); var encodingId = font.getUint16(); var offset = font.getInt32() >>> 0; var useTable = false; - if (platformId === 1 && encodingId === 0) { + if (platformId === 0 && encodingId === 0) { useTable = true; // Continue the loop since there still may be a higher priority // table. - } else if (!isSymbolicFont && platformId === 3 && encodingId === 1) { + } else if (platformId === 1 && encodingId === 0) { useTable = true; - canBreak = true; + // Continue the loop since there still may be a higher priority + // table. + } else if (platformId === 3 && encodingId === 1 && + ((!isSymbolicFont && hasEncoding) || !potentialTable)) { + useTable = true; + if (!isSymbolicFont) { + canBreak = true; + } } else if (isSymbolicFont && platformId === 3 && encodingId === 0) { useTable = true; canBreak = true; } @@ -23353,21 +25782,23 @@ if (canBreak) { break; } } - if (!potentialTable) { + if (potentialTable) { + font.pos = start + potentialTable.offset; + } + if (!potentialTable || font.peekByte() === -1) { warn('Could not find a preferred cmap table.'); return { platformId: -1, encodingId: -1, mappings: [], hasShortCmap: false }; } - font.pos = start + potentialTable.offset; var format = font.getUint16(); var length = font.getUint16(); var language = font.getUint16(); var hasShortCmap = false; @@ -23466,11 +25897,17 @@ charCode: charCode, glyphId: glyphId }); } } else { - error('cmap table has unsupported format: ' + format); + warn('cmap table has unsupported format: ' + format); + return { + platformId: -1, + encodingId: -1, + mappings: [], + hasShortCmap: false + }; } // removing duplicate entries mappings.sort(function (a, b) { return a.charCode - b.charCode; @@ -23680,10 +26117,11 @@ var oldGlyfData = glyf.data; var oldGlyfDataLength = oldGlyfData.length; var newGlyfData = new Uint8Array(oldGlyfDataLength); var startOffset = itemDecode(locaData, 0); var writeOffset = 0; + var missingGlyphData = {}; itemEncode(locaData, 0, writeOffset); var i, j; for (i = 0, j = itemSize; i < numGlyphs; i++, j += itemSize) { var endOffset = itemDecode(locaData, j); if (endOffset > oldGlyfDataLength && @@ -23697,10 +26135,14 @@ itemEncode(locaData, j, writeOffset); startOffset = endOffset; continue; } + if (startOffset === endOffset) { + missingGlyphData[i] = true; + } + var newLength = sanitizeGlyph(oldGlyfData, startOffset, endOffset, newGlyfData, writeOffset, hintsValid); writeOffset += newLength; itemEncode(locaData, j, writeOffset); startOffset = endOffset; @@ -23713,11 +26155,11 @@ [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 0]); for (i = 0, j = itemSize; i < numGlyphs; i++, j += itemSize) { itemEncode(locaData, j, simpleGlyph.length); } glyf.data = simpleGlyph; - return; + return missingGlyphData; } if (dupFirstEntry) { var firstEntryLength = itemDecode(locaData, itemSize); if (newGlyfData.length > firstEntryLength + writeOffset) { @@ -23730,10 +26172,11 @@ itemEncode(loca.data, locaData.length - itemSize, writeOffset + firstEntryLength); } else { glyf.data = newGlyfData.subarray(0, writeOffset); } + return missingGlyphData; } function readPostScriptTable(post, properties, maxpNumGlyphs) { var start = (font.start ? font.start : 0) + post.offset; font.pos = start; @@ -23792,10 +26235,13 @@ case 0x00030000: break; default: warn('Unknown/unsupported post table version ' + version); valid = false; + if (properties.defaultEncoding) { + glyphNames = properties.defaultEncoding; + } break; } properties.glyphNames = glyphNames; return valid; } @@ -24120,15 +26566,18 @@ } var isTrueType = !tables['CFF ']; if (!isTrueType) { // OpenType font - if (!tables.head || !tables.hhea || !tables.maxp || !tables.post) { + if ((header.version === 'OTTO' && properties.type !== 'CIDFontType2') || + !tables.head || !tables.hhea || !tables.maxp || !tables.post) { // no major tables: throwing everything at CFFFont cffFile = new Stream(tables['CFF '].data); cff = new CFFFont(cffFile, properties); + adjustWidths(properties); + return this.convert(name, cff, properties); } delete tables.glyf; delete tables.loca; @@ -24189,15 +26638,17 @@ error('Required "head" table is not found'); } sanitizeHead(tables.head, numGlyphs, isTrueType ? tables.loca.length : 0); + var missingGlyphs = {}; if (isTrueType) { var isGlyphLocationsLong = int16(tables.head.data[50], tables.head.data[51]); - sanitizeGlyphLocations(tables.loca, tables.glyf, numGlyphs, - isGlyphLocationsLong, hintsValid, dupFirstEntry); + missingGlyphs = sanitizeGlyphLocations(tables.loca, tables.glyf, + numGlyphs, isGlyphLocationsLong, + hintsValid, dupFirstEntry); } if (!tables.hhea) { error('Required "hhea" table is not found'); } @@ -24207,47 +26658,85 @@ if (tables.hhea.data[10] === 0 && tables.hhea.data[11] === 0) { tables.hhea.data[10] = 0xFF; tables.hhea.data[11] = 0xFF; } + // Extract some more font properties from the OpenType head and + // hhea tables; yMin and descent value are always negative. + var metricsOverride = { + unitsPerEm: int16(tables.head.data[18], tables.head.data[19]), + yMax: int16(tables.head.data[42], tables.head.data[43]), + yMin: int16(tables.head.data[38], tables.head.data[39]) - 0x10000, + ascent: int16(tables.hhea.data[4], tables.hhea.data[5]), + descent: int16(tables.hhea.data[6], tables.hhea.data[7]) - 0x10000 + }; + + // PDF FontDescriptor metrics lie -- using data from actual font. + this.ascent = metricsOverride.ascent / metricsOverride.unitsPerEm; + this.descent = metricsOverride.descent / metricsOverride.unitsPerEm; + // The 'post' table has glyphs names. if (tables.post) { var valid = readPostScriptTable(tables.post, properties, numGlyphs); if (!valid) { tables.post = null; } } var charCodeToGlyphId = [], charCode; + var toUnicode = properties.toUnicode, widths = properties.widths; + var skipToUnicode = (toUnicode instanceof IdentityToUnicodeMap || + toUnicode.length === 0x10000); + + // Helper function to try to skip mapping of empty glyphs. + // Note: In some cases, just relying on the glyph data doesn't work, + // hence we also use a few heuristics to fix various PDF files. + function hasGlyph(glyphId, charCode, widthCode) { + if (!missingGlyphs[glyphId]) { + return true; + } + if (!skipToUnicode && charCode >= 0 && toUnicode.has(charCode)) { + return true; + } + if (widths && widthCode >= 0 && isNum(widths[widthCode])) { + return true; + } + return false; + } + if (properties.type === 'CIDFontType2') { var cidToGidMap = properties.cidToGidMap || []; - var cidToGidMapLength = cidToGidMap.length; + var isCidToGidMapEmpty = cidToGidMap.length === 0; + properties.cMap.forEach(function(charCode, cid) { assert(cid <= 0xffff, 'Max size of CID is 65,535'); var glyphId = -1; - if (cidToGidMapLength === 0) { + if (isCidToGidMapEmpty) { glyphId = charCode; } else if (cidToGidMap[cid] !== undefined) { glyphId = cidToGidMap[cid]; } - if (glyphId >= 0 && glyphId < numGlyphs) { + + if (glyphId >= 0 && glyphId < numGlyphs && + hasGlyph(glyphId, charCode, cid)) { charCodeToGlyphId[charCode] = glyphId; } }); if (dupFirstEntry) { charCodeToGlyphId[0] = numGlyphs - 1; } } else { // Most of the following logic in this code branch is based on the // 9.6.6.4 of the PDF spec. - var cmapTable = readCmapTable(tables.cmap, font, this.isSymbolicFont); + var hasEncoding = + properties.differences.length > 0 || !!properties.baseEncodingName; + var cmapTable = + readCmapTable(tables.cmap, font, this.isSymbolicFont, hasEncoding); var cmapPlatformId = cmapTable.platformId; var cmapEncodingId = cmapTable.encodingId; var cmapMappings = cmapTable.mappings; var cmapMappingsLength = cmapMappings.length; - var hasEncoding = properties.differences.length || - !!properties.baseEncodingName; // The spec seems to imply that if the font is symbolic the encoding // should be ignored, this doesn't appear to work for 'preistabelle.pdf' // where the the font is symbolic and it has an encoding. if (hasEncoding && @@ -24277,35 +26766,47 @@ glyphName = Encodings.StandardEncoding[charCode]; } if (!glyphName) { continue; } - var unicodeOrCharCode; + var unicodeOrCharCode, isUnicode = false; if (cmapPlatformId === 3 && cmapEncodingId === 1) { unicodeOrCharCode = GlyphsUnicode[glyphName]; + isUnicode = true; } else if (cmapPlatformId === 1 && cmapEncodingId === 0) { // TODO: the encoding needs to be updated with mac os table. unicodeOrCharCode = Encodings.MacRomanEncoding.indexOf(glyphName); } var found = false; for (i = 0; i < cmapMappingsLength; ++i) { - if (cmapMappings[i].charCode === unicodeOrCharCode) { + if (cmapMappings[i].charCode !== unicodeOrCharCode) { + continue; + } + var code = isUnicode ? charCode : unicodeOrCharCode; + if (hasGlyph(cmapMappings[i].glyphId, code, -1)) { charCodeToGlyphId[charCode] = cmapMappings[i].glyphId; found = true; break; } } if (!found && properties.glyphNames) { - // Try to map using the post table. There are currently no known - // pdfs that this fixes. + // Try to map using the post table. var glyphId = properties.glyphNames.indexOf(glyphName); - if (glyphId > 0) { + if (glyphId > 0 && hasGlyph(glyphId, -1, -1)) { charCodeToGlyphId[charCode] = glyphId; + } else { + charCodeToGlyphId[charCode] = 0; // notdef } } } + } else if (cmapPlatformId === 0 && cmapEncodingId === 0) { + // Default Unicode semantics, use the charcodes as is. + for (i = 0; i < cmapMappingsLength; ++i) { + charCodeToGlyphId[cmapMappings[i].charCode] = + cmapMappings[i].glyphId; + } } else { // For (3, 0) cmap tables: // The charcode key being stored in charCodeToGlyphId is the lower // byte of the two-byte charcodes of the cmap table since according to // the spec: 'each byte from the string shall be prepended with the @@ -24333,28 +26834,18 @@ // Converting glyphs and ids into font's cmap table var newMapping = adjustMapping(charCodeToGlyphId, properties); this.toFontChar = newMapping.toFontChar; tables.cmap = { tag: 'cmap', - data: createCmapTable(newMapping.charCodeToGlyphId) + data: createCmapTable(newMapping.charCodeToGlyphId, numGlyphs) }; if (!tables['OS/2'] || !validateOS2Table(tables['OS/2'])) { - // extract some more font properties from the OpenType head and - // hhea tables; yMin and descent value are always negative - var override = { - unitsPerEm: int16(tables.head.data[18], tables.head.data[19]), - yMax: int16(tables.head.data[42], tables.head.data[43]), - yMin: int16(tables.head.data[38], tables.head.data[39]) - 0x10000, - ascent: int16(tables.hhea.data[4], tables.hhea.data[5]), - descent: int16(tables.hhea.data[6], tables.hhea.data[7]) - 0x10000 - }; - tables['OS/2'] = { tag: 'OS/2', data: createOS2Table(properties, newMapping.charCodeToGlyphId, - override) + metricsOverride) }; } // Rewrite the 'post' table if needed if (!tables.post) { @@ -24481,11 +26972,12 @@ builder.addTable('CFF ', font.data); // OS/2 and Windows Specific metrics builder.addTable('OS/2', createOS2Table(properties, newMapping.charCodeToGlyphId)); // Character to glyphs mapping - builder.addTable('cmap', createCmapTable(newMapping.charCodeToGlyphId)); + builder.addTable('cmap', createCmapTable(newMapping.charCodeToGlyphId, + numGlyphs)); // Font header builder.addTable('head', '\x00\x01\x00\x00' + // Version number '\x00\x00\x10\x00' + // fontRevision '\x00\x00\x00\x00' + // checksumAdjustement @@ -24703,11 +27195,11 @@ if (this.cMap.contains(glyphUnicode)) { charcode = this.cMap.lookup(glyphUnicode); } } // ... via toUnicode map - if (!charcode && 'toUnicode' in this) { + if (!charcode && this.toUnicode) { charcode = this.toUnicode.charCodeOf(glyphUnicode); } // setting it to unicode if negative or undefined if (charcode <= 0) { charcode = glyphUnicode; @@ -24723,11 +27215,11 @@ // https://github.com/mozilla/pdf.js/pull/2127#discussion_r1662280 this._shadowWidth = width; return width; }, - charToGlyph: function Font_charToGlyph(charcode) { + charToGlyph: function Font_charToGlyph(charcode, isSpace) { var fontCharCode, width, operatorListId; var widthCode = charcode; if (this.cMap && this.cMap.contains(charcode)) { widthCode = this.cMap.lookup(charcode); @@ -24766,13 +27258,13 @@ var fontChar = String.fromCharCode(fontCharCode); var glyph = this.glyphCache[charcode]; if (!glyph || !glyph.matchesForCache(fontChar, unicode, accent, width, vmetric, - operatorListId)) { + operatorListId, isSpace)) { glyph = new Glyph(fontChar, unicode, accent, width, vmetric, - operatorListId); + operatorListId, isSpace); this.glyphCache[charcode] = glyph; } return glyph; }, @@ -24804,26 +27296,20 @@ while (i < chars.length) { this.cMap.readCharCode(chars, i, c); charcode = c.charcode; var length = c.length; i += length; - glyph = this.charToGlyph(charcode); + // Space is char with code 0x20 and length 1 in multiple-byte codes. + var isSpace = length === 1 && chars.charCodeAt(i - 1) === 0x20; + glyph = this.charToGlyph(charcode, isSpace); glyphs.push(glyph); - // placing null after each word break charcode (ASCII SPACE) - // Ignore occurences of 0x20 in multiple-byte codes. - if (length === 1 && chars.charCodeAt(i - 1) === 0x20) { - glyphs.push(null); - } } } else { for (i = 0, ii = chars.length; i < ii; ++i) { charcode = chars.charCodeAt(i); - glyph = this.charToGlyph(charcode); + glyph = this.charToGlyph(charcode, charcode === 0x20); glyphs.push(glyph); - if (charcode === 0x20) { - glyphs.push(null); - } } } // Enter the translated string into the cache return (charsCache[charsCacheKey] = glyphs); @@ -24871,10 +27357,12 @@ baseEncoding = Encodings[properties.baseEncodingName]; for (charCode = 0; charCode < baseEncoding.length; charCode++) { glyphId = glyphNames.indexOf(baseEncoding[charCode]); if (glyphId >= 0) { charCodeToGlyphId[charCode] = glyphId; + } else { + charCodeToGlyphId[charCode] = 0; // notdef } } } else if (!!(properties.flags & FontFlags.Symbolic)) { // For a symbolic font the encoding should be the fonts built-in // encoding. @@ -24887,10 +27375,12 @@ baseEncoding = Encodings.StandardEncoding; for (charCode = 0; charCode < baseEncoding.length; charCode++) { glyphId = glyphNames.indexOf(baseEncoding[charCode]); if (glyphId >= 0) { charCodeToGlyphId[charCode] = glyphId; + } else { + charCodeToGlyphId[charCode] = 0; // notdef } } } // Lastly, merge in the differences. @@ -24899,10 +27389,12 @@ for (charCode in differences) { var glyphName = differences[charCode]; glyphId = glyphNames.indexOf(glyphName); if (glyphId >= 0) { charCodeToGlyphId[charCode] = glyphId; + } else { + charCodeToGlyphId[charCode] = 0; // notdef } } } return charCodeToGlyphId; } @@ -27482,20 +29974,19 @@ ranges[l].ids[code - ranges[l].start] : code)) & 0xFFFF; } return 0; } - function compileGlyf(code, js, font) { + function compileGlyf(code, cmds, font) { function moveTo(x, y) { - js.push('c.moveTo(' + x + ',' + y + ');'); + cmds.push({cmd: 'moveTo', args: [x, y]}); } function lineTo(x, y) { - js.push('c.lineTo(' + x + ',' + y + ');'); + cmds.push({cmd: 'lineTo', args: [x, y]}); } function quadraticCurveTo(xa, ya, x, y) { - js.push('c.quadraticCurveTo(' + xa + ',' + ya + ',' + - x + ',' + y + ');'); + cmds.push({cmd: 'quadraticCurveTo', args: [xa, ya, x, y]}); } var i = 0; var numberOfContours = ((code[i] << 24) | (code[i + 1] << 16)) >> 16; var flags; @@ -27537,15 +30028,15 @@ 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();'); + cmds.push({cmd: 'save'}); + cmds.push({cmd: 'transform', + args: [scaleX, scale01, scale10, scaleY, x, y]}); + compileGlyf(subglyph, cmds, font); + cmds.push({cmd: 'restore'}); } } while ((flags & 0x20)); } else { // simple glyph var endPtsOfContours = []; @@ -27637,24 +30128,23 @@ startPoint = endPoint + 1; } } } - function compileCharString(code, js, font) { + function compileCharString(code, cmds, font) { var stack = []; var x = 0, y = 0; var stems = 0; function moveTo(x, y) { - js.push('c.moveTo(' + x + ',' + y + ');'); + cmds.push({cmd: 'moveTo', args: [x, y]}); } function lineTo(x, y) { - js.push('c.lineTo(' + x + ',' + y + ');'); + cmds.push({cmd: 'lineTo', args: [x, y]}); } function bezierCurveTo(x1, y1, x2, y2, x, y) { - js.push('c.bezierCurveTo(' + x1 + ',' + y1 + ',' + x2 + ',' + y2 + ',' + - x + ',' + y + ');'); + cmds.push({cmd: 'bezierCurveTo', args: [x1, y1, x2, y2, x, y]}); } function parse(code) { var i = 0; while (i < code.length) { @@ -27779,20 +30269,20 @@ 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 + ');'); + cmds.push({cmd: 'save'}); + cmds.push({cmd: 'translate', args: [x, y]}); var gid = lookupCmap(font.cmap, String.fromCharCode( font.glyphNameMap[Encodings.StandardEncoding[achar]])); - compileCharString(font.glyphs[gid], js, font); - js.push('c.restore();'); + compileCharString(font.glyphs[gid], cmds, font); + cmds.push({cmd: 'restore'}); gid = lookupCmap(font.cmap, String.fromCharCode( font.glyphNameMap[Encodings.StandardEncoding[bchar]])); - compileCharString(font.glyphs[gid], js, font); + compileCharString(font.glyphs[gid], cmds, font); } return; case 18: // hstemhm stems += stack.length >> 1; stackClean = true; @@ -27957,20 +30447,20 @@ 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 cmds = []; + cmds.push({cmd: 'save'}); + cmds.push({cmd: 'transform', args: this.fontMatrix.slice()}); + cmds.push({cmd: 'scale', args: ['size', '-size']}); - this.compileGlyphImpl(code, js); + this.compileGlyphImpl(code, cmds); - js.push('c.restore();'); + cmds.push({cmd: 'restore'}); - return js.join('\n'); + return cmds; }, compileGlyphImpl: function () { error('Children classes should implement this.'); }, @@ -27990,12 +30480,12 @@ this.compiledGlyphs = []; } Util.inherit(TrueTypeCompiled, CompiledFont, { - compileGlyphImpl: function (code, js) { - compileGlyf(code, js, this); + compileGlyphImpl: function (code, cmds) { + compileGlyf(code, cmds, this); } }); function Type2Compiled(cffInfo, cmap, fontMatrix, glyphNameMap) { fontMatrix = fontMatrix || [0.001, 0, 0, 0.001, 0, 0]; @@ -28012,12 +30502,12 @@ this.subrsBias = (this.subrs.length < 1240 ? 107 : (this.subrs.length < 33900 ? 1131 : 32768)); } Util.inherit(Type2Compiled, CompiledFont, { - compileGlyphImpl: function (code, js) { - compileCharString(code, js, this); + compileGlyphImpl: function (code, cmds) { + compileCharString(code, cmds, this); } }); return { @@ -28060,11 +30550,10 @@ } }; })(); - var GlyphsUnicode = { A: 0x0041, AE: 0x00C6, AEacute: 0x01FC, AEmacron: 0x01E2, @@ -32596,11 +35085,16 @@ if (smask) { this.smask = new PDFImage(xref, res, smask, false); } else if (mask) { if (isStream(mask)) { - this.mask = new PDFImage(xref, res, mask, false, null, null, true); + var maskDict = mask.dict, imageMask = maskDict.get('ImageMask', 'IM'); + if (!imageMask) { + warn('Ignoring /Mask in image without /ImageMask.'); + } else { + this.mask = new PDFImage(xref, res, mask, false, null, null, true); + } } else { // Color key mask (just an array). this.mask = mask; } } @@ -33018,11 +35512,14 @@ buffer[i] ^= 0xff; } } return imgData; } - if (this.image instanceof JpegStream && !this.smask && !this.mask) { + if (this.image instanceof JpegStream && !this.smask && !this.mask && + (this.colorSpace.name === 'DeviceGray' || + this.colorSpace.name === 'DeviceRGB' || + this.colorSpace.name === 'DeviceCMYK')) { imgData.kind = ImageKind.RGB_24BPP; imgData.data = this.getImageBytes(originalHeight * rowBytes, drawWidth, drawHeight, true); return imgData; } @@ -36067,27 +38564,24 @@ 'a191': 918 } }; - var EOF = {}; function isEOF(v) { return (v === EOF); } +var MAX_LENGTH_TO_CACHE = 1000; + var Parser = (function ParserClosure() { function Parser(lexer, allowStreams, xref) { this.lexer = lexer; this.allowStreams = allowStreams; this.xref = xref; - this.imageCache = { - length: 0, - adler32: 0, - stream: null - }; + this.imageCache = {}; this.refill(); } Parser.prototype = { refill: function Parser_refill() { @@ -36101,10 +38595,23 @@ } else { this.buf1 = this.buf2; this.buf2 = this.lexer.getObj(); } }, + tryShift: function Parser_tryShift() { + try { + this.shift(); + return true; + } catch (e) { + if (e instanceof MissingDataException) { + throw e; + } + // Upon failure, the caller should reset this.lexer.pos to a known good + // state and call this.shift() twice to reset the buffers. + return false; + } + }, getObj: function Parser_getObj(cipherTransform) { var buf1 = this.buf1; this.shift(); if (buf1 instanceof Cmd) { @@ -36174,140 +38681,310 @@ } // simple object return buf1; }, - makeInlineImage: function Parser_makeInlineImage(cipherTransform) { - var lexer = this.lexer; - var stream = lexer.stream; - - // parse dictionary - var dict = new Dict(null); - while (!isCmd(this.buf1, 'ID') && !isEOF(this.buf1)) { - if (!isName(this.buf1)) { - error('Dictionary key must be a name object'); - } - - var key = this.buf1.name; - this.shift(); - if (isEOF(this.buf1)) { - break; - } - dict.set(key, this.getObj(cipherTransform)); - } - - // parse image stream - var startPos = stream.pos; - - // searching for the /EI\s/ - var state = 0, ch, i, ii; - var E = 0x45, I = 0x49, SPACE = 0x20, NL = 0xA, CR = 0xD; + /** + * Find the end of the stream by searching for the /EI\s/. + * @returns {number} The inline stream length. + */ + findDefaultInlineStreamEnd: + function Parser_findDefaultInlineStreamEnd(stream) { + var E = 0x45, I = 0x49, SPACE = 0x20, LF = 0xA, CR = 0xD; + var startPos = stream.pos, state = 0, ch, i, n, followingBytes; while ((ch = stream.getByte()) !== -1) { if (state === 0) { state = (ch === E) ? 1 : 0; } else if (state === 1) { state = (ch === I) ? 2 : 0; } else { assert(state === 2); - if (ch === SPACE || ch === NL || ch === CR) { + if (ch === SPACE || ch === LF || ch === CR) { // Let's check the next five bytes are ASCII... just be sure. - var n = 5; - var followingBytes = stream.peekBytes(n); + n = 5; + followingBytes = stream.peekBytes(n); for (i = 0; i < n; i++) { ch = followingBytes[i]; - if (ch !== NL && ch !== CR && (ch < SPACE || ch > 0x7F)) { + if (ch !== LF && ch !== CR && (ch < SPACE || ch > 0x7F)) { // Not a LF, CR, SPACE or any visible ASCII character, i.e. // it's binary stuff. Resetting the state. state = 0; break; } } if (state === 2) { - break; // finished! + break; // Finished! } } else { state = 0; } } } + return ((stream.pos - 4) - startPos); + }, + /** + * Find the EOI (end-of-image) marker 0xFFD9 of the stream. + * @returns {number} The inline stream length. + */ + findDCTDecodeInlineStreamEnd: + function Parser_findDCTDecodeInlineStreamEnd(stream) { + var startPos = stream.pos, foundEOI = false, b, markerLength, length; + while ((b = stream.getByte()) !== -1) { + if (b !== 0xFF) { // Not a valid marker. + continue; + } + switch (stream.getByte()) { + case 0x00: // Byte stuffing. + // 0xFF00 appears to be a very common byte sequence in JPEG images. + break; - var length = (stream.pos - 4) - startPos; + case 0xFF: // Fill byte. + // Avoid skipping a valid marker, resetting the stream position. + stream.skip(-1); + break; + + case 0xD9: // EOI + foundEOI = true; + break; + + case 0xC0: // SOF0 + case 0xC1: // SOF1 + case 0xC2: // SOF2 + case 0xC3: // SOF3 + + case 0xC5: // SOF5 + case 0xC6: // SOF6 + case 0xC7: // SOF7 + + case 0xC9: // SOF9 + case 0xCA: // SOF10 + case 0xCB: // SOF11 + + case 0xCD: // SOF13 + case 0xCE: // SOF14 + case 0xCF: // SOF15 + + case 0xC4: // DHT + case 0xCC: // DAC + + case 0xDA: // SOS + case 0xDB: // DQT + case 0xDC: // DNL + case 0xDD: // DRI + case 0xDE: // DHP + case 0xDF: // EXP + + case 0xE0: // APP0 + case 0xE1: // APP1 + case 0xE2: // APP2 + case 0xE3: // APP3 + case 0xE4: // APP4 + case 0xE5: // APP5 + case 0xE6: // APP6 + case 0xE7: // APP7 + case 0xE8: // APP8 + case 0xE9: // APP9 + case 0xEA: // APP10 + case 0xEB: // APP11 + case 0xEC: // APP12 + case 0xED: // APP13 + case 0xEE: // APP14 + case 0xEF: // APP15 + + case 0xFE: // COM + // The marker should be followed by the length of the segment. + markerLength = stream.getUint16(); + if (markerLength > 2) { + // |markerLength| contains the byte length of the marker segment, + // including its own length (2 bytes) and excluding the marker. + stream.skip(markerLength - 2); // Jump to the next marker. + } else { + // The marker length is invalid, resetting the stream position. + stream.skip(-2); + } + break; + } + if (foundEOI) { + break; + } + } + length = stream.pos - startPos; + if (b === -1) { + warn('Inline DCTDecode image stream: ' + + 'EOI marker not found, searching for /EI/ instead.'); + stream.skip(-length); // Reset the stream position. + return this.findDefaultInlineStreamEnd(stream); + } + this.inlineStreamSkipEI(stream); + return length; + }, + /** + * Find the EOD (end-of-data) marker '~>' (i.e. TILDE + GT) of the stream. + * @returns {number} The inline stream length. + */ + findASCII85DecodeInlineStreamEnd: + function Parser_findASCII85DecodeInlineStreamEnd(stream) { + var TILDE = 0x7E, GT = 0x3E; + var startPos = stream.pos, ch, length; + while ((ch = stream.getByte()) !== -1) { + if (ch === TILDE && stream.peekByte() === GT) { + stream.skip(); + break; + } + } + length = stream.pos - startPos; + if (ch === -1) { + warn('Inline ASCII85Decode image stream: ' + + 'EOD marker not found, searching for /EI/ instead.'); + stream.skip(-length); // Reset the stream position. + return this.findDefaultInlineStreamEnd(stream); + } + this.inlineStreamSkipEI(stream); + return length; + }, + /** + * Find the EOD (end-of-data) marker '>' (i.e. GT) of the stream. + * @returns {number} The inline stream length. + */ + findASCIIHexDecodeInlineStreamEnd: + function Parser_findASCIIHexDecodeInlineStreamEnd(stream) { + var GT = 0x3E; + var startPos = stream.pos, ch, length; + while ((ch = stream.getByte()) !== -1) { + if (ch === GT) { + break; + } + } + length = stream.pos - startPos; + if (ch === -1) { + warn('Inline ASCIIHexDecode image stream: ' + + 'EOD marker not found, searching for /EI/ instead.'); + stream.skip(-length); // Reset the stream position. + return this.findDefaultInlineStreamEnd(stream); + } + this.inlineStreamSkipEI(stream); + return length; + }, + /** + * Skip over the /EI/ for streams where we search for an EOD marker. + */ + inlineStreamSkipEI: function Parser_inlineStreamSkipEI(stream) { + var E = 0x45, I = 0x49; + var state = 0, ch; + while ((ch = stream.getByte()) !== -1) { + if (state === 0) { + state = (ch === E) ? 1 : 0; + } else if (state === 1) { + state = (ch === I) ? 2 : 0; + } else if (state === 2) { + break; + } + } + }, + makeInlineImage: function Parser_makeInlineImage(cipherTransform) { + var lexer = this.lexer; + var stream = lexer.stream; + + // Parse dictionary. + var dict = new Dict(this.xref); + while (!isCmd(this.buf1, 'ID') && !isEOF(this.buf1)) { + if (!isName(this.buf1)) { + error('Dictionary key must be a name object'); + } + var key = this.buf1.name; + this.shift(); + if (isEOF(this.buf1)) { + break; + } + dict.set(key, this.getObj(cipherTransform)); + } + + // Extract the name of the first (i.e. the current) image filter. + var filter = dict.get('Filter', 'F'), filterName; + if (isName(filter)) { + filterName = filter.name; + } else if (isArray(filter) && isName(filter[0])) { + filterName = filter[0].name; + } + + // Parse image stream. + var startPos = stream.pos, length, i, ii; + if (filterName === 'DCTDecode' || filterName === 'DCT') { + length = this.findDCTDecodeInlineStreamEnd(stream); + } else if (filterName === 'ASCII85Decide' || filterName === 'A85') { + length = this.findASCII85DecodeInlineStreamEnd(stream); + } else if (filterName === 'ASCIIHexDecode' || filterName === 'AHx') { + length = this.findASCIIHexDecodeInlineStreamEnd(stream); + } else { + length = this.findDefaultInlineStreamEnd(stream); + } var imageStream = stream.makeSubStream(startPos, length, dict); - // trying to cache repeat images, first we are trying to "warm up" caching - // using length, then comparing adler32 - var MAX_LENGTH_TO_CACHE = 1000; - var cacheImage = false, adler32; - if (length < MAX_LENGTH_TO_CACHE && this.imageCache.length === length) { + // Cache all images below the MAX_LENGTH_TO_CACHE threshold by their + // adler32 checksum. + var adler32; + if (length < MAX_LENGTH_TO_CACHE) { var imageBytes = imageStream.getBytes(); imageStream.reset(); var a = 1; var b = 0; for (i = 0, ii = imageBytes.length; i < ii; ++i) { - a = (a + (imageBytes[i] & 0xff)) % 65521; - b = (b + a) % 65521; + // No modulo required in the loop if imageBytes.length < 5552. + a += imageBytes[i] & 0xff; + b += a; } - adler32 = (b << 16) | a; + adler32 = ((b % 65521) << 16) | (a % 65521); - if (this.imageCache.stream && this.imageCache.adler32 === adler32) { + if (this.imageCache.adler32 === adler32) { this.buf2 = Cmd.get('EI'); this.shift(); - this.imageCache.stream.reset(); - return this.imageCache.stream; + this.imageCache[adler32].reset(); + return this.imageCache[adler32]; } - cacheImage = true; } - if (!cacheImage && !this.imageCache.stream) { - this.imageCache.length = length; - this.imageCache.stream = null; - } if (cipherTransform) { imageStream = cipherTransform.createStream(imageStream, length); } imageStream = this.filter(imageStream, dict, length); imageStream.dict = dict; - if (cacheImage) { + if (adler32 !== undefined) { imageStream.cacheKey = 'inline_' + length + '_' + adler32; - this.imageCache.adler32 = adler32; - this.imageCache.stream = imageStream; + this.imageCache[adler32] = imageStream; } this.buf2 = Cmd.get('EI'); this.shift(); return imageStream; }, - fetchIfRef: function Parser_fetchIfRef(obj) { - // not relying on the xref.fetchIfRef -- xref might not be set - return (isRef(obj) ? this.xref.fetch(obj) : obj); - }, makeStream: function Parser_makeStream(dict, cipherTransform) { var lexer = this.lexer; var stream = lexer.stream; // get stream start position lexer.skipToNextLine(); var pos = stream.pos - 1; // get length - var length = this.fetchIfRef(dict.get('Length')); + var length = dict.get('Length'); if (!isInt(length)) { info('Bad ' + length + ' attribute in stream'); length = 0; } // skip over the stream data stream.pos = pos + length; lexer.nextChar(); - this.shift(); // '>>' - this.shift(); // 'stream' - if (!isCmd(this.buf1, 'endstream')) { + // Shift '>>' and check whether the new object marks the end of the stream + if (this.tryShift() && isCmd(this.buf2, 'endstream')) { + this.shift(); // 'stream' + } else { // bad stream length, scanning for endstream stream.pos = pos; var SCAN_BLOCK_SIZE = 2048; var ENDSTREAM_SIGNATURE_LENGTH = 9; var ENDSTREAM_SIGNATURE = [0x65, 0x6E, 0x64, 0x73, 0x74, 0x72, 0x65, @@ -36360,12 +39037,12 @@ stream = this.filter(stream, dict, length); stream.dict = dict; return stream; }, filter: function Parser_filter(stream, dict, length) { - var filter = this.fetchIfRef(dict.get('Filter', 'F')); - var params = this.fetchIfRef(dict.get('DecodeParms', 'DP')); + var filter = dict.get('Filter', 'F'); + var params = dict.get('DecodeParms', 'DP'); if (isName(filter)) { return this.makeFilter(stream, filter.name, length, params); } var maybeLength = length; @@ -36388,16 +39065,17 @@ } } return stream; }, makeFilter: function Parser_makeFilter(stream, name, maybeLength, params) { - if (stream.dict.get('Length') === 0) { + if (stream.dict.get('Length') === 0 && !maybeLength) { + warn('Empty "' + name + '" stream.'); return new NullStream(stream); } try { - if (params) { - params = this.fetchIfRef(params); + if (params && this.xref) { + params = this.xref.fetchIfRef(params); } var xrefStreamStats = this.xref.stats.streamTypes; if (name === 'FlateDecode' || name === 'Fl') { xrefStreamStats[StreamType.FLATE] = true; if (params) { @@ -36418,26 +39096,10 @@ maybeLength, params); } return new LZWStream(stream, maybeLength, earlyChange); } if (name === 'DCTDecode' || name === 'DCT') { - // According to the specification: for inline images, the ID operator - // shall be followed by a single whitespace character (unless it uses - // ASCII85Decode or ASCIIHexDecode filters). - // In practice this only seems to be followed for inline JPEG images, - // and generally ignoring the first byte of the stream if it is a - // whitespace char can even *cause* issues (e.g. in the CCITTFaxDecode - // filters used in issue2984.pdf). - // Hence when the first byte of the stream of an inline JPEG image is - // a whitespace character, we thus simply skip over it. - if (isCmd(this.buf1, 'ID')) { - var firstByte = stream.peekByte(); - if (firstByte === 0x0A /* LF */ || firstByte === 0x0D /* CR */ || - firstByte === 0x20 /* SPACE */) { - stream.skip(); - } - } xrefStreamStats[StreamType.DCT] = true; return new JpegStream(stream, maybeLength, stream.dict, this.xref); } if (name === 'JPXDecode' || name === 'JPX') { xrefStreamStats[StreamType.JPX] = true; @@ -36551,10 +39213,15 @@ var sign = 1; if (ch === 0x2D) { // '-' sign = -1; ch = this.nextChar(); + + if (ch === 0x2D) { // '-' + // Ignore double negative (this is consistent with Adobe Reader). + ch = this.nextChar(); + } } else if (ch === 0x2B) { // '+' ch = this.nextChar(); } if (ch === 0x2E) { // '.' divideBy = 10; @@ -36709,33 +39376,47 @@ } } return strBuf.join(''); }, getName: function Lexer_getName() { - var ch; + var ch, previousCh; var strBuf = this.strBuf; strBuf.length = 0; while ((ch = this.nextChar()) >= 0 && !specialChars[ch]) { if (ch === 0x23) { // '#' ch = this.nextChar(); + if (specialChars[ch]) { + warn('Lexer_getName: ' + + 'NUMBER SIGN (#) should be followed by a hexadecimal number.'); + strBuf.push('#'); + break; + } var x = toHexDigit(ch); if (x !== -1) { - var x2 = toHexDigit(this.nextChar()); + previousCh = ch; + ch = this.nextChar(); + var x2 = toHexDigit(ch); if (x2 === -1) { - error('Illegal digit in hex char in name: ' + x2); + warn('Lexer_getName: Illegal digit (' + + String.fromCharCode(ch) +') in hexadecimal number.'); + strBuf.push('#', String.fromCharCode(previousCh)); + if (specialChars[ch]) { + break; + } + strBuf.push(String.fromCharCode(ch)); + continue; } strBuf.push(String.fromCharCode((x << 4) | x2)); } else { strBuf.push('#', String.fromCharCode(ch)); } } else { strBuf.push(String.fromCharCode(ch)); } } - if (strBuf.length > 128) { - error('Warning: name token is longer than allowed by the spec: ' + - strBuf.length); + if (strBuf.length > 127) { + warn('name token is longer than allowed by the spec: ' + strBuf.length); } return Name.get(strBuf.join('')); }, getHexString: function Lexer_getHexString() { var strBuf = this.strBuf; @@ -37171,10 +39852,13 @@ return this.bytes[this.pos++]; }, getUint16: function Stream_getUint16() { var b0 = this.getByte(); var b1 = this.getByte(); + if (b0 === -1 || b1 === -1) { + return -1; + } return (b0 << 8) + b1; }, getInt32: function Stream_getInt32() { var b0 = this.getByte(); var b1 = this.getByte(); @@ -37298,10 +39982,13 @@ return this.buffer[this.pos++]; }, getUint16: function DecodeStream_getUint16() { var b0 = this.getByte(); var b1 = this.getByte(); + if (b0 === -1 || b1 === -1) { + return -1; + } return (b0 << 8) + b1; }, getInt32: function DecodeStream_getInt32() { var b0 = this.getByte(); var b1 = this.getByte(); @@ -37410,29 +40097,29 @@ return StreamsSequenceStream; })(); var FlateStream = (function FlateStreamClosure() { - var codeLenCodeMap = new Uint32Array([ + var codeLenCodeMap = new Int32Array([ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ]); - var lengthDecode = new Uint32Array([ + var lengthDecode = new Int32Array([ 0x00003, 0x00004, 0x00005, 0x00006, 0x00007, 0x00008, 0x00009, 0x0000a, 0x1000b, 0x1000d, 0x1000f, 0x10011, 0x20013, 0x20017, 0x2001b, 0x2001f, 0x30023, 0x3002b, 0x30033, 0x3003b, 0x40043, 0x40053, 0x40063, 0x40073, 0x50083, 0x500a3, 0x500c3, 0x500e3, 0x00102, 0x00102, 0x00102 ]); - var distDecode = new Uint32Array([ + var distDecode = new Int32Array([ 0x00001, 0x00002, 0x00003, 0x00004, 0x10005, 0x10007, 0x20009, 0x2000d, 0x30011, 0x30019, 0x40021, 0x40031, 0x50041, 0x50061, 0x60081, 0x600c1, 0x70101, 0x70181, 0x80201, 0x80301, 0x90401, 0x90601, 0xa0801, 0xa0c01, 0xb1001, 0xb1801, 0xc2001, 0xc3001, 0xd4001, 0xd6001 ]); - var fixedLitCodeTab = [new Uint32Array([ + var fixedLitCodeTab = [new Int32Array([ 0x70100, 0x80050, 0x80010, 0x80118, 0x70110, 0x80070, 0x80030, 0x900c0, 0x70108, 0x80060, 0x80020, 0x900a0, 0x80000, 0x80080, 0x80040, 0x900e0, 0x70104, 0x80058, 0x80018, 0x90090, 0x70114, 0x80078, 0x80038, 0x900d0, 0x7010c, 0x80068, 0x80028, 0x900b0, 0x80008, 0x80088, 0x80048, 0x900f0, 0x70102, 0x80054, 0x80014, 0x8011c, 0x70112, 0x80074, 0x80034, 0x900c8, @@ -37495,11 +40182,11 @@ 0x7010b, 0x80067, 0x80027, 0x900af, 0x80007, 0x80087, 0x80047, 0x900ef, 0x70107, 0x8005f, 0x8001f, 0x9009f, 0x70117, 0x8007f, 0x8003f, 0x900df, 0x7010f, 0x8006f, 0x8002f, 0x900bf, 0x8000f, 0x8008f, 0x8004f, 0x900ff ]), 9]; - var fixedDistCodeTab = [new Uint32Array([ + var fixedDistCodeTab = [new Int32Array([ 0x50000, 0x50010, 0x50008, 0x50018, 0x50004, 0x50014, 0x5000c, 0x5001c, 0x50002, 0x50012, 0x5000a, 0x5001a, 0x50006, 0x50016, 0x5000e, 0x00000, 0x50001, 0x50011, 0x50009, 0x50019, 0x50005, 0x50015, 0x5000d, 0x5001d, 0x50003, 0x50013, 0x5000b, 0x5001b, 0x50007, 0x50017, 0x5000f, 0x00000 ]), 5]; @@ -37592,11 +40279,11 @@ } } // build the table var size = 1 << maxLen; - var codes = new Uint32Array(size); + var codes = new Int32Array(size); for (var len = 1, code = 0, skip = 2; len <= maxLen; ++len, code <<= 1, skip <<= 1) { for (var val = 0; val < n; ++val) { if (lengths[val] === len) { @@ -37981,12 +40668,19 @@ * a library to decode these images and the stream behaves like all the other * DecodeStreams. */ var JpegStream = (function JpegStreamClosure() { function JpegStream(stream, maybeLength, dict, xref) { - // TODO: per poppler, some images may have 'junk' before that - // need to be removed + // Some images may contain 'junk' before the SOI (start-of-image) marker. + // Note: this seems to mainly affect inline images. + var ch; + while ((ch = stream.getByte()) !== -1) { + if (ch === 0xFF) { // Find the first byte of the SOI marker (0xFFD8). + stream.skip(-1); // Reset the stream position to the SOI. + break; + } + } this.stream = stream; this.maybeLength = maybeLength; this.dict = dict; DecodeStream.call(this, maybeLength); @@ -38053,20 +40747,21 @@ * further processing such as color space conversions. */ JpegStream.prototype.isNativelySupported = function JpegStream_isNativelySupported(xref, res) { var cs = ColorSpace.parse(this.dict.get('ColorSpace', 'CS'), xref, res); - return cs.name === 'DeviceGray' || cs.name === 'DeviceRGB'; + return (cs.name === 'DeviceGray' || cs.name === 'DeviceRGB') && + cs.isDefaultDecode(this.dict.get('Decode', 'D')); }; /** * Checks if the image can be decoded by the browser. */ JpegStream.prototype.isNativelyDecodable = function JpegStream_isNativelyDecodable(xref, res) { var cs = ColorSpace.parse(this.dict.get('ColorSpace', 'CS'), xref, res); - var numComps = cs.numComps; - return numComps === 1 || numComps === 3; + return (cs.numComps === 1 || cs.numComps === 3) && + cs.isDefaultDecode(this.dict.get('Decode', 'D')); }; return JpegStream; })(); @@ -39155,13 +41850,17 @@ } } var gotEOL = false; + if (this.byteAlign) { + this.inputBits &= ~7; + } + if (!this.eoblock && this.row === this.rows - 1) { this.eof = true; - } else if (this.eoline || !this.byteAlign) { + } else { code1 = this.lookBits(12); if (this.eoline) { while (code1 !== EOF && code1 !== 1) { this.eatBits(1); code1 = this.lookBits(12); @@ -39178,14 +41877,10 @@ } else if (code1 === EOF) { this.eof = true; } } - if (this.byteAlign && !gotEOL) { - this.inputBits &= ~7; - } - if (!this.eof && this.encoding > 0) { this.nextLine2D = !this.lookBits(1); this.eatBits(1); } @@ -39578,14 +42273,102 @@ return NullStream; })(); +var WorkerTask = (function WorkerTaskClosure() { + function WorkerTask(name) { + this.name = name; + this.terminated = false; + this._capability = createPromiseCapability(); + } + + WorkerTask.prototype = { + get finished() { + return this._capability.promise; + }, + + finish: function () { + this._capability.resolve(); + }, + + terminate: function () { + this.terminated = true; + }, + + ensureNotTerminated: function () { + if (this.terminated) { + throw new Error('Worker task was terminated'); + } + } + }; + + return WorkerTask; +})(); + var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { - setup: function wphSetup(handler) { + setup: function wphSetup(handler, port) { + handler.on('test', function wphSetupTest(data) { + // check if Uint8Array can be sent to worker + if (!(data instanceof Uint8Array)) { + handler.send('test', 'main', false); + return; + } + // making sure postMessage transfers are working + var supportTransfers = data[0] === 255; + handler.postMessageTransfers = supportTransfers; + // check if the response property is supported by xhr + var xhr = new XMLHttpRequest(); + var responseExists = 'response' in xhr; + // check if the property is actually implemented + try { + var dummy = xhr.responseType; + } catch (e) { + responseExists = false; + } + if (!responseExists) { + handler.send('test', false); + return; + } + handler.send('test', { + supportTypedArray: true, + supportTransfers: supportTransfers + }); + }); + + handler.on('GetDocRequest', function wphSetupDoc(data) { + return WorkerMessageHandler.createDocumentHandler(data, port); + }); + }, + createDocumentHandler: function wphCreateDocumentHandler(docParams, port) { + // This context is actually holds references on pdfManager and handler, + // until the latter is destroyed. var pdfManager; + var terminated = false; + var cancelXHRs = null; + var WorkerTasks = []; + var docId = docParams.docId; + var workerHandlerName = docParams.docId + '_worker'; + var handler = new MessageHandler(workerHandlerName, docId, port); + + function ensureNotTerminated() { + if (terminated) { + throw new Error('Worker was terminated'); + } + } + + function startWorkerTask(task) { + WorkerTasks.push(task); + } + + function finishWorkerTask(task) { + task.finish(); + var i = WorkerTasks.indexOf(task); + WorkerTasks.splice(i, 1); + } + function loadDocument(recoveryMode) { var loadDocumentCapability = createPromiseCapability(); var parseSuccess = function parseSuccess() { var numPagesPromise = pdfManager.ensureDoc('numPages'); @@ -39617,25 +42400,26 @@ return loadDocumentCapability.promise; } function getPdfManager(data) { var pdfManagerCapability = createPromiseCapability(); + var pdfManager; var source = data.source; var disableRange = data.disableRange; if (source.data) { try { - pdfManager = new LocalPdfManager(source.data, source.password); - pdfManagerCapability.resolve(); + pdfManager = new LocalPdfManager(docId, source.data, source.password); + pdfManagerCapability.resolve(pdfManager); } catch (ex) { pdfManagerCapability.reject(ex); } return pdfManagerCapability.promise; } else if (source.chunkedViewerLoading) { try { - pdfManager = new NetworkPdfManager(source, handler); - pdfManagerCapability.resolve(); + pdfManager = new NetworkPdfManager(docId, source, handler); + pdfManagerCapability.resolve(pdfManager); } catch (ex) { pdfManagerCapability.reject(ex); } return pdfManagerCapability.promise; } @@ -39666,11 +42450,11 @@ length = parseInt(length, 10); if (!isInt(length)) { return; } source.length = length; - if (length <= 2 * RANGE_CHUNK_SIZE) { + if (length <= 2 * source.rangeChunkSize) { // The file size is smaller than the size of two chunks, so it does // not make any sense to abort the request and retry with a range // request. return; } @@ -39687,15 +42471,16 @@ // requests. networkManager.abortRequest(fullRequestXhrId); } try { - pdfManager = new NetworkPdfManager(source, handler); + pdfManager = new NetworkPdfManager(docId, source, handler); pdfManagerCapability.resolve(pdfManager); } catch (ex) { pdfManagerCapability.reject(ex); } + cancelXHRs = null; }, onProgressiveData: source.disableStream ? null : function onProgressiveData(chunk) { if (!pdfManager) { @@ -39731,73 +42516,51 @@ pdfFile = args.chunk; } // the data is array, instantiating directly from it try { - pdfManager = new LocalPdfManager(pdfFile, source.password); - pdfManagerCapability.resolve(); + pdfManager = new LocalPdfManager(docId, pdfFile, source.password); + pdfManagerCapability.resolve(pdfManager); } catch (ex) { pdfManagerCapability.reject(ex); } + cancelXHRs = null; }, onError: function onError(status) { var exception; - if (status === 404) { + if (status === 404 || status === 0 && /^file:/.test(source.url)) { exception = new MissingPDFException('Missing PDF "' + source.url + '".'); handler.send('MissingPDF', exception); } else { exception = new UnexpectedResponseException( 'Unexpected server response (' + status + ') while retrieving PDF "' + source.url + '".', status); handler.send('UnexpectedResponse', exception); } + cancelXHRs = null; }, onProgress: function onProgress(evt) { handler.send('DocProgress', { loaded: evt.loaded, total: evt.lengthComputable ? evt.total : source.length }); } }); + cancelXHRs = function () { + networkManager.abortRequest(fullRequestXhrId); + }; + return pdfManagerCapability.promise; } - handler.on('test', function wphSetupTest(data) { - // check if Uint8Array can be sent to worker - if (!(data instanceof Uint8Array)) { - handler.send('test', false); - return; - } - // making sure postMessage transfers are working - var supportTransfers = data[0] === 255; - handler.postMessageTransfers = supportTransfers; - // check if the response property is supported by xhr - var xhr = new XMLHttpRequest(); - var responseExists = 'response' in xhr; - // check if the property is actually implemented - try { - var dummy = xhr.responseType; - } catch (e) { - responseExists = false; - } - if (!responseExists) { - handler.send('test', false); - return; - } - handler.send('test', { - supportTypedArray: true, - supportTransfers: supportTransfers - }); - }); - - handler.on('GetDocRequest', function wphSetupDoc(data) { - + var setupDoc = function(data) { var onSuccess = function(doc) { + ensureNotTerminated(); handler.send('GetDoc', { pdfInfo: doc }); }; var onFailure = function(e) { if (e instanceof PasswordException) { @@ -39816,26 +42579,40 @@ handler.send('UnknownError', new UnknownErrorException(e.message, e.toString())); } }; + ensureNotTerminated(); + PDFJS.maxImageSize = data.maxImageSize === undefined ? -1 : data.maxImageSize; PDFJS.disableFontFace = data.disableFontFace; PDFJS.disableCreateObjectURL = data.disableCreateObjectURL; PDFJS.verbosity = data.verbosity; PDFJS.cMapUrl = data.cMapUrl === undefined ? null : data.cMapUrl; PDFJS.cMapPacked = data.cMapPacked === true; - getPdfManager(data).then(function () { + getPdfManager(data).then(function (newPdfManager) { + if (terminated) { + // We were in a process of setting up the manager, but it got + // terminated in the middle. + newPdfManager.terminate(); + throw new Error('Worker was terminated'); + } + + pdfManager = newPdfManager; handler.send('PDFManagerReady', null); pdfManager.onLoadedStream().then(function(stream) { handler.send('DataLoaded', { length: stream.bytes.byteLength }); }); }).then(function pdfManagerReady() { + ensureNotTerminated(); + loadDocument(false).then(onSuccess, function loadFailure(ex) { + ensureNotTerminated(); + // Try again with recoveryMode == true if (!(ex instanceof XRefParseException)) { if (ex instanceof PasswordException) { // after password exception prepare to receive a new password // to repeat loading @@ -39846,15 +42623,17 @@ return; } pdfManager.requestLoadedStream(); pdfManager.onLoadedStream().then(function() { + ensureNotTerminated(); + loadDocument(true).then(onSuccess, onFailure); }); }, onFailure); }, onFailure); - }); + }; handler.on('GetPage', function wphSetupGetPage(data) { return pdfManager.getPage(data.pageIndex).then(function(page) { var rotatePromise = pdfManager.ensure(page, 'rotate'); var refPromise = pdfManager.ensure(page, 'ref'); @@ -39883,11 +42662,11 @@ } ); handler.on('GetDestination', function wphSetupGetDestination(data) { - return pdfManager.ensureCatalog('getDestination', [ data.id ]); + return pdfManager.ensureCatalog('getDestination', [data.id]); } ); handler.on('GetAttachments', function wphSetupGetAttachments(data) { @@ -39931,27 +42710,40 @@ pdfManager.updatePassword(data); }); handler.on('GetAnnotations', function wphSetupGetAnnotations(data) { return pdfManager.getPage(data.pageIndex).then(function(page) { - return pdfManager.ensure(page, 'getAnnotationsData', []); + return pdfManager.ensure(page, 'getAnnotationsData', [data.intent]); }); }); handler.on('RenderPageRequest', function wphSetupRenderPage(data) { - pdfManager.getPage(data.pageIndex).then(function(page) { + var pageIndex = data.pageIndex; + pdfManager.getPage(pageIndex).then(function(page) { + var task = new WorkerTask('RenderPageRequest: page ' + pageIndex); + startWorkerTask(task); - var pageNum = data.pageIndex + 1; + var pageNum = pageIndex + 1; var start = Date.now(); // Pre compile the pdf page and fetch the fonts/images. - page.getOperatorList(handler, data.intent).then(function(operatorList) { + page.getOperatorList(handler, task, data.intent).then( + function(operatorList) { + finishWorkerTask(task); info('page=' + pageNum + ' - getOperatorList: time=' + - (Date.now() - start) + 'ms, len=' + operatorList.fnArray.length); - + (Date.now() - start) + 'ms, len=' + operatorList.totalLength); }, function(e) { + finishWorkerTask(task); + if (task.terminated) { + return; // ignoring errors from the terminated thread + } + // For compatibility with older behavior, generating unknown + // unsupported feature notification on errors. + handler.send('UnsupportedFeature', + {featureId: UNSUPPORTED_FEATURES.unknown}); + var minimumStackMessage = 'worker.js: while trying to getPage() and getOperatorList()'; var wrappedException; @@ -39981,45 +42773,85 @@ }); }); }, this); handler.on('GetTextContent', function wphExtractText(data) { - return pdfManager.getPage(data.pageIndex).then(function(page) { - var pageNum = data.pageIndex + 1; + var pageIndex = data.pageIndex; + var normalizeWhitespace = data.normalizeWhitespace; + return pdfManager.getPage(pageIndex).then(function(page) { + var task = new WorkerTask('GetTextContent: page ' + pageIndex); + startWorkerTask(task); + var pageNum = pageIndex + 1; var start = Date.now(); - return page.extractTextContent().then(function(textContent) { + return page.extractTextContent(task, normalizeWhitespace).then( + function(textContent) { + finishWorkerTask(task); info('text indexing: page=' + pageNum + ' - time=' + (Date.now() - start) + 'ms'); return textContent; + }, function (reason) { + finishWorkerTask(task); + if (task.terminated) { + return; // ignoring errors from the terminated thread + } + throw reason; }); }); }); handler.on('Cleanup', function wphCleanup(data) { return pdfManager.cleanup(); }); handler.on('Terminate', function wphTerminate(data) { - pdfManager.terminate(); + terminated = true; + if (pdfManager) { + pdfManager.terminate(); + pdfManager = null; + } + if (cancelXHRs) { + cancelXHRs(); + } + + var waitOn = []; + WorkerTasks.forEach(function (task) { + waitOn.push(task.finished); + task.terminate(); + }); + + return Promise.all(waitOn).then(function () { + // Notice that even if we destroying handler, resolved response promise + // must be sent back. + handler.destroy(); + handler = null; + }); }); + + handler.on('Ready', function wphReady(data) { + setupDoc(docParams); + docParams = null; // we don't need docParams anymore -- saving memory. + }); + return workerHandlerName; } }; var consoleTimer = {}; var workerConsole = { log: function log() { var args = Array.prototype.slice.call(arguments); globalScope.postMessage({ + targetName: 'main', action: 'console_log', data: args }); }, error: function error() { var args = Array.prototype.slice.call(arguments); globalScope.postMessage({ + targetName: 'main', action: 'console_error', data: args }); throw 'pdf.js execution error'; }, @@ -40042,28 +42874,20 @@ if (typeof window === 'undefined') { if (!('console' in globalScope)) { globalScope.console = workerConsole; } - // Listen for unsupported features so we can pass them on to the main thread. - PDFJS.UnsupportedManager.listen(function (msg) { - globalScope.postMessage({ - action: '_unsupported_feature', - data: msg - }); - }); - - var handler = new MessageHandler('worker_processor', this); - WorkerMessageHandler.setup(handler); + var handler = new MessageHandler('worker', 'main', this); + WorkerMessageHandler.setup(handler, this); } /* This class implements the QM Coder decoding as defined in * JPEG 2000 Part I Final Committee Draft Version 1.0 - * Annex C.3 Arithmetic decoding procedure + * Annex C.3 Arithmetic decoding procedure * available at http://www.jpeg.org/public/fcd15444-1.pdf - * + * * The arithmetic decoder is used in conjunction with context models to decode * JPEG2000 and JBIG2 streams. */ var ArithmeticDecoder = (function ArithmeticDecoderClosure() { // Table C-2 @@ -40223,10 +43047,11 @@ return ArithmeticDecoder; })(); + var JpegImage = (function jpegImage() { var dctZigZag = new Uint8Array([ 0, 1, 8, 16, 9, 2, @@ -40322,31 +43147,25 @@ return bitsData >>> 7; } function decodeHuffman(tree) { var node = tree; - var bit; - while ((bit = readBit()) !== null) { - node = node[bit]; + while (true) { + node = node[readBit()]; if (typeof node === 'number') { return node; } if (typeof node !== 'object') { throw 'invalid huffman sequence'; } } - return null; } function receive(length) { var n = 0; while (length > 0) { - var bit = readBit(); - if (bit === null) { - return; - } - n = (n << 1) | bit; + n = (n << 1) | readBit(); length--; } return n; } @@ -40701,11 +43520,11 @@ v5 = p3; v6 = p5; // stage 3 // Shift v0 by 128.5 << 5 here, so we don't need to shift p0...p7 when - // converting to UInt8 range later. + // converting to UInt8 range later. v0 = ((v0 + v1 + 1) >> 1) + 4112; v1 = v0 - v1; t = (v2 * dctSin6 + v3 * dctCos6 + 2048) >> 12; v2 = (v2 * dctCos6 - v3 * dctSin6 + 2048) >> 12; v3 = t; @@ -40867,13 +43686,13 @@ } // TODO APP1 - Exif if (fileMarker === 0xFFEE) { if (appData[0] === 0x41 && appData[1] === 0x64 && appData[2] === 0x6F && appData[3] === 0x62 && - appData[4] === 0x65 && appData[5] === 0) { // 'Adobe\x00' + appData[4] === 0x65) { // 'Adobe' adobe = { - version: appData[6], + version: (appData[5] << 8) | appData[6], flags0: (appData[7] << 8) | appData[8], flags1: (appData[9] << 8) | appData[10], transformCode: appData[11] }; } @@ -40990,10 +43809,17 @@ frame, components, resetInterval, spectralStart, spectralEnd, successiveApproximation >> 4, successiveApproximation & 15); offset += processed; break; + + case 0xFFFF: // Fill bytes + if (data[offset] !== 0xFF) { // Avoid skipping a valid marker. + offset--; + } + break; + default: if (data[offset - 3] === 0xFF && data[offset - 2] >= 0xC0 && data[offset - 2] <= 0xFE) { // could be incorrect encoding -- last 0xFF byte of the previous // block was eaten by the encoder @@ -41058,12 +43884,11 @@ offset += numComponents; } } } - // decodeTransform will contains pairs of multiplier (-256..256) and - // additive + // decodeTransform contains pairs of multiplier (-256..256) and additive var transform = this.decodeTransform; if (transform) { for (i = 0; i < dataLength;) { for (j = 0, k = 0; j < numComponents; j++, i++, k += 2) { data[i] = ((data[i] * transform[k]) >> 8) + transform[k + 1]; @@ -41096,55 +43921,47 @@ } return data; }, _convertYcckToRgb: function convertYcckToRgb(data) { - var Y, Cb, Cr, k, CbCb, CbCr, CbY, Cbk, CrCr, Crk, CrY, YY, Yk, kk; + var Y, Cb, Cr, k; var offset = 0; for (var i = 0, length = data.length; i < length; i += 4) { Y = data[i]; Cb = data[i + 1]; Cr = data[i + 2]; k = data[i + 3]; - CbCb = Cb * Cb; - CbCr = Cb * Cr; - CbY = Cb * Y; - Cbk = Cb * k; - CrCr = Cr * Cr; - Crk = Cr * k; - CrY = Cr * Y; - YY = Y * Y; - Yk = Y * k; - kk = k * k; + var r = -122.67195406894 + + Cb * (-6.60635669420364e-5 * Cb + 0.000437130475926232 * Cr - + 5.4080610064599e-5 * Y + 0.00048449797120281 * k - + 0.154362151871126) + + Cr * (-0.000957964378445773 * Cr + 0.000817076911346625 * Y - + 0.00477271405408747 * k + 1.53380253221734) + + Y * (0.000961250184130688 * Y - 0.00266257332283933 * k + + 0.48357088451265) + + k * (-0.000336197177618394 * k + 0.484791561490776); - var r = - 122.67195406894 - - 6.60635669420364e-5 * CbCb + 0.000437130475926232 * CbCr - - 5.4080610064599e-5* CbY + 0.00048449797120281* Cbk - - 0.154362151871126 * Cb - 0.000957964378445773 * CrCr + - 0.000817076911346625 * CrY - 0.00477271405408747 * Crk + - 1.53380253221734 * Cr + 0.000961250184130688 * YY - - 0.00266257332283933 * Yk + 0.48357088451265 * Y - - 0.000336197177618394 * kk + 0.484791561490776 * k; - var g = 107.268039397724 + - 2.19927104525741e-5 * CbCb - 0.000640992018297945 * CbCr + - 0.000659397001245577* CbY + 0.000426105652938837* Cbk - - 0.176491792462875 * Cb - 0.000778269941513683 * CrCr + - 0.00130872261408275 * CrY + 0.000770482631801132 * Crk - - 0.151051492775562 * Cr + 0.00126935368114843 * YY - - 0.00265090189010898 * Yk + 0.25802910206845 * Y - - 0.000318913117588328 * kk - 0.213742400323665 * k; + Cb * (2.19927104525741e-5 * Cb - 0.000640992018297945 * Cr + + 0.000659397001245577 * Y + 0.000426105652938837 * k - + 0.176491792462875) + + Cr * (-0.000778269941513683 * Cr + 0.00130872261408275 * Y + + 0.000770482631801132 * k - 0.151051492775562) + + Y * (0.00126935368114843 * Y - 0.00265090189010898 * k + + 0.25802910206845) + + k * (-0.000318913117588328 * k - 0.213742400323665); - var b = - 20.810012546947 - - 0.000570115196973677 * CbCb - 2.63409051004589e-5 * CbCr + - 0.0020741088115012* CbY - 0.00288260236853442* Cbk + - 0.814272968359295 * Cb - 1.53496057440975e-5 * CrCr - - 0.000132689043961446 * CrY + 0.000560833691242812 * Crk - - 0.195152027534049 * Cr + 0.00174418132927582 * YY - - 0.00255243321439347 * Yk + 0.116935020465145 * Y - - 0.000343531996510555 * kk + 0.24165260232407 * k; + var b = -20.810012546947 + + Cb * (-0.000570115196973677 * Cb - 2.63409051004589e-5 * Cr + + 0.0020741088115012 * Y - 0.00288260236853442 * k + + 0.814272968359295) + + Cr * (-1.53496057440975e-5 * Cr - 0.000132689043961446 * Y + + 0.000560833691242812 * k - 0.195152027534049) + + Y * (0.00174418132927582 * Y - 0.00255243321439347 * k + + 0.116935020465145) + + k * (-0.000343531996510555 * k + 0.24165260232407); data[offset++] = clamp0to255(r); data[offset++] = clamp0to255(g); data[offset++] = clamp0to255(b); } @@ -41281,22 +44098,56 @@ throw new Error('JPX Error: Invalid box field size'); } var dataLength = lbox - headerSize; var jumpDataLength = true; switch (tbox) { - case 0x6A501A1A: // 'jP\032\032' - // TODO - break; case 0x6A703268: // 'jp2h' jumpDataLength = false; // parsing child boxes break; case 0x636F6C72: // 'colr' - // TODO + // Colorspaces are not used, the CS from the PDF is used. + var method = data[position]; + var precedence = data[position + 1]; + var approximation = data[position + 2]; + if (method === 1) { + // enumerated colorspace + var colorspace = readUint32(data, position + 3); + switch (colorspace) { + case 16: // this indicates a sRGB colorspace + case 17: // this indicates a grayscale colorspace + case 18: // this indicates a YUV colorspace + break; + default: + warn('Unknown colorspace ' + colorspace); + break; + } + } else if (method === 2) { + info('ICC profile not supported'); + } break; case 0x6A703263: // 'jp2c' this.parseCodestream(data, position, position + dataLength); break; + case 0x6A502020: // 'jP\024\024' + if (0x0d0a870a !== readUint32(data, position)) { + warn('Invalid JP2 signature'); + } + break; + // The following header types are valid but currently not used: + case 0x6A501A1A: // 'jP\032\032' + case 0x66747970: // 'ftyp' + case 0x72726571: // 'rreq' + case 0x72657320: // 'res ' + case 0x69686472: // 'ihdr' + break; + default: + var headerType = String.fromCharCode((tbox >> 24) & 0xFF, + (tbox >> 16) & 0xFF, + (tbox >> 8) & 0xFF, + tbox & 0xFF); + warn('Unsupported header type ' + tbox + ' (' + headerType + ')'); + break; } if (jumpDataLength) { position += dataLength; } } @@ -41371,15 +44222,10 @@ context.components = components; calculateTileGrids(context, components); context.QCC = []; context.COC = []; break; - case 0xFF55: // Tile-part lengths, main header (TLM) - var Ltlm = readUint16(data, position); // Marker segment length - // Skip tile length markers - position += Ltlm; - break; case 0xFF5C: // Quantization default (QCD) length = readUint16(data, position); var qcd = {}; j = position + 2; sqcd = data[j++]; @@ -41508,16 +44354,10 @@ }); } cod.precinctsSizes = precinctsSizes; } var unsupported = []; - if (cod.sopMarkerUsed) { - unsupported.push('sopMarkerUsed'); - } - if (cod.ephMarkerUsed) { - unsupported.push('ephMarkerUsed'); - } if (cod.selectiveArithmeticCodingBypass) { unsupported.push('selectiveArithmeticCodingBypass'); } if (cod.resetContextProbabilities) { unsupported.push('resetContextProbabilities'); @@ -41571,10 +44411,13 @@ // moving to the end of the data length = tile.dataEnd - position; parseTilePackets(context, data, position, length); break; + case 0xFF55: // Tile-part lengths, main header (TLM) + case 0xFF57: // Packet length, main header (PLM) + case 0xFF58: // Packet length, tile-part header (PLT) case 0xFF64: // Comment (COM) length = readUint16(data, position); // skipping content break; case 0xFF53: // Coding style component (COC) @@ -41664,29 +44507,43 @@ } function buildPrecincts(context, resolution, dimensions) { // Section B.6 Division resolution to precincts var precinctWidth = 1 << dimensions.PPx; var precinctHeight = 1 << dimensions.PPy; + // Jasper introduces codeblock groups for mapping each subband codeblocks + // to precincts. Precinct partition divides a resolution according to width + // and height parameters. The subband that belongs to the resolution level + // has a different size than the level, unless it is the zero resolution. + + // From Jasper documentation: jpeg2000.pdf, section K: Tier-2 coding: + // The precinct partitioning for a particular subband is derived from a + // partitioning of its parent LL band (i.e., the LL band at the next higher + // resolution level)... The LL band associated with each resolution level is + // divided into precincts... Each of the resulting precinct regions is then + // mapped into its child subbands (if any) at the next lower resolution + // level. This is accomplished by using the coordinate transformation + // (u, v) = (ceil(x/2), ceil(y/2)) where (x, y) and (u, v) are the + // coordinates of a point in the LL band and child subband, respectively. + var isZeroRes = resolution.resLevel === 0; + var precinctWidthInSubband = 1 << (dimensions.PPx + (isZeroRes ? 0 : -1)); + var precinctHeightInSubband = 1 << (dimensions.PPy + (isZeroRes ? 0 : -1)); var numprecinctswide = (resolution.trx1 > resolution.trx0 ? Math.ceil(resolution.trx1 / precinctWidth) - Math.floor(resolution.trx0 / precinctWidth) : 0); var numprecinctshigh = (resolution.try1 > resolution.try0 ? Math.ceil(resolution.try1 / precinctHeight) - Math.floor(resolution.try0 / precinctHeight) : 0); var numprecincts = numprecinctswide * numprecinctshigh; - var precinctXOffset = Math.floor(resolution.trx0 / precinctWidth) * - precinctWidth; - var precinctYOffset = Math.floor(resolution.try0 / precinctHeight) * - precinctHeight; + resolution.precinctParameters = { - precinctXOffset: precinctXOffset, - precinctYOffset: precinctYOffset, precinctWidth: precinctWidth, precinctHeight: precinctHeight, numprecinctswide: numprecinctswide, numprecinctshigh: numprecinctshigh, - numprecincts: numprecincts + numprecincts: numprecincts, + precinctWidthInSubband: precinctWidthInSubband, + precinctHeightInSubband: precinctHeightInSubband }; } function buildCodeblocks(context, subband, dimensions) { // Section B.7 Division sub-band into code-blocks var xcb_ = dimensions.xcb_; @@ -41709,25 +44566,33 @@ tbx0: codeblockWidth * i, tby0: codeblockHeight * j, tbx1: codeblockWidth * (i + 1), tby1: codeblockHeight * (j + 1) }; - // calculate precinct number - var pi = Math.floor((codeblock.tbx0 - - precinctParameters.precinctXOffset) / - precinctParameters.precinctWidth); - var pj = Math.floor((codeblock.tby0 - - precinctParameters.precinctYOffset) / - precinctParameters.precinctHeight); - precinctNumber = pj + pi * precinctParameters.numprecinctswide; + codeblock.tbx0_ = Math.max(subband.tbx0, codeblock.tbx0); codeblock.tby0_ = Math.max(subband.tby0, codeblock.tby0); codeblock.tbx1_ = Math.min(subband.tbx1, codeblock.tbx1); codeblock.tby1_ = Math.min(subband.tby1, codeblock.tby1); + + // Calculate precinct number for this codeblock, codeblock position + // should be relative to its subband, use actual dimension and position + // See comment about codeblock group width and height + var pi = Math.floor((codeblock.tbx0_ - subband.tbx0) / + precinctParameters.precinctWidthInSubband); + var pj = Math.floor((codeblock.tby0_ - subband.tby0) / + precinctParameters.precinctHeightInSubband); + precinctNumber = pi + (pj * precinctParameters.numprecinctswide); + codeblock.precinctNumber = precinctNumber; codeblock.subbandType = subband.type; codeblock.Lblock = 3; + + if (codeblock.tbx1_ <= codeblock.tbx0_ || + codeblock.tby1_ <= codeblock.tby0_) { + continue; + } codeblocks.push(codeblock); // building precinct for the sub-band var precinct = precincts[precinctNumber]; if (precinct !== undefined) { if (i < precinct.cbxMin) { @@ -41859,10 +44724,234 @@ l = 0; } throw new Error('JPX Error: Out of packets'); }; } + function ResolutionPositionComponentLayerIterator(context) { + var siz = context.SIZ; + var tileIndex = context.currentTile.index; + var tile = context.tiles[tileIndex]; + var layersCount = tile.codingStyleDefaultParameters.layersCount; + var componentsCount = siz.Csiz; + var l, r, c, p; + var maxDecompositionLevelsCount = 0; + for (c = 0; c < componentsCount; c++) { + var component = tile.components[c]; + maxDecompositionLevelsCount = Math.max(maxDecompositionLevelsCount, + component.codingStyleParameters.decompositionLevelsCount); + } + var maxNumPrecinctsInLevel = new Int32Array( + maxDecompositionLevelsCount + 1); + for (r = 0; r <= maxDecompositionLevelsCount; ++r) { + var maxNumPrecincts = 0; + for (c = 0; c < componentsCount; ++c) { + var resolutions = tile.components[c].resolutions; + if (r < resolutions.length) { + maxNumPrecincts = Math.max(maxNumPrecincts, + resolutions[r].precinctParameters.numprecincts); + } + } + maxNumPrecinctsInLevel[r] = maxNumPrecincts; + } + l = 0; + r = 0; + c = 0; + p = 0; + + this.nextPacket = function JpxImage_nextPacket() { + // Section B.12.1.3 Resolution-position-component-layer + for (; r <= maxDecompositionLevelsCount; r++) { + for (; p < maxNumPrecinctsInLevel[r]; p++) { + for (; c < componentsCount; c++) { + var component = tile.components[c]; + if (r > component.codingStyleParameters.decompositionLevelsCount) { + continue; + } + var resolution = component.resolutions[r]; + var numprecincts = resolution.precinctParameters.numprecincts; + if (p >= numprecincts) { + continue; + } + for (; l < layersCount;) { + var packet = createPacket(resolution, p, l); + l++; + return packet; + } + l = 0; + } + c = 0; + } + p = 0; + } + throw new Error('JPX Error: Out of packets'); + }; + } + function PositionComponentResolutionLayerIterator(context) { + var siz = context.SIZ; + var tileIndex = context.currentTile.index; + var tile = context.tiles[tileIndex]; + var layersCount = tile.codingStyleDefaultParameters.layersCount; + var componentsCount = siz.Csiz; + var precinctsSizes = getPrecinctSizesInImageScale(tile); + var precinctsIterationSizes = precinctsSizes; + var l = 0, r = 0, c = 0, px = 0, py = 0; + + this.nextPacket = function JpxImage_nextPacket() { + // Section B.12.1.4 Position-component-resolution-layer + for (; py < precinctsIterationSizes.maxNumHigh; py++) { + for (; px < precinctsIterationSizes.maxNumWide; px++) { + for (; c < componentsCount; c++) { + var component = tile.components[c]; + var decompositionLevelsCount = + component.codingStyleParameters.decompositionLevelsCount; + for (; r <= decompositionLevelsCount; r++) { + var resolution = component.resolutions[r]; + var sizeInImageScale = + precinctsSizes.components[c].resolutions[r]; + var k = getPrecinctIndexIfExist( + px, + py, + sizeInImageScale, + precinctsIterationSizes, + resolution); + if (k === null) { + continue; + } + for (; l < layersCount;) { + var packet = createPacket(resolution, k, l); + l++; + return packet; + } + l = 0; + } + r = 0; + } + c = 0; + } + px = 0; + } + throw new Error('JPX Error: Out of packets'); + }; + } + function ComponentPositionResolutionLayerIterator(context) { + var siz = context.SIZ; + var tileIndex = context.currentTile.index; + var tile = context.tiles[tileIndex]; + var layersCount = tile.codingStyleDefaultParameters.layersCount; + var componentsCount = siz.Csiz; + var precinctsSizes = getPrecinctSizesInImageScale(tile); + var l = 0, r = 0, c = 0, px = 0, py = 0; + + this.nextPacket = function JpxImage_nextPacket() { + // Section B.12.1.5 Component-position-resolution-layer + for (; c < componentsCount; ++c) { + var component = tile.components[c]; + var precinctsIterationSizes = precinctsSizes.components[c]; + var decompositionLevelsCount = + component.codingStyleParameters.decompositionLevelsCount; + for (; py < precinctsIterationSizes.maxNumHigh; py++) { + for (; px < precinctsIterationSizes.maxNumWide; px++) { + for (; r <= decompositionLevelsCount; r++) { + var resolution = component.resolutions[r]; + var sizeInImageScale = precinctsIterationSizes.resolutions[r]; + var k = getPrecinctIndexIfExist( + px, + py, + sizeInImageScale, + precinctsIterationSizes, + resolution); + if (k === null) { + continue; + } + for (; l < layersCount;) { + var packet = createPacket(resolution, k, l); + l++; + return packet; + } + l = 0; + } + r = 0; + } + px = 0; + } + py = 0; + } + throw new Error('JPX Error: Out of packets'); + }; + } + function getPrecinctIndexIfExist( + pxIndex, pyIndex, sizeInImageScale, precinctIterationSizes, resolution) { + var posX = pxIndex * precinctIterationSizes.minWidth; + var posY = pyIndex * precinctIterationSizes.minHeight; + if (posX % sizeInImageScale.width !== 0 || + posY % sizeInImageScale.height !== 0) { + return null; + } + var startPrecinctRowIndex = + (posY / sizeInImageScale.width) * + resolution.precinctParameters.numprecinctswide; + return (posX / sizeInImageScale.height) + startPrecinctRowIndex; + } + function getPrecinctSizesInImageScale(tile) { + var componentsCount = tile.components.length; + var minWidth = Number.MAX_VALUE; + var minHeight = Number.MAX_VALUE; + var maxNumWide = 0; + var maxNumHigh = 0; + var sizePerComponent = new Array(componentsCount); + for (var c = 0; c < componentsCount; c++) { + var component = tile.components[c]; + var decompositionLevelsCount = + component.codingStyleParameters.decompositionLevelsCount; + var sizePerResolution = new Array(decompositionLevelsCount + 1); + var minWidthCurrentComponent = Number.MAX_VALUE; + var minHeightCurrentComponent = Number.MAX_VALUE; + var maxNumWideCurrentComponent = 0; + var maxNumHighCurrentComponent = 0; + var scale = 1; + for (var r = decompositionLevelsCount; r >= 0; --r) { + var resolution = component.resolutions[r]; + var widthCurrentResolution = + scale * resolution.precinctParameters.precinctWidth; + var heightCurrentResolution = + scale * resolution.precinctParameters.precinctHeight; + minWidthCurrentComponent = Math.min( + minWidthCurrentComponent, + widthCurrentResolution); + minHeightCurrentComponent = Math.min( + minHeightCurrentComponent, + heightCurrentResolution); + maxNumWideCurrentComponent = Math.max(maxNumWideCurrentComponent, + resolution.precinctParameters.numprecinctswide); + maxNumHighCurrentComponent = Math.max(maxNumHighCurrentComponent, + resolution.precinctParameters.numprecinctshigh); + sizePerResolution[r] = { + width: widthCurrentResolution, + height: heightCurrentResolution + }; + scale <<= 1; + } + minWidth = Math.min(minWidth, minWidthCurrentComponent); + minHeight = Math.min(minHeight, minHeightCurrentComponent); + maxNumWide = Math.max(maxNumWide, maxNumWideCurrentComponent); + maxNumHigh = Math.max(maxNumHigh, maxNumHighCurrentComponent); + sizePerComponent[c] = { + resolutions: sizePerResolution, + minWidth: minWidthCurrentComponent, + minHeight: minHeightCurrentComponent, + maxNumWide: maxNumWideCurrentComponent, + maxNumHigh: maxNumHighCurrentComponent + }; + } + return { + components: sizePerComponent, + minWidth: minWidth, + minHeight: minHeight, + maxNumWide: maxNumWide, + maxNumHigh: maxNumHigh + }; + } function buildPackets(context) { var siz = context.SIZ; var tileIndex = context.currentTile.index; var tile = context.tiles[tileIndex]; var componentsCount = siz.Csiz; @@ -41880,10 +44969,11 @@ var scale = 1 << (decompositionLevelsCount - r); resolution.trx0 = Math.ceil(component.tcx0 / scale); resolution.try0 = Math.ceil(component.tcy0 / scale); resolution.trx1 = Math.ceil(component.tcx1 / scale); resolution.try1 = Math.ceil(component.tcy1 / scale); + resolution.resLevel = r; buildPrecincts(context, resolution, blocksDimensions); resolutions.push(resolution); var subband; if (r === 0) { @@ -41950,10 +45040,22 @@ break; case 1: tile.packetsIterator = new ResolutionLayerComponentPositionIterator(context); break; + case 2: + tile.packetsIterator = + new ResolutionPositionComponentLayerIterator(context); + break; + case 3: + tile.packetsIterator = + new PositionComponentResolutionLayerIterator(context); + break; + case 4: + tile.packetsIterator = + new ComponentPositionResolutionLayerIterator(context); + break; default: throw new Error('JPX Error: Unsupported progression order ' + progressionOrder); } } @@ -41977,10 +45079,25 @@ } } bufferSize -= count; return (buffer >>> bufferSize) & ((1 << count) - 1); } + function skipMarkerIfEqual(value) { + if (data[offset + position - 1] === 0xFF && + data[offset + position] === value) { + skipBytes(1); + return true; + } else if (data[offset + position] === 0xFF && + data[offset + position + 1] === value) { + skipBytes(2); + return true; + } + return false; + } + function skipBytes(count) { + position += count; + } function alignToByte() { bufferSize = 0; if (skipNextBit) { position++; skipNextBit = false; @@ -42004,15 +45121,21 @@ value = readBits(7); return value + 37; } var tileIndex = context.currentTile.index; var tile = context.tiles[tileIndex]; + var sopMarkerUsed = context.COD.sopMarkerUsed; + var ephMarkerUsed = context.COD.ephMarkerUsed; var packetsIterator = tile.packetsIterator; while (position < dataLength) { + alignToByte(); + if (sopMarkerUsed && skipMarkerIfEqual(0x91)) { + // Skip also marker segment length and packet sequence ID + skipBytes(4); + } var packet = packetsIterator.nextPacket(); if (!readBits(1)) { - alignToByte(); continue; } var layerNumber = packet.layerNumber; var queue = [], codeblock; for (var i = 0, ii = packet.codeblocks.length; i < ii; i++) { @@ -42021,17 +45144,17 @@ var codeblockColumn = codeblock.cbx - precinct.cbxMin; var codeblockRow = codeblock.cby - precinct.cbyMin; var codeblockIncluded = false; var firstTimeInclusion = false; var valueReady; - if ('included' in codeblock) { + if (codeblock['included'] !== undefined) { codeblockIncluded = !!readBits(1); } else { // reading inclusion tree precinct = codeblock.precinct; var inclusionTree, zeroBitPlanesTree; - if ('inclusionTree' in precinct) { + if (precinct['inclusionTree'] !== undefined) { inclusionTree = precinct.inclusionTree; } else { // building inclusion and zero bit-planes trees var width = precinct.cbxMax - precinct.cbxMin + 1; var height = precinct.cbyMax - precinct.cbyMin + 1; @@ -42089,14 +45212,17 @@ codingpasses: codingpasses, dataLength: codedDataLength }); } alignToByte(); + if (ephMarkerUsed) { + skipMarkerIfEqual(0x92); + } while (queue.length > 0) { var packetItem = queue.shift(); codeblock = packetItem.codeblock; - if (!('data' in codeblock)) { + if (codeblock['data'] === undefined) { codeblock.data = []; } codeblock.data.push({ data: data, start: offset + position, @@ -42122,11 +45248,11 @@ var blockWidth = codeblock.tbx1_ - codeblock.tbx0_; var blockHeight = codeblock.tby1_ - codeblock.tby0_; if (blockWidth === 0 || blockHeight === 0) { continue; } - if (!('data' in codeblock)) { + if (codeblock['data'] === undefined) { continue; } var bitModel, currentCodingpassType; bitModel = new BitModel(blockWidth, blockHeight, codeblock.subbandType, @@ -42377,14 +45503,14 @@ var siz = context.SIZ; var componentsCount = siz.Csiz; var tile = context.tiles[tileIndex]; for (var c = 0; c < componentsCount; c++) { var component = tile.components[c]; - var qcdOrQcc = (c in context.currentTile.QCC ? + var qcdOrQcc = (context.currentTile.QCC[c] !== undefined ? context.currentTile.QCC[c] : context.currentTile.QCD); component.quantizationParameters = qcdOrQcc; - var codOrCoc = (c in context.currentTile.COC ? + var codOrCoc = (context.currentTile.COC[c] !== undefined ? context.currentTile.COC[c] : context.currentTile.COD); component.codingStyleParameters = codOrCoc; } tile.codingStyleDefaultParameters = context.currentTile.COD; } @@ -42409,11 +45535,11 @@ reset: function TagTree_reset(i, j) { var currentLevel = 0, value = 0, level; while (currentLevel < this.levels.length) { level = this.levels[currentLevel]; var index = i + j * level.width; - if (index in level.items) { + if (level.items[index] !== undefined) { value = level.items[index]; break; } level.index = index; i >>= 1; @@ -43124,11 +46250,10 @@ return JpxImage; })(); - var Jbig2Image = (function Jbig2ImageClosure() { // Utility data structures function ContextCache() {} ContextCache.prototype = { @@ -43285,14 +46410,13 @@ for (j = 0; j < width; j++) { row[j] = pixel = decoder.readBit(contexts, contextLabel); // At each pixel: Clear contextLabel pixels that are shifted // out of the context, then add new ones. - // If j + n is out of range at the right image border, then - // the undefined value of bitmap[i - 2][j + n] is shifted to 0 contextLabel = ((contextLabel & OLD_PIXEL_MASK) << 1) | - (row2[j + 3] << 11) | (row1[j + 4] << 4) | pixel; + (j + 3 < width ? row2[j + 3] << 11 : 0) | + (j + 4 < width ? row1[j + 4] << 4 : 0) | pixel; } } return bitmap; } @@ -44589,49 +47713,24 @@ */ // don't mirror as characters are already mirrored in the pdf // Finally, return string - var result = ''; for (i = 0, ii = chars.length; i < ii; ++i) { var ch = chars[i]; - if (ch !== '<' && ch !== '>') { - result += ch; + if (ch === '<' || ch === '>') { + chars[i] = ''; } } - return createBidiText(result, isLTR); + return createBidiText(chars.join(''), isLTR); } return bidi; })(); -/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ -/* Copyright 2014 Opera Software ASA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * 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. - * - * - * Based on https://code.google.com/p/smhasher/wiki/MurmurHash3. - * Hashes roughly 100 KB per millisecond on i7 3.4 GHz. - */ -/* globals Uint32ArrayView */ - -'use strict'; - var MurmurHash3_64 = (function MurmurHash3_64Closure (seed) { // Workaround for missing math precison in JS. var MASK_HIGH = 0xffff0000; var MASK_LOW = 0xffff; @@ -44775,13 +47874,11 @@ if (!PDFJS.workerSrc && typeof document !== 'undefined') { // workerSrc is not set -- using last script url to define default location PDFJS.workerSrc = (function () { 'use strict'; - var scriptTagContainer = document.body || - document.getElementsByTagName('head')[0]; - var pdfjsSrc = scriptTagContainer.lastChild.src; - return pdfjsSrc && pdfjsSrc.replace(/\.js$/i, '.worker.js'); + var pdfJsSrc = document.currentScript.src; + return pdfJsSrc && pdfJsSrc.replace(/\.js$/i, '.worker.js'); })(); }