fix(sign): preserve PNG signature placement and page content (#7093)

# Description of Changes

This PR fixes PNG signature application issues in the PDF signing
workflow.

## What was changed

- Reworked signature application to create locked and printable PDFium
stamp annotations with dedicated appearance streams.
- Removed the use of `FPDFPage_GenerateContent()` from the signature
workflow.
- Preserved the signature's original position and dimensions when
converting from the viewer's top-left coordinate system to PDF
coordinates.
- Added CropBox-aware coordinate conversion for PDFs whose visible page
origin differs from the MediaBox origin.
- Improved signature image extraction to handle internal EmbedPDF asset
references and nested image data.
- Refactored PDFium bitmap creation so image objects can safely be
transferred to annotations.
- Corrected PDFium bitmap ownership and cleanup to prevent duplicate
destruction.
- Added a PDFium WASM integration test covering:
  - Existing page-content preservation
  - Stamp appearance generation
  - Signature coordinates and dimensions
  - Printable, read-only, and locked annotation flags
- Persisted image data taking precedence over internal asset references

## Why the change was made

Applying a PNG signature previously regenerated the complete page
content through PDFium. This could corrupt existing vector or font-based
page elements, including the university logo reported in the linked
issue.

The previous coordinate conversion also relied only on the page height
and did not account for CropBox offsets, allowing the applied signature
to move from its preview position.

Creating a PDFium stamp annotation with its own appearance stream avoids
regenerating existing page content while retaining the selected
signature position and size.


Closes #7083

---

## 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/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 run `task check` to verify linters, typechecks, and tests
pass
- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.
This commit is contained in:
Ludy
2026-07-20 12:05:01 +00:00
committed by GitHub
parent 4cfe29139a
commit f4d760b139
3 changed files with 436 additions and 237 deletions
@@ -82,6 +82,82 @@ export interface DecodedImage {
height: number;
}
function setImageObjectMatrix(
m: WrappedPdfiumModule,
imageObjPtr: number,
pdfX: number,
pdfY: number,
drawWidth: number,
drawHeight: number,
): boolean {
const matrixPtr = m.pdfium.wasmExports.malloc(6 * 4);
try {
m.pdfium.setValue(matrixPtr, drawWidth, "float");
m.pdfium.setValue(matrixPtr + 4, 0, "float");
m.pdfium.setValue(matrixPtr + 8, 0, "float");
m.pdfium.setValue(matrixPtr + 12, drawHeight, "float");
m.pdfium.setValue(matrixPtr + 16, pdfX, "float");
m.pdfium.setValue(matrixPtr + 20, pdfY, "float");
return m.FPDFPageObj_SetMatrix(imageObjPtr, matrixPtr);
} finally {
m.pdfium.wasmExports.free(matrixPtr);
}
}
/**
* Create a PDFium image page object from decoded pixels.
*
* The caller owns the returned object until it is inserted into a page or
* appended to an annotation. Destroy it with FPDFPageObj_Destroy on failure.
*/
export function createBitmapImageObject(
m: WrappedPdfiumModule,
docPtr: number,
pagePtr: number,
image: DecodedImage,
pdfX: number,
pdfY: number,
drawWidth: number,
drawHeight: number,
): number | null {
const bitmapPtr = m.FPDFBitmap_Create(image.width, image.height, 1);
if (!bitmapPtr) return null;
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 null;
if (!m.FPDFImageObj_SetBitmap(pagePtr, 0, imageObjPtr, bitmapPtr)) {
m.FPDFPageObj_Destroy(imageObjPtr);
return null;
}
if (
!setImageObjectMatrix(m, imageObjPtr, pdfX, pdfY, drawWidth, drawHeight)
) {
m.FPDFPageObj_Destroy(imageObjPtr);
return null;
}
return imageObjPtr;
} finally {
// FPDFImageObj_SetBitmap copies the bitmap data into the image object.
m.FPDFBitmap_Destroy(bitmapPtr);
}
}
/**
* 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.
@@ -99,70 +175,20 @@ export function embedBitmapImageOnPage(
drawWidth: number,
drawHeight: number,
): boolean {
const bitmapPtr = m.FPDFBitmap_Create(image.width, image.height, 1);
if (!bitmapPtr) return false;
const imageObjPtr = createBitmapImageObject(
m,
docPtr,
pagePtr,
image,
pdfX,
pdfY,
drawWidth,
drawHeight,
);
if (!imageObjPtr) 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 */
}
}
m.FPDFPage_InsertObject(pagePtr, imageObjPtr);
return true;
}
/**
* Draw a simple light-grey rectangle as a placeholder for annotations
@@ -206,8 +232,8 @@ export function decodeImageDataUrl(
img.onload = () => {
try {
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
canvas.width = img.naturalWidth || img.width;
canvas.height = img.naturalHeight || img.height;
const ctx = canvas.getContext("2d");
if (!ctx) {
resolve(null);
@@ -0,0 +1,114 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
import {
PDFArray,
PDFDict,
PDFDocument,
PDFName,
PDFNumber,
PDFRawStream,
decodePDFRawStream,
} from "@cantoo/pdf-lib";
import { embedSignatureImages } from "@app/utils/signatureFlattening";
const ONE_PIXEL_PNG =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
beforeAll(async () => {
const wasmPath = path.resolve(
process.cwd(),
"node_modules/@embedpdf/pdfium/dist/pdfium.wasm",
);
const wasmBytes = await readFile(wasmPath);
vi.stubGlobal(
"fetch",
vi.fn(async () =>
Promise.resolve(
new Response(wasmBytes, {
headers: { "Content-Type": "application/wasm" },
}),
),
),
);
});
afterAll(() => {
vi.unstubAllGlobals();
});
const readPageContentStreams = (document: PDFDocument): string[] => {
const contents = document.getPage(0).node.Contents();
if (!(contents instanceof PDFArray)) return [];
const decoder = new TextDecoder();
const streams: string[] = [];
for (let index = 0; index < contents.size(); index++) {
const stream = contents.lookup(index, PDFRawStream);
streams.push(decoder.decode(decodePDFRawStream(stream).decode()));
}
return streams;
};
describe("signatureFlattening", () => {
test("adds a PDFium stamp without regenerating page content", async () => {
const sourceDocument = await PDFDocument.create();
const sourcePage = sourceDocument.addPage([300, 400]);
const markerStream = sourceDocument.context.stream(
"q\n% ORIGINAL_TYPE3_CONTENT\nQ\n",
);
sourcePage.node.addContentStream(
sourceDocument.context.register(markerStream),
);
const sourceBytes = await sourceDocument.save();
const outputBytes = await embedSignatureImages(
Uint8Array.from(sourceBytes).buffer,
[
{
pageIndex: 0,
annotations: [
{
id: "signature-1",
// EmbedPDF may expose an internal asset reference here after the
// annotation has been placed. The persisted PNG must win.
imageData: "embedpdf-asset-reference",
rect: {
origin: { x: 25, y: 30 },
size: { width: 120, height: 50 },
},
imageSrc: `data:image/png;base64,${ONE_PIXEL_PNG}`,
},
],
},
],
(id) =>
id === "signature-1"
? `data:image/png;base64,${ONE_PIXEL_PNG}`
: undefined,
async () => ({
width: 1,
height: 1,
rgba: new Uint8Array([0, 80, 180, 255]),
}),
);
const outputDocument = await PDFDocument.load(outputBytes);
const contentStreams = readPageContentStreams(outputDocument);
const annotations = outputDocument.getPage(0).node.Annots();
expect(contentStreams).toContain("q\n% ORIGINAL_TYPE3_CONTENT\nQ\n");
expect(annotations).toBeInstanceOf(PDFArray);
const stamp = annotations?.lookup(0, PDFDict);
const stampRect = stamp?.lookup(PDFName.of("Rect"), PDFArray);
expect(stamp?.get(PDFName.of("Subtype"))).toEqual(PDFName.of("Stamp"));
expect(stamp?.lookup(PDFName.of("F"), PDFNumber).asNumber()).toBe(196);
expect(stamp?.get(PDFName.of("AP"))).toBeDefined();
expect(
Array.from({ length: stampRect?.size() ?? 0 }, (_, index) =>
stampRect?.lookup(index, PDFNumber).asNumber(),
),
).toEqual([25, 320, 145, 370]);
}, 20_000);
});
@@ -1,11 +1,15 @@
// PDFium annotation subtype constants
import {
FPDF_ANNOT_INK,
FPDF_ANNOT_LINE,
embedBitmapImageOnPage,
drawPlaceholderRect,
createBitmapImageObject,
decodeImageDataUrl,
type DecodedImage,
} from "@app/utils/pdfiumBitmapUtils";
import {
closeDocAndFreeBuffer,
getPdfiumModule,
openRawDocumentSafe,
readEffectivePageBox,
saveRawDocument,
} from "@app/services/pdfiumService";
import { generateThumbnailWithMetadata } from "@app/utils/thumbnailUtils";
import {
createChildStub,
@@ -18,12 +22,6 @@ import {
StirlingFileStub,
} from "@app/types/fileContext";
import type { SignatureAPI } from "@app/components/viewer/viewerTypes";
import {
getPdfiumModule,
openRawDocumentSafe,
closeDocAndFreeBuffer,
saveRawDocument,
} from "@app/services/pdfiumService";
interface MinimalFileContextSelectors {
getAllFileIds: () => FileId[];
@@ -75,23 +73,9 @@ export async function flattenSignatures(
const pageAnnotations =
await signatureApiRef.current.getPageAnnotations(pageIndex);
if (pageAnnotations && pageAnnotations.length > 0) {
const sessionAnnotations = pageAnnotations.filter((annotation) => {
const hasStoredImageData =
annotation.id && getImageData(annotation.id);
const hasDirectImageData =
annotation.imageData ||
annotation.appearance ||
annotation.stampData ||
annotation.imageSrc ||
annotation.contents ||
annotation.data;
return (
hasStoredImageData ||
(hasDirectImageData &&
typeof hasDirectImageData === "string" &&
hasDirectImageData.startsWith("data:image"))
);
});
const sessionAnnotations = pageAnnotations.filter((annotation) =>
Boolean(getAnnotationImageData(annotation, getImageData)),
);
if (sessionAnnotations.length > 0) {
allAnnotations.push({
@@ -166,143 +150,23 @@ export async function flattenSignatures(
type: "application/pdf",
});
// Step 4: Manually render extracted annotations onto the PDF using PDFium WASM
// Step 4: Add signatures as locked, printable PDFium stamp annotations.
// FPDFAnnot_AppendObject creates the annotation appearance without asking
// PDFium to regenerate the page's existing content. GenerateContent would
// corrupt some Type3/vector content, including the issue #7083 logo.
if (allAnnotations.length > 0) {
try {
const pdfArrayBufferForFlattening = await signedFile.arrayBuffer();
const m = await getPdfiumModule();
const docPtr = await openRawDocumentSafe(pdfArrayBufferForFlattening);
try {
const pageCount = m.FPDF_GetPageCount(docPtr);
for (const pageData of allAnnotations) {
const { pageIndex, annotations } = pageData;
if (pageIndex < pageCount) {
const pagePtr = m.FPDF_LoadPage(docPtr, pageIndex);
if (!pagePtr) continue;
const pageHeight = m.FPDF_GetPageHeightF(pagePtr);
for (const annotation of annotations) {
try {
const rect =
annotation.rect ||
annotation.bounds ||
annotation.rectangle ||
annotation.position;
if (rect) {
const originalX =
rect.origin?.x || rect.x || rect.left || 0;
const originalY =
rect.origin?.y || rect.y || rect.top || 0;
const width = rect.size?.width || rect.width || 100;
const height = rect.size?.height || rect.height || 50;
// Convert from CSS top-left to PDF bottom-left
const pdfX = originalX;
const pdfY = pageHeight - originalY - height;
let imageDataUrl =
annotation.imageData ||
annotation.appearance ||
annotation.stampData ||
annotation.imageSrc ||
annotation.contents ||
annotation.data;
if (!imageDataUrl && annotation.id) {
const storedImageData = getImageData(annotation.id);
if (storedImageData) {
imageDataUrl = storedImageData;
}
}
// Convert SVG to PNG first if needed
if (
imageDataUrl &&
typeof imageDataUrl === "string" &&
imageDataUrl.startsWith("data:image/svg+xml")
) {
const pngBytes = await rasteriseSvgToPng(
imageDataUrl,
width * 2,
height * 2,
);
if (pngBytes) {
imageDataUrl = await uint8ArrayToPngDataUrl(pngBytes);
} else {
drawPlaceholderRect(
m,
pagePtr,
pdfX,
pdfY,
width,
height,
);
continue;
}
}
if (
imageDataUrl &&
typeof imageDataUrl === "string" &&
imageDataUrl.startsWith("data:image")
) {
// Decode the image data URL to raw pixels via canvas
const imageResult =
await decodeImageDataUrl(imageDataUrl);
if (imageResult) {
embedBitmapImageOnPage(
m,
docPtr,
pagePtr,
imageResult,
pdfX,
pdfY,
width,
height,
);
}
} else if (
annotation.type === FPDF_ANNOT_INK ||
annotation.type === FPDF_ANNOT_LINE
) {
drawPlaceholderRect(
m,
pagePtr,
pdfX,
pdfY,
width,
height,
);
}
}
} catch (annotationError) {
console.warn(
"Failed to render annotation:",
annotationError,
);
}
}
m.FPDFPage_GenerateContent(pagePtr);
m.FPDF_ClosePage(pagePtr);
}
}
const resultBuf = await saveRawDocument(docPtr);
signedFile = new File([resultBuf], currentFile.name, {
type: "application/pdf",
});
} finally {
closeDocAndFreeBuffer(m, docPtr);
}
const resultBytes = await embedSignatureImages(
await signedFile.arrayBuffer(),
allAnnotations,
getImageData,
);
signedFile = new File([resultBytes as BlobPart], currentFile.name, {
type: "application/pdf",
});
} catch (renderError) {
console.error("Failed to manually render annotations:", renderError);
console.warn("Signatures may only show as annotations");
console.error("Failed to embed signature images:", renderError);
console.warn("Signatures may only remain as annotations");
}
}
@@ -343,16 +207,211 @@ export async function flattenSignatures(
}
}
/**
* Convert Uint8Array PNG bytes to a data URL for canvas decoding.
*/
function uint8ArrayToPngDataUrl(pngBytes: Uint8Array): Promise<string> {
return new Promise((resolve) => {
const blob = new Blob([pngBytes as BlobPart], { type: "image/png" });
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.readAsDataURL(blob);
});
type SignatureAnnotationsByPage = Array<{
pageIndex: number;
annotations: any[];
}>;
function extractImageDataUrl(
value: unknown,
depth = 0,
visited: Set<unknown> = new Set(),
): string | undefined {
if (!value || depth > 6) return undefined;
if (typeof value === "string") {
return value.startsWith("data:image") ? value : undefined;
}
if (typeof value !== "object" || visited.has(value)) return undefined;
visited.add(value);
const entries = Array.isArray(value)
? value
: Object.values(value as Record<string, unknown>);
for (const entry of entries) {
const imageDataUrl = extractImageDataUrl(entry, depth + 1, visited);
if (imageDataUrl) return imageDataUrl;
}
return undefined;
}
function getAnnotationImageData(
annotation: any,
getImageData: (id: string) => string | undefined,
): string | undefined {
// EmbedPDF can replace fields such as imageData/appearance with an internal
// asset reference after placement. Prefer our persistent original and only
// accept values that actually contain an image data URL.
const candidates: unknown[] = [
annotation.id ? getImageData(annotation.id) : undefined,
annotation.imageSrc,
annotation.imageData,
annotation.appearance,
annotation.stampData,
annotation.contents,
annotation.data,
annotation.customData,
annotation.asset,
];
for (const candidate of candidates) {
const imageDataUrl = extractImageDataUrl(candidate);
if (imageDataUrl) return imageDataUrl;
}
return undefined;
}
export async function embedSignatureImages(
pdfArrayBuffer: ArrayBuffer,
annotationsByPage: SignatureAnnotationsByPage,
getImageData: (id: string) => string | undefined,
imageDecoder: (
dataUrl: string,
) => Promise<DecodedImage | null> = decodeImageDataUrl,
): Promise<ArrayBuffer> {
const m = await getPdfiumModule();
const docPtr = await openRawDocumentSafe(pdfArrayBuffer);
try {
const pageCount = m.FPDF_GetPageCount(docPtr);
for (const { pageIndex, annotations } of annotationsByPage) {
if (pageIndex < 0 || pageIndex >= pageCount) continue;
const pagePtr = m.FPDF_LoadPage(docPtr, pageIndex);
if (!pagePtr) continue;
try {
const pageBox = readEffectivePageBox(m, pagePtr);
const cropHeight = pageBox.top - pageBox.bottom;
for (const annotation of annotations) {
const rect =
annotation.rect ??
annotation.bounds ??
annotation.rectangle ??
annotation.position;
if (!rect) continue;
const originalX = rect.origin?.x ?? rect.x ?? rect.left ?? 0;
const originalY = rect.origin?.y ?? rect.y ?? rect.top ?? 0;
const width = rect.size?.width ?? rect.width ?? 100;
const height = rect.size?.height ?? rect.height ?? 50;
if (width <= 0 || height <= 0) continue;
let imageDataUrl = getAnnotationImageData(annotation, getImageData);
if (!imageDataUrl) continue;
if (imageDataUrl.startsWith("data:image/svg+xml")) {
const pngBytes = await rasteriseSvgToPng(
imageDataUrl,
width * 2,
height * 2,
);
if (!pngBytes) continue;
imageDataUrl = `data:image/png;base64,${uint8ArrayToBase64(pngBytes)}`;
}
const decodedImage = await imageDecoder(imageDataUrl);
if (!decodedImage) continue;
const pdfX = pageBox.left + originalX;
const pdfY = pageBox.bottom + cropHeight - originalY - height;
appendStampAnnotation(
m,
docPtr,
pagePtr,
decodedImage,
pdfX,
pdfY,
width,
height,
);
}
} finally {
m.FPDF_ClosePage(pagePtr);
}
}
return await saveRawDocument(docPtr);
} finally {
closeDocAndFreeBuffer(m, docPtr);
}
}
const FPDF_ANNOT_STAMP = 13;
const FPDF_ANNOT_FLAG_PRINT = 1 << 2;
const FPDF_ANNOT_FLAG_READONLY = 1 << 6;
const FPDF_ANNOT_FLAG_LOCKED = 1 << 7;
function appendStampAnnotation(
m: Awaited<ReturnType<typeof getPdfiumModule>>,
docPtr: number,
pagePtr: number,
image: DecodedImage,
pdfX: number,
pdfY: number,
width: number,
height: number,
): boolean {
const annotationIndex = m.FPDFPage_GetAnnotCount(pagePtr);
const annotPtr = m.FPDFPage_CreateAnnot(pagePtr, FPDF_ANNOT_STAMP);
if (!annotPtr) return false;
let appended = false;
let imageObjPtr = 0;
const rectPtr = m.pdfium.wasmExports.malloc(4 * 4);
try {
// FS_RECTF layout: left, top, right, bottom.
m.pdfium.setValue(rectPtr, pdfX, "float");
m.pdfium.setValue(rectPtr + 4, pdfY + height, "float");
m.pdfium.setValue(rectPtr + 8, pdfX + width, "float");
m.pdfium.setValue(rectPtr + 12, pdfY, "float");
if (!m.FPDFAnnot_SetRect(annotPtr, rectPtr)) return false;
imageObjPtr =
createBitmapImageObject(
m,
docPtr,
pagePtr,
image,
pdfX,
pdfY,
width,
height,
) ?? 0;
if (!imageObjPtr) return false;
if (!m.FPDFAnnot_AppendObject(annotPtr, imageObjPtr)) return false;
imageObjPtr = 0; // The annotation owns the object after a successful append.
m.FPDFAnnot_SetFlags(
annotPtr,
FPDF_ANNOT_FLAG_PRINT | FPDF_ANNOT_FLAG_READONLY | FPDF_ANNOT_FLAG_LOCKED,
);
appended = true;
return true;
} finally {
m.pdfium.wasmExports.free(rectPtr);
if (imageObjPtr) m.FPDFPageObj_Destroy(imageObjPtr);
m.FPDFPage_CloseAnnot(annotPtr);
if (!appended) m.FPDFPage_RemoveAnnot(pagePtr, annotationIndex);
}
}
function uint8ArrayToBase64(bytes: Uint8Array): string {
let binary = "";
const chunkSize = 0x8000;
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
binary += String.fromCharCode(
...bytes.subarray(offset, offset + chunkSize),
);
}
return btoa(binary);
}
/**