1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-19 22:58:07 +02:00

[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.
This commit is contained in:
Jonas Jenwald 2025-03-29 12:06:12 +01:00
parent a4950c0b71
commit 9cd5a9658a
5 changed files with 197 additions and 201 deletions

View file

@ -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;
}

View file

@ -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,

View file

@ -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;

View file

@ -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;

View file

@ -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