Merge pull request #13380 from Snuffleupagus/pattern_helper-class

Re-factor and convert the code in `src/display/pattern_helper.js` to use standard classes
This commit is contained in:
Tim van der Meij 2021-05-16 13:11:04 +02:00 committed by GitHub
commit 8a8a67de3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 469 additions and 484 deletions

View File

@ -27,7 +27,7 @@ import {
Util, Util,
warn, warn,
} from "../shared/util.js"; } from "../shared/util.js";
import { getShadingPatternFromIR, TilingPattern } from "./pattern_helper.js"; import { getShadingPattern, TilingPattern } from "./pattern_helper.js";
// <canvas> contexts store most of the state we need natively. // <canvas> contexts store most of the state we need natively.
// However, PDF needs a bit more state, which we store here. // However, PDF needs a bit more state, which we store here.
@ -1973,7 +1973,7 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
baseTransform baseTransform
); );
} else { } else {
pattern = getShadingPatternFromIR(IR); pattern = getShadingPattern(IR);
} }
return pattern; return pattern;
} }
@ -2007,7 +2007,7 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
const ctx = this.ctx; const ctx = this.ctx;
this.save(); this.save();
const pattern = getShadingPatternFromIR(patternIR); const pattern = getShadingPattern(patternIR);
ctx.fillStyle = pattern.getPattern(ctx, this, true); ctx.fillStyle = pattern.getPattern(ctx, this, true);
const inv = ctx.mozCurrentTransformInverse; const inv = ctx.mozCurrentTransformInverse;

View File

