fix(reader): use range-request-only mode for PDF lazy loading

This commit is contained in:
neon
2026-01-03 06:51:52 -06:00
parent 546c9e2984
commit 475c48fce9
4 changed files with 31 additions and 30 deletions
@@ -13,7 +13,7 @@ import type { PageDim } from '../composables/usePdf'
const props = defineProps<{ bookId: number; fileId: number }>()
const router = useRouter()
const { pdfDoc, totalPages, loading, error, load, getAllPageDims, renderPage, getTextContent } = usePdf()
const { pdfDoc, totalPages, loading, error, load, getPageDim, renderPage, getTextContent } = usePdf()
const progress = useReaderProgress(props.bookId, props.fileId)
const { headerVisible, showHeader } = useVisibility()
@@ -51,7 +51,13 @@ const { scale, zoomMode, customScale, zoomLabel, adjustZoom, applyZoomPreset } =
const layout = usePdfLayout(scrollRef, totalPages, effectiveDims, scale, containerH, spread)
const { scrollMode, currentPage, pageInput, pageRows, rowHeights, goToPage, onScroll } = layout
const renderer = usePdfRenderer(renderPage, getTextContent, scale, rotation, totalPages)
function onDimUpdate(pageNum: number, dim: PageDim) {
const prev = pageDims.value[pageNum - 1]
if (prev && prev.width === dim.width && prev.height === dim.height) return
pageDims.value = pageDims.value.map((d, i) => (i === pageNum - 1 ? dim : d))
}
const renderer = usePdfRenderer(renderPage, getTextContent, scale, rotation, totalPages, onDimUpdate)
const { canvasMap, textLayerMap, invalidate, setupIO, reset, destroy } = renderer
// ── Invalidation ──────────────────────────────────────────────────────────────
@@ -178,7 +184,11 @@ onMounted(async () => {
await load(props.fileId)
if (!pdfDoc.value) return
pageDims.value = await getAllPageDims()
// Load only page 1 to establish initial layout dims. All pages are seeded with
// page 1's size (uniform assumption). onDimUpdate corrects individual pages
// lazily as they are rendered — no upfront full-document traversal.
const firstDim = await getPageDim(1)
pageDims.value = Array.from({ length: totalPages.value }, () => ({ ...firstDim }))
reset()
await nextTick()
@@ -22,11 +22,10 @@ export function usePdf() {
loading.value = true
error.value = null
try {
// Pass URL directly so PDF.js can use HTTP range requests (206 Partial Content).
// This avoids downloading the entire file upfront and allows rendering to start immediately.
const doc = await pdfjsLib.getDocument({
url: `/api/books/files/${fileId}/serve`,
rangeChunkSize: 512 * 1024,
rangeChunkSize: 65536,
disableStream: true,
disableAutoFetch: true,
}).promise
pdfDoc.value = doc
@@ -38,26 +37,13 @@ export function usePdf() {
}
}
// Fetch ALL page natural dims in parallel (capped at 10 concurrent).
async function getAllPageDims(): Promise<PageDim[]> {
async function getPageDim(pageNum: number): Promise<PageDim> {
const doc = pdfDoc.value
if (!doc) return []
const total = doc.numPages
const results: PageDim[] = new Array(total)
const CONCURRENCY = 10
async function fetchDim(i: number) {
const page = await doc.getPage(i + 1)
const vp = page.getViewport({ scale: 1 })
page.cleanup()
results[i] = { width: vp.width, height: vp.height }
}
for (let start = 0; start < total; start += CONCURRENCY) {
const batch = Array.from({ length: Math.min(CONCURRENCY, total - start) }, (_, k) => fetchDim(start + k))
await Promise.all(batch)
}
return results
if (!doc) return { width: 595, height: 842 }
const page = await doc.getPage(pageNum)
const vp = page.getViewport({ scale: 1 })
page.cleanup()
return { width: vp.width, height: vp.height }
}
async function renderPage(pageNum: number, canvas: HTMLCanvasElement, scale: number): Promise<void> {
@@ -86,5 +72,5 @@ export function usePdf() {
pdfDoc.value = null
})
return { pdfDoc, totalPages, loading, error, load, getAllPageDims, renderPage, getTextContent }
return { pdfDoc, totalPages, loading, error, load, getPageDim, renderPage, getTextContent }
}
@@ -1,9 +1,11 @@
import { nextTick, ref } from 'vue'
import type * as pdfjsLib from 'pdfjs-dist'
import type { ComputedRef, Ref } from 'vue'
import type { PageDim } from './usePdf'
type RenderPageFn = (pageNum: number, canvas: HTMLCanvasElement, scale: number) => Promise<void>
type GetTextContentFn = (pageNum: number) => Promise<{ content: pdfjsLib.TextContent; viewport: pdfjsLib.PageViewport } | null>
type OnDimUpdateFn = (pageNum: number, dim: PageDim) => void
export function usePdfRenderer(
renderPageFn: RenderPageFn,
@@ -11,6 +13,7 @@ export function usePdfRenderer(
scale: ComputedRef<number>,
rotation: Ref<0 | 90 | 180 | 270>,
totalPages: Ref<number>,
onDimUpdate: OnDimUpdateFn,
) {
const canvasMap = ref(new Map<number, HTMLCanvasElement>())
const textLayerMap = ref(new Map<number, HTMLElement>())
@@ -35,6 +38,8 @@ export function usePdfRenderer(
const canvas = canvasMap.value.get(pageNum)
if (!canvas) continue
await renderPageFn(pageNum, canvas, scale.value)
// Report natural dim back so layout can correct any initial estimate.
onDimUpdate(pageNum, { width: canvas.width / scale.value, height: canvas.height / scale.value })
rendered.add(pageNum)
await buildTextLayer(pageNum)
}
+4 -4
View File
@@ -5,28 +5,28 @@
{
"idx": 0,
"version": "7",
"when": 1767433168702,
"when": 1767491359647,
"tag": "0000_init_schema",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1767433168702,
"when": 1767491359647,
"tag": "0001_lethal_pixie",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1767433168702,
"when": 1767491359647,
"tag": "0002_powerful_thundra",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1767433168702,
"when": 1767491359647,
"tag": "0003_reading_progress_by_file",
"breakpoints": true
}