2024-07-16 12:36:18 +03:00
|
|
|
import { PdfFile, PdfPage } from '../types/drawing.ts'
|
|
|
|
import * as PDFJS from 'pdfjs-dist'
|
2024-07-25 17:51:31 +03:00
|
|
|
import { PDFDocument } from 'pdf-lib'
|
|
|
|
import { Mark } from '../types/mark.ts'
|
|
|
|
|
2024-08-08 12:39:29 +03:00
|
|
|
PDFJS.GlobalWorkerOptions.workerSrc = new URL(
|
2024-08-08 12:37:47 +02:00
|
|
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
2024-08-08 12:39:29 +03:00
|
|
|
import.meta.url
|
|
|
|
).toString()
|
2024-07-16 12:36:18 +03:00
|
|
|
|
2024-07-25 17:51:31 +03:00
|
|
|
/**
|
|
|
|
* Scale between the PDF page's natural size and rendered size
|
|
|
|
* @constant {number}
|
|
|
|
*/
|
2024-08-13 12:48:52 +03:00
|
|
|
const SCALE: number = 3
|
2024-08-02 11:39:00 +01:00
|
|
|
/**
|
|
|
|
* Defined font size used when generating a PDF. Currently it is difficult to fully
|
|
|
|
* correlate font size used at the time of filling in / drawing on the PDF
|
|
|
|
* because it is dynamically rendered, and the final size.
|
|
|
|
* This should be fixed going forward.
|
|
|
|
* Switching to PDF-Lib will most likely make this problem redundant.
|
|
|
|
*/
|
2024-08-13 12:48:52 +03:00
|
|
|
const FONT_SIZE: number = 40
|
2024-08-02 11:39:00 +01:00
|
|
|
/**
|
|
|
|
* Current font type used when generating a PDF.
|
|
|
|
*/
|
2024-08-13 12:48:52 +03:00
|
|
|
const FONT_TYPE: string = 'Arial'
|
2024-07-25 17:51:31 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts a PDF ArrayBuffer to a generic PDF File
|
|
|
|
* @param arrayBuffer of a PDF
|
|
|
|
* @param fileName identifier of the pdf file
|
2024-08-21 17:07:29 +02:00
|
|
|
* @param type optional file type (defaults to pdf)
|
2024-07-25 17:51:31 +03:00
|
|
|
*/
|
2024-08-21 17:07:29 +02:00
|
|
|
const toFile = (
|
|
|
|
arrayBuffer: ArrayBuffer,
|
|
|
|
fileName: string,
|
|
|
|
type: string = 'application/pdf'
|
|
|
|
): File => {
|
|
|
|
const blob = new Blob([arrayBuffer], { type })
|
|
|
|
return new File([blob], fileName, { type })
|
2024-07-16 12:36:18 +03:00
|
|
|
}
|
|
|
|
|
2024-07-25 17:51:31 +03:00
|
|
|
/**
|
|
|
|
* Converts a generic PDF File to Sigit's internal Pdf File type
|
|
|
|
* @param {File} file
|
|
|
|
* @return {PdfFile} Sigit's internal PDF File type
|
|
|
|
*/
|
2024-07-16 12:36:18 +03:00
|
|
|
const toPdfFile = async (file: File): Promise<PdfFile> => {
|
|
|
|
const data = await readPdf(file)
|
|
|
|
const pages = await pdfToImages(data)
|
|
|
|
return { file, pages, expanded: false }
|
|
|
|
}
|
2024-07-25 17:51:31 +03:00
|
|
|
/**
|
|
|
|
* Transforms an array of generic PDF Files into an array of Sigit's
|
|
|
|
* internal representation of Pdf Files
|
|
|
|
* @param selectedFiles - an array of generic PDF Files
|
|
|
|
* @return PdfFile[] - an array of Sigit's internal Pdf File type
|
|
|
|
*/
|
2024-07-16 12:36:18 +03:00
|
|
|
const toPdfFiles = async (selectedFiles: File[]): Promise<PdfFile[]> => {
|
2024-08-13 12:48:52 +03:00
|
|
|
return Promise.all(selectedFiles.filter(isPdf).map(toPdfFile))
|
2024-07-16 12:36:18 +03:00
|
|
|
}
|
|
|
|
|
2024-07-25 17:51:31 +03:00
|
|
|
/**
|
|
|
|
* A utility that transforms a drawing coordinate number into a CSS-compatible string
|
|
|
|
* @param coordinate
|
|
|
|
*/
|
2024-08-13 12:48:52 +03:00
|
|
|
const inPx = (coordinate: number): string => `${coordinate}px`
|
2024-07-17 11:25:02 +03:00
|
|
|
|
2024-07-25 17:51:31 +03:00
|
|
|
/**
|
|
|
|
* A utility that checks if a given file is of the pdf type
|
|
|
|
* @param file
|
|
|
|
*/
|
2024-08-13 12:48:52 +03:00
|
|
|
const isPdf = (file: File) => file.type.toLowerCase().includes('pdf')
|
2024-07-16 12:36:18 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Reads the pdf file binaries
|
|
|
|
*/
|
2024-08-16 12:01:41 +02:00
|
|
|
const readPdf = (file: File): Promise<string | ArrayBuffer> => {
|
2024-07-16 12:36:18 +03:00
|
|
|
return new Promise((resolve, reject) => {
|
2024-08-13 12:48:52 +03:00
|
|
|
const reader = new FileReader()
|
2024-07-16 12:36:18 +03:00
|
|
|
|
2024-08-16 12:01:41 +02:00
|
|
|
reader.onload = (e) => {
|
|
|
|
const data = e.target?.result
|
|
|
|
// Make sure we only resolve for string or ArrayBuffer type
|
|
|
|
// They are accepted by PDFJS.getDocument function
|
|
|
|
if (data && typeof data !== 'undefined') {
|
|
|
|
resolve(data)
|
|
|
|
} else {
|
|
|
|
reject(new Error('File is null or undefined'))
|
|
|
|
}
|
2024-08-13 12:48:52 +03:00
|
|
|
}
|
2024-07-16 12:36:18 +03:00
|
|
|
|
|
|
|
reader.onerror = (err) => {
|
|
|
|
console.error('err', err)
|
|
|
|
reject(err)
|
2024-08-13 12:48:52 +03:00
|
|
|
}
|
2024-07-16 12:36:18 +03:00
|
|
|
|
2024-08-13 12:48:52 +03:00
|
|
|
reader.readAsDataURL(file)
|
2024-07-16 12:36:18 +03:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts pdf to the images
|
|
|
|
* @param data pdf file bytes
|
|
|
|
*/
|
2024-08-16 12:01:41 +02:00
|
|
|
const pdfToImages = async (data: string | ArrayBuffer): Promise<PdfPage[]> => {
|
2024-08-13 12:48:52 +03:00
|
|
|
const images: string[] = []
|
|
|
|
const pdf = await PDFJS.getDocument(data).promise
|
|
|
|
const canvas = document.createElement('canvas')
|
2024-07-16 12:36:18 +03:00
|
|
|
|
|
|
|
for (let i = 0; i < pdf.numPages; i++) {
|
2024-08-13 12:48:52 +03:00
|
|
|
const page = await pdf.getPage(i + 1)
|
|
|
|
const viewport = page.getViewport({ scale: SCALE })
|
|
|
|
const context = canvas.getContext('2d')
|
|
|
|
canvas.height = viewport.height
|
|
|
|
canvas.width = viewport.width
|
|
|
|
await page.render({ canvasContext: context!, viewport: viewport }).promise
|
|
|
|
images.push(canvas.toDataURL())
|
2024-07-16 12:36:18 +03:00
|
|
|
}
|
|
|
|
|
2024-08-13 12:48:52 +03:00
|
|
|
return Promise.resolve(
|
|
|
|
images.map((image) => {
|
|
|
|
return {
|
|
|
|
image,
|
|
|
|
drawnFields: []
|
|
|
|
}
|
|
|
|
})
|
|
|
|
)
|
2024-07-16 12:36:18 +03:00
|
|
|
}
|
|
|
|
|
2024-07-25 17:51:31 +03:00
|
|
|
/**
|
|
|
|
* Takes in individual pdf file and an object with Marks grouped by Page number
|
|
|
|
* Returns an array of encoded images where each image is a representation
|
|
|
|
* of a PDF page with completed and signed marks from all users
|
|
|
|
*/
|
2024-08-13 12:48:52 +03:00
|
|
|
const addMarks = async (
|
|
|
|
file: File,
|
|
|
|
marksPerPage: { [key: string]: Mark[] }
|
|
|
|
) => {
|
|
|
|
const p = await readPdf(file)
|
|
|
|
const pdf = await PDFJS.getDocument(p).promise
|
|
|
|
const canvas = document.createElement('canvas')
|
2024-07-25 17:51:31 +03:00
|
|
|
|
2024-08-13 12:48:52 +03:00
|
|
|
const images: string[] = []
|
2024-07-25 17:51:31 +03:00
|
|
|
|
2024-08-13 12:48:52 +03:00
|
|
|
for (let i = 0; i < pdf.numPages; i++) {
|
|
|
|
const page = await pdf.getPage(i + 1)
|
|
|
|
const viewport = page.getViewport({ scale: SCALE })
|
|
|
|
const context = canvas.getContext('2d')
|
|
|
|
canvas.height = viewport.height
|
|
|
|
canvas.width = viewport.width
|
|
|
|
await page.render({ canvasContext: context!, viewport: viewport }).promise
|
2024-07-25 17:51:31 +03:00
|
|
|
|
2024-08-16 12:01:41 +02:00
|
|
|
if (marksPerPage && Object.hasOwn(marksPerPage, i))
|
|
|
|
marksPerPage[i]?.forEach((mark) => draw(mark, context!))
|
2024-07-25 17:51:31 +03:00
|
|
|
|
2024-08-13 12:48:52 +03:00
|
|
|
images.push(canvas.toDataURL())
|
2024-07-25 17:51:31 +03:00
|
|
|
}
|
|
|
|
|
2024-08-13 12:48:52 +03:00
|
|
|
return Promise.resolve(images)
|
2024-07-25 17:51:31 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Utility to scale mark in line with the PDF-to-PNG scale
|
|
|
|
*/
|
|
|
|
const scaleMark = (mark: Mark): Mark => {
|
2024-08-13 12:48:52 +03:00
|
|
|
const { location } = mark
|
2024-07-25 17:51:31 +03:00
|
|
|
return {
|
|
|
|
...mark,
|
|
|
|
location: {
|
|
|
|
...location,
|
|
|
|
width: location.width * SCALE,
|
|
|
|
height: location.height * SCALE,
|
|
|
|
left: location.left * SCALE,
|
|
|
|
top: location.top * SCALE
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Utility to check if a Mark has value
|
|
|
|
* @param mark
|
|
|
|
*/
|
2024-08-13 12:48:52 +03:00
|
|
|
const hasValue = (mark: Mark): boolean => !!mark.value
|
2024-07-25 17:51:31 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Draws a Mark on a Canvas representation of a PDF Page
|
|
|
|
* @param mark to be drawn
|
|
|
|
* @param ctx a Canvas representation of a specific PDF Page
|
|
|
|
*/
|
|
|
|
const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
|
2024-08-13 12:48:52 +03:00
|
|
|
const { location } = mark
|
|
|
|
|
|
|
|
ctx!.font = FONT_SIZE + 'px ' + FONT_TYPE
|
|
|
|
ctx!.fillStyle = 'black'
|
|
|
|
const textMetrics = ctx!.measureText(mark.value!)
|
|
|
|
const textX = location.left + (location.width - textMetrics.width) / 2
|
|
|
|
const textY = location.top + (location.height + parseInt(ctx!.font)) / 2
|
|
|
|
ctx!.fillText(mark.value!, textX, textY)
|
2024-07-25 17:51:31 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Takes an array of encoded PDF pages and returns a blob that is a complete PDF file
|
|
|
|
* @param markedPdfPages
|
|
|
|
*/
|
|
|
|
const convertToPdfBlob = async (markedPdfPages: string[]): Promise<Blob> => {
|
2024-08-13 12:48:52 +03:00
|
|
|
const pdfDoc = await PDFDocument.create()
|
2024-07-25 17:51:31 +03:00
|
|
|
|
|
|
|
for (const page of markedPdfPages) {
|
|
|
|
const pngImage = await pdfDoc.embedPng(page)
|
|
|
|
const p = pdfDoc.addPage([pngImage.width, pngImage.height])
|
|
|
|
p.drawImage(pngImage, {
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
width: pngImage.width,
|
|
|
|
height: pngImage.height
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
const pdfBytes = await pdfDoc.save()
|
|
|
|
return new Blob([pdfBytes], { type: 'application/pdf' })
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Takes an ArrayBuffer of a PDF file and converts to Sigit's Internal Pdf File type
|
|
|
|
* @param arrayBuffer
|
|
|
|
* @param fileName
|
|
|
|
*/
|
2024-08-13 12:48:52 +03:00
|
|
|
const convertToPdfFile = async (
|
|
|
|
arrayBuffer: ArrayBuffer,
|
|
|
|
fileName: string
|
|
|
|
): Promise<PdfFile> => {
|
|
|
|
const file = toFile(arrayBuffer, fileName)
|
|
|
|
return toPdfFile(file)
|
2024-07-25 17:51:31 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param marks - an array of Marks
|
|
|
|
* @function hasValue removes any Mark without a property
|
|
|
|
* @function scaleMark scales remaining marks in line with SCALE
|
|
|
|
* @function byPage groups remaining Marks by their page marks.location.page
|
|
|
|
*/
|
2024-08-16 12:01:41 +02:00
|
|
|
const groupMarksByFileNamePage = (marks: Mark[]) => {
|
2024-07-25 17:51:31 +03:00
|
|
|
return marks
|
|
|
|
.filter(hasValue)
|
|
|
|
.map(scaleMark)
|
2024-08-16 12:01:41 +02:00
|
|
|
.reduce<{ [filename: string]: { [page: number]: Mark[] } }>(byPage, {})
|
2024-07-25 17:51:31 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A reducer callback that transforms an array of marks into an object grouped by the page number
|
|
|
|
* Can be replaced by Object.groupBy https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy
|
|
|
|
* when it is implemented in TypeScript
|
|
|
|
* Implementation is standard from the Array.prototype.reduce documentation
|
|
|
|
* @param obj - accumulator in the reducer callback
|
|
|
|
* @param mark - current value, i.e. Mark being examined
|
|
|
|
*/
|
2024-08-16 12:01:41 +02:00
|
|
|
const byPage = (
|
|
|
|
obj: { [filename: string]: { [page: number]: Mark[] } },
|
|
|
|
mark: Mark
|
|
|
|
) => {
|
|
|
|
const filename = mark.fileName
|
|
|
|
const pageNumber = mark.location.page
|
|
|
|
const pages = obj[filename] ?? {}
|
|
|
|
const marks = pages[pageNumber] ?? []
|
|
|
|
return {
|
|
|
|
...obj,
|
|
|
|
[filename]: {
|
|
|
|
...pages,
|
|
|
|
[pageNumber]: [...marks, mark]
|
|
|
|
}
|
|
|
|
}
|
2024-07-25 17:51:31 +03:00
|
|
|
}
|
|
|
|
|
2024-07-16 12:36:18 +03:00
|
|
|
export {
|
|
|
|
toFile,
|
|
|
|
toPdfFile,
|
2024-07-17 11:25:02 +03:00
|
|
|
toPdfFiles,
|
2024-07-25 17:51:31 +03:00
|
|
|
inPx,
|
|
|
|
convertToPdfFile,
|
|
|
|
addMarks,
|
|
|
|
convertToPdfBlob,
|
2024-08-16 12:01:41 +02:00
|
|
|
groupMarksByFileNamePage
|
|
|
|
}
|