diff --git a/LICENSE b/LICENSE index f8a848205..db52dec8e 100644 --- a/LICENSE +++ b/LICENSE @@ -9,6 +9,7 @@ Yury Delendik Kalervo Kujala Adil Allawi <@ironymark> + Jakob Miland Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), diff --git a/Makefile b/Makefile index f36e74130..e99daa832 100644 --- a/Makefile +++ b/Makefile @@ -68,7 +68,8 @@ bundle: | $(BUILD_DIR) @cd src; \ cat $(PDF_JS_FILES) > all_files.tmp; \ sed '/PDFJSSCRIPT_INCLUDE_ALL/ r all_files.tmp' pdf.js > ../$(BUILD_TARGET); \ - sed -i '' "s/PDFJSSCRIPT_BUNDLE_VER/`git log --format="%H" -n 1`/" ../$(BUILD_TARGET); \ + sed -i.bak "s/PDFJSSCRIPT_BUNDLE_VER/`git log --format="%h" -n 1`/" ../$(BUILD_TARGET); \ + rm -f ../$(BUILD_TARGET).bak rm -f *.tmp; \ cd .. @@ -138,8 +139,8 @@ browser-test: # To install gjslint, see: # # -SRC_DIRS := . src utils web test test/unit examples/helloworld \ - extensions/firefox extensions/firefox/components extensions/chrome +SRC_DIRS := . src utils web test examples/helloworld extensions/firefox \ + extensions/firefox/components extensions/chrome test/unit GJSLINT_FILES = $(foreach DIR,$(SRC_DIRS),$(wildcard $(DIR)/*.js)) lint: gjslint --nojsdoc $(GJSLINT_FILES) diff --git a/README.md b/README.md index 97db68d36..7e5d2eeb3 100644 --- a/README.md +++ b/README.md @@ -205,3 +205,4 @@ a "PDF Reference" from Adobe: Recommended chapters to read: "2. Overview", "3.4 File Structure", "4.1 Graphics Objects" that lists the PDF commands. + diff --git a/examples/acroforms/forms.js b/examples/acroforms/forms.js new file mode 100644 index 000000000..6ec92766d --- /dev/null +++ b/examples/acroforms/forms.js @@ -0,0 +1,141 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +// +// Basic AcroForms input controls rendering +// + +'use strict'; + +var formFields = {}; + +function setupForm(div, content, scale) { + function bindInputItem(input, item) { + if (input.name in formFields) { + var value = formFields[input.name]; + if (input.type == 'checkbox') + input.checked = value; + else if (!input.type || input.type == 'text') + input.value = value; + } + input.onchange = function pageViewSetupInputOnBlur() { + if (input.type == 'checkbox') + formFields[input.name] = input.checked; + else if (!input.type || input.type == 'text') + formFields[input.name] = input.value; + }; + } + function createElementWithStyle(tagName, item) { + var element = document.createElement(tagName); + element.style.left = (item.x * scale) + 'px'; + element.style.top = (item.y * scale) + 'px'; + element.style.width = Math.ceil(item.width * scale) + 'px'; + element.style.height = Math.ceil(item.height * scale) + 'px'; + return element; + } + function assignFontStyle(element, item) { + var fontStyles = ''; + if ('fontSize' in item) + fontStyles += 'font-size: ' + Math.round(item.fontSize * scale) + 'px;'; + switch (item.textAlignment) { + case 0: + fontStyles += 'text-align: left;'; + break; + case 1: + fontStyles += 'text-align: center;'; + break; + case 2: + fontStyles += 'text-align: right;'; + break; + } + element.setAttribute('style', element.getAttribute('style') + fontStyles); + } + + var items = content.getAnnotations(); + for (var i = 0; i < items.length; i++) { + var item = items[i]; + switch (item.type) { + case 'Widget': + if (item.fieldType != 'Tx' && item.fieldType != 'Btn' && + item.fieldType != 'Ch') + break; + var inputDiv = createElementWithStyle('div', item); + inputDiv.className = 'inputHint'; + div.appendChild(inputDiv); + var input; + if (item.fieldType == 'Tx') { + input = createElementWithStyle('input', item); + } + if (item.fieldType == 'Btn') { + input = createElementWithStyle('input', item); + if (item.flags & 32768) { + input.type = 'radio'; + // radio button is not supported + } else if (item.flags & 65536) { + input.type = 'button'; + // pushbutton is not supported + } else { + input.type = 'checkbox'; + } + } + if (item.fieldType == 'Ch') { + input = createElementWithStyle('select', item); + // select box is not supported + } + input.className = 'inputControl'; + input.name = item.fullName; + input.title = item.alternativeText; + assignFontStyle(input, item); + bindInputItem(input, item); + div.appendChild(input); + break; + } + } +} + +function renderPage(div, pdf, pageNumber, callback) { + var page = pdf.getPage(pageNumber); + var scale = 1.5; + + var pageDisplayWidth = page.width * scale; + var pageDisplayHeight = page.height * scale; + + var pageDivHolder = document.createElement('div'); + pageDivHolder.className = 'pdfpage'; + pageDivHolder.style.width = pageDisplayWidth + 'px'; + pageDivHolder.style.height = pageDisplayHeight + 'px'; + div.appendChild(pageDivHolder); + + // Prepare canvas using PDF page dimensions + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + canvas.width = pageDisplayWidth; + canvas.height = pageDisplayHeight; + pageDivHolder.appendChild(canvas); + + + // Render PDF page into canvas context + page.startRendering(context, callback); + + // Prepare and populate form elements layer + var formDiv = document.createElement('div'); + pageDivHolder.appendChild(formDiv); + + setupForm(formDiv, page, scale); +} + +PDFJS.getPdf(pdfWithFormsPath, function getPdfForm(data) { + // Instantiate PDFDoc with PDF data + var pdf = new PDFJS.PDFDoc(data); + + // Rendering all pages starting from first + var viewer = document.getElementById('viewer'); + var pageNumber = 1; + renderPage(viewer, pdf, pageNumber++, function pageRenderingComplete() { + if (pageNumber > pdf.numPages) + return; // All pages rendered + // Continue rendering of the next page + renderPage(viewer, pdf, pageNumber++, pageRenderingComplete); + }); +}); + diff --git a/examples/acroforms/index.html b/examples/acroforms/index.html new file mode 100644 index 000000000..5fad4648a --- /dev/null +++ b/examples/acroforms/index.html @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/extensions/firefox/install.rdf b/extensions/firefox/install.rdf index 952d55fbf..8b793c525 100644 --- a/extensions/firefox/install.rdf +++ b/extensions/firefox/install.rdf @@ -6,13 +6,13 @@ uriloader@pdf.js pdf.js - 0.1 + 0.1.0 chrome://pdf.js/skin/logo.png {ec8030f7-c20a-464f-9b0e-13a3a9e97384} 6.0 - 11.0.* + 11.0a1 true @@ -20,5 +20,6 @@ Vivien Nicolas pdf.js uri loader https://github.com/mozilla/pdf.js/ + 2 diff --git a/src/canvas.js b/src/canvas.js index 9b3ed0ba9..3fd55b45d 100644 --- a/src/canvas.js +++ b/src/canvas.js @@ -6,8 +6,19 @@ // contexts store most of the state we need natively. // However, PDF needs a bit more state, which we store here. -var CanvasExtraState = (function canvasExtraState() { - function constructor(old) { +var TextRenderingMode = { + FILL: 0, + STROKE: 1, + FILL_STROKE: 2, + INVISIBLE: 3, + FILL_ADD_TO_PATH: 4, + STROKE_ADD_TO_PATH: 5, + FILL_STROKE_ADD_TO_PATH: 6, + ADD_TO_PATH: 7 +}; + +var CanvasExtraState = (function CanvasExtraStateClosure() { + function CanvasExtraState(old) { // Are soft masks and alpha values shapes or opacities? this.alphaIsShape = false; this.fontSize = 0; @@ -23,6 +34,7 @@ var CanvasExtraState = (function canvasExtraState() { this.charSpacing = 0; this.wordSpacing = 0; this.textHScale = 1; + this.textRenderingMode = TextRenderingMode.FILL; // Color spaces this.fillColorSpace = new DeviceGrayCS(); this.fillColorSpaceObj = null; @@ -40,7 +52,7 @@ var CanvasExtraState = (function canvasExtraState() { this.old = old; } - constructor.prototype = { + CanvasExtraState.prototype = { clone: function canvasextra_clone() { return Object.create(this); }, @@ -49,7 +61,7 @@ var CanvasExtraState = (function canvasExtraState() { this.y = y; } }; - return constructor; + return CanvasExtraState; })(); function ScratchCanvas(width, height) { @@ -59,16 +71,122 @@ function ScratchCanvas(width, height) { return canvas; } -var CanvasGraphics = (function canvasGraphics() { +function addContextCurrentTransform(ctx) { + // If the context doesn't expose a `mozCurrentTransform`, add a JS based on. + if (!ctx.mozCurrentTransform) { + // Store the original context + ctx._originalSave = ctx.save; + ctx._originalRestore = ctx.restore; + ctx._originalRotate = ctx.rotate; + ctx._originalScale = ctx.scale; + ctx._originalTranslate = ctx.translate; + ctx._originalTransform = ctx.transform; + + ctx._transformMatrix = [1, 0, 0, 1, 0, 0]; + ctx._transformStack = []; + + Object.defineProperty(ctx, 'mozCurrentTransform', { + get: function getCurrentTransform() { + return this._transformMatrix; + } + }); + + Object.defineProperty(ctx, 'mozCurrentTransformInverse', { + get: function getCurrentTransformInverse() { + // Calculation done using WolframAlpha: + // http://www.wolframalpha.com/input/? + // i=Inverse+{{a%2C+c%2C+e}%2C+{b%2C+d%2C+f}%2C+{0%2C+0%2C+1}} + + var m = this._transformMatrix; + var a = m[0], b = m[1], c = m[2], d = m[3], e = m[4], f = m[5]; + + var ad_bc = a * d - b * c; + var bc_ad = b * c - a * d; + + return [ + d / ad_bc, + b / bc_ad, + c / bc_ad, + a / ad_bc, + (d * e - c * f) / bc_ad, + (b * e - a * f) / ad_bc + ]; + } + }); + + ctx.save = function ctxSave() { + var old = this._transformMatrix; + this._transformStack.push(old); + this._transformMatrix = old.slice(0, 6); + + this._originalSave(); + }; + + ctx.restore = function ctxRestore() { + var prev = this._transformStack.pop(); + if (prev) { + this._transformMatrix = prev; + this._originalRestore(); + } + }; + + ctx.translate = function ctxTranslate(x, y) { + var m = this._transformMatrix; + m[4] = m[0] * x + m[2] * y + m[4]; + m[5] = m[1] * x + m[3] * y + m[5]; + + this._originalTranslate(x, y); + }; + + ctx.scale = function ctxScale(x, y) { + var m = this._transformMatrix; + m[0] = m[0] * x; + m[1] = m[1] * x; + m[2] = m[2] * y; + m[3] = m[3] * y; + + this._originalScale(x, y); + }; + + ctx.transform = function ctxTransform(a, b, c, d, e, f) { + var m = this._transformMatrix; + this._transformMatrix = [ + m[0] * a + m[2] * b, + m[1] * a + m[3] * b, + m[0] * c + m[2] * d, + m[1] * c + m[3] * d, + m[0] * e + m[2] * f + m[4], + m[1] * e + m[3] * f + m[5] + ]; + + ctx._originalTransform(a, b, c, d, e, f); + }; + + ctx.rotate = function ctxRotate(angle) { + var cosValue = Math.cos(angle); + var sinValue = Math.sin(angle); + + var m = this._transformMatrix; + this._transformMatrix = [ + m[0] * cosValue + m[2] * sinValue, + m[1] * cosValue + m[3] * sinValue, + m[0] * (-sinValue) + m[2] * cosValue, + m[1] * (-sinValue) + m[3] * cosValue, + m[4], + m[5] + ]; + + this._originalRotate(angle); + }; + } +} + +var CanvasGraphics = (function CanvasGraphicsClosure() { // Defines the time the executeIRQueue is going to 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) { + function CanvasGraphics(canvasCtx, objs, textLayer) { this.ctx = canvasCtx; this.current = new CanvasExtraState(); this.stateStack = []; @@ -77,6 +195,10 @@ var CanvasGraphics = (function canvasGraphics() { this.xobjs = null; this.ScratchCanvas = ScratchCanvas; this.objs = objs; + this.textLayer = textLayer; + if (canvasCtx) { + addContextCurrentTransform(canvasCtx); + } } var LINE_CAP_STYLES = ['butt', 'round', 'square']; @@ -84,7 +206,36 @@ var CanvasGraphics = (function canvasGraphics() { var NORMAL_CLIP = {}; var EO_CLIP = {}; - constructor.prototype = { + CanvasGraphics.prototype = { + slowCommands: { + 'stroke': true, + 'closeStroke': true, + 'fill': true, + 'eoFill': true, + 'fillStroke': true, + 'eoFillStroke': true, + 'closeFillStroke': true, + 'closeEOFillStroke': true, + 'showText': true, + 'showSpacedText': true, + 'setStrokeColorSpace': true, + 'setFillColorSpace': true, + 'setStrokeColor': true, + 'setStrokeColorN': true, + 'setFillColor': true, + 'setFillColorN_IR': true, + 'setStrokeGray': true, + 'setFillGray': true, + 'setStrokeRGBColor': true, + 'setFillRGBColor': true, + 'setStrokeCMYKColor': true, + 'setFillCMYKColor': true, + 'paintJpegXObject': true, + 'paintImageXObject': true, + 'paintImageMaskXObject': true, + 'shadingFill': true + }, + beginDrawing: function canvasGraphicsBeginDrawing(mediaBox) { var cw = this.ctx.canvas.width, ch = this.ctx.canvas.height; this.ctx.save(); @@ -102,7 +253,13 @@ var CanvasGraphics = (function canvasGraphics() { this.ctx.transform(0, -1, -1, 0, cw, ch); break; } + // Scale so that canvas units are the same as PDF user space units this.ctx.scale(cw / mediaBox.width, ch / mediaBox.height); + // Move the media left-top corner to the (0,0) canvas position + this.ctx.translate(-mediaBox.x, -mediaBox.y); + + if (this.textLayer) + this.textLayer.beginLayout(); }, executeIRQueue: function canvasGraphicsExecuteIRQueue(codeIR, @@ -112,32 +269,39 @@ var CanvasGraphics = (function canvasGraphics() { var i = executionStartIdx || 0; var argsArrayLen = argsArray.length; + // Sometimes the IRQueue to execute is empty. + if (argsArrayLen == i) { + return i; + } + var executionEndIdx; - var startTime = Date.now(); + var endTime = Date.now() + kExecutionTime; var objs = this.objs; + var fnName; + var slowCommands = this.slowCommands; - do { - executionEndIdx = Math.min(argsArrayLen, i + kExecutionTimeCheck); + while (true) { + fnName = fnArray[i]; - 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, nn = deps.length; n < nn; n++) { - var depObjId = deps[n]; + if (fnName !== 'dependency') { + this[fnName].apply(this, argsArray[i]); + } else { + var deps = argsArray[i]; + for (var n = 0, nn = deps.length; n < nn; 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 promise isn't resolved yet, add the continueCallback + // to the promise and bail out. + if (!objs.isResolved(depObjId)) { + objs.get(depObjId, continueCallback); + return i; } } } + i++; + // If the entire IRQueue was executed, stop as were done. if (i == argsArrayLen) { return i; @@ -146,18 +310,21 @@ var CanvasGraphics = (function canvasGraphics() { // 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. - if (continueCallback && (Date.now() - startTime) > kExecutionTime) { + if (continueCallback && slowCommands[fnName] && Date.now() > endTime) { setTimeout(continueCallback, 0); return i; } // If the IRQueue isn't executed completly yet OR the execution time // was short enough, do another execution round. - } while (true); + } }, endDrawing: function canvasGraphicsEndDrawing() { this.ctx.restore(); + + if (this.textLayer) + this.textLayer.endLayout(); }, // Graphics state @@ -176,6 +343,8 @@ var CanvasGraphics = (function canvasGraphics() { setDash: function canvasGraphicsSetDash(dashArray, dashPhase) { this.ctx.mozDash = dashArray; this.ctx.mozDashOffset = dashPhase; + this.ctx.webkitLineDash = dashArray; + this.ctx.webkitLineDashOffset = dashPhase; }, setRenderingIntent: function canvasGraphicsSetRenderingIntent(intent) { TODO('set rendering intent: ' + intent); @@ -394,7 +563,9 @@ var CanvasGraphics = (function canvasGraphics() { this.ctx.font = rule; }, setTextRenderingMode: function canvasGraphicsSetTextRenderingMode(mode) { - TODO('text rendering mode: ' + mode); + if (mode >= TextRenderingMode.FILL_ADD_TO_PATH) + TODO('unsupported text rendering mode: ' + mode); + this.current.textRenderingMode = mode; }, setTextRise: function canvasGraphicsSetTextRise(rise) { TODO('text rise: ' + rise); @@ -416,23 +587,67 @@ var CanvasGraphics = (function canvasGraphics() { nextLine: function canvasGraphicsNextLine() { this.moveText(0, this.current.leading); }, - showText: function canvasGraphicsShowText(text) { + applyTextTransforms: function canvasApplyTransforms() { + var ctx = this.ctx; + var current = this.current; + var textHScale = current.textHScale; + var fontMatrix = current.font.fontMatrix || IDENTITY_MATRIX; + + ctx.transform.apply(ctx, current.textMatrix); + ctx.scale(1, -1); + ctx.translate(current.x, -1 * current.y); + ctx.transform.apply(ctx, fontMatrix); + ctx.scale(textHScale, 1); + }, + getTextGeometry: function canvasGetTextGeometry() { + var geometry = {}; + var ctx = this.ctx; + var font = this.current.font; + var ctxMatrix = ctx.mozCurrentTransform; + if (ctxMatrix) { + var bl = Util.applyTransform([0, 0], ctxMatrix); + var tr = Util.applyTransform([1, 1], ctxMatrix); + geometry.x = bl[0]; + geometry.y = bl[1]; + geometry.hScale = tr[0] - bl[0]; + geometry.vScale = tr[1] - bl[1]; + } + geometry.spaceWidth = font.spaceWidth; + return geometry; + }, + + showText: function canvasGraphicsShowText(str, skipTextSelection) { var ctx = this.ctx; var current = this.current; var font = current.font; - var glyphs = font.charsToGlyphs(text); + var glyphs = font.charsToGlyphs(str); var fontSize = current.fontSize; var charSpacing = current.charSpacing; var wordSpacing = current.wordSpacing; var textHScale = current.textHScale; + var fontMatrix = font.fontMatrix || IDENTITY_MATRIX; + var textHScale2 = textHScale * fontMatrix[0]; var glyphsLength = glyphs.length; + var textLayer = this.textLayer; + var text = {str: '', length: 0, canvasWidth: 0, geom: {}}; + var textSelection = textLayer && !skipTextSelection ? true : false; + var textRenderingMode = current.textRenderingMode; + + // Type3 fonts - each glyph is a "mini-PDF" 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); + ctx.scale(textHScale, 1); + ctx.lineWidth /= current.textMatrix[0]; + + if (textSelection) { + this.save(); + ctx.scale(1, -1); + text.geom = this.getTextGeometry(); + this.restore(); + } for (var i = 0; i < glyphsLength; ++i) { var glyph = glyphs[i]; @@ -452,18 +667,20 @@ var CanvasGraphics = (function canvasGraphics() { var width = transformed[0] * fontSize + charSpacing; ctx.translate(width, 0); - current.x += width; + current.x += width * textHScale; + text.str += glyph.unicode; + text.length++; + text.canvasWidth += 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); + this.applyTextTransforms(); + ctx.lineWidth /= current.textMatrix[0] * fontMatrix[0]; - ctx.scale(1 / textHScale, 1); + if (textSelection) + text.geom = this.getTextGeometry(); var width = 0; for (var i = 0; i < glyphsLength; ++i) { @@ -474,36 +691,106 @@ var CanvasGraphics = (function canvasGraphics() { continue; } - var unicode = glyph.unicode; - var char = (unicode >= 0x10000) ? - String.fromCharCode(0xD800 | ((unicode - 0x10000) >> 10), - 0xDC00 | (unicode & 0x3FF)) : String.fromCharCode(unicode); + var char = glyph.fontChar; + var charWidth = glyph.width * fontSize * 0.001 + charSpacing; - ctx.fillText(char, width, 0); - width += glyph.width * fontSize * 0.001 + charSpacing; + switch (textRenderingMode) { + default: // other unsupported rendering modes + case TextRenderingMode.FILL: + case TextRenderingMode.FILL_ADD_TO_PATH: + ctx.fillText(char, width, 0); + break; + case TextRenderingMode.STROKE: + case TextRenderingMode.STROKE_ADD_TO_PATH: + ctx.strokeText(char, width, 0); + break; + case TextRenderingMode.FILL_STROKE: + case TextRenderingMode.FILL_STROKE_ADD_TO_PATH: + ctx.fillText(char, width, 0); + ctx.strokeText(char, width, 0); + break; + case TextRenderingMode.INVISIBLE: + break; + } + + width += charWidth; + + text.str += glyph.unicode === ' ' ? '\u00A0' : glyph.unicode; + text.length++; + text.canvasWidth += charWidth; } - current.x += width; - + current.x += width * textHScale2; ctx.restore(); } - }, + if (textSelection) + this.textLayer.appendText(text, font.loadedName, fontSize); + + return text; + }, showSpacedText: function canvasGraphicsShowSpacedText(arr) { var ctx = this.ctx; var current = this.current; + var font = current.font; var fontSize = current.fontSize; var textHScale = current.textHScale; + if (!font.coded) + textHScale *= (font.fontMatrix || IDENTITY_MATRIX)[0]; var arrLength = arr.length; + var textLayer = this.textLayer; + var text = {str: '', length: 0, canvasWidth: 0, geom: {}}; + var textSelection = textLayer ? true : false; + + if (textSelection) { + ctx.save(); + // Type3 fonts - each glyph is a "mini-PDF" (see also showText) + if (font.coded) { + ctx.transform.apply(ctx, current.textMatrix); + ctx.scale(1, -1); + ctx.translate(current.x, -1 * current.y); + ctx.scale(textHScale, 1); + } else + this.applyTextTransforms(); + text.geom = this.getTextGeometry(); + ctx.restore(); + } + for (var i = 0; i < arrLength; ++i) { var e = arr[i]; if (isNum(e)) { - current.x -= e * 0.001 * fontSize * textHScale; + var spacingLength = -e * 0.001 * fontSize * textHScale; + current.x += spacingLength; + + if (textSelection) { + // Emulate precise spacing via HTML spaces + text.canvasWidth += spacingLength; + if (e < 0 && text.geom.spaceWidth > 0) { // avoid div by zero + var numFakeSpaces = Math.round(-e / text.geom.spaceWidth); + if (numFakeSpaces > 0) { + text.str += '\u00A0'; + text.length++; + } + } + } } else if (isString(e)) { - this.showText(e); + var shownText = this.showText(e, true); + + if (textSelection) { + if (shownText.str === ' ') { + text.str += '\u00A0'; + } else { + text.str += shownText.str; + } + text.canvasWidth += shownText.canvasWidth; + text.length += e.length; + } } else { malformed('TJ array element ' + e + ' is not string or num'); } } + + if (textSelection) + this.textLayer.appendText(text, font.loadedName, fontSize); }, nextLineShowText: function canvasGraphicsNextLineShowText(text) { this.nextLine(); @@ -658,9 +945,9 @@ var CanvasGraphics = (function canvasGraphics() { var height = canvas.height; var bl = Util.applyTransform([0, 0], inv); - var br = Util.applyTransform([0, width], inv); - var ul = Util.applyTransform([height, 0], inv); - var ur = Util.applyTransform([height, width], inv); + var br = Util.applyTransform([0, height], inv); + var ul = Util.applyTransform([width, 0], inv); + var ur = Util.applyTransform([width, height], inv); var x0 = Math.min(bl[0], br[0], ul[0], ur[0]); var y0 = Math.min(bl[1], br[1], ul[1], ur[1]); @@ -710,8 +997,8 @@ var CanvasGraphics = (function canvasGraphics() { }, paintJpegXObject: function canvasGraphicsPaintJpegXObject(objId, w, h) { - var image = this.objs.get(objId); - if (!image) { + var domImage = this.objs.get(objId); + if (!domImage) { error('Dependent image isn\'t ready yet'); } @@ -721,7 +1008,6 @@ var CanvasGraphics = (function canvasGraphics() { // scale the image to the unit square ctx.scale(1 / w, -1 / h); - var domImage = image.getImage(); ctx.drawImage(domImage, 0, 0, domImage.width, domImage.height, 0, -h, w, h); @@ -777,7 +1063,11 @@ var CanvasGraphics = (function canvasGraphics() { this.restore(); }, - paintImageXObject: function canvasGraphicsPaintImageXObject(imgData) { + paintImageXObject: function canvasGraphicsPaintImageXObject(objId) { + var imgData = this.objs.get(objId); + if (!imgData) + error('Dependent image isn\'t ready yet'); + this.save(); var ctx = this.ctx; var w = imgData.width; @@ -787,26 +1077,16 @@ var CanvasGraphics = (function canvasGraphics() { var tmpCanvas = new this.ScratchCanvas(w, h); var tmpCtx = tmpCanvas.getContext('2d'); - var tmpImgData; + this.putBinaryImageData(tmpCtx, imgData, w, h); - // Some browsers can set an UInt8Array directly as imageData, some - // can't. As long as we don't have proper feature detection, just - // copy over each pixel and set the imageData that way. - tmpImgData = tmpCtx.getImageData(0, 0, w, h); - - // Copy over the imageData. - var tmpImgDataPixels = tmpImgData.data; - var len = tmpImgDataPixels.length; - - while (len--) { - tmpImgDataPixels[len] = imgData.data[len]; - } - - tmpCtx.putImageData(tmpImgData, 0, 0); ctx.drawImage(tmpCanvas, 0, -h); this.restore(); }, + putBinaryImageData: function canvasPutBinaryImageData() { + // + }, + // Marked content markPoint: function canvasGraphicsMarkPoint(tag) { @@ -864,6 +1144,41 @@ var CanvasGraphics = (function canvasGraphics() { } }; - return constructor; + return CanvasGraphics; })(); +if (!isWorker) { + // Feature detection if the browser can use an Uint8Array directly as imgData. + var canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 1; + var ctx = canvas.getContext('2d'); + + try { + ctx.putImageData({ + width: 1, + height: 1, + data: new Uint8Array(4) + }, 0, 0); + + CanvasGraphics.prototype.putBinaryImageData = + function CanvasGraphicsPutBinaryImageDataNative(ctx, imgData) { + ctx.putImageData(imgData, 0, 0); + }; + } catch (e) { + CanvasGraphics.prototype.putBinaryImageData = + function CanvasGraphicsPutBinaryImageDataShim(ctx, imgData, w, h) { + var tmpImgData = ctx.getImageData(0, 0, w, h); + + // Copy over the imageData pixel by pixel. + var tmpImgDataPixels = tmpImgData.data; + var len = tmpImgDataPixels.length; + + while (len--) { + tmpImgDataPixels[len] = imgData.data[len]; + } + + ctx.putImageData(tmpImgData, 0, 0); + }; + } +} diff --git a/src/colorspace.js b/src/colorspace.js index b369d0f88..827fd2e19 100644 --- a/src/colorspace.js +++ b/src/colorspace.js @@ -3,13 +3,13 @@ 'use strict'; -var ColorSpace = (function colorSpaceColorSpace() { +var ColorSpace = (function ColorSpaceClosure() { // Constructor should define this.numComps, this.defaultColor, this.name - function constructor() { + function ColorSpace() { error('should not call ColorSpace constructor'); } - constructor.prototype = { + ColorSpace.prototype = { // Input: array of size numComps representing color component values // Output: array of rgb values, each value ranging from [0.1] getRgb: function colorSpaceGetRgb(color) { @@ -22,15 +22,15 @@ var ColorSpace = (function colorSpaceColorSpace() { } }; - constructor.parse = function colorSpaceParse(cs, xref, res) { - var IR = constructor.parseToIR(cs, xref, res); + ColorSpace.parse = function colorSpaceParse(cs, xref, res) { + var IR = ColorSpace.parseToIR(cs, xref, res); if (IR instanceof AlternateCS) return IR; - return constructor.fromIR(IR); + return ColorSpace.fromIR(IR); }; - constructor.fromIR = function colorSpaceFromIR(IR) { + ColorSpace.fromIR = function colorSpaceFromIR(IR) { var name = isArray(IR) ? IR[0] : IR; switch (name) { @@ -63,7 +63,7 @@ var ColorSpace = (function colorSpaceColorSpace() { return null; }; - constructor.parseToIR = function colorSpaceParseToIR(cs, xref, res) { + ColorSpace.parseToIR = function colorSpaceParseToIR(cs, xref, res) { if (isName(cs)) { var colorSpaces = xref.fetchIfRef(res.get('ColorSpace')); if (isDict(colorSpaces)) { @@ -154,8 +154,31 @@ var ColorSpace = (function colorSpaceColorSpace() { } return null; }; + /** + * Checks if a decode map matches the default decode map for a color space. + * This handles the general decode maps where there are two values per + * component. e.g. [0, 1, 0, 1, 0, 1] for a RGB color. + * This does not handle Lab, Indexed, or Pattern decode maps since they are + * slightly different. + * @param {Array} decode Decode map (usually from an image). + * @param {Number} n Number of components the color space has. + */ + ColorSpace.isDefaultDecode = function colorSpaceIsDefaultDecode(decode, n) { + if (!decode) + return true; - return constructor; + if (n * 2 !== decode.length) { + warning('The decode map is not the correct length'); + return true; + } + for (var i = 0, ii = decode.length; i < ii; i += 2) { + if (decode[i] != 0 || decode[i + 1] != 1) + return false; + } + return true; + }; + + return ColorSpace; })(); /** @@ -164,8 +187,8 @@ var ColorSpace = (function colorSpaceColorSpace() { * Both color spaces use a tinting function to convert colors to a base color * space. */ -var AlternateCS = (function alternateCS() { - function constructor(numComps, base, tintFn) { +var AlternateCS = (function AlternateCSClosure() { + function AlternateCS(numComps, base, tintFn) { this.name = 'Alternate'; this.numComps = numComps; this.defaultColor = []; @@ -175,7 +198,7 @@ var AlternateCS = (function alternateCS() { this.tintFn = tintFn; } - constructor.prototype = { + AlternateCS.prototype = { getRgb: function altcs_getRgb(color) { var tinted = this.tintFn(color); return this.base.getRgb(tinted); @@ -200,24 +223,27 @@ var AlternateCS = (function alternateCS() { baseBuf[pos++] = 255 * tinted[j]; } return base.getRgbBuffer(baseBuf, 8); + }, + isDefaultDecode: function altcs_isDefaultDecode(decodeMap) { + return ColorSpace.isDefaultDecode(decodeMap, this.numComps); } }; - return constructor; + return AlternateCS; })(); -var PatternCS = (function patternCS() { - function constructor(baseCS) { +var PatternCS = (function PatternCSClosure() { + function PatternCS(baseCS) { this.name = 'Pattern'; this.base = baseCS; } - constructor.prototype = {}; + PatternCS.prototype = {}; - return constructor; + return PatternCS; })(); -var IndexedCS = (function indexedCS() { - function constructor(base, highVal, lookup) { +var IndexedCS = (function IndexedCSClosure() { + function IndexedCS(base, highVal, lookup) { this.name = 'Indexed'; this.numComps = 1; this.defaultColor = [0]; @@ -240,7 +266,7 @@ var IndexedCS = (function indexedCS() { this.lookup = lookupArray; } - constructor.prototype = { + IndexedCS.prototype = { getRgb: function indexcs_getRgb(color) { var numComps = this.base.numComps; var start = color[0] * numComps; @@ -267,19 +293,23 @@ var IndexedCS = (function indexedCS() { } return base.getRgbBuffer(baseBuf, 8); + }, + isDefaultDecode: function indexcs_isDefaultDecode(decodeMap) { + // indexed color maps shouldn't be changed + return true; } }; - return constructor; + return IndexedCS; })(); -var DeviceGrayCS = (function deviceGrayCS() { - function constructor() { +var DeviceGrayCS = (function DeviceGrayCSClosure() { + function DeviceGrayCS() { this.name = 'DeviceGray'; this.numComps = 1; this.defaultColor = [0]; } - constructor.prototype = { + DeviceGrayCS.prototype = { getRgb: function graycs_getRgb(color) { var c = color[0]; return [c, c, c]; @@ -295,18 +325,21 @@ var DeviceGrayCS = (function deviceGrayCS() { rgbBuf[j++] = c; } return rgbBuf; + }, + isDefaultDecode: function graycs_isDefaultDecode(decodeMap) { + return ColorSpace.isDefaultDecode(decodeMap, this.numComps); } }; - return constructor; + return DeviceGrayCS; })(); -var DeviceRgbCS = (function deviceRgbCS() { - function constructor() { +var DeviceRgbCS = (function DeviceRgbCSClosure() { + function DeviceRgbCS() { this.name = 'DeviceRGB'; this.numComps = 3; this.defaultColor = [0, 0, 0]; } - constructor.prototype = { + DeviceRgbCS.prototype = { getRgb: function rgbcs_getRgb(color) { return color; }, @@ -319,18 +352,21 @@ var DeviceRgbCS = (function deviceRgbCS() { for (i = 0; i < length; ++i) rgbBuf[i] = (scale * input[i]) | 0; return rgbBuf; + }, + isDefaultDecode: function rgbcs_isDefaultDecode(decodeMap) { + return ColorSpace.isDefaultDecode(decodeMap, this.numComps); } }; - return constructor; + return DeviceRgbCS; })(); -var DeviceCmykCS = (function deviceCmykCS() { - function constructor() { +var DeviceCmykCS = (function DeviceCmykCSClosure() { + function DeviceCmykCS() { this.name = 'DeviceCMYK'; this.numComps = 4; this.defaultColor = [0, 0, 0, 1]; } - constructor.prototype = { + DeviceCmykCS.prototype = { getRgb: function cmykcs_getRgb(color) { var c = color[0], m = color[1], y = color[2], k = color[3]; var c1 = 1 - c, m1 = 1 - m, y1 = 1 - y, k1 = 1 - k; @@ -403,9 +439,12 @@ var DeviceCmykCS = (function deviceCmykCS() { } return rgbBuf; + }, + isDefaultDecode: function cmykcs_isDefaultDecode(decodeMap) { + return ColorSpace.isDefaultDecode(decodeMap, this.numComps); } }; - return constructor; + return DeviceCmykCS; })(); diff --git a/src/core.js b/src/core.js index 3549eb906..765a239b7 100644 --- a/src/core.js +++ b/src/core.js @@ -5,6 +5,8 @@ var globalScope = (typeof window === 'undefined') ? this : window; +var isWorker = (typeof window == 'undefined'); + var ERRORS = 0, WARNINGS = 1, TODOS = 5; var verbosity = WARNINGS; @@ -31,7 +33,7 @@ function getPdf(arg, callback) { var xhr = new XMLHttpRequest(); xhr.open('GET', params.url); xhr.mozResponseType = xhr.responseType = 'arraybuffer'; - xhr.expected = (document.URL.indexOf('file:') === 0) ? 0 : 200; + xhr.expected = (params.url.indexOf('file:') === 0) ? 0 : 200; if ('progress' in params) xhr.onprogress = params.progress || undefined; @@ -39,19 +41,23 @@ function getPdf(arg, callback) { if ('error' in params) xhr.onerror = params.error || undefined; - xhr.onreadystatechange = function getPdfOnreadystatechange() { - if (xhr.readyState === 4 && xhr.status === xhr.expected) { - var data = (xhr.mozResponseArrayBuffer || xhr.mozResponse || - xhr.responseArrayBuffer || xhr.response); - callback(data); + xhr.onreadystatechange = function getPdfOnreadystatechange(e) { + if (xhr.readyState === 4) { + if (xhr.status === xhr.expected) { + var data = (xhr.mozResponseArrayBuffer || xhr.mozResponse || + xhr.responseArrayBuffer || xhr.response); + callback(data); + } else if (params.error) { + params.error(e); + } } }; xhr.send(null); } globalScope.PDFJS.getPdf = getPdf; -var Page = (function pagePage() { - function constructor(xref, pageNumber, pageDict, ref) { +var Page = (function PageClosure() { + function Page(xref, pageNumber, pageDict, ref) { this.pageNumber = pageNumber; this.pageDict = pageDict; this.stats = { @@ -63,9 +69,11 @@ var Page = (function pagePage() { }; this.xref = xref; this.ref = ref; + + this.displayReadyPromise = null; } - constructor.prototype = { + Page.prototype = { getPageProp: function pageGetPageProp(key) { return this.xref.fetchIfRef(this.pageDict.get(key)); }, @@ -101,9 +109,11 @@ var Page = (function pagePage() { width: this.width, height: this.height }; + var mediaBox = this.mediaBox; + var offsetX = mediaBox[0], offsetY = mediaBox[1]; if (isArray(obj) && obj.length == 4) { - var tl = this.rotatePoint(obj[0], obj[1]); - var br = this.rotatePoint(obj[2], obj[3]); + var tl = this.rotatePoint(obj[0] - offsetX, obj[1] - offsetY); + var br = this.rotatePoint(obj[2] - offsetX, obj[3] - offsetY); view.x = Math.min(tl.x, br.x); view.y = Math.min(tl.y, br.y); view.width = Math.abs(tl.x - br.x); @@ -156,18 +166,12 @@ var Page = (function pagePage() { IRQueue, fonts) { var self = this; this.IRQueue = IRQueue; - var gfx = new CanvasGraphics(this.ctx, this.objs); var displayContinuation = function pageDisplayContinuation() { // Always defer call to display() to work around bug in // Firefox error reporting from XHR callbacks. setTimeout(function pageSetTimeout() { - try { - self.display(gfx, self.callback); - } catch (e) { - if (self.callback) self.callback(e.toString()); - throw e; - } + self.displayReadyPromise.resolve(); }); }; @@ -241,6 +245,7 @@ var Page = (function pagePage() { startIdx = gfx.executeIRQueue(IRQueue, startIdx, next); if (startIdx == length) { self.stats.render = Date.now(); + gfx.endDrawing(); if (callback) callback(); } } @@ -262,57 +267,160 @@ var Page = (function pagePage() { } }, getLinks: function pageGetLinks() { + var links = []; + var annotations = pageGetAnnotations(); + var i, n = annotations.length; + for (i = 0; i < n; ++i) { + if (annotations[i].type != 'Link') + continue; + links.push(annotations[i]); + } + return links; + }, + getAnnotations: function pageGetAnnotations() { var xref = this.xref; + function getInheritableProperty(annotation, name) { + var item = annotation; + while (item && !item.has(name)) { + item = xref.fetchIfRef(item.get('Parent')); + } + if (!item) + return null; + return item.get(name); + } + var annotations = xref.fetchIfRef(this.annotations) || []; var i, n = annotations.length; - var links = []; + var items = []; for (i = 0; i < n; ++i) { - var annotation = xref.fetch(annotations[i]); + var annotationRef = annotations[i]; + var annotation = xref.fetch(annotationRef); if (!isDict(annotation)) continue; var subtype = annotation.get('Subtype'); - if (!isName(subtype) || subtype.name != 'Link') + if (!isName(subtype)) continue; var rect = annotation.get('Rect'); var topLeftCorner = this.rotatePoint(rect[0], rect[1]); var bottomRightCorner = this.rotatePoint(rect[2], rect[3]); - var link = {}; - link.x = Math.min(topLeftCorner.x, bottomRightCorner.x); - link.y = Math.min(topLeftCorner.y, bottomRightCorner.y); - link.width = Math.abs(topLeftCorner.x - bottomRightCorner.x); - link.height = Math.abs(topLeftCorner.y - bottomRightCorner.y); - var a = this.xref.fetchIfRef(annotation.get('A')); - if (a) { - switch (a.get('S').name) { - case 'URI': - link.url = a.get('URI'); + var item = {}; + item.type = subtype.name; + item.x = Math.min(topLeftCorner.x, bottomRightCorner.x); + item.y = Math.min(topLeftCorner.y, bottomRightCorner.y); + item.width = Math.abs(topLeftCorner.x - bottomRightCorner.x); + item.height = Math.abs(topLeftCorner.y - bottomRightCorner.y); + switch (subtype.name) { + case 'Link': + var a = this.xref.fetchIfRef(annotation.get('A')); + if (a) { + switch (a.get('S').name) { + case 'URI': + item.url = a.get('URI'); + break; + case 'GoTo': + item.dest = a.get('D'); + break; + default: + TODO('other link types'); + } + } else if (annotation.has('Dest')) { + // simple destination link + var dest = annotation.get('Dest'); + item.dest = isName(dest) ? dest.name : dest; + } + break; + case 'Widget': + var fieldType = getInheritableProperty(annotation, 'FT'); + if (!isName(fieldType)) break; - case 'GoTo': - link.dest = a.get('D'); - break; - default: - TODO('other link types'); - } - } else if (annotation.has('Dest')) { - // simple destination link - var dest = annotation.get('Dest'); - link.dest = isName(dest) ? dest.name : dest; + item.fieldType = fieldType.name; + // Building the full field name by collecting the field and + // its ancestors 'T' properties and joining them using '.'. + var fieldName = []; + var namedItem = annotation, ref = annotationRef; + while (namedItem) { + var parentRef = namedItem.get('Parent'); + var parent = xref.fetchIfRef(parentRef); + var name = namedItem.get('T'); + if (name) + fieldName.unshift(stringToPDFString(name)); + else { + // The field name is absent, that means more than one field + // with the same name may exist. Replacing the empty name + // with the '`' plus index in the parent's 'Kids' array. + // This is not in the PDF spec but necessary to id the + // the input controls. + var kids = xref.fetchIfRef(parent.get('Kids')); + var j, jj; + for (j = 0, jj = kids.length; j < jj; j++) { + if (kids[j].num == ref.num && kids[j].gen == ref.gen) + break; + } + fieldName.unshift('`' + j); + } + namedItem = parent; + ref = parentRef; + } + item.fullName = fieldName.join('.'); + var alternativeText = stringToPDFString(annotation.get('TU') || ''); + item.alternativeText = alternativeText; + var da = getInheritableProperty(annotation, 'DA') || ''; + var m = /([\d\.]+)\sTf/.exec(da); + if (m) + item.fontSize = parseFloat(m[1]); + item.textAlignment = getInheritableProperty(annotation, 'Q'); + item.flags = getInheritableProperty(annotation, 'Ff') || 0; + break; + case 'Text': + var content = annotation.get('Contents'); + var title = annotation.get('T'); + item.content = stringToPDFString(content || ''); + item.title = stringToPDFString(title || ''); + item.name = annotation.get('Name').name; + break; + default: + TODO('unimplemented annotation type: ' + subtype.name); + break; } - links.push(link); + items.push(item); } - return links; + return items; }, - startRendering: function pageStartRendering(ctx, callback) { - this.ctx = ctx; - this.callback = callback; - + startRendering: function pageStartRendering(ctx, callback, textLayer) { this.startRenderingTime = Date.now(); - this.pdf.startRendering(this); + + // If there is no displayReadyPromise yet, then the IRQueue was never + // requested before. Make the request and create the promise. + if (!this.displayReadyPromise) { + this.pdf.startRendering(this); + this.displayReadyPromise = new Promise(); + } + + // Once the IRQueue and fonts are loaded, perform the actual rendering. + this.displayReadyPromise.then( + function pageDisplayReadyPromise() { + var gfx = new CanvasGraphics(ctx, this.objs, textLayer); + try { + this.display(gfx, callback); + } catch (e) { + if (callback) + callback(e); + else + throw e; + } + }.bind(this), + function pageDisplayReadPromiseError(reason) { + if (callback) + callback(reason); + else + throw reason; + } + ); } }; - return constructor; + return Page; })(); /** @@ -325,8 +433,8 @@ var Page = (function pagePage() { * need for the `PDFDocModel` anymore and there is only one object on the * main thread and not one entire copy on each worker instance. */ -var PDFDocModel = (function pdfDoc() { - function constructor(arg, callback) { +var PDFDocModel = (function PDFDocModelClosure() { + function PDFDocModel(arg, callback) { if (isStream(arg)) init.call(this, arg); else if (isArrayBuffer(arg)) @@ -339,6 +447,7 @@ var PDFDocModel = (function pdfDoc() { assertWellFormed(stream.length > 0, 'stream must have data'); this.stream = stream; this.setup(); + this.acroForm = this.xref.fetchIfRef(this.catalog.catDict.get('AcroForm')); } function find(stream, needle, limit, backwards) { @@ -357,7 +466,7 @@ var PDFDocModel = (function pdfDoc() { return true; /* found */ } - constructor.prototype = { + PDFDocModel.prototype = { get linearization() { var length = this.stream.length; var linearization = false; @@ -379,12 +488,17 @@ var PDFDocModel = (function pdfDoc() { if (find(stream, 'endobj', 1024)) startXRef = stream.pos + 6; } else { - // Find startxref at the end of the file. - var start = stream.end - 1024; - if (start < 0) - start = 0; - stream.pos = start; - if (find(stream, 'startxref', 1024, true)) { + // Find startxref by jumping backward from the end of the file. + var step = 1024; + var found = false, pos = stream.end; + while (!found && pos > 0) { + pos -= step - 'startxref'.length; + if (pos < 0) + pos = 0; + stream.pos = pos; + found = find(stream, 'startxref', step, true); + } + if (found) { stream.skip(9); var ch; do { @@ -425,10 +539,19 @@ var PDFDocModel = (function pdfDoc() { }, setup: function pdfDocSetup(ownerPassword, userPassword) { this.checkHeader(); - this.xref = new XRef(this.stream, - this.startXRef, - this.mainXRefEntriesOffset); - this.catalog = new Catalog(this.xref); + var xref = new XRef(this.stream, + this.startXRef, + this.mainXRefEntriesOffset); + this.xref = xref; + this.catalog = new Catalog(xref); + if (xref.trailer && xref.trailer.has('ID')) { + var fileID = ''; + var id = xref.fetchIfRef(xref.trailer.get('ID'))[0]; + id.split('').forEach(function(el) { + fileID += Number(el.charCodeAt(0)).toString(16); + }); + this.fileID = fileID; + } }, get numPages() { var linearization = this.linearization; @@ -436,16 +559,32 @@ var PDFDocModel = (function pdfDoc() { // shadow the prototype getter return shadow(this, 'numPages', num); }, + getFingerprint: function pdfDocGetFingerprint() { + if (this.fileID) { + return this.fileID; + } else { + // If we got no fileID, then we generate one, + // from the first 100 bytes of PDF + var data = this.stream.bytes.subarray(0, 100); + var hash = calculateMD5(data, 0, data.length); + var strHash = ''; + for (var i = 0, length = hash.length; i < length; i++) { + strHash += Number(hash[i]).toString(16); + } + + return strHash; + } + }, getPage: function pdfDocGetPage(n) { return this.catalog.getPage(n); } }; - return constructor; + return PDFDocModel; })(); -var PDFDoc = (function pdfDoc() { - function constructor(arg, callback) { +var PDFDoc = (function PDFDocClosure() { + function PDFDoc(arg, callback) { var stream = null; var data = null; @@ -462,7 +601,7 @@ var PDFDoc = (function pdfDoc() { this.data = data; this.stream = stream; this.pdf = new PDFDocModel(stream); - + this.fingerprint = this.pdf.getFingerprint(); this.catalog = this.pdf.catalog; this.objs = new PDFObjects(); @@ -481,39 +620,38 @@ var PDFDoc = (function pdfDoc() { throw 'No PDFJS.workerSrc specified'; } - var worker; try { - worker = new Worker(workerSrc); - } catch (e) { // Some versions of FF can't create a worker on localhost, see: // https://bugzilla.mozilla.org/show_bug.cgi?id=683280 - globalScope.PDFJS.disableWorker = true; - this.setupFakeWorker(); + var worker = new Worker(workerSrc); + + var messageHandler = new MessageHandler('main', worker); + // Tell the worker the file it was created from. + messageHandler.send('workerSrc', workerSrc); + messageHandler.on('test', function pdfDocTest(supportTypedArray) { + if (supportTypedArray) { + this.worker = worker; + this.setupMessageHandler(messageHandler); + } else { + globalScope.PDFJS.disableWorker = true; + this.setupFakeWorker(); + } + }.bind(this)); + + var testObj = new Uint8Array(1); + // Some versions of Opera throw a DATA_CLONE_ERR on + // serializing the typed array. + messageHandler.send('test', testObj); return; - } - - var messageHandler = new MessageHandler('main', worker); - - // Tell the worker the file it was created from. - messageHandler.send('workerSrc', workerSrc); - - messageHandler.on('test', function pdfDocTest(supportTypedArray) { - if (supportTypedArray) { - this.worker = worker; - this.setupMessageHandler(messageHandler); - } else { - this.setupFakeWorker(); - } - }.bind(this)); - - var testObj = new Uint8Array(1); - messageHandler.send('test', testObj); - } else { - this.setupFakeWorker(); + } catch (e) {} } + // Either workers are disabled, not supported or have thrown an exception. + // Thus, we fallback to a faked worker. + globalScope.PDFJS.disableWorker = true; + this.setupFakeWorker(); } - constructor.prototype = { + PDFDoc.prototype = { setupFakeWorker: function() { // If we don't use a worker, just post/sendMessage to the main thread. var fakeWorker = { @@ -549,8 +687,12 @@ var PDFDoc = (function pdfDoc() { switch (type) { case 'JpegStream': - var IR = data[2]; - new JpegImageLoader(id, IR, this.objs); + var imageData = data[2]; + loadJpegStream(id, imageData, this.objs); + break; + case 'Image': + var imageData = data[2]; + this.objs.resolve(id, imageData); break; case 'Font': var name = data[2]; @@ -558,20 +700,9 @@ var PDFDoc = (function pdfDoc() { var properties = data[4]; if (file) { + // Rewrap the ArrayBuffer in a stream. var fontFileDict = new Dict(); - fontFileDict.map = file.dict.map; - - var fontFile = new Stream(file.bytes, file.start, - file.end - file.start, fontFileDict); - - // Check if this is a FlateStream. Otherwise just use the created - // Stream one. This makes complex_ttf_font.pdf work. - var cmf = file.bytes[0]; - if ((cmf & 0x0f) == 0x08) { - file = new FlateStream(fontFile); - } else { - file = fontFile; - } + file = new Stream(file, 0, file.length, fontFileDict); } // For now, resolve the font object here direclty. The real font @@ -599,6 +730,49 @@ var PDFDoc = (function pdfDoc() { } }.bind(this)); + messageHandler.on('page_error', function pdfDocError(data) { + var page = this.pageCache[data.pageNum]; + if (page.displayReadyPromise) + page.displayReadyPromise.reject(data.error); + else + throw data.error; + }, this); + + messageHandler.on('jpeg_decode', function(data, promise) { + var imageData = data[0]; + var components = data[1]; + if (components != 3 && components != 1) + error('Only 3 component or 1 component can be returned'); + + var img = new Image(); + img.onload = (function jpegImageLoaderOnload() { + var width = img.width; + var height = img.height; + var size = width * height; + var rgbaLength = size * 4; + var buf = new Uint8Array(size * components); + var tmpCanvas = new ScratchCanvas(width, height); + var tmpCtx = tmpCanvas.getContext('2d'); + tmpCtx.drawImage(img, 0, 0); + var data = tmpCtx.getImageData(0, 0, width, height).data; + + if (components == 3) { + for (var i = 0, j = 0; i < rgbaLength; i += 4, j += 3) { + buf[j] = data[i]; + buf[j + 1] = data[i + 1]; + buf[j + 2] = data[i + 2]; + } + } else if (components == 1) { + for (var i = 0, j = 0; i < rgbaLength; i += 4, j++) { + buf[j] = data[i]; + } + } + promise.resolve({ data: buf, width: width, height: height}); + }).bind(this); + var src = 'data:image/jpeg;base64,' + window.btoa(imageData); + img.src = src; + }); + setTimeout(function pdfDocFontReadySetTimeout() { messageHandler.send('doc', this.data); this.workerReadyPromise.resolve(true); @@ -645,7 +819,7 @@ var PDFDoc = (function pdfDoc() { } }; - return constructor; + return PDFDoc; })(); globalScope.PDFJS.PDFDoc = PDFDoc; diff --git a/src/crypto.js b/src/crypto.js index 955598644..7c34a8506 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -3,8 +3,8 @@ 'use strict'; -var ARCFourCipher = (function arcFourCipher() { - function constructor(key) { +var ARCFourCipher = (function ARCFourCipherClosure() { + function ARCFourCipher(key) { this.a = 0; this.b = 0; var s = new Uint8Array(256); @@ -20,7 +20,7 @@ var ARCFourCipher = (function arcFourCipher() { this.s = s; } - constructor.prototype = { + ARCFourCipher.prototype = { encryptBlock: function arcFourCipherEncryptBlock(data) { var i, n = data.length, tmp, tmp2; var a = this.a, b = this.b, s = this.s; @@ -39,12 +39,12 @@ var ARCFourCipher = (function arcFourCipher() { return output; } }; - constructor.prototype.decryptBlock = constructor.prototype.encryptBlock; + ARCFourCipher.prototype.decryptBlock = ARCFourCipher.prototype.encryptBlock; - return constructor; + return ARCFourCipher; })(); -var calculateMD5 = (function calculateMD5() { +var calculateMD5 = (function calculateMD5Closure() { var r = new Uint8Array([ 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, @@ -128,20 +128,20 @@ var calculateMD5 = (function calculateMD5() { return hash; })(); -var NullCipher = (function nullCipher() { - function constructor() { +var NullCipher = (function NullCipherClosure() { + function NullCipher() { } - constructor.prototype = { + NullCipher.prototype = { decryptBlock: function nullCipherDecryptBlock(data) { return data; } }; - return constructor; + return NullCipher; })(); -var AES128Cipher = (function aes128Cipher() { +var AES128Cipher = (function AES128CipherClosure() { var rcon = new Uint8Array([ 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, @@ -330,7 +330,7 @@ var AES128Cipher = (function aes128Cipher() { return state; } - function constructor(key) { + function AES128Cipher(key) { this.key = expandKey128(key); this.buffer = new Uint8Array(16); this.bufferPosition = 0; @@ -370,7 +370,7 @@ var AES128Cipher = (function aes128Cipher() { return output; } - constructor.prototype = { + AES128Cipher.prototype = { decryptBlock: function aes128CipherDecryptBlock(data) { var i, sourceLength = data.length; var buffer = this.buffer, bufferLength = this.bufferPosition; @@ -391,15 +391,15 @@ var AES128Cipher = (function aes128Cipher() { } }; - return constructor; + return AES128Cipher; })(); -var CipherTransform = (function cipherTransform() { - function constructor(stringCipherConstructor, streamCipherConstructor) { +var CipherTransform = (function CipherTransformClosure() { + function CipherTransform(stringCipherConstructor, streamCipherConstructor) { this.stringCipherConstructor = stringCipherConstructor; this.streamCipherConstructor = streamCipherConstructor; } - constructor.prototype = { + CipherTransform.prototype = { createStream: function cipherTransformCreateStream(stream) { var cipher = new this.streamCipherConstructor(); return new DecryptStream(stream, @@ -415,10 +415,10 @@ var CipherTransform = (function cipherTransform() { return bytesToString(data); } }; - return constructor; + return CipherTransform; })(); -var CipherTransformFactory = (function cipherTransformFactory() { +var CipherTransformFactory = (function CipherTransformFactoryClosure() { function prepareKeyData(fileId, password, ownerPassword, userPassword, flags, revision, keyLength, encryptMetadata) { var defaultPasswordBytes = new Uint8Array([ @@ -490,7 +490,7 @@ var CipherTransformFactory = (function cipherTransformFactory() { var identityName = new Name('Identity'); - function constructor(dict, fileId, password) { + function CipherTransformFactory(dict, fileId, password) { var filter = dict.get('Filter'); if (!isName(filter) || filter.name != 'Standard') error('unknown encryption method'); @@ -573,7 +573,7 @@ var CipherTransformFactory = (function cipherTransformFactory() { return null; } - constructor.prototype = { + CipherTransformFactory.prototype = { createCipherTransform: function buildCipherCreateCipherTransform(num, gen) { if (this.algorithm == 4) { @@ -592,6 +592,6 @@ var CipherTransformFactory = (function cipherTransformFactory() { } }; - return constructor; + return CipherTransformFactory; })(); diff --git a/src/evaluator.js b/src/evaluator.js index a863a531e..2905565da 100644 --- a/src/evaluator.js +++ b/src/evaluator.js @@ -3,8 +3,8 @@ 'use strict'; -var PartialEvaluator = (function partialEvaluator() { - function constructor(xref, handler, uniquePrefix) { +var PartialEvaluator = (function PartialEvaluatorClosure() { + function PartialEvaluator(xref, handler, uniquePrefix) { this.state = new EvalState(); this.stateStack = []; @@ -111,7 +111,7 @@ var PartialEvaluator = (function partialEvaluator() { EX: 'endCompat' }; - constructor.prototype = { + PartialEvaluator.prototype = { getIRQueue: function partialEvaluatorGetIRQueue(stream, resources, queue, dependency) { @@ -155,6 +155,11 @@ var PartialEvaluator = (function partialEvaluator() { font.loadedName = loadedName; var translated = font.translated; + // Convert the file to an ArrayBuffer which will be turned back into + // a Stream in the main thread. + if (translated.file) + translated.file = translated.file.getBytes(); + handler.send('obj', [ loadedName, 'Font', @@ -179,62 +184,54 @@ var PartialEvaluator = (function partialEvaluator() { var w = dict.get('Width', 'W'); var h = dict.get('Height', 'H'); - if (image instanceof JpegStream && image.isNative) { - var objId = 'img_' + uniquePrefix + (++self.objIdCounter); - handler.send('obj', [objId, 'JpegStream', image.getIR()]); + var imageMask = dict.get('ImageMask', 'IM') || false; + if (imageMask) { + // 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. - // Add the dependency on the image object. - insertDependency([objId]); - - // The normal fn. - fn = 'paintJpegXObject'; - args = [objId, w, h]; + 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; + fn = 'paintImageMaskXObject'; + args = [imgArray, inverseDecode, width, height]; return; } - // 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); + var objId = 'img_' + uniquePrefix + (++self.objIdCounter); + insertDependency([objId]); + args = [objId, w, h]; - 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]; + var softMask = dict.get('SMask', 'IM') || false; + if (!softMask && image instanceof JpegStream && image.isNative) { + // These JPEGs don't need any more processing so we can just send it. + fn = 'paintJpegXObject'; + handler.send('obj', [objId, 'JpegStream', image.getIR()]); return; } - // 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'; + fn = 'paintImageXObject'; - 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]; + PDFImage.buildImage(function(imageObj) { + var drawWidth = imageObj.drawWidth; + var drawHeight = imageObj.drawHeight; + var imgData = { + width: drawWidth, + height: drawHeight, + data: new Uint8Array(drawWidth * drawHeight * 4) + }; + var pixels = imgData.data; + imageObj.fillRgbaBuffer(pixels, drawWidth, drawHeight); + handler.send('obj', [objId, 'Image', imgData]); + }, handler, xref, resources, image, inline); } uniquePrefix = uniquePrefix || ''; @@ -493,6 +490,8 @@ var PartialEvaluator = (function partialEvaluator() { var baseName = encoding.get('BaseEncoding'); if (baseName) baseEncoding = Encodings[baseName.name]; + else + hasEncoding = false; // base encoding was not provided // Load the differences between the base and original if (encoding.has('Differences')) { @@ -512,6 +511,7 @@ var PartialEvaluator = (function partialEvaluator() { error('Encoding is not a Name nor a Dict'); } } + properties.differences = differences; properties.baseEncoding = baseEncoding; properties.hasEncoding = hasEncoding; @@ -554,9 +554,21 @@ var PartialEvaluator = (function partialEvaluator() { var startRange = tokens[j]; var endRange = tokens[j + 1]; var code = tokens[j + 2]; - while (startRange <= endRange) { - charToUnicode[startRange] = code++; - ++startRange; + if (code == 0xFFFF) { + // CMap is broken, assuming code == startRange + code = startRange; + } + if (isArray(code)) { + var codeindex = 0; + while (startRange <= endRange) { + charToUnicode[startRange] = code[codeindex++]; + ++startRange; + } + } else { + while (startRange <= endRange) { + charToUnicode[startRange] = code++; + ++startRange; + } } } break; @@ -595,9 +607,18 @@ var PartialEvaluator = (function partialEvaluator() { } } else if (byte == 0x3E) { if (token.length) { - // parsing hex number - tokens.push(parseInt(token, 16)); - token = ''; + if (token.length <= 4) { + // parsing hex number + tokens.push(parseInt(token, 16)); + token = ''; + } else { + // parsing hex UTF-16BE numbers + var str = []; + for (var i = 0, ii = token.length; i < ii; i += 4) + str.push(parseInt(token.substr(i, 4), 16)); + tokens.push(String.fromCharCode.apply(String, str)); + token = ''; + } } } else { token += String.fromCharCode(byte); @@ -829,11 +850,11 @@ var PartialEvaluator = (function partialEvaluator() { } }; - return constructor; + return PartialEvaluator; })(); -var EvalState = (function evalState() { - function constructor() { +var EvalState = (function EvalStateClosure() { + function EvalState() { // Are soft masks and alpha values shapes or opacities? this.alphaIsShape = false; this.fontSize = 0; @@ -850,8 +871,8 @@ var EvalState = (function evalState() { this.fillColorSpace = null; this.strokeColorSpace = null; } - constructor.prototype = { + EvalState.prototype = { }; - return constructor; + return EvalState; })(); diff --git a/src/fonts.js b/src/fonts.js index 116bb4dfc..1b959d6c2 100644 --- a/src/fonts.js +++ b/src/fonts.js @@ -3,8 +3,6 @@ 'use strict'; -var isWorker = (typeof window == 'undefined'); - /** * Maximum time to wait for a font to be loaded by font-face rules. */ @@ -719,20 +717,10 @@ function getUnicodeRangeFor(value) { return -1; } -function adaptUnicode(unicode) { - return (unicode <= 0x1F || (unicode >= 127 && unicode < kSizeOfGlyphArea)) ? - unicode + kCmapGlyphOffset : unicode; -} - -function isAdaptedUnicode(unicode) { - return unicode >= kCmapGlyphOffset && - unicode < kCmapGlyphOffset + kSizeOfGlyphArea; -} - function isSpecialUnicode(unicode) { return (unicode <= 0x1F || (unicode >= 127 && unicode < kSizeOfGlyphArea)) || - unicode >= kCmapGlyphOffset && - unicode < kCmapGlyphOffset + kSizeOfGlyphArea; + (unicode >= kCmapGlyphOffset && + unicode < kCmapGlyphOffset + kSizeOfGlyphArea); } /** @@ -743,8 +731,8 @@ function isSpecialUnicode(unicode) { * var type1Font = new Font("MyFontName", binaryFile, propertiesObject); * type1Font.bind(); */ -var Font = (function Font() { - var constructor = function font_constructor(name, file, properties) { +var Font = (function FontClosure() { + function Font(name, file, properties) { this.name = name; this.coded = properties.coded; this.charProcIRQueues = properties.charProcIRQueues; @@ -771,16 +759,23 @@ var Font = (function Font() { this.widths = properties.widths; this.defaultWidth = properties.defaultWidth; this.composite = properties.composite; - this.toUnicode = properties.toUnicode; this.hasEncoding = properties.hasEncoding; this.fontMatrix = properties.fontMatrix; - if (properties.type == 'Type3') + this.widthMultiplier = 1.0; + if (properties.type == 'Type3') { + this.encoding = properties.baseEncoding; return; + } // Trying to fix encoding using glyph CIDSystemInfo. this.loadCidToUnicode(properties); + if (properties.toUnicode) + this.toUnicode = properties.toUnicode; + else + this.rebuildToUnicode(properties); + if (!file) { // The file data is not specified. Trying to fix the font name // to be used with the canvas.font. @@ -832,6 +827,8 @@ var Font = (function Font() { this.data = data; this.fontMatrix = properties.fontMatrix; + this.widthMultiplier = !properties.fontMatrix ? 1.0 : + 1.0 / properties.fontMatrix[0]; this.encoding = properties.baseEncoding; this.hasShortCmap = properties.hasShortCmap; this.loadedName = getUniqueName(); @@ -887,6 +884,13 @@ var Font = (function Font() { String.fromCharCode(value & 0xff); }; + function safeString16(value) { + // clamp value to the 16-bit int range + value = value > 0x7FFF ? 0x7FFF : value < -0x8000 ? -0x8000 : value; + return String.fromCharCode((value >> 8) & 0xff) + + String.fromCharCode(value & 0xff); + }; + function string32(value) { return String.fromCharCode((value >> 24) & 0xff) + String.fromCharCode((value >> 16) & 0xff) + @@ -961,15 +965,15 @@ var Font = (function Font() { var ranges = []; for (var n = 0; n < length; ) { var start = codes[n].unicode; - var startCode = codes[n].code; + var codeIndices = [codes[n].code]; ++n; var end = start; while (n < length && end + 1 == codes[n].unicode) { + codeIndices.push(codes[n].code); ++end; ++n; } - var endCode = codes[n - 1].code; - ranges.push([start, end, startCode, endCode]); + ranges.push([start, end, codeIndices]); } return ranges; @@ -1012,17 +1016,16 @@ var Font = (function Font() { idDeltas += string16(0); idRangeOffsets += string16(offset); - var startCode = range[2]; - var endCode = range[3]; - for (var j = startCode; j <= endCode; ++j) - glyphsIds += string16(deltas[j]); + var codes = range[2]; + for (var j = 0, jj = codes.length; j < jj; ++j) + glyphsIds += string16(deltas[codes[j]]); } } else { for (var i = 0; i < segCount - 1; i++) { var range = ranges[i]; var start = range[0]; var end = range[1]; - var startCode = range[2]; + var startCode = range[2][0]; startCount += string16(start); endCount += string16(end); @@ -1226,7 +1229,7 @@ var Font = (function Font() { return nameTable; } - constructor.prototype = { + Font.prototype = { name: null, font: null, mimetype: null, @@ -1299,7 +1302,7 @@ var Font = (function Font() { properties.baseEncoding = encoding; } - function replaceCMapTable(cmap, font, properties) { + function readCMapTable(cmap, font) { var start = (font.start ? font.start : 0) + cmap.offset; font.pos = start; @@ -1316,7 +1319,7 @@ var Font = (function Font() { } // Check that table are sorted by platformID then encodingID, - records.sort(function fontReplaceCMapTableSort(a, b) { + records.sort(function fontReadCMapTableSort(a, b) { return ((a.platformID << 16) + a.encodingID) - ((b.platformID << 16) + b.encodingID); }); @@ -1371,16 +1374,15 @@ var Font = (function Font() { for (var j = 0; j < 256; j++) { var index = font.getByte(); if (index) { - var unicode = adaptUnicode(j); - glyphs.push({ unicode: unicode, code: j }); + glyphs.push({ unicode: j, code: j }); ids.push(index); } } - - properties.hasShortCmap = true; - - createGlyphNameMap(glyphs, ids, properties); - return cmap.data = createCMapTable(glyphs, ids); + return { + glyphs: glyphs, + ids: ids, + hasShortCmap: true + }; } else if (format == 4) { // re-creating the table in format 4 since the encoding // might be changed @@ -1432,17 +1434,18 @@ var Font = (function Font() { var glyphCode = offsetIndex < 0 ? j : offsets[offsetIndex + j - start]; glyphCode = (glyphCode + delta) & 0xFFFF; - if (glyphCode == 0 || isAdaptedUnicode(j)) + if (glyphCode == 0) continue; - var unicode = adaptUnicode(j); - glyphs.push({ unicode: unicode, code: j }); + glyphs.push({ unicode: j, code: j }); ids.push(glyphCode); } } - createGlyphNameMap(glyphs, ids, properties); - return cmap.data = createCMapTable(glyphs, ids); + return { + glyphs: glyphs, + ids: ids + }; } else if (format == 6) { // Format 6 is a 2-bytes dense mapping, which means the font data // lives glue together even if they are pretty far in the unicode @@ -1457,19 +1460,18 @@ var Font = (function Font() { for (var j = 0; j < entryCount; j++) { var glyphCode = int16(font.getBytes(2)); var code = firstCode + j; - if (isAdaptedUnicode(glyphCode)) - continue; - var unicode = adaptUnicode(code); - glyphs.push({ unicode: unicode, code: code }); + glyphs.push({ unicode: code, code: code }); ids.push(glyphCode); } - createGlyphNameMap(glyphs, ids, properties); - return cmap.data = createCMapTable(glyphs, ids); + return { + glyphs: glyphs, + ids: ids + }; } } - return cmap.data; + error('Unsupported cmap table format'); }; function sanitizeMetrics(font, header, metrics, numGlyphs) { @@ -1708,17 +1710,108 @@ var Font = (function Font() { tables.push(cmap); } - var glyphs = []; - for (i = 1; i < numGlyphs; i++) { - if (isAdaptedUnicode(i)) - continue; - - glyphs.push({ unicode: adaptUnicode(i) }); + var cidToGidMap = properties.cidToGidMap || []; + var gidToCidMap = [0]; + if (cidToGidMap.length > 0) { + for (var j = cidToGidMap.length - 1; j >= 0; j--) { + var gid = cidToGidMap[j]; + if (gid) + gidToCidMap[gid] = j; + } + // filling the gaps using CID above the CIDs currently used in font + var nextCid = cidToGidMap.length; + for (var i = 1; i < numGlyphs; i++) { + if (!gidToCidMap[i]) + gidToCidMap[i] = nextCid++; + } } - cmap.data = createCMapTable(glyphs); + + var glyphs = [], ids = []; + var usedUnicodes = []; + var unassignedUnicodeItems = []; + for (var i = 1; i < numGlyphs; i++) { + var cid = gidToCidMap[i] || i; + var unicode = this.toUnicode[cid]; + if (!unicode || isSpecialUnicode(unicode) || + unicode in usedUnicodes) { + unassignedUnicodeItems.push(i); + continue; + } + usedUnicodes[unicode] = true; + glyphs.push({ unicode: unicode, code: cid }); + ids.push(i); + } + // trying to fit as many unassigned symbols as we can + // in the range allocated for the user defined symbols + var unusedUnicode = kCmapGlyphOffset; + for (var j = 0, jj = unassignedUnicodeItems.length; j < jj; j++) { + var i = unassignedUnicodeItems[j]; + var cid = gidToCidMap[i] || i; + while (unusedUnicode in usedUnicodes) + unusedUnicode++; + if (unusedUnicode >= kCmapGlyphOffset + kSizeOfGlyphArea) + break; + var unicode = unusedUnicode++; + this.toUnicode[cid] = unicode; + usedUnicodes[unicode] = true; + glyphs.push({ unicode: unicode, code: cid }); + ids.push(i); + } + cmap.data = createCMapTable(glyphs, ids); } else { - replaceCMapTable(cmap, font, properties); + var cmapTable = readCMapTable(cmap, font); + var glyphs = cmapTable.glyphs; + var ids = cmapTable.ids; + var hasShortCmap = !!cmapTable.hasShortCmap; + var toUnicode = this.toUnicode; + + if (toUnicode && toUnicode.length > 0) { + // checking if cmap is just identity map + var isIdentity = true; + for (var i = 0, ii = glyphs.length; i < ii; i++) { + if (glyphs[i].unicode != i + 1) { + isIdentity = false; + break; + } + } + // if it is, replacing with meaningful toUnicode values + if (isIdentity) { + var usedUnicodes = [], unassignedUnicodeItems = []; + for (var i = 0, ii = glyphs.length; i < ii; i++) { + var unicode = toUnicode[i + 1]; + if (!unicode || unicode in usedUnicodes) { + unassignedUnicodeItems.push(i); + continue; + } + glyphs[i].unicode = unicode; + usedUnicodes[unicode] = true; + } + var unusedUnicode = kCmapGlyphOffset; + for (var j = 0, jj = unassignedUnicodeItems.length; j < jj; j++) { + var i = unassignedUnicodeItems[j]; + while (unusedUnicode in usedUnicodes) + unusedUnicode++; + var cid = i + 1; + // override only if unicode mapping is not specified + if (!(cid in toUnicode)) + toUnicode[cid] = unusedUnicode; + glyphs[i].unicode = unusedUnicode++; + } + this.useToUnicode = true; + } + } + properties.hasShortCmap = hasShortCmap; + + // remove glyph references outside range of avaialable glyphs + for (var i = 0, ii = ids.length; i < ii; i++) { + if (ids[i] >= numGlyphs) + ids[i] = 0; + } + + createGlyphNameMap(glyphs, ids, properties); this.glyphNameMap = properties.glyphNameMap; + + cmap.data = createCMapTable(glyphs, ids); } // Rewrite the 'post' table if needed @@ -1808,6 +1901,14 @@ var Font = (function Font() { } properties.baseEncoding = encoding; } + if (properties.subtype == 'CIDFontType0C') { + var toUnicode = []; + for (var i = 0; i < charstrings.length; ++i) { + var charstring = charstrings[i]; + toUnicode[charstring.code] = charstring.unicode; + } + this.toUnicode = toUnicode; + } var fields = { // PostScript Font Program @@ -1832,9 +1933,9 @@ var Font = (function Font() { '\x00\x00\x00\x00\x9e\x0b\x7e\x27' + // creation date '\x00\x00\x00\x00\x9e\x0b\x7e\x27' + // modifification date '\x00\x00' + // xMin - string16(properties.descent) + // yMin + safeString16(properties.descent) + // yMin '\x0F\xFF' + // xMax - string16(properties.ascent) + // yMax + safeString16(properties.ascent) + // yMax string16(properties.italicAngle ? 2 : 0) + // macStyle '\x00\x11' + // lowestRecPPEM '\x00\x00' + // fontDirectionHint @@ -1846,15 +1947,15 @@ var Font = (function Font() { 'hhea': (function fontFieldsHhea() { return stringToArray( '\x00\x01\x00\x00' + // Version number - string16(properties.ascent) + // Typographic Ascent - string16(properties.descent) + // Typographic Descent + safeString16(properties.ascent) + // Typographic Ascent + safeString16(properties.descent) + // Typographic Descent '\x00\x00' + // Line Gap '\xFF\xFF' + // advanceWidthMax '\x00\x00' + // minLeftSidebearing '\x00\x00' + // minRightSidebearing '\x00\x00' + // xMaxExtent - string16(properties.capHeight) + // caretSlopeRise - string16(Math.tan(properties.italicAngle) * + safeString16(properties.capHeight) + // caretSlopeRise + safeString16(Math.tan(properties.italicAngle) * properties.xHeight) + // caretSlopeRun '\x00\x00' + // caretOffset '\x00\x00' + // -reserved- @@ -1868,8 +1969,11 @@ var Font = (function Font() { // Horizontal metrics 'hmtx': (function fontFieldsHmtx() { var hmtx = '\x00\x00\x00\x00'; // Fake .notdef - for (var i = 0, ii = charstrings.length; i < ii; i++) - hmtx += string16(charstrings[i].width) + string16(0); + for (var i = 0, ii = charstrings.length; i < ii; i++) { + var charstring = charstrings[i]; + var width = 'width' in charstring ? charstring.width : 0; + hmtx += string16(width) + string16(0); + } return stringToArray(hmtx); })(), @@ -1898,17 +2002,35 @@ var Font = (function Font() { return stringToArray(otf.file); }, - loadCidToUnicode: function font_loadCidToUnicode(properties) { - if (properties.cidToGidMap) { - this.cidToUnicode = properties.cidToGidMap; - return; + rebuildToUnicode: function font_rebuildToUnicode(properties) { + var firstChar = properties.firstChar, lastChar = properties.lastChar; + var map = []; + if (properties.composite) { + var isIdentityMap = this.cidToUnicode.length == 0; + for (var i = firstChar, ii = lastChar; i <= ii; i++) { + // TODO missing map the character according font's CMap + var cid = i; + map[i] = isIdentityMap ? cid : this.cidToUnicode[cid]; + } + } else { + for (var i = firstChar, ii = lastChar; i <= ii; i++) { + var glyph = properties.differences[i]; + if (!glyph) + glyph = properties.baseEncoding[i]; + if (!!glyph && (glyph in GlyphsUnicode)) + map[i] = GlyphsUnicode[glyph]; + } } + this.toUnicode = map; + }, + loadCidToUnicode: function font_loadCidToUnicode(properties) { if (!properties.cidSystemInfo) return; - var cidToUnicodeMap = []; + var cidToUnicodeMap = [], unicodeToCIDMap = []; this.cidToUnicode = cidToUnicodeMap; + this.unicodeToCID = unicodeToCIDMap; var cidSystemInfo = properties.cidSystemInfo; var cidToUnicode; @@ -1920,28 +2042,34 @@ var Font = (function Font() { if (!cidToUnicode) return; // identity encoding - var glyph = 1, i, j, k, ii; + var cid = 1, i, j, k, ii; for (i = 0, ii = cidToUnicode.length; i < ii; ++i) { var unicode = cidToUnicode[i]; if (isArray(unicode)) { var length = unicode.length; - for (j = 0; j < length; j++) - cidToUnicodeMap[unicode[j]] = glyph; - glyph++; + for (j = 0; j < length; j++) { + cidToUnicodeMap[cid] = unicode[j]; + unicodeToCIDMap[unicode[j]] = cid; + } + cid++; } else if (typeof unicode === 'object') { var fillLength = unicode.f; if (fillLength) { k = unicode.c; for (j = 0; j < fillLength; ++j) { - cidToUnicodeMap[k] = glyph++; + cidToUnicodeMap[cid] = k; + unicodeToCIDMap[k] = cid; + cid++; k++; } } else - glyph += unicode.s; + cid += unicode.s; } else if (unicode) { - cidToUnicodeMap[unicode] = glyph++; + cidToUnicodeMap[cid] = unicode; + unicodeToCIDMap[unicode] = cid; + cid++; } else - glyph++; + cid++; } }, @@ -1964,7 +2092,7 @@ var Font = (function Font() { window.btoa(data) + ');'); var rule = "@font-face { font-family:'" + fontName + "';src:" + url + '}'; - document.documentElement.firstChild.appendChild( + document.documentElement.getElementsByTagName('head')[0].appendChild( document.createElement('style')); var styleSheet = document.styleSheets[document.styleSheets.length - 1]; @@ -1973,6 +2101,37 @@ var Font = (function Font() { return rule; }, + get spaceWidth() { + // trying to estimate space character width + var possibleSpaceReplacements = ['space', 'minus', 'one', 'i']; + var width; + for (var i = 0, ii = possibleSpaceReplacements.length; i < ii; i++) { + var glyphName = possibleSpaceReplacements[i]; + // if possible, getting width by glyph name + if (glyphName in this.widths) { + width = this.widths[glyphName]; + break; + } + var glyphUnicode = GlyphsUnicode[glyphName]; + // finding the charcode via unicodeToCID map + var charcode = 0; + if (this.composite) + charcode = this.unicodeToCID[glyphUnicode]; + // ... via toUnicode map + if (!charcode && 'toUnicode' in this) + charcode = this.toUnicode.indexOf(glyphUnicode); + // setting it to unicode if negative or undefined + if (!(charcode > 0)) + charcode = glyphUnicode; + // trying to get width via charcode + width = this.widths[charcode]; + if (width) + break; // the non-zero width found + } + width = (width || this.defaultWidth) * this.widthMultiplier; + return shadow(this, 'spaceWidth', width); + }, + charToGlyph: function fonts_charToGlyph(charcode) { var unicode, width, codeIRQueue; @@ -1981,30 +2140,30 @@ var Font = (function Font() { switch (this.type) { case 'CIDFontType0': if (this.noUnicodeAdaptation) { - width = this.widths[this.cidToUnicode[charcode]]; + width = this.widths[this.unicodeToCID[charcode] || charcode]; unicode = charcode; break; } - unicode = adaptUnicode(this.cidToUnicode[charcode] || charcode); + unicode = this.toUnicode[charcode] || charcode; break; case 'CIDFontType2': if (this.noUnicodeAdaptation) { - width = this.widths[this.cidToUnicode[charcode]]; + width = this.widths[this.unicodeToCID[charcode] || charcode]; unicode = charcode; break; } - unicode = adaptUnicode(this.cidToUnicode[charcode] || charcode); + unicode = this.toUnicode[charcode] || charcode; break; case 'Type1': var glyphName = this.differences[charcode] || this.encoding[charcode]; + if (!isNum(width)) + width = this.widths[glyphName]; if (this.noUnicodeAdaptation) { - if (!isNum(width)) - width = this.widths[glyphName]; unicode = GlyphsUnicode[glyphName] || charcode; break; } unicode = this.glyphNameMap[glyphName] || - adaptUnicode(GlyphsUnicode[glyphName] || charcode); + GlyphsUnicode[glyphName] || charcode; break; case 'Type3': var glyphName = this.differences[charcode] || this.encoding[charcode]; @@ -2012,6 +2171,10 @@ var Font = (function Font() { unicode = charcode; break; case 'TrueType': + if (this.useToUnicode) { + unicode = this.toUnicode[charcode] || charcode; + break; + } var glyphName = this.differences[charcode] || this.encoding[charcode]; if (!glyphName) glyphName = Encodings.StandardEncoding[charcode]; @@ -2022,16 +2185,16 @@ var Font = (function Font() { break; } if (!this.hasEncoding) { - unicode = adaptUnicode(charcode); + unicode = this.useToUnicode ? this.toUnicode[charcode] : charcode; break; } - if (this.hasShortCmap) { + if (this.hasShortCmap && false) { var j = Encodings.MacRomanEncoding.indexOf(glyphName); - unicode = j >= 0 && !isSpecialUnicode(j) ? j : + unicode = j >= 0 ? j : this.glyphNameMap[glyphName]; } else { unicode = glyphName in GlyphsUnicode ? - adaptUnicode(GlyphsUnicode[glyphName]) : + GlyphsUnicode[glyphName] : this.glyphNameMap[glyphName]; } break; @@ -2039,14 +2202,23 @@ var Font = (function Font() { warn('Unsupported font type: ' + this.type); break; } + + var unicodeChars = !('toUnicode' in this) ? charcode : + this.toUnicode[charcode] || charcode; + if (typeof unicodeChars === 'number') + unicodeChars = String.fromCharCode(unicodeChars); + + width = (isNum(width) ? width : this.defaultWidth) * this.widthMultiplier; + return { - unicode: unicode, - width: isNum(width) ? width : this.defaultWidth, + fontChar: String.fromCharCode(unicode), + unicode: unicodeChars, + width: width, codeIRQueue: codeIRQueue }; }, - charsToGlyphs: function fonts_chars2Glyphs(chars) { + charsToGlyphs: function fonts_charsToGlyphs(chars) { var charsCache = this.charsCache; var glyphs; @@ -2094,7 +2266,7 @@ var Font = (function Font() { } }; - return constructor; + return Font; })(); /* @@ -2753,22 +2925,13 @@ CFF.prototype = { getOrderedCharStrings: function cff_getOrderedCharStrings(glyphs, properties) { var charstrings = []; - var reverseMapping = {}; - var encoding = properties.baseEncoding; var i, length, glyphName; - for (i = 0, length = encoding.length; i < length; ++i) { - glyphName = encoding[i]; - if (!glyphName || isSpecialUnicode(i)) - continue; - reverseMapping[glyphName] = i; - } - reverseMapping['.notdef'] = 0; var unusedUnicode = kCmapGlyphOffset; for (i = 0, length = glyphs.length; i < length; i++) { var item = glyphs[i]; var glyphName = item.glyph; - var unicode = glyphName in reverseMapping ? - reverseMapping[glyphName] : unusedUnicode++; + var unicode = glyphName in GlyphsUnicode ? + GlyphsUnicode[glyphName] : unusedUnicode++; charstrings.push({ glyph: glyphName, unicode: unicode, @@ -3013,9 +3176,9 @@ CFF.prototype = { } }; -var Type2CFF = (function type2CFF() { +var Type2CFF = (function Type2CFFClosure() { // TODO: replace parsing code with the Type2Parser in font_utils.js - function constructor(file, properties) { + function Type2CFF(file, properties) { var bytes = file.getBytes(); this.bytes = bytes; this.properties = properties; @@ -3023,7 +3186,7 @@ var Type2CFF = (function type2CFF() { this.data = this.parse(); } - constructor.prototype = { + Type2CFF.prototype = { parse: function cff_parse() { var header = this.parseHeader(); var properties = this.properties; @@ -3055,16 +3218,14 @@ var Type2CFF = (function type2CFF() { } var charStrings = this.parseIndex(topDict.CharStrings); - var charset = this.parseCharsets(topDict.charset, - charStrings.length, strings); - var encoding = this.parseEncoding(topDict.Encoding, properties, - strings, charset); var charset, encoding; var isCIDFont = properties.subtype == 'CIDFontType0C'; if (isCIDFont) { - charset = []; - charset.length = charStrings.length; + charset = ['.notdef']; + for (var i = 1, ii = charStrings.length; i < ii; ++i) + charset.push('glyph' + i); + encoding = this.parseCidMap(topDict.charset, charStrings.length); } else { @@ -3133,38 +3294,44 @@ var Type2CFF = (function type2CFF() { var charstrings = []; var unicodeUsed = []; var unassignedUnicodeItems = []; + var inverseEncoding = []; + for (var charcode in encoding) + inverseEncoding[encoding[charcode]] = charcode | 0; for (var i = 0, ii = charsets.length; i < ii; i++) { var glyph = charsets[i]; - var encodingFound = false; - for (var charcode in encoding) { - if (encoding[charcode] == i) { - var code = charcode | 0; - charstrings.push({ - unicode: adaptUnicode(code), - code: code, - gid: i, - glyph: glyph - }); - unicodeUsed[code] = true; - encodingFound = true; - break; - } + if (glyph == '.notdef') { + charstrings.push({ + unicode: 0, + code: 0, + gid: i, + glyph: glyph + }); + continue; } - if (!encodingFound) { + var code = inverseEncoding[i]; + if (!code || isSpecialUnicode(code)) { unassignedUnicodeItems.push(i); + continue; } + charstrings.push({ + unicode: code, + code: code, + gid: i, + glyph: glyph + }); + unicodeUsed[code] = true; } - var nextUnusedUnicode = 0x21; + var nextUnusedUnicode = kCmapGlyphOffset; for (var j = 0, jj = unassignedUnicodeItems.length; j < jj; ++j) { var i = unassignedUnicodeItems[j]; // giving unicode value anyway - while (unicodeUsed[nextUnusedUnicode]) + while (nextUnusedUnicode in unicodeUsed) nextUnusedUnicode++; - var code = nextUnusedUnicode++; + var unicode = nextUnusedUnicode++; charstrings.push({ - unicode: adaptUnicode(code), - code: code, + unicode: unicode, + code: inverseEncoding[i] || 0, gid: i, glyph: charsets[i] }); @@ -3563,6 +3730,6 @@ var Type2CFF = (function type2CFF() { } }; - return constructor; + return Type2CFF; })(); diff --git a/src/function.js b/src/function.js index ef24736c1..26b8fe679 100644 --- a/src/function.js +++ b/src/function.js @@ -3,7 +3,7 @@ 'use strict'; -var PDFFunction = (function pdfFunction() { +var PDFFunction = (function PDFFunctionClosure() { var CONSTRUCT_SAMPLED = 0; var CONSTRUCT_INTERPOLATED = 2; var CONSTRUCT_STICHED = 3; @@ -270,7 +270,6 @@ var PDFFunction = (function pdfFunction() { constructStiched: function pdfFunctionConstructStiched(fn, dict, xref) { var domain = dict.get('Domain'); - var range = dict.get('Range'); if (!domain) error('No domain'); @@ -279,13 +278,13 @@ var PDFFunction = (function pdfFunction() { if (inputSize != 1) error('Bad domain for stiched function'); - var fnRefs = dict.get('Functions'); + var fnRefs = xref.fetchIfRef(dict.get('Functions')); var fns = []; for (var i = 0, ii = fnRefs.length; i < ii; ++i) fns.push(PDFFunction.getIR(xref, xref.fetchIfRef(fnRefs[i]))); - var bounds = dict.get('Bounds'); - var encode = dict.get('Encode'); + var bounds = xref.fetchIfRef(dict.get('Bounds')); + var encode = xref.fetchIfRef(dict.get('Encode')); return [CONSTRUCT_STICHED, domain, bounds, encode, fns]; }, @@ -336,16 +335,550 @@ var PDFFunction = (function pdfFunction() { }; }, - constructPostScript: function pdfFunctionConstructPostScript() { - return [CONSTRUCT_POSTSCRIPT]; + constructPostScript: function pdfFunctionConstructPostScript(fn, dict, + xref) { + var domain = dict.get('Domain'); + var range = dict.get('Range'); + + if (!domain) + error('No domain.'); + + if (!range) + error('No range.'); + + var lexer = new PostScriptLexer(fn); + var parser = new PostScriptParser(lexer); + var code = parser.parse(); + + return [CONSTRUCT_POSTSCRIPT, domain, range, code]; }, - constructPostScriptFromIR: function pdfFunctionConstructPostScriptFromIR() { - TODO('unhandled type of function'); - return function constructPostScriptFromIRResult() { - return [255, 105, 180]; + constructPostScriptFromIR: + function pdfFunctionConstructPostScriptFromIR(IR) { + var domain = IR[1]; + var range = IR[2]; + var code = IR[3]; + var numOutputs = range.length / 2; + var evaluator = new PostScriptEvaluator(code); + // Cache the values for a big speed up, the cache size is limited though + // since the number of possible values can be huge from a PS function. + var cache = new FunctionCache(); + return function constructPostScriptFromIRResult(args) { + var initialStack = []; + for (var i = 0, ii = (domain.length / 2); i < ii; ++i) { + initialStack.push(args[i]); + } + + var key = initialStack.join('_'); + if (cache.has(key)) + return cache.get(key); + + var stack = evaluator.execute(initialStack); + var transformed = new Array(numOutputs); + for (i = numOutputs - 1; i >= 0; --i) { + var out = stack.pop(); + var rangeIndex = 2 * i; + if (out < range[rangeIndex]) + out = range[rangeIndex]; + else if (out > range[rangeIndex + 1]) + out = range[rangeIndex + 1]; + transformed[i] = out; + } + cache.set(key, transformed); + return transformed; }; } }; })(); +var FunctionCache = (function FunctionCacheClosure() { + // Of 10 PDF's with type4 functions the maxium number of distinct values seen + // was 256. This still may need some tweaking in the future though. + var MAX_CACHE_SIZE = 1024; + function FunctionCache() { + this.cache = {}; + this.total = 0; + } + FunctionCache.prototype = { + has: function has(key) { + return key in this.cache; + }, + get: function get(key) { + return this.cache[key]; + }, + set: function set(key, value) { + if (this.total < MAX_CACHE_SIZE) { + this.cache[key] = value; + this.total++; + } + } + }; + return FunctionCache; +})(); + +var PostScriptStack = (function PostScriptStackClosure() { + var MAX_STACK_SIZE = 100; + function PostScriptStack(initialStack) { + this.stack = initialStack || []; + } + + PostScriptStack.prototype = { + push: function push(value) { + if (this.stack.length >= MAX_STACK_SIZE) + error('PostScript function stack overflow.'); + this.stack.push(value); + }, + pop: function pop() { + if (this.stack.length <= 0) + error('PostScript function stack underflow.'); + return this.stack.pop(); + }, + copy: function copy(n) { + if (this.stack.length + n >= MAX_STACK_SIZE) + error('PostScript function stack overflow.'); + var stack = this.stack; + for (var i = stack.length - n, j = n - 1; j >= 0; j--, i++) + stack.push(stack[i]); + }, + index: function index(n) { + this.push(this.stack[this.stack.length - n - 1]); + }, + // rotate the last n stack elements p times + roll: function roll(n, p) { + var stack = this.stack; + var l = stack.length - n; + var r = stack.length - 1, c = l + (p - Math.floor(p / n) * n), i, j, t; + for (i = l, j = r; i < j; i++, j--) { + t = stack[i]; stack[i] = stack[j]; stack[j] = t; + } + for (i = l, j = c - 1; i < j; i++, j--) { + t = stack[i]; stack[i] = stack[j]; stack[j] = t; + } + for (i = c, j = r; i < j; i++, j--) { + t = stack[i]; stack[i] = stack[j]; stack[j] = t; + } + } + }; + return PostScriptStack; +})(); +var PostScriptEvaluator = (function PostScriptEvaluatorClosure() { + function PostScriptEvaluator(operators, operands) { + this.operators = operators; + this.operands = operands; + } + PostScriptEvaluator.prototype = { + execute: function execute(initialStack) { + var stack = new PostScriptStack(initialStack); + var counter = 0; + var operators = this.operators; + var length = operators.length; + var operator, a, b; + while (counter < length) { + operator = operators[counter++]; + if (typeof operator == 'number') { + // Operator is really an operand and should be pushed to the stack. + stack.push(operator); + continue; + } + switch (operator) { + // non standard ps operators + case 'jz': // jump if false + b = stack.pop(); + a = stack.pop(); + if (!a) + counter = b; + break; + case 'j': // jump + a = stack.pop(); + counter = a; + break; + + // all ps operators in alphabetical order (excluding if/ifelse) + case 'abs': + a = stack.pop(); + stack.push(Math.abs(a)); + break; + case 'add': + b = stack.pop(); + a = stack.pop(); + stack.push(a + b); + break; + case 'and': + b = stack.pop(); + a = stack.pop(); + if (isBool(a) && isBool(b)) + stack.push(a && b); + else + stack.push(a & b); + break; + case 'atan': + a = stack.pop(); + stack.push(Math.atan(a)); + break; + case 'bitshift': + b = stack.pop(); + a = stack.pop(); + if (a > 0) + stack.push(a << b); + else + stack.push(a >> b); + break; + case 'ceiling': + a = stack.pop(); + stack.push(Math.ceil(a)); + break; + case 'copy': + a = stack.pop(); + stack.copy(a); + break; + case 'cos': + a = stack.pop(); + stack.push(Math.cos(a)); + break; + case 'cvi': + a = stack.pop() | 0; + stack.push(a); + break; + case 'cvr': + // noop + break; + case 'div': + b = stack.pop(); + a = stack.pop(); + stack.push(a / b); + break; + case 'dup': + stack.copy(1); + break; + case 'eq': + b = stack.pop(); + a = stack.pop(); + stack.push(a == b); + break; + case 'exch': + stack.roll(2, 1); + break; + case 'exp': + b = stack.pop(); + a = stack.pop(); + stack.push(Math.pow(a, b)); + break; + case 'false': + stack.push(false); + break; + case 'floor': + a = stack.pop(); + stack.push(Math.floor(a)); + break; + case 'ge': + b = stack.pop(); + a = stack.pop(); + stack.push(a >= b); + break; + case 'gt': + b = stack.pop(); + a = stack.pop(); + stack.push(a > b); + break; + case 'idiv': + b = stack.pop(); + a = stack.pop(); + stack.push((a / b) | 0); + break; + case 'index': + a = stack.pop(); + stack.index(a); + break; + case 'le': + b = stack.pop(); + a = stack.pop(); + stack.push(a <= b); + break; + case 'ln': + a = stack.pop(); + stack.push(Math.log(a)); + break; + case 'log': + a = stack.pop(); + stack.push(Math.log(a) / Math.LN10); + break; + case 'lt': + b = stack.pop(); + a = stack.pop(); + stack.push(a < b); + break; + case 'mod': + b = stack.pop(); + a = stack.pop(); + stack.push(a % b); + break; + case 'mul': + b = stack.pop(); + a = stack.pop(); + stack.push(a * b); + break; + case 'ne': + b = stack.pop(); + a = stack.pop(); + stack.push(a != b); + break; + case 'neg': + a = stack.pop(); + stack.push(-b); + break; + case 'not': + a = stack.pop(); + if (isBool(a) && isBool(b)) + stack.push(a && b); + else + stack.push(a & b); + break; + case 'or': + b = stack.pop(); + a = stack.pop(); + if (isBool(a) && isBool(b)) + stack.push(a || b); + else + stack.push(a | b); + break; + case 'pop': + stack.pop(); + break; + case 'roll': + b = stack.pop(); + a = stack.pop(); + stack.roll(a, b); + break; + case 'round': + a = stack.pop(); + stack.push(Math.round(a)); + break; + case 'sin': + a = stack.pop(); + stack.push(Math.sin(a)); + break; + case 'sqrt': + a = stack.pop(); + stack.push(Math.sqrt(a)); + break; + case 'sub': + b = stack.pop(); + a = stack.pop(); + stack.push(a - b); + break; + case 'true': + stack.push(true); + break; + case 'truncate': + a = stack.pop(); + a = a < 0 ? Math.ceil(a) : Math.floor(a); + stack.push(a); + break; + case 'xor': + b = stack.pop(); + a = stack.pop(); + if (isBool(a) && isBool(b)) + stack.push(a != b); + else + stack.push(a ^ b); + break; + default: + error('Unknown operator ' + operator); + break; + } + } + return stack.stack; + } + }; + return PostScriptEvaluator; +})(); + +var PostScriptParser = (function PostScriptParserClosure() { + function PostScriptParser(lexer) { + this.lexer = lexer; + this.operators = []; + this.token; + this.prev; + } + PostScriptParser.prototype = { + nextToken: function nextToken() { + this.prev = this.token; + this.token = this.lexer.getToken(); + }, + accept: function accept(type) { + if (this.token.type == type) { + this.nextToken(); + return true; + } + return false; + }, + expect: function expect(type) { + if (this.accept(type)) + return true; + error('Unexpected symbol: found ' + this.token.type + ' expected ' + + type + '.'); + }, + parse: function parse() { + this.nextToken(); + this.expect(PostScriptTokenTypes.LBRACE); + this.parseBlock(); + this.expect(PostScriptTokenTypes.RBRACE); + return this.operators; + }, + parseBlock: function parseBlock() { + while (true) { + if (this.accept(PostScriptTokenTypes.NUMBER)) { + this.operators.push(this.prev.value); + } else if (this.accept(PostScriptTokenTypes.OPERATOR)) { + this.operators.push(this.prev.value); + } else if (this.accept(PostScriptTokenTypes.LBRACE)) { + this.parseCondition(); + } else { + return; + } + } + }, + parseCondition: function parseCondition() { + // Add two place holders that will be updated later + var conditionLocation = this.operators.length; + this.operators.push(null, null); + + this.parseBlock(); + this.expect(PostScriptTokenTypes.RBRACE); + if (this.accept(PostScriptTokenTypes.IF)) { + // The true block is right after the 'if' so it just falls through on + // true else it jumps and skips the true block. + this.operators[conditionLocation] = this.operators.length; + this.operators[conditionLocation + 1] = 'jz'; + } else if (this.accept(PostScriptTokenTypes.LBRACE)) { + var jumpLocation = this.operators.length; + this.operators.push(null, null); + var endOfTrue = this.operators.length; + this.parseBlock(); + this.expect(PostScriptTokenTypes.RBRACE); + this.expect(PostScriptTokenTypes.IFELSE); + // The jump is added at the end of the true block to skip the false + // block. + this.operators[jumpLocation] = this.operators.length; + this.operators[jumpLocation + 1] = 'j'; + + this.operators[conditionLocation] = endOfTrue; + this.operators[conditionLocation + 1] = 'jz'; + } else { + error('PS Function: error parsing conditional.'); + } + } + }; + return PostScriptParser; +})(); + +var PostScriptTokenTypes = { + LBRACE: 0, + RBRACE: 1, + NUMBER: 2, + OPERATOR: 3, + IF: 4, + IFELSE: 5 +}; + +var PostScriptToken = (function PostScriptTokenClosure() { + function PostScriptToken(type, value) { + this.type = type; + this.value = value; + } + + var opCache = {}; + + PostScriptToken.getOperator = function getOperator(op) { + var opValue = opCache[op]; + if (opValue) + return opValue; + + return opCache[op] = new PostScriptToken(PostScriptTokenTypes.OPERATOR, op); + }; + + PostScriptToken.LBRACE = new PostScriptToken(PostScriptTokenTypes.LBRACE, + '{'); + PostScriptToken.RBRACE = new PostScriptToken(PostScriptTokenTypes.RBRACE, + '}'); + PostScriptToken.IF = new PostScriptToken(PostScriptTokenTypes.IF, 'IF'); + PostScriptToken.IFELSE = new PostScriptToken(PostScriptTokenTypes.IFELSE, + 'IFELSE'); + return PostScriptToken; +})(); + +var PostScriptLexer = (function PostScriptLexerClosure() { + function PostScriptLexer(stream) { + this.stream = stream; + } + PostScriptLexer.prototype = { + getToken: function getToken() { + var s = ''; + var ch; + var comment = false; + var stream = this.stream; + + // skip comments + while (true) { + if (!(ch = stream.getChar())) + return EOF; + + if (comment) { + if (ch == '\x0a' || ch == '\x0d') + comment = false; + } else if (ch == '%') { + comment = true; + } else if (!Lexer.isSpace(ch)) { + break; + } + } + switch (ch) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case '+': case '-': case '.': + return new PostScriptToken(PostScriptTokenTypes.NUMBER, + this.getNumber(ch)); + case '{': + return PostScriptToken.LBRACE; + case '}': + return PostScriptToken.RBRACE; + } + // operator + var str = ch.toLowerCase(); + while (true) { + ch = stream.lookChar().toLowerCase(); + if (ch >= 'a' && ch <= 'z') + str += ch; + else + break; + stream.skip(); + } + switch (str) { + case 'if': + return PostScriptToken.IF; + case 'ifelse': + return PostScriptToken.IFELSE; + default: + return PostScriptToken.getOperator(str); + } + }, + getNumber: function getNumber(ch) { + var str = ch; + var stream = this.stream; + while (true) { + ch = stream.lookChar(); + if ((ch >= '0' && ch <= '9') || ch == '-' || ch == '.') + str += ch; + else + break; + stream.skip(); + } + var value = parseFloat(str); + if (isNaN(value)) + error('Invalid floating point number: ' + value); + return value; + } + }; + return PostScriptLexer; +})(); + diff --git a/src/glyphlist.js b/src/glyphlist.js index 5691f8546..01b94442a 100644 --- a/src/glyphlist.js +++ b/src/glyphlist.js @@ -4287,6 +4287,7 @@ var GlyphsUnicode = { zretroflexhook: 0x0290, zstroke: 0x01B6, zuhiragana: 0x305A, - zukatakana: 0x30BA + zukatakana: 0x30BA, + '.notdef': 0x0000 }; diff --git a/src/image.js b/src/image.js index 17ef7b06d..29bad4d8a 100644 --- a/src/image.js +++ b/src/image.js @@ -3,8 +3,37 @@ 'use strict'; -var PDFImage = (function pdfImage() { - function constructor(xref, res, image, inline) { +var PDFImage = (function PDFImageClosure() { + /** + * Decode the image in the main thread if it supported. Resovles the promise + * when the image data is ready. + */ + function handleImageData(handler, xref, res, image, promise) { + if (image instanceof JpegStream && image.isNative) { + // For natively supported jpegs send them to the main thread for decoding. + var dict = image.dict; + var colorSpace = dict.get('ColorSpace', 'CS'); + colorSpace = ColorSpace.parse(colorSpace, xref, res); + var numComps = colorSpace.numComps; + handler.send('jpeg_decode', [image.getIR(), numComps], function(message) { + var data = message.data; + var stream = new Stream(data, 0, data.length, image.dict); + promise.resolve(stream); + }); + } else { + promise.resolve(image); + } + } + /** + * Decode and clamp a value. The formula is different from the spec because we + * don't decode to float range [0,1], we decode it in the [0,max] range. + */ + function decodeAndClamp(value, addend, coefficient, max) { + value = addend + value * coefficient; + // Clamp the value to the range + return value < 0 ? 0 : value > max ? max : value; + } + function PDFImage(xref, res, image, inline, smask) { this.image = image; if (image.getParams) { // JPX/JPEG2000 streams directly contain bits per component @@ -49,34 +78,142 @@ var PDFImage = (function pdfImage() { } this.decode = dict.get('Decode', 'D'); + this.needsDecode = false; + if (this.decode && this.colorSpace && + !this.colorSpace.isDefaultDecode(this.decode)) { + this.needsDecode = true; + // Do some preprocessing to avoid more math. + var max = (1 << bitsPerComponent) - 1; + this.decodeCoefficients = []; + this.decodeAddends = []; + for (var i = 0, j = 0; i < this.decode.length; i += 2, ++j) { + var dmin = this.decode[i]; + var dmax = this.decode[i + 1]; + this.decodeCoefficients[j] = dmax - dmin; + this.decodeAddends[j] = max * dmin; + } + } var mask = xref.fetchIfRef(dict.get('Mask')); - var smask = xref.fetchIfRef(dict.get('SMask')); if (mask) { TODO('masked images'); } else if (smask) { - this.smask = new PDFImage(xref, res, smask); + this.smask = new PDFImage(xref, res, smask, false); } } + /** + * Handles processing of image data and calls the callback with an argument + * of a PDFImage when the image is ready to be used. + */ + PDFImage.buildImage = function buildImage(callback, handler, xref, res, + image, inline) { + var imageDataPromise = new Promise(); + var smaskPromise = new Promise(); + // The image data and smask data may not be ready yet, wait till both are + // resolved. + Promise.all([imageDataPromise, smaskPromise]).then(function(results) { + var imageData = results[0], smaskData = results[1]; + var image = new PDFImage(xref, res, imageData, inline, smaskData); + callback(image); + }); - constructor.prototype = { - getComponents: function getComponents(buffer, decodeMap) { + handleImageData(handler, xref, res, image, imageDataPromise); + + var smask = xref.fetchIfRef(image.dict.get('SMask')); + if (smask) + handleImageData(handler, xref, res, smask, smaskPromise); + else + smaskPromise.resolve(null); + }; + + /** + * Resize an image using the nearest neighbor algorithm. Currently only + * supports one and three component images. + * @param {TypedArray} pixels The original image with one component. + * @param {Number} bpc Number of bits per component. + * @param {Number} components Number of color components, 1 or 3 is supported. + * @param {Number} w1 Original width. + * @param {Number} h1 Original height. + * @param {Number} w2 New width. + * @param {Number} h2 New height. + * @return {TypedArray} Resized image data. + */ + PDFImage.resize = function resize(pixels, bpc, components, w1, h1, w2, h2) { + var length = w2 * h2 * components; + var temp = bpc <= 8 ? new Uint8Array(length) : + bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length); + var xRatio = w1 / w2; + var yRatio = h1 / h2; + var px, py, newIndex, oldIndex; + for (var i = 0; i < h2; i++) { + for (var j = 0; j < w2; j++) { + px = Math.floor(j * xRatio); + py = Math.floor(i * yRatio); + newIndex = (i * w2) + j; + oldIndex = ((py * w1) + px); + if (components === 1) { + temp[newIndex] = pixels[oldIndex]; + } else if (components === 3) { + newIndex *= 3; + oldIndex *= 3; + temp[newIndex] = pixels[oldIndex]; + temp[newIndex + 1] = pixels[oldIndex + 1]; + temp[newIndex + 2] = pixels[oldIndex + 2]; + } + } + } + return temp; + }; + + PDFImage.prototype = { + get drawWidth() { + if (!this.smask) + return this.width; + return Math.max(this.width, this.smask.width); + }, + get drawHeight() { + if (!this.smask) + return this.height; + return Math.max(this.height, this.smask.height); + }, + getComponents: function getComponents(buffer) { var bpc = this.bpc; - if (bpc == 8) + var needsDecode = this.needsDecode; + var decodeMap = this.decode; + + // This image doesn't require any extra work. + if (bpc == 8 && !needsDecode) return buffer; + var bufferLength = buffer.length; var width = this.width; var height = this.height; var numComps = this.numComps; - var length = width * height; + var length = width * height * numComps; var bufferPos = 0; var output = bpc <= 8 ? new Uint8Array(length) : bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length); var rowComps = width * numComps; + var decodeAddends, decodeCoefficients; + if (needsDecode) { + decodeAddends = this.decodeAddends; + decodeCoefficients = this.decodeCoefficients; + } + var max = (1 << bpc) - 1; - if (bpc == 1) { + if (bpc == 8) { + // Optimization for reading 8 bpc images that have a decode. + for (var i = 0, ii = length; i < ii; ++i) { + var compIndex = i % numComps; + var value = buffer[i]; + value = decodeAndClamp(value, decodeAddends[compIndex], + decodeCoefficients[compIndex], max); + output[i] = value; + } + } else if (bpc == 1) { + // Optimization for reading 1 bpc images. var valueZero = 0, valueOne = 1; if (decodeMap) { valueZero = decodeMap[0] ? 1 : 0; @@ -101,8 +238,7 @@ var PDFImage = (function pdfImage() { output[i] = !(buf & mask) ? valueZero : valueOne; } } else { - if (decodeMap != null) - TODO('interpolate component values'); + // The general case that handles all other bpc values. var bits = 0, buf = 0; for (var i = 0, ii = length; i < ii; ++i) { if (i % rowComps == 0) { @@ -116,41 +252,34 @@ var PDFImage = (function pdfImage() { } var remainingBits = bits - bpc; - output[i] = buf >> remainingBits; + var value = buf >> remainingBits; + if (needsDecode) { + var compIndex = i % numComps; + value = decodeAndClamp(value, decodeAddends[compIndex], + decodeCoefficients[compIndex], max); + } + output[i] = value; buf = buf & ((1 << remainingBits) - 1); bits = remainingBits; } } return output; }, - getOpacity: function getOpacity() { + getOpacity: function getOpacity(width, height) { var smask = this.smask; - var width = this.width; - var height = this.height; - var buf = new Uint8Array(width * height); + var originalWidth = this.width; + var originalHeight = this.height; + var buf; if (smask) { - if (smask.image.getImage) { - // smask is a DOM image - var tempCanvas = new ScratchCanvas(width, height); - var tempCtx = tempCanvas.getContext('2d'); - var domImage = smask.image.getImage(); - tempCtx.drawImage(domImage, 0, 0, domImage.width, domImage.height, - 0, 0, width, height); - var data = tempCtx.getImageData(0, 0, width, height).data; - for (var i = 0, j = 0, ii = width * height; i < ii; ++i, j += 4) - buf[i] = data[j]; // getting first component value - return buf; - } var sw = smask.width; var sh = smask.height; - if (sw != this.width || sh != this.height) - error('smask dimensions do not match image dimensions: ' + sw + - ' != ' + this.width + ', ' + sh + ' != ' + this.height); - + buf = new Uint8Array(sw * sh); smask.fillGrayBuffer(buf); - return buf; + if (sw != width || sh != height) + buf = PDFImage.resize(buf, smask.bps, 1, sw, sh, width, height); } else { + buf = new Uint8Array(width * height); for (var i = 0, ii = width * height; i < ii; ++i) buf[i] = 255; } @@ -159,8 +288,7 @@ var PDFImage = (function pdfImage() { applyStencilMask: function applyStencilMask(buffer, inverseDecode) { var width = this.width, height = this.height; var bitStrideLength = (width + 7) >> 3; - this.image.reset(); - var imgArray = this.image.getBytes(bitStrideLength * height); + var imgArray = this.getImageBytes(bitStrideLength * height); var imgArrayPos = 0; var i, j, mask, buf; // removing making non-masked pixels transparent @@ -180,21 +308,23 @@ var PDFImage = (function pdfImage() { } } }, - fillRgbaBuffer: function fillRgbaBuffer(buffer, decodeMap) { + fillRgbaBuffer: function fillRgbaBuffer(buffer, width, height) { var numComps = this.numComps; - var width = this.width; - var height = this.height; + var originalWidth = this.width; + var originalHeight = this.height; var bpc = this.bpc; // rows start at byte boundary; - var rowBytes = (width * numComps * bpc + 7) >> 3; - this.image.reset(); - var imgArray = this.image.getBytes(height * rowBytes); + var rowBytes = (originalWidth * numComps * bpc + 7) >> 3; + var imgArray = this.getImageBytes(originalHeight * rowBytes); var comps = this.colorSpace.getRgbBuffer( - this.getComponents(imgArray, decodeMap), bpc); + this.getComponents(imgArray), bpc); + if (originalWidth != width || originalHeight != height) + comps = PDFImage.resize(comps, this.bpc, 3, originalWidth, + originalHeight, width, height); var compsPos = 0; - var opacity = this.getOpacity(); + var opacity = this.getOpacity(width, height); var opacityPos = 0; var length = width * height * 4; @@ -216,42 +346,28 @@ var PDFImage = (function pdfImage() { // rows start at byte boundary; var rowBytes = (width * numComps * bpc + 7) >> 3; - this.image.reset(); - var imgArray = this.image.getBytes(height * rowBytes); + var imgArray = this.getImageBytes(height * rowBytes); var comps = this.getComponents(imgArray); var length = width * height; - + // we aren't using a colorspace so we need to scale the value + var scale = 255 / ((1 << bpc) - 1); for (var i = 0; i < length; ++i) - buffer[i] = comps[i]; + buffer[i] = (scale * comps[i]) | 0; + }, + getImageBytes: function getImageBytes(length) { + this.image.reset(); + return this.image.getBytes(length); } }; - return constructor; + return PDFImage; })(); -var JpegImageLoader = (function jpegImage() { - function JpegImageLoader(objId, imageData, objs) { - var src = 'data:image/jpeg;base64,' + window.btoa(imageData); - - var img = new Image(); - img.onload = (function jpegImageLoaderOnload() { - this.loaded = true; - - objs.resolve(objId, this); - - if (this.onLoad) - this.onLoad(); - }).bind(this); - img.src = src; - this.domImage = img; - } - - JpegImageLoader.prototype = { - getImage: function jpegImageLoaderGetImage() { - return this.domImage; - } - }; - - return JpegImageLoader; -})(); +function loadJpegStream(id, imageData, objs) { + var img = new Image(); + img.onload = (function jpegImageLoaderOnload() { + objs.resolve(id, img); + }); + img.src = 'data:image/jpeg;base64,' + window.btoa(imageData); +} diff --git a/src/metrics.js b/src/metrics.js index c21b4aed1..e64961aa7 100644 --- a/src/metrics.js +++ b/src/metrics.js @@ -3,6 +3,9 @@ 'use strict'; +// The Metrics object contains glyph widths (in glyph space units). +// As per PDF spec, for most fonts (Type 3 being an exception) a glyph +// space unit corresponds to 1/1000th of text space unit. var Metrics = { 'Courier': 600, 'Courier-Bold': 600, diff --git a/src/obj.js b/src/obj.js index 7aebb732a..a0c1fdc8a 100644 --- a/src/obj.js +++ b/src/obj.js @@ -3,34 +3,42 @@ 'use strict'; -var Name = (function nameName() { - function constructor(name) { +var Name = (function NameClosure() { + function Name(name) { this.name = name; } - constructor.prototype = { - }; + Name.prototype = {}; - return constructor; + return Name; })(); -var Cmd = (function cmdCmd() { - function constructor(cmd) { +var Cmd = (function CmdClosure() { + function Cmd(cmd) { this.cmd = cmd; } - constructor.prototype = { + Cmd.prototype = {}; + + var cmdCache = {}; + + Cmd.get = function cmdGet(cmd) { + var cmdValue = cmdCache[cmd]; + if (cmdValue) + return cmdValue; + + return cmdCache[cmd] = new Cmd(cmd); }; - return constructor; + return Cmd; })(); -var Dict = (function dictDict() { - function constructor() { +var Dict = (function DictClosure() { + function Dict() { this.map = Object.create(null); } - constructor.prototype = { + Dict.prototype = { get: function dictGet(key1, key2, key3) { var value; if (typeof (value = this.map[key1]) != 'undefined' || key1 in this.map || @@ -60,29 +68,28 @@ var Dict = (function dictDict() { } }; - return constructor; + return Dict; })(); -var Ref = (function refRef() { - function constructor(num, gen) { +var Ref = (function RefClosure() { + function Ref(num, gen) { this.num = num; this.gen = gen; } - constructor.prototype = { - }; + Ref.prototype = {}; - return constructor; + return Ref; })(); // The reference is identified by number and generation, // this structure stores only one instance of the reference. -var RefSet = (function refSet() { - function constructor() { +var RefSet = (function RefSetClosure() { + function RefSet() { this.dict = {}; } - constructor.prototype = { + RefSet.prototype = { has: function refSetHas(ref) { return !!this.dict['R' + ref.num + '.' + ref.gen]; }, @@ -92,18 +99,18 @@ var RefSet = (function refSet() { } }; - return constructor; + return RefSet; })(); -var Catalog = (function catalogCatalog() { - function constructor(xref) { +var Catalog = (function CatalogClosure() { + function Catalog(xref) { this.xref = xref; var obj = xref.getCatalogObj(); assertWellFormed(isDict(obj), 'catalog object is not a dictionary'); this.catDict = obj; } - constructor.prototype = { + Catalog.prototype = { get toplevelPagesDict() { var pagesObj = this.catDict.get('Pages'); assertWellFormed(isRef(pagesObj), 'invalid top-level pages reference'); @@ -253,16 +260,16 @@ var Catalog = (function catalogCatalog() { } }; - return constructor; + return Catalog; })(); -var XRef = (function xRefXRef() { - function constructor(stream, startXRef, mainXRefEntriesOffset) { +var XRef = (function XRefClosure() { + function XRef(stream, startXRef, mainXRefEntriesOffset) { this.stream = stream; this.entries = []; this.xrefstms = {}; var trailerDict = this.readXRef(startXRef); - + this.trailer = trailerDict; // prepare the XRef cache this.cache = []; @@ -278,7 +285,7 @@ var XRef = (function xRefXRef() { error('Invalid root reference'); } - constructor.prototype = { + XRef.prototype = { readXRefTable: function readXRefTable(parser) { var obj; while (true) { @@ -518,20 +525,29 @@ var XRef = (function xRefXRef() { readXRef: function readXref(startXRef) { var stream = this.stream; stream.pos = startXRef; - var parser = new Parser(new Lexer(stream), true); - var obj = parser.getObj(); - // parse an old-style xref table - if (isCmd(obj, 'xref')) - return this.readXRefTable(parser); - // parse an xref stream - if (isInt(obj)) { - if (!isInt(parser.getObj()) || - !isCmd(parser.getObj(), 'obj') || - !isStream(obj = parser.getObj())) { - error('Invalid XRef stream'); + + try { + var parser = new Parser(new Lexer(stream), true); + var obj = parser.getObj(); + + // parse an old-style xref table + if (isCmd(obj, 'xref')) + return this.readXRefTable(parser); + + // parse an xref stream + if (isInt(obj)) { + if (!isInt(parser.getObj()) || + !isCmd(parser.getObj(), 'obj') || + !isStream(obj = parser.getObj())) { + error('Invalid XRef stream'); + } + return this.readXRefStream(obj); } - return this.readXRefStream(obj); + } catch (e) { + log('Reading of the xref table/stream failed: ' + e); } + + warn('Indexing all PDF objects'); return this.indexObjects(); }, getEntry: function xRefGetEntry(i) { @@ -589,7 +605,7 @@ var XRef = (function xRefXRef() { e = parser.getObj(); } // Don't cache streams since they are mutable (except images). - if (!isStream(e) || e.getImage) + if (!isStream(e) || e instanceof JpegStream) this.cache[num] = e; return e; } @@ -633,7 +649,7 @@ var XRef = (function xRefXRef() { } }; - return constructor; + return XRef; })(); /** @@ -642,7 +658,7 @@ var XRef = (function xRefXRef() { * inside of a worker. The `PDFObjects` implements some basic functions to * manage these objects. */ -var PDFObjects = (function pdfObjects() { +var PDFObjects = (function PDFObjectsClosure() { function PDFObjects() { this.objs = {}; } diff --git a/src/parser.js b/src/parser.js index 93a3f21b5..e50b12b9b 100644 --- a/src/parser.js +++ b/src/parser.js @@ -9,8 +9,8 @@ function isEOF(v) { return v == EOF; } -var Parser = (function parserParser() { - function constructor(lexer, allowStreams, xref) { +var Parser = (function ParserClosure() { + function Parser(lexer, allowStreams, xref) { this.lexer = lexer; this.allowStreams = allowStreams; this.xref = xref; @@ -18,7 +18,7 @@ var Parser = (function parserParser() { this.refill(); } - constructor.prototype = { + Parser.prototype = { refill: function parserRefill() { this.buf1 = this.lexer.getObj(); this.buf2 = this.lexer.getObj(); @@ -157,7 +157,7 @@ var Parser = (function parserParser() { imageStream = this.filter(imageStream, dict, length); imageStream.parameters = dict; - this.buf2 = new Cmd('EI'); + this.buf2 = Cmd.get('EI'); this.shift(); return imageStream; @@ -225,7 +225,8 @@ var Parser = (function parserParser() { return new PredictorStream(new FlateStream(stream), params); } return new FlateStream(stream); - } else if (name == 'LZWDecode' || name == 'LZW') { + } + if (name == 'LZWDecode' || name == 'LZW') { var earlyChange = 1; if (params) { if (params.has('EarlyChange')) @@ -234,31 +235,34 @@ var Parser = (function parserParser() { new LZWStream(stream, earlyChange), params); } return new LZWStream(stream, earlyChange); - } else if (name == 'DCTDecode' || name == 'DCT') { + } + if (name == 'DCTDecode' || name == 'DCT') { var bytes = stream.getBytes(length); return new JpegStream(bytes, stream.dict, this.xref); - } else if (name == 'ASCII85Decode' || name == 'A85') { - return new Ascii85Stream(stream); - } else if (name == 'ASCIIHexDecode' || name == 'AHx') { - return new AsciiHexStream(stream); - } else if (name == 'CCITTFaxDecode' || name == 'CCF') { - return new CCITTFaxStream(stream, params); - } else { - TODO('filter "' + name + '" not supported yet'); } + if (name == 'ASCII85Decode' || name == 'A85') { + return new Ascii85Stream(stream); + } + if (name == 'ASCIIHexDecode' || name == 'AHx') { + return new AsciiHexStream(stream); + } + if (name == 'CCITTFaxDecode' || name == 'CCF') { + return new CCITTFaxStream(stream, params); + } + warn('filter "' + name + '" not supported yet'); return stream; } }; - return constructor; + return Parser; })(); -var Lexer = (function lexer() { - function constructor(stream) { +var Lexer = (function LexerClosure() { + function Lexer(stream) { this.stream = stream; } - constructor.isSpace = function lexerIsSpace(ch) { + Lexer.isSpace = function lexerIsSpace(ch) { return ch == ' ' || ch == '\t' || ch == '\x0d' || ch == '\x0a'; }; @@ -292,7 +296,7 @@ var Lexer = (function lexer() { return -1; } - constructor.prototype = { + Lexer.prototype = { getNumber: function lexerGetNumber(ch) { var floating = false; var str = ch; @@ -492,14 +496,14 @@ var Lexer = (function lexer() { // array punctuation case '[': case ']': - return new Cmd(ch); + return Cmd.get(ch); // hex string or dict punctuation case '<': ch = stream.lookChar(); if (ch == '<') { // dict punctuation stream.skip(); - return new Cmd('<<'); + return Cmd.get('<<'); } return this.getHexString(ch); // dict punctuation @@ -507,11 +511,11 @@ var Lexer = (function lexer() { ch = stream.lookChar(); if (ch == '>') { stream.skip(); - return new Cmd('>>'); + return Cmd.get('>>'); } case '{': case '}': - return new Cmd(ch); + return Cmd.get(ch); // fall through case ')': error('Illegal character: ' + ch); @@ -534,7 +538,7 @@ var Lexer = (function lexer() { return false; if (str == 'null') return null; - return new Cmd(str); + return Cmd.get(str); }, skipToNextLine: function lexerSkipToNextLine() { var stream = this.stream; @@ -554,11 +558,11 @@ var Lexer = (function lexer() { } }; - return constructor; + return Lexer; })(); -var Linearization = (function linearizationLinearization() { - function constructor(stream) { +var Linearization = (function LinearizationClosure() { + function Linearization(stream) { this.parser = new Parser(new Lexer(stream), false); var obj1 = this.parser.getObj(); var obj2 = this.parser.getObj(); @@ -572,7 +576,7 @@ var Linearization = (function linearizationLinearization() { } } - constructor.prototype = { + Linearization.prototype = { getInt: function linearizationGetInt(name) { var linDict = this.linDict; var obj; @@ -631,6 +635,6 @@ var Linearization = (function linearizationLinearization() { } }; - return constructor; + return Linearization; })(); diff --git a/src/pattern.js b/src/pattern.js index 72d13d896..dbe2e5c23 100644 --- a/src/pattern.js +++ b/src/pattern.js @@ -3,13 +3,18 @@ 'use strict'; -var Pattern = (function patternPattern() { +var PatternType = { + AXIAL: 2, + RADIAL: 3 +}; + +var Pattern = (function PatternClosure() { // Constructor should define this.getPattern - function constructor() { + function Pattern() { error('should not call Pattern constructor'); } - constructor.prototype = { + Pattern.prototype = { // Input: current Canvas context // Output: the appropriate fillStyle or strokeStyle getPattern: function pattern_getStyle(ctx) { @@ -17,34 +22,34 @@ var Pattern = (function patternPattern() { } }; - constructor.shadingFromIR = function pattern_shadingFromIR(ctx, raw) { + Pattern.shadingFromIR = function pattern_shadingFromIR(ctx, raw) { return Shadings[raw[0]].fromIR(ctx, raw); }; - constructor.parseShading = function pattern_shading(shading, matrix, xref, + Pattern.parseShading = function pattern_shading(shading, matrix, xref, res, ctx) { var dict = isStream(shading) ? shading.dict : shading; var type = dict.get('ShadingType'); switch (type) { - case 2: - case 3: - // both radial and axial shadings are handled by RadialAxial shading + case PatternType.AXIAL: + case PatternType.RADIAL: + // Both radial and axial shadings are handled by RadialAxial shading. return new Shadings.RadialAxial(dict, matrix, xref, res, ctx); default: return new Shadings.Dummy(); } }; - return constructor; + return Pattern; })(); var Shadings = {}; // Radial and axial shading have very similar implementations // If needed, the implementations can be broken into two classes -Shadings.RadialAxial = (function radialAxialShading() { - function constructor(dict, matrix, xref, res, ctx) { +Shadings.RadialAxial = (function RadialAxialClosure() { + function RadialAxial(dict, matrix, xref, res, ctx) { this.matrix = matrix; this.coordsArr = dict.get('Coords'); this.shadingType = dict.get('ShadingType'); @@ -97,7 +102,7 @@ Shadings.RadialAxial = (function radialAxialShading() { this.colorStops = colorStops; } - constructor.fromIR = function radialAxialShadingGetIR(ctx, raw) { + RadialAxial.fromIR = function radialAxialShadingGetIR(ctx, raw) { var type = raw[1]; var colorStops = raw[2]; var p0 = raw[3]; @@ -117,9 +122,9 @@ Shadings.RadialAxial = (function radialAxialShading() { } var grad; - if (type == 2) + if (type == PatternType.AXIAL) grad = ctx.createLinearGradient(p0[0], p0[1], p1[0], p1[1]); - else if (type == 3) + else if (type == PatternType.RADIAL) grad = ctx.createRadialGradient(p0[0], p0[1], r0, p1[0], p1[1], r1); for (var i = 0, ii = colorStops.length; i < ii; ++i) { @@ -129,16 +134,16 @@ Shadings.RadialAxial = (function radialAxialShading() { return grad; }; - constructor.prototype = { + RadialAxial.prototype = { getIR: function radialAxialShadingGetIR() { var coordsArr = this.coordsArr; var type = this.shadingType; - if (type == 2) { + if (type == PatternType.AXIAL) { var p0 = [coordsArr[0], coordsArr[1]]; var p1 = [coordsArr[2], coordsArr[3]]; var r0 = null; var r1 = null; - } else if (type == 3) { + } else if (type == PatternType.RADIAL) { var p0 = [coordsArr[0], coordsArr[1]]; var p1 = [coordsArr[3], coordsArr[4]]; var r0 = coordsArr[2]; @@ -157,28 +162,32 @@ Shadings.RadialAxial = (function radialAxialShading() { } }; - return constructor; + return RadialAxial; })(); -Shadings.Dummy = (function dummyShading() { - function constructor() { +Shadings.Dummy = (function DummyClosure() { + function Dummy() { this.type = 'Pattern'; } - constructor.fromIR = function dummyShadingFromIR() { + Dummy.fromIR = function dummyShadingFromIR() { return 'hotpink'; }; - constructor.prototype = { + Dummy.prototype = { getIR: function dummyShadingGetIR() { return ['Dummy']; } }; - return constructor; + return Dummy; })(); -var TilingPattern = (function tilingPattern() { - var PAINT_TYPE_COLORED = 1, PAINT_TYPE_UNCOLORED = 2; +var TilingPattern = (function TilingPatternClosure() { + var PaintType = { + COLORED: 1, + UNCOLORED: 2 + }; + var MAX_PATTERN_SIZE = 512; function TilingPattern(IR, color, ctx, objs) { var IRQueue = IR[2]; @@ -204,13 +213,13 @@ var TilingPattern = (function tilingPattern() { var width = botRight[0] - topLeft[0]; var height = botRight[1] - topLeft[1]; - // TODO: hack to avoid OOM, we would idealy compute the tiling + // TODO: hack to avoid OOM, we would ideally 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; + while (Math.abs(width) > MAX_PATTERN_SIZE || + Math.abs(height) > MAX_PATTERN_SIZE) { + width = height = MAX_PATTERN_SIZE; } var tmpCanvas = new ScratchCanvas(width, height); @@ -220,11 +229,11 @@ var TilingPattern = (function tilingPattern() { var graphics = new CanvasGraphics(tmpCtx, objs); switch (paintType) { - case PAINT_TYPE_COLORED: + case PaintType.COLORED: tmpCtx.fillStyle = ctx.fillStyle; tmpCtx.strokeStyle = ctx.strokeStyle; break; - case PAINT_TYPE_UNCOLORED: + case PaintType.UNCOLORED: color = Util.makeCssRgb.apply(this, color); tmpCtx.fillStyle = color; tmpCtx.strokeStyle = color; diff --git a/src/stream.js b/src/stream.js index 559fb2ca2..8d3f0f5bb 100644 --- a/src/stream.js +++ b/src/stream.js @@ -3,8 +3,8 @@ 'use strict'; -var Stream = (function streamStream() { - function constructor(arrayBuffer, start, length, dict) { +var Stream = (function StreamClosure() { + function Stream(arrayBuffer, start, length, dict) { this.bytes = new Uint8Array(arrayBuffer); this.start = start || 0; this.pos = this.start; @@ -14,7 +14,7 @@ var Stream = (function streamStream() { // required methods for a stream. if a particular stream does not // implement these, an error should be thrown - constructor.prototype = { + Stream.prototype = { get length() { return this.end - this.start; }, @@ -67,11 +67,11 @@ var Stream = (function streamStream() { isStream: true }; - return constructor; + return Stream; })(); -var StringStream = (function stringStream() { - function constructor(str) { +var StringStream = (function StringStreamClosure() { + function StringStream(str) { var length = str.length; var bytes = new Uint8Array(length); for (var n = 0; n < length; ++n) @@ -79,21 +79,21 @@ var StringStream = (function stringStream() { Stream.call(this, bytes); } - constructor.prototype = Stream.prototype; + StringStream.prototype = Stream.prototype; - return constructor; + return StringStream; })(); // super class for the decoding streams -var DecodeStream = (function decodeStream() { - function constructor() { +var DecodeStream = (function DecodeStreamClosure() { + function DecodeStream() { this.pos = 0; this.bufferLength = 0; this.eof = false; this.buffer = null; } - constructor.prototype = { + DecodeStream.prototype = { ensureBuffer: function decodestream_ensureBuffer(requested) { var buffer = this.buffer; var current = buffer ? buffer.byteLength : 0; @@ -178,24 +178,24 @@ var DecodeStream = (function decodeStream() { } }; - return constructor; + return DecodeStream; })(); -var FakeStream = (function fakeStream() { - function constructor(stream) { +var FakeStream = (function FakeStreamClosure() { + function FakeStream(stream) { this.dict = stream.dict; DecodeStream.call(this); } - constructor.prototype = Object.create(DecodeStream.prototype); - constructor.prototype.readBlock = function fakeStreamReadBlock() { + FakeStream.prototype = Object.create(DecodeStream.prototype); + FakeStream.prototype.readBlock = function fakeStreamReadBlock() { var bufferLength = this.bufferLength; bufferLength += 1024; var buffer = this.ensureBuffer(bufferLength); this.bufferLength = bufferLength; }; - constructor.prototype.getBytes = function fakeStreamGetBytes(length) { + FakeStream.prototype.getBytes = function fakeStreamGetBytes(length) { var end, pos = this.pos; if (length) { @@ -217,18 +217,20 @@ var FakeStream = (function fakeStream() { return this.buffer.subarray(pos, end); }; - return constructor; + return FakeStream; })(); -var StreamsSequenceStream = (function streamSequenceStream() { - function constructor(streams) { +var StreamsSequenceStream = (function StreamsSequenceStreamClosure() { + function StreamsSequenceStream(streams) { this.streams = streams; DecodeStream.call(this); } - constructor.prototype = Object.create(DecodeStream.prototype); + StreamsSequenceStream.prototype = Object.create(DecodeStream.prototype); + + StreamsSequenceStream.prototype.readBlock = + function streamSequenceStreamReadBlock() { - constructor.prototype.readBlock = function streamSequenceStreamReadBlock() { var streams = this.streams; if (streams.length == 0) { this.eof = true; @@ -243,10 +245,10 @@ var StreamsSequenceStream = (function streamSequenceStream() { this.bufferLength = newLength; }; - return constructor; + return StreamsSequenceStream; })(); -var FlateStream = (function flateStream() { +var FlateStream = (function FlateStreamClosure() { var codeLenCodeMap = new Uint32Array([ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ]); @@ -339,7 +341,7 @@ var FlateStream = (function flateStream() { 0x50003, 0x50013, 0x5000b, 0x5001b, 0x50007, 0x50017, 0x5000f, 0x00000 ]), 5]; - function constructor(stream) { + function FlateStream(stream) { var bytes = stream.getBytes(); var bytesPos = 0; @@ -364,9 +366,9 @@ var FlateStream = (function flateStream() { DecodeStream.call(this); } - constructor.prototype = Object.create(DecodeStream.prototype); + FlateStream.prototype = Object.create(DecodeStream.prototype); - constructor.prototype.getBits = function flateStreamGetBits(bits) { + FlateStream.prototype.getBits = function flateStreamGetBits(bits) { var codeSize = this.codeSize; var codeBuf = this.codeBuf; var bytes = this.bytes; @@ -386,7 +388,7 @@ var FlateStream = (function flateStream() { return b; }; - constructor.prototype.getCode = function flateStreamGetCode(table) { + FlateStream.prototype.getCode = function flateStreamGetCode(table) { var codes = table[0]; var maxLen = table[1]; var codeSize = this.codeSize; @@ -412,7 +414,7 @@ var FlateStream = (function flateStream() { return codeVal; }; - constructor.prototype.generateHuffmanTable = + FlateStream.prototype.generateHuffmanTable = function flateStreamGenerateHuffmanTable(lengths) { var n = lengths.length; @@ -451,7 +453,7 @@ var FlateStream = (function flateStream() { return [codes, maxLen]; }; - constructor.prototype.readBlock = function flateStreamReadBlock() { + FlateStream.prototype.readBlock = function flateStreamReadBlock() { // read block header var hdr = this.getBits(3); if (hdr & 1) @@ -582,11 +584,11 @@ var FlateStream = (function flateStream() { } }; - return constructor; + return FlateStream; })(); -var PredictorStream = (function predictorStream() { - function constructor(stream, params) { +var PredictorStream = (function PredictorStreamClosure() { + function PredictorStream(stream, params) { var predictor = this.predictor = params.get('Predictor') || 1; if (predictor <= 1) @@ -613,9 +615,9 @@ var PredictorStream = (function predictorStream() { return this; } - constructor.prototype = Object.create(DecodeStream.prototype); + PredictorStream.prototype = Object.create(DecodeStream.prototype); - constructor.prototype.readBlockTiff = + PredictorStream.prototype.readBlockTiff = function predictorStreamReadBlockTiff() { var rowBytes = this.rowBytes; @@ -676,7 +678,9 @@ var PredictorStream = (function predictorStream() { this.bufferLength += rowBytes; }; - constructor.prototype.readBlockPng = function predictorStreamReadBlockPng() { + PredictorStream.prototype.readBlockPng = + function predictorStreamReadBlockPng() { + var rowBytes = this.rowBytes; var pixBytes = this.pixBytes; @@ -753,7 +757,7 @@ var PredictorStream = (function predictorStream() { this.bufferLength += rowBytes; }; - return constructor; + return PredictorStream; })(); /** @@ -763,7 +767,7 @@ var PredictorStream = (function predictorStream() { * a library to decode these images and the stream behaves like all the other * DecodeStreams. */ -var JpegStream = (function jpegStream() { +var JpegStream = (function JpegStreamClosure() { function isAdobeImage(bytes) { var maxBytesScanned = Math.max(bytes.length - 16, 1024); // Looking for APP14, 'Adobe' @@ -794,7 +798,7 @@ var JpegStream = (function jpegStream() { return newBytes; } - function constructor(bytes, dict, xref) { + function JpegStream(bytes, dict, xref) { // TODO: per poppler, some images may have 'junk' before that // need to be removed this.dict = dict; @@ -825,9 +829,9 @@ var JpegStream = (function jpegStream() { DecodeStream.call(this); } - constructor.prototype = Object.create(DecodeStream.prototype); + JpegStream.prototype = Object.create(DecodeStream.prototype); - constructor.prototype.ensureBuffer = function jpegStreamEnsureBuffer(req) { + JpegStream.prototype.ensureBuffer = function jpegStreamEnsureBuffer(req) { if (this.bufferLength) return; var jpegImage = new JpegImage(); @@ -839,18 +843,18 @@ var JpegStream = (function jpegStream() { this.buffer = data; this.bufferLength = data.length; }; - constructor.prototype.getIR = function jpegStreamGetIR() { + JpegStream.prototype.getIR = function jpegStreamGetIR() { return this.src; }; - constructor.prototype.getChar = function jpegStreamGetChar() { + JpegStream.prototype.getChar = function jpegStreamGetChar() { error('internal error: getChar is not valid on JpegStream'); }; - return constructor; + return JpegStream; })(); -var DecryptStream = (function decryptStream() { - function constructor(str, decrypt) { +var DecryptStream = (function DecryptStreamClosure() { + function DecryptStream(str, decrypt) { this.str = str; this.dict = str.dict; this.decrypt = decrypt; @@ -860,9 +864,9 @@ var DecryptStream = (function decryptStream() { var chunkSize = 512; - constructor.prototype = Object.create(DecodeStream.prototype); + DecryptStream.prototype = Object.create(DecodeStream.prototype); - constructor.prototype.readBlock = function decryptStreamReadBlock() { + DecryptStream.prototype.readBlock = function decryptStreamReadBlock() { var chunk = this.str.getBytes(chunkSize); if (!chunk || chunk.length == 0) { this.eof = true; @@ -879,11 +883,11 @@ var DecryptStream = (function decryptStream() { this.bufferLength = bufferLength; }; - return constructor; + return DecryptStream; })(); -var Ascii85Stream = (function ascii85Stream() { - function constructor(str) { +var Ascii85Stream = (function Ascii85StreamClosure() { + function Ascii85Stream(str) { this.str = str; this.dict = str.dict; this.input = new Uint8Array(5); @@ -891,9 +895,9 @@ var Ascii85Stream = (function ascii85Stream() { DecodeStream.call(this); } - constructor.prototype = Object.create(DecodeStream.prototype); + Ascii85Stream.prototype = Object.create(DecodeStream.prototype); - constructor.prototype.readBlock = function ascii85StreamReadBlock() { + Ascii85Stream.prototype.readBlock = function ascii85StreamReadBlock() { var tildaCode = '~'.charCodeAt(0); var zCode = 'z'.charCodeAt(0); var str = this.str; @@ -948,11 +952,11 @@ var Ascii85Stream = (function ascii85Stream() { } }; - return constructor; + return Ascii85Stream; })(); -var AsciiHexStream = (function asciiHexStream() { - function constructor(str) { +var AsciiHexStream = (function AsciiHexStreamClosure() { + function AsciiHexStream(str) { this.str = str; this.dict = str.dict; @@ -986,9 +990,9 @@ var AsciiHexStream = (function asciiHexStream() { 102: 15 }; - constructor.prototype = Object.create(DecodeStream.prototype); + AsciiHexStream.prototype = Object.create(DecodeStream.prototype); - constructor.prototype.readBlock = function asciiHexStreamReadBlock() { + AsciiHexStream.prototype.readBlock = function asciiHexStreamReadBlock() { var gtCode = '>'.charCodeAt(0), bytes = this.str.getBytes(), c, n, decodeLength, buffer, bufferLength, i, length; @@ -1018,10 +1022,10 @@ var AsciiHexStream = (function asciiHexStream() { this.eof = true; }; - return constructor; + return AsciiHexStream; })(); -var CCITTFaxStream = (function ccittFaxStream() { +var CCITTFaxStream = (function CCITTFaxStreamClosure() { var ccittEOL = -2; var twoDimPass = 0; @@ -1449,7 +1453,7 @@ var CCITTFaxStream = (function ccittFaxStream() { [2, 2], [2, 2], [2, 2], [2, 2] ]; - function constructor(str, params) { + function CCITTFaxStream(str, params) { this.str = str; this.dict = str.dict; @@ -1494,9 +1498,9 @@ var CCITTFaxStream = (function ccittFaxStream() { DecodeStream.call(this); } - constructor.prototype = Object.create(DecodeStream.prototype); + CCITTFaxStream.prototype = Object.create(DecodeStream.prototype); - constructor.prototype.readBlock = function ccittFaxStreamReadBlock() { + CCITTFaxStream.prototype.readBlock = function ccittFaxStreamReadBlock() { while (!this.eof) { var c = this.lookChar(); this.buf = EOF; @@ -1505,7 +1509,7 @@ var CCITTFaxStream = (function ccittFaxStream() { } }; - constructor.prototype.addPixels = + CCITTFaxStream.prototype.addPixels = function ccittFaxStreamAddPixels(a1, blackPixels) { var codingLine = this.codingLine; var codingPos = this.codingPos; @@ -1525,7 +1529,7 @@ var CCITTFaxStream = (function ccittFaxStream() { this.codingPos = codingPos; }; - constructor.prototype.addPixelsNeg = + CCITTFaxStream.prototype.addPixelsNeg = function ccittFaxStreamAddPixelsNeg(a1, blackPixels) { var codingLine = this.codingLine; var codingPos = this.codingPos; @@ -1554,7 +1558,7 @@ var CCITTFaxStream = (function ccittFaxStream() { this.codingPos = codingPos; }; - constructor.prototype.lookChar = function ccittFaxStreamLookChar() { + CCITTFaxStream.prototype.lookChar = function ccittFaxStreamLookChar() { if (this.buf != EOF) return this.buf; @@ -1852,10 +1856,10 @@ var CCITTFaxStream = (function ccittFaxStream() { // values. The first array element indicates whether a valid code is being // returned. The second array element is the actual code. The third array // element indicates whether EOF was reached. - var findTableCode = function ccittFaxStreamFindTableCode(start, end, table, - limit) { - var limitValue = limit || 0; + CCITTFaxStream.prototype.findTableCode = + function ccittFaxStreamFindTableCode(start, end, table, limit) { + var limitValue = limit || 0; for (var i = start; i <= end; ++i) { var code = this.lookBits(i); if (code == EOF) @@ -1873,7 +1877,9 @@ var CCITTFaxStream = (function ccittFaxStream() { return [false, 0, false]; }; - constructor.prototype.getTwoDimCode = function ccittFaxStreamGetTwoDimCode() { + CCITTFaxStream.prototype.getTwoDimCode = + function ccittFaxStreamGetTwoDimCode() { + var code = 0; var p; if (this.eoblock) { @@ -1884,7 +1890,7 @@ var CCITTFaxStream = (function ccittFaxStream() { return p[1]; } } else { - var result = findTableCode(1, 7, twoDimTable); + var result = this.findTableCode(1, 7, twoDimTable); if (result[0] && result[2]) return result[1]; } @@ -1892,7 +1898,9 @@ var CCITTFaxStream = (function ccittFaxStream() { return EOF; }; - constructor.prototype.getWhiteCode = function ccittFaxStreamGetWhiteCode() { + CCITTFaxStream.prototype.getWhiteCode = + function ccittFaxStreamGetWhiteCode() { + var code = 0; var p; var n; @@ -1911,11 +1919,11 @@ var CCITTFaxStream = (function ccittFaxStream() { return p[1]; } } else { - var result = findTableCode(1, 9, whiteTable2); + var result = this.findTableCode(1, 9, whiteTable2); if (result[0]) return result[1]; - result = findTableCode(11, 12, whiteTable1); + result = this.findTableCode(11, 12, whiteTable1); if (result[0]) return result[1]; } @@ -1924,7 +1932,9 @@ var CCITTFaxStream = (function ccittFaxStream() { return 1; }; - constructor.prototype.getBlackCode = function ccittFaxStreamGetBlackCode() { + CCITTFaxStream.prototype.getBlackCode = + function ccittFaxStreamGetBlackCode() { + var code, p; if (this.eoblock) { code = this.lookBits(13); @@ -1942,15 +1952,15 @@ var CCITTFaxStream = (function ccittFaxStream() { return p[1]; } } else { - var result = findTableCode(2, 6, blackTable3); + var result = this.findTableCode(2, 6, blackTable3); if (result[0]) return result[1]; - result = findTableCode(7, 12, blackTable2, 64); + result = this.findTableCode(7, 12, blackTable2, 64); if (result[0]) return result[1]; - result = findTableCode(10, 13, blackTable1); + result = this.findTableCode(10, 13, blackTable1); if (result[0]) return result[1]; } @@ -1959,7 +1969,7 @@ var CCITTFaxStream = (function ccittFaxStream() { return 1; }; - constructor.prototype.lookBits = function ccittFaxStreamLookBits(n) { + CCITTFaxStream.prototype.lookBits = function ccittFaxStreamLookBits(n) { var c; while (this.inputBits < n) { if ((c = this.str.getByte()) == null) { @@ -1974,16 +1984,16 @@ var CCITTFaxStream = (function ccittFaxStream() { return (this.inputBuf >> (this.inputBits - n)) & (0xFFFF >> (16 - n)); }; - constructor.prototype.eatBits = function ccittFaxStreamEatBits(n) { + CCITTFaxStream.prototype.eatBits = function ccittFaxStreamEatBits(n) { if ((this.inputBits -= n) < 0) this.inputBits = 0; }; - return constructor; + return CCITTFaxStream; })(); -var LZWStream = (function lzwStream() { - function constructor(str, earlyChange) { +var LZWStream = (function LZWStreamClosure() { + function LZWStream(str, earlyChange) { this.str = str; this.dict = str.dict; this.cachedData = 0; @@ -2009,9 +2019,9 @@ var LZWStream = (function lzwStream() { DecodeStream.call(this); } - constructor.prototype = Object.create(DecodeStream.prototype); + LZWStream.prototype = Object.create(DecodeStream.prototype); - constructor.prototype.readBits = function lzwStreamReadBits(n) { + LZWStream.prototype.readBits = function lzwStreamReadBits(n) { var bitsCached = this.bitsCached; var cachedData = this.cachedData; while (bitsCached < n) { @@ -2029,7 +2039,7 @@ var LZWStream = (function lzwStream() { return (cachedData >>> bitsCached) & ((1 << n) - 1); }; - constructor.prototype.readBlock = function lzwStreamReadBlock() { + LZWStream.prototype.readBlock = function lzwStreamReadBlock() { var blockSize = 512; var estimatedDecodedSize = blockSize * 2, decodedSizeDelta = blockSize; var i, j, q; @@ -2108,6 +2118,6 @@ var LZWStream = (function lzwStream() { this.bufferLength = currentBufferLength; }; - return constructor; + return LZWStream; })(); diff --git a/src/util.js b/src/util.js index 4fb96f062..99b422296 100644 --- a/src/util.js +++ b/src/util.js @@ -76,24 +76,24 @@ function stringToBytes(str) { var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0]; -var Util = (function utilUtil() { - function constructor() {} - constructor.makeCssRgb = function makergb(r, g, b) { +var Util = (function UtilClosure() { + function Util() {} + Util.makeCssRgb = function makergb(r, g, b) { var ri = (255 * r) | 0, gi = (255 * g) | 0, bi = (255 * b) | 0; return 'rgb(' + ri + ',' + gi + ',' + bi + ')'; }; - constructor.makeCssCmyk = function makecmyk(c, m, y, k) { + Util.makeCssCmyk = function makecmyk(c, m, y, k) { c = (new DeviceCmykCS()).getRgb([c, m, y, k]); var ri = (255 * c[0]) | 0, gi = (255 * c[1]) | 0, bi = (255 * c[2]) | 0; return 'rgb(' + ri + ',' + gi + ',' + bi + ')'; }; - constructor.applyTransform = function apply(p, m) { + Util.applyTransform = function apply(p, m) { var xt = p[0] * m[0] + p[1] * m[2] + m[4]; var yt = p[0] * m[1] + p[1] * m[3] + m[5]; return [xt, yt]; }; - return constructor; + return Util; })(); var PDFStringTranslateTable = [ @@ -197,7 +197,7 @@ function isPDFFunction(v) { * can be set. If any of these happens twice or the data is required before * it was set, an exception is throw. */ -var Promise = (function promise() { +var Promise = (function PromiseClosure() { var EMPTY_PROMISE = {}; /** @@ -206,6 +206,8 @@ var Promise = (function promise() { */ function Promise(name, data) { this.name = name; + this.isRejected = false; + this.error = null; // If you build a promise and pass in some data it's already resolved. if (data != null) { this.isResolved = true; @@ -216,8 +218,35 @@ var Promise = (function promise() { this._data = EMPTY_PROMISE; } this.callbacks = []; + this.errbacks = []; + }; + /** + * Builds a promise that is resolved when all the passed in promises are + * resolved. + * @param {Promise[]} promises Array of promises to wait for. + * @return {Promise} New dependant promise. + */ + Promise.all = function(promises) { + var deferred = new Promise(); + var unresolved = promises.length; + var results = []; + if (unresolved === 0) { + deferred.resolve(results); + return deferred; + } + for (var i = 0; i < unresolved; ++i) { + var promise = promises[i]; + promise.then((function(i) { + return function(value) { + results[i] = value; + unresolved--; + if (unresolved === 0) + deferred.resolve(results); + }; + })(i)); + } + return deferred; }; - Promise.prototype = { hasData: false, @@ -256,9 +285,12 @@ var Promise = (function promise() { if (this.isResolved) { throw 'A Promise can be resolved only once ' + this.name; } + if (this.isRejected) { + throw 'The Promise was already rejected ' + this.name; + } this.isResolved = true; - this.data = data; + this.data = data || null; var callbacks = this.callbacks; for (var i = 0, ii = callbacks.length; i < ii; i++) { @@ -266,7 +298,24 @@ var Promise = (function promise() { } }, - then: function promiseThen(callback) { + reject: function proimseReject(reason) { + if (this.isRejected) { + throw 'A Promise can be rejected only once ' + this.name; + } + if (this.isResolved) { + throw 'The Promise was already resolved ' + this.name; + } + + this.isRejected = true; + this.error = reason || null; + var errbacks = this.errbacks; + + for (var i = 0, ii = errbacks.length; i < ii; i++) { + errbacks[i].call(null, reason); + } + }, + + then: function promiseThen(callback, errback) { if (!callback) { throw 'Requiring callback' + this.name; } @@ -275,8 +324,13 @@ var Promise = (function promise() { if (this.isResolved) { var data = this.data; callback.call(null, data); + } else if (this.isRejected && errorback) { + var error = this.error; + errback.call(null, error); } else { this.callbacks.push(callback); + if (errback) + this.errbacks.push(errback); } } }; diff --git a/src/worker.js b/src/worker.js index 67f1bf658..c18de65ad 100644 --- a/src/worker.js +++ b/src/worker.js @@ -6,6 +6,8 @@ function MessageHandler(name, comObj) { this.name = name; this.comObj = comObj; + this.callbackIndex = 1; + var callbacks = this.callbacks = {}; var ah = this.actionHandler = {}; ah['console_log'] = [function ahConsoleLog(data) { @@ -17,9 +19,30 @@ function MessageHandler(name, comObj) { comObj.onmessage = function messageHandlerComObjOnMessage(event) { var data = event.data; - if (data.action in ah) { + if (data.isReply) { + var callbackId = data.callbackId; + if (data.callbackId in callbacks) { + var callback = callbacks[callbackId]; + delete callbacks[callbackId]; + callback(data.data); + } else { + throw 'Cannot resolve callback ' + callbackId; + } + } else if (data.action in ah) { var action = ah[data.action]; - action[0].call(action[1], data.data); + if (data.callbackId) { + var promise = new Promise(); + promise.then(function(resolvedData) { + comObj.postMessage({ + isReply: true, + callbackId: data.callbackId, + data: resolvedData + }); + }); + action[0].call(action[1], data.data, promise); + } else { + action[0].call(action[1], data.data); + } } else { throw 'Unkown action from worker: ' + data.action; } @@ -34,12 +57,23 @@ MessageHandler.prototype = { } ah[actionName] = [handler, scope]; }, - - send: function messageHandlerSend(actionName, data) { - this.comObj.postMessage({ + /** + * Sends a message to the comObj to invoke the action with the supplied data. + * @param {String} actionName Action to call. + * @param {JSON} data JSON data to send. + * @param {function} [callback] Optional callback that will handle a reply. + */ + send: function messageHandlerSend(actionName, data, callback) { + var message = { action: actionName, data: data - }); + }; + if (callback) { + var callbackId = this.callbackIndex++; + this.callbacks[callbackId] = callback; + message.callbackId = callbackId; + } + this.comObj.postMessage(message); } }; @@ -67,7 +101,6 @@ var WorkerMessageHandler = { handler.on('page_request', function wphSetupPageRequest(pageNum) { pageNum = parseInt(pageNum); - var page = pdfDoc.getPage(pageNum); // The following code does quite the same as // Page.prototype.startRendering, but stops at one point and sends the @@ -77,9 +110,23 @@ var WorkerMessageHandler = { var start = Date.now(); var dependency = []; - - // Pre compile the pdf page and fetch the fonts/images. - var IRQueue = page.getIRQueue(handler, dependency); + var IRQueue = null; + try { + var page = pdfDoc.getPage(pageNum); + // Pre compile the pdf page and fetch the fonts/images. + IRQueue = page.getIRQueue(handler, dependency); + } catch (e) { + // Turn the error into an obj that can be serialized + e = { + message: typeof e === 'object' ? e.message : e, + stack: typeof e === 'object' ? e.stack : null + }; + handler.send('page_error', { + pageNum: pageNum, + error: e + }); + return; + } console.log('page=%d - getIRQueue: time=%dms, len=%d', pageNum, Date.now() - start, IRQueue.fnArray.length); diff --git a/test/driver.js b/test/driver.js index c11cecf56..85d25658a 100644 --- a/test/driver.js +++ b/test/driver.js @@ -139,6 +139,11 @@ function nextPage(task, loadError) { if (task.skipPages && task.skipPages.indexOf(task.pageNum) >= 0) { log(' skipping page ' + task.pageNum + '/' + task.pdfDoc.numPages + '... '); + // empty the canvas + canvas.width = 1; + canvas.height = 1; + clear(canvas.getContext('2d')); + snapshotCurrentPage(task, ''); return; } @@ -160,12 +165,24 @@ function nextPage(task, loadError) { canvas.height = pageHeight * pdfToCssUnitsCoef; clear(ctx); + // using the text layer builder that does nothing to test + // text layer creation operations + var textLayerBuilder = { + beginLayout: function nullTextLayerBuilderBeginLayout() {}, + endLayout: function nullTextLayerBuilderEndLayout() {}, + appendText: function nullTextLayerBuilderAppendText(text, fontName, + fontSize) {} + }; + page.startRendering( ctx, - function nextPageStartRendering(e) { - snapshotCurrentPage(task, (!failure && e) ? - ('render : ' + e) : failure); - } + function nextPageStartRendering(error) { + var failureMessage = false; + if (error) + failureMessage = 'render : ' + error.message; + snapshotCurrentPage(task, failureMessage); + }, + textLayerBuilder ); } catch (e) { failure = 'page setup : ' + e.toString(); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index a757acf34..956980782 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -16,3 +16,9 @@ !alphatrans.pdf !devicen.pdf !cmykjpeg.pdf +!issue840.pdf +!scan-bad.pdf +!freeculture.pdf +!issue918.pdf +!smaskdim.pdf +!type4psfunc.pdf diff --git a/test/pdfs/aboutstacks.pdf.link b/test/pdfs/aboutstacks.pdf.link new file mode 100644 index 000000000..8b04ec042 --- /dev/null +++ b/test/pdfs/aboutstacks.pdf.link @@ -0,0 +1 @@ +http://greenhousechallenge.org/media/item/313/38/About-Stacks.pdf diff --git a/test/pdfs/bpl13210.pdf.link b/test/pdfs/bpl13210.pdf.link new file mode 100644 index 000000000..7cde56a22 --- /dev/null +++ b/test/pdfs/bpl13210.pdf.link @@ -0,0 +1 @@ +http://h20000.www2.hp.com/bc/docs/support/SupportManual/bpl13210/bpl13210.pdf diff --git a/test/pdfs/freeculture.pdf b/test/pdfs/freeculture.pdf new file mode 100644 index 000000000..8b27e9355 Binary files /dev/null and b/test/pdfs/freeculture.pdf differ diff --git a/test/pdfs/geothermal.pdf.link b/test/pdfs/geothermal.pdf.link new file mode 100644 index 000000000..6a255647f --- /dev/null +++ b/test/pdfs/geothermal.pdf.link @@ -0,0 +1 @@ +http://geothermal.inel.gov/publications/future_of_geothermal_energy.pdf diff --git a/test/pdfs/issue1001.pdf.link b/test/pdfs/issue1001.pdf.link new file mode 100644 index 000000000..24e1bebc2 --- /dev/null +++ b/test/pdfs/issue1001.pdf.link @@ -0,0 +1 @@ +http://www.myhillsapartment.com/island_club/floorplans/images/links/Island_IC_brochure.pdf diff --git a/test/pdfs/issue1015.pdf.link b/test/pdfs/issue1015.pdf.link new file mode 100644 index 000000000..0878ab443 --- /dev/null +++ b/test/pdfs/issue1015.pdf.link @@ -0,0 +1 @@ +http://faculty.washington.edu/fidelr/RayaPubs/TheCaseStudyMethod.pdf diff --git a/test/pdfs/issue840.pdf b/test/pdfs/issue840.pdf new file mode 100644 index 000000000..6501a8c95 Binary files /dev/null and b/test/pdfs/issue840.pdf differ diff --git a/test/pdfs/issue918.pdf b/test/pdfs/issue918.pdf new file mode 100644 index 000000000..ac1a9c37f Binary files /dev/null and b/test/pdfs/issue918.pdf differ diff --git a/test/pdfs/issue919.pdf.link b/test/pdfs/issue919.pdf.link new file mode 100644 index 000000000..683001139 --- /dev/null +++ b/test/pdfs/issue919.pdf.link @@ -0,0 +1 @@ +http://agb.traviangames.com/Travian_AR_Terms.pdf diff --git a/test/pdfs/lista_preliminar.pdf.link b/test/pdfs/lista_preliminar.pdf.link new file mode 100644 index 000000000..54102b3b1 --- /dev/null +++ b/test/pdfs/lista_preliminar.pdf.link @@ -0,0 +1 @@ +http://www.lfg.com.br/concursodebolsas/lista_preliminar_classificao.pdf diff --git a/test/pdfs/ocs.pdf.link b/test/pdfs/ocs.pdf.link new file mode 100644 index 000000000..10c2b1b9e --- /dev/null +++ b/test/pdfs/ocs.pdf.link @@ -0,0 +1 @@ +http://www.unibuc.ro/uploads_en/29535/10/Cyrillic_Alphabets-Chars.pdf diff --git a/test/pdfs/piperine.pdf.link b/test/pdfs/piperine.pdf.link new file mode 100644 index 000000000..0d38690ee --- /dev/null +++ b/test/pdfs/piperine.pdf.link @@ -0,0 +1 @@ +http://www.erowid.org/archive/rhodium/chemistry/3base/piperonal.pepper/piperine.pepper/465e03piperine.pdf diff --git a/test/pdfs/protectip.pdf.link b/test/pdfs/protectip.pdf.link new file mode 100644 index 000000000..1af1bd87b --- /dev/null +++ b/test/pdfs/protectip.pdf.link @@ -0,0 +1 @@ +http://leahy.senate.gov/imo/media/doc/BillText-PROTECTIPAct.pdf diff --git a/test/pdfs/scan-bad.pdf b/test/pdfs/scan-bad.pdf new file mode 100755 index 000000000..ca09315f9 Binary files /dev/null and b/test/pdfs/scan-bad.pdf differ diff --git a/test/pdfs/smaskdim.pdf b/test/pdfs/smaskdim.pdf new file mode 100755 index 000000000..1ca92c3ac Binary files /dev/null and b/test/pdfs/smaskdim.pdf differ diff --git a/test/pdfs/tutorial.pdf.link b/test/pdfs/tutorial.pdf.link new file mode 100644 index 000000000..ec8141ce7 --- /dev/null +++ b/test/pdfs/tutorial.pdf.link @@ -0,0 +1 @@ +http://cplusplus.com/files/tutorial.pdf diff --git a/test/pdfs/type4psfunc.pdf b/test/pdfs/type4psfunc.pdf new file mode 100755 index 000000000..e4886e918 Binary files /dev/null and b/test/pdfs/type4psfunc.pdf differ diff --git a/test/test.py b/test/test.py index 256200587..888bd9ce8 100644 --- a/test/test.py +++ b/test/test.py @@ -12,6 +12,7 @@ DOC_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__),"..")) ANAL = True DEFAULT_MANIFEST_FILE = 'test_manifest.json' EQLOG_FILE = 'eq.log' +BROWSERLOG_FILE = 'browser.log' REFDIR = 'ref' TMPDIR = 'tmp' VERBOSE = False @@ -229,6 +230,7 @@ class BaseBrowserCommand(object): def setup(self): self.tempDir = tempfile.mkdtemp() self.profileDir = os.path.join(self.tempDir, "profile") + self.browserLog = open(BROWSERLOG_FILE, "w") def teardown(self): # If the browser is still running, wait up to ten seconds for it to quit @@ -245,6 +247,8 @@ class BaseBrowserCommand(object): if self.tempDir is not None and os.path.exists(self.tempDir): shutil.rmtree(self.tempDir) + self.browserLog.close() + def start(self, url): raise Exception("Can't start BaseBrowserCommand") @@ -262,7 +266,7 @@ class FirefoxBrowserCommand(BaseBrowserCommand): if platform.system() == "Darwin": cmds.append("-foreground") cmds.extend(["-no-remote", "-profile", self.profileDir, url]) - self.process = subprocess.Popen(cmds) + self.process = subprocess.Popen(cmds, stdout = self.browserLog, stderr = self.browserLog) class ChromeBrowserCommand(BaseBrowserCommand): def _fixupMacPath(self): @@ -272,7 +276,7 @@ class ChromeBrowserCommand(BaseBrowserCommand): cmds = [self.path] cmds.extend(["--user-data-dir=%s" % self.profileDir, "--no-first-run", "--disable-sync", url]) - self.process = subprocess.Popen(cmds) + self.process = subprocess.Popen(cmds, stdout = self.browserLog, stderr = self.browserLog) def makeBrowserCommand(browser): path = browser["path"].lower() diff --git a/test/test_manifest.json b/test/test_manifest.json index 87af30659..7954aa094 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -17,13 +17,13 @@ "rounds": 1, "type": "load" }, - { "id": "intelisa-load", + { "id": "intelisa-eq", "file": "pdfs/intelisa.pdf", "md5": "f5712097d29287a97f1278839814f682", - "md5": "f3ed5487d1afa34d8b77c0c734a95c79", "link": true, + "pageLimit": 100, "rounds": 1, - "type": "load" + "type": "eq" }, { "id": "pdfspec-load", "file": "pdfs/pdf.pdf", @@ -88,6 +88,13 @@ "rounds": 1, "type": "eq" }, + { "id": "freeculture", + "file": "pdfs/freeculture.pdf", + "md5": "dcdf3a8268e6a18938a42d5149efcfca", + "rounds": 1, + "pageLimit": 5, + "type": "eq" + }, { "id": "wnv_chinese-pdf", "file": "pdfs/wnv_chinese.pdf", "md5": "db682638e68391125e8982d3c984841e", @@ -221,6 +228,12 @@ "rounds": 1, "type": "load" }, + { "id": "scan-bad", + "file": "pdfs/scan-bad.pdf", + "md5": "4cf988f01ab83f61aca57f406dfd6584", + "rounds": 1, + "type": "load" + }, { "id": "ibwa-bad", "file": "pdfs/ibwa-bad.pdf", "md5": "6ca059d32b74ac2688ae06f727fee755", @@ -276,5 +289,111 @@ "link": false, "rounds": 1, "type": "eq" + }, + { "id": "protectip", + "file": "pdfs/protectip.pdf", + "md5": "676e7a7b8f96d04825361832b1838a93", + "link": true, + "rounds": 1, + "type": "eq" + }, + { "id": "piperine", + "file": "pdfs/piperine.pdf", + "md5": "603ca43dc5732dbba1579f122958c0c2", + "link": true, + "rounds": 1, + "type": "eq" + }, + { "id": "issue840", + "file": "pdfs/issue840.pdf", + "md5": "20d88011dd7e3c4fb5274979094dab93", + "rounds": 1, + "type": "eq" + }, + { "id": "bpl13210", + "file": "pdfs/bpl13210.pdf", + "md5": "8a08512baa9fa95378d9ad4b995947c7", + "link": true, + "pageLimit": 5, + "rounds": 1, + "type": "eq" + }, + { "id": "tutorial", + "file": "pdfs/tutorial.pdf", + "md5": "6e122f618c27f3aa9a689423e3be6b8d", + "link": true, + "rounds": 1, + "type": "eq" + }, + { "id": "geothermal.pdf", + "file": "pdfs/geothermal.pdf", + "md5": "ecffc0ce38ffdf1e90dc952f186e9a91", + "rounds": 1, + "link": true, + "pageLimit": 5, + "skipPages": [1], + "type": "eq" + }, + { "id": "lista_preliminar", + "file": "pdfs/lista_preliminar.pdf", + "md5": "4eff251319eeb660ba8a7a5cfac7787d", + "rounds": 1, + "link": true, + "pageLimit": 3, + "type": "eq" + }, + { "id": "issue919", + "file": "pdfs/issue919.pdf", + "md5": "3a1716a512aca4d7a8d6106bd4885d14", + "rounds": 1, + "link": true, + "pageLimit": 3, + "type": "eq" + }, + { "id": "issue918", + "file": "pdfs/issue918.pdf", + "md5": "d582cc0f2592ae82936589ced2a47e55", + "rounds": 1, + "type": "eq" + }, + { "id": "issue1001", + "file": "pdfs/issue1001.pdf", + "md5": "0f1496e80a82a923e91d9e74c55ad94e", + "rounds": 1, + "link": true, + "type": "eq" + }, + { "id": "aboutstacks", + "file": "pdfs/aboutstacks.pdf", + "md5": "6e7c8416a293ba2d83bc8dd20c6ccf51", + "rounds": 1, + "link": true, + "type": "eq" + }, + { "id": "smaskdim", + "file": "pdfs/smaskdim.pdf", + "md5": "de80aeca7cbf79940189fd34d59671ee", + "rounds": 1, + "type": "eq" + }, + { "id": "type4psfunc", + "file": "pdfs/type4psfunc.pdf", + "md5": "7e6027a02ff78577f74dccdf84e37189", + "rounds": 1, + "type": "eq" + }, + { "id": "ocs", + "file": "pdfs/ocs.pdf", + "md5": "2ade57e954ae7632749cf328daeaa7a8", + "rounds": 1, + "link": true, + "type": "load" + }, + { "id": "issue1015", + "file": "pdfs/issue1015.pdf", + "md5": "b61503d1b445742b665212866afb60e2", + "rounds": 1, + "link": true, + "type": "eq" } ] diff --git a/test/unit/function_spec.js b/test/unit/function_spec.js new file mode 100644 index 000000000..2a1dc0b75 --- /dev/null +++ b/test/unit/function_spec.js @@ -0,0 +1,225 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +'use strict'; + +describe('function', function() { + beforeEach(function() { + this.addMatchers({ + toMatchArray: function(expected) { + var actual = this.actual; + if (actual.length != expected.length) + return false; + for (var i = 0; i < expected.length; i++) { + var a = actual[i], b = expected[i]; + if (isArray(b)) { + if (a.length != b.length) + return false; + for (var j = 0; j < a.length; j++) { + var suba = a[j], subb = b[j]; + if (suba !== subb) + return false; + } + } else { + if (a !== b) + return false; + } + } + return true; + } + }); + }); + + describe('PostScriptParser', function() { + function parse(program) { + var stream = new StringStream(program); + var parser = new PostScriptParser(new PostScriptLexer(stream)); + return parser.parse(); + } + it('parses empty programs', function() { + var output = parse('{}'); + expect(output.length).toEqual(0); + }); + it('parses positive numbers', function() { + var number = 999; + var program = parse('{ ' + number + ' }'); + var expectedProgram = [number]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses negative numbers', function() { + var number = -999; + var program = parse('{ ' + number + ' }'); + var expectedProgram = [number]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses negative floats', function() { + var number = 3.3; + var program = parse('{ ' + number + ' }'); + var expectedProgram = [number]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses operators', function() { + var program = parse('{ sub }'); + var expectedProgram = ['sub']; + expect(program).toMatchArray(expectedProgram); + }); + it('parses if statements', function() { + var program = parse('{ { 99 } if }'); + var expectedProgram = [3, 'jz', 99]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses ifelse statements', function() { + var program = parse('{ { 99 } { 44 } ifelse }'); + var expectedProgram = [5, 'jz', 99, 6, 'j', 44]; + expect(program).toMatchArray(expectedProgram); + }); + it('handles missing brackets', function() { + expect(function() { parse('{'); }).toThrow( + new Error('Unexpected symbol: found undefined expected 1.')); + }); + }); + + describe('PostScriptEvaluator', function() { + function evaluate(program) { + var stream = new StringStream(program); + var parser = new PostScriptParser(new PostScriptLexer(stream)); + var code = parser.parse(); + var evaluator = new PostScriptEvaluator(code); + var output = evaluator.execute(); + return output; + } + + it('pushes stack', function() { + var stack = evaluate('{ 99 }'); + var expectedStack = [99]; + expect(stack).toMatchArray(expectedStack); + }); + it('handles if with true', function() { + var stack = evaluate('{ 1 {99} if }'); + var expectedStack = [99]; + expect(stack).toMatchArray(expectedStack); + }); + it('handles if with false', function() { + var stack = evaluate('{ 0 {99} if }'); + var expectedStack = []; + expect(stack).toMatchArray(expectedStack); + }); + it('handles ifelse with true', function() { + var stack = evaluate('{ 1 {99} {77} ifelse }'); + var expectedStack = [99]; + expect(stack).toMatchArray(expectedStack); + }); + it('handles ifelse with false', function() { + var stack = evaluate('{ 0 {99} {77} ifelse }'); + var expectedStack = [77]; + expect(stack).toMatchArray(expectedStack); + }); + it('handles nested if', function() { + var stack = evaluate('{ 1 {1 {77} if} if }'); + var expectedStack = [77]; + expect(stack).toMatchArray(expectedStack); + }); + + it('abs', function() { + var stack = evaluate('{ -2 abs }'); + var expectedStack = [2]; + expect(stack).toMatchArray(expectedStack); + }); + it('adds', function() { + var stack = evaluate('{ 1 2 add }'); + var expectedStack = [3]; + expect(stack).toMatchArray(expectedStack); + }); + it('boolean ands', function() { + var stack = evaluate('{ true false and }'); + var expectedStack = [false]; + expect(stack).toMatchArray(expectedStack); + }); + it('bitwise ands', function() { + var stack = evaluate('{ 254 1 and }'); + var expectedStack = [254 & 1]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO atan + // TODO bitshift + // TODO ceiling + // TODO copy + // TODO cos + it('converts to int', function() { + var stack = evaluate('{ 9.9 cvi }'); + var expectedStack = [9]; + expect(stack).toMatchArray(expectedStack); + }); + it('converts negatives to int', function() { + var stack = evaluate('{ -9.9 cvi }'); + var expectedStack = [-9]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO cvr + // TODO div + it('duplicates', function() { + var stack = evaluate('{ 99 dup }'); + var expectedStack = [99, 99]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO eq + it('exchanges', function() { + var stack = evaluate('{ 44 99 exch }'); + var expectedStack = [99, 44]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO exp + // TODO false + // TODO floor + // TODO ge + // TODO gt + it('divides to integer', function() { + var stack = evaluate('{ 2 3 idiv }'); + var expectedStack = [0]; + expect(stack).toMatchArray(expectedStack); + }); + it('divides to negative integer', function() { + var stack = evaluate('{ -2 3 idiv }'); + var expectedStack = [0]; + expect(stack).toMatchArray(expectedStack); + }); + it('duplicates index', function() { + var stack = evaluate('{ 4 3 2 1 2 index }'); + var expectedStack = [4, 3, 2, 1, 3]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO le + // TODO ln + // TODO log + // TODO lt + // TODO mod + // TODO mul + // TODO ne + // TODO neg + // TODO not + // TODO or + it('pops stack', function() { + var stack = evaluate('{ 1 2 pop }'); + var expectedStack = [1]; + expect(stack).toMatchArray(expectedStack); + }); + it('rolls stack right', function() { + var stack = evaluate('{ 1 3 2 2 4 1 roll }'); + var expectedStack = [2, 1, 3, 2]; + expect(stack).toMatchArray(expectedStack); + }); + it('rolls stack left', function() { + var stack = evaluate('{ 1 3 2 2 4 -1 roll }'); + var expectedStack = [3, 2, 2, 1]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO round + // TODO sin + // TODO sqrt + // TODO sub + // TODO true + // TODO truncate + // TODO xor + }); +}); + diff --git a/test/unit/obj_spec.js b/test/unit/obj_spec.js index f3a4185dd..02e268fd4 100644 --- a/test/unit/obj_spec.js +++ b/test/unit/obj_spec.js @@ -12,5 +12,120 @@ describe('obj', function() { expect(name.name).toEqual(givenName); }); }); + + describe('Cmd', function() { + it('should retain the given cmd name', function() { + var givenCmd = 'BT'; + var cmd = new Cmd(givenCmd); + expect(cmd.cmd).toEqual(givenCmd); + }); + + it('should create only one object for a command and cache it', function() { + var firstBT = Cmd.get('BT'); + var secondBT = Cmd.get('BT'); + var firstET = Cmd.get('ET'); + var secondET = Cmd.get('ET'); + expect(firstBT).toBe(secondBT); + expect(firstET).toBe(secondET); + expect(firstBT).not.toBe(firstET); + }); + }); + + describe('Dict', function() { + var checkInvalidHasValues = function(dict) { + expect(dict.has()).toBeFalsy(); + expect(dict.has('Prev')).toBeFalsy(); + }; + + var checkInvalidKeyValues = function(dict) { + expect(dict.get()).toBeUndefined(); + expect(dict.get('Prev')).toBeUndefined(); + expect(dict.get('Decode', 'D')).toBeUndefined(); + + // Note that the getter with three arguments breaks the pattern here. + expect(dict.get('FontFile', 'FontFile2', 'FontFile3')).toBeNull(); + }; + + var emptyDict, dictWithSizeKey, dictWithManyKeys; + var storedSize = 42; + var testFontFile = 'file1'; + var testFontFile2 = 'file2'; + var testFontFile3 = 'file3'; + + beforeEach(function() { + emptyDict = new Dict(); + + dictWithSizeKey = new Dict(); + dictWithSizeKey.set('Size', storedSize); + + dictWithManyKeys = new Dict(); + dictWithManyKeys.set('FontFile', testFontFile); + dictWithManyKeys.set('FontFile2', testFontFile2); + dictWithManyKeys.set('FontFile3', testFontFile3); + }); + + it('should return invalid values for unknown keys', function() { + checkInvalidHasValues(emptyDict); + checkInvalidKeyValues(emptyDict); + }); + + it('should return correct value for stored Size key', function() { + expect(dictWithSizeKey.has('Size')).toBeTruthy(); + + expect(dictWithSizeKey.get('Size')).toEqual(storedSize); + expect(dictWithSizeKey.get('Prev', 'Size')).toEqual(storedSize); + expect(dictWithSizeKey.get('Prev', 'Root', 'Size')).toEqual(storedSize); + }); + + it('should return invalid values for unknown keys when Size key is stored', + function() { + checkInvalidHasValues(dictWithSizeKey); + checkInvalidKeyValues(dictWithSizeKey); + }); + + it('should return correct value for stored Size key with undefined value', + function() { + var dict = new Dict(); + dict.set('Size'); + + expect(dict.has('Size')).toBeTruthy(); + + checkInvalidKeyValues(dict); + }); + + it('should return correct values for multiple stored keys', function() { + expect(dictWithManyKeys.has('FontFile')).toBeTruthy(); + expect(dictWithManyKeys.has('FontFile2')).toBeTruthy(); + expect(dictWithManyKeys.has('FontFile3')).toBeTruthy(); + + expect(dictWithManyKeys.get('FontFile3')).toEqual(testFontFile3); + expect(dictWithManyKeys.get('FontFile2', 'FontFile3')) + .toEqual(testFontFile2); + expect(dictWithManyKeys.get('FontFile', 'FontFile2', 'FontFile3')) + .toEqual(testFontFile); + }); + + it('should callback for each stored key', function() { + var callbackSpy = jasmine.createSpy('spy on callback in dictionary'); + + dictWithManyKeys.forEach(callbackSpy); + + expect(callbackSpy).wasCalled(); + expect(callbackSpy.argsForCall[0]).toEqual(['FontFile', testFontFile]); + expect(callbackSpy.argsForCall[1]).toEqual(['FontFile2', testFontFile2]); + expect(callbackSpy.argsForCall[2]).toEqual(['FontFile3', testFontFile3]); + expect(callbackSpy.callCount).toEqual(3); + }); + }); + + describe('Ref', function() { + it('should retain the stored values', function() { + var storedNum = 4; + var storedGen = 2; + var ref = new Ref(storedNum, storedGen); + expect(ref.num).toEqual(storedNum); + expect(ref.gen).toEqual(storedGen); + }); + }); }); diff --git a/web/compatibility.js b/web/compatibility.js index 7d1d72553..26405ad8f 100644 --- a/web/compatibility.js +++ b/web/compatibility.js @@ -5,11 +5,16 @@ // Checking if the typed arrays are supported (function checkTypedArrayCompatibility() { - if (typeof Uint8Array !== 'undefined') + if (typeof Uint8Array !== 'undefined') { + // some mobile version might not support Float64Array + if (typeof Float64Array === 'undefined') + window.Float64Array = Float32Array; + return; + } function subarray(start, end) { - return this.slice(start, end); + return new TypedArray(this.slice(start, end)); } function setArrayOffset(array, offset) { @@ -46,6 +51,8 @@ window.Uint32Array = TypedArray; window.Int32Array = TypedArray; window.Uint16Array = TypedArray; + window.Float32Array = TypedArray; + window.Float64Array = TypedArray; })(); // Object.create() ? @@ -205,3 +212,15 @@ }); })(); +// HTMLElement dataset property +(function checkDatasetProperty() { + var div = document.createElement('div'); + if ('dataset' in div) + return; // dataset property exists + Object.defineProperty(HTMLElement.prototype, 'dataset', { + get: function htmlElementDatasetGetter() { + // adding dataset field to the actual object + return (this.dataset = {}); + } + }); +})(); diff --git a/web/images/check.svg b/web/images/check.svg new file mode 100644 index 000000000..e0e1590a9 --- /dev/null +++ b/web/images/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/images/comment.svg b/web/images/comment.svg new file mode 100644 index 000000000..84feef1c8 --- /dev/null +++ b/web/images/comment.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/viewer.css b/web/viewer.css index c379e91c4..e355f7fc2 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -15,6 +15,7 @@ body { /* === Toolbar === */ #controls { background-color: #eee; + background: -o-linear-gradient(bottom,#eee 0%,#fff 100%); background: -moz-linear-gradient(center bottom, #eee 0%, #fff 100%); background: -webkit-gradient(linear, left bottom, left top, color-stop(0.0, #ddd), color-stop(1.0, #fff)); border-bottom: 1px solid #666; @@ -82,6 +83,7 @@ span#info { bottom: 18px; left: -290px; transition: left 0.25s ease-in-out 1s; + -o-transition: left 0.25s ease-in-out 1s; -moz-transition: left 0.25s ease-in-out 1s; -webkit-transition: left 0.25s ease-in-out 1s; z-index: 1; @@ -90,6 +92,7 @@ span#info { #sidebar:hover { left: 0px; transition: left 0.25s ease-in-out 0s; + -o-transition: left 0.25s ease-in-out 0s; -moz-transition: left 0.25s ease-in-out 0s; -webkit-transition: left 0.25s ease-in-out 0s; } @@ -232,6 +235,56 @@ canvas { -webkit-box-shadow: 0px 2px 10px #ff0; } +.textLayer { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + color: #000; +} + +.textLayer > div { + color: transparent; + position: absolute; + line-height:1.3; +} + +.annotComment > div { + position: absolute; +} + +.annotComment > img { + position: absolute; +} + +.annotComment > img:hover { + cursor: pointer; + opacity: 0.7; +} + +.annotComment > div { + padding: 0.2em; + max-width: 20em; + background-color: #F1E47B; + box-shadow: 0px 2px 10px #333; + -moz-box-shadow: 0px 2px 10px #333; + -webkit-box-shadow: 0px 2px 10px #333; +} + +.annotComment > div > h1 { + font-weight: normal; + font-size: 1.2em; + border-bottom: 1px solid #000000; + margin: 0px; +} + +/* TODO: file FF bug to support ::-moz-selection:window-inactive + so we can override the opaque grey background when the window is inactive; + see https://bugzilla.mozilla.org/show_bug.cgi?id=706209 */ +::selection { background:rgba(0,0,255,0.3); } +::-moz-selection { background:rgba(0,0,255,0.3); } + #viewer { margin: 44px 0px 0px; padding: 8px 0px; @@ -252,6 +305,38 @@ canvas { display: none; } +#errorWrapper { + background: none repeat scroll 0 0 #FF5555; + color: white; + left: 0; + position: fixed; + right: 0; + top: 30px; + z-index: 1000; + padding: 3px; + font-size: 0.8em; +} + +#errorMessageLeft { + float: left; +} + +#errorMessageRight { + float: right; +} + +#errorMoreInfo { + background-color: #FFFFFF; + color: black; + padding: 3px; + margin: 3px; + width: 98%; +} + +.clearBoth { + clear: both; +} + /* === Printed media overrides === */ @media print { #sidebar { diff --git a/web/viewer.html b/web/viewer.html index e441a9847..40e99004f 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -27,9 +27,9 @@ - - + +
@@ -67,10 +67,11 @@ - + +
@@ -97,6 +98,24 @@ --
+