@libresign/pdf-elements 0.3.1 → 1.0.0

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.
Files changed (39) hide show
  1. package/README.md +24 -1
  2. package/dist/components/DraggableElement.vue.d.ts +240 -0
  3. package/dist/components/PDFElements.vue.d.ts +552 -0
  4. package/dist/components/PDFPage.vue.d.ts +40 -0
  5. package/dist/index.css +1 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.mjs +1293 -0
  8. package/dist/pdf-uHvE5neP.mjs +17593 -0
  9. package/dist/pdf.worker.min-DmO9Xdfo.mjs +4 -0
  10. package/dist/types.d.ts +30 -0
  11. package/dist/utils/asyncReader.d.ts +10 -0
  12. package/dist/utils/geometry.d.ts +5 -0
  13. package/dist/utils/measurements.d.ts +12 -0
  14. package/dist/utils/objectStore.d.ts +5 -0
  15. package/dist/utils/pageBounds.d.ts +5 -0
  16. package/dist/utils/zoom.d.ts +2 -0
  17. package/package.json +46 -21
  18. package/src/components/DraggableElement.vue +18 -15
  19. package/src/components/PDFElements.vue +135 -60
  20. package/src/components/PDFPage.vue +11 -7
  21. package/src/index.ts +18 -0
  22. package/src/types.ts +35 -0
  23. package/src/utils/asyncReader.ts +103 -0
  24. package/src/utils/{geometry.js → geometry.ts} +16 -2
  25. package/src/utils/{measurements.js → measurements.ts} +20 -1
  26. package/src/utils/{objectStore.js → objectStore.ts} +15 -4
  27. package/src/utils/{pageBounds.js → pageBounds.ts} +2 -2
  28. package/src/utils/{zoom.js → zoom.ts} +3 -1
  29. package/dist/demo.html +0 -1
  30. package/dist/pdf-elements.common.js +0 -31698
  31. package/dist/pdf-elements.common.js.map +0 -1
  32. package/dist/pdf-elements.css +0 -1
  33. package/dist/pdf-elements.umd.js +0 -31710
  34. package/dist/pdf-elements.umd.js.map +0 -1
  35. package/dist/pdf-elements.umd.min.js +0 -2
  36. package/dist/pdf-elements.umd.min.js.map +0 -1
  37. package/dist/pdf.worker.min.mjs +0 -22
  38. package/src/index.js +0 -17
  39. package/src/utils/asyncReader.js +0 -44
@@ -25,6 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-or-later
25
25
  class="overlay"
26
26
  @mousemove="handleMouseMove"
27
27
  @touchmove="handleMouseMove"
28
+ @click="handleOverlayClick(docIndex, pIndex, $event)"
29
+ @touchend="handleOverlayClick(docIndex, pIndex, $event)"
28
30
  >
29
31
  <div
30
32
  v-if="isAddingMode && previewPageDocIndex === docIndex && previewPageIndex === pIndex && previewElement && previewVisible"
@@ -137,18 +139,21 @@ SPDX-License-Identifier: AGPL-3.0-or-later
137
139
  </div>
138
140
  </template>
139
141
 
140
- <script>
142
+ <script lang="ts">
143
+ import { defineComponent, type PropType, markRaw } from 'vue'
141
144
  import PDFPage from './PDFPage.vue'
142
145
  import DraggableElement from './DraggableElement.vue'
