@jasy/pdf 1.0.0-alpha.7 → 1.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -10
- package/dist/api/content.d.ts +5 -4
- package/dist/api/content.js +13 -7
- package/dist/api/descriptor.js +23 -0
- package/dist/api/dimension.d.ts +56 -0
- package/dist/api/dimension.js +27 -0
- package/dist/api/font-url.d.ts +28 -0
- package/dist/api/font-url.js +134 -0
- package/dist/api/forms.d.ts +347 -0
- package/dist/api/forms.js +189 -0
- package/dist/api/gradient.d.ts +53 -0
- package/dist/api/gradient.js +97 -0
- package/dist/api/index.d.ts +3 -0
- package/dist/api/index.js +3 -0
- package/dist/api/insets.d.ts +19 -12
- package/dist/api/insets.js +18 -7
- package/dist/api/layout.d.ts +20 -6
- package/dist/api/layout.js +31 -6
- package/dist/api/page-builder.d.ts +8 -0
- package/dist/api/page-builder.js +12 -0
- package/dist/api/structure.d.ts +11 -0
- package/dist/api/structure.js +17 -4
- package/dist/api/text.d.ts +42 -6
- package/dist/api/text.js +29 -5
- package/dist/crypto/legacy-handler.d.ts +43 -0
- package/dist/crypto/legacy-handler.js +145 -0
- package/dist/crypto/security-handler.d.ts +16 -0
- package/dist/crypto/security-handler.js +26 -0
- package/dist/edit/acroform-reader.d.ts +70 -0
- package/dist/edit/acroform-reader.js +126 -0
- package/dist/edit/appearance.d.ts +47 -0
- package/dist/edit/appearance.js +171 -0
- package/dist/edit/document.d.ts +145 -0
- package/dist/edit/document.js +642 -0
- package/dist/edit/fill.d.ts +65 -0
- package/dist/edit/fill.js +424 -0
- package/dist/edit/flatten.d.ts +54 -0
- package/dist/edit/flatten.js +462 -0
- package/dist/edit/index.d.ts +20 -0
- package/dist/edit/index.js +18 -0
- package/dist/edit/lexer.d.ts +30 -0
- package/dist/edit/lexer.js +285 -0
- package/dist/edit/objects.d.ts +68 -0
- package/dist/edit/objects.js +83 -0
- package/dist/edit/writer.d.ts +60 -0
- package/dist/edit/writer.js +283 -0
- package/dist/elements/container-element.d.ts +21 -1
- package/dist/elements/container-element.js +25 -10
- package/dist/elements/forms/checkbox-element.d.ts +51 -0
- package/dist/elements/forms/checkbox-element.js +48 -0
- package/dist/elements/forms/choice-element.d.ts +53 -0
- package/dist/elements/forms/choice-element.js +59 -0
- package/dist/elements/forms/push-button-element.d.ts +56 -0
- package/dist/elements/forms/push-button-element.js +49 -0
- package/dist/elements/forms/radio-element.d.ts +52 -0
- package/dist/elements/forms/radio-element.js +49 -0
- package/dist/elements/forms/signature-element.d.ts +54 -0
- package/dist/elements/forms/signature-element.js +47 -0
- package/dist/elements/forms/text-field-element.d.ts +61 -0
- package/dist/elements/forms/text-field-element.js +54 -0
- package/dist/elements/image-element.d.ts +18 -4
- package/dist/elements/image-element.js +27 -12
- package/dist/elements/index.d.ts +6 -0
- package/dist/elements/index.js +6 -0
- package/dist/elements/layout/expanded-element.d.ts +3 -1
- package/dist/elements/layout/expanded-element.js +12 -3
- package/dist/elements/layout/padding-element.d.ts +4 -2
- package/dist/elements/layout/padding-element.js +4 -2
- package/dist/elements/layout/page-builder-element.js +2 -0
- package/dist/elements/pdf-element.d.ts +59 -1
- package/dist/elements/pdf-element.js +62 -1
- package/dist/elements/rectangle-element.d.ts +20 -6
- package/dist/elements/rectangle-element.js +25 -12
- package/dist/elements/row-element.d.ts +24 -1
- package/dist/elements/row-element.js +25 -10
- package/dist/elements/text-element.d.ts +60 -1
- package/dist/elements/text-element.js +132 -33
- package/dist/forms/acroform.d.ts +63 -0
- package/dist/forms/acroform.js +427 -0
- package/dist/forms/appearance.d.ts +58 -0
- package/dist/forms/appearance.js +204 -0
- package/dist/forms/field.d.ts +164 -0
- package/dist/forms/field.js +1 -0
- package/dist/forms/pdf.d.ts +19 -0
- package/dist/forms/pdf.js +28 -0
- package/dist/ir/display-list.d.ts +40 -7
- package/dist/ir/display-list.js +8 -1
- package/dist/layout/alignment.d.ts +10 -0
- package/dist/layout/alignment.js +6 -0
- package/dist/layout/box-constraints.d.ts +53 -0
- package/dist/layout/box-constraints.js +72 -0
- package/dist/layout/insets.d.ts +28 -0
- package/dist/layout/insets.js +20 -0
- package/dist/renderer/form-field-renderer.d.ts +20 -0
- package/dist/renderer/form-field-renderer.js +12 -0
- package/dist/renderer/image-renderer.js +2 -1
- package/dist/renderer/page-renderer.js +8 -2
- package/dist/renderer/pdf-backend.d.ts +6 -0
- package/dist/renderer/pdf-backend.js +86 -31
- package/dist/renderer/pdf-document-renderer.js +8 -1
- package/dist/renderer/pdf-renderer.js +19 -1
- package/dist/renderer/rectangle-renderer.js +8 -2
- package/dist/renderer/text-renderer.d.ts +2 -2
- package/dist/renderer/text-renderer.js +150 -42
- package/dist/text/advance.js +5 -2
- package/dist/text/arabic.d.ts +20 -0
- package/dist/text/arabic.js +101 -0
- package/dist/text/bidi.d.ts +33 -0
- package/dist/text/bidi.js +94 -0
- package/dist/text/font-fallback.d.ts +25 -0
- package/dist/text/font-fallback.js +38 -0
- package/dist/text/line-breaker.d.ts +35 -1
- package/dist/text/line-breaker.js +76 -16
- package/dist/text/orphans-widows.d.ts +27 -0
- package/dist/text/orphans-widows.js +53 -0
- package/dist/text/shape.d.ts +29 -0
- package/dist/text/shape.js +67 -0
- package/dist/text/text-style.d.ts +25 -0
- package/dist/text/text-style.js +44 -1
- package/dist/utils/dest-registry.d.ts +5 -2
- package/dist/utils/dest-registry.js +5 -5
- package/dist/utils/flex-layout.d.ts +24 -4
- package/dist/utils/flex-layout.js +169 -7
- package/dist/utils/font-metrics.d.ts +5 -0
- package/dist/utils/gsub.d.ts +36 -0
- package/dist/utils/gsub.js +178 -0
- package/dist/utils/inflate.d.ts +16 -0
- package/dist/utils/inflate.js +52 -0
- package/dist/utils/outline.js +1 -2
- package/dist/utils/pdf-object-manager.d.ts +39 -0
- package/dist/utils/pdf-object-manager.js +166 -15
- package/dist/utils/struct-tree.js +2 -3
- package/dist/utils/ttf-parser.d.ts +6 -0
- package/dist/utils/ttf-parser.js +19 -0
- package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
- package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
- package/dist/utils/woff.d.ts +10 -0
- package/dist/utils/woff.js +116 -0
- package/dist/validators/element-validator.js +14 -3
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
-
<code>npm i @jasy/
|
|
13
|
+
<code>npm i @jasy/e-invoice</code> · <code>npx @jasy/cli</code> · MIT
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
---
|
|
@@ -42,7 +42,7 @@ invoices validate: the amounts are correct **by construction**, so the single bi
|
|
|
42
42
|
EN-16931 failures (the BR-CO total checks) simply cannot happen.
|
|
43
43
|
|
|
44
44
|
```ts
|
|
45
|
-
import { renderZugferd } from "@jasy/
|
|
45
|
+
import { renderZugferd } from "@jasy/e-invoice";
|
|
46
46
|
|
|
47
47
|
const { bytes, xml } = await renderZugferd({
|
|
48
48
|
number: "RE-2026-001",
|
|
@@ -140,7 +140,7 @@ verify all of it:
|
|
|
140
140
|
- **PDF/A-3, matched not approximated.** The conformance graph is hand-built and **passes veraPDF**, the
|
|
141
141
|
official ISO 19005 validator.
|
|
142
142
|
- **Byte-exact round-trips.** Generate and parse are inverses: `generate → parse → regenerate` reproduces
|
|
143
|
-
the identical XML.
|
|
143
|
+
the identical XML. Over 900 tests hold the line across the workspace.
|
|
144
144
|
- **Schematron, local.** The official EN-16931 + XRechnung rules run via saxon-js (the real XSLT, in pure
|
|
145
145
|
JS) - no Java, no upload.
|
|
146
146
|
|
|
@@ -148,18 +148,19 @@ verify all of it:
|
|
|
148
148
|
|
|
149
149
|
## Packages
|
|
150
150
|
|
|
151
|
-
| Package
|
|
152
|
-
|
|
|
153
|
-
| **[@jasy/
|
|
154
|
-
| **[@jasy/cli](https://npmx.dev/@jasy/cli)**
|
|
155
|
-
| **[@jasy/pdf](https://npmx.dev/@jasy/pdf)**
|
|
151
|
+
| Package | What it is |
|
|
152
|
+
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
153
|
+
| **[@jasy/e-invoice](https://npmx.dev/@jasy/e-invoice)** | ZUGFeRD / XRechnung: your data → PDF/A-3 + EN-16931 XML, with local validation. **The prize.** |
|
|
154
|
+
| **[@jasy/cli](https://npmx.dev/@jasy/cli)** | the `jasy` terminal: read · validate · export, headless **and** interactive |
|
|
155
|
+
| **[@jasy/pdf](https://npmx.dev/@jasy/pdf)** | the declarative, Flutter-style PDF layout engine that powers them |
|
|
156
156
|
|
|
157
157
|
---
|
|
158
158
|
|
|
159
159
|
## Why you can trust it
|
|
160
160
|
|
|
161
|
-
- **
|
|
162
|
-
`generate → parse → regenerate` reproduces the identical XML. Nothing is
|
|
161
|
+
- **Over 900 tests, green** across the workspace (850 in the engine alone). The generator and the parser
|
|
162
|
+
are **byte-exact inverses**: `generate → parse → regenerate` reproduces the identical XML. Nothing is
|
|
163
|
+
silently lost.
|
|
163
164
|
- **The same rules the authorities use** - the official KoSIT EN-16931 + XRechnung Schematron, and the
|
|
164
165
|
official **veraPDF** for the full ISO 19005 (PDF/A) check.
|
|
165
166
|
- **Proven on real third-party invoices**, not only our own.
|
package/dist/api/content.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ImageElement, CustomImage } from "../elements/image-element.js";
|
|
|
2
2
|
import { PDFElement } from "../elements/pdf-element.js";
|
|
3
3
|
import { ColorInput } from "./color.js";
|
|
4
4
|
import { Insets } from "./insets.js";
|
|
5
|
-
import { SizeInput } from "./dimension.js";
|
|
5
|
+
import { BoundsInput, SizeInput, RadiusInput } from "./dimension.js";
|
|
6
6
|
/** A horizontal rule (locked §4). */
|
|
7
7
|
export interface DividerOptions {
|
|
8
8
|
/** Line color (default a light gray). */
|
|
@@ -22,15 +22,16 @@ export declare function Divider(opts?: DividerOptions): PDFElement;
|
|
|
22
22
|
export type ImageSource = string | Uint8Array | CustomImage;
|
|
23
23
|
/** How the image fills its box (locked §4). Mirrors CSS `object-fit`. */
|
|
24
24
|
export type ImageFit = "none" | "contain" | "cover" | "fill";
|
|
25
|
-
export interface ImageOptions {
|
|
25
|
+
export interface ImageOptions extends BoundsInput {
|
|
26
26
|
/** Size on each axis: points (fixed) or a percentage string like `"50%"` (a fraction of the offered
|
|
27
27
|
* space). Pin exactly ONE axis and the other follows the image's aspect ratio (CSS `height: auto`). */
|
|
28
28
|
width?: SizeInput;
|
|
29
29
|
height?: SizeInput;
|
|
30
|
-
/** Fit within the box
|
|
30
|
+
/** Fit within the box. Default `none`, except where the box was DERIVED rather than given outright -
|
|
31
|
+
* exactly one axis pinned, or an `aspectRatio` - which defaults to `fill` so the image scales into it. */
|
|
31
32
|
fit?: ImageFit;
|
|
32
33
|
/** Corner radius in points (rounds the image box). */
|
|
33
|
-
radius?:
|
|
34
|
+
radius?: RadiusInput;
|
|
34
35
|
/** Alternate text for accessibility (tagged PDF): describes the image for screen readers. With `alt`
|
|
35
36
|
* the image is a `Figure`; without it (and when rendered `accessible`) it counts as decoration. */
|
|
36
37
|
alt?: string;
|
package/dist/api/content.js
CHANGED
|
@@ -3,7 +3,7 @@ import { PaddingElement } from "../elements/layout/padding-element.js";
|
|
|
3
3
|
import { ImageElement, CustomLocalImage, CustomBytesImage, BoxFit, } from "../elements/image-element.js";
|
|
4
4
|
import { toColor } from "./color.js";
|
|
5
5
|
import { toEdges } from "./insets.js";
|
|
6
|
-
import { toDimension } from "./dimension.js";
|
|
6
|
+
import { toBounds, toDimension, toRadius, } from "./dimension.js";
|
|
7
7
|
const DEFAULT_DIVIDER_COLOR = "lightgray";
|
|
8
8
|
const DEFAULT_DIVIDER_MARGIN = { y: 6 };
|
|
9
9
|
/**
|
|
@@ -38,10 +38,12 @@ const FIT = {
|
|
|
38
38
|
export function Image(src, opts = {}) {
|
|
39
39
|
const w = opts.width !== undefined ? toDimension(opts.width) : undefined;
|
|
40
40
|
const h = opts.height !== undefined ? toDimension(opts.height) : undefined;
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
// ANY explicit size scales the image into the box (fit: fill), which is what `<img width height>`
|
|
42
|
+
// does in HTML and what react-pdf does. Two pinned axes used to keep `none` - drawing the image at
|
|
43
|
+
// its pixel size regardless - so a logo asked for at 48x48 spanned a third of the page.
|
|
44
|
+
// An image with NO size given still draws at its intrinsic size, as an unsized `<img>` does.
|
|
45
|
+
const sized = opts.width !== undefined || opts.height !== undefined || opts.aspectRatio !== undefined;
|
|
46
|
+
const fit = opts.fit ? FIT[opts.fit] : sized ? BoxFit.fill : undefined;
|
|
45
47
|
return new ImageElement({
|
|
46
48
|
image: typeof src === "string"
|
|
47
49
|
? new CustomLocalImage(src)
|
|
@@ -52,8 +54,12 @@ export function Image(src, opts = {}) {
|
|
|
52
54
|
height: h?.points,
|
|
53
55
|
widthFactor: w?.factor,
|
|
54
56
|
heightFactor: h?.factor,
|
|
57
|
+
...toBounds(opts),
|
|
55
58
|
fit,
|
|
56
|
-
radius: opts.radius,
|
|
59
|
+
radius: opts.radius !== undefined ? toRadius(opts.radius) : undefined,
|
|
57
60
|
alt: opts.alt,
|
|
58
|
-
})
|
|
61
|
+
})
|
|
62
|
+
.withAlignSelf(opts.alignSelf)
|
|
63
|
+
.withOrder(opts.order)
|
|
64
|
+
.withFlexShrink(opts.flexShrink);
|
|
59
65
|
}
|
package/dist/api/descriptor.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Divider, Image } from "./content.js";
|
|
|
4
4
|
import { Page, Document, DefaultTextStyle } from "./structure.js";
|
|
5
5
|
import { PageNumber, PageCount } from "./page-builder.js";
|
|
6
6
|
import { Table } from "./table.js";
|
|
7
|
+
import { Checkbox, Dropdown, ListBox, PushButton, RadioGroup, SignatureField, TextField, } from "./forms.js";
|
|
7
8
|
// Element children (Column/Row/Box/Page/…) → built recursively. Text/Paragraph children are
|
|
8
9
|
// strings or `span` descriptors and become content instead.
|
|
9
10
|
function elementChildren(children) {
|
|
@@ -112,7 +113,29 @@ const REGISTRY = {
|
|
|
112
113
|
// cannot express. These two cover what the closure exists for.
|
|
113
114
|
"page-number": (props) => PageNumber(props),
|
|
114
115
|
"page-count": (props) => PageCount(props),
|
|
116
|
+
// Form fields. The three list-like ones take their choices as a SECOND argument in TypeScript; a
|
|
117
|
+
// template has no second argument, so here they arrive as an `options` prop. `choiceList` accepts both
|
|
118
|
+
// the short spelling and the explicit one, because an export value and its label are usually the same
|
|
119
|
+
// string and having to write it twice is noise.
|
|
120
|
+
"text-field": (props) => TextField(props),
|
|
121
|
+
checkbox: (props) => Checkbox(props),
|
|
122
|
+
"radio-group": (props) => RadioGroup(props, radioChoices(props.options)),
|
|
123
|
+
dropdown: (props) => Dropdown(props, choiceList(props.options)),
|
|
124
|
+
"list-box": (props) => ListBox(props, choiceList(props.options)),
|
|
125
|
+
"push-button": (props) => PushButton(props),
|
|
126
|
+
"signature-field": (props) => SignatureField(props),
|
|
115
127
|
};
|
|
128
|
+
/** `["S", "M"]` or `[{ value: "DE", label: "Germany" }]` - both mean the same thing to a choice field. */
|
|
129
|
+
function choiceList(raw) {
|
|
130
|
+
if (!Array.isArray(raw)) {
|
|
131
|
+
throw new Error("jasy: a choice field needs an `options` array (strings or {value, label}).");
|
|
132
|
+
}
|
|
133
|
+
return raw;
|
|
134
|
+
}
|
|
135
|
+
/** A radio button always shows a label, so the short spelling means "label is the value". */
|
|
136
|
+
function radioChoices(raw) {
|
|
137
|
+
return choiceList(raw).map((o) => typeof o === "string" ? { value: o, label: o } : { value: o.value, label: o.label ?? o.value });
|
|
138
|
+
}
|
|
116
139
|
/**
|
|
117
140
|
* Registers a custom element type, so a binding (or a user-defined component) can introduce
|
|
118
141
|
* its own tag that resolves to an engine element through this same seam. Overwrites an
|
package/dist/api/dimension.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Radii } from "../ir/display-list.js";
|
|
2
|
+
import type { CrossAlign } from "../layout/alignment.js";
|
|
1
3
|
/**
|
|
2
4
|
* Size input for a Box's `width` / `height` (relative sizing). Either a number of PDF points (a
|
|
3
5
|
* fixed size) or a percentage string like `"50%"` - a fraction of the space the parent offers on
|
|
@@ -18,3 +20,57 @@ export interface Dimension {
|
|
|
18
20
|
* boundary so `resolveExtent` and the layout never see a negative size.
|
|
19
21
|
*/
|
|
20
22
|
export declare function toDimension(value: SizeInput): Dimension;
|
|
23
|
+
/** The bound and ratio options every sized factory accepts, in the public `SizeInput` form. */
|
|
24
|
+
export interface BoundsInput {
|
|
25
|
+
/**
|
|
26
|
+
* This element's own cross-axis alignment inside its Column / Row (CSS `align-self`), overriding the
|
|
27
|
+
* container's `align`. On a `Spacer` / `Expanded` it is a no-op - a flex child fills the main axis and
|
|
28
|
+
* has no natural cross size to align.
|
|
29
|
+
*/
|
|
30
|
+
alignSelf?: CrossAlign;
|
|
31
|
+
/**
|
|
32
|
+
* Where this child sits among its siblings (CSS `order`), lowest first, source order deciding ties.
|
|
33
|
+
* It moves the child in the LAYOUT only - the element tree is untouched, so a tagged PDF still
|
|
34
|
+
* exposes the source reading order. Default 0.
|
|
35
|
+
*/
|
|
36
|
+
order?: number;
|
|
37
|
+
/**
|
|
38
|
+
* How willingly this child gives up main-axis space when the line overflows (CSS `flex-shrink`),
|
|
39
|
+
* weighted by its own size. **Default 0, deliberately unlike CSS's 1** - shrinking changes what a
|
|
40
|
+
* document looks like, so it is opted into per child rather than applied to everything at once.
|
|
41
|
+
*/
|
|
42
|
+
flexShrink?: number;
|
|
43
|
+
minWidth?: SizeInput;
|
|
44
|
+
maxWidth?: SizeInput;
|
|
45
|
+
minHeight?: SizeInput;
|
|
46
|
+
maxHeight?: SizeInput;
|
|
47
|
+
/**
|
|
48
|
+
* width / height (CSS `aspect-ratio`). Fills in whichever axis you leave open, and sizes the box
|
|
49
|
+
* from the offered width when you leave both open. An explicit `min`/`max` still wins over it.
|
|
50
|
+
*/
|
|
51
|
+
aspectRatio?: number;
|
|
52
|
+
}
|
|
53
|
+
/** Split `BoundsInput` into the points-and-factors form the elements take. */
|
|
54
|
+
export declare function toBounds(o: BoundsInput): {
|
|
55
|
+
minWidth: number | undefined;
|
|
56
|
+
maxWidth: number | undefined;
|
|
57
|
+
minHeight: number | undefined;
|
|
58
|
+
maxHeight: number | undefined;
|
|
59
|
+
minWidthFactor: number | undefined;
|
|
60
|
+
maxWidthFactor: number | undefined;
|
|
61
|
+
minHeightFactor: number | undefined;
|
|
62
|
+
maxHeightFactor: number | undefined;
|
|
63
|
+
aspectRatio: number | undefined;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Corner radius input. A single number rounds all four corners; the object names them; the tuple is
|
|
67
|
+
* CSS order - `[topLeft, topRight, bottomRight, bottomLeft]`, clockwise from the top left.
|
|
68
|
+
*/
|
|
69
|
+
export type RadiusInput = number | {
|
|
70
|
+
topLeft?: number;
|
|
71
|
+
topRight?: number;
|
|
72
|
+
bottomRight?: number;
|
|
73
|
+
bottomLeft?: number;
|
|
74
|
+
} | [number, number, number, number];
|
|
75
|
+
/** Normalizes a `RadiusInput` to the engine's short corner names. */
|
|
76
|
+
export declare function toRadius(r: RadiusInput): number | Radii;
|
package/dist/api/dimension.js
CHANGED
|
@@ -14,3 +14,30 @@ export function toDimension(value) {
|
|
|
14
14
|
return { factor: parseFloat(m[1]) / 100 };
|
|
15
15
|
throw new Error(`Invalid size "${value}": use a number of points or a percentage like "50%".`);
|
|
16
16
|
}
|
|
17
|
+
/** Split `BoundsInput` into the points-and-factors form the elements take. */
|
|
18
|
+
export function toBounds(o) {
|
|
19
|
+
const d = (v) => (v !== undefined ? toDimension(v) : undefined);
|
|
20
|
+
const [minW, maxW, minH, maxH] = [d(o.minWidth), d(o.maxWidth), d(o.minHeight), d(o.maxHeight)];
|
|
21
|
+
if (o.aspectRatio !== undefined && !(o.aspectRatio > 0)) {
|
|
22
|
+
throw new Error(`Invalid aspectRatio ${o.aspectRatio}: it is width / height and must be above 0.`);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
minWidth: minW?.points,
|
|
26
|
+
maxWidth: maxW?.points,
|
|
27
|
+
minHeight: minH?.points,
|
|
28
|
+
maxHeight: maxH?.points,
|
|
29
|
+
minWidthFactor: minW?.factor,
|
|
30
|
+
maxWidthFactor: maxW?.factor,
|
|
31
|
+
minHeightFactor: minH?.factor,
|
|
32
|
+
maxHeightFactor: maxH?.factor,
|
|
33
|
+
aspectRatio: o.aspectRatio,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Normalizes a `RadiusInput` to the engine's short corner names. */
|
|
37
|
+
export function toRadius(r) {
|
|
38
|
+
if (typeof r === "number")
|
|
39
|
+
return r;
|
|
40
|
+
if (Array.isArray(r))
|
|
41
|
+
return { tl: r[0], tr: r[1], br: r[2], bl: r[3] };
|
|
42
|
+
return { tl: r.topLeft, tr: r.topRight, br: r.bottomRight, bl: r.bottomLeft };
|
|
43
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading a font over the network for `doc.addFontFromUrl(...)`.
|
|
3
|
+
*
|
|
4
|
+
* `addFont` resolves its source AT REGISTRATION - a path is read there and then - and this keeps the
|
|
5
|
+
* same contract, differing only in being async. So the failure lands where you asked for the font,
|
|
6
|
+
* not somewhere inside a later render.
|
|
7
|
+
*
|
|
8
|
+
* `fetch` is used directly: it is in every browser and in Node since 18, so this needs no platform
|
|
9
|
+
* port, unlike the filesystem read beside it.
|
|
10
|
+
*/
|
|
11
|
+
/** One font file by URL, or a styled family of them. Mirrors `FontSource`, with strings meaning URLs. */
|
|
12
|
+
export type UrlFontSource = string | {
|
|
13
|
+
normal: string;
|
|
14
|
+
bold?: string;
|
|
15
|
+
italic?: string;
|
|
16
|
+
boldItalic?: string;
|
|
17
|
+
};
|
|
18
|
+
/** Thrown when a font could not be fetched or is not one we can parse. Always says which URL. */
|
|
19
|
+
export declare class FontUrlError extends Error {
|
|
20
|
+
constructor(message: string);
|
|
21
|
+
}
|
|
22
|
+
/** Fetch a whole `UrlFontSource`, in parallel for a family. */
|
|
23
|
+
export declare function loadFontFromUrl(source: UrlFontSource): Promise<Uint8Array | {
|
|
24
|
+
normal: Uint8Array;
|
|
25
|
+
bold?: Uint8Array;
|
|
26
|
+
italic?: Uint8Array;
|
|
27
|
+
boldItalic?: Uint8Array;
|
|
28
|
+
}>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading a font over the network for `doc.addFontFromUrl(...)`.
|
|
3
|
+
*
|
|
4
|
+
* `addFont` resolves its source AT REGISTRATION - a path is read there and then - and this keeps the
|
|
5
|
+
* same contract, differing only in being async. So the failure lands where you asked for the font,
|
|
6
|
+
* not somewhere inside a later render.
|
|
7
|
+
*
|
|
8
|
+
* `fetch` is used directly: it is in every browser and in Node since 18, so this needs no platform
|
|
9
|
+
* port, unlike the filesystem read beside it.
|
|
10
|
+
*/
|
|
11
|
+
/** Thrown when a font could not be fetched or is not one we can parse. Always says which URL. */
|
|
12
|
+
export class FontUrlError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(`@jasy/pdf: ${message}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* What the first four bytes of an sfnt file say it is. Checked here because `TTFParser` does not look
|
|
19
|
+
* at them: it goes straight for the table directory, so an HTML error page or a WOFF would fail deep
|
|
20
|
+
* inside with `missing required table "head"` instead of saying what the file actually is.
|
|
21
|
+
*/
|
|
22
|
+
function describeSignature(bytes) {
|
|
23
|
+
const tag = String.fromCharCode(...bytes.subarray(0, 4));
|
|
24
|
+
const v = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];
|
|
25
|
+
if (v === 0x00010000 || tag === "true")
|
|
26
|
+
return { ok: true, what: "TrueType" };
|
|
27
|
+
if (tag === "OTTO")
|
|
28
|
+
return { ok: false, what: "an OpenType/CFF font, which jasy does not parse yet" };
|
|
29
|
+
if (tag === "wOFF")
|
|
30
|
+
return { ok: true, what: "WOFF" };
|
|
31
|
+
if (tag === "wOF2")
|
|
32
|
+
return { ok: false, what: "a WOFF2 font, which jasy does not parse yet" };
|
|
33
|
+
if (tag === "ttcf")
|
|
34
|
+
return { ok: false, what: "a TrueType Collection, which jasy does not parse yet" };
|
|
35
|
+
if (tag.startsWith("<") || tag === "%PDF")
|
|
36
|
+
return { ok: false, what: "not a font at all" };
|
|
37
|
+
return { ok: false, what: "not a font we recognise" };
|
|
38
|
+
}
|
|
39
|
+
/** How long one font may take to arrive before we give up. A hung server must not hang a render. */
|
|
40
|
+
const TIMEOUT_MS = 15000;
|
|
41
|
+
/**
|
|
42
|
+
* How large a font file may be. Generous on purpose - a full CJK face is tens of megabytes and is a
|
|
43
|
+
* legitimate thing to embed - but not unbounded: a URL pointing at the wrong file (a video, a disk
|
|
44
|
+
* image) would otherwise be read into memory in full before anyone notices it is not a font.
|
|
45
|
+
*/
|
|
46
|
+
const MAX_BYTES = 32 * 1024 * 1024;
|
|
47
|
+
/** Read the body with a running ceiling, so an oversized response is dropped WHILE it arrives. */
|
|
48
|
+
async function readBounded(response, url) {
|
|
49
|
+
// `Content-Length` is the cheap early exit; a server may omit or misstate it, so it is a hint only
|
|
50
|
+
// and the real ceiling is enforced below while reading.
|
|
51
|
+
const declared = Number(response.headers?.get?.("content-length") ?? NaN);
|
|
52
|
+
if (Number.isFinite(declared) && declared > MAX_BYTES) {
|
|
53
|
+
throw new FontUrlError(`the font at ${url} is ${declared} bytes; the limit is ${MAX_BYTES}`);
|
|
54
|
+
}
|
|
55
|
+
const body = response.body;
|
|
56
|
+
if (!body?.getReader) {
|
|
57
|
+
// No streaming body (an older runtime, or a stubbed response): fall back to reading it whole and
|
|
58
|
+
// checking afterwards. Weaker, but never worse than not checking at all.
|
|
59
|
+
const whole = new Uint8Array(await response.arrayBuffer());
|
|
60
|
+
if (whole.length > MAX_BYTES) {
|
|
61
|
+
throw new FontUrlError(`the font at ${url} is larger than the ${MAX_BYTES} byte limit`);
|
|
62
|
+
}
|
|
63
|
+
return whole;
|
|
64
|
+
}
|
|
65
|
+
const reader = body.getReader();
|
|
66
|
+
const chunks = [];
|
|
67
|
+
let total = 0;
|
|
68
|
+
for (;;) {
|
|
69
|
+
const { done, value } = await reader.read();
|
|
70
|
+
if (done)
|
|
71
|
+
break;
|
|
72
|
+
total += value.length;
|
|
73
|
+
if (total > MAX_BYTES) {
|
|
74
|
+
await reader.cancel();
|
|
75
|
+
throw new FontUrlError(`the font at ${url} is larger than the ${MAX_BYTES} byte limit`);
|
|
76
|
+
}
|
|
77
|
+
chunks.push(value);
|
|
78
|
+
}
|
|
79
|
+
if (chunks.length === 1)
|
|
80
|
+
return chunks[0];
|
|
81
|
+
const out = new Uint8Array(total);
|
|
82
|
+
let at = 0;
|
|
83
|
+
for (const c of chunks) {
|
|
84
|
+
out.set(c, at);
|
|
85
|
+
at += c.length;
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
/** Fetch one font file and hand back its bytes, refusing anything we could not embed. */
|
|
90
|
+
async function fetchFont(url) {
|
|
91
|
+
let response;
|
|
92
|
+
try {
|
|
93
|
+
response = await fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
const why = e?.name === "TimeoutError" || e?.name === "AbortError"
|
|
97
|
+
? `it did not respond within ${TIMEOUT_MS} ms`
|
|
98
|
+
: String(e?.message ?? e);
|
|
99
|
+
throw new FontUrlError(`could not fetch the font at ${url}: ${why}`);
|
|
100
|
+
}
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new FontUrlError(`the font at ${url} could not be fetched: HTTP ${response.status}`);
|
|
103
|
+
}
|
|
104
|
+
// Everything past here still talks to the network, so a failure mid-body must not escape as a raw
|
|
105
|
+
// TypeError - a caller catching FontUrlError would miss it.
|
|
106
|
+
let bytes;
|
|
107
|
+
try {
|
|
108
|
+
bytes = await readBounded(response, url);
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
if (e instanceof FontUrlError)
|
|
112
|
+
throw e;
|
|
113
|
+
throw new FontUrlError(`the font at ${url} could not be read: ${String(e?.message ?? e)}`);
|
|
114
|
+
}
|
|
115
|
+
if (bytes.length < 4) {
|
|
116
|
+
throw new FontUrlError(`the font at ${url} is empty`);
|
|
117
|
+
}
|
|
118
|
+
const { ok, what } = describeSignature(bytes);
|
|
119
|
+
if (!ok) {
|
|
120
|
+
throw new FontUrlError(`the file at ${url} is ${what}. jasy embeds TrueType-flavoured fonts (.ttf and .woff)`);
|
|
121
|
+
}
|
|
122
|
+
return bytes;
|
|
123
|
+
}
|
|
124
|
+
/** Fetch a whole `UrlFontSource`, in parallel for a family. */
|
|
125
|
+
export async function loadFontFromUrl(source) {
|
|
126
|
+
if (typeof source === "string")
|
|
127
|
+
return fetchFont(source);
|
|
128
|
+
const styles = ["normal", "bold", "italic", "boldItalic"];
|
|
129
|
+
const wanted = styles.filter((s) => source[s] !== undefined);
|
|
130
|
+
const loaded = await Promise.all(wanted.map((s) => fetchFont(source[s])));
|
|
131
|
+
const family = { normal: new Uint8Array() };
|
|
132
|
+
wanted.forEach((s, i) => (family[s] = loaded[i]));
|
|
133
|
+
return family;
|
|
134
|
+
}
|