import { MARK_TYPE_CONFIG } from '../components/MarkTypeStrategy/MarkStrategy.tsx' import { NostrController } from '../controllers/NostrController.ts' import store from '../store/store.ts' import { Meta } from '../types' import { PdfPage } from '../types/drawing.ts' import { MOST_COMMON_MEDIA_TYPES } from './const.ts' import { extractMarksFromSignedMeta } from './mark.ts' import { hexToNpub } from './nostr.ts' import { addMarks, groupMarksByFileNamePage, isPdf, pdfToImages } from './pdf.ts' import JSZip from 'jszip' export const getZipWithFiles = async ( meta: Meta, files: { [filename: string]: SigitFile } ): Promise => { const zip = new JSZip() const marks = extractMarksFromSignedMeta(meta) const marksByFileNamePage = groupMarksByFileNamePage(marks) for (const [fileName, file] of Object.entries(files)) { // Handle PDF Files, add marks if (file.isPdf && fileName in marksByFileNamePage) { const marksToAdd = marksByFileNamePage[fileName] if (meta.keys) { for (let i = 0; i < marks.length; i++) { const m = marks[i] const { sender, keys } = meta.keys const usersPubkey = store.getState().auth.usersPubkey! const usersNpub = hexToNpub(usersPubkey) if (usersNpub in keys) { const encryptionKey = await NostrController.getInstance() .nip04Decrypt(sender, keys[usersNpub]) .catch((err) => { console.log( 'An error occurred in decrypting encryption key', err ) return null }) try { const { fetchAndDecrypt } = MARK_TYPE_CONFIG[m.type] || {} if ( typeof fetchAndDecrypt === 'function' && m.value && encryptionKey ) { // Fetch and decrypt the original file const link = m.value.split('/') const decrypted = await fetchAndDecrypt(m.value, encryptionKey) // Save decrypted zip.file( `signatures/${link[link.length - 1]}.json`, new Blob([decrypted]) ) marks[i].value = decrypted } } catch (error) { console.error(`Error during mark fetchAndDecrypt phase`, error) } } } } const blob = await addMarks(file, marksToAdd) zip.file(`marked/${fileName}`, blob) } // Save original files zip.file(`files/${fileName}`, file) } return zip } /** * Converts a PDF ArrayBuffer to a generic PDF File * @param arrayBuffer of a PDF * @param fileName identifier of the pdf file * @param type optional file type (defaults to pdf) */ export const toFile = ( arrayBuffer: ArrayBuffer, fileName: string, type: string = 'application/pdf' ): File => { const blob = new Blob([arrayBuffer], { type }) return new File([blob], fileName, { type }) } export class SigitFile extends File { extension: string isPdf: boolean isImage: boolean pages?: PdfPage[] objectUrl?: string constructor(file: File) { super([file], file.name, { type: file.type }) this.isPdf = isPdf(this) this.isImage = isImage(this) this.extension = extractFileExtension(this.name) } async process() { if (this.isPdf) this.pages = await pdfToImages(await this.arrayBuffer()) if (this.isImage) this.objectUrl = URL.createObjectURL(this) } } export const getSigitFile = async (file: File) => { const sigitFile = new SigitFile(file) // Process sigit file // - generate pages for PDF files // - generate ObjectRL for image files await sigitFile.process() return sigitFile } /** * Takes an ArrayBuffer and converts to Sigit's Internal File type * @param arrayBuffer * @param fileName */ export const convertToSigitFile = async ( arrayBuffer: ArrayBuffer, fileName: string ): Promise => { const type = getMediaType(extractFileExtension(fileName)) const file = toFile(arrayBuffer, fileName, type) const sigitFile = await getSigitFile(file) return sigitFile } /** * @param fileNames - List of filenames to check * @returns List of extensions and if all are same */ export const extractFileExtensions = (fileNames: string[]) => { const extensions = fileNames.reduce((result: string[], file: string) => { const extension = file.split('.').pop() if (extension) { result.push(extension) } return result }, []) const isSame = extensions.every((ext) => ext === extensions[0]) return { extensions, isSame } } /** * @param fileName - Filename to check * @returns Extension string */ export const extractFileExtension = (fileName: string) => { const parts = fileName.split('.') return parts[parts.length - 1] } export const getMediaType = (extension: string) => { return MOST_COMMON_MEDIA_TYPES.get(extension) } export const isImage = (file: File) => { const validImageMediaTypes = [ 'image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/svg+xml', 'image/bmp', 'image/x-icon' ] return validImageMediaTypes.includes(file.type.toLowerCase()) }