@openpresentation/opf-render 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,45 @@
1
1
  # OPF Render
2
2
 
3
- Version 0.7.0 requires core 0.9.0 and renders accepted quote and code geometry without fitting it again. Code filename/language/body parts preserve source whitespace, literal tabs and metadata case and expose trace targets for editing. The [43 reviewed code raster changes](docs/evidence/shared-code/raster-review.json) retain the other 762 corpus hashes. Planned coordinated releases PPTX 0.7.0 and editor 0.6.0 will add native source recovery and editing. Glyph containment and separation do not establish native pixel equivalence.
3
+ Version 0.8.1 requires core 0.10.1, including metric, quote and timeline layout placeholders and the corrected text-bullet contract. It retains the existing rendering APIs.
4
+
5
+ Unfinished prepared shaping work is preserved in the [September 15 roadmap](docs/roadmap-shaping-20260915.md); it is not part of the published runtime.
6
+
7
+ Version 0.8.0 and this checkout require Node 24 (`24.x`). Use `.nvmrc` for local development. Earlier published versions retain their original engine declarations. Browser entrypoints remain browser-safe; native application compatibility is verified separately.
8
+
9
+ Version 0.8.0 adds optional font-registry vector outlines and shared heading/scalar/rich line placement. Pass identical `textMeasurement` and `textRasterPadding` options to preview, pagination and export; padding defaults to one scaled reference pixel. SVG consumes accepted origins without remeasuring. See the [source contract and limits](https://github.com/OpenPresentation/opf/blob/f94125a1ff95bf0974a055fe1c081d348dad54f2/docs/plans/text-placement.md). Native raster certification remains separate.
10
+
11
+ Version 0.8.0 requires core 0.10.0 and renders accepted quote and code geometry without fitting it again. Code filename/language/body parts preserve source whitespace, literal tabs and metadata case and expose trace targets for editing. The [43 reviewed code raster changes](docs/evidence/shared-code/raster-review.json) retain the other 762 corpus hashes. Coordinated releases PPTX 0.8.0 and editor 0.7.0 add native source recovery and editing. Glyph containment and separation do not establish native pixel equivalence.
4
12
 
5
13
  Deterministic local renderer for Open Presentation Format documents. The shared SVG core implements validation, catalog resolution, placeholder binding and text layout. Node APIs additionally convert SVG to PNG and raster-backed PDF.
6
14
 
15
+ In version 0.8.0, `design.contentBox` uses core's shared padded geometry. The card renders at `item.frameBox`; its payload uses `item.box` and accepted internals. Card padding participates in composition scoring, strict overflow and pagination. This requires core 0.10.0.
16
+
17
+ Version 0.8.0 paints plain and rich text, titles, subtitles and tags using composition's accepted fits and resolved styles. Painting does not measure those payloads again. Supply a font registry during composition: reusing an estimated fit cannot correct spacing when the actual drawing font has different advances.
18
+
19
+ Version 0.8.0 includes `prepareNodeFonts` in `/fonts-node`. It verifies all selected font files and license notices against the versioned `BUNDLED_FONT_MANIFEST`, including exact npm versions and SHA-256 hashes. The returned options configure layout, SVG, editing, PPTX, and Node raster output with the same font inputs:
20
+
21
+ ```js
22
+ import { prepareNodeFonts } from '@openpresentation/opf-render/fonts-node';
23
+ import { renderSvgDeck, svgToPng } from '@openpresentation/opf-render';
24
+ import { paginatePresentation } from '@openpresentation/opf/pagination';
25
+ import { toPptx } from '@openpresentation/opf-pptx';
26
+
27
+ const { registry, options } = await prepareNodeFonts({
28
+ pack: 'office', substitutionPolicy: 'visual',
29
+ });
30
+ const { presentation } = paginatePresentation(opf, options);
31
+ const slides = renderSvgDeck(presentation, options);
32
+ const png = await svgToPng(slides[0], options);
33
+ const pptx = await toPptx(presentation, options);
34
+ console.log(registry.substitutions);
35
+ ```
36
+
37
+ The default `pack: 'base'` contains nine Roboto/Roboto Mono faces and suits a document using `design.fontScheme: 'roboto'`. The Office pack includes 24 additional faces and defaults to metric substitution policy; visual substitution remains explicit. Neither helper changes the document's authored font scheme or installs system fonts. Missing resources, changed bytes/notices, and unexpected package versions fail with actionable font errors. Requested/resolved substitution records remain on `registry.substitutions`; source paths appear where the caller supplies them. Font coverage, variant naming, shaping, and native fidelity retain their documented limits.
38
+
39
+ Default PNG/PDF raster loading now uses the same complete nine-face base pack, fixing omitted semibold and italic faces. Custom raster callers may still set `useBundledFonts: false`. Font files must stay available and unchanged for subsequent raster calls. `node scripts/update-font-manifest.mjs` is an explicit maintenance operation requiring review of font bytes, style metadata, licenses and raster changes; builds and installs never regenerate the expected hashes.
40
+
41
+ Version 0.8.0 resolves OpenType preferred-family groups, so a Roboto request at 500, 600 or 800 selects the installed Medium, SemiBold or ExtraBold face. Explicit caller family renames keep their own namespace; ambiguous grouped faces reject. Resolved styles also carry optional `fontFace` metadata with the physical legacy family and its bold/italic style-link flags. Coordinated PPTX export consumes those flags independently of numeric weight. This prevents requesting a second bold style from an already named ExtraBold/SemiBold family. Browser CSS and native selectors retain their respective family names; no font is synthesized or installed by this lookup. See [exact-weight evidence and limits](docs/evidence/font-variants/README.md).
42
+
7
43
  Code strings that [XML 1.0 cannot represent](https://www.w3.org/TR/xml/#charsets) reject rendering with `invalid-code-text`, the OPF field path and UTF-16 offset. Input JSON stays unchanged. Tabs, line endings and valid supplementary Unicode remain accepted; schema validity and XML serialization do not certify font coverage or native fidelity.
8
44
 
9
45
  ## Scope
@@ -18,20 +54,36 @@ Code strings that [XML 1.0 cannot represent](https://www.w3.org/TR/xml/#charsets
18
54
 
19
55
  ```js
20
56
  import { renderSvgDeck } from "@openpresentation/opf-render";
57
+ import { loadOfficeFontRegistry } from "@openpresentation/opf-render/fonts-node";
21
58
 
22
- const svgs = renderSvgDeck(opf, { trace: true });
59
+ const fonts = await loadOfficeFontRegistry({ substitutionPolicy: "visual" });
60
+ const svgs = renderSvgDeck(opf, {
61
+ trace: true,
62
+ textMeasurement: fonts.textMeasurement,
63
+ embeddedFonts: fonts.embeddedFonts,
64
+ });
65
+ console.log(fonts.substitutions);
23
66
  ```
24
67
 
25
68
  Set `trace: true` to stamp rendered SVG elements with `data-opf-path` values such as `slides.0.title`; omit it for smaller production SVG.
26
69
 
70
+ The installed open font pack works offline. This example explicitly permits visual substitutes such as Carlito for Aptos; the substitution report identifies requested and resolved faces. A visual substitute is not a claim of metric or pixel equivalence. Omit `substitutionPolicy` to restrict the Office loader to its metric mappings, or supply your own licensed faces. Missing fonts/glyphs fail explicitly. Without `textMeasurement`, synchronous SVG APIs use estimated widths; named fonts alone do not make rich-run spacing reliable. Browser hosts should load the same bytes using the loader below.
71
+
72
+ Unresolved images produce an `unresolved-asset` diagnostic through `onDiagnostic`, including the OPF path, reason and complete description. The fallback shows a bounded status label when it fits above the selected readability floor, otherwise an icon with the full accessible description. It preserves authored opacity, including faint decorative watermarks; a missing image is still missing even when its fallback fits. Use `strictAssets: true` to reject unresolved images. Supply embedded raster data URIs or a synchronous host `imageResolver` to resolve them; the renderer does not fetch URLs or read local paths. Caller descriptions and other metadata override referenced asset metadata through alias chains. These diagnostics identify unresolved sources, not malformed image bytes or native PowerPoint compatibility.
73
+
74
+ Header/footer images and watermarks fit their complete artwork within their allocated regions. `design.imageFill: "crop"` continues to crop content picture placeholders; background images retain their own fit policy.
75
+
27
76
  PNG and PDF conversion APIs are async because they load the local raster/PDF engines on demand:
28
77
 
29
78
  ```js
30
79
  import { renderSvgDeck, svgToPdf, svgToPng } from "@openpresentation/opf-render";
80
+ import { loadOfficeFontRegistry } from "@openpresentation/opf-render/fonts-node";
31
81
 
32
- const svgs = renderSvgDeck(opf);
33
- const png = await svgToPng(svgs[0], { scale: 1 });
34
- const pdf = await svgToPdf(svgs, { scale: 1 });
82
+ const fonts = await loadOfficeFontRegistry({ substitutionPolicy: "visual" });
83
+ const svgs = renderSvgDeck(opf, { textMeasurement: fonts.textMeasurement });
84
+ const rasterOptions = { scale: 1, fontFiles: fonts.fontFiles, useBundledFonts: false, loadSystemFonts: false };
85
+ const png = await svgToPng(svgs[0], rasterOptions);
86
+ const pdf = await svgToPdf(svgs, rasterOptions);
35
87
  ```
36
88
 
37
89
  `svgToPng` returns PNG bytes for one SVG. `svgToPdf` accepts one SVG or an array of SVGs and returns PDF bytes with one slide per page. The SVG page `width`/`height` or `viewBox` determines the PDF page size; `scale` controls raster density only.
@@ -86,6 +138,8 @@ npm run validate
86
138
  npm test
87
139
  ```
88
140
 
141
+ The accepted-text regression `npm run test:text` checks 24 combinations of dimensions, alignment, cards and formatting, including exact accepted positions/styles and strict overflow. `npm run test:rich-spacing-browser -- <new-output-directory>` compares two unchanged gallery slides with identical open font bytes. `npm run test:text-browser -- <new-output-directory>` also checks actual text paint: it currently exits 1 for four portrait/right title cases with one painted pixel beyond the 0.1-pixel cell tolerance in the recorded Windows Edge environment. Its advance/origin checks pass. Font text rectangles, painted-pixel containment, source whitespace and native fidelity are separate results; that historical plain fitting normalized whitespace. Later source-whitespace evidence below supersedes that behavior and has its own reviewed corpus checkpoint.
142
+
89
143
  When this repo is checked out beside `openpresentation/opf`, `npm test` also renders every `examples/**/*.opf.json` deck twice and asserts byte-identical SVG output. Golden PNG drift is checked on every run against all 805 slides from the installed core package, identified by a content digest. Changed or missing corpora fail; Git history cannot skip the gate. To generate a review candidate after an intentional visual change:
90
144
 
91
145
  ```sh
@@ -122,7 +176,7 @@ Version 0.1.1 adds rich-text line offsets and measured boxes to opt-in SVG traci
122
176
 
123
177
  PNG and PDF conversion now decode embedded WebP image data URIs to static PNG before rasterization. This preserves fit/crop, transparency, EXIF orientation and the first animation frame. The source SVG and OPF document remain unchanged; browser SVG output keeps the original embedded image. Both href and legacy xlink:href are supported, including base64 and percent-encoded data. This step does not resolve local filenames, download remote images or change the existing host imageResolver contract.
124
178
 
125
- The Node-only raster path lazily loads pinned Sharp 0.35.4, requiring Node 20.9 or later and normal installation of its optional platform binaries. Browser exports do not include the decoder. Malformed embedded WebP produces image-conversion-failed with its data-opf-path when present, otherwise svg.images.N. Decoding has a 40-megapixel limit. PNG/PDF are static outputs; animation beyond the first frame and original image metadata are not retained.
179
+ The Node-only raster path lazily loads pinned Sharp 0.35.4, requiring Node 24 for this package and normal installation of its optional platform binaries. Browser exports do not include the decoder. Malformed embedded WebP produces image-conversion-failed with its data-opf-path when present, otherwise svg.images.N. Decoding has a 40-megapixel limit. PNG/PDF are static outputs; animation beyond the first frame and original image metadata are not retained.
126
180
 
127
181
  Regression checks compare six fixtures against independent Pillow pixels, verify fit/crop, inspect actual PDF image streams and transparency masks, and exercise input encodings and diagnostics. All 805 existing raster baselines remain unchanged. Test fixtures are project-authored and MIT licensed; the decoder and its bundled codecs retain their upstream licenses.
128
182
 
@@ -147,3 +201,5 @@ Renderer 0.5.0 requires core 0.7.0 and uses its shared styled-cell geometry. A c
147
201
  Every covered position is explicit `null`. Each merged anchor renders once; traced text uses its `.value` path for editing. Styles support RGB/RGBA fills and text colors, horizontal and vertical alignment, reference-pixel padding and per-edge solid/dashed/dotted borders. Rich-run colors override the cell text color. Columns remain equal width; rows grow from measured text, padding and span constraints. Pagination preserves connected vertical merge groups.
148
202
 
149
203
  The Node20/24 renderer suite retains all 805 existing raster baselines. Browser fixtures verify merged text containment and styled-cell typing, formatting, cancellation and undo with actual loaded fonts. Native PowerPoint appearance remains a separate converter verification boundary.
204
+
205
+ Version 0.8.0 preserves scalar spaces, literal tabs and exact hard-line source ranges across core layout and SVG. Explicit tab positions and whitespace-preserving SVG text prevent source collapse. Its separate 805-slide checkpoint follows review of all 279 changed slides in 35 paired sheets and six full-size pairs; historical baselines remain retained. See [the review and bounds](docs/evidence/source-whitespace-corpus-review/README.md). Estimated wrapping/advances, multilingual shaping and native Office remain separate gates.
@@ -0,0 +1,326 @@
1
+ // Generated deliberately by scripts/update-font-manifest.mjs; never during build/install.
2
+ const freeze=value=>{if(value&&typeof value==='object'){for(const child of Object.values(value))freeze(child);Object.freeze(value);}return value;};
3
+ export const BUNDLED_FONT_MANIFEST=freeze({
4
+ "version": 1,
5
+ "packages": [
6
+ {
7
+ "name": "@expo-google-fonts/roboto",
8
+ "version": "0.4.3",
9
+ "pack": "base",
10
+ "source": "https://www.npmjs.com/package/@expo-google-fonts/roboto/v/0.4.3",
11
+ "license": "OFL-1.1",
12
+ "licenseFile": "LICENSE_FONT",
13
+ "licenseSha256": "061402327a96aadb0bfb694a960ed289ecd38d383e396243831ab81feb109c41",
14
+ "faces": [
15
+ {
16
+ "file": "400Regular/Roboto_400Regular.ttf",
17
+ "family": "Roboto",
18
+ "weight": 400,
19
+ "italic": false,
20
+ "sha256": "15256405ecb0d880678833a582760efad538ab2932318b52c8105b267d159459"
21
+ },
22
+ {
23
+ "file": "500Medium/Roboto_500Medium.ttf",
24
+ "family": "Roboto Medium",
25
+ "weight": 500,
26
+ "italic": false,
27
+ "sha256": "79343dc641a2e8d48a703a4667b71fc8dc6565cc8ce47dfc34fc52c12aadbf38"
28
+ },
29
+ {
30
+ "file": "600SemiBold/Roboto_600SemiBold.ttf",
31
+ "family": "Roboto SemiBold",
32
+ "weight": 600,
33
+ "italic": false,
34
+ "sha256": "32fe2086b0e54eca85a68eb74e3d89bb16a7ffe59085d6dc7d079f5b3c26d6ab"
35
+ },
36
+ {
37
+ "file": "700Bold/Roboto_700Bold.ttf",
38
+ "family": "Roboto",
39
+ "weight": 700,
40
+ "italic": false,
41
+ "sha256": "4aaf8c5b661a386998c2e70cf2b87e2440f5404e0b8fd81164f0413fb3435ec6"
42
+ },
43
+ {
44
+ "file": "800ExtraBold/Roboto_800ExtraBold.ttf",
45
+ "family": "Roboto ExtraBold",
46
+ "weight": 800,
47
+ "italic": false,
48
+ "sha256": "43d5ce98c58336045220fa1dac2bd22281b1aab3eca66785cb34eb3ad1dfe1a5"
49
+ },
50
+ {
51
+ "file": "400Regular_Italic/Roboto_400Regular_Italic.ttf",
52
+ "family": "Roboto",
53
+ "weight": 400,
54
+ "italic": true,
55
+ "sha256": "6a372c348dc20c246b92d60b9bda3c0a9ffedc129790ed3f31faf89daa99d793"
56
+ },
57
+ {
58
+ "file": "700Bold_Italic/Roboto_700Bold_Italic.ttf",
59
+ "family": "Roboto",
60
+ "weight": 700,
61
+ "italic": true,
62
+ "sha256": "c6a5e7c224d3bf6698e5d3c124f0f0220cad20d5d413920447a96f033a0292ee"
63
+ }
64
+ ]
65
+ },
66
+ {
67
+ "name": "@expo-google-fonts/roboto-mono",
68
+ "version": "0.4.2",
69
+ "pack": "base",
70
+ "source": "https://www.npmjs.com/package/@expo-google-fonts/roboto-mono/v/0.4.2",
71
+ "license": "OFL-1.1",
72
+ "licenseFile": "LICENSE_FONT",
73
+ "licenseSha256": "50ab8dd54680d3473f649c9db86fece88434d097c7834475c1c72d2f8c429215",
74
+ "faces": [
75
+ {
76
+ "file": "400Regular/RobotoMono_400Regular.ttf",
77
+ "family": "Roboto Mono",
78
+ "weight": 400,
79
+ "italic": false,
80
+ "sha256": "e4bf45837847d4abfa5a502b807a83393aa138727bdb9ea9c93fb17d53208d95"
81
+ },
82
+ {
83
+ "file": "700Bold/RobotoMono_700Bold.ttf",
84
+ "family": "Roboto Mono",
85
+ "weight": 700,
86
+ "italic": false,
87
+ "sha256": "92649d819cc4c20480b23a47aaa111769924eb31c66b6ecdcf6f573662558c83"
88
+ }
89
+ ]
90
+ },
91
+ {
92
+ "name": "@expo-google-fonts/carlito",
93
+ "version": "0.4.1",
94
+ "pack": "office",
95
+ "source": "https://www.npmjs.com/package/@expo-google-fonts/carlito/v/0.4.1",
96
+ "license": "OFL-1.1",
97
+ "licenseFile": "LICENSE_FONT",
98
+ "licenseSha256": "58402f82a7c332a700294988fe7554fbb0a63a8d27ccc1ee3bbc640311990a00",
99
+ "faces": [
100
+ {
101
+ "file": "400Regular/Carlito_400Regular.ttf",
102
+ "family": "Carlito",
103
+ "weight": 400,
104
+ "italic": false,
105
+ "sha256": "ca019755404c45627a8566915df99068949dc32ee2bce48d6aeee7542d2a0a89"
106
+ },
107
+ {
108
+ "file": "400Regular_Italic/Carlito_400Regular_Italic.ttf",
109
+ "family": "Carlito",
110
+ "weight": 400,
111
+ "italic": true,
112
+ "sha256": "074cd1b89d53765d90d0ed3b4bfe49523efaaf4f3f430c006bc3233778b0ebb5"
113
+ },
114
+ {
115
+ "file": "700Bold/Carlito_700Bold.ttf",
116
+ "family": "Carlito",
117
+ "weight": 700,
118
+ "italic": false,
119
+ "sha256": "51edbfa32d8af939913ae1f4ad0a5173e32083499218c133384638090295f0b0"
120
+ },
121
+ {
122
+ "file": "700Bold_Italic/Carlito_700Bold_Italic.ttf",
123
+ "family": "Carlito",
124
+ "weight": 700,
125
+ "italic": true,
126
+ "sha256": "25f5672c1985d168d6bc2973864fc5a7e374bb95fe8d0f91cff47ae17fa67691"
127
+ }
128
+ ]
129
+ },
130
+ {
131
+ "name": "@expo-google-fonts/caladea",
132
+ "version": "0.4.2",
133
+ "pack": "office",
134
+ "source": "https://www.npmjs.com/package/@expo-google-fonts/caladea/v/0.4.2",
135
+ "license": "OFL-1.1",
136
+ "licenseFile": "LICENSE_FONT",
137
+ "licenseSha256": "ccdab61d371d8c8683a128a92cd7d498dbdb1d37689f7cb21f1bf6b16658d213",
138
+ "faces": [
139
+ {
140
+ "file": "400Regular/Caladea_400Regular.ttf",
141
+ "family": "Caladea",
142
+ "weight": 400,
143
+ "italic": false,
144
+ "sha256": "59faf27a90dee5c1b7813b12b26c725ea88adc1bfe37b0ac8e0b8b32a5732c2a"
145
+ },
146
+ {
147
+ "file": "400Regular_Italic/Caladea_400Regular_Italic.ttf",
148
+ "family": "Caladea",
149
+ "weight": 400,
150
+ "italic": true,
151
+ "sha256": "669e65b265d40899b59d7f3531ca136b7e0fda80a3d1b5d47bbe7825be81fbe3"
152
+ },
153
+ {
154
+ "file": "700Bold/Caladea_700Bold.ttf",
155
+ "family": "Caladea",
156
+ "weight": 700,
157
+ "italic": false,
158
+ "sha256": "11d1e1dc461d1e491b6a396d91934c3bb09d06a77cc3d21ad0b10346c39c3ec2"
159
+ },
160
+ {
161
+ "file": "700Bold_Italic/Caladea_700Bold_Italic.ttf",
162
+ "family": "Caladea",
163
+ "weight": 700,
164
+ "italic": true,
165
+ "sha256": "93d7c2003fe3fd8381cbd518af6cc2f4b510a37c7741c49a95893b6da8edbbc7"
166
+ }
167
+ ]
168
+ },
169
+ {
170
+ "name": "@expo-google-fonts/arimo",
171
+ "version": "0.4.3",
172
+ "pack": "office",
173
+ "source": "https://www.npmjs.com/package/@expo-google-fonts/arimo/v/0.4.3",
174
+ "license": "OFL-1.1",
175
+ "licenseFile": "LICENSE_FONT",
176
+ "licenseSha256": "11cce536cd2f3864d767003af5dcd739e2e15818cf2279b6175edeadd3960992",
177
+ "faces": [
178
+ {
179
+ "file": "400Regular/Arimo_400Regular.ttf",
180
+ "family": "Arimo",
181
+ "weight": 400,
182
+ "italic": false,
183
+ "sha256": "88cc899855d30f9c779b73c3319d0e077b098f927187aafce11ca029add707a6"
184
+ },
185
+ {
186
+ "file": "400Regular_Italic/Arimo_400Regular_Italic.ttf",
187
+ "family": "Arimo",
188
+ "weight": 400,
189
+ "italic": true,
190
+ "sha256": "6bd2c6a6fa87f4566cf0a92c5834884597acbd7be9719e550837865ac4167a42"
191
+ },
192
+ {
193
+ "file": "700Bold/Arimo_700Bold.ttf",
194
+ "family": "Arimo",
195
+ "weight": 700,
196
+ "italic": false,
197
+ "sha256": "ec96ac9fdd94766f66f6140c1375bf011577558f88d43f76b3b59307d2a6bca3"
198
+ },
199
+ {
200
+ "file": "700Bold_Italic/Arimo_700Bold_Italic.ttf",
201
+ "family": "Arimo",
202
+ "weight": 700,
203
+ "italic": true,
204
+ "sha256": "b42b19c10bf5d526f00068303a643282c3df55a79c6fa2eea80b4a078a4a705a"
205
+ }
206
+ ]
207
+ },
208
+ {
209
+ "name": "@expo-google-fonts/tinos",
210
+ "version": "0.4.2",
211
+ "pack": "office",
212
+ "source": "https://www.npmjs.com/package/@expo-google-fonts/tinos/v/0.4.2",
213
+ "license": "OFL-1.1",
214
+ "licenseFile": "LICENSE_FONT",
215
+ "licenseSha256": "cb3382d4643e8b02c12e322c220a3c76a5020d667e4fd4e7c75e744cca6caa6b",
216
+ "faces": [
217
+ {
218
+ "file": "400Regular/Tinos_400Regular.ttf",
219
+ "family": "Tinos",
220
+ "weight": 400,
221
+ "italic": false,
222
+ "sha256": "924ef269e73da94c1803ff68877f5d998dd16605c5ffad82ee181034f8a1fffe"
223
+ },
224
+ {
225
+ "file": "400Regular_Italic/Tinos_400Regular_Italic.ttf",
226
+ "family": "Tinos",
227
+ "weight": 400,
228
+ "italic": true,
229
+ "sha256": "4a52de5bcf70e37bd71949f8a85302b75795a1943aedffbbee4f2db7744e1c81"
230
+ },
231
+ {
232
+ "file": "700Bold/Tinos_700Bold.ttf",
233
+ "family": "Tinos",
234
+ "weight": 700,
235
+ "italic": false,
236
+ "sha256": "576a19b5dc026cafe6ae8fde4c849588dae6475cf5d912cebdf2827388c43ad9"
237
+ },
238
+ {
239
+ "file": "700Bold_Italic/Tinos_700Bold_Italic.ttf",
240
+ "family": "Tinos",
241
+ "weight": 700,
242
+ "italic": true,
243
+ "sha256": "10c90ef7896d758923c06a1485d7824cdb457466da0bf7d42f81c5228931281b"
244
+ }
245
+ ]
246
+ },
247
+ {
248
+ "name": "@expo-google-fonts/cousine",
249
+ "version": "0.4.3",
250
+ "pack": "office",
251
+ "source": "https://www.npmjs.com/package/@expo-google-fonts/cousine/v/0.4.3",
252
+ "license": "OFL-1.1",
253
+ "licenseFile": "LICENSE_FONT",
254
+ "licenseSha256": "b81c4d4dc0a9f72c9155e78187316e016e2012a8102468804173dc61468b906d",
255
+ "faces": [
256
+ {
257
+ "file": "400Regular/Cousine_400Regular.ttf",
258
+ "family": "Cousine",
259
+ "weight": 400,
260
+ "italic": false,
261
+ "sha256": "ca2264d6750d6c47df703be4486361a37ab943d42de6ea2bf794b7c7af767799"
262
+ },
263
+ {
264
+ "file": "400Regular_Italic/Cousine_400Regular_Italic.ttf",
265
+ "family": "Cousine",
266
+ "weight": 400,
267
+ "italic": true,
268
+ "sha256": "f1626b7287db36af8ba2ff88840c82f121f4cbda8971fe3f717f975e9fd782a8"
269
+ },
270
+ {
271
+ "file": "700Bold/Cousine_700Bold.ttf",
272
+ "family": "Cousine",
273
+ "weight": 700,
274
+ "italic": false,
275
+ "sha256": "f03c46c4bc5c0a3af4a1f4fbe5e3e16954581cced05cea20b3bffe12740d99d3"
276
+ },
277
+ {
278
+ "file": "700Bold_Italic/Cousine_700Bold_Italic.ttf",
279
+ "family": "Cousine",
280
+ "weight": 700,
281
+ "italic": true,
282
+ "sha256": "acb798f3629cda0041fbd6234237cf549a1b932792f2a71b0986eb3e1382ed37"
283
+ }
284
+ ]
285
+ },
286
+ {
287
+ "name": "@expo-google-fonts/gelasio",
288
+ "version": "0.4.1",
289
+ "pack": "office",
290
+ "source": "https://www.npmjs.com/package/@expo-google-fonts/gelasio/v/0.4.1",
291
+ "license": "OFL-1.1",
292
+ "licenseFile": "LICENSE_FONT",
293
+ "licenseSha256": "b393cb01867c919b44381512120dc3e4c954c7b47e2035c405f3a324799a4d29",
294
+ "faces": [
295
+ {
296
+ "file": "400Regular/Gelasio_400Regular.ttf",
297
+ "family": "Gelasio",
298
+ "weight": 400,
299
+ "italic": false,
300
+ "sha256": "44aae47442ad39e623788930f38f1e702febcf094c284c5058bf49f7da23de9a"
301
+ },
302
+ {
303
+ "file": "400Regular_Italic/Gelasio_400Regular_Italic.ttf",
304
+ "family": "Gelasio",
305
+ "weight": 400,
306
+ "italic": true,
307
+ "sha256": "2b24b3d41c06d030a31db6610454595775e572a2992d73171cab783407d202a8"
308
+ },
309
+ {
310
+ "file": "700Bold/Gelasio_700Bold.ttf",
311
+ "family": "Gelasio",
312
+ "weight": 700,
313
+ "italic": false,
314
+ "sha256": "fade86eca52203e2ccaf9aee494aa4b0335589879738c47e9d92a53630696d10"
315
+ },
316
+ {
317
+ "file": "700Bold_Italic/Gelasio_700Bold_Italic.ttf",
318
+ "family": "Gelasio",
319
+ "weight": 700,
320
+ "italic": true,
321
+ "sha256": "24f1cb5cf5f765acc5d744a54a6c5dfe34723e58842f5b1b696716497e032852"
322
+ }
323
+ ]
324
+ }
325
+ ]
326
+ });
@@ -1,4 +1,26 @@
1
- import type {FontRegistry,FontRegistryOptions} from "./fonts.js";
1
+ import type {FontRegistry,FontRegistryOptions,EmbeddedFont} from "./fonts.js";
2
2
  export declare function loadBundledFontRegistry(options?: FontRegistryOptions): Promise<FontRegistry & {fontFiles:string[]}>;
3
3
 
4
4
  export declare function loadOfficeFontRegistry(options?: FontRegistryOptions & {includeBaseFonts?:boolean}): Promise<FontRegistry & {fontFiles:string[]}>;
5
+
6
+ export interface BundledFontManifest {
7
+ readonly version: number;
8
+ readonly packages: readonly Readonly<{
9
+ name:string; version:string; pack:"base"|"office"; source:string;
10
+ license:string; licenseFile:string; licenseSha256:string;
11
+ faces:readonly Readonly<{file:string; family:string; weight:number; italic:boolean; sha256:string}>[];
12
+ }>[];
13
+ }
14
+ export declare const BUNDLED_FONT_MANIFEST: BundledFontManifest;
15
+ export interface PreparedNodeFonts {
16
+ registry: FontRegistry & {fontFiles:string[]};
17
+ manifest: BundledFontManifest;
18
+ options: {
19
+ textMeasurement: FontRegistry["textMeasurement"];
20
+ embeddedFonts: EmbeddedFont[];
21
+ fontFiles: string[];
22
+ useBundledFonts: false;
23
+ loadSystemFonts: false;
24
+ };
25
+ }
26
+ export declare function prepareNodeFonts(options?: FontRegistryOptions & {pack?:"base"|"office"; includeBaseFonts?:boolean}): Promise<PreparedNodeFonts>;
@@ -1,44 +1,74 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { createRequire } from "node:module";
3
- import { createFontRegistry } from "./fonts.js";
3
+ import path from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { createFontRegistry, OPFFontError } from "./fonts.js";
6
+ import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
7
+ export { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
4
8
  const require = createRequire(import.meta.url);
9
+
10
+ async function verifiedFile(file, expected, details) {
11
+ let bytes;
12
+ try { bytes = await readFile(file); }
13
+ catch (error) { throw new OPFFontError("font-resource-unavailable", "Reinstall the pinned font package: a required local resource is unavailable.", {...details, cause: error.code}); }
14
+ const actual = createHash("sha256").update(bytes).digest("hex");
15
+ if (actual !== expected) throw new OPFFontError("font-integrity-mismatch", "The local font resource differs from the reviewed font manifest; reinstall the pinned package.", {...details, expected, actual});
16
+ return bytes;
17
+ }
18
+
19
+ async function loadPack(pack) {
20
+ const entries = [], fontFiles = [];
21
+ for (const pkg of BUNDLED_FONT_MANIFEST.packages.filter(item => item.pack === pack)) {
22
+ let manifestPath, installed;
23
+ try {
24
+ manifestPath = require.resolve(`${pkg.name}/package.json`);
25
+ installed = JSON.parse(await readFile(manifestPath, "utf8"));
26
+ } catch (error) {
27
+ throw new OPFFontError("font-resource-unavailable", `Install ${pkg.name}@${pkg.version} to use this offline font pack.`, {package:pkg.name, cause:error.code});
28
+ }
29
+ if (installed.version !== pkg.version) throw new OPFFontError("font-version-mismatch", `Expected ${pkg.name}@${pkg.version}; reinstall the pinned package.`, {package:pkg.name, expected:pkg.version, actual:installed.version});
30
+ const directory = path.dirname(manifestPath);
31
+ const license = (await verifiedFile(path.join(directory, pkg.licenseFile), pkg.licenseSha256, {package:pkg.name, file:pkg.licenseFile})).toString("utf8");
32
+ for (const face of pkg.faces) {
33
+ const file = path.join(directory, face.file);
34
+ const data = await verifiedFile(file, face.sha256, {package:pkg.name, file:face.file});
35
+ fontFiles.push(file);
36
+ entries.push({data:new Uint8Array(data), family:face.family, weight:face.weight, italic:face.italic, license});
37
+ }
38
+ }
39
+ return {entries, fontFiles};
40
+ }
41
+
5
42
  /** Bundled, openly licensed faces. No system font discovery or network requests. */
6
43
  export async function loadBundledFontRegistry(options = {}) {
7
- const definitions = [
8
- ["roboto","400Regular","Roboto_400Regular",400,false],
9
- ["roboto","500Medium","Roboto_500Medium",500,false],
10
- ["roboto","600SemiBold","Roboto_600SemiBold",600,false],
11
- ["roboto","700Bold","Roboto_700Bold",700,false],
12
- ["roboto","800ExtraBold","Roboto_800ExtraBold",800,false],
13
- ["roboto","400Regular_Italic","Roboto_400Regular_Italic",400,true],
14
- ["roboto","700Bold_Italic","Roboto_700Bold_Italic",700,true],
15
- ["roboto-mono","400Regular","RobotoMono_400Regular",400,false],
16
- ["roboto-mono","700Bold","RobotoMono_700Bold",700,false],
17
- ];
18
- const licenses = Object.fromEntries(await Promise.all(["roboto","roboto-mono"].map(async pkg=>[pkg,await readFile(require.resolve(`@expo-google-fonts/${pkg}/LICENSE_FONT`),"utf8")])));
19
- const fontFiles = definitions.map(([pkg,directory,name])=>require.resolve(`@expo-google-fonts/${pkg}/${directory}/${name}.ttf`));
20
- const entries = await Promise.all(definitions.map(async ([pkg,,,weight,italic],index)=>({license:licenses[pkg],data:new Uint8Array(await readFile(fontFiles[index])),weight,italic})));
44
+ const {entries, fontFiles} = await loadPack("base");
21
45
  return Object.assign(createFontRegistry(entries,options),{fontFiles});
22
46
  }
23
47
 
24
48
  /** Six pinned open-source Office substitutes, optionally alongside the base Roboto pack. */
25
49
  export async function loadOfficeFontRegistry(options = {}) {
26
- const packages = ["carlito","caladea","arimo","tinos","cousine","gelasio"];
27
- const styles = [["400Regular",400,false],["400Regular_Italic",400,true],["700Bold",700,false],["700Bold_Italic",700,true]];
28
- const entries = [], fontFiles = [];
29
- for (const pkg of packages) {
30
- const name = pkg[0].toUpperCase()+pkg.slice(1);
31
- const license = await readFile(require.resolve(`@expo-google-fonts/${pkg}/LICENSE_FONT`),"utf8");
32
- for (const [directory,weight,italic] of styles) {
33
- const path = require.resolve(`@expo-google-fonts/${pkg}/${directory}/${name}_${directory}.ttf`);
34
- fontFiles.push(path);
35
- entries.push({data:new Uint8Array(await readFile(path)),weight,italic,license});
36
- }
37
- }
50
+ const {entries, fontFiles} = await loadPack("office");
38
51
  if (options.includeBaseFonts !== false) {
39
- const base = await loadBundledFontRegistry();
52
+ const base = await loadPack("base");
40
53
  fontFiles.push(...base.fontFiles);
41
- for (const face of base.embeddedFonts) entries.push({...face,data:Uint8Array.from(Buffer.from(face.dataUrl.split(",")[1],"base64"))});
54
+ entries.push(...base.entries);
42
55
  }
43
56
  return Object.assign(createFontRegistry(entries,{substitutionPolicy:"metric",...options}),{fontFiles});
44
57
  }
58
+
59
+ /** One set of verified font inputs for layout, SVG, editor, PPTX, and Node raster export. */
60
+ export async function prepareNodeFonts({pack = "base", ...options} = {}) {
61
+ if (pack !== "base" && pack !== "office") throw new OPFFontError("invalid-font-pack", "Choose the base or office font pack.", {pack});
62
+ const registry = await (pack === "base" ? loadBundledFontRegistry(options) : loadOfficeFontRegistry(options));
63
+ return {
64
+ registry,
65
+ manifest:BUNDLED_FONT_MANIFEST,
66
+ options:{
67
+ textMeasurement:registry.textMeasurement,
68
+ embeddedFonts:registry.embeddedFonts,
69
+ fontFiles:[...registry.fontFiles],
70
+ useBundledFonts:false,
71
+ loadSystemFonts:false,
72
+ },
73
+ };
74
+ }
package/dist/fonts.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { TextMeasurement, TextStyle } from "@openpresentation/opf/compositi
2
2
  export interface FontFaceInput { data: Uint8Array; family?: string; weight?: number; italic?: boolean; postscriptName?: string; license?: string }
3
3
  export interface EmbeddedFont { family: string; weight: number; italic?: boolean; dataUrl: string; license?: string }
4
4
  export type FontCompatibility = "exact" | "metric" | "visual" | "generic";
5
- export interface FontResolution { requestedFamily:string; sourceFamily:string; resolvedFamily:string; requestedWeight:number; resolvedWeight:number; italic:boolean; compatibility:FontCompatibility; path?:string; source?:string; note?:string }
5
+ export interface FontResolution { requestedFamily:string; sourceFamily:string; resolvedFamily:string; requestedWeight:number; resolvedWeight:number; italic:boolean; compatibility:FontCompatibility; fontFace?:TextStyle['fontFace']; path?:string; source?:string; note?:string }
6
6
  export interface FontRegistryOptions {
7
7
  aliases?: Record<string,string>; fallbackFamily?: string; strictGlyphs?: boolean;
8
8
  substitutionPolicy?: "none" | "metric" | "visual";
@@ -11,7 +11,7 @@ export interface FontRegistryOptions {
11
11
  export declare const FONT_COMPATIBILITY: readonly Readonly<{requestedFamily:string;substitutes:readonly string[];compatibility:"metric"|"visual";weights?:readonly number[];source?:string;note:string}>[];
12
12
  export declare const EXPERIMENTAL_FONT_CANDIDATES: readonly Readonly<{requestedFamily:string;substitute:string;source:string;note:string}>[];
13
13
  export interface FontRegistry {
14
- textMeasurement: TextMeasurement;
14
+ textMeasurement: TextMeasurement & {outlineBounds: NonNullable<TextMeasurement['outlineBounds']>};
15
15
  clearSubstitutions(): void;
16
16
  resolveFont(style: TextStyle): FontResolution;
17
17
  readonly embeddedFonts: EmbeddedFont[];