fix(pdf): dynamic mark scaling
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s

Caveat: scaling is not fluid, refresh is required
This commit is contained in:
enes 2024-08-23 21:30:32 +02:00
parent ecc1707212
commit ea09daa669
11 changed files with 87 additions and 88 deletions

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 */
} }
} }

View File

@ -17,6 +17,7 @@ import { settleAllFullfilfedPromises, hexToNpub } 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'
PDFJS.GlobalWorkerOptions.workerSrc = new URL( PDFJS.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@ -105,8 +106,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: mouseX / page.scale,
top: mouseY, top: mouseY / page.scale,
width: 0, width: 0,
height: 0, height: 0,
counterpart: '', counterpart: '',
@ -160,8 +161,8 @@ export const DrawPDFFields = (props: Props) => {
const { mouseX, mouseY } = getMouseCoordinates(event) const { mouseX, mouseY } = getMouseCoordinates(event)
const width = mouseX - lastDrawnField.left const width = mouseX / page.scale - lastDrawnField.left
const height = mouseY - lastDrawnField.top const height = mouseY / page.scale - lastDrawnField.top
lastDrawnField.width = width lastDrawnField.width = width
lastDrawnField.height = height lastDrawnField.height = height
@ -209,7 +210,8 @@ export const DrawPDFFields = (props: Props) => {
*/ */
const onDrawnFieldMouseMove = ( const onDrawnFieldMouseMove = (
event: React.MouseEvent<HTMLDivElement>, event: React.MouseEvent<HTMLDivElement>,
drawnField: DrawnField drawnField: DrawnField,
scale: number
) => { ) => {
if (mouseState.dragging) { if (mouseState.dragging) {
const { mouseX, mouseY, rect } = getMouseCoordinates( const { mouseX, mouseY, rect } = getMouseCoordinates(
@ -219,11 +221,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 = (mouseX - coordsOffset.mouseX) / scale
let top = mouseY - coordsOffset.mouseY let top = (mouseY - coordsOffset.mouseY) / scale
const rightLimit = rect.width - drawnField.width - 3 const rightLimit = rect.width / scale - drawnField.width - 3
const bottomLimit = rect.height - drawnField.height - 3 const bottomLimit = rect.height / scale - 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 +265,8 @@ export const DrawPDFFields = (props: Props) => {
*/ */
const onResizeHandleMouseMove = ( const onResizeHandleMouseMove = (
event: React.MouseEvent<HTMLSpanElement>, event: React.MouseEvent<HTMLSpanElement>,
drawnField: DrawnField drawnField: DrawnField,
scale: number
) => { ) => {
if (mouseState.resizing) { if (mouseState.resizing) {
const { mouseX, mouseY } = getMouseCoordinates( const { mouseX, mouseY } = getMouseCoordinates(
@ -274,8 +277,8 @@ export const DrawPDFFields = (props: Props) => {
event.currentTarget.parentElement?.parentElement event.currentTarget.parentElement?.parentElement
) )
const width = mouseX - drawnField.left const width = mouseX / scale - drawnField.left
const height = mouseY - drawnField.top const height = mouseY / scale - drawnField.top
drawnField.width = width drawnField.width = width
drawnField.height = height drawnField.height = height
@ -372,21 +375,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.scale)
}} }}
className={styles.drawingRectangle} className={styles.drawingRectangle}
style={{ style={{
left: `${drawnField.left}px`, left: inPx(drawnField.left * page.scale),
top: `${drawnField.top}px`, top: inPx(drawnField.top * page.scale),
width: `${drawnField.width}px`, width: inPx(drawnField.width * page.scale),
height: `${drawnField.height}px`, height: inPx(drawnField.height * page.scale),
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.scale)
}} }}
className={styles.resizeHandle} className={styles.resizeHandle}
></span> ></span>

View File

@ -7,6 +7,7 @@ interface PdfMarkItemProps {
handleMarkClick: (id: number) => void handleMarkClick: (id: number) => void
selectedMarkValue: string selectedMarkValue: string
selectedMark: CurrentUserMark | null selectedMark: CurrentUserMark | null
scale: number
} }
/** /**
@ -16,7 +17,8 @@ const PdfMarkItem = ({
selectedMark, selectedMark,
handleMarkClick, handleMarkClick,
selectedMarkValue, selectedMarkValue,
userMark userMark,
scale
}: PdfMarkItemProps) => { }: PdfMarkItemProps) => {
const { location } = userMark.mark const { location } = userMark.mark
const handleClick = () => handleMarkClick(userMark.mark.id) const handleClick = () => handleMarkClick(userMark.mark.id)
@ -28,12 +30,12 @@ const PdfMarkItem = ({
onClick={handleClick} onClick={handleClick}
className={`file-mark ${styles.drawingRectangle} ${isEdited() && styles.edited}`} className={`file-mark ${styles.drawingRectangle} ${isEdited() && styles.edited}`}
style={{ style={{
left: inPx(location.left), left: inPx(location.left * scale),
top: inPx(location.top), top: inPx(location.top * scale),
width: inPx(location.width), width: inPx(location.width * scale),
height: inPx(location.height), height: inPx(location.height * scale),
fontFamily: FONT_TYPE, fontFamily: FONT_TYPE,
fontSize: inPx(FONT_SIZE) fontSize: inPx(FONT_SIZE * scale)
}} }}
> >
{getMarkValue()} {getMarkValue()}

View File

@ -4,7 +4,7 @@ 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'
interface PdfPageProps { interface PdfPageProps {
currentUserMarks: CurrentUserMark[] currentUserMarks: CurrentUserMark[]
handleMarkClick: (id: number) => void handleMarkClick: (id: number) => void
@ -44,23 +44,28 @@ const PdfPageItem = ({
selectedMarkValue={selectedMarkValue} selectedMarkValue={selectedMarkValue}
userMark={m} userMark={m}
selectedMark={selectedMark} selectedMark={selectedMark}
scale={page.scale}
/> />
</div> </div>
))} ))}
{otherUserMarks.map((m, i) => ( {otherUserMarks.map((m, i) => {
return (
<div <div
key={i} key={i}
className={pdfViewStyles.otherUserMarksDisplay} className={pdfViewStyles.otherUserMarksDisplay}
style={{ style={{
left: inPx(m.location.left), left: inPx(m.location.left * page.scale),
top: inPx(m.location.top), top: inPx(m.location.top * page.scale),
width: inPx(m.location.width), width: inPx(m.location.width * page.scale),
height: inPx(m.location.height) height: inPx(m.location.height * page.scale),
fontFamily: FONT_TYPE,
fontSize: inPx(FONT_SIZE * page.scale)
}} }}
> >
{m.value} {m.value}
</div> </div>
))} )
})}
</div> </div>
) )
} }

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

@ -110,12 +110,12 @@ const SlimPdfView = ({
className={`file-mark ${styles.mark}`} className={`file-mark ${styles.mark}`}
key={m.id} key={m.id}
style={{ style={{
left: inPx(m.location.left), left: inPx(m.location.left * page.scale),
top: inPx(m.location.top), top: inPx(m.location.top * page.scale),
width: inPx(m.location.width), width: inPx(m.location.width * page.scale),
height: inPx(m.location.height), height: inPx(m.location.height * page.scale),
fontFamily: FONT_TYPE, fontFamily: FONT_TYPE,
fontSize: inPx(FONT_SIZE) fontSize: inPx(FONT_SIZE * page.scale)
}} }}
> >
{m.value} {m.value}
@ -431,7 +431,7 @@ export const VerifyPage = () => {
for (const [fileName, file] of Object.entries(files)) { for (const [fileName, file] of Object.entries(files)) {
if (file.isPdf) { if (file.isPdf) {
// Draw marks into PDF file and generate a brand new blob // Draw marks into PDF file and generate a brand new blob
const pages = await addMarks(file, file.pages!, marksByPage[fileName]) const pages = await addMarks(file, marksByPage[fileName])
const blob = await convertToPdfBlob(pages) const blob = await convertToPdfBlob(pages)
zip.file(`files/${fileName}`, blob) zip.file(`files/${fileName}`, blob)
} else { } else {

View File

@ -1,3 +1,5 @@
import { MarkRect } from './mark'
export interface MouseState { export interface MouseState {
clicked?: boolean clicked?: boolean
dragging?: boolean dragging?: boolean
@ -14,11 +16,7 @@ export interface PdfPage {
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

@ -22,11 +22,7 @@ export const getZipWithFiles = async (
for (const [fileName, file] of Object.entries(files)) { for (const [fileName, file] of Object.entries(files)) {
if (file.isPdf) { if (file.isPdf) {
// Handle PDF Files // Handle PDF Files
const pages = await addMarks( const pages = await addMarks(file, marksByFileNamePage[fileName])
file,
file.pages!,
marksByFileNamePage[fileName]
)
const blob = await convertToPdfBlob(pages) const blob = await convertToPdfBlob(pages)
zip.file(`files/${fileName}`, blob) zip.file(`files/${fileName}`, blob)
} else { } else {
@ -78,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()
/**
* Default width of the rendered element on the website
* @constant {number}
*/
export const DEFAULT_VIEWPORT_WIDTH = 550
/** /**
* Scale between the PDF page's natural size and rendered size * Scale between the PDF page's natural size and rendered size
* @constant {number} * @constant {number}
@ -32,11 +27,17 @@ export const FONT_SIZE: number = 16
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`
/**
* A utility that transforms a drawing coordinate number into a CSS-compatible percentage string
* @param coordinate
*/
export const inPerc = (coordinate: number): string => `${coordinate}%`
/** /**
* A utility that checks if a given file is of the pdf type * A utility that checks if a given file is of the pdf type
* @param file * @param file
@ -80,14 +81,14 @@ export const pdfToImages = async (
const pages: PdfPage[] = [] 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 = document.querySelector('#content-preview > *')?.clientWidth || 1
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 originalViewport = page.getViewport({ scale: 1 }) const originalViewport = page.getViewport({ scale: 1 })
const scale = originalViewport.width / DEFAULT_VIEWPORT_WIDTH const scale = width / originalViewport.width
const viewport = page.getViewport({ scale: scale })
const viewport = page.getViewport({ 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
@ -110,7 +111,6 @@ export const pdfToImages = async (
*/ */
export const addMarks = async ( export const addMarks = async (
file: File, file: File,
pages: PdfPage[],
marksPerPage: { [key: string]: Mark[] } marksPerPage: { [key: string]: Mark[] }
) => { ) => {
const p = await readPdf(file) const p = await readPdf(file)
@ -129,7 +129,7 @@ export const addMarks = async (
await page.render({ canvasContext: context, viewport: viewport }).promise 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, pages[i].scale)) marksPerPage[i]?.forEach((mark) => draw(mark, context))
} }
images.push(canvas.toDataURL()) images.push(canvas.toDataURL())
@ -169,14 +169,9 @@ export const hasValue = (mark: Mark): boolean => !!mark.value
* @param mark to be drawn * @param mark to be drawn
* @param ctx a Canvas representation of a specific PDF Page * @param ctx a Canvas representation of a specific PDF Page
*/ */
export const draw = ( export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
mark: Mark, const { location } = mark
ctx: CanvasRenderingContext2D, ctx.font = FONT_SIZE + 'px ' + FONT_TYPE
scale: number
) => {
const { location } = scaleMark(mark, scale)
ctx.font = scale * 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 = const textHeight =