fix(pdf): dynamic mark scaling #165

Merged
enes merged 9 commits from 146-pdf-scaling into staging 2024-08-28 11:23:34 +00:00
15 changed files with 285 additions and 107 deletions

23
package-lock.json generated
View File

@ -1,13 +1,14 @@
{ {
"name": "web", "name": "sigit",
"version": "0.0.0", "version": "0.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "web", "name": "sigit",
"version": "0.0.0", "version": "0.0.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0-or-later ",
"dependencies": { "dependencies": {
"@emotion/react": "11.11.4", "@emotion/react": "11.11.4",
"@emotion/styled": "11.11.0", "@emotion/styled": "11.11.0",
@ -40,6 +41,7 @@
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-redux": "9.1.0", "react-redux": "9.1.0",
"react-router-dom": "6.22.1", "react-router-dom": "6.22.1",
"react-singleton-hook": "^4.0.1",
"react-toastify": "10.0.4", "react-toastify": "10.0.4",
"redux": "5.0.1", "redux": "5.0.1",
"tseep": "1.2.1" "tseep": "1.2.1"
@ -5832,6 +5834,23 @@
"react-dom": ">=16.8" "react-dom": ">=16.8"
} }
}, },
"node_modules/react-singleton-hook": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/react-singleton-hook/-/react-singleton-hook-4.0.1.tgz",
"integrity": "sha512-fWuk8VxcZPChrkQasDLM8pgd/7kyi+Cr/5FfCiD99FicjEru+JmtEZNnN4lJ8Z7KbqAST5CYPlpz6lmNsZFGNw==",
"license": "MIT",
"peerDependencies": {
"react": "18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-toastify": { "node_modules/react-toastify": {
"version": "10.0.4", "version": "10.0.4",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.4.tgz", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.4.tgz",

View File

@ -51,6 +51,7 @@
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-redux": "9.1.0", "react-redux": "9.1.0",
"react-router-dom": "6.22.1", "react-router-dom": "6.22.1",
"react-singleton-hook": "^4.0.1",
"react-toastify": "10.0.4", "react-toastify": "10.0.4",
"redux": "5.0.1", "redux": "5.0.1",
"tseep": "1.2.1" "tseep": "1.2.1"
@ -82,4 +83,4 @@
], ],
"*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}": "npm run formatter:staged" "*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}": "npm run formatter:staged"
} }
} }

View File

