@murumets-ee/pdf 0.0.0-preview-20261014110621

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 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.
@@ -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;;;cCI9B,mBAAA;AAAb;;;;AAAA,cAiBa,eAAA;EAAA,SAGH,OAAA;EAAA,SAAA,IAAA;AAAA;;cAGG,sBAAA;AAAb;;;;AAA+D;AAU/D;;;AAVA,iBAUgB,oBAAA;AAAoB;AAuCpC;;;;AAAiD;;;;AC9EjD;;;;;;ADuCoC,iBAuCd,gBAAA,IAAoB,OAAO;;;;;AD5EjD;;;;AAA2C;UEF1B,gBAAA;;;ADMjB;;;WCAW,YAAA,EAAc,IAAA;EAAA,SACd,gBAAA,GAAmB,IAAI;EAAA,SACvB,QAAA;EAAA,SACA,OAAA;AAAA;AAAA,UAGM,eAAA;EAAA,SACN,KAAA,EAAO,UAAU;EDgBf;;;;AAAkD;AAU/D;;;;AAAoC;AAuCpC;;;;AAAiD;;;;AC9EjD;;;;;;;;;ED6Ba,SCYF,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;;;;ACI3C;;;;AAAgC;AAiBhC;iBEwBgB,sBAAA,IAA0B,WAAW"}