@jasy/pdf 1.0.0-alpha.8 → 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 +4 -3
- package/dist/api/content.d.ts +5 -4
- package/dist/api/content.js +13 -7
- 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/gradient.d.ts +53 -0
- package/dist/api/gradient.js +97 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -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 +5 -0
- package/dist/api/structure.js +16 -4
- package/dist/api/text.d.ts +42 -6
- package/dist/api/text.js +29 -5
- package/dist/edit/document.d.ts +14 -4
- package/dist/edit/document.js +15 -22
- package/dist/edit/fill.d.ts +19 -2
- package/dist/edit/fill.js +63 -34
- package/dist/edit/flatten.d.ts +13 -1
- package/dist/edit/flatten.js +38 -13
- package/dist/edit/index.d.ts +2 -0
- package/dist/edit/index.js +3 -1
- package/dist/edit/objects.d.ts +5 -0
- package/dist/edit/objects.js +27 -0
- package/dist/edit/writer.d.ts +7 -0
- package/dist/edit/writer.js +23 -1
- package/dist/elements/container-element.d.ts +21 -1
- package/dist/elements/container-element.js +25 -10
- package/dist/elements/image-element.d.ts +18 -4
- package/dist/elements/image-element.js +27 -12
- 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/ir/display-list.d.ts +23 -6
- 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/image-renderer.js +2 -1
- package/dist/renderer/pdf-backend.d.ts +6 -0
- package/dist/renderer/pdf-backend.js +78 -31
- package/dist/renderer/pdf-document-renderer.js +8 -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/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/pdf-object-manager.d.ts +21 -0
- package/dist/utils/pdf-object-manager.js +107 -9
- 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 +9 -2
- package/package.json +2 -1
|
@@ -5,8 +5,10 @@ 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";
|
|
@@ -161,6 +163,11 @@ export class PDFObjectManager {
|
|
|
161
163
|
// Per-registered face: the reserved font-object numbers + the glyph ids actually used. The font
|
|
162
164
|
// program is subsetted and the objects filled in by finalizeCustomFonts(), after the render pass.
|
|
163
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();
|
|
164
171
|
// Embedded file attachments (their /Filespec object numbers + display names). Referenced from
|
|
165
172
|
// the catalog's /Names/EmbeddedFiles + /AF. Drives ZUGFeRD's embedded factur-x.xml.
|
|
166
173
|
this.attachments = [];
|
|
@@ -558,7 +565,10 @@ endstream`;
|
|
|
558
565
|
}
|
|
559
566
|
if (byStyle.has(style))
|
|
560
567
|
return;
|
|
561
|
-
|
|
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));
|
|
562
572
|
this.verticalsCache.delete(name); // this family now answers from the .ttf, not from an AFM
|
|
563
573
|
this.decorationCache.delete(name);
|
|
564
574
|
// Emission (the PDF font objects + page resource) is DEFERRED to first use via ensureEmitted(),
|
|
@@ -595,13 +605,13 @@ endstream`;
|
|
|
595
605
|
// Fills the reserved font objects with a SUBSET of each font (only the glyphs the document used),
|
|
596
606
|
// tagged "ABCDEF+" as PDF/A requires for subsets. Call once, after the render pass, before output.
|
|
597
607
|
finalizeCustomFonts() {
|
|
598
|
-
for (const e of this.customFontEmit
|
|
608
|
+
for (const [key, e] of this.customFontEmit) {
|
|
599
609
|
const ttf = e.ttf;
|
|
600
610
|
const base = `${this.subsetTag(e.pdfName, e.used)}+${e.pdfName}`;
|
|
601
611
|
this.replaceObject(e.fontFile, this.buildFontFile2(ttf, e.used));
|
|
602
612
|
this.replaceObject(e.descriptor, this.buildFontDescriptor(base, ttf, e.fontFile));
|
|
603
613
|
this.replaceObject(e.cidFont, this.buildCIDFont(base, ttf, e.descriptor, e.used));
|
|
604
|
-
this.replaceObject(e.toUnicode, this.buildToUnicode(ttf, e.used));
|
|
614
|
+
this.replaceObject(e.toUnicode, this.buildToUnicode(ttf, e.used, this.shapedOrigins.get(key)));
|
|
605
615
|
this.replaceObject(e.type0, this.buildType0(base, e.cidFont, e.toUnicode));
|
|
606
616
|
}
|
|
607
617
|
}
|
|
@@ -673,6 +683,17 @@ endstream`;
|
|
|
673
683
|
return this.emojiImage;
|
|
674
684
|
}
|
|
675
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
|
+
}
|
|
676
697
|
colorRenders(ttf, codePoint) {
|
|
677
698
|
return ttf.getColorGlyph(ttf.getGlyphIndex(codePoint)) !== null;
|
|
678
699
|
}
|
|
@@ -685,6 +706,53 @@ endstream`;
|
|
|
685
706
|
this.ensureEmitted(name, resolved);
|
|
686
707
|
return this.fonts.getFont(name, resolved);
|
|
687
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
|
+
}
|
|
688
756
|
// Encodes text as a hex Identity-H string for an embedded font's Tj operator: each codepoint
|
|
689
757
|
// becomes its 2-byte glyph id (CID == GID under /CIDToGIDMap /Identity).
|
|
690
758
|
encodeCustomText(name, text, style = FontStyle.Normal) {
|
|
@@ -695,11 +763,22 @@ endstream`;
|
|
|
695
763
|
// Once per text run, not per character: the flat key is fine here.
|
|
696
764
|
const emit = this.customFontEmit.get(this.customKey(name, resolved));
|
|
697
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
|
+
}
|
|
698
777
|
let hex = "";
|
|
699
778
|
for (const ch of text) {
|
|
700
779
|
const gid = ttf.getGlyphIndex(ch.codePointAt(0));
|
|
701
780
|
used.add(gid); // record the glyph so the subset keeps it
|
|
702
|
-
hex += gid
|
|
781
|
+
hex += hex4(gid);
|
|
703
782
|
}
|
|
704
783
|
return hex;
|
|
705
784
|
}
|
|
@@ -732,17 +811,24 @@ endstream`;
|
|
|
732
811
|
}
|
|
733
812
|
// Maps glyph id -> Unicode so the text stays copy-/searchable (rendering doesn't need it). Only
|
|
734
813
|
// the used glyphs are listed, to match the subset.
|
|
735
|
-
buildToUnicode(ttf, used) {
|
|
814
|
+
buildToUnicode(ttf, used, shapedOrigins = new Map()) {
|
|
736
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);
|
|
737
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.
|
|
738
824
|
const entries = [...used]
|
|
739
825
|
.sort((a, b) => a - b)
|
|
740
|
-
.filter((g) => rev.has(g))
|
|
741
|
-
.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)]]);
|
|
742
828
|
const blocks = [];
|
|
743
829
|
for (let i = 0; i < entries.length; i += 100) {
|
|
744
830
|
const block = entries.slice(i, i + 100);
|
|
745
|
-
const lines = block.map(([gid,
|
|
831
|
+
const lines = block.map(([gid, codes]) => `<${hex4(gid)}> <${codes.map(utf16).join("")}>`);
|
|
746
832
|
blocks.push(`${block.length} beginbfchar\n${lines.join("\n")}\nendbfchar`);
|
|
747
833
|
}
|
|
748
834
|
const body = `/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n` +
|
|
@@ -818,6 +904,11 @@ endstream`;
|
|
|
818
904
|
const chars = [...text];
|
|
819
905
|
if (chars.length < 2)
|
|
820
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 [];
|
|
821
912
|
const out = [];
|
|
822
913
|
// An embedded font kerns by GLYPH id (kern table / GPOS); a standard-14 one by character (AFM).
|
|
823
914
|
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
@@ -869,6 +960,13 @@ endstream`;
|
|
|
869
960
|
}
|
|
870
961
|
// Returns the current width of a text, included kernings
|
|
871
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
|
+
}
|
|
872
970
|
let width = 0;
|
|
873
971
|
// Iterate code points, not UTF-16 units: an astral char (emoji, CJK-ext) is a surrogate pair,
|
|
874
972
|
// and indexing by unit would measure each half as its own (zero-width) "char". The spread splits
|
|
@@ -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
|
+
}
|
|
@@ -44,10 +44,17 @@ export class Validator {
|
|
|
44
44
|
// Ensure flexible elements have valid flex values. The message names what the CALLER wrote, not the
|
|
45
45
|
// element class: `Spacer` and `Expanded` are what exists in their document, `ExpandedElement` is not.
|
|
46
46
|
const flex = element.getFlex();
|
|
47
|
-
|
|
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)) {
|
|
48
54
|
throw new Error(`@jasy/pdf: Spacer/Expanded needs a flex above 0, got ${flex}. Flex is a SHARE of the leftover ` +
|
|
49
55
|
"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."
|
|
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.)");
|
|
51
58
|
}
|
|
52
59
|
// Ensure a flexible element does not contain another flexible element
|
|
53
60
|
if (hasChildProp(element)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"./package.json": "./package.json"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
+
"bidi-js": "^1.0.3",
|
|
43
44
|
"fflate": "^0.8.3",
|
|
44
45
|
"jimp": "^1.6.0"
|
|
45
46
|
},
|