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:
parent
77e2182b8e
commit
82989e6790
5 changed files with 242 additions and 140 deletions
|
@ -30,6 +30,14 @@ class BaseFilterFactory {
|
|||
return "none";
|
||||
}
|
||||
|
||||
addAlphaFilter(map) {
|
||||
return "none";
|
||||
}
|
||||
|
||||
addLuminosityFilter(map) {
|
||||
return "none";
|
||||
}
|
||||
|
||||
addHighlightHCMFilter(filterName, fgColor, bgColor, newFgColor, newBgColor) {
|
||||
return "none";
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue