@jasy/pdf 1.0.0-alpha.7 → 1.0.0-alpha.9
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 +11 -10
- package/dist/api/content.d.ts +5 -4
- package/dist/api/content.js +13 -7
- package/dist/api/descriptor.js +23 -0
- package/dist/api/dimension.d.ts +56 -0
- package/dist/api/dimension.js +27 -0
- package/dist/api/font-url.d.ts +28 -0
- package/dist/api/font-url.js +134 -0
- package/dist/api/forms.d.ts +347 -0
- package/dist/api/forms.js +189 -0
- package/dist/api/gradient.d.ts +53 -0
- package/dist/api/gradient.js +97 -0
- package/dist/api/index.d.ts +3 -0
- package/dist/api/index.js +3 -0
- package/dist/api/insets.d.ts +19 -12
- package/dist/api/insets.js +18 -7
- package/dist/api/layout.d.ts +20 -6
- package/dist/api/layout.js +31 -6
- package/dist/api/page-builder.d.ts +8 -0
- package/dist/api/page-builder.js +12 -0
- package/dist/api/structure.d.ts +11 -0
- package/dist/api/structure.js +17 -4
- package/dist/api/text.d.ts +42 -6
- package/dist/api/text.js +29 -5
- 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 +145 -0
- package/dist/edit/document.js +642 -0
- package/dist/edit/fill.d.ts +65 -0
- package/dist/edit/fill.js +424 -0
- package/dist/edit/flatten.d.ts +54 -0
- package/dist/edit/flatten.js +462 -0
- package/dist/edit/index.d.ts +20 -0
- package/dist/edit/index.js +18 -0
- package/dist/edit/lexer.d.ts +30 -0
- package/dist/edit/lexer.js +285 -0
- package/dist/edit/objects.d.ts +68 -0
- package/dist/edit/objects.js +83 -0
- package/dist/edit/writer.d.ts +60 -0
- package/dist/edit/writer.js +283 -0
- package/dist/elements/container-element.d.ts +21 -1
- package/dist/elements/container-element.js +25 -10
- 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/image-element.d.ts +18 -4
- package/dist/elements/image-element.js +27 -12
- package/dist/elements/index.d.ts +6 -0
- package/dist/elements/index.js +6 -0
- package/dist/elements/layout/expanded-element.d.ts +3 -1
- package/dist/elements/layout/expanded-element.js +12 -3
- package/dist/elements/layout/padding-element.d.ts +4 -2
- package/dist/elements/layout/padding-element.js +4 -2
- package/dist/elements/layout/page-builder-element.js +2 -0
- package/dist/elements/pdf-element.d.ts +59 -1
- package/dist/elements/pdf-element.js +62 -1
- package/dist/elements/rectangle-element.d.ts +20 -6
- package/dist/elements/rectangle-element.js +25 -12
- package/dist/elements/row-element.d.ts +24 -1
- package/dist/elements/row-element.js +25 -10
- package/dist/elements/text-element.d.ts +60 -1
- package/dist/elements/text-element.js +132 -33
- 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 +40 -7
- package/dist/ir/display-list.js +8 -1
- package/dist/layout/alignment.d.ts +10 -0
- package/dist/layout/alignment.js +6 -0
- package/dist/layout/box-constraints.d.ts +53 -0
- package/dist/layout/box-constraints.js +72 -0
- package/dist/layout/insets.d.ts +28 -0
- package/dist/layout/insets.js +20 -0
- package/dist/renderer/form-field-renderer.d.ts +20 -0
- package/dist/renderer/form-field-renderer.js +12 -0
- package/dist/renderer/image-renderer.js +2 -1
- package/dist/renderer/page-renderer.js +8 -2
- package/dist/renderer/pdf-backend.d.ts +6 -0
- package/dist/renderer/pdf-backend.js +86 -31
- package/dist/renderer/pdf-document-renderer.js +8 -1
- package/dist/renderer/pdf-renderer.js +19 -1
- package/dist/renderer/rectangle-renderer.js +8 -2
- package/dist/renderer/text-renderer.d.ts +2 -2
- package/dist/renderer/text-renderer.js +150 -42
- package/dist/text/advance.js +5 -2
- package/dist/text/arabic.d.ts +20 -0
- package/dist/text/arabic.js +101 -0
- package/dist/text/bidi.d.ts +33 -0
- package/dist/text/bidi.js +94 -0
- package/dist/text/font-fallback.d.ts +25 -0
- package/dist/text/font-fallback.js +38 -0
- package/dist/text/line-breaker.d.ts +35 -1
- package/dist/text/line-breaker.js +76 -16
- package/dist/text/orphans-widows.d.ts +27 -0
- package/dist/text/orphans-widows.js +53 -0
- package/dist/text/shape.d.ts +29 -0
- package/dist/text/shape.js +67 -0
- package/dist/text/text-style.d.ts +25 -0
- package/dist/text/text-style.js +44 -1
- package/dist/utils/dest-registry.d.ts +5 -2
- package/dist/utils/dest-registry.js +5 -5
- package/dist/utils/flex-layout.d.ts +24 -4
- package/dist/utils/flex-layout.js +169 -7
- package/dist/utils/font-metrics.d.ts +5 -0
- package/dist/utils/gsub.d.ts +36 -0
- package/dist/utils/gsub.js +178 -0
- package/dist/utils/inflate.d.ts +16 -0
- package/dist/utils/inflate.js +52 -0
- package/dist/utils/outline.js +1 -2
- package/dist/utils/pdf-object-manager.d.ts +39 -0
- package/dist/utils/pdf-object-manager.js +166 -15
- package/dist/utils/struct-tree.js +2 -3
- package/dist/utils/ttf-parser.d.ts +6 -0
- package/dist/utils/ttf-parser.js +19 -0
- package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
- package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
- package/dist/utils/woff.d.ts +10 -0
- package/dist/utils/woff.js +116 -0
- package/dist/validators/element-validator.js +14 -3
- package/package.json +10 -2
|
@@ -5,12 +5,15 @@ 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
9
|
import { subsetTTF } from "./ttf-subsetter.js";
|
|
9
|
-
import {
|
|
10
|
+
import { shapeRun } from "../text/shape.js";
|
|
11
|
+
import { getArrayBuffer, isWindows1252 } from "./utf8-to-windows1252-encoder.js";
|
|
10
12
|
import { isEmojiCodePoint } from "../text/emoji-codepoints.js";
|
|
11
13
|
import { StructTree } from "./struct-tree.js";
|
|
12
14
|
import { OutlineBuilder } from "./outline.js";
|
|
13
15
|
import { DestRegistry } from "./dest-registry.js";
|
|
16
|
+
import { AcroFormCollector } from "../forms/acroform.js";
|
|
14
17
|
// Enums come from the leaf config module (never in a cycle); the config type is
|
|
15
18
|
// erased at runtime so it can come from the cyclic module safely.
|
|
16
19
|
import { ColorMode, Orientation } from "../renderer/pdf-config.js";
|
|
@@ -134,6 +137,9 @@ export class PDFObjectManager {
|
|
|
134
137
|
get dests() {
|
|
135
138
|
return this._dests;
|
|
136
139
|
}
|
|
140
|
+
get acroform() {
|
|
141
|
+
return this._acroform;
|
|
142
|
+
}
|
|
137
143
|
constructor(pageSize) {
|
|
138
144
|
this.objects = [];
|
|
139
145
|
this.objectPositions = [];
|
|
@@ -157,6 +163,11 @@ export class PDFObjectManager {
|
|
|
157
163
|
// Per-registered face: the reserved font-object numbers + the glyph ids actually used. The font
|
|
158
164
|
// program is subsetted and the objects filled in by finalizeCustomFonts(), after the render pass.
|
|
159
165
|
this.customFontEmit = new Map();
|
|
166
|
+
// Shaped runs, keyed by font + text; `null` means "asked, and there was nothing to shape".
|
|
167
|
+
this.shapeCache = new Map();
|
|
168
|
+
// Per font, glyph -> the code points it stands for. A shaped glyph is often absent from the cmap (a
|
|
169
|
+
// ligature) or maps back to a presentation form, so `ToUnicode` cannot come from the cmap alone.
|
|
170
|
+
this.shapedOrigins = new Map();
|
|
160
171
|
// Embedded file attachments (their /Filespec object numbers + display names). Referenced from
|
|
161
172
|
// the catalog's /Names/EmbeddedFiles + /AF. Drives ZUGFeRD's embedded factur-x.xml.
|
|
162
173
|
this.attachments = [];
|
|
@@ -167,6 +178,7 @@ export class PDFObjectManager {
|
|
|
167
178
|
this.compress = false;
|
|
168
179
|
this.overflowPolicy = "error";
|
|
169
180
|
this.encJobs = [];
|
|
181
|
+
this.strJobs = [];
|
|
170
182
|
// Accessible (PDF/UA) tagging, off by default (byte-identical output); the API turns it on.
|
|
171
183
|
this._struct = new StructTree();
|
|
172
184
|
// Document outline (bookmarks). Empty unless a `Bookmark` was placed; then finalize() emits /Outlines.
|
|
@@ -174,6 +186,9 @@ export class PDFObjectManager {
|
|
|
174
186
|
// Named destinations (internal-link jump targets). Empty unless an `Anchor` was placed; then it emits
|
|
175
187
|
// a /Names /Dests tree.
|
|
176
188
|
this._dests = new DestRegistry();
|
|
189
|
+
// Interactive form fields (AcroForm). Empty unless a form field was placed; then finalize() emits the
|
|
190
|
+
// catalog /AcroForm dict and each field's widget lands in its page /Annots.
|
|
191
|
+
this._acroform = new AcroFormCollector();
|
|
177
192
|
// Whether to kern. Document-level: measuring (via `runAdvance`, which reads this off the metrics)
|
|
178
193
|
// and drawing (the backend emits a `TJ`) both consult it, so they can never disagree. OFF while the
|
|
179
194
|
// embedded-font path (kern/GPOS) is unbuilt - a standard-14-only kerning would set the 14 system
|
|
@@ -213,6 +228,32 @@ export class PDFObjectManager {
|
|
|
213
228
|
encToken(id) {
|
|
214
229
|
return `JENC:${id}`;
|
|
215
230
|
}
|
|
231
|
+
// The same trick for a literal STRING. A separate marker because the two are swapped differently: a
|
|
232
|
+
// stream keeps its delimiters and only the payload changes, a string is replaced whole by a hex one.
|
|
233
|
+
strToken(id) {
|
|
234
|
+
return `\x00\x02JSTR:${id}\x02\x00`;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The single choke-point every literal string in an OBJECT goes through - a field's `/T` and `/V`, a
|
|
238
|
+
* bookmark `/Title`, a link `/URI`, an `/Alt` text, a destination name.
|
|
239
|
+
*
|
|
240
|
+
* Without a security handler this is the escaped literal we have always written, so output stays
|
|
241
|
+
* byte-identical. With one, the raw bytes are registered for the finalize pass and a placeholder takes
|
|
242
|
+
* their place: ISO 32000-1 7.6.2 requires ALL strings and streams to be encrypted, and the ones missed
|
|
243
|
+
* here were exactly the values a password is supposed to protect (ISSUE-7).
|
|
244
|
+
*
|
|
245
|
+
* A string INSIDE a content stream needs none of this - the stream is enciphered as a whole.
|
|
246
|
+
*/
|
|
247
|
+
pdfString(text) {
|
|
248
|
+
if (!this.security) {
|
|
249
|
+
return `(${text.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)")})`;
|
|
250
|
+
}
|
|
251
|
+
// Encrypt the bytes the unencrypted path would have written, so a viewer decrypts to the same value.
|
|
252
|
+
// No escaping is applied: the result is emitted as a HEX string, which has no escapes to get wrong.
|
|
253
|
+
const bytes = new Uint8Array(getArrayBuffer(text));
|
|
254
|
+
const id = this.strJobs.push(bytes) - 1;
|
|
255
|
+
return this.strToken(id);
|
|
256
|
+
}
|
|
216
257
|
// The single encryption choke-point every stream emitter routes through (stream(), registerImage). Without
|
|
217
258
|
// a security handler: the bytes as a latin1 string + their length. With one: register the raw bytes for the
|
|
218
259
|
// finalize pass and return a placeholder + the pre-computable encrypted length (16-byte IV + PKCS#7 padding).
|
|
@@ -246,6 +287,14 @@ export class PDFObjectManager {
|
|
|
246
287
|
addContentStream(content) {
|
|
247
288
|
return this.addObject(this.stream("", new Uint8Array(getArrayBuffer(content))));
|
|
248
289
|
}
|
|
290
|
+
// A Form XObject: a self-contained content stream with its own /BBox (+ optional /Resources). Used for
|
|
291
|
+
// form-field appearance streams (/AP). Routes through the same choke-point as page content, so
|
|
292
|
+
// compression and encryption apply. Returns the object number.
|
|
293
|
+
addFormXObject(bbox, content, resources) {
|
|
294
|
+
const dict = `/Type /XObject /Subtype /Form /BBox ${bbox}` +
|
|
295
|
+
(resources ? ` /Resources << ${resources} >>` : "");
|
|
296
|
+
return this.addObject(this.stream(dict, new Uint8Array(getArrayBuffer(content))));
|
|
297
|
+
}
|
|
249
298
|
changePDFConfig(config) {
|
|
250
299
|
this.pdfConfig = { ...this.pdfConfig, ...config };
|
|
251
300
|
}
|
|
@@ -327,9 +376,8 @@ endstream`;
|
|
|
327
376
|
attachFile(name, data, opts = {}) {
|
|
328
377
|
const subtype = (opts.mimeType ?? "application/octet-stream").replace(/\//g, "#2F");
|
|
329
378
|
const embedded = this.addObject(this.stream(`/Type /EmbeddedFile /Subtype /${subtype} /Params << /Size ${data.length} >>`, data));
|
|
330
|
-
const
|
|
331
|
-
const
|
|
332
|
-
const filespec = this.addObject(`<< /Type /Filespec /F (${escaped(name)}) /UF (${escaped(name)}) ` +
|
|
379
|
+
const desc = opts.description ? ` /Desc ${this.pdfString(opts.description)}` : "";
|
|
380
|
+
const filespec = this.addObject(`<< /Type /Filespec /F ${this.pdfString(name)} /UF ${this.pdfString(name)} ` +
|
|
333
381
|
`/AFRelationship /${opts.relationship ?? "Unspecified"}${desc} ` +
|
|
334
382
|
`/EF << /F ${embedded} 0 R /UF ${embedded} 0 R >> >>`);
|
|
335
383
|
this.attachments.push({ name, filespec });
|
|
@@ -350,8 +398,8 @@ endstream`;
|
|
|
350
398
|
setOutputIntent(icc, opts = {}) {
|
|
351
399
|
const profile = this.addObject(this.stream("/N 3", icc));
|
|
352
400
|
this.outputIntent = this.addObject(`<< /Type /OutputIntent /S /GTS_PDFA1 ` +
|
|
353
|
-
`/OutputConditionIdentifier
|
|
354
|
-
`/Info
|
|
401
|
+
`/OutputConditionIdentifier ${this.pdfString(opts.identifier ?? "sRGB")} ` +
|
|
402
|
+
`/Info ${this.pdfString(opts.info ?? "sRGB IEC61966-2.1")} /DestOutputProfile ${profile} 0 R >>`);
|
|
355
403
|
}
|
|
356
404
|
getOutputIntent() {
|
|
357
405
|
return this.outputIntent;
|
|
@@ -399,6 +447,18 @@ endstream`;
|
|
|
399
447
|
if (idx >= 0)
|
|
400
448
|
this.objects[idx] = this.objects[idx].replace(token, () => ciphertext);
|
|
401
449
|
}
|
|
450
|
+
// Strings: the same idea, but a string is replaced WHOLE - `(text)` becomes `<ciphertext>`. Hex,
|
|
451
|
+
// because the ciphertext is binary and a hex string has no escape rules to get wrong.
|
|
452
|
+
for (let id = 0; id < this.strJobs.length; id++) {
|
|
453
|
+
const cipher = await this.security.encrypt(this.strJobs[id]);
|
|
454
|
+
const hex = Array.from(cipher, (b) => b.toString(16).padStart(2, "0").toUpperCase()).join("");
|
|
455
|
+
const token = this.strToken(id);
|
|
456
|
+
const idx = this.objects.findIndex((o) => o.includes(token));
|
|
457
|
+
if (idx >= 0)
|
|
458
|
+
this.objects[idx] = this.objects[idx].replace(token, () => `<${hex}>`);
|
|
459
|
+
}
|
|
460
|
+
// Added LAST on purpose: the /Encrypt dictionary's own strings must stay in the clear (7.6.2), and
|
|
461
|
+
// they would otherwise be swept up by the pass above.
|
|
402
462
|
this.encryptObjNum = this.addObject(`<< ${this.security.encryptDict()} >>`);
|
|
403
463
|
}
|
|
404
464
|
// Registers (or reuses) a transparency graphics state and returns its resource name
|
|
@@ -505,7 +565,10 @@ endstream`;
|
|
|
505
565
|
}
|
|
506
566
|
if (byStyle.has(style))
|
|
507
567
|
return;
|
|
508
|
-
|
|
568
|
+
// A WOFF is the same sfnt behind a small wrapper, so it is unpacked HERE - the one place every
|
|
569
|
+
// font path meets (a file, raw bytes, a URL, a browser upload) - and TTFParser never learns a
|
|
570
|
+
// second container format.
|
|
571
|
+
byStyle.set(style, new TTFParser(isWoff(data) ? woffToSfnt(data) : data));
|
|
509
572
|
this.verticalsCache.delete(name); // this family now answers from the .ttf, not from an AFM
|
|
510
573
|
this.decorationCache.delete(name);
|
|
511
574
|
// Emission (the PDF font objects + page resource) is DEFERRED to first use via ensureEmitted(),
|
|
@@ -542,13 +605,13 @@ endstream`;
|
|
|
542
605
|
// Fills the reserved font objects with a SUBSET of each font (only the glyphs the document used),
|
|
543
606
|
// tagged "ABCDEF+" as PDF/A requires for subsets. Call once, after the render pass, before output.
|
|
544
607
|
finalizeCustomFonts() {
|
|
545
|
-
for (const e of this.customFontEmit
|
|
608
|
+
for (const [key, e] of this.customFontEmit) {
|
|
546
609
|
const ttf = e.ttf;
|
|
547
610
|
const base = `${this.subsetTag(e.pdfName, e.used)}+${e.pdfName}`;
|
|
548
611
|
this.replaceObject(e.fontFile, this.buildFontFile2(ttf, e.used));
|
|
549
612
|
this.replaceObject(e.descriptor, this.buildFontDescriptor(base, ttf, e.fontFile));
|
|
550
613
|
this.replaceObject(e.cidFont, this.buildCIDFont(base, ttf, e.descriptor, e.used));
|
|
551
|
-
this.replaceObject(e.toUnicode, this.buildToUnicode(ttf, e.used));
|
|
614
|
+
this.replaceObject(e.toUnicode, this.buildToUnicode(ttf, e.used, this.shapedOrigins.get(key)));
|
|
552
615
|
this.replaceObject(e.type0, this.buildType0(base, e.cidFont, e.toUnicode));
|
|
553
616
|
}
|
|
554
617
|
}
|
|
@@ -620,6 +683,17 @@ endstream`;
|
|
|
620
683
|
return this.emojiImage;
|
|
621
684
|
}
|
|
622
685
|
// Whether `ttf` has a COLOR glyph for a code point (i.e. it would render it as color emoji).
|
|
686
|
+
/**
|
|
687
|
+
* Whether a family can draw this code point at all. An embedded face answers from its `cmap`; a
|
|
688
|
+
* standard-14 one can only draw what Windows-1252 encodes, which is what its `/Widths` array covers.
|
|
689
|
+
* Used to pick a family out of a fallback stack, per code point.
|
|
690
|
+
*/
|
|
691
|
+
hasGlyph(codePoint, fontFamily, fontStyle = FontStyle.Normal) {
|
|
692
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
693
|
+
if (ttf)
|
|
694
|
+
return ttf.getGlyphIndex(codePoint) !== 0;
|
|
695
|
+
return isWindows1252(codePoint);
|
|
696
|
+
}
|
|
623
697
|
colorRenders(ttf, codePoint) {
|
|
624
698
|
return ttf.getColorGlyph(ttf.getGlyphIndex(codePoint)) !== null;
|
|
625
699
|
}
|
|
@@ -632,6 +706,53 @@ endstream`;
|
|
|
632
706
|
this.ensureEmitted(name, resolved);
|
|
633
707
|
return this.fonts.getFont(name, resolved);
|
|
634
708
|
}
|
|
709
|
+
/** How many glyphs a run draws - its code-point count unless a ligature merged some. `letterSpacing`
|
|
710
|
+
* is per glyph (`Tc`), so this is what it multiplies. */
|
|
711
|
+
shapedGlyphCount(text, fontFamily, fontStyle) {
|
|
712
|
+
return this.shapeText(text, fontFamily, fontStyle)?.length;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* The shaped glyphs of a run, or undefined when nothing needed shaping. The ONE place shaping is
|
|
716
|
+
* decided, so measuring and drawing cannot disagree. Memoised: a paragraph is measured several
|
|
717
|
+
* times (layout, pagination, drawing).
|
|
718
|
+
*/
|
|
719
|
+
shapeText(text, fontFamily, fontStyle = FontStyle.Normal) {
|
|
720
|
+
const resolved = this.resolveCustomStyle(fontFamily, fontStyle);
|
|
721
|
+
if (!resolved)
|
|
722
|
+
return undefined;
|
|
723
|
+
const key = `${this.customKey(fontFamily, resolved)}\u0000${text}`;
|
|
724
|
+
const cached = this.shapeCache.get(key);
|
|
725
|
+
if (cached !== undefined)
|
|
726
|
+
return cached ?? undefined;
|
|
727
|
+
const ttf = this.customFonts.get(fontFamily).get(resolved);
|
|
728
|
+
const shaped = shapeRun([...text].map((c) => c.codePointAt(0)), ttf) ?? null;
|
|
729
|
+
this.shapeCache.set(key, shaped);
|
|
730
|
+
if (shaped) {
|
|
731
|
+
// Recorded at shaping time - the only moment the glyph and its code points are known together.
|
|
732
|
+
const fontKey = this.customKey(fontFamily, resolved);
|
|
733
|
+
let origins = this.shapedOrigins.get(fontKey);
|
|
734
|
+
if (!origins)
|
|
735
|
+
this.shapedOrigins.set(fontKey, (origins = new Map()));
|
|
736
|
+
for (const g of shaped)
|
|
737
|
+
origins.set(g.glyph, g.codePoints);
|
|
738
|
+
}
|
|
739
|
+
return shaped ?? undefined;
|
|
740
|
+
}
|
|
741
|
+
/** Hex Identity-H for glyphs the producer already resolved (a shaped run), which cannot be given as
|
|
742
|
+
* text. Recording each keeps it in the subset. */
|
|
743
|
+
registerGlyphs(name, glyphs, style = FontStyle.Normal) {
|
|
744
|
+
const resolved = this.resolveCustomStyle(name, style);
|
|
745
|
+
if (!resolved)
|
|
746
|
+
return "";
|
|
747
|
+
this.ensureEmitted(name, resolved);
|
|
748
|
+
const emit = this.customFontEmit.get(this.customKey(name, resolved));
|
|
749
|
+
let out = "";
|
|
750
|
+
for (const g of glyphs) {
|
|
751
|
+
emit.used.add(g);
|
|
752
|
+
out += g.toString(16).padStart(4, "0").toUpperCase();
|
|
753
|
+
}
|
|
754
|
+
return out;
|
|
755
|
+
}
|
|
635
756
|
// Encodes text as a hex Identity-H string for an embedded font's Tj operator: each codepoint
|
|
636
757
|
// becomes its 2-byte glyph id (CID == GID under /CIDToGIDMap /Identity).
|
|
637
758
|
encodeCustomText(name, text, style = FontStyle.Normal) {
|
|
@@ -642,11 +763,22 @@ endstream`;
|
|
|
642
763
|
// Once per text run, not per character: the flat key is fine here.
|
|
643
764
|
const emit = this.customFontEmit.get(this.customKey(name, resolved));
|
|
644
765
|
const { ttf, used } = emit;
|
|
766
|
+
const hex4 = (gid) => gid.toString(16).padStart(4, "0").toUpperCase();
|
|
767
|
+
// The SAME shaped run the measuring path used, so the glyphs drawn are the glyphs measured.
|
|
768
|
+
const shaped = this.shapeText(text, name, style);
|
|
769
|
+
if (shaped) {
|
|
770
|
+
let out = "";
|
|
771
|
+
for (const g of shaped) {
|
|
772
|
+
used.add(g.glyph);
|
|
773
|
+
out += hex4(g.glyph);
|
|
774
|
+
}
|
|
775
|
+
return out;
|
|
776
|
+
}
|
|
645
777
|
let hex = "";
|
|
646
778
|
for (const ch of text) {
|
|
647
779
|
const gid = ttf.getGlyphIndex(ch.codePointAt(0));
|
|
648
780
|
used.add(gid); // record the glyph so the subset keeps it
|
|
649
|
-
hex += gid
|
|
781
|
+
hex += hex4(gid);
|
|
650
782
|
}
|
|
651
783
|
return hex;
|
|
652
784
|
}
|
|
@@ -673,23 +805,30 @@ endstream`;
|
|
|
673
805
|
.map((g) => `${g} [${all[g] ?? 1000}]`)
|
|
674
806
|
.join(" ");
|
|
675
807
|
return (`<< /Type /Font /Subtype /CIDFontType2 /BaseFont /${pdfName(name)} ` +
|
|
676
|
-
`/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> ` +
|
|
808
|
+
`/CIDSystemInfo << /Registry ${this.pdfString("Adobe")} /Ordering ${this.pdfString("Identity")} /Supplement 0 >> ` +
|
|
677
809
|
`/FontDescriptor ${descriptor} 0 R /CIDToGIDMap /Identity ` +
|
|
678
810
|
`/DW 1000 /W [${w}] >>`);
|
|
679
811
|
}
|
|
680
812
|
// Maps glyph id -> Unicode so the text stays copy-/searchable (rendering doesn't need it). Only
|
|
681
813
|
// the used glyphs are listed, to match the subset.
|
|
682
|
-
buildToUnicode(ttf, used) {
|
|
814
|
+
buildToUnicode(ttf, used, shapedOrigins = new Map()) {
|
|
683
815
|
const hex4 = (n) => n.toString(16).padStart(4, "0").toUpperCase();
|
|
816
|
+
// A code point above the BMP needs its surrogate PAIR here - a `bfchar` destination is UTF-16BE.
|
|
817
|
+
const utf16 = (code) => code > 0xffff
|
|
818
|
+
? hex4(0xd800 + ((code - 0x10000) >> 10)) + hex4(0xdc00 + ((code - 0x10000) & 0x3ff))
|
|
819
|
+
: hex4(code);
|
|
684
820
|
const rev = ttf.reverseCmap();
|
|
821
|
+
// A SHAPED glyph wins: it is often absent from the cmap (a ligature) or maps back to a
|
|
822
|
+
// presentation form rather than the letter that was written (a joining form), so the reverse
|
|
823
|
+
// cmap alone would make copied text wrong.
|
|
685
824
|
const entries = [...used]
|
|
686
825
|
.sort((a, b) => a - b)
|
|
687
|
-
.filter((g) => rev.has(g))
|
|
688
|
-
.map((g) => [g, rev.get(g)]);
|
|
826
|
+
.filter((g) => shapedOrigins.has(g) || rev.has(g))
|
|
827
|
+
.map((g) => [g, shapedOrigins.get(g) ?? [rev.get(g)]]);
|
|
689
828
|
const blocks = [];
|
|
690
829
|
for (let i = 0; i < entries.length; i += 100) {
|
|
691
830
|
const block = entries.slice(i, i + 100);
|
|
692
|
-
const lines = block.map(([gid,
|
|
831
|
+
const lines = block.map(([gid, codes]) => `<${hex4(gid)}> <${codes.map(utf16).join("")}>`);
|
|
693
832
|
blocks.push(`${block.length} beginbfchar\n${lines.join("\n")}\nendbfchar`);
|
|
694
833
|
}
|
|
695
834
|
const body = `/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n` +
|
|
@@ -765,6 +904,11 @@ endstream`;
|
|
|
765
904
|
const chars = [...text];
|
|
766
905
|
if (chars.length < 2)
|
|
767
906
|
return [];
|
|
907
|
+
// A shaped run does not kern here: these pairs are keyed by the UNSHAPED glyphs, and the `TJ`
|
|
908
|
+
// path would re-shape each chunk on its own, turning every letter isolated. Arabic kerning is in
|
|
909
|
+
// GPOS, applied during shaping - not built (`todo.md`).
|
|
910
|
+
if (this.shapeText(text, fontFamily, fontStyle))
|
|
911
|
+
return [];
|
|
768
912
|
const out = [];
|
|
769
913
|
// An embedded font kerns by GLYPH id (kern table / GPOS); a standard-14 one by character (AFM).
|
|
770
914
|
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
@@ -816,6 +960,13 @@ endstream`;
|
|
|
816
960
|
}
|
|
817
961
|
// Returns the current width of a text, included kernings
|
|
818
962
|
getStringWidth(text, fontFamily, fontSize, fontStyle) {
|
|
963
|
+
// Shaping first: a joined run is narrower than the same letters apart, so measuring the unshaped
|
|
964
|
+
// form while drawing the shaped one would break every line. Both sides ask `shapeText`.
|
|
965
|
+
const shaped = this.shapeText(text, fontFamily, fontStyle);
|
|
966
|
+
if (shaped) {
|
|
967
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
968
|
+
return shaped.reduce((w, g) => w + ttf.unitsToPoints(g.advance, fontSize), 0);
|
|
969
|
+
}
|
|
819
970
|
let width = 0;
|
|
820
971
|
// Iterate code points, not UTF-16 units: an astral char (emoji, CJK-ext) is a surrogate pair,
|
|
821
972
|
// and indexing by unit would measure each half as its own (zero-width) "char". The spread splits
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// Escapes a PDF literal string ( ... ) - the language tag is safe ASCII, but /Alt can carry arbitrary text.
|
|
2
|
-
const esc = (s) => s.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
|
|
3
2
|
// A structure role reaches the PDF as a raw name (/H1, /S /Table). Roles come from a controlled set,
|
|
4
3
|
// but restrict them to a safe PDF-name token anyway (defence-in-depth) so a stray "/", whitespace, or
|
|
5
4
|
// operator can never corrupt the content stream / object dictionary. Empty after stripping -> /Span.
|
|
@@ -90,7 +89,7 @@ export class StructTree {
|
|
|
90
89
|
...childrenOf(e.key).map((c) => `${objNum.get(c.key)} 0 R`),
|
|
91
90
|
...e.mc.map((m) => `<< /Type /MCR /Pg ${pg(m.structParents)} 0 R /MCID ${m.mcid} >>`),
|
|
92
91
|
].join(" ");
|
|
93
|
-
const alt = e.alt ? ` /Alt
|
|
92
|
+
const alt = e.alt ? ` /Alt ${om.pdfString(e.alt)}` : "";
|
|
94
93
|
// PDF/UA 7.5: a header cell needs a Scope so a reader can associate data cells with it. Our headers
|
|
95
94
|
// are a header ROW (the Table `header` option), so each TH is the head of its Column.
|
|
96
95
|
const attr = e.role === "TH" ? " /A << /O /Table /Scope /Column >>" : "";
|
|
@@ -125,6 +124,6 @@ export class StructTree {
|
|
|
125
124
|
om.replaceObject(parentTreeNum, `<< /Nums [ ${nums.join(" ")} ] >>`);
|
|
126
125
|
om.replaceObject(rootNum, `<< /Type /StructTreeRoot /K ${docNum} 0 R /ParentTree ${parentTreeNum} 0 R ` +
|
|
127
126
|
`/ParentTreeNextKey ${this.structParentsCounter} >>`);
|
|
128
|
-
return `/MarkInfo << /Marked true >> /StructTreeRoot ${rootNum} 0 R /Lang
|
|
127
|
+
return `/MarkInfo << /Marked true >> /StructTreeRoot ${rootNum} 0 R /Lang ${om.pdfString(this.lang)}`;
|
|
129
128
|
}
|
|
130
129
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FontDecoration } from "../text/text-decoration.js";
|
|
2
|
+
import { GsubTable } from "./gsub.js";
|
|
2
3
|
export type GlyphPathCommand = {
|
|
3
4
|
type: "M";
|
|
4
5
|
x: number;
|
|
@@ -77,14 +78,19 @@ export declare class TTFParser {
|
|
|
77
78
|
private kernPairs;
|
|
78
79
|
private hasKerning;
|
|
79
80
|
private gposPairPos;
|
|
81
|
+
private gsubTable?;
|
|
80
82
|
constructor(data: Uint8Array);
|
|
81
83
|
getData(): Uint8Array;
|
|
82
84
|
glyphCount(): number;
|
|
83
85
|
glyphWidths(): number[];
|
|
84
86
|
reverseCmap(): Map<number, number>;
|
|
85
87
|
private toGlyphSpace;
|
|
88
|
+
/** The font's `GSUB`, or undefined when it has none. Parsed once, on demand. */
|
|
89
|
+
gsub(): GsubTable | undefined;
|
|
86
90
|
getGlyphIndex(codePoint: number): number;
|
|
87
91
|
getAdvanceWidth(glyphIndex: number): number;
|
|
92
|
+
/** Font units -> points at `fontSize`; the one scaling rule for shaped and plain advances alike. */
|
|
93
|
+
unitsToPoints(units: number, fontSize: number): number;
|
|
88
94
|
getCharWidth(char: string, fontSize: number): number;
|
|
89
95
|
getStringWidth(text: string, fontSize: number): number;
|
|
90
96
|
private readDecorationMetrics;
|
package/dist/utils/ttf-parser.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Slice 1: glyph advance widths (hmtx) + the Unicode→glyph map (cmap), enough to compute
|
|
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
|
+
import { GsubTable } from "./gsub.js";
|
|
5
6
|
const IDENTITY = [1, 0, 0, 1, 0, 0];
|
|
6
7
|
/** Sorts x-intervals and unions the overlapping ones. */
|
|
7
8
|
export function mergeSpans(spans) {
|
|
@@ -151,6 +152,20 @@ export class TTFParser {
|
|
|
151
152
|
toGlyphSpace(v) {
|
|
152
153
|
return Math.round((v * 1000) / this.unitsPerEm);
|
|
153
154
|
}
|
|
155
|
+
/** The font's `GSUB`, or undefined when it has none. Parsed once, on demand. */
|
|
156
|
+
gsub() {
|
|
157
|
+
if (this.gsubTable === undefined) {
|
|
158
|
+
const record = this.tables["GSUB"];
|
|
159
|
+
// Degrade to "no shaping" rather than break font loading - same rule as kern.
|
|
160
|
+
try {
|
|
161
|
+
this.gsubTable = record ? new GsubTable(this.data, record.offset) : null;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
this.gsubTable = null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return this.gsubTable ?? undefined;
|
|
168
|
+
}
|
|
154
169
|
// Codepoint → glyph id (0 = .notdef when the font has no glyph for it).
|
|
155
170
|
getGlyphIndex(codePoint) {
|
|
156
171
|
const g = this.cmap.get(codePoint);
|
|
@@ -169,6 +184,10 @@ export class TTFParser {
|
|
|
169
184
|
return this.advanceWidths[i] ?? 0;
|
|
170
185
|
}
|
|
171
186
|
// Char width at fontSize, scaled from font units (em = unitsPerEm) to points.
|
|
187
|
+
/** Font units -> points at `fontSize`; the one scaling rule for shaped and plain advances alike. */
|
|
188
|
+
unitsToPoints(units, fontSize) {
|
|
189
|
+
return (units / this.unitsPerEm) * fontSize;
|
|
190
|
+
}
|
|
172
191
|
getCharWidth(char, fontSize) {
|
|
173
192
|
const units = this.getAdvanceWidth(this.getGlyphIndex(char.codePointAt(0)));
|
|
174
193
|
return (units / this.unitsPerEm) * fontSize;
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether Windows-1252 can carry this code point - which is exactly what a standard-14 font can draw,
|
|
3
|
+
* since that is the encoding its `/Widths` array is indexed by. Latin-1 passes straight through,
|
|
4
|
+
* except the C1 range, which the table above fills with printable punctuation instead.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isWindows1252(codePoint: number): boolean;
|
|
1
7
|
/** Encodes a JavaScript string to a Windows-1252 byte buffer (the PDF text encoding). */
|
|
2
8
|
export declare function getArrayBuffer(data: string): ArrayBuffer;
|
|
@@ -33,6 +33,16 @@ const CP1252_FROM_UNICODE = {
|
|
|
33
33
|
0x017e: 0x9e, // ž z caron
|
|
34
34
|
0x0178: 0x9f, // Ÿ Y diaeresis
|
|
35
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Whether Windows-1252 can carry this code point - which is exactly what a standard-14 font can draw,
|
|
38
|
+
* since that is the encoding its `/Widths` array is indexed by. Latin-1 passes straight through,
|
|
39
|
+
* except the C1 range, which the table above fills with printable punctuation instead.
|
|
40
|
+
*/
|
|
41
|
+
export function isWindows1252(codePoint) {
|
|
42
|
+
if (codePoint in CP1252_FROM_UNICODE)
|
|
43
|
+
return true;
|
|
44
|
+
return codePoint <= 0xff && !(codePoint >= 0x80 && codePoint <= 0x9f);
|
|
45
|
+
}
|
|
36
46
|
/** Encodes a JavaScript string to a Windows-1252 byte buffer (the PDF text encoding). */
|
|
37
47
|
export function getArrayBuffer(data) {
|
|
38
48
|
const u8 = new Uint8Array(data.length);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class WoffError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
/** Whether these bytes are a WOFF 1.0 container. Cheap enough to call on every font. */
|
|
5
|
+
export declare const isWoff: (bytes: Uint8Array) => boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Unpack a WOFF into the sfnt it wraps. The result is byte-for-byte what a `.ttf` of the same font
|
|
8
|
+
* would be, apart from table padding, so nothing downstream can tell the difference.
|
|
9
|
+
*/
|
|
10
|
+
export declare function woffToSfnt(woff: Uint8Array): Uint8Array;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { inflateBounded } from "./inflate.js";
|
|
2
|
+
/**
|
|
3
|
+
* WOFF 1.0 -> plain sfnt, so the existing `TTFParser` never learns a second container format.
|
|
4
|
+
*
|
|
5
|
+
* A WOFF is not a different FONT, it is a different wrapper: the same sfnt tables, each optionally
|
|
6
|
+
* zlib-compressed, behind a small header. Unpacking it back into an sfnt is therefore the whole job -
|
|
7
|
+
* every metric, glyph and cmap path downstream stays exactly as it was.
|
|
8
|
+
*
|
|
9
|
+
* WOFF2 is deliberately NOT here: it compresses with Brotli, which `fflate` does not do, and it also
|
|
10
|
+
* TRANSFORMS the glyf/loca tables rather than merely deflating them. That is a different piece of work,
|
|
11
|
+
* not a bigger version of this one.
|
|
12
|
+
*/
|
|
13
|
+
const SIG_WOFF = 0x774f4646; // "wOFF"
|
|
14
|
+
const HEADER = 44;
|
|
15
|
+
const DIR_ENTRY = 20;
|
|
16
|
+
/** One table may not inflate past this. A single sfnt table above 64 MB is not a real font. */
|
|
17
|
+
const MAX_TABLE_BYTES = 64 * 1024 * 1024;
|
|
18
|
+
/**
|
|
19
|
+
* And all of them together may not either. The per-table ceiling alone is not a bound: `numTables` is a
|
|
20
|
+
* uint16, so 65535 directory entries may all point at the SAME small compressed blob, and each would be
|
|
21
|
+
* inflated and kept. Generous enough for a full CJK face, which is the largest real font there is.
|
|
22
|
+
*/
|
|
23
|
+
const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
24
|
+
export class WoffError extends Error {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(`@jasy/pdf: ${message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const be32 = (b, at) => ((b[at] << 24) | (b[at + 1] << 16) | (b[at + 2] << 8) | b[at + 3]) >>> 0;
|
|
30
|
+
const be16 = (b, at) => (b[at] << 8) | b[at + 1];
|
|
31
|
+
/** Whether these bytes are a WOFF 1.0 container. Cheap enough to call on every font. */
|
|
32
|
+
export const isWoff = (bytes) => bytes.length >= HEADER && be32(bytes, 0) === SIG_WOFF;
|
|
33
|
+
/**
|
|
34
|
+
* Unpack a WOFF into the sfnt it wraps. The result is byte-for-byte what a `.ttf` of the same font
|
|
35
|
+
* would be, apart from table padding, so nothing downstream can tell the difference.
|
|
36
|
+
*/
|
|
37
|
+
export function woffToSfnt(woff) {
|
|
38
|
+
if (!isWoff(woff))
|
|
39
|
+
throw new WoffError("these bytes are not a WOFF container");
|
|
40
|
+
const flavor = be32(woff, 4);
|
|
41
|
+
const numTables = be16(woff, 12);
|
|
42
|
+
if (numTables === 0)
|
|
43
|
+
throw new WoffError("this WOFF declares no tables");
|
|
44
|
+
if (woff.length < HEADER + numTables * DIR_ENTRY) {
|
|
45
|
+
throw new WoffError("this WOFF is truncated: its table directory does not fit in the file");
|
|
46
|
+
}
|
|
47
|
+
// Read the directory first, then the tables, so a malformed offset is caught before any allocation.
|
|
48
|
+
const entries = Array.from({ length: numTables }, (_, i) => {
|
|
49
|
+
const at = HEADER + i * DIR_ENTRY;
|
|
50
|
+
return {
|
|
51
|
+
tag: be32(woff, at),
|
|
52
|
+
offset: be32(woff, at + 4),
|
|
53
|
+
compLength: be32(woff, at + 8),
|
|
54
|
+
origLength: be32(woff, at + 12),
|
|
55
|
+
checksum: be32(woff, at + 16),
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
// Tracked across ALL tables, and checked BEFORE each inflate so a hostile directory costs neither the
|
|
59
|
+
// CPU of unpacking nor the memory of keeping the result.
|
|
60
|
+
let unpacked = 0;
|
|
61
|
+
const claim = (bytes) => {
|
|
62
|
+
unpacked += bytes;
|
|
63
|
+
if (unpacked > MAX_TOTAL_BYTES) {
|
|
64
|
+
throw new WoffError(`this WOFF unpacks to more than ${MAX_TOTAL_BYTES} bytes in total; no real font is that large`);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const tables = entries.map((e) => {
|
|
68
|
+
if (e.offset + e.compLength > woff.length) {
|
|
69
|
+
throw new WoffError(`this WOFF is truncated: a table runs past the end of the file`);
|
|
70
|
+
}
|
|
71
|
+
claim(e.origLength); // what it DECLARES, before any work is done on it
|
|
72
|
+
const raw = woff.subarray(e.offset, e.offset + e.compLength);
|
|
73
|
+
// Equal lengths mean the table was stored as-is; anything else is zlib, per the spec.
|
|
74
|
+
if (e.compLength === e.origLength)
|
|
75
|
+
return raw;
|
|
76
|
+
let out;
|
|
77
|
+
try {
|
|
78
|
+
out = inflateBounded(raw, MAX_TABLE_BYTES);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
throw new WoffError(`a table in this WOFF could not be unpacked: ${String(err?.message ?? err)}`);
|
|
82
|
+
}
|
|
83
|
+
if (out.length !== e.origLength) {
|
|
84
|
+
throw new WoffError(`a table in this WOFF unpacked to ${out.length} bytes where it declared ${e.origLength}`);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
});
|
|
88
|
+
// Reassemble: offset table, directory, then the tables each padded to a 4-byte boundary.
|
|
89
|
+
const padded = (n) => (n + 3) & ~3;
|
|
90
|
+
const dirSize = numTables * 16;
|
|
91
|
+
const total = 12 + dirSize + tables.reduce((n, t) => n + padded(t.length), 0);
|
|
92
|
+
const out = new Uint8Array(total);
|
|
93
|
+
const view = new DataView(out.buffer);
|
|
94
|
+
// The binary-search hints an sfnt offset table carries. Readers may ignore them; a correct file has
|
|
95
|
+
// them right, and getting them wrong is the classic way a "valid" font is rejected by a strict one.
|
|
96
|
+
const entrySelector = Math.floor(Math.log2(numTables));
|
|
97
|
+
const searchRange = 2 ** entrySelector * 16;
|
|
98
|
+
view.setUint32(0, flavor);
|
|
99
|
+
view.setUint16(4, numTables);
|
|
100
|
+
view.setUint16(6, searchRange);
|
|
101
|
+
view.setUint16(8, entrySelector);
|
|
102
|
+
view.setUint16(10, numTables * 16 - searchRange);
|
|
103
|
+
let dir = 12;
|
|
104
|
+
let body = 12 + dirSize;
|
|
105
|
+
entries.forEach((e, i) => {
|
|
106
|
+
const data = tables[i];
|
|
107
|
+
view.setUint32(dir, e.tag);
|
|
108
|
+
view.setUint32(dir + 4, e.checksum);
|
|
109
|
+
view.setUint32(dir + 8, body);
|
|
110
|
+
view.setUint32(dir + 12, data.length); // the DECLARED length excludes the padding
|
|
111
|
+
out.set(data, body);
|
|
112
|
+
dir += 16;
|
|
113
|
+
body += padded(data.length);
|
|
114
|
+
});
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
@@ -41,9 +41,20 @@ 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
|
+
// `flex: 0` used to be meaningless - claiming no share is what leaving the element out does. With a
|
|
48
|
+
// `flexBasis` it is not: the child is then exactly its basis, a fixed slot that still participates
|
|
49
|
+
// in the line. So the rule now reads "claim SOMETHING": a share, a basis, or both.
|
|
50
|
+
// A percentage basis resolves to 0 on an unbounded axis, so it is measured against a finite
|
|
51
|
+
// reference instead - otherwise `flexBasis: "0%"` would claim nothing and still pass.
|
|
52
|
+
const claimsSpace = element.getBasis(Infinity) > 0 || element.getBasis(1) > 0;
|
|
53
|
+
if (flex < 0 || (flex === 0 && !claimsSpace)) {
|
|
54
|
+
throw new Error(`@jasy/pdf: Spacer/Expanded needs a flex above 0, got ${flex}. Flex is a SHARE of the leftover ` +
|
|
55
|
+
"space, so 0 would claim none - which is what leaving the element out does. For a fixed gap " +
|
|
56
|
+
"use the `gap` of the surrounding Column or Row, or a Box with a height. (A `flexBasis` " +
|
|
57
|
+
"makes `flex: 0` meaningful: the child is then exactly its basis.)");
|
|
47
58
|
}
|
|
48
59
|
// Ensure a flexible element does not contain another flexible element
|
|
49
60
|
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.9",
|
|
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,9 +32,15 @@
|
|
|
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": {
|
|
43
|
+
"bidi-js": "^1.0.3",
|
|
36
44
|
"fflate": "^0.8.3",
|
|
37
45
|
"jimp": "^1.6.0"
|
|
38
46
|
},
|