diff --git a/package-lock.json b/package-lock.json
index 2bcd952..71e0923 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,6 +21,7 @@
"@mui/material": "5.15.11",
"@noble/hashes": "^1.4.0",
"@nostr-dev-kit/ndk": "2.5.0",
+ "@pdf-lib/fontkit": "^1.1.1",
"@reduxjs/toolkit": "2.2.1",
"axios": "^1.7.4",
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
diff --git a/package.json b/package.json
index aaeba6e..a833103 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"@mui/material": "5.15.11",
"@noble/hashes": "^1.4.0",
"@nostr-dev-kit/ndk": "2.5.0",
+ "@pdf-lib/fontkit": "^1.1.1",
"@reduxjs/toolkit": "2.2.1",
"axios": "^1.7.4",
"crypto-hash": "3.0.0",
diff --git a/src/App.scss b/src/App.scss
index 4d95be6..c4fa323 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -135,12 +135,18 @@ li {
// Consistent styling for every file mark
// Reverts some of the design defaults for font
.file-mark {
- font-family: Arial;
- font-size: 16px;
+ font-family: 'Roboto';
+ font-style: normal;
font-weight: normal;
- color: black;
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;
}
diff --git a/src/assets/fonts/roboto-regular.ttf b/src/assets/fonts/roboto-regular.ttf
new file mode 100644
index 0000000..2d116d9
Binary files /dev/null and b/src/assets/fonts/roboto-regular.ttf differ
diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx
index 7efae8b..7c1227d 100644
--- a/src/components/DrawPDFFields/index.tsx
+++ b/src/components/DrawPDFFields/index.tsx
@@ -14,9 +14,10 @@ import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing'
import { truncate } from 'lodash'
import { settleAllFullfilfedPromises, hexToNpub, npubToHex } from '../../utils'
import { getSigitFile, SigitFile } from '../../utils/file'
+import { getToolboxLabelByMarkType } from '../../utils/mark'
import { FileDivider } from '../FileDivider'
import { ExtensionFileBox } from '../ExtensionFileBox'
-import { inPx } from '../../utils/pdf'
+import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf'
import { useScale } from '../../hooks/useScale'
import { AvatarIconButton } from '../UserAvatarIconButton'
import { LoadingSpinner } from '../LoadingSpinner'
@@ -390,7 +391,7 @@ export const DrawPDFFields = (props: Props) => {
backgroundColor: drawnField.counterpart
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}4b`
: undefined,
- borderColor: drawnField.counterpart
+ outlineColor: drawnField.counterpart
? `#${npubToHex(drawnField.counterpart)?.substring(0, 6)}`
: undefined,
left: inPx(from(page.width, drawnField.left)),
@@ -406,6 +407,16 @@ export const DrawPDFFields = (props: Props) => {
: undefined
}}
>
+
+ {getToolboxLabelByMarkType(drawnField.type) ||
+ 'placeholder'}
+
handleResizePointerDown(event, drawnFieldIndex)
diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss
index 62fa688..13afb9f 100644
--- a/src/components/DrawPDFFields/style.module.scss
+++ b/src/components/DrawPDFFields/style.module.scss
@@ -13,9 +13,15 @@
}
}
+.placeholder {
+ position: absolute;
+ opacity: 0.5;
+ inset: 0;
+}
+
.drawingRectangle {
position: absolute;
- border: 1px solid #01aaad;
+ outline: 1px solid #01aaad;
z-index: 50;
background-color: #01aaad4b;
cursor: pointer;
@@ -29,7 +35,7 @@
}
&.edited {
- border: 1px dotted #01aaad;
+ outline: 1px dotted #01aaad;
}
.resizeHandle {
diff --git a/src/components/PDFView/PdfMarkItem.tsx b/src/components/PDFView/PdfMarkItem.tsx
index db57800..cb06455 100644
--- a/src/components/PDFView/PdfMarkItem.tsx
+++ b/src/components/PDFView/PdfMarkItem.tsx
@@ -36,7 +36,7 @@ const PdfMarkItem = forwardRef(
backgroundColor: selectedMark?.mark.npub
? `#${npubToHex(selectedMark?.mark.npub)?.substring(0, 6)}4b`
: undefined,
- borderColor: selectedMark?.mark.npub
+ outlineColor: selectedMark?.mark.npub
? `#${npubToHex(selectedMark?.mark.npub)?.substring(0, 6)}`
: undefined,
left: inPx(from(pageWidth, location.left)),
diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx
index f4dc550..dd17450 100644
--- a/src/pages/create/index.tsx
+++ b/src/pages/create/index.tsx
@@ -38,41 +38,24 @@ import {
sendNotification,
signEventForMetaFile,
updateUsersAppData,
- uploadToFileStorage
+ uploadToFileStorage,
+ DEFAULT_TOOLBOX
} from '../../utils'
import { Container } from '../../components/Container'
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 { Mark } from '../../types/mark.ts'
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
- fa1,
- faBriefcase,
- faCalendarDays,
- faCheckDouble,
- faCircleDot,
- faClock,
- faCreditCard,
faEllipsis,
faEye,
faFile,
faFileCirclePlus,
faGripLines,
- faHeading,
- faIdCard,
- faImage,
- faPaperclip,
faPen,
- faPhone,
faPlus,
- faSignature,
- faSquareCaretDown,
- faSquareCheck,
- faStamp,
- faT,
- faTableCellsLarge,
faToolbox,
faTrash,
faUpload
@@ -125,116 +108,7 @@ export const CreatePage = () => {
const [drawnFiles, setDrawnFiles] = useState([])
const [selectedTool, setSelectedTool] = useState()
- const [toolbox] = useState([
- {
- 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
- }
- ])
+ const [toolbox] = useState(DEFAULT_TOOLBOX)
/**
* Changes the drawing tool
diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx
index af0b4c7..de66991 100644
--- a/src/pages/verify/index.tsx
+++ b/src/pages/verify/index.tsx
@@ -22,7 +22,6 @@ import { useLocation } from 'react-router-dom'
import axios from 'axios'
import {
addMarks,
- convertToPdfBlob,
FONT_SIZE,
FONT_TYPE,
groupMarksByFileNamePage,
@@ -399,8 +398,7 @@ export const VerifyPage = () => {
for (const [fileName, file] of Object.entries(files)) {
if (file.isPdf) {
// Draw marks into PDF file and generate a brand new blob
- const pages = await addMarks(file, marksByPage[fileName])
- const blob = await convertToPdfBlob(pages)
+ const blob = await addMarks(file, marksByPage[fileName])
zip.file(`files/${fileName}`, blob)
} else {
zip.file(`files/${fileName}`, file)
diff --git a/src/pages/verify/style.module.scss b/src/pages/verify/style.module.scss
index af93107..b63ba60 100644
--- a/src/pages/verify/style.module.scss
+++ b/src/pages/verify/style.module.scss
@@ -61,6 +61,6 @@
[data-dev='true'] {
.mark {
- border: 1px dotted black;
+ outline: 1px dotted black;
}
}
diff --git a/src/utils/file.ts b/src/utils/file.ts
index 63d40e5..6156858 100644
--- a/src/utils/file.ts
+++ b/src/utils/file.ts
@@ -4,7 +4,6 @@ import { MOST_COMMON_MEDIA_TYPES } from './const.ts'
import { extractMarksFromSignedMeta } from './mark.ts'
import {
addMarks,
- convertToPdfBlob,
groupMarksByFileNamePage,
isPdf,
pdfToImages
@@ -22,8 +21,7 @@ export const getZipWithFiles = async (
for (const [fileName, file] of Object.entries(files)) {
if (file.isPdf) {
// Handle PDF Files
- const pages = await addMarks(file, marksByFileNamePage[fileName])
- const blob = await convertToPdfBlob(pages)
+ const blob = await addMarks(file, marksByFileNamePage[fileName])
zip.file(`files/${fileName}`, blob)
} else {
// Handle other files
diff --git a/src/utils/mark.ts b/src/utils/mark.ts
index 44540c4..ac80623 100644
--- a/src/utils/mark.ts
+++ b/src/utils/mark.ts
@@ -3,6 +3,27 @@ import { hexToNpub } from './nostr.ts'
import { Meta, SignedEventContent } from '../types'
import { Event } from 'nostr-tools'
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.
@@ -131,6 +152,121 @@ const findOtherUserMarks = (marks: Mark[], pubkey: string): Mark[] => {
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 {
getCurrentUserMarks,
filterMarksByPubkey,
diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts
index 763d582..c3e381d 100644
--- a/src/utils/pdf.ts
+++ b/src/utils/pdf.ts
@@ -1,7 +1,6 @@
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 * as PDFJS from 'pdfjs-dist'
import PDFJSWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker'
if (!PDFJS.GlobalWorkerOptions.workerPort) {
@@ -10,18 +9,23 @@ if (!PDFJS.GlobalWorkerOptions.workerPort) {
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
* correlate font size used at the time of filling in / drawing on the PDF
* 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
/**
* 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
@@ -115,56 +119,28 @@ export const pdfToImages = async (
/**
* 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
- * of a PDF page with completed and signed marks from all users
+ * Returns a PDF blob with embedded, completed and signed marks from all users as text
*/
export const addMarks = async (
file: File,
marksPerPage: { [key: string]: Mark[] }
) => {
const p = await readPdf(file)
- const pdf = await PDFJS.getDocument(p).promise
- const canvas = document.createElement('canvas')
+ const pdf = await PDFDocument.load(p)
+ 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
-
- if (marksPerPage && Object.hasOwn(marksPerPage, i)) {
- marksPerPage[i]?.forEach((mark) => draw(mark, context))
- }
-
- images.push(canvas.toDataURL())
+ for (let i = 0; i < pages.length; i++) {
+ if (marksPerPage && Object.hasOwn(marksPerPage, i)) {
+ marksPerPage[i]?.forEach((mark) =>
+ drawMarkText(mark, pages[i], robotoFont)
+ )
}
}
- canvas.remove()
+ const blob = await pdf.save()
- return images
-}
-
-/**
- * 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
- }
- }
+ return blob
}
/**
@@ -177,6 +153,7 @@ export const hasValue = (mark: Mark): boolean => !!mark.value
* Draws a Mark on a Canvas representation of a PDF Page
* @param mark to be drawn
* @param ctx a Canvas representation of a specific PDF Page
+ * @deprecated use drawMarkText
*/
export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
const { location } = mark
@@ -191,27 +168,37 @@ export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
}
/**
- * Takes an array of encoded PDF pages and returns a blob that is a complete PDF file
- * @param markedPdfPages
+ * Draws a Mark on a PDF Page
+ * @param mark to be drawn
+ * @param page PDF Page
+ * @param font embedded font
*/
-export const convertToPdfBlob = async (
- markedPdfPages: string[]
-): Promise => {
- const pdfDoc = await PDFDocument.create()
+export const drawMarkText = (mark: Mark, page: PDFPage, font: PDFFont) => {
+ const { location } = mark
+ const { height } = page.getSize()
- for (const page of markedPdfPages) {
- const pngImage = await pdfDoc.embedPng(page)
- const p = pdfDoc.addPage([pngImage.width, pngImage.height])
- p.drawImage(pngImage, {
- x: 0,
- y: 0,
- width: pngImage.width,
- height: pngImage.height
- })
- }
+ // Convert the mark location origin (top, left) to PDF origin (bottom, left)
+ const x = location.left
+ const y = height - location.top
- const pdfBytes = await pdfDoc.save()
- return new Blob([pdfBytes], { type: 'application/pdf' })
+ // Adjust y-coordinate for the text, drawText's y is the baseline for the font
+ // We start from the y (top location border) and we need to bump it down
+ // We move font baseline by the difference between rendered height and actual height (gap)
+ // 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
+ })
}
/**
@@ -249,3 +236,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
+}