Implement complete modularization of reader code by separating format-specific functionality into dedicated modules. This replaces the monolithic structure with a clean, maintainable architecture that separates concerns by format type. ## New Architecture ### Format-Specific Modules - **formats/reflowable/**: EPUB, FB2, TXT, HTML (page-based pagination) - types.ts: Shared type definitions for reflowable formats - page-calculator.ts: Word-count based pagination with HTML slicing - navigation.ts: Page-based navigation logic - progress-tracker.ts: CFI-based progress tracking - content-renderer.ts: DOM rendering for page content - parser.ts: Unified parser interface for all reflowable formats - ebook/**: Migrated ebook-specific features - **formats/pdf/**: PDF format support - Core PDF functionality (navigation, text selection, annotations) - Advanced features (bookmarks, search, outlines, dual-page) - Page cache and rendering optimizations - **formats/comic/**: Comic format support - Background color, chapter markers, page caching - Page ordering, gap adjustments - **formats/manga/**: Manga format support - RTL navigation, vertical scrolling, reading direction ## Key Improvements 1. **Separation of Concerns**: Each format has its own dedicated module 2. **No Circular Dependencies**: Clean import structure 3. **Type Safety**: Comprehensive TypeScript types throughout 4. **Functional Programming**: Pure functions, no OOP complexity 5. **Scalability**: Easy to add new formats without touching core code ## Migration Path - Old format-specific code in reader/, ebook/, pdf/, comic/, manga/ - New code in formats/[format]/ structure - Maintains backward compatibility during transition - Core reader logic remains format-agnostic This change enables the implementation of page-based pagination for reflowable formats while keeping PDF, comic, and manga functionality unchanged.
110 lines
2.5 KiB
TypeScript
110 lines
2.5 KiB
TypeScript
// Handle PDFs with variable page sizes
|
|
// Procedural implementation (no OOP)
|
|
|
|
interface PageInfo {
|
|
pageNumber: number;
|
|
width: number;
|
|
height: number;
|
|
rotation: number;
|
|
}
|
|
|
|
interface PDFPageSizesState {
|
|
pageSizes: Map<number, PageInfo>;
|
|
defaultSize: { width: number; height: number };
|
|
}
|
|
|
|
function createPDFPageSizes(): PDFPageSizesState {
|
|
return {
|
|
pageSizes: new Map(),
|
|
defaultSize: { width: 595, height: 842 },
|
|
};
|
|
}
|
|
|
|
async function loadPDFPageSizes(
|
|
state: PDFPageSizesState,
|
|
doc: any,
|
|
): Promise<PDFPageSizesState> {
|
|
const pageSizes = new Map<number, PageInfo>();
|
|
|
|
for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
|
|
const page = await doc.getPage(pageNum);
|
|
const viewport = page.getViewport({ scale: 1 });
|
|
|
|
const pageInfo: PageInfo = {
|
|
pageNumber: pageNum,
|
|
width: viewport.width,
|
|
height: viewport.height,
|
|
rotation: viewport.rotation,
|
|
};
|
|
|
|
pageSizes.set(pageNum, pageInfo);
|
|
}
|
|
|
|
return { ...state, pageSizes };
|
|
}
|
|
|
|
function getPDFPageSize(
|
|
state: PDFPageSizesState,
|
|
pageNumber: number,
|
|
): PageInfo | null {
|
|
return state.pageSizes.get(pageNumber) || null;
|
|
}
|
|
|
|
function isPDFPageLandscape(
|
|
state: PDFPageSizesState,
|
|
pageNumber: number,
|
|
): boolean {
|
|
const size = getPDFPageSize(state, pageNumber);
|
|
if (!size) return false;
|
|
|
|
const effectiveWidth =
|
|
size.rotation === 90 || size.rotation === 270 ? size.height : size.width;
|
|
const effectiveHeight =
|
|
size.rotation === 90 || size.rotation === 270 ? size.width : size.height;
|
|
|
|
return effectiveWidth > effectiveHeight;
|
|
}
|
|
|
|
function getPDFCommonSize(state: PDFPageSizesState): {
|
|
width: number;
|
|
height: number;
|
|
} {
|
|
if (state.pageSizes.size === 0) {
|
|
return state.defaultSize;
|
|
}
|
|
|
|
const sizeGroups: Map<
|
|
string,
|
|
{ width: number; height: number; count: number }
|
|
> = new Map();
|
|
|
|
state.pageSizes.forEach((size) => {
|
|
const key = getPageSizeKey(size.width, size.height);
|
|
const existing = sizeGroups.get(key);
|
|
|
|
if (existing) {
|
|
existing.count++;
|
|
} else {
|
|
sizeGroups.set(key, { width: size.width, height: size.height, count: 1 });
|
|
}
|
|
});
|
|
|
|
let mostCommon = state.defaultSize;
|
|
let maxCount = 0;
|
|
|
|
sizeGroups.forEach((size) => {
|
|
if (size.count > maxCount) {
|
|
maxCount = size.count;
|
|
mostCommon = { width: size.width, height: size.height };
|
|
}
|
|
});
|
|
|
|
return mostCommon;
|
|
}
|
|
|
|
function getPageSizeKey(width: number, height: number): string {
|
|
const w = Math.round(width / 10) * 10;
|
|
const h = Math.round(height / 10) * 10;
|
|
return `${w}x${h}`;
|
|
}
|