mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
feat(pdf): replace PdfLib with Pdfium for form handling and general rendering tasks (#5899)
# Description of Changes Improves PDF rendering in the viewer by adding digital signature field support, cleaning up overlay rendering, and migrating the contrast tool off pdf-lib to PDFium WASM. ### Signature Field Overlay - Added `SignatureFieldOverlay` component that renders digital signature form fields - Renders appearance streams when present; shows a fallback badge for unsigned fields - Uses PDFium WASM for bitmap extraction ### Overlay Rendering - Integrated `SignatureFieldOverlay` and `ButtonAppearanceOverlay` into `LocalEmbedPDF` - Overlays are now clipped to page boundaries - Clarified in `EmbedPdfViewer` that frontend overlays use PDFium WASM, backend overlays use PDFBox ### Contrast Tool Migration - Replaced pdf-lib with PDFium WASM in `useAdjustContrastOperation` - PDF page creation and image embedding now go through PDFium APIs directly - Updated bitmap handling and memory management accordingly ### Cleanup - Fixed import ordering in viewer components - Removed stale comments in the contrast operation hook <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Signed-off-by: Balázs Szücs <[email protected]> Co-authored-by: Reece Browne <[email protected]>
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolType, useToolOperation, CustomProcessorResult } from '@app/hooks/tools/shared/useToolOperation';
|
||||
import { AdjustContrastParameters, defaultParameters } from '@app/hooks/tools/adjustContrast/useAdjustContrastParameters';
|
||||
import { PDFDocument as PDFLibDocument } from '@cantoo/pdf-lib';
|
||||
import { applyAdjustmentsToCanvas } from '@app/components/tools/adjustContrast/utils';
|
||||
import { pdfWorkerManager } from '@app/services/pdfWorkerManager';
|
||||
import { createFileFromApiResponse } from '@app/utils/fileResponseUtils';
|
||||
import {
|
||||
getPdfiumModule,
|
||||
saveRawDocument,
|
||||
} from '@app/services/pdfiumService';
|
||||
import { copyRgbaToBgraHeap } from '@app/utils/pdfiumBitmapUtils';
|
||||
|
||||
async function renderPdfPageToCanvas(pdf: any, pageNumber: number, scale: number): Promise<HTMLCanvasElement> {
|
||||
const page = await pdf.getPage(pageNumber);
|
||||
@@ -18,37 +22,94 @@ async function renderPdfPageToCanvas(pdf: any, pageNumber: number, scale: number
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// adjustment logic moved to shared util
|
||||
|
||||
// Render, adjust, and assemble all pages of a single PDF into a new PDF
|
||||
// Render, adjust, and assemble all pages of a single PDF into a new PDF using PDFium
|
||||
async function buildAdjustedPdfForFile(file: File, params: AdjustContrastParameters): Promise<File> {
|
||||
const m = await getPdfiumModule();
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdf = await pdfWorkerManager.createDocument(arrayBuffer, {});
|
||||
const pageCount = pdf.numPages;
|
||||
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
const docPtr = m.FPDF_CreateNewDocument();
|
||||
if (!docPtr) throw new Error('PDFium: failed to create document');
|
||||
|
||||
for (let p = 1; p <= pageCount; p++) {
|
||||
const srcCanvas = await renderPdfPageToCanvas(pdf, p, 2);
|
||||
const adjusted = applyAdjustmentsToCanvas(srcCanvas, params);
|
||||
const pngUrl = adjusted.toDataURL('image/png');
|
||||
const res = await fetch(pngUrl);
|
||||
const pngBytes = new Uint8Array(await res.arrayBuffer());
|
||||
const embedded = await newDoc.embedPng(pngBytes);
|
||||
const { width, height } = embedded.scale(1);
|
||||
const page = newDoc.addPage([width, height]);
|
||||
page.drawImage(embedded, { x: 0, y: 0, width, height });
|
||||
try {
|
||||
for (let p = 1; p <= pageCount; p++) {
|
||||
const srcCanvas = await renderPdfPageToCanvas(pdf, p, 2);
|
||||
const adjusted = applyAdjustmentsToCanvas(srcCanvas, params);
|
||||
const ctx = adjusted.getContext('2d');
|
||||
if (!ctx) {
|
||||
console.warn(`[adjustContrast] Skipping page ${p}: failed to get canvas context`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, adjusted.width, adjusted.height);
|
||||
const imgWidth = imageData.width;
|
||||
const imgHeight = imageData.height;
|
||||
|
||||
// Since we render at scale 2, the actual PDF page size is half
|
||||
const pdfPageWidth = imgWidth / 2;
|
||||
const pdfPageHeight = imgHeight / 2;
|
||||
|
||||
const pagePtr = m.FPDFPage_New(docPtr, p - 1, pdfPageWidth, pdfPageHeight);
|
||||
if (!pagePtr) {
|
||||
console.warn(`[adjustContrast] Skipping page ${p}: failed to create PDFium page`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let bitmapPtr = 0;
|
||||
try {
|
||||
bitmapPtr = m.FPDFBitmap_Create(imgWidth, imgHeight, 1);
|
||||
if (!bitmapPtr) {
|
||||
console.warn(`[adjustContrast] Skipping page ${p}: failed to create bitmap`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const bufferPtr = m.FPDFBitmap_GetBuffer(bitmapPtr);
|
||||
const stride = m.FPDFBitmap_GetStride(bitmapPtr);
|
||||
copyRgbaToBgraHeap(m, new Uint8Array(imageData.data.buffer), bufferPtr, imgWidth, imgHeight, stride);
|
||||
|
||||
const imageObjPtr = m.FPDFPageObj_NewImageObj(docPtr);
|
||||
if (imageObjPtr) {
|
||||
const setBitmapOk = m.FPDFImageObj_SetBitmap(pagePtr, 0, imageObjPtr, bitmapPtr);
|
||||
if (setBitmapOk) {
|
||||
const matrixPtr = m.pdfium.wasmExports.malloc(6 * 4);
|
||||
try {
|
||||
m.pdfium.setValue(matrixPtr, pdfPageWidth, 'float');
|
||||
m.pdfium.setValue(matrixPtr + 4, 0, 'float');
|
||||
m.pdfium.setValue(matrixPtr + 8, 0, 'float');
|
||||
m.pdfium.setValue(matrixPtr + 12, pdfPageHeight, 'float');
|
||||
m.pdfium.setValue(matrixPtr + 16, 0, 'float');
|
||||
m.pdfium.setValue(matrixPtr + 20, 0, 'float');
|
||||
|
||||
if (m.FPDFPageObj_SetMatrix(imageObjPtr, matrixPtr)) {
|
||||
m.FPDFPage_InsertObject(pagePtr, imageObjPtr);
|
||||
} else {
|
||||
m.FPDFPageObj_Destroy(imageObjPtr);
|
||||
}
|
||||
} finally {
|
||||
m.pdfium.wasmExports.free(matrixPtr);
|
||||
}
|
||||
} else {
|
||||
m.FPDFPageObj_Destroy(imageObjPtr);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (bitmapPtr) m.FPDFBitmap_Destroy(bitmapPtr);
|
||||
m.FPDFPage_GenerateContent(pagePtr);
|
||||
m.FPDF_ClosePage(pagePtr);
|
||||
}
|
||||
}
|
||||
|
||||
const pdfBytes = await saveRawDocument(docPtr);
|
||||
const out = createFileFromApiResponse(pdfBytes, { 'content-type': 'application/pdf' }, file.name);
|
||||
pdfWorkerManager.destroyDocument(pdf);
|
||||
return out;
|
||||
} finally {
|
||||
m.FPDF_CloseDocument(docPtr);
|
||||
}
|
||||
|
||||
const pdfBytes = await newDoc.save();
|
||||
const out = createFileFromApiResponse(pdfBytes, { 'content-type': 'application/pdf' }, file.name);
|
||||
pdfWorkerManager.destroyDocument(pdf);
|
||||
return out;
|
||||
}
|
||||
|
||||
async function processPdfClientSide(params: AdjustContrastParameters, files: File[]): Promise<CustomProcessorResult> {
|
||||
// Limit concurrency to avoid exhausting memory/CPU while still getting speedups
|
||||
// Heuristic: use up to 4 workers on capable machines, otherwise 2-3
|
||||
let CONCURRENCY_LIMIT = 2;
|
||||
if (typeof navigator !== 'undefined' && typeof navigator.hardwareConcurrency === 'number') {
|
||||
if (navigator.hardwareConcurrency >= 8) CONCURRENCY_LIMIT = 4;
|
||||
@@ -85,7 +146,6 @@ export const adjustContrastOperationConfig = {
|
||||
customProcessor: processPdfClientSide,
|
||||
operationType: 'adjustContrast',
|
||||
defaultParameters,
|
||||
// Single-step settings component for Automate
|
||||
settingsComponentPath: 'components/tools/adjustContrast/AdjustContrastSingleStepSettings',
|
||||
} as const;
|
||||
|
||||
@@ -96,4 +156,3 @@ export const useAdjustContrastOperation = () => {
|
||||
getErrorMessage: () => t('adjustContrast.error.failed', 'Failed to adjust colors/contrast')
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -2,73 +2,49 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useToolOperation, ToolType, CustomProcessorResult } from '@app/hooks/tools/shared/useToolOperation';
|
||||
import { createStandardErrorHandler } from '@app/utils/toolErrorHandler';
|
||||
import { RemoveAnnotationsParameters, defaultParameters } from '@app/hooks/tools/removeAnnotations/useRemoveAnnotationsParameters';
|
||||
import { PDFDocument, PDFName, PDFRef, PDFDict } from '@cantoo/pdf-lib';
|
||||
// Client-side PDF processing using PDF-lib
|
||||
import {
|
||||
getPdfiumModule,
|
||||
openRawDocumentSafe,
|
||||
closeDocAndFreeBuffer,
|
||||
saveRawDocument,
|
||||
} from '@app/services/pdfiumService';
|
||||
|
||||
// Client-side PDF processing using PDFium WASM
|
||||
const removeAnnotationsProcessor = async (_parameters: RemoveAnnotationsParameters, files: File[]): Promise<CustomProcessorResult> => {
|
||||
const processedFiles: File[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
// Load the PDF
|
||||
const m = await getPdfiumModule();
|
||||
const fileArrayBuffer = await file.arrayBuffer();
|
||||
const pdfBytesIn = new Uint8Array(fileArrayBuffer);
|
||||
const pdfDoc = await PDFDocument.load(pdfBytesIn, { ignoreEncryption: true });
|
||||
const ctx = pdfDoc.context;
|
||||
const docPtr = await openRawDocumentSafe(fileArrayBuffer);
|
||||
|
||||
const pages = pdfDoc.getPages();
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const page = pages[i];
|
||||
|
||||
// Annots() returns PDFArray | undefined
|
||||
const annots = page.node.Annots();
|
||||
if (!annots || annots.size() === 0) continue;
|
||||
|
||||
// Delete each annotation object (they are usually PDFRef)
|
||||
for (let j = annots.size() - 1; j >= 0; j--) {
|
||||
try {
|
||||
const entry = annots.get(j);
|
||||
if (entry instanceof PDFRef) {
|
||||
ctx.delete(entry);
|
||||
} else if (entry instanceof PDFDict) {
|
||||
// In practice, Annots array should contain refs; if not, just remove the array linkage.
|
||||
// (We avoid poking internal maps to find a ref for the dict.)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to remove annotation ${j} on page ${i + 1}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the Annots key entirely
|
||||
try {
|
||||
if (page.node.has(PDFName.of('Annots'))) {
|
||||
page.node.delete(PDFName.of('Annots'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to delete /Annots on page ${i + 1}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: if removing ALL annotations across the doc, strip AcroForm to avoid dangling widget refs
|
||||
try {
|
||||
const catalog = pdfDoc.context.lookup(pdfDoc.context.trailerInfo.Root);
|
||||
if (catalog && 'has' in catalog && 'delete' in catalog) {
|
||||
const catalogDict = catalog as any;
|
||||
if (catalogDict.has(PDFName.of('AcroForm'))) {
|
||||
catalogDict.delete(PDFName.of('AcroForm'));
|
||||
const pageCount = m.FPDF_GetPageCount(docPtr);
|
||||
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
const pagePtr = m.FPDF_LoadPage(docPtr, i);
|
||||
if (!pagePtr) continue;
|
||||
|
||||
// Remove all annotations from the page (iterate backward)
|
||||
const annotCount = m.FPDFPage_GetAnnotCount(pagePtr);
|
||||
for (let j = annotCount - 1; j >= 0; j--) {
|
||||
try {
|
||||
m.FPDFPage_RemoveAnnot(pagePtr, j);
|
||||
} catch (err) {
|
||||
console.warn(`Failed to remove annotation ${j} on page ${i + 1}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
m.FPDF_ClosePage(pagePtr);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to remove /AcroForm:', err);
|
||||
|
||||
const outBytes = await saveRawDocument(docPtr);
|
||||
const processedFile = new File([outBytes], file.name, { type: 'application/pdf' });
|
||||
processedFiles.push(processedFile);
|
||||
} finally {
|
||||
closeDocAndFreeBuffer(m, docPtr);
|
||||
}
|
||||
|
||||
// Save returns Uint8Array — safe for Blob
|
||||
const outBytes = await pdfDoc.save();
|
||||
const outBlob = new Blob([new Uint8Array(outBytes)], { type: 'application/pdf' });
|
||||
|
||||
// Create new file with original name
|
||||
const processedFile = new File([outBlob], file.name, { type: 'application/pdf' });
|
||||
|
||||
processedFiles.push(processedFile);
|
||||
} catch (error) {
|
||||
console.error('Error processing file:', file.name, error);
|
||||
throw new Error(
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
import { PDFFont, PDFPage, rgb } from '@cantoo/pdf-lib';
|
||||
import { PdfiumFont, PdfiumPage, rgb } from '@app/services/pdfiumDocBuilder';
|
||||
import { wrapText } from '@app/hooks/tools/validateSignature/utils/pdfText';
|
||||
import { colorPalette } from '@app/hooks/tools/validateSignature/utils/pdfPalette';
|
||||
|
||||
interface DrawCenteredMessageOptions {
|
||||
page: PDFPage;
|
||||
font: PDFFont;
|
||||
fontBold: PDFFont;
|
||||
page: PdfiumPage;
|
||||
font: PdfiumFont;
|
||||
fontBold: PdfiumFont;
|
||||
text: string;
|
||||
description: string;
|
||||
marginX: number;
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
import { PDFFont, PDFPage } from '@cantoo/pdf-lib';
|
||||
import { PdfiumFont, PdfiumPage } from '@app/services/pdfiumDocBuilder';
|
||||
import { wrapText } from '@app/hooks/tools/validateSignature/utils/pdfText';
|
||||
import { colorPalette } from '@app/hooks/tools/validateSignature/utils/pdfPalette';
|
||||
|
||||
interface FieldBoxOptions {
|
||||
page: PDFPage;
|
||||
font: PDFFont;
|
||||
fontBold: PDFFont;
|
||||
page: PdfiumPage;
|
||||
font: PdfiumFont;
|
||||
fontBold: PdfiumFont;
|
||||
x: number;
|
||||
top: number;
|
||||
width: number;
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
import type { TFunction } from 'i18next';
|
||||
import { PDFFont, PDFPage } from '@cantoo/pdf-lib';
|
||||
import { PdfiumFont, PdfiumPage } from '@app/services/pdfiumDocBuilder';
|
||||
import { SignatureValidationSignature } from '@app/types/validateSignature';
|
||||
import { drawFieldBox } from '@app/hooks/tools/validateSignature/outputtedPDFSections/FieldBoxSection';
|
||||
import { drawStatusBadge } from '@app/hooks/tools/validateSignature/outputtedPDFSections/StatusBadgeSection';
|
||||
@@ -8,15 +8,15 @@ import { formatDate } from '@app/hooks/tools/validateSignature/utils/pdfText';
|
||||
import { colorPalette } from '@app/hooks/tools/validateSignature/utils/pdfPalette';
|
||||
|
||||
interface DrawSignatureSectionOptions {
|
||||
page: PDFPage;
|
||||
page: PdfiumPage;
|
||||
cursorY: number;
|
||||
signature: SignatureValidationSignature;
|
||||
index: number;
|
||||
marginX: number;
|
||||
contentWidth: number;
|
||||
columnGap: number;
|
||||
font: PDFFont;
|
||||
fontBold: PDFFont;
|
||||
font: PdfiumFont;
|
||||
fontBold: PdfiumFont;
|
||||
t: TFunction<'translation'>;
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ export const drawSignatureSection = ({
|
||||
thickness: 1,
|
||||
color: colorPalette.boxBorder,
|
||||
});
|
||||
nextY -= 20;
|
||||
nextY -= 20;
|
||||
|
||||
const certificateFields = [
|
||||
{ label: t('validateSignature.cert.issuer', 'Issuer'), value: signature.issuerDN || '-' },
|
||||
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import { PDFFont, PDFPage, rgb } from '@cantoo/pdf-lib';
|
||||
import { PdfiumFont, PdfiumPage, rgb } from '@app/services/pdfiumDocBuilder';
|
||||
|
||||
interface StatusBadgeOptions {
|
||||
page: PDFPage;
|
||||
font: PDFFont;
|
||||
fontBold: PDFFont;
|
||||
page: PdfiumPage;
|
||||
font: PdfiumFont;
|
||||
fontBold: PdfiumFont;
|
||||
text: string;
|
||||
x: number;
|
||||
y: number;
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
import type { TFunction } from 'i18next';
|
||||
import { PDFFont, PDFImage, PDFPage } from '@cantoo/pdf-lib';
|
||||
import { PdfiumFont, PdfiumImage, PdfiumPage } from '@app/services/pdfiumDocBuilder';
|
||||
import { SignatureValidationReportEntry } from '@app/types/validateSignature';
|
||||
import { drawFieldBox } from '@app/hooks/tools/validateSignature/outputtedPDFSections/FieldBoxSection';
|
||||
import { drawThumbnailImage, drawThumbnailPlaceholder } from '@app/hooks/tools/validateSignature/outputtedPDFSections/ThumbnailSection';
|
||||
@@ -7,17 +7,17 @@ import { colorPalette } from '@app/hooks/tools/validateSignature/utils/pdfPalett
|
||||
import { formatFileSize } from '@app/hooks/tools/validateSignature/utils/pdfText';
|
||||
|
||||
interface DrawSummarySectionOptions {
|
||||
page: PDFPage;
|
||||
page: PdfiumPage;
|
||||
cursorY: number;
|
||||
entry: SignatureValidationReportEntry;
|
||||
font: PDFFont;
|
||||
fontBold: PDFFont;
|
||||
font: PdfiumFont;
|
||||
fontBold: PdfiumFont;
|
||||
marginX: number;
|
||||
contentWidth: number;
|
||||
columnGap: number;
|
||||
statusText: string;
|
||||
statusColor: (typeof colorPalette)['success'];
|
||||
loadThumbnail: (url: string) => Promise<{ image: PDFImage } | null>;
|
||||
loadThumbnail: (url: string) => Promise<{ image: PdfiumImage } | null>;
|
||||
t: TFunction<'translation'>;
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
import { PDFFont, PDFPage, PDFImage } from '@cantoo/pdf-lib';
|
||||
import { PdfiumFont, PdfiumPage, PdfiumImage } from '@app/services/pdfiumDocBuilder';
|
||||
import { colorPalette } from '@app/hooks/tools/validateSignature/utils/pdfPalette';
|
||||
|
||||
export const drawThumbnailPlaceholder = (
|
||||
page: PDFPage,
|
||||
fontBold: PDFFont,
|
||||
page: PdfiumPage,
|
||||
fontBold: PdfiumFont,
|
||||
x: number,
|
||||
top: number,
|
||||
width: number,
|
||||
@@ -35,8 +35,8 @@ export const drawThumbnailPlaceholder = (
|
||||
};
|
||||
|
||||
export const drawThumbnailImage = (
|
||||
page: PDFPage,
|
||||
image: PDFImage,
|
||||
page: PdfiumPage,
|
||||
image: PdfiumImage,
|
||||
x: number,
|
||||
top: number,
|
||||
width: number,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PDFDocument, PDFPage, StandardFonts } from '@cantoo/pdf-lib';
|
||||
import { PdfiumDocument, PdfiumPage, StandardFonts } from '@app/services/pdfiumDocBuilder';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { SignatureValidationReportEntry } from '@app/types/validateSignature';
|
||||
import { REPORT_PDF_FILENAME } from '@app/hooks/tools/validateSignature/utils/signatureUtils';
|
||||
@@ -16,7 +16,7 @@ const MARGIN_Y = 22;
|
||||
const CONTENT_WIDTH = PAGE_WIDTH - MARGIN_X * 2;
|
||||
const COLUMN_GAP = 18;
|
||||
|
||||
const drawDivider = (page: PDFPage, marginX: number, contentWidth: number, y: number) => {
|
||||
const drawDivider = (page: PdfiumPage, marginX: number, contentWidth: number, y: number) => {
|
||||
page.drawLine({
|
||||
start: { x: marginX, y },
|
||||
end: { x: marginX + contentWidth, y },
|
||||
@@ -29,7 +29,7 @@ export const createReportPdf = async (
|
||||
entries: SignatureValidationReportEntry[],
|
||||
t: TFunction<'translation'>
|
||||
): Promise<File> => {
|
||||
const doc = await PDFDocument.create();
|
||||
const doc = await PdfiumDocument.create();
|
||||
const font = await doc.embedFont(StandardFonts.Helvetica);
|
||||
const fontBold = await doc.embedFont(StandardFonts.HelveticaBold);
|
||||
const loadThumbnail = createThumbnailLoader(doc);
|
||||
@@ -101,7 +101,6 @@ export const createReportPdf = async (
|
||||
}
|
||||
|
||||
for (let i = 0; i < entry.signatures.length; i += 1) {
|
||||
// After the first signature, start a new page per signature
|
||||
if (i > 0) {
|
||||
({ page, cursorY } = startReportPage({
|
||||
doc,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { PDFDocument, PDFFont, PDFImage } from '@cantoo/pdf-lib';
|
||||
import { PdfiumDocument, PdfiumFont, PdfiumImage } from '@app/services/pdfiumDocBuilder';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { colorPalette } from '@app/hooks/tools/validateSignature/utils/pdfPalette';
|
||||
|
||||
interface StartPageParams {
|
||||
doc: PDFDocument;
|
||||
font: PDFFont;
|
||||
fontBold: PDFFont;
|
||||
doc: PdfiumDocument;
|
||||
font: PdfiumFont;
|
||||
fontBold: PdfiumFont;
|
||||
marginX: number;
|
||||
marginY: number;
|
||||
contentWidth: number;
|
||||
@@ -63,8 +63,8 @@ export const startReportPage = ({
|
||||
return { page, cursorY };
|
||||
};
|
||||
|
||||
export const createThumbnailLoader = (doc: PDFDocument) => {
|
||||
const cache = new Map<string, { image: PDFImage } | null>();
|
||||
export const createThumbnailLoader = (doc: PdfiumDocument) => {
|
||||
const cache = new Map<string, { image: PdfiumImage } | null>();
|
||||
|
||||
return async (url: string) => {
|
||||
if (cache.has(url)) {
|
||||
@@ -75,7 +75,7 @@ export const createThumbnailLoader = (doc: PDFDocument) => {
|
||||
const response = await fetch(url);
|
||||
const bytes = new Uint8Array(await response.arrayBuffer());
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
let image: PDFImage;
|
||||
let image: PdfiumImage;
|
||||
|
||||
if (contentType.includes('png')) {
|
||||
image = await doc.embedPng(bytes);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { rgb } from '@cantoo/pdf-lib';
|
||||
import { rgb } from '@app/services/pdfiumDocBuilder';
|
||||
|
||||
type RgbTuple = [number, number, number];
|
||||
|
||||
@@ -21,7 +21,7 @@ const defaultLightPalette: Record<
|
||||
const toRgb = ([r, g, b]: RgbTuple) => rgb(r / 255, g / 255, b / 255);
|
||||
|
||||
/**
|
||||
* Utility function to get CSS variable values and convert them to pdf-lib RGB format.
|
||||
* Utility function to get CSS variable values and convert them to RGB format.
|
||||
* Falls back to sensible defaults when the CSS variable cannot be resolved.
|
||||
*/
|
||||
function getCssVariableAsRgb(variableName: string, fallback: RgbTuple) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PDFFont } from '@cantoo/pdf-lib';
|
||||
import { PdfiumFont } from '@app/services/pdfiumDocBuilder';
|
||||
|
||||
export const wrapText = (text: string, font: PDFFont, fontSize: number, maxWidth: number): string[] => {
|
||||
export const wrapText = (text: string, font: PdfiumFont, fontSize: number, maxWidth: number): string[] => {
|
||||
const lines: string[] = [];
|
||||
const paragraphs = text.split(/\r?\n/);
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
PDFDocument,
|
||||
} from '@cantoo/pdf-lib';
|
||||
import {
|
||||
PdfLibLink,
|
||||
extractLinksFromPage,
|
||||
@@ -20,14 +17,14 @@ export interface PdfLibLinksResult {
|
||||
}
|
||||
|
||||
interface CachedDoc {
|
||||
doc: PDFDocument;
|
||||
data: ArrayBuffer;
|
||||
/** Number of active consumers (hook instances) holding this entry. */
|
||||
refCount: number;
|
||||
/** Per-page extracted links (lazy, filled on first request). */
|
||||
pageLinks: Map<number, { links: PdfLibLink[]; width: number; height: number }>;
|
||||
/** Set to true when the PDF catalog/pages tree is invalid, so we
|
||||
/** Set to true when the PDF is invalid, so we
|
||||
* skip link extraction on all subsequent calls without retrying. */
|
||||
invalidCatalog?: boolean;
|
||||
invalidDocument?: boolean;
|
||||
}
|
||||
|
||||
const docCache = new Map<string, Promise<CachedDoc>>();
|
||||
@@ -37,13 +34,8 @@ async function acquireDocument(url: string): Promise<CachedDoc> {
|
||||
const promise = (async (): Promise<CachedDoc> => {
|
||||
const response = await fetch(url);
|
||||
const buffer = await response.arrayBuffer();
|
||||
const doc = await PDFDocument.load(new Uint8Array(buffer), {
|
||||
ignoreEncryption: true,
|
||||
updateMetadata: false,
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
|
||||
return { doc, refCount: 0, pageLinks: new Map() };
|
||||
return { data: buffer, refCount: 0, pageLinks: new Map() };
|
||||
})();
|
||||
docCache.set(url, promise);
|
||||
|
||||
@@ -107,7 +99,7 @@ export function usePdfLibLinks(
|
||||
return;
|
||||
}
|
||||
|
||||
if (cached.invalidCatalog) {
|
||||
if (cached.invalidDocument) {
|
||||
setResult({ links: [], pdfPageWidth: 0, pdfPageHeight: 0, loading: false });
|
||||
releaseDocument(url);
|
||||
return;
|
||||
@@ -115,27 +107,12 @@ export function usePdfLibLinks(
|
||||
|
||||
let pageData = cached.pageLinks.get(pageIndex);
|
||||
if (!pageData) {
|
||||
let pageCount: number;
|
||||
try {
|
||||
pageCount = cached.doc.getPageCount();
|
||||
} catch {
|
||||
cached.invalidCatalog = true;
|
||||
setResult({ links: [], pdfPageWidth: 0, pdfPageHeight: 0, loading: false });
|
||||
releaseDocument(url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageIndex < 0 || pageIndex >= pageCount) {
|
||||
setResult({ links: [], pdfPageWidth: 0, pdfPageHeight: 0, loading: false });
|
||||
releaseDocument(url);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const page = cached.doc.getPage(pageIndex);
|
||||
const { width, height } = page.getSize();
|
||||
const links = extractLinksFromPage(cached.doc, page, pageIndex);
|
||||
pageData = { links, width, height };
|
||||
const { links, pdfPageWidth, pdfPageHeight } = await extractLinksFromPage(
|
||||
cached.data,
|
||||
pageIndex,
|
||||
);
|
||||
pageData = { links, width: pdfPageWidth, height: pdfPageHeight };
|
||||
cached.pageLinks.set(pageIndex, pageData);
|
||||
} catch (pageError) {
|
||||
console.warn(`[usePdfLibLinks] Failed to read page ${pageIndex}:`, pageError);
|
||||
|
||||
Reference in New Issue
Block a user