|
@@ -28,8 +28,8 @@ factory((root.pdfjsDistBuildPdfCombined = {}));
|
|
|
// Use strict in our context only - users might not want it
|
|
|
'use strict';
|
|
|
|
|
|
-var pdfjsVersion = '1.4.76';
|
|
|
-var pdfjsBuild = '45fea88';
|
|
|
+var pdfjsVersion = '1.4.79';
|
|
|
+var pdfjsBuild = '9eedfc1';
|
|
|
|
|
|
var pdfjsFilePath =
|
|
|
typeof document !== 'undefined' && document.currentScript ?
|
|
@@ -31559,12474 +31559,12484 @@ exports.IdentityCMap = IdentityCMap;
|
|
|
|
|
|
(function (root, factory) {
|
|
|
{
|
|
|
- factory((root.pdfjsCoreObj = {}), root.pdfjsSharedUtil,
|
|
|
- root.pdfjsCorePrimitives, root.pdfjsCoreCrypto, root.pdfjsCoreParser,
|
|
|
- root.pdfjsCoreChunkedStream);
|
|
|
+ factory((root.pdfjsCorePsParser = {}), root.pdfjsSharedUtil,
|
|
|
+ root.pdfjsCoreParser);
|
|
|
}
|
|
|
-}(this, function (exports, sharedUtil, corePrimitives, coreCrypto, coreParser,
|
|
|
- coreChunkedStream) {
|
|
|
+}(this, function (exports, sharedUtil, coreParser) {
|
|
|
|
|
|
-var InvalidPDFException = sharedUtil.InvalidPDFException;
|
|
|
-var MissingDataException = sharedUtil.MissingDataException;
|
|
|
-var XRefParseException = sharedUtil.XRefParseException;
|
|
|
-var assert = sharedUtil.assert;
|
|
|
-var bytesToString = sharedUtil.bytesToString;
|
|
|
-var createPromiseCapability = sharedUtil.createPromiseCapability;
|
|
|
var error = sharedUtil.error;
|
|
|
-var info = sharedUtil.info;
|
|
|
-var isArray = sharedUtil.isArray;
|
|
|
-var isInt = sharedUtil.isInt;
|
|
|
-var isString = sharedUtil.isString;
|
|
|
-var shadow = sharedUtil.shadow;
|
|
|
-var stringToPDFString = sharedUtil.stringToPDFString;
|
|
|
-var stringToUTF8String = sharedUtil.stringToUTF8String;
|
|
|
-var warn = sharedUtil.warn;
|
|
|
-var isValidUrl = sharedUtil.isValidUrl;
|
|
|
-var Util = sharedUtil.Util;
|
|
|
-var Ref = corePrimitives.Ref;
|
|
|
-var RefSet = corePrimitives.RefSet;
|
|
|
-var RefSetCache = corePrimitives.RefSetCache;
|
|
|
-var isName = corePrimitives.isName;
|
|
|
-var isCmd = corePrimitives.isCmd;
|
|
|
-var isDict = corePrimitives.isDict;
|
|
|
-var isRef = corePrimitives.isRef;
|
|
|
-var isStream = corePrimitives.isStream;
|
|
|
-var CipherTransformFactory = coreCrypto.CipherTransformFactory;
|
|
|
+var EOF = coreParser.EOF;
|
|
|
var Lexer = coreParser.Lexer;
|
|
|
-var Parser = coreParser.Parser;
|
|
|
-var ChunkedStream = coreChunkedStream.ChunkedStream;
|
|
|
-
|
|
|
-var Catalog = (function CatalogClosure() {
|
|
|
- function Catalog(pdfManager, xref, pageFactory) {
|
|
|
- this.pdfManager = pdfManager;
|
|
|
- this.xref = xref;
|
|
|
- this.catDict = xref.getCatalogObj();
|
|
|
- this.fontCache = new RefSetCache();
|
|
|
- assert(isDict(this.catDict),
|
|
|
- 'catalog object is not a dictionary');
|
|
|
|
|
|
- // TODO refactor to move getPage() to the PDFDocument.
|
|
|
- this.pageFactory = pageFactory;
|
|
|
- this.pagePromises = [];
|
|
|
+var PostScriptParser = (function PostScriptParserClosure() {
|
|
|
+ function PostScriptParser(lexer) {
|
|
|
+ this.lexer = lexer;
|
|
|
+ this.operators = [];
|
|
|
+ this.token = null;
|
|
|
+ this.prev = null;
|
|
|
}
|
|
|
-
|
|
|
- Catalog.prototype = {
|
|
|
- get metadata() {
|
|
|
- var streamRef = this.catDict.getRaw('Metadata');
|
|
|
- if (!isRef(streamRef)) {
|
|
|
- return shadow(this, 'metadata', null);
|
|
|
- }
|
|
|
-
|
|
|
- var encryptMetadata = (!this.xref.encrypt ? false :
|
|
|
- this.xref.encrypt.encryptMetadata);
|
|
|
-
|
|
|
- var stream = this.xref.fetch(streamRef, !encryptMetadata);
|
|
|
- var metadata;
|
|
|
- if (stream && isDict(stream.dict)) {
|
|
|
- var type = stream.dict.get('Type');
|
|
|
- var subtype = stream.dict.get('Subtype');
|
|
|
-
|
|
|
- if (isName(type) && isName(subtype) &&
|
|
|
- type.name === 'Metadata' && subtype.name === 'XML') {
|
|
|
- // XXX: This should examine the charset the XML document defines,
|
|
|
- // however since there are currently no real means to decode
|
|
|
- // arbitrary charsets, let's just hope that the author of the PDF
|
|
|
- // was reasonable enough to stick with the XML default charset,
|
|
|
- // which is UTF-8.
|
|
|
- try {
|
|
|
- metadata = stringToUTF8String(bytesToString(stream.getBytes()));
|
|
|
- } catch (e) {
|
|
|
- info('Skipping invalid metadata.');
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return shadow(this, 'metadata', metadata);
|
|
|
- },
|
|
|
- get toplevelPagesDict() {
|
|
|
- var pagesObj = this.catDict.get('Pages');
|
|
|
- assert(isDict(pagesObj), 'invalid top-level pages dictionary');
|
|
|
- // shadow the prototype getter
|
|
|
- return shadow(this, 'toplevelPagesDict', pagesObj);
|
|
|
+ PostScriptParser.prototype = {
|
|
|
+ nextToken: function PostScriptParser_nextToken() {
|
|
|
+ this.prev = this.token;
|
|
|
+ this.token = this.lexer.getToken();
|
|
|
},
|
|
|
- get documentOutline() {
|
|
|
- var obj = null;
|
|
|
- try {
|
|
|
- obj = this.readDocumentOutline();
|
|
|
- } catch (ex) {
|
|
|
- if (ex instanceof MissingDataException) {
|
|
|
- throw ex;
|
|
|
- }
|
|
|
- warn('Unable to read document outline');
|
|
|
+ accept: function PostScriptParser_accept(type) {
|
|
|
+ if (this.token.type === type) {
|
|
|
+ this.nextToken();
|
|
|
+ return true;
|
|
|
}
|
|
|
- return shadow(this, 'documentOutline', obj);
|
|
|
+ return false;
|
|
|
},
|
|
|
- readDocumentOutline: function Catalog_readDocumentOutline() {
|
|
|
- var xref = this.xref;
|
|
|
- var obj = this.catDict.get('Outlines');
|
|
|
- var root = { items: [] };
|
|
|
- if (isDict(obj)) {
|
|
|
- obj = obj.getRaw('First');
|
|
|
- var processed = new RefSet();
|
|
|
- if (isRef(obj)) {
|
|
|
- var queue = [{obj: obj, parent: root}];
|
|
|
- // to avoid recursion keeping track of the items
|
|
|
- // in the processed dictionary
|
|
|
- processed.put(obj);
|
|
|
- while (queue.length > 0) {
|
|
|
- var i = queue.shift();
|
|
|
- var outlineDict = xref.fetchIfRef(i.obj);
|
|
|
- if (outlineDict === null) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- if (!outlineDict.has('Title')) {
|
|
|
- error('Invalid outline item');
|
|
|
- }
|
|
|
- var actionDict = outlineDict.get('A'), dest = null, url = null;
|
|
|
- if (actionDict) {
|
|
|
- var destEntry = actionDict.get('D');
|
|
|
- if (destEntry) {
|
|
|
- dest = destEntry;
|
|
|
- } else {
|
|
|
- var uriEntry = actionDict.get('URI');
|
|
|
- if (isString(uriEntry) && isValidUrl(uriEntry, false)) {
|
|
|
- url = uriEntry;
|
|
|
- }
|
|
|
- }
|
|
|
- } else if (outlineDict.has('Dest')) {
|
|
|
- dest = outlineDict.getRaw('Dest');
|
|
|
- if (isName(dest)) {
|
|
|
- dest = dest.name;
|
|
|
- }
|
|
|
- }
|
|
|
- var title = outlineDict.get('Title');
|
|
|
- var outlineItem = {
|
|
|
- dest: dest,
|
|
|
- url: url,
|
|
|
- title: stringToPDFString(title),
|
|
|
- color: outlineDict.get('C') || [0, 0, 0],
|
|
|
- count: outlineDict.get('Count'),
|
|
|
- bold: !!(outlineDict.get('F') & 2),
|
|
|
- italic: !!(outlineDict.get('F') & 1),
|
|
|
- items: []
|
|
|
- };
|
|
|
- i.parent.items.push(outlineItem);
|
|
|
- obj = outlineDict.getRaw('First');
|
|
|
- if (isRef(obj) && !processed.has(obj)) {
|
|
|
- queue.push({obj: obj, parent: outlineItem});
|
|
|
- processed.put(obj);
|
|
|
- }
|
|
|
- obj = outlineDict.getRaw('Next');
|
|
|
- if (isRef(obj) && !processed.has(obj)) {
|
|
|
- queue.push({obj: obj, parent: i.parent});
|
|
|
- processed.put(obj);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ expect: function PostScriptParser_expect(type) {
|
|
|
+ if (this.accept(type)) {
|
|
|
+ return true;
|
|
|
}
|
|
|
- return (root.items.length > 0 ? root.items : null);
|
|
|
+ error('Unexpected symbol: found ' + this.token.type + ' expected ' +
|
|
|
+ type + '.');
|
|
|
},
|
|
|
- get numPages() {
|
|
|
- var obj = this.toplevelPagesDict.get('Count');
|
|
|
- assert(
|
|
|
- isInt(obj),
|
|
|
- 'page count in top level pages object is not an integer'
|
|
|
- );
|
|
|
- // shadow the prototype getter
|
|
|
- return shadow(this, 'num', obj);
|
|
|
+ parse: function PostScriptParser_parse() {
|
|
|
+ this.nextToken();
|
|
|
+ this.expect(PostScriptTokenTypes.LBRACE);
|
|
|
+ this.parseBlock();
|
|
|
+ this.expect(PostScriptTokenTypes.RBRACE);
|
|
|
+ return this.operators;
|
|
|
},
|
|
|
- get destinations() {
|
|
|
- function fetchDestination(dest) {
|
|
|
- return isDict(dest) ? dest.get('D') : dest;
|
|
|
- }
|
|
|
-
|
|
|
- var xref = this.xref;
|
|
|
- var dests = {}, 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;
|
|
|
- }
|
|
|
- dests[key] = fetchDestination(value);
|
|
|
- });
|
|
|
- }
|
|
|
- if (nameTreeRef) {
|
|
|
- var nameTree = new NameTree(nameTreeRef, xref);
|
|
|
- var names = nameTree.getAll();
|
|
|
- for (var name in names) {
|
|
|
- dests[name] = fetchDestination(names[name]);
|
|
|
+ parseBlock: function PostScriptParser_parseBlock() {
|
|
|
+ while (true) {
|
|
|
+ if (this.accept(PostScriptTokenTypes.NUMBER)) {
|
|
|
+ this.operators.push(this.prev.value);
|
|
|
+ } else if (this.accept(PostScriptTokenTypes.OPERATOR)) {
|
|
|
+ this.operators.push(this.prev.value);
|
|
|
+ } else if (this.accept(PostScriptTokenTypes.LBRACE)) {
|
|
|
+ this.parseCondition();
|
|
|
+ } else {
|
|
|
+ return;
|
|
|
}
|
|
|
}
|
|
|
- return shadow(this, 'destinations', dests);
|
|
|
},
|
|
|
- getDestination: function Catalog_getDestination(destinationId) {
|
|
|
- function fetchDestination(dest) {
|
|
|
- return isDict(dest) ? dest.get('D') : dest;
|
|
|
- }
|
|
|
-
|
|
|
- var xref = this.xref;
|
|
|
- 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');
|
|
|
- }
|
|
|
+ parseCondition: function PostScriptParser_parseCondition() {
|
|
|
+ // Add two place holders that will be updated later
|
|
|
+ var conditionLocation = this.operators.length;
|
|
|
+ this.operators.push(null, null);
|
|
|
|
|
|
- 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));
|
|
|
- }
|
|
|
- return dest;
|
|
|
- },
|
|
|
+ this.parseBlock();
|
|
|
+ this.expect(PostScriptTokenTypes.RBRACE);
|
|
|
+ if (this.accept(PostScriptTokenTypes.IF)) {
|
|
|
+ // The true block is right after the 'if' so it just falls through on
|
|
|
+ // true else it jumps and skips the true block.
|
|
|
+ this.operators[conditionLocation] = this.operators.length;
|
|
|
+ this.operators[conditionLocation + 1] = 'jz';
|
|
|
+ } else if (this.accept(PostScriptTokenTypes.LBRACE)) {
|
|
|
+ var jumpLocation = this.operators.length;
|
|
|
+ this.operators.push(null, null);
|
|
|
+ var endOfTrue = this.operators.length;
|
|
|
+ this.parseBlock();
|
|
|
+ this.expect(PostScriptTokenTypes.RBRACE);
|
|
|
+ this.expect(PostScriptTokenTypes.IFELSE);
|
|
|
+ // The jump is added at the end of the true block to skip the false
|
|
|
+ // block.
|
|
|
+ this.operators[jumpLocation] = this.operators.length;
|
|
|
+ this.operators[jumpLocation + 1] = 'j';
|
|
|
|
|
|
- get pageLabels() {
|
|
|
- var obj = null;
|
|
|
- try {
|
|
|
- obj = this.readPageLabels();
|
|
|
- } catch (ex) {
|
|
|
- if (ex instanceof MissingDataException) {
|
|
|
- throw ex;
|
|
|
- }
|
|
|
- warn('Unable to read page labels.');
|
|
|
- }
|
|
|
- return shadow(this, 'pageLabels', obj);
|
|
|
- },
|
|
|
- readPageLabels: function Catalog_readPageLabels() {
|
|
|
- var obj = this.catDict.getRaw('PageLabels');
|
|
|
- if (!obj) {
|
|
|
- return null;
|
|
|
+ this.operators[conditionLocation] = endOfTrue;
|
|
|
+ this.operators[conditionLocation + 1] = 'jz';
|
|
|
+ } else {
|
|
|
+ error('PS Function: error parsing conditional.');
|
|
|
}
|
|
|
- var pageLabels = new Array(this.numPages);
|
|
|
- var style = null;
|
|
|
- var prefix = '';
|
|
|
- var start = 1;
|
|
|
-
|
|
|
- var numberTree = new NumberTree(obj, this.xref);
|
|
|
- var nums = numberTree.getAll();
|
|
|
- var currentLabel = '', currentIndex = 1;
|
|
|
-
|
|
|
- for (var i = 0, ii = this.numPages; i < ii; i++) {
|
|
|
- if (i in nums) {
|
|
|
- var labelDict = nums[i];
|
|
|
- assert(isDict(labelDict), 'The PageLabel is not a dictionary.');
|
|
|
+ }
|
|
|
+ };
|
|
|
+ return PostScriptParser;
|
|
|
+})();
|
|
|
|
|
|
- var type = labelDict.get('Type');
|
|
|
- assert(!type || (isName(type) && type.name === 'PageLabel'),
|
|
|
- 'Invalid type in PageLabel dictionary.');
|
|
|
+var PostScriptTokenTypes = {
|
|
|
+ LBRACE: 0,
|
|
|
+ RBRACE: 1,
|
|
|
+ NUMBER: 2,
|
|
|
+ OPERATOR: 3,
|
|
|
+ IF: 4,
|
|
|
+ IFELSE: 5
|
|
|
+};
|
|
|
|
|
|
- var s = labelDict.get('S');
|
|
|
- assert(!s || isName(s), 'Invalid style in PageLabel dictionary.');
|
|
|
- style = (s ? s.name : null);
|
|
|
+var PostScriptToken = (function PostScriptTokenClosure() {
|
|
|
+ function PostScriptToken(type, value) {
|
|
|
+ this.type = type;
|
|
|
+ this.value = value;
|
|
|
+ }
|
|
|
|
|
|
- prefix = labelDict.get('P') || '';
|
|
|
- assert(isString(prefix), 'Invalid prefix in PageLabel dictionary.');
|
|
|
+ var opCache = Object.create(null);
|
|
|
|
|
|
- start = labelDict.get('St') || 1;
|
|
|
- assert(isInt(start), 'Invalid start in PageLabel dictionary.');
|
|
|
- currentIndex = start;
|
|
|
- }
|
|
|
+ PostScriptToken.getOperator = function PostScriptToken_getOperator(op) {
|
|
|
+ var opValue = opCache[op];
|
|
|
+ if (opValue) {
|
|
|
+ return opValue;
|
|
|
+ }
|
|
|
+ return opCache[op] = new PostScriptToken(PostScriptTokenTypes.OPERATOR, op);
|
|
|
+ };
|
|
|
|
|
|
- switch (style) {
|
|
|
- case 'D':
|
|
|
- currentLabel = currentIndex;
|
|
|
- break;
|
|
|
- case 'R':
|
|
|
- case 'r':
|
|
|
- currentLabel = Util.toRoman(currentIndex, style === 'r');
|
|
|
- break;
|
|
|
- case 'A':
|
|
|
- case 'a':
|
|
|
- var LIMIT = 26; // Use only the characters A--Z, or a--z.
|
|
|
- var A_UPPER_CASE = 0x41, A_LOWER_CASE = 0x61;
|
|
|
+ PostScriptToken.LBRACE = new PostScriptToken(PostScriptTokenTypes.LBRACE,
|
|
|
+ '{');
|
|
|
+ PostScriptToken.RBRACE = new PostScriptToken(PostScriptTokenTypes.RBRACE,
|
|
|
+ '}');
|
|
|
+ PostScriptToken.IF = new PostScriptToken(PostScriptTokenTypes.IF, 'IF');
|
|
|
+ PostScriptToken.IFELSE = new PostScriptToken(PostScriptTokenTypes.IFELSE,
|
|
|
+ 'IFELSE');
|
|
|
+ return PostScriptToken;
|
|
|
+})();
|
|
|
|
|
|
- var baseCharCode = (style === 'a' ? A_LOWER_CASE : A_UPPER_CASE);
|
|
|
- var letterIndex = currentIndex - 1;
|
|
|
- var character = String.fromCharCode(baseCharCode +
|
|
|
- (letterIndex % LIMIT));
|
|
|
- var charBuf = [];
|
|
|
- for (var j = 0, jj = (letterIndex / LIMIT) | 0; j <= jj; j++) {
|
|
|
- charBuf.push(character);
|
|
|
- }
|
|
|
- currentLabel = charBuf.join('');
|
|
|
- break;
|
|
|
- default:
|
|
|
- assert(!style,
|
|
|
- 'Invalid style "' + style + '" in PageLabel dictionary.');
|
|
|
- }
|
|
|
- pageLabels[i] = prefix + currentLabel;
|
|
|
+var PostScriptLexer = (function PostScriptLexerClosure() {
|
|
|
+ function PostScriptLexer(stream) {
|
|
|
+ this.stream = stream;
|
|
|
+ this.nextChar();
|
|
|
|
|
|
- currentLabel = '';
|
|
|
- currentIndex++;
|
|
|
- }
|
|
|
- return pageLabels;
|
|
|
+ this.strBuf = [];
|
|
|
+ }
|
|
|
+ PostScriptLexer.prototype = {
|
|
|
+ nextChar: function PostScriptLexer_nextChar() {
|
|
|
+ return (this.currentChar = this.stream.getByte());
|
|
|
},
|
|
|
+ getToken: function PostScriptLexer_getToken() {
|
|
|
+ var comment = false;
|
|
|
+ var ch = this.currentChar;
|
|
|
|
|
|
- get attachments() {
|
|
|
- var xref = this.xref;
|
|
|
- var attachments = null, nameTreeRef;
|
|
|
- var obj = this.catDict.get('Names');
|
|
|
- if (obj) {
|
|
|
- nameTreeRef = obj.getRaw('EmbeddedFiles');
|
|
|
- }
|
|
|
+ // skip comments
|
|
|
+ while (true) {
|
|
|
+ if (ch < 0) {
|
|
|
+ return EOF;
|
|
|
+ }
|
|
|
|
|
|
- if (nameTreeRef) {
|
|
|
- var nameTree = new NameTree(nameTreeRef, xref);
|
|
|
- var names = nameTree.getAll();
|
|
|
- for (var name in names) {
|
|
|
- var fs = new FileSpec(names[name], xref);
|
|
|
- if (!attachments) {
|
|
|
- attachments = Object.create(null);
|
|
|
+ if (comment) {
|
|
|
+ if (ch === 0x0A || ch === 0x0D) {
|
|
|
+ comment = false;
|
|
|
}
|
|
|
- attachments[stringToPDFString(name)] = fs.serializable;
|
|
|
+ } else if (ch === 0x25) { // '%'
|
|
|
+ comment = true;
|
|
|
+ } else if (!Lexer.isSpace(ch)) {
|
|
|
+ break;
|
|
|
}
|
|
|
+ ch = this.nextChar();
|
|
|
}
|
|
|
- return shadow(this, 'attachments', attachments);
|
|
|
- },
|
|
|
- get javaScript() {
|
|
|
- var xref = this.xref;
|
|
|
- var obj = this.catDict.get('Names');
|
|
|
+ switch (ch | 0) {
|
|
|
+ case 0x30: case 0x31: case 0x32: case 0x33: case 0x34: // '0'-'4'
|
|
|
+ case 0x35: case 0x36: case 0x37: case 0x38: case 0x39: // '5'-'9'
|
|
|
+ case 0x2B: case 0x2D: case 0x2E: // '+', '-', '.'
|
|
|
+ return new PostScriptToken(PostScriptTokenTypes.NUMBER,
|
|
|
+ this.getNumber());
|
|
|
+ case 0x7B: // '{'
|
|
|
+ this.nextChar();
|
|
|
+ return PostScriptToken.LBRACE;
|
|
|
+ case 0x7D: // '}'
|
|
|
+ this.nextChar();
|
|
|
+ return PostScriptToken.RBRACE;
|
|
|
+ }
|
|
|
+ // operator
|
|
|
+ var strBuf = this.strBuf;
|
|
|
+ strBuf.length = 0;
|
|
|
+ strBuf[0] = String.fromCharCode(ch);
|
|
|
|
|
|
- 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));
|
|
|
+ while ((ch = this.nextChar()) >= 0 && // and 'A'-'Z', 'a'-'z'
|
|
|
+ ((ch >= 0x41 && ch <= 0x5A) || (ch >= 0x61 && ch <= 0x7A))) {
|
|
|
+ strBuf.push(String.fromCharCode(ch));
|
|
|
}
|
|
|
- if (obj && obj.has('JavaScript')) {
|
|
|
- var nameTree = new NameTree(obj.getRaw('JavaScript'), xref);
|
|
|
- var names = nameTree.getAll();
|
|
|
- for (var name in names) {
|
|
|
- // 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)) {
|
|
|
- appendIfJavaScriptDict(jsDict);
|
|
|
- }
|
|
|
- }
|
|
|
+ var str = strBuf.join('');
|
|
|
+ switch (str.toLowerCase()) {
|
|
|
+ case 'if':
|
|
|
+ return PostScriptToken.IF;
|
|
|
+ case 'ifelse':
|
|
|
+ return PostScriptToken.IFELSE;
|
|
|
+ default:
|
|
|
+ return PostScriptToken.getOperator(str);
|
|
|
}
|
|
|
+ },
|
|
|
+ getNumber: function PostScriptLexer_getNumber() {
|
|
|
+ var ch = this.currentChar;
|
|
|
+ var strBuf = this.strBuf;
|
|
|
+ strBuf.length = 0;
|
|
|
+ strBuf[0] = String.fromCharCode(ch);
|
|
|
|
|
|
- // Append OpenAction actions to javaScript array
|
|
|
- var openactionDict = this.catDict.get('OpenAction');
|
|
|
- if (isDict(openactionDict, 'Action')) {
|
|
|
- var actionType = openactionDict.get('S');
|
|
|
- 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({});');
|
|
|
- }
|
|
|
+ while ((ch = this.nextChar()) >= 0) {
|
|
|
+ if ((ch >= 0x30 && ch <= 0x39) || // '0'-'9'
|
|
|
+ ch === 0x2D || ch === 0x2E) { // '-', '.'
|
|
|
+ strBuf.push(String.fromCharCode(ch));
|
|
|
} else {
|
|
|
- appendIfJavaScriptDict(openactionDict);
|
|
|
+ break;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- return shadow(this, 'javaScript', javaScript);
|
|
|
- },
|
|
|
-
|
|
|
- cleanup: function Catalog_cleanup() {
|
|
|
- var promises = [];
|
|
|
- this.fontCache.forEach(function (promise) {
|
|
|
- promises.push(promise);
|
|
|
- });
|
|
|
- return Promise.all(promises).then(function (translatedFonts) {
|
|
|
- for (var i = 0, ii = translatedFonts.length; i < ii; i++) {
|
|
|
- var font = translatedFonts[i].dict;
|
|
|
- delete font.translated;
|
|
|
- }
|
|
|
- this.fontCache.clear();
|
|
|
- }.bind(this));
|
|
|
- },
|
|
|
-
|
|
|
- getPage: function Catalog_getPage(pageIndex) {
|
|
|
- if (!(pageIndex in this.pagePromises)) {
|
|
|
- this.pagePromises[pageIndex] = this.getPageDict(pageIndex).then(
|
|
|
- function (a) {
|
|
|
- var dict = a[0];
|
|
|
- var ref = a[1];
|
|
|
- return this.pageFactory.createPage(pageIndex, dict, ref,
|
|
|
- this.fontCache);
|
|
|
- }.bind(this)
|
|
|
- );
|
|
|
+ var value = parseFloat(strBuf.join(''));
|
|
|
+ if (isNaN(value)) {
|
|
|
+ error('Invalid floating point number: ' + value);
|
|
|
}
|
|
|
- return this.pagePromises[pageIndex];
|
|
|
- },
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ return PostScriptLexer;
|
|
|
+})();
|
|
|
|
|
|
- getPageDict: function Catalog_getPageDict(pageIndex) {
|
|
|
- var capability = createPromiseCapability();
|
|
|
- var nodesToVisit = [this.catDict.getRaw('Pages')];
|
|
|
- var currentPageIndex = 0;
|
|
|
- var xref = this.xref;
|
|
|
- var checkAllKids = false;
|
|
|
+exports.PostScriptLexer = PostScriptLexer;
|
|
|
+exports.PostScriptParser = PostScriptParser;
|
|
|
+}));
|
|
|
|
|
|
- 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 (pageIndex === currentPageIndex) {
|
|
|
- capability.resolve([obj, currentNode]);
|
|
|
- } else {
|
|
|
- currentPageIndex++;
|
|
|
- next();
|
|
|
- }
|
|
|
- return;
|
|
|
- }
|
|
|
- nodesToVisit.push(obj);
|
|
|
- next();
|
|
|
- }, capability.reject);
|
|
|
- return;
|
|
|
- }
|
|
|
+(function (root, factory) {
|
|
|
+ {
|
|
|
+ factory((root.pdfjsDisplayAPI = {}), root.pdfjsSharedUtil,
|
|
|
+ root.pdfjsDisplayFontLoader, root.pdfjsDisplayCanvas,
|
|
|
+ root.pdfjsDisplayMetadata, root.pdfjsSharedGlobal);
|
|
|
+ }
|
|
|
+}(this, function (exports, sharedUtil, displayFontLoader, displayCanvas,
|
|
|
+ displayMetadata, sharedGlobal, amdRequire) {
|
|
|
|
|
|
- // 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 InvalidPDFException = sharedUtil.InvalidPDFException;
|
|
|
+var MessageHandler = sharedUtil.MessageHandler;
|
|
|
+var MissingPDFException = sharedUtil.MissingPDFException;
|
|
|
+var PasswordResponses = sharedUtil.PasswordResponses;
|
|
|
+var PasswordException = sharedUtil.PasswordException;
|
|
|
+var StatTimer = sharedUtil.StatTimer;
|
|
|
+var UnexpectedResponseException = sharedUtil.UnexpectedResponseException;
|
|
|
+var UnknownErrorException = sharedUtil.UnknownErrorException;
|
|
|
+var Util = sharedUtil.Util;
|
|
|
+var createPromiseCapability = sharedUtil.createPromiseCapability;
|
|
|
+var combineUrl = sharedUtil.combineUrl;
|
|
|
+var error = sharedUtil.error;
|
|
|
+var deprecated = sharedUtil.deprecated;
|
|
|
+var info = sharedUtil.info;
|
|
|
+var isArrayBuffer = sharedUtil.isArrayBuffer;
|
|
|
+var isSameOrigin = sharedUtil.isSameOrigin;
|
|
|
+var loadJpegStream = sharedUtil.loadJpegStream;
|
|
|
+var stringToBytes = sharedUtil.stringToBytes;
|
|
|
+var warn = sharedUtil.warn;
|
|
|
+var FontFaceObject = displayFontLoader.FontFaceObject;
|
|
|
+var FontLoader = displayFontLoader.FontLoader;
|
|
|
+var CanvasGraphics = displayCanvas.CanvasGraphics;
|
|
|
+var createScratchCanvas = displayCanvas.createScratchCanvas;
|
|
|
+var Metadata = displayMetadata.Metadata;
|
|
|
+var PDFJS = sharedGlobal.PDFJS;
|
|
|
+var globalScope = sharedGlobal.globalScope;
|
|
|
|
|
|
- var kids = currentNode.get('Kids');
|
|
|
- assert(isArray(kids), 'page dictionary kids object is not an array');
|
|
|
- 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
|
|
|
- // handle this push it back on the queue so if it is a pages node it
|
|
|
- // will be descended into.
|
|
|
- nodesToVisit = [kids[pageIndex - currentPageIndex]];
|
|
|
- currentPageIndex = pageIndex;
|
|
|
- continue;
|
|
|
- } else {
|
|
|
- for (var last = kids.length - 1; last >= 0; last--) {
|
|
|
- nodesToVisit.push(kids[last]);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- capability.reject('Page index ' + pageIndex + ' not found.');
|
|
|
- }
|
|
|
- next();
|
|
|
- return capability.promise;
|
|
|
- },
|
|
|
+var DEFAULT_RANGE_CHUNK_SIZE = 65536; // 2^16 = 65536
|
|
|
|
|
|
- getPageIndex: function Catalog_getPageIndex(ref) {
|
|
|
- // The page tree nodes have the count of all the leaves below them. To get
|
|
|
- // how many pages are before we just have to walk up the tree and keep
|
|
|
- // adding the count of siblings to the left of the node.
|
|
|
- var xref = this.xref;
|
|
|
- function pagesBeforeRef(kidRef) {
|
|
|
- var total = 0;
|
|
|
- var parentRef;
|
|
|
- return xref.fetchAsync(kidRef).then(function (node) {
|
|
|
- if (!node) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- parentRef = node.getRaw('Parent');
|
|
|
- return node.getAsync('Parent');
|
|
|
- }).then(function (parent) {
|
|
|
- if (!parent) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- return parent.getAsync('Kids');
|
|
|
- }).then(function (kids) {
|
|
|
- if (!kids) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- var kidPromises = [];
|
|
|
- var found = false;
|
|
|
- for (var i = 0; i < kids.length; i++) {
|
|
|
- var kid = kids[i];
|
|
|
- assert(isRef(kid), 'kids must be a ref');
|
|
|
- if (kid.num === kidRef.num) {
|
|
|
- found = true;
|
|
|
- break;
|
|
|
- }
|
|
|
- kidPromises.push(xref.fetchAsync(kid).then(function (kid) {
|
|
|
- if (kid.has('Count')) {
|
|
|
- var count = kid.get('Count');
|
|
|
- total += count;
|
|
|
- } else { // page leaf node
|
|
|
- total++;
|
|
|
- }
|
|
|
- }));
|
|
|
- }
|
|
|
- if (!found) {
|
|
|
- error('kid ref not found in parents kids');
|
|
|
- }
|
|
|
- return Promise.all(kidPromises).then(function () {
|
|
|
- return [total, parentRef];
|
|
|
- });
|
|
|
- });
|
|
|
- }
|
|
|
|
|
|
- var total = 0;
|
|
|
- function next(ref) {
|
|
|
- return pagesBeforeRef(ref).then(function (args) {
|
|
|
- if (!args) {
|
|
|
- return total;
|
|
|
- }
|
|
|
- var count = args[0];
|
|
|
- var parentRef = args[1];
|
|
|
- total += count;
|
|
|
- return next(parentRef);
|
|
|
- });
|
|
|
- }
|
|
|
+/**
|
|
|
+ * 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}
|
|
|
+ */
|
|
|
+PDFJS.maxImageSize = (PDFJS.maxImageSize === undefined ?
|
|
|
+ -1 : PDFJS.maxImageSize);
|
|
|
|
|
|
- return next(ref);
|
|
|
- }
|
|
|
- };
|
|
|
+/**
|
|
|
+ * The url of where the predefined Adobe CMaps are located. Include trailing
|
|
|
+ * slash.
|
|
|
+ * @var {string}
|
|
|
+ */
|
|
|
+PDFJS.cMapUrl = (PDFJS.cMapUrl === undefined ? null : PDFJS.cMapUrl);
|
|
|
|
|
|
- return Catalog;
|
|
|
-})();
|
|
|
+/**
|
|
|
+ * Specifies if CMaps are binary packed.
|
|
|
+ * @var {boolean}
|
|
|
+ */
|
|
|
+PDFJS.cMapPacked = PDFJS.cMapPacked === undefined ? false : PDFJS.cMapPacked;
|
|
|
|
|
|
-var XRef = (function XRefClosure() {
|
|
|
- function XRef(stream, password) {
|
|
|
- this.stream = stream;
|
|
|
- this.entries = [];
|
|
|
- this.xrefstms = Object.create(null);
|
|
|
- // prepare the XRef cache
|
|
|
- this.cache = [];
|
|
|
- this.password = password;
|
|
|
- this.stats = {
|
|
|
- streamTypes: [],
|
|
|
- fontTypes: []
|
|
|
- };
|
|
|
- }
|
|
|
+/**
|
|
|
+ * 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}
|
|
|
+ */
|
|
|
+PDFJS.disableFontFace = (PDFJS.disableFontFace === undefined ?
|
|
|
+ false : PDFJS.disableFontFace);
|
|
|
|
|
|
- XRef.prototype = {
|
|
|
- setStartXRef: function XRef_setStartXRef(startXRef) {
|
|
|
- // Store the starting positions of xref tables as we process them
|
|
|
- // so we can recover from missing data errors
|
|
|
- this.startXRefQueue = [startXRef];
|
|
|
- },
|
|
|
+/**
|
|
|
+ * Path for image resources, mainly for annotation icons. Include trailing
|
|
|
+ * slash.
|
|
|
+ * @var {string}
|
|
|
+ */
|
|
|
+PDFJS.imageResourcesPath = (PDFJS.imageResourcesPath === undefined ?
|
|
|
+ '' : PDFJS.imageResourcesPath);
|
|
|
|
|
|
- parse: function XRef_parse(recoveryMode) {
|
|
|
- var trailerDict;
|
|
|
- if (!recoveryMode) {
|
|
|
- trailerDict = this.readXRef();
|
|
|
- } else {
|
|
|
- warn('Indexing all PDF objects');
|
|
|
- trailerDict = this.indexObjects();
|
|
|
- }
|
|
|
- trailerDict.assignXref(this);
|
|
|
- this.trailer = trailerDict;
|
|
|
- var encrypt = trailerDict.get('Encrypt');
|
|
|
- if (encrypt) {
|
|
|
- var ids = trailerDict.get('ID');
|
|
|
- var fileId = (ids && ids.length) ? ids[0] : '';
|
|
|
- this.encrypt = new CipherTransformFactory(encrypt, fileId,
|
|
|
- this.password);
|
|
|
- }
|
|
|
+/**
|
|
|
+ * Disable the web worker and run all code on the main thread. This will happen
|
|
|
+ * automatically if the browser doesn't support workers or sending typed arrays
|
|
|
+ * to workers.
|
|
|
+ * @var {boolean}
|
|
|
+ */
|
|
|
+PDFJS.disableWorker = (PDFJS.disableWorker === undefined ?
|
|
|
+ false : PDFJS.disableWorker);
|
|
|
|
|
|
- // get the root dictionary (catalog) object
|
|
|
- if (!(this.root = trailerDict.get('Root'))) {
|
|
|
- error('Invalid root reference');
|
|
|
- }
|
|
|
- },
|
|
|
+/**
|
|
|
+ * 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. 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);
|
|
|
|
|
|
- processXRefTable: function XRef_processXRefTable(parser) {
|
|
|
- if (!('tableState' in this)) {
|
|
|
- // Stores state of the table as we process it so we can resume
|
|
|
- // from middle of table in case of missing data error
|
|
|
- this.tableState = {
|
|
|
- entryNum: 0,
|
|
|
- streamPos: parser.lexer.stream.pos,
|
|
|
- parserBuf1: parser.buf1,
|
|
|
- parserBuf2: parser.buf2
|
|
|
- };
|
|
|
- }
|
|
|
+/**
|
|
|
+ * Disable range request loading of PDF files. When enabled and if the server
|
|
|
+ * supports partial content requests then the PDF will be fetched in chunks.
|
|
|
+ * Enabled (false) by default.
|
|
|
+ * @var {boolean}
|
|
|
+ */
|
|
|
+PDFJS.disableRange = (PDFJS.disableRange === undefined ?
|
|
|
+ false : PDFJS.disableRange);
|
|
|
|
|
|
- var obj = this.readXRefTable(parser);
|
|
|
+/**
|
|
|
+ * Disable streaming of PDF file data. By default PDF.js attempts to load PDF
|
|
|
+ * in chunks. This default behavior can be disabled.
|
|
|
+ * @var {boolean}
|
|
|
+ */
|
|
|
+PDFJS.disableStream = (PDFJS.disableStream === undefined ?
|
|
|
+ false : PDFJS.disableStream);
|
|
|
|
|
|
- // Sanity check
|
|
|
- if (!isCmd(obj, 'trailer')) {
|
|
|
- error('Invalid XRef table: could not find trailer dictionary');
|
|
|
- }
|
|
|
- // Read trailer dictionary, e.g.
|
|
|
- // trailer
|
|
|
- // << /Size 22
|
|
|
- // /Root 20R
|
|
|
- // /Info 10R
|
|
|
- // /ID [ <81b14aafa313db63dbd6f981e49f94f4> ]
|
|
|
- // >>
|
|
|
- // The parser goes through the entire stream << ... >> and provides
|
|
|
- // a getter interface for the key-value table
|
|
|
- var dict = parser.getObj();
|
|
|
+/**
|
|
|
+ * 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);
|
|
|
|
|
|
- // The pdflib PDF generator can generate a nested trailer dictionary
|
|
|
- if (!isDict(dict) && dict.dict) {
|
|
|
- dict = dict.dict;
|
|
|
- }
|
|
|
- if (!isDict(dict)) {
|
|
|
- error('Invalid XRef table: could not parse trailer dictionary');
|
|
|
- }
|
|
|
- delete this.tableState;
|
|
|
+/**
|
|
|
+ * Enables special hooks for debugging PDF.js.
|
|
|
+ * @var {boolean}
|
|
|
+ */
|
|
|
+PDFJS.pdfBug = (PDFJS.pdfBug === undefined ? false : PDFJS.pdfBug);
|
|
|
|
|
|
- return dict;
|
|
|
- },
|
|
|
+/**
|
|
|
+ * Enables transfer usage in postMessage for ArrayBuffers.
|
|
|
+ * @var {boolean}
|
|
|
+ */
|
|
|
+PDFJS.postMessageTransfers = (PDFJS.postMessageTransfers === undefined ?
|
|
|
+ true : PDFJS.postMessageTransfers);
|
|
|
|
|
|
- readXRefTable: function XRef_readXRefTable(parser) {
|
|
|
- // Example of cross-reference table:
|
|
|
- // xref
|
|
|
- // 0 1 <-- subsection header (first obj #, obj count)
|
|
|
- // 0000000000 65535 f <-- actual object (offset, generation #, f/n)
|
|
|
- // 23 2 <-- subsection header ... and so on ...
|
|
|
- // 0000025518 00002 n
|
|
|
- // 0000025635 00000 n
|
|
|
- // trailer
|
|
|
- // ...
|
|
|
+/**
|
|
|
+ * Disables URL.createObjectURL usage.
|
|
|
+ * @var {boolean}
|
|
|
+ */
|
|
|
+PDFJS.disableCreateObjectURL = (PDFJS.disableCreateObjectURL === undefined ?
|
|
|
+ false : PDFJS.disableCreateObjectURL);
|
|
|
|
|
|
- var stream = parser.lexer.stream;
|
|
|
- var tableState = this.tableState;
|
|
|
- stream.pos = tableState.streamPos;
|
|
|
- parser.buf1 = tableState.parserBuf1;
|
|
|
- parser.buf2 = tableState.parserBuf2;
|
|
|
+/**
|
|
|
+ * Disables WebGL usage.
|
|
|
+ * @var {boolean}
|
|
|
+ */
|
|
|
+PDFJS.disableWebGL = (PDFJS.disableWebGL === undefined ?
|
|
|
+ true : PDFJS.disableWebGL);
|
|
|
|
|
|
- // Outer loop is over subsection headers
|
|
|
- var obj;
|
|
|
+/**
|
|
|
+ * 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);
|
|
|
|
|
|
- while (true) {
|
|
|
- if (!('firstEntryNum' in tableState) || !('entryCount' in tableState)) {
|
|
|
- if (isCmd(obj = parser.getObj(), 'trailer')) {
|
|
|
- break;
|
|
|
- }
|
|
|
- tableState.firstEntryNum = obj;
|
|
|
- tableState.entryCount = parser.getObj();
|
|
|
- }
|
|
|
+/**
|
|
|
+ * Enables CSS only zooming.
|
|
|
+ * @var {boolean}
|
|
|
+ */
|
|
|
+PDFJS.useOnlyCssZoom = (PDFJS.useOnlyCssZoom === undefined ?
|
|
|
+ false : PDFJS.useOnlyCssZoom);
|
|
|
|
|
|
- var first = tableState.firstEntryNum;
|
|
|
- var count = tableState.entryCount;
|
|
|
- if (!isInt(first) || !isInt(count)) {
|
|
|
- error('Invalid XRef table: wrong types in subsection header');
|
|
|
- }
|
|
|
- // Inner loop is over objects themselves
|
|
|
- for (var i = tableState.entryNum; i < count; i++) {
|
|
|
- tableState.streamPos = stream.pos;
|
|
|
- tableState.entryNum = i;
|
|
|
- tableState.parserBuf1 = parser.buf1;
|
|
|
- tableState.parserBuf2 = parser.buf2;
|
|
|
+/**
|
|
|
+ * Controls the logging level.
|
|
|
+ * The constants from PDFJS.VERBOSITY_LEVELS should be used:
|
|
|
+ * - errors
|
|
|
+ * - warnings [default]
|
|
|
+ * - infos
|
|
|
+ * @var {number}
|
|
|
+ */
|
|
|
+PDFJS.verbosity = (PDFJS.verbosity === undefined ?
|
|
|
+ PDFJS.VERBOSITY_LEVELS.warnings : PDFJS.verbosity);
|
|
|
|
|
|
- var entry = {};
|
|
|
- entry.offset = parser.getObj();
|
|
|
- entry.gen = parser.getObj();
|
|
|
- var type = parser.getObj();
|
|
|
+/**
|
|
|
+ * 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);
|
|
|
|
|
|
- if (isCmd(type, 'f')) {
|
|
|
- entry.free = true;
|
|
|
- } else if (isCmd(type, 'n')) {
|
|
|
- entry.uncompressed = true;
|
|
|
- }
|
|
|
+/**
|
|
|
+ * (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);
|
|
|
|
|
|
- // Validate entry obj
|
|
|
- if (!isInt(entry.offset) || !isInt(entry.gen) ||
|
|
|
- !(entry.free || entry.uncompressed)) {
|
|
|
- error('Invalid entry in XRef subsection: ' + first + ', ' + count);
|
|
|
- }
|
|
|
+/**
|
|
|
+ * 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);
|
|
|
|
|
|
- if (!this.entries[i + first]) {
|
|
|
- this.entries[i + first] = entry;
|
|
|
- }
|
|
|
- }
|
|
|
+/**
|
|
|
+ * Specifies the |rel| attribute for external links. Defaults to stripping
|
|
|
+ * the referrer.
|
|
|
+ * @var {string}
|
|
|
+ */
|
|
|
+PDFJS.externalLinkRel = (PDFJS.externalLinkRel === undefined ?
|
|
|
+ 'noreferrer' : PDFJS.externalLinkRel);
|
|
|
|
|
|
- tableState.entryNum = 0;
|
|
|
- tableState.streamPos = stream.pos;
|
|
|
- tableState.parserBuf1 = parser.buf1;
|
|
|
- tableState.parserBuf2 = parser.buf2;
|
|
|
- delete tableState.firstEntryNum;
|
|
|
- delete tableState.entryCount;
|
|
|
- }
|
|
|
+/**
|
|
|
+ * 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);
|
|
|
|
|
|
- // Per issue 3248: hp scanners generate bad XRef
|
|
|
- if (first === 1 && this.entries[1] && this.entries[1].free) {
|
|
|
- // shifting the entries
|
|
|
- this.entries.shift();
|
|
|
- }
|
|
|
+/**
|
|
|
+ * Document initialization / loading parameters object.
|
|
|
+ *
|
|
|
+ * @typedef {Object} DocumentInitParameters
|
|
|
+ * @property {string} url - The URL of the PDF.
|
|
|
+ * @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.
|
|
|
+ */
|
|
|
|
|
|
- // Sanity check: as per spec, first object must be free
|
|
|
- if (this.entries[0] && !this.entries[0].free) {
|
|
|
- error('Invalid XRef table: unexpected first object');
|
|
|
- }
|
|
|
- return obj;
|
|
|
- },
|
|
|
+/**
|
|
|
+ * @typedef {Object} PDFDocumentStats
|
|
|
+ * @property {Array} streamTypes - Used stream types in the document (an item
|
|
|
+ * is set to true if specific stream ID was used in the document).
|
|
|
+ * @property {Array} fontTypes - Used font type in the document (an item is set
|
|
|
+ * to true if specific font ID was used in the document).
|
|
|
+ */
|
|
|
|
|
|
- processXRefStream: function XRef_processXRefStream(stream) {
|
|
|
- if (!('streamState' in this)) {
|
|
|
- // Stores state of the stream as we process it so we can resume
|
|
|
- // from middle of stream in case of missing data error
|
|
|
- var streamParameters = stream.dict;
|
|
|
- var byteWidths = streamParameters.get('W');
|
|
|
- var range = streamParameters.get('Index');
|
|
|
- if (!range) {
|
|
|
- range = [0, streamParameters.get('Size')];
|
|
|
- }
|
|
|
+/**
|
|
|
+ * 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|PDFDataRangeTransport} src
|
|
|
+ * Can be a url to where a PDF is located, a typed array (Uint8Array)
|
|
|
+ * already populated with data or parameter object.
|
|
|
+ *
|
|
|
+ * @param {PDFDataRangeTransport} pdfDataRangeTransport (deprecated) It is used
|
|
|
+ * if you want to manually serve range requests for data in the PDF.
|
|
|
+ *
|
|
|
+ * @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 (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 {PDFDocumentLoadingTask}
|
|
|
+ */
|
|
|
+PDFJS.getDocument = function getDocument(src,
|
|
|
+ pdfDataRangeTransport,
|
|
|
+ passwordCallback,
|
|
|
+ progressCallback) {
|
|
|
+ var task = new PDFDocumentLoadingTask();
|
|
|
|
|
|
- this.streamState = {
|
|
|
- entryRanges: range,
|
|
|
- byteWidths: byteWidths,
|
|
|
- entryNum: 0,
|
|
|
- streamPos: stream.pos
|
|
|
- };
|
|
|
+ // 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 () {};
|
|
|
}
|
|
|
- this.readXRefStream(stream);
|
|
|
- delete this.streamState;
|
|
|
-
|
|
|
- return stream.dict;
|
|
|
- },
|
|
|
-
|
|
|
- readXRefStream: function XRef_readXRefStream(stream) {
|
|
|
- var i, j;
|
|
|
- var streamState = this.streamState;
|
|
|
- stream.pos = streamState.streamPos;
|
|
|
-
|
|
|
- var byteWidths = streamState.byteWidths;
|
|
|
- var typeFieldWidth = byteWidths[0];
|
|
|
- var offsetFieldWidth = byteWidths[1];
|
|
|
- var generationFieldWidth = byteWidths[2];
|
|
|
-
|
|
|
- var entryRanges = streamState.entryRanges;
|
|
|
- while (entryRanges.length > 0) {
|
|
|
- var first = entryRanges[0];
|
|
|
- var n = entryRanges[1];
|
|
|
+ }
|
|
|
+ src = Object.create(src);
|
|
|
+ src.range = pdfDataRangeTransport;
|
|
|
+ }
|
|
|
+ task.onPassword = passwordCallback || null;
|
|
|
+ task.onProgress = progressCallback || null;
|
|
|
|
|
|
- if (!isInt(first) || !isInt(n)) {
|
|
|
- error('Invalid XRef range fields: ' + first + ', ' + n);
|
|
|
- }
|
|
|
- if (!isInt(typeFieldWidth) || !isInt(offsetFieldWidth) ||
|
|
|
- !isInt(generationFieldWidth)) {
|
|
|
- error('Invalid XRef entry fields length: ' + first + ', ' + n);
|
|
|
- }
|
|
|
- for (i = streamState.entryNum; i < n; ++i) {
|
|
|
- streamState.entryNum = i;
|
|
|
- streamState.streamPos = stream.pos;
|
|
|
+ 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');
|
|
|
+ }
|
|
|
|
|
|
- var type = 0, offset = 0, generation = 0;
|
|
|
- for (j = 0; j < typeFieldWidth; ++j) {
|
|
|
- type = (type << 8) | stream.getByte();
|
|
|
- }
|
|
|
- // if type field is absent, its default value is 1
|
|
|
- if (typeFieldWidth === 0) {
|
|
|
- type = 1;
|
|
|
- }
|
|
|
- for (j = 0; j < offsetFieldWidth; ++j) {
|
|
|
- offset = (offset << 8) | stream.getByte();
|
|
|
- }
|
|
|
- for (j = 0; j < generationFieldWidth; ++j) {
|
|
|
- generation = (generation << 8) | stream.getByte();
|
|
|
- }
|
|
|
- var entry = {};
|
|
|
- entry.offset = offset;
|
|
|
- entry.gen = generation;
|
|
|
- switch (type) {
|
|
|
- case 0:
|
|
|
- entry.free = true;
|
|
|
- break;
|
|
|
- case 1:
|
|
|
- entry.uncompressed = true;
|
|
|
- break;
|
|
|
- case 2:
|
|
|
- break;
|
|
|
- default:
|
|
|
- error('Invalid XRef entry type: ' + type);
|
|
|
- }
|
|
|
- if (!this.entries[first + i]) {
|
|
|
- this.entries[first + i] = entry;
|
|
|
- }
|
|
|
- }
|
|
|
+ source = src;
|
|
|
+ }
|
|
|
|
|
|
- streamState.entryNum = 0;
|
|
|
- streamState.streamPos = stream.pos;
|
|
|
- entryRanges.splice(0, 2);
|
|
|
+ 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];
|
|
|
+ }
|
|
|
|
|
|
- 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;
|
|
|
+ params.rangeChunkSize = params.rangeChunkSize || DEFAULT_RANGE_CHUNK_SIZE;
|
|
|
|
|
|
- function readToken(data, offset) {
|
|
|
- var token = '', ch = data[offset];
|
|
|
- while (ch !== LF && ch !== CR && ch !== LT) {
|
|
|
- if (++offset >= data.length) {
|
|
|
- break;
|
|
|
- }
|
|
|
- token += String.fromCharCode(ch);
|
|
|
- ch = data[offset];
|
|
|
- }
|
|
|
- return token;
|
|
|
- }
|
|
|
- function skipUntil(data, offset, what) {
|
|
|
- var length = what.length, dataLength = data.length;
|
|
|
- var skipped = 0;
|
|
|
- // finding byte sequence
|
|
|
- while (offset < dataLength) {
|
|
|
- var i = 0;
|
|
|
- while (i < length && data[offset + i] === what[i]) {
|
|
|
- ++i;
|
|
|
- }
|
|
|
- if (i >= length) {
|
|
|
- break; // sequence found
|
|
|
- }
|
|
|
- offset++;
|
|
|
- skipped++;
|
|
|
- }
|
|
|
- return skipped;
|
|
|
+ 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 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]);
|
|
|
+ var messageHandler = new MessageHandler(docId, workerId, worker.port);
|
|
|
+ var transport = new WorkerTransport(messageHandler, task, rangeTransport);
|
|
|
+ task._transport = transport;
|
|
|
+ messageHandler.send('Ready', null);
|
|
|
+ });
|
|
|
+ }).catch(task._capability.reject);
|
|
|
|
|
|
- // Clear out any existing entries, since they may be bogus.
|
|
|
- this.entries.length = 0;
|
|
|
+ return task;
|
|
|
+};
|
|
|
|
|
|
- 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 === TAB || ch === LF || ch === CR || ch === SPACE) {
|
|
|
- ++position;
|
|
|
- continue;
|
|
|
- }
|
|
|
- if (ch === PERCENT) { // %-comment
|
|
|
- do {
|
|
|
- ++position;
|
|
|
- if (position >= length) {
|
|
|
- break;
|
|
|
- }
|
|
|
- ch = buffer[position];
|
|
|
- } while (ch !== LF && ch !== CR);
|
|
|
- continue;
|
|
|
- }
|
|
|
- var token = readToken(buffer, position);
|
|
|
- var m;
|
|
|
- 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 = 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);
|
|
|
+/**
|
|
|
+ * 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'));
|
|
|
+ }
|
|
|
|
|
|
- // 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 - stream.start);
|
|
|
- this.xrefstms[position - stream.start] = 1; // Avoid recursion
|
|
|
- }
|
|
|
+ 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;
|
|
|
+ });
|
|
|
+}
|
|
|
|
|
|
- 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
|
|
|
- var i, ii;
|
|
|
- for (i = 0, ii = xrefStms.length; i < ii; ++i) {
|
|
|
- this.startXRefQueue.push(xrefStms[i]);
|
|
|
- this.readXRef(/* recoveryMode */ true);
|
|
|
- }
|
|
|
- // finding main trailer
|
|
|
- var dict;
|
|
|
- for (i = 0, ii = trailers.length; i < ii; ++i) {
|
|
|
- stream.pos = trailers[i];
|
|
|
- var parser = new Parser(new Lexer(stream), true, this);
|
|
|
- var obj = parser.getObj();
|
|
|
- if (!isCmd(obj, 'trailer')) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- // read the trailer dictionary
|
|
|
- if (!isDict(dict = parser.getObj())) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- // taking the first one with 'ID'
|
|
|
- if (dict.has('ID')) {
|
|
|
- return dict;
|
|
|
- }
|
|
|
- }
|
|
|
- // no tailer with 'ID', taking last one (if exists)
|
|
|
- if (dict) {
|
|
|
- return dict;
|
|
|
- }
|
|
|
- // nothing helps
|
|
|
- // calling error() would reject worker with an UnknownErrorException.
|
|
|
- throw new InvalidPDFException('Invalid PDF structure');
|
|
|
- },
|
|
|
+/**
|
|
|
+ * PDF document loading operation.
|
|
|
+ * @class
|
|
|
+ * @alias PDFDocumentLoadingTask
|
|
|
+ */
|
|
|
+var PDFDocumentLoadingTask = (function PDFDocumentLoadingTaskClosure() {
|
|
|
+ var nextDocumentId = 0;
|
|
|
|
|
|
- readXRef: function XRef_readXRef(recoveryMode) {
|
|
|
- var stream = this.stream;
|
|
|
+ /** @constructs PDFDocumentLoadingTask */
|
|
|
+ function PDFDocumentLoadingTask() {
|
|
|
+ this._capability = createPromiseCapability();
|
|
|
+ this._transport = null;
|
|
|
+ this._worker = null;
|
|
|
|
|
|
- try {
|
|
|
- while (this.startXRefQueue.length) {
|
|
|
- var startXRef = this.startXRefQueue[0];
|
|
|
+ /**
|
|
|
+ * Unique document loading task id -- used in MessageHandlers.
|
|
|
+ * @type {string}
|
|
|
+ */
|
|
|
+ this.docId = 'd' + (nextDocumentId++);
|
|
|
|
|
|
- stream.pos = startXRef + stream.start;
|
|
|
+ /**
|
|
|
+ * Shows if loading task is destroyed.
|
|
|
+ * @type {boolean}
|
|
|
+ */
|
|
|
+ this.destroyed = false;
|
|
|
|
|
|
- var parser = new Parser(new Lexer(stream), true, this);
|
|
|
- var obj = parser.getObj();
|
|
|
- var dict;
|
|
|
+ /**
|
|
|
+ * 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;
|
|
|
|
|
|
- // Get dictionary
|
|
|
- if (isCmd(obj, 'xref')) {
|
|
|
- // Parse end-of-file XRef
|
|
|
- dict = this.processXRefTable(parser);
|
|
|
- if (!this.topDict) {
|
|
|
- this.topDict = dict;
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * 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;
|
|
|
|
|
|
- // Recursively get other XRefs 'XRefStm', if any
|
|
|
- obj = dict.get('XRefStm');
|
|
|
- if (isInt(obj)) {
|
|
|
- var pos = obj;
|
|
|
- // ignore previously loaded xref streams
|
|
|
- // (possible infinite recursion)
|
|
|
- if (!(pos in this.xrefstms)) {
|
|
|
- this.xrefstms[pos] = 1;
|
|
|
- this.startXRefQueue.push(pos);
|
|
|
- }
|
|
|
- }
|
|
|
- } else if (isInt(obj)) {
|
|
|
- // Parse in-stream XRef
|
|
|
- if (!isInt(parser.getObj()) ||
|
|
|
- !isCmd(parser.getObj(), 'obj') ||
|
|
|
- !isStream(obj = parser.getObj())) {
|
|
|
- error('Invalid XRef stream');
|
|
|
- }
|
|
|
- dict = this.processXRefStream(obj);
|
|
|
- if (!this.topDict) {
|
|
|
- this.topDict = dict;
|
|
|
- }
|
|
|
- if (!dict) {
|
|
|
- error('Failed to read XRef stream');
|
|
|
- }
|
|
|
- } else {
|
|
|
- error('Invalid XRef stream header');
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * Callback to when unsupported feature is used. The callback receives
|
|
|
+ * an {PDFJS.UNSUPPORTED_FEATURES} argument.
|
|
|
+ */
|
|
|
+ this.onUnsupportedFeature = null;
|
|
|
+ }
|
|
|
|
|
|
- // Recursively get previous dictionary, if any
|
|
|
- obj = dict.get('Prev');
|
|
|
- if (isInt(obj)) {
|
|
|
- this.startXRefQueue.push(obj);
|
|
|
- } else if (isRef(obj)) {
|
|
|
- // The spec says Prev must not be a reference, i.e. "/Prev NNN"
|
|
|
- // This is a fallback for non-compliant PDFs, i.e. "/Prev NNN 0 R"
|
|
|
- this.startXRefQueue.push(obj.num);
|
|
|
- }
|
|
|
+ PDFDocumentLoadingTask.prototype =
|
|
|
+ /** @lends PDFDocumentLoadingTask.prototype */ {
|
|
|
+ /**
|
|
|
+ * @return {Promise}
|
|
|
+ */
|
|
|
+ get promise() {
|
|
|
+ return this._capability.promise;
|
|
|
+ },
|
|
|
|
|
|
- this.startXRefQueue.shift();
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * Aborts all network requests and destroys worker.
|
|
|
+ * @return {Promise} A promise that is resolved after destruction activity
|
|
|
+ * is completed.
|
|
|
+ */
|
|
|
+ destroy: function () {
|
|
|
+ this.destroyed = true;
|
|
|
|
|
|
- return this.topDict;
|
|
|
- } catch (e) {
|
|
|
- if (e instanceof MissingDataException) {
|
|
|
- throw e;
|
|
|
+ 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;
|
|
|
}
|
|
|
- info('(while reading XRef): ' + e);
|
|
|
- }
|
|
|
-
|
|
|
- if (recoveryMode) {
|
|
|
- return;
|
|
|
- }
|
|
|
- throw new XRefParseException();
|
|
|
+ }.bind(this));
|
|
|
},
|
|
|
|
|
|
- getEntry: function XRef_getEntry(i) {
|
|
|
- var xrefEntry = this.entries[i];
|
|
|
- if (xrefEntry && !xrefEntry.free && xrefEntry.offset) {
|
|
|
- return xrefEntry;
|
|
|
- }
|
|
|
- return null;
|
|
|
- },
|
|
|
+ /**
|
|
|
+ * 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);
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- fetchIfRef: function XRef_fetchIfRef(obj) {
|
|
|
- if (!isRef(obj)) {
|
|
|
- return obj;
|
|
|
- }
|
|
|
- return this.fetch(obj);
|
|
|
- },
|
|
|
+ return PDFDocumentLoadingTask;
|
|
|
+})();
|
|
|
|
|
|
- fetch: function XRef_fetch(ref, suppressEncryption) {
|
|
|
- assert(isRef(ref), 'ref object is not a reference');
|
|
|
- var num = ref.num;
|
|
|
- if (num in this.cache) {
|
|
|
- var cacheEntry = this.cache[num];
|
|
|
- return cacheEntry;
|
|
|
- }
|
|
|
+/**
|
|
|
+ * 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;
|
|
|
|
|
|
- var xrefEntry = this.getEntry(num);
|
|
|
+ this._rangeListeners = [];
|
|
|
+ this._progressListeners = [];
|
|
|
+ this._progressiveReadListeners = [];
|
|
|
+ this._readyCapability = createPromiseCapability();
|
|
|
+ }
|
|
|
+ PDFDataRangeTransport.prototype =
|
|
|
+ /** @lends PDFDataRangeTransport.prototype */ {
|
|
|
+ addRangeListener:
|
|
|
+ function PDFDataRangeTransport_addRangeListener(listener) {
|
|
|
+ this._rangeListeners.push(listener);
|
|
|
+ },
|
|
|
|
|
|
- // the referenced entry can be free
|
|
|
- if (xrefEntry === null) {
|
|
|
- return (this.cache[num] = null);
|
|
|
- }
|
|
|
+ addProgressListener:
|
|
|
+ function PDFDataRangeTransport_addProgressListener(listener) {
|
|
|
+ this._progressListeners.push(listener);
|
|
|
+ },
|
|
|
|
|
|
- if (xrefEntry.uncompressed) {
|
|
|
- xrefEntry = this.fetchUncompressed(ref, xrefEntry, suppressEncryption);
|
|
|
- } else {
|
|
|
- xrefEntry = this.fetchCompressed(xrefEntry, suppressEncryption);
|
|
|
- }
|
|
|
- if (isDict(xrefEntry)){
|
|
|
- xrefEntry.objId = ref.toString();
|
|
|
- } else if (isStream(xrefEntry)) {
|
|
|
- xrefEntry.dict.objId = ref.toString();
|
|
|
- }
|
|
|
- return xrefEntry;
|
|
|
+ addProgressiveReadListener:
|
|
|
+ function PDFDataRangeTransport_addProgressiveReadListener(listener) {
|
|
|
+ this._progressiveReadListeners.push(listener);
|
|
|
},
|
|
|
|
|
|
- fetchUncompressed: function XRef_fetchUncompressed(ref, xrefEntry,
|
|
|
- suppressEncryption) {
|
|
|
- var gen = ref.gen;
|
|
|
- var num = ref.num;
|
|
|
- if (xrefEntry.gen !== gen) {
|
|
|
- error('inconsistent generation in XRef');
|
|
|
- }
|
|
|
- var stream = this.stream.makeSubStream(xrefEntry.offset +
|
|
|
- this.stream.start);
|
|
|
- var parser = new Parser(new Lexer(stream), true, this);
|
|
|
- var obj1 = parser.getObj();
|
|
|
- var obj2 = parser.getObj();
|
|
|
- var obj3 = parser.getObj();
|
|
|
- if (!isInt(obj1) || parseInt(obj1, 10) !== num ||
|
|
|
- !isInt(obj2) || parseInt(obj2, 10) !== gen ||
|
|
|
- !isCmd(obj3)) {
|
|
|
- error('bad XRef entry');
|
|
|
- }
|
|
|
- if (!isCmd(obj3, 'obj')) {
|
|
|
- // some bad PDFs use "obj1234" and really mean 1234
|
|
|
- if (obj3.cmd.indexOf('obj') === 0) {
|
|
|
- num = parseInt(obj3.cmd.substring(3), 10);
|
|
|
- if (!isNaN(num)) {
|
|
|
- return num;
|
|
|
- }
|
|
|
- }
|
|
|
- error('bad XRef entry');
|
|
|
- }
|
|
|
- if (this.encrypt && !suppressEncryption) {
|
|
|
- xrefEntry = parser.getObj(this.encrypt.createCipherTransform(num, gen));
|
|
|
- } else {
|
|
|
- xrefEntry = parser.getObj();
|
|
|
- }
|
|
|
- if (!isStream(xrefEntry)) {
|
|
|
- this.cache[num] = xrefEntry;
|
|
|
+ onDataRange: function PDFDataRangeTransport_onDataRange(begin, chunk) {
|
|
|
+ var listeners = this._rangeListeners;
|
|
|
+ for (var i = 0, n = listeners.length; i < n; ++i) {
|
|
|
+ listeners[i](begin, chunk);
|
|
|
}
|
|
|
- return xrefEntry;
|
|
|
},
|
|
|
|
|
|
- fetchCompressed: function XRef_fetchCompressed(xrefEntry,
|
|
|
- suppressEncryption) {
|
|
|
- var tableOffset = xrefEntry.offset;
|
|
|
- var stream = this.fetch(new Ref(tableOffset, 0));
|
|
|
- if (!isStream(stream)) {
|
|
|
- error('bad ObjStm stream');
|
|
|
- }
|
|
|
- var first = stream.dict.get('First');
|
|
|
- var n = stream.dict.get('N');
|
|
|
- if (!isInt(first) || !isInt(n)) {
|
|
|
- error('invalid first and n parameters for ObjStm stream');
|
|
|
- }
|
|
|
- var parser = new Parser(new Lexer(stream), false, this);
|
|
|
- parser.allowStreams = true;
|
|
|
- var i, entries = [], num, nums = [];
|
|
|
- // read the object numbers to populate cache
|
|
|
- for (i = 0; i < n; ++i) {
|
|
|
- num = parser.getObj();
|
|
|
- if (!isInt(num)) {
|
|
|
- error('invalid object number in the ObjStm stream: ' + num);
|
|
|
- }
|
|
|
- nums.push(num);
|
|
|
- var offset = parser.getObj();
|
|
|
- if (!isInt(offset)) {
|
|
|
- error('invalid object offset in the ObjStm stream: ' + offset);
|
|
|
+ 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);
|
|
|
}
|
|
|
- }
|
|
|
- // read stream objects for cache
|
|
|
- for (i = 0; i < n; ++i) {
|
|
|
- entries.push(parser.getObj());
|
|
|
- num = nums[i];
|
|
|
- var entry = this.entries[num];
|
|
|
- if (entry && entry.offset === tableOffset && entry.gen === i) {
|
|
|
- this.cache[num] = entries[i];
|
|
|
+ }.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);
|
|
|
}
|
|
|
- }
|
|
|
- xrefEntry = entries[xrefEntry.gen];
|
|
|
- if (xrefEntry === undefined) {
|
|
|
- error('bad XRef entry for compressed object');
|
|
|
- }
|
|
|
- return xrefEntry;
|
|
|
+ }.bind(this));
|
|
|
},
|
|
|
|
|
|
- fetchIfRefAsync: function XRef_fetchIfRefAsync(obj) {
|
|
|
- if (!isRef(obj)) {
|
|
|
- return Promise.resolve(obj);
|
|
|
- }
|
|
|
- return this.fetchAsync(obj);
|
|
|
+ transportReady: function PDFDataRangeTransport_transportReady() {
|
|
|
+ this._readyCapability.resolve();
|
|
|
},
|
|
|
|
|
|
- fetchAsync: function XRef_fetchAsync(ref, suppressEncryption) {
|
|
|
- var streamManager = this.stream.manager;
|
|
|
- var xref = this;
|
|
|
- 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).then(function () {
|
|
|
- tryFetch(resolve, reject);
|
|
|
- }, reject);
|
|
|
- return;
|
|
|
- }
|
|
|
- reject(e);
|
|
|
- }
|
|
|
- });
|
|
|
+ requestDataRange:
|
|
|
+ function PDFDataRangeTransport_requestDataRange(begin, end) {
|
|
|
+ throw new Error('Abstract method PDFDataRangeTransport.requestDataRange');
|
|
|
},
|
|
|
|
|
|
- getCatalogObj: function XRef_getCatalogObj() {
|
|
|
- return this.root;
|
|
|
+ abort: function PDFDataRangeTransport_abort() {
|
|
|
}
|
|
|
};
|
|
|
-
|
|
|
- return XRef;
|
|
|
+ return PDFDataRangeTransport;
|
|
|
})();
|
|
|
|
|
|
+PDFJS.PDFDataRangeTransport = PDFDataRangeTransport;
|
|
|
+
|
|
|
/**
|
|
|
- * A NameTree/NumberTree is like a Dict but has some advantageous properties,
|
|
|
- * see the specification (7.9.6 and 7.9.7) for additional details.
|
|
|
- * TODO: implement all the Dict functions and make this more efficient.
|
|
|
+ * Proxy to a PDFDocument in the worker thread. Also, contains commonly used
|
|
|
+ * properties that can be read synchronously.
|
|
|
+ * @class
|
|
|
+ * @alias PDFDocumentProxy
|
|
|
*/
|
|
|
-var NameOrNumberTree = (function NameOrNumberTreeClosure() {
|
|
|
- function NameOrNumberTree(root, xref) {
|
|
|
- throw new Error('Cannot initialize NameOrNumberTree.');
|
|
|
+var PDFDocumentProxy = (function PDFDocumentProxyClosure() {
|
|
|
+ function PDFDocumentProxy(pdfInfo, transport, loadingTask) {
|
|
|
+ this.pdfInfo = pdfInfo;
|
|
|
+ this.transport = transport;
|
|
|
+ this.loadingTask = loadingTask;
|
|
|
}
|
|
|
-
|
|
|
- NameOrNumberTree.prototype = {
|
|
|
- getAll: function NameOrNumberTree_getAll() {
|
|
|
- var dict = Object.create(null);
|
|
|
- if (!this.root) {
|
|
|
- return dict;
|
|
|
- }
|
|
|
- var xref = this.xref;
|
|
|
- // Reading Name/Number tree.
|
|
|
- var processed = new RefSet();
|
|
|
- processed.put(this.root);
|
|
|
- var queue = [this.root];
|
|
|
- while (queue.length > 0) {
|
|
|
- var i, n;
|
|
|
- var obj = xref.fetchIfRef(queue.shift());
|
|
|
- if (!isDict(obj)) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- if (obj.has('Kids')) {
|
|
|
- var kids = obj.get('Kids');
|
|
|
- for (i = 0, n = kids.length; i < n; i++) {
|
|
|
- var kid = kids[i];
|
|
|
- assert(!processed.has(kid),
|
|
|
- 'Duplicate entry in "' + this._type + '" tree.');
|
|
|
- queue.push(kid);
|
|
|
- processed.put(kid);
|
|
|
- }
|
|
|
- continue;
|
|
|
- }
|
|
|
- var entries = obj.get(this._type);
|
|
|
- if (isArray(entries)) {
|
|
|
- for (i = 0, n = entries.length; i < n; i += 2) {
|
|
|
- dict[xref.fetchIfRef(entries[i])] = xref.fetchIfRef(entries[i + 1]);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- return dict;
|
|
|
+ PDFDocumentProxy.prototype = /** @lends PDFDocumentProxy.prototype */ {
|
|
|
+ /**
|
|
|
+ * @return {number} Total number of pages the PDF contains.
|
|
|
+ */
|
|
|
+ get numPages() {
|
|
|
+ return this.pdfInfo.numPages;
|
|
|
},
|
|
|
-
|
|
|
- get: function NameOrNumberTree_get(key) {
|
|
|
- if (!this.root) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- var xref = this.xref;
|
|
|
- var kidsOrEntries = xref.fetchIfRef(this.root);
|
|
|
- var loopCount = 0;
|
|
|
- var MAX_LEVELS = 10;
|
|
|
- var l, r, m;
|
|
|
-
|
|
|
- // Perform a binary search to quickly find the entry that
|
|
|
- // contains the key we are looking for.
|
|
|
- while (kidsOrEntries.has('Kids')) {
|
|
|
- if (++loopCount > MAX_LEVELS) {
|
|
|
- warn('Search depth limit reached for "' + this._type + '" tree.');
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- var kids = kidsOrEntries.get('Kids');
|
|
|
- if (!isArray(kids)) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- l = 0;
|
|
|
- r = kids.length - 1;
|
|
|
- while (l <= r) {
|
|
|
- m = (l + r) >> 1;
|
|
|
- var kid = xref.fetchIfRef(kids[m]);
|
|
|
- var limits = kid.get('Limits');
|
|
|
-
|
|
|
- if (key < xref.fetchIfRef(limits[0])) {
|
|
|
- r = m - 1;
|
|
|
- } else if (key > xref.fetchIfRef(limits[1])) {
|
|
|
- l = m + 1;
|
|
|
- } else {
|
|
|
- kidsOrEntries = xref.fetchIfRef(kids[m]);
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- if (l > r) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // If we get here, then we have found the right entry. Now go through the
|
|
|
- // entries in the dictionary until we find the key we're looking for.
|
|
|
- var entries = kidsOrEntries.get(this._type);
|
|
|
- if (isArray(entries)) {
|
|
|
- // Perform a binary search to reduce the lookup time.
|
|
|
- l = 0;
|
|
|
- r = entries.length - 2;
|
|
|
- while (l <= r) {
|
|
|
- // Check only even indices (0, 2, 4, ...) because the
|
|
|
- // odd indices contain the actual data.
|
|
|
- m = (l + r) & ~1;
|
|
|
- var currentKey = xref.fetchIfRef(entries[m]);
|
|
|
- if (key < currentKey) {
|
|
|
- r = m - 2;
|
|
|
- } else if (key > currentKey) {
|
|
|
- l = m + 2;
|
|
|
- } else {
|
|
|
- return xref.fetchIfRef(entries[m + 1]);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- return null;
|
|
|
+ /**
|
|
|
+ * @return {string} A unique ID to identify a PDF. Not guaranteed to be
|
|
|
+ * unique.
|
|
|
+ */
|
|
|
+ get fingerprint() {
|
|
|
+ return this.pdfInfo.fingerprint;
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * @param {number} pageNumber The page number to get. The first page is 1.
|
|
|
+ * @return {Promise} A promise that is resolved with a {@link PDFPageProxy}
|
|
|
+ * object.
|
|
|
+ */
|
|
|
+ getPage: function PDFDocumentProxy_getPage(pageNumber) {
|
|
|
+ return this.transport.getPage(pageNumber);
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * @param {{num: number, gen: number}} ref The page reference. Must have
|
|
|
+ * the 'num' and 'gen' properties.
|
|
|
+ * @return {Promise} A promise that is resolved with the page index that is
|
|
|
+ * associated with the reference.
|
|
|
+ */
|
|
|
+ getPageIndex: function PDFDocumentProxy_getPageIndex(ref) {
|
|
|
+ return this.transport.getPageIndex(ref);
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * @return {Promise} A promise that is resolved with a lookup table for
|
|
|
+ * mapping named destinations to reference numbers.
|
|
|
+ *
|
|
|
+ * This can be slow for large documents: use getDestination instead
|
|
|
+ */
|
|
|
+ getDestinations: function PDFDocumentProxy_getDestinations() {
|
|
|
+ return this.transport.getDestinations();
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * @param {string} id The named destination to get.
|
|
|
+ * @return {Promise} A promise that is resolved with all information
|
|
|
+ * of the given named destination.
|
|
|
+ */
|
|
|
+ getDestination: function PDFDocumentProxy_getDestination(id) {
|
|
|
+ return this.transport.getDestination(id);
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * @return {Promise} A promise that is resolved with:
|
|
|
+ * an Array containing the pageLabels that correspond to the pageIndexes,
|
|
|
+ * or `null` when no pageLabels are present in the PDF file.
|
|
|
+ */
|
|
|
+ getPageLabels: function PDFDocumentProxy_getPageLabels() {
|
|
|
+ return this.transport.getPageLabels();
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * @return {Promise} A promise that is resolved with a lookup table for
|
|
|
+ * mapping named attachments to their content.
|
|
|
+ */
|
|
|
+ getAttachments: function PDFDocumentProxy_getAttachments() {
|
|
|
+ return this.transport.getAttachments();
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * @return {Promise} A promise that is resolved with an array of all the
|
|
|
+ * JavaScript strings in the name tree.
|
|
|
+ */
|
|
|
+ getJavaScript: function PDFDocumentProxy_getJavaScript() {
|
|
|
+ return this.transport.getJavaScript();
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * @return {Promise} A promise that is resolved with an {Array} that is a
|
|
|
+ * tree outline (if it has one) of the PDF. The tree is in the format of:
|
|
|
+ * [
|
|
|
+ * {
|
|
|
+ * title: string,
|
|
|
+ * bold: boolean,
|
|
|
+ * italic: boolean,
|
|
|
+ * color: rgb Uint8Array,
|
|
|
+ * dest: dest obj,
|
|
|
+ * url: string,
|
|
|
+ * items: array of more items like this
|
|
|
+ * },
|
|
|
+ * ...
|
|
|
+ * ].
|
|
|
+ */
|
|
|
+ getOutline: function PDFDocumentProxy_getOutline() {
|
|
|
+ return this.transport.getOutline();
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * @return {Promise} A promise that is resolved with an {Object} that has
|
|
|
+ * info and metadata properties. Info is an {Object} filled with anything
|
|
|
+ * available in the information dictionary and similarly metadata is a
|
|
|
+ * {Metadata} object with information from the metadata section of the PDF.
|
|
|
+ */
|
|
|
+ getMetadata: function PDFDocumentProxy_getMetadata() {
|
|
|
+ return this.transport.getMetadata();
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * @return {Promise} A promise that is resolved with a TypedArray that has
|
|
|
+ * the raw data from the PDF.
|
|
|
+ */
|
|
|
+ getData: function PDFDocumentProxy_getData() {
|
|
|
+ return this.transport.getData();
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * @return {Promise} A promise that is resolved when the document's data
|
|
|
+ * is loaded. It is resolved with an {Object} that contains the length
|
|
|
+ * property that indicates size of the PDF data in bytes.
|
|
|
+ */
|
|
|
+ getDownloadInfo: function PDFDocumentProxy_getDownloadInfo() {
|
|
|
+ return this.transport.downloadInfoCapability.promise;
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * @return {Promise} A promise this is resolved with current stats about
|
|
|
+ * document structures (see {@link PDFDocumentStats}).
|
|
|
+ */
|
|
|
+ getStats: function PDFDocumentProxy_getStats() {
|
|
|
+ return this.transport.getStats();
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * Cleans up resources allocated by the document, e.g. created @font-face.
|
|
|
+ */
|
|
|
+ cleanup: function PDFDocumentProxy_cleanup() {
|
|
|
+ this.transport.startCleanup();
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * Destroys current document instance and terminates worker.
|
|
|
+ */
|
|
|
+ destroy: function PDFDocumentProxy_destroy() {
|
|
|
+ return this.loadingTask.destroy();
|
|
|
}
|
|
|
};
|
|
|
- return NameOrNumberTree;
|
|
|
+ return PDFDocumentProxy;
|
|
|
})();
|
|
|
|
|
|
-var NameTree = (function NameTreeClosure() {
|
|
|
- function NameTree(root, xref) {
|
|
|
- this.root = root;
|
|
|
- this.xref = xref;
|
|
|
- this._type = 'Names';
|
|
|
- }
|
|
|
-
|
|
|
- Util.inherit(NameTree, NameOrNumberTree, {});
|
|
|
-
|
|
|
- return NameTree;
|
|
|
-})();
|
|
|
+/**
|
|
|
+ * Page getTextContent parameters.
|
|
|
+ *
|
|
|
+ * @typedef {Object} getTextContentParameters
|
|
|
+ * @param {boolean} normalizeWhitespace - replaces all occurrences of
|
|
|
+ * whitespace with standard spaces (0x20). The default value is `false`.
|
|
|
+ */
|
|
|
|
|
|
-var NumberTree = (function NumberTreeClosure() {
|
|
|
- function NumberTree(root, xref) {
|
|
|
- this.root = root;
|
|
|
- this.xref = xref;
|
|
|
- this._type = 'Nums';
|
|
|
- }
|
|
|
+/**
|
|
|
+ * Page text content.
|
|
|
+ *
|
|
|
+ * @typedef {Object} TextContent
|
|
|
+ * @property {array} items - array of {@link TextItem}
|
|
|
+ * @property {Object} styles - {@link TextStyles} objects, indexed by font
|
|
|
+ * name.
|
|
|
+ */
|
|
|
|
|
|
- Util.inherit(NumberTree, NameOrNumberTree, {});
|
|
|
+/**
|
|
|
+ * Page text content part.
|
|
|
+ *
|
|
|
+ * @typedef {Object} TextItem
|
|
|
+ * @property {string} str - text content.
|
|
|
+ * @property {string} dir - text direction: 'ttb', 'ltr' or 'rtl'.
|
|
|
+ * @property {array} transform - transformation matrix.
|
|
|
+ * @property {number} width - width in device space.
|
|
|
+ * @property {number} height - height in device space.
|
|
|
+ * @property {string} fontName - font name used by pdf.js for converted font.
|
|
|
+ */
|
|
|
|
|
|
- return NumberTree;
|
|
|
-})();
|
|
|
+/**
|
|
|
+ * Text style.
|
|
|
+ *
|
|
|
+ * @typedef {Object} TextStyle
|
|
|
+ * @property {number} ascent - font ascent.
|
|
|
+ * @property {number} descent - font descent.
|
|
|
+ * @property {boolean} vertical - text is in vertical mode.
|
|
|
+ * @property {string} fontFamily - possible font family
|
|
|
+ */
|
|
|
|
|
|
/**
|
|
|
- * "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
|
|
|
- * collections attributes and related files (/RF)
|
|
|
+ * 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.
|
|
|
*/
|
|
|
-var FileSpec = (function FileSpecClosure() {
|
|
|
- function FileSpec(root, xref) {
|
|
|
- if (!root || !isDict(root)) {
|
|
|
- return;
|
|
|
- }
|
|
|
- this.xref = xref;
|
|
|
- this.root = root;
|
|
|
- if (root.has('FS')) {
|
|
|
- this.fs = root.get('FS');
|
|
|
- }
|
|
|
- this.description = root.has('Desc') ?
|
|
|
- stringToPDFString(root.get('Desc')) :
|
|
|
- '';
|
|
|
- if (root.has('RF')) {
|
|
|
- warn('Related file specifications are not supported');
|
|
|
- }
|
|
|
- this.contentAvailable = true;
|
|
|
- if (!root.has('EF')) {
|
|
|
- this.contentAvailable = false;
|
|
|
- warn('Non-embedded file specifications are not supported');
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- function pickPlatformItem(dict) {
|
|
|
- // Look for the filename in this order:
|
|
|
- // UF, F, Unix, Mac, DOS
|
|
|
- if (dict.has('UF')) {
|
|
|
- return dict.get('UF');
|
|
|
- } else if (dict.has('F')) {
|
|
|
- return dict.get('F');
|
|
|
- } else if (dict.has('Unix')) {
|
|
|
- return dict.get('Unix');
|
|
|
- } else if (dict.has('Mac')) {
|
|
|
- return dict.get('Mac');
|
|
|
- } else if (dict.has('DOS')) {
|
|
|
- return dict.get('DOS');
|
|
|
- } else {
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
- FileSpec.prototype = {
|
|
|
- get filename() {
|
|
|
- if (!this._filename && this.root) {
|
|
|
- var filename = pickPlatformItem(this.root) || 'unnamed';
|
|
|
- this._filename = stringToPDFString(filename).
|
|
|
- replace(/\\\\/g, '\\').
|
|
|
- replace(/\\\//g, '/').
|
|
|
- replace(/\\/g, '/');
|
|
|
- }
|
|
|
- return this._filename;
|
|
|
- },
|
|
|
- get content() {
|
|
|
- if (!this.contentAvailable) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- if (!this.contentRef && this.root) {
|
|
|
- this.contentRef = pickPlatformItem(this.root.get('EF'));
|
|
|
- }
|
|
|
- var content = null;
|
|
|
- if (this.contentRef) {
|
|
|
- var xref = this.xref;
|
|
|
- var fileObj = xref.fetchIfRef(this.contentRef);
|
|
|
- if (fileObj && isStream(fileObj)) {
|
|
|
- content = fileObj.getBytes();
|
|
|
- } else {
|
|
|
- warn('Embedded file specification points to non-existing/invalid ' +
|
|
|
- 'content');
|
|
|
- }
|
|
|
- } else {
|
|
|
- warn('Embedded file specification does not have a content');
|
|
|
- }
|
|
|
- return content;
|
|
|
- },
|
|
|
- get serializable() {
|
|
|
- return {
|
|
|
- filename: this.filename,
|
|
|
- content: this.content
|
|
|
- };
|
|
|
- }
|
|
|
- };
|
|
|
- return FileSpec;
|
|
|
-})();
|
|
|
+/**
|
|
|
+ * 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 - (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.
|
|
|
+ */
|
|
|
|
|
|
/**
|
|
|
- * A helper for loading missing data in object graphs. It traverses the graph
|
|
|
- * depth first and queues up any objects that have missing data. Once it has
|
|
|
- * has traversed as many objects that are available it attempts to bundle the
|
|
|
- * missing data requests and then resume from the nodes that weren't ready.
|
|
|
+ * PDF page operator list.
|
|
|
*
|
|
|
- * NOTE: It provides protection from circular references by keeping track of
|
|
|
- * of loaded references. However, you must be careful not to load any graphs
|
|
|
- * that have references to the catalog or other pages since that will cause the
|
|
|
- * entire PDF document object graph to be traversed.
|
|
|
+ * @typedef {Object} PDFOperatorList
|
|
|
+ * @property {Array} fnArray - Array containing the operator functions.
|
|
|
+ * @property {Array} argsArray - Array containing the arguments of the
|
|
|
+ * functions.
|
|
|
*/
|
|
|
-var ObjectLoader = (function() {
|
|
|
- function mayHaveChildren(value) {
|
|
|
- return isRef(value) || isDict(value) || isArray(value) || isStream(value);
|
|
|
- }
|
|
|
|
|
|
- function addChildren(node, nodesToVisit) {
|
|
|
- var value;
|
|
|
- if (isDict(node) || isStream(node)) {
|
|
|
- var map;
|
|
|
- if (isDict(node)) {
|
|
|
- map = node.map;
|
|
|
- } else {
|
|
|
- map = node.dict.map;
|
|
|
- }
|
|
|
- for (var key in map) {
|
|
|
- value = map[key];
|
|
|
- if (mayHaveChildren(value)) {
|
|
|
- nodesToVisit.push(value);
|
|
|
- }
|
|
|
+/**
|
|
|
+ * 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;
|
|
|
+ this.transport = transport;
|
|
|
+ this.stats = new StatTimer();
|
|
|
+ this.stats.enabled = !!globalScope.PDFJS.enableStats;
|
|
|
+ this.commonObjs = transport.commonObjs;
|
|
|
+ this.objs = new PDFObjects();
|
|
|
+ this.cleanupAfterRender = false;
|
|
|
+ this.pendingCleanup = false;
|
|
|
+ this.intentStates = Object.create(null);
|
|
|
+ this.destroyed = false;
|
|
|
+ }
|
|
|
+ PDFPageProxy.prototype = /** @lends PDFPageProxy.prototype */ {
|
|
|
+ /**
|
|
|
+ * @return {number} Page number of the page. First page is 1.
|
|
|
+ */
|
|
|
+ get pageNumber() {
|
|
|
+ return this.pageIndex + 1;
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * @return {number} The number of degrees the page is rotated clockwise.
|
|
|
+ */
|
|
|
+ get rotate() {
|
|
|
+ return this.pageInfo.rotate;
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * @return {Object} The reference that points to this page. It has 'num' and
|
|
|
+ * 'gen' properties.
|
|
|
+ */
|
|
|
+ get ref() {
|
|
|
+ return this.pageInfo.ref;
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * @return {Array} An array of the visible portion of the PDF page in the
|
|
|
+ * user space units - [x1, y1, x2, y2].
|
|
|
+ */
|
|
|
+ get view() {
|
|
|
+ return this.pageInfo.view;
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * @param {number} scale The desired scale of the viewport.
|
|
|
+ * @param {number} rotate Degrees to rotate the viewport. If omitted this
|
|
|
+ * defaults to the page rotation.
|
|
|
+ * @return {PDFJS.PageViewport} Contains 'width' and 'height' properties
|
|
|
+ * along with transforms required for rendering.
|
|
|
+ */
|
|
|
+ getViewport: function PDFPageProxy_getViewport(scale, rotate) {
|
|
|
+ if (arguments.length < 2) {
|
|
|
+ rotate = this.rotate;
|
|
|
}
|
|
|
- } else if (isArray(node)) {
|
|
|
- for (var i = 0, ii = node.length; i < ii; i++) {
|
|
|
- value = node[i];
|
|
|
- if (mayHaveChildren(value)) {
|
|
|
- nodesToVisit.push(value);
|
|
|
- }
|
|
|
+ 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(params) {
|
|
|
+ var intent = (params && params.intent) || null;
|
|
|
+
|
|
|
+ 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
|
|
|
+ * is resolved when the page finishes rendering.
|
|
|
+ */
|
|
|
+ render: function PDFPageProxy_render(params) {
|
|
|
+ var stats = this.stats;
|
|
|
+ stats.time('Overall');
|
|
|
|
|
|
- function ObjectLoader(obj, keys, xref) {
|
|
|
- this.obj = obj;
|
|
|
- this.keys = keys;
|
|
|
- this.xref = xref;
|
|
|
- this.refSet = null;
|
|
|
- this.capability = null;
|
|
|
- }
|
|
|
+ // If there was a pending destroy cancel it so no cleanup happens during
|
|
|
+ // this call to render.
|
|
|
+ this.pendingCleanup = false;
|
|
|
|
|
|
- ObjectLoader.prototype = {
|
|
|
- load: function ObjectLoader_load() {
|
|
|
- var keys = this.keys;
|
|
|
- this.capability = createPromiseCapability();
|
|
|
- // Don't walk the graph if all the data is already loaded.
|
|
|
- if (!(this.xref.stream instanceof ChunkedStream) ||
|
|
|
- this.xref.stream.getMissingChunks().length === 0) {
|
|
|
- this.capability.resolve();
|
|
|
- return this.capability.promise;
|
|
|
+ var renderingIntent = (params.intent === 'print' ? 'print' : 'display');
|
|
|
+
|
|
|
+ if (!this.intentStates[renderingIntent]) {
|
|
|
+ this.intentStates[renderingIntent] = Object.create(null);
|
|
|
}
|
|
|
+ var intentState = this.intentStates[renderingIntent];
|
|
|
|
|
|
- this.refSet = new RefSet();
|
|
|
- // Setup the initial nodes to visit.
|
|
|
- var nodesToVisit = [];
|
|
|
- for (var i = 0; i < keys.length; i++) {
|
|
|
- nodesToVisit.push(this.obj[keys[i]]);
|
|
|
+ // If there's no displayReadyCapability yet, then the operatorList
|
|
|
+ // was never requested before. Make the request and create the promise.
|
|
|
+ if (!intentState.displayReadyCapability) {
|
|
|
+ intentState.receivingOperatorList = true;
|
|
|
+ intentState.displayReadyCapability = createPromiseCapability();
|
|
|
+ intentState.operatorList = {
|
|
|
+ fnArray: [],
|
|
|
+ argsArray: [],
|
|
|
+ lastChunk: false
|
|
|
+ };
|
|
|
+
|
|
|
+ this.stats.time('Page Request');
|
|
|
+ this.transport.messageHandler.send('RenderPageRequest', {
|
|
|
+ pageIndex: this.pageNumber - 1,
|
|
|
+ intent: renderingIntent
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- this._walk(nodesToVisit);
|
|
|
- return this.capability.promise;
|
|
|
- },
|
|
|
+ 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 = internalRenderTask.task;
|
|
|
|
|
|
- _walk: function ObjectLoader_walk(nodesToVisit) {
|
|
|
- var nodesToRevisit = [];
|
|
|
- var pendingRequests = [];
|
|
|
- // DFS walk of the object graph.
|
|
|
- while (nodesToVisit.length) {
|
|
|
- var currentNode = nodesToVisit.pop();
|
|
|
+ // Obsolete parameter support
|
|
|
+ if (params.continueCallback) {
|
|
|
+ deprecated('render is used with continueCallback parameter');
|
|
|
+ renderTask.onContinue = params.continueCallback;
|
|
|
+ }
|
|
|
|
|
|
- // Only references or chunked streams can cause missing data exceptions.
|
|
|
- if (isRef(currentNode)) {
|
|
|
- // Skip nodes that have already been visited.
|
|
|
- if (this.refSet.has(currentNode)) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- try {
|
|
|
- var ref = currentNode;
|
|
|
- this.refSet.put(ref);
|
|
|
- currentNode = this.xref.fetch(currentNode);
|
|
|
- } catch (e) {
|
|
|
- if (!(e instanceof MissingDataException)) {
|
|
|
- throw e;
|
|
|
- }
|
|
|
- nodesToRevisit.push(currentNode);
|
|
|
- pendingRequests.push({ begin: e.begin, end: e.end });
|
|
|
+ var self = this;
|
|
|
+ intentState.displayReadyCapability.promise.then(
|
|
|
+ function pageDisplayReadyPromise(transparency) {
|
|
|
+ if (self.pendingCleanup) {
|
|
|
+ complete();
|
|
|
+ return;
|
|
|
}
|
|
|
+ stats.time('Rendering');
|
|
|
+ internalRenderTask.initalizeGraphics(transparency);
|
|
|
+ internalRenderTask.operatorListChanged();
|
|
|
+ },
|
|
|
+ function pageDisplayReadPromiseError(reason) {
|
|
|
+ complete(reason);
|
|
|
}
|
|
|
- if (currentNode && currentNode.getBaseStreams) {
|
|
|
- var baseStreams = currentNode.getBaseStreams();
|
|
|
- var foundMissingData = false;
|
|
|
- for (var i = 0; i < baseStreams.length; i++) {
|
|
|
- var stream = baseStreams[i];
|
|
|
- if (stream.getMissingChunks && stream.getMissingChunks().length) {
|
|
|
- foundMissingData = true;
|
|
|
- pendingRequests.push({
|
|
|
- begin: stream.start,
|
|
|
- end: stream.end
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- if (foundMissingData) {
|
|
|
- nodesToRevisit.push(currentNode);
|
|
|
- }
|
|
|
+ );
|
|
|
+
|
|
|
+ function complete(error) {
|
|
|
+ var i = intentState.renderTasks.indexOf(internalRenderTask);
|
|
|
+ if (i >= 0) {
|
|
|
+ intentState.renderTasks.splice(i, 1);
|
|
|
}
|
|
|
|
|
|
- addChildren(currentNode, nodesToVisit);
|
|
|
- }
|
|
|
+ if (self.cleanupAfterRender) {
|
|
|
+ self.pendingCleanup = true;
|
|
|
+ }
|
|
|
+ self._tryCleanup();
|
|
|
|
|
|
- if (pendingRequests.length) {
|
|
|
- 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.capability.reject);
|
|
|
- return;
|
|
|
+ if (error) {
|
|
|
+ internalRenderTask.capability.reject(error);
|
|
|
+ } else {
|
|
|
+ internalRenderTask.capability.resolve();
|
|
|
+ }
|
|
|
+ stats.timeEnd('Rendering');
|
|
|
+ stats.timeEnd('Overall');
|
|
|
}
|
|
|
- // Everything is loaded.
|
|
|
- this.refSet = null;
|
|
|
- this.capability.resolve();
|
|
|
- }
|
|
|
- };
|
|
|
|
|
|
- return ObjectLoader;
|
|
|
-})();
|
|
|
+ return renderTask;
|
|
|
+ },
|
|
|
|
|
|
-exports.Catalog = Catalog;
|
|
|
-exports.ObjectLoader = ObjectLoader;
|
|
|
-exports.XRef = XRef;
|
|
|
-}));
|
|
|
+ /**
|
|
|
+ * @return {Promise} A promise resolved with an {@link PDFOperatorList}
|
|
|
+ * object that represents page's operator list.
|
|
|
+ */
|
|
|
+ getOperatorList: function PDFPageProxy_getOperatorList() {
|
|
|
+ function operatorListChanged() {
|
|
|
+ if (intentState.operatorList.lastChunk) {
|
|
|
+ intentState.opListReadCapability.resolve(intentState.operatorList);
|
|
|
|
|
|
+ var i = intentState.renderTasks.indexOf(opListTask);
|
|
|
+ if (i >= 0) {
|
|
|
+ intentState.renderTasks.splice(i, 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
-(function (root, factory) {
|
|
|
- {
|
|
|
- factory((root.pdfjsCorePsParser = {}), root.pdfjsSharedUtil,
|
|
|
- root.pdfjsCoreParser);
|
|
|
- }
|
|
|
-}(this, function (exports, sharedUtil, coreParser) {
|
|
|
+ var renderingIntent = 'oplist';
|
|
|
+ if (!this.intentStates[renderingIntent]) {
|
|
|
+ this.intentStates[renderingIntent] = Object.create(null);
|
|
|
+ }
|
|
|
+ var intentState = this.intentStates[renderingIntent];
|
|
|
+ var opListTask;
|
|
|
|
|
|
-var error = sharedUtil.error;
|
|
|
-var EOF = coreParser.EOF;
|
|
|
-var Lexer = coreParser.Lexer;
|
|
|
+ if (!intentState.opListReadCapability) {
|
|
|
+ opListTask = {};
|
|
|
+ opListTask.operatorListChanged = operatorListChanged;
|
|
|
+ intentState.receivingOperatorList = true;
|
|
|
+ intentState.opListReadCapability = createPromiseCapability();
|
|
|
+ intentState.renderTasks = [];
|
|
|
+ intentState.renderTasks.push(opListTask);
|
|
|
+ intentState.operatorList = {
|
|
|
+ fnArray: [],
|
|
|
+ argsArray: [],
|
|
|
+ lastChunk: false
|
|
|
+ };
|
|
|
|
|
|
-var PostScriptParser = (function PostScriptParserClosure() {
|
|
|
- function PostScriptParser(lexer) {
|
|
|
- this.lexer = lexer;
|
|
|
- this.operators = [];
|
|
|
- this.token = null;
|
|
|
- this.prev = null;
|
|
|
- }
|
|
|
- PostScriptParser.prototype = {
|
|
|
- nextToken: function PostScriptParser_nextToken() {
|
|
|
- this.prev = this.token;
|
|
|
- this.token = this.lexer.getToken();
|
|
|
- },
|
|
|
- accept: function PostScriptParser_accept(type) {
|
|
|
- if (this.token.type === type) {
|
|
|
- this.nextToken();
|
|
|
- return true;
|
|
|
- }
|
|
|
- return false;
|
|
|
- },
|
|
|
- expect: function PostScriptParser_expect(type) {
|
|
|
- if (this.accept(type)) {
|
|
|
- return true;
|
|
|
+ this.transport.messageHandler.send('RenderPageRequest', {
|
|
|
+ pageIndex: this.pageIndex,
|
|
|
+ intent: renderingIntent
|
|
|
+ });
|
|
|
}
|
|
|
- error('Unexpected symbol: found ' + this.token.type + ' expected ' +
|
|
|
- type + '.');
|
|
|
+ return intentState.opListReadCapability.promise;
|
|
|
},
|
|
|
- parse: function PostScriptParser_parse() {
|
|
|
- this.nextToken();
|
|
|
- this.expect(PostScriptTokenTypes.LBRACE);
|
|
|
- this.parseBlock();
|
|
|
- this.expect(PostScriptTokenTypes.RBRACE);
|
|
|
- return this.operators;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param {getTextContentParameters} params - getTextContent parameters.
|
|
|
+ * @return {Promise} That is resolved a {@link TextContent}
|
|
|
+ * object that represent the page text content.
|
|
|
+ */
|
|
|
+ getTextContent: function PDFPageProxy_getTextContent(params) {
|
|
|
+ var normalizeWhitespace = (params && params.normalizeWhitespace) || false;
|
|
|
+
|
|
|
+ return this.transport.messageHandler.sendWithPromise('GetTextContent', {
|
|
|
+ pageIndex: this.pageNumber - 1,
|
|
|
+ normalizeWhitespace: normalizeWhitespace,
|
|
|
+ });
|
|
|
},
|
|
|
- parseBlock: function PostScriptParser_parseBlock() {
|
|
|
- while (true) {
|
|
|
- if (this.accept(PostScriptTokenTypes.NUMBER)) {
|
|
|
- this.operators.push(this.prev.value);
|
|
|
- } else if (this.accept(PostScriptTokenTypes.OPERATOR)) {
|
|
|
- this.operators.push(this.prev.value);
|
|
|
- } else if (this.accept(PostScriptTokenTypes.LBRACE)) {
|
|
|
- this.parseCondition();
|
|
|
- } else {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Destroys page object.
|
|
|
+ */
|
|
|
+ _destroy: function PDFPageProxy_destroy() {
|
|
|
+ this.destroyed = true;
|
|
|
+ this.transport.pageCache[this.pageIndex] = null;
|
|
|
+
|
|
|
+ var waitOn = [];
|
|
|
+ Object.keys(this.intentStates).forEach(function(intent) {
|
|
|
+ if (intent === 'oplist') {
|
|
|
+ // Avoid errors below, since the renderTasks are just stubs.
|
|
|
return;
|
|
|
}
|
|
|
- }
|
|
|
+ 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);
|
|
|
},
|
|
|
- parseCondition: function PostScriptParser_parseCondition() {
|
|
|
- // Add two place holders that will be updated later
|
|
|
- var conditionLocation = this.operators.length;
|
|
|
- this.operators.push(null, null);
|
|
|
|
|
|
- this.parseBlock();
|
|
|
- this.expect(PostScriptTokenTypes.RBRACE);
|
|
|
- if (this.accept(PostScriptTokenTypes.IF)) {
|
|
|
- // The true block is right after the 'if' so it just falls through on
|
|
|
- // true else it jumps and skips the true block.
|
|
|
- this.operators[conditionLocation] = this.operators.length;
|
|
|
- this.operators[conditionLocation + 1] = 'jz';
|
|
|
- } else if (this.accept(PostScriptTokenTypes.LBRACE)) {
|
|
|
- var jumpLocation = this.operators.length;
|
|
|
- this.operators.push(null, null);
|
|
|
- var endOfTrue = this.operators.length;
|
|
|
- this.parseBlock();
|
|
|
- this.expect(PostScriptTokenTypes.RBRACE);
|
|
|
- this.expect(PostScriptTokenTypes.IFELSE);
|
|
|
- // The jump is added at the end of the true block to skip the false
|
|
|
- // block.
|
|
|
- this.operators[jumpLocation] = this.operators.length;
|
|
|
- this.operators[jumpLocation + 1] = 'j';
|
|
|
+ /**
|
|
|
+ * Cleans up resources allocated by the page. (deprecated)
|
|
|
+ */
|
|
|
+ destroy: function() {
|
|
|
+ deprecated('page destroy method, use cleanup() instead');
|
|
|
+ this.cleanup();
|
|
|
+ },
|
|
|
|
|
|
- this.operators[conditionLocation] = endOfTrue;
|
|
|
- this.operators[conditionLocation + 1] = 'jz';
|
|
|
- } else {
|
|
|
- error('PS Function: error parsing conditional.');
|
|
|
+ /**
|
|
|
+ * 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
|
|
|
+ */
|
|
|
+ _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)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Object.keys(this.intentStates).forEach(function(intent) {
|
|
|
+ delete this.intentStates[intent];
|
|
|
+ }, this);
|
|
|
+ this.objs.clear();
|
|
|
+ this.annotationsPromise = null;
|
|
|
+ this.pendingCleanup = false;
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * For internal use only.
|
|
|
+ * @ignore
|
|
|
+ */
|
|
|
+ _startRenderPage: function PDFPageProxy_startRenderPage(transparency,
|
|
|
+ intent) {
|
|
|
+ var intentState = this.intentStates[intent];
|
|
|
+ // TODO Refactor RenderPageRequest to separate rendering
|
|
|
+ // and operator list logic
|
|
|
+ if (intentState.displayReadyCapability) {
|
|
|
+ intentState.displayReadyCapability.resolve(transparency);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * For internal use only.
|
|
|
+ * @ignore
|
|
|
+ */
|
|
|
+ _renderPageChunk: function PDFPageProxy_renderPageChunk(operatorListChunk,
|
|
|
+ intent) {
|
|
|
+ var intentState = this.intentStates[intent];
|
|
|
+ var i, ii;
|
|
|
+ // Add the new chunk to the current operator list.
|
|
|
+ for (i = 0, ii = operatorListChunk.length; i < ii; i++) {
|
|
|
+ intentState.operatorList.fnArray.push(operatorListChunk.fnArray[i]);
|
|
|
+ intentState.operatorList.argsArray.push(
|
|
|
+ operatorListChunk.argsArray[i]);
|
|
|
+ }
|
|
|
+ intentState.operatorList.lastChunk = operatorListChunk.lastChunk;
|
|
|
+
|
|
|
+ // Notify all the rendering tasks there are more operators to be consumed.
|
|
|
+ for (i = 0; i < intentState.renderTasks.length; i++) {
|
|
|
+ intentState.renderTasks[i].operatorListChanged();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (operatorListChunk.lastChunk) {
|
|
|
+ intentState.receivingOperatorList = false;
|
|
|
+ this._tryCleanup();
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
- return PostScriptParser;
|
|
|
+ return PDFPageProxy;
|
|
|
})();
|
|
|
|
|
|
-var PostScriptTokenTypes = {
|
|
|
- LBRACE: 0,
|
|
|
- RBRACE: 1,
|
|
|
- NUMBER: 2,
|
|
|
- OPERATOR: 3,
|
|
|
- IF: 4,
|
|
|
- IFELSE: 5
|
|
|
-};
|
|
|
+/**
|
|
|
+ * 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;
|
|
|
|
|
|
-var PostScriptToken = (function PostScriptTokenClosure() {
|
|
|
- function PostScriptToken(type, value) {
|
|
|
- this.type = type;
|
|
|
- this.value = value;
|
|
|
+ function getWorkerSrc() {
|
|
|
+ if (PDFJS.workerSrc) {
|
|
|
+ return PDFJS.workerSrc;
|
|
|
+ }
|
|
|
+ if (pdfjsFilePath) {
|
|
|
+ return pdfjsFilePath.replace(/\.js$/i, '.worker.js');
|
|
|
+ }
|
|
|
+ error('No PDFJS.workerSrc specified');
|
|
|
}
|
|
|
|
|
|
- var opCache = Object.create(null);
|
|
|
-
|
|
|
- PostScriptToken.getOperator = function PostScriptToken_getOperator(op) {
|
|
|
- var opValue = opCache[op];
|
|
|
- if (opValue) {
|
|
|
- return opValue;
|
|
|
+ // 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 opCache[op] = new PostScriptToken(PostScriptTokenTypes.OPERATOR, op);
|
|
|
- };
|
|
|
+ return PDFJS.fakeWorkerFilesLoadedCapability.promise;
|
|
|
+ }
|
|
|
|
|
|
- PostScriptToken.LBRACE = new PostScriptToken(PostScriptTokenTypes.LBRACE,
|
|
|
- '{');
|
|
|
- PostScriptToken.RBRACE = new PostScriptToken(PostScriptTokenTypes.RBRACE,
|
|
|
- '}');
|
|
|
- PostScriptToken.IF = new PostScriptToken(PostScriptTokenTypes.IF, 'IF');
|
|
|
- PostScriptToken.IFELSE = new PostScriptToken(PostScriptTokenTypes.IFELSE,
|
|
|
- 'IFELSE');
|
|
|
- return PostScriptToken;
|
|
|
-})();
|
|
|
+ function createCDNWrapper(url) {
|
|
|
+ // We will rely on blob URL's property to specify origin.
|
|
|
+ // We want this function to fail in case if createObjectURL or Blob do not
|
|
|
+ // exist or fail for some reason -- our Worker creation will fail anyway.
|
|
|
+ var wrapper = 'importScripts(\'' + url + '\');';
|
|
|
+ return URL.createObjectURL(new Blob([wrapper]));
|
|
|
+ }
|
|
|
|
|
|
-var PostScriptLexer = (function PostScriptLexerClosure() {
|
|
|
- function PostScriptLexer(stream) {
|
|
|
- this.stream = stream;
|
|
|
- this.nextChar();
|
|
|
+ function PDFWorker(name) {
|
|
|
+ this.name = name;
|
|
|
+ this.destroyed = false;
|
|
|
|
|
|
- this.strBuf = [];
|
|
|
+ this._readyCapability = createPromiseCapability();
|
|
|
+ this._port = null;
|
|
|
+ this._webWorker = null;
|
|
|
+ this._messageHandler = null;
|
|
|
+ this._initialize();
|
|
|
}
|
|
|
- PostScriptLexer.prototype = {
|
|
|
- nextChar: function PostScriptLexer_nextChar() {
|
|
|
- return (this.currentChar = this.stream.getByte());
|
|
|
+
|
|
|
+ PDFWorker.prototype = /** @lends PDFWorker.prototype */ {
|
|
|
+ get promise() {
|
|
|
+ return this._readyCapability.promise;
|
|
|
},
|
|
|
- getToken: function PostScriptLexer_getToken() {
|
|
|
- var comment = false;
|
|
|
- var ch = this.currentChar;
|
|
|
|
|
|
- // skip comments
|
|
|
- while (true) {
|
|
|
- if (ch < 0) {
|
|
|
- return EOF;
|
|
|
- }
|
|
|
+ get port() {
|
|
|
+ return this._port;
|
|
|
+ },
|
|
|
|
|
|
- if (comment) {
|
|
|
- if (ch === 0x0A || ch === 0x0D) {
|
|
|
- comment = false;
|
|
|
- }
|
|
|
- } else if (ch === 0x25) { // '%'
|
|
|
- comment = true;
|
|
|
- } else if (!Lexer.isSpace(ch)) {
|
|
|
- break;
|
|
|
- }
|
|
|
- ch = this.nextChar();
|
|
|
- }
|
|
|
- switch (ch | 0) {
|
|
|
- case 0x30: case 0x31: case 0x32: case 0x33: case 0x34: // '0'-'4'
|
|
|
- case 0x35: case 0x36: case 0x37: case 0x38: case 0x39: // '5'-'9'
|
|
|
- case 0x2B: case 0x2D: case 0x2E: // '+', '-', '.'
|
|
|
- return new PostScriptToken(PostScriptTokenTypes.NUMBER,
|
|
|
- this.getNumber());
|
|
|
- case 0x7B: // '{'
|
|
|
- this.nextChar();
|
|
|
- return PostScriptToken.LBRACE;
|
|
|
- case 0x7D: // '}'
|
|
|
- this.nextChar();
|
|
|
- return PostScriptToken.RBRACE;
|
|
|
- }
|
|
|
- // operator
|
|
|
- var strBuf = this.strBuf;
|
|
|
- strBuf.length = 0;
|
|
|
- strBuf[0] = String.fromCharCode(ch);
|
|
|
+ get messageHandler() {
|
|
|
+ return this._messageHandler;
|
|
|
+ },
|
|
|
|
|
|
- while ((ch = this.nextChar()) >= 0 && // and 'A'-'Z', 'a'-'z'
|
|
|
- ((ch >= 0x41 && ch <= 0x5A) || (ch >= 0x61 && ch <= 0x7A))) {
|
|
|
- strBuf.push(String.fromCharCode(ch));
|
|
|
- }
|
|
|
- var str = strBuf.join('');
|
|
|
- switch (str.toLowerCase()) {
|
|
|
- case 'if':
|
|
|
- return PostScriptToken.IF;
|
|
|
- case 'ifelse':
|
|
|
- return PostScriptToken.IFELSE;
|
|
|
- default:
|
|
|
- return PostScriptToken.getOperator(str);
|
|
|
- }
|
|
|
+ _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 fulfills
|
|
|
+ // 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();
|
|
|
},
|
|
|
- getNumber: function PostScriptLexer_getNumber() {
|
|
|
- var ch = this.currentChar;
|
|
|
- var strBuf = this.strBuf;
|
|
|
- strBuf.length = 0;
|
|
|
- strBuf[0] = String.fromCharCode(ch);
|
|
|
|
|
|
- while ((ch = this.nextChar()) >= 0) {
|
|
|
- if ((ch >= 0x30 && ch <= 0x39) || // '0'-'9'
|
|
|
- ch === 0x2D || ch === 0x2E) { // '-', '.'
|
|
|
- strBuf.push(String.fromCharCode(ch));
|
|
|
- } else {
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- var value = parseFloat(strBuf.join(''));
|
|
|
- if (isNaN(value)) {
|
|
|
- error('Invalid floating point number: ' + value);
|
|
|
+ _setupFakeWorker: function PDFWorker_setupFakeWorker() {
|
|
|
+ if (!globalScope.PDFJS.disableWorker) {
|
|
|
+ warn('Setting up fake worker.');
|
|
|
+ globalScope.PDFJS.disableWorker = true;
|
|
|
}
|
|
|
- return value;
|
|
|
- }
|
|
|
- };
|
|
|
- return PostScriptLexer;
|
|
|
-})();
|
|
|
|
|
|
-exports.PostScriptLexer = PostScriptLexer;
|
|
|
-exports.PostScriptParser = PostScriptParser;
|
|
|
-}));
|
|
|
+ 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;
|
|
|
|
|
|
-(function (root, factory) {
|
|
|
- {
|
|
|
- factory((root.pdfjsDisplayAPI = {}), root.pdfjsSharedUtil,
|
|
|
- root.pdfjsDisplayFontLoader, root.pdfjsDisplayCanvas,
|
|
|
- root.pdfjsDisplayMetadata, root.pdfjsSharedGlobal);
|
|
|
- }
|
|
|
-}(this, function (exports, sharedUtil, displayFontLoader, displayCanvas,
|
|
|
- displayMetadata, sharedGlobal, amdRequire) {
|
|
|
+ // All fake workers use the same port, making id unique.
|
|
|
+ var id = 'fake' + (nextFakeWorkerId++);
|
|
|
|
|
|
-var InvalidPDFException = sharedUtil.InvalidPDFException;
|
|
|
-var MessageHandler = sharedUtil.MessageHandler;
|
|
|
-var MissingPDFException = sharedUtil.MissingPDFException;
|
|
|
-var PasswordResponses = sharedUtil.PasswordResponses;
|
|
|
-var PasswordException = sharedUtil.PasswordException;
|
|
|
-var StatTimer = sharedUtil.StatTimer;
|
|
|
-var UnexpectedResponseException = sharedUtil.UnexpectedResponseException;
|
|
|
-var UnknownErrorException = sharedUtil.UnknownErrorException;
|
|
|
-var Util = sharedUtil.Util;
|
|
|
-var createPromiseCapability = sharedUtil.createPromiseCapability;
|
|
|
-var combineUrl = sharedUtil.combineUrl;
|
|
|
-var error = sharedUtil.error;
|
|
|
-var deprecated = sharedUtil.deprecated;
|
|
|
-var info = sharedUtil.info;
|
|
|
-var isArrayBuffer = sharedUtil.isArrayBuffer;
|
|
|
-var isSameOrigin = sharedUtil.isSameOrigin;
|
|
|
-var loadJpegStream = sharedUtil.loadJpegStream;
|
|
|
-var stringToBytes = sharedUtil.stringToBytes;
|
|
|
-var warn = sharedUtil.warn;
|
|
|
-var FontFaceObject = displayFontLoader.FontFaceObject;
|
|
|
-var FontLoader = displayFontLoader.FontLoader;
|
|
|
-var CanvasGraphics = displayCanvas.CanvasGraphics;
|
|
|
-var createScratchCanvas = displayCanvas.createScratchCanvas;
|
|
|
-var Metadata = displayMetadata.Metadata;
|
|
|
-var PDFJS = sharedGlobal.PDFJS;
|
|
|
-var globalScope = sharedGlobal.globalScope;
|
|
|
+ // 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 DEFAULT_RANGE_CHUNK_SIZE = 65536; // 2^16 = 65536
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
-/**
|
|
|
- * 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}
|
|
|
- */
|
|
|
-PDFJS.maxImageSize = (PDFJS.maxImageSize === undefined ?
|
|
|
- -1 : PDFJS.maxImageSize);
|
|
|
+ return PDFWorker;
|
|
|
+})();
|
|
|
+PDFJS.PDFWorker = PDFWorker;
|
|
|
|
|
|
/**
|
|
|
- * The url of where the predefined Adobe CMaps are located. Include trailing
|
|
|
- * slash.
|
|
|
- * @var {string}
|
|
|
+ * For internal use only.
|
|
|
+ * @ignore
|
|
|
*/
|
|
|
-PDFJS.cMapUrl = (PDFJS.cMapUrl === undefined ? null : PDFJS.cMapUrl);
|
|
|
+var WorkerTransport = (function WorkerTransportClosure() {
|
|
|
+ function WorkerTransport(messageHandler, loadingTask, pdfDataRangeTransport) {
|
|
|
+ this.messageHandler = messageHandler;
|
|
|
+ this.loadingTask = loadingTask;
|
|
|
+ this.pdfDataRangeTransport = pdfDataRangeTransport;
|
|
|
+ this.commonObjs = new PDFObjects();
|
|
|
+ this.fontLoader = new FontLoader(loadingTask.docId);
|
|
|
|
|
|
-/**
|
|
|
- * Specifies if CMaps are binary packed.
|
|
|
- * @var {boolean}
|
|
|
- */
|
|
|
-PDFJS.cMapPacked = PDFJS.cMapPacked === undefined ? false : PDFJS.cMapPacked;
|
|
|
+ this.destroyed = false;
|
|
|
+ this.destroyCapability = null;
|
|
|
|
|
|
-/**
|
|
|
- * 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}
|
|
|
- */
|
|
|
-PDFJS.disableFontFace = (PDFJS.disableFontFace === undefined ?
|
|
|
- false : PDFJS.disableFontFace);
|
|
|
+ this.pageCache = [];
|
|
|
+ this.pagePromises = [];
|
|
|
+ this.downloadInfoCapability = createPromiseCapability();
|
|
|
|
|
|
-/**
|
|
|
- * Path for image resources, mainly for annotation icons. Include trailing
|
|
|
- * slash.
|
|
|
- * @var {string}
|
|
|
- */
|
|
|
-PDFJS.imageResourcesPath = (PDFJS.imageResourcesPath === undefined ?
|
|
|
- '' : PDFJS.imageResourcesPath);
|
|
|
+ this.setupMessageHandler();
|
|
|
+ }
|
|
|
+ WorkerTransport.prototype = {
|
|
|
+ destroy: function WorkerTransport_destroy() {
|
|
|
+ if (this.destroyCapability) {
|
|
|
+ return this.destroyCapability.promise;
|
|
|
+ }
|
|
|
|
|
|
-/**
|
|
|
- * Disable the web worker and run all code on the main thread. This will happen
|
|
|
- * automatically if the browser doesn't support workers or sending typed arrays
|
|
|
- * to workers.
|
|
|
- * @var {boolean}
|
|
|
- */
|
|
|
-PDFJS.disableWorker = (PDFJS.disableWorker === undefined ?
|
|
|
- false : PDFJS.disableWorker);
|
|
|
+ this.destroyed = true;
|
|
|
+ this.destroyCapability = createPromiseCapability();
|
|
|
|
|
|
-/**
|
|
|
- * 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. 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);
|
|
|
+ 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;
|
|
|
+ // 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;
|
|
|
+ },
|
|
|
|
|
|
-/**
|
|
|
- * Disable range request loading of PDF files. When enabled and if the server
|
|
|
- * supports partial content requests then the PDF will be fetched in chunks.
|
|
|
- * Enabled (false) by default.
|
|
|
- * @var {boolean}
|
|
|
- */
|
|
|
-PDFJS.disableRange = (PDFJS.disableRange === undefined ?
|
|
|
- false : PDFJS.disableRange);
|
|
|
+ setupMessageHandler:
|
|
|
+ function WorkerTransport_setupMessageHandler() {
|
|
|
+ var messageHandler = this.messageHandler;
|
|
|
|
|
|
-/**
|
|
|
- * Disable streaming of PDF file data. By default PDF.js attempts to load PDF
|
|
|
- * in chunks. This default behavior can be disabled.
|
|
|
- * @var {boolean}
|
|
|
- */
|
|
|
-PDFJS.disableStream = (PDFJS.disableStream === undefined ?
|
|
|
- false : PDFJS.disableStream);
|
|
|
+ function updatePassword(password) {
|
|
|
+ messageHandler.send('UpdatePassword', password);
|
|
|
+ }
|
|
|
|
|
|
-/**
|
|
|
- * 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);
|
|
|
+ var pdfDataRangeTransport = this.pdfDataRangeTransport;
|
|
|
+ if (pdfDataRangeTransport) {
|
|
|
+ pdfDataRangeTransport.addRangeListener(function(begin, chunk) {
|
|
|
+ messageHandler.send('OnDataRange', {
|
|
|
+ begin: begin,
|
|
|
+ chunk: chunk
|
|
|
+ });
|
|
|
+ });
|
|
|
|
|
|
-/**
|
|
|
- * Enables special hooks for debugging PDF.js.
|
|
|
- * @var {boolean}
|
|
|
- */
|
|
|
-PDFJS.pdfBug = (PDFJS.pdfBug === undefined ? false : PDFJS.pdfBug);
|
|
|
+ pdfDataRangeTransport.addProgressListener(function(loaded) {
|
|
|
+ messageHandler.send('OnDataProgress', {
|
|
|
+ loaded: loaded
|
|
|
+ });
|
|
|
+ });
|
|
|
|
|
|
-/**
|
|
|
- * Enables transfer usage in postMessage for ArrayBuffers.
|
|
|
- * @var {boolean}
|
|
|
- */
|
|
|
-PDFJS.postMessageTransfers = (PDFJS.postMessageTransfers === undefined ?
|
|
|
- true : PDFJS.postMessageTransfers);
|
|
|
+ pdfDataRangeTransport.addProgressiveReadListener(function(chunk) {
|
|
|
+ messageHandler.send('OnDataRange', {
|
|
|
+ chunk: chunk
|
|
|
+ });
|
|
|
+ });
|
|
|
|
|
|
-/**
|
|
|
- * Disables URL.createObjectURL usage.
|
|
|
- * @var {boolean}
|
|
|
- */
|
|
|
-PDFJS.disableCreateObjectURL = (PDFJS.disableCreateObjectURL === undefined ?
|
|
|
- false : PDFJS.disableCreateObjectURL);
|
|
|
-
|
|
|
-/**
|
|
|
- * Disables WebGL usage.
|
|
|
- * @var {boolean}
|
|
|
- */
|
|
|
-PDFJS.disableWebGL = (PDFJS.disableWebGL === undefined ?
|
|
|
- true : PDFJS.disableWebGL);
|
|
|
+ messageHandler.on('RequestDataRange',
|
|
|
+ function transportDataRange(data) {
|
|
|
+ pdfDataRangeTransport.requestDataRange(data.begin, data.end);
|
|
|
+ }, this);
|
|
|
+ }
|
|
|
|
|
|
-/**
|
|
|
- * 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);
|
|
|
+ messageHandler.on('GetDoc', function transportDoc(data) {
|
|
|
+ var pdfInfo = data.pdfInfo;
|
|
|
+ this.numPages = data.pdfInfo.numPages;
|
|
|
+ var loadingTask = this.loadingTask;
|
|
|
+ var pdfDocument = new PDFDocumentProxy(pdfInfo, this, loadingTask);
|
|
|
+ this.pdfDocument = pdfDocument;
|
|
|
+ loadingTask._capability.resolve(pdfDocument);
|
|
|
+ }, this);
|
|
|
|
|
|
-/**
|
|
|
- * Enables CSS only zooming.
|
|
|
- * @var {boolean}
|
|
|
- */
|
|
|
-PDFJS.useOnlyCssZoom = (PDFJS.useOnlyCssZoom === undefined ?
|
|
|
- false : PDFJS.useOnlyCssZoom);
|
|
|
+ messageHandler.on('NeedPassword',
|
|
|
+ function transportNeedPassword(exception) {
|
|
|
+ var loadingTask = this.loadingTask;
|
|
|
+ if (loadingTask.onPassword) {
|
|
|
+ return loadingTask.onPassword(updatePassword,
|
|
|
+ PasswordResponses.NEED_PASSWORD);
|
|
|
+ }
|
|
|
+ loadingTask._capability.reject(
|
|
|
+ new PasswordException(exception.message, exception.code));
|
|
|
+ }, this);
|
|
|
|
|
|
-/**
|
|
|
- * Controls the logging level.
|
|
|
- * The constants from PDFJS.VERBOSITY_LEVELS should be used:
|
|
|
- * - errors
|
|
|
- * - warnings [default]
|
|
|
- * - infos
|
|
|
- * @var {number}
|
|
|
- */
|
|
|
-PDFJS.verbosity = (PDFJS.verbosity === undefined ?
|
|
|
- PDFJS.VERBOSITY_LEVELS.warnings : PDFJS.verbosity);
|
|
|
+ messageHandler.on('IncorrectPassword',
|
|
|
+ function transportIncorrectPassword(exception) {
|
|
|
+ var loadingTask = this.loadingTask;
|
|
|
+ if (loadingTask.onPassword) {
|
|
|
+ return loadingTask.onPassword(updatePassword,
|
|
|
+ PasswordResponses.INCORRECT_PASSWORD);
|
|
|
+ }
|
|
|
+ loadingTask._capability.reject(
|
|
|
+ new PasswordException(exception.message, exception.code));
|
|
|
+ }, this);
|
|
|
|
|
|
-/**
|
|
|
- * 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);
|
|
|
+ messageHandler.on('InvalidPDF', function transportInvalidPDF(exception) {
|
|
|
+ this.loadingTask._capability.reject(
|
|
|
+ new InvalidPDFException(exception.message));
|
|
|
+ }, this);
|
|
|
|
|
|
-/**
|
|
|
- * (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);
|
|
|
+ messageHandler.on('MissingPDF', function transportMissingPDF(exception) {
|
|
|
+ this.loadingTask._capability.reject(
|
|
|
+ new MissingPDFException(exception.message));
|
|
|
+ }, this);
|
|
|
|
|
|
-/**
|
|
|
- * 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);
|
|
|
+ messageHandler.on('UnexpectedResponse',
|
|
|
+ function transportUnexpectedResponse(exception) {
|
|
|
+ this.loadingTask._capability.reject(
|
|
|
+ new UnexpectedResponseException(exception.message, exception.status));
|
|
|
+ }, this);
|
|
|
|
|
|
-/**
|
|
|
- * Specifies the |rel| attribute for external links. Defaults to stripping
|
|
|
- * the referrer.
|
|
|
- * @var {string}
|
|
|
- */
|
|
|
-PDFJS.externalLinkRel = (PDFJS.externalLinkRel === undefined ?
|
|
|
- 'noreferrer' : PDFJS.externalLinkRel);
|
|
|
+ messageHandler.on('UnknownError',
|
|
|
+ function transportUnknownError(exception) {
|
|
|
+ this.loadingTask._capability.reject(
|
|
|
+ new UnknownErrorException(exception.message, exception.details));
|
|
|
+ }, this);
|
|
|
|
|
|
-/**
|
|
|
- * 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);
|
|
|
+ messageHandler.on('DataLoaded', function transportPage(data) {
|
|
|
+ this.downloadInfoCapability.resolve(data);
|
|
|
+ }, this);
|
|
|
|
|
|
-/**
|
|
|
- * Document initialization / loading parameters object.
|
|
|
- *
|
|
|
- * @typedef {Object} DocumentInitParameters
|
|
|
- * @property {string} url - The URL of the PDF.
|
|
|
- * @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.
|
|
|
- */
|
|
|
+ messageHandler.on('PDFManagerReady', function transportPage(data) {
|
|
|
+ if (this.pdfDataRangeTransport) {
|
|
|
+ this.pdfDataRangeTransport.transportReady();
|
|
|
+ }
|
|
|
+ }, this);
|
|
|
|
|
|
-/**
|
|
|
- * @typedef {Object} PDFDocumentStats
|
|
|
- * @property {Array} streamTypes - Used stream types in the document (an item
|
|
|
- * is set to true if specific stream ID was used in the document).
|
|
|
- * @property {Array} fontTypes - Used font type in the document (an item is set
|
|
|
- * to true if specific font ID was used in the document).
|
|
|
- */
|
|
|
+ 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];
|
|
|
|
|
|
-/**
|
|
|
- * 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|PDFDataRangeTransport} src
|
|
|
- * Can be a url to where a PDF is located, a typed array (Uint8Array)
|
|
|
- * already populated with data or parameter object.
|
|
|
- *
|
|
|
- * @param {PDFDataRangeTransport} pdfDataRangeTransport (deprecated) It is used
|
|
|
- * if you want to manually serve range requests for data in the PDF.
|
|
|
- *
|
|
|
- * @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 (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 {PDFDocumentLoadingTask}
|
|
|
- */
|
|
|
-PDFJS.getDocument = function getDocument(src,
|
|
|
- pdfDataRangeTransport,
|
|
|
- passwordCallback,
|
|
|
- progressCallback) {
|
|
|
- var task = new PDFDocumentLoadingTask();
|
|
|
+ page.stats.timeEnd('Page Request');
|
|
|
+ page._startRenderPage(data.transparency, data.intent);
|
|
|
+ }, this);
|
|
|
|
|
|
- // 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;
|
|
|
+ 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];
|
|
|
|
|
|
- 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');
|
|
|
- }
|
|
|
+ page._renderPageChunk(data.operatorList, data.intent);
|
|
|
+ }, this);
|
|
|
|
|
|
- source = src;
|
|
|
- }
|
|
|
+ messageHandler.on('commonobj', function transportObj(data) {
|
|
|
+ if (this.destroyed) {
|
|
|
+ return; // Ignore any pending requests if the worker was terminated.
|
|
|
+ }
|
|
|
|
|
|
- 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];
|
|
|
- }
|
|
|
+ var id = data[0];
|
|
|
+ var type = data[1];
|
|
|
+ if (this.commonObjs.hasData(id)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- params.rangeChunkSize = params.rangeChunkSize || DEFAULT_RANGE_CHUNK_SIZE;
|
|
|
+ switch (type) {
|
|
|
+ case 'Font':
|
|
|
+ var exportedData = data[2];
|
|
|
|
|
|
- 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);
|
|
|
- var transport = new WorkerTransport(messageHandler, task, rangeTransport);
|
|
|
- task._transport = transport;
|
|
|
- messageHandler.send('Ready', null);
|
|
|
- });
|
|
|
- }).catch(task._capability.reject);
|
|
|
+ var font;
|
|
|
+ if ('error' in exportedData) {
|
|
|
+ var error = exportedData.error;
|
|
|
+ warn('Error during font loading: ' + error);
|
|
|
+ this.commonObjs.resolve(id, error);
|
|
|
+ break;
|
|
|
+ } else {
|
|
|
+ font = new FontFaceObject(exportedData);
|
|
|
+ }
|
|
|
|
|
|
- return task;
|
|
|
-};
|
|
|
+ this.fontLoader.bind(
|
|
|
+ [font],
|
|
|
+ function fontReady(fontObjs) {
|
|
|
+ this.commonObjs.resolve(id, font);
|
|
|
+ }.bind(this)
|
|
|
+ );
|
|
|
+ break;
|
|
|
+ case 'FontPath':
|
|
|
+ this.commonObjs.resolve(id, data[2]);
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ error('Got unknown common object type ' + type);
|
|
|
+ }
|
|
|
+ }, this);
|
|
|
|
|
|
-/**
|
|
|
- * 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'));
|
|
|
- }
|
|
|
+ messageHandler.on('obj', function transportObj(data) {
|
|
|
+ if (this.destroyed) {
|
|
|
+ return; // Ignore any pending requests if the worker was terminated.
|
|
|
+ }
|
|
|
|
|
|
- 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;
|
|
|
- });
|
|
|
-}
|
|
|
+ var id = data[0];
|
|
|
+ var pageIndex = data[1];
|
|
|
+ var type = data[2];
|
|
|
+ var pageProxy = this.pageCache[pageIndex];
|
|
|
+ var imageData;
|
|
|
+ if (pageProxy.objs.hasData(id)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
-/**
|
|
|
- * PDF document loading operation.
|
|
|
- * @class
|
|
|
- * @alias PDFDocumentLoadingTask
|
|
|
- */
|
|
|
-var PDFDocumentLoadingTask = (function PDFDocumentLoadingTaskClosure() {
|
|
|
- var nextDocumentId = 0;
|
|
|
+ switch (type) {
|
|
|
+ case 'JpegStream':
|
|
|
+ imageData = data[3];
|
|
|
+ loadJpegStream(id, imageData, pageProxy.objs);
|
|
|
+ break;
|
|
|
+ case 'Image':
|
|
|
+ imageData = data[3];
|
|
|
+ pageProxy.objs.resolve(id, imageData);
|
|
|
|
|
|
- /** @constructs PDFDocumentLoadingTask */
|
|
|
- function PDFDocumentLoadingTask() {
|
|
|
- this._capability = createPromiseCapability();
|
|
|
- this._transport = null;
|
|
|
- this._worker = null;
|
|
|
+ // heuristics that will allow not to store large data
|
|
|
+ var MAX_IMAGE_SIZE_TO_STORE = 8000000;
|
|
|
+ if (imageData && 'data' in imageData &&
|
|
|
+ imageData.data.length > MAX_IMAGE_SIZE_TO_STORE) {
|
|
|
+ pageProxy.cleanupAfterRender = true;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ error('Got unknown object type ' + type);
|
|
|
+ }
|
|
|
+ }, this);
|
|
|
|
|
|
- /**
|
|
|
- * Unique document loading task id -- used in MessageHandlers.
|
|
|
- * @type {string}
|
|
|
- */
|
|
|
- this.docId = 'd' + (nextDocumentId++);
|
|
|
+ messageHandler.on('DocProgress', function transportDocProgress(data) {
|
|
|
+ if (this.destroyed) {
|
|
|
+ return; // Ignore any pending requests if the worker was terminated.
|
|
|
+ }
|
|
|
|
|
|
- /**
|
|
|
- * Shows if loading task is destroyed.
|
|
|
- * @type {boolean}
|
|
|
- */
|
|
|
- this.destroyed = false;
|
|
|
+ var loadingTask = this.loadingTask;
|
|
|
+ if (loadingTask.onProgress) {
|
|
|
+ loadingTask.onProgress({
|
|
|
+ loaded: data.loaded,
|
|
|
+ total: data.total
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }, this);
|
|
|
|
|
|
- /**
|
|
|
- * 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;
|
|
|
+ messageHandler.on('PageError', function transportError(data) {
|
|
|
+ if (this.destroyed) {
|
|
|
+ return; // Ignore any pending requests if the worker was terminated.
|
|
|
+ }
|
|
|
|
|
|
- /**
|
|
|
- * 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;
|
|
|
+ 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);
|
|
|
|
|
|
- /**
|
|
|
- * Callback to when unsupported feature is used. The callback receives
|
|
|
- * an {PDFJS.UNSUPPORTED_FEATURES} argument.
|
|
|
- */
|
|
|
- this.onUnsupportedFeature = null;
|
|
|
- }
|
|
|
+ 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);
|
|
|
|
|
|
- PDFDocumentLoadingTask.prototype =
|
|
|
- /** @lends PDFDocumentLoadingTask.prototype */ {
|
|
|
- /**
|
|
|
- * @return {Promise}
|
|
|
- */
|
|
|
- get promise() {
|
|
|
- return this._capability.promise;
|
|
|
- },
|
|
|
+ messageHandler.on('JpegDecode', function(data) {
|
|
|
+ if (this.destroyed) {
|
|
|
+ return Promise.reject('Worker was terminated');
|
|
|
+ }
|
|
|
|
|
|
- /**
|
|
|
- * 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 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'));
|
|
|
+ }
|
|
|
|
|
|
- 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;
|
|
|
+ return new Promise(function (resolve, reject) {
|
|
|
+ var img = new Image();
|
|
|
+ img.onload = function () {
|
|
|
+ var width = img.width;
|
|
|
+ var height = img.height;
|
|
|
+ var size = width * height;
|
|
|
+ var rgbaLength = size * 4;
|
|
|
+ var buf = new Uint8Array(size * components);
|
|
|
+ var tmpCanvas = createScratchCanvas(width, height);
|
|
|
+ var tmpCtx = tmpCanvas.getContext('2d');
|
|
|
+ tmpCtx.drawImage(img, 0, 0);
|
|
|
+ var data = tmpCtx.getImageData(0, 0, width, height).data;
|
|
|
+ var i, j;
|
|
|
+
|
|
|
+ if (components === 3) {
|
|
|
+ for (i = 0, j = 0; i < rgbaLength; i += 4, j += 3) {
|
|
|
+ buf[j] = data[i];
|
|
|
+ buf[j + 1] = data[i + 1];
|
|
|
+ buf[j + 2] = data[i + 2];
|
|
|
+ }
|
|
|
+ } else if (components === 1) {
|
|
|
+ for (i = 0, j = 0; i < rgbaLength; i += 4, j++) {
|
|
|
+ buf[j] = data[i];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ resolve({ data: buf, width: width, height: height});
|
|
|
+ };
|
|
|
+ img.onerror = function () {
|
|
|
+ reject(new Error('JpegDecode failed to load image'));
|
|
|
+ };
|
|
|
+ img.src = imageUrl;
|
|
|
+ });
|
|
|
+ }, this);
|
|
|
+ },
|
|
|
+
|
|
|
+ getData: function WorkerTransport_getData() {
|
|
|
+ return this.messageHandler.sendWithPromise('GetData', null);
|
|
|
+ },
|
|
|
+
|
|
|
+ getPage: function WorkerTransport_getPage(pageNumber, capability) {
|
|
|
+ if (pageNumber <= 0 || pageNumber > this.numPages ||
|
|
|
+ (pageNumber|0) !== pageNumber) {
|
|
|
+ return Promise.reject(new Error('Invalid page request'));
|
|
|
+ }
|
|
|
+
|
|
|
+ var pageIndex = pageNumber - 1;
|
|
|
+ if (pageIndex in this.pagePromises) {
|
|
|
+ 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;
|
|
|
+ return promise;
|
|
|
},
|
|
|
|
|
|
- /**
|
|
|
- * 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;
|
|
|
-})();
|
|
|
+ getPageIndex: function WorkerTransport_getPageIndexByRef(ref) {
|
|
|
+ return this.messageHandler.sendWithPromise('GetPageIndex', { ref: ref });
|
|
|
+ },
|
|
|
|
|
|
-/**
|
|
|
- * 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;
|
|
|
+ getAnnotations: function WorkerTransport_getAnnotations(pageIndex, intent) {
|
|
|
+ return this.messageHandler.sendWithPromise('GetAnnotations', {
|
|
|
+ pageIndex: pageIndex,
|
|
|
+ intent: intent,
|
|
|
+ });
|
|
|
+ },
|
|
|
|
|
|
- this._rangeListeners = [];
|
|
|
- this._progressListeners = [];
|
|
|
- this._progressiveReadListeners = [];
|
|
|
- this._readyCapability = createPromiseCapability();
|
|
|
- }
|
|
|
- PDFDataRangeTransport.prototype =
|
|
|
- /** @lends PDFDataRangeTransport.prototype */ {
|
|
|
- addRangeListener:
|
|
|
- function PDFDataRangeTransport_addRangeListener(listener) {
|
|
|
- this._rangeListeners.push(listener);
|
|
|
+ getDestinations: function WorkerTransport_getDestinations() {
|
|
|
+ return this.messageHandler.sendWithPromise('GetDestinations', null);
|
|
|
},
|
|
|
|
|
|
- addProgressListener:
|
|
|
- function PDFDataRangeTransport_addProgressListener(listener) {
|
|
|
- this._progressListeners.push(listener);
|
|
|
+ getDestination: function WorkerTransport_getDestination(id) {
|
|
|
+ return this.messageHandler.sendWithPromise('GetDestination', { id: id });
|
|
|
},
|
|
|
|
|
|
- addProgressiveReadListener:
|
|
|
- function PDFDataRangeTransport_addProgressiveReadListener(listener) {
|
|
|
- this._progressiveReadListeners.push(listener);
|
|
|
+ getPageLabels: function WorkerTransport_getPageLabels() {
|
|
|
+ return this.messageHandler.sendWithPromise('GetPageLabels', null);
|
|
|
},
|
|
|
|
|
|
- onDataRange: function PDFDataRangeTransport_onDataRange(begin, chunk) {
|
|
|
- var listeners = this._rangeListeners;
|
|
|
- for (var i = 0, n = listeners.length; i < n; ++i) {
|
|
|
- listeners[i](begin, chunk);
|
|
|
- }
|
|
|
+ getAttachments: function WorkerTransport_getAttachments() {
|
|
|
+ return this.messageHandler.sendWithPromise('GetAttachments', null);
|
|
|
},
|
|
|
|
|
|
- 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));
|
|
|
+ getJavaScript: function WorkerTransport_getJavaScript() {
|
|
|
+ return this.messageHandler.sendWithPromise('GetJavaScript', null);
|
|
|
},
|
|
|
|
|
|
- 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));
|
|
|
+ getOutline: function WorkerTransport_getOutline() {
|
|
|
+ return this.messageHandler.sendWithPromise('GetOutline', null);
|
|
|
},
|
|
|
|
|
|
- transportReady: function PDFDataRangeTransport_transportReady() {
|
|
|
- this._readyCapability.resolve();
|
|
|
+ getMetadata: function WorkerTransport_getMetadata() {
|
|
|
+ return this.messageHandler.sendWithPromise('GetMetadata', null).
|
|
|
+ then(function transportMetadata(results) {
|
|
|
+ return {
|
|
|
+ info: results[0],
|
|
|
+ metadata: (results[1] ? new Metadata(results[1]) : null)
|
|
|
+ };
|
|
|
+ });
|
|
|
},
|
|
|
|
|
|
- requestDataRange:
|
|
|
- function PDFDataRangeTransport_requestDataRange(begin, end) {
|
|
|
- throw new Error('Abstract method PDFDataRangeTransport.requestDataRange');
|
|
|
+ getStats: function WorkerTransport_getStats() {
|
|
|
+ return this.messageHandler.sendWithPromise('GetStats', null);
|
|
|
},
|
|
|
|
|
|
- abort: function PDFDataRangeTransport_abort() {
|
|
|
+ startCleanup: function WorkerTransport_startCleanup() {
|
|
|
+ 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.cleanup();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.commonObjs.clear();
|
|
|
+ this.fontLoader.clear();
|
|
|
+ }.bind(this));
|
|
|
}
|
|
|
};
|
|
|
- return PDFDataRangeTransport;
|
|
|
-})();
|
|
|
+ return WorkerTransport;
|
|
|
|
|
|
-PDFJS.PDFDataRangeTransport = PDFDataRangeTransport;
|
|
|
+})();
|
|
|
|
|
|
/**
|
|
|
- * Proxy to a PDFDocument in the worker thread. Also, contains commonly used
|
|
|
- * properties that can be read synchronously.
|
|
|
- * @class
|
|
|
- * @alias PDFDocumentProxy
|
|
|
+ * A PDF document and page is built of many objects. E.g. there are objects
|
|
|
+ * for fonts, images, rendering code and such. These objects might get processed
|
|
|
+ * inside of a worker. The `PDFObjects` implements some basic functions to
|
|
|
+ * manage these objects.
|
|
|
+ * @ignore
|
|
|
*/
|
|
|
-var PDFDocumentProxy = (function PDFDocumentProxyClosure() {
|
|
|
- function PDFDocumentProxy(pdfInfo, transport, loadingTask) {
|
|
|
- this.pdfInfo = pdfInfo;
|
|
|
- this.transport = transport;
|
|
|
- this.loadingTask = loadingTask;
|
|
|
+var PDFObjects = (function PDFObjectsClosure() {
|
|
|
+ function PDFObjects() {
|
|
|
+ this.objs = Object.create(null);
|
|
|
}
|
|
|
- PDFDocumentProxy.prototype = /** @lends PDFDocumentProxy.prototype */ {
|
|
|
- /**
|
|
|
- * @return {number} Total number of pages the PDF contains.
|
|
|
- */
|
|
|
- get numPages() {
|
|
|
- return this.pdfInfo.numPages;
|
|
|
- },
|
|
|
- /**
|
|
|
- * @return {string} A unique ID to identify a PDF. Not guaranteed to be
|
|
|
- * unique.
|
|
|
- */
|
|
|
- get fingerprint() {
|
|
|
- return this.pdfInfo.fingerprint;
|
|
|
- },
|
|
|
- /**
|
|
|
- * @param {number} pageNumber The page number to get. The first page is 1.
|
|
|
- * @return {Promise} A promise that is resolved with a {@link PDFPageProxy}
|
|
|
- * object.
|
|
|
- */
|
|
|
- getPage: function PDFDocumentProxy_getPage(pageNumber) {
|
|
|
- return this.transport.getPage(pageNumber);
|
|
|
- },
|
|
|
+
|
|
|
+ PDFObjects.prototype = {
|
|
|
/**
|
|
|
- * @param {{num: number, gen: number}} ref The page reference. Must have
|
|
|
- * the 'num' and 'gen' properties.
|
|
|
- * @return {Promise} A promise that is resolved with the page index that is
|
|
|
- * associated with the reference.
|
|
|
+ * Internal function.
|
|
|
+ * Ensures there is an object defined for `objId`.
|
|
|
*/
|
|
|
- getPageIndex: function PDFDocumentProxy_getPageIndex(ref) {
|
|
|
- return this.transport.getPageIndex(ref);
|
|
|
+ ensureObj: function PDFObjects_ensureObj(objId) {
|
|
|
+ if (this.objs[objId]) {
|
|
|
+ return this.objs[objId];
|
|
|
+ }
|
|
|
+
|
|
|
+ var obj = {
|
|
|
+ capability: createPromiseCapability(),
|
|
|
+ data: null,
|
|
|
+ resolved: false
|
|
|
+ };
|
|
|
+ this.objs[objId] = obj;
|
|
|
+
|
|
|
+ return obj;
|
|
|
},
|
|
|
+
|
|
|
/**
|
|
|
- * @return {Promise} A promise that is resolved with a lookup table for
|
|
|
- * mapping named destinations to reference numbers.
|
|
|
+ * If called *without* callback, this returns the data of `objId` but the
|
|
|
+ * object needs to be resolved. If it isn't, this function throws.
|
|
|
*
|
|
|
- * This can be slow for large documents: use getDestination instead
|
|
|
- */
|
|
|
- getDestinations: function PDFDocumentProxy_getDestinations() {
|
|
|
- return this.transport.getDestinations();
|
|
|
- },
|
|
|
- /**
|
|
|
- * @param {string} id The named destination to get.
|
|
|
- * @return {Promise} A promise that is resolved with all information
|
|
|
- * of the given named destination.
|
|
|
- */
|
|
|
- getDestination: function PDFDocumentProxy_getDestination(id) {
|
|
|
- return this.transport.getDestination(id);
|
|
|
- },
|
|
|
- /**
|
|
|
- * @return {Promise} A promise that is resolved with:
|
|
|
- * an Array containing the pageLabels that correspond to the pageIndexes,
|
|
|
- * or `null` when no pageLabels are present in the PDF file.
|
|
|
- */
|
|
|
- getPageLabels: function PDFDocumentProxy_getPageLabels() {
|
|
|
- return this.transport.getPageLabels();
|
|
|
- },
|
|
|
- /**
|
|
|
- * @return {Promise} A promise that is resolved with a lookup table for
|
|
|
- * mapping named attachments to their content.
|
|
|
+ * If called *with* a callback, the callback is called with the data of the
|
|
|
+ * object once the object is resolved. That means, if you call this
|
|
|
+ * function and the object is already resolved, the callback gets called
|
|
|
+ * right away.
|
|
|
*/
|
|
|
- getAttachments: function PDFDocumentProxy_getAttachments() {
|
|
|
- return this.transport.getAttachments();
|
|
|
+ get: function PDFObjects_get(objId, callback) {
|
|
|
+ // If there is a callback, then the get can be async and the object is
|
|
|
+ // not required to be resolved right now
|
|
|
+ if (callback) {
|
|
|
+ this.ensureObj(objId).capability.promise.then(callback);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // If there isn't a callback, the user expects to get the resolved data
|
|
|
+ // directly.
|
|
|
+ var obj = this.objs[objId];
|
|
|
+
|
|
|
+ // If there isn't an object yet or the object isn't resolved, then the
|
|
|
+ // data isn't ready yet!
|
|
|
+ if (!obj || !obj.resolved) {
|
|
|
+ error('Requesting object that isn\'t resolved yet ' + objId);
|
|
|
+ }
|
|
|
+
|
|
|
+ return obj.data;
|
|
|
},
|
|
|
+
|
|
|
/**
|
|
|
- * @return {Promise} A promise that is resolved with an array of all the
|
|
|
- * JavaScript strings in the name tree.
|
|
|
+ * Resolves the object `objId` with optional `data`.
|
|
|
*/
|
|
|
- getJavaScript: function PDFDocumentProxy_getJavaScript() {
|
|
|
- return this.transport.getJavaScript();
|
|
|
+ resolve: function PDFObjects_resolve(objId, data) {
|
|
|
+ var obj = this.ensureObj(objId);
|
|
|
+
|
|
|
+ obj.resolved = true;
|
|
|
+ obj.data = data;
|
|
|
+ obj.capability.resolve(data);
|
|
|
},
|
|
|
- /**
|
|
|
- * @return {Promise} A promise that is resolved with an {Array} that is a
|
|
|
- * tree outline (if it has one) of the PDF. The tree is in the format of:
|
|
|
- * [
|
|
|
- * {
|
|
|
- * title: string,
|
|
|
- * bold: boolean,
|
|
|
- * italic: boolean,
|
|
|
- * color: rgb array,
|
|
|
- * dest: dest obj,
|
|
|
- * url: string,
|
|
|
- * items: array of more items like this
|
|
|
- * },
|
|
|
- * ...
|
|
|
- * ].
|
|
|
- */
|
|
|
- getOutline: function PDFDocumentProxy_getOutline() {
|
|
|
- return this.transport.getOutline();
|
|
|
+
|
|
|
+ isResolved: function PDFObjects_isResolved(objId) {
|
|
|
+ var objs = this.objs;
|
|
|
+
|
|
|
+ if (!objs[objId]) {
|
|
|
+ return false;
|
|
|
+ } else {
|
|
|
+ return objs[objId].resolved;
|
|
|
+ }
|
|
|
},
|
|
|
- /**
|
|
|
- * @return {Promise} A promise that is resolved with an {Object} that has
|
|
|
- * info and metadata properties. Info is an {Object} filled with anything
|
|
|
- * available in the information dictionary and similarly metadata is a
|
|
|
- * {Metadata} object with information from the metadata section of the PDF.
|
|
|
- */
|
|
|
- getMetadata: function PDFDocumentProxy_getMetadata() {
|
|
|
- return this.transport.getMetadata();
|
|
|
+
|
|
|
+ hasData: function PDFObjects_hasData(objId) {
|
|
|
+ return this.isResolved(objId);
|
|
|
},
|
|
|
+
|
|
|
/**
|
|
|
- * @return {Promise} A promise that is resolved with a TypedArray that has
|
|
|
- * the raw data from the PDF.
|
|
|
+ * Returns the data of `objId` if object exists, null otherwise.
|
|
|
*/
|
|
|
- getData: function PDFDocumentProxy_getData() {
|
|
|
- return this.transport.getData();
|
|
|
+ getData: function PDFObjects_getData(objId) {
|
|
|
+ var objs = this.objs;
|
|
|
+ if (!objs[objId] || !objs[objId].resolved) {
|
|
|
+ return null;
|
|
|
+ } else {
|
|
|
+ return objs[objId].data;
|
|
|
+ }
|
|
|
},
|
|
|
+
|
|
|
+ clear: function PDFObjects_clear() {
|
|
|
+ this.objs = Object.create(null);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ return PDFObjects;
|
|
|
+})();
|
|
|
+
|
|
|
+/**
|
|
|
+ * Allows controlling of the rendering tasks.
|
|
|
+ * @class
|
|
|
+ * @alias RenderTask
|
|
|
+ */
|
|
|
+var RenderTask = (function RenderTaskClosure() {
|
|
|
+ function RenderTask(internalRenderTask) {
|
|
|
+ this._internalRenderTask = internalRenderTask;
|
|
|
+
|
|
|
/**
|
|
|
- * @return {Promise} A promise that is resolved when the document's data
|
|
|
- * is loaded. It is resolved with an {Object} that contains the length
|
|
|
- * property that indicates size of the PDF data in bytes.
|
|
|
+ * 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}
|
|
|
*/
|
|
|
- getDownloadInfo: function PDFDocumentProxy_getDownloadInfo() {
|
|
|
- return this.transport.downloadInfoCapability.promise;
|
|
|
- },
|
|
|
+ this.onContinue = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ RenderTask.prototype = /** @lends RenderTask.prototype */ {
|
|
|
/**
|
|
|
- * @return {Promise} A promise this is resolved with current stats about
|
|
|
- * document structures (see {@link PDFDocumentStats}).
|
|
|
+ * Promise for rendering task completion.
|
|
|
+ * @return {Promise}
|
|
|
*/
|
|
|
- getStats: function PDFDocumentProxy_getStats() {
|
|
|
- return this.transport.getStats();
|
|
|
+ get promise() {
|
|
|
+ return this._internalRenderTask.capability.promise;
|
|
|
},
|
|
|
+
|
|
|
/**
|
|
|
- * Cleans up resources allocated by the document, e.g. created @font-face.
|
|
|
+ * 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.
|
|
|
*/
|
|
|
- cleanup: function PDFDocumentProxy_cleanup() {
|
|
|
- this.transport.startCleanup();
|
|
|
+ cancel: function RenderTask_cancel() {
|
|
|
+ this._internalRenderTask.cancel();
|
|
|
},
|
|
|
+
|
|
|
/**
|
|
|
- * Destroys current document instance and terminates worker.
|
|
|
+ * 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.
|
|
|
*/
|
|
|
- destroy: function PDFDocumentProxy_destroy() {
|
|
|
- return this.loadingTask.destroy();
|
|
|
+ then: function RenderTask_then(onFulfilled, onRejected) {
|
|
|
+ return this.promise.then.apply(this.promise, arguments);
|
|
|
}
|
|
|
};
|
|
|
- return PDFDocumentProxy;
|
|
|
+
|
|
|
+ return RenderTask;
|
|
|
})();
|
|
|
|
|
|
/**
|
|
|
- * Page getTextContent parameters.
|
|
|
- *
|
|
|
- * @typedef {Object} getTextContentParameters
|
|
|
- * @param {boolean} normalizeWhitespace - replaces all occurrences of
|
|
|
- * whitespace with standard spaces (0x20). The default value is `false`.
|
|
|
+ * For internal use only.
|
|
|
+ * @ignore
|
|
|
*/
|
|
|
+var InternalRenderTask = (function InternalRenderTaskClosure() {
|
|
|
|
|
|
-/**
|
|
|
- * Page text content.
|
|
|
- *
|
|
|
- * @typedef {Object} TextContent
|
|
|
- * @property {array} items - array of {@link TextItem}
|
|
|
- * @property {Object} styles - {@link TextStyles} objects, indexed by font
|
|
|
- * name.
|
|
|
- */
|
|
|
+ function InternalRenderTask(callback, params, objs, commonObjs, operatorList,
|
|
|
+ pageNumber) {
|
|
|
+ this.callback = callback;
|
|
|
+ this.params = params;
|
|
|
+ this.objs = objs;
|
|
|
+ this.commonObjs = commonObjs;
|
|
|
+ this.operatorListIdx = null;
|
|
|
+ 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);
|
|
|
+ }
|
|
|
|
|
|
-/**
|
|
|
- * Page text content part.
|
|
|
- *
|
|
|
- * @typedef {Object} TextItem
|
|
|
- * @property {string} str - text content.
|
|
|
- * @property {string} dir - text direction: 'ttb', 'ltr' or 'rtl'.
|
|
|
- * @property {array} transform - transformation matrix.
|
|
|
- * @property {number} width - width in device space.
|
|
|
- * @property {number} height - height in device space.
|
|
|
- * @property {string} fontName - font name used by pdf.js for converted font.
|
|
|
- */
|
|
|
+ InternalRenderTask.prototype = {
|
|
|
|
|
|
-/**
|
|
|
- * Text style.
|
|
|
- *
|
|
|
- * @typedef {Object} TextStyle
|
|
|
- * @property {number} ascent - font ascent.
|
|
|
- * @property {number} descent - font descent.
|
|
|
- * @property {boolean} vertical - text is in vertical mode.
|
|
|
- * @property {string} fontFamily - possible font family
|
|
|
- */
|
|
|
+ initalizeGraphics:
|
|
|
+ function InternalRenderTask_initalizeGraphics(transparency) {
|
|
|
|
|
|
-/**
|
|
|
- * 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.
|
|
|
- */
|
|
|
+ if (this.cancelled) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (PDFJS.pdfBug && 'StepperManager' in globalScope &&
|
|
|
+ globalScope.StepperManager.enabled) {
|
|
|
+ this.stepper = globalScope.StepperManager.create(this.pageNumber - 1);
|
|
|
+ this.stepper.init(this.operatorList);
|
|
|
+ this.stepper.nextBreakPoint = this.stepper.getNextBreakPoint();
|
|
|
+ }
|
|
|
|
|
|
-/**
|
|
|
- * 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 - (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.
|
|
|
- * @property {Array} argsArray - Array containing the arguments of the
|
|
|
- * functions.
|
|
|
- */
|
|
|
+ var params = this.params;
|
|
|
+ this.gfx = new CanvasGraphics(params.canvasContext, this.commonObjs,
|
|
|
+ this.objs, params.imageLayer);
|
|
|
|
|
|
-/**
|
|
|
- * 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;
|
|
|
- this.transport = transport;
|
|
|
- this.stats = new StatTimer();
|
|
|
- this.stats.enabled = !!globalScope.PDFJS.enableStats;
|
|
|
- this.commonObjs = transport.commonObjs;
|
|
|
- this.objs = new PDFObjects();
|
|
|
- this.cleanupAfterRender = false;
|
|
|
- this.pendingCleanup = false;
|
|
|
- this.intentStates = Object.create(null);
|
|
|
- this.destroyed = false;
|
|
|
- }
|
|
|
- PDFPageProxy.prototype = /** @lends PDFPageProxy.prototype */ {
|
|
|
- /**
|
|
|
- * @return {number} Page number of the page. First page is 1.
|
|
|
- */
|
|
|
- get pageNumber() {
|
|
|
- return this.pageIndex + 1;
|
|
|
- },
|
|
|
- /**
|
|
|
- * @return {number} The number of degrees the page is rotated clockwise.
|
|
|
- */
|
|
|
- get rotate() {
|
|
|
- return this.pageInfo.rotate;
|
|
|
- },
|
|
|
- /**
|
|
|
- * @return {Object} The reference that points to this page. It has 'num' and
|
|
|
- * 'gen' properties.
|
|
|
- */
|
|
|
- get ref() {
|
|
|
- return this.pageInfo.ref;
|
|
|
- },
|
|
|
- /**
|
|
|
- * @return {Array} An array of the visible portion of the PDF page in the
|
|
|
- * user space units - [x1, y1, x2, y2].
|
|
|
- */
|
|
|
- get view() {
|
|
|
- return this.pageInfo.view;
|
|
|
- },
|
|
|
- /**
|
|
|
- * @param {number} scale The desired scale of the viewport.
|
|
|
- * @param {number} rotate Degrees to rotate the viewport. If omitted this
|
|
|
- * defaults to the page rotation.
|
|
|
- * @return {PDFJS.PageViewport} Contains 'width' and 'height' properties
|
|
|
- * along with transforms required for rendering.
|
|
|
- */
|
|
|
- getViewport: function PDFPageProxy_getViewport(scale, rotate) {
|
|
|
- if (arguments.length < 2) {
|
|
|
- rotate = this.rotate;
|
|
|
+ this.gfx.beginDrawing(params.transform, params.viewport, transparency);
|
|
|
+ this.operatorListIdx = 0;
|
|
|
+ this.graphicsReady = true;
|
|
|
+ if (this.graphicsReadyCallback) {
|
|
|
+ this.graphicsReadyCallback();
|
|
|
}
|
|
|
- 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(params) {
|
|
|
- var intent = (params && params.intent) || null;
|
|
|
|
|
|
- if (!this.annotationsPromise || this.annotationsIntent !== intent) {
|
|
|
- this.annotationsPromise = this.transport.getAnnotations(this.pageIndex,
|
|
|
- intent);
|
|
|
- this.annotationsIntent = intent;
|
|
|
- }
|
|
|
- return this.annotationsPromise;
|
|
|
+ cancel: function InternalRenderTask_cancel() {
|
|
|
+ this.running = false;
|
|
|
+ this.cancelled = true;
|
|
|
+ this.callback('cancelled');
|
|
|
},
|
|
|
- /**
|
|
|
- * 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
|
|
|
- * is resolved when the page finishes rendering.
|
|
|
- */
|
|
|
- render: function PDFPageProxy_render(params) {
|
|
|
- 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.pendingCleanup = false;
|
|
|
-
|
|
|
- var renderingIntent = (params.intent === 'print' ? 'print' : 'display');
|
|
|
-
|
|
|
- if (!this.intentStates[renderingIntent]) {
|
|
|
- this.intentStates[renderingIntent] = Object.create(null);
|
|
|
- }
|
|
|
- var intentState = this.intentStates[renderingIntent];
|
|
|
-
|
|
|
- // If there's no displayReadyCapability yet, then the operatorList
|
|
|
- // was never requested before. Make the request and create the promise.
|
|
|
- if (!intentState.displayReadyCapability) {
|
|
|
- intentState.receivingOperatorList = true;
|
|
|
- intentState.displayReadyCapability = createPromiseCapability();
|
|
|
- intentState.operatorList = {
|
|
|
- fnArray: [],
|
|
|
- argsArray: [],
|
|
|
- lastChunk: false
|
|
|
- };
|
|
|
-
|
|
|
- this.stats.time('Page Request');
|
|
|
- this.transport.messageHandler.send('RenderPageRequest', {
|
|
|
- pageIndex: this.pageNumber - 1,
|
|
|
- intent: renderingIntent
|
|
|
- });
|
|
|
- }
|
|
|
|
|
|
- var internalRenderTask = new InternalRenderTask(complete, params,
|
|
|
- this.objs,
|
|
|
- this.commonObjs,
|
|
|
- intentState.operatorList,
|
|
|
- this.pageNumber);
|
|
|
- internalRenderTask.useRequestAnimationFrame = renderingIntent !== 'print';
|
|
|
- if (!intentState.renderTasks) {
|
|
|
- intentState.renderTasks = [];
|
|
|
+ operatorListChanged: function InternalRenderTask_operatorListChanged() {
|
|
|
+ if (!this.graphicsReady) {
|
|
|
+ if (!this.graphicsReadyCallback) {
|
|
|
+ this.graphicsReadyCallback = this._continueBound;
|
|
|
+ }
|
|
|
+ return;
|
|
|
}
|
|
|
- intentState.renderTasks.push(internalRenderTask);
|
|
|
- var renderTask = internalRenderTask.task;
|
|
|
|
|
|
- // Obsolete parameter support
|
|
|
- if (params.continueCallback) {
|
|
|
- deprecated('render is used with continueCallback parameter');
|
|
|
- renderTask.onContinue = params.continueCallback;
|
|
|
+ if (this.stepper) {
|
|
|
+ this.stepper.updateOperatorList(this.operatorList);
|
|
|
}
|
|
|
|
|
|
- var self = this;
|
|
|
- intentState.displayReadyCapability.promise.then(
|
|
|
- function pageDisplayReadyPromise(transparency) {
|
|
|
- if (self.pendingCleanup) {
|
|
|
- complete();
|
|
|
- return;
|
|
|
- }
|
|
|
- stats.time('Rendering');
|
|
|
- internalRenderTask.initalizeGraphics(transparency);
|
|
|
- internalRenderTask.operatorListChanged();
|
|
|
- },
|
|
|
- function pageDisplayReadPromiseError(reason) {
|
|
|
- complete(reason);
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- function complete(error) {
|
|
|
- var i = intentState.renderTasks.indexOf(internalRenderTask);
|
|
|
- if (i >= 0) {
|
|
|
- intentState.renderTasks.splice(i, 1);
|
|
|
- }
|
|
|
-
|
|
|
- if (self.cleanupAfterRender) {
|
|
|
- self.pendingCleanup = true;
|
|
|
- }
|
|
|
- self._tryCleanup();
|
|
|
-
|
|
|
- if (error) {
|
|
|
- internalRenderTask.capability.reject(error);
|
|
|
- } else {
|
|
|
- internalRenderTask.capability.resolve();
|
|
|
- }
|
|
|
- stats.timeEnd('Rendering');
|
|
|
- stats.timeEnd('Overall');
|
|
|
+ if (this.running) {
|
|
|
+ return;
|
|
|
}
|
|
|
-
|
|
|
- return renderTask;
|
|
|
+ this._continue();
|
|
|
},
|
|
|
|
|
|
- /**
|
|
|
- * @return {Promise} A promise resolved with an {@link PDFOperatorList}
|
|
|
- * object that represents page's operator list.
|
|
|
- */
|
|
|
- getOperatorList: function PDFPageProxy_getOperatorList() {
|
|
|
- function operatorListChanged() {
|
|
|
- if (intentState.operatorList.lastChunk) {
|
|
|
- intentState.opListReadCapability.resolve(intentState.operatorList);
|
|
|
-
|
|
|
- var i = intentState.renderTasks.indexOf(opListTask);
|
|
|
- if (i >= 0) {
|
|
|
- intentState.renderTasks.splice(i, 1);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- var renderingIntent = 'oplist';
|
|
|
- if (!this.intentStates[renderingIntent]) {
|
|
|
- this.intentStates[renderingIntent] = Object.create(null);
|
|
|
+ _continue: function InternalRenderTask__continue() {
|
|
|
+ this.running = true;
|
|
|
+ if (this.cancelled) {
|
|
|
+ return;
|
|
|
}
|
|
|
- var intentState = this.intentStates[renderingIntent];
|
|
|
- var opListTask;
|
|
|
-
|
|
|
- if (!intentState.opListReadCapability) {
|
|
|
- opListTask = {};
|
|
|
- opListTask.operatorListChanged = operatorListChanged;
|
|
|
- intentState.receivingOperatorList = true;
|
|
|
- intentState.opListReadCapability = createPromiseCapability();
|
|
|
- intentState.renderTasks = [];
|
|
|
- intentState.renderTasks.push(opListTask);
|
|
|
- intentState.operatorList = {
|
|
|
- fnArray: [],
|
|
|
- argsArray: [],
|
|
|
- lastChunk: false
|
|
|
- };
|
|
|
-
|
|
|
- this.transport.messageHandler.send('RenderPageRequest', {
|
|
|
- pageIndex: this.pageIndex,
|
|
|
- intent: renderingIntent
|
|
|
- });
|
|
|
+ if (this.task.onContinue) {
|
|
|
+ this.task.onContinue.call(this.task, this._scheduleNextBound);
|
|
|
+ } else {
|
|
|
+ this._scheduleNext();
|
|
|
}
|
|
|
- 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(params) {
|
|
|
- var normalizeWhitespace = (params && params.normalizeWhitespace) || false;
|
|
|
-
|
|
|
- return this.transport.messageHandler.sendWithPromise('GetTextContent', {
|
|
|
- pageIndex: this.pageNumber - 1,
|
|
|
- normalizeWhitespace: normalizeWhitespace,
|
|
|
- });
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * Destroys page object.
|
|
|
- */
|
|
|
- _destroy: function PDFPageProxy_destroy() {
|
|
|
- this.destroyed = true;
|
|
|
- this.transport.pageCache[this.pageIndex] = null;
|
|
|
-
|
|
|
- var waitOn = [];
|
|
|
- Object.keys(this.intentStates).forEach(function(intent) {
|
|
|
- if (intent === 'oplist') {
|
|
|
- // Avoid errors below, since the renderTasks are just stubs.
|
|
|
- return;
|
|
|
- }
|
|
|
- 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();
|
|
|
+ _scheduleNext: function InternalRenderTask__scheduleNext() {
|
|
|
+ if (this.useRequestAnimationFrame) {
|
|
|
+ window.requestAnimationFrame(this._nextBound);
|
|
|
+ } else {
|
|
|
+ Promise.resolve(undefined).then(this._nextBound);
|
|
|
+ }
|
|
|
},
|
|
|
|
|
|
- /**
|
|
|
- * 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
|
|
|
- */
|
|
|
- _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)) {
|
|
|
+ _next: function InternalRenderTask__next() {
|
|
|
+ if (this.cancelled) {
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
- Object.keys(this.intentStates).forEach(function(intent) {
|
|
|
- delete this.intentStates[intent];
|
|
|
- }, this);
|
|
|
- this.objs.clear();
|
|
|
- this.annotationsPromise = null;
|
|
|
- this.pendingCleanup = false;
|
|
|
- },
|
|
|
- /**
|
|
|
- * For internal use only.
|
|
|
- * @ignore
|
|
|
- */
|
|
|
- _startRenderPage: function PDFPageProxy_startRenderPage(transparency,
|
|
|
- intent) {
|
|
|
- var intentState = this.intentStates[intent];
|
|
|
- // TODO Refactor RenderPageRequest to separate rendering
|
|
|
- // and operator list logic
|
|
|
- if (intentState.displayReadyCapability) {
|
|
|
- intentState.displayReadyCapability.resolve(transparency);
|
|
|
- }
|
|
|
- },
|
|
|
- /**
|
|
|
- * For internal use only.
|
|
|
- * @ignore
|
|
|
- */
|
|
|
- _renderPageChunk: function PDFPageProxy_renderPageChunk(operatorListChunk,
|
|
|
- intent) {
|
|
|
- var intentState = this.intentStates[intent];
|
|
|
- var i, ii;
|
|
|
- // Add the new chunk to the current operator list.
|
|
|
- for (i = 0, ii = operatorListChunk.length; i < ii; i++) {
|
|
|
- intentState.operatorList.fnArray.push(operatorListChunk.fnArray[i]);
|
|
|
- intentState.operatorList.argsArray.push(
|
|
|
- operatorListChunk.argsArray[i]);
|
|
|
+ this.operatorListIdx = this.gfx.executeOperatorList(this.operatorList,
|
|
|
+ this.operatorListIdx,
|
|
|
+ this._continueBound,
|
|
|
+ this.stepper);
|
|
|
+ if (this.operatorListIdx === this.operatorList.argsArray.length) {
|
|
|
+ this.running = false;
|
|
|
+ if (this.operatorList.lastChunk) {
|
|
|
+ this.gfx.endDrawing();
|
|
|
+ this.callback();
|
|
|
+ }
|
|
|
}
|
|
|
- intentState.operatorList.lastChunk = operatorListChunk.lastChunk;
|
|
|
+ }
|
|
|
|
|
|
- // Notify all the rendering tasks there are more operators to be consumed.
|
|
|
- for (i = 0; i < intentState.renderTasks.length; i++) {
|
|
|
- intentState.renderTasks[i].operatorListChanged();
|
|
|
- }
|
|
|
+ };
|
|
|
|
|
|
- if (operatorListChunk.lastChunk) {
|
|
|
- intentState.receivingOperatorList = false;
|
|
|
- this._tryCleanup();
|
|
|
+ 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);
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
- 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;
|
|
|
+exports.getDocument = PDFJS.getDocument;
|
|
|
+exports.PDFDataRangeTransport = PDFDataRangeTransport;
|
|
|
+exports.PDFDocumentProxy = PDFDocumentProxy;
|
|
|
+exports.PDFPageProxy = PDFPageProxy;
|
|
|
+}));
|
|
|
|
|
|
- function getWorkerSrc() {
|
|
|
- if (PDFJS.workerSrc) {
|
|
|
- return PDFJS.workerSrc;
|
|
|
- }
|
|
|
- if (pdfjsFilePath) {
|
|
|
- return pdfjsFilePath.replace(/\.js$/i, '.worker.js');
|
|
|
- }
|
|
|
- error('No PDFJS.workerSrc specified');
|
|
|
- }
|
|
|
|
|
|
- // 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 (root, factory) {
|
|
|
+ {
|
|
|
+ factory((root.pdfjsCoreFonts = {}), root.pdfjsSharedUtil,
|
|
|
+ root.pdfjsCorePrimitives, root.pdfjsCoreStream, root.pdfjsCoreParser,
|
|
|
+ root.pdfjsCoreCMap, root.pdfjsCoreGlyphList, root.pdfjsCoreCharsets,
|
|
|
+ root.pdfjsCoreFontRenderer, root.pdfjsCoreEncodings,
|
|
|
+ root.pdfjsCoreStandardFonts, root.pdfjsCoreUnicode);
|
|
|
}
|
|
|
+}(this, function (exports, sharedUtil, corePrimitives, coreStream, coreParser,
|
|
|
+ coreCMap, coreGlyphList, coreCharsets, coreFontRenderer,
|
|
|
+ coreEncodings, coreStandardFonts, coreUnicode) {
|
|
|
|
|
|
- function createCDNWrapper(url) {
|
|
|
- // We will rely on blob URL's property to specify origin.
|
|
|
- // We want this function to fail in case if createObjectURL or Blob do not
|
|
|
- // exist or fail for some reason -- our Worker creation will fail anyway.
|
|
|
- var wrapper = 'importScripts(\'' + url + '\');';
|
|
|
- return URL.createObjectURL(new Blob([wrapper]));
|
|
|
- }
|
|
|
+var FONT_IDENTITY_MATRIX = sharedUtil.FONT_IDENTITY_MATRIX;
|
|
|
+var FontType = sharedUtil.FontType;
|
|
|
+var Util = sharedUtil.Util;
|
|
|
+var assert = sharedUtil.assert;
|
|
|
+var bytesToString = sharedUtil.bytesToString;
|
|
|
+var error = sharedUtil.error;
|
|
|
+var info = sharedUtil.info;
|
|
|
+var isArray = sharedUtil.isArray;
|
|
|
+var isInt = sharedUtil.isInt;
|
|
|
+var isNum = sharedUtil.isNum;
|
|
|
+var readUint32 = sharedUtil.readUint32;
|
|
|
+var shadow = sharedUtil.shadow;
|
|
|
+var stringToBytes = sharedUtil.stringToBytes;
|
|
|
+var string32 = sharedUtil.string32;
|
|
|
+var warn = sharedUtil.warn;
|
|
|
+var Name = corePrimitives.Name;
|
|
|
+var Stream = coreStream.Stream;
|
|
|
+var Lexer = coreParser.Lexer;
|
|
|
+var CMapFactory = coreCMap.CMapFactory;
|
|
|
+var IdentityCMap = coreCMap.IdentityCMap;
|
|
|
+var getGlyphsUnicode = coreGlyphList.getGlyphsUnicode;
|
|
|
+var getDingbatsGlyphsUnicode = coreGlyphList.getDingbatsGlyphsUnicode;
|
|
|
+var ISOAdobeCharset = coreCharsets.ISOAdobeCharset;
|
|
|
+var ExpertCharset = coreCharsets.ExpertCharset;
|
|
|
+var ExpertSubsetCharset = coreCharsets.ExpertSubsetCharset;
|
|
|
+var FontRendererFactory = coreFontRenderer.FontRendererFactory;
|
|
|
+var WinAnsiEncoding = coreEncodings.WinAnsiEncoding;
|
|
|
+var StandardEncoding = coreEncodings.StandardEncoding;
|
|
|
+var MacRomanEncoding = coreEncodings.MacRomanEncoding;
|
|
|
+var SymbolSetEncoding = coreEncodings.SymbolSetEncoding;
|
|
|
+var ZapfDingbatsEncoding = coreEncodings.ZapfDingbatsEncoding;
|
|
|
+var ExpertEncoding = coreEncodings.ExpertEncoding;
|
|
|
+var getEncoding = coreEncodings.getEncoding;
|
|
|
+var getStdFontMap = coreStandardFonts.getStdFontMap;
|
|
|
+var getNonStdFontMap = coreStandardFonts.getNonStdFontMap;
|
|
|
+var getGlyphMapForStandardFonts = coreStandardFonts.getGlyphMapForStandardFonts;
|
|
|
+var getSupplementalGlyphMapForArialBlack =
|
|
|
+ coreStandardFonts.getSupplementalGlyphMapForArialBlack;
|
|
|
+var getUnicodeRangeFor = coreUnicode.getUnicodeRangeFor;
|
|
|
+var mapSpecialUnicodeValues = coreUnicode.mapSpecialUnicodeValues;
|
|
|
|
|
|
- function PDFWorker(name) {
|
|
|
- this.name = name;
|
|
|
- this.destroyed = false;
|
|
|
+// Unicode Private Use Area
|
|
|
+var PRIVATE_USE_OFFSET_START = 0xE000;
|
|
|
+var PRIVATE_USE_OFFSET_END = 0xF8FF;
|
|
|
+var SKIP_PRIVATE_USE_RANGE_F000_TO_F01F = false;
|
|
|
|
|
|
- this._readyCapability = createPromiseCapability();
|
|
|
- this._port = null;
|
|
|
- this._webWorker = null;
|
|
|
- this._messageHandler = null;
|
|
|
- this._initialize();
|
|
|
- }
|
|
|
+// PDF Glyph Space Units are one Thousandth of a TextSpace Unit
|
|
|
+// except for Type 3 fonts
|
|
|
+var PDF_GLYPH_SPACE_UNITS = 1000;
|
|
|
|
|
|
- PDFWorker.prototype = /** @lends PDFWorker.prototype */ {
|
|
|
- get promise() {
|
|
|
- return this._readyCapability.promise;
|
|
|
- },
|
|
|
+// Hinting is currently disabled due to unknown problems on windows
|
|
|
+// in tracemonkey and various other pdfs with type1 fonts.
|
|
|
+var HINTING_ENABLED = false;
|
|
|
|
|
|
- get port() {
|
|
|
- return this._port;
|
|
|
- },
|
|
|
+// Accented charactars are not displayed properly on windows, using this flag
|
|
|
+// to control analysis of seac charstrings.
|
|
|
+var SEAC_ANALYSIS_ENABLED = false;
|
|
|
|
|
|
- get messageHandler() {
|
|
|
- return this._messageHandler;
|
|
|
- },
|
|
|
+// Maximum subroutine call depth of type 2 chartrings. Matches OTS.
|
|
|
+var MAX_SUBR_NESTING = 10;
|
|
|
|
|
|
- _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 fulfills
|
|
|
- // 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();
|
|
|
- },
|
|
|
+var FontFlags = {
|
|
|
+ FixedPitch: 1,
|
|
|
+ Serif: 2,
|
|
|
+ Symbolic: 4,
|
|
|
+ Script: 8,
|
|
|
+ Nonsymbolic: 32,
|
|
|
+ Italic: 64,
|
|
|
+ AllCap: 65536,
|
|
|
+ SmallCap: 131072,
|
|
|
+ ForceBold: 262144
|
|
|
+};
|
|
|
|
|
|
- _setupFakeWorker: function PDFWorker_setupFakeWorker() {
|
|
|
- if (!globalScope.PDFJS.disableWorker) {
|
|
|
- warn('Setting up fake worker.');
|
|
|
- globalScope.PDFJS.disableWorker = true;
|
|
|
- }
|
|
|
+var MacStandardGlyphOrdering = [
|
|
|
+ '.notdef', '.null', 'nonmarkingreturn', 'space', 'exclam', 'quotedbl',
|
|
|
+ 'numbersign', 'dollar', 'percent', 'ampersand', 'quotesingle', 'parenleft',
|
|
|
+ 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash',
|
|
|
+ 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight',
|
|
|
+ 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater', 'question', 'at',
|
|
|
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
|
|
|
+ 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft',
|
|
|
+ 'backslash', 'bracketright', 'asciicircum', 'underscore', 'grave', 'a', 'b',
|
|
|
+ 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
|
|
|
+ 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright',
|
|
|
+ 'asciitilde', 'Adieresis', 'Aring', 'Ccedilla', 'Eacute', 'Ntilde',
|
|
|
+ 'Odieresis', 'Udieresis', 'aacute', 'agrave', 'acircumflex', 'adieresis',
|
|
|
+ 'atilde', 'aring', 'ccedilla', 'eacute', 'egrave', 'ecircumflex', 'edieresis',
|
|
|
+ 'iacute', 'igrave', 'icircumflex', 'idieresis', 'ntilde', 'oacute', 'ograve',
|
|
|
+ 'ocircumflex', 'odieresis', 'otilde', 'uacute', 'ugrave', 'ucircumflex',
|
|
|
+ 'udieresis', 'dagger', 'degree', 'cent', 'sterling', 'section', 'bullet',
|
|
|
+ 'paragraph', 'germandbls', 'registered', 'copyright', 'trademark', 'acute',
|
|
|
+ 'dieresis', 'notequal', 'AE', 'Oslash', 'infinity', 'plusminus', 'lessequal',
|
|
|
+ 'greaterequal', 'yen', 'mu', 'partialdiff', 'summation', 'product', 'pi',
|
|
|
+ 'integral', 'ordfeminine', 'ordmasculine', 'Omega', 'ae', 'oslash',
|
|
|
+ 'questiondown', 'exclamdown', 'logicalnot', 'radical', 'florin',
|
|
|
+ 'approxequal', 'Delta', 'guillemotleft', 'guillemotright', 'ellipsis',
|
|
|
+ 'nonbreakingspace', 'Agrave', 'Atilde', 'Otilde', 'OE', 'oe', 'endash',
|
|
|
+ 'emdash', 'quotedblleft', 'quotedblright', 'quoteleft', 'quoteright',
|
|
|
+ 'divide', 'lozenge', 'ydieresis', 'Ydieresis', 'fraction', 'currency',
|
|
|
+ 'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'daggerdbl', 'periodcentered',
|
|
|
+ 'quotesinglbase', 'quotedblbase', 'perthousand', 'Acircumflex',
|
|
|
+ 'Ecircumflex', 'Aacute', 'Edieresis', 'Egrave', 'Iacute', 'Icircumflex',
|
|
|
+ 'Idieresis', 'Igrave', 'Oacute', 'Ocircumflex', 'apple', 'Ograve', 'Uacute',
|
|
|
+ 'Ucircumflex', 'Ugrave', 'dotlessi', 'circumflex', 'tilde', 'macron',
|
|
|
+ 'breve', 'dotaccent', 'ring', 'cedilla', 'hungarumlaut', 'ogonek', 'caron',
|
|
|
+ 'Lslash', 'lslash', 'Scaron', 'scaron', 'Zcaron', 'zcaron', 'brokenbar',
|
|
|
+ 'Eth', 'eth', 'Yacute', 'yacute', 'Thorn', 'thorn', 'minus', 'multiply',
|
|
|
+ 'onesuperior', 'twosuperior', 'threesuperior', 'onehalf', 'onequarter',
|
|
|
+ 'threequarters', 'franc', 'Gbreve', 'gbreve', 'Idotaccent', 'Scedilla',
|
|
|
+ 'scedilla', 'Cacute', 'cacute', 'Ccaron', 'ccaron', 'dcroat'];
|
|
|
|
|
|
- setupFakeWorkerGlobal().then(function () {
|
|
|
- if (this.destroyed) {
|
|
|
- this._readyCapability.reject(new Error('Worker was destroyed'));
|
|
|
- return;
|
|
|
- }
|
|
|
+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];
|
|
|
+ var glyphsWidths = properties.widths;
|
|
|
+ for (var glyph in glyphsWidths) {
|
|
|
+ glyphsWidths[glyph] *= scale;
|
|
|
+ }
|
|
|
+ properties.defaultWidth *= scale;
|
|
|
+}
|
|
|
|
|
|
- // 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;
|
|
|
+function getFontType(type, subtype) {
|
|
|
+ switch (type) {
|
|
|
+ case 'Type1':
|
|
|
+ return subtype === 'Type1C' ? FontType.TYPE1C : FontType.TYPE1;
|
|
|
+ case 'CIDFontType0':
|
|
|
+ return subtype === 'CIDFontType0C' ? FontType.CIDFONTTYPE0C :
|
|
|
+ FontType.CIDFONTTYPE0;
|
|
|
+ case 'OpenType':
|
|
|
+ return FontType.OPENTYPE;
|
|
|
+ case 'TrueType':
|
|
|
+ return FontType.TRUETYPE;
|
|
|
+ case 'CIDFontType2':
|
|
|
+ return FontType.CIDFONTTYPE2;
|
|
|
+ case 'MMType1':
|
|
|
+ return FontType.MMTYPE1;
|
|
|
+ case 'Type0':
|
|
|
+ return FontType.TYPE0;
|
|
|
+ default:
|
|
|
+ return FontType.UNKNOWN;
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- // All fake workers use the same port, making id unique.
|
|
|
- var id = 'fake' + (nextFakeWorkerId++);
|
|
|
+var Glyph = (function GlyphClosure() {
|
|
|
+ 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;
|
|
|
+ }
|
|
|
|
|
|
- // 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);
|
|
|
+ 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.isSpace === isSpace;
|
|
|
+ };
|
|
|
|
|
|
- var messageHandler = new MessageHandler(id, id + '_worker', port);
|
|
|
- this._messageHandler = messageHandler;
|
|
|
- this._readyCapability.resolve();
|
|
|
- }.bind(this));
|
|
|
+ return Glyph;
|
|
|
+})();
|
|
|
+
|
|
|
+var ToUnicodeMap = (function ToUnicodeMapClosure() {
|
|
|
+ function ToUnicodeMap(cmap) {
|
|
|
+ // The elements of this._map can be integers or strings, depending on how
|
|
|
+ // |cmap| was created.
|
|
|
+ this._map = cmap;
|
|
|
+ }
|
|
|
+
|
|
|
+ ToUnicodeMap.prototype = {
|
|
|
+ get length() {
|
|
|
+ return this._map.length;
|
|
|
},
|
|
|
|
|
|
- /**
|
|
|
- * 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;
|
|
|
+ forEach: function(callback) {
|
|
|
+ for (var charCode in this._map) {
|
|
|
+ callback(charCode, this._map[charCode].charCodeAt(0));
|
|
|
}
|
|
|
- }
|
|
|
- };
|
|
|
+ },
|
|
|
|
|
|
- return PDFWorker;
|
|
|
-})();
|
|
|
-PDFJS.PDFWorker = PDFWorker;
|
|
|
+ has: function(i) {
|
|
|
+ return this._map[i] !== undefined;
|
|
|
+ },
|
|
|
|
|
|
-/**
|
|
|
- * For internal use only.
|
|
|
- * @ignore
|
|
|
- */
|
|
|
-var WorkerTransport = (function WorkerTransportClosure() {
|
|
|
- function WorkerTransport(messageHandler, loadingTask, pdfDataRangeTransport) {
|
|
|
- this.messageHandler = messageHandler;
|
|
|
- this.loadingTask = loadingTask;
|
|
|
- this.pdfDataRangeTransport = pdfDataRangeTransport;
|
|
|
- this.commonObjs = new PDFObjects();
|
|
|
- this.fontLoader = new FontLoader(loadingTask.docId);
|
|
|
+ get: function(i) {
|
|
|
+ return this._map[i];
|
|
|
+ },
|
|
|
|
|
|
- this.destroyed = false;
|
|
|
- this.destroyCapability = null;
|
|
|
+ charCodeOf: function(v) {
|
|
|
+ return this._map.indexOf(v);
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- this.pageCache = [];
|
|
|
- this.pagePromises = [];
|
|
|
- this.downloadInfoCapability = createPromiseCapability();
|
|
|
+ return ToUnicodeMap;
|
|
|
+})();
|
|
|
|
|
|
- this.setupMessageHandler();
|
|
|
+var IdentityToUnicodeMap = (function IdentityToUnicodeMapClosure() {
|
|
|
+ function IdentityToUnicodeMap(firstChar, lastChar) {
|
|
|
+ this.firstChar = firstChar;
|
|
|
+ this.lastChar = lastChar;
|
|
|
}
|
|
|
- WorkerTransport.prototype = {
|
|
|
- destroy: function WorkerTransport_destroy() {
|
|
|
- if (this.destroyCapability) {
|
|
|
- return this.destroyCapability.promise;
|
|
|
- }
|
|
|
|
|
|
- this.destroyed = true;
|
|
|
- this.destroyCapability = createPromiseCapability();
|
|
|
+ IdentityToUnicodeMap.prototype = {
|
|
|
+ get length() {
|
|
|
+ return (this.lastChar + 1) - this.firstChar;
|
|
|
+ },
|
|
|
|
|
|
- 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;
|
|
|
- // 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;
|
|
|
+ forEach: function (callback) {
|
|
|
+ for (var i = this.firstChar, ii = this.lastChar; i <= ii; i++) {
|
|
|
+ callback(i, i);
|
|
|
+ }
|
|
|
},
|
|
|
|
|
|
- setupMessageHandler:
|
|
|
- function WorkerTransport_setupMessageHandler() {
|
|
|
- var messageHandler = this.messageHandler;
|
|
|
+ has: function (i) {
|
|
|
+ return this.firstChar <= i && i <= this.lastChar;
|
|
|
+ },
|
|
|
|
|
|
- function updatePassword(password) {
|
|
|
- messageHandler.send('UpdatePassword', password);
|
|
|
+ get: function (i) {
|
|
|
+ if (this.firstChar <= i && i <= this.lastChar) {
|
|
|
+ return String.fromCharCode(i);
|
|
|
}
|
|
|
+ return undefined;
|
|
|
+ },
|
|
|
|
|
|
- var pdfDataRangeTransport = this.pdfDataRangeTransport;
|
|
|
- if (pdfDataRangeTransport) {
|
|
|
- pdfDataRangeTransport.addRangeListener(function(begin, chunk) {
|
|
|
- messageHandler.send('OnDataRange', {
|
|
|
- begin: begin,
|
|
|
- chunk: chunk
|
|
|
- });
|
|
|
- });
|
|
|
+ charCodeOf: function (v) {
|
|
|
+ return (isInt(v) && v >= this.firstChar && v <= this.lastChar) ? v : -1;
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- pdfDataRangeTransport.addProgressListener(function(loaded) {
|
|
|
- messageHandler.send('OnDataProgress', {
|
|
|
- loaded: loaded
|
|
|
- });
|
|
|
- });
|
|
|
+ return IdentityToUnicodeMap;
|
|
|
+})();
|
|
|
|
|
|
- pdfDataRangeTransport.addProgressiveReadListener(function(chunk) {
|
|
|
- messageHandler.send('OnDataRange', {
|
|
|
- chunk: chunk
|
|
|
- });
|
|
|
- });
|
|
|
+var OpenTypeFileBuilder = (function OpenTypeFileBuilderClosure() {
|
|
|
+ function writeInt16(dest, offset, num) {
|
|
|
+ dest[offset] = (num >> 8) & 0xFF;
|
|
|
+ dest[offset + 1] = num & 0xFF;
|
|
|
+ }
|
|
|
|
|
|
- messageHandler.on('RequestDataRange',
|
|
|
- function transportDataRange(data) {
|
|
|
- pdfDataRangeTransport.requestDataRange(data.begin, data.end);
|
|
|
- }, this);
|
|
|
+ function writeInt32(dest, offset, num) {
|
|
|
+ dest[offset] = (num >> 24) & 0xFF;
|
|
|
+ dest[offset + 1] = (num >> 16) & 0xFF;
|
|
|
+ dest[offset + 2] = (num >> 8) & 0xFF;
|
|
|
+ dest[offset + 3] = num & 0xFF;
|
|
|
+ }
|
|
|
+
|
|
|
+ function writeData(dest, offset, data) {
|
|
|
+ var i, ii;
|
|
|
+ if (data instanceof Uint8Array) {
|
|
|
+ dest.set(data, offset);
|
|
|
+ } else if (typeof data === 'string') {
|
|
|
+ for (i = 0, ii = data.length; i < ii; i++) {
|
|
|
+ dest[offset++] = data.charCodeAt(i) & 0xFF;
|
|
|
}
|
|
|
+ } else {
|
|
|
+ // treating everything else as array
|
|
|
+ for (i = 0, ii = data.length; i < ii; i++) {
|
|
|
+ dest[offset++] = data[i] & 0xFF;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- messageHandler.on('GetDoc', function transportDoc(data) {
|
|
|
- var pdfInfo = data.pdfInfo;
|
|
|
- this.numPages = data.pdfInfo.numPages;
|
|
|
- var loadingTask = this.loadingTask;
|
|
|
- var pdfDocument = new PDFDocumentProxy(pdfInfo, this, loadingTask);
|
|
|
- this.pdfDocument = pdfDocument;
|
|
|
- loadingTask._capability.resolve(pdfDocument);
|
|
|
- }, this);
|
|
|
+ function OpenTypeFileBuilder(sfnt) {
|
|
|
+ this.sfnt = sfnt;
|
|
|
+ this.tables = Object.create(null);
|
|
|
+ }
|
|
|
|
|
|
- messageHandler.on('NeedPassword',
|
|
|
- function transportNeedPassword(exception) {
|
|
|
- var loadingTask = this.loadingTask;
|
|
|
- if (loadingTask.onPassword) {
|
|
|
- return loadingTask.onPassword(updatePassword,
|
|
|
- PasswordResponses.NEED_PASSWORD);
|
|
|
- }
|
|
|
- loadingTask._capability.reject(
|
|
|
- new PasswordException(exception.message, exception.code));
|
|
|
- }, this);
|
|
|
+ OpenTypeFileBuilder.getSearchParams =
|
|
|
+ function OpenTypeFileBuilder_getSearchParams(entriesCount, entrySize) {
|
|
|
+ var maxPower2 = 1, log2 = 0;
|
|
|
+ while ((maxPower2 ^ entriesCount) > maxPower2) {
|
|
|
+ maxPower2 <<= 1;
|
|
|
+ log2++;
|
|
|
+ }
|
|
|
+ var searchRange = maxPower2 * entrySize;
|
|
|
+ return {
|
|
|
+ range: searchRange,
|
|
|
+ entry: log2,
|
|
|
+ rangeShift: entrySize * entriesCount - searchRange
|
|
|
+ };
|
|
|
+ };
|
|
|
|
|
|
- messageHandler.on('IncorrectPassword',
|
|
|
- function transportIncorrectPassword(exception) {
|
|
|
- var loadingTask = this.loadingTask;
|
|
|
- if (loadingTask.onPassword) {
|
|
|
- return loadingTask.onPassword(updatePassword,
|
|
|
- PasswordResponses.INCORRECT_PASSWORD);
|
|
|
- }
|
|
|
- loadingTask._capability.reject(
|
|
|
- new PasswordException(exception.message, exception.code));
|
|
|
- }, this);
|
|
|
+ var OTF_HEADER_SIZE = 12;
|
|
|
+ var OTF_TABLE_ENTRY_SIZE = 16;
|
|
|
|
|
|
- messageHandler.on('InvalidPDF', function transportInvalidPDF(exception) {
|
|
|
- this.loadingTask._capability.reject(
|
|
|
- new InvalidPDFException(exception.message));
|
|
|
- }, this);
|
|
|
+ OpenTypeFileBuilder.prototype = {
|
|
|
+ toArray: function OpenTypeFileBuilder_toArray() {
|
|
|
+ var sfnt = this.sfnt;
|
|
|
|
|
|
- messageHandler.on('MissingPDF', function transportMissingPDF(exception) {
|
|
|
- this.loadingTask._capability.reject(
|
|
|
- new MissingPDFException(exception.message));
|
|
|
- }, this);
|
|
|
+ // Tables needs to be written by ascendant alphabetic order
|
|
|
+ var tables = this.tables;
|
|
|
+ var tablesNames = Object.keys(tables);
|
|
|
+ tablesNames.sort();
|
|
|
+ var numTables = tablesNames.length;
|
|
|
|
|
|
- messageHandler.on('UnexpectedResponse',
|
|
|
- function transportUnexpectedResponse(exception) {
|
|
|
- this.loadingTask._capability.reject(
|
|
|
- new UnexpectedResponseException(exception.message, exception.status));
|
|
|
- }, this);
|
|
|
+ var i, j, jj, table, tableName;
|
|
|
+ // layout the tables data
|
|
|
+ var offset = OTF_HEADER_SIZE + numTables * OTF_TABLE_ENTRY_SIZE;
|
|
|
+ var tableOffsets = [offset];
|
|
|
+ for (i = 0; i < numTables; i++) {
|
|
|
+ table = tables[tablesNames[i]];
|
|
|
+ var paddedLength = ((table.length + 3) & ~3) >>> 0;
|
|
|
+ offset += paddedLength;
|
|
|
+ tableOffsets.push(offset);
|
|
|
+ }
|
|
|
|
|
|
- messageHandler.on('UnknownError',
|
|
|
- function transportUnknownError(exception) {
|
|
|
- this.loadingTask._capability.reject(
|
|
|
- new UnknownErrorException(exception.message, exception.details));
|
|
|
- }, this);
|
|
|
+ var file = new Uint8Array(offset);
|
|
|
+ // write the table data first (mostly for checksum)
|
|
|
+ for (i = 0; i < numTables; i++) {
|
|
|
+ table = tables[tablesNames[i]];
|
|
|
+ writeData(file, tableOffsets[i], table);
|
|
|
+ }
|
|
|
|
|
|
- messageHandler.on('DataLoaded', function transportPage(data) {
|
|
|
- this.downloadInfoCapability.resolve(data);
|
|
|
- }, this);
|
|
|
+ // sfnt version (4 bytes)
|
|
|
+ if (sfnt === 'true') {
|
|
|
+ // Windows hates the Mac TrueType sfnt version number
|
|
|
+ sfnt = string32(0x00010000);
|
|
|
+ }
|
|
|
+ file[0] = sfnt.charCodeAt(0) & 0xFF;
|
|
|
+ file[1] = sfnt.charCodeAt(1) & 0xFF;
|
|
|
+ file[2] = sfnt.charCodeAt(2) & 0xFF;
|
|
|
+ file[3] = sfnt.charCodeAt(3) & 0xFF;
|
|
|
|
|
|
- messageHandler.on('PDFManagerReady', function transportPage(data) {
|
|
|
- if (this.pdfDataRangeTransport) {
|
|
|
- this.pdfDataRangeTransport.transportReady();
|
|
|
- }
|
|
|
- }, this);
|
|
|
+ // numTables (2 bytes)
|
|
|
+ writeInt16(file, 4, numTables);
|
|
|
|
|
|
- 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];
|
|
|
+ var searchParams = OpenTypeFileBuilder.getSearchParams(numTables, 16);
|
|
|
|
|
|
- page.stats.timeEnd('Page Request');
|
|
|
- page._startRenderPage(data.transparency, data.intent);
|
|
|
- }, this);
|
|
|
+ // searchRange (2 bytes)
|
|
|
+ writeInt16(file, 6, searchParams.range);
|
|
|
+ // entrySelector (2 bytes)
|
|
|
+ writeInt16(file, 8, searchParams.entry);
|
|
|
+ // rangeShift (2 bytes)
|
|
|
+ writeInt16(file, 10, searchParams.rangeShift);
|
|
|
|
|
|
- messageHandler.on('RenderPageChunk', function transportRender(data) {
|
|
|
- if (this.destroyed) {
|
|
|
- return; // Ignore any pending requests if the worker was terminated.
|
|
|
+ offset = OTF_HEADER_SIZE;
|
|
|
+ // writing table entries
|
|
|
+ for (i = 0; i < numTables; i++) {
|
|
|
+ tableName = tablesNames[i];
|
|
|
+ file[offset] = tableName.charCodeAt(0) & 0xFF;
|
|
|
+ file[offset + 1] = tableName.charCodeAt(1) & 0xFF;
|
|
|
+ file[offset + 2] = tableName.charCodeAt(2) & 0xFF;
|
|
|
+ file[offset + 3] = tableName.charCodeAt(3) & 0xFF;
|
|
|
+
|
|
|
+ // checksum
|
|
|
+ var checksum = 0;
|
|
|
+ for (j = tableOffsets[i], jj = tableOffsets[i + 1]; j < jj; j += 4) {
|
|
|
+ var quad = (file[j] << 24) + (file[j + 1] << 16) +
|
|
|
+ (file[j + 2] << 8) + file[j + 3];
|
|
|
+ checksum = (checksum + quad) | 0;
|
|
|
}
|
|
|
- var page = this.pageCache[data.pageIndex];
|
|
|
+ writeInt32(file, offset + 4, checksum);
|
|
|
|
|
|
- page._renderPageChunk(data.operatorList, data.intent);
|
|
|
- }, this);
|
|
|
+ // offset
|
|
|
+ writeInt32(file, offset + 8, tableOffsets[i]);
|
|
|
+ // length
|
|
|
+ writeInt32(file, offset + 12, tables[tableName].length);
|
|
|
|
|
|
- messageHandler.on('commonobj', function transportObj(data) {
|
|
|
- if (this.destroyed) {
|
|
|
- return; // Ignore any pending requests if the worker was terminated.
|
|
|
- }
|
|
|
+ offset += OTF_TABLE_ENTRY_SIZE;
|
|
|
+ }
|
|
|
+ return file;
|
|
|
+ },
|
|
|
|
|
|
- var id = data[0];
|
|
|
- var type = data[1];
|
|
|
- if (this.commonObjs.hasData(id)) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ addTable: function OpenTypeFileBuilder_addTable(tag, data) {
|
|
|
+ if (tag in this.tables) {
|
|
|
+ throw new Error('Table ' + tag + ' already exists');
|
|
|
+ }
|
|
|
+ this.tables[tag] = data;
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- switch (type) {
|
|
|
- case 'Font':
|
|
|
- var exportedData = data[2];
|
|
|
+ return OpenTypeFileBuilder;
|
|
|
+})();
|
|
|
|
|
|
- var font;
|
|
|
- if ('error' in exportedData) {
|
|
|
- var error = exportedData.error;
|
|
|
- warn('Error during font loading: ' + error);
|
|
|
- this.commonObjs.resolve(id, error);
|
|
|
- break;
|
|
|
- } else {
|
|
|
- font = new FontFaceObject(exportedData);
|
|
|
- }
|
|
|
-
|
|
|
- this.fontLoader.bind(
|
|
|
- [font],
|
|
|
- function fontReady(fontObjs) {
|
|
|
- this.commonObjs.resolve(id, font);
|
|
|
- }.bind(this)
|
|
|
- );
|
|
|
- break;
|
|
|
- case 'FontPath':
|
|
|
- this.commonObjs.resolve(id, data[2]);
|
|
|
- break;
|
|
|
- default:
|
|
|
- error('Got unknown common object type ' + type);
|
|
|
- }
|
|
|
- }, this);
|
|
|
+// 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
|
|
|
+]);
|
|
|
|
|
|
- messageHandler.on('obj', function transportObj(data) {
|
|
|
- if (this.destroyed) {
|
|
|
- return; // Ignore any pending requests if the worker was terminated.
|
|
|
- }
|
|
|
+/**
|
|
|
+ * '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:
|
|
|
+ * var type1Font = new Font("MyFontName", binaryFile, propertiesObject);
|
|
|
+ * type1Font.bind();
|
|
|
+ */
|
|
|
+var Font = (function FontClosure() {
|
|
|
+ function Font(name, file, properties) {
|
|
|
+ var charCode, glyphName, fontChar;
|
|
|
|
|
|
- var id = data[0];
|
|
|
- var pageIndex = data[1];
|
|
|
- var type = data[2];
|
|
|
- var pageProxy = this.pageCache[pageIndex];
|
|
|
- var imageData;
|
|
|
- if (pageProxy.objs.hasData(id)) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ this.name = name;
|
|
|
+ this.loadedName = properties.loadedName;
|
|
|
+ this.isType3Font = properties.isType3Font;
|
|
|
+ this.sizes = [];
|
|
|
|
|
|
- switch (type) {
|
|
|
- case 'JpegStream':
|
|
|
- imageData = data[3];
|
|
|
- loadJpegStream(id, imageData, pageProxy.objs);
|
|
|
- break;
|
|
|
- case 'Image':
|
|
|
- imageData = data[3];
|
|
|
- pageProxy.objs.resolve(id, imageData);
|
|
|
+ this.glyphCache = Object.create(null);
|
|
|
|
|
|
- // heuristics that will allow not to store large data
|
|
|
- var MAX_IMAGE_SIZE_TO_STORE = 8000000;
|
|
|
- if (imageData && 'data' in imageData &&
|
|
|
- imageData.data.length > MAX_IMAGE_SIZE_TO_STORE) {
|
|
|
- pageProxy.cleanupAfterRender = true;
|
|
|
- }
|
|
|
- break;
|
|
|
- default:
|
|
|
- error('Got unknown object type ' + type);
|
|
|
- }
|
|
|
- }, this);
|
|
|
+ var names = name.split('+');
|
|
|
+ names = names.length > 1 ? names[1] : names[0];
|
|
|
+ names = names.split(/[-,_]/g)[0];
|
|
|
+ this.isSerifFont = !!(properties.flags & FontFlags.Serif);
|
|
|
+ this.isSymbolicFont = !!(properties.flags & FontFlags.Symbolic);
|
|
|
+ this.isMonospace = !!(properties.flags & FontFlags.FixedPitch);
|
|
|
|
|
|
- messageHandler.on('DocProgress', function transportDocProgress(data) {
|
|
|
- if (this.destroyed) {
|
|
|
- return; // Ignore any pending requests if the worker was terminated.
|
|
|
- }
|
|
|
+ var type = properties.type;
|
|
|
+ var subtype = properties.subtype;
|
|
|
+ this.type = type;
|
|
|
|
|
|
- var loadingTask = this.loadingTask;
|
|
|
- if (loadingTask.onProgress) {
|
|
|
- loadingTask.onProgress({
|
|
|
- loaded: data.loaded,
|
|
|
- total: data.total
|
|
|
- });
|
|
|
- }
|
|
|
- }, this);
|
|
|
+ this.fallbackName = (this.isMonospace ? 'monospace' :
|
|
|
+ (this.isSerifFont ? 'serif' : 'sans-serif'));
|
|
|
|
|
|
- messageHandler.on('PageError', function transportError(data) {
|
|
|
- if (this.destroyed) {
|
|
|
- return; // Ignore any pending requests if the worker was terminated.
|
|
|
- }
|
|
|
+ this.differences = properties.differences;
|
|
|
+ this.widths = properties.widths;
|
|
|
+ this.defaultWidth = properties.defaultWidth;
|
|
|
+ this.composite = properties.composite;
|
|
|
+ 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;
|
|
|
|
|
|
- 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);
|
|
|
+ this.toUnicode = properties.toUnicode = this.buildToUnicode(properties);
|
|
|
|
|
|
- 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);
|
|
|
+ this.toFontChar = [];
|
|
|
|
|
|
- messageHandler.on('JpegDecode', function(data) {
|
|
|
- if (this.destroyed) {
|
|
|
- return Promise.reject('Worker was terminated');
|
|
|
- }
|
|
|
+ if (properties.type === 'Type3') {
|
|
|
+ for (charCode = 0; charCode < 256; charCode++) {
|
|
|
+ this.toFontChar[charCode] = (this.differences[charCode] ||
|
|
|
+ properties.defaultEncoding[charCode]);
|
|
|
+ }
|
|
|
+ this.fontType = FontType.TYPE3;
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- 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'));
|
|
|
- }
|
|
|
+ this.cidEncoding = properties.cidEncoding;
|
|
|
+ this.vertical = properties.vertical;
|
|
|
+ if (this.vertical) {
|
|
|
+ this.vmetrics = properties.vmetrics;
|
|
|
+ this.defaultVMetrics = properties.defaultVMetrics;
|
|
|
+ }
|
|
|
+ var glyphsUnicodeMap;
|
|
|
+ if (!file || file.isEmpty) {
|
|
|
+ if (file) {
|
|
|
+ // Some bad PDF generators will include empty font files,
|
|
|
+ // attempting to recover by assuming that no file exists.
|
|
|
+ warn('Font file is empty in "' + name + '" (' + this.loadedName + ')');
|
|
|
+ }
|
|
|
|
|
|
- return new Promise(function (resolve, reject) {
|
|
|
- var img = new Image();
|
|
|
- img.onload = function () {
|
|
|
- var width = img.width;
|
|
|
- var height = img.height;
|
|
|
- var size = width * height;
|
|
|
- var rgbaLength = size * 4;
|
|
|
- var buf = new Uint8Array(size * components);
|
|
|
- var tmpCanvas = createScratchCanvas(width, height);
|
|
|
- var tmpCtx = tmpCanvas.getContext('2d');
|
|
|
- tmpCtx.drawImage(img, 0, 0);
|
|
|
- var data = tmpCtx.getImageData(0, 0, width, height).data;
|
|
|
- var i, j;
|
|
|
+ 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 stdFontMap = getStdFontMap(), nonStdFontMap = getNonStdFontMap();
|
|
|
+ var isStandardFont = !!stdFontMap[fontName] ||
|
|
|
+ !!(nonStdFontMap[fontName] && stdFontMap[nonStdFontMap[fontName]]);
|
|
|
+ fontName = stdFontMap[fontName] || nonStdFontMap[fontName] || fontName;
|
|
|
|
|
|
- if (components === 3) {
|
|
|
- for (i = 0, j = 0; i < rgbaLength; i += 4, j += 3) {
|
|
|
- buf[j] = data[i];
|
|
|
- buf[j + 1] = data[i + 1];
|
|
|
- buf[j + 2] = data[i + 2];
|
|
|
- }
|
|
|
- } else if (components === 1) {
|
|
|
- for (i = 0, j = 0; i < rgbaLength; i += 4, j++) {
|
|
|
- buf[j] = data[i];
|
|
|
- }
|
|
|
- }
|
|
|
- resolve({ data: buf, width: width, height: height});
|
|
|
- };
|
|
|
- img.onerror = function () {
|
|
|
- reject(new Error('JpegDecode failed to load image'));
|
|
|
- };
|
|
|
- img.src = imageUrl;
|
|
|
- });
|
|
|
- }, this);
|
|
|
- },
|
|
|
+ this.bold = (fontName.search(/bold/gi) !== -1);
|
|
|
+ this.italic = ((fontName.search(/oblique/gi) !== -1) ||
|
|
|
+ (fontName.search(/italic/gi) !== -1));
|
|
|
|
|
|
- getData: function WorkerTransport_getData() {
|
|
|
- return this.messageHandler.sendWithPromise('GetData', null);
|
|
|
- },
|
|
|
+ // Use 'name' instead of 'fontName' here because the original
|
|
|
+ // name ArialBlack for example will be replaced by Helvetica.
|
|
|
+ this.black = (name.search(/Black/g) !== -1);
|
|
|
|
|
|
- getPage: function WorkerTransport_getPage(pageNumber, capability) {
|
|
|
- if (pageNumber <= 0 || pageNumber > this.numPages ||
|
|
|
- (pageNumber|0) !== pageNumber) {
|
|
|
- return Promise.reject(new Error('Invalid page request'));
|
|
|
+ // if at least one width is present, remeasure all chars when exists
|
|
|
+ this.remeasure = Object.keys(this.widths).length > 0;
|
|
|
+ if (isStandardFont && type === 'CIDFontType2' &&
|
|
|
+ properties.cidEncoding.indexOf('Identity-') === 0) {
|
|
|
+ var GlyphMapForStandardFonts = getGlyphMapForStandardFonts();
|
|
|
+ // Standard fonts might be embedded as CID font without glyph mapping.
|
|
|
+ // Building one based on GlyphMapForStandardFonts.
|
|
|
+ var map = [];
|
|
|
+ for (charCode in GlyphMapForStandardFonts) {
|
|
|
+ map[+charCode] = GlyphMapForStandardFonts[charCode];
|
|
|
+ }
|
|
|
+ if (/ArialBlack/i.test(name)) {
|
|
|
+ var SupplementalGlyphMapForArialBlack =
|
|
|
+ getSupplementalGlyphMapForArialBlack();
|
|
|
+ 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;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ this.toFontChar = map;
|
|
|
+ this.toUnicode = new ToUnicodeMap(map);
|
|
|
+ } else if (/Symbol/i.test(fontName)) {
|
|
|
+ var symbols = SymbolSetEncoding;
|
|
|
+ glyphsUnicodeMap = getGlyphsUnicode();
|
|
|
+ for (charCode in symbols) {
|
|
|
+ fontChar = glyphsUnicodeMap[symbols[charCode]];
|
|
|
+ if (!fontChar) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ this.toFontChar[charCode] = fontChar;
|
|
|
+ }
|
|
|
+ for (charCode in properties.differences) {
|
|
|
+ fontChar = glyphsUnicodeMap[properties.differences[charCode]];
|
|
|
+ if (!fontChar) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ this.toFontChar[charCode] = fontChar;
|
|
|
+ }
|
|
|
+ } else if (/Dingbats/i.test(fontName)) {
|
|
|
+ glyphsUnicodeMap = getDingbatsGlyphsUnicode();
|
|
|
+ if (/Wingdings/i.test(name)) {
|
|
|
+ warn('Wingdings font without embedded font file, ' +
|
|
|
+ 'falling back to the ZapfDingbats encoding.');
|
|
|
+ }
|
|
|
+ var dingbats = ZapfDingbatsEncoding;
|
|
|
+ for (charCode in dingbats) {
|
|
|
+ fontChar = glyphsUnicodeMap[dingbats[charCode]];
|
|
|
+ if (!fontChar) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ this.toFontChar[charCode] = fontChar;
|
|
|
+ }
|
|
|
+ for (charCode in properties.differences) {
|
|
|
+ fontChar = glyphsUnicodeMap[properties.differences[charCode]];
|
|
|
+ if (!fontChar) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ this.toFontChar[charCode] = fontChar;
|
|
|
+ }
|
|
|
+ } else if (isStandardFont) {
|
|
|
+ this.toFontChar = [];
|
|
|
+ glyphsUnicodeMap = getGlyphsUnicode();
|
|
|
+ for (charCode in properties.defaultEncoding) {
|
|
|
+ glyphName = (properties.differences[charCode] ||
|
|
|
+ properties.defaultEncoding[charCode]);
|
|
|
+ this.toFontChar[charCode] = glyphsUnicodeMap[glyphName];
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ var unicodeCharCode, notCidFont = (type.indexOf('CIDFontType') === -1);
|
|
|
+ glyphsUnicodeMap = getGlyphsUnicode();
|
|
|
+ this.toUnicode.forEach(function(charCode, unicodeCharCode) {
|
|
|
+ if (notCidFont) {
|
|
|
+ glyphName = (properties.differences[charCode] ||
|
|
|
+ properties.defaultEncoding[charCode]);
|
|
|
+ unicodeCharCode = (glyphsUnicodeMap[glyphName] || unicodeCharCode);
|
|
|
+ }
|
|
|
+ this.toFontChar[charCode] = unicodeCharCode;
|
|
|
+ }.bind(this));
|
|
|
}
|
|
|
+ this.loadedName = fontName.split('-')[0];
|
|
|
+ this.loading = false;
|
|
|
+ this.fontType = getFontType(type, subtype);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- var pageIndex = pageNumber - 1;
|
|
|
- if (pageIndex in this.pagePromises) {
|
|
|
- return this.pagePromises[pageIndex];
|
|
|
+ // Some fonts might use wrong font types for Type1C or CIDFontType0C
|
|
|
+ if (subtype === 'Type1C' && (type !== 'Type1' && type !== 'MMType1')) {
|
|
|
+ // Some TrueType fonts by mistake claim Type1C
|
|
|
+ if (isTrueTypeFile(file)) {
|
|
|
+ subtype = 'TrueType';
|
|
|
+ } else {
|
|
|
+ type = 'Type1';
|
|
|
}
|
|
|
- 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;
|
|
|
- return promise;
|
|
|
- },
|
|
|
+ }
|
|
|
+ if (subtype === 'CIDFontType0C' && type !== 'CIDFontType0') {
|
|
|
+ type = 'CIDFontType0';
|
|
|
+ }
|
|
|
+ if (subtype === 'OpenType') {
|
|
|
+ type = 'OpenType';
|
|
|
+ }
|
|
|
+ // Some CIDFontType0C fonts by mistake claim CIDFontType0.
|
|
|
+ if (type === 'CIDFontType0') {
|
|
|
+ if (isType1File(file)) {
|
|
|
+ subtype = 'CIDFontType0';
|
|
|
+ } else if (isOpenTypeFile(file)) {
|
|
|
+ // Sometimes the type/subtype can be a complete lie (see issue6782.pdf).
|
|
|
+ type = subtype = 'OpenType';
|
|
|
+ } else {
|
|
|
+ subtype = 'CIDFontType0C';
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- getPageIndex: function WorkerTransport_getPageIndexByRef(ref) {
|
|
|
- return this.messageHandler.sendWithPromise('GetPageIndex', { ref: ref });
|
|
|
- },
|
|
|
+ var data;
|
|
|
+ switch (type) {
|
|
|
+ case 'MMType1':
|
|
|
+ info('MMType1 font (' + name + '), falling back to Type1.');
|
|
|
+ /* falls through */
|
|
|
+ case 'Type1':
|
|
|
+ case 'CIDFontType0':
|
|
|
+ this.mimetype = 'font/opentype';
|
|
|
|
|
|
- getAnnotations: function WorkerTransport_getAnnotations(pageIndex, intent) {
|
|
|
- return this.messageHandler.sendWithPromise('GetAnnotations', {
|
|
|
- pageIndex: pageIndex,
|
|
|
- intent: intent,
|
|
|
- });
|
|
|
- },
|
|
|
+ var cff = (subtype === 'Type1C' || subtype === 'CIDFontType0C') ?
|
|
|
+ new CFFFont(file, properties) : new Type1Font(name, file, properties);
|
|
|
|
|
|
- getDestinations: function WorkerTransport_getDestinations() {
|
|
|
- return this.messageHandler.sendWithPromise('GetDestinations', null);
|
|
|
- },
|
|
|
+ adjustWidths(properties);
|
|
|
|
|
|
- getDestination: function WorkerTransport_getDestination(id) {
|
|
|
- return this.messageHandler.sendWithPromise('GetDestination', { id: id });
|
|
|
- },
|
|
|
+ // Wrap the CFF data inside an OTF font file
|
|
|
+ data = this.convert(name, cff, properties);
|
|
|
+ break;
|
|
|
|
|
|
- getPageLabels: function WorkerTransport_getPageLabels() {
|
|
|
- return this.messageHandler.sendWithPromise('GetPageLabels', null);
|
|
|
- },
|
|
|
+ case 'OpenType':
|
|
|
+ case 'TrueType':
|
|
|
+ case 'CIDFontType2':
|
|
|
+ this.mimetype = 'font/opentype';
|
|
|
|
|
|
- getAttachments: function WorkerTransport_getAttachments() {
|
|
|
- return this.messageHandler.sendWithPromise('GetAttachments', null);
|
|
|
- },
|
|
|
+ // 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);
|
|
|
|
|
|
- getJavaScript: function WorkerTransport_getJavaScript() {
|
|
|
- return this.messageHandler.sendWithPromise('GetJavaScript', null);
|
|
|
- },
|
|
|
+ type = 'OpenType';
|
|
|
+ }
|
|
|
+ break;
|
|
|
|
|
|
- getOutline: function WorkerTransport_getOutline() {
|
|
|
- return this.messageHandler.sendWithPromise('GetOutline', null);
|
|
|
- },
|
|
|
+ default:
|
|
|
+ error('Font ' + type + ' is not supported');
|
|
|
+ break;
|
|
|
+ }
|
|
|
|
|
|
- getMetadata: function WorkerTransport_getMetadata() {
|
|
|
- return this.messageHandler.sendWithPromise('GetMetadata', null).
|
|
|
- then(function transportMetadata(results) {
|
|
|
- return {
|
|
|
- info: results[0],
|
|
|
- metadata: (results[1] ? new Metadata(results[1]) : null)
|
|
|
- };
|
|
|
- });
|
|
|
- },
|
|
|
+ this.data = data;
|
|
|
+ this.fontType = getFontType(type, subtype);
|
|
|
|
|
|
- getStats: function WorkerTransport_getStats() {
|
|
|
- return this.messageHandler.sendWithPromise('GetStats', null);
|
|
|
- },
|
|
|
+ // Transfer some properties again that could change during font conversion
|
|
|
+ this.fontMatrix = properties.fontMatrix;
|
|
|
+ this.widths = properties.widths;
|
|
|
+ this.defaultWidth = properties.defaultWidth;
|
|
|
+ this.encoding = properties.baseEncoding;
|
|
|
+ this.seacMap = properties.seacMap;
|
|
|
|
|
|
- startCleanup: function WorkerTransport_startCleanup() {
|
|
|
- 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.cleanup();
|
|
|
- }
|
|
|
- }
|
|
|
- this.commonObjs.clear();
|
|
|
- this.fontLoader.clear();
|
|
|
- }.bind(this));
|
|
|
- }
|
|
|
- };
|
|
|
- return WorkerTransport;
|
|
|
+ this.loading = true;
|
|
|
+ }
|
|
|
|
|
|
-})();
|
|
|
+ Font.getFontID = (function () {
|
|
|
+ var ID = 1;
|
|
|
+ return function Font_getFontID() {
|
|
|
+ return String(ID++);
|
|
|
+ };
|
|
|
+ })();
|
|
|
|
|
|
-/**
|
|
|
- * A PDF document and page is built of many objects. E.g. there are objects
|
|
|
- * for fonts, images, rendering code and such. These objects might get processed
|
|
|
- * inside of a worker. The `PDFObjects` implements some basic functions to
|
|
|
- * manage these objects.
|
|
|
- * @ignore
|
|
|
- */
|
|
|
-var PDFObjects = (function PDFObjectsClosure() {
|
|
|
- function PDFObjects() {
|
|
|
- this.objs = Object.create(null);
|
|
|
+ function int16(b0, b1) {
|
|
|
+ return (b0 << 8) + b1;
|
|
|
}
|
|
|
|
|
|
- PDFObjects.prototype = {
|
|
|
- /**
|
|
|
- * Internal function.
|
|
|
- * Ensures there is an object defined for `objId`.
|
|
|
- */
|
|
|
- ensureObj: function PDFObjects_ensureObj(objId) {
|
|
|
- if (this.objs[objId]) {
|
|
|
- return this.objs[objId];
|
|
|
- }
|
|
|
+ function int32(b0, b1, b2, b3) {
|
|
|
+ return (b0 << 24) + (b1 << 16) + (b2 << 8) + b3;
|
|
|
+ }
|
|
|
|
|
|
- var obj = {
|
|
|
- capability: createPromiseCapability(),
|
|
|
- data: null,
|
|
|
- resolved: false
|
|
|
- };
|
|
|
- this.objs[objId] = obj;
|
|
|
+ function string16(value) {
|
|
|
+ return String.fromCharCode((value >> 8) & 0xff, value & 0xff);
|
|
|
+ }
|
|
|
|
|
|
- return obj;
|
|
|
- },
|
|
|
+ function safeString16(value) {
|
|
|
+ // clamp value to the 16-bit int range
|
|
|
+ value = (value > 0x7FFF ? 0x7FFF : (value < -0x8000 ? -0x8000 : value));
|
|
|
+ return String.fromCharCode((value >> 8) & 0xff, value & 0xff);
|
|
|
+ }
|
|
|
|
|
|
- /**
|
|
|
- * If called *without* callback, this returns the data of `objId` but the
|
|
|
- * object needs to be resolved. If it isn't, this function throws.
|
|
|
- *
|
|
|
- * If called *with* a callback, the callback is called with the data of the
|
|
|
- * object once the object is resolved. That means, if you call this
|
|
|
- * function and the object is already resolved, the callback gets called
|
|
|
- * right away.
|
|
|
- */
|
|
|
- get: function PDFObjects_get(objId, callback) {
|
|
|
- // If there is a callback, then the get can be async and the object is
|
|
|
- // not required to be resolved right now
|
|
|
- if (callback) {
|
|
|
- this.ensureObj(objId).capability.promise.then(callback);
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- // If there isn't a callback, the user expects to get the resolved data
|
|
|
- // directly.
|
|
|
- var obj = this.objs[objId];
|
|
|
-
|
|
|
- // If there isn't an object yet or the object isn't resolved, then the
|
|
|
- // data isn't ready yet!
|
|
|
- if (!obj || !obj.resolved) {
|
|
|
- error('Requesting object that isn\'t resolved yet ' + objId);
|
|
|
- }
|
|
|
-
|
|
|
- return obj.data;
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * Resolves the object `objId` with optional `data`.
|
|
|
- */
|
|
|
- resolve: function PDFObjects_resolve(objId, data) {
|
|
|
- var obj = this.ensureObj(objId);
|
|
|
+ function isTrueTypeFile(file) {
|
|
|
+ var header = file.peekBytes(4);
|
|
|
+ return readUint32(header, 0) === 0x00010000;
|
|
|
+ }
|
|
|
|
|
|
- obj.resolved = true;
|
|
|
- obj.data = data;
|
|
|
- obj.capability.resolve(data);
|
|
|
- },
|
|
|
+ function isOpenTypeFile(file) {
|
|
|
+ var header = file.peekBytes(4);
|
|
|
+ return bytesToString(header) === 'OTTO';
|
|
|
+ }
|
|
|
|
|
|
- isResolved: function PDFObjects_isResolved(objId) {
|
|
|
- var objs = this.objs;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
|
|
|
- if (!objs[objId]) {
|
|
|
- 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 {
|
|
|
- return objs[objId].resolved;
|
|
|
+ i = c;
|
|
|
}
|
|
|
- },
|
|
|
-
|
|
|
- hasData: function PDFObjects_hasData(objId) {
|
|
|
- return this.isResolved(objId);
|
|
|
- },
|
|
|
+ }
|
|
|
+ // Even index means code in problematic range.
|
|
|
+ return !(i & 1);
|
|
|
+ }
|
|
|
|
|
|
- /**
|
|
|
- * Returns the data of `objId` if object exists, null otherwise.
|
|
|
- */
|
|
|
- getData: function PDFObjects_getData(objId) {
|
|
|
- var objs = this.objs;
|
|
|
- if (!objs[objId] || !objs[objId].resolved) {
|
|
|
- return null;
|
|
|
- } else {
|
|
|
- return objs[objId].data;
|
|
|
+ /**
|
|
|
+ * 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
|
|
|
+ * from commands such as show text) to the char codes that will be used in the
|
|
|
+ * font that we build
|
|
|
+ * 'charCodeToGlyphId' - maps the new font char codes to glyph ids
|
|
|
+ */
|
|
|
+ function adjustMapping(charCodeToGlyphId, properties) {
|
|
|
+ var toUnicode = properties.toUnicode;
|
|
|
+ var isSymbolic = !!(properties.flags & FontFlags.Symbolic);
|
|
|
+ var isIdentityUnicode =
|
|
|
+ properties.toUnicode instanceof IdentityToUnicodeMap;
|
|
|
+ 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 && 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 ||
|
|
|
+ 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++;
|
|
|
|
|
|
- clear: function PDFObjects_clear() {
|
|
|
- this.objs = Object.create(null);
|
|
|
- }
|
|
|
- };
|
|
|
- return PDFObjects;
|
|
|
-})();
|
|
|
+ if (SKIP_PRIVATE_USE_RANGE_F000_TO_F01F && fontCharCode === 0xF000) {
|
|
|
+ fontCharCode = 0xF020;
|
|
|
+ nextAvailableFontCharCode = fontCharCode + 1;
|
|
|
+ }
|
|
|
|
|
|
-/**
|
|
|
- * Allows controlling of the rendering tasks.
|
|
|
- * @class
|
|
|
- * @alias RenderTask
|
|
|
- */
|
|
|
-var RenderTask = (function RenderTaskClosure() {
|
|
|
- function RenderTask(internalRenderTask) {
|
|
|
- this._internalRenderTask = internalRenderTask;
|
|
|
+ } while (usedFontCharCodes[fontCharCode] !== undefined &&
|
|
|
+ nextAvailableFontCharCode <= PRIVATE_USE_OFFSET_END);
|
|
|
+ }
|
|
|
|
|
|
- /**
|
|
|
- * 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.onContinue = null;
|
|
|
+ newMap[fontCharCode] = glyphId;
|
|
|
+ toFontChar[originalCharCode] = fontCharCode;
|
|
|
+ usedFontCharCodes[fontCharCode] = true;
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ toFontChar: toFontChar,
|
|
|
+ charCodeToGlyphId: newMap,
|
|
|
+ nextAvailableFontCharCode: nextAvailableFontCharCode
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
- 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();
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * 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.apply(this.promise, arguments);
|
|
|
+ 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] });
|
|
|
}
|
|
|
- };
|
|
|
-
|
|
|
- return RenderTask;
|
|
|
-})();
|
|
|
+ codes.sort(function fontGetRangesSort(a, b) {
|
|
|
+ return a.fontCharCode - b.fontCharCode;
|
|
|
+ });
|
|
|
|
|
|
-/**
|
|
|
- * For internal use only.
|
|
|
- * @ignore
|
|
|
- */
|
|
|
-var InternalRenderTask = (function InternalRenderTaskClosure() {
|
|
|
+ // Split the sorted codes into ranges.
|
|
|
+ var ranges = [];
|
|
|
+ var length = codes.length;
|
|
|
+ for (var n = 0; n < length; ) {
|
|
|
+ var start = codes[n].fontCharCode;
|
|
|
+ var codeIndices = [codes[n].glyphId];
|
|
|
+ ++n;
|
|
|
+ var end = start;
|
|
|
+ while (n < length && end + 1 === codes[n].fontCharCode) {
|
|
|
+ codeIndices.push(codes[n].glyphId);
|
|
|
+ ++end;
|
|
|
+ ++n;
|
|
|
+ if (end === 0xFFFF) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ranges.push([start, end, codeIndices]);
|
|
|
+ }
|
|
|
|
|
|
- function InternalRenderTask(callback, params, objs, commonObjs, operatorList,
|
|
|
- pageNumber) {
|
|
|
- this.callback = callback;
|
|
|
- this.params = params;
|
|
|
- this.objs = objs;
|
|
|
- this.commonObjs = commonObjs;
|
|
|
- this.operatorListIdx = null;
|
|
|
- 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);
|
|
|
+ return ranges;
|
|
|
}
|
|
|
|
|
|
- InternalRenderTask.prototype = {
|
|
|
+ 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
|
|
|
+ string32(4 + numTables * 8); // start of the table record
|
|
|
|
|
|
- initalizeGraphics:
|
|
|
- function InternalRenderTask_initalizeGraphics(transparency) {
|
|
|
+ var i, ii, j, jj;
|
|
|
+ for (i = ranges.length - 1; i >= 0; --i) {
|
|
|
+ if (ranges[i][0] <= 0xFFFF) { break; }
|
|
|
+ }
|
|
|
+ var bmpLength = i + 1;
|
|
|
|
|
|
- if (this.cancelled) {
|
|
|
- return;
|
|
|
- }
|
|
|
- if (PDFJS.pdfBug && 'StepperManager' in globalScope &&
|
|
|
- globalScope.StepperManager.enabled) {
|
|
|
- this.stepper = globalScope.StepperManager.create(this.pageNumber - 1);
|
|
|
- this.stepper.init(this.operatorList);
|
|
|
- this.stepper.nextBreakPoint = this.stepper.getNextBreakPoint();
|
|
|
- }
|
|
|
+ if (ranges[i][0] < 0xFFFF && ranges[i][1] === 0xFFFF) {
|
|
|
+ ranges[i][1] = 0xFFFE;
|
|
|
+ }
|
|
|
+ var trailingRangesCount = ranges[i][1] < 0xFFFF ? 1 : 0;
|
|
|
+ var segCount = bmpLength + trailingRangesCount;
|
|
|
+ var searchParams = OpenTypeFileBuilder.getSearchParams(segCount, 2);
|
|
|
|
|
|
- var params = this.params;
|
|
|
- this.gfx = new CanvasGraphics(params.canvasContext, this.commonObjs,
|
|
|
- this.objs, params.imageLayer);
|
|
|
+ // Fill up the 4 parallel arrays describing the segments.
|
|
|
+ var startCount = '';
|
|
|
+ var endCount = '';
|
|
|
+ var idDeltas = '';
|
|
|
+ var idRangeOffsets = '';
|
|
|
+ var glyphsIds = '';
|
|
|
+ var bias = 0;
|
|
|
|
|
|
- this.gfx.beginDrawing(params.transform, params.viewport, transparency);
|
|
|
- this.operatorListIdx = 0;
|
|
|
- this.graphicsReady = true;
|
|
|
- if (this.graphicsReadyCallback) {
|
|
|
- this.graphicsReadyCallback();
|
|
|
+ var range, start, end, codes;
|
|
|
+ for (i = 0, ii = bmpLength; i < ii; i++) {
|
|
|
+ range = ranges[i];
|
|
|
+ start = range[0];
|
|
|
+ end = range[1];
|
|
|
+ startCount += string16(start);
|
|
|
+ endCount += string16(end);
|
|
|
+ codes = range[2];
|
|
|
+ var contiguous = true;
|
|
|
+ for (j = 1, jj = codes.length; j < jj; ++j) {
|
|
|
+ if (codes[j] !== codes[j - 1] + 1) {
|
|
|
+ contiguous = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
}
|
|
|
- },
|
|
|
+ if (!contiguous) {
|
|
|
+ var offset = (segCount - i) * 2 + bias * 2;
|
|
|
+ bias += (end - start + 1);
|
|
|
|
|
|
- cancel: function InternalRenderTask_cancel() {
|
|
|
- this.running = false;
|
|
|
- this.cancelled = true;
|
|
|
- this.callback('cancelled');
|
|
|
- },
|
|
|
+ idDeltas += string16(0);
|
|
|
+ idRangeOffsets += string16(offset);
|
|
|
|
|
|
- operatorListChanged: function InternalRenderTask_operatorListChanged() {
|
|
|
- if (!this.graphicsReady) {
|
|
|
- if (!this.graphicsReadyCallback) {
|
|
|
- this.graphicsReadyCallback = this._continueBound;
|
|
|
+ for (j = 0, jj = codes.length; j < jj; ++j) {
|
|
|
+ glyphsIds += string16(codes[j]);
|
|
|
}
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (this.stepper) {
|
|
|
- this.stepper.updateOperatorList(this.operatorList);
|
|
|
- }
|
|
|
+ } else {
|
|
|
+ var startCode = codes[0];
|
|
|
|
|
|
- if (this.running) {
|
|
|
- return;
|
|
|
+ idDeltas += string16((startCode - start) & 0xFFFF);
|
|
|
+ idRangeOffsets += string16(0);
|
|
|
}
|
|
|
- this._continue();
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
- _continue: function InternalRenderTask__continue() {
|
|
|
- this.running = true;
|
|
|
- if (this.cancelled) {
|
|
|
- return;
|
|
|
- }
|
|
|
- if (this.task.onContinue) {
|
|
|
- this.task.onContinue.call(this.task, this._scheduleNextBound);
|
|
|
- } else {
|
|
|
- this._scheduleNext();
|
|
|
- }
|
|
|
- },
|
|
|
+ if (trailingRangesCount > 0) {
|
|
|
+ endCount += '\xFF\xFF';
|
|
|
+ startCount += '\xFF\xFF';
|
|
|
+ idDeltas += '\x00\x01';
|
|
|
+ idRangeOffsets += '\x00\x00';
|
|
|
+ }
|
|
|
|
|
|
- _scheduleNext: function InternalRenderTask__scheduleNext() {
|
|
|
- if (this.useRequestAnimationFrame) {
|
|
|
- window.requestAnimationFrame(this._nextBound);
|
|
|
- } else {
|
|
|
- Promise.resolve(undefined).then(this._nextBound);
|
|
|
- }
|
|
|
- },
|
|
|
+ var format314 = '\x00\x00' + // language
|
|
|
+ string16(2 * segCount) +
|
|
|
+ string16(searchParams.range) +
|
|
|
+ string16(searchParams.entry) +
|
|
|
+ string16(searchParams.rangeShift) +
|
|
|
+ endCount + '\x00\x00' + startCount +
|
|
|
+ idDeltas + idRangeOffsets + glyphsIds;
|
|
|
|
|
|
- _next: function InternalRenderTask__next() {
|
|
|
- if (this.cancelled) {
|
|
|
- return;
|
|
|
- }
|
|
|
- this.operatorListIdx = this.gfx.executeOperatorList(this.operatorList,
|
|
|
- this.operatorListIdx,
|
|
|
- this._continueBound,
|
|
|
- this.stepper);
|
|
|
- if (this.operatorListIdx === this.operatorList.argsArray.length) {
|
|
|
- this.running = false;
|
|
|
- if (this.operatorList.lastChunk) {
|
|
|
- this.gfx.endDrawing();
|
|
|
- this.callback();
|
|
|
+ var format31012 = '';
|
|
|
+ var header31012 = '';
|
|
|
+ if (numTables > 1) {
|
|
|
+ cmap += '\x00\x03' + // platformID
|
|
|
+ '\x00\x0A' + // encodingID
|
|
|
+ string32(4 + numTables * 8 +
|
|
|
+ 4 + format314.length); // start of the table record
|
|
|
+ format31012 = '';
|
|
|
+ for (i = 0, ii = ranges.length; i < ii; i++) {
|
|
|
+ range = ranges[i];
|
|
|
+ start = range[0];
|
|
|
+ codes = range[2];
|
|
|
+ var code = codes[0];
|
|
|
+ for (j = 1, jj = codes.length; j < jj; ++j) {
|
|
|
+ if (codes[j] !== codes[j - 1] + 1) {
|
|
|
+ end = range[0] + j - 1;
|
|
|
+ format31012 += string32(start) + // startCharCode
|
|
|
+ string32(end) + // endCharCode
|
|
|
+ string32(code); // startGlyphID
|
|
|
+ start = end + 1;
|
|
|
+ code = codes[j];
|
|
|
+ }
|
|
|
}
|
|
|
+ format31012 += string32(start) + // startCharCode
|
|
|
+ string32(range[1]) + // endCharCode
|
|
|
+ string32(code); // startGlyphID
|
|
|
}
|
|
|
+ header31012 = '\x00\x0C' + // format
|
|
|
+ '\x00\x00' + // reserved
|
|
|
+ string32(format31012.length + 16) + // length
|
|
|
+ '\x00\x00\x00\x00' + // language
|
|
|
+ string32(format31012.length / 12); // nGroups
|
|
|
}
|
|
|
|
|
|
- };
|
|
|
-
|
|
|
- return InternalRenderTask;
|
|
|
-})();
|
|
|
+ return cmap + '\x00\x04' + // format
|
|
|
+ string16(format314.length + 4) + // length
|
|
|
+ format314 + header31012 + format31012;
|
|
|
+ }
|
|
|
|
|
|
-/**
|
|
|
- * (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);
|
|
|
- }
|
|
|
+ function validateOS2Table(os2) {
|
|
|
+ var stream = new Stream(os2.data);
|
|
|
+ var version = stream.getUint16();
|
|
|
+ // TODO verify all OS/2 tables fields, but currently we validate only those
|
|
|
+ // that give us issues
|
|
|
+ stream.getBytes(60); // skipping type, misc sizes, panose, unicode ranges
|
|
|
+ var selection = stream.getUint16();
|
|
|
+ if (version < 4 && (selection & 0x0300)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ var firstChar = stream.getUint16();
|
|
|
+ var lastChar = stream.getUint16();
|
|
|
+ if (firstChar > lastChar) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ stream.getBytes(6); // skipping sTypoAscender/Descender/LineGap
|
|
|
+ var usWinAscent = stream.getUint16();
|
|
|
+ if (usWinAscent === 0) { // makes font unreadable by windows
|
|
|
+ return false;
|
|
|
}
|
|
|
- };
|
|
|
-})();
|
|
|
|
|
|
-exports.getDocument = PDFJS.getDocument;
|
|
|
-exports.PDFDataRangeTransport = PDFDataRangeTransport;
|
|
|
-exports.PDFDocumentProxy = PDFDocumentProxy;
|
|
|
-exports.PDFPageProxy = PDFPageProxy;
|
|
|
-}));
|
|
|
+ // OS/2 appears to be valid, resetting some fields
|
|
|
+ os2.data[8] = os2.data[9] = 0; // IE rejects fonts if fsType != 0
|
|
|
+ return true;
|
|
|
+ }
|
|
|
|
|
|
+ function createOS2Table(properties, charstrings, override) {
|
|
|
+ override = override || {
|
|
|
+ unitsPerEm: 0,
|
|
|
+ yMax: 0,
|
|
|
+ yMin: 0,
|
|
|
+ ascent: 0,
|
|
|
+ descent: 0
|
|
|
+ };
|
|
|
|
|
|
-(function (root, factory) {
|
|
|
- {
|
|
|
- factory((root.pdfjsCoreFonts = {}), root.pdfjsSharedUtil,
|
|
|
- root.pdfjsCorePrimitives, root.pdfjsCoreStream, root.pdfjsCoreParser,
|
|
|
- root.pdfjsCoreCMap, root.pdfjsCoreGlyphList, root.pdfjsCoreCharsets,
|
|
|
- root.pdfjsCoreFontRenderer, root.pdfjsCoreEncodings,
|
|
|
- root.pdfjsCoreStandardFonts, root.pdfjsCoreUnicode);
|
|
|
- }
|
|
|
-}(this, function (exports, sharedUtil, corePrimitives, coreStream, coreParser,
|
|
|
- coreCMap, coreGlyphList, coreCharsets, coreFontRenderer,
|
|
|
- coreEncodings, coreStandardFonts, coreUnicode) {
|
|
|
+ var ulUnicodeRange1 = 0;
|
|
|
+ var ulUnicodeRange2 = 0;
|
|
|
+ var ulUnicodeRange3 = 0;
|
|
|
+ var ulUnicodeRange4 = 0;
|
|
|
|
|
|
-var FONT_IDENTITY_MATRIX = sharedUtil.FONT_IDENTITY_MATRIX;
|
|
|
-var FontType = sharedUtil.FontType;
|
|
|
-var Util = sharedUtil.Util;
|
|
|
-var assert = sharedUtil.assert;
|
|
|
-var bytesToString = sharedUtil.bytesToString;
|
|
|
-var error = sharedUtil.error;
|
|
|
-var info = sharedUtil.info;
|
|
|
-var isArray = sharedUtil.isArray;
|
|
|
-var isInt = sharedUtil.isInt;
|
|
|
-var isNum = sharedUtil.isNum;
|
|
|
-var readUint32 = sharedUtil.readUint32;
|
|
|
-var shadow = sharedUtil.shadow;
|
|
|
-var stringToBytes = sharedUtil.stringToBytes;
|
|
|
-var string32 = sharedUtil.string32;
|
|
|
-var warn = sharedUtil.warn;
|
|
|
-var Name = corePrimitives.Name;
|
|
|
-var Stream = coreStream.Stream;
|
|
|
-var Lexer = coreParser.Lexer;
|
|
|
-var CMapFactory = coreCMap.CMapFactory;
|
|
|
-var IdentityCMap = coreCMap.IdentityCMap;
|
|
|
-var getGlyphsUnicode = coreGlyphList.getGlyphsUnicode;
|
|
|
-var getDingbatsGlyphsUnicode = coreGlyphList.getDingbatsGlyphsUnicode;
|
|
|
-var ISOAdobeCharset = coreCharsets.ISOAdobeCharset;
|
|
|
-var ExpertCharset = coreCharsets.ExpertCharset;
|
|
|
-var ExpertSubsetCharset = coreCharsets.ExpertSubsetCharset;
|
|
|
-var FontRendererFactory = coreFontRenderer.FontRendererFactory;
|
|
|
-var WinAnsiEncoding = coreEncodings.WinAnsiEncoding;
|
|
|
-var StandardEncoding = coreEncodings.StandardEncoding;
|
|
|
-var MacRomanEncoding = coreEncodings.MacRomanEncoding;
|
|
|
-var SymbolSetEncoding = coreEncodings.SymbolSetEncoding;
|
|
|
-var ZapfDingbatsEncoding = coreEncodings.ZapfDingbatsEncoding;
|
|
|
-var ExpertEncoding = coreEncodings.ExpertEncoding;
|
|
|
-var getEncoding = coreEncodings.getEncoding;
|
|
|
-var getStdFontMap = coreStandardFonts.getStdFontMap;
|
|
|
-var getNonStdFontMap = coreStandardFonts.getNonStdFontMap;
|
|
|
-var getGlyphMapForStandardFonts = coreStandardFonts.getGlyphMapForStandardFonts;
|
|
|
-var getSupplementalGlyphMapForArialBlack =
|
|
|
- coreStandardFonts.getSupplementalGlyphMapForArialBlack;
|
|
|
-var getUnicodeRangeFor = coreUnicode.getUnicodeRangeFor;
|
|
|
-var mapSpecialUnicodeValues = coreUnicode.mapSpecialUnicodeValues;
|
|
|
-
|
|
|
-// Unicode Private Use Area
|
|
|
-var PRIVATE_USE_OFFSET_START = 0xE000;
|
|
|
-var PRIVATE_USE_OFFSET_END = 0xF8FF;
|
|
|
-var SKIP_PRIVATE_USE_RANGE_F000_TO_F01F = false;
|
|
|
-
|
|
|
-// PDF Glyph Space Units are one Thousandth of a TextSpace Unit
|
|
|
-// except for Type 3 fonts
|
|
|
-var PDF_GLYPH_SPACE_UNITS = 1000;
|
|
|
-
|
|
|
-// Hinting is currently disabled due to unknown problems on windows
|
|
|
-// in tracemonkey and various other pdfs with type1 fonts.
|
|
|
-var HINTING_ENABLED = false;
|
|
|
+ var firstCharIndex = null;
|
|
|
+ var lastCharIndex = 0;
|
|
|
|
|
|
-// Accented charactars are not displayed properly on windows, using this flag
|
|
|
-// to control analysis of seac charstrings.
|
|
|
-var SEAC_ANALYSIS_ENABLED = false;
|
|
|
+ if (charstrings) {
|
|
|
+ for (var code in charstrings) {
|
|
|
+ code |= 0;
|
|
|
+ if (firstCharIndex > code || !firstCharIndex) {
|
|
|
+ firstCharIndex = code;
|
|
|
+ }
|
|
|
+ if (lastCharIndex < code) {
|
|
|
+ lastCharIndex = code;
|
|
|
+ }
|
|
|
|
|
|
-// Maximum subroutine call depth of type 2 chartrings. Matches OTS.
|
|
|
-var MAX_SUBR_NESTING = 10;
|
|
|
+ var position = getUnicodeRangeFor(code);
|
|
|
+ if (position < 32) {
|
|
|
+ ulUnicodeRange1 |= 1 << position;
|
|
|
+ } else if (position < 64) {
|
|
|
+ ulUnicodeRange2 |= 1 << position - 32;
|
|
|
+ } else if (position < 96) {
|
|
|
+ ulUnicodeRange3 |= 1 << position - 64;
|
|
|
+ } else if (position < 123) {
|
|
|
+ ulUnicodeRange4 |= 1 << position - 96;
|
|
|
+ } else {
|
|
|
+ error('Unicode ranges Bits > 123 are reserved for internal usage');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // TODO
|
|
|
+ firstCharIndex = 0;
|
|
|
+ lastCharIndex = 255;
|
|
|
+ }
|
|
|
|
|
|
-var FontFlags = {
|
|
|
- FixedPitch: 1,
|
|
|
- Serif: 2,
|
|
|
- Symbolic: 4,
|
|
|
- Script: 8,
|
|
|
- Nonsymbolic: 32,
|
|
|
- Italic: 64,
|
|
|
- AllCap: 65536,
|
|
|
- SmallCap: 131072,
|
|
|
- ForceBold: 262144
|
|
|
-};
|
|
|
+ var bbox = properties.bbox || [0, 0, 0, 0];
|
|
|
+ var unitsPerEm = (override.unitsPerEm ||
|
|
|
+ 1 / (properties.fontMatrix || FONT_IDENTITY_MATRIX)[0]);
|
|
|
|
|
|
-var MacStandardGlyphOrdering = [
|
|
|
- '.notdef', '.null', 'nonmarkingreturn', 'space', 'exclam', 'quotedbl',
|
|
|
- 'numbersign', 'dollar', 'percent', 'ampersand', 'quotesingle', 'parenleft',
|
|
|
- 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash',
|
|
|
- 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight',
|
|
|
- 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater', 'question', 'at',
|
|
|
- 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
|
|
|
- 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft',
|
|
|
- 'backslash', 'bracketright', 'asciicircum', 'underscore', 'grave', 'a', 'b',
|
|
|
- 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
|
|
|
- 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright',
|
|
|
- 'asciitilde', 'Adieresis', 'Aring', 'Ccedilla', 'Eacute', 'Ntilde',
|
|
|
- 'Odieresis', 'Udieresis', 'aacute', 'agrave', 'acircumflex', 'adieresis',
|
|
|
- 'atilde', 'aring', 'ccedilla', 'eacute', 'egrave', 'ecircumflex', 'edieresis',
|
|
|
- 'iacute', 'igrave', 'icircumflex', 'idieresis', 'ntilde', 'oacute', 'ograve',
|
|
|
- 'ocircumflex', 'odieresis', 'otilde', 'uacute', 'ugrave', 'ucircumflex',
|
|
|
- 'udieresis', 'dagger', 'degree', 'cent', 'sterling', 'section', 'bullet',
|
|
|
- 'paragraph', 'germandbls', 'registered', 'copyright', 'trademark', 'acute',
|
|
|
- 'dieresis', 'notequal', 'AE', 'Oslash', 'infinity', 'plusminus', 'lessequal',
|
|
|
- 'greaterequal', 'yen', 'mu', 'partialdiff', 'summation', 'product', 'pi',
|
|
|
- 'integral', 'ordfeminine', 'ordmasculine', 'Omega', 'ae', 'oslash',
|
|
|
- 'questiondown', 'exclamdown', 'logicalnot', 'radical', 'florin',
|
|
|
- 'approxequal', 'Delta', 'guillemotleft', 'guillemotright', 'ellipsis',
|
|
|
- 'nonbreakingspace', 'Agrave', 'Atilde', 'Otilde', 'OE', 'oe', 'endash',
|
|
|
- 'emdash', 'quotedblleft', 'quotedblright', 'quoteleft', 'quoteright',
|
|
|
- 'divide', 'lozenge', 'ydieresis', 'Ydieresis', 'fraction', 'currency',
|
|
|
- 'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'daggerdbl', 'periodcentered',
|
|
|
- 'quotesinglbase', 'quotedblbase', 'perthousand', 'Acircumflex',
|
|
|
- 'Ecircumflex', 'Aacute', 'Edieresis', 'Egrave', 'Iacute', 'Icircumflex',
|
|
|
- 'Idieresis', 'Igrave', 'Oacute', 'Ocircumflex', 'apple', 'Ograve', 'Uacute',
|
|
|
- 'Ucircumflex', 'Ugrave', 'dotlessi', 'circumflex', 'tilde', 'macron',
|
|
|
- 'breve', 'dotaccent', 'ring', 'cedilla', 'hungarumlaut', 'ogonek', 'caron',
|
|
|
- 'Lslash', 'lslash', 'Scaron', 'scaron', 'Zcaron', 'zcaron', 'brokenbar',
|
|
|
- 'Eth', 'eth', 'Yacute', 'yacute', 'Thorn', 'thorn', 'minus', 'multiply',
|
|
|
- 'onesuperior', 'twosuperior', 'threesuperior', 'onehalf', 'onequarter',
|
|
|
- 'threequarters', 'franc', 'Gbreve', 'gbreve', 'Idotaccent', 'Scedilla',
|
|
|
- 'scedilla', 'Cacute', 'cacute', 'Ccaron', 'ccaron', 'dcroat'];
|
|
|
+ // if the font units differ to the PDF glyph space units
|
|
|
+ // then scale up the values
|
|
|
+ var scale = (properties.ascentScaled ? 1.0 :
|
|
|
+ unitsPerEm / PDF_GLYPH_SPACE_UNITS);
|
|
|
|
|
|
-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];
|
|
|
- var glyphsWidths = properties.widths;
|
|
|
- for (var glyph in glyphsWidths) {
|
|
|
- glyphsWidths[glyph] *= scale;
|
|
|
- }
|
|
|
- properties.defaultWidth *= scale;
|
|
|
-}
|
|
|
+ var typoAscent = (override.ascent ||
|
|
|
+ Math.round(scale * (properties.ascent || bbox[3])));
|
|
|
+ var typoDescent = (override.descent ||
|
|
|
+ Math.round(scale * (properties.descent || bbox[1])));
|
|
|
+ if (typoDescent > 0 && properties.descent > 0 && bbox[1] < 0) {
|
|
|
+ typoDescent = -typoDescent; // fixing incorrect descent
|
|
|
+ }
|
|
|
+ var winAscent = override.yMax || typoAscent;
|
|
|
+ var winDescent = -override.yMin || -typoDescent;
|
|
|
|
|
|
-function getFontType(type, subtype) {
|
|
|
- switch (type) {
|
|
|
- case 'Type1':
|
|
|
- return subtype === 'Type1C' ? FontType.TYPE1C : FontType.TYPE1;
|
|
|
- case 'CIDFontType0':
|
|
|
- return subtype === 'CIDFontType0C' ? FontType.CIDFONTTYPE0C :
|
|
|
- FontType.CIDFONTTYPE0;
|
|
|
- case 'OpenType':
|
|
|
- return FontType.OPENTYPE;
|
|
|
- case 'TrueType':
|
|
|
- return FontType.TRUETYPE;
|
|
|
- case 'CIDFontType2':
|
|
|
- return FontType.CIDFONTTYPE2;
|
|
|
- case 'MMType1':
|
|
|
- return FontType.MMTYPE1;
|
|
|
- case 'Type0':
|
|
|
- return FontType.TYPE0;
|
|
|
- default:
|
|
|
- return FontType.UNKNOWN;
|
|
|
+ return '\x00\x03' + // version
|
|
|
+ '\x02\x24' + // xAvgCharWidth
|
|
|
+ '\x01\xF4' + // usWeightClass
|
|
|
+ '\x00\x05' + // usWidthClass
|
|
|
+ '\x00\x00' + // fstype (0 to let the font loads via font-face on IE)
|
|
|
+ '\x02\x8A' + // ySubscriptXSize
|
|
|
+ '\x02\xBB' + // ySubscriptYSize
|
|
|
+ '\x00\x00' + // ySubscriptXOffset
|
|
|
+ '\x00\x8C' + // ySubscriptYOffset
|
|
|
+ '\x02\x8A' + // ySuperScriptXSize
|
|
|
+ '\x02\xBB' + // ySuperScriptYSize
|
|
|
+ '\x00\x00' + // ySuperScriptXOffset
|
|
|
+ '\x01\xDF' + // ySuperScriptYOffset
|
|
|
+ '\x00\x31' + // yStrikeOutSize
|
|
|
+ '\x01\x02' + // yStrikeOutPosition
|
|
|
+ '\x00\x00' + // sFamilyClass
|
|
|
+ '\x00\x00\x06' +
|
|
|
+ String.fromCharCode(properties.fixedPitch ? 0x09 : 0x00) +
|
|
|
+ '\x00\x00\x00\x00\x00\x00' + // Panose
|
|
|
+ string32(ulUnicodeRange1) + // ulUnicodeRange1 (Bits 0-31)
|
|
|
+ string32(ulUnicodeRange2) + // ulUnicodeRange2 (Bits 32-63)
|
|
|
+ string32(ulUnicodeRange3) + // ulUnicodeRange3 (Bits 64-95)
|
|
|
+ string32(ulUnicodeRange4) + // ulUnicodeRange4 (Bits 96-127)
|
|
|
+ '\x2A\x32\x31\x2A' + // achVendID
|
|
|
+ string16(properties.italicAngle ? 1 : 0) + // fsSelection
|
|
|
+ string16(firstCharIndex ||
|
|
|
+ properties.firstChar) + // usFirstCharIndex
|
|
|
+ string16(lastCharIndex || properties.lastChar) + // usLastCharIndex
|
|
|
+ string16(typoAscent) + // sTypoAscender
|
|
|
+ string16(typoDescent) + // sTypoDescender
|
|
|
+ '\x00\x64' + // sTypoLineGap (7%-10% of the unitsPerEM value)
|
|
|
+ string16(winAscent) + // usWinAscent
|
|
|
+ string16(winDescent) + // usWinDescent
|
|
|
+ '\x00\x00\x00\x00' + // ulCodePageRange1 (Bits 0-31)
|
|
|
+ '\x00\x00\x00\x00' + // ulCodePageRange2 (Bits 32-63)
|
|
|
+ string16(properties.xHeight) + // sxHeight
|
|
|
+ string16(properties.capHeight) + // sCapHeight
|
|
|
+ string16(0) + // usDefaultChar
|
|
|
+ string16(firstCharIndex || properties.firstChar) + // usBreakChar
|
|
|
+ '\x00\x03'; // usMaxContext
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
-var Glyph = (function GlyphClosure() {
|
|
|
- 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;
|
|
|
+ function createPostTable(properties) {
|
|
|
+ var angle = Math.floor(properties.italicAngle * (Math.pow(2, 16)));
|
|
|
+ return ('\x00\x03\x00\x00' + // Version number
|
|
|
+ string32(angle) + // italicAngle
|
|
|
+ '\x00\x00' + // underlinePosition
|
|
|
+ '\x00\x00' + // underlineThickness
|
|
|
+ string32(properties.fixedPitch) + // isFixedPitch
|
|
|
+ '\x00\x00\x00\x00' + // minMemType42
|
|
|
+ '\x00\x00\x00\x00' + // maxMemType42
|
|
|
+ '\x00\x00\x00\x00' + // minMemType1
|
|
|
+ '\x00\x00\x00\x00'); // maxMemType1
|
|
|
}
|
|
|
|
|
|
- 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.isSpace === isSpace;
|
|
|
- };
|
|
|
-
|
|
|
- return Glyph;
|
|
|
-})();
|
|
|
+ function createNameTable(name, proto) {
|
|
|
+ if (!proto) {
|
|
|
+ proto = [[], []]; // no strings and unicode strings
|
|
|
+ }
|
|
|
|
|
|
-var ToUnicodeMap = (function ToUnicodeMapClosure() {
|
|
|
- function ToUnicodeMap(cmap) {
|
|
|
- // The elements of this._map can be integers or strings, depending on how
|
|
|
- // |cmap| was created.
|
|
|
- this._map = cmap;
|
|
|
- }
|
|
|
+ var strings = [
|
|
|
+ proto[0][0] || 'Original licence', // 0.Copyright
|
|
|
+ proto[0][1] || name, // 1.Font family
|
|
|
+ proto[0][2] || 'Unknown', // 2.Font subfamily (font weight)
|
|
|
+ proto[0][3] || 'uniqueID', // 3.Unique ID
|
|
|
+ proto[0][4] || name, // 4.Full font name
|
|
|
+ proto[0][5] || 'Version 0.11', // 5.Version
|
|
|
+ proto[0][6] || '', // 6.Postscript name
|
|
|
+ proto[0][7] || 'Unknown', // 7.Trademark
|
|
|
+ proto[0][8] || 'Unknown', // 8.Manufacturer
|
|
|
+ proto[0][9] || 'Unknown' // 9.Designer
|
|
|
+ ];
|
|
|
|
|
|
- ToUnicodeMap.prototype = {
|
|
|
- get length() {
|
|
|
- return this._map.length;
|
|
|
- },
|
|
|
+ // Mac want 1-byte per character strings while Windows want
|
|
|
+ // 2-bytes per character, so duplicate the names table
|
|
|
+ var stringsUnicode = [];
|
|
|
+ var i, ii, j, jj, str;
|
|
|
+ for (i = 0, ii = strings.length; i < ii; i++) {
|
|
|
+ str = proto[1][i] || strings[i];
|
|
|
|
|
|
- forEach: function(callback) {
|
|
|
- for (var charCode in this._map) {
|
|
|
- callback(charCode, this._map[charCode].charCodeAt(0));
|
|
|
+ var strBufUnicode = [];
|
|
|
+ for (j = 0, jj = str.length; j < jj; j++) {
|
|
|
+ strBufUnicode.push(string16(str.charCodeAt(j)));
|
|
|
}
|
|
|
- },
|
|
|
+ stringsUnicode.push(strBufUnicode.join(''));
|
|
|
+ }
|
|
|
|
|
|
- has: function(i) {
|
|
|
- return this._map[i] !== undefined;
|
|
|
- },
|
|
|
+ var names = [strings, stringsUnicode];
|
|
|
+ var platforms = ['\x00\x01', '\x00\x03'];
|
|
|
+ var encodings = ['\x00\x00', '\x00\x01'];
|
|
|
+ var languages = ['\x00\x00', '\x04\x09'];
|
|
|
|
|
|
- get: function(i) {
|
|
|
- return this._map[i];
|
|
|
- },
|
|
|
+ var namesRecordCount = strings.length * platforms.length;
|
|
|
+ var nameTable =
|
|
|
+ '\x00\x00' + // format
|
|
|
+ string16(namesRecordCount) + // Number of names Record
|
|
|
+ string16(namesRecordCount * 12 + 6); // Storage
|
|
|
|
|
|
- charCodeOf: function(v) {
|
|
|
- return this._map.indexOf(v);
|
|
|
+ // Build the name records field
|
|
|
+ var strOffset = 0;
|
|
|
+ for (i = 0, ii = platforms.length; i < ii; i++) {
|
|
|
+ var strs = names[i];
|
|
|
+ for (j = 0, jj = strs.length; j < jj; j++) {
|
|
|
+ str = strs[j];
|
|
|
+ var nameRecord =
|
|
|
+ platforms[i] + // platform ID
|
|
|
+ encodings[i] + // encoding ID
|
|
|
+ languages[i] + // language ID
|
|
|
+ string16(j) + // name ID
|
|
|
+ string16(str.length) +
|
|
|
+ string16(strOffset);
|
|
|
+ nameTable += nameRecord;
|
|
|
+ strOffset += str.length;
|
|
|
+ }
|
|
|
}
|
|
|
- };
|
|
|
|
|
|
- return ToUnicodeMap;
|
|
|
-})();
|
|
|
-
|
|
|
-var IdentityToUnicodeMap = (function IdentityToUnicodeMapClosure() {
|
|
|
- function IdentityToUnicodeMap(firstChar, lastChar) {
|
|
|
- this.firstChar = firstChar;
|
|
|
- this.lastChar = lastChar;
|
|
|
+ nameTable += strings.join('') + stringsUnicode.join('');
|
|
|
+ return nameTable;
|
|
|
}
|
|
|
|
|
|
- IdentityToUnicodeMap.prototype = {
|
|
|
- get 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;
|
|
|
+ Font.prototype = {
|
|
|
+ name: null,
|
|
|
+ font: null,
|
|
|
+ mimetype: null,
|
|
|
+ encoding: null,
|
|
|
+ get renderer() {
|
|
|
+ var renderer = FontRendererFactory.create(this);
|
|
|
+ return shadow(this, 'renderer', renderer);
|
|
|
},
|
|
|
|
|
|
- get: function (i) {
|
|
|
- if (this.firstChar <= i && i <= this.lastChar) {
|
|
|
- return String.fromCharCode(i);
|
|
|
+ exportData: function Font_exportData() {
|
|
|
+ // TODO remove enumerating of the properties, e.g. hardcode exact names.
|
|
|
+ var data = {};
|
|
|
+ for (var i in this) {
|
|
|
+ if (this.hasOwnProperty(i)) {
|
|
|
+ data[i] = this[i];
|
|
|
+ }
|
|
|
}
|
|
|
- return undefined;
|
|
|
+ return data;
|
|
|
},
|
|
|
|
|
|
- charCodeOf: function (v) {
|
|
|
- return (isInt(v) && v >= this.firstChar && v <= this.lastChar) ? v : -1;
|
|
|
- }
|
|
|
- };
|
|
|
+ checkAndRepair: function Font_checkAndRepair(name, font, properties) {
|
|
|
+ function readTableEntry(file) {
|
|
|
+ var tag = bytesToString(file.getBytes(4));
|
|
|
|
|
|
- return IdentityToUnicodeMap;
|
|
|
-})();
|
|
|
+ var checksum = file.getInt32();
|
|
|
+ var offset = file.getInt32() >>> 0;
|
|
|
+ var length = file.getInt32() >>> 0;
|
|
|
|
|
|
-var OpenTypeFileBuilder = (function OpenTypeFileBuilderClosure() {
|
|
|
- function writeInt16(dest, offset, num) {
|
|
|
- dest[offset] = (num >> 8) & 0xFF;
|
|
|
- dest[offset + 1] = num & 0xFF;
|
|
|
- }
|
|
|
+ // Read the table associated data
|
|
|
+ var previousPosition = file.pos;
|
|
|
+ file.pos = file.start ? file.start : 0;
|
|
|
+ file.skip(offset);
|
|
|
+ var data = file.getBytes(length);
|
|
|
+ file.pos = previousPosition;
|
|
|
|
|
|
- function writeInt32(dest, offset, num) {
|
|
|
- dest[offset] = (num >> 24) & 0xFF;
|
|
|
- dest[offset + 1] = (num >> 16) & 0xFF;
|
|
|
- dest[offset + 2] = (num >> 8) & 0xFF;
|
|
|
- dest[offset + 3] = num & 0xFF;
|
|
|
- }
|
|
|
+ if (tag === 'head') {
|
|
|
+ // clearing checksum adjustment
|
|
|
+ data[8] = data[9] = data[10] = data[11] = 0;
|
|
|
+ data[17] |= 0x20; //Set font optimized for cleartype flag
|
|
|
+ }
|
|
|
|
|
|
- function writeData(dest, offset, data) {
|
|
|
- var i, ii;
|
|
|
- if (data instanceof Uint8Array) {
|
|
|
- dest.set(data, offset);
|
|
|
- } else if (typeof data === 'string') {
|
|
|
- for (i = 0, ii = data.length; i < ii; i++) {
|
|
|
- dest[offset++] = data.charCodeAt(i) & 0xFF;
|
|
|
- }
|
|
|
- } else {
|
|
|
- // treating everything else as array
|
|
|
- for (i = 0, ii = data.length; i < ii; i++) {
|
|
|
- dest[offset++] = data[i] & 0xFF;
|
|
|
+ return {
|
|
|
+ tag: tag,
|
|
|
+ checksum: checksum,
|
|
|
+ length: length,
|
|
|
+ offset: offset,
|
|
|
+ data: data
|
|
|
+ };
|
|
|
}
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
- function OpenTypeFileBuilder(sfnt) {
|
|
|
- this.sfnt = sfnt;
|
|
|
- this.tables = Object.create(null);
|
|
|
- }
|
|
|
+ function readOpenTypeHeader(ttf) {
|
|
|
+ return {
|
|
|
+ version: bytesToString(ttf.getBytes(4)),
|
|
|
+ numTables: ttf.getUint16(),
|
|
|
+ searchRange: ttf.getUint16(),
|
|
|
+ entrySelector: ttf.getUint16(),
|
|
|
+ rangeShift: ttf.getUint16()
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
- OpenTypeFileBuilder.getSearchParams =
|
|
|
- function OpenTypeFileBuilder_getSearchParams(entriesCount, entrySize) {
|
|
|
- var maxPower2 = 1, log2 = 0;
|
|
|
- while ((maxPower2 ^ entriesCount) > maxPower2) {
|
|
|
- maxPower2 <<= 1;
|
|
|
- log2++;
|
|
|
- }
|
|
|
- var searchRange = maxPower2 * entrySize;
|
|
|
- return {
|
|
|
- range: searchRange,
|
|
|
- entry: log2,
|
|
|
- rangeShift: entrySize * entriesCount - searchRange
|
|
|
- };
|
|
|
- };
|
|
|
+ /**
|
|
|
+ * Read the appropriate subtable from the cmap according to 9.6.6.4 from
|
|
|
+ * PDF spec
|
|
|
+ */
|
|
|
+ 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 OTF_HEADER_SIZE = 12;
|
|
|
- var OTF_TABLE_ENTRY_SIZE = 16;
|
|
|
+ var version = font.getUint16();
|
|
|
+ var numTables = font.getUint16();
|
|
|
|
|
|
- OpenTypeFileBuilder.prototype = {
|
|
|
- toArray: function OpenTypeFileBuilder_toArray() {
|
|
|
- var sfnt = this.sfnt;
|
|
|
+ var potentialTable;
|
|
|
+ var canBreak = false;
|
|
|
+ // There's an order of preference in terms of which cmap subtable to
|
|
|
+ // use:
|
|
|
+ // - non-symbolic fonts the preference is a 3,1 table then a 1,0 table
|
|
|
+ // - symbolic fonts the preference is a 3,0 table then a 1,0 table
|
|
|
+ // The following takes advantage of the fact that the tables are sorted
|
|
|
+ // to work.
|
|
|
+ for (var i = 0; i < numTables; i++) {
|
|
|
+ var platformId = font.getUint16();
|
|
|
+ var encodingId = font.getUint16();
|
|
|
+ var offset = font.getInt32() >>> 0;
|
|
|
+ var useTable = false;
|
|
|
|
|
|
- // Tables needs to be written by ascendant alphabetic order
|
|
|
- var tables = this.tables;
|
|
|
- var tablesNames = Object.keys(tables);
|
|
|
- tablesNames.sort();
|
|
|
- var numTables = tablesNames.length;
|
|
|
+ if (platformId === 0 && encodingId === 0) {
|
|
|
+ useTable = true;
|
|
|
+ // Continue the loop since there still may be a higher priority
|
|
|
+ // table.
|
|
|
+ } else if (platformId === 1 && encodingId === 0) {
|
|
|
+ useTable = 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;
|
|
|
+ }
|
|
|
|
|
|
- var i, j, jj, table, tableName;
|
|
|
- // layout the tables data
|
|
|
- var offset = OTF_HEADER_SIZE + numTables * OTF_TABLE_ENTRY_SIZE;
|
|
|
- var tableOffsets = [offset];
|
|
|
- for (i = 0; i < numTables; i++) {
|
|
|
- table = tables[tablesNames[i]];
|
|
|
- var paddedLength = ((table.length + 3) & ~3) >>> 0;
|
|
|
- offset += paddedLength;
|
|
|
- tableOffsets.push(offset);
|
|
|
- }
|
|
|
-
|
|
|
- var file = new Uint8Array(offset);
|
|
|
- // write the table data first (mostly for checksum)
|
|
|
- for (i = 0; i < numTables; i++) {
|
|
|
- table = tables[tablesNames[i]];
|
|
|
- writeData(file, tableOffsets[i], table);
|
|
|
- }
|
|
|
-
|
|
|
- // sfnt version (4 bytes)
|
|
|
- if (sfnt === 'true') {
|
|
|
- // Windows hates the Mac TrueType sfnt version number
|
|
|
- sfnt = string32(0x00010000);
|
|
|
- }
|
|
|
- file[0] = sfnt.charCodeAt(0) & 0xFF;
|
|
|
- file[1] = sfnt.charCodeAt(1) & 0xFF;
|
|
|
- file[2] = sfnt.charCodeAt(2) & 0xFF;
|
|
|
- file[3] = sfnt.charCodeAt(3) & 0xFF;
|
|
|
-
|
|
|
- // numTables (2 bytes)
|
|
|
- writeInt16(file, 4, numTables);
|
|
|
-
|
|
|
- var searchParams = OpenTypeFileBuilder.getSearchParams(numTables, 16);
|
|
|
-
|
|
|
- // searchRange (2 bytes)
|
|
|
- writeInt16(file, 6, searchParams.range);
|
|
|
- // entrySelector (2 bytes)
|
|
|
- writeInt16(file, 8, searchParams.entry);
|
|
|
- // rangeShift (2 bytes)
|
|
|
- writeInt16(file, 10, searchParams.rangeShift);
|
|
|
-
|
|
|
- offset = OTF_HEADER_SIZE;
|
|
|
- // writing table entries
|
|
|
- for (i = 0; i < numTables; i++) {
|
|
|
- tableName = tablesNames[i];
|
|
|
- file[offset] = tableName.charCodeAt(0) & 0xFF;
|
|
|
- file[offset + 1] = tableName.charCodeAt(1) & 0xFF;
|
|
|
- file[offset + 2] = tableName.charCodeAt(2) & 0xFF;
|
|
|
- file[offset + 3] = tableName.charCodeAt(3) & 0xFF;
|
|
|
-
|
|
|
- // checksum
|
|
|
- var checksum = 0;
|
|
|
- for (j = tableOffsets[i], jj = tableOffsets[i + 1]; j < jj; j += 4) {
|
|
|
- var quad = (file[j] << 24) + (file[j + 1] << 16) +
|
|
|
- (file[j + 2] << 8) + file[j + 3];
|
|
|
- checksum = (checksum + quad) | 0;
|
|
|
+ if (useTable) {
|
|
|
+ potentialTable = {
|
|
|
+ platformId: platformId,
|
|
|
+ encodingId: encodingId,
|
|
|
+ offset: offset
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (canBreak) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
}
|
|
|
- writeInt32(file, offset + 4, checksum);
|
|
|
|
|
|
- // offset
|
|
|
- writeInt32(file, offset + 8, tableOffsets[i]);
|
|
|
- // length
|
|
|
- writeInt32(file, offset + 12, tables[tableName].length);
|
|
|
+ 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
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
- offset += OTF_TABLE_ENTRY_SIZE;
|
|
|
- }
|
|
|
- return file;
|
|
|
- },
|
|
|
+ var format = font.getUint16();
|
|
|
+ var length = font.getUint16();
|
|
|
+ var language = font.getUint16();
|
|
|
|
|
|
- addTable: function OpenTypeFileBuilder_addTable(tag, data) {
|
|
|
- if (tag in this.tables) {
|
|
|
- throw new Error('Table ' + tag + ' already exists');
|
|
|
- }
|
|
|
- this.tables[tag] = data;
|
|
|
- }
|
|
|
- };
|
|
|
+ var hasShortCmap = false;
|
|
|
+ var mappings = [];
|
|
|
+ var j, glyphId;
|
|
|
|
|
|
- return OpenTypeFileBuilder;
|
|
|
-})();
|
|
|
+ // TODO(mack): refactor this cmap subtable reading logic out
|
|
|
+ if (format === 0) {
|
|
|
+ for (j = 0; j < 256; j++) {
|
|
|
+ var index = font.getByte();
|
|
|
+ if (!index) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ mappings.push({
|
|
|
+ charCode: j,
|
|
|
+ glyphId: index
|
|
|
+ });
|
|
|
+ }
|
|
|
+ hasShortCmap = true;
|
|
|
+ } else if (format === 4) {
|
|
|
+ // re-creating the table in format 4 since the encoding
|
|
|
+ // might be changed
|
|
|
+ var segCount = (font.getUint16() >> 1);
|
|
|
+ font.getBytes(6); // skipping range fields
|
|
|
+ var segIndex, segments = [];
|
|
|
+ for (segIndex = 0; segIndex < segCount; segIndex++) {
|
|
|
+ segments.push({ end: font.getUint16() });
|
|
|
+ }
|
|
|
+ font.getUint16();
|
|
|
+ for (segIndex = 0; segIndex < segCount; segIndex++) {
|
|
|
+ segments[segIndex].start = font.getUint16();
|
|
|
+ }
|
|
|
|
|
|
-// 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
|
|
|
-]);
|
|
|
+ for (segIndex = 0; segIndex < segCount; segIndex++) {
|
|
|
+ segments[segIndex].delta = font.getUint16();
|
|
|
+ }
|
|
|
|
|
|
-/**
|
|
|
- * '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:
|
|
|
- * var type1Font = new Font("MyFontName", binaryFile, propertiesObject);
|
|
|
- * type1Font.bind();
|
|
|
- */
|
|
|
-var Font = (function FontClosure() {
|
|
|
- function Font(name, file, properties) {
|
|
|
- var charCode, glyphName, fontChar;
|
|
|
+ var offsetsCount = 0;
|
|
|
+ for (segIndex = 0; segIndex < segCount; segIndex++) {
|
|
|
+ segment = segments[segIndex];
|
|
|
+ var rangeOffset = font.getUint16();
|
|
|
+ if (!rangeOffset) {
|
|
|
+ segment.offsetIndex = -1;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
- this.name = name;
|
|
|
- this.loadedName = properties.loadedName;
|
|
|
- this.isType3Font = properties.isType3Font;
|
|
|
- this.sizes = [];
|
|
|
+ var offsetIndex = (rangeOffset >> 1) - (segCount - segIndex);
|
|
|
+ segment.offsetIndex = offsetIndex;
|
|
|
+ offsetsCount = Math.max(offsetsCount, offsetIndex +
|
|
|
+ segment.end - segment.start + 1);
|
|
|
+ }
|
|
|
|
|
|
- this.glyphCache = Object.create(null);
|
|
|
+ var offsets = [];
|
|
|
+ for (j = 0; j < offsetsCount; j++) {
|
|
|
+ offsets.push(font.getUint16());
|
|
|
+ }
|
|
|
|
|
|
- var names = name.split('+');
|
|
|
- names = names.length > 1 ? names[1] : names[0];
|
|
|
- names = names.split(/[-,_]/g)[0];
|
|
|
- this.isSerifFont = !!(properties.flags & FontFlags.Serif);
|
|
|
- this.isSymbolicFont = !!(properties.flags & FontFlags.Symbolic);
|
|
|
- this.isMonospace = !!(properties.flags & FontFlags.FixedPitch);
|
|
|
+ for (segIndex = 0; segIndex < segCount; segIndex++) {
|
|
|
+ segment = segments[segIndex];
|
|
|
+ start = segment.start;
|
|
|
+ var end = segment.end;
|
|
|
+ var delta = segment.delta;
|
|
|
+ offsetIndex = segment.offsetIndex;
|
|
|
|
|
|
- var type = properties.type;
|
|
|
- var subtype = properties.subtype;
|
|
|
- this.type = type;
|
|
|
+ for (j = start; j <= end; j++) {
|
|
|
+ if (j === 0xFFFF) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
- this.fallbackName = (this.isMonospace ? 'monospace' :
|
|
|
- (this.isSerifFont ? 'serif' : 'sans-serif'));
|
|
|
+ glyphId = (offsetIndex < 0 ?
|
|
|
+ j : offsets[offsetIndex + j - start]);
|
|
|
+ glyphId = (glyphId + delta) & 0xFFFF;
|
|
|
+ if (glyphId === 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ mappings.push({
|
|
|
+ charCode: j,
|
|
|
+ glyphId: glyphId
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (format === 6) {
|
|
|
+ // Format 6 is a 2-bytes dense mapping, which means the font data
|
|
|
+ // lives glue together even if they are pretty far in the unicode
|
|
|
+ // table. (This looks weird, so I can have missed something), this
|
|
|
+ // works on Linux but seems to fails on Mac so let's rewrite the
|
|
|
+ // cmap table to a 3-1-4 style
|
|
|
+ var firstCode = font.getUint16();
|
|
|
+ var entryCount = font.getUint16();
|
|
|
|
|
|
- this.differences = properties.differences;
|
|
|
- this.widths = properties.widths;
|
|
|
- this.defaultWidth = properties.defaultWidth;
|
|
|
- this.composite = properties.composite;
|
|
|
- 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;
|
|
|
+ for (j = 0; j < entryCount; j++) {
|
|
|
+ glyphId = font.getUint16();
|
|
|
+ var charCode = firstCode + j;
|
|
|
|
|
|
- this.toUnicode = properties.toUnicode = this.buildToUnicode(properties);
|
|
|
+ mappings.push({
|
|
|
+ charCode: charCode,
|
|
|
+ glyphId: glyphId
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ warn('cmap table has unsupported format: ' + format);
|
|
|
+ return {
|
|
|
+ platformId: -1,
|
|
|
+ encodingId: -1,
|
|
|
+ mappings: [],
|
|
|
+ hasShortCmap: false
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
- this.toFontChar = [];
|
|
|
+ // removing duplicate entries
|
|
|
+ mappings.sort(function (a, b) {
|
|
|
+ return a.charCode - b.charCode;
|
|
|
+ });
|
|
|
+ for (i = 1; i < mappings.length; i++) {
|
|
|
+ if (mappings[i - 1].charCode === mappings[i].charCode) {
|
|
|
+ mappings.splice(i, 1);
|
|
|
+ i--;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (properties.type === 'Type3') {
|
|
|
- for (charCode = 0; charCode < 256; charCode++) {
|
|
|
- this.toFontChar[charCode] = (this.differences[charCode] ||
|
|
|
- properties.defaultEncoding[charCode]);
|
|
|
+ return {
|
|
|
+ platformId: potentialTable.platformId,
|
|
|
+ encodingId: potentialTable.encodingId,
|
|
|
+ mappings: mappings,
|
|
|
+ hasShortCmap: hasShortCmap
|
|
|
+ };
|
|
|
}
|
|
|
- this.fontType = FontType.TYPE3;
|
|
|
- return;
|
|
|
- }
|
|
|
|
|
|
- this.cidEncoding = properties.cidEncoding;
|
|
|
- this.vertical = properties.vertical;
|
|
|
- if (this.vertical) {
|
|
|
- this.vmetrics = properties.vmetrics;
|
|
|
- this.defaultVMetrics = properties.defaultVMetrics;
|
|
|
- }
|
|
|
- var glyphsUnicodeMap;
|
|
|
- if (!file || file.isEmpty) {
|
|
|
- if (file) {
|
|
|
- // Some bad PDF generators will include empty font files,
|
|
|
- // attempting to recover by assuming that no file exists.
|
|
|
- warn('Font file is empty in "' + name + '" (' + this.loadedName + ')');
|
|
|
- }
|
|
|
+ function sanitizeMetrics(font, header, metrics, numGlyphs) {
|
|
|
+ if (!header) {
|
|
|
+ if (metrics) {
|
|
|
+ metrics.data = null;
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- 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 stdFontMap = getStdFontMap(), nonStdFontMap = getNonStdFontMap();
|
|
|
- var isStandardFont = !!stdFontMap[fontName] ||
|
|
|
- !!(nonStdFontMap[fontName] && stdFontMap[nonStdFontMap[fontName]]);
|
|
|
- fontName = stdFontMap[fontName] || nonStdFontMap[fontName] || fontName;
|
|
|
+ font.pos = (font.start ? font.start : 0) + header.offset;
|
|
|
+ font.pos += header.length - 2;
|
|
|
+ var numOfMetrics = font.getUint16();
|
|
|
|
|
|
- this.bold = (fontName.search(/bold/gi) !== -1);
|
|
|
- this.italic = ((fontName.search(/oblique/gi) !== -1) ||
|
|
|
- (fontName.search(/italic/gi) !== -1));
|
|
|
+ if (numOfMetrics > numGlyphs) {
|
|
|
+ info('The numOfMetrics (' + numOfMetrics + ') should not be ' +
|
|
|
+ 'greater than the numGlyphs (' + numGlyphs + ')');
|
|
|
+ // Reduce numOfMetrics if it is greater than numGlyphs
|
|
|
+ numOfMetrics = numGlyphs;
|
|
|
+ header.data[34] = (numOfMetrics & 0xff00) >> 8;
|
|
|
+ header.data[35] = numOfMetrics & 0x00ff;
|
|
|
+ }
|
|
|
|
|
|
- // Use 'name' instead of 'fontName' here because the original
|
|
|
- // name ArialBlack for example will be replaced by Helvetica.
|
|
|
- this.black = (name.search(/Black/g) !== -1);
|
|
|
+ var numOfSidebearings = numGlyphs - numOfMetrics;
|
|
|
+ var numMissing = numOfSidebearings -
|
|
|
+ ((metrics.length - numOfMetrics * 4) >> 1);
|
|
|
|
|
|
- // if at least one width is present, remeasure all chars when exists
|
|
|
- this.remeasure = Object.keys(this.widths).length > 0;
|
|
|
- if (isStandardFont && type === 'CIDFontType2' &&
|
|
|
- properties.cidEncoding.indexOf('Identity-') === 0) {
|
|
|
- var GlyphMapForStandardFonts = getGlyphMapForStandardFonts();
|
|
|
- // Standard fonts might be embedded as CID font without glyph mapping.
|
|
|
- // Building one based on GlyphMapForStandardFonts.
|
|
|
- var map = [];
|
|
|
- for (charCode in GlyphMapForStandardFonts) {
|
|
|
- map[+charCode] = GlyphMapForStandardFonts[charCode];
|
|
|
+ if (numMissing > 0) {
|
|
|
+ // For each missing glyph, we set both the width and lsb to 0 (zero).
|
|
|
+ // Since we need to add two properties for each glyph, this explains
|
|
|
+ // the use of |numMissing * 2| when initializing the typed array.
|
|
|
+ var entries = new Uint8Array(metrics.length + numMissing * 2);
|
|
|
+ entries.set(metrics.data);
|
|
|
+ metrics.data = entries;
|
|
|
}
|
|
|
- if (/ArialBlack/i.test(name)) {
|
|
|
- var SupplementalGlyphMapForArialBlack =
|
|
|
- getSupplementalGlyphMapForArialBlack();
|
|
|
- for (charCode in SupplementalGlyphMapForArialBlack) {
|
|
|
- map[+charCode] = SupplementalGlyphMapForArialBlack[charCode];
|
|
|
- }
|
|
|
+ }
|
|
|
+
|
|
|
+ function sanitizeGlyph(source, sourceStart, sourceEnd, dest, destStart,
|
|
|
+ hintsValid) {
|
|
|
+ if (sourceEnd - sourceStart <= 12) {
|
|
|
+ // glyph with data less than 12 is invalid one
|
|
|
+ return 0;
|
|
|
}
|
|
|
- var isIdentityUnicode = this.toUnicode instanceof IdentityToUnicodeMap;
|
|
|
- if (!isIdentityUnicode) {
|
|
|
- this.toUnicode.forEach(function(charCode, unicodeCharCode) {
|
|
|
- map[+charCode] = unicodeCharCode;
|
|
|
- });
|
|
|
+ var glyf = source.subarray(sourceStart, sourceEnd);
|
|
|
+ var contoursCount = (glyf[0] << 8) | glyf[1];
|
|
|
+ if (contoursCount & 0x8000) {
|
|
|
+ // complex glyph, writing as is
|
|
|
+ dest.set(glyf, destStart);
|
|
|
+ return glyf.length;
|
|
|
}
|
|
|
- this.toFontChar = map;
|
|
|
- this.toUnicode = new ToUnicodeMap(map);
|
|
|
- } else if (/Symbol/i.test(fontName)) {
|
|
|
- var symbols = SymbolSetEncoding;
|
|
|
- glyphsUnicodeMap = getGlyphsUnicode();
|
|
|
- for (charCode in symbols) {
|
|
|
- fontChar = glyphsUnicodeMap[symbols[charCode]];
|
|
|
- if (!fontChar) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- this.toFontChar[charCode] = fontChar;
|
|
|
+
|
|
|
+ var i, j = 10, flagsCount = 0;
|
|
|
+ for (i = 0; i < contoursCount; i++) {
|
|
|
+ var endPoint = (glyf[j] << 8) | glyf[j + 1];
|
|
|
+ flagsCount = endPoint + 1;
|
|
|
+ j += 2;
|
|
|
}
|
|
|
- for (charCode in properties.differences) {
|
|
|
- fontChar = glyphsUnicodeMap[properties.differences[charCode]];
|
|
|
- if (!fontChar) {
|
|
|
- continue;
|
|
|
+ // skipping instructions
|
|
|
+ var instructionsStart = j;
|
|
|
+ var instructionsLength = (glyf[j] << 8) | glyf[j + 1];
|
|
|
+ j += 2 + instructionsLength;
|
|
|
+ var instructionsEnd = j;
|
|
|
+ // validating flags
|
|
|
+ var coordinatesLength = 0;
|
|
|
+ for (i = 0; i < flagsCount; i++) {
|
|
|
+ var flag = glyf[j++];
|
|
|
+ if (flag & 0xC0) {
|
|
|
+ // reserved flags must be zero, cleaning up
|
|
|
+ glyf[j - 1] = flag & 0x3F;
|
|
|
+ }
|
|
|
+ var xyLength = ((flag & 2) ? 1 : (flag & 16) ? 0 : 2) +
|
|
|
+ ((flag & 4) ? 1 : (flag & 32) ? 0 : 2);
|
|
|
+ coordinatesLength += xyLength;
|
|
|
+ if (flag & 8) {
|
|
|
+ var repeat = glyf[j++];
|
|
|
+ i += repeat;
|
|
|
+ coordinatesLength += repeat * xyLength;
|
|
|
}
|
|
|
- this.toFontChar[charCode] = fontChar;
|
|
|
}
|
|
|
- } else if (/Dingbats/i.test(fontName)) {
|
|
|
- glyphsUnicodeMap = getDingbatsGlyphsUnicode();
|
|
|
- if (/Wingdings/i.test(name)) {
|
|
|
- warn('Wingdings font without embedded font file, ' +
|
|
|
- 'falling back to the ZapfDingbats encoding.');
|
|
|
+ // glyph without coordinates will be rejected
|
|
|
+ if (coordinatesLength === 0) {
|
|
|
+ return 0;
|
|
|
}
|
|
|
- var dingbats = ZapfDingbatsEncoding;
|
|
|
- for (charCode in dingbats) {
|
|
|
- fontChar = glyphsUnicodeMap[dingbats[charCode]];
|
|
|
- if (!fontChar) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- this.toFontChar[charCode] = fontChar;
|
|
|
+ var glyphDataLength = j + coordinatesLength;
|
|
|
+ if (glyphDataLength > glyf.length) {
|
|
|
+ // not enough data for coordinates
|
|
|
+ return 0;
|
|
|
}
|
|
|
- for (charCode in properties.differences) {
|
|
|
- fontChar = glyphsUnicodeMap[properties.differences[charCode]];
|
|
|
- if (!fontChar) {
|
|
|
- continue;
|
|
|
+ if (!hintsValid && instructionsLength > 0) {
|
|
|
+ dest.set(glyf.subarray(0, instructionsStart), destStart);
|
|
|
+ dest.set([0, 0], destStart + instructionsStart);
|
|
|
+ dest.set(glyf.subarray(instructionsEnd, glyphDataLength),
|
|
|
+ destStart + instructionsStart + 2);
|
|
|
+ glyphDataLength -= instructionsLength;
|
|
|
+ if (glyf.length - glyphDataLength > 3) {
|
|
|
+ glyphDataLength = (glyphDataLength + 3) & ~3;
|
|
|
}
|
|
|
- this.toFontChar[charCode] = fontChar;
|
|
|
+ return glyphDataLength;
|
|
|
}
|
|
|
- } else if (isStandardFont) {
|
|
|
- this.toFontChar = [];
|
|
|
- glyphsUnicodeMap = getGlyphsUnicode();
|
|
|
- for (charCode in properties.defaultEncoding) {
|
|
|
- glyphName = (properties.differences[charCode] ||
|
|
|
- properties.defaultEncoding[charCode]);
|
|
|
- this.toFontChar[charCode] = glyphsUnicodeMap[glyphName];
|
|
|
+ if (glyf.length - glyphDataLength > 3) {
|
|
|
+ // truncating and aligning to 4 bytes the long glyph data
|
|
|
+ glyphDataLength = (glyphDataLength + 3) & ~3;
|
|
|
+ dest.set(glyf.subarray(0, glyphDataLength), destStart);
|
|
|
+ return glyphDataLength;
|
|
|
}
|
|
|
- } else {
|
|
|
- var unicodeCharCode, notCidFont = (type.indexOf('CIDFontType') === -1);
|
|
|
- glyphsUnicodeMap = getGlyphsUnicode();
|
|
|
- this.toUnicode.forEach(function(charCode, unicodeCharCode) {
|
|
|
- if (notCidFont) {
|
|
|
- glyphName = (properties.differences[charCode] ||
|
|
|
- properties.defaultEncoding[charCode]);
|
|
|
- unicodeCharCode = (glyphsUnicodeMap[glyphName] || unicodeCharCode);
|
|
|
- }
|
|
|
- this.toFontChar[charCode] = unicodeCharCode;
|
|
|
- }.bind(this));
|
|
|
+ // glyph data is fine
|
|
|
+ dest.set(glyf, destStart);
|
|
|
+ return glyf.length;
|
|
|
}
|
|
|
- this.loadedName = fontName.split('-')[0];
|
|
|
- this.loading = false;
|
|
|
- this.fontType = getFontType(type, subtype);
|
|
|
- return;
|
|
|
- }
|
|
|
|
|
|
- // Some fonts might use wrong font types for Type1C or CIDFontType0C
|
|
|
- if (subtype === 'Type1C' && (type !== 'Type1' && type !== 'MMType1')) {
|
|
|
- // Some TrueType fonts by mistake claim Type1C
|
|
|
- if (isTrueTypeFile(file)) {
|
|
|
- subtype = 'TrueType';
|
|
|
- } else {
|
|
|
- type = 'Type1';
|
|
|
- }
|
|
|
- }
|
|
|
- if (subtype === 'CIDFontType0C' && type !== 'CIDFontType0') {
|
|
|
- type = 'CIDFontType0';
|
|
|
- }
|
|
|
- if (subtype === 'OpenType') {
|
|
|
- type = 'OpenType';
|
|
|
- }
|
|
|
- // Some CIDFontType0C fonts by mistake claim CIDFontType0.
|
|
|
- if (type === 'CIDFontType0') {
|
|
|
- if (isType1File(file)) {
|
|
|
- subtype = 'CIDFontType0';
|
|
|
- } else if (isOpenTypeFile(file)) {
|
|
|
- // Sometimes the type/subtype can be a complete lie (see issue6782.pdf).
|
|
|
- type = subtype = 'OpenType';
|
|
|
- } else {
|
|
|
- subtype = 'CIDFontType0C';
|
|
|
- }
|
|
|
- }
|
|
|
+ function sanitizeHead(head, numGlyphs, locaLength) {
|
|
|
+ var data = head.data;
|
|
|
|
|
|
- var data;
|
|
|
- switch (type) {
|
|
|
- case 'MMType1':
|
|
|
- info('MMType1 font (' + name + '), falling back to Type1.');
|
|
|
- /* falls through */
|
|
|
- case 'Type1':
|
|
|
- case 'CIDFontType0':
|
|
|
- this.mimetype = 'font/opentype';
|
|
|
-
|
|
|
- var cff = (subtype === 'Type1C' || subtype === 'CIDFontType0C') ?
|
|
|
- new CFFFont(file, properties) : new Type1Font(name, file, properties);
|
|
|
-
|
|
|
- adjustWidths(properties);
|
|
|
-
|
|
|
- // Wrap the CFF data inside an OTF font file
|
|
|
- data = this.convert(name, cff, properties);
|
|
|
- break;
|
|
|
-
|
|
|
- case 'OpenType':
|
|
|
- case 'TrueType':
|
|
|
- case 'CIDFontType2':
|
|
|
- this.mimetype = 'font/opentype';
|
|
|
-
|
|
|
- // 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';
|
|
|
+ // Validate version:
|
|
|
+ // Should always be 0x00010000
|
|
|
+ var version = int32(data[0], data[1], data[2], data[3]);
|
|
|
+ if (version >> 16 !== 1) {
|
|
|
+ info('Attempting to fix invalid version in head table: ' + version);
|
|
|
+ data[0] = 0;
|
|
|
+ data[1] = 1;
|
|
|
+ data[2] = 0;
|
|
|
+ data[3] = 0;
|
|
|
}
|
|
|
- break;
|
|
|
-
|
|
|
- default:
|
|
|
- error('Font ' + type + ' is not supported');
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- this.data = data;
|
|
|
- this.fontType = getFontType(type, subtype);
|
|
|
-
|
|
|
- // Transfer some properties again that could change during font conversion
|
|
|
- this.fontMatrix = properties.fontMatrix;
|
|
|
- this.widths = properties.widths;
|
|
|
- this.defaultWidth = properties.defaultWidth;
|
|
|
- this.encoding = properties.baseEncoding;
|
|
|
- this.seacMap = properties.seacMap;
|
|
|
-
|
|
|
- this.loading = true;
|
|
|
- }
|
|
|
-
|
|
|
- Font.getFontID = (function () {
|
|
|
- var ID = 1;
|
|
|
- return function Font_getFontID() {
|
|
|
- return String(ID++);
|
|
|
- };
|
|
|
- })();
|
|
|
-
|
|
|
- function int16(b0, b1) {
|
|
|
- return (b0 << 8) + b1;
|
|
|
- }
|
|
|
-
|
|
|
- function int32(b0, b1, b2, b3) {
|
|
|
- return (b0 << 24) + (b1 << 16) + (b2 << 8) + b3;
|
|
|
- }
|
|
|
-
|
|
|
- function string16(value) {
|
|
|
- return String.fromCharCode((value >> 8) & 0xff, value & 0xff);
|
|
|
- }
|
|
|
-
|
|
|
- function safeString16(value) {
|
|
|
- // clamp value to the 16-bit int range
|
|
|
- value = (value > 0x7FFF ? 0x7FFF : (value < -0x8000 ? -0x8000 : value));
|
|
|
- return String.fromCharCode((value >> 8) & 0xff, value & 0xff);
|
|
|
- }
|
|
|
-
|
|
|
- function isTrueTypeFile(file) {
|
|
|
- var header = file.peekBytes(4);
|
|
|
- return readUint32(header, 0) === 0x00010000;
|
|
|
- }
|
|
|
|
|
|
- function isOpenTypeFile(file) {
|
|
|
- var header = file.peekBytes(4);
|
|
|
- return bytesToString(header) === 'OTTO';
|
|
|
- }
|
|
|
-
|
|
|
- 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;
|
|
|
- }
|
|
|
+ var indexToLocFormat = int16(data[50], data[51]);
|
|
|
+ if (indexToLocFormat < 0 || indexToLocFormat > 1) {
|
|
|
+ info('Attempting to fix invalid indexToLocFormat in head table: ' +
|
|
|
+ indexToLocFormat);
|
|
|
|
|
|
- /**
|
|
|
- * 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);
|
|
|
- }
|
|
|
+ // The value of indexToLocFormat should be 0 if the loca table
|
|
|
+ // consists of short offsets, and should be 1 if the loca table
|
|
|
+ // consists of long offsets.
|
|
|
+ //
|
|
|
+ // The number of entries in the loca table should be numGlyphs + 1.
|
|
|
+ //
|
|
|
+ // Using this information, we can work backwards to deduce if the
|
|
|
+ // size of each offset in the loca table, and thus figure out the
|
|
|
+ // appropriate value for indexToLocFormat.
|
|
|
|
|
|
- /**
|
|
|
- * 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
|
|
|
- * from commands such as show text) to the char codes that will be used in the
|
|
|
- * font that we build
|
|
|
- * 'charCodeToGlyphId' - maps the new font char codes to glyph ids
|
|
|
- */
|
|
|
- function adjustMapping(charCodeToGlyphId, properties) {
|
|
|
- var toUnicode = properties.toUnicode;
|
|
|
- var isSymbolic = !!(properties.flags & FontFlags.Symbolic);
|
|
|
- var isIdentityUnicode =
|
|
|
- properties.toUnicode instanceof IdentityToUnicodeMap;
|
|
|
- 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 && 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);
|
|
|
+ var numGlyphsPlusOne = numGlyphs + 1;
|
|
|
+ if (locaLength === numGlyphsPlusOne << 1) {
|
|
|
+ // 0x0000 indicates the loca table consists of short offsets
|
|
|
+ data[50] = 0;
|
|
|
+ data[51] = 0;
|
|
|
+ } else if (locaLength === numGlyphsPlusOne << 2) {
|
|
|
+ // 0x0001 indicates the loca table consists of long offsets
|
|
|
+ data[50] = 0;
|
|
|
+ data[51] = 1;
|
|
|
+ } else {
|
|
|
+ warn('Could not fix indexToLocFormat: ' + indexToLocFormat);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- // 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 ||
|
|
|
- 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++;
|
|
|
|
|
|
- if (SKIP_PRIVATE_USE_RANGE_F000_TO_F01F && fontCharCode === 0xF000) {
|
|
|
- fontCharCode = 0xF020;
|
|
|
- nextAvailableFontCharCode = fontCharCode + 1;
|
|
|
+ function sanitizeGlyphLocations(loca, glyf, numGlyphs,
|
|
|
+ isGlyphLocationsLong, hintsValid,
|
|
|
+ dupFirstEntry) {
|
|
|
+ var itemSize, itemDecode, itemEncode;
|
|
|
+ if (isGlyphLocationsLong) {
|
|
|
+ itemSize = 4;
|
|
|
+ itemDecode = function fontItemDecodeLong(data, offset) {
|
|
|
+ return (data[offset] << 24) | (data[offset + 1] << 16) |
|
|
|
+ (data[offset + 2] << 8) | data[offset + 3];
|
|
|
+ };
|
|
|
+ itemEncode = function fontItemEncodeLong(data, offset, value) {
|
|
|
+ data[offset] = (value >>> 24) & 0xFF;
|
|
|
+ data[offset + 1] = (value >> 16) & 0xFF;
|
|
|
+ data[offset + 2] = (value >> 8) & 0xFF;
|
|
|
+ data[offset + 3] = value & 0xFF;
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ itemSize = 2;
|
|
|
+ itemDecode = function fontItemDecode(data, offset) {
|
|
|
+ return (data[offset] << 9) | (data[offset + 1] << 1);
|
|
|
+ };
|
|
|
+ itemEncode = function fontItemEncode(data, offset, value) {
|
|
|
+ data[offset] = (value >> 9) & 0xFF;
|
|
|
+ data[offset + 1] = (value >> 1) & 0xFF;
|
|
|
+ };
|
|
|
+ }
|
|
|
+ var locaData = loca.data;
|
|
|
+ var locaDataSize = itemSize * (1 + numGlyphs);
|
|
|
+ // is loca.data too short or long?
|
|
|
+ if (locaData.length !== locaDataSize) {
|
|
|
+ locaData = new Uint8Array(locaDataSize);
|
|
|
+ locaData.set(loca.data.subarray(0, locaDataSize));
|
|
|
+ loca.data = locaData;
|
|
|
+ }
|
|
|
+ // removing the invalid glyphs
|
|
|
+ var oldGlyfData = glyf.data;
|
|
|
+ var oldGlyfDataLength = oldGlyfData.length;
|
|
|
+ var newGlyfData = new Uint8Array(oldGlyfDataLength);
|
|
|
+ var startOffset = itemDecode(locaData, 0);
|
|
|
+ var writeOffset = 0;
|
|
|
+ var missingGlyphData = Object.create(null);
|
|
|
+ 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 &&
|
|
|
+ ((oldGlyfDataLength + 3) & ~3) === endOffset) {
|
|
|
+ // Aspose breaks fonts by aligning the glyphs to the qword, but not
|
|
|
+ // the glyf table size, which makes last glyph out of range.
|
|
|
+ endOffset = oldGlyfDataLength;
|
|
|
+ }
|
|
|
+ if (endOffset > oldGlyfDataLength) {
|
|
|
+ // glyph end offset points outside glyf data, rejecting the glyph
|
|
|
+ itemEncode(locaData, j, writeOffset);
|
|
|
+ startOffset = endOffset;
|
|
|
+ continue;
|
|
|
}
|
|
|
|
|
|
- } while (usedFontCharCodes[fontCharCode] !== undefined &&
|
|
|
- nextAvailableFontCharCode <= PRIVATE_USE_OFFSET_END);
|
|
|
- }
|
|
|
+ if (startOffset === endOffset) {
|
|
|
+ missingGlyphData[i] = true;
|
|
|
+ }
|
|
|
|
|
|
- newMap[fontCharCode] = glyphId;
|
|
|
- toFontChar[originalCharCode] = fontCharCode;
|
|
|
- usedFontCharCodes[fontCharCode] = true;
|
|
|
- }
|
|
|
- return {
|
|
|
- toFontChar: toFontChar,
|
|
|
- charCodeToGlyphId: newMap,
|
|
|
- nextAvailableFontCharCode: nextAvailableFontCharCode
|
|
|
- };
|
|
|
- }
|
|
|
+ var newLength = sanitizeGlyph(oldGlyfData, startOffset, endOffset,
|
|
|
+ newGlyfData, writeOffset, hintsValid);
|
|
|
+ writeOffset += newLength;
|
|
|
+ itemEncode(locaData, j, writeOffset);
|
|
|
+ startOffset = endOffset;
|
|
|
+ }
|
|
|
|
|
|
- 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;
|
|
|
- });
|
|
|
+ if (writeOffset === 0) {
|
|
|
+ // glyf table cannot be empty -- redoing the glyf and loca tables
|
|
|
+ // to have single glyph with one point
|
|
|
+ var simpleGlyph = new Uint8Array(
|
|
|
+ [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 missingGlyphData;
|
|
|
+ }
|
|
|
|
|
|
- // Split the sorted codes into ranges.
|
|
|
- var ranges = [];
|
|
|
- var length = codes.length;
|
|
|
- for (var n = 0; n < length; ) {
|
|
|
- var start = codes[n].fontCharCode;
|
|
|
- var codeIndices = [codes[n].glyphId];
|
|
|
- ++n;
|
|
|
- var end = start;
|
|
|
- while (n < length && end + 1 === codes[n].fontCharCode) {
|
|
|
- codeIndices.push(codes[n].glyphId);
|
|
|
- ++end;
|
|
|
- ++n;
|
|
|
- if (end === 0xFFFF) {
|
|
|
- break;
|
|
|
+ if (dupFirstEntry) {
|
|
|
+ var firstEntryLength = itemDecode(locaData, itemSize);
|
|
|
+ if (newGlyfData.length > firstEntryLength + writeOffset) {
|
|
|
+ glyf.data = newGlyfData.subarray(0, firstEntryLength + writeOffset);
|
|
|
+ } else {
|
|
|
+ glyf.data = new Uint8Array(firstEntryLength + writeOffset);
|
|
|
+ glyf.data.set(newGlyfData.subarray(0, writeOffset));
|
|
|
+ }
|
|
|
+ glyf.data.set(newGlyfData.subarray(0, firstEntryLength), writeOffset);
|
|
|
+ itemEncode(loca.data, locaData.length - itemSize,
|
|
|
+ writeOffset + firstEntryLength);
|
|
|
+ } else {
|
|
|
+ glyf.data = newGlyfData.subarray(0, writeOffset);
|
|
|
}
|
|
|
+ return missingGlyphData;
|
|
|
}
|
|
|
- ranges.push([start, end, codeIndices]);
|
|
|
- }
|
|
|
|
|
|
- return ranges;
|
|
|
- }
|
|
|
-
|
|
|
- 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
|
|
|
- string32(4 + numTables * 8); // start of the table record
|
|
|
-
|
|
|
- var i, ii, j, jj;
|
|
|
- for (i = ranges.length - 1; i >= 0; --i) {
|
|
|
- if (ranges[i][0] <= 0xFFFF) { break; }
|
|
|
- }
|
|
|
- var bmpLength = i + 1;
|
|
|
+ function readPostScriptTable(post, properties, maxpNumGlyphs) {
|
|
|
+ var start = (font.start ? font.start : 0) + post.offset;
|
|
|
+ font.pos = start;
|
|
|
|
|
|
- if (ranges[i][0] < 0xFFFF && ranges[i][1] === 0xFFFF) {
|
|
|
- ranges[i][1] = 0xFFFE;
|
|
|
- }
|
|
|
- var trailingRangesCount = ranges[i][1] < 0xFFFF ? 1 : 0;
|
|
|
- var segCount = bmpLength + trailingRangesCount;
|
|
|
- var searchParams = OpenTypeFileBuilder.getSearchParams(segCount, 2);
|
|
|
+ var length = post.length, end = start + length;
|
|
|
+ var version = font.getInt32();
|
|
|
+ // skip rest to the tables
|
|
|
+ font.getBytes(28);
|
|
|
|
|
|
- // Fill up the 4 parallel arrays describing the segments.
|
|
|
- var startCount = '';
|
|
|
- var endCount = '';
|
|
|
- var idDeltas = '';
|
|
|
- var idRangeOffsets = '';
|
|
|
- var glyphsIds = '';
|
|
|
- var bias = 0;
|
|
|
+ var glyphNames;
|
|
|
+ var valid = true;
|
|
|
+ var i;
|
|
|
|
|
|
- var range, start, end, codes;
|
|
|
- for (i = 0, ii = bmpLength; i < ii; i++) {
|
|
|
- range = ranges[i];
|
|
|
- start = range[0];
|
|
|
- end = range[1];
|
|
|
- startCount += string16(start);
|
|
|
- endCount += string16(end);
|
|
|
- codes = range[2];
|
|
|
- var contiguous = true;
|
|
|
- for (j = 1, jj = codes.length; j < jj; ++j) {
|
|
|
- if (codes[j] !== codes[j - 1] + 1) {
|
|
|
- contiguous = false;
|
|
|
- break;
|
|
|
+ switch (version) {
|
|
|
+ case 0x00010000:
|
|
|
+ glyphNames = MacStandardGlyphOrdering;
|
|
|
+ break;
|
|
|
+ case 0x00020000:
|
|
|
+ var numGlyphs = font.getUint16();
|
|
|
+ if (numGlyphs !== maxpNumGlyphs) {
|
|
|
+ valid = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ var glyphNameIndexes = [];
|
|
|
+ for (i = 0; i < numGlyphs; ++i) {
|
|
|
+ var index = font.getUint16();
|
|
|
+ if (index >= 32768) {
|
|
|
+ valid = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ glyphNameIndexes.push(index);
|
|
|
+ }
|
|
|
+ if (!valid) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ var customNames = [];
|
|
|
+ var strBuf = [];
|
|
|
+ while (font.pos < end) {
|
|
|
+ var stringLength = font.getByte();
|
|
|
+ strBuf.length = stringLength;
|
|
|
+ for (i = 0; i < stringLength; ++i) {
|
|
|
+ strBuf[i] = String.fromCharCode(font.getByte());
|
|
|
+ }
|
|
|
+ customNames.push(strBuf.join(''));
|
|
|
+ }
|
|
|
+ glyphNames = [];
|
|
|
+ for (i = 0; i < numGlyphs; ++i) {
|
|
|
+ var j = glyphNameIndexes[i];
|
|
|
+ if (j < 258) {
|
|
|
+ glyphNames.push(MacStandardGlyphOrdering[j]);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ glyphNames.push(customNames[j - 258]);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ 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;
|
|
|
}
|
|
|
- if (!contiguous) {
|
|
|
- var offset = (segCount - i) * 2 + bias * 2;
|
|
|
- bias += (end - start + 1);
|
|
|
|
|
|
- idDeltas += string16(0);
|
|
|
- idRangeOffsets += string16(offset);
|
|
|
+ function readNameTable(nameTable) {
|
|
|
+ var start = (font.start ? font.start : 0) + nameTable.offset;
|
|
|
+ font.pos = start;
|
|
|
|
|
|
- for (j = 0, jj = codes.length; j < jj; ++j) {
|
|
|
- glyphsIds += string16(codes[j]);
|
|
|
+ var names = [[], []];
|
|
|
+ var length = nameTable.length, end = start + length;
|
|
|
+ var format = font.getUint16();
|
|
|
+ var FORMAT_0_HEADER_LENGTH = 6;
|
|
|
+ if (format !== 0 || length < FORMAT_0_HEADER_LENGTH) {
|
|
|
+ // unsupported name table format or table "too" small
|
|
|
+ return names;
|
|
|
}
|
|
|
- } else {
|
|
|
- var startCode = codes[0];
|
|
|
+ var numRecords = font.getUint16();
|
|
|
+ var stringsStart = font.getUint16();
|
|
|
+ var records = [];
|
|
|
+ var NAME_RECORD_LENGTH = 12;
|
|
|
+ var i, ii;
|
|
|
|
|
|
- idDeltas += string16((startCode - start) & 0xFFFF);
|
|
|
- idRangeOffsets += string16(0);
|
|
|
+ for (i = 0; i < numRecords &&
|
|
|
+ font.pos + NAME_RECORD_LENGTH <= end; i++) {
|
|
|
+ var r = {
|
|
|
+ platform: font.getUint16(),
|
|
|
+ encoding: font.getUint16(),
|
|
|
+ language: font.getUint16(),
|
|
|
+ name: font.getUint16(),
|
|
|
+ length: font.getUint16(),
|
|
|
+ offset: font.getUint16()
|
|
|
+ };
|
|
|
+ // using only Macintosh and Windows platform/encoding names
|
|
|
+ if ((r.platform === 1 && r.encoding === 0 && r.language === 0) ||
|
|
|
+ (r.platform === 3 && r.encoding === 1 && r.language === 0x409)) {
|
|
|
+ records.push(r);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (i = 0, ii = records.length; i < ii; i++) {
|
|
|
+ var record = records[i];
|
|
|
+ var pos = start + stringsStart + record.offset;
|
|
|
+ if (pos + record.length > end) {
|
|
|
+ continue; // outside of name table, ignoring
|
|
|
+ }
|
|
|
+ font.pos = pos;
|
|
|
+ var nameIndex = record.name;
|
|
|
+ if (record.encoding) {
|
|
|
+ // unicode
|
|
|
+ var str = '';
|
|
|
+ for (var j = 0, jj = record.length; j < jj; j += 2) {
|
|
|
+ str += String.fromCharCode(font.getUint16());
|
|
|
+ }
|
|
|
+ names[1][nameIndex] = str;
|
|
|
+ } else {
|
|
|
+ names[0][nameIndex] = bytesToString(font.getBytes(record.length));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return names;
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- if (trailingRangesCount > 0) {
|
|
|
- endCount += '\xFF\xFF';
|
|
|
- startCount += '\xFF\xFF';
|
|
|
- idDeltas += '\x00\x01';
|
|
|
- idRangeOffsets += '\x00\x00';
|
|
|
- }
|
|
|
+ var TTOpsStackDeltas = [
|
|
|
+ 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -2, -2, 0, 0, -2, -5,
|
|
|
+ -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, -1, -1,
|
|
|
+ 1, -1, -999, 0, 1, 0, -1, -2, 0, -1, -2, -1, -1, 0, -1, -1,
|
|
|
+ 0, 0, -999, -999, -1, -1, -1, -1, -2, -999, -2, -2, -999, 0, -2, -2,
|
|
|
+ 0, 0, -2, 0, -2, 0, 0, 0, -2, -1, -1, 1, 1, 0, 0, -1,
|
|
|
+ -1, -1, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, 0, -999, -1, -1,
|
|
|
+ -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
+ -2, -999, -999, -999, -999, -999, -1, -1, -2, -2, 0, 0, 0, 0, -1, -1,
|
|
|
+ -999, -2, -2, 0, 0, -1, -2, -2, 0, 0, 0, -1, -1, -1, -2];
|
|
|
+ // 0xC0-DF == -1 and 0xE0-FF == -2
|
|
|
|
|
|
- var format314 = '\x00\x00' + // language
|
|
|
- string16(2 * segCount) +
|
|
|
- string16(searchParams.range) +
|
|
|
- string16(searchParams.entry) +
|
|
|
- string16(searchParams.rangeShift) +
|
|
|
- endCount + '\x00\x00' + startCount +
|
|
|
- idDeltas + idRangeOffsets + glyphsIds;
|
|
|
+ function sanitizeTTProgram(table, ttContext) {
|
|
|
+ var data = table.data;
|
|
|
+ var i = 0, j, n, b, funcId, pc, lastEndf = 0, lastDeff = 0;
|
|
|
+ var stack = [];
|
|
|
+ var callstack = [];
|
|
|
+ var functionsCalled = [];
|
|
|
+ var tooComplexToFollowFunctions =
|
|
|
+ ttContext.tooComplexToFollowFunctions;
|
|
|
+ var inFDEF = false, ifLevel = 0, inELSE = 0;
|
|
|
+ for (var ii = data.length; i < ii;) {
|
|
|
+ var op = data[i++];
|
|
|
+ // The TrueType instruction set docs can be found at
|
|
|
+ // https://developer.apple.com/fonts/TTRefMan/RM05/Chap5.html
|
|
|
+ if (op === 0x40) { // NPUSHB - pushes n bytes
|
|
|
+ n = data[i++];
|
|
|
+ if (inFDEF || inELSE) {
|
|
|
+ i += n;
|
|
|
+ } else {
|
|
|
+ for (j = 0; j < n; j++) {
|
|
|
+ stack.push(data[i++]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (op === 0x41) { // NPUSHW - pushes n words
|
|
|
+ n = data[i++];
|
|
|
+ if (inFDEF || inELSE) {
|
|
|
+ i += n * 2;
|
|
|
+ } else {
|
|
|
+ for (j = 0; j < n; j++) {
|
|
|
+ b = data[i++];
|
|
|
+ stack.push((b << 8) | data[i++]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if ((op & 0xF8) === 0xB0) { // PUSHB - pushes bytes
|
|
|
+ n = op - 0xB0 + 1;
|
|
|
+ if (inFDEF || inELSE) {
|
|
|
+ i += n;
|
|
|
+ } else {
|
|
|
+ for (j = 0; j < n; j++) {
|
|
|
+ stack.push(data[i++]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if ((op & 0xF8) === 0xB8) { // PUSHW - pushes words
|
|
|
+ n = op - 0xB8 + 1;
|
|
|
+ if (inFDEF || inELSE) {
|
|
|
+ i += n * 2;
|
|
|
+ } else {
|
|
|
+ for (j = 0; j < n; j++) {
|
|
|
+ b = data[i++];
|
|
|
+ stack.push((b << 8) | data[i++]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (op === 0x2B && !tooComplexToFollowFunctions) { // CALL
|
|
|
+ if (!inFDEF && !inELSE) {
|
|
|
+ // collecting inforamtion about which functions are used
|
|
|
+ funcId = stack[stack.length - 1];
|
|
|
+ ttContext.functionsUsed[funcId] = true;
|
|
|
+ if (funcId in ttContext.functionsStackDeltas) {
|
|
|
+ stack.length += ttContext.functionsStackDeltas[funcId];
|
|
|
+ } else if (funcId in ttContext.functionsDefined &&
|
|
|
+ functionsCalled.indexOf(funcId) < 0) {
|
|
|
+ callstack.push({data: data, i: i, stackTop: stack.length - 1});
|
|
|
+ functionsCalled.push(funcId);
|
|
|
+ pc = ttContext.functionsDefined[funcId];
|
|
|
+ if (!pc) {
|
|
|
+ warn('TT: CALL non-existent function');
|
|
|
+ ttContext.hintsValid = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ data = pc.data;
|
|
|
+ i = pc.i;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (op === 0x2C && !tooComplexToFollowFunctions) { // FDEF
|
|
|
+ if (inFDEF || inELSE) {
|
|
|
+ warn('TT: nested FDEFs not allowed');
|
|
|
+ tooComplexToFollowFunctions = true;
|
|
|
+ }
|
|
|
+ inFDEF = true;
|
|
|
+ // collecting inforamtion about which functions are defined
|
|
|
+ lastDeff = i;
|
|
|
+ funcId = stack.pop();
|
|
|
+ ttContext.functionsDefined[funcId] = {data: data, i: i};
|
|
|
+ } else if (op === 0x2D) { // ENDF - end of function
|
|
|
+ if (inFDEF) {
|
|
|
+ inFDEF = false;
|
|
|
+ lastEndf = i;
|
|
|
+ } else {
|
|
|
+ pc = callstack.pop();
|
|
|
+ if (!pc) {
|
|
|
+ warn('TT: ENDF bad stack');
|
|
|
+ ttContext.hintsValid = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ funcId = functionsCalled.pop();
|
|
|
+ data = pc.data;
|
|
|
+ i = pc.i;
|
|
|
+ ttContext.functionsStackDeltas[funcId] =
|
|
|
+ stack.length - pc.stackTop;
|
|
|
+ }
|
|
|
+ } else if (op === 0x89) { // IDEF - instruction definition
|
|
|
+ if (inFDEF || inELSE) {
|
|
|
+ warn('TT: nested IDEFs not allowed');
|
|
|
+ tooComplexToFollowFunctions = true;
|
|
|
+ }
|
|
|
+ inFDEF = true;
|
|
|
+ // recording it as a function to track ENDF
|
|
|
+ lastDeff = i;
|
|
|
+ } else if (op === 0x58) { // IF
|
|
|
+ ++ifLevel;
|
|
|
+ } else if (op === 0x1B) { // ELSE
|
|
|
+ inELSE = ifLevel;
|
|
|
+ } else if (op === 0x59) { // EIF
|
|
|
+ if (inELSE === ifLevel) {
|
|
|
+ inELSE = 0;
|
|
|
+ }
|
|
|
+ --ifLevel;
|
|
|
+ } else if (op === 0x1C) { // JMPR
|
|
|
+ if (!inFDEF && !inELSE) {
|
|
|
+ var offset = stack[stack.length - 1];
|
|
|
+ // only jumping forward to prevent infinite loop
|
|
|
+ if (offset > 0) {
|
|
|
+ i += offset - 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Adjusting stack not extactly, but just enough to get function id
|
|
|
+ if (!inFDEF && !inELSE) {
|
|
|
+ var stackDelta = op <= 0x8E ? TTOpsStackDeltas[op] :
|
|
|
+ op >= 0xC0 && op <= 0xDF ? -1 : op >= 0xE0 ? -2 : 0;
|
|
|
+ if (op >= 0x71 && op <= 0x75) {
|
|
|
+ n = stack.pop();
|
|
|
+ if (n === n) {
|
|
|
+ stackDelta = -n * 2;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ while (stackDelta < 0 && stack.length > 0) {
|
|
|
+ stack.pop();
|
|
|
+ stackDelta++;
|
|
|
+ }
|
|
|
+ while (stackDelta > 0) {
|
|
|
+ stack.push(NaN); // pushing any number into stack
|
|
|
+ stackDelta--;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ttContext.tooComplexToFollowFunctions = tooComplexToFollowFunctions;
|
|
|
+ var content = [data];
|
|
|
+ if (i > data.length) {
|
|
|
+ content.push(new Uint8Array(i - data.length));
|
|
|
+ }
|
|
|
+ if (lastDeff > lastEndf) {
|
|
|
+ warn('TT: complementing a missing function tail');
|
|
|
+ // new function definition started, but not finished
|
|
|
+ // complete function by [CLEAR, ENDF]
|
|
|
+ content.push(new Uint8Array([0x22, 0x2D]));
|
|
|
+ }
|
|
|
+ foldTTTable(table, content);
|
|
|
+ }
|
|
|
|
|
|
- var format31012 = '';
|
|
|
- var header31012 = '';
|
|
|
- if (numTables > 1) {
|
|
|
- cmap += '\x00\x03' + // platformID
|
|
|
- '\x00\x0A' + // encodingID
|
|
|
- string32(4 + numTables * 8 +
|
|
|
- 4 + format314.length); // start of the table record
|
|
|
- format31012 = '';
|
|
|
- for (i = 0, ii = ranges.length; i < ii; i++) {
|
|
|
- range = ranges[i];
|
|
|
- start = range[0];
|
|
|
- codes = range[2];
|
|
|
- var code = codes[0];
|
|
|
- for (j = 1, jj = codes.length; j < jj; ++j) {
|
|
|
- if (codes[j] !== codes[j - 1] + 1) {
|
|
|
- end = range[0] + j - 1;
|
|
|
- format31012 += string32(start) + // startCharCode
|
|
|
- string32(end) + // endCharCode
|
|
|
- string32(code); // startGlyphID
|
|
|
- start = end + 1;
|
|
|
- code = codes[j];
|
|
|
+ function checkInvalidFunctions(ttContext, maxFunctionDefs) {
|
|
|
+ if (ttContext.tooComplexToFollowFunctions) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (ttContext.functionsDefined.length > maxFunctionDefs) {
|
|
|
+ warn('TT: more functions defined than expected');
|
|
|
+ ttContext.hintsValid = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ for (var j = 0, jj = ttContext.functionsUsed.length; j < jj; j++) {
|
|
|
+ if (j > maxFunctionDefs) {
|
|
|
+ warn('TT: invalid function id: ' + j);
|
|
|
+ ttContext.hintsValid = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (ttContext.functionsUsed[j] && !ttContext.functionsDefined[j]) {
|
|
|
+ warn('TT: undefined function: ' + j);
|
|
|
+ ttContext.hintsValid = false;
|
|
|
+ return;
|
|
|
}
|
|
|
}
|
|
|
- format31012 += string32(start) + // startCharCode
|
|
|
- string32(range[1]) + // endCharCode
|
|
|
- string32(code); // startGlyphID
|
|
|
}
|
|
|
- header31012 = '\x00\x0C' + // format
|
|
|
- '\x00\x00' + // reserved
|
|
|
- string32(format31012.length + 16) + // length
|
|
|
- '\x00\x00\x00\x00' + // language
|
|
|
- string32(format31012.length / 12); // nGroups
|
|
|
- }
|
|
|
|
|
|
- return cmap + '\x00\x04' + // format
|
|
|
- string16(format314.length + 4) + // length
|
|
|
- format314 + header31012 + format31012;
|
|
|
- }
|
|
|
+ function foldTTTable(table, content) {
|
|
|
+ if (content.length > 1) {
|
|
|
+ // concatenating the content items
|
|
|
+ var newLength = 0;
|
|
|
+ var j, jj;
|
|
|
+ for (j = 0, jj = content.length; j < jj; j++) {
|
|
|
+ newLength += content[j].length;
|
|
|
+ }
|
|
|
+ newLength = (newLength + 3) & ~3;
|
|
|
+ var result = new Uint8Array(newLength);
|
|
|
+ var pos = 0;
|
|
|
+ for (j = 0, jj = content.length; j < jj; j++) {
|
|
|
+ result.set(content[j], pos);
|
|
|
+ pos += content[j].length;
|
|
|
+ }
|
|
|
+ table.data = result;
|
|
|
+ table.length = newLength;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- function validateOS2Table(os2) {
|
|
|
- var stream = new Stream(os2.data);
|
|
|
- var version = stream.getUint16();
|
|
|
- // TODO verify all OS/2 tables fields, but currently we validate only those
|
|
|
- // that give us issues
|
|
|
- stream.getBytes(60); // skipping type, misc sizes, panose, unicode ranges
|
|
|
- var selection = stream.getUint16();
|
|
|
- if (version < 4 && (selection & 0x0300)) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- var firstChar = stream.getUint16();
|
|
|
- var lastChar = stream.getUint16();
|
|
|
- if (firstChar > lastChar) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- stream.getBytes(6); // skipping sTypoAscender/Descender/LineGap
|
|
|
- var usWinAscent = stream.getUint16();
|
|
|
- if (usWinAscent === 0) { // makes font unreadable by windows
|
|
|
- return false;
|
|
|
- }
|
|
|
+ function sanitizeTTPrograms(fpgm, prep, cvt) {
|
|
|
+ var ttContext = {
|
|
|
+ functionsDefined: [],
|
|
|
+ functionsUsed: [],
|
|
|
+ functionsStackDeltas: [],
|
|
|
+ tooComplexToFollowFunctions: false,
|
|
|
+ hintsValid: true
|
|
|
+ };
|
|
|
+ if (fpgm) {
|
|
|
+ sanitizeTTProgram(fpgm, ttContext);
|
|
|
+ }
|
|
|
+ if (prep) {
|
|
|
+ sanitizeTTProgram(prep, ttContext);
|
|
|
+ }
|
|
|
+ if (fpgm) {
|
|
|
+ checkInvalidFunctions(ttContext, maxFunctionDefs);
|
|
|
+ }
|
|
|
+ if (cvt && (cvt.length & 1)) {
|
|
|
+ var cvtData = new Uint8Array(cvt.length + 1);
|
|
|
+ cvtData.set(cvt.data);
|
|
|
+ cvt.data = cvtData;
|
|
|
+ }
|
|
|
+ return ttContext.hintsValid;
|
|
|
+ }
|
|
|
|
|
|
- // OS/2 appears to be valid, resetting some fields
|
|
|
- os2.data[8] = os2.data[9] = 0; // IE rejects fonts if fsType != 0
|
|
|
- return true;
|
|
|
- }
|
|
|
+ // The following steps modify the original font data, making copy
|
|
|
+ font = new Stream(new Uint8Array(font.getBytes()));
|
|
|
|
|
|
- function createOS2Table(properties, charstrings, override) {
|
|
|
- override = override || {
|
|
|
- unitsPerEm: 0,
|
|
|
- yMax: 0,
|
|
|
- yMin: 0,
|
|
|
- ascent: 0,
|
|
|
- descent: 0
|
|
|
- };
|
|
|
+ var VALID_TABLES = ['OS/2', 'cmap', 'head', 'hhea', 'hmtx', 'maxp',
|
|
|
+ 'name', 'post', 'loca', 'glyf', 'fpgm', 'prep', 'cvt ', 'CFF '];
|
|
|
|
|
|
- var ulUnicodeRange1 = 0;
|
|
|
- var ulUnicodeRange2 = 0;
|
|
|
- var ulUnicodeRange3 = 0;
|
|
|
- var ulUnicodeRange4 = 0;
|
|
|
+ var header = readOpenTypeHeader(font);
|
|
|
+ var numTables = header.numTables;
|
|
|
+ var cff, cffFile;
|
|
|
|
|
|
- var firstCharIndex = null;
|
|
|
- var lastCharIndex = 0;
|
|
|
+ var tables = Object.create(null);
|
|
|
+ tables['OS/2'] = null;
|
|
|
+ tables['cmap'] = null;
|
|
|
+ tables['head'] = null;
|
|
|
+ tables['hhea'] = null;
|
|
|
+ tables['hmtx'] = null;
|
|
|
+ tables['maxp'] = null;
|
|
|
+ tables['name'] = null;
|
|
|
+ tables['post'] = null;
|
|
|
|
|
|
- if (charstrings) {
|
|
|
- for (var code in charstrings) {
|
|
|
- code |= 0;
|
|
|
- if (firstCharIndex > code || !firstCharIndex) {
|
|
|
- firstCharIndex = code;
|
|
|
- }
|
|
|
- if (lastCharIndex < code) {
|
|
|
- lastCharIndex = code;
|
|
|
+ var table;
|
|
|
+ for (var i = 0; i < numTables; i++) {
|
|
|
+ table = readTableEntry(font);
|
|
|
+ if (VALID_TABLES.indexOf(table.tag) < 0) {
|
|
|
+ continue; // skipping table if it's not a required or optional table
|
|
|
}
|
|
|
-
|
|
|
- var position = getUnicodeRangeFor(code);
|
|
|
- if (position < 32) {
|
|
|
- ulUnicodeRange1 |= 1 << position;
|
|
|
- } else if (position < 64) {
|
|
|
- ulUnicodeRange2 |= 1 << position - 32;
|
|
|
- } else if (position < 96) {
|
|
|
- ulUnicodeRange3 |= 1 << position - 64;
|
|
|
- } else if (position < 123) {
|
|
|
- ulUnicodeRange4 |= 1 << position - 96;
|
|
|
- } else {
|
|
|
- error('Unicode ranges Bits > 123 are reserved for internal usage');
|
|
|
+ if (table.length === 0) {
|
|
|
+ continue; // skipping empty tables
|
|
|
}
|
|
|
+ tables[table.tag] = table;
|
|
|
}
|
|
|
- } else {
|
|
|
- // TODO
|
|
|
- firstCharIndex = 0;
|
|
|
- lastCharIndex = 255;
|
|
|
- }
|
|
|
-
|
|
|
- var bbox = properties.bbox || [0, 0, 0, 0];
|
|
|
- var unitsPerEm = (override.unitsPerEm ||
|
|
|
- 1 / (properties.fontMatrix || FONT_IDENTITY_MATRIX)[0]);
|
|
|
|
|
|
- // if the font units differ to the PDF glyph space units
|
|
|
- // then scale up the values
|
|
|
- var scale = (properties.ascentScaled ? 1.0 :
|
|
|
- unitsPerEm / PDF_GLYPH_SPACE_UNITS);
|
|
|
+ var isTrueType = !tables['CFF '];
|
|
|
+ if (!isTrueType) {
|
|
|
+ // OpenType font
|
|
|
+ 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);
|
|
|
|
|
|
- var typoAscent = (override.ascent ||
|
|
|
- Math.round(scale * (properties.ascent || bbox[3])));
|
|
|
- var typoDescent = (override.descent ||
|
|
|
- Math.round(scale * (properties.descent || bbox[1])));
|
|
|
- if (typoDescent > 0 && properties.descent > 0 && bbox[1] < 0) {
|
|
|
- typoDescent = -typoDescent; // fixing incorrect descent
|
|
|
- }
|
|
|
- var winAscent = override.yMax || typoAscent;
|
|
|
- var winDescent = -override.yMin || -typoDescent;
|
|
|
+ adjustWidths(properties);
|
|
|
|
|
|
- return '\x00\x03' + // version
|
|
|
- '\x02\x24' + // xAvgCharWidth
|
|
|
- '\x01\xF4' + // usWeightClass
|
|
|
- '\x00\x05' + // usWidthClass
|
|
|
- '\x00\x00' + // fstype (0 to let the font loads via font-face on IE)
|
|
|
- '\x02\x8A' + // ySubscriptXSize
|
|
|
- '\x02\xBB' + // ySubscriptYSize
|
|
|
- '\x00\x00' + // ySubscriptXOffset
|
|
|
- '\x00\x8C' + // ySubscriptYOffset
|
|
|
- '\x02\x8A' + // ySuperScriptXSize
|
|
|
- '\x02\xBB' + // ySuperScriptYSize
|
|
|
- '\x00\x00' + // ySuperScriptXOffset
|
|
|
- '\x01\xDF' + // ySuperScriptYOffset
|
|
|
- '\x00\x31' + // yStrikeOutSize
|
|
|
- '\x01\x02' + // yStrikeOutPosition
|
|
|
- '\x00\x00' + // sFamilyClass
|
|
|
- '\x00\x00\x06' +
|
|
|
- String.fromCharCode(properties.fixedPitch ? 0x09 : 0x00) +
|
|
|
- '\x00\x00\x00\x00\x00\x00' + // Panose
|
|
|
- string32(ulUnicodeRange1) + // ulUnicodeRange1 (Bits 0-31)
|
|
|
- string32(ulUnicodeRange2) + // ulUnicodeRange2 (Bits 32-63)
|
|
|
- string32(ulUnicodeRange3) + // ulUnicodeRange3 (Bits 64-95)
|
|
|
- string32(ulUnicodeRange4) + // ulUnicodeRange4 (Bits 96-127)
|
|
|
- '\x2A\x32\x31\x2A' + // achVendID
|
|
|
- string16(properties.italicAngle ? 1 : 0) + // fsSelection
|
|
|
- string16(firstCharIndex ||
|
|
|
- properties.firstChar) + // usFirstCharIndex
|
|
|
- string16(lastCharIndex || properties.lastChar) + // usLastCharIndex
|
|
|
- string16(typoAscent) + // sTypoAscender
|
|
|
- string16(typoDescent) + // sTypoDescender
|
|
|
- '\x00\x64' + // sTypoLineGap (7%-10% of the unitsPerEM value)
|
|
|
- string16(winAscent) + // usWinAscent
|
|
|
- string16(winDescent) + // usWinDescent
|
|
|
- '\x00\x00\x00\x00' + // ulCodePageRange1 (Bits 0-31)
|
|
|
- '\x00\x00\x00\x00' + // ulCodePageRange2 (Bits 32-63)
|
|
|
- string16(properties.xHeight) + // sxHeight
|
|
|
- string16(properties.capHeight) + // sCapHeight
|
|
|
- string16(0) + // usDefaultChar
|
|
|
- string16(firstCharIndex || properties.firstChar) + // usBreakChar
|
|
|
- '\x00\x03'; // usMaxContext
|
|
|
- }
|
|
|
+ return this.convert(name, cff, properties);
|
|
|
+ }
|
|
|
|
|
|
- function createPostTable(properties) {
|
|
|
- var angle = Math.floor(properties.italicAngle * (Math.pow(2, 16)));
|
|
|
- return ('\x00\x03\x00\x00' + // Version number
|
|
|
- string32(angle) + // italicAngle
|
|
|
- '\x00\x00' + // underlinePosition
|
|
|
- '\x00\x00' + // underlineThickness
|
|
|
- string32(properties.fixedPitch) + // isFixedPitch
|
|
|
- '\x00\x00\x00\x00' + // minMemType42
|
|
|
- '\x00\x00\x00\x00' + // maxMemType42
|
|
|
- '\x00\x00\x00\x00' + // minMemType1
|
|
|
- '\x00\x00\x00\x00'); // maxMemType1
|
|
|
- }
|
|
|
+ delete tables['glyf'];
|
|
|
+ delete tables['loca'];
|
|
|
+ delete tables['fpgm'];
|
|
|
+ delete tables['prep'];
|
|
|
+ delete tables['cvt '];
|
|
|
+ this.isOpenType = true;
|
|
|
+ } else {
|
|
|
+ if (!tables['loca']) {
|
|
|
+ error('Required "loca" table is not found');
|
|
|
+ }
|
|
|
+ if (!tables['glyf']) {
|
|
|
+ warn('Required "glyf" table is not found -- trying to recover.');
|
|
|
+ // Note: We use `sanitizeGlyphLocations` to add dummy glyf data below.
|
|
|
+ tables['glyf'] = {
|
|
|
+ tag: 'glyf',
|
|
|
+ data: new Uint8Array(0),
|
|
|
+ };
|
|
|
+ }
|
|
|
+ this.isOpenType = false;
|
|
|
+ }
|
|
|
|
|
|
- function createNameTable(name, proto) {
|
|
|
- if (!proto) {
|
|
|
- proto = [[], []]; // no strings and unicode strings
|
|
|
- }
|
|
|
+ if (!tables['maxp']) {
|
|
|
+ error('Required "maxp" table is not found');
|
|
|
+ }
|
|
|
|
|
|
- var strings = [
|
|
|
- proto[0][0] || 'Original licence', // 0.Copyright
|
|
|
- proto[0][1] || name, // 1.Font family
|
|
|
- proto[0][2] || 'Unknown', // 2.Font subfamily (font weight)
|
|
|
- proto[0][3] || 'uniqueID', // 3.Unique ID
|
|
|
- proto[0][4] || name, // 4.Full font name
|
|
|
- proto[0][5] || 'Version 0.11', // 5.Version
|
|
|
- proto[0][6] || '', // 6.Postscript name
|
|
|
- proto[0][7] || 'Unknown', // 7.Trademark
|
|
|
- proto[0][8] || 'Unknown', // 8.Manufacturer
|
|
|
- proto[0][9] || 'Unknown' // 9.Designer
|
|
|
- ];
|
|
|
+ font.pos = (font.start || 0) + tables['maxp'].offset;
|
|
|
+ var version = font.getInt32();
|
|
|
+ var numGlyphs = font.getUint16();
|
|
|
+ var maxFunctionDefs = 0;
|
|
|
+ if (version >= 0x00010000 && tables['maxp'].length >= 22) {
|
|
|
+ // maxZones can be invalid
|
|
|
+ font.pos += 8;
|
|
|
+ var maxZones = font.getUint16();
|
|
|
+ if (maxZones > 2) { // reset to 2 if font has invalid maxZones
|
|
|
+ tables['maxp'].data[14] = 0;
|
|
|
+ tables['maxp'].data[15] = 2;
|
|
|
+ }
|
|
|
+ font.pos += 4;
|
|
|
+ maxFunctionDefs = font.getUint16();
|
|
|
+ }
|
|
|
|
|
|
- // Mac want 1-byte per character strings while Windows want
|
|
|
- // 2-bytes per character, so duplicate the names table
|
|
|
- var stringsUnicode = [];
|
|
|
- var i, ii, j, jj, str;
|
|
|
- for (i = 0, ii = strings.length; i < ii; i++) {
|
|
|
- str = proto[1][i] || strings[i];
|
|
|
+ var dupFirstEntry = false;
|
|
|
+ if (properties.type === 'CIDFontType2' && properties.toUnicode &&
|
|
|
+ properties.toUnicode.get(0) > '\u0000') {
|
|
|
+ // oracle's defect (see 3427), duplicating first entry
|
|
|
+ dupFirstEntry = true;
|
|
|
+ numGlyphs++;
|
|
|
+ tables['maxp'].data[4] = numGlyphs >> 8;
|
|
|
+ tables['maxp'].data[5] = numGlyphs & 255;
|
|
|
+ }
|
|
|
|
|
|
- var strBufUnicode = [];
|
|
|
- for (j = 0, jj = str.length; j < jj; j++) {
|
|
|
- strBufUnicode.push(string16(str.charCodeAt(j)));
|
|
|
+ var hintsValid = sanitizeTTPrograms(tables['fpgm'], tables['prep'],
|
|
|
+ tables['cvt '], maxFunctionDefs);
|
|
|
+ if (!hintsValid) {
|
|
|
+ delete tables['fpgm'];
|
|
|
+ delete tables['prep'];
|
|
|
+ delete tables['cvt '];
|
|
|
}
|
|
|
- stringsUnicode.push(strBufUnicode.join(''));
|
|
|
- }
|
|
|
|
|
|
- var names = [strings, stringsUnicode];
|
|
|
- var platforms = ['\x00\x01', '\x00\x03'];
|
|
|
- var encodings = ['\x00\x00', '\x00\x01'];
|
|
|
- var languages = ['\x00\x00', '\x04\x09'];
|
|
|
+ // Ensure the hmtx table contains the advance width and
|
|
|
+ // sidebearings information for numGlyphs in the maxp table
|
|
|
+ sanitizeMetrics(font, tables['hhea'], tables['hmtx'], numGlyphs);
|
|
|
|
|
|
- var namesRecordCount = strings.length * platforms.length;
|
|
|
- var nameTable =
|
|
|
- '\x00\x00' + // format
|
|
|
- string16(namesRecordCount) + // Number of names Record
|
|
|
- string16(namesRecordCount * 12 + 6); // Storage
|
|
|
-
|
|
|
- // Build the name records field
|
|
|
- var strOffset = 0;
|
|
|
- for (i = 0, ii = platforms.length; i < ii; i++) {
|
|
|
- var strs = names[i];
|
|
|
- for (j = 0, jj = strs.length; j < jj; j++) {
|
|
|
- str = strs[j];
|
|
|
- var nameRecord =
|
|
|
- platforms[i] + // platform ID
|
|
|
- encodings[i] + // encoding ID
|
|
|
- languages[i] + // language ID
|
|
|
- string16(j) + // name ID
|
|
|
- string16(str.length) +
|
|
|
- string16(strOffset);
|
|
|
- nameTable += nameRecord;
|
|
|
- strOffset += str.length;
|
|
|
+ if (!tables['head']) {
|
|
|
+ error('Required "head" table is not found');
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- nameTable += strings.join('') + stringsUnicode.join('');
|
|
|
- return nameTable;
|
|
|
- }
|
|
|
+ sanitizeHead(tables['head'], numGlyphs,
|
|
|
+ isTrueType ? tables['loca'].length : 0);
|
|
|
|
|
|
- Font.prototype = {
|
|
|
- name: null,
|
|
|
- font: null,
|
|
|
- mimetype: null,
|
|
|
- encoding: null,
|
|
|
- get renderer() {
|
|
|
- var renderer = FontRendererFactory.create(this);
|
|
|
- return shadow(this, 'renderer', renderer);
|
|
|
- },
|
|
|
+ var missingGlyphs = Object.create(null);
|
|
|
+ if (isTrueType) {
|
|
|
+ var isGlyphLocationsLong = int16(tables['head'].data[50],
|
|
|
+ tables['head'].data[51]);
|
|
|
+ missingGlyphs = sanitizeGlyphLocations(tables['loca'], tables['glyf'],
|
|
|
+ numGlyphs, isGlyphLocationsLong,
|
|
|
+ hintsValid, dupFirstEntry);
|
|
|
+ }
|
|
|
|
|
|
- exportData: function Font_exportData() {
|
|
|
- // TODO remove enumerating of the properties, e.g. hardcode exact names.
|
|
|
- var data = {};
|
|
|
- for (var i in this) {
|
|
|
- if (this.hasOwnProperty(i)) {
|
|
|
- data[i] = this[i];
|
|
|
- }
|
|
|
+ if (!tables['hhea']) {
|
|
|
+ error('Required "hhea" table is not found');
|
|
|
}
|
|
|
- return data;
|
|
|
- },
|
|
|
|
|
|
- checkAndRepair: function Font_checkAndRepair(name, font, properties) {
|
|
|
- function readTableEntry(file) {
|
|
|
- var tag = bytesToString(file.getBytes(4));
|
|
|
+ // Sanitizer reduces the glyph advanceWidth to the maxAdvanceWidth
|
|
|
+ // Sometimes it's 0. That needs to be fixed
|
|
|
+ if (tables['hhea'].data[10] === 0 && tables['hhea'].data[11] === 0) {
|
|
|
+ tables['hhea'].data[10] = 0xFF;
|
|
|
+ tables['hhea'].data[11] = 0xFF;
|
|
|
+ }
|
|
|
|
|
|
- var checksum = file.getInt32();
|
|
|
- var offset = file.getInt32() >>> 0;
|
|
|
- var length = file.getInt32() >>> 0;
|
|
|
+ // 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
|
|
|
+ };
|
|
|
|
|
|
- // Read the table associated data
|
|
|
- var previousPosition = file.pos;
|
|
|
- file.pos = file.start ? file.start : 0;
|
|
|
- file.skip(offset);
|
|
|
- var data = file.getBytes(length);
|
|
|
- file.pos = previousPosition;
|
|
|
+ // PDF FontDescriptor metrics lie -- using data from actual font.
|
|
|
+ this.ascent = metricsOverride.ascent / metricsOverride.unitsPerEm;
|
|
|
+ this.descent = metricsOverride.descent / metricsOverride.unitsPerEm;
|
|
|
|
|
|
- if (tag === 'head') {
|
|
|
- // clearing checksum adjustment
|
|
|
- data[8] = data[9] = data[10] = data[11] = 0;
|
|
|
- data[17] |= 0x20; //Set font optimized for cleartype flag
|
|
|
+ // The 'post' table has glyphs names.
|
|
|
+ if (tables['post']) {
|
|
|
+ var valid = readPostScriptTable(tables['post'], properties, numGlyphs);
|
|
|
+ if (!valid) {
|
|
|
+ tables['post'] = null;
|
|
|
}
|
|
|
-
|
|
|
- return {
|
|
|
- tag: tag,
|
|
|
- checksum: checksum,
|
|
|
- length: length,
|
|
|
- offset: offset,
|
|
|
- data: data
|
|
|
- };
|
|
|
}
|
|
|
|
|
|
- function readOpenTypeHeader(ttf) {
|
|
|
- return {
|
|
|
- version: bytesToString(ttf.getBytes(4)),
|
|
|
- numTables: ttf.getUint16(),
|
|
|
- searchRange: ttf.getUint16(),
|
|
|
- entrySelector: ttf.getUint16(),
|
|
|
- rangeShift: ttf.getUint16()
|
|
|
- };
|
|
|
- }
|
|
|
+ var charCodeToGlyphId = [], charCode;
|
|
|
+ var toUnicode = properties.toUnicode, widths = properties.widths;
|
|
|
+ var skipToUnicode = (toUnicode instanceof IdentityToUnicodeMap ||
|
|
|
+ toUnicode.length === 0x10000);
|
|
|
|
|
|
- /**
|
|
|
- * Read the appropriate subtable from the cmap according to 9.6.6.4 from
|
|
|
- * PDF spec
|
|
|
- */
|
|
|
- function readCmapTable(cmap, font, isSymbolicFont, hasEncoding) {
|
|
|
- if (!cmap) {
|
|
|
- warn('No cmap table available.');
|
|
|
- return {
|
|
|
- platformId: -1,
|
|
|
- encodingId: -1,
|
|
|
- mappings: [],
|
|
|
- hasShortCmap: false
|
|
|
- };
|
|
|
+ // 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;
|
|
|
}
|
|
|
- var segment;
|
|
|
- var start = (font.start ? font.start : 0) + cmap.offset;
|
|
|
- font.pos = start;
|
|
|
-
|
|
|
- var version = font.getUint16();
|
|
|
- var numTables = font.getUint16();
|
|
|
+ if (!skipToUnicode && charCode >= 0 && toUnicode.has(charCode)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (widths && widthCode >= 0 && isNum(widths[widthCode])) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
|
|
|
- var potentialTable;
|
|
|
- var canBreak = false;
|
|
|
- // There's an order of preference in terms of which cmap subtable to
|
|
|
- // use:
|
|
|
- // - non-symbolic fonts the preference is a 3,1 table then a 1,0 table
|
|
|
- // - symbolic fonts the preference is a 3,0 table then a 1,0 table
|
|
|
- // The following takes advantage of the fact that the tables are sorted
|
|
|
- // to work.
|
|
|
- for (var i = 0; i < numTables; i++) {
|
|
|
- var platformId = font.getUint16();
|
|
|
- var encodingId = font.getUint16();
|
|
|
- var offset = font.getInt32() >>> 0;
|
|
|
- var useTable = false;
|
|
|
+ if (properties.type === 'CIDFontType2') {
|
|
|
+ var cidToGidMap = properties.cidToGidMap || [];
|
|
|
+ var isCidToGidMapEmpty = cidToGidMap.length === 0;
|
|
|
|
|
|
- if (platformId === 0 && encodingId === 0) {
|
|
|
- useTable = true;
|
|
|
- // Continue the loop since there still may be a higher priority
|
|
|
- // table.
|
|
|
- } else if (platformId === 1 && encodingId === 0) {
|
|
|
- useTable = 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;
|
|
|
+ properties.cMap.forEach(function(charCode, cid) {
|
|
|
+ assert(cid <= 0xffff, 'Max size of CID is 65,535');
|
|
|
+ var glyphId = -1;
|
|
|
+ if (isCidToGidMapEmpty) {
|
|
|
+ glyphId = charCode;
|
|
|
+ } else if (cidToGidMap[cid] !== undefined) {
|
|
|
+ glyphId = cidToGidMap[cid];
|
|
|
}
|
|
|
|
|
|
- if (useTable) {
|
|
|
- potentialTable = {
|
|
|
- platformId: platformId,
|
|
|
- encodingId: encodingId,
|
|
|
- offset: offset
|
|
|
- };
|
|
|
- }
|
|
|
- if (canBreak) {
|
|
|
- break;
|
|
|
+ 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 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;
|
|
|
|
|
|
- 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
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- var format = font.getUint16();
|
|
|
- var length = font.getUint16();
|
|
|
- var language = font.getUint16();
|
|
|
-
|
|
|
- var hasShortCmap = false;
|
|
|
- var mappings = [];
|
|
|
- var j, glyphId;
|
|
|
-
|
|
|
- // TODO(mack): refactor this cmap subtable reading logic out
|
|
|
- if (format === 0) {
|
|
|
- for (j = 0; j < 256; j++) {
|
|
|
- var index = font.getByte();
|
|
|
- if (!index) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- mappings.push({
|
|
|
- charCode: j,
|
|
|
- glyphId: index
|
|
|
- });
|
|
|
- }
|
|
|
- hasShortCmap = true;
|
|
|
- } else if (format === 4) {
|
|
|
- // re-creating the table in format 4 since the encoding
|
|
|
- // might be changed
|
|
|
- var segCount = (font.getUint16() >> 1);
|
|
|
- font.getBytes(6); // skipping range fields
|
|
|
- var segIndex, segments = [];
|
|
|
- for (segIndex = 0; segIndex < segCount; segIndex++) {
|
|
|
- segments.push({ end: font.getUint16() });
|
|
|
- }
|
|
|
- font.getUint16();
|
|
|
- for (segIndex = 0; segIndex < segCount; segIndex++) {
|
|
|
- segments[segIndex].start = font.getUint16();
|
|
|
- }
|
|
|
+ // 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 &&
|
|
|
+ (cmapPlatformId === 3 && cmapEncodingId === 1 ||
|
|
|
+ cmapPlatformId === 1 && cmapEncodingId === 0) ||
|
|
|
+ (cmapPlatformId === -1 && cmapEncodingId === -1 && // Temporary hack
|
|
|
+ !!getEncoding(properties.baseEncodingName))) { // Temporary hack
|
|
|
+ // When no preferred cmap table was found and |baseEncodingName| is
|
|
|
+ // one of the predefined encodings, we seem to obtain a better
|
|
|
+ // |charCodeToGlyphId| map from the code below (fixes bug 1057544).
|
|
|
+ // TODO: Note that this is a hack which should be removed as soon as
|
|
|
+ // we have proper support for more exotic cmap tables.
|
|
|
|
|
|
- for (segIndex = 0; segIndex < segCount; segIndex++) {
|
|
|
- segments[segIndex].delta = font.getUint16();
|
|
|
+ var baseEncoding = [];
|
|
|
+ if (properties.baseEncodingName === 'MacRomanEncoding' ||
|
|
|
+ properties.baseEncodingName === 'WinAnsiEncoding') {
|
|
|
+ baseEncoding = getEncoding(properties.baseEncodingName);
|
|
|
}
|
|
|
-
|
|
|
- var offsetsCount = 0;
|
|
|
- for (segIndex = 0; segIndex < segCount; segIndex++) {
|
|
|
- segment = segments[segIndex];
|
|
|
- var rangeOffset = font.getUint16();
|
|
|
- if (!rangeOffset) {
|
|
|
- segment.offsetIndex = -1;
|
|
|
+ var glyphsUnicodeMap = getGlyphsUnicode();
|
|
|
+ for (charCode = 0; charCode < 256; charCode++) {
|
|
|
+ var glyphName;
|
|
|
+ if (this.differences && charCode in this.differences) {
|
|
|
+ glyphName = this.differences[charCode];
|
|
|
+ } else if (charCode in baseEncoding &&
|
|
|
+ baseEncoding[charCode] !== '') {
|
|
|
+ glyphName = baseEncoding[charCode];
|
|
|
+ } else {
|
|
|
+ glyphName = StandardEncoding[charCode];
|
|
|
+ }
|
|
|
+ if (!glyphName) {
|
|
|
continue;
|
|
|
}
|
|
|
+ var unicodeOrCharCode, isUnicode = false;
|
|
|
+ if (cmapPlatformId === 3 && cmapEncodingId === 1) {
|
|
|
+ unicodeOrCharCode = glyphsUnicodeMap[glyphName];
|
|
|
+ isUnicode = true;
|
|
|
+ } else if (cmapPlatformId === 1 && cmapEncodingId === 0) {
|
|
|
+ // TODO: the encoding needs to be updated with mac os table.
|
|
|
+ unicodeOrCharCode = MacRomanEncoding.indexOf(glyphName);
|
|
|
+ }
|
|
|
|
|
|
- var offsetIndex = (rangeOffset >> 1) - (segCount - segIndex);
|
|
|
- segment.offsetIndex = offsetIndex;
|
|
|
- offsetsCount = Math.max(offsetsCount, offsetIndex +
|
|
|
- segment.end - segment.start + 1);
|
|
|
- }
|
|
|
-
|
|
|
- var offsets = [];
|
|
|
- for (j = 0; j < offsetsCount; j++) {
|
|
|
- offsets.push(font.getUint16());
|
|
|
- }
|
|
|
-
|
|
|
- for (segIndex = 0; segIndex < segCount; segIndex++) {
|
|
|
- segment = segments[segIndex];
|
|
|
- start = segment.start;
|
|
|
- var end = segment.end;
|
|
|
- var delta = segment.delta;
|
|
|
- offsetIndex = segment.offsetIndex;
|
|
|
-
|
|
|
- for (j = start; j <= end; j++) {
|
|
|
- if (j === 0xFFFF) {
|
|
|
+ var found = false;
|
|
|
+ for (i = 0; i < cmapMappingsLength; ++i) {
|
|
|
+ if (cmapMappings[i].charCode !== unicodeOrCharCode) {
|
|
|
continue;
|
|
|
}
|
|
|
-
|
|
|
- glyphId = (offsetIndex < 0 ?
|
|
|
- j : offsets[offsetIndex + j - start]);
|
|
|
- glyphId = (glyphId + delta) & 0xFFFF;
|
|
|
- if (glyphId === 0) {
|
|
|
- continue;
|
|
|
+ var code = isUnicode ? charCode : unicodeOrCharCode;
|
|
|
+ if (hasGlyph(cmapMappings[i].glyphId, code, -1)) {
|
|
|
+ charCodeToGlyphId[charCode] = cmapMappings[i].glyphId;
|
|
|
+ found = true;
|
|
|
+ break;
|
|
|
}
|
|
|
- mappings.push({
|
|
|
- charCode: j,
|
|
|
- glyphId: glyphId
|
|
|
- });
|
|
|
+ }
|
|
|
+ if (!found && properties.glyphNames) {
|
|
|
+ // Try to map using the post table.
|
|
|
+ var glyphId = properties.glyphNames.indexOf(glyphName);
|
|
|
+ if (glyphId > 0 && hasGlyph(glyphId, -1, -1)) {
|
|
|
+ charCodeToGlyphId[charCode] = glyphId;
|
|
|
+ found = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!found) {
|
|
|
+ charCodeToGlyphId[charCode] = 0; // notdef
|
|
|
}
|
|
|
}
|
|
|
- } else if (format === 6) {
|
|
|
- // Format 6 is a 2-bytes dense mapping, which means the font data
|
|
|
- // lives glue together even if they are pretty far in the unicode
|
|
|
- // table. (This looks weird, so I can have missed something), this
|
|
|
- // works on Linux but seems to fails on Mac so let's rewrite the
|
|
|
- // cmap table to a 3-1-4 style
|
|
|
- var firstCode = font.getUint16();
|
|
|
- var entryCount = font.getUint16();
|
|
|
-
|
|
|
- for (j = 0; j < entryCount; j++) {
|
|
|
- glyphId = font.getUint16();
|
|
|
- var charCode = firstCode + j;
|
|
|
-
|
|
|
- mappings.push({
|
|
|
- charCode: charCode,
|
|
|
- glyphId: glyphId
|
|
|
- });
|
|
|
+ } 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 {
|
|
|
- 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;
|
|
|
- });
|
|
|
- for (i = 1; i < mappings.length; i++) {
|
|
|
- if (mappings[i - 1].charCode === mappings[i].charCode) {
|
|
|
- mappings.splice(i, 1);
|
|
|
- i--;
|
|
|
+ // 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
|
|
|
+ // high byte of the range [of charcodes in the cmap table], to form
|
|
|
+ // a two-byte character, which shall be used to select the
|
|
|
+ // associated glyph description from the subtable'.
|
|
|
+ //
|
|
|
+ // For (1, 0) cmap tables:
|
|
|
+ // 'single bytes from the string shall be used to look up the
|
|
|
+ // associated glyph descriptions from the subtable'. This means
|
|
|
+ // charcodes in the cmap will be single bytes, so no-op since
|
|
|
+ // glyph.charCode & 0xFF === glyph.charCode
|
|
|
+ for (i = 0; i < cmapMappingsLength; ++i) {
|
|
|
+ charCode = cmapMappings[i].charCode & 0xFF;
|
|
|
+ charCodeToGlyphId[charCode] = cmapMappings[i].glyphId;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- return {
|
|
|
- platformId: potentialTable.platformId,
|
|
|
- encodingId: potentialTable.encodingId,
|
|
|
- mappings: mappings,
|
|
|
- hasShortCmap: hasShortCmap
|
|
|
- };
|
|
|
}
|
|
|
|
|
|
- function sanitizeMetrics(font, header, metrics, numGlyphs) {
|
|
|
- if (!header) {
|
|
|
- if (metrics) {
|
|
|
- metrics.data = null;
|
|
|
- }
|
|
|
- return;
|
|
|
- }
|
|
|
+ if (charCodeToGlyphId.length === 0) {
|
|
|
+ // defines at least one glyph
|
|
|
+ charCodeToGlyphId[0] = 0;
|
|
|
+ }
|
|
|
|
|
|
- font.pos = (font.start ? font.start : 0) + header.offset;
|
|
|
- font.pos += header.length - 2;
|
|
|
- var numOfMetrics = font.getUint16();
|
|
|
+ // 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, numGlyphs)
|
|
|
+ };
|
|
|
|
|
|
- if (numOfMetrics > numGlyphs) {
|
|
|
- info('The numOfMetrics (' + numOfMetrics + ') should not be ' +
|
|
|
- 'greater than the numGlyphs (' + numGlyphs + ')');
|
|
|
- // Reduce numOfMetrics if it is greater than numGlyphs
|
|
|
- numOfMetrics = numGlyphs;
|
|
|
- header.data[34] = (numOfMetrics & 0xff00) >> 8;
|
|
|
- header.data[35] = numOfMetrics & 0x00ff;
|
|
|
- }
|
|
|
+ if (!tables['OS/2'] || !validateOS2Table(tables['OS/2'])) {
|
|
|
+ tables['OS/2'] = {
|
|
|
+ tag: 'OS/2',
|
|
|
+ data: createOS2Table(properties, newMapping.charCodeToGlyphId,
|
|
|
+ metricsOverride)
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
- var numOfSidebearings = numGlyphs - numOfMetrics;
|
|
|
- var numMissing = numOfSidebearings -
|
|
|
- ((metrics.length - numOfMetrics * 4) >> 1);
|
|
|
+ // Rewrite the 'post' table if needed
|
|
|
+ if (!tables['post']) {
|
|
|
+ tables['post'] = {
|
|
|
+ tag: 'post',
|
|
|
+ data: createPostTable(properties)
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
- if (numMissing > 0) {
|
|
|
- // For each missing glyph, we set both the width and lsb to 0 (zero).
|
|
|
- // Since we need to add two properties for each glyph, this explains
|
|
|
- // the use of |numMissing * 2| when initializing the typed array.
|
|
|
- var entries = new Uint8Array(metrics.length + numMissing * 2);
|
|
|
- entries.set(metrics.data);
|
|
|
- metrics.data = entries;
|
|
|
+ if (!isTrueType) {
|
|
|
+ try {
|
|
|
+ // Trying to repair CFF file
|
|
|
+ cffFile = new Stream(tables['CFF '].data);
|
|
|
+ var parser = new CFFParser(cffFile, properties);
|
|
|
+ cff = parser.parse();
|
|
|
+ var compiler = new CFFCompiler(cff);
|
|
|
+ tables['CFF '].data = compiler.compile();
|
|
|
+ } catch (e) {
|
|
|
+ warn('Failed to compile font ' + properties.loadedName);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- function sanitizeGlyph(source, sourceStart, sourceEnd, dest, destStart,
|
|
|
- hintsValid) {
|
|
|
- if (sourceEnd - sourceStart <= 12) {
|
|
|
- // glyph with data less than 12 is invalid one
|
|
|
- return 0;
|
|
|
- }
|
|
|
- var glyf = source.subarray(sourceStart, sourceEnd);
|
|
|
- var contoursCount = (glyf[0] << 8) | glyf[1];
|
|
|
- if (contoursCount & 0x8000) {
|
|
|
- // complex glyph, writing as is
|
|
|
- dest.set(glyf, destStart);
|
|
|
- return glyf.length;
|
|
|
- }
|
|
|
+ // Re-creating 'name' table
|
|
|
+ if (!tables['name']) {
|
|
|
+ tables['name'] = {
|
|
|
+ tag: 'name',
|
|
|
+ data: createNameTable(this.name)
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ // ... using existing 'name' table as prototype
|
|
|
+ var namePrototype = readNameTable(tables['name']);
|
|
|
+ tables['name'].data = createNameTable(name, namePrototype);
|
|
|
+ }
|
|
|
|
|
|
- var i, j = 10, flagsCount = 0;
|
|
|
- for (i = 0; i < contoursCount; i++) {
|
|
|
- var endPoint = (glyf[j] << 8) | glyf[j + 1];
|
|
|
- flagsCount = endPoint + 1;
|
|
|
- j += 2;
|
|
|
- }
|
|
|
- // skipping instructions
|
|
|
- var instructionsStart = j;
|
|
|
- var instructionsLength = (glyf[j] << 8) | glyf[j + 1];
|
|
|
- j += 2 + instructionsLength;
|
|
|
- var instructionsEnd = j;
|
|
|
- // validating flags
|
|
|
- var coordinatesLength = 0;
|
|
|
- for (i = 0; i < flagsCount; i++) {
|
|
|
- var flag = glyf[j++];
|
|
|
- if (flag & 0xC0) {
|
|
|
- // reserved flags must be zero, cleaning up
|
|
|
- glyf[j - 1] = flag & 0x3F;
|
|
|
- }
|
|
|
- var xyLength = ((flag & 2) ? 1 : (flag & 16) ? 0 : 2) +
|
|
|
- ((flag & 4) ? 1 : (flag & 32) ? 0 : 2);
|
|
|
- coordinatesLength += xyLength;
|
|
|
- if (flag & 8) {
|
|
|
- var repeat = glyf[j++];
|
|
|
- i += repeat;
|
|
|
- coordinatesLength += repeat * xyLength;
|
|
|
- }
|
|
|
- }
|
|
|
- // glyph without coordinates will be rejected
|
|
|
- if (coordinatesLength === 0) {
|
|
|
- return 0;
|
|
|
- }
|
|
|
- var glyphDataLength = j + coordinatesLength;
|
|
|
- if (glyphDataLength > glyf.length) {
|
|
|
- // not enough data for coordinates
|
|
|
- return 0;
|
|
|
- }
|
|
|
- if (!hintsValid && instructionsLength > 0) {
|
|
|
- dest.set(glyf.subarray(0, instructionsStart), destStart);
|
|
|
- dest.set([0, 0], destStart + instructionsStart);
|
|
|
- dest.set(glyf.subarray(instructionsEnd, glyphDataLength),
|
|
|
- destStart + instructionsStart + 2);
|
|
|
- glyphDataLength -= instructionsLength;
|
|
|
- if (glyf.length - glyphDataLength > 3) {
|
|
|
- glyphDataLength = (glyphDataLength + 3) & ~3;
|
|
|
- }
|
|
|
- return glyphDataLength;
|
|
|
- }
|
|
|
- if (glyf.length - glyphDataLength > 3) {
|
|
|
- // truncating and aligning to 4 bytes the long glyph data
|
|
|
- glyphDataLength = (glyphDataLength + 3) & ~3;
|
|
|
- dest.set(glyf.subarray(0, glyphDataLength), destStart);
|
|
|
- return glyphDataLength;
|
|
|
- }
|
|
|
- // glyph data is fine
|
|
|
- dest.set(glyf, destStart);
|
|
|
- return glyf.length;
|
|
|
+ var builder = new OpenTypeFileBuilder(header.version);
|
|
|
+ for (var tableTag in tables) {
|
|
|
+ builder.addTable(tableTag, tables[tableTag].data);
|
|
|
}
|
|
|
+ return builder.toArray();
|
|
|
+ },
|
|
|
|
|
|
- function sanitizeHead(head, numGlyphs, locaLength) {
|
|
|
- var data = head.data;
|
|
|
-
|
|
|
- // Validate version:
|
|
|
- // Should always be 0x00010000
|
|
|
- var version = int32(data[0], data[1], data[2], data[3]);
|
|
|
- if (version >> 16 !== 1) {
|
|
|
- info('Attempting to fix invalid version in head table: ' + version);
|
|
|
- data[0] = 0;
|
|
|
- data[1] = 1;
|
|
|
- data[2] = 0;
|
|
|
- data[3] = 0;
|
|
|
- }
|
|
|
-
|
|
|
- var indexToLocFormat = int16(data[50], data[51]);
|
|
|
- if (indexToLocFormat < 0 || indexToLocFormat > 1) {
|
|
|
- info('Attempting to fix invalid indexToLocFormat in head table: ' +
|
|
|
- indexToLocFormat);
|
|
|
+ convert: function Font_convert(fontName, font, properties) {
|
|
|
+ // TODO: Check the charstring widths to determine this.
|
|
|
+ properties.fixedPitch = false;
|
|
|
|
|
|
- // The value of indexToLocFormat should be 0 if the loca table
|
|
|
- // consists of short offsets, and should be 1 if the loca table
|
|
|
- // consists of long offsets.
|
|
|
- //
|
|
|
- // The number of entries in the loca table should be numGlyphs + 1.
|
|
|
- //
|
|
|
- // Using this information, we can work backwards to deduce if the
|
|
|
- // size of each offset in the loca table, and thus figure out the
|
|
|
- // appropriate value for indexToLocFormat.
|
|
|
+ var mapping = font.getGlyphMapping(properties);
|
|
|
+ var newMapping = adjustMapping(mapping, properties);
|
|
|
+ this.toFontChar = newMapping.toFontChar;
|
|
|
+ var numGlyphs = font.numGlyphs;
|
|
|
|
|
|
- var numGlyphsPlusOne = numGlyphs + 1;
|
|
|
- if (locaLength === numGlyphsPlusOne << 1) {
|
|
|
- // 0x0000 indicates the loca table consists of short offsets
|
|
|
- data[50] = 0;
|
|
|
- data[51] = 0;
|
|
|
- } else if (locaLength === numGlyphsPlusOne << 2) {
|
|
|
- // 0x0001 indicates the loca table consists of long offsets
|
|
|
- data[50] = 0;
|
|
|
- data[51] = 1;
|
|
|
- } else {
|
|
|
- warn('Could not fix indexToLocFormat: ' + indexToLocFormat);
|
|
|
+ function getCharCodes(charCodeToGlyphId, glyphId) {
|
|
|
+ var charCodes = null;
|
|
|
+ for (var charCode in charCodeToGlyphId) {
|
|
|
+ if (glyphId === charCodeToGlyphId[charCode]) {
|
|
|
+ if (!charCodes) {
|
|
|
+ charCodes = [];
|
|
|
+ }
|
|
|
+ charCodes.push(charCode | 0);
|
|
|
}
|
|
|
}
|
|
|
+ return charCodes;
|
|
|
}
|
|
|
|
|
|
- function sanitizeGlyphLocations(loca, glyf, numGlyphs,
|
|
|
- isGlyphLocationsLong, hintsValid,
|
|
|
- dupFirstEntry) {
|
|
|
- var itemSize, itemDecode, itemEncode;
|
|
|
- if (isGlyphLocationsLong) {
|
|
|
- itemSize = 4;
|
|
|
- itemDecode = function fontItemDecodeLong(data, offset) {
|
|
|
- return (data[offset] << 24) | (data[offset + 1] << 16) |
|
|
|
- (data[offset + 2] << 8) | data[offset + 3];
|
|
|
- };
|
|
|
- itemEncode = function fontItemEncodeLong(data, offset, value) {
|
|
|
- data[offset] = (value >>> 24) & 0xFF;
|
|
|
- data[offset + 1] = (value >> 16) & 0xFF;
|
|
|
- data[offset + 2] = (value >> 8) & 0xFF;
|
|
|
- data[offset + 3] = value & 0xFF;
|
|
|
- };
|
|
|
- } else {
|
|
|
- itemSize = 2;
|
|
|
- itemDecode = function fontItemDecode(data, offset) {
|
|
|
- return (data[offset] << 9) | (data[offset + 1] << 1);
|
|
|
- };
|
|
|
- itemEncode = function fontItemEncode(data, offset, value) {
|
|
|
- data[offset] = (value >> 9) & 0xFF;
|
|
|
- data[offset + 1] = (value >> 1) & 0xFF;
|
|
|
- };
|
|
|
- }
|
|
|
- var locaData = loca.data;
|
|
|
- var locaDataSize = itemSize * (1 + numGlyphs);
|
|
|
- // is loca.data too short or long?
|
|
|
- if (locaData.length !== locaDataSize) {
|
|
|
- locaData = new Uint8Array(locaDataSize);
|
|
|
- locaData.set(loca.data.subarray(0, locaDataSize));
|
|
|
- loca.data = locaData;
|
|
|
- }
|
|
|
- // removing the invalid glyphs
|
|
|
- var oldGlyfData = glyf.data;
|
|
|
- var oldGlyfDataLength = oldGlyfData.length;
|
|
|
- var newGlyfData = new Uint8Array(oldGlyfDataLength);
|
|
|
- var startOffset = itemDecode(locaData, 0);
|
|
|
- var writeOffset = 0;
|
|
|
- var missingGlyphData = Object.create(null);
|
|
|
- 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 &&
|
|
|
- ((oldGlyfDataLength + 3) & ~3) === endOffset) {
|
|
|
- // Aspose breaks fonts by aligning the glyphs to the qword, but not
|
|
|
- // the glyf table size, which makes last glyph out of range.
|
|
|
- endOffset = oldGlyfDataLength;
|
|
|
- }
|
|
|
- if (endOffset > oldGlyfDataLength) {
|
|
|
- // glyph end offset points outside glyf data, rejecting the glyph
|
|
|
- itemEncode(locaData, j, writeOffset);
|
|
|
- startOffset = endOffset;
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- if (startOffset === endOffset) {
|
|
|
- missingGlyphData[i] = true;
|
|
|
+ function createCharCode(charCodeToGlyphId, glyphId) {
|
|
|
+ for (var charCode in charCodeToGlyphId) {
|
|
|
+ if (glyphId === charCodeToGlyphId[charCode]) {
|
|
|
+ return charCode | 0;
|
|
|
}
|
|
|
-
|
|
|
- var newLength = sanitizeGlyph(oldGlyfData, startOffset, endOffset,
|
|
|
- newGlyfData, writeOffset, hintsValid);
|
|
|
- writeOffset += newLength;
|
|
|
- itemEncode(locaData, j, writeOffset);
|
|
|
- startOffset = endOffset;
|
|
|
}
|
|
|
+ newMapping.charCodeToGlyphId[newMapping.nextAvailableFontCharCode] =
|
|
|
+ glyphId;
|
|
|
+ return newMapping.nextAvailableFontCharCode++;
|
|
|
+ }
|
|
|
|
|
|
- if (writeOffset === 0) {
|
|
|
- // glyf table cannot be empty -- redoing the glyf and loca tables
|
|
|
- // to have single glyph with one point
|
|
|
- var simpleGlyph = new Uint8Array(
|
|
|
- [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);
|
|
|
+ var seacs = font.seacs;
|
|
|
+ if (SEAC_ANALYSIS_ENABLED && seacs && seacs.length) {
|
|
|
+ var matrix = properties.fontMatrix || FONT_IDENTITY_MATRIX;
|
|
|
+ var charset = font.getCharset();
|
|
|
+ var seacMap = Object.create(null);
|
|
|
+ for (var glyphId in seacs) {
|
|
|
+ glyphId |= 0;
|
|
|
+ var seac = seacs[glyphId];
|
|
|
+ var baseGlyphName = StandardEncoding[seac[2]];
|
|
|
+ var accentGlyphName = StandardEncoding[seac[3]];
|
|
|
+ var baseGlyphId = charset.indexOf(baseGlyphName);
|
|
|
+ var accentGlyphId = charset.indexOf(accentGlyphName);
|
|
|
+ if (baseGlyphId < 0 || accentGlyphId < 0) {
|
|
|
+ continue;
|
|
|
}
|
|
|
- glyf.data = simpleGlyph;
|
|
|
- return missingGlyphData;
|
|
|
- }
|
|
|
+ var accentOffset = {
|
|
|
+ x: seac[0] * matrix[0] + seac[1] * matrix[2] + matrix[4],
|
|
|
+ y: seac[0] * matrix[1] + seac[1] * matrix[3] + matrix[5]
|
|
|
+ };
|
|
|
|
|
|
- if (dupFirstEntry) {
|
|
|
- var firstEntryLength = itemDecode(locaData, itemSize);
|
|
|
- if (newGlyfData.length > firstEntryLength + writeOffset) {
|
|
|
- glyf.data = newGlyfData.subarray(0, firstEntryLength + writeOffset);
|
|
|
- } else {
|
|
|
- glyf.data = new Uint8Array(firstEntryLength + writeOffset);
|
|
|
- glyf.data.set(newGlyfData.subarray(0, writeOffset));
|
|
|
+ var charCodes = getCharCodes(mapping, glyphId);
|
|
|
+ if (!charCodes) {
|
|
|
+ // There's no point in mapping it if the char code was never mapped
|
|
|
+ // to begin with.
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ for (var i = 0, ii = charCodes.length; i < ii; i++) {
|
|
|
+ var charCode = charCodes[i];
|
|
|
+ // Find a fontCharCode that maps to the base and accent glyphs.
|
|
|
+ // If one doesn't exists, create it.
|
|
|
+ var charCodeToGlyphId = newMapping.charCodeToGlyphId;
|
|
|
+ var baseFontCharCode = createCharCode(charCodeToGlyphId,
|
|
|
+ baseGlyphId);
|
|
|
+ var accentFontCharCode = createCharCode(charCodeToGlyphId,
|
|
|
+ accentGlyphId);
|
|
|
+ seacMap[charCode] = {
|
|
|
+ baseFontCharCode: baseFontCharCode,
|
|
|
+ accentFontCharCode: accentFontCharCode,
|
|
|
+ accentOffset: accentOffset
|
|
|
+ };
|
|
|
}
|
|
|
- glyf.data.set(newGlyfData.subarray(0, firstEntryLength), writeOffset);
|
|
|
- itemEncode(loca.data, locaData.length - itemSize,
|
|
|
- writeOffset + firstEntryLength);
|
|
|
- } else {
|
|
|
- glyf.data = newGlyfData.subarray(0, writeOffset);
|
|
|
}
|
|
|
- return missingGlyphData;
|
|
|
+ properties.seacMap = seacMap;
|
|
|
}
|
|
|
|
|
|
- function readPostScriptTable(post, properties, maxpNumGlyphs) {
|
|
|
- var start = (font.start ? font.start : 0) + post.offset;
|
|
|
- font.pos = start;
|
|
|
+ var unitsPerEm = 1 / (properties.fontMatrix || FONT_IDENTITY_MATRIX)[0];
|
|
|
|
|
|
- var length = post.length, end = start + length;
|
|
|
- var version = font.getInt32();
|
|
|
- // skip rest to the tables
|
|
|
- font.getBytes(28);
|
|
|
+ var builder = new OpenTypeFileBuilder('\x4F\x54\x54\x4F');
|
|
|
+ // PostScript Font Program
|
|
|
+ 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,
|
|
|
+ numGlyphs));
|
|
|
+ // Font header
|
|
|
+ builder.addTable('head',
|
|
|
+ '\x00\x01\x00\x00' + // Version number
|
|
|
+ '\x00\x00\x10\x00' + // fontRevision
|
|
|
+ '\x00\x00\x00\x00' + // checksumAdjustement
|
|
|
+ '\x5F\x0F\x3C\xF5' + // magicNumber
|
|
|
+ '\x00\x00' + // Flags
|
|
|
+ safeString16(unitsPerEm) + // unitsPerEM
|
|
|
+ '\x00\x00\x00\x00\x9e\x0b\x7e\x27' + // creation date
|
|
|
+ '\x00\x00\x00\x00\x9e\x0b\x7e\x27' + // modifification date
|
|
|
+ '\x00\x00' + // xMin
|
|
|
+ safeString16(properties.descent) + // yMin
|
|
|
+ '\x0F\xFF' + // xMax
|
|
|
+ safeString16(properties.ascent) + // yMax
|
|
|
+ string16(properties.italicAngle ? 2 : 0) + // macStyle
|
|
|
+ '\x00\x11' + // lowestRecPPEM
|
|
|
+ '\x00\x00' + // fontDirectionHint
|
|
|
+ '\x00\x00' + // indexToLocFormat
|
|
|
+ '\x00\x00'); // glyphDataFormat
|
|
|
|
|
|
- var glyphNames;
|
|
|
- var valid = true;
|
|
|
- var i;
|
|
|
+ // Horizontal header
|
|
|
+ builder.addTable('hhea',
|
|
|
+ '\x00\x01\x00\x00' + // Version number
|
|
|
+ safeString16(properties.ascent) + // Typographic Ascent
|
|
|
+ safeString16(properties.descent) + // Typographic Descent
|
|
|
+ '\x00\x00' + // Line Gap
|
|
|
+ '\xFF\xFF' + // advanceWidthMax
|
|
|
+ '\x00\x00' + // minLeftSidebearing
|
|
|
+ '\x00\x00' + // minRightSidebearing
|
|
|
+ '\x00\x00' + // xMaxExtent
|
|
|
+ safeString16(properties.capHeight) + // caretSlopeRise
|
|
|
+ safeString16(Math.tan(properties.italicAngle) *
|
|
|
+ properties.xHeight) + // caretSlopeRun
|
|
|
+ '\x00\x00' + // caretOffset
|
|
|
+ '\x00\x00' + // -reserved-
|
|
|
+ '\x00\x00' + // -reserved-
|
|
|
+ '\x00\x00' + // -reserved-
|
|
|
+ '\x00\x00' + // -reserved-
|
|
|
+ '\x00\x00' + // metricDataFormat
|
|
|
+ string16(numGlyphs)); // Number of HMetrics
|
|
|
|
|
|
- switch (version) {
|
|
|
- case 0x00010000:
|
|
|
- glyphNames = MacStandardGlyphOrdering;
|
|
|
- break;
|
|
|
- case 0x00020000:
|
|
|
- var numGlyphs = font.getUint16();
|
|
|
- if (numGlyphs !== maxpNumGlyphs) {
|
|
|
- valid = false;
|
|
|
- break;
|
|
|
- }
|
|
|
- var glyphNameIndexes = [];
|
|
|
- for (i = 0; i < numGlyphs; ++i) {
|
|
|
- var index = font.getUint16();
|
|
|
- if (index >= 32768) {
|
|
|
- valid = false;
|
|
|
- break;
|
|
|
- }
|
|
|
- glyphNameIndexes.push(index);
|
|
|
- }
|
|
|
- if (!valid) {
|
|
|
- break;
|
|
|
- }
|
|
|
- var customNames = [];
|
|
|
- var strBuf = [];
|
|
|
- while (font.pos < end) {
|
|
|
- var stringLength = font.getByte();
|
|
|
- strBuf.length = stringLength;
|
|
|
- for (i = 0; i < stringLength; ++i) {
|
|
|
- strBuf[i] = String.fromCharCode(font.getByte());
|
|
|
- }
|
|
|
- customNames.push(strBuf.join(''));
|
|
|
- }
|
|
|
- glyphNames = [];
|
|
|
- for (i = 0; i < numGlyphs; ++i) {
|
|
|
- var j = glyphNameIndexes[i];
|
|
|
- if (j < 258) {
|
|
|
- glyphNames.push(MacStandardGlyphOrdering[j]);
|
|
|
- continue;
|
|
|
- }
|
|
|
- glyphNames.push(customNames[j - 258]);
|
|
|
- }
|
|
|
- break;
|
|
|
- case 0x00030000:
|
|
|
- break;
|
|
|
- default:
|
|
|
- warn('Unknown/unsupported post table version ' + version);
|
|
|
- valid = false;
|
|
|
- if (properties.defaultEncoding) {
|
|
|
- glyphNames = properties.defaultEncoding;
|
|
|
+ // Horizontal metrics
|
|
|
+ builder.addTable('hmtx', (function fontFieldsHmtx() {
|
|
|
+ var charstrings = font.charstrings;
|
|
|
+ var cffWidths = font.cff ? font.cff.widths : null;
|
|
|
+ var hmtx = '\x00\x00\x00\x00'; // Fake .notdef
|
|
|
+ for (var i = 1, ii = numGlyphs; i < ii; i++) {
|
|
|
+ var width = 0;
|
|
|
+ if (charstrings) {
|
|
|
+ var charstring = charstrings[i - 1];
|
|
|
+ width = 'width' in charstring ? charstring.width : 0;
|
|
|
+ } else if (cffWidths) {
|
|
|
+ width = Math.ceil(cffWidths[i] || 0);
|
|
|
}
|
|
|
- break;
|
|
|
- }
|
|
|
- properties.glyphNames = glyphNames;
|
|
|
- return valid;
|
|
|
- }
|
|
|
+ hmtx += string16(width) + string16(0);
|
|
|
+ }
|
|
|
+ return hmtx;
|
|
|
+ })());
|
|
|
|
|
|
- function readNameTable(nameTable) {
|
|
|
- var start = (font.start ? font.start : 0) + nameTable.offset;
|
|
|
- font.pos = start;
|
|
|
+ // Maximum profile
|
|
|
+ builder.addTable('maxp',
|
|
|
+ '\x00\x00\x50\x00' + // Version number
|
|
|
+ string16(numGlyphs)); // Num of glyphs
|
|
|
|
|
|
- var names = [[], []];
|
|
|
- var length = nameTable.length, end = start + length;
|
|
|
- var format = font.getUint16();
|
|
|
- var FORMAT_0_HEADER_LENGTH = 6;
|
|
|
- if (format !== 0 || length < FORMAT_0_HEADER_LENGTH) {
|
|
|
- // unsupported name table format or table "too" small
|
|
|
- return names;
|
|
|
- }
|
|
|
- var numRecords = font.getUint16();
|
|
|
- var stringsStart = font.getUint16();
|
|
|
- var records = [];
|
|
|
- var NAME_RECORD_LENGTH = 12;
|
|
|
- var i, ii;
|
|
|
+ // Naming tables
|
|
|
+ builder.addTable('name', createNameTable(fontName));
|
|
|
|
|
|
- for (i = 0; i < numRecords &&
|
|
|
- font.pos + NAME_RECORD_LENGTH <= end; i++) {
|
|
|
- var r = {
|
|
|
- platform: font.getUint16(),
|
|
|
- encoding: font.getUint16(),
|
|
|
- language: font.getUint16(),
|
|
|
- name: font.getUint16(),
|
|
|
- length: font.getUint16(),
|
|
|
- offset: font.getUint16()
|
|
|
- };
|
|
|
- // using only Macintosh and Windows platform/encoding names
|
|
|
- if ((r.platform === 1 && r.encoding === 0 && r.language === 0) ||
|
|
|
- (r.platform === 3 && r.encoding === 1 && r.language === 0x409)) {
|
|
|
- records.push(r);
|
|
|
- }
|
|
|
+ // PostScript informations
|
|
|
+ builder.addTable('post', createPostTable(properties));
|
|
|
+
|
|
|
+ return builder.toArray();
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Builds a char code to unicode map based on section 9.10 of the spec.
|
|
|
+ * @param {Object} properties Font properties object.
|
|
|
+ * @return {Object} A ToUnicodeMap object.
|
|
|
+ */
|
|
|
+ buildToUnicode: function Font_buildToUnicode(properties) {
|
|
|
+ // Section 9.10.2 Mapping Character Codes to Unicode Values
|
|
|
+ if (properties.toUnicode && properties.toUnicode.length !== 0) {
|
|
|
+ return properties.toUnicode;
|
|
|
+ }
|
|
|
+ // According to the spec if the font is a simple font we should only map
|
|
|
+ // to unicode if the base encoding is MacRoman, MacExpert, or WinAnsi or
|
|
|
+ // the differences array only contains adobe standard or symbol set names,
|
|
|
+ // in pratice it seems better to always try to create a toUnicode
|
|
|
+ // map based of the default encoding.
|
|
|
+ var toUnicode, charcode;
|
|
|
+ if (!properties.composite /* is simple font */) {
|
|
|
+ toUnicode = [];
|
|
|
+ var encoding = properties.defaultEncoding.slice();
|
|
|
+ var baseEncodingName = properties.baseEncodingName;
|
|
|
+ // Merge in the differences array.
|
|
|
+ var differences = properties.differences;
|
|
|
+ for (charcode in differences) {
|
|
|
+ encoding[charcode] = differences[charcode];
|
|
|
}
|
|
|
- for (i = 0, ii = records.length; i < ii; i++) {
|
|
|
- var record = records[i];
|
|
|
- var pos = start + stringsStart + record.offset;
|
|
|
- if (pos + record.length > end) {
|
|
|
- continue; // outside of name table, ignoring
|
|
|
- }
|
|
|
- font.pos = pos;
|
|
|
- var nameIndex = record.name;
|
|
|
- if (record.encoding) {
|
|
|
- // unicode
|
|
|
- var str = '';
|
|
|
- for (var j = 0, jj = record.length; j < jj; j += 2) {
|
|
|
- str += String.fromCharCode(font.getUint16());
|
|
|
+ var glyphsUnicodeMap = getGlyphsUnicode();
|
|
|
+ for (charcode in encoding) {
|
|
|
+ // a) Map the character code to a character name.
|
|
|
+ var glyphName = encoding[charcode];
|
|
|
+ // b) Look up the character name in the Adobe Glyph List (see the
|
|
|
+ // Bibliography) to obtain the corresponding Unicode value.
|
|
|
+ if (glyphName === '') {
|
|
|
+ continue;
|
|
|
+ } else if (glyphsUnicodeMap[glyphName] === undefined) {
|
|
|
+ // (undocumented) c) Few heuristics to recognize unknown glyphs
|
|
|
+ // NOTE: Adobe Reader does not do this step, but OSX Preview does
|
|
|
+ var code = 0;
|
|
|
+ switch (glyphName[0]) {
|
|
|
+ case 'G': // Gxx glyph
|
|
|
+ if (glyphName.length === 3) {
|
|
|
+ code = parseInt(glyphName.substr(1), 16);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 'g': // g00xx glyph
|
|
|
+ if (glyphName.length === 5) {
|
|
|
+ code = parseInt(glyphName.substr(1), 16);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 'C': // Cddd glyph
|
|
|
+ case 'c': // cddd glyph
|
|
|
+ if (glyphName.length >= 3) {
|
|
|
+ code = +glyphName.substr(1);
|
|
|
+ }
|
|
|
+ break;
|
|
|
}
|
|
|
- names[1][nameIndex] = str;
|
|
|
- } else {
|
|
|
- names[0][nameIndex] = bytesToString(font.getBytes(record.length));
|
|
|
+ if (code) {
|
|
|
+ // If |baseEncodingName| is one the predefined encodings,
|
|
|
+ // and |code| equals |charcode|, using the glyph defined in the
|
|
|
+ // baseEncoding seems to yield a better |toUnicode| mapping
|
|
|
+ // (fixes issue 5070).
|
|
|
+ if (baseEncodingName && code === +charcode) {
|
|
|
+ var baseEncoding = getEncoding(baseEncodingName);
|
|
|
+ if (baseEncoding && (glyphName = baseEncoding[charcode])) {
|
|
|
+ toUnicode[charcode] =
|
|
|
+ String.fromCharCode(glyphsUnicodeMap[glyphName]);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ toUnicode[charcode] = String.fromCharCode(code);
|
|
|
+ }
|
|
|
+ continue;
|
|
|
}
|
|
|
+ toUnicode[charcode] =
|
|
|
+ String.fromCharCode(glyphsUnicodeMap[glyphName]);
|
|
|
}
|
|
|
- return names;
|
|
|
+ return new ToUnicodeMap(toUnicode);
|
|
|
}
|
|
|
-
|
|
|
- var TTOpsStackDeltas = [
|
|
|
- 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -2, -2, 0, 0, -2, -5,
|
|
|
- -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, -1, -1,
|
|
|
- 1, -1, -999, 0, 1, 0, -1, -2, 0, -1, -2, -1, -1, 0, -1, -1,
|
|
|
- 0, 0, -999, -999, -1, -1, -1, -1, -2, -999, -2, -2, -999, 0, -2, -2,
|
|
|
- 0, 0, -2, 0, -2, 0, 0, 0, -2, -1, -1, 1, 1, 0, 0, -1,
|
|
|
- -1, -1, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, 0, -999, -1, -1,
|
|
|
- -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
- -2, -999, -999, -999, -999, -999, -1, -1, -2, -2, 0, 0, 0, 0, -1, -1,
|
|
|
- -999, -2, -2, 0, 0, -1, -2, -2, 0, 0, 0, -1, -1, -1, -2];
|
|
|
- // 0xC0-DF == -1 and 0xE0-FF == -2
|
|
|
-
|
|
|
- function sanitizeTTProgram(table, ttContext) {
|
|
|
- var data = table.data;
|
|
|
- var i = 0, j, n, b, funcId, pc, lastEndf = 0, lastDeff = 0;
|
|
|
- var stack = [];
|
|
|
- var callstack = [];
|
|
|
- var functionsCalled = [];
|
|
|
- var tooComplexToFollowFunctions =
|
|
|
- ttContext.tooComplexToFollowFunctions;
|
|
|
- var inFDEF = false, ifLevel = 0, inELSE = 0;
|
|
|
- for (var ii = data.length; i < ii;) {
|
|
|
- var op = data[i++];
|
|
|
- // The TrueType instruction set docs can be found at
|
|
|
- // https://developer.apple.com/fonts/TTRefMan/RM05/Chap5.html
|
|
|
- if (op === 0x40) { // NPUSHB - pushes n bytes
|
|
|
- n = data[i++];
|
|
|
- if (inFDEF || inELSE) {
|
|
|
- i += n;
|
|
|
- } else {
|
|
|
- for (j = 0; j < n; j++) {
|
|
|
- stack.push(data[i++]);
|
|
|
- }
|
|
|
- }
|
|
|
- } else if (op === 0x41) { // NPUSHW - pushes n words
|
|
|
- n = data[i++];
|
|
|
- if (inFDEF || inELSE) {
|
|
|
- i += n * 2;
|
|
|
- } else {
|
|
|
- for (j = 0; j < n; j++) {
|
|
|
- b = data[i++];
|
|
|
- stack.push((b << 8) | data[i++]);
|
|
|
- }
|
|
|
- }
|
|
|
- } else if ((op & 0xF8) === 0xB0) { // PUSHB - pushes bytes
|
|
|
- n = op - 0xB0 + 1;
|
|
|
- if (inFDEF || inELSE) {
|
|
|
- i += n;
|
|
|
- } else {
|
|
|
- for (j = 0; j < n; j++) {
|
|
|
- stack.push(data[i++]);
|
|
|
- }
|
|
|
- }
|
|
|
- } else if ((op & 0xF8) === 0xB8) { // PUSHW - pushes words
|
|
|
- n = op - 0xB8 + 1;
|
|
|
- if (inFDEF || inELSE) {
|
|
|
- i += n * 2;
|
|
|
- } else {
|
|
|
- for (j = 0; j < n; j++) {
|
|
|
- b = data[i++];
|
|
|
- stack.push((b << 8) | data[i++]);
|
|
|
- }
|
|
|
- }
|
|
|
- } else if (op === 0x2B && !tooComplexToFollowFunctions) { // CALL
|
|
|
- if (!inFDEF && !inELSE) {
|
|
|
- // collecting inforamtion about which functions are used
|
|
|
- funcId = stack[stack.length - 1];
|
|
|
- ttContext.functionsUsed[funcId] = true;
|
|
|
- if (funcId in ttContext.functionsStackDeltas) {
|
|
|
- stack.length += ttContext.functionsStackDeltas[funcId];
|
|
|
- } else if (funcId in ttContext.functionsDefined &&
|
|
|
- functionsCalled.indexOf(funcId) < 0) {
|
|
|
- callstack.push({data: data, i: i, stackTop: stack.length - 1});
|
|
|
- functionsCalled.push(funcId);
|
|
|
- pc = ttContext.functionsDefined[funcId];
|
|
|
- if (!pc) {
|
|
|
- warn('TT: CALL non-existent function');
|
|
|
- ttContext.hintsValid = false;
|
|
|
- return;
|
|
|
- }
|
|
|
- data = pc.data;
|
|
|
- i = pc.i;
|
|
|
- }
|
|
|
- }
|
|
|
- } else if (op === 0x2C && !tooComplexToFollowFunctions) { // FDEF
|
|
|
- if (inFDEF || inELSE) {
|
|
|
- warn('TT: nested FDEFs not allowed');
|
|
|
- tooComplexToFollowFunctions = true;
|
|
|
- }
|
|
|
- inFDEF = true;
|
|
|
- // collecting inforamtion about which functions are defined
|
|
|
- lastDeff = i;
|
|
|
- funcId = stack.pop();
|
|
|
- ttContext.functionsDefined[funcId] = {data: data, i: i};
|
|
|
- } else if (op === 0x2D) { // ENDF - end of function
|
|
|
- if (inFDEF) {
|
|
|
- inFDEF = false;
|
|
|
- lastEndf = i;
|
|
|
- } else {
|
|
|
- pc = callstack.pop();
|
|
|
- if (!pc) {
|
|
|
- warn('TT: ENDF bad stack');
|
|
|
- ttContext.hintsValid = false;
|
|
|
- return;
|
|
|
- }
|
|
|
- funcId = functionsCalled.pop();
|
|
|
- data = pc.data;
|
|
|
- i = pc.i;
|
|
|
- ttContext.functionsStackDeltas[funcId] =
|
|
|
- stack.length - pc.stackTop;
|
|
|
- }
|
|
|
- } else if (op === 0x89) { // IDEF - instruction definition
|
|
|
- if (inFDEF || inELSE) {
|
|
|
- warn('TT: nested IDEFs not allowed');
|
|
|
- tooComplexToFollowFunctions = true;
|
|
|
- }
|
|
|
- inFDEF = true;
|
|
|
- // recording it as a function to track ENDF
|
|
|
- lastDeff = i;
|
|
|
- } else if (op === 0x58) { // IF
|
|
|
- ++ifLevel;
|
|
|
- } else if (op === 0x1B) { // ELSE
|
|
|
- inELSE = ifLevel;
|
|
|
- } else if (op === 0x59) { // EIF
|
|
|
- if (inELSE === ifLevel) {
|
|
|
- inELSE = 0;
|
|
|
- }
|
|
|
- --ifLevel;
|
|
|
- } else if (op === 0x1C) { // JMPR
|
|
|
- if (!inFDEF && !inELSE) {
|
|
|
- var offset = stack[stack.length - 1];
|
|
|
- // only jumping forward to prevent infinite loop
|
|
|
- if (offset > 0) {
|
|
|
- i += offset - 1;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- // Adjusting stack not extactly, but just enough to get function id
|
|
|
- if (!inFDEF && !inELSE) {
|
|
|
- var stackDelta = op <= 0x8E ? TTOpsStackDeltas[op] :
|
|
|
- op >= 0xC0 && op <= 0xDF ? -1 : op >= 0xE0 ? -2 : 0;
|
|
|
- if (op >= 0x71 && op <= 0x75) {
|
|
|
- n = stack.pop();
|
|
|
- if (n === n) {
|
|
|
- stackDelta = -n * 2;
|
|
|
- }
|
|
|
- }
|
|
|
- while (stackDelta < 0 && stack.length > 0) {
|
|
|
- stack.pop();
|
|
|
- stackDelta++;
|
|
|
- }
|
|
|
- while (stackDelta > 0) {
|
|
|
- stack.push(NaN); // pushing any number into stack
|
|
|
- stackDelta--;
|
|
|
- }
|
|
|
+ // If the font is a composite font that uses one of the predefined CMaps
|
|
|
+ // listed in Table 118 (except Identity–H and Identity–V) or whose
|
|
|
+ // descendant CIDFont uses the Adobe-GB1, Adobe-CNS1, Adobe-Japan1, or
|
|
|
+ // Adobe-Korea1 character collection:
|
|
|
+ if (properties.composite && (
|
|
|
+ (properties.cMap.builtInCMap &&
|
|
|
+ !(properties.cMap instanceof IdentityCMap)) ||
|
|
|
+ (properties.cidSystemInfo.registry === 'Adobe' &&
|
|
|
+ (properties.cidSystemInfo.ordering === 'GB1' ||
|
|
|
+ properties.cidSystemInfo.ordering === 'CNS1' ||
|
|
|
+ properties.cidSystemInfo.ordering === 'Japan1' ||
|
|
|
+ properties.cidSystemInfo.ordering === 'Korea1')))) {
|
|
|
+ // Then:
|
|
|
+ // a) Map the character code to a character identifier (CID) according
|
|
|
+ // to the font’s CMap.
|
|
|
+ // b) Obtain the registry and ordering of the character collection used
|
|
|
+ // by the font’s CMap (for example, Adobe and Japan1) from its
|
|
|
+ // CIDSystemInfo dictionary.
|
|
|
+ var registry = properties.cidSystemInfo.registry;
|
|
|
+ var ordering = properties.cidSystemInfo.ordering;
|
|
|
+ // c) Construct a second CMap name by concatenating the registry and
|
|
|
+ // ordering obtained in step (b) in the format registry–ordering–UCS2
|
|
|
+ // (for example, Adobe–Japan1–UCS2).
|
|
|
+ var ucs2CMapName = new Name(registry + '-' + ordering + '-UCS2');
|
|
|
+ // d) Obtain the CMap with the name constructed in step (c) (available
|
|
|
+ // from the ASN Web site; see the Bibliography).
|
|
|
+ var ucs2CMap = CMapFactory.create(ucs2CMapName,
|
|
|
+ { url: PDFJS.cMapUrl, packed: PDFJS.cMapPacked }, null);
|
|
|
+ var cMap = properties.cMap;
|
|
|
+ toUnicode = [];
|
|
|
+ cMap.forEach(function(charcode, cid) {
|
|
|
+ assert(cid <= 0xffff, 'Max size of CID is 65,535');
|
|
|
+ // e) Map the CID obtained in step (a) according to the CMap obtained
|
|
|
+ // in step (d), producing a Unicode value.
|
|
|
+ var ucs2 = ucs2CMap.lookup(cid);
|
|
|
+ if (ucs2) {
|
|
|
+ toUnicode[charcode] =
|
|
|
+ String.fromCharCode((ucs2.charCodeAt(0) << 8) +
|
|
|
+ ucs2.charCodeAt(1));
|
|
|
}
|
|
|
- }
|
|
|
- ttContext.tooComplexToFollowFunctions = tooComplexToFollowFunctions;
|
|
|
- var content = [data];
|
|
|
- if (i > data.length) {
|
|
|
- content.push(new Uint8Array(i - data.length));
|
|
|
- }
|
|
|
- if (lastDeff > lastEndf) {
|
|
|
- warn('TT: complementing a missing function tail');
|
|
|
- // new function definition started, but not finished
|
|
|
- // complete function by [CLEAR, ENDF]
|
|
|
- content.push(new Uint8Array([0x22, 0x2D]));
|
|
|
- }
|
|
|
- foldTTTable(table, content);
|
|
|
+ });
|
|
|
+ return new ToUnicodeMap(toUnicode);
|
|
|
}
|
|
|
|
|
|
- function checkInvalidFunctions(ttContext, maxFunctionDefs) {
|
|
|
- if (ttContext.tooComplexToFollowFunctions) {
|
|
|
- return;
|
|
|
- }
|
|
|
- if (ttContext.functionsDefined.length > maxFunctionDefs) {
|
|
|
- warn('TT: more functions defined than expected');
|
|
|
- ttContext.hintsValid = false;
|
|
|
- return;
|
|
|
- }
|
|
|
- for (var j = 0, jj = ttContext.functionsUsed.length; j < jj; j++) {
|
|
|
- if (j > maxFunctionDefs) {
|
|
|
- warn('TT: invalid function id: ' + j);
|
|
|
- ttContext.hintsValid = false;
|
|
|
- return;
|
|
|
- }
|
|
|
- if (ttContext.functionsUsed[j] && !ttContext.functionsDefined[j]) {
|
|
|
- warn('TT: undefined function: ' + j);
|
|
|
- ttContext.hintsValid = false;
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ // The viewer's choice, just use an identity map.
|
|
|
+ return new IdentityToUnicodeMap(properties.firstChar,
|
|
|
+ properties.lastChar);
|
|
|
+ },
|
|
|
|
|
|
- function foldTTTable(table, content) {
|
|
|
- if (content.length > 1) {
|
|
|
- // concatenating the content items
|
|
|
- var newLength = 0;
|
|
|
- var j, jj;
|
|
|
- for (j = 0, jj = content.length; j < jj; j++) {
|
|
|
- newLength += content[j].length;
|
|
|
- }
|
|
|
- newLength = (newLength + 3) & ~3;
|
|
|
- var result = new Uint8Array(newLength);
|
|
|
- var pos = 0;
|
|
|
- for (j = 0, jj = content.length; j < jj; j++) {
|
|
|
- result.set(content[j], pos);
|
|
|
- pos += content[j].length;
|
|
|
- }
|
|
|
- table.data = result;
|
|
|
- table.length = newLength;
|
|
|
- }
|
|
|
+ get spaceWidth() {
|
|
|
+ if ('_shadowWidth' in this) {
|
|
|
+ return this._shadowWidth;
|
|
|
}
|
|
|
|
|
|
- function sanitizeTTPrograms(fpgm, prep, cvt) {
|
|
|
- var ttContext = {
|
|
|
- functionsDefined: [],
|
|
|
- functionsUsed: [],
|
|
|
- functionsStackDeltas: [],
|
|
|
- tooComplexToFollowFunctions: false,
|
|
|
- hintsValid: true
|
|
|
- };
|
|
|
- if (fpgm) {
|
|
|
- sanitizeTTProgram(fpgm, ttContext);
|
|
|
- }
|
|
|
- if (prep) {
|
|
|
- sanitizeTTProgram(prep, ttContext);
|
|
|
+ // trying to estimate space character width
|
|
|
+ var possibleSpaceReplacements = ['space', 'minus', 'one', 'i'];
|
|
|
+ var width;
|
|
|
+ for (var i = 0, ii = possibleSpaceReplacements.length; i < ii; i++) {
|
|
|
+ var glyphName = possibleSpaceReplacements[i];
|
|
|
+ // if possible, getting width by glyph name
|
|
|
+ if (glyphName in this.widths) {
|
|
|
+ width = this.widths[glyphName];
|
|
|
+ break;
|
|
|
}
|
|
|
- if (fpgm) {
|
|
|
- checkInvalidFunctions(ttContext, maxFunctionDefs);
|
|
|
+ var glyphsUnicodeMap = getGlyphsUnicode();
|
|
|
+ var glyphUnicode = glyphsUnicodeMap[glyphName];
|
|
|
+ // finding the charcode via unicodeToCID map
|
|
|
+ var charcode = 0;
|
|
|
+ if (this.composite) {
|
|
|
+ if (this.cMap.contains(glyphUnicode)) {
|
|
|
+ charcode = this.cMap.lookup(glyphUnicode);
|
|
|
+ }
|
|
|
}
|
|
|
- if (cvt && (cvt.length & 1)) {
|
|
|
- var cvtData = new Uint8Array(cvt.length + 1);
|
|
|
- cvtData.set(cvt.data);
|
|
|
- cvt.data = cvtData;
|
|
|
+ // ... via toUnicode map
|
|
|
+ if (!charcode && this.toUnicode) {
|
|
|
+ charcode = this.toUnicode.charCodeOf(glyphUnicode);
|
|
|
}
|
|
|
- return ttContext.hintsValid;
|
|
|
- }
|
|
|
-
|
|
|
- // The following steps modify the original font data, making copy
|
|
|
- font = new Stream(new Uint8Array(font.getBytes()));
|
|
|
-
|
|
|
- var VALID_TABLES = ['OS/2', 'cmap', 'head', 'hhea', 'hmtx', 'maxp',
|
|
|
- 'name', 'post', 'loca', 'glyf', 'fpgm', 'prep', 'cvt ', 'CFF '];
|
|
|
-
|
|
|
- var header = readOpenTypeHeader(font);
|
|
|
- var numTables = header.numTables;
|
|
|
- var cff, cffFile;
|
|
|
-
|
|
|
- var tables = Object.create(null);
|
|
|
- tables['OS/2'] = null;
|
|
|
- tables['cmap'] = null;
|
|
|
- tables['head'] = null;
|
|
|
- tables['hhea'] = null;
|
|
|
- tables['hmtx'] = null;
|
|
|
- tables['maxp'] = null;
|
|
|
- tables['name'] = null;
|
|
|
- tables['post'] = null;
|
|
|
-
|
|
|
- var table;
|
|
|
- for (var i = 0; i < numTables; i++) {
|
|
|
- table = readTableEntry(font);
|
|
|
- if (VALID_TABLES.indexOf(table.tag) < 0) {
|
|
|
- continue; // skipping table if it's not a required or optional table
|
|
|
+ // setting it to unicode if negative or undefined
|
|
|
+ if (charcode <= 0) {
|
|
|
+ charcode = glyphUnicode;
|
|
|
}
|
|
|
- if (table.length === 0) {
|
|
|
- continue; // skipping empty tables
|
|
|
+ // trying to get width via charcode
|
|
|
+ width = this.widths[charcode];
|
|
|
+ if (width) {
|
|
|
+ break; // the non-zero width found
|
|
|
}
|
|
|
- tables[table.tag] = table;
|
|
|
}
|
|
|
+ width = width || this.defaultWidth;
|
|
|
+ // Do not shadow the property here. See discussion:
|
|
|
+ // https://github.com/mozilla/pdf.js/pull/2127#discussion_r1662280
|
|
|
+ this._shadowWidth = width;
|
|
|
+ return width;
|
|
|
+ },
|
|
|
|
|
|
- var isTrueType = !tables['CFF '];
|
|
|
- if (!isTrueType) {
|
|
|
- // OpenType font
|
|
|
- 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);
|
|
|
- }
|
|
|
+ charToGlyph: function Font_charToGlyph(charcode, isSpace) {
|
|
|
+ var fontCharCode, width, operatorListId;
|
|
|
|
|
|
- delete tables['glyf'];
|
|
|
- delete tables['loca'];
|
|
|
- delete tables['fpgm'];
|
|
|
- delete tables['prep'];
|
|
|
- delete tables['cvt '];
|
|
|
- this.isOpenType = true;
|
|
|
- } else {
|
|
|
- if (!tables['loca']) {
|
|
|
- error('Required "loca" table is not found');
|
|
|
- }
|
|
|
- if (!tables['glyf']) {
|
|
|
- warn('Required "glyf" table is not found -- trying to recover.');
|
|
|
- // Note: We use `sanitizeGlyphLocations` to add dummy glyf data below.
|
|
|
- tables['glyf'] = {
|
|
|
- tag: 'glyf',
|
|
|
- data: new Uint8Array(0),
|
|
|
- };
|
|
|
- }
|
|
|
- this.isOpenType = false;
|
|
|
+ var widthCode = charcode;
|
|
|
+ if (this.cMap && this.cMap.contains(charcode)) {
|
|
|
+ widthCode = this.cMap.lookup(charcode);
|
|
|
}
|
|
|
+ width = this.widths[widthCode];
|
|
|
+ width = isNum(width) ? width : this.defaultWidth;
|
|
|
+ var vmetric = this.vmetrics && this.vmetrics[widthCode];
|
|
|
|
|
|
- if (!tables['maxp']) {
|
|
|
- error('Required "maxp" table is not found');
|
|
|
+ var unicode = this.toUnicode.get(charcode) || charcode;
|
|
|
+ if (typeof unicode === 'number') {
|
|
|
+ unicode = String.fromCharCode(unicode);
|
|
|
}
|
|
|
|
|
|
- font.pos = (font.start || 0) + tables['maxp'].offset;
|
|
|
- var version = font.getInt32();
|
|
|
- var numGlyphs = font.getUint16();
|
|
|
- var maxFunctionDefs = 0;
|
|
|
- if (version >= 0x00010000 && tables['maxp'].length >= 22) {
|
|
|
- // maxZones can be invalid
|
|
|
- font.pos += 8;
|
|
|
- var maxZones = font.getUint16();
|
|
|
- if (maxZones > 2) { // reset to 2 if font has invalid maxZones
|
|
|
- tables['maxp'].data[14] = 0;
|
|
|
- tables['maxp'].data[15] = 2;
|
|
|
- }
|
|
|
- font.pos += 4;
|
|
|
- maxFunctionDefs = font.getUint16();
|
|
|
+ // First try the toFontChar map, if it's not there then try falling
|
|
|
+ // back to the char code.
|
|
|
+ fontCharCode = this.toFontChar[charcode] || charcode;
|
|
|
+ if (this.missingFile) {
|
|
|
+ fontCharCode = mapSpecialUnicodeValues(fontCharCode);
|
|
|
}
|
|
|
|
|
|
- var dupFirstEntry = false;
|
|
|
- if (properties.type === 'CIDFontType2' && properties.toUnicode &&
|
|
|
- properties.toUnicode.get(0) > '\u0000') {
|
|
|
- // oracle's defect (see 3427), duplicating first entry
|
|
|
- dupFirstEntry = true;
|
|
|
- numGlyphs++;
|
|
|
- tables['maxp'].data[4] = numGlyphs >> 8;
|
|
|
- tables['maxp'].data[5] = numGlyphs & 255;
|
|
|
+ if (this.isType3Font) {
|
|
|
+ // Font char code in this case is actually a glyph name.
|
|
|
+ operatorListId = fontCharCode;
|
|
|
}
|
|
|
|
|
|
- var hintsValid = sanitizeTTPrograms(tables['fpgm'], tables['prep'],
|
|
|
- tables['cvt '], maxFunctionDefs);
|
|
|
- if (!hintsValid) {
|
|
|
- delete tables['fpgm'];
|
|
|
- delete tables['prep'];
|
|
|
- delete tables['cvt '];
|
|
|
+ var accent = null;
|
|
|
+ if (this.seacMap && this.seacMap[charcode]) {
|
|
|
+ var seac = this.seacMap[charcode];
|
|
|
+ fontCharCode = seac.baseFontCharCode;
|
|
|
+ accent = {
|
|
|
+ fontChar: String.fromCharCode(seac.accentFontCharCode),
|
|
|
+ offset: seac.accentOffset
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
- // Ensure the hmtx table contains the advance width and
|
|
|
- // sidebearings information for numGlyphs in the maxp table
|
|
|
- sanitizeMetrics(font, tables['hhea'], tables['hmtx'], numGlyphs);
|
|
|
+ var fontChar = String.fromCharCode(fontCharCode);
|
|
|
|
|
|
- if (!tables['head']) {
|
|
|
- error('Required "head" table is not found');
|
|
|
+ var glyph = this.glyphCache[charcode];
|
|
|
+ if (!glyph ||
|
|
|
+ !glyph.matchesForCache(fontChar, unicode, accent, width, vmetric,
|
|
|
+ operatorListId, isSpace)) {
|
|
|
+ glyph = new Glyph(fontChar, unicode, accent, width, vmetric,
|
|
|
+ operatorListId, isSpace);
|
|
|
+ this.glyphCache[charcode] = glyph;
|
|
|
}
|
|
|
+ return glyph;
|
|
|
+ },
|
|
|
|
|
|
- sanitizeHead(tables['head'], numGlyphs,
|
|
|
- isTrueType ? tables['loca'].length : 0);
|
|
|
-
|
|
|
- var missingGlyphs = Object.create(null);
|
|
|
- if (isTrueType) {
|
|
|
- var isGlyphLocationsLong = int16(tables['head'].data[50],
|
|
|
- tables['head'].data[51]);
|
|
|
- missingGlyphs = sanitizeGlyphLocations(tables['loca'], tables['glyf'],
|
|
|
- numGlyphs, isGlyphLocationsLong,
|
|
|
- hintsValid, dupFirstEntry);
|
|
|
- }
|
|
|
+ charsToGlyphs: function Font_charsToGlyphs(chars) {
|
|
|
+ var charsCache = this.charsCache;
|
|
|
+ var glyphs, glyph, charcode;
|
|
|
|
|
|
- if (!tables['hhea']) {
|
|
|
- error('Required "hhea" table is not found');
|
|
|
+ // if we translated this string before, just grab it from the cache
|
|
|
+ if (charsCache) {
|
|
|
+ glyphs = charsCache[chars];
|
|
|
+ if (glyphs) {
|
|
|
+ return glyphs;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- // Sanitizer reduces the glyph advanceWidth to the maxAdvanceWidth
|
|
|
- // Sometimes it's 0. That needs to be fixed
|
|
|
- if (tables['hhea'].data[10] === 0 && tables['hhea'].data[11] === 0) {
|
|
|
- tables['hhea'].data[10] = 0xFF;
|
|
|
- tables['hhea'].data[11] = 0xFF;
|
|
|
+ // lazily create the translation cache
|
|
|
+ if (!charsCache) {
|
|
|
+ charsCache = this.charsCache = Object.create(null);
|
|
|
}
|
|
|
|
|
|
- // 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;
|
|
|
+ glyphs = [];
|
|
|
+ var charsCacheKey = chars;
|
|
|
+ var i = 0, ii;
|
|
|
|
|
|
- // The 'post' table has glyphs names.
|
|
|
- if (tables['post']) {
|
|
|
- var valid = readPostScriptTable(tables['post'], properties, numGlyphs);
|
|
|
- if (!valid) {
|
|
|
- tables['post'] = null;
|
|
|
+ if (this.cMap) {
|
|
|
+ // composite fonts have multi-byte strings convert the string from
|
|
|
+ // single-byte to multi-byte
|
|
|
+ var c = Object.create(null);
|
|
|
+ while (i < chars.length) {
|
|
|
+ this.cMap.readCharCode(chars, i, c);
|
|
|
+ charcode = c.charcode;
|
|
|
+ var length = c.length;
|
|
|
+ i += length;
|
|
|
+ // 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);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ for (i = 0, ii = chars.length; i < ii; ++i) {
|
|
|
+ charcode = chars.charCodeAt(i);
|
|
|
+ glyph = this.charToGlyph(charcode, charcode === 0x20);
|
|
|
+ glyphs.push(glyph);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- var charCodeToGlyphId = [], charCode;
|
|
|
- var toUnicode = properties.toUnicode, widths = properties.widths;
|
|
|
- var skipToUnicode = (toUnicode instanceof IdentityToUnicodeMap ||
|
|
|
- toUnicode.length === 0x10000);
|
|
|
+ // Enter the translated string into the cache
|
|
|
+ return (charsCache[charsCacheKey] = glyphs);
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- // 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;
|
|
|
- }
|
|
|
+ return Font;
|
|
|
+})();
|
|
|
|
|
|
- if (properties.type === 'CIDFontType2') {
|
|
|
- var cidToGidMap = properties.cidToGidMap || [];
|
|
|
- var isCidToGidMapEmpty = cidToGidMap.length === 0;
|
|
|
+var ErrorFont = (function ErrorFontClosure() {
|
|
|
+ function ErrorFont(error) {
|
|
|
+ this.error = error;
|
|
|
+ this.loadedName = 'g_font_error';
|
|
|
+ this.loading = false;
|
|
|
+ }
|
|
|
|
|
|
- properties.cMap.forEach(function(charCode, cid) {
|
|
|
- assert(cid <= 0xffff, 'Max size of CID is 65,535');
|
|
|
- var glyphId = -1;
|
|
|
- if (isCidToGidMapEmpty) {
|
|
|
- glyphId = charCode;
|
|
|
- } else if (cidToGidMap[cid] !== undefined) {
|
|
|
- glyphId = cidToGidMap[cid];
|
|
|
- }
|
|
|
+ ErrorFont.prototype = {
|
|
|
+ charsToGlyphs: function ErrorFont_charsToGlyphs() {
|
|
|
+ return [];
|
|
|
+ },
|
|
|
+ exportData: function ErrorFont_exportData() {
|
|
|
+ return {error: this.error};
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- if (glyphId >= 0 && glyphId < numGlyphs &&
|
|
|
- hasGlyph(glyphId, charCode, cid)) {
|
|
|
- charCodeToGlyphId[charCode] = glyphId;
|
|
|
- }
|
|
|
- });
|
|
|
- if (dupFirstEntry) {
|
|
|
- charCodeToGlyphId[0] = numGlyphs - 1;
|
|
|
- }
|
|
|
+ return ErrorFont;
|
|
|
+})();
|
|
|
+
|
|
|
+/**
|
|
|
+ * Shared logic for building a char code to glyph id mapping for Type1 and
|
|
|
+ * simple CFF fonts. See section 9.6.6.2 of the spec.
|
|
|
+ * @param {Object} properties Font properties object.
|
|
|
+ * @param {Object} builtInEncoding The encoding contained within the actual font
|
|
|
+ * data.
|
|
|
+ * @param {Array} Array of glyph names where the index is the glyph ID.
|
|
|
+ * @returns {Object} A char code to glyph ID map.
|
|
|
+ */
|
|
|
+function type1FontGlyphMapping(properties, builtInEncoding, glyphNames) {
|
|
|
+ var charCodeToGlyphId = Object.create(null);
|
|
|
+ var glyphId, charCode, baseEncoding;
|
|
|
+
|
|
|
+ if (properties.baseEncodingName) {
|
|
|
+ // If a valid base encoding name was used, the mapping is initialized with
|
|
|
+ // that.
|
|
|
+ baseEncoding = getEncoding(properties.baseEncodingName);
|
|
|
+ for (charCode = 0; charCode < baseEncoding.length; charCode++) {
|
|
|
+ glyphId = glyphNames.indexOf(baseEncoding[charCode]);
|
|
|
+ if (glyphId >= 0) {
|
|
|
+ charCodeToGlyphId[charCode] = glyphId;
|
|
|
} else {
|
|
|
- // Most of the following logic in this code branch is based on the
|
|
|
- // 9.6.6.4 of the PDF spec.
|
|
|
- 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;
|
|
|
+ charCodeToGlyphId[charCode] = 0; // notdef
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (!!(properties.flags & FontFlags.Symbolic)) {
|
|
|
+ // For a symbolic font the encoding should be the fonts built-in
|
|
|
+ // encoding.
|
|
|
+ for (charCode in builtInEncoding) {
|
|
|
+ charCodeToGlyphId[charCode] = builtInEncoding[charCode];
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // For non-symbolic fonts that don't have a base encoding the standard
|
|
|
+ // encoding should be used.
|
|
|
+ baseEncoding = StandardEncoding;
|
|
|
+ for (charCode = 0; charCode < baseEncoding.length; charCode++) {
|
|
|
+ glyphId = glyphNames.indexOf(baseEncoding[charCode]);
|
|
|
+ if (glyphId >= 0) {
|
|
|
+ charCodeToGlyphId[charCode] = glyphId;
|
|
|
+ } else {
|
|
|
+ charCodeToGlyphId[charCode] = 0; // notdef
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 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 &&
|
|
|
- (cmapPlatformId === 3 && cmapEncodingId === 1 ||
|
|
|
- cmapPlatformId === 1 && cmapEncodingId === 0) ||
|
|
|
- (cmapPlatformId === -1 && cmapEncodingId === -1 && // Temporary hack
|
|
|
- !!getEncoding(properties.baseEncodingName))) { // Temporary hack
|
|
|
- // When no preferred cmap table was found and |baseEncodingName| is
|
|
|
- // one of the predefined encodings, we seem to obtain a better
|
|
|
- // |charCodeToGlyphId| map from the code below (fixes bug 1057544).
|
|
|
- // TODO: Note that this is a hack which should be removed as soon as
|
|
|
- // we have proper support for more exotic cmap tables.
|
|
|
+ // Lastly, merge in the differences.
|
|
|
+ var differences = properties.differences;
|
|
|
+ if (differences) {
|
|
|
+ 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;
|
|
|
+}
|
|
|
|
|
|
- var baseEncoding = [];
|
|
|
- if (properties.baseEncodingName === 'MacRomanEncoding' ||
|
|
|
- properties.baseEncodingName === 'WinAnsiEncoding') {
|
|
|
- baseEncoding = getEncoding(properties.baseEncodingName);
|
|
|
- }
|
|
|
- var glyphsUnicodeMap = getGlyphsUnicode();
|
|
|
- for (charCode = 0; charCode < 256; charCode++) {
|
|
|
- var glyphName;
|
|
|
- if (this.differences && charCode in this.differences) {
|
|
|
- glyphName = this.differences[charCode];
|
|
|
- } else if (charCode in baseEncoding &&
|
|
|
- baseEncoding[charCode] !== '') {
|
|
|
- glyphName = baseEncoding[charCode];
|
|
|
- } else {
|
|
|
- glyphName = StandardEncoding[charCode];
|
|
|
- }
|
|
|
- if (!glyphName) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- var unicodeOrCharCode, isUnicode = false;
|
|
|
- if (cmapPlatformId === 3 && cmapEncodingId === 1) {
|
|
|
- unicodeOrCharCode = glyphsUnicodeMap[glyphName];
|
|
|
- isUnicode = true;
|
|
|
- } else if (cmapPlatformId === 1 && cmapEncodingId === 0) {
|
|
|
- // TODO: the encoding needs to be updated with mac os table.
|
|
|
- unicodeOrCharCode = MacRomanEncoding.indexOf(glyphName);
|
|
|
- }
|
|
|
+/*
|
|
|
+ * CharStrings are encoded following the the CharString Encoding sequence
|
|
|
+ * describe in Chapter 6 of the "Adobe Type1 Font Format" specification.
|
|
|
+ * The value in a byte indicates a command, a number, or subsequent bytes
|
|
|
+ * that are to be interpreted in a special way.
|
|
|
+ *
|
|
|
+ * CharString Number Encoding:
|
|
|
+ * A CharString byte containing the values from 32 through 255 inclusive
|
|
|
+ * indicate an integer. These values are decoded in four ranges.
|
|
|
+ *
|
|
|
+ * 1. A CharString byte containing a value, v, between 32 and 246 inclusive,
|
|
|
+ * indicate the integer v - 139. Thus, the integer values from -107 through
|
|
|
+ * 107 inclusive may be encoded in single byte.
|
|
|
+ *
|
|
|
+ * 2. A CharString byte containing a value, v, between 247 and 250 inclusive,
|
|
|
+ * indicates an integer involving the next byte, w, according to the formula:
|
|
|
+ * [(v - 247) x 256] + w + 108
|
|
|
+ *
|
|
|
+ * 3. A CharString byte containing a value, v, between 251 and 254 inclusive,
|
|
|
+ * indicates an integer involving the next byte, w, according to the formula:
|
|
|
+ * -[(v - 251) * 256] - w - 108
|
|
|
+ *
|
|
|
+ * 4. A CharString containing the value 255 indicates that the next 4 bytes
|
|
|
+ * are a two complement signed integer. The first of these bytes contains the
|
|
|
+ * highest order bits, the second byte contains the next higher order bits
|
|
|
+ * and the fourth byte contain the lowest order bits.
|
|
|
+ *
|
|
|
+ *
|
|
|
+ * CharString Command Encoding:
|
|
|
+ * CharStrings commands are encoded in 1 or 2 bytes.
|
|
|
+ *
|
|
|
+ * Single byte commands are encoded in 1 byte that contains a value between
|
|
|
+ * 0 and 31 inclusive.
|
|
|
+ * If a command byte contains the value 12, then the value in the next byte
|
|
|
+ * indicates a command. This "escape" mechanism allows many extra commands
|
|
|
+ * to be encoded and this encoding technique helps to minimize the length of
|
|
|
+ * the charStrings.
|
|
|
+ */
|
|
|
+var Type1CharString = (function Type1CharStringClosure() {
|
|
|
+ var COMMAND_MAP = {
|
|
|
+ 'hstem': [1],
|
|
|
+ 'vstem': [3],
|
|
|
+ 'vmoveto': [4],
|
|
|
+ 'rlineto': [5],
|
|
|
+ 'hlineto': [6],
|
|
|
+ 'vlineto': [7],
|
|
|
+ 'rrcurveto': [8],
|
|
|
+ 'callsubr': [10],
|
|
|
+ 'flex': [12, 35],
|
|
|
+ 'drop' : [12, 18],
|
|
|
+ 'endchar': [14],
|
|
|
+ 'rmoveto': [21],
|
|
|
+ 'hmoveto': [22],
|
|
|
+ 'vhcurveto': [30],
|
|
|
+ 'hvcurveto': [31]
|
|
|
+ };
|
|
|
|
|
|
- var found = false;
|
|
|
- for (i = 0; i < cmapMappingsLength; ++i) {
|
|
|
- if (cmapMappings[i].charCode !== unicodeOrCharCode) {
|
|
|
- continue;
|
|
|
+ function Type1CharString() {
|
|
|
+ this.width = 0;
|
|
|
+ this.lsb = 0;
|
|
|
+ this.flexing = false;
|
|
|
+ this.output = [];
|
|
|
+ this.stack = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ Type1CharString.prototype = {
|
|
|
+ convert: function Type1CharString_convert(encoded, subrs) {
|
|
|
+ var count = encoded.length;
|
|
|
+ var error = false;
|
|
|
+ var wx, sbx, subrNumber;
|
|
|
+ for (var i = 0; i < count; i++) {
|
|
|
+ var value = encoded[i];
|
|
|
+ if (value < 32) {
|
|
|
+ if (value === 12) {
|
|
|
+ value = (value << 8) + encoded[++i];
|
|
|
+ }
|
|
|
+ switch (value) {
|
|
|
+ case 1: // hstem
|
|
|
+ if (!HINTING_ENABLED) {
|
|
|
+ this.stack = [];
|
|
|
+ break;
|
|
|
}
|
|
|
- var code = isUnicode ? charCode : unicodeOrCharCode;
|
|
|
- if (hasGlyph(cmapMappings[i].glyphId, code, -1)) {
|
|
|
- charCodeToGlyphId[charCode] = cmapMappings[i].glyphId;
|
|
|
- found = true;
|
|
|
+ error = this.executeCommand(2, COMMAND_MAP.hstem);
|
|
|
+ break;
|
|
|
+ case 3: // vstem
|
|
|
+ if (!HINTING_ENABLED) {
|
|
|
+ this.stack = [];
|
|
|
break;
|
|
|
}
|
|
|
- }
|
|
|
- if (!found && properties.glyphNames) {
|
|
|
- // Try to map using the post table.
|
|
|
- var glyphId = properties.glyphNames.indexOf(glyphName);
|
|
|
- if (glyphId > 0 && hasGlyph(glyphId, -1, -1)) {
|
|
|
- charCodeToGlyphId[charCode] = glyphId;
|
|
|
- found = true;
|
|
|
+ error = this.executeCommand(2, COMMAND_MAP.vstem);
|
|
|
+ break;
|
|
|
+ case 4: // vmoveto
|
|
|
+ if (this.flexing) {
|
|
|
+ if (this.stack.length < 1) {
|
|
|
+ error = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ // Add the dx for flex and but also swap the values so they are
|
|
|
+ // the right order.
|
|
|
+ var dy = this.stack.pop();
|
|
|
+ this.stack.push(0, dy);
|
|
|
+ break;
|
|
|
}
|
|
|
- }
|
|
|
- if (!found) {
|
|
|
- charCodeToGlyphId[charCode] = 0; // notdef
|
|
|
- }
|
|
|
+ error = this.executeCommand(1, COMMAND_MAP.vmoveto);
|
|
|
+ break;
|
|
|
+ case 5: // rlineto
|
|
|
+ error = this.executeCommand(2, COMMAND_MAP.rlineto);
|
|
|
+ break;
|
|
|
+ case 6: // hlineto
|
|
|
+ error = this.executeCommand(1, COMMAND_MAP.hlineto);
|
|
|
+ break;
|
|
|
+ case 7: // vlineto
|
|
|
+ error = this.executeCommand(1, COMMAND_MAP.vlineto);
|
|
|
+ break;
|
|
|
+ case 8: // rrcurveto
|
|
|
+ error = this.executeCommand(6, COMMAND_MAP.rrcurveto);
|
|
|
+ break;
|
|
|
+ case 9: // closepath
|
|
|
+ // closepath is a Type1 command that does not take argument and is
|
|
|
+ // useless in Type2 and it can simply be ignored.
|
|
|
+ this.stack = [];
|
|
|
+ break;
|
|
|
+ case 10: // callsubr
|
|
|
+ if (this.stack.length < 1) {
|
|
|
+ error = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ subrNumber = this.stack.pop();
|
|
|
+ error = this.convert(subrs[subrNumber], subrs);
|
|
|
+ break;
|
|
|
+ case 11: // return
|
|
|
+ return error;
|
|
|
+ case 13: // hsbw
|
|
|
+ if (this.stack.length < 2) {
|
|
|
+ error = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ // To convert to type2 we have to move the width value to the
|
|
|
+ // first part of the charstring and then use hmoveto with lsb.
|
|
|
+ wx = this.stack.pop();
|
|
|
+ sbx = this.stack.pop();
|
|
|
+ this.lsb = sbx;
|
|
|
+ this.width = wx;
|
|
|
+ this.stack.push(wx, sbx);
|
|
|
+ error = this.executeCommand(2, COMMAND_MAP.hmoveto);
|
|
|
+ break;
|
|
|
+ case 14: // endchar
|
|
|
+ this.output.push(COMMAND_MAP.endchar[0]);
|
|
|
+ break;
|
|
|
+ case 21: // rmoveto
|
|
|
+ if (this.flexing) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ error = this.executeCommand(2, COMMAND_MAP.rmoveto);
|
|
|
+ break;
|
|
|
+ case 22: // hmoveto
|
|
|
+ if (this.flexing) {
|
|
|
+ // Add the dy for flex.
|
|
|
+ this.stack.push(0);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ error = this.executeCommand(1, COMMAND_MAP.hmoveto);
|
|
|
+ break;
|
|
|
+ case 30: // vhcurveto
|
|
|
+ error = this.executeCommand(4, COMMAND_MAP.vhcurveto);
|
|
|
+ break;
|
|
|
+ case 31: // hvcurveto
|
|
|
+ error = this.executeCommand(4, COMMAND_MAP.hvcurveto);
|
|
|
+ break;
|
|
|
+ case (12 << 8) + 0: // dotsection
|
|
|
+ // dotsection is a Type1 command to specify some hinting feature
|
|
|
+ // for dots that do not take a parameter and it can safely be
|
|
|
+ // ignored for Type2.
|
|
|
+ this.stack = [];
|
|
|
+ break;
|
|
|
+ case (12 << 8) + 1: // vstem3
|
|
|
+ if (!HINTING_ENABLED) {
|
|
|
+ this.stack = [];
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ // [vh]stem3 are Type1 only and Type2 supports [vh]stem with
|
|
|
+ // multiple parameters, so instead of returning [vh]stem3 take a
|
|
|
+ // shortcut and return [vhstem] instead.
|
|
|
+ error = this.executeCommand(2, COMMAND_MAP.vstem);
|
|
|
+ break;
|
|
|
+ case (12 << 8) + 2: // hstem3
|
|
|
+ if (!HINTING_ENABLED) {
|
|
|
+ this.stack = [];
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ // See vstem3.
|
|
|
+ error = this.executeCommand(2, COMMAND_MAP.hstem);
|
|
|
+ break;
|
|
|
+ case (12 << 8) + 6: // seac
|
|
|
+ // seac is like type 2's special endchar but it doesn't use the
|
|
|
+ // first argument asb, so remove it.
|
|
|
+ if (SEAC_ANALYSIS_ENABLED) {
|
|
|
+ this.seac = this.stack.splice(-4, 4);
|
|
|
+ error = this.executeCommand(0, COMMAND_MAP.endchar);
|
|
|
+ } else {
|
|
|
+ error = this.executeCommand(4, COMMAND_MAP.endchar);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case (12 << 8) + 7: // sbw
|
|
|
+ if (this.stack.length < 4) {
|
|
|
+ error = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ // To convert to type2 we have to move the width value to the
|
|
|
+ // first part of the charstring and then use rmoveto with
|
|
|
+ // (dx, dy). The height argument will not be used for vmtx and
|
|
|
+ // vhea tables reconstruction -- ignoring it.
|
|
|
+ var wy = this.stack.pop();
|
|
|
+ wx = this.stack.pop();
|
|
|
+ var sby = this.stack.pop();
|
|
|
+ sbx = this.stack.pop();
|
|
|
+ this.lsb = sbx;
|
|
|
+ this.width = wx;
|
|
|
+ this.stack.push(wx, sbx, sby);
|
|
|
+ error = this.executeCommand(3, COMMAND_MAP.rmoveto);
|
|
|
+ break;
|
|
|
+ case (12 << 8) + 12: // div
|
|
|
+ if (this.stack.length < 2) {
|
|
|
+ error = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ var num2 = this.stack.pop();
|
|
|
+ var num1 = this.stack.pop();
|
|
|
+ this.stack.push(num1 / num2);
|
|
|
+ break;
|
|
|
+ case (12 << 8) + 16: // callothersubr
|
|
|
+ if (this.stack.length < 2) {
|
|
|
+ error = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ subrNumber = this.stack.pop();
|
|
|
+ var numArgs = this.stack.pop();
|
|
|
+ if (subrNumber === 0 && numArgs === 3) {
|
|
|
+ var flexArgs = this.stack.splice(this.stack.length - 17, 17);
|
|
|
+ this.stack.push(
|
|
|
+ flexArgs[2] + flexArgs[0], // bcp1x + rpx
|
|
|
+ flexArgs[3] + flexArgs[1], // bcp1y + rpy
|
|
|
+ flexArgs[4], // bcp2x
|
|
|
+ flexArgs[5], // bcp2y
|
|
|
+ flexArgs[6], // p2x
|
|
|
+ flexArgs[7], // p2y
|
|
|
+ flexArgs[8], // bcp3x
|
|
|
+ flexArgs[9], // bcp3y
|
|
|
+ flexArgs[10], // bcp4x
|
|
|
+ flexArgs[11], // bcp4y
|
|
|
+ flexArgs[12], // p3x
|
|
|
+ flexArgs[13], // p3y
|
|
|
+ flexArgs[14] // flexDepth
|
|
|
+ // 15 = finalx unused by flex
|
|
|
+ // 16 = finaly unused by flex
|
|
|
+ );
|
|
|
+ error = this.executeCommand(13, COMMAND_MAP.flex, true);
|
|
|
+ this.flexing = false;
|
|
|
+ this.stack.push(flexArgs[15], flexArgs[16]);
|
|
|
+ } else if (subrNumber === 1 && numArgs === 0) {
|
|
|
+ this.flexing = true;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case (12 << 8) + 17: // pop
|
|
|
+ // Ignore this since it is only used with othersubr.
|
|
|
+ break;
|
|
|
+ case (12 << 8) + 33: // setcurrentpoint
|
|
|
+ // Ignore for now.
|
|
|
+ this.stack = [];
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ warn('Unknown type 1 charstring command of "' + value + '"');
|
|
|
+ break;
|
|
|
}
|
|
|
- } 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;
|
|
|
+ if (error) {
|
|
|
+ break;
|
|
|
}
|
|
|
+ continue;
|
|
|
+ } else if (value <= 246) {
|
|
|
+ value = value - 139;
|
|
|
+ } else if (value <= 250) {
|
|
|
+ value = ((value - 247) * 256) + encoded[++i] + 108;
|
|
|
+ } else if (value <= 254) {
|
|
|
+ value = -((value - 251) * 256) - encoded[++i] - 108;
|
|
|
} 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
|
|
|
- // high byte of the range [of charcodes in the cmap table], to form
|
|
|
- // a two-byte character, which shall be used to select the
|
|
|
- // associated glyph description from the subtable'.
|
|
|
- //
|
|
|
- // For (1, 0) cmap tables:
|
|
|
- // 'single bytes from the string shall be used to look up the
|
|
|
- // associated glyph descriptions from the subtable'. This means
|
|
|
- // charcodes in the cmap will be single bytes, so no-op since
|
|
|
- // glyph.charCode & 0xFF === glyph.charCode
|
|
|
- for (i = 0; i < cmapMappingsLength; ++i) {
|
|
|
- charCode = cmapMappings[i].charCode & 0xFF;
|
|
|
- charCodeToGlyphId[charCode] = cmapMappings[i].glyphId;
|
|
|
- }
|
|
|
+ value = (encoded[++i] & 0xff) << 24 | (encoded[++i] & 0xff) << 16 |
|
|
|
+ (encoded[++i] & 0xff) << 8 | (encoded[++i] & 0xff) << 0;
|
|
|
}
|
|
|
+ this.stack.push(value);
|
|
|
}
|
|
|
+ return error;
|
|
|
+ },
|
|
|
|
|
|
- if (charCodeToGlyphId.length === 0) {
|
|
|
- // defines at least one glyph
|
|
|
- charCodeToGlyphId[0] = 0;
|
|
|
+ executeCommand: function(howManyArgs, command, keepStack) {
|
|
|
+ var stackLength = this.stack.length;
|
|
|
+ if (howManyArgs > stackLength) {
|
|
|
+ return true;
|
|
|
}
|
|
|
-
|
|
|
- // 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, numGlyphs)
|
|
|
- };
|
|
|
-
|
|
|
- if (!tables['OS/2'] || !validateOS2Table(tables['OS/2'])) {
|
|
|
- tables['OS/2'] = {
|
|
|
- tag: 'OS/2',
|
|
|
- data: createOS2Table(properties, newMapping.charCodeToGlyphId,
|
|
|
- metricsOverride)
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- // Rewrite the 'post' table if needed
|
|
|
- if (!tables['post']) {
|
|
|
- tables['post'] = {
|
|
|
- tag: 'post',
|
|
|
- data: createPostTable(properties)
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- if (!isTrueType) {
|
|
|
- try {
|
|
|
- // Trying to repair CFF file
|
|
|
- cffFile = new Stream(tables['CFF '].data);
|
|
|
- var parser = new CFFParser(cffFile, properties);
|
|
|
- cff = parser.parse();
|
|
|
- var compiler = new CFFCompiler(cff);
|
|
|
- tables['CFF '].data = compiler.compile();
|
|
|
- } catch (e) {
|
|
|
- warn('Failed to compile font ' + properties.loadedName);
|
|
|
+ var start = stackLength - howManyArgs;
|
|
|
+ for (var i = start; i < stackLength; i++) {
|
|
|
+ var value = this.stack[i];
|
|
|
+ if (value === (value | 0)) { // int
|
|
|
+ this.output.push(28, (value >> 8) & 0xff, value & 0xff);
|
|
|
+ } else { // fixed point
|
|
|
+ value = (65536 * value) | 0;
|
|
|
+ this.output.push(255,
|
|
|
+ (value >> 24) & 0xFF,
|
|
|
+ (value >> 16) & 0xFF,
|
|
|
+ (value >> 8) & 0xFF,
|
|
|
+ value & 0xFF);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- // Re-creating 'name' table
|
|
|
- if (!tables['name']) {
|
|
|
- tables['name'] = {
|
|
|
- tag: 'name',
|
|
|
- data: createNameTable(this.name)
|
|
|
- };
|
|
|
+ this.output.push.apply(this.output, command);
|
|
|
+ if (keepStack) {
|
|
|
+ this.stack.splice(start, howManyArgs);
|
|
|
} else {
|
|
|
- // ... using existing 'name' table as prototype
|
|
|
- var namePrototype = readNameTable(tables['name']);
|
|
|
- tables['name'].data = createNameTable(name, namePrototype);
|
|
|
+ this.stack.length = 0;
|
|
|
}
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- var builder = new OpenTypeFileBuilder(header.version);
|
|
|
- for (var tableTag in tables) {
|
|
|
- builder.addTable(tableTag, tables[tableTag].data);
|
|
|
- }
|
|
|
- return builder.toArray();
|
|
|
- },
|
|
|
+ return Type1CharString;
|
|
|
+})();
|
|
|
|
|
|
- convert: function Font_convert(fontName, font, properties) {
|
|
|
- // TODO: Check the charstring widths to determine this.
|
|
|
- properties.fixedPitch = false;
|
|
|
+/*
|
|
|
+ * Type1Parser encapsulate the needed code for parsing a Type1 font
|
|
|
+ * program. Some of its logic depends on the Type2 charstrings
|
|
|
+ * structure.
|
|
|
+ * Note: this doesn't really parse the font since that would require evaluation
|
|
|
+ * of PostScript, but it is possible in most cases to extract what we need
|
|
|
+ * without a full parse.
|
|
|
+ */
|
|
|
+var Type1Parser = (function Type1ParserClosure() {
|
|
|
+ /*
|
|
|
+ * Decrypt a Sequence of Ciphertext Bytes to Produce the Original Sequence
|
|
|
+ * of Plaintext Bytes. The function took a key as a parameter which can be
|
|
|
+ * for decrypting the eexec block of for decoding charStrings.
|
|
|
+ */
|
|
|
+ var EEXEC_ENCRYPT_KEY = 55665;
|
|
|
+ var CHAR_STRS_ENCRYPT_KEY = 4330;
|
|
|
|
|
|
- var mapping = font.getGlyphMapping(properties);
|
|
|
- var newMapping = adjustMapping(mapping, properties);
|
|
|
- this.toFontChar = newMapping.toFontChar;
|
|
|
- var numGlyphs = font.numGlyphs;
|
|
|
+ function isHexDigit(code) {
|
|
|
+ return code >= 48 && code <= 57 || // '0'-'9'
|
|
|
+ code >= 65 && code <= 70 || // 'A'-'F'
|
|
|
+ code >= 97 && code <= 102; // 'a'-'f'
|
|
|
+ }
|
|
|
|
|
|
- function getCharCodes(charCodeToGlyphId, glyphId) {
|
|
|
- var charCodes = null;
|
|
|
- for (var charCode in charCodeToGlyphId) {
|
|
|
- if (glyphId === charCodeToGlyphId[charCode]) {
|
|
|
- if (!charCodes) {
|
|
|
- charCodes = [];
|
|
|
- }
|
|
|
- charCodes.push(charCode | 0);
|
|
|
- }
|
|
|
- }
|
|
|
- return charCodes;
|
|
|
- }
|
|
|
+ function decrypt(data, key, discardNumber) {
|
|
|
+ if (discardNumber >= data.length) {
|
|
|
+ return new Uint8Array(0);
|
|
|
+ }
|
|
|
+ var r = key | 0, c1 = 52845, c2 = 22719, i, j;
|
|
|
+ for (i = 0; i < discardNumber; i++) {
|
|
|
+ r = ((data[i] + r) * c1 + c2) & ((1 << 16) - 1);
|
|
|
+ }
|
|
|
+ var count = data.length - discardNumber;
|
|
|
+ var decrypted = new Uint8Array(count);
|
|
|
+ for (i = discardNumber, j = 0; j < count; i++, j++) {
|
|
|
+ var value = data[i];
|
|
|
+ decrypted[j] = value ^ (r >> 8);
|
|
|
+ r = ((value + r) * c1 + c2) & ((1 << 16) - 1);
|
|
|
+ }
|
|
|
+ return decrypted;
|
|
|
+ }
|
|
|
|
|
|
- function createCharCode(charCodeToGlyphId, glyphId) {
|
|
|
- for (var charCode in charCodeToGlyphId) {
|
|
|
- if (glyphId === charCodeToGlyphId[charCode]) {
|
|
|
- return charCode | 0;
|
|
|
- }
|
|
|
- }
|
|
|
- newMapping.charCodeToGlyphId[newMapping.nextAvailableFontCharCode] =
|
|
|
- glyphId;
|
|
|
- return newMapping.nextAvailableFontCharCode++;
|
|
|
+ function decryptAscii(data, key, discardNumber) {
|
|
|
+ var r = key | 0, c1 = 52845, c2 = 22719;
|
|
|
+ var count = data.length, maybeLength = count >>> 1;
|
|
|
+ var decrypted = new Uint8Array(maybeLength);
|
|
|
+ var i, j;
|
|
|
+ for (i = 0, j = 0; i < count; i++) {
|
|
|
+ var digit1 = data[i];
|
|
|
+ if (!isHexDigit(digit1)) {
|
|
|
+ continue;
|
|
|
}
|
|
|
-
|
|
|
- var seacs = font.seacs;
|
|
|
- if (SEAC_ANALYSIS_ENABLED && seacs && seacs.length) {
|
|
|
- var matrix = properties.fontMatrix || FONT_IDENTITY_MATRIX;
|
|
|
- var charset = font.getCharset();
|
|
|
- var seacMap = Object.create(null);
|
|
|
- for (var glyphId in seacs) {
|
|
|
- glyphId |= 0;
|
|
|
- var seac = seacs[glyphId];
|
|
|
- var baseGlyphName = StandardEncoding[seac[2]];
|
|
|
- var accentGlyphName = StandardEncoding[seac[3]];
|
|
|
- var baseGlyphId = charset.indexOf(baseGlyphName);
|
|
|
- var accentGlyphId = charset.indexOf(accentGlyphName);
|
|
|
- if (baseGlyphId < 0 || accentGlyphId < 0) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- var accentOffset = {
|
|
|
- x: seac[0] * matrix[0] + seac[1] * matrix[2] + matrix[4],
|
|
|
- y: seac[0] * matrix[1] + seac[1] * matrix[3] + matrix[5]
|
|
|
- };
|
|
|
-
|
|
|
- var charCodes = getCharCodes(mapping, glyphId);
|
|
|
- if (!charCodes) {
|
|
|
- // There's no point in mapping it if the char code was never mapped
|
|
|
- // to begin with.
|
|
|
- continue;
|
|
|
- }
|
|
|
- for (var i = 0, ii = charCodes.length; i < ii; i++) {
|
|
|
- var charCode = charCodes[i];
|
|
|
- // Find a fontCharCode that maps to the base and accent glyphs.
|
|
|
- // If one doesn't exists, create it.
|
|
|
- var charCodeToGlyphId = newMapping.charCodeToGlyphId;
|
|
|
- var baseFontCharCode = createCharCode(charCodeToGlyphId,
|
|
|
- baseGlyphId);
|
|
|
- var accentFontCharCode = createCharCode(charCodeToGlyphId,
|
|
|
- accentGlyphId);
|
|
|
- seacMap[charCode] = {
|
|
|
- baseFontCharCode: baseFontCharCode,
|
|
|
- accentFontCharCode: accentFontCharCode,
|
|
|
- accentOffset: accentOffset
|
|
|
- };
|
|
|
- }
|
|
|
- }
|
|
|
- properties.seacMap = seacMap;
|
|
|
+ i++;
|
|
|
+ var digit2;
|
|
|
+ while (i < count && !isHexDigit(digit2 = data[i])) {
|
|
|
+ i++;
|
|
|
+ }
|
|
|
+ if (i < count) {
|
|
|
+ var value = parseInt(String.fromCharCode(digit1, digit2), 16);
|
|
|
+ decrypted[j++] = value ^ (r >> 8);
|
|
|
+ r = ((value + r) * c1 + c2) & ((1 << 16) - 1);
|
|
|
}
|
|
|
+ }
|
|
|
+ return Array.prototype.slice.call(decrypted, discardNumber, j);
|
|
|
+ }
|
|
|
|
|
|
- var unitsPerEm = 1 / (properties.fontMatrix || FONT_IDENTITY_MATRIX)[0];
|
|
|
+ function isSpecial(c) {
|
|
|
+ return c === 0x2F || // '/'
|
|
|
+ c === 0x5B || c === 0x5D || // '[', ']'
|
|
|
+ c === 0x7B || c === 0x7D || // '{', '}'
|
|
|
+ c === 0x28 || c === 0x29; // '(', ')'
|
|
|
+ }
|
|
|
|
|
|
- var builder = new OpenTypeFileBuilder('\x4F\x54\x54\x4F');
|
|
|
- // PostScript Font Program
|
|
|
- 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,
|
|
|
- numGlyphs));
|
|
|
- // Font header
|
|
|
- builder.addTable('head',
|
|
|
- '\x00\x01\x00\x00' + // Version number
|
|
|
- '\x00\x00\x10\x00' + // fontRevision
|
|
|
- '\x00\x00\x00\x00' + // checksumAdjustement
|
|
|
- '\x5F\x0F\x3C\xF5' + // magicNumber
|
|
|
- '\x00\x00' + // Flags
|
|
|
- safeString16(unitsPerEm) + // unitsPerEM
|
|
|
- '\x00\x00\x00\x00\x9e\x0b\x7e\x27' + // creation date
|
|
|
- '\x00\x00\x00\x00\x9e\x0b\x7e\x27' + // modifification date
|
|
|
- '\x00\x00' + // xMin
|
|
|
- safeString16(properties.descent) + // yMin
|
|
|
- '\x0F\xFF' + // xMax
|
|
|
- safeString16(properties.ascent) + // yMax
|
|
|
- string16(properties.italicAngle ? 2 : 0) + // macStyle
|
|
|
- '\x00\x11' + // lowestRecPPEM
|
|
|
- '\x00\x00' + // fontDirectionHint
|
|
|
- '\x00\x00' + // indexToLocFormat
|
|
|
- '\x00\x00'); // glyphDataFormat
|
|
|
+ function Type1Parser(stream, encrypted) {
|
|
|
+ if (encrypted) {
|
|
|
+ var data = stream.getBytes();
|
|
|
+ var isBinary = !(isHexDigit(data[0]) && isHexDigit(data[1]) &&
|
|
|
+ isHexDigit(data[2]) && isHexDigit(data[3]));
|
|
|
+ stream = new Stream(isBinary ? decrypt(data, EEXEC_ENCRYPT_KEY, 4) :
|
|
|
+ decryptAscii(data, EEXEC_ENCRYPT_KEY, 4));
|
|
|
+ }
|
|
|
+ this.stream = stream;
|
|
|
+ this.nextChar();
|
|
|
+ }
|
|
|
|
|
|
- // Horizontal header
|
|
|
- builder.addTable('hhea',
|
|
|
- '\x00\x01\x00\x00' + // Version number
|
|
|
- safeString16(properties.ascent) + // Typographic Ascent
|
|
|
- safeString16(properties.descent) + // Typographic Descent
|
|
|
- '\x00\x00' + // Line Gap
|
|
|
- '\xFF\xFF' + // advanceWidthMax
|
|
|
- '\x00\x00' + // minLeftSidebearing
|
|
|
- '\x00\x00' + // minRightSidebearing
|
|
|
- '\x00\x00' + // xMaxExtent
|
|
|
- safeString16(properties.capHeight) + // caretSlopeRise
|
|
|
- safeString16(Math.tan(properties.italicAngle) *
|
|
|
- properties.xHeight) + // caretSlopeRun
|
|
|
- '\x00\x00' + // caretOffset
|
|
|
- '\x00\x00' + // -reserved-
|
|
|
- '\x00\x00' + // -reserved-
|
|
|
- '\x00\x00' + // -reserved-
|
|
|
- '\x00\x00' + // -reserved-
|
|
|
- '\x00\x00' + // metricDataFormat
|
|
|
- string16(numGlyphs)); // Number of HMetrics
|
|
|
+ Type1Parser.prototype = {
|
|
|
+ readNumberArray: function Type1Parser_readNumberArray() {
|
|
|
+ this.getToken(); // read '[' or '{' (arrays can start with either)
|
|
|
+ var array = [];
|
|
|
+ while (true) {
|
|
|
+ var token = this.getToken();
|
|
|
+ if (token === null || token === ']' || token === '}') {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ array.push(parseFloat(token || 0));
|
|
|
+ }
|
|
|
+ return array;
|
|
|
+ },
|
|
|
|
|
|
- // Horizontal metrics
|
|
|
- builder.addTable('hmtx', (function fontFieldsHmtx() {
|
|
|
- var charstrings = font.charstrings;
|
|
|
- var cffWidths = font.cff ? font.cff.widths : null;
|
|
|
- var hmtx = '\x00\x00\x00\x00'; // Fake .notdef
|
|
|
- for (var i = 1, ii = numGlyphs; i < ii; i++) {
|
|
|
- var width = 0;
|
|
|
- if (charstrings) {
|
|
|
- var charstring = charstrings[i - 1];
|
|
|
- width = 'width' in charstring ? charstring.width : 0;
|
|
|
- } else if (cffWidths) {
|
|
|
- width = Math.ceil(cffWidths[i] || 0);
|
|
|
- }
|
|
|
- hmtx += string16(width) + string16(0);
|
|
|
- }
|
|
|
- return hmtx;
|
|
|
- })());
|
|
|
+ readNumber: function Type1Parser_readNumber() {
|
|
|
+ var token = this.getToken();
|
|
|
+ return parseFloat(token || 0);
|
|
|
+ },
|
|
|
|
|
|
- // Maximum profile
|
|
|
- builder.addTable('maxp',
|
|
|
- '\x00\x00\x50\x00' + // Version number
|
|
|
- string16(numGlyphs)); // Num of glyphs
|
|
|
+ readInt: function Type1Parser_readInt() {
|
|
|
+ // Use '| 0' to prevent setting a double into length such as the double
|
|
|
+ // does not flow into the loop variable.
|
|
|
+ var token = this.getToken();
|
|
|
+ return parseInt(token || 0, 10) | 0;
|
|
|
+ },
|
|
|
|
|
|
- // Naming tables
|
|
|
- builder.addTable('name', createNameTable(fontName));
|
|
|
+ readBoolean: function Type1Parser_readBoolean() {
|
|
|
+ var token = this.getToken();
|
|
|
|
|
|
- // PostScript informations
|
|
|
- builder.addTable('post', createPostTable(properties));
|
|
|
+ // Use 1 and 0 since that's what type2 charstrings use.
|
|
|
+ return token === 'true' ? 1 : 0;
|
|
|
+ },
|
|
|
|
|
|
- return builder.toArray();
|
|
|
+ nextChar : function Type1_nextChar() {
|
|
|
+ return (this.currentChar = this.stream.getByte());
|
|
|
},
|
|
|
|
|
|
- /**
|
|
|
- * Builds a char code to unicode map based on section 9.10 of the spec.
|
|
|
- * @param {Object} properties Font properties object.
|
|
|
- * @return {Object} A ToUnicodeMap object.
|
|
|
- */
|
|
|
- buildToUnicode: function Font_buildToUnicode(properties) {
|
|
|
- // Section 9.10.2 Mapping Character Codes to Unicode Values
|
|
|
- if (properties.toUnicode && properties.toUnicode.length !== 0) {
|
|
|
- return properties.toUnicode;
|
|
|
- }
|
|
|
- // According to the spec if the font is a simple font we should only map
|
|
|
- // to unicode if the base encoding is MacRoman, MacExpert, or WinAnsi or
|
|
|
- // the differences array only contains adobe standard or symbol set names,
|
|
|
- // in pratice it seems better to always try to create a toUnicode
|
|
|
- // map based of the default encoding.
|
|
|
- var toUnicode, charcode;
|
|
|
- if (!properties.composite /* is simple font */) {
|
|
|
- toUnicode = [];
|
|
|
- var encoding = properties.defaultEncoding.slice();
|
|
|
- var baseEncodingName = properties.baseEncodingName;
|
|
|
- // Merge in the differences array.
|
|
|
- var differences = properties.differences;
|
|
|
- for (charcode in differences) {
|
|
|
- encoding[charcode] = differences[charcode];
|
|
|
+ getToken: function Type1Parser_getToken() {
|
|
|
+ // Eat whitespace and comments.
|
|
|
+ var comment = false;
|
|
|
+ var ch = this.currentChar;
|
|
|
+ while (true) {
|
|
|
+ if (ch === -1) {
|
|
|
+ return null;
|
|
|
}
|
|
|
- var glyphsUnicodeMap = getGlyphsUnicode();
|
|
|
- for (charcode in encoding) {
|
|
|
- // a) Map the character code to a character name.
|
|
|
- var glyphName = encoding[charcode];
|
|
|
- // b) Look up the character name in the Adobe Glyph List (see the
|
|
|
- // Bibliography) to obtain the corresponding Unicode value.
|
|
|
- if (glyphName === '') {
|
|
|
- continue;
|
|
|
- } else if (glyphsUnicodeMap[glyphName] === undefined) {
|
|
|
- // (undocumented) c) Few heuristics to recognize unknown glyphs
|
|
|
- // NOTE: Adobe Reader does not do this step, but OSX Preview does
|
|
|
- var code = 0;
|
|
|
- switch (glyphName[0]) {
|
|
|
- case 'G': // Gxx glyph
|
|
|
- if (glyphName.length === 3) {
|
|
|
- code = parseInt(glyphName.substr(1), 16);
|
|
|
- }
|
|
|
- break;
|
|
|
- case 'g': // g00xx glyph
|
|
|
- if (glyphName.length === 5) {
|
|
|
- code = parseInt(glyphName.substr(1), 16);
|
|
|
- }
|
|
|
- break;
|
|
|
- case 'C': // Cddd glyph
|
|
|
- case 'c': // cddd glyph
|
|
|
- if (glyphName.length >= 3) {
|
|
|
- code = +glyphName.substr(1);
|
|
|
- }
|
|
|
- break;
|
|
|
- }
|
|
|
- if (code) {
|
|
|
- // If |baseEncodingName| is one the predefined encodings,
|
|
|
- // and |code| equals |charcode|, using the glyph defined in the
|
|
|
- // baseEncoding seems to yield a better |toUnicode| mapping
|
|
|
- // (fixes issue 5070).
|
|
|
- if (baseEncodingName && code === +charcode) {
|
|
|
- var baseEncoding = getEncoding(baseEncodingName);
|
|
|
- if (baseEncoding && (glyphName = baseEncoding[charcode])) {
|
|
|
- toUnicode[charcode] =
|
|
|
- String.fromCharCode(glyphsUnicodeMap[glyphName]);
|
|
|
- continue;
|
|
|
- }
|
|
|
- }
|
|
|
- toUnicode[charcode] = String.fromCharCode(code);
|
|
|
- }
|
|
|
- continue;
|
|
|
+
|
|
|
+ if (comment) {
|
|
|
+ if (ch === 0x0A || ch === 0x0D) {
|
|
|
+ comment = false;
|
|
|
}
|
|
|
- toUnicode[charcode] =
|
|
|
- String.fromCharCode(glyphsUnicodeMap[glyphName]);
|
|
|
+ } else if (ch === 0x25) { // '%'
|
|
|
+ comment = true;
|
|
|
+ } else if (!Lexer.isSpace(ch)) {
|
|
|
+ break;
|
|
|
}
|
|
|
- return new ToUnicodeMap(toUnicode);
|
|
|
+ ch = this.nextChar();
|
|
|
}
|
|
|
- // If the font is a composite font that uses one of the predefined CMaps
|
|
|
- // listed in Table 118 (except Identity–H and Identity–V) or whose
|
|
|
- // descendant CIDFont uses the Adobe-GB1, Adobe-CNS1, Adobe-Japan1, or
|
|
|
- // Adobe-Korea1 character collection:
|
|
|
- if (properties.composite && (
|
|
|
- (properties.cMap.builtInCMap &&
|
|
|
- !(properties.cMap instanceof IdentityCMap)) ||
|
|
|
- (properties.cidSystemInfo.registry === 'Adobe' &&
|
|
|
- (properties.cidSystemInfo.ordering === 'GB1' ||
|
|
|
- properties.cidSystemInfo.ordering === 'CNS1' ||
|
|
|
- properties.cidSystemInfo.ordering === 'Japan1' ||
|
|
|
- properties.cidSystemInfo.ordering === 'Korea1')))) {
|
|
|
- // Then:
|
|
|
- // a) Map the character code to a character identifier (CID) according
|
|
|
- // to the font’s CMap.
|
|
|
- // b) Obtain the registry and ordering of the character collection used
|
|
|
- // by the font’s CMap (for example, Adobe and Japan1) from its
|
|
|
- // CIDSystemInfo dictionary.
|
|
|
- var registry = properties.cidSystemInfo.registry;
|
|
|
- var ordering = properties.cidSystemInfo.ordering;
|
|
|
- // c) Construct a second CMap name by concatenating the registry and
|
|
|
- // ordering obtained in step (b) in the format registry–ordering–UCS2
|
|
|
- // (for example, Adobe–Japan1–UCS2).
|
|
|
- var ucs2CMapName = new Name(registry + '-' + ordering + '-UCS2');
|
|
|
- // d) Obtain the CMap with the name constructed in step (c) (available
|
|
|
- // from the ASN Web site; see the Bibliography).
|
|
|
- var ucs2CMap = CMapFactory.create(ucs2CMapName,
|
|
|
- { url: PDFJS.cMapUrl, packed: PDFJS.cMapPacked }, null);
|
|
|
- var cMap = properties.cMap;
|
|
|
- toUnicode = [];
|
|
|
- cMap.forEach(function(charcode, cid) {
|
|
|
- assert(cid <= 0xffff, 'Max size of CID is 65,535');
|
|
|
- // e) Map the CID obtained in step (a) according to the CMap obtained
|
|
|
- // in step (d), producing a Unicode value.
|
|
|
- var ucs2 = ucs2CMap.lookup(cid);
|
|
|
- if (ucs2) {
|
|
|
- toUnicode[charcode] =
|
|
|
- String.fromCharCode((ucs2.charCodeAt(0) << 8) +
|
|
|
- ucs2.charCodeAt(1));
|
|
|
- }
|
|
|
- });
|
|
|
- return new ToUnicodeMap(toUnicode);
|
|
|
+ if (isSpecial(ch)) {
|
|
|
+ this.nextChar();
|
|
|
+ return String.fromCharCode(ch);
|
|
|
}
|
|
|
-
|
|
|
- // The viewer's choice, just use an identity map.
|
|
|
- return new IdentityToUnicodeMap(properties.firstChar,
|
|
|
- properties.lastChar);
|
|
|
+ var token = '';
|
|
|
+ do {
|
|
|
+ token += String.fromCharCode(ch);
|
|
|
+ ch = this.nextChar();
|
|
|
+ } while (ch >= 0 && !Lexer.isSpace(ch) && !isSpecial(ch));
|
|
|
+ return token;
|
|
|
},
|
|
|
|
|
|
- get spaceWidth() {
|
|
|
- if ('_shadowWidth' in this) {
|
|
|
- return this._shadowWidth;
|
|
|
- }
|
|
|
+ /*
|
|
|
+ * Returns an object containing a Subrs array and a CharStrings
|
|
|
+ * array extracted from and eexec encrypted block of data
|
|
|
+ */
|
|
|
+ extractFontProgram: function Type1Parser_extractFontProgram() {
|
|
|
+ var stream = this.stream;
|
|
|
|
|
|
- // trying to estimate space character width
|
|
|
- var possibleSpaceReplacements = ['space', 'minus', 'one', 'i'];
|
|
|
- var width;
|
|
|
- for (var i = 0, ii = possibleSpaceReplacements.length; i < ii; i++) {
|
|
|
- var glyphName = possibleSpaceReplacements[i];
|
|
|
- // if possible, getting width by glyph name
|
|
|
- if (glyphName in this.widths) {
|
|
|
- width = this.widths[glyphName];
|
|
|
- break;
|
|
|
- }
|
|
|
- var glyphsUnicodeMap = getGlyphsUnicode();
|
|
|
- var glyphUnicode = glyphsUnicodeMap[glyphName];
|
|
|
- // finding the charcode via unicodeToCID map
|
|
|
- var charcode = 0;
|
|
|
- if (this.composite) {
|
|
|
- if (this.cMap.contains(glyphUnicode)) {
|
|
|
- charcode = this.cMap.lookup(glyphUnicode);
|
|
|
- }
|
|
|
- }
|
|
|
- // ... via toUnicode map
|
|
|
- if (!charcode && this.toUnicode) {
|
|
|
- charcode = this.toUnicode.charCodeOf(glyphUnicode);
|
|
|
- }
|
|
|
- // setting it to unicode if negative or undefined
|
|
|
- if (charcode <= 0) {
|
|
|
- charcode = glyphUnicode;
|
|
|
+ var subrs = [], charstrings = [];
|
|
|
+ var privateData = Object.create(null);
|
|
|
+ privateData['lenIV'] = 4;
|
|
|
+ var program = {
|
|
|
+ subrs: [],
|
|
|
+ charstrings: [],
|
|
|
+ properties: {
|
|
|
+ 'privateData': privateData
|
|
|
}
|
|
|
- // trying to get width via charcode
|
|
|
- width = this.widths[charcode];
|
|
|
- if (width) {
|
|
|
- break; // the non-zero width found
|
|
|
+ };
|
|
|
+ var token, length, data, lenIV, encoded;
|
|
|
+ while ((token = this.getToken()) !== null) {
|
|
|
+ if (token !== '/') {
|
|
|
+ continue;
|
|
|
}
|
|
|
- }
|
|
|
- width = width || this.defaultWidth;
|
|
|
- // Do not shadow the property here. See discussion:
|
|
|
- // https://github.com/mozilla/pdf.js/pull/2127#discussion_r1662280
|
|
|
- this._shadowWidth = width;
|
|
|
- return width;
|
|
|
- },
|
|
|
-
|
|
|
- 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);
|
|
|
- }
|
|
|
- width = this.widths[widthCode];
|
|
|
- width = isNum(width) ? width : this.defaultWidth;
|
|
|
- var vmetric = this.vmetrics && this.vmetrics[widthCode];
|
|
|
-
|
|
|
- var unicode = this.toUnicode.get(charcode) || charcode;
|
|
|
- if (typeof unicode === 'number') {
|
|
|
- unicode = String.fromCharCode(unicode);
|
|
|
- }
|
|
|
-
|
|
|
- // First try the toFontChar map, if it's not there then try falling
|
|
|
- // back to the char code.
|
|
|
- fontCharCode = this.toFontChar[charcode] || charcode;
|
|
|
- if (this.missingFile) {
|
|
|
- fontCharCode = mapSpecialUnicodeValues(fontCharCode);
|
|
|
- }
|
|
|
+ token = this.getToken();
|
|
|
+ switch (token) {
|
|
|
+ case 'CharStrings':
|
|
|
+ // The number immediately following CharStrings must be greater or
|
|
|
+ // equal to the number of CharStrings.
|
|
|
+ this.getToken();
|
|
|
+ this.getToken(); // read in 'dict'
|
|
|
+ this.getToken(); // read in 'dup'
|
|
|
+ this.getToken(); // read in 'begin'
|
|
|
+ while(true) {
|
|
|
+ token = this.getToken();
|
|
|
+ if (token === null || token === 'end') {
|
|
|
+ break;
|
|
|
+ }
|
|
|
|
|
|
- if (this.isType3Font) {
|
|
|
- // Font char code in this case is actually a glyph name.
|
|
|
- operatorListId = fontCharCode;
|
|
|
+ if (token !== '/') {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ var glyph = this.getToken();
|
|
|
+ length = this.readInt();
|
|
|
+ this.getToken(); // read in 'RD' or '-|'
|
|
|
+ data = stream.makeSubStream(stream.pos, length);
|
|
|
+ lenIV = program.properties.privateData['lenIV'];
|
|
|
+ encoded = decrypt(data.getBytes(), CHAR_STRS_ENCRYPT_KEY, lenIV);
|
|
|
+ // Skip past the required space and binary data.
|
|
|
+ stream.skip(length);
|
|
|
+ this.nextChar();
|
|
|
+ token = this.getToken(); // read in 'ND' or '|-'
|
|
|
+ if (token === 'noaccess') {
|
|
|
+ this.getToken(); // read in 'def'
|
|
|
+ }
|
|
|
+ charstrings.push({
|
|
|
+ glyph: glyph,
|
|
|
+ encoded: encoded
|
|
|
+ });
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 'Subrs':
|
|
|
+ var num = this.readInt();
|
|
|
+ this.getToken(); // read in 'array'
|
|
|
+ while ((token = this.getToken()) === 'dup') {
|
|
|
+ var index = this.readInt();
|
|
|
+ length = this.readInt();
|
|
|
+ this.getToken(); // read in 'RD' or '-|'
|
|
|
+ data = stream.makeSubStream(stream.pos, length);
|
|
|
+ lenIV = program.properties.privateData['lenIV'];
|
|
|
+ encoded = decrypt(data.getBytes(), CHAR_STRS_ENCRYPT_KEY, lenIV);
|
|
|
+ // Skip past the required space and binary data.
|
|
|
+ stream.skip(length);
|
|
|
+ this.nextChar();
|
|
|
+ token = this.getToken(); // read in 'NP' or '|'
|
|
|
+ if (token === 'noaccess') {
|
|
|
+ this.getToken(); // read in 'put'
|
|
|
+ }
|
|
|
+ subrs[index] = encoded;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 'BlueValues':
|
|
|
+ case 'OtherBlues':
|
|
|
+ case 'FamilyBlues':
|
|
|
+ case 'FamilyOtherBlues':
|
|
|
+ var blueArray = this.readNumberArray();
|
|
|
+ // *Blue* values may contain invalid data: disables reading of
|
|
|
+ // those values when hinting is disabled.
|
|
|
+ if (blueArray.length > 0 && (blueArray.length % 2) === 0 &&
|
|
|
+ HINTING_ENABLED) {
|
|
|
+ program.properties.privateData[token] = blueArray;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 'StemSnapH':
|
|
|
+ case 'StemSnapV':
|
|
|
+ program.properties.privateData[token] = this.readNumberArray();
|
|
|
+ break;
|
|
|
+ case 'StdHW':
|
|
|
+ case 'StdVW':
|
|
|
+ program.properties.privateData[token] =
|
|
|
+ this.readNumberArray()[0];
|
|
|
+ break;
|
|
|
+ case 'BlueShift':
|
|
|
+ case 'lenIV':
|
|
|
+ case 'BlueFuzz':
|
|
|
+ case 'BlueScale':
|
|
|
+ case 'LanguageGroup':
|
|
|
+ case 'ExpansionFactor':
|
|
|
+ program.properties.privateData[token] = this.readNumber();
|
|
|
+ break;
|
|
|
+ case 'ForceBold':
|
|
|
+ program.properties.privateData[token] = this.readBoolean();
|
|
|
+ break;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- var accent = null;
|
|
|
- if (this.seacMap && this.seacMap[charcode]) {
|
|
|
- var seac = this.seacMap[charcode];
|
|
|
- fontCharCode = seac.baseFontCharCode;
|
|
|
- accent = {
|
|
|
- fontChar: String.fromCharCode(seac.accentFontCharCode),
|
|
|
- offset: seac.accentOffset
|
|
|
- };
|
|
|
+ for (var i = 0; i < charstrings.length; i++) {
|
|
|
+ glyph = charstrings[i].glyph;
|
|
|
+ encoded = charstrings[i].encoded;
|
|
|
+ var charString = new Type1CharString();
|
|
|
+ var error = charString.convert(encoded, subrs);
|
|
|
+ var output = charString.output;
|
|
|
+ if (error) {
|
|
|
+ // It seems when FreeType encounters an error while evaluating a glyph
|
|
|
+ // that it completely ignores the glyph so we'll mimic that behaviour
|
|
|
+ // here and put an endchar to make the validator happy.
|
|
|
+ output = [14];
|
|
|
+ }
|
|
|
+ program.charstrings.push({
|
|
|
+ glyphName: glyph,
|
|
|
+ charstring: output,
|
|
|
+ width: charString.width,
|
|
|
+ lsb: charString.lsb,
|
|
|
+ seac: charString.seac
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- var fontChar = String.fromCharCode(fontCharCode);
|
|
|
-
|
|
|
- var glyph = this.glyphCache[charcode];
|
|
|
- if (!glyph ||
|
|
|
- !glyph.matchesForCache(fontChar, unicode, accent, width, vmetric,
|
|
|
- operatorListId, isSpace)) {
|
|
|
- glyph = new Glyph(fontChar, unicode, accent, width, vmetric,
|
|
|
- operatorListId, isSpace);
|
|
|
- this.glyphCache[charcode] = glyph;
|
|
|
- }
|
|
|
- return glyph;
|
|
|
+ return program;
|
|
|
},
|
|
|
|
|
|
- charsToGlyphs: function Font_charsToGlyphs(chars) {
|
|
|
- var charsCache = this.charsCache;
|
|
|
- var glyphs, glyph, charcode;
|
|
|
-
|
|
|
- // if we translated this string before, just grab it from the cache
|
|
|
- if (charsCache) {
|
|
|
- glyphs = charsCache[chars];
|
|
|
- if (glyphs) {
|
|
|
- return glyphs;
|
|
|
+ extractFontHeader: function Type1Parser_extractFontHeader(properties) {
|
|
|
+ var token;
|
|
|
+ while ((token = this.getToken()) !== null) {
|
|
|
+ if (token !== '/') {
|
|
|
+ continue;
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- // lazily create the translation cache
|
|
|
- if (!charsCache) {
|
|
|
- charsCache = this.charsCache = Object.create(null);
|
|
|
- }
|
|
|
-
|
|
|
- glyphs = [];
|
|
|
- var charsCacheKey = chars;
|
|
|
- var i = 0, ii;
|
|
|
+ token = this.getToken();
|
|
|
+ switch (token) {
|
|
|
+ case 'FontMatrix':
|
|
|
+ var matrix = this.readNumberArray();
|
|
|
+ properties.fontMatrix = matrix;
|
|
|
+ break;
|
|
|
+ case 'Encoding':
|
|
|
+ var encodingArg = this.getToken();
|
|
|
+ var encoding;
|
|
|
+ if (!/^\d+$/.test(encodingArg)) {
|
|
|
+ // encoding name is specified
|
|
|
+ encoding = getEncoding(encodingArg);
|
|
|
+ } else {
|
|
|
+ encoding = [];
|
|
|
+ var size = parseInt(encodingArg, 10) | 0;
|
|
|
+ this.getToken(); // read in 'array'
|
|
|
|
|
|
- if (this.cMap) {
|
|
|
- // composite fonts have multi-byte strings convert the string from
|
|
|
- // single-byte to multi-byte
|
|
|
- var c = Object.create(null);
|
|
|
- while (i < chars.length) {
|
|
|
- this.cMap.readCharCode(chars, i, c);
|
|
|
- charcode = c.charcode;
|
|
|
- var length = c.length;
|
|
|
- i += length;
|
|
|
- // 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);
|
|
|
- }
|
|
|
- } else {
|
|
|
- for (i = 0, ii = chars.length; i < ii; ++i) {
|
|
|
- charcode = chars.charCodeAt(i);
|
|
|
- glyph = this.charToGlyph(charcode, charcode === 0x20);
|
|
|
- glyphs.push(glyph);
|
|
|
+ for (var j = 0; j < size; j++) {
|
|
|
+ token = this.getToken();
|
|
|
+ // skipping till first dup or def (e.g. ignoring for statement)
|
|
|
+ while (token !== 'dup' && token !== 'def') {
|
|
|
+ token = this.getToken();
|
|
|
+ if (token === null) {
|
|
|
+ return; // invalid header
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (token === 'def') {
|
|
|
+ break; // read all array data
|
|
|
+ }
|
|
|
+ var index = this.readInt();
|
|
|
+ this.getToken(); // read in '/'
|
|
|
+ var glyph = this.getToken();
|
|
|
+ encoding[index] = glyph;
|
|
|
+ this.getToken(); // read the in 'put'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ properties.builtInEncoding = encoding;
|
|
|
+ break;
|
|
|
+ case 'FontBBox':
|
|
|
+ var fontBBox = this.readNumberArray();
|
|
|
+ // adjusting ascent/descent
|
|
|
+ properties.ascent = fontBBox[3];
|
|
|
+ properties.descent = fontBBox[1];
|
|
|
+ properties.ascentScaled = true;
|
|
|
+ break;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- // Enter the translated string into the cache
|
|
|
- return (charsCache[charsCacheKey] = glyphs);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- return Font;
|
|
|
+ return Type1Parser;
|
|
|
})();
|
|
|
|
|
|
-var ErrorFont = (function ErrorFontClosure() {
|
|
|
- function ErrorFont(error) {
|
|
|
- this.error = error;
|
|
|
- this.loadedName = 'g_font_error';
|
|
|
- this.loading = false;
|
|
|
+/**
|
|
|
+ * The CFF class takes a Type1 file and wrap it into a
|
|
|
+ * 'Compact Font Format' which itself embed Type2 charstrings.
|
|
|
+ */
|
|
|
+var CFFStandardStrings = [
|
|
|
+ '.notdef', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent',
|
|
|
+ 'ampersand', 'quoteright', 'parenleft', 'parenright', 'asterisk', 'plus',
|
|
|
+ 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four',
|
|
|
+ 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less',
|
|
|
+ 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
|
|
|
+ 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
|
|
|
+ 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', 'asciicircum',
|
|
|
+ 'underscore', 'quoteleft', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
|
|
|
+ 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y',
|
|
|
+ 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', 'exclamdown', 'cent',
|
|
|
+ 'sterling', 'fraction', 'yen', 'florin', 'section', 'currency',
|
|
|
+ 'quotesingle', 'quotedblleft', 'guillemotleft', 'guilsinglleft',
|
|
|
+ 'guilsinglright', 'fi', 'fl', 'endash', 'dagger', 'daggerdbl',
|
|
|
+ 'periodcentered', 'paragraph', 'bullet', 'quotesinglbase', 'quotedblbase',
|
|
|
+ 'quotedblright', 'guillemotright', 'ellipsis', 'perthousand', 'questiondown',
|
|
|
+ 'grave', 'acute', 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent',
|
|
|
+ 'dieresis', 'ring', 'cedilla', 'hungarumlaut', 'ogonek', 'caron', 'emdash',
|
|
|
+ 'AE', 'ordfeminine', 'Lslash', 'Oslash', 'OE', 'ordmasculine', 'ae',
|
|
|
+ 'dotlessi', 'lslash', 'oslash', 'oe', 'germandbls', 'onesuperior',
|
|
|
+ 'logicalnot', 'mu', 'trademark', 'Eth', 'onehalf', 'plusminus', 'Thorn',
|
|
|
+ 'onequarter', 'divide', 'brokenbar', 'degree', 'thorn', 'threequarters',
|
|
|
+ 'twosuperior', 'registered', 'minus', 'eth', 'multiply', 'threesuperior',
|
|
|
+ 'copyright', 'Aacute', 'Acircumflex', 'Adieresis', 'Agrave', 'Aring',
|
|
|
+ 'Atilde', 'Ccedilla', 'Eacute', 'Ecircumflex', 'Edieresis', 'Egrave',
|
|
|
+ 'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', 'Ntilde', 'Oacute',
|
|
|
+ 'Ocircumflex', 'Odieresis', 'Ograve', 'Otilde', 'Scaron', 'Uacute',
|
|
|
+ 'Ucircumflex', 'Udieresis', 'Ugrave', 'Yacute', 'Ydieresis', 'Zcaron',
|
|
|
+ 'aacute', 'acircumflex', 'adieresis', 'agrave', 'aring', 'atilde',
|
|
|
+ 'ccedilla', 'eacute', 'ecircumflex', 'edieresis', 'egrave', 'iacute',
|
|
|
+ 'icircumflex', 'idieresis', 'igrave', 'ntilde', 'oacute', 'ocircumflex',
|
|
|
+ 'odieresis', 'ograve', 'otilde', 'scaron', 'uacute', 'ucircumflex',
|
|
|
+ 'udieresis', 'ugrave', 'yacute', 'ydieresis', 'zcaron', 'exclamsmall',
|
|
|
+ 'Hungarumlautsmall', 'dollaroldstyle', 'dollarsuperior', 'ampersandsmall',
|
|
|
+ 'Acutesmall', 'parenleftsuperior', 'parenrightsuperior', 'twodotenleader',
|
|
|
+ 'onedotenleader', 'zerooldstyle', 'oneoldstyle', 'twooldstyle',
|
|
|
+ 'threeoldstyle', 'fouroldstyle', 'fiveoldstyle', 'sixoldstyle',
|
|
|
+ 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', 'commasuperior',
|
|
|
+ 'threequartersemdash', 'periodsuperior', 'questionsmall', 'asuperior',
|
|
|
+ 'bsuperior', 'centsuperior', 'dsuperior', 'esuperior', 'isuperior',
|
|
|
+ 'lsuperior', 'msuperior', 'nsuperior', 'osuperior', 'rsuperior', 'ssuperior',
|
|
|
+ 'tsuperior', 'ff', 'ffi', 'ffl', 'parenleftinferior', 'parenrightinferior',
|
|
|
+ 'Circumflexsmall', 'hyphensuperior', 'Gravesmall', 'Asmall', 'Bsmall',
|
|
|
+ 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', 'Ismall',
|
|
|
+ 'Jsmall', 'Ksmall', 'Lsmall', 'Msmall', 'Nsmall', 'Osmall', 'Psmall',
|
|
|
+ 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall',
|
|
|
+ 'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah',
|
|
|
+ 'Tildesmall', 'exclamdownsmall', 'centoldstyle', 'Lslashsmall',
|
|
|
+ 'Scaronsmall', 'Zcaronsmall', 'Dieresissmall', 'Brevesmall', 'Caronsmall',
|
|
|
+ 'Dotaccentsmall', 'Macronsmall', 'figuredash', 'hypheninferior',
|
|
|
+ 'Ogoneksmall', 'Ringsmall', 'Cedillasmall', 'questiondownsmall', 'oneeighth',
|
|
|
+ 'threeeighths', 'fiveeighths', 'seveneighths', 'onethird', 'twothirds',
|
|
|
+ 'zerosuperior', 'foursuperior', 'fivesuperior', 'sixsuperior',
|
|
|
+ 'sevensuperior', 'eightsuperior', 'ninesuperior', 'zeroinferior',
|
|
|
+ 'oneinferior', 'twoinferior', 'threeinferior', 'fourinferior',
|
|
|
+ 'fiveinferior', 'sixinferior', 'seveninferior', 'eightinferior',
|
|
|
+ 'nineinferior', 'centinferior', 'dollarinferior', 'periodinferior',
|
|
|
+ 'commainferior', 'Agravesmall', 'Aacutesmall', 'Acircumflexsmall',
|
|
|
+ 'Atildesmall', 'Adieresissmall', 'Aringsmall', 'AEsmall', 'Ccedillasmall',
|
|
|
+ 'Egravesmall', 'Eacutesmall', 'Ecircumflexsmall', 'Edieresissmall',
|
|
|
+ 'Igravesmall', 'Iacutesmall', 'Icircumflexsmall', 'Idieresissmall',
|
|
|
+ 'Ethsmall', 'Ntildesmall', 'Ogravesmall', 'Oacutesmall', 'Ocircumflexsmall',
|
|
|
+ 'Otildesmall', 'Odieresissmall', 'OEsmall', 'Oslashsmall', 'Ugravesmall',
|
|
|
+ 'Uacutesmall', 'Ucircumflexsmall', 'Udieresissmall', 'Yacutesmall',
|
|
|
+ 'Thornsmall', 'Ydieresissmall', '001.000', '001.001', '001.002', '001.003',
|
|
|
+ 'Black', 'Bold', 'Book', 'Light', 'Medium', 'Regular', 'Roman', 'Semibold'
|
|
|
+];
|
|
|
+
|
|
|
+// Type1Font is also a CIDFontType0.
|
|
|
+var Type1Font = function Type1Font(name, file, properties) {
|
|
|
+ // Some bad generators embed pfb file as is, we have to strip 6-byte headers.
|
|
|
+ // Also, length1 and length2 might be off by 6 bytes as well.
|
|
|
+ // http://www.math.ubc.ca/~cass/piscript/type1.pdf
|
|
|
+ var PFB_HEADER_SIZE = 6;
|
|
|
+ var headerBlockLength = properties.length1;
|
|
|
+ var eexecBlockLength = properties.length2;
|
|
|
+ var pfbHeader = file.peekBytes(PFB_HEADER_SIZE);
|
|
|
+ var pfbHeaderPresent = pfbHeader[0] === 0x80 && pfbHeader[1] === 0x01;
|
|
|
+ if (pfbHeaderPresent) {
|
|
|
+ file.skip(PFB_HEADER_SIZE);
|
|
|
+ headerBlockLength = (pfbHeader[5] << 24) | (pfbHeader[4] << 16) |
|
|
|
+ (pfbHeader[3] << 8) | pfbHeader[2];
|
|
|
}
|
|
|
|
|
|
- ErrorFont.prototype = {
|
|
|
- charsToGlyphs: function ErrorFont_charsToGlyphs() {
|
|
|
- return [];
|
|
|
- },
|
|
|
- exportData: function ErrorFont_exportData() {
|
|
|
- return {error: this.error};
|
|
|
- }
|
|
|
- };
|
|
|
+ // Get the data block containing glyphs and subrs informations
|
|
|
+ var headerBlock = new Stream(file.getBytes(headerBlockLength));
|
|
|
+ var headerBlockParser = new Type1Parser(headerBlock);
|
|
|
+ headerBlockParser.extractFontHeader(properties);
|
|
|
|
|
|
- return ErrorFont;
|
|
|
-})();
|
|
|
+ if (pfbHeaderPresent) {
|
|
|
+ pfbHeader = file.getBytes(PFB_HEADER_SIZE);
|
|
|
+ eexecBlockLength = (pfbHeader[5] << 24) | (pfbHeader[4] << 16) |
|
|
|
+ (pfbHeader[3] << 8) | pfbHeader[2];
|
|
|
+ }
|
|
|
|
|
|
-/**
|
|
|
- * Shared logic for building a char code to glyph id mapping for Type1 and
|
|
|
- * simple CFF fonts. See section 9.6.6.2 of the spec.
|
|
|
- * @param {Object} properties Font properties object.
|
|
|
- * @param {Object} builtInEncoding The encoding contained within the actual font
|
|
|
- * data.
|
|
|
- * @param {Array} Array of glyph names where the index is the glyph ID.
|
|
|
- * @returns {Object} A char code to glyph ID map.
|
|
|
- */
|
|
|
-function type1FontGlyphMapping(properties, builtInEncoding, glyphNames) {
|
|
|
- var charCodeToGlyphId = Object.create(null);
|
|
|
- var glyphId, charCode, baseEncoding;
|
|
|
+ // Decrypt the data blocks and retrieve it's content
|
|
|
+ var eexecBlock = new Stream(file.getBytes(eexecBlockLength));
|
|
|
+ var eexecBlockParser = new Type1Parser(eexecBlock, true);
|
|
|
+ var data = eexecBlockParser.extractFontProgram();
|
|
|
+ for (var info in data.properties) {
|
|
|
+ properties[info] = data.properties[info];
|
|
|
+ }
|
|
|
|
|
|
- if (properties.baseEncodingName) {
|
|
|
- // If a valid base encoding name was used, the mapping is initialized with
|
|
|
- // that.
|
|
|
- baseEncoding = getEncoding(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
|
|
|
- }
|
|
|
+ var charstrings = data.charstrings;
|
|
|
+ var type2Charstrings = this.getType2Charstrings(charstrings);
|
|
|
+ var subrs = this.getType2Subrs(data.subrs);
|
|
|
+
|
|
|
+ this.charstrings = charstrings;
|
|
|
+ this.data = this.wrap(name, type2Charstrings, this.charstrings,
|
|
|
+ subrs, properties);
|
|
|
+ this.seacs = this.getSeacs(data.charstrings);
|
|
|
+};
|
|
|
+
|
|
|
+Type1Font.prototype = {
|
|
|
+ get numGlyphs() {
|
|
|
+ return this.charstrings.length + 1;
|
|
|
+ },
|
|
|
+
|
|
|
+ getCharset: function Type1Font_getCharset() {
|
|
|
+ var charset = ['.notdef'];
|
|
|
+ var charstrings = this.charstrings;
|
|
|
+ for (var glyphId = 0; glyphId < charstrings.length; glyphId++) {
|
|
|
+ charset.push(charstrings[glyphId].glyphName);
|
|
|
}
|
|
|
- } else if (!!(properties.flags & FontFlags.Symbolic)) {
|
|
|
- // For a symbolic font the encoding should be the fonts built-in
|
|
|
- // encoding.
|
|
|
- for (charCode in builtInEncoding) {
|
|
|
- charCodeToGlyphId[charCode] = builtInEncoding[charCode];
|
|
|
+ return charset;
|
|
|
+ },
|
|
|
+
|
|
|
+ getGlyphMapping: function Type1Font_getGlyphMapping(properties) {
|
|
|
+ var charstrings = this.charstrings;
|
|
|
+ var glyphNames = ['.notdef'], glyphId;
|
|
|
+ for (glyphId = 0; glyphId < charstrings.length; glyphId++) {
|
|
|
+ glyphNames.push(charstrings[glyphId].glyphName);
|
|
|
}
|
|
|
- } else {
|
|
|
- // For non-symbolic fonts that don't have a base encoding the standard
|
|
|
- // encoding should be used.
|
|
|
- baseEncoding = StandardEncoding;
|
|
|
- for (charCode = 0; charCode < baseEncoding.length; charCode++) {
|
|
|
- glyphId = glyphNames.indexOf(baseEncoding[charCode]);
|
|
|
- if (glyphId >= 0) {
|
|
|
- charCodeToGlyphId[charCode] = glyphId;
|
|
|
- } else {
|
|
|
- charCodeToGlyphId[charCode] = 0; // notdef
|
|
|
+ var encoding = properties.builtInEncoding;
|
|
|
+ if (encoding) {
|
|
|
+ var builtInEncoding = Object.create(null);
|
|
|
+ for (var charCode in encoding) {
|
|
|
+ glyphId = glyphNames.indexOf(encoding[charCode]);
|
|
|
+ if (glyphId >= 0) {
|
|
|
+ builtInEncoding[charCode] = glyphId;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- // Lastly, merge in the differences.
|
|
|
- var differences = properties.differences;
|
|
|
- if (differences) {
|
|
|
- for (charCode in differences) {
|
|
|
- var glyphName = differences[charCode];
|
|
|
- glyphId = glyphNames.indexOf(glyphName);
|
|
|
- if (glyphId >= 0) {
|
|
|
- charCodeToGlyphId[charCode] = glyphId;
|
|
|
- } else {
|
|
|
- charCodeToGlyphId[charCode] = 0; // notdef
|
|
|
+ return type1FontGlyphMapping(properties, builtInEncoding, glyphNames);
|
|
|
+ },
|
|
|
+
|
|
|
+ getSeacs: function Type1Font_getSeacs(charstrings) {
|
|
|
+ var i, ii;
|
|
|
+ var seacMap = [];
|
|
|
+ for (i = 0, ii = charstrings.length; i < ii; i++) {
|
|
|
+ var charstring = charstrings[i];
|
|
|
+ if (charstring.seac) {
|
|
|
+ // Offset by 1 for .notdef
|
|
|
+ seacMap[i + 1] = charstring.seac;
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
- return charCodeToGlyphId;
|
|
|
-}
|
|
|
+ return seacMap;
|
|
|
+ },
|
|
|
|
|
|
-/*
|
|
|
- * CharStrings are encoded following the the CharString Encoding sequence
|
|
|
- * describe in Chapter 6 of the "Adobe Type1 Font Format" specification.
|
|
|
- * The value in a byte indicates a command, a number, or subsequent bytes
|
|
|
- * that are to be interpreted in a special way.
|
|
|
- *
|
|
|
- * CharString Number Encoding:
|
|
|
- * A CharString byte containing the values from 32 through 255 inclusive
|
|
|
- * indicate an integer. These values are decoded in four ranges.
|
|
|
- *
|
|
|
- * 1. A CharString byte containing a value, v, between 32 and 246 inclusive,
|
|
|
- * indicate the integer v - 139. Thus, the integer values from -107 through
|
|
|
- * 107 inclusive may be encoded in single byte.
|
|
|
- *
|
|
|
- * 2. A CharString byte containing a value, v, between 247 and 250 inclusive,
|
|
|
- * indicates an integer involving the next byte, w, according to the formula:
|
|
|
- * [(v - 247) x 256] + w + 108
|
|
|
- *
|
|
|
- * 3. A CharString byte containing a value, v, between 251 and 254 inclusive,
|
|
|
- * indicates an integer involving the next byte, w, according to the formula:
|
|
|
- * -[(v - 251) * 256] - w - 108
|
|
|
- *
|
|
|
- * 4. A CharString containing the value 255 indicates that the next 4 bytes
|
|
|
- * are a two complement signed integer. The first of these bytes contains the
|
|
|
- * highest order bits, the second byte contains the next higher order bits
|
|
|
- * and the fourth byte contain the lowest order bits.
|
|
|
- *
|
|
|
- *
|
|
|
- * CharString Command Encoding:
|
|
|
- * CharStrings commands are encoded in 1 or 2 bytes.
|
|
|
- *
|
|
|
- * Single byte commands are encoded in 1 byte that contains a value between
|
|
|
- * 0 and 31 inclusive.
|
|
|
- * If a command byte contains the value 12, then the value in the next byte
|
|
|
- * indicates a command. This "escape" mechanism allows many extra commands
|
|
|
- * to be encoded and this encoding technique helps to minimize the length of
|
|
|
- * the charStrings.
|
|
|
- */
|
|
|
-var Type1CharString = (function Type1CharStringClosure() {
|
|
|
- var COMMAND_MAP = {
|
|
|
- 'hstem': [1],
|
|
|
- 'vstem': [3],
|
|
|
- 'vmoveto': [4],
|
|
|
- 'rlineto': [5],
|
|
|
- 'hlineto': [6],
|
|
|
- 'vlineto': [7],
|
|
|
- 'rrcurveto': [8],
|
|
|
- 'callsubr': [10],
|
|
|
- 'flex': [12, 35],
|
|
|
- 'drop' : [12, 18],
|
|
|
- 'endchar': [14],
|
|
|
- 'rmoveto': [21],
|
|
|
- 'hmoveto': [22],
|
|
|
- 'vhcurveto': [30],
|
|
|
- 'hvcurveto': [31]
|
|
|
- };
|
|
|
+ getType2Charstrings: function Type1Font_getType2Charstrings(
|
|
|
+ type1Charstrings) {
|
|
|
+ var type2Charstrings = [];
|
|
|
+ for (var i = 0, ii = type1Charstrings.length; i < ii; i++) {
|
|
|
+ type2Charstrings.push(type1Charstrings[i].charstring);
|
|
|
+ }
|
|
|
+ return type2Charstrings;
|
|
|
+ },
|
|
|
|
|
|
- function Type1CharString() {
|
|
|
- this.width = 0;
|
|
|
- this.lsb = 0;
|
|
|
- this.flexing = false;
|
|
|
- this.output = [];
|
|
|
- this.stack = [];
|
|
|
- }
|
|
|
+ getType2Subrs: function Type1Font_getType2Subrs(type1Subrs) {
|
|
|
+ var bias = 0;
|
|
|
+ var count = type1Subrs.length;
|
|
|
+ if (count < 1133) {
|
|
|
+ bias = 107;
|
|
|
+ } else if (count < 33769) {
|
|
|
+ bias = 1131;
|
|
|
+ } else {
|
|
|
+ bias = 32768;
|
|
|
+ }
|
|
|
|
|
|
- Type1CharString.prototype = {
|
|
|
- convert: function Type1CharString_convert(encoded, subrs) {
|
|
|
- var count = encoded.length;
|
|
|
- var error = false;
|
|
|
- var wx, sbx, subrNumber;
|
|
|
- for (var i = 0; i < count; i++) {
|
|
|
- var value = encoded[i];
|
|
|
- if (value < 32) {
|
|
|
- if (value === 12) {
|
|
|
- value = (value << 8) + encoded[++i];
|
|
|
- }
|
|
|
- switch (value) {
|
|
|
- case 1: // hstem
|
|
|
- if (!HINTING_ENABLED) {
|
|
|
- this.stack = [];
|
|
|
- break;
|
|
|
- }
|
|
|
- error = this.executeCommand(2, COMMAND_MAP.hstem);
|
|
|
- break;
|
|
|
- case 3: // vstem
|
|
|
- if (!HINTING_ENABLED) {
|
|
|
- this.stack = [];
|
|
|
- break;
|
|
|
- }
|
|
|
- error = this.executeCommand(2, COMMAND_MAP.vstem);
|
|
|
- break;
|
|
|
- case 4: // vmoveto
|
|
|
- if (this.flexing) {
|
|
|
- if (this.stack.length < 1) {
|
|
|
- error = true;
|
|
|
- break;
|
|
|
- }
|
|
|
- // Add the dx for flex and but also swap the values so they are
|
|
|
- // the right order.
|
|
|
- var dy = this.stack.pop();
|
|
|
- this.stack.push(0, dy);
|
|
|
- break;
|
|
|
- }
|
|
|
- error = this.executeCommand(1, COMMAND_MAP.vmoveto);
|
|
|
- break;
|
|
|
- case 5: // rlineto
|
|
|
- error = this.executeCommand(2, COMMAND_MAP.rlineto);
|
|
|
- break;
|
|
|
- case 6: // hlineto
|
|
|
- error = this.executeCommand(1, COMMAND_MAP.hlineto);
|
|
|
- break;
|
|
|
- case 7: // vlineto
|
|
|
- error = this.executeCommand(1, COMMAND_MAP.vlineto);
|
|
|
- break;
|
|
|
- case 8: // rrcurveto
|
|
|
- error = this.executeCommand(6, COMMAND_MAP.rrcurveto);
|
|
|
- break;
|
|
|
- case 9: // closepath
|
|
|
- // closepath is a Type1 command that does not take argument and is
|
|
|
- // useless in Type2 and it can simply be ignored.
|
|
|
- this.stack = [];
|
|
|
- break;
|
|
|
- case 10: // callsubr
|
|
|
- if (this.stack.length < 1) {
|
|
|
- error = true;
|
|
|
- break;
|
|
|
- }
|
|
|
- subrNumber = this.stack.pop();
|
|
|
- error = this.convert(subrs[subrNumber], subrs);
|
|
|
- break;
|
|
|
- case 11: // return
|
|
|
- return error;
|
|
|
- case 13: // hsbw
|
|
|
- if (this.stack.length < 2) {
|
|
|
- error = true;
|
|
|
- break;
|
|
|
- }
|
|
|
- // To convert to type2 we have to move the width value to the
|
|
|
- // first part of the charstring and then use hmoveto with lsb.
|
|
|
- wx = this.stack.pop();
|
|
|
- sbx = this.stack.pop();
|
|
|
- this.lsb = sbx;
|
|
|
- this.width = wx;
|
|
|
- this.stack.push(wx, sbx);
|
|
|
- error = this.executeCommand(2, COMMAND_MAP.hmoveto);
|
|
|
- break;
|
|
|
- case 14: // endchar
|
|
|
- this.output.push(COMMAND_MAP.endchar[0]);
|
|
|
- break;
|
|
|
- case 21: // rmoveto
|
|
|
- if (this.flexing) {
|
|
|
- break;
|
|
|
- }
|
|
|
- error = this.executeCommand(2, COMMAND_MAP.rmoveto);
|
|
|
- break;
|
|
|
- case 22: // hmoveto
|
|
|
- if (this.flexing) {
|
|
|
- // Add the dy for flex.
|
|
|
- this.stack.push(0);
|
|
|
- break;
|
|
|
- }
|
|
|
- error = this.executeCommand(1, COMMAND_MAP.hmoveto);
|
|
|
- break;
|
|
|
- case 30: // vhcurveto
|
|
|
- error = this.executeCommand(4, COMMAND_MAP.vhcurveto);
|
|
|
- break;
|
|
|
- case 31: // hvcurveto
|
|
|
- error = this.executeCommand(4, COMMAND_MAP.hvcurveto);
|
|
|
- break;
|
|
|
- case (12 << 8) + 0: // dotsection
|
|
|
- // dotsection is a Type1 command to specify some hinting feature
|
|
|
- // for dots that do not take a parameter and it can safely be
|
|
|
- // ignored for Type2.
|
|
|
- this.stack = [];
|
|
|
- break;
|
|
|
- case (12 << 8) + 1: // vstem3
|
|
|
- if (!HINTING_ENABLED) {
|
|
|
- this.stack = [];
|
|
|
- break;
|
|
|
- }
|
|
|
- // [vh]stem3 are Type1 only and Type2 supports [vh]stem with
|
|
|
- // multiple parameters, so instead of returning [vh]stem3 take a
|
|
|
- // shortcut and return [vhstem] instead.
|
|
|
- error = this.executeCommand(2, COMMAND_MAP.vstem);
|
|
|
- break;
|
|
|
- case (12 << 8) + 2: // hstem3
|
|
|
- if (!HINTING_ENABLED) {
|
|
|
- this.stack = [];
|
|
|
- break;
|
|
|
- }
|
|
|
- // See vstem3.
|
|
|
- error = this.executeCommand(2, COMMAND_MAP.hstem);
|
|
|
- break;
|
|
|
- case (12 << 8) + 6: // seac
|
|
|
- // seac is like type 2's special endchar but it doesn't use the
|
|
|
- // first argument asb, so remove it.
|
|
|
- if (SEAC_ANALYSIS_ENABLED) {
|
|
|
- this.seac = this.stack.splice(-4, 4);
|
|
|
- error = this.executeCommand(0, COMMAND_MAP.endchar);
|
|
|
- } else {
|
|
|
- error = this.executeCommand(4, COMMAND_MAP.endchar);
|
|
|
- }
|
|
|
- break;
|
|
|
- case (12 << 8) + 7: // sbw
|
|
|
- if (this.stack.length < 4) {
|
|
|
- error = true;
|
|
|
- break;
|
|
|
- }
|
|
|
- // To convert to type2 we have to move the width value to the
|
|
|
- // first part of the charstring and then use rmoveto with
|
|
|
- // (dx, dy). The height argument will not be used for vmtx and
|
|
|
- // vhea tables reconstruction -- ignoring it.
|
|
|
- var wy = this.stack.pop();
|
|
|
- wx = this.stack.pop();
|
|
|
- var sby = this.stack.pop();
|
|
|
- sbx = this.stack.pop();
|
|
|
- this.lsb = sbx;
|
|
|
- this.width = wx;
|
|
|
- this.stack.push(wx, sbx, sby);
|
|
|
- error = this.executeCommand(3, COMMAND_MAP.rmoveto);
|
|
|
- break;
|
|
|
- case (12 << 8) + 12: // div
|
|
|
- if (this.stack.length < 2) {
|
|
|
- error = true;
|
|
|
- break;
|
|
|
- }
|
|
|
- var num2 = this.stack.pop();
|
|
|
- var num1 = this.stack.pop();
|
|
|
- this.stack.push(num1 / num2);
|
|
|
- break;
|
|
|
- case (12 << 8) + 16: // callothersubr
|
|
|
- if (this.stack.length < 2) {
|
|
|
- error = true;
|
|
|
- break;
|
|
|
- }
|
|
|
- subrNumber = this.stack.pop();
|
|
|
- var numArgs = this.stack.pop();
|
|
|
- if (subrNumber === 0 && numArgs === 3) {
|
|
|
- var flexArgs = this.stack.splice(this.stack.length - 17, 17);
|
|
|
- this.stack.push(
|
|
|
- flexArgs[2] + flexArgs[0], // bcp1x + rpx
|
|
|
- flexArgs[3] + flexArgs[1], // bcp1y + rpy
|
|
|
- flexArgs[4], // bcp2x
|
|
|
- flexArgs[5], // bcp2y
|
|
|
- flexArgs[6], // p2x
|
|
|
- flexArgs[7], // p2y
|
|
|
- flexArgs[8], // bcp3x
|
|
|
- flexArgs[9], // bcp3y
|
|
|
- flexArgs[10], // bcp4x
|
|
|
- flexArgs[11], // bcp4y
|
|
|
- flexArgs[12], // p3x
|
|
|
- flexArgs[13], // p3y
|
|
|
- flexArgs[14] // flexDepth
|
|
|
- // 15 = finalx unused by flex
|
|
|
- // 16 = finaly unused by flex
|
|
|
- );
|
|
|
- error = this.executeCommand(13, COMMAND_MAP.flex, true);
|
|
|
- this.flexing = false;
|
|
|
- this.stack.push(flexArgs[15], flexArgs[16]);
|
|
|
- } else if (subrNumber === 1 && numArgs === 0) {
|
|
|
- this.flexing = true;
|
|
|
- }
|
|
|
- break;
|
|
|
- case (12 << 8) + 17: // pop
|
|
|
- // Ignore this since it is only used with othersubr.
|
|
|
- break;
|
|
|
- case (12 << 8) + 33: // setcurrentpoint
|
|
|
- // Ignore for now.
|
|
|
- this.stack = [];
|
|
|
- break;
|
|
|
- default:
|
|
|
- warn('Unknown type 1 charstring command of "' + value + '"');
|
|
|
- break;
|
|
|
- }
|
|
|
- if (error) {
|
|
|
- break;
|
|
|
- }
|
|
|
- continue;
|
|
|
- } else if (value <= 246) {
|
|
|
- value = value - 139;
|
|
|
- } else if (value <= 250) {
|
|
|
- value = ((value - 247) * 256) + encoded[++i] + 108;
|
|
|
- } else if (value <= 254) {
|
|
|
- value = -((value - 251) * 256) - encoded[++i] - 108;
|
|
|
- } else {
|
|
|
- value = (encoded[++i] & 0xff) << 24 | (encoded[++i] & 0xff) << 16 |
|
|
|
- (encoded[++i] & 0xff) << 8 | (encoded[++i] & 0xff) << 0;
|
|
|
- }
|
|
|
- this.stack.push(value);
|
|
|
- }
|
|
|
- return error;
|
|
|
- },
|
|
|
+ // Add a bunch of empty subrs to deal with the Type2 bias
|
|
|
+ var type2Subrs = [];
|
|
|
+ var i;
|
|
|
+ for (i = 0; i < bias; i++) {
|
|
|
+ type2Subrs.push([0x0B]);
|
|
|
+ }
|
|
|
|
|
|
- executeCommand: function(howManyArgs, command, keepStack) {
|
|
|
- var stackLength = this.stack.length;
|
|
|
- if (howManyArgs > stackLength) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- var start = stackLength - howManyArgs;
|
|
|
- for (var i = start; i < stackLength; i++) {
|
|
|
- var value = this.stack[i];
|
|
|
- if (value === (value | 0)) { // int
|
|
|
- this.output.push(28, (value >> 8) & 0xff, value & 0xff);
|
|
|
- } else { // fixed point
|
|
|
- value = (65536 * value) | 0;
|
|
|
- this.output.push(255,
|
|
|
- (value >> 24) & 0xFF,
|
|
|
- (value >> 16) & 0xFF,
|
|
|
- (value >> 8) & 0xFF,
|
|
|
- value & 0xFF);
|
|
|
- }
|
|
|
- }
|
|
|
- this.output.push.apply(this.output, command);
|
|
|
- if (keepStack) {
|
|
|
- this.stack.splice(start, howManyArgs);
|
|
|
- } else {
|
|
|
- this.stack.length = 0;
|
|
|
- }
|
|
|
- return false;
|
|
|
+ for (i = 0; i < count; i++) {
|
|
|
+ type2Subrs.push(type1Subrs[i]);
|
|
|
}
|
|
|
- };
|
|
|
|
|
|
- return Type1CharString;
|
|
|
-})();
|
|
|
+ return type2Subrs;
|
|
|
+ },
|
|
|
|
|
|
-/*
|
|
|
- * Type1Parser encapsulate the needed code for parsing a Type1 font
|
|
|
- * program. Some of its logic depends on the Type2 charstrings
|
|
|
- * structure.
|
|
|
- * Note: this doesn't really parse the font since that would require evaluation
|
|
|
- * of PostScript, but it is possible in most cases to extract what we need
|
|
|
- * without a full parse.
|
|
|
- */
|
|
|
-var Type1Parser = (function Type1ParserClosure() {
|
|
|
- /*
|
|
|
- * Decrypt a Sequence of Ciphertext Bytes to Produce the Original Sequence
|
|
|
- * of Plaintext Bytes. The function took a key as a parameter which can be
|
|
|
- * for decrypting the eexec block of for decoding charStrings.
|
|
|
- */
|
|
|
- var EEXEC_ENCRYPT_KEY = 55665;
|
|
|
- var CHAR_STRS_ENCRYPT_KEY = 4330;
|
|
|
+ wrap: function Type1Font_wrap(name, glyphs, charstrings, subrs, properties) {
|
|
|
+ var cff = new CFF();
|
|
|
+ cff.header = new CFFHeader(1, 0, 4, 4);
|
|
|
|
|
|
- function isHexDigit(code) {
|
|
|
- return code >= 48 && code <= 57 || // '0'-'9'
|
|
|
- code >= 65 && code <= 70 || // 'A'-'F'
|
|
|
- code >= 97 && code <= 102; // 'a'-'f'
|
|
|
- }
|
|
|
+ cff.names = [name];
|
|
|
|
|
|
- function decrypt(data, key, discardNumber) {
|
|
|
- if (discardNumber >= data.length) {
|
|
|
- return new Uint8Array(0);
|
|
|
- }
|
|
|
- var r = key | 0, c1 = 52845, c2 = 22719, i, j;
|
|
|
- for (i = 0; i < discardNumber; i++) {
|
|
|
- r = ((data[i] + r) * c1 + c2) & ((1 << 16) - 1);
|
|
|
+ var topDict = new CFFTopDict();
|
|
|
+ // CFF strings IDs 0...390 are predefined names, so refering
|
|
|
+ // to entries in our own String INDEX starts at SID 391.
|
|
|
+ topDict.setByName('version', 391);
|
|
|
+ topDict.setByName('Notice', 392);
|
|
|
+ topDict.setByName('FullName', 393);
|
|
|
+ topDict.setByName('FamilyName', 394);
|
|
|
+ topDict.setByName('Weight', 395);
|
|
|
+ topDict.setByName('Encoding', null); // placeholder
|
|
|
+ topDict.setByName('FontMatrix', properties.fontMatrix);
|
|
|
+ topDict.setByName('FontBBox', properties.bbox);
|
|
|
+ topDict.setByName('charset', null); // placeholder
|
|
|
+ topDict.setByName('CharStrings', null); // placeholder
|
|
|
+ topDict.setByName('Private', null); // placeholder
|
|
|
+ cff.topDict = topDict;
|
|
|
+
|
|
|
+ var strings = new CFFStrings();
|
|
|
+ strings.add('Version 0.11'); // Version
|
|
|
+ strings.add('See original notice'); // Notice
|
|
|
+ strings.add(name); // FullName
|
|
|
+ strings.add(name); // FamilyName
|
|
|
+ strings.add('Medium'); // Weight
|
|
|
+ cff.strings = strings;
|
|
|
+
|
|
|
+ cff.globalSubrIndex = new CFFIndex();
|
|
|
+
|
|
|
+ var count = glyphs.length;
|
|
|
+ var charsetArray = [0];
|
|
|
+ var i, ii;
|
|
|
+ for (i = 0; i < count; i++) {
|
|
|
+ var index = CFFStandardStrings.indexOf(charstrings[i].glyphName);
|
|
|
+ // TODO: Insert the string and correctly map it. Previously it was
|
|
|
+ // thought mapping names that aren't in the standard strings to .notdef
|
|
|
+ // was fine, however in issue818 when mapping them all to .notdef the
|
|
|
+ // adieresis glyph no longer worked.
|
|
|
+ if (index === -1) {
|
|
|
+ index = 0;
|
|
|
+ }
|
|
|
+ charsetArray.push((index >> 8) & 0xff, index & 0xff);
|
|
|
}
|
|
|
- var count = data.length - discardNumber;
|
|
|
- var decrypted = new Uint8Array(count);
|
|
|
- for (i = discardNumber, j = 0; j < count; i++, j++) {
|
|
|
- var value = data[i];
|
|
|
- decrypted[j] = value ^ (r >> 8);
|
|
|
- r = ((value + r) * c1 + c2) & ((1 << 16) - 1);
|
|
|
+ cff.charset = new CFFCharset(false, 0, [], charsetArray);
|
|
|
+
|
|
|
+ var charStringsIndex = new CFFIndex();
|
|
|
+ charStringsIndex.add([0x8B, 0x0E]); // .notdef
|
|
|
+ for (i = 0; i < count; i++) {
|
|
|
+ charStringsIndex.add(glyphs[i]);
|
|
|
}
|
|
|
- return decrypted;
|
|
|
- }
|
|
|
+ cff.charStrings = charStringsIndex;
|
|
|
|
|
|
- function decryptAscii(data, key, discardNumber) {
|
|
|
- var r = key | 0, c1 = 52845, c2 = 22719;
|
|
|
- var count = data.length, maybeLength = count >>> 1;
|
|
|
- var decrypted = new Uint8Array(maybeLength);
|
|
|
- var i, j;
|
|
|
- for (i = 0, j = 0; i < count; i++) {
|
|
|
- var digit1 = data[i];
|
|
|
- if (!isHexDigit(digit1)) {
|
|
|
+ var privateDict = new CFFPrivateDict();
|
|
|
+ privateDict.setByName('Subrs', null); // placeholder
|
|
|
+ var fields = [
|
|
|
+ 'BlueValues',
|
|
|
+ 'OtherBlues',
|
|
|
+ 'FamilyBlues',
|
|
|
+ 'FamilyOtherBlues',
|
|
|
+ 'StemSnapH',
|
|
|
+ 'StemSnapV',
|
|
|
+ 'BlueShift',
|
|
|
+ 'BlueFuzz',
|
|
|
+ 'BlueScale',
|
|
|
+ 'LanguageGroup',
|
|
|
+ 'ExpansionFactor',
|
|
|
+ 'ForceBold',
|
|
|
+ 'StdHW',
|
|
|
+ 'StdVW'
|
|
|
+ ];
|
|
|
+ for (i = 0, ii = fields.length; i < ii; i++) {
|
|
|
+ var field = fields[i];
|
|
|
+ if (!(field in properties.privateData)) {
|
|
|
continue;
|
|
|
}
|
|
|
- i++;
|
|
|
- var digit2;
|
|
|
- while (i < count && !isHexDigit(digit2 = data[i])) {
|
|
|
- i++;
|
|
|
- }
|
|
|
- if (i < count) {
|
|
|
- var value = parseInt(String.fromCharCode(digit1, digit2), 16);
|
|
|
- decrypted[j++] = value ^ (r >> 8);
|
|
|
- r = ((value + r) * c1 + c2) & ((1 << 16) - 1);
|
|
|
+ var value = properties.privateData[field];
|
|
|
+ if (isArray(value)) {
|
|
|
+ // All of the private dictionary array data in CFF must be stored as
|
|
|
+ // "delta-encoded" numbers.
|
|
|
+ for (var j = value.length - 1; j > 0; j--) {
|
|
|
+ value[j] -= value[j - 1]; // ... difference from previous value
|
|
|
+ }
|
|
|
}
|
|
|
+ privateDict.setByName(field, value);
|
|
|
}
|
|
|
- return Array.prototype.slice.call(decrypted, discardNumber, j);
|
|
|
- }
|
|
|
+ cff.topDict.privateDict = privateDict;
|
|
|
|
|
|
- function isSpecial(c) {
|
|
|
- return c === 0x2F || // '/'
|
|
|
- c === 0x5B || c === 0x5D || // '[', ']'
|
|
|
- c === 0x7B || c === 0x7D || // '{', '}'
|
|
|
- c === 0x28 || c === 0x29; // '(', ')'
|
|
|
+ var subrIndex = new CFFIndex();
|
|
|
+ for (i = 0, ii = subrs.length; i < ii; i++) {
|
|
|
+ subrIndex.add(subrs[i]);
|
|
|
+ }
|
|
|
+ privateDict.subrsIndex = subrIndex;
|
|
|
+
|
|
|
+ var compiler = new CFFCompiler(cff);
|
|
|
+ return compiler.compile();
|
|
|
}
|
|
|
+};
|
|
|
|
|
|
- function Type1Parser(stream, encrypted) {
|
|
|
- if (encrypted) {
|
|
|
- var data = stream.getBytes();
|
|
|
- var isBinary = !(isHexDigit(data[0]) && isHexDigit(data[1]) &&
|
|
|
- isHexDigit(data[2]) && isHexDigit(data[3]));
|
|
|
- stream = new Stream(isBinary ? decrypt(data, EEXEC_ENCRYPT_KEY, 4) :
|
|
|
- decryptAscii(data, EEXEC_ENCRYPT_KEY, 4));
|
|
|
+var CFFFont = (function CFFFontClosure() {
|
|
|
+ function CFFFont(file, properties) {
|
|
|
+ this.properties = properties;
|
|
|
+
|
|
|
+ var parser = new CFFParser(file, properties);
|
|
|
+ this.cff = parser.parse();
|
|
|
+ var compiler = new CFFCompiler(this.cff);
|
|
|
+ this.seacs = this.cff.seacs;
|
|
|
+ try {
|
|
|
+ this.data = compiler.compile();
|
|
|
+ } catch (e) {
|
|
|
+ warn('Failed to compile font ' + properties.loadedName);
|
|
|
+ // There may have just been an issue with the compiler, set the data
|
|
|
+ // anyway and hope the font loaded.
|
|
|
+ this.data = file;
|
|
|
}
|
|
|
- this.stream = stream;
|
|
|
- this.nextChar();
|
|
|
}
|
|
|
|
|
|
- Type1Parser.prototype = {
|
|
|
- readNumberArray: function Type1Parser_readNumberArray() {
|
|
|
- this.getToken(); // read '[' or '{' (arrays can start with either)
|
|
|
- var array = [];
|
|
|
- while (true) {
|
|
|
- var token = this.getToken();
|
|
|
- if (token === null || token === ']' || token === '}') {
|
|
|
- break;
|
|
|
- }
|
|
|
- array.push(parseFloat(token || 0));
|
|
|
- }
|
|
|
- return array;
|
|
|
- },
|
|
|
-
|
|
|
- readNumber: function Type1Parser_readNumber() {
|
|
|
- var token = this.getToken();
|
|
|
- return parseFloat(token || 0);
|
|
|
+ CFFFont.prototype = {
|
|
|
+ get numGlyphs() {
|
|
|
+ return this.cff.charStrings.count;
|
|
|
},
|
|
|
-
|
|
|
- readInt: function Type1Parser_readInt() {
|
|
|
- // Use '| 0' to prevent setting a double into length such as the double
|
|
|
- // does not flow into the loop variable.
|
|
|
- var token = this.getToken();
|
|
|
- return parseInt(token || 0, 10) | 0;
|
|
|
+ getCharset: function CFFFont_getCharset() {
|
|
|
+ return this.cff.charset.charset;
|
|
|
},
|
|
|
+ getGlyphMapping: function CFFFont_getGlyphMapping() {
|
|
|
+ var cff = this.cff;
|
|
|
+ var properties = this.properties;
|
|
|
+ var charsets = cff.charset.charset;
|
|
|
+ var charCodeToGlyphId;
|
|
|
+ var glyphId;
|
|
|
|
|
|
- readBoolean: function Type1Parser_readBoolean() {
|
|
|
- var token = this.getToken();
|
|
|
-
|
|
|
- // Use 1 and 0 since that's what type2 charstrings use.
|
|
|
- return token === 'true' ? 1 : 0;
|
|
|
- },
|
|
|
+ if (properties.composite) {
|
|
|
+ charCodeToGlyphId = Object.create(null);
|
|
|
+ if (cff.isCIDFont) {
|
|
|
+ // If the font is actually a CID font then we should use the charset
|
|
|
+ // to map CIDs to GIDs.
|
|
|
+ for (glyphId = 0; glyphId < charsets.length; glyphId++) {
|
|
|
+ var cid = charsets[glyphId];
|
|
|
+ var charCode = properties.cMap.charCodeOf(cid);
|
|
|
+ charCodeToGlyphId[charCode] = glyphId;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // If it is NOT actually a CID font then CIDs should be mapped
|
|
|
+ // directly to GIDs.
|
|
|
+ for (glyphId = 0; glyphId < cff.charStrings.count; glyphId++) {
|
|
|
+ charCodeToGlyphId[glyphId] = glyphId;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return charCodeToGlyphId;
|
|
|
+ }
|
|
|
|
|
|
- nextChar : function Type1_nextChar() {
|
|
|
- return (this.currentChar = this.stream.getByte());
|
|
|
- },
|
|
|
+ var encoding = cff.encoding ? cff.encoding.encoding : null;
|
|
|
+ charCodeToGlyphId = type1FontGlyphMapping(properties, encoding, charsets);
|
|
|
+ return charCodeToGlyphId;
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- getToken: function Type1Parser_getToken() {
|
|
|
- // Eat whitespace and comments.
|
|
|
- var comment = false;
|
|
|
- var ch = this.currentChar;
|
|
|
- while (true) {
|
|
|
- if (ch === -1) {
|
|
|
- return null;
|
|
|
- }
|
|
|
+ return CFFFont;
|
|
|
+})();
|
|
|
|
|
|
- if (comment) {
|
|
|
- if (ch === 0x0A || ch === 0x0D) {
|
|
|
- comment = false;
|
|
|
- }
|
|
|
- } else if (ch === 0x25) { // '%'
|
|
|
- comment = true;
|
|
|
- } else if (!Lexer.isSpace(ch)) {
|
|
|
- break;
|
|
|
- }
|
|
|
- ch = this.nextChar();
|
|
|
+var CFFParser = (function CFFParserClosure() {
|
|
|
+ var CharstringValidationData = [
|
|
|
+ null,
|
|
|
+ { id: 'hstem', min: 2, stackClearing: true, stem: true },
|
|
|
+ null,
|
|
|
+ { id: 'vstem', min: 2, stackClearing: true, stem: true },
|
|
|
+ { id: 'vmoveto', min: 1, stackClearing: true },
|
|
|
+ { id: 'rlineto', min: 2, resetStack: true },
|
|
|
+ { id: 'hlineto', min: 1, resetStack: true },
|
|
|
+ { id: 'vlineto', min: 1, resetStack: true },
|
|
|
+ { id: 'rrcurveto', min: 6, resetStack: true },
|
|
|
+ null,
|
|
|
+ { id: 'callsubr', min: 1, undefStack: true },
|
|
|
+ { id: 'return', min: 0, undefStack: true },
|
|
|
+ null, // 12
|
|
|
+ null,
|
|
|
+ { id: 'endchar', min: 0, stackClearing: true },
|
|
|
+ null,
|
|
|
+ null,
|
|
|
+ null,
|
|
|
+ { id: 'hstemhm', min: 2, stackClearing: true, stem: true },
|
|
|
+ { id: 'hintmask', min: 0, stackClearing: true },
|
|
|
+ { id: 'cntrmask', min: 0, stackClearing: true },
|
|
|
+ { id: 'rmoveto', min: 2, stackClearing: true },
|
|
|
+ { id: 'hmoveto', min: 1, stackClearing: true },
|
|
|
+ { id: 'vstemhm', min: 2, stackClearing: true, stem: true },
|
|
|
+ { id: 'rcurveline', min: 8, resetStack: true },
|
|
|
+ { id: 'rlinecurve', min: 8, resetStack: true },
|
|
|
+ { id: 'vvcurveto', min: 4, resetStack: true },
|
|
|
+ { id: 'hhcurveto', min: 4, resetStack: true },
|
|
|
+ null, // shortint
|
|
|
+ { id: 'callgsubr', min: 1, undefStack: true },
|
|
|
+ { id: 'vhcurveto', min: 4, resetStack: true },
|
|
|
+ { id: 'hvcurveto', min: 4, resetStack: true }
|
|
|
+ ];
|
|
|
+ var CharstringValidationData12 = [
|
|
|
+ null,
|
|
|
+ null,
|
|
|
+ null,
|
|
|
+ { id: 'and', min: 2, stackDelta: -1 },
|
|
|
+ { id: 'or', min: 2, stackDelta: -1 },
|
|
|
+ { id: 'not', min: 1, stackDelta: 0 },
|
|
|
+ null,
|
|
|
+ null,
|
|
|
+ null,
|
|
|
+ { id: 'abs', min: 1, stackDelta: 0 },
|
|
|
+ { id: 'add', min: 2, stackDelta: -1,
|
|
|
+ stackFn: function stack_div(stack, index) {
|
|
|
+ stack[index - 2] = stack[index - 2] + stack[index - 1];
|
|
|
}
|
|
|
- if (isSpecial(ch)) {
|
|
|
- this.nextChar();
|
|
|
- return String.fromCharCode(ch);
|
|
|
+ },
|
|
|
+ { id: 'sub', min: 2, stackDelta: -1,
|
|
|
+ stackFn: function stack_div(stack, index) {
|
|
|
+ stack[index - 2] = stack[index - 2] - stack[index - 1];
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { id: 'div', min: 2, stackDelta: -1,
|
|
|
+ stackFn: function stack_div(stack, index) {
|
|
|
+ stack[index - 2] = stack[index - 2] / stack[index - 1];
|
|
|
+ }
|
|
|
+ },
|
|
|
+ null,
|
|
|
+ { id: 'neg', min: 1, stackDelta: 0,
|
|
|
+ stackFn: function stack_div(stack, index) {
|
|
|
+ stack[index - 1] = -stack[index - 1];
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { id: 'eq', min: 2, stackDelta: -1 },
|
|
|
+ null,
|
|
|
+ null,
|
|
|
+ { id: 'drop', min: 1, stackDelta: -1 },
|
|
|
+ null,
|
|
|
+ { id: 'put', min: 2, stackDelta: -2 },
|
|
|
+ { id: 'get', min: 1, stackDelta: 0 },
|
|
|
+ { id: 'ifelse', min: 4, stackDelta: -3 },
|
|
|
+ { id: 'random', min: 0, stackDelta: 1 },
|
|
|
+ { id: 'mul', min: 2, stackDelta: -1,
|
|
|
+ stackFn: function stack_div(stack, index) {
|
|
|
+ stack[index - 2] = stack[index - 2] * stack[index - 1];
|
|
|
}
|
|
|
- var token = '';
|
|
|
- do {
|
|
|
- token += String.fromCharCode(ch);
|
|
|
- ch = this.nextChar();
|
|
|
- } while (ch >= 0 && !Lexer.isSpace(ch) && !isSpecial(ch));
|
|
|
- return token;
|
|
|
},
|
|
|
+ null,
|
|
|
+ { id: 'sqrt', min: 1, stackDelta: 0 },
|
|
|
+ { id: 'dup', min: 1, stackDelta: 1 },
|
|
|
+ { id: 'exch', min: 2, stackDelta: 0 },
|
|
|
+ { id: 'index', min: 2, stackDelta: 0 },
|
|
|
+ { id: 'roll', min: 3, stackDelta: -2 },
|
|
|
+ null,
|
|
|
+ null,
|
|
|
+ null,
|
|
|
+ { id: 'hflex', min: 7, resetStack: true },
|
|
|
+ { id: 'flex', min: 13, resetStack: true },
|
|
|
+ { id: 'hflex1', min: 9, resetStack: true },
|
|
|
+ { id: 'flex1', min: 11, resetStack: true }
|
|
|
+ ];
|
|
|
|
|
|
- /*
|
|
|
- * Returns an object containing a Subrs array and a CharStrings
|
|
|
- * array extracted from and eexec encrypted block of data
|
|
|
- */
|
|
|
- extractFontProgram: function Type1Parser_extractFontProgram() {
|
|
|
- var stream = this.stream;
|
|
|
+ function CFFParser(file, properties) {
|
|
|
+ this.bytes = file.getBytes();
|
|
|
+ this.properties = properties;
|
|
|
+ }
|
|
|
+ CFFParser.prototype = {
|
|
|
+ parse: function CFFParser_parse() {
|
|
|
+ var properties = this.properties;
|
|
|
+ var cff = new CFF();
|
|
|
+ this.cff = cff;
|
|
|
|
|
|
- var subrs = [], charstrings = [];
|
|
|
- var privateData = Object.create(null);
|
|
|
- privateData['lenIV'] = 4;
|
|
|
- var program = {
|
|
|
- subrs: [],
|
|
|
- charstrings: [],
|
|
|
- properties: {
|
|
|
- 'privateData': privateData
|
|
|
- }
|
|
|
- };
|
|
|
- var token, length, data, lenIV, encoded;
|
|
|
- while ((token = this.getToken()) !== null) {
|
|
|
- if (token !== '/') {
|
|
|
- continue;
|
|
|
- }
|
|
|
- token = this.getToken();
|
|
|
- switch (token) {
|
|
|
- case 'CharStrings':
|
|
|
- // The number immediately following CharStrings must be greater or
|
|
|
- // equal to the number of CharStrings.
|
|
|
- this.getToken();
|
|
|
- this.getToken(); // read in 'dict'
|
|
|
- this.getToken(); // read in 'dup'
|
|
|
- this.getToken(); // read in 'begin'
|
|
|
- while(true) {
|
|
|
- token = this.getToken();
|
|
|
- if (token === null || token === 'end') {
|
|
|
- break;
|
|
|
- }
|
|
|
+ // The first five sections must be in order, all the others are reached
|
|
|
+ // via offsets contained in one of the below.
|
|
|
+ var header = this.parseHeader();
|
|
|
+ var nameIndex = this.parseIndex(header.endPos);
|
|
|
+ var topDictIndex = this.parseIndex(nameIndex.endPos);
|
|
|
+ var stringIndex = this.parseIndex(topDictIndex.endPos);
|
|
|
+ var globalSubrIndex = this.parseIndex(stringIndex.endPos);
|
|
|
|
|
|
- if (token !== '/') {
|
|
|
- continue;
|
|
|
- }
|
|
|
- var glyph = this.getToken();
|
|
|
- length = this.readInt();
|
|
|
- this.getToken(); // read in 'RD' or '-|'
|
|
|
- data = stream.makeSubStream(stream.pos, length);
|
|
|
- lenIV = program.properties.privateData['lenIV'];
|
|
|
- encoded = decrypt(data.getBytes(), CHAR_STRS_ENCRYPT_KEY, lenIV);
|
|
|
- // Skip past the required space and binary data.
|
|
|
- stream.skip(length);
|
|
|
- this.nextChar();
|
|
|
- token = this.getToken(); // read in 'ND' or '|-'
|
|
|
- if (token === 'noaccess') {
|
|
|
- this.getToken(); // read in 'def'
|
|
|
- }
|
|
|
- charstrings.push({
|
|
|
- glyph: glyph,
|
|
|
- encoded: encoded
|
|
|
- });
|
|
|
- }
|
|
|
- break;
|
|
|
- case 'Subrs':
|
|
|
- var num = this.readInt();
|
|
|
- this.getToken(); // read in 'array'
|
|
|
- while ((token = this.getToken()) === 'dup') {
|
|
|
- var index = this.readInt();
|
|
|
- length = this.readInt();
|
|
|
- this.getToken(); // read in 'RD' or '-|'
|
|
|
- data = stream.makeSubStream(stream.pos, length);
|
|
|
- lenIV = program.properties.privateData['lenIV'];
|
|
|
- encoded = decrypt(data.getBytes(), CHAR_STRS_ENCRYPT_KEY, lenIV);
|
|
|
- // Skip past the required space and binary data.
|
|
|
- stream.skip(length);
|
|
|
- this.nextChar();
|
|
|
- token = this.getToken(); // read in 'NP' or '|'
|
|
|
- if (token === 'noaccess') {
|
|
|
- this.getToken(); // read in 'put'
|
|
|
- }
|
|
|
- subrs[index] = encoded;
|
|
|
- }
|
|
|
- break;
|
|
|
- case 'BlueValues':
|
|
|
- case 'OtherBlues':
|
|
|
- case 'FamilyBlues':
|
|
|
- case 'FamilyOtherBlues':
|
|
|
- var blueArray = this.readNumberArray();
|
|
|
- // *Blue* values may contain invalid data: disables reading of
|
|
|
- // those values when hinting is disabled.
|
|
|
- if (blueArray.length > 0 && (blueArray.length % 2) === 0 &&
|
|
|
- HINTING_ENABLED) {
|
|
|
- program.properties.privateData[token] = blueArray;
|
|
|
- }
|
|
|
- break;
|
|
|
- case 'StemSnapH':
|
|
|
- case 'StemSnapV':
|
|
|
- program.properties.privateData[token] = this.readNumberArray();
|
|
|
- break;
|
|
|
- case 'StdHW':
|
|
|
- case 'StdVW':
|
|
|
- program.properties.privateData[token] =
|
|
|
- this.readNumberArray()[0];
|
|
|
- break;
|
|
|
- case 'BlueShift':
|
|
|
- case 'lenIV':
|
|
|
- case 'BlueFuzz':
|
|
|
- case 'BlueScale':
|
|
|
- case 'LanguageGroup':
|
|
|
- case 'ExpansionFactor':
|
|
|
- program.properties.privateData[token] = this.readNumber();
|
|
|
- break;
|
|
|
- case 'ForceBold':
|
|
|
- program.properties.privateData[token] = this.readBoolean();
|
|
|
- break;
|
|
|
- }
|
|
|
+ var topDictParsed = this.parseDict(topDictIndex.obj.get(0));
|
|
|
+ var topDict = this.createDict(CFFTopDict, topDictParsed, cff.strings);
|
|
|
+
|
|
|
+ cff.header = header.obj;
|
|
|
+ cff.names = this.parseNameIndex(nameIndex.obj);
|
|
|
+ cff.strings = this.parseStringIndex(stringIndex.obj);
|
|
|
+ cff.topDict = topDict;
|
|
|
+ cff.globalSubrIndex = globalSubrIndex.obj;
|
|
|
+
|
|
|
+ this.parsePrivateDict(cff.topDict);
|
|
|
+
|
|
|
+ cff.isCIDFont = topDict.hasName('ROS');
|
|
|
+
|
|
|
+ var charStringOffset = topDict.getByName('CharStrings');
|
|
|
+ var charStringIndex = this.parseIndex(charStringOffset).obj;
|
|
|
+
|
|
|
+ var fontMatrix = topDict.getByName('FontMatrix');
|
|
|
+ if (fontMatrix) {
|
|
|
+ properties.fontMatrix = fontMatrix;
|
|
|
}
|
|
|
|
|
|
- for (var i = 0; i < charstrings.length; i++) {
|
|
|
- glyph = charstrings[i].glyph;
|
|
|
- encoded = charstrings[i].encoded;
|
|
|
- var charString = new Type1CharString();
|
|
|
- var error = charString.convert(encoded, subrs);
|
|
|
- var output = charString.output;
|
|
|
- if (error) {
|
|
|
- // It seems when FreeType encounters an error while evaluating a glyph
|
|
|
- // that it completely ignores the glyph so we'll mimic that behaviour
|
|
|
- // here and put an endchar to make the validator happy.
|
|
|
- output = [14];
|
|
|
+ var fontBBox = topDict.getByName('FontBBox');
|
|
|
+ if (fontBBox) {
|
|
|
+ // adjusting ascent/descent
|
|
|
+ properties.ascent = fontBBox[3];
|
|
|
+ properties.descent = fontBBox[1];
|
|
|
+ properties.ascentScaled = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ var charset, encoding;
|
|
|
+ if (cff.isCIDFont) {
|
|
|
+ var fdArrayIndex = this.parseIndex(topDict.getByName('FDArray')).obj;
|
|
|
+ for (var i = 0, ii = fdArrayIndex.count; i < ii; ++i) {
|
|
|
+ var dictRaw = fdArrayIndex.get(i);
|
|
|
+ var fontDict = this.createDict(CFFTopDict, this.parseDict(dictRaw),
|
|
|
+ cff.strings);
|
|
|
+ this.parsePrivateDict(fontDict);
|
|
|
+ cff.fdArray.push(fontDict);
|
|
|
}
|
|
|
- program.charstrings.push({
|
|
|
- glyphName: glyph,
|
|
|
- charstring: output,
|
|
|
- width: charString.width,
|
|
|
- lsb: charString.lsb,
|
|
|
- seac: charString.seac
|
|
|
- });
|
|
|
+ // cid fonts don't have an encoding
|
|
|
+ encoding = null;
|
|
|
+ charset = this.parseCharsets(topDict.getByName('charset'),
|
|
|
+ charStringIndex.count, cff.strings, true);
|
|
|
+ cff.fdSelect = this.parseFDSelect(topDict.getByName('FDSelect'),
|
|
|
+ charStringIndex.count);
|
|
|
+ } else {
|
|
|
+ charset = this.parseCharsets(topDict.getByName('charset'),
|
|
|
+ charStringIndex.count, cff.strings, false);
|
|
|
+ encoding = this.parseEncoding(topDict.getByName('Encoding'),
|
|
|
+ properties,
|
|
|
+ cff.strings, charset.charset);
|
|
|
}
|
|
|
|
|
|
- return program;
|
|
|
+ cff.charset = charset;
|
|
|
+ cff.encoding = encoding;
|
|
|
+
|
|
|
+ var charStringsAndSeacs = this.parseCharStrings(
|
|
|
+ charStringIndex,
|
|
|
+ topDict.privateDict.subrsIndex,
|
|
|
+ globalSubrIndex.obj,
|
|
|
+ cff.fdSelect,
|
|
|
+ cff.fdArray);
|
|
|
+ cff.charStrings = charStringsAndSeacs.charStrings;
|
|
|
+ cff.seacs = charStringsAndSeacs.seacs;
|
|
|
+ cff.widths = charStringsAndSeacs.widths;
|
|
|
+
|
|
|
+ return cff;
|
|
|
},
|
|
|
+ parseHeader: function CFFParser_parseHeader() {
|
|
|
+ var bytes = this.bytes;
|
|
|
+ var bytesLength = bytes.length;
|
|
|
+ var offset = 0;
|
|
|
|
|
|
- extractFontHeader: function Type1Parser_extractFontHeader(properties) {
|
|
|
- var token;
|
|
|
- while ((token = this.getToken()) !== null) {
|
|
|
- if (token !== '/') {
|
|
|
- continue;
|
|
|
+ // Prevent an infinite loop, by checking that the offset is within the
|
|
|
+ // bounds of the bytes array. Necessary in empty, or invalid, font files.
|
|
|
+ while (offset < bytesLength && bytes[offset] !== 1) {
|
|
|
+ ++offset;
|
|
|
+ }
|
|
|
+ if (offset >= bytesLength) {
|
|
|
+ error('Invalid CFF header');
|
|
|
+ } else if (offset !== 0) {
|
|
|
+ info('cff data is shifted');
|
|
|
+ bytes = bytes.subarray(offset);
|
|
|
+ this.bytes = bytes;
|
|
|
+ }
|
|
|
+ var major = bytes[0];
|
|
|
+ var minor = bytes[1];
|
|
|
+ var hdrSize = bytes[2];
|
|
|
+ var offSize = bytes[3];
|
|
|
+ var header = new CFFHeader(major, minor, hdrSize, offSize);
|
|
|
+ return { obj: header, endPos: hdrSize };
|
|
|
+ },
|
|
|
+ parseDict: function CFFParser_parseDict(dict) {
|
|
|
+ var pos = 0;
|
|
|
+
|
|
|
+ function parseOperand() {
|
|
|
+ var value = dict[pos++];
|
|
|
+ if (value === 30) {
|
|
|
+ return parseFloatOperand(pos);
|
|
|
+ } else if (value === 28) {
|
|
|
+ value = dict[pos++];
|
|
|
+ value = ((value << 24) | (dict[pos++] << 16)) >> 16;
|
|
|
+ return value;
|
|
|
+ } else if (value === 29) {
|
|
|
+ value = dict[pos++];
|
|
|
+ value = (value << 8) | dict[pos++];
|
|
|
+ value = (value << 8) | dict[pos++];
|
|
|
+ value = (value << 8) | dict[pos++];
|
|
|
+ return value;
|
|
|
+ } else if (value >= 32 && value <= 246) {
|
|
|
+ return value - 139;
|
|
|
+ } else if (value >= 247 && value <= 250) {
|
|
|
+ return ((value - 247) * 256) + dict[pos++] + 108;
|
|
|
+ } else if (value >= 251 && value <= 254) {
|
|
|
+ return -((value - 251) * 256) - dict[pos++] - 108;
|
|
|
+ } else {
|
|
|
+ error('255 is not a valid DICT command');
|
|
|
}
|
|
|
- token = this.getToken();
|
|
|
- switch (token) {
|
|
|
- case 'FontMatrix':
|
|
|
- var matrix = this.readNumberArray();
|
|
|
- properties.fontMatrix = matrix;
|
|
|
- break;
|
|
|
- case 'Encoding':
|
|
|
- var encodingArg = this.getToken();
|
|
|
- var encoding;
|
|
|
- if (!/^\d+$/.test(encodingArg)) {
|
|
|
- // encoding name is specified
|
|
|
- encoding = getEncoding(encodingArg);
|
|
|
- } else {
|
|
|
- encoding = [];
|
|
|
- var size = parseInt(encodingArg, 10) | 0;
|
|
|
- this.getToken(); // read in 'array'
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
|
|
|
- for (var j = 0; j < size; j++) {
|
|
|
- token = this.getToken();
|
|
|
- // skipping till first dup or def (e.g. ignoring for statement)
|
|
|
- while (token !== 'dup' && token !== 'def') {
|
|
|
- token = this.getToken();
|
|
|
- if (token === null) {
|
|
|
- return; // invalid header
|
|
|
- }
|
|
|
- }
|
|
|
- if (token === 'def') {
|
|
|
- break; // read all array data
|
|
|
- }
|
|
|
- var index = this.readInt();
|
|
|
- this.getToken(); // read in '/'
|
|
|
- var glyph = this.getToken();
|
|
|
- encoding[index] = glyph;
|
|
|
- this.getToken(); // read the in 'put'
|
|
|
- }
|
|
|
- }
|
|
|
- properties.builtInEncoding = encoding;
|
|
|
+ function parseFloatOperand() {
|
|
|
+ var str = '';
|
|
|
+ var eof = 15;
|
|
|
+ var lookup = ['0', '1', '2', '3', '4', '5', '6', '7', '8',
|
|
|
+ '9', '.', 'E', 'E-', null, '-'];
|
|
|
+ var length = dict.length;
|
|
|
+ while (pos < length) {
|
|
|
+ var b = dict[pos++];
|
|
|
+ var b1 = b >> 4;
|
|
|
+ var b2 = b & 15;
|
|
|
+
|
|
|
+ if (b1 === eof) {
|
|
|
break;
|
|
|
- case 'FontBBox':
|
|
|
- var fontBBox = this.readNumberArray();
|
|
|
- // adjusting ascent/descent
|
|
|
- properties.ascent = fontBBox[3];
|
|
|
- properties.descent = fontBBox[1];
|
|
|
- properties.ascentScaled = true;
|
|
|
+ }
|
|
|
+ str += lookup[b1];
|
|
|
+
|
|
|
+ if (b2 === eof) {
|
|
|
break;
|
|
|
+ }
|
|
|
+ str += lookup[b2];
|
|
|
}
|
|
|
+ return parseFloat(str);
|
|
|
}
|
|
|
- }
|
|
|
- };
|
|
|
|
|
|
- return Type1Parser;
|
|
|
-})();
|
|
|
+ var operands = [];
|
|
|
+ var entries = [];
|
|
|
|
|
|
-/**
|
|
|
- * The CFF class takes a Type1 file and wrap it into a
|
|
|
- * 'Compact Font Format' which itself embed Type2 charstrings.
|
|
|
- */
|
|
|
-var CFFStandardStrings = [
|
|
|
- '.notdef', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent',
|
|
|
- 'ampersand', 'quoteright', 'parenleft', 'parenright', 'asterisk', 'plus',
|
|
|
- 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four',
|
|
|
- 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less',
|
|
|
- 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
|
|
|
- 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
|
|
|
- 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', 'asciicircum',
|
|
|
- 'underscore', 'quoteleft', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
|
|
|
- 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y',
|
|
|
- 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', 'exclamdown', 'cent',
|
|
|
- 'sterling', 'fraction', 'yen', 'florin', 'section', 'currency',
|
|
|
- 'quotesingle', 'quotedblleft', 'guillemotleft', 'guilsinglleft',
|
|
|
- 'guilsinglright', 'fi', 'fl', 'endash', 'dagger', 'daggerdbl',
|
|
|
- 'periodcentered', 'paragraph', 'bullet', 'quotesinglbase', 'quotedblbase',
|
|
|
- 'quotedblright', 'guillemotright', 'ellipsis', 'perthousand', 'questiondown',
|
|
|
- 'grave', 'acute', 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent',
|
|
|
- 'dieresis', 'ring', 'cedilla', 'hungarumlaut', 'ogonek', 'caron', 'emdash',
|
|
|
- 'AE', 'ordfeminine', 'Lslash', 'Oslash', 'OE', 'ordmasculine', 'ae',
|
|
|
- 'dotlessi', 'lslash', 'oslash', 'oe', 'germandbls', 'onesuperior',
|
|
|
- 'logicalnot', 'mu', 'trademark', 'Eth', 'onehalf', 'plusminus', 'Thorn',
|
|
|
- 'onequarter', 'divide', 'brokenbar', 'degree', 'thorn', 'threequarters',
|
|
|
- 'twosuperior', 'registered', 'minus', 'eth', 'multiply', 'threesuperior',
|
|
|
- 'copyright', 'Aacute', 'Acircumflex', 'Adieresis', 'Agrave', 'Aring',
|
|
|
- 'Atilde', 'Ccedilla', 'Eacute', 'Ecircumflex', 'Edieresis', 'Egrave',
|
|
|
- 'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', 'Ntilde', 'Oacute',
|
|
|
- 'Ocircumflex', 'Odieresis', 'Ograve', 'Otilde', 'Scaron', 'Uacute',
|
|
|
- 'Ucircumflex', 'Udieresis', 'Ugrave', 'Yacute', 'Ydieresis', 'Zcaron',
|
|
|
- 'aacute', 'acircumflex', 'adieresis', 'agrave', 'aring', 'atilde',
|
|
|
- 'ccedilla', 'eacute', 'ecircumflex', 'edieresis', 'egrave', 'iacute',
|
|
|
- 'icircumflex', 'idieresis', 'igrave', 'ntilde', 'oacute', 'ocircumflex',
|
|
|
- 'odieresis', 'ograve', 'otilde', 'scaron', 'uacute', 'ucircumflex',
|
|
|
- 'udieresis', 'ugrave', 'yacute', 'ydieresis', 'zcaron', 'exclamsmall',
|
|
|
- 'Hungarumlautsmall', 'dollaroldstyle', 'dollarsuperior', 'ampersandsmall',
|
|
|
- 'Acutesmall', 'parenleftsuperior', 'parenrightsuperior', 'twodotenleader',
|
|
|
- 'onedotenleader', 'zerooldstyle', 'oneoldstyle', 'twooldstyle',
|
|
|
- 'threeoldstyle', 'fouroldstyle', 'fiveoldstyle', 'sixoldstyle',
|
|
|
- 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', 'commasuperior',
|
|
|
- 'threequartersemdash', 'periodsuperior', 'questionsmall', 'asuperior',
|
|
|
- 'bsuperior', 'centsuperior', 'dsuperior', 'esuperior', 'isuperior',
|
|
|
- 'lsuperior', 'msuperior', 'nsuperior', 'osuperior', 'rsuperior', 'ssuperior',
|
|
|
- 'tsuperior', 'ff', 'ffi', 'ffl', 'parenleftinferior', 'parenrightinferior',
|
|
|
- 'Circumflexsmall', 'hyphensuperior', 'Gravesmall', 'Asmall', 'Bsmall',
|
|
|
- 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', 'Ismall',
|
|
|
- 'Jsmall', 'Ksmall', 'Lsmall', 'Msmall', 'Nsmall', 'Osmall', 'Psmall',
|
|
|
- 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall',
|
|
|
- 'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah',
|
|
|
- 'Tildesmall', 'exclamdownsmall', 'centoldstyle', 'Lslashsmall',
|
|
|
- 'Scaronsmall', 'Zcaronsmall', 'Dieresissmall', 'Brevesmall', 'Caronsmall',
|
|
|
- 'Dotaccentsmall', 'Macronsmall', 'figuredash', 'hypheninferior',
|
|
|
- 'Ogoneksmall', 'Ringsmall', 'Cedillasmall', 'questiondownsmall', 'oneeighth',
|
|
|
- 'threeeighths', 'fiveeighths', 'seveneighths', 'onethird', 'twothirds',
|
|
|
- 'zerosuperior', 'foursuperior', 'fivesuperior', 'sixsuperior',
|
|
|
- 'sevensuperior', 'eightsuperior', 'ninesuperior', 'zeroinferior',
|
|
|
- 'oneinferior', 'twoinferior', 'threeinferior', 'fourinferior',
|
|
|
- 'fiveinferior', 'sixinferior', 'seveninferior', 'eightinferior',
|
|
|
- 'nineinferior', 'centinferior', 'dollarinferior', 'periodinferior',
|
|
|
- 'commainferior', 'Agravesmall', 'Aacutesmall', 'Acircumflexsmall',
|
|
|
- 'Atildesmall', 'Adieresissmall', 'Aringsmall', 'AEsmall', 'Ccedillasmall',
|
|
|
- 'Egravesmall', 'Eacutesmall', 'Ecircumflexsmall', 'Edieresissmall',
|
|
|
- 'Igravesmall', 'Iacutesmall', 'Icircumflexsmall', 'Idieresissmall',
|
|
|
- 'Ethsmall', 'Ntildesmall', 'Ogravesmall', 'Oacutesmall', 'Ocircumflexsmall',
|
|
|
- 'Otildesmall', 'Odieresissmall', 'OEsmall', 'Oslashsmall', 'Ugravesmall',
|
|
|
- 'Uacutesmall', 'Ucircumflexsmall', 'Udieresissmall', 'Yacutesmall',
|
|
|
- 'Thornsmall', 'Ydieresissmall', '001.000', '001.001', '001.002', '001.003',
|
|
|
- 'Black', 'Bold', 'Book', 'Light', 'Medium', 'Regular', 'Roman', 'Semibold'
|
|
|
-];
|
|
|
-
|
|
|
-// Type1Font is also a CIDFontType0.
|
|
|
-var Type1Font = function Type1Font(name, file, properties) {
|
|
|
- // Some bad generators embed pfb file as is, we have to strip 6-byte headers.
|
|
|
- // Also, length1 and length2 might be off by 6 bytes as well.
|
|
|
- // http://www.math.ubc.ca/~cass/piscript/type1.pdf
|
|
|
- var PFB_HEADER_SIZE = 6;
|
|
|
- var headerBlockLength = properties.length1;
|
|
|
- var eexecBlockLength = properties.length2;
|
|
|
- var pfbHeader = file.peekBytes(PFB_HEADER_SIZE);
|
|
|
- var pfbHeaderPresent = pfbHeader[0] === 0x80 && pfbHeader[1] === 0x01;
|
|
|
- if (pfbHeaderPresent) {
|
|
|
- file.skip(PFB_HEADER_SIZE);
|
|
|
- headerBlockLength = (pfbHeader[5] << 24) | (pfbHeader[4] << 16) |
|
|
|
- (pfbHeader[3] << 8) | pfbHeader[2];
|
|
|
- }
|
|
|
-
|
|
|
- // Get the data block containing glyphs and subrs informations
|
|
|
- var headerBlock = new Stream(file.getBytes(headerBlockLength));
|
|
|
- var headerBlockParser = new Type1Parser(headerBlock);
|
|
|
- headerBlockParser.extractFontHeader(properties);
|
|
|
-
|
|
|
- if (pfbHeaderPresent) {
|
|
|
- pfbHeader = file.getBytes(PFB_HEADER_SIZE);
|
|
|
- eexecBlockLength = (pfbHeader[5] << 24) | (pfbHeader[4] << 16) |
|
|
|
- (pfbHeader[3] << 8) | pfbHeader[2];
|
|
|
- }
|
|
|
-
|
|
|
- // Decrypt the data blocks and retrieve it's content
|
|
|
- var eexecBlock = new Stream(file.getBytes(eexecBlockLength));
|
|
|
- var eexecBlockParser = new Type1Parser(eexecBlock, true);
|
|
|
- var data = eexecBlockParser.extractFontProgram();
|
|
|
- for (var info in data.properties) {
|
|
|
- properties[info] = data.properties[info];
|
|
|
- }
|
|
|
-
|
|
|
- var charstrings = data.charstrings;
|
|
|
- var type2Charstrings = this.getType2Charstrings(charstrings);
|
|
|
- var subrs = this.getType2Subrs(data.subrs);
|
|
|
-
|
|
|
- this.charstrings = charstrings;
|
|
|
- this.data = this.wrap(name, type2Charstrings, this.charstrings,
|
|
|
- subrs, properties);
|
|
|
- this.seacs = this.getSeacs(data.charstrings);
|
|
|
-};
|
|
|
-
|
|
|
-Type1Font.prototype = {
|
|
|
- get numGlyphs() {
|
|
|
- return this.charstrings.length + 1;
|
|
|
- },
|
|
|
-
|
|
|
- getCharset: function Type1Font_getCharset() {
|
|
|
- var charset = ['.notdef'];
|
|
|
- var charstrings = this.charstrings;
|
|
|
- for (var glyphId = 0; glyphId < charstrings.length; glyphId++) {
|
|
|
- charset.push(charstrings[glyphId].glyphName);
|
|
|
- }
|
|
|
- return charset;
|
|
|
- },
|
|
|
-
|
|
|
- getGlyphMapping: function Type1Font_getGlyphMapping(properties) {
|
|
|
- var charstrings = this.charstrings;
|
|
|
- var glyphNames = ['.notdef'], glyphId;
|
|
|
- for (glyphId = 0; glyphId < charstrings.length; glyphId++) {
|
|
|
- glyphNames.push(charstrings[glyphId].glyphName);
|
|
|
- }
|
|
|
- var encoding = properties.builtInEncoding;
|
|
|
- if (encoding) {
|
|
|
- var builtInEncoding = Object.create(null);
|
|
|
- for (var charCode in encoding) {
|
|
|
- glyphId = glyphNames.indexOf(encoding[charCode]);
|
|
|
- if (glyphId >= 0) {
|
|
|
- builtInEncoding[charCode] = glyphId;
|
|
|
+ pos = 0;
|
|
|
+ var end = dict.length;
|
|
|
+ while (pos < end) {
|
|
|
+ var b = dict[pos];
|
|
|
+ if (b <= 21) {
|
|
|
+ if (b === 12) {
|
|
|
+ b = (b << 8) | dict[++pos];
|
|
|
+ }
|
|
|
+ entries.push([b, operands]);
|
|
|
+ operands = [];
|
|
|
+ ++pos;
|
|
|
+ } else {
|
|
|
+ operands.push(parseOperand());
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
+ return entries;
|
|
|
+ },
|
|
|
+ parseIndex: function CFFParser_parseIndex(pos) {
|
|
|
+ var cffIndex = new CFFIndex();
|
|
|
+ var bytes = this.bytes;
|
|
|
+ var count = (bytes[pos++] << 8) | bytes[pos++];
|
|
|
+ var offsets = [];
|
|
|
+ var end = pos;
|
|
|
+ var i, ii;
|
|
|
|
|
|
- return type1FontGlyphMapping(properties, builtInEncoding, glyphNames);
|
|
|
- },
|
|
|
+ if (count !== 0) {
|
|
|
+ var offsetSize = bytes[pos++];
|
|
|
+ // add 1 for offset to determine size of last object
|
|
|
+ var startPos = pos + ((count + 1) * offsetSize) - 1;
|
|
|
|
|
|
- getSeacs: function Type1Font_getSeacs(charstrings) {
|
|
|
- var i, ii;
|
|
|
- var seacMap = [];
|
|
|
- for (i = 0, ii = charstrings.length; i < ii; i++) {
|
|
|
- var charstring = charstrings[i];
|
|
|
- if (charstring.seac) {
|
|
|
- // Offset by 1 for .notdef
|
|
|
- seacMap[i + 1] = charstring.seac;
|
|
|
+ for (i = 0, ii = count + 1; i < ii; ++i) {
|
|
|
+ var offset = 0;
|
|
|
+ for (var j = 0; j < offsetSize; ++j) {
|
|
|
+ offset <<= 8;
|
|
|
+ offset += bytes[pos++];
|
|
|
+ }
|
|
|
+ offsets.push(startPos + offset);
|
|
|
+ }
|
|
|
+ end = offsets[count];
|
|
|
}
|
|
|
- }
|
|
|
- return seacMap;
|
|
|
- },
|
|
|
-
|
|
|
- getType2Charstrings: function Type1Font_getType2Charstrings(
|
|
|
- type1Charstrings) {
|
|
|
- var type2Charstrings = [];
|
|
|
- for (var i = 0, ii = type1Charstrings.length; i < ii; i++) {
|
|
|
- type2Charstrings.push(type1Charstrings[i].charstring);
|
|
|
- }
|
|
|
- return type2Charstrings;
|
|
|
- },
|
|
|
-
|
|
|
- getType2Subrs: function Type1Font_getType2Subrs(type1Subrs) {
|
|
|
- var bias = 0;
|
|
|
- var count = type1Subrs.length;
|
|
|
- if (count < 1133) {
|
|
|
- bias = 107;
|
|
|
- } else if (count < 33769) {
|
|
|
- bias = 1131;
|
|
|
- } else {
|
|
|
- bias = 32768;
|
|
|
- }
|
|
|
-
|
|
|
- // Add a bunch of empty subrs to deal with the Type2 bias
|
|
|
- var type2Subrs = [];
|
|
|
- var i;
|
|
|
- for (i = 0; i < bias; i++) {
|
|
|
- type2Subrs.push([0x0B]);
|
|
|
- }
|
|
|
-
|
|
|
- for (i = 0; i < count; i++) {
|
|
|
- type2Subrs.push(type1Subrs[i]);
|
|
|
- }
|
|
|
+ for (i = 0, ii = offsets.length - 1; i < ii; ++i) {
|
|
|
+ var offsetStart = offsets[i];
|
|
|
+ var offsetEnd = offsets[i + 1];
|
|
|
+ cffIndex.add(bytes.subarray(offsetStart, offsetEnd));
|
|
|
+ }
|
|
|
+ return {obj: cffIndex, endPos: end};
|
|
|
+ },
|
|
|
+ parseNameIndex: function CFFParser_parseNameIndex(index) {
|
|
|
+ var names = [];
|
|
|
+ for (var i = 0, ii = index.count; i < ii; ++i) {
|
|
|
+ var name = index.get(i);
|
|
|
+ // OTS doesn't allow names to be over 127 characters.
|
|
|
+ var length = Math.min(name.length, 127);
|
|
|
+ var data = [];
|
|
|
+ // OTS also only permits certain characters in the name.
|
|
|
+ for (var j = 0; j < length; ++j) {
|
|
|
+ var c = name[j];
|
|
|
+ if (j === 0 && c === 0) {
|
|
|
+ data[j] = c;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if ((c < 33 || c > 126) || c === 91 /* [ */ || c === 93 /* ] */ ||
|
|
|
+ c === 40 /* ( */ || c === 41 /* ) */ || c === 123 /* { */ ||
|
|
|
+ c === 125 /* } */ || c === 60 /* < */ || c === 62 /* > */ ||
|
|
|
+ c === 47 /* / */ || c === 37 /* % */ || c === 35 /* # */) {
|
|
|
+ data[j] = 95;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ data[j] = c;
|
|
|
+ }
|
|
|
+ names.push(bytesToString(data));
|
|
|
+ }
|
|
|
+ return names;
|
|
|
+ },
|
|
|
+ parseStringIndex: function CFFParser_parseStringIndex(index) {
|
|
|
+ var strings = new CFFStrings();
|
|
|
+ for (var i = 0, ii = index.count; i < ii; ++i) {
|
|
|
+ var data = index.get(i);
|
|
|
+ strings.add(bytesToString(data));
|
|
|
+ }
|
|
|
+ return strings;
|
|
|
+ },
|
|
|
+ createDict: function CFFParser_createDict(Type, dict, strings) {
|
|
|
+ var cffDict = new Type(strings);
|
|
|
+ for (var i = 0, ii = dict.length; i < ii; ++i) {
|
|
|
+ var pair = dict[i];
|
|
|
+ var key = pair[0];
|
|
|
+ var value = pair[1];
|
|
|
+ cffDict.setByKey(key, value);
|
|
|
+ }
|
|
|
+ return cffDict;
|
|
|
+ },
|
|
|
+ parseCharString: function CFFParser_parseCharString(state, data,
|
|
|
+ localSubrIndex,
|
|
|
+ globalSubrIndex) {
|
|
|
+ if (state.callDepth > MAX_SUBR_NESTING) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ var stackSize = state.stackSize;
|
|
|
+ var stack = state.stack;
|
|
|
|
|
|
- return type2Subrs;
|
|
|
- },
|
|
|
+ var length = data.length;
|
|
|
|
|
|
- wrap: function Type1Font_wrap(name, glyphs, charstrings, subrs, properties) {
|
|
|
- var cff = new CFF();
|
|
|
- cff.header = new CFFHeader(1, 0, 4, 4);
|
|
|
+ for (var j = 0; j < length;) {
|
|
|
+ var value = data[j++];
|
|
|
+ var validationCommand = null;
|
|
|
+ if (value === 12) {
|
|
|
+ var q = data[j++];
|
|
|
+ if (q === 0) {
|
|
|
+ // The CFF specification state that the 'dotsection' command
|
|
|
+ // (12, 0) is deprecated and treated as a no-op, but all Type2
|
|
|
+ // charstrings processors should support them. Unfortunately
|
|
|
+ // the font sanitizer don't. As a workaround the sequence (12, 0)
|
|
|
+ // is replaced by a useless (0, hmoveto).
|
|
|
+ data[j - 2] = 139;
|
|
|
+ data[j - 1] = 22;
|
|
|
+ stackSize = 0;
|
|
|
+ } else {
|
|
|
+ validationCommand = CharstringValidationData12[q];
|
|
|
+ }
|
|
|
+ } else if (value === 28) { // number (16 bit)
|
|
|
+ stack[stackSize] = ((data[j] << 24) | (data[j + 1] << 16)) >> 16;
|
|
|
+ j += 2;
|
|
|
+ stackSize++;
|
|
|
+ } else if (value === 14) {
|
|
|
+ if (stackSize >= 4) {
|
|
|
+ stackSize -= 4;
|
|
|
+ if (SEAC_ANALYSIS_ENABLED) {
|
|
|
+ state.seac = stack.slice(stackSize, stackSize + 4);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ validationCommand = CharstringValidationData[value];
|
|
|
+ } else if (value >= 32 && value <= 246) { // number
|
|
|
+ stack[stackSize] = value - 139;
|
|
|
+ stackSize++;
|
|
|
+ } else if (value >= 247 && value <= 254) { // number (+1 bytes)
|
|
|
+ stack[stackSize] = (value < 251 ?
|
|
|
+ ((value - 247) << 8) + data[j] + 108 :
|
|
|
+ -((value - 251) << 8) - data[j] - 108);
|
|
|
+ j++;
|
|
|
+ stackSize++;
|
|
|
+ } else if (value === 255) { // number (32 bit)
|
|
|
+ stack[stackSize] = ((data[j] << 24) | (data[j + 1] << 16) |
|
|
|
+ (data[j + 2] << 8) | data[j + 3]) / 65536;
|
|
|
+ j += 4;
|
|
|
+ stackSize++;
|
|
|
+ } else if (value === 19 || value === 20) {
|
|
|
+ state.hints += stackSize >> 1;
|
|
|
+ // skipping right amount of hints flag data
|
|
|
+ j += (state.hints + 7) >> 3;
|
|
|
+ stackSize %= 2;
|
|
|
+ validationCommand = CharstringValidationData[value];
|
|
|
+ } else if (value === 10 || value === 29) {
|
|
|
+ var subrsIndex;
|
|
|
+ if (value === 10) {
|
|
|
+ subrsIndex = localSubrIndex;
|
|
|
+ } else {
|
|
|
+ subrsIndex = globalSubrIndex;
|
|
|
+ }
|
|
|
+ if (!subrsIndex) {
|
|
|
+ validationCommand = CharstringValidationData[value];
|
|
|
+ warn('Missing subrsIndex for ' + validationCommand.id);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ var bias = 32768;
|
|
|
+ if (subrsIndex.count < 1240) {
|
|
|
+ bias = 107;
|
|
|
+ } else if (subrsIndex.count < 33900) {
|
|
|
+ bias = 1131;
|
|
|
+ }
|
|
|
+ var subrNumber = stack[--stackSize] + bias;
|
|
|
+ if (subrNumber < 0 || subrNumber >= subrsIndex.count) {
|
|
|
+ validationCommand = CharstringValidationData[value];
|
|
|
+ warn('Out of bounds subrIndex for ' + validationCommand.id);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ state.stackSize = stackSize;
|
|
|
+ state.callDepth++;
|
|
|
+ var valid = this.parseCharString(state, subrsIndex.get(subrNumber),
|
|
|
+ localSubrIndex, globalSubrIndex);
|
|
|
+ if (!valid) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ state.callDepth--;
|
|
|
+ stackSize = state.stackSize;
|
|
|
+ continue;
|
|
|
+ } else if (value === 11) {
|
|
|
+ state.stackSize = stackSize;
|
|
|
+ return true;
|
|
|
+ } else {
|
|
|
+ validationCommand = CharstringValidationData[value];
|
|
|
+ }
|
|
|
+ if (validationCommand) {
|
|
|
+ if (validationCommand.stem) {
|
|
|
+ state.hints += stackSize >> 1;
|
|
|
+ }
|
|
|
+ if ('min' in validationCommand) {
|
|
|
+ if (!state.undefStack && stackSize < validationCommand.min) {
|
|
|
+ warn('Not enough parameters for ' + validationCommand.id +
|
|
|
+ '; actual: ' + stackSize +
|
|
|
+ ', expected: ' + validationCommand.min);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (state.firstStackClearing && validationCommand.stackClearing) {
|
|
|
+ state.firstStackClearing = false;
|
|
|
+ // the optional character width can be found before the first
|
|
|
+ // stack-clearing command arguments
|
|
|
+ stackSize -= validationCommand.min;
|
|
|
+ if (stackSize >= 2 && validationCommand.stem) {
|
|
|
+ // there are even amount of arguments for stem commands
|
|
|
+ stackSize %= 2;
|
|
|
+ } else if (stackSize > 1) {
|
|
|
+ warn('Found too many parameters for stack-clearing command');
|
|
|
+ }
|
|
|
+ if (stackSize > 0 && stack[stackSize - 1] >= 0) {
|
|
|
+ state.width = stack[stackSize - 1];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ('stackDelta' in validationCommand) {
|
|
|
+ if ('stackFn' in validationCommand) {
|
|
|
+ validationCommand.stackFn(stack, stackSize);
|
|
|
+ }
|
|
|
+ stackSize += validationCommand.stackDelta;
|
|
|
+ } else if (validationCommand.stackClearing) {
|
|
|
+ stackSize = 0;
|
|
|
+ } else if (validationCommand.resetStack) {
|
|
|
+ stackSize = 0;
|
|
|
+ state.undefStack = false;
|
|
|
+ } else if (validationCommand.undefStack) {
|
|
|
+ stackSize = 0;
|
|
|
+ state.undefStack = true;
|
|
|
+ state.firstStackClearing = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ state.stackSize = stackSize;
|
|
|
+ return true;
|
|
|
+ },
|
|
|
+ parseCharStrings: function CFFParser_parseCharStrings(charStrings,
|
|
|
+ localSubrIndex,
|
|
|
+ globalSubrIndex,
|
|
|
+ fdSelect,
|
|
|
+ fdArray) {
|
|
|
+ var seacs = [];
|
|
|
+ var widths = [];
|
|
|
+ var count = charStrings.count;
|
|
|
+ for (var i = 0; i < count; i++) {
|
|
|
+ var charstring = charStrings.get(i);
|
|
|
+ var state = {
|
|
|
+ callDepth: 0,
|
|
|
+ stackSize: 0,
|
|
|
+ stack: [],
|
|
|
+ undefStack: true,
|
|
|
+ hints: 0,
|
|
|
+ firstStackClearing: true,
|
|
|
+ seac: null,
|
|
|
+ width: null
|
|
|
+ };
|
|
|
+ var valid = true;
|
|
|
+ var localSubrToUse = null;
|
|
|
+ if (fdSelect && fdArray.length) {
|
|
|
+ var fdIndex = fdSelect.getFDIndex(i);
|
|
|
+ if (fdIndex === -1) {
|
|
|
+ warn('Glyph index is not in fd select.');
|
|
|
+ valid = false;
|
|
|
+ }
|
|
|
+ if (fdIndex >= fdArray.length) {
|
|
|
+ warn('Invalid fd index for glyph index.');
|
|
|
+ valid = false;
|
|
|
+ }
|
|
|
+ if (valid) {
|
|
|
+ localSubrToUse = fdArray[fdIndex].privateDict.subrsIndex;
|
|
|
+ }
|
|
|
+ } else if (localSubrIndex) {
|
|
|
+ localSubrToUse = localSubrIndex;
|
|
|
+ }
|
|
|
+ if (valid) {
|
|
|
+ valid = this.parseCharString(state, charstring, localSubrToUse,
|
|
|
+ globalSubrIndex);
|
|
|
+ }
|
|
|
+ if (state.width !== null) {
|
|
|
+ widths[i] = state.width;
|
|
|
+ }
|
|
|
+ if (state.seac !== null) {
|
|
|
+ seacs[i] = state.seac;
|
|
|
+ }
|
|
|
+ if (!valid) {
|
|
|
+ // resetting invalid charstring to single 'endchar'
|
|
|
+ charStrings.set(i, new Uint8Array([14]));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return { charStrings: charStrings, seacs: seacs, widths: widths };
|
|
|
+ },
|
|
|
+ emptyPrivateDictionary:
|
|
|
+ function CFFParser_emptyPrivateDictionary(parentDict) {
|
|
|
+ var privateDict = this.createDict(CFFPrivateDict, [],
|
|
|
+ parentDict.strings);
|
|
|
+ parentDict.setByKey(18, [0, 0]);
|
|
|
+ parentDict.privateDict = privateDict;
|
|
|
+ },
|
|
|
+ parsePrivateDict: function CFFParser_parsePrivateDict(parentDict) {
|
|
|
+ // no private dict, do nothing
|
|
|
+ if (!parentDict.hasName('Private')) {
|
|
|
+ this.emptyPrivateDictionary(parentDict);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ var privateOffset = parentDict.getByName('Private');
|
|
|
+ // make sure the params are formatted correctly
|
|
|
+ if (!isArray(privateOffset) || privateOffset.length !== 2) {
|
|
|
+ parentDict.removeByName('Private');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ var size = privateOffset[0];
|
|
|
+ var offset = privateOffset[1];
|
|
|
+ // remove empty dicts or ones that refer to invalid location
|
|
|
+ if (size === 0 || offset >= this.bytes.length) {
|
|
|
+ this.emptyPrivateDictionary(parentDict);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- cff.names = [name];
|
|
|
+ var privateDictEnd = offset + size;
|
|
|
+ var dictData = this.bytes.subarray(offset, privateDictEnd);
|
|
|
+ var dict = this.parseDict(dictData);
|
|
|
+ var privateDict = this.createDict(CFFPrivateDict, dict,
|
|
|
+ parentDict.strings);
|
|
|
+ parentDict.privateDict = privateDict;
|
|
|
|
|
|
- var topDict = new CFFTopDict();
|
|
|
- // CFF strings IDs 0...390 are predefined names, so refering
|
|
|
- // to entries in our own String INDEX starts at SID 391.
|
|
|
- topDict.setByName('version', 391);
|
|
|
- topDict.setByName('Notice', 392);
|
|
|
- topDict.setByName('FullName', 393);
|
|
|
- topDict.setByName('FamilyName', 394);
|
|
|
- topDict.setByName('Weight', 395);
|
|
|
- topDict.setByName('Encoding', null); // placeholder
|
|
|
- topDict.setByName('FontMatrix', properties.fontMatrix);
|
|
|
- topDict.setByName('FontBBox', properties.bbox);
|
|
|
- topDict.setByName('charset', null); // placeholder
|
|
|
- topDict.setByName('CharStrings', null); // placeholder
|
|
|
- topDict.setByName('Private', null); // placeholder
|
|
|
- cff.topDict = topDict;
|
|
|
+ // Parse the Subrs index also since it's relative to the private dict.
|
|
|
+ if (!privateDict.getByName('Subrs')) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ var subrsOffset = privateDict.getByName('Subrs');
|
|
|
+ var relativeOffset = offset + subrsOffset;
|
|
|
+ // Validate the offset.
|
|
|
+ if (subrsOffset === 0 || relativeOffset >= this.bytes.length) {
|
|
|
+ this.emptyPrivateDictionary(parentDict);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ var subrsIndex = this.parseIndex(relativeOffset);
|
|
|
+ privateDict.subrsIndex = subrsIndex.obj;
|
|
|
+ },
|
|
|
+ parseCharsets: function CFFParser_parseCharsets(pos, length, strings, cid) {
|
|
|
+ if (pos === 0) {
|
|
|
+ return new CFFCharset(true, CFFCharsetPredefinedTypes.ISO_ADOBE,
|
|
|
+ ISOAdobeCharset);
|
|
|
+ } else if (pos === 1) {
|
|
|
+ return new CFFCharset(true, CFFCharsetPredefinedTypes.EXPERT,
|
|
|
+ ExpertCharset);
|
|
|
+ } else if (pos === 2) {
|
|
|
+ return new CFFCharset(true, CFFCharsetPredefinedTypes.EXPERT_SUBSET,
|
|
|
+ ExpertSubsetCharset);
|
|
|
+ }
|
|
|
|
|
|
- var strings = new CFFStrings();
|
|
|
- strings.add('Version 0.11'); // Version
|
|
|
- strings.add('See original notice'); // Notice
|
|
|
- strings.add(name); // FullName
|
|
|
- strings.add(name); // FamilyName
|
|
|
- strings.add('Medium'); // Weight
|
|
|
- cff.strings = strings;
|
|
|
+ var bytes = this.bytes;
|
|
|
+ var start = pos;
|
|
|
+ var format = bytes[pos++];
|
|
|
+ var charset = ['.notdef'];
|
|
|
+ var id, count, i;
|
|
|
|
|
|
- cff.globalSubrIndex = new CFFIndex();
|
|
|
+ // subtract 1 for the .notdef glyph
|
|
|
+ length -= 1;
|
|
|
|
|
|
- var count = glyphs.length;
|
|
|
- var charsetArray = [0];
|
|
|
- var i, ii;
|
|
|
- for (i = 0; i < count; i++) {
|
|
|
- var index = CFFStandardStrings.indexOf(charstrings[i].glyphName);
|
|
|
- // TODO: Insert the string and correctly map it. Previously it was
|
|
|
- // thought mapping names that aren't in the standard strings to .notdef
|
|
|
- // was fine, however in issue818 when mapping them all to .notdef the
|
|
|
- // adieresis glyph no longer worked.
|
|
|
- if (index === -1) {
|
|
|
- index = 0;
|
|
|
+ switch (format) {
|
|
|
+ case 0:
|
|
|
+ for (i = 0; i < length; i++) {
|
|
|
+ id = (bytes[pos++] << 8) | bytes[pos++];
|
|
|
+ charset.push(cid ? id : strings.get(id));
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 1:
|
|
|
+ while (charset.length <= length) {
|
|
|
+ id = (bytes[pos++] << 8) | bytes[pos++];
|
|
|
+ count = bytes[pos++];
|
|
|
+ for (i = 0; i <= count; i++) {
|
|
|
+ charset.push(cid ? id++ : strings.get(id++));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 2:
|
|
|
+ while (charset.length <= length) {
|
|
|
+ id = (bytes[pos++] << 8) | bytes[pos++];
|
|
|
+ count = (bytes[pos++] << 8) | bytes[pos++];
|
|
|
+ for (i = 0; i <= count; i++) {
|
|
|
+ charset.push(cid ? id++ : strings.get(id++));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ error('Unknown charset format');
|
|
|
}
|
|
|
- charsetArray.push((index >> 8) & 0xff, index & 0xff);
|
|
|
- }
|
|
|
- cff.charset = new CFFCharset(false, 0, [], charsetArray);
|
|
|
+ // Raw won't be needed if we actually compile the charset.
|
|
|
+ var end = pos;
|
|
|
+ var raw = bytes.subarray(start, end);
|
|
|
|
|
|
- var charStringsIndex = new CFFIndex();
|
|
|
- charStringsIndex.add([0x8B, 0x0E]); // .notdef
|
|
|
- for (i = 0; i < count; i++) {
|
|
|
- charStringsIndex.add(glyphs[i]);
|
|
|
- }
|
|
|
- cff.charStrings = charStringsIndex;
|
|
|
+ return new CFFCharset(false, format, charset, raw);
|
|
|
+ },
|
|
|
+ parseEncoding: function CFFParser_parseEncoding(pos,
|
|
|
+ properties,
|
|
|
+ strings,
|
|
|
+ charset) {
|
|
|
+ var encoding = Object.create(null);
|
|
|
+ var bytes = this.bytes;
|
|
|
+ var predefined = false;
|
|
|
+ var hasSupplement = false;
|
|
|
+ var format, i, ii;
|
|
|
+ var raw = null;
|
|
|
|
|
|
- var privateDict = new CFFPrivateDict();
|
|
|
- privateDict.setByName('Subrs', null); // placeholder
|
|
|
- var fields = [
|
|
|
- 'BlueValues',
|
|
|
- 'OtherBlues',
|
|
|
- 'FamilyBlues',
|
|
|
- 'FamilyOtherBlues',
|
|
|
- 'StemSnapH',
|
|
|
- 'StemSnapV',
|
|
|
- 'BlueShift',
|
|
|
- 'BlueFuzz',
|
|
|
- 'BlueScale',
|
|
|
- 'LanguageGroup',
|
|
|
- 'ExpansionFactor',
|
|
|
- 'ForceBold',
|
|
|
- 'StdHW',
|
|
|
- 'StdVW'
|
|
|
- ];
|
|
|
- for (i = 0, ii = fields.length; i < ii; i++) {
|
|
|
- var field = fields[i];
|
|
|
- if (!(field in properties.privateData)) {
|
|
|
- continue;
|
|
|
+ function readSupplement() {
|
|
|
+ var supplementsCount = bytes[pos++];
|
|
|
+ for (i = 0; i < supplementsCount; i++) {
|
|
|
+ var code = bytes[pos++];
|
|
|
+ var sid = (bytes[pos++] << 8) + (bytes[pos++] & 0xff);
|
|
|
+ encoding[code] = charset.indexOf(strings.get(sid));
|
|
|
+ }
|
|
|
}
|
|
|
- var value = properties.privateData[field];
|
|
|
- if (isArray(value)) {
|
|
|
- // All of the private dictionary array data in CFF must be stored as
|
|
|
- // "delta-encoded" numbers.
|
|
|
- for (var j = value.length - 1; j > 0; j--) {
|
|
|
- value[j] -= value[j - 1]; // ... difference from previous value
|
|
|
+
|
|
|
+ if (pos === 0 || pos === 1) {
|
|
|
+ predefined = true;
|
|
|
+ format = pos;
|
|
|
+ var baseEncoding = pos ? ExpertEncoding : StandardEncoding;
|
|
|
+ for (i = 0, ii = charset.length; i < ii; i++) {
|
|
|
+ var index = baseEncoding.indexOf(charset[i]);
|
|
|
+ if (index !== -1) {
|
|
|
+ encoding[index] = i;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ var dataStart = pos;
|
|
|
+ format = bytes[pos++];
|
|
|
+ switch (format & 0x7f) {
|
|
|
+ case 0:
|
|
|
+ var glyphsCount = bytes[pos++];
|
|
|
+ for (i = 1; i <= glyphsCount; i++) {
|
|
|
+ encoding[bytes[pos++]] = i;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 1:
|
|
|
+ var rangesCount = bytes[pos++];
|
|
|
+ var gid = 1;
|
|
|
+ for (i = 0; i < rangesCount; i++) {
|
|
|
+ var start = bytes[pos++];
|
|
|
+ var left = bytes[pos++];
|
|
|
+ for (var j = start; j <= start + left; j++) {
|
|
|
+ encoding[j] = gid++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ default:
|
|
|
+ error('Unknow encoding format: ' + format + ' in CFF');
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ var dataEnd = pos;
|
|
|
+ if (format & 0x80) {
|
|
|
+ // The font sanitizer does not support CFF encoding with a
|
|
|
+ // supplement, since the encoding is not really used to map
|
|
|
+ // between gid to glyph, let's overwrite what is declared in
|
|
|
+ // the top dictionary to let the sanitizer think the font use
|
|
|
+ // StandardEncoding, that's a lie but that's ok.
|
|
|
+ bytes[dataStart] &= 0x7f;
|
|
|
+ readSupplement();
|
|
|
+ hasSupplement = true;
|
|
|
}
|
|
|
+ raw = bytes.subarray(dataStart, dataEnd);
|
|
|
}
|
|
|
- privateDict.setByName(field, value);
|
|
|
- }
|
|
|
- cff.topDict.privateDict = privateDict;
|
|
|
+ format = format & 0x7f;
|
|
|
+ return new CFFEncoding(predefined, format, encoding, raw);
|
|
|
+ },
|
|
|
+ parseFDSelect: function CFFParser_parseFDSelect(pos, length) {
|
|
|
+ var start = pos;
|
|
|
+ var bytes = this.bytes;
|
|
|
+ var format = bytes[pos++];
|
|
|
+ var fdSelect = [];
|
|
|
+ var i;
|
|
|
|
|
|
- var subrIndex = new CFFIndex();
|
|
|
- for (i = 0, ii = subrs.length; i < ii; i++) {
|
|
|
- subrIndex.add(subrs[i]);
|
|
|
+ switch (format) {
|
|
|
+ case 0:
|
|
|
+ for (i = 0; i < length; ++i) {
|
|
|
+ var id = bytes[pos++];
|
|
|
+ fdSelect.push(id);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 3:
|
|
|
+ var rangesCount = (bytes[pos++] << 8) | bytes[pos++];
|
|
|
+ for (i = 0; i < rangesCount; ++i) {
|
|
|
+ var first = (bytes[pos++] << 8) | bytes[pos++];
|
|
|
+ var fdIndex = bytes[pos++];
|
|
|
+ var next = (bytes[pos] << 8) | bytes[pos + 1];
|
|
|
+ for (var j = first; j < next; ++j) {
|
|
|
+ fdSelect.push(fdIndex);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Advance past the sentinel(next).
|
|
|
+ pos += 2;
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ error('Unknown fdselect format ' + format);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ var end = pos;
|
|
|
+ return new CFFFDSelect(fdSelect, bytes.subarray(start, end));
|
|
|
}
|
|
|
- privateDict.subrsIndex = subrIndex;
|
|
|
+ };
|
|
|
+ return CFFParser;
|
|
|
+})();
|
|
|
|
|
|
- var compiler = new CFFCompiler(cff);
|
|
|
- return compiler.compile();
|
|
|
+// Compact Font Format
|
|
|
+var CFF = (function CFFClosure() {
|
|
|
+ function CFF() {
|
|
|
+ this.header = null;
|
|
|
+ this.names = [];
|
|
|
+ this.topDict = null;
|
|
|
+ this.strings = new CFFStrings();
|
|
|
+ this.globalSubrIndex = null;
|
|
|
+
|
|
|
+ // The following could really be per font, but since we only have one font
|
|
|
+ // store them here.
|
|
|
+ this.encoding = null;
|
|
|
+ this.charset = null;
|
|
|
+ this.charStrings = null;
|
|
|
+ this.fdArray = [];
|
|
|
+ this.fdSelect = null;
|
|
|
+
|
|
|
+ this.isCIDFont = false;
|
|
|
}
|
|
|
-};
|
|
|
+ return CFF;
|
|
|
+})();
|
|
|
|
|
|
-var CFFFont = (function CFFFontClosure() {
|
|
|
- function CFFFont(file, properties) {
|
|
|
- this.properties = properties;
|
|
|
+var CFFHeader = (function CFFHeaderClosure() {
|
|
|
+ function CFFHeader(major, minor, hdrSize, offSize) {
|
|
|
+ this.major = major;
|
|
|
+ this.minor = minor;
|
|
|
+ this.hdrSize = hdrSize;
|
|
|
+ this.offSize = offSize;
|
|
|
+ }
|
|
|
+ return CFFHeader;
|
|
|
+})();
|
|
|
|
|
|
- var parser = new CFFParser(file, properties);
|
|
|
- this.cff = parser.parse();
|
|
|
- var compiler = new CFFCompiler(this.cff);
|
|
|
- this.seacs = this.cff.seacs;
|
|
|
- try {
|
|
|
- this.data = compiler.compile();
|
|
|
- } catch (e) {
|
|
|
- warn('Failed to compile font ' + properties.loadedName);
|
|
|
- // There may have just been an issue with the compiler, set the data
|
|
|
- // anyway and hope the font loaded.
|
|
|
- this.data = file;
|
|
|
- }
|
|
|
+var CFFStrings = (function CFFStringsClosure() {
|
|
|
+ function CFFStrings() {
|
|
|
+ this.strings = [];
|
|
|
}
|
|
|
+ CFFStrings.prototype = {
|
|
|
+ get: function CFFStrings_get(index) {
|
|
|
+ if (index >= 0 && index <= 390) {
|
|
|
+ return CFFStandardStrings[index];
|
|
|
+ }
|
|
|
+ if (index - 391 <= this.strings.length) {
|
|
|
+ return this.strings[index - 391];
|
|
|
+ }
|
|
|
+ return CFFStandardStrings[0];
|
|
|
+ },
|
|
|
+ add: function CFFStrings_add(value) {
|
|
|
+ this.strings.push(value);
|
|
|
+ },
|
|
|
+ get count() {
|
|
|
+ return this.strings.length;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ return CFFStrings;
|
|
|
+})();
|
|
|
|
|
|
- CFFFont.prototype = {
|
|
|
- get numGlyphs() {
|
|
|
- return this.cff.charStrings.count;
|
|
|
+var CFFIndex = (function CFFIndexClosure() {
|
|
|
+ function CFFIndex() {
|
|
|
+ this.objects = [];
|
|
|
+ this.length = 0;
|
|
|
+ }
|
|
|
+ CFFIndex.prototype = {
|
|
|
+ add: function CFFIndex_add(data) {
|
|
|
+ this.length += data.length;
|
|
|
+ this.objects.push(data);
|
|
|
},
|
|
|
- getCharset: function CFFFont_getCharset() {
|
|
|
- return this.cff.charset.charset;
|
|
|
+ set: function CFFIndex_set(index, data) {
|
|
|
+ this.length += data.length - this.objects[index].length;
|
|
|
+ this.objects[index] = data;
|
|
|
},
|
|
|
- getGlyphMapping: function CFFFont_getGlyphMapping() {
|
|
|
- var cff = this.cff;
|
|
|
- var properties = this.properties;
|
|
|
- var charsets = cff.charset.charset;
|
|
|
- var charCodeToGlyphId;
|
|
|
- var glyphId;
|
|
|
+ get: function CFFIndex_get(index) {
|
|
|
+ return this.objects[index];
|
|
|
+ },
|
|
|
+ get count() {
|
|
|
+ return this.objects.length;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ return CFFIndex;
|
|
|
+})();
|
|
|
|
|
|
- if (properties.composite) {
|
|
|
- charCodeToGlyphId = Object.create(null);
|
|
|
- if (cff.isCIDFont) {
|
|
|
- // If the font is actually a CID font then we should use the charset
|
|
|
- // to map CIDs to GIDs.
|
|
|
- for (glyphId = 0; glyphId < charsets.length; glyphId++) {
|
|
|
- var cid = charsets[glyphId];
|
|
|
- var charCode = properties.cMap.charCodeOf(cid);
|
|
|
- charCodeToGlyphId[charCode] = glyphId;
|
|
|
- }
|
|
|
- } else {
|
|
|
- // If it is NOT actually a CID font then CIDs should be mapped
|
|
|
- // directly to GIDs.
|
|
|
- for (glyphId = 0; glyphId < cff.charStrings.count; glyphId++) {
|
|
|
- charCodeToGlyphId[glyphId] = glyphId;
|
|
|
- }
|
|
|
- }
|
|
|
- return charCodeToGlyphId;
|
|
|
+var CFFDict = (function CFFDictClosure() {
|
|
|
+ function CFFDict(tables, strings) {
|
|
|
+ this.keyToNameMap = tables.keyToNameMap;
|
|
|
+ this.nameToKeyMap = tables.nameToKeyMap;
|
|
|
+ this.defaults = tables.defaults;
|
|
|
+ this.types = tables.types;
|
|
|
+ this.opcodes = tables.opcodes;
|
|
|
+ this.order = tables.order;
|
|
|
+ this.strings = strings;
|
|
|
+ this.values = Object.create(null);
|
|
|
+ }
|
|
|
+ CFFDict.prototype = {
|
|
|
+ // value should always be an array
|
|
|
+ setByKey: function CFFDict_setByKey(key, value) {
|
|
|
+ if (!(key in this.keyToNameMap)) {
|
|
|
+ return false;
|
|
|
}
|
|
|
-
|
|
|
- var encoding = cff.encoding ? cff.encoding.encoding : null;
|
|
|
- charCodeToGlyphId = type1FontGlyphMapping(properties, encoding, charsets);
|
|
|
- return charCodeToGlyphId;
|
|
|
+ // ignore empty values
|
|
|
+ if (value.length === 0) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ var type = this.types[key];
|
|
|
+ // remove the array wrapping these types of values
|
|
|
+ if (type === 'num' || type === 'sid' || type === 'offset') {
|
|
|
+ value = value[0];
|
|
|
+ }
|
|
|
+ this.values[key] = value;
|
|
|
+ return true;
|
|
|
+ },
|
|
|
+ setByName: function CFFDict_setByName(name, value) {
|
|
|
+ if (!(name in this.nameToKeyMap)) {
|
|
|
+ error('Invalid dictionary name "' + name + '"');
|
|
|
+ }
|
|
|
+ this.values[this.nameToKeyMap[name]] = value;
|
|
|
+ },
|
|
|
+ hasName: function CFFDict_hasName(name) {
|
|
|
+ return this.nameToKeyMap[name] in this.values;
|
|
|
+ },
|
|
|
+ getByName: function CFFDict_getByName(name) {
|
|
|
+ if (!(name in this.nameToKeyMap)) {
|
|
|
+ error('Invalid dictionary name "' + name + '"');
|
|
|
+ }
|
|
|
+ var key = this.nameToKeyMap[name];
|
|
|
+ if (!(key in this.values)) {
|
|
|
+ return this.defaults[key];
|
|
|
+ }
|
|
|
+ return this.values[key];
|
|
|
+ },
|
|
|
+ removeByName: function CFFDict_removeByName(name) {
|
|
|
+ delete this.values[this.nameToKeyMap[name]];
|
|
|
+ }
|
|
|
+ };
|
|
|
+ CFFDict.createTables = function CFFDict_createTables(layout) {
|
|
|
+ var tables = {
|
|
|
+ keyToNameMap: {},
|
|
|
+ nameToKeyMap: {},
|
|
|
+ defaults: {},
|
|
|
+ types: {},
|
|
|
+ opcodes: {},
|
|
|
+ order: []
|
|
|
+ };
|
|
|
+ for (var i = 0, ii = layout.length; i < ii; ++i) {
|
|
|
+ var entry = layout[i];
|
|
|
+ var key = isArray(entry[0]) ? (entry[0][0] << 8) + entry[0][1] : entry[0];
|
|
|
+ tables.keyToNameMap[key] = entry[1];
|
|
|
+ tables.nameToKeyMap[entry[1]] = key;
|
|
|
+ tables.types[key] = entry[2];
|
|
|
+ tables.defaults[key] = entry[3];
|
|
|
+ tables.opcodes[key] = isArray(entry[0]) ? entry[0] : [entry[0]];
|
|
|
+ tables.order.push(key);
|
|
|
}
|
|
|
+ return tables;
|
|
|
};
|
|
|
+ return CFFDict;
|
|
|
+})();
|
|
|
|
|
|
- return CFFFont;
|
|
|
+var CFFTopDict = (function CFFTopDictClosure() {
|
|
|
+ var layout = [
|
|
|
+ [[12, 30], 'ROS', ['sid', 'sid', 'num'], null],
|
|
|
+ [[12, 20], 'SyntheticBase', 'num', null],
|
|
|
+ [0, 'version', 'sid', null],
|
|
|
+ [1, 'Notice', 'sid', null],
|
|
|
+ [[12, 0], 'Copyright', 'sid', null],
|
|
|
+ [2, 'FullName', 'sid', null],
|
|
|
+ [3, 'FamilyName', 'sid', null],
|
|
|
+ [4, 'Weight', 'sid', null],
|
|
|
+ [[12, 1], 'isFixedPitch', 'num', 0],
|
|
|
+ [[12, 2], 'ItalicAngle', 'num', 0],
|
|
|
+ [[12, 3], 'UnderlinePosition', 'num', -100],
|
|
|
+ [[12, 4], 'UnderlineThickness', 'num', 50],
|
|
|
+ [[12, 5], 'PaintType', 'num', 0],
|
|
|
+ [[12, 6], 'CharstringType', 'num', 2],
|
|
|
+ [[12, 7], 'FontMatrix', ['num', 'num', 'num', 'num', 'num', 'num'],
|
|
|
+ [0.001, 0, 0, 0.001, 0, 0]],
|
|
|
+ [13, 'UniqueID', 'num', null],
|
|
|
+ [5, 'FontBBox', ['num', 'num', 'num', 'num'], [0, 0, 0, 0]],
|
|
|
+ [[12, 8], 'StrokeWidth', 'num', 0],
|
|
|
+ [14, 'XUID', 'array', null],
|
|
|
+ [15, 'charset', 'offset', 0],
|
|
|
+ [16, 'Encoding', 'offset', 0],
|
|
|
+ [17, 'CharStrings', 'offset', 0],
|
|
|
+ [18, 'Private', ['offset', 'offset'], null],
|
|
|
+ [[12, 21], 'PostScript', 'sid', null],
|
|
|
+ [[12, 22], 'BaseFontName', 'sid', null],
|
|
|
+ [[12, 23], 'BaseFontBlend', 'delta', null],
|
|
|
+ [[12, 31], 'CIDFontVersion', 'num', 0],
|
|
|
+ [[12, 32], 'CIDFontRevision', 'num', 0],
|
|
|
+ [[12, 33], 'CIDFontType', 'num', 0],
|
|
|
+ [[12, 34], 'CIDCount', 'num', 8720],
|
|
|
+ [[12, 35], 'UIDBase', 'num', null],
|
|
|
+ // XXX: CID Fonts on DirectWrite 6.1 only seem to work if FDSelect comes
|
|
|
+ // before FDArray.
|
|
|
+ [[12, 37], 'FDSelect', 'offset', null],
|
|
|
+ [[12, 36], 'FDArray', 'offset', null],
|
|
|
+ [[12, 38], 'FontName', 'sid', null]
|
|
|
+ ];
|
|
|
+ var tables = null;
|
|
|
+ function CFFTopDict(strings) {
|
|
|
+ if (tables === null) {
|
|
|
+ tables = CFFDict.createTables(layout);
|
|
|
+ }
|
|
|
+ CFFDict.call(this, tables, strings);
|
|
|
+ this.privateDict = null;
|
|
|
+ }
|
|
|
+ CFFTopDict.prototype = Object.create(CFFDict.prototype);
|
|
|
+ return CFFTopDict;
|
|
|
})();
|
|
|
|
|
|
-var CFFParser = (function CFFParserClosure() {
|
|
|
- var CharstringValidationData = [
|
|
|
- null,
|
|
|
- { id: 'hstem', min: 2, stackClearing: true, stem: true },
|
|
|
- null,
|
|
|
- { id: 'vstem', min: 2, stackClearing: true, stem: true },
|
|
|
- { id: 'vmoveto', min: 1, stackClearing: true },
|
|
|
- { id: 'rlineto', min: 2, resetStack: true },
|
|
|
- { id: 'hlineto', min: 1, resetStack: true },
|
|
|
- { id: 'vlineto', min: 1, resetStack: true },
|
|
|
- { id: 'rrcurveto', min: 6, resetStack: true },
|
|
|
- null,
|
|
|
- { id: 'callsubr', min: 1, undefStack: true },
|
|
|
- { id: 'return', min: 0, undefStack: true },
|
|
|
- null, // 12
|
|
|
- null,
|
|
|
- { id: 'endchar', min: 0, stackClearing: true },
|
|
|
- null,
|
|
|
- null,
|
|
|
- null,
|
|
|
- { id: 'hstemhm', min: 2, stackClearing: true, stem: true },
|
|
|
- { id: 'hintmask', min: 0, stackClearing: true },
|
|
|
- { id: 'cntrmask', min: 0, stackClearing: true },
|
|
|
- { id: 'rmoveto', min: 2, stackClearing: true },
|
|
|
- { id: 'hmoveto', min: 1, stackClearing: true },
|
|
|
- { id: 'vstemhm', min: 2, stackClearing: true, stem: true },
|
|
|
- { id: 'rcurveline', min: 8, resetStack: true },
|
|
|
- { id: 'rlinecurve', min: 8, resetStack: true },
|
|
|
- { id: 'vvcurveto', min: 4, resetStack: true },
|
|
|
- { id: 'hhcurveto', min: 4, resetStack: true },
|
|
|
- null, // shortint
|
|
|
- { id: 'callgsubr', min: 1, undefStack: true },
|
|
|
- { id: 'vhcurveto', min: 4, resetStack: true },
|
|
|
- { id: 'hvcurveto', min: 4, resetStack: true }
|
|
|
+var CFFPrivateDict = (function CFFPrivateDictClosure() {
|
|
|
+ var layout = [
|
|
|
+ [6, 'BlueValues', 'delta', null],
|
|
|
+ [7, 'OtherBlues', 'delta', null],
|
|
|
+ [8, 'FamilyBlues', 'delta', null],
|
|
|
+ [9, 'FamilyOtherBlues', 'delta', null],
|
|
|
+ [[12, 9], 'BlueScale', 'num', 0.039625],
|
|
|
+ [[12, 10], 'BlueShift', 'num', 7],
|
|
|
+ [[12, 11], 'BlueFuzz', 'num', 1],
|
|
|
+ [10, 'StdHW', 'num', null],
|
|
|
+ [11, 'StdVW', 'num', null],
|
|
|
+ [[12, 12], 'StemSnapH', 'delta', null],
|
|
|
+ [[12, 13], 'StemSnapV', 'delta', null],
|
|
|
+ [[12, 14], 'ForceBold', 'num', 0],
|
|
|
+ [[12, 17], 'LanguageGroup', 'num', 0],
|
|
|
+ [[12, 18], 'ExpansionFactor', 'num', 0.06],
|
|
|
+ [[12, 19], 'initialRandomSeed', 'num', 0],
|
|
|
+ [20, 'defaultWidthX', 'num', 0],
|
|
|
+ [21, 'nominalWidthX', 'num', 0],
|
|
|
+ [19, 'Subrs', 'offset', null]
|
|
|
];
|
|
|
- var CharstringValidationData12 = [
|
|
|
- null,
|
|
|
- null,
|
|
|
- null,
|
|
|
- { id: 'and', min: 2, stackDelta: -1 },
|
|
|
- { id: 'or', min: 2, stackDelta: -1 },
|
|
|
- { id: 'not', min: 1, stackDelta: 0 },
|
|
|
- null,
|
|
|
- null,
|
|
|
- null,
|
|
|
- { id: 'abs', min: 1, stackDelta: 0 },
|
|
|
- { id: 'add', min: 2, stackDelta: -1,
|
|
|
- stackFn: function stack_div(stack, index) {
|
|
|
- stack[index - 2] = stack[index - 2] + stack[index - 1];
|
|
|
+ var tables = null;
|
|
|
+ function CFFPrivateDict(strings) {
|
|
|
+ if (tables === null) {
|
|
|
+ tables = CFFDict.createTables(layout);
|
|
|
+ }
|
|
|
+ CFFDict.call(this, tables, strings);
|
|
|
+ this.subrsIndex = null;
|
|
|
+ }
|
|
|
+ CFFPrivateDict.prototype = Object.create(CFFDict.prototype);
|
|
|
+ return CFFPrivateDict;
|
|
|
+})();
|
|
|
+
|
|
|
+var CFFCharsetPredefinedTypes = {
|
|
|
+ ISO_ADOBE: 0,
|
|
|
+ EXPERT: 1,
|
|
|
+ EXPERT_SUBSET: 2
|
|
|
+};
|
|
|
+var CFFCharset = (function CFFCharsetClosure() {
|
|
|
+ function CFFCharset(predefined, format, charset, raw) {
|
|
|
+ this.predefined = predefined;
|
|
|
+ this.format = format;
|
|
|
+ this.charset = charset;
|
|
|
+ this.raw = raw;
|
|
|
+ }
|
|
|
+ return CFFCharset;
|
|
|
+})();
|
|
|
+
|
|
|
+var CFFEncoding = (function CFFEncodingClosure() {
|
|
|
+ function CFFEncoding(predefined, format, encoding, raw) {
|
|
|
+ this.predefined = predefined;
|
|
|
+ this.format = format;
|
|
|
+ this.encoding = encoding;
|
|
|
+ this.raw = raw;
|
|
|
+ }
|
|
|
+ return CFFEncoding;
|
|
|
+})();
|
|
|
+
|
|
|
+var CFFFDSelect = (function CFFFDSelectClosure() {
|
|
|
+ function CFFFDSelect(fdSelect, raw) {
|
|
|
+ this.fdSelect = fdSelect;
|
|
|
+ this.raw = raw;
|
|
|
+ }
|
|
|
+ CFFFDSelect.prototype = {
|
|
|
+ getFDIndex: function CFFFDSelect_get(glyphIndex) {
|
|
|
+ if (glyphIndex < 0 || glyphIndex >= this.fdSelect.length) {
|
|
|
+ return -1;
|
|
|
}
|
|
|
+ return this.fdSelect[glyphIndex];
|
|
|
+ }
|
|
|
+ };
|
|
|
+ return CFFFDSelect;
|
|
|
+})();
|
|
|
+
|
|
|
+// Helper class to keep track of where an offset is within the data and helps
|
|
|
+// filling in that offset once it's known.
|
|
|
+var CFFOffsetTracker = (function CFFOffsetTrackerClosure() {
|
|
|
+ function CFFOffsetTracker() {
|
|
|
+ this.offsets = Object.create(null);
|
|
|
+ }
|
|
|
+ CFFOffsetTracker.prototype = {
|
|
|
+ isTracking: function CFFOffsetTracker_isTracking(key) {
|
|
|
+ return key in this.offsets;
|
|
|
},
|
|
|
- { id: 'sub', min: 2, stackDelta: -1,
|
|
|
- stackFn: function stack_div(stack, index) {
|
|
|
- stack[index - 2] = stack[index - 2] - stack[index - 1];
|
|
|
+ track: function CFFOffsetTracker_track(key, location) {
|
|
|
+ if (key in this.offsets) {
|
|
|
+ error('Already tracking location of ' + key);
|
|
|
}
|
|
|
+ this.offsets[key] = location;
|
|
|
},
|
|
|
- { id: 'div', min: 2, stackDelta: -1,
|
|
|
- stackFn: function stack_div(stack, index) {
|
|
|
- stack[index - 2] = stack[index - 2] / stack[index - 1];
|
|
|
+ offset: function CFFOffsetTracker_offset(value) {
|
|
|
+ for (var key in this.offsets) {
|
|
|
+ this.offsets[key] += value;
|
|
|
}
|
|
|
},
|
|
|
- null,
|
|
|
- { id: 'neg', min: 1, stackDelta: 0,
|
|
|
- stackFn: function stack_div(stack, index) {
|
|
|
- stack[index - 1] = -stack[index - 1];
|
|
|
+ setEntryLocation: function CFFOffsetTracker_setEntryLocation(key,
|
|
|
+ values,
|
|
|
+ output) {
|
|
|
+ if (!(key in this.offsets)) {
|
|
|
+ error('Not tracking location of ' + key);
|
|
|
}
|
|
|
- },
|
|
|
- { id: 'eq', min: 2, stackDelta: -1 },
|
|
|
- null,
|
|
|
- null,
|
|
|
- { id: 'drop', min: 1, stackDelta: -1 },
|
|
|
- null,
|
|
|
- { id: 'put', min: 2, stackDelta: -2 },
|
|
|
- { id: 'get', min: 1, stackDelta: 0 },
|
|
|
- { id: 'ifelse', min: 4, stackDelta: -3 },
|
|
|
- { id: 'random', min: 0, stackDelta: 1 },
|
|
|
- { id: 'mul', min: 2, stackDelta: -1,
|
|
|
- stackFn: function stack_div(stack, index) {
|
|
|
- stack[index - 2] = stack[index - 2] * stack[index - 1];
|
|
|
+ var data = output.data;
|
|
|
+ var dataOffset = this.offsets[key];
|
|
|
+ var size = 5;
|
|
|
+ for (var i = 0, ii = values.length; i < ii; ++i) {
|
|
|
+ var offset0 = i * size + dataOffset;
|
|
|
+ var offset1 = offset0 + 1;
|
|
|
+ var offset2 = offset0 + 2;
|
|
|
+ var offset3 = offset0 + 3;
|
|
|
+ var offset4 = offset0 + 4;
|
|
|
+ // It's easy to screw up offsets so perform this sanity check.
|
|
|
+ if (data[offset0] !== 0x1d || data[offset1] !== 0 ||
|
|
|
+ data[offset2] !== 0 || data[offset3] !== 0 || data[offset4] !== 0) {
|
|
|
+ error('writing to an offset that is not empty');
|
|
|
+ }
|
|
|
+ var value = values[i];
|
|
|
+ data[offset0] = 0x1d;
|
|
|
+ data[offset1] = (value >> 24) & 0xFF;
|
|
|
+ data[offset2] = (value >> 16) & 0xFF;
|
|
|
+ data[offset3] = (value >> 8) & 0xFF;
|
|
|
+ data[offset4] = value & 0xFF;
|
|
|
}
|
|
|
- },
|
|
|
- null,
|
|
|
- { id: 'sqrt', min: 1, stackDelta: 0 },
|
|
|
- { id: 'dup', min: 1, stackDelta: 1 },
|
|
|
- { id: 'exch', min: 2, stackDelta: 0 },
|
|
|
- { id: 'index', min: 2, stackDelta: 0 },
|
|
|
- { id: 'roll', min: 3, stackDelta: -2 },
|
|
|
- null,
|
|
|
- null,
|
|
|
- null,
|
|
|
- { id: 'hflex', min: 7, resetStack: true },
|
|
|
- { id: 'flex', min: 13, resetStack: true },
|
|
|
- { id: 'hflex1', min: 9, resetStack: true },
|
|
|
- { id: 'flex1', min: 11, resetStack: true }
|
|
|
- ];
|
|
|
+ }
|
|
|
+ };
|
|
|
+ return CFFOffsetTracker;
|
|
|
+})();
|
|
|
|
|
|
- function CFFParser(file, properties) {
|
|
|
- this.bytes = file.getBytes();
|
|
|
- this.properties = properties;
|
|
|
+// Takes a CFF and converts it to the binary representation.
|
|
|
+var CFFCompiler = (function CFFCompilerClosure() {
|
|
|
+ function CFFCompiler(cff) {
|
|
|
+ this.cff = cff;
|
|
|
}
|
|
|
- CFFParser.prototype = {
|
|
|
- parse: function CFFParser_parse() {
|
|
|
- var properties = this.properties;
|
|
|
- var cff = new CFF();
|
|
|
- this.cff = cff;
|
|
|
+ CFFCompiler.prototype = {
|
|
|
+ compile: function CFFCompiler_compile() {
|
|
|
+ var cff = this.cff;
|
|
|
+ var output = {
|
|
|
+ data: [],
|
|
|
+ length: 0,
|
|
|
+ add: function CFFCompiler_add(data) {
|
|
|
+ this.data = this.data.concat(data);
|
|
|
+ this.length = this.data.length;
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- // The first five sections must be in order, all the others are reached
|
|
|
- // via offsets contained in one of the below.
|
|
|
- var header = this.parseHeader();
|
|
|
- var nameIndex = this.parseIndex(header.endPos);
|
|
|
- var topDictIndex = this.parseIndex(nameIndex.endPos);
|
|
|
- var stringIndex = this.parseIndex(topDictIndex.endPos);
|
|
|
- var globalSubrIndex = this.parseIndex(stringIndex.endPos);
|
|
|
+ // Compile the five entries that must be in order.
|
|
|
+ var header = this.compileHeader(cff.header);
|
|
|
+ output.add(header);
|
|
|
|
|
|
- var topDictParsed = this.parseDict(topDictIndex.obj.get(0));
|
|
|
- var topDict = this.createDict(CFFTopDict, topDictParsed, cff.strings);
|
|
|
+ var nameIndex = this.compileNameIndex(cff.names);
|
|
|
+ output.add(nameIndex);
|
|
|
|
|
|
- cff.header = header.obj;
|
|
|
- cff.names = this.parseNameIndex(nameIndex.obj);
|
|
|
- cff.strings = this.parseStringIndex(stringIndex.obj);
|
|
|
- cff.topDict = topDict;
|
|
|
- cff.globalSubrIndex = globalSubrIndex.obj;
|
|
|
+ if (cff.isCIDFont) {
|
|
|
+ // The spec is unclear on how font matrices should relate to each other
|
|
|
+ // when there is one in the main top dict and the sub top dicts.
|
|
|
+ // Windows handles this differently than linux and osx so we have to
|
|
|
+ // normalize to work on all.
|
|
|
+ // Rules based off of some mailing list discussions:
|
|
|
+ // - If main font has a matrix and subfont doesn't, use the main matrix.
|
|
|
+ // - If no main font matrix and there is a subfont matrix, use the
|
|
|
+ // subfont matrix.
|
|
|
+ // - If both have matrices, concat together.
|
|
|
+ // - If neither have matrices, use default.
|
|
|
+ // To make this work on all platforms we move the top matrix into each
|
|
|
+ // sub top dict and concat if necessary.
|
|
|
+ if (cff.topDict.hasName('FontMatrix')) {
|
|
|
+ var base = cff.topDict.getByName('FontMatrix');
|
|
|
+ cff.topDict.removeByName('FontMatrix');
|
|
|
+ for (var i = 0, ii = cff.fdArray.length; i < ii; i++) {
|
|
|
+ var subDict = cff.fdArray[i];
|
|
|
+ var matrix = base.slice(0);
|
|
|
+ if (subDict.hasName('FontMatrix')) {
|
|
|
+ matrix = Util.transform(matrix, subDict.getByName('FontMatrix'));
|
|
|
+ }
|
|
|
+ subDict.setByName('FontMatrix', matrix);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- this.parsePrivateDict(cff.topDict);
|
|
|
+ var compiled = this.compileTopDicts([cff.topDict],
|
|
|
+ output.length,
|
|
|
+ cff.isCIDFont);
|
|
|
+ output.add(compiled.output);
|
|
|
+ var topDictTracker = compiled.trackers[0];
|
|
|
|
|
|
- cff.isCIDFont = topDict.hasName('ROS');
|
|
|
+ var stringIndex = this.compileStringIndex(cff.strings.strings);
|
|
|
+ output.add(stringIndex);
|
|
|
|
|
|
- var charStringOffset = topDict.getByName('CharStrings');
|
|
|
- var charStringIndex = this.parseIndex(charStringOffset).obj;
|
|
|
+ var globalSubrIndex = this.compileIndex(cff.globalSubrIndex);
|
|
|
+ output.add(globalSubrIndex);
|
|
|
|
|
|
- var fontMatrix = topDict.getByName('FontMatrix');
|
|
|
- if (fontMatrix) {
|
|
|
- properties.fontMatrix = fontMatrix;
|
|
|
+ // Now start on the other entries that have no specfic order.
|
|
|
+ if (cff.encoding && cff.topDict.hasName('Encoding')) {
|
|
|
+ if (cff.encoding.predefined) {
|
|
|
+ topDictTracker.setEntryLocation('Encoding', [cff.encoding.format],
|
|
|
+ output);
|
|
|
+ } else {
|
|
|
+ var encoding = this.compileEncoding(cff.encoding);
|
|
|
+ topDictTracker.setEntryLocation('Encoding', [output.length], output);
|
|
|
+ output.add(encoding);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- var fontBBox = topDict.getByName('FontBBox');
|
|
|
- if (fontBBox) {
|
|
|
- // adjusting ascent/descent
|
|
|
- properties.ascent = fontBBox[3];
|
|
|
- properties.descent = fontBBox[1];
|
|
|
- properties.ascentScaled = true;
|
|
|
+ if (cff.charset && cff.topDict.hasName('charset')) {
|
|
|
+ if (cff.charset.predefined) {
|
|
|
+ topDictTracker.setEntryLocation('charset', [cff.charset.format],
|
|
|
+ output);
|
|
|
+ } else {
|
|
|
+ var charset = this.compileCharset(cff.charset);
|
|
|
+ topDictTracker.setEntryLocation('charset', [output.length], output);
|
|
|
+ output.add(charset);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- var charset, encoding;
|
|
|
+ var charStrings = this.compileCharStrings(cff.charStrings);
|
|
|
+ topDictTracker.setEntryLocation('CharStrings', [output.length], output);
|
|
|
+ output.add(charStrings);
|
|
|
+
|
|
|
if (cff.isCIDFont) {
|
|
|
- var fdArrayIndex = this.parseIndex(topDict.getByName('FDArray')).obj;
|
|
|
- for (var i = 0, ii = fdArrayIndex.count; i < ii; ++i) {
|
|
|
- var dictRaw = fdArrayIndex.get(i);
|
|
|
- var fontDict = this.createDict(CFFTopDict, this.parseDict(dictRaw),
|
|
|
- cff.strings);
|
|
|
- this.parsePrivateDict(fontDict);
|
|
|
- cff.fdArray.push(fontDict);
|
|
|
- }
|
|
|
- // cid fonts don't have an encoding
|
|
|
- encoding = null;
|
|
|
- charset = this.parseCharsets(topDict.getByName('charset'),
|
|
|
- charStringIndex.count, cff.strings, true);
|
|
|
- cff.fdSelect = this.parseFDSelect(topDict.getByName('FDSelect'),
|
|
|
- charStringIndex.count);
|
|
|
- } else {
|
|
|
- charset = this.parseCharsets(topDict.getByName('charset'),
|
|
|
- charStringIndex.count, cff.strings, false);
|
|
|
- encoding = this.parseEncoding(topDict.getByName('Encoding'),
|
|
|
- properties,
|
|
|
- cff.strings, charset.charset);
|
|
|
+ // For some reason FDSelect must be in front of FDArray on windows. OSX
|
|
|
+ // and linux don't seem to care.
|
|
|
+ topDictTracker.setEntryLocation('FDSelect', [output.length], output);
|
|
|
+ var fdSelect = this.compileFDSelect(cff.fdSelect.raw);
|
|
|
+ output.add(fdSelect);
|
|
|
+ // It is unclear if the sub font dictionary can have CID related
|
|
|
+ // dictionary keys, but the sanitizer doesn't like them so remove them.
|
|
|
+ compiled = this.compileTopDicts(cff.fdArray, output.length, true);
|
|
|
+ topDictTracker.setEntryLocation('FDArray', [output.length], output);
|
|
|
+ output.add(compiled.output);
|
|
|
+ var fontDictTrackers = compiled.trackers;
|
|
|
+
|
|
|
+ this.compilePrivateDicts(cff.fdArray, fontDictTrackers, output);
|
|
|
}
|
|
|
|
|
|
- cff.charset = charset;
|
|
|
- cff.encoding = encoding;
|
|
|
+ this.compilePrivateDicts([cff.topDict], [topDictTracker], output);
|
|
|
|
|
|
- var charStringsAndSeacs = this.parseCharStrings(
|
|
|
- charStringIndex,
|
|
|
- topDict.privateDict.subrsIndex,
|
|
|
- globalSubrIndex.obj,
|
|
|
- cff.fdSelect,
|
|
|
- cff.fdArray);
|
|
|
- cff.charStrings = charStringsAndSeacs.charStrings;
|
|
|
- cff.seacs = charStringsAndSeacs.seacs;
|
|
|
- cff.widths = charStringsAndSeacs.widths;
|
|
|
+ // If the font data ends with INDEX whose object data is zero-length,
|
|
|
+ // the sanitizer will bail out. Add a dummy byte to avoid that.
|
|
|
+ output.add([0]);
|
|
|
|
|
|
- return cff;
|
|
|
+ return output.data;
|
|
|
},
|
|
|
- parseHeader: function CFFParser_parseHeader() {
|
|
|
- var bytes = this.bytes;
|
|
|
- var bytesLength = bytes.length;
|
|
|
- var offset = 0;
|
|
|
-
|
|
|
- // Prevent an infinite loop, by checking that the offset is within the
|
|
|
- // bounds of the bytes array. Necessary in empty, or invalid, font files.
|
|
|
- while (offset < bytesLength && bytes[offset] !== 1) {
|
|
|
- ++offset;
|
|
|
- }
|
|
|
- if (offset >= bytesLength) {
|
|
|
- error('Invalid CFF header');
|
|
|
- } else if (offset !== 0) {
|
|
|
- info('cff data is shifted');
|
|
|
- bytes = bytes.subarray(offset);
|
|
|
- this.bytes = bytes;
|
|
|
+ encodeNumber: function CFFCompiler_encodeNumber(value) {
|
|
|
+ if (parseFloat(value) === parseInt(value, 10) && !isNaN(value)) { // isInt
|
|
|
+ return this.encodeInteger(value);
|
|
|
+ } else {
|
|
|
+ return this.encodeFloat(value);
|
|
|
}
|
|
|
- var major = bytes[0];
|
|
|
- var minor = bytes[1];
|
|
|
- var hdrSize = bytes[2];
|
|
|
- var offSize = bytes[3];
|
|
|
- var header = new CFFHeader(major, minor, hdrSize, offSize);
|
|
|
- return { obj: header, endPos: hdrSize };
|
|
|
},
|
|
|
- parseDict: function CFFParser_parseDict(dict) {
|
|
|
- var pos = 0;
|
|
|
+ encodeFloat: function CFFCompiler_encodeFloat(num) {
|
|
|
+ var value = num.toString();
|
|
|
|
|
|
- function parseOperand() {
|
|
|
- var value = dict[pos++];
|
|
|
- if (value === 30) {
|
|
|
- return parseFloatOperand(pos);
|
|
|
- } else if (value === 28) {
|
|
|
- value = dict[pos++];
|
|
|
- value = ((value << 24) | (dict[pos++] << 16)) >> 16;
|
|
|
- return value;
|
|
|
- } else if (value === 29) {
|
|
|
- value = dict[pos++];
|
|
|
- value = (value << 8) | dict[pos++];
|
|
|
- value = (value << 8) | dict[pos++];
|
|
|
- value = (value << 8) | dict[pos++];
|
|
|
- return value;
|
|
|
- } else if (value >= 32 && value <= 246) {
|
|
|
- return value - 139;
|
|
|
- } else if (value >= 247 && value <= 250) {
|
|
|
- return ((value - 247) * 256) + dict[pos++] + 108;
|
|
|
- } else if (value >= 251 && value <= 254) {
|
|
|
- return -((value - 251) * 256) - dict[pos++] - 108;
|
|
|
+ // rounding inaccurate doubles
|
|
|
+ var m = /\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/.exec(value);
|
|
|
+ if (m) {
|
|
|
+ var epsilon = parseFloat('1e' + ((m[2] ? +m[2] : 0) + m[1].length));
|
|
|
+ value = (Math.round(num * epsilon) / epsilon).toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ var nibbles = '';
|
|
|
+ var i, ii;
|
|
|
+ for (i = 0, ii = value.length; i < ii; ++i) {
|
|
|
+ var a = value[i];
|
|
|
+ if (a === 'e') {
|
|
|
+ nibbles += value[++i] === '-' ? 'c' : 'b';
|
|
|
+ } else if (a === '.') {
|
|
|
+ nibbles += 'a';
|
|
|
+ } else if (a === '-') {
|
|
|
+ nibbles += 'e';
|
|
|
} else {
|
|
|
- error('255 is not a valid DICT command');
|
|
|
+ nibbles += a;
|
|
|
}
|
|
|
- return -1;
|
|
|
}
|
|
|
+ nibbles += (nibbles.length & 1) ? 'f' : 'ff';
|
|
|
+ var out = [30];
|
|
|
+ for (i = 0, ii = nibbles.length; i < ii; i += 2) {
|
|
|
+ out.push(parseInt(nibbles.substr(i, 2), 16));
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+ },
|
|
|
+ encodeInteger: function CFFCompiler_encodeInteger(value) {
|
|
|
+ var code;
|
|
|
+ if (value >= -107 && value <= 107) {
|
|
|
+ code = [value + 139];
|
|
|
+ } else if (value >= 108 && value <= 1131) {
|
|
|
+ value = [value - 108];
|
|
|
+ code = [(value >> 8) + 247, value & 0xFF];
|
|
|
+ } else if (value >= -1131 && value <= -108) {
|
|
|
+ value = -value - 108;
|
|
|
+ code = [(value >> 8) + 251, value & 0xFF];
|
|
|
+ } else if (value >= -32768 && value <= 32767) {
|
|
|
+ code = [0x1c, (value >> 8) & 0xFF, value & 0xFF];
|
|
|
+ } else {
|
|
|
+ code = [0x1d,
|
|
|
+ (value >> 24) & 0xFF,
|
|
|
+ (value >> 16) & 0xFF,
|
|
|
+ (value >> 8) & 0xFF,
|
|
|
+ value & 0xFF];
|
|
|
+ }
|
|
|
+ return code;
|
|
|
+ },
|
|
|
+ compileHeader: function CFFCompiler_compileHeader(header) {
|
|
|
+ return [
|
|
|
+ header.major,
|
|
|
+ header.minor,
|
|
|
+ header.hdrSize,
|
|
|
+ header.offSize
|
|
|
+ ];
|
|
|
+ },
|
|
|
+ compileNameIndex: function CFFCompiler_compileNameIndex(names) {
|
|
|
+ var nameIndex = new CFFIndex();
|
|
|
+ for (var i = 0, ii = names.length; i < ii; ++i) {
|
|
|
+ nameIndex.add(stringToBytes(names[i]));
|
|
|
+ }
|
|
|
+ return this.compileIndex(nameIndex);
|
|
|
+ },
|
|
|
+ compileTopDicts: function CFFCompiler_compileTopDicts(dicts,
|
|
|
+ length,
|
|
|
+ removeCidKeys) {
|
|
|
+ var fontDictTrackers = [];
|
|
|
+ var fdArrayIndex = new CFFIndex();
|
|
|
+ for (var i = 0, ii = dicts.length; i < ii; ++i) {
|
|
|
+ var fontDict = dicts[i];
|
|
|
+ if (removeCidKeys) {
|
|
|
+ fontDict.removeByName('CIDFontVersion');
|
|
|
+ fontDict.removeByName('CIDFontRevision');
|
|
|
+ fontDict.removeByName('CIDFontType');
|
|
|
+ fontDict.removeByName('CIDCount');
|
|
|
+ fontDict.removeByName('UIDBase');
|
|
|
+ }
|
|
|
+ var fontDictTracker = new CFFOffsetTracker();
|
|
|
+ var fontDictData = this.compileDict(fontDict, fontDictTracker);
|
|
|
+ fontDictTrackers.push(fontDictTracker);
|
|
|
+ fdArrayIndex.add(fontDictData);
|
|
|
+ fontDictTracker.offset(length);
|
|
|
+ }
|
|
|
+ fdArrayIndex = this.compileIndex(fdArrayIndex, fontDictTrackers);
|
|
|
+ return {
|
|
|
+ trackers: fontDictTrackers,
|
|
|
+ output: fdArrayIndex
|
|
|
+ };
|
|
|
+ },
|
|
|
+ compilePrivateDicts: function CFFCompiler_compilePrivateDicts(dicts,
|
|
|
+ trackers,
|
|
|
+ output) {
|
|
|
+ for (var i = 0, ii = dicts.length; i < ii; ++i) {
|
|
|
+ var fontDict = dicts[i];
|
|
|
+ assert(fontDict.privateDict && fontDict.hasName('Private'),
|
|
|
+ 'There must be an private dictionary.');
|
|
|
+ var privateDict = fontDict.privateDict;
|
|
|
+ var privateDictTracker = new CFFOffsetTracker();
|
|
|
+ var privateDictData = this.compileDict(privateDict, privateDictTracker);
|
|
|
|
|
|
- function parseFloatOperand() {
|
|
|
- var str = '';
|
|
|
- var eof = 15;
|
|
|
- var lookup = ['0', '1', '2', '3', '4', '5', '6', '7', '8',
|
|
|
- '9', '.', 'E', 'E-', null, '-'];
|
|
|
- var length = dict.length;
|
|
|
- while (pos < length) {
|
|
|
- var b = dict[pos++];
|
|
|
- var b1 = b >> 4;
|
|
|
- var b2 = b & 15;
|
|
|
+ var outputLength = output.length;
|
|
|
+ privateDictTracker.offset(outputLength);
|
|
|
+ if (!privateDictData.length) {
|
|
|
+ // The private dictionary was empty, set the output length to zero to
|
|
|
+ // ensure the offset length isn't out of bounds in the eyes of the
|
|
|
+ // sanitizer.
|
|
|
+ outputLength = 0;
|
|
|
+ }
|
|
|
|
|
|
- if (b1 === eof) {
|
|
|
- break;
|
|
|
- }
|
|
|
- str += lookup[b1];
|
|
|
+ trackers[i].setEntryLocation('Private',
|
|
|
+ [privateDictData.length, outputLength],
|
|
|
+ output);
|
|
|
+ output.add(privateDictData);
|
|
|
|
|
|
- if (b2 === eof) {
|
|
|
- break;
|
|
|
- }
|
|
|
- str += lookup[b2];
|
|
|
+ if (privateDict.subrsIndex && privateDict.hasName('Subrs')) {
|
|
|
+ var subrs = this.compileIndex(privateDict.subrsIndex);
|
|
|
+ privateDictTracker.setEntryLocation('Subrs', [privateDictData.length],
|
|
|
+ output);
|
|
|
+ output.add(subrs);
|
|
|
}
|
|
|
- return parseFloat(str);
|
|
|
}
|
|
|
-
|
|
|
- var operands = [];
|
|
|
- var entries = [];
|
|
|
-
|
|
|
- pos = 0;
|
|
|
- var end = dict.length;
|
|
|
- while (pos < end) {
|
|
|
- var b = dict[pos];
|
|
|
- if (b <= 21) {
|
|
|
- if (b === 12) {
|
|
|
- b = (b << 8) | dict[++pos];
|
|
|
- }
|
|
|
- entries.push([b, operands]);
|
|
|
- operands = [];
|
|
|
- ++pos;
|
|
|
- } else {
|
|
|
- operands.push(parseOperand());
|
|
|
+ },
|
|
|
+ compileDict: function CFFCompiler_compileDict(dict, offsetTracker) {
|
|
|
+ var out = [];
|
|
|
+ // The dictionary keys must be in a certain order.
|
|
|
+ var order = dict.order;
|
|
|
+ for (var i = 0; i < order.length; ++i) {
|
|
|
+ var key = order[i];
|
|
|
+ if (!(key in dict.values)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ var values = dict.values[key];
|
|
|
+ var types = dict.types[key];
|
|
|
+ if (!isArray(types)) {
|
|
|
+ types = [types];
|
|
|
+ }
|
|
|
+ if (!isArray(values)) {
|
|
|
+ values = [values];
|
|
|
}
|
|
|
- }
|
|
|
- return entries;
|
|
|
- },
|
|
|
- parseIndex: function CFFParser_parseIndex(pos) {
|
|
|
- var cffIndex = new CFFIndex();
|
|
|
- var bytes = this.bytes;
|
|
|
- var count = (bytes[pos++] << 8) | bytes[pos++];
|
|
|
- var offsets = [];
|
|
|
- var end = pos;
|
|
|
- var i, ii;
|
|
|
|
|
|
- if (count !== 0) {
|
|
|
- var offsetSize = bytes[pos++];
|
|
|
- // add 1 for offset to determine size of last object
|
|
|
- var startPos = pos + ((count + 1) * offsetSize) - 1;
|
|
|
+ // Remove any empty dict values.
|
|
|
+ if (values.length === 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
- for (i = 0, ii = count + 1; i < ii; ++i) {
|
|
|
- var offset = 0;
|
|
|
- for (var j = 0; j < offsetSize; ++j) {
|
|
|
- offset <<= 8;
|
|
|
- offset += bytes[pos++];
|
|
|
+ for (var j = 0, jj = types.length; j < jj; ++j) {
|
|
|
+ var type = types[j];
|
|
|
+ var value = values[j];
|
|
|
+ switch (type) {
|
|
|
+ case 'num':
|
|
|
+ case 'sid':
|
|
|
+ out = out.concat(this.encodeNumber(value));
|
|
|
+ break;
|
|
|
+ case 'offset':
|
|
|
+ // For offsets we just insert a 32bit integer so we don't have to
|
|
|
+ // deal with figuring out the length of the offset when it gets
|
|
|
+ // replaced later on by the compiler.
|
|
|
+ var name = dict.keyToNameMap[key];
|
|
|
+ // Some offsets have the offset and the length, so just record the
|
|
|
+ // position of the first one.
|
|
|
+ if (!offsetTracker.isTracking(name)) {
|
|
|
+ offsetTracker.track(name, out.length);
|
|
|
+ }
|
|
|
+ out = out.concat([0x1d, 0, 0, 0, 0]);
|
|
|
+ break;
|
|
|
+ case 'array':
|
|
|
+ case 'delta':
|
|
|
+ out = out.concat(this.encodeNumber(value));
|
|
|
+ for (var k = 1, kk = values.length; k < kk; ++k) {
|
|
|
+ out = out.concat(this.encodeNumber(values[k]));
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ error('Unknown data type of ' + type);
|
|
|
+ break;
|
|
|
}
|
|
|
- offsets.push(startPos + offset);
|
|
|
}
|
|
|
- end = offsets[count];
|
|
|
- }
|
|
|
- for (i = 0, ii = offsets.length - 1; i < ii; ++i) {
|
|
|
- var offsetStart = offsets[i];
|
|
|
- var offsetEnd = offsets[i + 1];
|
|
|
- cffIndex.add(bytes.subarray(offsetStart, offsetEnd));
|
|
|
+ out = out.concat(dict.opcodes[key]);
|
|
|
}
|
|
|
- return {obj: cffIndex, endPos: end};
|
|
|
+ return out;
|
|
|
},
|
|
|
- parseNameIndex: function CFFParser_parseNameIndex(index) {
|
|
|
- var names = [];
|
|
|
- for (var i = 0, ii = index.count; i < ii; ++i) {
|
|
|
- var name = index.get(i);
|
|
|
- // OTS doesn't allow names to be over 127 characters.
|
|
|
- var length = Math.min(name.length, 127);
|
|
|
- var data = [];
|
|
|
- // OTS also only permits certain characters in the name.
|
|
|
- for (var j = 0; j < length; ++j) {
|
|
|
- var c = name[j];
|
|
|
- if (j === 0 && c === 0) {
|
|
|
- data[j] = c;
|
|
|
- continue;
|
|
|
- }
|
|
|
- if ((c < 33 || c > 126) || c === 91 /* [ */ || c === 93 /* ] */ ||
|
|
|
- c === 40 /* ( */ || c === 41 /* ) */ || c === 123 /* { */ ||
|
|
|
- c === 125 /* } */ || c === 60 /* < */ || c === 62 /* > */ ||
|
|
|
- c === 47 /* / */ || c === 37 /* % */ || c === 35 /* # */) {
|
|
|
- data[j] = 95;
|
|
|
- continue;
|
|
|
- }
|
|
|
- data[j] = c;
|
|
|
- }
|
|
|
- names.push(bytesToString(data));
|
|
|
+ compileStringIndex: function CFFCompiler_compileStringIndex(strings) {
|
|
|
+ var stringIndex = new CFFIndex();
|
|
|
+ for (var i = 0, ii = strings.length; i < ii; ++i) {
|
|
|
+ stringIndex.add(stringToBytes(strings[i]));
|
|
|
}
|
|
|
- return names;
|
|
|
+ return this.compileIndex(stringIndex);
|
|
|
},
|
|
|
- parseStringIndex: function CFFParser_parseStringIndex(index) {
|
|
|
- var strings = new CFFStrings();
|
|
|
- for (var i = 0, ii = index.count; i < ii; ++i) {
|
|
|
- var data = index.get(i);
|
|
|
- strings.add(bytesToString(data));
|
|
|
- }
|
|
|
- return strings;
|
|
|
+ compileGlobalSubrIndex: function CFFCompiler_compileGlobalSubrIndex() {
|
|
|
+ var globalSubrIndex = this.cff.globalSubrIndex;
|
|
|
+ this.out.writeByteArray(this.compileIndex(globalSubrIndex));
|
|
|
},
|
|
|
- createDict: function CFFParser_createDict(Type, dict, strings) {
|
|
|
- var cffDict = new Type(strings);
|
|
|
- for (var i = 0, ii = dict.length; i < ii; ++i) {
|
|
|
- var pair = dict[i];
|
|
|
- var key = pair[0];
|
|
|
- var value = pair[1];
|
|
|
- cffDict.setByKey(key, value);
|
|
|
+ compileCharStrings: function CFFCompiler_compileCharStrings(charStrings) {
|
|
|
+ return this.compileIndex(charStrings);
|
|
|
+ },
|
|
|
+ compileCharset: function CFFCompiler_compileCharset(charset) {
|
|
|
+ return this.compileTypedArray(charset.raw);
|
|
|
+ },
|
|
|
+ compileEncoding: function CFFCompiler_compileEncoding(encoding) {
|
|
|
+ return this.compileTypedArray(encoding.raw);
|
|
|
+ },
|
|
|
+ compileFDSelect: function CFFCompiler_compileFDSelect(fdSelect) {
|
|
|
+ return this.compileTypedArray(fdSelect);
|
|
|
+ },
|
|
|
+ compileTypedArray: function CFFCompiler_compileTypedArray(data) {
|
|
|
+ var out = [];
|
|
|
+ for (var i = 0, ii = data.length; i < ii; ++i) {
|
|
|
+ out[i] = data[i];
|
|
|
}
|
|
|
- return cffDict;
|
|
|
+ return out;
|
|
|
},
|
|
|
- parseCharString: function CFFParser_parseCharString(state, data,
|
|
|
- localSubrIndex,
|
|
|
- globalSubrIndex) {
|
|
|
- if (state.callDepth > MAX_SUBR_NESTING) {
|
|
|
- return false;
|
|
|
+ compileIndex: function CFFCompiler_compileIndex(index, trackers) {
|
|
|
+ trackers = trackers || [];
|
|
|
+ var objects = index.objects;
|
|
|
+ // First 2 bytes contains the number of objects contained into this index
|
|
|
+ var count = objects.length;
|
|
|
+
|
|
|
+ // If there is no object, just create an index. This technically
|
|
|
+ // should just be [0, 0] but OTS has an issue with that.
|
|
|
+ if (count === 0) {
|
|
|
+ return [0, 0, 0];
|
|
|
}
|
|
|
- var stackSize = state.stackSize;
|
|
|
- var stack = state.stack;
|
|
|
|
|
|
- var length = data.length;
|
|
|
+ var data = [(count >> 8) & 0xFF, count & 0xff];
|
|
|
|
|
|
- for (var j = 0; j < length;) {
|
|
|
- var value = data[j++];
|
|
|
- var validationCommand = null;
|
|
|
- if (value === 12) {
|
|
|
- var q = data[j++];
|
|
|
- if (q === 0) {
|
|
|
- // The CFF specification state that the 'dotsection' command
|
|
|
- // (12, 0) is deprecated and treated as a no-op, but all Type2
|
|
|
- // charstrings processors should support them. Unfortunately
|
|
|
- // the font sanitizer don't. As a workaround the sequence (12, 0)
|
|
|
- // is replaced by a useless (0, hmoveto).
|
|
|
- data[j - 2] = 139;
|
|
|
- data[j - 1] = 22;
|
|
|
- stackSize = 0;
|
|
|
- } else {
|
|
|
- validationCommand = CharstringValidationData12[q];
|
|
|
- }
|
|
|
- } else if (value === 28) { // number (16 bit)
|
|
|
- stack[stackSize] = ((data[j] << 24) | (data[j + 1] << 16)) >> 16;
|
|
|
- j += 2;
|
|
|
- stackSize++;
|
|
|
- } else if (value === 14) {
|
|
|
- if (stackSize >= 4) {
|
|
|
- stackSize -= 4;
|
|
|
- if (SEAC_ANALYSIS_ENABLED) {
|
|
|
- state.seac = stack.slice(stackSize, stackSize + 4);
|
|
|
- return false;
|
|
|
- }
|
|
|
- }
|
|
|
- validationCommand = CharstringValidationData[value];
|
|
|
- } else if (value >= 32 && value <= 246) { // number
|
|
|
- stack[stackSize] = value - 139;
|
|
|
- stackSize++;
|
|
|
- } else if (value >= 247 && value <= 254) { // number (+1 bytes)
|
|
|
- stack[stackSize] = (value < 251 ?
|
|
|
- ((value - 247) << 8) + data[j] + 108 :
|
|
|
- -((value - 251) << 8) - data[j] - 108);
|
|
|
- j++;
|
|
|
- stackSize++;
|
|
|
- } else if (value === 255) { // number (32 bit)
|
|
|
- stack[stackSize] = ((data[j] << 24) | (data[j + 1] << 16) |
|
|
|
- (data[j + 2] << 8) | data[j + 3]) / 65536;
|
|
|
- j += 4;
|
|
|
- stackSize++;
|
|
|
- } else if (value === 19 || value === 20) {
|
|
|
- state.hints += stackSize >> 1;
|
|
|
- // skipping right amount of hints flag data
|
|
|
- j += (state.hints + 7) >> 3;
|
|
|
- stackSize %= 2;
|
|
|
- validationCommand = CharstringValidationData[value];
|
|
|
- } else if (value === 10 || value === 29) {
|
|
|
- var subrsIndex;
|
|
|
- if (value === 10) {
|
|
|
- subrsIndex = localSubrIndex;
|
|
|
- } else {
|
|
|
- subrsIndex = globalSubrIndex;
|
|
|
- }
|
|
|
- if (!subrsIndex) {
|
|
|
- validationCommand = CharstringValidationData[value];
|
|
|
- warn('Missing subrsIndex for ' + validationCommand.id);
|
|
|
- return false;
|
|
|
- }
|
|
|
- var bias = 32768;
|
|
|
- if (subrsIndex.count < 1240) {
|
|
|
- bias = 107;
|
|
|
- } else if (subrsIndex.count < 33900) {
|
|
|
- bias = 1131;
|
|
|
- }
|
|
|
- var subrNumber = stack[--stackSize] + bias;
|
|
|
- if (subrNumber < 0 || subrNumber >= subrsIndex.count) {
|
|
|
- validationCommand = CharstringValidationData[value];
|
|
|
- warn('Out of bounds subrIndex for ' + validationCommand.id);
|
|
|
- return false;
|
|
|
- }
|
|
|
- state.stackSize = stackSize;
|
|
|
- state.callDepth++;
|
|
|
- var valid = this.parseCharString(state, subrsIndex.get(subrNumber),
|
|
|
- localSubrIndex, globalSubrIndex);
|
|
|
- if (!valid) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- state.callDepth--;
|
|
|
- stackSize = state.stackSize;
|
|
|
- continue;
|
|
|
- } else if (value === 11) {
|
|
|
- state.stackSize = stackSize;
|
|
|
- return true;
|
|
|
+ var lastOffset = 1, i;
|
|
|
+ for (i = 0; i < count; ++i) {
|
|
|
+ lastOffset += objects[i].length;
|
|
|
+ }
|
|
|
+
|
|
|
+ var offsetSize;
|
|
|
+ if (lastOffset < 0x100) {
|
|
|
+ offsetSize = 1;
|
|
|
+ } else if (lastOffset < 0x10000) {
|
|
|
+ offsetSize = 2;
|
|
|
+ } else if (lastOffset < 0x1000000) {
|
|
|
+ offsetSize = 3;
|
|
|
+ } else {
|
|
|
+ offsetSize = 4;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Next byte contains the offset size use to reference object in the file
|
|
|
+ data.push(offsetSize);
|
|
|
+
|
|
|
+ // Add another offset after this one because we need a new offset
|
|
|
+ var relativeOffset = 1;
|
|
|
+ for (i = 0; i < count + 1; i++) {
|
|
|
+ if (offsetSize === 1) {
|
|
|
+ data.push(relativeOffset & 0xFF);
|
|
|
+ } else if (offsetSize === 2) {
|
|
|
+ data.push((relativeOffset >> 8) & 0xFF,
|
|
|
+ relativeOffset & 0xFF);
|
|
|
+ } else if (offsetSize === 3) {
|
|
|
+ data.push((relativeOffset >> 16) & 0xFF,
|
|
|
+ (relativeOffset >> 8) & 0xFF,
|
|
|
+ relativeOffset & 0xFF);
|
|
|
} else {
|
|
|
- validationCommand = CharstringValidationData[value];
|
|
|
+ data.push((relativeOffset >>> 24) & 0xFF,
|
|
|
+ (relativeOffset >> 16) & 0xFF,
|
|
|
+ (relativeOffset >> 8) & 0xFF,
|
|
|
+ relativeOffset & 0xFF);
|
|
|
}
|
|
|
- if (validationCommand) {
|
|
|
- if (validationCommand.stem) {
|
|
|
- state.hints += stackSize >> 1;
|
|
|
- }
|
|
|
- if ('min' in validationCommand) {
|
|
|
- if (!state.undefStack && stackSize < validationCommand.min) {
|
|
|
- warn('Not enough parameters for ' + validationCommand.id +
|
|
|
- '; actual: ' + stackSize +
|
|
|
- ', expected: ' + validationCommand.min);
|
|
|
- return false;
|
|
|
- }
|
|
|
- }
|
|
|
- if (state.firstStackClearing && validationCommand.stackClearing) {
|
|
|
- state.firstStackClearing = false;
|
|
|
- // the optional character width can be found before the first
|
|
|
- // stack-clearing command arguments
|
|
|
- stackSize -= validationCommand.min;
|
|
|
- if (stackSize >= 2 && validationCommand.stem) {
|
|
|
- // there are even amount of arguments for stem commands
|
|
|
- stackSize %= 2;
|
|
|
- } else if (stackSize > 1) {
|
|
|
- warn('Found too many parameters for stack-clearing command');
|
|
|
- }
|
|
|
- if (stackSize > 0 && stack[stackSize - 1] >= 0) {
|
|
|
- state.width = stack[stackSize - 1];
|
|
|
- }
|
|
|
- }
|
|
|
- if ('stackDelta' in validationCommand) {
|
|
|
- if ('stackFn' in validationCommand) {
|
|
|
- validationCommand.stackFn(stack, stackSize);
|
|
|
- }
|
|
|
- stackSize += validationCommand.stackDelta;
|
|
|
- } else if (validationCommand.stackClearing) {
|
|
|
- stackSize = 0;
|
|
|
- } else if (validationCommand.resetStack) {
|
|
|
- stackSize = 0;
|
|
|
- state.undefStack = false;
|
|
|
- } else if (validationCommand.undefStack) {
|
|
|
- stackSize = 0;
|
|
|
- state.undefStack = true;
|
|
|
- state.firstStackClearing = false;
|
|
|
- }
|
|
|
+
|
|
|
+ if (objects[i]) {
|
|
|
+ relativeOffset += objects[i].length;
|
|
|
}
|
|
|
}
|
|
|
- state.stackSize = stackSize;
|
|
|
- return true;
|
|
|
- },
|
|
|
- parseCharStrings: function CFFParser_parseCharStrings(charStrings,
|
|
|
- localSubrIndex,
|
|
|
- globalSubrIndex,
|
|
|
- fdSelect,
|
|
|
- fdArray) {
|
|
|
- var seacs = [];
|
|
|
- var widths = [];
|
|
|
- var count = charStrings.count;
|
|
|
- for (var i = 0; i < count; i++) {
|
|
|
- var charstring = charStrings.get(i);
|
|
|
- var state = {
|
|
|
- callDepth: 0,
|
|
|
- stackSize: 0,
|
|
|
- stack: [],
|
|
|
- undefStack: true,
|
|
|
- hints: 0,
|
|
|
- firstStackClearing: true,
|
|
|
- seac: null,
|
|
|
- width: null
|
|
|
- };
|
|
|
- var valid = true;
|
|
|
- var localSubrToUse = null;
|
|
|
- if (fdSelect && fdArray.length) {
|
|
|
- var fdIndex = fdSelect.getFDIndex(i);
|
|
|
- if (fdIndex === -1) {
|
|
|
- warn('Glyph index is not in fd select.');
|
|
|
- valid = false;
|
|
|
- }
|
|
|
- if (fdIndex >= fdArray.length) {
|
|
|
- warn('Invalid fd index for glyph index.');
|
|
|
- valid = false;
|
|
|
- }
|
|
|
- if (valid) {
|
|
|
- localSubrToUse = fdArray[fdIndex].privateDict.subrsIndex;
|
|
|
- }
|
|
|
- } else if (localSubrIndex) {
|
|
|
- localSubrToUse = localSubrIndex;
|
|
|
- }
|
|
|
- if (valid) {
|
|
|
- valid = this.parseCharString(state, charstring, localSubrToUse,
|
|
|
- globalSubrIndex);
|
|
|
- }
|
|
|
- if (state.width !== null) {
|
|
|
- widths[i] = state.width;
|
|
|
+
|
|
|
+ for (i = 0; i < count; i++) {
|
|
|
+ // Notify the tracker where the object will be offset in the data.
|
|
|
+ if (trackers[i]) {
|
|
|
+ trackers[i].offset(data.length);
|
|
|
}
|
|
|
- if (state.seac !== null) {
|
|
|
- seacs[i] = state.seac;
|
|
|
+ for (var j = 0, jj = objects[i].length; j < jj; j++) {
|
|
|
+ data.push(objects[i][j]);
|
|
|
}
|
|
|
- if (!valid) {
|
|
|
- // resetting invalid charstring to single 'endchar'
|
|
|
- charStrings.set(i, new Uint8Array([14]));
|
|
|
+ }
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ return CFFCompiler;
|
|
|
+})();
|
|
|
+
|
|
|
+function _enableSeacAnalysis(enabled) {
|
|
|
+ exports.SEAC_ANALYSIS_ENABLED = SEAC_ANALYSIS_ENABLED = enabled;
|
|
|
+}
|
|
|
+
|
|
|
+// Workaround for seac on Windows.
|
|
|
+(function checkSeacSupport() {
|
|
|
+ if (typeof navigator !== 'undefined' && /Windows/.test(navigator.userAgent)) {
|
|
|
+ SEAC_ANALYSIS_ENABLED = true;
|
|
|
+ }
|
|
|
+})();
|
|
|
+
|
|
|
+// Workaround for Private Use Area characters in Chrome on Windows
|
|
|
+// http://code.google.com/p/chromium/issues/detail?id=122465
|
|
|
+// https://github.com/mozilla/pdf.js/issues/1689
|
|
|
+(function checkChromeWindows() {
|
|
|
+ if (typeof navigator !== 'undefined' &&
|
|
|
+ /Windows.*Chrome/.test(navigator.userAgent)) {
|
|
|
+ SKIP_PRIVATE_USE_RANGE_F000_TO_F01F = true;
|
|
|
+ }
|
|
|
+})();
|
|
|
+
|
|
|
+exports.SEAC_ANALYSIS_ENABLED = SEAC_ANALYSIS_ENABLED;
|
|
|
+exports.CFFCompiler = CFFCompiler;
|
|
|
+exports.CFFIndex = CFFIndex;
|
|
|
+exports.CFFParser = CFFParser;
|
|
|
+exports.CFFStrings = CFFStrings;
|
|
|
+exports.ErrorFont = ErrorFont;
|
|
|
+exports.FontFlags = FontFlags;
|
|
|
+exports.Font = Font;
|
|
|
+exports.IdentityToUnicodeMap = IdentityToUnicodeMap;
|
|
|
+exports.ToUnicodeMap = ToUnicodeMap;
|
|
|
+exports.Type1Parser = Type1Parser;
|
|
|
+exports.getFontType = getFontType;
|
|
|
+exports._enableSeacAnalysis = _enableSeacAnalysis;
|
|
|
+
|
|
|
+// TODO refactor to remove cyclic dependency on font_renderer.js
|
|
|
+coreFontRenderer._setCoreFonts(exports);
|
|
|
+}));
|
|
|
+
|
|
|
+
|
|
|
+(function (root, factory) {
|
|
|
+ {
|
|
|
+ factory((root.pdfjsCoreFunction = {}), root.pdfjsSharedUtil,
|
|
|
+ root.pdfjsCorePrimitives, root.pdfjsCorePsParser);
|
|
|
+ }
|
|
|
+}(this, function (exports, sharedUtil, corePrimitives, corePsParser) {
|
|
|
+
|
|
|
+var error = sharedUtil.error;
|
|
|
+var info = sharedUtil.info;
|
|
|
+var isArray = sharedUtil.isArray;
|
|
|
+var isBool = sharedUtil.isBool;
|
|
|
+var isDict = corePrimitives.isDict;
|
|
|
+var isStream = corePrimitives.isStream;
|
|
|
+var PostScriptLexer = corePsParser.PostScriptLexer;
|
|
|
+var PostScriptParser = corePsParser.PostScriptParser;
|
|
|
+
|
|
|
+var PDFFunction = (function PDFFunctionClosure() {
|
|
|
+ var CONSTRUCT_SAMPLED = 0;
|
|
|
+ var CONSTRUCT_INTERPOLATED = 2;
|
|
|
+ var CONSTRUCT_STICHED = 3;
|
|
|
+ var CONSTRUCT_POSTSCRIPT = 4;
|
|
|
+
|
|
|
+ return {
|
|
|
+ getSampleArray: function PDFFunction_getSampleArray(size, outputSize, bps,
|
|
|
+ str) {
|
|
|
+ var i, ii;
|
|
|
+ var length = 1;
|
|
|
+ for (i = 0, ii = size.length; i < ii; i++) {
|
|
|
+ length *= size[i];
|
|
|
+ }
|
|
|
+ length *= outputSize;
|
|
|
+
|
|
|
+ var array = new Array(length);
|
|
|
+ var codeSize = 0;
|
|
|
+ var codeBuf = 0;
|
|
|
+ // 32 is a valid bps so shifting won't work
|
|
|
+ var sampleMul = 1.0 / (Math.pow(2.0, bps) - 1);
|
|
|
+
|
|
|
+ var strBytes = str.getBytes((length * bps + 7) / 8);
|
|
|
+ var strIdx = 0;
|
|
|
+ for (i = 0; i < length; i++) {
|
|
|
+ while (codeSize < bps) {
|
|
|
+ codeBuf <<= 8;
|
|
|
+ codeBuf |= strBytes[strIdx++];
|
|
|
+ codeSize += 8;
|
|
|
}
|
|
|
+ codeSize -= bps;
|
|
|
+ array[i] = (codeBuf >> codeSize) * sampleMul;
|
|
|
+ codeBuf &= (1 << codeSize) - 1;
|
|
|
}
|
|
|
- return { charStrings: charStrings, seacs: seacs, widths: widths };
|
|
|
+ return array;
|
|
|
},
|
|
|
- emptyPrivateDictionary:
|
|
|
- function CFFParser_emptyPrivateDictionary(parentDict) {
|
|
|
- var privateDict = this.createDict(CFFPrivateDict, [],
|
|
|
- parentDict.strings);
|
|
|
- parentDict.setByKey(18, [0, 0]);
|
|
|
- parentDict.privateDict = privateDict;
|
|
|
+
|
|
|
+ getIR: function PDFFunction_getIR(xref, fn) {
|
|
|
+ var dict = fn.dict;
|
|
|
+ if (!dict) {
|
|
|
+ dict = fn;
|
|
|
+ }
|
|
|
+
|
|
|
+ var types = [this.constructSampled,
|
|
|
+ null,
|
|
|
+ this.constructInterpolated,
|
|
|
+ this.constructStiched,
|
|
|
+ this.constructPostScript];
|
|
|
+
|
|
|
+ var typeNum = dict.get('FunctionType');
|
|
|
+ var typeFn = types[typeNum];
|
|
|
+ if (!typeFn) {
|
|
|
+ error('Unknown type of function');
|
|
|
+ }
|
|
|
+
|
|
|
+ return typeFn.call(this, fn, dict, xref);
|
|
|
+ },
|
|
|
+
|
|
|
+ fromIR: function PDFFunction_fromIR(IR) {
|
|
|
+ var type = IR[0];
|
|
|
+ switch (type) {
|
|
|
+ case CONSTRUCT_SAMPLED:
|
|
|
+ return this.constructSampledFromIR(IR);
|
|
|
+ case CONSTRUCT_INTERPOLATED:
|
|
|
+ return this.constructInterpolatedFromIR(IR);
|
|
|
+ case CONSTRUCT_STICHED:
|
|
|
+ return this.constructStichedFromIR(IR);
|
|
|
+ //case CONSTRUCT_POSTSCRIPT:
|
|
|
+ default:
|
|
|
+ return this.constructPostScriptFromIR(IR);
|
|
|
+ }
|
|
|
},
|
|
|
- parsePrivateDict: function CFFParser_parsePrivateDict(parentDict) {
|
|
|
- // no private dict, do nothing
|
|
|
- if (!parentDict.hasName('Private')) {
|
|
|
- this.emptyPrivateDictionary(parentDict);
|
|
|
- return;
|
|
|
+
|
|
|
+ parse: function PDFFunction_parse(xref, fn) {
|
|
|
+ var IR = this.getIR(xref, fn);
|
|
|
+ return this.fromIR(IR);
|
|
|
+ },
|
|
|
+
|
|
|
+ parseArray: function PDFFunction_parseArray(xref, fnObj) {
|
|
|
+ if (!isArray(fnObj)) {
|
|
|
+ // not an array -- parsing as regular function
|
|
|
+ return this.parse(xref, fnObj);
|
|
|
}
|
|
|
- var privateOffset = parentDict.getByName('Private');
|
|
|
- // make sure the params are formatted correctly
|
|
|
- if (!isArray(privateOffset) || privateOffset.length !== 2) {
|
|
|
- parentDict.removeByName('Private');
|
|
|
- return;
|
|
|
+
|
|
|
+ var fnArray = [];
|
|
|
+ for (var j = 0, jj = fnObj.length; j < jj; j++) {
|
|
|
+ var obj = xref.fetchIfRef(fnObj[j]);
|
|
|
+ fnArray.push(PDFFunction.parse(xref, obj));
|
|
|
}
|
|
|
- var size = privateOffset[0];
|
|
|
- var offset = privateOffset[1];
|
|
|
- // remove empty dicts or ones that refer to invalid location
|
|
|
- if (size === 0 || offset >= this.bytes.length) {
|
|
|
- this.emptyPrivateDictionary(parentDict);
|
|
|
- return;
|
|
|
+ return function (src, srcOffset, dest, destOffset) {
|
|
|
+ for (var i = 0, ii = fnArray.length; i < ii; i++) {
|
|
|
+ fnArray[i](src, srcOffset, dest, destOffset + i);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ constructSampled: function PDFFunction_constructSampled(str, dict) {
|
|
|
+ function toMultiArray(arr) {
|
|
|
+ var inputLength = arr.length;
|
|
|
+ var out = [];
|
|
|
+ var index = 0;
|
|
|
+ for (var i = 0; i < inputLength; i += 2) {
|
|
|
+ out[index] = [arr[i], arr[i + 1]];
|
|
|
+ ++index;
|
|
|
+ }
|
|
|
+ return out;
|
|
|
}
|
|
|
+ var domain = dict.get('Domain');
|
|
|
+ var range = dict.get('Range');
|
|
|
|
|
|
- var privateDictEnd = offset + size;
|
|
|
- var dictData = this.bytes.subarray(offset, privateDictEnd);
|
|
|
- var dict = this.parseDict(dictData);
|
|
|
- var privateDict = this.createDict(CFFPrivateDict, dict,
|
|
|
- parentDict.strings);
|
|
|
- parentDict.privateDict = privateDict;
|
|
|
+ if (!domain || !range) {
|
|
|
+ error('No domain or range');
|
|
|
+ }
|
|
|
|
|
|
- // Parse the Subrs index also since it's relative to the private dict.
|
|
|
- if (!privateDict.getByName('Subrs')) {
|
|
|
- return;
|
|
|
+ var inputSize = domain.length / 2;
|
|
|
+ var outputSize = range.length / 2;
|
|
|
+
|
|
|
+ domain = toMultiArray(domain);
|
|
|
+ range = toMultiArray(range);
|
|
|
+
|
|
|
+ var size = dict.get('Size');
|
|
|
+ var bps = dict.get('BitsPerSample');
|
|
|
+ var order = dict.get('Order') || 1;
|
|
|
+ if (order !== 1) {
|
|
|
+ // No description how cubic spline interpolation works in PDF32000:2008
|
|
|
+ // As in poppler, ignoring order, linear interpolation may work as good
|
|
|
+ info('No support for cubic spline interpolation: ' + order);
|
|
|
}
|
|
|
- var subrsOffset = privateDict.getByName('Subrs');
|
|
|
- var relativeOffset = offset + subrsOffset;
|
|
|
- // Validate the offset.
|
|
|
- if (subrsOffset === 0 || relativeOffset >= this.bytes.length) {
|
|
|
- this.emptyPrivateDictionary(parentDict);
|
|
|
- return;
|
|
|
+
|
|
|
+ var encode = dict.get('Encode');
|
|
|
+ if (!encode) {
|
|
|
+ encode = [];
|
|
|
+ for (var i = 0; i < inputSize; ++i) {
|
|
|
+ encode.push(0);
|
|
|
+ encode.push(size[i] - 1);
|
|
|
+ }
|
|
|
}
|
|
|
- var subrsIndex = this.parseIndex(relativeOffset);
|
|
|
- privateDict.subrsIndex = subrsIndex.obj;
|
|
|
+ encode = toMultiArray(encode);
|
|
|
+
|
|
|
+ var decode = dict.get('Decode');
|
|
|
+ if (!decode) {
|
|
|
+ decode = range;
|
|
|
+ } else {
|
|
|
+ decode = toMultiArray(decode);
|
|
|
+ }
|
|
|
+
|
|
|
+ var samples = this.getSampleArray(size, outputSize, bps, str);
|
|
|
+
|
|
|
+ return [
|
|
|
+ CONSTRUCT_SAMPLED, inputSize, domain, encode, decode, samples, size,
|
|
|
+ outputSize, Math.pow(2, bps) - 1, range
|
|
|
+ ];
|
|
|
},
|
|
|
- parseCharsets: function CFFParser_parseCharsets(pos, length, strings, cid) {
|
|
|
- if (pos === 0) {
|
|
|
- return new CFFCharset(true, CFFCharsetPredefinedTypes.ISO_ADOBE,
|
|
|
- ISOAdobeCharset);
|
|
|
- } else if (pos === 1) {
|
|
|
- return new CFFCharset(true, CFFCharsetPredefinedTypes.EXPERT,
|
|
|
- ExpertCharset);
|
|
|
- } else if (pos === 2) {
|
|
|
- return new CFFCharset(true, CFFCharsetPredefinedTypes.EXPERT_SUBSET,
|
|
|
- ExpertSubsetCharset);
|
|
|
+
|
|
|
+ constructSampledFromIR: function PDFFunction_constructSampledFromIR(IR) {
|
|
|
+ // See chapter 3, page 109 of the PDF reference
|
|
|
+ function interpolate(x, xmin, xmax, ymin, ymax) {
|
|
|
+ return ymin + ((x - xmin) * ((ymax - ymin) / (xmax - xmin)));
|
|
|
}
|
|
|
|
|
|
- var bytes = this.bytes;
|
|
|
- var start = pos;
|
|
|
- var format = bytes[pos++];
|
|
|
- var charset = ['.notdef'];
|
|
|
- var id, count, i;
|
|
|
+ return function constructSampledFromIRResult(src, srcOffset,
|
|
|
+ dest, destOffset) {
|
|
|
+ // See chapter 3, page 110 of the PDF reference.
|
|
|
+ var m = IR[1];
|
|
|
+ var domain = IR[2];
|
|
|
+ var encode = IR[3];
|
|
|
+ var decode = IR[4];
|
|
|
+ var samples = IR[5];
|
|
|
+ var size = IR[6];
|
|
|
+ var n = IR[7];
|
|
|
+ //var mask = IR[8];
|
|
|
+ var range = IR[9];
|
|
|
|
|
|
- // subtract 1 for the .notdef glyph
|
|
|
- length -= 1;
|
|
|
+ // Building the cube vertices: its part and sample index
|
|
|
+ // http://rjwagner49.com/Mathematics/Interpolation.pdf
|
|
|
+ var cubeVertices = 1 << m;
|
|
|
+ var cubeN = new Float64Array(cubeVertices);
|
|
|
+ var cubeVertex = new Uint32Array(cubeVertices);
|
|
|
+ var i, j;
|
|
|
+ for (j = 0; j < cubeVertices; j++) {
|
|
|
+ cubeN[j] = 1;
|
|
|
+ }
|
|
|
|
|
|
- switch (format) {
|
|
|
- case 0:
|
|
|
- for (i = 0; i < length; i++) {
|
|
|
- id = (bytes[pos++] << 8) | bytes[pos++];
|
|
|
- charset.push(cid ? id : strings.get(id));
|
|
|
- }
|
|
|
- break;
|
|
|
- case 1:
|
|
|
- while (charset.length <= length) {
|
|
|
- id = (bytes[pos++] << 8) | bytes[pos++];
|
|
|
- count = bytes[pos++];
|
|
|
- for (i = 0; i <= count; i++) {
|
|
|
- charset.push(cid ? id++ : strings.get(id++));
|
|
|
+ var k = n, pos = 1;
|
|
|
+ // Map x_i to y_j for 0 <= i < m using the sampled function.
|
|
|
+ for (i = 0; i < m; ++i) {
|
|
|
+ // x_i' = min(max(x_i, Domain_2i), Domain_2i+1)
|
|
|
+ var domain_2i = domain[i][0];
|
|
|
+ var domain_2i_1 = domain[i][1];
|
|
|
+ var xi = Math.min(Math.max(src[srcOffset +i], domain_2i),
|
|
|
+ domain_2i_1);
|
|
|
+
|
|
|
+ // e_i = Interpolate(x_i', Domain_2i, Domain_2i+1,
|
|
|
+ // Encode_2i, Encode_2i+1)
|
|
|
+ var e = interpolate(xi, domain_2i, domain_2i_1,
|
|
|
+ encode[i][0], encode[i][1]);
|
|
|
+
|
|
|
+ // e_i' = min(max(e_i, 0), Size_i - 1)
|
|
|
+ var size_i = size[i];
|
|
|
+ e = Math.min(Math.max(e, 0), size_i - 1);
|
|
|
+
|
|
|
+ // Adjusting the cube: N and vertex sample index
|
|
|
+ var e0 = e < size_i - 1 ? Math.floor(e) : e - 1; // e1 = e0 + 1;
|
|
|
+ var n0 = e0 + 1 - e; // (e1 - e) / (e1 - e0);
|
|
|
+ var n1 = e - e0; // (e - e0) / (e1 - e0);
|
|
|
+ var offset0 = e0 * k;
|
|
|
+ var offset1 = offset0 + k; // e1 * k
|
|
|
+ for (j = 0; j < cubeVertices; j++) {
|
|
|
+ if (j & pos) {
|
|
|
+ cubeN[j] *= n1;
|
|
|
+ cubeVertex[j] += offset1;
|
|
|
+ } else {
|
|
|
+ cubeN[j] *= n0;
|
|
|
+ cubeVertex[j] += offset0;
|
|
|
}
|
|
|
}
|
|
|
- break;
|
|
|
- case 2:
|
|
|
- while (charset.length <= length) {
|
|
|
- id = (bytes[pos++] << 8) | bytes[pos++];
|
|
|
- count = (bytes[pos++] << 8) | bytes[pos++];
|
|
|
- for (i = 0; i <= count; i++) {
|
|
|
- charset.push(cid ? id++ : strings.get(id++));
|
|
|
- }
|
|
|
+
|
|
|
+ k *= size_i;
|
|
|
+ pos <<= 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (j = 0; j < n; ++j) {
|
|
|
+ // Sum all cube vertices' samples portions
|
|
|
+ var rj = 0;
|
|
|
+ for (i = 0; i < cubeVertices; i++) {
|
|
|
+ rj += samples[cubeVertex[i] + j] * cubeN[i];
|
|
|
}
|
|
|
- break;
|
|
|
- default:
|
|
|
- error('Unknown charset format');
|
|
|
+
|
|
|
+ // r_j' = Interpolate(r_j, 0, 2^BitsPerSample - 1,
|
|
|
+ // Decode_2j, Decode_2j+1)
|
|
|
+ rj = interpolate(rj, 0, 1, decode[j][0], decode[j][1]);
|
|
|
+
|
|
|
+ // y_j = min(max(r_j, range_2j), range_2j+1)
|
|
|
+ dest[destOffset + j] = Math.min(Math.max(rj, range[j][0]),
|
|
|
+ range[j][1]);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ constructInterpolated: function PDFFunction_constructInterpolated(str,
|
|
|
+ dict) {
|
|
|
+ var c0 = dict.get('C0') || [0];
|
|
|
+ var c1 = dict.get('C1') || [1];
|
|
|
+ var n = dict.get('N');
|
|
|
+
|
|
|
+ if (!isArray(c0) || !isArray(c1)) {
|
|
|
+ error('Illegal dictionary for interpolated function');
|
|
|
}
|
|
|
- // Raw won't be needed if we actually compile the charset.
|
|
|
- var end = pos;
|
|
|
- var raw = bytes.subarray(start, end);
|
|
|
|
|
|
- return new CFFCharset(false, format, charset, raw);
|
|
|
+ var length = c0.length;
|
|
|
+ var diff = [];
|
|
|
+ for (var i = 0; i < length; ++i) {
|
|
|
+ diff.push(c1[i] - c0[i]);
|
|
|
+ }
|
|
|
+
|
|
|
+ return [CONSTRUCT_INTERPOLATED, c0, diff, n];
|
|
|
},
|
|
|
- parseEncoding: function CFFParser_parseEncoding(pos,
|
|
|
- properties,
|
|
|
- strings,
|
|
|
- charset) {
|
|
|
- var encoding = Object.create(null);
|
|
|
- var bytes = this.bytes;
|
|
|
- var predefined = false;
|
|
|
- var hasSupplement = false;
|
|
|
- var format, i, ii;
|
|
|
- var raw = null;
|
|
|
|
|
|
- function readSupplement() {
|
|
|
- var supplementsCount = bytes[pos++];
|
|
|
- for (i = 0; i < supplementsCount; i++) {
|
|
|
- var code = bytes[pos++];
|
|
|
- var sid = (bytes[pos++] << 8) + (bytes[pos++] & 0xff);
|
|
|
- encoding[code] = charset.indexOf(strings.get(sid));
|
|
|
+ constructInterpolatedFromIR:
|
|
|
+ function PDFFunction_constructInterpolatedFromIR(IR) {
|
|
|
+ var c0 = IR[1];
|
|
|
+ var diff = IR[2];
|
|
|
+ var n = IR[3];
|
|
|
+
|
|
|
+ var length = diff.length;
|
|
|
+
|
|
|
+ return function constructInterpolatedFromIRResult(src, srcOffset,
|
|
|
+ dest, destOffset) {
|
|
|
+ var x = n === 1 ? src[srcOffset] : Math.pow(src[srcOffset], n);
|
|
|
+
|
|
|
+ for (var j = 0; j < length; ++j) {
|
|
|
+ dest[destOffset + j] = c0[j] + (x * diff[j]);
|
|
|
}
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ constructStiched: function PDFFunction_constructStiched(fn, dict, xref) {
|
|
|
+ var domain = dict.get('Domain');
|
|
|
+
|
|
|
+ if (!domain) {
|
|
|
+ error('No domain');
|
|
|
}
|
|
|
|
|
|
- if (pos === 0 || pos === 1) {
|
|
|
- predefined = true;
|
|
|
- format = pos;
|
|
|
- var baseEncoding = pos ? ExpertEncoding : StandardEncoding;
|
|
|
- for (i = 0, ii = charset.length; i < ii; i++) {
|
|
|
- var index = baseEncoding.indexOf(charset[i]);
|
|
|
- if (index !== -1) {
|
|
|
- encoding[index] = i;
|
|
|
+ var inputSize = domain.length / 2;
|
|
|
+ if (inputSize !== 1) {
|
|
|
+ error('Bad domain for stiched function');
|
|
|
+ }
|
|
|
+
|
|
|
+ var fnRefs = dict.get('Functions');
|
|
|
+ var fns = [];
|
|
|
+ for (var i = 0, ii = fnRefs.length; i < ii; ++i) {
|
|
|
+ fns.push(PDFFunction.getIR(xref, xref.fetchIfRef(fnRefs[i])));
|
|
|
+ }
|
|
|
+
|
|
|
+ var bounds = dict.get('Bounds');
|
|
|
+ var encode = dict.get('Encode');
|
|
|
+
|
|
|
+ return [CONSTRUCT_STICHED, domain, bounds, encode, fns];
|
|
|
+ },
|
|
|
+
|
|
|
+ constructStichedFromIR: function PDFFunction_constructStichedFromIR(IR) {
|
|
|
+ var domain = IR[1];
|
|
|
+ var bounds = IR[2];
|
|
|
+ var encode = IR[3];
|
|
|
+ var fnsIR = IR[4];
|
|
|
+ var fns = [];
|
|
|
+ var tmpBuf = new Float32Array(1);
|
|
|
+
|
|
|
+ for (var i = 0, ii = fnsIR.length; i < ii; i++) {
|
|
|
+ fns.push(PDFFunction.fromIR(fnsIR[i]));
|
|
|
+ }
|
|
|
+
|
|
|
+ return function constructStichedFromIRResult(src, srcOffset,
|
|
|
+ dest, destOffset) {
|
|
|
+ var clip = function constructStichedFromIRClip(v, min, max) {
|
|
|
+ if (v > max) {
|
|
|
+ v = max;
|
|
|
+ } else if (v < min) {
|
|
|
+ v = min;
|
|
|
}
|
|
|
- }
|
|
|
- } else {
|
|
|
- var dataStart = pos;
|
|
|
- format = bytes[pos++];
|
|
|
- switch (format & 0x7f) {
|
|
|
- case 0:
|
|
|
- var glyphsCount = bytes[pos++];
|
|
|
- for (i = 1; i <= glyphsCount; i++) {
|
|
|
- encoding[bytes[pos++]] = i;
|
|
|
- }
|
|
|
- break;
|
|
|
+ return v;
|
|
|
+ };
|
|
|
|
|
|
- case 1:
|
|
|
- var rangesCount = bytes[pos++];
|
|
|
- var gid = 1;
|
|
|
- for (i = 0; i < rangesCount; i++) {
|
|
|
- var start = bytes[pos++];
|
|
|
- var left = bytes[pos++];
|
|
|
- for (var j = start; j <= start + left; j++) {
|
|
|
- encoding[j] = gid++;
|
|
|
- }
|
|
|
- }
|
|
|
+ // clip to domain
|
|
|
+ var v = clip(src[srcOffset], domain[0], domain[1]);
|
|
|
+ // calulate which bound the value is in
|
|
|
+ for (var i = 0, ii = bounds.length; i < ii; ++i) {
|
|
|
+ if (v < bounds[i]) {
|
|
|
break;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- default:
|
|
|
- error('Unknow encoding format: ' + format + ' in CFF');
|
|
|
- break;
|
|
|
+ // encode value into domain of function
|
|
|
+ var dmin = domain[0];
|
|
|
+ if (i > 0) {
|
|
|
+ dmin = bounds[i - 1];
|
|
|
}
|
|
|
- var dataEnd = pos;
|
|
|
- if (format & 0x80) {
|
|
|
- // The font sanitizer does not support CFF encoding with a
|
|
|
- // supplement, since the encoding is not really used to map
|
|
|
- // between gid to glyph, let's overwrite what is declared in
|
|
|
- // the top dictionary to let the sanitizer think the font use
|
|
|
- // StandardEncoding, that's a lie but that's ok.
|
|
|
- bytes[dataStart] &= 0x7f;
|
|
|
- readSupplement();
|
|
|
- hasSupplement = true;
|
|
|
+ var dmax = domain[1];
|
|
|
+ if (i < bounds.length) {
|
|
|
+ dmax = bounds[i];
|
|
|
}
|
|
|
- raw = bytes.subarray(dataStart, dataEnd);
|
|
|
+
|
|
|
+ var rmin = encode[2 * i];
|
|
|
+ var rmax = encode[2 * i + 1];
|
|
|
+
|
|
|
+ // 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);
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ constructPostScript: function PDFFunction_constructPostScript(fn, dict,
|
|
|
+ xref) {
|
|
|
+ var domain = dict.get('Domain');
|
|
|
+ var range = dict.get('Range');
|
|
|
+
|
|
|
+ if (!domain) {
|
|
|
+ error('No domain.');
|
|
|
}
|
|
|
- format = format & 0x7f;
|
|
|
- return new CFFEncoding(predefined, format, encoding, raw);
|
|
|
+
|
|
|
+ if (!range) {
|
|
|
+ error('No range.');
|
|
|
+ }
|
|
|
+
|
|
|
+ var lexer = new PostScriptLexer(fn);
|
|
|
+ var parser = new PostScriptParser(lexer);
|
|
|
+ var code = parser.parse();
|
|
|
+
|
|
|
+ return [CONSTRUCT_POSTSCRIPT, domain, range, code];
|
|
|
},
|
|
|
- parseFDSelect: function CFFParser_parseFDSelect(pos, length) {
|
|
|
- var start = pos;
|
|
|
- var bytes = this.bytes;
|
|
|
- var format = bytes[pos++];
|
|
|
- var fdSelect = [];
|
|
|
- var i;
|
|
|
|
|
|
- switch (format) {
|
|
|
- case 0:
|
|
|
- for (i = 0; i < length; ++i) {
|
|
|
- var id = bytes[pos++];
|
|
|
- fdSelect.push(id);
|
|
|
- }
|
|
|
- break;
|
|
|
- case 3:
|
|
|
- var rangesCount = (bytes[pos++] << 8) | bytes[pos++];
|
|
|
- for (i = 0; i < rangesCount; ++i) {
|
|
|
- var first = (bytes[pos++] << 8) | bytes[pos++];
|
|
|
- var fdIndex = bytes[pos++];
|
|
|
- var next = (bytes[pos] << 8) | bytes[pos + 1];
|
|
|
- for (var j = first; j < next; ++j) {
|
|
|
- fdSelect.push(fdIndex);
|
|
|
- }
|
|
|
- }
|
|
|
- // Advance past the sentinel(next).
|
|
|
- pos += 2;
|
|
|
- break;
|
|
|
- default:
|
|
|
- error('Unknown fdselect format ' + format);
|
|
|
- break;
|
|
|
+ constructPostScriptFromIR: function PDFFunction_constructPostScriptFromIR(
|
|
|
+ IR) {
|
|
|
+ var domain = IR[1];
|
|
|
+ var range = IR[2];
|
|
|
+ var code = IR[3];
|
|
|
+
|
|
|
+ var compiled = (new PostScriptCompiler()).compile(code, domain, range);
|
|
|
+ if (compiled) {
|
|
|
+ // Compiled function consists of simple expressions such as addition,
|
|
|
+ // subtraction, Math.max, and also contains 'var' and 'return'
|
|
|
+ // statements. See the generation in the PostScriptCompiler below.
|
|
|
+ /*jshint -W054 */
|
|
|
+ return new Function('src', 'srcOffset', 'dest', 'destOffset', compiled);
|
|
|
}
|
|
|
- var end = pos;
|
|
|
- return new CFFFDSelect(fdSelect, bytes.subarray(start, end));
|
|
|
- }
|
|
|
- };
|
|
|
- return CFFParser;
|
|
|
-})();
|
|
|
|
|
|
-// Compact Font Format
|
|
|
-var CFF = (function CFFClosure() {
|
|
|
- function CFF() {
|
|
|
- this.header = null;
|
|
|
- this.names = [];
|
|
|
- this.topDict = null;
|
|
|
- this.strings = new CFFStrings();
|
|
|
- this.globalSubrIndex = null;
|
|
|
+ info('Unable to compile PS function');
|
|
|
|
|
|
- // The following could really be per font, but since we only have one font
|
|
|
- // store them here.
|
|
|
- this.encoding = null;
|
|
|
- this.charset = null;
|
|
|
- this.charStrings = null;
|
|
|
- this.fdArray = [];
|
|
|
- this.fdSelect = null;
|
|
|
+ var numOutputs = range.length >> 1;
|
|
|
+ var numInputs = domain.length >> 1;
|
|
|
+ var evaluator = new PostScriptEvaluator(code);
|
|
|
+ // Cache the values for a big speed up, the cache size is limited though
|
|
|
+ // since the number of possible values can be huge from a PS function.
|
|
|
+ var cache = Object.create(null);
|
|
|
+ // The MAX_CACHE_SIZE is set to ~4x the maximum number of distinct values
|
|
|
+ // seen in our tests.
|
|
|
+ var MAX_CACHE_SIZE = 2048 * 4;
|
|
|
+ var cache_available = MAX_CACHE_SIZE;
|
|
|
+ var tmpBuf = new Float32Array(numInputs);
|
|
|
|
|
|
- this.isCIDFont = false;
|
|
|
- }
|
|
|
- return CFF;
|
|
|
-})();
|
|
|
+ return function constructPostScriptFromIRResult(src, srcOffset,
|
|
|
+ dest, destOffset) {
|
|
|
+ var i, value;
|
|
|
+ var key = '';
|
|
|
+ var input = tmpBuf;
|
|
|
+ for (i = 0; i < numInputs; i++) {
|
|
|
+ value = src[srcOffset + i];
|
|
|
+ input[i] = value;
|
|
|
+ key += value + '_';
|
|
|
+ }
|
|
|
|
|
|
-var CFFHeader = (function CFFHeaderClosure() {
|
|
|
- function CFFHeader(major, minor, hdrSize, offSize) {
|
|
|
- this.major = major;
|
|
|
- this.minor = minor;
|
|
|
- this.hdrSize = hdrSize;
|
|
|
- this.offSize = offSize;
|
|
|
- }
|
|
|
- return CFFHeader;
|
|
|
-})();
|
|
|
+ var cachedValue = cache[key];
|
|
|
+ if (cachedValue !== undefined) {
|
|
|
+ dest.set(cachedValue, destOffset);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
-var CFFStrings = (function CFFStringsClosure() {
|
|
|
- function CFFStrings() {
|
|
|
- this.strings = [];
|
|
|
- }
|
|
|
- CFFStrings.prototype = {
|
|
|
- get: function CFFStrings_get(index) {
|
|
|
- if (index >= 0 && index <= 390) {
|
|
|
- return CFFStandardStrings[index];
|
|
|
- }
|
|
|
- if (index - 391 <= this.strings.length) {
|
|
|
- return this.strings[index - 391];
|
|
|
- }
|
|
|
- return CFFStandardStrings[0];
|
|
|
- },
|
|
|
- add: function CFFStrings_add(value) {
|
|
|
- this.strings.push(value);
|
|
|
- },
|
|
|
- get count() {
|
|
|
- return this.strings.length;
|
|
|
+ var output = new Float32Array(numOutputs);
|
|
|
+ var stack = evaluator.execute(input);
|
|
|
+ var stackIndex = stack.length - numOutputs;
|
|
|
+ for (i = 0; i < numOutputs; i++) {
|
|
|
+ value = stack[stackIndex + i];
|
|
|
+ var bound = range[i * 2];
|
|
|
+ if (value < bound) {
|
|
|
+ value = bound;
|
|
|
+ } else {
|
|
|
+ bound = range[i * 2 +1];
|
|
|
+ if (value > bound) {
|
|
|
+ value = bound;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ output[i] = value;
|
|
|
+ }
|
|
|
+ if (cache_available > 0) {
|
|
|
+ cache_available--;
|
|
|
+ cache[key] = output;
|
|
|
+ }
|
|
|
+ dest.set(output, destOffset);
|
|
|
+ };
|
|
|
}
|
|
|
};
|
|
|
- return CFFStrings;
|
|
|
})();
|
|
|
|
|
|
-var CFFIndex = (function CFFIndexClosure() {
|
|
|
- function CFFIndex() {
|
|
|
- this.objects = [];
|
|
|
- this.length = 0;
|
|
|
+function isPDFFunction(v) {
|
|
|
+ var fnDict;
|
|
|
+ if (typeof v !== 'object') {
|
|
|
+ return false;
|
|
|
+ } else if (isDict(v)) {
|
|
|
+ fnDict = v;
|
|
|
+ } else if (isStream(v)) {
|
|
|
+ fnDict = v.dict;
|
|
|
+ } else {
|
|
|
+ return false;
|
|
|
}
|
|
|
- CFFIndex.prototype = {
|
|
|
- add: function CFFIndex_add(data) {
|
|
|
- this.length += data.length;
|
|
|
- this.objects.push(data);
|
|
|
- },
|
|
|
- set: function CFFIndex_set(index, data) {
|
|
|
- this.length += data.length - this.objects[index].length;
|
|
|
- this.objects[index] = data;
|
|
|
- },
|
|
|
- get: function CFFIndex_get(index) {
|
|
|
- return this.objects[index];
|
|
|
- },
|
|
|
- get count() {
|
|
|
- return this.objects.length;
|
|
|
- }
|
|
|
- };
|
|
|
- return CFFIndex;
|
|
|
-})();
|
|
|
+ return fnDict.has('FunctionType');
|
|
|
+}
|
|
|
|
|
|
-var CFFDict = (function CFFDictClosure() {
|
|
|
- function CFFDict(tables, strings) {
|
|
|
- this.keyToNameMap = tables.keyToNameMap;
|
|
|
- this.nameToKeyMap = tables.nameToKeyMap;
|
|
|
- this.defaults = tables.defaults;
|
|
|
- this.types = tables.types;
|
|
|
- this.opcodes = tables.opcodes;
|
|
|
- this.order = tables.order;
|
|
|
- this.strings = strings;
|
|
|
- this.values = Object.create(null);
|
|
|
+var PostScriptStack = (function PostScriptStackClosure() {
|
|
|
+ var MAX_STACK_SIZE = 100;
|
|
|
+ function PostScriptStack(initialStack) {
|
|
|
+ this.stack = !initialStack ? [] :
|
|
|
+ Array.prototype.slice.call(initialStack, 0);
|
|
|
}
|
|
|
- CFFDict.prototype = {
|
|
|
- // value should always be an array
|
|
|
- setByKey: function CFFDict_setByKey(key, value) {
|
|
|
- if (!(key in this.keyToNameMap)) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- // ignore empty values
|
|
|
- if (value.length === 0) {
|
|
|
- return true;
|
|
|
+
|
|
|
+ PostScriptStack.prototype = {
|
|
|
+ push: function PostScriptStack_push(value) {
|
|
|
+ if (this.stack.length >= MAX_STACK_SIZE) {
|
|
|
+ error('PostScript function stack overflow.');
|
|
|
}
|
|
|
- var type = this.types[key];
|
|
|
- // remove the array wrapping these types of values
|
|
|
- if (type === 'num' || type === 'sid' || type === 'offset') {
|
|
|
- value = value[0];
|
|
|
+ this.stack.push(value);
|
|
|
+ },
|
|
|
+ pop: function PostScriptStack_pop() {
|
|
|
+ if (this.stack.length <= 0) {
|
|
|
+ error('PostScript function stack underflow.');
|
|
|
}
|
|
|
- this.values[key] = value;
|
|
|
- return true;
|
|
|
+ return this.stack.pop();
|
|
|
},
|
|
|
- setByName: function CFFDict_setByName(name, value) {
|
|
|
- if (!(name in this.nameToKeyMap)) {
|
|
|
- error('Invalid dictionary name "' + name + '"');
|
|
|
+ copy: function PostScriptStack_copy(n) {
|
|
|
+ if (this.stack.length + n >= MAX_STACK_SIZE) {
|
|
|
+ error('PostScript function stack overflow.');
|
|
|
+ }
|
|
|
+ var stack = this.stack;
|
|
|
+ for (var i = stack.length - n, j = n - 1; j >= 0; j--, i++) {
|
|
|
+ stack.push(stack[i]);
|
|
|
}
|
|
|
- this.values[this.nameToKeyMap[name]] = value;
|
|
|
},
|
|
|
- hasName: function CFFDict_hasName(name) {
|
|
|
- return this.nameToKeyMap[name] in this.values;
|
|
|
+ index: function PostScriptStack_index(n) {
|
|
|
+ this.push(this.stack[this.stack.length - n - 1]);
|
|
|
},
|
|
|
- getByName: function CFFDict_getByName(name) {
|
|
|
- if (!(name in this.nameToKeyMap)) {
|
|
|
- error('Invalid dictionary name "' + name + '"');
|
|
|
+ // rotate the last n stack elements p times
|
|
|
+ roll: function PostScriptStack_roll(n, p) {
|
|
|
+ var stack = this.stack;
|
|
|
+ var l = stack.length - n;
|
|
|
+ var r = stack.length - 1, c = l + (p - Math.floor(p / n) * n), i, j, t;
|
|
|
+ for (i = l, j = r; i < j; i++, j--) {
|
|
|
+ t = stack[i]; stack[i] = stack[j]; stack[j] = t;
|
|
|
}
|
|
|
- var key = this.nameToKeyMap[name];
|
|
|
- if (!(key in this.values)) {
|
|
|
- return this.defaults[key];
|
|
|
+ for (i = l, j = c - 1; i < j; i++, j--) {
|
|
|
+ t = stack[i]; stack[i] = stack[j]; stack[j] = t;
|
|
|
+ }
|
|
|
+ for (i = c, j = r; i < j; i++, j--) {
|
|
|
+ t = stack[i]; stack[i] = stack[j]; stack[j] = t;
|
|
|
}
|
|
|
- return this.values[key];
|
|
|
- },
|
|
|
- removeByName: function CFFDict_removeByName(name) {
|
|
|
- delete this.values[this.nameToKeyMap[name]];
|
|
|
- }
|
|
|
- };
|
|
|
- CFFDict.createTables = function CFFDict_createTables(layout) {
|
|
|
- var tables = {
|
|
|
- keyToNameMap: {},
|
|
|
- nameToKeyMap: {},
|
|
|
- defaults: {},
|
|
|
- types: {},
|
|
|
- opcodes: {},
|
|
|
- order: []
|
|
|
- };
|
|
|
- for (var i = 0, ii = layout.length; i < ii; ++i) {
|
|
|
- var entry = layout[i];
|
|
|
- var key = isArray(entry[0]) ? (entry[0][0] << 8) + entry[0][1] : entry[0];
|
|
|
- tables.keyToNameMap[key] = entry[1];
|
|
|
- tables.nameToKeyMap[entry[1]] = key;
|
|
|
- tables.types[key] = entry[2];
|
|
|
- tables.defaults[key] = entry[3];
|
|
|
- tables.opcodes[key] = isArray(entry[0]) ? entry[0] : [entry[0]];
|
|
|
- tables.order.push(key);
|
|
|
}
|
|
|
- return tables;
|
|
|
};
|
|
|
- return CFFDict;
|
|
|
-})();
|
|
|
-
|
|
|
-var CFFTopDict = (function CFFTopDictClosure() {
|
|
|
- var layout = [
|
|
|
- [[12, 30], 'ROS', ['sid', 'sid', 'num'], null],
|
|
|
- [[12, 20], 'SyntheticBase', 'num', null],
|
|
|
- [0, 'version', 'sid', null],
|
|
|
- [1, 'Notice', 'sid', null],
|
|
|
- [[12, 0], 'Copyright', 'sid', null],
|
|
|
- [2, 'FullName', 'sid', null],
|
|
|
- [3, 'FamilyName', 'sid', null],
|
|
|
- [4, 'Weight', 'sid', null],
|
|
|
- [[12, 1], 'isFixedPitch', 'num', 0],
|
|
|
- [[12, 2], 'ItalicAngle', 'num', 0],
|
|
|
- [[12, 3], 'UnderlinePosition', 'num', -100],
|
|
|
- [[12, 4], 'UnderlineThickness', 'num', 50],
|
|
|
- [[12, 5], 'PaintType', 'num', 0],
|
|
|
- [[12, 6], 'CharstringType', 'num', 2],
|
|
|
- [[12, 7], 'FontMatrix', ['num', 'num', 'num', 'num', 'num', 'num'],
|
|
|
- [0.001, 0, 0, 0.001, 0, 0]],
|
|
|
- [13, 'UniqueID', 'num', null],
|
|
|
- [5, 'FontBBox', ['num', 'num', 'num', 'num'], [0, 0, 0, 0]],
|
|
|
- [[12, 8], 'StrokeWidth', 'num', 0],
|
|
|
- [14, 'XUID', 'array', null],
|
|
|
- [15, 'charset', 'offset', 0],
|
|
|
- [16, 'Encoding', 'offset', 0],
|
|
|
- [17, 'CharStrings', 'offset', 0],
|
|
|
- [18, 'Private', ['offset', 'offset'], null],
|
|
|
- [[12, 21], 'PostScript', 'sid', null],
|
|
|
- [[12, 22], 'BaseFontName', 'sid', null],
|
|
|
- [[12, 23], 'BaseFontBlend', 'delta', null],
|
|
|
- [[12, 31], 'CIDFontVersion', 'num', 0],
|
|
|
- [[12, 32], 'CIDFontRevision', 'num', 0],
|
|
|
- [[12, 33], 'CIDFontType', 'num', 0],
|
|
|
- [[12, 34], 'CIDCount', 'num', 8720],
|
|
|
- [[12, 35], 'UIDBase', 'num', null],
|
|
|
- // XXX: CID Fonts on DirectWrite 6.1 only seem to work if FDSelect comes
|
|
|
- // before FDArray.
|
|
|
- [[12, 37], 'FDSelect', 'offset', null],
|
|
|
- [[12, 36], 'FDArray', 'offset', null],
|
|
|
- [[12, 38], 'FontName', 'sid', null]
|
|
|
- ];
|
|
|
- var tables = null;
|
|
|
- function CFFTopDict(strings) {
|
|
|
- if (tables === null) {
|
|
|
- tables = CFFDict.createTables(layout);
|
|
|
- }
|
|
|
- CFFDict.call(this, tables, strings);
|
|
|
- this.privateDict = null;
|
|
|
- }
|
|
|
- CFFTopDict.prototype = Object.create(CFFDict.prototype);
|
|
|
- return CFFTopDict;
|
|
|
-})();
|
|
|
-
|
|
|
-var CFFPrivateDict = (function CFFPrivateDictClosure() {
|
|
|
- var layout = [
|
|
|
- [6, 'BlueValues', 'delta', null],
|
|
|
- [7, 'OtherBlues', 'delta', null],
|
|
|
- [8, 'FamilyBlues', 'delta', null],
|
|
|
- [9, 'FamilyOtherBlues', 'delta', null],
|
|
|
- [[12, 9], 'BlueScale', 'num', 0.039625],
|
|
|
- [[12, 10], 'BlueShift', 'num', 7],
|
|
|
- [[12, 11], 'BlueFuzz', 'num', 1],
|
|
|
- [10, 'StdHW', 'num', null],
|
|
|
- [11, 'StdVW', 'num', null],
|
|
|
- [[12, 12], 'StemSnapH', 'delta', null],
|
|
|
- [[12, 13], 'StemSnapV', 'delta', null],
|
|
|
- [[12, 14], 'ForceBold', 'num', 0],
|
|
|
- [[12, 17], 'LanguageGroup', 'num', 0],
|
|
|
- [[12, 18], 'ExpansionFactor', 'num', 0.06],
|
|
|
- [[12, 19], 'initialRandomSeed', 'num', 0],
|
|
|
- [20, 'defaultWidthX', 'num', 0],
|
|
|
- [21, 'nominalWidthX', 'num', 0],
|
|
|
- [19, 'Subrs', 'offset', null]
|
|
|
- ];
|
|
|
- var tables = null;
|
|
|
- function CFFPrivateDict(strings) {
|
|
|
- if (tables === null) {
|
|
|
- tables = CFFDict.createTables(layout);
|
|
|
- }
|
|
|
- CFFDict.call(this, tables, strings);
|
|
|
- this.subrsIndex = null;
|
|
|
- }
|
|
|
- CFFPrivateDict.prototype = Object.create(CFFDict.prototype);
|
|
|
- return CFFPrivateDict;
|
|
|
-})();
|
|
|
-
|
|
|
-var CFFCharsetPredefinedTypes = {
|
|
|
- ISO_ADOBE: 0,
|
|
|
- EXPERT: 1,
|
|
|
- EXPERT_SUBSET: 2
|
|
|
-};
|
|
|
-var CFFCharset = (function CFFCharsetClosure() {
|
|
|
- function CFFCharset(predefined, format, charset, raw) {
|
|
|
- this.predefined = predefined;
|
|
|
- this.format = format;
|
|
|
- this.charset = charset;
|
|
|
- this.raw = raw;
|
|
|
- }
|
|
|
- return CFFCharset;
|
|
|
+ return PostScriptStack;
|
|
|
})();
|
|
|
-
|
|
|
-var CFFEncoding = (function CFFEncodingClosure() {
|
|
|
- function CFFEncoding(predefined, format, encoding, raw) {
|
|
|
- this.predefined = predefined;
|
|
|
- this.format = format;
|
|
|
- this.encoding = encoding;
|
|
|
- this.raw = raw;
|
|
|
+var PostScriptEvaluator = (function PostScriptEvaluatorClosure() {
|
|
|
+ function PostScriptEvaluator(operators) {
|
|
|
+ this.operators = operators;
|
|
|
}
|
|
|
- return CFFEncoding;
|
|
|
-})();
|
|
|
+ PostScriptEvaluator.prototype = {
|
|
|
+ execute: function PostScriptEvaluator_execute(initialStack) {
|
|
|
+ var stack = new PostScriptStack(initialStack);
|
|
|
+ var counter = 0;
|
|
|
+ var operators = this.operators;
|
|
|
+ var length = operators.length;
|
|
|
+ var operator, a, b;
|
|
|
+ while (counter < length) {
|
|
|
+ operator = operators[counter++];
|
|
|
+ if (typeof operator === 'number') {
|
|
|
+ // Operator is really an operand and should be pushed to the stack.
|
|
|
+ stack.push(operator);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ switch (operator) {
|
|
|
+ // non standard ps operators
|
|
|
+ case 'jz': // jump if false
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ if (!a) {
|
|
|
+ counter = b;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 'j': // jump
|
|
|
+ a = stack.pop();
|
|
|
+ counter = a;
|
|
|
+ break;
|
|
|
|
|
|
-var CFFFDSelect = (function CFFFDSelectClosure() {
|
|
|
- function CFFFDSelect(fdSelect, raw) {
|
|
|
- this.fdSelect = fdSelect;
|
|
|
- this.raw = raw;
|
|
|
- }
|
|
|
- CFFFDSelect.prototype = {
|
|
|
- getFDIndex: function CFFFDSelect_get(glyphIndex) {
|
|
|
- if (glyphIndex < 0 || glyphIndex >= this.fdSelect.length) {
|
|
|
- return -1;
|
|
|
+ // all ps operators in alphabetical order (excluding if/ifelse)
|
|
|
+ case 'abs':
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(Math.abs(a));
|
|
|
+ break;
|
|
|
+ case 'add':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(a + b);
|
|
|
+ break;
|
|
|
+ case 'and':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ if (isBool(a) && isBool(b)) {
|
|
|
+ stack.push(a && b);
|
|
|
+ } else {
|
|
|
+ stack.push(a & b);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 'atan':
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(Math.atan(a));
|
|
|
+ break;
|
|
|
+ case 'bitshift':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ if (a > 0) {
|
|
|
+ stack.push(a << b);
|
|
|
+ } else {
|
|
|
+ stack.push(a >> b);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 'ceiling':
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(Math.ceil(a));
|
|
|
+ break;
|
|
|
+ case 'copy':
|
|
|
+ a = stack.pop();
|
|
|
+ stack.copy(a);
|
|
|
+ break;
|
|
|
+ case 'cos':
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(Math.cos(a));
|
|
|
+ break;
|
|
|
+ case 'cvi':
|
|
|
+ a = stack.pop() | 0;
|
|
|
+ stack.push(a);
|
|
|
+ break;
|
|
|
+ case 'cvr':
|
|
|
+ // noop
|
|
|
+ break;
|
|
|
+ case 'div':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(a / b);
|
|
|
+ break;
|
|
|
+ case 'dup':
|
|
|
+ stack.copy(1);
|
|
|
+ break;
|
|
|
+ case 'eq':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(a === b);
|
|
|
+ break;
|
|
|
+ case 'exch':
|
|
|
+ stack.roll(2, 1);
|
|
|
+ break;
|
|
|
+ case 'exp':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(Math.pow(a, b));
|
|
|
+ break;
|
|
|
+ case 'false':
|
|
|
+ stack.push(false);
|
|
|
+ break;
|
|
|
+ case 'floor':
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(Math.floor(a));
|
|
|
+ break;
|
|
|
+ case 'ge':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(a >= b);
|
|
|
+ break;
|
|
|
+ case 'gt':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(a > b);
|
|
|
+ break;
|
|
|
+ case 'idiv':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push((a / b) | 0);
|
|
|
+ break;
|
|
|
+ case 'index':
|
|
|
+ a = stack.pop();
|
|
|
+ stack.index(a);
|
|
|
+ break;
|
|
|
+ case 'le':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(a <= b);
|
|
|
+ break;
|
|
|
+ case 'ln':
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(Math.log(a));
|
|
|
+ break;
|
|
|
+ case 'log':
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(Math.log(a) / Math.LN10);
|
|
|
+ break;
|
|
|
+ case 'lt':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(a < b);
|
|
|
+ break;
|
|
|
+ case 'mod':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(a % b);
|
|
|
+ break;
|
|
|
+ case 'mul':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(a * b);
|
|
|
+ break;
|
|
|
+ case 'ne':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(a !== b);
|
|
|
+ break;
|
|
|
+ case 'neg':
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(-a);
|
|
|
+ break;
|
|
|
+ case 'not':
|
|
|
+ a = stack.pop();
|
|
|
+ if (isBool(a)) {
|
|
|
+ stack.push(!a);
|
|
|
+ } else {
|
|
|
+ stack.push(~a);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 'or':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ if (isBool(a) && isBool(b)) {
|
|
|
+ stack.push(a || b);
|
|
|
+ } else {
|
|
|
+ stack.push(a | b);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 'pop':
|
|
|
+ stack.pop();
|
|
|
+ break;
|
|
|
+ case 'roll':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ stack.roll(a, b);
|
|
|
+ break;
|
|
|
+ case 'round':
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(Math.round(a));
|
|
|
+ break;
|
|
|
+ case 'sin':
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(Math.sin(a));
|
|
|
+ break;
|
|
|
+ case 'sqrt':
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(Math.sqrt(a));
|
|
|
+ break;
|
|
|
+ case 'sub':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ stack.push(a - b);
|
|
|
+ break;
|
|
|
+ case 'true':
|
|
|
+ stack.push(true);
|
|
|
+ break;
|
|
|
+ case 'truncate':
|
|
|
+ a = stack.pop();
|
|
|
+ a = a < 0 ? Math.ceil(a) : Math.floor(a);
|
|
|
+ stack.push(a);
|
|
|
+ break;
|
|
|
+ case 'xor':
|
|
|
+ b = stack.pop();
|
|
|
+ a = stack.pop();
|
|
|
+ if (isBool(a) && isBool(b)) {
|
|
|
+ stack.push(a !== b);
|
|
|
+ } else {
|
|
|
+ stack.push(a ^ b);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ error('Unknown operator ' + operator);
|
|
|
+ break;
|
|
|
+ }
|
|
|
}
|
|
|
- return this.fdSelect[glyphIndex];
|
|
|
+ return stack.stack;
|
|
|
}
|
|
|
};
|
|
|
- return CFFFDSelect;
|
|
|
+ return PostScriptEvaluator;
|
|
|
})();
|
|
|
|
|
|
-// Helper class to keep track of where an offset is within the data and helps
|
|
|
-// filling in that offset once it's known.
|
|
|
-var CFFOffsetTracker = (function CFFOffsetTrackerClosure() {
|
|
|
- function CFFOffsetTracker() {
|
|
|
- this.offsets = Object.create(null);
|
|
|
+// Most of the PDFs functions consist of simple operations such as:
|
|
|
+// roll, exch, sub, cvr, pop, index, dup, mul, if, gt, add.
|
|
|
+//
|
|
|
+// We can compile most of such programs, and at the same moment, we can
|
|
|
+// optimize some expressions using basic math properties. Keeping track of
|
|
|
+// min/max values will allow us to avoid extra Math.min/Math.max calls.
|
|
|
+var PostScriptCompiler = (function PostScriptCompilerClosure() {
|
|
|
+ function AstNode(type) {
|
|
|
+ this.type = type;
|
|
|
}
|
|
|
- CFFOffsetTracker.prototype = {
|
|
|
- isTracking: function CFFOffsetTracker_isTracking(key) {
|
|
|
- return key in this.offsets;
|
|
|
- },
|
|
|
- track: function CFFOffsetTracker_track(key, location) {
|
|
|
- if (key in this.offsets) {
|
|
|
- error('Already tracking location of ' + key);
|
|
|
- }
|
|
|
- this.offsets[key] = location;
|
|
|
- },
|
|
|
- offset: function CFFOffsetTracker_offset(value) {
|
|
|
- for (var key in this.offsets) {
|
|
|
- this.offsets[key] += value;
|
|
|
- }
|
|
|
- },
|
|
|
- setEntryLocation: function CFFOffsetTracker_setEntryLocation(key,
|
|
|
- values,
|
|
|
- output) {
|
|
|
- if (!(key in this.offsets)) {
|
|
|
- error('Not tracking location of ' + key);
|
|
|
- }
|
|
|
- var data = output.data;
|
|
|
- var dataOffset = this.offsets[key];
|
|
|
- var size = 5;
|
|
|
- for (var i = 0, ii = values.length; i < ii; ++i) {
|
|
|
- var offset0 = i * size + dataOffset;
|
|
|
- var offset1 = offset0 + 1;
|
|
|
- var offset2 = offset0 + 2;
|
|
|
- var offset3 = offset0 + 3;
|
|
|
- var offset4 = offset0 + 4;
|
|
|
- // It's easy to screw up offsets so perform this sanity check.
|
|
|
- if (data[offset0] !== 0x1d || data[offset1] !== 0 ||
|
|
|
- data[offset2] !== 0 || data[offset3] !== 0 || data[offset4] !== 0) {
|
|
|
- error('writing to an offset that is not empty');
|
|
|
- }
|
|
|
- var value = values[i];
|
|
|
- data[offset0] = 0x1d;
|
|
|
- data[offset1] = (value >> 24) & 0xFF;
|
|
|
- data[offset2] = (value >> 16) & 0xFF;
|
|
|
- data[offset3] = (value >> 8) & 0xFF;
|
|
|
- data[offset4] = value & 0xFF;
|
|
|
- }
|
|
|
- }
|
|
|
+ AstNode.prototype.visit = function (visitor) {
|
|
|
+ throw new Error('abstract method');
|
|
|
};
|
|
|
- return CFFOffsetTracker;
|
|
|
-})();
|
|
|
|
|
|
-// Takes a CFF and converts it to the binary representation.
|
|
|
-var CFFCompiler = (function CFFCompilerClosure() {
|
|
|
- function CFFCompiler(cff) {
|
|
|
- this.cff = cff;
|
|
|
+ function AstArgument(index, min, max) {
|
|
|
+ AstNode.call(this, 'args');
|
|
|
+ this.index = index;
|
|
|
+ this.min = min;
|
|
|
+ this.max = max;
|
|
|
}
|
|
|
- CFFCompiler.prototype = {
|
|
|
- compile: function CFFCompiler_compile() {
|
|
|
- var cff = this.cff;
|
|
|
- var output = {
|
|
|
- data: [],
|
|
|
- length: 0,
|
|
|
- add: function CFFCompiler_add(data) {
|
|
|
- this.data = this.data.concat(data);
|
|
|
- this.length = this.data.length;
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // Compile the five entries that must be in order.
|
|
|
- var header = this.compileHeader(cff.header);
|
|
|
- output.add(header);
|
|
|
-
|
|
|
- var nameIndex = this.compileNameIndex(cff.names);
|
|
|
- output.add(nameIndex);
|
|
|
-
|
|
|
- if (cff.isCIDFont) {
|
|
|
- // The spec is unclear on how font matrices should relate to each other
|
|
|
- // when there is one in the main top dict and the sub top dicts.
|
|
|
- // Windows handles this differently than linux and osx so we have to
|
|
|
- // normalize to work on all.
|
|
|
- // Rules based off of some mailing list discussions:
|
|
|
- // - If main font has a matrix and subfont doesn't, use the main matrix.
|
|
|
- // - If no main font matrix and there is a subfont matrix, use the
|
|
|
- // subfont matrix.
|
|
|
- // - If both have matrices, concat together.
|
|
|
- // - If neither have matrices, use default.
|
|
|
- // To make this work on all platforms we move the top matrix into each
|
|
|
- // sub top dict and concat if necessary.
|
|
|
- if (cff.topDict.hasName('FontMatrix')) {
|
|
|
- var base = cff.topDict.getByName('FontMatrix');
|
|
|
- cff.topDict.removeByName('FontMatrix');
|
|
|
- for (var i = 0, ii = cff.fdArray.length; i < ii; i++) {
|
|
|
- var subDict = cff.fdArray[i];
|
|
|
- var matrix = base.slice(0);
|
|
|
- if (subDict.hasName('FontMatrix')) {
|
|
|
- matrix = Util.transform(matrix, subDict.getByName('FontMatrix'));
|
|
|
- }
|
|
|
- subDict.setByName('FontMatrix', matrix);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- var compiled = this.compileTopDicts([cff.topDict],
|
|
|
- output.length,
|
|
|
- cff.isCIDFont);
|
|
|
- output.add(compiled.output);
|
|
|
- var topDictTracker = compiled.trackers[0];
|
|
|
-
|
|
|
- var stringIndex = this.compileStringIndex(cff.strings.strings);
|
|
|
- output.add(stringIndex);
|
|
|
-
|
|
|
- var globalSubrIndex = this.compileIndex(cff.globalSubrIndex);
|
|
|
- output.add(globalSubrIndex);
|
|
|
-
|
|
|
- // Now start on the other entries that have no specfic order.
|
|
|
- if (cff.encoding && cff.topDict.hasName('Encoding')) {
|
|
|
- if (cff.encoding.predefined) {
|
|
|
- topDictTracker.setEntryLocation('Encoding', [cff.encoding.format],
|
|
|
- output);
|
|
|
- } else {
|
|
|
- var encoding = this.compileEncoding(cff.encoding);
|
|
|
- topDictTracker.setEntryLocation('Encoding', [output.length], output);
|
|
|
- output.add(encoding);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (cff.charset && cff.topDict.hasName('charset')) {
|
|
|
- if (cff.charset.predefined) {
|
|
|
- topDictTracker.setEntryLocation('charset', [cff.charset.format],
|
|
|
- output);
|
|
|
- } else {
|
|
|
- var charset = this.compileCharset(cff.charset);
|
|
|
- topDictTracker.setEntryLocation('charset', [output.length], output);
|
|
|
- output.add(charset);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- var charStrings = this.compileCharStrings(cff.charStrings);
|
|
|
- topDictTracker.setEntryLocation('CharStrings', [output.length], output);
|
|
|
- output.add(charStrings);
|
|
|
-
|
|
|
- if (cff.isCIDFont) {
|
|
|
- // For some reason FDSelect must be in front of FDArray on windows. OSX
|
|
|
- // and linux don't seem to care.
|
|
|
- topDictTracker.setEntryLocation('FDSelect', [output.length], output);
|
|
|
- var fdSelect = this.compileFDSelect(cff.fdSelect.raw);
|
|
|
- output.add(fdSelect);
|
|
|
- // It is unclear if the sub font dictionary can have CID related
|
|
|
- // dictionary keys, but the sanitizer doesn't like them so remove them.
|
|
|
- compiled = this.compileTopDicts(cff.fdArray, output.length, true);
|
|
|
- topDictTracker.setEntryLocation('FDArray', [output.length], output);
|
|
|
- output.add(compiled.output);
|
|
|
- var fontDictTrackers = compiled.trackers;
|
|
|
+ AstArgument.prototype = Object.create(AstNode.prototype);
|
|
|
+ AstArgument.prototype.visit = function (visitor) {
|
|
|
+ visitor.visitArgument(this);
|
|
|
+ };
|
|
|
|
|
|
- this.compilePrivateDicts(cff.fdArray, fontDictTrackers, output);
|
|
|
- }
|
|
|
+ function AstLiteral(number) {
|
|
|
+ AstNode.call(this, 'literal');
|
|
|
+ this.number = number;
|
|
|
+ this.min = number;
|
|
|
+ this.max = number;
|
|
|
+ }
|
|
|
+ AstLiteral.prototype = Object.create(AstNode.prototype);
|
|
|
+ AstLiteral.prototype.visit = function (visitor) {
|
|
|
+ visitor.visitLiteral(this);
|
|
|
+ };
|
|
|
|
|
|
- this.compilePrivateDicts([cff.topDict], [topDictTracker], output);
|
|
|
+ function AstBinaryOperation(op, arg1, arg2, min, max) {
|
|
|
+ AstNode.call(this, 'binary');
|
|
|
+ this.op = op;
|
|
|
+ this.arg1 = arg1;
|
|
|
+ this.arg2 = arg2;
|
|
|
+ this.min = min;
|
|
|
+ this.max = max;
|
|
|
+ }
|
|
|
+ AstBinaryOperation.prototype = Object.create(AstNode.prototype);
|
|
|
+ AstBinaryOperation.prototype.visit = function (visitor) {
|
|
|
+ visitor.visitBinaryOperation(this);
|
|
|
+ };
|
|
|
|
|
|
- // If the font data ends with INDEX whose object data is zero-length,
|
|
|
- // the sanitizer will bail out. Add a dummy byte to avoid that.
|
|
|
- output.add([0]);
|
|
|
+ function AstMin(arg, max) {
|
|
|
+ AstNode.call(this, 'max');
|
|
|
+ this.arg = arg;
|
|
|
+ this.min = arg.min;
|
|
|
+ this.max = max;
|
|
|
+ }
|
|
|
+ AstMin.prototype = Object.create(AstNode.prototype);
|
|
|
+ AstMin.prototype.visit = function (visitor) {
|
|
|
+ visitor.visitMin(this);
|
|
|
+ };
|
|
|
|
|
|
- return output.data;
|
|
|
- },
|
|
|
- encodeNumber: function CFFCompiler_encodeNumber(value) {
|
|
|
- if (parseFloat(value) === parseInt(value, 10) && !isNaN(value)) { // isInt
|
|
|
- return this.encodeInteger(value);
|
|
|
- } else {
|
|
|
- return this.encodeFloat(value);
|
|
|
- }
|
|
|
- },
|
|
|
- encodeFloat: function CFFCompiler_encodeFloat(num) {
|
|
|
- var value = num.toString();
|
|
|
+ function AstVariable(index, min, max) {
|
|
|
+ AstNode.call(this, 'var');
|
|
|
+ this.index = index;
|
|
|
+ this.min = min;
|
|
|
+ this.max = max;
|
|
|
+ }
|
|
|
+ AstVariable.prototype = Object.create(AstNode.prototype);
|
|
|
+ AstVariable.prototype.visit = function (visitor) {
|
|
|
+ visitor.visitVariable(this);
|
|
|
+ };
|
|
|
|
|
|
- // rounding inaccurate doubles
|
|
|
- var m = /\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/.exec(value);
|
|
|
- if (m) {
|
|
|
- var epsilon = parseFloat('1e' + ((m[2] ? +m[2] : 0) + m[1].length));
|
|
|
- value = (Math.round(num * epsilon) / epsilon).toString();
|
|
|
- }
|
|
|
+ function AstVariableDefinition(variable, arg) {
|
|
|
+ AstNode.call(this, 'definition');
|
|
|
+ this.variable = variable;
|
|
|
+ this.arg = arg;
|
|
|
+ }
|
|
|
+ AstVariableDefinition.prototype = Object.create(AstNode.prototype);
|
|
|
+ AstVariableDefinition.prototype.visit = function (visitor) {
|
|
|
+ visitor.visitVariableDefinition(this);
|
|
|
+ };
|
|
|
|
|
|
- var nibbles = '';
|
|
|
- var i, ii;
|
|
|
- for (i = 0, ii = value.length; i < ii; ++i) {
|
|
|
- var a = value[i];
|
|
|
- if (a === 'e') {
|
|
|
- nibbles += value[++i] === '-' ? 'c' : 'b';
|
|
|
- } else if (a === '.') {
|
|
|
- nibbles += 'a';
|
|
|
- } else if (a === '-') {
|
|
|
- nibbles += 'e';
|
|
|
- } else {
|
|
|
- nibbles += a;
|
|
|
- }
|
|
|
- }
|
|
|
- nibbles += (nibbles.length & 1) ? 'f' : 'ff';
|
|
|
- var out = [30];
|
|
|
- for (i = 0, ii = nibbles.length; i < ii; i += 2) {
|
|
|
- out.push(parseInt(nibbles.substr(i, 2), 16));
|
|
|
- }
|
|
|
- return out;
|
|
|
+ function ExpressionBuilderVisitor() {
|
|
|
+ this.parts = [];
|
|
|
+ }
|
|
|
+ ExpressionBuilderVisitor.prototype = {
|
|
|
+ visitArgument: function (arg) {
|
|
|
+ this.parts.push('Math.max(', arg.min, ', Math.min(',
|
|
|
+ arg.max, ', src[srcOffset + ', arg.index, ']))');
|
|
|
},
|
|
|
- encodeInteger: function CFFCompiler_encodeInteger(value) {
|
|
|
- var code;
|
|
|
- if (value >= -107 && value <= 107) {
|
|
|
- code = [value + 139];
|
|
|
- } else if (value >= 108 && value <= 1131) {
|
|
|
- value = [value - 108];
|
|
|
- code = [(value >> 8) + 247, value & 0xFF];
|
|
|
- } else if (value >= -1131 && value <= -108) {
|
|
|
- value = -value - 108;
|
|
|
- code = [(value >> 8) + 251, value & 0xFF];
|
|
|
- } else if (value >= -32768 && value <= 32767) {
|
|
|
- code = [0x1c, (value >> 8) & 0xFF, value & 0xFF];
|
|
|
- } else {
|
|
|
- code = [0x1d,
|
|
|
- (value >> 24) & 0xFF,
|
|
|
- (value >> 16) & 0xFF,
|
|
|
- (value >> 8) & 0xFF,
|
|
|
- value & 0xFF];
|
|
|
- }
|
|
|
- return code;
|
|
|
+ visitVariable: function (variable) {
|
|
|
+ this.parts.push('v', variable.index);
|
|
|
},
|
|
|
- compileHeader: function CFFCompiler_compileHeader(header) {
|
|
|
- return [
|
|
|
- header.major,
|
|
|
- header.minor,
|
|
|
- header.hdrSize,
|
|
|
- header.offSize
|
|
|
- ];
|
|
|
+ visitLiteral: function (literal) {
|
|
|
+ this.parts.push(literal.number);
|
|
|
},
|
|
|
- compileNameIndex: function CFFCompiler_compileNameIndex(names) {
|
|
|
- var nameIndex = new CFFIndex();
|
|
|
- for (var i = 0, ii = names.length; i < ii; ++i) {
|
|
|
- nameIndex.add(stringToBytes(names[i]));
|
|
|
- }
|
|
|
- return this.compileIndex(nameIndex);
|
|
|
+ visitBinaryOperation: function (operation) {
|
|
|
+ this.parts.push('(');
|
|
|
+ operation.arg1.visit(this);
|
|
|
+ this.parts.push(' ', operation.op, ' ');
|
|
|
+ operation.arg2.visit(this);
|
|
|
+ this.parts.push(')');
|
|
|
},
|
|
|
- compileTopDicts: function CFFCompiler_compileTopDicts(dicts,
|
|
|
- length,
|
|
|
- removeCidKeys) {
|
|
|
- var fontDictTrackers = [];
|
|
|
- var fdArrayIndex = new CFFIndex();
|
|
|
- for (var i = 0, ii = dicts.length; i < ii; ++i) {
|
|
|
- var fontDict = dicts[i];
|
|
|
- if (removeCidKeys) {
|
|
|
- fontDict.removeByName('CIDFontVersion');
|
|
|
- fontDict.removeByName('CIDFontRevision');
|
|
|
- fontDict.removeByName('CIDFontType');
|
|
|
- fontDict.removeByName('CIDCount');
|
|
|
- fontDict.removeByName('UIDBase');
|
|
|
- }
|
|
|
- var fontDictTracker = new CFFOffsetTracker();
|
|
|
- var fontDictData = this.compileDict(fontDict, fontDictTracker);
|
|
|
- fontDictTrackers.push(fontDictTracker);
|
|
|
- fdArrayIndex.add(fontDictData);
|
|
|
- fontDictTracker.offset(length);
|
|
|
- }
|
|
|
- fdArrayIndex = this.compileIndex(fdArrayIndex, fontDictTrackers);
|
|
|
- return {
|
|
|
- trackers: fontDictTrackers,
|
|
|
- output: fdArrayIndex
|
|
|
- };
|
|
|
+ visitVariableDefinition: function (definition) {
|
|
|
+ this.parts.push('var ');
|
|
|
+ definition.variable.visit(this);
|
|
|
+ this.parts.push(' = ');
|
|
|
+ definition.arg.visit(this);
|
|
|
+ this.parts.push(';');
|
|
|
},
|
|
|
- compilePrivateDicts: function CFFCompiler_compilePrivateDicts(dicts,
|
|
|
- trackers,
|
|
|
- output) {
|
|
|
- for (var i = 0, ii = dicts.length; i < ii; ++i) {
|
|
|
- var fontDict = dicts[i];
|
|
|
- assert(fontDict.privateDict && fontDict.hasName('Private'),
|
|
|
- 'There must be an private dictionary.');
|
|
|
- var privateDict = fontDict.privateDict;
|
|
|
- var privateDictTracker = new CFFOffsetTracker();
|
|
|
- var privateDictData = this.compileDict(privateDict, privateDictTracker);
|
|
|
-
|
|
|
- var outputLength = output.length;
|
|
|
- privateDictTracker.offset(outputLength);
|
|
|
- if (!privateDictData.length) {
|
|
|
- // The private dictionary was empty, set the output length to zero to
|
|
|
- // ensure the offset length isn't out of bounds in the eyes of the
|
|
|
- // sanitizer.
|
|
|
- outputLength = 0;
|
|
|
- }
|
|
|
-
|
|
|
- trackers[i].setEntryLocation('Private',
|
|
|
- [privateDictData.length, outputLength],
|
|
|
- output);
|
|
|
- output.add(privateDictData);
|
|
|
-
|
|
|
- if (privateDict.subrsIndex && privateDict.hasName('Subrs')) {
|
|
|
- var subrs = this.compileIndex(privateDict.subrsIndex);
|
|
|
- privateDictTracker.setEntryLocation('Subrs', [privateDictData.length],
|
|
|
- output);
|
|
|
- output.add(subrs);
|
|
|
- }
|
|
|
- }
|
|
|
+ visitMin: function (max) {
|
|
|
+ this.parts.push('Math.min(');
|
|
|
+ max.arg.visit(this);
|
|
|
+ this.parts.push(', ', max.max, ')');
|
|
|
},
|
|
|
- compileDict: function CFFCompiler_compileDict(dict, offsetTracker) {
|
|
|
- var out = [];
|
|
|
- // The dictionary keys must be in a certain order.
|
|
|
- var order = dict.order;
|
|
|
- for (var i = 0; i < order.length; ++i) {
|
|
|
- var key = order[i];
|
|
|
- if (!(key in dict.values)) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- var values = dict.values[key];
|
|
|
- var types = dict.types[key];
|
|
|
- if (!isArray(types)) {
|
|
|
- types = [types];
|
|
|
- }
|
|
|
- if (!isArray(values)) {
|
|
|
- values = [values];
|
|
|
- }
|
|
|
-
|
|
|
- // Remove any empty dict values.
|
|
|
- if (values.length === 0) {
|
|
|
- continue;
|
|
|
- }
|
|
|
+ toString: function () {
|
|
|
+ return this.parts.join('');
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- for (var j = 0, jj = types.length; j < jj; ++j) {
|
|
|
- var type = types[j];
|
|
|
- var value = values[j];
|
|
|
- switch (type) {
|
|
|
- case 'num':
|
|
|
- case 'sid':
|
|
|
- out = out.concat(this.encodeNumber(value));
|
|
|
- break;
|
|
|
- case 'offset':
|
|
|
- // For offsets we just insert a 32bit integer so we don't have to
|
|
|
- // deal with figuring out the length of the offset when it gets
|
|
|
- // replaced later on by the compiler.
|
|
|
- var name = dict.keyToNameMap[key];
|
|
|
- // Some offsets have the offset and the length, so just record the
|
|
|
- // position of the first one.
|
|
|
- if (!offsetTracker.isTracking(name)) {
|
|
|
- offsetTracker.track(name, out.length);
|
|
|
- }
|
|
|
- out = out.concat([0x1d, 0, 0, 0, 0]);
|
|
|
- break;
|
|
|
- case 'array':
|
|
|
- case 'delta':
|
|
|
- out = out.concat(this.encodeNumber(value));
|
|
|
- for (var k = 1, kk = values.length; k < kk; ++k) {
|
|
|
- out = out.concat(this.encodeNumber(values[k]));
|
|
|
- }
|
|
|
- break;
|
|
|
- default:
|
|
|
- error('Unknown data type of ' + type);
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- out = out.concat(dict.opcodes[key]);
|
|
|
- }
|
|
|
- return out;
|
|
|
- },
|
|
|
- compileStringIndex: function CFFCompiler_compileStringIndex(strings) {
|
|
|
- var stringIndex = new CFFIndex();
|
|
|
- for (var i = 0, ii = strings.length; i < ii; ++i) {
|
|
|
- stringIndex.add(stringToBytes(strings[i]));
|
|
|
- }
|
|
|
- return this.compileIndex(stringIndex);
|
|
|
- },
|
|
|
- compileGlobalSubrIndex: function CFFCompiler_compileGlobalSubrIndex() {
|
|
|
- var globalSubrIndex = this.cff.globalSubrIndex;
|
|
|
- this.out.writeByteArray(this.compileIndex(globalSubrIndex));
|
|
|
- },
|
|
|
- compileCharStrings: function CFFCompiler_compileCharStrings(charStrings) {
|
|
|
- return this.compileIndex(charStrings);
|
|
|
- },
|
|
|
- compileCharset: function CFFCompiler_compileCharset(charset) {
|
|
|
- return this.compileTypedArray(charset.raw);
|
|
|
- },
|
|
|
- compileEncoding: function CFFCompiler_compileEncoding(encoding) {
|
|
|
- return this.compileTypedArray(encoding.raw);
|
|
|
- },
|
|
|
- compileFDSelect: function CFFCompiler_compileFDSelect(fdSelect) {
|
|
|
- return this.compileTypedArray(fdSelect);
|
|
|
- },
|
|
|
- compileTypedArray: function CFFCompiler_compileTypedArray(data) {
|
|
|
- var out = [];
|
|
|
- for (var i = 0, ii = data.length; i < ii; ++i) {
|
|
|
- out[i] = data[i];
|
|
|
- }
|
|
|
- return out;
|
|
|
- },
|
|
|
- compileIndex: function CFFCompiler_compileIndex(index, trackers) {
|
|
|
- trackers = trackers || [];
|
|
|
- var objects = index.objects;
|
|
|
- // First 2 bytes contains the number of objects contained into this index
|
|
|
- var count = objects.length;
|
|
|
+ function buildAddOperation(num1, num2) {
|
|
|
+ if (num2.type === 'literal' && num2.number === 0) {
|
|
|
+ // optimization: second operand is 0
|
|
|
+ return num1;
|
|
|
+ }
|
|
|
+ if (num1.type === 'literal' && num1.number === 0) {
|
|
|
+ // optimization: first operand is 0
|
|
|
+ return num2;
|
|
|
+ }
|
|
|
+ if (num2.type === 'literal' && num1.type === 'literal') {
|
|
|
+ // optimization: operands operand are literals
|
|
|
+ return new AstLiteral(num1.number + num2.number);
|
|
|
+ }
|
|
|
+ return new AstBinaryOperation('+', num1, num2,
|
|
|
+ num1.min + num2.min, num1.max + num2.max);
|
|
|
+ }
|
|
|
|
|
|
- // If there is no object, just create an index. This technically
|
|
|
- // should just be [0, 0] but OTS has an issue with that.
|
|
|
- if (count === 0) {
|
|
|
- return [0, 0, 0];
|
|
|
+ function buildMulOperation(num1, num2) {
|
|
|
+ if (num2.type === 'literal') {
|
|
|
+ // optimization: second operands is a literal...
|
|
|
+ if (num2.number === 0) {
|
|
|
+ return new AstLiteral(0); // and it's 0
|
|
|
+ } else if (num2.number === 1) {
|
|
|
+ return num1; // and it's 1
|
|
|
+ } else if (num1.type === 'literal') {
|
|
|
+ // ... and first operands is a literal too
|
|
|
+ return new AstLiteral(num1.number * num2.number);
|
|
|
}
|
|
|
-
|
|
|
- var data = [(count >> 8) & 0xFF, count & 0xff];
|
|
|
-
|
|
|
- var lastOffset = 1, i;
|
|
|
- for (i = 0; i < count; ++i) {
|
|
|
- lastOffset += objects[i].length;
|
|
|
+ }
|
|
|
+ if (num1.type === 'literal') {
|
|
|
+ // optimization: first operands is a literal...
|
|
|
+ if (num1.number === 0) {
|
|
|
+ return new AstLiteral(0); // and it's 0
|
|
|
+ } else if (num1.number === 1) {
|
|
|
+ return num2; // and it's 1
|
|
|
}
|
|
|
+ }
|
|
|
+ var min = Math.min(num1.min * num2.min, num1.min * num2.max,
|
|
|
+ num1.max * num2.min, num1.max * num2.max);
|
|
|
+ var max = Math.max(num1.min * num2.min, num1.min * num2.max,
|
|
|
+ num1.max * num2.min, num1.max * num2.max);
|
|
|
+ return new AstBinaryOperation('*', num1, num2, min, max);
|
|
|
+ }
|
|
|
|
|
|
- var offsetSize;
|
|
|
- if (lastOffset < 0x100) {
|
|
|
- offsetSize = 1;
|
|
|
- } else if (lastOffset < 0x10000) {
|
|
|
- offsetSize = 2;
|
|
|
- } else if (lastOffset < 0x1000000) {
|
|
|
- offsetSize = 3;
|
|
|
- } else {
|
|
|
- offsetSize = 4;
|
|
|
+ function buildSubOperation(num1, num2) {
|
|
|
+ if (num2.type === 'literal') {
|
|
|
+ // optimization: second operands is a literal...
|
|
|
+ if (num2.number === 0) {
|
|
|
+ return num1; // ... and it's 0
|
|
|
+ } else if (num1.type === 'literal') {
|
|
|
+ // ... and first operands is a literal too
|
|
|
+ return new AstLiteral(num1.number - num2.number);
|
|
|
}
|
|
|
+ }
|
|
|
+ if (num2.type === 'binary' && num2.op === '-' &&
|
|
|
+ num1.type === 'literal' && num1.number === 1 &&
|
|
|
+ num2.arg1.type === 'literal' && num2.arg1.number === 1) {
|
|
|
+ // optimization for case: 1 - (1 - x)
|
|
|
+ return num2.arg2;
|
|
|
+ }
|
|
|
+ return new AstBinaryOperation('-', num1, num2,
|
|
|
+ num1.min - num2.max, num1.max - num2.min);
|
|
|
+ }
|
|
|
|
|
|
- // Next byte contains the offset size use to reference object in the file
|
|
|
- data.push(offsetSize);
|
|
|
+ function buildMinOperation(num1, max) {
|
|
|
+ if (num1.min >= max) {
|
|
|
+ // optimization: num1 min value is not less than required max
|
|
|
+ return new AstLiteral(max); // just returning max
|
|
|
+ } else if (num1.max <= max) {
|
|
|
+ // optimization: num1 max value is not greater than required max
|
|
|
+ return num1; // just returning an argument
|
|
|
+ }
|
|
|
+ return new AstMin(num1, max);
|
|
|
+ }
|
|
|
|
|
|
- // Add another offset after this one because we need a new offset
|
|
|
- var relativeOffset = 1;
|
|
|
- for (i = 0; i < count + 1; i++) {
|
|
|
- if (offsetSize === 1) {
|
|
|
- data.push(relativeOffset & 0xFF);
|
|
|
- } else if (offsetSize === 2) {
|
|
|
- data.push((relativeOffset >> 8) & 0xFF,
|
|
|
- relativeOffset & 0xFF);
|
|
|
- } else if (offsetSize === 3) {
|
|
|
- data.push((relativeOffset >> 16) & 0xFF,
|
|
|
- (relativeOffset >> 8) & 0xFF,
|
|
|
- relativeOffset & 0xFF);
|
|
|
- } else {
|
|
|
- data.push((relativeOffset >>> 24) & 0xFF,
|
|
|
- (relativeOffset >> 16) & 0xFF,
|
|
|
- (relativeOffset >> 8) & 0xFF,
|
|
|
- relativeOffset & 0xFF);
|
|
|
+ function PostScriptCompiler() {}
|
|
|
+ PostScriptCompiler.prototype = {
|
|
|
+ compile: function PostScriptCompiler_compile(code, domain, range) {
|
|
|
+ var stack = [];
|
|
|
+ var i, ii;
|
|
|
+ var instructions = [];
|
|
|
+ var inputSize = domain.length >> 1, outputSize = range.length >> 1;
|
|
|
+ var lastRegister = 0;
|
|
|
+ var n, j;
|
|
|
+ var num1, num2, ast1, ast2, tmpVar, item;
|
|
|
+ for (i = 0; i < inputSize; i++) {
|
|
|
+ stack.push(new AstArgument(i, domain[i * 2], domain[i * 2 + 1]));
|
|
|
+ }
|
|
|
+
|
|
|
+ for (i = 0, ii = code.length; i < ii; i++) {
|
|
|
+ item = code[i];
|
|
|
+ if (typeof item === 'number') {
|
|
|
+ stack.push(new AstLiteral(item));
|
|
|
+ continue;
|
|
|
}
|
|
|
|
|
|
- if (objects[i]) {
|
|
|
- relativeOffset += objects[i].length;
|
|
|
+ switch (item) {
|
|
|
+ case 'add':
|
|
|
+ if (stack.length < 2) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ num2 = stack.pop();
|
|
|
+ num1 = stack.pop();
|
|
|
+ stack.push(buildAddOperation(num1, num2));
|
|
|
+ break;
|
|
|
+ case 'cvr':
|
|
|
+ if (stack.length < 1) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 'mul':
|
|
|
+ if (stack.length < 2) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ num2 = stack.pop();
|
|
|
+ num1 = stack.pop();
|
|
|
+ stack.push(buildMulOperation(num1, num2));
|
|
|
+ break;
|
|
|
+ case 'sub':
|
|
|
+ if (stack.length < 2) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ num2 = stack.pop();
|
|
|
+ num1 = stack.pop();
|
|
|
+ stack.push(buildSubOperation(num1, num2));
|
|
|
+ break;
|
|
|
+ case 'exch':
|
|
|
+ if (stack.length < 2) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ ast1 = stack.pop(); ast2 = stack.pop();
|
|
|
+ stack.push(ast1, ast2);
|
|
|
+ break;
|
|
|
+ case 'pop':
|
|
|
+ if (stack.length < 1) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ stack.pop();
|
|
|
+ break;
|
|
|
+ case 'index':
|
|
|
+ if (stack.length < 1) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ num1 = stack.pop();
|
|
|
+ if (num1.type !== 'literal') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ n = num1.number;
|
|
|
+ if (n < 0 || (n|0) !== n || stack.length < n) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ ast1 = stack[stack.length - n - 1];
|
|
|
+ if (ast1.type === 'literal' || ast1.type === 'var') {
|
|
|
+ stack.push(ast1);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ tmpVar = new AstVariable(lastRegister++, ast1.min, ast1.max);
|
|
|
+ stack[stack.length - n - 1] = tmpVar;
|
|
|
+ stack.push(tmpVar);
|
|
|
+ instructions.push(new AstVariableDefinition(tmpVar, ast1));
|
|
|
+ break;
|
|
|
+ case 'dup':
|
|
|
+ if (stack.length < 1) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (typeof code[i + 1] === 'number' && code[i + 2] === 'gt' &&
|
|
|
+ code[i + 3] === i + 7 && code[i + 4] === 'jz' &&
|
|
|
+ code[i + 5] === 'pop' && code[i + 6] === code[i + 1]) {
|
|
|
+ // special case of the commands sequence for the min operation
|
|
|
+ num1 = stack.pop();
|
|
|
+ stack.push(buildMinOperation(num1, code[i + 1]));
|
|
|
+ i += 6;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ ast1 = stack[stack.length - 1];
|
|
|
+ if (ast1.type === 'literal' || ast1.type === 'var') {
|
|
|
+ // we don't have to save into intermediate variable a literal or
|
|
|
+ // variable.
|
|
|
+ stack.push(ast1);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ tmpVar = new AstVariable(lastRegister++, ast1.min, ast1.max);
|
|
|
+ stack[stack.length - 1] = tmpVar;
|
|
|
+ stack.push(tmpVar);
|
|
|
+ instructions.push(new AstVariableDefinition(tmpVar, ast1));
|
|
|
+ break;
|
|
|
+ case 'roll':
|
|
|
+ if (stack.length < 2) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ num2 = stack.pop();
|
|
|
+ num1 = stack.pop();
|
|
|
+ if (num2.type !== 'literal' || num1.type !== 'literal') {
|
|
|
+ // both roll operands must be numbers
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ j = num2.number;
|
|
|
+ n = num1.number;
|
|
|
+ if (n <= 0 || (n|0) !== n || (j|0) !== j || stack.length < n) {
|
|
|
+ // ... and integers
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ j = ((j % n) + n) % n;
|
|
|
+ if (j === 0) {
|
|
|
+ break; // just skipping -- there are nothing to rotate
|
|
|
+ }
|
|
|
+ Array.prototype.push.apply(stack,
|
|
|
+ stack.splice(stack.length - n, n - j));
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ return null; // unsupported operator
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- for (i = 0; i < count; i++) {
|
|
|
- // Notify the tracker where the object will be offset in the data.
|
|
|
- if (trackers[i]) {
|
|
|
- trackers[i].offset(data.length);
|
|
|
+ if (stack.length !== outputSize) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ var result = [];
|
|
|
+ instructions.forEach(function (instruction) {
|
|
|
+ var statementBuilder = new ExpressionBuilderVisitor();
|
|
|
+ instruction.visit(statementBuilder);
|
|
|
+ result.push(statementBuilder.toString());
|
|
|
+ });
|
|
|
+ stack.forEach(function (expr, i) {
|
|
|
+ var statementBuilder = new ExpressionBuilderVisitor();
|
|
|
+ expr.visit(statementBuilder);
|
|
|
+ var min = range[i * 2], max = range[i * 2 + 1];
|
|
|
+ var out = [statementBuilder.toString()];
|
|
|
+ if (min > expr.min) {
|
|
|
+ out.unshift('Math.max(', min, ', ');
|
|
|
+ out.push(')');
|
|
|
}
|
|
|
- for (var j = 0, jj = objects[i].length; j < jj; j++) {
|
|
|
- data.push(objects[i][j]);
|
|
|
+ if (max < expr.max) {
|
|
|
+ out.unshift('Math.min(', max, ', ');
|
|
|
+ out.push(')');
|
|
|
}
|
|
|
- }
|
|
|
- return data;
|
|
|
+ out.unshift('dest[destOffset + ', i, '] = ');
|
|
|
+ out.push(';');
|
|
|
+ result.push(out.join(''));
|
|
|
+ });
|
|
|
+ return result.join('\n');
|
|
|
}
|
|
|
};
|
|
|
- return CFFCompiler;
|
|
|
-})();
|
|
|
-
|
|
|
-function _enableSeacAnalysis(enabled) {
|
|
|
- exports.SEAC_ANALYSIS_ENABLED = SEAC_ANALYSIS_ENABLED = enabled;
|
|
|
-}
|
|
|
-
|
|
|
-// Workaround for seac on Windows.
|
|
|
-(function checkSeacSupport() {
|
|
|
- if (typeof navigator !== 'undefined' && /Windows/.test(navigator.userAgent)) {
|
|
|
- SEAC_ANALYSIS_ENABLED = true;
|
|
|
- }
|
|
|
-})();
|
|
|
|
|
|
-// Workaround for Private Use Area characters in Chrome on Windows
|
|
|
-// http://code.google.com/p/chromium/issues/detail?id=122465
|
|
|
-// https://github.com/mozilla/pdf.js/issues/1689
|
|
|
-(function checkChromeWindows() {
|
|
|
- if (typeof navigator !== 'undefined' &&
|
|
|
- /Windows.*Chrome/.test(navigator.userAgent)) {
|
|
|
- SKIP_PRIVATE_USE_RANGE_F000_TO_F01F = true;
|
|
|
- }
|
|
|
+ return PostScriptCompiler;
|
|
|
})();
|
|
|
|
|
|
-exports.SEAC_ANALYSIS_ENABLED = SEAC_ANALYSIS_ENABLED;
|
|
|
-exports.CFFCompiler = CFFCompiler;
|
|
|
-exports.CFFIndex = CFFIndex;
|
|
|
-exports.CFFParser = CFFParser;
|
|
|
-exports.CFFStrings = CFFStrings;
|
|
|
-exports.ErrorFont = ErrorFont;
|
|
|
-exports.FontFlags = FontFlags;
|
|
|
-exports.Font = Font;
|
|
|
-exports.IdentityToUnicodeMap = IdentityToUnicodeMap;
|
|
|
-exports.ToUnicodeMap = ToUnicodeMap;
|
|
|
-exports.Type1Parser = Type1Parser;
|
|
|
-exports.getFontType = getFontType;
|
|
|
-exports._enableSeacAnalysis = _enableSeacAnalysis;
|
|
|
-
|
|
|
-// TODO refactor to remove cyclic dependency on font_renderer.js
|
|
|
-coreFontRenderer._setCoreFonts(exports);
|
|
|
+exports.isPDFFunction = isPDFFunction;
|
|
|
+exports.PDFFunction = PDFFunction;
|
|
|
+exports.PostScriptEvaluator = PostScriptEvaluator;
|
|
|
+exports.PostScriptCompiler = PostScriptCompiler;
|
|
|
}));
|
|
|
|
|
|
|
|
|
(function (root, factory) {
|
|
|
{
|
|
|
- factory((root.pdfjsCoreFunction = {}), root.pdfjsSharedUtil,
|
|
|
- root.pdfjsCorePrimitives, root.pdfjsCorePsParser);
|
|
|
+ factory((root.pdfjsCoreColorSpace = {}), root.pdfjsSharedUtil,
|
|
|
+ root.pdfjsCorePrimitives, root.pdfjsCoreFunction, root.pdfjsCoreStream);
|
|
|
}
|
|
|
-}(this, function (exports, sharedUtil, corePrimitives, corePsParser) {
|
|
|
+}(this, function (exports, sharedUtil, corePrimitives, coreFunction,
|
|
|
+ coreStream) {
|
|
|
|
|
|
var error = sharedUtil.error;
|
|
|
var info = sharedUtil.info;
|
|
|
var isArray = sharedUtil.isArray;
|
|
|
-var isBool = sharedUtil.isBool;
|
|
|
+var isString = sharedUtil.isString;
|
|
|
+var shadow = sharedUtil.shadow;
|
|
|
+var warn = sharedUtil.warn;
|
|
|
var isDict = corePrimitives.isDict;
|
|
|
+var isName = corePrimitives.isName;
|
|
|
var isStream = corePrimitives.isStream;
|
|
|
-var PostScriptLexer = corePsParser.PostScriptLexer;
|
|
|
-var PostScriptParser = corePsParser.PostScriptParser;
|
|
|
-
|
|
|
-var PDFFunction = (function PDFFunctionClosure() {
|
|
|
- var CONSTRUCT_SAMPLED = 0;
|
|
|
- var CONSTRUCT_INTERPOLATED = 2;
|
|
|
- var CONSTRUCT_STICHED = 3;
|
|
|
- var CONSTRUCT_POSTSCRIPT = 4;
|
|
|
-
|
|
|
- return {
|
|
|
- getSampleArray: function PDFFunction_getSampleArray(size, outputSize, bps,
|
|
|
- str) {
|
|
|
- var i, ii;
|
|
|
- var length = 1;
|
|
|
- for (i = 0, ii = size.length; i < ii; i++) {
|
|
|
- length *= size[i];
|
|
|
- }
|
|
|
- length *= outputSize;
|
|
|
-
|
|
|
- var array = new Array(length);
|
|
|
- var codeSize = 0;
|
|
|
- var codeBuf = 0;
|
|
|
- // 32 is a valid bps so shifting won't work
|
|
|
- var sampleMul = 1.0 / (Math.pow(2.0, bps) - 1);
|
|
|
-
|
|
|
- var strBytes = str.getBytes((length * bps + 7) / 8);
|
|
|
- var strIdx = 0;
|
|
|
- for (i = 0; i < length; i++) {
|
|
|
- while (codeSize < bps) {
|
|
|
- codeBuf <<= 8;
|
|
|
- codeBuf |= strBytes[strIdx++];
|
|
|
- codeSize += 8;
|
|
|
- }
|
|
|
- codeSize -= bps;
|
|
|
- array[i] = (codeBuf >> codeSize) * sampleMul;
|
|
|
- codeBuf &= (1 << codeSize) - 1;
|
|
|
- }
|
|
|
- return array;
|
|
|
- },
|
|
|
-
|
|
|
- getIR: function PDFFunction_getIR(xref, fn) {
|
|
|
- var dict = fn.dict;
|
|
|
- if (!dict) {
|
|
|
- dict = fn;
|
|
|
- }
|
|
|
-
|
|
|
- var types = [this.constructSampled,
|
|
|
- null,
|
|
|
- this.constructInterpolated,
|
|
|
- this.constructStiched,
|
|
|
- this.constructPostScript];
|
|
|
-
|
|
|
- var typeNum = dict.get('FunctionType');
|
|
|
- var typeFn = types[typeNum];
|
|
|
- if (!typeFn) {
|
|
|
- error('Unknown type of function');
|
|
|
- }
|
|
|
-
|
|
|
- return typeFn.call(this, fn, dict, xref);
|
|
|
- },
|
|
|
-
|
|
|
- fromIR: function PDFFunction_fromIR(IR) {
|
|
|
- var type = IR[0];
|
|
|
- switch (type) {
|
|
|
- case CONSTRUCT_SAMPLED:
|
|
|
- return this.constructSampledFromIR(IR);
|
|
|
- case CONSTRUCT_INTERPOLATED:
|
|
|
- return this.constructInterpolatedFromIR(IR);
|
|
|
- case CONSTRUCT_STICHED:
|
|
|
- return this.constructStichedFromIR(IR);
|
|
|
- //case CONSTRUCT_POSTSCRIPT:
|
|
|
- default:
|
|
|
- return this.constructPostScriptFromIR(IR);
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- parse: function PDFFunction_parse(xref, fn) {
|
|
|
- var IR = this.getIR(xref, fn);
|
|
|
- return this.fromIR(IR);
|
|
|
- },
|
|
|
-
|
|
|
- parseArray: function PDFFunction_parseArray(xref, fnObj) {
|
|
|
- if (!isArray(fnObj)) {
|
|
|
- // not an array -- parsing as regular function
|
|
|
- return this.parse(xref, fnObj);
|
|
|
- }
|
|
|
-
|
|
|
- var fnArray = [];
|
|
|
- for (var j = 0, jj = fnObj.length; j < jj; j++) {
|
|
|
- var obj = xref.fetchIfRef(fnObj[j]);
|
|
|
- fnArray.push(PDFFunction.parse(xref, obj));
|
|
|
- }
|
|
|
- return function (src, srcOffset, dest, destOffset) {
|
|
|
- for (var i = 0, ii = fnArray.length; i < ii; i++) {
|
|
|
- fnArray[i](src, srcOffset, dest, destOffset + i);
|
|
|
- }
|
|
|
- };
|
|
|
- },
|
|
|
-
|
|
|
- constructSampled: function PDFFunction_constructSampled(str, dict) {
|
|
|
- function toMultiArray(arr) {
|
|
|
- var inputLength = arr.length;
|
|
|
- var out = [];
|
|
|
- var index = 0;
|
|
|
- for (var i = 0; i < inputLength; i += 2) {
|
|
|
- out[index] = [arr[i], arr[i + 1]];
|
|
|
- ++index;
|
|
|
- }
|
|
|
- return out;
|
|
|
- }
|
|
|
- var domain = dict.get('Domain');
|
|
|
- var range = dict.get('Range');
|
|
|
-
|
|
|
- if (!domain || !range) {
|
|
|
- error('No domain or range');
|
|
|
- }
|
|
|
-
|
|
|
- var inputSize = domain.length / 2;
|
|
|
- var outputSize = range.length / 2;
|
|
|
-
|
|
|
- domain = toMultiArray(domain);
|
|
|
- range = toMultiArray(range);
|
|
|
-
|
|
|
- var size = dict.get('Size');
|
|
|
- var bps = dict.get('BitsPerSample');
|
|
|
- var order = dict.get('Order') || 1;
|
|
|
- if (order !== 1) {
|
|
|
- // No description how cubic spline interpolation works in PDF32000:2008
|
|
|
- // As in poppler, ignoring order, linear interpolation may work as good
|
|
|
- info('No support for cubic spline interpolation: ' + order);
|
|
|
- }
|
|
|
-
|
|
|
- var encode = dict.get('Encode');
|
|
|
- if (!encode) {
|
|
|
- encode = [];
|
|
|
- for (var i = 0; i < inputSize; ++i) {
|
|
|
- encode.push(0);
|
|
|
- encode.push(size[i] - 1);
|
|
|
- }
|
|
|
- }
|
|
|
- encode = toMultiArray(encode);
|
|
|
-
|
|
|
- var decode = dict.get('Decode');
|
|
|
- if (!decode) {
|
|
|
- decode = range;
|
|
|
- } else {
|
|
|
- decode = toMultiArray(decode);
|
|
|
- }
|
|
|
-
|
|
|
- var samples = this.getSampleArray(size, outputSize, bps, str);
|
|
|
-
|
|
|
- return [
|
|
|
- CONSTRUCT_SAMPLED, inputSize, domain, encode, decode, samples, size,
|
|
|
- outputSize, Math.pow(2, bps) - 1, range
|
|
|
- ];
|
|
|
- },
|
|
|
-
|
|
|
- constructSampledFromIR: function PDFFunction_constructSampledFromIR(IR) {
|
|
|
- // See chapter 3, page 109 of the PDF reference
|
|
|
- function interpolate(x, xmin, xmax, ymin, ymax) {
|
|
|
- return ymin + ((x - xmin) * ((ymax - ymin) / (xmax - xmin)));
|
|
|
- }
|
|
|
-
|
|
|
- return function constructSampledFromIRResult(src, srcOffset,
|
|
|
- dest, destOffset) {
|
|
|
- // See chapter 3, page 110 of the PDF reference.
|
|
|
- var m = IR[1];
|
|
|
- var domain = IR[2];
|
|
|
- var encode = IR[3];
|
|
|
- var decode = IR[4];
|
|
|
- var samples = IR[5];
|
|
|
- var size = IR[6];
|
|
|
- var n = IR[7];
|
|
|
- //var mask = IR[8];
|
|
|
- var range = IR[9];
|
|
|
-
|
|
|
- // Building the cube vertices: its part and sample index
|
|
|
- // http://rjwagner49.com/Mathematics/Interpolation.pdf
|
|
|
- var cubeVertices = 1 << m;
|
|
|
- var cubeN = new Float64Array(cubeVertices);
|
|
|
- var cubeVertex = new Uint32Array(cubeVertices);
|
|
|
- var i, j;
|
|
|
- for (j = 0; j < cubeVertices; j++) {
|
|
|
- cubeN[j] = 1;
|
|
|
- }
|
|
|
-
|
|
|
- var k = n, pos = 1;
|
|
|
- // Map x_i to y_j for 0 <= i < m using the sampled function.
|
|
|
- for (i = 0; i < m; ++i) {
|
|
|
- // x_i' = min(max(x_i, Domain_2i), Domain_2i+1)
|
|
|
- var domain_2i = domain[i][0];
|
|
|
- var domain_2i_1 = domain[i][1];
|
|
|
- var xi = Math.min(Math.max(src[srcOffset +i], domain_2i),
|
|
|
- domain_2i_1);
|
|
|
+var PDFFunction = coreFunction.PDFFunction;
|
|
|
|
|
|
- // e_i = Interpolate(x_i', Domain_2i, Domain_2i+1,
|
|
|
- // Encode_2i, Encode_2i+1)
|
|
|
- var e = interpolate(xi, domain_2i, domain_2i_1,
|
|
|
- encode[i][0], encode[i][1]);
|
|
|
+var coreImage; // see _setCoreImage below
|
|
|
+var PDFImage; // = coreImage.PDFImage;
|
|
|
|
|
|
- // e_i' = min(max(e_i, 0), Size_i - 1)
|
|
|
- var size_i = size[i];
|
|
|
- e = Math.min(Math.max(e, 0), size_i - 1);
|
|
|
+var ColorSpace = (function ColorSpaceClosure() {
|
|
|
+ // Constructor should define this.numComps, this.defaultColor, this.name
|
|
|
+ function ColorSpace() {
|
|
|
+ error('should not call ColorSpace constructor');
|
|
|
+ }
|
|
|
|
|
|
- // Adjusting the cube: N and vertex sample index
|
|
|
- var e0 = e < size_i - 1 ? Math.floor(e) : e - 1; // e1 = e0 + 1;
|
|
|
- var n0 = e0 + 1 - e; // (e1 - e) / (e1 - e0);
|
|
|
- var n1 = e - e0; // (e - e0) / (e1 - e0);
|
|
|
- var offset0 = e0 * k;
|
|
|
- var offset1 = offset0 + k; // e1 * k
|
|
|
- for (j = 0; j < cubeVertices; j++) {
|
|
|
- if (j & pos) {
|
|
|
- cubeN[j] *= n1;
|
|
|
- cubeVertex[j] += offset1;
|
|
|
- } else {
|
|
|
- cubeN[j] *= n0;
|
|
|
- cubeVertex[j] += offset0;
|
|
|
- }
|
|
|
- }
|
|
|
+ ColorSpace.prototype = {
|
|
|
+ /**
|
|
|
+ * Converts the color value to the RGB color. The color components are
|
|
|
+ * located in the src array starting from the srcOffset. Returns the array
|
|
|
+ * of the rgb components, each value ranging from [0,255].
|
|
|
+ */
|
|
|
+ getRgb: function ColorSpace_getRgb(src, srcOffset) {
|
|
|
+ var rgb = new Uint8Array(3);
|
|
|
+ this.getRgbItem(src, srcOffset, rgb, 0);
|
|
|
+ return rgb;
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * Converts the color value to the RGB color, similar to the getRgb method.
|
|
|
+ * The result placed into the dest array starting from the destOffset.
|
|
|
+ */
|
|
|
+ getRgbItem: function ColorSpace_getRgbItem(src, srcOffset,
|
|
|
+ dest, destOffset) {
|
|
|
+ error('Should not call ColorSpace.getRgbItem');
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * Converts the specified number of the color values to the RGB colors.
|
|
|
+ * The colors are located in the src array starting from the srcOffset.
|
|
|
+ * The result is placed into the dest array starting from the destOffset.
|
|
|
+ * The src array items shall be in [0,2^bits) range, the dest array items
|
|
|
+ * will be in [0,255] range. alpha01 indicates how many alpha components
|
|
|
+ * there are in the dest array; it will be either 0 (RGB array) or 1 (RGBA
|
|
|
+ * array).
|
|
|
+ */
|
|
|
+ getRgbBuffer: function ColorSpace_getRgbBuffer(src, srcOffset, count,
|
|
|
+ dest, destOffset, bits,
|
|
|
+ alpha01) {
|
|
|
+ error('Should not call ColorSpace.getRgbBuffer');
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * Determines the number of bytes required to store the result of the
|
|
|
+ * conversion done by the getRgbBuffer method. As in getRgbBuffer,
|
|
|
+ * |alpha01| is either 0 (RGB output) or 1 (RGBA output).
|
|
|
+ */
|
|
|
+ getOutputLength: function ColorSpace_getOutputLength(inputLength,
|
|
|
+ alpha01) {
|
|
|
+ error('Should not call ColorSpace.getOutputLength');
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * Returns true if source data will be equal the result/output data.
|
|
|
+ */
|
|
|
+ isPassthrough: function ColorSpace_isPassthrough(bits) {
|
|
|
+ return false;
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * Fills in the RGB colors in the destination buffer. alpha01 indicates
|
|
|
+ * how many alpha components there are in the dest array; it will be either
|
|
|
+ * 0 (RGB array) or 1 (RGBA array).
|
|
|
+ */
|
|
|
+ fillRgb: function ColorSpace_fillRgb(dest, originalWidth,
|
|
|
+ originalHeight, width, height,
|
|
|
+ actualHeight, bpc, comps, alpha01) {
|
|
|
+ var count = originalWidth * originalHeight;
|
|
|
+ var rgbBuf = null;
|
|
|
+ var numComponentColors = 1 << bpc;
|
|
|
+ var needsResizing = originalHeight !== height || originalWidth !== width;
|
|
|
+ var i, ii;
|
|
|
|
|
|
- k *= size_i;
|
|
|
- pos <<= 1;
|
|
|
+ if (this.isPassthrough(bpc)) {
|
|
|
+ rgbBuf = comps;
|
|
|
+ } else if (this.numComps === 1 && count > numComponentColors &&
|
|
|
+ this.name !== 'DeviceGray' && this.name !== 'DeviceRGB') {
|
|
|
+ // Optimization: create a color map when there is just one component and
|
|
|
+ // we are converting more colors than the size of the color map. We
|
|
|
+ // don't build the map if the colorspace is gray or rgb since those
|
|
|
+ // methods are faster than building a map. This mainly offers big speed
|
|
|
+ // ups for indexed and alternate colorspaces.
|
|
|
+ //
|
|
|
+ // TODO it may be worth while to cache the color map. While running
|
|
|
+ // testing I never hit a cache so I will leave that out for now (perhaps
|
|
|
+ // we are reparsing colorspaces too much?).
|
|
|
+ var allColors = bpc <= 8 ? new Uint8Array(numComponentColors) :
|
|
|
+ new Uint16Array(numComponentColors);
|
|
|
+ var key;
|
|
|
+ for (i = 0; i < numComponentColors; i++) {
|
|
|
+ allColors[i] = i;
|
|
|
}
|
|
|
+ var colorMap = new Uint8Array(numComponentColors * 3);
|
|
|
+ this.getRgbBuffer(allColors, 0, numComponentColors, colorMap, 0, bpc,
|
|
|
+ /* alpha01 = */ 0);
|
|
|
|
|
|
- for (j = 0; j < n; ++j) {
|
|
|
- // Sum all cube vertices' samples portions
|
|
|
- var rj = 0;
|
|
|
- for (i = 0; i < cubeVertices; i++) {
|
|
|
- rj += samples[cubeVertex[i] + j] * cubeN[i];
|
|
|
+ var destPos, rgbPos;
|
|
|
+ if (!needsResizing) {
|
|
|
+ // Fill in the RGB values directly into |dest|.
|
|
|
+ destPos = 0;
|
|
|
+ for (i = 0; i < count; ++i) {
|
|
|
+ key = comps[i] * 3;
|
|
|
+ dest[destPos++] = colorMap[key];
|
|
|
+ dest[destPos++] = colorMap[key + 1];
|
|
|
+ dest[destPos++] = colorMap[key + 2];
|
|
|
+ destPos += alpha01;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ rgbBuf = new Uint8Array(count * 3);
|
|
|
+ rgbPos = 0;
|
|
|
+ for (i = 0; i < count; ++i) {
|
|
|
+ key = comps[i] * 3;
|
|
|
+ rgbBuf[rgbPos++] = colorMap[key];
|
|
|
+ rgbBuf[rgbPos++] = colorMap[key + 1];
|
|
|
+ rgbBuf[rgbPos++] = colorMap[key + 2];
|
|
|
}
|
|
|
-
|
|
|
- // r_j' = Interpolate(r_j, 0, 2^BitsPerSample - 1,
|
|
|
- // Decode_2j, Decode_2j+1)
|
|
|
- rj = interpolate(rj, 0, 1, decode[j][0], decode[j][1]);
|
|
|
-
|
|
|
- // y_j = min(max(r_j, range_2j), range_2j+1)
|
|
|
- dest[destOffset + j] = Math.min(Math.max(rj, range[j][0]),
|
|
|
- range[j][1]);
|
|
|
}
|
|
|
- };
|
|
|
- },
|
|
|
-
|
|
|
- constructInterpolated: function PDFFunction_constructInterpolated(str,
|
|
|
- dict) {
|
|
|
- var c0 = dict.get('C0') || [0];
|
|
|
- var c1 = dict.get('C1') || [1];
|
|
|
- var n = dict.get('N');
|
|
|
-
|
|
|
- if (!isArray(c0) || !isArray(c1)) {
|
|
|
- error('Illegal dictionary for interpolated function');
|
|
|
+ } else {
|
|
|
+ if (!needsResizing) {
|
|
|
+ // Fill in the RGB values directly into |dest|.
|
|
|
+ this.getRgbBuffer(comps, 0, width * actualHeight, dest, 0, bpc,
|
|
|
+ alpha01);
|
|
|
+ } else {
|
|
|
+ rgbBuf = new Uint8Array(count * 3);
|
|
|
+ this.getRgbBuffer(comps, 0, count, rgbBuf, 0, bpc,
|
|
|
+ /* alpha01 = */ 0);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- var length = c0.length;
|
|
|
- var diff = [];
|
|
|
- for (var i = 0; i < length; ++i) {
|
|
|
- diff.push(c1[i] - c0[i]);
|
|
|
+ if (rgbBuf) {
|
|
|
+ if (needsResizing) {
|
|
|
+ PDFImage.resize(rgbBuf, bpc, 3, originalWidth, originalHeight, width,
|
|
|
+ height, dest, alpha01);
|
|
|
+ } else {
|
|
|
+ rgbPos = 0;
|
|
|
+ destPos = 0;
|
|
|
+ for (i = 0, ii = width * actualHeight; i < ii; i++) {
|
|
|
+ dest[destPos++] = rgbBuf[rgbPos++];
|
|
|
+ dest[destPos++] = rgbBuf[rgbPos++];
|
|
|
+ dest[destPos++] = rgbBuf[rgbPos++];
|
|
|
+ destPos += alpha01;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
- return [CONSTRUCT_INTERPOLATED, c0, diff, n];
|
|
|
},
|
|
|
+ /**
|
|
|
+ * True if the colorspace has components in the default range of [0, 1].
|
|
|
+ * This should be true for all colorspaces except for lab color spaces
|
|
|
+ * which are [0,100], [-128, 127], [-128, 127].
|
|
|
+ */
|
|
|
+ usesZeroToOneRange: true
|
|
|
+ };
|
|
|
|
|
|
- constructInterpolatedFromIR:
|
|
|
- function PDFFunction_constructInterpolatedFromIR(IR) {
|
|
|
- var c0 = IR[1];
|
|
|
- var diff = IR[2];
|
|
|
- var n = IR[3];
|
|
|
-
|
|
|
- var length = diff.length;
|
|
|
+ ColorSpace.parse = function ColorSpace_parse(cs, xref, res) {
|
|
|
+ var IR = ColorSpace.parseToIR(cs, xref, res);
|
|
|
+ if (IR instanceof AlternateCS) {
|
|
|
+ return IR;
|
|
|
+ }
|
|
|
+ return ColorSpace.fromIR(IR);
|
|
|
+ };
|
|
|
|
|
|
- return function constructInterpolatedFromIRResult(src, srcOffset,
|
|
|
- dest, destOffset) {
|
|
|
- var x = n === 1 ? src[srcOffset] : Math.pow(src[srcOffset], n);
|
|
|
+ ColorSpace.fromIR = function ColorSpace_fromIR(IR) {
|
|
|
+ var name = isArray(IR) ? IR[0] : IR;
|
|
|
+ var whitePoint, blackPoint, gamma;
|
|
|
|
|
|
- for (var j = 0; j < length; ++j) {
|
|
|
- dest[destOffset + j] = c0[j] + (x * diff[j]);
|
|
|
+ switch (name) {
|
|
|
+ case 'DeviceGrayCS':
|
|
|
+ return this.singletons.gray;
|
|
|
+ case 'DeviceRgbCS':
|
|
|
+ return this.singletons.rgb;
|
|
|
+ case 'DeviceCmykCS':
|
|
|
+ return this.singletons.cmyk;
|
|
|
+ case 'CalGrayCS':
|
|
|
+ whitePoint = IR[1];
|
|
|
+ blackPoint = IR[2];
|
|
|
+ gamma = IR[3];
|
|
|
+ return new CalGrayCS(whitePoint, blackPoint, gamma);
|
|
|
+ case 'CalRGBCS':
|
|
|
+ whitePoint = IR[1];
|
|
|
+ blackPoint = IR[2];
|
|
|
+ gamma = IR[3];
|
|
|
+ var matrix = IR[4];
|
|
|
+ return new CalRGBCS(whitePoint, blackPoint, gamma, matrix);
|
|
|
+ case 'PatternCS':
|
|
|
+ var basePatternCS = IR[1];
|
|
|
+ if (basePatternCS) {
|
|
|
+ basePatternCS = ColorSpace.fromIR(basePatternCS);
|
|
|
}
|
|
|
- };
|
|
|
- },
|
|
|
+ return new PatternCS(basePatternCS);
|
|
|
+ case 'IndexedCS':
|
|
|
+ var baseIndexedCS = IR[1];
|
|
|
+ var hiVal = IR[2];
|
|
|
+ var lookup = IR[3];
|
|
|
+ return new IndexedCS(ColorSpace.fromIR(baseIndexedCS), hiVal, lookup);
|
|
|
+ case 'AlternateCS':
|
|
|
+ var numComps = IR[1];
|
|
|
+ var alt = IR[2];
|
|
|
+ var tintFnIR = IR[3];
|
|
|
|
|
|
- constructStiched: function PDFFunction_constructStiched(fn, dict, xref) {
|
|
|
- var domain = dict.get('Domain');
|
|
|
+ return new AlternateCS(numComps, ColorSpace.fromIR(alt),
|
|
|
+ PDFFunction.fromIR(tintFnIR));
|
|
|
+ case 'LabCS':
|
|
|
+ whitePoint = IR[1];
|
|
|
+ blackPoint = IR[2];
|
|
|
+ var range = IR[3];
|
|
|
+ return new LabCS(whitePoint, blackPoint, range);
|
|
|
+ default:
|
|
|
+ error('Unknown name ' + name);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ };
|
|
|
|
|
|
- if (!domain) {
|
|
|
- error('No domain');
|
|
|
+ ColorSpace.parseToIR = function ColorSpace_parseToIR(cs, xref, res) {
|
|
|
+ if (isName(cs)) {
|
|
|
+ var colorSpaces = res.get('ColorSpace');
|
|
|
+ if (isDict(colorSpaces)) {
|
|
|
+ var refcs = colorSpaces.get(cs.name);
|
|
|
+ if (refcs) {
|
|
|
+ cs = refcs;
|
|
|
+ }
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- var inputSize = domain.length / 2;
|
|
|
- if (inputSize !== 1) {
|
|
|
- error('Bad domain for stiched function');
|
|
|
+ cs = xref.fetchIfRef(cs);
|
|
|
+ var mode;
|
|
|
+
|
|
|
+ if (isName(cs)) {
|
|
|
+ mode = cs.name;
|
|
|
+ this.mode = mode;
|
|
|
+
|
|
|
+ switch (mode) {
|
|
|
+ case 'DeviceGray':
|
|
|
+ case 'G':
|
|
|
+ return 'DeviceGrayCS';
|
|
|
+ case 'DeviceRGB':
|
|
|
+ case 'RGB':
|
|
|
+ return 'DeviceRgbCS';
|
|
|
+ case 'DeviceCMYK':
|
|
|
+ case 'CMYK':
|
|
|
+ return 'DeviceCmykCS';
|
|
|
+ case 'Pattern':
|
|
|
+ return ['PatternCS', null];
|
|
|
+ default:
|
|
|
+ error('unrecognized colorspace ' + mode);
|
|
|
}
|
|
|
+ } else if (isArray(cs)) {
|
|
|
+ mode = xref.fetchIfRef(cs[0]).name;
|
|
|
+ this.mode = mode;
|
|
|
+ var numComps, params, alt, whitePoint, blackPoint, gamma;
|
|
|
|
|
|
- var fnRefs = dict.get('Functions');
|
|
|
- var fns = [];
|
|
|
- for (var i = 0, ii = fnRefs.length; i < ii; ++i) {
|
|
|
- fns.push(PDFFunction.getIR(xref, xref.fetchIfRef(fnRefs[i])));
|
|
|
+ switch (mode) {
|
|
|
+ case 'DeviceGray':
|
|
|
+ case 'G':
|
|
|
+ return 'DeviceGrayCS';
|
|
|
+ case 'DeviceRGB':
|
|
|
+ case 'RGB':
|
|
|
+ return 'DeviceRgbCS';
|
|
|
+ case 'DeviceCMYK':
|
|
|
+ case 'CMYK':
|
|
|
+ return 'DeviceCmykCS';
|
|
|
+ case 'CalGray':
|
|
|
+ params = xref.fetchIfRef(cs[1]);
|
|
|
+ whitePoint = params.get('WhitePoint');
|
|
|
+ blackPoint = params.get('BlackPoint');
|
|
|
+ gamma = params.get('Gamma');
|
|
|
+ return ['CalGrayCS', whitePoint, blackPoint, gamma];
|
|
|
+ case 'CalRGB':
|
|
|
+ params = xref.fetchIfRef(cs[1]);
|
|
|
+ whitePoint = params.get('WhitePoint');
|
|
|
+ blackPoint = params.get('BlackPoint');
|
|
|
+ gamma = params.get('Gamma');
|
|
|
+ var matrix = params.get('Matrix');
|
|
|
+ return ['CalRGBCS', whitePoint, blackPoint, gamma, matrix];
|
|
|
+ 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] || 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 = 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 = xref.fetchIfRef(cs[1]);
|
|
|
+ numComps = 1;
|
|
|
+ if (isName(name)) {
|
|
|
+ numComps = 1;
|
|
|
+ } else if (isArray(name)) {
|
|
|
+ numComps = name.length;
|
|
|
+ }
|
|
|
+ alt = ColorSpace.parseToIR(cs[2], xref, res);
|
|
|
+ var tintFnIR = PDFFunction.getIR(xref, xref.fetchIfRef(cs[3]));
|
|
|
+ return ['AlternateCS', numComps, alt, tintFnIR];
|
|
|
+ case 'Lab':
|
|
|
+ params = xref.fetchIfRef(cs[1]);
|
|
|
+ whitePoint = params.get('WhitePoint');
|
|
|
+ blackPoint = params.get('BlackPoint');
|
|
|
+ var range = params.get('Range');
|
|
|
+ return ['LabCS', whitePoint, blackPoint, range];
|
|
|
+ default:
|
|
|
+ error('unimplemented color space object "' + mode + '"');
|
|
|
}
|
|
|
+ } else {
|
|
|
+ error('unrecognized color space object: "' + cs + '"');
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ };
|
|
|
+ /**
|
|
|
+ * Checks if a decode map matches the default decode map for a color space.
|
|
|
+ * This handles the general decode maps where there are two values per
|
|
|
+ * component. e.g. [0, 1, 0, 1, 0, 1] for a RGB color.
|
|
|
+ * This does not handle Lab, Indexed, or Pattern decode maps since they are
|
|
|
+ * 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 (!isArray(decode)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
|
|
|
- var bounds = dict.get('Bounds');
|
|
|
- var encode = dict.get('Encode');
|
|
|
+ if (n * 2 !== decode.length) {
|
|
|
+ warn('The decode map is not the correct length');
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ for (var i = 0, ii = decode.length; i < ii; i += 2) {
|
|
|
+ if (decode[i] !== 0 || decode[i + 1] !== 1) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ };
|
|
|
|
|
|
- return [CONSTRUCT_STICHED, domain, bounds, encode, fns];
|
|
|
+ ColorSpace.singletons = {
|
|
|
+ get gray() {
|
|
|
+ return shadow(this, 'gray', new DeviceGrayCS());
|
|
|
+ },
|
|
|
+ get rgb() {
|
|
|
+ return shadow(this, 'rgb', new DeviceRgbCS());
|
|
|
},
|
|
|
+ get cmyk() {
|
|
|
+ return shadow(this, 'cmyk', new DeviceCmykCS());
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- constructStichedFromIR: function PDFFunction_constructStichedFromIR(IR) {
|
|
|
- var domain = IR[1];
|
|
|
- var bounds = IR[2];
|
|
|
- var encode = IR[3];
|
|
|
- var fnsIR = IR[4];
|
|
|
- var fns = [];
|
|
|
- var tmpBuf = new Float32Array(1);
|
|
|
+ return ColorSpace;
|
|
|
+})();
|
|
|
|
|
|
- for (var i = 0, ii = fnsIR.length; i < ii; i++) {
|
|
|
- fns.push(PDFFunction.fromIR(fnsIR[i]));
|
|
|
- }
|
|
|
+/**
|
|
|
+ * Alternate color space handles both Separation and DeviceN color spaces. A
|
|
|
+ * Separation color space is actually just a DeviceN with one color component.
|
|
|
+ * Both color spaces use a tinting function to convert colors to a base color
|
|
|
+ * space.
|
|
|
+ */
|
|
|
+var AlternateCS = (function AlternateCSClosure() {
|
|
|
+ function AlternateCS(numComps, base, tintFn) {
|
|
|
+ this.name = 'Alternate';
|
|
|
+ this.numComps = numComps;
|
|
|
+ this.defaultColor = new Float32Array(numComps);
|
|
|
+ for (var i = 0; i < numComps; ++i) {
|
|
|
+ this.defaultColor[i] = 1;
|
|
|
+ }
|
|
|
+ this.base = base;
|
|
|
+ this.tintFn = tintFn;
|
|
|
+ this.tmpBuf = new Float32Array(base.numComps);
|
|
|
+ }
|
|
|
|
|
|
- return function constructStichedFromIRResult(src, srcOffset,
|
|
|
- dest, destOffset) {
|
|
|
- var clip = function constructStichedFromIRClip(v, min, max) {
|
|
|
- if (v > max) {
|
|
|
- v = max;
|
|
|
- } else if (v < min) {
|
|
|
- v = min;
|
|
|
- }
|
|
|
- return v;
|
|
|
- };
|
|
|
+ AlternateCS.prototype = {
|
|
|
+ getRgb: ColorSpace.prototype.getRgb,
|
|
|
+ getRgbItem: function AlternateCS_getRgbItem(src, srcOffset,
|
|
|
+ dest, destOffset) {
|
|
|
+ var tmpBuf = this.tmpBuf;
|
|
|
+ this.tintFn(src, srcOffset, tmpBuf, 0);
|
|
|
+ this.base.getRgbItem(tmpBuf, 0, dest, destOffset);
|
|
|
+ },
|
|
|
+ getRgbBuffer: function AlternateCS_getRgbBuffer(src, srcOffset, count,
|
|
|
+ dest, destOffset, bits,
|
|
|
+ alpha01) {
|
|
|
+ var tintFn = this.tintFn;
|
|
|
+ var base = this.base;
|
|
|
+ var scale = 1 / ((1 << bits) - 1);
|
|
|
+ var baseNumComps = base.numComps;
|
|
|
+ var usesZeroToOneRange = base.usesZeroToOneRange;
|
|
|
+ var isPassthrough = (base.isPassthrough(8) || !usesZeroToOneRange) &&
|
|
|
+ alpha01 === 0;
|
|
|
+ var pos = isPassthrough ? destOffset : 0;
|
|
|
+ var baseBuf = isPassthrough ? dest : new Uint8Array(baseNumComps * count);
|
|
|
+ var numComps = this.numComps;
|
|
|
|
|
|
- // clip to domain
|
|
|
- var v = clip(src[srcOffset], domain[0], domain[1]);
|
|
|
- // calulate which bound the value is in
|
|
|
- for (var i = 0, ii = bounds.length; i < ii; ++i) {
|
|
|
- if (v < bounds[i]) {
|
|
|
- break;
|
|
|
+ var scaled = new Float32Array(numComps);
|
|
|
+ var tinted = new Float32Array(baseNumComps);
|
|
|
+ var i, j;
|
|
|
+ if (usesZeroToOneRange) {
|
|
|
+ for (i = 0; i < count; i++) {
|
|
|
+ for (j = 0; j < numComps; j++) {
|
|
|
+ scaled[j] = src[srcOffset++] * scale;
|
|
|
+ }
|
|
|
+ tintFn(scaled, 0, tinted, 0);
|
|
|
+ for (j = 0; j < baseNumComps; j++) {
|
|
|
+ baseBuf[pos++] = tinted[j] * 255;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- // encode value into domain of function
|
|
|
- var dmin = domain[0];
|
|
|
- if (i > 0) {
|
|
|
- dmin = bounds[i - 1];
|
|
|
- }
|
|
|
- var dmax = domain[1];
|
|
|
- if (i < bounds.length) {
|
|
|
- dmax = bounds[i];
|
|
|
+ } else {
|
|
|
+ for (i = 0; i < count; i++) {
|
|
|
+ for (j = 0; j < numComps; j++) {
|
|
|
+ scaled[j] = src[srcOffset++] * scale;
|
|
|
+ }
|
|
|
+ tintFn(scaled, 0, tinted, 0);
|
|
|
+ base.getRgbItem(tinted, 0, baseBuf, pos);
|
|
|
+ pos += baseNumComps;
|
|
|
}
|
|
|
+ }
|
|
|
+ if (!isPassthrough) {
|
|
|
+ base.getRgbBuffer(baseBuf, 0, count, dest, destOffset, 8, alpha01);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ getOutputLength: function AlternateCS_getOutputLength(inputLength,
|
|
|
+ alpha01) {
|
|
|
+ return this.base.getOutputLength(inputLength *
|
|
|
+ this.base.numComps / this.numComps,
|
|
|
+ alpha01);
|
|
|
+ },
|
|
|
+ isPassthrough: ColorSpace.prototype.isPassthrough,
|
|
|
+ fillRgb: ColorSpace.prototype.fillRgb,
|
|
|
+ isDefaultDecode: function AlternateCS_isDefaultDecode(decodeMap) {
|
|
|
+ return ColorSpace.isDefaultDecode(decodeMap, this.numComps);
|
|
|
+ },
|
|
|
+ usesZeroToOneRange: true
|
|
|
+ };
|
|
|
|
|
|
- var rmin = encode[2 * i];
|
|
|
- var rmax = encode[2 * i + 1];
|
|
|
+ return AlternateCS;
|
|
|
+})();
|
|
|
|
|
|
- // 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);
|
|
|
+var PatternCS = (function PatternCSClosure() {
|
|
|
+ function PatternCS(baseCS) {
|
|
|
+ this.name = 'Pattern';
|
|
|
+ this.base = baseCS;
|
|
|
+ }
|
|
|
+ PatternCS.prototype = {};
|
|
|
|
|
|
- // call the appropriate function
|
|
|
- fns[i](tmpBuf, 0, dest, destOffset);
|
|
|
- };
|
|
|
- },
|
|
|
+ return PatternCS;
|
|
|
+})();
|
|
|
|
|
|
- constructPostScript: function PDFFunction_constructPostScript(fn, dict,
|
|
|
- xref) {
|
|
|
- var domain = dict.get('Domain');
|
|
|
- var range = dict.get('Range');
|
|
|
+var IndexedCS = (function IndexedCSClosure() {
|
|
|
+ function IndexedCS(base, highVal, lookup) {
|
|
|
+ this.name = 'Indexed';
|
|
|
+ this.numComps = 1;
|
|
|
+ this.defaultColor = new Uint8Array([0]);
|
|
|
+ this.base = base;
|
|
|
+ this.highVal = highVal;
|
|
|
|
|
|
- if (!domain) {
|
|
|
- error('No domain.');
|
|
|
- }
|
|
|
+ var baseNumComps = base.numComps;
|
|
|
+ var length = baseNumComps * highVal;
|
|
|
+ var lookupArray;
|
|
|
|
|
|
- if (!range) {
|
|
|
- error('No range.');
|
|
|
+ if (isStream(lookup)) {
|
|
|
+ lookupArray = new Uint8Array(length);
|
|
|
+ var bytes = lookup.getBytes(length);
|
|
|
+ lookupArray.set(bytes);
|
|
|
+ } else if (isString(lookup)) {
|
|
|
+ lookupArray = new Uint8Array(length);
|
|
|
+ for (var i = 0; i < length; ++i) {
|
|
|
+ lookupArray[i] = lookup.charCodeAt(i);
|
|
|
}
|
|
|
+ } else if (lookup instanceof Uint8Array || lookup instanceof Array) {
|
|
|
+ lookupArray = lookup;
|
|
|
+ } else {
|
|
|
+ error('Unrecognized lookup table: ' + lookup);
|
|
|
+ }
|
|
|
+ this.lookup = lookupArray;
|
|
|
+ }
|
|
|
|
|
|
- var lexer = new PostScriptLexer(fn);
|
|
|
- var parser = new PostScriptParser(lexer);
|
|
|
- var code = parser.parse();
|
|
|
-
|
|
|
- return [CONSTRUCT_POSTSCRIPT, domain, range, code];
|
|
|
+ IndexedCS.prototype = {
|
|
|
+ getRgb: ColorSpace.prototype.getRgb,
|
|
|
+ getRgbItem: function IndexedCS_getRgbItem(src, srcOffset,
|
|
|
+ dest, destOffset) {
|
|
|
+ var numComps = this.base.numComps;
|
|
|
+ var start = src[srcOffset] * numComps;
|
|
|
+ this.base.getRgbItem(this.lookup, start, dest, destOffset);
|
|
|
},
|
|
|
+ getRgbBuffer: function IndexedCS_getRgbBuffer(src, srcOffset, count,
|
|
|
+ dest, destOffset, bits,
|
|
|
+ alpha01) {
|
|
|
+ var base = this.base;
|
|
|
+ var numComps = base.numComps;
|
|
|
+ var outputDelta = base.getOutputLength(numComps, alpha01);
|
|
|
+ var lookup = this.lookup;
|
|
|
|
|
|
- constructPostScriptFromIR: function PDFFunction_constructPostScriptFromIR(
|
|
|
- IR) {
|
|
|
- var domain = IR[1];
|
|
|
- var range = IR[2];
|
|
|
- var code = IR[3];
|
|
|
-
|
|
|
- var compiled = (new PostScriptCompiler()).compile(code, domain, range);
|
|
|
- if (compiled) {
|
|
|
- // Compiled function consists of simple expressions such as addition,
|
|
|
- // subtraction, Math.max, and also contains 'var' and 'return'
|
|
|
- // statements. See the generation in the PostScriptCompiler below.
|
|
|
- /*jshint -W054 */
|
|
|
- return new Function('src', 'srcOffset', 'dest', 'destOffset', compiled);
|
|
|
+ for (var i = 0; i < count; ++i) {
|
|
|
+ var lookupPos = src[srcOffset++] * numComps;
|
|
|
+ base.getRgbBuffer(lookup, lookupPos, 1, dest, destOffset, 8, alpha01);
|
|
|
+ destOffset += outputDelta;
|
|
|
}
|
|
|
-
|
|
|
- info('Unable to compile PS function');
|
|
|
-
|
|
|
- var numOutputs = range.length >> 1;
|
|
|
- var numInputs = domain.length >> 1;
|
|
|
- var evaluator = new PostScriptEvaluator(code);
|
|
|
- // Cache the values for a big speed up, the cache size is limited though
|
|
|
- // since the number of possible values can be huge from a PS function.
|
|
|
- var cache = Object.create(null);
|
|
|
- // The MAX_CACHE_SIZE is set to ~4x the maximum number of distinct values
|
|
|
- // seen in our tests.
|
|
|
- var MAX_CACHE_SIZE = 2048 * 4;
|
|
|
- var cache_available = MAX_CACHE_SIZE;
|
|
|
- var tmpBuf = new Float32Array(numInputs);
|
|
|
-
|
|
|
- return function constructPostScriptFromIRResult(src, srcOffset,
|
|
|
- dest, destOffset) {
|
|
|
- var i, value;
|
|
|
- var key = '';
|
|
|
- var input = tmpBuf;
|
|
|
- for (i = 0; i < numInputs; i++) {
|
|
|
- value = src[srcOffset + i];
|
|
|
- input[i] = value;
|
|
|
- key += value + '_';
|
|
|
- }
|
|
|
-
|
|
|
- var cachedValue = cache[key];
|
|
|
- if (cachedValue !== undefined) {
|
|
|
- dest.set(cachedValue, destOffset);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- var output = new Float32Array(numOutputs);
|
|
|
- var stack = evaluator.execute(input);
|
|
|
- var stackIndex = stack.length - numOutputs;
|
|
|
- for (i = 0; i < numOutputs; i++) {
|
|
|
- value = stack[stackIndex + i];
|
|
|
- var bound = range[i * 2];
|
|
|
- if (value < bound) {
|
|
|
- value = bound;
|
|
|
- } else {
|
|
|
- bound = range[i * 2 +1];
|
|
|
- if (value > bound) {
|
|
|
- value = bound;
|
|
|
- }
|
|
|
- }
|
|
|
- output[i] = value;
|
|
|
- }
|
|
|
- if (cache_available > 0) {
|
|
|
- cache_available--;
|
|
|
- cache[key] = output;
|
|
|
- }
|
|
|
- dest.set(output, destOffset);
|
|
|
- };
|
|
|
- }
|
|
|
+ },
|
|
|
+ getOutputLength: function IndexedCS_getOutputLength(inputLength, alpha01) {
|
|
|
+ return this.base.getOutputLength(inputLength * this.base.numComps,
|
|
|
+ alpha01);
|
|
|
+ },
|
|
|
+ isPassthrough: ColorSpace.prototype.isPassthrough,
|
|
|
+ fillRgb: ColorSpace.prototype.fillRgb,
|
|
|
+ isDefaultDecode: function IndexedCS_isDefaultDecode(decodeMap) {
|
|
|
+ // indexed color maps shouldn't be changed
|
|
|
+ return true;
|
|
|
+ },
|
|
|
+ usesZeroToOneRange: true
|
|
|
};
|
|
|
+ return IndexedCS;
|
|
|
})();
|
|
|
|
|
|
-function isPDFFunction(v) {
|
|
|
- var fnDict;
|
|
|
- if (typeof v !== 'object') {
|
|
|
- return false;
|
|
|
- } else if (isDict(v)) {
|
|
|
- fnDict = v;
|
|
|
- } else if (isStream(v)) {
|
|
|
- fnDict = v.dict;
|
|
|
- } else {
|
|
|
- return false;
|
|
|
- }
|
|
|
- return fnDict.has('FunctionType');
|
|
|
-}
|
|
|
-
|
|
|
-var PostScriptStack = (function PostScriptStackClosure() {
|
|
|
- var MAX_STACK_SIZE = 100;
|
|
|
- function PostScriptStack(initialStack) {
|
|
|
- this.stack = !initialStack ? [] :
|
|
|
- Array.prototype.slice.call(initialStack, 0);
|
|
|
+var DeviceGrayCS = (function DeviceGrayCSClosure() {
|
|
|
+ function DeviceGrayCS() {
|
|
|
+ this.name = 'DeviceGray';
|
|
|
+ this.numComps = 1;
|
|
|
+ this.defaultColor = new Float32Array([0]);
|
|
|
}
|
|
|
|
|
|
- PostScriptStack.prototype = {
|
|
|
- push: function PostScriptStack_push(value) {
|
|
|
- if (this.stack.length >= MAX_STACK_SIZE) {
|
|
|
- error('PostScript function stack overflow.');
|
|
|
- }
|
|
|
- this.stack.push(value);
|
|
|
+ DeviceGrayCS.prototype = {
|
|
|
+ getRgb: ColorSpace.prototype.getRgb,
|
|
|
+ getRgbItem: function DeviceGrayCS_getRgbItem(src, srcOffset,
|
|
|
+ dest, destOffset) {
|
|
|
+ var c = (src[srcOffset] * 255) | 0;
|
|
|
+ c = c < 0 ? 0 : c > 255 ? 255 : c;
|
|
|
+ dest[destOffset] = dest[destOffset + 1] = dest[destOffset + 2] = c;
|
|
|
},
|
|
|
- pop: function PostScriptStack_pop() {
|
|
|
- if (this.stack.length <= 0) {
|
|
|
- error('PostScript function stack underflow.');
|
|
|
+ getRgbBuffer: function DeviceGrayCS_getRgbBuffer(src, srcOffset, count,
|
|
|
+ dest, destOffset, bits,
|
|
|
+ alpha01) {
|
|
|
+ var scale = 255 / ((1 << bits) - 1);
|
|
|
+ var j = srcOffset, q = destOffset;
|
|
|
+ for (var i = 0; i < count; ++i) {
|
|
|
+ var c = (scale * src[j++]) | 0;
|
|
|
+ dest[q++] = c;
|
|
|
+ dest[q++] = c;
|
|
|
+ dest[q++] = c;
|
|
|
+ q += alpha01;
|
|
|
}
|
|
|
- return this.stack.pop();
|
|
|
},
|
|
|
- copy: function PostScriptStack_copy(n) {
|
|
|
- if (this.stack.length + n >= MAX_STACK_SIZE) {
|
|
|
- error('PostScript function stack overflow.');
|
|
|
- }
|
|
|
- var stack = this.stack;
|
|
|
- for (var i = stack.length - n, j = n - 1; j >= 0; j--, i++) {
|
|
|
- stack.push(stack[i]);
|
|
|
- }
|
|
|
+ getOutputLength: function DeviceGrayCS_getOutputLength(inputLength,
|
|
|
+ alpha01) {
|
|
|
+ return inputLength * (3 + alpha01);
|
|
|
},
|
|
|
- index: function PostScriptStack_index(n) {
|
|
|
- this.push(this.stack[this.stack.length - n - 1]);
|
|
|
+ isPassthrough: ColorSpace.prototype.isPassthrough,
|
|
|
+ fillRgb: ColorSpace.prototype.fillRgb,
|
|
|
+ isDefaultDecode: function DeviceGrayCS_isDefaultDecode(decodeMap) {
|
|
|
+ return ColorSpace.isDefaultDecode(decodeMap, this.numComps);
|
|
|
},
|
|
|
- // rotate the last n stack elements p times
|
|
|
- roll: function PostScriptStack_roll(n, p) {
|
|
|
- var stack = this.stack;
|
|
|
- var l = stack.length - n;
|
|
|
- var r = stack.length - 1, c = l + (p - Math.floor(p / n) * n), i, j, t;
|
|
|
- for (i = l, j = r; i < j; i++, j--) {
|
|
|
- t = stack[i]; stack[i] = stack[j]; stack[j] = t;
|
|
|
- }
|
|
|
- for (i = l, j = c - 1; i < j; i++, j--) {
|
|
|
- t = stack[i]; stack[i] = stack[j]; stack[j] = t;
|
|
|
- }
|
|
|
- for (i = c, j = r; i < j; i++, j--) {
|
|
|
- t = stack[i]; stack[i] = stack[j]; stack[j] = t;
|
|
|
- }
|
|
|
- }
|
|
|
+ usesZeroToOneRange: true
|
|
|
};
|
|
|
- return PostScriptStack;
|
|
|
+ return DeviceGrayCS;
|
|
|
})();
|
|
|
-var PostScriptEvaluator = (function PostScriptEvaluatorClosure() {
|
|
|
- function PostScriptEvaluator(operators) {
|
|
|
- this.operators = operators;
|
|
|
- }
|
|
|
- PostScriptEvaluator.prototype = {
|
|
|
- execute: function PostScriptEvaluator_execute(initialStack) {
|
|
|
- var stack = new PostScriptStack(initialStack);
|
|
|
- var counter = 0;
|
|
|
- var operators = this.operators;
|
|
|
- var length = operators.length;
|
|
|
- var operator, a, b;
|
|
|
- while (counter < length) {
|
|
|
- operator = operators[counter++];
|
|
|
- if (typeof operator === 'number') {
|
|
|
- // Operator is really an operand and should be pushed to the stack.
|
|
|
- stack.push(operator);
|
|
|
- continue;
|
|
|
- }
|
|
|
- switch (operator) {
|
|
|
- // non standard ps operators
|
|
|
- case 'jz': // jump if false
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- if (!a) {
|
|
|
- counter = b;
|
|
|
- }
|
|
|
- break;
|
|
|
- case 'j': // jump
|
|
|
- a = stack.pop();
|
|
|
- counter = a;
|
|
|
- break;
|
|
|
|
|
|
- // all ps operators in alphabetical order (excluding if/ifelse)
|
|
|
- case 'abs':
|
|
|
- a = stack.pop();
|
|
|
- stack.push(Math.abs(a));
|
|
|
- break;
|
|
|
- case 'add':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- stack.push(a + b);
|
|
|
- break;
|
|
|
- case 'and':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- if (isBool(a) && isBool(b)) {
|
|
|
- stack.push(a && b);
|
|
|
- } else {
|
|
|
- stack.push(a & b);
|
|
|
- }
|
|
|
- break;
|
|
|
- case 'atan':
|
|
|
- a = stack.pop();
|
|
|
- stack.push(Math.atan(a));
|
|
|
- break;
|
|
|
- case 'bitshift':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- if (a > 0) {
|
|
|
- stack.push(a << b);
|
|
|
- } else {
|
|
|
- stack.push(a >> b);
|
|
|
- }
|
|
|
- break;
|
|
|
- case 'ceiling':
|
|
|
- a = stack.pop();
|
|
|
- stack.push(Math.ceil(a));
|
|
|
- break;
|
|
|
- case 'copy':
|
|
|
- a = stack.pop();
|
|
|
- stack.copy(a);
|
|
|
- break;
|
|
|
- case 'cos':
|
|
|
- a = stack.pop();
|
|
|
- stack.push(Math.cos(a));
|
|
|
- break;
|
|
|
- case 'cvi':
|
|
|
- a = stack.pop() | 0;
|
|
|
- stack.push(a);
|
|
|
- break;
|
|
|
- case 'cvr':
|
|
|
- // noop
|
|
|
- break;
|
|
|
- case 'div':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- stack.push(a / b);
|
|
|
- break;
|
|
|
- case 'dup':
|
|
|
- stack.copy(1);
|
|
|
- break;
|
|
|
- case 'eq':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- stack.push(a === b);
|
|
|
- break;
|
|
|
- case 'exch':
|
|
|
- stack.roll(2, 1);
|
|
|
- break;
|
|
|
- case 'exp':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- stack.push(Math.pow(a, b));
|
|
|
- break;
|
|
|
- case 'false':
|
|
|
- stack.push(false);
|
|
|
- break;
|
|
|
- case 'floor':
|
|
|
- a = stack.pop();
|
|
|
- stack.push(Math.floor(a));
|
|
|
- break;
|
|
|
- case 'ge':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- stack.push(a >= b);
|
|
|
- break;
|
|
|
- case 'gt':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- stack.push(a > b);
|
|
|
- break;
|
|
|
- case 'idiv':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- stack.push((a / b) | 0);
|
|
|
- break;
|
|
|
- case 'index':
|
|
|
- a = stack.pop();
|
|
|
- stack.index(a);
|
|
|
- break;
|
|
|
- case 'le':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- stack.push(a <= b);
|
|
|
- break;
|
|
|
- case 'ln':
|
|
|
- a = stack.pop();
|
|
|
- stack.push(Math.log(a));
|
|
|
- break;
|
|
|
- case 'log':
|
|
|
- a = stack.pop();
|
|
|
- stack.push(Math.log(a) / Math.LN10);
|
|
|
- break;
|
|
|
- case 'lt':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- stack.push(a < b);
|
|
|
- break;
|
|
|
- case 'mod':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- stack.push(a % b);
|
|
|
- break;
|
|
|
- case 'mul':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- stack.push(a * b);
|
|
|
- break;
|
|
|
- case 'ne':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- stack.push(a !== b);
|
|
|
- break;
|
|
|
- case 'neg':
|
|
|
- a = stack.pop();
|
|
|
- stack.push(-a);
|
|
|
- break;
|
|
|
- case 'not':
|
|
|
- a = stack.pop();
|
|
|
- if (isBool(a)) {
|
|
|
- stack.push(!a);
|
|
|
- } else {
|
|
|
- stack.push(~a);
|
|
|
- }
|
|
|
- break;
|
|
|
- case 'or':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- if (isBool(a) && isBool(b)) {
|
|
|
- stack.push(a || b);
|
|
|
- } else {
|
|
|
- stack.push(a | b);
|
|
|
- }
|
|
|
- break;
|
|
|
- case 'pop':
|
|
|
- stack.pop();
|
|
|
- break;
|
|
|
- case 'roll':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- stack.roll(a, b);
|
|
|
- break;
|
|
|
- case 'round':
|
|
|
- a = stack.pop();
|
|
|
- stack.push(Math.round(a));
|
|
|
- break;
|
|
|
- case 'sin':
|
|
|
- a = stack.pop();
|
|
|
- stack.push(Math.sin(a));
|
|
|
- break;
|
|
|
- case 'sqrt':
|
|
|
- a = stack.pop();
|
|
|
- stack.push(Math.sqrt(a));
|
|
|
- break;
|
|
|
- case 'sub':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- stack.push(a - b);
|
|
|
- break;
|
|
|
- case 'true':
|
|
|
- stack.push(true);
|
|
|
- break;
|
|
|
- case 'truncate':
|
|
|
- a = stack.pop();
|
|
|
- a = a < 0 ? Math.ceil(a) : Math.floor(a);
|
|
|
- stack.push(a);
|
|
|
- break;
|
|
|
- case 'xor':
|
|
|
- b = stack.pop();
|
|
|
- a = stack.pop();
|
|
|
- if (isBool(a) && isBool(b)) {
|
|
|
- stack.push(a !== b);
|
|
|
- } else {
|
|
|
- stack.push(a ^ b);
|
|
|
- }
|
|
|
- break;
|
|
|
- default:
|
|
|
- error('Unknown operator ' + operator);
|
|
|
- break;
|
|
|
- }
|
|
|
+var DeviceRgbCS = (function DeviceRgbCSClosure() {
|
|
|
+ function DeviceRgbCS() {
|
|
|
+ this.name = 'DeviceRGB';
|
|
|
+ this.numComps = 3;
|
|
|
+ this.defaultColor = new Float32Array([0, 0, 0]);
|
|
|
+ }
|
|
|
+ DeviceRgbCS.prototype = {
|
|
|
+ getRgb: ColorSpace.prototype.getRgb,
|
|
|
+ getRgbItem: function DeviceRgbCS_getRgbItem(src, srcOffset,
|
|
|
+ dest, destOffset) {
|
|
|
+ var r = (src[srcOffset] * 255) | 0;
|
|
|
+ var g = (src[srcOffset + 1] * 255) | 0;
|
|
|
+ var b = (src[srcOffset + 2] * 255) | 0;
|
|
|
+ dest[destOffset] = r < 0 ? 0 : r > 255 ? 255 : r;
|
|
|
+ dest[destOffset + 1] = g < 0 ? 0 : g > 255 ? 255 : g;
|
|
|
+ dest[destOffset + 2] = b < 0 ? 0 : b > 255 ? 255 : b;
|
|
|
+ },
|
|
|
+ getRgbBuffer: function DeviceRgbCS_getRgbBuffer(src, srcOffset, count,
|
|
|
+ dest, destOffset, bits,
|
|
|
+ alpha01) {
|
|
|
+ if (bits === 8 && alpha01 === 0) {
|
|
|
+ dest.set(src.subarray(srcOffset, srcOffset + count * 3), destOffset);
|
|
|
+ return;
|
|
|
}
|
|
|
- return stack.stack;
|
|
|
- }
|
|
|
+ var scale = 255 / ((1 << bits) - 1);
|
|
|
+ var j = srcOffset, q = destOffset;
|
|
|
+ for (var i = 0; i < count; ++i) {
|
|
|
+ dest[q++] = (scale * src[j++]) | 0;
|
|
|
+ dest[q++] = (scale * src[j++]) | 0;
|
|
|
+ dest[q++] = (scale * src[j++]) | 0;
|
|
|
+ q += alpha01;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ getOutputLength: function DeviceRgbCS_getOutputLength(inputLength,
|
|
|
+ alpha01) {
|
|
|
+ return (inputLength * (3 + alpha01) / 3) | 0;
|
|
|
+ },
|
|
|
+ isPassthrough: function DeviceRgbCS_isPassthrough(bits) {
|
|
|
+ return bits === 8;
|
|
|
+ },
|
|
|
+ fillRgb: ColorSpace.prototype.fillRgb,
|
|
|
+ isDefaultDecode: function DeviceRgbCS_isDefaultDecode(decodeMap) {
|
|
|
+ return ColorSpace.isDefaultDecode(decodeMap, this.numComps);
|
|
|
+ },
|
|
|
+ usesZeroToOneRange: true
|
|
|
};
|
|
|
- return PostScriptEvaluator;
|
|
|
+ return DeviceRgbCS;
|
|
|
})();
|
|
|
|
|
|
-// Most of the PDFs functions consist of simple operations such as:
|
|
|
-// roll, exch, sub, cvr, pop, index, dup, mul, if, gt, add.
|
|
|
-//
|
|
|
-// We can compile most of such programs, and at the same moment, we can
|
|
|
-// optimize some expressions using basic math properties. Keeping track of
|
|
|
-// min/max values will allow us to avoid extra Math.min/Math.max calls.
|
|
|
-var PostScriptCompiler = (function PostScriptCompilerClosure() {
|
|
|
- function AstNode(type) {
|
|
|
- this.type = type;
|
|
|
- }
|
|
|
- AstNode.prototype.visit = function (visitor) {
|
|
|
- throw new Error('abstract method');
|
|
|
- };
|
|
|
+var DeviceCmykCS = (function DeviceCmykCSClosure() {
|
|
|
+ // The coefficients below was found using numerical analysis: the method of
|
|
|
+ // steepest descent for the sum((f_i - color_value_i)^2) for r/g/b colors,
|
|
|
+ // where color_value is the tabular value from the table of sampled RGB colors
|
|
|
+ // from CMYK US Web Coated (SWOP) colorspace, and f_i is the corresponding
|
|
|
+ // CMYK color conversion using the estimation below:
|
|
|
+ // f(A, B,.. N) = Acc+Bcm+Ccy+Dck+c+Fmm+Gmy+Hmk+Im+Jyy+Kyk+Ly+Mkk+Nk+255
|
|
|
+ function convertToRgb(src, srcOffset, srcScale, dest, destOffset) {
|
|
|
+ var c = src[srcOffset + 0] * srcScale;
|
|
|
+ var m = src[srcOffset + 1] * srcScale;
|
|
|
+ var y = src[srcOffset + 2] * srcScale;
|
|
|
+ var k = src[srcOffset + 3] * srcScale;
|
|
|
|
|
|
- function AstArgument(index, min, max) {
|
|
|
- AstNode.call(this, 'args');
|
|
|
- this.index = index;
|
|
|
- this.min = min;
|
|
|
- this.max = max;
|
|
|
- }
|
|
|
- AstArgument.prototype = Object.create(AstNode.prototype);
|
|
|
- AstArgument.prototype.visit = function (visitor) {
|
|
|
- visitor.visitArgument(this);
|
|
|
- };
|
|
|
+ var r =
|
|
|
+ (c * (-4.387332384609988 * c + 54.48615194189176 * m +
|
|
|
+ 18.82290502165302 * y + 212.25662451639585 * k +
|
|
|
+ -285.2331026137004) +
|
|
|
+ m * (1.7149763477362134 * m - 5.6096736904047315 * y +
|
|
|
+ -17.873870861415444 * k - 5.497006427196366) +
|
|
|
+ y * (-2.5217340131683033 * y - 21.248923337353073 * k +
|
|
|
+ 17.5119270841813) +
|
|
|
+ k * (-21.86122147463605 * k - 189.48180835922747) + 255) | 0;
|
|
|
+ var g =
|
|
|
+ (c * (8.841041422036149 * c + 60.118027045597366 * m +
|
|
|
+ 6.871425592049007 * y + 31.159100130055922 * k +
|
|
|
+ -79.2970844816548) +
|
|
|
+ m * (-15.310361306967817 * m + 17.575251261109482 * y +
|
|
|
+ 131.35250912493976 * k - 190.9453302588951) +
|
|
|
+ y * (4.444339102852739 * y + 9.8632861493405 * k - 24.86741582555878) +
|
|
|
+ k * (-20.737325471181034 * k - 187.80453709719578) + 255) | 0;
|
|
|
+ var b =
|
|
|
+ (c * (0.8842522430003296 * c + 8.078677503112928 * m +
|
|
|
+ 30.89978309703729 * y - 0.23883238689178934 * k +
|
|
|
+ -14.183576799673286) +
|
|
|
+ m * (10.49593273432072 * m + 63.02378494754052 * y +
|
|
|
+ 50.606957656360734 * k - 112.23884253719248) +
|
|
|
+ y * (0.03296041114873217 * y + 115.60384449646641 * k +
|
|
|
+ -193.58209356861505) +
|
|
|
+ k * (-22.33816807309886 * k - 180.12613974708367) + 255) | 0;
|
|
|
|
|
|
- function AstLiteral(number) {
|
|
|
- AstNode.call(this, 'literal');
|
|
|
- this.number = number;
|
|
|
- this.min = number;
|
|
|
- this.max = number;
|
|
|
+ dest[destOffset] = r > 255 ? 255 : r < 0 ? 0 : r;
|
|
|
+ dest[destOffset + 1] = g > 255 ? 255 : g < 0 ? 0 : g;
|
|
|
+ dest[destOffset + 2] = b > 255 ? 255 : b < 0 ? 0 : b;
|
|
|
}
|
|
|
- AstLiteral.prototype = Object.create(AstNode.prototype);
|
|
|
- AstLiteral.prototype.visit = function (visitor) {
|
|
|
- visitor.visitLiteral(this);
|
|
|
- };
|
|
|
|
|
|
- function AstBinaryOperation(op, arg1, arg2, min, max) {
|
|
|
- AstNode.call(this, 'binary');
|
|
|
- this.op = op;
|
|
|
- this.arg1 = arg1;
|
|
|
- this.arg2 = arg2;
|
|
|
- this.min = min;
|
|
|
- this.max = max;
|
|
|
+ function DeviceCmykCS() {
|
|
|
+ this.name = 'DeviceCMYK';
|
|
|
+ this.numComps = 4;
|
|
|
+ this.defaultColor = new Float32Array([0, 0, 0, 1]);
|
|
|
}
|
|
|
- AstBinaryOperation.prototype = Object.create(AstNode.prototype);
|
|
|
- AstBinaryOperation.prototype.visit = function (visitor) {
|
|
|
- visitor.visitBinaryOperation(this);
|
|
|
+ DeviceCmykCS.prototype = {
|
|
|
+ getRgb: ColorSpace.prototype.getRgb,
|
|
|
+ getRgbItem: function DeviceCmykCS_getRgbItem(src, srcOffset,
|
|
|
+ dest, destOffset) {
|
|
|
+ convertToRgb(src, srcOffset, 1, dest, destOffset);
|
|
|
+ },
|
|
|
+ getRgbBuffer: function DeviceCmykCS_getRgbBuffer(src, srcOffset, count,
|
|
|
+ dest, destOffset, bits,
|
|
|
+ alpha01) {
|
|
|
+ var scale = 1 / ((1 << bits) - 1);
|
|
|
+ for (var i = 0; i < count; i++) {
|
|
|
+ convertToRgb(src, srcOffset, scale, dest, destOffset);
|
|
|
+ srcOffset += 4;
|
|
|
+ destOffset += 3 + alpha01;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ getOutputLength: function DeviceCmykCS_getOutputLength(inputLength,
|
|
|
+ alpha01) {
|
|
|
+ return (inputLength / 4 * (3 + alpha01)) | 0;
|
|
|
+ },
|
|
|
+ isPassthrough: ColorSpace.prototype.isPassthrough,
|
|
|
+ fillRgb: ColorSpace.prototype.fillRgb,
|
|
|
+ isDefaultDecode: function DeviceCmykCS_isDefaultDecode(decodeMap) {
|
|
|
+ return ColorSpace.isDefaultDecode(decodeMap, this.numComps);
|
|
|
+ },
|
|
|
+ usesZeroToOneRange: true
|
|
|
};
|
|
|
|
|
|
- function AstMin(arg, max) {
|
|
|
- AstNode.call(this, 'max');
|
|
|
- this.arg = arg;
|
|
|
- this.min = arg.min;
|
|
|
- this.max = max;
|
|
|
- }
|
|
|
- AstMin.prototype = Object.create(AstNode.prototype);
|
|
|
- AstMin.prototype.visit = function (visitor) {
|
|
|
- visitor.visitMin(this);
|
|
|
- };
|
|
|
+ return DeviceCmykCS;
|
|
|
+})();
|
|
|
|
|
|
- function AstVariable(index, min, max) {
|
|
|
- AstNode.call(this, 'var');
|
|
|
- this.index = index;
|
|
|
- this.min = min;
|
|
|
- this.max = max;
|
|
|
- }
|
|
|
- AstVariable.prototype = Object.create(AstNode.prototype);
|
|
|
- AstVariable.prototype.visit = function (visitor) {
|
|
|
- visitor.visitVariable(this);
|
|
|
- };
|
|
|
+//
|
|
|
+// CalGrayCS: Based on "PDF Reference, Sixth Ed", p.245
|
|
|
+//
|
|
|
+var CalGrayCS = (function CalGrayCSClosure() {
|
|
|
+ function CalGrayCS(whitePoint, blackPoint, gamma) {
|
|
|
+ this.name = 'CalGray';
|
|
|
+ this.numComps = 1;
|
|
|
+ this.defaultColor = new Float32Array([0]);
|
|
|
|
|
|
- function AstVariableDefinition(variable, arg) {
|
|
|
- AstNode.call(this, 'definition');
|
|
|
- this.variable = variable;
|
|
|
- this.arg = arg;
|
|
|
+ if (!whitePoint) {
|
|
|
+ error('WhitePoint missing - required for color space CalGray');
|
|
|
+ }
|
|
|
+ blackPoint = blackPoint || [0, 0, 0];
|
|
|
+ gamma = gamma || 1;
|
|
|
+
|
|
|
+ // Translate arguments to spec variables.
|
|
|
+ this.XW = whitePoint[0];
|
|
|
+ this.YW = whitePoint[1];
|
|
|
+ this.ZW = whitePoint[2];
|
|
|
+
|
|
|
+ this.XB = blackPoint[0];
|
|
|
+ this.YB = blackPoint[1];
|
|
|
+ this.ZB = blackPoint[2];
|
|
|
+
|
|
|
+ this.G = gamma;
|
|
|
+
|
|
|
+ // Validate variables as per spec.
|
|
|
+ if (this.XW < 0 || this.ZW < 0 || this.YW !== 1) {
|
|
|
+ error('Invalid WhitePoint components for ' + this.name +
|
|
|
+ ', no fallback available');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.XB < 0 || this.YB < 0 || this.ZB < 0) {
|
|
|
+ info('Invalid BlackPoint for ' + this.name + ', falling back to default');
|
|
|
+ this.XB = this.YB = this.ZB = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.XB !== 0 || this.YB !== 0 || this.ZB !== 0) {
|
|
|
+ warn(this.name + ', BlackPoint: XB: ' + this.XB + ', YB: ' + this.YB +
|
|
|
+ ', ZB: ' + this.ZB + ', only default values are supported.');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.G < 1) {
|
|
|
+ info('Invalid Gamma: ' + this.G + ' for ' + this.name +
|
|
|
+ ', falling back to default');
|
|
|
+ this.G = 1;
|
|
|
+ }
|
|
|
}
|
|
|
- AstVariableDefinition.prototype = Object.create(AstNode.prototype);
|
|
|
- AstVariableDefinition.prototype.visit = function (visitor) {
|
|
|
- visitor.visitVariableDefinition(this);
|
|
|
- };
|
|
|
|
|
|
- function ExpressionBuilderVisitor() {
|
|
|
- this.parts = [];
|
|
|
+ function convertToRgb(cs, src, srcOffset, dest, destOffset, scale) {
|
|
|
+ // A represents a gray component of a calibrated gray space.
|
|
|
+ // A <---> AG in the spec
|
|
|
+ var A = src[srcOffset] * scale;
|
|
|
+ var AG = Math.pow(A, cs.G);
|
|
|
+
|
|
|
+ // Computes L as per spec. ( = cs.YW * AG )
|
|
|
+ // Except if other than default BlackPoint values are used.
|
|
|
+ var L = cs.YW * AG;
|
|
|
+ // http://www.poynton.com/notes/colour_and_gamma/ColorFAQ.html, Ch 4.
|
|
|
+ // Convert values to rgb range [0, 255].
|
|
|
+ var val = Math.max(295.8 * Math.pow(L, 0.333333333333333333) - 40.8, 0) | 0;
|
|
|
+ dest[destOffset] = val;
|
|
|
+ dest[destOffset + 1] = val;
|
|
|
+ dest[destOffset + 2] = val;
|
|
|
}
|
|
|
- ExpressionBuilderVisitor.prototype = {
|
|
|
- visitArgument: function (arg) {
|
|
|
- this.parts.push('Math.max(', arg.min, ', Math.min(',
|
|
|
- arg.max, ', src[srcOffset + ', arg.index, ']))');
|
|
|
- },
|
|
|
- visitVariable: function (variable) {
|
|
|
- this.parts.push('v', variable.index);
|
|
|
- },
|
|
|
- visitLiteral: function (literal) {
|
|
|
- this.parts.push(literal.number);
|
|
|
+
|
|
|
+ CalGrayCS.prototype = {
|
|
|
+ getRgb: ColorSpace.prototype.getRgb,
|
|
|
+ getRgbItem: function CalGrayCS_getRgbItem(src, srcOffset,
|
|
|
+ dest, destOffset) {
|
|
|
+ convertToRgb(this, src, srcOffset, dest, destOffset, 1);
|
|
|
},
|
|
|
- visitBinaryOperation: function (operation) {
|
|
|
- this.parts.push('(');
|
|
|
- operation.arg1.visit(this);
|
|
|
- this.parts.push(' ', operation.op, ' ');
|
|
|
- operation.arg2.visit(this);
|
|
|
- this.parts.push(')');
|
|
|
+ getRgbBuffer: function CalGrayCS_getRgbBuffer(src, srcOffset, count,
|
|
|
+ dest, destOffset, bits,
|
|
|
+ alpha01) {
|
|
|
+ var scale = 1 / ((1 << bits) - 1);
|
|
|
+
|
|
|
+ for (var i = 0; i < count; ++i) {
|
|
|
+ convertToRgb(this, src, srcOffset, dest, destOffset, scale);
|
|
|
+ srcOffset += 1;
|
|
|
+ destOffset += 3 + alpha01;
|
|
|
+ }
|
|
|
},
|
|
|
- visitVariableDefinition: function (definition) {
|
|
|
- this.parts.push('var ');
|
|
|
- definition.variable.visit(this);
|
|
|
- this.parts.push(' = ');
|
|
|
- definition.arg.visit(this);
|
|
|
- this.parts.push(';');
|
|
|
+ getOutputLength: function CalGrayCS_getOutputLength(inputLength, alpha01) {
|
|
|
+ return inputLength * (3 + alpha01);
|
|
|
},
|
|
|
- visitMin: function (max) {
|
|
|
- this.parts.push('Math.min(');
|
|
|
- max.arg.visit(this);
|
|
|
- this.parts.push(', ', max.max, ')');
|
|
|
+ isPassthrough: ColorSpace.prototype.isPassthrough,
|
|
|
+ fillRgb: ColorSpace.prototype.fillRgb,
|
|
|
+ isDefaultDecode: function CalGrayCS_isDefaultDecode(decodeMap) {
|
|
|
+ return ColorSpace.isDefaultDecode(decodeMap, this.numComps);
|
|
|
},
|
|
|
- toString: function () {
|
|
|
- return this.parts.join('');
|
|
|
- }
|
|
|
+ usesZeroToOneRange: true
|
|
|
};
|
|
|
+ return CalGrayCS;
|
|
|
+})();
|
|
|
|
|
|
- function buildAddOperation(num1, num2) {
|
|
|
- if (num2.type === 'literal' && num2.number === 0) {
|
|
|
- // optimization: second operand is 0
|
|
|
- return num1;
|
|
|
+//
|
|
|
+// CalRGBCS: Based on "PDF Reference, Sixth Ed", p.247
|
|
|
+//
|
|
|
+var CalRGBCS = (function CalRGBCSClosure() {
|
|
|
+
|
|
|
+ // See http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html for these
|
|
|
+ // matrices.
|
|
|
+ var BRADFORD_SCALE_MATRIX = new Float32Array([
|
|
|
+ 0.8951, 0.2664, -0.1614,
|
|
|
+ -0.7502, 1.7135, 0.0367,
|
|
|
+ 0.0389, -0.0685, 1.0296]);
|
|
|
+
|
|
|
+ var BRADFORD_SCALE_INVERSE_MATRIX = new Float32Array([
|
|
|
+ 0.9869929, -0.1470543, 0.1599627,
|
|
|
+ 0.4323053, 0.5183603, 0.0492912,
|
|
|
+ -0.0085287, 0.0400428, 0.9684867]);
|
|
|
+
|
|
|
+ // See http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html.
|
|
|
+ var SRGB_D65_XYZ_TO_RGB_MATRIX = new Float32Array([
|
|
|
+ 3.2404542, -1.5371385, -0.4985314,
|
|
|
+ -0.9692660, 1.8760108, 0.0415560,
|
|
|
+ 0.0556434, -0.2040259, 1.0572252]);
|
|
|
+
|
|
|
+ var FLAT_WHITEPOINT_MATRIX = new Float32Array([1, 1, 1]);
|
|
|
+
|
|
|
+ var tempNormalizeMatrix = new Float32Array(3);
|
|
|
+ var tempConvertMatrix1 = new Float32Array(3);
|
|
|
+ var tempConvertMatrix2 = new Float32Array(3);
|
|
|
+
|
|
|
+ var DECODE_L_CONSTANT = Math.pow(((8 + 16) / 116), 3) / 8.0;
|
|
|
+
|
|
|
+ function CalRGBCS(whitePoint, blackPoint, gamma, matrix) {
|
|
|
+ this.name = 'CalRGB';
|
|
|
+ this.numComps = 3;
|
|
|
+ this.defaultColor = new Float32Array(3);
|
|
|
+
|
|
|
+ if (!whitePoint) {
|
|
|
+ error('WhitePoint missing - required for color space CalRGB');
|
|
|
}
|
|
|
- if (num1.type === 'literal' && num1.number === 0) {
|
|
|
- // optimization: first operand is 0
|
|
|
- return num2;
|
|
|
+ blackPoint = blackPoint || new Float32Array(3);
|
|
|
+ gamma = gamma || new Float32Array([1, 1, 1]);
|
|
|
+ matrix = matrix || new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
|
+
|
|
|
+ // Translate arguments to spec variables.
|
|
|
+ var XW = whitePoint[0];
|
|
|
+ var YW = whitePoint[1];
|
|
|
+ var ZW = whitePoint[2];
|
|
|
+ this.whitePoint = whitePoint;
|
|
|
+
|
|
|
+ var XB = blackPoint[0];
|
|
|
+ var YB = blackPoint[1];
|
|
|
+ var ZB = blackPoint[2];
|
|
|
+ this.blackPoint = blackPoint;
|
|
|
+
|
|
|
+ this.GR = gamma[0];
|
|
|
+ this.GG = gamma[1];
|
|
|
+ this.GB = gamma[2];
|
|
|
+
|
|
|
+ this.MXA = matrix[0];
|
|
|
+ this.MYA = matrix[1];
|
|
|
+ this.MZA = matrix[2];
|
|
|
+ this.MXB = matrix[3];
|
|
|
+ this.MYB = matrix[4];
|
|
|
+ this.MZB = matrix[5];
|
|
|
+ this.MXC = matrix[6];
|
|
|
+ this.MYC = matrix[7];
|
|
|
+ this.MZC = matrix[8];
|
|
|
+
|
|
|
+ // Validate variables as per spec.
|
|
|
+ if (XW < 0 || ZW < 0 || YW !== 1) {
|
|
|
+ error('Invalid WhitePoint components for ' + this.name +
|
|
|
+ ', no fallback available');
|
|
|
}
|
|
|
- if (num2.type === 'literal' && num1.type === 'literal') {
|
|
|
- // optimization: operands operand are literals
|
|
|
- return new AstLiteral(num1.number + num2.number);
|
|
|
+
|
|
|
+ if (XB < 0 || YB < 0 || ZB < 0) {
|
|
|
+ info('Invalid BlackPoint for ' + this.name + ' [' + XB + ', ' + YB +
|
|
|
+ ', ' + ZB + '], falling back to default');
|
|
|
+ this.blackPoint = new Float32Array(3);
|
|
|
}
|
|
|
- return new AstBinaryOperation('+', num1, num2,
|
|
|
- num1.min + num2.min, num1.max + num2.max);
|
|
|
- }
|
|
|
|
|
|
- function buildMulOperation(num1, num2) {
|
|
|
- if (num2.type === 'literal') {
|
|
|
- // optimization: second operands is a literal...
|
|
|
- if (num2.number === 0) {
|
|
|
- return new AstLiteral(0); // and it's 0
|
|
|
- } else if (num2.number === 1) {
|
|
|
- return num1; // and it's 1
|
|
|
- } else if (num1.type === 'literal') {
|
|
|
- // ... and first operands is a literal too
|
|
|
- return new AstLiteral(num1.number * num2.number);
|
|
|
- }
|
|
|
+ if (this.GR < 0 || this.GG < 0 || this.GB < 0) {
|
|
|
+ info('Invalid Gamma [' + this.GR + ', ' + this.GG + ', ' + this.GB +
|
|
|
+ '] for ' + this.name + ', falling back to default');
|
|
|
+ this.GR = this.GG = this.GB = 1;
|
|
|
}
|
|
|
- if (num1.type === 'literal') {
|
|
|
- // optimization: first operands is a literal...
|
|
|
- if (num1.number === 0) {
|
|
|
- return new AstLiteral(0); // and it's 0
|
|
|
- } else if (num1.number === 1) {
|
|
|
- return num2; // and it's 1
|
|
|
- }
|
|
|
+
|
|
|
+ if (this.MXA < 0 || this.MYA < 0 || this.MZA < 0 ||
|
|
|
+ this.MXB < 0 || this.MYB < 0 || this.MZB < 0 ||
|
|
|
+ this.MXC < 0 || this.MYC < 0 || this.MZC < 0) {
|
|
|
+ info('Invalid Matrix for ' + this.name + ' [' +
|
|
|
+ this.MXA + ', ' + this.MYA + ', ' + this.MZA +
|
|
|
+ this.MXB + ', ' + this.MYB + ', ' + this.MZB +
|
|
|
+ this.MXC + ', ' + this.MYC + ', ' + this.MZC +
|
|
|
+ '], falling back to default');
|
|
|
+ this.MXA = this.MYB = this.MZC = 1;
|
|
|
+ this.MXB = this.MYA = this.MZA = this.MXC = this.MYC = this.MZB = 0;
|
|
|
}
|
|
|
- var min = Math.min(num1.min * num2.min, num1.min * num2.max,
|
|
|
- num1.max * num2.min, num1.max * num2.max);
|
|
|
- var max = Math.max(num1.min * num2.min, num1.min * num2.max,
|
|
|
- num1.max * num2.min, num1.max * num2.max);
|
|
|
- return new AstBinaryOperation('*', num1, num2, min, max);
|
|
|
}
|
|
|
|
|
|
- function buildSubOperation(num1, num2) {
|
|
|
- if (num2.type === 'literal') {
|
|
|
- // optimization: second operands is a literal...
|
|
|
- if (num2.number === 0) {
|
|
|
- return num1; // ... and it's 0
|
|
|
- } else if (num1.type === 'literal') {
|
|
|
- // ... and first operands is a literal too
|
|
|
- return new AstLiteral(num1.number - num2.number);
|
|
|
- }
|
|
|
+ function matrixProduct(a, b, result) {
|
|
|
+ result[0] = a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
|
+ result[1] = a[3] * b[0] + a[4] * b[1] + a[5] * b[2];
|
|
|
+ result[2] = a[6] * b[0] + a[7] * b[1] + a[8] * b[2];
|
|
|
+ }
|
|
|
+
|
|
|
+ function convertToFlat(sourceWhitePoint, LMS, result) {
|
|
|
+ result[0] = LMS[0] * 1 / sourceWhitePoint[0];
|
|
|
+ result[1] = LMS[1] * 1 / sourceWhitePoint[1];
|
|
|
+ result[2] = LMS[2] * 1 / sourceWhitePoint[2];
|
|
|
+ }
|
|
|
+
|
|
|
+ function convertToD65(sourceWhitePoint, LMS, result) {
|
|
|
+ var D65X = 0.95047;
|
|
|
+ var D65Y = 1;
|
|
|
+ var D65Z = 1.08883;
|
|
|
+
|
|
|
+ result[0] = LMS[0] * D65X / sourceWhitePoint[0];
|
|
|
+ result[1] = LMS[1] * D65Y / sourceWhitePoint[1];
|
|
|
+ result[2] = LMS[2] * D65Z / sourceWhitePoint[2];
|
|
|
+ }
|
|
|
+
|
|
|
+ function sRGBTransferFunction(color) {
|
|
|
+ // See http://en.wikipedia.org/wiki/SRGB.
|
|
|
+ if (color <= 0.0031308){
|
|
|
+ return adjustToRange(0, 1, 12.92 * color);
|
|
|
}
|
|
|
- if (num2.type === 'binary' && num2.op === '-' &&
|
|
|
- num1.type === 'literal' && num1.number === 1 &&
|
|
|
- num2.arg1.type === 'literal' && num2.arg1.number === 1) {
|
|
|
- // optimization for case: 1 - (1 - x)
|
|
|
- return num2.arg2;
|
|
|
+
|
|
|
+ return adjustToRange(0, 1, (1 + 0.055) * Math.pow(color, 1 / 2.4) - 0.055);
|
|
|
+ }
|
|
|
+
|
|
|
+ function adjustToRange(min, max, value) {
|
|
|
+ return Math.max(min, Math.min(max, value));
|
|
|
+ }
|
|
|
+
|
|
|
+ function decodeL(L) {
|
|
|
+ if (L < 0) {
|
|
|
+ return -decodeL(-L);
|
|
|
}
|
|
|
- return new AstBinaryOperation('-', num1, num2,
|
|
|
- num1.min - num2.max, num1.max - num2.min);
|
|
|
+
|
|
|
+ if (L > 8.0) {
|
|
|
+ return Math.pow(((L + 16) / 116), 3);
|
|
|
+ }
|
|
|
+
|
|
|
+ return L * DECODE_L_CONSTANT;
|
|
|
+ }
|
|
|
+
|
|
|
+ function compensateBlackPoint(sourceBlackPoint, XYZ_Flat, result) {
|
|
|
+
|
|
|
+ // In case the blackPoint is already the default blackPoint then there is
|
|
|
+ // no need to do compensation.
|
|
|
+ if (sourceBlackPoint[0] === 0 &&
|
|
|
+ sourceBlackPoint[1] === 0 &&
|
|
|
+ sourceBlackPoint[2] === 0) {
|
|
|
+ result[0] = XYZ_Flat[0];
|
|
|
+ result[1] = XYZ_Flat[1];
|
|
|
+ result[2] = XYZ_Flat[2];
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // For the blackPoint calculation details, please see
|
|
|
+ // http://www.adobe.com/content/dam/Adobe/en/devnet/photoshop/sdk/
|
|
|
+ // AdobeBPC.pdf.
|
|
|
+ // The destination blackPoint is the default blackPoint [0, 0, 0].
|
|
|
+ var zeroDecodeL = decodeL(0);
|
|
|
+
|
|
|
+ var X_DST = zeroDecodeL;
|
|
|
+ var X_SRC = decodeL(sourceBlackPoint[0]);
|
|
|
+
|
|
|
+ var Y_DST = zeroDecodeL;
|
|
|
+ var Y_SRC = decodeL(sourceBlackPoint[1]);
|
|
|
+
|
|
|
+ var Z_DST = zeroDecodeL;
|
|
|
+ var Z_SRC = decodeL(sourceBlackPoint[2]);
|
|
|
+
|
|
|
+ var X_Scale = (1 - X_DST) / (1 - X_SRC);
|
|
|
+ var X_Offset = 1 - X_Scale;
|
|
|
+
|
|
|
+ var Y_Scale = (1 - Y_DST) / (1 - Y_SRC);
|
|
|
+ var Y_Offset = 1 - Y_Scale;
|
|
|
+
|
|
|
+ var Z_Scale = (1 - Z_DST) / (1 - Z_SRC);
|
|
|
+ var Z_Offset = 1 - Z_Scale;
|
|
|
+
|
|
|
+ result[0] = XYZ_Flat[0] * X_Scale + X_Offset;
|
|
|
+ result[1] = XYZ_Flat[1] * Y_Scale + Y_Offset;
|
|
|
+ result[2] = XYZ_Flat[2] * Z_Scale + Z_Offset;
|
|
|
}
|
|
|
|
|
|
- function buildMinOperation(num1, max) {
|
|
|
- if (num1.min >= max) {
|
|
|
- // optimization: num1 min value is not less than required max
|
|
|
- return new AstLiteral(max); // just returning max
|
|
|
- } else if (num1.max <= max) {
|
|
|
- // optimization: num1 max value is not greater than required max
|
|
|
- return num1; // just returning an argument
|
|
|
+ function normalizeWhitePointToFlat(sourceWhitePoint, XYZ_In, result) {
|
|
|
+
|
|
|
+ // In case the whitePoint is already flat then there is no need to do
|
|
|
+ // normalization.
|
|
|
+ if (sourceWhitePoint[0] === 1 && sourceWhitePoint[2] === 1) {
|
|
|
+ result[0] = XYZ_In[0];
|
|
|
+ result[1] = XYZ_In[1];
|
|
|
+ result[2] = XYZ_In[2];
|
|
|
+ return;
|
|
|
}
|
|
|
- return new AstMin(num1, max);
|
|
|
+
|
|
|
+ var LMS = result;
|
|
|
+ matrixProduct(BRADFORD_SCALE_MATRIX, XYZ_In, LMS);
|
|
|
+
|
|
|
+ var LMS_Flat = tempNormalizeMatrix;
|
|
|
+ convertToFlat(sourceWhitePoint, LMS, LMS_Flat);
|
|
|
+
|
|
|
+ matrixProduct(BRADFORD_SCALE_INVERSE_MATRIX, LMS_Flat, result);
|
|
|
}
|
|
|
|
|
|
- function PostScriptCompiler() {}
|
|
|
- PostScriptCompiler.prototype = {
|
|
|
- compile: function PostScriptCompiler_compile(code, domain, range) {
|
|
|
- var stack = [];
|
|
|
- var i, ii;
|
|
|
- var instructions = [];
|
|
|
- var inputSize = domain.length >> 1, outputSize = range.length >> 1;
|
|
|
- var lastRegister = 0;
|
|
|
- var n, j;
|
|
|
- var num1, num2, ast1, ast2, tmpVar, item;
|
|
|
- for (i = 0; i < inputSize; i++) {
|
|
|
- stack.push(new AstArgument(i, domain[i * 2], domain[i * 2 + 1]));
|
|
|
- }
|
|
|
+ function normalizeWhitePointToD65(sourceWhitePoint, XYZ_In, result) {
|
|
|
|
|
|
- for (i = 0, ii = code.length; i < ii; i++) {
|
|
|
- item = code[i];
|
|
|
- if (typeof item === 'number') {
|
|
|
- stack.push(new AstLiteral(item));
|
|
|
- continue;
|
|
|
- }
|
|
|
+ var LMS = result;
|
|
|
+ matrixProduct(BRADFORD_SCALE_MATRIX, XYZ_In, LMS);
|
|
|
|
|
|
- switch (item) {
|
|
|
- case 'add':
|
|
|
- if (stack.length < 2) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- num2 = stack.pop();
|
|
|
- num1 = stack.pop();
|
|
|
- stack.push(buildAddOperation(num1, num2));
|
|
|
- break;
|
|
|
- case 'cvr':
|
|
|
- if (stack.length < 1) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- break;
|
|
|
- case 'mul':
|
|
|
- if (stack.length < 2) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- num2 = stack.pop();
|
|
|
- num1 = stack.pop();
|
|
|
- stack.push(buildMulOperation(num1, num2));
|
|
|
- break;
|
|
|
- case 'sub':
|
|
|
- if (stack.length < 2) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- num2 = stack.pop();
|
|
|
- num1 = stack.pop();
|
|
|
- stack.push(buildSubOperation(num1, num2));
|
|
|
- break;
|
|
|
- case 'exch':
|
|
|
- if (stack.length < 2) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- ast1 = stack.pop(); ast2 = stack.pop();
|
|
|
- stack.push(ast1, ast2);
|
|
|
- break;
|
|
|
- case 'pop':
|
|
|
- if (stack.length < 1) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- stack.pop();
|
|
|
- break;
|
|
|
- case 'index':
|
|
|
- if (stack.length < 1) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- num1 = stack.pop();
|
|
|
- if (num1.type !== 'literal') {
|
|
|
- return null;
|
|
|
- }
|
|
|
- n = num1.number;
|
|
|
- if (n < 0 || (n|0) !== n || stack.length < n) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- ast1 = stack[stack.length - n - 1];
|
|
|
- if (ast1.type === 'literal' || ast1.type === 'var') {
|
|
|
- stack.push(ast1);
|
|
|
- break;
|
|
|
- }
|
|
|
- tmpVar = new AstVariable(lastRegister++, ast1.min, ast1.max);
|
|
|
- stack[stack.length - n - 1] = tmpVar;
|
|
|
- stack.push(tmpVar);
|
|
|
- instructions.push(new AstVariableDefinition(tmpVar, ast1));
|
|
|
- break;
|
|
|
- case 'dup':
|
|
|
- if (stack.length < 1) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- if (typeof code[i + 1] === 'number' && code[i + 2] === 'gt' &&
|
|
|
- code[i + 3] === i + 7 && code[i + 4] === 'jz' &&
|
|
|
- code[i + 5] === 'pop' && code[i + 6] === code[i + 1]) {
|
|
|
- // special case of the commands sequence for the min operation
|
|
|
- num1 = stack.pop();
|
|
|
- stack.push(buildMinOperation(num1, code[i + 1]));
|
|
|
- i += 6;
|
|
|
- break;
|
|
|
- }
|
|
|
- ast1 = stack[stack.length - 1];
|
|
|
- if (ast1.type === 'literal' || ast1.type === 'var') {
|
|
|
- // we don't have to save into intermediate variable a literal or
|
|
|
- // variable.
|
|
|
- stack.push(ast1);
|
|
|
- break;
|
|
|
- }
|
|
|
- tmpVar = new AstVariable(lastRegister++, ast1.min, ast1.max);
|
|
|
- stack[stack.length - 1] = tmpVar;
|
|
|
- stack.push(tmpVar);
|
|
|
- instructions.push(new AstVariableDefinition(tmpVar, ast1));
|
|
|
- break;
|
|
|
- case 'roll':
|
|
|
- if (stack.length < 2) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- num2 = stack.pop();
|
|
|
- num1 = stack.pop();
|
|
|
- if (num2.type !== 'literal' || num1.type !== 'literal') {
|
|
|
- // both roll operands must be numbers
|
|
|
- return null;
|
|
|
- }
|
|
|
- j = num2.number;
|
|
|
- n = num1.number;
|
|
|
- if (n <= 0 || (n|0) !== n || (j|0) !== j || stack.length < n) {
|
|
|
- // ... and integers
|
|
|
- return null;
|
|
|
- }
|
|
|
- j = ((j % n) + n) % n;
|
|
|
- if (j === 0) {
|
|
|
- break; // just skipping -- there are nothing to rotate
|
|
|
- }
|
|
|
- Array.prototype.push.apply(stack,
|
|
|
- stack.splice(stack.length - n, n - j));
|
|
|
- break;
|
|
|
- default:
|
|
|
- return null; // unsupported operator
|
|
|
- }
|
|
|
- }
|
|
|
+ var LMS_D65 = tempNormalizeMatrix;
|
|
|
+ convertToD65(sourceWhitePoint, LMS, LMS_D65);
|
|
|
|
|
|
- if (stack.length !== outputSize) {
|
|
|
- return null;
|
|
|
- }
|
|
|
+ matrixProduct(BRADFORD_SCALE_INVERSE_MATRIX, LMS_D65, result);
|
|
|
+ }
|
|
|
|
|
|
- var result = [];
|
|
|
- instructions.forEach(function (instruction) {
|
|
|
- var statementBuilder = new ExpressionBuilderVisitor();
|
|
|
- instruction.visit(statementBuilder);
|
|
|
- result.push(statementBuilder.toString());
|
|
|
- });
|
|
|
- stack.forEach(function (expr, i) {
|
|
|
- var statementBuilder = new ExpressionBuilderVisitor();
|
|
|
- expr.visit(statementBuilder);
|
|
|
- var min = range[i * 2], max = range[i * 2 + 1];
|
|
|
- var out = [statementBuilder.toString()];
|
|
|
- if (min > expr.min) {
|
|
|
- out.unshift('Math.max(', min, ', ');
|
|
|
- out.push(')');
|
|
|
- }
|
|
|
- if (max < expr.max) {
|
|
|
- out.unshift('Math.min(', max, ', ');
|
|
|
- out.push(')');
|
|
|
- }
|
|
|
- out.unshift('dest[destOffset + ', i, '] = ');
|
|
|
- out.push(';');
|
|
|
- result.push(out.join(''));
|
|
|
- });
|
|
|
- return result.join('\n');
|
|
|
- }
|
|
|
- };
|
|
|
+ function convertToRgb(cs, src, srcOffset, dest, destOffset, scale) {
|
|
|
+ // A, B and C represent a red, green and blue components of a calibrated
|
|
|
+ // rgb space.
|
|
|
+ var A = adjustToRange(0, 1, src[srcOffset] * scale);
|
|
|
+ var B = adjustToRange(0, 1, src[srcOffset + 1] * scale);
|
|
|
+ var C = adjustToRange(0, 1, src[srcOffset + 2] * scale);
|
|
|
|
|
|
- return PostScriptCompiler;
|
|
|
-})();
|
|
|
+ // A <---> AGR in the spec
|
|
|
+ // B <---> BGG in the spec
|
|
|
+ // C <---> CGB in the spec
|
|
|
+ var AGR = Math.pow(A, cs.GR);
|
|
|
+ var BGG = Math.pow(B, cs.GG);
|
|
|
+ var CGB = Math.pow(C, cs.GB);
|
|
|
|
|
|
-exports.isPDFFunction = isPDFFunction;
|
|
|
-exports.PDFFunction = PDFFunction;
|
|
|
-exports.PostScriptEvaluator = PostScriptEvaluator;
|
|
|
-exports.PostScriptCompiler = PostScriptCompiler;
|
|
|
-}));
|
|
|
+ // Computes intermediate variables L, M, N as per spec.
|
|
|
+ // To decode X, Y, Z values map L, M, N directly to them.
|
|
|
+ var X = cs.MXA * AGR + cs.MXB * BGG + cs.MXC * CGB;
|
|
|
+ var Y = cs.MYA * AGR + cs.MYB * BGG + cs.MYC * CGB;
|
|
|
+ var Z = cs.MZA * AGR + cs.MZB * BGG + cs.MZC * CGB;
|
|
|
|
|
|
+ // The following calculations are based on this document:
|
|
|
+ // http://www.adobe.com/content/dam/Adobe/en/devnet/photoshop/sdk/
|
|
|
+ // AdobeBPC.pdf.
|
|
|
+ var XYZ = tempConvertMatrix1;
|
|
|
+ XYZ[0] = X;
|
|
|
+ XYZ[1] = Y;
|
|
|
+ XYZ[2] = Z;
|
|
|
+ var XYZ_Flat = tempConvertMatrix2;
|
|
|
|
|
|
-(function (root, factory) {
|
|
|
- {
|
|
|
- factory((root.pdfjsCoreColorSpace = {}), root.pdfjsSharedUtil,
|
|
|
- root.pdfjsCorePrimitives, root.pdfjsCoreFunction, root.pdfjsCoreStream);
|
|
|
- }
|
|
|
-}(this, function (exports, sharedUtil, corePrimitives, coreFunction,
|
|
|
- coreStream) {
|
|
|
+ normalizeWhitePointToFlat(cs.whitePoint, XYZ, XYZ_Flat);
|
|
|
|
|
|
-var error = sharedUtil.error;
|
|
|
-var info = sharedUtil.info;
|
|
|
-var isArray = sharedUtil.isArray;
|
|
|
-var isString = sharedUtil.isString;
|
|
|
-var shadow = sharedUtil.shadow;
|
|
|
-var warn = sharedUtil.warn;
|
|
|
-var isDict = corePrimitives.isDict;
|
|
|
-var isName = corePrimitives.isName;
|
|
|
-var isStream = corePrimitives.isStream;
|
|
|
-var PDFFunction = coreFunction.PDFFunction;
|
|
|
+ var XYZ_Black = tempConvertMatrix1;
|
|
|
+ compensateBlackPoint(cs.blackPoint, XYZ_Flat, XYZ_Black);
|
|
|
|
|
|
-var coreImage; // see _setCoreImage below
|
|
|
-var PDFImage; // = coreImage.PDFImage;
|
|
|
+ var XYZ_D65 = tempConvertMatrix2;
|
|
|
+ normalizeWhitePointToD65(FLAT_WHITEPOINT_MATRIX, XYZ_Black, XYZ_D65);
|
|
|
|
|
|
-var ColorSpace = (function ColorSpaceClosure() {
|
|
|
- // Constructor should define this.numComps, this.defaultColor, this.name
|
|
|
- function ColorSpace() {
|
|
|
- error('should not call ColorSpace constructor');
|
|
|
+ var SRGB = tempConvertMatrix1;
|
|
|
+ matrixProduct(SRGB_D65_XYZ_TO_RGB_MATRIX, XYZ_D65, SRGB);
|
|
|
+
|
|
|
+ var sR = sRGBTransferFunction(SRGB[0]);
|
|
|
+ var sG = sRGBTransferFunction(SRGB[1]);
|
|
|
+ var sB = sRGBTransferFunction(SRGB[2]);
|
|
|
+
|
|
|
+ // Convert the values to rgb range [0, 255].
|
|
|
+ dest[destOffset] = Math.round(sR * 255);
|
|
|
+ dest[destOffset + 1] = Math.round(sG * 255);
|
|
|
+ dest[destOffset + 2] = Math.round(sB * 255);
|
|
|
}
|
|
|
|
|
|
- ColorSpace.prototype = {
|
|
|
- /**
|
|
|
- * Converts the color value to the RGB color. The color components are
|
|
|
- * located in the src array starting from the srcOffset. Returns the array
|
|
|
- * of the rgb components, each value ranging from [0,255].
|
|
|
- */
|
|
|
- getRgb: function ColorSpace_getRgb(src, srcOffset) {
|
|
|
+ CalRGBCS.prototype = {
|
|
|
+ getRgb: function CalRGBCS_getRgb(src, srcOffset) {
|
|
|
var rgb = new Uint8Array(3);
|
|
|
this.getRgbItem(src, srcOffset, rgb, 0);
|
|
|
return rgb;
|
|
|
},
|
|
|
- /**
|
|
|
- * Converts the color value to the RGB color, similar to the getRgb method.
|
|
|
- * The result placed into the dest array starting from the destOffset.
|
|
|
- */
|
|
|
- getRgbItem: function ColorSpace_getRgbItem(src, srcOffset,
|
|
|
- dest, destOffset) {
|
|
|
- error('Should not call ColorSpace.getRgbItem');
|
|
|
+ getRgbItem: function CalRGBCS_getRgbItem(src, srcOffset,
|
|
|
+ dest, destOffset) {
|
|
|
+ convertToRgb(this, src, srcOffset, dest, destOffset, 1);
|
|
|
},
|
|
|
- /**
|
|
|
- * Converts the specified number of the color values to the RGB colors.
|
|
|
- * The colors are located in the src array starting from the srcOffset.
|
|
|
- * The result is placed into the dest array starting from the destOffset.
|
|
|
- * The src array items shall be in [0,2^bits) range, the dest array items
|
|
|
- * will be in [0,255] range. alpha01 indicates how many alpha components
|
|
|
- * there are in the dest array; it will be either 0 (RGB array) or 1 (RGBA
|
|
|
- * array).
|
|
|
- */
|
|
|
- getRgbBuffer: function ColorSpace_getRgbBuffer(src, srcOffset, count,
|
|
|
- dest, destOffset, bits,
|
|
|
- alpha01) {
|
|
|
- error('Should not call ColorSpace.getRgbBuffer');
|
|
|
+ getRgbBuffer: function CalRGBCS_getRgbBuffer(src, srcOffset, count,
|
|
|
+ dest, destOffset, bits,
|
|
|
+ alpha01) {
|
|
|
+ var scale = 1 / ((1 << bits) - 1);
|
|
|
+
|
|
|
+ for (var i = 0; i < count; ++i) {
|
|
|
+ convertToRgb(this, src, srcOffset, dest, destOffset, scale);
|
|
|
+ srcOffset += 3;
|
|
|
+ destOffset += 3 + alpha01;
|
|
|
+ }
|
|
|
},
|
|
|
- /**
|
|
|
- * Determines the number of bytes required to store the result of the
|
|
|
- * conversion done by the getRgbBuffer method. As in getRgbBuffer,
|
|
|
- * |alpha01| is either 0 (RGB output) or 1 (RGBA output).
|
|
|
- */
|
|
|
- getOutputLength: function ColorSpace_getOutputLength(inputLength,
|
|
|
- alpha01) {
|
|
|
- error('Should not call ColorSpace.getOutputLength');
|
|
|
+ getOutputLength: function CalRGBCS_getOutputLength(inputLength, alpha01) {
|
|
|
+ return (inputLength * (3 + alpha01) / 3) | 0;
|
|
|
},
|
|
|
- /**
|
|
|
- * Returns true if source data will be equal the result/output data.
|
|
|
- */
|
|
|
- isPassthrough: function ColorSpace_isPassthrough(bits) {
|
|
|
- return false;
|
|
|
+ isPassthrough: ColorSpace.prototype.isPassthrough,
|
|
|
+ fillRgb: ColorSpace.prototype.fillRgb,
|
|
|
+ isDefaultDecode: function CalRGBCS_isDefaultDecode(decodeMap) {
|
|
|
+ return ColorSpace.isDefaultDecode(decodeMap, this.numComps);
|
|
|
},
|
|
|
- /**
|
|
|
- * Fills in the RGB colors in the destination buffer. alpha01 indicates
|
|
|
- * how many alpha components there are in the dest array; it will be either
|
|
|
- * 0 (RGB array) or 1 (RGBA array).
|
|
|
- */
|
|
|
- fillRgb: function ColorSpace_fillRgb(dest, originalWidth,
|
|
|
- originalHeight, width, height,
|
|
|
- actualHeight, bpc, comps, alpha01) {
|
|
|
- var count = originalWidth * originalHeight;
|
|
|
- var rgbBuf = null;
|
|
|
- var numComponentColors = 1 << bpc;
|
|
|
- var needsResizing = originalHeight !== height || originalWidth !== width;
|
|
|
- var i, ii;
|
|
|
+ usesZeroToOneRange: true
|
|
|
+ };
|
|
|
+ return CalRGBCS;
|
|
|
+})();
|
|
|
|
|
|
- if (this.isPassthrough(bpc)) {
|
|
|
- rgbBuf = comps;
|
|
|
- } else if (this.numComps === 1 && count > numComponentColors &&
|
|
|
- this.name !== 'DeviceGray' && this.name !== 'DeviceRGB') {
|
|
|
- // Optimization: create a color map when there is just one component and
|
|
|
- // we are converting more colors than the size of the color map. We
|
|
|
- // don't build the map if the colorspace is gray or rgb since those
|
|
|
- // methods are faster than building a map. This mainly offers big speed
|
|
|
- // ups for indexed and alternate colorspaces.
|
|
|
- //
|
|
|
- // TODO it may be worth while to cache the color map. While running
|
|
|
- // testing I never hit a cache so I will leave that out for now (perhaps
|
|
|
- // we are reparsing colorspaces too much?).
|
|
|
- var allColors = bpc <= 8 ? new Uint8Array(numComponentColors) :
|
|
|
- new Uint16Array(numComponentColors);
|
|
|
- var key;
|
|
|
- for (i = 0; i < numComponentColors; i++) {
|
|
|
- allColors[i] = i;
|
|
|
- }
|
|
|
- var colorMap = new Uint8Array(numComponentColors * 3);
|
|
|
- this.getRgbBuffer(allColors, 0, numComponentColors, colorMap, 0, bpc,
|
|
|
- /* alpha01 = */ 0);
|
|
|
+//
|
|
|
+// LabCS: Based on "PDF Reference, Sixth Ed", p.250
|
|
|
+//
|
|
|
+var LabCS = (function LabCSClosure() {
|
|
|
+ function LabCS(whitePoint, blackPoint, range) {
|
|
|
+ this.name = 'Lab';
|
|
|
+ this.numComps = 3;
|
|
|
+ this.defaultColor = new Float32Array([0, 0, 0]);
|
|
|
|
|
|
- var destPos, rgbPos;
|
|
|
- if (!needsResizing) {
|
|
|
- // Fill in the RGB values directly into |dest|.
|
|
|
- destPos = 0;
|
|
|
- for (i = 0; i < count; ++i) {
|
|
|
- key = comps[i] * 3;
|
|
|
- dest[destPos++] = colorMap[key];
|
|
|
- dest[destPos++] = colorMap[key + 1];
|
|
|
- dest[destPos++] = colorMap[key + 2];
|
|
|
- destPos += alpha01;
|
|
|
- }
|
|
|
- } else {
|
|
|
- rgbBuf = new Uint8Array(count * 3);
|
|
|
- rgbPos = 0;
|
|
|
- for (i = 0; i < count; ++i) {
|
|
|
- key = comps[i] * 3;
|
|
|
- rgbBuf[rgbPos++] = colorMap[key];
|
|
|
- rgbBuf[rgbPos++] = colorMap[key + 1];
|
|
|
- rgbBuf[rgbPos++] = colorMap[key + 2];
|
|
|
- }
|
|
|
- }
|
|
|
- } else {
|
|
|
- if (!needsResizing) {
|
|
|
- // Fill in the RGB values directly into |dest|.
|
|
|
- this.getRgbBuffer(comps, 0, width * actualHeight, dest, 0, bpc,
|
|
|
- alpha01);
|
|
|
- } else {
|
|
|
- rgbBuf = new Uint8Array(count * 3);
|
|
|
- this.getRgbBuffer(comps, 0, count, rgbBuf, 0, bpc,
|
|
|
- /* alpha01 = */ 0);
|
|
|
- }
|
|
|
- }
|
|
|
+ if (!whitePoint) {
|
|
|
+ error('WhitePoint missing - required for color space Lab');
|
|
|
+ }
|
|
|
+ blackPoint = blackPoint || [0, 0, 0];
|
|
|
+ range = range || [-100, 100, -100, 100];
|
|
|
+
|
|
|
+ // Translate args to spec variables
|
|
|
+ this.XW = whitePoint[0];
|
|
|
+ this.YW = whitePoint[1];
|
|
|
+ this.ZW = whitePoint[2];
|
|
|
+ this.amin = range[0];
|
|
|
+ this.amax = range[1];
|
|
|
+ this.bmin = range[2];
|
|
|
+ this.bmax = range[3];
|
|
|
+
|
|
|
+ // These are here just for completeness - the spec doesn't offer any
|
|
|
+ // formulas that use BlackPoint in Lab
|
|
|
+ this.XB = blackPoint[0];
|
|
|
+ this.YB = blackPoint[1];
|
|
|
+ this.ZB = blackPoint[2];
|
|
|
+
|
|
|
+ // Validate vars as per spec
|
|
|
+ if (this.XW < 0 || this.ZW < 0 || this.YW !== 1) {
|
|
|
+ error('Invalid WhitePoint components, no fallback available');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.XB < 0 || this.YB < 0 || this.ZB < 0) {
|
|
|
+ info('Invalid BlackPoint, falling back to default');
|
|
|
+ this.XB = this.YB = this.ZB = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.amin > this.amax || this.bmin > this.bmax) {
|
|
|
+ info('Invalid Range, falling back to defaults');
|
|
|
+ this.amin = -100;
|
|
|
+ this.amax = 100;
|
|
|
+ this.bmin = -100;
|
|
|
+ this.bmax = 100;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Function g(x) from spec
|
|
|
+ function fn_g(x) {
|
|
|
+ if (x >= 6 / 29) {
|
|
|
+ return x * x * x;
|
|
|
+ } else {
|
|
|
+ return (108 / 841) * (x - 4 / 29);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function decode(value, high1, low2, high2) {
|
|
|
+ return low2 + (value) * (high2 - low2) / (high1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // If decoding is needed maxVal should be 2^bits per component - 1.
|
|
|
+ function convertToRgb(cs, src, srcOffset, maxVal, dest, destOffset) {
|
|
|
+ // XXX: Lab input is in the range of [0, 100], [amin, amax], [bmin, bmax]
|
|
|
+ // not the usual [0, 1]. If a command like setFillColor is used the src
|
|
|
+ // values will already be within the correct range. However, if we are
|
|
|
+ // converting an image we have to map the values to the correct range given
|
|
|
+ // above.
|
|
|
+ // Ls,as,bs <---> L*,a*,b* in the spec
|
|
|
+ var Ls = src[srcOffset];
|
|
|
+ var as = src[srcOffset + 1];
|
|
|
+ var bs = src[srcOffset + 2];
|
|
|
+ if (maxVal !== false) {
|
|
|
+ Ls = decode(Ls, maxVal, 0, 100);
|
|
|
+ as = decode(as, maxVal, cs.amin, cs.amax);
|
|
|
+ bs = decode(bs, maxVal, cs.bmin, cs.bmax);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Adjust limits of 'as' and 'bs'
|
|
|
+ as = as > cs.amax ? cs.amax : as < cs.amin ? cs.amin : as;
|
|
|
+ bs = bs > cs.bmax ? cs.bmax : bs < cs.bmin ? cs.bmin : bs;
|
|
|
+
|
|
|
+ // Computes intermediate variables X,Y,Z as per spec
|
|
|
+ var M = (Ls + 16) / 116;
|
|
|
+ var L = M + (as / 500);
|
|
|
+ var N = M - (bs / 200);
|
|
|
+
|
|
|
+ var X = cs.XW * fn_g(L);
|
|
|
+ var Y = cs.YW * fn_g(M);
|
|
|
+ var Z = cs.ZW * fn_g(N);
|
|
|
+
|
|
|
+ var r, g, b;
|
|
|
+ // Using different conversions for D50 and D65 white points,
|
|
|
+ // per http://www.color.org/srgb.pdf
|
|
|
+ if (cs.ZW < 1) {
|
|
|
+ // Assuming D50 (X=0.9642, Y=1.00, Z=0.8249)
|
|
|
+ r = X * 3.1339 + Y * -1.6170 + Z * -0.4906;
|
|
|
+ g = X * -0.9785 + Y * 1.9160 + Z * 0.0333;
|
|
|
+ b = X * 0.0720 + Y * -0.2290 + Z * 1.4057;
|
|
|
+ } else {
|
|
|
+ // Assuming D65 (X=0.9505, Y=1.00, Z=1.0888)
|
|
|
+ r = X * 3.2406 + Y * -1.5372 + Z * -0.4986;
|
|
|
+ g = X * -0.9689 + Y * 1.8758 + Z * 0.0415;
|
|
|
+ b = X * 0.0557 + Y * -0.2040 + Z * 1.0570;
|
|
|
+ }
|
|
|
+ // clamp color values to [0,1] range then convert to [0,255] range.
|
|
|
+ dest[destOffset] = r <= 0 ? 0 : r >= 1 ? 255 : Math.sqrt(r) * 255 | 0;
|
|
|
+ dest[destOffset + 1] = g <= 0 ? 0 : g >= 1 ? 255 : Math.sqrt(g) * 255 | 0;
|
|
|
+ dest[destOffset + 2] = b <= 0 ? 0 : b >= 1 ? 255 : Math.sqrt(b) * 255 | 0;
|
|
|
+ }
|
|
|
|
|
|
- if (rgbBuf) {
|
|
|
- if (needsResizing) {
|
|
|
- PDFImage.resize(rgbBuf, bpc, 3, originalWidth, originalHeight, width,
|
|
|
- height, dest, alpha01);
|
|
|
- } else {
|
|
|
- rgbPos = 0;
|
|
|
- destPos = 0;
|
|
|
- for (i = 0, ii = width * actualHeight; i < ii; i++) {
|
|
|
- dest[destPos++] = rgbBuf[rgbPos++];
|
|
|
- dest[destPos++] = rgbBuf[rgbPos++];
|
|
|
- dest[destPos++] = rgbBuf[rgbPos++];
|
|
|
- destPos += alpha01;
|
|
|
- }
|
|
|
- }
|
|
|
+ LabCS.prototype = {
|
|
|
+ getRgb: ColorSpace.prototype.getRgb,
|
|
|
+ getRgbItem: function LabCS_getRgbItem(src, srcOffset, dest, destOffset) {
|
|
|
+ convertToRgb(this, src, srcOffset, false, dest, destOffset);
|
|
|
+ },
|
|
|
+ getRgbBuffer: function LabCS_getRgbBuffer(src, srcOffset, count,
|
|
|
+ dest, destOffset, bits,
|
|
|
+ alpha01) {
|
|
|
+ var maxVal = (1 << bits) - 1;
|
|
|
+ for (var i = 0; i < count; i++) {
|
|
|
+ convertToRgb(this, src, srcOffset, maxVal, dest, destOffset);
|
|
|
+ srcOffset += 3;
|
|
|
+ destOffset += 3 + alpha01;
|
|
|
}
|
|
|
},
|
|
|
- /**
|
|
|
- * True if the colorspace has components in the default range of [0, 1].
|
|
|
- * This should be true for all colorspaces except for lab color spaces
|
|
|
- * which are [0,100], [-128, 127], [-128, 127].
|
|
|
- */
|
|
|
- usesZeroToOneRange: true
|
|
|
+ getOutputLength: function LabCS_getOutputLength(inputLength, alpha01) {
|
|
|
+ return (inputLength * (3 + alpha01) / 3) | 0;
|
|
|
+ },
|
|
|
+ isPassthrough: ColorSpace.prototype.isPassthrough,
|
|
|
+ fillRgb: ColorSpace.prototype.fillRgb,
|
|
|
+ isDefaultDecode: function LabCS_isDefaultDecode(decodeMap) {
|
|
|
+ // XXX: Decoding is handled with the lab conversion because of the strange
|
|
|
+ // ranges that are used.
|
|
|
+ return true;
|
|
|
+ },
|
|
|
+ usesZeroToOneRange: false
|
|
|
};
|
|
|
+ return LabCS;
|
|
|
+})();
|
|
|
|
|
|
- ColorSpace.parse = function ColorSpace_parse(cs, xref, res) {
|
|
|
- var IR = ColorSpace.parseToIR(cs, xref, res);
|
|
|
- if (IR instanceof AlternateCS) {
|
|
|
- return IR;
|
|
|
- }
|
|
|
- return ColorSpace.fromIR(IR);
|
|
|
- };
|
|
|
+// TODO refactor to remove dependency on image.js
|
|
|
+function _setCoreImage(coreImage_) {
|
|
|
+ coreImage = coreImage_;
|
|
|
+ PDFImage = coreImage_.PDFImage;
|
|
|
+}
|
|
|
+exports._setCoreImage = _setCoreImage;
|
|
|
|
|
|
- ColorSpace.fromIR = function ColorSpace_fromIR(IR) {
|
|
|
- var name = isArray(IR) ? IR[0] : IR;
|
|
|
- var whitePoint, blackPoint, gamma;
|
|
|
+exports.ColorSpace = ColorSpace;
|
|
|
|
|
|
- switch (name) {
|
|
|
- case 'DeviceGrayCS':
|
|
|
- return this.singletons.gray;
|
|
|
- case 'DeviceRgbCS':
|
|
|
- return this.singletons.rgb;
|
|
|
- case 'DeviceCmykCS':
|
|
|
- return this.singletons.cmyk;
|
|
|
- case 'CalGrayCS':
|
|
|
- whitePoint = IR[1];
|
|
|
- blackPoint = IR[2];
|
|
|
- gamma = IR[3];
|
|
|
- return new CalGrayCS(whitePoint, blackPoint, gamma);
|
|
|
- case 'CalRGBCS':
|
|
|
- whitePoint = IR[1];
|
|
|
- blackPoint = IR[2];
|
|
|
- gamma = IR[3];
|
|
|
- var matrix = IR[4];
|
|
|
- return new CalRGBCS(whitePoint, blackPoint, gamma, matrix);
|
|
|
- case 'PatternCS':
|
|
|
- var basePatternCS = IR[1];
|
|
|
- if (basePatternCS) {
|
|
|
- basePatternCS = ColorSpace.fromIR(basePatternCS);
|
|
|
- }
|
|
|
- return new PatternCS(basePatternCS);
|
|
|
- case 'IndexedCS':
|
|
|
- var baseIndexedCS = IR[1];
|
|
|
- var hiVal = IR[2];
|
|
|
- var lookup = IR[3];
|
|
|
- return new IndexedCS(ColorSpace.fromIR(baseIndexedCS), hiVal, lookup);
|
|
|
- case 'AlternateCS':
|
|
|
- var numComps = IR[1];
|
|
|
- var alt = IR[2];
|
|
|
- var tintFnIR = IR[3];
|
|
|
+// TODO refactor to remove dependency on colorspace.js
|
|
|
+coreStream._setCoreColorSpace(exports);
|
|
|
+}));
|
|
|
|
|
|
- return new AlternateCS(numComps, ColorSpace.fromIR(alt),
|
|
|
- PDFFunction.fromIR(tintFnIR));
|
|
|
- case 'LabCS':
|
|
|
- whitePoint = IR[1];
|
|
|
- blackPoint = IR[2];
|
|
|
- var range = IR[3];
|
|
|
- return new LabCS(whitePoint, blackPoint, range);
|
|
|
- default:
|
|
|
- error('Unknown name ' + name);
|
|
|
+
|
|
|
+(function (root, factory) {
|
|
|
+ {
|
|
|
+ factory((root.pdfjsCoreImage = {}), root.pdfjsSharedUtil,
|
|
|
+ root.pdfjsCorePrimitives, root.pdfjsCoreColorSpace, root.pdfjsCoreStream,
|
|
|
+ root.pdfjsCoreJpx);
|
|
|
+ }
|
|
|
+}(this, function (exports, sharedUtil, corePrimitives, coreColorSpace,
|
|
|
+ coreStream, coreJpx) {
|
|
|
+
|
|
|
+var ImageKind = sharedUtil.ImageKind;
|
|
|
+var assert = sharedUtil.assert;
|
|
|
+var error = sharedUtil.error;
|
|
|
+var info = sharedUtil.info;
|
|
|
+var isArray = sharedUtil.isArray;
|
|
|
+var warn = sharedUtil.warn;
|
|
|
+var Name = corePrimitives.Name;
|
|
|
+var isStream = corePrimitives.isStream;
|
|
|
+var ColorSpace = coreColorSpace.ColorSpace;
|
|
|
+var DecodeStream = coreStream.DecodeStream;
|
|
|
+var Stream = coreStream.Stream;
|
|
|
+var JpegStream = coreStream.JpegStream;
|
|
|
+var JpxImage = coreJpx.JpxImage;
|
|
|
+
|
|
|
+var PDFImage = (function PDFImageClosure() {
|
|
|
+ /**
|
|
|
+ * Decode the image in the main thread if it supported. Resovles the promise
|
|
|
+ * when the image data is ready.
|
|
|
+ */
|
|
|
+ function handleImageData(handler, xref, res, image) {
|
|
|
+ if (image instanceof JpegStream && image.isNativelyDecodable(xref, res)) {
|
|
|
+ // For natively supported jpegs send them to the main thread for decoding.
|
|
|
+ var dict = image.dict;
|
|
|
+ var colorSpace = dict.get('ColorSpace', 'CS');
|
|
|
+ colorSpace = ColorSpace.parse(colorSpace, xref, res);
|
|
|
+ var numComps = colorSpace.numComps;
|
|
|
+ var decodePromise = handler.sendWithPromise('JpegDecode',
|
|
|
+ [image.getIR(), numComps]);
|
|
|
+ return decodePromise.then(function (message) {
|
|
|
+ var data = message.data;
|
|
|
+ return new Stream(data, 0, data.length, image.dict);
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ return Promise.resolve(image);
|
|
|
}
|
|
|
- return null;
|
|
|
- };
|
|
|
+ }
|
|
|
|
|
|
- ColorSpace.parseToIR = function ColorSpace_parseToIR(cs, xref, res) {
|
|
|
- if (isName(cs)) {
|
|
|
- var colorSpaces = res.get('ColorSpace');
|
|
|
- if (isDict(colorSpaces)) {
|
|
|
- var refcs = colorSpaces.get(cs.name);
|
|
|
- if (refcs) {
|
|
|
- cs = refcs;
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * Decode and clamp a value. The formula is different from the spec because we
|
|
|
+ * don't decode to float range [0,1], we decode it in the [0,max] range.
|
|
|
+ */
|
|
|
+ function decodeAndClamp(value, addend, coefficient, max) {
|
|
|
+ value = addend + value * coefficient;
|
|
|
+ // Clamp the value to the range
|
|
|
+ return (value < 0 ? 0 : (value > max ? max : value));
|
|
|
+ }
|
|
|
+
|
|
|
+ function PDFImage(xref, res, image, inline, smask, mask, isMask) {
|
|
|
+ this.image = image;
|
|
|
+ var dict = image.dict;
|
|
|
+ if (dict.has('Filter')) {
|
|
|
+ var filter = dict.get('Filter').name;
|
|
|
+ if (filter === 'JPXDecode') {
|
|
|
+ var jpxImage = new JpxImage();
|
|
|
+ jpxImage.parseImageProperties(image.stream);
|
|
|
+ image.stream.reset();
|
|
|
+ image.bitsPerComponent = jpxImage.bitsPerComponent;
|
|
|
+ image.numComps = jpxImage.componentsCount;
|
|
|
+ } else if (filter === 'JBIG2Decode') {
|
|
|
+ image.bitsPerComponent = 1;
|
|
|
+ image.numComps = 1;
|
|
|
}
|
|
|
}
|
|
|
+ // TODO cache rendered images?
|
|
|
|
|
|
- cs = xref.fetchIfRef(cs);
|
|
|
- var mode;
|
|
|
+ this.width = dict.get('Width', 'W');
|
|
|
+ this.height = dict.get('Height', 'H');
|
|
|
|
|
|
- if (isName(cs)) {
|
|
|
- mode = cs.name;
|
|
|
- this.mode = mode;
|
|
|
+ if (this.width < 1 || this.height < 1) {
|
|
|
+ error('Invalid image width: ' + this.width + ' or height: ' +
|
|
|
+ this.height);
|
|
|
+ }
|
|
|
|
|
|
- switch (mode) {
|
|
|
- case 'DeviceGray':
|
|
|
- case 'G':
|
|
|
- return 'DeviceGrayCS';
|
|
|
- case 'DeviceRGB':
|
|
|
- case 'RGB':
|
|
|
- return 'DeviceRgbCS';
|
|
|
- case 'DeviceCMYK':
|
|
|
- case 'CMYK':
|
|
|
- return 'DeviceCmykCS';
|
|
|
- case 'Pattern':
|
|
|
- return ['PatternCS', null];
|
|
|
- default:
|
|
|
- error('unrecognized colorspace ' + mode);
|
|
|
- }
|
|
|
- } else if (isArray(cs)) {
|
|
|
- mode = xref.fetchIfRef(cs[0]).name;
|
|
|
- this.mode = mode;
|
|
|
- var numComps, params, alt, whitePoint, blackPoint, gamma;
|
|
|
+ this.interpolate = dict.get('Interpolate', 'I') || false;
|
|
|
+ this.imageMask = dict.get('ImageMask', 'IM') || false;
|
|
|
+ this.matte = dict.get('Matte') || false;
|
|
|
|
|
|
- switch (mode) {
|
|
|
- case 'DeviceGray':
|
|
|
- case 'G':
|
|
|
- return 'DeviceGrayCS';
|
|
|
- case 'DeviceRGB':
|
|
|
- case 'RGB':
|
|
|
- return 'DeviceRgbCS';
|
|
|
- case 'DeviceCMYK':
|
|
|
- case 'CMYK':
|
|
|
- return 'DeviceCmykCS';
|
|
|
- case 'CalGray':
|
|
|
- params = xref.fetchIfRef(cs[1]);
|
|
|
- whitePoint = params.get('WhitePoint');
|
|
|
- blackPoint = params.get('BlackPoint');
|
|
|
- gamma = params.get('Gamma');
|
|
|
- return ['CalGrayCS', whitePoint, blackPoint, gamma];
|
|
|
- case 'CalRGB':
|
|
|
- params = xref.fetchIfRef(cs[1]);
|
|
|
- whitePoint = params.get('WhitePoint');
|
|
|
- blackPoint = params.get('BlackPoint');
|
|
|
- gamma = params.get('Gamma');
|
|
|
- var matrix = params.get('Matrix');
|
|
|
- return ['CalRGBCS', whitePoint, blackPoint, gamma, matrix];
|
|
|
- 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] || 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 = 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 = xref.fetchIfRef(cs[1]);
|
|
|
- numComps = 1;
|
|
|
- if (isName(name)) {
|
|
|
- numComps = 1;
|
|
|
- } else if (isArray(name)) {
|
|
|
- numComps = name.length;
|
|
|
- }
|
|
|
- alt = ColorSpace.parseToIR(cs[2], xref, res);
|
|
|
- var tintFnIR = PDFFunction.getIR(xref, xref.fetchIfRef(cs[3]));
|
|
|
- return ['AlternateCS', numComps, alt, tintFnIR];
|
|
|
- case 'Lab':
|
|
|
- params = xref.fetchIfRef(cs[1]);
|
|
|
- whitePoint = params.get('WhitePoint');
|
|
|
- blackPoint = params.get('BlackPoint');
|
|
|
- var range = params.get('Range');
|
|
|
- return ['LabCS', whitePoint, blackPoint, range];
|
|
|
- default:
|
|
|
- error('unimplemented color space object "' + mode + '"');
|
|
|
+ var bitsPerComponent = image.bitsPerComponent;
|
|
|
+ if (!bitsPerComponent) {
|
|
|
+ bitsPerComponent = dict.get('BitsPerComponent', 'BPC');
|
|
|
+ if (!bitsPerComponent) {
|
|
|
+ if (this.imageMask) {
|
|
|
+ bitsPerComponent = 1;
|
|
|
+ } else {
|
|
|
+ error('Bits per component missing in image: ' + this.imageMask);
|
|
|
+ }
|
|
|
}
|
|
|
- } else {
|
|
|
- error('unrecognized color space object: "' + cs + '"');
|
|
|
}
|
|
|
- return null;
|
|
|
- };
|
|
|
- /**
|
|
|
- * Checks if a decode map matches the default decode map for a color space.
|
|
|
- * This handles the general decode maps where there are two values per
|
|
|
- * component. e.g. [0, 1, 0, 1, 0, 1] for a RGB color.
|
|
|
- * This does not handle Lab, Indexed, or Pattern decode maps since they are
|
|
|
- * 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 (!isArray(decode)) {
|
|
|
- return true;
|
|
|
+ this.bpc = bitsPerComponent;
|
|
|
+
|
|
|
+ if (!this.imageMask) {
|
|
|
+ var colorSpace = dict.get('ColorSpace', 'CS');
|
|
|
+ if (!colorSpace) {
|
|
|
+ info('JPX images (which do not require color spaces)');
|
|
|
+ switch (image.numComps) {
|
|
|
+ case 1:
|
|
|
+ colorSpace = Name.get('DeviceGray');
|
|
|
+ break;
|
|
|
+ case 3:
|
|
|
+ colorSpace = Name.get('DeviceRGB');
|
|
|
+ break;
|
|
|
+ case 4:
|
|
|
+ colorSpace = Name.get('DeviceCMYK');
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ error('JPX images with ' + this.numComps +
|
|
|
+ ' color components not supported.');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.colorSpace = ColorSpace.parse(colorSpace, xref, res);
|
|
|
+ this.numComps = this.colorSpace.numComps;
|
|
|
}
|
|
|
|
|
|
- if (n * 2 !== decode.length) {
|
|
|
- warn('The decode map is not the correct length');
|
|
|
- return true;
|
|
|
+ this.decode = dict.get('Decode', 'D');
|
|
|
+ this.needsDecode = false;
|
|
|
+ if (this.decode &&
|
|
|
+ ((this.colorSpace && !this.colorSpace.isDefaultDecode(this.decode)) ||
|
|
|
+ (isMask && !ColorSpace.isDefaultDecode(this.decode, 1)))) {
|
|
|
+ this.needsDecode = true;
|
|
|
+ // Do some preprocessing to avoid more math.
|
|
|
+ var max = (1 << bitsPerComponent) - 1;
|
|
|
+ this.decodeCoefficients = [];
|
|
|
+ this.decodeAddends = [];
|
|
|
+ for (var i = 0, j = 0; i < this.decode.length; i += 2, ++j) {
|
|
|
+ var dmin = this.decode[i];
|
|
|
+ var dmax = this.decode[i + 1];
|
|
|
+ this.decodeCoefficients[j] = dmax - dmin;
|
|
|
+ this.decodeAddends[j] = max * dmin;
|
|
|
+ }
|
|
|
}
|
|
|
- for (var i = 0, ii = decode.length; i < ii; i += 2) {
|
|
|
- if (decode[i] !== 0 || decode[i + 1] !== 1) {
|
|
|
- return false;
|
|
|
+
|
|
|
+ if (smask) {
|
|
|
+ this.smask = new PDFImage(xref, res, smask, false);
|
|
|
+ } else if (mask) {
|
|
|
+ if (isStream(mask)) {
|
|
|
+ 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;
|
|
|
}
|
|
|
}
|
|
|
- return true;
|
|
|
- };
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * Handles processing of image data and returns the Promise that is resolved
|
|
|
+ * with a PDFImage when the image is ready to be used.
|
|
|
+ */
|
|
|
+ PDFImage.buildImage = function PDFImage_buildImage(handler, xref,
|
|
|
+ res, image, inline) {
|
|
|
+ var imagePromise = handleImageData(handler, xref, res, image);
|
|
|
+ var smaskPromise;
|
|
|
+ var maskPromise;
|
|
|
|
|
|
- ColorSpace.singletons = {
|
|
|
- get gray() {
|
|
|
- return shadow(this, 'gray', new DeviceGrayCS());
|
|
|
- },
|
|
|
- get rgb() {
|
|
|
- return shadow(this, 'rgb', new DeviceRgbCS());
|
|
|
- },
|
|
|
- get cmyk() {
|
|
|
- return shadow(this, 'cmyk', new DeviceCmykCS());
|
|
|
+ var smask = image.dict.get('SMask');
|
|
|
+ var mask = image.dict.get('Mask');
|
|
|
+
|
|
|
+ if (smask) {
|
|
|
+ smaskPromise = handleImageData(handler, xref, res, smask);
|
|
|
+ maskPromise = Promise.resolve(null);
|
|
|
+ } else {
|
|
|
+ smaskPromise = Promise.resolve(null);
|
|
|
+ if (mask) {
|
|
|
+ if (isStream(mask)) {
|
|
|
+ maskPromise = handleImageData(handler, xref, res, mask);
|
|
|
+ } else if (isArray(mask)) {
|
|
|
+ maskPromise = Promise.resolve(mask);
|
|
|
+ } else {
|
|
|
+ warn('Unsupported mask format.');
|
|
|
+ maskPromise = Promise.resolve(null);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ maskPromise = Promise.resolve(null);
|
|
|
+ }
|
|
|
}
|
|
|
+ return Promise.all([imagePromise, smaskPromise, maskPromise]).then(
|
|
|
+ function(results) {
|
|
|
+ var imageData = results[0];
|
|
|
+ var smaskData = results[1];
|
|
|
+ var maskData = results[2];
|
|
|
+ return new PDFImage(xref, res, imageData, inline, smaskData, maskData);
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
- return ColorSpace;
|
|
|
-})();
|
|
|
+ /**
|
|
|
+ * Resize an image using the nearest neighbor algorithm. Currently only
|
|
|
+ * supports one and three component images.
|
|
|
+ * @param {TypedArray} pixels The original image with one component.
|
|
|
+ * @param {Number} bpc Number of bits per component.
|
|
|
+ * @param {Number} components Number of color components, 1 or 3 is supported.
|
|
|
+ * @param {Number} w1 Original width.
|
|
|
+ * @param {Number} h1 Original height.
|
|
|
+ * @param {Number} w2 New width.
|
|
|
+ * @param {Number} h2 New height.
|
|
|
+ * @param {TypedArray} dest (Optional) The destination buffer.
|
|
|
+ * @param {Number} alpha01 (Optional) Size reserved for the alpha channel.
|
|
|
+ * @return {TypedArray} Resized image data.
|
|
|
+ */
|
|
|
+ PDFImage.resize = function PDFImage_resize(pixels, bpc, components,
|
|
|
+ w1, h1, w2, h2, dest, alpha01) {
|
|
|
|
|
|
-/**
|
|
|
- * Alternate color space handles both Separation and DeviceN color spaces. A
|
|
|
- * Separation color space is actually just a DeviceN with one color component.
|
|
|
- * Both color spaces use a tinting function to convert colors to a base color
|
|
|
- * space.
|
|
|
- */
|
|
|
-var AlternateCS = (function AlternateCSClosure() {
|
|
|
- function AlternateCS(numComps, base, tintFn) {
|
|
|
- this.name = 'Alternate';
|
|
|
- this.numComps = numComps;
|
|
|
- this.defaultColor = new Float32Array(numComps);
|
|
|
- for (var i = 0; i < numComps; ++i) {
|
|
|
- this.defaultColor[i] = 1;
|
|
|
+ if (components !== 1 && components !== 3) {
|
|
|
+ error('Unsupported component count for resizing.');
|
|
|
}
|
|
|
- this.base = base;
|
|
|
- this.tintFn = tintFn;
|
|
|
- this.tmpBuf = new Float32Array(base.numComps);
|
|
|
- }
|
|
|
|
|
|
- AlternateCS.prototype = {
|
|
|
- getRgb: ColorSpace.prototype.getRgb,
|
|
|
- getRgbItem: function AlternateCS_getRgbItem(src, srcOffset,
|
|
|
- dest, destOffset) {
|
|
|
- var tmpBuf = this.tmpBuf;
|
|
|
- this.tintFn(src, srcOffset, tmpBuf, 0);
|
|
|
- this.base.getRgbItem(tmpBuf, 0, dest, destOffset);
|
|
|
- },
|
|
|
- getRgbBuffer: function AlternateCS_getRgbBuffer(src, srcOffset, count,
|
|
|
- dest, destOffset, bits,
|
|
|
- alpha01) {
|
|
|
- var tintFn = this.tintFn;
|
|
|
- var base = this.base;
|
|
|
- var scale = 1 / ((1 << bits) - 1);
|
|
|
- var baseNumComps = base.numComps;
|
|
|
- var usesZeroToOneRange = base.usesZeroToOneRange;
|
|
|
- var isPassthrough = (base.isPassthrough(8) || !usesZeroToOneRange) &&
|
|
|
- alpha01 === 0;
|
|
|
- var pos = isPassthrough ? destOffset : 0;
|
|
|
- var baseBuf = isPassthrough ? dest : new Uint8Array(baseNumComps * count);
|
|
|
- var numComps = this.numComps;
|
|
|
+ var length = w2 * h2 * components;
|
|
|
+ var temp = dest ? dest : (bpc <= 8 ? new Uint8Array(length) :
|
|
|
+ (bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length)));
|
|
|
+ var xRatio = w1 / w2;
|
|
|
+ var yRatio = h1 / h2;
|
|
|
+ var i, j, py, newIndex = 0, oldIndex;
|
|
|
+ var xScaled = new Uint16Array(w2);
|
|
|
+ var w1Scanline = w1 * components;
|
|
|
+ if (alpha01 !== 1) {
|
|
|
+ alpha01 = 0;
|
|
|
+ }
|
|
|
|
|
|
- var scaled = new Float32Array(numComps);
|
|
|
- var tinted = new Float32Array(baseNumComps);
|
|
|
- var i, j;
|
|
|
- if (usesZeroToOneRange) {
|
|
|
- for (i = 0; i < count; i++) {
|
|
|
- for (j = 0; j < numComps; j++) {
|
|
|
- scaled[j] = src[srcOffset++] * scale;
|
|
|
- }
|
|
|
- tintFn(scaled, 0, tinted, 0);
|
|
|
- for (j = 0; j < baseNumComps; j++) {
|
|
|
- baseBuf[pos++] = tinted[j] * 255;
|
|
|
- }
|
|
|
- }
|
|
|
- } else {
|
|
|
- for (i = 0; i < count; i++) {
|
|
|
- for (j = 0; j < numComps; j++) {
|
|
|
- scaled[j] = src[srcOffset++] * scale;
|
|
|
- }
|
|
|
- tintFn(scaled, 0, tinted, 0);
|
|
|
- base.getRgbItem(tinted, 0, baseBuf, pos);
|
|
|
- pos += baseNumComps;
|
|
|
+ for (j = 0; j < w2; j++) {
|
|
|
+ xScaled[j] = Math.floor(j * xRatio) * components;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (components === 1) {
|
|
|
+ for (i = 0; i < h2; i++) {
|
|
|
+ py = Math.floor(i * yRatio) * w1Scanline;
|
|
|
+ for (j = 0; j < w2; j++) {
|
|
|
+ oldIndex = py + xScaled[j];
|
|
|
+ temp[newIndex++] = pixels[oldIndex];
|
|
|
}
|
|
|
}
|
|
|
- if (!isPassthrough) {
|
|
|
- base.getRgbBuffer(baseBuf, 0, count, dest, destOffset, 8, alpha01);
|
|
|
+ } else if (components === 3) {
|
|
|
+ for (i = 0; i < h2; i++) {
|
|
|
+ py = Math.floor(i * yRatio) * w1Scanline;
|
|
|
+ for (j = 0; j < w2; j++) {
|
|
|
+ oldIndex = py + xScaled[j];
|
|
|
+ temp[newIndex++] = pixels[oldIndex++];
|
|
|
+ temp[newIndex++] = pixels[oldIndex++];
|
|
|
+ temp[newIndex++] = pixels[oldIndex++];
|
|
|
+ newIndex += alpha01;
|
|
|
+ }
|
|
|
}
|
|
|
- },
|
|
|
- getOutputLength: function AlternateCS_getOutputLength(inputLength,
|
|
|
- alpha01) {
|
|
|
- return this.base.getOutputLength(inputLength *
|
|
|
- this.base.numComps / this.numComps,
|
|
|
- alpha01);
|
|
|
- },
|
|
|
- isPassthrough: ColorSpace.prototype.isPassthrough,
|
|
|
- fillRgb: ColorSpace.prototype.fillRgb,
|
|
|
- isDefaultDecode: function AlternateCS_isDefaultDecode(decodeMap) {
|
|
|
- return ColorSpace.isDefaultDecode(decodeMap, this.numComps);
|
|
|
- },
|
|
|
- usesZeroToOneRange: true
|
|
|
+ }
|
|
|
+ return temp;
|
|
|
};
|
|
|
|
|
|
- return AlternateCS;
|
|
|
-})();
|
|
|
-
|
|
|
-var PatternCS = (function PatternCSClosure() {
|
|
|
- function PatternCS(baseCS) {
|
|
|
- this.name = 'Pattern';
|
|
|
- this.base = baseCS;
|
|
|
- }
|
|
|
- PatternCS.prototype = {};
|
|
|
+ PDFImage.createMask =
|
|
|
+ function PDFImage_createMask(imgArray, width, height,
|
|
|
+ imageIsFromDecodeStream, inverseDecode) {
|
|
|
|
|
|
- return PatternCS;
|
|
|
-})();
|
|
|
+ // |imgArray| might not contain full data for every pixel of the mask, so
|
|
|
+ // we need to distinguish between |computedLength| and |actualLength|.
|
|
|
+ // In particular, if inverseDecode is true, then the array we return must
|
|
|
+ // have a length of |computedLength|.
|
|
|
|
|
|
-var IndexedCS = (function IndexedCSClosure() {
|
|
|
- function IndexedCS(base, highVal, lookup) {
|
|
|
- this.name = 'Indexed';
|
|
|
- this.numComps = 1;
|
|
|
- this.defaultColor = new Uint8Array([0]);
|
|
|
- this.base = base;
|
|
|
- this.highVal = highVal;
|
|
|
+ var computedLength = ((width + 7) >> 3) * height;
|
|
|
+ var actualLength = imgArray.byteLength;
|
|
|
+ var haveFullData = computedLength === actualLength;
|
|
|
+ var data, i;
|
|
|
|
|
|
- var baseNumComps = base.numComps;
|
|
|
- var length = baseNumComps * highVal;
|
|
|
- var lookupArray;
|
|
|
+ if (imageIsFromDecodeStream && (!inverseDecode || haveFullData)) {
|
|
|
+ // imgArray came from a DecodeStream and its data is in an appropriate
|
|
|
+ // form, so we can just transfer it.
|
|
|
+ data = imgArray;
|
|
|
+ } else if (!inverseDecode) {
|
|
|
+ data = new Uint8Array(actualLength);
|
|
|
+ data.set(imgArray);
|
|
|
+ } else {
|
|
|
+ data = new Uint8Array(computedLength);
|
|
|
+ data.set(imgArray);
|
|
|
+ for (i = actualLength; i < computedLength; i++) {
|
|
|
+ data[i] = 0xff;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (isStream(lookup)) {
|
|
|
- lookupArray = new Uint8Array(length);
|
|
|
- var bytes = lookup.getBytes(length);
|
|
|
- lookupArray.set(bytes);
|
|
|
- } else if (isString(lookup)) {
|
|
|
- lookupArray = new Uint8Array(length);
|
|
|
- for (var i = 0; i < length; ++i) {
|
|
|
- lookupArray[i] = lookup.charCodeAt(i);
|
|
|
+ // If necessary, invert the original mask data (but not any extra we might
|
|
|
+ // have added above). It's safe to modify the array -- whether it's the
|
|
|
+ // original or a copy, we're about to transfer it anyway, so nothing else
|
|
|
+ // in this thread can be relying on its contents.
|
|
|
+ if (inverseDecode) {
|
|
|
+ for (i = 0; i < actualLength; i++) {
|
|
|
+ data[i] = ~data[i];
|
|
|
}
|
|
|
- } else if (lookup instanceof Uint8Array || lookup instanceof Array) {
|
|
|
- lookupArray = lookup;
|
|
|
- } else {
|
|
|
- error('Unrecognized lookup table: ' + lookup);
|
|
|
}
|
|
|
- this.lookup = lookupArray;
|
|
|
- }
|
|
|
|
|
|
- IndexedCS.prototype = {
|
|
|
- getRgb: ColorSpace.prototype.getRgb,
|
|
|
- getRgbItem: function IndexedCS_getRgbItem(src, srcOffset,
|
|
|
- dest, destOffset) {
|
|
|
- var numComps = this.base.numComps;
|
|
|
- var start = src[srcOffset] * numComps;
|
|
|
- this.base.getRgbItem(this.lookup, start, dest, destOffset);
|
|
|
- },
|
|
|
- getRgbBuffer: function IndexedCS_getRgbBuffer(src, srcOffset, count,
|
|
|
- dest, destOffset, bits,
|
|
|
- alpha01) {
|
|
|
- var base = this.base;
|
|
|
- var numComps = base.numComps;
|
|
|
- var outputDelta = base.getOutputLength(numComps, alpha01);
|
|
|
- var lookup = this.lookup;
|
|
|
+ return {data: data, width: width, height: height};
|
|
|
+ };
|
|
|
|
|
|
- for (var i = 0; i < count; ++i) {
|
|
|
- var lookupPos = src[srcOffset++] * numComps;
|
|
|
- base.getRgbBuffer(lookup, lookupPos, 1, dest, destOffset, 8, alpha01);
|
|
|
- destOffset += outputDelta;
|
|
|
- }
|
|
|
- },
|
|
|
- getOutputLength: function IndexedCS_getOutputLength(inputLength, alpha01) {
|
|
|
- return this.base.getOutputLength(inputLength * this.base.numComps,
|
|
|
- alpha01);
|
|
|
+ PDFImage.prototype = {
|
|
|
+ get drawWidth() {
|
|
|
+ return Math.max(this.width,
|
|
|
+ this.smask && this.smask.width || 0,
|
|
|
+ this.mask && this.mask.width || 0);
|
|
|
},
|
|
|
- isPassthrough: ColorSpace.prototype.isPassthrough,
|
|
|
- fillRgb: ColorSpace.prototype.fillRgb,
|
|
|
- isDefaultDecode: function IndexedCS_isDefaultDecode(decodeMap) {
|
|
|
- // indexed color maps shouldn't be changed
|
|
|
- return true;
|
|
|
+
|
|
|
+ get drawHeight() {
|
|
|
+ return Math.max(this.height,
|
|
|
+ this.smask && this.smask.height || 0,
|
|
|
+ this.mask && this.mask.height || 0);
|
|
|
},
|
|
|
- usesZeroToOneRange: true
|
|
|
- };
|
|
|
- return IndexedCS;
|
|
|
-})();
|
|
|
|
|
|
-var DeviceGrayCS = (function DeviceGrayCSClosure() {
|
|
|
- function DeviceGrayCS() {
|
|
|
- this.name = 'DeviceGray';
|
|
|
- this.numComps = 1;
|
|
|
- this.defaultColor = new Float32Array([0]);
|
|
|
- }
|
|
|
+ decodeBuffer: function PDFImage_decodeBuffer(buffer) {
|
|
|
+ var bpc = this.bpc;
|
|
|
+ var numComps = this.numComps;
|
|
|
|
|
|
- DeviceGrayCS.prototype = {
|
|
|
- getRgb: ColorSpace.prototype.getRgb,
|
|
|
- getRgbItem: function DeviceGrayCS_getRgbItem(src, srcOffset,
|
|
|
- dest, destOffset) {
|
|
|
- var c = (src[srcOffset] * 255) | 0;
|
|
|
- c = c < 0 ? 0 : c > 255 ? 255 : c;
|
|
|
- dest[destOffset] = dest[destOffset + 1] = dest[destOffset + 2] = c;
|
|
|
- },
|
|
|
- getRgbBuffer: function DeviceGrayCS_getRgbBuffer(src, srcOffset, count,
|
|
|
- dest, destOffset, bits,
|
|
|
- alpha01) {
|
|
|
- var scale = 255 / ((1 << bits) - 1);
|
|
|
- var j = srcOffset, q = destOffset;
|
|
|
- for (var i = 0; i < count; ++i) {
|
|
|
- var c = (scale * src[j++]) | 0;
|
|
|
- dest[q++] = c;
|
|
|
- dest[q++] = c;
|
|
|
- dest[q++] = c;
|
|
|
- q += alpha01;
|
|
|
- }
|
|
|
- },
|
|
|
- getOutputLength: function DeviceGrayCS_getOutputLength(inputLength,
|
|
|
- alpha01) {
|
|
|
- return inputLength * (3 + alpha01);
|
|
|
- },
|
|
|
- isPassthrough: ColorSpace.prototype.isPassthrough,
|
|
|
- fillRgb: ColorSpace.prototype.fillRgb,
|
|
|
- isDefaultDecode: function DeviceGrayCS_isDefaultDecode(decodeMap) {
|
|
|
- return ColorSpace.isDefaultDecode(decodeMap, this.numComps);
|
|
|
- },
|
|
|
- usesZeroToOneRange: true
|
|
|
- };
|
|
|
- return DeviceGrayCS;
|
|
|
-})();
|
|
|
+ var decodeAddends = this.decodeAddends;
|
|
|
+ var decodeCoefficients = this.decodeCoefficients;
|
|
|
+ var max = (1 << bpc) - 1;
|
|
|
+ var i, ii;
|
|
|
|
|
|
-var DeviceRgbCS = (function DeviceRgbCSClosure() {
|
|
|
- function DeviceRgbCS() {
|
|
|
- this.name = 'DeviceRGB';
|
|
|
- this.numComps = 3;
|
|
|
- this.defaultColor = new Float32Array([0, 0, 0]);
|
|
|
- }
|
|
|
- DeviceRgbCS.prototype = {
|
|
|
- getRgb: ColorSpace.prototype.getRgb,
|
|
|
- getRgbItem: function DeviceRgbCS_getRgbItem(src, srcOffset,
|
|
|
- dest, destOffset) {
|
|
|
- var r = (src[srcOffset] * 255) | 0;
|
|
|
- var g = (src[srcOffset + 1] * 255) | 0;
|
|
|
- var b = (src[srcOffset + 2] * 255) | 0;
|
|
|
- dest[destOffset] = r < 0 ? 0 : r > 255 ? 255 : r;
|
|
|
- dest[destOffset + 1] = g < 0 ? 0 : g > 255 ? 255 : g;
|
|
|
- dest[destOffset + 2] = b < 0 ? 0 : b > 255 ? 255 : b;
|
|
|
- },
|
|
|
- getRgbBuffer: function DeviceRgbCS_getRgbBuffer(src, srcOffset, count,
|
|
|
- dest, destOffset, bits,
|
|
|
- alpha01) {
|
|
|
- if (bits === 8 && alpha01 === 0) {
|
|
|
- dest.set(src.subarray(srcOffset, srcOffset + count * 3), destOffset);
|
|
|
+ if (bpc === 1) {
|
|
|
+ // If the buffer needed decode that means it just needs to be inverted.
|
|
|
+ for (i = 0, ii = buffer.length; i < ii; i++) {
|
|
|
+ buffer[i] = +!(buffer[i]);
|
|
|
+ }
|
|
|
return;
|
|
|
}
|
|
|
- var scale = 255 / ((1 << bits) - 1);
|
|
|
- var j = srcOffset, q = destOffset;
|
|
|
- for (var i = 0; i < count; ++i) {
|
|
|
- dest[q++] = (scale * src[j++]) | 0;
|
|
|
- dest[q++] = (scale * src[j++]) | 0;
|
|
|
- dest[q++] = (scale * src[j++]) | 0;
|
|
|
- q += alpha01;
|
|
|
+ var index = 0;
|
|
|
+ for (i = 0, ii = this.width * this.height; i < ii; i++) {
|
|
|
+ for (var j = 0; j < numComps; j++) {
|
|
|
+ buffer[index] = decodeAndClamp(buffer[index], decodeAddends[j],
|
|
|
+ decodeCoefficients[j], max);
|
|
|
+ index++;
|
|
|
+ }
|
|
|
}
|
|
|
},
|
|
|
- getOutputLength: function DeviceRgbCS_getOutputLength(inputLength,
|
|
|
- alpha01) {
|
|
|
- return (inputLength * (3 + alpha01) / 3) | 0;
|
|
|
- },
|
|
|
- isPassthrough: function DeviceRgbCS_isPassthrough(bits) {
|
|
|
- return bits === 8;
|
|
|
- },
|
|
|
- fillRgb: ColorSpace.prototype.fillRgb,
|
|
|
- isDefaultDecode: function DeviceRgbCS_isDefaultDecode(decodeMap) {
|
|
|
- return ColorSpace.isDefaultDecode(decodeMap, this.numComps);
|
|
|
- },
|
|
|
- usesZeroToOneRange: true
|
|
|
- };
|
|
|
- return DeviceRgbCS;
|
|
|
-})();
|
|
|
-
|
|
|
-var DeviceCmykCS = (function DeviceCmykCSClosure() {
|
|
|
- // The coefficients below was found using numerical analysis: the method of
|
|
|
- // steepest descent for the sum((f_i - color_value_i)^2) for r/g/b colors,
|
|
|
- // where color_value is the tabular value from the table of sampled RGB colors
|
|
|
- // from CMYK US Web Coated (SWOP) colorspace, and f_i is the corresponding
|
|
|
- // CMYK color conversion using the estimation below:
|
|
|
- // f(A, B,.. N) = Acc+Bcm+Ccy+Dck+c+Fmm+Gmy+Hmk+Im+Jyy+Kyk+Ly+Mkk+Nk+255
|
|
|
- function convertToRgb(src, srcOffset, srcScale, dest, destOffset) {
|
|
|
- var c = src[srcOffset + 0] * srcScale;
|
|
|
- var m = src[srcOffset + 1] * srcScale;
|
|
|
- var y = src[srcOffset + 2] * srcScale;
|
|
|
- var k = src[srcOffset + 3] * srcScale;
|
|
|
-
|
|
|
- var r =
|
|
|
- (c * (-4.387332384609988 * c + 54.48615194189176 * m +
|
|
|
- 18.82290502165302 * y + 212.25662451639585 * k +
|
|
|
- -285.2331026137004) +
|
|
|
- m * (1.7149763477362134 * m - 5.6096736904047315 * y +
|
|
|
- -17.873870861415444 * k - 5.497006427196366) +
|
|
|
- y * (-2.5217340131683033 * y - 21.248923337353073 * k +
|
|
|
- 17.5119270841813) +
|
|
|
- k * (-21.86122147463605 * k - 189.48180835922747) + 255) | 0;
|
|
|
- var g =
|
|
|
- (c * (8.841041422036149 * c + 60.118027045597366 * m +
|
|
|
- 6.871425592049007 * y + 31.159100130055922 * k +
|
|
|
- -79.2970844816548) +
|
|
|
- m * (-15.310361306967817 * m + 17.575251261109482 * y +
|
|
|
- 131.35250912493976 * k - 190.9453302588951) +
|
|
|
- y * (4.444339102852739 * y + 9.8632861493405 * k - 24.86741582555878) +
|
|
|
- k * (-20.737325471181034 * k - 187.80453709719578) + 255) | 0;
|
|
|
- var b =
|
|
|
- (c * (0.8842522430003296 * c + 8.078677503112928 * m +
|
|
|
- 30.89978309703729 * y - 0.23883238689178934 * k +
|
|
|
- -14.183576799673286) +
|
|
|
- m * (10.49593273432072 * m + 63.02378494754052 * y +
|
|
|
- 50.606957656360734 * k - 112.23884253719248) +
|
|
|
- y * (0.03296041114873217 * y + 115.60384449646641 * k +
|
|
|
- -193.58209356861505) +
|
|
|
- k * (-22.33816807309886 * k - 180.12613974708367) + 255) | 0;
|
|
|
|
|
|
- dest[destOffset] = r > 255 ? 255 : r < 0 ? 0 : r;
|
|
|
- dest[destOffset + 1] = g > 255 ? 255 : g < 0 ? 0 : g;
|
|
|
- dest[destOffset + 2] = b > 255 ? 255 : b < 0 ? 0 : b;
|
|
|
- }
|
|
|
+ getComponents: function PDFImage_getComponents(buffer) {
|
|
|
+ var bpc = this.bpc;
|
|
|
|
|
|
- function DeviceCmykCS() {
|
|
|
- this.name = 'DeviceCMYK';
|
|
|
- this.numComps = 4;
|
|
|
- this.defaultColor = new Float32Array([0, 0, 0, 1]);
|
|
|
- }
|
|
|
- DeviceCmykCS.prototype = {
|
|
|
- getRgb: ColorSpace.prototype.getRgb,
|
|
|
- getRgbItem: function DeviceCmykCS_getRgbItem(src, srcOffset,
|
|
|
- dest, destOffset) {
|
|
|
- convertToRgb(src, srcOffset, 1, dest, destOffset);
|
|
|
- },
|
|
|
- getRgbBuffer: function DeviceCmykCS_getRgbBuffer(src, srcOffset, count,
|
|
|
- dest, destOffset, bits,
|
|
|
- alpha01) {
|
|
|
- var scale = 1 / ((1 << bits) - 1);
|
|
|
- for (var i = 0; i < count; i++) {
|
|
|
- convertToRgb(src, srcOffset, scale, dest, destOffset);
|
|
|
- srcOffset += 4;
|
|
|
- destOffset += 3 + alpha01;
|
|
|
+ // This image doesn't require any extra work.
|
|
|
+ if (bpc === 8) {
|
|
|
+ return buffer;
|
|
|
}
|
|
|
- },
|
|
|
- getOutputLength: function DeviceCmykCS_getOutputLength(inputLength,
|
|
|
- alpha01) {
|
|
|
- return (inputLength / 4 * (3 + alpha01)) | 0;
|
|
|
- },
|
|
|
- isPassthrough: ColorSpace.prototype.isPassthrough,
|
|
|
- fillRgb: ColorSpace.prototype.fillRgb,
|
|
|
- isDefaultDecode: function DeviceCmykCS_isDefaultDecode(decodeMap) {
|
|
|
- return ColorSpace.isDefaultDecode(decodeMap, this.numComps);
|
|
|
- },
|
|
|
- usesZeroToOneRange: true
|
|
|
- };
|
|
|
|
|
|
- return DeviceCmykCS;
|
|
|
-})();
|
|
|
+ var width = this.width;
|
|
|
+ var height = this.height;
|
|
|
+ var numComps = this.numComps;
|
|
|
|
|
|
-//
|
|
|
-// CalGrayCS: Based on "PDF Reference, Sixth Ed", p.245
|
|
|
-//
|
|
|
-var CalGrayCS = (function CalGrayCSClosure() {
|
|
|
- function CalGrayCS(whitePoint, blackPoint, gamma) {
|
|
|
- this.name = 'CalGray';
|
|
|
- this.numComps = 1;
|
|
|
- this.defaultColor = new Float32Array([0]);
|
|
|
+ var length = width * height * numComps;
|
|
|
+ var bufferPos = 0;
|
|
|
+ var output = (bpc <= 8 ? new Uint8Array(length) :
|
|
|
+ (bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length)));
|
|
|
+ var rowComps = width * numComps;
|
|
|
|
|
|
- if (!whitePoint) {
|
|
|
- error('WhitePoint missing - required for color space CalGray');
|
|
|
- }
|
|
|
- blackPoint = blackPoint || [0, 0, 0];
|
|
|
- gamma = gamma || 1;
|
|
|
+ var max = (1 << bpc) - 1;
|
|
|
+ var i = 0, ii, buf;
|
|
|
|
|
|
- // Translate arguments to spec variables.
|
|
|
- this.XW = whitePoint[0];
|
|
|
- this.YW = whitePoint[1];
|
|
|
- this.ZW = whitePoint[2];
|
|
|
+ if (bpc === 1) {
|
|
|
+ // Optimization for reading 1 bpc images.
|
|
|
+ var mask, loop1End, loop2End;
|
|
|
+ for (var j = 0; j < height; j++) {
|
|
|
+ loop1End = i + (rowComps & ~7);
|
|
|
+ loop2End = i + rowComps;
|
|
|
|
|
|
- this.XB = blackPoint[0];
|
|
|
- this.YB = blackPoint[1];
|
|
|
- this.ZB = blackPoint[2];
|
|
|
+ // unroll loop for all full bytes
|
|
|
+ while (i < loop1End) {
|
|
|
+ buf = buffer[bufferPos++];
|
|
|
+ output[i] = (buf >> 7) & 1;
|
|
|
+ output[i + 1] = (buf >> 6) & 1;
|
|
|
+ output[i + 2] = (buf >> 5) & 1;
|
|
|
+ output[i + 3] = (buf >> 4) & 1;
|
|
|
+ output[i + 4] = (buf >> 3) & 1;
|
|
|
+ output[i + 5] = (buf >> 2) & 1;
|
|
|
+ output[i + 6] = (buf >> 1) & 1;
|
|
|
+ output[i + 7] = buf & 1;
|
|
|
+ i += 8;
|
|
|
+ }
|
|
|
|
|
|
- this.G = gamma;
|
|
|
+ // handle remaing bits
|
|
|
+ if (i < loop2End) {
|
|
|
+ buf = buffer[bufferPos++];
|
|
|
+ mask = 128;
|
|
|
+ while (i < loop2End) {
|
|
|
+ output[i++] = +!!(buf & mask);
|
|
|
+ mask >>= 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // The general case that handles all other bpc values.
|
|
|
+ var bits = 0;
|
|
|
+ buf = 0;
|
|
|
+ for (i = 0, ii = length; i < ii; ++i) {
|
|
|
+ if (i % rowComps === 0) {
|
|
|
+ buf = 0;
|
|
|
+ bits = 0;
|
|
|
+ }
|
|
|
|
|
|
- // Validate variables as per spec.
|
|
|
- if (this.XW < 0 || this.ZW < 0 || this.YW !== 1) {
|
|
|
- error('Invalid WhitePoint components for ' + this.name +
|
|
|
- ', no fallback available');
|
|
|
- }
|
|
|
+ while (bits < bpc) {
|
|
|
+ buf = (buf << 8) | buffer[bufferPos++];
|
|
|
+ bits += 8;
|
|
|
+ }
|
|
|
|
|
|
- if (this.XB < 0 || this.YB < 0 || this.ZB < 0) {
|
|
|
- info('Invalid BlackPoint for ' + this.name + ', falling back to default');
|
|
|
- this.XB = this.YB = this.ZB = 0;
|
|
|
- }
|
|
|
+ var remainingBits = bits - bpc;
|
|
|
+ var value = buf >> remainingBits;
|
|
|
+ output[i] = (value < 0 ? 0 : (value > max ? max : value));
|
|
|
+ buf = buf & ((1 << remainingBits) - 1);
|
|
|
+ bits = remainingBits;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return output;
|
|
|
+ },
|
|
|
|
|
|
- if (this.XB !== 0 || this.YB !== 0 || this.ZB !== 0) {
|
|
|
- warn(this.name + ', BlackPoint: XB: ' + this.XB + ', YB: ' + this.YB +
|
|
|
- ', ZB: ' + this.ZB + ', only default values are supported.');
|
|
|
- }
|
|
|
+ fillOpacity: function PDFImage_fillOpacity(rgbaBuf, width, height,
|
|
|
+ actualHeight, image) {
|
|
|
+ var smask = this.smask;
|
|
|
+ var mask = this.mask;
|
|
|
+ var alphaBuf, sw, sh, i, ii, j;
|
|
|
|
|
|
- if (this.G < 1) {
|
|
|
- info('Invalid Gamma: ' + this.G + ' for ' + this.name +
|
|
|
- ', falling back to default');
|
|
|
- this.G = 1;
|
|
|
- }
|
|
|
- }
|
|
|
+ if (smask) {
|
|
|
+ sw = smask.width;
|
|
|
+ sh = smask.height;
|
|
|
+ alphaBuf = new Uint8Array(sw * sh);
|
|
|
+ smask.fillGrayBuffer(alphaBuf);
|
|
|
+ if (sw !== width || sh !== height) {
|
|
|
+ alphaBuf = PDFImage.resize(alphaBuf, smask.bpc, 1, sw, sh, width,
|
|
|
+ height);
|
|
|
+ }
|
|
|
+ } else if (mask) {
|
|
|
+ if (mask instanceof PDFImage) {
|
|
|
+ sw = mask.width;
|
|
|
+ sh = mask.height;
|
|
|
+ alphaBuf = new Uint8Array(sw * sh);
|
|
|
+ mask.numComps = 1;
|
|
|
+ mask.fillGrayBuffer(alphaBuf);
|
|
|
|
|
|
- function convertToRgb(cs, src, srcOffset, dest, destOffset, scale) {
|
|
|
- // A represents a gray component of a calibrated gray space.
|
|
|
- // A <---> AG in the spec
|
|
|
- var A = src[srcOffset] * scale;
|
|
|
- var AG = Math.pow(A, cs.G);
|
|
|
+ // Need to invert values in rgbaBuf
|
|
|
+ for (i = 0, ii = sw * sh; i < ii; ++i) {
|
|
|
+ alphaBuf[i] = 255 - alphaBuf[i];
|
|
|
+ }
|
|
|
|
|
|
- // Computes L as per spec. ( = cs.YW * AG )
|
|
|
- // Except if other than default BlackPoint values are used.
|
|
|
- var L = cs.YW * AG;
|
|
|
- // http://www.poynton.com/notes/colour_and_gamma/ColorFAQ.html, Ch 4.
|
|
|
- // Convert values to rgb range [0, 255].
|
|
|
- var val = Math.max(295.8 * Math.pow(L, 0.333333333333333333) - 40.8, 0) | 0;
|
|
|
- dest[destOffset] = val;
|
|
|
- dest[destOffset + 1] = val;
|
|
|
- dest[destOffset + 2] = val;
|
|
|
- }
|
|
|
+ if (sw !== width || sh !== height) {
|
|
|
+ alphaBuf = PDFImage.resize(alphaBuf, mask.bpc, 1, sw, sh, width,
|
|
|
+ height);
|
|
|
+ }
|
|
|
+ } else if (isArray(mask)) {
|
|
|
+ // Color key mask: if any of the compontents are outside the range
|
|
|
+ // then they should be painted.
|
|
|
+ alphaBuf = new Uint8Array(width * height);
|
|
|
+ var numComps = this.numComps;
|
|
|
+ for (i = 0, ii = width * height; i < ii; ++i) {
|
|
|
+ var opacity = 0;
|
|
|
+ var imageOffset = i * numComps;
|
|
|
+ for (j = 0; j < numComps; ++j) {
|
|
|
+ var color = image[imageOffset + j];
|
|
|
+ var maskOffset = j * 2;
|
|
|
+ if (color < mask[maskOffset] || color > mask[maskOffset + 1]) {
|
|
|
+ opacity = 255;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ alphaBuf[i] = opacity;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ error('Unknown mask format.');
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- CalGrayCS.prototype = {
|
|
|
- getRgb: ColorSpace.prototype.getRgb,
|
|
|
- getRgbItem: function CalGrayCS_getRgbItem(src, srcOffset,
|
|
|
- dest, destOffset) {
|
|
|
- convertToRgb(this, src, srcOffset, dest, destOffset, 1);
|
|
|
+ if (alphaBuf) {
|
|
|
+ for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) {
|
|
|
+ rgbaBuf[j] = alphaBuf[i];
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // No mask.
|
|
|
+ for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) {
|
|
|
+ rgbaBuf[j] = 255;
|
|
|
+ }
|
|
|
+ }
|
|
|
},
|
|
|
- getRgbBuffer: function CalGrayCS_getRgbBuffer(src, srcOffset, count,
|
|
|
- dest, destOffset, bits,
|
|
|
- alpha01) {
|
|
|
- var scale = 1 / ((1 << bits) - 1);
|
|
|
|
|
|
- for (var i = 0; i < count; ++i) {
|
|
|
- convertToRgb(this, src, srcOffset, dest, destOffset, scale);
|
|
|
- srcOffset += 1;
|
|
|
- destOffset += 3 + alpha01;
|
|
|
+ undoPreblend: function PDFImage_undoPreblend(buffer, width, height) {
|
|
|
+ var matte = this.smask && this.smask.matte;
|
|
|
+ if (!matte) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ var matteRgb = this.colorSpace.getRgb(matte, 0);
|
|
|
+ var matteR = matteRgb[0];
|
|
|
+ var matteG = matteRgb[1];
|
|
|
+ var matteB = matteRgb[2];
|
|
|
+ var length = width * height * 4;
|
|
|
+ var r, g, b;
|
|
|
+ for (var i = 0; i < length; i += 4) {
|
|
|
+ var alpha = buffer[i + 3];
|
|
|
+ if (alpha === 0) {
|
|
|
+ // according formula we have to get Infinity in all components
|
|
|
+ // making it white (typical paper color) should be okay
|
|
|
+ buffer[i] = 255;
|
|
|
+ buffer[i + 1] = 255;
|
|
|
+ buffer[i + 2] = 255;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ var k = 255 / alpha;
|
|
|
+ r = (buffer[i] - matteR) * k + matteR;
|
|
|
+ g = (buffer[i + 1] - matteG) * k + matteG;
|
|
|
+ b = (buffer[i + 2] - matteB) * k + matteB;
|
|
|
+ buffer[i] = r <= 0 ? 0 : r >= 255 ? 255 : r | 0;
|
|
|
+ buffer[i + 1] = g <= 0 ? 0 : g >= 255 ? 255 : g | 0;
|
|
|
+ buffer[i + 2] = b <= 0 ? 0 : b >= 255 ? 255 : b | 0;
|
|
|
}
|
|
|
},
|
|
|
- getOutputLength: function CalGrayCS_getOutputLength(inputLength, alpha01) {
|
|
|
- return inputLength * (3 + alpha01);
|
|
|
- },
|
|
|
- isPassthrough: ColorSpace.prototype.isPassthrough,
|
|
|
- fillRgb: ColorSpace.prototype.fillRgb,
|
|
|
- isDefaultDecode: function CalGrayCS_isDefaultDecode(decodeMap) {
|
|
|
- return ColorSpace.isDefaultDecode(decodeMap, this.numComps);
|
|
|
- },
|
|
|
- usesZeroToOneRange: true
|
|
|
- };
|
|
|
- return CalGrayCS;
|
|
|
-})();
|
|
|
-
|
|
|
-//
|
|
|
-// CalRGBCS: Based on "PDF Reference, Sixth Ed", p.247
|
|
|
-//
|
|
|
-var CalRGBCS = (function CalRGBCSClosure() {
|
|
|
-
|
|
|
- // See http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html for these
|
|
|
- // matrices.
|
|
|
- var BRADFORD_SCALE_MATRIX = new Float32Array([
|
|
|
- 0.8951, 0.2664, -0.1614,
|
|
|
- -0.7502, 1.7135, 0.0367,
|
|
|
- 0.0389, -0.0685, 1.0296]);
|
|
|
-
|
|
|
- var BRADFORD_SCALE_INVERSE_MATRIX = new Float32Array([
|
|
|
- 0.9869929, -0.1470543, 0.1599627,
|
|
|
- 0.4323053, 0.5183603, 0.0492912,
|
|
|
- -0.0085287, 0.0400428, 0.9684867]);
|
|
|
-
|
|
|
- // See http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html.
|
|
|
- var SRGB_D65_XYZ_TO_RGB_MATRIX = new Float32Array([
|
|
|
- 3.2404542, -1.5371385, -0.4985314,
|
|
|
- -0.9692660, 1.8760108, 0.0415560,
|
|
|
- 0.0556434, -0.2040259, 1.0572252]);
|
|
|
|
|
|
- var FLAT_WHITEPOINT_MATRIX = new Float32Array([1, 1, 1]);
|
|
|
+ createImageData: function PDFImage_createImageData(forceRGBA) {
|
|
|
+ var drawWidth = this.drawWidth;
|
|
|
+ var drawHeight = this.drawHeight;
|
|
|
+ var imgData = { // other fields are filled in below
|
|
|
+ width: drawWidth,
|
|
|
+ height: drawHeight
|
|
|
+ };
|
|
|
|
|
|
- var tempNormalizeMatrix = new Float32Array(3);
|
|
|
- var tempConvertMatrix1 = new Float32Array(3);
|
|
|
- var tempConvertMatrix2 = new Float32Array(3);
|
|
|
+ var numComps = this.numComps;
|
|
|
+ var originalWidth = this.width;
|
|
|
+ var originalHeight = this.height;
|
|
|
+ var bpc = this.bpc;
|
|
|
|
|
|
- var DECODE_L_CONSTANT = Math.pow(((8 + 16) / 116), 3) / 8.0;
|
|
|
+ // Rows start at byte boundary.
|
|
|
+ var rowBytes = (originalWidth * numComps * bpc + 7) >> 3;
|
|
|
+ var imgArray;
|
|
|
|
|
|
- function CalRGBCS(whitePoint, blackPoint, gamma, matrix) {
|
|
|
- this.name = 'CalRGB';
|
|
|
- this.numComps = 3;
|
|
|
- this.defaultColor = new Float32Array(3);
|
|
|
+ if (!forceRGBA) {
|
|
|
+ // If it is a 1-bit-per-pixel grayscale (i.e. black-and-white) image
|
|
|
+ // without any complications, we pass a same-sized copy to the main
|
|
|
+ // thread rather than expanding by 32x to RGBA form. This saves *lots*
|
|
|
+ // of memory for many scanned documents. It's also much faster.
|
|
|
+ //
|
|
|
+ // Similarly, if it is a 24-bit-per pixel RGB image without any
|
|
|
+ // complications, we avoid expanding by 1.333x to RGBA form.
|
|
|
+ var kind;
|
|
|
+ if (this.colorSpace.name === 'DeviceGray' && bpc === 1) {
|
|
|
+ kind = ImageKind.GRAYSCALE_1BPP;
|
|
|
+ } else if (this.colorSpace.name === 'DeviceRGB' && bpc === 8 &&
|
|
|
+ !this.needsDecode) {
|
|
|
+ kind = ImageKind.RGB_24BPP;
|
|
|
+ }
|
|
|
+ if (kind && !this.smask && !this.mask &&
|
|
|
+ drawWidth === originalWidth && drawHeight === originalHeight) {
|
|
|
+ imgData.kind = kind;
|
|
|
|
|
|
- if (!whitePoint) {
|
|
|
- error('WhitePoint missing - required for color space CalRGB');
|
|
|
- }
|
|
|
- blackPoint = blackPoint || new Float32Array(3);
|
|
|
- gamma = gamma || new Float32Array([1, 1, 1]);
|
|
|
- matrix = matrix || new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
|
+ imgArray = this.getImageBytes(originalHeight * rowBytes);
|
|
|
+ // If imgArray came from a DecodeStream, we're safe to transfer it
|
|
|
+ // (and thus detach its underlying buffer) because it will constitute
|
|
|
+ // the entire DecodeStream's data. But if it came from a Stream, we
|
|
|
+ // need to copy it because it'll only be a portion of the Stream's
|
|
|
+ // data, and the rest will be read later on.
|
|
|
+ if (this.image instanceof DecodeStream) {
|
|
|
+ imgData.data = imgArray;
|
|
|
+ } else {
|
|
|
+ var newArray = new Uint8Array(imgArray.length);
|
|
|
+ newArray.set(imgArray);
|
|
|
+ imgData.data = newArray;
|
|
|
+ }
|
|
|
+ if (this.needsDecode) {
|
|
|
+ // Invert the buffer (which must be grayscale if we reached here).
|
|
|
+ assert(kind === ImageKind.GRAYSCALE_1BPP);
|
|
|
+ var buffer = imgData.data;
|
|
|
+ for (var i = 0, ii = buffer.length; i < ii; i++) {
|
|
|
+ buffer[i] ^= 0xff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return imgData;
|
|
|
+ }
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // Translate arguments to spec variables.
|
|
|
- var XW = whitePoint[0];
|
|
|
- var YW = whitePoint[1];
|
|
|
- var ZW = whitePoint[2];
|
|
|
- this.whitePoint = whitePoint;
|
|
|
+ imgArray = this.getImageBytes(originalHeight * rowBytes);
|
|
|
+ // imgArray can be incomplete (e.g. after CCITT fax encoding).
|
|
|
+ var actualHeight = 0 | (imgArray.length / rowBytes *
|
|
|
+ drawHeight / originalHeight);
|
|
|
|
|
|
- var XB = blackPoint[0];
|
|
|
- var YB = blackPoint[1];
|
|
|
- var ZB = blackPoint[2];
|
|
|
- this.blackPoint = blackPoint;
|
|
|
+ var comps = this.getComponents(imgArray);
|
|
|
|
|
|
- this.GR = gamma[0];
|
|
|
- this.GG = gamma[1];
|
|
|
- this.GB = gamma[2];
|
|
|
+ // If opacity data is present, use RGBA_32BPP form. Otherwise, use the
|
|
|
+ // more compact RGB_24BPP form if allowable.
|
|
|
+ var alpha01, maybeUndoPreblend;
|
|
|
+ if (!forceRGBA && !this.smask && !this.mask) {
|
|
|
+ imgData.kind = ImageKind.RGB_24BPP;
|
|
|
+ imgData.data = new Uint8Array(drawWidth * drawHeight * 3);
|
|
|
+ alpha01 = 0;
|
|
|
+ maybeUndoPreblend = false;
|
|
|
+ } else {
|
|
|
+ imgData.kind = ImageKind.RGBA_32BPP;
|
|
|
+ imgData.data = new Uint8Array(drawWidth * drawHeight * 4);
|
|
|
+ alpha01 = 1;
|
|
|
+ maybeUndoPreblend = true;
|
|
|
|
|
|
- this.MXA = matrix[0];
|
|
|
- this.MYA = matrix[1];
|
|
|
- this.MZA = matrix[2];
|
|
|
- this.MXB = matrix[3];
|
|
|
- this.MYB = matrix[4];
|
|
|
- this.MZB = matrix[5];
|
|
|
- this.MXC = matrix[6];
|
|
|
- this.MYC = matrix[7];
|
|
|
- this.MZC = matrix[8];
|
|
|
+ // Color key masking (opacity) must be performed before decoding.
|
|
|
+ this.fillOpacity(imgData.data, drawWidth, drawHeight, actualHeight,
|
|
|
+ comps);
|
|
|
+ }
|
|
|
|
|
|
- // Validate variables as per spec.
|
|
|
- if (XW < 0 || ZW < 0 || YW !== 1) {
|
|
|
- error('Invalid WhitePoint components for ' + this.name +
|
|
|
- ', no fallback available');
|
|
|
- }
|
|
|
+ if (this.needsDecode) {
|
|
|
+ this.decodeBuffer(comps);
|
|
|
+ }
|
|
|
+ this.colorSpace.fillRgb(imgData.data, originalWidth, originalHeight,
|
|
|
+ drawWidth, drawHeight, actualHeight, bpc, comps,
|
|
|
+ alpha01);
|
|
|
+ if (maybeUndoPreblend) {
|
|
|
+ this.undoPreblend(imgData.data, drawWidth, actualHeight);
|
|
|
+ }
|
|
|
|
|
|
- if (XB < 0 || YB < 0 || ZB < 0) {
|
|
|
- info('Invalid BlackPoint for ' + this.name + ' [' + XB + ', ' + YB +
|
|
|
- ', ' + ZB + '], falling back to default');
|
|
|
- this.blackPoint = new Float32Array(3);
|
|
|
- }
|
|
|
+ return imgData;
|
|
|
+ },
|
|
|
|
|
|
- if (this.GR < 0 || this.GG < 0 || this.GB < 0) {
|
|
|
- info('Invalid Gamma [' + this.GR + ', ' + this.GG + ', ' + this.GB +
|
|
|
- '] for ' + this.name + ', falling back to default');
|
|
|
- this.GR = this.GG = this.GB = 1;
|
|
|
- }
|
|
|
+ fillGrayBuffer: function PDFImage_fillGrayBuffer(buffer) {
|
|
|
+ var numComps = this.numComps;
|
|
|
+ if (numComps !== 1) {
|
|
|
+ error('Reading gray scale from a color image: ' + numComps);
|
|
|
+ }
|
|
|
|
|
|
- if (this.MXA < 0 || this.MYA < 0 || this.MZA < 0 ||
|
|
|
- this.MXB < 0 || this.MYB < 0 || this.MZB < 0 ||
|
|
|
- this.MXC < 0 || this.MYC < 0 || this.MZC < 0) {
|
|
|
- info('Invalid Matrix for ' + this.name + ' [' +
|
|
|
- this.MXA + ', ' + this.MYA + ', ' + this.MZA +
|
|
|
- this.MXB + ', ' + this.MYB + ', ' + this.MZB +
|
|
|
- this.MXC + ', ' + this.MYC + ', ' + this.MZC +
|
|
|
- '], falling back to default');
|
|
|
- this.MXA = this.MYB = this.MZC = 1;
|
|
|
- this.MXB = this.MYA = this.MZA = this.MXC = this.MYC = this.MZB = 0;
|
|
|
- }
|
|
|
- }
|
|
|
+ var width = this.width;
|
|
|
+ var height = this.height;
|
|
|
+ var bpc = this.bpc;
|
|
|
|
|
|
- function matrixProduct(a, b, result) {
|
|
|
- result[0] = a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
|
- result[1] = a[3] * b[0] + a[4] * b[1] + a[5] * b[2];
|
|
|
- result[2] = a[6] * b[0] + a[7] * b[1] + a[8] * b[2];
|
|
|
- }
|
|
|
+ // rows start at byte boundary
|
|
|
+ var rowBytes = (width * numComps * bpc + 7) >> 3;
|
|
|
+ var imgArray = this.getImageBytes(height * rowBytes);
|
|
|
|
|
|
- function convertToFlat(sourceWhitePoint, LMS, result) {
|
|
|
- result[0] = LMS[0] * 1 / sourceWhitePoint[0];
|
|
|
- result[1] = LMS[1] * 1 / sourceWhitePoint[1];
|
|
|
- result[2] = LMS[2] * 1 / sourceWhitePoint[2];
|
|
|
- }
|
|
|
+ var comps = this.getComponents(imgArray);
|
|
|
+ var i, length;
|
|
|
|
|
|
- function convertToD65(sourceWhitePoint, LMS, result) {
|
|
|
- var D65X = 0.95047;
|
|
|
- var D65Y = 1;
|
|
|
- var D65Z = 1.08883;
|
|
|
+ if (bpc === 1) {
|
|
|
+ // inline decoding (= inversion) for 1 bpc images
|
|
|
+ length = width * height;
|
|
|
+ if (this.needsDecode) {
|
|
|
+ // invert and scale to {0, 255}
|
|
|
+ for (i = 0; i < length; ++i) {
|
|
|
+ buffer[i] = (comps[i] - 1) & 255;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // scale to {0, 255}
|
|
|
+ for (i = 0; i < length; ++i) {
|
|
|
+ buffer[i] = (-comps[i]) & 255;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- result[0] = LMS[0] * D65X / sourceWhitePoint[0];
|
|
|
- result[1] = LMS[1] * D65Y / sourceWhitePoint[1];
|
|
|
- result[2] = LMS[2] * D65Z / sourceWhitePoint[2];
|
|
|
- }
|
|
|
+ if (this.needsDecode) {
|
|
|
+ this.decodeBuffer(comps);
|
|
|
+ }
|
|
|
+ length = width * height;
|
|
|
+ // we aren't using a colorspace so we need to scale the value
|
|
|
+ var scale = 255 / ((1 << bpc) - 1);
|
|
|
+ for (i = 0; i < length; ++i) {
|
|
|
+ buffer[i] = (scale * comps[i]) | 0;
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- function sRGBTransferFunction(color) {
|
|
|
- // See http://en.wikipedia.org/wiki/SRGB.
|
|
|
- if (color <= 0.0031308){
|
|
|
- return adjustToRange(0, 1, 12.92 * color);
|
|
|
+ getImageBytes: function PDFImage_getImageBytes(length,
|
|
|
+ drawWidth, drawHeight,
|
|
|
+ forceRGB) {
|
|
|
+ this.image.reset();
|
|
|
+ this.image.drawWidth = drawWidth || this.width;
|
|
|
+ this.image.drawHeight = drawHeight || this.height;
|
|
|
+ this.image.forceRGB = !!forceRGB;
|
|
|
+ return this.image.getBytes(length);
|
|
|
}
|
|
|
+ };
|
|
|
+ return PDFImage;
|
|
|
+})();
|
|
|
|
|
|
- return adjustToRange(0, 1, (1 + 0.055) * Math.pow(color, 1 / 2.4) - 0.055);
|
|
|
- }
|
|
|
-
|
|
|
- function adjustToRange(min, max, value) {
|
|
|
- return Math.max(min, Math.min(max, value));
|
|
|
- }
|
|
|
+exports.PDFImage = PDFImage;
|
|
|
|
|
|
- function decodeL(L) {
|
|
|
- if (L < 0) {
|
|
|
- return -decodeL(-L);
|
|
|
- }
|
|
|
+// TODO refactor to remove dependency on colorspace.js
|
|
|
+coreColorSpace._setCoreImage(exports);
|
|
|
+}));
|
|
|
|
|
|
- if (L > 8.0) {
|
|
|
- return Math.pow(((L + 16) / 116), 3);
|
|
|
- }
|
|
|
|
|
|
- return L * DECODE_L_CONSTANT;
|
|
|
+(function (root, factory) {
|
|
|
+ {
|
|
|
+ factory((root.pdfjsCoreObj = {}), root.pdfjsSharedUtil,
|
|
|
+ root.pdfjsCorePrimitives, root.pdfjsCoreCrypto, root.pdfjsCoreParser,
|
|
|
+ root.pdfjsCoreChunkedStream, root.pdfjsCoreColorSpace);
|
|
|
}
|
|
|
+}(this, function (exports, sharedUtil, corePrimitives, coreCrypto, coreParser,
|
|
|
+ coreChunkedStream, coreColorSpace) {
|
|
|
|
|
|
- function compensateBlackPoint(sourceBlackPoint, XYZ_Flat, result) {
|
|
|
-
|
|
|
- // In case the blackPoint is already the default blackPoint then there is
|
|
|
- // no need to do compensation.
|
|
|
- if (sourceBlackPoint[0] === 0 &&
|
|
|
- sourceBlackPoint[1] === 0 &&
|
|
|
- sourceBlackPoint[2] === 0) {
|
|
|
- result[0] = XYZ_Flat[0];
|
|
|
- result[1] = XYZ_Flat[1];
|
|
|
- result[2] = XYZ_Flat[2];
|
|
|
- return;
|
|
|
- }
|
|
|
+var InvalidPDFException = sharedUtil.InvalidPDFException;
|
|
|
+var MissingDataException = sharedUtil.MissingDataException;
|
|
|
+var XRefParseException = sharedUtil.XRefParseException;
|
|
|
+var assert = sharedUtil.assert;
|
|
|
+var bytesToString = sharedUtil.bytesToString;
|
|
|
+var createPromiseCapability = sharedUtil.createPromiseCapability;
|
|
|
+var error = sharedUtil.error;
|
|
|
+var info = sharedUtil.info;
|
|
|
+var isArray = sharedUtil.isArray;
|
|
|
+var isInt = sharedUtil.isInt;
|
|
|
+var isString = sharedUtil.isString;
|
|
|
+var shadow = sharedUtil.shadow;
|
|
|
+var stringToPDFString = sharedUtil.stringToPDFString;
|
|
|
+var stringToUTF8String = sharedUtil.stringToUTF8String;
|
|
|
+var warn = sharedUtil.warn;
|
|
|
+var isValidUrl = sharedUtil.isValidUrl;
|
|
|
+var Util = sharedUtil.Util;
|
|
|
+var Ref = corePrimitives.Ref;
|
|
|
+var RefSet = corePrimitives.RefSet;
|
|
|
+var RefSetCache = corePrimitives.RefSetCache;
|
|
|
+var isName = corePrimitives.isName;
|
|
|
+var isCmd = corePrimitives.isCmd;
|
|
|
+var isDict = corePrimitives.isDict;
|
|
|
+var isRef = corePrimitives.isRef;
|
|
|
+var isStream = corePrimitives.isStream;
|
|
|
+var CipherTransformFactory = coreCrypto.CipherTransformFactory;
|
|
|
+var Lexer = coreParser.Lexer;
|
|
|
+var Parser = coreParser.Parser;
|
|
|
+var ChunkedStream = coreChunkedStream.ChunkedStream;
|
|
|
+var ColorSpace = coreColorSpace.ColorSpace;
|
|
|
|
|
|
- // For the blackPoint calculation details, please see
|
|
|
- // http://www.adobe.com/content/dam/Adobe/en/devnet/photoshop/sdk/
|
|
|
- // AdobeBPC.pdf.
|
|
|
- // The destination blackPoint is the default blackPoint [0, 0, 0].
|
|
|
- var zeroDecodeL = decodeL(0);
|
|
|
+var Catalog = (function CatalogClosure() {
|
|
|
+ function Catalog(pdfManager, xref, pageFactory) {
|
|
|
+ this.pdfManager = pdfManager;
|
|
|
+ this.xref = xref;
|
|
|
+ this.catDict = xref.getCatalogObj();
|
|
|
+ this.fontCache = new RefSetCache();
|
|
|
+ assert(isDict(this.catDict),
|
|
|
+ 'catalog object is not a dictionary');
|
|
|
|
|
|
- var X_DST = zeroDecodeL;
|
|
|
- var X_SRC = decodeL(sourceBlackPoint[0]);
|
|
|
+ // TODO refactor to move getPage() to the PDFDocument.
|
|
|
+ this.pageFactory = pageFactory;
|
|
|
+ this.pagePromises = [];
|
|
|
+ }
|
|
|
|
|
|
- var Y_DST = zeroDecodeL;
|
|
|
- var Y_SRC = decodeL(sourceBlackPoint[1]);
|
|
|
+ Catalog.prototype = {
|
|
|
+ get metadata() {
|
|
|
+ var streamRef = this.catDict.getRaw('Metadata');
|
|
|
+ if (!isRef(streamRef)) {
|
|
|
+ return shadow(this, 'metadata', null);
|
|
|
+ }
|
|
|
|
|
|
- var Z_DST = zeroDecodeL;
|
|
|
- var Z_SRC = decodeL(sourceBlackPoint[2]);
|
|
|
+ var encryptMetadata = (!this.xref.encrypt ? false :
|
|
|
+ this.xref.encrypt.encryptMetadata);
|
|
|
|
|
|
- var X_Scale = (1 - X_DST) / (1 - X_SRC);
|
|
|
- var X_Offset = 1 - X_Scale;
|
|
|
+ var stream = this.xref.fetch(streamRef, !encryptMetadata);
|
|
|
+ var metadata;
|
|
|
+ if (stream && isDict(stream.dict)) {
|
|
|
+ var type = stream.dict.get('Type');
|
|
|
+ var subtype = stream.dict.get('Subtype');
|
|
|
|
|
|
- var Y_Scale = (1 - Y_DST) / (1 - Y_SRC);
|
|
|
- var Y_Offset = 1 - Y_Scale;
|
|
|
+ if (isName(type) && isName(subtype) &&
|
|
|
+ type.name === 'Metadata' && subtype.name === 'XML') {
|
|
|
+ // XXX: This should examine the charset the XML document defines,
|
|
|
+ // however since there are currently no real means to decode
|
|
|
+ // arbitrary charsets, let's just hope that the author of the PDF
|
|
|
+ // was reasonable enough to stick with the XML default charset,
|
|
|
+ // which is UTF-8.
|
|
|
+ try {
|
|
|
+ metadata = stringToUTF8String(bytesToString(stream.getBytes()));
|
|
|
+ } catch (e) {
|
|
|
+ info('Skipping invalid metadata.');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- var Z_Scale = (1 - Z_DST) / (1 - Z_SRC);
|
|
|
- var Z_Offset = 1 - Z_Scale;
|
|
|
+ return shadow(this, 'metadata', metadata);
|
|
|
+ },
|
|
|
+ get toplevelPagesDict() {
|
|
|
+ var pagesObj = this.catDict.get('Pages');
|
|
|
+ assert(isDict(pagesObj), 'invalid top-level pages dictionary');
|
|
|
+ // shadow the prototype getter
|
|
|
+ return shadow(this, 'toplevelPagesDict', pagesObj);
|
|
|
+ },
|
|
|
+ get documentOutline() {
|
|
|
+ var obj = null;
|
|
|
+ try {
|
|
|
+ obj = this.readDocumentOutline();
|
|
|
+ } catch (ex) {
|
|
|
+ if (ex instanceof MissingDataException) {
|
|
|
+ throw ex;
|
|
|
+ }
|
|
|
+ warn('Unable to read document outline');
|
|
|
+ }
|
|
|
+ return shadow(this, 'documentOutline', obj);
|
|
|
+ },
|
|
|
+ readDocumentOutline: function Catalog_readDocumentOutline() {
|
|
|
+ var obj = this.catDict.get('Outlines');
|
|
|
+ if (!isDict(obj)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ obj = obj.getRaw('First');
|
|
|
+ if (!isRef(obj)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ var root = { items: [] };
|
|
|
+ var queue = [{obj: obj, parent: root}];
|
|
|
+ // To avoid recursion, keep track of the already processed items.
|
|
|
+ var processed = new RefSet();
|
|
|
+ processed.put(obj);
|
|
|
+ var xref = this.xref, blackColor = new Uint8Array(3);
|
|
|
|
|
|
- result[0] = XYZ_Flat[0] * X_Scale + X_Offset;
|
|
|
- result[1] = XYZ_Flat[1] * Y_Scale + Y_Offset;
|
|
|
- result[2] = XYZ_Flat[2] * Z_Scale + Z_Offset;
|
|
|
- }
|
|
|
+ while (queue.length > 0) {
|
|
|
+ var i = queue.shift();
|
|
|
+ var outlineDict = xref.fetchIfRef(i.obj);
|
|
|
+ if (outlineDict === null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ assert(outlineDict.has('Title'), 'Invalid outline item');
|
|
|
|
|
|
- function normalizeWhitePointToFlat(sourceWhitePoint, XYZ_In, result) {
|
|
|
+ var actionDict = outlineDict.get('A'), dest = null, url = null;
|
|
|
+ if (actionDict) {
|
|
|
+ var destEntry = actionDict.get('D');
|
|
|
+ if (destEntry) {
|
|
|
+ dest = destEntry;
|
|
|
+ } else {
|
|
|
+ var uriEntry = actionDict.get('URI');
|
|
|
+ if (isString(uriEntry) && isValidUrl(uriEntry, false)) {
|
|
|
+ url = uriEntry;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (outlineDict.has('Dest')) {
|
|
|
+ dest = outlineDict.getRaw('Dest');
|
|
|
+ if (isName(dest)) {
|
|
|
+ dest = dest.name;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ var title = outlineDict.get('Title');
|
|
|
+ var flags = outlineDict.get('F') || 0;
|
|
|
+
|
|
|
+ var color = outlineDict.get('C'), rgbColor = blackColor;
|
|
|
+ // We only need to parse the color when it's valid, and non-default.
|
|
|
+ if (isArray(color) && color.length === 3 &&
|
|
|
+ (color[0] !== 0 || color[1] !== 0 || color[2] !== 0)) {
|
|
|
+ rgbColor = ColorSpace.singletons.rgb.getRgb(color, 0);
|
|
|
+ }
|
|
|
+ var outlineItem = {
|
|
|
+ dest: dest,
|
|
|
+ url: url,
|
|
|
+ title: stringToPDFString(title),
|
|
|
+ color: rgbColor,
|
|
|
+ count: outlineDict.get('Count'),
|
|
|
+ bold: !!(flags & 2),
|
|
|
+ italic: !!(flags & 1),
|
|
|
+ items: []
|
|
|
+ };
|
|
|
+ i.parent.items.push(outlineItem);
|
|
|
+ obj = outlineDict.getRaw('First');
|
|
|
+ if (isRef(obj) && !processed.has(obj)) {
|
|
|
+ queue.push({obj: obj, parent: outlineItem});
|
|
|
+ processed.put(obj);
|
|
|
+ }
|
|
|
+ obj = outlineDict.getRaw('Next');
|
|
|
+ if (isRef(obj) && !processed.has(obj)) {
|
|
|
+ queue.push({obj: obj, parent: i.parent});
|
|
|
+ processed.put(obj);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return (root.items.length > 0 ? root.items : null);
|
|
|
+ },
|
|
|
+ get numPages() {
|
|
|
+ var obj = this.toplevelPagesDict.get('Count');
|
|
|
+ assert(
|
|
|
+ isInt(obj),
|
|
|
+ 'page count in top level pages object is not an integer'
|
|
|
+ );
|
|
|
+ // shadow the prototype getter
|
|
|
+ return shadow(this, 'num', obj);
|
|
|
+ },
|
|
|
+ get destinations() {
|
|
|
+ function fetchDestination(dest) {
|
|
|
+ return isDict(dest) ? dest.get('D') : dest;
|
|
|
+ }
|
|
|
|
|
|
- // In case the whitePoint is already flat then there is no need to do
|
|
|
- // normalization.
|
|
|
- if (sourceWhitePoint[0] === 1 && sourceWhitePoint[2] === 1) {
|
|
|
- result[0] = XYZ_In[0];
|
|
|
- result[1] = XYZ_In[1];
|
|
|
- result[2] = XYZ_In[2];
|
|
|
- return;
|
|
|
- }
|
|
|
+ var xref = this.xref;
|
|
|
+ var dests = {}, 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');
|
|
|
+ }
|
|
|
|
|
|
- var LMS = result;
|
|
|
- matrixProduct(BRADFORD_SCALE_MATRIX, XYZ_In, LMS);
|
|
|
+ if (nameDictionaryRef) {
|
|
|
+ // reading simple destination dictionary
|
|
|
+ obj = nameDictionaryRef;
|
|
|
+ obj.forEach(function catalogForEach(key, value) {
|
|
|
+ if (!value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ dests[key] = fetchDestination(value);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (nameTreeRef) {
|
|
|
+ var nameTree = new NameTree(nameTreeRef, xref);
|
|
|
+ var names = nameTree.getAll();
|
|
|
+ for (var name in names) {
|
|
|
+ dests[name] = fetchDestination(names[name]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return shadow(this, 'destinations', dests);
|
|
|
+ },
|
|
|
+ getDestination: function Catalog_getDestination(destinationId) {
|
|
|
+ function fetchDestination(dest) {
|
|
|
+ return isDict(dest) ? dest.get('D') : dest;
|
|
|
+ }
|
|
|
|
|
|
- var LMS_Flat = tempNormalizeMatrix;
|
|
|
- convertToFlat(sourceWhitePoint, LMS, LMS_Flat);
|
|
|
+ var xref = this.xref;
|
|
|
+ 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');
|
|
|
+ }
|
|
|
|
|
|
- matrixProduct(BRADFORD_SCALE_INVERSE_MATRIX, LMS_Flat, result);
|
|
|
- }
|
|
|
+ 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));
|
|
|
+ }
|
|
|
+ return dest;
|
|
|
+ },
|
|
|
|
|
|
- function normalizeWhitePointToD65(sourceWhitePoint, XYZ_In, result) {
|
|
|
+ get pageLabels() {
|
|
|
+ var obj = null;
|
|
|
+ try {
|
|
|
+ obj = this.readPageLabels();
|
|
|
+ } catch (ex) {
|
|
|
+ if (ex instanceof MissingDataException) {
|
|
|
+ throw ex;
|
|
|
+ }
|
|
|
+ warn('Unable to read page labels.');
|
|
|
+ }
|
|
|
+ return shadow(this, 'pageLabels', obj);
|
|
|
+ },
|
|
|
+ readPageLabels: function Catalog_readPageLabels() {
|
|
|
+ var obj = this.catDict.getRaw('PageLabels');
|
|
|
+ if (!obj) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ var pageLabels = new Array(this.numPages);
|
|
|
+ var style = null;
|
|
|
+ var prefix = '';
|
|
|
+ var start = 1;
|
|
|
|
|
|
- var LMS = result;
|
|
|
- matrixProduct(BRADFORD_SCALE_MATRIX, XYZ_In, LMS);
|
|
|
+ var numberTree = new NumberTree(obj, this.xref);
|
|
|
+ var nums = numberTree.getAll();
|
|
|
+ var currentLabel = '', currentIndex = 1;
|
|
|
|
|
|
- var LMS_D65 = tempNormalizeMatrix;
|
|
|
- convertToD65(sourceWhitePoint, LMS, LMS_D65);
|
|
|
+ for (var i = 0, ii = this.numPages; i < ii; i++) {
|
|
|
+ if (i in nums) {
|
|
|
+ var labelDict = nums[i];
|
|
|
+ assert(isDict(labelDict), 'The PageLabel is not a dictionary.');
|
|
|
|
|
|
- matrixProduct(BRADFORD_SCALE_INVERSE_MATRIX, LMS_D65, result);
|
|
|
- }
|
|
|
+ var type = labelDict.get('Type');
|
|
|
+ assert(!type || (isName(type) && type.name === 'PageLabel'),
|
|
|
+ 'Invalid type in PageLabel dictionary.');
|
|
|
|
|
|
- function convertToRgb(cs, src, srcOffset, dest, destOffset, scale) {
|
|
|
- // A, B and C represent a red, green and blue components of a calibrated
|
|
|
- // rgb space.
|
|
|
- var A = adjustToRange(0, 1, src[srcOffset] * scale);
|
|
|
- var B = adjustToRange(0, 1, src[srcOffset + 1] * scale);
|
|
|
- var C = adjustToRange(0, 1, src[srcOffset + 2] * scale);
|
|
|
+ var s = labelDict.get('S');
|
|
|
+ assert(!s || isName(s), 'Invalid style in PageLabel dictionary.');
|
|
|
+ style = (s ? s.name : null);
|
|
|
|
|
|
- // A <---> AGR in the spec
|
|
|
- // B <---> BGG in the spec
|
|
|
- // C <---> CGB in the spec
|
|
|
- var AGR = Math.pow(A, cs.GR);
|
|
|
- var BGG = Math.pow(B, cs.GG);
|
|
|
- var CGB = Math.pow(C, cs.GB);
|
|
|
+ prefix = labelDict.get('P') || '';
|
|
|
+ assert(isString(prefix), 'Invalid prefix in PageLabel dictionary.');
|
|
|
|
|
|
- // Computes intermediate variables L, M, N as per spec.
|
|
|
- // To decode X, Y, Z values map L, M, N directly to them.
|
|
|
- var X = cs.MXA * AGR + cs.MXB * BGG + cs.MXC * CGB;
|
|
|
- var Y = cs.MYA * AGR + cs.MYB * BGG + cs.MYC * CGB;
|
|
|
- var Z = cs.MZA * AGR + cs.MZB * BGG + cs.MZC * CGB;
|
|
|
+ start = labelDict.get('St') || 1;
|
|
|
+ assert(isInt(start), 'Invalid start in PageLabel dictionary.');
|
|
|
+ currentIndex = start;
|
|
|
+ }
|
|
|
|
|
|
- // The following calculations are based on this document:
|
|
|
- // http://www.adobe.com/content/dam/Adobe/en/devnet/photoshop/sdk/
|
|
|
- // AdobeBPC.pdf.
|
|
|
- var XYZ = tempConvertMatrix1;
|
|
|
- XYZ[0] = X;
|
|
|
- XYZ[1] = Y;
|
|
|
- XYZ[2] = Z;
|
|
|
- var XYZ_Flat = tempConvertMatrix2;
|
|
|
+ switch (style) {
|
|
|
+ case 'D':
|
|
|
+ currentLabel = currentIndex;
|
|
|
+ break;
|
|
|
+ case 'R':
|
|
|
+ case 'r':
|
|
|
+ currentLabel = Util.toRoman(currentIndex, style === 'r');
|
|
|
+ break;
|
|
|
+ case 'A':
|
|
|
+ case 'a':
|
|
|
+ var LIMIT = 26; // Use only the characters A--Z, or a--z.
|
|
|
+ var A_UPPER_CASE = 0x41, A_LOWER_CASE = 0x61;
|
|
|
|
|
|
- normalizeWhitePointToFlat(cs.whitePoint, XYZ, XYZ_Flat);
|
|
|
+ var baseCharCode = (style === 'a' ? A_LOWER_CASE : A_UPPER_CASE);
|
|
|
+ var letterIndex = currentIndex - 1;
|
|
|
+ var character = String.fromCharCode(baseCharCode +
|
|
|
+ (letterIndex % LIMIT));
|
|
|
+ var charBuf = [];
|
|
|
+ for (var j = 0, jj = (letterIndex / LIMIT) | 0; j <= jj; j++) {
|
|
|
+ charBuf.push(character);
|
|
|
+ }
|
|
|
+ currentLabel = charBuf.join('');
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ assert(!style,
|
|
|
+ 'Invalid style "' + style + '" in PageLabel dictionary.');
|
|
|
+ }
|
|
|
+ pageLabels[i] = prefix + currentLabel;
|
|
|
|
|
|
- var XYZ_Black = tempConvertMatrix1;
|
|
|
- compensateBlackPoint(cs.blackPoint, XYZ_Flat, XYZ_Black);
|
|
|
+ currentLabel = '';
|
|
|
+ currentIndex++;
|
|
|
+ }
|
|
|
+ return pageLabels;
|
|
|
+ },
|
|
|
|
|
|
- var XYZ_D65 = tempConvertMatrix2;
|
|
|
- normalizeWhitePointToD65(FLAT_WHITEPOINT_MATRIX, XYZ_Black, XYZ_D65);
|
|
|
+ get attachments() {
|
|
|
+ var xref = this.xref;
|
|
|
+ var attachments = null, nameTreeRef;
|
|
|
+ var obj = this.catDict.get('Names');
|
|
|
+ if (obj) {
|
|
|
+ nameTreeRef = obj.getRaw('EmbeddedFiles');
|
|
|
+ }
|
|
|
|
|
|
- var SRGB = tempConvertMatrix1;
|
|
|
- matrixProduct(SRGB_D65_XYZ_TO_RGB_MATRIX, XYZ_D65, SRGB);
|
|
|
+ if (nameTreeRef) {
|
|
|
+ var nameTree = new NameTree(nameTreeRef, xref);
|
|
|
+ var names = nameTree.getAll();
|
|
|
+ for (var name in names) {
|
|
|
+ var fs = new FileSpec(names[name], xref);
|
|
|
+ if (!attachments) {
|
|
|
+ attachments = Object.create(null);
|
|
|
+ }
|
|
|
+ attachments[stringToPDFString(name)] = fs.serializable;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return shadow(this, 'attachments', attachments);
|
|
|
+ },
|
|
|
+ get javaScript() {
|
|
|
+ var xref = this.xref;
|
|
|
+ var obj = this.catDict.get('Names');
|
|
|
|
|
|
- var sR = sRGBTransferFunction(SRGB[0]);
|
|
|
- var sG = sRGBTransferFunction(SRGB[1]);
|
|
|
- var sB = sRGBTransferFunction(SRGB[2]);
|
|
|
+ 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) {
|
|
|
+ // 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)) {
|
|
|
+ appendIfJavaScriptDict(jsDict);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // Convert the values to rgb range [0, 255].
|
|
|
- dest[destOffset] = Math.round(sR * 255);
|
|
|
- dest[destOffset + 1] = Math.round(sG * 255);
|
|
|
- dest[destOffset + 2] = Math.round(sB * 255);
|
|
|
- }
|
|
|
+ // Append OpenAction actions to javaScript array
|
|
|
+ var openactionDict = this.catDict.get('OpenAction');
|
|
|
+ if (isDict(openactionDict, 'Action')) {
|
|
|
+ var actionType = openactionDict.get('S');
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- CalRGBCS.prototype = {
|
|
|
- getRgb: function CalRGBCS_getRgb(src, srcOffset) {
|
|
|
- var rgb = new Uint8Array(3);
|
|
|
- this.getRgbItem(src, srcOffset, rgb, 0);
|
|
|
- return rgb;
|
|
|
+ return shadow(this, 'javaScript', javaScript);
|
|
|
},
|
|
|
- getRgbItem: function CalRGBCS_getRgbItem(src, srcOffset,
|
|
|
- dest, destOffset) {
|
|
|
- convertToRgb(this, src, srcOffset, dest, destOffset, 1);
|
|
|
+
|
|
|
+ cleanup: function Catalog_cleanup() {
|
|
|
+ var promises = [];
|
|
|
+ this.fontCache.forEach(function (promise) {
|
|
|
+ promises.push(promise);
|
|
|
+ });
|
|
|
+ return Promise.all(promises).then(function (translatedFonts) {
|
|
|
+ for (var i = 0, ii = translatedFonts.length; i < ii; i++) {
|
|
|
+ var font = translatedFonts[i].dict;
|
|
|
+ delete font.translated;
|
|
|
+ }
|
|
|
+ this.fontCache.clear();
|
|
|
+ }.bind(this));
|
|
|
},
|
|
|
- getRgbBuffer: function CalRGBCS_getRgbBuffer(src, srcOffset, count,
|
|
|
- dest, destOffset, bits,
|
|
|
- alpha01) {
|
|
|
- var scale = 1 / ((1 << bits) - 1);
|
|
|
|
|
|
- for (var i = 0; i < count; ++i) {
|
|
|
- convertToRgb(this, src, srcOffset, dest, destOffset, scale);
|
|
|
- srcOffset += 3;
|
|
|
- destOffset += 3 + alpha01;
|
|
|
+ getPage: function Catalog_getPage(pageIndex) {
|
|
|
+ if (!(pageIndex in this.pagePromises)) {
|
|
|
+ this.pagePromises[pageIndex] = this.getPageDict(pageIndex).then(
|
|
|
+ function (a) {
|
|
|
+ var dict = a[0];
|
|
|
+ var ref = a[1];
|
|
|
+ return this.pageFactory.createPage(pageIndex, dict, ref,
|
|
|
+ this.fontCache);
|
|
|
+ }.bind(this)
|
|
|
+ );
|
|
|
}
|
|
|
+ return this.pagePromises[pageIndex];
|
|
|
},
|
|
|
- getOutputLength: function CalRGBCS_getOutputLength(inputLength, alpha01) {
|
|
|
- return (inputLength * (3 + alpha01) / 3) | 0;
|
|
|
- },
|
|
|
- isPassthrough: ColorSpace.prototype.isPassthrough,
|
|
|
- fillRgb: ColorSpace.prototype.fillRgb,
|
|
|
- isDefaultDecode: function CalRGBCS_isDefaultDecode(decodeMap) {
|
|
|
- return ColorSpace.isDefaultDecode(decodeMap, this.numComps);
|
|
|
- },
|
|
|
- usesZeroToOneRange: true
|
|
|
- };
|
|
|
- return CalRGBCS;
|
|
|
-})();
|
|
|
-
|
|
|
-//
|
|
|
-// LabCS: Based on "PDF Reference, Sixth Ed", p.250
|
|
|
-//
|
|
|
-var LabCS = (function LabCSClosure() {
|
|
|
- function LabCS(whitePoint, blackPoint, range) {
|
|
|
- this.name = 'Lab';
|
|
|
- this.numComps = 3;
|
|
|
- this.defaultColor = new Float32Array([0, 0, 0]);
|
|
|
-
|
|
|
- if (!whitePoint) {
|
|
|
- error('WhitePoint missing - required for color space Lab');
|
|
|
- }
|
|
|
- blackPoint = blackPoint || [0, 0, 0];
|
|
|
- range = range || [-100, 100, -100, 100];
|
|
|
-
|
|
|
- // Translate args to spec variables
|
|
|
- this.XW = whitePoint[0];
|
|
|
- this.YW = whitePoint[1];
|
|
|
- this.ZW = whitePoint[2];
|
|
|
- this.amin = range[0];
|
|
|
- this.amax = range[1];
|
|
|
- this.bmin = range[2];
|
|
|
- this.bmax = range[3];
|
|
|
-
|
|
|
- // These are here just for completeness - the spec doesn't offer any
|
|
|
- // formulas that use BlackPoint in Lab
|
|
|
- this.XB = blackPoint[0];
|
|
|
- this.YB = blackPoint[1];
|
|
|
- this.ZB = blackPoint[2];
|
|
|
|
|
|
- // Validate vars as per spec
|
|
|
- if (this.XW < 0 || this.ZW < 0 || this.YW !== 1) {
|
|
|
- error('Invalid WhitePoint components, no fallback available');
|
|
|
- }
|
|
|
-
|
|
|
- if (this.XB < 0 || this.YB < 0 || this.ZB < 0) {
|
|
|
- info('Invalid BlackPoint, falling back to default');
|
|
|
- this.XB = this.YB = this.ZB = 0;
|
|
|
- }
|
|
|
-
|
|
|
- if (this.amin > this.amax || this.bmin > this.bmax) {
|
|
|
- info('Invalid Range, falling back to defaults');
|
|
|
- this.amin = -100;
|
|
|
- this.amax = 100;
|
|
|
- this.bmin = -100;
|
|
|
- this.bmax = 100;
|
|
|
- }
|
|
|
- }
|
|
|
+ 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 g(x) from spec
|
|
|
- function fn_g(x) {
|
|
|
- if (x >= 6 / 29) {
|
|
|
- return x * x * x;
|
|
|
- } else {
|
|
|
- return (108 / 841) * (x - 4 / 29);
|
|
|
- }
|
|
|
- }
|
|
|
+ function next() {
|
|
|
+ while (nodesToVisit.length) {
|
|
|
+ var currentNode = nodesToVisit.pop();
|
|
|
|
|
|
- function decode(value, high1, low2, high2) {
|
|
|
- return low2 + (value) * (high2 - low2) / (high1);
|
|
|
- }
|
|
|
+ if (isRef(currentNode)) {
|
|
|
+ xref.fetchAsync(currentNode).then(function (obj) {
|
|
|
+ if (isDict(obj, 'Page') || (isDict(obj) && !obj.has('Kids'))) {
|
|
|
+ if (pageIndex === currentPageIndex) {
|
|
|
+ capability.resolve([obj, currentNode]);
|
|
|
+ } else {
|
|
|
+ currentPageIndex++;
|
|
|
+ next();
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ nodesToVisit.push(obj);
|
|
|
+ next();
|
|
|
+ }, capability.reject);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- // If decoding is needed maxVal should be 2^bits per component - 1.
|
|
|
- function convertToRgb(cs, src, srcOffset, maxVal, dest, destOffset) {
|
|
|
- // XXX: Lab input is in the range of [0, 100], [amin, amax], [bmin, bmax]
|
|
|
- // not the usual [0, 1]. If a command like setFillColor is used the src
|
|
|
- // values will already be within the correct range. However, if we are
|
|
|
- // converting an image we have to map the values to the correct range given
|
|
|
- // above.
|
|
|
- // Ls,as,bs <---> L*,a*,b* in the spec
|
|
|
- var Ls = src[srcOffset];
|
|
|
- var as = src[srcOffset + 1];
|
|
|
- var bs = src[srcOffset + 2];
|
|
|
- if (maxVal !== false) {
|
|
|
- Ls = decode(Ls, maxVal, 0, 100);
|
|
|
- as = decode(as, maxVal, cs.amin, cs.amax);
|
|
|
- bs = decode(bs, maxVal, cs.bmin, cs.bmax);
|
|
|
- }
|
|
|
+ // 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;
|
|
|
+ }
|
|
|
|
|
|
- // Adjust limits of 'as' and 'bs'
|
|
|
- as = as > cs.amax ? cs.amax : as < cs.amin ? cs.amin : as;
|
|
|
- bs = bs > cs.bmax ? cs.bmax : bs < cs.bmin ? cs.bmin : bs;
|
|
|
+ var kids = currentNode.get('Kids');
|
|
|
+ assert(isArray(kids), 'page dictionary kids object is not an array');
|
|
|
+ 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
|
|
|
+ // handle this push it back on the queue so if it is a pages node it
|
|
|
+ // will be descended into.
|
|
|
+ nodesToVisit = [kids[pageIndex - currentPageIndex]];
|
|
|
+ currentPageIndex = pageIndex;
|
|
|
+ continue;
|
|
|
+ } else {
|
|
|
+ for (var last = kids.length - 1; last >= 0; last--) {
|
|
|
+ nodesToVisit.push(kids[last]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ capability.reject('Page index ' + pageIndex + ' not found.');
|
|
|
+ }
|
|
|
+ next();
|
|
|
+ return capability.promise;
|
|
|
+ },
|
|
|
|
|
|
- // Computes intermediate variables X,Y,Z as per spec
|
|
|
- var M = (Ls + 16) / 116;
|
|
|
- var L = M + (as / 500);
|
|
|
- var N = M - (bs / 200);
|
|
|
+ getPageIndex: function Catalog_getPageIndex(ref) {
|
|
|
+ // The page tree nodes have the count of all the leaves below them. To get
|
|
|
+ // how many pages are before we just have to walk up the tree and keep
|
|
|
+ // adding the count of siblings to the left of the node.
|
|
|
+ var xref = this.xref;
|
|
|
+ function pagesBeforeRef(kidRef) {
|
|
|
+ var total = 0;
|
|
|
+ var parentRef;
|
|
|
+ return xref.fetchAsync(kidRef).then(function (node) {
|
|
|
+ if (!node) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ parentRef = node.getRaw('Parent');
|
|
|
+ return node.getAsync('Parent');
|
|
|
+ }).then(function (parent) {
|
|
|
+ if (!parent) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return parent.getAsync('Kids');
|
|
|
+ }).then(function (kids) {
|
|
|
+ if (!kids) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ var kidPromises = [];
|
|
|
+ var found = false;
|
|
|
+ for (var i = 0; i < kids.length; i++) {
|
|
|
+ var kid = kids[i];
|
|
|
+ assert(isRef(kid), 'kids must be a ref');
|
|
|
+ if (kid.num === kidRef.num) {
|
|
|
+ found = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ kidPromises.push(xref.fetchAsync(kid).then(function (kid) {
|
|
|
+ if (kid.has('Count')) {
|
|
|
+ var count = kid.get('Count');
|
|
|
+ total += count;
|
|
|
+ } else { // page leaf node
|
|
|
+ total++;
|
|
|
+ }
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ if (!found) {
|
|
|
+ error('kid ref not found in parents kids');
|
|
|
+ }
|
|
|
+ return Promise.all(kidPromises).then(function () {
|
|
|
+ return [total, parentRef];
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- var X = cs.XW * fn_g(L);
|
|
|
- var Y = cs.YW * fn_g(M);
|
|
|
- var Z = cs.ZW * fn_g(N);
|
|
|
+ var total = 0;
|
|
|
+ function next(ref) {
|
|
|
+ return pagesBeforeRef(ref).then(function (args) {
|
|
|
+ if (!args) {
|
|
|
+ return total;
|
|
|
+ }
|
|
|
+ var count = args[0];
|
|
|
+ var parentRef = args[1];
|
|
|
+ total += count;
|
|
|
+ return next(parentRef);
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- var r, g, b;
|
|
|
- // Using different conversions for D50 and D65 white points,
|
|
|
- // per http://www.color.org/srgb.pdf
|
|
|
- if (cs.ZW < 1) {
|
|
|
- // Assuming D50 (X=0.9642, Y=1.00, Z=0.8249)
|
|
|
- r = X * 3.1339 + Y * -1.6170 + Z * -0.4906;
|
|
|
- g = X * -0.9785 + Y * 1.9160 + Z * 0.0333;
|
|
|
- b = X * 0.0720 + Y * -0.2290 + Z * 1.4057;
|
|
|
- } else {
|
|
|
- // Assuming D65 (X=0.9505, Y=1.00, Z=1.0888)
|
|
|
- r = X * 3.2406 + Y * -1.5372 + Z * -0.4986;
|
|
|
- g = X * -0.9689 + Y * 1.8758 + Z * 0.0415;
|
|
|
- b = X * 0.0557 + Y * -0.2040 + Z * 1.0570;
|
|
|
+ return next(ref);
|
|
|
}
|
|
|
- // clamp color values to [0,1] range then convert to [0,255] range.
|
|
|
- dest[destOffset] = r <= 0 ? 0 : r >= 1 ? 255 : Math.sqrt(r) * 255 | 0;
|
|
|
- dest[destOffset + 1] = g <= 0 ? 0 : g >= 1 ? 255 : Math.sqrt(g) * 255 | 0;
|
|
|
- dest[destOffset + 2] = b <= 0 ? 0 : b >= 1 ? 255 : Math.sqrt(b) * 255 | 0;
|
|
|
+ };
|
|
|
+
|
|
|
+ return Catalog;
|
|
|
+})();
|
|
|
+
|
|
|
+var XRef = (function XRefClosure() {
|
|
|
+ function XRef(stream, password) {
|
|
|
+ this.stream = stream;
|
|
|
+ this.entries = [];
|
|
|
+ this.xrefstms = Object.create(null);
|
|
|
+ // prepare the XRef cache
|
|
|
+ this.cache = [];
|
|
|
+ this.password = password;
|
|
|
+ this.stats = {
|
|
|
+ streamTypes: [],
|
|
|
+ fontTypes: []
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
- LabCS.prototype = {
|
|
|
- getRgb: ColorSpace.prototype.getRgb,
|
|
|
- getRgbItem: function LabCS_getRgbItem(src, srcOffset, dest, destOffset) {
|
|
|
- convertToRgb(this, src, srcOffset, false, dest, destOffset);
|
|
|
+ XRef.prototype = {
|
|
|
+ setStartXRef: function XRef_setStartXRef(startXRef) {
|
|
|
+ // Store the starting positions of xref tables as we process them
|
|
|
+ // so we can recover from missing data errors
|
|
|
+ this.startXRefQueue = [startXRef];
|
|
|
},
|
|
|
- getRgbBuffer: function LabCS_getRgbBuffer(src, srcOffset, count,
|
|
|
- dest, destOffset, bits,
|
|
|
- alpha01) {
|
|
|
- var maxVal = (1 << bits) - 1;
|
|
|
- for (var i = 0; i < count; i++) {
|
|
|
- convertToRgb(this, src, srcOffset, maxVal, dest, destOffset);
|
|
|
- srcOffset += 3;
|
|
|
- destOffset += 3 + alpha01;
|
|
|
+
|
|
|
+ parse: function XRef_parse(recoveryMode) {
|
|
|
+ var trailerDict;
|
|
|
+ if (!recoveryMode) {
|
|
|
+ trailerDict = this.readXRef();
|
|
|
+ } else {
|
|
|
+ warn('Indexing all PDF objects');
|
|
|
+ trailerDict = this.indexObjects();
|
|
|
+ }
|
|
|
+ trailerDict.assignXref(this);
|
|
|
+ this.trailer = trailerDict;
|
|
|
+ var encrypt = trailerDict.get('Encrypt');
|
|
|
+ if (encrypt) {
|
|
|
+ var ids = trailerDict.get('ID');
|
|
|
+ var fileId = (ids && ids.length) ? ids[0] : '';
|
|
|
+ this.encrypt = new CipherTransformFactory(encrypt, fileId,
|
|
|
+ this.password);
|
|
|
+ }
|
|
|
+
|
|
|
+ // get the root dictionary (catalog) object
|
|
|
+ if (!(this.root = trailerDict.get('Root'))) {
|
|
|
+ error('Invalid root reference');
|
|
|
}
|
|
|
},
|
|
|
- getOutputLength: function LabCS_getOutputLength(inputLength, alpha01) {
|
|
|
- return (inputLength * (3 + alpha01) / 3) | 0;
|
|
|
- },
|
|
|
- isPassthrough: ColorSpace.prototype.isPassthrough,
|
|
|
- fillRgb: ColorSpace.prototype.fillRgb,
|
|
|
- isDefaultDecode: function LabCS_isDefaultDecode(decodeMap) {
|
|
|
- // XXX: Decoding is handled with the lab conversion because of the strange
|
|
|
- // ranges that are used.
|
|
|
- return true;
|
|
|
- },
|
|
|
- usesZeroToOneRange: false
|
|
|
- };
|
|
|
- return LabCS;
|
|
|
-})();
|
|
|
|
|
|
-// TODO refactor to remove dependency on image.js
|
|
|
-function _setCoreImage(coreImage_) {
|
|
|
- coreImage = coreImage_;
|
|
|
- PDFImage = coreImage_.PDFImage;
|
|
|
-}
|
|
|
-exports._setCoreImage = _setCoreImage;
|
|
|
+ processXRefTable: function XRef_processXRefTable(parser) {
|
|
|
+ if (!('tableState' in this)) {
|
|
|
+ // Stores state of the table as we process it so we can resume
|
|
|
+ // from middle of table in case of missing data error
|
|
|
+ this.tableState = {
|
|
|
+ entryNum: 0,
|
|
|
+ streamPos: parser.lexer.stream.pos,
|
|
|
+ parserBuf1: parser.buf1,
|
|
|
+ parserBuf2: parser.buf2
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
-exports.ColorSpace = ColorSpace;
|
|
|
+ var obj = this.readXRefTable(parser);
|
|
|
|
|
|
-// TODO refactor to remove dependency on colorspace.js
|
|
|
-coreStream._setCoreColorSpace(exports);
|
|
|
-}));
|
|
|
+ // Sanity check
|
|
|
+ if (!isCmd(obj, 'trailer')) {
|
|
|
+ error('Invalid XRef table: could not find trailer dictionary');
|
|
|
+ }
|
|
|
+ // Read trailer dictionary, e.g.
|
|
|
+ // trailer
|
|
|
+ // << /Size 22
|
|
|
+ // /Root 20R
|
|
|
+ // /Info 10R
|
|
|
+ // /ID [ <81b14aafa313db63dbd6f981e49f94f4> ]
|
|
|
+ // >>
|
|
|
+ // The parser goes through the entire stream << ... >> and provides
|
|
|
+ // a getter interface for the key-value table
|
|
|
+ var dict = parser.getObj();
|
|
|
|
|
|
+ // The pdflib PDF generator can generate a nested trailer dictionary
|
|
|
+ if (!isDict(dict) && dict.dict) {
|
|
|
+ dict = dict.dict;
|
|
|
+ }
|
|
|
+ if (!isDict(dict)) {
|
|
|
+ error('Invalid XRef table: could not parse trailer dictionary');
|
|
|
+ }
|
|
|
+ delete this.tableState;
|
|
|
|
|
|
-(function (root, factory) {
|
|
|
- {
|
|
|
- factory((root.pdfjsCoreImage = {}), root.pdfjsSharedUtil,
|
|
|
- root.pdfjsCorePrimitives, root.pdfjsCoreColorSpace, root.pdfjsCoreStream,
|
|
|
- root.pdfjsCoreJpx);
|
|
|
- }
|
|
|
-}(this, function (exports, sharedUtil, corePrimitives, coreColorSpace,
|
|
|
- coreStream, coreJpx) {
|
|
|
+ return dict;
|
|
|
+ },
|
|
|
|
|
|
-var ImageKind = sharedUtil.ImageKind;
|
|
|
-var assert = sharedUtil.assert;
|
|
|
-var error = sharedUtil.error;
|
|
|
-var info = sharedUtil.info;
|
|
|
-var isArray = sharedUtil.isArray;
|
|
|
-var warn = sharedUtil.warn;
|
|
|
-var Name = corePrimitives.Name;
|
|
|
-var isStream = corePrimitives.isStream;
|
|
|
-var ColorSpace = coreColorSpace.ColorSpace;
|
|
|
-var DecodeStream = coreStream.DecodeStream;
|
|
|
-var Stream = coreStream.Stream;
|
|
|
-var JpegStream = coreStream.JpegStream;
|
|
|
-var JpxImage = coreJpx.JpxImage;
|
|
|
+ readXRefTable: function XRef_readXRefTable(parser) {
|
|
|
+ // Example of cross-reference table:
|
|
|
+ // xref
|
|
|
+ // 0 1 <-- subsection header (first obj #, obj count)
|
|
|
+ // 0000000000 65535 f <-- actual object (offset, generation #, f/n)
|
|
|
+ // 23 2 <-- subsection header ... and so on ...
|
|
|
+ // 0000025518 00002 n
|
|
|
+ // 0000025635 00000 n
|
|
|
+ // trailer
|
|
|
+ // ...
|
|
|
|
|
|
-var PDFImage = (function PDFImageClosure() {
|
|
|
- /**
|
|
|
- * Decode the image in the main thread if it supported. Resovles the promise
|
|
|
- * when the image data is ready.
|
|
|
- */
|
|
|
- function handleImageData(handler, xref, res, image) {
|
|
|
- if (image instanceof JpegStream && image.isNativelyDecodable(xref, res)) {
|
|
|
- // For natively supported jpegs send them to the main thread for decoding.
|
|
|
- var dict = image.dict;
|
|
|
- var colorSpace = dict.get('ColorSpace', 'CS');
|
|
|
- colorSpace = ColorSpace.parse(colorSpace, xref, res);
|
|
|
- var numComps = colorSpace.numComps;
|
|
|
- var decodePromise = handler.sendWithPromise('JpegDecode',
|
|
|
- [image.getIR(), numComps]);
|
|
|
- return decodePromise.then(function (message) {
|
|
|
- var data = message.data;
|
|
|
- return new Stream(data, 0, data.length, image.dict);
|
|
|
- });
|
|
|
- } else {
|
|
|
- return Promise.resolve(image);
|
|
|
- }
|
|
|
- }
|
|
|
+ var stream = parser.lexer.stream;
|
|
|
+ var tableState = this.tableState;
|
|
|
+ stream.pos = tableState.streamPos;
|
|
|
+ parser.buf1 = tableState.parserBuf1;
|
|
|
+ parser.buf2 = tableState.parserBuf2;
|
|
|
|
|
|
- /**
|
|
|
- * Decode and clamp a value. The formula is different from the spec because we
|
|
|
- * don't decode to float range [0,1], we decode it in the [0,max] range.
|
|
|
- */
|
|
|
- function decodeAndClamp(value, addend, coefficient, max) {
|
|
|
- value = addend + value * coefficient;
|
|
|
- // Clamp the value to the range
|
|
|
- return (value < 0 ? 0 : (value > max ? max : value));
|
|
|
- }
|
|
|
+ // Outer loop is over subsection headers
|
|
|
+ var obj;
|
|
|
|
|
|
- function PDFImage(xref, res, image, inline, smask, mask, isMask) {
|
|
|
- this.image = image;
|
|
|
- var dict = image.dict;
|
|
|
- if (dict.has('Filter')) {
|
|
|
- var filter = dict.get('Filter').name;
|
|
|
- if (filter === 'JPXDecode') {
|
|
|
- var jpxImage = new JpxImage();
|
|
|
- jpxImage.parseImageProperties(image.stream);
|
|
|
- image.stream.reset();
|
|
|
- image.bitsPerComponent = jpxImage.bitsPerComponent;
|
|
|
- image.numComps = jpxImage.componentsCount;
|
|
|
- } else if (filter === 'JBIG2Decode') {
|
|
|
- image.bitsPerComponent = 1;
|
|
|
- image.numComps = 1;
|
|
|
- }
|
|
|
- }
|
|
|
- // TODO cache rendered images?
|
|
|
+ while (true) {
|
|
|
+ if (!('firstEntryNum' in tableState) || !('entryCount' in tableState)) {
|
|
|
+ if (isCmd(obj = parser.getObj(), 'trailer')) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ tableState.firstEntryNum = obj;
|
|
|
+ tableState.entryCount = parser.getObj();
|
|
|
+ }
|
|
|
|
|
|
- this.width = dict.get('Width', 'W');
|
|
|
- this.height = dict.get('Height', 'H');
|
|
|
+ var first = tableState.firstEntryNum;
|
|
|
+ var count = tableState.entryCount;
|
|
|
+ if (!isInt(first) || !isInt(count)) {
|
|
|
+ error('Invalid XRef table: wrong types in subsection header');
|
|
|
+ }
|
|
|
+ // Inner loop is over objects themselves
|
|
|
+ for (var i = tableState.entryNum; i < count; i++) {
|
|
|
+ tableState.streamPos = stream.pos;
|
|
|
+ tableState.entryNum = i;
|
|
|
+ tableState.parserBuf1 = parser.buf1;
|
|
|
+ tableState.parserBuf2 = parser.buf2;
|
|
|
|
|
|
- if (this.width < 1 || this.height < 1) {
|
|
|
- error('Invalid image width: ' + this.width + ' or height: ' +
|
|
|
- this.height);
|
|
|
- }
|
|
|
+ var entry = {};
|
|
|
+ entry.offset = parser.getObj();
|
|
|
+ entry.gen = parser.getObj();
|
|
|
+ var type = parser.getObj();
|
|
|
|
|
|
- this.interpolate = dict.get('Interpolate', 'I') || false;
|
|
|
- this.imageMask = dict.get('ImageMask', 'IM') || false;
|
|
|
- this.matte = dict.get('Matte') || false;
|
|
|
+ if (isCmd(type, 'f')) {
|
|
|
+ entry.free = true;
|
|
|
+ } else if (isCmd(type, 'n')) {
|
|
|
+ entry.uncompressed = true;
|
|
|
+ }
|
|
|
|
|
|
- var bitsPerComponent = image.bitsPerComponent;
|
|
|
- if (!bitsPerComponent) {
|
|
|
- bitsPerComponent = dict.get('BitsPerComponent', 'BPC');
|
|
|
- if (!bitsPerComponent) {
|
|
|
- if (this.imageMask) {
|
|
|
- bitsPerComponent = 1;
|
|
|
- } else {
|
|
|
- error('Bits per component missing in image: ' + this.imageMask);
|
|
|
+ // Validate entry obj
|
|
|
+ if (!isInt(entry.offset) || !isInt(entry.gen) ||
|
|
|
+ !(entry.free || entry.uncompressed)) {
|
|
|
+ error('Invalid entry in XRef subsection: ' + first + ', ' + count);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this.entries[i + first]) {
|
|
|
+ this.entries[i + first] = entry;
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+ tableState.entryNum = 0;
|
|
|
+ tableState.streamPos = stream.pos;
|
|
|
+ tableState.parserBuf1 = parser.buf1;
|
|
|
+ tableState.parserBuf2 = parser.buf2;
|
|
|
+ delete tableState.firstEntryNum;
|
|
|
+ delete tableState.entryCount;
|
|
|
}
|
|
|
- }
|
|
|
- this.bpc = bitsPerComponent;
|
|
|
|
|
|
- if (!this.imageMask) {
|
|
|
- var colorSpace = dict.get('ColorSpace', 'CS');
|
|
|
- if (!colorSpace) {
|
|
|
- info('JPX images (which do not require color spaces)');
|
|
|
- switch (image.numComps) {
|
|
|
- case 1:
|
|
|
- colorSpace = Name.get('DeviceGray');
|
|
|
- break;
|
|
|
- case 3:
|
|
|
- colorSpace = Name.get('DeviceRGB');
|
|
|
- break;
|
|
|
- case 4:
|
|
|
- colorSpace = Name.get('DeviceCMYK');
|
|
|
- break;
|
|
|
- default:
|
|
|
- error('JPX images with ' + this.numComps +
|
|
|
- ' color components not supported.');
|
|
|
- }
|
|
|
+ // Per issue 3248: hp scanners generate bad XRef
|
|
|
+ if (first === 1 && this.entries[1] && this.entries[1].free) {
|
|
|
+ // shifting the entries
|
|
|
+ this.entries.shift();
|
|
|
}
|
|
|
- this.colorSpace = ColorSpace.parse(colorSpace, xref, res);
|
|
|
- this.numComps = this.colorSpace.numComps;
|
|
|
- }
|
|
|
|
|
|
- this.decode = dict.get('Decode', 'D');
|
|
|
- this.needsDecode = false;
|
|
|
- if (this.decode &&
|
|
|
- ((this.colorSpace && !this.colorSpace.isDefaultDecode(this.decode)) ||
|
|
|
- (isMask && !ColorSpace.isDefaultDecode(this.decode, 1)))) {
|
|
|
- this.needsDecode = true;
|
|
|
- // Do some preprocessing to avoid more math.
|
|
|
- var max = (1 << bitsPerComponent) - 1;
|
|
|
- this.decodeCoefficients = [];
|
|
|
- this.decodeAddends = [];
|
|
|
- for (var i = 0, j = 0; i < this.decode.length; i += 2, ++j) {
|
|
|
- var dmin = this.decode[i];
|
|
|
- var dmax = this.decode[i + 1];
|
|
|
- this.decodeCoefficients[j] = dmax - dmin;
|
|
|
- this.decodeAddends[j] = max * dmin;
|
|
|
+ // Sanity check: as per spec, first object must be free
|
|
|
+ if (this.entries[0] && !this.entries[0].free) {
|
|
|
+ error('Invalid XRef table: unexpected first object');
|
|
|
}
|
|
|
- }
|
|
|
+ return obj;
|
|
|
+ },
|
|
|
|
|
|
- if (smask) {
|
|
|
- this.smask = new PDFImage(xref, res, smask, false);
|
|
|
- } else if (mask) {
|
|
|
- if (isStream(mask)) {
|
|
|
- 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);
|
|
|
+ processXRefStream: function XRef_processXRefStream(stream) {
|
|
|
+ if (!('streamState' in this)) {
|
|
|
+ // Stores state of the stream as we process it so we can resume
|
|
|
+ // from middle of stream in case of missing data error
|
|
|
+ var streamParameters = stream.dict;
|
|
|
+ var byteWidths = streamParameters.get('W');
|
|
|
+ var range = streamParameters.get('Index');
|
|
|
+ if (!range) {
|
|
|
+ range = [0, streamParameters.get('Size')];
|
|
|
}
|
|
|
- } else {
|
|
|
- // Color key mask (just an array).
|
|
|
- this.mask = mask;
|
|
|
+
|
|
|
+ this.streamState = {
|
|
|
+ entryRanges: range,
|
|
|
+ byteWidths: byteWidths,
|
|
|
+ entryNum: 0,
|
|
|
+ streamPos: stream.pos
|
|
|
+ };
|
|
|
}
|
|
|
- }
|
|
|
- }
|
|
|
- /**
|
|
|
- * Handles processing of image data and returns the Promise that is resolved
|
|
|
- * with a PDFImage when the image is ready to be used.
|
|
|
- */
|
|
|
- PDFImage.buildImage = function PDFImage_buildImage(handler, xref,
|
|
|
- res, image, inline) {
|
|
|
- var imagePromise = handleImageData(handler, xref, res, image);
|
|
|
- var smaskPromise;
|
|
|
- var maskPromise;
|
|
|
+ this.readXRefStream(stream);
|
|
|
+ delete this.streamState;
|
|
|
|
|
|
- var smask = image.dict.get('SMask');
|
|
|
- var mask = image.dict.get('Mask');
|
|
|
+ return stream.dict;
|
|
|
+ },
|
|
|
|
|
|
- if (smask) {
|
|
|
- smaskPromise = handleImageData(handler, xref, res, smask);
|
|
|
- maskPromise = Promise.resolve(null);
|
|
|
- } else {
|
|
|
- smaskPromise = Promise.resolve(null);
|
|
|
- if (mask) {
|
|
|
- if (isStream(mask)) {
|
|
|
- maskPromise = handleImageData(handler, xref, res, mask);
|
|
|
- } else if (isArray(mask)) {
|
|
|
- maskPromise = Promise.resolve(mask);
|
|
|
- } else {
|
|
|
- warn('Unsupported mask format.');
|
|
|
- maskPromise = Promise.resolve(null);
|
|
|
+ readXRefStream: function XRef_readXRefStream(stream) {
|
|
|
+ var i, j;
|
|
|
+ var streamState = this.streamState;
|
|
|
+ stream.pos = streamState.streamPos;
|
|
|
+
|
|
|
+ var byteWidths = streamState.byteWidths;
|
|
|
+ var typeFieldWidth = byteWidths[0];
|
|
|
+ var offsetFieldWidth = byteWidths[1];
|
|
|
+ var generationFieldWidth = byteWidths[2];
|
|
|
+
|
|
|
+ var entryRanges = streamState.entryRanges;
|
|
|
+ while (entryRanges.length > 0) {
|
|
|
+ var first = entryRanges[0];
|
|
|
+ var n = entryRanges[1];
|
|
|
+
|
|
|
+ if (!isInt(first) || !isInt(n)) {
|
|
|
+ error('Invalid XRef range fields: ' + first + ', ' + n);
|
|
|
}
|
|
|
- } else {
|
|
|
- maskPromise = Promise.resolve(null);
|
|
|
+ if (!isInt(typeFieldWidth) || !isInt(offsetFieldWidth) ||
|
|
|
+ !isInt(generationFieldWidth)) {
|
|
|
+ error('Invalid XRef entry fields length: ' + first + ', ' + n);
|
|
|
+ }
|
|
|
+ for (i = streamState.entryNum; i < n; ++i) {
|
|
|
+ streamState.entryNum = i;
|
|
|
+ streamState.streamPos = stream.pos;
|
|
|
+
|
|
|
+ var type = 0, offset = 0, generation = 0;
|
|
|
+ for (j = 0; j < typeFieldWidth; ++j) {
|
|
|
+ type = (type << 8) | stream.getByte();
|
|
|
+ }
|
|
|
+ // if type field is absent, its default value is 1
|
|
|
+ if (typeFieldWidth === 0) {
|
|
|
+ type = 1;
|
|
|
+ }
|
|
|
+ for (j = 0; j < offsetFieldWidth; ++j) {
|
|
|
+ offset = (offset << 8) | stream.getByte();
|
|
|
+ }
|
|
|
+ for (j = 0; j < generationFieldWidth; ++j) {
|
|
|
+ generation = (generation << 8) | stream.getByte();
|
|
|
+ }
|
|
|
+ var entry = {};
|
|
|
+ entry.offset = offset;
|
|
|
+ entry.gen = generation;
|
|
|
+ switch (type) {
|
|
|
+ case 0:
|
|
|
+ entry.free = true;
|
|
|
+ break;
|
|
|
+ case 1:
|
|
|
+ entry.uncompressed = true;
|
|
|
+ break;
|
|
|
+ case 2:
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ error('Invalid XRef entry type: ' + type);
|
|
|
+ }
|
|
|
+ if (!this.entries[first + i]) {
|
|
|
+ this.entries[first + i] = entry;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ streamState.entryNum = 0;
|
|
|
+ streamState.streamPos = stream.pos;
|
|
|
+ entryRanges.splice(0, 2);
|
|
|
}
|
|
|
- }
|
|
|
- return Promise.all([imagePromise, smaskPromise, maskPromise]).then(
|
|
|
- function(results) {
|
|
|
- var imageData = results[0];
|
|
|
- var smaskData = results[1];
|
|
|
- var maskData = results[2];
|
|
|
- return new PDFImage(xref, res, imageData, inline, smaskData, maskData);
|
|
|
- });
|
|
|
- };
|
|
|
+ },
|
|
|
|
|
|
- /**
|
|
|
- * Resize an image using the nearest neighbor algorithm. Currently only
|
|
|
- * supports one and three component images.
|
|
|
- * @param {TypedArray} pixels The original image with one component.
|
|
|
- * @param {Number} bpc Number of bits per component.
|
|
|
- * @param {Number} components Number of color components, 1 or 3 is supported.
|
|
|
- * @param {Number} w1 Original width.
|
|
|
- * @param {Number} h1 Original height.
|
|
|
- * @param {Number} w2 New width.
|
|
|
- * @param {Number} h2 New height.
|
|
|
- * @param {TypedArray} dest (Optional) The destination buffer.
|
|
|
- * @param {Number} alpha01 (Optional) Size reserved for the alpha channel.
|
|
|
- * @return {TypedArray} Resized image data.
|
|
|
- */
|
|
|
- PDFImage.resize = function PDFImage_resize(pixels, bpc, components,
|
|
|
- w1, h1, w2, h2, dest, alpha01) {
|
|
|
+ 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 !== LF && ch !== CR && ch !== LT) {
|
|
|
+ if (++offset >= data.length) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ token += String.fromCharCode(ch);
|
|
|
+ ch = data[offset];
|
|
|
+ }
|
|
|
+ return token;
|
|
|
+ }
|
|
|
+ function skipUntil(data, offset, what) {
|
|
|
+ var length = what.length, dataLength = data.length;
|
|
|
+ var skipped = 0;
|
|
|
+ // finding byte sequence
|
|
|
+ while (offset < dataLength) {
|
|
|
+ var i = 0;
|
|
|
+ while (i < length && data[offset + i] === what[i]) {
|
|
|
+ ++i;
|
|
|
+ }
|
|
|
+ if (i >= length) {
|
|
|
+ break; // sequence found
|
|
|
+ }
|
|
|
+ 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]);
|
|
|
|
|
|
- if (components !== 1 && components !== 3) {
|
|
|
- error('Unsupported component count for resizing.');
|
|
|
- }
|
|
|
+ // Clear out any existing entries, since they may be bogus.
|
|
|
+ this.entries.length = 0;
|
|
|
|
|
|
- var length = w2 * h2 * components;
|
|
|
- var temp = dest ? dest : (bpc <= 8 ? new Uint8Array(length) :
|
|
|
- (bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length)));
|
|
|
- var xRatio = w1 / w2;
|
|
|
- var yRatio = h1 / h2;
|
|
|
- var i, j, py, newIndex = 0, oldIndex;
|
|
|
- var xScaled = new Uint16Array(w2);
|
|
|
- var w1Scanline = w1 * components;
|
|
|
- if (alpha01 !== 1) {
|
|
|
- alpha01 = 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 === TAB || ch === LF || ch === CR || ch === SPACE) {
|
|
|
+ ++position;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (ch === PERCENT) { // %-comment
|
|
|
+ do {
|
|
|
+ ++position;
|
|
|
+ if (position >= length) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ ch = buffer[position];
|
|
|
+ } while (ch !== LF && ch !== CR);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ var token = readToken(buffer, position);
|
|
|
+ var m;
|
|
|
+ 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 = 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);
|
|
|
|
|
|
- for (j = 0; j < w2; j++) {
|
|
|
- xScaled[j] = Math.floor(j * xRatio) * components;
|
|
|
- }
|
|
|
+ // 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 - stream.start);
|
|
|
+ this.xrefstms[position - stream.start] = 1; // Avoid recursion
|
|
|
+ }
|
|
|
|
|
|
- if (components === 1) {
|
|
|
- for (i = 0; i < h2; i++) {
|
|
|
- py = Math.floor(i * yRatio) * w1Scanline;
|
|
|
- for (j = 0; j < w2; j++) {
|
|
|
- oldIndex = py + xScaled[j];
|
|
|
- temp[newIndex++] = pixels[oldIndex];
|
|
|
+ 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;
|
|
|
}
|
|
|
}
|
|
|
- } else if (components === 3) {
|
|
|
- for (i = 0; i < h2; i++) {
|
|
|
- py = Math.floor(i * yRatio) * w1Scanline;
|
|
|
- for (j = 0; j < w2; j++) {
|
|
|
- oldIndex = py + xScaled[j];
|
|
|
- temp[newIndex++] = pixels[oldIndex++];
|
|
|
- temp[newIndex++] = pixels[oldIndex++];
|
|
|
- temp[newIndex++] = pixels[oldIndex++];
|
|
|
- newIndex += alpha01;
|
|
|
+ // reading XRef streams
|
|
|
+ var i, ii;
|
|
|
+ for (i = 0, ii = xrefStms.length; i < ii; ++i) {
|
|
|
+ this.startXRefQueue.push(xrefStms[i]);
|
|
|
+ this.readXRef(/* recoveryMode */ true);
|
|
|
+ }
|
|
|
+ // finding main trailer
|
|
|
+ var dict;
|
|
|
+ for (i = 0, ii = trailers.length; i < ii; ++i) {
|
|
|
+ stream.pos = trailers[i];
|
|
|
+ var parser = new Parser(new Lexer(stream), true, this);
|
|
|
+ var obj = parser.getObj();
|
|
|
+ if (!isCmd(obj, 'trailer')) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // read the trailer dictionary
|
|
|
+ if (!isDict(dict = parser.getObj())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // taking the first one with 'ID'
|
|
|
+ if (dict.has('ID')) {
|
|
|
+ return dict;
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
- return temp;
|
|
|
- };
|
|
|
-
|
|
|
- PDFImage.createMask =
|
|
|
- function PDFImage_createMask(imgArray, width, height,
|
|
|
- imageIsFromDecodeStream, inverseDecode) {
|
|
|
-
|
|
|
- // |imgArray| might not contain full data for every pixel of the mask, so
|
|
|
- // we need to distinguish between |computedLength| and |actualLength|.
|
|
|
- // In particular, if inverseDecode is true, then the array we return must
|
|
|
- // have a length of |computedLength|.
|
|
|
+ // no tailer with 'ID', taking last one (if exists)
|
|
|
+ if (dict) {
|
|
|
+ return dict;
|
|
|
+ }
|
|
|
+ // nothing helps
|
|
|
+ // calling error() would reject worker with an UnknownErrorException.
|
|
|
+ throw new InvalidPDFException('Invalid PDF structure');
|
|
|
+ },
|
|
|
|
|
|
- var computedLength = ((width + 7) >> 3) * height;
|
|
|
- var actualLength = imgArray.byteLength;
|
|
|
- var haveFullData = computedLength === actualLength;
|
|
|
- var data, i;
|
|
|
+ readXRef: function XRef_readXRef(recoveryMode) {
|
|
|
+ var stream = this.stream;
|
|
|
|
|
|
- if (imageIsFromDecodeStream && (!inverseDecode || haveFullData)) {
|
|
|
- // imgArray came from a DecodeStream and its data is in an appropriate
|
|
|
- // form, so we can just transfer it.
|
|
|
- data = imgArray;
|
|
|
- } else if (!inverseDecode) {
|
|
|
- data = new Uint8Array(actualLength);
|
|
|
- data.set(imgArray);
|
|
|
- } else {
|
|
|
- data = new Uint8Array(computedLength);
|
|
|
- data.set(imgArray);
|
|
|
- for (i = actualLength; i < computedLength; i++) {
|
|
|
- data[i] = 0xff;
|
|
|
- }
|
|
|
- }
|
|
|
+ try {
|
|
|
+ while (this.startXRefQueue.length) {
|
|
|
+ var startXRef = this.startXRefQueue[0];
|
|
|
|
|
|
- // If necessary, invert the original mask data (but not any extra we might
|
|
|
- // have added above). It's safe to modify the array -- whether it's the
|
|
|
- // original or a copy, we're about to transfer it anyway, so nothing else
|
|
|
- // in this thread can be relying on its contents.
|
|
|
- if (inverseDecode) {
|
|
|
- for (i = 0; i < actualLength; i++) {
|
|
|
- data[i] = ~data[i];
|
|
|
- }
|
|
|
- }
|
|
|
+ stream.pos = startXRef + stream.start;
|
|
|
|
|
|
- return {data: data, width: width, height: height};
|
|
|
- };
|
|
|
+ var parser = new Parser(new Lexer(stream), true, this);
|
|
|
+ var obj = parser.getObj();
|
|
|
+ var dict;
|
|
|
|
|
|
- PDFImage.prototype = {
|
|
|
- get drawWidth() {
|
|
|
- return Math.max(this.width,
|
|
|
- this.smask && this.smask.width || 0,
|
|
|
- this.mask && this.mask.width || 0);
|
|
|
- },
|
|
|
+ // Get dictionary
|
|
|
+ if (isCmd(obj, 'xref')) {
|
|
|
+ // Parse end-of-file XRef
|
|
|
+ dict = this.processXRefTable(parser);
|
|
|
+ if (!this.topDict) {
|
|
|
+ this.topDict = dict;
|
|
|
+ }
|
|
|
|
|
|
- get drawHeight() {
|
|
|
- return Math.max(this.height,
|
|
|
- this.smask && this.smask.height || 0,
|
|
|
- this.mask && this.mask.height || 0);
|
|
|
- },
|
|
|
+ // Recursively get other XRefs 'XRefStm', if any
|
|
|
+ obj = dict.get('XRefStm');
|
|
|
+ if (isInt(obj)) {
|
|
|
+ var pos = obj;
|
|
|
+ // ignore previously loaded xref streams
|
|
|
+ // (possible infinite recursion)
|
|
|
+ if (!(pos in this.xrefstms)) {
|
|
|
+ this.xrefstms[pos] = 1;
|
|
|
+ this.startXRefQueue.push(pos);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (isInt(obj)) {
|
|
|
+ // Parse in-stream XRef
|
|
|
+ if (!isInt(parser.getObj()) ||
|
|
|
+ !isCmd(parser.getObj(), 'obj') ||
|
|
|
+ !isStream(obj = parser.getObj())) {
|
|
|
+ error('Invalid XRef stream');
|
|
|
+ }
|
|
|
+ dict = this.processXRefStream(obj);
|
|
|
+ if (!this.topDict) {
|
|
|
+ this.topDict = dict;
|
|
|
+ }
|
|
|
+ if (!dict) {
|
|
|
+ error('Failed to read XRef stream');
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ error('Invalid XRef stream header');
|
|
|
+ }
|
|
|
|
|
|
- decodeBuffer: function PDFImage_decodeBuffer(buffer) {
|
|
|
- var bpc = this.bpc;
|
|
|
- var numComps = this.numComps;
|
|
|
+ // Recursively get previous dictionary, if any
|
|
|
+ obj = dict.get('Prev');
|
|
|
+ if (isInt(obj)) {
|
|
|
+ this.startXRefQueue.push(obj);
|
|
|
+ } else if (isRef(obj)) {
|
|
|
+ // The spec says Prev must not be a reference, i.e. "/Prev NNN"
|
|
|
+ // This is a fallback for non-compliant PDFs, i.e. "/Prev NNN 0 R"
|
|
|
+ this.startXRefQueue.push(obj.num);
|
|
|
+ }
|
|
|
|
|
|
- var decodeAddends = this.decodeAddends;
|
|
|
- var decodeCoefficients = this.decodeCoefficients;
|
|
|
- var max = (1 << bpc) - 1;
|
|
|
- var i, ii;
|
|
|
+ this.startXRefQueue.shift();
|
|
|
+ }
|
|
|
|
|
|
- if (bpc === 1) {
|
|
|
- // If the buffer needed decode that means it just needs to be inverted.
|
|
|
- for (i = 0, ii = buffer.length; i < ii; i++) {
|
|
|
- buffer[i] = +!(buffer[i]);
|
|
|
+ return this.topDict;
|
|
|
+ } catch (e) {
|
|
|
+ if (e instanceof MissingDataException) {
|
|
|
+ throw e;
|
|
|
}
|
|
|
- return;
|
|
|
+ info('(while reading XRef): ' + e);
|
|
|
}
|
|
|
- var index = 0;
|
|
|
- for (i = 0, ii = this.width * this.height; i < ii; i++) {
|
|
|
- for (var j = 0; j < numComps; j++) {
|
|
|
- buffer[index] = decodeAndClamp(buffer[index], decodeAddends[j],
|
|
|
- decodeCoefficients[j], max);
|
|
|
- index++;
|
|
|
- }
|
|
|
+
|
|
|
+ if (recoveryMode) {
|
|
|
+ return;
|
|
|
}
|
|
|
+ throw new XRefParseException();
|
|
|
},
|
|
|
|
|
|
- getComponents: function PDFImage_getComponents(buffer) {
|
|
|
- var bpc = this.bpc;
|
|
|
-
|
|
|
- // This image doesn't require any extra work.
|
|
|
- if (bpc === 8) {
|
|
|
- return buffer;
|
|
|
+ getEntry: function XRef_getEntry(i) {
|
|
|
+ var xrefEntry = this.entries[i];
|
|
|
+ if (xrefEntry && !xrefEntry.free && xrefEntry.offset) {
|
|
|
+ return xrefEntry;
|
|
|
}
|
|
|
+ return null;
|
|
|
+ },
|
|
|
|
|
|
- var width = this.width;
|
|
|
- var height = this.height;
|
|
|
- var numComps = this.numComps;
|
|
|
-
|
|
|
- var length = width * height * numComps;
|
|
|
- var bufferPos = 0;
|
|
|
- var output = (bpc <= 8 ? new Uint8Array(length) :
|
|
|
- (bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length)));
|
|
|
- var rowComps = width * numComps;
|
|
|
+ fetchIfRef: function XRef_fetchIfRef(obj) {
|
|
|
+ if (!isRef(obj)) {
|
|
|
+ return obj;
|
|
|
+ }
|
|
|
+ return this.fetch(obj);
|
|
|
+ },
|
|
|
|
|
|
- var max = (1 << bpc) - 1;
|
|
|
- var i = 0, ii, buf;
|
|
|
+ fetch: function XRef_fetch(ref, suppressEncryption) {
|
|
|
+ assert(isRef(ref), 'ref object is not a reference');
|
|
|
+ var num = ref.num;
|
|
|
+ if (num in this.cache) {
|
|
|
+ var cacheEntry = this.cache[num];
|
|
|
+ return cacheEntry;
|
|
|
+ }
|
|
|
|
|
|
- if (bpc === 1) {
|
|
|
- // Optimization for reading 1 bpc images.
|
|
|
- var mask, loop1End, loop2End;
|
|
|
- for (var j = 0; j < height; j++) {
|
|
|
- loop1End = i + (rowComps & ~7);
|
|
|
- loop2End = i + rowComps;
|
|
|
+ var xrefEntry = this.getEntry(num);
|
|
|
|
|
|
- // unroll loop for all full bytes
|
|
|
- while (i < loop1End) {
|
|
|
- buf = buffer[bufferPos++];
|
|
|
- output[i] = (buf >> 7) & 1;
|
|
|
- output[i + 1] = (buf >> 6) & 1;
|
|
|
- output[i + 2] = (buf >> 5) & 1;
|
|
|
- output[i + 3] = (buf >> 4) & 1;
|
|
|
- output[i + 4] = (buf >> 3) & 1;
|
|
|
- output[i + 5] = (buf >> 2) & 1;
|
|
|
- output[i + 6] = (buf >> 1) & 1;
|
|
|
- output[i + 7] = buf & 1;
|
|
|
- i += 8;
|
|
|
- }
|
|
|
+ // the referenced entry can be free
|
|
|
+ if (xrefEntry === null) {
|
|
|
+ return (this.cache[num] = null);
|
|
|
+ }
|
|
|
|
|
|
- // handle remaing bits
|
|
|
- if (i < loop2End) {
|
|
|
- buf = buffer[bufferPos++];
|
|
|
- mask = 128;
|
|
|
- while (i < loop2End) {
|
|
|
- output[i++] = +!!(buf & mask);
|
|
|
- mask >>= 1;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ if (xrefEntry.uncompressed) {
|
|
|
+ xrefEntry = this.fetchUncompressed(ref, xrefEntry, suppressEncryption);
|
|
|
} else {
|
|
|
- // The general case that handles all other bpc values.
|
|
|
- var bits = 0;
|
|
|
- buf = 0;
|
|
|
- for (i = 0, ii = length; i < ii; ++i) {
|
|
|
- if (i % rowComps === 0) {
|
|
|
- buf = 0;
|
|
|
- bits = 0;
|
|
|
- }
|
|
|
+ xrefEntry = this.fetchCompressed(xrefEntry, suppressEncryption);
|
|
|
+ }
|
|
|
+ if (isDict(xrefEntry)){
|
|
|
+ xrefEntry.objId = ref.toString();
|
|
|
+ } else if (isStream(xrefEntry)) {
|
|
|
+ xrefEntry.dict.objId = ref.toString();
|
|
|
+ }
|
|
|
+ return xrefEntry;
|
|
|
+ },
|
|
|
|
|
|
- while (bits < bpc) {
|
|
|
- buf = (buf << 8) | buffer[bufferPos++];
|
|
|
- bits += 8;
|
|
|
+ fetchUncompressed: function XRef_fetchUncompressed(ref, xrefEntry,
|
|
|
+ suppressEncryption) {
|
|
|
+ var gen = ref.gen;
|
|
|
+ var num = ref.num;
|
|
|
+ if (xrefEntry.gen !== gen) {
|
|
|
+ error('inconsistent generation in XRef');
|
|
|
+ }
|
|
|
+ var stream = this.stream.makeSubStream(xrefEntry.offset +
|
|
|
+ this.stream.start);
|
|
|
+ var parser = new Parser(new Lexer(stream), true, this);
|
|
|
+ var obj1 = parser.getObj();
|
|
|
+ var obj2 = parser.getObj();
|
|
|
+ var obj3 = parser.getObj();
|
|
|
+ if (!isInt(obj1) || parseInt(obj1, 10) !== num ||
|
|
|
+ !isInt(obj2) || parseInt(obj2, 10) !== gen ||
|
|
|
+ !isCmd(obj3)) {
|
|
|
+ error('bad XRef entry');
|
|
|
+ }
|
|
|
+ if (!isCmd(obj3, 'obj')) {
|
|
|
+ // some bad PDFs use "obj1234" and really mean 1234
|
|
|
+ if (obj3.cmd.indexOf('obj') === 0) {
|
|
|
+ num = parseInt(obj3.cmd.substring(3), 10);
|
|
|
+ if (!isNaN(num)) {
|
|
|
+ return num;
|
|
|
}
|
|
|
-
|
|
|
- var remainingBits = bits - bpc;
|
|
|
- var value = buf >> remainingBits;
|
|
|
- output[i] = (value < 0 ? 0 : (value > max ? max : value));
|
|
|
- buf = buf & ((1 << remainingBits) - 1);
|
|
|
- bits = remainingBits;
|
|
|
}
|
|
|
+ error('bad XRef entry');
|
|
|
}
|
|
|
- return output;
|
|
|
+ if (this.encrypt && !suppressEncryption) {
|
|
|
+ xrefEntry = parser.getObj(this.encrypt.createCipherTransform(num, gen));
|
|
|
+ } else {
|
|
|
+ xrefEntry = parser.getObj();
|
|
|
+ }
|
|
|
+ if (!isStream(xrefEntry)) {
|
|
|
+ this.cache[num] = xrefEntry;
|
|
|
+ }
|
|
|
+ return xrefEntry;
|
|
|
},
|
|
|
|
|
|
- fillOpacity: function PDFImage_fillOpacity(rgbaBuf, width, height,
|
|
|
- actualHeight, image) {
|
|
|
- var smask = this.smask;
|
|
|
- var mask = this.mask;
|
|
|
- var alphaBuf, sw, sh, i, ii, j;
|
|
|
-
|
|
|
- if (smask) {
|
|
|
- sw = smask.width;
|
|
|
- sh = smask.height;
|
|
|
- alphaBuf = new Uint8Array(sw * sh);
|
|
|
- smask.fillGrayBuffer(alphaBuf);
|
|
|
- if (sw !== width || sh !== height) {
|
|
|
- alphaBuf = PDFImage.resize(alphaBuf, smask.bpc, 1, sw, sh, width,
|
|
|
- height);
|
|
|
+ fetchCompressed: function XRef_fetchCompressed(xrefEntry,
|
|
|
+ suppressEncryption) {
|
|
|
+ var tableOffset = xrefEntry.offset;
|
|
|
+ var stream = this.fetch(new Ref(tableOffset, 0));
|
|
|
+ if (!isStream(stream)) {
|
|
|
+ error('bad ObjStm stream');
|
|
|
+ }
|
|
|
+ var first = stream.dict.get('First');
|
|
|
+ var n = stream.dict.get('N');
|
|
|
+ if (!isInt(first) || !isInt(n)) {
|
|
|
+ error('invalid first and n parameters for ObjStm stream');
|
|
|
+ }
|
|
|
+ var parser = new Parser(new Lexer(stream), false, this);
|
|
|
+ parser.allowStreams = true;
|
|
|
+ var i, entries = [], num, nums = [];
|
|
|
+ // read the object numbers to populate cache
|
|
|
+ for (i = 0; i < n; ++i) {
|
|
|
+ num = parser.getObj();
|
|
|
+ if (!isInt(num)) {
|
|
|
+ error('invalid object number in the ObjStm stream: ' + num);
|
|
|
}
|
|
|
- } else if (mask) {
|
|
|
- if (mask instanceof PDFImage) {
|
|
|
- sw = mask.width;
|
|
|
- sh = mask.height;
|
|
|
- alphaBuf = new Uint8Array(sw * sh);
|
|
|
- mask.numComps = 1;
|
|
|
- mask.fillGrayBuffer(alphaBuf);
|
|
|
-
|
|
|
- // Need to invert values in rgbaBuf
|
|
|
- for (i = 0, ii = sw * sh; i < ii; ++i) {
|
|
|
- alphaBuf[i] = 255 - alphaBuf[i];
|
|
|
- }
|
|
|
-
|
|
|
- if (sw !== width || sh !== height) {
|
|
|
- alphaBuf = PDFImage.resize(alphaBuf, mask.bpc, 1, sw, sh, width,
|
|
|
- height);
|
|
|
- }
|
|
|
- } else if (isArray(mask)) {
|
|
|
- // Color key mask: if any of the compontents are outside the range
|
|
|
- // then they should be painted.
|
|
|
- alphaBuf = new Uint8Array(width * height);
|
|
|
- var numComps = this.numComps;
|
|
|
- for (i = 0, ii = width * height; i < ii; ++i) {
|
|
|
- var opacity = 0;
|
|
|
- var imageOffset = i * numComps;
|
|
|
- for (j = 0; j < numComps; ++j) {
|
|
|
- var color = image[imageOffset + j];
|
|
|
- var maskOffset = j * 2;
|
|
|
- if (color < mask[maskOffset] || color > mask[maskOffset + 1]) {
|
|
|
- opacity = 255;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- alphaBuf[i] = opacity;
|
|
|
- }
|
|
|
- } else {
|
|
|
- error('Unknown mask format.');
|
|
|
+ nums.push(num);
|
|
|
+ var offset = parser.getObj();
|
|
|
+ if (!isInt(offset)) {
|
|
|
+ error('invalid object offset in the ObjStm stream: ' + offset);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- if (alphaBuf) {
|
|
|
- for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) {
|
|
|
- rgbaBuf[j] = alphaBuf[i];
|
|
|
+ // read stream objects for cache
|
|
|
+ for (i = 0; i < n; ++i) {
|
|
|
+ entries.push(parser.getObj());
|
|
|
+ num = nums[i];
|
|
|
+ var entry = this.entries[num];
|
|
|
+ if (entry && entry.offset === tableOffset && entry.gen === i) {
|
|
|
+ this.cache[num] = entries[i];
|
|
|
}
|
|
|
- } else {
|
|
|
- // No mask.
|
|
|
- for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) {
|
|
|
- rgbaBuf[j] = 255;
|
|
|
+ }
|
|
|
+ xrefEntry = entries[xrefEntry.gen];
|
|
|
+ if (xrefEntry === undefined) {
|
|
|
+ error('bad XRef entry for compressed object');
|
|
|
+ }
|
|
|
+ return xrefEntry;
|
|
|
+ },
|
|
|
+
|
|
|
+ fetchIfRefAsync: function XRef_fetchIfRefAsync(obj) {
|
|
|
+ if (!isRef(obj)) {
|
|
|
+ return Promise.resolve(obj);
|
|
|
+ }
|
|
|
+ return this.fetchAsync(obj);
|
|
|
+ },
|
|
|
+
|
|
|
+ fetchAsync: function XRef_fetchAsync(ref, suppressEncryption) {
|
|
|
+ var streamManager = this.stream.manager;
|
|
|
+ var xref = this;
|
|
|
+ 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).then(function () {
|
|
|
+ tryFetch(resolve, reject);
|
|
|
+ }, reject);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ reject(e);
|
|
|
}
|
|
|
- }
|
|
|
+ });
|
|
|
},
|
|
|
|
|
|
- undoPreblend: function PDFImage_undoPreblend(buffer, width, height) {
|
|
|
- var matte = this.smask && this.smask.matte;
|
|
|
- if (!matte) {
|
|
|
- return;
|
|
|
+ getCatalogObj: function XRef_getCatalogObj() {
|
|
|
+ return this.root;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ return XRef;
|
|
|
+})();
|
|
|
+
|
|
|
+/**
|
|
|
+ * A NameTree/NumberTree is like a Dict but has some advantageous properties,
|
|
|
+ * see the specification (7.9.6 and 7.9.7) for additional details.
|
|
|
+ * TODO: implement all the Dict functions and make this more efficient.
|
|
|
+ */
|
|
|
+var NameOrNumberTree = (function NameOrNumberTreeClosure() {
|
|
|
+ function NameOrNumberTree(root, xref) {
|
|
|
+ throw new Error('Cannot initialize NameOrNumberTree.');
|
|
|
+ }
|
|
|
+
|
|
|
+ NameOrNumberTree.prototype = {
|
|
|
+ getAll: function NameOrNumberTree_getAll() {
|
|
|
+ var dict = Object.create(null);
|
|
|
+ if (!this.root) {
|
|
|
+ return dict;
|
|
|
}
|
|
|
- var matteRgb = this.colorSpace.getRgb(matte, 0);
|
|
|
- var matteR = matteRgb[0];
|
|
|
- var matteG = matteRgb[1];
|
|
|
- var matteB = matteRgb[2];
|
|
|
- var length = width * height * 4;
|
|
|
- var r, g, b;
|
|
|
- for (var i = 0; i < length; i += 4) {
|
|
|
- var alpha = buffer[i + 3];
|
|
|
- if (alpha === 0) {
|
|
|
- // according formula we have to get Infinity in all components
|
|
|
- // making it white (typical paper color) should be okay
|
|
|
- buffer[i] = 255;
|
|
|
- buffer[i + 1] = 255;
|
|
|
- buffer[i + 2] = 255;
|
|
|
+ var xref = this.xref;
|
|
|
+ // Reading Name/Number tree.
|
|
|
+ var processed = new RefSet();
|
|
|
+ processed.put(this.root);
|
|
|
+ var queue = [this.root];
|
|
|
+ while (queue.length > 0) {
|
|
|
+ var i, n;
|
|
|
+ var obj = xref.fetchIfRef(queue.shift());
|
|
|
+ if (!isDict(obj)) {
|
|
|
continue;
|
|
|
}
|
|
|
- var k = 255 / alpha;
|
|
|
- r = (buffer[i] - matteR) * k + matteR;
|
|
|
- g = (buffer[i + 1] - matteG) * k + matteG;
|
|
|
- b = (buffer[i + 2] - matteB) * k + matteB;
|
|
|
- buffer[i] = r <= 0 ? 0 : r >= 255 ? 255 : r | 0;
|
|
|
- buffer[i + 1] = g <= 0 ? 0 : g >= 255 ? 255 : g | 0;
|
|
|
- buffer[i + 2] = b <= 0 ? 0 : b >= 255 ? 255 : b | 0;
|
|
|
+ if (obj.has('Kids')) {
|
|
|
+ var kids = obj.get('Kids');
|
|
|
+ for (i = 0, n = kids.length; i < n; i++) {
|
|
|
+ var kid = kids[i];
|
|
|
+ assert(!processed.has(kid),
|
|
|
+ 'Duplicate entry in "' + this._type + '" tree.');
|
|
|
+ queue.push(kid);
|
|
|
+ processed.put(kid);
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ var entries = obj.get(this._type);
|
|
|
+ if (isArray(entries)) {
|
|
|
+ for (i = 0, n = entries.length; i < n; i += 2) {
|
|
|
+ dict[xref.fetchIfRef(entries[i])] = xref.fetchIfRef(entries[i + 1]);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
+ return dict;
|
|
|
},
|
|
|
|
|
|
- createImageData: function PDFImage_createImageData(forceRGBA) {
|
|
|
- var drawWidth = this.drawWidth;
|
|
|
- var drawHeight = this.drawHeight;
|
|
|
- var imgData = { // other fields are filled in below
|
|
|
- width: drawWidth,
|
|
|
- height: drawHeight
|
|
|
- };
|
|
|
+ get: function NameOrNumberTree_get(key) {
|
|
|
+ if (!this.root) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
|
|
|
- var numComps = this.numComps;
|
|
|
- var originalWidth = this.width;
|
|
|
- var originalHeight = this.height;
|
|
|
- var bpc = this.bpc;
|
|
|
+ var xref = this.xref;
|
|
|
+ var kidsOrEntries = xref.fetchIfRef(this.root);
|
|
|
+ var loopCount = 0;
|
|
|
+ var MAX_LEVELS = 10;
|
|
|
+ var l, r, m;
|
|
|
|
|
|
- // Rows start at byte boundary.
|
|
|
- var rowBytes = (originalWidth * numComps * bpc + 7) >> 3;
|
|
|
- var imgArray;
|
|
|
+ // Perform a binary search to quickly find the entry that
|
|
|
+ // contains the key we are looking for.
|
|
|
+ while (kidsOrEntries.has('Kids')) {
|
|
|
+ if (++loopCount > MAX_LEVELS) {
|
|
|
+ warn('Search depth limit reached for "' + this._type + '" tree.');
|
|
|
+ return null;
|
|
|
+ }
|
|
|
|
|
|
- if (!forceRGBA) {
|
|
|
- // If it is a 1-bit-per-pixel grayscale (i.e. black-and-white) image
|
|
|
- // without any complications, we pass a same-sized copy to the main
|
|
|
- // thread rather than expanding by 32x to RGBA form. This saves *lots*
|
|
|
- // of memory for many scanned documents. It's also much faster.
|
|
|
- //
|
|
|
- // Similarly, if it is a 24-bit-per pixel RGB image without any
|
|
|
- // complications, we avoid expanding by 1.333x to RGBA form.
|
|
|
- var kind;
|
|
|
- if (this.colorSpace.name === 'DeviceGray' && bpc === 1) {
|
|
|
- kind = ImageKind.GRAYSCALE_1BPP;
|
|
|
- } else if (this.colorSpace.name === 'DeviceRGB' && bpc === 8 &&
|
|
|
- !this.needsDecode) {
|
|
|
- kind = ImageKind.RGB_24BPP;
|
|
|
+ var kids = kidsOrEntries.get('Kids');
|
|
|
+ if (!isArray(kids)) {
|
|
|
+ return null;
|
|
|
}
|
|
|
- if (kind && !this.smask && !this.mask &&
|
|
|
- drawWidth === originalWidth && drawHeight === originalHeight) {
|
|
|
- imgData.kind = kind;
|
|
|
|
|
|
- imgArray = this.getImageBytes(originalHeight * rowBytes);
|
|
|
- // If imgArray came from a DecodeStream, we're safe to transfer it
|
|
|
- // (and thus detach its underlying buffer) because it will constitute
|
|
|
- // the entire DecodeStream's data. But if it came from a Stream, we
|
|
|
- // need to copy it because it'll only be a portion of the Stream's
|
|
|
- // data, and the rest will be read later on.
|
|
|
- if (this.image instanceof DecodeStream) {
|
|
|
- imgData.data = imgArray;
|
|
|
+ l = 0;
|
|
|
+ r = kids.length - 1;
|
|
|
+ while (l <= r) {
|
|
|
+ m = (l + r) >> 1;
|
|
|
+ var kid = xref.fetchIfRef(kids[m]);
|
|
|
+ var limits = kid.get('Limits');
|
|
|
+
|
|
|
+ if (key < xref.fetchIfRef(limits[0])) {
|
|
|
+ r = m - 1;
|
|
|
+ } else if (key > xref.fetchIfRef(limits[1])) {
|
|
|
+ l = m + 1;
|
|
|
} else {
|
|
|
- var newArray = new Uint8Array(imgArray.length);
|
|
|
- newArray.set(imgArray);
|
|
|
- imgData.data = newArray;
|
|
|
- }
|
|
|
- if (this.needsDecode) {
|
|
|
- // Invert the buffer (which must be grayscale if we reached here).
|
|
|
- assert(kind === ImageKind.GRAYSCALE_1BPP);
|
|
|
- var buffer = imgData.data;
|
|
|
- for (var i = 0, ii = buffer.length; i < ii; i++) {
|
|
|
- buffer[i] ^= 0xff;
|
|
|
- }
|
|
|
+ kidsOrEntries = xref.fetchIfRef(kids[m]);
|
|
|
+ break;
|
|
|
}
|
|
|
- return imgData;
|
|
|
}
|
|
|
- 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;
|
|
|
+ if (l > r) {
|
|
|
+ return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- imgArray = this.getImageBytes(originalHeight * rowBytes);
|
|
|
- // imgArray can be incomplete (e.g. after CCITT fax encoding).
|
|
|
- var actualHeight = 0 | (imgArray.length / rowBytes *
|
|
|
- drawHeight / originalHeight);
|
|
|
+ // If we get here, then we have found the right entry. Now go through the
|
|
|
+ // entries in the dictionary until we find the key we're looking for.
|
|
|
+ var entries = kidsOrEntries.get(this._type);
|
|
|
+ if (isArray(entries)) {
|
|
|
+ // Perform a binary search to reduce the lookup time.
|
|
|
+ l = 0;
|
|
|
+ r = entries.length - 2;
|
|
|
+ while (l <= r) {
|
|
|
+ // Check only even indices (0, 2, 4, ...) because the
|
|
|
+ // odd indices contain the actual data.
|
|
|
+ m = (l + r) & ~1;
|
|
|
+ var currentKey = xref.fetchIfRef(entries[m]);
|
|
|
+ if (key < currentKey) {
|
|
|
+ r = m - 2;
|
|
|
+ } else if (key > currentKey) {
|
|
|
+ l = m + 2;
|
|
|
+ } else {
|
|
|
+ return xref.fetchIfRef(entries[m + 1]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ return NameOrNumberTree;
|
|
|
+})();
|
|
|
|
|
|
- var comps = this.getComponents(imgArray);
|
|
|
+var NameTree = (function NameTreeClosure() {
|
|
|
+ function NameTree(root, xref) {
|
|
|
+ this.root = root;
|
|
|
+ this.xref = xref;
|
|
|
+ this._type = 'Names';
|
|
|
+ }
|
|
|
|
|
|
- // If opacity data is present, use RGBA_32BPP form. Otherwise, use the
|
|
|
- // more compact RGB_24BPP form if allowable.
|
|
|
- var alpha01, maybeUndoPreblend;
|
|
|
- if (!forceRGBA && !this.smask && !this.mask) {
|
|
|
- imgData.kind = ImageKind.RGB_24BPP;
|
|
|
- imgData.data = new Uint8Array(drawWidth * drawHeight * 3);
|
|
|
- alpha01 = 0;
|
|
|
- maybeUndoPreblend = false;
|
|
|
- } else {
|
|
|
- imgData.kind = ImageKind.RGBA_32BPP;
|
|
|
- imgData.data = new Uint8Array(drawWidth * drawHeight * 4);
|
|
|
- alpha01 = 1;
|
|
|
- maybeUndoPreblend = true;
|
|
|
+ Util.inherit(NameTree, NameOrNumberTree, {});
|
|
|
|
|
|
- // Color key masking (opacity) must be performed before decoding.
|
|
|
- this.fillOpacity(imgData.data, drawWidth, drawHeight, actualHeight,
|
|
|
- comps);
|
|
|
+ return NameTree;
|
|
|
+})();
|
|
|
+
|
|
|
+var NumberTree = (function NumberTreeClosure() {
|
|
|
+ function NumberTree(root, xref) {
|
|
|
+ this.root = root;
|
|
|
+ this.xref = xref;
|
|
|
+ this._type = 'Nums';
|
|
|
+ }
|
|
|
+
|
|
|
+ Util.inherit(NumberTree, NameOrNumberTree, {});
|
|
|
+
|
|
|
+ return NumberTree;
|
|
|
+})();
|
|
|
+
|
|
|
+/**
|
|
|
+ * "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
|
|
|
+ * collections attributes and related files (/RF)
|
|
|
+ */
|
|
|
+var FileSpec = (function FileSpecClosure() {
|
|
|
+ function FileSpec(root, xref) {
|
|
|
+ if (!root || !isDict(root)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.xref = xref;
|
|
|
+ this.root = root;
|
|
|
+ if (root.has('FS')) {
|
|
|
+ this.fs = root.get('FS');
|
|
|
+ }
|
|
|
+ this.description = root.has('Desc') ?
|
|
|
+ stringToPDFString(root.get('Desc')) :
|
|
|
+ '';
|
|
|
+ if (root.has('RF')) {
|
|
|
+ warn('Related file specifications are not supported');
|
|
|
+ }
|
|
|
+ this.contentAvailable = true;
|
|
|
+ if (!root.has('EF')) {
|
|
|
+ this.contentAvailable = false;
|
|
|
+ warn('Non-embedded file specifications are not supported');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function pickPlatformItem(dict) {
|
|
|
+ // Look for the filename in this order:
|
|
|
+ // UF, F, Unix, Mac, DOS
|
|
|
+ if (dict.has('UF')) {
|
|
|
+ return dict.get('UF');
|
|
|
+ } else if (dict.has('F')) {
|
|
|
+ return dict.get('F');
|
|
|
+ } else if (dict.has('Unix')) {
|
|
|
+ return dict.get('Unix');
|
|
|
+ } else if (dict.has('Mac')) {
|
|
|
+ return dict.get('Mac');
|
|
|
+ } else if (dict.has('DOS')) {
|
|
|
+ return dict.get('DOS');
|
|
|
+ } else {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ FileSpec.prototype = {
|
|
|
+ get filename() {
|
|
|
+ if (!this._filename && this.root) {
|
|
|
+ var filename = pickPlatformItem(this.root) || 'unnamed';
|
|
|
+ this._filename = stringToPDFString(filename).
|
|
|
+ replace(/\\\\/g, '\\').
|
|
|
+ replace(/\\\//g, '/').
|
|
|
+ replace(/\\/g, '/');
|
|
|
+ }
|
|
|
+ return this._filename;
|
|
|
+ },
|
|
|
+ get content() {
|
|
|
+ if (!this.contentAvailable) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (!this.contentRef && this.root) {
|
|
|
+ this.contentRef = pickPlatformItem(this.root.get('EF'));
|
|
|
+ }
|
|
|
+ var content = null;
|
|
|
+ if (this.contentRef) {
|
|
|
+ var xref = this.xref;
|
|
|
+ var fileObj = xref.fetchIfRef(this.contentRef);
|
|
|
+ if (fileObj && isStream(fileObj)) {
|
|
|
+ content = fileObj.getBytes();
|
|
|
+ } else {
|
|
|
+ warn('Embedded file specification points to non-existing/invalid ' +
|
|
|
+ 'content');
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ warn('Embedded file specification does not have a content');
|
|
|
}
|
|
|
+ return content;
|
|
|
+ },
|
|
|
+ get serializable() {
|
|
|
+ return {
|
|
|
+ filename: this.filename,
|
|
|
+ content: this.content
|
|
|
+ };
|
|
|
+ }
|
|
|
+ };
|
|
|
+ return FileSpec;
|
|
|
+})();
|
|
|
|
|
|
- if (this.needsDecode) {
|
|
|
- this.decodeBuffer(comps);
|
|
|
+/**
|
|
|
+ * A helper for loading missing data in object graphs. It traverses the graph
|
|
|
+ * depth first and queues up any objects that have missing data. Once it has
|
|
|
+ * has traversed as many objects that are available it attempts to bundle the
|
|
|
+ * missing data requests and then resume from the nodes that weren't ready.
|
|
|
+ *
|
|
|
+ * NOTE: It provides protection from circular references by keeping track of
|
|
|
+ * of loaded references. However, you must be careful not to load any graphs
|
|
|
+ * that have references to the catalog or other pages since that will cause the
|
|
|
+ * entire PDF document object graph to be traversed.
|
|
|
+ */
|
|
|
+var ObjectLoader = (function() {
|
|
|
+ function mayHaveChildren(value) {
|
|
|
+ return isRef(value) || isDict(value) || isArray(value) || isStream(value);
|
|
|
+ }
|
|
|
+
|
|
|
+ function addChildren(node, nodesToVisit) {
|
|
|
+ var value;
|
|
|
+ if (isDict(node) || isStream(node)) {
|
|
|
+ var map;
|
|
|
+ if (isDict(node)) {
|
|
|
+ map = node.map;
|
|
|
+ } else {
|
|
|
+ map = node.dict.map;
|
|
|
}
|
|
|
- this.colorSpace.fillRgb(imgData.data, originalWidth, originalHeight,
|
|
|
- drawWidth, drawHeight, actualHeight, bpc, comps,
|
|
|
- alpha01);
|
|
|
- if (maybeUndoPreblend) {
|
|
|
- this.undoPreblend(imgData.data, drawWidth, actualHeight);
|
|
|
+ for (var key in map) {
|
|
|
+ value = map[key];
|
|
|
+ if (mayHaveChildren(value)) {
|
|
|
+ nodesToVisit.push(value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (isArray(node)) {
|
|
|
+ for (var i = 0, ii = node.length; i < ii; i++) {
|
|
|
+ value = node[i];
|
|
|
+ if (mayHaveChildren(value)) {
|
|
|
+ nodesToVisit.push(value);
|
|
|
+ }
|
|
|
}
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- return imgData;
|
|
|
- },
|
|
|
+ function ObjectLoader(obj, keys, xref) {
|
|
|
+ this.obj = obj;
|
|
|
+ this.keys = keys;
|
|
|
+ this.xref = xref;
|
|
|
+ this.refSet = null;
|
|
|
+ this.capability = null;
|
|
|
+ }
|
|
|
|
|
|
- fillGrayBuffer: function PDFImage_fillGrayBuffer(buffer) {
|
|
|
- var numComps = this.numComps;
|
|
|
- if (numComps !== 1) {
|
|
|
- error('Reading gray scale from a color image: ' + numComps);
|
|
|
+ ObjectLoader.prototype = {
|
|
|
+ load: function ObjectLoader_load() {
|
|
|
+ var keys = this.keys;
|
|
|
+ this.capability = createPromiseCapability();
|
|
|
+ // Don't walk the graph if all the data is already loaded.
|
|
|
+ if (!(this.xref.stream instanceof ChunkedStream) ||
|
|
|
+ this.xref.stream.getMissingChunks().length === 0) {
|
|
|
+ this.capability.resolve();
|
|
|
+ return this.capability.promise;
|
|
|
}
|
|
|
|
|
|
- var width = this.width;
|
|
|
- var height = this.height;
|
|
|
- var bpc = this.bpc;
|
|
|
+ this.refSet = new RefSet();
|
|
|
+ // Setup the initial nodes to visit.
|
|
|
+ var nodesToVisit = [];
|
|
|
+ for (var i = 0; i < keys.length; i++) {
|
|
|
+ nodesToVisit.push(this.obj[keys[i]]);
|
|
|
+ }
|
|
|
|
|
|
- // rows start at byte boundary
|
|
|
- var rowBytes = (width * numComps * bpc + 7) >> 3;
|
|
|
- var imgArray = this.getImageBytes(height * rowBytes);
|
|
|
+ this._walk(nodesToVisit);
|
|
|
+ return this.capability.promise;
|
|
|
+ },
|
|
|
|
|
|
- var comps = this.getComponents(imgArray);
|
|
|
- var i, length;
|
|
|
+ _walk: function ObjectLoader_walk(nodesToVisit) {
|
|
|
+ var nodesToRevisit = [];
|
|
|
+ var pendingRequests = [];
|
|
|
+ // DFS walk of the object graph.
|
|
|
+ while (nodesToVisit.length) {
|
|
|
+ var currentNode = nodesToVisit.pop();
|
|
|
|
|
|
- if (bpc === 1) {
|
|
|
- // inline decoding (= inversion) for 1 bpc images
|
|
|
- length = width * height;
|
|
|
- if (this.needsDecode) {
|
|
|
- // invert and scale to {0, 255}
|
|
|
- for (i = 0; i < length; ++i) {
|
|
|
- buffer[i] = (comps[i] - 1) & 255;
|
|
|
+ // Only references or chunked streams can cause missing data exceptions.
|
|
|
+ if (isRef(currentNode)) {
|
|
|
+ // Skip nodes that have already been visited.
|
|
|
+ if (this.refSet.has(currentNode)) {
|
|
|
+ continue;
|
|
|
}
|
|
|
- } else {
|
|
|
- // scale to {0, 255}
|
|
|
- for (i = 0; i < length; ++i) {
|
|
|
- buffer[i] = (-comps[i]) & 255;
|
|
|
+ try {
|
|
|
+ var ref = currentNode;
|
|
|
+ this.refSet.put(ref);
|
|
|
+ currentNode = this.xref.fetch(currentNode);
|
|
|
+ } catch (e) {
|
|
|
+ if (!(e instanceof MissingDataException)) {
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ nodesToRevisit.push(currentNode);
|
|
|
+ pendingRequests.push({ begin: e.begin, end: e.end });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (currentNode && currentNode.getBaseStreams) {
|
|
|
+ var baseStreams = currentNode.getBaseStreams();
|
|
|
+ var foundMissingData = false;
|
|
|
+ for (var i = 0; i < baseStreams.length; i++) {
|
|
|
+ var stream = baseStreams[i];
|
|
|
+ if (stream.getMissingChunks && stream.getMissingChunks().length) {
|
|
|
+ foundMissingData = true;
|
|
|
+ pendingRequests.push({
|
|
|
+ begin: stream.start,
|
|
|
+ end: stream.end
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (foundMissingData) {
|
|
|
+ nodesToRevisit.push(currentNode);
|
|
|
}
|
|
|
}
|
|
|
- return;
|
|
|
- }
|
|
|
|
|
|
- if (this.needsDecode) {
|
|
|
- this.decodeBuffer(comps);
|
|
|
- }
|
|
|
- length = width * height;
|
|
|
- // we aren't using a colorspace so we need to scale the value
|
|
|
- var scale = 255 / ((1 << bpc) - 1);
|
|
|
- for (i = 0; i < length; ++i) {
|
|
|
- buffer[i] = (scale * comps[i]) | 0;
|
|
|
+ addChildren(currentNode, nodesToVisit);
|
|
|
}
|
|
|
- },
|
|
|
|
|
|
- getImageBytes: function PDFImage_getImageBytes(length,
|
|
|
- drawWidth, drawHeight,
|
|
|
- forceRGB) {
|
|
|
- this.image.reset();
|
|
|
- this.image.drawWidth = drawWidth || this.width;
|
|
|
- this.image.drawHeight = drawHeight || this.height;
|
|
|
- this.image.forceRGB = !!forceRGB;
|
|
|
- return this.image.getBytes(length);
|
|
|
+ if (pendingRequests.length) {
|
|
|
+ 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.capability.reject);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // Everything is loaded.
|
|
|
+ this.refSet = null;
|
|
|
+ this.capability.resolve();
|
|
|
}
|
|
|
};
|
|
|
- return PDFImage;
|
|
|
-})();
|
|
|
|
|
|
-exports.PDFImage = PDFImage;
|
|
|
+ return ObjectLoader;
|
|
|
+})();
|
|
|
|
|
|
-// TODO refactor to remove dependency on colorspace.js
|
|
|
-coreColorSpace._setCoreImage(exports);
|
|
|
+exports.Catalog = Catalog;
|
|
|
+exports.ObjectLoader = ObjectLoader;
|
|
|
+exports.XRef = XRef;
|
|
|
}));
|
|
|
|
|
|
|