feat(pdf-marking): add pdf-view components

This commit is contained in:
eugene 2024-07-17 11:25:02 +03:00
parent bf167d78f2
commit b58ba625f9
14 changed files with 264 additions and 36 deletions

View File

@ -101,7 +101,7 @@ export const DrawPDFFields = (props: Props) => {
* It is re rendered and visible right away * It is re rendered and visible right away
* *
* @param event Mouse event * @param event Mouse event
* @param page PdfPage where press happened * @param page PdfItem where press happened
*/ */
const onMouseDown = (event: any, page: PdfPage) => { const onMouseDown = (event: any, page: PdfPage) => {
// Proceed only if left click // Proceed only if left click
@ -154,7 +154,7 @@ export const DrawPDFFields = (props: Props) => {
* After {@link onMouseDown} create an drawing element, this function gets called on every pixel moved * After {@link onMouseDown} create an drawing element, this function gets called on every pixel moved
* which alters the newly created drawing element, resizing it while mouse move * which alters the newly created drawing element, resizing it while mouse move
* @param event Mouse event * @param event Mouse event
* @param page PdfPage where moving is happening * @param page PdfItem where moving is happening
*/ */
const onMouseMove = (event: any, page: PdfPage) => { const onMouseMove = (event: any, page: PdfPage) => {
if (mouseState.clicked && selectedTool) { if (mouseState.clicked && selectedTool) {

View File

View File

@ -0,0 +1,28 @@
import { PdfFile } from '../../types/drawing.ts'
import { MarkConfigDetails} from '../../types/mark.ts'
import PdfPageItem from './PdfPageItem.tsx';
interface PdfItemProps {
pdfFile: PdfFile
markConfigDetails: MarkConfigDetails[]
}
const PdfItem = ({ pdfFile, markConfigDetails }: PdfItemProps) => {
const filterMarkConfigDetails = (i: number) => {
return markConfigDetails.filter(
(details) => details.markLocation.page === i);
}
return (
pdfFile.pages.map((page, i) => {
console.log('page: ', page);
return (
<PdfPageItem
page={page}
key={i}
markConfigDetails={filterMarkConfigDetails(i)}
/>
)
}))
}
export default PdfItem

View File

@ -0,0 +1,23 @@
import { MarkLocation } from '../../types/mark.ts'
import styles from '../DrawPDFFields/style.module.scss'
import { inPx } from '../../utils/pdf.ts'
interface PdfMarkItemProps {
markLocation: MarkLocation
}
const PdfMarkItem = ({ markLocation }: PdfMarkItemProps) => {
return (
<div
className={styles.drawingRectangle}
style={{
left: inPx(markLocation.left),
top: inPx(markLocation.top),
width: inPx(markLocation.width),
height: inPx(markLocation.height)
}}
/>
)
}
export default PdfMarkItem

View File

@ -0,0 +1,30 @@
import styles from '../DrawPDFFields/style.module.scss'
import { PdfPage } from '../../types/drawing.ts'
import { MarkConfigDetails, MarkLocation } from '../../types/mark.ts'
import PdfMarkItem from './PdfMarkItem.tsx'
import { useState } from 'react';
interface PdfPageProps {
page: PdfPage
markConfigDetails: MarkConfigDetails[]
}
const PdfPageItem = ({ page, markConfigDetails }: PdfPageProps) => {
const [currentMark, setCurrentMark] = useState<MarkLocation | null>(null);
return (
<div
style={{
border: '1px solid #c4c4c4',
marginBottom: '10px'
}}
className={styles.pdfImageWrapper}
>
<img draggable="false" style={{width: '100%'}} src={page.image} />
{markConfigDetails.map((detail, i) => (
<PdfMarkItem key={i} markLocation={detail.markLocation} />
))}
</div>
)
}
export default PdfPageItem

View File

@ -0,0 +1,45 @@
import { PdfFile } from '../../types/drawing.ts'
import { Box } from '@mui/material'
import PdfItem from './PdfItem.tsx'
import { MarkConfig, MarkConfigDetails } from '../../types/mark.ts'
import { State } from '../../store/rootReducer'
import { useSelector } from 'react-redux';
import { hexToNpub, npubToHex } from '../../utils'
interface PdfViewProps {
files: { [filename: string]: PdfFile },
fileHashes: { [key: string]: string | null },
markConfig: MarkConfig,
}
const PdfView = (props: PdfViewProps) => {
console.log('current file hashes: ', props.fileHashes)
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey);
if (!usersPubkey) return;
console.log(props.markConfig[hexToNpub(usersPubkey)]);
console.log('users pubkey: ', usersPubkey);
console.log('mark config: ', props.markConfig);
const getMarkConfigDetails = (fileName: string): MarkConfigDetails[] | undefined => {
const fileHash = props.fileHashes[fileName];
if (!fileHash) return;
return props.markConfig[hexToNpub(usersPubkey)][fileHash];
}
const { files } = props;
return (
<Box>
{Object.entries(files)
.filter(([name]) => !!getMarkConfigDetails(name))
.map(([name, file], i) => (
<PdfItem
pdfFile={file}
key={i}
markConfigDetails={getMarkConfigDetails(name) as MarkConfigDetails[]} />
))}
</Box>
)
}
export default PdfView;

View File

@ -350,9 +350,17 @@ export const CreatePage = () => {
if (!markConfig[drawnField.counterpart]) markConfig[drawnField.counterpart] = {} if (!markConfig[drawnField.counterpart]) markConfig[drawnField.counterpart] = {}
if (!markConfig[drawnField.counterpart][fileHash]) markConfig[drawnField.counterpart][fileHash] = [] if (!markConfig[drawnField.counterpart][fileHash]) markConfig[drawnField.counterpart][fileHash] = []
console.log('drawn field: ', drawnField);
markConfig[drawnField.counterpart][fileHash].push({ markConfig[drawnField.counterpart][fileHash].push({
markType: drawnField.type, markType: drawnField.type,
markLocation: `P:${pageIndex};X:${drawnField.left};Y:${drawnField.top}` markLocation: {
page: pageIndex,
top: drawnField.top,
left: drawnField.left,
height: drawnField.height,
width: drawnField.width,
}
}) })
}) })
}) })
@ -510,6 +518,8 @@ export const CreatePage = () => {
const viewers = users.filter((user) => user.role === UserRole.viewer) const viewers = users.filter((user) => user.role === UserRole.viewer)
const markConfig = createMarkConfig(fileHashes) const markConfig = createMarkConfig(fileHashes)
console.log('mark config: ', markConfig)
const content: CreateSignatureEventContent = { const content: CreateSignatureEventContent = {
signers: signers.map((signer) => hexToNpub(signer.pubkey)), signers: signers.map((signer) => hexToNpub(signer.pubkey)),
viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)), viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
@ -519,6 +529,8 @@ export const CreatePage = () => {
title title
} }
console.log('content: ', content)
setLoadingSpinnerDesc('Signing nostr event for create signature') setLoadingSpinnerDesc('Signing nostr event for create signature')
const createSignature = await signEventForMetaFile( const createSignature = await signEventForMetaFile(

View File

@ -22,7 +22,7 @@ import {
generateKeysFile, generateKeysFile,
getHash, getHash,
hexToNpub, hexToNpub,
isOnline, isOnline, loadZip,
now, now,
npubToHex, npubToHex,
parseJson, parseJson,
@ -33,6 +33,10 @@ import {
} from '../../utils' } from '../../utils'
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 { toFile, toPdfFile } from '../../utils/pdf.ts'
import PdfView from '../../components/PDFView'
import { MarkConfig } from '../../types/mark.ts'
enum SignedStatus { enum SignedStatus {
Fully_Signed, Fully_Signed,
User_Is_Next_Signer, User_Is_Next_Signer,
@ -58,7 +62,7 @@ export const SignPage = () => {
const [selectedFile, setSelectedFile] = useState<File | null>(null) const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [files, setFiles] = useState<{ [filename: string]: ArrayBuffer }>({}) const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({})
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
@ -70,6 +74,7 @@ export const SignPage = () => {
const [signers, setSigners] = useState<`npub1${string}`[]>([]) const [signers, setSigners] = useState<`npub1${string}`[]>([])
const [viewers, setViewers] = useState<`npub1${string}`[]>([]) const [viewers, setViewers] = useState<`npub1${string}`[]>([])
const [markConfig, setMarkConfig] = useState<MarkConfig | null>(null)
const [creatorFileHashes, setCreatorFileHashes] = useState<{ const [creatorFileHashes, setCreatorFileHashes] = useState<{
[key: string]: string [key: string]: string
}>({}) }>({})
@ -178,6 +183,9 @@ export const SignPage = () => {
setViewers(createSignatureContent.viewers) setViewers(createSignatureContent.viewers)
setCreatorFileHashes(createSignatureContent.fileHashes) setCreatorFileHashes(createSignatureContent.fileHashes)
setSubmittedBy(createSignatureEvent.pubkey) setSubmittedBy(createSignatureEvent.pubkey)
setMarkConfig(createSignatureContent.markConfig);
console.log('createSignatureContent', createSignatureContent)
setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[]) setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[])
} }
@ -262,16 +270,13 @@ export const SignPage = () => {
if (!decrypted) return if (!decrypted) return
const zip = await JSZip.loadAsync(decrypted).catch((err) => { const zip = await loadZip(decrypted)
console.log('err in loading zip file :>> ', err) if (!zip) {
toast.error(err.message || 'An error occurred in loading zip file.')
setIsLoading(false) setIsLoading(false)
return null return
}) }
if (!zip) return const files: { [filename: string]: PdfFile } = {}
const files: { [filename: string]: ArrayBuffer } = {}
const fileHashes: { [key: string]: string | null } = {} const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files).map((entry) => entry.name) const fileNames = Object.values(zip.files).map((entry) => entry.name)
@ -285,7 +290,7 @@ export const SignPage = () => {
) )
if (arrayBuffer) { if (arrayBuffer) {
files[fileName] = arrayBuffer files[fileName] = await convertToPdfFile(arrayBuffer, fileName);
const hash = await getHash(arrayBuffer) const hash = await getHash(arrayBuffer)
if (hash) { if (hash) {
@ -296,10 +301,17 @@ export const SignPage = () => {
} }
} }
console.log('processed files: ', files);
setFiles(files) setFiles(files)
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,
@ -323,11 +335,7 @@ export const SignPage = () => {
const decrypt = async (file: File) => { const decrypt = async (file: File) => {
setLoadingSpinnerDesc('Decrypting file') setLoadingSpinnerDesc('Decrypting file')
const zip = await JSZip.loadAsync(file).catch((err) => { const zip = await loadZip(file);
console.log('err in loading zip file :>> ', err)
toast.error(err.message || 'An error occurred in loading zip file.')
return null
})
if (!zip) return if (!zip) return
const parsedKeysJson = await parseKeysJson(zip) const parsedKeysJson = await parseKeysJson(zip)
@ -398,32 +406,27 @@ export const SignPage = () => {
setLoadingSpinnerDesc('Parsing zip file') setLoadingSpinnerDesc('Parsing zip file')
const zip = await JSZip.loadAsync(decryptedZipFile).catch((err) => { const zip = await loadZip(decryptedZipFile)
console.log('err in loading zip file :>> ', err)
toast.error(err.message || 'An error occurred in loading zip file.')
return null
})
if (!zip) return if (!zip) return
const files: { [filename: string]: ArrayBuffer } = {} const files: { [filename: string]: PdfFile } = {}
const fileHashes: { [key: string]: string | null } = {} const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files) const fileNames = Object.values(zip.files)
.filter((entry) => entry.name.startsWith('files/') && !entry.dir) .filter((entry) => entry.name.startsWith('files/') && !entry.dir)
.map((entry) => entry.name) .map((entry) => entry.name)
.map((entry) => entry.replace(/^files\//, ''))
// generate hashes for all entries in files folder of zipArchive // generate hashes for all entries in files folder of zipArchive
// these hashes can be used to verify the originality of files // these hashes can be used to verify the originality of files
for (let fileName of fileNames) { for (const fileName of fileNames) {
const arrayBuffer = await readContentOfZipEntry( const arrayBuffer = await readContentOfZipEntry(
zip, zip,
fileName, fileName,
'arraybuffer' 'arraybuffer'
) )
fileName = fileName.replace(/^files\//, '')
if (arrayBuffer) { if (arrayBuffer) {
files[fileName] = arrayBuffer files[fileName] = await convertToPdfFile(arrayBuffer, fileName);
const hash = await getHash(arrayBuffer) const hash = await getHash(arrayBuffer)
if (hash) { if (hash) {
@ -434,6 +437,8 @@ export const SignPage = () => {
} }
} }
console.log('processed files: ', files);
setFiles(files) setFiles(files)
setCurrentFileHashes(fileHashes) setCurrentFileHashes(fileHashes)
@ -916,6 +921,17 @@ export const SignPage = () => {
)} )}
</> </>
)} )}
{markConfig && (<PdfView
files={files}
markConfig={markConfig}
fileHashes={currentFileHashes}/>)}
<div
className={styles.fixedBottomForm}>
<input type="text" placeholder="type here..." />
<button>Next</button>
</div>
</Box> </Box>
</> </>
) )

View File

@ -34,10 +34,11 @@ import { UserComponent } from '../../../components/username'
import { MetadataController } from '../../../controllers' import { MetadataController } from '../../../controllers'
import { npubToHex, shorten, hexToNpub, parseJson } from '../../../utils' import { npubToHex, shorten, hexToNpub, parseJson } from '../../../utils'
import styles from '../style.module.scss' import styles from '../style.module.scss'
import { PdfFile } from '../../../types/drawing.ts'
type DisplayMetaProps = { type DisplayMetaProps = {
meta: Meta meta: Meta
files: { [filename: string]: ArrayBuffer } files: { [filename: string]: PdfFile }
submittedBy: string submittedBy: string
signers: `npub1${string}`[] signers: `npub1${string}`[]
viewers: `npub1${string}`[] viewers: `npub1${string}`[]
@ -143,7 +144,7 @@ export const DisplayMeta = ({
}, [users, submittedBy]) }, [users, submittedBy])
const downloadFile = async (filename: string) => { const downloadFile = async (filename: string) => {
const arrayBuffer = files[filename] const arrayBuffer = await files[filename].file.arrayBuffer()
if (!arrayBuffer) return if (!arrayBuffer) return
const blob = new Blob([arrayBuffer]) const blob = new Blob([arrayBuffer])

View File

@ -47,4 +47,36 @@
@extend .user; @extend .user;
} }
} }
.fixedBottomForm {
position: fixed;
bottom: 0;
width: 50%;
border-top: 1px solid #ccc;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
padding: 10px 20px;
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.fixedBottomForm input[type="text"] {
width: 80%;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
}
.fixedBottomForm button {
background-color: #3f3d56;
color: white;
border: none;
padding: 10px 20px;
font-size: 16px;
margin-left: 10px;
border-radius: 4px;
cursor: pointer;
}
} }

