@printwithsynergy/lens-pdf 0.3.0-beta.81

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.
Files changed (213) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +344 -0
  3. package/dist/browser/codexOverlay.d.ts +109 -0
  4. package/dist/browser/codexOverlay.d.ts.map +1 -0
  5. package/dist/browser/codexOverlay.js +256 -0
  6. package/dist/browser/codexOverlay.js.map +1 -0
  7. package/dist/browser/constants.d.ts +13 -0
  8. package/dist/browser/constants.d.ts.map +1 -0
  9. package/dist/browser/constants.js +13 -0
  10. package/dist/browser/constants.js.map +1 -0
  11. package/dist/browser/index.d.ts +211 -0
  12. package/dist/browser/index.d.ts.map +1 -0
  13. package/dist/browser/index.js +1190 -0
  14. package/dist/browser/index.js.map +1 -0
  15. package/dist/browser/pantone-gold.d.ts +59 -0
  16. package/dist/browser/pantone-gold.d.ts.map +1 -0
  17. package/dist/browser/pantone-gold.js +237 -0
  18. package/dist/browser/pantone-gold.js.map +1 -0
  19. package/dist/components/AnnotationCanvas.d.ts +27 -0
  20. package/dist/components/AnnotationCanvas.d.ts.map +1 -0
  21. package/dist/components/AnnotationCanvas.js +401 -0
  22. package/dist/components/AnnotationCanvas.js.map +1 -0
  23. package/dist/components/AnnotationNotesPanel.d.ts +15 -0
  24. package/dist/components/AnnotationNotesPanel.d.ts.map +1 -0
  25. package/dist/components/AnnotationNotesPanel.js +235 -0
  26. package/dist/components/AnnotationNotesPanel.js.map +1 -0
  27. package/dist/components/AnnotationThread.d.ts +18 -0
  28. package/dist/components/AnnotationThread.d.ts.map +1 -0
  29. package/dist/components/AnnotationThread.js +163 -0
  30. package/dist/components/AnnotationThread.js.map +1 -0
  31. package/dist/components/AnnotationToolbar.d.ts +39 -0
  32. package/dist/components/AnnotationToolbar.d.ts.map +1 -0
  33. package/dist/components/AnnotationToolbar.js +258 -0
  34. package/dist/components/AnnotationToolbar.js.map +1 -0
  35. package/dist/components/BoxOverlay.d.ts +20 -0
  36. package/dist/components/BoxOverlay.d.ts.map +1 -0
  37. package/dist/components/BoxOverlay.js +107 -0
  38. package/dist/components/BoxOverlay.js.map +1 -0
  39. package/dist/components/ColorPickerTool.d.ts +11 -0
  40. package/dist/components/ColorPickerTool.d.ts.map +1 -0
  41. package/dist/components/ColorPickerTool.js +220 -0
  42. package/dist/components/ColorPickerTool.js.map +1 -0
  43. package/dist/components/DensitometerTool.d.ts +25 -0
  44. package/dist/components/DensitometerTool.d.ts.map +1 -0
  45. package/dist/components/DensitometerTool.js +246 -0
  46. package/dist/components/DensitometerTool.js.map +1 -0
  47. package/dist/components/DielineInfoPanel.d.ts +27 -0
  48. package/dist/components/DielineInfoPanel.d.ts.map +1 -0
  49. package/dist/components/DielineInfoPanel.js +23 -0
  50. package/dist/components/DielineInfoPanel.js.map +1 -0
  51. package/dist/components/DielineOverlay.d.ts +10 -0
  52. package/dist/components/DielineOverlay.d.ts.map +1 -0
  53. package/dist/components/DielineOverlay.js +57 -0
  54. package/dist/components/DielineOverlay.js.map +1 -0
  55. package/dist/components/FindingsSidebar.d.ts +50 -0
  56. package/dist/components/FindingsSidebar.d.ts.map +1 -0
  57. package/dist/components/FindingsSidebar.js +78 -0
  58. package/dist/components/FindingsSidebar.js.map +1 -0
  59. package/dist/components/LayerCanvas.d.ts +30 -0
  60. package/dist/components/LayerCanvas.d.ts.map +1 -0
  61. package/dist/components/LayerCanvas.js +84 -0
  62. package/dist/components/LayerCanvas.js.map +1 -0
  63. package/dist/components/LayerPanel.d.ts +9 -0
  64. package/dist/components/LayerPanel.d.ts.map +1 -0
  65. package/dist/components/LayerPanel.js +144 -0
  66. package/dist/components/LayerPanel.js.map +1 -0
  67. package/dist/components/LensPDF.d.ts +61 -0
  68. package/dist/components/LensPDF.d.ts.map +1 -0
  69. package/dist/components/LensPDF.js +49 -0
  70. package/dist/components/LensPDF.js.map +1 -0
  71. package/dist/components/LensPDFDemo.d.ts +160 -0
  72. package/dist/components/LensPDFDemo.d.ts.map +1 -0
  73. package/dist/components/LensPDFDemo.js +1060 -0
  74. package/dist/components/LensPDFDemo.js.map +1 -0
  75. package/dist/components/LensPDFDemo.styles.d.ts +38 -0
  76. package/dist/components/LensPDFDemo.styles.d.ts.map +1 -0
  77. package/dist/components/LensPDFDemo.styles.js +282 -0
  78. package/dist/components/LensPDFDemo.styles.js.map +1 -0
  79. package/dist/components/LensPDFViewer.d.ts +79 -0
  80. package/dist/components/LensPDFViewer.d.ts.map +1 -0
  81. package/dist/components/LensPDFViewer.js +254 -0
  82. package/dist/components/LensPDFViewer.js.map +1 -0
  83. package/dist/components/MeasureTool.d.ts +16 -0
  84. package/dist/components/MeasureTool.d.ts.map +1 -0
  85. package/dist/components/MeasureTool.js +137 -0
  86. package/dist/components/MeasureTool.js.map +1 -0
  87. package/dist/components/MobileBottomSheet.d.ts +12 -0
  88. package/dist/components/MobileBottomSheet.d.ts.map +1 -0
  89. package/dist/components/MobileBottomSheet.js +113 -0
  90. package/dist/components/MobileBottomSheet.js.map +1 -0
  91. package/dist/components/MobileDrawer.d.ts +31 -0
  92. package/dist/components/MobileDrawer.d.ts.map +1 -0
  93. package/dist/components/MobileDrawer.js +67 -0
  94. package/dist/components/MobileDrawer.js.map +1 -0
  95. package/dist/components/PageCanvas.d.ts +33 -0
  96. package/dist/components/PageCanvas.d.ts.map +1 -0
  97. package/dist/components/PageCanvas.js +385 -0
  98. package/dist/components/PageCanvas.js.map +1 -0
  99. package/dist/components/PageNavigator.d.ts +18 -0
  100. package/dist/components/PageNavigator.d.ts.map +1 -0
  101. package/dist/components/PageNavigator.js +44 -0
  102. package/dist/components/PageNavigator.js.map +1 -0
  103. package/dist/components/SeparationCanvas.d.ts +12 -0
  104. package/dist/components/SeparationCanvas.d.ts.map +1 -0
  105. package/dist/components/SeparationCanvas.js +174 -0
  106. package/dist/components/SeparationCanvas.js.map +1 -0
  107. package/dist/components/TACHeatmapOverlay.d.ts +17 -0
  108. package/dist/components/TACHeatmapOverlay.d.ts.map +1 -0
  109. package/dist/components/TACHeatmapOverlay.js +119 -0
  110. package/dist/components/TACHeatmapOverlay.js.map +1 -0
  111. package/dist/components/ZoomControls.d.ts +11 -0
  112. package/dist/components/ZoomControls.d.ts.map +1 -0
  113. package/dist/components/ZoomControls.js +26 -0
  114. package/dist/components/ZoomControls.js.map +1 -0
  115. package/dist/components/defaultShellPlugins.d.ts +3 -0
  116. package/dist/components/defaultShellPlugins.d.ts.map +1 -0
  117. package/dist/components/defaultShellPlugins.js +273 -0
  118. package/dist/components/defaultShellPlugins.js.map +1 -0
  119. package/dist/components/index.d.ts +32 -0
  120. package/dist/components/index.d.ts.map +1 -0
  121. package/dist/components/index.js +32 -0
  122. package/dist/components/index.js.map +1 -0
  123. package/dist/components/presets.d.ts +8 -0
  124. package/dist/components/presets.d.ts.map +1 -0
  125. package/dist/components/presets.js +14 -0
  126. package/dist/components/presets.js.map +1 -0
  127. package/dist/components/shellPlugins.d.ts +105 -0
  128. package/dist/components/shellPlugins.d.ts.map +1 -0
  129. package/dist/components/shellPlugins.js +52 -0
  130. package/dist/components/shellPlugins.js.map +1 -0
  131. package/dist/components/useIsMobile.d.ts +16 -0
  132. package/dist/components/useIsMobile.d.ts.map +1 -0
  133. package/dist/components/useIsMobile.js +30 -0
  134. package/dist/components/useIsMobile.js.map +1 -0
  135. package/dist/fallback-pdfjs/index.d.ts +60 -0
  136. package/dist/fallback-pdfjs/index.d.ts.map +1 -0
  137. package/dist/fallback-pdfjs/index.js +163 -0
  138. package/dist/fallback-pdfjs/index.js.map +1 -0
  139. package/dist/host/LensPDFProvider.d.ts +36 -0
  140. package/dist/host/LensPDFProvider.d.ts.map +1 -0
  141. package/dist/host/LensPDFProvider.js +12 -0
  142. package/dist/host/LensPDFProvider.js.map +1 -0
  143. package/dist/host/index.d.ts +167 -0
  144. package/dist/host/index.d.ts.map +1 -0
  145. package/dist/host/index.js +173 -0
  146. package/dist/host/index.js.map +1 -0
  147. package/dist/host/pdfFallback.d.ts +50 -0
  148. package/dist/host/pdfFallback.d.ts.map +1 -0
  149. package/dist/host/pdfFallback.js +171 -0
  150. package/dist/host/pdfFallback.js.map +1 -0
  151. package/dist/host/pdfValidation.d.ts +45 -0
  152. package/dist/host/pdfValidation.d.ts.map +1 -0
  153. package/dist/host/pdfValidation.js +78 -0
  154. package/dist/host/pdfValidation.js.map +1 -0
  155. package/dist/host/shareLink.d.ts +80 -0
  156. package/dist/host/shareLink.d.ts.map +1 -0
  157. package/dist/host/shareLink.js +114 -0
  158. package/dist/host/shareLink.js.map +1 -0
  159. package/dist/host/useLensPDF.d.ts +73 -0
  160. package/dist/host/useLensPDF.d.ts.map +1 -0
  161. package/dist/host/useLensPDF.js +213 -0
  162. package/dist/host/useLensPDF.js.map +1 -0
  163. package/dist/index.d.ts +68 -0
  164. package/dist/index.d.ts.map +1 -0
  165. package/dist/index.js +62 -0
  166. package/dist/index.js.map +1 -0
  167. package/dist/plugin/context.d.ts +70 -0
  168. package/dist/plugin/context.d.ts.map +1 -0
  169. package/dist/plugin/context.js +16 -0
  170. package/dist/plugin/context.js.map +1 -0
  171. package/dist/plugin/findings-location.d.ts +53 -0
  172. package/dist/plugin/findings-location.d.ts.map +1 -0
  173. package/dist/plugin/findings-location.js +72 -0
  174. package/dist/plugin/findings-location.js.map +1 -0
  175. package/dist/plugin/index.d.ts +19 -0
  176. package/dist/plugin/index.d.ts.map +1 -0
  177. package/dist/plugin/index.js +16 -0
  178. package/dist/plugin/index.js.map +1 -0
  179. package/dist/plugin/registry.d.ts +61 -0
  180. package/dist/plugin/registry.d.ts.map +1 -0
  181. package/dist/plugin/registry.js +102 -0
  182. package/dist/plugin/registry.js.map +1 -0
  183. package/dist/plugin/services.d.ts +380 -0
  184. package/dist/plugin/services.d.ts.map +1 -0
  185. package/dist/plugin/services.js +104 -0
  186. package/dist/plugin/services.js.map +1 -0
  187. package/dist/plugin/types.d.ts +198 -0
  188. package/dist/plugin/types.d.ts.map +1 -0
  189. package/dist/plugin/types.js +24 -0
  190. package/dist/plugin/types.js.map +1 -0
  191. package/dist/types/index.d.ts +191 -0
  192. package/dist/types/index.d.ts.map +1 -0
  193. package/dist/types/index.js +95 -0
  194. package/dist/types/index.js.map +1 -0
  195. package/dist/units/index.d.ts +64 -0
  196. package/dist/units/index.d.ts.map +1 -0
  197. package/dist/units/index.js +98 -0
  198. package/dist/units/index.js.map +1 -0
  199. package/docs/architecture.md +90 -0
  200. package/docs/components.md +569 -0
  201. package/docs/contributing.md +78 -0
  202. package/docs/fallback.md +174 -0
  203. package/docs/lens-pdf-viewer.md +128 -0
  204. package/docs/licensing.md +78 -0
  205. package/docs/measurement-units.md +87 -0
  206. package/docs/plugins.md +256 -0
  207. package/docs/security.md +69 -0
  208. package/docs/server.md +212 -0
  209. package/docs/services.md +210 -0
  210. package/docs/share-links.md +111 -0
  211. package/docs/theming.md +164 -0
  212. package/docs/validation.md +83 -0
  213. package/package.json +139 -0
