- Add image-parser.ts: CBZ archive parser for comics/manga - Add image-parter.ts: image splitting utility for comic pages - Update panel-detector.ts: export Panel interface - Update panel-editor.ts: export functions for panel editing - Implement initializeComicReader() and initializeMangaReader() - Support page navigation for image-based readers - Parse comic archives using JSZip with natural sort order
173 lines
4.3 KiB
TypeScript
173 lines
4.3 KiB
TypeScript
// Grid-based panel detection (fast, lightweight)
|
|
// Keep as final fallback
|
|
|
|
export interface Panel {
|
|
id: string;
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
reading_order: number;
|
|
}
|
|
|
|
interface GridConfig {
|
|
rows: number;
|
|
cols: number;
|
|
}
|
|
|
|
function detectPanelsGrid(
|
|
imageData: ImageData,
|
|
config: GridConfig = { rows: 3, cols: 3 },
|
|
): Panel[] {
|
|
const panels: Panel[] = [];
|
|
const cellWidth = imageData.width / config.cols;
|
|
const cellHeight = imageData.height / config.rows;
|
|
|
|
for (let y = 0; y < config.rows; y++) {
|
|
for (let x = 0; x < config.cols; x++) {
|
|
const cell = extractCell(imageData, x, y, cellWidth, cellHeight);
|
|
|
|
if (!isEmpty(cell)) {
|
|
panels.push({
|
|
id: `panel-${panels.length}`,
|
|
x: (x / config.cols) * 100,
|
|
y: (y / config.rows) * 100,
|
|
width: (1 / config.cols) * 100,
|
|
height: (1 / config.rows) * 100,
|
|
reading_order: panels.length,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return mergeAdjacentPanels(panels);
|
|
}
|
|
|
|
function isEmpty(cellData: ImageData): boolean {
|
|
// Simple edge detection to find empty space
|
|
// Count white/transparent pixels
|
|
let emptyPixels = 0;
|
|
const totalPixels = cellData.width * cellData.height;
|
|
const threshold = 0.95; // 95% empty = empty cell
|
|
|
|
for (let i = 0; i < cellData.data.length; i += 4) {
|
|
const r = cellData.data[i];
|
|
const g = cellData.data[i + 1];
|
|
const b = cellData.data[i + 2];
|
|
const a = cellData.data[i + 3];
|
|
|
|
// Consider white or transparent as empty
|
|
if (a < 10 || (r > 250 && g > 250 && b > 250)) {
|
|
emptyPixels++;
|
|
}
|
|
}
|
|
|
|
return emptyPixels / totalPixels > threshold;
|
|
}
|
|
|
|
function mergeAdjacentPanels(panels: Panel[]): Panel[] {
|
|
// Merge panels that are next to each other
|
|
// Simplified algorithm - can be enhanced
|
|
const merged: Panel[] = [];
|
|
const used = new Set<number>();
|
|
|
|
for (let i = 0; i < panels.length; i++) {
|
|
if (used.has(i)) continue;
|
|
|
|
let current = { ...panels[i] };
|
|
used.add(i);
|
|
|
|
// Look for adjacent panels
|
|
for (let j = i + 1; j < panels.length; j++) {
|
|
if (used.has(j)) continue;
|
|
if (isAdjacent(current, panels[j])) {
|
|
current = mergePanels(current, panels[j]);
|
|
used.add(j);
|
|
}
|
|
}
|
|
|
|
merged.push(current);
|
|
}
|
|
|
|
return merged;
|
|
}
|
|
|
|
function extractCell(
|
|
imageData: ImageData,
|
|
gridX: number,
|
|
gridY: number,
|
|
cellWidth: number,
|
|
cellHeight: number,
|
|
): ImageData {
|
|
const startX = Math.floor(gridX * cellWidth);
|
|
const startY = Math.floor(gridY * cellHeight);
|
|
const width = Math.floor(cellWidth);
|
|
const height = Math.floor(cellHeight);
|
|
|
|
const cellData = new Uint8ClampedArray(width * height * 4);
|
|
// Copy pixels for the cell region
|
|
for (let y = 0; y < height; y++) {
|
|
for (let x = 0; x < width; x++) {
|
|
const srcIdx = ((startY + y) * imageData.width + (startX + x)) * 4;
|
|
const destIdx = (y * width + x) * 4;
|
|
cellData[destIdx] = imageData.data[srcIdx];
|
|
cellData[destIdx + 1] = imageData.data[srcIdx + 1];
|
|
cellData[destIdx + 2] = imageData.data[srcIdx + 2];
|
|
cellData[destIdx + 3] = imageData.data[srcIdx + 3];
|
|
}
|
|
}
|
|
|
|
return new ImageData(cellData, width, height);
|
|
}
|
|
|
|
function isAdjacent(p1: Panel, p2: Panel): boolean {
|
|
const tolerance = 5; // 5% tolerance for alignment
|
|
// Check horizontal adjacency
|
|
if (
|
|
Math.abs(p1.y - p2.y) < tolerance &&
|
|
Math.abs(p1.height - p2.height) < tolerance
|
|
) {
|
|
return (
|
|
Math.abs(p1.x + p1.width - p2.x) < tolerance ||
|
|
Math.abs(p2.x + p2.width - p1.x) < tolerance
|
|
);
|
|
}
|
|
// Check vertical adjacency
|
|
if (
|
|
Math.abs(p1.x - p2.x) < tolerance &&
|
|
Math.abs(p1.width - p2.width) < tolerance
|
|
) {
|
|
return (
|
|
Math.abs(p1.y + p1.height - p2.y) < tolerance ||
|
|
Math.abs(p2.y + p2.height - p1.y) < tolerance
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function mergePanels(p1: Panel, p2: Panel): Panel {
|
|
const minX = Math.min(p1.x, p2.x);
|
|
const minY = Math.min(p1.y, p2.y);
|
|
const maxX = Math.max(p1.x + p1.width, p2.x + p2.width);
|
|
const maxY = Math.max(p1.y + p1.height, p2.y + p2.height);
|
|
|
|
return {
|
|
id: p1.id,
|
|
x: minX,
|
|
y: minY,
|
|
width: maxX - minX,
|
|
height: maxY - minY,
|
|
reading_order: Math.min(p1.reading_order, p2.reading_order),
|
|
};
|
|
}
|
|
|
|
// ADD THIS EXPORT AT THE END OF THE FILE
|
|
export {
|
|
detectPanelsGrid,
|
|
isEmpty,
|
|
mergeAdjacentPanels,
|
|
extractCell,
|
|
isAdjacent,
|
|
mergePanels,
|
|
};
|