/**
* 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