diff --git a/src/core/evaluator.js b/src/core/evaluator.js index faa04f15d..db55fabef 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"; @@ -611,6 +611,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, @@ -619,25 +625,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 69a4650cd..a061d5f45 100644 --- a/src/core/operator_list.js +++ b/src/core/operator_list.js @@ -770,7 +770,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 959f0b64b..ad74675fd 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