/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* Copyright 2014 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. */ /* globals CanvasGraphics, CachedCanvases, ColorSpace, Util, error, info, isArray, makeCssRgb, WebGLUtils */ 'use strict'; var ShadingIRs = {}; ShadingIRs.RadialAxial = { fromIR: function RadialAxial_fromIR(raw) { var type = raw[1]; var colorStops = raw[2]; var p0 = raw[3]; var p1 = raw[4]; var r0 = raw[5]; var r1 = raw[6]; return { type: 'Pattern', getPattern: function RadialAxial_getPattern(ctx) { var grad; if (type === 'axial') { grad = ctx.createLinearGradient(p0[0], p0[1], p1[0], p1[1]); } else if (type === 'radial') { grad = ctx.createRadialGradient(p0[0], p0[1], r0, p1[0], p1[1], r1); } for (var i = 0, ii = colorStops.length; i < ii; ++i) { var c = colorStops[i]; grad.addColorStop(c[0], c[1]); } return grad; } }; } }; var createMeshCanvas = (function createMeshCanvasClosure() { function drawTriangle(data, context, p1, p2, p3, c1, c2, c3) { // Very basic Gouraud-shaded triangle rasterization algorithm. var coords = context.coords, colors = context.colors; var bytes = data.data, rowSize = data.width * 4; var tmp; if (coords[p1 + 1] > coords[p2 + 1]) { tmp = p1; p1 = p2; p2 = tmp; tmp = c1; c1 = c2; c2 = tmp; } if (coords[p2 + 1] > coords[p3 + 1]) { tmp = p2; p2 = p3; p3 = tmp; tmp = c2; c2 = c3; c3 = tmp; } if (coords[p1 + 1] > coords[p2 + 1]) { tmp = p1; p1 = p2; p2 = tmp; tmp = c1; c1 = c2; c2 = tmp; } var x1 = (coords[p1] + context.offsetX) * context.scaleX; var y1 = (coords[p1 + 1] + context.offsetY) * context.scaleY; var x2 = (coords[p2] + context.offsetX) * context.scaleX; var y2 = (coords[p2 + 1] + context.offsetY) * context.scaleY; var x3 = (coords[p3] + context.offsetX) * context.scaleX; var y3 = (coords[p3 + 1] + context.offsetY) * context.scaleY; if (y1 >= y3) { return; } var c1r = colors[c1], c1g = colors[c1 + 1], c1b = colors[c1 + 2]; var c2r = colors[c2], c2g = colors[c2 + 1], c2b = colors[c2 + 2]; var c3r = colors[c3], c3g = colors[c3 + 1], c3b = colors[c3 + 2]; var minY = Math.round(y1), maxY = Math.round(y3); var xa, car, cag, cab; var xb, cbr, cbg, cbb; var k; for (var y = minY; y <= maxY; y++) { if (y < y2) { k = y < y1 ? 0 : y1 === y2 ? 1 : (y1 - y) / (y1 - y2); xa = x1 - (x1 - x2) * k; car = c1r - (c1r - c2r) * k; cag = c1g - (c1g - c2g) * k; cab = c1b - (c1b - c2b) * k; } else { k = y > y3 ? 1 : y2 === y3 ? 0 : (y2 - y) / (y2 - y3); xa = x2 - (x2 - x3) * k; car = c2r - (c2r - c3r) * k; cag = c2g - (c2g - c3g) * k; cab = c2b - (c2b - c3b) * k; } k = y < y1 ? 0 : y > y3 ? 1 : (y1 - y) / (y1 - y3); xb = x1 - (x1 - x3) * k; cbr = c1r - (c1r - c3r) * k; cbg = c1g - (c1g - c3g) * k; cbb = c1b - (c1b - c3b) * k; var x1_ = Math.round(Math.min(xa, xb)); var x2_ = Math.round(Math.max(xa, xb)); var j = rowSize * y + x1_ * 4; for (var x = x1_; x <= x2_; x++) { k = (xa - x) / (xa - xb); k = k < 0 ? 0 : k > 1 ? 1 : k; bytes[j++] = (car - (car - cbr) * k) | 0; bytes[j++] = (cag - (cag - cbg) * k) | 0; bytes[j++] = (cab - (cab - cbb) * k) | 0; bytes[j++] = 255; } } } function drawFigure(data, figure, context) { var ps = figure.coords; var cs = figure.colors; switch (figure.type) { case 'lattice': var verticesPerRow = figure.verticesPerRow; var rows = Math.floor(ps.length / verticesPerRow) - 1; var cols = verticesPerRow - 1; for (var i = 0; i < rows; i++) { var q = i * verticesPerRow; for (var j = 0; j < cols; j++, q++) { drawTriangle(data, context, ps[q], ps[q + 1], ps[q + verticesPerRow], cs[q], cs[q + 1], cs[q + verticesPerRow]); drawTriangle(data, context, ps[q + verticesPerRow + 1], ps[q + 1], ps[q + verticesPerRow], cs[q + verticesPerRow + 1], cs[q + 1], cs[q + verticesPerRow]); } } break; case 'triangles': for (var i = 0, ii = ps.length; i < ii; i += 3) { drawTriangle(data, context, ps[i], ps[i + 1], ps[i + 2], cs[i], cs[i + 1], cs[i + 2]); } break; default: error('illigal figure'); break; } } function createMeshCanvas(bounds, combinesScale, coords, colors, figures, backgroundColor) { // we will increase scale on some weird factor to let antialiasing take // care of "rough" edges var EXPECTED_SCALE = 1.1; // MAX_PATTERN_SIZE is used to avoid OOM situation. var MAX_PATTERN_SIZE = 3000; // 10in @ 300dpi shall be enough var offsetX = Math.floor(bounds[0]); var offsetY = Math.floor(bounds[1]); var boundsWidth = Math.ceil(bounds[2]) - offsetX; var boundsHeight = Math.ceil(bounds[3]) - offsetY; var width = Math.min(Math.ceil(Math.abs(boundsWidth * combinesScale[0] * EXPECTED_SCALE)), MAX_PATTERN_SIZE); var height = Math.min(Math.ceil(Math.abs(boundsHeight * combinesScale[1] * EXPECTED_SCALE)), MAX_PATTERN_SIZE); var scaleX = boundsWidth / width; var scaleY = boundsHeight / height; var context = { coords: coords, colors: colors, offsetX: -offsetX, offsetY: -offsetY, scaleX: 1 / scaleX, scaleY: 1 / scaleY }; var canvas; if (WebGLUtils.isEnabled) { canvas = WebGLUtils.drawFigures(width, height, backgroundColor, figures, context); // https://bugzilla.mozilla.org/show_bug.cgi?id=972126 var tmpCanvas = CachedCanvases.getCanvas('mesh', width, height, false); tmpCanvas.context.drawImage(canvas, 0, 0); canvas = tmpCanvas.canvas; } else { var tmpCanvas = CachedCanvases.getCanvas('mesh', width, height, false); var tmpCtx = tmpCanvas.context; var data = tmpCtx.createImageData(width, height); if (backgroundColor) { var bytes = data.data; for (var i = 0, ii = bytes.length; i < ii; i += 4) { bytes[i] = backgroundColor[0]; bytes[i + 1] = backgroundColor[1]; bytes[i + 2] = backgroundColor[2]; bytes[i + 3] = 255; } } for (var i = 0; i < figures.length; i++) { drawFigure(data, figures[i], context); } tmpCtx.putImageData(data, 0, 0); canvas = tmpCanvas.canvas; } return {canvas: canvas, offsetX: offsetX, offsetY: offsetY, scaleX: scaleX, scaleY: scaleY}; } return createMeshCanvas; })(); ShadingIRs.Mesh = { fromIR: function Mesh_fromIR(raw) { var type = raw[1]; var coords = raw[2]; var colors = raw[3]; var figures = raw[4]; var bounds = raw[5]; var matrix = raw[6]; var bbox = raw[7]; var background = raw[8]; return { type: 'Pattern', getPattern: function Mesh_getPattern(ctx, owner, shadingFill) { var combinedScale; // Obtain scale from matrix and current transformation matrix. if (shadingFill) { combinedScale = Util.singularValueDecompose2dScale( ctx.mozCurrentTransform); } else { var matrixScale = Util.singularValueDecompose2dScale(matrix); var curMatrixScale = Util.singularValueDecompose2dScale( owner.baseTransform); combinedScale = [matrixScale[0] * curMatrixScale[0], matrixScale[1] * curMatrixScale[1]]; } // Rasterizing on the main thread since sending/queue large canvases // might cause OOM. var temporaryPatternCanvas = createMeshCanvas(bounds, combinedScale, coords, colors, figures, shadingFill ? null : background); if (!shadingFill) { ctx.setTransform.apply(ctx, owner.baseTransform); if (matrix) { ctx.transform.apply(ctx, matrix); } } ctx.translate(temporaryPatternCanvas.offsetX, temporaryPatternCanvas.offsetY); ctx.scale(temporaryPatternCanvas.scaleX, temporaryPatternCanvas.scaleY); return ctx.createPattern(temporaryPatternCanvas.canvas, 'no-repeat'); } }; } }; ShadingIRs.Dummy = { fromIR: function Dummy_fromIR() { return { type: 'Pattern', getPattern: function Dummy_fromIR_getPattern() { return 'hotpink'; } }; } }; function getShadingPatternFromIR(raw) { var shadingIR = ShadingIRs[raw[0]]; if (!shadingIR) { error('Unknown IR type: ' + raw[0]); } return shadingIR.fromIR(raw); } var TilingPattern = (function TilingPatternClosure() { var PaintType = { COLORED: 1, UNCOLORED: 2 }; var MAX_PATTERN_SIZE = 3000; // 10in @ 300dpi shall be enough function TilingPattern(IR, color, ctx, objs, commonObjs, baseTransform) { this.name = IR[1][0].name; this.operatorList = IR[2]; this.matrix = IR[3] || [1, 0, 0, 1, 0, 0]; this.bbox = IR[4]; this.xstep = IR[5]; this.ystep = IR[6]; this.paintType = IR[7]; this.tilingType = IR[8]; this.color = color; this.objs = objs; this.commonObjs = commonObjs; this.baseTransform = baseTransform; this.type = 'Pattern'; this.ctx = ctx; } TilingPattern.prototype = { createPatternCanvas: function TilinPattern_createPatternCanvas(owner) { var operatorList = this.operatorList; var bbox = this.bbox; var xstep = this.xstep; var ystep = this.ystep; var paintType = this.paintType; var tilingType = this.tilingType; var color = this.color; var objs = this.objs; var commonObjs = this.commonObjs; var ctx = this.ctx; info('TilingType: ' + tilingType); var x0 = bbox[0], y0 = bbox[1], x1 = bbox[2], y1 = bbox[3]; var topLeft = [x0, y0]; // we want the canvas to be as large as the step size var botRight = [x0 + xstep, y0 + ystep]; var width = botRight[0] - topLeft[0]; var height = botRight[1] - topLeft[1]; // Obtain scale from matrix and current transformation matrix. var matrixScale = Util.singularValueDecompose2dScale(this.matrix); var curMatrixScale = Util.singularValueDecompose2dScale( this.baseTransform); var combinedScale = [matrixScale[0] * curMatrixScale[0], matrixScale[1] * curMatrixScale[1]]; // MAX_PATTERN_SIZE is used to avoid OOM situation. // Use width and height values that are as close as possible to the end // result when the pattern is used. Too low value makes the pattern look // blurry. Too large value makes it look too crispy. width = Math.min(Math.ceil(Math.abs(width * combinedScale[0])), MAX_PATTERN_SIZE); height = Math.min(Math.ceil(Math.abs(height * combinedScale[1])), MAX_PATTERN_SIZE); var tmpCanvas = CachedCanvases.getCanvas('pattern', width, height, true); var tmpCtx = tmpCanvas.context; var graphics = new CanvasGraphics(tmpCtx, commonObjs, objs); graphics.groupLevel = owner.groupLevel; this.setFillAndStrokeStyleToContext(tmpCtx, paintType, color); this.setScale(width, height, xstep, ystep); this.transformToScale(graphics); // transform coordinates to pattern space var tmpTranslate = [1, 0, 0, 1, -topLeft[0], -topLeft[1]]; graphics.transform.apply(graphics, tmpTranslate); this.clipBbox(graphics, bbox, x0, y0, x1, y1); graphics.executeOperatorList(operatorList); return tmpCanvas.canvas; }, setScale: function TilingPattern_setScale(width, height, xstep, ystep) { this.scale = [width / xstep, height / ystep]; }, transformToScale: function TilingPattern_transformToScale(graphics) { var scale = this.scale; var tmpScale = [scale[0], 0, 0, scale[1], 0, 0]; graphics.transform.apply(graphics, tmpScale); }, scaleToContext: function TilingPattern_scaleToContext() { var scale = this.scale; this.ctx.scale(1 / scale[0], 1 / scale[1]); }, clipBbox: function clipBbox(graphics, bbox, x0, y0, x1, y1) { if (bbox && isArray(bbox) && 4 == bbox.length) { var bboxWidth = x1 - x0; var bboxHeight = y1 - y0; graphics.rectangle(x0, y0, bboxWidth, bboxHeight); graphics.clip(); graphics.endPath(); } }, setFillAndStrokeStyleToContext: function setFillAndStrokeStyleToContext(context, paintType, color) { switch (paintType) { case PaintType.COLORED: var ctx = this.ctx; context.fillStyle = ctx.fillStyle; context.strokeStyle = ctx.strokeStyle; break; case PaintType.UNCOLORED: var rgbColor = ColorSpace.singletons.rgb.getRgb(color, 0); var cssColor = Util.makeCssRgb(rgbColor); context.fillStyle = cssColor; context.strokeStyle = cssColor; break; default: error('Unsupported paint type: ' + paintType); } }, getPattern: function TilingPattern_getPattern(ctx, owner) { var temporaryPatternCanvas = this.createPatternCanvas(owner); var ctx = this.ctx; ctx.setTransform.apply(ctx, this.baseTransform); ctx.transform.apply(ctx, this.matrix); this.scaleToContext(); return ctx.createPattern(temporaryPatternCanvas, 'repeat'); } }; return TilingPattern; })();