Files
Stirling-PDF/frontend/src/core/utils/pdfiumBitmapUtils.ts
T
c3530024c4 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]>
2026-03-24 13:34:52 +00:00

209 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* pdfiumBitmapUtils — Shared utilities for working with PDFium WASM bitmaps.
*
* Centralises the RGBA→BGRA pixel conversion and image-to-page-object
* embedding that previously appeared (copy-pasted) in at least 5 files.
*
* Performance note: instead of calling `m.pdfium.setValue()` per pixel
* (which crosses the JS↔WASM FFI boundary on every call), we perform
* the colour-channel swizzle in a plain JS TypedArray and then bulk-
* copy the result into the WASM heap with a single `HEAPU8.set()`.
*/
import type { WrappedPdfiumModule } from '@embedpdf/pdfium';
/** FPDF_ANNOT_LINK */
export const FPDF_ANNOT_LINK = 4;
/** FPDF_ANNOT_WIDGET (form field) */
export const FPDF_ANNOT_WIDGET = 20;
/** FPDF_ANNOT_INK */
export const FPDF_ANNOT_INK = 15;
/** FPDF_ANNOT_LINE */
export const FPDF_ANNOT_LINE = 3;
/** PDFACTION_GOTO */
export const PDFACTION_GOTO = 1;
/** PDFACTION_URI */
export const PDFACTION_URI = 3;
/** FLAT_PRINT (for FPDFPage_Flatten) */
export const FLAT_PRINT = 2;
/**
* Convert an RGBA pixel buffer to BGRA (PDFium's expected format) **in place
* inside the WASM heap** with a single bulk memcpy.
*
* When `stride === width * 4` the copy is a single `HEAPU8.set()`.
* When the bitmap has padding (stride > width * 4), rows are copied
* individually to skip the padding bytes.
*
* This is ~100× faster than per-pixel `m.pdfium.setValue()` calls for
* large images.
*/
export function copyRgbaToBgraHeap(
m: WrappedPdfiumModule,
rgba: Uint8Array | Uint8ClampedArray,
bufferPtr: number,
width: number,
height: number,
stride: number,
): void {
const rowBytes = width * 4;
if (stride === rowBytes) {
// Fast path: no padding — single bulk copy after swizzle
const bgra = new Uint8Array(rgba.length);
for (let i = 0; i < rgba.length; i += 4) {
bgra[i] = rgba[i + 2]; // B
bgra[i + 1] = rgba[i + 1]; // G
bgra[i + 2] = rgba[i]; // R
bgra[i + 3] = rgba[i + 3]; // A
}
new Uint8Array(
(m.pdfium.wasmExports as any).memory.buffer,
).set(bgra, bufferPtr);
} else {
// Stride has padding — swizzle + copy row by row
const rowBuf = new Uint8Array(rowBytes);
const heap = new Uint8Array(
(m.pdfium.wasmExports as any).memory.buffer,
);
for (let y = 0; y < height; y++) {
const srcRowStart = y * rowBytes;
for (let x = 0; x < rowBytes; x += 4) {
rowBuf[x] = rgba[srcRowStart + x + 2]; // B
rowBuf[x + 1] = rgba[srcRowStart + x + 1]; // G
rowBuf[x + 2] = rgba[srcRowStart + x]; // R
rowBuf[x + 3] = rgba[srcRowStart + x + 3]; // A
}
heap.set(rowBuf, bufferPtr + y * stride);
}
}
}
export interface DecodedImage {
rgba: Uint8Array;
width: number;
height: number;
}
/**
* Create a PDFium bitmap from decoded RGBA pixels, attach it to a new image
* page object, position it via an affine matrix, and insert it into the page.
*
* Returns `true` if the image was successfully inserted, `false` otherwise.
* All intermediate WASM resources are cleaned up on failure.
*/
export function embedBitmapImageOnPage(
m: WrappedPdfiumModule,
docPtr: number,
pagePtr: number,
image: DecodedImage,
pdfX: number,
pdfY: number,
drawWidth: number,
drawHeight: number,
): boolean {
const bitmapPtr = m.FPDFBitmap_Create(image.width, image.height, 1);
if (!bitmapPtr) return false;
try {
const bufferPtr = m.FPDFBitmap_GetBuffer(bitmapPtr);
const stride = m.FPDFBitmap_GetStride(bitmapPtr);
copyRgbaToBgraHeap(m, image.rgba, bufferPtr, image.width, image.height, stride);
const imageObjPtr = m.FPDFPageObj_NewImageObj(docPtr);
if (!imageObjPtr) return false;
const setBitmapOk = m.FPDFImageObj_SetBitmap(pagePtr, 0, imageObjPtr, bitmapPtr);
if (!setBitmapOk) {
m.FPDFPageObj_Destroy(imageObjPtr);
return false;
}
// -- early-destroy the bitmap; PDFium has copied the pixel data internally
m.FPDFBitmap_Destroy(bitmapPtr);
// Set affine transform: [a b c d e f]
const matrixPtr = m.pdfium.wasmExports.malloc(6 * 4);
try {
m.pdfium.setValue(matrixPtr, drawWidth, 'float'); // a — scaleX
m.pdfium.setValue(matrixPtr + 4, 0, 'float'); // b
m.pdfium.setValue(matrixPtr + 8, 0, 'float'); // c
m.pdfium.setValue(matrixPtr + 12, drawHeight, 'float'); // d — scaleY
m.pdfium.setValue(matrixPtr + 16, pdfX, 'float'); // e — translateX
m.pdfium.setValue(matrixPtr + 20, pdfY, 'float'); // f — translateY
if (!m.FPDFPageObj_SetMatrix(imageObjPtr, matrixPtr)) {
m.FPDFPageObj_Destroy(imageObjPtr);
return false;
}
} finally {
m.pdfium.wasmExports.free(matrixPtr);
}
m.FPDFPage_InsertObject(pagePtr, imageObjPtr);
return true;
} finally {
// Safety net: FPDFBitmap_Destroy is a no-op if ptr is 0 in most PDFium
// builds but guard anyway. If already destroyed above, the second call
// is harmless because we allow it to be idempotent.
// We use a try-catch to be safe across PDFium WASM builds.
try { m.FPDFBitmap_Destroy(bitmapPtr); } catch { /* already freed */ }
}
}
/**
* Draw a simple light-grey rectangle as a placeholder for annotations
* that could not be rendered.
*/
export function drawPlaceholderRect(
m: WrappedPdfiumModule,
pagePtr: number,
x: number,
y: number,
width: number,
height: number,
): void {
const pathPtr = m.FPDFPageObj_CreateNewPath(x, y);
if (!pathPtr) return;
m.FPDFPath_LineTo(pathPtr, x + width, y);
m.FPDFPath_LineTo(pathPtr, x + width, y + height);
m.FPDFPath_LineTo(pathPtr, x, y + height);
m.FPDFPath_Close(pathPtr);
m.FPDFPageObj_SetFillColor(pathPtr, 230, 230, 230, 150);
m.FPDFPageObj_SetStrokeColor(pathPtr, 128, 128, 128, 255);
m.FPDFPageObj_SetStrokeWidth(pathPtr, 1);
// fillMode 1 = alternate fill, stroke = true
m.FPDFPath_SetDrawMode(pathPtr, 1, true);
m.FPDFPage_InsertObject(pagePtr, pathPtr);
}
/**
* Decode an image data URL (e.g. `data:image/png;base64,...`) to raw RGBA
* pixel data via an offscreen canvas.
*/
export function decodeImageDataUrl(dataUrl: string): Promise<DecodedImage | null> {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
try {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (!ctx) { resolve(null); return; }
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
resolve({
rgba: new Uint8Array(imageData.data.buffer),
width: canvas.width,
height: canvas.height,
});
} catch { resolve(null); }
};
img.onerror = () => resolve(null);
img.src = dataUrl;
});
}