[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
@ -30,6 +30,14 @@ class BaseFilterFactory {
|
|||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addAlphaFilter(map) {
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
addLuminosityFilter(map) {
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
addHighlightHCMFilter(filterName, fgColor, bgColor, newFgColor, newBgColor) {
|
addHighlightHCMFilter(filterName, fgColor, bgColor, newFgColor, newBgColor) {
|
||||||
return "none";
|
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) {
|
function getImageSmoothingEnabled(transform, interpolate) {
|
||||||
// In section 8.9.5.3 of the PDF spec, it's mentioned that the 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.
|
// flag should be used when the image is upscaled.
|
||||||
@ -1556,7 +1440,7 @@ class CanvasGraphics {
|
|||||||
const smask = this.current.activeSMask;
|
const smask = this.current.activeSMask;
|
||||||
const suspendedCtx = this.suspendedCtx;
|
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
|
// Whatever was drawn has been moved to the suspended canvas, now clear it
|
||||||
// out of the current canvas.
|
// out of the current canvas.
|
||||||
this.ctx.save();
|
this.ctx.save();
|
||||||
@ -1565,6 +1449,117 @@ class CanvasGraphics {
|
|||||||
this.ctx.restore();
|
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() {
|
save() {
|
||||||
if (this.inSMaskMode) {
|
if (this.inSMaskMode) {
|
||||||
// SMask mode may be turned on/off causing us to lose graphics state.
|
// SMask mode may be turned on/off causing us to lose graphics state.
|
||||||
|
|||||||
@ -97,6 +97,30 @@ class DOMFilterFactory extends BaseFilterFactory {
|
|||||||
return this.#_defs;
|
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) {
|
addFilter(maps) {
|
||||||
if (!maps) {
|
if (!maps) {
|
||||||
return "none";
|
return "none";
|
||||||
@ -109,29 +133,8 @@ class DOMFilterFactory extends BaseFilterFactory {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tableR, tableG, tableB, key;
|
const [tableR, tableG, tableB] = this.#createTables(maps);
|
||||||
if (maps.length === 1) {
|
const key = maps.length === 1 ? tableR : `${tableR}${tableG}${tableB}`;
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = this.#cache.get(key);
|
value = this.#cache.get(key);
|
||||||
if (value) {
|
if (value) {
|
||||||
@ -233,6 +236,70 @@ class DOMFilterFactory extends BaseFilterFactory {
|
|||||||
return info.url;
|
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) {
|
addHighlightHCMFilter(filterName, fgColor, bgColor, newFgColor, newBgColor) {
|
||||||
const key = `${fgColor}-${bgColor}-${newFgColor}-${newBgColor}`;
|
const key = `${fgColor}-${bgColor}-${newFgColor}-${newBgColor}`;
|
||||||
let info = this.#hcmCache.get(filterName);
|
let info = this.#hcmCache.get(filterName);
|
||||||
@ -341,6 +408,19 @@ class DOMFilterFactory extends BaseFilterFactory {
|
|||||||
this.#id = 0;
|
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) {
|
#addGrayConversion(filter) {
|
||||||
const feColorMatrix = this.#document.createElementNS(
|
const feColorMatrix = this.#document.createElementNS(
|
||||||
SVG_NS,
|
SVG_NS,
|
||||||
@ -381,6 +461,15 @@ class DOMFilterFactory extends BaseFilterFactory {
|
|||||||
this.#appendFeFunc(feComponentTransfer, "feFuncB", bTable);
|
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) {
|
#getRGB(color) {
|
||||||
this.#defs.style.color = color;
|
this.#defs.style.color = color;
|
||||||
return getRGB(getComputedStyle(this.#defs).getPropertyValue("color"));
|
return getRGB(getComputedStyle(this.#defs).getPropertyValue("color"));
|
||||||
|
|||||||
2
test/pdfs/issue17779.pdf.link
Normal file
2
test/pdfs/issue17779.pdf.link
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
https://github.com/mozilla/pdf.js/files/14522359/p95.pdf
|
||||||
|
|
||||||
@ -9957,5 +9957,13 @@
|
|||||||
"id": "51R"
|
"id": "51R"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "issue17779",
|
||||||
|
"file": "pdfs/issue17779.pdf",
|
||||||
|
"md5": "764b72e8e56e22662b321b308254fd2b",
|
||||||
|
"rounds": 1,
|
||||||
|
"link": true,
|
||||||
|
"type": "eq"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user