Create the css color to use with the canvas in the worker

It slightly reduces the time spent to draw and the memory used.
This commit is contained in:
Calixte Denizet 2025-05-18 19:58:21 +02:00
parent 60574fb7e3
commit 5789afd3f8
7 changed files with 82 additions and 83 deletions

View File

@ -21,6 +21,7 @@ import {
MathClamp,
shadow,
unreachable,
Util,
warn,
} from "../shared/util.js";
import { BaseStream } from "./base_stream.js";
@ -116,6 +117,8 @@ function copyRgbaImage(src, dest, alpha01) {
}
class ColorSpace {
static #rgbBuf = new Uint8ClampedArray(3);
constructor(name, numComps) {
if (
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
@ -132,10 +135,14 @@ class ColorSpace {
* located in the src array starting from the srcOffset. Returns the array
* of the rgb components, each value ranging from [0,255].
*/
getRgb(src, srcOffset) {
const rgb = new Uint8ClampedArray(3);
this.getRgbItem(src, srcOffset, rgb, 0);
return rgb;
getRgb(src, srcOffset, output = new Uint8ClampedArray(3)) {
this.getRgbItem(src, srcOffset, output, 0);
return output;
}
getRgbHex(src, srcOffset) {
const buffer = this.getRgb(src, srcOffset, ColorSpace.#rgbBuf);
return Util.makeHexColor(buffer[0], buffer[1], buffer[2]);
}
/**

View File

@ -507,7 +507,7 @@ class PartialEvaluator {
if (smask?.backdrop) {
colorSpace ||= ColorSpaceUtils.rgb;
smask.backdrop = colorSpace.getRgb(smask.backdrop, 0);
smask.backdrop = colorSpace.getRgbHex(smask.backdrop, 0);
}
operatorList.addOp(OPS.beginGroup, [groupOptions]);
@ -1546,7 +1546,7 @@ class PartialEvaluator {
localTilingPatternCache.getByRef(rawPattern);
if (localTilingPattern) {
try {
const color = cs.base ? cs.base.getRgb(args, 0) : null;
const color = cs.base ? cs.base.getRgbHex(args, 0) : null;
const tilingPatternIR = getTilingPatternIR(
localTilingPattern.operatorListIR,
localTilingPattern.dict,
@ -1565,7 +1565,7 @@ class PartialEvaluator {
const typeNum = dict.get("PatternType");
if (typeNum === PatternType.TILING) {
const color = cs.base ? cs.base.getRgb(args, 0) : null;
const color = cs.base ? cs.base.getRgbHex(args, 0) : null;
return this.handleTilingType(
fn,
color,
@ -2000,47 +2000,47 @@ class PartialEvaluator {
}
case OPS.setFillColor:
cs = stateManager.state.fillColorSpace;
args = cs.getRgb(args, 0);
args = [cs.getRgbHex(args, 0)];
fn = OPS.setFillRGBColor;
break;
case OPS.setStrokeColor:
cs = stateManager.state.strokeColorSpace;
args = cs.getRgb(args, 0);
args = [cs.getRgbHex(args, 0)];
fn = OPS.setStrokeRGBColor;
break;
case OPS.setFillGray:
stateManager.state.fillColorSpace = ColorSpaceUtils.gray;
args = ColorSpaceUtils.gray.getRgb(args, 0);
args = [ColorSpaceUtils.gray.getRgbHex(args, 0)];
fn = OPS.setFillRGBColor;
break;
case OPS.setStrokeGray:
stateManager.state.strokeColorSpace = ColorSpaceUtils.gray;
args = ColorSpaceUtils.gray.getRgb(args, 0);
args = [ColorSpaceUtils.gray.getRgbHex(args, 0)];
fn = OPS.setStrokeRGBColor;
break;
case OPS.setFillCMYKColor:
stateManager.state.fillColorSpace = ColorSpaceUtils.cmyk;
args = ColorSpaceUtils.cmyk.getRgb(args, 0);
args = [ColorSpaceUtils.cmyk.getRgbHex(args, 0)];
fn = OPS.setFillRGBColor;
break;
case OPS.setStrokeCMYKColor:
stateManager.state.strokeColorSpace = ColorSpaceUtils.cmyk;
args = ColorSpaceUtils.cmyk.getRgb(args, 0);
args = [ColorSpaceUtils.cmyk.getRgbHex(args, 0)];
fn = OPS.setStrokeRGBColor;
break;
case OPS.setFillRGBColor:
stateManager.state.fillColorSpace = ColorSpaceUtils.rgb;
args = ColorSpaceUtils.rgb.getRgb(args, 0);
args = [ColorSpaceUtils.rgb.getRgbHex(args, 0)];
break;
case OPS.setStrokeRGBColor:
stateManager.state.strokeColorSpace = ColorSpaceUtils.rgb;
args = ColorSpaceUtils.rgb.getRgb(args, 0);
args = [ColorSpaceUtils.rgb.getRgbHex(args, 0)];
break;
case OPS.setFillColorN:
cs = stateManager.state.patternFillColorSpace;
if (!cs) {
if (isNumberArray(args, null)) {
args = ColorSpaceUtils.gray.getRgb(args, 0);
args = [ColorSpaceUtils.gray.getRgbHex(args, 0)];
fn = OPS.setFillRGBColor;
break;
}
@ -2065,14 +2065,14 @@ class PartialEvaluator {
);
return;
}
args = cs.getRgb(args, 0);
args = [cs.getRgbHex(args, 0)];
fn = OPS.setFillRGBColor;
break;
case OPS.setStrokeColorN:
cs = stateManager.state.patternStrokeColorSpace;
if (!cs) {
if (isNumberArray(args, null)) {
args = ColorSpaceUtils.gray.getRgb(args, 0);
args = [ColorSpaceUtils.gray.getRgbHex(args, 0)];
fn = OPS.setStrokeRGBColor;
break;
}
@ -2097,7 +2097,7 @@ class PartialEvaluator {
);
return;
}
args = cs.getRgb(args, 0);
args = [cs.getRgbHex(args, 0)];
fn = OPS.setStrokeRGBColor;
break;

View File

@ -198,19 +198,20 @@ class RadialAxialShading extends BaseShading {
const color = new Float32Array(cs.numComps),
ratio = new Float32Array(1);
let rgbColor;
let iBase = 0;
ratio[0] = t0;
fn(ratio, 0, color, 0);
let rgbBase = cs.getRgb(color, 0);
const cssColorBase = Util.makeHexColor(rgbBase[0], rgbBase[1], rgbBase[2]);
colorStops.push([0, cssColorBase]);
const rgbBuffer = new Uint8ClampedArray(3);
cs.getRgb(color, 0, rgbBuffer);
let [rBase, gBase, bBase] = rgbBuffer;
colorStops.push([0, Util.makeHexColor(rBase, gBase, bBase)]);
let iPrev = 1;
ratio[0] = t0 + step;
fn(ratio, 0, color, 0);
let rgbPrev = cs.getRgb(color, 0);
cs.getRgb(color, 0, rgbBuffer);
let [rPrev, gPrev, bPrev] = rgbBuffer;
// Slopes are rise / run.
// A max slope is from the least value the base component could have been
@ -221,28 +222,29 @@ class RadialAxialShading extends BaseShading {
// so the conservative deltas are +-1 (+-.5 for base and -+.5 for current).
// The run is iPrev - iBase = 1, so omitted.
let maxSlopeR = rgbPrev[0] - rgbBase[0] + 1;
let maxSlopeG = rgbPrev[1] - rgbBase[1] + 1;
let maxSlopeB = rgbPrev[2] - rgbBase[2] + 1;
let minSlopeR = rgbPrev[0] - rgbBase[0] - 1;
let minSlopeG = rgbPrev[1] - rgbBase[1] - 1;
let minSlopeB = rgbPrev[2] - rgbBase[2] - 1;
let maxSlopeR = rPrev - rBase + 1;
let maxSlopeG = gPrev - gBase + 1;
let maxSlopeB = bPrev - bBase + 1;
let minSlopeR = rPrev - rBase - 1;
let minSlopeG = gPrev - gBase - 1;
let minSlopeB = bPrev - bBase - 1;
for (let i = 2; i < NUMBER_OF_SAMPLES; i++) {
ratio[0] = t0 + i * step;
fn(ratio, 0, color, 0);
rgbColor = cs.getRgb(color, 0);
cs.getRgb(color, 0, rgbBuffer);
const [r, g, b] = rgbBuffer;
// Keep going if the maximum minimum slope <= the minimum maximum slope.
// Otherwise add a rgbPrev color stop and make it the new base.
const run = i - iBase;
maxSlopeR = Math.min(maxSlopeR, (rgbColor[0] - rgbBase[0] + 1) / run);
maxSlopeG = Math.min(maxSlopeG, (rgbColor[1] - rgbBase[1] + 1) / run);
maxSlopeB = Math.min(maxSlopeB, (rgbColor[2] - rgbBase[2] + 1) / run);
minSlopeR = Math.max(minSlopeR, (rgbColor[0] - rgbBase[0] - 1) / run);
minSlopeG = Math.max(minSlopeG, (rgbColor[1] - rgbBase[1] - 1) / run);
minSlopeB = Math.max(minSlopeB, (rgbColor[2] - rgbBase[2] - 1) / run);
maxSlopeR = Math.min(maxSlopeR, (r - rBase + 1) / run);
maxSlopeG = Math.min(maxSlopeG, (g - gBase + 1) / run);
maxSlopeB = Math.min(maxSlopeB, (b - bBase + 1) / run);
minSlopeR = Math.max(minSlopeR, (r - rBase - 1) / run);
minSlopeG = Math.max(minSlopeG, (g - gBase - 1) / run);
minSlopeB = Math.max(minSlopeB, (b - bBase - 1) / run);
const slopesExist =
minSlopeR <= maxSlopeR &&
@ -250,34 +252,36 @@ class RadialAxialShading extends BaseShading {
minSlopeB <= maxSlopeB;
if (!slopesExist) {
const cssColor = Util.makeHexColor(rgbPrev[0], rgbPrev[1], rgbPrev[2]);
const cssColor = Util.makeHexColor(rPrev, gPrev, bPrev);
colorStops.push([iPrev / NUMBER_OF_SAMPLES, cssColor]);
// TODO: When fn frequency is high (iPrev - iBase === 1 twice in a row),
// send the color space and function to do the sampling display side.
// The run is i - iPrev = 1, so omitted.
maxSlopeR = rgbColor[0] - rgbPrev[0] + 1;
maxSlopeG = rgbColor[1] - rgbPrev[1] + 1;
maxSlopeB = rgbColor[2] - rgbPrev[2] + 1;
minSlopeR = rgbColor[0] - rgbPrev[0] - 1;
minSlopeG = rgbColor[1] - rgbPrev[1] - 1;
minSlopeB = rgbColor[2] - rgbPrev[2] - 1;
maxSlopeR = r - rPrev + 1;
maxSlopeG = g - gPrev + 1;
maxSlopeB = b - bPrev + 1;
minSlopeR = r - rPrev - 1;
minSlopeG = g - gPrev - 1;
minSlopeB = b - bPrev - 1;
iBase = iPrev;
rgbBase = rgbPrev;
rBase = rPrev;
gBase = gPrev;
bBase = bPrev;
}
iPrev = i;
rgbPrev = rgbColor;
rPrev = r;
gPrev = g;
bPrev = b;
}
const cssColor = Util.makeHexColor(rgbPrev[0], rgbPrev[1], rgbPrev[2]);
colorStops.push([1, cssColor]);
colorStops.push([1, Util.makeHexColor(rPrev, gPrev, bPrev)]);
let background = "transparent";
if (dict.has("Background")) {
rgbColor = cs.getRgb(dict.get("Background"), 0);
background = Util.makeHexColor(rgbColor[0], rgbColor[1], rgbColor[2]);
background = cs.getRgbHex(dict.get("Background"), 0);
}
if (!extendStart) {

View File

@ -1311,7 +1311,6 @@ class CanvasGraphics {
let maskY = layerOffsetY - maskOffsetY;
if (backdrop) {
const backdropRGB = Util.makeHexColor(...backdrop);
if (
maskX < 0 ||
maskY < 0 ||
@ -1326,7 +1325,7 @@ class CanvasGraphics {
const ctx = canvas.context;
ctx.drawImage(maskCanvas, -maskX, -maskY);
ctx.globalCompositeOperation = "destination-atop";
ctx.fillStyle = backdropRGB;
ctx.fillStyle = backdrop;
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeOperation = "source-over";
@ -1340,7 +1339,7 @@ class CanvasGraphics {
clip.rect(maskX, maskY, width, height);
maskCtx.clip(clip);
maskCtx.globalCompositeOperation = "destination-atop";
maskCtx.fillStyle = backdropRGB;
maskCtx.fillStyle = backdrop;
maskCtx.fillRect(maskX, maskY, width, height);
maskCtx.restore();
}
@ -2193,12 +2192,8 @@ class CanvasGraphics {
this.current.patternFill = true;
}
setStrokeRGBColor(r, g, b) {
this.ctx.strokeStyle = this.current.strokeColor = Util.makeHexColor(
r,
g,
b
);
setStrokeRGBColor(color) {
this.ctx.strokeStyle = this.current.strokeColor = color;
this.current.patternStroke = false;
}
@ -2207,8 +2202,8 @@ class CanvasGraphics {
this.current.patternStroke = false;
}
setFillRGBColor(r, g, b) {
this.ctx.fillStyle = this.current.fillColor = Util.makeHexColor(r, g, b);
setFillRGBColor(color) {
this.ctx.fillStyle = this.current.fillColor = color;
this.current.patternFill = false;
}

View File

@ -694,19 +694,14 @@ class TilingPattern {
current = graphics.current;
switch (paintType) {
case PaintType.COLORED:
const ctx = this.ctx;
context.fillStyle = ctx.fillStyle;
context.strokeStyle = ctx.strokeStyle;
current.fillColor = ctx.fillStyle;
current.strokeColor = ctx.strokeStyle;
const { fillStyle, strokeStyle } = this.ctx;
context.fillStyle = current.fillColor = fillStyle;
context.strokeStyle = current.strokeColor = strokeStyle;
break;
case PaintType.UNCOLORED:
const cssColor = Util.makeHexColor(color[0], color[1], color[2]);
context.fillStyle = cssColor;
context.strokeStyle = cssColor;
context.fillStyle = context.strokeStyle = color;
// Set color needed by image masks (fixes issues 3226 and 8741).
current.fillColor = cssColor;
current.strokeColor = cssColor;
current.fillColor = current.strokeColor = color;
break;
default:
throw new FormatError(`Unsupported paint type: ${paintType}`);

View File

@ -1783,7 +1783,7 @@ describe("annotation", function () {
[1, 0, 0, 1, 0, 0],
false,
]);
expect(opList.argsArray[1]).toEqual(new Uint8ClampedArray([26, 51, 76]));
expect(opList.argsArray[1]).toEqual(["#1a334c"]);
});
it("should render auto-sized text for printing", async function () {
@ -2664,7 +2664,7 @@ describe("annotation", function () {
[1, 0, 0, 1, 0, 0],
false,
]);
expect(opList1.argsArray[1]).toEqual(new Uint8ClampedArray([26, 51, 76]));
expect(opList1.argsArray[1]).toEqual(["#1a334c"]);
annotationStorage.set(annotation.data.id, { value: false });
@ -2687,7 +2687,7 @@ describe("annotation", function () {
[1, 0, 0, 1, 0, 0],
false,
]);
expect(opList2.argsArray[1]).toEqual(new Uint8ClampedArray([76, 51, 26]));
expect(opList2.argsArray[1]).toEqual(["#4c331a"]);
});
it("should render checkboxes for printing twice", async function () {
@ -2748,9 +2748,7 @@ describe("annotation", function () {
[1, 0, 0, 1, 0, 0],
false,
]);
expect(opList.argsArray[1]).toEqual(
new Uint8ClampedArray([26, 51, 76])
);
expect(opList.argsArray[1]).toEqual(["#1a334c"]);
}
});
@ -2809,7 +2807,7 @@ describe("annotation", function () {
[1, 0, 0, 1, 0, 0],
false,
]);
expect(opList.argsArray[1]).toEqual(new Uint8ClampedArray([26, 51, 76]));
expect(opList.argsArray[1]).toEqual(["#1a334c"]);
});
it("should save checkboxes", async function () {
@ -3052,7 +3050,7 @@ describe("annotation", function () {
[1, 0, 0, 1, 0, 0],
false,
]);
expect(opList1.argsArray[1]).toEqual(new Uint8ClampedArray([26, 51, 76]));
expect(opList1.argsArray[1]).toEqual(["#1a334c"]);
annotationStorage.set(annotation.data.id, { value: false });
@ -3075,7 +3073,7 @@ describe("annotation", function () {
[1, 0, 0, 1, 0, 0],
false,
]);
expect(opList2.argsArray[1]).toEqual(new Uint8ClampedArray([76, 51, 26]));
expect(opList2.argsArray[1]).toEqual(["#4c331a"]);
});
it("should render radio buttons for printing using normal appearance", async function () {
@ -3134,7 +3132,7 @@ describe("annotation", function () {
[1, 0, 0, 1, 0, 0],
false,
]);
expect(opList.argsArray[1]).toEqual(new Uint8ClampedArray([76, 51, 26]));
expect(opList.argsArray[1]).toEqual(["#4c331a"]);
});
it("should save radio buttons", async function () {
@ -4677,7 +4675,7 @@ describe("annotation", function () {
// LineJoin.
expect(opList.argsArray[3]).toEqual([1]);
// Color.
expect(opList.argsArray[4]).toEqual(new Uint8ClampedArray([0, 255, 0]));
expect(opList.argsArray[4]).toEqual(["#00ff00"]);
// Path.
expect(opList.argsArray[5][0]).toEqual(OPS.stroke);
expect(opList.argsArray[5][1]).toEqual([

View File

@ -798,7 +798,7 @@ describe("api", function () {
]);
expect(opList.argsArray).toEqual([
[0.5],
new Uint8ClampedArray([255, 0, 0]),
["#ff0000"],
[
OPS.closeStroke,
[