Vue integration
Build a PDF Viewer with Vue 3 and <script setup>. Start with the smallest useful component, then add page navigation, thumbnails, an annotation toolbar, and a sidebar.
Start with a minimal Viewer
Create Core after the component mounts, load a PDF, and destroy the instance when the component unmounts:
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import type { InkLayerInstance } from '@inklayer-dev/core'
import { createInkLayer } from '@inklayer-dev/core/capabilities'
import '@inklayer-dev/core/style'
const props = defineProps<{ source: string }>()
const root = ref<HTMLDivElement>()
const pages = ref<HTMLDivElement>()
let core: InkLayerInstance | undefined
onMounted(async () => {
core = await createInkLayer({
root: root.value!,
pageFlow: { container: pages.value!, scale: 'page-width' }
})
await core.load({ url: props.source })
})
onBeforeUnmount(() => {
void core?.destroy()
})
</script>
<template>
<div ref="root" class="pdf-viewer">
<div ref="pages" class="pdf-pages"></div>
</div>
</template>
<style scoped>
.pdf-viewer { height: 100vh; }
.pdf-pages { height: 100%; overflow: auto; }
</style>Use the component from its parent and pass the PDF URL through source:
<script setup lang="ts">
import PdfViewer from './PdfViewer.vue'
</script>
<template>
<PdfViewer source="/documents/review.pdf" />
</template>Add navigation and thumbnails
Use Page Flow to navigate and zoom. Render a thumbnail as a blob, then create an image URL for it:
core.getPageFlow()?.scrollToPage(pageIndex)
await core.getPageFlow()?.zoomIn()
const thumbnail = await core.viewer.renderThumbnail({ pageIndex, maxWidth: 140 })
const thumbnailUrl = URL.createObjectURL(thumbnail.blob)Keep the URL while the thumbnail is displayed. Call URL.revokeObjectURL(thumbnailUrl) when the component unmounts.
Build an annotation workspace
The complete component below adds previous/next buttons, zoom controls, thumbnails, a rectangle tool, and an annotation list. Clicking an annotation selects it and opens its page:
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import type { Annotation, InkLayerInstance } from '@inklayer-dev/core'
import { createInkLayer } from '@inklayer-dev/core/capabilities'
import '@inklayer-dev/core/style'
const props = defineProps<{ source: string }>()
const root = ref<HTMLDivElement>()
const pages = ref<HTMLDivElement>()
const annotations = ref<Annotation[]>([])
const thumbnails = ref<Array<{ pageIndex: number; url: string }>>([])
const currentPage = ref(0)
const pageCount = ref(0)
const zoom = ref(100)
let core: InkLayerInstance | undefined
let stop: (() => void) | undefined
onMounted(async () => {
core = await createInkLayer({
root: root.value!,
pageFlow: {
container: pages.value!,
scale: 'page-width',
onCurrentPageChanged: pageIndex => { currentPage.value = pageIndex },
onScaleChanged: state => { zoom.value = state.percentage }
}
})
const pdf = await core.load({ url: props.source })
pageCount.value = pdf.numPages
stop = core.annotations.repository.subscribe(() => {
annotations.value = [...core!.annotations.repository.getAll()]
})
for (let pageIndex = 0; pageIndex < pdf.numPages; pageIndex++) {
const thumbnail = await core.viewer.renderThumbnail({ pageIndex, maxWidth: 140 })
thumbnails.value.push({
pageIndex,
url: URL.createObjectURL(thumbnail.blob)
})
}
})
function goTo(pageIndex: number): void {
if (pageIndex >= 0 && pageIndex < pageCount.value) {
core?.getPageFlow()?.scrollToPage(pageIndex)
}
}
function selectAnnotation(annotation: Annotation): void {
core?.annotations.setSelection({ ids: [annotation.id] })
goTo(annotation.pageIndex)
}
onBeforeUnmount(() => {
stop?.()
for (const thumbnail of thumbnails.value) URL.revokeObjectURL(thumbnail.url)
void core?.destroy()
})
</script>
<template>
<div ref="root" class="workspace">
<header class="toolbar">
<button @click="goTo(currentPage - 1)">Previous</button>
<span>{{ currentPage + 1 }} / {{ pageCount }}</span>
<button @click="goTo(currentPage + 1)">Next</button>
<button @click="core?.getPageFlow()?.zoomOut()">−</button>
<span>{{ zoom }}%</span>
<button @click="core?.getPageFlow()?.zoomIn()">+</button>
<button @click="core?.annotations.setTool('select')">Select</button>
<button @click="core?.annotations.setTool('rectangle')">Rectangle</button>
</header>
<aside class="sidebar">
<button v-for="thumbnail in thumbnails" :key="thumbnail.pageIndex"
@click="goTo(thumbnail.pageIndex)">
<img :src="thumbnail.url" :alt="`Page ${thumbnail.pageIndex + 1}`">
</button>
</aside>
<div ref="pages" class="pages"></div>
<aside class="sidebar">
<button v-for="annotation in annotations" :key="annotation.id"
@click="selectAnnotation(annotation)">
{{ annotation.type }} · Page {{ annotation.pageIndex + 1 }}
</button>
</aside>
</div>
</template>
<style scoped>
.workspace {
display: grid;
grid-template-columns: 160px minmax(0, 1fr) 220px;
grid-template-rows: auto minmax(0, 1fr);
height: 100vh;
}
.toolbar { grid-column: 1 / -1; }
.pages, .sidebar { overflow: auto; }
.sidebar img { max-width: 100%; }
</style>The parent uses PdfWorkspace in the same way as the minimal component:
<script setup lang="ts">
import PdfWorkspace from './PdfWorkspace.vue'
</script>
<template>
<PdfWorkspace source="/documents/review.pdf" />
</template>For text highlights, author permissions, and saving annotations, continue with Create your first annotation and Persist annotations.