├── .husky
└── pre-commit
├── env.d.ts
├── .lintstagedrc
├── dev
├── main.ts
├── index.html
└── App.vue
├── .prettierrc
├── global.d.ts
├── vite.config.dev.ts
├── src
├── index.essential.ts
├── types.ts
├── index.ts
├── utils.ts
├── composables.ts
└── VuePdfEmbed.vue
├── tsconfig.test.json
├── tsconfig.lib.json
├── tsconfig.json
├── vitest.config.ts
├── tsconfig.node.json
├── tsconfig.build.json
├── .gitignore
├── vite.config.essential.ts
├── .eslintrc
├── .github
└── workflows
│ └── publish-to-npm.yml
├── CONTRIBUTING.md
├── LICENSE
├── vite.config.ts
├── test
└── VuePdfEmbed.test.ts
├── package.json
└── README.md
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | lint-staged --concurrent false
2 |
--------------------------------------------------------------------------------
/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "*.{js,ts,vue}": "eslint --fix",
3 | "*": "prettier --write"
4 | }
5 |
--------------------------------------------------------------------------------
/dev/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 |
3 | import App from './App.vue'
4 |
5 | createApp(App).mount('#app')
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "semi": false,
4 | "singleQuote": true,
5 | "tabWidth": 2,
6 | "trailingComma": "es5"
7 | }
8 |
--------------------------------------------------------------------------------
/global.d.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | interface Window {
3 | Vue: unknown
4 | VuePdfEmbed: unknown
5 | useVuePdfEmbed: unknown
6 | }
7 | }
8 |
9 | export {}
10 |
--------------------------------------------------------------------------------
/vite.config.dev.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 |
4 | export default defineConfig({
5 | root: 'dev',
6 | plugins: [vue()],
7 | })
8 |
--------------------------------------------------------------------------------
/src/index.essential.ts:
--------------------------------------------------------------------------------
1 | export { GlobalWorkerOptions } from 'pdfjs-dist/legacy/build/pdf.mjs'
2 | export { useVuePdfEmbed } from './composables'
3 | export { default } from './VuePdfEmbed.vue'
4 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.lib.json",
3 | "compilerOptions": {
4 | "composite": true,
5 | "lib": ["DOM"],
6 | "types": ["happy-dom", "node"]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.dom.json",
3 | "include": ["env.d.ts", "global.d.ts", "src/**/*"],
4 | "compilerOptions": {
5 | "composite": true,
6 | "baseUrl": "."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {
5 | "path": "./tsconfig.lib.json"
6 | },
7 | {
8 | "path": "./tsconfig.node.json"
9 | },
10 | {
11 | "path": "./tsconfig.test.json"
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { getDocument, type PDFDocumentProxy } from 'pdfjs-dist'
2 |
3 | export type Source = Parameters[0] | PDFDocumentProxy | null
4 |
5 | export type PasswordRequestParams = {
6 | callback: Function
7 | isWrongPassword: boolean
8 | }
9 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, mergeConfig } from 'vitest/config'
2 |
3 | import viteConfig from './vite.config'
4 |
5 | export default mergeConfig(
6 | viteConfig,
7 | defineConfig({
8 | test: {
9 | environment: 'happy-dom',
10 | },
11 | })
12 | )
13 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node20/tsconfig.json",
3 | "include": ["vite.config.*", "vitest.config.*"],
4 | "compilerOptions": {
5 | "composite": true,
6 | "module": "ESNext",
7 | "moduleResolution": "Bundler",
8 | "types": ["node"]
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.lib.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "declaration": true,
6 | "declarationDir": "dist/types",
7 | "emitDeclarationOnly": true,
8 | "noEmit": false,
9 | "stripInternal": true,
10 | "composite": false
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/dev/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | vue-pdf-embed
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | dist
4 |
5 | # Local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw?
23 |
24 | # TypeScript
25 | *.tsbuildinfo
26 |
--------------------------------------------------------------------------------
/vite.config.essential.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 |
4 | import { rollupOptions } from './vite.config'
5 |
6 | export default defineConfig({
7 | plugins: [vue()],
8 | build: {
9 | lib: {
10 | entry: new URL('./src/index.essential.ts', import.meta.url).pathname,
11 | name: 'VuePdfEmbed',
12 | fileName: 'index.essential',
13 | formats: ['es'],
14 | },
15 | rollupOptions,
16 | },
17 | })
18 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "plugin:vue/vue3-recommended",
5 | "@vue/typescript/recommended",
6 | "prettier"
7 | ],
8 | "parser": "vue-eslint-parser",
9 | "parserOptions": {
10 | "parser": "@typescript-eslint/parser",
11 | "ecmaVersion": "latest"
12 | },
13 | "plugins": ["@typescript-eslint", "prettier"],
14 | "rules": {
15 | "@typescript-eslint/ban-types": "off",
16 | "prettier/prettier": "warn",
17 | "vue/require-default-prop": "off"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { GlobalWorkerOptions } from 'pdfjs-dist/legacy/build/pdf.mjs'
2 | import PdfWorker from 'pdfjs-dist/legacy/build/pdf.worker.min.mjs?url'
3 |
4 | import { useVuePdfEmbed } from './composables'
5 | import VuePdfEmbed from './VuePdfEmbed.vue'
6 |
7 | if (window?.Vue) {
8 | window.VuePdfEmbed = VuePdfEmbed
9 | window.useVuePdfEmbed = useVuePdfEmbed
10 | }
11 |
12 | if (!GlobalWorkerOptions?.workerSrc) {
13 | GlobalWorkerOptions.workerSrc = PdfWorker
14 | }
15 |
16 | export { useVuePdfEmbed }
17 | export default VuePdfEmbed
18 |
--------------------------------------------------------------------------------
/.github/workflows/publish-to-npm.yml:
--------------------------------------------------------------------------------
1 | name: Publish to npm
2 |
3 | on:
4 | release:
5 | types:
6 | - published
7 |
8 | permissions:
9 | contents: read
10 | id-token: write
11 |
12 | env:
13 | HUSKY: 0
14 |
15 | jobs:
16 | publish:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v4
20 | - uses: actions/setup-node@v4
21 | with:
22 | node-version: 20
23 | registry-url: https://registry.npmjs.org/
24 | - run: npm ci
25 | - run: npm publish --provenance
26 | env:
27 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
28 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to vue-pdf-embed
2 |
3 | Thank you for considering to contribute to vue-pdf-embed!
4 |
5 | ## Bug reports and feature requests
6 |
7 | We use [GitHub issues](https://github.com/hrynko/vue-pdf-embed/issues) to report bugs and request new features. Please provide as much detail as possible when preparing the description, as this will help other contributors to better understand the problem.
8 |
9 | ## Proposing features and submitting fixes
10 |
11 | All code changes happen through [pull requests](https://github.com/hrynko/vue-pdf-embed/pulls). We actively welcome your features and fixes. Please provide a detailed description of the changes made and link them to [issues](https://github.com/hrynko/vue-pdf-embed/issues), if any.
12 |
13 | ## License
14 |
15 | By contributing, you agree that your contribution will be licensed under the [MIT License](LICENSE).
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Aliaksei Hrynko
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 | import copy from 'rollup-plugin-copy'
4 | import CleanCSS from 'clean-css'
5 | import type { RollupOptions } from 'rollup'
6 |
7 | export const rollupOptions: RollupOptions = {
8 | external: ['pdfjs-dist', 'vue'],
9 | output: {
10 | globals: {
11 | 'pdfjs-dist': 'pdfjsLib',
12 | vue: 'Vue',
13 | },
14 | compact: true,
15 | inlineDynamicImports: true,
16 | },
17 | }
18 |
19 | export default defineConfig({
20 | plugins: [
21 | copy({
22 | hook: 'writeBundle',
23 | targets: Object.entries({
24 | textLayer: [
25 | [3103, 3122],
26 | [582, 709],
27 | ],
28 | annotationLayer: [
29 | [3103, 3122],
30 | [710, 1074],
31 | ],
32 | }).map(([key, ranges]) => ({
33 | src: 'node_modules/pdfjs-dist/web/pdf_viewer.css',
34 | dest: 'dist/styles',
35 | rename: `${key}.css`,
36 | transform: (contents) => {
37 | const lines = contents.toString().split('\n')
38 | const css = ranges.reduce((acc, [start, end]) => {
39 | return acc + lines.slice(start, end).join('\n')
40 | }, '')
41 | return new CleanCSS().minify(css).styles + '\n'
42 | },
43 | })),
44 | }),
45 | vue(),
46 | ],
47 | build: {
48 | lib: {
49 | entry: new URL('./src/index.ts', import.meta.url).pathname,
50 | name: 'VuePdfEmbed',
51 | fileName: 'index',
52 | },
53 | rollupOptions,
54 | },
55 | })
56 |
--------------------------------------------------------------------------------
/dev/App.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 |
26 |
44 |
--------------------------------------------------------------------------------
/test/VuePdfEmbed.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test, vi } from 'vitest'
2 | import { flushPromises, mount } from '@vue/test-utils'
3 |
4 | import VuePdfEmbed from '../src/VuePdfEmbed.vue'
5 |
6 | HTMLCanvasElement.prototype.getContext = () => null
7 |
8 | vi.mock('pdfjs-dist/legacy/build/pdf.mjs', () => ({
9 | GlobalWorkerOptions: {},
10 | getDocument: () => ({
11 | promise: {
12 | numPages: 3,
13 | getPage: () => ({
14 | view: [],
15 | getViewport: () => ({
16 | clone: () => ({}),
17 | }),
18 | render: () => ({}),
19 | }),
20 | },
21 | }),
22 | }))
23 |
24 | vi.mock('pdfjs-dist/web/pdf_viewer.mjs', () => ({}))
25 |
26 | test('sets correct data', async () => {
27 | const wrapper = mount(VuePdfEmbed, {
28 | props: {
29 | source: 'SOURCE',
30 | },
31 | })
32 | await flushPromises()
33 | expect(wrapper.vm.doc).toBeTruthy()
34 | expect(wrapper.vm.doc?.numPages).toBe(3)
35 | })
36 |
37 | test('sets page IDs', async () => {
38 | const wrapper = mount(VuePdfEmbed, {
39 | props: {
40 | id: 'ID',
41 | source: 'SOURCE',
42 | },
43 | })
44 | await flushPromises()
45 | expect(wrapper.find('#ID.vue-pdf-embed').exists()).toBe(true)
46 | expect(wrapper.find('#ID-0.vue-pdf-embed__page').exists()).toBe(false)
47 | expect(wrapper.find('#ID-1.vue-pdf-embed__page').exists()).toBe(true)
48 | expect(wrapper.find('#ID-2.vue-pdf-embed__page').exists()).toBe(true)
49 | expect(wrapper.find('#ID-3.vue-pdf-embed__page').exists()).toBe(true)
50 | expect(wrapper.find('#ID-4.vue-pdf-embed__page').exists()).toBe(false)
51 | })
52 |
53 | test('emits successful events', async () => {
54 | const wrapper = mount(VuePdfEmbed, {
55 | props: {
56 | source: 'SOURCE',
57 | },
58 | })
59 | await flushPromises()
60 | expect(wrapper.emitted()).toHaveProperty('loaded')
61 | expect(wrapper.emitted()).toHaveProperty('rendered')
62 | })
63 |
64 | test('renders slots content', async () => {
65 | const wrapper = mount(VuePdfEmbed, {
66 | props: {
67 | source: 'SOURCE',
68 | },
69 | slots: {
70 | 'after-page': 'AFTER',
71 | 'before-page': 'BEFORE',
72 | },
73 | })
74 | await flushPromises()
75 | expect(wrapper.html()).toMatch('AFTER')
76 | expect(wrapper.html()).toMatch('BEFORE')
77 | })
78 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-pdf-embed",
3 | "version": "2.1.3",
4 | "description": "PDF embed component for Vue",
5 | "keywords": [
6 | "vue",
7 | "vuejs",
8 | "pdf"
9 | ],
10 | "license": "MIT",
11 | "author": "Aliaksei Hrynko (https://github.com/hrynko)",
12 | "main": "./dist/index.umd.js",
13 | "module": "./dist/index.mjs",
14 | "exports": {
15 | ".": {
16 | "import": "./dist/index.mjs",
17 | "require": "./dist/index.umd.js",
18 | "types": "./dist/types/index.d.ts"
19 | },
20 | "./dist/index.essential.mjs": {
21 | "import": "./dist/index.essential.mjs",
22 | "types": "./dist/types/index.essential.d.ts"
23 | },
24 | "./dist/styles/*.css": "./dist/styles/*.css"
25 | },
26 | "types": "./dist/types/index.d.ts",
27 | "files": [
28 | "dist"
29 | ],
30 | "repository": "github:hrynko/vue-pdf-embed",
31 | "scripts": {
32 | "prepare": "husky install && npm run build",
33 | "dev": "vite -c vite.config.dev.ts",
34 | "build": "vite build && vite build -c vite.config.essential.ts --emptyOutDir false",
35 | "postbuild": "vue-tsc -p tsconfig.build.json",
36 | "test": "vitest",
37 | "lint": "eslint . --ext .js,.ts,.vue --fix --ignore-path .gitignore",
38 | "format": "prettier . --write --ignore-path .gitignore"
39 | },
40 | "dependencies": {
41 | "pdfjs-dist": "^4.10.38"
42 | },
43 | "devDependencies": {
44 | "@tsconfig/node20": "^20.1.4",
45 | "@types/clean-css": "^4.2.11",
46 | "@typescript-eslint/eslint-plugin": "^7.18.0",
47 | "@typescript-eslint/parser": "^7.18.0",
48 | "@vitejs/plugin-vue": "^4.5.0",
49 | "@vue/eslint-config-typescript": "^13.0.0",
50 | "@vue/test-utils": "^2.4.6",
51 | "@vue/tsconfig": "^0.5.1",
52 | "clean-css": "^5.3.3",
53 | "eslint": "^8.57.0",
54 | "eslint-config-prettier": "^9.1.0",
55 | "eslint-plugin-prettier": "^5.2.1",
56 | "eslint-plugin-vue": "^9.27.0",
57 | "happy-dom": "^15.7.4",
58 | "husky": "^9.1.4",
59 | "lint-staged": "^15.2.9",
60 | "prettier": "^3.3.3",
61 | "rollup-plugin-copy": "^3.5.0",
62 | "sass": "^1.77.8",
63 | "typescript": "^5.5.4",
64 | "vite": "^5.4.7",
65 | "vitest": "^2.1.1",
66 | "vue": "^3.5.0",
67 | "vue-eslint-parser": "^9.4.3",
68 | "vue-tsc": "^2.0.29"
69 | },
70 | "peerDependencies": {
71 | "vue": "^3.3.0"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import type { PDFDocumentProxy } from 'pdfjs-dist'
2 |
3 | // @internal
4 | export function addPrintStyles(
5 | iframe: HTMLIFrameElement,
6 | sizeX: number,
7 | sizeY: number
8 | ) {
9 | const style = iframe.contentWindow!.document.createElement('style')
10 | style.textContent = `
11 | @page {
12 | margin: 3mm;
13 | size: ${sizeX}pt ${sizeY}pt;
14 | }
15 | body {
16 | margin: 0;
17 | }
18 | canvas {
19 | width: 100%;
20 | page-break-after: always;
21 | page-break-before: avoid;
22 | page-break-inside: avoid;
23 | }
24 | `
25 | iframe.contentWindow!.document.head.appendChild(style)
26 | iframe.contentWindow!.document.body.style.width = '100%'
27 | }
28 |
29 | // @internal
30 | export function createPrintIframe(
31 | container: HTMLDivElement
32 | ): Promise {
33 | return new Promise((resolve) => {
34 | const iframe = document.createElement('iframe')
35 | iframe.width = '0'
36 | iframe.height = '0'
37 | iframe.style.position = 'absolute'
38 | iframe.style.top = '0'
39 | iframe.style.left = '0'
40 | iframe.style.border = 'none'
41 | iframe.style.overflow = 'hidden'
42 | iframe.onload = () => resolve(iframe)
43 | container.appendChild(iframe)
44 | })
45 | }
46 |
47 | // @internal
48 | export function downloadPdf(data: Uint8Array, filename: string) {
49 | const url = URL.createObjectURL(
50 | new Blob([data], {
51 | type: 'application/pdf',
52 | })
53 | )
54 | const anchor = document.createElement('a')
55 | anchor.href = url
56 | anchor.download = filename
57 | anchor.style.display = 'none'
58 | document.body.append(anchor)
59 | anchor.click()
60 | setTimeout(() => {
61 | URL.revokeObjectURL(url)
62 | document.body.removeChild(anchor)
63 | }, 1000)
64 | }
65 |
66 | // @internal
67 | export function emptyElement(el?: HTMLElement | null) {
68 | while (el?.firstChild) {
69 | el.removeChild(el.firstChild)
70 | }
71 | }
72 |
73 | // @internal
74 | export function isDocument(doc: unknown): doc is PDFDocumentProxy {
75 | return doc ? Object.prototype.hasOwnProperty.call(doc, '_pdfInfo') : false
76 | }
77 |
78 | // @internal
79 | export function releaseChildCanvases(el?: HTMLElement | null) {
80 | el?.querySelectorAll('canvas').forEach((canvas: HTMLCanvasElement) => {
81 | canvas.width = 1
82 | canvas.height = 1
83 | canvas.getContext('2d')?.clearRect(0, 0, 1, 1)
84 | })
85 | }
86 |
--------------------------------------------------------------------------------
/src/composables.ts:
--------------------------------------------------------------------------------
1 | import {
2 | onBeforeUnmount,
3 | shallowRef,
4 | toValue,
5 | watch,
6 | watchEffect,
7 | type ComputedRef,
8 | type MaybeRef,
9 | type ShallowRef,
10 | } from 'vue'
11 | import { PasswordResponses, getDocument } from 'pdfjs-dist/legacy/build/pdf.mjs'
12 | import type {
13 | OnProgressParameters,
14 | PDFDocumentLoadingTask,
15 | PDFDocumentProxy,
16 | } from 'pdfjs-dist'
17 |
18 | import type { PasswordRequestParams, Source } from './types'
19 | import { isDocument } from './utils'
20 |
21 | export function useVuePdfEmbed({
22 | onError,
23 | onPasswordRequest,
24 | onProgress,
25 | source,
26 | }: {
27 | onError?: (e: Error) => unknown
28 | onPasswordRequest?: (passwordRequestParams: PasswordRequestParams) => unknown
29 | onProgress?: (progressParams: OnProgressParameters) => unknown
30 | source: ComputedRef | MaybeRef | ShallowRef
31 | }) {
32 | const doc = shallowRef(null)
33 | const docLoadingTask = shallowRef(null)
34 |
35 | watchEffect(async () => {
36 | const sourceValue = toValue(source)
37 |
38 | if (!sourceValue) {
39 | return
40 | } else if (isDocument(sourceValue)) {
41 | doc.value = sourceValue
42 | return
43 | }
44 |
45 | try {
46 | docLoadingTask.value = getDocument(
47 | sourceValue as Parameters[0]
48 | )
49 |
50 | if (onPasswordRequest) {
51 | docLoadingTask.value!.onPassword = (
52 | callback: Function,
53 | response: number
54 | ) => {
55 | onPasswordRequest({
56 | callback,
57 | isWrongPassword: response === PasswordResponses.INCORRECT_PASSWORD,
58 | })
59 | }
60 | }
61 |
62 | if (onProgress) {
63 | docLoadingTask.value.onProgress = onProgress
64 | }
65 |
66 | doc.value = await docLoadingTask.value.promise
67 | } catch (e) {
68 | doc.value = null
69 |
70 | if (onError) {
71 | onError(e as Error)
72 | } else {
73 | throw e
74 | }
75 | }
76 | })
77 |
78 | watch(doc, (_, oldDoc) => {
79 | oldDoc?.destroy()
80 | })
81 |
82 | onBeforeUnmount(() => {
83 | if (docLoadingTask.value?.onPassword) {
84 | // @ts-expect-error: onPassword must be reset
85 | docLoadingTask.value.onPassword = null
86 | }
87 | if (docLoadingTask.value?.onProgress) {
88 | // @ts-expect-error: onProgress must be reset
89 | docLoadingTask.value.onProgress = null
90 | }
91 | docLoadingTask.value?.destroy()
92 | if (!isDocument(toValue(source))) {
93 | doc.value?.destroy()
94 | }
95 | })
96 |
97 | return {
98 | doc,
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 📄 vue-pdf-embed
2 |
3 | PDF embed component for Vue 3 (see [Compatibility](#compatibility) for Vue 2 support)
4 |
5 | [](https://github.com/vuejs/awesome-vue)
6 | [](https://npmjs.com/package/vue-pdf-embed)
7 | [](https://npmjs.com/package/vue-pdf-embed)
8 | [](https://github.com/hrynko/vue-pdf-embed)
9 | [](https://github.com/hrynko/vue-pdf-embed/blob/main/LICENSE)
10 |
11 | ## Features
12 |
13 | - Controlled rendering of PDF documents in Vue apps
14 | - Handling password-protected documents
15 | - Includes text layer (searchable and selectable documents)
16 | - Includes annotation layer (annotations and links)
17 | - No peer dependencies or additional configuration required
18 | - Can be used directly in the browser (see [Examples](#examples))
19 |
20 | ## Compatibility
21 |
22 | This package is only compatible with Vue 3. For Vue 2 support, install `vue-pdf-embed@1` and refer to the [v1 docs](https://github.com/hrynko/vue-pdf-embed/tree/v1).
23 |
24 | ## Installation
25 |
26 | Depending on the environment, the package can be installed in one of the following ways:
27 |
28 | ```shell
29 | npm install vue-pdf-embed
30 | ```
31 |
32 | ```shell
33 | yarn add vue-pdf-embed
34 | ```
35 |
36 | ```html
37 |
38 | ```
39 |
40 | ## Usage
41 |
42 | ```vue
43 |
53 |
54 |
55 |
56 |
57 | ```
58 |
59 | ### Props
60 |
61 | | Name | Type | Accepted values | Description |
62 | | ------------------ | ---------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
63 | | annotationLayer | `boolean` | | whether the annotation layer should be enabled |
64 | | height | `number` | natural numbers | desired page height in pixels (ignored if the width property is specified) |
65 | | imageResourcesPath | `string` | URL or path with trailing slash | path for icons used in the annotation layer |
66 | | linkService | `PDFLinkService` | | document navigation service to override the default one (emitting `internal-link-clicked`) |
67 | | page | `number`
`number[]` | `1` to the last page number | page number(s) to display (displaying all pages if not specified) |
68 | | rotation | `number` | `0`, `90`, `180`, `270` (multiples of `90`) | desired page rotation angle in degrees |
69 | | scale | `number` | rational numbers | desired page viewport scale |
70 | | source | `string`
`object`
`PDFDocumentProxy` | document URL or Base64 or typed array or document proxy | source of the document to display |
71 | | textLayer | `boolean` | | whether the text layer should be enabled |
72 | | width | `number` | natural numbers | desired page width in pixels |
73 |
74 | ### Events
75 |
76 | | Name | Value | Description |
77 | | --------------------- | ----------------------------------------------------------------------- | ------------------------------------------ |
78 | | internal-link-clicked | destination page number | internal link was clicked |
79 | | loaded | PDF document proxy | finished loading the document |
80 | | loading-failed | error object | failed to load document |
81 | | password-requested | object with `callback` function and `isWrongPassword` flag | password is needed to display the document |
82 | | progress | object with number of `loaded` pages along with `total` number of pages | tracking document loading progress |
83 | | rendered | – | finished rendering the document |
84 | | rendering-failed | error object | failed to render document |
85 |
86 | ### Slots
87 |
88 | | Name | Props | Description |
89 | | ----------- | -------------------- | ------------------------------------ |
90 | | after-page | `page` (page number) | content to be added after each page |
91 | | before-page | `page` (page number) | content to be added before each page |
92 |
93 | ### Public Methods
94 |
95 | | Name | Arguments | Description |
96 | | -------- | ---------------------------------------------------------------------------- | ------------------------------------ |
97 | | download | filename (`string`) | download document |
98 | | print | print resolution (`number`), filename (`string`), all pages flag (`boolean`) | print document via browser interface |
99 |
100 | **Note:** Public methods can be accessed through a [template ref](https://vuejs.org/guide/essentials/template-refs.html).
101 |
102 | ## Common Issues and Caveats
103 |
104 | ### Server-Side Rendering
105 |
106 | This is a client-side library, so it is important to keep this in mind when working with SSR (server-side rendering) frameworks such as Nuxt. Depending on the framework used, you may need to properly configure the library import or use a wrapper.
107 |
108 | ### Web Worker Loading
109 |
110 | The web worker used to handle PDF documents is loaded by default. However, this may not be acceptable due to bundler restrictions or CSP (Content Security Policy). In such cases it is recommended to use the essential build (`index.essential.mjs`) and set up the worker manually using the exposed `GlobalWorkerOptions`.
111 |
112 | ```js
113 | import { GlobalWorkerOptions } from 'vue-pdf-embed/dist/index.essential.mjs'
114 | import PdfWorker from 'pdfjs-dist/build/pdf.worker.mjs?url'
115 |
116 | GlobalWorkerOptions.workerSrc = PdfWorker
117 | ```
118 |
119 | ### Document Loading
120 |
121 | Typically, document loading is internally handled within the component. However, for optimization purposes, the document can be loaded in the `useVuePdfEmbed` composable function and then passed as the `source` prop of the component (e.g. when sharing the source between multiple instances of the component).
122 |
123 | ```vue
124 |
129 |
130 |
131 |
132 |
133 | ```
134 |
135 | ### Resources
136 |
137 | The path to predefined CMaps should be specified to ensure correct rendering of documents containing non-Latin characters, as well as in case of CMap-related errors:
138 |
139 | ```vue
140 |
146 | ```
147 |
148 | The image resource path must be specified for annotations to display correctly:
149 |
150 | ```vue
151 |
155 | ```
156 |
157 | **Note:** The examples above use a CDN to load resources, however these resources can also be included in the build by installing the `pdfjs-dist` package as a dependency and further configuring the bundler.
158 |
159 | ## Examples
160 |
161 | [Basic Usage Demo (JSFiddle)](https://jsfiddle.net/hrynko/atcn32yp)
162 |
163 | [Advanced Usage Demo (JSFiddle)](https://jsfiddle.net/hrynko/273a59qr)
164 |
165 | [Lazy Loading Demo (JSFiddle)](https://jsfiddle.net/hrynko/u149my7h)
166 |
167 | ## License
168 |
169 | MIT License. Please see [LICENSE file](LICENSE) for more information.
170 |
--------------------------------------------------------------------------------
/src/VuePdfEmbed.vue:
--------------------------------------------------------------------------------
1 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
--------------------------------------------------------------------------------