cbvirtua 1.0.39 → 1.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,462 @@
1
+ <template>
2
+ <div :id="id" class="vue-pdf-embed">
3
+ <div v-for="pageNum in pageNums" :key="pageNum">
4
+ <slot name="before-page" :page="pageNum" />
5
+
6
+ <div :id="id && `${id}-${pageNum}`" class="vue-pdf-embed__page">
7
+ <canvas />
8
+
9
+ <div v-if="textLayer" class="textLayer" />
10
+
11
+ <div v-if="annotationLayer" class="annotationLayer" />
12
+ </div>
13
+
14
+ <slot name="after-page" :page="pageNum" />
15
+ </div>
16
+ </div>
17
+ </template>
18
+
19
+ <script>
20
+ import * as pdf from 'pdfjs-dist/legacy/build/pdf.js'
21
+ import PdfWorker from 'pdfjs-dist/legacy/build/pdf.worker.js'
22
+ import { PDFLinkService } from 'pdfjs-dist/legacy/web/pdf_viewer.js'
23
+ import {
24
+ addPrintStyles,
25
+ createPrintIframe,
26
+ downloadPdf,
27
+ emptyElement,
28
+ releaseChildCanvases,
29
+ } from './util.js'
30
+
31
+ pdf.GlobalWorkerOptions.workerPort = new PdfWorker()
32
+
33
+ export default {
34
+ name: 'VuePdfEmbed',
35
+ props: {
36
+ /**
37
+ * Whether the annotation layer should be enabled.
38
+ * @values Boolean
39
+ */
40
+ annotationLayer: Boolean,
41
+ /**
42
+ * Desired page height.
43
+ * @values Number, String
44
+ */
45
+ height: [Number, String],
46
+ /**
47
+ * Component identifier (inherited by page containers with page number
48
+ * postfixes).
49
+ * @values String
50
+ */
51
+ id: String,
52
+ /**
53
+ * Path for annotation icons, including trailing slash.
54
+ * @values String
55
+ */
56
+ imageResourcesPath: String,
57
+ /**
58
+ * Number of the page to display.
59
+ * @values Number
60
+ */
61
+ page: Number,
62
+ /**
63
+ * Desired page rotation angle.
64
+ * @values Number, String
65
+ */
66
+ rotation: {
67
+ type: [Number, String],
68
+ default: 0,
69
+ validator(value) {
70
+ if (value % 90 !== 0) {
71
+ throw new Error('Rotation must be 0 or a multiple of 90.')
72
+ }
73
+ return true
74
+ },
75
+ },
76
+ /**
77
+ * Desired ratio of canvas size to document size.
78
+ * @values Number
79
+ */
80
+ scale: Number,
81
+ /**
82
+ * Source of the document to display.
83
+ * @values Object, String, URL, TypedArray
84
+ */
85
+ source: {
86
+ type: [Object, String, URL, Uint8Array],
87
+ required: true,
88
+ },
89
+ /**
90
+ * Whether the text layer should be enabled.
91
+ * @values Boolean
92
+ */
93
+ textLayer: Boolean,
94
+ /**
95
+ * Desired page width.
96
+ * @values Number, String
97
+ */
98
+ width: [Number, String],
99
+ },
100
+ data() {
101
+ return {
102
+ documentLoadingTask: null,
103
+ document: null,
104
+ pageCount: null,
105
+ pageNums: [],
106
+ }
107
+ },
108
+ computed: {
109
+ linkService() {
110
+ if (!this.document || !this.annotationLayer) {
111
+ return null
112
+ }
113
+
114
+ const service = new PDFLinkService()
115
+ service.setDocument(this.document)
116
+ service.setViewer({
117
+ scrollPageIntoView: ({ pageNumber }) => {
118
+ this.$emit('internal-link-clicked', pageNumber)
119
+ },
120
+ })
121
+ return service
122
+ },
123
+ },
124
+ created() {
125
+ this.$watch(
126
+ () => [
127
+ this.source,
128
+ this.annotationLayer,
129
+ this.height,
130
+ this.page,
131
+ this.rotation,
132
+ this.textLayer,
133
+ this.width,
134
+ ],
135
+ async ([newSource], [oldSource]) => {
136
+ if (newSource !== oldSource) {
137
+ releaseChildCanvases(this.$el)
138
+ await this.load()
139
+ }
140
+ this.render()
141
+ }
142
+ )
143
+ },
144
+ async mounted() {
145
+ await this.load()
146
+ this.render()
147
+ },
148
+ beforeDestroy() {
149
+ releaseChildCanvases(this.$el)
150
+ if (this.documentLoadingTask?.onPassword) {
151
+ this.documentLoadingTask.onPassword = null
152
+ }
153
+ if (this.documentLoadingTask?.onProgress) {
154
+ this.documentLoadingTask.onProgress = null
155
+ }
156
+ this.document?.destroy()
157
+ },
158
+ beforeUnmount() {
159
+ releaseChildCanvases(this.$el)
160
+ if (this.documentLoadingTask?.onPassword) {
161
+ this.documentLoadingTask.onPassword = null
162
+ }
163
+ if (this.documentLoadingTask?.onProgress) {
164
+ this.documentLoadingTask.onProgress = null
165
+ }
166
+ this.document?.destroy()
167
+ },
168
+ methods: {
169
+ /**
170
+ * Downloads a PDF document.
171
+ *
172
+ * NOTE: Ignored if the document is not loaded.
173
+ *
174
+ * @param {string} filename - Predefined filename to save.
175
+ */
176
+ async download(filename) {
177
+ if (!this.document) {
178
+ return
179
+ }
180
+
181
+ const data = await this.document.getData()
182
+ const metadata = await this.document.getMetadata()
183
+ downloadPdf(data, filename ?? metadata.contentDispositionFilename ?? '')
184
+ },
185
+ /**
186
+ * Returns an array of the actual page width and height based on props and
187
+ * aspect ratio.
188
+ * @param {number} ratio - Page aspect ratio.
189
+ */
190
+ getPageDimensions(ratio) {
191
+ let width, height
192
+
193
+ if (this.height && !this.width) {
194
+ height = this.height
195
+ width = height / ratio
196
+ } else {
197
+ width = this.width || this.$el.clientWidth
198
+ height = width * ratio
199
+ }
200
+
201
+ return [width, height]
202
+ },
203
+ /**
204
+ * Loads a PDF document. Defines a password callback for protected
205
+ * documents.
206
+ *
207
+ * NOTE: Ignored if source property is not provided.
208
+ */
209
+ async load() {
210
+ if (!this.source) {
211
+ return
212
+ }
213
+
214
+ try {
215
+ if (this.source._pdfInfo) {
216
+ this.document = this.source
217
+ } else {
218
+ this.documentLoadingTask = pdf.getDocument(this.source)
219
+ this.documentLoadingTask.onProgress = (progressParams) => {
220
+ this.$emit('progress', progressParams)
221
+ }
222
+ this.documentLoadingTask.onPassword = (callback, reason) => {
223
+ const retry = reason === pdf.PasswordResponses.INCORRECT_PASSWORD
224
+ this.$emit('password-requested', callback, retry)
225
+ }
226
+ this.document = await this.documentLoadingTask.promise
227
+ }
228
+ this.pageCount = this.document.numPages
229
+ this.$emit('loaded', this.document)
230
+ } catch (e) {
231
+ this.document = null
232
+ this.pageCount = null
233
+ this.pageNums = []
234
+ this.$emit('loading-failed', e)
235
+ }
236
+ },
237
+ /**
238
+ * Prints a PDF document via the browser interface.
239
+ *
240
+ * NOTE: Ignored if the document is not loaded.
241
+ *
242
+ * @param {number} dpi - Print resolution.
243
+ * @param {string} filename - Predefined filename to save.
244
+ * @param {boolean} allPages - Ignore page prop to print all pages.
245
+ */
246
+ async print(dpi = 300, filename = '', allPages = false) {
247
+ if (!this.document) {
248
+ return
249
+ }
250
+
251
+ const printUnits = dpi / 72
252
+ const styleUnits = 96 / 72
253
+ let container, iframe, title
254
+
255
+ try {
256
+ container = document.createElement('div')
257
+ container.style.display = 'none'
258
+ window.document.body.appendChild(container)
259
+ iframe = await createPrintIframe(container)
260
+
261
+ const pageNums =
262
+ this.page && !allPages
263
+ ? [this.page]
264
+ : [...Array(this.document.numPages + 1).keys()].slice(1)
265
+
266
+ await Promise.all(
267
+ pageNums.map(async (pageNum, i) => {
268
+ const page = await this.document.getPage(pageNum)
269
+ const viewport = page.getViewport({
270
+ scale: 1,
271
+ rotation: 0,
272
+ })
273
+
274
+ if (i === 0) {
275
+ const sizeX = (viewport.width * printUnits) / styleUnits
276
+ const sizeY = (viewport.height * printUnits) / styleUnits
277
+ addPrintStyles(iframe, sizeX, sizeY)
278
+ }
279
+
280
+ const canvas = document.createElement('canvas')
281
+ canvas.width = viewport.width * printUnits
282
+ canvas.height = viewport.height * printUnits
283
+ container.appendChild(canvas)
284
+ const canvasClone = canvas.cloneNode()
285
+ iframe.contentWindow.document.body.appendChild(canvasClone)
286
+
287
+ await page.render({
288
+ canvasContext: canvas.getContext('2d'),
289
+ intent: 'print',
290
+ transform: [printUnits, 0, 0, printUnits, 0, 0],
291
+ viewport,
292
+ }).promise
293
+
294
+ canvasClone.getContext('2d').drawImage(canvas, 0, 0)
295
+ })
296
+ )
297
+
298
+ if (filename) {
299
+ title = window.document.title
300
+ window.document.title = filename
301
+ }
302
+
303
+ iframe.contentWindow.focus()
304
+ iframe.contentWindow.print()
305
+ } finally {
306
+ if (title) {
307
+ window.document.title = title
308
+ }
309
+
310
+ releaseChildCanvases(container)
311
+ container.parentNode?.removeChild(container)
312
+ }
313
+ },
314
+ /**
315
+ * Renders the PDF document as canvas element(s) and additional layers.
316
+ *
317
+ * NOTE: Ignored if the document is not loaded.
318
+ */
319
+ async render() {
320
+ if (!this.document) {
321
+ return
322
+ }
323
+
324
+ try {
325
+ this.pageNums = this.page
326
+ ? [this.page]
327
+ : [...Array(this.document.numPages + 1).keys()].slice(1)
328
+
329
+ const pageElements = this.$el.getElementsByClassName(
330
+ 'vue-pdf-embed__page'
331
+ )
332
+
333
+ await Promise.all(
334
+ this.pageNums.map(async (pageNum, i) => {
335
+ const page = await this.document.getPage(pageNum)
336
+ const pageRotation = this.rotation + page.rotate
337
+ const [canvas, div1, div2] = pageElements[i].children
338
+ const [actualWidth, actualHeight] = this.getPageDimensions(
339
+ (pageRotation / 90) % 2
340
+ ? page.view[2] / page.view[3]
341
+ : page.view[3] / page.view[2]
342
+ )
343
+
344
+ canvas.style.width = `${Math.floor(actualWidth)}px`
345
+ canvas.style.height = `${Math.floor(actualHeight)}px`
346
+
347
+ await this.renderPage(page, canvas, actualWidth, pageRotation)
348
+
349
+ if (this.textLayer) {
350
+ await this.renderPageTextLayer(
351
+ page,
352
+ div1,
353
+ actualWidth,
354
+ pageRotation
355
+ )
356
+ }
357
+
358
+ if (this.annotationLayer) {
359
+ await this.renderPageAnnotationLayer(
360
+ page,
361
+ div2 || div1,
362
+ actualWidth,
363
+ pageRotation
364
+ )
365
+ }
366
+ })
367
+ )
368
+
369
+ this.$emit('rendered')
370
+ } catch (e) {
371
+ this.document = null
372
+ this.pageCount = null
373
+ this.pageNums = []
374
+ this.$emit('rendering-failed', e)
375
+ }
376
+ },
377
+ /**
378
+ * Renders the page content.
379
+ * @param {PDFPageProxy} page - Page proxy.
380
+ * @param {HTMLCanvasElement} canvas - HTML canvas.
381
+ * @param {number} width - Actual page width.
382
+ * @param {number} rotation - Total page rotation.
383
+ */
384
+ async renderPage(page, canvas, width, rotation) {
385
+ const pageWidth = (rotation / 90) % 2 ? page.view[3] : page.view[2]
386
+ const viewport = page.getViewport({
387
+ scale: this.scale ?? Math.ceil(width / pageWidth) + 1,
388
+ rotation,
389
+ })
390
+
391
+ canvas.width = viewport.width
392
+ canvas.height = viewport.height
393
+
394
+ await page.render({
395
+ canvasContext: canvas.getContext('2d'),
396
+ viewport,
397
+ }).promise
398
+ },
399
+ /**
400
+ * Renders the annotation layer for the specified page.
401
+ * @param {PDFPageProxy} page - Page proxy.
402
+ * @param {HTMLElement} container - HTML container.
403
+ * @param {number} width - Actual page width.
404
+ * @param {number} rotation - Total page rotation.
405
+ */
406
+ async renderPageAnnotationLayer(page, container, width, rotation) {
407
+ emptyElement(container)
408
+ const pageWidth = (rotation / 90) % 2 ? page.view[3] : page.view[2]
409
+ pdf.AnnotationLayer.render({
410
+ annotations: await page.getAnnotations(),
411
+ div: container,
412
+ linkService: this.linkService,
413
+ page,
414
+ renderInteractiveForms: false,
415
+ viewport: page
416
+ .getViewport({
417
+ scale: width / pageWidth,
418
+ rotation,
419
+ })
420
+ .clone({
421
+ dontFlip: true,
422
+ }),
423
+ imageResourcesPath: this.imageResourcesPath,
424
+ })
425
+ },
426
+ /**
427
+ * Renders the text layer for the specified page.
428
+ * @param {PDFPageProxy} page - Page proxy.
429
+ * @param {HTMLElement} container - HTML container.
430
+ * @param {number} width - Actual page width.
431
+ * @param {number} rotation - Total page rotation.
432
+ */
433
+ async renderPageTextLayer(page, container, width, rotation) {
434
+ emptyElement(container)
435
+ const pageWidth = (rotation / 90) % 2 ? page.view[3] : page.view[2]
436
+ await pdf.renderTextLayer({
437
+ container,
438
+ textContent: await page.getTextContent(),
439
+ viewport: page.getViewport({
440
+ scale: width / pageWidth,
441
+ rotation,
442
+ }),
443
+ }).promise
444
+ },
445
+ },
446
+ }
447
+ </script>
448
+
449
+ <style lang="scss">
450
+ @import 'styles/text-layer';
451
+ @import 'styles/annotation-layer';
452
+
453
+ .vue-pdf-embed {
454
+ &__page {
455
+ position: relative;
456
+
457
+ canvas {
458
+ display: block;
459
+ }
460
+ }
461
+ }
462
+ </style>
@@ -0,0 +1,5 @@
1
+ {
2
+ "env": {
3
+ "jest": true
4
+ }
5
+ }
@@ -0,0 +1,55 @@
1
+ import Vue from 'vue'
2
+ import VuePdfEmbed from '../src/vue-pdf-embed.vue'
3
+
4
+ HTMLCanvasElement.prototype.getContext = () => {}
5
+
6
+ jest.mock('pdfjs-dist/legacy/build/pdf.worker.js', () => jest.fn())
7
+
8
+ jest.mock('pdfjs-dist/legacy/build/pdf.js', () => ({
9
+ GlobalWorkerOptions: {},
10
+ getDocument: () => ({
11
+ promise: {
12
+ numPages: 5,
13
+ getPage: () => ({
14
+ view: [],
15
+ getViewport: () => ({}),
16
+ render: () => ({}),
17
+ }),
18
+ },
19
+ }),
20
+ }))
21
+
22
+ const Component = Vue.extend(VuePdfEmbed)
23
+ let vm, emitSpy
24
+
25
+ beforeEach(() => {
26
+ vm = new Component({
27
+ propsData: {
28
+ disableAnnotationLayer: true,
29
+ disableTextLayer: true,
30
+ source: 'SOURCE',
31
+ },
32
+ }).$mount()
33
+ emitSpy = jest.spyOn(vm, '$emit')
34
+ })
35
+
36
+ test('sets correct data', () => {
37
+ expect(vm.document).toBeTruthy()
38
+ expect(vm.pageCount).toBe(5)
39
+ expect(vm.pageNums).toEqual([1, 2, 3, 4, 5])
40
+ })
41
+
42
+ test('sets page IDs', async () => {
43
+ vm.id = 'ID'
44
+ await vm.$nextTick()
45
+ vm.$el.childNodes.forEach((node, i) => {
46
+ expect(node.id).toEqual(`ID-${i + 1}`)
47
+ })
48
+ })
49
+
50
+ test('emits successful event', async () => {
51
+ await vm.$nextTick()
52
+ expect(emitSpy).lastCalledWith('loaded', expect.anything())
53
+ await vm.$nextTick()
54
+ expect(emitSpy).lastCalledWith('rendered')
55
+ })
@@ -0,0 +1,34 @@
1
+ import { VueConstructor } from 'vue';
2
+
3
+ export interface VuePdfEmbedProps {
4
+ annotationLayer?: boolean;
5
+ height?: number | string;
6
+ imageResourcesPath?: string;
7
+ page?: number;
8
+ rotation?: number | string;
9
+ source: object | string | URL | Uint8Array;
10
+ textLayer?: boolean;
11
+ width?: number | string;
12
+ }
13
+
14
+ export interface VuePdfEmbedData {
15
+ document: object | null;
16
+ pageCount: number | null;
17
+ pageNums: number[];
18
+ }
19
+
20
+ export interface VuePdfEmbedMethods {
21
+ download: (filename?: string) => Promise<void>;
22
+ print: (dpi?: number, filename?: string, allPages?: boolean) => Promise<void>;
23
+ render: () => Promise<void>;
24
+ }
25
+
26
+ export interface VuePdfEmbedConstructor extends VueConstructor {
27
+ props: VuePdfEmbedProps;
28
+ data: () => VuePdfEmbedData;
29
+ methods: VuePdfEmbedMethods;
30
+ }
31
+
32
+ declare const VuePdfEmbed: VuePdfEmbedConstructor;
33
+
34
+ export default VuePdfEmbed;
@@ -0,0 +1,34 @@
1
+ import { ComputedOptions, DefineComponent, MethodOptions } from 'vue';
2
+
3
+ export interface VuePdfEmbedProps {
4
+ annotationLayer?: boolean;
5
+ height?: number | string;
6
+ imageResourcesPath?: string;
7
+ page?: number;
8
+ rotation?: number | string;
9
+ source: object | string | URL | Uint8Array;
10
+ textLayer?: boolean;
11
+ width?: number | string;
12
+ }
13
+
14
+ export interface VuePdfEmbedData {
15
+ document: object | null;
16
+ pageCount: number | null;
17
+ pageNums: number[];
18
+ }
19
+
20
+ export interface VuePdfEmbedMethods extends MethodOptions {
21
+ download: (filename?: string) => Promise<void>;
22
+ print: (dpi?: number, filename?: string, allPages?: boolean) => Promise<void>;
23
+ render: () => Promise<void>;
24
+ }
25
+
26
+ declare const VuePdfEmbed: DefineComponent<
27
+ VuePdfEmbedProps,
28
+ {},
29
+ VuePdfEmbedData,
30
+ ComputedOptions,
31
+ VuePdfEmbedMethods
32
+ >;
33
+
34
+ export default VuePdfEmbed;
@@ -0,0 +1,77 @@
1
+ const { merge } = require('webpack-merge')
2
+ const CopyPlugin = require('copy-webpack-plugin')
3
+ const TerserPlugin = require('terser-webpack-plugin')
4
+ const { VueLoaderPlugin: Vue2LoaderPlugin } = require('vue-loader')
5
+ const { VueLoaderPlugin: Vue3LoaderPlugin } = require('vue-loader-next')
6
+
7
+ const commonConfig = {
8
+ mode: 'production',
9
+ entry: './src/index.js',
10
+ output: {
11
+ library: {
12
+ name: 'vue-pdf-embed',
13
+ type: 'umd',
14
+ },
15
+ },
16
+ module: {
17
+ rules: [
18
+ {
19
+ test: /\.js$/,
20
+ loader: 'babel-loader',
21
+ exclude: /node_modules/,
22
+ },
23
+ {
24
+ test: /\.vue$/,
25
+ loader: 'vue-loader',
26
+ },
27
+ {
28
+ test: /\.s?css$/,
29
+ use: ['vue-style-loader', 'css-loader', 'sass-loader'],
30
+ },
31
+ {
32
+ test: /\.worker\.js$/,
33
+ loader: 'worker-loader',
34
+ options: {
35
+ inline: 'no-fallback',
36
+ },
37
+ },
38
+ ],
39
+ },
40
+ optimization: {
41
+ minimizer: [
42
+ new TerserPlugin({
43
+ extractComments: false,
44
+ terserOptions: {
45
+ format: {
46
+ comments: false,
47
+ },
48
+ },
49
+ }),
50
+ ],
51
+ },
52
+ externals: {
53
+ vue: 'vue',
54
+ },
55
+ performance: {
56
+ hints: false,
57
+ },
58
+ }
59
+
60
+ module.exports = [
61
+ merge(commonConfig, {
62
+ output: {
63
+ clean: true,
64
+ filename: 'vue2-pdf-embed.js',
65
+ },
66
+ plugins: [new Vue2LoaderPlugin()],
67
+ }),
68
+ merge(commonConfig, {
69
+ output: {
70
+ filename: 'vue3-pdf-embed.js',
71
+ },
72
+ plugins: [
73
+ new Vue3LoaderPlugin(),
74
+ new CopyPlugin({ patterns: [{ from: 'types' }] }),
75
+ ],
76
+ }),
77
+ ]
@@ -0,0 +1,29 @@
1
+ const { VueLoaderPlugin } = require('vue-loader')
2
+ const HtmlWebpackPlugin = require('html-webpack-plugin')
3
+
4
+ module.exports = {
5
+ mode: 'development',
6
+ entry: './demo/main.js',
7
+ module: {
8
+ rules: [
9
+ {
10
+ test: /\.vue$/,
11
+ use: 'vue-loader',
12
+ },
13
+ {
14
+ test: /\.s?css$/,
15
+ use: ['vue-style-loader', 'css-loader', 'sass-loader'],
16
+ },
17
+ {
18
+ test: /\.worker\.js$/,
19
+ loader: 'worker-loader',
20
+ },
21
+ ],
22
+ },
23
+ plugins: [
24
+ new VueLoaderPlugin(),
25
+ new HtmlWebpackPlugin({
26
+ template: './demo/index.html',
27
+ }),
28
+ ],
29
+ }