From: Julian Viereck Date: Sat, 8 Oct 2011 12:18:23 +0000 (+0200) Subject: Merge worker_pull with master X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=3054102d3bdf241049a9c4564fc9607b78a4214a;p=pdf.js.git Merge worker_pull with master --- 3054102d3bdf241049a9c4564fc9607b78a4214a diff --cc fonts.js index 974c8e7,2ab3a90..371a472 mode 100755,100644..100644 --- a/fonts.js +++ b/fonts.js @@@ -185,43 -125,7 +190,43 @@@ if (!isWorker) var FontLoader = { listeningForFontLoad: false, - bind: function fontLoaderBind(fonts, callback) { + /** + * Attach the fontObj to the DOM. + */ + bindDOM: function font_bindDom(fontObj) { + // The browser isn't loading a font until it's used on the page. Attaching + // a hidden div that uses the font 'tells' the browser to load the font. + var div = document.createElement('div'); + div.setAttribute('style', + 'visibility: hidden;' + + 'width: 10px; height: 10px;' + + 'position: absolute; top: 0px; left: 0px;' + + 'font-family: ' + fontObj.loadedName); + div.innerHTML = "Hi"; + document.body.appendChild(div); + + // Add the font-face rule to the document + var fontName = fontObj.loadedName; + var url = ('url(data:' + fontObj.mimetype + ';base64,' + + window.btoa(fontObj.str) + ');'); + var rule = "@font-face { font-family:'" + fontName + "';src:" + url + '}'; + var styleSheet = document.styleSheets[0]; + styleSheet.insertRule(rule, styleSheet.cssRules.length); + return rule; + }, + + bind: function fontLoaderBind(fonts, callback, objects) { + var fontsToLoad = {}; + // check if there are twice the same font. + for (var i = 0; i < fonts.length; i++) { + var fontName = fonts[i].loadedName; + if (fontsToLoad[fontName]) { + throw "Got twice the same font!"; + } else { + fontsToLoad[fontName] = true; + } + } - ++ function checkFontsLoaded() { for (var i = 0; i < objs.length; i++) { var fontObj = objs[i]; @@@ -611,8 -415,8 +616,12 @@@ var Font = (function Font() var constructor = function font_constructor(name, file, properties) { this.name = name; this.encoding = properties.encoding; - this.glyphs = properties.glyphs; ++ ++ // Glyhps are no needed anymore? MERGE ++ // this.glyphs = properties.glyphs; + this.loadedName = properties.loadedName; + this.coded = properties.coded; + this.resources = properties.resources; this.sizes = []; var names = name.split('+'); @@@ -640,6 -450,9 +655,10 @@@ // name ArialBlack for example will be replaced by Helvetica. this.black = (name.search(/Black/g) != -1); + this.defaultWidth = properties.defaultWidth; - this.loadedName = fontName.split('-')[0]; ++ // MERGE ++ // this.loadedName = fontName.split('-')[0]; + this.composite = properties.composite; this.loading = false; return; } @@@ -673,16 -487,11 +693,20 @@@ } this.data = data; - this.type = properties.type; - this.textMatrix = properties.textMatrix; ++ // MERGE ++ //this.textMatrix = properties.textMatrix; + this.type = type; + this.fontMatrix = properties.fontMatrix; + this.defaultWidth = properties.defaultWidth; - this.loadedName = getUniqueName(); ++ this.loadedName = properties.loadedName; this.composite = properties.composite; + + // TODO: Remove this once we can be sure nothing got broken to du changes + // in this commit. + if (!this.loadedName) { + throw "There has to be a `loadedName`"; + } + this.loading = true; }; @@@ -1474,6 -1399,182 +1609,153 @@@ } return stringToArray(otf.file); + }, + + fixWidths: function font_fixWidths(properties) { + if (properties.type !== 'CIDFontType0' && + properties.type !== 'CIDFontType2') + return; + + var encoding = properties.encoding; + if (encoding[0]) + return; + var glyphsWidths = properties.widths; + if (!glyphsWidths) + return; + + var defaultWidth = properties.defaultWidth; + var cidSystemInfo = properties.cidSystemInfo; + var cidToUnicode; + if (cidSystemInfo) { + cidToUnicode = CIDToUnicodeMaps[ + cidSystemInfo.registry + '-' + cidSystemInfo.ordering]; + } + if (!cidToUnicode) { + // the font is directly characters to glyphs with no encoding + // so create an identity encoding + for (i = 0; i < 0xD800; i++) { + var width = glyphsWidths[i]; + encoding[i] = { + unicode: i, + width: isNum(width) ? width : defaultWidth + }; + } + // skipping surrogates + 256-user defined + for (i = 0xE100; i <= 0xFFFF; i++) { + var width = glyphsWidths[i]; + encoding[i] = { + unicode: i, + width: isNum(width) ? width : defaultWidth + }; + } + return; + } + + encoding[0] = { unicode: 0, width: 0 }; + var glyph = 1, i, j, k; + for (i = 0; i < cidToUnicode.length; ++i) { + var unicode = cidToUnicode[i]; + var width; + if (isArray(unicode)) { + var length = unicode.length; + width = glyphsWidths[glyph]; + for (j = 0; j < length; j++) { + k = unicode[j]; + encoding[k] = { + unicode: k, + width: isNum(width) ? width : defaultWidth + }; + } + glyph++; + } else if (typeof unicode === 'object') { + var fillLength = unicode.f; + if (fillLength) { + k = unicode.c; + for (j = 0; j < fillLength; ++j) { + width = glyphsWidths[glyph++]; + encoding[k] = { + unicode: k, + width: isNum(width) ? width : defaultWidth + }; + k++; + } + } else + glyph += unicode.s; + } else if (unicode) { + width = glyphsWidths[glyph++]; + encoding[unicode] = { + unicode: unicode, + width: isNum(width) ? width : defaultWidth + }; + } else + glyph++; + } + }, + - bindWorker: function font_bindWorker(data) { - postMessage({ - action: 'font', - data: { - raw: data, - fontName: this.loadedName, - mimetype: this.mimetype - } - }); - }, - - bindDOM: function font_bindDom(data) { - var fontName = this.loadedName; - - // Add the font-face rule to the document - var url = ('url(data:' + this.mimetype + ';base64,' + - window.btoa(data) + ');'); - var rule = "@font-face { font-family:'" + fontName + "';src:" + url + '}'; - var styleSheet = document.styleSheets[0]; - if (!styleSheet) { - document.documentElement.firstChild.appendChild( - document.createElement('style')); - styleSheet = document.styleSheets[0]; - } - styleSheet.insertRule(rule, styleSheet.cssRules.length); - - return rule; - }, - + charsToGlyphs: function fonts_chars2Glyphs(chars) { + var charsCache = this.charsCache; + var glyphs; + + // if we translated this string before, just grab it from the cache + if (charsCache) { + glyphs = charsCache[chars]; + if (glyphs) + return glyphs; + } + + // lazily create the translation cache + if (!charsCache) + charsCache = this.charsCache = Object.create(null); + + // translate the string using the font's encoding + var encoding = this.encoding; + if (!encoding) + return chars; + + glyphs = []; + + if (this.composite) { + // composite fonts have multi-byte strings convert the string from + // single-byte to multi-byte + // XXX assuming CIDFonts are two-byte - later need to extract the + // correct byte encoding according to the PDF spec + var length = chars.length - 1; // looping over two bytes at a time so + // loop should never end on the last byte + for (var i = 0; i < length; i++) { + var charcode = int16([chars.charCodeAt(i++), chars.charCodeAt(i)]); + var glyph = encoding[charcode]; + if ('undefined' == typeof(glyph)) { + warn('Unencoded charcode ' + charcode); + glyph = { + unicode: charcode, + width: this.defaultWidth + }; + } + glyphs.push(glyph); + // placing null after each word break charcode (ASCII SPACE) + if (charcode == 0x20) + glyphs.push(null); + } + } + else { + for (var i = 0; i < chars.length; ++i) { + var charcode = chars.charCodeAt(i); + var glyph = encoding[charcode]; + if ('undefined' == typeof(glyph)) { + warn('Unencoded charcode ' + charcode); + glyph = { + unicode: charcode, + width: this.defaultWidth + }; + } + glyphs.push(glyph); + if (charcode == 0x20) + glyphs.push(null); + } + } + + // Enter the translated string into the cache + return (charsCache[chars] = glyphs); } }; diff --cc pdf.js index d7088e6,e804126..afb2cea --- a/pdf.js +++ b/pdf.js @@@ -859,39 -895,12 +895,39 @@@ var PredictorStream = (function predict return constructor; })(); +var JpegStreamIR = (function() { + function JpegStreamIR(objId, IR, objs) { + var src = 'data:image/jpeg;base64,' + window.btoa(IR); + + // create DOM image + var img = new Image(); + img.onload = (function() { + this.loaded = true; + + objs.resolve(objId, this); + + if (this.onLoad) + this.onLoad(); + }).bind(this); + img.src = src; + this.domImage = img; + } + + JpegStreamIR.prototype = { + getImage: function() { + return this.domImage; + } + } + + return JpegStreamIR; +})() + // A JpegStream can't be read directly. We use the platform to render // the underlying JPEG data for us. - var JpegStream = (function() { - function isYcckImage(bytes) { + var JpegStream = (function jpegStream() { + function isAdobeImage(bytes) { var maxBytesScanned = Math.max(bytes.length - 16, 1024); - // Looking for APP14, 'Adobe' and transform = 2 + // Looking for APP14, 'Adobe' for (var i = 0; i < maxBytesScanned; ++i) { if (bytes[i] == 0xFF && bytes[i + 1] == 0xEE && bytes[i + 2] == 0x00 && bytes[i + 3] == 0x0E && @@@ -923,19 -932,26 +959,25 @@@ // TODO: per poppler, some images may have "junk" before that // need to be removed this.dict = dict; - - if (isYcckImage(bytes)) - bytes = fixYcckImage(bytes); + if (isAdobeImage(bytes)) + bytes = fixAdobeImage(bytes); + + // create DOM image + var img = new Image(); + img.onload = (function jpegStreamOnload() { + this.loaded = true; + if (this.onLoad) + this.onLoad(); + }).bind(this); - img.src = 'data:image/jpeg;base64,' + window.btoa(bytesToString(bytes)); - this.domImage = img; + this.src = bytesToString(bytes); } constructor.prototype = { - getImage: function jpegStreamGetImage() { - return this.domImage; + getIR: function() { + return this.src; }, - - getChar: function() { + getChar: function jpegStreamGetChar() { error('internal error: getChar is not valid on JpegStream'); } }; @@@ -943,7 -959,45 +985,7 @@@ return constructor; })(); - var DecryptStream = (function() { -// Simple object to track the loading images -// Initialy for every that is in loading call imageLoading() -// and, when images onload is fired, call imageLoaded() -// When all images are loaded, the onLoad event is fired. -var ImagesLoader = (function imagesLoader() { - function constructor() { - this.loading = 0; - } - - constructor.prototype = { - imageLoading: function imagesLoaderImageLoading() { - ++this.loading; - }, - - imageLoaded: function imagesLoaderImageLoaded() { - if (--this.loading == 0 && this.onLoad) { - this.onLoad(); - delete this.onLoad; - } - }, - - bind: function imagesLoaderBind(jpegStream) { - if (jpegStream.loaded || jpegStream.onLoad) - return; - this.imageLoading(); - jpegStream.onLoad = this.imageLoaded.bind(this); - }, - - notifyOnLoad: function imagesLoaderNotifyOnLoad(callback) { - if (this.loading == 0) - callback(); - this.onLoad = callback; - } - }; - - return constructor; -})(); - + var DecryptStream = (function decryptStream() { function constructor(str, decrypt) { this.str = str; this.dict = str.dict; @@@ -3338,36 -3554,52 +3542,37 @@@ var Page = (function pagePage() } return shadow(this, 'rotate', rotate); }, - startRendering: function pageStartRendering(canvasCtx, continuation) { + - startRenderingFromIRQueue: function(gfx, IRQueue, fonts, continuation) { ++ startRenderingFromIRQueue: function startRenderingFromIRQueue(gfx, IRQueue, fonts, continuation) { var self = this; - var stats = self.stats; - stats.compile = stats.fonts = stats.render = 0; + this.IRQueue = IRQueue; + - var displayContinuation = function() { ++ var displayContinuation = function pageDisplayContinuation() { + console.log('--display--'); + - var gfx = new CanvasGraphics(canvasCtx); - var fonts = []; - var images = new ImagesLoader(); - - this.compile(gfx, fonts, images); - stats.compile = Date.now(); - - var displayContinuation = function pageDisplayContinuation() { // Always defer call to display() to work around bug in // Firefox error reporting from XHR callbacks. - setTimeout(function() { + setTimeout(function pageSetTimeout() { var exc = null; try { - self.display(gfx); - stats.render = Date.now(); + self.display(gfx, continuation); } catch (e) { exc = e.toString(); + continuation(exc); + throw e; } - if (continuation) continuation(exc); }); }; - - var fontObjs = FontLoader.bind( - fonts, - function pageFontObjs() { - stats.fonts = Date.now(); - images.notifyOnLoad(function pageNotifyOnLoad() { - stats.images = Date.now(); - displayContinuation(); - }); - }); - - for (var i = 0, ii = fonts.length; i < ii; ++i) - fonts[i].dict.fontObj = fontObjs[i]; + + this.ensureFonts(fonts, function() { + displayContinuation(); + }); }, - - compile: function pageCompile(gfx, fonts, images) { - if (this.code) { + getIRQueue: function(handler, dependency) { + if (this.IRQueue) { // content was compiled - return; + return this.IRQueue; } var xref = this.xref; @@@ -3416,25 -3625,10 +3621,25 @@@ width: this.width, height: this.height, rotate: this.rotate }); - gfx.execute(this.code, xref, resources); - gfx.endDrawing(); + + var startIdx = 0; + var length = this.IRQueue.fnArray.length; + var IRQueue = this.IRQueue; + + var self = this; + var startTime = Date.now(); + function next() { + startIdx = gfx.executeIRQueue(IRQueue, startIdx, next); + if (startIdx == length) { + self.stats.render = Date.now(); + console.log("page=%d - executeIRQueue: time=%dms", + self.pageNumber + 1, self.stats.render - startTime); + callback(); + } + } + next(); }, - rotatePoint: function(x, y) { + rotatePoint: function pageRotatePoint(x, y) { var rotate = this.rotate; switch (rotate) { case 180: @@@ -4055,12 -4264,7 +4275,12 @@@ var EvalState = (function evalState() return constructor; })(); +var FontsMap = {}; +var FontLoadedCounter = 0; + +var objIdCounter = 0; + - var PartialEvaluator = (function() { + var PartialEvaluator = (function partialEvaluator() { function constructor() { this.state = new EvalState(); this.stateStack = []; @@@ -4164,102 -4368,32 +4384,116 @@@ }; constructor.prototype = { - getIRQueue: function(stream, xref, resources, queue, handler, - evaluate: function partialEvaluatorEvaluate(stream, xref, resources, fonts, - images) { ++ getIRQueue: function partialEvaluatorGetIRQueue(stream, xref, resources, queue, handler, + uniquePrefix, dependency) { + + function insertDependency(depList) { + fnArray.push("dependency"); + argsArray.push(depList); + for (var i = 0; i < depList.length; i++) { + var dep = depList[i]; + if (dependency.indexOf(dep) == -1) { + dependency.push(depList[i]); + } + } + } + + function buildPaintImageXObject(image, inline) { + var dict = image.dict; + var w = dict.get('Width', 'W'); + var h = dict.get('Height', 'H'); + + if (image instanceof JpegStream) { + var objId = 'img_' + ++objIdCounter; + handler.send("obj", [objId, "JpegStream", image.getIR()]); + + // Add the dependency on the image object. + insertDependency([objId]); + + // The normal fn. + fn = 'paintJpegXObject'; + args = [ objId, w, h ]; + } else { + // Needs to be rendered ourself. + + // Figure out if the image has an imageMask. + var imageMask = dict.get('ImageMask', 'IM') || false; + + // If there is no imageMask, create the PDFImage and a lot + // of image processing can be done here. + if (!imageMask) { + var imageObj = new PDFImage(xref, resources, image, inline); + + if (imageObj.imageMask) { + throw "Can't handle this in the web worker :/"; + } + + var imgData = { + width: w, + height: h, + data: new Uint8Array(w * h * 4) + }; + var pixels = imgData.data; + imageObj.fillRgbaBuffer(pixels, imageObj.decode); + + fn = "paintImageXObject"; + args = [ imgData ]; + } else /* imageMask == true */ { + // This depends on a tmpCanvas beeing filled with the + // current fillStyle, such that processing the pixel + // data can't be done here. Instead of creating a + // complete PDFImage, only read the information needed + // for later. + fn = "paintImageMaskXObject"; + + var width = dict.get('Width', 'W'); + var height = dict.get('Height', 'H'); + var bitStrideLength = (width + 7) >> 3; + var imgArray = image.getBytes(bitStrideLength * height); + var decode = dict.get('Decode', 'D'); + var inverseDecode = !!decode && decode[0] > 0; + + args = [ imgArray, inverseDecode, width, height ]; + } + } + } + + uniquePrefix = uniquePrefix || ""; + if (!queue.argsArray) { + queue.argsArray = [] + } + if (!queue.fnArray) { + queue.fnArray = []; + } + + var fnArray = queue.fnArray, argsArray = queue.argsArray; + var dependency = dependency || []; + resources = xref.fetchIfRef(resources) || new Dict(); var xobjs = xref.fetchIfRef(resources.get('XObject')) || new Dict(); var patterns = xref.fetchIfRef(resources.get('Pattern')) || new Dict(); var parser = new Parser(new Lexer(stream), false); - var args = [], argsArray = [], fnArray = [], obj; + var args = [], obj; + var res = resources; - while (!IsEOF(obj = parser.getObj())) { - if (IsCmd(obj)) { + while (!isEOF(obj = parser.getObj())) { + if (isCmd(obj)) { var cmd = obj.cmd; var fn = OP_MAP[cmd]; + if (!fn) { + // invalid content command, trying to recover + if (cmd.substr(-2) == 'BT') { + fn = OP_MAP[cmd.substr(0, cmd.length - 2)]; + // feeding 'BT' on next interation + parser = { + getObj: function() { + parser = this.oldParser; + return { name: 'BT' }; + }, + oldParser: parser + }; + } + } assertWellFormed(fn, "Unknown command '" + cmd + "'"); // TODO figure out how to type-check vararg functions @@@ -4270,34 -4401,15 +4504,34 @@@ // compile tiling patterns var patternName = args[args.length - 1]; // SCN/scn applies patterns along with normal colors - if (IsName(patternName)) { + if (isName(patternName)) { var pattern = xref.fetchIfRef(patterns.get(patternName.name)); if (pattern) { - var dict = IsStream(pattern) ? pattern.dict : pattern; + var dict = isStream(pattern) ? pattern.dict : pattern; var typeNum = dict.get('PatternType'); + + // Type1 is TilingPattern if (typeNum == 1) { - patternName.code = this.evaluate(pattern, xref, - dict.get('Resources'), - fonts, images); + // Create an IR of the pattern code. + var depIdx = dependency.length; + var codeIR = this.getIRQueue(pattern, xref, + dict.get('Resources'), {}, handler, + uniquePrefix, dependency); + + // Add the dependencies that are required to execute the + // codeIR. + insertDependency(dependency.slice(depIdx)); + + args = TilingPattern.getIR(codeIR, dict, args); + } + // Type2 is ShadingPattern. + else if (typeNum == 2) { + var shading = xref.fetchIfRef(dict.get('Shading')); + var matrix = dict.get('Matrix'); + var pattern = Pattern.parseShading(shading, matrix, xref, res, null /*ctx*/); + args = pattern.getIR(); + } else { + error("Unkown PatternType " + typeNum); } } } @@@ -4346,64 -4444,17 +4580,64 @@@ var fontRes = resources.get('Font'); if (fontRes) { fontRes = xref.fetchIfRef(fontRes); - var font = xref.fetchIfRef(fontRes.get(args[0].name)); + var font = xref.fetchIfRef(fontRes.get(fontName)); - assertWellFormed(IsDict(font)); + assertWellFormed(isDict(font)); if (!font.translated) { font.translated = this.translateFont(font, xref, resources); - if (fonts && font.translated) { + if (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); + var loadedName = "font_" + uniquePrefix + (FontLoadedCounter++); + font.translated.properties.loadedName = loadedName; + FontsMap[loadedName] = font; + + handler.send("obj", [ + loadedName, + "Font", + font.translated.name, + font.translated.file, + font.translated.properties + ]); } } + args[0].name = font.translated.properties.loadedName; + + // Ensure the font is ready before the font is set + // and later on used for drawing. + // TODO: This should get insert to the IRQueue only once per + // page. + insertDependency([font.translated.properties.loadedName]); + } else { + // TODO: TOASK: Is it possible to get here? If so, what does + // args[0].name should be like??? } + } else if (cmd == 'EI') { + buildPaintImageXObject(args[0], true); + } + + // Transform some cmds. + switch (fn) { + // Parse the ColorSpace data to a raw format. + case "setFillColorSpace": + case "setStrokeColorSpace": + args = [ ColorSpace.parseToIR(args[0], xref, resources) ]; + break; + case "shadingFill": + var shadingRes = xref.fetchIfRef(res.get('Shading')); + if (!shadingRes) + error('No shading resource found'); + + var shading = xref.fetchIfRef(shadingRes.get(args[0].name)); + if (!shading) + error('No shading object found'); + + var shadingFill = Pattern.parseShading(shading, null, xref, res, /* ctx */ null); + var patternIR = shadingFill.getIR(); + + args = [ patternIR ]; + fn = "shadingFill"; + + break; } fnArray.push(fn); @@@ -4845,15 -4955,8 +5138,15 @@@ function ScratchCanvas(width, height) return canvas; } - var CanvasGraphics = (function() { + var CanvasGraphics = (function canvasGraphics() { - function constructor(canvasCtx, imageCanvas) { + // Defines the time the executeIRQueue gone be executing + // before it stops and shedules a continue of execution. + var kExecutionTime = 50; + // Number of IR commands to execute before checking + // if we execute longer then `kExecutionTime`. + var kExecutionTimeCheck = 500; + + function constructor(canvasCtx, objs) { this.ctx = canvasCtx; this.current = new CanvasExtraState(); this.stateStack = []; @@@ -4890,57 -4992,27 +5183,58 @@@ this.ctx.scale(cw / mediaBox.width, ch / mediaBox.height); }, - executeIRQueue: function(codeIR, executionStartIdx, continueCallback) { - compile: function canvasGraphicsCompile(stream, xref, resources, fonts, - images) { - var pe = new PartialEvaluator(); - return pe.evaluate(stream, xref, resources, fonts, images); - }, ++ executeIRQueue: function canvasGraphicsExecuteIRQueue(codeIR, ++ executionStartIdx, continueCallback) { + var argsArray = codeIR.argsArray; + var fnArray = codeIR.fnArray; + var i = executionStartIdx || 0; + var argsArrayLen = argsArray.length; + + var executionEndIdx; + var startTime = Date.now(); - execute: function canvasGraphicsExecute(code, xref, resources) { - resources = xref.fetchIfRef(resources) || new Dict(); - 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(); + var objs = this.objs; - code(this); + do { + executionEndIdx = Math.min(argsArrayLen, i + kExecutionTimeCheck); + + for (i; i < executionEndIdx; i++) { + if (fnArray[i] !== "dependency") { + this[fnArray[i]].apply(this, argsArray[i]); + } else { + var deps = argsArray[i]; + for (var n = 0; n < deps.length; n++) { + var depObjId = deps[n]; + + // If the promise isn't resolved yet, add the continueCallback + // to the promise and bail out. + if (!objs.isResolved(depObjId)) { + objs.get(depObjId, continueCallback); + return i; + } + } + } + } + + // If the entire IRQueue was executed, stop as were done. + if (i == argsArrayLen) { + return i; + } + // If the execution took longer then a certain amount of time, shedule + // to continue exeution after a short delay. + // However, this is only possible if a 'continueCallback' is passed in. + else if (continueCallback && + (Date.now() - startTime) > kExecutionTime) { + setTimeout(continueCallback, 0); + return i; + } - this.xobjs = savedXobjs; - this.res = savedRes; - this.xref = savedXref; + // If the IRQueue isn't executed completly yet OR the execution time + // was short enough, do another execution round. + } while (true); }, - endDrawing: function() { + endDrawing: function canvasGraphicsEndDrawing() { this.ctx.restore(); }, @@@ -5120,52 -5250,61 +5472,47 @@@ this.current.x = this.current.lineX = 0; this.current.y = this.current.lineY = 0; }, - endText: function() { + endText: function canvasGraphicsEndText() { }, - setCharSpacing: function(spacing) { + setCharSpacing: function canvasGraphicsSetCharSpacing(spacing) { this.current.charSpacing = spacing; }, - setWordSpacing: function(spacing) { + setWordSpacing: function canvasGraphicsSetWordSpacing(spacing) { this.current.wordSpacing = spacing; }, - setHScale: function(scale) { + setHScale: function canvasGraphicsSetHScale(scale) { this.current.textHScale = scale / 100; }, - setLeading: function(leading) { + setLeading: function canvasGraphicsSetLeading(leading) { this.current.leading = -leading; }, - setFont: function(fontRef, size) { + setFont: function canvasGraphicsSetFont(fontRef, size) { - var font; - // the tf command uses a name, but graphics state uses a reference - if (isName(fontRef)) { - font = this.xref.fetchIfRef(this.res.get('Font')); - if (!isDict(font)) - return; - - font = font.get(fontRef.name); - } else if (isRef(fontRef)) { - font = fontRef; - } - font = this.xref.fetchIfRef(font); - if (!font) - error('Referenced font is not found'); - - var fontObj = font.fontObj; + // Lookup the fontObj using fontRef only. + var fontRefName = fontRef.name; + var fontObj = this.objs.get(fontRefName); + + if (!fontObj) { + throw "Can't find font for " + fontRefName; + } + - var name = fontObj.loadedName; - if (!name) { - // TODO: fontDescriptor is not available, fallback to default font - name = 'sans-serif'; - } ++ var name = fontObj.loadedName || 'sans-serif'; + this.current.font = fontObj; this.current.fontSize = size; -- var name = fontObj.loadedName || 'sans-serif'; if (this.ctx.$setFont) { this.ctx.$setFont(name, size); } else { - var bold = fontObj.black ? (fontObj.bold ? 'bolder' : 'bold') : - (fontObj.bold ? 'bold' : 'normal'); - - var italic = fontObj.italic ? 'italic' : 'normal'; - var serif = fontObj.serif ? 'serif' : 'sans-serif'; - var typeface = '"' + name + '", ' + serif; - var rule = italic + ' ' + bold + ' ' + size + 'px ' + typeface; - this.ctx.font = rule; + this.ctx.font = fontObj.getRule(size); } }, - setTextRenderingMode: function(mode) { + setTextRenderingMode: function canvasGraphicsSetTextRenderingMode(mode) { TODO('text rendering mode: ' + mode); }, - setTextRise: function(rise) { + setTextRise: function canvasGraphicsSetTextRise(rise) { TODO('text rise: ' + rise); }, - moveText: function(x, y) { + moveText: function canvasGraphicsMoveText(x, y) { this.current.x = this.current.lineX += x; this.current.y = this.current.lineY += y; if (this.ctx.$setCurrentX) { @@@ -5185,83 -5324,98 +5532,112 @@@ this.current.x = this.current.lineX = 0; this.current.y = this.current.lineY = 0; }, - nextLine: function() { + nextLine: function canvasGraphicsNextLine() { this.moveText(0, this.current.leading); }, - showText: function(text) { ++ + showText: function canvasGraphicsShowText(text) { + // If the current font isn't supported, we can't display the text and + // bail out. + if (!this.current.font.supported) { + return; + } - ++ var ctx = this.ctx; var current = this.current; - var originalText = text; - - ctx.save(); - ctx.transform.apply(ctx, current.textMatrix); - ctx.scale(1, -1); - - ctx.translate(current.x, -1 * current.y); - var font = current.font; - if (font) { - ctx.transform.apply(ctx, font.textMatrix || IDENTITY_MATRIX); - text = font.charsToUnicode(text); - } - - var composite = font.composite; - var encoding = font.encoding; + var glyphs = font.charsToGlyphs(text); var fontSize = current.fontSize; var charSpacing = current.charSpacing; var wordSpacing = current.wordSpacing; var textHScale = current.textHScale; - ctx.scale(1 / textHScale, 1); - - var width = 0; - for (var i = 0; i < text.length; i++) { - if (composite) { - var position = i * 2 + 1; - var charcode = (originalText.charCodeAt(position - 1) << 8) + - originalText.charCodeAt(position); - } else { - var charcode = originalText.charCodeAt(i); + var glyphsLength = glyphs.length; + if (font.coded) { + ctx.save(); + ctx.transform.apply(ctx, current.textMatrix); + ctx.translate(current.x, current.y); + + var fontMatrix = font.fontMatrix || IDENTITY_MATRIX; + ctx.scale(1 / textHScale, 1); + for (var i = 0; i < glyphsLength; ++i) { + + var glyph = glyphs[i]; + if (glyph === null) { + // word break + this.ctx.translate(wordSpacing, 0); + continue; + } + + this.save(); + ctx.scale(fontSize, fontSize); + ctx.transform.apply(ctx, fontMatrix); + this.execute(glyph.code, this.xref, font.resources); + this.restore(); + + var transformed = Util.applyTransform([glyph.width, 0], fontMatrix); + var width = transformed[0] * fontSize + charSpacing; + + ctx.translate(width, 0); + current.x += width; + } + ctx.restore(); + } else { + ctx.save(); + ctx.transform.apply(ctx, current.textMatrix); + ctx.scale(1, -1); + ctx.translate(current.x, -1 * current.y); + ctx.transform.apply(ctx, font.fontMatrix || IDENTITY_MATRIX); + + ctx.scale(1 / textHScale, 1); + + var width = 0; + for (var i = 0; i < glyphsLength; ++i) { + var glyph = glyphs[i]; + if (glyph === null) { + // word break + width += wordSpacing; + continue; + } - var charWidth = font.encoding[charcode].width * fontSize * 0.001; - charWidth += charSpacing; - if (charcode == 32) - charWidth += wordSpacing; + var unicode = glyph.unicode; + var char = (unicode >= 0x10000) ? + String.fromCharCode(0xD800 | ((unicode - 0x10000) >> 10), + 0xDC00 | (unicode & 0x3FF)) : String.fromCharCode(unicode); - ctx.fillText(text.charAt(i), 0, 0); - ctx.translate(charWidth, 0); - width += charWidth; - } - current.x += width; + ctx.fillText(char, width, 0); + width += glyph.width * fontSize * 0.001 + charSpacing; + } + current.x += width; - this.ctx.restore(); + ctx.restore(); + } }, - showSpacedText: function(arr) { ++ + showSpacedText: function canvasGraphicsShowSpacedText(arr) { + // If the current font isn't supported, we can't display the text and + // bail out. + if (!this.current.font.supported) { + return; + } + - for (var i = 0; i < arr.length; ++i) { + var ctx = this.ctx; + var current = this.current; + var fontSize = current.fontSize; + var textHScale = current.textHScale; + var arrLength = arr.length; + for (var i = 0; i < arrLength; ++i) { var e = arr[i]; - if (IsNum(e)) { - if (this.ctx.$addCurrentX) { - this.ctx.$addCurrentX(-e * 0.001 * this.current.fontSize); + if (isNum(e)) { + if (ctx.$addCurrentX) { + ctx.$addCurrentX(-e * 0.001 * fontSize); } else { - this.current.x -= e * 0.001 * this.current.fontSize * - this.current.textHScale; + current.x -= e * 0.001 * fontSize * textHScale; } - } else if (IsString(e)) { + } else if (isString(e)) { this.showText(e); } else { - malformed('TJ array element ' + e + " isn't string or num"); + malformed('TJ array element ' + e + ' is not string or num'); } } }, @@@ -5287,46 -5451,20 +5673,46 @@@ }, // Color - setStrokeColorSpace: function(raw) { - setStrokeColorSpace: function canvasGraphicsSetStrokeColorSpace(space) { ++ setStrokeColorSpace: function canvasGraphicsSetStrokeColorSpacefunction(raw) { this.current.strokeColorSpace = - ColorSpace.parse(space, this.xref, this.res); + ColorSpace.fromIR(raw); }, - setFillColorSpace: function(raw) { - setFillColorSpace: function canvasGraphicsSetFillColorSpace(space) { ++ setFillColorSpace: function canvasGraphicsSetFillColorSpace(raw) { this.current.fillColorSpace = - ColorSpace.parse(space, this.xref, this.res); + ColorSpace.fromIR(raw); }, - setStrokeColor: function(/*...*/) { + setStrokeColor: function canvasGraphicsSetStrokeColor(/*...*/) { var cs = this.current.strokeColorSpace; var color = cs.getRgb(arguments); this.setStrokeRGBColor.apply(this, color); }, - setStrokeColorN: function canvasGraphicsSetStrokeColorN(/*...*/) { + getColorN_IR_Pattern: function(IR, cs) { + if (IR[0] == "TilingPatternIR") { + // First, build the `color` var like it's done in the + // Pattern.prototype.parse function. + var args = IR[1]; + var base = cs.base; + var color; + if (base) { + var baseComps = base.numComps; + + color = []; + for (var i = 0; i < baseComps; ++i) + color.push(args[i]); + + color = base.getRgb(color); + } + + // Build the pattern based on the IR data. + var pattern = new TilingPatternIR(IR, color, this.ctx, this.objs); + } else if (IR[0] == "RadialAxialShading" || IR[0] == "DummyShading") { + var pattern = Pattern.shadingFromIR(this.ctx, IR); + } else { + throw "Unkown IR type"; + } + return pattern; + }, - setStrokeColorN_IR: function(/*...*/) { ++ setStrokeColorN_IR: function canvasGraphicsSetStrokeColorN(/*...*/) { var cs = this.current.strokeColorSpace; if (cs.name == 'Pattern') { @@@ -5340,7 -5483,7 +5726,7 @@@ var color = cs.getRgb(arguments); this.setFillRGBColor.apply(this, color); }, - setFillColorN_IR: function(/*...*/) { - setFillColorN: function canvasGraphicsSetFillColorN(/*...*/) { ++ setFillColorN_IR: function canvasGraphicsSetFillColorN(/*...*/) { var cs = this.current.fillColorSpace; if (cs.name == 'Pattern') { @@@ -5376,11 -5522,24 +5762,11 @@@ this.current.fillColor = color; }, - shadingFill: function(patternIR) { - // Shading - shadingFill: function canvasGraphicsShadingFill(shadingName) { - var xref = this.xref; - var res = this.res; ++ shadingFill: function canvasGraphicsShadingFill(patternIR) { var ctx = this.ctx; - - var shadingRes = xref.fetchIfRef(res.get('Shading')); - if (!shadingRes) - error('No shading resource found'); - - var shading = xref.fetchIfRef(shadingRes.get(shadingName.name)); - if (!shading) - error('No shading object found'); - - var shadingFill = Pattern.parseShading(shading, null, xref, res, ctx); - + this.save(); - ctx.fillStyle = shadingFill.getPattern(); + ctx.fillStyle = Pattern.shadingFromIR(ctx, patternIR); var inv = ctx.mozCurrentTransformInverse; if (inv) { @@@ -5413,21 -5572,59 +5799,23 @@@ }, // Images - beginInlineImage: function() { + beginInlineImage: function canvasGraphicsBeginInlineImage() { error('Should not call beginInlineImage'); }, - beginImageData: function() { + beginImageData: function canvasGraphicsBeginImageData() { error('Should not call beginImageData'); }, - endInlineImage: function canvasGraphicsEndInlineImage(image) { - this.paintImageXObject(null, image, true); - }, - - // XObjects - paintXObject: function canvasGraphicsPaintXObject(obj) { - var xobj = this.xobjs.get(obj.name); - if (!xobj) - return; - xobj = this.xref.fetchIfRef(xobj); - assertWellFormed(isStream(xobj), 'XObject should be a stream'); - - var oc = xobj.dict.get('OC'); - if (oc) { - TODO('oc for xobject'); - } - - var opi = xobj.dict.get('OPI'); - if (opi) { - TODO('opi for xobject'); - } - - var type = xobj.dict.get('Subtype'); - assertWellFormed(isName(type), 'XObject should have a Name subtype'); - if ('Image' == type.name) { - this.paintImageXObject(obj, xobj, false); - } else if ('Form' == type.name) { - this.paintFormXObject(obj, xobj); - } else if ('PS' == type.name) { - warn('(deprecated) PostScript XObjects are not supported'); - } else { - malformed('Unknown XObject subtype ' + type.name); - } - }, - - paintFormXObject: function canvasGraphicsPaintFormXObject(ref, stream) { + - paintFormXObjectBegin: function(matrix, bbox) { ++ paintFormXObjectBegin: function canvasGraphicsPaintFormXObject(matrix, bbox) { this.save(); - if (matrix && IsArray(matrix) && 6 == matrix.length) - var matrix = stream.dict.get('Matrix'); + if (matrix && isArray(matrix) && 6 == matrix.length) this.transform.apply(this, matrix); - if (bbox && IsArray(bbox) && 4 == bbox.length) { - this.rectangle.apply(this, bbox); - var bbox = stream.dict.get('BBox'); + if (bbox && isArray(bbox) && 4 == bbox.length) { + var width = bbox[2] - bbox[0]; + var height = bbox[3] - bbox[1]; + this.rectangle(bbox[0], bbox[1], width, height); this.clip(); this.endPath(); } @@@ -5503,55 -5680,22 +5891,56 @@@ this.restore(); }, + paintImageXObject: function(imgData) { + this.save(); + + var ctx = this.ctx; + var w = imgData.width; + var h = imgData.height; + // scale the image to the unit square + ctx.scale(1 / w, -1 / h); + + var tmpCanvas = new this.ScratchCanvas(w, h); + var tmpCtx = tmpCanvas.getContext('2d'); + var tmpImgData; + + // Deactivating this for now until we have feature detection. + // if (isGecko) { + // tmpImgData = imgData; + // } else { + tmpImgData = tmpCtx.getImageData(0, 0, w, h); + + // Copy over the imageData. + var tmpImgDataPixels = tmpImgData.data; + var len = tmpImgDataPixels.length; + + // TODO: There got to be a better way to copy an ImageData array + // then coping over all the bytes one by one :/ + while (len--) + tmpImgDataPixels[len] = imgData.data[len]; + // } + + tmpCtx.putImageData(tmpImgData, 0, 0); + ctx.drawImage(tmpCanvas, 0, -h); + this.restore(); + }, + // Marked content - markPoint: function(tag) { + markPoint: function canvasGraphicsMarkPoint(tag) { TODO('Marked content'); }, - markPointProps: function(tag, properties) { + markPointProps: function canvasGraphicsMarkPointProps(tag, properties) { TODO('Marked content'); }, - beginMarkedContent: function(tag) { + beginMarkedContent: function canvasGraphicsBeginMarkedContent(tag) { TODO('Marked content'); }, - beginMarkedContentProps: function(tag, properties) { + beginMarkedContentProps: + function canvasGraphicsBeginMarkedContentProps(tag, properties) { TODO('Marked content'); }, - endMarkedContent: function() { + endMarkedContent: function canvasGraphicsEndMarkedContent() { TODO('Marked content'); }, @@@ -5636,59 -5780,9 +6025,59 @@@ var ColorSpace = (function colorSpaceCo }; constructor.parse = function colorspace_parse(cs, xref, res) { + var IR = constructor.parseToIR(cs, xref, res, true); + if (!(IR instanceof SeparationCS)) { + return constructor.fromIR(IR); + } else { + return IR + } + }; + + constructor.fromIR = function(IR) { + var name; - if (IsArray(IR)) { ++ if (isArray(IR)) { + name = IR[0]; + } else { + name = IR; + } + + switch (name) { + case "DeviceGrayCS": + return new DeviceGrayCS(); + case "DeviceRgbCS": + return new DeviceRgbCS(); + case "DeviceCmykCS": + return new DeviceCmykCS(); + case "PatternCS": + var baseCS = IR[1]; + if (baseCS == null) { + return new PatternCS(null); + } else { + return new PatternCS(ColorSpace.fromIR(baseCS)); + } + case "IndexedCS": + var baseCS = IR[1]; + var hiVal = IR[2]; + var lookup = IR[3]; + return new IndexedCS(ColorSpace.fromIR(baseCS), hiVal, lookup) + case "SeparationCS": + var alt = IR[1]; + var tintFnIR = IR[2]; + + return new SeparationCS( + ColorSpace.fromIR(alt), + PDFFunction.fromIR(tintFnIR) + ); + default: + error("Unkown name " + name); + } + return null; + } + + constructor.parseToIR = function colorspace_parse(cs, xref, res, parseOnly) { - if (IsName(cs)) { + if (isName(cs)) { - var colorSpaces = xref.fetchIfRef(res.get('ColorSpace')); + var colorSpaces = res.get('ColorSpace'); - if (IsDict(colorSpaces)) { + if (isDict(colorSpaces)) { var refcs = colorSpaces.get(cs.name); if (refcs) cs = refcs; @@@ -5702,21 -5796,21 +6091,21 @@@ this.mode = mode; switch (mode) { - case 'DeviceGray': - case 'G': - return new DeviceGrayCS(); - case 'DeviceRGB': - case 'RGB': - return new DeviceRgbCS(); - case 'DeviceCMYK': - case 'CMYK': - return new DeviceCmykCS(); - case 'Pattern': - return new PatternCS(null); - default: - error('unrecognized colorspace ' + mode); + case 'DeviceGray': + case 'G': + return "DeviceGrayCS"; + case 'DeviceRGB': + case 'RGB': + return "DeviceRgbCS"; + case 'DeviceCMYK': + case 'CMYK': + return "DeviceCmykCS"; + case 'Pattern': + return ["PatternCS", null]; + default: + error('unrecognized colorspace ' + mode); } - } else if (IsArray(cs)) { + } else if (isArray(cs)) { var mode = cs[0].name; this.mode = mode; @@@ -6028,10 -6123,45 +6417,10 @@@ var Pattern = (function patternPattern( } }; - constructor.shadingFromIR = function(ctx, raw) { - constructor.parse = function pattern_parse(args, cs, xref, res, ctx) { - var length = args.length; - - var patternName = args[length - 1]; - if (!isName(patternName)) - error('Bad args to getPattern: ' + patternName); - - var patternRes = xref.fetchIfRef(res.get('Pattern')); - if (!patternRes) - error('Unable to find pattern resource'); - - var pattern = xref.fetchIfRef(patternRes.get(patternName.name)); - var dict = isStream(pattern) ? pattern.dict : pattern; - var typeNum = dict.get('PatternType'); - - switch (typeNum) { - case 1: - var base = cs.base; - var color; - if (base) { - var baseComps = base.numComps; - - color = []; - for (var i = 0; i < baseComps; ++i) - color.push(args[i]); - - color = base.getRgb(color); - } - var code = patternName.code; - return new TilingPattern(pattern, code, dict, color, xref, ctx); - case 2: - var shading = xref.fetchIfRef(dict.get('Shading')); - var matrix = dict.get('Matrix'); - return Pattern.parseShading(shading, matrix, xref, res, ctx); - default: - error('Unknown type of pattern: ' + typeNum); - } - return null; - }; ++ constructor.shadingFromIR = function pattern_shadingFromIR(ctx, raw) { + var obj = window[raw[0]]; + return obj.fromIR(ctx, raw); + } constructor.parseShading = function pattern_shading(shading, matrix, xref, res, ctx) { @@@ -6102,11 -6229,11 +6491,11 @@@ var RadialAxialShading = (function radi var fnObj = dict.get('Function'); fnObj = xref.fetchIfRef(fnObj); - if (IsArray(fnObj)) + if (isArray(fnObj)) error('No support for array of functions'); - else if (!IsPDFFunction(fnObj)) + else if (!isPDFFunction(fnObj)) error('Invalid function'); - var fn = new PDFFunction(xref, fnObj); + var fn = PDFFunction.parse(xref, fnObj); // 10 samples seems good enough for now, but probably won't work // if there are sharp color changes. Ideally, we would implement @@@ -6124,51 -6251,19 +6513,51 @@@ this.colorStops = colorStops; } + constructor.fromIR = function(ctx, raw) { + var type = raw[1]; + var colorStops = raw[2]; + var p0 = raw[3]; + var p1 = raw[4]; + var r0 = raw[5]; + var r1 = raw[6]; + + var curMatrix = ctx.mozCurrentTransform; + if (curMatrix) { + var userMatrix = ctx.mozCurrentTransformInverse; + + p0 = Util.applyTransform(p0, curMatrix); + p0 = Util.applyTransform(p0, userMatrix); + + p1 = Util.applyTransform(p1, curMatrix); + p1 = Util.applyTransform(p1, userMatrix); + } + + if (type == 2) + var grad = ctx.createLinearGradient(p0[0], p0[1], p1[0], p1[1]); + else if (type == 3) + var grad = ctx.createRadialGradient(p0[0], p0[1], r0, p1[0], p1[1], r1); + + for (var i = 0, ii = colorStops.length; i < ii; ++i) { + var c = colorStops[i]; + grad.addColorStop(c[0], c[1]); + } + return grad; + } + constructor.prototype = { - getIR: function() { - getPattern: function radialAxialShadingGetPattern() { ++ getIR: function RadialAxialShading_getIR() { var coordsArr = this.coordsArr; var type = this.shadingType; - var p0, p1, r0, r1; if (type == 2) { - p0 = [coordsArr[0], coordsArr[1]]; - p1 = [coordsArr[2], coordsArr[3]]; + var p0 = [coordsArr[0], coordsArr[1]]; + var p1 = [coordsArr[2], coordsArr[3]]; + var r0 = null; + var r1 = null; } else if (type == 3) { - p0 = [coordsArr[0], coordsArr[1]]; - p1 = [coordsArr[3], coordsArr[4]]; - r0 = coordsArr[2]; - r1 = coordsArr[5]; + var p0 = [coordsArr[0], coordsArr[1]]; + var p1 = [coordsArr[3], coordsArr[4]]; + var r0 = coordsArr[2]; + var r1 = coordsArr[5]; } else { error('getPattern type unknown: ' + type); } @@@ -6186,84 -6304,95 +6575,86 @@@ return constructor; })(); - var TilingPatternIR = (function() { -var TilingPattern = (function tilingPattern() { ++var TilingPatternIR = (function tilingPattern() { var PAINT_TYPE_COLORED = 1, PAINT_TYPE_UNCOLORED = 2; - function constructor(pattern, code, dict, color, xref, ctx) { - function multiply(m, tm) { - var a = m[0] * tm[0] + m[1] * tm[2]; - var b = m[0] * tm[1] + m[1] * tm[3]; - var c = m[2] * tm[0] + m[3] * tm[2]; - var d = m[2] * tm[1] + m[3] * tm[3]; - var e = m[4] * tm[0] + m[5] * tm[2] + tm[4]; - var f = m[4] * tm[1] + m[5] * tm[3] + tm[5]; - return [a, b, c, d, e, f]; - } - - TODO('TilingType'); - - this.matrix = dict.get('Matrix'); - this.curMatrix = ctx.mozCurrentTransform; - this.invMatrix = ctx.mozCurrentTransformInverse; - this.ctx = ctx; - this.type = 'Pattern'; - - var bbox = dict.get('BBox'); - var x0 = bbox[0], y0 = bbox[1], x1 = bbox[2], y1 = bbox[3]; - - var xstep = dict.get('XStep'); - var ystep = dict.get('YStep'); - - var topLeft = [x0, y0]; - // we want the canvas to be as large as the step size - var botRight = [x0 + xstep, y0 + ystep]; - - var width = botRight[0] - topLeft[0]; - var height = botRight[1] - topLeft[1]; - - // TODO: hack to avoid OOM, we would idealy compute the tiling - // pattern to be only as large as the acual size in device space - // This could be computed with .mozCurrentTransform, but still - // needs to be implemented - while (Math.abs(width) > 512 || Math.abs(height) > 512) { - width = 512; - height = 512; - } + function TilingPatternIR(IR, color, ctx, objs) { + // "Unfolding" the IR. + var IRQueue = IR[2]; + this.matrix = IR[3]; + var bbox = IR[4]; + var xstep = IR[5]; + var ystep = IR[6]; + var paintType = IR[7]; - var tmpCanvas = new ScratchCanvas(width, height); + // + TODO('TilingType'); - // set the new canvas element context as the graphics context - var tmpCtx = tmpCanvas.getContext('2d'); - var graphics = new CanvasGraphics(tmpCtx); - - var paintType = dict.get('PaintType'); - switch (paintType) { - case PAINT_TYPE_COLORED: - tmpCtx.fillStyle = ctx.fillStyle; - tmpCtx.strokeStyle = ctx.strokeStyle; - break; - case PAINT_TYPE_UNCOLORED: - color = Util.makeCssRgb.apply(this, color); - tmpCtx.fillStyle = color; - tmpCtx.strokeStyle = color; - break; - default: - error('Unsupported paint type: ' + paintType); - } + this.curMatrix = ctx.mozCurrentTransform; + this.invMatrix = ctx.mozCurrentTransformInverse; + this.ctx = ctx; + this.type = 'Pattern'; - var scale = [width / xstep, height / ystep]; - this.scale = scale; + var x0 = bbox[0], y0 = bbox[1], x1 = bbox[2], y1 = bbox[3]; + var topLeft = [x0, y0]; + // we want the canvas to be as large as the step size + var botRight = [x0 + xstep, y0 + ystep]; + + var width = botRight[0] - topLeft[0]; + var height = botRight[1] - topLeft[1]; + + // TODO: hack to avoid OOM, we would idealy compute the tiling + // pattern to be only as large as the acual size in device space + // This could be computed with .mozCurrentTransform, but still + // needs to be implemented + while (Math.abs(width) > 512 || Math.abs(height) > 512) { + width = 512; + height = 512; + } - // transform coordinates to pattern space - var tmpTranslate = [1, 0, 0, 1, -topLeft[0], -topLeft[1]]; - var tmpScale = [scale[0], 0, 0, scale[1], 0, 0]; - graphics.transform.apply(graphics, tmpScale); - graphics.transform.apply(graphics, tmpTranslate); + var tmpCanvas = new ScratchCanvas(width, height); + + // set the new canvas element context as the graphics context + var tmpCtx = tmpCanvas.getContext('2d'); + var graphics = new CanvasGraphics(tmpCtx, objs); + + switch (paintType) { + case PAINT_TYPE_COLORED: + tmpCtx.fillStyle = ctx.fillStyle; + tmpCtx.strokeStyle = ctx.strokeStyle; + break; + case PAINT_TYPE_UNCOLORED: + color = Util.makeCssRgb.apply(this, color); + tmpCtx.fillStyle = color; + tmpCtx.strokeStyle = color; + break; + default: + error('Unsupported paint type: ' + paintType); + } - if (bbox && isArray(bbox) && 4 == bbox.length) { - var bboxWidth = bbox[2] - bbox[0]; - var bboxHeight = bbox[3] - bbox[1]; - graphics.rectangle(bbox[0], bbox[1], bboxWidth, bboxHeight); - graphics.clip(); - graphics.endPath(); - } + var scale = [width / xstep, height / ystep]; + this.scale = scale; + + // transform coordinates to pattern space + var tmpTranslate = [1, 0, 0, 1, -topLeft[0], -topLeft[1]]; + var tmpScale = [scale[0], 0, 0, scale[1], 0, 0]; + graphics.transform.apply(graphics, tmpScale); + graphics.transform.apply(graphics, tmpTranslate); + - if (bbox && IsArray(bbox) && 4 == bbox.length) { - graphics.rectangle.apply(graphics, bbox); ++ if (bbox && isArray(bbox) && 4 == bbox.length) { ++ var bboxWidth = bbox[2] - bbox[0]; ++ var bboxHeight = bbox[3] - bbox[1]; ++ graphics.rectangle(bbox[0], bbox[1], bboxWidth, bboxHeight); + graphics.clip(); + graphics.endPath(); + } - var res = xref.fetchIfRef(dict.get('Resources')); - graphics.execute(code, xref, res); + graphics.executeIRQueue(IRQueue); - this.canvas = tmpCanvas; + this.canvas = tmpCanvas; } - constructor.prototype = { + TilingPatternIR.prototype = { getPattern: function tiling_getPattern() { var matrix = this.matrix; var curMatrix = this.curMatrix; @@@ -6280,24 -6409,12 +6671,24 @@@ return ctx.createPattern(this.canvas, 'repeat'); } - }; - return constructor; + } + + return TilingPatternIR; })(); +var TilingPattern = { + getIR: function(codeIR, dict, args) { + var matrix = dict.get('Matrix'); + var bbox = dict.get('BBox'); + var xstep = dict.get('XStep'); + var ystep = dict.get('YStep'); + var paintType = dict.get('PaintType'); + + return ["TilingPatternIR", args, codeIR, matrix, bbox, xstep, ystep, paintType]; + } +}; - var PDFImage = (function() { + var PDFImage = (function pdfImage() { function constructor(xref, res, image, inline) { this.image = image; if (image.getParams) { @@@ -6663,10 -6754,36 +7092,10 @@@ var PDFFunction = (function() } return output; - }; - }, - getSampleArray: function pdfFunctionGetSampleArray(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 pdfFunctionConstructInterpolated(str, - dict) { + - constructInterpolated: function(str, dict) { ++ constructInterpolated: function pdfFunctionConstructInterpolated(str, dict) { var c0 = dict.get('C0') || [0]; var c1 = dict.get('C1') || [1]; var n = dict.get('N'); @@@ -6679,18 -6796,7 +7108,18 @@@ for (var i = 0; i < length; ++i) diff.push(c1[i] - c0[i]); - this.func = function pdfFunctionConstructInterpolatedFunc(args) { + return [ CONSTRUCT_INTERPOLATED, c0, diff, n, i ]; + }, + - constructInterpolatedFromIR: function(IR) { ++ constructInterpolatedFromIR: function pdfFunctionconstructInterpolatedFromIR(IR) { + var c0 = IR[1]; + var diff = IR[2]; + var n = IR[3]; + var i = IR[4]; + + var length = diff.length; + + return function(args) { var x = args[0]; var out = []; @@@ -6698,11 -6804,9 +7127,11 @@@ out.push(c0[j] + (x^n * diff[i])); return out; - }; + + } }, + - constructStiched: function(fn, dict, xref) { + constructStiched: function pdfFunctionConstructStiched(fn, dict, xref) { var domain = dict.get('Domain'); var range = dict.get('Range'); @@@ -6721,22 -6825,8 +7150,22 @@@ var bounds = dict.get('Bounds'); var encode = dict.get('Encode'); - this.func = function pdfFunctionConstructStichedFunc(args) { - var clip = function pdfFunctionConstructStichedFuncClip(v, min, max) { + return [ CONSTRUCT_STICHED, domain, bounds, encoding, fns ]; + }, + - constructStichedFromIR: function(IR) { ++ constructStichedFromIR: function pdfFunctionConstructStichedFromIR(IR) { + var domain = IR[1]; + var bounds = IR[2]; + var encoding = IR[3]; + var fnsIR = IR[4]; + var fns = []; + + for (var i = 0; i < fnsIR.length; i++) { + fns.push(PDFFunction.fromIR(fnsIR[i])); + } + + return function(args) { + var clip = function(v, min, max) { if (v > max) v = max; else if (v < min) @@@ -6766,17 -6856,12 +7195,17 @@@ var v2 = rmin + (v - dmin) * (rmax - rmin) / (dmax - dmin); // call the appropropriate function - return fns[i].func([v2]); + return fns[i]([v2]); }; }, + - constructPostScript: function() { + constructPostScript: function pdfFunctionConstructPostScript() { + return [ CONSTRUCT_POSTSCRIPT ]; + }, + - constructPostScriptFromIR: function() { ++ constructPostScriptFromIR: function pdfFunctionConstructPostScriptFromIR() { TODO('unhandled type of function'); - this.func = function pdfFunctionConstructPostScriptFunc() { + return function() { return [255, 105, 180]; }; } diff --cc test/driver.js index 79f9456,4bc14bf..546397b --- a/test/driver.js +++ b/test/driver.js @@@ -47,14 -49,22 +49,27 @@@ function load() }; r.send(null); } - window.onload = load; + + function cleanup() { + var styleSheet = document.styleSheets[0]; + if (styleSheet) { + while (styleSheet.cssRules.length > 0) + styleSheet.deleteRule(0); + } + var guard = document.getElementById('content-end'); + var body = document.body; + while (body.lastChild !== guard) + body.removeChild(body.lastChild); + } function nextTask() { + // If there is a pdfDoc on the last task executed, destroy it to free memory. + if (task && task.pdfDoc) { + task.pdfDoc.destroy(); + delete task.pdfDoc; + } + cleanup(); + if (currentTaskIdx == manifest.length) { return done(); } @@@ -63,26 -73,16 +78,16 @@@ log('Loading file "' + task.file + '"\n'); - var r = new XMLHttpRequest(); - r.open('GET', task.file); - r.mozResponseType = r.responseType = 'arraybuffer'; - r.onreadystatechange = function() { + getPdf(task.file, function nextTaskGetPdf(data) { var failure; - if (r.readyState == 4) { - var data = r.mozResponseArrayBuffer || r.mozResponse || - r.responseArrayBuffer || r.response; - - try { - task.pdfDoc = new WorkerPDFDoc(data); - // task.pdfDoc = new PDFDoc(new Stream(data)); - } catch (e) { - failure = 'load PDF doc : ' + e.toString(); - } - - task.pageNum = task.firstPage || 1, nextPage(task, failure); + try { + task.pdfDoc = new PDFDoc(data); + } catch (e) { + failure = 'load PDF doc : ' + e.toString(); } - }; - r.send(null); - task.pageNum = 1; ++ task.pageNum = task.firstPage || 1; + nextPage(task, failure); + }); } function isLastPage(task) { diff --cc test/test_slave.html index bbec69d,9d8185b..ace21e1 --- a/test/test_slave.html +++ b/test/test_slave.html @@@ -7,11 -7,9 +7,13 @@@ + + + + + + diff --cc web/viewer.html index 6168710,160c5ce..beab965 --- a/web/viewer.html +++ b/web/viewer.html @@@ -11,9 -11,8 +11,11 @@@ + + + + + diff --cc web/viewer.js index 3d994df,34a1cf8..11db7cc --- a/web/viewer.js +++ b/web/viewer.js @@@ -152,15 -181,10 +181,10 @@@ var PDFView = while (container.hasChildNodes()) container.removeChild(container.lastChild); - var pdf; - if (true /* Use Worker */) { - pdf = new WorkerPDFDoc(data); - } else { - pdf = new PDFDoc(new Stream(data)); - } - - var pdf = new PDFDoc(data); ++ var pdf = new WorkerPDFDoc(data); var pagesCount = pdf.numPages; document.getElementById('numPages').innerHTML = pagesCount; + document.getElementById('pageNumber').max = pagesCount; var pages = this.pages = []; var pagesRefMap = {}; diff --cc worker.js index 0f686b4,0000000..646d9d4 mode 100644,000000..100644 --- a/worker.js +++ b/worker.js @@@ -1,419 -1,0 +1,423 @@@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +'use strict'; + +// Set this to true if you want to use workers. +var useWorker = false; + +var WorkerPage = (function() { + function constructor(workerPDF, page, objs) { + this.workerPDF = workerPDF; + this.page = page; + this.objs = objs; + + this.ref = page.ref; + } + + constructor.prototype = { + get width() { + return this.page.width; + }, + + get height() { + return this.page.height; + }, + + get stats() { + return this.page.stats; + }, + ++ get view() { ++ return this.page.view; ++ }, ++ + startRendering: function(ctx, callback, errback) { + this.ctx = ctx; + this.callback = callback; + // TODO: Place the worker magic HERE. + // this.page.startRendering(ctx, callback, errback); + + this.startRenderingTime = Date.now(); + this.workerPDF.startRendering(this) + }, + + startRenderingFromIRQueue: function(IRQueue, fonts) { + var gfx = new CanvasGraphics(this.ctx, this.objs); + + var startTime = Date.now(); + var callback = function(err) { + var pageNum = this.page.pageNumber + 1; + console.log("page=%d - rendering time: time=%dms", + pageNum, Date.now() - startTime); + console.log("page=%d - total time: time=%dms", + pageNum, Date.now() - this.startRenderingTime); + + this.callback(err); + }.bind(this); + this.page.startRenderingFromIRQueue(gfx, IRQueue, fonts, callback); + }, + + getLinks: function() { + return this.page.getLinks(); + } + }; + + return constructor; +})(); + +var PDFObjects = (function() { + function PDFObjects() { + this.objs = {}; + } + + PDFObjects.prototype = { + objs: null, + + /** + * Ensures there is an object defined for `objId`. Stores `data` on the + * object *if* it is created. + */ + ensureObj: function(objId, data) { + if (!this.objs[objId]) { + return this.objs[objId] = new Promise(objId, data); + } else { + return this.objs[objId]; + } + }, + + /** + * If called *without* callback, this returns the data of `objId` but the + * object needs to be resolved. If it isn't, this function throws. + * + * If called *with* a callback, the callback is called with the data of the + * object once the object is resolved. That means, if you call this + * function and the object is already resolved, the callback gets called + * right away. + */ + get: function(objId, callback) { + // If there is a callback, then the get can be async and the object is + // not required to be resolved right now + if (callback) { + this.ensureObj(objId).then(callback); + } + // If there isn't a callback, the user expects to get the resolved data + // directly. + else { + var obj = this.objs[objId]; + + // If there isn't an object yet or the object isn't resolved, then the + // data isn't ready yet! + if (!obj || !obj.isResolved) { + throw "Requesting object that isn't resolved yet " + objId; + } + // Direct access. + else { + return obj.data; + } + } + }, + + /** + * Resolves the object `objId` with optional `data`. + */ + resolve: function(objId, data) { + var objs = this.objs; + + // In case there is a promise already on this object, just resolve it. + if (objs[objId]) { + objs[objId].resolve(data); + } else { + this.ensureObj(objId, data); + } + }, + + onData: function(objId, callback) { + this.ensureObj(objId).onData(callback); + }, + + isResolved: function(objId) { + var objs = this.objs; + if (!objs[objId]) { + return false; + } else { + return objs[objId].isResolved; + } + }, + + hasData: function(objId) { + var objs = this.objs; + if (!objs[objId]) { + return false; + } else { + return objs[objId].hasData; + } + }, + + /** + * Sets the data of an object but *doesn't* resolve it. + */ + setData: function(objId, data) { + // Watchout! If you call `this.ensureObj(objId, data)` you'll gone create + // a *resolved* promise which shouldn't be the case! + this.ensureObj(objId).data = data; + } + } + return PDFObjects; +})(); + + +/** + * "Promise" object. + */ +var Promise = (function() { + var EMPTY_PROMISE = {}; + + /** + * If `data` is passed in this constructor, the promise is created resolved. + * If there isn't data, it isn't resolved at the beginning. + */ + function Promise(name, data) { + this.name = name; + // If you build a promise and pass in some data it's already resolved. + if (data != null) { + this.isResolved = true; + this._data = data; + this.hasData = true; + } else { + this.isResolved = false; + this._data = EMPTY_PROMISE; + } + this.callbacks = []; + }; + + Promise.prototype = { + hasData: false, + + set data(data) { + if (data === undefined) { + return; + } + if (this._data !== EMPTY_PROMISE) { + throw "Promise " + this.name + ": Cannot set the data of a promise twice"; + } + this._data = data; + this.hasData = true; + + if (this.onDataCallback) { + this.onDataCallback(data); + } + }, + + get data() { + if (this._data === EMPTY_PROMISE) { + throw "Promise " + this.name + ": Cannot get data that isn't set"; + } + return this._data; + }, + + onData: function(callback) { + if (this._data !== EMPTY_PROMISE) { + callback(this._data); + } else { + this.onDataCallback = callback; + } + }, + + resolve: function(data) { + if (this.isResolved) { + throw "A Promise can be resolved only once " + this.name; + } + + this.isResolved = true; + this.data = data; + var callbacks = this.callbacks; + + for (var i = 0; i < callbacks.length; i++) { + callbacks[i].call(null, data); + } + }, + + then: function(callback) { + if (!callback) { + throw "Requiring callback" + this.name; + } + + // If the promise is already resolved, call the callback directly. + if (this.isResolved) { + var data = this.data; + callback.call(null, data); + } else { + this.callbacks.push(callback); + } + } + } + return Promise; +})(); + +var WorkerPDFDoc = (function() { + function constructor(data) { + + this.data = data; + this.stream = new Stream(data); + this.pdf = new PDFDoc(this.stream); + + this.catalog = this.pdf.catalog; + this.objs = new PDFObjects(); + + this.pageCache = []; + + if (useWorker) { + var worker = this.worker = new Worker("../worker/pdf_worker_loader.js"); + } else { + // If we don't use a worker, just post/sendMessage to the main thread. + var worker = { + postMessage: function(obj) { + worker.onmessage({data: obj}); + } + } + } + + this.fontsLoading = {}; + + var processorHandler = this.processorHandler = new MessageHandler("main", worker); + processorHandler.on("page", function(data) { + var pageNum = data.pageNum; + var page = this.pageCache[pageNum]; + + // DepFonts are all fonts are required to render the page. `fontsToLoad` + // are all the fonts that are required to render the page AND that + // aren't loaded on the page yet. + var depFonts = data.depFonts; + var objs = this.objs; + var fontsToLoad = []; + var fontsLoading = this.fontsLoading; + // The `i` for the checkFontData is stored here to keep the state in + // the closure. + var i = 0; + + + function checkFontData() { + // Check if all fontObjs have been processed. If not, shedule a + // callback that is called once the data arrives and that checks + // the next fonts. + for (i; i < depFonts.length; i++) { + var fontName = depFonts[i]; + if (!objs.hasData(fontName)) { + console.log('need to wait for fontData', fontName); + objs.onData(fontName, checkFontData); + return; + } else if (!objs.isResolved(fontName)) { + fontsToLoad.push(fontName); + } + } + + // There can be edge cases where two pages wait for one font and then + // call startRenderingFromIRQueue twice with the same font. That makes + // the font getting loaded twice and throw an error later as the font + // promise gets resolved twice. + // This prevents thats fonts are loaded really only once. + for (var j = 0; j < fontsToLoad.length; j++) { + var fontName = fontsToLoad[j]; + if (fontsLoading[fontName]) { + fontsToLoad.splice(j, 1); + j--; + } else { + fontsLoading[fontName] = true; + } + } + + // At this point, all font data ia loaded. Start the actuall rendering. + page.startRenderingFromIRQueue(data.IRQueue, fontsToLoad); + } + + checkFontData(); + }, this); + + processorHandler.on("obj", function(data) { + var objId = data[0]; + var objType = data[1]; + + switch (objType) { + case "JpegStream": + var IR = data[2]; + new JpegStreamIR(objId, IR, this.objs); + console.log('got image'); + break; + case "Font": + var name = data[2]; + var file = data[3]; + var properties = data[4]; + + processorHandler.send("font", [objId, name, file, properties]); + break; + default: + throw "Got unkown object type " + objType; + } + }, this); + + processorHandler.on('font_ready', function(data) { + var objId = data[0]; + var fontObj = new FontShape(data[1]); + + console.log('got fontData', objId); + + // If there is no string, then there is nothing to attach to the DOM. + if (!fontObj.str) { + this.objs.resolve(objId, fontObj); + } else { + this.objs.setData(objId, fontObj); + } + }.bind(this)); + + if (!useWorker) { + // If the main thread is our worker, setup the handling for the messages + // the main thread sends to it self. + WorkerProcessorHandler.setup(processorHandler); + } + + processorHandler.send("doc", data); + } + + constructor.prototype = { + get numPages() { + return this.pdf.numPages; + }, + + startRendering: function(page) { + this.processorHandler.send("page_request", page.page.pageNumber + 1); + }, + + getPage: function(n) { + if (this.pageCache[n]) { + return this.pageCache[n]; + } + + var page = this.pdf.getPage(n); + // Add a reference to the objects such that Page can forward the reference + // to the CanvasGraphics and so on. + page.objs = this.objs; + return this.pageCache[n] = new WorkerPage(this, page, this.objs); + }, + + destroy: function() { + console.log("destroy worker"); + if (this.worker) { + this.worker.terminate(); + } + if (this.fontWorker) { + this.fontWorker.terminate(); + } + + for (var n in this.pageCache) { + delete this.pageCache[n]; + } + delete this.data; + delete this.stream; + delete this.pdf; + delete this.catalog; + } + }; + + return constructor; +})();