From 9cd5a9658ab00b8a65cfa2dca742ef0a87c12452 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sat, 29 Mar 2025 12:06:12 +0100 Subject: [PATCH] [api-minor] Move Type3-glyph compilation to the worker-thread After PR 19731 the format of compiled Type3-glyphs is now simple enough that the compilation can be moved to the worker-thread, without introducing any significant additional complexity. This allows us to, ever so slightly, simplify the implementation in `src/display/canvas.js` since the Type3 operatorLists will now directly include standard path-rendering operators (using the format introduced in PR 19689). As part of these changes we also stop caching Type3 image masks since: we've not come across any cases where that actually helps, they're usually fairly small, and it simplifies the code. Note that one "negative" change introduced in this patch is that we'll now compile Type3-glyphs *eagerly*, whereas previously we'd only do that lazily upon their first use. However, this doesn't seem to impact performance in any noticeable way since the compilation is fast enough (way below 1 ms/glyph in my testing) and Type3-fonts are also limited to just 256 glyphs. Also, many (or most?) Type3-fonts don't even use image masks and are thus not affected by these changes. --- src/core/evaluator.js | 38 ++++---- src/core/fonts_utils.js | 172 ++++++++++++++++++++++++++++++++++- src/core/operator_list.js | 2 +- src/display/canvas.js | 185 +------------------------------------- src/shared/util.js | 1 + 5 files changed, 197 insertions(+), 201 deletions(-) diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 574cb9981..233a5c16d 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -32,6 +32,7 @@ import { } from "../shared/util.js"; import { CMapFactory, IdentityCMap } from "./cmap.js"; import { Cmd, Dict, EOF, isName, Name, Ref, RefSet } from "./primitives.js"; +import { compileType3Glyph, FontFlags } from "./fonts_utils.js"; import { ErrorFont, Font } from "./fonts.js"; import { fetchBinaryData, @@ -72,7 +73,6 @@ import { bidi } from "./bidi.js"; import { ColorSpace } from "./colorspace.js"; import { ColorSpaceUtils } from "./colorspace_utils.js"; import { DecodeStream } from "./decode_stream.js"; -import { FontFlags } from "./fonts_utils.js"; import { getFontSubstitution } from "./font_substitutions.js"; import { getGlyphsUnicode } from "./glyphlist.js"; import { getMetrics } from "./metrics.js"; @@ -608,6 +608,12 @@ class PartialEvaluator { const decode = dict.getArray("D", "Decode"); if (this.parsingType3Font) { + // NOTE: Compared to other image resources we don't bother caching + // Type3-glyph image masks, since we've not come across any cases + // where that actually helps. + // In Type3-glyphs image masks are "always" inline resources, + // they're usually fairly small and aren't being re-used either. + imgData = PDFImage.createRawMask({ imgArray, width: w, @@ -616,25 +622,21 @@ class PartialEvaluator { inverseDecode: decode?.[0] > 0, interpolate, }); + args = compileType3Glyph(imgData); - imgData.cached = !!cacheKey; - - fn = OPS.paintImageMaskXObject; - args = [imgData]; - operatorList.addImageOps(fn, args, optionalContent); - - if (cacheKey) { - const cacheData = { fn, args, optionalContent }; - localImageCache.set(cacheKey, imageRef, cacheData); - - if (imageRef) { - this._regionalImageCache.set( - /* name = */ null, - imageRef, - cacheData - ); - } + if (args) { + operatorList.addImageOps(OPS.constructPath, args, optionalContent); + return; } + warn("Cannot compile Type3 glyph."); + + // If compilation failed, or was disabled, fallback to using an inline + // image mask; this case should be extremely rare. + operatorList.addImageOps( + OPS.paintImageMaskXObject, + [imgData], + optionalContent + ); return; } diff --git a/src/core/fonts_utils.js b/src/core/fonts_utils.js index 20c8e87e8..a443a4e1a 100644 --- a/src/core/fonts_utils.js +++ b/src/core/fonts_utils.js @@ -13,11 +13,11 @@ * limitations under the License. */ +import { DrawOPS, info, OPS } from "../shared/util.js"; import { getEncoding, StandardEncoding } from "./encodings.js"; import { getGlyphsUnicode } from "./glyphlist.js"; import { getLookupTableFactory } from "./core_utils.js"; import { getUnicodeForGlyph } from "./unicode.js"; -import { info } from "../shared/util.js"; // Accented characters have issues on Windows and Linux. When this flag is // enabled glyphs that use seac and seac style endchar operators are truncated @@ -207,7 +207,177 @@ const getVerticalPresentationForm = getLookupTableFactory(t => { t[0xff5d] = 0xfe38; // FULLWIDTH RIGHT CURLY BRACKET }); +// To disable Type3 compilation, set the value to `-1`. +const MAX_SIZE_TO_COMPILE = 1000; + +function compileType3Glyph({ data: img, width, height }) { + if (width > MAX_SIZE_TO_COMPILE || height > MAX_SIZE_TO_COMPILE) { + return null; + } + + const POINT_TO_PROCESS_LIMIT = 1000; + const POINT_TYPES = new Uint8Array([ + 0, 2, 4, 0, 1, 0, 5, 4, 8, 10, 0, 8, 0, 2, 1, 0, + ]); + + const width1 = width + 1; + const points = new Uint8Array(width1 * (height + 1)); + let i, j, j0; + + // decodes bit-packed mask data + const lineSize = (width + 7) & ~7; + const data = new Uint8Array(lineSize * height); + let pos = 0; + for (const elem of img) { + let mask = 128; + while (mask > 0) { + data[pos++] = elem & mask ? 0 : 255; + mask >>= 1; + } + } + + // finding interesting points: every point is located between mask pixels, + // so there will be points of the (width + 1)x(height + 1) grid. Every point + // will have flags assigned based on neighboring mask pixels: + // 4 | 8 + // --P-- + // 2 | 1 + // We are interested only in points with the flags: + // - outside corners: 1, 2, 4, 8; + // - inside corners: 7, 11, 13, 14; + // - and, intersections: 5, 10. + let count = 0; + pos = 0; + if (data[pos] !== 0) { + points[0] = 1; + ++count; + } + for (j = 1; j < width; j++) { + if (data[pos] !== data[pos + 1]) { + points[j] = data[pos] ? 2 : 1; + ++count; + } + pos++; + } + if (data[pos] !== 0) { + points[j] = 2; + ++count; + } + for (i = 1; i < height; i++) { + pos = i * lineSize; + j0 = i * width1; + if (data[pos - lineSize] !== data[pos]) { + points[j0] = data[pos] ? 1 : 8; + ++count; + } + // 'sum' is the position of the current pixel configuration in the 'TYPES' + // array (in order 8-1-2-4, so we can use '>>2' to shift the column). + let sum = (data[pos] ? 4 : 0) + (data[pos - lineSize] ? 8 : 0); + for (j = 1; j < width; j++) { + sum = + (sum >> 2) + + (data[pos + 1] ? 4 : 0) + + (data[pos - lineSize + 1] ? 8 : 0); + if (POINT_TYPES[sum]) { + points[j0 + j] = POINT_TYPES[sum]; + ++count; + } + pos++; + } + if (data[pos - lineSize] !== data[pos]) { + points[j0 + j] = data[pos] ? 2 : 4; + ++count; + } + + if (count > POINT_TO_PROCESS_LIMIT) { + return null; + } + } + + pos = lineSize * (height - 1); + j0 = i * width1; + if (data[pos] !== 0) { + points[j0] = 8; + ++count; + } + for (j = 1; j < width; j++) { + if (data[pos] !== data[pos + 1]) { + points[j0 + j] = data[pos] ? 4 : 8; + ++count; + } + pos++; + } + if (data[pos] !== 0) { + points[j0 + j] = 4; + ++count; + } + if (count > POINT_TO_PROCESS_LIMIT) { + return null; + } + + // building outlines + const steps = new Int32Array([0, width1, -1, 0, -width1, 0, 0, 0, 1]); + const pathBuf = []; + + // the path shall be painted in [0..1]x[0..1] space + const { a, b, c, d, e, f } = new DOMMatrix() + .scaleSelf(1 / width, -1 / height) + .translateSelf(0, -height); + + for (i = 0; count && i <= height; i++) { + let p = i * width1; + const end = p + width; + while (p < end && !points[p]) { + p++; + } + if (p === end) { + continue; + } + let x = p % width1; + let y = i; + pathBuf.push(DrawOPS.moveTo, a * x + c * y + e, b * x + d * y + f); + + const p0 = p; + let type = points[p]; + do { + const step = steps[type]; + do { + p += step; + } while (!points[p]); + + const pp = points[p]; + if (pp !== 5 && pp !== 10) { + // set new direction + type = pp; + // delete mark + points[p] = 0; + } else { + // type is 5 or 10, ie, a crossing + // set new direction + type = pp & ((0x33 * type) >> 4); + // set new type for "future hit" + points[p] &= (type >> 2) | (type << 2); + } + x = p % width1; + y = (p / width1) | 0; + pathBuf.push(DrawOPS.lineTo, a * x + c * y + e, b * x + d * y + f); + + if (!points[p]) { + --count; + } + } while (p0 !== p); + --i; + } + + return [ + OPS.rawFillPath, + [new Float32Array(pathBuf)], + new Float32Array([0, 0, width, height]), + ]; +} + export { + compileType3Glyph, FontFlags, getVerticalPresentationForm, MacStandardGlyphOrdering, diff --git a/src/core/operator_list.js b/src/core/operator_list.js index 00a732997..cbb3d7753 100644 --- a/src/core/operator_list.js +++ b/src/core/operator_list.js @@ -699,7 +699,7 @@ class OperatorList { case OPS.paintInlineImageXObjectGroup: case OPS.paintImageMaskXObject: const arg = argsArray[i][0]; // First parameter in imgData. - if (!arg.cached && arg.data?.buffer instanceof ArrayBuffer) { + if (arg.data?.buffer instanceof ArrayBuffer) { transfers.push(arg.data.buffer); } break; diff --git a/src/display/canvas.js b/src/display/canvas.js index 9bbc2cb21..70b632b31 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -54,9 +54,6 @@ const EXECUTION_TIME = 15; // ms // Defines the number of steps before checking the execution time. const EXECUTION_STEPS = 10; -// To disable Type3 compilation, set the value to `-1`. -const MAX_SIZE_TO_COMPILE = 1000; - const FULL_CHUNK_HEIGHT = 16; // Only used in rescaleAndStroke. The goal is to avoid @@ -299,169 +296,6 @@ function drawImageAtIntegerCoords( return [scaleX * destW, scaleY * destH]; } -function compileType3Glyph(imgData) { - const { width, height } = imgData; - if (width > MAX_SIZE_TO_COMPILE || height > MAX_SIZE_TO_COMPILE) { - return null; - } - - const POINT_TO_PROCESS_LIMIT = 1000; - const POINT_TYPES = new Uint8Array([ - 0, 2, 4, 0, 1, 0, 5, 4, 8, 10, 0, 8, 0, 2, 1, 0, - ]); - - const width1 = width + 1; - const points = new Uint8Array(width1 * (height + 1)); - let i, j, j0; - - // decodes bit-packed mask data - const lineSize = (width + 7) & ~7; - const data = new Uint8Array(lineSize * height); - let pos = 0; - for (const elem of imgData.data) { - let mask = 128; - while (mask > 0) { - data[pos++] = elem & mask ? 0 : 255; - mask >>= 1; - } - } - - // finding interesting points: every point is located between mask pixels, - // so there will be points of the (width + 1)x(height + 1) grid. Every point - // will have flags assigned based on neighboring mask pixels: - // 4 | 8 - // --P-- - // 2 | 1 - // We are interested only in points with the flags: - // - outside corners: 1, 2, 4, 8; - // - inside corners: 7, 11, 13, 14; - // - and, intersections: 5, 10. - let count = 0; - pos = 0; - if (data[pos] !== 0) { - points[0] = 1; - ++count; - } - for (j = 1; j < width; j++) { - if (data[pos] !== data[pos + 1]) { - points[j] = data[pos] ? 2 : 1; - ++count; - } - pos++; - } - if (data[pos] !== 0) { - points[j] = 2; - ++count; - } - for (i = 1; i < height; i++) { - pos = i * lineSize; - j0 = i * width1; - if (data[pos - lineSize] !== data[pos]) { - points[j0] = data[pos] ? 1 : 8; - ++count; - } - // 'sum' is the position of the current pixel configuration in the 'TYPES' - // array (in order 8-1-2-4, so we can use '>>2' to shift the column). - let sum = (data[pos] ? 4 : 0) + (data[pos - lineSize] ? 8 : 0); - for (j = 1; j < width; j++) { - sum = - (sum >> 2) + - (data[pos + 1] ? 4 : 0) + - (data[pos - lineSize + 1] ? 8 : 0); - if (POINT_TYPES[sum]) { - points[j0 + j] = POINT_TYPES[sum]; - ++count; - } - pos++; - } - if (data[pos - lineSize] !== data[pos]) { - points[j0 + j] = data[pos] ? 2 : 4; - ++count; - } - - if (count > POINT_TO_PROCESS_LIMIT) { - return null; - } - } - - pos = lineSize * (height - 1); - j0 = i * width1; - if (data[pos] !== 0) { - points[j0] = 8; - ++count; - } - for (j = 1; j < width; j++) { - if (data[pos] !== data[pos + 1]) { - points[j0 + j] = data[pos] ? 4 : 8; - ++count; - } - pos++; - } - if (data[pos] !== 0) { - points[j0 + j] = 4; - ++count; - } - if (count > POINT_TO_PROCESS_LIMIT) { - return null; - } - - // building outlines - const steps = new Int32Array([0, width1, -1, 0, -width1, 0, 0, 0, 1]); - const path = new Path2D(); - - // the path shall be painted in [0..1]x[0..1] space - const { a, b, c, d, e, f } = new DOMMatrix() - .scaleSelf(1 / width, -1 / height) - .translateSelf(0, -height); - - for (i = 0; count && i <= height; i++) { - let p = i * width1; - const end = p + width; - while (p < end && !points[p]) { - p++; - } - if (p === end) { - continue; - } - let x = p % width1; - let y = i; - path.moveTo(a * x + c * y + e, b * x + d * y + f); - - const p0 = p; - let type = points[p]; - do { - const step = steps[type]; - do { - p += step; - } while (!points[p]); - - const pp = points[p]; - if (pp !== 5 && pp !== 10) { - // set new direction - type = pp; - // delete mark - points[p] = 0; - } else { - // type is 5 or 10, ie, a crossing - // set new direction - type = pp & ((0x33 * type) >> 4); - // set new type for "future hit" - points[p] &= (type >> 2) | (type << 2); - } - x = p % width1; - y = (p / width1) | 0; - path.lineTo(a * x + c * y + e, b * x + d * y + f); - - if (!points[p]) { - --count; - } - } while (p0 !== p); - --i; - } - - return path; -} - class CanvasExtraState { constructor(width, height) { // Are soft masks and alpha values shapes or opacities? @@ -821,7 +655,6 @@ class CanvasGraphics { this.canvasFactory = canvasFactory; this.filterFactory = filterFactory; this.groupStack = []; - this.processingType3 = null; // Patterns are painted relative to the initial page/form transform, see // PDF spec 8.7.2 NOTE 1. this.baseTransform = null; @@ -1747,6 +1580,10 @@ class CanvasGraphics { this.consumePath(path); } + rawFillPath(path) { + this.ctx.fill(path); + } + // Clipping clip() { this.pendingClip = NORMAL_CLIP; @@ -2267,7 +2104,6 @@ class CanvasGraphics { if (!operatorList) { warn(`Type3 character "${glyph.operatorListId}" is not available.`); } else if (this.contentVisible) { - this.processingType3 = glyph; this.save(); ctx.scale(fontSize, fontSize); ctx.transform(...fontMatrix); @@ -2282,7 +2118,6 @@ class CanvasGraphics { current.x += width * textHScale; } ctx.restore(); - this.processingType3 = null; } // Type3 fonts @@ -2703,18 +2538,6 @@ class CanvasGraphics { img.count = count; const ctx = this.ctx; - const glyph = this.processingType3; - - if (glyph) { - if (glyph.compiled === undefined) { - glyph.compiled = compileType3Glyph(img); - } - - if (glyph.compiled) { - ctx.fill(glyph.compiled); - return; - } - } const mask = this._createMaskCanvas(img); const maskCanvas = mask.canvas; diff --git a/src/shared/util.js b/src/shared/util.js index d2f47893a..4f8567853 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -339,6 +339,7 @@ const OPS = { constructPath: 91, setStrokeTransparent: 92, setFillTransparent: 93, + rawFillPath: 94, }; // In order to have a switch statement that is fast (i.e. which use a jump