@jasy/pdf 1.0.0-alpha.9 → 1.0.0-beta.1
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 +4 -3
- 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/flex-layout.js +67 -18
- package/dist/utils/font-metrics.d.ts +9 -3
- package/dist/utils/pdf-object-manager.d.ts +25 -5
- package/dist/utils/pdf-object-manager.js +82 -18
- package/dist/utils/renderer-registry.d.ts +13 -1
- package/dist/utils/renderer-registry.js +22 -1
- 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`). */
|
|
@@ -22,10 +22,16 @@ export interface FontMetrics {
|
|
|
22
22
|
readonly kerningEnabled: boolean;
|
|
23
23
|
/** Per-adjacent-pair kerning of `text`, in em/1000 (negative tightens); length `codePoints - 1`,
|
|
24
24
|
* zero next to a space. Only meaningful when `kerningEnabled`. */
|
|
25
|
-
getKernPairs(text: string, fontFamily: string, fontStyle: FontStyle): number[];
|
|
25
|
+
getKernPairs(text: string, fontFamily: string, fontStyle: FontStyle, ligatures?: boolean): number[];
|
|
26
26
|
/** Whether this family can draw the code point at all - what picks a face out of a fallback stack. */
|
|
27
27
|
hasGlyph(codePoint: number, fontFamily: string, fontStyle: FontStyle): boolean;
|
|
28
|
+
/** Whether the document draws this code point as colour emoji rather than from the text font - it
|
|
29
|
+
* is missing from that font on purpose, so glyph coverage must not touch it. */
|
|
30
|
+
rendersAsEmoji?(codePoint: number, fontFamily: string, fontStyle: FontStyle): boolean;
|
|
31
|
+
/** Told when a code point had to be removed because no font could draw it. Optional: a metrics
|
|
32
|
+
* object used only for measuring (a test double) has nothing to report to. */
|
|
33
|
+
reportMissingGlyph?(codePoints: number[]): void;
|
|
28
34
|
/** How many glyphs the run will DRAW, when a ligature made that differ from its code-point count.
|
|
29
35
|
* Absent or `undefined` means "the same", which is every Latin run. */
|
|
30
|
-
shapedGlyphCount?(text: string, fontFamily?: string, fontStyle?: FontStyle): number | undefined;
|
|
36
|
+
shapedGlyphCount?(text: string, fontFamily?: string, fontStyle?: FontStyle, ligatures?: boolean): number | undefined;
|
|
31
37
|
}
|
|
@@ -65,6 +65,16 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
65
65
|
addObject(content: string): number;
|
|
66
66
|
replaceObject(objectNumber: number, content: string): void;
|
|
67
67
|
setCompress(on: boolean): void;
|
|
68
|
+
/**
|
|
69
|
+
* Code points no font in the document could draw, so they were removed rather than emitted as
|
|
70
|
+
* `.notdef` (which PDF/A forbids). Collected here because this object is the one thing threaded
|
|
71
|
+
* through BOTH passes; the caller gets them back with the bytes, so an application can say which
|
|
72
|
+
* character went missing instead of a server log nobody reads.
|
|
73
|
+
*/
|
|
74
|
+
private readonly missingGlyphs;
|
|
75
|
+
reportMissingGlyph(codePoints: number[]): void;
|
|
76
|
+
/** The dropped code points, in the order first seen. Empty for a document that hit none. */
|
|
77
|
+
getMissingGlyphs(): number[];
|
|
68
78
|
setOverflowPolicy(policy: OverflowPolicy): void;
|
|
69
79
|
getOverflowPolicy(): OverflowPolicy;
|
|
70
80
|
private encToken;
|
|
@@ -142,21 +152,28 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
142
152
|
* Used to pick a family out of a fallback stack, per code point.
|
|
143
153
|
*/
|
|
144
154
|
hasGlyph(codePoint: number, fontFamily: string, fontStyle?: FontStyle): boolean;
|
|
155
|
+
/**
|
|
156
|
+
* Whether the document draws this code point through the colour-emoji path instead of the text
|
|
157
|
+
* font - a fallback emoji FONT, or an image source (`Document({ emoji })`). Such a code point is
|
|
158
|
+
* missing from the text font ON PURPOSE, so glyph coverage must leave it alone. Mirrors exactly
|
|
159
|
+
* what `getCharWidth` measures it as, or a dropped emoji would have been measured and not drawn.
|
|
160
|
+
*/
|
|
161
|
+
rendersAsEmoji(codePoint: number, fontFamily: string, fontStyle: FontStyle): boolean;
|
|
145
162
|
private colorRenders;
|
|
146
163
|
getCustomFontResource(name: string, style?: FontStyle): FontIndexes | undefined;
|
|
147
164
|
/** How many glyphs a run draws - its code-point count unless a ligature merged some. `letterSpacing`
|
|
148
165
|
* is per glyph (`Tc`), so this is what it multiplies. */
|
|
149
|
-
shapedGlyphCount(text: string, fontFamily?: string, fontStyle?: FontStyle): number | undefined;
|
|
166
|
+
shapedGlyphCount(text: string, fontFamily?: string, fontStyle?: FontStyle, ligatures?: boolean): number | undefined;
|
|
150
167
|
/**
|
|
151
168
|
* The shaped glyphs of a run, or undefined when nothing needed shaping. The ONE place shaping is
|
|
152
169
|
* decided, so measuring and drawing cannot disagree. Memoised: a paragraph is measured several
|
|
153
170
|
* times (layout, pagination, drawing).
|
|
154
171
|
*/
|
|
155
|
-
shapeText(text: string, fontFamily?: string, fontStyle?: FontStyle): ShapedGlyph[] | undefined;
|
|
172
|
+
shapeText(text: string, fontFamily?: string, fontStyle?: FontStyle, features?: readonly string[]): ShapedGlyph[] | undefined;
|
|
156
173
|
/** Hex Identity-H for glyphs the producer already resolved (a shaped run), which cannot be given as
|
|
157
174
|
* text. Recording each keeps it in the subset. */
|
|
158
175
|
registerGlyphs(name: string, glyphs: readonly number[], style?: FontStyle): string;
|
|
159
|
-
encodeCustomText(name: string, text: string, style?: FontStyle): string;
|
|
176
|
+
encodeCustomText(name: string, text: string, style?: FontStyle, shape?: boolean): string;
|
|
160
177
|
private buildFontFile2;
|
|
161
178
|
private buildFontDescriptor;
|
|
162
179
|
private buildCIDFont;
|
|
@@ -175,7 +192,10 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
175
192
|
* Standard-14 answers from the AFM `KPX` pairs. An embedded font has no kerning yet (its `kern` /
|
|
176
193
|
* `GPOS` tables are unparsed), so it returns zeros - never a wrong value, just no adjustment.
|
|
177
194
|
*/
|
|
178
|
-
|
|
195
|
+
/** Kerning between adjacent GLYPHS - what a shaped run needs, and what the backend already holds.
|
|
196
|
+
* Same lookup as the character path; `getKerning` is keyed by glyph id either way. */
|
|
197
|
+
getGlyphKernPairs(glyphs: readonly number[], fontFamily: string, fontStyle: FontStyle): number[];
|
|
198
|
+
getKernPairs(text: string, fontFamily: string, fontStyle: FontStyle, ligatures?: boolean): number[];
|
|
179
199
|
/**
|
|
180
200
|
* Where the glyphs of `text` put ink inside a horizontal band, as x-intervals in POINTS measured
|
|
181
201
|
* from the start of the run. This is what an underline steps around ("skip-ink").
|
|
@@ -185,7 +205,7 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
185
205
|
* Callers must check `isCustomFont` first rather than treat an empty result as "no descenders".
|
|
186
206
|
*/
|
|
187
207
|
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;
|
|
208
|
+
getStringWidth(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle, ligatures?: boolean): number;
|
|
189
209
|
private getAVMParserByFont;
|
|
190
210
|
getCharWidth(char: string, fontSize: number, fullFontName?: string, fontName?: string, fontStyle?: FontStyle): number;
|
|
191
211
|
getAllFontsRaw(): Map<string, FontIndexes>;
|
|
@@ -5,9 +5,16 @@ 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
|
+
/** What a run is shaped with. Arabic joining is unconditional; this is only the Latin part. */
|
|
17
|
+
const features = (ligatures) => (ligatures ? LATIN_FEATURES : []);
|
|
11
18
|
import { getArrayBuffer, isWindows1252 } from "./utf8-to-windows1252-encoder.js";
|
|
12
19
|
import { isEmojiCodePoint } from "../text/emoji-codepoints.js";
|
|
13
20
|
import { StructTree } from "./struct-tree.js";
|
|
@@ -189,6 +196,15 @@ export class PDFObjectManager {
|
|
|
189
196
|
// Interactive form fields (AcroForm). Empty unless a form field was placed; then finalize() emits the
|
|
190
197
|
// catalog /AcroForm dict and each field's widget lands in its page /Annots.
|
|
191
198
|
this._acroform = new AcroFormCollector();
|
|
199
|
+
// Overflow policy for unbreakable content taller than a page region (default "error"); the renderer
|
|
200
|
+
// seeds it into the layout-context root so packChildren can act on it.
|
|
201
|
+
/**
|
|
202
|
+
* Code points no font in the document could draw, so they were removed rather than emitted as
|
|
203
|
+
* `.notdef` (which PDF/A forbids). Collected here because this object is the one thing threaded
|
|
204
|
+
* through BOTH passes; the caller gets them back with the bytes, so an application can say which
|
|
205
|
+
* character went missing instead of a server log nobody reads.
|
|
206
|
+
*/
|
|
207
|
+
this.missingGlyphs = new Set();
|
|
192
208
|
// Whether to kern. Document-level: measuring (via `runAdvance`, which reads this off the metrics)
|
|
193
209
|
// and drawing (the backend emits a `TJ`) both consult it, so they can never disagree. OFF while the
|
|
194
210
|
// embedded-font path (kern/GPOS) is unbuilt - a standard-14-only kerning would set the 14 system
|
|
@@ -216,8 +232,14 @@ export class PDFObjectManager {
|
|
|
216
232
|
setCompress(on) {
|
|
217
233
|
this.compress = on;
|
|
218
234
|
}
|
|
219
|
-
|
|
220
|
-
|
|
235
|
+
reportMissingGlyph(codePoints) {
|
|
236
|
+
for (const cp of codePoints)
|
|
237
|
+
this.missingGlyphs.add(cp);
|
|
238
|
+
}
|
|
239
|
+
/** The dropped code points, in the order first seen. Empty for a document that hit none. */
|
|
240
|
+
getMissingGlyphs() {
|
|
241
|
+
return [...this.missingGlyphs];
|
|
242
|
+
}
|
|
221
243
|
setOverflowPolicy(policy) {
|
|
222
244
|
this.overflowPolicy = policy;
|
|
223
245
|
}
|
|
@@ -568,6 +590,14 @@ endstream`;
|
|
|
568
590
|
// A WOFF is the same sfnt behind a small wrapper, so it is unpacked HERE - the one place every
|
|
569
591
|
// font path meets (a file, raw bytes, a URL, a browser upload) - and TTFParser never learns a
|
|
570
592
|
// second container format.
|
|
593
|
+
// WOFF2 cannot be unwrapped here: its Brotli decoder is imported lazily, which makes it async,
|
|
594
|
+
// and this is a synchronous constructor path. `renderPdf` unwraps it one step earlier - so the
|
|
595
|
+
// only way to arrive here still packed is by calling the engine directly.
|
|
596
|
+
if (isWoff2(data)) {
|
|
597
|
+
throw new WoffError("a WOFF2 font reached the object manager still packed. Pass it through " +
|
|
598
|
+
"renderToBytes(doc, { fonts }) or doc.addFont(...) - unpacking WOFF2 needs Brotli, which " +
|
|
599
|
+
"is loaded asynchronously, so it happens one step earlier in renderPdf.");
|
|
600
|
+
}
|
|
571
601
|
byStyle.set(style, new TTFParser(isWoff(data) ? woffToSfnt(data) : data));
|
|
572
602
|
this.verticalsCache.delete(name); // this family now answers from the .ttf, not from an AFM
|
|
573
603
|
this.decorationCache.delete(name);
|
|
@@ -694,6 +724,23 @@ endstream`;
|
|
|
694
724
|
return ttf.getGlyphIndex(codePoint) !== 0;
|
|
695
725
|
return isWindows1252(codePoint);
|
|
696
726
|
}
|
|
727
|
+
/**
|
|
728
|
+
* Whether the document draws this code point through the colour-emoji path instead of the text
|
|
729
|
+
* font - a fallback emoji FONT, or an image source (`Document({ emoji })`). Such a code point is
|
|
730
|
+
* missing from the text font ON PURPOSE, so glyph coverage must leave it alone. Mirrors exactly
|
|
731
|
+
* what `getCharWidth` measures it as, or a dropped emoji would have been measured and not drawn.
|
|
732
|
+
*/
|
|
733
|
+
rendersAsEmoji(codePoint, fontFamily, fontStyle) {
|
|
734
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
735
|
+
if (ttf && this.colorRenders(ttf, codePoint))
|
|
736
|
+
return true;
|
|
737
|
+
if (this.emojiFontName && this.emojiFontName !== fontFamily) {
|
|
738
|
+
const emoji = this.getColorFont(this.emojiFontName, fontStyle);
|
|
739
|
+
if (emoji && this.colorRenders(emoji, codePoint))
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
return this.emojiImage !== undefined && isEmojiCodePoint(codePoint);
|
|
743
|
+
}
|
|
697
744
|
colorRenders(ttf, codePoint) {
|
|
698
745
|
return ttf.getColorGlyph(ttf.getGlyphIndex(codePoint)) !== null;
|
|
699
746
|
}
|
|
@@ -708,24 +755,24 @@ endstream`;
|
|
|
708
755
|
}
|
|
709
756
|
/** How many glyphs a run draws - its code-point count unless a ligature merged some. `letterSpacing`
|
|
710
757
|
* is per glyph (`Tc`), so this is what it multiplies. */
|
|
711
|
-
shapedGlyphCount(text, fontFamily, fontStyle) {
|
|
712
|
-
return this.shapeText(text, fontFamily, fontStyle)?.length;
|
|
758
|
+
shapedGlyphCount(text, fontFamily, fontStyle, ligatures = true) {
|
|
759
|
+
return this.shapeText(text, fontFamily, fontStyle, features(ligatures))?.length;
|
|
713
760
|
}
|
|
714
761
|
/**
|
|
715
762
|
* The shaped glyphs of a run, or undefined when nothing needed shaping. The ONE place shaping is
|
|
716
763
|
* decided, so measuring and drawing cannot disagree. Memoised: a paragraph is measured several
|
|
717
764
|
* times (layout, pagination, drawing).
|
|
718
765
|
*/
|
|
719
|
-
shapeText(text, fontFamily, fontStyle = FontStyle.Normal) {
|
|
766
|
+
shapeText(text, fontFamily, fontStyle = FontStyle.Normal, features = LATIN_FEATURES) {
|
|
720
767
|
const resolved = this.resolveCustomStyle(fontFamily, fontStyle);
|
|
721
768
|
if (!resolved)
|
|
722
769
|
return undefined;
|
|
723
|
-
const key = `${this.customKey(fontFamily, resolved)}\u0000${text}`;
|
|
770
|
+
const key = `${this.customKey(fontFamily, resolved)}\u0000${features.join(",")}\u0000${text}`;
|
|
724
771
|
const cached = this.shapeCache.get(key);
|
|
725
772
|
if (cached !== undefined)
|
|
726
773
|
return cached ?? undefined;
|
|
727
774
|
const ttf = this.customFonts.get(fontFamily).get(resolved);
|
|
728
|
-
const shaped = shapeRun([...text].map((c) => c.codePointAt(0)), ttf) ?? null;
|
|
775
|
+
const shaped = shapeRun([...text].map((c) => c.codePointAt(0)), ttf, features) ?? null;
|
|
729
776
|
this.shapeCache.set(key, shaped);
|
|
730
777
|
if (shaped) {
|
|
731
778
|
// Recorded at shaping time - the only moment the glyph and its code points are known together.
|
|
@@ -755,7 +802,7 @@ endstream`;
|
|
|
755
802
|
}
|
|
756
803
|
// Encodes text as a hex Identity-H string for an embedded font's Tj operator: each codepoint
|
|
757
804
|
// becomes its 2-byte glyph id (CID == GID under /CIDToGIDMap /Identity).
|
|
758
|
-
encodeCustomText(name, text, style = FontStyle.Normal) {
|
|
805
|
+
encodeCustomText(name, text, style = FontStyle.Normal, shape = true) {
|
|
759
806
|
const resolved = this.resolveCustomStyle(name, style);
|
|
760
807
|
if (!resolved)
|
|
761
808
|
return "";
|
|
@@ -765,7 +812,9 @@ endstream`;
|
|
|
765
812
|
const { ttf, used } = emit;
|
|
766
813
|
const hex4 = (gid) => gid.toString(16).padStart(4, "0").toUpperCase();
|
|
767
814
|
// The SAME shaped run the measuring path used, so the glyphs drawn are the glyphs measured.
|
|
768
|
-
|
|
815
|
+
// `shape: false` is for a caller that has ALREADY decided the run is unshaped - the renderer puts
|
|
816
|
+
// shaped glyphs in the IR node, so the backend's text path is the unshaped one by definition.
|
|
817
|
+
const shaped = shape ? this.shapeText(text, name, style) : undefined;
|
|
769
818
|
if (shaped) {
|
|
770
819
|
let out = "";
|
|
771
820
|
for (const g of shaped) {
|
|
@@ -900,15 +949,30 @@ endstream`;
|
|
|
900
949
|
* Standard-14 answers from the AFM `KPX` pairs. An embedded font has no kerning yet (its `kern` /
|
|
901
950
|
* `GPOS` tables are unparsed), so it returns zeros - never a wrong value, just no adjustment.
|
|
902
951
|
*/
|
|
903
|
-
|
|
952
|
+
/** Kerning between adjacent GLYPHS - what a shaped run needs, and what the backend already holds.
|
|
953
|
+
* Same lookup as the character path; `getKerning` is keyed by glyph id either way. */
|
|
954
|
+
getGlyphKernPairs(glyphs, fontFamily, fontStyle) {
|
|
955
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
956
|
+
if (!ttf)
|
|
957
|
+
return [];
|
|
958
|
+
const pairs = [];
|
|
959
|
+
for (let i = 0; i < glyphs.length - 1; i++) {
|
|
960
|
+
pairs.push(ttf.getKerning(glyphs[i], glyphs[i + 1]));
|
|
961
|
+
}
|
|
962
|
+
return pairs;
|
|
963
|
+
}
|
|
964
|
+
getKernPairs(text, fontFamily, fontStyle, ligatures = true) {
|
|
904
965
|
const chars = [...text];
|
|
905
966
|
if (chars.length < 2)
|
|
906
967
|
return [];
|
|
907
|
-
// A shaped run
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
|
|
911
|
-
|
|
968
|
+
// A shaped run kerns over its SHAPED glyphs, not its characters: after shaping there may be fewer
|
|
969
|
+
// glyphs than code points (a ligature), and the pairs that matter are the ones actually drawn.
|
|
970
|
+
// `getKerning` is keyed by glyph id anyway, so this is the same lookup - and the backend now emits
|
|
971
|
+
// a `TJ` over glyph chunks, which is what used to be missing. One adjustment per adjacent DRAWN
|
|
972
|
+
// pair, either path, so `runAdvance` measures exactly what is drawn.
|
|
973
|
+
const shaped = this.shapeText(text, fontFamily, fontStyle, features(ligatures));
|
|
974
|
+
if (shaped)
|
|
975
|
+
return this.getGlyphKernPairs(shaped.map((g) => g.glyph), fontFamily, fontStyle);
|
|
912
976
|
const out = [];
|
|
913
977
|
// An embedded font kerns by GLYPH id (kern table / GPOS); a standard-14 one by character (AFM).
|
|
914
978
|
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
@@ -959,10 +1023,10 @@ endstream`;
|
|
|
959
1023
|
return mergeSpans(spans);
|
|
960
1024
|
}
|
|
961
1025
|
// Returns the current width of a text, included kernings
|
|
962
|
-
getStringWidth(text, fontFamily, fontSize, fontStyle) {
|
|
1026
|
+
getStringWidth(text, fontFamily, fontSize, fontStyle, ligatures = true) {
|
|
963
1027
|
// Shaping first: a joined run is narrower than the same letters apart, so measuring the unshaped
|
|
964
1028
|
// form while drawing the shaped one would break every line. Both sides ask `shapeText`.
|
|
965
|
-
const shaped = this.shapeText(text, fontFamily, fontStyle);
|
|
1029
|
+
const shaped = this.shapeText(text, fontFamily, fontStyle, features(ligatures));
|
|
966
1030
|
if (shaped) {
|
|
967
1031
|
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
968
1032
|
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";
|
|
@@ -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>;
|