@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 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;;;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
+ }