├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report-🪲.md │ ├── feature-request-🆕.md │ └── something-else-👀.md └── workflows │ ├── deploy-docs.yaml │ └── publish-package.yaml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── docs ├── .vitepress │ ├── config.js │ ├── styles │ │ └── index.scss │ └── theme │ │ └── index.js ├── components │ ├── AllPages.vue │ ├── AnnoAttachment.vue │ ├── AnnoForms.vue │ ├── AnnoLinks.vue │ ├── AnnotationFilter.vue │ ├── AnnotationLayer.vue │ ├── AnnotationLoaded.vue │ ├── ChaptersList.vue │ ├── FitParent.vue │ ├── HighlightText.vue │ ├── Loaded.vue │ ├── MultiplePDF.vue │ ├── OnePage.vue │ ├── Rotation.vue │ ├── Scale.vue │ ├── TOC.vue │ ├── TextHighlight.vue │ ├── TextLayer.vue │ ├── TextLoaded.vue │ ├── Watermark.vue │ ├── XFALayer.vue │ └── XFALoaded.vue ├── examples │ ├── README.md │ ├── advanced │ │ ├── annotation_filter.md │ │ ├── fit_parent.md │ │ ├── highlight_text.md │ │ ├── multiple_pdf.md │ │ ├── toc.md │ │ └── watermark.md │ ├── annotation_events │ │ ├── annotation_attachment.md │ │ ├── annotation_forms.md │ │ └── annotation_links.md │ ├── basic │ │ ├── all_pages.md │ │ ├── annotation_layer.md │ │ ├── one_page.md │ │ ├── rotation.md │ │ ├── scale.md │ │ ├── text_layer.md │ │ └── xfa_layer.md │ ├── loaded_events │ │ ├── annotation_loaded.md │ │ ├── loaded.md │ │ ├── text_loaded.md │ │ └── xfa_loaded.md │ └── text_events │ │ └── text_highlight.md ├── guide │ ├── composables.md │ ├── events.md │ ├── introduction.md │ ├── methods.md │ ├── props.md │ └── slots.md ├── index.md ├── package.json └── public ├── package-lock.json ├── package.json ├── packages ├── playground │ ├── App.vue │ ├── env.d.ts │ ├── index.html │ ├── main.ts │ ├── package.json │ ├── src │ │ ├── AnnoLayer.vue │ │ ├── CustomLoading.vue │ │ ├── FitParent.vue │ │ ├── MultiPages.vue │ │ ├── PageNavigation.vue │ │ ├── TextLayer.vue │ │ └── XFALayer.vue │ ├── tsconfig.json │ └── vite.config.ts └── vue-pdf │ ├── README.md │ ├── package.json │ ├── src │ ├── components │ │ ├── VuePDF.vue │ │ ├── composable.ts │ │ ├── index.ts │ │ ├── layers │ │ │ ├── AnnotationLayer.vue │ │ │ ├── TextLayer.vue │ │ │ └── XFALayer.vue │ │ ├── types.ts │ │ └── utils │ │ │ ├── annotations.ts │ │ │ ├── destination.ts │ │ │ ├── highlight.ts │ │ │ ├── link_service.ts │ │ │ └── miscellaneous.ts │ ├── global.d.ts │ └── index.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vite.config.ts ├── samples ├── 14.pdf ├── 36.pdf ├── 41.pdf ├── 45.pdf ├── issue126.pdf ├── issue133.pdf ├── issue141.pdf ├── issue41.pdf ├── issue91.pdf ├── issue93.pdf ├── logo.png └── xfa.pdf ├── tests ├── env.d.ts ├── layers.spec.ts ├── loading.spec.ts ├── package.json ├── sizing.spec.ts ├── tsconfig.json └── vitest.config.ts └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | insert_final_newline = false 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | README.md 2 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require("@rushstack/eslint-patch/modern-module-resolution") 3 | 4 | module.exports = { 5 | extends: [ 6 | 'eslint:recommended', 7 | 'plugin:vue/vue3-essential', 8 | '@vue/eslint-config-typescript' 9 | ] 10 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: TaTo30 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report-🪲.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug report \U0001FAB2" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | A clear and concise description of what the bug is, (use english). 11 | 12 | **Additional context** 13 | - vue-pdf: [e.g. 1.9.5] 14 | - vue: [e.g. 3] 15 | 16 | 1. If you are crashing with a rendering issue attach the PDF file so we can test. 17 | 2. If a code snippet is given better :) 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request-🆕.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Feature request \U0001F195" 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | Explain here what you want `VuePDF` to have. (use english) 11 | 12 | Some considerations before submit the issue: 13 | * `VuePDF` is not a viewer per se, rather that is a component to make one. 14 | * This library is mostly for display PDFs, editing features aren't supported and won't be it on the near future. 15 | * Since `pdf.js` is the core library you can use its features directly without depends on `VuePDF` that only will use the most common features. 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/something-else-👀.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Something else \U0001F440" 3 | about: Questions, complains or greetings lives here 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yaml: -------------------------------------------------------------------------------- 1 | name: docs-build-deployment 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | paths: 8 | - 'docs/**/*' 9 | 10 | jobs: 11 | docs: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 22 23 | 24 | - name: Install packages 25 | run: npm install 26 | 27 | - name: Build VitePress site 28 | run: npm run build:docs 29 | 30 | - name: Deploy to GitHub Pages 31 | uses: crazy-max/ghaction-github-pages@v4 32 | with: 33 | target_branch: gh-pages 34 | build_dir: docs/.vitepress/dist 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/publish-package.yaml: -------------------------------------------------------------------------------- 1 | name: publish-npm-package 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | force-depth: 0 16 | 17 | - name: Setup NodeJS 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 22 21 | registry-url: https://registry.npmjs.org 22 | 23 | - name: Install packages 24 | run: npm install 25 | 26 | - name: Run tests and build 27 | run: npm run build 28 | 29 | - name: Publish packages 30 | working-directory: packages/vue-pdf 31 | run: npm publish --access public 32 | env: 33 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .ghpages 12 | cache 13 | temp 14 | playground/pdf/* 15 | playground/samples/* 16 | cjs 17 | esm 18 | .DS_Store 19 | dist 20 | dist-ssr 21 | coverage 22 | *.local 23 | 24 | /cypress/videos/ 25 | /cypress/screenshots/ 26 | 27 | # Editor directories and files 28 | .vscode/* 29 | !.vscode/extensions.json 30 | .idea 31 | *.suo 32 | *.ntvs* 33 | *.njsproj 34 | *.sln 35 | *.sw? 36 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | .vscode 11 | _site 12 | node_modules 13 | public 14 | index.html 15 | .DS_Store 16 | dist 17 | dist-ssr 18 | coverage 19 | *.local 20 | 21 | /cypress/videos/ 22 | /cypress/screenshots/ 23 | 24 | # Editor directories and files 25 | .vscode/* 26 | !.vscode/extensions.json 27 | .idea 28 | *.suo 29 | *.ntvs* 30 | *.njsproj 31 | *.sln 32 | *.sw? 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022–2023 Aldo Hernandez 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ./packages/vue-pdf/README.md -------------------------------------------------------------------------------- /docs/.vitepress/config.js: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:path"; 2 | import { version } from '../../packages/vue-pdf/package.json'; 3 | 4 | 5 | export default { 6 | vite: { 7 | optimizeDeps: { 8 | esbuildOptions: { 9 | supported: { 10 | "top-level-await": true 11 | }, 12 | }, 13 | }, 14 | build: { 15 | target: 'esnext' 16 | }, 17 | resolve: { 18 | alias: { 19 | "@tato30/vue-pdf": resolve(__dirname, "../../packages/vue-pdf/dist") 20 | }, 21 | }, 22 | }, 23 | title: "VuePDF", 24 | description: "PDF component for Vue 3", 25 | base: "/vue-pdf/", 26 | lastUpdated: true, 27 | head: [["link", { rel: "icon", type: "image/png", href: "/logo.png" }]], 28 | themeConfig: { 29 | logo: "/logo.png", 30 | editLink: { 31 | pattern: "https://github.com/TaTo30/vue-pdf/edit/master/docs/:path", 32 | }, 33 | socialLinks: [ 34 | { 35 | icon: "github", 36 | link: "https://github.com/TaTo30/vue-pdf", 37 | }, 38 | ], 39 | search: { 40 | provider: "local", 41 | }, 42 | nav: [ 43 | { 44 | text: "Guide", 45 | link: "/guide/introduction.md", 46 | }, 47 | { 48 | text: "Examples", 49 | items: [ 50 | { 51 | text: "Basic usages", 52 | link: "/examples/basic/one_page.md", 53 | }, 54 | { 55 | text: "Advanced usages", 56 | link: "/examples/advanced/watermark.md", 57 | }, 58 | { 59 | text: "Events", 60 | link: "/examples/loaded_events/loaded.md", 61 | }, 62 | ], 63 | }, 64 | { 65 | text: `v${version}`, 66 | items: [ 67 | { 68 | text: "Changelog", 69 | link: "https://github.com/TaTo30/vue-pdf/releases", 70 | }, 71 | { 72 | text: "Contributing", 73 | link: "https://github.com/TaTo30/vue-pdf#contributing", 74 | }, 75 | ], 76 | }, 77 | ], 78 | sidebar: { 79 | "/guide/": { 80 | base: "/guide/", 81 | items: [ 82 | { 83 | text: "Guide", 84 | items: [ 85 | { 86 | text: "Introduction", 87 | link: "introduction", 88 | }, 89 | { 90 | text: "Composables", 91 | link: "composables", 92 | }, 93 | ], 94 | }, 95 | { 96 | text: "Reference", 97 | items: [ 98 | { 99 | text: "Props", 100 | link: "props", 101 | }, 102 | { 103 | text: "Events", 104 | link: "events", 105 | }, 106 | { 107 | text: "Methods", 108 | link: "methods", 109 | }, 110 | { 111 | text: "Slots", 112 | link: "slots", 113 | }, 114 | ], 115 | }, 116 | ], 117 | }, 118 | "/examples/": { 119 | items: [ 120 | { 121 | text: "Basic usages", 122 | base: "/examples/basic/", 123 | items: [ 124 | { 125 | text: "One Page", 126 | link: "one_page", 127 | }, 128 | { 129 | text: "All Pages", 130 | link: "all_pages", 131 | }, 132 | { 133 | text: "Scale", 134 | link: "scale", 135 | }, 136 | { 137 | text: "Rotation", 138 | link: "rotation", 139 | }, 140 | { 141 | text: "Text Layer", 142 | link: "text_layer", 143 | }, 144 | { 145 | text: "Annotation Layer", 146 | link: "annotation_layer", 147 | }, 148 | { 149 | text: "XFA Layer", 150 | link: "xfa_layer", 151 | }, 152 | ], 153 | }, 154 | { 155 | text: "Advanced usages", 156 | base: "/examples/advanced/", 157 | items: [ 158 | { 159 | text: "Watermark", 160 | link: "watermark", 161 | }, 162 | { 163 | text: "Fit Parent", 164 | link: "fit_parent", 165 | }, 166 | { 167 | text: "Highlight Text", 168 | link: "highlight_text", 169 | }, 170 | { 171 | text: "Annotation Filter", 172 | link: "annotation_filter", 173 | }, 174 | { 175 | text: "Multiple PDF", 176 | link: "multiple_pdf", 177 | }, 178 | { 179 | text: "Table of Content", 180 | link: "toc", 181 | }, 182 | ], 183 | }, 184 | { 185 | text: "Events", 186 | base: "/examples/", 187 | items: [ 188 | { 189 | text: "Loaded Event", 190 | link: "/loaded_events/loaded", 191 | }, 192 | { 193 | text: "Text Loaded Event", 194 | link: "/loaded_events/text_loaded", 195 | }, 196 | { 197 | text: "Annotation Loaded Event", 198 | link: "/loaded_events/annotation_loaded", 199 | }, 200 | { 201 | text: "XFA Loaded Event", 202 | link: "/loaded_events/xfa_loaded", 203 | }, 204 | { 205 | text: "Highlight Event", 206 | link: "/text_events/text_highlight", 207 | }, 208 | { 209 | text: "Annotation Events", 210 | base: "/examples/annotation_events/", 211 | items: [ 212 | { 213 | text: "Form fields", 214 | link: "annotation_forms", 215 | }, 216 | { 217 | text: "Links", 218 | link: "annotation_links", 219 | }, 220 | { 221 | text: "Attachment", 222 | link: "annotation_attachment", 223 | }, 224 | ], 225 | }, 226 | ], 227 | }, 228 | ], 229 | }, 230 | }, 231 | }, 232 | }; -------------------------------------------------------------------------------- /docs/.vitepress/styles/index.scss: -------------------------------------------------------------------------------- 1 | :root{ 2 | --vp-home-hero-image-filter: blur(100px); 3 | --vp-home-hero-image-background-image: url('data:image/svg+xml,'); 4 | } 5 | 6 | .button-example { 7 | background-color: var(--vp-c-brand-2); 8 | color: white; 9 | padding: 10px; 10 | margin: 7px; 11 | border-radius: 4px; 12 | border: none; 13 | cursor: pointer; 14 | } 15 | 16 | .button-example:hover { 17 | background-color: var(--vp-c-brand-1); 18 | } 19 | 20 | .checkbox-example { 21 | width: 15px; 22 | height: 15px; 23 | } 24 | 25 | .input-example { 26 | appearance: none; 27 | padding: 7px 15px; 28 | border: 1px solid transparent; 29 | border-radius: 6px; 30 | outline: none; 31 | 32 | } 33 | .input-example:focus{ 34 | cursor: auto; 35 | border-color: var(--vp-c-brand-1); 36 | } 37 | 38 | .select-example { 39 | background-color: var(--vp-c-brand-2); 40 | color: white; 41 | padding: 10px; 42 | margin: 7px; 43 | border-radius: 4px; 44 | border: none; 45 | } 46 | 47 | .vue-pdf-container { 48 | display: flex; 49 | align-items:center; 50 | flex-direction: column 51 | } 52 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | import '../styles/index.scss' 3 | 4 | import AllPages from '../../components/AllPages.vue' 5 | import AnnoAttachment from '../../components/AnnoAttachment.vue' 6 | import AnnoForms from '../../components/AnnoForms.vue' 7 | import AnnoLinks from '../../components/AnnoLinks.vue' 8 | import AnnotationFilter from '../../components/AnnotationFilter.vue' 9 | import AnnotationLayer from '../../components/AnnotationLayer.vue' 10 | import FitParent from '../../components/FitParent.vue' 11 | import Loaded from '../../components/Loaded.vue' 12 | import MultiplePDF from '../../components/MultiplePDF.vue' 13 | import OnePage from '../../components/OnePage.vue' 14 | import Rotation from '../../components/Rotation.vue' 15 | import Scale from '../../components/Scale.vue' 16 | import TextLayer from '../../components/TextLayer.vue' 17 | import XFALayer from '../../components/XFALayer.vue' 18 | import Watermark from '../../components/Watermark.vue' 19 | import TOC from '../../components/TOC.vue' 20 | import HighlightText from '../../components/HighlightText.vue' 21 | import TextHighlight from '../../components/TextHighlight.vue' 22 | import TextLoaded from '../../components/TextLoaded.vue' 23 | import AnnotationLoaded from '../../components/AnnotationLoaded.vue' 24 | import XFALoaded from '../../components/XFALoaded.vue' 25 | 26 | /** @type {import('vitepress').Theme} */ 27 | export default { 28 | extends: DefaultTheme, 29 | enhanceApp({ app }) { 30 | app.component('OnePage', OnePage) 31 | app.component('Watermark', Watermark) 32 | app.component('AllPages', AllPages) 33 | app.component('Scale', Scale) 34 | app.component('Rotation', Rotation) 35 | app.component('TextLayer', TextLayer) 36 | app.component('AnnotationLayer', AnnotationLayer) 37 | app.component('XFALayer', XFALayer) 38 | app.component('FitParent', FitParent) 39 | app.component('AnnotationFilter', AnnotationFilter) 40 | app.component('MultiplePDF', MultiplePDF) 41 | app.component('AnnoAttachment', AnnoAttachment) 42 | app.component('AnnoForms', AnnoForms) 43 | app.component('AnnoLinks', AnnoLinks) 44 | app.component('Loaded', Loaded) 45 | app.component('TOC', TOC) 46 | app.component('HighlightText', HighlightText) 47 | app.component('TextHighlight', TextHighlight) 48 | app.component('TextLoaded', TextLoaded) 49 | app.component('AnnotationLoaded', AnnotationLoaded) 50 | app.component('XFALoaded', XFALoaded) 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /docs/components/AllPages.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /docs/components/AnnoAttachment.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | -------------------------------------------------------------------------------- /docs/components/AnnoForms.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | -------------------------------------------------------------------------------- /docs/components/AnnoLinks.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /docs/components/AnnotationFilter.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 29 | -------------------------------------------------------------------------------- /docs/components/AnnotationLayer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /docs/components/AnnotationLoaded.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /docs/components/ChaptersList.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 28 | -------------------------------------------------------------------------------- /docs/components/FitParent.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | -------------------------------------------------------------------------------- /docs/components/HighlightText.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 36 | -------------------------------------------------------------------------------- /docs/components/Loaded.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /docs/components/MultiplePDF.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 37 | -------------------------------------------------------------------------------- /docs/components/OnePage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | -------------------------------------------------------------------------------- /docs/components/Rotation.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | -------------------------------------------------------------------------------- /docs/components/Scale.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | -------------------------------------------------------------------------------- /docs/components/TOC.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 54 | 55 | 74 | -------------------------------------------------------------------------------- /docs/components/TextHighlight.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 36 | -------------------------------------------------------------------------------- /docs/components/TextLayer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /docs/components/TextLoaded.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /docs/components/Watermark.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 63 | -------------------------------------------------------------------------------- /docs/components/XFALayer.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /docs/components/XFALoaded.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | -------------------------------------------------------------------------------- /docs/examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples List -------------------------------------------------------------------------------- /docs/examples/advanced/annotation_filter.md: -------------------------------------------------------------------------------- 1 | # Annotations Filter 2 | 3 | ```vue 4 | 18 | 19 | 31 | ``` 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/examples/advanced/fit_parent.md: -------------------------------------------------------------------------------- 1 | # Fit parent 2 | 3 | ```vue 4 | 18 | 19 | 35 | ``` 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/examples/advanced/highlight_text.md: -------------------------------------------------------------------------------- 1 | # Highlight Text 2 | 3 | ```vue 4 | 17 | 18 | 28 | ``` 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/examples/advanced/multiple_pdf.md: -------------------------------------------------------------------------------- 1 | # Multiples PDF 2 | 3 | ```vue 4 | 28 | 29 | 39 | ``` 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/examples/advanced/toc.md: -------------------------------------------------------------------------------- 1 | # Table of content 2 | 3 | ```vue 4 | 35 | 36 | 49 | ``` 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/examples/advanced/watermark.md: -------------------------------------------------------------------------------- 1 | # Watermark Text 2 | 3 | ```vue 4 | 24 | 25 | 38 | ``` 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/examples/annotation_events/annotation_attachment.md: -------------------------------------------------------------------------------- 1 | # File attachment 2 | 3 | ```vue 4 | 13 | 14 | 19 | ``` 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/examples/annotation_events/annotation_forms.md: -------------------------------------------------------------------------------- 1 | # Forms fields 2 | 3 | ```vue 4 | 13 | 14 | 19 | ``` 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/examples/annotation_events/annotation_links.md: -------------------------------------------------------------------------------- 1 | # Links 2 | 3 | ```vue 4 | 13 | 14 | 19 | ``` 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/examples/basic/all_pages.md: -------------------------------------------------------------------------------- 1 | # All pages 2 | 3 | ```vue 4 | 9 | 10 | 15 | ``` 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/examples/basic/annotation_layer.md: -------------------------------------------------------------------------------- 1 | # Annotation Layer 2 | 3 | ```vue 4 | 12 | 13 | 23 | ``` 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/examples/basic/one_page.md: -------------------------------------------------------------------------------- 1 | # One page 2 | 3 | ```vue 4 | 11 | 12 | 26 | ``` 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/examples/basic/rotation.md: -------------------------------------------------------------------------------- 1 | # Rotation 2 | 3 | ```vue 4 | 11 | 12 | 26 | ``` 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/examples/basic/scale.md: -------------------------------------------------------------------------------- 1 | # Scale 2 | 3 | ```vue 4 | 11 | 12 | 26 | ``` 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/examples/basic/text_layer.md: -------------------------------------------------------------------------------- 1 | # Text Layer 2 | 3 | ```vue 4 | 12 | 13 | 23 | ``` 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/examples/basic/xfa_layer.md: -------------------------------------------------------------------------------- 1 | # XFA Forms 2 | 3 | ```vue 4 | 13 | 14 | 19 | ``` 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/examples/loaded_events/annotation_loaded.md: -------------------------------------------------------------------------------- 1 | # Annotation Loaded Event 2 | 3 | ::: warning 4 | Annotation loaded event's payload has too many data to display on screen, open the console to see the results. 5 | ::: 6 | 7 | ```vue 8 | 16 | 17 | 22 | ``` 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/examples/loaded_events/loaded.md: -------------------------------------------------------------------------------- 1 | # Loaded Event 2 | 3 | ```vue 4 | 13 | 14 | 19 | ``` 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/examples/loaded_events/text_loaded.md: -------------------------------------------------------------------------------- 1 | # Text Loaded Event 2 | 3 | ::: warning 4 | Text loaded event's payload has too many data to display on screen, open the console to see the results. 5 | ::: 6 | 7 | ```vue 8 | 16 | 17 | 22 | ``` 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/examples/loaded_events/xfa_loaded.md: -------------------------------------------------------------------------------- 1 | # XFA Loaded Event 2 | 3 | ```vue 4 | 16 | 17 | 22 | ``` 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/examples/text_events/text_highlight.md: -------------------------------------------------------------------------------- 1 | # Highlight Event 2 | 3 | ::: warning 4 | Highlight event's payload has too many data to display on screen, open the console to see the results. 5 | ::: 6 | 7 | ```vue 8 | 25 | 26 | 32 | ``` 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /docs/guide/composables.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: [2,3] 3 | --- 4 | 5 | # Composables 6 | 7 | ## usePDF 8 | 9 | This package provides a default composable named `usePDF` that loads and prepare the PDF Document for it usage with `VuePDF` component, also let you get some basic information and properties about the document. 10 | 11 | Keep in mind that `usePDF` use the same [DocumentInitParameter](https://github.com/mozilla/pdf.js/blob/38287d943532eee939ceffbe6861163f93805ca7/src/display/api.js#L145) as `pdf.js`, so you could decide how `pdf.js` should loads your PDF and then make use of more of `pdf.js` features that are not included in `VuePDF` by default. 12 | 13 | ```vue 14 | 19 | 20 | 23 | ``` 24 | 25 | ### Reactivity 26 | 27 | `usePDF` is also reactive if you use a `ref` instead of a plain `src`, when the value of `ref` changes the returned values also will chage. 28 | 29 | ```vue 30 | 37 | 38 | 41 | ``` 42 | 43 | ### Parameters 44 | 45 | #### src 46 | 47 | Type: `string | URL | TypedArray | DocumentInitParameters | ref | ref | ref | ref`
48 | Required: `True` 49 | 50 | This parameter is the same `src` of [pdf.js](https://github.com/mozilla/pdf.js/blob/38287d943532eee939ceffbe6861163f93805ca7/src/display/api.js#L145) 51 | 52 | ```js 53 | const { pdf, pages, info } = usePDF('sample.pdf') 54 | ``` 55 | 56 | #### options 57 | 58 | Type: `object` 59 | 60 | An object with the following properties: 61 | 62 | - `onPassword`: Callback function to request the document password if no password (or wrong password) was provided. 63 | - `onProgress`: Callback function to enable progress monitor. 64 | - `onError`: function to handle pdf loading errors 65 | 66 | ```js 67 | function onPassword(updatePassword, reason) { 68 | console.log(`Reason for callback: ${reason}`) 69 | updatePassword('password1234') 70 | } 71 | 72 | function onProgress({ loaded, total }) { 73 | console.log(`${loaded / total * 100}% Loaded`) 74 | } 75 | 76 | function onError(reason) { 77 | console.error(`PDF loading error: ${reason}`) 78 | } 79 | 80 | const { pdf, pages, info } = usePDF('sample.pdf', { 81 | onPassword, 82 | onProgress, 83 | onError 84 | }) 85 | ``` 86 | 87 | ### Properties 88 | 89 | > All values returned by [`usePDF`](#usepdf-composable) are [`shallowRef`](https://vuejs.org/api/reactivity-advanced.html#shallowref) objects. 90 | 91 | #### pdf 92 | 93 | Type: `PDFDocumentLoadingTask` 94 | 95 | Document's loading task, see [PDFDocumentLoadingTask](https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib-PDFDocumentLoadingTask.html) for more details. 96 | 97 | --- 98 | 99 | #### pages 100 | 101 | Type: `int` 102 | 103 | Document's number pages. 104 | 105 | --- 106 | 107 | #### info 108 | 109 | Type: `object` 110 | 111 | Document's information object. 112 | 113 | ```json 114 | { 115 | "metadata": {...}, // Metadata object 116 | "attachments": {...}, // File attachments object 117 | "javascript": [...], // Array of embedded scripts 118 | "outline": {...} // Outline objects 119 | } 120 | ``` 121 | --- 122 | 123 | #### getPDFDestination 124 | 125 | Type: `function` 126 | 127 | This function returns the page number referenced by `dest` object used by internal-links or outline object. Check the related example in [Table of Content](../examples/advanced/toc.md) 128 | 129 | --- 130 | 131 | #### print 132 | 133 | Type: `function` 134 | 135 | Open the browser's print dialog with current PDF loaded with the following parameters: 136 | 137 | - `dpi`: Pages resolution (default: `150`). 138 | - `filename`: Filename of the printed file (default: `'filename'`). 139 | 140 | --- 141 | 142 | #### download 143 | 144 | Type: `function` 145 | 146 | Trigger a downloading action using an `HTMLAnchorElement` with the following parameters: 147 | 148 | - `filename`: Filename of the downloaded file (default: `'filename'`) 149 | 150 | --- 151 | 152 | ### Document API 153 | 154 | You can access to [PDFDocumentProxy](https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib-PDFDocumentProxy.html) through [pdf's](#pdf) promise property and use its API methods to get more document's info like `annotationStorage` or use functions like `saveDocument`, `cleanup`, etc. 155 | 156 | ```js 157 | const { pdf } = usePDF('document.pdf') 158 | 159 | function doSomething() { 160 | pdf.value.promise.then((doc) => { 161 | // doc.annotationsStorage 162 | // doc.saveDocument() 163 | // doc.cleanup() 164 | // doc.getData() 165 | // ... 166 | }) 167 | } 168 | ``` 169 | 170 | ## Make your own composable 171 | 172 | Using `usePDF` it's not required, you can use the `pdf.js` API in your components or build your own composable yourself. Just need to be sure to send on [`pdf`](./props.md#pdf) prop a `shallowRef | ref` [PDFDocumentLoadingTask](https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib-PDFDocumentLoadingTask.html) object. 173 | 174 | ```vue 175 | 190 | 191 | 196 | ``` -------------------------------------------------------------------------------- /docs/guide/events.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # Events 6 | 7 | ## loaded 8 | 9 | ```vue 10 | 11 | ``` 12 | 13 | Emitted when page has finished to render, the payload value contains the page's data. 14 | 15 | Payload example: 16 | ```json 17 | { 18 | "viewBox": [0, 0, 595.276, 841.89], 19 | "scale": 1, 20 | "rotation": 90, 21 | "offsetX": 0, 22 | "offsetY": 0, 23 | "transform": [0, 1, 1, 0, 0, 0], 24 | "width": 841.89, 25 | "height": 595.276 26 | } 27 | ``` 28 | 29 | ## text-loaded 30 | 31 | ```vue 32 | 33 | ``` 34 | 35 | Emitted when text layer has finished to render, the payload value contains the `textDivs` and `textContent` of the page. 36 | 37 | Payload example: 38 | ```json 39 | { 40 | "textContent": { 41 | "items": [{ 42 | "dir": "ltr", 43 | "fontName": "g_d3_f1", 44 | "hasEOL": true, 45 | "height": 17.9328, 46 | "str": "Trace-based Just-in-Time Type Specialization for Dynamic", 47 | "transform": [17.9328, 0, 0, 17.9328, 90.5159, 700.6706], 48 | "width": 449.09111040000033 49 | }], // ... more text items 50 | "styles": { 51 | "g_d3_f1": { 52 | "fontFamily": "sans-serif", 53 | "ascent": 0.69, 54 | "descent": -0.209, 55 | "vertical": false 56 | } // ... more objects 57 | } 58 | }, 59 | "textDivs": ["", "", "..."] 60 | } 61 | ``` 62 | 63 | ## annotation-loaded 64 | 65 | ```vue 66 | 67 | ``` 68 | 69 | Emitted when annotation layer has finished to render, the payload value contains the `annotations` of the page. 70 | 71 | Payload example: 72 | ```json 73 | [ 74 | { 75 | "annotationFlags": 4, 76 | "annotationType": 20, 77 | "rotation": 0, 78 | "fieldType": "Tx", 79 | "subType": "Widget" 80 | // more properties... 81 | } 82 | ] // more annotations 83 | ``` 84 | 85 | ## xfa-loaded 86 | 87 | ```vue 88 | 89 | ``` 90 | 91 | Emitted when XFA page has finished to render. 92 | 93 | 94 | ## highlight 95 | 96 | ```vue 97 | 98 | ``` 99 | 100 | Emitted when a text has been searched in page using [highlight-text](/guide/props.md#highlight-text) and [highlight-options](/guide/props.md#highlight-options), this event return a list of matches and the page where the text was found with its `textDivs` and `textContent`. 101 | 102 | Check the example: [Highlight Event](/examples/text_events/text_highlight.md) 103 | 104 | 105 | 106 | ## annotation 107 | 108 | 109 | ```vue 110 | 111 | ``` 112 | 113 | Emitted when user has an interaction with any annotation. 114 | 115 | Annotation event data depends on what type of annotation has triggered the event, in general, the events value follows this structure: 116 | | Property | Value | 117 | | -------- | ------------------------------------------------------------------------------------------------------------------------------------- | 118 | | `type` | Possible values: `internal-link`, `link`, `file-attachment`, `form-text`, `form-select`, `form-checkbox`, `form-radio`, `form-button` | 119 | | `data` | Annotation's associated data | 120 | 121 | 122 | ### internal-link 123 | 124 | `internal-link` emitted when the user clicks on a link that redirects to another content within the document. 125 | 126 | ```json 127 | { 128 | "type": "internal-link", 129 | "data": { 130 | "referencedPage": 3, 131 | "offset": { 132 | "left": 82, 133 | "bottom": 716 134 | } 135 | } 136 | } 137 | ``` 138 | 139 | ### link 140 | 141 | `link` emitted when the user clicks on an external link. 142 | 143 | ```json 144 | { 145 | "type": "link", 146 | "data": { 147 | "url": "mailto:aor@testmail.com", 148 | "unsafeUrl": "mailto:aor@testmail.com" 149 | } 150 | } 151 | ``` 152 | 153 | ### file-attachment 154 | 155 | `file-attachment` emitted when the user double-clicks an attachment annotation. 156 | 157 | ```json 158 | { 159 | "type": "file-attachment", 160 | "data": { 161 | "filename": "utf8test.txt", 162 | "content": [83, 101, 110] // Uint8Array 163 | } 164 | } 165 | ``` 166 | 167 | ### form-text 168 | 169 | `form-text` emitted when the user inputs a value in an text-field element. 170 | 171 | ```json 172 | { 173 | "type": "form-text", 174 | "data": { 175 | "fieldName": "firstname", 176 | "value": "Aldo Hernandez" 177 | } 178 | } 179 | ``` 180 | 181 | ### form-select 182 | 183 | `form-select` emitted when the user inputs a value in an one-select or multi-select element. 184 | 185 | ```json 186 | { 187 | "type": "form-select", 188 | "data": { 189 | "fieldName": "gender", 190 | "value": [ 191 | { 192 | "value": "M", 193 | "label": "Male" 194 | } 195 | ], 196 | "options": [ 197 | { 198 | "value": "", 199 | "label": "-" 200 | }, 201 | { 202 | "value": "M", 203 | "label": "Male" 204 | }, 205 | { 206 | "value": "F", 207 | "label": "Female" 208 | } 209 | ] 210 | } 211 | } 212 | ``` 213 | 214 | ### form-checkbox 215 | 216 | `form-checkbox` emitted when the user changes a checkbox field element. 217 | 218 | ```json 219 | { 220 | "type": "form-checkbox", 221 | "data": { 222 | "fieldName": "newsletter", 223 | "checked": true 224 | } 225 | } 226 | ``` 227 | 228 | ### form-radio 229 | 230 | `form-radio` emitted when the user changes a radio field. 231 | 232 | ```json 233 | { 234 | "type": "form-radio", 235 | "data": { 236 | "fieldName": "drink", 237 | "value": "Wine", 238 | "defaultValue": "Beer", 239 | "options": ["Water", "Beer", "Wine", "Milk"] 240 | } 241 | } 242 | ``` 243 | 244 | ### form-button 245 | 246 | `form-button` emitted when the user clicks on a push button element. 247 | 248 | ```json 249 | { 250 | "type": "form-button", 251 | "data": { 252 | "fieldName": "Print", 253 | "actions": { 254 | "Mouse Down": ["Print()"] 255 | }, 256 | "reset": false 257 | } 258 | } 259 | ``` 260 | -------------------------------------------------------------------------------- /docs/guide/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: [2,3] 3 | --- 4 | 5 | # Introduction 6 | 7 | VuePDF is a **client-side** component for **Vue 3** that allows you to flexibly render PDF pages within your project. This library wraps `pdf.js` library so all main features of `pdf.js` are supported by `VuePDF` as well. 8 | 9 | ## Installation 10 | 11 | ::: code-group 12 | ```sh [npm] 13 | npm i @tato30/vue-pdf 14 | ``` 15 | 16 | ```sh [yarn] 17 | yarn add @tato30/vue-pdf 18 | ``` 19 | ::: 20 | 21 | ## Basic Usage 22 | 23 | The most basic usage is as simple as import the `VuePDF` and `usePDF` and use them on your project :) 24 | 25 | ```vue 26 | 31 | 32 | 35 | ``` 36 | 37 | ## Working With Layers 38 | 39 | ### Text and Annotations 40 | 41 | This component supports text selection and annotation interaction by enabling them with `text-layer` and `annotation-layer` props respectively, but for this layers renders correctly is necessary set some `css` styles, it can be done by importing default styles from `@tato30/vue-pdf/style.css`. 42 | 43 | ```vue 44 | 50 | 51 | 54 | ``` 55 | 56 | Check the examples: 57 | 58 | - [Text Layer](../examples/basic/text_layer.md) 59 | - [Annotation Layer](../examples/basic/annotation_layer.md.md) 60 | 61 | You could create your own custom styles and set them in your project, use this styles as a guide: 62 | 63 | - [text-layer styles](https://github.com/mozilla/pdf.js/blob/master/web/text_layer_builder.css) 64 | - [annotation-layer styles](https://github.com/mozilla/pdf.js/blob/master/web/annotation_layer_builder.css) 65 | 66 | ### XFA Forms 67 | XFA forms also can be supported by enabling them from `usePDF`. 68 | 69 | ```vue 70 | 79 | 80 | 83 | ``` 84 | 85 | Check the example: 86 | 87 | - [XFA Forms](../examples/basic/xfa_layer.md) 88 | 89 | ## Server-Side Rendering 90 | 91 | `VuePDF` is a client-side library, so if you are working with a SSR framework like `nuxt`, surely it will throw an error during the building stage, if that is the case, you could wrap `VuePDF` in some sort of "client only" directive or component, also `usePDF` should be wrapped. 92 | 93 | ## Supporting Non-Latin characters 94 | 95 | If you are looking for display non-latin text or you are getting a warning like: 96 | > Warning: Error during font loading: CMapReaderFactory not initialized, see the useWorkerFetch parameter 97 | 98 | you will probably need to copy the `cmaps` directory from `node_modules/pdfjs-dist` to your project's `public` directory, don't worry about no having `pdfjs-dist` it's installed alongside `vue-pdf` package. 99 | 100 | 101 | ``` 102 | . 103 | ├─ node_modules 104 | │ ├─ pdfjs-dist 105 | │ │ └─ cmaps <--- Copy this directory 106 | ├─ src 107 | ├─ public 108 | | ├─ *cmaps* <--- Paste it here! 109 | ├─ package.json 110 | | ... 111 | ``` 112 | 113 | With that made the `cmaps` will be available on relative path `/cmaps/`, now you need the tell `usePDF` uses that `cmaps` url: 114 | 115 | ```js 116 | const { pdf } = usePDF({ 117 | url: pdfsource, 118 | cMapUrl: '/cmaps/', 119 | }) 120 | ``` 121 | 122 | ## Supporting legacy browsers 123 | 124 | If you need to support legacy browsers you could use any polyfill to patch modern functions, but this workaround only works on the **main** thread, the *worker* that runs in other thread will not get reached by any polyfills you apply. 125 | 126 | This package embed and configure the `pdf.js` *worker* for you but in case you need to support legacy environments you will need to configure the `legacy` *worker* by adding this code: 127 | 128 | ```vue 129 | 138 | ``` 139 | 140 | Just be aware to set the `legacy` worker before use `usePDF`. 141 | 142 | ## Common issues 143 | 144 | ### Promise.withResolvers 145 | 146 | > Promise.withResolvers is not a function 147 | 148 | That throws because `Promise.withResolvers` is a relative "new feature" of JavaScript's Promises, even if almost all browsers [support it](https://caniuse.com/?search=withResolvers), in NodeJS this feature was fully included on version v22 as a base feature. To solve this issue consider updating node version if you are currently using a lower one. 149 | 150 | ### Top-level await is not available in the configured target environment 151 | 152 | > [ERROR] Top-level await is not available in the configured target environment ("chrome87", "edge88", "es2020", "firefox78", "safari14" + 2 overrides) 153 | 154 | This error is more related to ESBuild settings instead of compatibility matters, `Top-level await` is (as usually) a "new feature" of the JavaScript definition, practically all browsers [support it](https://caniuse.com/?search=top-level%20await) and was included on NodeJS since v14. 155 | 156 | To solve this issue you will need to add this settings on `vite.config`: 157 | 158 | ```json 159 | optimizeDeps: { 160 | esbuildOptions: { 161 | supported: { 162 | 'top-level-await': true, 163 | }, 164 | }, 165 | }, 166 | esbuild: { 167 | supported: { 168 | 'top-level-await': true, 169 | }, 170 | } 171 | ``` 172 | 173 | ## Contributing 174 | 175 | Any idea, suggestion or contribution to the code or documentation are very welcome. 176 | 177 | ```sh 178 | # Clone the repository 179 | git clone https://github.com/TaTo30/vue-pdf.git 180 | # Change to code folder 181 | cd vue-pdf 182 | # Install node_modules 183 | npm install 184 | # Run code with hot reload 185 | npm run dev 186 | # Run docs 187 | npm run dev:docs 188 | ``` -------------------------------------------------------------------------------- /docs/guide/methods.md: -------------------------------------------------------------------------------- 1 | # Methods 2 | 3 | ## reload 4 | 5 | Reload page's render task, useful to update some props, for example, the parent width when [`fit-parent`](./props.html#fit-parent) is used 6 | 7 | ```vue 8 | 16 | 17 | 20 | ``` 21 | 22 | ## cancel 23 | 24 | Cancel the render task if the page is currently rendering. 25 | 26 | ```vue 27 | 35 | 36 | 39 | ``` -------------------------------------------------------------------------------- /docs/guide/props.md: -------------------------------------------------------------------------------- 1 | # Props 2 | 3 | ## pdf 4 | 5 | Type: `PDFDocumentLoadingTask`
6 | Required: `true` 7 | 8 | The `PDFDocumentLoadingTask` obtained from usePDF. 9 | 10 | ```vue 11 | 12 | ``` 13 | 14 | ## page 15 | 16 | Type: `int`
17 | Required: `false`
18 | Default: `1` 19 | 20 | Page to render, this prop must be a page number starting at 1. 21 | 22 | ```vue 23 | 24 | ``` 25 | 26 | ## intent 27 | 28 | Type: `string`
29 | Required: `false`
30 | Default: `display` 31 | 32 | Rendering intent, can be `display`, `print`, or `any`. 33 | 34 | ```vue 35 | 36 | ``` 37 | 38 | ## scale 39 | 40 | Type: `int`
41 | Required: `false`
42 | Default: `1` 43 | 44 | Page's scale. 45 | 46 | ```vue 47 | 48 | ``` 49 | 50 | ## fit-parent 51 | 52 | Type: `boolean`
53 | Required: `false`
54 | Default: `false` 55 | 56 | Fit the page's width with the parent width. This prop replace [scale](#scale) in size calculation and has more precedence than [width](#width). 57 | 58 | ```vue 59 | 60 | ``` 61 | 62 | ## width 63 | 64 | Type: `number`
65 | Required: `false`
66 | Default: `null` 67 | 68 | Scale the page using a `width` in px. This prop replace [scale](#scale) in size calculation and has more precedence than [height](#height). 69 | 70 | ```vue 71 | 72 | ``` 73 | 74 | ## height 75 | 76 | Type: `number`
77 | Required: `false`
78 | Default: `null` 79 | 80 | Scale the page using a `height` in px. This prop replace [scale](#scale) in size calculation. 81 | 82 | ```vue 83 | 84 | ``` 85 | 86 | ## rotation 87 | 88 | Type: `int`
89 | Required: `false`
90 | Default: `Document's Default` 91 | 92 | Rotate the page in 90° multiples eg. (`90`, `180`, `270`) 93 | 94 | ```vue 95 | 96 | ``` 97 | 98 | ## text-layer 99 | 100 | Type: `boolean`
101 | Required: `false`
102 | Default: `false` 103 | 104 | Enables text selection. 105 | 106 | ```vue 107 | 108 | ``` 109 | 110 | ## highlight-text 111 | 112 | Type: `string | string[]`
113 | Required: `false`
114 | Default: `null` 115 | 116 | Highlight on the page the searched text or the searched array of text. 117 | 118 | ```vue 119 | 120 | 121 | 122 | ``` 123 | 124 | ## highlight-options 125 | 126 | Type: `object`
127 | Required: `false`
128 | Default: 129 | ``` 130 | { 131 | completeWords: false, 132 | ignoreCase: true 133 | } 134 | ``` 135 | 136 | Settings for how to find the [highlight-text](#highlight-text) on page's text. 137 | 138 | ```vue 139 | 144 | ``` 145 | 146 | ## annotation-layer 147 | 148 | Type: `boolean`
149 | Required: `false`
150 | Default: `false` 151 | 152 | Enables document annotations like links, popups, widgets, etc. 153 | 154 | ```vue 155 | 156 | ``` 157 | 158 | ## watermark-text 159 | 160 | Type: `string`
161 | Required: `false`
162 | Default: `null` 163 | 164 | Prints a watermark pattern over the canvas. 165 | 166 | ```vue 167 | 168 | ``` 169 | 170 | ## watermark-options 171 | 172 | Type: `object`
173 | Required: `false`
174 | Default: 175 | ``` 176 | { 177 | columns: 4, 178 | rows: 4, 179 | rotation: 45, 180 | fontSize: 18, 181 | color: 'rgba(211, 210, 211, 0.4)', 182 | } 183 | ``` 184 | 185 | Customize how watermark is printed over the canvas. 186 | 187 | ```vue 188 | 197 | 198 | 199 | ``` 200 | 201 | ## image-resources-path 202 | 203 | Type: `string`
204 | Required: `false`
205 | Default: `null`
206 | 207 | Path to image resources needed to render some graphics when required. 208 | 209 | ```vue 210 | 211 | ``` 212 | 213 | ## hide-forms 214 | 215 | Type: `boolean`
216 | Required: `false`
217 | Default: `false`
218 | 219 | Hide AcroForms from annotation-layer. 220 | 221 | ```vue 222 | 223 | ``` 224 | 225 | ## annotations-filter 226 | 227 | Type: `array`
228 | Required: `false`
229 | Default: `null` 230 | 231 | Allows to choose which annotations display on page, the following options are available: 232 | 233 | * `Link` 234 | * `Text` 235 | * `Stamp` 236 | * `Popup` 237 | * `FreeText` 238 | * `Line` 239 | * `Square` 240 | * `Circle` 241 | * `PolyLine` 242 | * `Caret` 243 | * `Ink` 244 | * `Polygon` 245 | * `Highlight` 246 | * `Underline` 247 | * `Squiggly` 248 | * `StrikeOut` 249 | * `FileAttachment` 250 | * `Widget` 251 | * `Widget.Tx` 252 | * `Widget.Btn` 253 | * `Widget.Ch` 254 | * `Widget.Sig` 255 | 256 | > NOTE: `Widget` shows all `Widget` subtypes like `Widget.Tx`, `Widget.Btn`, etc. 257 | 258 | 259 | ```vue 260 | 263 | 264 | 265 | ``` 266 | 267 | ## annotations-map 268 | 269 | Type: `object`
270 | Required: `false`
271 | Default: `null`
272 | 273 | Allows to map values to annotation's storage, useful for edit annotation's data before rendering. 274 | 275 | ```vue 276 | 279 | 280 | 281 | ``` 282 | -------------------------------------------------------------------------------- /docs/guide/slots.md: -------------------------------------------------------------------------------- 1 | # Slots 2 | 3 | ## loading: default 4 | 5 | Content to display when page is rendering 6 | 7 | ```vue 8 | 15 | ``` -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | sidebar: true 4 | 5 | title: Home 6 | titleTemplate: PDF component for Vue 3 7 | 8 | hero: 9 | name: VuePDF 10 | text: Render PDF pages on your website 11 | tagline: An easy-to-use component for rendering PDF pages in a dynamically and customizable way 12 | image: 13 | src: /logo.png 14 | alt: VuePDF 15 | actions: 16 | - text: Get started 17 | link: /guide/introduction 18 | theme: brand 19 | - text: Examples 20 | link: /examples/basic/one_page 21 | theme: alt 22 | - text: Try on StackBlitz 23 | link: https://stackblitz.com/edit/vue-pdf-playground?file=src%2FApp.vue 24 | theme: alt 25 | 26 | features: 27 | - icon: ↗️ 28 | title: Sizing 29 | details: Set a scale, width, height or fit the PDF page with parent width 30 | - icon: 🔆 31 | title: Highlight Text 32 | details: Search and highlight text 33 | - icon: ©️ 34 | title: Watermark 35 | details: Watermark your pages to protect your content 36 | - icon: 📖 37 | title: Content Layers 38 | details: Enable text selection, annotations and XFA forms 39 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-pdf-docs", 3 | "version": "1.0.0", 4 | "description": "vue-pdf docs", 5 | "scripts": { 6 | "dev": "vitepress dev .", 7 | "build": "vitepress build --clean-temp --clean-cache ." 8 | }, 9 | "dependencies": { 10 | "@tato30/vue-pdf": "*" 11 | }, 12 | "devDependencies": { 13 | "sass": "^1.77.2", 14 | "vite": "^4.3.4", 15 | "vitepress": "^1.3.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/public: -------------------------------------------------------------------------------- 1 | ../samples -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-pdf-monorepo", 3 | "version": "1.0.0", 4 | "description": "vue-pdf-monorepo", 5 | "workspaces": [ 6 | "packages/*", 7 | "docs", 8 | "tests" 9 | ], 10 | "scripts": { 11 | "dev": "npm run --prefix packages/playground dev", 12 | "dev:docs": "npm run --prefix packages/vue-pdf build:lib && npm run --prefix docs dev", 13 | "test": "npm run --prefix tests test", 14 | "build": "npm run test && npm run --prefix packages/vue-pdf build", 15 | "build:docs": "npm run --prefix packages/vue-pdf build:lib && npm run --prefix docs build", 16 | "lint:fix": "eslint --fix ." 17 | }, 18 | "dependencies": { 19 | "@rushstack/eslint-patch": "^1.10.3", 20 | "@vue/eslint-config-typescript": "^13.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/playground/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /packages/playground/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import type { DefineComponent } from "vue"; 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component; 5 | } 6 | 7 | declare module "@samples/*.pdf" { 8 | const pdfurl: string; 9 | export default pdfurl; 10 | } -------------------------------------------------------------------------------- /packages/playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/playground/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /packages/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-pdf-playground", 3 | "version": "1.0.0", 4 | "description": "vue-pdf-playground", 5 | "scripts": { 6 | "dev": "vite ." 7 | }, 8 | "dependencies": { 9 | "@tato30/vue-pdf": "*" 10 | }, 11 | "devDependencies": { 12 | "@types/node": "^18.16.3", 13 | "typescript": "^4.9.4", 14 | "vite": "^4.3.4", 15 | "vue": "^3.2.47" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/playground/src/AnnoLayer.vue: -------------------------------------------------------------------------------- 1 | 2 | 48 | 49 | -------------------------------------------------------------------------------- /packages/playground/src/CustomLoading.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /packages/playground/src/FitParent.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 27 | 28 | 31 | -------------------------------------------------------------------------------- /packages/playground/src/MultiPages.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | 21 | 32 | -------------------------------------------------------------------------------- /packages/playground/src/PageNavigation.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 28 | -------------------------------------------------------------------------------- /packages/playground/src/TextLayer.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 37 | 38 | 41 | -------------------------------------------------------------------------------- /packages/playground/src/XFALayer.vue: -------------------------------------------------------------------------------- 1 | 2 | 27 | 28 | 57 | 58 | 61 | -------------------------------------------------------------------------------- /packages/playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "resolveJsonModule": true, 12 | "baseUrl": ".", 13 | "paths": { 14 | "@tato30/vue-pdf": [ 15 | "../vue-pdf/src" 16 | ] 17 | } 18 | }, 19 | "include": [ 20 | "vite.config.ts", 21 | "src/**/*.vue", 22 | "src/**/*.ts", 23 | "env.d.ts", 24 | "main.ts", 25 | "App.vue" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /packages/playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import { defineConfig, mergeConfig } from 'vite'; 3 | import commonConfig from '../../vite.config'; 4 | 5 | export default mergeConfig( 6 | commonConfig, 7 | defineConfig({ 8 | resolve: { 9 | alias: { 10 | "@tato30/vue-pdf": resolve(__dirname, "../vue-pdf/src"), 11 | }, 12 | }, 13 | }) 14 | ); 15 | -------------------------------------------------------------------------------- /packages/vue-pdf/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

VuePDF

4 |
5 | 6 |

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

17 | 18 |
19 |

📖Documentation

20 |
21 | 22 | # Introduction 23 | 24 | VuePDF is a **client-side** component for **Vue 3** that allows you to flexibly render PDF pages within your project. This library wraps `pdf.js` library so all main features of `pdf.js` are supported by `VuePDF` as well. 25 | 26 | ## Installation 27 | 28 | ```sh 29 | npm i @tato30/vue-pdf 30 | yarn add @tato30/vue-pdf 31 | ``` 32 | 33 | ## Basic Usage 34 | 35 | The most basic usage is as simple as import the `VuePDF` and `usePDF` and use them on your project :) 36 | 37 | ```vue 38 | 43 | 44 | 47 | ``` 48 | 49 | ## Working With Layers 50 | 51 | ### Text and Annotations 52 | 53 | This component supports text selection and annotation interaction by enabling them with `text-layer` and `annotation-layer` props respectively, but for this layers renders correctly is necessary set some `css` styles, it can be done by importing default styles from `@tato30/vue-pdf/style.css`. 54 | 55 | ```vue 56 | 62 | 63 | 66 | ``` 67 | 68 | ### XFA Forms 69 | XFA forms also can be supported by enabling them from `usePDF`. 70 | 71 | ```vue 72 | 81 | 82 | 85 | ``` 86 | 87 | ## Server-Side Rendering 88 | 89 | `VuePDF` is a client-side library, so if you are working with a SSR framework like `nuxt`, surely it will throw an error during the building stage, if that is the case, you could wrap `VuePDF` in some sort of "client only" directive or component, also `usePDF` should be wrapped. 90 | 91 | ## Supporting Non-Latin characters 92 | 93 | If you are looking for display non-latin text or you are getting a warning like: 94 | > Warning: Error during font loading: CMapReaderFactory not initialized, see the useWorkerFetch parameter 95 | 96 | you will probably need to copy the `cmaps` directory from `node_modules/pdfjs-dist` to your project's `public` directory, don't worry about no having `pdfjs-dist` it's installed alongside `vue-pdf` package. 97 | 98 | 99 | ``` 100 | . 101 | ├─ node_modules 102 | │ ├─ pdfjs-dist 103 | │ │ └─ cmaps <--- Copy this directory 104 | ├─ src 105 | ├─ public 106 | | ├─ *cmaps* <--- Paste it here! 107 | ├─ package.json 108 | | ... 109 | ``` 110 | 111 | With that made the `cmaps` will be available on relative path `/cmaps/`, now you need the tell `usePDF` uses that `cmaps` url: 112 | 113 | ```js 114 | const { pdf } = usePDF({ 115 | url: pdfsource, 116 | cMapUrl: '/cmaps/', 117 | }) 118 | ``` 119 | 120 | ## Supporting legacy browsers 121 | 122 | If you need to support legacy browsers you could use any polyfill to patch modern functions, but this workaround only works on the **main** thread, the *worker* that runs in other thread will not get reached by any polyfills you apply. 123 | 124 | This package embed and configure the `pdf.js` *worker* for you but in case you need to support legacy environments you will need to configure the `legacy` *worker* by adding this code: 125 | 126 | ```vue 127 | 136 | ``` 137 | 138 | Just be aware to set the `legacy` worker before use `usePDF`. 139 | 140 | ## Common issues 141 | 142 | ### Promise.withResolvers 143 | 144 | > Promise.withResolvers is not a function 145 | 146 | That throws because `Promise.withResolvers` is a relative "new feature" of JavaScript's Promises, even if almost all browsers [support it](https://caniuse.com/?search=withResolvers), in NodeJS this feature was fully included on version v22 as a base feature. To solve this issue consider updating node version if you are currently using a lower one. 147 | 148 | ### Top-level await is not available in the configured target environment 149 | 150 | > [ERROR] Top-level await is not available in the configured target environment ("chrome87", "edge88", "es2020", "firefox78", "safari14" + 2 overrides) 151 | 152 | This error is more related to ESBuild settings instead of compatibility matters, `Top-level await` is (as usually) a "new feature" of the JavaScript definition, practically all browsers [support it](https://caniuse.com/?search=top-level%20await) and was included on NodeJS since v14. 153 | 154 | To solve this issue you will need to add this settings on `vite.config`: 155 | 156 | ```js 157 | optimizeDeps: { 158 | esbuildOptions: { 159 | supported: { 160 | 'top-level-await': true, 161 | }, 162 | }, 163 | }, 164 | esbuild: { 165 | supported: { 166 | 'top-level-await': true, 167 | }, 168 | } 169 | ``` 170 | 171 | 172 | ## Contributing 173 | 174 | Any idea, suggestion or contribution to the code or documentation are very welcome. 175 | 176 | ```sh 177 | # Clone the repository 178 | git clone https://github.com/TaTo30/vue-pdf.git 179 | # Change to code folder 180 | cd vue-pdf 181 | # Install node_modules 182 | npm install 183 | # Run code with hot reload 184 | npm run dev 185 | # Run docs 186 | npm run dev:docs 187 | ``` 188 | 189 | ## Looking for maintainers and current status 190 | 191 | Refer to this announcement for more details: https://github.com/TaTo30/vue-pdf/discussions/128 192 | -------------------------------------------------------------------------------- /packages/vue-pdf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tato30/vue-pdf", 3 | "version": "1.11.3", 4 | "description": "PDF component for Vue 3", 5 | "author": { 6 | "name": "Aldo Hernandez", 7 | "url": "https://github.com/TaTo30" 8 | }, 9 | "license": "MIT", 10 | "homepage": "https://github.com/TaTo30/vue-pdf/", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/TaTo30/vue-pdf.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/TaTo30/vue-pdf/issues" 17 | }, 18 | "keywords": [ 19 | "pdf", 20 | "vue", 21 | "viewer" 22 | ], 23 | "exports": { 24 | ".": { 25 | "types": "./dist/types/index.d.ts", 26 | "require": "./dist/index.umd.js", 27 | "import": "./dist/index.mjs" 28 | }, 29 | "./style.css": "./dist/style.css", 30 | "./src/*": "./src/*" 31 | }, 32 | "main": "./dist/index.umd.js", 33 | "module": "./dist/index.mjs", 34 | "types": "./dist/types/index.d.ts", 35 | "files": [ 36 | "dist", 37 | "src" 38 | ], 39 | "scripts": { 40 | "build": "npm run build:lib && npm run build:dts", 41 | "build:lib": "vite build", 42 | "build:dts": "vue-tsc --declaration --emitDeclarationOnly -p tsconfig.build.json", 43 | "lint": "eslint .", 44 | "lint:fix": "eslint --fix ." 45 | }, 46 | "peerDependencies": { 47 | "vue": "^3.2.33" 48 | }, 49 | "dependencies": { 50 | "pdfjs-dist": "4.9.124" 51 | }, 52 | "devDependencies": { 53 | "@types/node": "^18.16.3", 54 | "eslint": "^8.39.0", 55 | "typescript": "^4.9.4", 56 | "vite": "^4.3.4", 57 | "vue": "^3.2.47", 58 | "vue-tsc": "^1.6.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/vue-pdf/src/components/VuePDF.vue: -------------------------------------------------------------------------------- 1 | 2 | 335 | 336 | 357 | -------------------------------------------------------------------------------- /packages/vue-pdf/src/components/composable.ts: -------------------------------------------------------------------------------- 1 | import * as PDFJS from 'pdfjs-dist' 2 | import PDFWorker from 'pdfjs-dist/build/pdf.worker.min?url' 3 | import { isRef, shallowRef, watch } from 'vue' 4 | 5 | import type { PDFDocumentLoadingTask, PDFDocumentProxy } from 'pdfjs-dist' 6 | import type { Ref } from 'vue' 7 | import type { OnPasswordCallback, PDFDestination, PDFInfo, PDFOptions, PDFSrc } from './types' 8 | import { getDestinationArray, getDestinationRef, getLocation, isSpecLike } from './utils/destination' 9 | import { addStylesToIframe, createIframe } from './utils/miscellaneous' 10 | 11 | // Could not find a way to make this work with vite, importing the worker entry bundle the whole worker to the the final output 12 | // https://erindoyle.dev/using-pdfjs-with-vite/ 13 | // PDFJS.GlobalWorkerOptions.workerSrc = PDFWorker 14 | function configWorker(wokerSrc: string) { 15 | PDFJS.GlobalWorkerOptions.workerSrc = wokerSrc 16 | } 17 | 18 | /** 19 | * @typedef {Object} UsePDFParameters 20 | * @property {string} password 21 | * Document password to unlock content 22 | * @property {function} onProgress 23 | * Callback to request a password if a wrong or no password was provided. The callback receives two parameters: a function that should be called with the new password, and a reason (see PasswordResponses). 24 | * @property {function} onPassword 25 | * Callback to be able to monitor the loading progress of the PDF file (necessary to implement e.g. a loading bar). The callback receives an OnProgressParameters argument. if this function is used option.password is ignored 26 | * @property {function} onError 27 | * Callback to be able to handle errors during loading 28 | * */ 29 | 30 | /** 31 | * 32 | * @param {string | URL | TypedArray | PDFDataRangeTransport | DocumentInitParameters} src 33 | * Can be a URL where a PDF file is located, a typed array (Uint8Array) already populated with data, or a parameter object. 34 | * @param {UsePDFParameters} options 35 | * UsePDF object parameters 36 | */ 37 | export function usePDF(src: PDFSrc | Ref, 38 | options: PDFOptions = { 39 | onProgress: undefined, 40 | onPassword: undefined, 41 | onError: undefined, 42 | password: '', 43 | }, 44 | ) { 45 | if (!PDFJS.GlobalWorkerOptions?.workerSrc) 46 | configWorker(PDFWorker) 47 | 48 | const pdf = shallowRef() 49 | const pdfDoc = shallowRef() 50 | const pages = shallowRef(0) 51 | const info = shallowRef({}) 52 | 53 | function processLoadingTask(source: PDFSrc) { 54 | if (pdfDoc.value) 55 | void pdfDoc.value.destroy() 56 | 57 | const loadingTask = PDFJS.getDocument(source!) 58 | if (options.onProgress) 59 | loadingTask.onProgress = options.onProgress 60 | 61 | if (options.onPassword) { 62 | loadingTask.onPassword = options.onPassword 63 | } 64 | else if (options.password) { 65 | const onPassword: OnPasswordCallback = (updatePassword, _) => { 66 | updatePassword(options.password ?? '') 67 | } 68 | loadingTask.onPassword = onPassword 69 | } 70 | 71 | loadingTask.promise.then( 72 | async (doc) => { 73 | pdfDoc.value = doc 74 | 75 | pdf.value = doc.loadingTask 76 | pages.value = doc.numPages 77 | 78 | const metadata = await doc.getMetadata() 79 | const attachments = (await doc.getAttachments()) as Record 80 | const javascript = await doc.getJSActions() 81 | const outline = await doc.getOutline() 82 | 83 | info.value = { 84 | metadata, 85 | attachments, 86 | javascript, 87 | outline, 88 | } 89 | }, 90 | (error) => { 91 | // PDF loading error 92 | if (typeof options.onError === 'function') 93 | options.onError(error) 94 | }, 95 | ) 96 | } 97 | 98 | async function getPDFDestination(destination: string | any[] | null): Promise { 99 | const document = await pdf.value?.promise 100 | if (!document) 101 | return null 102 | 103 | const destArray = await getDestinationArray(document, destination) 104 | const destRef = await getDestinationRef(document, destArray) 105 | if (!destRef || !destArray) 106 | return null 107 | 108 | const pageIndex = await document.getPageIndex(destRef) 109 | 110 | const name = destArray[1].name 111 | const rest = destArray.slice(2) 112 | 113 | const location = isSpecLike(rest) ? getLocation(name, rest) : null 114 | 115 | return { pageIndex, location: location ?? { type: 'Fit', spec: [] } } 116 | } 117 | 118 | async function download(filename = 'filename') { 119 | if (!pdfDoc.value) 120 | throw new Error('Current PDFDocumentProxy have not loaded yet') 121 | const bytes = await pdfDoc.value?.saveDocument() 122 | const blobBytes = new Blob([bytes], { type: 'application/pdf' }) 123 | const blobUrl = URL.createObjectURL(blobBytes) 124 | 125 | const anchorDownload = document.createElement('a') 126 | document.body.appendChild(anchorDownload) 127 | anchorDownload.href = blobUrl 128 | anchorDownload.download = filename 129 | anchorDownload.style.display = 'none' 130 | anchorDownload.click() 131 | 132 | setTimeout(() => { 133 | URL.revokeObjectURL(blobUrl) 134 | document.body.removeChild(anchorDownload) 135 | }, 10) 136 | } 137 | 138 | async function print(dpi = 150, filename = 'filename') { 139 | if (!pdfDoc.value) 140 | throw new Error('Current PDFDocumentProxy have not loaded yet') 141 | const bytes = await pdfDoc.value?.saveDocument() 142 | const savedLoadingTask = PDFJS.getDocument(bytes.buffer) 143 | const savedDocument = await savedLoadingTask.promise 144 | 145 | const PRINT_UNITS = dpi / 72 146 | const CSS_UNITS = 96 / 72 147 | 148 | const iframe = await createIframe() 149 | const contentWindow = iframe.contentWindow 150 | contentWindow!.document.title = filename 151 | 152 | const pagesNumbers = [...Array(savedDocument.numPages).keys()].map(val => val + 1) 153 | 154 | for (const pageNumber of pagesNumbers) { 155 | const pageToPrint = await savedDocument.getPage(pageNumber) 156 | const viewport = pageToPrint.getViewport({ scale: 1 })! 157 | 158 | if (pageNumber === 1) { 159 | addStylesToIframe( 160 | contentWindow!, 161 | (viewport.width * PRINT_UNITS) / CSS_UNITS, 162 | (viewport.height * PRINT_UNITS) / CSS_UNITS, 163 | ) 164 | } 165 | 166 | const canvas = document.createElement('canvas') 167 | canvas.width = viewport.width * PRINT_UNITS 168 | canvas.height = viewport.height * PRINT_UNITS 169 | 170 | const canvasCloned = canvas.cloneNode() as HTMLCanvasElement 171 | contentWindow?.document.body.appendChild(canvasCloned) 172 | 173 | await pageToPrint?.render({ 174 | canvasContext: canvas.getContext('2d')!, 175 | intent: 'print', 176 | transform: [PRINT_UNITS, 0, 0, PRINT_UNITS, 0, 0], 177 | viewport, 178 | }).promise 179 | 180 | canvasCloned.getContext('2d')?.drawImage(canvas, 0, 0) 181 | } 182 | 183 | contentWindow?.focus() 184 | contentWindow?.print() 185 | document.body.removeChild(iframe) 186 | } 187 | 188 | if (isRef(src)) { 189 | if (src.value) 190 | processLoadingTask(src.value) 191 | watch(src, () => { 192 | if (src.value) 193 | processLoadingTask(src.value) 194 | }) 195 | } 196 | else { 197 | if (src) 198 | processLoadingTask(src) 199 | } 200 | 201 | return { 202 | pdf, 203 | pages, 204 | info, 205 | print, 206 | download, 207 | getPDFDestination, 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /packages/vue-pdf/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as VuePDF } from './VuePDF.vue' 2 | export * from './composable' 3 | export * from './types' 4 | -------------------------------------------------------------------------------- /packages/vue-pdf/src/components/layers/AnnotationLayer.vue: -------------------------------------------------------------------------------- 1 | 142 | 143 | 146 | 147 | 158 | -------------------------------------------------------------------------------- /packages/vue-pdf/src/components/layers/TextLayer.vue: -------------------------------------------------------------------------------- 1 | 127 | 128 | 137 | -------------------------------------------------------------------------------- /packages/vue-pdf/src/components/layers/XFALayer.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 57 | 58 | 64 | -------------------------------------------------------------------------------- /packages/vue-pdf/src/components/types.ts: -------------------------------------------------------------------------------- 1 | import type { PageViewport } from 'pdfjs-dist' 2 | import type { 3 | DocumentInitParameters, 4 | OnProgressParameters, 5 | PDFDataRangeTransport, 6 | TextContent, 7 | TypedArray, 8 | } from 'pdfjs-dist/types/src/display/api' 9 | import type { Metadata } from 'pdfjs-dist/types/src/display/metadata' 10 | 11 | export interface Match { 12 | start: { 13 | idx: number 14 | offset: number 15 | } 16 | end: { 17 | idx: number 18 | offset: number 19 | } 20 | str: string 21 | oindex: number 22 | } 23 | 24 | export type LoadedEventPayload = PageViewport 25 | 26 | export interface AnnotationEventPayload { 27 | type: string 28 | data: any 29 | } 30 | 31 | export interface HighlightEventPayload { 32 | matches: Match[] 33 | page: number 34 | textContent: TextContent 35 | textDivs: HTMLElement[] 36 | } 37 | 38 | export interface TextLayerLoadedEventPayload { 39 | textDivs: HTMLElement[] 40 | textContent: TextContent | undefined 41 | } 42 | 43 | export interface WatermarkOptions { 44 | columns?: number 45 | rows?: number 46 | rotation?: number 47 | fontSize?: number 48 | color?: string 49 | } 50 | 51 | export interface HighlightOptions { 52 | ignoreCase?: boolean 53 | completeWords?: boolean 54 | } 55 | 56 | export interface Base { 57 | type: T 58 | spec: S 59 | } 60 | // These are types from the PDF 1.7 reference manual; Adobe 61 | // Table 151 – Destination syntax 62 | // (Coordinates origin is bottom left of page) 63 | export type XYZ = Base<'XYZ', [left: number, top: number, zoom: number]> 64 | export type Fit = Base<'Fit', []> 65 | export type FitH = Base<'FitH', [top: number]> 66 | export type FitV = Base<'FitV', [left: number]> 67 | export type FitR = Base< 68 | 'FitR', 69 | [left: number, bottom: number, right: number, top: number] 70 | > 71 | export type FitB = Base<'FitB', []> 72 | export type FitBH = Base<'FitBH', [top: number]> 73 | export type FitBV = Base<'FitBV', [left: number]> 74 | 75 | export type PDFLocation = XYZ | Fit | FitH | FitV | FitR | FitB | FitBH | FitBV 76 | 77 | export interface PDFDestination { 78 | pageIndex: number 79 | location: PDFLocation 80 | } 81 | 82 | export type OnProgressCallback = (progressData: OnProgressParameters) => void 83 | export type UpdatePasswordFn = (newPassword: string) => void 84 | export type OnPasswordCallback = (updatePassword: UpdatePasswordFn, reason: any) => void 85 | export type OnErrorCallback = (error: any) => void 86 | 87 | export type PDFSrc = 88 | | string 89 | | URL 90 | | TypedArray 91 | | PDFDataRangeTransport 92 | | DocumentInitParameters 93 | | undefined 94 | | null 95 | 96 | export interface PDFOptions { 97 | onProgress?: OnProgressCallback 98 | onPassword?: OnPasswordCallback 99 | onError?: OnErrorCallback 100 | password?: string 101 | } 102 | 103 | export interface PDFInfoMetadata { 104 | info: Object 105 | metadata: Metadata 106 | } 107 | 108 | export interface PDFInfo { 109 | metadata: PDFInfoMetadata 110 | attachments: Record 111 | javascript: string[] | null 112 | outline: any 113 | } 114 | -------------------------------------------------------------------------------- /packages/vue-pdf/src/components/utils/annotations.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/restrict-template-expressions */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 4 | /* eslint-disable no-case-declarations */ 5 | import type { PDFDocumentProxy } from 'pdfjs-dist' 6 | import type { RefProxy } from 'pdfjs-dist/types/src/display/api' 7 | import type { AnnotationEventPayload } from '../types' 8 | 9 | interface PopupArgs { 10 | [key: string]: string 11 | } 12 | 13 | interface LinkAnnotation { 14 | dest: Array | string 15 | url: string 16 | unsafeurl: string 17 | } 18 | 19 | const INTERNAL_LINK = 'internal-link' 20 | const LINK = 'link' 21 | const FILE_ATTACHMENT = 'file-attachment' 22 | const FORM_TEXT = 'form-text' 23 | const FORM_SELECT = 'form-select' 24 | const FORM_CHECKBOX = 'form-checkbox' 25 | const FORM_RADIO = 'form-radio' 26 | const FORM_BUTTON = 'form-button' 27 | 28 | const EVENTS_TO_HANDLER = ['click', 'dblclick', 'mouseover', 'input', 'change'] 29 | 30 | function getAnnotationsByKey(key: string, value: any, annotations: Object[]): any[] { 31 | const result = [] 32 | if (annotations) { 33 | for (const annotation of annotations) { 34 | type Key = keyof typeof annotation 35 | if (annotation[key as Key] === value) 36 | result.push(annotation) 37 | } 38 | } 39 | return result 40 | } 41 | 42 | function buildAnnotationData(type: string, data: any): AnnotationEventPayload { 43 | return { type, data } 44 | } 45 | 46 | function inputAnnotation(inputEl: any, args?: any) { 47 | switch (inputEl.type) { 48 | case 'textarea': 49 | case 'text': 50 | return buildAnnotationData(FORM_TEXT, { 51 | fieldName: inputEl.name, 52 | value: inputEl.value, 53 | }) 54 | case 'select-one': 55 | case 'select-multiple': 56 | const options = [] 57 | for (const opt of inputEl.options) { 58 | options.push({ 59 | value: opt.value, 60 | label: opt.label, 61 | }) 62 | } 63 | const selected = [] 64 | for (const opt of inputEl.selectedOptions) { 65 | selected.push({ 66 | value: opt.value, 67 | label: opt.label, 68 | }) 69 | } 70 | return buildAnnotationData(FORM_SELECT, { 71 | fieldName: inputEl.name, 72 | value: selected, 73 | options, 74 | }) 75 | case 'checkbox': 76 | return buildAnnotationData(FORM_CHECKBOX, { 77 | fieldName: inputEl.name, 78 | checked: inputEl.checked, 79 | }) 80 | case 'radio': 81 | return buildAnnotationData(FORM_RADIO, { 82 | fieldName: inputEl.name, 83 | ...args, 84 | }) 85 | case 'button': 86 | return buildAnnotationData(FORM_BUTTON, { 87 | fieldName: inputEl.name, 88 | ...args, 89 | }) 90 | } 91 | } 92 | 93 | function fileAnnotation(annotation: any) { 94 | return buildAnnotationData(FILE_ATTACHMENT, annotation.file) 95 | } 96 | 97 | async function linkAnnotation(annotation: { 98 | dest?: any 99 | url?: string 100 | unsafeUrl?: string 101 | }, PDFDoc: PDFDocumentProxy) { 102 | if (annotation.dest) { 103 | let explicitDest 104 | if (typeof annotation.dest === 'string') 105 | explicitDest = await PDFDoc.getDestination(annotation.dest) 106 | else 107 | explicitDest = annotation.dest 108 | 109 | if (!Array.isArray(explicitDest)) { 110 | console.warn(`Destination "${explicitDest}" is not a valid destination (dest="${annotation.dest}")`) 111 | return buildAnnotationData(INTERNAL_LINK, { 112 | referencedPage: null, 113 | offset: null, 114 | }) 115 | } 116 | 117 | let offset = null 118 | if (explicitDest.length === 5) { 119 | offset = { 120 | left: annotation.dest[2], 121 | bottom: annotation.dest[3], 122 | } 123 | } 124 | 125 | const [destRef] = explicitDest 126 | if (Number.isInteger(destRef)) { 127 | return buildAnnotationData(INTERNAL_LINK, { 128 | referencedPage: Number(destRef) + 1, 129 | offset, 130 | }) 131 | } 132 | else if (typeof destRef === 'object') { 133 | const pageNumber = await PDFDoc.getPageIndex(destRef as RefProxy) 134 | return buildAnnotationData(INTERNAL_LINK, { 135 | referencedPage: pageNumber + 1, 136 | offset, 137 | }) 138 | } 139 | else { 140 | console.warn( 141 | `Destination "${destRef}" is not a valid destination (dest="${annotation.dest}")`, 142 | ) 143 | return buildAnnotationData(INTERNAL_LINK, { 144 | referencedPage: null, 145 | offset: null, 146 | }) 147 | } 148 | } 149 | else if (annotation.url) { 150 | return buildAnnotationData(LINK, { 151 | url: annotation.url, 152 | unsafeUrl: annotation.unsafeUrl, 153 | }) 154 | } 155 | } 156 | 157 | function mergePopupArgs(annotation: HTMLElement) { 158 | for (const spanElement of annotation.getElementsByTagName('span')) { 159 | let content = spanElement.textContent 160 | const args = JSON.parse(spanElement.dataset.l10nArgs ?? '{}') as PopupArgs 161 | if (content) { 162 | for (const key in args) 163 | content = content.replace(`{{${key}}}`, args[key]) 164 | } 165 | spanElement.textContent = content 166 | } 167 | } 168 | 169 | // Use this function to handle annotation events 170 | function annotationEventsHandler(evt: Event, PDFDoc: PDFDocumentProxy, Annotations: Object[]) { 171 | let annotation = (evt.target as HTMLInputElement).parentNode! as HTMLElement 172 | 173 | // annotations are
elements if div returned find in child nodes the section element 174 | if (annotation.tagName === 'DIV') 175 | annotation = annotation.firstChild! as HTMLElement 176 | 177 | if (annotation.className === 'linkAnnotation' && evt.type === 'click') { 178 | const id: string | undefined = annotation.dataset?.annotationId 179 | if (id) 180 | return linkAnnotation(getAnnotationsByKey('id', id, Annotations)[0] as LinkAnnotation, PDFDoc) 181 | } 182 | else if (annotation.className.includes('popupAnnotation') || annotation.className.includes('textAnnotation')) { 183 | mergePopupArgs(annotation) 184 | } 185 | else if (annotation.className.includes('fileAttachmentAnnotation')) { 186 | mergePopupArgs(annotation) 187 | const id = annotation.dataset.annotationId 188 | if (id && evt.type === 'dblclick') 189 | return fileAnnotation(getAnnotationsByKey('id', id, Annotations)[0]) 190 | } 191 | else if (annotation.className.includes('textWidgetAnnotation') && evt.type === 'input') { 192 | let inputElement: HTMLInputElement | HTMLTextAreaElement = annotation.getElementsByTagName('input')[0] 193 | if (!inputElement) 194 | inputElement = annotation.getElementsByTagName('textarea')[0] 195 | return inputAnnotation(inputElement) 196 | } 197 | else if (annotation.className.includes('choiceWidgetAnnotation') && evt.type === 'input') { 198 | return inputAnnotation(annotation.getElementsByTagName('select')[0]) 199 | } 200 | else if (annotation.className.includes('buttonWidgetAnnotation checkBox') && evt.type === 'change') { 201 | return inputAnnotation(annotation.getElementsByTagName('input')[0]) 202 | } 203 | else if (annotation.className.includes('buttonWidgetAnnotation radioButton') && evt.type === 'change') { 204 | const id = annotation.dataset.annotationId 205 | if (id) { 206 | const anno = getAnnotationsByKey('id', id, Annotations)[0] 207 | const radioOptions = [] 208 | for (const radioAnnotations of getAnnotationsByKey('fieldName', anno.fieldName, Annotations)) { 209 | if (radioAnnotations.buttonValue) 210 | radioOptions.push(radioAnnotations.buttonValue) 211 | } 212 | return inputAnnotation(annotation.getElementsByTagName('input')[0], { 213 | value: anno.buttonValue, 214 | defaultValue: anno.fieldValue, 215 | options: radioOptions, 216 | }) 217 | } 218 | } 219 | else if (annotation.className.includes('buttonWidgetAnnotation pushButton') && evt.type === 'click') { 220 | const id = annotation.dataset.annotationId 221 | if (id) { 222 | const anno = getAnnotationsByKey('id', id, Annotations)[0] 223 | if (!anno.resetForm) { 224 | return inputAnnotation( 225 | { name: anno.fieldName, type: 'button' }, 226 | { actions: anno.actions, reset: false }, 227 | ) 228 | } 229 | else { 230 | return inputAnnotation( 231 | { name: anno.fieldName, type: 'button' }, 232 | { actions: anno.actions, reset: true }, 233 | ) 234 | } 235 | } 236 | } 237 | } 238 | 239 | export { 240 | annotationEventsHandler, EVENTS_TO_HANDLER 241 | } 242 | -------------------------------------------------------------------------------- /packages/vue-pdf/src/components/utils/destination.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Written by Jason Harwig as part of PDFjs React Outline Viewer 3 | * Source: https://codesandbox.io/s/rp18w 4 | */ 5 | import type { PDFDocumentProxy, RefProxy } from 'pdfjs-dist/types/src/display/api' 6 | import type { Fit, FitB, FitBH, FitBV, FitH, FitR, FitV, PDFLocation, XYZ } from '../types' 7 | 8 | function isRefProxy(obj: unknown): obj is RefProxy { 9 | return Boolean(typeof obj === 'object' && obj && 'gen' in obj && 'num' in obj) 10 | } 11 | 12 | async function getDestinationArray(doc: PDFDocumentProxy, 13 | dest: string | any[] | null): Promise { 14 | return typeof dest === 'string' ? doc.getDestination(dest) : dest 15 | } 16 | 17 | // eslint-disable-next-line @typescript-eslint/require-await 18 | async function getDestinationRef(doc: PDFDocumentProxy, 19 | destArray: any[] | null): Promise { 20 | if (destArray && isRefProxy(destArray[0])) 21 | return destArray[0] 22 | 23 | return null 24 | } 25 | 26 | const isXYZ = (obj: { type: string; spec: number[] }): obj is XYZ => obj.type === 'XYZ' && obj.spec.length === 3 27 | const isFit = (obj: { type: string; spec: number[] }): obj is Fit => obj.type === 'Fit' && obj.spec.length === 0 28 | const isFitH = (obj: { type: string; spec: number[] }): obj is FitH => obj.type === 'FitH' && obj.spec.length === 1 29 | const isFitV = (obj: { type: string; spec: number[] }): obj is FitV => obj.type === 'FitV' && obj.spec.length === 1 30 | const isFitR = (obj: { type: string; spec: number[] }): obj is FitR => obj.type === 'FitR' && obj.spec.length === 4 31 | const isFitB = (obj: { type: string; spec: number[] }): obj is FitB => obj.type === 'FitB' && obj.spec.length === 0 32 | const isFitBH = (obj: { type: string; spec: number[] }): obj is FitBH => obj.type === 'FitBH' && obj.spec.length === 1 33 | const isFitBV = (obj: { type: string; spec: number[] }): obj is FitBV => obj.type === 'FitBV' && obj.spec.length === 1 34 | 35 | function getLocation(type: string, spec: number[]): PDFLocation | null { 36 | const obj = { type, spec } 37 | if (isXYZ(obj)) 38 | return obj 39 | if (isFit(obj)) 40 | return obj 41 | if (isFitH(obj)) 42 | return obj 43 | if (isFitV(obj)) 44 | return obj 45 | if (isFitR(obj)) 46 | return obj 47 | if (isFitB(obj)) 48 | return obj 49 | if (isFitBH(obj)) 50 | return obj 51 | if (isFitBV(obj)) 52 | return obj 53 | console.warn('no location type found for ', type, spec) 54 | 55 | return null 56 | } 57 | 58 | const isSpecLike = (list: any[]): list is number[] => list && list.every(v => !isNaN(v)) 59 | 60 | export { 61 | getDestinationArray, 62 | getDestinationRef, 63 | getLocation, 64 | isSpecLike, 65 | } 66 | -------------------------------------------------------------------------------- /packages/vue-pdf/src/components/utils/highlight.ts: -------------------------------------------------------------------------------- 1 | import type { TextItem } from 'pdfjs-dist/types/src/display/api' 2 | import type { TextContent } from 'pdfjs-dist/types/src/display/text_layer' 3 | import type { HighlightOptions, Match } from '../types' 4 | 5 | function searchQuery(textContent: TextContent, query: string, options: HighlightOptions) { 6 | const strs = [] 7 | for (const textItem of textContent.items as TextItem[]) { 8 | if (textItem.hasEOL) { 9 | // Remove the break line hyphen in the middle of the sentence 10 | if (textItem.str.endsWith('-')) { 11 | const lastHyphen = textItem.str.lastIndexOf('-') 12 | strs.push(textItem.str.substring(0, lastHyphen)) 13 | } 14 | else { 15 | strs.push(textItem.str, '\n') 16 | } 17 | } 18 | else { 19 | strs.push(textItem.str) 20 | } 21 | } 22 | 23 | // Join the text as is presented in textlayer and then replace newlines (/n) with whitespaces 24 | const textJoined = strs.join('').replace(/\n/g, ' ') 25 | 26 | const regexFlags = ['g'] 27 | if (options.ignoreCase) 28 | regexFlags.push('i') 29 | 30 | // Trim the query and escape all regex special characters 31 | let fquery = query.trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 32 | if (options.completeWords) 33 | fquery = `\\b${fquery}\\b` 34 | 35 | const regex = new RegExp(fquery, regexFlags.join('')) 36 | 37 | const matches = [] 38 | let match 39 | 40 | // eslint-disable-next-line no-cond-assign 41 | while ((match = regex.exec(textJoined)) !== null) 42 | matches.push([match.index, match[0].length, match[0]]) 43 | 44 | return matches 45 | } 46 | 47 | function convertMatches(matches: (number | string)[][], textContent: TextContent): Match[] { 48 | function endOfLineOffset(item: TextItem) { 49 | // When textitem has a EOL flag and the string has a hyphen at the end 50 | // the hyphen should be removed (-1 len) so the sentence could be searched as a joined one. 51 | // In other cases the EOL flag introduce a whitespace (+1 len) between two different sentences 52 | if (item.hasEOL) { 53 | if (item.str.endsWith('-')) 54 | return -1 55 | else 56 | return 1 57 | } 58 | return 0 59 | } 60 | 61 | let index = 0 62 | let tindex = 0 63 | const textItems = textContent.items as TextItem[] 64 | const end = textItems.length - 1 65 | 66 | const convertedMatches = [] 67 | 68 | // iterate over all matches 69 | for (let m = 0; m < matches.length; m++) { 70 | let mindex = matches[m][0] as number 71 | 72 | while (index !== end && mindex >= tindex + textItems[index].str.length) { 73 | const item = textItems[index] 74 | tindex += item.str.length + endOfLineOffset(item) 75 | index++ 76 | } 77 | 78 | const divStart = { 79 | idx: index, 80 | offset: mindex - tindex, 81 | } 82 | 83 | mindex += matches[m][1] as number 84 | 85 | while (index !== end && mindex > tindex + textItems[index].str.length) { 86 | const item = textItems[index] 87 | tindex += item.str.length + endOfLineOffset(item) 88 | index++ 89 | } 90 | 91 | const divEnd = { 92 | idx: index, 93 | offset: mindex - tindex, 94 | } 95 | convertedMatches.push({ 96 | start: divStart, 97 | end: divEnd, 98 | str: matches[m][2] as string, 99 | oindex: matches[m][0] as number, 100 | }) 101 | } 102 | return convertedMatches 103 | } 104 | 105 | function highlightMatches(matches: Match[], textContent: TextContent, textDivs: HTMLElement[]) { 106 | function appendHighlightDiv(idx: number, startOffset = -1, endOffset = -1) { 107 | const textItem = textContent.items[idx] as TextItem 108 | const nodes = [] 109 | 110 | let content = '' 111 | let prevContent = '' 112 | let nextContent = '' 113 | 114 | let div = textDivs[idx] 115 | 116 | if (!div) 117 | return // don't process if div is undefinied 118 | 119 | if (div.nodeType === Node.TEXT_NODE) { 120 | const span = document.createElement('span') 121 | div.before(span) 122 | span.append(div) 123 | textDivs[idx] = span 124 | div = span 125 | } 126 | 127 | if (startOffset >= 0 && endOffset >= 0) 128 | content = textItem.str.substring(startOffset, endOffset) 129 | else if (startOffset < 0 && endOffset < 0) 130 | content = textItem.str 131 | else if (startOffset >= 0) 132 | content = textItem.str.substring(startOffset) 133 | else if (endOffset >= 0) 134 | content = textItem.str.substring(0, endOffset) 135 | 136 | const node = document.createTextNode(content) 137 | const span = document.createElement('span') 138 | span.className = 'highlight appended' 139 | span.append(node) 140 | 141 | nodes.push(span) 142 | 143 | if (startOffset > 0) { 144 | if (div.childNodes.length === 1 && div.childNodes[0].nodeType === Node.TEXT_NODE) { 145 | prevContent = textItem.str.substring(0, startOffset) 146 | const node = document.createTextNode(prevContent) 147 | nodes.unshift(node) 148 | } 149 | else { 150 | let alength = 0 151 | const prevNodes = [] 152 | for (const childNode of div.childNodes) { 153 | const textValue = childNode.nodeType === Node.TEXT_NODE 154 | ? childNode.nodeValue! 155 | : childNode.firstChild!.nodeValue! 156 | alength += textValue.length 157 | 158 | if (alength <= startOffset) 159 | prevNodes.push(childNode) 160 | else if (startOffset >= alength - textValue.length && endOffset <= alength) 161 | prevNodes.push(document.createTextNode(textValue.substring(0, startOffset - (alength - textValue.length)))) 162 | } 163 | nodes.unshift(...prevNodes) 164 | } 165 | } 166 | if (endOffset > 0) { 167 | nextContent = textItem.str.substring(endOffset) 168 | const node = document.createTextNode(nextContent) 169 | nodes.push(node) 170 | } 171 | 172 | div.replaceChildren(...nodes) 173 | } 174 | 175 | for (const match of matches) { 176 | if (match.start.idx === match.end.idx) { 177 | appendHighlightDiv(match.start.idx, match.start.offset, match.end.offset) 178 | } 179 | else { 180 | for (let si = match.start.idx, ei = match.end.idx; si <= ei; si++) { 181 | if (si === match.start.idx) 182 | appendHighlightDiv(si, match.start.offset) 183 | else if (si === match.end.idx) 184 | appendHighlightDiv(si, -1, match.end.offset) 185 | else 186 | appendHighlightDiv(si) 187 | } 188 | } 189 | } 190 | } 191 | 192 | function resetDivs(textContent: TextContent, textDivs: HTMLElement[]) { 193 | const textItems = textContent.items.map(val => (val as TextItem).str) 194 | for (let idx = 0; idx < textDivs.length; idx++) { 195 | const div = textDivs[idx] 196 | 197 | if (div && div.nodeType !== Node.TEXT_NODE) { 198 | const textNode = document.createTextNode(textItems[idx]) 199 | div.replaceChildren(textNode) 200 | } 201 | } 202 | } 203 | 204 | function findMatches(queries: string[], textContent: TextContent, options: HighlightOptions) { 205 | const convertedMatches = [] 206 | for (const query of queries) { 207 | const matches = searchQuery(textContent, query, options) 208 | convertedMatches.push(...convertMatches(matches, textContent)) 209 | } 210 | return convertedMatches 211 | } 212 | 213 | export { findMatches, highlightMatches, resetDivs } 214 | -------------------------------------------------------------------------------- /packages/vue-pdf/src/components/utils/link_service.ts: -------------------------------------------------------------------------------- 1 | import type { IPDFLinkService } from 'pdfjs-dist/types/web/interfaces' 2 | 3 | class SimpleLinkService implements IPDFLinkService { 4 | externalLinkEnabled: boolean 5 | 6 | constructor() { 7 | this.externalLinkEnabled = true 8 | } 9 | 10 | /** 11 | * @type {number} 12 | */ 13 | get pagesCount() { 14 | return 0 15 | } 16 | 17 | /** 18 | * @type {number} 19 | */ 20 | get page() { 21 | return 0 22 | } 23 | 24 | /** 25 | * @param {number} _value 26 | */ 27 | set page(_value: number) {} 28 | 29 | /** 30 | * @type {number} 31 | */ 32 | get rotation() { 33 | return 0 34 | } 35 | 36 | /** 37 | * @param {number} _value 38 | */ 39 | set rotation(_value: number) {} 40 | 41 | /** 42 | * @type {boolean} 43 | */ 44 | get isInPresentationMode() { 45 | return false 46 | } 47 | 48 | /** 49 | * @param {string|Array} _dest - The named, or explicit, PDF destination. 50 | */ 51 | async goToDestination(_dest: string | Array) {} 52 | 53 | /** 54 | * @param {number|string} _val - The page number, or page label. 55 | */ 56 | goToPage(_val: number | string) {} 57 | 58 | /** 59 | * @param {HTMLAnchorElement} link 60 | * @param {string} url 61 | * @param {boolean} [_newWindow] 62 | */ 63 | addLinkAttributes(link: HTMLAnchorElement, url: string, _newWindow = false) { } 64 | 65 | /** 66 | * @param _dest - The PDF destination object. 67 | * @returns {string} The hyperlink to the PDF object. 68 | */ 69 | getDestinationHash(_dest: any): string { 70 | return '#' 71 | } 72 | 73 | /** 74 | * @param _hash - The PDF parameters/hash. 75 | * @returns {string} The hyperlink to the PDF object. 76 | */ 77 | getAnchorUrl(_hash: any): string { 78 | return '#' 79 | } 80 | 81 | /** 82 | * @param {string} _hash 83 | */ 84 | setHash(_hash: string) {} 85 | 86 | /** 87 | * @param {string} _action 88 | */ 89 | executeNamedAction(_action: string) {} 90 | 91 | /** 92 | * @param {Object} _action 93 | */ 94 | executeSetOCGState(_action: object) {} 95 | 96 | /** 97 | * @param {number} _pageNum - page number. 98 | * @param {Object} _pageRef - reference to the page. 99 | */ 100 | cachePageRef(_pageNum: number, _pageRef: object) {} 101 | } 102 | 103 | export { SimpleLinkService } 104 | -------------------------------------------------------------------------------- /packages/vue-pdf/src/components/utils/miscellaneous.ts: -------------------------------------------------------------------------------- 1 | async function createIframe(): Promise { 2 | return new Promise((resolve, reject) => { 3 | const iframe = document.createElement('iframe') 4 | 5 | iframe.width = '0px' 6 | iframe.height = '0px' 7 | iframe.style.cssText = 'position: absolute; top:0; left:0' 8 | iframe.style.display = 'none' 9 | 10 | iframe.onload = function () { 11 | resolve(iframe) 12 | } 13 | document.body.appendChild(iframe) 14 | }) 15 | } 16 | 17 | function addStylesToIframe(content: Window, sizeX: number, sizeY: number) { 18 | const style = content.document.createElement('style') 19 | style.textContent = ` 20 | @page { 21 | margin: 0; 22 | size: ${sizeX}pt ${sizeY}pt; 23 | } 24 | body { 25 | margin: 0; 26 | width: 100%; 27 | } 28 | canvas { 29 | width: 100%; 30 | page-break-after: always; 31 | page-break-before: avoid; 32 | page-break-inside: avoid; 33 | } 34 | ` 35 | content.document.head.appendChild(style) 36 | } 37 | 38 | export { 39 | addStylesToIframe, createIframe, 40 | } 41 | -------------------------------------------------------------------------------- /packages/vue-pdf/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*?worker" { 2 | const workerConstructor: { 3 | new (): Worker; 4 | }; 5 | export default workerConstructor; 6 | } 7 | 8 | declare module "*?url" { 9 | const url: string; 10 | export default url; 11 | } 12 | 13 | declare module "*?worker&url" { 14 | const workerUrl: string; 15 | export default workerUrl; 16 | } 17 | 18 | declare module "pdfjs-dist/build/pdf" { 19 | export * from "pdfjs-dist"; 20 | } 21 | 22 | declare module "*.vue" { 23 | import type { DefineComponent } from "vue"; 24 | const component: DefineComponent<{}, {}, any>; 25 | export default component; 26 | } 27 | -------------------------------------------------------------------------------- /packages/vue-pdf/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vue' 2 | import VuePDF from './components/VuePDF.vue' 3 | 4 | export const VuePDFPlugin: Plugin = { 5 | install(Vue) { 6 | Vue.component(VuePDF.name, VuePDF) 7 | }, 8 | } 9 | 10 | export * from './components' 11 | export default VuePDFPlugin 12 | -------------------------------------------------------------------------------- /packages/vue-pdf/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "dist/types" 6 | }, 7 | "include": ["src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/vue-pdf/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": [ 14 | "vite.config.ts", 15 | "src/**/*.vue", 16 | "src/**/*.ts", 17 | "src/global.dt.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/vue-pdf/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import { defineConfig, mergeConfig } from 'vite' 3 | import commonConfig from '../../vite.config' 4 | 5 | // https://vitejs.dev/config/ 6 | export default mergeConfig( 7 | commonConfig, 8 | defineConfig({ 9 | build: { 10 | lib: { 11 | entry: resolve(__dirname, './src/index.ts'), 12 | name: '@tato30/vue-pdf', 13 | fileName: 'index', 14 | }, 15 | rollupOptions: { 16 | external: ['vue', 'pdfjs-dist'], 17 | output: { 18 | exports: 'named', 19 | globals: { 20 | 'vue': 'vue', 21 | 'pdfjs-dist': 'PDFJS', 22 | }, 23 | }, 24 | }, 25 | }, 26 | }), 27 | ) 28 | -------------------------------------------------------------------------------- /samples/14.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaTo30/vue-pdf/cdf02aaf921048c575d396fd8b4e2125ada6ffe6/samples/14.pdf -------------------------------------------------------------------------------- /samples/36.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaTo30/vue-pdf/cdf02aaf921048c575d396fd8b4e2125ada6ffe6/samples/36.pdf -------------------------------------------------------------------------------- /samples/41.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaTo30/vue-pdf/cdf02aaf921048c575d396fd8b4e2125ada6ffe6/samples/41.pdf -------------------------------------------------------------------------------- /samples/45.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaTo30/vue-pdf/cdf02aaf921048c575d396fd8b4e2125ada6ffe6/samples/45.pdf -------------------------------------------------------------------------------- /samples/issue126.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaTo30/vue-pdf/cdf02aaf921048c575d396fd8b4e2125ada6ffe6/samples/issue126.pdf -------------------------------------------------------------------------------- /samples/issue133.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaTo30/vue-pdf/cdf02aaf921048c575d396fd8b4e2125ada6ffe6/samples/issue133.pdf -------------------------------------------------------------------------------- /samples/issue141.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaTo30/vue-pdf/cdf02aaf921048c575d396fd8b4e2125ada6ffe6/samples/issue141.pdf -------------------------------------------------------------------------------- /samples/issue41.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaTo30/vue-pdf/cdf02aaf921048c575d396fd8b4e2125ada6ffe6/samples/issue41.pdf -------------------------------------------------------------------------------- /samples/issue91.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaTo30/vue-pdf/cdf02aaf921048c575d396fd8b4e2125ada6ffe6/samples/issue91.pdf -------------------------------------------------------------------------------- /samples/issue93.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaTo30/vue-pdf/cdf02aaf921048c575d396fd8b4e2125ada6ffe6/samples/issue93.pdf -------------------------------------------------------------------------------- /samples/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaTo30/vue-pdf/cdf02aaf921048c575d396fd8b4e2125ada6ffe6/samples/logo.png -------------------------------------------------------------------------------- /samples/xfa.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaTo30/vue-pdf/cdf02aaf921048c575d396fd8b4e2125ada6ffe6/samples/xfa.pdf -------------------------------------------------------------------------------- /tests/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@samples/*.pdf" { 2 | const pdfurl: string; 3 | export default pdfurl; 4 | } -------------------------------------------------------------------------------- /tests/layers.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test, vi } from 'vitest' 2 | 3 | import { mount } from '@vue/test-utils' 4 | 5 | import { VuePDF, usePDF } from '@tato30/vue-pdf' 6 | import type { HighlightEventPayload } from '@tato30/vue-pdf/src/components/types.ts' 7 | 8 | import a14PDF from '@samples/14.pdf' 9 | import a45PDF from "@samples/45.pdf" 10 | import xfaPDF from "@samples/xfa.pdf" 11 | 12 | describe('Text Layer', () => { 13 | const { pdf } = usePDF( 14 | 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf', 15 | ) 16 | 17 | beforeAll(async () => { 18 | await vi.waitUntil(() => pdf.value, { timeout: 5000 }) 19 | }) 20 | 21 | test('Visibility', async () => { 22 | const wrapper = mount(VuePDF, { 23 | props: { 24 | pdf: pdf.value, 25 | }, 26 | }) 27 | expect(wrapper).toBeTruthy() 28 | 29 | await vi.waitUntil(() => wrapper.vm.internalProps.viewport) 30 | expect(() => wrapper.get('div.textLayer')).toThrowError() 31 | 32 | await wrapper.setProps({ textLayer: true }) 33 | expect(wrapper.get('div.textLayer')).toBeTruthy() 34 | 35 | await vi.waitUntil(() => wrapper.emitted('textLoaded')) 36 | expect(wrapper.emitted('textLoaded')).toHaveLength(1) 37 | }) 38 | 39 | test('Highlight', async () => { 40 | const wrapper = mount(VuePDF, { 41 | props: { 42 | pdf: pdf.value, 43 | textLayer: true, 44 | highlightText: 'dynamic', 45 | highlightOptions: { 46 | completeWords: false, 47 | ignoreCase: true, 48 | }, 49 | }, 50 | }) 51 | 52 | expect(wrapper).toBeTruthy() 53 | await vi.waitUntil(() => wrapper.emitted('highlight')) 54 | 55 | expect(wrapper.emitted('highlight')).toHaveLength(1) 56 | expect((wrapper.emitted('highlight')![0][0]! as HighlightEventPayload).textDivs).toHaveLength(182) 57 | expect((wrapper.emitted('highlight')![0][0]! as HighlightEventPayload).matches).toHaveLength(11) 58 | 59 | await wrapper.setProps({ 60 | highlightOptions: { 61 | completeWords: true, 62 | ignoreCase: true, 63 | }, 64 | }) 65 | await vi.waitUntil(() => wrapper.emitted('highlight')?.length === 2) 66 | expect((wrapper.emitted('highlight')![1][0]! as HighlightEventPayload).matches).toHaveLength(8) 67 | 68 | await wrapper.setProps({ 69 | highlightOptions: { 70 | ignoreCase: false, 71 | completeWords: true, 72 | }, 73 | }) 74 | await vi.waitUntil(() => wrapper.emitted('highlight')?.length === 3) 75 | expect((wrapper.emitted('highlight')![2][0]! as HighlightEventPayload).matches).toHaveLength(5) 76 | 77 | await wrapper.setProps({ 78 | highlightOptions: { 79 | ignoreCase: false, 80 | completeWords: false, 81 | }, 82 | }) 83 | await vi.waitUntil(() => wrapper.emitted('highlight')?.length === 4) 84 | expect((wrapper.emitted('highlight')![3][0]! as HighlightEventPayload).matches).toHaveLength(8) 85 | }) 86 | }) 87 | 88 | describe('Annotation Layer', () => { 89 | const { pdf } = usePDF(a14PDF) 90 | const { pdf: pdf45 } = usePDF(a45PDF) 91 | 92 | beforeAll(async () => { 93 | await vi.waitUntil(() => pdf.value, { timeout: 5000 }) 94 | await vi.waitUntil(() => pdf45.value, { timeout: 5000 }) 95 | }) 96 | 97 | test('Visibility', async () => { 98 | const wrapper = mount(VuePDF, { 99 | props: { 100 | pdf: pdf.value, 101 | }, 102 | }) 103 | expect(wrapper).toBeTruthy() 104 | 105 | await vi.waitUntil(() => wrapper.vm.internalProps.viewport) 106 | expect(() => wrapper.get('div.annotationLayer')).toThrowError() 107 | 108 | await wrapper.setProps({ annotationLayer: true }) 109 | expect(wrapper.get('div.annotationLayer')).toBeTruthy() 110 | 111 | await vi.waitUntil(() => wrapper.emitted('annotationLoaded')) 112 | expect(wrapper.emitted('annotationLoaded')).toHaveLength(1) 113 | }) 114 | 115 | test('Forms Fields', async () => { 116 | const wrapper = mount(VuePDF, { 117 | props: { 118 | pdf: pdf.value, 119 | annotationLayer: true, 120 | }, 121 | }) 122 | expect(wrapper).toBeTruthy() 123 | 124 | await vi.waitUntil(() => wrapper.get('div.annotationLayer').element.childNodes.length > 0) 125 | 126 | const checkbox = wrapper.get('input[type=\'checkbox\']').element as HTMLInputElement 127 | checkbox.click() 128 | await vi.waitUntil(() => wrapper.emitted('annotation')) 129 | 130 | expect(wrapper.emitted('annotation')).toHaveLength(1) 131 | expect(wrapper.emitted('annotation')![0][0]).toEqual( 132 | { 133 | type: 'form-checkbox', 134 | data: { 135 | fieldName: 'newsletter', 136 | checked: true, 137 | }, 138 | }) 139 | 140 | const radiobutton = wrapper.get('input[data-element-id="14R"]').element as HTMLInputElement 141 | radiobutton.click() 142 | await vi.waitUntil(() => wrapper.emitted('annotation')?.length === 2) 143 | expect(wrapper.emitted('annotation')![1][0]).toEqual({ 144 | type: 'form-radio', 145 | data: { 146 | fieldName: 'drink', 147 | value: 'Wine', 148 | defaultValue: 'Beer', 149 | options: ['Water', 'Beer', 'Wine', 'Milk'], 150 | }, 151 | }) 152 | 153 | const selectOption = wrapper.get('select[data-element-id="9R"]').element as HTMLSelectElement 154 | selectOption.value = 'F' 155 | selectOption.dispatchEvent(new Event('input', { bubbles: true })) 156 | await vi.waitUntil(() => wrapper.emitted('annotation')?.length === 3) 157 | expect(wrapper.emitted('annotation')![2][0]).toEqual({ 158 | type: 'form-select', 159 | data: { 160 | fieldName: 'gender', 161 | value: [ 162 | { 163 | value: 'F', 164 | label: 'Female', 165 | }, 166 | ], 167 | options: [ 168 | { 169 | value: '', 170 | label: '-', 171 | }, 172 | { 173 | value: 'M', 174 | label: 'Male', 175 | }, 176 | { 177 | value: 'F', 178 | label: 'Female', 179 | }, 180 | ], 181 | }, 182 | }) 183 | 184 | const textInput = wrapper.get('input[name="firstname"]').element as HTMLInputElement 185 | textInput.value = 'Testing' 186 | textInput.dispatchEvent(new Event('input', { bubbles: true })) 187 | await vi.waitUntil(() => wrapper.emitted('annotation')?.length === 4) 188 | expect(wrapper.emitted('annotation')![3][0]).toEqual({ 189 | type: 'form-text', 190 | data: { 191 | fieldName: 'firstname', 192 | value: 'Testing', 193 | }, 194 | }) 195 | }) 196 | 197 | test('Links', async () => { 198 | const wrapper = mount(VuePDF, { 199 | props: { 200 | pdf: pdf45.value, 201 | annotationLayer: true, 202 | }, 203 | }) 204 | expect(wrapper).toBeTruthy() 205 | 206 | await vi.waitUntil(() => wrapper.get('div.annotationLayer').element.childNodes.length > 0) 207 | 208 | const indexLink = wrapper.get('a[data-element-id="13R"]').element as HTMLAnchorElement 209 | indexLink.click() 210 | await vi.waitUntil(() => wrapper.emitted('annotation')) 211 | expect(wrapper.emitted('annotation')![0][0]).toEqual({ 212 | type: 'internal-link', 213 | data: { 214 | referencedPage: 2, 215 | offset: { 216 | left: 0, 217 | bottom: 841.89, 218 | }, 219 | }, 220 | }) 221 | }) 222 | }) 223 | 224 | describe('XFA Layer', () => { 225 | const { pdf } = usePDF({ 226 | url: xfaPDF, 227 | enableXfa: true, 228 | }) 229 | 230 | beforeAll(async () => { 231 | await vi.waitUntil(() => pdf.value, { timeout: 5000 }) 232 | }) 233 | 234 | test('Visibility', async () => { 235 | const wrapper = mount(VuePDF, { 236 | props: { 237 | pdf: pdf.value, 238 | }, 239 | }) 240 | expect(wrapper).toBeTruthy() 241 | 242 | await vi.waitUntil(() => wrapper.vm.internalProps.viewport) 243 | expect(wrapper.get('div.xfaLayer')).toBeTruthy() 244 | 245 | await vi.waitUntil(() => wrapper.emitted('xfaLoaded')) 246 | expect(wrapper.emitted('xfaLoaded')).toHaveLength(1) 247 | }) 248 | }) 249 | -------------------------------------------------------------------------------- /tests/loading.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, expect, test, vi } from 'vitest' 2 | 3 | import { mount } from '@vue/test-utils' 4 | 5 | import { VuePDF, usePDF } from '@tato30/vue-pdf' 6 | 7 | const { pdf, pages } = usePDF( 8 | 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf', 9 | ) 10 | 11 | beforeAll(async () => { 12 | await vi.waitUntil(() => pdf.value, { timeout: 5000 }) 13 | }) 14 | 15 | test('Load/Mount component', async () => { 16 | expect(pdf.value).toBeTruthy() 17 | expect(pages.value).toBe(14) 18 | 19 | const wrapper = mount(VuePDF, { 20 | props: { 21 | pdf: pdf.value, 22 | }, 23 | }) 24 | 25 | expect(wrapper).toBeTruthy() 26 | 27 | await vi.waitUntil(() => wrapper.emitted('loaded'), { 28 | timeout: 5000, 29 | }) 30 | 31 | expect(wrapper.emitted('loaded')).toHaveLength(1) 32 | expect(wrapper.emitted('loaded')![0]).toEqual([ 33 | { 34 | viewBox: [0, 0, 612, 792], 35 | scale: 1, 36 | rotation: 0, 37 | offsetX: 0, 38 | offsetY: 0, 39 | transform: [1, 0, 0, -1, 0, 792], 40 | width: 612, 41 | height: 792, 42 | }, 43 | ]) 44 | 45 | // console.log(wrapper.get('canvas').element.toDataURL('image/png')) 46 | }) 47 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-pdf-tests", 3 | "version": "1.0.0", 4 | "description": "vue-pdf-tests", 5 | "scripts": { 6 | "test": "vitest run" 7 | }, 8 | "dependencies": { 9 | "@tato30/vue-pdf": "*" 10 | }, 11 | "devDependencies": { 12 | "@vitejs/plugin-vue": "^4.2.1", 13 | "@vitest/browser": "^1.0.4", 14 | "@vue/test-utils": "^2.4.6", 15 | "typescript": "^4.9.4", 16 | "vite": "^4.3.4", 17 | "vitest": "^1.0.4", 18 | "vue": "^3.2.47", 19 | "vue-tsc": "^1.6.3", 20 | "webdriverio": "^8.26.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/sizing.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, expect, test, vi } from 'vitest' 2 | 3 | import { mount } from '@vue/test-utils' 4 | 5 | import { VuePDF, usePDF } from '@tato30/vue-pdf' 6 | 7 | const { pdf } = usePDF( 8 | 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf', 9 | ) 10 | 11 | beforeAll(async () => { 12 | await vi.waitUntil(() => pdf.value, { timeout: 5000 }) 13 | }) 14 | 15 | test('Scaling', async () => { 16 | const wrapper = mount(VuePDF, { 17 | props: { 18 | pdf: pdf.value, 19 | }, 20 | }) 21 | 22 | expect(wrapper).toBeTruthy() 23 | await vi.waitUntil(() => wrapper.vm.internalProps.viewport, { 24 | timeout: 5000, 25 | }) 26 | 27 | let viewport = wrapper.vm.internalProps.viewport 28 | expect(viewport.width).toBe(612) 29 | 30 | await wrapper.setProps({ scale: 2 }) 31 | viewport = wrapper.vm.internalProps.viewport 32 | expect(viewport.width).toBe(612 * 2) 33 | }) 34 | 35 | test('Width and Height', async () => { 36 | const wrapper = mount(VuePDF, { 37 | props: { 38 | pdf: pdf.value, 39 | height: 500, 40 | }, 41 | }) 42 | 43 | expect(wrapper).toBeTruthy() 44 | await vi.waitUntil(() => wrapper.vm.internalProps.viewport, { 45 | timeout: 5000, 46 | }) 47 | 48 | let viewport = wrapper.vm.internalProps.viewport 49 | expect(Math.round(viewport.height as number)).toBe(500) 50 | 51 | await wrapper.setProps({ width: 500 }) 52 | viewport = wrapper.vm.internalProps.viewport 53 | expect(Math.round(viewport.width as number)).toBe(500) 54 | }) 55 | 56 | test('Fit Parent', async () => { 57 | const wrapper = mount(VuePDF, { 58 | props: { 59 | pdf: pdf.value, 60 | scale: 2, 61 | fitParent: true, 62 | }, 63 | }) 64 | 65 | expect(wrapper).toBeTruthy() 66 | await vi.waitUntil(() => wrapper.vm.internalProps.viewport, { 67 | timeout: 5000, 68 | }) 69 | 70 | const viewport = wrapper.vm.internalProps.viewport 71 | expect(viewport.width).toBe(0) // 0 because there is not a parent node 72 | }) 73 | 74 | test('Rotation', async () => { 75 | const wrapper = mount(VuePDF, { 76 | props: { 77 | pdf: pdf.value, 78 | rotation: 90, 79 | }, 80 | }) 81 | 82 | expect(wrapper).toBeTruthy() 83 | await vi.waitUntil(() => wrapper.vm.internalProps.viewport, { 84 | timeout: 5000, 85 | }) 86 | 87 | const viewport = wrapper.vm.internalProps.viewport 88 | expect(viewport.rotation).toBe(90) 89 | }) 90 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "resolveJsonModule": true, 12 | "baseUrl": ".", 13 | "paths": { 14 | "@tato30/vue-pdf": [ 15 | "../packages/vue-pdf/src" 16 | ] 17 | } 18 | }, 19 | "include": [ 20 | "*.ts", 21 | "env.d.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:url"; 2 | import { defineConfig, mergeConfig } from 'vitest/config'; 3 | import commonConfig from "../vite.config"; 4 | 5 | export default mergeConfig( 6 | commonConfig, 7 | defineConfig({ 8 | test: { 9 | browser: { 10 | enabled: true, 11 | name: "firefox", 12 | headless: true, 13 | }, 14 | }, 15 | resolve: { 16 | alias: { 17 | "@tato30/vue-pdf": resolve(__dirname, "packages/vue-pdf/src"), 18 | }, 19 | } 20 | }) 21 | ); 22 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import vue from '@vitejs/plugin-vue' 3 | import { defineConfig } from 'vite' 4 | 5 | export default defineConfig({ 6 | optimizeDeps: { 7 | esbuildOptions: { 8 | supported: { 9 | 'top-level-await': true, 10 | }, 11 | }, 12 | }, 13 | resolve: { 14 | alias: { 15 | '@samples': resolve(__dirname, 'samples'), 16 | }, 17 | }, 18 | plugins: [vue()], 19 | }) 20 | --------------------------------------------------------------------------------