fix(pdf): dynamic mark scaling #165

Merged
enes merged 9 commits from 146-pdf-scaling into staging 2024-08-28 11:23:34 +00:00
8 changed files with 78 additions and 45 deletions
Showing only changes of commit ac3186a02e - Show all commits

View File

@ -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 */

View File

@ -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;

View File

@ -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()}

View File

@ -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 {

View File

@ -61,6 +61,6 @@
[data-dev='true'] { [data-dev='true'] {
.mark { .mark {
border: 1px dotted black; outline: 1px dotted black;
} }
} }

View File

@ -10,6 +10,7 @@ export interface MouseState {
export interface PdfPage { export interface PdfPage {
image: string image: string
scale: number
drawnFields: DrawnField[] drawnFields: DrawnField[]
} }

View File

@ -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 {

View File

@ -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, {})
} }