Files
bookhoard/web/src/reader/comic/page-cache.ts
T
john-okeefe c36d8e6b57 Add comic panel detection system with multi-tier fallback
- panel-detection.service.ts: Main orchestration with OpenCV → ML → Grid fallback chain
- panel-detection.opencv.ts: Edge detection using OpenCV for 80% of comics
- panel-detection.ml.ts: COCO-SSD object detection for irregular layouts
- panel-detector.ts: Unified detector interface
- panel-editor.ts: Manual panel editor UI for user corrections
- panel-ml-detector.ts: TensorFlow.js integration for ML detection
- page-cache.ts: Efficient page caching for large comics
- background-color.ts: Auto-detect comic background color
- chapter-markers.ts: Chapter detection and navigation
- page-scrubber.ts: Fast page scrubbing/thumbnails
- page-order.ts: RTL/LTR page ordering support
- panel-gap.ts: Panel gap detection
2026-04-04 01:00:43 -04:00

152 lines
3.9 KiB
TypeScript

// Lazy-loading page cache with 5-page ahead prefetch
// Shared by both comic and manga readers
// Lazy-loading page cache with 5-page ahead prefetch
// Procedural implementation (no OOP)
import { detectPanels } from "./panel-detection.service";
interface PageCacheState {
cache: Map<number, HTMLImageElement>;
loading: Set<number>;
maxAhead: number;
mediaItemId: string;
panelData: Map<number, { panels: any[]; method: string; confidence: number }>;
}
function createPageCache(mediaItemId: string): PageCacheState {
return {
cache: new Map(),
loading: new Set(),
maxAhead: 5,
mediaItemId,
panelData: new Map(),
};
}
async function getCachedPage(
state: PageCacheState,
pageNumber: number,
): Promise<PageCacheState & { page: HTMLImageElement }> {
if (state.cache.has(pageNumber)) {
return { ...state, page: state.cache.get(pageNumber)! };
}
if (state.loading.has(pageNumber)) {
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (state.cache.has(pageNumber)) {
clearInterval(checkInterval);
resolve({ ...state, page: state.cache.get(pageNumber)! });
}
}, 100);
}) as Promise<PageCacheState & { page: HTMLImageElement }>;
}
const newLoading = new Set(state.loading);
newLoading.add(pageNumber);
const img = await loadComicPage(state, pageNumber);
const newCache = new Map(state.cache);
newCache.set(pageNumber, img);
newLoading.delete(pageNumber);
const newState = { ...state, cache: newCache, loading: newLoading };
prefetchPages(newState, pageNumber + 1);
cleanupPageCache(newState, pageNumber);
return { ...newState, page: img };
}
async function loadComicPage(
state: PageCacheState,
pageNumber: number,
): Promise<HTMLImageElement> {
const token = localStorage.getItem("token");
const response = await fetch(
`/readers/${state.mediaItemId}/pages/${pageNumber}`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
if (!response.ok) {
throw new Error(`Failed to load page ${pageNumber}`);
}
const blob = await response.blob();
const img = new Image();
img.src = URL.createObjectURL(blob);
await new Promise((resolve) => {
img.onload = resolve;
});
return img;
}
function prefetchPages(state: PageCacheState, startPage: number): void {
for (let i = startPage; i < startPage + state.maxAhead; i++) {
if (!state.cache.has(i) && !state.loading.has(i)) {
loadComicPage(state, i).then((img) => {
state.cache.set(i, img);
});
}
}
}
function cleanupPageCache(
state: PageCacheState,
currentPage: number,
): PageCacheState {
const keepPages = 10;
const newCache = new Map(state.cache);
for (const [page] of state.cache) {
if (page < currentPage - keepPages) {
newCache.delete(page);
}
}
return { ...state, cache: newCache };
}
// Add this function
async function detectPagePanels(
state: PageCacheState,
pageNumber: number,
): Promise<any[]> {
// Check if already detected
if (state.panelData?.has(pageNumber)) {
return state.panelData.get(pageNumber)!.panels;
}
// Get or create image
let image: HTMLImageElement;
if (state.cache.has(pageNumber)) {
image = state.cache.get(pageNumber)!;
} else {
image = await loadComicPage(state, pageNumber);
}
// Run detection on demand
const canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext("2d")!;
ctx.drawImage(image, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const result = await detectPanels(imageData, true);
if (!state.panelData) {
state.panelData = new Map();
}
state.panelData.set(pageNumber, result);
return result.panels;
}
// Export the new function
export { createPageCache, getCachedPage, loadComicPage, detectPagePanels };