@jasy/pdf 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -0
- package/dist/api/args.d.ts +10 -0
- package/dist/api/args.js +13 -0
- package/dist/api/color.d.ts +24 -0
- package/dist/api/color.js +215 -0
- package/dist/api/content.d.ts +36 -0
- package/dist/api/content.js +50 -0
- package/dist/api/descriptor.d.ts +25 -0
- package/dist/api/descriptor.js +71 -0
- package/dist/api/index.d.ts +8 -0
- package/dist/api/index.js +27 -0
- package/dist/api/insets.d.ts +16 -0
- package/dist/api/insets.js +21 -0
- package/dist/api/layout.d.ts +72 -0
- package/dist/api/layout.js +99 -0
- package/dist/api/structure.d.ts +80 -0
- package/dist/api/structure.js +125 -0
- package/dist/api/table.d.ts +37 -0
- package/dist/api/table.js +87 -0
- package/dist/api/text.d.ts +28 -0
- package/dist/api/text.js +61 -0
- package/dist/assets/Courier-Bold.afm +342 -0
- package/dist/assets/Courier-BoldOblique.afm +342 -0
- package/dist/assets/Courier-Oblique.afm +342 -0
- package/dist/assets/Courier.afm +342 -0
- package/dist/assets/Helvetica-Bold.afm +2827 -0
- package/dist/assets/Helvetica-BoldOblique.afm +2827 -0
- package/dist/assets/Helvetica-Oblique.afm +3051 -0
- package/dist/assets/Helvetica.afm +3051 -0
- package/dist/assets/Symbol.afm +213 -0
- package/dist/assets/Times-Bold.afm +2588 -0
- package/dist/assets/Times-BoldItalic.afm +2384 -0
- package/dist/assets/Times-Italic.afm +2667 -0
- package/dist/assets/Times-Roman.afm +2419 -0
- package/dist/assets/ZapfDingbats.afm +225 -0
- package/dist/assets/agl.txt +695 -0
- package/dist/common/color.d.ts +37 -0
- package/dist/common/color.js +62 -0
- package/dist/constants/page-sizes.d.ts +44 -0
- package/dist/constants/page-sizes.js +92 -0
- package/dist/constants/pdf-parts.d.ts +5 -0
- package/dist/constants/pdf-parts.js +8 -0
- package/dist/elements/container-element.d.ts +35 -0
- package/dist/elements/container-element.js +91 -0
- package/dist/elements/image-element.d.ts +56 -0
- package/dist/elements/image-element.js +151 -0
- package/dist/elements/index.d.ts +10 -0
- package/dist/elements/index.js +28 -0
- package/dist/elements/layout/deferred-element.d.ts +19 -0
- package/dist/elements/layout/deferred-element.js +33 -0
- package/dist/elements/layout/expanded-element.d.ts +30 -0
- package/dist/elements/layout/expanded-element.js +68 -0
- package/dist/elements/layout/padding-element.d.ts +29 -0
- package/dist/elements/layout/padding-element.js +76 -0
- package/dist/elements/layout/repeating-header-element.d.ts +29 -0
- package/dist/elements/layout/repeating-header-element.js +60 -0
- package/dist/elements/layout/sized-container-element.d.ts +14 -0
- package/dist/elements/layout/sized-container-element.js +37 -0
- package/dist/elements/line-element.d.ts +31 -0
- package/dist/elements/line-element.js +44 -0
- package/dist/elements/page-element.d.ts +58 -0
- package/dist/elements/page-element.js +90 -0
- package/dist/elements/pdf-document-element.d.ts +13 -0
- package/dist/elements/pdf-document-element.js +22 -0
- package/dist/elements/pdf-element.d.ts +67 -0
- package/dist/elements/pdf-element.js +55 -0
- package/dist/elements/rectangle-element.d.ts +55 -0
- package/dist/elements/rectangle-element.js +120 -0
- package/dist/elements/row-element.d.ts +42 -0
- package/dist/elements/row-element.js +54 -0
- package/dist/elements/text-element.d.ts +59 -0
- package/dist/elements/text-element.js +137 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +21 -0
- package/dist/ir/display-list.d.ts +74 -0
- package/dist/ir/display-list.js +2 -0
- package/dist/layout/box-constraints.d.ts +56 -0
- package/dist/layout/box-constraints.js +73 -0
- package/dist/layout/fragmentation.d.ts +42 -0
- package/dist/layout/fragmentation.js +61 -0
- package/dist/renderer/container-renderer.d.ts +6 -0
- package/dist/renderer/container-renderer.js +30 -0
- package/dist/renderer/deferred-renderer.d.ts +6 -0
- package/dist/renderer/deferred-renderer.js +25 -0
- package/dist/renderer/expanded-renderer.d.ts +6 -0
- package/dist/renderer/expanded-renderer.js +23 -0
- package/dist/renderer/image-renderer.d.ts +6 -0
- package/dist/renderer/image-renderer.js +85 -0
- package/dist/renderer/index.d.ts +10 -0
- package/dist/renderer/index.js +26 -0
- package/dist/renderer/line-renderer.d.ts +6 -0
- package/dist/renderer/line-renderer.js +30 -0
- package/dist/renderer/padding-renderer.d.ts +6 -0
- package/dist/renderer/padding-renderer.js +23 -0
- package/dist/renderer/page-renderer.d.ts +5 -0
- package/dist/renderer/page-renderer.js +81 -0
- package/dist/renderer/pdf-backend.d.ts +45 -0
- package/dist/renderer/pdf-backend.js +184 -0
- package/dist/renderer/pdf-config.d.ts +8 -0
- package/dist/renderer/pdf-config.js +17 -0
- package/dist/renderer/pdf-document-class.d.ts +42 -0
- package/dist/renderer/pdf-document-class.js +118 -0
- package/dist/renderer/pdf-document-renderer.d.ts +20 -0
- package/dist/renderer/pdf-document-renderer.js +104 -0
- package/dist/renderer/pdf-renderer.d.ts +5 -0
- package/dist/renderer/pdf-renderer.js +92 -0
- package/dist/renderer/rectangle-renderer.d.ts +8 -0
- package/dist/renderer/rectangle-renderer.js +61 -0
- package/dist/renderer/repeating-header-renderer.d.ts +6 -0
- package/dist/renderer/repeating-header-renderer.js +28 -0
- package/dist/renderer/row-renderer.d.ts +6 -0
- package/dist/renderer/row-renderer.js +30 -0
- package/dist/renderer/text-renderer.d.ts +9 -0
- package/dist/renderer/text-renderer.js +125 -0
- package/dist/text/line-breaker.d.ts +40 -0
- package/dist/text/line-breaker.js +106 -0
- package/dist/utils/afm-parser.d.ts +12 -0
- package/dist/utils/afm-parser.js +91 -0
- package/dist/utils/flex-layout.d.ts +53 -0
- package/dist/utils/flex-layout.js +119 -0
- package/dist/utils/font-metrics.d.ts +12 -0
- package/dist/utils/font-metrics.js +2 -0
- package/dist/utils/font-path.d.ts +1 -0
- package/dist/utils/font-path.js +19 -0
- package/dist/utils/image-helper.d.ts +43 -0
- package/dist/utils/image-helper.js +206 -0
- package/dist/utils/pdf-object-manager.d.ts +97 -0
- package/dist/utils/pdf-object-manager.js +645 -0
- package/dist/utils/renderer-registry.d.ts +6 -0
- package/dist/utils/renderer-registry.js +19 -0
- package/dist/utils/ttf-parser.d.ts +29 -0
- package/dist/utils/ttf-parser.js +191 -0
- package/dist/utils/ttf-subsetter.d.ts +1 -0
- package/dist/utils/ttf-subsetter.js +161 -0
- package/dist/utils/utf8-to-windows1252-encoder.d.ts +2 -0
- package/dist/utils/utf8-to-windows1252-encoder.js +55 -0
- package/dist/validators/element-validator.d.ts +8 -0
- package/dist/validators/element-validator.js +61 -0
- package/package.json +50 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare class TTFParser {
|
|
2
|
+
private data;
|
|
3
|
+
unitsPerEm: number;
|
|
4
|
+
ascent: number;
|
|
5
|
+
descent: number;
|
|
6
|
+
bbox: [number, number, number, number];
|
|
7
|
+
private numGlyphs;
|
|
8
|
+
private numHMetrics;
|
|
9
|
+
private advanceWidths;
|
|
10
|
+
private cmap;
|
|
11
|
+
private cmapGroups;
|
|
12
|
+
private tables;
|
|
13
|
+
constructor(data: Buffer);
|
|
14
|
+
getData(): Buffer;
|
|
15
|
+
glyphCount(): number;
|
|
16
|
+
glyphWidths(): number[];
|
|
17
|
+
reverseCmap(): Map<number, number>;
|
|
18
|
+
private toGlyphSpace;
|
|
19
|
+
getGlyphIndex(codePoint: number): number;
|
|
20
|
+
getAdvanceWidth(glyphIndex: number): number;
|
|
21
|
+
getCharWidth(char: string, fontSize: number): number;
|
|
22
|
+
getStringWidth(text: string, fontSize: number): number;
|
|
23
|
+
private table;
|
|
24
|
+
private readTableDirectory;
|
|
25
|
+
private readHmtx;
|
|
26
|
+
private readCmap;
|
|
27
|
+
private readCmapFormat4;
|
|
28
|
+
private readCmapFormat12;
|
|
29
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Parses the metric tables of a TrueType (.ttf) font - the binary counterpart to AFMParser.
|
|
3
|
+
// Slice 1: glyph advance widths (hmtx) + the Unicode→glyph map (cmap), enough to compute
|
|
4
|
+
// text width the same way AFMParser does for the standard-14. Embedding/subsetting come later.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TTFParser = void 0;
|
|
7
|
+
class TTFParser {
|
|
8
|
+
constructor(data) {
|
|
9
|
+
this.data = data;
|
|
10
|
+
this.unitsPerEm = 1000;
|
|
11
|
+
// FontDescriptor metrics, in PDF glyph space (1000 units / em).
|
|
12
|
+
this.ascent = 0;
|
|
13
|
+
this.descent = 0;
|
|
14
|
+
this.bbox = [0, 0, 0, 0];
|
|
15
|
+
this.numGlyphs = 0;
|
|
16
|
+
this.numHMetrics = 0;
|
|
17
|
+
this.advanceWidths = []; // per glyph id, in font units
|
|
18
|
+
this.cmap = new Map(); // codepoint → glyph id (format 4)
|
|
19
|
+
this.cmapGroups = []; // format 12, searched on lookup
|
|
20
|
+
this.tables = {};
|
|
21
|
+
this.readTableDirectory();
|
|
22
|
+
const head = this.table("head").offset;
|
|
23
|
+
this.unitsPerEm = this.data.readUInt16BE(head + 18);
|
|
24
|
+
this.numGlyphs = this.data.readUInt16BE(this.table("maxp").offset + 4);
|
|
25
|
+
this.numHMetrics = this.data.readUInt16BE(this.table("hhea").offset + 34);
|
|
26
|
+
this.bbox = [
|
|
27
|
+
this.toGlyphSpace(this.data.readInt16BE(head + 36)), // xMin
|
|
28
|
+
this.toGlyphSpace(this.data.readInt16BE(head + 38)), // yMin
|
|
29
|
+
this.toGlyphSpace(this.data.readInt16BE(head + 40)), // xMax
|
|
30
|
+
this.toGlyphSpace(this.data.readInt16BE(head + 42)), // yMax
|
|
31
|
+
];
|
|
32
|
+
const hhea = this.table("hhea").offset;
|
|
33
|
+
this.ascent = this.toGlyphSpace(this.data.readInt16BE(hhea + 4));
|
|
34
|
+
this.descent = this.toGlyphSpace(this.data.readInt16BE(hhea + 6));
|
|
35
|
+
this.readHmtx();
|
|
36
|
+
this.readCmap();
|
|
37
|
+
}
|
|
38
|
+
// The raw font bytes, for the /FontFile2 stream.
|
|
39
|
+
getData() {
|
|
40
|
+
return this.data;
|
|
41
|
+
}
|
|
42
|
+
glyphCount() {
|
|
43
|
+
return this.numGlyphs;
|
|
44
|
+
}
|
|
45
|
+
// Advance width of every glyph in PDF glyph space (the /W array of the CIDFont).
|
|
46
|
+
glyphWidths() {
|
|
47
|
+
const out = [];
|
|
48
|
+
for (let g = 0; g < this.numGlyphs; g++) {
|
|
49
|
+
out.push(this.toGlyphSpace(this.getAdvanceWidth(g)));
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
// glyph id → Unicode codepoint, for the /ToUnicode CMap (BMP / format-4 coverage).
|
|
54
|
+
reverseCmap() {
|
|
55
|
+
const out = new Map();
|
|
56
|
+
this.cmap.forEach((gid, code) => {
|
|
57
|
+
if (!out.has(gid))
|
|
58
|
+
out.set(gid, code);
|
|
59
|
+
});
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
toGlyphSpace(v) {
|
|
63
|
+
return Math.round((v * 1000) / this.unitsPerEm);
|
|
64
|
+
}
|
|
65
|
+
// Codepoint → glyph id (0 = .notdef when the font has no glyph for it).
|
|
66
|
+
getGlyphIndex(codePoint) {
|
|
67
|
+
const g = this.cmap.get(codePoint);
|
|
68
|
+
if (g !== undefined)
|
|
69
|
+
return g;
|
|
70
|
+
for (const r of this.cmapGroups) {
|
|
71
|
+
if (codePoint >= r.start && codePoint <= r.end) {
|
|
72
|
+
return r.startGlyph + (codePoint - r.start);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
// Advance width of a glyph in font units. Glyphs past numHMetrics reuse the last advance.
|
|
78
|
+
getAdvanceWidth(glyphIndex) {
|
|
79
|
+
var _a;
|
|
80
|
+
const i = Math.min(glyphIndex, this.numHMetrics - 1);
|
|
81
|
+
return (_a = this.advanceWidths[i]) !== null && _a !== void 0 ? _a : 0;
|
|
82
|
+
}
|
|
83
|
+
// Char width at fontSize, scaled from font units (em = unitsPerEm) to points.
|
|
84
|
+
getCharWidth(char, fontSize) {
|
|
85
|
+
const units = this.getAdvanceWidth(this.getGlyphIndex(char.codePointAt(0)));
|
|
86
|
+
return (units / this.unitsPerEm) * fontSize;
|
|
87
|
+
}
|
|
88
|
+
// String width at fontSize.
|
|
89
|
+
getStringWidth(text, fontSize) {
|
|
90
|
+
let width = 0;
|
|
91
|
+
for (const ch of text)
|
|
92
|
+
width += this.getCharWidth(ch, fontSize);
|
|
93
|
+
return width;
|
|
94
|
+
}
|
|
95
|
+
table(tag) {
|
|
96
|
+
const t = this.tables[tag];
|
|
97
|
+
if (!t)
|
|
98
|
+
throw new Error(`TTF: missing required table "${tag}"`);
|
|
99
|
+
return t;
|
|
100
|
+
}
|
|
101
|
+
readTableDirectory() {
|
|
102
|
+
const numTables = this.data.readUInt16BE(4);
|
|
103
|
+
let p = 12; // after the offset table (sfntVersion + numTables + 3 fields)
|
|
104
|
+
for (let i = 0; i < numTables; i++) {
|
|
105
|
+
const tag = this.data.toString("latin1", p, p + 4);
|
|
106
|
+
this.tables[tag] = {
|
|
107
|
+
offset: this.data.readUInt32BE(p + 8),
|
|
108
|
+
length: this.data.readUInt32BE(p + 12),
|
|
109
|
+
};
|
|
110
|
+
p += 16;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// hmtx is numHMetrics {advanceWidth, lsb} pairs, then lsb-only entries that reuse the last advance.
|
|
114
|
+
readHmtx() {
|
|
115
|
+
const o = this.table("hmtx").offset;
|
|
116
|
+
let last = 0;
|
|
117
|
+
for (let i = 0; i < this.numGlyphs; i++) {
|
|
118
|
+
if (i < this.numHMetrics)
|
|
119
|
+
last = this.data.readUInt16BE(o + i * 4);
|
|
120
|
+
this.advanceWidths.push(last);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
readCmap() {
|
|
124
|
+
const base = this.table("cmap").offset;
|
|
125
|
+
const numTables = this.data.readUInt16BE(base + 2);
|
|
126
|
+
// Prefer the Windows Unicode BMP subtable (3,1); accept full-Unicode (3,10) or any (0,x).
|
|
127
|
+
let subtable = -1;
|
|
128
|
+
let p = base + 4;
|
|
129
|
+
for (let i = 0; i < numTables; i++) {
|
|
130
|
+
const platform = this.data.readUInt16BE(p);
|
|
131
|
+
const encoding = this.data.readUInt16BE(p + 2);
|
|
132
|
+
const offset = this.data.readUInt32BE(p + 4);
|
|
133
|
+
const unicode = (platform === 3 && (encoding === 1 || encoding === 10)) || platform === 0;
|
|
134
|
+
if (unicode) {
|
|
135
|
+
subtable = base + offset;
|
|
136
|
+
if (platform === 3 && encoding === 1)
|
|
137
|
+
break; // best fit, stop looking
|
|
138
|
+
}
|
|
139
|
+
p += 8;
|
|
140
|
+
}
|
|
141
|
+
if (subtable < 0)
|
|
142
|
+
return;
|
|
143
|
+
const format = this.data.readUInt16BE(subtable);
|
|
144
|
+
if (format === 4)
|
|
145
|
+
this.readCmapFormat4(subtable);
|
|
146
|
+
else if (format === 12)
|
|
147
|
+
this.readCmapFormat12(subtable);
|
|
148
|
+
}
|
|
149
|
+
// Format 4: segment mapping. Four parallel arrays keyed by segment; the glyph comes either
|
|
150
|
+
// from idDelta or, when idRangeOffset != 0, from the glyphIdArray it points into.
|
|
151
|
+
readCmapFormat4(o) {
|
|
152
|
+
const segCountX2 = this.data.readUInt16BE(o + 6);
|
|
153
|
+
const endCodes = o + 14;
|
|
154
|
+
const startCodes = endCodes + segCountX2 + 2; // +2 reservedPad
|
|
155
|
+
const idDeltas = startCodes + segCountX2;
|
|
156
|
+
const idRangeOffsets = idDeltas + segCountX2;
|
|
157
|
+
for (let i = 0; i < segCountX2 / 2; i++) {
|
|
158
|
+
const end = this.data.readUInt16BE(endCodes + i * 2);
|
|
159
|
+
const start = this.data.readUInt16BE(startCodes + i * 2);
|
|
160
|
+
const delta = this.data.readUInt16BE(idDeltas + i * 2);
|
|
161
|
+
const rangeOffset = this.data.readUInt16BE(idRangeOffsets + i * 2);
|
|
162
|
+
for (let c = start; c <= end && c !== 0xffff; c++) {
|
|
163
|
+
let glyph;
|
|
164
|
+
if (rangeOffset === 0) {
|
|
165
|
+
glyph = (c + delta) & 0xffff;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
glyph = this.data.readUInt16BE(idRangeOffsets + i * 2 + rangeOffset + (c - start) * 2);
|
|
169
|
+
if (glyph !== 0)
|
|
170
|
+
glyph = (glyph + delta) & 0xffff;
|
|
171
|
+
}
|
|
172
|
+
if (glyph !== 0)
|
|
173
|
+
this.cmap.set(c, glyph);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Format 12: sequential groups for full Unicode (incl. astral). Kept un-expanded.
|
|
178
|
+
readCmapFormat12(o) {
|
|
179
|
+
const nGroups = this.data.readUInt32BE(o + 12);
|
|
180
|
+
let p = o + 16;
|
|
181
|
+
for (let i = 0; i < nGroups; i++) {
|
|
182
|
+
this.cmapGroups.push({
|
|
183
|
+
start: this.data.readUInt32BE(p),
|
|
184
|
+
end: this.data.readUInt32BE(p + 4),
|
|
185
|
+
startGlyph: this.data.readUInt32BE(p + 8),
|
|
186
|
+
});
|
|
187
|
+
p += 12;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
exports.TTFParser = TTFParser;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function subsetTTF(data: Buffer, used: Set<number>): Buffer;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Builds a subset TrueType font that KEEPS the original glyph ids: the PDF content stream emits
|
|
3
|
+
// original gids and /W + CIDToGIDMap=Identity stay valid, so only the font program shrinks. We drop
|
|
4
|
+
// the `glyf` outlines of unused glyphs (the bulk of the file) and rebuild `loca`; every other table
|
|
5
|
+
// is copied verbatim. The used-glyph set is closed over composite-glyph components first, so a glyph
|
|
6
|
+
// built from others (e.g. "ä") keeps its parts. Fonts without `glyf`/`loca` (CFF/OTF) pass through.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.subsetTTF = subsetTTF;
|
|
9
|
+
const u32 = (n) => n >>> 0;
|
|
10
|
+
/** TrueType table checksum: sum of big-endian uint32 over the 4-byte-padded table data. */
|
|
11
|
+
function checksum(buf) {
|
|
12
|
+
let sum = 0;
|
|
13
|
+
for (let i = 0; i < buf.length; i += 4)
|
|
14
|
+
sum = u32(sum + buf.readUInt32BE(i));
|
|
15
|
+
return sum;
|
|
16
|
+
}
|
|
17
|
+
/** Right-pad to a 4-byte boundary (table data is aligned in the sfnt). */
|
|
18
|
+
function pad4(buf) {
|
|
19
|
+
const rem = buf.length % 4;
|
|
20
|
+
return rem === 0 ? buf : Buffer.concat([buf, Buffer.alloc(4 - rem)]);
|
|
21
|
+
}
|
|
22
|
+
function subsetTTF(data, used) {
|
|
23
|
+
const numTables = data.readUInt16BE(4);
|
|
24
|
+
const dir = new Map();
|
|
25
|
+
let p = 12;
|
|
26
|
+
for (let i = 0; i < numTables; i++) {
|
|
27
|
+
dir.set(data.toString("latin1", p, p + 4), {
|
|
28
|
+
offset: data.readUInt32BE(p + 8),
|
|
29
|
+
length: data.readUInt32BE(p + 12),
|
|
30
|
+
});
|
|
31
|
+
p += 16;
|
|
32
|
+
}
|
|
33
|
+
// No glyf/loca → not a TrueType-outline font we can subset this way; hand it back unchanged.
|
|
34
|
+
if (!dir.has("glyf") || !dir.has("loca") || !dir.has("head") || !dir.has("maxp"))
|
|
35
|
+
return data;
|
|
36
|
+
const headOff = dir.get("head").offset;
|
|
37
|
+
const longLoca = data.readInt16BE(headOff + 50) === 1;
|
|
38
|
+
const numGlyphs = data.readUInt16BE(dir.get("maxp").offset + 4);
|
|
39
|
+
const locaOff = dir.get("loca").offset;
|
|
40
|
+
const glyfOff = dir.get("glyf").offset;
|
|
41
|
+
// Original glyph offsets (numGlyphs + 1 entries; short loca stores half-offsets).
|
|
42
|
+
const loca = [];
|
|
43
|
+
for (let g = 0; g <= numGlyphs; g++) {
|
|
44
|
+
loca.push(longLoca ? data.readUInt32BE(locaOff + g * 4) : data.readUInt16BE(locaOff + g * 2) * 2);
|
|
45
|
+
}
|
|
46
|
+
const glyphBytes = (g) => data.subarray(glyfOff + loca[g], glyfOff + loca[g + 1]);
|
|
47
|
+
// Close the used set over composite components (and always keep .notdef = gid 0).
|
|
48
|
+
const keep = new Set([0]);
|
|
49
|
+
const stack = [...used, 0];
|
|
50
|
+
while (stack.length) {
|
|
51
|
+
const g = stack.pop();
|
|
52
|
+
if (g < 0 || g >= numGlyphs || keep.has(g))
|
|
53
|
+
continue;
|
|
54
|
+
keep.add(g);
|
|
55
|
+
const gd = glyphBytes(g);
|
|
56
|
+
if (gd.length >= 10 && gd.readInt16BE(0) < 0) {
|
|
57
|
+
// Composite glyph: walk the component records to collect the referenced gids.
|
|
58
|
+
let q = 10; // numberOfContours(2) + xMin/yMin/xMax/yMax(8)
|
|
59
|
+
for (;;) {
|
|
60
|
+
const flags = gd.readUInt16BE(q);
|
|
61
|
+
stack.push(gd.readUInt16BE(q + 2));
|
|
62
|
+
q += 4 + (flags & 0x0001 ? 4 : 2); // ARG_1_AND_2_ARE_WORDS
|
|
63
|
+
if (flags & 0x0008)
|
|
64
|
+
q += 2; // WE_HAVE_A_SCALE
|
|
65
|
+
else if (flags & 0x0040)
|
|
66
|
+
q += 4; // WE_HAVE_AN_X_AND_Y_SCALE
|
|
67
|
+
else if (flags & 0x0080)
|
|
68
|
+
q += 8; // WE_HAVE_A_TWO_BY_TWO
|
|
69
|
+
if (!(flags & 0x0020))
|
|
70
|
+
break; // MORE_COMPONENTS
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Rebuild glyf (kept glyphs only, original numbering) + a long-format loca.
|
|
75
|
+
const parts = [];
|
|
76
|
+
const newLoca = Buffer.alloc((numGlyphs + 1) * 4);
|
|
77
|
+
let off = 0;
|
|
78
|
+
for (let g = 0; g < numGlyphs; g++) {
|
|
79
|
+
newLoca.writeUInt32BE(off, g * 4);
|
|
80
|
+
if (keep.has(g)) {
|
|
81
|
+
let gd = Buffer.from(glyphBytes(g));
|
|
82
|
+
if (gd.length % 2 === 1)
|
|
83
|
+
gd = Buffer.concat([gd, Buffer.alloc(1)]); // keep offsets even
|
|
84
|
+
parts.push(gd);
|
|
85
|
+
off += gd.length;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
newLoca.writeUInt32BE(off, numGlyphs * 4);
|
|
89
|
+
const newGlyf = Buffer.concat(parts);
|
|
90
|
+
// head copy with indexToLocFormat = 1 (long); checkSumAdjustment zeroed, fixed up at the end.
|
|
91
|
+
const newHead = Buffer.from(data.subarray(headOff, headOff + dir.get("head").length));
|
|
92
|
+
newHead.writeInt16BE(1, 50);
|
|
93
|
+
newHead.writeUInt32BE(0, 8);
|
|
94
|
+
// Tables a PDF-embedded CIDFontType2 doesn't need - layout (GSUB/GPOS…), hinting hints and the
|
|
95
|
+
// signature are dead weight; dropping them is most of the size win. `post` is reduced to format 3
|
|
96
|
+
// (header only, no glyph names). cmap/name/OS-2 are kept for a still-valid standalone font.
|
|
97
|
+
const DROP = new Set([
|
|
98
|
+
"GSUB",
|
|
99
|
+
"GPOS",
|
|
100
|
+
"GDEF",
|
|
101
|
+
"BASE",
|
|
102
|
+
"JSTF",
|
|
103
|
+
"kern",
|
|
104
|
+
"DSIG",
|
|
105
|
+
"hdmx",
|
|
106
|
+
"LTSH",
|
|
107
|
+
"VDMX",
|
|
108
|
+
"PCLT",
|
|
109
|
+
"gasp",
|
|
110
|
+
]);
|
|
111
|
+
const tags = [...dir.keys()].filter((t) => !DROP.has(t)).sort();
|
|
112
|
+
const tables = tags.map((tag) => {
|
|
113
|
+
let body;
|
|
114
|
+
if (tag === "glyf")
|
|
115
|
+
body = newGlyf;
|
|
116
|
+
else if (tag === "loca")
|
|
117
|
+
body = newLoca;
|
|
118
|
+
else if (tag === "head")
|
|
119
|
+
body = newHead;
|
|
120
|
+
else if (tag === "post") {
|
|
121
|
+
// Keep the 32-byte header, force version 3.0 (drops the per-glyph name list).
|
|
122
|
+
body = Buffer.from(data.subarray(dir.get("post").offset, dir.get("post").offset + 32));
|
|
123
|
+
body.writeUInt32BE(0x00030000, 0);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const d = dir.get(tag);
|
|
127
|
+
body = data.subarray(d.offset, d.offset + d.length);
|
|
128
|
+
}
|
|
129
|
+
return { tag, body, length: body.length };
|
|
130
|
+
});
|
|
131
|
+
// sfnt: offset table + table directory + 4-byte-aligned table data.
|
|
132
|
+
const n = tables.length;
|
|
133
|
+
const maxPow2 = 1 << Math.floor(Math.log2(n));
|
|
134
|
+
const out = [];
|
|
135
|
+
const offsetTable = Buffer.alloc(12);
|
|
136
|
+
offsetTable.writeUInt32BE(data.readUInt32BE(0), 0); // sfntVersion
|
|
137
|
+
offsetTable.writeUInt16BE(n, 4);
|
|
138
|
+
offsetTable.writeUInt16BE(maxPow2 * 16, 6); // searchRange
|
|
139
|
+
offsetTable.writeUInt16BE(Math.floor(Math.log2(n)), 8); // entrySelector
|
|
140
|
+
offsetTable.writeUInt16BE(n * 16 - maxPow2 * 16, 10); // rangeShift
|
|
141
|
+
out.push(offsetTable);
|
|
142
|
+
const dirBuf = Buffer.alloc(n * 16);
|
|
143
|
+
let tableOffset = 12 + n * 16;
|
|
144
|
+
const dataBufs = [];
|
|
145
|
+
tables.forEach((t, i) => {
|
|
146
|
+
const padded = pad4(t.body);
|
|
147
|
+
dirBuf.write(t.tag, i * 16, 4, "latin1");
|
|
148
|
+
dirBuf.writeUInt32BE(checksum(padded), i * 16 + 4);
|
|
149
|
+
dirBuf.writeUInt32BE(tableOffset, i * 16 + 8);
|
|
150
|
+
dirBuf.writeUInt32BE(t.length, i * 16 + 12); // real (unpadded) length
|
|
151
|
+
tableOffset += padded.length;
|
|
152
|
+
dataBufs.push(padded);
|
|
153
|
+
});
|
|
154
|
+
out.push(dirBuf, ...dataBufs);
|
|
155
|
+
const font = Buffer.concat(out);
|
|
156
|
+
// head.checkSumAdjustment = 0xB1B0AFBA - checksum(whole font). Locate head in the assembled font.
|
|
157
|
+
const headIdx = tables.findIndex((t) => t.tag === "head");
|
|
158
|
+
const headPos = dirBuf.readUInt32BE(headIdx * 16 + 8);
|
|
159
|
+
font.writeUInt32BE(u32(0xb1b0afba - checksum(font)), headPos + 8);
|
|
160
|
+
return font;
|
|
161
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getArrayBuffer = getArrayBuffer;
|
|
4
|
+
// Windows-1252 equals Latin-1 (codepoint == byte) EXCEPT in 0x80-0x9F, where it carries
|
|
5
|
+
// printable punctuation - the Euro sign, smart quotes, dashes, ellipsis, ... - instead of
|
|
6
|
+
// C1 controls. Those glyphs sit at Unicode codepoints far outside 0x00-0xFF, so a naive
|
|
7
|
+
// low-byte cast turns "€" (U+20AC) into 0xAC ("¬"). This table maps each back to its
|
|
8
|
+
// Windows-1252 byte. (The font metrics already resolve these via the Adobe Glyph List, so
|
|
9
|
+
// only the emitted byte was wrong.)
|
|
10
|
+
const CP1252_FROM_UNICODE = {
|
|
11
|
+
0x20ac: 0x80, // € Euro
|
|
12
|
+
0x201a: 0x82, // ‚ single low-9 quote
|
|
13
|
+
0x0192: 0x83, // ƒ florin
|
|
14
|
+
0x201e: 0x84, // „ double low-9 quote
|
|
15
|
+
0x2026: 0x85, // … ellipsis
|
|
16
|
+
0x2020: 0x86, // † dagger
|
|
17
|
+
0x2021: 0x87, // ‡ double dagger
|
|
18
|
+
0x02c6: 0x88, // ˆ circumflex
|
|
19
|
+
0x2030: 0x89, // ‰ per mille
|
|
20
|
+
0x0160: 0x8a, // Š S caron
|
|
21
|
+
0x2039: 0x8b, // ‹ single left angle quote
|
|
22
|
+
0x0152: 0x8c, // Œ OE ligature
|
|
23
|
+
0x017d: 0x8e, // Ž Z caron
|
|
24
|
+
0x2018: 0x91, // ' left single quote
|
|
25
|
+
0x2019: 0x92, // ' right single quote
|
|
26
|
+
0x201c: 0x93, // " left double quote
|
|
27
|
+
0x201d: 0x94, // " right double quote
|
|
28
|
+
0x2022: 0x95, // • bullet
|
|
29
|
+
0x2013: 0x96, // – en dash
|
|
30
|
+
0x2014: 0x97, // — em dash
|
|
31
|
+
0x02dc: 0x98, // ˜ small tilde
|
|
32
|
+
0x2122: 0x99, // ™ trademark
|
|
33
|
+
0x0161: 0x9a, // š s caron
|
|
34
|
+
0x203a: 0x9b, // › single right angle quote
|
|
35
|
+
0x0153: 0x9c, // œ oe ligature
|
|
36
|
+
0x017e: 0x9e, // ž z caron
|
|
37
|
+
0x0178: 0x9f, // Ÿ Y diaeresis
|
|
38
|
+
};
|
|
39
|
+
/** Encodes a JavaScript string to a Windows-1252 byte buffer (the PDF text encoding). */
|
|
40
|
+
function getArrayBuffer(data) {
|
|
41
|
+
const u8 = new Uint8Array(data.length);
|
|
42
|
+
for (let i = 0; i < data.length; i++) {
|
|
43
|
+
const code = data.charCodeAt(i);
|
|
44
|
+
if (code <= 0xff) {
|
|
45
|
+
u8[i] = code; // Latin-1 range: codepoint is the Windows-1252 byte
|
|
46
|
+
}
|
|
47
|
+
else if (CP1252_FROM_UNICODE[code] !== undefined) {
|
|
48
|
+
u8[i] = CP1252_FROM_UNICODE[code];
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
u8[i] = 0x3f; // "?" - not representable in Windows-1252
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return u8.buffer;
|
|
55
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PDFDocumentElement } from "../elements/pdf-document-element";
|
|
2
|
+
import { PDFElement, FlexiblePDFElement, SizedPDFElement } from "../elements/pdf-element";
|
|
3
|
+
export declare class Validator {
|
|
4
|
+
static validateDocument(document: PDFDocumentElement): void;
|
|
5
|
+
static validateElement(element: PDFElement): void;
|
|
6
|
+
static validateSizedElement(element: SizedPDFElement): void;
|
|
7
|
+
static validateFlexElement(element: FlexiblePDFElement): void;
|
|
8
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Validator = void 0;
|
|
4
|
+
const pdf_document_element_1 = require("../elements/pdf-document-element");
|
|
5
|
+
const pdf_element_1 = require("../elements/pdf-element");
|
|
6
|
+
class Validator {
|
|
7
|
+
static validateDocument(document) {
|
|
8
|
+
// More validation will be added later...
|
|
9
|
+
document.getProps().children.forEach((page) => {
|
|
10
|
+
page.getProps().children.forEach((element) => {
|
|
11
|
+
if (element instanceof pdf_document_element_1.PDFDocumentElement) {
|
|
12
|
+
throw new Error("PDFDocument cannot be nested inside another element.");
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
static validateElement(element) {
|
|
18
|
+
// Structural validation
|
|
19
|
+
if (element instanceof pdf_document_element_1.PDFDocumentElement) {
|
|
20
|
+
throw new Error("PDFDocument cannot be nested inside another element.");
|
|
21
|
+
}
|
|
22
|
+
// Layout validation: geometry comes from the typed getSize(), not the props bag.
|
|
23
|
+
if (element instanceof pdf_element_1.SizedPDFElement) {
|
|
24
|
+
const { x, y, width, height } = element.getSize();
|
|
25
|
+
if (x < 0 || y < 0) {
|
|
26
|
+
throw new Error(`Element ${element.constructor.name} has invalid coordinates (x: ${x}, y: ${y})`);
|
|
27
|
+
}
|
|
28
|
+
// 0 is legitimate (a hairline divider, an empty spacer); only a NEGATIVE size is invalid.
|
|
29
|
+
if ((width !== undefined && width < 0) || (height !== undefined && height < 0)) {
|
|
30
|
+
throw new Error(`Element ${element.constructor.name} has invalid size (width: ${width}, height: ${height})`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Logical validation: Flexible and fixed elements
|
|
34
|
+
if (element instanceof pdf_element_1.FlexiblePDFElement) {
|
|
35
|
+
this.validateFlexElement(element);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
static validateSizedElement(element) {
|
|
39
|
+
const { x, y, width, height } = element.getSize();
|
|
40
|
+
if (x < 0 || y < 0) {
|
|
41
|
+
throw new Error(`Element ${element.constructor.name} has invalid coordinates (x: ${x}, y: ${y})`);
|
|
42
|
+
}
|
|
43
|
+
// A size must be set, but 0 is legitimate (a hairline divider); only NEGATIVE is invalid.
|
|
44
|
+
if (width === undefined || height === undefined || width < 0 || height < 0) {
|
|
45
|
+
throw new Error(`Element ${element.constructor.name} has invalid size (width: ${width}, height: ${height})`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
static validateFlexElement(element) {
|
|
49
|
+
// Ensure flexible elements have valid flex values
|
|
50
|
+
if (element.getFlex() <= 0) {
|
|
51
|
+
throw new Error(`Flexible element ${element.constructor.name} has invalid flex value`);
|
|
52
|
+
}
|
|
53
|
+
// Ensure a flexible element does not contain another flexible element
|
|
54
|
+
if ((0, pdf_element_1.hasChildProp)(element)) {
|
|
55
|
+
if (element.child instanceof pdf_element_1.FlexiblePDFElement) {
|
|
56
|
+
throw new Error(`Flexible element ${element.constructor.name} cannot hold another flexible element`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.Validator = Validator;
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jasy/pdf",
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"declarative",
|
|
7
|
+
"factur-x",
|
|
8
|
+
"invoice",
|
|
9
|
+
"layout",
|
|
10
|
+
"pagination",
|
|
11
|
+
"pdf",
|
|
12
|
+
"pdf-a",
|
|
13
|
+
"pdf-generation",
|
|
14
|
+
"typescript",
|
|
15
|
+
"zugferd"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Florian Heuberger",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"main": "dist/index.js",
|
|
23
|
+
"types": "dist/index.d.ts",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"jimp": "^1.6.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@jimp/types": "^1.6.0",
|
|
29
|
+
"@types/node": "^25.9.3",
|
|
30
|
+
"@vitest/coverage-v8": "^2.1.1",
|
|
31
|
+
"ncp": "^2.0.0",
|
|
32
|
+
"oxfmt": "^0.55.0",
|
|
33
|
+
"oxlint": "^1.70.0",
|
|
34
|
+
"typescript": "^5.5.4",
|
|
35
|
+
"vitest": "^2.1.1"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"copy-assets": "ncp ./src/lib/assets ./dist/assets",
|
|
39
|
+
"cli": "pnpm --filter @jasy/cli dev",
|
|
40
|
+
"cli:watch": "pnpm --filter @jasy/cli watch",
|
|
41
|
+
"play": "pnpm --filter @jasy/playground play",
|
|
42
|
+
"test": "vitest",
|
|
43
|
+
"test:coverage": "vitest --coverage",
|
|
44
|
+
"build": "tsc && pnpm run copy-assets",
|
|
45
|
+
"lint": "oxlint",
|
|
46
|
+
"lint:fix": "oxlint --fix",
|
|
47
|
+
"fmt": "oxfmt",
|
|
48
|
+
"fmt:check": "oxfmt --check"
|
|
49
|
+
}
|
|
50
|
+
}
|