diff --git a/src/core/font_renderer.js b/src/core/font_renderer.js index 572cda50995bd..37342fc300dc9 100644 --- a/src/core/font_renderer.js +++ b/src/core/font_renderer.js @@ -16,14 +16,13 @@ import { bytesToString, FONT_IDENTITY_MATRIX, - FontRenderOps, FormatError, unreachable, + Util, warn, } from "../shared/util.js"; import { CFFParser } from "./cff_parser.js"; import { getGlyphsUnicode } from "./glyphlist.js"; -import { isNumberArray } from "./core_utils.js"; import { StandardEncoding } from "./encodings.js"; import { Stream } from "./stream.js"; @@ -182,13 +181,13 @@ function lookupCmap(ranges, unicode) { function compileGlyf(code, cmds, font) { function moveTo(x, y) { - cmds.add(FontRenderOps.MOVE_TO, [x, y]); + cmds.add("M", [x, y]); } function lineTo(x, y) { - cmds.add(FontRenderOps.LINE_TO, [x, y]); + cmds.add("L", [x, y]); } function quadraticCurveTo(xa, ya, x, y) { - cmds.add(FontRenderOps.QUADRATIC_CURVE_TO, [xa, ya, x, y]); + cmds.add("Q", [xa, ya, x, y]); } let i = 0; @@ -249,22 +248,15 @@ function compileGlyf(code, cmds, font) { if (subglyph) { // TODO: the transform should be applied only if there is a scale: // https://github.com/freetype/freetype/blob/edd4fedc5427cf1cf1f4b045e53ff91eb282e9d4/src/truetype/ttgload.c#L1205 - cmds.add(FontRenderOps.SAVE); - cmds.add(FontRenderOps.TRANSFORM, [ - scaleX, - scale01, - scale10, - scaleY, - x, - y, - ]); + cmds.save(); + cmds.transform([scaleX, scale01, scale10, scaleY, x, y]); if (!(flags & 0x02)) { // TODO: we must use arg1 and arg2 to make something similar to: // https://github.com/freetype/freetype/blob/edd4fedc5427cf1cf1f4b045e53ff91eb282e9d4/src/truetype/ttgload.c#L1209 } compileGlyf(subglyph, cmds, font); - cmds.add(FontRenderOps.RESTORE); + cmds.restore(); } } while (flags & 0x20); } else { @@ -369,13 +361,13 @@ function compileGlyf(code, cmds, font) { function compileCharString(charStringCode, cmds, font, glyphId) { function moveTo(x, y) { - cmds.add(FontRenderOps.MOVE_TO, [x, y]); + cmds.add("M", [x, y]); } function lineTo(x, y) { - cmds.add(FontRenderOps.LINE_TO, [x, y]); + cmds.add("L", [x, y]); } function bezierCurveTo(x1, y1, x2, y2, x, y) { - cmds.add(FontRenderOps.BEZIER_CURVE_TO, [x1, y1, x2, y2, x, y]); + cmds.add("C", [x1, y1, x2, y2, x, y]); } const stack = []; @@ -548,8 +540,8 @@ function compileCharString(charStringCode, cmds, font, glyphId) { const bchar = stack.pop(); y = stack.pop(); x = stack.pop(); - cmds.add(FontRenderOps.SAVE); - cmds.add(FontRenderOps.TRANSLATE, [x, y]); + cmds.save(); + cmds.translate(x, y); let cmap = lookupCmap( font.cmap, String.fromCharCode(font.glyphNameMap[StandardEncoding[achar]]) @@ -560,7 +552,7 @@ function compileCharString(charStringCode, cmds, font, glyphId) { font, cmap.glyphId ); - cmds.add(FontRenderOps.RESTORE); + cmds.restore(); cmap = lookupCmap( font.cmap, @@ -744,27 +736,49 @@ function compileCharString(charStringCode, cmds, font, glyphId) { parse(charStringCode); } -const NOOP = []; +const NOOP = ""; class Commands { cmds = []; + transformStack = []; + + currentTransform = [1, 0, 0, 1, 0, 0]; + add(cmd, args) { if (args) { - if (!isNumberArray(args, null)) { - warn( - `Commands.add - "${cmd}" has at least one non-number arg: "${args}".` - ); - // "Fix" the wrong args by replacing them with 0. - const newArgs = args.map(arg => (typeof arg === "number" ? arg : 0)); - this.cmds.push(cmd, ...newArgs); - } else { - this.cmds.push(cmd, ...args); + const [a, b, c, d, e, f] = this.currentTransform; + for (let i = 0, ii = args.length; i < ii; i += 2) { + const x = args[i]; + const y = args[i + 1]; + args[i] = a * x + c * y + e; + args[i + 1] = b * x + d * y + f; } + this.cmds.push(`${cmd}${args.join(" ")}`); } else { this.cmds.push(cmd); } } + + transform(transf) { + this.currentTransform = Util.transform(this.currentTransform, transf); + } + + translate(x, y) { + this.transform([1, 0, 0, 1, x, y]); + } + + save() { + this.transformStack.push(this.currentTransform.slice()); + } + + restore() { + this.currentTransform = this.transformStack.pop() || [1, 0, 0, 1, 0, 0]; + } + + getSVG() { + return this.cmds.join(""); + } } class CompiledFont { @@ -785,7 +799,7 @@ class CompiledFont { const { charCode, glyphId } = lookupCmap(this.cmap, unicode); let fn = this.compiledGlyphs[glyphId], compileEx; - if (!fn) { + if (fn === undefined) { try { fn = this.compileGlyph(this.glyphs[glyphId], glyphId); } catch (ex) { @@ -822,13 +836,11 @@ class CompiledFont { } const cmds = new Commands(); - cmds.add(FontRenderOps.SAVE); - cmds.add(FontRenderOps.TRANSFORM, fontMatrix.slice()); - cmds.add(FontRenderOps.SCALE); + cmds.transform(fontMatrix.slice()); this.compileGlyphImpl(code, cmds, glyphId); - cmds.add(FontRenderOps.RESTORE); + cmds.add("Z"); - return cmds.cmds; + return cmds.getSVG(); } compileGlyphImpl() { diff --git a/src/display/canvas.js b/src/display/canvas.js index debeca6a55047..ee9c4f05320f9 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -1885,15 +1885,19 @@ class CanvasGraphics { return; } - ctx.save(); - ctx.beginPath(); - for (const path of paths) { - ctx.setTransform(...path.transform); - ctx.translate(path.x, path.y); - path.addToPath(ctx, path.fontSize); + const newPath = new Path2D(); + const invTransf = ctx.getTransform().invertSelf(); + for (const { transform, x, y, fontSize, path } of paths) { + newPath.addPath( + path, + new DOMMatrix(transform) + .preMultiplySelf(invTransf) + .translate(x, y) + .scale(fontSize, -fontSize) + ); } - ctx.restore(); - ctx.clip(); + + ctx.clip(newPath); ctx.beginPath(); delete this.pendingTextPaths; } @@ -2002,6 +2006,15 @@ class CanvasGraphics { this.moveText(0, this.current.leading); } + #getScaledPath(path, currentTransform, transform) { + const newPath = new Path2D(); + newPath.addPath( + path, + new DOMMatrix(transform).invertSelf().multiplySelf(currentTransform) + ); + return newPath; + } + paintChar(character, x, y, patternFillTransform, patternStrokeTransform) { const ctx = this.ctx; const current = this.current; @@ -2016,38 +2029,48 @@ class CanvasGraphics { const patternFill = current.patternFill && !font.missingFile; const patternStroke = current.patternStroke && !font.missingFile; - let addToPath; + let path; if ( font.disableFontFace || isAddToPathSet || patternFill || patternStroke ) { - addToPath = font.getPathGenerator(this.commonObjs, character); + path = font.getPathGenerator(this.commonObjs, character); } if (font.disableFontFace || patternFill || patternStroke) { ctx.save(); ctx.translate(x, y); - ctx.beginPath(); - addToPath(ctx, fontSize); + ctx.scale(fontSize, -fontSize); if ( fillStrokeMode === TextRenderingMode.FILL || fillStrokeMode === TextRenderingMode.FILL_STROKE ) { if (patternFillTransform) { + const currentTransform = ctx.getTransform(); ctx.setTransform(...patternFillTransform); + ctx.fill( + this.#getScaledPath(path, currentTransform, patternFillTransform) + ); + } else { + ctx.fill(path); } - ctx.fill(); } if ( fillStrokeMode === TextRenderingMode.STROKE || fillStrokeMode === TextRenderingMode.FILL_STROKE ) { if (patternStrokeTransform) { + const currentTransform = ctx.getTransform(); ctx.setTransform(...patternStrokeTransform); + ctx.stroke( + this.#getScaledPath(path, currentTransform, patternStrokeTransform) + ); + } else { + ctx.lineWidth /= fontSize; + ctx.stroke(path); } - ctx.stroke(); } ctx.restore(); } else { @@ -2072,7 +2095,7 @@ class CanvasGraphics { x, y, fontSize, - addToPath, + path, }); } } diff --git a/src/display/font_loader.js b/src/display/font_loader.js index f0012eb358e0b..79a33ca369cb4 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -15,7 +15,6 @@ import { assert, - FontRenderOps, isNodeJS, shadow, string32, @@ -427,89 +426,7 @@ class FontFaceObject { } catch (ex) { warn(`getPathGenerator - ignoring character: "${ex}".`); } - - if (!Array.isArray(cmds) || cmds.length === 0) { - return (this.compiledGlyphs[character] = function (c, size) { - // No-op function, to allow rendering to continue. - }); - } - - const commands = []; - for (let i = 0, ii = cmds.length; i < ii; ) { - switch (cmds[i++]) { - case FontRenderOps.BEZIER_CURVE_TO: - { - const [a, b, c, d, e, f] = cmds.slice(i, i + 6); - commands.push(ctx => ctx.bezierCurveTo(a, b, c, d, e, f)); - i += 6; - } - break; - case FontRenderOps.MOVE_TO: - { - const [a, b] = cmds.slice(i, i + 2); - commands.push(ctx => ctx.moveTo(a, b)); - i += 2; - } - break; - case FontRenderOps.LINE_TO: - { - const [a, b] = cmds.slice(i, i + 2); - commands.push(ctx => ctx.lineTo(a, b)); - i += 2; - } - break; - case FontRenderOps.QUADRATIC_CURVE_TO: - { - const [a, b, c, d] = cmds.slice(i, i + 4); - commands.push(ctx => ctx.quadraticCurveTo(a, b, c, d)); - i += 4; - } - break; - case FontRenderOps.RESTORE: - commands.push(ctx => ctx.restore()); - break; - case FontRenderOps.SAVE: - commands.push(ctx => ctx.save()); - break; - case FontRenderOps.SCALE: - // The scale command must be at the third position, after save and - // transform (for the font matrix) commands (see also - // font_renderer.js). - // The goal is to just scale the canvas and then run the commands loop - // without the need to pass the size parameter to each command. - assert( - commands.length === 2, - "Scale command is only valid at the third position." - ); - break; - case FontRenderOps.TRANSFORM: - { - const [a, b, c, d, e, f] = cmds.slice(i, i + 6); - commands.push(ctx => ctx.transform(a, b, c, d, e, f)); - i += 6; - } - break; - case FontRenderOps.TRANSLATE: - { - const [a, b] = cmds.slice(i, i + 2); - commands.push(ctx => ctx.translate(a, b)); - i += 2; - } - break; - } - } - // From https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#paths - // All contours must be closed with a lineto operation. - commands.push(ctx => ctx.closePath()); - - return (this.compiledGlyphs[character] = function glyphDrawer(ctx, size) { - commands[0](ctx); - commands[1](ctx); - ctx.scale(size, -size); - for (let i = 2, ii = commands.length; i < ii; i++) { - commands[i](ctx); - } - }); + return (this.compiledGlyphs[character] = new Path2D(cmds || "")); } } diff --git a/src/shared/util.js b/src/shared/util.js index 1d812bf010442..e0f66716ab9ca 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -1087,18 +1087,6 @@ function getUuid() { const AnnotationPrefix = "pdfjs_internal_id_"; -const FontRenderOps = { - BEZIER_CURVE_TO: 0, - MOVE_TO: 1, - LINE_TO: 2, - QUADRATIC_CURVE_TO: 3, - RESTORE: 4, - SAVE: 5, - SCALE: 6, - TRANSFORM: 7, - TRANSLATE: 8, -}; - // TODO: Remove this once `Uint8Array.prototype.toHex` is generally available. function toHexUtil(arr) { if (Uint8Array.prototype.toHex) { @@ -1158,7 +1146,6 @@ export { DocumentActionEventType, FeatureTest, FONT_IDENTITY_MATRIX, - FontRenderOps, FormatError, fromBase64Util, getModificationDate,