fix(pdf): dynamic mark scaling #165
@ -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 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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()}
|
||||||
|
@ -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) => {
|
||||||
<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(m.location.left * page.scale),
|
||||||
width: inPx(m.location.width),
|
top: inPx(m.location.top * page.scale),
|
||||||
height: inPx(m.location.height)
|
width: inPx(m.location.width * page.scale),
|
||||||
}}
|
height: inPx(m.location.height * page.scale),
|
||||||
>
|
fontFamily: FONT_TYPE,
|
||||||
{m.value}
|
fontSize: inPx(FONT_SIZE * page.scale)
|
||||||
</div>
|
}}
|
||||||
))}
|
>
|
||||||
|
{m.value}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 =
|
||||||
|
Loading…
Reference in New Issue
Block a user