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
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user