├── docs
├── public
├── examples
│ ├── README.md
│ ├── basic
│ │ ├── xfa_layer.md
│ │ ├── all_pages.md
│ │ ├── annotation_layer.md
│ │ ├── text_layer.md
│ │ ├── one_page.md
│ │ ├── rotation.md
│ │ └── scale.md
│ ├── annotation_events
│ │ ├── annotation_links.md
│ │ ├── annotation_forms.md
│ │ └── annotation_attachment.md
│ ├── loaded_events
│ │ ├── xfa_loaded.md
│ │ ├── loaded.md
│ │ ├── annotation_loaded.md
│ │ └── text_loaded.md
│ ├── advanced
│ │ ├── annotation_filter.md
│ │ ├── highlight_text.md
│ │ ├── fit_parent.md
│ │ ├── multiple_pdf.md
│ │ ├── watermark.md
│ │ └── toc.md
│ └── text_events
│ │ └── text_highlight.md
├── components
│ ├── AllPages.vue
│ ├── XFALayer.vue
│ ├── AnnotationLoaded.vue
│ ├── TextLoaded.vue
│ ├── AnnotationLayer.vue
│ ├── TextLayer.vue
│ ├── AnnoForms.vue
│ ├── LoadedEvent.vue
│ ├── ChaptersList.vue
│ ├── XFALoaded.vue
│ ├── RotationPage.vue
│ ├── AnnoLinks.vue
│ ├── ScalePage.vue
│ ├── AnnoAttachment.vue
│ ├── OnePage.vue
│ ├── AnnotationFilter.vue
│ ├── FitParent.vue
│ ├── TextHighlight.vue
│ ├── MultiplePDF.vue
│ ├── HighlightText.vue
│ ├── TOC.vue
│ └── WatermarkPage.vue
├── package.json
├── guide
│ ├── slots.md
│ ├── methods.md
│ ├── composables.md
│ ├── props.md
│ ├── events.md
│ └── introduction.md
├── index.md
└── .vitepress
│ ├── styles
│ └── index.scss
│ ├── theme
│ └── index.js
│ └── config.js
├── .eslintignore
├── README.md
├── samples
├── 14.pdf
├── 32.pdf
├── 36.pdf
├── 41.pdf
├── 42.pdf
├── 45.pdf
├── 58.pdf
├── 9.pdf
├── logo.png
├── xfa.pdf
├── issue41.pdf
├── issue91.pdf
├── issue93.pdf
├── issue126.pdf
├── issue133.pdf
├── issue141.pdf
└── issue170.pdf
├── tests
├── env.d.ts
├── tsconfig.json
├── package.json
├── vitest.config.ts
├── loading.spec.ts
├── sizing.spec.ts
└── layers.spec.ts
├── packages
├── playground
│ ├── main.ts
│ ├── env.d.ts
│ ├── index.html
│ ├── vite.config.ts
│ ├── package.json
│ ├── App.vue
│ ├── src
│ │ ├── CustomLoading.vue
│ │ ├── PageNavigation.vue
│ │ ├── MultiPages.vue
│ │ ├── FitParent.vue
│ │ ├── TextLayer.vue
│ │ ├── XFALayer.vue
│ │ └── AnnoLayer.vue
│ └── tsconfig.json
└── vue-pdf
│ ├── src
│ ├── components
│ │ ├── index.ts
│ │ ├── utils
│ │ │ ├── miscellaneous.ts
│ │ │ ├── link_service.ts
│ │ │ ├── destination.ts
│ │ │ ├── highlight.ts
│ │ │ └── annotations.ts
│ │ ├── layers
│ │ │ ├── XFALayer.vue
│ │ │ ├── TextLayer.vue
│ │ │ └── AnnotationLayer.vue
│ │ ├── types.ts
│ │ ├── composable.ts
│ │ └── VuePDF.vue
│ ├── index.ts
│ └── global.d.ts
│ ├── tsconfig.build.json
│ ├── tsconfig.json
│ ├── vite.config.ts
│ ├── package.json
│ └── README.md
├── .github
├── ISSUE_TEMPLATE
│ ├── something-else-👀.md
│ ├── bug-report-🪲.md
│ └── feature-request-🆕.md
├── FUNDING.yml
└── workflows
│ ├── deploy-docs.yaml
│ └── publish-package.yaml
├── .eslintrc.cjs
├── .editorconfig
├── vite.config.ts
├── .npmignore
├── .gitignore
├── package.json
└── LICENSE
/docs/public:
--------------------------------------------------------------------------------
1 | ../samples
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | README.md
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ./packages/vue-pdf/README.md
--------------------------------------------------------------------------------
/docs/examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples List
--------------------------------------------------------------------------------
/samples/14.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/14.pdf
--------------------------------------------------------------------------------
/samples/32.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/32.pdf
--------------------------------------------------------------------------------
/samples/36.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/36.pdf
--------------------------------------------------------------------------------
/samples/41.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/41.pdf
--------------------------------------------------------------------------------
/samples/42.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/42.pdf
--------------------------------------------------------------------------------
/samples/45.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/45.pdf
--------------------------------------------------------------------------------
/samples/58.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/58.pdf
--------------------------------------------------------------------------------
/samples/9.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/9.pdf
--------------------------------------------------------------------------------
/samples/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/logo.png
--------------------------------------------------------------------------------
/samples/xfa.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/xfa.pdf
--------------------------------------------------------------------------------
/samples/issue41.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/issue41.pdf
--------------------------------------------------------------------------------
/samples/issue91.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/issue91.pdf
--------------------------------------------------------------------------------
/samples/issue93.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/issue93.pdf
--------------------------------------------------------------------------------
/samples/issue126.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/issue126.pdf
--------------------------------------------------------------------------------
/samples/issue133.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/issue133.pdf
--------------------------------------------------------------------------------
/samples/issue141.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/issue141.pdf
--------------------------------------------------------------------------------
/samples/issue170.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TaTo30/vue-pdf/HEAD/samples/issue170.pdf
--------------------------------------------------------------------------------
/tests/env.d.ts:
--------------------------------------------------------------------------------
1 | declare module "@samples/*.pdf" {
2 | const pdfurl: string;
3 | export default pdfurl;
4 | }
--------------------------------------------------------------------------------
/packages/playground/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import App from './App.vue'
3 |
4 | createApp(App).mount('#app')
5 |
--------------------------------------------------------------------------------
/packages/vue-pdf/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as VuePDF } from './VuePDF.vue'
2 | export * from './composable'
3 | export * from './types'
4 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/playground/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/components/AllPages.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/components/XFALayer.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/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": "^22.14.0",
13 | "typescript": "^4.9.4",
14 | "vite": "^7.1.9",
15 | "vue": "^3.2.47"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/playground/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/docs/components/AnnotationLoaded.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/docs/examples/basic/xfa_layer.md:
--------------------------------------------------------------------------------
1 | # XFA Forms
2 |
3 | ```vue
4 |
13 |
14 |
15 |
16 |
17 |
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 |
--------------------------------------------------------------------------------
/docs/components/TextLoaded.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/docs/examples/basic/all_pages.md:
--------------------------------------------------------------------------------
1 | # All pages
2 |
3 | ```vue
4 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ```
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/docs/examples/annotation_events/annotation_links.md:
--------------------------------------------------------------------------------
1 | # Links
2 |
3 | ```vue
4 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ```
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/docs/examples/annotation_events/annotation_forms.md:
--------------------------------------------------------------------------------
1 | # Forms fields
2 |
3 | ```vue
4 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ```
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/examples/loaded_events/xfa_loaded.md:
--------------------------------------------------------------------------------
1 | # XFA Loaded Event
2 |
3 | ```vue
4 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ```
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.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 | __screenshots__
28 |
29 | # Editor directories and files
30 | .vscode/*
31 | !.vscode/extensions.json
32 | .idea
33 | *.suo
34 | *.ntvs*
35 | *.njsproj
36 | *.sln
37 | *.sw?
38 |
--------------------------------------------------------------------------------
/docs/examples/loaded_events/loaded.md:
--------------------------------------------------------------------------------
1 | # Loaded Event
2 |
3 | ```vue
4 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ```
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/examples/annotation_events/annotation_attachment.md:
--------------------------------------------------------------------------------
1 | # File attachment
2 |
3 | ```vue
4 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ```
20 |
21 |
22 |
--------------------------------------------------------------------------------
/packages/playground/src/CustomLoading.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
18 |
19 |
20 |
23 |
--------------------------------------------------------------------------------
/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": "npm run patch && vitepress build --clean-temp --clean-cache .",
8 | "patch": "replace-in-file 'pdfjs-dist' 'pdfjs-dist/legacy/build/pdf.mjs' ../packages/vue-pdf/dist/index.mjs"
9 | },
10 | "dependencies": {
11 | "@tato30/vue-pdf": "*"
12 | },
13 | "devDependencies": {
14 | "sass": "^1.77.2",
15 | "vite": "^7.1.9",
16 | "vitepress": "^1.6.4",
17 | "replace-in-file": "^8.3.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/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": "^3.2.4",
14 | "@vue/test-utils": "^2.4.6",
15 | "playwright": "^1.55.1",
16 | "typescript": "^4.9.4",
17 | "vite": "^7.1.9",
18 | "vitest": "^3.2.4",
19 | "vue": "^3.2.47",
20 | "vue-tsc": "^1.6.3",
21 | "webdriverio": "^8.26.2"
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 | provider: "playwright",
11 | enabled: true,
12 | headless: true,
13 | instances: [{ browser: "firefox" }],
14 | },
15 | },
16 | resolve: {
17 | alias: {
18 | "@tato30/vue-pdf": resolve(__dirname, "packages/vue-pdf/src"),
19 | },
20 | },
21 | })
22 | );
23 |
--------------------------------------------------------------------------------
/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 |
18 |
19 |
20 |
21 |
22 | ```
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/components/AnnotationLayer.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 | Change to {{ !annotation_layer }}
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/docs/components/TextLayer.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 | Change to {{ !text_layer }}
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/docs/components/AnnoForms.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
{{ eventValue }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/docs/components/LoadedEvent.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
{{ eventValue }}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/packages/playground/src/PageNavigation.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 | + Page
14 |
15 |
16 | - Page
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
--------------------------------------------------------------------------------
/docs/components/ChaptersList.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 | {{ item.title }}
19 |
20 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/docs/examples/basic/annotation_layer.md:
--------------------------------------------------------------------------------
1 | # Annotation Layer
2 |
3 | ```vue
4 |
12 |
13 |
14 |
15 |
16 |
17 | Change to {{ !annotation_layer }}
18 |
19 |
20 |
21 |
22 |
23 | ```
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
18 |
19 |
20 |
21 |
22 | ```
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/docs/examples/basic/text_layer.md:
--------------------------------------------------------------------------------
1 | # Text Layer
2 |
3 | ```vue
4 |
12 |
13 |
14 |
15 |
16 |
17 | Change to {{ !text_layer }}
18 |
19 |
20 |
21 |
22 |
23 | ```
24 |
25 |
26 |
--------------------------------------------------------------------------------
/docs/components/XFALoaded.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
{{ eventValue }}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/docs/guide/slots.md:
--------------------------------------------------------------------------------
1 | # Slots
2 |
3 | ## loading: default
4 |
5 | Content to display when page is rendering
6 |
7 | ```vue
8 |
9 |
10 |
11 | Loading...
12 |
13 |
14 |
15 | ```
16 |
17 | ## overlay
18 |
19 | Enable to add overlay content
20 |
21 | ```vue
22 |
23 |
24 |
25 |
26 | Page size {{ width }}x{{ height }}
27 |
28 |
29 |
30 |
31 | ```
32 |
33 | ::: warning
34 | DO NOT ADD a `` element as root of template since it can break the component when page reload on scaling, rotating, etc.
35 | :::
--------------------------------------------------------------------------------
/docs/components/RotationPage.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 | - 90°
14 |
15 | {{ rotation }}°
16 |
17 | + 90°
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/docs/components/AnnoLinks.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
{{ eventValue }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/docs/components/ScalePage.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 | 0.25 ? scale - 0.25 : scale">
13 | -
14 |
15 | {{ scale * 100 }}%
16 |
17 | +
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/docs/examples/basic/one_page.md:
--------------------------------------------------------------------------------
1 | # One page
2 |
3 | ```vue
4 |
11 |
12 |
13 |
14 |
15 | 1 ? page - 1 : page">
16 | Prev
17 |
18 | {{ page }} / {{ pages }}
19 |
20 | Next
21 |
22 |
23 |
24 |
25 |
26 | ```
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/docs/examples/basic/rotation.md:
--------------------------------------------------------------------------------
1 | # Rotation
2 |
3 | ```vue
4 |
11 |
12 |
13 |
14 |
15 |
16 | -90
17 |
18 | {{ rotation }}
19 |
20 | +90
21 |
22 |
23 |
24 |
25 |
26 | ```
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/packages/playground/src/MultiPages.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Loading page... {{ page }}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
32 |
--------------------------------------------------------------------------------
/docs/components/AnnoAttachment.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
// "content" is a uint8Array {{ eventValue }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/docs/examples/basic/scale.md:
--------------------------------------------------------------------------------
1 | # Scale
2 |
3 | ```vue
4 |
11 |
12 |
13 |
14 |
15 | 0.25 ? scale - 0.25 : scale">
16 | -
17 |
18 | {{ scale * 100 }}%
19 |
20 | +
21 |
22 |
23 |
24 |
25 |
26 | ```
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/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/src/FitParent.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 | Add 20px
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
--------------------------------------------------------------------------------
/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 | cssFileName: 'style'
15 | },
16 | rollupOptions: {
17 | external: ['vue', 'pdfjs-dist'],
18 | output: {
19 | exports: 'named',
20 | globals: {
21 | 'vue': 'vue',
22 | 'pdfjs-dist': 'PDFJS',
23 | },
24 | },
25 | },
26 | },
27 | }),
28 | )
29 |
--------------------------------------------------------------------------------
/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 |
18 |
19 |
20 | ```
21 |
22 | ## cancel
23 |
24 | Cancel the render task if the page is currently rendering.
25 |
26 | ```vue
27 |
35 |
36 |
37 |
38 |
39 | ```
--------------------------------------------------------------------------------
/docs/components/OnePage.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 | 1 ? page - 1 : page">
13 | PREV
14 |
15 | {{ page }}/{{ pages }}
16 |
17 | NEXT
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.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/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 }}
--------------------------------------------------------------------------------
/docs/components/AnnotationFilter.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {{ flt }}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/docs/examples/advanced/annotation_filter.md:
--------------------------------------------------------------------------------
1 | # Annotations Filter
2 |
3 | ```vue
4 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{ flt }}
25 |
26 |
27 |
28 |
29 |
30 |
31 | ```
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/docs/examples/advanced/highlight_text.md:
--------------------------------------------------------------------------------
1 | # Highlight Text
2 |
3 | ```vue
4 |
17 |
18 |
19 |
27 |
28 | ```
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/.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: Setup Playwright
27 | run: npx playwright install --with-deps
28 |
29 | - name: Run tests and build
30 | run: npm run build
31 |
32 | - name: Publish packages
33 | working-directory: packages/vue-pdf
34 | run: npm publish --access public
35 | env:
36 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
37 |
38 |
--------------------------------------------------------------------------------
/docs/components/FitParent.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 | Remove 50px
21 |
22 | Parent width: {{ parentWidth }}px
23 |
24 | Add 50px
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/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 |
27 |
28 |
29 |
30 |
31 |
32 | ```
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/docs/examples/advanced/fit_parent.md:
--------------------------------------------------------------------------------
1 | # Fit parent
2 |
3 | ```vue
4 |
18 |
19 |
20 |
21 |
22 |
23 | Remove 50px
24 |
25 | Parent width: {{ parentWidth }}px
26 |
27 | Add 50px
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | ```
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/docs/examples/advanced/multiple_pdf.md:
--------------------------------------------------------------------------------
1 | # Multiples PDF
2 |
3 | ```vue
4 |
28 |
29 |
30 |
31 |
32 |
33 | Next PDF (Current index: {{ pdfSourceIdx }})
34 |
35 |
36 |
37 |
38 |
39 | ```
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/docs/components/TextHighlight.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
35 |
36 |
--------------------------------------------------------------------------------
/docs/components/MultiplePDF.vue:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
30 |
31 | Next PDF (Current index: {{ pdfSourceIdx }})
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/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 | page-break-after: always;
30 | page-break-before: avoid;
31 | page-break-inside: avoid;
32 | }
33 | `
34 | content.document.head.appendChild(style)
35 | }
36 |
37 | export {
38 | addStylesToIframe, createIframe,
39 | }
40 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/packages/playground/src/TextLayer.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 | - Scale
19 |
20 | {{ scale }}
21 |
22 | + Scale
23 |
24 |
25 | - Rotation
26 |
27 | {{ rotation }}
28 |
29 | + Rotation
30 |
31 |
32 | Layer {{ layer }}
33 |
34 |
35 |
36 |
37 |
38 |
41 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/docs/examples/advanced/watermark.md:
--------------------------------------------------------------------------------
1 | # Watermark Text
2 |
3 | ```vue
4 |
24 |
25 |
26 |
37 |
38 | ```
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/docs/examples/advanced/toc.md:
--------------------------------------------------------------------------------
1 | # Table of content
2 |
3 | ```vue
4 |
35 |
36 |
37 |
38 |
39 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | ```
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/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: 10000 });
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 | userUnit: 1,
37 | rotation: 0,
38 | offsetX: 0,
39 | offsetY: 0,
40 | transform: [1, 0, 0, -1, 0, 792],
41 | width: 612,
42 | height: 792,
43 | },
44 | ])
45 |
46 | // console.log(wrapper.get('canvas').element.toDataURL('image/png'))
47 | })
48 |
--------------------------------------------------------------------------------
/docs/components/HighlightText.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
55 |
56 |
--------------------------------------------------------------------------------
/packages/playground/src/XFALayer.vue:
--------------------------------------------------------------------------------
1 |
2 |
27 |
28 |
29 |
30 |
31 | - Scale
32 |
33 |
34 | + Scale
35 |
36 |
37 | - Rotation
38 |
39 |
40 | + Rotation
41 |
42 |
43 | get annotations
44 |
45 |
46 |
54 |
55 |
56 |
57 |
58 |
61 |
--------------------------------------------------------------------------------
/packages/vue-pdf/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tato30/vue-pdf",
3 | "version": "1.11.5",
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": "5.4.296"
51 | },
52 | "devDependencies": {
53 | "@types/node": "^22.14.0",
54 | "eslint": "^8.39.0",
55 | "typescript": "^4.9.4",
56 | "vite": "^7.1.9",
57 | "vue": "^3.2.47",
58 | "vue-tsc": "^1.6.3"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/packages/vue-pdf/src/components/layers/XFALayer.vue:
--------------------------------------------------------------------------------
1 |
53 |
54 |
55 |
56 |
57 |
58 |
64 |
--------------------------------------------------------------------------------
/docs/components/TOC.vue:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 |
36 |
37 |
41 |
42 |
43 |
44 |
45 |
{{ eventValue }}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
74 |
--------------------------------------------------------------------------------
/docs/components/WatermarkPage.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
62 |
63 |
64 | Reload
65 |
66 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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: 10000 });
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 |
--------------------------------------------------------------------------------
/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 LoadedEvent from "../../components/LoadedEvent.vue";
12 | import MultiplePDF from '../../components/MultiplePDF.vue'
13 | import OnePage from '../../components/OnePage.vue'
14 | import RotationPage from "../../components/RotationPage.vue";
15 | import ScalePage from "../../components/ScalePage.vue";
16 | import TextLayer from '../../components/TextLayer.vue'
17 | import XFALayer from '../../components/XFALayer.vue'
18 | import WatermarkPage from "../../components/WatermarkPage.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("WatermarkPage", WatermarkPage);
32 | app.component('AllPages', AllPages)
33 | app.component("ScalePage", ScalePage);
34 | app.component("RotationPage", RotationPage);
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("LoadedEvent", LoadedEvent);
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 |
--------------------------------------------------------------------------------
/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 | goToXY(pageNumber: number, x: number, y: number): void {}
10 |
11 | /**
12 | * @type {number}
13 | */
14 | get pagesCount() {
15 | return 0
16 | }
17 |
18 | /**
19 | * @type {number}
20 | */
21 | get page() {
22 | return 0
23 | }
24 |
25 | /**
26 | * @param {number} _value
27 | */
28 | set page(_value: number) {}
29 |
30 | /**
31 | * @type {number}
32 | */
33 | get rotation() {
34 | return 0
35 | }
36 |
37 | /**
38 | * @param {number} _value
39 | */
40 | set rotation(_value: number) {}
41 |
42 | /**
43 | * @type {boolean}
44 | */
45 | get isInPresentationMode() {
46 | return false
47 | }
48 |
49 | /**
50 | * @param {string|Array} _dest - The named, or explicit, PDF destination.
51 | */
52 | async goToDestination(_dest: string | Array) {}
53 |
54 | /**
55 | * @param {number|string} _val - The page number, or page label.
56 | */
57 | goToPage(_val: number | string) {}
58 |
59 | /**
60 | * @param {HTMLAnchorElement} link
61 | * @param {string} url
62 | * @param {boolean} [_newWindow]
63 | */
64 | addLinkAttributes(link: HTMLAnchorElement, url: string, _newWindow = false) { }
65 |
66 | /**
67 | * @param _dest - The PDF destination object.
68 | * @returns {string} The hyperlink to the PDF object.
69 | */
70 | getDestinationHash(_dest: any): string {
71 | return '#'
72 | }
73 |
74 | /**
75 | * @param _hash - The PDF parameters/hash.
76 | * @returns {string} The hyperlink to the PDF object.
77 | */
78 | getAnchorUrl(_hash: any): string {
79 | return '#'
80 | }
81 |
82 | /**
83 | * @param {string} _hash
84 | */
85 | setHash(_hash: string) {}
86 |
87 | /**
88 | * @param {string} _action
89 | */
90 | executeNamedAction(_action: string) {}
91 |
92 | /**
93 | * @param {Object} _action
94 | */
95 | executeSetOCGState(_action: object) {}
96 |
97 | /**
98 | * @param {number} _pageNum - page number.
99 | * @param {Object} _pageRef - reference to the page.
100 | */
101 | cachePageRef(_pageNum: number, _pageRef: object) {}
102 | }
103 |
104 | export { SimpleLinkService }
105 |
--------------------------------------------------------------------------------
/packages/playground/src/AnnoLayer.vue:
--------------------------------------------------------------------------------
1 |
2 |
48 |
49 |
50 |
51 |
52 | - Scale
53 |
54 |
55 | + Scale
56 |
57 |
58 | - Rotation
59 |
60 |
61 | + Rotation
62 |
63 |
64 | get annotations
65 |
66 |
67 | reload
68 |
69 |
70 |
71 | {{ flt }}
72 |
73 |
74 |
75 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/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/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/layers/TextLayer.vue:
--------------------------------------------------------------------------------
1 |
140 |
141 |
142 |
149 |
150 |
--------------------------------------------------------------------------------
/packages/vue-pdf/src/components/layers/AnnotationLayer.vue:
--------------------------------------------------------------------------------
1 |
145 |
146 |
147 |
148 |
149 |
150 |
161 |
--------------------------------------------------------------------------------
/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 |
21 |
22 |
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 |
39 |
40 |
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 |
192 |
193 |
194 |
195 |
196 | ```
--------------------------------------------------------------------------------
/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/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 |
33 |
34 |
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 |
52 |
53 |
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 |
81 |
82 |
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 | ```
--------------------------------------------------------------------------------
/packages/vue-pdf/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
VuePDF
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 |
45 |
46 |
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 |
64 |
65 |
66 | ```
67 |
68 | ### XFA Forms
69 | XFA forms also can be supported by enabling them from `usePDF`.
70 |
71 | ```vue
72 |
81 |
82 |
83 |
84 |
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 |
--------------------------------------------------------------------------------
/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 | };
--------------------------------------------------------------------------------
/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 (pdf.value)
55 | void pdf.value.destroy()
56 | if (pdfDoc.value)
57 | void pdfDoc.value.destroy()
58 |
59 | const loadingTask = PDFJS.getDocument(source!)
60 | if (options.onProgress)
61 | loadingTask.onProgress = options.onProgress
62 |
63 | if (options.onPassword) {
64 | loadingTask.onPassword = options.onPassword
65 | }
66 | else if (options.password) {
67 | const onPassword: OnPasswordCallback = (updatePassword, _) => {
68 | updatePassword(options.password ?? '')
69 | }
70 | loadingTask.onPassword = onPassword
71 | }
72 |
73 | loadingTask.promise.then(
74 | async (doc) => {
75 | pdfDoc.value = doc
76 |
77 | pdf.value = doc.loadingTask
78 | pages.value = doc.numPages
79 |
80 | const metadata = await doc.getMetadata()
81 | const attachments = (await doc.getAttachments()) as Record
82 | const javascript = await doc.getJSActions()
83 | const outline = await doc.getOutline()
84 |
85 | info.value = {
86 | metadata,
87 | attachments,
88 | javascript,
89 | outline,
90 | }
91 | },
92 | (error) => {
93 | // PDF loading error
94 | if (typeof options.onError === 'function')
95 | options.onError(error)
96 | },
97 | )
98 | }
99 |
100 | async function getPDFDestination(destination: string | any[] | null): Promise {
101 | const document = await pdf.value?.promise
102 | if (!document)
103 | return null
104 |
105 | const destArray = await getDestinationArray(document, destination)
106 | const destRef = await getDestinationRef(document, destArray)
107 | if (!destRef || !destArray)
108 | return null
109 |
110 | const pageIndex = await document.getPageIndex(destRef)
111 |
112 | const name = destArray[1].name
113 | const rest = destArray.slice(2)
114 |
115 | const location = isSpecLike(rest) ? getLocation(name, rest) : null
116 |
117 | return { pageIndex, location: location ?? { type: 'Fit', spec: [] } }
118 | }
119 |
120 | async function getBytes() {
121 | if (!pdfDoc.value)
122 | throw new Error("Current PDFDocumentProxy have not loaded yet");
123 | try {
124 | return await pdfDoc.value.saveDocument();
125 | } catch (error) {
126 | console.error("Error saving PDF document:", error);
127 | return await pdfDoc.value.getData();
128 | }
129 | }
130 |
131 | async function download(filename = 'filename') {
132 | const bytes = await getBytes()
133 | const blobBytes = new Blob([bytes], { type: 'application/pdf' })
134 | const blobUrl = URL.createObjectURL(blobBytes)
135 |
136 | const anchorDownload = document.createElement('a')
137 | document.body.appendChild(anchorDownload)
138 | anchorDownload.href = blobUrl
139 | anchorDownload.download = filename
140 | anchorDownload.style.display = 'none'
141 | anchorDownload.click()
142 |
143 | setTimeout(() => {
144 | URL.revokeObjectURL(blobUrl)
145 | document.body.removeChild(anchorDownload)
146 | }, 10)
147 | }
148 |
149 | async function print(dpi = 150, filename = 'filename') {
150 | if (!pdf.value)
151 | throw new Error("Current PDFDocumentLoadingTask have not loaded yet");
152 |
153 | const savedDocument = await pdf.value.promise;
154 |
155 | const PRINT_UNITS = dpi / 72
156 | const CSS_UNITS = 96 / 72
157 |
158 | const iframe = await createIframe()
159 | const contentWindow = iframe.contentWindow
160 | contentWindow!.document.title = filename
161 |
162 | const pagesNumbers = [...Array(savedDocument.numPages).keys()].map(val => val + 1)
163 |
164 | for (const pageNumber of pagesNumbers) {
165 | const pageToPrint = await savedDocument.getPage(pageNumber)
166 | const viewport = pageToPrint.getViewport({ scale: 1 })!
167 |
168 | if (pageNumber === 1) {
169 | addStylesToIframe(
170 | contentWindow!,
171 | (viewport.width * PRINT_UNITS) / CSS_UNITS,
172 | (viewport.height * PRINT_UNITS) / CSS_UNITS,
173 | )
174 | }
175 |
176 | const canvas = document.createElement('canvas')
177 | canvas.width = viewport.width * PRINT_UNITS
178 | canvas.height = viewport.height * PRINT_UNITS
179 |
180 | const canvasCloned = canvas.cloneNode() as HTMLCanvasElement
181 | contentWindow?.document.body.appendChild(canvasCloned)
182 |
183 | await pageToPrint?.render({
184 | canvas: canvas,
185 | intent: 'print',
186 | transform: [PRINT_UNITS, 0, 0, PRINT_UNITS, 0, 0],
187 | viewport,
188 | }).promise
189 |
190 | canvasCloned.getContext('2d')?.drawImage(canvas, 0, 0)
191 | }
192 |
193 | contentWindow?.focus()
194 | contentWindow?.print()
195 | document.body.removeChild(iframe)
196 | }
197 |
198 | if (isRef(src)) {
199 | if (src.value)
200 | processLoadingTask(src.value)
201 | watch(src, () => {
202 | if (src.value)
203 | processLoadingTask(src.value)
204 | })
205 | }
206 | else {
207 | if (src)
208 | processLoadingTask(src)
209 | }
210 |
211 | return {
212 | pdf,
213 | pages,
214 | info,
215 | print,
216 | download,
217 | getPDFDestination,
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/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: 10000 })
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 | await wrapper.get('input[type=\'checkbox\']').setValue(true)
127 | await vi.waitUntil(() => wrapper.emitted('annotation'))
128 |
129 | expect(wrapper.emitted('annotation')).toHaveLength(1)
130 | expect(wrapper.emitted('annotation')![0][0]).toEqual(
131 | {
132 | type: 'form-checkbox',
133 | data: {
134 | fieldName: 'newsletter',
135 | checked: true,
136 | },
137 | })
138 |
139 | await wrapper.get('input[data-element-id="14R"]').setValue(true)
140 | await vi.waitUntil(() => wrapper.emitted('annotation')?.length === 2)
141 | expect(wrapper.emitted('annotation')![1][0]).toEqual({
142 | type: 'form-radio',
143 | data: {
144 | fieldName: 'drink',
145 | value: 'Wine',
146 | defaultValue: 'Beer',
147 | options: ['Water', 'Beer', 'Wine', 'Milk'],
148 | },
149 | })
150 |
151 | const selectOption = wrapper.get('select[data-element-id="9R"]').element as HTMLSelectElement
152 | selectOption.value = 'F'
153 | selectOption.dispatchEvent(new Event('input', { bubbles: true }))
154 | await vi.waitUntil(() => wrapper.emitted('annotation')?.length === 3)
155 | expect(wrapper.emitted('annotation')![2][0]).toEqual({
156 | type: 'form-select',
157 | data: {
158 | fieldName: 'gender',
159 | value: [
160 | {
161 | value: 'F',
162 | label: 'Female',
163 | },
164 | ],
165 | options: [
166 | {
167 | value: '',
168 | label: '-',
169 | },
170 | {
171 | value: 'M',
172 | label: 'Male',
173 | },
174 | {
175 | value: 'F',
176 | label: 'Female',
177 | },
178 | ],
179 | },
180 | })
181 |
182 | const textInput = wrapper.get('input[name="firstname"]').element as HTMLInputElement
183 | textInput.value = 'Testing'
184 | textInput.dispatchEvent(new Event('input', { bubbles: true }))
185 | await vi.waitUntil(() => wrapper.emitted('annotation')?.length === 4)
186 | expect(wrapper.emitted('annotation')![3][0]).toEqual({
187 | type: 'form-text',
188 | data: {
189 | fieldName: 'firstname',
190 | value: 'Testing',
191 | },
192 | })
193 | })
194 |
195 | test('Links', async () => {
196 | const wrapper = mount(VuePDF, {
197 | props: {
198 | pdf: pdf45.value,
199 | annotationLayer: true,
200 | },
201 | })
202 | expect(wrapper).toBeTruthy()
203 |
204 | await vi.waitUntil(() => wrapper.get('div.annotationLayer').element.childNodes.length > 0)
205 |
206 | await wrapper.get('a[data-element-id="13R"]').trigger("click")
207 | await vi.waitUntil(() => wrapper.emitted('annotation'))
208 | expect(wrapper.emitted('annotation')![0][0]).toEqual({
209 | type: 'internal-link',
210 | data: {
211 | referencedPage: 2,
212 | offset: {
213 | left: 0,
214 | bottom: 841.89,
215 | },
216 | },
217 | })
218 | })
219 | })
220 |
221 | describe('XFA Layer', () => {
222 | const { pdf } = usePDF({
223 | url: xfaPDF,
224 | enableXfa: true,
225 | })
226 |
227 | beforeAll(async () => {
228 | await vi.waitUntil(() => pdf.value, { timeout: 5000 })
229 | })
230 |
231 | test('Visibility', async () => {
232 | const wrapper = mount(VuePDF, {
233 | props: {
234 | pdf: pdf.value,
235 | },
236 | })
237 | expect(wrapper).toBeTruthy()
238 |
239 | await vi.waitUntil(() => wrapper.vm.internalProps.viewport)
240 | expect(wrapper.get('div.xfaLayer')).toBeTruthy()
241 |
242 | await vi.waitUntil(() => wrapper.emitted('xfaLoaded'))
243 | expect(wrapper.emitted('xfaLoaded')).toHaveLength(1)
244 | })
245 | })
246 |
--------------------------------------------------------------------------------
/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 | strs.push(textItem.str);
9 | if (textItem.hasEOL)
10 | strs.push("\n");
11 | }
12 |
13 | let textJoined = strs.join('')
14 | // Join the text as is presented in textlayer and then perform this replacements to build up broken words
15 | // 1. newline between CJK characters should be removed
16 | // 2. hyphen at the end of a line should be removed
17 | textJoined = textJoined.replace(/(?<=\p{Ideographic}|[\u3040-\u30FF])\n(\p{Ideographic}|[\u3040-\u30FF])/gmu, '$1');
18 | textJoined = textJoined.replace(/(?<=\S)-\n/gmu, "");
19 |
20 | // Replace all "valid" newlines with a space
21 | textJoined = textJoined.replace(/\n/g, " ");
22 |
23 | const regexFlags = ['g']
24 | if (options.ignoreCase)
25 | regexFlags.push('i')
26 |
27 | // Trim the query and escape all regex special characters
28 | let fquery = query.trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29 | if (options.completeWords)
30 | fquery = `\\b${fquery}\\b`
31 |
32 | const regex = new RegExp(fquery, regexFlags.join(''))
33 |
34 | const matches = []
35 | let match
36 |
37 | // eslint-disable-next-line no-cond-assign
38 | while ((match = regex.exec(textJoined)) !== null)
39 | matches.push([match.index, match[0].length, match[0]])
40 |
41 | return matches
42 | }
43 |
44 | function convertMatches(matches: (number | string)[][], textContent: TextContent): Match[] {
45 | function endOfLineOffset(
46 | item: TextItem,
47 | prevItem: TextItem | null,
48 | nextItem: TextItem | null
49 | ): number {
50 | // When textitem has a EOL flag and the string has a hyphen at the end
51 | // the hyphen should be removed (-1 len) so the sentence could be searched as a joined one.
52 | // In other cases the EOL flag introduce a whitespace (+1 len) between two different sentences
53 | // In cases where the EOL is between two CJK characters, no offset is added
54 | if (item.hasEOL && item.str.length === 0) {
55 | const lastchar = prevItem?.str[prevItem.str.length - 1];
56 | const nextchar = nextItem?.str[0];
57 |
58 | const isCJK = new RegExp(/(\p{Ideographic}|[\u3040-\u30FF])/u);
59 | if (lastchar && isCJK.test(lastchar) && nextchar && isCJK.test(nextchar))
60 | return 0;
61 | }
62 |
63 | if (item.hasEOL) {
64 | if (item.str.endsWith("-")) return -1;
65 | else return 1;
66 | }
67 | return 0;
68 | }
69 |
70 | let index = 0
71 | let tindex = 0
72 | const textItems = textContent.items as TextItem[]
73 | const end = textItems.length - 1
74 |
75 | const convertedMatches = []
76 |
77 | // iterate over all matches
78 | for (let m = 0; m < matches.length; m++) {
79 | let mindex = matches[m][0] as number
80 |
81 | while (index !== end && mindex >= tindex + textItems[index].str.length) {
82 | const item = textItems[index]
83 | tindex += item.str.length + endOfLineOffset(
84 | item,
85 | index - 1 < 0 ? null : textItems[index - 1],
86 | index + 1 >= textItems.length ? null : textItems[index + 1]
87 | )
88 | index++
89 | }
90 | const divStart = {
91 | idx: index,
92 | offset: mindex - tindex,
93 | }
94 |
95 | mindex += matches[m][1] as number
96 |
97 | while (index !== end && mindex > tindex + textItems[index].str.length) {
98 | const item = textItems[index]
99 | tindex +=
100 | item.str.length +
101 | endOfLineOffset(
102 | item,
103 | index - 1 < 0 ? null : textItems[index - 1],
104 | index + 1 >= textItems.length ? null : textItems[index + 1]
105 | );
106 | index++
107 | }
108 |
109 | const divEnd = {
110 | idx: index,
111 | offset: mindex - tindex,
112 | }
113 |
114 | convertedMatches.push({
115 | start: divStart,
116 | end: divEnd,
117 | str: matches[m][2] as string,
118 | oindex: matches[m][0] as number,
119 | })
120 | }
121 | return convertedMatches
122 | }
123 |
124 | function highlightMatches(matches: Match[], textContent: TextContent, textDivs: HTMLElement[]) {
125 | function appendHighlightDiv(idx: number, startOffset = -1, endOffset = -1) {
126 | const textItem = textContent.items[idx] as TextItem
127 | const nodes = []
128 |
129 | let content = ''
130 | let prevContent = ''
131 | let nextContent = ''
132 |
133 | let div = textDivs[idx]
134 |
135 | if (!div)
136 | return // don't process if div is undefinied
137 |
138 | if (div.nodeType === Node.TEXT_NODE) {
139 | const span = document.createElement('span')
140 | div.before(span)
141 | span.append(div)
142 | textDivs[idx] = span
143 | div = span
144 | }
145 |
146 | if (startOffset >= 0 && endOffset >= 0)
147 | content = textItem.str.substring(startOffset, endOffset)
148 | else if (startOffset < 0 && endOffset < 0)
149 | content = textItem.str
150 | else if (startOffset >= 0)
151 | content = textItem.str.substring(startOffset)
152 | else if (endOffset >= 0)
153 | content = textItem.str.substring(0, endOffset)
154 |
155 | const node = document.createTextNode(content)
156 | const span = document.createElement('span')
157 | span.className = 'highlight appended'
158 | span.append(node)
159 |
160 | nodes.push(span)
161 |
162 | if (startOffset > 0) {
163 | if (div.childNodes.length === 1 && div.childNodes[0].nodeType === Node.TEXT_NODE) {
164 | prevContent = textItem.str.substring(0, startOffset)
165 | const node = document.createTextNode(prevContent)
166 | nodes.unshift(node)
167 | }
168 | else {
169 | let alength = 0
170 | const prevNodes = []
171 | for (const childNode of div.childNodes) {
172 | const textValue = childNode.nodeType === Node.TEXT_NODE
173 | ? childNode.nodeValue!
174 | : childNode.firstChild!.nodeValue!
175 | alength += textValue.length
176 |
177 | if (alength <= startOffset)
178 | prevNodes.push(childNode)
179 | else if (startOffset >= alength - textValue.length && endOffset <= alength)
180 | prevNodes.push(document.createTextNode(textValue.substring(0, startOffset - (alength - textValue.length))))
181 | }
182 | nodes.unshift(...prevNodes)
183 | }
184 | }
185 | if (endOffset > 0) {
186 | nextContent = textItem.str.substring(endOffset)
187 | const node = document.createTextNode(nextContent)
188 | nodes.push(node)
189 | }
190 |
191 | div.replaceChildren(...nodes)
192 | }
193 |
194 | for (const match of matches.sort((a, b) => a.oindex - b.oindex)) {
195 | if (match.start.idx === match.end.idx) {
196 | appendHighlightDiv(match.start.idx, match.start.offset, match.end.offset)
197 | } else {
198 | for (let si = match.start.idx, ei = match.end.idx; si <= ei; si++) {
199 | if (si === match.start.idx)
200 | appendHighlightDiv(si, match.start.offset)
201 | else if (si === match.end.idx)
202 | appendHighlightDiv(si, -1, match.end.offset)
203 | else
204 | appendHighlightDiv(si)
205 | }
206 | }
207 | }
208 | }
209 |
210 | function resetDivs(textContent: TextContent, textDivs: HTMLElement[]) {
211 | const textItems = textContent.items.map(val => (val as TextItem).str)
212 | for (let idx = 0; idx < textDivs.length; idx++) {
213 | const div = textDivs[idx]
214 |
215 | if (div && div.nodeType !== Node.TEXT_NODE) {
216 | const textNode = document.createTextNode(textItems[idx])
217 | div.replaceChildren(textNode)
218 | }
219 | }
220 | }
221 |
222 | function findMatches(queries: string[], textContent: TextContent, options: HighlightOptions) {
223 | const convertedMatches = []
224 | for (const query of queries) {
225 | const matches = searchQuery(textContent, query, options)
226 | convertedMatches.push(...convertMatches(matches, textContent))
227 | }
228 | return convertedMatches
229 | }
230 |
231 | export { findMatches, highlightMatches, resetDivs }
232 |
--------------------------------------------------------------------------------
/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 HTMLElement
172 |
173 | // annotations are elements if target element are not the parentNode should be
174 | if (annotation.tagName !== 'SECTION')
175 | annotation = annotation.parentNode! 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/VuePDF.vue:
--------------------------------------------------------------------------------
1 |
2 |
341 |
342 |
343 |
344 |
345 |
351 |
357 |
358 |
359 |
360 |
361 |
366 |
367 |
368 |
--------------------------------------------------------------------------------