From 6d881ccb45e440c343b768c6d26d6933b2a4b813 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 13 Aug 2024 12:48:52 +0300 Subject: [PATCH] feat(pdf-marking): adds file downloading functionality --- src/components/FileList/index.tsx | 41 +++++++ src/components/FileList/style.module.scss | 78 ++++++++++++ src/components/PDFView/PdfMarking.tsx | 54 +++++--- src/components/PDFView/index.tsx | 49 +++++--- src/components/PDFView/style.module.scss | 15 ++- src/pages/sign/index.tsx | 98 ++++++++++----- src/pages/verify/index.tsx | 31 ++--- src/types/file.ts | 8 ++ src/utils/const.ts | 2 + src/utils/file.ts | 24 ++++ src/utils/pdf.ts | 143 +++++++++++----------- src/utils/utils.ts | 16 ++- 12 files changed, 402 insertions(+), 157 deletions(-) create mode 100644 src/components/FileList/index.tsx create mode 100644 src/components/FileList/style.module.scss create mode 100644 src/types/file.ts create mode 100644 src/utils/file.ts diff --git a/src/components/FileList/index.tsx b/src/components/FileList/index.tsx new file mode 100644 index 0000000..ba69332 --- /dev/null +++ b/src/components/FileList/index.tsx @@ -0,0 +1,41 @@ +import { CurrentUserFile } from '../../types/file.ts' +import styles from './style.module.scss' +import { Button } from '@mui/material' + +interface FileListProps { + files: CurrentUserFile[] + currentFile: CurrentUserFile + setCurrentFile: (file: CurrentUserFile) => void + handleDownload: () => void +} + +const FileList = ({ + files, + currentFile, + setCurrentFile, + handleDownload +}: FileListProps) => { + const isActive = (file: CurrentUserFile) => file.id === currentFile.id + return ( +
+
+ + +
+ ) +} + +export default FileList diff --git a/src/components/FileList/style.module.scss b/src/components/FileList/style.module.scss new file mode 100644 index 0000000..bbf9311 --- /dev/null +++ b/src/components/FileList/style.module.scss @@ -0,0 +1,78 @@ +.container { + border-radius: 4px; + background: white; + padding: 15px; + display: flex; + flex-direction: column; + grid-gap: 0px; +} + +.files { + display: flex; + flex-direction: column; + width: 100%; + grid-gap: 15px; + max-height: 350px; + overflow: auto; + padding: 0 5px 0 0; + margin: 0 -5px 0 0; +} + +.files::-webkit-scrollbar { + width: 10px; +} + +.files::-webkit-scrollbar-track { + background-color: rgba(0,0,0,0.15); +} + +.files::-webkit-scrollbar-thumb { + max-width: 10px; + border-radius: 2px; + background-color: #4c82a3; + cursor: grab; +} + +.fileItem { + transition: ease 0.2s; + display: flex; + flex-direction: row; + grid-gap: 10px; + border-radius: 4px; + overflow: hidden; + background: #ffffff; + padding: 5px 10px; + align-items: center; + color: rgba(0,0,0,0.5); + cursor: pointer; + flex-grow: 1; + font-size: 16px; + font-weight: 500; +} + +.fileItem:hover { + transition: ease 0.2s; + background: #4c82a3; + color: white; + + &.active { + background: #4c82a3; + color: white; + } +} + +.fileName { + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + -webkit-line-clamp: 1; +} + +.fileNumber { + font-size: 14px; + font-weight: 500; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} \ No newline at end of file diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index b0e0748..fc49f79 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -1,24 +1,26 @@ import PdfView from './index.tsx' import MarkFormField from '../MarkFormField' -import { PdfFile } from '../../types/drawing.ts' import { CurrentUserMark, Mark } from '../../types/mark.ts' import React, { useState, useEffect } from 'react' import { findNextIncompleteCurrentUserMark, getUpdatedMark, - isCurrentUserMarksComplete, updateCurrentUserMarks } from '../../utils' import { EMPTY } from '../../utils/const.ts' import { Container } from '../Container' -import styles from '../../pages/sign/style.module.scss' +import signPageStyles from '../../pages/sign/style.module.scss' +import styles from './style.module.scss' +import { CurrentUserFile } from '../../types/file.ts' +import FileList from '../FileList' interface PdfMarkingProps { - files: { pdfFile: PdfFile; filename: string; hash: string | null }[] + files: CurrentUserFile[] currentUserMarks: CurrentUserMark[] setIsReadyToSign: (isReadyToSign: boolean) => void setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void setUpdatedMarks: (markToUpdate: Mark) => void + handleDownload: () => void } /** @@ -33,10 +35,12 @@ const PdfMarking = (props: PdfMarkingProps) => { currentUserMarks, setIsReadyToSign, setCurrentUserMarks, - setUpdatedMarks + setUpdatedMarks, + handleDownload } = props const [selectedMark, setSelectedMark] = useState(null) const [selectedMarkValue, setSelectedMarkValue] = useState('') + const [currentFile, setCurrentFile] = useState(null) useEffect(() => { if (selectedMark === null && currentUserMarks.length > 0) { @@ -47,6 +51,12 @@ const PdfMarking = (props: PdfMarkingProps) => { } }, [currentUserMarks, selectedMark]) + useEffect(() => { + if (currentFile === null && files.length > 0) { + setCurrentFile(files[0]) + } + }, [files, currentFile]) + const handleMarkClick = (id: number) => { const nextMark = currentUserMarks.find((mark) => mark.mark.id === id) setSelectedMark(nextMark!) @@ -101,16 +111,30 @@ const PdfMarking = (props: PdfMarkingProps) => { return ( <> - - {currentUserMarks?.length > 0 && ( - - )} + +
+
+ {currentFile !== null && ( + + )} +
+ {currentUserMarks?.length > 0 && ( +
+ +
+ )} +
{selectedMark !== null && ( void selectedMarkValue: string @@ -14,29 +14,38 @@ interface PdfViewProps { /** * Responsible for rendering Pdf files. */ -const PdfView = ({ files, currentUserMarks, handleMarkClick, selectedMarkValue, selectedMark }: PdfViewProps) => { - const filterByFile = (currentUserMarks: CurrentUserMark[], hash: string): CurrentUserMark[] => { - return currentUserMarks.filter((currentUserMark) => currentUserMark.mark.pdfFileHash === hash) +const PdfView = ({ + files, + currentUserMarks, + handleMarkClick, + selectedMarkValue, + selectedMark +}: PdfViewProps) => { + const filterByFile = ( + currentUserMarks: CurrentUserMark[], + hash: string + ): CurrentUserMark[] => { + return currentUserMarks.filter( + (currentUserMark) => currentUserMark.mark.pdfFileHash === hash + ) } return ( - { - files.map(({ pdfFile, hash }, i) => { - if (!hash) return - return ( - { + if (!hash) return + return ( + - ) - }) - } + ) + })} ) } -export default PdfView; \ No newline at end of file +export default PdfView diff --git a/src/components/PDFView/style.module.scss b/src/components/PDFView/style.module.scss index 2e6e519..3ebbc85 100644 --- a/src/components/PDFView/style.module.scss +++ b/src/components/PDFView/style.module.scss @@ -13,4 +13,17 @@ max-width: 100%; max-height: 100%; object-fit: contain; /* Ensure the image fits within the container */ -} \ No newline at end of file +} + +.container { + display: flex; + width: 100%; + flex-direction: column; +} + +.pdfView { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; +} diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 07c385e..02ac415 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -16,13 +16,16 @@ import { State } from '../../store/rootReducer' import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types' import { decryptArrayBuffer, - encryptArrayBuffer, extractMarksFromSignedMeta, + encryptArrayBuffer, + extractMarksFromSignedMeta, extractZipUrlAndEncryptionKey, generateEncryptionKey, - generateKeysFile, getFilesWithHashes, + generateKeysFile, + getCurrentUserFiles, getHash, hexToNpub, - isOnline, loadZip, + isOnline, + loadZip, now, npubToHex, parseJson, @@ -41,9 +44,12 @@ import { getLastSignersSig } from '../../utils/sign.ts' import { filterMarksByPubkey, getCurrentUserMarks, - isCurrentUserMarksComplete, updateMarks + isCurrentUserMarksComplete, + updateMarks } from '../../utils' import PdfMarking from '../../components/PDFView/PdfMarking.tsx' +import { getZipWithFiles } from '../../utils/file.ts' +import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts' enum SignedStatus { Fully_Signed, User_Is_Next_Signer, @@ -81,7 +87,7 @@ export const SignPage = () => { const [signers, setSigners] = useState<`npub1${string}`[]>([]) const [viewers, setViewers] = useState<`npub1${string}`[]>([]) - const [marks, setMarks] = useState([]) + const [marks, setMarks] = useState([]) const [creatorFileHashes, setCreatorFileHashes] = useState<{ [key: string]: string }>({}) @@ -100,8 +106,10 @@ export const SignPage = () => { const [authUrl, setAuthUrl] = useState() const nostrController = NostrController.getInstance() - const [currentUserMarks, setCurrentUserMarks] = useState([]); - const [isReadyToSign, setIsReadyToSign] = useState(false); + const [currentUserMarks, setCurrentUserMarks] = useState( + [] + ) + const [isReadyToSign, setIsReadyToSign] = useState(false) useEffect(() => { if (signers.length > 0) { @@ -192,13 +200,16 @@ export const SignPage = () => { setViewers(createSignatureContent.viewers) setCreatorFileHashes(createSignatureContent.fileHashes) setSubmittedBy(createSignatureEvent.pubkey) - setMarks(createSignatureContent.markConfig); + setMarks(createSignatureContent.markConfig) if (usersPubkey) { - const metaMarks = filterMarksByPubkey(createSignatureContent.markConfig, usersPubkey!) + const metaMarks = filterMarksByPubkey( + createSignatureContent.markConfig, + usersPubkey! + ) const signedMarks = extractMarksFromSignedMeta(meta) - const currentUserMarks = getCurrentUserMarks(metaMarks, signedMarks); - setCurrentUserMarks(currentUserMarks); + const currentUserMarks = getCurrentUserMarks(metaMarks, signedMarks) + setCurrentUserMarks(currentUserMarks) // setCurrentUserMark(findNextCurrentUserMark(currentUserMarks) || null) setIsReadyToSign(isCurrentUserMarksComplete(currentUserMarks)) } @@ -307,7 +318,7 @@ export const SignPage = () => { ) if (arrayBuffer) { - files[fileName] = await convertToPdfFile(arrayBuffer, fileName); + files[fileName] = await convertToPdfFile(arrayBuffer, fileName) const hash = await getHash(arrayBuffer) if (hash) { @@ -348,7 +359,7 @@ export const SignPage = () => { const decrypt = async (file: File) => { setLoadingSpinnerDesc('Decrypting file') - const zip = await loadZip(file); + const zip = await loadZip(file) if (!zip) return const parsedKeysJson = await parseKeysJson(zip) @@ -414,6 +425,27 @@ export const SignPage = () => { return null } + const handleDownload = async () => { + if (Object.entries(files).length === 0 || !meta || !usersPubkey) return + setLoadingSpinnerDesc('Generating file') + try { + const zip = await getZipWithFiles(meta, files) + const arrayBuffer = await zip.generateAsync({ + type: ARRAY_BUFFER, + compression: DEFLATE, + compressionOptions: { + level: 6 + } + }) + if (!arrayBuffer) return + const blob = new Blob([arrayBuffer]) + saveAs(blob, `exported-${now()}.sigit.zip`) + } catch (error: any) { + console.log('error in zip:>> ', error) + toast.error(error.message || 'Error occurred in generating zip file') + } + } + const handleDecryptedArrayBuffer = async (arrayBuffer: ArrayBuffer) => { const decryptedZipFile = new File([arrayBuffer], 'decrypted.zip') @@ -439,7 +471,7 @@ export const SignPage = () => { ) if (arrayBuffer) { - files[fileName] = await convertToPdfFile(arrayBuffer, fileName); + files[fileName] = await convertToPdfFile(arrayBuffer, fileName) const hash = await getHash(arrayBuffer) if (hash) { @@ -520,7 +552,10 @@ export const SignPage = () => { } // Sign the event for the meta file - const signEventForMeta = async (signerContent: { prevSig: string, marks: Mark[] }) => { + const signEventForMeta = async (signerContent: { + prevSig: string + marks: Mark[] + }) => { return await signEventForMetaFile( JSON.stringify(signerContent), nostrController, @@ -529,8 +564,8 @@ export const SignPage = () => { } const getSignerMarksForMeta = (): Mark[] | undefined => { - if (currentUserMarks.length === 0) return; - return currentUserMarks.map(( { mark }: CurrentUserMark) => mark); + if (currentUserMarks.length === 0) return + return currentUserMarks.map(({ mark }: CurrentUserMark) => mark) } // Update the meta signatures @@ -600,13 +635,9 @@ export const SignPage = () => { if (!arraybuffer) return null - return new File( - [new Blob([arraybuffer])], - `${unixNow}.sigit.zip`, - { - type: 'application/zip' - } - ) + return new File([new Blob([arraybuffer])], `${unixNow}.sigit.zip`, { + type: 'application/zip' + }) } // Handle errors during zip file generation @@ -694,7 +725,7 @@ export const SignPage = () => { setIsLoading(true) setLoadingSpinnerDesc('Signing nostr event') - if (!meta) return; + if (!meta) return const prevSig = getLastSignersSig(meta, signers) if (!prevSig) return @@ -918,11 +949,14 @@ export const SignPage = () => { ) } - return + return ( + + ) } diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 6290c02..b39fe74 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -23,14 +23,17 @@ import { SignedEventContent } from '../../types' import { - decryptArrayBuffer, extractMarksFromSignedMeta, + decryptArrayBuffer, + extractMarksFromSignedMeta, extractZipUrlAndEncryptionKey, getHash, - hexToNpub, now, + hexToNpub, + now, npubToHex, parseJson, readContentOfZipEntry, - shorten, signEventForMetaFile + shorten, + signEventForMetaFile } from '../../utils' import styles from './style.module.scss' import { Cancel, CheckCircle } from '@mui/icons-material' @@ -41,7 +44,7 @@ import { addMarks, convertToPdfBlob, convertToPdfFile, - groupMarksByPage, + groupMarksByPage } from '../../utils/pdf.ts' import { State } from '../../store/rootReducer.ts' import { useSelector } from 'react-redux' @@ -78,7 +81,7 @@ export const VerifyPage = () => { const [currentFileHashes, setCurrentFileHashes] = useState<{ [key: string]: string | null }>({}) - const [files, setFiles] = useState<{ [filename: string]: PdfFile}>({}) + const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>( {} @@ -155,7 +158,10 @@ export const VerifyPage = () => { ) if (arrayBuffer) { - files[fileName] = await convertToPdfFile(arrayBuffer, fileName!) + files[fileName] = await convertToPdfFile( + arrayBuffer, + fileName! + ) const hash = await getHash(arrayBuffer) if (hash) { @@ -169,7 +175,6 @@ export const VerifyPage = () => { setCurrentFileHashes(fileHashes) setFiles(files) - setSigners(createSignatureContent.signers) setViewers(createSignatureContent.viewers) setCreatorFileHashes(createSignatureContent.fileHashes) @@ -177,8 +182,6 @@ export const VerifyPage = () => { setMeta(metaInNavState) setIsLoading(false) - - } }) .catch((err) => { @@ -381,7 +384,7 @@ export const VerifyPage = () => { } const handleExport = async () => { - if (Object.entries(files).length === 0 ||!meta ||!usersPubkey) return; + if (Object.entries(files).length === 0 || !meta || !usersPubkey) return const usersNpub = hexToNpub(usersPubkey) if ( @@ -395,10 +398,8 @@ export const VerifyPage = () => { setIsLoading(true) setLoadingSpinnerDesc('Signing nostr event') - if (!meta) return; - const prevSig = getLastSignersSig(meta, signers) - if (!prevSig) return; + if (!prevSig) return const signedEvent = await signEventForMetaFile( JSON.stringify({ prevSig }), @@ -406,10 +407,10 @@ export const VerifyPage = () => { setIsLoading ) - if (!signedEvent) return; + if (!signedEvent) return const exportSignature = JSON.stringify(signedEvent, null, 2) - const updatedMeta = {...meta, exportSignature } + const updatedMeta = { ...meta, exportSignature } const stringifiedMeta = JSON.stringify(updatedMeta, null, 2) const zip = new JSZip() diff --git a/src/types/file.ts b/src/types/file.ts new file mode 100644 index 0000000..0f2c127 --- /dev/null +++ b/src/types/file.ts @@ -0,0 +1,8 @@ +import { PdfFile } from './drawing.ts' + +export interface CurrentUserFile { + id: number + pdfFile: PdfFile + filename: string + hash?: string +} diff --git a/src/utils/const.ts b/src/utils/const.ts index 2a97dfe..511f418 100644 --- a/src/utils/const.ts +++ b/src/utils/const.ts @@ -6,3 +6,5 @@ export const MARK_TYPE_TRANSLATION: { [key: string]: string } = { } export const SIGN: string = 'Sign' export const NEXT: string = 'Next' +export const ARRAY_BUFFER = 'arraybuffer' +export const DEFLATE = 'DEFLATE' diff --git a/src/utils/file.ts b/src/utils/file.ts new file mode 100644 index 0000000..94308d5 --- /dev/null +++ b/src/utils/file.ts @@ -0,0 +1,24 @@ +import { Meta } from '../types' +import { extractMarksFromSignedMeta } from './mark.ts' +import { addMarks, convertToPdfBlob, groupMarksByPage } from './pdf.ts' +import JSZip from 'jszip' +import { PdfFile } from '../types/drawing.ts' + +const getZipWithFiles = async ( + meta: Meta, + files: { [filename: string]: PdfFile } +): Promise => { + const zip = new JSZip() + const marks = extractMarksFromSignedMeta(meta) + const marksByPage = groupMarksByPage(marks) + + for (const [fileName, pdf] of Object.entries(files)) { + const pages = await addMarks(pdf.file, marksByPage) + const blob = await convertToPdfBlob(pages) + zip.file(`/files/${fileName}`, blob) + } + + return zip +} + +export { getZipWithFiles } diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index 70b6539..a684d78 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -3,13 +3,14 @@ import * as PDFJS from 'pdfjs-dist' import { PDFDocument } from 'pdf-lib' import { Mark } from '../types/mark.ts' -PDFJS.GlobalWorkerOptions.workerSrc = 'node_modules/pdfjs-dist/build/pdf.worker.mjs'; +PDFJS.GlobalWorkerOptions.workerSrc = + 'node_modules/pdfjs-dist/build/pdf.worker.mjs' /** * Scale between the PDF page's natural size and rendered size * @constant {number} */ -const SCALE: number = 3; +const SCALE: number = 3 /** * 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 @@ -17,20 +18,20 @@ const SCALE: number = 3; * This should be fixed going forward. * Switching to PDF-Lib will most likely make this problem redundant. */ -const FONT_SIZE: number = 40; +const FONT_SIZE: number = 40 /** * Current font type used when generating a PDF. */ -const FONT_TYPE: string = 'Arial'; +const FONT_TYPE: string = 'Arial' /** * Converts a PDF ArrayBuffer to a generic PDF File * @param arrayBuffer of a PDF * @param fileName identifier of the pdf file */ -const toFile = (arrayBuffer: ArrayBuffer, fileName: string) : File => { - const blob = new Blob([arrayBuffer], { type: "application/pdf" }); - return new File([blob], fileName, { type: "application/pdf" }); +const toFile = (arrayBuffer: ArrayBuffer, fileName: string): File => { + const blob = new Blob([arrayBuffer], { type: 'application/pdf' }) + return new File([blob], fileName, { type: 'application/pdf' }) } /** @@ -50,42 +51,40 @@ const toPdfFile = async (file: File): Promise => { * @return PdfFile[] - an array of Sigit's internal Pdf File type */ const toPdfFiles = async (selectedFiles: File[]): Promise => { - return Promise.all(selectedFiles - .filter(isPdf) - .map(toPdfFile)); + return Promise.all(selectedFiles.filter(isPdf).map(toPdfFile)) } /** * A utility that transforms a drawing coordinate number into a CSS-compatible string * @param coordinate */ -const inPx = (coordinate: number): string => `${coordinate}px`; +const inPx = (coordinate: number): string => `${coordinate}px` /** * A utility that checks if a given file is of the pdf type * @param file */ -const isPdf = (file: File) => file.type.toLowerCase().includes('pdf'); +const isPdf = (file: File) => file.type.toLowerCase().includes('pdf') /** * Reads the pdf file binaries */ const readPdf = (file: File): Promise => { return new Promise((resolve, reject) => { - const reader = new FileReader(); + const reader = new FileReader() reader.onload = (e: any) => { const data = e.target.result resolve(data) - }; + } reader.onerror = (err) => { console.error('err', err) reject(err) - }; + } - reader.readAsDataURL(file); + reader.readAsDataURL(file) }) } @@ -94,26 +93,28 @@ const readPdf = (file: File): Promise => { * @param data pdf file bytes */ const pdfToImages = async (data: any): Promise => { - const images: string[] = []; - const pdf = await PDFJS.getDocument(data).promise; - const canvas = document.createElement("canvas"); + const images: string[] = [] + const pdf = await PDFJS.getDocument(data).promise + const canvas = document.createElement('canvas') 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; - images.push(canvas.toDataURL()); + 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()) } - return Promise.resolve(images.map((image) => { - return { - image, - drawnFields: [] - } - })) + return Promise.resolve( + images.map((image) => { + return { + image, + drawnFields: [] + } + }) + ) } /** @@ -121,34 +122,37 @@ const pdfToImages = async (data: any): Promise => { * Returns an array of encoded images where each image is a representation * of a PDF page with completed and signed marks from all users */ -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"); +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') - const images: string[] = []; + const images: string[] = [] - 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; + 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 - marksPerPage[i].forEach(mark => draw(mark, context!)) + marksPerPage[i]?.forEach((mark) => draw(mark, context!)) - images.push(canvas.toDataURL()); + images.push(canvas.toDataURL()) } - return Promise.resolve(images); + return Promise.resolve(images) } /** * Utility to scale mark in line with the PDF-to-PNG scale */ const scaleMark = (mark: Mark): Mark => { - const { location } = mark; + const { location } = mark return { ...mark, location: { @@ -165,7 +169,7 @@ const scaleMark = (mark: Mark): Mark => { * Utility to check if a Mark has value * @param mark */ -const hasValue = (mark: Mark): boolean => !!mark.value; +const hasValue = (mark: Mark): boolean => !!mark.value /** * Draws a Mark on a Canvas representation of a PDF Page @@ -173,14 +177,14 @@ const hasValue = (mark: Mark): boolean => !!mark.value; * @param ctx a Canvas representation of a specific PDF Page */ const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => { - const { location } = mark; + 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); + 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) } /** @@ -188,7 +192,7 @@ const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => { * @param markedPdfPages */ const convertToPdfBlob = async (markedPdfPages: string[]): Promise => { - const pdfDoc = await PDFDocument.create(); + const pdfDoc = await PDFDocument.create() for (const page of markedPdfPages) { const pngImage = await pdfDoc.embedPng(page) @@ -203,7 +207,6 @@ const convertToPdfBlob = async (markedPdfPages: string[]): Promise => { const pdfBytes = await pdfDoc.save() return new Blob([pdfBytes], { type: 'application/pdf' }) - } /** @@ -211,9 +214,12 @@ const convertToPdfBlob = async (markedPdfPages: string[]): Promise => { * @param arrayBuffer * @param fileName */ -const convertToPdfFile = async (arrayBuffer: ArrayBuffer, fileName: string): Promise => { - const file = toFile(arrayBuffer, fileName); - return toPdfFile(file); +const convertToPdfFile = async ( + arrayBuffer: ArrayBuffer, + fileName: string +): Promise => { + const file = toFile(arrayBuffer, fileName) + return toPdfFile(file) } /** @@ -226,7 +232,7 @@ const groupMarksByPage = (marks: Mark[]) => { return marks .filter(hasValue) .map(scaleMark) - .reduce<{[key: number]: Mark[]}>(byPage, {}) + .reduce<{ [key: number]: Mark[] }>(byPage, {}) } /** @@ -237,11 +243,10 @@ const groupMarksByPage = (marks: Mark[]) => { * @param obj - accumulator in the reducer callback * @param mark - current value, i.e. Mark being examined */ -const byPage = (obj: { [key: number]: Mark[]}, mark: Mark) => { - const key = mark.location.page; - const curGroup = obj[key] ?? []; - return { ...obj, [key]: [...curGroup, mark] - } +const byPage = (obj: { [key: number]: Mark[] }, mark: Mark) => { + const key = mark.location.page + const curGroup = obj[key] ?? [] + return { ...obj, [key]: [...curGroup, mark] } } export { @@ -252,5 +257,5 @@ export { convertToPdfFile, addMarks, convertToPdfBlob, - groupMarksByPage, -} \ No newline at end of file + groupMarksByPage +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index ed830a2..e201c41 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,4 +1,5 @@ import { PdfFile } from '../types/drawing.ts' +import { CurrentUserFile } from '../types/file.ts' export const compareObjects = ( obj1: object | null | undefined, @@ -72,11 +73,16 @@ export const timeout = (ms: number = 60000) => { * @param files * @param fileHashes */ -export const getFilesWithHashes = ( - files: { [filename: string ]: PdfFile }, +export const getCurrentUserFiles = ( + files: { [filename: string]: PdfFile }, fileHashes: { [key: string]: string | null } - ) => { - return Object.entries(files).map(([filename, pdfFile]) => { - return { pdfFile, filename, hash: fileHashes[filename] } +): CurrentUserFile[] => { + return Object.entries(files).map(([filename, pdfFile], index) => { + return { + pdfFile, + filename, + id: index + 1, + ...(!!fileHashes[filename] && { hash: fileHashes[filename]! }) + } }) }