@ -13,9 +13,13 @@
* limitations under the License. * limitations under the License.
*/ */
import { FormatError, info, Util } from "../shared/util.js"; import {
FormatError,
const ShadingIRs = {}; info,
shadow,
unreachable,
Util,
} from "../shared/util.js";
let svgElement; let svgElement;
@ -41,19 +45,32 @@ function applyBoundingBox(ctx, bbox) {
ctx.clip(region); ctx.clip(region);
} }
ShadingIRs.RadialAxial = { class BaseShadingPattern {
fromIR: function RadialAxial_fromIR(raw) { constructor() {
const type = raw[1]; if (this.constructor === BaseShadingPattern) {
const bbox = raw[2]; unreachable("Cannot initialize BaseShadingPattern.");
const colorStops = raw[3]; }
const p0 = raw[4]; }
const p1 = raw[5];
const r0 = raw[6];
const r1 = raw[7];
const matrix = raw[8];
return { getPattern() {
getPattern: function RadialAxial_getPattern(ctx, owner, shadingFill) { unreachable("Abstract method `getPattern` called.");
}
}
class RadialAxialShadingPattern extends BaseShadingPattern {
constructor(IR) {
super();
this._type = IR[1];
this._bbox = IR[2];
this._colorStops = IR[3];
this._p0 = IR[4];
this._p1 = IR[5];
this._r0 = IR[6];
this._r1 = IR[7];
this._matrix = IR[8];
}
getPattern(ctx, owner, shadingFill) {
const tmpCanvas = owner.cachedCanvases.getCanvas( const tmpCanvas = owner.cachedCanvases.getCanvas(
"pattern", "pattern",
ctx.canvas.width, ctx.canvas.width,
@ -68,46 +85,46 @@ ShadingIRs.RadialAxial = {
if (!shadingFill) { if (!shadingFill) {
tmpCtx.setTransform.apply(tmpCtx, owner.baseTransform); tmpCtx.setTransform.apply(tmpCtx, owner.baseTransform);
if (matrix) { if (this._matrix) {
tmpCtx.transform.apply(tmpCtx, matrix); tmpCtx.transform.apply(tmpCtx, this._matrix);
} }
} else { } else {
tmpCtx.setTransform.apply(tmpCtx, ctx.mozCurrentTransform); tmpCtx.setTransform.apply(tmpCtx, ctx.mozCurrentTransform);
} }
applyBoundingBox(tmpCtx, bbox); applyBoundingBox(tmpCtx, this._bbox);
let grad; let grad;
if (type === "axial") { if (this._type === "axial") {
grad = tmpCtx.createLinearGradient(p0[0], p0[1], p1[0], p1[1]); grad = tmpCtx.createLinearGradient(
} else if (type === "radial") { this._p0[0],
this._p0[1],
this._p1[0],
this._p1[1]
);
} else if (this._type === "radial") {
grad = tmpCtx.createRadialGradient( grad = tmpCtx.createRadialGradient(
p0[0], this._p0[0],
p0[1], this._p0[1],
r0, this._r0,
p1[0], this._p1[0],
p1[1], this._p1[1],
r1 this._r1
); );
} }
for (let i = 0, ii = colorStops.length; i < ii; ++i) { for (const colorStop of this._colorStops) {
const c = colorStops[i]; grad.addColorStop(colorStop[0], colorStop[1]);
grad.addColorStop(c[0], c[1]);
} }
tmpCtx.fillStyle = grad; tmpCtx.fillStyle = grad;
tmpCtx.fill(); tmpCtx.fill();
const pattern = ctx.createPattern(tmpCanvas.canvas, "repeat"); const pattern = ctx.createPattern(tmpCanvas.canvas, "repeat");
pattern.setTransform(createMatrix(ctx.mozCurrentTransformInverse)); pattern.setTransform(createMatrix(ctx.mozCurrentTransformInverse));
return pattern; return pattern;
}, }
}; }
},
};
const createMeshCanvas = (function createMeshCanvasClosure() { function drawTriangle(data, context, p1, p2, p3, c1, c2, c3) {
function drawTriangle(data, context, p1, p2, p3, c1, c2, c3) {
// Very basic Gouraud-shaded triangle rasterization algorithm. // Very basic Gouraud-shaded triangle rasterization algorithm.
const coords = context.coords, const coords = context.coords,
colors = context.colors; colors = context.colors;
@ -218,9 +235,9 @@ const createMeshCanvas = (function createMeshCanvasClosure() {
bytes[j++] = 255; bytes[j++] = 255;
} }
} }
} }
function drawFigure(data, figure, context) { function drawFigure(data, figure, context) {
const ps = figure.coords; const ps = figure.coords;
const cs = figure.colors; const cs = figure.colors;
let i, ii; let i, ii;
@ -272,18 +289,21 @@ const createMeshCanvas = (function createMeshCanvasClosure() {
default: default:
throw new Error("illegal figure"); throw new Error("illegal figure");
} }
}
class MeshShadingPattern extends BaseShadingPattern {
constructor(IR) {
super();
this._coords = IR[2];
this._colors = IR[3];
this._figures = IR[4];
this._bounds = IR[5];
this._matrix = IR[6];
this._bbox = IR[7];
this._background = IR[8];
} }
// eslint-disable-next-line no-shadow _createMeshCanvas(combinedScale, backgroundColor, cachedCanvases) {
function createMeshCanvas(
bounds,
combinesScale,
coords,
colors,
figures,
backgroundColor,
cachedCanvases
) {
// we will increase scale on some weird factor to let antialiasing take // we will increase scale on some weird factor to let antialiasing take
// care of "rough" edges // care of "rough" edges
const EXPECTED_SCALE = 1.1; const EXPECTED_SCALE = 1.1;
@ -293,25 +313,25 @@ const createMeshCanvas = (function createMeshCanvasClosure() {
// createPattern with 'no-repeat' will bleed edges across entire area. // createPattern with 'no-repeat' will bleed edges across entire area.
const BORDER_SIZE = 2; const BORDER_SIZE = 2;
const offsetX = Math.floor(bounds[0]); const offsetX = Math.floor(this._bounds[0]);
const offsetY = Math.floor(bounds[1]); const offsetY = Math.floor(this._bounds[1]);
const boundsWidth = Math.ceil(bounds[2]) - offsetX; const boundsWidth = Math.ceil(this._bounds[2]) - offsetX;
const boundsHeight = Math.ceil(bounds[3]) - offsetY; const boundsHeight = Math.ceil(this._bounds[3]) - offsetY;
const width = Math.min( const width = Math.min(
Math.ceil(Math.abs(boundsWidth * combinesScale[0] * EXPECTED_SCALE)), Math.ceil(Math.abs(boundsWidth * combinedScale[0] * EXPECTED_SCALE)),
MAX_PATTERN_SIZE MAX_PATTERN_SIZE
); );
const height = Math.min( const height = Math.min(
Math.ceil(Math.abs(boundsHeight * combinesScale[1] * EXPECTED_SCALE)), Math.ceil(Math.abs(boundsHeight * combinedScale[1] * EXPECTED_SCALE)),
MAX_PATTERN_SIZE MAX_PATTERN_SIZE
); );
const scaleX = boundsWidth / width; const scaleX = boundsWidth / width;
const scaleY = boundsHeight / height; const scaleY = boundsHeight / height;
const context = { const context = {
coords, coords: this._coords,
colors, colors: this._colors,
offsetX: -offsetX, offsetX: -offsetX,
offsetY: -offsetY, offsetY: -offsetY,
scaleX: 1 / scaleX, scaleX: 1 / scaleX,
@ -339,8 +359,8 @@ const createMeshCanvas = (function createMeshCanvasClosure() {
bytes[i + 3] = 255; bytes[i + 3] = 255;
} }
} }
for (let i = 0, ii = figures.length; i < ii; i++) { for (const figure of this._figures) {
drawFigure(data, figures[i], context); drawFigure(data, figure, context);
} }
tmpCtx.putImageData(data, BORDER_SIZE, BORDER_SIZE); tmpCtx.putImageData(data, BORDER_SIZE, BORDER_SIZE);
const canvas = tmpCanvas.canvas; const canvas = tmpCanvas.canvas;
@ -353,50 +373,33 @@ const createMeshCanvas = (function createMeshCanvasClosure() {
scaleY, scaleY,
}; };
} }
return createMeshCanvas;
})();
ShadingIRs.Mesh = { getPattern(ctx, owner, shadingFill) {
fromIR: function Mesh_fromIR(raw) { applyBoundingBox(ctx, this._bbox);
// var type = raw[1];
const coords = raw[2];
const colors = raw[3];
const figures = raw[4];
const bounds = raw[5];
const matrix = raw[6];
const bbox = raw[7];
const background = raw[8];
return {
getPattern: function Mesh_getPattern(ctx, owner, shadingFill) {
applyBoundingBox(ctx, bbox);
let scale; let scale;
if (shadingFill) { if (shadingFill) {
scale = Util.singularValueDecompose2dScale(ctx.mozCurrentTransform); scale = Util.singularValueDecompose2dScale(ctx.mozCurrentTransform);
} else { } else {
// Obtain scale from matrix and current transformation matrix. // Obtain scale from matrix and current transformation matrix.
scale = Util.singularValueDecompose2dScale(owner.baseTransform); scale = Util.singularValueDecompose2dScale(owner.baseTransform);
if (matrix) { if (this._matrix) {
const matrixScale = Util.singularValueDecompose2dScale(matrix); const matrixScale = Util.singularValueDecompose2dScale(this._matrix);
scale = [scale[0] * matrixScale[0], scale[1] * matrixScale[1]]; scale = [scale[0] * matrixScale[0], scale[1] * matrixScale[1]];
} }
} }
// Rasterizing on the main thread since sending/queue large canvases // Rasterizing on the main thread since sending/queue large canvases
// might cause OOM. // might cause OOM.
const temporaryPatternCanvas = createMeshCanvas( const temporaryPatternCanvas = this._createMeshCanvas(
bounds,
scale, scale,
coords, shadingFill ? null : this._background,
colors,
figures,
shadingFill ? null : background,
owner.cachedCanvases owner.cachedCanvases
); );
if (!shadingFill) { if (!shadingFill) {
ctx.setTransform.apply(ctx, owner.baseTransform); ctx.setTransform.apply(ctx, owner.baseTransform);
if (matrix) { if (this._matrix) {
ctx.transform.apply(ctx, matrix); ctx.transform.apply(ctx, this._matrix);
} }
} }
@ -407,42 +410,39 @@ ShadingIRs.Mesh = {
ctx.scale(temporaryPatternCanvas.scaleX, temporaryPatternCanvas.scaleY); ctx.scale(temporaryPatternCanvas.scaleX, temporaryPatternCanvas.scaleY);
return ctx.createPattern(temporaryPatternCanvas.canvas, "no-repeat"); return ctx.createPattern(temporaryPatternCanvas.canvas, "no-repeat");
},
};
},
};
ShadingIRs.Dummy = {
fromIR: function Dummy_fromIR() {
return {
getPattern: function Dummy_fromIR_getPattern() {
return "hotpink";
},
};
},
};
function getShadingPatternFromIR(raw) {
const shadingIR = ShadingIRs[raw[0]];
if (!shadingIR) {
throw new Error(`Unknown IR type: ${raw[0]}`);
} }
return shadingIR.fromIR(raw);
} }
/** class DummyShadingPattern extends BaseShadingPattern {
* @type {any} getPattern() {
*/ return "hotpink";
const TilingPattern = (function TilingPatternClosure() { }
const PaintType = { }
function getShadingPattern(IR) {
switch (IR[0]) {
case "RadialAxial":
return new RadialAxialShadingPattern(IR);
case "Mesh":
return new MeshShadingPattern(IR);
case "Dummy":
return new DummyShadingPattern();
}
throw new Error(`Unknown IR type: ${IR[0]}`);
}
const PaintType = {
COLORED: 1, COLORED: 1,
UNCOLORED: 2, UNCOLORED: 2,
}; };
const MAX_PATTERN_SIZE = 3000; // 10in @ 300dpi shall be enough class TilingPattern {
// 10in @ 300dpi shall be enough.
static get MAX_PATTERN_SIZE() {
return shadow(this, "MAX_PATTERN_SIZE", 3000);
}
// eslint-disable-next-line no-shadow constructor(IR, color, ctx, canvasGraphicsFactory, baseTransform) {
function TilingPattern(IR, color, ctx, canvasGraphicsFactory, baseTransform) {
this.operatorList = IR[2]; this.operatorList = IR[2];
this.matrix = IR[3] || [1, 0, 0, 1, 0, 0]; this.matrix = IR[3] || [1, 0, 0, 1, 0, 0];
this.bbox = IR[4]; this.bbox = IR[4];
@ -451,13 +451,12 @@ const TilingPattern = (function TilingPatternClosure() {
this.paintType = IR[7]; this.paintType = IR[7];
this.tilingType = IR[8]; this.tilingType = IR[8];
this.color = color; this.color = color;
this.ctx = ctx;
this.canvasGraphicsFactory = canvasGraphicsFactory; this.canvasGraphicsFactory = canvasGraphicsFactory;
this.baseTransform = baseTransform; this.baseTransform = baseTransform;
this.ctx = ctx;
} }
TilingPattern.prototype = { createPatternCanvas(owner) {
createPatternCanvas: function TilinPattern_createPatternCanvas(owner) {
const operatorList = this.operatorList; const operatorList = this.operatorList;
const bbox = this.bbox; const bbox = this.bbox;
const xstep = this.xstep; const xstep = this.xstep;
@ -545,20 +544,16 @@ const TilingPattern = (function TilingPatternClosure() {
scaleX: dimx.scale, scaleX: dimx.scale,
scaleY: dimy.scale, scaleY: dimy.scale,
}; };
}, }
getSizeAndScale: function TilingPattern_getSizeAndScale( getSizeAndScale(step, realOutputSize, scale) {
step,
realOutputSize,
scale
) {
// xstep / ystep may be negative -- normalize. // xstep / ystep may be negative -- normalize.
step = Math.abs(step); step = Math.abs(step);
// MAX_PATTERN_SIZE is used to avoid OOM situation. // MAX_PATTERN_SIZE is used to avoid OOM situation.
// Use the destination canvas's size if it is bigger than the hard-coded // 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 // limit of MAX_PATTERN_SIZE to avoid clipping patterns that cover the
// whole canvas. // whole canvas.
const maxSize = Math.max(MAX_PATTERN_SIZE, realOutputSize); const maxSize = Math.max(TilingPattern.MAX_PATTERN_SIZE, realOutputSize);
let size = Math.ceil(step * scale); let size = Math.ceil(step * scale);
if (size >= maxSize) { if (size >= maxSize) {
size = maxSize; size = maxSize;
@ -566,9 +561,9 @@ const TilingPattern = (function TilingPatternClosure() {
scale = size / step; scale = size / step;
} }
return { scale, size }; return { scale, size };
}, }
clipBbox: function clipBbox(graphics, bbox, x0, y0, x1, y1) { clipBbox(graphics, bbox, x0, y0, x1, y1) {
if (Array.isArray(bbox) && bbox.length === 4) { if (Array.isArray(bbox) && bbox.length === 4) {
const bboxWidth = x1 - x0; const bboxWidth = x1 - x0;
const bboxHeight = y1 - y0; const bboxHeight = y1 - y0;
@ -576,13 +571,9 @@ const TilingPattern = (function TilingPatternClosure() {
graphics.clip(); graphics.clip();
graphics.endPath(); graphics.endPath();
} }
}, }
setFillAndStrokeStyleToContext: function setFillAndStrokeStyleToContext( setFillAndStrokeStyleToContext(graphics, paintType, color) {
graphics,
paintType,
color
) {
const context = graphics.ctx, const context = graphics.ctx,
current = graphics.current; current = graphics.current;
switch (paintType) { switch (paintType) {
@ -604,9 +595,9 @@ const TilingPattern = (function TilingPatternClosure() {
default: default:
throw new FormatError(`Unsupported paint type: ${paintType}`); throw new FormatError(`Unsupported paint type: ${paintType}`);
} }
}, }
getPattern: function TilingPattern_getPattern(ctx, owner, shadingFill) { getPattern(ctx, owner, shadingFill) {
ctx = this.ctx; ctx = this.ctx;
// PDF spec 8.7.2 NOTE 1: pattern's matrix is relative to initial matrix. // PDF spec 8.7.2 NOTE 1: pattern's matrix is relative to initial matrix.
let matrix = ctx.mozCurrentTransformInverse; let matrix = ctx.mozCurrentTransformInverse;
@ -627,17 +618,11 @@ const TilingPattern = (function TilingPatternClosure() {
1 / temporaryPatternCanvas.scaleY 1 / temporaryPatternCanvas.scaleY
); );
const pattern = ctx.createPattern( const pattern = ctx.createPattern(temporaryPatternCanvas.canvas, "repeat");
temporaryPatternCanvas.canvas,
"repeat"
);
pattern.setTransform(domMatrix); pattern.setTransform(domMatrix);
return pattern; return pattern;
}, }
}; }
return TilingPattern; export { getShadingPattern, TilingPattern };
})();
export { getShadingPatternFromIR, TilingPattern };