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, MathClamp,
shadow, shadow,
unreachable, unreachable,
Util,
warn, warn,
} from "../shared/util.js"; } from "../shared/util.js";
import { BaseStream } from "./base_stream.js"; import { BaseStream } from "./base_stream.js";
@ -116,6 +117,8 @@ function copyRgbaImage(src, dest, alpha01) {
} }
class ColorSpace { class ColorSpace {
static #rgbBuf = new Uint8ClampedArray(3);
constructor(name, numComps) { constructor(name, numComps) {
if ( if (
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
@ -132,10 +135,14 @@ class ColorSpace {
* located in the src array starting from the srcOffset. Returns the array * located in the src array starting from the srcOffset. Returns the array
* of the rgb components, each value ranging from [0,255]. * of the rgb components, each value ranging from [0,255].
*/ */
getRgb(src, srcOffset) { getRgb(src, srcOffset, output = new Uint8ClampedArray(3)) {
const rgb = new Uint8ClampedArray(3); this.getRgbItem(src, srcOffset, output, 0);
this.getRgbItem(src, srcOffset, rgb, 0); return output;
return rgb; }
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) { if (smask?.backdrop) {
colorSpace ||= ColorSpaceUtils.rgb; colorSpace ||= ColorSpaceUtils.rgb;
smask.backdrop = colorSpace.getRgb(smask.backdrop, 0); smask.backdrop = colorSpace.getRgbHex(smask.backdrop, 0);
} }
operatorList.addOp(OPS.beginGroup, [groupOptions]); operatorList.addOp(OPS.beginGroup, [groupOptions]);
@ -1546,7 +1546,7 @@ class PartialEvaluator {
localTilingPatternCache.getByRef(rawPattern); localTilingPatternCache.getByRef(rawPattern);
if (localTilingPattern) { if (localTilingPattern) {
try { try {
const color = cs.base ? cs.base.getRgb(args, 0) : null; const color = cs.base ? cs.base.getRgbHex(args, 0) : null;
const tilingPatternIR = getTilingPatternIR( const tilingPatternIR = getTilingPatternIR(
localTilingPattern.operatorListIR, localTilingPattern.operatorListIR,
localTilingPattern.dict, localTilingPattern.dict,
@ -1565,7 +1565,7 @@ class PartialEvaluator {
const typeNum = dict.get("PatternType"); const typeNum = dict.get("PatternType");
if (typeNum === PatternType.TILING) { 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( return this.handleTilingType(
fn, fn,
color, color,
@ -2000,47 +2000,47 @@ class PartialEvaluator {
} }
case OPS.setFillColor: case OPS.setFillColor:
cs = stateManager.state.fillColorSpace; cs = stateManager.state.fillColorSpace;
args = cs.getRgb(args, 0); args = [cs.getRgbHex(args, 0)];
fn = OPS.setFillRGBColor; fn = OPS.setFillRGBColor;
break; break;
case OPS.setStrokeColor: case OPS.setStrokeColor:
cs = stateManager.state.strokeColorSpace; cs = stateManager.state.strokeColorSpace;
args = cs.getRgb(args, 0); args = [cs.getRgbHex(args, 0)];
fn = OPS.setStrokeRGBColor; fn = OPS.setStrokeRGBColor;
break; break;
case OPS.setFillGray: case OPS.setFillGray:
stateManager.state.fillColorSpace = ColorSpaceUtils.gray; stateManager.state.fillColorSpace = ColorSpaceUtils.gray;
args = ColorSpaceUtils.gray.getRgb(args, 0); args = [ColorSpaceUtils.gray.getRgbHex(args, 0)];
fn = OPS.setFillRGBColor; fn = OPS.setFillRGBColor;
break; break;
case OPS.setStrokeGray: case OPS.setStrokeGray:
stateManager.state.strokeColorSpace = ColorSpaceUtils.gray; stateManager.state.strokeColorSpace = ColorSpaceUtils.gray;
args = ColorSpaceUtils.gray.getRgb(args, 0); args = [ColorSpaceUtils.gray.getRgbHex(args, 0)];
fn = OPS.setStrokeRGBColor; fn = OPS.setStrokeRGBColor;
break; break;
case OPS.setFillCMYKColor: case OPS.setFillCMYKColor:
stateManager.state.fillColorSpace = ColorSpaceUtils.cmyk; stateManager.state.fillColorSpace = ColorSpaceUtils.cmyk;
args = ColorSpaceUtils.cmyk.getRgb(args, 0); args = [ColorSpaceUtils.cmyk.getRgbHex(args, 0)];
fn = OPS.setFillRGBColor; fn = OPS.setFillRGBColor;
break; break;
case OPS.setStrokeCMYKColor: case OPS.setStrokeCMYKColor:
stateManager.state.strokeColorSpace = ColorSpaceUtils.cmyk; stateManager.state.strokeColorSpace = ColorSpaceUtils.cmyk;
args = ColorSpaceUtils.cmyk.getRgb(args, 0); args = [ColorSpaceUtils.cmyk.getRgbHex(args, 0)];
fn = OPS.setStrokeRGBColor; fn = OPS.setStrokeRGBColor;
break; break;
case OPS.setFillRGBColor: case OPS.setFillRGBColor:
stateManager.state.fillColorSpace = ColorSpaceUtils.rgb; stateManager.state.fillColorSpace = ColorSpaceUtils.rgb;
args = ColorSpaceUtils.rgb.getRgb(args, 0); args = [ColorSpaceUtils.rgb.getRgbHex(args, 0)];
break; break;
case OPS.setStrokeRGBColor: case OPS.setStrokeRGBColor:
stateManager.state.strokeColorSpace = ColorSpaceUtils.rgb; stateManager.state.strokeColorSpace = ColorSpaceUtils.rgb;
args = ColorSpaceUtils.rgb.getRgb(args, 0); args = [ColorSpaceUtils.rgb.getRgbHex(args, 0)];
break; break;
case OPS.setFillColorN: case OPS.setFillColorN:
cs = stateManager.state.patternFillColorSpace; cs = stateManager.state.patternFillColorSpace;
if (!cs) { if (!cs) {
if (isNumberArray(args, null)) { if (isNumberArray(args, null)) {
args = ColorSpaceUtils.gray.getRgb(args, 0); args = [ColorSpaceUtils.gray.getRgbHex(args, 0)];
fn = OPS.setFillRGBColor; fn = OPS.setFillRGBColor;
break; break;
} }
@ -2065,14 +2065,14 @@ class PartialEvaluator {
); );
return; return;
} }
args = cs.getRgb(args, 0); args = [cs.getRgbHex(args, 0)];
fn = OPS.setFillRGBColor; fn = OPS.setFillRGBColor;
break; break;
case OPS.setStrokeColorN: case OPS.setStrokeColorN:
cs = stateManager.state.patternStrokeColorSpace; cs = stateManager.state.patternStrokeColorSpace;
if (!cs) { if (!cs) {
if (isNumberArray(args, null)) { if (isNumberArray(args, null)) {
args = ColorSpaceUtils.gray.getRgb(args, 0); args = [ColorSpaceUtils.gray.getRgbHex(args, 0)];
fn = OPS.setStrokeRGBColor; fn = OPS.setStrokeRGBColor;
break; break;
} }
@ -2097,7 +2097,7 @@ class PartialEvaluator {
); );
return; return;
} }
args = cs.getRgb(args, 0); args = [cs.getRgbHex(args, 0)];
fn = OPS.setStrokeRGBColor; fn = OPS.setStrokeRGBColor;
break; break;

View File

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

View File

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

View File

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

View File

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

View File

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