fix(pdf): dynamic mark scaling #165
10
src/App.scss
10
src/App.scss
@ -121,6 +121,16 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
[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 */
|
||||||
|
@ -23,10 +23,6 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
font-family: Arial;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: normal;
|
|
||||||
|
|
||||||
&.nonEditable {
|
&.nonEditable {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
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 { FONT_TYPE, inPx } from '../../utils/pdf.ts'
|
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
|
||||||
|
|
||||||
interface PdfMarkItemProps {
|
interface PdfMarkItemProps {
|
||||||
userMark: CurrentUserMark
|
userMark: CurrentUserMark
|
||||||
@ -26,13 +26,14 @@ const PdfMarkItem = ({
|
|||||||
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(location.left),
|
||||||
top: inPx(location.top),
|
top: inPx(location.top),
|
||||||
width: inPx(location.width),
|
width: inPx(location.width),
|
||||||
height: inPx(location.height),
|
height: inPx(location.height),
|
||||||
fontFamily: FONT_TYPE
|
fontFamily: FONT_TYPE,
|
||||||
|
fontSize: inPx(FONT_SIZE)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{getMarkValue()}
|
{getMarkValue()}
|
||||||
|
@ -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'
|
||||||
@ -105,13 +107,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(m.location.left),
|
||||||
top: inPx(m.location.top),
|
top: inPx(m.location.top),
|
||||||
width: inPx(m.location.width),
|
width: inPx(m.location.width),
|
||||||
height: inPx(m.location.height)
|
height: inPx(m.location.height),
|
||||||
|
fontFamily: FONT_TYPE,
|
||||||
|
fontSize: inPx(FONT_SIZE)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{m.value}
|
{m.value}
|
||||||
@ -427,7 +431,7 @@ export const VerifyPage = () => {
|
|||||||
for (const [fileName, file] of Object.entries(files)) {
|
for (const [fileName, file] of Object.entries(files)) {
|
||||||
if (file.isPdf) {
|
if (file.isPdf) {
|
||||||
// Draw marks into PDF file and generate a brand new blob
|
// Draw marks into PDF file and generate a brand new blob
|
||||||
const pages = await addMarks(file, marksByPage[fileName])
|
const pages = await addMarks(file, file.pages!, marksByPage[fileName])
|
||||||
const blob = await convertToPdfBlob(pages)
|
const blob = await convertToPdfBlob(pages)
|
||||||
zip.file(`files/${fileName}`, blob)
|
zip.file(`files/${fileName}`, blob)
|
||||||
} else {
|
} else {
|
||||||
|
@ -61,6 +61,6 @@
|
|||||||
|
|
||||||
[data-dev='true'] {
|
[data-dev='true'] {
|
||||||
.mark {
|
.mark {
|
||||||
border: 1px dotted black;
|
outline: 1px dotted black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ export interface MouseState {
|
|||||||
|
|
||||||
export interface PdfPage {
|
export interface PdfPage {
|
||||||
image: string
|
image: string
|
||||||
|
scale: number
|
||||||
drawnFields: DrawnField[]
|
drawnFields: DrawnField[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,11 @@ 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 pages = await addMarks(
|
||||||
|
file,
|
||||||
|
file.pages!,
|
||||||
|
marksByFileNamePage[fileName]
|
||||||
|
)
|
||||||
const blob = await convertToPdfBlob(pages)
|
const blob = await convertToPdfBlob(pages)
|
||||||
zip.file(`files/${fileName}`, blob)
|
zip.file(`files/${fileName}`, blob)
|
||||||
} else {
|
} else {
|
||||||
|
@ -8,11 +8,16 @@ PDFJS.GlobalWorkerOptions.workerSrc = new URL(
|
|||||||
import.meta.url
|
import.meta.url
|
||||||
).toString()
|
).toString()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default width of the rendered element on the website
|
||||||
|
* @constant {number}
|
||||||
|
*/
|
||||||
|
export const DEFAULT_VIEWPORT_WIDTH = 550
|
||||||
/**
|
/**
|
||||||
* Scale between the PDF page's natural size and rendered size
|
* Scale between the PDF page's natural size and rendered size
|
||||||
* @constant {number}
|
* @constant {number}
|
||||||
*/
|
*/
|
||||||
export const SCALE: number = 3
|
export const DEFAULT_SCALE: number = 1
|
||||||
/**
|
/**
|
||||||
* 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,7 +25,7 @@ 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.
|
||||||
*/
|
*/
|
||||||
@ -72,28 +77,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')
|
||||||
|
|
||||||
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 = originalViewport.width / DEFAULT_VIEWPORT_WIDTH
|
||||||
|
|
||||||
|
const viewport = page.getViewport({ scale })
|
||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
canvas.height = viewport.height
|
canvas.height = viewport.height
|
||||||
canvas.width = viewport.width
|
canvas.width = viewport.width
|
||||||
|
|
||||||
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(),
|
||||||
|
scale,
|
||||||
|
drawnFields: []
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(
|
return pages
|
||||||
images.map((image) => {
|
|
||||||
return {
|
|
||||||
image,
|
|
||||||
drawnFields: []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -103,6 +110,7 @@ export const pdfToImages = async (
|
|||||||
*/
|
*/
|
||||||
export const addMarks = async (
|
export const addMarks = async (
|
||||||
file: File,
|
file: File,
|
||||||
|
pages: PdfPage[],
|
||||||
marksPerPage: { [key: string]: Mark[] }
|
marksPerPage: { [key: string]: Mark[] }
|
||||||
) => {
|
) => {
|
||||||
const p = await readPdf(file)
|
const p = await readPdf(file)
|
||||||
@ -113,34 +121,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, pages[i].scale))
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,15 +169,21 @@ export const hasValue = (mark: Mark): boolean => !!mark.value
|
|||||||
* @param mark to be drawn
|
* @param mark to be drawn
|
||||||
* @param ctx a Canvas representation of a specific PDF Page
|
* @param ctx a Canvas representation of a specific PDF Page
|
||||||
*/
|
*/
|
||||||
export const draw = (mark: Mark, ctx: CanvasRenderingContext2D) => {
|
export const draw = (
|
||||||
const { location } = mark
|
mark: Mark,
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
scale: number
|
||||||
|
) => {
|
||||||
|
const { location } = scaleMark(mark, scale)
|
||||||
|
|
||||||
ctx!.font = FONT_SIZE + 'px ' + FONT_TYPE
|
ctx.font = scale * FONT_SIZE + 'px ' + FONT_TYPE
|
||||||
ctx!.fillStyle = 'black'
|
ctx.fillStyle = 'black'
|
||||||
const textMetrics = ctx!.measureText(mark.value!)
|
const textMetrics = ctx.measureText(mark.value!)
|
||||||
|
const textHeight =
|
||||||
|
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 +213,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