Skip to content

Commit

Permalink
Merge pull request #66 from dlnsk/master
Browse files Browse the repository at this point in the history
Provide outline data to build TOC
  • Loading branch information
TaTo30 authored Nov 22, 2023
2 parents c8fe18b + 04889fd commit 94e9c74
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 1 deletion.
4 changes: 3 additions & 1 deletion docs/.vuepress/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Scale from '../components/Scale.vue'
import TextLayer from '../components/TextLayer.vue'
import XFALayer from '../components/XFALayer.vue'
import Watermark from '../components/Watermark.vue'
import TOC from '../components/TOC.vue'


export default defineClientConfig({
Expand All @@ -35,5 +36,6 @@ export default defineClientConfig({
app.component('AnnoForms', AnnoForms)
app.component('AnnoLinks', AnnoLinks)
app.component('Loaded', Loaded)
app.component('TOC', TOC)
}
})
})
1 change: 1 addition & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export default defineUserConfig({
'/examples/advanced/fit_parent.md',
'/examples/advanced/annotation_filter.md',
'/examples/advanced/multiple_pdf.md',
'/examples/advanced/toc.md',
]
},
{
Expand Down
27 changes: 27 additions & 0 deletions docs/components/ChaptersList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script>
export default {
name: 'ChaptersList',
props: {
items: Array,
},
methods: {
onChapterClick: function (e) {
this.$emit('chapterClick', e)
},
},
}
</script>

<template>
<ol>
<li v-for="item in items">
<a href="#" @click.prevent="$emit('chapterClick', item.destination)">{{ item.title }}</a>
<div v-if="item.items.length">
<ChaptersList
:items="item.items"
@chapterClick="onChapterClick"
></ChaptersList>
</div>
</li>
</ol>
</template>
73 changes: 73 additions & 0 deletions docs/components/TOC.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script setup>
import { ref, triggerRef, watchEffect } from 'vue';
import { VuePDF, getPDFDestination, usePDF } from '@tato30/vue-pdf';
import { withBase } from '@vuepress/client';
import ChaptersList from './ChaptersList.vue';
const { pdf, info } = usePDF(withBase('/example_045.pdf'))
const eventValue = ref({})
const outlineTree = ref([])
watchEffect(() => {
if (info.value.outline !== undefined) {
outlineTree.value = info.value.outline.map(function convert(node) {
return {
title: node.title,
destination: getPDFDestination(info.value.document, node.dest),
items: node.items.map((item) => {
return convert(item)
}),
}
})
}
})
triggerRef(info)
function onChapterClick(value) {
value.then(v => {
console.log(v)
eventValue.value = v
})
}
</script>

<template>
<div id="toc_wrapper">
<div class="toc">
<ChaptersList
:items="outlineTree"
@chapterClick="onChapterClick"
>
</ChaptersList>
</div>
<div>
<div class="language-json" data-ext="json">
<pre class="language-json"><code>{{ eventValue }}</code></pre>
</div>

<div class="container">
<VuePDF :pdf="pdf" :scale="0.75" />
</div>
</div>
</div>
</template>

<style>
#toc_wrapper {
display: flex;
flex-direction: row;
}
#toc_wrapper .toc {
width: 300px;
background-color: #eaeaea;
}
#toc_wrapper ol ol {
padding-left: 20px;
}
#toc_wrapper ol {
padding-left: 2em;
}
#toc_wrapper a {
color: black;
}
</style>
52 changes: 52 additions & 0 deletions docs/examples/advanced/toc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Table of content

