@jasy/pdf 1.0.0-alpha.9 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -9
- package/dist/api/color.js +69 -0
- package/dist/api/content.d.ts +40 -1
- package/dist/api/content.js +83 -0
- package/dist/api/descriptor.js +6 -1
- package/dist/api/dimension.d.ts +3 -2
- package/dist/api/font-url.js +3 -2
- package/dist/api/structure.d.ts +7 -0
- package/dist/api/structure.js +38 -3
- package/dist/api/text.d.ts +23 -2
- package/dist/api/text.js +9 -0
- package/dist/canvas/painter.d.ts +102 -0
- package/dist/canvas/painter.js +183 -0
- package/dist/elements/canvas-element.d.ts +38 -0
- package/dist/elements/canvas-element.js +47 -0
- package/dist/elements/container-element.d.ts +2 -0
- package/dist/elements/container-element.js +6 -0
- package/dist/elements/index.d.ts +2 -0
- package/dist/elements/index.js +2 -0
- package/dist/elements/layout/expanded-element.d.ts +2 -0
- package/dist/elements/layout/expanded-element.js +4 -0
- package/dist/elements/layout/keep-together-element.d.ts +2 -0
- package/dist/elements/layout/keep-together-element.js +4 -0
- package/dist/elements/layout/padding-element.d.ts +5 -0
- package/dist/elements/layout/padding-element.js +8 -0
- package/dist/elements/layout/struct-group.d.ts +2 -0
- package/dist/elements/layout/struct-group.js +4 -0
- package/dist/elements/pdf-element.d.ts +19 -3
- package/dist/elements/pdf-element.js +22 -4
- package/dist/elements/rectangle-element.d.ts +2 -0
- package/dist/elements/rectangle-element.js +6 -0
- package/dist/elements/row-element.d.ts +2 -0
- package/dist/elements/row-element.js +6 -0
- package/dist/elements/svg-element.d.ts +41 -0
- package/dist/elements/svg-element.js +93 -0
- package/dist/elements/text-element.d.ts +33 -1
- package/dist/elements/text-element.js +80 -4
- package/dist/ir/display-list.d.ts +44 -5
- package/dist/layout/min-intrinsic.d.ts +29 -0
- package/dist/layout/min-intrinsic.js +37 -0
- package/dist/renderer/anchor-renderer.js +1 -1
- package/dist/renderer/bookmark-renderer.js +1 -1
- package/dist/renderer/canvas-renderer.d.ts +14 -0
- package/dist/renderer/canvas-renderer.js +36 -0
- package/dist/renderer/container-renderer.js +1 -3
- package/dist/renderer/default-text-style-renderer.js +1 -1
- package/dist/renderer/deferred-renderer.js +1 -1
- package/dist/renderer/expanded-renderer.js +1 -1
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.js +2 -0
- package/dist/renderer/keep-together-renderer.js +1 -1
- package/dist/renderer/link-renderer.js +1 -1
- package/dist/renderer/padding-renderer.js +1 -1
- package/dist/renderer/page-builder-renderer.js +1 -1
- package/dist/renderer/page-renderer.js +1 -3
- package/dist/renderer/pdf-backend.d.ts +15 -4
- package/dist/renderer/pdf-backend.js +125 -35
- package/dist/renderer/pdf-renderer.js +5 -1
- package/dist/renderer/positioned-renderer.js +1 -1
- package/dist/renderer/rectangle-renderer.js +1 -3
- package/dist/renderer/repeating-header-renderer.js +1 -2
- package/dist/renderer/rotated-renderer.js +1 -1
- package/dist/renderer/row-renderer.js +1 -3
- package/dist/renderer/struct-group-renderer.js +1 -1
- package/dist/renderer/svg-renderer.d.ts +11 -0
- package/dist/renderer/svg-renderer.js +27 -0
- package/dist/renderer/text-renderer.js +30 -19
- package/dist/svg/css.d.ts +38 -0
- package/dist/svg/css.js +115 -0
- package/dist/svg/errors.d.ts +24 -0
- package/dist/svg/errors.js +30 -0
- package/dist/svg/gradients.d.ts +34 -0
- package/dist/svg/gradients.js +156 -0
- package/dist/svg/index.d.ts +22 -0
- package/dist/svg/index.js +331 -0
- package/dist/svg/shapes.d.ts +6 -0
- package/dist/svg/shapes.js +177 -0
- package/dist/svg/style.d.ts +61 -0
- package/dist/svg/style.js +144 -0
- package/dist/svg/transform.d.ts +13 -0
- package/dist/svg/transform.js +68 -0
- package/dist/svg/units.d.ts +11 -0
- package/dist/svg/units.js +73 -0
- package/dist/svg/xml.d.ts +24 -0
- package/dist/svg/xml.js +158 -0
- package/dist/text/advance.d.ts +3 -0
- package/dist/text/advance.js +51 -8
- package/dist/text/glyph-coverage.d.ts +15 -0
- package/dist/text/glyph-coverage.js +77 -0
- package/dist/text/line-breaker.d.ts +8 -0
- package/dist/text/line-breaker.js +146 -70
- package/dist/text/shape.d.ts +3 -3
- package/dist/text/shape.js +34 -10
- package/dist/text/text-style.d.ts +13 -0
- package/dist/text/text-style.js +6 -0
- package/dist/text/whitespace.d.ts +4 -0
- package/dist/text/whitespace.js +43 -0
- package/dist/text/word-splitting.d.ts +42 -0
- package/dist/text/word-splitting.js +72 -0
- package/dist/utils/afm-parser.d.ts +0 -1
- package/dist/utils/afm-parser.js +15 -16
- package/dist/utils/flex-layout.js +67 -18
- package/dist/utils/font-metrics.d.ts +12 -3
- package/dist/utils/pdf-object-manager.d.ts +31 -5
- package/dist/utils/pdf-object-manager.js +107 -21
- package/dist/utils/renderer-registry.d.ts +13 -1
- package/dist/utils/renderer-registry.js +22 -1
- package/dist/utils/utf8-to-windows1252-encoder.js +5 -3
- package/dist/utils/woff2.d.ts +6 -0
- package/dist/utils/woff2.js +651 -0
- package/dist/vector/path.d.ts +33 -0
- package/dist/vector/path.js +81 -0
- package/package.json +5 -2
|
@@ -205,24 +205,69 @@ export class FlexLayoutHelper {
|
|
|
205
205
|
const shrinkers = children.filter((c) => c.flexShrink > 0 && !(c instanceof FlexiblePDFElement));
|
|
206
206
|
const overflow = fixedMain + totalBasis + totalGap - mainAvail;
|
|
207
207
|
if (shrinkers.length > 0 && Number.isFinite(mainAvail) && overflow > 0) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
208
|
+
// Every child is floored at what it can actually hold - its `min-content` extent, which is CSS's
|
|
209
|
+
// automatic `min-width` on a flex item. Without it a badly overflowing line would grind a
|
|
210
|
+
// paragraph down to one word per line: the box gets narrow, the text gets tall, and nothing is
|
|
211
|
+
// gained. Past that floor a child overflows again, which is what a browser does too.
|
|
212
|
+
const naturalOf = (c) => axis.mainOf(fixedSize.get(c));
|
|
213
|
+
const floorOf = new Map(shrinkers.map((c) => [
|
|
214
|
+
c,
|
|
215
|
+
// Never above its natural size: the shrink pass may only take space away, never hand it out.
|
|
216
|
+
// Never below zero either - when the GAPS alone outgrow the line a proportional share asks
|
|
217
|
+
// for more than a child has, and a negative constraint is nonsense even though
|
|
218
|
+
// `constrainWidth` would floor it further down anyway.
|
|
219
|
+
Math.min(naturalOf(c), Math.max(0, c.minIntrinsicMain(axis.mainHorizontal, ctx))),
|
|
220
|
+
]));
|
|
221
|
+
// CSS resolves this iteratively (Flexbox 9.7): a child that lands on its floor is FROZEN there
|
|
222
|
+
// and the share it could not absorb is handed to the rest. A single pass would leave that share
|
|
223
|
+
// unspent, so one unsqueezable child would keep the whole line overflowing while its neighbours
|
|
224
|
+
// still had room to give.
|
|
225
|
+
const target = new Map(shrinkers.map((c) => [c, naturalOf(c)]));
|
|
226
|
+
const frozen = new Set();
|
|
227
|
+
let flexible = [...shrinkers];
|
|
228
|
+
// What the frozen ones took off the overflow; the rest is always shared out from scratch, never
|
|
229
|
+
// from what a previous round already assigned.
|
|
230
|
+
let absorbedByFrozen = 0;
|
|
231
|
+
while (flexible.length > 0) {
|
|
232
|
+
const toAbsorb = overflow - absorbedByFrozen;
|
|
233
|
+
if (toAbsorb <= 1e-9)
|
|
234
|
+
break;
|
|
235
|
+
const weight = (c) => c.flexShrink * naturalOf(c);
|
|
236
|
+
const totalWeight = flexible.reduce((n, c) => n + weight(c), 0);
|
|
237
|
+
if (totalWeight <= 0)
|
|
238
|
+
break;
|
|
239
|
+
const froze = [];
|
|
240
|
+
for (const child of flexible) {
|
|
241
|
+
const natural = naturalOf(child);
|
|
242
|
+
const floor = floorOf.get(child);
|
|
243
|
+
const wanted = natural - (weight(child) / totalWeight) * toAbsorb;
|
|
244
|
+
if (wanted < floor) {
|
|
245
|
+
target.set(child, floor);
|
|
246
|
+
froze.push(child);
|
|
247
|
+
absorbedByFrozen += natural - floor;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
target.set(child, wanted);
|
|
251
|
+
}
|
|
225
252
|
}
|
|
253
|
+
if (froze.length === 0)
|
|
254
|
+
break;
|
|
255
|
+
for (const c of froze)
|
|
256
|
+
frozen.add(c);
|
|
257
|
+
flexible = flexible.filter((c) => !frozen.has(c));
|
|
258
|
+
}
|
|
259
|
+
for (const child of shrinkers) {
|
|
260
|
+
const natural = naturalOf(child);
|
|
261
|
+
const capped = target.get(child);
|
|
262
|
+
if (capped >= natural)
|
|
263
|
+
continue;
|
|
264
|
+
shrunkCap.set(child, capped);
|
|
265
|
+
// Re-measure NOW, not in pass 2: a narrower child may wrap to more lines, and the line's
|
|
266
|
+
// cross extent is settled just below.
|
|
267
|
+
const size = child.calculateLayout(axis.measureConstraints(crossAvail, capped), axis.offsetAt(mainStart, crossOrigin), ctx);
|
|
268
|
+
fixedMain += axis.mainOf(size) - natural;
|
|
269
|
+
crossUsed = Math.max(crossUsed, axis.crossOf(size));
|
|
270
|
+
fixedSize.set(child, size);
|
|
226
271
|
}
|
|
227
272
|
}
|
|
228
273
|
const leftover = mainAvail - fixedMain - totalBasis - totalGap;
|
|
@@ -272,7 +317,11 @@ export class FlexLayoutHelper {
|
|
|
272
317
|
const stretch = align === "stretch";
|
|
273
318
|
let mainExtent;
|
|
274
319
|
if (child instanceof FlexiblePDFElement) {
|
|
275
|
-
|
|
320
|
+
// `totalFlex` is 0 when every flex child asked for `flex: 0` (a pure `flexBasis` slot). The
|
|
321
|
+
// share is then 0/0 - a NaN that becomes the offset of every later sibling, which is the
|
|
322
|
+
// shape of the old Spacer bug. Such a child simply takes its basis.
|
|
323
|
+
const share = totalFlex > 0 ? (child.getFlex() / totalFlex) * remaining : 0;
|
|
324
|
+
mainExtent = child.getBasis(percentBase) + share;
|
|
276
325
|
// A flex child fills the MAIN axis, and its cross size is only known after layout - there is
|
|
277
326
|
// nothing to align against. So alignSelf is a no-op here, and that has to hold for the CROSS
|
|
278
327
|
// CONSTRAINT too: reading the per-child alignment would hand an `alignSelf: "start"` flex child
|
|
@@ -9,7 +9,7 @@ import type { FontDecoration } from "../text/text-decoration.js";
|
|
|
9
9
|
* touching layout code.
|
|
10
10
|
*/
|
|
11
11
|
export interface FontMetrics {
|
|
12
|
-
getStringWidth(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle): number;
|
|
12
|
+
getStringWidth(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle, ligatures?: boolean): number;
|
|
13
13
|
getCharWidth(char: string, fontSize: number, fullFontName?: string, fontName?: string, fontStyle?: FontStyle): number;
|
|
14
14
|
/** Ascent / descent / lineGap of a face, in em fractions - the vertical counterpart of the widths
|
|
15
15
|
* above. Drives the line box and the baseline (see `text/line-metrics.ts`). */
|
|
@@ -20,12 +20,21 @@ export interface FontMetrics {
|
|
|
20
20
|
/** Whether kerning is on for this document. `runAdvance` reads it so a measured advance matches the
|
|
21
21
|
* drawn one (the backend emits a `TJ` under the same flag). */
|
|
22
22
|
readonly kerningEnabled: boolean;
|
|
23
|
+
/** Bumped whenever a face is registered, so a memoised measurement knows to drop it: the same family
|
|
24
|
+
* name can mean a different face afterwards. Optional - leaving it out means never invalidating. */
|
|
25
|
+
readonly fontEpoch?: number;
|
|
23
26
|
/** Per-adjacent-pair kerning of `text`, in em/1000 (negative tightens); length `codePoints - 1`,
|
|
24
27
|
* zero next to a space. Only meaningful when `kerningEnabled`. */
|
|
25
|
-
getKernPairs(text: string, fontFamily: string, fontStyle: FontStyle): number[];
|
|
28
|
+
getKernPairs(text: string, fontFamily: string, fontStyle: FontStyle, ligatures?: boolean): number[];
|
|
26
29
|
/** Whether this family can draw the code point at all - what picks a face out of a fallback stack. */
|
|
27
30
|
hasGlyph(codePoint: number, fontFamily: string, fontStyle: FontStyle): boolean;
|
|
31
|
+
/** Whether the document draws this code point as colour emoji rather than from the text font - it
|
|
32
|
+
* is missing from that font on purpose, so glyph coverage must not touch it. */
|
|
33
|
+
rendersAsEmoji?(codePoint: number, fontFamily: string, fontStyle: FontStyle): boolean;
|
|
34
|
+
/** Told when a code point had to be removed because no font could draw it. Optional: a metrics
|
|
35
|
+
* object used only for measuring (a test double) has nothing to report to. */
|
|
36
|
+
reportMissingGlyph?(codePoints: number[]): void;
|
|
28
37
|
/** How many glyphs the run will DRAW, when a ligature made that differ from its code-point count.
|
|
29
38
|
* Absent or `undefined` means "the same", which is every Latin run. */
|
|
30
|
-
shapedGlyphCount?(text: string, fontFamily?: string, fontStyle?: FontStyle): number | undefined;
|
|
39
|
+
shapedGlyphCount?(text: string, fontFamily?: string, fontStyle?: FontStyle, ligatures?: boolean): number | undefined;
|
|
31
40
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OverflowPolicy } from "../layout/fragmentation.js";
|
|
2
2
|
import { TTFParser } from "./ttf-parser.js";
|
|
3
3
|
import { type ShapedGlyph } from "../text/shape.js";
|
|
4
|
+
export declare function featureKey(f: readonly string[]): string;
|
|
4
5
|
import type { SecurityHandler } from "../crypto/security-handler.js";
|
|
5
6
|
import type { Gradient } from "../ir/display-list.js";
|
|
6
7
|
import type { FontVerticals } from "../text/line-metrics.js";
|
|
@@ -40,7 +41,12 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
40
41
|
private verticalsCache;
|
|
41
42
|
private decorationCache;
|
|
42
43
|
private customFontEmit;
|
|
44
|
+
/** Shaped runs, cached face -> feature set -> text. Nested, not one map under a composed key: that
|
|
45
|
+
* key would carry the whole run's text, so every lookup would copy it into a fresh string. */
|
|
43
46
|
private shapeCache;
|
|
47
|
+
/** Bumped by every font registration; `runAdvance` watches it to drop memoised widths, since a
|
|
48
|
+
* family name can mean a different face afterwards. */
|
|
49
|
+
fontEpoch: number;
|
|
44
50
|
private shapedOrigins;
|
|
45
51
|
private attachments;
|
|
46
52
|
private xmpMetadata?;
|
|
@@ -65,6 +71,16 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
65
71
|
addObject(content: string): number;
|
|
66
72
|
replaceObject(objectNumber: number, content: string): void;
|
|
67
73
|
setCompress(on: boolean): void;
|
|
74
|
+
/**
|
|
75
|
+
* Code points no font in the document could draw, so they were removed rather than emitted as
|
|
76
|
+
* `.notdef` (which PDF/A forbids). Collected here because this object is the one thing threaded
|
|
77
|
+
* through BOTH passes; the caller gets them back with the bytes, so an application can say which
|
|
78
|
+
* character went missing instead of a server log nobody reads.
|
|
79
|
+
*/
|
|
80
|
+
private readonly missingGlyphs;
|
|
81
|
+
reportMissingGlyph(codePoints: number[]): void;
|
|
82
|
+
/** The dropped code points, in the order first seen. Empty for a document that hit none. */
|
|
83
|
+
getMissingGlyphs(): number[];
|
|
68
84
|
setOverflowPolicy(policy: OverflowPolicy): void;
|
|
69
85
|
getOverflowPolicy(): OverflowPolicy;
|
|
70
86
|
private encToken;
|
|
@@ -142,21 +158,28 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
142
158
|
* Used to pick a family out of a fallback stack, per code point.
|
|
143
159
|
*/
|
|
144
160
|
hasGlyph(codePoint: number, fontFamily: string, fontStyle?: FontStyle): boolean;
|
|
161
|
+
/**
|
|
162
|
+
* Whether the document draws this code point through the colour-emoji path instead of the text
|
|
163
|
+
* font - a fallback emoji FONT, or an image source (`Document({ emoji })`). Such a code point is
|
|
164
|
+
* missing from the text font ON PURPOSE, so glyph coverage must leave it alone. Mirrors exactly
|
|
165
|
+
* what `getCharWidth` measures it as, or a dropped emoji would have been measured and not drawn.
|
|
166
|
+
*/
|
|
167
|
+
rendersAsEmoji(codePoint: number, fontFamily: string, fontStyle: FontStyle): boolean;
|
|
145
168
|
private colorRenders;
|
|
146
169
|
getCustomFontResource(name: string, style?: FontStyle): FontIndexes | undefined;
|
|
147
170
|
/** How many glyphs a run draws - its code-point count unless a ligature merged some. `letterSpacing`
|
|
148
171
|
* is per glyph (`Tc`), so this is what it multiplies. */
|
|
149
|
-
shapedGlyphCount(text: string, fontFamily?: string, fontStyle?: FontStyle): number | undefined;
|
|
172
|
+
shapedGlyphCount(text: string, fontFamily?: string, fontStyle?: FontStyle, ligatures?: boolean): number | undefined;
|
|
150
173
|
/**
|
|
151
174
|
* The shaped glyphs of a run, or undefined when nothing needed shaping. The ONE place shaping is
|
|
152
175
|
* decided, so measuring and drawing cannot disagree. Memoised: a paragraph is measured several
|
|
153
176
|
* times (layout, pagination, drawing).
|
|
154
177
|
*/
|
|
155
|
-
shapeText(text: string, fontFamily?: string, fontStyle?: FontStyle): ShapedGlyph[] | undefined;
|
|
178
|
+
shapeText(text: string, fontFamily?: string, fontStyle?: FontStyle, features?: readonly string[]): ShapedGlyph[] | undefined;
|
|
156
179
|
/** Hex Identity-H for glyphs the producer already resolved (a shaped run), which cannot be given as
|
|
157
180
|
* text. Recording each keeps it in the subset. */
|
|
158
181
|
registerGlyphs(name: string, glyphs: readonly number[], style?: FontStyle): string;
|
|
159
|
-
encodeCustomText(name: string, text: string, style?: FontStyle): string;
|
|
182
|
+
encodeCustomText(name: string, text: string, style?: FontStyle, shape?: boolean): string;
|
|
160
183
|
private buildFontFile2;
|
|
161
184
|
private buildFontDescriptor;
|
|
162
185
|
private buildCIDFont;
|
|
@@ -175,7 +198,10 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
175
198
|
* Standard-14 answers from the AFM `KPX` pairs. An embedded font has no kerning yet (its `kern` /
|
|
176
199
|
* `GPOS` tables are unparsed), so it returns zeros - never a wrong value, just no adjustment.
|
|
177
200
|
*/
|
|
178
|
-
|
|
201
|
+
/** Kerning between adjacent GLYPHS - what a shaped run needs, and what the backend already holds.
|
|
202
|
+
* Same lookup as the character path; `getKerning` is keyed by glyph id either way. */
|
|
203
|
+
getGlyphKernPairs(glyphs: readonly number[], fontFamily: string, fontStyle: FontStyle): number[];
|
|
204
|
+
getKernPairs(text: string, fontFamily: string, fontStyle: FontStyle, ligatures?: boolean): number[];
|
|
179
205
|
/**
|
|
180
206
|
* Where the glyphs of `text` put ink inside a horizontal band, as x-intervals in POINTS measured
|
|
181
207
|
* from the start of the run. This is what an underline steps around ("skip-ink").
|
|
@@ -185,7 +211,7 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
185
211
|
* Callers must check `isCustomFont` first rather than treat an empty result as "no descenders".
|
|
186
212
|
*/
|
|
187
213
|
getInkSpansInBand(text: string, fontSize: number, fontFamily: string, fontStyle: FontStyle, bandTop: number, bandBottom: number, letterSpacing?: number): Array<[number, number]>;
|
|
188
|
-
getStringWidth(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle): number;
|
|
214
|
+
getStringWidth(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle, ligatures?: boolean): number;
|
|
189
215
|
private getAVMParserByFont;
|
|
190
216
|
getCharWidth(char: string, fontSize: number, fullFontName?: string, fontName?: string, fontStyle?: FontStyle): number;
|
|
191
217
|
getAllFontsRaw(): Map<string, FontIndexes>;
|
|
@@ -5,9 +5,26 @@ import { zlibSync } from "fflate";
|
|
|
5
5
|
import { bytesFromLatin1, latin1FromBytes } from "./bytes.js";
|
|
6
6
|
import { AFMParser } from "./afm-parser.js";
|
|
7
7
|
import { mergeSpans, TTFParser } from "./ttf-parser.js";
|
|
8
|
-
import { isWoff, woffToSfnt } from "./woff.js";
|
|
8
|
+
import { WoffError, isWoff, woffToSfnt } from "./woff.js";
|
|
9
|
+
import { isWoff2 } from "./woff2.js";
|
|
9
10
|
import { subsetTTF } from "./ttf-subsetter.js";
|
|
10
11
|
import { shapeRun } from "../text/shape.js";
|
|
12
|
+
/** What Latin text is shaped with unless a run says otherwise: the ligatures a font's designer
|
|
13
|
+
* drew for it. On by default like kerning - CSS, browsers and every other renderer do the same,
|
|
14
|
+
* and nobody should have to know the word to get text that is set properly. */
|
|
15
|
+
const LATIN_FEATURES = ["liga"];
|
|
16
|
+
/** Stable, so `features(false)` does not allocate per call. */
|
|
17
|
+
const NO_FEATURES = [];
|
|
18
|
+
/** A feature set's cache key. Memoised only for the two arrays this module owns - a caller's array is
|
|
19
|
+
* joined every time, since holding on to its key would outlive a mutation of it. */
|
|
20
|
+
const OWN_FEATURE_KEYS = new Map([
|
|
21
|
+
[LATIN_FEATURES, LATIN_FEATURES.join(",")],
|
|
22
|
+
[NO_FEATURES, ""],
|
|
23
|
+
]);
|
|
24
|
+
export function featureKey(f) {
|
|
25
|
+
return OWN_FEATURE_KEYS.get(f) ?? f.join(",");
|
|
26
|
+
}
|
|
27
|
+
const features = (ligatures) => ligatures ? LATIN_FEATURES : NO_FEATURES;
|
|
11
28
|
import { getArrayBuffer, isWindows1252 } from "./utf8-to-windows1252-encoder.js";
|
|
12
29
|
import { isEmojiCodePoint } from "../text/emoji-codepoints.js";
|
|
13
30
|
import { StructTree } from "./struct-tree.js";
|
|
@@ -164,7 +181,12 @@ export class PDFObjectManager {
|
|
|
164
181
|
// program is subsetted and the objects filled in by finalizeCustomFonts(), after the render pass.
|
|
165
182
|
this.customFontEmit = new Map();
|
|
166
183
|
// Shaped runs, keyed by font + text; `null` means "asked, and there was nothing to shape".
|
|
184
|
+
/** Shaped runs, cached face -> feature set -> text. Nested, not one map under a composed key: that
|
|
185
|
+
* key would carry the whole run's text, so every lookup would copy it into a fresh string. */
|
|
167
186
|
this.shapeCache = new Map();
|
|
187
|
+
/** Bumped by every font registration; `runAdvance` watches it to drop memoised widths, since a
|
|
188
|
+
* family name can mean a different face afterwards. */
|
|
189
|
+
this.fontEpoch = 0;
|
|
168
190
|
// Per font, glyph -> the code points it stands for. A shaped glyph is often absent from the cmap (a
|
|
169
191
|
// ligature) or maps back to a presentation form, so `ToUnicode` cannot come from the cmap alone.
|
|
170
192
|
this.shapedOrigins = new Map();
|
|
@@ -189,6 +211,15 @@ export class PDFObjectManager {
|
|
|
189
211
|
// Interactive form fields (AcroForm). Empty unless a form field was placed; then finalize() emits the
|
|
190
212
|
// catalog /AcroForm dict and each field's widget lands in its page /Annots.
|
|
191
213
|
this._acroform = new AcroFormCollector();
|
|
214
|
+
// Overflow policy for unbreakable content taller than a page region (default "error"); the renderer
|
|
215
|
+
// seeds it into the layout-context root so packChildren can act on it.
|
|
216
|
+
/**
|
|
217
|
+
* Code points no font in the document could draw, so they were removed rather than emitted as
|
|
218
|
+
* `.notdef` (which PDF/A forbids). Collected here because this object is the one thing threaded
|
|
219
|
+
* through BOTH passes; the caller gets them back with the bytes, so an application can say which
|
|
220
|
+
* character went missing instead of a server log nobody reads.
|
|
221
|
+
*/
|
|
222
|
+
this.missingGlyphs = new Set();
|
|
192
223
|
// Whether to kern. Document-level: measuring (via `runAdvance`, which reads this off the metrics)
|
|
193
224
|
// and drawing (the backend emits a `TJ`) both consult it, so they can never disagree. OFF while the
|
|
194
225
|
// embedded-font path (kern/GPOS) is unbuilt - a standard-14-only kerning would set the 14 system
|
|
@@ -216,8 +247,14 @@ export class PDFObjectManager {
|
|
|
216
247
|
setCompress(on) {
|
|
217
248
|
this.compress = on;
|
|
218
249
|
}
|
|
219
|
-
|
|
220
|
-
|
|
250
|
+
reportMissingGlyph(codePoints) {
|
|
251
|
+
for (const cp of codePoints)
|
|
252
|
+
this.missingGlyphs.add(cp);
|
|
253
|
+
}
|
|
254
|
+
/** The dropped code points, in the order first seen. Empty for a document that hit none. */
|
|
255
|
+
getMissingGlyphs() {
|
|
256
|
+
return [...this.missingGlyphs];
|
|
257
|
+
}
|
|
221
258
|
setOverflowPolicy(policy) {
|
|
222
259
|
this.overflowPolicy = policy;
|
|
223
260
|
}
|
|
@@ -528,6 +565,7 @@ endstream`;
|
|
|
528
565
|
}
|
|
529
566
|
// Registers a font
|
|
530
567
|
registerFont(fontName, fontStyle = FontStyle.Normal, fullName = fontName) {
|
|
568
|
+
this.fontEpoch++;
|
|
531
569
|
if (this.fonts.hasFont(fontName, fontStyle)) {
|
|
532
570
|
return this.fonts.getFont(fontName, fontStyle); // Already exists? Return it!
|
|
533
571
|
}
|
|
@@ -558,6 +596,7 @@ endstream`;
|
|
|
558
596
|
// metrics and emits its PDF font objects. All variants share the family `name`; bold/italic are
|
|
559
597
|
// separate .ttf files registered under the same name with a different style.
|
|
560
598
|
registerCustomFont(name, data, style = FontStyle.Normal) {
|
|
599
|
+
this.fontEpoch++;
|
|
561
600
|
let byStyle = this.customFonts.get(name);
|
|
562
601
|
if (!byStyle) {
|
|
563
602
|
byStyle = new Map();
|
|
@@ -568,6 +607,14 @@ endstream`;
|
|
|
568
607
|
// A WOFF is the same sfnt behind a small wrapper, so it is unpacked HERE - the one place every
|
|
569
608
|
// font path meets (a file, raw bytes, a URL, a browser upload) - and TTFParser never learns a
|
|
570
609
|
// second container format.
|
|
610
|
+
// WOFF2 cannot be unwrapped here: its Brotli decoder is imported lazily, which makes it async,
|
|
611
|
+
// and this is a synchronous constructor path. `renderPdf` unwraps it one step earlier - so the
|
|
612
|
+
// only way to arrive here still packed is by calling the engine directly.
|
|
613
|
+
if (isWoff2(data)) {
|
|
614
|
+
throw new WoffError("a WOFF2 font reached the object manager still packed. Pass it through " +
|
|
615
|
+
"renderToBytes(doc, { fonts }) or doc.addFont(...) - unpacking WOFF2 needs Brotli, which " +
|
|
616
|
+
"is loaded asynchronously, so it happens one step earlier in renderPdf.");
|
|
617
|
+
}
|
|
571
618
|
byStyle.set(style, new TTFParser(isWoff(data) ? woffToSfnt(data) : data));
|
|
572
619
|
this.verticalsCache.delete(name); // this family now answers from the .ttf, not from an AFM
|
|
573
620
|
this.decorationCache.delete(name);
|
|
@@ -694,6 +741,23 @@ endstream`;
|
|
|
694
741
|
return ttf.getGlyphIndex(codePoint) !== 0;
|
|
695
742
|
return isWindows1252(codePoint);
|
|
696
743
|
}
|
|
744
|
+
/**
|
|
745
|
+
* Whether the document draws this code point through the colour-emoji path instead of the text
|
|
746
|
+
* font - a fallback emoji FONT, or an image source (`Document({ emoji })`). Such a code point is
|
|
747
|
+
* missing from the text font ON PURPOSE, so glyph coverage must leave it alone. Mirrors exactly
|
|
748
|
+
* what `getCharWidth` measures it as, or a dropped emoji would have been measured and not drawn.
|
|
749
|
+
*/
|
|
750
|
+
rendersAsEmoji(codePoint, fontFamily, fontStyle) {
|
|
751
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
752
|
+
if (ttf && this.colorRenders(ttf, codePoint))
|
|
753
|
+
return true;
|
|
754
|
+
if (this.emojiFontName && this.emojiFontName !== fontFamily) {
|
|
755
|
+
const emoji = this.getColorFont(this.emojiFontName, fontStyle);
|
|
756
|
+
if (emoji && this.colorRenders(emoji, codePoint))
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
759
|
+
return this.emojiImage !== undefined && isEmojiCodePoint(codePoint);
|
|
760
|
+
}
|
|
697
761
|
colorRenders(ttf, codePoint) {
|
|
698
762
|
return ttf.getColorGlyph(ttf.getGlyphIndex(codePoint)) !== null;
|
|
699
763
|
}
|
|
@@ -708,25 +772,30 @@ endstream`;
|
|
|
708
772
|
}
|
|
709
773
|
/** How many glyphs a run draws - its code-point count unless a ligature merged some. `letterSpacing`
|
|
710
774
|
* is per glyph (`Tc`), so this is what it multiplies. */
|
|
711
|
-
shapedGlyphCount(text, fontFamily, fontStyle) {
|
|
712
|
-
return this.shapeText(text, fontFamily, fontStyle)?.length;
|
|
775
|
+
shapedGlyphCount(text, fontFamily, fontStyle, ligatures = true) {
|
|
776
|
+
return this.shapeText(text, fontFamily, fontStyle, features(ligatures))?.length;
|
|
713
777
|
}
|
|
714
778
|
/**
|
|
715
779
|
* The shaped glyphs of a run, or undefined when nothing needed shaping. The ONE place shaping is
|
|
716
780
|
* decided, so measuring and drawing cannot disagree. Memoised: a paragraph is measured several
|
|
717
781
|
* times (layout, pagination, drawing).
|
|
718
782
|
*/
|
|
719
|
-
shapeText(text, fontFamily, fontStyle = FontStyle.Normal) {
|
|
783
|
+
shapeText(text, fontFamily, fontStyle = FontStyle.Normal, features = LATIN_FEATURES) {
|
|
720
784
|
const resolved = this.resolveCustomStyle(fontFamily, fontStyle);
|
|
721
785
|
if (!resolved)
|
|
722
786
|
return undefined;
|
|
723
|
-
const
|
|
724
|
-
|
|
787
|
+
const ttf = this.customFonts.get(fontFamily).get(resolved);
|
|
788
|
+
let byFeature = this.shapeCache.get(ttf);
|
|
789
|
+
if (!byFeature)
|
|
790
|
+
this.shapeCache.set(ttf, (byFeature = new Map()));
|
|
791
|
+
let byText = byFeature.get(featureKey(features));
|
|
792
|
+
if (!byText)
|
|
793
|
+
byFeature.set(featureKey(features), (byText = new Map()));
|
|
794
|
+
const cached = byText.get(text);
|
|
725
795
|
if (cached !== undefined)
|
|
726
796
|
return cached ?? undefined;
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
this.shapeCache.set(key, shaped);
|
|
797
|
+
const shaped = shapeRun([...text].map((c) => c.codePointAt(0)), ttf, features) ?? null;
|
|
798
|
+
byText.set(text, shaped);
|
|
730
799
|
if (shaped) {
|
|
731
800
|
// Recorded at shaping time - the only moment the glyph and its code points are known together.
|
|
732
801
|
const fontKey = this.customKey(fontFamily, resolved);
|
|
@@ -755,7 +824,7 @@ endstream`;
|
|
|
755
824
|
}
|
|
756
825
|
// Encodes text as a hex Identity-H string for an embedded font's Tj operator: each codepoint
|
|
757
826
|
// becomes its 2-byte glyph id (CID == GID under /CIDToGIDMap /Identity).
|
|
758
|
-
encodeCustomText(name, text, style = FontStyle.Normal) {
|
|
827
|
+
encodeCustomText(name, text, style = FontStyle.Normal, shape = true) {
|
|
759
828
|
const resolved = this.resolveCustomStyle(name, style);
|
|
760
829
|
if (!resolved)
|
|
761
830
|
return "";
|
|
@@ -765,7 +834,9 @@ endstream`;
|
|
|
765
834
|
const { ttf, used } = emit;
|
|
766
835
|
const hex4 = (gid) => gid.toString(16).padStart(4, "0").toUpperCase();
|
|
767
836
|
// The SAME shaped run the measuring path used, so the glyphs drawn are the glyphs measured.
|
|
768
|
-
|
|
837
|
+
// `shape: false` is for a caller that has ALREADY decided the run is unshaped - the renderer puts
|
|
838
|
+
// shaped glyphs in the IR node, so the backend's text path is the unshaped one by definition.
|
|
839
|
+
const shaped = shape ? this.shapeText(text, name, style) : undefined;
|
|
769
840
|
if (shaped) {
|
|
770
841
|
let out = "";
|
|
771
842
|
for (const g of shaped) {
|
|
@@ -900,15 +971,30 @@ endstream`;
|
|
|
900
971
|
* Standard-14 answers from the AFM `KPX` pairs. An embedded font has no kerning yet (its `kern` /
|
|
901
972
|
* `GPOS` tables are unparsed), so it returns zeros - never a wrong value, just no adjustment.
|
|
902
973
|
*/
|
|
903
|
-
|
|
974
|
+
/** Kerning between adjacent GLYPHS - what a shaped run needs, and what the backend already holds.
|
|
975
|
+
* Same lookup as the character path; `getKerning` is keyed by glyph id either way. */
|
|
976
|
+
getGlyphKernPairs(glyphs, fontFamily, fontStyle) {
|
|
977
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
978
|
+
if (!ttf)
|
|
979
|
+
return [];
|
|
980
|
+
const pairs = [];
|
|
981
|
+
for (let i = 0; i < glyphs.length - 1; i++) {
|
|
982
|
+
pairs.push(ttf.getKerning(glyphs[i], glyphs[i + 1]));
|
|
983
|
+
}
|
|
984
|
+
return pairs;
|
|
985
|
+
}
|
|
986
|
+
getKernPairs(text, fontFamily, fontStyle, ligatures = true) {
|
|
904
987
|
const chars = [...text];
|
|
905
988
|
if (chars.length < 2)
|
|
906
989
|
return [];
|
|
907
|
-
// A shaped run
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
|
|
911
|
-
|
|
990
|
+
// A shaped run kerns over its SHAPED glyphs, not its characters: after shaping there may be fewer
|
|
991
|
+
// glyphs than code points (a ligature), and the pairs that matter are the ones actually drawn.
|
|
992
|
+
// `getKerning` is keyed by glyph id anyway, so this is the same lookup - and the backend now emits
|
|
993
|
+
// a `TJ` over glyph chunks, which is what used to be missing. One adjustment per adjacent DRAWN
|
|
994
|
+
// pair, either path, so `runAdvance` measures exactly what is drawn.
|
|
995
|
+
const shaped = this.shapeText(text, fontFamily, fontStyle, features(ligatures));
|
|
996
|
+
if (shaped)
|
|
997
|
+
return this.getGlyphKernPairs(shaped.map((g) => g.glyph), fontFamily, fontStyle);
|
|
912
998
|
const out = [];
|
|
913
999
|
// An embedded font kerns by GLYPH id (kern table / GPOS); a standard-14 one by character (AFM).
|
|
914
1000
|
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
@@ -959,10 +1045,10 @@ endstream`;
|
|
|
959
1045
|
return mergeSpans(spans);
|
|
960
1046
|
}
|
|
961
1047
|
// Returns the current width of a text, included kernings
|
|
962
|
-
getStringWidth(text, fontFamily, fontSize, fontStyle) {
|
|
1048
|
+
getStringWidth(text, fontFamily, fontSize, fontStyle, ligatures = true) {
|
|
963
1049
|
// Shaping first: a joined run is narrower than the same letters apart, so measuring the unshaped
|
|
964
1050
|
// form while drawing the shaped one would break every line. Both sides ask `shapeText`.
|
|
965
|
-
const shaped = this.shapeText(text, fontFamily, fontStyle);
|
|
1051
|
+
const shaped = this.shapeText(text, fontFamily, fontStyle, features(ligatures));
|
|
966
1052
|
if (shaped) {
|
|
967
1053
|
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
968
1054
|
return shaped.reduce((w, g) => w + ttf.unitsToPoints(g.advance, fontSize), 0);
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when an element reaches the render pass with no registered renderer.
|
|
3
|
+
*
|
|
4
|
+
* This used to return undefined and every call site skipped the element silently, so a whole document
|
|
5
|
+
* could serialize to an empty content stream and still be a valid PDF - a blank page, no warning
|
|
6
|
+
* (ISSUE-11). The registry is keyed on the CONSTRUCTOR, which makes it the one place in the engine that
|
|
7
|
+
* two loaded copies of the library break: elements built by copy A are invisible to copy B.
|
|
8
|
+
*/
|
|
9
|
+
export declare class MissingRendererError extends Error {
|
|
10
|
+
readonly elementName: string;
|
|
11
|
+
constructor(elementName: string);
|
|
12
|
+
}
|
|
1
13
|
export declare class RendererRegistry {
|
|
2
14
|
private static renderers;
|
|
3
15
|
static register(elementClass: Function, renderer: Function): void;
|
|
4
|
-
static getRenderer(element: object): Function
|
|
16
|
+
static getRenderer(element: object): Function;
|
|
5
17
|
static isRendererAsync(renderer: Function): boolean;
|
|
6
18
|
}
|
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when an element reaches the render pass with no registered renderer.
|
|
3
|
+
*
|
|
4
|
+
* This used to return undefined and every call site skipped the element silently, so a whole document
|
|
5
|
+
* could serialize to an empty content stream and still be a valid PDF - a blank page, no warning
|
|
6
|
+
* (ISSUE-11). The registry is keyed on the CONSTRUCTOR, which makes it the one place in the engine that
|
|
7
|
+
* two loaded copies of the library break: elements built by copy A are invisible to copy B.
|
|
8
|
+
*/
|
|
9
|
+
export class MissingRendererError extends Error {
|
|
10
|
+
constructor(elementName) {
|
|
11
|
+
super(`@jasy/pdf: no renderer registered for "${elementName}". Either the element was never registered ` +
|
|
12
|
+
`in PDFRenderer.render(), or two copies of @jasy/pdf are loaded - the registry is keyed on the ` +
|
|
13
|
+
`element's constructor, so elements built by one copy cannot be rendered by the other. ` +
|
|
14
|
+
`Check the dependency tree for a duplicate (e.g. \`pnpm why @jasy/pdf\`).`);
|
|
15
|
+
this.elementName = elementName;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
1
18
|
export class RendererRegistry {
|
|
2
19
|
static register(elementClass, renderer) {
|
|
3
20
|
if (!RendererRegistry.renderers.has(elementClass)) {
|
|
@@ -5,8 +22,12 @@ export class RendererRegistry {
|
|
|
5
22
|
}
|
|
6
23
|
}
|
|
7
24
|
// Keyed on the element's constructor, so it only needs an object - not `any`.
|
|
25
|
+
// Throws rather than returning undefined: a missing renderer is always a bug, and skipping it drew nothing.
|
|
8
26
|
static getRenderer(element) {
|
|
9
|
-
|
|
27
|
+
const renderer = RendererRegistry.renderers.get(element.constructor);
|
|
28
|
+
if (!renderer)
|
|
29
|
+
throw new MissingRendererError(element.constructor.name);
|
|
30
|
+
return renderer;
|
|
10
31
|
}
|
|
11
32
|
static isRendererAsync(renderer) {
|
|
12
33
|
return renderer.constructor.name === "AsyncFunction";
|
|
@@ -39,9 +39,11 @@ const CP1252_FROM_UNICODE = {
|
|
|
39
39
|
* except the C1 range, which the table above fills with printable punctuation instead.
|
|
40
40
|
*/
|
|
41
41
|
export function isWindows1252(codePoint) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
// Latin-1 first: every key in the table above is > 0xff, so this range can never be in it. Order
|
|
43
|
+
// matters - this runs per character of every measured string, and the object lookup is the slow half.
|
|
44
|
+
if (codePoint <= 0xff)
|
|
45
|
+
return codePoint < 0x80 || codePoint > 0x9f;
|
|
46
|
+
return codePoint in CP1252_FROM_UNICODE;
|
|
45
47
|
}
|
|
46
48
|
/** Encodes a JavaScript string to a Windows-1252 byte buffer (the PDF text encoding). */
|
|
47
49
|
export function getArrayBuffer(data) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const isWoff2: (bytes: Uint8Array) => boolean;
|
|
2
|
+
/**
|
|
3
|
+
* The whole container: header, directory, one Brotli stream holding every table back to back, then
|
|
4
|
+
* `glyf`/`loca` put back together and the sfnt reassembled around them.
|
|
5
|
+
*/
|
|
6
|
+
export declare function woff2ToSfnt(woff2: Uint8Array): Promise<Uint8Array>;
|