├── .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 | 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 | [![Awesome List](https://raw.githubusercontent.com/sindresorhus/awesome/main/media/mentioned-badge.svg)](https://github.com/vuejs/awesome-vue) 6 | [![npm Version](https://img.shields.io/npm/v/vue-pdf-embed?style=flat)](https://npmjs.com/package/vue-pdf-embed) 7 | [![npm Downloads](https://img.shields.io/npm/dm/vue-pdf-embed?style=flat)](https://npmjs.com/package/vue-pdf-embed) 8 | [![GitHub Stars](https://img.shields.io/github/stars/hrynko/vue-pdf-embed?style=flat)](https://github.com/hrynko/vue-pdf-embed) 9 | [![License](https://img.shields.io/npm/l/vue-pdf-embed?style=flat)](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 | 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 | 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 | 485 | --------------------------------------------------------------------------------