@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.
- package/LICENSE +661 -0
- package/README.md +344 -0
- package/dist/browser/codexOverlay.d.ts +109 -0
- package/dist/browser/codexOverlay.d.ts.map +1 -0
- package/dist/browser/codexOverlay.js +256 -0
- package/dist/browser/codexOverlay.js.map +1 -0
- package/dist/browser/constants.d.ts +13 -0
- package/dist/browser/constants.d.ts.map +1 -0
- package/dist/browser/constants.js +13 -0
- package/dist/browser/constants.js.map +1 -0
- package/dist/browser/index.d.ts +211 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +1190 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/pantone-gold.d.ts +59 -0
- package/dist/browser/pantone-gold.d.ts.map +1 -0
- package/dist/browser/pantone-gold.js +237 -0
- package/dist/browser/pantone-gold.js.map +1 -0
- package/dist/components/AnnotationCanvas.d.ts +27 -0
- package/dist/components/AnnotationCanvas.d.ts.map +1 -0
- package/dist/components/AnnotationCanvas.js +401 -0
- package/dist/components/AnnotationCanvas.js.map +1 -0
- package/dist/components/AnnotationNotesPanel.d.ts +15 -0
- package/dist/components/AnnotationNotesPanel.d.ts.map +1 -0
- package/dist/components/AnnotationNotesPanel.js +235 -0
- package/dist/components/AnnotationNotesPanel.js.map +1 -0
- package/dist/components/AnnotationThread.d.ts +18 -0
- package/dist/components/AnnotationThread.d.ts.map +1 -0
- package/dist/components/AnnotationThread.js +163 -0
- package/dist/components/AnnotationThread.js.map +1 -0
- package/dist/components/AnnotationToolbar.d.ts +39 -0
- package/dist/components/AnnotationToolbar.d.ts.map +1 -0
- package/dist/components/AnnotationToolbar.js +258 -0
- package/dist/components/AnnotationToolbar.js.map +1 -0
- package/dist/components/BoxOverlay.d.ts +20 -0
- package/dist/components/BoxOverlay.d.ts.map +1 -0
- package/dist/components/BoxOverlay.js +107 -0
- package/dist/components/BoxOverlay.js.map +1 -0
- package/dist/components/ColorPickerTool.d.ts +11 -0
- package/dist/components/ColorPickerTool.d.ts.map +1 -0
- package/dist/components/ColorPickerTool.js +220 -0
- package/dist/components/ColorPickerTool.js.map +1 -0
- package/dist/components/DensitometerTool.d.ts +25 -0
- package/dist/components/DensitometerTool.d.ts.map +1 -0
- package/dist/components/DensitometerTool.js +246 -0
- package/dist/components/DensitometerTool.js.map +1 -0
- package/dist/components/DielineInfoPanel.d.ts +27 -0
- package/dist/components/DielineInfoPanel.d.ts.map +1 -0
- package/dist/components/DielineInfoPanel.js +23 -0
- package/dist/components/DielineInfoPanel.js.map +1 -0
- package/dist/components/DielineOverlay.d.ts +10 -0
- package/dist/components/DielineOverlay.d.ts.map +1 -0
- package/dist/components/DielineOverlay.js +57 -0
- package/dist/components/DielineOverlay.js.map +1 -0
- package/dist/components/FindingsSidebar.d.ts +50 -0
- package/dist/components/FindingsSidebar.d.ts.map +1 -0
- package/dist/components/FindingsSidebar.js +78 -0
- package/dist/components/FindingsSidebar.js.map +1 -0
- package/dist/components/LayerCanvas.d.ts +30 -0
- package/dist/components/LayerCanvas.d.ts.map +1 -0
- package/dist/components/LayerCanvas.js +84 -0
- package/dist/components/LayerCanvas.js.map +1 -0
- package/dist/components/LayerPanel.d.ts +9 -0
- package/dist/components/LayerPanel.d.ts.map +1 -0
- package/dist/components/LayerPanel.js +144 -0
- package/dist/components/LayerPanel.js.map +1 -0
- package/dist/components/LensPDF.d.ts +61 -0
- package/dist/components/LensPDF.d.ts.map +1 -0
- package/dist/components/LensPDF.js +49 -0
- package/dist/components/LensPDF.js.map +1 -0
- package/dist/components/LensPDFDemo.d.ts +160 -0
- package/dist/components/LensPDFDemo.d.ts.map +1 -0
- package/dist/components/LensPDFDemo.js +1060 -0
- package/dist/components/LensPDFDemo.js.map +1 -0
- package/dist/components/LensPDFDemo.styles.d.ts +38 -0
- package/dist/components/LensPDFDemo.styles.d.ts.map +1 -0
- package/dist/components/LensPDFDemo.styles.js +282 -0
- package/dist/components/LensPDFDemo.styles.js.map +1 -0
- package/dist/components/LensPDFViewer.d.ts +79 -0
- package/dist/components/LensPDFViewer.d.ts.map +1 -0
- package/dist/components/LensPDFViewer.js +254 -0
- package/dist/components/LensPDFViewer.js.map +1 -0
- package/dist/components/MeasureTool.d.ts +16 -0
- package/dist/components/MeasureTool.d.ts.map +1 -0
- package/dist/components/MeasureTool.js +137 -0
- package/dist/components/MeasureTool.js.map +1 -0
- package/dist/components/MobileBottomSheet.d.ts +12 -0
- package/dist/components/MobileBottomSheet.d.ts.map +1 -0
- package/dist/components/MobileBottomSheet.js +113 -0
- package/dist/components/MobileBottomSheet.js.map +1 -0
- package/dist/components/MobileDrawer.d.ts +31 -0
- package/dist/components/MobileDrawer.d.ts.map +1 -0
- package/dist/components/MobileDrawer.js +67 -0
- package/dist/components/MobileDrawer.js.map +1 -0
- package/dist/components/PageCanvas.d.ts +33 -0
- package/dist/components/PageCanvas.d.ts.map +1 -0
- package/dist/components/PageCanvas.js +385 -0
- package/dist/components/PageCanvas.js.map +1 -0
- package/dist/components/PageNavigator.d.ts +18 -0
- package/dist/components/PageNavigator.d.ts.map +1 -0
- package/dist/components/PageNavigator.js +44 -0
- package/dist/components/PageNavigator.js.map +1 -0
- package/dist/components/SeparationCanvas.d.ts +12 -0
- package/dist/components/SeparationCanvas.d.ts.map +1 -0
- package/dist/components/SeparationCanvas.js +174 -0
- package/dist/components/SeparationCanvas.js.map +1 -0
- package/dist/components/TACHeatmapOverlay.d.ts +17 -0
- package/dist/components/TACHeatmapOverlay.d.ts.map +1 -0
- package/dist/components/TACHeatmapOverlay.js +119 -0
- package/dist/components/TACHeatmapOverlay.js.map +1 -0
- package/dist/components/ZoomControls.d.ts +11 -0
- package/dist/components/ZoomControls.d.ts.map +1 -0
- package/dist/components/ZoomControls.js +26 -0
- package/dist/components/ZoomControls.js.map +1 -0
- package/dist/components/defaultShellPlugins.d.ts +3 -0
- package/dist/components/defaultShellPlugins.d.ts.map +1 -0
- package/dist/components/defaultShellPlugins.js +273 -0
- package/dist/components/defaultShellPlugins.js.map +1 -0
- package/dist/components/index.d.ts +32 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +32 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/presets.d.ts +8 -0
- package/dist/components/presets.d.ts.map +1 -0
- package/dist/components/presets.js +14 -0
- package/dist/components/presets.js.map +1 -0
- package/dist/components/shellPlugins.d.ts +105 -0
- package/dist/components/shellPlugins.d.ts.map +1 -0
- package/dist/components/shellPlugins.js +52 -0
- package/dist/components/shellPlugins.js.map +1 -0
- package/dist/components/useIsMobile.d.ts +16 -0
- package/dist/components/useIsMobile.d.ts.map +1 -0
- package/dist/components/useIsMobile.js +30 -0
- package/dist/components/useIsMobile.js.map +1 -0
- package/dist/fallback-pdfjs/index.d.ts +60 -0
- package/dist/fallback-pdfjs/index.d.ts.map +1 -0
- package/dist/fallback-pdfjs/index.js +163 -0
- package/dist/fallback-pdfjs/index.js.map +1 -0
- package/dist/host/LensPDFProvider.d.ts +36 -0
- package/dist/host/LensPDFProvider.d.ts.map +1 -0
- package/dist/host/LensPDFProvider.js +12 -0
- package/dist/host/LensPDFProvider.js.map +1 -0
- package/dist/host/index.d.ts +167 -0
- package/dist/host/index.d.ts.map +1 -0
- package/dist/host/index.js +173 -0
- package/dist/host/index.js.map +1 -0
- package/dist/host/pdfFallback.d.ts +50 -0
- package/dist/host/pdfFallback.d.ts.map +1 -0
- package/dist/host/pdfFallback.js +171 -0
- package/dist/host/pdfFallback.js.map +1 -0
- package/dist/host/pdfValidation.d.ts +45 -0
- package/dist/host/pdfValidation.d.ts.map +1 -0
- package/dist/host/pdfValidation.js +78 -0
- package/dist/host/pdfValidation.js.map +1 -0
- package/dist/host/shareLink.d.ts +80 -0
- package/dist/host/shareLink.d.ts.map +1 -0
- package/dist/host/shareLink.js +114 -0
- package/dist/host/shareLink.js.map +1 -0
- package/dist/host/useLensPDF.d.ts +73 -0
- package/dist/host/useLensPDF.d.ts.map +1 -0
- package/dist/host/useLensPDF.js +213 -0
- package/dist/host/useLensPDF.js.map +1 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/context.d.ts +70 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +16 -0
- package/dist/plugin/context.js.map +1 -0
- package/dist/plugin/findings-location.d.ts +53 -0
- package/dist/plugin/findings-location.d.ts.map +1 -0
- package/dist/plugin/findings-location.js +72 -0
- package/dist/plugin/findings-location.js.map +1 -0
- package/dist/plugin/index.d.ts +19 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +16 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/registry.d.ts +61 -0
- package/dist/plugin/registry.d.ts.map +1 -0
- package/dist/plugin/registry.js +102 -0
- package/dist/plugin/registry.js.map +1 -0
- package/dist/plugin/services.d.ts +380 -0
- package/dist/plugin/services.d.ts.map +1 -0
- package/dist/plugin/services.js +104 -0
- package/dist/plugin/services.js.map +1 -0
- package/dist/plugin/types.d.ts +198 -0
- package/dist/plugin/types.d.ts.map +1 -0
- package/dist/plugin/types.js +24 -0
- package/dist/plugin/types.js.map +1 -0
- package/dist/types/index.d.ts +191 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +95 -0
- package/dist/types/index.js.map +1 -0
- package/dist/units/index.d.ts +64 -0
- package/dist/units/index.d.ts.map +1 -0
- package/dist/units/index.js +98 -0
- package/dist/units/index.js.map +1 -0
- package/docs/architecture.md +90 -0
- package/docs/components.md +569 -0
- package/docs/contributing.md +78 -0
- package/docs/fallback.md +174 -0
- package/docs/lens-pdf-viewer.md +128 -0
- package/docs/licensing.md +78 -0
- package/docs/measurement-units.md +87 -0
- package/docs/plugins.md +256 -0
- package/docs/security.md +69 -0
- package/docs/server.md +212 -0
- package/docs/services.md +210 -0
- package/docs/share-links.md +111 -0
- package/docs/theming.md +164 -0
- package/docs/validation.md +83 -0
- package/package.json +139 -0
package/docs/plugins.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Plugin model"
|
|
3
|
+
description: "Slot identifiers, plugin shapes, and registration semantics. Includes the replaces mechanism for shadowing first-party plugins with third-party drop-in alternatives."
|
|
4
|
+
group: "Reference"
|
|
5
|
+
order: 6
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Plugin model
|
|
9
|
+
|
|
10
|
+
LensPDF mounts plugins into nine slots:
|
|
11
|
+
|
|
12
|
+
- `overlay.canvas` — drawn on top of the page tile.
|
|
13
|
+
- `panel.right`, `panel.left`, `panel.bottom` — side / bottom panels.
|
|
14
|
+
- `toolbar.top`, `toolbar.left`, `toolbar.bottom` — toolbar pills.
|
|
15
|
+
- `annotation.source` — non-visual; supplies annotation data via
|
|
16
|
+
`AnnotationSourceProvider`.
|
|
17
|
+
- `dialog.modal` — modal dialog launched from another plugin.
|
|
18
|
+
|
|
19
|
+
## The manifest
|
|
20
|
+
|
|
21
|
+
Every plugin shares a manifest:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
interface ViewerPluginManifest {
|
|
25
|
+
id: string; // "vendor.area.feature"
|
|
26
|
+
version: string; // semver — bump on protocol-affecting changes
|
|
27
|
+
slot: ViewerSlot;
|
|
28
|
+
replaces?: string; // shadow another plugin's id in slot lookups
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Visual plugins (overlay / panel / toolbar / dialog) implement
|
|
33
|
+
`mount(ctx: ViewerContext): ReactNode`. `AnnotationSourceProvider`
|
|
34
|
+
instead provides `subscribe(ctx, onChange)` returning an unsubscribe
|
|
35
|
+
callback.
|
|
36
|
+
|
|
37
|
+
`ViewerContext` carries the live viewer state and the same
|
|
38
|
+
`ViewerServices` your host wired up:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
interface ViewerContext {
|
|
42
|
+
readonly page: number; // 1-indexed current page
|
|
43
|
+
readonly zoom: number; // multiplier; 1.0 = 100%
|
|
44
|
+
readonly pan: { x: number; y: number }; // CSS px
|
|
45
|
+
readonly viewport: { width: number; height: number }; // CSS px
|
|
46
|
+
readonly selectionBbox: readonly [number, number, number, number] | null;
|
|
47
|
+
readonly document: { pageCount: number; pageDimensions: ReadonlyArray<{ width: number; height: number }> };
|
|
48
|
+
readonly services: ViewerServices;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Plugin shapes
|
|
53
|
+
|
|
54
|
+
### `OverlayPlugin`
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
interface OverlayPlugin extends ViewerPluginManifest {
|
|
58
|
+
slot: "overlay.canvas";
|
|
59
|
+
mount(ctx: ViewerContext): ReactNode;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Use for overlays that draw on top of the page canvas (rulers, finding
|
|
64
|
+
boxes, brand-spec violations, etc.).
|
|
65
|
+
|
|
66
|
+
### `PanelPlugin`
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
interface PanelPlugin extends ViewerPluginManifest {
|
|
70
|
+
slot: "panel.right" | "panel.left" | "panel.bottom";
|
|
71
|
+
title: string; // tab / header label
|
|
72
|
+
order?: number; // lower renders first
|
|
73
|
+
mount(ctx: ViewerContext): ReactNode;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `ToolbarPlugin`
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
interface ToolbarPlugin extends ViewerPluginManifest {
|
|
81
|
+
slot: "toolbar.top" | "toolbar.left" | "toolbar.bottom";
|
|
82
|
+
order?: number;
|
|
83
|
+
mount(ctx: ViewerContext): ReactNode;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `AnnotationSourceProvider`
|
|
88
|
+
|
|
89
|
+
Non-visual; supplies annotation data to the viewer. The viewer subscribes
|
|
90
|
+
on mount and the provider invokes the callback with the current list and
|
|
91
|
+
on every change.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
interface AnnotationSourceProvider extends ViewerPluginManifest {
|
|
95
|
+
slot: "annotation.source";
|
|
96
|
+
subscribe(
|
|
97
|
+
ctx: ViewerContext,
|
|
98
|
+
onChange: (annotations: ReadonlyArray<unknown>) => void,
|
|
99
|
+
): () => void; // returns an unsubscribe
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `DialogPlugin`
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
interface DialogPlugin extends ViewerPluginManifest {
|
|
107
|
+
slot: "dialog.modal";
|
|
108
|
+
mount(ctx: ViewerContext): ReactNode;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Registering a plugin
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { register, type OverlayPlugin } from "@printwithsynergy/lens-pdf/plugin";
|
|
116
|
+
|
|
117
|
+
const ruler: OverlayPlugin = {
|
|
118
|
+
id: "demo.overlay.ruler",
|
|
119
|
+
version: "0.1.0",
|
|
120
|
+
slot: "overlay.canvas",
|
|
121
|
+
mount(ctx) {
|
|
122
|
+
return <RulerOverlay zoom={ctx.zoom} viewport={ctx.viewport} />;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
register(ruler);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`register` throws if an id is already registered or if a `replaces` claim
|
|
130
|
+
collides — both are programmer errors.
|
|
131
|
+
|
|
132
|
+
`unregister(id)` removes a plugin and frees any `replaces` claim it held.
|
|
133
|
+
`listAll()` returns every registered plugin (including the shadowed ones)
|
|
134
|
+
for inspection / debugging.
|
|
135
|
+
|
|
136
|
+
`_resetRegistryForTesting()` is exported for tests only — production code
|
|
137
|
+
never calls it.
|
|
138
|
+
|
|
139
|
+
## Reading plugins back at render-time
|
|
140
|
+
|
|
141
|
+
The host mounts each slot by calling `getPluginsForSlot(slot)`:
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
import { Fragment } from "react";
|
|
145
|
+
import {
|
|
146
|
+
getPluginsForSlot,
|
|
147
|
+
type ViewerContext,
|
|
148
|
+
} from "@printwithsynergy/lens-pdf/plugin";
|
|
149
|
+
|
|
150
|
+
function OverlaySlot({ ctx }: { ctx: ViewerContext }) {
|
|
151
|
+
const plugins = getPluginsForSlot("overlay.canvas");
|
|
152
|
+
return (
|
|
153
|
+
<>
|
|
154
|
+
{plugins.map((p) => (
|
|
155
|
+
<Fragment key={p.id}>{p.mount(ctx)}</Fragment>
|
|
156
|
+
))}
|
|
157
|
+
</>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`getPluginsForSlot` returns plugins:
|
|
163
|
+
|
|
164
|
+
- Sorted by `order` ascending (lowest first); insertion order breaks ties.
|
|
165
|
+
- With anything shadowed by a `replaces` claim filtered out.
|
|
166
|
+
|
|
167
|
+
## Replacing a first-party plugin
|
|
168
|
+
|
|
169
|
+
When a plugin pack ships a drop-in alternative, set `replaces` on the
|
|
170
|
+
override:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
register({
|
|
174
|
+
id: "thirdparty.panel.findings",
|
|
175
|
+
version: "0.1.0",
|
|
176
|
+
slot: "panel.right",
|
|
177
|
+
replaces: "vendor.panel.findings", // shadow the original
|
|
178
|
+
title: "Findings",
|
|
179
|
+
mount: (ctx) => <ThirdPartyFindings ctx={ctx} />,
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Constraints:
|
|
184
|
+
|
|
185
|
+
- The replacement must declare the same `slot` as the target. Cross-slot
|
|
186
|
+
overrides are not supported (panels can't replace overlays, etc.).
|
|
187
|
+
- At most one plugin can claim a given `replaces` target — a second
|
|
188
|
+
registration that targets the same id throws.
|
|
189
|
+
- The target id does not need to be registered yet. The override
|
|
190
|
+
registers cleanly even before the target loads, and starts shadowing
|
|
191
|
+
as soon as the target appears.
|
|
192
|
+
|
|
193
|
+
## Viewer shell plugins (`LensPDF` / `LensPDFDemo`)
|
|
194
|
+
|
|
195
|
+
The drop-in components also expose a focused shell-plugin API for
|
|
196
|
+
sidebar/menu/tool customization without touching the global plugin
|
|
197
|
+
registry.
|
|
198
|
+
|
|
199
|
+
Import from `@printwithsynergy/lens-pdf/components`:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
type LensPDFShellSlot = "panel.left" | "overlay.toolbar";
|
|
203
|
+
|
|
204
|
+
interface LensPDFShellPlugin {
|
|
205
|
+
id: string;
|
|
206
|
+
slot: LensPDFShellSlot;
|
|
207
|
+
order?: number;
|
|
208
|
+
replaces?: string;
|
|
209
|
+
isAvailable?: (ctx: LensPDFShellPluginContext) => boolean;
|
|
210
|
+
render: (ctx: LensPDFShellPluginContext) => ReactNode;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Pass plugins directly:
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
<LensPDF
|
|
218
|
+
pdfUrl="/proofs/abc.pdf"
|
|
219
|
+
plugins={[
|
|
220
|
+
{
|
|
221
|
+
id: "acme.left.custom",
|
|
222
|
+
slot: "panel.left",
|
|
223
|
+
order: 15,
|
|
224
|
+
render: (ctx) => <div>Page {ctx.currentPage}</div>,
|
|
225
|
+
},
|
|
226
|
+
]}
|
|
227
|
+
/>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`replaces` uses the same shadow semantics as the global registry:
|
|
231
|
+
set `replaces: "<builtin-id>"` to override a first-party shell plugin.
|
|
232
|
+
|
|
233
|
+
## `OverlayItem`
|
|
234
|
+
|
|
235
|
+
Plugins and host adapters translate their domain types — findings,
|
|
236
|
+
annotations, brand-spec violations — into `OverlayItem`s before handing
|
|
237
|
+
them to a core component. The shape is deliberately minimal:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
interface OverlayItem {
|
|
241
|
+
readonly id: string;
|
|
242
|
+
readonly page: number; // 1-indexed
|
|
243
|
+
readonly bbox?: readonly [number, number, number, number]; // PDF points
|
|
244
|
+
readonly tier?: "error" | "warning" | "advisory" | "info" | "neutral";
|
|
245
|
+
readonly color?: string; // CSS hex, optional override
|
|
246
|
+
readonly label?: string;
|
|
247
|
+
readonly description?: string;
|
|
248
|
+
readonly code?: string; // short identifier code
|
|
249
|
+
readonly data?: Record<string, unknown>; // round-trip payload
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`PageCanvas` and `PageNavigator` consume `OverlayItem[]` directly. The
|
|
254
|
+
default tier→colour map is `error` red, `warning` amber, `advisory` blue,
|
|
255
|
+
`info` / `neutral` slate (see `SEVERITY_COLORS` in `/types`); set `color`
|
|
256
|
+
on an item to override per-item.
|
package/docs/security.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Security policy"
|
|
3
|
+
description: "How to report a vulnerability in LensPDF, what's in scope vs. out of scope, supported versions, and how disclosure is coordinated."
|
|
4
|
+
group: "Project"
|
|
5
|
+
order: 10
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Security policy
|
|
9
|
+
|
|
10
|
+
LensPDF is the renderer, not the access layer. This page covers what
|
|
11
|
+
counts as a LensPDF vulnerability, how to report one, and what we
|
|
12
|
+
promise back.
|
|
13
|
+
|
|
14
|
+
## Reporting a vulnerability
|
|
15
|
+
|
|
16
|
+
If you believe you've found a security issue in LensPDF, please **do
|
|
17
|
+
not** open a public GitHub issue. Instead, email
|
|
18
|
+
**security@printwithsynergy.com** with:
|
|
19
|
+
|
|
20
|
+
- A clear description of the issue and its impact.
|
|
21
|
+
- Steps to reproduce (a minimal repro repo or code snippet helps).
|
|
22
|
+
- The version / commit you tested against.
|
|
23
|
+
- Any suggested mitigation, if you have one.
|
|
24
|
+
|
|
25
|
+
We aim to acknowledge reports within **3 business days** and to ship a
|
|
26
|
+
fix or workaround within **30 days** for confirmed issues, depending on
|
|
27
|
+
severity.
|
|
28
|
+
|
|
29
|
+
You're welcome to request a CVE assignment; we'll coordinate disclosure
|
|
30
|
+
timing with you.
|
|
31
|
+
|
|
32
|
+
## Scope
|
|
33
|
+
|
|
34
|
+
LensPDF is a pure renderer. It does not authenticate, sign, or
|
|
35
|
+
rate-limit any URL it consumes — those concerns are the host's. The
|
|
36
|
+
following are **not** in scope as LensPDF vulnerabilities and should
|
|
37
|
+
be reported to the relevant host instead:
|
|
38
|
+
|
|
39
|
+
- A signed URL that didn't expire when the host expected it to.
|
|
40
|
+
- Unauthorised access to a PDF the host served from an unguarded
|
|
41
|
+
endpoint.
|
|
42
|
+
- Cross-tenant data leaks at the host's API layer.
|
|
43
|
+
|
|
44
|
+
In scope:
|
|
45
|
+
|
|
46
|
+
- Issues in the viewer's rendering or sampling that could leak data the
|
|
47
|
+
user's session shouldn't see (e.g., a tool returning a value derived
|
|
48
|
+
from a PDF object the host meant to hide).
|
|
49
|
+
- XSS / injection / prototype-pollution in any code shipped from this
|
|
50
|
+
repo.
|
|
51
|
+
- Issues in the pdf.js fallback adapter or in any dependency we
|
|
52
|
+
bundle / re-export.
|
|
53
|
+
- DoS-grade resource exhaustion in the renderer or sampling tools.
|
|
54
|
+
|
|
55
|
+
For dependency vulnerabilities, please report to the upstream project
|
|
56
|
+
first; we'll bump our version after they ship a fix.
|
|
57
|
+
|
|
58
|
+
## Supported versions
|
|
59
|
+
|
|
60
|
+
Until the package reaches `1.0.0`, only the latest minor version line
|
|
61
|
+
receives security fixes. Once `1.0.0` ships, the latest two minor
|
|
62
|
+
version lines are supported.
|
|
63
|
+
|
|
64
|
+
## Disclosure
|
|
65
|
+
|
|
66
|
+
We follow [coordinated
|
|
67
|
+
disclosure](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure):
|
|
68
|
+
fixes ship before details are made public, and reporters are credited
|
|
69
|
+
in the release notes unless they request otherwise.
|
package/docs/server.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Reference server"
|
|
3
|
+
description: "Optional Node + Ghostscript backend that supplies real ink separations, densitometer readings, TAC heatmap, and color sampling. Deploy if you need preflight-grade tools the in-browser fallback can't provide."
|
|
4
|
+
group: "Reference"
|
|
5
|
+
order: 9
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Reference server
|
|
9
|
+
|
|
10
|
+
The viewer's [`SeparationCanvas`](./components.md#separationcanvas),
|
|
11
|
+
[`DensitometerTool`](./components.md#densitometertool), and
|
|
12
|
+
[`TACHeatmapOverlay`](./components.md#tacheatmapoverlay) all require
|
|
13
|
+
real ink-channel rasters. The pdf.js fallback can't produce those —
|
|
14
|
+
pdf.js renders to RGB, and there's no in-browser path to reconstruct
|
|
15
|
+
CMYK or spot inks from the result. For preflight-grade output you need
|
|
16
|
+
a server-side renderer.
|
|
17
|
+
|
|
18
|
+
The repo ships a small reference implementation under
|
|
19
|
+
[`server/`](https://github.com/Printwithsynergy/lens-pdf/tree/main/server)
|
|
20
|
+
that you can deploy as-is or read as a contract guide and replace with
|
|
21
|
+
your own. It's a Node + Express service that shells out to Ghostscript
|
|
22
|
+
(`tiffsep` device) for separation rendering. Auth, rate limiting, and
|
|
23
|
+
multi-tenant isolation are deliberately out of scope; run it behind
|
|
24
|
+
your gateway.
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
git clone https://github.com/Printwithsynergy/lens-pdf
|
|
30
|
+
cd lens-pdf/server
|
|
31
|
+
docker build -t lens-pdf-server .
|
|
32
|
+
docker run -p 3000:3000 -v lens-jobs:/var/lib/lens-pdf/jobs lens-pdf-server
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`server/README.md` has the full local-development workflow and the
|
|
36
|
+
list of environment variables.
|
|
37
|
+
|
|
38
|
+
## When you need it
|
|
39
|
+
|
|
40
|
+
| Component | Reference server | pdf.js fallback | Empty |
|
|
41
|
+
| --- | --- | --- | --- |
|
|
42
|
+
| `PageCanvas` | ✅ | ✅ | hidden |
|
|
43
|
+
| `PageNavigator` | ✅ | ✅ | hidden |
|
|
44
|
+
| `LayerPanel` | wire your own `layers` service | ✅ | hidden |
|
|
45
|
+
| `MeasureTool` | ✅ (page dims via PDF) | ✅ | hidden |
|
|
46
|
+
| `ColorPickerTool` | ✅ (true RGB sample) | ✅ (RGB only) | hidden |
|
|
47
|
+
| `SeparationCanvas` | **✅ only here** | hidden | hidden |
|
|
48
|
+
| `DensitometerTool` | **✅ only here** | hidden | hidden |
|
|
49
|
+
| `TACHeatmapOverlay` | **✅ only here** | hidden | hidden |
|
|
50
|
+
| `AnnotationCanvas` | wire your own `annotations` service | hidden | hidden |
|
|
51
|
+
| Reports | wire your own `reports` service | hidden | hidden |
|
|
52
|
+
|
|
53
|
+
Mix and match — the host can use the reference server for separations
|
|
54
|
+
and the pdf.js fallback for everything else, or wire its own
|
|
55
|
+
implementations for any subset.
|
|
56
|
+
|
|
57
|
+
## Wiring example
|
|
58
|
+
|
|
59
|
+
Pre-register the PDF on the server (do this server-side at upload
|
|
60
|
+
time, not from the browser, so you don't have to expose the source
|
|
61
|
+
URL to the user):
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
await fetch(`${apiBase}/jobs/${jobId}/source`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: { "Content-Type": "application/json" },
|
|
67
|
+
body: JSON.stringify({ url: signedPdfUrl }),
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Then point the viewer's services at the same base URL:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import type { ViewerServices } from "@printwithsynergy/lens-pdf/plugin";
|
|
75
|
+
|
|
76
|
+
const services: ViewerServices = {
|
|
77
|
+
pageImages: {
|
|
78
|
+
getPageImageUrl: ({ pageNum, dpi }) =>
|
|
79
|
+
`${apiBase}/jobs/${jobId}/page/${pageNum}.png?dpi=${dpi}`,
|
|
80
|
+
},
|
|
81
|
+
separations: {
|
|
82
|
+
getChannelImageUrl: ({ pageNum, channelName, dpi }) =>
|
|
83
|
+
`${apiBase}/jobs/${jobId}/channel/${encodeURIComponent(channelName)}.png?page=${pageNum}&dpi=${dpi}`,
|
|
84
|
+
},
|
|
85
|
+
tacHeatmap: {
|
|
86
|
+
getHeatmapImageUrl: ({ pageNum, dpi, tacLimit }) =>
|
|
87
|
+
`${apiBase}/jobs/${jobId}/tac.png?page=${pageNum}&dpi=${dpi}&limit=${tacLimit}`,
|
|
88
|
+
listRuns: async () => [], // not implemented in the reference server yet
|
|
89
|
+
},
|
|
90
|
+
colorSample: {
|
|
91
|
+
sampleAt: async ({ pageNum, pdfX, pdfY, dpi = 150 }) => {
|
|
92
|
+
const r = await fetch(
|
|
93
|
+
`${apiBase}/jobs/${jobId}/color?page=${pageNum}` +
|
|
94
|
+
`&x=${pdfX}&y=${pdfY}&dpi=${dpi}` +
|
|
95
|
+
`&pageWidthPts=${pageWidthPts}&pageHeightPts=${pageHeightPts}`,
|
|
96
|
+
);
|
|
97
|
+
return r.ok ? await r.json() : null;
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
densitometer: {
|
|
101
|
+
sampleAt: async ({ pageNum, pdfX, pdfY, dpi = 150, tacLimit }) => {
|
|
102
|
+
const r = await fetch(`${apiBase}/jobs/${jobId}/density`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "Content-Type": "application/json" },
|
|
105
|
+
body: JSON.stringify({
|
|
106
|
+
page: pageNum,
|
|
107
|
+
x: pdfX,
|
|
108
|
+
y: pdfY,
|
|
109
|
+
pageWidthPts,
|
|
110
|
+
pageHeightPts,
|
|
111
|
+
dpi,
|
|
112
|
+
tacLimit,
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
if (!r.ok) {
|
|
116
|
+
if (r.status === 422) throw new Error("No separations available for this page.");
|
|
117
|
+
throw new Error(`Sampling failed (${r.status})`);
|
|
118
|
+
}
|
|
119
|
+
return await r.json();
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
// …leave layers / annotations / reports unwired or supply your own.
|
|
123
|
+
} as ViewerServices;
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The viewer doesn't care that any of these came from the same backend —
|
|
127
|
+
each `ViewerServices` field is independent and can point anywhere.
|
|
128
|
+
|
|
129
|
+
## HTTP contract
|
|
130
|
+
|
|
131
|
+
The reference server is one shape; you can implement any of the
|
|
132
|
+
endpoints differently as long as the responses match. The contract:
|
|
133
|
+
|
|
134
|
+
| Method | Path | Returns |
|
|
135
|
+
| --- | --- | --- |
|
|
136
|
+
| `POST` | `/jobs/{jobId}/source` | Accept the PDF (raw bytes via `application/pdf`, or `application/json` `{ url }` to fetch). |
|
|
137
|
+
| `GET` | `/jobs/{jobId}/page/{n}.png?dpi=N` | Composite RGB PNG. |
|
|
138
|
+
| `GET` | `/jobs/{jobId}/channels?page=N` | `{ "channels": ["Cyan", "Magenta", ...] }`. |
|
|
139
|
+
| `GET` | `/jobs/{jobId}/channel/{name}.png?page=N&dpi=N` | Grayscale PNG, white = no ink. |
|
|
140
|
+
| `GET` | `/jobs/{jobId}/tac.png?page=N&dpi=N&limit=L` | RGBA PNG, transparent under the limit. |
|
|
141
|
+
| `GET` | `/jobs/{jobId}/color?page=N&x=X&y=Y&pageWidthPts=W&pageHeightPts=H&dpi=N` | `ColorSample` JSON. |
|
|
142
|
+
| `POST` | `/jobs/{jobId}/density` | `DensitometerSample` JSON. Body: `{ page, x, y, pageWidthPts, pageHeightPts, dpi, tacLimit }`. |
|
|
143
|
+
| `DELETE` | `/jobs/{jobId}` | Drop server-side state for the job. |
|
|
144
|
+
|
|
145
|
+
`ColorSample` and `DensitometerSample` shapes are defined in
|
|
146
|
+
`@printwithsynergy/lens-pdf/types` — match those exactly.
|
|
147
|
+
|
|
148
|
+
## Security caveats
|
|
149
|
+
|
|
150
|
+
The viewer is a pure renderer; the reference server is a thin
|
|
151
|
+
Ghostscript wrapper. Authz, rate limiting, multi-tenant isolation, and
|
|
152
|
+
SSRF prevention are **your responsibility**. Specifically:
|
|
153
|
+
|
|
154
|
+
- The optional `LENS_BEARER_TOKEN` is a coarse single-secret check
|
|
155
|
+
meant for private-network deploys. For anything user-facing, run the
|
|
156
|
+
service behind your real gateway.
|
|
157
|
+
- The `{ url: "..." }` upload mode fetches whatever URL you give it.
|
|
158
|
+
Block internal hostnames at your egress layer or skip the URL flow
|
|
159
|
+
and upload PDFs directly.
|
|
160
|
+
- Treat every uploaded PDF as hostile. Run the container with
|
|
161
|
+
`--read-only`, drop capabilities, set ulimits.
|
|
162
|
+
- Ghostscript with `-dSAFER` is the default but historical sandbox
|
|
163
|
+
bypasses exist; isolate the process accordingly.
|
|
164
|
+
- The 60 s render timeout protects against the most obvious DoS
|
|
165
|
+
attempts; pair with per-tenant concurrency caps.
|
|
166
|
+
|
|
167
|
+
See [`server/README.md`](https://github.com/Printwithsynergy/lens-pdf/tree/main/server#security)
|
|
168
|
+
for the full list.
|
|
169
|
+
|
|
170
|
+
## Cloudflare / CDN deployment
|
|
171
|
+
|
|
172
|
+
Every per-job GET response is marked **immutable** with a 1-year TTL
|
|
173
|
+
and tagged with `Cache-Tag: job-{jobId}`:
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
Cache-Control: public, max-age=31536000, immutable, s-maxage=31536000
|
|
177
|
+
Cache-Tag: job-{jobId}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
So putting Cloudflare in front of the server gives you free edge
|
|
181
|
+
caching with no extra config — the default Cache Rules will respect
|
|
182
|
+
the headers and store responses at the edge for a year.
|
|
183
|
+
|
|
184
|
+
Two things to watch:
|
|
185
|
+
|
|
186
|
+
1. **Don't set `LENS_BEARER_TOKEN`** if you want CDN caching. An
|
|
187
|
+
`Authorization` header makes Cloudflare bypass the edge cache.
|
|
188
|
+
Move auth to the gateway tier (Cloudflare Access, signed URLs,
|
|
189
|
+
mTLS at the origin) instead.
|
|
190
|
+
2. **`DELETE /jobs/{jobId}` should be paired with a Cloudflare
|
|
191
|
+
purge-by-tag call** from your control plane (tag: `job-{jobId}`).
|
|
192
|
+
On Cloudflare plans without tag purges, rely on the immutable URL
|
|
193
|
+
pattern — a new `jobId` produces new URLs that haven't been cached
|
|
194
|
+
yet.
|
|
195
|
+
|
|
196
|
+
The reference server's `server/README.md` has the full Cloudflare
|
|
197
|
+
deployment writeup.
|
|
198
|
+
|
|
199
|
+
## Limitations of this reference
|
|
200
|
+
|
|
201
|
+
- Per-text-run TAC metadata (the hover-tooltip layer of
|
|
202
|
+
`TACHeatmapOverlay`) is not implemented — heatmap renders fine, the
|
|
203
|
+
per-run list is empty.
|
|
204
|
+
- ICC output-intent overrides are not exposed as env vars yet.
|
|
205
|
+
- The in-process cache is in-memory; multi-pod deployments need to
|
|
206
|
+
swap `cache.ts` for a shared backend (or rely entirely on the
|
|
207
|
+
Cloudflare edge tier).
|
|
208
|
+
- Layers (OCGs), annotations, reports — wire those to your own
|
|
209
|
+
services.
|
|
210
|
+
|
|
211
|
+
If you need any of these, the source is small enough to fork. Pull
|
|
212
|
+
requests welcome.
|