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",
|
||||
"@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",
|
||||
|
@ -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",
|
||||
|
14
src/App.scss
14
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;
|
||||
}
|
||||
|
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 { 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
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`file-mark ${styles.placeholder}`}
|
||||
style={{
|
||||
fontFamily: FONT_TYPE,
|
||||
fontSize: inPx(from(page.width, FONT_SIZE))
|
||||
}}
|
||||
>
|
||||
{getToolboxLabelByMarkType(drawnField.type) ||
|
||||
'placeholder'}
|
||||
</div>
|
||||
<span
|
||||
onPointerDown={(event) =>
|
||||
handleResizePointerDown(event, drawnFieldIndex)
|
||||
|
@ -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 {
|
||||
|
@ -36,7 +36,7 @@ const PdfMarkItem = forwardRef<HTMLDivElement, PdfMarkItemProps>(
|
||||
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)),
|
||||
|
@ -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<SigitFile[]>([])
|
||||
|
||||
const [selectedTool, setSelectedTool] = useState<DrawTool>()
|
||||
const [toolbox] = useState<DrawTool[]>([
|
||||
{
|
||||
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<DrawTool[]>(DEFAULT_TOOLBOX)
|
||||
|
||||
/**
|
||||
* Changes the drawing tool
|
||||
|
@ -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)
|
||||
|
@ -61,6 +61,6 @@
|
||||
|
||||
[data-dev='true'] {
|
||||
.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 {
|
||||
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
|
||||
|
@ -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,
|
||||
|
127
src/utils/pdf.ts
127
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
|
||||
@ -90,14 +94,11 @@ export const pdfToImages = async (
|
||||
const pages: PdfPage[] = []
|
||||
const pdf = await PDFJS.getDocument(data).promise
|
||||
const canvas = document.createElement('canvas')
|
||||
const width = getInnerContentWidth()
|
||||
|
||||
for (let i = 0; i < pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i + 1)
|
||||
|
||||
const originalViewport = page.getViewport({ scale: 1 })
|
||||
const scale = width / originalViewport.width
|
||||
const viewport = page.getViewport({ scale: scale })
|
||||
const viewport = page.getViewport({ scale: 1 })
|
||||
const context = canvas.getContext('2d')
|
||||
canvas.height = viewport.height
|
||||
canvas.width = viewport.width
|
||||
@ -105,7 +106,7 @@ export const pdfToImages = async (
|
||||
await page.render({ canvasContext: context!, viewport: viewport }).promise
|
||||
pages.push({
|
||||
image: canvas.toDataURL(),
|
||||
width: originalViewport.width,
|
||||
width: viewport.width,
|
||||
drawnFields: []
|
||||
})
|
||||
}
|
||||
@ -115,56 +116,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 +150,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 +165,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<Blob> => {
|
||||
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 +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