diff --git a/src/evaluator.js b/src/evaluator.js index 6756c9b64..3da4322c0 100644 --- a/src/evaluator.js +++ b/src/evaluator.js @@ -30,101 +30,104 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { this.fontIdCounter = 0; } + // Specifies properties for each command + // + // If variableArgs === true: [0, `numArgs`] expected + // If variableArgs === false: exactly `numArgs` expected var OP_MAP = { - // Graphics state - w: 'setLineWidth', - J: 'setLineCap', - j: 'setLineJoin', - M: 'setMiterLimit', - d: 'setDash', - ri: 'setRenderingIntent', - i: 'setFlatness', - gs: 'setGState', - q: 'save', - Q: 'restore', - cm: 'transform', + // Graphic state + w: { fnName: 'setLineWidth', numArgs: 1, variableArgs: false }, + J: { fnName: 'setLineCap', numArgs: 1, variableArgs: false }, + j: { fnName: 'setLineJoin', numArgs: 1, variableArgs: false }, + M: { fnName: 'setMiterLimit', numArgs: 1, variableArgs: false }, + d: { fnName: 'setDash', numArgs: 2, variableArgs: false }, + ri: { fnName: 'setRenderingIntent', numArgs: 1, variableArgs: false }, + i: { fnName: 'setFlatness', numArgs: 1, variableArgs: false }, + gs: { fnName: 'setGState', numArgs: 1, variableArgs: false }, + q: { fnName: 'save', numArgs: 0, variableArgs: false }, + Q: { fnName: 'restore', numArgs: 0, variableArgs: false }, + cm: { fnName: 'transform', numArgs: 6, variableArgs: false }, // Path - m: 'moveTo', - l: 'lineTo', - c: 'curveTo', - v: 'curveTo2', - y: 'curveTo3', - h: 'closePath', - re: 'rectangle', - S: 'stroke', - s: 'closeStroke', - f: 'fill', - F: 'fill', - 'f*': 'eoFill', - B: 'fillStroke', - 'B*': 'eoFillStroke', - b: 'closeFillStroke', - 'b*': 'closeEOFillStroke', - n: 'endPath', + m: { fnName: 'moveTo', numArgs: 2, variableArgs: false }, + l: { fnName: 'lineTo', numArgs: 2, variableArgs: false }, + c: { fnName: 'curveTo', numArgs: 6, variableArgs: false }, + v: { fnName: 'curveTo2', numArgs: 4, variableArgs: false }, + y: { fnName: 'curveTo3', numArgs: 4, variableArgs: false }, + h: { fnName: 'closePath', numArgs: 0, variableArgs: false }, + re: { fnName: 'rectangle', numArgs: 4, variableArgs: false }, + S: { fnName: 'stroke', numArgs: 0, variableArgs: false }, + s: { fnName: 'closeStroke', numArgs: 0, variableArgs: false }, + f: { fnName: 'fill', numArgs: 0, variableArgs: false }, + F: { fnName: 'fill', numArgs: 0, variableArgs: false }, + 'f*': { fnName: 'eoFill', numArgs: 0, variableArgs: false }, + B: { fnName: 'fillStroke', numArgs: 0, variableArgs: false }, + 'B*': { fnName: 'eoFillStroke', numArgs: 0, variableArgs: false }, + b: { fnName: 'closeFillStroke', numArgs: 0, variableArgs: false }, + 'b*': { fnName: 'closeEOFillStroke', numArgs: 0, variableArgs: false }, + n: { fnName: 'endPath', numArgs: 0, variableArgs: false }, // Clipping - W: 'clip', - 'W*': 'eoClip', + W: { fnName: 'clip', numArgs: 0, variableArgs: false }, + 'W*': { fnName: 'eoClip', numArgs: 0, variableArgs: false }, // Text - BT: 'beginText', - ET: 'endText', - Tc: 'setCharSpacing', - Tw: 'setWordSpacing', - Tz: 'setHScale', - TL: 'setLeading', - Tf: 'setFont', - Tr: 'setTextRenderingMode', - Ts: 'setTextRise', - Td: 'moveText', - TD: 'setLeadingMoveText', - Tm: 'setTextMatrix', - 'T*': 'nextLine', - Tj: 'showText', - TJ: 'showSpacedText', - "'": 'nextLineShowText', - '"': 'nextLineSetSpacingShowText', + BT: { fnName: 'beginText', numArgs: 0, variableArgs: false }, + ET: { fnName: 'endText', numArgs: 0, variableArgs: false }, + Tc: { fnName: 'setCharSpacing', numArgs: 1, variableArgs: false }, + Tw: { fnName: 'setWordSpacing', numArgs: 1, variableArgs: false }, + Tz: { fnName: 'setHScale', numArgs: 1, variableArgs: false }, + TL: { fnName: 'setLeading', numArgs: 1, variableArgs: false }, + Tf: { fnName: 'setFont', numArgs: 2, variableArgs: false }, + Tr: { fnName: 'setTextRenderingMode', numArgs: 1, variableArgs: false }, + Ts: { fnName: 'setTextRise', numArgs: 1, variableArgs: false }, + Td: { fnName: 'moveText', numArgs: 2, variableArgs: false }, + TD: { fnName: 'setLeadingMoveText', numArgs: 2, variableArgs: false }, + Tm: { fnName: 'setTextMatrix', numArgs: 6, variableArgs: false }, + 'T*': { fnName: 'nextLine', numArgs: 0, variableArgs: false }, + Tj: { fnName: 'showText', numArgs: 1, variableArgs: false }, + TJ: { fnName: 'showSpacedText', numArgs: 1, variableArgs: false }, + '\'': { fnName: 'nextLineShowText', numArgs: 1, variableArgs: false }, + '"': { fnName: 'nextLineSetSpacingShowText', numArgs: 3, + variableArgs: false }, // Type3 fonts - d0: 'setCharWidth', - d1: 'setCharWidthAndBounds', + d0: { fnName: 'setCharWidth', numArgs: 2, variableArgs: false }, + d1: { fnName: 'setCharWidthAndBounds', numArgs: 6, variableArgs: false }, // Color - CS: 'setStrokeColorSpace', - cs: 'setFillColorSpace', - SC: 'setStrokeColor', - SCN: 'setStrokeColorN', - sc: 'setFillColor', - scn: 'setFillColorN', - G: 'setStrokeGray', - g: 'setFillGray', - RG: 'setStrokeRGBColor', - rg: 'setFillRGBColor', - K: 'setStrokeCMYKColor', - k: 'setFillCMYKColor', + CS: { fnName: 'setStrokeColorSpace', numArgs: 1, variableArgs: false }, + cs: { fnName: 'setFillColorSpace', numArgs: 1, variableArgs: false }, + SC: { fnName: 'setStrokeColor', numArgs: 4, variableArgs: true }, + SCN: { fnName: 'setStrokeColorN', numArgs: 33, variableArgs: true }, + sc: { fnName: 'setFillColor', numArgs: 4, variableArgs: true }, + scn: { fnName: 'setFillColorN', numArgs: 33, variableArgs: true }, + G: { fnName: 'setStrokeGray', numArgs: 1, variableArgs: false }, + g: { fnName: 'setFillGray', numArgs: 1, variableArgs: false }, + RG: { fnName: 'setStrokeRGBColor', numArgs: 3, variableArgs: false }, + rg: { fnName: 'setFillRGBColor', numArgs: 3, variableArgs: false }, + K: { fnName: 'setStrokeCMYKColor', numArgs: 4, variableArgs: false }, + k: { fnName: 'setFillCMYKColor', numArgs: 4, variableArgs: false }, // Shading - sh: 'shadingFill', + sh: { fnName: 'shadingFill', numArgs: 1, variableArgs: false }, // Images - BI: 'beginInlineImage', - ID: 'beginImageData', - EI: 'endInlineImage', + BI: { fnName: 'beginInlineImage', numArgs: 0, variableArgs: false }, + ID: { fnName: 'beginImageData', numArgs: 0, variableArgs: false }, + EI: { fnName: 'endInlineImage', numArgs: 0, variableArgs: false }, // XObjects - Do: 'paintXObject', - - // Marked content - MP: 'markPoint', - DP: 'markPointProps', - BMC: 'beginMarkedContent', - BDC: 'beginMarkedContentProps', - EMC: 'endMarkedContent', + Do: { fnName: 'paintXObject', numArgs: 1, variableArgs: false }, + MP: { fnName: 'markPoint', numArgs: 1, variableArgs: false }, + DP: { fnName: 'markPointProps', numArgs: 2, variableArgs: false }, + BMC: { fnName: 'beginMarkedContent', numArgs: 1, variableArgs: false }, + BDC: { fnName: 'beginMarkedContentProps', numArgs: 2, variableArgs: false }, + EMC: { fnName: 'endMarkedContent', numArgs: 0, variableArgs: false }, // Compatibility - BX: 'beginCompat', - EX: 'endCompat', + BX: { fnName: 'beginCompat', numArgs: 0, variableArgs: false }, + EX: { fnName: 'endCompat', numArgs: 0, variableArgs: false }, // (reserved partial commands for the lexer) BM: null, @@ -314,6 +317,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { resources = resources || new Dict(); var xobjs = resources.get('XObject') || new Dict(); var patterns = resources.get('Pattern') || new Dict(); + // TODO(mduan): pass array of knownCommands rather than OP_MAP + // dictionary var parser = new Parser(new Lexer(stream, OP_MAP), false, xref); var res = resources; var args = [], obj; @@ -321,13 +326,42 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { while (true) { obj = parser.getObj(); - if (isEOF(obj)) + if (isEOF(obj)) { break; + } if (isCmd(obj)) { var cmd = obj.cmd; - var fn = OP_MAP[cmd]; - assertWellFormed(fn, 'Unknown command "' + cmd + '"'); + + // Check that the command is valid + var opSpec = OP_MAP[cmd]; + if (!opSpec) { + warn('Unknown command "' + cmd + '"'); + continue; + } + + var fn = opSpec.fnName; + + // Validate the number of arguments for the command + if (opSpec.variableArgs) { + if (args.length > opSpec.numArgs) { + info('Command ' + fn + ': expected [0,' + opSpec.numArgs + + '] args, but received ' + args.length + ' args'); + } + } else { + if (args.length < opSpec.numArgs) { + // If we receive too few args, it's not possible to possible + // to execute the command, so skip the command + info('Command ' + fn + ': because expected ' + opSpec.numArgs + + ' args, but received ' + args.length + ' args; skipping'); + args = []; + continue; + } else if (args.length > opSpec.numArgs) { + info('Command ' + fn + ': expected ' + opSpec.numArgs + + ' args, but received ' + args.length + ' args'); + } + } + // TODO figure out how to type-check vararg functions if ((cmd == 'SCN' || cmd == 'scn') && !args[args.length - 1].code) { @@ -509,8 +543,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { argsArray.push(args); args = []; } else if (obj != null) { - assertWellFormed(args.length <= 33, 'Too many arguments'); args.push(obj instanceof Dict ? obj.getAll() : obj); + assertWellFormed(args.length <= 33, 'Too many arguments'); } } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index a6a6eac10..050b533fe 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -1,6 +1,7 @@ *.pdf !tracemonkey.pdf +!issue2391-1.pdf !ArabicCIDTrueType.pdf !ThuluthFeatures.pdf !arial_unicode_ab_cidfont.pdf diff --git a/test/pdfs/issue2391-1.pdf b/test/pdfs/issue2391-1.pdf new file mode 100644 index 000000000..bf0c4da75 --- /dev/null +++ b/test/pdfs/issue2391-1.pdf @@ -0,0 +1,131 @@ +%PDF-1.4 +%âãÏÓ +1 0 obj +<< +/Pages 2 0 R +/Metadata 3 0 R +/Type /Catalog +>> +endobj +2 0 obj +<< +/Kids [4 0 R] +/Count 1 +/Type /Pages +>> +endobj +4 0 obj +<< +/Rotate 0 +/Parent 2 0 R +/Resources +<< +/ExtGState 5 0 R +/Font 6 0 R +/ProcSet [/PDF /Text] +>> +/MediaBox [0 0 595 842] +/Contents 7 0 R +/Type /Page +>> +endobj +7 0 obj +<< +/Length 148 +>> +stream +q 0.1 0 0 0.1 0 0 cm +/R7 gs +0 g +q +10 0 0 10 0 0 cm BT +undefined 10 Tf +/R8 10 Tf +1 0 0 1 29 805 Tm +(test command with wrong number of args)Tj +ET +Q +Q + +endstream +endobj +8 0 obj +<< +/Type /ExtGState +/OPM 1 +>> +endobj +9 0 obj +<< +/BaseFont /Courier +/Subtype /Type1 +/Type /Font +>> +endobj +5 0 obj +<< +/R7 8 0 R +>> +endobj +6 0 obj +<< +/R8 9 0 R +>> +endobj +3 0 obj +<< +/Subtype /XML +/Length 1421 +/Type /Metadata +>> +stream + + + + + +2013-01-10T17:46:10-08:00 +2013-01-10T17:46:10-08:00 +GNU Enscript 1.6.6 + +Enscript OutputMack Duan + + + + + +endstream +endobj +10 0 obj +<< +/Creator (GNU Enscript 1.6.6) +/Title (Enscript Output) +/Producer (GPL Ghostscript 9.05) +/Author (Mack Duan) +/ModDate (D:20130110174610-08'00') +/CreationDate (D:20130110174610-08'00') +>> +endobj xref +0 11 +0000000000 65535 f +0000000015 00000 n +0000000082 00000 n +0000000694 00000 n +0000000141 00000 n +0000000628 00000 n +0000000661 00000 n +0000000309 00000 n +0000000511 00000 n +0000000558 00000 n +0000002200 00000 n +trailer + +<< +/Info 10 0 R +/Root 1 0 R +/Size 11 +/ID [<5807376a23851d4cc6096009432892a3> <5807376a23851d4cc6096009432892a3>] +>> +startxref +2406 +%%EOF diff --git a/test/test_manifest.json b/test/test_manifest.json index 2563be202..14026c9e2 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -17,6 +17,13 @@ "rounds": 1, "type": "text" }, + { "id": "issue2391-1", + "file": "pdfs/issue2391-1.pdf", + "md5": "25ae9cb959612e7b343b55da63af2716", + "rounds": 1, + "pageLimit": 1, + "type": "load" + }, { "id": "html5-canvas-cheat-sheet-load", "file": "pdfs/canvas.pdf", "md5": "59510028561daf62e00bf9f6f066b033", diff --git a/test/unit/evaluator_spec.js b/test/unit/evaluator_spec.js index e31a525ac..f1f41cf37 100644 --- a/test/unit/evaluator_spec.js +++ b/test/unit/evaluator_spec.js @@ -32,13 +32,12 @@ describe('evaluator', function() { var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), 'prefix'); var stream = new StringStream('qTT'); - var thrown = false; - try { - evaluator.getOperatorList(stream, new ResourcesMock(), []); - } catch (e) { - thrown = e; - } - expect(thrown).toNotEqual(false); + var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); + + expect(!!result.fnArray && !!result.argsArray).toEqual(true); + expect(result.fnArray.length).toEqual(1); + expect(result.fnArray[0]).toEqual('save'); + expect(result.argsArray[0].length).toEqual(0); }); it('should handle one operations', function() { @@ -84,14 +83,14 @@ describe('evaluator', function() { 'prefix'); var resources = new ResourcesMock(); resources.Res1 = {}; - var stream = new StringStream('B*BBMC'); + var stream = new StringStream('B*Bf*'); var result = evaluator.getOperatorList(stream, resources, []); expect(!!result.fnArray && !!result.argsArray).toEqual(true); expect(result.fnArray.length).toEqual(3); expect(result.fnArray[0]).toEqual('eoFillStroke'); expect(result.fnArray[1]).toEqual('fillStroke'); - expect(result.fnArray[2]).toEqual('beginMarkedContent'); + expect(result.fnArray[2]).toEqual('eoFill'); }); it('should handle glued operations and operands', function() { @@ -112,19 +111,53 @@ describe('evaluator', function() { it('should handle glued operations and literals', function() { var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), 'prefix'); - var stream = new StringStream('trueifalserinulli'); + var stream = new StringStream('trueifalserinullq'); var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); expect(!!result.fnArray && !!result.argsArray).toEqual(true); expect(result.fnArray.length).toEqual(3); expect(result.fnArray[0]).toEqual('setFlatness'); expect(result.fnArray[1]).toEqual('setRenderingIntent'); - expect(result.fnArray[2]).toEqual('setFlatness'); + expect(result.fnArray[2]).toEqual('save'); expect(result.argsArray.length).toEqual(3); expect(result.argsArray[0].length).toEqual(1); expect(result.argsArray[0][0]).toEqual(true); expect(result.argsArray[1].length).toEqual(1); expect(result.argsArray[1][0]).toEqual(false); + expect(result.argsArray[2].length).toEqual(0); + }); + }); + + describe('validateNumberOfArgs', function() { + it('should execute if correct number of arguments', function() { + var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), + 'prefix'); + var stream = new StringStream('5 1 d0'); + var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); + + expect(result.argsArray[0][0]).toEqual(5); + expect(result.argsArray[0][1]).toEqual(1); + expect(result.fnArray[0]).toEqual('setCharWidth'); + }); + it('should execute if too many arguments', function() { + var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), + 'prefix'); + var stream = new StringStream('5 1 4 d0'); + var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); + + expect(result.argsArray[0][0]).toEqual(5); + expect(result.argsArray[0][1]).toEqual(1); + expect(result.argsArray[0][2]).toEqual(4); + expect(result.fnArray[0]).toEqual('setCharWidth'); + }); + it('should skip if too few arguments', function() { + var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), + 'prefix'); + var stream = new StringStream('5 d0'); + var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); + + expect(result.argsArray).toEqual([]); + expect(result.fnArray).toEqual([]); }); }); });