/** * Viewer services — host-supplied data-source protocols. * * Components reach page images, layers, separations, annotations, * telemetry, i18n, and theme tokens through these protocols rather * than hardcoding any backend specifics. Hosts implement the * protocols their backend supports; the rest fall through to no-op * defaults (see `host/index.ts`) and the consuming components * self-hide. * * @public */ import type { ColorSample, DensitometerSample } from "../types"; /** * Page-image source. Returns a URL the viewer renders into a canvas * / `` tag. The viewer caches results — services should not * implement their own cache. * * URL builders are **synchronous**: hosts that need async signing * pre-resolve into a redirect proxy or blob URL upstream. Returning * a Promise here would force every consumer through `useEffect` + * state, which doesn't fit the `` rendering pattern. * * @public */ export interface PageImageService { /** * Standard page-tile URL. The host resolves whatever path / * blob / signed URL is appropriate. */ getPageImageUrl(args: { pageNum: number; /** Render DPI; viewer asks for the DPI it needs. */ dpi: number; }): string; } /** * PDF Optional Content Group (OCG / "layer") source. * * Hosts that don't expose layers should leave the no-op default * (returns no layers); the `LayerPanel` then renders an empty- * state placeholder. * * @public */ export interface LayerService { /** * Synchronous URL for an isolated layer image. The host renders * one PNG per OCG with a transparent background; the viewer * composites the active subset locally. */ getLayerImageUrl(args: { pageNum: number; layerIndex: number; dpi: number; }): string; /** List the OCGs available for the current document. */ listLayers(): Promise>; } /** * Per-channel separation source. Hosts that don't expose ink * separations leave the no-op default (returns an empty URL); the * SeparationCanvas renders blank stock for any unrenderable channel. * * @public */ export interface SeparationService { /** * Synchronous URL for an isolated channel image (one PNG per ink * with a transparent background). Channel name is process-ink * (`"Cyan"`, `"Magenta"`, `"Yellow"`, `"Black"`) or a spot ink * (`"Pantone Reflex Blue C"`, etc.). The host is responsible for * percent-encoding the channel name in whatever URL it returns. */ getChannelImageUrl(args: { pageNum: number; channelName: string; dpi: number; }): string; /** * Page-average ink coverage per channel, keyed by ink name, each a * percentage in `[0, 100]` — the mean ink % over every pixel of the * page (the same metric a CIP3 / Ghostscript ink-coverage report * produces). The separations panel renders it next to each ink and * hides any plate whose coverage is ~0% (an unused / phantom plate). * * Returns `null` when coverage can't be computed for this page — e.g. * the server-side renderer is unavailable (codex `computed === false`) * or the host wired a `SeparationService` that predates this method. * The panel then falls back to showing names only (no percent column). * * Optional so existing hosts implementing only `getChannelImageUrl` * keep compiling and keep working; the viewer feature-detects with * `typeof svc.getChannelCoverage === "function"`. Async because both * real implementations are inherently async (raster read / network). * Keys are the exact ink display names from `detectedInks`; channels * the implementation couldn't measure are omitted from the record. */ getChannelCoverage?(args: { pageNum: number; }): Promise | null>; } /** * Total-Area-Coverage heatmap source. Hosts that don't compute a * TAC heatmap leave the no-op default (URL returns empty, runs * resolves to []); the overlay renders nothing in that case. * * @public */ export interface TACHeatmapService { /** Synchronous URL for the heatmap image (per-pixel RGBA tint). */ getHeatmapImageUrl(args: { pageNum: number; dpi: number; tacLimit: number; }): string; /** * Per-text-run TAC readings used to drive the hover-tooltip layer. * Coordinates are PDF points with origin at the **top-left** of the * page (matches poppler's ``pdftotext -bbox`` output). */ listRuns(args: { pageNum: number; dpi: number; tacLimit: number; }): Promise>; } /** * Color-sampler source — picks the rendered colour at a single PDF * point and returns RGB + hex + total area coverage. Hosts that * don't expose colour sampling leave the no-op default (returns * `null`); the `ColorPickerTool` then shows nothing. * * @public */ export interface ColorSampleService { /** * Sample at the given PDF coordinates (origin lower-left). Returns * `null` on any failure — the tool deliberately swallows errors so * a flaky network doesn't push a popover with a confusing chrome * fallback colour. */ sampleAt(args: { pageNum: number; pdfX: number; pdfY: number; /** Optional render DPI override; service decides the default. */ dpi?: number; }): Promise; } /** * Densitometer source — reads ink-channel percentages + Total Area * Coverage at a PDF point. Hosts that can't split ink channels * (RGB-only documents, no Ghostscript) leave the no-op default — * `sampleAt` then throws a `"No separations"` error so the tool * renders its friendly amber banner. * * @public */ export interface DensitometerService { /** * Sample at the given PDF coordinates. On success returns the * ink-channel readings + TAC. On failure throws an `Error` with * a short user-facing message — the tool surfaces `.message` * verbatim in its readout panel. Distinct error paths a typical * server-side implementation produces: * - "No separations available for this page." — backend signals * the page can't be split (e.g. RGB-only document, no CMYK) * - "Sampling failed (NNN)" — backend non-2xx other than the * "no separations" case * - "Network error" — fetch rejected */ sampleAt(args: { pageNum: number; pdfX: number; pdfY: number; /** Optional render DPI override; service decides the default. */ dpi?: number; tacLimit: number; }): Promise; } /** * One annotation record exposed by `AnnotationService.list()` and * `getForPage()`. `fabricJson` is the serialised Fabric.js canvas * snapshot — opaque to `core/`, only the host + the canvas component * inspect it. * * @public */ export interface AnnotationEntry { id: string; jobId: string; pageNum: number; authorEmail: string; authorName: string | null; createdAt: string; updatedAt: string; fabricJson?: unknown; } /** * Annotation source. Phase-2 shape replaces the speculative * `list/create/update/remove` Protocol with concrete methods that * match actual call sites: per-page upsert (canvas autosave), * per-page load (canvas init), full list (sidebar thread), delete by * id (sidebar thread). * * Hosts that don't expose annotations leave the no-op default * (returns empty list / null). The `` and * `` components both gracefully render their * empty states. * * @public */ export interface AnnotationService { /** List every annotation for the current job (every page, every author). */ list(): Promise>; /** * Load the active author's saved drawing for one page. Returns * `null` when nothing is saved yet (or the author isn't logged in). */ getForPage(pageNum: number): Promise; /** * Upsert the active author's drawing for one page. Best-effort — * implementations should swallow network errors so flaky * connectivity doesn't block the user from continuing to draw. */ saveForPage(pageNum: number, fabricJson: unknown): Promise; /** Delete a single annotation by id. */ remove(id: string): Promise; } /** * Report-export source — supplies the URLs the drawer / toolbar * link to for the HTML report viewer and PDF download. Hosts that * don't expose report exports leave the no-op default (returns * empty strings); the consuming menu items render but their links * resolve to the current page (still inert). * * @public */ export interface ReportsService { /** URL the "View HTML Report" link points at. */ getHtmlReportUrl(): string; /** URL the "Download PDF" link points at. */ getPdfDownloadUrl(): string; } /** * Telemetry / analytics. No-op default keeps OSS hosts fast. * * @public */ export interface TelemetryService { track(event: string, properties?: Record): void; } /** * Internationalisation. No-op default returns the key unchanged. * * @public */ export interface I18nService { t(key: string, params?: Record): string; } /** * Optional in-browser fallback that pulls "minimum data" directly from * a raw PDF blob when a host hasn't wired a richer service. Hosts that * supply this — typically by calling ``createPdfJsFallback(pdfUrl)`` * exported from ``@printwithsynergy/lens-pdf/host`` — let the viewer * keep tools like PageCanvas, PageNavigator, MeasureTool, LayerPanel, * and ColorPickerTool functional without a server backend. * * **Capabilities not covered**: true ink separations (CMYK/spot * channels), TAC heatmaps, and the densitometer all require server- * side rendering (Ghostscript/MuPDF). pdf.js only renders to RGB, so * those tools stay hidden when their dedicated services are unwired. * * Every method returns a Promise so the adapter can lazy-load pdf.js * on first use. ``sampleColorAt`` returns ``null`` on failure to match * the {@link ColorSampleService} contract. * * @public */ export interface PdfFallbackAdapter { getPageCount(): Promise; getPageDimensions(pageNum: number): Promise<{ widthPts: number; heightPts: number; }>; renderPageToUrl(args: { pageNum: number; dpi: number; }): Promise; listLayers(): Promise>; sampleColorAt(args: { pageNum: number; pdfX: number; pdfY: number; dpi?: number; }): Promise; } /** * Theme tokens. Plugins that need brand colours read them from here * rather than hardcoding hex strings. The optional `logo*` fields let * a host bundle its full visual identity (colours + logo + label) into * a single object — `` reads them as a fallback when the * equivalent props (`brand`, `brandLogoUrl`) are not set. * * @public */ export interface ThemeTokens { readonly primary: string; readonly accent: string; readonly bg: string; readonly fg: string; readonly border: string; /** Optional brand logo image URL shown in the demo top bar. Any URL the browser can fetch — PNG / SVG / `data:` URI. */ readonly logoUrl?: string; /** Optional brand label shown next to the logo. Mirrors the `brand` prop on ``. Default: `"LensPDF"`. */ readonly logoText?: string; /** Optional pixel cap on the rendered logo height. Width is auto so non-square logos keep their aspect ratio. Default: 24. */ readonly logoMaxHeight?: number; /** Optional alt text for the logo ``. When omitted, the logo is `aria-hidden` and `logoText` carries the meaning. */ readonly logoAlt?: string; } /** * Aggregate service surface exposed via `ViewerContext.services`. * * @public */ export interface ViewerServices { readonly pageImages: PageImageService; readonly layers: LayerService; readonly separations: SeparationService; readonly tacHeatmap: TACHeatmapService; readonly colorSample: ColorSampleService; readonly densitometer: DensitometerService; readonly annotations: AnnotationService; readonly reports: ReportsService; readonly telemetry: TelemetryService; readonly i18n: I18nService; readonly tokens: ThemeTokens; } /** * Telemetry stub — drops every event on the floor. * * @public */ export declare const noopTelemetry: TelemetryService; /** * I18n stub — returns the key unchanged. Suitable for English-only * environments and tests. * * @public */ export declare const noopI18n: I18nService; /** * Default theme tokens — neutral light palette. Hosts typically * override these with their tenant or product branding. * * @public */ export declare const defaultThemeTokens: ThemeTokens; /** * Dark theme tokens — dark-mode palette. Drop-in alternative to * {@link defaultThemeTokens} for hosts that want a dark chrome. * * @public */ export declare const darkThemeTokens: ThemeTokens; /** * Tag a service object as a no-op default. Hosts almost never call * this — it's used internally by ``defaultViewerServices`` and * exposed only so unit tests / advanced hosts can simulate the * unwired state. * * @public */ export declare function markUnwired(service: T): T; /** * Returns ``true`` when the given service is the no-op default — i.e. * the host did not wire a real implementation. Components call this * to decide between hiding themselves (unwired) and rendering an * empty state (wired but returned no data). * * @public */ export declare function isUnwired(service: object | null | undefined): boolean; //# sourceMappingURL=services.d.ts.map