/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* Copyright 2012 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; // contexts store most of the state we need natively. // However, PDF needs a bit more state, which we store here. 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 }; // Minimal font size that would be used during canvas fillText operations. var MIN_FONT_SIZE = 1; function createScratchCanvas(width, height) { var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; return canvas; } 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 CanvasExtraState = (function CanvasExtraStateClosure() { function CanvasExtraState(old) { // Are soft masks and alpha values shapes or opacities? this.alphaIsShape = false; this.fontSize = 0; this.fontSizeScale = 1; this.textMatrix = IDENTITY_MATRIX; this.fontMatrix = IDENTITY_MATRIX; this.leading = 0; // Current point (in user coordinates) this.x = 0; this.y = 0; // Start of text line (in text coordinates) this.lineX = 0; this.lineY = 0; // Character and word spacing this.charSpacing = 0; this.wordSpacing = 0; this.textHScale = 1; this.textRenderingMode = TextRenderingMode.FILL; this.textRise = 0; // Color spaces this.fillColorSpace = new DeviceGrayCS(); this.fillColorSpaceObj = null; this.strokeColorSpace = new DeviceGrayCS(); this.strokeColorSpaceObj = null; this.fillColorObj = null; this.strokeColorObj = null; // Default fore and background colors this.fillColor = '#000000'; this.strokeColor = '#000000'; // Note: fill alpha applies to all non-stroking operations this.fillAlpha = 1; this.strokeAlpha = 1; this.lineWidth = 1; this.old = old; } CanvasExtraState.prototype = { clone: function CanvasExtraState_clone() { return Object.create(this); }, setCurrentPoint: function CanvasExtraState_setCurrentPoint(x, y) { this.x = x; this.y = y; } }; return CanvasExtraState; })(); var CanvasGraphics = (function CanvasGraphicsClosure() { // Defines the time the executeOperatorList is going to be executing // before it stops and shedules a continue of execution. var kExecutionTime = 15; function CanvasGraphics(canvasCtx, objs, textLayer) { this.ctx = canvasCtx; this.current = new CanvasExtraState(); this.stateStack = []; this.pendingClip = null; this.res = null; this.xobjs = null; this.objs = objs; this.textLayer = textLayer; if (canvasCtx) { addContextCurrentTransform(canvasCtx); } } var LINE_CAP_STYLES = ['butt', 'round', 'square']; var LINE_JOIN_STYLES = ['miter', 'round', 'bevel']; var NORMAL_CLIP = {}; var EO_CLIP = {}; 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': true, 'setStrokeGray': true, 'setFillGray': true, 'setStrokeRGBColor': true, 'setFillRGBColor': true, 'setStrokeCMYKColor': true, 'setFillCMYKColor': true, 'paintJpegXObject': true, 'paintImageXObject': true, 'paintImageMaskXObject': true, 'shadingFill': true }, beginDrawing: function CanvasGraphics_beginDrawing(viewport) { var transform = viewport.transform; this.ctx.save(); this.ctx.transform.apply(this.ctx, transform); if (this.textLayer) this.textLayer.beginLayout(); }, executeOperatorList: function CanvasGraphics_executeOperatorList( operatorList, executionStartIdx, continueCallback, stepper) { var argsArray = operatorList.argsArray; var fnArray = operatorList.fnArray; var i = executionStartIdx || 0; var argsArrayLen = argsArray.length; // Sometimes the OperatorList to execute is empty. if (argsArrayLen == i) { return i; } var executionEndIdx; var endTime = Date.now() + kExecutionTime; var objs = this.objs; var fnName; var slowCommands = this.slowCommands; while (true) { if (stepper && i === stepper.nextBreakPoint) { stepper.breakIt(i, continueCallback); return i; } fnName = fnArray[i]; 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; } } } i++; // If the entire operatorList was executed, stop as were done. if (i == argsArrayLen) { return i; } // If the execution took longer then a certain amount of time, shedule // to continue exeution after a short delay. // However, this is only possible if a 'continueCallback' is passed in. if (continueCallback && slowCommands[fnName] && Date.now() > endTime) { setTimeout(continueCallback, 0); return i; } // If the operatorList isn't executed completely yet OR the execution // time was short enough, do another execution round. } }, endDrawing: function CanvasGraphics_endDrawing() { this.ctx.restore(); if (this.textLayer) this.textLayer.endLayout(); }, // Graphics state setLineWidth: function CanvasGraphics_setLineWidth(width) { this.current.lineWidth = width; this.ctx.lineWidth = width; }, setLineCap: function CanvasGraphics_setLineCap(style) { this.ctx.lineCap = LINE_CAP_STYLES[style]; }, setLineJoin: function CanvasGraphics_setLineJoin(style) { this.ctx.lineJoin = LINE_JOIN_STYLES[style]; }, setMiterLimit: function CanvasGraphics_setMiterLimit(limit) { this.ctx.miterLimit = limit; }, setDash: function CanvasGraphics_setDash(dashArray, dashPhase) { this.ctx.mozDash = dashArray; this.ctx.mozDashOffset = dashPhase; this.ctx.webkitLineDash = dashArray; this.ctx.webkitLineDashOffset = dashPhase; }, setRenderingIntent: function CanvasGraphics_setRenderingIntent(intent) { // Maybe if we one day fully support color spaces this will be important // for now we can ignore. // TODO set rendering intent? }, setFlatness: function CanvasGraphics_setFlatness(flatness) { // There's no way to control this with canvas, but we can safely ignore. // TODO set flatness? }, setGState: function CanvasGraphics_setGState(states) { for (var i = 0, ii = states.length; i < ii; i++) { var state = states[i]; var key = state[0]; var value = state[1]; switch (key) { case 'LW': this.setLineWidth(value); break; case 'LC': this.setLineCap(value); break; case 'LJ': this.setLineJoin(value); break; case 'ML': this.setMiterLimit(value); break; case 'D': this.setDash(value[0], value[1]); break; case 'RI': this.setRenderingIntent(value); break; case 'FL': this.setFlatness(value); break; case 'Font': this.setFont(state[1], state[2]); break; case 'CA': this.current.strokeAlpha = state[1]; break; case 'ca': this.current.fillAlpha = state[1]; this.ctx.globalAlpha = state[1]; break; } } }, save: function CanvasGraphics_save() { this.ctx.save(); var old = this.current; this.stateStack.push(old); this.current = old.clone(); }, restore: function CanvasGraphics_restore() { var prev = this.stateStack.pop(); if (prev) { this.current = prev; this.ctx.restore(); } }, transform: function CanvasGraphics_transform(a, b, c, d, e, f) { this.ctx.transform(a, b, c, d, e, f); }, // Path moveTo: function CanvasGraphics_moveTo(x, y) { this.ctx.moveTo(x, y); this.current.setCurrentPoint(x, y); }, lineTo: function CanvasGraphics_lineTo(x, y) { this.ctx.lineTo(x, y); this.current.setCurrentPoint(x, y); }, curveTo: function CanvasGraphics_curveTo(x1, y1, x2, y2, x3, y3) { this.ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3); this.current.setCurrentPoint(x3, y3); }, curveTo2: function CanvasGraphics_curveTo2(x2, y2, x3, y3) { var current = this.current; this.ctx.bezierCurveTo(current.x, current.y, x2, y2, x3, y3); current.setCurrentPoint(x3, y3); }, curveTo3: function CanvasGraphics_curveTo3(x1, y1, x3, y3) { this.curveTo(x1, y1, x3, y3, x3, y3); this.current.setCurrentPoint(x3, y3); }, closePath: function CanvasGraphics_closePath() { this.ctx.closePath(); }, rectangle: function CanvasGraphics_rectangle(x, y, width, height) { this.ctx.rect(x, y, width, height); }, stroke: function CanvasGraphics_stroke(consumePath) { consumePath = typeof consumePath !== 'undefined' ? consumePath : true; var ctx = this.ctx; var strokeColor = this.current.strokeColor; if (this.current.lineWidth === 0) ctx.lineWidth = this.getSinglePixelWidth(); // For stroke we want to temporarily change the global alpha to the // stroking alpha. ctx.globalAlpha = this.current.strokeAlpha; if (strokeColor && strokeColor.hasOwnProperty('type') && strokeColor.type === 'Pattern') { // for patterns, we transform to pattern space, calculate // the pattern, call stroke, and restore to user space ctx.save(); ctx.strokeStyle = strokeColor.getPattern(ctx); ctx.stroke(); ctx.restore(); } else { ctx.stroke(); } if (consumePath) this.consumePath(); // Restore the global alpha to the fill alpha ctx.globalAlpha = this.current.fillAlpha; }, closeStroke: function CanvasGraphics_closeStroke() { this.closePath(); this.stroke(); }, fill: function CanvasGraphics_fill(consumePath) { consumePath = typeof consumePath !== 'undefined' ? consumePath : true; var ctx = this.ctx; var fillColor = this.current.fillColor; if (fillColor && fillColor.hasOwnProperty('type') && fillColor.type === 'Pattern') { ctx.save(); ctx.fillStyle = fillColor.getPattern(ctx); ctx.fill(); ctx.restore(); } else { ctx.fill(); } if (consumePath) this.consumePath(); }, eoFill: function CanvasGraphics_eoFill() { var savedFillRule = this.setEOFillRule(); this.fill(); this.restoreFillRule(savedFillRule); }, fillStroke: function CanvasGraphics_fillStroke() { this.fill(false); this.stroke(false); this.consumePath(); }, eoFillStroke: function CanvasGraphics_eoFillStroke() { var savedFillRule = this.setEOFillRule(); this.fillStroke(); this.restoreFillRule(savedFillRule); }, closeFillStroke: function CanvasGraphics_closeFillStroke() { this.closePath(); this.fillStroke(); }, closeEOFillStroke: function CanvasGraphics_closeEOFillStroke() { var savedFillRule = this.setEOFillRule(); this.closePath(); this.fillStroke(); this.restoreFillRule(savedFillRule); }, endPath: function CanvasGraphics_endPath() { this.consumePath(); }, // Clipping clip: function CanvasGraphics_clip() { this.pendingClip = NORMAL_CLIP; }, eoClip: function CanvasGraphics_eoClip() { this.pendingClip = EO_CLIP; }, // Text beginText: function CanvasGraphics_beginText() { this.current.textMatrix = IDENTITY_MATRIX; this.current.x = this.current.lineX = 0; this.current.y = this.current.lineY = 0; }, endText: function CanvasGraphics_endText() { }, setCharSpacing: function CanvasGraphics_setCharSpacing(spacing) { this.current.charSpacing = spacing; }, setWordSpacing: function CanvasGraphics_setWordSpacing(spacing) { this.current.wordSpacing = spacing; }, setHScale: function CanvasGraphics_setHScale(scale) { this.current.textHScale = scale / 100; }, setLeading: function CanvasGraphics_setLeading(leading) { this.current.leading = -leading; }, setFont: function CanvasGraphics_setFont(fontRefName, size) { var fontObj = this.objs.get(fontRefName); var current = this.current; if (!fontObj) error('Can\'t find font for ' + fontRefName); // Slice-clone matrix so we can manipulate it without affecting original if (fontObj.fontMatrix) current.fontMatrix = fontObj.fontMatrix.slice(0); else current.fontMatrix = IDENTITY_MATRIX.slice(0); // A valid matrix needs all main diagonal elements to be non-zero // This also ensures we bypass FF bugzilla bug #719844. if (current.fontMatrix[0] === 0 || current.fontMatrix[3] === 0) { warn('Invalid font matrix for font ' + fontRefName); } // The spec for Tf (setFont) says that 'size' specifies the font 'scale', // and in some docs this can be negative (inverted x-y axes). // We implement this condition with fontMatrix. if (size < 0) { size = -size; current.fontMatrix[0] *= -1; current.fontMatrix[3] *= -1; } this.current.font = fontObj; this.current.fontSize = size; if (fontObj.coded) return; // we don't need ctx.font for Type3 fonts var name = fontObj.loadedName || 'sans-serif'; var bold = fontObj.black ? (fontObj.bold ? 'bolder' : 'bold') : (fontObj.bold ? 'bold' : 'normal'); var italic = fontObj.italic ? 'italic' : 'normal'; var typeface = '"' + name + '", ' + fontObj.fallbackName; // Some font backends cannot handle fonts below certain size. // Keeping the font at minimal size and using the fontSizeScale to change // the current transformation matrix before the fillText/strokeText. // See https://bugzilla.mozilla.org/show_bug.cgi?id=726227 var browserFontSize = size >= MIN_FONT_SIZE ? size : MIN_FONT_SIZE; this.current.fontSizeScale = browserFontSize != MIN_FONT_SIZE ? 1.0 : size / MIN_FONT_SIZE; var rule = italic + ' ' + bold + ' ' + browserFontSize + 'px ' + typeface; this.ctx.font = rule; }, setTextRenderingMode: function CanvasGraphics_setTextRenderingMode(mode) { if (mode >= TextRenderingMode.FILL_ADD_TO_PATH) TODO('unsupported text rendering mode: ' + mode); this.current.textRenderingMode = mode; }, setTextRise: function CanvasGraphics_setTextRise(rise) { this.current.textRise = rise; }, moveText: function CanvasGraphics_moveText(x, y) { this.current.x = this.current.lineX += x; this.current.y = this.current.lineY += y; }, setLeadingMoveText: function CanvasGraphics_setLeadingMoveText(x, y) { this.setLeading(-y); this.moveText(x, y); }, setTextMatrix: function CanvasGraphics_setTextMatrix(a, b, c, d, e, f) { this.current.textMatrix = [a, b, c, d, e, f]; this.current.x = this.current.lineX = 0; this.current.y = this.current.lineY = 0; }, nextLine: function CanvasGraphics_nextLine() { this.moveText(0, this.current.leading); }, applyTextTransforms: function CanvasGraphics_applyTextTransforms() { var ctx = this.ctx; var current = this.current; var textHScale = current.textHScale; var fontMatrix = current.fontMatrix || IDENTITY_MATRIX; ctx.transform.apply(ctx, current.textMatrix); ctx.scale(1, -1); ctx.translate(current.x, -current.y - current.textRise); ctx.transform.apply(ctx, fontMatrix); ctx.scale(textHScale, 1); }, createTextGeometry: function CanvasGraphics_createTextGeometry() { 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; geometry.fontName = font.loadedName; geometry.fontFamily = font.fallbackName; geometry.fontSize = this.current.fontSize; return geometry; }, showText: function CanvasGraphics_showText(str, skipTextSelection) { var ctx = this.ctx; var current = this.current; var font = current.font; var glyphs = font.charsToGlyphs(str); var fontSize = current.fontSize; var fontSizeScale = current.fontSizeScale; var charSpacing = current.charSpacing; var wordSpacing = current.wordSpacing; var textHScale = current.textHScale; var fontMatrix = current.fontMatrix || IDENTITY_MATRIX; var textHScale2 = textHScale * fontMatrix[0]; var glyphsLength = glyphs.length; var textLayer = this.textLayer; var geom; var textSelection = textLayer && !skipTextSelection ? true : false; var textRenderingMode = current.textRenderingMode; var canvasWidth = 0.0; // 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); ctx.scale(textHScale, 1); if (textSelection) { this.save(); ctx.scale(1, -1); geom = this.createTextGeometry(); this.restore(); } for (var i = 0; i < glyphsLength; ++i) { var glyph = glyphs[i]; if (glyph === null) { // word break this.ctx.translate(wordSpacing, 0); current.x += wordSpacing * textHScale; continue; } this.save(); ctx.scale(fontSize, fontSize); ctx.transform.apply(ctx, fontMatrix); this.executeOperatorList(glyph.operatorList); this.restore(); var transformed = Util.applyTransform([glyph.width, 0], fontMatrix); var width = transformed[0] * fontSize + Util.sign(current.fontMatrix[0]) * charSpacing; ctx.translate(width, 0); current.x += width * textHScale; canvasWidth += width; } ctx.restore(); } else { ctx.save(); this.applyTextTransforms(); var lineWidth = current.lineWidth; var scale = Math.abs(current.textMatrix[0] * fontMatrix[0]); if (scale == 0 || lineWidth == 0) lineWidth = this.getSinglePixelWidth(); else lineWidth /= scale; if (textSelection) geom = this.createTextGeometry(); if (fontSizeScale != 1.0) { ctx.scale(fontSizeScale, fontSizeScale); lineWidth /= fontSizeScale; } ctx.lineWidth = lineWidth; var x = 0; for (var i = 0; i < glyphsLength; ++i) { var glyph = glyphs[i]; if (glyph === null) { // word break x += Util.sign(current.fontMatrix[0]) * wordSpacing; continue; } var character = glyph.fontChar; var charWidth = glyph.width * fontSize * 0.001 + Util.sign(current.fontMatrix[0]) * charSpacing; if (!glyph.disabled) { var scaledX = x / fontSizeScale; switch (textRenderingMode) { default: // other unsupported rendering modes case TextRenderingMode.FILL: case TextRenderingMode.FILL_ADD_TO_PATH: ctx.fillText(character, scaledX, 0); break; case TextRenderingMode.STROKE: case TextRenderingMode.STROKE_ADD_TO_PATH: ctx.strokeText(character, scaledX, 0); break; case TextRenderingMode.FILL_STROKE: case TextRenderingMode.FILL_STROKE_ADD_TO_PATH: ctx.fillText(character, scaledX, 0); ctx.strokeText(character, scaledX, 0); break; case TextRenderingMode.INVISIBLE: break; } } x += charWidth; var glyphUnicode = glyph.unicode === ' ' ? '\u00A0' : glyph.unicode; if (glyphUnicode in NormalizedUnicodes) glyphUnicode = NormalizedUnicodes[glyphUnicode]; canvasWidth += charWidth; } current.x += x * textHScale2; ctx.restore(); } if (textSelection) { geom.canvasWidth = canvasWidth; this.textLayer.appendText(geom); } return canvasWidth; }, showSpacedText: function CanvasGraphics_showSpacedText(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 *= (current.fontMatrix || IDENTITY_MATRIX)[0]; var arrLength = arr.length; var textLayer = this.textLayer; var geom; var canvasWidth = 0.0; 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(); geom = this.createTextGeometry(); ctx.restore(); } for (var i = 0; i < arrLength; ++i) { var e = arr[i]; if (isNum(e)) { var spacingLength = -e * 0.001 * fontSize * textHScale; current.x += spacingLength; if (textSelection) canvasWidth += spacingLength; } else if (isString(e)) { var shownCanvasWidth = this.showText(e, true); if (textSelection) canvasWidth += shownCanvasWidth; } else { error('TJ array element ' + e + ' is not string or num'); } } if (textSelection) { geom.canvasWidth = canvasWidth; this.textLayer.appendText(geom); } }, nextLineShowText: function CanvasGraphics_nextLineShowText(text) { this.nextLine(); this.showText(text); }, nextLineSetSpacingShowText: function CanvasGraphics_nextLineSetSpacingShowText(wordSpacing, charSpacing, text) { this.setWordSpacing(wordSpacing); this.setCharSpacing(charSpacing); this.nextLineShowText(text); }, // Type3 fonts setCharWidth: function CanvasGraphics_setCharWidth(xWidth, yWidth) { // We can safely ignore this since the width should be the same // as the width in the Widths array. }, setCharWidthAndBounds: function CanvasGraphics_setCharWidthAndBounds(xWidth, yWidth, llx, lly, urx, ury) { // TODO According to the spec we're also suppose to ignore any operators // that set color or include images while processing this type3 font. this.rectangle(llx, lly, urx - llx, ury - lly); this.clip(); this.endPath(); }, // Color setStrokeColorSpace: function CanvasGraphics_setStrokeColorSpace(raw) { this.current.strokeColorSpace = ColorSpace.fromIR(raw); }, setFillColorSpace: function CanvasGraphics_setFillColorSpace(raw) { this.current.fillColorSpace = ColorSpace.fromIR(raw); }, setStrokeColor: function CanvasGraphics_setStrokeColor(/*...*/) { var cs = this.current.strokeColorSpace; var rgbColor = cs.getRgb(arguments); var color = Util.makeCssRgb(rgbColor[0], rgbColor[1], rgbColor[2]); this.ctx.strokeStyle = color; this.current.strokeColor = color; }, getColorN_Pattern: function CanvasGraphics_getColorN_Pattern(IR, cs) { if (IR[0] == 'TilingPattern') { var args = IR[1]; var base = cs.base; var color; if (base) { var baseComps = base.numComps; color = []; for (var i = 0; i < baseComps; ++i) color.push(args[i]); color = base.getRgb(color); } var pattern = new TilingPattern(IR, color, this.ctx, this.objs); } else if (IR[0] == 'RadialAxial' || IR[0] == 'Dummy') { var pattern = Pattern.shadingFromIR(IR); } else { error('Unkown IR type ' + IR[0]); } return pattern; }, setStrokeColorN: function CanvasGraphics_setStrokeColorN(/*...*/) { var cs = this.current.strokeColorSpace; if (cs.name == 'Pattern') { this.current.strokeColor = this.getColorN_Pattern(arguments, cs); } else { this.setStrokeColor.apply(this, arguments); } }, setFillColor: function CanvasGraphics_setFillColor(/*...*/) { var cs = this.current.fillColorSpace; var rgbColor = cs.getRgb(arguments); var color = Util.makeCssRgb(rgbColor[0], rgbColor[1], rgbColor[2]); this.ctx.fillStyle = color; this.current.fillColor = color; }, setFillColorN: function CanvasGraphics_setFillColorN(/*...*/) { var cs = this.current.fillColorSpace; if (cs.name == 'Pattern') { this.current.fillColor = this.getColorN_Pattern(arguments, cs); } else { this.setFillColor.apply(this, arguments); } }, setStrokeGray: function CanvasGraphics_setStrokeGray(gray) { if (!(this.current.strokeColorSpace instanceof DeviceGrayCS)) this.current.strokeColorSpace = new DeviceGrayCS(); var color = Util.makeCssRgb(gray, gray, gray); this.ctx.strokeStyle = color; this.current.strokeColor = color; }, setFillGray: function CanvasGraphics_setFillGray(gray) { if (!(this.current.fillColorSpace instanceof DeviceGrayCS)) this.current.fillColorSpace = new DeviceGrayCS(); var color = Util.makeCssRgb(gray, gray, gray); this.ctx.fillStyle = color; this.current.fillColor = color; }, setStrokeRGBColor: function CanvasGraphics_setStrokeRGBColor(r, g, b) { if (!(this.current.strokeColorSpace instanceof DeviceRgbCS)) this.current.strokeColorSpace = new DeviceRgbCS(); var color = Util.makeCssRgb(r, g, b); this.ctx.strokeStyle = color; this.current.strokeColor = color; }, setFillRGBColor: function CanvasGraphics_setFillRGBColor(r, g, b) { if (!(this.current.fillColorSpace instanceof DeviceRgbCS)) this.current.fillColorSpace = new DeviceRgbCS(); var color = Util.makeCssRgb(r, g, b); this.ctx.fillStyle = color; this.current.fillColor = color; }, setStrokeCMYKColor: function CanvasGraphics_setStrokeCMYKColor(c, m, y, k) { if (!(this.current.strokeColorSpace instanceof DeviceCmykCS)) this.current.strokeColorSpace = new DeviceCmykCS(); var color = Util.makeCssCmyk(c, m, y, k); this.ctx.strokeStyle = color; this.current.strokeColor = color; }, setFillCMYKColor: function CanvasGraphics_setFillCMYKColor(c, m, y, k) { if (!(this.current.fillColorSpace instanceof DeviceCmykCS)) this.current.fillColorSpace = new DeviceCmykCS(); var color = Util.makeCssCmyk(c, m, y, k); this.ctx.fillStyle = color; this.current.fillColor = color; }, shadingFill: function CanvasGraphics_shadingFill(patternIR) { var ctx = this.ctx; this.save(); var pattern = Pattern.shadingFromIR(patternIR); ctx.fillStyle = pattern.getPattern(ctx); var inv = ctx.mozCurrentTransformInverse; if (inv) { var canvas = ctx.canvas; var width = canvas.width; var height = canvas.height; var bl = Util.applyTransform([0, 0], 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]); var x1 = Math.max(bl[0], br[0], ul[0], ur[0]); var y1 = Math.max(bl[1], br[1], ul[1], ur[1]); this.ctx.fillRect(x0, y0, x1 - x0, y1 - y0); } else { // HACK to draw the gradient onto an infinite rectangle. // PDF gradients are drawn across the entire image while // Canvas only allows gradients to be drawn in a rectangle // The following bug should allow us to remove this. // https://bugzilla.mozilla.org/show_bug.cgi?id=664884 this.ctx.fillRect(-1e10, -1e10, 2e10, 2e10); } this.restore(); }, // Images beginInlineImage: function CanvasGraphics_beginInlineImage() { error('Should not call beginInlineImage'); }, beginImageData: function CanvasGraphics_beginImageData() { error('Should not call beginImageData'); }, paintFormXObjectBegin: function CanvasGraphics_paintFormXObjectBegin(matrix, bbox) { this.save(); if (matrix && isArray(matrix) && 6 == matrix.length) this.transform.apply(this, matrix); if (bbox && isArray(bbox) && 4 == bbox.length) { var width = bbox[2] - bbox[0]; var height = bbox[3] - bbox[1]; this.rectangle(bbox[0], bbox[1], width, height); this.clip(); this.endPath(); } }, paintFormXObjectEnd: function CanvasGraphics_paintFormXObjectEnd() { this.restore(); }, paintJpegXObject: function CanvasGraphics_paintJpegXObject(objId, w, h) { var domImage = this.objs.get(objId); if (!domImage) { error('Dependent image isn\'t ready yet'); } this.save(); var ctx = this.ctx; // scale the image to the unit square ctx.scale(1 / w, -1 / h); ctx.drawImage(domImage, 0, 0, domImage.width, domImage.height, 0, -h, w, h); this.restore(); }, paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject( imgArray, inverseDecode, width, height) { function applyStencilMask(buffer, inverseDecode) { var imgArrayPos = 0; var i, j, mask, buf; // removing making non-masked pixels transparent var bufferPos = 3; // alpha component offset for (i = 0; i < height; i++) { mask = 0; for (j = 0; j < width; j++) { if (!mask) { buf = imgArray[imgArrayPos++]; mask = 128; } if (!(buf & mask) == inverseDecode) { buffer[bufferPos] = 0; } bufferPos += 4; mask >>= 1; } } } function rescaleImage(pixels, widthScale, heightScale) { var scaledWidth = Math.ceil(width / widthScale); var scaledHeight = Math.ceil(height / heightScale); var itemsSum = new Uint32Array(scaledWidth * scaledHeight * 4); var itemsCount = new Uint32Array(scaledWidth * scaledHeight); for (var i = 0, position = 0; i < height; i++) { var lineOffset = (0 | (i / heightScale)) * scaledWidth; for (var j = 0; j < width; j++) { var countOffset = lineOffset + (0 | (j / widthScale)); var sumOffset = countOffset << 2; itemsSum[sumOffset] += pixels[position]; itemsSum[sumOffset + 1] += pixels[position + 1]; itemsSum[sumOffset + 2] += pixels[position + 2]; itemsSum[sumOffset + 3] += pixels[position + 3]; itemsCount[countOffset]++; position += 4; } } var tmpCanvas = createScratchCanvas(scaledWidth, scaledHeight); var tmpCtx = tmpCanvas.getContext('2d'); var imgData = tmpCtx.getImageData(0, 0, scaledWidth, scaledHeight); pixels = imgData.data; for (var i = 0, j = 0, ii = scaledWidth * scaledHeight; i < ii; i++) { var count = itemsCount[i]; pixels[j] = itemsSum[j] / count; pixels[j + 1] = itemsSum[j + 1] / count; pixels[j + 2] = itemsSum[j + 2] / count; pixels[j + 3] = itemsSum[j + 3] / count; j += 4; } tmpCtx.putImageData(imgData, 0, 0); return tmpCanvas; } this.save(); var ctx = this.ctx; var w = width, h = height; // scale the image to the unit square ctx.scale(1 / w, -1 / h); var tmpCanvas = createScratchCanvas(w, h); var tmpCtx = tmpCanvas.getContext('2d'); var fillColor = this.current.fillColor; tmpCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && fillColor.type === 'Pattern') ? fillColor.getPattern(tmpCtx) : fillColor; tmpCtx.fillRect(0, 0, w, h); var imgData = tmpCtx.getImageData(0, 0, w, h); var pixels = imgData.data; applyStencilMask(pixels, inverseDecode); var currentTransform = ctx.mozCurrentTransformInverse; var widthScale = Math.max(Math.abs(currentTransform[0]), 1); var heightScale = Math.max(Math.abs(currentTransform[3]), 1); if (widthScale >= 2 || heightScale >= 2) { // canvas does not resize well large images to small -- using simple // algorithm to perform pre-scaling tmpCanvas = rescaleImage(imgData.data, widthScale, heightScale); ctx.scale(widthScale, heightScale); ctx.drawImage(tmpCanvas, 0, -h / heightScale); } else { tmpCtx.putImageData(imgData, 0, 0); ctx.drawImage(tmpCanvas, 0, -h); } this.restore(); }, paintImageXObject: function CanvasGraphics_paintImageXObject(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; var h = imgData.height; // scale the image to the unit square ctx.scale(1 / w, -1 / h); var tmpCanvas = createScratchCanvas(w, h); var tmpCtx = tmpCanvas.getContext('2d'); this.putBinaryImageData(tmpCtx, imgData, w, h); ctx.drawImage(tmpCanvas, 0, -h); this.restore(); }, putBinaryImageData: function CanvasGraphics_putBinaryImageData(ctx, imgData, w, h) { var tmpImgData = 'createImageData' in ctx ? ctx.createImageData(w, h) : ctx.getImageData(0, 0, w, h); var tmpImgDataPixels = tmpImgData.data; var data = imgData.data; if ('set' in tmpImgDataPixels) tmpImgDataPixels.set(data); else { // Copy over the imageData pixel by pixel. for (var i = 0, ii = tmpImgDataPixels.length; i < ii; i++) tmpImgDataPixels[i] = data[i]; } ctx.putImageData(tmpImgData, 0, 0); }, // Marked content markPoint: function CanvasGraphics_markPoint(tag) { // TODO Marked content. }, markPointProps: function CanvasGraphics_markPointProps(tag, properties) { // TODO Marked content. }, beginMarkedContent: function CanvasGraphics_beginMarkedContent(tag) { // TODO Marked content. }, beginMarkedContentProps: function CanvasGraphics_beginMarkedContentProps( tag, properties) { // TODO Marked content. }, endMarkedContent: function CanvasGraphics_endMarkedContent() { // TODO Marked content. }, // Compatibility beginCompat: function CanvasGraphics_beginCompat() { TODO('ignore undefined operators (should we do that anyway?)'); }, endCompat: function CanvasGraphics_endCompat() { TODO('stop ignoring undefined operators'); }, // Helper functions consumePath: function CanvasGraphics_consumePath() { if (this.pendingClip) { var savedFillRule = null; if (this.pendingClip == EO_CLIP) savedFillRule = this.setEOFillRule(); this.ctx.clip(); this.pendingClip = null; if (savedFillRule !== null) this.restoreFillRule(savedFillRule); } this.ctx.beginPath(); }, // We generally keep the canvas context set for // nonzero-winding, and just set evenodd for the operations // that need them. setEOFillRule: function CanvasGraphics_setEOFillRule() { var savedFillRule = this.ctx.mozFillRule; this.ctx.mozFillRule = 'evenodd'; return savedFillRule; }, restoreFillRule: function CanvasGraphics_restoreFillRule(rule) { this.ctx.mozFillRule = rule; }, getSinglePixelWidth: function CanvasGraphics_getSinglePixelWidth(scale) { var inverse = this.ctx.mozCurrentTransformInverse; return Math.abs(inverse[0] + inverse[2]); } }; return CanvasGraphics; })();