143
- import { readAsPDF, readAsArrayBuffer } from '../utils/asyncReader.js'
144
- import { clampPosition, getVisibleArea } from '../utils/geometry.js'
145
- import { getViewportWindow, isPageInViewport } from '../utils/pageBounds.js'
146
- import { applyScaleToDocs } from '../utils/zoom.js'
147
- import { objectIdExistsInDoc, findObjectPageIndex, updateObjectInDoc, removeObjectFromDoc } from '../utils/objectStore.js'
148
- import { getCachedMeasurement } from '../utils/measurements.js'
149
-
150
- export default {
146
+ import { readAsPDF, readAsArrayBuffer } from '../utils/asyncReader'
147
+ import { clampPosition, getVisibleArea } from '../utils/geometry'
148
+ import { getViewportWindow, isPageInViewport } from '../utils/pageBounds'
149
+ import { applyScaleToDocs } from '../utils/zoom'
150
+ import { objectIdExistsInDoc, findObjectPageIndex, updateObjectInDoc, removeObjectFromDoc } from '../utils/objectStore'
151
+ import { getCachedMeasurement } from '../utils/measurements'
152
+ import type { PDFDocumentEntry, PDFElementObject } from '../types'
153
+
154
+ export default defineComponent({
151
155
  name: 'PDFElements',
156
+ emits: ['pdf-elements:end-init', 'pdf-elements:delete-object', 'pdf-elements:object-click'],
152
157
  components: {
153
158
  PDFPage,
154
159
  DraggableElement,
@@ -163,11 +168,13 @@ export default {
163
168
  default: '100%',
164
169
  },
165
170
  initFiles: {
166
- type: Array,
171
+ type: Array as PropType<
172
+ (string | Blob | ArrayBuffer | ArrayBufferView | Record<string, unknown>)[]
173
+ >,
167
174
  default: () => [],
168
175
  },
169
176
  initFileNames: {
170
- type: Array,
177
+ type: Array as PropType<string[]>,
171
178
  default: () => [],
172
179
  },
173
180
  initialScale: {
@@ -194,8 +201,12 @@ export default {
194
201
  type: Boolean,
195
202
  default: false,
196
203
  },
204
+ emitObjectClick: {
205
+ type: Boolean,
206
+ default: false,
207
+ },
197
208
  ignoreClickOutsideSelectors: {
198
- type: Array,
209
+ type: Array as PropType<string[]>,
199
210
  default: () => [],
200
211
  },
201
212
  pageCountFormat: {
@@ -206,32 +217,36 @@ export default {
206
217
  type: Boolean,
207
218
  default: false,
208
219
  },
220
+ pdfjsOptions: {
221
+ type: Object as PropType<Record<string, unknown>>,
222
+ default: () => ({}),
223
+ },
209
224
  },
210
225
  data() {
211
226
  return {
212
227
  scale: this.initialScale,
213
- pdfDocuments: [],
228
+ pdfDocuments: [] as PDFDocumentEntry[],
214
229
  selectedDocIndex: -1,
215
230
  selectedPageIndex: -1,
216
231
  isAddingMode: false,
217
- previewElement: null,
232
+ previewElement: null as PDFElementObject | null,
218
233
  previewPosition: { x: 0, y: 0 },
219
234
  previewScale: { x: 1, y: 1 },
220
235
  previewPageDocIndex: -1,
221
236
  previewPageIndex: -1,
222
237
  previewVisible: false,
223
238
  hoverRafId: 0,
224
- pendingHoverClientPos: null,
225
- lastHoverRect: null,
239
+ pendingHoverClientPos: null as { x: number; y: number } | null,
240
+ lastHoverRect: null as DOMRect | null,
226
241
  addingListenersAttached: false,
227
242
  dragRafId: 0,
228
- pendingDragClientPos: null,
243
+ pendingDragClientPos: null as { x: number; y: number } | null,
229
244
  pageBoundsVersion: 0,
230
245
  lastScrollTop: 0,
231
246
  lastClientWidth: 0,
232
247
  nextObjectCounter: 0,
233
248
  isDraggingElement: false,
234
- draggingObject: null,
249
+ draggingObject: null as PDFElementObject | null,
235
250
  draggingDocIndex: -1,
236
251
  draggingPageIndex: -1,
237
252
  draggingClientPosition: { x: 0, y: 0 },
@@ -240,22 +255,20 @@ export default {
240
255
  draggingElementShift: { x: 0, y: 0 },
241
256
  lastMouseClientPos: { x: 0, y: 0 },
242
257
  viewportRafId: 0,
243
- objectIndexCache: {},
244
- zoomRafId: null,
245
- wheelZoomRafId: null,
246
- boundHandleWheel: null,
258
+ objectIndexCache: markRaw({}) as Record<string, number>,
259
+ zoomRafId: null as number | null,
260
+ wheelZoomRafId: null as number | null,
261
+ boundHandleWheel: null as ((event: WheelEvent) => void) | null,
247
262
  visualScale: this.initialScale,
248
263
  autoFitApplied: false,
249
264
  lastContainerWidth: 0,
265
+ _pagesBoundingRects: markRaw({}) as Record<string, { docIndex: number; pageIndex: number; rect: DOMRect }>,
266
+ _pagesBoundingRectsList: markRaw([]) as { docIndex: number; pageIndex: number; rect: DOMRect }[],
267
+ _pageMeasurementCache: markRaw({}) as Record<string, { width: number; height: number }>,
268
+ _lastPageBoundsScrollTop: 0,
269
+ _lastPageBoundsClientHeight: 0,
250
270
  }
251
271
  },
252
- created() {
253
- this._pagesBoundingRects = {}
254
- this._pagesBoundingRectsList = []
255
- this._pageMeasurementCache = {}
256
- this._lastPageBoundsScrollTop = 0
257
- this._lastPageBoundsClientHeight = 0
258
- },
259
272
  mounted() {
260
273
  this.boundHandleWheel = this.handleWheel.bind(this)
261
274
  this.init()
@@ -295,7 +308,7 @@ export default {
295
308
  methods: {
296
309
  async init() {
297
310
  if (!this.initFiles || this.initFiles.length === 0) return
298
- const docs = []
311
+ const docs: PDFDocumentEntry[] = []
299
312
  this.autoFitApplied = false
300
313
 
301
314
  for (let i = 0; i < this.initFiles.length; i++) {
@@ -305,9 +318,9 @@ export default {
305
318
  let pdfDoc
306
319
  if (file instanceof Blob) {
307
320
  const buffer = await readAsArrayBuffer(file)
308
- pdfDoc = await readAsPDF({ data: buffer })
321
+ pdfDoc = await readAsPDF({ data: buffer }, this.pdfjsOptions)
309
322
  } else {
310
- pdfDoc = await readAsPDF(file)
323
+ pdfDoc = await readAsPDF(file, this.pdfjsOptions)
311
324
  }
312
325
 
313
326
  const pages = []
@@ -323,12 +336,14 @@ export default {
323
336
  pages.push(pagePromise)
324
337
  }
325
338
 
339
+ const rawPages = markRaw(pages)
340
+
326
341
  docs.push({
327
342
  name,
328
343
  file,
329
- pdfDoc,
344
+ pdfDoc: markRaw(pdfDoc),
330
345
  numPages: pdfDoc.numPages,
331
- pages,
346
+ pages: rawPages,
332
347
  pageWidths,
333
348
  pagesScale: Array(pdfDoc.numPages).fill(this.scale),
334
349
  allObjects: Array(pdfDoc.numPages).fill(0).map(() => []),
@@ -336,7 +351,7 @@ export default {
336
351
  }
337
352
 
338
353
  this.pdfDocuments = docs
339
- this._pageMeasurementCache = {}
354
+ this._pageMeasurementCache = markRaw({})
340
355
  if (docs.length) {
341
356
  this.selectedDocIndex = 0
342
357
  this.selectedPageIndex = 0
@@ -388,7 +403,12 @@ export default {
388
403
  updateDraggingPosition(clientX, clientY) {
389
404
  if (!this.isDraggingElement) return
390
405
 
391
- this.pendingDragClientPos = { x: clientX, y: clientY }
406
+ if (this.pendingDragClientPos) {
407
+ this.pendingDragClientPos.x = clientX
408
+ this.pendingDragClientPos.y = clientY
409
+ } else {
410
+ this.pendingDragClientPos = { x: clientX, y: clientY }
411
+ }
392
412
  if (this.dragRafId) return
393
413
  this.dragRafId = window.requestAnimationFrame(() => {
394
414
  this.dragRafId = 0
@@ -447,6 +467,7 @@ export default {
447
467
 
448
468
  cachePageBounds() {
449
469
  const nextRects = {}
470
+ const nextList = []
450
471
  const container = this.$el
451
472
  const scrollTop = container?.scrollTop || 0
452
473
  const viewHeight = container?.clientHeight || 0
@@ -471,22 +492,24 @@ export default {
471
492
  }
472
493
  }
473
494
  const rect = canvas.getBoundingClientRect()
474
- nextRects[`${docIdx}-${pageIdx}`] = {
495
+ const entry = {
475
496
  docIndex: docIdx,
476
497
  pageIndex: pageIdx,
477
498
  rect,
478
499
  }
500
+ nextRects[`${docIdx}-${pageIdx}`] = entry
501
+ nextList.push(entry)
479
502
  }
480
503
  }
481
- this._pagesBoundingRects = nextRects
482
- this._pagesBoundingRectsList = Object.values(nextRects)
504
+ this._pagesBoundingRects = markRaw(nextRects)
505
+ this._pagesBoundingRectsList = markRaw(nextList)
483
506
  this.pageBoundsVersion++
484
507
  },
485
508
  cachePageBoundsForPage(docIndex, pageIndex) {
486
509
  const canvas = this.getPageCanvasElement(docIndex, pageIndex)
487
510
  if (!canvas) return
488
511
  const rect = canvas.getBoundingClientRect()
489
- this._pagesBoundingRects = {
512
+ const nextRects = {
490
513
  ...this._pagesBoundingRects,
491
514
  [`${docIndex}-${pageIndex}`]: {
492
515
  docIndex,
@@ -494,7 +517,8 @@ export default {
494
517
  rect,
495
518
  },
496
519
  }
497
- this._pagesBoundingRectsList = Object.values(this._pagesBoundingRects)
520
+ this._pagesBoundingRects = markRaw(nextRects)
521
+ this._pagesBoundingRectsList = markRaw(Object.values(nextRects))
498
522
  this.pageBoundsVersion++
499
523
  },
500
524
  getPageBoundsMap() {
@@ -520,7 +544,7 @@ export default {
520
544
  },
521
545
 
522
546
  getDisplayedPageScale(docIndex, pageIndex) {
523
- this.pageBoundsVersion
547
+ void this.pageBoundsVersion
524
548
  const doc = this.pdfDocuments[docIndex]
525
549
  if (!doc) return 1
526
550
  const baseWidth = doc.pageWidths?.[pageIndex] || 0
@@ -583,7 +607,12 @@ export default {
583
607
  if (!this.isAddingMode || !this.previewElement) return
584
608
  const { x, y } = this.getPointerPosition(event)
585
609
  if (x === undefined || y === undefined) return
586
- this.pendingHoverClientPos = { x, y }
610
+ if (this.pendingHoverClientPos) {
611
+ this.pendingHoverClientPos.x = x
612
+ this.pendingHoverClientPos.y = y
613
+ } else {
614
+ this.pendingHoverClientPos = { x, y }
615
+ }
587
616
  if (this.hoverRafId) return
588
617
  this.hoverRafId = window.requestAnimationFrame(() => {
589
618
  this.hoverRafId = 0
@@ -603,11 +632,11 @@ export default {
603
632
  rect: this.lastHoverRect,
604
633
  }
605
634
  } else {
606
- const rects = this.getPageBoundsList().length
635
+ const rectEntries = this.getPageBoundsList().length
607
636
  ? this.getPageBoundsList()
608
637
  : Object.values(this.getPageBoundsMap())
609
- for (let i = 0; i < rects.length; i++) {
610
- const entry = rects[i]
638
+ for (let i = 0; i < rectEntries.length; i++) {
639
+ const entry = rectEntries[i]
611
640
  const rect = entry.rect
612
641
  if (cursorX >= rect.left && cursorX <= rect.right &&
613
642
  cursorY >= rect.top && cursorY <= rect.bottom) {
@@ -651,6 +680,49 @@ export default {
651
680
  this.previewVisible = true
652
681
  })
653
682
  },
683
+ handleOverlayClick(docIndex, pageIndex, event) {
684
+ if (!this.emitObjectClick) return
685
+
686
+ const { x: clientX, y: clientY } = this.getPointerPosition(event)
687
+ if (!Number.isFinite(clientX) || !Number.isFinite(clientY)) return
688
+
689
+ this.cachePageBoundsForPage(docIndex, pageIndex)
690
+ const pageRect = this.getPageRect(docIndex, pageIndex)
691
+ if (!pageRect) return
692
+
693
+ const pagesScale = this.getDisplayedPageScale(docIndex, pageIndex) || 1
694
+ const relX = (clientX - pageRect.left) / pagesScale
695
+ const relY = (clientY - pageRect.top) / pagesScale
696
+
697
+ const doc = this.pdfDocuments?.[docIndex]
698
+ const pageObjects = doc?.allObjects?.[pageIndex] || []
699
+ if (pageObjects.length === 0) return
700
+ let hitObject = null
701
+
702
+ for (let i = pageObjects.length - 1; i >= 0; i--) {
703
+ const object = pageObjects[i]
704
+ const x = Number(object.x)
705
+ const y = Number(object.y)
706
+ const width = Number(object.width)
707
+ const height = Number(object.height)
708
+ if (![x, y, width, height].every(Number.isFinite)) {
709
+ continue
710
+ }
711
+ if (relX >= x && relX <= x + width && relY >= y && relY <= y + height) {
712
+ hitObject = object
713
+ break
714
+ }
715
+ }
716
+
717
+ if (!hitObject) return
718
+
719
+ this.$emit('pdf-elements:object-click', {
720
+ docIndex,
721
+ pageIndex,
722
+ object: hitObject,
723
+ event,
724
+ })
725
+ },
654
726
 
655
727
  handleKeyDown(event) {
656
728
  if (event.key === 'Escape' && this.isAddingMode) {
@@ -679,7 +751,7 @@ export default {
679
751
 
680
752
  applyScaleToDocs(this.pdfDocuments, this.scale)
681
753
 
682
- this._pageMeasurementCache = {}
754
+ this._pageMeasurementCache = markRaw({})
683
755
  this.cachePageBounds()
684
756
  },
685
757
 
@@ -746,7 +818,7 @@ export default {
746
818
  if (!this.addingListenersAttached) return
747
819
  this.addingListenersAttached = false
748
820
  document.removeEventListener('mousemove', this.handleMouseMove)
749
- document.removeEventListener('touchmove', this.handleMouseMove, { passive: true })
821
+ document.removeEventListener('touchmove', this.handleMouseMove)
750
822
  document.removeEventListener('mouseup', this.finishAdding)
751
823
  document.removeEventListener('touchend', this.finishAdding)
752
824
  document.removeEventListener('keydown', this.handleKeyDown)
@@ -765,8 +837,9 @@ export default {
765
837
  objectToAdd = { ...objectToAdd, id: this.generateObjectId() }
766
838
  }
767
839
 
768
- const pageWidth = this.getPageWidth(docIndex, pageIndex)
769
- const pageHeight = this.getPageHeight(docIndex, pageIndex)
840
+ const measurement = this.getCachedMeasurement(docIndex, pageIndex, pageRef)
841
+ const pageWidth = measurement.width
842
+ const pageHeight = measurement.height
770
843
 
771
844
  if (objectToAdd.x < 0 || objectToAdd.y < 0 ||
772
845
  objectToAdd.x + objectToAdd.width > pageWidth ||
@@ -814,11 +887,11 @@ export default {
814
887
  pageNumber: pageIndex + 1,
815
888
  scale: pagesScale,
816
889
  normalizedCoordinates: {
817
- llx: parseInt(object.x, 10),
818
- lly: parseInt(normalizedCanvasHeight - object.y, 10),
819
- ury: parseInt(normalizedCanvasHeight - object.y - object.height, 10),
820
- width: parseInt(object.width, 10),
821
- height: parseInt(object.height, 10),
890
+ llx: Math.round(object.x),
891
+ lly: Math.round(normalizedCanvasHeight - object.y),
892
+ ury: Math.round(normalizedCanvasHeight - object.y - object.height),
893
+ width: Math.round(object.width),
894
+ height: Math.round(object.height),
822
895
  },
823
896
  })
824
897
  })
@@ -1026,9 +1099,11 @@ export default {
1026
1099
  if (!targetObject) return currentPageIndex
1027
1100
 
1028
1101
  let targetPageIndex = currentPageIndex
1102
+ const pageBoundsList = this.getPageBoundsList()
1029
1103
  const pageBoundsMap = this.getPageBoundsMap()
1030
- for (const key in pageBoundsMap) {
1031
- const { docIndex: rectDocIndex, pageIndex, rect } = pageBoundsMap[key]
1104
+ const boundsEntries = pageBoundsList.length ? pageBoundsList : Object.values(pageBoundsMap)
1105
+ for (let i = 0; i < boundsEntries.length; i++) {
1106
+ const { docIndex: rectDocIndex, pageIndex, rect } = boundsEntries[i]
1032
1107
  if (rectDocIndex === docIndex &&
1033
1108
  mouseX >= rect.left && mouseX <= rect.right &&
1034
1109
  mouseY >= rect.top && mouseY <= rect.bottom) {
@@ -1138,7 +1213,7 @@ export default {
1138
1213
  this.scheduleAutoFitZoom()
1139
1214
  return
1140
1215
  }
1141
- const canvases = this.$el?.querySelectorAll('canvas')
1216
+ const canvases = this.$el?.querySelectorAll('canvas') as NodeListOf<HTMLCanvasElement> | undefined
1142
1217
  if (!canvases?.length) return
1143
1218
  maxCanvasWidth = Math.max(...Array.from(canvases).map(canvas =>
1144
1219
  canvas.width / (this.scale || 1),
@@ -1151,12 +1226,12 @@ export default {
1151
1226
  this.scale = optimalScale
1152
1227
  this.visualScale = optimalScale
1153
1228
  applyScaleToDocs(this.pdfDocuments, this.scale)
1154
- this._pageMeasurementCache = {}
1229
+ this._pageMeasurementCache = markRaw({})
1155
1230
  this.cachePageBounds()
1156
1231
  }
1157
1232
  },
1158
1233
  },
1159
- }
1234
+ })
1160
1235
  </script>
1161
1236
 
1162
1237
  <style scoped>
@@ -7,12 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-or-later
7
7
  <canvas ref="canvas" />
8
8
  </template>
9
9
 
10
- <script>
11
- export default {
10
+ <script lang="ts">
11
+ import { defineComponent, type PropType } from 'vue'
12
+
13
+ export default defineComponent({
12
14
  name: 'PDFPage',
15
+ emits: ['onMeasure'],
13
16
  props: {
14
17
  page: {
15
- type: Promise,
18
+ type: Object as PropType<Promise<unknown>>,
16
19
  required: true,
17
20
  },
18
21
  scale: {
@@ -25,7 +28,7 @@ export default {
25
28
  dynamicScale: this.scale,
26
29
  isRendering: false,
27
30
  pendingRender: false,
28
- renderTask: null,
31
+ renderTask: null as { cancel: () => void; promise: Promise<void> } | null,
29
32
  }
30
33
  },
31
34
  watch: {
@@ -41,7 +44,7 @@ export default {
41
44
  if (this.renderTask) {
42
45
  try {
43
46
  this.renderTask.cancel()
44
- } catch (e) {
47
+ } catch {
45
48
  // Ignore render cancellation errors.
46
49
  }
47
50
  this.renderTask = null
@@ -73,12 +76,13 @@ export default {
73
76
  if (this.renderTask) {
74
77
  try {
75
78
  this.renderTask.cancel()
76
- } catch (e) {
79
+ } catch {
77
80
  // Ignore render cancellation errors.
78
81
  }
79
82
  this.renderTask = null
80
83
  }
81
84
  const context = canvas.getContext('2d')
85
+ if (!context) return
82
86
  const viewport = _page.getViewport({
83
87
  scale: this.dynamicScale,
84
88
  })
@@ -100,7 +104,7 @@ export default {
100
104
  }
101
105
  },
102
106
  },
103
- }
107
+ })
104
108
  </script>
105
109
 
106
110
  <style scoped>
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ // SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ import type { App } from 'vue'
5
+ import PDFElements from './components/PDFElements.vue'
6
+
7
+ export { setWorkerPath } from './utils/asyncReader'
8
+ export type { PDFDocumentEntry, PDFElementObject, PDFElementsPublicApi } from './types'
9
+
10
+ const install = (app: App) => {
11
+ const name = PDFElements.name || 'PDFElements'
12
+ app.component(name, PDFElements)
13
+ }
14
+
15
+ ;(PDFElements as { install?: (app: App) => void }).install = install
16
+
17
+ export default PDFElements
18
+ export { PDFElements }
package/src/types.ts ADDED
@@ -0,0 +1,35 @@
1
+ // SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ export interface PDFElementObject {
5
+ id?: string
6
+ x: number
7
+ y: number
8
+ width: number
9
+ height: number
10
+ type?: string
11
+ label?: string
12
+ icon?: string
13
+ resizable?: boolean
14
+ [key: string]: unknown
15
+ }
16
+
17
+ export interface PDFDocumentEntry {
18
+ name: string
19
+ file: unknown
20
+ pdfDoc: unknown
21
+ numPages: number
22
+ pages: Promise<unknown>[]
23
+ pageWidths: number[]
24
+ pagesScale: number[]
25
+ allObjects: PDFElementObject[][]
26
+ }
27
+
28
+ export interface PDFElementsPublicApi {
29
+ startAddingElement: (templateObject: PDFElementObject) => void
30
+ addObjectToPage: (object: PDFElementObject, pageIndex?: number, docIndex?: number) => boolean
31
+ getAllObjects: (docIndex?: number) => PDFElementObject[]
32
+ updateObject: (docIndex: number, objectId: string, payload: Partial<PDFElementObject>) => void
33
+ deleteObject: (docIndex: number, objectId: string) => void
34
+ duplicateObject: (docIndex: number, objectId: string) => void
35
+ }
@@ -0,0 +1,103 @@
1
+ // SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ import type { PDFDocumentProxy, PDFWorker } from 'pdfjs-dist'
5
+
6
+ let sharedWorker: PDFWorker | null = null
7
+ let pdfjsPromise: Promise<typeof import('pdfjs-dist')> | null = null
8
+ let workerUrlPromise: Promise<string> | null = null
9
+ let workerSrcOverride: string | null = null
10
+
11
+ function loadPdfjs() {
12
+ if (!pdfjsPromise) {
13
+ pdfjsPromise = import('pdfjs-dist')
14
+ }
15
+ return pdfjsPromise
16
+ }
17
+
18
+ function loadWorkerUrl() {
19
+ if (!workerUrlPromise) {
20
+ workerUrlPromise = import('pdfjs-dist/build/pdf.worker.min.mjs?url').then(
21
+ (mod) => mod.default as string
22
+ )
23
+ }
24
+ return workerUrlPromise
25
+ }
26
+
27
+ async function ensureWorkerSrc(pdfjs: typeof import('pdfjs-dist')) {
28
+ if (workerSrcOverride) {
29
+ pdfjs.GlobalWorkerOptions.workerSrc = workerSrcOverride
30
+ return
31
+ }
32
+ if (!pdfjs.GlobalWorkerOptions.workerSrc) {
33
+ pdfjs.GlobalWorkerOptions.workerSrc = await loadWorkerUrl()
34
+ }
35
+ }
36
+
37
+ async function getSharedWorker(pdfjs: typeof import('pdfjs-dist')): Promise<PDFWorker> {
38
+ if (!sharedWorker) {
39
+ await ensureWorkerSrc(pdfjs)
40
+ sharedWorker = new pdfjs.PDFWorker({}) as PDFWorker
41
+ }
42
+ return sharedWorker
43
+ }
44
+
45
+ export function setWorkerPath(path: string) {
46
+ workerSrcOverride = path
47
+ if (pdfjsPromise) {
48
+ pdfjsPromise.then((pdfjs) => {
49
+ pdfjs.GlobalWorkerOptions.workerSrc = path
50
+ })
51
+ }
52
+ }
53
+
54
+ export function readAsArrayBuffer(file: Blob): Promise<ArrayBuffer> {
55
+ return new Promise((resolve, reject) => {
56
+ const reader = new FileReader()
57
+ reader.onload = () => resolve(reader.result as ArrayBuffer)
58
+ reader.onerror = reject
59
+ reader.readAsArrayBuffer(file)
60
+ })
61
+ }
62
+
63
+ type PdfInput =
64
+ | string
65
+ | ArrayBuffer
66
+ | ArrayBufferView
67
+ | Blob
68
+ | {
69
+ url?: string
70
+ data?: ArrayBuffer | Uint8Array
71
+ [key: string]: unknown
72
+ }
73
+
74
+ export async function readAsPDF(
75
+ file: PdfInput,
76
+ options: Record<string, unknown> = {}
77
+ ): Promise<PDFDocumentProxy> {
78
+ const pdfjs = await loadPdfjs()
79
+ const worker = await getSharedWorker(pdfjs)
80
+ const isArrayBuffer = file instanceof ArrayBuffer
81
+ const isView = ArrayBuffer.isView(file)
82
+ const isBlob = typeof Blob !== 'undefined' && file instanceof Blob
83
+
84
+ if (file && typeof file === 'object' && !isArrayBuffer && !isView && !isBlob) {
85
+ return pdfjs.getDocument({ ...(file as Record<string, unknown>), ...options, worker }).promise
86
+ }
87
+ if (typeof file === 'string') {
88
+ return pdfjs.getDocument({ url: file, ...options, worker }).promise
89
+ }
90
+ if (isBlob) {
91
+ const data = await readAsArrayBuffer(file as Blob)
92
+ return pdfjs.getDocument({ data, ...options, worker }).promise
93
+ }
94
+ const data = isArrayBuffer
95
+ ? (file as ArrayBuffer)
96
+ : new Uint8Array(
97
+ (file as ArrayBufferView).buffer,
98
+ (file as ArrayBufferView).byteOffset,
99
+ (file as ArrayBufferView).byteLength
100
+ )
101
+
102
+ return pdfjs.getDocument({ data, ...options, worker }).promise
103
+ }
@@ -1,14 +1,28 @@
1
1
  // SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
- export function clampPosition(x, y, width, height, pageWidth, pageHeight) {
4
+ export function clampPosition(
5
+ x: number,
6
+ y: number,
7
+ width: number,
8
+ height: number,
9
+ pageWidth: number,
10
+ pageHeight: number
11
+ ) {
5
12
  return {
6
13
  x: Math.max(0, Math.min(x, pageWidth - width)),
7
14
  y: Math.max(0, Math.min(y, pageHeight - height)),
8
15
  }
9
16
  }
10
17
 
11
- export function getVisibleArea(newX, newY, objWidth, objHeight, pageWidth, pageHeight) {
18
+ export function getVisibleArea(
19
+ newX: number,
20
+ newY: number,
21
+ objWidth: number,
22
+ objHeight: number,
23
+ pageWidth: number,
24
+ pageHeight: number
25
+ ) {
12
26
  const visibleLeft = Math.max(0, newX)
13
27
  const visibleTop = Math.max(0, newY)
14
28
  const visibleRight = Math.min(pageWidth, newX + objWidth)