PDF Markings #114
@ -32,6 +32,7 @@
|
|||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mui-file-input": "4.0.4",
|
"mui-file-input": "4.0.4",
|
||||||
"nostr-tools": "2.7.0",
|
"nostr-tools": "2.7.0",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
"pdfjs-dist": "^4.4.168",
|
"pdfjs-dist": "^4.4.168",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dnd": "16.0.1",
|
"react-dnd": "16.0.1",
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Box, Button, FormControl, InputLabel, TextField, Typography } from '@mui/material'
|
import { Box, Button, Typography } from '@mui/material'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import saveAs from 'file-saver'
|
import saveAs from 'file-saver'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import _, { set } from 'lodash'
|
import _ from 'lodash'
|
||||||
import { MuiFileInput } from 'mui-file-input'
|
import { MuiFileInput } from 'mui-file-input'
|
||||||
import { Event, verifyEvent } from 'nostr-tools'
|
import { Event, verifyEvent } from 'nostr-tools'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@ -13,7 +13,7 @@ import { LoadingSpinner } from '../../components/LoadingSpinner'
|
|||||||
import { NostrController } from '../../controllers'
|
import { NostrController } from '../../controllers'
|
||||||
import { appPublicRoutes } from '../../routes'
|
import { appPublicRoutes } from '../../routes'
|
||||||
import { State } from '../../store/rootReducer'
|
import { State } from '../../store/rootReducer'
|
||||||
import { CreateSignatureEventContent, Meta, SignedEvent, UserRole } from '../../types'
|
import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
|
||||||
import {
|
import {
|
||||||
decryptArrayBuffer,
|
decryptArrayBuffer,
|
||||||
encryptArrayBuffer,
|
encryptArrayBuffer,
|
||||||
@ -34,10 +34,11 @@ import {
|
|||||||
import { DisplayMeta } from './internal/displayMeta'
|
import { DisplayMeta } from './internal/displayMeta'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { PdfFile } from '../../types/drawing.ts'
|
import { PdfFile } from '../../types/drawing.ts'
|
||||||
import { toFile, toPdfFile } from '../../utils/pdf.ts'
|
import { convertToPdfFile } from '../../utils/pdf.ts'
|
||||||
import PdfView from '../../components/PDFView'
|
import PdfView from '../../components/PDFView'
|
||||||
import { CurrentUserMark, Mark, MarkConfig, MarkConfigDetails, User } from '../../types/mark.ts'
|
import { CurrentUserMark, Mark, MarkConfig } from '../../types/mark.ts'
|
||||||
import MarkFormField from './MarkFormField.tsx'
|
import MarkFormField from './MarkFormField.tsx'
|
||||||
|
import { getLastSignersSig } from '../../utils/sign.ts'
|
||||||
enum SignedStatus {
|
enum SignedStatus {
|
||||||
Fully_Signed,
|
Fully_Signed,
|
||||||
User_Is_Next_Signer,
|
User_Is_Next_Signer,
|
||||||
@ -98,7 +99,6 @@ export const SignPage = () => {
|
|||||||
const [currentUserMarks, setCurrentUserMarks] = useState<CurrentUserMark[]>([]);
|
const [currentUserMarks, setCurrentUserMarks] = useState<CurrentUserMark[]>([]);
|
||||||
const [currentMarkValue, setCurrentMarkValue] = useState<string>('');
|
const [currentMarkValue, setCurrentMarkValue] = useState<string>('');
|
||||||
const [isMarksCompleted, setIsMarksCompleted] = useState(false);
|
const [isMarksCompleted, setIsMarksCompleted] = useState(false);
|
||||||
const [isLastUserMark, setIsLastUserMark] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (signers.length > 0) {
|
if (signers.length > 0) {
|
||||||
@ -330,11 +330,6 @@ export const SignPage = () => {
|
|||||||
setCurrentFileHashes(fileHashes)
|
setCurrentFileHashes(fileHashes)
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertToPdfFile = async (arrayBuffer: ArrayBuffer, fileName: string) => {
|
|
||||||
const file = toFile(arrayBuffer, fileName);
|
|
||||||
return toPdfFile(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseKeysJson = async (zip: JSZip) => {
|
const parseKeysJson = async (zip: JSZip) => {
|
||||||
const keysFileContent = await readContentOfZipEntry(
|
const keysFileContent = await readContentOfZipEntry(
|
||||||
zip,
|
zip,
|
||||||
@ -491,8 +486,6 @@ export const SignPage = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log('parsed meta: ', parsedMetaJson)
|
|
||||||
|
|
||||||
setMeta(parsedMetaJson)
|
setMeta(parsedMetaJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -616,7 +609,7 @@ export const SignPage = () => {
|
|||||||
encryptionKey: string
|
encryptionKey: string
|
||||||
): Promise<File | null> => {
|
): Promise<File | null> => {
|
||||||
// Get the current timestamp in seconds
|
// Get the current timestamp in seconds
|
||||||
const unixNow = Math.floor(Date.now() / 1000)
|
const unixNow = now()
|
||||||
const blob = new Blob([encryptedArrayBuffer])
|
const blob = new Blob([encryptedArrayBuffer])
|
||||||
// Create a File object with the Blob data
|
// Create a File object with the Blob data
|
||||||
const file = new File([blob], `compressed.sigit`, {
|
const file = new File([blob], `compressed.sigit`, {
|
||||||
@ -763,7 +756,9 @@ export const SignPage = () => {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setLoadingSpinnerDesc('Signing nostr event')
|
setLoadingSpinnerDesc('Signing nostr event')
|
||||||
|
|
||||||
const prevSig = getLastSignersSig()
|
if (!meta) return;
|
||||||
|
|
||||||
|
const prevSig = getLastSignersSig(meta, signers)
|
||||||
if (!prevSig) return
|
if (!prevSig) return
|
||||||
|
|
||||||
const signedEvent = await signEventForMetaFile(
|
const signedEvent = await signEventForMetaFile(
|
||||||
@ -813,7 +808,7 @@ export const SignPage = () => {
|
|||||||
if (!arrayBuffer) return
|
if (!arrayBuffer) return
|
||||||
|
|
||||||
const blob = new Blob([arrayBuffer])
|
const blob = new Blob([arrayBuffer])
|
||||||
const unixNow = Math.floor(Date.now() / 1000)
|
const unixNow = now()
|
||||||
saveAs(blob, `exported-${unixNow}.sigit.zip`)
|
saveAs(blob, `exported-${unixNow}.sigit.zip`)
|
||||||
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
@ -878,7 +873,7 @@ export const SignPage = () => {
|
|||||||
const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
|
const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
|
||||||
|
|
||||||
if (!finalZipFile) return
|
if (!finalZipFile) return
|
||||||
const unixNow = Math.floor(Date.now() / 1000)
|
const unixNow = now()
|
||||||
saveAs(finalZipFile, `exported-${unixNow}.sigit.zip`)
|
saveAs(finalZipFile, `exported-${unixNow}.sigit.zip`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -915,37 +910,6 @@ export const SignPage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This function returns the signature of last signer
|
|
||||||
* It will be used in the content of export signature's signedEvent
|
|
||||||
*/
|
|
||||||
const getLastSignersSig = () => {
|
|
||||||
if (!meta) return null
|
|
||||||
|
|
||||||
// if there're no signers then use creator's signature
|
|
||||||
if (signers.length === 0) {
|
|
||||||
try {
|
|
||||||
const createSignatureEvent: Event = JSON.parse(meta.createSignature)
|
|
||||||
return createSignatureEvent.sig
|
|
||||||
} catch (error) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get last signer
|
|
||||||
const lastSigner = signers[signers.length - 1]
|
|
||||||
|
|
||||||
// get the signature of last signer
|
|
||||||
try {
|
|
||||||
const lastSignatureEvent: Event = JSON.parse(
|
|
||||||
meta.docSignatures[lastSigner]
|
|
||||||
)
|
|
||||||
return lastSignatureEvent.sig
|
|
||||||
} catch (error) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authUrl) {
|
if (authUrl) {
|
||||||
return (
|
return (
|
||||||
<iframe
|
<iframe
|
||||||
|
@ -15,7 +15,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { UserComponent } from '../../components/username'
|
import { UserComponent } from '../../components/username'
|
||||||
import { MetadataController } from '../../controllers'
|
import { MetadataController, NostrController } from '../../controllers'
|
||||||
import {
|
import {
|
||||||
CreateSignatureEventContent,
|
CreateSignatureEventContent,
|
||||||
Meta,
|
Meta,
|
||||||
@ -23,19 +23,30 @@ import {
|
|||||||
SignedEventContent
|
SignedEventContent
|
||||||
} from '../../types'
|
} from '../../types'
|
||||||
import {
|
import {
|
||||||
decryptArrayBuffer,
|
decryptArrayBuffer, extractMarksFromSignedMeta,
|
||||||
extractZipUrlAndEncryptionKey,
|
extractZipUrlAndEncryptionKey,
|
||||||
getHash,
|
getHash,
|
||||||
hexToNpub,
|
hexToNpub, now,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
parseJson,
|
parseJson,
|
||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
shorten
|
shorten, signEventForMetaFile
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { Cancel, CheckCircle } from '@mui/icons-material'
|
import { Cancel, CheckCircle } from '@mui/icons-material'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { PdfFile } from '../../types/drawing.ts'
|
||||||
|
import {
|
||||||
|
addMarks,
|
||||||
|
convertToPdfBlob,
|
||||||
|
convertToPdfFile,
|
||||||
|
groupMarksByPage,
|
||||||
|
} from '../../utils/pdf.ts'
|
||||||
|
import { State } from '../../store/rootReducer.ts'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { getLastSignersSig } from '../../utils/sign.ts'
|
||||||
|
import { saveAs } from 'file-saver'
|
||||||
|
|
||||||
export const VerifyPage = () => {
|
export const VerifyPage = () => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
@ -66,10 +77,13 @@ export const VerifyPage = () => {
|
|||||||
const [currentFileHashes, setCurrentFileHashes] = useState<{
|
const [currentFileHashes, setCurrentFileHashes] = useState<{
|
||||||
[key: string]: string | null
|
[key: string]: string | null
|
||||||
}>({})
|
}>({})
|
||||||
|
const [files, setFiles] = useState<{ [filename: string]: PdfFile}>({})
|
||||||
|
|
||||||
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
|
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
||||||
|
const nostrController = NostrController.getInstance()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (uploadedZip) {
|
if (uploadedZip) {
|
||||||
@ -124,6 +138,7 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
if (!zip) return
|
if (!zip) return
|
||||||
|
|
||||||
|
const files: { [filename: string]: PdfFile } = {}
|
||||||
const fileHashes: { [key: string]: string | null } = {}
|
const fileHashes: { [key: string]: string | null } = {}
|
||||||
const fileNames = Object.values(zip.files).map(
|
const fileNames = Object.values(zip.files).map(
|
||||||
(entry) => entry.name
|
(entry) => entry.name
|
||||||
@ -139,6 +154,7 @@ export const VerifyPage = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (arrayBuffer) {
|
if (arrayBuffer) {
|
||||||
|
files[fileName] = await convertToPdfFile(arrayBuffer, fileName!)
|
||||||
const hash = await getHash(arrayBuffer)
|
const hash = await getHash(arrayBuffer)
|
||||||
|
|
||||||
if (hash) {
|
if (hash) {
|
||||||
@ -150,6 +166,8 @@ export const VerifyPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setCurrentFileHashes(fileHashes)
|
setCurrentFileHashes(fileHashes)
|
||||||
|
setFiles(files)
|
||||||
|
|
||||||
|
|
||||||
setSigners(createSignatureContent.signers)
|
setSigners(createSignatureContent.signers)
|
||||||
setViewers(createSignatureContent.viewers)
|
setViewers(createSignatureContent.viewers)
|
||||||
@ -158,6 +176,8 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
setMeta(metaInNavState)
|
setMeta(metaInNavState)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -359,6 +379,77 @@ export const VerifyPage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
// const proverbialUrls = await addMarksV2(Object.values(files)[0].file, meta!)
|
||||||
|
// await convertToFinalPdf(proverbialUrls)
|
||||||
|
|
||||||
|
|
||||||
|
if (Object.entries(files).length === 0 ||!meta ||!usersPubkey) return;
|
||||||
|
|
||||||
|
const usersNpub = hexToNpub(usersPubkey)
|
||||||
|
if (
|
||||||
|
!signers.includes(usersNpub) &&
|
||||||
|
!viewers.includes(usersNpub) &&
|
||||||
|
submittedBy !== usersNpub
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setLoadingSpinnerDesc('Signing nostr event')
|
||||||
|
|
||||||
|
if (!meta) return;
|
||||||
|
|
||||||
|
const prevSig = getLastSignersSig(meta, signers)
|
||||||
|
if (!prevSig) return;
|
||||||
|
|
||||||
|
const signedEvent = await signEventForMetaFile(
|
||||||
|
JSON.stringify({ prevSig }),
|
||||||
|
nostrController,
|
||||||
|
setIsLoading
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!signedEvent) return;
|
||||||
|
|
||||||
|
const exportSignature = JSON.stringify(signedEvent, null, 2)
|
||||||
|
const updatedMeta = {...meta, exportSignature }
|
||||||
|
const stringifiedMeta = JSON.stringify(updatedMeta, null, 2)
|
||||||
|
|
||||||
|
const zip = new JSZip()
|
||||||
|
zip.file('meta.json', stringifiedMeta)
|
||||||
|
|
||||||
|
const marks = extractMarksFromSignedMeta(updatedMeta)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await zip
|
||||||
|
.generateAsync({
|
||||||
|
type: 'arraybuffer',
|
||||||
|
compression: 'DEFLATE',
|
||||||
|
compressionOptions: {
|
||||||
|
level: 6
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('err in zip:>> ', err)
|
||||||
|
setIsLoading(false)
|
||||||
|
toast.error(err.message || 'Error occurred in generating zip file')
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!arrayBuffer) return
|
||||||
|
|
||||||
|
const blob = new Blob([arrayBuffer])
|
||||||
|
saveAs(blob, `exported-${now()}.sigit.zip`)
|
||||||
|
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
const displayUser = (pubkey: string, verifySignature = false) => {
|
const displayUser = (pubkey: string, verifySignature = false) => {
|
||||||
const profile = metadata[pubkey]
|
const profile = metadata[pubkey]
|
||||||
|
|
||||||
@ -521,6 +612,11 @@ export const VerifyPage = () => {
|
|||||||
Exported By
|
Exported By
|
||||||
</Typography>
|
</Typography>
|
||||||
{displayExportedBy()}
|
{displayExportedBy()}
|
||||||
|
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Button onClick={handleExport} variant="contained">
|
||||||
|
Export Sigit
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
{signers.length > 0 && (
|
{signers.length > 0 && (
|
||||||
|
@ -30,7 +30,7 @@ export interface CreateSignatureEventContent {
|
|||||||
|
|
||||||
export interface SignedEventContent {
|
export interface SignedEventContent {
|
||||||
prevSig: string
|
prevSig: string
|
||||||
markConfig: Mark[]
|
marks: Mark[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Sigit {
|
export interface Sigit {
|
||||||
|
@ -12,10 +12,11 @@ import { toast } from 'react-toastify'
|
|||||||
import { NostrController } from '../controllers'
|
import { NostrController } from '../controllers'
|
||||||
import { AuthState } from '../store/auth/types'
|
import { AuthState } from '../store/auth/types'
|
||||||
import store from '../store/store'
|
import store from '../store/store'
|
||||||
import { CreateSignatureEventContent, Meta } from '../types'
|
import { CreateSignatureEventContent, Meta, SignedEventContent } from '../types'
|
||||||
import { hexToNpub, now } from './nostr'
|
import { hexToNpub, now } from './nostr'
|
||||||
import { parseJson } from './string'
|
import { parseJson } from './string'
|
||||||
import { hexToBytes } from '@noble/hashes/utils'
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
|
import { Mark } from '../types/mark.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads a file to a file storage service.
|
* Uploads a file to a file storage service.
|
||||||
@ -264,3 +265,10 @@ export const extractZipUrlAndEncryptionKey = async (meta: Meta) => {
|
|||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const extractMarksFromSignedMeta = (meta: Meta): Mark[] => {
|
||||||
|
return Object.values(meta.docSignatures)
|
||||||
|
.map((val: string) => JSON.parse(val as string))
|
||||||
|
.map((val: Event) => JSON.parse(val.content))
|
||||||
|
.flatMap((val: SignedEventContent) => val.marks);
|
||||||
|
}
|
||||||
|
178
src/utils/pdf.ts
178
src/utils/pdf.ts
@ -1,33 +1,64 @@
|
|||||||
import { PdfFile, PdfPage } from '../types/drawing.ts'
|
import { PdfFile, PdfPage } from '../types/drawing.ts'
|
||||||
import * as PDFJS from 'pdfjs-dist'
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 toFile = (arrayBuffer: ArrayBuffer, fileName: string) : File => {
|
||||||
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
||||||
return new File([blob], fileName, { type: "application/pdf" });
|
return new File([blob], fileName, { type: "application/pdf" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a generic PDF File to Sigit's internal Pdf File type
|
||||||
|
* @param {File} file
|
||||||
|
* @return {PdfFile} Sigit's internal PDF File type
|
||||||
|
*/
|
||||||
const toPdfFile = async (file: File): Promise<PdfFile> => {
|
const toPdfFile = async (file: File): Promise<PdfFile> => {
|
||||||
const data = await readPdf(file)
|
const data = await readPdf(file)
|
||||||
const pages = await pdfToImages(data)
|
const pages = await pdfToImages(data)
|
||||||
return { file, pages, expanded: false }
|
return { file, pages, expanded: false }
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
const toPdfFiles = async (selectedFiles: File[]): Promise<PdfFile[]> => {
|
const toPdfFiles = async (selectedFiles: File[]): Promise<PdfFile[]> => {
|
||||||
return Promise.all(
|
return Promise.all(selectedFiles
|
||||||
selectedFiles
|
.filter(isPdf)
|
||||||
.filter(isPdf)
|
.map(toPdfFile));
|
||||||
.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
|
* Reads the pdf file binaries
|
||||||
*/
|
*/
|
||||||
const readPdf = (file: File) => {
|
const readPdf = (file: File): Promise<string> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
@ -57,7 +88,7 @@ const pdfToImages = async (data: any): Promise<PdfPage[]> => {
|
|||||||
|
|
||||||
for (let i = 0; i < pdf.numPages; i++) {
|
for (let i = 0; i < pdf.numPages; i++) {
|
||||||
const page = await pdf.getPage(i + 1);
|
const page = await pdf.getPage(i + 1);
|
||||||
const viewport = page.getViewport({ scale: 3 });
|
const viewport = page.getViewport({ scale: SCALE });
|
||||||
const context = canvas.getContext("2d");
|
const context = canvas.getContext("2d");
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
@ -73,9 +104,140 @@ const pdfToImages = async (data: any): Promise<PdfPage[]> => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
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[] = [];
|
||||||
|
|
||||||
|
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!))
|
||||||
|
|
||||||
|
images.push(canvas.toDataURL());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(images);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to scale mark in line with the PDF-to-PNG scale
|
||||||
|
*/
|
||||||
|
const scaleMark = (mark: Mark): Mark => {
|
||||||
|
const { location } = mark;
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
const hasValue = (mark: Mark): boolean => !!mark.value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) => {
|
||||||
|
const { location } = mark;
|
||||||
|
ctx!.font = '20px Arial';
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> => {
|
||||||
|
const pdfDoc = await PDFDocument.create();
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
const convertToPdfFile = async (arrayBuffer: ArrayBuffer, fileName: string): Promise<PdfFile> => {
|
||||||
|
const file = toFile(arrayBuffer, fileName);
|
||||||
|
return toPdfFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
const groupMarksByPage = (marks: Mark[]) => {
|
||||||
|
return marks
|
||||||
|
.filter(hasValue)
|
||||||
|
.map(scaleMark)
|
||||||
|
.reduce<{[key: number]: Mark[]}>(byPage, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
const byPage = (obj: { [key: number]: Mark[]}, mark: Mark) => {
|
||||||
|
const key = mark.location.page;
|
||||||
|
const curGroup = obj[key] ?? [];
|
||||||
|
return { ...obj, [key]: [...curGroup, mark]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
toFile,
|
toFile,
|
||||||
toPdfFile,
|
toPdfFile,
|
||||||
toPdfFiles,
|
toPdfFiles,
|
||||||
inPx
|
inPx,
|
||||||
|
convertToPdfFile,
|
||||||
|
addMarks,
|
||||||
|
convertToPdfBlob,
|
||||||
|
groupMarksByPage,
|
||||||
}
|
}
|
33
src/utils/sign.ts
Normal file
33
src/utils/sign.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Event } from 'nostr-tools'
|
||||||
|
import { Meta } from '../types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function returns the signature of last signer
|
||||||
|
* It will be used in the content of export signature's signedEvent
|
||||||
|
*/
|
||||||
|
const getLastSignersSig = (meta: Meta, signers: `npub1${string}`[]): string | null => {
|
||||||
|
// if there're no signers then use creator's signature
|
||||||
|
if (signers.length === 0) {
|
||||||
|
try {
|
||||||
|
const createSignatureEvent: Event = JSON.parse(meta.createSignature)
|
||||||
|
return createSignatureEvent.sig
|
||||||
|
} catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get last signer
|
||||||
|
const lastSigner = signers[signers.length - 1]
|
||||||
|
|
||||||
|
// get the signature of last signer
|
||||||
|
try {
|
||||||
|
const lastSignatureEvent: Event = JSON.parse(
|
||||||
|
meta.docSignatures[lastSigner]
|
||||||
|
)
|
||||||
|
return lastSignatureEvent.sig
|
||||||
|
} catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getLastSignersSig }
|
Loading…
Reference in New Issue
Block a user