pdf.js/src/core/evaluator.js
Samuel Chantaraud 076b3433b4 Improved annotations' display/behavior.
Added an "InteractiveAnnotation" class to homogenize the annotations' structure (highlighting) and user interactions (for now, used for text and link annotations).

Text annotations:
The appearance (AP) has priority over the icon (Name).
The popup extends horizontally (up to a limit) as well as vertically.
Reduced the title's font size.
The annotation's color (C) is used to color the popup's background.
On top of the mouseover show/hide behavior, a click on the icon will lock the annotation open (for mobile purposes). It can be closed with another click on either the icon or the popup.

An annotation printing is conditioned by its "print" bit
Unsupported annotations are not displayed at all.
2014-03-07 10:48:42 -04:00

1748 lines
62 KiB
JavaScript

/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals assert, assertWellFormed, ColorSpace, Dict, Encodings, error,
ErrorFont, Font, FONT_IDENTITY_MATRIX, fontCharsToUnicode, FontFlags,
ImageKind, info, isArray, isCmd, isDict, isEOF, isName, isNum,
isStream, isString, JpegStream, Lexer, Metrics, Name, Parser,
Pattern, PDFImage, PDFJS, serifFonts, stdFontMap, symbolsFonts,
getTilingPatternIR, warn, Util, Promise, LegacyPromise,
RefSetCache, isRef, TextRenderingMode, CMapFactory, OPS,
UNSUPPORTED_FEATURES, UnsupportedManager */
'use strict';
var PartialEvaluator = (function PartialEvaluatorClosure() {
function PartialEvaluator(pdfManager, xref, handler, pageIndex,
uniquePrefix, idCounters, fontCache) {
this.state = new EvalState();
this.stateStack = [];
this.pdfManager = pdfManager;
this.xref = xref;
this.handler = handler;
this.pageIndex = pageIndex;
this.uniquePrefix = uniquePrefix;
this.idCounters = idCounters;
this.fontCache = fontCache;
}
var TILING_PATTERN = 1, SHADING_PATTERN = 2;
PartialEvaluator.prototype = {
hasBlendModes: function PartialEvaluator_hasBlendModes(resources) {
if (!isDict(resources)) {
return false;
}
var nodes = [resources];
while (nodes.length) {
var node = nodes.shift();
// First check the current resources for blend modes.
var graphicStates = node.get('ExtGState');
if (isDict(graphicStates)) {
graphicStates = graphicStates.getAll();
for (var key in graphicStates) {
var graphicState = graphicStates[key];
var bm = graphicState['BM'];
if (isName(bm) && bm.name !== 'Normal') {
return true;
}
}
}
// Descend into the XObjects to look for more resources and blend modes.
var xObjects = node.get('XObject');
if (!isDict(xObjects)) {
continue;
}
xObjects = xObjects.getAll();
for (var key in xObjects) {
var xObject = xObjects[key];
if (!isStream(xObject)) {
continue;
}
var xResources = xObject.dict.get('Resources');
if (isDict(xResources)) {
nodes.push(xResources);
}
}
}
return false;
},
buildFormXObject: function PartialEvaluator_buildFormXObject(resources,
xobj, smask,
operatorList,
state) {
var matrix = xobj.dict.get('Matrix');
var bbox = xobj.dict.get('BBox');
var group = xobj.dict.get('Group');
if (group) {
var groupOptions = {
matrix: matrix,
bbox: bbox,
smask: smask,
isolated: false,
knockout: false
};
var groupSubtype = group.get('S');
if (isName(groupSubtype) && groupSubtype.name === 'Transparency') {
groupOptions.isolated = group.get('I') || false;
groupOptions.knockout = group.get('K') || false;
var colorSpace = group.get('CS');
groupOptions.colorSpace = colorSpace ?
ColorSpace.parseToIR(colorSpace, this.xref, resources) : null;
}
operatorList.addOp(OPS.beginGroup, [groupOptions]);
}
operatorList.addOp(OPS.paintFormXObjectBegin, [matrix, bbox]);
this.getOperatorList(xobj, xobj.dict.get('Resources') || resources,
operatorList, state);
operatorList.addOp(OPS.paintFormXObjectEnd, []);
if (group) {
operatorList.addOp(OPS.endGroup, [groupOptions]);
}
},
buildPaintImageXObject: function PartialEvaluator_buildPaintImageXObject(
resources, image, inline, operatorList) {
var self = this;
var dict = image.dict;
var w = dict.get('Width', 'W');
var h = dict.get('Height', 'H');
if (PDFJS.maxImageSize !== -1 && w * h > PDFJS.maxImageSize) {
warn('Image exceeded maximum allowed size and was removed.');
return;
}
var imageMask = dict.get('ImageMask', 'IM') || false;
if (imageMask) {
// This depends on a tmpCanvas beeing filled with the
// current fillStyle, such that processing the pixel
// data can't be done here. Instead of creating a
// complete PDFImage, only read the information needed
// for later.
var width = dict.get('Width', 'W');
var height = dict.get('Height', 'H');
var bitStrideLength = (width + 7) >> 3;
var imgArray = image.getBytes(bitStrideLength * height);
var decode = dict.get('Decode', 'D');
var inverseDecode = !!decode && decode[0] > 0;
operatorList.addOp(OPS.paintImageMaskXObject,
[PDFImage.createMask(imgArray, width, height, inverseDecode)]
);
return;
}
var softMask = dict.get('SMask', 'SM') || false;
var mask = dict.get('Mask') || false;
var SMALL_IMAGE_DIMENSIONS = 200;
// Inlining small images into the queue as RGB data
if (inline && !softMask && !mask &&
!(image instanceof JpegStream) &&
(w + h) < SMALL_IMAGE_DIMENSIONS) {
var imageObj = new PDFImage(this.xref, resources, image,
inline, null, null);
// We force the use of RGBA_32BPP images here, because we can't handle
// any other kind.
var imgData = imageObj.createImageData(/* forceRGBA = */ true);
operatorList.addOp(OPS.paintInlineImageXObject, [imgData]);
return;
}
// If there is no imageMask, create the PDFImage and a lot
// of image processing can be done here.
var uniquePrefix = this.uniquePrefix || '';
var objId = 'img_' + uniquePrefix + (++this.idCounters.obj);
operatorList.addDependency(objId);
var args = [objId, w, h];
if (!softMask && !mask && image instanceof JpegStream &&
image.isNativelySupported(this.xref, resources)) {
// These JPEGs don't need any more processing so we can just send it.
operatorList.addOp(OPS.paintJpegXObject, args);
this.handler.send(
'obj', [objId, this.pageIndex, 'JpegStream', image.getIR()]);
return;
}
PDFImage.buildImage(function(imageObj) {
var imgData = imageObj.createImageData(/* forceRGBA = */ false);
self.handler.send('obj', [objId, self.pageIndex, 'Image', imgData],
null, [imgData.data.buffer]);
}, self.handler, self.xref, resources, image, inline);
operatorList.addOp(OPS.paintImageXObject, args);
},
handleSMask: function PartialEvaluator_handleSmask(smask, resources,
operatorList) {
var smaskContent = smask.get('G');
var smaskOptions = {
subtype: smask.get('S').name,
backdrop: smask.get('BC')
};
this.buildFormXObject(resources, smaskContent, smaskOptions,
operatorList);
},
handleTilingType: function PartialEvaluator_handleTilingType(
fn, args, resources, pattern, patternDict,
operatorList) {
// Create an IR of the pattern code.
var tilingOpList = this.getOperatorList(pattern,
patternDict.get('Resources') || resources);
// Add the dependencies to the parent operator list so they are resolved
// before sub operator list is executed synchronously.
operatorList.addDependencies(tilingOpList.dependencies);
operatorList.addOp(fn, getTilingPatternIR({
fnArray: tilingOpList.fnArray,
argsArray: tilingOpList.argsArray
}, patternDict, args));
},
handleSetFont: function PartialEvaluator_handleSetFont(
resources, fontArgs, fontRef, operatorList) {
// TODO(mack): Not needed?
var fontName;
if (fontArgs) {
fontArgs = fontArgs.slice();
fontName = fontArgs[0].name;
}
var self = this;
var font = this.loadFont(fontName, fontRef, this.xref, resources,
operatorList);
this.state.font = font;
var loadedName = font.loadedName;
if (!font.sent) {
var fontData = font.translated.exportData();
self.handler.send('commonobj', [
loadedName,
'Font',
fontData
]);
font.sent = true;
}
return loadedName;
},
handleText: function PartialEvaluator_handleText(chars) {
var font = this.state.font.translated;
var glyphs = font.charsToGlyphs(chars);
var isAddToPathSet = !!(this.state.textRenderingMode &
TextRenderingMode.ADD_TO_PATH_FLAG);
if (font.data && (isAddToPathSet || PDFJS.disableFontFace)) {
for (var i = 0; i < glyphs.length; i++) {
if (glyphs[i] === null) {
continue;
}
var fontChar = glyphs[i].fontChar;
if (!font.renderer.hasBuiltPath(fontChar)) {
var path = font.renderer.getPathJs(fontChar);
this.handler.send('commonobj', [
font.loadedName + '_path_' + fontChar,
'FontPath',
path
]);
}
}
}
return glyphs;
},
setGState: function PartialEvaluator_setGState(resources, gState,
operatorList, xref) {
var self = this;
// TODO(mack): This should be rewritten so that this function returns
// what should be added to the queue during each iteration
function setGStateForKey(gStateObj, key, value) {
switch (key) {
case 'Type':
break;
case 'LW':
case 'LC':
case 'LJ':
case 'ML':
case 'D':
case 'RI':
case 'FL':
case 'CA':
case 'ca':
gStateObj.push([key, value]);
break;
case 'Font':
var loadedName = self.handleSetFont(resources, null, value[0],
operatorList);
operatorList.addDependency(loadedName);
gStateObj.push([key, [loadedName, value[1]]]);
break;
case 'BM':
gStateObj.push([key, value]);
break;
case 'SMask':
if (isName(value) && value.name === 'None') {
gStateObj.push([key, false]);
break;
}
var dict = xref.fetchIfRef(value);
if (isDict(dict)) {
self.handleSMask(dict, resources, operatorList);
gStateObj.push([key, true]);
} else {
warn('Unsupported SMask type');
}
break;
// Only generate info log messages for the following since
// they are unlikey to have a big impact on the rendering.
case 'OP':
case 'op':
case 'OPM':
case 'BG':
case 'BG2':
case 'UCR':
case 'UCR2':
case 'TR':
case 'TR2':
case 'HT':
case 'SM':
case 'SA':
case 'AIS':
case 'TK':
// TODO implement these operators.
info('graphic state operator ' + key);
break;
default:
info('Unknown graphic state operator ' + key);
break;
}
}
// This array holds the converted/processed state data.
var gStateObj = [];
var gStateMap = gState.map;
for (var key in gStateMap) {
var value = gStateMap[key];
setGStateForKey(gStateObj, key, value);
}
operatorList.addOp(OPS.setGState, [gStateObj]);
},
loadFont: function PartialEvaluator_loadFont(fontName, font, xref,
resources,
parentOperatorList) {
function errorFont() {
return {
translated: new ErrorFont('Font ' + fontName + ' is not available'),
loadedName: 'g_font_error'
};
}
var fontRef;
if (font) { // Loading by ref.
assert(isRef(font));
fontRef = font;
} else { // Loading by name.
var fontRes = resources.get('Font');
if (fontRes) {
fontRef = fontRes.getRaw(fontName);
} else {
warn('fontRes not available');
return errorFont();
}
}
if (this.fontCache.has(fontRef)) {
return this.fontCache.get(fontRef);
}
font = xref.fetchIfRef(fontRef);
if (!isDict(font)) {
return errorFont();
}
// Workaround for bad PDF generators that doesn't reference fonts
// properly, i.e. by not using an object identifier.
// Check if the fontRef is a Dict (as opposed to a standard object),
// in which case we don't cache the font and instead reference it by
// fontName in font.loadedName below.
var fontRefIsDict = isDict(fontRef);
if (!fontRefIsDict) {
this.fontCache.put(fontRef, font);
}
// keep track of each font we translated so the caller can
// load them asynchronously before calling display on a page
font.loadedName = 'g_font_' + (fontRefIsDict ?
fontName.replace(/\W/g, '') : (fontRef.num + '_' + fontRef.gen));
if (!font.translated) {
var translated;
try {
translated = this.translateFont(font, xref);
} catch (e) {
UnsupportedManager.notify(UNSUPPORTED_FEATURES.font);
translated = new ErrorFont(e instanceof Error ? e.message : e);
}
font.translated = translated;
}
if (font.translated.loadCharProcs) {
var charProcs = font.get('CharProcs').getAll();
var fontResources = font.get('Resources') || resources;
var charProcKeys = Object.keys(charProcs);
var charProcOperatorList = {};
for (var i = 0, n = charProcKeys.length; i < n; ++i) {
var key = charProcKeys[i];
var glyphStream = charProcs[key];
var operatorList = this.getOperatorList(glyphStream, fontResources);
charProcOperatorList[key] = operatorList.getIR();
if (!parentOperatorList) {
continue;
}
// Add the dependencies to the parent operator list so they are
// resolved before sub operator list is executed synchronously.
parentOperatorList.addDependencies(charProcOperatorList.dependencies);
}
font.translated.charProcOperatorList = charProcOperatorList;
font.loaded = true;
} else {
font.loaded = true;
}
return font;
},
getOperatorList: function PartialEvaluator_getOperatorList(stream,
resources,
operatorList,
evaluatorState) {
var self = this;
var xref = this.xref;
var handler = this.handler;
operatorList = operatorList || new OperatorList();
resources = resources || new Dict();
var xobjs = resources.get('XObject') || new Dict();
var patterns = resources.get('Pattern') || new Dict();
var preprocessor = new EvaluatorPreprocessor(stream, xref);
if (evaluatorState) {
preprocessor.setState(evaluatorState);
}
var promise = new LegacyPromise();
var operation;
while ((operation = preprocessor.read())) {
var args = operation.args;
var fn = operation.fn;
switch (fn) {
case OPS.setStrokeColorN:
case OPS.setFillColorN:
if (args[args.length - 1].code) {
break;
}
// compile tiling patterns
var patternName = args[args.length - 1];
// SCN/scn applies patterns along with normal colors
var pattern;
if (isName(patternName) &&
(pattern = patterns.get(patternName.name))) {
var dict = isStream(pattern) ? pattern.dict : pattern;
var typeNum = dict.get('PatternType');
if (typeNum == TILING_PATTERN) {
self.handleTilingType(fn, args, resources, pattern, dict,
operatorList);
args = [];
continue;
} else if (typeNum == SHADING_PATTERN) {
var shading = dict.get('Shading');
var matrix = dict.get('Matrix');
var pattern = Pattern.parseShading(shading, matrix, xref,
resources);
args = pattern.getIR();
} else {
error('Unkown PatternType ' + typeNum);
}
}
break;
case OPS.paintXObject:
if (args[0].code) {
break;
}
// eagerly compile XForm objects
var name = args[0].name;
var xobj = xobjs.get(name);
if (xobj) {
assertWellFormed(
isStream(xobj), 'XObject should be a stream');
var type = xobj.dict.get('Subtype');
assertWellFormed(
isName(type),
'XObject should have a Name subtype'
);
if ('Form' == type.name) {
self.buildFormXObject(resources, xobj, null, operatorList,
preprocessor.getState());
args = [];
continue;
} else if ('Image' == type.name) {
self.buildPaintImageXObject(resources, xobj, false,
operatorList);
args = [];
continue;
} else {
error('Unhandled XObject subtype ' + type.name);
}
}
break;
case OPS.setFont:
// eagerly collect all fonts
var loadedName = self.handleSetFont(resources, args, null,
operatorList);
operatorList.addDependency(loadedName);
args[0] = loadedName;
break;
case OPS.endInlineImage:
self.buildPaintImageXObject(resources, args[0], true,
operatorList);
args = [];
continue;
case OPS.save:
var old = this.state;
this.stateStack.push(this.state);
this.state = old.clone();
break;
case OPS.restore:
var prev = this.stateStack.pop();
if (prev) {
this.state = prev;
}
break;
case OPS.showText:
args[0] = this.handleText(args[0]);
break;
case OPS.showSpacedText:
var arr = args[0];
var arrLength = arr.length;
for (var i = 0; i < arrLength; ++i) {
if (isString(arr[i])) {
arr[i] = this.handleText(arr[i]);
}
}
break;
case OPS.nextLineShowText:
args[0] = this.handleText(args[0]);
break;
case OPS.nextLineSetSpacingShowText:
args[2] = this.handleText(args[2]);
break;
case OPS.setTextRenderingMode:
this.state.textRenderingMode = args[0];
break;
// Parse the ColorSpace data to a raw format.
case OPS.setFillColorSpace:
case OPS.setStrokeColorSpace:
args = [ColorSpace.parseToIR(args[0], xref, resources)];
break;
case OPS.shadingFill:
var shadingRes = resources.get('Shading');
if (!shadingRes)
error('No shading resource found');
var shading = shadingRes.get(args[0].name);
if (!shading)
error('No shading object found');
var shadingFill = Pattern.parseShading(
shading, null, xref, resources);
var patternIR = shadingFill.getIR();
args = [patternIR];
fn = OPS.shadingFill;
break;
case OPS.setGState:
var dictName = args[0];
var extGState = resources.get('ExtGState');
if (!isDict(extGState) || !extGState.has(dictName.name))
break;
var gState = extGState.get(dictName.name);
self.setGState(resources, gState, operatorList, xref);
args = [];
continue;
} // switch
operatorList.addOp(fn, args);
}
// some pdf don't close all restores inside object/form
// closing those for them
for (var i = 0, ii = preprocessor.savedStatesDepth; i < ii; i++) {
operatorList.addOp(OPS.restore, []);
}
return operatorList;
},
getTextContent: function PartialEvaluator_getTextContent(
stream, resources, textState) {
textState = textState || new TextState();
var bidiTexts = [];
var SPACE_FACTOR = 0.35;
var MULTI_SPACE_FACTOR = 1.5;
var self = this;
var xref = this.xref;
function handleSetFont(fontName, fontRef) {
return self.loadFont(fontName, fontRef, xref, resources, null);
}
resources = xref.fetchIfRef(resources) || new Dict();
// The xobj is parsed iff it's needed, e.g. if there is a `DO` cmd.
var xobjs = null;
var preprocessor = new EvaluatorPreprocessor(stream, xref);
var res = resources;
var chunk = '';
var font = null;
var charSpace = 0, wordSpace = 0;
var operation;
while ((operation = preprocessor.read())) {
var fn = operation.fn;
var args = operation.args;
switch (fn) {
// TODO: Add support for SAVE/RESTORE and XFORM here.
case OPS.setFont:
font = handleSetFont(args[0].name).translated;
textState.fontSize = args[1];
break;
case OPS.setTextRise:
textState.textRise = args[0];
break;
case OPS.setHScale:
textState.textHScale = args[0] / 100;
break;
case OPS.setLeading:
textState.leading = args[0];
break;
case OPS.moveText:
textState.translateTextMatrix(args[0], args[1]);
break;
case OPS.setLeadingMoveText:
textState.leading = -args[1];
textState.translateTextMatrix(args[0], args[1]);
break;
case OPS.nextLine:
textState.translateTextMatrix(0, -textState.leading);
break;
case OPS.setTextMatrix:
textState.setTextMatrix(args[0], args[1],
args[2], args[3], args[4], args[5]);
break;
case OPS.setCharSpacing:
charSpace = args[0];
break;
case OPS.setWordSpacing:
wordSpace = args[0];
break;
case OPS.beginText:
textState.initialiseTextObj();
break;
case OPS.showSpacedText:
var items = args[0];
for (var j = 0, jj = items.length; j < jj; j++) {
if (typeof items[j] === 'string') {
chunk += fontCharsToUnicode(items[j], font);
} else if (items[j] < 0 && font.spaceWidth > 0) {
var fakeSpaces = -items[j] / font.spaceWidth;
if (fakeSpaces > MULTI_SPACE_FACTOR) {
fakeSpaces = Math.round(fakeSpaces);
while (fakeSpaces--) {
chunk += ' ';
}
} else if (fakeSpaces > SPACE_FACTOR) {
chunk += ' ';
}
}
}
break;
case OPS.showText:
chunk += fontCharsToUnicode(args[0], font);
break;
case OPS.nextLineShowText:
// For search, adding a extra white space for line breaks would be
// better here, but that causes too much spaces in the
// text-selection divs.
chunk += fontCharsToUnicode(args[0], font);
break;
case OPS.nextLineSetSpacingShowText:
// Note comment in "'"
chunk += fontCharsToUnicode(args[2], font);
break;
case OPS.paintXObject:
// Set the chunk such that the following if won't add something
// to the state.
chunk = '';
if (args[0].code) {
break;
}
if (!xobjs) {
xobjs = resources.get('XObject') || new Dict();
}
var name = args[0].name;
var xobj = xobjs.get(name);
if (!xobj)
break;
assertWellFormed(isStream(xobj), 'XObject should be a stream');
var type = xobj.dict.get('Subtype');
assertWellFormed(
isName(type),
'XObject should have a Name subtype'
);
if ('Form' !== type.name)
break;
var formTexts = this.getTextContent(
xobj,
xobj.dict.get('Resources') || resources,
textState
);
Util.concatenateToArray(bidiTexts, formTexts);
break;
case OPS.setGState:
var dictName = args[0];
var extGState = resources.get('ExtGState');
if (!isDict(extGState) || !extGState.has(dictName.name))
break;
var gsState = extGState.get(dictName.name);
for (var i = 0; i < gsState.length; i++) {
if (gsState[i] === 'Font') {
font = handleSetFont(args[0].name).translated;
}
}
break;
} // switch
if (chunk !== '') {
var bidiResult = PDFJS.bidi(chunk, -1, font.vertical);
var bidiText = {
str: bidiResult.str,
dir: bidiResult.dir
};
var renderParams = textState.calcRenderParams(preprocessor.ctm);
var fontHeight = textState.fontSize * renderParams.vScale;
var fontAscent = font.ascent ? font.ascent * fontHeight :
font.descent ? (1 + font.descent) * fontHeight : fontHeight;
bidiText.x = renderParams.renderMatrix[4] - (fontAscent *
Math.sin(renderParams.angle));
bidiText.y = renderParams.renderMatrix[5] + (fontAscent *
Math.cos(renderParams.angle));
if (bidiText.dir == 'ttb') {
bidiText.x += renderParams.vScale / 2;
bidiText.y -= renderParams.vScale;
}
bidiText.angle = renderParams.angle;
bidiText.size = fontHeight;
bidiTexts.push(bidiText);
chunk = '';
}
} // while
return bidiTexts;
},
extractDataStructures: function
partialEvaluatorExtractDataStructures(dict, baseDict,
xref, properties) {
// 9.10.2
var toUnicode = dict.get('ToUnicode') ||
baseDict.get('ToUnicode');
if (toUnicode)
properties.toUnicode = this.readToUnicode(toUnicode, xref, properties);
if (properties.composite) {
// CIDSystemInfo helps to match CID to glyphs
var cidSystemInfo = dict.get('CIDSystemInfo');
if (isDict(cidSystemInfo)) {
properties.cidSystemInfo = {
registry: cidSystemInfo.get('Registry'),
ordering: cidSystemInfo.get('Ordering'),
supplement: cidSystemInfo.get('Supplement')
};
}
var cidToGidMap = dict.get('CIDToGIDMap');
if (isStream(cidToGidMap))
properties.cidToGidMap = this.readCidToGidMap(cidToGidMap);
}
// Based on 9.6.6 of the spec the encoding can come from multiple places
// but should be prioritized in the following order:
// 1. Encoding dictionary
// 2. Encoding within font file (Type1 or Type1C)
// 3. Default (depends on font type)
// Differences applied to the above.
// Note: we don't fill in the encoding from the font file(2) here but use
// the flag overridableEncoding to signal that the font can override the
// encoding if it has one built in.
var overridableEncoding = true;
var hasEncoding = false;
var flags = properties.flags;
var differences = [];
var baseEncoding = properties.type === 'TrueType' ?
Encodings.WinAnsiEncoding :
Encodings.StandardEncoding;
// The Symbolic attribute can be misused for regular fonts
// Heuristic: we have to check if the font is a standard one and has
// Symbolic font name
if (!!(flags & FontFlags.Symbolic)) {
baseEncoding = !properties.file && /Symbol/i.test(properties.name) ?
Encodings.SymbolSetEncoding : Encodings.MacRomanEncoding;
}
if (dict.has('Encoding')) {
var encoding = dict.get('Encoding');
if (isDict(encoding)) {
var baseName = encoding.get('BaseEncoding');
if (baseName) {
overridableEncoding = false;
hasEncoding = true;
baseEncoding = Encodings[baseName.name];
}
// Load the differences between the base and original
if (encoding.has('Differences')) {
hasEncoding = true;
var diffEncoding = encoding.get('Differences');
var index = 0;
for (var j = 0, jj = diffEncoding.length; j < jj; j++) {
var data = diffEncoding[j];
if (isNum(data))
index = data;
else
differences[index++] = data.name;
}
}
} else if (isName(encoding)) {
overridableEncoding = false;
hasEncoding = true;
var currentEncoding = Encodings[encoding.name];
// Some bad PDF files contain fonts whose encoding name is not among
// the predefined encodings, causing baseEncoding to be undefined.
// In this case, fallback to using the baseEncoding as defined above
// and let the font override the encoding if one is available.
if (currentEncoding) {
baseEncoding = currentEncoding;
} else {
overridableEncoding = true;
}
} else {
error('Encoding is not a Name nor a Dict');
}
}
properties.differences = differences;
properties.baseEncoding = baseEncoding;
properties.hasEncoding = hasEncoding;
properties.overridableEncoding = overridableEncoding;
},
readToUnicode: function PartialEvaluator_readToUnicode(toUnicode, xref,
properties) {
var cmapObj = toUnicode;
var charToUnicode = [];
if (isName(cmapObj)) {
var isIdentityMap = cmapObj.name.substr(0, 9) == 'Identity-';
if (!isIdentityMap)
error('ToUnicode file cmap translation not implemented');
} else if (isStream(cmapObj)) {
var cmap = CMapFactory.create(cmapObj).map;
// Convert UTF-16BE
// NOTE: cmap can be a sparse array, so use forEach instead of for(;;)
// to iterate over all keys.
cmap.forEach(function(token, i) {
var str = [];
for (var k = 0; k < token.length; k += 2) {
var w1 = (token.charCodeAt(k) << 8) | token.charCodeAt(k + 1);
if ((w1 & 0xF800) !== 0xD800) { // w1 < 0xD800 || w1 > 0xDFFF
str.push(w1);
continue;
}
k += 2;
var w2 = (token.charCodeAt(k) << 8) | token.charCodeAt(k + 1);
str.push(((w1 & 0x3ff) << 10) + (w2 & 0x3ff) + 0x10000);
}
cmap[i] = String.fromCharCode.apply(String, str);
});
return cmap;
}
return charToUnicode;
},
readCidToGidMap: function PartialEvaluator_readCidToGidMap(cidToGidStream) {
// Extract the encoding from the CIDToGIDMap
var glyphsData = cidToGidStream.getBytes();
// Set encoding 0 to later verify the font has an encoding
var result = [];
for (var j = 0, jj = glyphsData.length; j < jj; j++) {
var glyphID = (glyphsData[j++] << 8) | glyphsData[j];
if (glyphID === 0)
continue;
var code = j >> 1;
result[code] = glyphID;
}
return result;
},
extractWidths: function PartialEvaluator_extractWidths(dict,
xref,
descriptor,
properties) {
var glyphsWidths = [];
var defaultWidth = 0;
var glyphsVMetrics = [];
var defaultVMetrics;
if (properties.composite) {
defaultWidth = dict.get('DW') || 1000;
var widths = dict.get('W');
if (widths) {
for (var i = 0, ii = widths.length; i < ii; i++) {
var start = widths[i++];
var code = xref.fetchIfRef(widths[i]);
if (isArray(code)) {
for (var j = 0, jj = code.length; j < jj; j++)
glyphsWidths[start++] = code[j];
} else {
var width = widths[++i];
for (var j = start; j <= code; j++)
glyphsWidths[j] = width;
}
}
}
if (properties.vertical) {
var vmetrics = dict.get('DW2') || [880, -1000];
defaultVMetrics = [vmetrics[1], defaultWidth * 0.5, vmetrics[0]];
vmetrics = dict.get('W2');
if (vmetrics) {
for (var i = 0, ii = vmetrics.length; i < ii; i++) {
var start = vmetrics[i++];
var code = xref.fetchIfRef(vmetrics[i]);
if (isArray(code)) {
for (var j = 0, jj = code.length; j < jj; j++)
glyphsVMetrics[start++] = [code[j++], code[j++], code[j]];
} else {
var vmetric = [vmetrics[++i], vmetrics[++i], vmetrics[++i]];
for (var j = start; j <= code; j++)
glyphsVMetrics[j] = vmetric;
}
}
}
}
} else {
var firstChar = properties.firstChar;
var widths = dict.get('Widths');
if (widths) {
var j = firstChar;
for (var i = 0, ii = widths.length; i < ii; i++)
glyphsWidths[j++] = widths[i];
defaultWidth = parseFloat(descriptor.get('MissingWidth')) || 0;
} else {
// Trying get the BaseFont metrics (see comment above).
var baseFontName = dict.get('BaseFont');
if (isName(baseFontName)) {
var metrics = this.getBaseFontMetrics(baseFontName.name);
glyphsWidths = metrics.widths;
defaultWidth = metrics.defaultWidth;
}
}
}
// Heuristic: detection of monospace font by checking all non-zero widths
var isMonospace = true, firstWidth = defaultWidth;
for (var glyph in glyphsWidths) {
var glyphWidth = glyphsWidths[glyph];
if (!glyphWidth)
continue;
if (!firstWidth) {
firstWidth = glyphWidth;
continue;
}
if (firstWidth != glyphWidth) {
isMonospace = false;
break;
}
}
if (isMonospace)
properties.flags |= FontFlags.FixedPitch;
properties.defaultWidth = defaultWidth;
properties.widths = glyphsWidths;
properties.defaultVMetrics = defaultVMetrics;
properties.vmetrics = glyphsVMetrics;
},
isSerifFont: function PartialEvaluator_isSerifFont(baseFontName) {
// Simulating descriptor flags attribute
var fontNameWoStyle = baseFontName.split('-')[0];
return (fontNameWoStyle in serifFonts) ||
(fontNameWoStyle.search(/serif/gi) !== -1);
},
getBaseFontMetrics: function PartialEvaluator_getBaseFontMetrics(name) {
var defaultWidth = 0, widths = [], monospace = false;
var lookupName = stdFontMap[name] || name;
if (!(lookupName in Metrics)) {
// Use default fonts for looking up font metrics if the passed
// font is not a base font
if (this.isSerifFont(name)) {
lookupName = 'Times-Roman';
} else {
lookupName = 'Helvetica';
}
}
var glyphWidths = Metrics[lookupName];
if (isNum(glyphWidths)) {
defaultWidth = glyphWidths;
monospace = true;
} else {
widths = glyphWidths;
}
return {
defaultWidth: defaultWidth,
monospace: monospace,
widths: widths
};
},
translateFont: function PartialEvaluator_translateFont(dict,
xref) {
var baseDict = dict;
var type = dict.get('Subtype');
assertWellFormed(isName(type), 'invalid font Subtype');
var composite = false;
if (type.name == 'Type0') {
// If font is a composite
// - get the descendant font
// - set the type according to the descendant font
// - get the FontDescriptor from the descendant font
var df = dict.get('DescendantFonts');
if (!df)
error('Descendant fonts are not specified');
dict = isArray(df) ? xref.fetchIfRef(df[0]) : df;
type = dict.get('Subtype');
assertWellFormed(isName(type), 'invalid font Subtype');
composite = true;
}
var maxCharIndex = composite ? 0xFFFF : 0xFF;
var descriptor = dict.get('FontDescriptor');
if (!descriptor) {
if (type.name == 'Type3') {
// FontDescriptor is only required for Type3 fonts when the document
// is a tagged pdf. Create a barbebones one to get by.
descriptor = new Dict();
descriptor.set('FontName', Name.get(type.name));
} else {
// Before PDF 1.5 if the font was one of the base 14 fonts, having a
// FontDescriptor was not required.
// This case is here for compatibility.
var baseFontName = dict.get('BaseFont');
if (!isName(baseFontName))
error('Base font is not specified');
// Using base font name as a font name.
baseFontName = baseFontName.name.replace(/[,_]/g, '-');
var metrics = this.getBaseFontMetrics(baseFontName);
// Simulating descriptor flags attribute
var fontNameWoStyle = baseFontName.split('-')[0];
var flags = (
this.isSerifFont(fontNameWoStyle) ? FontFlags.Serif : 0) |
(metrics.monospace ? FontFlags.FixedPitch : 0) |
(symbolsFonts[fontNameWoStyle] ? FontFlags.Symbolic :
FontFlags.Nonsymbolic);
var properties = {
type: type.name,
name: baseFontName,
widths: metrics.widths,
defaultWidth: metrics.defaultWidth,
flags: flags,
firstChar: 0,
lastChar: maxCharIndex
};
this.extractDataStructures(dict, dict, xref, properties);
return new Font(baseFontName, null, properties);
}
}
// According to the spec if 'FontDescriptor' is declared, 'FirstChar',
// 'LastChar' and 'Widths' should exist too, but some PDF encoders seem
// to ignore this rule when a variant of a standart font is used.
// TODO Fill the width array depending on which of the base font this is
// a variant.
var firstChar = dict.get('FirstChar') || 0;
var lastChar = dict.get('LastChar') || maxCharIndex;
var fontName = descriptor.get('FontName');
var baseFont = dict.get('BaseFont');
// Some bad pdf's have a string as the font name.
if (isString(fontName)) {
fontName = Name.get(fontName);
}
if (isString(baseFont)) {
baseFont = Name.get(baseFont);
}
if (type.name !== 'Type3') {
var fontNameStr = fontName && fontName.name;
var baseFontStr = baseFont && baseFont.name;
if (fontNameStr !== baseFontStr) {
info('The FontDescriptor\'s FontName is "' + fontNameStr +
'" but should be the same as the Font\'s BaseFont "' +
baseFontStr + '"');
}
}
fontName = fontName || baseFont;
assertWellFormed(isName(fontName), 'invalid font name');
var fontFile = descriptor.get('FontFile', 'FontFile2', 'FontFile3');
if (fontFile) {
if (fontFile.dict) {
var subtype = fontFile.dict.get('Subtype');
if (subtype)
subtype = subtype.name;
var length1 = fontFile.dict.get('Length1');
var length2 = fontFile.dict.get('Length2');
}
}
var properties = {
type: type.name,
name: fontName.name,
subtype: subtype,
file: fontFile,
length1: length1,
length2: length2,
loadedName: baseDict.loadedName,
composite: composite,
wideChars: composite,
fixedPitch: false,
fontMatrix: dict.get('FontMatrix') || FONT_IDENTITY_MATRIX,
firstChar: firstChar || 0,
lastChar: lastChar || maxCharIndex,
bbox: descriptor.get('FontBBox'),
ascent: descriptor.get('Ascent'),
descent: descriptor.get('Descent'),
xHeight: descriptor.get('XHeight'),
capHeight: descriptor.get('CapHeight'),
flags: descriptor.get('Flags'),
italicAngle: descriptor.get('ItalicAngle'),
coded: false
};
if (composite) {
var cidEncoding = baseDict.get('Encoding');
if (isName(cidEncoding)) {
properties.cidEncoding = cidEncoding.name;
properties.vertical = /-V$/.test(cidEncoding.name);
}
properties.cmap = CMapFactory.create(cidEncoding);
}
this.extractWidths(dict, xref, descriptor, properties);
this.extractDataStructures(dict, baseDict, xref, properties);
if (type.name === 'Type3') {
properties.coded = true;
}
return new Font(fontName.name, fontFile, properties);
}
};
PartialEvaluator.optimizeQueue =
function PartialEvaluator_optimizeQueue(queue) {
function squash(array, index, howMany, element) {
if (isArray(array)) {
array.splice(index, howMany, element);
} else {
// Replace the element.
array[index] = element;
// Shift everything after the element up.
var sub = array.subarray(index + howMany);
array.set(sub, index + 1);
}
}
var fnArray = queue.fnArray, argsArray = queue.argsArray;
// grouping paintInlineImageXObject's into paintInlineImageXObjectGroup
// searching for (save, transform, paintInlineImageXObject, restore)+
var MIN_IMAGES_IN_INLINE_IMAGES_BLOCK = 10;
var MAX_IMAGES_IN_INLINE_IMAGES_BLOCK = 200;
var MAX_WIDTH = 1000;
var IMAGE_PADDING = 1;
for (var i = 0, ii = argsArray.length; i < ii; i++) {
if (fnArray[i] === OPS.paintInlineImageXObject &&
fnArray[i - 2] === OPS.save && fnArray[i - 1] === OPS.transform &&
fnArray[i + 1] === OPS.restore) {
var j = i - 2;
for (i += 2; i < ii && fnArray[i - 4] === fnArray[i]; i++) {
}
var count = Math.min((i - j) >> 2,
MAX_IMAGES_IN_INLINE_IMAGES_BLOCK);
if (count < MIN_IMAGES_IN_INLINE_IMAGES_BLOCK) {
continue;
}
// assuming that heights of those image is too small (~1 pixel)
// packing as much as possible by lines
var maxX = 0;
var map = [], maxLineHeight = 0;
var currentX = IMAGE_PADDING, currentY = IMAGE_PADDING;
for (var q = 0; q < count; q++) {
var transform = argsArray[j + (q << 2) + 1];
var img = argsArray[j + (q << 2) + 2][0];
if (currentX + img.width > MAX_WIDTH) {
// starting new line
maxX = Math.max(maxX, currentX);
currentY += maxLineHeight + 2 * IMAGE_PADDING;
currentX = 0;
maxLineHeight = 0;
}
map.push({
transform: transform,
x: currentX, y: currentY,
w: img.width, h: img.height
});
currentX += img.width + 2 * IMAGE_PADDING;
maxLineHeight = Math.max(maxLineHeight, img.height);
}
var imgWidth = Math.max(maxX, currentX) + IMAGE_PADDING;
var imgHeight = currentY + maxLineHeight + IMAGE_PADDING;
var imgData = new Uint8Array(imgWidth * imgHeight * 4);
var imgRowSize = imgWidth << 2;
for (var q = 0; q < count; q++) {
var data = argsArray[j + (q << 2) + 2][0].data;
// copy image by lines and extends pixels into padding
var rowSize = map[q].w << 2;
var dataOffset = 0;
var offset = (map[q].x + map[q].y * imgWidth) << 2;
imgData.set(
data.subarray(0, rowSize), offset - imgRowSize);
for (var k = 0, kk = map[q].h; k < kk; k++) {
imgData.set(
data.subarray(dataOffset, dataOffset + rowSize), offset);
dataOffset += rowSize;
offset += imgRowSize;
}
imgData.set(
data.subarray(dataOffset - rowSize, dataOffset), offset);
while (offset >= 0) {
data[offset - 4] = data[offset];
data[offset - 3] = data[offset + 1];
data[offset - 2] = data[offset + 2];
data[offset - 1] = data[offset + 3];
data[offset + rowSize] = data[offset + rowSize - 4];
data[offset + rowSize + 1] = data[offset + rowSize - 3];
data[offset + rowSize + 2] = data[offset + rowSize - 2];
data[offset + rowSize + 3] = data[offset + rowSize - 1];
offset -= imgRowSize;
}
}
// replacing queue items
squash(fnArray, j, count * 4, OPS.paintInlineImageXObjectGroup);
argsArray.splice(j, count * 4,
[{width: imgWidth, height: imgHeight, kind: ImageKind.RGBA_32BPP,
data: imgData}, map]);
i = j;
ii = argsArray.length;
}
}
// grouping paintImageMaskXObject's into paintImageMaskXObjectGroup
// searching for (save, transform, paintImageMaskXObject, restore)+
var MIN_IMAGES_IN_MASKS_BLOCK = 10;
var MAX_IMAGES_IN_MASKS_BLOCK = 100;
for (var i = 0, ii = argsArray.length; i < ii; i++) {
if (fnArray[i] === OPS.paintImageMaskXObject &&
fnArray[i - 2] === OPS.save && fnArray[i - 1] === OPS.transform &&
fnArray[i + 1] === OPS.restore) {
var j = i - 2;
for (i += 2; i < ii && fnArray[i - 4] === fnArray[i]; i++) {
}
var count = Math.min((i - j) >> 2,
MAX_IMAGES_IN_MASKS_BLOCK);
if (count < MIN_IMAGES_IN_MASKS_BLOCK) {
continue;
}
var images = [];
for (var q = 0; q < count; q++) {
var transform = argsArray[j + (q << 2) + 1];
var maskParams = argsArray[j + (q << 2) + 2][0];
images.push({data: maskParams.data, width: maskParams.width,
height: maskParams.height, transform: transform});
}
// replacing queue items
squash(fnArray, j, count * 4, OPS.paintImageMaskXObjectGroup);
argsArray.splice(j, count * 4, [images]);
i = j;
ii = argsArray.length;
}
}
};
return PartialEvaluator;
})();
var OperatorList = (function OperatorListClosure() {
var CHUNK_SIZE = 100;
function getTransfers(queue) {
var transfers = [];
var fnArray = queue.fnArray, argsArray = queue.argsArray;
for (var i = 0, ii = queue.length; i < ii; i++) {
switch (fnArray[i]) {
case OPS.paintInlineImageXObject:
case OPS.paintInlineImageXObjectGroup:
case OPS.paintImageMaskXObject:
var arg = argsArray[i][0]; // first param in imgData
transfers.push(arg.data.buffer);
break;
}
}
return transfers;
}
function OperatorList(intent, messageHandler, pageIndex) {
this.messageHandler = messageHandler;
// When there isn't a message handler the fn array needs to be able to grow
// since we can't flush the operators.
if (messageHandler) {
this.fnArray = new Uint8Array(CHUNK_SIZE);
} else {
this.fnArray = [];
}
this.argsArray = [];
this.dependencies = {};
this.pageIndex = pageIndex;
this.fnIndex = 0;
this.intent = intent;
}
OperatorList.prototype = {
get length() {
return this.argsArray.length;
},
addOp: function(fn, args) {
if (this.messageHandler) {
this.fnArray[this.fnIndex++] = fn;
this.argsArray.push(args);
if (this.fnIndex >= CHUNK_SIZE) {
this.flush();
}
} else {
this.fnArray.push(fn);
this.argsArray.push(args);
}
},
addDependency: function(dependency) {
if (dependency in this.dependencies) {
return;
}
this.dependencies[dependency] = true;
this.addOp(OPS.dependency, [dependency]);
},
addDependencies: function(dependencies) {
for (var key in dependencies) {
this.addDependency(key);
}
},
addOpList: function(opList) {
Util.extendObj(this.dependencies, opList.dependencies);
for (var i = 0, ii = opList.length; i < ii; i++) {
this.addOp(opList.fnArray[i], opList.argsArray[i]);
}
},
getIR: function() {
return {
fnArray: this.fnArray,
argsArray: this.argsArray,
length: this.length
};
},
flush: function(lastChunk) {
PartialEvaluator.optimizeQueue(this);
var transfers = getTransfers(this);
this.messageHandler.send('RenderPageChunk', {
operatorList: {
fnArray: this.fnArray,
argsArray: this.argsArray,
lastChunk: lastChunk,
length: this.length
},
pageIndex: this.pageIndex,
intent: this.intent
}, null, transfers);
this.dependencies = [];
this.fnIndex = 0;
this.argsArray = [];
}
};
return OperatorList;
})();
var TextState = (function TextStateClosure() {
function TextState() {
this.fontSize = 0;
this.textMatrix = [1, 0, 0, 1, 0, 0];
this.stateStack = [];
//textState variables
this.leading = 0;
this.textHScale = 1;
this.textRise = 0;
}
TextState.prototype = {
initialiseTextObj: function TextState_initialiseTextObj() {
var m = this.textMatrix;
m[0] = 1; m[1] = 0; m[2] = 0; m[3] = 1; m[4] = 0; m[5] = 0;
},
setTextMatrix: function TextState_setTextMatrix(a, b, c, d, e, f) {
var m = this.textMatrix;
m[0] = a; m[1] = b; m[2] = c; m[3] = d; m[4] = e; m[5] = f;
},
translateTextMatrix: function TextState_translateTextMatrix(x, y) {
var m = this.textMatrix;
m[4] = m[0] * x + m[2] * y + m[4];
m[5] = m[1] * x + m[3] * y + m[5];
},
calcRenderParams: function TextState_calcRenderingParams(cm) {
var tm = this.textMatrix;
var a = this.fontSize;
var b = a * this.textHScale;
var c = this.textRise;
var vScale = Math.sqrt((tm[2] * tm[2]) + (tm[3] * tm[3]));
var angle = Math.atan2(tm[1], tm[0]);
var m0 = tm[0] * cm[0] + tm[1] * cm[2];
var m1 = tm[0] * cm[1] + tm[1] * cm[3];
var m2 = tm[2] * cm[0] + tm[3] * cm[2];
var m3 = tm[2] * cm[1] + tm[3] * cm[3];
var m4 = tm[4] * cm[0] + tm[5] * cm[2] + cm[4];
var m5 = tm[4] * cm[1] + tm[5] * cm[3] + cm[5];
var renderMatrix = [
b * m0,
b * m1,
a * m2,
a * m3,
c * m2 + m4,
c * m3 + m5
];
return {
renderMatrix: renderMatrix,
vScale: vScale,
angle: angle
};
},
};
return TextState;
})();
var EvalState = (function EvalStateClosure() {
function EvalState() {
this.font = null;
this.textRenderingMode = TextRenderingMode.FILL;
}
EvalState.prototype = {
clone: function CanvasExtraState_clone() {
return Object.create(this);
},
};
return EvalState;
})();
var EvaluatorPreprocessor = (function EvaluatorPreprocessor() {
// Specifies properties for each command
//
// If variableArgs === true: [0, `numArgs`] expected
// If variableArgs === false: exactly `numArgs` expected
var OP_MAP = {
// Graphic state
w: { id: OPS.setLineWidth, numArgs: 1, variableArgs: false },
J: { id: OPS.setLineCap, numArgs: 1, variableArgs: false },
j: { id: OPS.setLineJoin, numArgs: 1, variableArgs: false },
M: { id: OPS.setMiterLimit, numArgs: 1, variableArgs: false },
d: { id: OPS.setDash, numArgs: 2, variableArgs: false },
ri: { id: OPS.setRenderingIntent, numArgs: 1, variableArgs: false },
i: { id: OPS.setFlatness, numArgs: 1, variableArgs: false },
gs: { id: OPS.setGState, numArgs: 1, variableArgs: false },
q: { id: OPS.save, numArgs: 0, variableArgs: false },
Q: { id: OPS.restore, numArgs: 0, variableArgs: false },
cm: { id: OPS.transform, numArgs: 6, variableArgs: false },
// Path
m: { id: OPS.moveTo, numArgs: 2, variableArgs: false },
l: { id: OPS.lineTo, numArgs: 2, variableArgs: false },
c: { id: OPS.curveTo, numArgs: 6, variableArgs: false },
v: { id: OPS.curveTo2, numArgs: 4, variableArgs: false },
y: { id: OPS.curveTo3, numArgs: 4, variableArgs: false },
h: { id: OPS.closePath, numArgs: 0, variableArgs: false },
re: { id: OPS.rectangle, numArgs: 4, variableArgs: false },
S: { id: OPS.stroke, numArgs: 0, variableArgs: false },
s: { id: OPS.closeStroke, numArgs: 0, variableArgs: false },
f: { id: OPS.fill, numArgs: 0, variableArgs: false },
F: { id: OPS.fill, numArgs: 0, variableArgs: false },
'f*': { id: OPS.eoFill, numArgs: 0, variableArgs: false },
B: { id: OPS.fillStroke, numArgs: 0, variableArgs: false },
'B*': { id: OPS.eoFillStroke, numArgs: 0, variableArgs: false },
b: { id: OPS.closeFillStroke, numArgs: 0, variableArgs: false },
'b*': { id: OPS.closeEOFillStroke, numArgs: 0, variableArgs: false },
n: { id: OPS.endPath, numArgs: 0, variableArgs: false },
// Clipping
W: { id: OPS.clip, numArgs: 0, variableArgs: false },
'W*': { id: OPS.eoClip, numArgs: 0, variableArgs: false },
// Text
BT: { id: OPS.beginText, numArgs: 0, variableArgs: false },
ET: { id: OPS.endText, numArgs: 0, variableArgs: false },
Tc: { id: OPS.setCharSpacing, numArgs: 1, variableArgs: false },
Tw: { id: OPS.setWordSpacing, numArgs: 1, variableArgs: false },
Tz: { id: OPS.setHScale, numArgs: 1, variableArgs: false },
TL: { id: OPS.setLeading, numArgs: 1, variableArgs: false },
Tf: { id: OPS.setFont, numArgs: 2, variableArgs: false },
Tr: { id: OPS.setTextRenderingMode, numArgs: 1, variableArgs: false },
Ts: { id: OPS.setTextRise, numArgs: 1, variableArgs: false },
Td: { id: OPS.moveText, numArgs: 2, variableArgs: false },
TD: { id: OPS.setLeadingMoveText, numArgs: 2, variableArgs: false },
Tm: { id: OPS.setTextMatrix, numArgs: 6, variableArgs: false },
'T*': { id: OPS.nextLine, numArgs: 0, variableArgs: false },
Tj: { id: OPS.showText, numArgs: 1, variableArgs: false },
TJ: { id: OPS.showSpacedText, numArgs: 1, variableArgs: false },
'\'': { id: OPS.nextLineShowText, numArgs: 1, variableArgs: false },
'"': { id: OPS.nextLineSetSpacingShowText, numArgs: 3,
variableArgs: false },
// Type3 fonts
d0: { id: OPS.setCharWidth, numArgs: 2, variableArgs: false },
d1: { id: OPS.setCharWidthAndBounds, numArgs: 6, variableArgs: false },
// Color
CS: { id: OPS.setStrokeColorSpace, numArgs: 1, variableArgs: false },
cs: { id: OPS.setFillColorSpace, numArgs: 1, variableArgs: false },
SC: { id: OPS.setStrokeColor, numArgs: 4, variableArgs: true },
SCN: { id: OPS.setStrokeColorN, numArgs: 33, variableArgs: true },
sc: { id: OPS.setFillColor, numArgs: 4, variableArgs: true },
scn: { id: OPS.setFillColorN, numArgs: 33, variableArgs: true },
G: { id: OPS.setStrokeGray, numArgs: 1, variableArgs: false },
g: { id: OPS.setFillGray, numArgs: 1, variableArgs: false },
RG: { id: OPS.setStrokeRGBColor, numArgs: 3, variableArgs: false },
rg: { id: OPS.setFillRGBColor, numArgs: 3, variableArgs: false },
K: { id: OPS.setStrokeCMYKColor, numArgs: 4, variableArgs: false },
k: { id: OPS.setFillCMYKColor, numArgs: 4, variableArgs: false },
// Shading
sh: { id: OPS.shadingFill, numArgs: 1, variableArgs: false },
// Images
BI: { id: OPS.beginInlineImage, numArgs: 0, variableArgs: false },
ID: { id: OPS.beginImageData, numArgs: 0, variableArgs: false },
EI: { id: OPS.endInlineImage, numArgs: 1, variableArgs: false },
// XObjects
Do: { id: OPS.paintXObject, numArgs: 1, variableArgs: false },
MP: { id: OPS.markPoint, numArgs: 1, variableArgs: false },
DP: { id: OPS.markPointProps, numArgs: 2, variableArgs: false },
BMC: { id: OPS.beginMarkedContent, numArgs: 1, variableArgs: false },
BDC: { id: OPS.beginMarkedContentProps, numArgs: 2,
variableArgs: false },
EMC: { id: OPS.endMarkedContent, numArgs: 0, variableArgs: false },
// Compatibility
BX: { id: OPS.beginCompat, numArgs: 0, variableArgs: false },
EX: { id: OPS.endCompat, numArgs: 0, variableArgs: false },
// (reserved partial commands for the lexer)
BM: null,
BD: null,
'true': null,
fa: null,
fal: null,
fals: null,
'false': null,
nu: null,
nul: null,
'null': null
};
function EvaluatorPreprocessor(stream, xref) {
// TODO(mduan): pass array of knownCommands rather than OP_MAP
// dictionary
this.parser = new Parser(new Lexer(stream, OP_MAP), false, xref);
this.ctm = new Float32Array([1, 0, 0, 1, 0, 0]);
this.savedStates = [];
}
EvaluatorPreprocessor.prototype = {
get savedStatesDepth() {
return this.savedStates.length;
},
read: function EvaluatorPreprocessor_read() {
var args = [];
while (true) {
var obj = this.parser.getObj();
if (isEOF(obj)) {
return null; // no more commands
}
if (!isCmd(obj)) {
// argument
if (obj !== null && obj !== undefined) {
args.push(obj instanceof Dict ? obj.getAll() : obj);
assertWellFormed(args.length <= 33, 'Too many arguments');
}
continue;
}
var cmd = obj.cmd;
// Check that the command is valid
var opSpec = OP_MAP[cmd];
if (!opSpec) {
warn('Unknown command "' + cmd + '"');
continue;
}
var fn = opSpec.id;
// Validate the number of arguments for the command
if (opSpec.variableArgs) {
if (args.length > opSpec.numArgs) {
info('Command ' + fn + ': expected [0,' + opSpec.numArgs +
'] args, but received ' + args.length + ' args');
}
} else {
if (args.length < opSpec.numArgs) {
// If we receive too few args, it's not possible to possible
// to execute the command, so skip the command
info('Command ' + fn + ': because expected ' +
opSpec.numArgs + ' args, but received ' + args.length +
' args; skipping');
args = [];
continue;
} else if (args.length > opSpec.numArgs) {
info('Command ' + fn + ': expected ' + opSpec.numArgs +
' args, but received ' + args.length + ' args');
}
}
// TODO figure out how to type-check vararg functions
this.preprocessCommand(fn, args);
return {fn: fn, args: args};
}
},
getState: function EvaluatorPreprocessor_getState() {
return {
ctm: this.ctm
};
},
setState: function EvaluatorPreprocessor_setState(state) {
this.ctm = state.ctm;
},
preprocessCommand: function EvaluatorPreprocessor_preprocessCommand(fn,
args) {
switch (fn | 0) {
case OPS.save:
this.savedStates.push(this.getState());
break;
case OPS.restore:
var previousState = this.savedStates.pop();
if (previousState) {
this.setState(previousState);
}
break;
case OPS.transform:
var ctm = this.ctm;
var m = new Float32Array(6);
m[0] = ctm[0] * args[0] + ctm[2] * args[1];
m[1] = ctm[1] * args[0] + ctm[3] * args[1];
m[2] = ctm[0] * args[2] + ctm[2] * args[3];
m[3] = ctm[1] * args[2] + ctm[3] * args[3];
m[4] = ctm[0] * args[4] + ctm[2] * args[5] + ctm[4];
m[5] = ctm[1] * args[4] + ctm[3] * args[5] + ctm[5];
this.ctm = m;
break;
}
}
};
return EvaluatorPreprocessor;
})();