This rule is already enabled in mozilla-central, and helps avoid some confusing formatting, see https://searchfox.org/mozilla-central/rev/9e45d74b956be046e5021a746b0c8912f1c27318/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js#209-210 With the recent introduction of Prettier some of the existing nested ternary statements became even more difficult to read, since any possibly helpful indentation was removed. This particular ESLint rule wasn't entirely straightforward to enable, and I do recognize that there's a certain amount of subjectivity in the changes being made. Generally, the changes in this patch fall into three categories: - Cases where a value is only clamped to a certain range (the easiest ones to update). - Cases where the values involved are "simple", such as Numbers and Strings, which are re-factored to initialize the variable with the *default* value and only update it when necessary by using `if`/`else if` statements. - Cases with more complex and/or larger values, such as TypedArrays, which are re-factored to let the variable be (implicitly) undefined and where all values are then set through `if`/`else if`/`else` statements. Please find additional details about the ESLint rule at https://eslint.org/docs/rules/no-nested-ternary
597 lines
17 KiB
JavaScript
597 lines
17 KiB
JavaScript
/* 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.
|
|
*/
|
|
|
|
import { FormatError, info, Util } from "../shared/util.js";
|
|
|
|
var ShadingIRs = {};
|
|
|
|
function applyBoundingBox(ctx, bbox) {
|
|
if (!bbox || typeof Path2D === "undefined") {
|
|
return;
|
|
}
|
|
const width = bbox[2] - bbox[0];
|
|
const height = bbox[3] - bbox[1];
|
|
const region = new Path2D();
|
|
region.rect(bbox[0], bbox[1], width, height);
|
|
ctx.clip(region);
|
|
}
|
|
|
|
ShadingIRs.RadialAxial = {
|
|
fromIR: function RadialAxial_fromIR(raw) {
|
|
var type = raw[1];
|
|
var bbox = raw[2];
|
|
var colorStops = raw[3];
|
|
var p0 = raw[4];
|
|
var p1 = raw[5];
|
|
var r0 = raw[6];
|
|
var r1 = raw[7];
|
|
return {
|
|
type: "Pattern",
|
|
getPattern: function RadialAxial_getPattern(ctx) {
|
|
applyBoundingBox(ctx, bbox);
|
|
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;
|
|
for (var y = minY; y <= maxY; y++) {
|
|
if (y < y2) {
|
|
let k;
|
|
if (y < y1) {
|
|
k = 0;
|
|
} else if (y1 === y2) {
|
|
k = 1;
|
|
} else {
|
|
k = (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 {
|
|
let k;
|
|
if (y > y3) {
|
|
k = 1;
|
|
} else if (y2 === y3) {
|
|
k = 0;
|
|
} else {
|
|
k = (y2 - y) / (y2 - y3);
|
|
}
|
|
xa = x2 - (x2 - x3) * k;
|
|
car = c2r - (c2r - c3r) * k;
|
|
cag = c2g - (c2g - c3g) * k;
|
|
cab = c2b - (c2b - c3b) * k;
|
|
}
|
|
|
|
let k;
|
|
if (y < y1) {
|
|
k = 0;
|
|
} else if (y > y3) {
|
|
k = 1;
|
|
} else {
|
|
k = (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++) {
|
|
let k = (xa - x) / (xa - xb);
|
|
if (k < 0) {
|
|
k = 0;
|
|
} else if (k > 1) {
|
|
k = 1;
|
|
}
|
|
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;
|
|
var i, ii;
|
|
switch (figure.type) {
|
|
case "lattice":
|
|
var verticesPerRow = figure.verticesPerRow;
|
|
var rows = Math.floor(ps.length / verticesPerRow) - 1;
|
|
var cols = verticesPerRow - 1;
|
|
for (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 (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:
|
|
throw new Error("illegal figure");
|
|
}
|
|
}
|
|
|
|
function createMeshCanvas(
|
|
bounds,
|
|
combinesScale,
|
|
coords,
|
|
colors,
|
|
figures,
|
|
backgroundColor,
|
|
cachedCanvases,
|
|
webGLContext
|
|
) {
|
|
// 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
|
|
// We need to keep transparent border around our pattern for fill():
|
|
// createPattern with 'no-repeat' will bleed edges across entire area.
|
|
var BORDER_SIZE = 2;
|
|
|
|
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,
|
|
colors,
|
|
offsetX: -offsetX,
|
|
offsetY: -offsetY,
|
|
scaleX: 1 / scaleX,
|
|
scaleY: 1 / scaleY,
|
|
};
|
|
|
|
var paddedWidth = width + BORDER_SIZE * 2;
|
|
var paddedHeight = height + BORDER_SIZE * 2;
|
|
|
|
var canvas, tmpCanvas, i, ii;
|
|
if (webGLContext.isEnabled) {
|
|
canvas = webGLContext.drawFigures({
|
|
width,
|
|
height,
|
|
backgroundColor,
|
|
figures,
|
|
context,
|
|
});
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=972126
|
|
tmpCanvas = cachedCanvases.getCanvas(
|
|
"mesh",
|
|
paddedWidth,
|
|
paddedHeight,
|
|
false
|
|
);
|
|
tmpCanvas.context.drawImage(canvas, BORDER_SIZE, BORDER_SIZE);
|
|
canvas = tmpCanvas.canvas;
|
|
} else {
|
|
tmpCanvas = cachedCanvases.getCanvas(
|
|
"mesh",
|
|
paddedWidth,
|
|
paddedHeight,
|
|
false
|
|
);
|
|
var tmpCtx = tmpCanvas.context;
|
|
|
|
var data = tmpCtx.createImageData(width, height);
|
|
if (backgroundColor) {
|
|
var bytes = data.data;
|
|
for (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 (i = 0; i < figures.length; i++) {
|
|
drawFigure(data, figures[i], context);
|
|
}
|
|
tmpCtx.putImageData(data, BORDER_SIZE, BORDER_SIZE);
|
|
canvas = tmpCanvas.canvas;
|
|
}
|
|
|
|
return {
|
|
canvas,
|
|
offsetX: offsetX - BORDER_SIZE * scaleX,
|
|
offsetY: offsetY - BORDER_SIZE * scaleY,
|
|
scaleX,
|
|
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) {
|
|
applyBoundingBox(ctx, bbox);
|
|
var scale;
|
|
if (shadingFill) {
|
|
scale = Util.singularValueDecompose2dScale(ctx.mozCurrentTransform);
|
|
} else {
|
|
// Obtain scale from matrix and current transformation matrix.
|
|
scale = Util.singularValueDecompose2dScale(owner.baseTransform);
|
|
if (matrix) {
|
|
var matrixScale = Util.singularValueDecompose2dScale(matrix);
|
|
scale = [scale[0] * matrixScale[0], scale[1] * matrixScale[1]];
|
|
}
|
|
}
|
|
|
|
// Rasterizing on the main thread since sending/queue large canvases
|
|
// might cause OOM.
|
|
var temporaryPatternCanvas = createMeshCanvas(
|
|
bounds,
|
|
scale,
|
|
coords,
|
|
colors,
|
|
figures,
|
|
shadingFill ? null : background,
|
|
owner.cachedCanvases,
|
|
owner.webGLContext
|
|
);
|
|
|
|
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) {
|
|
throw new 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, canvasGraphicsFactory, baseTransform) {
|
|
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.canvasGraphicsFactory = canvasGraphicsFactory;
|
|
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 canvasGraphicsFactory = this.canvasGraphicsFactory;
|
|
|
|
info("TilingType: " + tilingType);
|
|
|
|
// A tiling pattern as defined by PDF spec 8.7.2 is a cell whose size is
|
|
// described by bbox, and may repeat regularly by shifting the cell by
|
|
// xstep and ystep.
|
|
// Because the HTML5 canvas API does not support pattern repetition with
|
|
// gaps in between, we use the xstep/ystep instead of the bbox's size.
|
|
//
|
|
// This has the following consequences (similarly for ystep):
|
|
//
|
|
// - If xstep is the same as bbox, then there is no observable difference.
|
|
//
|
|
// - If xstep is larger than bbox, then the pattern canvas is partially
|
|
// empty: the area bounded by bbox is painted, the outside area is void.
|
|
//
|
|
// - If xstep is smaller than bbox, then the pixels between xstep and the
|
|
// bbox boundary will be missing. This is INCORRECT behavior.
|
|
// "Figures on adjacent tiles should not overlap" (PDF spec 8.7.3.1),
|
|
// but overlapping cells without common pixels are still valid.
|
|
// TODO: Fix the implementation, to allow this scenario to be painted
|
|
// correctly.
|
|
|
|
var x0 = bbox[0],
|
|
y0 = bbox[1],
|
|
x1 = bbox[2],
|
|
y1 = bbox[3];
|
|
|
|
// 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],
|
|
];
|
|
|
|
// 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.
|
|
var dimx = this.getSizeAndScale(
|
|
xstep,
|
|
this.ctx.canvas.width,
|
|
combinedScale[0]
|
|
);
|
|
var dimy = this.getSizeAndScale(
|
|
ystep,
|
|
this.ctx.canvas.height,
|
|
combinedScale[1]
|
|
);
|
|
|
|
var tmpCanvas = owner.cachedCanvases.getCanvas(
|
|
"pattern",
|
|
dimx.size,
|
|
dimy.size,
|
|
true
|
|
);
|
|
var tmpCtx = tmpCanvas.context;
|
|
var graphics = canvasGraphicsFactory.createCanvasGraphics(tmpCtx);
|
|
graphics.groupLevel = owner.groupLevel;
|
|
|
|
this.setFillAndStrokeStyleToContext(graphics, paintType, color);
|
|
|
|
graphics.transform(dimx.scale, 0, 0, dimy.scale, 0, 0);
|
|
|
|
// transform coordinates to pattern space
|
|
graphics.transform(1, 0, 0, 1, -x0, -y0);
|
|
|
|
this.clipBbox(graphics, bbox, x0, y0, x1, y1);
|
|
|
|
graphics.executeOperatorList(operatorList);
|
|
|
|
this.ctx.transform(1, 0, 0, 1, x0, y0);
|
|
|
|
// Rescale canvas so that the ctx.createPattern call generates a pattern
|
|
// with the desired size.
|
|
this.ctx.scale(1 / dimx.scale, 1 / dimy.scale);
|
|
return tmpCanvas.canvas;
|
|
},
|
|
|
|
getSizeAndScale: function TilingPattern_getSizeAndScale(
|
|
step,
|
|
realOutputSize,
|
|
scale
|
|
) {
|
|
// xstep / ystep may be negative -- normalize.
|
|
step = Math.abs(step);
|
|
// MAX_PATTERN_SIZE is used to avoid OOM situation.
|
|
// Use the destination canvas's size if it is bigger than the hard-coded
|
|
// limit of MAX_PATTERN_SIZE to avoid clipping patterns that cover the
|
|
// whole canvas.
|
|
var maxSize = Math.max(MAX_PATTERN_SIZE, realOutputSize);
|
|
var size = Math.ceil(step * scale);
|
|
if (size >= maxSize) {
|
|
size = maxSize;
|
|
} else {
|
|
scale = size / step;
|
|
}
|
|
return { scale, size };
|
|
},
|
|
|
|
clipBbox: function clipBbox(graphics, bbox, x0, y0, x1, y1) {
|
|
if (Array.isArray(bbox) && bbox.length === 4) {
|
|
var bboxWidth = x1 - x0;
|
|
var bboxHeight = y1 - y0;
|
|
graphics.ctx.rect(x0, y0, bboxWidth, bboxHeight);
|
|
graphics.clip();
|
|
graphics.endPath();
|
|
}
|
|
},
|
|
|
|
setFillAndStrokeStyleToContext: function setFillAndStrokeStyleToContext(
|
|
graphics,
|
|
paintType,
|
|
color
|
|
) {
|
|
let context = graphics.ctx,
|
|
current = graphics.current;
|
|
switch (paintType) {
|
|
case PaintType.COLORED:
|
|
var ctx = this.ctx;
|
|
context.fillStyle = ctx.fillStyle;
|
|
context.strokeStyle = ctx.strokeStyle;
|
|
current.fillColor = ctx.fillStyle;
|
|
current.strokeColor = ctx.strokeStyle;
|
|
break;
|
|
case PaintType.UNCOLORED:
|
|
var cssColor = Util.makeCssRgb(color[0], color[1], color[2]);
|
|
context.fillStyle = cssColor;
|
|
context.strokeStyle = cssColor;
|
|
// Set color needed by image masks (fixes issues 3226 and 8741).
|
|
current.fillColor = cssColor;
|
|
current.strokeColor = cssColor;
|
|
break;
|
|
default:
|
|
throw new FormatError(`Unsupported paint type: ${paintType}`);
|
|
}
|
|
},
|
|
|
|
getPattern: function TilingPattern_getPattern(ctx, owner) {
|
|
ctx = this.ctx;
|
|
// PDF spec 8.7.2 NOTE 1: pattern's matrix is relative to initial matrix.
|
|
ctx.setTransform.apply(ctx, this.baseTransform);
|
|
ctx.transform.apply(ctx, this.matrix);
|
|
|
|
var temporaryPatternCanvas = this.createPatternCanvas(owner);
|
|
|
|
return ctx.createPattern(temporaryPatternCanvas, "repeat");
|
|
},
|
|
};
|
|
|
|
return TilingPattern;
|
|
})();
|
|
|
|
export { getShadingPatternFromIR, TilingPattern };
|