@ -100,12 +100,10 @@ input {
-webkit-user-select: none; -webkit-user-select: none;
user-select: none; user-select: none;
overflow: hidden; /* Ensure no overflow */
> img { > img {
display: block; display: block;
max-width: 100%; width: 100%;
max-height: 100%; height: auto;
object-fit: contain; /* Ensure the image fits within the container */ object-fit: contain; /* Ensure the image fits within the container */
} }
} }
@ -121,6 +119,17 @@ input {
object-fit: contain; /* Ensure the image fits within the container */ object-fit: contain; /* Ensure the image fits within the container */
} }
// Consistent styling for every file mark
// Reverts some of the design defaults for font
.file-mark {
font-family: Arial;
font-size: 16px;
font-weight: normal;
color: black;
letter-spacing: normal;
border: 1px solid transparent;
}
[data-dev='true'] { [data-dev='true'] {
.image-wrapper { .image-wrapper {
// outline: 1px solid #ccc; /* Optional: for visual debugging */ // outline: 1px solid #ccc; /* Optional: for visual debugging */

View File

@ -4,6 +4,8 @@ import {
CircularProgress, CircularProgress,
FormControl, FormControl,
InputLabel, InputLabel,
ListItemIcon,
ListItemText,
MenuItem, MenuItem,
Select Select
} from '@mui/material' } from '@mui/material'
@ -13,10 +15,13 @@ import * as PDFJS from 'pdfjs-dist'
import { ProfileMetadata, User, UserRole } from '../../types' import { ProfileMetadata, User, UserRole } from '../../types'
import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing' import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing'
import { truncate } from 'lodash' import { truncate } from 'lodash'
import { settleAllFullfilfedPromises, hexToNpub } from '../../utils' import { settleAllFullfilfedPromises, hexToNpub, npubToHex } from '../../utils'
import { getSigitFile, SigitFile } from '../../utils/file' import { getSigitFile, SigitFile } from '../../utils/file'
import { FileDivider } from '../FileDivider' import { FileDivider } from '../FileDivider'
import { ExtensionFileBox } from '../ExtensionFileBox' import { ExtensionFileBox } from '../ExtensionFileBox'
import { inPx } from '../../utils/pdf'
import { useScale } from '../../hooks/useScale'
import { AvatarIconButton } from '../UserAvatarIconButton'
PDFJS.GlobalWorkerOptions.workerSrc = new URL( PDFJS.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@ -33,6 +38,7 @@ interface Props {
export const DrawPDFFields = (props: Props) => { export const DrawPDFFields = (props: Props) => {
const { selectedFiles, selectedTool, onDrawFieldsChange, users } = props const { selectedFiles, selectedTool, onDrawFieldsChange, users } = props
const { to, from } = useScale()
const [sigitFiles, setSigitFiles] = useState<SigitFile[]>([]) const [sigitFiles, setSigitFiles] = useState<SigitFile[]>([])
const [parsingPdf, setIsParsing] = useState<boolean>(false) const [parsingPdf, setIsParsing] = useState<boolean>(false)
@ -105,8 +111,8 @@ export const DrawPDFFields = (props: Props) => {
const { mouseX, mouseY } = getMouseCoordinates(event) const { mouseX, mouseY } = getMouseCoordinates(event)
const newField: DrawnField = { const newField: DrawnField = {
left: mouseX, left: to(page.width, mouseX),
top: mouseY, top: to(page.width, mouseY),
width: 0, width: 0,
height: 0, height: 0,
counterpart: '', counterpart: '',
@ -160,8 +166,8 @@ export const DrawPDFFields = (props: Props) => {
const { mouseX, mouseY } = getMouseCoordinates(event) const { mouseX, mouseY } = getMouseCoordinates(event)
const width = mouseX - lastDrawnField.left const width = to(page.width, mouseX) - lastDrawnField.left
const height = mouseY - lastDrawnField.top const height = to(page.width, mouseY) - lastDrawnField.top
lastDrawnField.width = width lastDrawnField.width = width
lastDrawnField.height = height lastDrawnField.height = height
@ -209,7 +215,8 @@ export const DrawPDFFields = (props: Props) => {
*/ */
const onDrawnFieldMouseMove = ( const onDrawnFieldMouseMove = (
event: React.MouseEvent<HTMLDivElement>, event: React.MouseEvent<HTMLDivElement>,
drawnField: DrawnField drawnField: DrawnField,
pageWidth: number
) => { ) => {
if (mouseState.dragging) { if (mouseState.dragging) {
const { mouseX, mouseY, rect } = getMouseCoordinates( const { mouseX, mouseY, rect } = getMouseCoordinates(
@ -219,11 +226,11 @@ export const DrawPDFFields = (props: Props) => {
const coordsOffset = mouseState.coordsInWrapper const coordsOffset = mouseState.coordsInWrapper
if (coordsOffset) { if (coordsOffset) {
let left = mouseX - coordsOffset.mouseX let left = to(pageWidth, mouseX - coordsOffset.mouseX)
let top = mouseY - coordsOffset.mouseY let top = to(pageWidth, mouseY - coordsOffset.mouseY)
const rightLimit = rect.width - drawnField.width - 3 const rightLimit = to(pageWidth, rect.width) - drawnField.width - 3
const bottomLimit = rect.height - drawnField.height - 3 const bottomLimit = to(pageWidth, rect.height) - drawnField.height - 3
if (left < 0) left = 0 if (left < 0) left = 0
if (top < 0) top = 0 if (top < 0) top = 0
@ -263,7 +270,8 @@ export const DrawPDFFields = (props: Props) => {
*/ */
const onResizeHandleMouseMove = ( const onResizeHandleMouseMove = (
event: React.MouseEvent<HTMLSpanElement>, event: React.MouseEvent<HTMLSpanElement>,
drawnField: DrawnField drawnField: DrawnField,
pageWidth: number
) => { ) => {
if (mouseState.resizing) { if (mouseState.resizing) {
const { mouseX, mouseY } = getMouseCoordinates( const { mouseX, mouseY } = getMouseCoordinates(
@ -274,8 +282,8 @@ export const DrawPDFFields = (props: Props) => {
event.currentTarget.parentElement?.parentElement event.currentTarget.parentElement?.parentElement
) )
const width = mouseX - drawnField.left const width = to(pageWidth, mouseX) - drawnField.left
const height = mouseY - drawnField.top const height = to(pageWidth, mouseY) - drawnField.top
drawnField.width = width drawnField.width = width
drawnField.height = height drawnField.height = height
@ -372,21 +380,21 @@ export const DrawPDFFields = (props: Props) => {
key={drawnFieldIndex} key={drawnFieldIndex}
onMouseDown={onDrawnFieldMouseDown} onMouseDown={onDrawnFieldMouseDown}
onMouseMove={(event) => { onMouseMove={(event) => {
onDrawnFieldMouseMove(event, drawnField) onDrawnFieldMouseMove(event, drawnField, page.width)
}} }}
className={styles.drawingRectangle} className={styles.drawingRectangle}
style={{ style={{
left: `${drawnField.left}px`, left: inPx(from(page.width, drawnField.left)),
top: `${drawnField.top}px`, top: inPx(from(page.width, drawnField.top)),
width: `${drawnField.width}px`, width: inPx(from(page.width, drawnField.width)),
height: `${drawnField.height}px`, height: inPx(from(page.width, drawnField.height)),
pointerEvents: mouseState.clicked ? 'none' : 'all' pointerEvents: mouseState.clicked ? 'none' : 'all'
}} }}
> >
<span <span
onMouseDown={onResizeHandleMouseDown} onMouseDown={onResizeHandleMouseDown}
onMouseMove={(event) => { onMouseMove={(event) => {
onResizeHandleMouseMove(event, drawnField) onResizeHandleMouseMove(event, drawnField, page.width)
}} }}
className={styles.resizeHandle} className={styles.resizeHandle}
></span> ></span>
@ -420,6 +428,7 @@ export const DrawPDFFields = (props: Props) => {
sx={{ sx={{
background: 'white' background: 'white'
}} }}
renderValue={(value) => renderCounterpartValue(value)}
> >
{users {users
.filter((u) => u.role === UserRole.signer) .filter((u) => u.role === UserRole.signer)
@ -448,7 +457,22 @@ export const DrawPDFFields = (props: Props) => {
key={index} key={index}
value={hexToNpub(user.pubkey)} value={hexToNpub(user.pubkey)}
> >
{displayValue} <ListItemIcon>
<AvatarIconButton
src={metadata?.picture}
hexKey={user.pubkey}
aria-label={`account of user ${displayValue}`}
color="inherit"
sx={{
padding: 0,
'> img': {
width: '30px',
height: '30px'
}
}}
/>
</ListItemIcon>
<ListItemText>{displayValue}</ListItemText>
</MenuItem> </MenuItem>
) )
})} })}
@ -465,6 +489,45 @@ export const DrawPDFFields = (props: Props) => {
) )
} }
const renderCounterpartValue = (value: string) => {
const user = users.find((u) => u.pubkey === npubToHex(value))
if (user) {
let displayValue = truncate(value, {
length: 16
})
const metadata = props.metadata[user.pubkey]
if (metadata) {
displayValue = truncate(
metadata.name || metadata.display_name || metadata.username || value,
{
length: 16
}
)
}
return (
<>
<AvatarIconButton
src={props.metadata[user.pubkey]?.picture}
hexKey={npubToHex(user.pubkey) || undefined}
sx={{
padding: 0,
marginRight: '6px',
'> img': {
width: '21px',
height: '21px'
}
}}
/>
{displayValue}
</>
)
}
return value
}
if (parsingPdf) { if (parsingPdf) {
return ( return (
<Box sx={{ width: '100%', textAlign: 'center' }}> <Box sx={{ width: '100%', textAlign: 'center' }}>

View File

@ -73,7 +73,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
bottom: -60px; bottom: -60px;
min-width: 170px; min-width: 193px;
min-height: 30px; min-height: 30px;
padding: 5px 0; padding: 5px 0;
} }

View File

@ -1,12 +1,14 @@
import { CurrentUserMark } from '../../types/mark.ts' import { CurrentUserMark } from '../../types/mark.ts'
import styles from '../DrawPDFFields/style.module.scss' import styles from '../DrawPDFFields/style.module.scss'
import { inPx } from '../../utils/pdf.ts' import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
import { useScale } from '../../hooks/useScale.tsx'
interface PdfMarkItemProps { interface PdfMarkItemProps {
userMark: CurrentUserMark userMark: CurrentUserMark
handleMarkClick: (id: number) => void handleMarkClick: (id: number) => void
selectedMarkValue: string selectedMarkValue: string
selectedMark: CurrentUserMark | null selectedMark: CurrentUserMark | null
pageWidth: number
} }
/** /**
@ -16,22 +18,26 @@ const PdfMarkItem = ({
selectedMark, selectedMark,
handleMarkClick, handleMarkClick,
selectedMarkValue, selectedMarkValue,
userMark userMark,
pageWidth
}: PdfMarkItemProps) => { }: PdfMarkItemProps) => {
const { location } = userMark.mark const { location } = userMark.mark
const handleClick = () => handleMarkClick(userMark.mark.id) const handleClick = () => handleMarkClick(userMark.mark.id)
const isEdited = () => selectedMark?.mark.id === userMark.mark.id const isEdited = () => selectedMark?.mark.id === userMark.mark.id
const getMarkValue = () => const getMarkValue = () =>
isEdited() ? selectedMarkValue : userMark.currentValue isEdited() ? selectedMarkValue : userMark.currentValue
const { from } = useScale()
return ( return (
<div <div
onClick={handleClick} onClick={handleClick}
className={`${styles.drawingRectangle} ${isEdited() && styles.edited}`} className={`file-mark ${styles.drawingRectangle} ${isEdited() && styles.edited}`}
style={{ style={{
left: inPx(location.left), left: inPx(from(pageWidth, location.left)),
top: inPx(location.top), top: inPx(from(pageWidth, location.top)),
width: inPx(location.width), width: inPx(from(pageWidth, location.width)),
height: inPx(location.height) height: inPx(from(pageWidth, location.height)),
fontFamily: FONT_TYPE,
fontSize: inPx(from(pageWidth, FONT_SIZE))
}} }}
> >
{getMarkValue()} {getMarkValue()}

View File

@ -4,7 +4,8 @@ import { CurrentUserMark, Mark } from '../../types/mark.ts'
import PdfMarkItem from './PdfMarkItem.tsx' import PdfMarkItem from './PdfMarkItem.tsx'
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import pdfViewStyles from './style.module.scss' import pdfViewStyles from './style.module.scss'
import { inPx } from '../../utils/pdf.ts' import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
import { useScale } from '../../hooks/useScale.tsx'
interface PdfPageProps { interface PdfPageProps {
currentUserMarks: CurrentUserMark[] currentUserMarks: CurrentUserMark[]
handleMarkClick: (id: number) => void handleMarkClick: (id: number) => void
@ -33,6 +34,8 @@ const PdfPageItem = ({
} }
}, [selectedMark]) }, [selectedMark])
const markRefs = useRef<(HTMLDivElement | null)[]>([]) const markRefs = useRef<(HTMLDivElement | null)[]>([])
const { from } = useScale()
return ( return (
<div className={`image-wrapper ${styles.pdfImageWrapper}`}> <div className={`image-wrapper ${styles.pdfImageWrapper}`}>
<img draggable="false" src={page.image} /> <img draggable="false" src={page.image} />
@ -44,23 +47,28 @@ const PdfPageItem = ({
selectedMarkValue={selectedMarkValue} selectedMarkValue={selectedMarkValue}
userMark={m} userMark={m}
selectedMark={selectedMark} selectedMark={selectedMark}
pageWidth={page.width}
/> />
</div> </div>
))} ))}
{otherUserMarks.map((m, i) => ( {otherUserMarks.map((m, i) => {
<div return (
key={i} <div
className={pdfViewStyles.otherUserMarksDisplay} key={i}
style={{ className={pdfViewStyles.otherUserMarksDisplay}
left: inPx(m.location.left), style={{
top: inPx(m.location.top), left: inPx(from(page.width, m.location.left)),
width: inPx(m.location.width), top: inPx(from(page.width, m.location.top)),
height: inPx(m.location.height) width: inPx(from(page.width, m.location.width)),
}} height: inPx(from(page.width, m.location.height)),
> fontFamily: FONT_TYPE,
{m.value} fontSize: inPx(from(page.width, FONT_SIZE))
</div> }}
))} >
{m.value}
</div>
)
})}
</div> </div>
) )
} }

46
src/hooks/useScale.tsx Normal file
View File

@ -0,0 +1,46 @@
import { useEffect, useState } from 'react'
import { singletonHook } from 'react-singleton-hook'
import { getInnerContentWidth } from '../utils/pdf'
const noScaleInit = {
to: (_: number, v: number) => v,
from: (_: number, v: number) => v
}
const useScaleImpl = () => {
const [width, setWidth] = useState(getInnerContentWidth())
// Get the scale based on the original width
const scale = (originalWidth: number) => {
return width / originalWidth
}
// Get the original pixel value
const to = (originalWidth: number, value: number) => {
return value / scale(originalWidth)
}
// Get the scaled pixel value
const from = (originalWidth: number, value: number) => {
return value * scale(originalWidth)
}
const resize = () => {
setWidth(getInnerContentWidth())
}
useEffect(() => {
resize()
window.addEventListener('resize', resize)
return () => {
window.removeEventListener('resize', resize)
}
}, [])
return { to, from }
}
export const useScale = singletonHook(noScaleInit, useScaleImpl, {
unmountIfNoConsumers: true
})

View File

@ -29,8 +29,4 @@
padding: 10px; padding: 10px;
border: 10px solid $overlay-background-color; border: 10px solid $overlay-background-color;
border-radius: 4px; border-radius: 4px;
max-width: 590px;
width: 590px;
margin: 0 auto;
} }

View File

@ -17,7 +17,9 @@ export const StickySideColumns = ({
<div className={`${styles.sidesWrap} ${styles.files}`}> <div className={`${styles.sidesWrap} ${styles.files}`}>
<div className={styles.sides}>{left}</div> <div className={styles.sides}>{left}</div>
</div> </div>
<div className={styles.content}>{children}</div> <div id="content-preview" className={styles.content}>
{children}
</div>
<div className={styles.sidesWrap}> <div className={styles.sidesWrap}>
<div className={styles.sides}>{right}</div> <div className={styles.sides}>{right}</div>
</div> </div>

View File

@ -29,6 +29,8 @@ import axios from 'axios'
import { import {
addMarks, addMarks,
convertToPdfBlob, convertToPdfBlob,
FONT_SIZE,
FONT_TYPE,
groupMarksByFileNamePage, groupMarksByFileNamePage,
inPx inPx
} from '../../utils/pdf.ts' } from '../../utils/pdf.ts'
@ -50,6 +52,7 @@ import React from 'react'
import { convertToSigitFile, SigitFile } from '../../utils/file.ts' import { convertToSigitFile, SigitFile } from '../../utils/file.ts'
import { FileDivider } from '../../components/FileDivider.tsx' import { FileDivider } from '../../components/FileDivider.tsx'
import { ExtensionFileBox } from '../../components/ExtensionFileBox.tsx' import { ExtensionFileBox } from '../../components/ExtensionFileBox.tsx'
import { useScale } from '../../hooks/useScale.tsx'
interface PdfViewProps { interface PdfViewProps {
files: CurrentUserFile[] files: CurrentUserFile[]
@ -65,6 +68,7 @@ const SlimPdfView = ({
parsedSignatureEvents parsedSignatureEvents
}: PdfViewProps) => { }: PdfViewProps) => {
const pdfRefs = useRef<(HTMLDivElement | null)[]>([]) const pdfRefs = useRef<(HTMLDivElement | null)[]>([])
const { from } = useScale()
useEffect(() => { useEffect(() => {
if (currentFile !== null && !!pdfRefs.current[currentFile.id]) { if (currentFile !== null && !!pdfRefs.current[currentFile.id]) {
pdfRefs.current[currentFile.id]?.scrollIntoView({ pdfRefs.current[currentFile.id]?.scrollIntoView({
@ -105,13 +109,15 @@ const SlimPdfView = ({
{marks.map((m) => { {marks.map((m) => {
return ( return (
<div <div
className={styles.mark} className={`file-mark ${styles.mark}`}
key={m.id} key={m.id}
style={{ style={{
left: inPx(m.location.left), left: inPx(from(page.width, m.location.left)),
top: inPx(m.location.top), top: inPx(from(page.width, m.location.top)),
width: inPx(m.location.width), width: inPx(from(page.width, m.location.width)),
height: inPx(m.location.height) height: inPx(from(page.width, m.location.height)),
fontFamily: FONT_TYPE,
fontSize: inPx(from(page.width, FONT_SIZE))
}} }}
> >
{m.value} {m.value}

View File

@ -1,3 +1,5 @@
import { MarkRect } from './mark'
export interface MouseState { export interface MouseState {
clicked?: boolean clicked?: boolean
dragging?: boolean dragging?: boolean
@ -10,14 +12,11 @@ export interface MouseState {
export interface PdfPage { export interface PdfPage {
image: string image: string
width: number
drawnFields: DrawnField[] drawnFields: DrawnField[]
} }
export interface DrawnField { export interface DrawnField extends MarkRect {
left: number
top: number
width: number
height: number
type: MarkType type: MarkType
/** /**
* npub of a counter part * npub of a counter part

View File

@ -18,10 +18,13 @@ export interface Mark {
value?: string value?: string
} }
export interface MarkLocation { export interface MarkLocation extends MarkRect {
top: number
left: number
height: number
width: number
page: number page: number
} }
export interface MarkRect {
left: number
top: number
width: number
height: number
}

View File

@ -74,6 +74,7 @@ export const getSigitFile = async (file: File) => {
const sigitFile = new SigitFile(file) const sigitFile = new SigitFile(file)
// Process sigit file // Process sigit file
// - generate pages for PDF files // - generate pages for PDF files
// - generate ObjectRL for image files
await sigitFile.process() await sigitFile.process()
return sigitFile return sigitFile
} }

View File

@ -8,11 +8,6 @@ PDFJS.GlobalWorkerOptions.workerSrc = new URL(
import.meta.url import.meta.url
).toString() ).toString()
/**
* Scale between the PDF page's natural size and rendered size
* @constant {number}
*/
export const SCALE: number = 3
/** /**
* Defined font size used when generating a PDF. Currently it is difficult to fully * 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 * correlate font size used at the time of filling in / drawing on the PDF
@ -20,14 +15,14 @@ export const SCALE: number = 3
* This should be fixed going forward. * This should be fixed going forward.
* Switching to PDF-Lib will most likely make this problem redundant. * Switching to PDF-Lib will most likely make this problem redundant.
*/ */
export const FONT_SIZE: number = 40 export const FONT_SIZE: number = 16
/** /**
* Current font type used when generating a PDF. * Current font type used when generating a PDF.
*/ */
export const FONT_TYPE: string = 'Arial' export const FONT_TYPE: string = 'Arial'
/** /**
* A utility that transforms a drawing coordinate number into a CSS-compatible string * A utility that transforms a drawing coordinate number into a CSS-compatible pixel string
* @param coordinate * @param coordinate
*/ */
export const inPx = (coordinate: number): string => `${coordinate}px` export const inPx = (coordinate: number): string => `${coordinate}px`
@ -65,6 +60,24 @@ export const readPdf = (file: File): Promise<string | ArrayBuffer> => {
}) })
} }
export const getInnerContentWidth = () => {
// Fetch the first container element we find
const element = document.querySelector('#content-preview')
if (element) {
const style = getComputedStyle(element)
// Calculate width without padding
const widthWithoutPadding =
element.clientWidth - parseFloat(style.padding) * 2
return widthWithoutPadding
}
// Default value
return 620
}
/** /**
* Converts pdf to the images * Converts pdf to the images
* @param data pdf file bytes * @param data pdf file bytes
@ -72,28 +85,30 @@ export const readPdf = (file: File): Promise<string | ArrayBuffer> => {
export const pdfToImages = async ( export const pdfToImages = async (
data: string | ArrayBuffer data: string | ArrayBuffer
): Promise<PdfPage[]> => { ): Promise<PdfPage[]> => {
const images: string[] = [] const pages: PdfPage[] = []
const pdf = await PDFJS.getDocument(data).promise const pdf = await PDFJS.getDocument(data).promise
const canvas = document.createElement('canvas') const canvas = document.createElement('canvas')
const width = getInnerContentWidth()
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: SCALE })
const originalViewport = page.getViewport({ scale: 1 })
const scale = width / originalViewport.width
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
await page.render({ canvasContext: context!, viewport: viewport }).promise await page.render({ canvasContext: context!, viewport: viewport }).promise
images.push(canvas.toDataURL()) pages.push({
image: canvas.toDataURL(),
width: originalViewport.width,
drawnFields: []
})
} }
return Promise.resolve( return pages
images.map((image) => {
return {
image,
drawnFields: []
}
})
)
} }
/** /**
@ -113,34 +128,39 @@ export const addMarks = async (
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: SCALE }) const viewport = page.getViewport({ scale: 1 })
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
await page.render({ canvasContext: context!, viewport: viewport }).promise if (context) {
await page.render({ canvasContext: context, viewport: viewport }).promise
if (marksPerPage && Object.hasOwn(marksPerPage, i)) if (marksPerPage && Object.hasOwn(marksPerPage, i)) {
marksPerPage[i]?.forEach((mark) => draw(mark, context!)) marksPerPage[i]?.forEach((mark) => draw(mark, context))
}
images.push(canvas.toDataURL()) images.push(canvas.toDataURL())
}
} }
return Promise.resolve(images) canvas.remove()
return images
} }
/** /**
* Utility to scale mark in line with the PDF-to-PNG scale * Utility to scale mark in line with the PDF-to-PNG scale
*/ */
export const scaleMark = (mark: Mark): Mark => { export const scaleMark = (mark: Mark, scale: number): Mark => {
const { location } = mark const { location } = mark
return { return {
...mark, ...mark,
location: { location: {
...location, ...location,
width: location.width * SCALE, width: location.width * scale,
height: location.height * SCALE, height: location.height * scale,
left: location.left * SCALE, left: location.left * scale,
top: location.top * SCALE top: location.top * scale
} }
} }
} }
@ -158,13 +178,14 @@ export const hasValue = (mark: Mark): boolean => !!mark.value
*/ */
export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => { export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
const { location } = mark const { location } = mark
ctx.font = FONT_SIZE + 'px ' + FONT_TYPE
ctx!.font = FONT_SIZE + 'px ' + FONT_TYPE ctx.fillStyle = 'black'
ctx!.fillStyle = 'black' const textMetrics = ctx.measureText(mark.value!)
const textMetrics = ctx!.measureText(mark.value!) const textHeight =
textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent
const textX = location.left + (location.width - textMetrics.width) / 2 const textX = location.left + (location.width - textMetrics.width) / 2
const textY = location.top + (location.height + parseInt(ctx!.font)) / 2 const textY = location.top + (location.height + textHeight) / 2
ctx!.fillText(mark.value!, textX, textY) ctx.fillText(mark.value!, textX, textY)
} }
/** /**
@ -194,13 +215,11 @@ export const convertToPdfBlob = async (
/** /**
* @param marks - an array of Marks * @param marks - an array of Marks
* @function hasValue removes any Mark without a property * @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 * @function byPage groups remaining Marks by their page marks.location.page
*/ */
export const groupMarksByFileNamePage = (marks: Mark[]) => { export const groupMarksByFileNamePage = (marks: Mark[]) => {
return marks return marks
.filter(hasValue) .filter(hasValue)
.map(scaleMark)
.reduce<{ [fileName: string]: { [page: number]: Mark[] } }>(byPage, {}) .reduce<{ [fileName: string]: { [page: number]: Mark[] } }>(byPage, {})
} }