@l1yp/file-viewer 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"ZoomImage.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/ZoomImage.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\n/**\n * A pan + zoom image stage, shared by the full-screen {@link ImageLightbox} and the in-archive image preview\n * ({@link ZipBrowser}/{@link SevenZipBrowser}/{@link ArchiveBrowser}). Fills its parent (which must give it a\n * size). Wheel zooms toward the cursor, drag pans, double-click toggles fit↔2×, and a bottom toolbar offers\n * −/百分比/+. Initial fit is \"contain\" for a normal image, or fill-width-top-aligned for a `long` one (a tall\n * screenshot) so it reads top-down and pans vertically. A no-drag click on empty area emits `emptyClick` (the\n * lightbox closes on it; the archive preview ignores it).\n *\n * Exposes `zoomIn`/`zoomOut`/`fit` so a host can wire keyboard shortcuts to them.\n */\nconst props = withDefaults(defineProps<{ src: string; alt?: string; long?: boolean }>(), {\n alt: '',\n long: false,\n})\nconst emit = defineEmits<{ emptyClick: [] }>()\n\nconst stageEl = ref<HTMLElement | null>(null)\nconst imgEl = ref<HTMLImageElement | null>(null)\nconst natW = ref(0)\nconst natH = ref(0)\nconst scale = ref(1) // CSS px per image px (1 = 100%)\nconst tx = ref(0) // translate of the image's top-left within the stage\nconst ty = ref(0)\nconst fitScale = ref(1) // scale that \"contains\" the image in the stage\nconst ready = ref(false) // image measured → show it (avoids a pre-fit flash)\n\nconst percent = ref(100)\nfunction syncPercent(): void {\n percent.value = Math.round(scale.value * 100)\n}\n\nfunction stageSize(): { w: number; h: number } {\n const el = stageEl.value\n if (el && el.clientWidth) return { w: el.clientWidth, h: el.clientHeight }\n return { w: 1024, h: 768 }\n}\nfunction minScale(): number {\n return Math.max(0.05, Math.min(fitScale.value, 1) * 0.5)\n}\nfunction maxScale(): number {\n return Math.max(fitScale.value, 1) * 8\n}\n\n// Keep the image within the stage: center an axis whose content is smaller than the stage, otherwise clamp so\n// its edges can't be dragged inside the viewport. `topAlign` pins a too-tall image to its top (initial long view).\nfunction clampView(topAlign = false): void {\n const { w, h } = stageSize()\n const cw = natW.value * scale.value\n const ch = natH.value * scale.value\n tx.value = cw <= w ? (w - cw) / 2 : Math.min(0, Math.max(w - cw, tx.value))\n if (ch <= h) ty.value = (h - ch) / 2\n else if (topAlign) ty.value = 0\n else ty.value = Math.min(0, Math.max(h - ch, ty.value))\n syncPercent()\n}\n\n// (Re)fit the measured image: contain it, or — for a long image — fill the width and pin to the top.\nfunction fitView(): void {\n if (!natW.value || !natH.value) return\n const { w, h } = stageSize()\n fitScale.value = Math.min(w / natW.value, h / natH.value)\n scale.value = props.long ? (w * 0.98) / natW.value : Math.min(fitScale.value, 1)\n clampView(props.long)\n}\n\nfunction clampScale(s: number): number {\n return Math.min(maxScale(), Math.max(minScale(), s))\n}\n\n// Zoom keeping the image point under (cx, cy) — stage-relative coords — fixed in place.\nfunction zoomAt(cx: number, cy: number, factor: number): void {\n const next = clampScale(scale.value * factor)\n if (next === scale.value) return\n const ix = (cx - tx.value) / scale.value\n const iy = (cy - ty.value) / scale.value\n scale.value = next\n tx.value = cx - ix * next\n ty.value = cy - iy * next\n clampView(false)\n}\nfunction zoomCenter(factor: number): void {\n const { w, h } = stageSize()\n zoomAt(w / 2, h / 2, factor)\n}\n\nfunction onWheel(e: WheelEvent): void {\n const rect = stageEl.value?.getBoundingClientRect()\n const cx = rect ? e.clientX - rect.left : e.clientX\n const cy = rect ? e.clientY - rect.top : e.clientY\n zoomAt(cx, cy, e.deltaY < 0 ? 1.15 : 1 / 1.15)\n}\nfunction onDblClick(e: MouseEvent): void {\n const rect = stageEl.value?.getBoundingClientRect()\n const cx = rect ? e.clientX - rect.left : e.clientX\n const cy = rect ? e.clientY - rect.top : e.clientY\n // Near the fit scale → zoom in toward the cursor; otherwise snap back to fit.\n if (scale.value <= fitScale.value * 1.05)\n zoomAt(cx, cy, Math.max(2, 1 / Math.max(fitScale.value, 0.001)))\n else fitView()\n}\n\n// Pointer drag → pan (covers mouse + touch). A no-move pointerup on empty area emits `emptyClick`.\nlet dragging = false\nlet moved = false\nlet lastX = 0\nlet lastY = 0\nlet downTarget: EventTarget | null = null\n\nfunction onPointerDown(e: PointerEvent): void {\n if (e.button !== 0) return\n dragging = true\n moved = false\n lastX = e.clientX\n lastY = e.clientY\n downTarget = e.target\n stageEl.value?.setPointerCapture(e.pointerId)\n}\nfunction onPointerMove(e: PointerEvent): void {\n if (!dragging) return\n const dx = e.clientX - lastX\n const dy = e.clientY - lastY\n if (!moved && Math.abs(dx) + Math.abs(dy) > 3) moved = true\n lastX = e.clientX\n lastY = e.clientY\n tx.value += dx\n ty.value += dy\n clampView(false)\n}\nfunction onPointerUp(e: PointerEvent): void {\n if (!dragging) return\n dragging = false\n try {\n stageEl.value?.releasePointerCapture(e.pointerId)\n } catch {\n /* capture already released — ignore */\n }\n if (!moved && downTarget !== imgEl.value) emit('emptyClick')\n}\n\nfunction onImgLoad(): void {\n const img = imgEl.value\n if (!img) return\n natW.value = img.naturalWidth || 1\n natH.value = img.naturalHeight || 1\n fitView()\n ready.value = true\n}\n\n// Re-fit when the container is resized (the side panel's drag splitter, or the window).\nlet ro: ResizeObserver | null = null\nfunction onResize(): void {\n if (ready.value) fitView()\n}\n\n// Reset for a new source; the transform is recomputed on the <img>'s load.\nwatch(\n () => props.src,\n () => {\n ready.value = false\n natW.value = 0\n natH.value = 0\n scale.value = 1\n tx.value = 0\n ty.value = 0\n fitScale.value = 1\n syncPercent()\n },\n)\n\nonMounted(() => {\n if (typeof ResizeObserver !== 'undefined' && stageEl.value) {\n ro = new ResizeObserver(onResize)\n ro.observe(stageEl.value)\n }\n})\nonBeforeUnmount(() => ro?.disconnect())\n\ndefineExpose({\n zoomIn: () => zoomCenter(1.2),\n zoomOut: () => zoomCenter(1 / 1.2),\n fit: () => fitView(),\n})\n</script>\n\n<template>\n <div\n ref=\"stageEl\"\n class=\"zi\"\n :class=\"{ 'zi--grab': scale > fitScale }\"\n @wheel.prevent=\"onWheel\"\n @pointerdown=\"onPointerDown\"\n @pointermove=\"onPointerMove\"\n @pointerup=\"onPointerUp\"\n @pointercancel=\"onPointerUp\"\n @dblclick=\"onDblClick\"\n >\n <img\n ref=\"imgEl\"\n class=\"zi__img\"\n :class=\"{ 'zi__img--ready': ready }\"\n :src=\"src\"\n :alt=\"alt\"\n draggable=\"false\"\n :style=\"{ transform: `translate(${tx}px, ${ty}px) scale(${scale})` }\"\n @load=\"onImgLoad\"\n @dragstart.prevent\n />\n <div class=\"zi__toolbar\" @pointerdown.stop @dblclick.stop @wheel.stop>\n <button type=\"button\" class=\"zi__tool\" title=\"缩小 (-)\" @click=\"zoomCenter(1 / 1.2)\">\n −\n </button>\n <button type=\"button\" class=\"zi__pct\" title=\"适应窗口 (0)\" @click=\"fitView()\">\n {{ percent }}%\n </button>\n <button type=\"button\" class=\"zi__tool\" title=\"放大 (+)\" @click=\"zoomCenter(1.2)\">+</button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.zi {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n cursor: zoom-out;\n touch-action: none;\n}\n.zi--grab {\n cursor: grab;\n}\n.zi--grab:active {\n cursor: grabbing;\n}\n.zi__img {\n position: absolute;\n top: 0;\n left: 0;\n transform-origin: 0 0;\n will-change: transform;\n user-select: none;\n -webkit-user-drag: none;\n opacity: 0;\n}\n.zi__img--ready {\n opacity: 1;\n}\n.zi__toolbar {\n position: absolute;\n bottom: 14px;\n left: 50%;\n transform: translateX(-50%);\n display: inline-flex;\n align-items: center;\n gap: 2px;\n padding: 4px;\n border-radius: 12px;\n background: rgba(28, 28, 30, 0.82);\n backdrop-filter: blur(8px);\n box-shadow: 0 8px 28px rgba(0, 0, 0, 0.4);\n}\n.zi__tool {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 32px;\n border: none;\n border-radius: 9px;\n background: transparent;\n color: #fff;\n font-size: 19px;\n line-height: 1;\n cursor: pointer;\n transition: background 0.15s ease;\n}\n.zi__tool:hover {\n background: rgba(255, 255, 255, 0.16);\n}\n.zi__pct {\n min-width: 56px;\n height: 32px;\n padding: 0 8px;\n border: none;\n border-radius: 9px;\n background: transparent;\n color: #fff;\n font-family: inherit;\n font-size: 12.5px;\n font-variant-numeric: tabular-nums;\n cursor: pointer;\n transition: background 0.15s ease;\n}\n.zi__pct:hover {\n background: rgba(255, 255, 255, 0.16);\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"ZoomImage.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/ZoomImage.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, shallowRef, useTemplateRef, watch } from 'vue'\n\n/**\n * A pan + zoom image stage, shared by the full-screen {@link ImageLightbox} and the in-archive image preview\n * ({@link ZipBrowser}/{@link SevenZipBrowser}/{@link ArchiveBrowser}). Fills its parent (which must give it a\n * size). Wheel zooms toward the cursor, drag pans, double-click toggles fit↔2×, and a bottom toolbar offers\n * −/百分比/+. Initial fit is \"contain\" for a normal image, or fill-width-top-aligned for a `long` one (a tall\n * screenshot) so it reads top-down and pans vertically. A no-drag click on empty area emits `emptyClick` (the\n * lightbox closes on it; the archive preview ignores it).\n *\n * Exposes `zoomIn`/`zoomOut`/`fit` so a host can wire keyboard shortcuts to them.\n */\nconst props = withDefaults(defineProps<{ src: string; alt?: string; long?: boolean }>(), {\n alt: '',\n long: false,\n})\nconst emit = defineEmits<{ emptyClick: [] }>()\n\nconst stageEl = useTemplateRef<HTMLElement>('stageEl')\nconst imgEl = useTemplateRef<HTMLImageElement>('imgEl')\nconst natW = shallowRef(0)\nconst natH = shallowRef(0)\nconst scale = shallowRef(1) // CSS px per image px (1 = 100%)\nconst tx = shallowRef(0) // translate of the image's top-left within the stage\nconst ty = shallowRef(0)\nconst fitScale = shallowRef(1) // scale that \"contains\" the image in the stage\nconst ready = shallowRef(false) // image measured → show it (avoids a pre-fit flash)\n\nconst percent = shallowRef(100)\nconst FIT_PADDING = 24\nfunction syncPercent(): void {\n percent.value = Math.round(scale.value * 100)\n}\n\nfunction stageSize(): { w: number; h: number } {\n const el = stageEl.value\n if (el && el.clientWidth) return { w: el.clientWidth, h: el.clientHeight }\n return { w: 1024, h: 768 }\n}\nfunction minScale(): number {\n return Math.max(0.05, Math.min(fitScale.value, 1) * 0.5)\n}\nfunction maxScale(): number {\n return Math.max(fitScale.value, 1) * 8\n}\n\n// Keep the image within the stage: center an axis whose content is smaller than the stage, otherwise clamp so\n// its edges can't be dragged inside the viewport. `topAlign` pins a too-tall image to its top (initial long view).\nfunction clampView(topAlign = false): void {\n const { w, h } = stageSize()\n const cw = natW.value * scale.value\n const ch = natH.value * scale.value\n tx.value = cw <= w ? (w - cw) / 2 : Math.min(0, Math.max(w - cw, tx.value))\n if (ch <= h) ty.value = (h - ch) / 2\n else if (topAlign) ty.value = 0\n else ty.value = Math.min(0, Math.max(h - ch, ty.value))\n syncPercent()\n}\n\n// (Re)fit the measured image: contain it, or — for a long image — fill the width and pin to the top.\nfunction fitView(): void {\n if (!natW.value || !natH.value) return\n const { w, h } = stageSize()\n const padding = props.long ? 0 : FIT_PADDING * 2\n const availableWidth = Math.max(1, w - padding)\n const availableHeight = Math.max(1, h - padding)\n fitScale.value = Math.min(availableWidth / natW.value, availableHeight / natH.value)\n scale.value = props.long ? (w * 0.98) / natW.value : Math.min(fitScale.value, 1)\n clampView(props.long)\n}\n\nfunction clampScale(s: number): number {\n return Math.min(maxScale(), Math.max(minScale(), s))\n}\n\n// Zoom keeping the image point under (cx, cy) — stage-relative coords — fixed in place.\nfunction zoomAt(cx: number, cy: number, factor: number): void {\n const next = clampScale(scale.value * factor)\n if (next === scale.value) return\n const ix = (cx - tx.value) / scale.value\n const iy = (cy - ty.value) / scale.value\n scale.value = next\n tx.value = cx - ix * next\n ty.value = cy - iy * next\n clampView(false)\n}\nfunction zoomCenter(factor: number): void {\n const { w, h } = stageSize()\n zoomAt(w / 2, h / 2, factor)\n}\n\nfunction onWheel(e: WheelEvent): void {\n const rect = stageEl.value?.getBoundingClientRect()\n const cx = rect ? e.clientX - rect.left : e.clientX\n const cy = rect ? e.clientY - rect.top : e.clientY\n zoomAt(cx, cy, e.deltaY < 0 ? 1.15 : 1 / 1.15)\n}\nfunction onDblClick(e: MouseEvent): void {\n const rect = stageEl.value?.getBoundingClientRect()\n const cx = rect ? e.clientX - rect.left : e.clientX\n const cy = rect ? e.clientY - rect.top : e.clientY\n // Near the fit scale → zoom in toward the cursor; otherwise snap back to fit.\n if (scale.value <= fitScale.value * 1.05)\n zoomAt(cx, cy, Math.max(2, 1 / Math.max(fitScale.value, 0.001)))\n else fitView()\n}\n\n// Pointer drag → pan (covers mouse + touch). A no-move pointerup on empty area emits `emptyClick`.\nlet dragging = false\nlet moved = false\nlet lastX = 0\nlet lastY = 0\nlet downTarget: EventTarget | null = null\n\nfunction onPointerDown(e: PointerEvent): void {\n if (e.button !== 0) return\n dragging = true\n moved = false\n lastX = e.clientX\n lastY = e.clientY\n downTarget = e.target\n stageEl.value?.setPointerCapture(e.pointerId)\n}\nfunction onPointerMove(e: PointerEvent): void {\n if (!dragging) return\n const dx = e.clientX - lastX\n const dy = e.clientY - lastY\n if (!moved && Math.abs(dx) + Math.abs(dy) > 3) moved = true\n lastX = e.clientX\n lastY = e.clientY\n tx.value += dx\n ty.value += dy\n clampView(false)\n}\nfunction onPointerUp(e: PointerEvent): void {\n if (!dragging) return\n dragging = false\n try {\n stageEl.value?.releasePointerCapture(e.pointerId)\n } catch {\n /* capture already released — ignore */\n }\n if (!moved && downTarget !== imgEl.value) emit('emptyClick')\n}\n\nfunction onImgLoad(): void {\n const img = imgEl.value\n if (!img) return\n natW.value = img.naturalWidth || 1\n natH.value = img.naturalHeight || 1\n fitView()\n ready.value = true\n}\n\n// Re-fit when the container is resized (the side panel's drag splitter, or the window).\nlet ro: ResizeObserver | null = null\nfunction onResize(): void {\n if (ready.value) fitView()\n}\n\n// Reset for a new source; the transform is recomputed on the <img>'s load.\nwatch(\n () => props.src,\n () => {\n ready.value = false\n natW.value = 0\n natH.value = 0\n scale.value = 1\n tx.value = 0\n ty.value = 0\n fitScale.value = 1\n syncPercent()\n },\n)\n\nonMounted(() => {\n if (typeof ResizeObserver !== 'undefined' && stageEl.value) {\n ro = new ResizeObserver(onResize)\n ro.observe(stageEl.value)\n }\n})\nonBeforeUnmount(() => ro?.disconnect())\n\ndefineExpose({\n zoomIn: () => zoomCenter(1.2),\n zoomOut: () => zoomCenter(1 / 1.2),\n fit: () => fitView(),\n})\n</script>\n\n<template>\n <div\n ref=\"stageEl\"\n class=\"zi\"\n :class=\"{ 'zi--grab': scale > fitScale }\"\n @wheel.prevent=\"onWheel\"\n @pointerdown=\"onPointerDown\"\n @pointermove=\"onPointerMove\"\n @pointerup=\"onPointerUp\"\n @pointercancel=\"onPointerUp\"\n @dblclick=\"onDblClick\"\n >\n <img\n ref=\"imgEl\"\n class=\"zi__img\"\n :class=\"{ 'zi__img--ready': ready }\"\n :src=\"src\"\n :alt=\"alt\"\n draggable=\"false\"\n :style=\"{ transform: `translate(${tx}px, ${ty}px) scale(${scale})` }\"\n @load=\"onImgLoad\"\n @dragstart.prevent\n />\n <div class=\"zi__toolbar\" @pointerdown.stop @dblclick.stop @wheel.stop>\n <button type=\"button\" class=\"zi__tool\" title=\"缩小 (-)\" @click=\"zoomCenter(1 / 1.2)\">\n −\n </button>\n <button type=\"button\" class=\"zi__pct\" title=\"适应窗口 (0)\" @click=\"fitView()\">\n {{ percent }}%\n </button>\n <button type=\"button\" class=\"zi__tool\" title=\"放大 (+)\" @click=\"zoomCenter(1.2)\">+</button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.zi {\n position: relative;\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n cursor: zoom-out;\n touch-action: none;\n}\n.zi--grab {\n cursor: grab;\n}\n.zi--grab:active {\n cursor: grabbing;\n}\n.zi__img {\n position: absolute;\n top: 0;\n left: 0;\n transform-origin: 0 0;\n will-change: transform;\n user-select: none;\n -webkit-user-drag: none;\n opacity: 0;\n}\n.zi__img--ready {\n opacity: 1;\n}\n.zi__toolbar {\n position: absolute;\n bottom: 14px;\n left: 50%;\n transform: translateX(-50%);\n display: inline-flex;\n align-items: center;\n gap: 2px;\n padding: 4px;\n border-radius: 12px;\n background: rgba(28, 28, 30, 0.82);\n backdrop-filter: blur(8px);\n box-shadow: 0 8px 28px rgba(0, 0, 0, 0.4);\n}\n.zi__tool {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 32px;\n border: none;\n border-radius: 9px;\n background: transparent;\n color: #fff;\n font-size: 19px;\n line-height: 1;\n cursor: pointer;\n transition: background 0.15s ease;\n}\n.zi__tool:hover {\n background: rgba(255, 255, 255, 0.16);\n}\n.zi__pct {\n min-width: 56px;\n height: 32px;\n padding: 0 8px;\n border: none;\n border-radius: 9px;\n background: transparent;\n color: #fff;\n font-family: inherit;\n font-size: 12.5px;\n font-variant-numeric: tabular-nums;\n cursor: pointer;\n transition: background 0.15s ease;\n}\n.zi__pct:hover {\n background: rgba(255, 255, 255, 0.16);\n}\n</style>\n"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { PDFDocumentProxy } from 'pdfjs-dist';
2
+ export interface PdfNavigationItem {
3
+ id: string;
4
+ title: string;
5
+ level: number;
6
+ pageNumber: number | null;
7
+ }
8
+ export declare function loadPdfNavigation(document: PDFDocumentProxy): Promise<{
9
+ outline: PdfNavigationItem[];
10
+ pageLabels: string[];
11
+ }>;
@@ -0,0 +1,32 @@
1
+ //#region src/lib/pdfNavigation.ts
2
+ async function e(e, t) {
3
+ if (!t) return null;
4
+ try {
5
+ let n = (typeof t == "string" ? await e.getDestination(t) : t)?.[0];
6
+ return !n || typeof n == "number" ? typeof n == "number" ? n + 1 : null : await e.getPageIndex(n) + 1;
7
+ } catch {
8
+ return null;
9
+ }
10
+ }
11
+ async function t(n, r, i = 0, a = "root") {
12
+ return (await Promise.all(r.map(async (r, o) => {
13
+ let s = `${a}-${o}`;
14
+ return [{
15
+ id: `pdf-outline-${s}`,
16
+ title: r.title.trim() || "未命名书签",
17
+ level: i,
18
+ pageNumber: await e(n, r.dest)
19
+ }, ...await t(n, r.items ?? [], i + 1, s)];
20
+ }))).flat();
21
+ }
22
+ async function n(e) {
23
+ let [n, r] = await Promise.all([e.getOutline().then((n) => t(e, n ?? [])).catch(() => []), e.getPageLabels().catch(() => null)]);
24
+ return {
25
+ outline: n,
26
+ pageLabels: r ?? []
27
+ };
28
+ }
29
+ //#endregion
30
+ export { n as loadPdfNavigation };
31
+
32
+ //# sourceMappingURL=pdfNavigation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdfNavigation.js","names":[],"sources":["../../src/lib/pdfNavigation.ts"],"sourcesContent":["import type { PDFDocumentProxy } from 'pdfjs-dist'\n\nexport interface PdfNavigationItem {\n id: string\n title: string\n level: number\n pageNumber: number | null\n}\n\ntype PdfOutline = Awaited<ReturnType<PDFDocumentProxy['getOutline']>>\ntype PdfOutlineItem = NonNullable<PdfOutline>[number]\ntype PdfPageReference = Parameters<PDFDocumentProxy['getPageIndex']>[0]\n\nasync function resolvePageNumber(\n document: PDFDocumentProxy,\n destination: PdfOutlineItem['dest'],\n): Promise<number | null> {\n if (!destination) return null\n\n try {\n const resolved =\n typeof destination === 'string' ? await document.getDestination(destination) : destination\n const pageReference = resolved?.[0]\n if (!pageReference || typeof pageReference === 'number') {\n return typeof pageReference === 'number' ? pageReference + 1 : null\n }\n return (await document.getPageIndex(pageReference as PdfPageReference)) + 1\n } catch {\n return null\n }\n}\n\nasync function flattenOutline(\n document: PDFDocumentProxy,\n items: readonly PdfOutlineItem[],\n level = 0,\n parentPath = 'root',\n): Promise<PdfNavigationItem[]> {\n const groups = await Promise.all(\n items.map(async (item, index) => {\n const path = `${parentPath}-${index}`\n const current: PdfNavigationItem = {\n id: `pdf-outline-${path}`,\n title: item.title.trim() || '未命名书签',\n level,\n pageNumber: await resolvePageNumber(document, item.dest),\n }\n const children = await flattenOutline(document, item.items ?? [], level + 1, path)\n return [current, ...children]\n }),\n )\n return groups.flat()\n}\n\nexport async function loadPdfNavigation(document: PDFDocumentProxy): Promise<{\n outline: PdfNavigationItem[]\n pageLabels: string[]\n}> {\n const [outline, pageLabels] = await Promise.all([\n document\n .getOutline()\n .then((items) => flattenOutline(document, items ?? []))\n .catch(() => []),\n document.getPageLabels().catch(() => null),\n ])\n return {\n outline,\n pageLabels: pageLabels ?? [],\n }\n}\n"],"mappings":";AAaA,eAAe,EACb,GACA,GACwB;CACxB,IAAI,CAAC,GAAa,OAAO;CAEzB,IAAI;EAGF,IAAM,KADJ,OAAO,KAAgB,WAAW,MAAM,EAAS,eAAe,CAAW,IAAI,EAAA,GAChD;EAIjC,OAHI,CAAC,KAAiB,OAAO,KAAkB,WACtC,OAAO,KAAkB,WAAW,IAAgB,IAAI,OAEzD,MAAM,EAAS,aAAa,CAAiC,IAAK;CAC5E,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAe,EACb,GACA,GACA,IAAQ,GACR,IAAa,QACiB;CAc9B,QAAO,MAbc,QAAQ,IAC3B,EAAM,IAAI,OAAO,GAAM,MAAU;EAC/B,IAAM,IAAO,GAAG,EAAW,GAAG;EAQ9B,OAAO,CAAC;GANN,IAAI,eAAe;GACnB,OAAO,EAAK,MAAM,KAAK,KAAK;GAC5B;GACA,YAAY,MAAM,EAAkB,GAAU,EAAK,IAAI;EAGjD,GAAS,GAAG,MADG,EAAe,GAAU,EAAK,SAAS,CAAC,GAAG,IAAQ,GAAG,CAAI,CACrD;CAC9B,CAAC,CACH,EAAA,CACc,KAAK;AACrB;AAEA,eAAsB,EAAkB,GAGrC;CACD,IAAM,CAAC,GAAS,KAAc,MAAM,QAAQ,IAAI,CAC9C,EACG,WAAW,CAAC,CACZ,MAAM,MAAU,EAAe,GAAU,KAAS,CAAC,CAAC,CAAC,CAAC,CACtD,YAAY,CAAC,CAAC,GACjB,EAAS,cAAc,CAAC,CAAC,YAAY,IAAI,CAC3C,CAAC;CACD,OAAO;EACL;EACA,YAAY,KAAc,CAAC;CAC7B;AACF"}