From de9150b528ff866f64715b544d5e26de781fe7ee Mon Sep 17 00:00:00 2001 From: sbarman Date: Tue, 14 Jun 2011 11:55:27 -0700 Subject: [PATCH 01/16] Implemented type 2 shading for the pdf (aka gradients) --- pdf.js | 209 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 194 insertions(+), 15 deletions(-) diff --git a/pdf.js b/pdf.js index 418db2462..87335317a 100644 --- a/pdf.js +++ b/pdf.js @@ -1911,14 +1911,19 @@ var CanvasGraphics = (function() { // Shading shadingFill: function(entryRef) { + if (!this.current.bbox) + TODO("bbox"); + var shadingRes = this.res.get("Shading"); if (!shadingRes) return; - shadingRes = this.xref.fetchIfRef(shadingRes); + + var xref = this.xref; + shadingRes = xref.fetchIfRef(shadingRes); var shading = shadingRes.get(entryRef.name); if (!shading) return; - shading = this.xref.fetchIfRef(shading); + shading = xref.fetchIfRef(shading); if (!shading) return; @@ -1931,8 +1936,13 @@ var CanvasGraphics = (function() { this.endPath(); } + var cs = shading.get2("ColorSpace", "CS"); TODO("shading-fill color space"); + var background = shading.get("Background"); + if (background) + TODO("handle background colors"); + var type = shading.get("ShadingType"); switch (type) { case 1: @@ -1952,10 +1962,10 @@ var CanvasGraphics = (function() { this.restore(); }, + fillAxialShading: function(sh) { - var coordsArr = sh.get("Coords"); - var x0 = coordsArr[0], y0 = coordsArr[1], - x1 = coordsArr[2], y1 = coordsArr[3]; + var cds = sh.get("Coords"); + var t0 = 0.0, t1 = 1.0; if (sh.has("Domain")) { @@ -1967,19 +1977,29 @@ var CanvasGraphics = (function() { if (sh.has("Extend")) { var extendArr = sh.get("Extend"); extendStart = extendArr[0], extendEnd = extendArr[1]; + TODO("Support extend"); + } + var fnObj = sh.get("Function"); + fnObj = this.xref.fetchIfRef(fnObj); + if (!IsFunction(fnObj)) + error("invalid function"); + fn = new Function(this.xref, fnObj); + + var gradient = this.ctx.createLinearGradient(cds[0], cds[1], cds[2], cds[3]); + var step = (t1 - t0) / 10; + + for (var i = t0; i <= t1; i += step) { + var c = fn.func([i]); + var clength = c.length; + for (var j = 0; j < clength; ++j) + c[j] = Math.round(c[j] * 255); + gradient.addColorStop(i, "rgb("+c[0] + "," + c[1] + "," + c[2] + ")"); } - var fn = sh.get("Function"); - fn = this.xref.fetchIfRef(fn); - - var gradient = this.ctx.createLinearGradient(x0, y0, x1, y1); - - gradient.addColorStop(0, 'rgb(0,0,255)'); - gradient.addColorStop(1, 'rgb(0,255,0)'); - this.ctx.fillStyle = gradient; - this.ctx.fill(); - this.consumePath(); + + // HACK to draw the gradient onto an infinite rectangle + this.ctx.fillRect(-1e10, -1e10, 2e10, 2e10); }, // XObjects @@ -2292,3 +2312,162 @@ var ColorSpace = (function() { return constructor; })(); + +var Function = (function() { + function constructor(xref, fn) { + var dict = fn.dict; + if (!dict) + dict = fn; + + var type = dict.get("FunctionType"); + + switch(type) { + case 0: + this.constructSampled(fn, dict); + break; + case 2: + this.constructInterpolated(); + break; + case 3: + this.constructStiched(); + break; + case 4: + this.constructPostScript(); + break; + default: + error("Unknown type of function"); + } + }; + + constructor.prototype = { + constructSampled: function(str, dict) { + var domain = dict.get("Domain"); + var range = dict.get("Range"); + + if (!domain || !range) + error("No domain or range"); + + var inputSize = domain.length / 2; + var outputSize = range.length / 2; + + if (inputSize != 1) + error("No support for multi-variable inputs to functions"); + + var size = dict.get("Size"); + var bps = dict.get("BitsPerSample"); + var order = dict.get("Order"); + if (!order) + order = 1; + if (order !== 1) + error ("No support for cubic spline interpolation"); + + var encode = dict.get("Encode"); + if (!encode) { + encode = []; + for (var i = 0; i < inputSize; ++i) { + encode.push(0); + encode.push(size[i] - 1); + } + } + var decode = dict.get("Decode"); + if (!decode) + decode = range; + + var samples = this.getSampleArray(size, outputSize, bps, str); + + this.func = function(args) { + var clip = function(v, min, max) { + if (v > max) + v = max; + else if (v < min) + v = min + return v; + } + + if (inputSize != args.length) + error("Incorrect number of arguments"); + + for (var i = 0; i < inputSize; i++) { + var i2 = i * 2; + + // clip to the domain + var v = clip(args[i], domain[i2], domain[i2 + 1]); + + // encode + v = encode[i2] + ((v - domain[i2]) * + (encode[i2 + 1] - encode[i2]) / + (domain[i2 + 1] - domain[i2])); + + // clip to the size + args[i] = clip(v, 0, size[i] - 1); + } + + // interpolate to table + TODO("Multi-dimensional interpolation"); + var floor = Math.floor(args[0]); + var ceil = Math.ceil(args[0]); + var scale = args[0] - floor; + + floor *= outputSize; + ceil *= outputSize; + + var output = []; + for (var i = 0; i < outputSize; ++i) { + if (ceil == floor) { + var v = samples[ceil + i]; + } else { + var low = samples[floor + i]; + var high = samples[ceil + i]; + var v = low * scale + high * (1 - scale); + } + + var i2 = i * 2; + // decode + v = decode[i2] + (v * (decode[i2 + 1] - decode[i2]) / + ((1 << bps) - 1)); + + // clip to the domain + output.push(clip(v, range[i2], range[i2 + 1])); + } + + return output; + } + }, + getSampleArray: function(size, outputSize, bps, str) { + var length = 1; + for (var i = 0; i < size.length; i++) + length *= size[i]; + length *= outputSize; + + var array = []; + var codeSize = 0; + var codeBuf = 0; + + var strBytes = str.getBytes((length * bps + 7) / 8); + var strIdx = 0; + for (var i = 0; i < length; i++) { + var b; + while (codeSize < bps) { + codeBuf <<= 8; + codeBuf |= strBytes[strIdx++]; + codeSize += 8; + } + codeSize -= bps + array.push(codeBuf >> codeSize); + codeBuf &= (1 << codeSize) - 1; + } + return array; + }, + constructInterpolated: function() { + error("unhandled type of function"); + }, + constructStiched: function() { + error("unhandled type of function"); + }, + constructPostScript: function() { + error("unhandled type of function"); + } + }; + + return constructor; +})(); From 3c12d9ee3116585ee3a15ddc286e3d0c16913b0a Mon Sep 17 00:00:00 2001 From: sbarman Date: Tue, 14 Jun 2011 12:19:35 -0700 Subject: [PATCH 02/16] cleaned up code --- pdf.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pdf.js b/pdf.js index 87335317a..1910a216a 100644 --- a/pdf.js +++ b/pdf.js @@ -1911,9 +1911,6 @@ var CanvasGraphics = (function() { // Shading shadingFill: function(entryRef) { - if (!this.current.bbox) - TODO("bbox"); - var shadingRes = this.res.get("Shading"); if (!shadingRes) return; @@ -1965,8 +1962,7 @@ var CanvasGraphics = (function() { fillAxialShading: function(sh) { var cds = sh.get("Coords"); - - + var t0 = 0.0, t1 = 1.0; if (sh.has("Domain")) { var domainArr = sh.get("Domain"); @@ -1981,8 +1977,10 @@ var CanvasGraphics = (function() { } var fnObj = sh.get("Function"); fnObj = this.xref.fetchIfRef(fnObj); - if (!IsFunction(fnObj)) - error("invalid function"); + if (IsArray(fnObj)) + error("No support for array of functions"); + else if (!IsFunction(fnObj)) + error("Invalid function"); fn = new Function(this.xref, fnObj); var gradient = this.ctx.createLinearGradient(cds[0], cds[1], cds[2], cds[3]); From 58ca7ab61c2aca41cc6d97dafba658b2f7d5f340 Mon Sep 17 00:00:00 2001 From: sbarman Date: Tue, 14 Jun 2011 13:49:50 -0700 Subject: [PATCH 03/16] Cleaned up code, renamed PDFFunction class --- pdf.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pdf.js b/pdf.js index 1910a216a..0beb5e22e 100644 --- a/pdf.js +++ b/pdf.js @@ -1961,7 +1961,9 @@ var CanvasGraphics = (function() { }, fillAxialShading: function(sh) { - var cds = sh.get("Coords"); + var coordsArr = sh.get("Coords"); + var x0 = coordsArr[0], y0 = coordsArr[1], + x1 = coordsArr[2], y1 = coordsArr[3]; var t0 = 0.0, t1 = 1.0; if (sh.has("Domain")) { @@ -1981,17 +1983,14 @@ var CanvasGraphics = (function() { error("No support for array of functions"); else if (!IsFunction(fnObj)) error("Invalid function"); - fn = new Function(this.xref, fnObj); + fn = new PDFFunction(this.xref, fnObj); - var gradient = this.ctx.createLinearGradient(cds[0], cds[1], cds[2], cds[3]); + var gradient = this.ctx.createLinearGradient(x0, y0, x1, y1); var step = (t1 - t0) / 10; for (var i = t0; i <= t1; i += step) { var c = fn.func([i]); - var clength = c.length; - for (var j = 0; j < clength; ++j) - c[j] = Math.round(c[j] * 255); - gradient.addColorStop(i, "rgb("+c[0] + "," + c[1] + "," + c[2] + ")"); + gradient.addColorStop(i, this.makeCssRgb.apply(this,c)); } this.ctx.fillStyle = gradient; @@ -2311,7 +2310,7 @@ var ColorSpace = (function() { return constructor; })(); -var Function = (function() { +var PDFFunction = (function() { function constructor(xref, fn) { var dict = fn.dict; if (!dict) From 7a0ba61a2a336deb8ae43dae49746f1780705f86 Mon Sep 17 00:00:00 2001 From: Andreas Gal Date: Tue, 14 Jun 2011 18:31:14 -0700 Subject: [PATCH 04/16] compile PDF command streams into JS code --- pdf.js | 136 ++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 82 insertions(+), 54 deletions(-) diff --git a/pdf.js b/pdf.js index 418db2462..bea42ad2e 100644 --- a/pdf.js +++ b/pdf.js @@ -1395,8 +1395,7 @@ var Page = (function() { gfx.beginDrawing({ x: mediaBox[0], y: mediaBox[1], width: mediaBox[2] - mediaBox[0], height: mediaBox[3] - mediaBox[1] }); - gfx.interpret(new Parser(new Lexer(contents), false), - xref, resources); + gfx.execute(contents, xref, resources); gfx.endDrawing(); } }; @@ -1605,65 +1604,65 @@ var CanvasGraphics = (function() { this.xobjs = null; this.map = { // Graphics state - w: this.setLineWidth, - J: this.setLineCap, - j: this.setLineJoin, - d: this.setDash, - ri: this.setRenderingIntent, - i: this.setFlatness, - gs: this.setGState, - q: this.save, - Q: this.restore, - cm: this.transform, + w: "setLineWidth", + J: "setLineCap", + j: "setLineJoin", + d: "setDash", + ri: "setRenderingIntent", + i: "setFlatness", + gs: "setGState", + q: "save", + Q: "restore", + cm: "transform", // Path - m: this.moveTo, - l: this.lineTo, - c: this.curveTo, - h: this.closePath, - re: this.rectangle, - S: this.stroke, - f: this.fill, - "f*": this.eoFill, - B: this.fillStroke, - b: this.closeFillStroke, - n: this.endPath, + m: "moveTo", + l: "lineTo", + c: "curveTo", + h: "closePath", + re: "rectangle", + S: "stroke", + f: "fill", + "f*": "eoFill", + B: "fillStroke", + b: "closeFillStroke", + n: "endPath", // Clipping - W: this.clip, - "W*": this.eoClip, + W: "clip", + "W*": "eoClip", // Text - BT: this.beginText, - ET: this.endText, - TL: this.setLeading, - Tf: this.setFont, - Td: this.moveText, - Tm: this.setTextMatrix, - "T*": this.nextLine, - Tj: this.showText, - TJ: this.showSpacedText, + BT: "beginText", + ET: "endText", + TL: "setLeading", + Tf: "setFont", + Td: "moveText", + Tm: "setTextMatrix", + "T*": "nextLine", + Tj: "showText", + TJ: "showSpacedText", // Type3 fonts // Color - CS: this.setStrokeColorSpace, - cs: this.setFillColorSpace, - SC: this.setStrokeColor, - SCN: this.setStrokeColorN, - sc: this.setFillColor, - scn: this.setFillColorN, - G: this.setStrokeGray, - g: this.setFillGray, - RG: this.setStrokeRGBColor, - rg: this.setFillRGBColor, + CS: "setStrokeColorSpace", + cs: "setFillColorSpace", + SC: "setStrokeColor", + SCN: "setStrokeColorN", + sc: "setFillColor", + scn: "setFillColorN", + G: "setStrokeGray", + g: "setFillGray", + RG: "setStrokeRGBColor", + rg: "setFillRGBColor", // Shading - sh: this.shadingFill, + sh: "shadingFill", // Images // XObjects - Do: this.paintXObject, + Do: "paintXObject", // Marked content // Compatibility @@ -1683,13 +1682,38 @@ var CanvasGraphics = (function() { this.ctx.translate(0, -mediaBox.height); }, - interpret: function(parser, xref, resources) { + execute: function(stream, xref, resources) { + if (!stream.execute) + this.compile(stream, xref, resources); + var savedXref = this.xref, savedRes = this.res, savedXobjs = this.xobjs; this.xref = xref; this.res = resources || new Dict(); this.xobjs = this.res.get("XObject") || new Dict(); this.xobjs = this.xref.fetchIfRef(this.xobjs); + stream.execute(this, stream.objpool); + + this.xobjs = savedXobjs; + this.res = savedRes; + this.xref = savedXref; + }, + + compile: function(stream, xref, resources) { + var parser = new Parser(new Lexer(stream), false); + var objpool = []; + + function emitArg(arg) { + if (typeof arg == "object" || typeof arg == "string") { + var index = objpool.length; + objpool[index] = arg; + return "objpool[" + index + "]"; + } + return arg; + } + + var src = "{\n"; + var args = []; var map = this.map; var obj; @@ -1699,7 +1723,12 @@ var CanvasGraphics = (function() { var fn = map[cmd]; assertWellFormed(fn, "Unknown command '" + cmd + "'"); // TODO figure out how to type-check vararg functions - fn.apply(this, args); + + src += "gfx."; + src += fn; + src += "("; + src += args.map(emitArg).join(","); + src += ");\n"; args.length = 0; } else { @@ -1708,9 +1737,10 @@ var CanvasGraphics = (function() { } } - this.xobjs = savedXobjs; - this.res = savedRes; - this.xref = savedXref; + src += "}"; + + stream.execute = new Function("gfx", "objpool", src); + stream.objpool = objpool; }, endDrawing: function() { @@ -2026,9 +2056,7 @@ var CanvasGraphics = (function() { this.clip(); this.endPath(); } - - this.interpret(new Parser(new Lexer(form), false), - this.xref, form.dict.get("Resources")); + this.execute(form, this.xref, form.dict.get("Resources")); this.restore(); }, From e7d6b47099ebccb589bf5d109e7a83799c77dfa3 Mon Sep 17 00:00:00 2001 From: Andreas Gal Date: Tue, 14 Jun 2011 20:36:45 -0700 Subject: [PATCH 05/16] return ready-to-run closure from compile that captures its objpool --- pdf.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pdf.js b/pdf.js index bea42ad2e..20299909e 100644 --- a/pdf.js +++ b/pdf.js @@ -1684,7 +1684,7 @@ var CanvasGraphics = (function() { execute: function(stream, xref, resources) { if (!stream.execute) - this.compile(stream, xref, resources); + stream.execute = this.compile(stream, xref, resources); var savedXref = this.xref, savedRes = this.res, savedXobjs = this.xobjs; this.xref = xref; @@ -1692,7 +1692,7 @@ var CanvasGraphics = (function() { this.xobjs = this.res.get("XObject") || new Dict(); this.xobjs = this.xref.fetchIfRef(this.xobjs); - stream.execute(this, stream.objpool); + stream.execute(this); this.xobjs = savedXobjs; this.res = savedRes; @@ -1724,7 +1724,7 @@ var CanvasGraphics = (function() { assertWellFormed(fn, "Unknown command '" + cmd + "'"); // TODO figure out how to type-check vararg functions - src += "gfx."; + src += "this."; src += fn; src += "("; src += args.map(emitArg).join(","); @@ -1739,8 +1739,8 @@ var CanvasGraphics = (function() { src += "}"; - stream.execute = new Function("gfx", "objpool", src); - stream.objpool = objpool; + var fn = new Function("objpool", src); + return function (gfx) { fn.call(gfx, objpool); }; }, endDrawing: function() { From e8ce0b361d524aa3ee5b91f8b0dccd10f85d51f9 Mon Sep 17 00:00:00 2001 From: Andreas Gal Date: Tue, 14 Jun 2011 22:54:49 -0700 Subject: [PATCH 06/16] eagerly compile XForm objects --- pdf.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/pdf.js b/pdf.js index 20299909e..48e20c455 100644 --- a/pdf.js +++ b/pdf.js @@ -1689,8 +1689,7 @@ var CanvasGraphics = (function() { var savedXref = this.xref, savedRes = this.res, savedXobjs = this.xobjs; this.xref = xref; this.res = resources || new Dict(); - this.xobjs = this.res.get("XObject") || new Dict(); - this.xobjs = this.xref.fetchIfRef(this.xobjs); + this.xobjs = xref.fetchIfRef(this.res.get("XObject")) || new Dict(); stream.execute(this); @@ -1700,6 +1699,8 @@ var CanvasGraphics = (function() { }, compile: function(stream, xref, resources) { + var xobjs = xref.fetchIfRef(resources.get("XObject")) || new Dict(); + var parser = new Parser(new Lexer(stream), false); var objpool = []; @@ -1724,6 +1725,22 @@ var CanvasGraphics = (function() { assertWellFormed(fn, "Unknown command '" + cmd + "'"); // TODO figure out how to type-check vararg functions + if (cmd == "Do") { // eagerly compile XForm objects + var name = args[0].name; + var xobj = xobjs.get(name); + if (xobj) { + xobj = xref.fetchIfRef(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) { + this.compile(xobj, xref, xobj.dict.get("Resources")); + } + } + } + src += "this."; src += fn; src += "("; From d1b9e4054a322a5a91cddbae30e9e1ab6937ba7c Mon Sep 17 00:00:00 2001 From: Andreas Gal Date: Tue, 14 Jun 2011 23:16:53 -0700 Subject: [PATCH 07/16] cache results of compilation --- pdf.js | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/pdf.js b/pdf.js index 48e20c455..d65151e46 100644 --- a/pdf.js +++ b/pdf.js @@ -1395,7 +1395,9 @@ var Page = (function() { gfx.beginDrawing({ x: mediaBox[0], y: mediaBox[1], width: mediaBox[2] - mediaBox[0], height: mediaBox[3] - mediaBox[1] }); - gfx.execute(contents, xref, resources); + if (!this.code) + this.code = gfx.compile(contents, xref, resources); + gfx.execute(this.code, xref, resources); gfx.endDrawing(); } }; @@ -1682,16 +1684,13 @@ var CanvasGraphics = (function() { this.ctx.translate(0, -mediaBox.height); }, - execute: function(stream, xref, resources) { - if (!stream.execute) - stream.execute = this.compile(stream, xref, resources); - + execute: function(code, xref, resources) { var savedXref = this.xref, savedRes = this.res, savedXobjs = this.xobjs; this.xref = xref; this.res = resources || new Dict(); this.xobjs = xref.fetchIfRef(this.res.get("XObject")) || new Dict(); - stream.execute(this); + code(this); this.xobjs = savedXobjs; this.res = savedRes; @@ -1699,6 +1698,7 @@ var CanvasGraphics = (function() { }, compile: function(stream, xref, resources) { + console.log("compiling"); var xobjs = xref.fetchIfRef(resources.get("XObject")) || new Dict(); var parser = new Parser(new Lexer(stream), false); @@ -1725,7 +1725,7 @@ var CanvasGraphics = (function() { assertWellFormed(fn, "Unknown command '" + cmd + "'"); // TODO figure out how to type-check vararg functions - if (cmd == "Do") { // eagerly compile XForm objects + if (cmd == "Do" && !args[0].code) { // eagerly compile XForm objects var name = args[0].name; var xobj = xobjs.get(name); if (xobj) { @@ -1736,7 +1736,7 @@ var CanvasGraphics = (function() { assertWellFormed(IsName(type), "XObject should have a Name subtype"); if ("Form" == type.name) { - this.compile(xobj, xref, xobj.dict.get("Resources")); + args[0].code = this.compile(xobj, xref, xobj.dict.get("Resources")); } } } @@ -2050,9 +2050,9 @@ var CanvasGraphics = (function() { var type = xobj.dict.get("Subtype"); assertWellFormed(IsName(type), "XObject should have a Name subtype"); if ("Image" == type.name) { - this.paintImageXObject(xobj, false); + this.paintImageXObject(obj, xobj, false); } else if ("Form" == type.name) { - this.paintFormXObject(xobj); + this.paintFormXObject(obj, xobj); } else if ("PS" == type.name) { warn("(deprecated) PostScript XObjects are not supported"); } else { @@ -2060,25 +2060,26 @@ var CanvasGraphics = (function() { } }, - paintFormXObject: function(form) { + paintFormXObject: function(ref, stream) { this.save(); - var matrix = form.dict.get("Matrix"); + var matrix = stream.dict.get("Matrix"); if (matrix && IsArray(matrix) && 6 == matrix.length) this.transform.apply(this, matrix); - var bbox = form.dict.get("BBox"); + var bbox = stream.dict.get("BBox"); if (bbox && IsArray(bbox) && 4 == bbox.length) { this.rectangle.apply(this, bbox); this.clip(); this.endPath(); } - this.execute(form, this.xref, form.dict.get("Resources")); + + this.execute(ref.code, this.xref, stream.dict.get("Resources")); this.restore(); }, - paintImageXObject: function(image, inline) { + paintImageXObject: function(ref, image, inline) { this.save(); if (image.getParams) { // JPX/JPEG2000 streams directly contain bits per component From 662fab04ca6552f9fcc35c167b25ccf0f0ee3515 Mon Sep 17 00:00:00 2001 From: Andreas Gal Date: Tue, 14 Jun 2011 23:22:19 -0700 Subject: [PATCH 08/16] pdf is using a dumb name, content makes much more sense than Contents --- pdf.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pdf.js b/pdf.js index d65151e46..23f249781 100644 --- a/pdf.js +++ b/pdf.js @@ -1373,8 +1373,8 @@ var Page = (function() { } constructor.prototype = { - get contents() { - return shadow(this, "contents", this.pageDict.get("Contents")); + get content() { + return shadow(this, "content", this.pageDict.get("Contents")); }, get resources() { return shadow(this, "resources", this.pageDict.get("Resources")); @@ -1387,16 +1387,16 @@ var Page = (function() { }, display: function(gfx) { var xref = this.xref; - var contents = xref.fetchIfRef(this.contents); + var content = xref.fetchIfRef(this.content); var resources = xref.fetchIfRef(this.resources); var mediaBox = xref.fetchIfRef(this.mediaBox); - assertWellFormed(IsStream(contents) && IsDict(resources), - "invalid page contents or resources"); + assertWellFormed(IsStream(content) && IsDict(resources), + "invalid page content or resources"); gfx.beginDrawing({ x: mediaBox[0], y: mediaBox[1], width: mediaBox[2] - mediaBox[0], height: mediaBox[3] - mediaBox[1] }); if (!this.code) - this.code = gfx.compile(contents, xref, resources); + this.code = gfx.compile(content, xref, resources); gfx.execute(this.code, xref, resources); gfx.endDrawing(); } From d94b3006a3b5e7bedac3e27e2c9139d2b41a3597 Mon Sep 17 00:00:00 2001 From: Andreas Gal Date: Tue, 14 Jun 2011 23:34:11 -0700 Subject: [PATCH 09/16] eagerly translate all fonts (a no-op currently) --- pdf.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pdf.js b/pdf.js index 23f249781..5aec4e76a 100644 --- a/pdf.js +++ b/pdf.js @@ -1677,6 +1677,10 @@ var CanvasGraphics = (function() { const EO_CLIP = {}; constructor.prototype = { + translateFont: function(fontDict) { + return fontDict; + }, + beginDrawing: function(mediaBox) { var cw = this.ctx.canvas.width, ch = this.ctx.canvas.height; this.ctx.save(); @@ -1698,7 +1702,6 @@ var CanvasGraphics = (function() { }, compile: function(stream, xref, resources) { - console.log("compiling"); var xobjs = xref.fetchIfRef(resources.get("XObject")) || new Dict(); var parser = new Parser(new Lexer(stream), false); @@ -1739,6 +1742,15 @@ var CanvasGraphics = (function() { args[0].code = this.compile(xobj, xref, xobj.dict.get("Resources")); } } + } else if (cmd == "Tf") { // eagerly collect all fonts + var fontRes = resources.get("Font"); + if (fontRes) { + fontRes = xref.fetchIfRef(fontRes); + var font = xref.fetchIfRef(fontRes.get(args[0].name)); + assertWellFormed(IsDict(font)); + if (!font.translated) + font.translated = this.translateFont(font); + } } src += "this."; From cf4bca7813da34114218f51369d874e6327abcfe Mon Sep 17 00:00:00 2001 From: Andreas Gal Date: Tue, 14 Jun 2011 23:41:26 -0700 Subject: [PATCH 10/16] completed async font loading framework --- pdf.js | 25 ++++++++++++++++++++----- test.html | 7 +++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/pdf.js b/pdf.js index 5aec4e76a..633437e7e 100644 --- a/pdf.js +++ b/pdf.js @@ -1385,6 +1385,14 @@ var Page = (function() { ? obj : null)); }, + compile: function(gfx, fonts) { + if (!this.code) { + var xref = this.xref; + var content = xref.fetchIfRef(this.content); + var resources = xref.fetchIfRef(this.resources); + this.code = gfx.compile(content, xref, resources, fonts); + } + }, display: function(gfx) { var xref = this.xref; var content = xref.fetchIfRef(this.content); @@ -1395,8 +1403,6 @@ var Page = (function() { gfx.beginDrawing({ x: mediaBox[0], y: mediaBox[1], width: mediaBox[2] - mediaBox[0], height: mediaBox[3] - mediaBox[1] }); - if (!this.code) - this.code = gfx.compile(content, xref, resources); gfx.execute(this.code, xref, resources); gfx.endDrawing(); } @@ -1701,7 +1707,7 @@ var CanvasGraphics = (function() { this.xref = savedXref; }, - compile: function(stream, xref, resources) { + compile: function(stream, xref, resources, fonts) { var xobjs = xref.fetchIfRef(resources.get("XObject")) || new Dict(); var parser = new Parser(new Lexer(stream), false); @@ -1739,7 +1745,10 @@ var CanvasGraphics = (function() { assertWellFormed(IsName(type), "XObject should have a Name subtype"); if ("Form" == type.name) { - args[0].code = this.compile(xobj, xref, xobj.dict.get("Resources")); + args[0].code = this.compile(xobj, + xref, + xobj.dict.get("Resources"), + fonts); } } } else if (cmd == "Tf") { // eagerly collect all fonts @@ -1748,8 +1757,14 @@ var CanvasGraphics = (function() { fontRes = xref.fetchIfRef(fontRes); var font = xref.fetchIfRef(fontRes.get(args[0].name)); assertWellFormed(IsDict(font)); - if (!font.translated) + if (!font.translated) { font.translated = this.translateFont(font); + if (fonts && font.translated) { + // keep track of each font we translated so the caller can + // load them asynchronously before calling display on a page + fonts.push(font.translated); + } + } } } diff --git a/test.html b/test.html index f78f22ce2..e59d0577e 100644 --- a/test.html +++ b/test.html @@ -95,6 +95,13 @@ function displayPage(num) { ctx.restore(); var gfx = new CanvasGraphics(ctx); + + // page.compile will collect all fonts for us, once we have loaded them + // we can trigger the actual page rendering with page.display + var fonts = []; + page.compile(gfx, fonts); + + // This should be called when font loading is complete page.display(gfx); var t2 = Date.now(); From 02df7f8e5888f9b7c78b55157cd596423462c484 Mon Sep 17 00:00:00 2001 From: Andreas Gal Date: Tue, 14 Jun 2011 23:44:59 -0700 Subject: [PATCH 11/16] clarify API a bit and hand in xref and resources to ease translation --- pdf.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pdf.js b/pdf.js index 633437e7e..6e6935f13 100644 --- a/pdf.js +++ b/pdf.js @@ -1683,8 +1683,8 @@ var CanvasGraphics = (function() { const EO_CLIP = {}; constructor.prototype = { - translateFont: function(fontDict) { - return fontDict; + translateFont: function(fontDict, xref, resources) { + return "translated"; }, beginDrawing: function(mediaBox) { @@ -1758,7 +1758,7 @@ var CanvasGraphics = (function() { var font = xref.fetchIfRef(fontRes.get(args[0].name)); assertWellFormed(IsDict(font)); if (!font.translated) { - font.translated = this.translateFont(font); + font.translated = this.translateFont(font, xref, resources); if (fonts && font.translated) { // keep track of each font we translated so the caller can // load them asynchronously before calling display on a page From 595f00f82a269b96a3e15f604b9c24389c11f5e7 Mon Sep 17 00:00:00 2001 From: Andreas Gal Date: Wed, 15 Jun 2011 00:20:26 -0700 Subject: [PATCH 12/16] measure load/compile/render times --- test.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test.html b/test.html index e59d0577e..9cadf2e1f 100644 --- a/test.html +++ b/test.html @@ -101,12 +101,14 @@ function displayPage(num) { var fonts = []; page.compile(gfx, fonts); + var t2 = Date.now(); + // This should be called when font loading is complete page.display(gfx); - var t2 = Date.now(); + var t3 = Date.now(); - infoDisplay.innerHTML = "Time to render: "+ (t1 - t0) + "/" + (t2 - t1) + " ms"; + infoDisplay.innerHTML = "Time to load/compile/render: "+ (t1 - t0) + "/" + (t2 - t1) + "/" + (t3 - t2) + " ms"; } function nextPage() { From 815544ab814eb1c659cab1e9cdc082ae1b6ce37e Mon Sep 17 00:00:00 2001 From: Andreas Gal Date: Wed, 15 Jun 2011 00:37:15 -0700 Subject: [PATCH 13/16] fixes suggested by @brendaneich --- pdf.js | 35 +++++++---------------------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/pdf.js b/pdf.js index 6e6935f13..3468c7b88 100644 --- a/pdf.js +++ b/pdf.js @@ -590,7 +590,7 @@ function IsString(v) { } function IsNull(v) { - return v == null; + return v === null; } function IsName(v) { @@ -617,27 +617,6 @@ function IsRef(v) { return v instanceof Ref; } -function IsFunction(v) { - var fnDict; - if (typeof v != "object") - return false; - else if (IsDict(v)) - fnDict = v; - else if (IsStream(v)) - fnDict = v.dict; - else - return false; - return fnDict.has("FunctionType"); -} - -function IsFunctionDict(v) { - return IsFunction(v) && IsDict(v); -} - -function IsFunctionStream(v) { - return IsFunction(v) && IsStream(v); -} - var EOF = {}; function IsEOF(v) { @@ -841,10 +820,12 @@ var Lexer = (function() { ch = stream.getChar(); if (ch == '>') { break; - } else if (!ch) { + } + if (!ch) { warn("Unterminated hex string"); break; - } else if (specialChars[ch.charCodeAt(0)] != 1) { + } + if (specialChars[ch.charCodeAt(0)] != 1) { var x, x2; if (((x = ToHexDigit(ch)) == -1) || ((x2 = ToHexDigit(stream.getChar())) == -1)) { @@ -1722,7 +1703,7 @@ var CanvasGraphics = (function() { return arg; } - var src = "{\n"; + var src = ""; var args = []; var map = this.map; @@ -1781,9 +1762,7 @@ var CanvasGraphics = (function() { } } - src += "}"; - - var fn = new Function("objpool", src); + var fn = Function("objpool", src); return function (gfx) { fn.call(gfx, objpool); }; }, From a0375c0f0b853b1ac8e609d20519b71a935a7a56 Mon Sep 17 00:00:00 2001 From: sbarman Date: Wed, 15 Jun 2011 11:34:47 -0700 Subject: [PATCH 14/16] cleaned up fetchIfRef, changed switch-case to array lookup, changed IsFunction to IsPDFFunction --- pdf.js | 78 +++++++++++++++++++++++----------------------------------- 1 file changed, 31 insertions(+), 47 deletions(-) diff --git a/pdf.js b/pdf.js index 0beb5e22e..d4df52452 100644 --- a/pdf.js +++ b/pdf.js @@ -617,7 +617,7 @@ function IsRef(v) { return v instanceof Ref; } -function IsFunction(v) { +function IsPDFFunction(v) { var fnDict; if (typeof v != "object") return false; @@ -1911,18 +1911,16 @@ var CanvasGraphics = (function() { // Shading shadingFill: function(entryRef) { - var shadingRes = this.res.get("Shading"); - if (!shadingRes) - return; - var xref = this.xref; - shadingRes = xref.fetchIfRef(shadingRes); - var shading = shadingRes.get(entryRef.name); + var res = this.res; + + var shadingRes = xref.fetchIfRef(res.get("Shading")); + if (!shadingRes) + error("No shading resource found"); + + var shading = xref.fetchIfRef(shadingRes.get(entryRef.name)); if (!shading) - return; - shading = xref.fetchIfRef(shading); - if (!shading) - return; + error("No shading object found"); this.save(); @@ -1940,22 +1938,14 @@ var CanvasGraphics = (function() { if (background) TODO("handle background colors"); - var type = shading.get("ShadingType"); - switch (type) { - case 1: - this.fillFunctionShading(shading); - break; - case 2: - this.fillAxialShading(shading); - break; - case 3: - this.fillRadialShading(shading); - break; - case 4: case 5: case 6: case 7: - TODO("shading fill type "+ type); - default: - malformed("Unknown shading type "+ type); - } + const types = [null, this.fillFunctionShading, + this.fillAxialShading, this.fillRadialShading]; + + var typeNum = shading.get("ShadingType"); + var fillFn = types[typeNum]; + if (!fillFn) + error("Unknown type of shading"); + fillFn.apply(this, [shading]); this.restore(); }, @@ -1981,7 +1971,7 @@ var CanvasGraphics = (function() { fnObj = this.xref.fetchIfRef(fnObj); if (IsArray(fnObj)) error("No support for array of functions"); - else if (!IsFunction(fnObj)) + else if (!IsPDFFunction(fnObj)) error("Invalid function"); fn = new PDFFunction(this.xref, fnObj); @@ -1990,12 +1980,14 @@ var CanvasGraphics = (function() { for (var i = t0; i <= t1; i += step) { var c = fn.func([i]); - gradient.addColorStop(i, this.makeCssRgb.apply(this,c)); + gradient.addColorStop(i, this.makeCssRgb.apply(this, c)); } this.ctx.fillStyle = gradient; - // HACK to draw the gradient onto an infinite rectangle + // HACK to draw the gradient onto an infinite rectangle. + // PDF gradients are drawn across the entire image while + // Canvas only allows gradients to be drawn in a rectangle this.ctx.fillRect(-1e10, -1e10, 2e10, 2e10); }, @@ -2316,24 +2308,16 @@ var PDFFunction = (function() { if (!dict) dict = fn; - var type = dict.get("FunctionType"); - - switch(type) { - case 0: - this.constructSampled(fn, dict); - break; - case 2: - this.constructInterpolated(); - break; - case 3: - this.constructStiched(); - break; - case 4: - this.constructPostScript(); - break; - default: + const types = [this.constructSampled, null, + this.constructInterpolated, this.constructStiched, + this.constructPostScript]; + + var typeNum = dict.get("FunctionType"); + var typeFn = types[typeNum]; + if (!typeFn) error("Unknown type of function"); - } + + typeFn.apply(this, [fn, dict]); }; constructor.prototype = { From 0c4e4d1651e0199591209b9272d3ad51c98442f8 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Wed, 15 Jun 2011 14:24:44 -0500 Subject: [PATCH 15/16] implement some operators and add TODOs for remaining undefined ones --- pdf.js | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/pdf.js b/pdf.js index 3468c7b88..517b2b27c 100644 --- a/pdf.js +++ b/pdf.js @@ -1596,6 +1596,7 @@ var CanvasGraphics = (function() { w: "setLineWidth", J: "setLineCap", j: "setLineJoin", + M: "setMiterLimit", d: "setDash", ri: "setRenderingIntent", i: "setFlatness", @@ -1608,13 +1609,18 @@ var CanvasGraphics = (function() { m: "moveTo", l: "lineTo", c: "curveTo", + v: "curveTo2", + y: "curveTo3", h: "closePath", re: "rectangle", S: "stroke", + s: "closeStroke", f: "fill", "f*": "eoFill", B: "fillStroke", + "B*": "eoFillStroke", b: "closeFillStroke", + "b*": "closeEOFillStroke", n: "endPath", // Clipping @@ -1624,15 +1630,25 @@ var CanvasGraphics = (function() { // 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", // Type3 fonts + d0: "setCharWidth", + d1: "setCharWidthAndBounds", // Color CS: "setStrokeColorSpace", @@ -1645,16 +1661,28 @@ var CanvasGraphics = (function() { g: "setFillGray", RG: "setStrokeRGBColor", rg: "setFillRGBColor", + K: "setStrokeCMYKColor", + k: "setFillCMYKColor", // Shading sh: "shadingFill", // Images + BI: "beginInlineImage", + // XObjects Do: "paintXObject", // Marked content + MP: "markPoint", + DP: "markPointProps", + BMC: "beginMarkedContent", + BDC: "beginMarkedContentProps", + EMC: "endMarkedContent", + // Compatibility + BX: "beginCompat", + EX: "endCompat", }; } @@ -1780,6 +1808,9 @@ var CanvasGraphics = (function() { setLineJoin: function(style) { this.ctx.lineJoin = LINE_JOIN_STYLES[style]; }, + setMiterLimit: function(limit) { + this.ctx.miterLimit = limit; + }, setDash: function(dashArray, dashPhase) { TODO("set dash"); }, @@ -1818,6 +1849,12 @@ var CanvasGraphics = (function() { curveTo: function(x1, y1, x2, y2, x3, y3) { this.ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3); }, + curveTo2: function(x2, y2, x3, y3) { + TODO("'v' operator: need current point in gfx context"); + }, + curveTo3: function(x1, y1, x3, y3) { + this.curveTo(x1, y1, x3, y3, x3, y3); + }, closePath: function() { this.ctx.closePath(); }, @@ -1828,6 +1865,10 @@ var CanvasGraphics = (function() { this.ctx.stroke(); this.consumePath(); }, + closeStroke: function() { + this.closePath(); + this.stroke(); + }, fill: function() { this.ctx.fill(); this.consumePath(); @@ -1842,9 +1883,19 @@ var CanvasGraphics = (function() { this.ctx.stroke(); this.consumePath(); }, + eoFillStroke: function() { + var savedFillRule = this.setEOFillRule(); + this.fillStroke(); + this.restoreFillRule(savedFillRule); + }, closeFillStroke: function() { return this.fillStroke(); }, + closeEOFillStroke: function() { + var savedFillRule = this.setEOFillRule(); + this.fillStroke(); + this.restoreFillRule(savedFillRule); + }, endPath: function() { this.consumePath(); }, @@ -1865,6 +1916,15 @@ var CanvasGraphics = (function() { }, endText: function() { }, + setCharSpacing: function(spacing) { + TODO("character (glyph?) spacing"); + }, + setWordSpacing: function(spacing) { + TODO("word spacing"); + }, + setHSpacing: function(scale) { + TODO("horizontal text scale"); + }, setLeading: function(leading) { this.current.leading = leading; }, @@ -1880,10 +1940,20 @@ var CanvasGraphics = (function() { TODO("using hard-coded font for testing"); this.ctx.font = this.current.fontSize +'px "Nimbus Roman No9 L"'; }, + setTextRenderingMode: function(mode) { + TODO("text rendering mode"); + }, + setTextRise: function(rise) { + TODO("text rise"); + }, moveText: function (x, y) { this.current.x = this.current.lineX += x; this.current.y = this.current.lineY += y; }, + setLeadingMoveText: function(x, y) { + this.setLeading(-y); + this.moveText(x, y); + }, setTextMatrix: function(a, b, c, d, e, f) { this.current.textMatrix = [ a, b, c, d, e, f ]; this.current.x = this.current.lineX = 0; @@ -1915,8 +1985,23 @@ var CanvasGraphics = (function() { } } }, + nextLineShowText: function(text) { + this.nextLine(); + this.showText(text); + }, + nextLineSetSpacingShowText: function(wordSpacing, charSpacing, text) { + this.setWordSpacing(wordSpacing); + this.setCharSpacing(charSpacing); + this.nextLineShowText(text); + }, // Type3 fonts + setCharWidth: function(xWidth, yWidth) { + TODO("type 3 fonts ('d0' operator)"); + }, + setCharWidthAndBounds: function(xWidth, yWidth, llx, lly, urx, ury) { + TODO("type 3 fonts ('d1' operator)"); + }, // Color setStrokeColorSpace: function(space) { @@ -1961,6 +2046,12 @@ var CanvasGraphics = (function() { setFillRGBColor: function(r, g, b) { this.ctx.fillStyle = this.makeCssRgb(r, g, b); }, + setStrokeCMYKColor: function(c, m, y, k) { + TODO("CMYK space"); + }, + setFillCMYKColor: function(c, m, y, k) { + TODO("CMYK space"); + }, // Shading shadingFill: function(entryRef) { @@ -2035,6 +2126,15 @@ var CanvasGraphics = (function() { this.consumePath(); }, + // Images + beginInlineImage: function() { + TODO("inline images"); + error("(Stream will not be parsed properly, bailing now)"); + // Like an inline stream: + // - key/value pairs up to Cmd(ID) + // - then image data up to Cmd(EI) + }, + // XObjects paintXObject: function(obj) { var xobj = this.xobjs.get(obj.name); @@ -2262,6 +2362,33 @@ var CanvasGraphics = (function() { this.restore(); }, + // Marked content + + markPoint: function(tag) { + TODO("Marked content"); + }, + markPointProps: function(tag, properties) { + TODO("Marked content"); + }, + beginMarkedContent: function(tag) { + TODO("Marked content"); + }, + beginMarkedContentProps: function(tag, properties) { + TODO("Marked content"); + }, + endMarkedContent: function() { + TODO("Marked content"); + }, + + // Compatibility + + beginCompat: function() { + TODO("ignore undefined operators (should we do that anyway?)"); + }, + endCompat: function() { + TODO("stop ignoring undefined operators"); + }, + // Helper functions consumePath: function() { From 8ab68cf17a019bc7b3d40a35a306b9d626cee63e Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Wed, 15 Jun 2011 12:55:48 -0700 Subject: [PATCH 16/16] implement setDash --- pdf.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pdf.js b/pdf.js index 10e661129..17537d233 100644 --- a/pdf.js +++ b/pdf.js @@ -1825,7 +1825,8 @@ var CanvasGraphics = (function() { this.ctx.miterLimit = limit; }, setDash: function(dashArray, dashPhase) { - TODO("set dash"); + this.ctx.mozDash = dashArray; + this.ctx.mozDashOffset = dashPhase; }, setRenderingIntent: function(intent) { TODO("set rendering intent");