new release #190

Merged
b merged 68 commits from staging into main 2024-09-06 18:59:34 +00:00
27 changed files with 632 additions and 518 deletions
Showing only changes of commit f4aefbb200 - Show all commits

View File

@ -69,3 +69,73 @@ a {
input {
font-family: inherit;
}
// Shared styles for center content (Create, Sign, Verify)
.files-wrapper {
display: flex;
flex-direction: column;
gap: 25px;
}
.file-wrapper {
display: flex;
flex-direction: column;
gap: 15px;
position: relative;
// CSS, scroll position when scrolling to the files is adjusted by
// - first-child Header height, default body padding, and center content border (10px) and padding (10px)
// - others We don't include border and padding and scroll to the top of the image
&:first-child {
scroll-margin-top: $header-height + $body-vertical-padding + 20px;
}
&:not(:first-child) {
scroll-margin-top: $header-height + $body-vertical-padding;
}
}
// For pdf marks
.image-wrapper {
position: relative;
-webkit-user-select: none;
user-select: none;
overflow: hidden; /* Ensure no overflow */
> img {
display: block;
max-width: 100%;
max-height: 100%;
object-fit: contain; /* Ensure the image fits within the container */
}
}
// For image rendering (uploaded image as a file)
.file-image {
-webkit-user-select: none;
user-select: none;
display: block;
width: 100%;
height: auto;
object-fit: contain; /* Ensure the image fits within the container */
}
[data-dev='true'] {
.image-wrapper {
// outline: 1px solid #ccc; /* Optional: for visual debugging */
background-color: #e0f7fa; /* Optional: for visual debugging */
}
}
.extension-file-box {
border-radius: 4px;
background: rgba(255, 255, 255, 0.5);
height: 100px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: rgba(0, 0, 0, 0.25);
font-size: 14px;
}

View File

@ -10,7 +10,8 @@ import {
faCalendar,
faCopy,
faEye,
faFile
faFile,
faFileCircleExclamation
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { UserAvatarGroup } from '../UserAvatarGroup'
@ -20,6 +21,7 @@ import { TooltipChild } from '../TooltipChild'
import { getExtensionIconLabel } from '../getExtensionIconLabel'
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
import { useSigitMeta } from '../../hooks/useSigitMeta'
import { extractFileExtensions } from '../../utils/file'
type SigitProps = {
meta: Meta
@ -27,23 +29,18 @@ type SigitProps = {
}
export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
const {
title,
createdAt,
submittedBy,
signers,
signedStatus,
fileExtensions,
isValid
} = parsedMeta
const { title, createdAt, submittedBy, signers, signedStatus, isValid } =
parsedMeta
const { signersStatus } = useSigitMeta(meta)
const { signersStatus, fileHashes } = useSigitMeta(meta)
const profiles = useSigitProfiles([
...(submittedBy ? [submittedBy] : []),
...signers
])
const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes))
return (
<div className={styles.itemWrapper}>
<Link
@ -120,17 +117,21 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => {
<span className={styles.iconLabel}>
<FontAwesomeIcon icon={faEye} /> {signedStatus}
</span>
{fileExtensions.length > 0 ? (
{extensions.length > 0 ? (
<span className={styles.iconLabel}>
{fileExtensions.length > 1 ? (
{!isSame ? (
<>
<FontAwesomeIcon icon={faFile} /> Multiple File Types
</>
) : (
getExtensionIconLabel(fileExtensions[0])
getExtensionIconLabel(extensions[0])
)}
</span>
) : null}
) : (
<>
<FontAwesomeIcon icon={faFileCircleExclamation} /> &mdash;
</>
)}
</div>
<div className={styles.itemActions}>
<Tooltip title="Duplicate" arrow placement="top" disableInteractive>

View File

@ -2,7 +2,6 @@ import { Close } from '@mui/icons-material'
import {
Box,
CircularProgress,
Divider,
FormControl,
InputLabel,
MenuItem,
@ -10,19 +9,15 @@ import {
} from '@mui/material'
import styles from './style.module.scss'
import React, { useEffect, useState } from 'react'
import * as PDFJS from 'pdfjs-dist'
import { ProfileMetadata, User, UserRole } from '../../types'
import {
PdfFile,
MouseState,
PdfPage,
DrawnField,
DrawTool
} from '../../types/drawing'
import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing'
import { truncate } from 'lodash'
import { extractFileExtension, hexToNpub } from '../../utils'
import { toPdfFiles } from '../../utils/pdf.ts'
import { settleAllFullfilfedPromises, hexToNpub } from '../../utils'
import { getSigitFile, SigitFile } from '../../utils/file'
import { FileDivider } from '../FileDivider'
import { ExtensionFileBox } from '../ExtensionFileBox'
PDFJS.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
@ -32,15 +27,15 @@ interface Props {
selectedFiles: File[]
users: User[]
metadata: { [key: string]: ProfileMetadata }
onDrawFieldsChange: (pdfFiles: PdfFile[]) => void
onDrawFieldsChange: (sigitFiles: SigitFile[]) => void
selectedTool?: DrawTool
}
export const DrawPDFFields = (props: Props) => {
const { selectedFiles, selectedTool, onDrawFieldsChange, users } = props
const [pdfFiles, setPdfFiles] = useState<PdfFile[]>([])
const [parsingPdf, setParsingPdf] = useState<boolean>(false)
const [sigitFiles, setSigitFiles] = useState<SigitFile[]>([])
const [parsingPdf, setIsParsing] = useState<boolean>(false)
const [mouseState, setMouseState] = useState<MouseState>({
clicked: false
@ -49,42 +44,43 @@ export const DrawPDFFields = (props: Props) => {
useEffect(() => {
if (selectedFiles) {
/**
* Reads the pdf binary files and converts it's pages to images
* creates the pdfFiles object and sets to a state
* Reads the binary files and converts to internal file type
* and sets to a state (adds images if it's a PDF)
*/
const parsePdfPages = async () => {
const pdfFiles: PdfFile[] = await toPdfFiles(selectedFiles)
const parsePages = async () => {
const files = await settleAllFullfilfedPromises(
selectedFiles,
getSigitFile
)
setPdfFiles(pdfFiles)
setSigitFiles(files)
}
setParsingPdf(true)
setIsParsing(true)
parsePdfPages().finally(() => {
setParsingPdf(false)
parsePages().finally(() => {
setIsParsing(false)
})
}
}, [selectedFiles])
useEffect(() => {
if (pdfFiles) onDrawFieldsChange(pdfFiles)
}, [onDrawFieldsChange, pdfFiles])
if (sigitFiles) onDrawFieldsChange(sigitFiles)
}, [onDrawFieldsChange, sigitFiles])
/**
* Drawing events
*/
useEffect(() => {
// window.addEventListener('mousedown', onMouseDown);
window.addEventListener('mouseup', onMouseUp)
return () => {
// window.removeEventListener('mousedown', onMouseDown);
window.removeEventListener('mouseup', onMouseUp)
}
}, [])
const refreshPdfFiles = () => {
setPdfFiles([...pdfFiles])
setSigitFiles([...sigitFiles])
}
/**
@ -303,10 +299,10 @@ export const DrawPDFFields = (props: Props) => {
) => {
event.stopPropagation()
pdfFiles[pdfFileIndex].pages[pdfPageIndex].drawnFields.splice(
drawnFileIndex,
1
)
const pages = sigitFiles[pdfFileIndex]?.pages
if (pages) {
pages[pdfPageIndex]?.drawnFields?.splice(drawnFileIndex, 1)
}
}
/**
@ -345,14 +341,17 @@ export const DrawPDFFields = (props: Props) => {
/**
* Renders the pdf pages and drawing elements
*/
const getPdfPages = (pdfFile: PdfFile, pdfFileIndex: number) => {
const getPdfPages = (file: SigitFile, fileIndex: number) => {
// Early return if this is not a pdf
if (!file.isPdf) return null
return (
<>
{pdfFile.pages.map((page, pdfPageIndex: number) => {
{file.pages?.map((page, pageIndex: number) => {
return (
<div
key={pdfPageIndex}
className={`${styles.pdfImageWrapper} ${selectedTool ? styles.drawing : ''}`}
key={pageIndex}
className={`image-wrapper ${styles.pdfImageWrapper} ${selectedTool ? styles.drawing : ''}`}
>
<img
onMouseMove={(event) => {
@ -393,8 +392,8 @@ export const DrawPDFFields = (props: Props) => {
onMouseDown={(event) => {
onRemoveHandleMouseDown(
event,
pdfFileIndex,
pdfPageIndex,
fileIndex,
pageIndex,
drawnFieldIndex
)
}}
@ -469,40 +468,29 @@ export const DrawPDFFields = (props: Props) => {
)
}
if (!pdfFiles.length) {
if (!sigitFiles.length) {
return ''
}
return (
<div className={styles.view}>
{selectedFiles.map((file, i) => {
const name = file.name
const extension = extractFileExtension(name)
const pdfFile = pdfFiles.find((pdf) => pdf.file.name === name)
<div className="files-wrapper">
{sigitFiles.map((file, i) => {
return (
<React.Fragment key={name}>
<div
className={`${styles.fileWrapper} ${styles.scrollTarget}`}
id={`file-${name}`}
>
{pdfFile ? (
getPdfPages(pdfFile, i)
) : (
<div className={styles.otherFile}>
This is a {extension} file
</div>
<React.Fragment key={file.name}>
<div className="file-wrapper" id={`file-${file.name}`}>
{file.isPdf && getPdfPages(file, i)}
{file.isImage && (
<img
className="file-image"
src={file.objectUrl}
alt={file.name}
/>
)}
{!(file.isPdf || file.isImage) && (
<ExtensionFileBox extension={file.extension} />
)}
</div>
{i < selectedFiles.length - 1 && (
<Divider
sx={{
fontSize: '12px',
color: 'rgba(0,0,0,0.15)'
}}
>
File Separator
</Divider>
)}
{i < selectedFiles.length - 1 && <FileDivider />}
</React.Fragment>
)
})}

View File

@ -8,17 +8,6 @@
}
.pdfImageWrapper {
position: relative;
-webkit-user-select: none;
user-select: none;
> img {
display: block;
max-width: 100%;
max-height: 100%;
object-fit: contain; /* Ensure the image fits within the container */
}
&.drawing {
cursor: crosshair;
}
@ -90,29 +79,3 @@
padding: 5px 0;
}
}
.fileWrapper {
display: flex;
flex-direction: column;
gap: 15px;
position: relative;
scroll-margin-top: $header-height + $body-vertical-padding;
}
.view {
display: flex;
flex-direction: column;
gap: 25px;
}
.otherFile {
border-radius: 4px;
background: rgba(255, 255, 255, 0.5);
height: 100px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: rgba(0, 0, 0, 0.25);
font-size: 14px;
}

View File

@ -0,0 +1,6 @@
interface ExtensionFileBoxProps {
extension: string
}
export const ExtensionFileBox = ({ extension }: ExtensionFileBoxProps) => (
<div className="extension-file-box">This is a {extension} file</div>
)

View File

@ -0,0 +1,12 @@
import Divider from '@mui/material/Divider/Divider'
export const FileDivider = () => (
<Divider
sx={{
fontSize: '12px',
color: 'rgba(0,0,0,0.15)'
}}
>
File Separator
</Divider>
)

View File

@ -24,19 +24,23 @@ const FileList = ({
<div className={styles.wrap}>
<div className={styles.container}>
<ul className={styles.files}>
{files.map((file: CurrentUserFile) => (
{files.map((currentUserFile: CurrentUserFile) => (
<li
key={file.id}
className={`${styles.fileItem} ${isActive(file) && styles.active}`}
onClick={() => setCurrentFile(file)}
key={currentUserFile.id}
className={`${styles.fileItem} ${isActive(currentUserFile) && styles.active}`}
onClick={() => setCurrentFile(currentUserFile)}
>
<div className={styles.fileNumber}>{file.id}</div>
<div className={styles.fileNumber}>{currentUserFile.id}</div>
<div className={styles.fileInfo}>
<div className={styles.fileName}>{file.filename}</div>
<div className={styles.fileName}>
{currentUserFile.file.name}
</div>
</div>
<div className={styles.fileVisual}>
{file.isHashValid && <FontAwesomeIcon icon={faCheck} />}
{currentUserFile.isHashValid && (
<FontAwesomeIcon icon={faCheck} />
)}
</div>
</li>
))}

View File

@ -1,12 +1,13 @@
import { PdfFile } from '../../types/drawing.ts'
import { CurrentUserMark, Mark } from '../../types/mark.ts'
import { SigitFile } from '../../utils/file.ts'
import { ExtensionFileBox } from '../ExtensionFileBox.tsx'
import PdfPageItem from './PdfPageItem.tsx'
interface PdfItemProps {
currentUserMarks: CurrentUserMark[]
handleMarkClick: (id: number) => void
otherUserMarks: Mark[]
pdfFile: PdfFile
file: SigitFile
selectedMark: CurrentUserMark | null
selectedMarkValue: string
}
@ -15,7 +16,7 @@ interface PdfItemProps {
* Responsible for displaying pages of a single Pdf File.
*/
const PdfItem = ({
pdfFile,
file,
currentUserMarks,
handleMarkClick,
selectedMarkValue,
@ -31,7 +32,8 @@ const PdfItem = ({
const filterMarksByPage = (marks: Mark[], page: number): Mark[] => {
return marks.filter((mark) => mark.location.page === page)
}
return pdfFile.pages.map((page, i) => {
if (file.isPdf) {
return file.pages?.map((page, i) => {
return (
<PdfPageItem
page={page}
@ -44,6 +46,11 @@ const PdfItem = ({
/>
)
})
} else if (file.isImage) {
return <img className="file-image" src={file.objectUrl} alt={file.name} />
} else {
return <ExtensionFileBox extension={file.extension} />
}
}
export default PdfItem

View File

@ -10,7 +10,6 @@ import {
import { EMPTY } from '../../utils/const.ts'
import { Container } from '../Container'
import signPageStyles from '../../pages/sign/style.module.scss'
import styles from './style.module.scss'
import { CurrentUserFile } from '../../types/file.ts'
import FileList from '../FileList'
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
@ -134,9 +133,7 @@ const PdfMarking = (props: PdfMarkingProps) => {
}
right={meta !== null && <UsersDetails meta={meta} />}
>
<div className={styles.container}>
{currentUserMarks?.length > 0 && (
<div className={styles.pdfView}>
<PdfView
currentFile={currentFile}
files={files}
@ -146,9 +143,7 @@ const PdfMarking = (props: PdfMarkingProps) => {
currentUserMarks={currentUserMarks}
otherUserMarks={otherUserMarks}
/>
</div>
)}
</div>
</StickySideColumns>
{selectedMark !== null && (
<MarkFormField

View File

@ -28,20 +28,14 @@ const PdfPageItem = ({
useEffect(() => {
if (selectedMark !== null && !!markRefs.current[selectedMark.id]) {
markRefs.current[selectedMark.id]?.scrollIntoView({
behavior: 'smooth',
block: 'end'
behavior: 'smooth'
})
}
}, [selectedMark])
const markRefs = useRef<(HTMLDivElement | null)[]>([])
return (
<div
className={styles.pdfImageWrapper}
style={{
border: '1px solid #c4c4c4'
}}
>
<img draggable="false" src={page.image} style={{ width: '100%' }} />
<div className={`image-wrapper ${styles.pdfImageWrapper}`}>
<img draggable="false" src={page.image} />
{currentUserMarks.map((m, i) => (
<div key={i} ref={(el) => (markRefs.current[m.id] = el)}>
<PdfMarkItem

View File

@ -1,8 +1,9 @@
import { Divider } from '@mui/material'
import PdfItem from './PdfItem.tsx'
import { CurrentUserMark, Mark } from '../../types/mark.ts'
import { CurrentUserFile } from '../../types/file.ts'
import { useEffect, useRef } from 'react'
import { FileDivider } from '../FileDivider.tsx'
import React from 'react'
interface PdfViewProps {
currentFile: CurrentUserFile | null
@ -29,10 +30,7 @@ const PdfView = ({
const pdfRefs = useRef<(HTMLDivElement | null)[]>([])
useEffect(() => {
if (currentFile !== null && !!pdfRefs.current[currentFile.id]) {
pdfRefs.current[currentFile.id]?.scrollIntoView({
behavior: 'smooth',
block: 'end'
})
pdfRefs.current[currentFile.id]?.scrollIntoView({ behavior: 'smooth' })
}
}, [currentFile])
const filterByFile = (
@ -49,29 +47,32 @@ const PdfView = ({
const isNotLastPdfFile = (index: number, files: CurrentUserFile[]): boolean =>
index !== files.length - 1
return (
<>
<div className="files-wrapper">
{files.map((currentUserFile, index, arr) => {
const { hash, pdfFile, id } = currentUserFile
const { hash, file, id } = currentUserFile
if (!hash) return
return (
<React.Fragment key={index}>
<div
id={pdfFile.file.name}
id={file.name}
className="file-wrapper"
ref={(el) => (pdfRefs.current[id] = el)}
key={index}
>
<PdfItem
pdfFile={pdfFile}
file={file}
currentUserMarks={filterByFile(currentUserMarks, hash)}
selectedMark={selectedMark}
handleMarkClick={handleMarkClick}
selectedMarkValue={selectedMarkValue}
otherUserMarks={filterMarksByFile(otherUserMarks, hash)}
/>
{isNotLastPdfFile(index, arr) && <Divider>File Separator</Divider>}
</div>
{isNotLastPdfFile(index, arr) && <FileDivider />}
</React.Fragment>
)
})}
</>
</div>
)
}

View File

@ -1,33 +1,7 @@
.imageWrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100%; /* Adjust as needed */
height: 100%; /* Adjust as needed */
overflow: hidden; /* Ensure no overflow */
border: 1px solid #ccc; /* Optional: for visual debugging */
background-color: #e0f7fa; /* Optional: for visual debugging */
}
.image {
max-width: 100%;
max-height: 100%;
object-fit: contain; /* Ensure the image fits within the container */
}
.container {
display: flex;
width: 100%;
flex-direction: column;
}
.pdfView {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
gap: 10px;
}
.otherUserMarksDisplay {

View File

@ -1,7 +1,6 @@
import { Divider, Tooltip } from '@mui/material'
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
import {
extractFileExtensions,
formatTimestamp,
fromUnixTimestamp,
hexToNpub,
@ -28,6 +27,7 @@ import { State } from '../../store/rootReducer'
import { TooltipChild } from '../TooltipChild'
import { DisplaySigner } from '../DisplaySigner'
import { Meta } from '../../types'
import { extractFileExtensions } from '../../utils/file'
interface UsersDetailsProps {
meta: Meta

View File

@ -1,5 +1,9 @@
import { Event, Filter, Relay } from 'nostr-tools'
import { normalizeWebSocketURL, timeout } from '../utils'
import {
settleAllFullfilfedPromises,
normalizeWebSocketURL,
timeout
} from '../utils'
import { SIGIT_RELAY } from '../utils/const'
/**
@ -105,24 +109,11 @@ export class RelayController {
}
// connect to all specified relays
const relayPromises = relayUrls.map((relayUrl) =>
this.connectRelay(relayUrl)
const relays = await settleAllFullfilfedPromises(
relayUrls,
this.connectRelay
)
// Use Promise.allSettled to wait for all promises to settle
const results = await Promise.allSettled(relayPromises)
// Extract non-null values from fulfilled promises in a single pass
const relays = results.reduce<Relay[]>((acc, result) => {
if (result.status === 'fulfilled') {
const value = result.value
if (value) {
acc.push(value)
}
}
return acc
}, [])
// Check if any relays are connected
if (relays.length === 0) {
throw new Error('No relay is connected to fetch events!')
@ -228,23 +219,10 @@ export class RelayController {
}
// connect to all specified relays
const relayPromises = relayUrls.map((relayUrl) => {
return this.connectRelay(relayUrl)
})
// Use Promise.allSettled to wait for all promises to settle
const results = await Promise.allSettled(relayPromises)
// Extract non-null values from fulfilled promises in a single pass
const relays = results.reduce<Relay[]>((acc, result) => {
if (result.status === 'fulfilled') {
const value = result.value
if (value) {
acc.push(value)
}
}
return acc
}, [])
const relays = await settleAllFullfilfedPromises(
relayUrls,
this.connectRelay
)
// Check if any relays are connected
if (relays.length === 0) {
@ -292,24 +270,11 @@ export class RelayController {
}
// connect to all specified relays
const relayPromises = relayUrls.map((relayUrl) =>
this.connectRelay(relayUrl)
const relays = await settleAllFullfilfedPromises(
relayUrls,
this.connectRelay
)
// Use Promise.allSettled to wait for all promises to settle
const results = await Promise.allSettled(relayPromises)
// Extract non-null values from fulfilled promises in a single pass
const relays = results.reduce<Relay[]>((acc, result) => {
if (result.status === 'fulfilled') {
const value = result.value
if (value) {
acc.push(value)
}
}
return acc
}, [])
// Check if any relays are connected
if (relays.length === 0) {
throw new Error('No relay is connected to publish event!')

View File

@ -51,7 +51,7 @@ import {
import { Container } from '../../components/Container'
import styles from './style.module.scss'
import fileListStyles from '../../components/FileList/style.module.scss'
import { DrawTool, MarkType, PdfFile } from '../../types/drawing'
import { DrawTool, MarkType } from '../../types/drawing'
import { DrawPDFFields } from '../../components/DrawPDFFields'
import { Mark } from '../../types/mark.ts'
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
@ -83,6 +83,7 @@ import {
faTrash,
faUpload
} from '@fortawesome/free-solid-svg-icons'
import { SigitFile } from '../../utils/file.ts'
export const CreatePage = () => {
const navigate = useNavigate()
@ -125,7 +126,7 @@ export const CreatePage = () => {
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
{}
)
const [drawnPdfs, setDrawnPdfs] = useState<PdfFile[]>([])
const [drawnFiles, setDrawnFiles] = useState<SigitFile[]>([])
const [selectedTool, setSelectedTool] = useState<DrawTool>()
const [toolbox] = useState<DrawTool[]>([
@ -507,10 +508,11 @@ export const CreatePage = () => {
}
const createMarks = (fileHashes: { [key: string]: string }): Mark[] => {
return drawnPdfs
.flatMap((drawnPdf) => {
const fileHash = fileHashes[drawnPdf.file.name]
return drawnPdf.pages.flatMap((page, index) => {
return drawnFiles
.flatMap((file) => {
const fileHash = fileHashes[file.name]
return (
file.pages?.flatMap((page, index) => {
return page.drawnFields.map((drawnField) => {
return {
type: drawnField.type,
@ -523,10 +525,11 @@ export const CreatePage = () => {
},
npub: drawnField.counterpart,
pdfFileHash: fileHash,
fileName: drawnPdf.file.name
fileName: file.name
}
})
})
}) || []
)
})
.map((mark, index) => {
return { ...mark, id: index }
@ -846,8 +849,8 @@ export const CreatePage = () => {
}
}
const onDrawFieldsChange = (pdfFiles: PdfFile[]) => {
setDrawnPdfs(pdfFiles)
const onDrawFieldsChange = (sigitFiles: SigitFile[]) => {
setDrawnFiles(sigitFiles)
}
if (authUrl) {

View File

@ -38,8 +38,6 @@ import {
import { Container } from '../../components/Container'
import { DisplayMeta } from './internal/displayMeta'
import styles from './style.module.scss'
import { PdfFile } from '../../types/drawing.ts'
import { convertToPdfFile } from '../../utils/pdf.ts'
import { CurrentUserMark, Mark } from '../../types/mark.ts'
import { getLastSignersSig } from '../../utils/sign.ts'
import {
@ -49,7 +47,11 @@ import {
updateMarks
} from '../../utils'
import PdfMarking from '../../components/PDFView/PdfMarking.tsx'
import { getZipWithFiles } from '../../utils/file.ts'
import {
convertToSigitFile,
getZipWithFiles,
SigitFile
} from '../../utils/file.ts'
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
enum SignedStatus {
Fully_Signed,
@ -76,7 +78,7 @@ export const SignPage = () => {
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({})
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
@ -402,7 +404,7 @@ export const SignPage = () => {
return
}
const files: { [filename: string]: PdfFile } = {}
const files: { [filename: string]: SigitFile } = {}
const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files).map((entry) => entry.name)
@ -416,8 +418,7 @@ export const SignPage = () => {
)
if (arrayBuffer) {
files[fileName] = await convertToPdfFile(arrayBuffer, fileName)
files[fileName] = await convertToSigitFile(arrayBuffer, fileName)
const hash = await getHash(arrayBuffer)
if (hash) {
fileHashes[fileName] = hash
@ -462,7 +463,7 @@ export const SignPage = () => {
const zip = await loadZip(decryptedZipFile)
if (!zip) return
const files: { [filename: string]: PdfFile } = {}
const files: { [filename: string]: SigitFile } = {}
const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files)
.filter((entry) => entry.name.startsWith('files/') && !entry.dir)
@ -479,7 +480,7 @@ export const SignPage = () => {
)
if (arrayBuffer) {
files[fileName] = await convertToPdfFile(arrayBuffer, fileName)
files[fileName] = await convertToSigitFile(arrayBuffer, fileName)
const hash = await getHash(arrayBuffer)
if (hash) {
@ -764,8 +765,8 @@ export const SignPage = () => {
zip.file('meta.json', stringifiedMeta)
for (const [fileName, pdf] of Object.entries(files)) {
zip.file(`files/${fileName}`, await pdf.file.arrayBuffer())
for (const [fileName, file] of Object.entries(files)) {
zip.file(`files/${fileName}`, await file.arrayBuffer())
}
const arrayBuffer = await zip
@ -802,8 +803,8 @@ export const SignPage = () => {
zip.file('meta.json', stringifiedMeta)
for (const [fileName, pdf] of Object.entries(files)) {
zip.file(`files/${fileName}`, await pdf.file.arrayBuffer())
for (const [fileName, file] of Object.entries(files)) {
zip.file(`files/${fileName}`, await file.arrayBuffer())
}
const arrayBuffer = await zip

View File

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

View File

@ -2,8 +2,6 @@
.container {
color: $text-color;
//width: 550px;
//max-width: 550px;
.inputBlock {
position: relative;
@ -67,7 +65,7 @@
//z-index: 200;
}
.fixedBottomForm input[type="text"] {
.fixedBottomForm input[type='text'] {
width: 80%;
padding: 10px;
font-size: 16px;

View File

@ -1,4 +1,4 @@
import { Box, Button, Divider, Tooltip, Typography } from '@mui/material'
import { Box, Button, Tooltip, Typography } from '@mui/material'
import JSZip from 'jszip'
import { MuiFileInput } from 'mui-file-input'
import { Event, verifyEvent } from 'nostr-tools'
@ -26,11 +26,9 @@ import {
import styles from './style.module.scss'
import { useLocation } from 'react-router-dom'
import axios from 'axios'
import { PdfFile } from '../../types/drawing.ts'
import {
addMarks,
convertToPdfBlob,
convertToPdfFile,
groupMarksByFileNamePage,
inPx
} from '../../utils/pdf.ts'
@ -49,6 +47,9 @@ import FileList from '../../components/FileList'
import { CurrentUserFile } from '../../types/file.ts'
import { Mark } from '../../types/mark.ts'
import React from 'react'
import { convertToSigitFile, SigitFile } from '../../utils/file.ts'
import { FileDivider } from '../../components/FileDivider.tsx'
import { ExtensionFileBox } from '../../components/ExtensionFileBox.tsx'
interface PdfViewProps {
files: CurrentUserFile[]
@ -67,25 +68,25 @@ const SlimPdfView = ({
useEffect(() => {
if (currentFile !== null && !!pdfRefs.current[currentFile.id]) {
pdfRefs.current[currentFile.id]?.scrollIntoView({
behavior: 'smooth',
block: 'end'
behavior: 'smooth'
})
}
}, [currentFile])
return (
<div className={styles.view}>
<div className="files-wrapper">
{files.map((currentUserFile, i) => {
const { hash, filename, pdfFile, id } = currentUserFile
const { hash, file, id } = currentUserFile
const signatureEvents = Object.keys(parsedSignatureEvents)
if (!hash) return
return (
<React.Fragment key={filename}>
<React.Fragment key={file.name}>
<div
id={filename}
id={file.name}
ref={(el) => (pdfRefs.current[id] = el)}
className={styles.fileWrapper}
className="file-wrapper"
>
{pdfFile.pages.map((page, i) => {
{file.isPdf &&
file.pages?.map((page, i) => {
const marks: Mark[] = []
signatureEvents.forEach((e) => {
@ -99,7 +100,7 @@ const SlimPdfView = ({
}
})
return (
<div className={styles.imageWrapper} key={i}>
<div className="image-wrapper" key={i}>
<img draggable="false" src={page.image} />
{marks.map((m) => {
return (
@ -120,18 +121,18 @@ const SlimPdfView = ({
</div>
)
})}
</div>
{i < files.length - 1 && (
<Divider
sx={{
fontSize: '12px',
color: 'rgba(0,0,0,0.15)'
}}
>
File Separator
</Divider>
{file.isImage && (
<img
className="file-image"
src={file.objectUrl}
alt={file.name}
/>
)}
{!(file.isPdf || file.isImage) && (
<ExtensionFileBox extension={file.extension} />
)}
</div>
{i < files.length - 1 && <FileDivider />}
</React.Fragment>
)
})}
@ -171,7 +172,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]: SigitFile }>({})
const [currentFile, setCurrentFile] = useState<CurrentUserFile | null>(null)
const [signatureFileHashes, setSignatureFileHashes] = useState<{
[key: string]: string
@ -230,7 +231,7 @@ export const VerifyPage = () => {
if (!zip) return
const files: { [filename: string]: PdfFile } = {}
const files: { [fileName: string]: SigitFile } = {}
const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files).map(
(entry) => entry.name
@ -246,7 +247,7 @@ export const VerifyPage = () => {
)
if (arrayBuffer) {
files[fileName] = await convertToPdfFile(
files[fileName] = await convertToSigitFile(
arrayBuffer,
fileName!
)
@ -423,10 +424,15 @@ export const VerifyPage = () => {
const marks = extractMarksFromSignedMeta(updatedMeta)
const marksByPage = groupMarksByFileNamePage(marks)
for (const [fileName, pdf] of Object.entries(files)) {
const pages = await addMarks(pdf.file, marksByPage[fileName])
for (const [fileName, file] of Object.entries(files)) {
if (file.isPdf) {
// Draw marks into PDF file and generate a brand new blob
const pages = await addMarks(file, marksByPage[fileName])
const blob = await convertToPdfBlob(pages)
zip.file(`files/${fileName}`, blob)
} else {
zip.file(`files/${fileName}`, file)
}
}
const arrayBuffer = await zip

View File

@ -51,30 +51,6 @@
}
}
.view {
width: 550px;
max-width: 550px;
display: flex;
flex-direction: column;
gap: 25px;
}
.imageWrapper {
position: relative;
img {
width: 100%;
display: block;
}
}
.fileWrapper {
display: flex;
flex-direction: column;
gap: 15px;
}
.mark {
position: absolute;

View File

@ -8,12 +8,6 @@ export interface MouseState {
}
}
export interface PdfFile {
file: File
pages: PdfPage[]
expanded?: boolean
}
export interface PdfPage {
image: string
drawnFields: DrawnField[]

View File

@ -1,9 +1,8 @@
import { PdfFile } from './drawing.ts'
import { SigitFile } from '../utils/file'
export interface CurrentUserFile {
id: number
pdfFile: PdfFile
filename: string
file: SigitFile
hash?: string
isHashValid: boolean
}

View File

@ -26,3 +26,94 @@ export const DEFAULT_LOOK_UP_RELAY_LIST = [
'wss://user.kindpag.es',
'wss://purplepag.es'
]
// Uses the https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types list
// Updated on 2024/08/22
export const MOST_COMMON_MEDIA_TYPES = new Map([
['aac', 'audio/aac'], // AAC audio
['abw', 'application/x-abiword'], // AbiWord document
['apng', 'image/apng'], // Animated Portable Network Graphics (APNG) image
['arc', 'application/x-freearc'], // Archive document (multiple files embedded)
['avif', 'image/avif'], // AVIF image
['avi', 'video/x-msvideo'], // AVI: Audio Video Interleave
['azw', 'application/vnd.amazon.ebook'], // Amazon Kindle eBook format
['bin', 'application/octet-stream'], // Any kind of binary data
['bmp', 'image/bmp'], // Windows OS/2 Bitmap Graphics
['bz', 'application/x-bzip'], // BZip archive
['bz2', 'application/x-bzip2'], // BZip2 archive
['cda', 'application/x-cdf'], // CD audio
['csh', 'application/x-csh'], // C-Shell script
['css', 'text/css'], // Cascading Style Sheets (CSS)
['csv', 'text/csv'], // Comma-separated values (CSV)
['doc', 'application/msword'], // Microsoft Word
[
'docx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
], // Microsoft Word (OpenXML)
['eot', 'application/vnd.ms-fontobject'], // MS Embedded OpenType fonts
['epub', 'application/epub+zip'], // Electronic publication (EPUB)
['gz', 'application/gzip'], // GZip Compressed Archive
['gif', 'image/gif'], // Graphics Interchange Format (GIF)
['htm', 'text/html'], // HyperText Markup Language (HTML)
['html', 'text/html'], // HyperText Markup Language (HTML)
['ico', 'image/vnd.microsoft.icon'], // Icon format
['ics', 'text/calendar'], // iCalendar format
['jar', 'application/java-archive'], // Java Archive (JAR)
['jpeg', 'image/jpeg'], // JPEG images
['jpg', 'image/jpeg'], // JPEG images
['js', 'text/javascript'], // JavaScript
['json', 'application/json'], // JSON format
['jsonld', 'application/ld+json'], // JSON-LD format
['mid', 'audio/midi'], // Musical Instrument Digital Interface (MIDI)
['midi', 'audio/midi'], // Musical Instrument Digital Interface (MIDI)
['mjs', 'text/javascript'], // JavaScript module
['mp3', 'audio/mpeg'], // MP3 audio
['mp4', 'video/mp4'], // MP4 video
['mpeg', 'video/mpeg'], // MPEG Video
['mpkg', 'application/vnd.apple.installer+xml'], // Apple Installer Package
['odp', 'application/vnd.oasis.opendocument.presentation'], // OpenDocument presentation document
['ods', 'application/vnd.oasis.opendocument.spreadsheet'], // OpenDocument spreadsheet document
['odt', 'application/vnd.oasis.opendocument.text'], // OpenDocument text document
['oga', 'audio/ogg'], // Ogg audio
['ogv', 'video/ogg'], // Ogg video
['ogx', 'application/ogg'], // Ogg
['opus', 'audio/ogg'], // Opus audio in Ogg container
['otf', 'font/otf'], // OpenType font
['png', 'image/png'], // Portable Network Graphics
['pdf', 'application/pdf'], // Adobe Portable Document Format (PDF)
['php', 'application/x-httpd-php'], // Hypertext Preprocessor (Personal Home Page)
['ppt', 'application/vnd.ms-powerpoint'], // Microsoft PowerPoint
[
'pptx',
'application/vnd.openxmlformats-officedocument.presentationml.presentation'
], // Microsoft PowerPoint (OpenXML)
['rar', 'application/vnd.rar'], // RAR archive
['rtf', 'application/rtf'], // Rich Text Format (RTF)
['sh', 'application/x-sh'], // Bourne shell script
['svg', 'image/svg+xml'], // Scalable Vector Graphics (SVG)
['tar', 'application/x-tar'], // Tape Archive (TAR)
['tif', 'image/tiff'], // Tagged Image File Format (TIFF)
['tiff', 'image/tiff'], // Tagged Image File Format (TIFF)
['ts', 'video/mp2t'], // MPEG transport stream
['ttf', 'font/ttf'], // TrueType Font
['txt', 'text/plain'], // Text, (generally ASCII or ISO 8859-n)
['vsd', 'application/vnd.visio'], // Microsoft Visio
['wav', 'audio/wav'], // Waveform Audio Format
['weba', 'audio/webm'], // WEBM audio
['webm', 'video/webm'], // WEBM video
['webp', 'image/webp'], // WEBP image
['woff', 'font/woff'], // Web Open Font Format (WOFF)
['woff2', 'font/woff2'], // Web Open Font Format (WOFF)
['xhtml', 'application/xhtml+xml'], // XHTML
['xls', 'application/vnd.ms-excel'], // Microsoft Excel
[
'.xlsx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
], // Microsoft Excel (OpenXML)
['xml', 'application/xml'], // XML
['xul', 'application/vnd.mozilla.xul+xml'], // XUL
['zip', 'application/zip'], // ZIP archive
['3gp', 'video/3gpp'], // 3GPP audio/video container
['3g2', 'video/3gpp2'], // 3GPP2 audio/video container
['7z', 'application/x-7z-compressed'] // 7-zip archive
])

View File

@ -1,24 +1,139 @@
import { Meta } from '../types'
import { PdfPage } from '../types/drawing.ts'
import { MOST_COMMON_MEDIA_TYPES } from './const.ts'
import { extractMarksFromSignedMeta } from './mark.ts'
import { addMarks, convertToPdfBlob, groupMarksByFileNamePage } from './pdf.ts'
import {
addMarks,
convertToPdfBlob,
groupMarksByFileNamePage,
isPdf,
pdfToImages
} from './pdf.ts'
import JSZip from 'jszip'
import { PdfFile } from '../types/drawing.ts'
const getZipWithFiles = async (
export const getZipWithFiles = async (
meta: Meta,
files: { [filename: string]: PdfFile }
files: { [filename: string]: SigitFile }
): Promise<JSZip> => {
const zip = new JSZip()
const marks = extractMarksFromSignedMeta(meta)
const marksByFileNamePage = groupMarksByFileNamePage(marks)
for (const [fileName, pdf] of Object.entries(files)) {
const pages = await addMarks(pdf.file, marksByFileNamePage[fileName])
for (const [fileName, file] of Object.entries(files)) {
if (file.isPdf) {
// Handle PDF Files
const pages = await addMarks(file, marksByFileNamePage[fileName])
const blob = await convertToPdfBlob(pages)
zip.file(`files/${fileName}`, blob)
} else {
// Handle other files
zip.file(`files/${fileName}`, file)
}
}
return zip
}
export { getZipWithFiles }
/**
* 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
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<SigitFile> => {
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())
}

View File

@ -2,6 +2,7 @@ import { CreateSignatureEventContent, Meta } from '../types'
import { fromUnixTimestamp, parseJson } from '.'
import { Event, verifyEvent } from 'nostr-tools'
import { toast } from 'react-toastify'
import { extractFileExtensions } from './file'
export enum SignStatus {
Signed = 'Signed',
@ -172,29 +173,3 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
}
}
}
/**
* @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) => {
return fileName.split('.').pop()
}

View File

@ -1,4 +1,4 @@
import { PdfFile, PdfPage } from '../types/drawing.ts'
import { PdfPage } from '../types/drawing.ts'
import * as PDFJS from 'pdfjs-dist'
import { PDFDocument } from 'pdf-lib'
import { Mark } from '../types/mark.ts'
@ -12,7 +12,7 @@ PDFJS.GlobalWorkerOptions.workerSrc = new URL(
* Scale between the PDF page's natural size and rendered size
* @constant {number}
*/
const SCALE: number = 3
export 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
@ -20,58 +20,28 @@ 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
export const FONT_SIZE: number = 40
/**
* Current font type used when generating a PDF.
*/
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' })
}
/**
* 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 data = await readPdf(file)
const pages = await pdfToImages(data)
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[]> => {
return Promise.all(selectedFiles.filter(isPdf).map(toPdfFile))
}
export const FONT_TYPE: string = 'Arial'
/**
* A utility that transforms a drawing coordinate number into a CSS-compatible string
* @param coordinate
*/
const inPx = (coordinate: number): string => `${coordinate}px`
export 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')
export const isPdf = (file: File) => file.type.toLowerCase().includes('pdf')
/**
* Reads the pdf file binaries
*/
const readPdf = (file: File): Promise<string | ArrayBuffer> => {
export const readPdf = (file: File): Promise<string | ArrayBuffer> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
@ -99,7 +69,9 @@ const readPdf = (file: File): Promise<string | ArrayBuffer> => {
* Converts pdf to the images
* @param data pdf file bytes
*/
const pdfToImages = async (data: string | ArrayBuffer): Promise<PdfPage[]> => {
export const pdfToImages = async (
data: string | ArrayBuffer
): Promise<PdfPage[]> => {
const images: string[] = []
const pdf = await PDFJS.getDocument(data).promise
const canvas = document.createElement('canvas')
@ -129,7 +101,7 @@ const pdfToImages = async (data: string | ArrayBuffer): Promise<PdfPage[]> => {
* 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 (
export const addMarks = async (
file: File,
marksPerPage: { [key: string]: Mark[] }
) => {
@ -159,7 +131,7 @@ const addMarks = async (
/**
* Utility to scale mark in line with the PDF-to-PNG scale
*/
const scaleMark = (mark: Mark): Mark => {
export const scaleMark = (mark: Mark): Mark => {
const { location } = mark
return {
...mark,
@ -177,14 +149,14 @@ const scaleMark = (mark: Mark): Mark => {
* Utility to check if a Mark has value
* @param mark
*/
const hasValue = (mark: Mark): boolean => !!mark.value
export 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) => {
export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
const { location } = mark
ctx!.font = FONT_SIZE + 'px ' + FONT_TYPE
@ -199,7 +171,9 @@ const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
* 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> => {
export const convertToPdfBlob = async (
markedPdfPages: string[]
): Promise<Blob> => {
const pdfDoc = await PDFDocument.create()
for (const page of markedPdfPages) {
@ -217,30 +191,17 @@ const convertToPdfBlob = async (markedPdfPages: string[]): Promise<Blob> => {
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 groupMarksByFileNamePage = (marks: Mark[]) => {
export const groupMarksByFileNamePage = (marks: Mark[]) => {
return marks
.filter(hasValue)
.map(scaleMark)
.reduce<{ [filename: string]: { [page: number]: Mark[] } }>(byPage, {})
.reduce<{ [fileName: string]: { [page: number]: Mark[] } }>(byPage, {})
}
/**
@ -251,30 +212,19 @@ const groupMarksByFileNamePage = (marks: Mark[]) => {
* @param obj - accumulator in the reducer callback
* @param mark - current value, i.e. Mark being examined
*/
const byPage = (
export const byPage = (
obj: { [filename: string]: { [page: number]: Mark[] } },
mark: Mark
) => {
const filename = mark.fileName
const fileName = mark.fileName
const pageNumber = mark.location.page
const pages = obj[filename] ?? {}
const pages = obj[fileName] ?? {}
const marks = pages[pageNumber] ?? []
return {
...obj,
[filename]: {
[fileName]: {
...pages,
[pageNumber]: [...marks, mark]
}
}
}
export {
toFile,
toPdfFile,
toPdfFiles,
inPx,
convertToPdfFile,
addMarks,
convertToPdfBlob,
groupMarksByFileNamePage
}

View File

@ -1,5 +1,5 @@
import { PdfFile } from '../types/drawing.ts'
import { CurrentUserFile } from '../types/file.ts'
import { SigitFile } from './file.ts'
export const compareObjects = (
obj1: object | null | undefined,
@ -75,13 +75,13 @@ export const timeout = (ms: number = 60000) => {
* @param creatorFileHashes
*/
export const getCurrentUserFiles = (
files: { [filename: string]: PdfFile },
files: { [filename: string]: SigitFile },
fileHashes: { [key: string]: string | null },
creatorFileHashes: { [key: string]: string }
): CurrentUserFile[] => {
return Object.entries(files).map(([filename, pdfFile], index) => {
return Object.entries(files).map(([filename, file], index) => {
return {
pdfFile,
file,
filename,
id: index + 1,
...(!!fileHashes[filename] && { hash: fileHashes[filename]! }),
@ -89,3 +89,32 @@ export const getCurrentUserFiles = (
}
})
}
/**
* Utility function that generates a promise with a callback on each array item
* and retuns only non-null fulfilled results
* @param array
* @param cb callback that generates a promise
* @returns Array with the non-null results
*/
export const settleAllFullfilfedPromises = async <Item, FulfilledItem = Item>(
array: Item[],
cb: (arg: Item) => Promise<FulfilledItem>
) => {
// Run the callback on the array to get promises
const promises = array.map(cb)
// Use Promise.allSettled to wait for all promises to settle
const results = await Promise.allSettled(promises)
// Extract non-null values from fulfilled promises in a single pass
return results.reduce<NonNullable<FulfilledItem>[]>((acc, result) => {
if (result.status === 'fulfilled') {
const value = result.value
if (value) {
acc.push(value)
}
}
return acc
}, [])
}