Skip to content

Commit 9cd5a96

Browse files
committed
[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.
1 parent a4950c0 commit 9cd5a96

5 files changed

Lines changed: 197 additions & 201 deletions

File tree

src/core/evaluator.js

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
} from "../shared/util.js";
3333
import { CMapFactory, IdentityCMap } from "./cmap.js";
3434
import { Cmd, Dict, EOF, isName, Name, Ref, RefSet } from "./primitives.js";
35+
import { compileType3Glyph, FontFlags } from "./fonts_utils.js";
3536
import { ErrorFont, Font } from "./fonts.js";
3637
import {
3738
fetchBinaryData,
@@ -72,7 +73,6 @@ import { bidi } from "./bidi.js";
7273
import { ColorSpace } from "./colorspace.js";
7374
import { ColorSpaceUtils } from "./colorspace_utils.js";
7475
import { DecodeStream } from "./decode_stream.js";
75-
import { FontFlags } from "./fonts_utils.js";
7676
import { getFontSubstitution } from "./font_substitutions.js";
7777
import { getGlyphsUnicode } from "./glyphlist.js";
7878
import { getMetrics } from "./metrics.js";
@@ -608,6 +608,12 @@ class PartialEvaluator {
608608
const decode = dict.getArray("D", "Decode");
609609

610610
if (this.parsingType3Font) {
611+
// NOTE: Compared to other image resources we don't bother caching
612+
// Type3-glyph image masks, since we've not come across any cases
613+
// where that actually helps.
614+
// In Type3-glyphs image masks are "always" inline resources,
615+
// they're usually fairly small and aren't being re-used either.
616+
611617
imgData = PDFImage.createRawMask({
612618
imgArray,
613619
width: w,
@@ -616,25 +622,21 @@ class PartialEvaluator {
616622
inverseDecode: decode?.[0] > 0,
617623
interpolate,
618624
});
625+
args = compileType3Glyph(imgData);
619626

620-
imgData.cached = !!cacheKey;
621-
622-
fn = OPS.paintImageMaskXObject;
623-
args = [imgData];
624-
operatorList.addImageOps(fn, args, optionalContent);
625-
626-
if (cacheKey) {
627-
const cacheData = { fn, args, optionalContent };
628-
localImageCache.set(cacheKey, imageRef, cacheData);
629-
630-
if (imageRef) {
631-
this._regionalImageCache.set(
632-
/* name = */ null,
633-
imageRef,
634-
cacheData
635-
);
636-
}
627+
if (args) {
628+
operatorList.addImageOps(OPS.constructPath, args, optionalContent);
629+
return;
637630
}
631+
warn("Cannot compile Type3 glyph.");
632+
633+
// If compilation failed, or was disabled, fallback to using an inline
634+
// image mask; this case should be extremely rare.
635+
operatorList.addImageOps(
636+
OPS.paintImageMaskXObject,
637+
[imgData],
638+
optionalContent
639+
);
638640
return;
639641
}
640642

src/core/fonts_utils.js

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
* limitations under the License.
1414
*/
1515

16+
import { DrawOPS, info, OPS } from "../shared/util.js";
1617
import { getEncoding, StandardEncoding } from "./encodings.js";
1718
import { getGlyphsUnicode } from "./glyphlist.js";
1819
import { getLookupTableFactory } from "./core_utils.js";
1920
import { getUnicodeForGlyph } from "./unicode.js";
20-
import { info } from "../shared/util.js";
2121

2222
// Accented characters have issues on Windows and Linux. When this flag is
2323
// enabled glyphs that use seac and seac style endchar operators are truncated
@@ -207,7 +207,177 @@ const getVerticalPresentationForm = getLookupTableFactory(t => {
207207
t[0xff5d] = 0xfe38; // FULLWIDTH RIGHT CURLY BRACKET
208208
});
209209

210+
// To disable Type3 compilation, set the value to `-1`.
211+
const MAX_SIZE_TO_COMPILE = 1000;
212+
213+
function compileType3Glyph({ data: img, width, height }) {
214+
if (width > MAX_SIZE_TO_COMPILE || height > MAX_SIZE_TO_COMPILE) {
215+
return null;
216+
}
217+
218+
const POINT_TO_PROCESS_LIMIT = 1000;
219+
const POINT_TYPES = new Uint8Array([
220+
0, 2, 4, 0, 1, 0, 5, 4, 8, 10, 0, 8, 0, 2, 1, 0,
221+
]);
222+
223+
const width1 = width + 1;
224+
const points = new Uint8Array(width1 * (height + 1));
225+
let i, j, j0;
226+
227+
// decodes bit-packed mask data
228+
const lineSize = (width + 7) & ~7;
229+
const data = new Uint8Array(lineSize * height);
230+
let pos = 0;
231+
for (const elem of img) {
232+
let mask = 128;
233+
while (mask > 0) {
234+
data[pos++] = elem & mask ? 0 : 255;
235+
mask >>= 1;
236+
}
237+
}
238+
239+
// finding interesting points: every point is located between mask pixels,
240+
// so there will be points of the (width + 1)x(height + 1) grid. Every point
241+
// will have flags assigned based on neighboring mask pixels:
242+
// 4 | 8
243+
// --P--
244+
// 2 | 1
245+
// We are interested only in points with the flags:
246+
// - outside corners: 1, 2, 4, 8;
247+
// - inside corners: 7, 11, 13, 14;
248+
// - and, intersections: 5, 10.
249+
let count = 0;
250+
pos = 0;
251+
if (data[pos] !== 0) {
252+
points[0] = 1;
253+
++count;
254+
}
255+
for (j = 1; j < width; j++) {
256+
if (data[pos] !== data[pos + 1]) {
257+
points[j] = data[pos] ? 2 : 1;
258+
++count;
259+
}
260+
pos++;
261+
}
262+
if (data[pos] !== 0) {
263+
points[j] = 2;
264+
++count;
265+
}
266+
for (i = 1; i < height; i++) {
267+
pos = i * lineSize;
268+
j0 = i * width1;
269+
if (data[pos - lineSize] !== data[pos]) {
270+
points[j0] = data[pos] ? 1 : 8;
271+
++count;
272+
}
273+
// 'sum' is the position of the current pixel configuration in the 'TYPES'
274+
// array (in order 8-1-2-4, so we can use '>>2' to shift the column).
275+
let sum = (data[pos] ? 4 : 0) + (data[pos - lineSize] ? 8 : 0);
276+
for (j = 1; j < width; j++) {
277+
sum =
278+
(sum >> 2) +
279+
(data[pos + 1] ? 4 : 0) +
280+
(data[pos - lineSize + 1] ? 8 : 0);
281+
if (POINT_TYPES[sum]) {
282+
points[j0 + j] = POINT_TYPES[sum];
283+
++count;
284+
}
285+
pos++;
286+
}
287+
if (data[pos - lineSize] !== data[pos]) {
288+
points[j0 + j] = data[pos] ? 2 : 4;
289+
++count;
290+
}
291+
292+
if (count > POINT_TO_PROCESS_LIMIT) {
293+
return null;
294+
}
295+
}
296+
297+
pos = lineSize * (height - 1);
298+
j0 = i * width1;
299+
if (data[pos] !== 0) {
300+
points[j0] = 8;
301+
++count;
302+
}
303+
for (j = 1; j < width; j++) {
304+
if (data[pos] !== data[pos + 1]) {
305+
points[j0 + j] = data[pos] ? 4 : 8;
306+
++count;
307+
}
308+
pos++;
309+
}
310+
if (data[pos] !== 0) {
311+
points[j0 + j] = 4;
312+
++count;
313+
}
314+
if (count > POINT_TO_PROCESS_LIMIT) {
315+
return null;
316+
}
317+
318+
// building outlines
319+
const steps = new Int32Array([0, width1, -1, 0, -width1, 0, 0, 0, 1]);
320+
const pathBuf = [];
321+
322+
// the path shall be painted in [0..1]x[0..1] space
323+
const { a, b, c, d, e, f } = new DOMMatrix()
324+
.scaleSelf(1 / width, -1 / height)
325+
.translateSelf(0, -height);
326+
327+
for (i = 0; count && i <= height; i++) {
328+
let p = i * width1;
329+
const end = p + width;
330+
while (p < end && !points[p]) {
331+
p++;
332+
}
333+
if (p === end) {
334+
continue;
335+
}
336+
let x = p % width1;
337+
let y = i;
338+
pathBuf.push(DrawOPS.moveTo, a * x + c * y + e, b * x + d * y + f);
339+
340+
const p0 = p;
341+
let type = points[p];
342+
do {
343+
const step = steps[type];
344+
do {
345+
p += step;
346+
} while (!points[p]);
347+
348+
const pp = points[p];
349+
if (pp !== 5 && pp !== 10) {
350+
// set new direction
351+
type = pp;
352+
// delete mark
353+
points[p] = 0;
354+
} else {
355+
// type is 5 or 10, ie, a crossing
356+
// set new direction
357+
type = pp & ((0x33 * type) >> 4);
358+
// set new type for "future hit"
359+
points[p] &= (type >> 2) | (type << 2);
360+
}
361+
x = p % width1;
362+
y = (p / width1) | 0;
363+
pathBuf.push(DrawOPS.lineTo, a * x + c * y + e, b * x + d * y + f);
364+
365+
if (!points[p]) {
366+
--count;
367+
}
368+
} while (p0 !== p);
369+
--i;
370+
}
371+
372+
return [
373+
OPS.rawFillPath,
374+
[new Float32Array(pathBuf)],
375+
new Float32Array([0, 0, width, height]),
376+
];
377+
}
378+
210379
export {
380+
compileType3Glyph,
211381
FontFlags,
212382
getVerticalPresentationForm,
213383
MacStandardGlyphOrdering,

src/core/operator_list.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ class OperatorList {
699699
case OPS.paintInlineImageXObjectGroup:
700700
case OPS.paintImageMaskXObject:
701701
const arg = argsArray[i][0]; // First parameter in imgData.
702-
if (!arg.cached && arg.data?.buffer instanceof ArrayBuffer) {
702+
if (arg.data?.buffer instanceof ArrayBuffer) {
703703
transfers.push(arg.data.buffer);
704704
}
705705
break;

0 commit comments

Comments
 (0)