fix(pdf): dynamic mark scaling #165
23
package-lock.json
generated
23
package-lock.json
generated
@ -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",
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
17
src/App.scss
17
src/App.scss
@ -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 */
|
||||||
|
@ -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' }}>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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()}
|
||||||
|
@ -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
46
src/hooks/useScale.tsx
Normal 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
|
||||||
|
})
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user