1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-22 16:18:08 +02:00

[api-minor] Remove the use of (get/put)ImageData when drawing SMasks (bug 1874013)

and implement then in using some SVG filters and composition.
Composing in using destination-in in order to multiply RGB components by
the alpha from the mask isn't perfect: it'd be a way better to natively have
alpha masks support, it induces some small rounding errors and consequently
computed RGB are approximatively correct.
In term of performance, it's a real improvement, for example, the pdf in
issue #17779 is now rendered in few seconds.
There are still some room for improvement, but overall it should be a way
better.
This commit is contained in:
Calixte Denizet 2024-04-30 16:55:58 +02:00
parent 77e2182b8e
commit 82989e6790
5 changed files with 242 additions and 140 deletions

View file

@ -30,6 +30,14 @@ class BaseFilterFactory {
return "none";
}
addAlphaFilter(map) {
return "none";
}
addLuminosityFilter(map) {
return "none";
}
addHighlightHCMFilter(filterName, fgColor, bgColor, newFgColor, newBgColor) {
return "none";
}

View file

@ -796,122 +796,6 @@ function resetCtxToDefault(ctx) {
}
}
function composeSMaskBackdrop(bytes, r0, g0, b0) {
const length = bytes.length;
for (let i = 3; i < length; i += 4) {
const alpha = bytes[i];
if (alpha === 0) {
bytes[i - 3] = r0;
bytes[i - 2] = g0;
bytes[i - 1] = b0;
} else if (alpha < 255) {
const alpha_ = 255 - alpha;
bytes[i - 3] = (bytes[i - 3] * alpha + r0 * alpha_) >> 8;
bytes[i - 2] = (bytes[i - 2] * alpha + g0 * alpha_) >> 8;
bytes[i - 1] = (bytes[i - 1] * alpha + b0 * alpha_) >> 8;
}
}
}
function composeSMaskAlpha(maskData, layerData, transferMap) {
const length = maskData.length;
const scale = 1 / 255;
for (let i = 3; i < length; i += 4) {
const alpha = transferMap ? transferMap[maskData[i]] : maskData[i];
layerData[i] = (layerData[i] * alpha * scale) | 0;
}
}
function composeSMaskLuminosity(maskData, layerData, transferMap) {
const length = maskData.length;
for (let i = 3; i < length; i += 4) {
const y =
maskData[i - 3] * 77 + // * 0.3 / 255 * 0x10000
maskData[i - 2] * 152 + // * 0.59 ....
maskData[i - 1] * 28; // * 0.11 ....
layerData[i] = transferMap
? (layerData[i] * transferMap[y >> 8]) >> 8
: (layerData[i] * y) >> 16;
}
}
function genericComposeSMask(
maskCtx,
layerCtx,
width,
height,
subtype,
backdrop,
transferMap,
layerOffsetX,
layerOffsetY,
maskOffsetX,
maskOffsetY
) {
const hasBackdrop = !!backdrop;
const r0 = hasBackdrop ? backdrop[0] : 0;
const g0 = hasBackdrop ? backdrop[1] : 0;
const b0 = hasBackdrop ? backdrop[2] : 0;
const composeFn =
subtype === "Luminosity" ? composeSMaskLuminosity : composeSMaskAlpha;
// processing image in chunks to save memory
const PIXELS_TO_PROCESS = 1048576;
const chunkSize = Math.min(height, Math.ceil(PIXELS_TO_PROCESS / width));
for (let row = 0; row < height; row += chunkSize) {
const chunkHeight = Math.min(chunkSize, height - row);
const maskData = maskCtx.getImageData(
layerOffsetX - maskOffsetX,
row + (layerOffsetY - maskOffsetY),
width,
chunkHeight
);
const layerData = layerCtx.getImageData(
layerOffsetX,
row + layerOffsetY,
width,
chunkHeight
);
if (hasBackdrop) {
composeSMaskBackdrop(maskData.data, r0, g0, b0);
}
composeFn(maskData.data, layerData.data, transferMap);
layerCtx.putImageData(layerData, layerOffsetX, row + layerOffsetY);
}
}
function composeSMask(ctx, smask, layerCtx, layerBox) {
const layerOffsetX = layerBox[0];
const layerOffsetY = layerBox[1];
const layerWidth = layerBox[2] - layerOffsetX;
const layerHeight = layerBox[3] - layerOffsetY;
if (layerWidth === 0 || layerHeight === 0) {
return;
}
genericComposeSMask(
smask.context,
layerCtx,
layerWidth,
layerHeight,
smask.subtype,
smask.backdrop,
smask.transferMap,
layerOffsetX,
layerOffsetY,
smask.offsetX,
smask.offsetY
);
ctx.save();
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.drawImage(layerCtx.canvas, 0, 0);
ctx.restore();
}
function getImageSmoothingEnabled(transform, interpolate) {
// In section 8.9.5.3 of the PDF spec, it's mentioned that the interpolate
// flag should be used when the image is upscaled.
@ -1556,7 +1440,7 @@ class CanvasGraphics {
const smask = this.current.activeSMask;
const suspendedCtx = this.suspendedCtx;
composeSMask(suspendedCtx, smask, this.ctx, dirtyBox);
this.composeSMask(suspendedCtx, smask, this.ctx, dirtyBox);
// Whatever was drawn has been moved to the suspended canvas, now clear it
// out of the current canvas.
this.ctx.save();
@ -1565,6 +1449,117 @@ class CanvasGraphics {
this.ctx.restore();
}
composeSMask(ctx, smask, layerCtx, layerBox) {
const layerOffsetX = layerBox[0];
const layerOffsetY = layerBox[1];
const layerWidth = layerBox[2] - layerOffsetX;
const layerHeight = layerBox[3] - layerOffsetY;
if (layerWidth === 0 || layerHeight === 0) {
return;
}
this.genericComposeSMask(
smask.context,
layerCtx,
layerWidth,
layerHeight,
smask.subtype,
smask.backdrop,
smask.transferMap,
layerOffsetX,
layerOffsetY,
smask.offsetX,
smask.offsetY
);
ctx.save();
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.drawImage(layerCtx.canvas, 0, 0);
ctx.restore();
}
genericComposeSMask(
maskCtx,
layerCtx,
width,
height,
subtype,
backdrop,
transferMap,
layerOffsetX,
layerOffsetY,
maskOffsetX,
maskOffsetY
) {
let maskCanvas = maskCtx.canvas;
let maskX = layerOffsetX - maskOffsetX;
let maskY = layerOffsetY - maskOffsetY;
if (backdrop) {
if (
maskX < 0 ||
maskY < 0 ||
maskX + width > maskCanvas.width ||
maskY + height > maskCanvas.height
) {
const canvas = this.cachedCanvases.getCanvas(
"maskExtension",
width,
height
);
const ctx = canvas.context;
ctx.drawImage(maskCanvas, -maskX, -maskY);
if (backdrop.some(c => c !== 0)) {
ctx.globalCompositeOperation = "destination-atop";
ctx.fillStyle = Util.makeHexColor(...backdrop);
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeOperation = "source-over";
}
maskCanvas = canvas.canvas;
maskX = maskY = 0;
} else if (backdrop.some(c => c !== 0)) {
maskCtx.save();
maskCtx.globalAlpha = 1;
maskCtx.setTransform(1, 0, 0, 1, 0, 0);
const clip = new Path2D();
clip.rect(maskX, maskY, width, height);
maskCtx.clip(clip);
maskCtx.globalCompositeOperation = "destination-atop";
maskCtx.fillStyle = Util.makeHexColor(...backdrop);
maskCtx.fillRect(maskX, maskY, width, height);
maskCtx.restore();
}
}
layerCtx.save();
layerCtx.globalAlpha = 1;
layerCtx.setTransform(1, 0, 0, 1, 0, 0);
if (subtype === "Alpha" && transferMap) {
layerCtx.filter = this.filterFactory.addAlphaFilter(transferMap);
} else if (subtype === "Luminosity") {
layerCtx.filter = this.filterFactory.addLuminosityFilter(transferMap);
}
const clip = new Path2D();
clip.rect(layerOffsetX, layerOffsetY, width, height);
layerCtx.clip(clip);
layerCtx.globalCompositeOperation = "destination-in";
layerCtx.drawImage(
maskCanvas,
maskX,
maskY,
width,
height,
layerOffsetX,
layerOffsetY,
width,
height
);
layerCtx.restore();
}
save() {
if (this.inSMaskMode) {
// SMask mode may be turned on/off causing us to lose graphics state.

View file

@ -97,6 +97,30 @@ class DOMFilterFactory extends BaseFilterFactory {
return this.#_defs;
}
#createTables(maps) {
if (maps.length === 1) {
const mapR = maps[0];
const buffer = new Array(256);
for (let i = 0; i < 256; i++) {
buffer[i] = mapR[i] / 255;
}
const table = buffer.join(",");
return [table, table, table];
}
const [mapR, mapG, mapB] = maps;
const bufferR = new Array(256);
const bufferG = new Array(256);
const bufferB = new Array(256);
for (let i = 0; i < 256; i++) {
bufferR[i] = mapR[i] / 255;
bufferG[i] = mapG[i] / 255;
bufferB[i] = mapB[i] / 255;
}
return [bufferR.join(","), bufferG.join(","), bufferB.join(",")];
}
addFilter(maps) {
if (!maps) {
return "none";
@ -109,29 +133,8 @@ class DOMFilterFactory extends BaseFilterFactory {
return value;
}
let tableR, tableG, tableB, key;
if (maps.length === 1) {
const mapR = maps[0];
const buffer = new Array(256);
for (let i = 0; i < 256; i++) {
buffer[i] = mapR[i] / 255;
}
key = tableR = tableG = tableB = buffer.join(",");
} else {
const [mapR, mapG, mapB] = maps;
const bufferR = new Array(256);
const bufferG = new Array(256);
const bufferB = new Array(256);
for (let i = 0; i < 256; i++) {
bufferR[i] = mapR[i] / 255;
bufferG[i] = mapG[i] / 255;
bufferB[i] = mapB[i] / 255;
}
tableR = bufferR.join(",");
tableG = bufferG.join(",");
tableB = bufferB.join(",");
key = `${tableR}${tableG}${tableB}`;
}
const [tableR, tableG, tableB] = this.#createTables(maps);
const key = maps.length === 1 ? tableR : `${tableR}${tableG}${tableB}`;
value = this.#cache.get(key);
if (value) {
@ -233,6 +236,70 @@ class DOMFilterFactory extends BaseFilterFactory {
return info.url;
}
addAlphaFilter(map) {
// When a page is zoomed the page is re-drawn but the maps are likely
// the same.
let value = this.#cache.get(map);
if (value) {
return value;
}
const [tableA] = this.#createTables([map]);
const key = `alpha_${tableA}`;
value = this.#cache.get(key);
if (value) {
this.#cache.set(map, value);
return value;
}
const id = `g_${this.#docId}_alpha_map_${this.#id++}`;
const url = `url(#${id})`;
this.#cache.set(map, url);
this.#cache.set(key, url);
const filter = this.#createFilter(id);
this.#addTransferMapAlphaConversion(tableA, filter);
return url;
}
addLuminosityFilter(map) {
// When a page is zoomed the page is re-drawn but the maps are likely
// the same.
let value = this.#cache.get(map || "luminosity");
if (value) {
return value;
}
let tableA, key;
if (map) {
[tableA] = this.#createTables([map]);
key = `luminosity_${tableA}`;
} else {
key = "luminosity";
}
value = this.#cache.get(key);
if (value) {
this.#cache.set(map, value);
return value;
}
const id = `g_${this.#docId}_luminosity_map_${this.#id++}`;
const url = `url(#${id})`;
this.#cache.set(map, url);
this.#cache.set(key, url);
const filter = this.#createFilter(id);
this.#addLuminosityConversion(filter);
if (map) {
this.#addTransferMapAlphaConversion(tableA, filter);
}
return url;
}
addHighlightHCMFilter(filterName, fgColor, bgColor, newFgColor, newBgColor) {
const key = `${fgColor}-${bgColor}-${newFgColor}-${newBgColor}`;
let info = this.#hcmCache.get(filterName);
@ -341,6 +408,19 @@ class DOMFilterFactory extends BaseFilterFactory {
this.#id = 0;
}
#addLuminosityConversion(filter) {
const feColorMatrix = this.#document.createElementNS(
SVG_NS,
"feColorMatrix"
);
feColorMatrix.setAttribute("type", "matrix");
feColorMatrix.setAttribute(
"values",
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0.59 0.11 0 0"
);
filter.append(feColorMatrix);
}
#addGrayConversion(filter) {
const feColorMatrix = this.#document.createElementNS(
SVG_NS,
@ -381,6 +461,15 @@ class DOMFilterFactory extends BaseFilterFactory {
this.#appendFeFunc(feComponentTransfer, "feFuncB", bTable);
}
#addTransferMapAlphaConversion(aTable, filter) {
const feComponentTransfer = this.#document.createElementNS(
SVG_NS,
"feComponentTransfer"
);
filter.append(feComponentTransfer);
this.#appendFeFunc(feComponentTransfer, "feFuncA", aTable);
}
#getRGB(color) {
this.#defs.style.color = color;
return getRGB(getComputedStyle(this.#defs).getPropertyValue("color"));