From 5789afd3f8e274a28a22f4483a47db7ea587da87 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Sun, 18 May 2025 19:58:21 +0200 Subject: [PATCH] Create the css color to use with the canvas in the worker It slightly reduces the time spent to draw and the memory used. --- src/core/colorspace.js | 15 +++++--- src/core/evaluator.js | 30 ++++++++-------- src/core/pattern.js | 66 +++++++++++++++++++---------------- src/display/canvas.js | 17 ++++----- src/display/pattern_helper.js | 15 +++----- test/unit/annotation_spec.js | 20 +++++------ test/unit/api_spec.js | 2 +- 7 files changed, 82 insertions(+), 83 deletions(-) diff --git a/src/core/colorspace.js b/src/core/colorspace.js index 4fd032ecf..089e1b7d7 100644 --- a/src/core/colorspace.js +++ b/src/core/colorspace.js @@ -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]); } /** diff --git a/src/core/evaluator.js b/src/core/evaluator.js index e84bad23e..08ee65cdc 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -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; diff --git a/src/core/pattern.js b/src/core/pattern.js index 53d98a2d0..a18b330ea 100644 --- a/src/core/pattern.js +++ b/src/core/pattern.js @@ -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) { diff --git a/src/display/canvas.js b/src/display/canvas.js index fd560ddd4..df0556ad0 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -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; } diff --git a/src/display/pattern_helper.js b/src/display/pattern_helper.js index 6e0d39588..9483d11f2 100644 --- a/src/display/pattern_helper.js +++ b/src/display/pattern_helper.js @@ -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}`); diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 1879ac1a8..679f526b8 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -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([ diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 6399961ea..6610de8a7 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -798,7 +798,7 @@ describe("api", function () { ]); expect(opList.argsArray).toEqual([ [0.5], - new Uint8ClampedArray([255, 0, 0]), + ["#ff0000"], [ OPS.closeStroke, [