import { BookViewer } from "./BookViewer.js";
import { WebGPUSpreadRenderer } from "./rendering/WebGPUSpreadRenderer.js";
import { SpreadRenderer } from "./rendering/SpreadRenderer.js";
import { PdfTextLayerController } from "./controllers/PdfTextLayerController.js";
function pickRendererClass(option) {
if (option === "2d") return SpreadRenderer;
if (option === "webgpu") return WebGPUSpreadRenderer;
if (option && typeof option === "function") return option;
return "gpu" in navigator ? WebGPUSpreadRenderer : SpreadRenderer;
}
/**
* Options for {@link Riffle}.
*
* @typedef {Object} RiffleOptions
* @property {"auto"|"webgpu"|"2d"|Function} [renderer="auto"] Renderer selection. `auto` uses WebGPU when available.
* @property {PageSource|null} [source=null] Initial page source.
* @property {Partial<Layout>|null} [layout=null] Initial layout overrides.
* @property {Partial<Display>|null} [display=null] Initial display overrides.
* @property {"natural"|"ivory"|"bright-white"} [paperPreset] Named paper preset.
* @property {string} [contentBlendMode="multiply"] Blend mode for page content.
* @property {number} [paperThickness] Paper edge and turn-lighting strength from 0 to 1.
* @property {number} [paperTextureStrength] Paper texture/normal strength from 0 to 1.
* @property {boolean} [showPageBorder=true] Whether to render the page edge treatment.
* @property {number} [maxHighResPages=8] High-resolution page bitmap LRU capacity.
* @property {HTMLElement|null} [viewport=null] Element used for zoom measurement and scroll preservation.
* @property {boolean} [selectablePdfText=true] Whether to overlay selectable PDF text on settled spreads.
* @property {number} [renderScale=1] Pixel supersampling multiplier for the rendered spread canvas.
*/
/**
* Creates a Riffle viewer canvas.
*
* The returned value is the canvas itself with viewer methods and getters
* mixed in. Riffle imposes no DOM wrapper or layout styling; the consumer
* decides how the canvas is positioned, scrolled, and decorated.
*
* @param {RiffleOptions} [options={}] Viewer options.
* @returns {RiffleCanvas} Canvas element with the public viewer API.
*/
export function Riffle({
renderer = "auto",
source = null,
layout = null,
display = null,
paperPreset,
contentBlendMode = "multiply",
paperThickness,
paperTextureStrength,
showPageBorder = true,
maxHighResPages = 8,
viewport = null,
selectablePdfText = true,
renderScale = 1,
} = {}) {
const spreadCanvas = document.createElement("canvas");
spreadCanvas.width = 0;
spreadCanvas.height = 0;
spreadCanvas.style.display = "block";
spreadCanvas.style.width = "100%";
spreadCanvas.style.height = "100%";
spreadCanvas.style.objectFit = "contain";
const rendererClass = pickRendererClass(renderer);
const bookViewer = new BookViewer({
spreadCanvas,
viewport, // BookViewer falls back to spreadCanvas.parentElement
rendererClass,
source,
layout,
display,
paperPreset,
contentBlendMode,
paperThickness,
paperTextureStrength,
showPageBorder,
maxHighResPages,
renderScale,
});
const pdfTextLayer = selectablePdfText ? new PdfTextLayerController(bookViewer) : null;
const api = {
bookViewer,
pdfTextLayer,
get backendName() { return bookViewer.backendName; },
get contentZoom() { return bookViewer.contentZoom; },
get renderZoom() { return bookViewer.renderZoom; },
get currentSpread() { return bookViewer.currentSpread; },
// The currently-targeted spread including any in-flight animation. Use
// this for "where are we heading" reads (e.g., relative navigation).
get effectiveSpread() { return bookViewer.navigationController.getEffectiveSpread(); },
get numSpreads() { return bookViewer.numSpreads; },
get isAnimating() { return bookViewer.isAnimating; },
navigateBy: (delta) => bookViewer.navigateTo(bookViewer.navigationController.getEffectiveSpread() + delta),
setSource: (s) => bookViewer.setSource(s),
setLayout: (l) => bookViewer.setLayout(l),
setDisplay: (d) => bookViewer.setDisplay(d),
setViewport: (el) => bookViewer.setViewport(el),
setShowPageBorder: (b) => bookViewer.setShowPageBorder(b),
navigateTo: (s, p) => bookViewer.navigateTo(s, p),
spreadIndexForPage: (pageIndex) => bookViewer.book.spreadIndexForPage(pageIndex),
primaryPageIndexForSpread: (spreadIndex) => bookViewer.book.primaryPageIndexForSpread(spreadIndex),
sourcePageCount: () => bookViewer.book.sourcePageCount(),
sourcePageIndexToPageIndex: (sourcePageIndex) => bookViewer.book.sourcePageIndexToPageIndex(sourcePageIndex),
pageIndexToSourcePageIndex: (pageIndex) => bookViewer.book.pageIndexToSourcePageIndex(pageIndex),
spreadIndexForSourcePage: (sourcePageIndex) => bookViewer.book.spreadIndexForSourcePage(sourcePageIndex),
primarySourcePageIndexForSpread: (spreadIndex) => bookViewer.book.primarySourcePageIndexForSpread(spreadIndex),
adjustZoom: (d) => bookViewer.adjustZoom(d),
resetZoom: () => bookViewer.resetZoom(),
redraw: () => bookViewer.redraw(),
getSpreadGeometry: () => bookViewer.getSpreadGeometry(),
on: (event, fn) => bookViewer.on(event, fn),
off: (event, fn) => bookViewer.off(event, fn),
openPdf: async (file) => {
const { PdfPageSource } = await import("./sources/PdfPageSource.js");
const src = new PdfPageSource();
await src.openPdf(file);
bookViewer.setSource(src);
const firstAspect = src.getPageMetadata(0)?.aspectRatio ?? 0.647;
bookViewer.setLayout({
pw: bookViewer.layout.ph * firstAspect,
ratio: firstAspect * 0.999,
});
},
openHocr: async (fileOrText, options = {}) => {
const { loadHocr } = await import("./loading/hocr.js");
const pages = await loadHocr(fileOrText);
const attach = bookViewer.source?.attachTextContent;
if (typeof attach !== "function") {
throw new Error("Current Riffle source does not support external text content");
}
const attached = attach.call(bookViewer.source, pages, options);
pdfTextLayer?.update();
return { pages, attached };
},
};
// Use defineProperties so getters stay live — Object.assign would
// invoke each getter once at copy time and stamp the resulting value,
// freezing `numSpreads`/`currentSpread`/etc. at construction-time
// values (back when the book was empty).
Object.defineProperties(spreadCanvas, Object.getOwnPropertyDescriptors(api));
return spreadCanvas;
}