/* -*- 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, ColorSpace, DecodeStream, Dict, Encodings, error, ErrorFont, Font, FONT_IDENTITY_MATRIX, fontCharsToUnicode, FontFlags, ImageKind, info, isArray, isCmd, isDict, isEOF, isName, isNum, isStream, isString, JpegStream, Lexer, Metrics, MurmurHash3_64, Name, Parser, Pattern, PDFImage, PDFJS, serifFonts, stdFontMap, symbolsFonts, getTilingPatternIR, warn, Util, Promise, RefSetCache, isRef, TextRenderingMode, CMapFactory, OPS, UNSUPPORTED_FEATURES, UnsupportedManager, NormalizedUnicodes, IDENTITY_MATRIX, reverseIfRtl */ 'use strict'; var PartialEvaluator = (function PartialEvaluatorClosure() { function PartialEvaluator(pdfManager, xref, handler, pageIndex, uniquePrefix, idCounters, fontCache) { 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 processed = Object.create(null); if (resources.objId) { processed[resources.objId] = true; } var nodes = [resources]; while (nodes.length) { var key; var node = nodes.shift(); // First check the current resources for blend modes. var graphicStates = node.get('ExtGState'); if (isDict(graphicStates)) { graphicStates = graphicStates.getAll(); for (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 (key in xObjects) { var xObject = xObjects[key]; if (!isStream(xObject)) { continue; } var xResources = xObject.dict.get('Resources'); // Checking objId to detect an infinite loop. if (isDict(xResources) && (!xResources.objId || !processed[xResources.objId])) { nodes.push(xResources); if (xResources.objId) { processed[xResources.objId] = true; } } } } return false; }, buildFormXObject: function PartialEvaluator_buildFormXObject(resources, xobj, smask, operatorList, initialState) { 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, initialState); operatorList.addOp(OPS.paintFormXObjectEnd, []); if (group) { operatorList.addOp(OPS.endGroup, [groupOptions]); } }, buildPaintImageXObject: function PartialEvaluator_buildPaintImageXObject(resources, image, inline, operatorList, cacheKey, cache) { var self = this; var dict = image.dict; var w = dict.get('Width', 'W'); var h = dict.get('Height', 'H'); if (!(w && isNum(w)) || !(h && isNum(h))) { warn('Image dimensions are missing, or not numbers.'); return; } 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); var imgData, args; if (imageMask) { // This depends on a tmpCanvas being 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 canTransfer = image instanceof DecodeStream; var inverseDecode = (!!decode && decode[0] > 0); imgData = PDFImage.createMask(imgArray, width, height, canTransfer, inverseDecode); imgData.cached = true; args = [imgData]; operatorList.addOp(OPS.paintImageMaskXObject, args); if (cacheKey) { cache.key = cacheKey; cache.fn = OPS.paintImageMaskXObject; cache.args = args; } 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. 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); 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(self.handler, self.xref, resources, image, inline). then(function(imageObj) { var imgData = imageObj.createImageData(/* forceRGBA = */ false); self.handler.send('obj', [objId, self.pageIndex, 'Image', imgData], [imgData.data.buffer]); }).then(null, function (reason) { warn('Unable to decode image: ' + reason); self.handler.send('obj', [objId, self.pageIndex, 'Image', null]); }); operatorList.addOp(OPS.paintImageXObject, args); if (cacheKey) { cache.key = cacheKey; cache.fn = OPS.paintImageXObject; cache.args = args; } }, handleSMask: function PartialEvaluator_handleSmask(smask, resources, operatorList, stateManager) { var smaskContent = smask.get('G'); var smaskOptions = { subtype: smask.get('S').name, backdrop: smask.get('BC') }; this.buildFormXObject(resources, smaskContent, smaskOptions, operatorList, stateManager.state.clone()); }, 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, state) { // 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); 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, state) { var font = state.font.translated; var glyphs = font.charsToGlyphs(chars); var isAddToPathSet = !!(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, stateManager) { 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, stateManager.state); 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, stateManager); 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(); } var preEvaluatedFont = this.preEvaluateFont(font, xref); var descriptor = preEvaluatedFont.descriptor; var fontID = fontRef.num + '_' + fontRef.gen; if (isDict(descriptor)) { if (!descriptor.fontAliases) { descriptor.fontAliases = Object.create(null); } var fontAliases = descriptor.fontAliases; var hash = preEvaluatedFont.hash; if (fontAliases[hash]) { var aliasFontRef = fontAliases[hash].aliasRef; if (aliasFontRef && this.fontCache.has(aliasFontRef)) { this.fontCache.putAlias(fontRef, aliasFontRef); var cachedFont = this.fontCache.get(fontRef); return cachedFont; } } if (!fontAliases[hash]) { fontAliases[hash] = { fontID: Font.getFontID() }; } fontAliases[hash].aliasRef = fontRef; fontID = fontAliases[hash].fontID; } // Workaround for bad PDF generators that don'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, '') : fontID); if (!font.translated) { var translated; try { translated = this.translateFont(preEvaluatedFont, 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; return font; }, buildPath: function PartialEvaluator_buildPath(operatorList, fn, args) { var lastIndex = operatorList.length - 1; if (lastIndex < 0 || operatorList.fnArray[lastIndex] !== OPS.constructPath) { operatorList.addOp(OPS.constructPath, [[fn], args]); } else { var opArgs = operatorList.argsArray[lastIndex]; opArgs[0].push(fn); Array.prototype.push.apply(opArgs[1], args); } }, getOperatorList: function PartialEvaluator_getOperatorList(stream, resources, operatorList, initialState) { var self = this; var xref = this.xref; var imageCache = {}; operatorList = (operatorList || new OperatorList()); resources = (resources || Dict.empty); var xobjs = (resources.get('XObject') || Dict.empty); var patterns = (resources.get('Pattern') || Dict.empty); var stateManager = new StateManager(initialState || new EvalState()); var preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager); var operation, i, ii; while ((operation = preprocessor.read())) { var args = operation.args; var fn = operation.fn; var shading; switch (fn | 0) { 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) { shading = dict.get('Shading'); var matrix = dict.get('Matrix'); 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; if (imageCache.key === name) { operatorList.addOp(imageCache.fn, imageCache.args); args = []; continue; } var xobj = xobjs.get(name); if (xobj) { assert(isStream(xobj), 'XObject should be a stream'); var type = xobj.dict.get('Subtype'); assert(isName(type), 'XObject should have a Name subtype'); if ('Form' == type.name) { stateManager.save(); self.buildFormXObject(resources, xobj, null, operatorList, stateManager.state.clone()); args = []; stateManager.restore(); continue; } else if ('Image' == type.name) { self.buildPaintImageXObject(resources, xobj, false, operatorList, name, imageCache); 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, stateManager.state); operatorList.addDependency(loadedName); args[0] = loadedName; break; case OPS.endInlineImage: var cacheKey = args[0].cacheKey; if (cacheKey && imageCache.key === cacheKey) { operatorList.addOp(imageCache.fn, imageCache.args); args = []; continue; } self.buildPaintImageXObject(resources, args[0], true, operatorList, cacheKey, imageCache); args = []; continue; case OPS.showText: args[0] = this.handleText(args[0], stateManager.state); break; case OPS.showSpacedText: var arr = args[0]; var arrLength = arr.length; for (i = 0; i < arrLength; ++i) { if (isString(arr[i])) { arr[i] = this.handleText(arr[i], stateManager.state); } } break; case OPS.nextLineShowText: args[0] = this.handleText(args[0], stateManager.state); break; case OPS.nextLineSetSpacingShowText: args[2] = this.handleText(args[2], stateManager.state); break; case OPS.setTextRenderingMode: stateManager.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'); } 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, stateManager); args = []; continue; case OPS.moveTo: case OPS.lineTo: case OPS.curveTo: case OPS.curveTo2: case OPS.curveTo3: case OPS.closePath: self.buildPath(operatorList, fn, args); continue; } operatorList.addOp(fn, args); } // Some PDFs don't close all restores inside object/form. // Closing those for them. for (i = 0, ii = preprocessor.savedStatesDepth; i < ii; i++) { operatorList.addOp(OPS.restore, []); } return operatorList; }, getTextContent: function PartialEvaluator_getTextContent(stream, resources, stateManager) { stateManager = (stateManager || new StateManager(new TextState())); var textContent = { items: [], styles: Object.create(null) }; var bidiTexts = textContent.items; var SPACE_FACTOR = 0.35; var MULTI_SPACE_FACTOR = 1.5; var self = this; var xref = this.xref; resources = (xref.fetchIfRef(resources) || Dict.empty); // The xobj is parsed iff it's needed, e.g. if there is a `DO` cmd. var xobjs = null; var xobjsCache = {}; var preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager); var operation; var textState; function newTextChunk() { var font = textState.font; if (!(font.loadedName in textContent.styles)) { textContent.styles[font.loadedName] = { fontFamily: font.fallbackName, ascent: font.ascent, descent: font.descent, vertical: font.vertical }; } return { str: '', dir: null, width: 0, height: 0, transform: null, fontName: font.loadedName }; } function runBidi(textChunk) { var bidiResult = PDFJS.bidi(textChunk.str, -1, textState.font.vertical); textChunk.str = bidiResult.str; textChunk.dir = bidiResult.dir; return textChunk; } function handleSetFont(fontName, fontRef) { var font = textState.font = self.loadFont(fontName, fontRef, xref, resources, null).translated; textState.fontMatrix = font.fontMatrix ? font.fontMatrix : FONT_IDENTITY_MATRIX; } function buildTextGeometry(chars, textChunk) { var font = textState.font; textChunk = textChunk || newTextChunk(); if (!textChunk.transform) { // 9.4.4 Text Space Details var tsm = [textState.fontSize * textState.textHScale, 0, 0, textState.fontSize, 0, textState.textRise]; var trm = textChunk.transform = Util.transform(textState.ctm, Util.transform(textState.textMatrix, tsm)); if (!font.vertical) { textChunk.height = Math.sqrt(trm[2] * trm[2] + trm[3] * trm[3]); } else { textChunk.width = Math.sqrt(trm[0] * trm[0] + trm[1] * trm[1]); } } var width = 0; var height = 0; var glyphs = font.charsToGlyphs(chars); var defaultVMetrics = font.defaultVMetrics; for (var i = 0; i < glyphs.length; i++) { var glyph = glyphs[i]; if (!glyph) { // Previous glyph was a space. width += textState.wordSpacing * textState.textHScale; continue; } var vMetricX = null; var vMetricY = null; var glyphWidth = null; if (font.vertical) { if (glyph.vmetric) { glyphWidth = glyph.vmetric[0]; vMetricX = glyph.vmetric[1]; vMetricY = glyph.vmetric[2]; } else { glyphWidth = glyph.width; vMetricX = glyph.width * 0.5; vMetricY = defaultVMetrics[2]; } } else { glyphWidth = glyph.width; } var glyphUnicode = glyph.unicode; if (glyphUnicode in NormalizedUnicodes) { glyphUnicode = NormalizedUnicodes[glyphUnicode]; } glyphUnicode = reverseIfRtl(glyphUnicode); // The following will calculate the x and y of the individual glyphs. // if (font.vertical) { // tsm[4] -= vMetricX * Math.abs(textState.fontSize) * // textState.fontMatrix[0]; // tsm[5] -= vMetricY * textState.fontSize * // textState.fontMatrix[0]; // } // var trm = Util.transform(textState.textMatrix, tsm); // var pt = Util.applyTransform([trm[4], trm[5]], textState.ctm); // var x = pt[0]; // var y = pt[1]; var tx = 0; var ty = 0; if (!font.vertical) { var w0 = glyphWidth * textState.fontMatrix[0]; tx = (w0 * textState.fontSize + textState.charSpacing) * textState.textHScale; width += tx; } else { var w1 = glyphWidth * textState.fontMatrix[0]; ty = w1 * textState.fontSize + textState.charSpacing; height += ty; } textState.translateTextMatrix(tx, ty); textChunk.str += glyphUnicode; } var a = textState.textLineMatrix[0]; var b = textState.textLineMatrix[1]; var scaleLineX = Math.sqrt(a * a + b * b); a = textState.ctm[0]; b = textState.ctm[1]; var scaleCtmX = Math.sqrt(a * a + b * b); if (!font.vertical) { textChunk.width += width * scaleCtmX * scaleLineX; } else { textChunk.height += Math.abs(height * scaleCtmX * scaleLineX); } return textChunk; } while ((operation = preprocessor.read())) { textState = stateManager.state; var fn = operation.fn; var args = operation.args; switch (fn | 0) { case OPS.setFont: handleSetFont(args[0].name); 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.translateTextLineMatrix(args[0], args[1]); textState.textMatrix = textState.textLineMatrix.slice(); break; case OPS.setLeadingMoveText: textState.leading = -args[1]; textState.translateTextLineMatrix(args[0], args[1]); textState.textMatrix = textState.textLineMatrix.slice(); break; case OPS.nextLine: textState.carriageReturn(); break; case OPS.setTextMatrix: textState.setTextMatrix(args[0], args[1], args[2], args[3], args[4], args[5]); textState.setTextLineMatrix(args[0], args[1], args[2], args[3], args[4], args[5]); break; case OPS.setCharSpacing: textState.charSpacing = args[0]; break; case OPS.setWordSpacing: textState.wordSpacing = args[0]; break; case OPS.beginText: textState.textMatrix = IDENTITY_MATRIX.slice(); textState.textLineMatrix = IDENTITY_MATRIX.slice(); break; case OPS.showSpacedText: var items = args[0]; var textChunk = newTextChunk(); var offset; for (var j = 0, jj = items.length; j < jj; j++) { if (typeof items[j] === 'string') { buildTextGeometry(items[j], textChunk); } else { var val = items[j] / 1000; if (!textState.font.vertical) { offset = -val * textState.fontSize * textState.textHScale * textState.textMatrix[0]; textState.translateTextMatrix(offset, 0); textChunk.width += offset; } else { offset = -val * textState.fontSize * textState.textMatrix[3]; textState.translateTextMatrix(0, offset); textChunk.height += offset; } if (items[j] < 0 && textState.font.spaceWidth > 0) { var fakeSpaces = -items[j] / textState.font.spaceWidth; if (fakeSpaces > MULTI_SPACE_FACTOR) { fakeSpaces = Math.round(fakeSpaces); while (fakeSpaces--) { textChunk.str += ' '; } } else if (fakeSpaces > SPACE_FACTOR) { textChunk.str += ' '; } } } } bidiTexts.push(runBidi(textChunk)); break; case OPS.showText: bidiTexts.push(runBidi(buildTextGeometry(args[0]))); break; case OPS.nextLineShowText: textState.carriageReturn(); bidiTexts.push(runBidi(buildTextGeometry(args[0]))); break; case OPS.nextLineSetSpacingShowText: textState.wordSpacing = args[0]; textState.charSpacing = args[1]; textState.carriageReturn(); bidiTexts.push(runBidi(buildTextGeometry(args[2]))); break; case OPS.paintXObject: if (args[0].code) { break; } if (!xobjs) { xobjs = (resources.get('XObject') || Dict.empty); } var name = args[0].name; if (xobjsCache.key === name) { if (xobjsCache.texts) { Util.concatenateToArray(bidiTexts, xobjsCache.texts.items); Util.extendObj(textContent.styles, xobjsCache.texts.styles); } break; } var xobj = xobjs.get(name); if (!xobj) { break; } assert(isStream(xobj), 'XObject should be a stream'); var type = xobj.dict.get('Subtype'); assert(isName(type), 'XObject should have a Name subtype'); if ('Form' !== type.name) { xobjsCache.key = name; xobjsCache.texts = null; break; } stateManager.save(); var matrix = xobj.dict.get('Matrix'); if (isArray(matrix) && matrix.length === 6) { stateManager.transform(matrix); } var formTextContent = this.getTextContent( xobj, xobj.dict.get('Resources') || resources, stateManager ); Util.concatenateToArray(bidiTexts, formTextContent.items); Util.extendObj(textContent.styles, formTextContent.styles); stateManager.restore(); xobjsCache.key = name; xobjsCache.texts = formTextContent; 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') { handleSetFont(args[0].name); } } break; } // switch } // while return textContent; }, 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 // and depends on the font type. The base encoding and differences are // read here, but the encoding that is actually used is chosen during // glyph mapping in the font. // TODO: Loading the built in encoding in the font would allow the // differences to be merged in here not require us to hold on to it. var differences = []; var baseEncodingName = null; var encoding; if (dict.has('Encoding')) { encoding = dict.get('Encoding'); if (isDict(encoding)) { baseEncodingName = encoding.get('BaseEncoding'); baseEncodingName = (isName(baseEncodingName) ? baseEncodingName.name : null); // Load the differences between the base and original if (encoding.has('Differences')) { 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)) { baseEncodingName = encoding.name; } else { error('Encoding is not a Name nor a Dict'); } // According to table 114 if the encoding is a named encoding it must be // one of these predefined encodings. if ((baseEncodingName !== 'MacRomanEncoding' && baseEncodingName !== 'MacExpertEncoding' && baseEncodingName !== 'WinAnsiEncoding')) { baseEncodingName = null; } } if (baseEncodingName) { properties.defaultEncoding = Encodings[baseEncodingName].slice(); } else { encoding = (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 also if (!!(properties.flags & FontFlags.Symbolic)) { encoding = (!properties.file && /Symbol/i.test(properties.name) ? Encodings.SymbolSetEncoding : Encodings.MacRomanEncoding); } properties.defaultEncoding = encoding; } properties.differences = differences; properties.baseEncodingName = baseEncodingName; properties.dict = dict; }, readToUnicode: function PartialEvaluator_readToUnicode(toUnicode) { var cmapObj = toUnicode; if (isName(cmapObj)) { return CMapFactory.create(cmapObj).map; } 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 null; }, 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; var i, ii, j, jj, start, code, widths; if (properties.composite) { defaultWidth = dict.get('DW') || 1000; widths = dict.get('W'); if (widths) { for (i = 0, ii = widths.length; i < ii; i++) { start = widths[i++]; code = xref.fetchIfRef(widths[i]); if (isArray(code)) { for (j = 0, jj = code.length; j < jj; j++) { glyphsWidths[start++] = code[j]; } } else { var width = widths[++i]; for (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 (i = 0, ii = vmetrics.length; i < ii; i++) { start = vmetrics[i++]; code = xref.fetchIfRef(vmetrics[i]); if (isArray(code)) { for (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 (j = start; j <= code; j++) { glyphsVMetrics[j] = vmetric; } } } } } } else { var firstChar = properties.firstChar; widths = dict.get('Widths'); if (widths) { j = firstChar; for (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 = this.buildCharCodeToWidth(metrics.widths, properties); defaultWidth = metrics.defaultWidth; } } } // Heuristic: detection of monospace font by checking all non-zero widths var isMonospace = true; var 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; var widths = []; var 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 }; }, buildCharCodeToWidth: function PartialEvaluator_bulildCharCodeToWidth(widthsByGlyphName, properties) { var widths = Object.create(null); var differences = properties.differences; var encoding = properties.defaultEncoding; for (var charCode = 0; charCode < 256; charCode++) { if (charCode in differences && widthsByGlyphName[differences[charCode]]) { widths[charCode] = widthsByGlyphName[differences[charCode]]; continue; } if (charCode in encoding && widthsByGlyphName[encoding[charCode]]) { widths[charCode] = widthsByGlyphName[encoding[charCode]]; continue; } } return widths; }, preEvaluateFont: function PartialEvaluator_preEvaluateFont(dict, xref) { var baseDict = dict; var type = dict.get('Subtype'); assert(isName(type), 'invalid font Subtype'); var composite = false; var uint8array; 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'); assert(isName(type), 'invalid font Subtype'); composite = true; } var descriptor = dict.get('FontDescriptor'); if (descriptor) { var hash = new MurmurHash3_64(); var encoding = baseDict.getRaw('Encoding'); if (isName(encoding)) { hash.update(encoding.name); } else if (isRef(encoding)) { hash.update(encoding.num + '_' + encoding.gen); } var toUnicode = dict.get('ToUnicode') || baseDict.get('ToUnicode'); if (isStream(toUnicode)) { var stream = toUnicode.str || toUnicode; uint8array = stream.buffer ? new Uint8Array(stream.buffer.buffer, 0, stream.bufferLength) : new Uint8Array(stream.bytes.buffer, stream.start, stream.end - stream.start); hash.update(uint8array); } else if (isName(toUnicode)) { hash.update(toUnicode.name); } var widths = dict.get('Widths') || baseDict.get('Widths'); if (widths) { uint8array = new Uint8Array(new Uint32Array(widths).buffer); hash.update(uint8array); } } return { descriptor: descriptor, dict: dict, baseDict: baseDict, composite: composite, hash: hash ? hash.hexdigest() : '' }; }, translateFont: function PartialEvaluator_translateFont(preEvaluatedFont, xref) { var baseDict = preEvaluatedFont.baseDict; var dict = preEvaluatedFont.dict; var composite = preEvaluatedFont.composite; var descriptor = preEvaluatedFont.descriptor; var type = dict.get('Subtype'); var maxCharIndex = (composite ? 0xFFFF : 0xFF); var properties; 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(null); 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); properties = { type: type.name, name: baseFontName, widths: metrics.widths, defaultWidth: metrics.defaultWidth, flags: flags, firstChar: 0, lastChar: maxCharIndex }; this.extractDataStructures(dict, dict, xref, properties); properties.widths = this.buildCharCodeToWidth(metrics.widths, 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 PDFs 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 + '"'); // Workaround for cases where e.g. fontNameStr = 'Arial' and // baseFontStr = 'Arial,Bold' (needed when no font file is embedded). if (fontNameStr && baseFontStr && baseFontStr.search(fontNameStr) === 0) { fontName = baseFont; } } } fontName = (fontName || baseFont); assert(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'); } } 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.cMap = CMapFactory.create(cidEncoding, { url: PDFJS.cMapUrl, packed: PDFJS.cMapPacked }, null); properties.vertical = properties.cMap.vertical; } this.extractDataStructures(dict, baseDict, xref, properties); this.extractWidths(dict, xref, descriptor, properties); if (type.name === 'Type3') { properties.coded = true; } return new Font(fontName.name, fontFile, properties); } }; return PartialEvaluator; })(); var OperatorList = (function OperatorListClosure() { var CHUNK_SIZE = 1000; var CHUNK_SIZE_ABOUT = CHUNK_SIZE - 5; // close to chunk size 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 if (!arg.cached) { transfers.push(arg.data.buffer); } break; } } return transfers; } function OperatorList(intent, messageHandler, pageIndex) { this.messageHandler = messageHandler; this.fnArray = []; this.argsArray = []; this.dependencies = {}; this.pageIndex = pageIndex; this.intent = intent; } OperatorList.prototype = { get length() { return this.argsArray.length; }, addOp: function(fn, args) { this.fnArray.push(fn); this.argsArray.push(args); if (this.messageHandler) { if (this.fnArray.length >= CHUNK_SIZE) { this.flush(); } else if (this.fnArray.length >= CHUNK_SIZE_ABOUT && (fn === OPS.restore || fn === OPS.endText)) { // heuristic to flush on boundary of restore or endText this.flush(); } } }, 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) { new QueueOptimizer().optimize(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 }, transfers); this.dependencies = {}; this.fnArray.length = 0; this.argsArray.length = 0; } }; return OperatorList; })(); var StateManager = (function StateManagerClosure() { function StateManager(initialState) { this.state = initialState; this.stateStack = []; } StateManager.prototype = { save: function () { var old = this.state; this.stateStack.push(this.state); this.state = old.clone(); }, restore: function () { var prev = this.stateStack.pop(); if (prev) { this.state = prev; } }, transform: function (args) { this.state.ctm = Util.transform(this.state.ctm, args); } }; return StateManager; })(); var TextState = (function TextStateClosure() { function TextState() { this.ctm = new Float32Array(IDENTITY_MATRIX); this.fontSize = 0; this.font = null; this.fontMatrix = FONT_IDENTITY_MATRIX; this.textMatrix = IDENTITY_MATRIX.slice(); this.textLineMatrix = IDENTITY_MATRIX.slice(); this.charSpacing = 0; this.wordSpacing = 0; this.leading = 0; this.textHScale = 1; this.textRise = 0; } TextState.prototype = { 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; }, setTextLineMatrix: function TextState_setTextMatrix(a, b, c, d, e, f) { var m = this.textLineMatrix; 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]; }, translateTextLineMatrix: function TextState_translateTextMatrix(x, y) { var m = this.textLineMatrix; m[4] = m[0] * x + m[2] * y + m[4]; m[5] = m[1] * x + m[3] * y + m[5]; }, calcRenderMatrix: function TextState_calcRendeMatrix(ctm) { // 9.4.4 Text Space Details var tsm = [this.fontSize * this.textHScale, 0, 0, this.fontSize, 0, this.textRise]; return Util.transform(ctm, Util.transform(this.textMatrix, tsm)); }, carriageReturn: function TextState_carriageReturn() { this.translateTextLineMatrix(0, -this.leading); this.textMatrix = this.textLineMatrix.slice(); }, clone: function TextState_clone() { var clone = Object.create(this); clone.textMatrix = this.textMatrix.slice(); clone.textLineMatrix = this.textLineMatrix.slice(); clone.fontMatrix = this.fontMatrix.slice(); return clone; } }; return TextState; })(); var EvalState = (function EvalStateClosure() { function EvalState() { this.ctm = new Float32Array(IDENTITY_MATRIX); this.font = null; this.textRenderingMode = TextRenderingMode.FILL; } EvalState.prototype = { clone: function CanvasExtraState_clone() { return Object.create(this); }, }; return EvalState; })(); var EvaluatorPreprocessor = (function EvaluatorPreprocessorClosure() { // 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, stateManager) { // TODO(mduan): pass array of knownCommands rather than OP_MAP // dictionary this.parser = new Parser(new Lexer(stream, OP_MAP), false, xref); this.stateManager = stateManager; } EvaluatorPreprocessor.prototype = { get savedStatesDepth() { return this.stateManager.stateStack.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)); assert(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 }; } }, preprocessCommand: function EvaluatorPreprocessor_preprocessCommand(fn, args) { switch (fn | 0) { case OPS.save: this.stateManager.save(); break; case OPS.restore: this.stateManager.restore(); break; case OPS.transform: this.stateManager.transform(args); break; } } }; return EvaluatorPreprocessor; })(); var QueueOptimizer = (function QueueOptimizerClosure() { function addState(parentState, pattern, fn) { var state = parentState; for (var i = 0, ii = pattern.length - 1; i < ii; i++) { var item = pattern[i]; state = (state[item] || (state[item] = [])); } state[pattern[pattern.length - 1]] = fn; } function handlePaintSolidColorImageMask(index, count, fnArray, argsArray) { // Handles special case of mainly LaTeX documents which // use image masks to draw lines with the current fill style. // 'count' groups of (save, transform, paintImageMaskXObject, restore)+ // have been found at index. for (var i = 0; i < count; i++) { var arg = argsArray[index + 4 * i + 2]; var imageMask = arg.length == 1 && arg[0]; if (imageMask && imageMask.width == 1 && imageMask.height == 1 && (!imageMask.data.length || (imageMask.data.length == 1 && imageMask.data[0] === 0))) { fnArray[index + 4 * i + 2] = OPS.paintSolidColorImageMask; continue; } break; } return count - i; } var InitialState = []; addState(InitialState, [OPS.save, OPS.transform, OPS.paintInlineImageXObject, OPS.restore], function foundInlineImageGroup(context) { // 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; var fnArray = context.fnArray, argsArray = context.argsArray; var j = context.currentOperation - 3, i = j + 4; var ii = fnArray.length; for (; 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) { context.currentOperation = i - 1; return; } // 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; var q; for (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 (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 fnArray.splice(j, count * 4, OPS.paintInlineImageXObjectGroup); argsArray.splice(j, count * 4, [{ width: imgWidth, height: imgHeight, kind: ImageKind.RGBA_32BPP, data: imgData }, map]); context.currentOperation = j; }); addState(InitialState, [OPS.save, OPS.transform, OPS.paintImageMaskXObject, OPS.restore], function foundImageMaskGroup(context) { // 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; var MAX_SAME_IMAGES_IN_MASKS_BLOCK = 1000; var fnArray = context.fnArray, argsArray = context.argsArray; var j = context.currentOperation - 3, i = j + 4; var ii = fnArray.length, q; for (; i < ii && fnArray[i - 4] === fnArray[i]; i++) {} var count = (i - j) >> 2; count = handlePaintSolidColorImageMask(j, count, fnArray, argsArray); if (count < MIN_IMAGES_IN_MASKS_BLOCK) { context.currentOperation = i - 1; return; } var isSameImage = false; var transformArgs; if (argsArray[j + 1][1] === 0 && argsArray[j + 1][2] === 0) { i = j + 4; isSameImage = true; for (q = 1; q < count; q++, i += 4) { var prevTransformArgs = argsArray[i - 3]; transformArgs = argsArray[i + 1]; if (argsArray[i - 2][0] !== argsArray[i + 2][0] || prevTransformArgs[0] !== transformArgs[0] || prevTransformArgs[1] !== transformArgs[1] || prevTransformArgs[2] !== transformArgs[2] || prevTransformArgs[3] !== transformArgs[3]) { if (q < MIN_IMAGES_IN_MASKS_BLOCK) { isSameImage = false; } else { count = q; } break; // different image or transform } } } if (isSameImage) { count = Math.min(count, MAX_SAME_IMAGES_IN_MASKS_BLOCK); var positions = new Float32Array(count * 2); i = j + 1; for (q = 0; q < count; q++) { transformArgs = argsArray[i]; positions[(q << 1)] = transformArgs[4]; positions[(q << 1) + 1] = transformArgs[5]; i += 4; } // replacing queue items fnArray.splice(j, count * 4, OPS.paintImageMaskXObjectRepeat); argsArray.splice(j, count * 4, [argsArray[j + 2][0], argsArray[j + 1][0], argsArray[j + 1][3], positions]); context.currentOperation = j; } else { count = Math.min(count, MAX_IMAGES_IN_MASKS_BLOCK); var images = []; for (q = 0; q < count; q++) { transformArgs = 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: transformArgs }); } // replacing queue items fnArray.splice(j, count * 4, OPS.paintImageMaskXObjectGroup); argsArray.splice(j, count * 4, [images]); context.currentOperation = j; } }); addState(InitialState, [OPS.save, OPS.transform, OPS.paintImageXObject, OPS.restore], function (context) { var MIN_IMAGES_IN_BLOCK = 3; var MAX_IMAGES_IN_BLOCK = 1000; var fnArray = context.fnArray, argsArray = context.argsArray; var j = context.currentOperation - 3, i = j + 4; if (argsArray[j + 1][1] !== 0 || argsArray[j + 1][2] !== 0) { return; } var ii = fnArray.length; var transformArgs; for (; i + 3 < ii && fnArray[i - 4] === fnArray[i]; i += 4) { if (fnArray[i - 3] !== fnArray[i + 1] || fnArray[i - 2] !== fnArray[i + 2] || fnArray[i - 1] !== fnArray[i + 3]) { break; } if (argsArray[i - 2][0] !== argsArray[i + 2][0]) { break; // different image } var prevTransformArgs = argsArray[i - 3]; transformArgs = argsArray[i + 1]; if (prevTransformArgs[0] !== transformArgs[0] || prevTransformArgs[1] !== transformArgs[1] || prevTransformArgs[2] !== transformArgs[2] || prevTransformArgs[3] !== transformArgs[3]) { break; // different transform } } var count = Math.min((i - j) >> 2, MAX_IMAGES_IN_BLOCK); if (count < MIN_IMAGES_IN_BLOCK) { context.currentOperation = i - 1; return; } var positions = new Float32Array(count * 2); i = j + 1; for (var q = 0; q < count; q++) { transformArgs = argsArray[i]; positions[(q << 1)] = transformArgs[4]; positions[(q << 1) + 1] = transformArgs[5]; i += 4; } var args = [argsArray[j + 2][0], argsArray[j + 1][0], argsArray[j + 1][3], positions]; // replacing queue items fnArray.splice(j, count * 4, OPS.paintImageXObjectRepeat); argsArray.splice(j, count * 4, args); context.currentOperation = j; }); addState(InitialState, [OPS.beginText, OPS.setFont, OPS.setTextMatrix, OPS.showText, OPS.endText], function (context) { // moving single chars with same font into beginText/endText groups // searching for (beginText, setFont, setTextMatrix, showText, endText)+ var MIN_CHARS_IN_BLOCK = 3; var MAX_CHARS_IN_BLOCK = 1000; var fnArray = context.fnArray, argsArray = context.argsArray; var j = context.currentOperation - 4, i = j + 5; var ii = fnArray.length; for (; i < ii && fnArray[i - 5] === fnArray[i]; i++) { if (fnArray[i] === OPS.setFont) { if (argsArray[i - 5][0] !== argsArray[i][0] || argsArray[i - 5][1] !== argsArray[i][1]) { break; } } } var count = Math.min(((i - j) / 5) | 0, MAX_CHARS_IN_BLOCK); if (count < MIN_CHARS_IN_BLOCK) { context.currentOperation = i - 1; return; } if (j >= 4 && fnArray[j - 4] === fnArray[j + 1] && fnArray[j - 3] === fnArray[j + 2] && fnArray[j - 2] === fnArray[j + 3] && fnArray[j - 1] === fnArray[j + 4] && argsArray[j - 4][0] === argsArray[j + 1][0] && argsArray[j - 4][1] === argsArray[j + 1][1]) { // extending one block ahead (very first block might have 'dependency') count++; j -= 5; } var k = j + 7; i = j + 4; for (var q = 1; q < count; q++) { fnArray[i] = fnArray[k]; argsArray[i] = argsArray[k]; fnArray[i + 1] = fnArray[k + 1]; argsArray[i + 1] = argsArray[k + 1]; i += 2; k += 5; } var removed = (count - 1) * 3; fnArray.splice(i, removed); argsArray.splice(i, removed); context.currentOperation = i; }); function QueueOptimizer() {} QueueOptimizer.prototype = { optimize: function QueueOptimizer_optimize(queue) { var fnArray = queue.fnArray, argsArray = queue.argsArray; var context = { currentOperation: 0, fnArray: fnArray, argsArray: argsArray }; var i, ii = argsArray.length; var state; for (i = 0; i < ii; i++) { state = (state || InitialState)[fnArray[i]]; if (typeof state === 'function') { // we found some handler context.currentOperation = i; state = state(context); i = context.currentOperation; ii = context.fnArray.length; } } } }; return QueueOptimizer; })();