@jasy/pdf 1.0.0-alpha.6 → 1.0.0-alpha.8
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 +7 -7
- package/dist/api/descriptor.js +26 -1
- package/dist/api/forms.d.ts +347 -0
- package/dist/api/forms.js +189 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/layout.d.ts +47 -14
- package/dist/api/layout.js +47 -11
- package/dist/api/structure.d.ts +11 -0
- package/dist/api/structure.js +2 -0
- package/dist/api/text.d.ts +18 -2
- package/dist/api/text.js +15 -0
- package/dist/crypto/legacy-handler.d.ts +43 -0
- package/dist/crypto/legacy-handler.js +145 -0
- package/dist/crypto/security-handler.d.ts +16 -0
- package/dist/crypto/security-handler.js +26 -0
- package/dist/edit/acroform-reader.d.ts +70 -0
- package/dist/edit/acroform-reader.js +126 -0
- package/dist/edit/appearance.d.ts +47 -0
- package/dist/edit/appearance.js +171 -0
- package/dist/edit/document.d.ts +135 -0
- package/dist/edit/document.js +649 -0
- package/dist/edit/fill.d.ts +48 -0
- package/dist/edit/fill.js +395 -0
- package/dist/edit/flatten.d.ts +42 -0
- package/dist/edit/flatten.js +437 -0
- package/dist/edit/index.d.ts +18 -0
- package/dist/edit/index.js +16 -0
- package/dist/edit/lexer.d.ts +30 -0
- package/dist/edit/lexer.js +285 -0
- package/dist/edit/objects.d.ts +63 -0
- package/dist/edit/objects.js +56 -0
- package/dist/edit/writer.d.ts +53 -0
- package/dist/edit/writer.js +261 -0
- package/dist/elements/container-element.d.ts +10 -1
- package/dist/elements/container-element.js +26 -8
- package/dist/elements/forms/checkbox-element.d.ts +51 -0
- package/dist/elements/forms/checkbox-element.js +48 -0
- package/dist/elements/forms/choice-element.d.ts +53 -0
- package/dist/elements/forms/choice-element.js +59 -0
- package/dist/elements/forms/push-button-element.d.ts +56 -0
- package/dist/elements/forms/push-button-element.js +49 -0
- package/dist/elements/forms/radio-element.d.ts +52 -0
- package/dist/elements/forms/radio-element.js +49 -0
- package/dist/elements/forms/signature-element.d.ts +54 -0
- package/dist/elements/forms/signature-element.js +47 -0
- package/dist/elements/forms/text-field-element.d.ts +61 -0
- package/dist/elements/forms/text-field-element.js +54 -0
- package/dist/elements/index.d.ts +8 -0
- package/dist/elements/index.js +8 -0
- package/dist/elements/layout/keep-together-element.d.ts +42 -0
- package/dist/elements/layout/keep-together-element.js +85 -0
- package/dist/elements/layout/padding-element.d.ts +3 -0
- package/dist/elements/layout/padding-element.js +12 -0
- package/dist/elements/layout/page-break-element.d.ts +17 -0
- package/dist/elements/layout/page-break-element.js +24 -0
- package/dist/elements/layout/positioned-element.d.ts +10 -7
- package/dist/elements/layout/positioned-element.js +21 -9
- package/dist/elements/page-element.d.ts +11 -1
- package/dist/elements/page-element.js +41 -16
- package/dist/elements/pdf-element.d.ts +28 -0
- package/dist/elements/pdf-element.js +32 -0
- package/dist/elements/rectangle-element.d.ts +10 -1
- package/dist/elements/rectangle-element.js +27 -6
- package/dist/elements/row-element.d.ts +9 -1
- package/dist/elements/row-element.js +9 -1
- package/dist/elements/text-element.d.ts +30 -4
- package/dist/elements/text-element.js +40 -17
- package/dist/forms/acroform.d.ts +63 -0
- package/dist/forms/acroform.js +427 -0
- package/dist/forms/appearance.d.ts +58 -0
- package/dist/forms/appearance.js +204 -0
- package/dist/forms/field.d.ts +164 -0
- package/dist/forms/field.js +1 -0
- package/dist/forms/pdf.d.ts +19 -0
- package/dist/forms/pdf.js +28 -0
- package/dist/ir/display-list.d.ts +18 -1
- package/dist/layout/fragmentation.d.ts +12 -0
- package/dist/layout/fragmentation.js +45 -8
- package/dist/renderer/form-field-renderer.d.ts +20 -0
- package/dist/renderer/form-field-renderer.js +12 -0
- package/dist/renderer/keep-together-renderer.d.ts +10 -0
- package/dist/renderer/keep-together-renderer.js +12 -0
- package/dist/renderer/page-break-renderer.d.ts +14 -0
- package/dist/renderer/page-break-renderer.js +15 -0
- package/dist/renderer/page-renderer.js +8 -2
- package/dist/renderer/pdf-backend.d.ts +7 -0
- package/dist/renderer/pdf-backend.js +48 -6
- package/dist/renderer/pdf-document-class.js +3 -0
- package/dist/renderer/pdf-document-renderer.js +26 -7
- package/dist/renderer/pdf-renderer.js +25 -1
- package/dist/renderer/text-renderer.d.ts +1 -1
- package/dist/renderer/text-renderer.js +107 -59
- package/dist/text/advance.d.ts +34 -0
- package/dist/text/advance.js +42 -0
- package/dist/text/line-breaker.d.ts +9 -8
- package/dist/text/line-breaker.js +28 -21
- package/dist/text/line-metrics.d.ts +49 -0
- package/dist/text/line-metrics.js +57 -0
- package/dist/text/text-decoration.d.ts +50 -0
- package/dist/text/text-decoration.js +62 -0
- package/dist/text/text-style.d.ts +13 -3
- package/dist/text/text-style.js +10 -3
- package/dist/utils/afm-parser.d.ts +34 -0
- package/dist/utils/afm-parser.js +67 -0
- package/dist/utils/dest-registry.d.ts +5 -2
- package/dist/utils/dest-registry.js +5 -5
- package/dist/utils/font-metrics.d.ts +14 -0
- package/dist/utils/outline.js +1 -2
- package/dist/utils/pdf-object-manager.d.ts +45 -1
- package/dist/utils/pdf-object-manager.js +193 -26
- package/dist/utils/struct-tree.js +2 -3
- package/dist/utils/ttf-parser.d.ts +53 -0
- package/dist/utils/ttf-parser.js +368 -0
- package/dist/validators/element-validator.js +7 -3
- package/package.json +9 -2
package/dist/utils/ttf-parser.js
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
// text width the same way AFMParser does for the standard-14. Embedding/subsetting come later.
|
|
4
4
|
import { i16, latin1FromBytes, u8, u16, u32 } from "./bytes.js";
|
|
5
5
|
const IDENTITY = [1, 0, 0, 1, 0, 0];
|
|
6
|
+
/** Sorts x-intervals and unions the overlapping ones. */
|
|
7
|
+
export function mergeSpans(spans) {
|
|
8
|
+
if (spans.length === 0)
|
|
9
|
+
return spans;
|
|
10
|
+
const sorted = [...spans].sort((a, b) => a[0] - b[0]);
|
|
11
|
+
const out = [sorted[0]];
|
|
12
|
+
for (const [start, end] of sorted.slice(1)) {
|
|
13
|
+
const last = out[out.length - 1];
|
|
14
|
+
if (start <= last[1])
|
|
15
|
+
last[1] = Math.max(last[1], end);
|
|
16
|
+
else
|
|
17
|
+
out.push([start, end]);
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
6
21
|
function isIdentity(m) {
|
|
7
22
|
return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
|
|
8
23
|
}
|
|
@@ -37,6 +52,15 @@ export class TTFParser {
|
|
|
37
52
|
// FontDescriptor metrics, in PDF glyph space (1000 units / em).
|
|
38
53
|
this.ascent = 0;
|
|
39
54
|
this.descent = 0;
|
|
55
|
+
// Extra leading the font asks for between two lines (hhea), same glyph space. Often 0.
|
|
56
|
+
this.lineGap = 0;
|
|
57
|
+
// Decoration metrics in RAW font units (divide by unitsPerEm). `post` always carries the underline
|
|
58
|
+
// pair; `OS/2` only carries sxHeight/sCapHeight from version 2 on, so those may stay 0 and are
|
|
59
|
+
// then measured off the glyph outlines instead (see `decoration()`).
|
|
60
|
+
this.underlinePositionRaw = 0; // negative = below the baseline
|
|
61
|
+
this.underlineThicknessRaw = 0;
|
|
62
|
+
this.xHeightRaw = 0;
|
|
63
|
+
this.capHeightRaw = 0;
|
|
40
64
|
this.bbox = [0, 0, 0, 0];
|
|
41
65
|
this.numGlyphs = 0;
|
|
42
66
|
this.numHMetrics = 0;
|
|
@@ -53,6 +77,16 @@ export class TTFParser {
|
|
|
53
77
|
// COLR v1: base glyph id → the absolute offset of its Paint table (a recursive paint graph).
|
|
54
78
|
this.colrBaseV1 = new Map();
|
|
55
79
|
this.colrLayerListOffset = 0; // absolute offset of the v1 LayerList (paint-offset array)
|
|
80
|
+
// Kerning: (leftGid << 16 | rightGid) -> value in FONT UNITS (from the `kern` table and/or GPOS).
|
|
81
|
+
this.kernPairs = new Map();
|
|
82
|
+
this.hasKerning = false;
|
|
83
|
+
// GPOS Type 2 (pair positioning) subtables, in absolute byte offsets. Queried per pair (format 2
|
|
84
|
+
// is class-based, so it cannot be enumerated into `kernPairs` up front). Preferred over `kern`.
|
|
85
|
+
this.gposPairPos = [];
|
|
86
|
+
// Memoised getKerning results, keyed (leftGid << 16 | rightGid) -> em/1000. A GPOS lookup walks
|
|
87
|
+
// subtables (coverage + classDef) per pair, and a long document repeats the same pairs thousands
|
|
88
|
+
// of times, so the first lookup of a pair pays; the rest are O(1).
|
|
89
|
+
this.kernCache = new Map();
|
|
56
90
|
this.readTableDirectory();
|
|
57
91
|
const head = this.table("head").offset;
|
|
58
92
|
this.unitsPerEm = u16(this.data, head + 18);
|
|
@@ -67,6 +101,8 @@ export class TTFParser {
|
|
|
67
101
|
const hhea = this.table("hhea").offset;
|
|
68
102
|
this.ascent = this.toGlyphSpace(i16(this.data, hhea + 4));
|
|
69
103
|
this.descent = this.toGlyphSpace(i16(this.data, hhea + 6));
|
|
104
|
+
this.lineGap = this.toGlyphSpace(i16(this.data, hhea + 8));
|
|
105
|
+
this.readDecorationMetrics();
|
|
70
106
|
this.indexToLocFormat = i16(this.data, head + 50);
|
|
71
107
|
this.readHmtx();
|
|
72
108
|
this.readCmap();
|
|
@@ -74,6 +110,19 @@ export class TTFParser {
|
|
|
74
110
|
this.readColr();
|
|
75
111
|
this.readCpal();
|
|
76
112
|
}
|
|
113
|
+
// Kerning tables are optional and only ever make text prettier; a malformed one must degrade to
|
|
114
|
+
// "no kerning", never break font loading. Guard both.
|
|
115
|
+
try {
|
|
116
|
+
if (this.tables["kern"])
|
|
117
|
+
this.readKern();
|
|
118
|
+
if (this.tables["GPOS"])
|
|
119
|
+
this.readGpos();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
this.kernPairs.clear();
|
|
123
|
+
this.gposPairPos = [];
|
|
124
|
+
this.hasKerning = false;
|
|
125
|
+
}
|
|
77
126
|
}
|
|
78
127
|
// The raw font bytes, for the /FontFile2 stream.
|
|
79
128
|
getData() {
|
|
@@ -131,6 +180,325 @@ export class TTFParser {
|
|
|
131
180
|
width += this.getCharWidth(ch, fontSize);
|
|
132
181
|
return width;
|
|
133
182
|
}
|
|
183
|
+
// `post` carries the underline pair (always, from its header); `OS/2` carries the x- and cap-height
|
|
184
|
+
// but only from version 2 on. Both tables are optional in principle, so nothing here may throw.
|
|
185
|
+
readDecorationMetrics() {
|
|
186
|
+
const post = this.tables["post"];
|
|
187
|
+
if (post) {
|
|
188
|
+
this.underlinePositionRaw = i16(this.data, post.offset + 8);
|
|
189
|
+
this.underlineThicknessRaw = i16(this.data, post.offset + 10);
|
|
190
|
+
}
|
|
191
|
+
const os2 = this.tables["OS/2"];
|
|
192
|
+
if (os2 && u16(this.data, os2.offset) >= 2) {
|
|
193
|
+
this.xHeightRaw = i16(this.data, os2.offset + 86);
|
|
194
|
+
this.capHeightRaw = i16(this.data, os2.offset + 88);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Underline / strikethrough geometry, as fractions of the em.
|
|
199
|
+
*
|
|
200
|
+
* A font without `post` (rare) gets the classic 10% below the baseline at 5% thickness - the same
|
|
201
|
+
* numbers all 14 standard AFMs happen to declare, so it is a measured convention rather than an
|
|
202
|
+
* invention. A font whose `OS/2` predates version 2 has no x- or cap-height, so we MEASURE them off
|
|
203
|
+
* the outlines of `x` and `H` instead of guessing.
|
|
204
|
+
*/
|
|
205
|
+
decoration() {
|
|
206
|
+
if (this.cachedDecoration)
|
|
207
|
+
return this.cachedDecoration;
|
|
208
|
+
const em = this.unitsPerEm;
|
|
209
|
+
const xHeight = this.xHeightRaw || this.outlineTop(0x78); // "x"
|
|
210
|
+
const capHeight = this.capHeightRaw || this.outlineTop(0x48); // "H"
|
|
211
|
+
this.cachedDecoration = {
|
|
212
|
+
underlinePosition: this.underlinePositionRaw ? -this.underlinePositionRaw / em : 0.1,
|
|
213
|
+
underlineThickness: this.underlineThicknessRaw ? this.underlineThicknessRaw / em : 0.05,
|
|
214
|
+
xHeight: xHeight / em,
|
|
215
|
+
capHeight: capHeight / em,
|
|
216
|
+
};
|
|
217
|
+
return this.cachedDecoration;
|
|
218
|
+
}
|
|
219
|
+
/** The highest ON-CURVE point of a code point's outline, in font units; 0 for a missing glyph.
|
|
220
|
+
* A quadratic's control point may sit above the curve, so it is not a bound - and `x`/`H` have
|
|
221
|
+
* flat tops in any face where this fallback matters. */
|
|
222
|
+
outlineTop(codePoint) {
|
|
223
|
+
let top = 0;
|
|
224
|
+
for (const cmd of this.getGlyphPath(this.getGlyphIndex(codePoint))) {
|
|
225
|
+
if (cmd.type !== "Z" && cmd.y > top)
|
|
226
|
+
top = cmd.y;
|
|
227
|
+
}
|
|
228
|
+
return top;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Where a glyph's INK crosses a horizontal band, as x-intervals in font units (pen origin at 0).
|
|
232
|
+
* This is what lets an underline step around a descender ("skip-ink"), the way a browser does.
|
|
233
|
+
*
|
|
234
|
+
* Scanline fill: sample a few horizontal lines across the band, find every place the outline
|
|
235
|
+
* crosses them, and pair the crossings up into filled spans. A handful of scanlines is plenty -
|
|
236
|
+
* the band is a few percent of the em tall, and a descender stem does not wander inside it.
|
|
237
|
+
*
|
|
238
|
+
* `yTop` / `yBottom` are in font units, y UP from the baseline (so both are negative under it).
|
|
239
|
+
* Returns [] for a glyph with no outline (a space, an unmapped code point, a composite we cannot
|
|
240
|
+
* decompose) - the caller then leaves the line unbroken there, which is the safe direction.
|
|
241
|
+
*/
|
|
242
|
+
inkSpansInBand(codePoint, yTop, yBottom) {
|
|
243
|
+
const path = this.getGlyphPath(this.getGlyphIndex(codePoint));
|
|
244
|
+
if (path.length === 0)
|
|
245
|
+
return [];
|
|
246
|
+
// Flatten to straight edges; a quadratic becomes a short polyline (the band is thin, so a
|
|
247
|
+
// coarse subdivision is already below the printing resolution).
|
|
248
|
+
const edges = [];
|
|
249
|
+
let startX = 0;
|
|
250
|
+
let startY = 0;
|
|
251
|
+
let curX = 0;
|
|
252
|
+
let curY = 0;
|
|
253
|
+
const edge = (x, y) => {
|
|
254
|
+
edges.push([curX, curY, x, y]);
|
|
255
|
+
curX = x;
|
|
256
|
+
curY = y;
|
|
257
|
+
};
|
|
258
|
+
for (const cmd of path) {
|
|
259
|
+
if (cmd.type === "M") {
|
|
260
|
+
[curX, curY] = [cmd.x, cmd.y];
|
|
261
|
+
[startX, startY] = [cmd.x, cmd.y];
|
|
262
|
+
}
|
|
263
|
+
else if (cmd.type === "L") {
|
|
264
|
+
edge(cmd.x, cmd.y);
|
|
265
|
+
}
|
|
266
|
+
else if (cmd.type === "Q") {
|
|
267
|
+
const [x0, y0] = [curX, curY];
|
|
268
|
+
const STEPS = 8;
|
|
269
|
+
for (let i = 1; i <= STEPS; i++) {
|
|
270
|
+
const t = i / STEPS;
|
|
271
|
+
const u = 1 - t;
|
|
272
|
+
edge(u * u * x0 + 2 * u * t * cmd.cx + t * t * cmd.x, u * u * y0 + 2 * u * t * cmd.cy + t * t * cmd.y);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
edge(startX, startY); // Z: close the contour
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const SCANLINES = 5;
|
|
280
|
+
const spans = [];
|
|
281
|
+
for (let i = 0; i < SCANLINES; i++) {
|
|
282
|
+
const y = yTop + ((yBottom - yTop) * i) / (SCANLINES - 1);
|
|
283
|
+
const xs = [];
|
|
284
|
+
for (const [x0, y0, x1, y1] of edges) {
|
|
285
|
+
if (y0 === y1)
|
|
286
|
+
continue;
|
|
287
|
+
// Half-open test so a vertex on the scanline is counted exactly once.
|
|
288
|
+
if (y >= Math.min(y0, y1) && y < Math.max(y0, y1)) {
|
|
289
|
+
xs.push(x0 + ((y - y0) / (y1 - y0)) * (x1 - x0));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
xs.sort((a, b) => a - b);
|
|
293
|
+
for (let k = 0; k + 1 < xs.length; k += 2)
|
|
294
|
+
spans.push([xs[k], xs[k + 1]]);
|
|
295
|
+
}
|
|
296
|
+
return mergeSpans(spans);
|
|
297
|
+
}
|
|
298
|
+
/** True if the font declares any horizontal kerning (kern table or GPOS). */
|
|
299
|
+
kerns() {
|
|
300
|
+
return this.hasKerning;
|
|
301
|
+
}
|
|
302
|
+
/** Kerning between two glyphs, in em/1000 (the standard-14 unit); 0 if the pair is not kerned.
|
|
303
|
+
* Negative tightens, matching the AFM sign. GPOS wins over the legacy `kern` table when both
|
|
304
|
+
* exist (it is what a modern shaper uses, and can carry more pairs). */
|
|
305
|
+
getKerning(leftGid, rightGid) {
|
|
306
|
+
const key = (leftGid << 16) | rightGid;
|
|
307
|
+
const cached = this.kernCache.get(key);
|
|
308
|
+
if (cached !== undefined)
|
|
309
|
+
return cached;
|
|
310
|
+
let units;
|
|
311
|
+
if (this.gposPairPos.length > 0)
|
|
312
|
+
units = this.gposKern(leftGid, rightGid);
|
|
313
|
+
if (units === undefined)
|
|
314
|
+
units = this.kernPairs.get(key);
|
|
315
|
+
const em = units === undefined ? 0 : Math.round((units * 1000) / this.unitsPerEm);
|
|
316
|
+
this.kernCache.set(key, em);
|
|
317
|
+
return em;
|
|
318
|
+
}
|
|
319
|
+
// The legacy `kern` table, format 0 (an explicit list of glyph-pair adjustments). Only horizontal
|
|
320
|
+
// format-0 subtables are read; GPOS (parsed separately) supersedes this in modern fonts, but old
|
|
321
|
+
// TrueType (DejaVu, FreeSans) carries its kerning here.
|
|
322
|
+
readKern() {
|
|
323
|
+
const base = this.tables["kern"].offset;
|
|
324
|
+
// The OpenType (Microsoft) header is version:u16 = 0, nTables:u16. (Apple's is a u32 version; the
|
|
325
|
+
// fonts we target use the OpenType form.)
|
|
326
|
+
if (u16(this.data, base) !== 0)
|
|
327
|
+
return;
|
|
328
|
+
const nTables = u16(this.data, base + 2);
|
|
329
|
+
let p = base + 4;
|
|
330
|
+
for (let t = 0; t < nTables; t++) {
|
|
331
|
+
const length = u16(this.data, p + 2);
|
|
332
|
+
const coverage = u16(this.data, p + 4);
|
|
333
|
+
const format = coverage >> 8;
|
|
334
|
+
const horizontal = (coverage & 0x1) === 1;
|
|
335
|
+
if (format === 0 && horizontal) {
|
|
336
|
+
const nPairs = u16(this.data, p + 6);
|
|
337
|
+
let q = p + 14; // past nPairs, searchRange, entrySelector, rangeShift
|
|
338
|
+
for (let i = 0; i < nPairs; i++) {
|
|
339
|
+
const left = u16(this.data, q);
|
|
340
|
+
const right = u16(this.data, q + 2);
|
|
341
|
+
const value = i16(this.data, q + 4);
|
|
342
|
+
this.kernPairs.set((left << 16) | right, value);
|
|
343
|
+
q += 6;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
p += length;
|
|
347
|
+
}
|
|
348
|
+
if (this.kernPairs.size > 0)
|
|
349
|
+
this.hasKerning = true;
|
|
350
|
+
}
|
|
351
|
+
// GPOS pair-adjustment kerning. We collect the PairPos (lookup type 2) subtables of the `kern`
|
|
352
|
+
// FEATURE only - not every pair-positioning lookup - so this is kerning, not arbitrary shaping.
|
|
353
|
+
// Script/language selection is skipped: the `kern` feature is the same across the Latin scripts we
|
|
354
|
+
// target, so a flat scan of the feature list for tag "kern" is correct here. Only pair positioning
|
|
355
|
+
// (type 2) is read; the rest of GPOS (mark attachment, cursive, contextual) is not kerning.
|
|
356
|
+
readGpos() {
|
|
357
|
+
const base = this.tables["GPOS"].offset;
|
|
358
|
+
const featureListOffset = base + u16(this.data, base + 6);
|
|
359
|
+
const lookupListOffset = base + u16(this.data, base + 8);
|
|
360
|
+
// Every lookup index referenced by a "kern" feature.
|
|
361
|
+
const kernLookups = new Set();
|
|
362
|
+
const featureCount = u16(this.data, featureListOffset);
|
|
363
|
+
for (let i = 0; i < featureCount; i++) {
|
|
364
|
+
const rec = featureListOffset + 2 + i * 6;
|
|
365
|
+
const tag = String.fromCharCode(this.data[rec], this.data[rec + 1], this.data[rec + 2], this.data[rec + 3]);
|
|
366
|
+
if (tag !== "kern")
|
|
367
|
+
continue;
|
|
368
|
+
const feature = featureListOffset + u16(this.data, rec + 4);
|
|
369
|
+
const lookupIndexCount = u16(this.data, feature + 2);
|
|
370
|
+
for (let j = 0; j < lookupIndexCount; j++) {
|
|
371
|
+
kernLookups.add(u16(this.data, feature + 4 + j * 2));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (kernLookups.size === 0)
|
|
375
|
+
return;
|
|
376
|
+
// The PairPos subtables of those lookups (a lookup of another type in the set is ignored).
|
|
377
|
+
const lookupCount = u16(this.data, lookupListOffset);
|
|
378
|
+
for (const idx of kernLookups) {
|
|
379
|
+
if (idx >= lookupCount)
|
|
380
|
+
continue;
|
|
381
|
+
const lookup = lookupListOffset + u16(this.data, lookupListOffset + 2 + idx * 2);
|
|
382
|
+
if (u16(this.data, lookup) !== 2)
|
|
383
|
+
continue; // lookupType 2 = pair adjustment
|
|
384
|
+
const subTableCount = u16(this.data, lookup + 4);
|
|
385
|
+
for (let s = 0; s < subTableCount; s++) {
|
|
386
|
+
this.gposPairPos.push(lookup + u16(this.data, lookup + 6 + s * 2));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (this.gposPairPos.length > 0)
|
|
390
|
+
this.hasKerning = true;
|
|
391
|
+
}
|
|
392
|
+
// The XAdvance adjustment on the LEFT glyph for a pair, in font units, from the first PairPos
|
|
393
|
+
// subtable that covers it; undefined if none does. (Kerning only ever uses value1's XAdvance.)
|
|
394
|
+
gposKern(leftGid, rightGid) {
|
|
395
|
+
for (const sub of this.gposPairPos) {
|
|
396
|
+
const posFormat = u16(this.data, sub);
|
|
397
|
+
const coverage = sub + u16(this.data, sub + 2);
|
|
398
|
+
const covIndex = this.coverageIndex(coverage, leftGid);
|
|
399
|
+
if (covIndex < 0)
|
|
400
|
+
continue;
|
|
401
|
+
const valueFormat1 = u16(this.data, sub + 4);
|
|
402
|
+
const valueFormat2 = u16(this.data, sub + 6);
|
|
403
|
+
const v1size = TTFParser.valueRecordSize(valueFormat1);
|
|
404
|
+
const v2size = TTFParser.valueRecordSize(valueFormat2);
|
|
405
|
+
if (posFormat === 1) {
|
|
406
|
+
const pairSet = sub + u16(this.data, sub + 10 + covIndex * 2);
|
|
407
|
+
const pairValueCount = u16(this.data, pairSet);
|
|
408
|
+
const recSize = 2 + v1size + v2size;
|
|
409
|
+
for (let i = 0; i < pairValueCount; i++) {
|
|
410
|
+
const rec = pairSet + 2 + i * recSize;
|
|
411
|
+
if (u16(this.data, rec) === rightGid) {
|
|
412
|
+
return this.xAdvance(rec + 2, valueFormat1);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else if (posFormat === 2) {
|
|
417
|
+
const classDef1 = sub + u16(this.data, sub + 8);
|
|
418
|
+
const classDef2 = sub + u16(this.data, sub + 10);
|
|
419
|
+
const class2Count = u16(this.data, sub + 14);
|
|
420
|
+
const c1 = this.classOf(classDef1, leftGid);
|
|
421
|
+
const c2 = this.classOf(classDef2, rightGid);
|
|
422
|
+
const recSize = v1size + v2size;
|
|
423
|
+
const record = sub + 16 + (c1 * class2Count + c2) * recSize;
|
|
424
|
+
return this.xAdvance(record, valueFormat1);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
static valueRecordSize(valueFormat) {
|
|
430
|
+
let bits = 0;
|
|
431
|
+
for (let m = valueFormat; m; m >>= 1)
|
|
432
|
+
bits += m & 1;
|
|
433
|
+
return bits * 2;
|
|
434
|
+
}
|
|
435
|
+
// The XAdvance field of a GPOS value record, or 0 if the format has none. XAdvance (bit 0x0004) is
|
|
436
|
+
// preceded by XPlacement (0x0001) and YPlacement (0x0002), each an i16 when present.
|
|
437
|
+
xAdvance(recordOffset, valueFormat) {
|
|
438
|
+
if ((valueFormat & 0x0004) === 0)
|
|
439
|
+
return 0;
|
|
440
|
+
let off = recordOffset;
|
|
441
|
+
if (valueFormat & 0x0001)
|
|
442
|
+
off += 2;
|
|
443
|
+
if (valueFormat & 0x0002)
|
|
444
|
+
off += 2;
|
|
445
|
+
return i16(this.data, off);
|
|
446
|
+
}
|
|
447
|
+
// Coverage index of a glyph, or -1. Format 1: sorted glyph list. Format 2: sorted ranges.
|
|
448
|
+
coverageIndex(offset, gid) {
|
|
449
|
+
const format = u16(this.data, offset);
|
|
450
|
+
if (format === 1) {
|
|
451
|
+
const count = u16(this.data, offset + 2);
|
|
452
|
+
let lo = 0;
|
|
453
|
+
let hi = count - 1;
|
|
454
|
+
while (lo <= hi) {
|
|
455
|
+
const mid = (lo + hi) >> 1;
|
|
456
|
+
const g = u16(this.data, offset + 4 + mid * 2);
|
|
457
|
+
if (gid < g)
|
|
458
|
+
hi = mid - 1;
|
|
459
|
+
else if (gid > g)
|
|
460
|
+
lo = mid + 1;
|
|
461
|
+
else
|
|
462
|
+
return mid;
|
|
463
|
+
}
|
|
464
|
+
return -1;
|
|
465
|
+
}
|
|
466
|
+
if (format === 2) {
|
|
467
|
+
const rangeCount = u16(this.data, offset + 2);
|
|
468
|
+
for (let i = 0; i < rangeCount; i++) {
|
|
469
|
+
const rec = offset + 4 + i * 6;
|
|
470
|
+
const start = u16(this.data, rec);
|
|
471
|
+
const end = u16(this.data, rec + 2);
|
|
472
|
+
if (gid >= start && gid <= end)
|
|
473
|
+
return u16(this.data, rec + 4) + (gid - start);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return -1;
|
|
477
|
+
}
|
|
478
|
+
// Class of a glyph in a ClassDef, or 0 (the default class). Format 1: run from a start glyph.
|
|
479
|
+
// Format 2: ranges. Glyphs not listed are class 0.
|
|
480
|
+
classOf(offset, gid) {
|
|
481
|
+
const format = u16(this.data, offset);
|
|
482
|
+
if (format === 1) {
|
|
483
|
+
const startGlyph = u16(this.data, offset + 2);
|
|
484
|
+
const glyphCount = u16(this.data, offset + 4);
|
|
485
|
+
if (gid >= startGlyph && gid < startGlyph + glyphCount) {
|
|
486
|
+
return u16(this.data, offset + 6 + (gid - startGlyph) * 2);
|
|
487
|
+
}
|
|
488
|
+
return 0;
|
|
489
|
+
}
|
|
490
|
+
if (format === 2) {
|
|
491
|
+
const rangeCount = u16(this.data, offset + 2);
|
|
492
|
+
for (let i = 0; i < rangeCount; i++) {
|
|
493
|
+
const rec = offset + 4 + i * 6;
|
|
494
|
+
const start = u16(this.data, rec);
|
|
495
|
+
const end = u16(this.data, rec + 2);
|
|
496
|
+
if (gid >= start && gid <= end)
|
|
497
|
+
return u16(this.data, rec + 4);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return 0;
|
|
501
|
+
}
|
|
134
502
|
table(tag) {
|
|
135
503
|
const t = this.tables[tag];
|
|
136
504
|
if (!t)
|
|
@@ -41,9 +41,13 @@ export class Validator {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
static validateFlexElement(element) {
|
|
44
|
-
// Ensure flexible elements have valid flex values
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
// Ensure flexible elements have valid flex values. The message names what the CALLER wrote, not the
|
|
45
|
+
// element class: `Spacer` and `Expanded` are what exists in their document, `ExpandedElement` is not.
|
|
46
|
+
const flex = element.getFlex();
|
|
47
|
+
if (flex <= 0) {
|
|
48
|
+
throw new Error(`@jasy/pdf: Spacer/Expanded needs a flex above 0, got ${flex}. Flex is a SHARE of the leftover ` +
|
|
49
|
+
"space, so 0 would claim none - which is what leaving the element out does. For a fixed gap " +
|
|
50
|
+
"use the `gap` of the surrounding Column or Row, or a Box with a height.");
|
|
47
51
|
}
|
|
48
52
|
// Ensure a flexible element does not contain another flexible element
|
|
49
53
|
if (hasChildProp(element)) {
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jasy/pdf",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.8",
|
|
4
4
|
"description": "Declarative, component-based PDF generation in pure TypeScript - Flutter-style components, real AFM font metrics, a hand-rolled PDF writer. No headless browser, no Java.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"declarative",
|
|
7
|
+
"e-invoice",
|
|
7
8
|
"factur-x",
|
|
8
9
|
"invoice",
|
|
9
10
|
"layout",
|
|
@@ -12,6 +13,7 @@
|
|
|
12
13
|
"pdf-a",
|
|
13
14
|
"pdf-generation",
|
|
14
15
|
"typescript",
|
|
16
|
+
"xrechnung",
|
|
15
17
|
"zugferd"
|
|
16
18
|
],
|
|
17
19
|
"license": "MIT",
|
|
@@ -30,7 +32,12 @@
|
|
|
30
32
|
".": {
|
|
31
33
|
"types": "./dist/index.d.ts",
|
|
32
34
|
"default": "./dist/index.js"
|
|
33
|
-
}
|
|
35
|
+
},
|
|
36
|
+
"./edit": {
|
|
37
|
+
"types": "./dist/edit/index.d.ts",
|
|
38
|
+
"default": "./dist/edit/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
34
41
|
},
|
|
35
42
|
"dependencies": {
|
|
36
43
|
"fflate": "^0.8.3",
|