From be3ba98562134fab44369815ab7e81f0766e1828 Mon Sep 17 00:00:00 2001 From: Stefano <valenzano.stefano99@gmail.com> Date: Sat, 11 Jan 2025 15:24:40 +0100 Subject: [PATCH] 1.0.9 - error handling, update docs, new tests, fix types export. Update app refs when document is opened form pdf.js --- .gitignore | 5 +- docs/.vitepress/config.ts | 4 + docs/components/PdfViewer.vue | 26 ++- docs/examples/basic-examples.md | 217 ++++++++++++++++++++++ docs/index.md | 12 +- packages/vue-pdfjs-playground/src/App.vue | 43 ++++- packages/vue/README.md | 104 ++++++----- packages/vue/package.json | 2 +- packages/vue/src/components/VuePDFjs.vue | 86 ++++++--- packages/vue/src/index.ts | 20 +- packages/vue/src/scripts/utils.ts | 33 +++- packages/vue/src/types.ts | 59 +++--- packages/vue/tests/mount.test.ts | 7 +- packages/vue/tests/options.test.ts | 63 ++++++- packages/vue/tests/utils.test.ts | 48 +++++ 15 files changed, 594 insertions(+), 135 deletions(-) create mode 100644 packages/vue/tests/utils.test.ts diff --git a/.gitignore b/.gitignore index 3f3b368..12a094c 100644 --- a/.gitignore +++ b/.gitignore @@ -176,4 +176,7 @@ dist # Vitepress docs/.vitepress/cache -docs/.vitepress/dist \ No newline at end of file +docs/.vitepress/dist + +# In any tests directory ignore __screenshots__ directory +__screenshots__/ \ No newline at end of file diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index d91727c..e50cd79 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -47,6 +47,10 @@ export default defineConfig({ socialLinks: [ { icon: "github", link: "https://github.com/tuttarealstep/vue-pdf.js" }, + { + icon: "npm", + link: "https://www.npmjs.com/package/@tuttarealstep/vue-pdf.js", + }, ], }, }); diff --git a/docs/components/PdfViewer.vue b/docs/components/PdfViewer.vue index d2379d1..a2beea3 100644 --- a/docs/components/PdfViewer.vue +++ b/docs/components/PdfViewer.vue @@ -27,6 +27,15 @@ const options = reactive<NonNullable<VuePDFjsProps['options']>>({ } }) +const onErrorHandler = (error: Error) => { + console.error('Error loading PDF:', error) + alert('An error occurred with the PDF') +} + +const sourceOptions = { + onError: onErrorHandler +} + watchEffect(() => { if (options.toolbar) { options.toolbar.visible = !hideToolbar.value @@ -62,6 +71,8 @@ const onPdfAppLoaded = () => { //Set the number of pages in the ref pages.value = e.pagesCount }) + + vuepdfjs.value.pdfApp.eventBus.on('documenterror', onErrorHandler) } const pdf = 'https://raw.githubusercontent.com/mozilla/pdf.js/v4.10.38/web/compressed.tracemonkey-pldi-09.pdf' @@ -73,8 +84,12 @@ const pdf = 'https://raw.githubusercontent.com/mozilla/pdf.js/v4.10.38/web/compr <div> <input type="checkbox" v-model="hideToolbar" /> Hide Toolbar <input type="checkbox" v-model="hideSidebar" /> Hide Sidebar + <button type="button" class="custom-button"> + Load Invalid PDF + </button> </div> - <VuePDFjs ref="vuepdfjs" :source="pdf" :options="options" @pdf-app:loaded="onPdfAppLoaded" /> + <VuePDFjs ref="vuepdfjs" :source="pdf" :options="options" :source-options="sourceOptions" + @pdf-app:loaded="onPdfAppLoaded" /> </template> <style> @@ -85,4 +100,13 @@ const pdf = 'https://raw.githubusercontent.com/mozilla/pdf.js/v4.10.38/web/compr display: flex; flex-direction: column; } + +.custom-button { + padding: 0.25rem 0.75rem; + background-color: #007bff; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; +} </style> \ No newline at end of file diff --git a/docs/examples/basic-examples.md b/docs/examples/basic-examples.md index bfea340..d94ab91 100644 --- a/docs/examples/basic-examples.md +++ b/docs/examples/basic-examples.md @@ -127,3 +127,220 @@ body { } </style> ``` + +## Handle errors + +### Handle errors with the `usePDF` composable + +In this example, we handle errors in the `usePDF` composable and display a message to the user. + +```vue +<script setup lang="ts"> +import { reactive, useTemplateRef } from "vue"; +import { + VuePDFjs, + usePDF, + type VuePDFjsProps, +} from "@tuttarealstep/vue-pdf.js"; +import "@tuttarealstep/vue-pdf.js/dist/style.css"; +import enUS_FTL from "@tuttarealstep/vue-pdf.js/l10n/en-US/viewer.ftl?raw"; + +const vuepdfjs = useTemplateRef<typeof VuePDFjs>("vuepdfjs"); + +const options = reactive<NonNullable<VuePDFjsProps["options"]>>({ + locale: { + code: "en-US", + ftl: enUS_FTL, + }, +}); + +const { + pdf: document, + info, + pages, +} = usePDF("invalid-source", { + onError: (error) => { + console.error("Error loading the PDF file", error); + alert("Error loading the PDF file"); + }, +}); + +console.log(document, info, pages); +</script> + +<template> + <div id="app"> + <VuePDFjs ref="vuepdfjs" :source="document" :options="options" /> + </div> +</template> + +<style> +html, +body, +#app { + height: 100%; + width: 100%; +} + +body { + margin: 0; + padding: 0; +} +</style> +``` + +### Handle errors only with the `VuePDFjs` component + +In this example, we handle errors only with the `VuePDFjs` component and display a message to the user. + +```vue +<script setup lang="ts"> +import { reactive, useTemplateRef } from "vue"; +import { + VuePDFjs, + usePDF, + type VuePDFjsProps, +} from "@tuttarealstep/vue-pdf.js"; +import "@tuttarealstep/vue-pdf.js/dist/style.css"; +import enUS_FTL from "@tuttarealstep/vue-pdf.js/l10n/en-US/viewer.ftl?raw"; + +const vuepdfjs = useTemplateRef<typeof VuePDFjs>("vuepdfjs"); + +const options = reactive<NonNullable<VuePDFjsProps["options"]>>({ + locale: { + code: "en-US", + ftl: enUS_FTL, + }, +}); + +// Custom error handler +const onPdfAppError = (error: unknown) => { + console.error(error); + + alert("An error occurred while loading the PDF document."); +}; + +// Source options +const sourceOptions = reactive<NonNullable<VuePDFjsProps["sourceOptions"]>>({ + onError: onPdfAppError, +}); + +const source = "invalid-source"; +</script> + +<template> + <div id="app"> + <VuePDFjs + ref="vuepdfjs" + :source="source" + :options="options" + :source-options="sourceOptions" + /> + </div> +</template> + +<style> +html, +body, +#app { + height: 100%; + width: 100%; +} + +body { + margin: 0; + padding: 0; +} +</style> +``` + +### Handle pdf.js document errors + +If we try to load a PDF document with the pdf.js interface it dispach a "documenterror" event. We can listen to this event and handle the error. + +```vue +<script setup lang="ts"> +import { reactive, useTemplateRef } from "vue"; +import { + VuePDFjs, + usePDF, + type VuePDFjsProps, +} from "@tuttarealstep/vue-pdf.js"; +import "@tuttarealstep/vue-pdf.js/dist/style.css"; +import enUS_FTL from "@tuttarealstep/vue-pdf.js/l10n/en-US/viewer.ftl?raw"; + +const vuepdfjs = useTemplateRef<typeof VuePDFjs>("vuepdfjs"); + +const options = reactive<NonNullable<VuePDFjsProps["options"]>>({ + locale: { + code: "en-US", + ftl: enUS_FTL, + }, +}); + +// Custom error handler +const onPdfAppError = (error: unknown) => { + console.error(error); + + alert("An error occurred with the PDF document."); +}; + +const onPdfAppLoaded = () => { + console.log("pdf-app:loaded"); + console.log(vuepdfjs.value?.pdfApp); + + if (!vuepdfjs.value?.pdfApp) { + return; + } + + // Wait for the pages to be loaded + vuepdfjs.value.pdfApp.eventBus.on("pagesloaded", (e: any) => { + // Search for a specific text in the document + vuepdfjs.value?.pdfApp.eventBus.dispatch("find", { + query: [ + "Dynamic languages such as JavaScript are more difficult to compile than statically typed ones.", + ], + caseSensitive: false, + entireWord: false, + highlightAll: true, + }); + }); + + // Listen to the documenterror event + vuepdfjs.value.pdfApp.eventBus.on("documenterror", onPdfAppError); +}; + +const { + pdf: document, + info, + pages, +} = usePDF("compressed.tracemonkey-pldi-09.pdf"); + +console.log(document, info, pages); +</script> + +<template> + <div id="app"> + <VuePDFjs + ref="vuepdfjs" + :source="document" + :options="options" + @pdf-app:loaded="onPdfAppLoaded" + /> + </div> +</template> + +<style> +html, +body, +#app { + height: 100%; + width: 100%; +} + +body { + margin: 0; + padding: 0; +} +</style> +``` diff --git a/docs/index.md b/docs/index.md index fc5830e..b3204d4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,22 +31,22 @@ features: <style> :root { - --vp-home-hero-name-color: transparent; - --vp-home-hero-name-background: -webkit-linear-gradient(45deg, #3fb984 45%, #31475e); + --vp-home-hero-name-color: transparent !important; + --vp-home-hero-name-background: -webkit-linear-gradient(45deg, #3fb984 45%, #31475e) !important; - --vp-home-hero-image-background-image: linear-gradient(45deg, #3fb984 50%, #31475e 50%); - --vp-home-hero-image-filter: blur(44px); + --vp-home-hero-image-background-image: linear-gradient(45deg, #3fb984 50%, #31475e 50%) !important; + --vp-home-hero-image-filter: blur(44px) !important; } @media (min-width: 640px) { :root { - --vp-home-hero-image-filter: blur(56px); + --vp-home-hero-image-filter: blur(56px) !important; } } @media (min-width: 960px) { :root { - --vp-home-hero-image-filter: blur(68px); + --vp-home-hero-image-filter: blur(68px) !important; } } </style> diff --git a/packages/vue-pdfjs-playground/src/App.vue b/packages/vue-pdfjs-playground/src/App.vue index 1889b6b..4b31d5b 100644 --- a/packages/vue-pdfjs-playground/src/App.vue +++ b/packages/vue-pdfjs-playground/src/App.vue @@ -1,5 +1,5 @@ <script setup lang="ts"> -import { h, reactive, ref, useTemplateRef, watchEffect } from 'vue' +import { h, reactive, ref, useTemplateRef, watch, watchEffect } from 'vue' import { VuePDFjs, usePDF } from '@tuttarealstep/vue-pdf.js' import '@tuttarealstep/vue-pdf.js/dist/style.css' import enUS_FTL from '@tuttarealstep/vue-pdf.js/l10n/en-US/viewer.ftl?raw' @@ -7,7 +7,7 @@ import type { VuePDFjsProps } from '../../vue/dist/src/components/VuePDFjs.vue'; const pdf = new URL('./assets/compressed.tracemonkey-pldi-09.pdf', import.meta.url); -const vuepdfjs = useTemplateRef<typeof VuePDFjs>('vuepdfjs') +const vuepdfjs = useTemplateRef<InstanceType<typeof VuePDFjs>>('vuepdfjs') const hideToolbar = ref(false) const hideSidebar = ref(false) @@ -21,8 +21,8 @@ const options = reactive<NonNullable<VuePDFjsProps['options']>>({ visible: true, options: { sidebarToggle: false, - secondaryOpenFile: false, - /*secondaryDownload: false, + /*secondaryOpenFile: false, + secondaryDownload: false, secondaryPrint: false, scaleSelect: false, print: false, @@ -31,9 +31,10 @@ const options = reactive<NonNullable<VuePDFjsProps['options']>>({ }, sidebar: { visible: true - } + }, }) + watchEffect(() => { if (options.toolbar) { options.toolbar.visible = !hideToolbar.value @@ -48,6 +49,12 @@ watchEffect(() => { options.sidebar.visible = !hideSidebar.value }) +const onPdfAppError = (error: unknown) => { + console.error(error) + + alert("An error occurred whit the PDF document.") +} + const onPdfAppLoaded = () => { console.log('pdf-app:loaded') console.log(vuepdfjs.value?.pdfApp) @@ -64,20 +71,40 @@ const onPdfAppLoaded = () => { highlightAll: true }) }) + + vuepdfjs.value.pdfApp.eventBus.on('documenterror', onPdfAppError) } -const { pdf: document, info, pages } = usePDF(pdf) +const sourceOptions = reactive<NonNullable<VuePDFjsProps['sourceOptions']>>({ + onError: onPdfAppError +}) + +const { pdf: document, info, pages } = usePDF(pdf, { + onError: onPdfAppError +}) + +const source = ref<any>() + +watch(document, (value) => { + source.value = value +}) -console.log(document, info, pages) </script> <template> <div> <input type="checkbox" v-model="hideToolbar" /> Hide Toolbar <input type="checkbox" v-model="hideSidebar" /> Hide Sidebar + <button type="button" class="custom-button" @click="source = `invalid${new Date().getTime()}.pdf`"> + Load Invalid PDF + </button> + </div> + <div> + We have {{ vuepdfjs?.pdfPages }} pages in this document. </div> <div id="playground"> - <VuePDFjs ref="vuepdfjs" :source="document" :options="options" @pdf-app:loaded="onPdfAppLoaded" /> + <VuePDFjs ref="vuepdfjs" :source :options="options" :source-options="sourceOptions" + @pdf-app:loaded="onPdfAppLoaded" /> </div> </template> diff --git a/packages/vue/README.md b/packages/vue/README.md index 242ae49..fb078c1 100644 --- a/packages/vue/README.md +++ b/packages/vue/README.md @@ -1,4 +1,4 @@ -# vue-pdfjs +# vue-pdfjs [](https://www.npmjs.com/package/@tuttarealstep/vue-pdf.js) `vue-pdfjs` is a Vue 3 component for displaying PDF files using the standard `pdf.js` viewer. This package provides a simple and powerful integration to embed PDF viewers in Vue applications. @@ -23,44 +23,51 @@ npm install @tuttarealstep/vue-pdf.js ```vue <script setup lang="ts"> -import { reactive, useTemplateRef } from 'vue' -import { VuePDFjs } from '@tuttarealstep/vue-pdf.js' -import '@tuttarealstep/vue-pdf.js/dist/style.css' -import enUS_FTL from '@tuttarealstep/vue-pdf.js/l10n/en-US/viewer.ftl?raw' +import { reactive, useTemplateRef } from "vue"; +import { VuePDFjs } from "@tuttarealstep/vue-pdf.js"; +import "@tuttarealstep/vue-pdf.js/dist/style.css"; +import enUS_FTL from "@tuttarealstep/vue-pdf.js/l10n/en-US/viewer.ftl?raw"; -const pdf = new URL('./path/to/custom.pdf', import.meta.url).href +const pdf = new URL("./path/to/custom.pdf", import.meta.url).href; -const vuepdfjs = useTemplateRef<typeof VuePDFjs>('vuepdfjs') +const vuepdfjs = useTemplateRef<typeof VuePDFjs>("vuepdfjs"); const options = reactive({ locale: { - code: 'en-US', - ftl: enUS_FTL - } -}) + code: "en-US", + ftl: enUS_FTL, + }, +}); const onPdfAppLoaded = () => { - console.log('pdf-app:loaded') - console.log(vuepdfjs.value?.pdfApp) + console.log("pdf-app:loaded"); + console.log(vuepdfjs.value?.pdfApp); if (!vuepdfjs.value?.pdfApp) { - return + return; } - vuepdfjs.value.pdfApp.eventBus.on('pagesloaded', (e: any) => { - vuepdfjs.value?.pdfApp.eventBus.dispatch('find', { - query: ['Dynamic languages such as JavaScript are more difficult to compile than statically typed ones.'], + vuepdfjs.value.pdfApp.eventBus.on("pagesloaded", (e: any) => { + vuepdfjs.value?.pdfApp.eventBus.dispatch("find", { + query: [ + "Dynamic languages such as JavaScript are more difficult to compile than statically typed ones.", + ], caseSensitive: false, entireWord: false, - highlightAll: true - }) - }) -} + highlightAll: true, + }); + }); +}; </script> <template> <div id="app"> - <VuePDFjs ref="vuepdfjs" :source="pdf" :options="options" @pdf-app:loaded="onPdfAppLoaded" /> + <VuePDFjs + ref="vuepdfjs" + :source="pdf" + :options="options" + @pdf-app:loaded="onPdfAppLoaded" + /> </div> </template> @@ -83,46 +90,55 @@ body { ```vue <script setup lang="ts"> -import { reactive, useTemplateRef } from 'vue' -import { VuePDFjs, usePDF } from '@tuttarealstep/vue-pdf.js' -import '@tuttarealstep/vue-pdf.js/dist/style.css' -import enUS_FTL from '@tuttarealstep/vue-pdf.js/l10n/en-US/viewer.ftl?raw' +import { reactive, useTemplateRef } from "vue"; +import { VuePDFjs, usePDF } from "@tuttarealstep/vue-pdf.js"; +import "@tuttarealstep/vue-pdf.js/dist/style.css"; +import enUS_FTL from "@tuttarealstep/vue-pdf.js/l10n/en-US/viewer.ftl?raw"; -const vuepdfjs = useTemplateRef<typeof VuePDFjs>('vuepdfjs') +const vuepdfjs = useTemplateRef<typeof VuePDFjs>("vuepdfjs"); const options = reactive({ locale: { - code: 'en-US', - ftl: enUS_FTL - } -}) + code: "en-US", + ftl: enUS_FTL, + }, +}); const onPdfAppLoaded = () => { - console.log('pdf-app:loaded') - console.log(vuepdfjs.value?.pdfApp) + console.log("pdf-app:loaded"); + console.log(vuepdfjs.value?.pdfApp); if (!vuepdfjs.value?.pdfApp) { - return + return; } - vuepdfjs.value.pdfApp.eventBus.on('pagesloaded', (e: any) => { - vuepdfjs.value?.pdfApp.eventBus.dispatch('find', { - query: ['Dynamic languages such as JavaScript are more difficult to compile than statically typed ones.'], + vuepdfjs.value.pdfApp.eventBus.on("pagesloaded", (e: any) => { + vuepdfjs.value?.pdfApp.eventBus.dispatch("find", { + query: [ + "Dynamic languages such as JavaScript are more difficult to compile than statically typed ones.", + ], caseSensitive: false, entireWord: false, - highlightAll: true - }) - }) -} + highlightAll: true, + }); + }); +}; -const { pdf, info, pages} = usePDF(new URL('./path/to/custom.pdf', import.meta.url).href) // or any other source type +const { pdf, info, pages } = usePDF( + new URL("./path/to/custom.pdf", import.meta.url).href +); // or any other source type -console.log(document, info, pages) +console.log(document, info, pages); </script> <template> <div id="app"> - <VuePDFjs ref="vuepdfjs" :source="pdf" :options="options" @pdf-app:loaded="onPdfAppLoaded" /> + <VuePDFjs + ref="vuepdfjs" + :source="pdf" + :options="options" + @pdf-app:loaded="onPdfAppLoaded" + /> </div> </template> diff --git a/packages/vue/package.json b/packages/vue/package.json index 74a0680..af159df 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,7 +1,7 @@ { "name": "@tuttarealstep/vue-pdf.js", "description": "A Vue component for displaying PDF files using the standard `pdf.js` viewer. This package provides a simple and powerful integration to embed PDF viewers in Vue applications.", - "version": "1.0.8", + "version": "1.0.9", "private": false, "type": "module", "author": "Stefano Valenzano (https://github.com/tuttarealstep)", diff --git a/packages/vue/src/components/VuePDFjs.vue b/packages/vue/src/components/VuePDFjs.vue index 68ca19e..11b06ff 100644 --- a/packages/vue/src/components/VuePDFjs.vue +++ b/packages/vue/src/components/VuePDFjs.vue @@ -1,7 +1,7 @@ <script setup lang="ts"> import '../assets/scss/main.scss' -import { onMounted, onUnmounted, ref, shallowRef, watch, provide } from 'vue'; +import { onMounted, onUnmounted, ref, shallowRef, provide, watch } from 'vue'; import { initViewer, PDFViewerApplicationOptions } from '../scripts/viewer'; import OuterContainer from './OuterContainer.vue'; @@ -49,6 +49,24 @@ const pdfInfo = shallowRef<PDFInfo | {}>({}) provide(toolbarOptionsKey, props.options?.toolbar) provide(sidebarOptionsKey, props.options?.sidebar) +async function loadDocumentInfo(document: PDFDocumentProxy) { + pdfDocument.value = document; + pdfLoadingTask.value = document.loadingTask; + pdfPages.value = document.numPages; + + const metadata = await document.getMetadata() + const attachments = (await document.getAttachments()) as Record<string, unknown> + const javascript = await document.getJSActions() + const outline = await document.getOutline() + + pdfInfo.value = { + metadata, + attachments, + javascript, + outline, + } +} + async function init() { if (!container.value) { throw new Error('Container not found'); @@ -75,9 +93,21 @@ async function init() { pdfApp.value = await initViewer(container.value); await pdfApp.value.initializedPromise; + pdfApp.value.eventBus.on('documentloaded', ( + event: { + source: PDFViewerApplication + } + ) => { + loadDocumentInfo(event.source.pdfDocument); + }) + emit('pdf-app:loaded'); - } catch (error) { + } catch (error: unknown) { console.error(error); + + if (props.sourceOptions?.onError) { + props.sourceOptions.onError(error); + } } isLoading.value = false; @@ -90,6 +120,7 @@ function clearCacheTimeout() { clearTimeout(cacheTimeoutId) } + async function initDocument(document: PDFDocumentProxy | null) { if (!document || !pdfApp.value) { return; @@ -97,35 +128,35 @@ async function initDocument(document: PDFDocumentProxy | null) { clearCacheTimeout() - pdfDocument.value = document; - pdfLoadingTask.value = document.loadingTask; - pdfPages.value = document.numPages; - - const metadata = await document.getMetadata() - const attachments = (await document.getAttachments()) as Record<string, unknown> - const javascript = await document.getJSActions() - const outline = await document.getOutline() - - pdfInfo.value = { - metadata, - attachments, - javascript, - outline, - } + await loadDocumentInfo(document); pdfApp.value?.load(document); } async function openSource(source: PDFSource | PDFSourceWithOptions | PDFDocumentProxy) { - if (source !== undefined && source !== null) { - if (source instanceof PDFDocumentProxy) { - initDocument(source); - } else { - initDocument(await processSource(source, props.sourceOptions)); + try { + if (source !== undefined && source !== null) { + if (source instanceof PDFDocumentProxy) { + initDocument(source); + } else { + initDocument(await processSource(source, { + ...props.sourceOptions, + onError: (error) => { + if (props.sourceOptions?.onError) { + props.sourceOptions.onError(error); + } else { + console.error(error); + } + } + })); + } } - } else { - if (pdfApp.value) { - await pdfApp.value.close(); + } + catch (error) { + if (props.sourceOptions?.onError) { + props.sourceOptions.onError(error); + } else { + console.error(error); } } } @@ -141,8 +172,9 @@ onMounted(async () => { await openSource(props.source); }) -onUnmounted(() => { - pdfDocument.value?.destroy(); +onUnmounted(async () => { + if (pdfApp.value) + await pdfApp.value.close(); }) defineExpose({ diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index dc40e23..ff073fa 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -1,12 +1,14 @@ -import type { Plugin } from 'vue'; -import VuePDFjs from './components/VuePDFjs.vue'; -import usePDF from './composable/usePDF'; +import type { Plugin } from 'vue' +import VuePDFjs, { type VuePDFjsProps } from './components/VuePDFjs.vue' +import usePDF from './composable/usePDF' +export * from './types' export const VuePDFjsPlugin: Plugin = { - install(app) { - app.component('VuePDFjs', VuePDFjs); - } -}; + install(app) { + app.component('VuePDFjs', VuePDFjs) + } +} -export { VuePDFjs, usePDF }; -export default VuePDFjsPlugin; \ No newline at end of file +export type { VuePDFjsProps } +export { VuePDFjs, usePDF } +export default VuePDFjsPlugin diff --git a/packages/vue/src/scripts/utils.ts b/packages/vue/src/scripts/utils.ts index 3bb95be..f42b736 100644 --- a/packages/vue/src/scripts/utils.ts +++ b/packages/vue/src/scripts/utils.ts @@ -8,7 +8,12 @@ import { UpdatePasswordFn } from '../types' -const getByteArray = function (file: File) { +/** + * Converts a File object to a Uint8Array. + * @param {File} file - The file to convert. + * @returns {Promise<Uint8Array>} A promise that resolves to a Uint8Array. + */ +const getByteArray = function (file: File): Promise<Uint8Array> { const fileReader = new FileReader() return new Promise(function (resolve: (value: Uint8Array) => void, reject) { fileReader.readAsArrayBuffer(file) @@ -24,6 +29,12 @@ const getByteArray = function (file: File) { }) } +/** + * Parses a PDF source file and returns a PDFDocumentLoadingTask. + * @param {PDFSource} source - The source of the PDF file. + * @returns {Promise<PDFDocumentLoadingTask>} A promise that resolves to a PDFDocumentLoadingTask. + * @throws {Error} If the source is invalid. + */ export async function parseSourceFile(source: PDFSource): Promise<PDFDocumentLoadingTask> { if (source == null || source === undefined) { throw new Error('Invalid parameter') @@ -66,6 +77,12 @@ export async function parseSourceFile(source: PDFSource): Promise<PDFDocumentLoa return getDocument(source!) } +/** + * Processes a PDF source and returns a PDFDocumentProxy. + * @param {PDFSource | PDFSourceWithOptions} inputSource - The input source of the PDF file. + * @param {PDFSourceOptions} [sourceOptions] - Optional source options. + * @returns {Promise<PDFDocumentProxy | null>} A promise that resolves to a PDFDocumentProxy or null. + */ export async function processSource( inputSource: PDFSource | PDFSourceWithOptions, sourceOptions?: PDFSourceOptions @@ -94,9 +111,15 @@ export async function processSource( const loadingTask = await parseSourceFile(source).catch((error) => { if (options?.onError) { options.onError(error) + } else { + console.error(error) } }) + if (!loadingTask) { + return null + } + if (options?.onProgress) { loadingTask.onProgress = options.onProgress } @@ -110,5 +133,11 @@ export async function processSource( loadingTask.onPassword = onPassword } - return loadingTask.promise + return loadingTask.promise.catch((error: any) => { + if (options?.onError) { + options.onError(error) + } else { + console.error(error) + } + }) } diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index 711ff70..1d5a450 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -1,8 +1,13 @@ +import { + TypedArray, + DocumentInitParameters, + PDFDocumentLoadingTask, + OnProgressParameters + //@ts-ignore +} from 'pdf.js/src/display/api' + //@ts-ignore -import { TypedArray, DocumentInitParameters, PDFDocumentLoadingTask, OnProgressParameters, PDFDocumentProxy } from "pdf.js/src/display/api"; -//@ts-ignore -import { Metadata } from "pdf.js/src/display/metadata"; -import { ToolbarContainerProps } from "./components/ToolbarContainer.vue"; +import { Metadata } from 'pdf.js/src/display/metadata' export type OnProgressCallback = (progressData: OnProgressParameters) => void export type UpdatePasswordFn = (newPassword: string) => void @@ -10,37 +15,37 @@ export type OnPasswordCallback = (updatePassword: UpdatePasswordFn, reason: any) export type OnErrorCallback = (error: any) => void export interface PDFSourceOptions { - onProgress?: OnProgressCallback - onPassword?: OnPasswordCallback - onError?: OnErrorCallback - password?: string + onProgress?: OnProgressCallback + onPassword?: OnPasswordCallback + onError?: OnErrorCallback + password?: string } export type PDFSource = - | string - | URL - | TypedArray - | ArrayBuffer - | DocumentInitParameters //End of standard types - | File - | Blob - | PDFDocumentLoadingTask - | undefined - | null + | string + | URL + | TypedArray + | ArrayBuffer + | DocumentInitParameters //End of standard types + | File + | Blob + | PDFDocumentLoadingTask + | undefined + | null export interface PDFSourceWithOptions { - source?: PDFSource - options?: PDFSourceOptions + source?: PDFSource + options?: PDFSourceOptions } export interface PDFInfoMetadata { - info: any - metadata: Metadata + info: any + metadata: Metadata } export interface PDFInfo { - metadata: PDFInfoMetadata - attachments: Record<string, unknown> - javascript: string[] | null - outline: any -} \ No newline at end of file + metadata: PDFInfoMetadata + attachments: Record<string, unknown> + javascript: string[] | null + outline: any +} diff --git a/packages/vue/tests/mount.test.ts b/packages/vue/tests/mount.test.ts index a2602a2..bfd0c39 100644 --- a/packages/vue/tests/mount.test.ts +++ b/packages/vue/tests/mount.test.ts @@ -9,9 +9,7 @@ const { pdf: pdfDocument, info: pdfInfo, pages: pdfPages -} = usePDF( - 'https://raw.githubusercontent.com/mozilla/pdf.js/v4.10.38/web/compressed.tracemonkey-pldi-09.pdf' -) +} = usePDF(new URL('../../vue-pdfjs-playground/src/assets/compressed.tracemonkey-pldi-09.pdf', import.meta.url)) beforeAll(async () => { await vi.waitUntil(() => pdfDocument.value, { timeout: 5000 }) @@ -32,8 +30,7 @@ test('Render component', () => { ftl: enUS_FTL } } - }, - + } }) //Expect the pdfInfo.value.metadata to be an object diff --git a/packages/vue/tests/options.test.ts b/packages/vue/tests/options.test.ts index 2ae4277..78533e2 100644 --- a/packages/vue/tests/options.test.ts +++ b/packages/vue/tests/options.test.ts @@ -1,17 +1,19 @@ import { beforeAll, expect, test, vi } from 'vitest' import { render } from 'vitest-browser-vue' -import { VuePDFjs, usePDF } from '@tuttarealstep/vue-pdf.js' +import { VuePDFjs, usePDF, type VuePDFjsProps } from '@tuttarealstep/vue-pdf.js' import '@tuttarealstep/vue-pdf.js/dist/style.css' // @ts-ignore import enUS_FTL from '../../../node_modules/pdf.js/l10n/en-US/viewer.ftl?raw' -import { ref } from 'vue' const { pdf: pdfDocument, - info: pdfInfo, + //info: pdfInfo, pages: pdfPages } = usePDF( - 'https://raw.githubusercontent.com/mozilla/pdf.js/v4.10.38/web/compressed.tracemonkey-pldi-09.pdf' + new URL( + '../../vue-pdfjs-playground/src/assets/compressed.tracemonkey-pldi-09.pdf', + import.meta.url + ) ) beforeAll(async () => { @@ -55,3 +57,56 @@ test('Test enable/disable options', () => { // Expect the "#editorStamp" display to not be "none" expect(document.getElementById('editorStamp')?.style.display).toBe('none') }) + +test('Catch error when source is invalid - usePDF composable', async () => { + const invalidSource = 'invalid-source' + + const onErrorCallback = vi.fn((error: unknown) => { + console.error(error) + }) + + const { pdf, info, pages } = usePDF(invalidSource, { + onError: onErrorCallback + }) + + expect(pdf.value).toBe(undefined) + expect(info.value).toBe(undefined) + expect(pages.value).toBe(0) + + await new Promise((resolve) => setTimeout(resolve, 100)) + await vi.waitFor(() => onErrorCallback.mock.calls.length > 0) + + expect(onErrorCallback).toHaveBeenCalled() +}) + +test('Catch error when source is invalid - without composable', async () => { + //Add a default css to make body > div height 100vh + const style = document.createElement('style') + style.innerHTML = 'body > div { height: 100vh }' + document.head.appendChild(style) + + const onErrorCallback = vi.fn((error: unknown) => { + console.error(error) + }) + + const props = { + source: 'invalid-source', + options: { + locale: { + code: 'en-US', + ftl: enUS_FTL + } + }, + sourceOptions: { + onError: onErrorCallback + } + } as VuePDFjsProps + + render(VuePDFjs, { + props + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(onErrorCallback).toHaveBeenCalled() +}) diff --git a/packages/vue/tests/utils.test.ts b/packages/vue/tests/utils.test.ts new file mode 100644 index 0000000..a3e33d4 --- /dev/null +++ b/packages/vue/tests/utils.test.ts @@ -0,0 +1,48 @@ +import { parseSourceFile, processSource } from '../src/scripts/utils' +import { describe, it, expect, vi } from 'vitest' +//@ts-expect-error +import { PDFDocumentLoadingTask, PDFDocumentProxy } from 'pdf.js/src/display/api' +import { PDFJSWorker } from '../src/scripts/viewer' + +//@ts-expect-error +import * as PDFJS from 'pdf.js/src/pdf.js' + +PDFJS.GlobalWorkerOptions.workerPort = new PDFJSWorker() + +describe('utils.ts', () => { + describe('parseSourceFile', () => { + it('should throw an error for null source', async () => { + await expect(parseSourceFile(null)).rejects.toThrow('Invalid parameter') + }) + + it('should return the same PDFDocumentLoadingTask if source is a File', async () => { + const file = new File([''], 'test.pdf') + const result = await parseSourceFile(file) + expect(result).toBeInstanceOf(PDFDocumentLoadingTask) + }) + + it('should return the same PDFDocumentLoadingTask if source is a Blob', async () => { + const blob = new Blob([''], { type: 'application/pdf' }) + const result = await parseSourceFile(blob) + expect(result).toBeInstanceOf(PDFDocumentLoadingTask) + }) + + it('should return the same PDFDocumentLoadingTask if source is a string', async () => { + const result = await parseSourceFile('https://example.com/test.pdf') + expect(result).toBeInstanceOf(PDFDocumentLoadingTask) + }) + + it('should return the same PDFDocumentLoadingTask if source is already a PDFDocumentLoadingTask', async () => { + const loadingTask = new PDFDocumentLoadingTask() + const result = await parseSourceFile(loadingTask) + expect(result).toBe(loadingTask) + }) + }) + + describe('processSource', () => { + it('should return null for null inputSource', async () => { + const result = await processSource(null) + expect(result).toBeNull() + }) + }) +})