@@ -0,0 +1,210 @@
1
+ ---
2
+ title: "Wiring ViewerServices"
3
+ description: "Implement only the protocols your host supports — page images, layers, separations, TAC heatmaps, color sampling, densitometer, annotations, reports. Unwired services hide their consuming components automatically."
4
+ group: "Reference"
5
+ order: 3
6
+ ---
7
+
8
+ # Wiring `ViewerServices`
9
+
10
+ Every field on `ViewerServices` is independent. Implement the ones your host
11
+ supports; the rest fall through to *unwired* no-op defaults exported from
12
+ `@printwithsynergy/lens-pdf/plugin`. Consuming components detect the unwired
13
+ state and self-hide rather than rendering empty placeholders — see
14
+ [docs/fallback.md](./fallback.md) for the full capability-detection model,
15
+ the in-browser pdf.js fallback, and debug logging.
16
+
17
+ ### Quick start with `defaultUnwiredServices`
18
+
19
+ The easiest way to build a partial `ViewerServices` is to spread the
20
+ pre-built defaults and override only the services your host provides:
21
+
22
+ ```ts
23
+ import { defaultUnwiredServices } from "@printwithsynergy/lens-pdf/host";
24
+
25
+ export const services = {
26
+ ...defaultUnwiredServices,
27
+ pageImages: {
28
+ getPageImageUrl: ({ pageNum, dpi }) =>
29
+ `/api/pdf/${pageNum}.png?dpi=${dpi}`,
30
+ },
31
+ tokens: { ...defaultUnwiredServices.tokens, accent: "#e50c6a" },
32
+ };
33
+ ```
34
+
35
+ Everything you don't override stays *unwired* and the consuming
36
+ components self-hide.
37
+
38
+ ### Manual approach
39
+
40
+ The full manual approach gives you finer-grained control:
41
+
42
+ ```ts
43
+ import type { ViewerServices } from "@printwithsynergy/lens-pdf/plugin";
44
+ import {
45
+ defaultThemeTokens,
46
+ noopI18n,
47
+ noopTelemetry,
48
+ } from "@printwithsynergy/lens-pdf/plugin";
49
+
50
+ export const services: ViewerServices = {
51
+ pageImages: {
52
+ getPageImageUrl: ({ pageNum, dpi }) =>
53
+ `/api/pdf/${pageNum}.png?dpi=${dpi}`,
54
+ },
55
+
56
+ layers: {
57
+ getLayerImageUrl: ({ pageNum, layerIndex, dpi }) =>
58
+ `/api/pdf/${pageNum}/layer/${layerIndex}.png?dpi=${dpi}`,
59
+ listLayers: async () => [
60
+ { name: "Background", ocg_index: 0, default_on: true },
61
+ { name: "CutContour", ocg_index: 1, default_on: true },
62
+ ],
63
+ },
64
+
65
+ separations: {
66
+ getChannelImageUrl: ({ pageNum, channelName, dpi }) =>
67
+ `/api/pdf/${pageNum}/channel/${encodeURIComponent(channelName)}.png?dpi=${dpi}`,
68
+ },
69
+
70
+ tacHeatmap: {
71
+ getHeatmapImageUrl: ({ pageNum, dpi, tacLimit }) =>
72
+ `/api/pdf/${pageNum}/tac.png?dpi=${dpi}&limit=${tacLimit}`,
73
+ listRuns: async () => [],
74
+ },
75
+
76
+ colorSample: {
77
+ sampleAt: async ({ pageNum, pdfX, pdfY }) => {
78
+ const r = await fetch(`/api/pdf/${pageNum}/color?x=${pdfX}&y=${pdfY}`);
79
+ return r.ok ? await r.json() : null;
80
+ },
81
+ },
82
+
83
+ densitometer: {
84
+ sampleAt: async (args) => {
85
+ const r = await fetch(`/api/pdf/${args.pageNum}/density`, {
86
+ method: "POST",
87
+ body: JSON.stringify(args),
88
+ });
89
+ if (!r.ok) {
90
+ if (r.status === 422) throw new Error("No separations available for this page.");
91
+ throw new Error(`Sampling failed (${r.status})`);
92
+ }
93
+ return await r.json();
94
+ },
95
+ },
96
+
97
+ annotations: {
98
+ list: async () => [],
99
+ getForPage: async () => null,
100
+ saveForPage: async () => {},
101
+ remove: async () => {},
102
+ },
103
+
104
+ reports: {
105
+ getHtmlReportUrl: () => "/api/pdf/report.html",
106
+ getPdfDownloadUrl: () => "/api/pdf/report.pdf",
107
+ },
108
+
109
+ telemetry: noopTelemetry,
110
+ i18n: noopI18n,
111
+ tokens: defaultThemeTokens,
112
+ };
113
+ ```
114
+
115
+ ## When you need each field
116
+
117
+ | Service | When you need it |
118
+ | --- | --- |
119
+ | `pageImages.getPageImageUrl` | Always. Returns the URL of the rendered page tile at a given DPI. |
120
+ | `layers.*` | Mounting `LayerCanvas` or `LayerPanel`. Provides per-OCG isolated tiles + the OCG list. |
121
+ | `separations.getChannelImageUrl` | Mounting `SeparationCanvas`. Returns one tile per ink channel with a transparent background — the canvas composites locally. |
122
+ | `tacHeatmap.*` | Mounting `TACHeatmapOverlay`. Provides a heatmap image plus per-text-run TAC readings for hover tooltips. |
123
+ | `colorSample.sampleAt` | `ColorPickerTool`. Returns RGB + hex + TAC at a PDF point, or `null` on failure. |
124
+ | `densitometer.sampleAt` | `DensitometerTool`. Returns ink-channel percentages + TAC. Throw `Error("No separations available for this page.")` for RGB-only PDFs — the tool surfaces the message verbatim. |
125
+ | `annotations.*` | `AnnotationCanvas`, `AnnotationThread`. Per-page upsert + global list + delete. |
126
+ | `reports.*` | Report-export menu items in `MobileDrawer` or your own toolbar. |
127
+ | `telemetry`, `i18n`, `tokens` | Always present; defaults are safe. Override to plug into your analytics, translation table, or brand palette. |
128
+
129
+ ## Notes on each service
130
+
131
+ ### `pageImages`
132
+
133
+ URL builders are **synchronous**. If your host needs async signing,
134
+ pre-resolve into a redirect proxy or blob URL upstream. Returning a Promise
135
+ here would force every `<img src={...}>` consumer through `useEffect` +
136
+ state, which doesn't fit the rendering pattern.
137
+
138
+ The viewer caches results internally — your service should not implement
139
+ its own cache.
140
+
141
+ ### `layers`
142
+
143
+ `getLayerImageUrl` returns one PNG per OCG with a transparent background
144
+ (typically rendered via Ghostscript's `pngalpha` device with every other
145
+ OCG hidden). The browser composites the active subset locally with
146
+ `source-over` blending, so toggling a layer is just a redraw — no API
147
+ round-trip after the first warm-up.
148
+
149
+ ### `separations`
150
+
151
+ Same instant-toggle pattern, but per ink channel. Channel name is a
152
+ process ink (`"Cyan"`, `"Magenta"`, `"Yellow"`, `"Black"`) or a spot ink
153
+ (`"Pantone Reflex Blue C"`, etc.). Your service is responsible for
154
+ percent-encoding the channel name in whatever URL it returns.
155
+
156
+ > Real ink separations require a server-side renderer (Ghostscript,
157
+ > MuPDF, etc.) — the in-browser pdf.js fallback can't produce them.
158
+ > See [server.md](./server.md) for a deployable reference.
159
+
160
+ ### `tacHeatmap`
161
+
162
+ `getHeatmapImageUrl` returns a per-pixel RGBA tint over the page.
163
+ `listRuns` returns per-text-run TAC readings used for the hover-tooltip
164
+ layer. Run coordinates use a **top-left origin** to match poppler's
165
+ `pdftotext -bbox` output (the rest of the API uses lower-left).
166
+
167
+ > Like `separations`, the heatmap is computed from per-ink rasters and
168
+ > is server-side only. See [server.md](./server.md).
169
+
170
+ ### `colorSample`
171
+
172
+ The tool deliberately swallows errors — return `null` instead of throwing
173
+ so a flaky network doesn't pop a tooltip with a confusing fallback color.
174
+
175
+ ### `densitometer`
176
+
177
+ Distinct error messages your `sampleAt` can throw to drive the tool's UI:
178
+
179
+ - `"No separations available for this page."` — engine 422 (RGB-only
180
+ document, no CMYK to split). Surfaces as the friendly amber banner.
181
+ - `"Sampling failed (NNN)"` — engine non-2xx other than 422.
182
+ - `"Network error"` — fetch rejected.
183
+
184
+ The tool reads `Error.message` verbatim — keep messages user-facing.
185
+
186
+ > Server-side only — see [server.md](./server.md).
187
+
188
+ ### `annotations`
189
+
190
+ Four concrete methods that match the actual call sites:
191
+
192
+ - `list()` — sidebar thread (every page, every author).
193
+ - `getForPage(pageNum)` — canvas init for the active author.
194
+ - `saveForPage(pageNum, fabricJson)` — canvas autosave (best-effort; the
195
+ canvas swallows network errors so the user can keep drawing).
196
+ - `remove(id)` — sidebar thread.
197
+
198
+ `fabricJson` is an opaque `unknown` — only the host and `AnnotationCanvas`
199
+ interpret it (it's the serialised Fabric.js canvas snapshot).
200
+
201
+ ### `reports`
202
+
203
+ Both URL builders are synchronous. Hosts without report exports leave the
204
+ no-op defaults — the consuming menu items (currently `MobileDrawer`'s
205
+ "Share &amp; Export" section) drop the report links entirely rather than
206
+ rendering inert hrefs.
207
+
208
+ ### `telemetry`, `i18n`, `tokens`
209
+
210
+ See [theming.md](./theming.md).
@@ -0,0 +1,111 @@
1
+ ---
2
+ title: "Shareable links"
3
+ description: "Generate and parse shareable viewer URLs that pre-load a specific PDF with custom settings — fullscreen, zoom, page, mode, tools, and theme."
4
+ group: "Reference"
5
+ order: 9
6
+ ---
7
+
8
+ # Shareable links
9
+
10
+ `generateShareLink()` builds a URL that opens any LensPDF host page
11
+ with a specific PDF pre-loaded and settings applied.
12
+ `parseShareParams()` reads those query params back into props your
13
+ component can consume.
14
+
15
+ Both are exported from `@printwithsynergy/lens-pdf/host`.
16
+
17
+ ## URL format
18
+
19
+ ```
20
+ https://lenspdf.com/demo?url=<encoded>&fullscreen=true&zoom=150&page=1&mode=single&theme=dark
21
+ ```
22
+
23
+ | Param | Type | Notes |
24
+ | --- | --- | --- |
25
+ | `url` | URL-encoded string | PDF URL to pre-load. |
26
+ | `fullscreen` | `"true"` \| `"1"` | Open in fullscreen (no site chrome). |
27
+ | `zoom` | integer | Initial zoom percentage. |
28
+ | `page` | integer | Initial page number (1-indexed). |
29
+ | `mode` | `"scroll"` \| `"single"` | Defaults to `"scroll"` when absent. |
30
+ | `tools` | comma-separated | Subset of tools to enable. |
31
+ | `theme` | `"light"` \| `"dark"` \| JSON | Preset name or inline `ThemeTokens`. |
32
+
33
+ ## Generating a link
34
+
35
+ ```ts
36
+ import { generateShareLink } from "@printwithsynergy/lens-pdf/host";
37
+
38
+ const link = generateShareLink({
39
+ baseUrl: "https://lenspdf.com/demo",
40
+ pdfUrl: "https://cdn.example.com/proof.pdf",
41
+ fullscreen: true,
42
+ zoom: 150,
43
+ page: 2,
44
+ });
45
+ // → "https://lenspdf.com/demo?url=https%3A%2F%2Fcdn.example.com%2Fproof.pdf&fullscreen=true&zoom=150&page=2"
46
+ ```
47
+
48
+ ### `ShareLinkOptions`
49
+
50
+ | Field | Type | Default | Notes |
51
+ | --- | --- | --- | --- |
52
+ | `baseUrl` | `string` | required | Host demo/viewer page URL. |
53
+ | `pdfUrl` | `string` | required | PDF URL to pre-load. |
54
+ | `fullscreen` | `boolean` | `false` | Fixed-position full-viewport. |
55
+ | `zoom` | `number` | _(omitted)_ | Percentage. |
56
+ | `page` | `number` | _(omitted)_ | 1-indexed page. |
57
+ | `mode` | `"scroll" \| "single"` | `"scroll"` | Only serialised when not `"scroll"`. |
58
+ | `tools` | `string[]` | _(omitted)_ | Comma-joined in URL. |
59
+ | `theme` | `"light" \| "dark" \| Partial<ThemeTokens>` | _(omitted)_ | Preset name or JSON-encoded tokens. |
60
+
61
+ ## Parsing on the consumer side
62
+
63
+ ```ts
64
+ import { parseShareParams } from "@printwithsynergy/lens-pdf/host";
65
+
66
+ const params = parseShareParams(new URLSearchParams(window.location.search));
67
+
68
+ // params.pdfUrl → "https://cdn.example.com/proof.pdf"
69
+ // params.fullscreen → true
70
+ // params.zoom → 150
71
+ // params.page → 2
72
+ ```
73
+
74
+ ### `ParsedShareParams`
75
+
76
+ | Field | Type | Notes |
77
+ | --- | --- | --- |
78
+ | `pdfUrl` | `string \| undefined` | |
79
+ | `fullscreen` | `boolean` | Defaults to `false`. |
80
+ | `zoom` | `number \| undefined` | |
81
+ | `page` | `number \| undefined` | |
82
+ | `mode` | `"scroll" \| "single" \| undefined` | |
83
+ | `tools` | `string[] \| undefined` | |
84
+ | `theme` | `"light" \| "dark" \| Partial<ThemeTokens> \| undefined` | |
85
+
86
+ ## End-to-end with `<LensPDFDemo>`
87
+
88
+ Wire `parseShareParams` into `<LensPDFDemo>` props:
89
+
90
+ ```tsx
91
+ import { LensPDFDemo } from "@printwithsynergy/lens-pdf/components";
92
+ import { parseShareParams } from "@printwithsynergy/lens-pdf/host";
93
+
94
+ export function DemoPage() {
95
+ const params = parseShareParams(new URLSearchParams(window.location.search));
96
+
97
+ return (
98
+ <LensPDFDemo
99
+ brand="MyApp"
100
+ initialPdfUrl={params.pdfUrl}
101
+ fullscreen={params.fullscreen}
102
+ initialZoom={params.zoom ?? 80}
103
+ initialPage={params.page}
104
+ />
105
+ );
106
+ }
107
+ ```
108
+
109
+ Users can now share links like
110
+ `https://myapp.com/demo?url=https://cdn.example.com/proof.pdf&fullscreen=true`
111
+ that open the viewer full-screen with the PDF pre-loaded.
@@ -0,0 +1,164 @@
1
+ ---
2
+ title: "Theme, i18n, telemetry, read-only mode"
3
+ description: "Brand tokens, translation strings, analytics hooks, and the read-only flag for share-link viewers. All optional — the no-op defaults keep OSS hosts fast."
4
+ group: "Reference"
5
+ order: 8
6
+ ---
7
+
8
+ # Theme, i18n, telemetry, read-only mode
9
+
10
+ Three of the `ViewerServices` fields are infrastructure rather than data:
11
+ `tokens`, `i18n`, `telemetry`. They always have safe defaults; override
12
+ when you need to plug into your brand palette, translation table, or
13
+ analytics. The `readOnly` flag lives on `ViewerHostContext` and toggles
14
+ write-only UI.
15
+
16
+ ## Theme tokens
17
+
18
+ ```ts
19
+ interface ThemeTokens {
20
+ readonly primary: string; // brand primary
21
+ readonly accent: string; // brand accent
22
+ readonly bg: string; // surface background
23
+ readonly fg: string; // foreground / body text
24
+ readonly border: string; // hairline border
25
+ // Optional brand-identity fields read by <LensPDFDemo> as a
26
+ // fallback when the equivalent props (`brand`, `brandLogoUrl`)
27
+ // aren't set. Lets a host bundle palette + logo + label into a
28
+ // single tokens object.
29
+ readonly logoUrl?: string; // brand logo image URL
30
+ readonly logoText?: string; // brand label (default: "LensPDF")
31
+ readonly logoMaxHeight?: number; // pixel cap on logo height (default: 24)
32
+ readonly logoAlt?: string; // alt text for the logo <img>
33
+ }
34
+ ```
35
+
36
+ `defaultThemeTokens` is a neutral light palette:
37
+
38
+ ```ts
39
+ import { defaultThemeTokens } from "@printwithsynergy/lens-pdf/plugin";
40
+
41
+ // {
42
+ // primary: "#0f172a",
43
+ // accent: "#3b82f6",
44
+ // bg: "#ffffff",
45
+ // fg: "#0f172a",
46
+ // border: "#e2e8f0",
47
+ // }
48
+ ```
49
+
50
+ `darkThemeTokens` is a dark palette preset for demo and dark-mode UIs:
51
+
52
+ ```ts
53
+ import { darkThemeTokens } from "@printwithsynergy/lens-pdf/plugin";
54
+
55
+ // {
56
+ // primary: "#0f172a",
57
+ // accent: "#3b82f6",
58
+ // bg: "#0e0a14",
59
+ // fg: "#f5f3f7",
60
+ // border: "#2b2138",
61
+ // }
62
+ ```
63
+
64
+ `<LensPDFDemo>` uses `darkThemeTokens` by default; `<LensPDFViewer>`
65
+ uses `defaultThemeTokens`.
66
+
67
+ Pass your own through `services.tokens`:
68
+
69
+ ```ts
70
+ const services: ViewerServices = {
71
+ // …
72
+ tokens: {
73
+ primary: "#1a3a7a",
74
+ accent: "#2563eb",
75
+ bg: "#ffffff",
76
+ fg: "#0f172a",
77
+ border: "#e2e8f0",
78
+ },
79
+ };
80
+ ```
81
+
82
+ Plugins read from `ctx.services.tokens` rather than hardcoding hex
83
+ strings, so swapping a brand palette is a single context-value change.
84
+
85
+ ## i18n
86
+
87
+ ```ts
88
+ interface I18nService {
89
+ t(key: string, params?: Record<string, string | number>): string;
90
+ }
91
+ ```
92
+
93
+ The `noopI18n` default returns the key unchanged with `{param}`
94
+ placeholders substituted. Drop in a real translator as needed:
95
+
96
+ ```ts
97
+ import type { I18nService } from "@printwithsynergy/lens-pdf/plugin";
98
+
99
+ export const i18n: I18nService = {
100
+ t: (key, params) => translateWithICU(key, params),
101
+ };
102
+ ```
103
+
104
+ Suitable for English-only environments and tests, the no-op behaves
105
+ like:
106
+
107
+ ```ts
108
+ noopI18n.t("hello.name", { name: "Ada" }); // "hello.name"
109
+ // (the key is returned because no entry exists; placeholders still
110
+ // substitute when present in the key text itself)
111
+ ```
112
+
113
+ ## Telemetry
114
+
115
+ ```ts
116
+ interface TelemetryService {
117
+ track(event: string, properties?: Record<string, unknown>): void;
118
+ }
119
+ ```
120
+
121
+ `noopTelemetry` drops every event on the floor. Wire your analytics by
122
+ overriding:
123
+
124
+ ```ts
125
+ import type { TelemetryService } from "@printwithsynergy/lens-pdf/plugin";
126
+
127
+ export const telemetry: TelemetryService = {
128
+ track: (event, props) => window.analytics?.track(event, props),
129
+ };
130
+ ```
131
+
132
+ OSS hosts that don't want to ship analytics can leave the no-op default —
133
+ no events will leave the browser.
134
+
135
+ ## Read-only mode
136
+
137
+ Set `ViewerHostContext.readOnly` to `true` to suppress write-only UI.
138
+
139
+ ```tsx
140
+ <ViewerHostContext.Provider
141
+ value={{
142
+ apiBase: "/api/share/abc123",
143
+ jobApiBase: "/api/share/abc123",
144
+ readOnly: true,
145
+ }}
146
+ >
147
+
148
+ </ViewerHostContext.Provider>
149
+ ```
150
+
151
+ What flips:
152
+
153
+ - `AnnotationCanvas` skips its autosave path entirely (reads still work,
154
+ saves are a no-op).
155
+ - `MobileDrawer` hides annotation, share, and verdict controls based on
156
+ the same flag.
157
+ - Your own host UI should branch on `useViewerHost().readOnly` to hide
158
+ any control that mutates server state.
159
+
160
+ Public-token / share-link viewers typically run with `readOnly: true` and
161
+ a constrained `apiBase` (`/api/share/<token>` etc.), with annotations
162
+ read but not written. The annotation service can be wired to a no-op
163
+ `saveForPage` / `remove` even when `readOnly` is false, but flipping the
164
+ host flag is the standard pattern.
@@ -0,0 +1,83 @@
1
+ ---
2
+ title: "PDF validation"
3
+ description: "Client-side PDF validation — magic bytes, MIME type, file size — before handing a file to the viewer or pdf.js fallback. Runs entirely in the browser."
4
+ group: "Reference"
5
+ order: 10
6
+ ---
7
+
8
+ # PDF validation
9
+
10
+ LensPDF ships client-side validation utilities that run entirely in the
11
+ browser. Use them before handing user-supplied files or URLs to the
12
+ viewer.
13
+
14
+ Both are exported from `@printwithsynergy/lens-pdf/host`.
15
+
16
+ ## `validatePdfFile(file, maxBytes?)`
17
+
18
+ Async. Checks a `File` object:
19
+
20
+ 1. **MIME type** — must be `application/pdf` (or empty — some browsers
21
+ don't populate MIME for drag-dropped files).
22
+ 2. **Magic bytes** — first 5 bytes must be `%PDF-` (hex `25 50 44 46 2D`).
23
+ 3. **Size** — must be ≤ `maxBytes` (default: 50 MB).
24
+
25
+ ```ts
26
+ import { validatePdfFile } from "@printwithsynergy/lens-pdf/host";
27
+
28
+ const result = await validatePdfFile(file);
29
+ if (!result.valid) {
30
+ console.error(result.error);
31
+ }
32
+ ```
33
+
34
+ Pass a custom limit:
35
+
36
+ ```ts
37
+ const result = await validatePdfFile(file, 100 * 1024 * 1024); // 100 MB
38
+ ```
39
+
40
+ ## `validatePdfUrl(url)`
41
+
42
+ Synchronous. Validates URL format only — does not fetch or inspect
43
+ headers.
44
+
45
+ Checks:
46
+ 1. String is non-empty after trimming.
47
+ 2. Parses as a valid `URL`.
48
+ 3. Protocol is `http:`, `https:`, or `blob:`.
49
+
50
+ ```ts
51
+ import { validatePdfUrl } from "@printwithsynergy/lens-pdf/host";
52
+
53
+ const result = validatePdfUrl(draftUrl);
54
+ if (!result.valid) {
55
+ setError(result.error);
56
+ return;
57
+ }
58
+ ```
59
+
60
+ ## `PdfValidationResult`
61
+
62
+ Both functions return the same shape:
63
+
64
+ ```ts
65
+ interface PdfValidationResult {
66
+ valid: boolean;
67
+ error?: string; // Human-readable message when valid is false
68
+ }
69
+ ```
70
+
71
+ ## Built-in usage
72
+
73
+ `<LensPDFDemo>` calls both validators automatically — file uploads go
74
+ through `validatePdfFile` and URL submissions go through
75
+ `validatePdfUrl`. The `maxFileSize` prop on `LensPDFDemo` is forwarded
76
+ to `validatePdfFile`.
77
+
78
+ ## Security note
79
+
80
+ These checks are **client-side only** and meant to catch user mistakes
81
+ early (wrong file type, oversized files, malformed URLs). They are not
82
+ a security boundary — a determined user can bypass them. Always validate
83
+ and sanitise uploads on the server side when applicable.