```vue
<script setup>
import { ref, triggerRef, watchEffect } from 'vue';
import { VuePDF, getPDFDestination, usePDF } from '@tato30/vue-pdf';
import { withBase } from '@vuepress/client';
import ChaptersList from './ChaptersList.vue';
const { pdf, info } = usePDF(withBase('/example_045.pdf'))
const outlineTree = ref([])
watchEffect(() => {
if (info.value.outline !== undefined) {
outlineTree.value = info.value.outline.map(function convert(node) {
return {
title: node.title,
destination: getPDFDestination(info.value.document, node.dest),
items: node.items.map((item) => {
return convert(item)
}),
}
})
}
})
triggerRef(info)
function onChapterClick(value) {
value.then(v => {
console.log(v)
})
}
</script>
<template>
<div id="toc_wrapper">
<div class="toc">
<ChaptersList
:items="outlineTree"
@chapterClick="onChapterClick"
>
</ChaptersList>
</div>
<div class="container">
<VuePDF :pdf="pdf" />
</div>
</div>
</template>
```
<ClientOnly>
<TOC />
</ClientOnly>
3 changes: 3 additions & 0 deletions src/components/usePDF.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,14 @@ export function usePDF(src: UsePDFSrc | Ref<UsePDFSrc>,
const metadata = await doc.getMetadata()
const attachments = (await doc.getAttachments()) as Record<string, unknown>
const javascript = await doc.getJavaScript()
const outline = await doc.getOutline();

info.value = {
document: doc,
metadata,
attachments,
javascript,
outline,
}
},
(error) => {
Expand Down
84 changes: 84 additions & 0 deletions src/components/utils/destination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Written by Jason Harwig as part of PDFjs React Outline Viewer
* Source: https://codesandbox.io/s/rp18w
*/
import { PDFDocumentProxy, RefProxy } from "pdfjs-dist/types/src/display/api";


type Base<T, S> = {type: T, spec: S}
// These are types from the PDF 1.7 reference manual; Adobe
// Table 151 – Destination syntax
// (Coordinates origin is bottom left of page)
type XYZ = Base<'XYZ', [left: number, top: number, zoom: number]>
type Fit = Base<'Fit', []>
type FitH = Base<'FitH', [top: number]>
type FitV = Base<'FitV', [left: number]>
type FitR = Base<'FitR', [ left: number, bottom: number, right: number, top: number ]>
type FitB = Base<'FitB', []>
type FitBH = Base<'FitBH', [top: number]>
type FitBV = Base<'FitBV', [left: number]>

type PDFLocation = XYZ | Fit | FitH | FitV | FitR | FitB | FitBH | FitBV
export interface PDFDestination {
pageIndex: number
location: PDFLocation
}

const isRefProxy = (obj: unknown): obj is RefProxy =>
Boolean(typeof obj === "object" && obj && "gen" in obj && "num" in obj);

const getDestinationArray = async (
doc: PDFDocumentProxy,
dest: string | any[] | null
): Promise<any[] | null> =>
typeof dest === "string" ? doc.getDestination(dest) : dest;

const getDestinationRef = async (
doc: PDFDocumentProxy,
destArray: any[] | null
): Promise<RefProxy | null> => {
if (destArray && isRefProxy(destArray[0])) {
return destArray[0];
}
return null;
};

const isXYZ = (obj: {type: string, spec: number[]}): obj is XYZ => obj.type === 'XYZ' && obj.spec.length === 3
const isFit = (obj: {type: string, spec: number[]}): obj is Fit => obj.type === 'Fit' && obj.spec.length === 0
const isFitH = (obj: {type: string, spec: number[]}): obj is FitH => obj.type === 'FitH' && obj.spec.length === 1
const isFitV = (obj: {type: string, spec: number[]}): obj is FitV => obj.type === 'FitV' && obj.spec.length === 1
const isFitR = (obj: {type: string, spec: number[]}): obj is FitR => obj.type === 'FitR' && obj.spec.length === 4
const isFitB = (obj: {type: string, spec: number[]}): obj is FitB => obj.type === 'FitB' && obj.spec.length === 0
const isFitBH = (obj: {type: string, spec: number[]}): obj is FitBH => obj.type === 'FitBH' && obj.spec.length === 1
const isFitBV = (obj: {type: string, spec: number[]}): obj is FitBV => obj.type === 'FitBV' && obj.spec.length === 1


const getLocation = (type: string, spec: number[]): PDFLocation | null => {
const obj = {type, spec}
if (isXYZ(obj)) return obj
if (isFit(obj)) return obj
if (isFitH(obj)) return obj
if (isFitV(obj)) return obj
if (isFitR(obj)) return obj
if (isFitB(obj)) return obj
if (isFitBH(obj)) return obj
if (isFitBV(obj)) return obj
console.warn("no location type found for ", type, spec)

return null
}

const isSpecLike = (list: any[]): list is number[] => list && list.every(v => !isNaN(v))

export async function getPDFDestination(document: PDFDocumentProxy, destination: string | any[] | null): Promise<PDFDestination | null> {
const destArray = await getDestinationArray(document, destination)
const destRef = await getDestinationRef(document, destArray);
if (!destRef || !destArray) return null;

const pageIndex = await document.getPageIndex(destRef);
const name = destArray[1].name
const rest = destArray.slice(2)
const location = isSpecLike(rest) ? getLocation(name, rest) : null

return {pageIndex, location: location ?? {type: 'Fit', spec: []}};
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export const VuePDFPlugin: Plugin = {
}

export * from './components'
export { getPDFDestination } from './components/utils/destination'

export default VuePDFPlugin

0 comments on commit 94e9c74

Please sign in to comment.