View File

@ -37,7 +37,15 @@ export interface MarkConfigDetails {
/** /**
* Coordinates in format: X:10;Y:50 * Coordinates in format: X:10;Y:50
*/ */
markLocation: string; markLocation: MarkLocation;
}
export interface MarkLocation {
top: number;
left: number;
height: number;
width: number;
page: number;
} }
// Creator Meta Object Example // Creator Meta Object Example

View File

@ -1,4 +1,4 @@
export interface OutputByType { export interface OutputByType {
base64: string base64: string
string: string string: string
text: string text: string
@ -10,4 +10,17 @@ export interface OutputByType {
nodebuffer: Buffer nodebuffer: Buffer
} }
interface InputByType {
base64: string;
string: string;
text: string;
binarystring: string;
array: number[];
uint8array: Uint8Array;
arraybuffer: ArrayBuffer;
blob: Blob;
stream: NodeJS.ReadableStream;
}
export type OutputType = keyof OutputByType export type OutputType = keyof OutputByType
export type InputFileFormat = InputByType[keyof InputByType] | Promise<InputByType[keyof InputByType]>;

View File

@ -20,6 +20,8 @@ const toPdfFiles = async (selectedFiles: File[]): Promise<PdfFile[]> => {
.map(toPdfFile)); .map(toPdfFile));
} }
const inPx = (coordinate: number): string => `${coordinate}px`;
const isPdf = (file: File) => file.type.toLowerCase().includes('pdf'); const isPdf = (file: File) => file.type.toLowerCase().includes('pdf');
/** /**
@ -74,5 +76,6 @@ const pdfToImages = async (data: any): Promise<PdfPage[]> => {
export { export {
toFile, toFile,
toPdfFile, toPdfFile,
toPdfFiles toPdfFiles,
inPx
} }

View File

@ -1,6 +1,6 @@
import JSZip from 'jszip' import JSZip from 'jszip'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { OutputByType, OutputType } from '../types' import { InputFileFormat, OutputByType, OutputType } from '../types'
/** /**
* Read the content of a file within a zip archive. * Read the content of a file within a zip archive.
@ -9,7 +9,7 @@ import { OutputByType, OutputType } from '../types'
* @param outputType The type of output to return (e.g., 'string', 'arraybuffer', 'uint8array', etc.). * @param outputType The type of output to return (e.g., 'string', 'arraybuffer', 'uint8array', etc.).
* @returns A Promise resolving to the content of the file, or null if an error occurs. * @returns A Promise resolving to the content of the file, or null if an error occurs.
*/ */
export const readContentOfZipEntry = async <T extends OutputType>( const readContentOfZipEntry = async <T extends OutputType>(
zip: JSZip, zip: JSZip,
filePath: string, filePath: string,
outputType: T outputType: T
@ -35,3 +35,20 @@ export const readContentOfZipEntry = async <T extends OutputType>(
// Return the file content or null if an error occurred // Return the file content or null if an error occurred
return fileContent return fileContent
} }
const loadZip = async (data: InputFileFormat): Promise<JSZip | null> => {
try {
return await JSZip.loadAsync(data);
} catch (err: any) {
console.log('err in loading zip file :>> ', err)
toast.error(err.message || 'An error occurred in loading zip file.')
return null;
}
}
export {
readContentOfZipEntry,
loadZip
}