Merge branch 'master' of github.com:andreasgal/pdf.js

This commit is contained in:
Andreas Gal 2011-05-21 02:34:22 +02:00
commit 4e57061d84
2 changed files with 178 additions and 70 deletions

240
pdf.js
View File

@ -1,27 +1,57 @@
/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- / /* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- /
/* vim: set shiftwidth=4 tabstop=8 autoindent cindent expandtab: */ /* vim: set shiftwidth=4 tabstop=8 autoindent cindent expandtab: */
function warn(msg) { var ERRORS = 0, WARNINGS = 1, TODOS = 5;
var verbosity = WARNINGS;
function log(msg) {
if (console && console.log) if (console && console.log)
console.log(msg); console.log(msg);
if (print) else if (print)
print(msg); print(msg);
} }
function warn(msg) {
if (verbosity >= WARNINGS)
log("Warning: "+ msg);
}
function error(msg) { function error(msg) {
throw new Error(msg); throw new Error(msg);
} }
function TODO(what) {
if (verbosity >= TODOS)
log("TODO: "+ what);
}
function malformed(msg) {
error("Malformed PDF: "+ msg);
}
function assert(cond, msg) {
if (!cond)
error(msg);
}
// In a well-formed PDF, |cond| holds. If it doesn't, subsequent
// behavior is undefined.
function assertWellFormed(cond, msg) {
if (!cond)
malformed(msg);
}
function shadow(obj, prop, value) { function shadow(obj, prop, value) {
Object.defineProperty(obj, prop, { value: value, enumerable: true }); Object.defineProperty(obj, prop, { value: value, enumerable: true });
return value; return value;
} }
var Stream = (function() { var Stream = (function() {
function constructor(arrayBuffer) { function constructor(arrayBuffer, dict) {
this.bytes = new Uint8Array(arrayBuffer); this.bytes = new Uint8Array(arrayBuffer);
this.pos = 0; this.pos = 0;
this.start = 0; this.start = 0;
this.dict = dict;
} }
constructor.prototype = { constructor.prototype = {
@ -58,11 +88,11 @@ var Stream = (function() {
moveStart: function() { moveStart: function() {
this.start = this.pos; this.start = this.pos;
}, },
makeSubStream: function(pos, length) { makeSubStream: function(pos, length, dict) {
var buffer = this.bytes.buffer; var buffer = this.bytes.buffer;
if (length) if (length)
return new Stream(new Uint8Array(buffer, pos, length)); return new Stream(new Uint8Array(buffer, pos, length), dict);
return new Stream(new Uint8Array(buffer, pos)); return new Stream(new Uint8Array(buffer, pos), dict);
} }
}; };
@ -1196,7 +1226,7 @@ var Lexer = (function() {
case '\\': case '\\':
case '(': case '(':
case ')': case ')':
str += c; str += ch;
break; break;
case '0': case '1': case '2': case '3': case '0': case '1': case '2': case '3':
case '4': case '5': case '6': case '7': case '4': case '5': case '6': case '7':
@ -1500,7 +1530,7 @@ var Parser = (function() {
error("Missing 'endstream'"); error("Missing 'endstream'");
this.shift(); this.shift();
stream = stream.makeSubStream(pos, length); stream = stream.makeSubStream(pos, length, dict);
if (this.fileKey) { if (this.fileKey) {
stream = new DecryptStream(stream, stream = new DecryptStream(stream,
this.fileKey, this.fileKey,
@ -1811,32 +1841,13 @@ var Page = (function() {
var contents = xref.fetchIfRef(this.contents); var contents = xref.fetchIfRef(this.contents);
var resources = xref.fetchIfRef(this.resources); var resources = xref.fetchIfRef(this.resources);
var mediaBox = xref.fetchIfRef(this.mediaBox); var mediaBox = xref.fetchIfRef(this.mediaBox);
if (!IsStream(contents) || !IsDict(resources)) assertWellFormed(IsStream(contents) && IsDict(resources),
error("invalid page contents or resources"); "invalid page contents or resources");
gfx.resources = resources;
gfx.beginDrawing({ x: mediaBox[0], y: mediaBox[1], gfx.beginDrawing({ x: mediaBox[0], y: mediaBox[1],
width: mediaBox[2] - mediaBox[0], width: mediaBox[2] - mediaBox[0],
height: mediaBox[3] - mediaBox[1] }); height: mediaBox[3] - mediaBox[1] });
var args = []; gfx.interpret(new Parser(new Lexer(contents), false),
var map = gfx.map; xref, resources);
var parser = new Parser(new Lexer(contents), false);
var obj;
while (!IsEOF(obj = parser.getObj())) {
if (IsCmd(obj)) {
var cmd = obj.cmd;
var fn = map[cmd];
if (fn)
// TODO figure out how to type-check vararg functions
fn.apply(gfx, args);
else
error("Unknown command '" + cmd + "'");
args.length = 0;
} else {
if (args.length > 33)
error("Too many arguments '" + cmd + "'");
args.push(obj);
}
}
gfx.endDrawing(); gfx.endDrawing();
} }
}; };
@ -1848,45 +1859,42 @@ var Catalog = (function() {
function constructor(xref) { function constructor(xref) {
this.xref = xref; this.xref = xref;
var obj = xref.getCatalogObj(); var obj = xref.getCatalogObj();
if (!IsDict(obj)) assertWellFormed(IsDict(obj), "catalog object is not a dictionary");
error("catalog object is not a dictionary");
this.catDict = obj; this.catDict = obj;
} }
constructor.prototype = { constructor.prototype = {
get toplevelPagesDict() { get toplevelPagesDict() {
var obj = this.catDict.get("Pages"); var obj = this.catDict.get("Pages");
if (!IsRef(obj)) assertWellFormed(IsRef(obj), "invalid top-level pages reference");
error("invalid top-level pages reference");
var obj = this.xref.fetch(obj); var obj = this.xref.fetch(obj);
if (!IsDict(obj)) assertWellFormed(IsDict(obj), "invalid top-level pages dictionary");
error("invalid top-level pages dictionary");
// shadow the prototype getter // shadow the prototype getter
return shadow(this, "toplevelPagesDict", obj); return shadow(this, "toplevelPagesDict", obj);
}, },
get numPages() { get numPages() {
obj = this.toplevelPagesDict.get("Count"); obj = this.toplevelPagesDict.get("Count");
if (!IsInt(obj)) assertWellFormed(IsInt(obj),
error("page count in top level pages object is not an integer"); "page count in top level pages object is not an integer");
// shadow the prototype getter // shadow the prototype getter
return shadow(this, "num", obj); return shadow(this, "num", obj);
}, },
traverseKids: function(pagesDict) { traverseKids: function(pagesDict) {
var pageCache = this.pageCache; var pageCache = this.pageCache;
var kids = pagesDict.get("Kids"); var kids = pagesDict.get("Kids");
if (!IsArray(kids)) assertWellFormed(IsArray(kids),
error("page dictionary kids object is not an array"); "page dictionary kids object is not an array");
for (var i = 0; i < kids.length; ++i) { for (var i = 0; i < kids.length; ++i) {
var kid = kids[i]; var kid = kids[i];
if (!IsRef(kid)) assertWellFormed(IsRef(kid),
error("page dictionary kid is not a reference"); "page dictionary kid is not a reference");
var obj = this.xref.fetch(kid); var obj = this.xref.fetch(kid);
if (IsDict(obj, "Page") || (IsDict(obj) && !obj.has("Kids"))) { if (IsDict(obj, "Page") || (IsDict(obj) && !obj.has("Kids"))) {
pageCache.push(new Page(this.xref, pageCache.length, obj)); pageCache.push(new Page(this.xref, pageCache.length, obj));
} else if (IsDict(obj)) { // must be a child page dictionary } else { // must be a child page dictionary
assertWellFormed(IsDict(obj),
"page dictionary kid reference points to wrong type of object");
this.traverseKids(obj); this.traverseKids(obj);
} else {
error("page dictionary kid reference points to wrong type of object");
} }
} }
}, },
@ -2007,9 +2015,7 @@ var PDFDoc = (function() {
}, },
getPage: function(n) { getPage: function(n) {
var linearization = this.linearization; var linearization = this.linearization;
if (linearization) { assert(!linearization, "linearized page access not implemented");
error("linearized page access not implemented");
}
return this.catalog.getPage(n); return this.catalog.getPage(n);
} }
}; };
@ -2017,11 +2023,14 @@ var PDFDoc = (function() {
return constructor; return constructor;
})(); })();
var IDENTITY_MATRIX = [ 1, 0, 0, 1, 0, 0 ];
// <canvas> contexts store most of the state we need natively. // <canvas> contexts store most of the state we need natively.
// However, PDF needs a bit more state, which we store here. // However, PDF needs a bit more state, which we store here.
var CanvasExtraState = (function() { var CanvasExtraState = (function() {
function constructor() { function constructor() {
this.fontSize = 0.0; this.fontSize = 0.0;
this.textMatrix = IDENTITY_MATRIX;
// Current point (in user coordinates) // Current point (in user coordinates)
this.curX = 0.0; this.curX = 0.0;
this.curY = 0.0; this.curY = 0.0;
@ -2040,6 +2049,8 @@ var CanvasGraphics = (function() {
this.current = new CanvasExtraState(); this.current = new CanvasExtraState();
this.stateStack = [ ]; this.stateStack = [ ];
this.pendingClip = null; this.pendingClip = null;
this.res = null;
this.xobjs = null;
this.map = { this.map = {
// Graphics state // Graphics state
w: this.setLineWidth, w: this.setLineWidth,
@ -2048,6 +2059,7 @@ var CanvasGraphics = (function() {
d: this.setDash, d: this.setDash,
ri: this.setRenderingIntent, ri: this.setRenderingIntent,
i: this.setFlatness, i: this.setFlatness,
gs: this.setGState,
q: this.save, q: this.save,
Q: this.restore, Q: this.restore,
cm: this.transform, cm: this.transform,
@ -2087,6 +2099,7 @@ var CanvasGraphics = (function() {
SCN: this.setStrokeColorN, SCN: this.setStrokeColorN,
sc: this.setFillColor, sc: this.setFillColor,
scn: this.setFillColorN, scn: this.setFillColorN,
G: this.setStrokeGray,
g: this.setFillGray, g: this.setFillGray,
RG: this.setStrokeRGBColor, RG: this.setStrokeRGBColor,
rg: this.setFillRGBColor, rg: this.setFillRGBColor,
@ -2115,6 +2128,36 @@ var CanvasGraphics = (function() {
this.ctx.scale(cw / mediaBox.width, -ch / mediaBox.height); this.ctx.scale(cw / mediaBox.width, -ch / mediaBox.height);
this.ctx.translate(0, -mediaBox.height); this.ctx.translate(0, -mediaBox.height);
}, },
interpret: function(parser, 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();
var args = [];
var map = this.map;
var obj;
while (!IsEOF(obj = parser.getObj())) {
if (IsCmd(obj)) {
var cmd = obj.cmd;
var fn = map[cmd];
assertWellFormed(fn, "Unknown command '" + cmd + "'");
// TODO figure out how to type-check vararg functions
fn.apply(this, args);
args.length = 0;
} else {
assertWellFormed(args.length <= 33, "Too many arguments");
args.push(obj);
}
}
this.xobjs = savedXobjs;
this.res = savedRes;
this.xref = savedXref;
},
endDrawing: function() { endDrawing: function() {
this.ctx.restore(); this.ctx.restore();
}, },
@ -2130,13 +2173,16 @@ var CanvasGraphics = (function() {
this.ctx.lineJoin = LINE_JOIN_STYLES[style]; this.ctx.lineJoin = LINE_JOIN_STYLES[style];
}, },
setDash: function(dashArray, dashPhase) { setDash: function(dashArray, dashPhase) {
// TODO TODO("set dash");
}, },
setRenderingIntent: function(intent) { setRenderingIntent: function(intent) {
// TODO TODO("set rendering intent");
}, },
setFlatness: function(flatness) { setFlatness: function(flatness) {
// TODO TODO("set flatness");
},
setGState: function(dictName) {
TODO("set graphics state from dict");
}, },
save: function() { save: function() {
this.ctx.save(); this.ctx.save();
@ -2144,8 +2190,11 @@ var CanvasGraphics = (function() {
this.current = new CanvasExtraState(); this.current = new CanvasExtraState();
}, },
restore: function() { restore: function() {
this.current = this.stateStack.pop(); var prev = this.stateStack.pop();
this.ctx.restore(); if (prev) {
this.current = prev;
this.ctx.restore();
}
}, },
transform: function(a, b, c, d, e, f) { transform: function(a, b, c, d, e, f) {
this.ctx.transform(a, b, c, d, e, f); this.ctx.transform(a, b, c, d, e, f);
@ -2176,7 +2225,7 @@ var CanvasGraphics = (function() {
this.consumePath(); this.consumePath();
}, },
eoFill: function() { eoFill: function() {
// TODO: <canvas> needs to support even-odd winding rule TODO("even-odd fill");
this.fill(); this.fill();
}, },
fillStroke: function() { fillStroke: function() {
@ -2201,13 +2250,14 @@ var CanvasGraphics = (function() {
// Text // Text
beginText: function() { beginText: function() {
// TODO this.current.textMatrix = IDENTITY_MATRIX;
}, },
endText: function() { endText: function() {
// TODO
}, },
setFont: function(fontRef, size) { setFont: function(fontRef, size) {
var font = this.resources.get("Font").get(fontRef.name); var font = this.res.get("Font").get(fontRef.name);
if (!font)
return;
this.current.fontSize = size; this.current.fontSize = size;
this.ctx.font = this.current.fontSize +'px '+ font.BaseFont; this.ctx.font = this.current.fontSize +'px '+ font.BaseFont;
}, },
@ -2219,12 +2269,13 @@ var CanvasGraphics = (function() {
this.current.curY = this.current.lineY; this.current.curY = this.current.lineY;
}, },
setTextMatrix: function(a, b, c, d, e, f) { setTextMatrix: function(a, b, c, d, e, f) {
// TODO this.current.textMatrix = [ a, b, c, d, e, f ];
}, },
showText: function(text) { showText: function(text) {
this.ctx.save(); this.ctx.save();
this.ctx.translate(0, 2 * this.current.curY); this.ctx.translate(0, 2 * this.current.curY);
this.ctx.scale(1, -1); this.ctx.scale(1, -1);
this.ctx.transform.apply(this.ctx, this.current.textMatrix);
this.ctx.fillText(text, this.current.curX, this.current.curY); this.ctx.fillText(text, this.current.curX, this.current.curY);
this.current.curX += this.ctx.measureText(text).width; this.current.curX += this.ctx.measureText(text).width;
@ -2239,7 +2290,7 @@ var CanvasGraphics = (function() {
} else if (IsString(e)) { } else if (IsString(e)) {
this.showText(e); this.showText(e);
} else { } else {
error("Unexpected element in TJ array"); malformed("TJ array element "+ e +" isn't string or num");
} }
} }
}, },
@ -2248,22 +2299,37 @@ var CanvasGraphics = (function() {
// Color // Color
setStrokeColorSpace: function(space) { setStrokeColorSpace: function(space) {
// TODO // TODO real impl
}, },
setFillColorSpace: function(space) { setFillColorSpace: function(space) {
// TODO // TODO real impl
}, },
setStrokeColor: function(/*...*/) { setStrokeColor: function(/*...*/) {
// TODO // TODO real impl
if (1 === arguments.length) {
this.setStrokeGray.apply(this, arguments);
} else if (3 === arguments.length) {
this.setStrokeRGBColor.apply(this, arguments);
}
}, },
setStrokeColorN: function(/*...*/) { setStrokeColorN: function(/*...*/) {
// TODO // TODO real impl
this.setStrokeColor.apply(this, arguments);
}, },
setFillColor: function(/*...*/) { setFillColor: function(/*...*/) {
// TODO // TODO real impl
if (1 === arguments.length) {
this.setFillGray.apply(this, arguments);
} else if (3 === arguments.length) {
this.setFillRGBColor.apply(this, arguments);
}
}, },
setFillColorN: function(/*...*/) { setFillColorN: function(/*...*/) {
// TODO // TODO real impl
this.setFillColor.apply(this, arguments);
},
setStrokeGray: function(gray) {
this.setStrokeRGBColor(gray, gray, gray);
}, },
setFillGray: function(gray) { setFillGray: function(gray) {
this.setFillRGBColor(gray, gray, gray); this.setFillRGBColor(gray, gray, gray);
@ -2277,19 +2343,55 @@ var CanvasGraphics = (function() {
// Shading // Shading
shadingFill: function(entry) { shadingFill: function(entry) {
// TODO TODO("shading fill");
}, },
// XObjects // XObjects
paintXObject: function(obj) { paintXObject: function(obj) {
// TODO var xobj = this.xobjs.get(obj.name);
if (!xobj)
return;
xobj = this.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 ("Image" == type.name) {
TODO("Image XObjects");
} else if ("Form" == type.name) {
this.paintFormXObject(xobj);
} else if ("PS" == type.name) {
warn("(deprecated) PostScript XObjects are not supported");
} else {
malformed("Unknown XObject subtype "+ type.name);
}
},
paintFormXObject: function(form) {
this.save();
var matrix = form.dict.get("Matrix");
if (matrix && IsArray(matrix) && 6 == matrix.length)
this.transform.apply(this, matrix);
var bbox = form.dict.get("BBox");
if (bbox && IsArray(bbox) && 4 == bbox.length) {
this.rectangle.apply(this, bbox);
this.clip();
this.endPath();
}
this.interpret(new Parser(new Lexer(form), false),
this.xref, form.dict.get("Resources"));
this.restore();
}, },
// Helper functions // Helper functions
consumePath: function() { consumePath: function() {
if (this.pendingClip) { if (this.pendingClip) {
// TODO: <canvas> needs to support even-odd winding rule if (this.pendingClip == EO_CLIP)
TODO("even-odd clipping");
this.ctx.clip(); this.ctx.clip();
this.pendingClip = null; this.pendingClip = null;
} }

View File

@ -81,6 +81,11 @@ function prevPage() {
if (pageNum > 1) if (pageNum > 1)
--pageNum; --pageNum;
displayPage(pageNum); displayPage(pageNum);
}
function gotoPage(num) {
if (0 <= num && num <= numPages)
pageNum = num;
displayPage(pageNum);
} }
</script> </script>
</head> </head>
@ -89,7 +94,8 @@ function prevPage() {
<div> <div>
<button onclick="prevPage();">Previous</button> <button onclick="prevPage();">Previous</button>
<button onclick="nextPage();">Next</button> <button onclick="nextPage();">Next</button>
<input type="text" id="pageNumber" value="1" size="5"></input> <input type="text" id="pageNumber" onchange="gotoPage(this.value);"
value="1" size="5"></input>
Time to render: <span id="time"></span> Time to render: <span id="time"></span>
<div id="viewer"> <div id="viewer">
<!-- Canvas dimensions must be specified in CSS pixels. CSS pixels <!-- Canvas dimensions must be specified in CSS pixels. CSS pixels