PDF Markings #114

Merged
eugene merged 33 commits from issue-99 into staging 2024-08-06 10:02:04 +00:00
14 changed files with 264 additions and 36 deletions
Showing only changes of commit b58ba625f9 - Show all commits

View File

@ -101,7 +101,7 @@ export const DrawPDFFields = (props: Props) => {
* It is re rendered and visible right away
*
* @param event Mouse event
* @param page PdfPage where press happened
* @param page PdfItem where press happened
*/
const onMouseDown = (event: any, page: PdfPage) => {
// 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
* which alters the newly created drawing element, resizing it while mouse move
* @param event Mouse event
* @param page PdfPage where moving is happening
* @param page PdfItem where moving is happening
*/
const onMouseMove = (event: any, page: PdfPage) => {
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][fileHash]) markConfig[drawnField.counterpart][fileHash] = []
console.log('drawn field: ', drawnField);
markConfig[drawnField.counterpart][fileHash].push({
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 markConfig = createMarkConfig(fileHashes)
console.log('mark config: ', markConfig)
const content: CreateSignatureEventContent = {
signers: signers.map((signer) => hexToNpub(signer.pubkey)),
viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
@ -519,6 +529,8 @@ export const CreatePage = () => {
title
}
console.log('content: ', content)
setLoadingSpinnerDesc('Signing nostr event for create signature')
const createSignature = await signEventForMetaFile(

View File

@ -22,7 +22,7 @@ import {
generateKeysFile,
getHash,
hexToNpub,
isOnline,
isOnline, loadZip,
now,
npubToHex,
parseJson,
@ -33,6 +33,10 @@ import {
} from '../../utils'
import { DisplayMeta } from './internal/displayMeta'
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 {
Fully_Signed,
User_Is_Next_Signer,
@ -58,7 +62,7 @@ export const SignPage = () => {
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 [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
@ -70,6 +74,7 @@ export const SignPage = () => {
const [signers, setSigners] = useState<`npub1${string}`[]>([])
const [viewers, setViewers] = useState<`npub1${string}`[]>([])
const [markConfig, setMarkConfig] = useState<MarkConfig | null>(null)
const [creatorFileHashes, setCreatorFileHashes] = useState<{
[key: string]: string
}>({})
@ -178,6 +183,9 @@ export const SignPage = () => {
setViewers(createSignatureContent.viewers)
setCreatorFileHashes(createSignatureContent.fileHashes)
setSubmittedBy(createSignatureEvent.pubkey)
setMarkConfig(createSignatureContent.markConfig);
console.log('createSignatureContent', createSignatureContent)
setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[])
}
@ -262,16 +270,13 @@ export const SignPage = () => {
if (!decrypted) return
const zip = await JSZip.loadAsync(decrypted).catch((err) => {
console.log('err in loading zip file :>> ', err)
toast.error(err.message || 'An error occurred in loading zip file.')
const zip = await loadZip(decrypted)
if (!zip) {
setIsLoading(false)
return null
})
return
}
if (!zip) return
const files: { [filename: string]: ArrayBuffer } = {}
const files: { [filename: string]: PdfFile } = {}
const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files).map((entry) => entry.name)
@ -285,7 +290,7 @@ export const SignPage = () => {
)
if (arrayBuffer) {
files[fileName] = arrayBuffer
files[fileName] = await convertToPdfFile(arrayBuffer, fileName);
const hash = await getHash(arrayBuffer)
if (hash) {
@ -296,10 +301,17 @@ export const SignPage = () => {
}
}
console.log('processed files: ', files);
setFiles(files)
setCurrentFileHashes(fileHashes)
}
const convertToPdfFile = async (arrayBuffer: ArrayBuffer, fileName: string) => {
const file = toFile(arrayBuffer, fileName);
return toPdfFile(file);
}
const parseKeysJson = async (zip: JSZip) => {
const keysFileContent = await readContentOfZipEntry(
zip,
@ -323,11 +335,7 @@ export const SignPage = () => {
const decrypt = async (file: File) => {
setLoadingSpinnerDesc('Decrypting file')
const zip = await JSZip.loadAsync(file).catch((err) => {
console.log('err in loading zip file :>> ', err)
toast.error(err.message || 'An error occurred in loading zip file.')
return null
})
const zip = await loadZip(file);
if (!zip) return
const parsedKeysJson = await parseKeysJson(zip)
@ -398,32 +406,27 @@ export const SignPage = () => {
setLoadingSpinnerDesc('Parsing zip file')
const zip = await JSZip.loadAsync(decryptedZipFile).catch((err) => {
console.log('err in loading zip file :>> ', err)
toast.error(err.message || 'An error occurred in loading zip file.')
return null
})
const zip = await loadZip(decryptedZipFile)
if (!zip) return
const files: { [filename: string]: ArrayBuffer } = {}
const files: { [filename: string]: PdfFile } = {}
const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files)
.filter((entry) => entry.name.startsWith('files/') && !entry.dir)
.map((entry) => entry.name)
.map((entry) => entry.replace(/^files\//, ''))
// generate hashes for all entries in files folder of zipArchive
// 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(
zip,
fileName,
'arraybuffer'
)
fileName = fileName.replace(/^files\//, '')
if (arrayBuffer) {
files[fileName] = arrayBuffer
files[fileName] = await convertToPdfFile(arrayBuffer, fileName);
const hash = await getHash(arrayBuffer)
if (hash) {
@ -434,6 +437,8 @@ export const SignPage = () => {
}
}
console.log('processed files: ', files);
setFiles(files)
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>
</>
)

View File

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

View File

@ -47,4 +47,36 @@
@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
*/
markLocation: string;
markLocation: MarkLocation;
}
export interface MarkLocation {
top: number;
left: number;
height: number;
width: number;
page: number;
}
// Creator Meta Object Example

View File

@ -1,4 +1,4 @@
export interface OutputByType {
export interface OutputByType {
base64: string
string: string
text: string
@ -10,4 +10,17 @@ export interface OutputByType {
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 InputFileFormat = InputByType[keyof InputByType] | Promise<InputByType[keyof InputByType]>;

View File

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

View File

@ -1,6 +1,6 @@
import JSZip from 'jszip'
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.
@ -9,7 +9,7 @@ import { OutputByType, OutputType } from '../types'
* @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.
*/
export const readContentOfZipEntry = async <T extends OutputType>(
const readContentOfZipEntry = async <T extends OutputType>(
zip: JSZip,
filePath: string,
outputType: T
@ -35,3 +35,20 @@ export const readContentOfZipEntry = async <T extends OutputType>(
// Return the file content or null if an error occurred
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
}