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:
brios
2026-03-24 13:34:52 +00:00
committed by GitHub
co-authored by Reece Browne
parent 3ea11352e3
commit c3530024c4
34 changed files with 4299 additions and 1910 deletions
@@ -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(
@@ -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;
@@ -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;
@@ -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 || '-' },
@@ -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;
@@ -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'>;
}
@@ -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/);
+10 -33
View File
@@ -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);