@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
|
@@ -24,6 +24,20 @@ function crossOffset(align, crossExtent, childCross) {
|
|
|
24
24
|
return Math.max(0, crossExtent - childCross);
|
|
25
25
|
return 0; // start, stretch (stretch fills, so no offset)
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The order children are LAID OUT in: by `order` (lowest first, ties keeping source order), then
|
|
29
|
+
* reversed if the container asks for it. The element tree itself is never touched, so the reading order
|
|
30
|
+
* a tagged PDF exposes stays the source order - which is what CSS says too.
|
|
31
|
+
*
|
|
32
|
+
* `sort` is stable in every engine we target, so equal `order` values keep their source positions and a
|
|
33
|
+
* container where nobody sets one comes back with the identical array.
|
|
34
|
+
*/
|
|
35
|
+
function inLayoutOrder(children, reverse) {
|
|
36
|
+
const ordered = children.some((c) => c.order !== 0)
|
|
37
|
+
? [...children].sort((a, b) => a.order - b.order)
|
|
38
|
+
: children;
|
|
39
|
+
return reverse ? [...ordered].reverse() : ordered;
|
|
40
|
+
}
|
|
27
41
|
export class FlexLayoutHelper {
|
|
28
42
|
/**
|
|
29
43
|
* Lays out a flex line along `axis`, IN SOURCE ORDER, and places every child.
|
|
@@ -34,10 +48,113 @@ export class FlexLayoutHelper {
|
|
|
34
48
|
* extent occupied. Vertical with `gap 0`, `main start`, `cross stretch` reproduces the
|
|
35
49
|
* previous Column layout exactly.
|
|
36
50
|
*/
|
|
51
|
+
/**
|
|
52
|
+
* Lays out the children along `axis`, on one line or several.
|
|
53
|
+
*
|
|
54
|
+
* Without `wrap` this is the single-line engine and nothing about it changed - the call goes
|
|
55
|
+
* straight through, which is what keeps every existing document byte-identical. With `wrap` the
|
|
56
|
+
* children are split into lines first and the SAME engine runs once per line.
|
|
57
|
+
*/
|
|
37
58
|
static layout(children, axis, mainAvail, crossAvail, mainStart, crossOrigin, options, ctx) {
|
|
59
|
+
if (!options.wrap || children.length === 0 || !Number.isFinite(mainAvail)) {
|
|
60
|
+
// An unbounded main axis has no edge to wrap at, so wrapping there is meaningless, not an error.
|
|
61
|
+
return FlexLayoutHelper.layoutLine(children, axis, mainAvail, crossAvail, mainStart, crossOrigin, options, ctx);
|
|
62
|
+
}
|
|
63
|
+
return FlexLayoutHelper.layoutWrapped(children, axis, mainAvail, crossAvail, mainStart, crossOrigin, options, ctx);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The wrapping path: split into lines, lay each one out, then place the lines across the axis.
|
|
67
|
+
*
|
|
68
|
+
* Lines are laid out TWICE - once to learn how tall each is, once at its final cross position. That
|
|
69
|
+
* is the same measure-then-place shape the single-line engine already uses, and it is what
|
|
70
|
+
* `alignContent` needs: the block of lines can only be centred once its total is known.
|
|
71
|
+
*/
|
|
72
|
+
static layoutWrapped(children, axis, mainAvail, crossAvail, mainStart, crossOrigin, options, ctx) {
|
|
73
|
+
const gap = options.gap ?? 0;
|
|
74
|
+
const ordered = inLayoutOrder(children, options.reverse ?? false);
|
|
75
|
+
// Which children share a line. A child is measured at its natural main extent; a flex child
|
|
76
|
+
// contributes its BASIS, which is 0 unless it asked for one - so it never forces a break by itself.
|
|
77
|
+
// A child's natural main extent. A PERCENTAGE child has no fixed answer: its size depends on how
|
|
78
|
+
// many gaps the line ends up with, which depends on who is on it. So it is asked again for every
|
|
79
|
+
// candidate membership below rather than measured once.
|
|
80
|
+
// A plain child's extent does not depend on the line, so it is laid out ONCE and remembered.
|
|
81
|
+
// Without this each membership probe re-lays every child already on the line - O(n^2) subtree
|
|
82
|
+
// layouts, which is expensive the moment those children hold a Table or a wrapping paragraph.
|
|
83
|
+
const measured = new Map();
|
|
84
|
+
const naturalMain = (child, base) => {
|
|
85
|
+
if (child instanceof FlexiblePDFElement)
|
|
86
|
+
return child.getBasis(base);
|
|
87
|
+
const factor = child.relativeSizeFactor(axis.mainHorizontal);
|
|
88
|
+
if (factor !== undefined)
|
|
89
|
+
return base * factor;
|
|
90
|
+
const cached = measured.get(child);
|
|
91
|
+
if (cached !== undefined)
|
|
92
|
+
return cached;
|
|
93
|
+
// Measured UNCAPPED, the same way the single-line engine measures a plain child. Handing the
|
|
94
|
+
// line width in as a cap would silently squash a child wider than the line - CSS lets such a
|
|
95
|
+
// child overflow, and so does our own non-wrapping path.
|
|
96
|
+
const needsBound = child.needsBoundedMain(axis.mainHorizontal);
|
|
97
|
+
const main = axis.mainOf(child.calculateLayout(axis.measureConstraints(crossAvail, needsBound ? mainAvail : Infinity), axis.offsetAt(mainStart, crossOrigin), ctx));
|
|
98
|
+
measured.set(child, main);
|
|
99
|
+
return main;
|
|
100
|
+
};
|
|
101
|
+
/** What a line of these children would occupy, resolving every `%` against ITS gap count. */
|
|
102
|
+
const lineExtent = (members) => {
|
|
103
|
+
const gaps = Math.max(0, members.length - 1) * gap;
|
|
104
|
+
const base = Math.max(0, mainAvail - gaps);
|
|
105
|
+
return members.reduce((n, c) => n + naturalMain(c, base), 0) + gaps;
|
|
106
|
+
};
|
|
107
|
+
const lines = [[]];
|
|
108
|
+
for (const child of ordered) {
|
|
109
|
+
const current = lines[lines.length - 1];
|
|
110
|
+
// A child wider than a whole line still gets one of its own rather than an empty line before it.
|
|
111
|
+
if (current.length > 0 && lineExtent([...current, child]) > mainAvail) {
|
|
112
|
+
lines.push([child]);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
current.push(child);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Each line keeps SOURCE order internally: the ordering was applied above, and applying it again
|
|
119
|
+
// per line would reverse twice.
|
|
120
|
+
const lineOptions = { ...options, wrap: false, reverse: false };
|
|
121
|
+
const measure = lines.map((line) => FlexLayoutHelper.layoutLine(line, axis, mainAvail, Infinity, // let each line report the cross extent it actually needs
|
|
122
|
+
mainStart, crossOrigin, lineOptions, ctx));
|
|
123
|
+
const heights = measure.map((m) => m.crossUsed);
|
|
124
|
+
const totalCross = heights.reduce((n, h) => n + h, 0) + Math.max(0, lines.length - 1) * gap;
|
|
125
|
+
// `alignContent` distributes the BLOCK of lines across the axis, using the same vocabulary the main
|
|
126
|
+
// axis uses. Only meaningful in a bounded cross axis with room left over.
|
|
127
|
+
const slack = Number.isFinite(crossAvail) ? crossAvail - totalCross : 0;
|
|
128
|
+
const align = options.alignContent ?? "start";
|
|
129
|
+
let cursor = crossOrigin;
|
|
130
|
+
let between = gap;
|
|
131
|
+
if (slack > 0) {
|
|
132
|
+
if (align === "center")
|
|
133
|
+
cursor += slack / 2;
|
|
134
|
+
else if (align === "end")
|
|
135
|
+
cursor += slack;
|
|
136
|
+
else if (align === "between" && lines.length > 1)
|
|
137
|
+
between = gap + slack / (lines.length - 1);
|
|
138
|
+
else if (align === "around") {
|
|
139
|
+
const unit = slack / lines.length;
|
|
140
|
+
cursor += unit / 2;
|
|
141
|
+
between = gap + unit;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
lines.forEach((line, i) => {
|
|
145
|
+
FlexLayoutHelper.layoutLine(line, axis, mainAvail, heights[i], // the line's own extent, so `stretch` fills the line and not the container
|
|
146
|
+
mainStart, cursor, lineOptions, ctx);
|
|
147
|
+
cursor += heights[i];
|
|
148
|
+
if (i < lines.length - 1)
|
|
149
|
+
cursor += between;
|
|
150
|
+
});
|
|
151
|
+
return { mainUsed: mainAvail, crossUsed: Math.max(totalCross, cursor - crossOrigin) };
|
|
152
|
+
}
|
|
153
|
+
static layoutLine(children, axis, mainAvail, crossAvail, mainStart, crossOrigin, options, ctx) {
|
|
38
154
|
const gap = options.gap ?? 0;
|
|
39
155
|
const main = options.main ?? "start";
|
|
40
156
|
const cross = options.cross ?? "stretch";
|
|
157
|
+
children = inLayoutOrder(children, options.reverse ?? false);
|
|
41
158
|
const count = children.length;
|
|
42
159
|
const totalGap = Math.max(0, count - 1) * gap;
|
|
43
160
|
// The main extent a percentage child resolves against: the line's own extent minus the gaps
|
|
@@ -48,7 +165,13 @@ export class FlexLayoutHelper {
|
|
|
48
165
|
// The main cap to hand a child: `percentBase` for a percentage child (so its fraction resolves) and
|
|
49
166
|
// for a child that cannot lay itself out without a bound (a nested stack holding an `Expanded`/
|
|
50
167
|
// `Spacer`); unbounded for everyone else, who keep their natural size and never fill the line.
|
|
168
|
+
// Filled by the shrink pass below; read by `mainCapFor` in pass 2. Declared here so the closure
|
|
169
|
+
// can see it - it is empty for every document that does not ask for shrinking.
|
|
170
|
+
const shrunkCap = new Map();
|
|
51
171
|
const mainCapFor = (child) => {
|
|
172
|
+
const target = shrunkCap.get(child);
|
|
173
|
+
if (target !== undefined)
|
|
174
|
+
return target;
|
|
52
175
|
if (percentBase === Infinity)
|
|
53
176
|
return Infinity;
|
|
54
177
|
const needsBound = child.relativeSizeFactor(axis.mainHorizontal) !== undefined ||
|
|
@@ -56,13 +179,17 @@ export class FlexLayoutHelper {
|
|
|
56
179
|
return needsBound ? percentBase : Infinity;
|
|
57
180
|
};
|
|
58
181
|
// Pass 1: measure the fixed children (main extent + cross size) and total the flex.
|
|
182
|
+
// A flex child's BASIS is reserved here too: it is main extent the leftover no longer contains,
|
|
183
|
+
// exactly like a fixed child's size. With the default basis of 0 this line adds nothing.
|
|
59
184
|
let fixedMain = 0;
|
|
60
185
|
let totalFlex = 0;
|
|
186
|
+
let totalBasis = 0;
|
|
61
187
|
let crossUsed = 0;
|
|
62
188
|
const fixedSize = new Map();
|
|
63
189
|
for (const child of children) {
|
|
64
190
|
if (child instanceof FlexiblePDFElement) {
|
|
65
191
|
totalFlex += child.getFlex();
|
|
192
|
+
totalBasis += child.getBasis(percentBase);
|
|
66
193
|
}
|
|
67
194
|
else {
|
|
68
195
|
const size = child.calculateLayout(axis.measureConstraints(crossAvail, mainCapFor(child)), axis.offsetAt(mainStart, crossOrigin), ctx);
|
|
@@ -71,7 +198,34 @@ export class FlexLayoutHelper {
|
|
|
71
198
|
fixedSize.set(child, size);
|
|
72
199
|
}
|
|
73
200
|
}
|
|
74
|
-
|
|
201
|
+
// Shrinking: the line overflows and some children said they would give space back. CSS weights a
|
|
202
|
+
// shrinker's share by `shrink x its own size`, so a big item yields more than a small one - which is
|
|
203
|
+
// why this cannot reuse the grow maths above. Nobody shrinks by default, so a document that never
|
|
204
|
+
// asks for it never enters this block.
|
|
205
|
+
const shrinkers = children.filter((c) => c.flexShrink > 0 && !(c instanceof FlexiblePDFElement));
|
|
206
|
+
const overflow = fixedMain + totalBasis + totalGap - mainAvail;
|
|
207
|
+
if (shrinkers.length > 0 && Number.isFinite(mainAvail) && overflow > 0) {
|
|
208
|
+
const weight = (c) => c.flexShrink * axis.mainOf(fixedSize.get(c));
|
|
209
|
+
const totalWeight = shrinkers.reduce((n, c) => n + weight(c), 0);
|
|
210
|
+
if (totalWeight > 0) {
|
|
211
|
+
for (const child of shrinkers) {
|
|
212
|
+
const natural = axis.mainOf(fixedSize.get(child));
|
|
213
|
+
// Never below zero: when the GAPS alone outgrow the line, a proportional share asks for more
|
|
214
|
+
// than a child has. No test can tell this clamp apart from its absence - `constrainWidth`
|
|
215
|
+
// floors at 0 further down either way - but handing out a negative constraint is nonsense,
|
|
216
|
+
// and the next reader should not have to work out that it happens to be harmless.
|
|
217
|
+
const target = Math.max(0, natural - (weight(child) / totalWeight) * overflow);
|
|
218
|
+
shrunkCap.set(child, target);
|
|
219
|
+
// Re-measure NOW, not in pass 2: a narrower child may wrap to more lines, and the line's
|
|
220
|
+
// cross extent is settled just below.
|
|
221
|
+
const size = child.calculateLayout(axis.measureConstraints(crossAvail, target), axis.offsetAt(mainStart, crossOrigin), ctx);
|
|
222
|
+
fixedMain += axis.mainOf(size) - natural;
|
|
223
|
+
crossUsed = Math.max(crossUsed, axis.crossOf(size));
|
|
224
|
+
fixedSize.set(child, size);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const leftover = mainAvail - fixedMain - totalBasis - totalGap;
|
|
75
229
|
// A flex child on an UNBOUNDED main axis has no leftover space to claim. It must collapse to zero,
|
|
76
230
|
// never to `Infinity`: an infinite extent would become the offset of every following sibling and get
|
|
77
231
|
// written into the content stream verbatim, silently corrupting the page from that point on.
|
|
@@ -97,7 +251,7 @@ export class FlexLayoutHelper {
|
|
|
97
251
|
if (totalFlex > 0) {
|
|
98
252
|
for (const child of children) {
|
|
99
253
|
if (child instanceof FlexiblePDFElement) {
|
|
100
|
-
const mainExtent = (child.getFlex() / totalFlex) * remaining;
|
|
254
|
+
const mainExtent = child.getBasis(percentBase) + (child.getFlex() / totalFlex) * remaining;
|
|
101
255
|
const size = child.calculateLayout(axis.flexConstraints(mainExtent, crossAvail), axis.offsetAt(mainStart, crossOrigin), ctx);
|
|
102
256
|
crossUsed = Math.max(crossUsed, axis.crossOf(size));
|
|
103
257
|
}
|
|
@@ -108,20 +262,28 @@ export class FlexLayoutHelper {
|
|
|
108
262
|
// `stretch` caps a child's cross to `crossExtent` (not `crossAvail`) so siblings end up
|
|
109
263
|
// equal across the axis. Bounded lines have crossExtent == crossAvail (byte-identical);
|
|
110
264
|
// only an unbounded line (a shrink-wrap Row) now equalises instead of staying natural.
|
|
111
|
-
|
|
112
|
-
|
|
265
|
+
// A child may override the container with its own `alignSelf` (CSS), so the decision is per child.
|
|
266
|
+
const alignFor = (child) => child.alignSelf ?? cross;
|
|
267
|
+
// Pass 2: place each child at the running main position, offset across by its alignment.
|
|
113
268
|
let mainPos = mainStart + leadingSpace;
|
|
114
269
|
let placedCross = 0;
|
|
115
270
|
children.forEach((child, index) => {
|
|
271
|
+
const align = alignFor(child);
|
|
272
|
+
const stretch = align === "stretch";
|
|
116
273
|
let mainExtent;
|
|
117
274
|
if (child instanceof FlexiblePDFElement) {
|
|
118
|
-
mainExtent = (child.getFlex() / totalFlex) * remaining;
|
|
119
|
-
|
|
275
|
+
mainExtent = child.getBasis(percentBase) + (child.getFlex() / totalFlex) * remaining;
|
|
276
|
+
// A flex child fills the MAIN axis, and its cross size is only known after layout - there is
|
|
277
|
+
// nothing to align against. So alignSelf is a no-op here, and that has to hold for the CROSS
|
|
278
|
+
// CONSTRAINT too: reading the per-child alignment would hand an `alignSelf: "start"` flex child
|
|
279
|
+
// an unbounded cross axis on a shrink-wrapping line, where the container's own rule gives it
|
|
280
|
+
// the line's extent. A no-op that changes the constraints is not a no-op.
|
|
281
|
+
const size = child.calculateLayout(axis.flexConstraints(mainExtent, cross === "stretch" ? crossExtent : crossAvail), axis.offsetAt(mainPos, crossOrigin), ctx);
|
|
120
282
|
placedCross = Math.max(placedCross, axis.crossOf(size));
|
|
121
283
|
}
|
|
122
284
|
else {
|
|
123
285
|
const childCross = axis.crossOf(fixedSize.get(child));
|
|
124
|
-
const size = child.calculateLayout(axis.measureConstraints(stretch ? crossExtent : crossAvail, mainCapFor(child)), axis.offsetAt(mainPos, crossOrigin + crossOffset(
|
|
286
|
+
const size = child.calculateLayout(axis.measureConstraints(stretch ? crossExtent : crossAvail, mainCapFor(child)), axis.offsetAt(mainPos, crossOrigin + crossOffset(align, crossExtent, childCross)), ctx);
|
|
125
287
|
mainExtent = axis.mainOf(size);
|
|
126
288
|
placedCross = Math.max(placedCross, axis.crossOf(size));
|
|
127
289
|
}
|
|
@@ -23,4 +23,9 @@ export interface FontMetrics {
|
|
|
23
23
|
/** Per-adjacent-pair kerning of `text`, in em/1000 (negative tightens); length `codePoints - 1`,
|
|
24
24
|
* zero next to a space. Only meaningful when `kerningEnabled`. */
|
|
25
25
|
getKernPairs(text: string, fontFamily: string, fontStyle: FontStyle): number[];
|
|
26
|
+
/** Whether this family can draw the code point at all - what picks a face out of a fallback stack. */
|
|
27
|
+
hasGlyph(codePoint: number, fontFamily: string, fontStyle: FontStyle): boolean;
|
|
28
|
+
/** How many glyphs the run will DRAW, when a ligature made that differ from its code-point count.
|
|
29
|
+
* Absent or `undefined` means "the same", which is every Latin run. */
|
|
30
|
+
shapedGlyphCount?(text: string, fontFamily?: string, fontStyle?: FontStyle): number | undefined;
|
|
26
31
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare class GsubTable {
|
|
2
|
+
private data;
|
|
3
|
+
private base;
|
|
4
|
+
private scriptList;
|
|
5
|
+
private featureList;
|
|
6
|
+
private lookupList;
|
|
7
|
+
private lookupCache;
|
|
8
|
+
private coverageCache;
|
|
9
|
+
constructor(data: Uint8Array, base: number);
|
|
10
|
+
/** Whether the font carries this script at all - `arab` is the question the shaper asks. */
|
|
11
|
+
hasScript(script: string): boolean;
|
|
12
|
+
/** A feature's lookup indices, under the script's DEFAULT language system - Arabic shaping features
|
|
13
|
+
* are script-wide, so a language-specific override would only add typographic niceties. */
|
|
14
|
+
lookups(script: string, feature: string): number[];
|
|
15
|
+
/** Type 1: substitute one glyph. `null` when no subtable covers it - the feature does not apply. */
|
|
16
|
+
substituteSingle(lookupIndex: number, glyph: number): number | null;
|
|
17
|
+
/**
|
|
18
|
+
* Type 4: collapse a run of glyphs into one, starting at `at`. Longest match wins, as the spec
|
|
19
|
+
* requires - a three-glyph ligature beats the two-glyph one that starts the same way.
|
|
20
|
+
*/
|
|
21
|
+
substituteLigature(lookupIndex: number, glyphs: readonly number[], at: number): {
|
|
22
|
+
glyph: number;
|
|
23
|
+
consumed: number;
|
|
24
|
+
} | null;
|
|
25
|
+
/** The type of a lookup, for reporting what a font needs that we do not do. */
|
|
26
|
+
lookupType(lookupIndex: number): number | undefined;
|
|
27
|
+
private scriptOffset;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a lookup, unwrapping type 7 (extension) - which exists only because the 16-bit offsets
|
|
30
|
+
* elsewhere cannot reach far enough in a big font. Callers never learn it was wrapped.
|
|
31
|
+
*/
|
|
32
|
+
private lookup;
|
|
33
|
+
/** Coverage table -> glyph id to its INDEX in the table, which is how subtables address glyphs. */
|
|
34
|
+
private coverage;
|
|
35
|
+
private cacheCoverage;
|
|
36
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { u16, u32, i16, latin1FromBytes } from "./bytes.js";
|
|
2
|
+
const EXTENSION = 7;
|
|
3
|
+
/** A coverage table may not expand past this: a glyph id is a uint16, so no honest one can. */
|
|
4
|
+
const MAX_COVERAGE_ENTRIES = 65536;
|
|
5
|
+
export class GsubTable {
|
|
6
|
+
constructor(data, base) {
|
|
7
|
+
this.data = data;
|
|
8
|
+
this.base = base;
|
|
9
|
+
this.lookupCache = new Map();
|
|
10
|
+
this.coverageCache = new Map();
|
|
11
|
+
this.scriptList = base + u16(data, base + 4);
|
|
12
|
+
this.featureList = base + u16(data, base + 6);
|
|
13
|
+
this.lookupList = base + u16(data, base + 8);
|
|
14
|
+
}
|
|
15
|
+
/** Whether the font carries this script at all - `arab` is the question the shaper asks. */
|
|
16
|
+
hasScript(script) {
|
|
17
|
+
return this.scriptOffset(script) !== undefined;
|
|
18
|
+
}
|
|
19
|
+
/** A feature's lookup indices, under the script's DEFAULT language system - Arabic shaping features
|
|
20
|
+
* are script-wide, so a language-specific override would only add typographic niceties. */
|
|
21
|
+
lookups(script, feature) {
|
|
22
|
+
const scriptOff = this.scriptOffset(script);
|
|
23
|
+
if (scriptOff === undefined)
|
|
24
|
+
return [];
|
|
25
|
+
// The default LangSys is optional; fall back to the first named one - a font declaring only
|
|
26
|
+
// `ARA ` still shapes Arabic.
|
|
27
|
+
const defaultOff = u16(this.data, scriptOff);
|
|
28
|
+
const langSys = defaultOff !== 0
|
|
29
|
+
? scriptOff + defaultOff
|
|
30
|
+
: u16(this.data, scriptOff + 2) > 0
|
|
31
|
+
? // The first LangSysRecord is at +4 and is tag(4) + offset(2), so its offset is at +8.
|
|
32
|
+
scriptOff + u16(this.data, scriptOff + 8)
|
|
33
|
+
: undefined;
|
|
34
|
+
if (langSys === undefined)
|
|
35
|
+
return [];
|
|
36
|
+
const out = [];
|
|
37
|
+
const count = u16(this.data, langSys + 4);
|
|
38
|
+
for (let i = 0; i < count; i++) {
|
|
39
|
+
const featureIndex = u16(this.data, langSys + 6 + i * 2);
|
|
40
|
+
const record = this.featureList + 2 + featureIndex * 6;
|
|
41
|
+
if (latin1FromBytes(this.data.subarray(record, record + 4)) !== feature)
|
|
42
|
+
continue;
|
|
43
|
+
const featureOff = this.featureList + u16(this.data, record + 4);
|
|
44
|
+
const lookupCount = u16(this.data, featureOff + 2);
|
|
45
|
+
for (let j = 0; j < lookupCount; j++)
|
|
46
|
+
out.push(u16(this.data, featureOff + 4 + j * 2));
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/** Type 1: substitute one glyph. `null` when no subtable covers it - the feature does not apply. */
|
|
51
|
+
substituteSingle(lookupIndex, glyph) {
|
|
52
|
+
const lookup = this.lookup(lookupIndex);
|
|
53
|
+
if (!lookup || lookup.type !== 1)
|
|
54
|
+
return null;
|
|
55
|
+
for (const sub of lookup.subtables) {
|
|
56
|
+
const coverage = this.coverage(sub + u16(this.data, sub + 2));
|
|
57
|
+
const index = coverage.get(glyph);
|
|
58
|
+
if (index === undefined)
|
|
59
|
+
continue;
|
|
60
|
+
// Format 1 shifts every covered glyph by one delta; format 2 lists a replacement per glyph.
|
|
61
|
+
return u16(this.data, sub) === 1
|
|
62
|
+
? (glyph + i16(this.data, sub + 4)) & 0xffff
|
|
63
|
+
: u16(this.data, sub + 6 + index * 2);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Type 4: collapse a run of glyphs into one, starting at `at`. Longest match wins, as the spec
|
|
69
|
+
* requires - a three-glyph ligature beats the two-glyph one that starts the same way.
|
|
70
|
+
*/
|
|
71
|
+
substituteLigature(lookupIndex, glyphs, at) {
|
|
72
|
+
const lookup = this.lookup(lookupIndex);
|
|
73
|
+
if (!lookup || lookup.type !== 4)
|
|
74
|
+
return null;
|
|
75
|
+
let best = null;
|
|
76
|
+
for (const sub of lookup.subtables) {
|
|
77
|
+
const coverage = this.coverage(sub + u16(this.data, sub + 2));
|
|
78
|
+
const setIndex = coverage.get(glyphs[at]);
|
|
79
|
+
if (setIndex === undefined)
|
|
80
|
+
continue;
|
|
81
|
+
const set = sub + u16(this.data, sub + 6 + setIndex * 2);
|
|
82
|
+
const ligatureCount = u16(this.data, set);
|
|
83
|
+
for (let i = 0; i < ligatureCount; i++) {
|
|
84
|
+
const lig = set + u16(this.data, set + 2 + i * 2);
|
|
85
|
+
const components = u16(this.data, lig + 2); // includes the glyph coverage already matched
|
|
86
|
+
if (at + components > glyphs.length)
|
|
87
|
+
continue;
|
|
88
|
+
let matches = true;
|
|
89
|
+
for (let c = 1; c < components && matches; c++) {
|
|
90
|
+
matches = glyphs[at + c] === u16(this.data, lig + 2 + c * 2);
|
|
91
|
+
}
|
|
92
|
+
if (matches && (best === null || components > best.consumed)) {
|
|
93
|
+
best = { glyph: u16(this.data, lig), consumed: components };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return best;
|
|
98
|
+
}
|
|
99
|
+
/** The type of a lookup, for reporting what a font needs that we do not do. */
|
|
100
|
+
lookupType(lookupIndex) {
|
|
101
|
+
return this.lookup(lookupIndex)?.type;
|
|
102
|
+
}
|
|
103
|
+
scriptOffset(script) {
|
|
104
|
+
const count = u16(this.data, this.scriptList);
|
|
105
|
+
for (let i = 0; i < count; i++) {
|
|
106
|
+
const record = this.scriptList + 2 + i * 6;
|
|
107
|
+
if (latin1FromBytes(this.data.subarray(record, record + 4)) === script) {
|
|
108
|
+
return this.scriptList + u16(this.data, record + 4);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolve a lookup, unwrapping type 7 (extension) - which exists only because the 16-bit offsets
|
|
115
|
+
* elsewhere cannot reach far enough in a big font. Callers never learn it was wrapped.
|
|
116
|
+
*/
|
|
117
|
+
lookup(index) {
|
|
118
|
+
const cached = this.lookupCache.get(index);
|
|
119
|
+
if (cached !== undefined)
|
|
120
|
+
return cached;
|
|
121
|
+
let result = null;
|
|
122
|
+
if (index < u16(this.data, this.lookupList)) {
|
|
123
|
+
const off = this.lookupList + u16(this.data, this.lookupList + 2 + index * 2);
|
|
124
|
+
const type = u16(this.data, off);
|
|
125
|
+
const count = u16(this.data, off + 4);
|
|
126
|
+
const subtables = [];
|
|
127
|
+
let realType = type;
|
|
128
|
+
for (let i = 0; i < count; i++) {
|
|
129
|
+
const sub = off + u16(this.data, off + 6 + i * 2);
|
|
130
|
+
if (type === EXTENSION) {
|
|
131
|
+
realType = u16(this.data, sub + 2);
|
|
132
|
+
subtables.push(sub + u32(this.data, sub + 4));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
subtables.push(sub);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
result = { type: realType, subtables };
|
|
139
|
+
}
|
|
140
|
+
this.lookupCache.set(index, result);
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
/** Coverage table -> glyph id to its INDEX in the table, which is how subtables address glyphs. */
|
|
144
|
+
coverage(off) {
|
|
145
|
+
const cached = this.coverageCache.get(off);
|
|
146
|
+
if (cached)
|
|
147
|
+
return cached;
|
|
148
|
+
const map = new Map();
|
|
149
|
+
const count = u16(this.data, off + 2);
|
|
150
|
+
if (u16(this.data, off) === 1) {
|
|
151
|
+
for (let i = 0; i < count; i++)
|
|
152
|
+
map.set(u16(this.data, off + 4 + i * 2), i);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// Format 2 is RANGES, and a malformed file can claim far more glyphs than any font holds. Such
|
|
156
|
+
// a table is refused whole rather than expanded - the feature then simply does not apply.
|
|
157
|
+
let total = 0;
|
|
158
|
+
for (let i = 0; i < count; i++) {
|
|
159
|
+
const record = off + 4 + i * 6;
|
|
160
|
+
const start = u16(this.data, record);
|
|
161
|
+
const end = u16(this.data, record + 2);
|
|
162
|
+
if (end < start)
|
|
163
|
+
continue; // an inverted range covers nothing
|
|
164
|
+
total += end - start + 1;
|
|
165
|
+
if (total > MAX_COVERAGE_ENTRIES)
|
|
166
|
+
return this.cacheCoverage(off, new Map());
|
|
167
|
+
const startIndex = u16(this.data, record + 4);
|
|
168
|
+
for (let g = start; g <= end; g++)
|
|
169
|
+
map.set(g, startIndex + (g - start));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return this.cacheCoverage(off, map);
|
|
173
|
+
}
|
|
174
|
+
cacheCoverage(off, map) {
|
|
175
|
+
this.coverageCache.set(off, map);
|
|
176
|
+
return map;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inflating with a ceiling, so a hostile or mistaken input cannot exhaust memory.
|
|
3
|
+
*
|
|
4
|
+
* Used by BOTH sides: the reader inflating a PDF stream, and the font path unpacking a WOFF table.
|
|
5
|
+
* It lives in `utils/` for exactly that reason - the generate path must not import from `edit/`.
|
|
6
|
+
*
|
|
7
|
+
* fflate's `unzlibSync(data, { out })` is NOT usable for this: it truncates silently instead of failing.
|
|
8
|
+
* Hence the streamed form - input in slices, so the total can be checked while it grows.
|
|
9
|
+
*/
|
|
10
|
+
/** Thrown when a stream expands past the ceiling; distinct from a stream that simply will not decode. */
|
|
11
|
+
export declare class PdfStreamTooLargeError extends Error {
|
|
12
|
+
readonly limit: number;
|
|
13
|
+
constructor(limit: number, message: string);
|
|
14
|
+
}
|
|
15
|
+
export declare const DEFAULT_MAX_STREAM_SIZE: number;
|
|
16
|
+
export declare function inflateBounded(data: Uint8Array, limit: number): Uint8Array;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Unzlib, unzlibSync } from "fflate";
|
|
2
|
+
/**
|
|
3
|
+
* Inflating with a ceiling, so a hostile or mistaken input cannot exhaust memory.
|
|
4
|
+
*
|
|
5
|
+
* Used by BOTH sides: the reader inflating a PDF stream, and the font path unpacking a WOFF table.
|
|
6
|
+
* It lives in `utils/` for exactly that reason - the generate path must not import from `edit/`.
|
|
7
|
+
*
|
|
8
|
+
* fflate's `unzlibSync(data, { out })` is NOT usable for this: it truncates silently instead of failing.
|
|
9
|
+
* Hence the streamed form - input in slices, so the total can be checked while it grows.
|
|
10
|
+
*/
|
|
11
|
+
/** Thrown when a stream expands past the ceiling; distinct from a stream that simply will not decode. */
|
|
12
|
+
export class PdfStreamTooLargeError extends Error {
|
|
13
|
+
constructor(limit, message) {
|
|
14
|
+
super(`@jasy/pdf: ${message}`);
|
|
15
|
+
this.limit = limit;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export const DEFAULT_MAX_STREAM_SIZE = 64 * 1024 * 1024;
|
|
19
|
+
/** Only the granularity of the check: fflate buffers internally, so the abort lands one block past. */
|
|
20
|
+
const INPUT_SLICE = 16 * 1024;
|
|
21
|
+
/** DEFLATE's maximum expansion (a 258-byte match in its shortest encoding). A format property. */
|
|
22
|
+
const MAX_DEFLATE_RATIO = 1032;
|
|
23
|
+
export function inflateBounded(data, limit) {
|
|
24
|
+
// Too small to reach the ceiling even at maximum compression, so checking as it grows is pointless.
|
|
25
|
+
// Nearly every stream in a real PDF lands here, which is why the guard costs nothing.
|
|
26
|
+
if (data.length <= limit / MAX_DEFLATE_RATIO)
|
|
27
|
+
return unzlibSync(data);
|
|
28
|
+
const parts = [];
|
|
29
|
+
let total = 0;
|
|
30
|
+
const inflater = new Unzlib();
|
|
31
|
+
inflater.ondata = (chunk) => {
|
|
32
|
+
total += chunk.length;
|
|
33
|
+
if (total > limit) {
|
|
34
|
+
throw new PdfStreamTooLargeError(limit, `a stream in this PDF expands past the ${limit} byte limit for a single stream; if the file is ` +
|
|
35
|
+
`genuinely that large, raise it with { maxStreamSize }`);
|
|
36
|
+
}
|
|
37
|
+
parts.push(chunk);
|
|
38
|
+
};
|
|
39
|
+
for (let at = 0; at < data.length; at += INPUT_SLICE) {
|
|
40
|
+
const end = Math.min(at + INPUT_SLICE, data.length);
|
|
41
|
+
inflater.push(data.subarray(at, end), end >= data.length);
|
|
42
|
+
}
|
|
43
|
+
if (parts.length === 1)
|
|
44
|
+
return parts[0];
|
|
45
|
+
const out = new Uint8Array(total);
|
|
46
|
+
let at = 0;
|
|
47
|
+
for (const part of parts) {
|
|
48
|
+
out.set(part, at);
|
|
49
|
+
at += part.length;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OverflowPolicy } from "../layout/fragmentation.js";
|
|
2
2
|
import { TTFParser } from "./ttf-parser.js";
|
|
3
|
+
import { type ShapedGlyph } from "../text/shape.js";
|
|
3
4
|
import type { SecurityHandler } from "../crypto/security-handler.js";
|
|
4
5
|
import type { Gradient } from "../ir/display-list.js";
|
|
5
6
|
import type { FontVerticals } from "../text/line-metrics.js";
|
|
@@ -39,6 +40,8 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
39
40
|
private verticalsCache;
|
|
40
41
|
private decorationCache;
|
|
41
42
|
private customFontEmit;
|
|
43
|
+
private shapeCache;
|
|
44
|
+
private shapedOrigins;
|
|
42
45
|
private attachments;
|
|
43
46
|
private xmpMetadata?;
|
|
44
47
|
private outputIntent?;
|
|
@@ -133,8 +136,26 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
133
136
|
url: string;
|
|
134
137
|
format: string;
|
|
135
138
|
} | undefined;
|
|
139
|
+
/**
|
|
140
|
+
* Whether a family can draw this code point at all. An embedded face answers from its `cmap`; a
|
|
141
|
+
* standard-14 one can only draw what Windows-1252 encodes, which is what its `/Widths` array covers.
|
|
142
|
+
* Used to pick a family out of a fallback stack, per code point.
|
|
143
|
+
*/
|
|
144
|
+
hasGlyph(codePoint: number, fontFamily: string, fontStyle?: FontStyle): boolean;
|
|
136
145
|
private colorRenders;
|
|
137
146
|
getCustomFontResource(name: string, style?: FontStyle): FontIndexes | undefined;
|
|
147
|
+
/** How many glyphs a run draws - its code-point count unless a ligature merged some. `letterSpacing`
|
|
148
|
+
* is per glyph (`Tc`), so this is what it multiplies. */
|
|
149
|
+
shapedGlyphCount(text: string, fontFamily?: string, fontStyle?: FontStyle): number | undefined;
|
|
150
|
+
/**
|
|
151
|
+
* The shaped glyphs of a run, or undefined when nothing needed shaping. The ONE place shaping is
|
|
152
|
+
* decided, so measuring and drawing cannot disagree. Memoised: a paragraph is measured several
|
|
153
|
+
* times (layout, pagination, drawing).
|
|
154
|
+
*/
|
|
155
|
+
shapeText(text: string, fontFamily?: string, fontStyle?: FontStyle): ShapedGlyph[] | undefined;
|
|
156
|
+
/** Hex Identity-H for glyphs the producer already resolved (a shaped run), which cannot be given as
|
|
157
|
+
* text. Recording each keeps it in the subset. */
|
|
158
|
+
registerGlyphs(name: string, glyphs: readonly number[], style?: FontStyle): string;
|
|
138
159
|
encodeCustomText(name: string, text: string, style?: FontStyle): string;
|
|
139
160
|
private buildFontFile2;
|
|
140
161
|
private buildFontDescriptor;
|