@murumets-ee/pdf 0.38.0
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/LICENSE +94 -0
- package/README.md +104 -0
- package/dist/index.d.mts +143 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/primitives.d.mts +10 -0
- package/dist/primitives.d.mts.map +1 -0
- package/dist/primitives.mjs +1 -0
- package/fonts/NotoSans-Bold.ttf +0 -0
- package/fonts/NotoSans-Regular.ttf +0 -0
- package/fonts/OFL.txt +93 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Elastic License 2.0 (ELv2)
|
|
2
|
+
|
|
3
|
+
URL: https://www.elastic.co/licensing/elastic-license
|
|
4
|
+
|
|
5
|
+
## Acceptance
|
|
6
|
+
|
|
7
|
+
By using the software, you agree to all of the terms and conditions below.
|
|
8
|
+
|
|
9
|
+
## Copyright License
|
|
10
|
+
|
|
11
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide,
|
|
12
|
+
non-sublicensable, non-transferable license to use, copy, distribute, make
|
|
13
|
+
available, and prepare derivative works of the software, in each case subject
|
|
14
|
+
to the limitations and conditions below.
|
|
15
|
+
|
|
16
|
+
## Limitations
|
|
17
|
+
|
|
18
|
+
You may not provide the software to third parties as a hosted or managed
|
|
19
|
+
service, where the service provides users with access to any substantial set
|
|
20
|
+
of the features or functionality of the software.
|
|
21
|
+
|
|
22
|
+
You may not move, change, disable, or circumvent the license key functionality
|
|
23
|
+
in the software, and you may not remove or obscure any functionality in the
|
|
24
|
+
software that is protected by the license key.
|
|
25
|
+
|
|
26
|
+
You may not alter, remove, or obscure any licensing, copyright, or other
|
|
27
|
+
notices of the licensor in the software. Any use of the licensor's trademarks
|
|
28
|
+
is subject to applicable law.
|
|
29
|
+
|
|
30
|
+
## Patents
|
|
31
|
+
|
|
32
|
+
The licensor grants you a license, under any patent claims the licensor can
|
|
33
|
+
license, or becomes able to license, to make, have made, use, sell, offer for
|
|
34
|
+
sale, import and have imported the software, in each case subject to the
|
|
35
|
+
limitations and conditions in this license. This license does not cover any
|
|
36
|
+
patent claims that you cause to be infringed by modifications or additions to
|
|
37
|
+
the software. If you or your company make any written claim that the software
|
|
38
|
+
infringes or contributes to infringement of any patent, your patent license
|
|
39
|
+
for the software granted under these terms ends immediately. If your company
|
|
40
|
+
makes such a claim, your patent license ends immediately for work on behalf
|
|
41
|
+
of your company.
|
|
42
|
+
|
|
43
|
+
## Notices
|
|
44
|
+
|
|
45
|
+
You must ensure that anyone who gets a copy of any part of the software from
|
|
46
|
+
you also gets a copy of these terms.
|
|
47
|
+
|
|
48
|
+
If you modify the software, you must include in any modified copies of the
|
|
49
|
+
software prominent notices stating that you have modified the software.
|
|
50
|
+
|
|
51
|
+
## No Other Rights
|
|
52
|
+
|
|
53
|
+
These terms do not imply any licenses other than those expressly granted in
|
|
54
|
+
these terms.
|
|
55
|
+
|
|
56
|
+
## Termination
|
|
57
|
+
|
|
58
|
+
If you use the software in violation of these terms, such use is not licensed,
|
|
59
|
+
and your licenses will automatically terminate. If the licensor provides you
|
|
60
|
+
with a notice of your violation, and you cease all violation of this license
|
|
61
|
+
no later than 30 days after you receive that notice, your licenses will be
|
|
62
|
+
reinstated retroactively. However, if you violate these terms after such
|
|
63
|
+
reinstatement, any additional violation of these terms will cause your
|
|
64
|
+
licenses to terminate automatically and permanently.
|
|
65
|
+
|
|
66
|
+
## No Liability
|
|
67
|
+
|
|
68
|
+
As far as the law allows, the software comes as is, without any warranty or
|
|
69
|
+
condition, and the licensor will not be liable to you for any damages arising
|
|
70
|
+
out of these terms or the use or nature of the software, under any kind of
|
|
71
|
+
legal claim.
|
|
72
|
+
|
|
73
|
+
## Definitions
|
|
74
|
+
|
|
75
|
+
The **licensor** is the entity offering these terms, and the **software** is
|
|
76
|
+
the software the licensor makes available under these terms, including any
|
|
77
|
+
portion of it.
|
|
78
|
+
|
|
79
|
+
**you** refers to the individual or entity agreeing to these terms.
|
|
80
|
+
|
|
81
|
+
**your company** is any legal entity, sole proprietorship, or other kind of
|
|
82
|
+
organization that you work for, plus all organizations that have control over,
|
|
83
|
+
are under the control of, or are under common control with that organization.
|
|
84
|
+
**control** means ownership of substantially all the assets of an entity, or
|
|
85
|
+
the power to direct the management and policies of an entity (for example, by
|
|
86
|
+
voting right, contract, or otherwise). Control can be direct or indirect.
|
|
87
|
+
|
|
88
|
+
**your licenses** are all the licenses granted to you for the software under
|
|
89
|
+
these terms.
|
|
90
|
+
|
|
91
|
+
**use** means anything you do with the software requiring one of your
|
|
92
|
+
licenses.
|
|
93
|
+
|
|
94
|
+
**trademark** means trademarks, service marks, and similar rights.
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# @murumets-ee/pdf
|
|
2
|
+
|
|
3
|
+
A React element tree in, PDF bytes plus a SHA-256 digest out. Domain-free and server-only in
|
|
4
|
+
intent: it knows nothing about contracts, invoices, entities, storage buckets or blocks.
|
|
5
|
+
|
|
6
|
+
Engine rationale, flip conditions and the determinism policy are locked in
|
|
7
|
+
[`plan/rent/decisions/D028-pdf-package-and-engine.md`](../../plan/rent/decisions/D028-pdf-package-and-engine.md);
|
|
8
|
+
the legal background is [R06](../../plan/rent/research/R06-contract-legal-and-pdf.md).
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { createReactPdfRenderer, DEFAULT_FONT_FAMILY } from '@murumets-ee/pdf'
|
|
14
|
+
import { Document, Page, StyleSheet, Text, View } from '@murumets-ee/pdf/primitives'
|
|
15
|
+
|
|
16
|
+
const styles = StyleSheet.create({
|
|
17
|
+
page: { fontFamily: DEFAULT_FONT_FAMILY, padding: 40, fontSize: 11 },
|
|
18
|
+
heading: { fontFamily: DEFAULT_FONT_FAMILY, fontWeight: 700, fontSize: 16 },
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const renderer = createReactPdfRenderer()
|
|
22
|
+
|
|
23
|
+
const { bytes, sha256, byteLength } = await renderer.render(
|
|
24
|
+
<Document>
|
|
25
|
+
<Page size="A4" style={styles.page}>
|
|
26
|
+
<View>
|
|
27
|
+
<Text style={styles.heading}>Üürileping</Text>
|
|
28
|
+
<Text>Договор аренды</Text>
|
|
29
|
+
</View>
|
|
30
|
+
</Page>
|
|
31
|
+
</Document>,
|
|
32
|
+
{ creationDate: signedAt, producer: 'Lumi CMS', creator: 'Lumi CMS' },
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Build documents out of `@murumets-ee/pdf/primitives` — `Document`, `Page`, `View`, `Text`,
|
|
37
|
+
`Image`, `StyleSheet`, `Font`, plus the `Style` / `Styles` types. Consumers should never add a
|
|
38
|
+
direct `@react-pdf/renderer` dependency; the engine seam is this package.
|
|
39
|
+
|
|
40
|
+
`creationDate` is **required**. There is no wall-clock default: an implicit `new Date()` is the
|
|
41
|
+
single biggest source of gratuitous non-determinism, and a caller that wants "now" should say so.
|
|
42
|
+
|
|
43
|
+
## The digest is not a reproducibility claim
|
|
44
|
+
|
|
45
|
+
`sha256` identifies **the bytes it was returned with**. Re-rendering the same tree with the same
|
|
46
|
+
options is *not* expected to reproduce it, so **re-render-and-compare is not a valid integrity
|
|
47
|
+
check** — it will report a false mismatch on a document nobody touched.
|
|
48
|
+
|
|
49
|
+
Two independent reasons, both inside the engine:
|
|
50
|
+
|
|
51
|
+
1. `@react-pdf/renderer` vendors a pdfkit fork whose six-letter font-subset tag (`ABCDEF+NotoSans`)
|
|
52
|
+
comes from `Math.random()`. Upstream pdfkit derives it from the font's object id; the fork has
|
|
53
|
+
not picked that up. We deliberately do **not** patch or seed the global RNG — it is
|
|
54
|
+
process-wide and would corrupt any concurrent render in the same worker.
|
|
55
|
+
2. pdfkit compresses each stream through an **async** `zlib.createDeflate()` and writes each object
|
|
56
|
+
when *its* deflate finishes, so two streams of different sizes can complete in either order.
|
|
57
|
+
The `FontFile2` and `ToUnicode` objects are observed swapping places between runs.
|
|
58
|
+
|
|
59
|
+
What *is* pinned, and covered by tests: the Info dictionary (`/CreationDate`, `/ModificationDate`,
|
|
60
|
+
`/Producer`, `/Creator`), the file `/ID` that pdfkit derives from `md5(info)`, the total byte
|
|
61
|
+
length, and every indirect object's content.
|
|
62
|
+
|
|
63
|
+
The policy, per [R06](../../plan/rent/research/R06-contract-legal-and-pdf.md) §2: **hash on write,
|
|
64
|
+
persist the exact bytes, never re-render for comparison.** ISO 32000-1 §14.4 says outright that
|
|
65
|
+
*"the calculation of the file identifier need not be reproducible"*, and no e-signature vendor
|
|
66
|
+
re-renders from source to verify — they all hash the stored bytes.
|
|
67
|
+
|
|
68
|
+
## Fonts
|
|
69
|
+
|
|
70
|
+
**Noto Sans**, regular + bold, shipped inside the package. A render never fetches a font over the
|
|
71
|
+
network.
|
|
72
|
+
|
|
73
|
+
One file covers Latin, Latin Extended (Estonian `õäöüšž`), Greek and Cyrillic — these are the
|
|
74
|
+
[`notofonts/latin-greek-cyrillic`](https://github.com/notofonts/latin-greek-cyrillic) static
|
|
75
|
+
builds, and `src/fonts.test.ts` asserts glyph coverage per script rather than assuming it. That
|
|
76
|
+
single-file property is load-bearing: `@react-pdf/renderer` has no `unicode-range` mechanism, so a
|
|
77
|
+
per-script split would need a fallback chain it cannot express, and a missing glyph renders as
|
|
78
|
+
`.notdef` **silently**.
|
|
79
|
+
|
|
80
|
+
`loadDefaultFonts()` reads both weights off disk *before* the render rather than letting react-pdf
|
|
81
|
+
load them lazily mid-layout — the lazy path changes pdfkit's object emission order, so the first
|
|
82
|
+
render in a process would otherwise differ structurally from every later one.
|
|
83
|
+
|
|
84
|
+
The fonts are licensed under the **SIL Open Font License 1.1**; the licence text ships with them at
|
|
85
|
+
[`fonts/OFL.txt`](./fonts/OFL.txt). Copyright 2022 The Noto Project Authors. Install weight: ~1.2 MB
|
|
86
|
+
of font files, plus `@react-pdf/renderer` (~33 MB, 67 transitive packages, no native build, no
|
|
87
|
+
postinstall, no browser download).
|
|
88
|
+
|
|
89
|
+
## Not `server-only`-tagged — deliberately
|
|
90
|
+
|
|
91
|
+
The package is server-only in intent, but the entry does **not** `import 'server-only'`.
|
|
92
|
+
|
|
93
|
+
Its consumers reach it from an admin route, which is reachable from a plugin's static entry — the
|
|
94
|
+
exact chain `pnpm verify:plugin-loads` loads under plain Node, where `server-only` throws
|
|
95
|
+
unconditionally. `@murumets-ee/rent`'s `plugin.ts` already dynamic-imports its server-only runtime
|
|
96
|
+
deps to stay jiti-safe; tagging this package would force the same escape hatch on every consumer
|
|
97
|
+
for no security gain, since nothing here reads a request, a session or the database.
|
|
98
|
+
|
|
99
|
+
`@murumets-ee/storage` carries the tag on `./` precisely because it keeps a *separate*, clean
|
|
100
|
+
`./plugin` entry for the CLI path. This package has no such split — the renderer is the whole
|
|
101
|
+
surface.
|
|
102
|
+
|
|
103
|
+
Keep it out of any `*-ui/src/pages/**` static-import path (or add it to that package's tsdown
|
|
104
|
+
`neverBundle`) so it is never inlined into a server-pages bundle.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/digest.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Lowercase hex SHA-256 of exactly the bytes in `bytes`.
|
|
6
|
+
*
|
|
7
|
+
* Respects `byteOffset` / `byteLength`, so a `Buffer` that is a window into a
|
|
8
|
+
* larger pooled `ArrayBuffer` hashes its own contents and nothing else.
|
|
9
|
+
*
|
|
10
|
+
* See {@link PdfRenderResult.sha256} for what this digest does and does not
|
|
11
|
+
* attest to — it identifies a specific byte sequence, not a rendering input.
|
|
12
|
+
*/
|
|
13
|
+
declare function sha256Hex(bytes: Uint8Array): string;
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/fonts.d.ts
|
|
16
|
+
/**
|
|
17
|
+
* The one font family this package registers.
|
|
18
|
+
*
|
|
19
|
+
* **Noto Sans** (SIL Open Font License 1.1). A single file covers Latin,
|
|
20
|
+
* Latin Extended (Estonian `õäöüšž`), Greek and Cyrillic — these are the
|
|
21
|
+
* `notofonts/latin-greek-cyrillic` static builds, verified glyph-by-glyph in
|
|
22
|
+
* `fonts.test.ts`. That matters because `@react-pdf/renderer` has no
|
|
23
|
+
* `unicode-range` mechanism: a per-script split would need a fallback chain it
|
|
24
|
+
* cannot express, and missing glyphs render as `.notdef` **silently**.
|
|
25
|
+
*/
|
|
26
|
+
declare const DEFAULT_FONT_FAMILY = "Noto Sans";
|
|
27
|
+
/**
|
|
28
|
+
* Absolute paths to the bundled font files. They ship **inside the package**;
|
|
29
|
+
* a render never fetches a font over the network.
|
|
30
|
+
*/
|
|
31
|
+
declare const NOTO_SANS_FILES: {
|
|
32
|
+
readonly regular: string;
|
|
33
|
+
readonly bold: string;
|
|
34
|
+
};
|
|
35
|
+
/** The SIL Open Font License 1.1 text that covers {@link NOTO_SANS_FILES}. */
|
|
36
|
+
declare const NOTO_SANS_LICENSE_FILE: string;
|
|
37
|
+
/**
|
|
38
|
+
* Register {@link DEFAULT_FONT_FAMILY} on react-pdf's font store.
|
|
39
|
+
*
|
|
40
|
+
* Idempotent: react-pdf's store is a module singleton, and `register` appends
|
|
41
|
+
* to a family's source list rather than replacing it, so a naive repeat call
|
|
42
|
+
* would stack duplicates. The guard reads the store instead of tracking local
|
|
43
|
+
* state, so it stays correct across a caller's `Font.reset()`.
|
|
44
|
+
*/
|
|
45
|
+
declare function registerDefaultFonts(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Register the default family AND read both weights off disk before returning.
|
|
48
|
+
*
|
|
49
|
+
* This is a determinism fix, not a performance one. react-pdf loads a font
|
|
50
|
+
* lazily, mid-layout, the first time a glyph needs it — and that extra `await`
|
|
51
|
+
* changes the order in which pdfkit emits objects, so the FIRST render in a
|
|
52
|
+
* process produces a different object numbering (and different xref offsets)
|
|
53
|
+
* than every render after it. Measured: renders 2/3/4 were byte-identical once
|
|
54
|
+
* the subset tag is normalised away, render 1 was not. Pre-loading collapses
|
|
55
|
+
* that difference.
|
|
56
|
+
*
|
|
57
|
+
* Bounded by construction: exactly the two weights this package registers, no
|
|
58
|
+
* fan-out over a caller-supplied list. A consumer that registers additional
|
|
59
|
+
* families is responsible for warming them the same way.
|
|
60
|
+
*/
|
|
61
|
+
declare function loadDefaultFonts(): Promise<void>;
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/types.d.ts
|
|
64
|
+
/**
|
|
65
|
+
* Document-level metadata pinned onto every render.
|
|
66
|
+
*
|
|
67
|
+
* Everything here exists to remove *gratuitous* non-determinism from the
|
|
68
|
+
* output. It is deliberately small: this package renders a React tree, it does
|
|
69
|
+
* not model documents.
|
|
70
|
+
*/
|
|
71
|
+
interface PdfRenderOptions {
|
|
72
|
+
/**
|
|
73
|
+
* Pinned document timestamps. REQUIRED — callers must pass a deliberate value.
|
|
74
|
+
* There is no wall-clock default: an implicit `new Date()` is the single biggest
|
|
75
|
+
* source of gratuitous non-determinism, and a caller that wants "now" should say so.
|
|
76
|
+
*/
|
|
77
|
+
readonly creationDate: Date;
|
|
78
|
+
readonly modificationDate?: Date;
|
|
79
|
+
readonly producer?: string;
|
|
80
|
+
readonly creator?: string;
|
|
81
|
+
}
|
|
82
|
+
interface PdfRenderResult {
|
|
83
|
+
readonly bytes: Uint8Array;
|
|
84
|
+
/**
|
|
85
|
+
* Lowercase hex SHA-256 of `bytes`, computed over exactly what is returned.
|
|
86
|
+
*
|
|
87
|
+
* ## This digest identifies THESE BYTES — it is not a reproducibility claim
|
|
88
|
+
*
|
|
89
|
+
* Re-rendering the same React tree with the same {@link PdfRenderOptions} is
|
|
90
|
+
* **not** expected to reproduce this digest, and **re-render-and-compare is
|
|
91
|
+
* therefore not a valid integrity check**. Doing so will report a false
|
|
92
|
+
* mismatch on a document that was never touched.
|
|
93
|
+
*
|
|
94
|
+
* The concrete reason: `@react-pdf/renderer` vendors a fork of pdfkit whose
|
|
95
|
+
* font-subset tag (the six-letter `ABCDEF+` prefix on `/BaseFont`) comes from
|
|
96
|
+
* `Math.random()`. Upstream pdfkit derives that tag from the font's object id;
|
|
97
|
+
* the fork has not picked the change up. We deliberately do **not** patch or
|
|
98
|
+
* seed the global RNG to work around it — see
|
|
99
|
+
* `plan/rent/decisions/D028-pdf-package-and-engine.md`.
|
|
100
|
+
*
|
|
101
|
+
* The policy, per `plan/rent/research/R06-contract-legal-and-pdf.md` §2:
|
|
102
|
+
* **hash on write, persist the exact bytes, never re-render for comparison.**
|
|
103
|
+
* That is also the legally correct model and matches universal industry
|
|
104
|
+
* practice — ISO 32000-1 §14.4 states outright that *"the calculation of the
|
|
105
|
+
* file identifier need not be reproducible"*, and no e-signature vendor
|
|
106
|
+
* re-renders from source to verify; they all hash the stored bytes.
|
|
107
|
+
*
|
|
108
|
+
* To verify a stored document: hash the stored bytes and compare against the
|
|
109
|
+
* digest recorded at write time. Never regenerate the document.
|
|
110
|
+
*/
|
|
111
|
+
readonly sha256: string;
|
|
112
|
+
readonly byteLength: number;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The engine seam. One implementation ships today
|
|
116
|
+
* (`createReactPdfRenderer`); the interface exists so a swap costs one
|
|
117
|
+
* package rather than every consumer.
|
|
118
|
+
*/
|
|
119
|
+
interface PdfRenderer {
|
|
120
|
+
render(document: ReactElement, options: PdfRenderOptions): Promise<PdfRenderResult>;
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/react-pdf-renderer.d.ts
|
|
124
|
+
/**
|
|
125
|
+
* The `@react-pdf/renderer` implementation of {@link PdfRenderer}.
|
|
126
|
+
*
|
|
127
|
+
* Stateless and cheap to construct — react-pdf's font store is a module
|
|
128
|
+
* singleton, so several renderers share one registration. Renders happen
|
|
129
|
+
* entirely in-process: no browser, no sidecar, no network (the fonts ship
|
|
130
|
+
* inside this package), and the document never executes script.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* const renderer = createReactPdfRenderer()
|
|
135
|
+
* const { bytes, sha256 } = await renderer.render(<Document>…</Document>, {
|
|
136
|
+
* creationDate: signedAt, // a deliberate instant, never `new Date()` by default
|
|
137
|
+
* })
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
declare function createReactPdfRenderer(): PdfRenderer;
|
|
141
|
+
//#endregion
|
|
142
|
+
export { DEFAULT_FONT_FAMILY, NOTO_SANS_FILES, NOTO_SANS_LICENSE_FILE, type PdfRenderOptions, type PdfRenderResult, type PdfRenderer, createReactPdfRenderer, loadDefaultFonts, registerDefaultFonts, sha256Hex };
|
|
143
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/digest.ts","../src/fonts.ts","../src/types.ts","../src/react-pdf-renderer.ts"],"mappings":";;;;;;AAWA;;;;AAA2C;;iBAA3B,SAAA,CAAU,KAAiB,EAAV,UAAU;;;;;;AAA3C;;;;AAA2C;;;cCG9B,mBAAA;AAAb;;;;AAAA,cAiBa,eAAA;EAAA,SAGH,OAAA;EAAA,SAAA,IAAA;AAAA;;cAGG,sBAAA;AAAb;;;;AAA+D;AAU/D;;;AAVA,iBAUgB,oBAAA,CAAA;AAAoB;AA2BpC;;;;AAAiD;;;;ACjEjD;;;;;;ADsCoC,iBA2Bd,gBAAA,CAAA,GAAoB,OAAO;;;;;AD/DjD;;;;AAA2C;UEF1B,gBAAA;;;ADKjB;;;WCCW,YAAA,EAAc,IAAA;EAAA,SACd,gBAAA,GAAmB,IAAI;EAAA,SACvB,QAAA;EAAA,SACA,OAAA;AAAA;AAAA,UAGM,eAAA;EAAA,SACN,KAAA,EAAO,UAAU;EDef;;;;AAAkD;AAU/D;;;;AAAoC;AA2BpC;;;;AAAiD;;;;ACjEjD;;;;;;;;;ED4Ba,SCaF,MAAA;EAAA,SACA,UAAA;AAAA;AA9BX;;;;;AAAA,UAsCiB,WAAA;EACf,MAAA,CAAO,QAAA,EAAU,YAAA,EAAc,OAAA,EAAS,gBAAA,GAAmB,OAAA,CAAQ,eAAA;AAAA;;;;;AFjDrE;;;;AAA2C;;;;ACG3C;;;;AAAgC;AAiBhC;iBEyBgB,sBAAA,CAAA,GAA0B,WAAW"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createHash as e}from"node:crypto";import{dirname as t,join as n}from"node:path";import{fileURLToPath as r}from"node:url";import{Document as i,Font as a,renderToBuffer as o}from"@react-pdf/renderer";import{cloneElement as s,isValidElement as c}from"react";function l(t){return e(`sha256`).update(t).digest(`hex`)}const u=`Noto Sans`,d=n(t(r(import.meta.url)),`..`,`fonts`),f={regular:n(d,`NotoSans-Regular.ttf`),bold:n(d,`NotoSans-Bold.ttf`)},p=n(d,`OFL.txt`);function m(){a.getRegisteredFontFamilies().includes(`Noto Sans`)||a.register({family:u,fonts:[{src:f.regular,fontWeight:400},{src:f.bold,fontWeight:700}]})}async function h(){m(),await Promise.all([a.load({fontFamily:u,fontWeight:400}),a.load({fontFamily:u,fontWeight:700})])}function g(e,t){if(!c(e)||e.type!==i)throw TypeError(`The root element passed to PdfRenderer.render must be a <Document> from @murumets-ee/pdf/primitives. Cloning any other element would attach the pinned creationDate/producer/creator to props that nothing reads, silently producing a document stamped with the wall-clock time instead.`);return s(e,{creationDate:t.creationDate,...t.modificationDate!==void 0&&{modificationDate:t.modificationDate},...t.producer!==void 0&&{producer:t.producer},...t.creator!==void 0&&{creator:t.creator}})}function _(){return{async render(e,t){await h();let n=await o(g(e,t));return{bytes:n,sha256:l(n),byteLength:n.byteLength}}}}export{u as DEFAULT_FONT_FAMILY,f as NOTO_SANS_FILES,p as NOTO_SANS_LICENSE_FILE,_ as createReactPdfRenderer,h as loadDefaultFonts,m as registerDefaultFonts,l as sha256Hex};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/digest.ts","../src/fonts.ts","../src/react-pdf-renderer.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\n\n/**\n * Lowercase hex SHA-256 of exactly the bytes in `bytes`.\n *\n * Respects `byteOffset` / `byteLength`, so a `Buffer` that is a window into a\n * larger pooled `ArrayBuffer` hashes its own contents and nothing else.\n *\n * See {@link PdfRenderResult.sha256} for what this digest does and does not\n * attest to — it identifies a specific byte sequence, not a rendering input.\n */\nexport function sha256Hex(bytes: Uint8Array): string {\n return createHash('sha256').update(bytes).digest('hex')\n}\n","import { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { Font } from '@react-pdf/renderer'\n\n/**\n * The one font family this package registers.\n *\n * **Noto Sans** (SIL Open Font License 1.1). A single file covers Latin,\n * Latin Extended (Estonian `õäöüšž`), Greek and Cyrillic — these are the\n * `notofonts/latin-greek-cyrillic` static builds, verified glyph-by-glyph in\n * `fonts.test.ts`. That matters because `@react-pdf/renderer` has no\n * `unicode-range` mechanism: a per-script split would need a fallback chain it\n * cannot express, and missing glyphs render as `.notdef` **silently**.\n */\nexport const DEFAULT_FONT_FAMILY = 'Noto Sans'\n\n/**\n * Package-relative, resolved from this module's own URL rather than `cwd`.\n *\n * `src/fonts.ts` and the bundled `dist/*.mjs` sit at the same depth, so\n * `../fonts` is correct under both vitest (source) and a published install\n * (dist). Built with `fileURLToPath` + `join` on purpose — a bundler rewrites\n * `new URL('...', import.meta.url)` into an asset reference, which would break\n * the published path.\n */\nconst FONT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'fonts')\n\n/**\n * Absolute paths to the bundled font files. They ship **inside the package**;\n * a render never fetches a font over the network.\n */\nexport const NOTO_SANS_FILES = {\n regular: join(FONT_DIR, 'NotoSans-Regular.ttf'),\n bold: join(FONT_DIR, 'NotoSans-Bold.ttf'),\n} as const\n\n/** The SIL Open Font License 1.1 text that covers {@link NOTO_SANS_FILES}. */\nexport const NOTO_SANS_LICENSE_FILE = join(FONT_DIR, 'OFL.txt')\n\n/**\n * Register {@link DEFAULT_FONT_FAMILY} on react-pdf's font store.\n *\n * Idempotent: react-pdf's store is a module singleton, and `register` appends\n * to a family's source list rather than replacing it, so a naive repeat call\n * would stack duplicates. The guard reads the store instead of tracking local\n * state, so it stays correct across a caller's `Font.reset()`.\n */\nexport function registerDefaultFonts(): void {\n if (Font.getRegisteredFontFamilies().includes(DEFAULT_FONT_FAMILY)) return\n\n Font.register({\n family: DEFAULT_FONT_FAMILY,\n fonts: [\n { src: NOTO_SANS_FILES.regular, fontWeight: 400 },\n { src: NOTO_SANS_FILES.bold, fontWeight: 700 },\n ],\n })\n}\n\n/**\n * Register the default family AND read both weights off disk before returning.\n *\n * This is a determinism fix, not a performance one. react-pdf loads a font\n * lazily, mid-layout, the first time a glyph needs it — and that extra `await`\n * changes the order in which pdfkit emits objects, so the FIRST render in a\n * process produces a different object numbering (and different xref offsets)\n * than every render after it. Measured: renders 2/3/4 were byte-identical once\n * the subset tag is normalised away, render 1 was not. Pre-loading collapses\n * that difference.\n *\n * Bounded by construction: exactly the two weights this package registers, no\n * fan-out over a caller-supplied list. A consumer that registers additional\n * families is responsible for warming them the same way.\n */\nexport async function loadDefaultFonts(): Promise<void> {\n registerDefaultFonts()\n\n await Promise.all([\n Font.load({ fontFamily: DEFAULT_FONT_FAMILY, fontWeight: 400 }),\n Font.load({ fontFamily: DEFAULT_FONT_FAMILY, fontWeight: 700 }),\n ])\n}\n","import { Document, type DocumentProps, renderToBuffer } from '@react-pdf/renderer'\nimport { type ReactElement, cloneElement, isValidElement } from 'react'\nimport { sha256Hex } from './digest.js'\nimport { loadDefaultFonts } from './fonts.js'\nimport type { PdfRenderOptions, PdfRenderResult, PdfRenderer } from './types.js'\n\n/**\n * Attach the caller's pinned metadata to the root `<Document>`.\n *\n * Everything downstream follows from these four props: pdfkit derives `/ID`\n * from `md5(info)` (not from a random source) and the XMP `xmp:CreateDate`\n * from `info.CreationDate`, so pinning the dates pins those too. Left\n * unpinned, react-pdf substitutes `new Date()` plus `producer`/`creator` of\n * `'react-pdf'` — the gratuitous non-determinism this package exists to\n * remove.\n *\n * Fields the caller omitted are omitted here as well, so react-pdf's own\n * defaults apply rather than a value this package invented.\n */\nfunction pinDocumentMetadata(\n document: ReactElement,\n options: PdfRenderOptions,\n): ReactElement<DocumentProps> {\n if (!isValidElement(document) || document.type !== Document) {\n throw new TypeError(\n 'The root element passed to PdfRenderer.render must be a <Document> from ' +\n '@murumets-ee/pdf/primitives. Cloning any other element would attach the pinned ' +\n 'creationDate/producer/creator to props that nothing reads, silently producing a ' +\n 'document stamped with the wall-clock time instead.',\n )\n }\n\n return cloneElement(document as ReactElement<DocumentProps>, {\n creationDate: options.creationDate,\n ...(options.modificationDate !== undefined && { modificationDate: options.modificationDate }),\n ...(options.producer !== undefined && { producer: options.producer }),\n ...(options.creator !== undefined && { creator: options.creator }),\n })\n}\n\n/**\n * The `@react-pdf/renderer` implementation of {@link PdfRenderer}.\n *\n * Stateless and cheap to construct — react-pdf's font store is a module\n * singleton, so several renderers share one registration. Renders happen\n * entirely in-process: no browser, no sidecar, no network (the fonts ship\n * inside this package), and the document never executes script.\n *\n * @example\n * ```ts\n * const renderer = createReactPdfRenderer()\n * const { bytes, sha256 } = await renderer.render(<Document>…</Document>, {\n * creationDate: signedAt, // a deliberate instant, never `new Date()` by default\n * })\n * ```\n */\nexport function createReactPdfRenderer(): PdfRenderer {\n return {\n async render(document: ReactElement, options: PdfRenderOptions): Promise<PdfRenderResult> {\n // Awaited BEFORE the render, not during it — see `loadDefaultFonts`.\n await loadDefaultFonts()\n\n const buffer = await renderToBuffer(pinDocumentMetadata(document, options))\n // `Buffer` IS a `Uint8Array`; no copy, and the digest below covers this\n // exact view (offset + length), not the whole backing ArrayBuffer.\n const bytes: Uint8Array = buffer\n\n return {\n bytes,\n sha256: sha256Hex(bytes),\n byteLength: bytes.byteLength,\n }\n },\n }\n}\n"],"mappings":"sQAWA,SAAgB,EAAU,EAA2B,CACnD,OAAO,EAAW,QAAQ,CAAC,CAAC,OAAO,CAAK,CAAC,CAAC,OAAO,KAAK,CACxD,CCCA,MAAa,EAAsB,YAW7B,EAAW,EAAK,EAAQ,EAAc,OAAO,KAAK,GAAG,CAAC,EAAG,KAAM,OAAO,EAM/D,EAAkB,CAC7B,QAAS,EAAK,EAAU,sBAAsB,EAC9C,KAAM,EAAK,EAAU,mBAAmB,CAC1C,EAGa,EAAyB,EAAK,EAAU,SAAS,EAU9D,SAAgB,GAA6B,CACvC,EAAK,0BAA0B,CAAC,CAAC,SAAA,WAA4B,GAEjE,EAAK,SAAS,CACZ,OAAQ,EACR,MAAO,CACL,CAAE,IAAK,EAAgB,QAAS,WAAY,GAAI,EAChD,CAAE,IAAK,EAAgB,KAAM,WAAY,GAAI,CAC/C,CACF,CAAC,CACH,CAiBA,eAAsB,GAAkC,CACtD,EAAqB,EAErB,MAAM,QAAQ,IAAI,CAChB,EAAK,KAAK,CAAE,WAAY,EAAqB,WAAY,GAAI,CAAC,EAC9D,EAAK,KAAK,CAAE,WAAY,EAAqB,WAAY,GAAI,CAAC,CAChE,CAAC,CACH,CC9DA,SAAS,EACP,EACA,EAC6B,CAC7B,GAAI,CAAC,EAAe,CAAQ,GAAK,EAAS,OAAS,EACjD,MAAU,UACR,2RAIF,EAGF,OAAO,EAAa,EAAyC,CAC3D,aAAc,EAAQ,aACtB,GAAI,EAAQ,mBAAqB,IAAA,IAAa,CAAE,iBAAkB,EAAQ,gBAAiB,EAC3F,GAAI,EAAQ,WAAa,IAAA,IAAa,CAAE,SAAU,EAAQ,QAAS,EACnE,GAAI,EAAQ,UAAY,IAAA,IAAa,CAAE,QAAS,EAAQ,OAAQ,CAClE,CAAC,CACH,CAkBA,SAAgB,GAAsC,CACpD,MAAO,CACL,MAAM,OAAO,EAAwB,EAAqD,CAExF,MAAM,EAAiB,EAKvB,IAAM,EAAoB,MAHL,EAAe,EAAoB,EAAU,CAAO,CAAC,EAK1E,MAAO,CACL,QACA,OAAQ,EAAU,CAAK,EACvB,WAAY,EAAM,UACpB,CACF,CACF,CACF"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Document, Font, Image, Page, StyleSheet, StyleSheet as StyleSheet$1, Text, View } from "@react-pdf/renderer";
|
|
2
|
+
|
|
3
|
+
//#region src/primitives.d.ts
|
|
4
|
+
/** The map shape accepted by `StyleSheet.create`. */
|
|
5
|
+
type Styles = Parameters<typeof StyleSheet$1.create>[0];
|
|
6
|
+
/** A single style object — what a `style` prop takes. */
|
|
7
|
+
type Style = Styles[string];
|
|
8
|
+
//#endregion
|
|
9
|
+
export { Document, Font, Image, Page, Style, StyleSheet, Styles, Text, View };
|
|
10
|
+
//# sourceMappingURL=primitives.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"primitives.d.mts","names":[],"sources":["../src/primitives.ts"],"mappings":";;;AAkBA;AAAA,KAHY,MAAA,GAAS,UAAU,QAAQ,YAAA,CAAe,MAAA;;KAG1C,KAAA,GAAQ,MAAM"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Document as e,Font as t,Image as n,Page as r,StyleSheet as i,Text as a,View as o}from"@react-pdf/renderer";export{e as Document,t as Font,n as Image,r as Page,i as StyleSheet,a as Text,o as View};
|
|
Binary file
|
|
Binary file
|
package/fonts/OFL.txt
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
https://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@murumets-ee/pdf",
|
|
3
|
+
"version": "0.38.0",
|
|
4
|
+
"license": "Elastic-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.mts",
|
|
9
|
+
"import": "./dist/index.mjs"
|
|
10
|
+
},
|
|
11
|
+
"./primitives": {
|
|
12
|
+
"types": "./dist/primitives.d.mts",
|
|
13
|
+
"import": "./dist/primitives.mjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"fonts"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@react-pdf/renderer": "^4.5.1"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"react": ">=19.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/fontkit": "^2.0.9",
|
|
28
|
+
"@types/node": "^20.19.42",
|
|
29
|
+
"@types/react": "^19.2.17",
|
|
30
|
+
"fontkit": "^2.0.4",
|
|
31
|
+
"react": "19.2.7",
|
|
32
|
+
"tsdown": "^0.22.2",
|
|
33
|
+
"typescript": "^5.7.3",
|
|
34
|
+
"vitest": "^2.1.8"
|
|
35
|
+
},
|
|
36
|
+
"typeCoverage": {
|
|
37
|
+
"atLeast": 100
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsdown",
|
|
41
|
+
"dev": "tsdown --watch",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:watch": "vitest"
|
|
44
|
+
}
|
|
45
|
+
}
|