fix(pdf): pdf quality and multiline #204
10
package-lock.json
generated
10
package-lock.json
generated
@ -21,6 +21,7 @@
|
|||||||
"@mui/material": "5.15.11",
|
"@mui/material": "5.15.11",
|
||||||
"@noble/hashes": "^1.4.0",
|
"@noble/hashes": "^1.4.0",
|
||||||
"@nostr-dev-kit/ndk": "2.5.0",
|
"@nostr-dev-kit/ndk": "2.5.0",
|
||||||
|
"@pdf-lib/fontkit": "^1.1.1",
|
||||||
"@reduxjs/toolkit": "2.2.1",
|
"@reduxjs/toolkit": "2.2.1",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.7.4",
|
||||||
"crypto-hash": "3.0.0",
|
"crypto-hash": "3.0.0",
|
||||||
@ -1749,6 +1750,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@pdf-lib/fontkit": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pdf-lib/fontkit/-/fontkit-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-KjMd7grNapIWS/Dm0gvfHEilSyAmeLvrEGVcqLGi0VYebuqqzTbgF29efCx7tvx+IEbG3zQciRSWl3GkUSvjZg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pako": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pdf-lib/standard-fonts": {
|
"node_modules/@pdf-lib/standard-fonts": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
"@mui/material": "5.15.11",
|
"@mui/material": "5.15.11",
|
||||||
"@noble/hashes": "^1.4.0",
|
"@noble/hashes": "^1.4.0",
|
||||||
"@nostr-dev-kit/ndk": "2.5.0",
|
"@nostr-dev-kit/ndk": "2.5.0",
|
||||||
|
"@pdf-lib/fontkit": "^1.1.1",
|
||||||
"@reduxjs/toolkit": "2.2.1",
|
"@reduxjs/toolkit": "2.2.1",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.7.4",
|
||||||
"crypto-hash": "3.0.0",
|
"crypto-hash": "3.0.0",
|
||||||
|
14
src/App.scss
14
src/App.scss
@ -135,12 +135,18 @@ li {
|
|||||||
// Consistent styling for every file mark
|
// Consistent styling for every file mark
|
||||||
// Reverts some of the design defaults for font
|
// Reverts some of the design defaults for font
|
||||||
.file-mark {
|
.file-mark {
|
||||||
font-family: Arial;
|
font-family: 'Roboto';
|
||||||
font-size: 16px;
|
font-style: normal;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: black;
|
|
||||||
letter-spacing: normal;
|
letter-spacing: normal;
|
||||||
border: 1px solid transparent;
|
line-height: 1;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
color: black;
|
||||||
|
outline: 1px solid transparent;
|
||||||
|
|
||||||
|
justify-content: start;
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
scroll-margin-top: $header-height + $body-vertical-padding;
|
scroll-margin-top: $header-height + $body-vertical-padding;
|
||||||
}
|
}
|
||||||
|
BIN
src/assets/fonts/roboto-regular.ttf
Normal file
BIN
src/assets/fonts/roboto-regular.ttf
Normal file
Binary file not shown.
@ -14,9 +14,10 @@ import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing'
|
|||||||
import { truncate } from 'lodash'
|
import { truncate } from 'lodash'
|
||||||
import { settleAllFullfilfedPromises, hexToNpub, npubToHex } from '../../utils'
|
import { settleAllFullfilfedPromises, hexToNpub, npubToHex } from '../../utils'
|
||||||
import { getSigitFile, SigitFile } from '../../utils/file'
|
import { getSigitFile, SigitFile } from '../../utils/file'
|
||||||
|
import { getToolboxLabelByMarkType } from '../../utils/mark'
|
||||||
import { FileDivider } from '../FileDivider'
|
import { FileDivider } from '../FileDivider'
|
||||||
import { ExtensionFileBox } from '../ExtensionFileBox'
|
import { ExtensionFileBox } from '../ExtensionFileBox'
|
||||||
import { inPx } from '../../utils/pdf'
|
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf'
|
||||||
import { useScale } from '../../hooks/useScale'
|
import { useScale } from '../../hooks/useScale'
|
||||||
import { AvatarIconButton } from '../UserAvatarIconButton'
|
import { AvatarIconButton } from '../UserAvatarIconButton'
|
||||||
import { LoadingSpinner } from '../LoadingSpinner'
|
import { LoadingSpinner } from '../LoadingSpinner'
|
||||||
@ -390,7 +391,7 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
backgroundColor: drawnField.counterpart
|
backgroundColor: drawnField.counterpart
|
||||||
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}4b`
|
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}4b`
|
||||||
: undefined,
|
: undefined,
|
||||||
borderColor: drawnField.counterpart
|
outlineColor: drawnField.counterpart
|
||||||
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}`
|
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}`
|
||||||
: undefined,
|
: undefined,
|
||||||
left: inPx(from(page.width, drawnField.left)),
|
left: inPx(from(page.width, drawnField.left)),
|
||||||
@ -406,6 +407,16 @@ export const DrawPDFFields = (props: Props) => {
|
|||||||
: undefined
|
: undefined
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
className={`file-mark ${styles.placeholder}`}
|
||||||
|
style={{
|
||||||
|
fontFamily: FONT_TYPE,
|
||||||
|
fontSize: inPx(from(page.width, FONT_SIZE))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getToolboxLabelByMarkType(drawnField.type) ||
|
||||||
|
'placeholder'}
|
||||||
|
</div>
|
||||||
<span
|
<span
|
||||||
onPointerDown={(event) =>
|
onPointerDown={(event) =>
|
||||||
handleResizePointerDown(event, drawnFieldIndex)
|
handleResizePointerDown(event, drawnFieldIndex)
|
||||||
|
@ -13,9 +13,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0.5;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.drawingRectangle {
|
.drawingRectangle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border: 1px solid #01aaad;
|
outline: 1px solid #01aaad;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
background-color: #01aaad4b;
|
background-color: #01aaad4b;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -29,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.edited {
|
&.edited {
|
||||||
border: 1px dotted #01aaad;
|
outline: 1px dotted #01aaad;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resizeHandle {
|
.resizeHandle {
|
||||||
|
@ -36,7 +36,7 @@ const PdfMarkItem = forwardRef<HTMLDivElement, PdfMarkItemProps>(
|
|||||||
backgroundColor: selectedMark?.mark.npub
|
backgroundColor: selectedMark?.mark.npub
|
||||||
? `#${npubToHex(selectedMark?.mark.npub)?.substring(0, 6)}4b`
|
? `#${npubToHex(selectedMark?.mark.npub)?.substring(0, 6)}4b`
|
||||||
: undefined,
|
: undefined,
|
||||||
borderColor: selectedMark?.mark.npub
|
outlineColor: selectedMark?.mark.npub
|
||||||
? `#${npubToHex(selectedMark?.mark.npub)?.substring(0, 6)}`
|
? `#${npubToHex(selectedMark?.mark.npub)?.substring(0, 6)}`
|
||||||
: undefined,
|
: undefined,
|
||||||
left: inPx(from(pageWidth, location.left)),
|
left: inPx(from(pageWidth, location.left)),
|
||||||
|
@ -38,41 +38,24 @@ import {
|
|||||||
sendNotification,
|
sendNotification,
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
updateUsersAppData,
|
updateUsersAppData,
|
||||||
uploadToFileStorage
|
uploadToFileStorage,
|
||||||
|
DEFAULT_TOOLBOX
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { Container } from '../../components/Container'
|
import { Container } from '../../components/Container'
|
||||||
import fileListStyles from '../../components/FileList/style.module.scss'
|
import fileListStyles from '../../components/FileList/style.module.scss'
|
||||||
import { DrawTool, MarkType } from '../../types/drawing'
|
import { DrawTool } from '../../types/drawing'
|
||||||
import { DrawPDFFields } from '../../components/DrawPDFFields'
|
import { DrawPDFFields } from '../../components/DrawPDFFields'
|
||||||
import { Mark } from '../../types/mark.ts'
|
import { Mark } from '../../types/mark.ts'
|
||||||
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
|
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import {
|
import {
|
||||||
fa1,
|
|
||||||
faBriefcase,
|
|
||||||
faCalendarDays,
|
|
||||||
faCheckDouble,
|
|
||||||
faCircleDot,
|
|
||||||
faClock,
|
|
||||||
faCreditCard,
|
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faEye,
|
faEye,
|
||||||
faFile,
|
faFile,
|
||||||
faFileCirclePlus,
|
faFileCirclePlus,
|
||||||
faGripLines,
|
faGripLines,
|
||||||
faHeading,
|
|
||||||
faIdCard,
|
|
||||||
faImage,
|
|
||||||
faPaperclip,
|
|
||||||
faPen,
|
faPen,
|
||||||
faPhone,
|
|
||||||
faPlus,
|
faPlus,
|
||||||
faSignature,
|
|
||||||
faSquareCaretDown,
|
|
||||||
faSquareCheck,
|
|
||||||
faStamp,
|
|
||||||
faT,
|
|
||||||
faTableCellsLarge,
|
|
||||||
faToolbox,
|
faToolbox,
|
||||||
faTrash,
|
faTrash,
|
||||||
faUpload
|
faUpload
|
||||||
@ -125,116 +108,7 @@ export const CreatePage = () => {
|
|||||||
const [drawnFiles, setDrawnFiles] = useState<SigitFile[]>([])
|
const [drawnFiles, setDrawnFiles] = useState<SigitFile[]>([])
|
||||||
|
|
||||||
const [selectedTool, setSelectedTool] = useState<DrawTool>()
|
const [selectedTool, setSelectedTool] = useState<DrawTool>()
|
||||||
const [toolbox] = useState<DrawTool[]>([
|
const [toolbox] = useState<DrawTool[]>(DEFAULT_TOOLBOX)
|
||||||
{
|
|
||||||
identifier: MarkType.TEXT,
|
|
||||||
icon: faT,
|
|
||||||
label: 'Text',
|
|
||||||
active: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.SIGNATURE,
|
|
||||||
icon: faSignature,
|
|
||||||
label: 'Signature',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.JOBTITLE,
|
|
||||||
icon: faBriefcase,
|
|
||||||
label: 'Job Title',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.FULLNAME,
|
|
||||||
icon: faIdCard,
|
|
||||||
label: 'Full Name',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.INITIALS,
|
|
||||||
icon: faHeading,
|
|
||||||
label: 'Initials',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.DATETIME,
|
|
||||||
icon: faClock,
|
|
||||||
label: 'Date Time',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.DATE,
|
|
||||||
icon: faCalendarDays,
|
|
||||||
label: 'Date',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.NUMBER,
|
|
||||||
icon: fa1,
|
|
||||||
label: 'Number',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.IMAGES,
|
|
||||||
icon: faImage,
|
|
||||||
label: 'Images',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.CHECKBOX,
|
|
||||||
icon: faSquareCheck,
|
|
||||||
label: 'Checkbox',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.MULTIPLE,
|
|
||||||
icon: faCheckDouble,
|
|
||||||
label: 'Multiple',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.FILE,
|
|
||||||
icon: faPaperclip,
|
|
||||||
label: 'File',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.RADIO,
|
|
||||||
icon: faCircleDot,
|
|
||||||
label: 'Radio',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.SELECT,
|
|
||||||
icon: faSquareCaretDown,
|
|
||||||
label: 'Select',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.CELLS,
|
|
||||||
icon: faTableCellsLarge,
|
|
||||||
label: 'Cells',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.STAMP,
|
|
||||||
icon: faStamp,
|
|
||||||
label: 'Stamp',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.PAYMENT,
|
|
||||||
icon: faCreditCard,
|
|
||||||
label: 'Payment',
|
|
||||||
active: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: MarkType.PHONE,
|
|
||||||
icon: faPhone,
|
|
||||||
label: 'Phone',
|
|
||||||
active: false
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the drawing tool
|
* Changes the drawing tool
|
||||||
|
@ -22,7 +22,6 @@ import { useLocation } from 'react-router-dom'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import {
|
import {
|
||||||
addMarks,
|
addMarks,
|
||||||
convertToPdfBlob,
|
|
||||||
FONT_SIZE,
|
FONT_SIZE,
|
||||||
FONT_TYPE,
|
FONT_TYPE,
|
||||||
groupMarksByFileNamePage,
|
groupMarksByFileNamePage,
|
||||||
@ -399,8 +398,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, marksByPage[fileName])
|
const blob = await addMarks(file, marksByPage[fileName])
|
||||||
const blob = await convertToPdfBlob(pages)
|
|
||||||
zip.file(`files/${fileName}`, blob)
|
zip.file(`files/${fileName}`, blob)
|
||||||
} else {
|
} else {
|
||||||
zip.file(`files/${fileName}`, file)
|
zip.file(`files/${fileName}`, file)
|
||||||
|
@ -61,6 +61,6 @@
|
|||||||
|
|
||||||
[data-dev='true'] {
|
[data-dev='true'] {
|
||||||
.mark {
|
.mark {
|
||||||
border: 1px dotted black;
|
outline: 1px dotted black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import { MOST_COMMON_MEDIA_TYPES } from './const.ts'
|
|||||||
import { extractMarksFromSignedMeta } from './mark.ts'
|
import { extractMarksFromSignedMeta } from './mark.ts'
|
||||||
import {
|
import {
|
||||||
addMarks,
|
addMarks,
|
||||||
convertToPdfBlob,
|
|
||||||
groupMarksByFileNamePage,
|
groupMarksByFileNamePage,
|
||||||
isPdf,
|
isPdf,
|
||||||
pdfToImages
|
pdfToImages
|
||||||
@ -22,8 +21,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(file, marksByFileNamePage[fileName])
|
const blob = await addMarks(file, marksByFileNamePage[fileName])
|
||||||
const blob = await convertToPdfBlob(pages)
|
|
||||||
zip.file(`files/${fileName}`, blob)
|
zip.file(`files/${fileName}`, blob)
|
||||||
} else {
|
} else {
|
||||||
// Handle other files
|
// Handle other files
|
||||||
|
@ -3,6 +3,27 @@ import { hexToNpub } from './nostr.ts'
|
|||||||
import { Meta, SignedEventContent } from '../types'
|
import { Meta, SignedEventContent } from '../types'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
import { EMPTY } from './const.ts'
|
import { EMPTY } from './const.ts'
|
||||||
|
import { MarkType } from '../types/drawing.ts'
|
||||||
|
import {
|
||||||
|
faT,
|
||||||
|
faSignature,
|
||||||
|
faBriefcase,
|
||||||
|
faIdCard,
|
||||||
|
faHeading,
|
||||||
|
faClock,
|
||||||
|
faCalendarDays,
|
||||||
|
fa1,
|
||||||
|
faImage,
|
||||||
|
faSquareCheck,
|
||||||
|
faCheckDouble,
|
||||||
|
faPaperclip,
|
||||||
|
faCircleDot,
|
||||||
|
faSquareCaretDown,
|
||||||
|
faTableCellsLarge,
|
||||||
|
faStamp,
|
||||||
|
faCreditCard,
|
||||||
|
faPhone
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes in an array of Marks already filtered by User.
|
* Takes in an array of Marks already filtered by User.
|
||||||
@ -131,6 +152,121 @@ const findOtherUserMarks = (marks: Mark[], pubkey: string): Mark[] => {
|
|||||||
return marks.filter((mark) => mark.npub !== hexToNpub(pubkey))
|
return marks.filter((mark) => mark.npub !== hexToNpub(pubkey))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_TOOLBOX = [
|
||||||
|
{
|
||||||
|
identifier: MarkType.TEXT,
|
||||||
|
icon: faT,
|
||||||
|
label: 'Text',
|
||||||
|
active: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.SIGNATURE,
|
||||||
|
icon: faSignature,
|
||||||
|
label: 'Signature',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.JOBTITLE,
|
||||||
|
icon: faBriefcase,
|
||||||
|
label: 'Job Title',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.FULLNAME,
|
||||||
|
icon: faIdCard,
|
||||||
|
label: 'Full Name',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.INITIALS,
|
||||||
|
icon: faHeading,
|
||||||
|
label: 'Initials',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.DATETIME,
|
||||||
|
icon: faClock,
|
||||||
|
label: 'Date Time',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.DATE,
|
||||||
|
icon: faCalendarDays,
|
||||||
|
label: 'Date',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.NUMBER,
|
||||||
|
icon: fa1,
|
||||||
|
label: 'Number',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.IMAGES,
|
||||||
|
icon: faImage,
|
||||||
|
label: 'Images',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.CHECKBOX,
|
||||||
|
icon: faSquareCheck,
|
||||||
|
label: 'Checkbox',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.MULTIPLE,
|
||||||
|
icon: faCheckDouble,
|
||||||
|
label: 'Multiple',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.FILE,
|
||||||
|
icon: faPaperclip,
|
||||||
|
label: 'File',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.RADIO,
|
||||||
|
icon: faCircleDot,
|
||||||
|
label: 'Radio',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.SELECT,
|
||||||
|
icon: faSquareCaretDown,
|
||||||
|
label: 'Select',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.CELLS,
|
||||||
|
icon: faTableCellsLarge,
|
||||||
|
label: 'Cells',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.STAMP,
|
||||||
|
icon: faStamp,
|
||||||
|
label: 'Stamp',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.PAYMENT,
|
||||||
|
icon: faCreditCard,
|
||||||
|
label: 'Payment',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: MarkType.PHONE,
|
||||||
|
icon: faPhone,
|
||||||
|
label: 'Phone',
|
||||||
|
active: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const getToolboxLabelByMarkType = (markType: MarkType) => {
|
||||||
|
return DEFAULT_TOOLBOX.find((t) => t.identifier === markType)?.label
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getCurrentUserMarks,
|
getCurrentUserMarks,
|
||||||
filterMarksByPubkey,
|
filterMarksByPubkey,
|
||||||
|
125
src/utils/pdf.ts
125
src/utils/pdf.ts
@ -1,7 +1,6 @@
|
|||||||
import { PdfPage } from '../types/drawing.ts'
|
import { PdfPage } from '../types/drawing.ts'
|
||||||
import { PDFDocument } from 'pdf-lib'
|
import { PDFDocument, PDFFont, PDFPage, rgb } from 'pdf-lib'
|
||||||
import { Mark } from '../types/mark.ts'
|
import { Mark } from '../types/mark.ts'
|
||||||
|
|
||||||
import * as PDFJS from 'pdfjs-dist'
|
import * as PDFJS from 'pdfjs-dist'
|
||||||
import PDFJSWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker'
|
import PDFJSWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker'
|
||||||
if (!PDFJS.GlobalWorkerOptions.workerPort) {
|
if (!PDFJS.GlobalWorkerOptions.workerPort) {
|
||||||
@ -10,18 +9,23 @@ if (!PDFJS.GlobalWorkerOptions.workerPort) {
|
|||||||
PDFJS.GlobalWorkerOptions.workerPort = worker
|
PDFJS.GlobalWorkerOptions.workerPort = worker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import fontkit from '@pdf-lib/fontkit'
|
||||||
|
import defaultFont from '../assets/fonts/roboto-regular.ttf'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
* because it is dynamically rendered, and the final size.
|
* because it is dynamically rendered, and the final size.
|
||||||
* This should be fixed going forward.
|
|
||||||
* Switching to PDF-Lib will most likely make this problem redundant.
|
|
||||||
*/
|
*/
|
||||||
export const FONT_SIZE: number = 16
|
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 = 'Roboto'
|
||||||
|
/**
|
||||||
|
* Current line height used when generating a PDF.
|
||||||
|
*/
|
||||||
|
export const FONT_LINE_HEIGHT: number = 1
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A utility that transforms a drawing coordinate number into a CSS-compatible pixel string
|
* A utility that transforms a drawing coordinate number into a CSS-compatible pixel string
|
||||||
@ -90,14 +94,11 @@ 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 = 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 originalViewport = page.getViewport({ scale: 1 })
|
const viewport = 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
|
||||||
@ -105,7 +106,7 @@ export const pdfToImages = async (
|
|||||||
await page.render({ canvasContext: context!, viewport: viewport }).promise
|
await page.render({ canvasContext: context!, viewport: viewport }).promise
|
||||||
pages.push({
|
pages.push({
|
||||||
image: canvas.toDataURL(),
|
image: canvas.toDataURL(),
|
||||||
width: originalViewport.width,
|
width: viewport.width,
|
||||||
drawnFields: []
|
drawnFields: []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -115,56 +116,28 @@ export const pdfToImages = async (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes in individual pdf file and an object with Marks grouped by Page number
|
* Takes in individual pdf file and an object with Marks grouped by Page number
|
||||||
* Returns an array of encoded images where each image is a representation
|
* Returns a PDF blob with embedded, completed and signed marks from all users as text
|
||||||
* of a PDF page with completed and signed marks from all users
|
|
||||||
*/
|
*/
|
||||||
export const addMarks = async (
|
export const addMarks = async (
|
||||||
file: File,
|
file: File,
|
||||||
marksPerPage: { [key: string]: Mark[] }
|
marksPerPage: { [key: string]: Mark[] }
|
||||||
) => {
|
) => {
|
||||||
const p = await readPdf(file)
|
const p = await readPdf(file)
|
||||||
const pdf = await PDFJS.getDocument(p).promise
|
const pdf = await PDFDocument.load(p)
|
||||||
const canvas = document.createElement('canvas')
|
const robotoFont = await embedFont(pdf)
|
||||||
|
const pages = pdf.getPages()
|
||||||
const images: string[] = []
|
|
||||||
|
|
||||||
for (let i = 0; i < pdf.numPages; i++) {
|
|
||||||
const page = await pdf.getPage(i + 1)
|
|
||||||
const viewport = page.getViewport({ scale: 1 })
|
|
||||||
const context = canvas.getContext('2d')
|
|
||||||
canvas.height = viewport.height
|
|
||||||
canvas.width = viewport.width
|
|
||||||
if (context) {
|
|
||||||
await page.render({ canvasContext: context, viewport: viewport }).promise
|
|
||||||
|
|
||||||
|
for (let i = 0; i < pages.length; i++) {
|
||||||
if (marksPerPage && Object.hasOwn(marksPerPage, i)) {
|
if (marksPerPage && Object.hasOwn(marksPerPage, i)) {
|
||||||
marksPerPage[i]?.forEach((mark) => draw(mark, context))
|
marksPerPage[i]?.forEach((mark) =>
|
||||||
}
|
drawMarkText(mark, pages[i], robotoFont)
|
||||||
|
)
|
||||||
images.push(canvas.toDataURL())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.remove()
|
const blob = await pdf.save()
|
||||||
|
|
||||||
return images
|
return blob
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility to scale mark in line with the PDF-to-PNG scale
|
|
||||||
*/
|
|
||||||
export const scaleMark = (mark: Mark, scale: number): Mark => {
|
|
||||||
const { location } = mark
|
|
||||||
return {
|
|
||||||
...mark,
|
|
||||||
location: {
|
|
||||||
...location,
|
|
||||||
width: location.width * scale,
|
|
||||||
height: location.height * scale,
|
|
||||||
left: location.left * scale,
|
|
||||||
top: location.top * scale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -177,6 +150,7 @@ export const hasValue = (mark: Mark): boolean => !!mark.value
|
|||||||
* Draws a Mark on a Canvas representation of a PDF Page
|
* Draws a Mark on a Canvas representation of a PDF Page
|
||||||
* @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
|
||||||
|
* @deprecated use drawMarkText
|
||||||
*/
|
*/
|
||||||
export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
|
export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
|
||||||
const { location } = mark
|
const { location } = mark
|
||||||
@ -191,29 +165,39 @@ export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes an array of encoded PDF pages and returns a blob that is a complete PDF file
|
* Draws a Mark on a PDF Page
|
||||||
* @param markedPdfPages
|
* @param mark to be drawn
|
||||||
|
* @param page PDF Page
|
||||||
|
* @param font embedded font
|
||||||
*/
|
*/
|
||||||
export const convertToPdfBlob = async (
|
export const drawMarkText = (mark: Mark, page: PDFPage, font: PDFFont) => {
|
||||||
markedPdfPages: string[]
|
const { location } = mark
|
||||||
): Promise<Blob> => {
|
const { height } = page.getSize()
|
||||||
const pdfDoc = await PDFDocument.create()
|
|
||||||
|
|
||||||
for (const page of markedPdfPages) {
|
// Convert the mark location origin (top, left) to PDF origin (bottom, left)
|
||||||
const pngImage = await pdfDoc.embedPng(page)
|
const x = location.left
|
||||||
const p = pdfDoc.addPage([pngImage.width, pngImage.height])
|
const y = height - location.top
|
||||||
p.drawImage(pngImage, {
|
|
||||||
x: 0,
|
// Adjust y-coordinate for the text, drawText's y is the baseline for the font
|
||||||
y: 0,
|
// We start from the y (top location border) and we need to bump it down
|
||||||
width: pngImage.width,
|
// We move font baseline by the difference between rendered height and actual height (gap)
|
||||||
height: pngImage.height
|
// And finally move down by the height without descender to get the new baseline
|
||||||
|
const adjustedY =
|
||||||
|
y -
|
||||||
|
(font.heightAtSize(FONT_SIZE) - FONT_SIZE) -
|
||||||
|
font.heightAtSize(FONT_SIZE, { descender: false })
|
||||||
|
|
||||||
|
page.drawText(`${mark.value}`, {
|
||||||
|
x,
|
||||||
|
y: adjustedY,
|
||||||
|
size: FONT_SIZE,
|
||||||
|
font: font,
|
||||||
|
color: rgb(0, 0, 0),
|
||||||
|
maxWidth: location.width,
|
||||||
|
lineHeight: FONT_SIZE * FONT_LINE_HEIGHT
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const pdfBytes = await pdfDoc.save()
|
|
||||||
return new Blob([pdfBytes], { type: 'application/pdf' })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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
|
||||||
@ -249,3 +233,12 @@ export const byPage = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function embedFont(pdf: PDFDocument) {
|
||||||
|
const fontBytes = await fetch(defaultFont).then((res) => res.arrayBuffer())
|
||||||
|
|
||||||
|
pdf.registerFontkit(fontkit)
|
||||||
|
|
||||||
|
const embeddedFont = await pdf.embedFont(fontBytes)
|
||||||
|
return embeddedFont
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user