From 0e67adde097c401cbb23a10a12c2464ec79d72e4 Mon Sep 17 00:00:00 2001 From: Raphael Taylor-Davies Date: Mon, 2 Feb 2026 22:56:22 +0000 Subject: [PATCH] Add PdfPage insertText --- packages/pdfrx/assets/pdfium_worker.js | 138 +++++++++++++++++- packages/pdfrx/lib/src/wasm/pdfrx_wasm.dart | 32 ++++ .../lib/pdfrx_coregraphics.dart | 13 ++ .../lib/src/native/pdfrx_pdfium.dart | 80 ++++++++++ packages/pdfrx_engine/lib/src/pdf_page.dart | 20 +++ .../lib/src/pdf_page_proxies.dart | 46 ++++++ 6 files changed, 328 insertions(+), 1 deletion(-) diff --git a/packages/pdfrx/assets/pdfium_worker.js b/packages/pdfrx/assets/pdfium_worker.js index f22c6aa2..9dde7373 100644 --- a/packages/pdfrx/assets/pdfium_worker.js +++ b/packages/pdfrx/assets/pdfium_worker.js @@ -982,7 +982,7 @@ function loadPagesProgressively(params) { } /** - * + * * @param {{docHandle: number, pageIndices: number[]|undefined, currentPagesCount: number}} params * @returns {{pages: PdfPage[], missingFonts: FontQueries}} */ @@ -2116,6 +2116,119 @@ function _setImageObjPixels(pageHandle, imageObj, pixels, pixelWidth, pixelHeigh } } +/** + * @param {{ + * docHandle: number, + * pageIndex: number, + * text: string, + * fontSize: number, + * x: number, + * y: number, + * anchorX: number, + * anchorY: number, + * rotation: number, + * textColor: number, + * fontName: string, + * }} params + */ +function insertText(params) { + const { + docHandle, + pageIndex, + text, + fontSize, + x, + y, + anchorX, + anchorY, + rotation, + textColor, + fontName, + } = params; + + let textUtf16 = StringUtils.allocateUTF16(text); + let fontNameUtf8 = StringUtils.allocateUTF8(fontName); + let pageHandle = 0; + let fontHandle = 0; + try { + pageHandle = Pdfium.wasmExports.FPDF_LoadPage(docHandle, pageIndex); + if (!pageHandle) { + throw new Error(`Failed to load page ${pageIndex} from document ${docHandle}`); + } + + fontHandle = Pdfium.wasmExports.FPDFText_LoadStandardFont(docHandle, fontNameUtf8); + if (!fontHandle) { + const error = Pdfium.wasmExports.FPDF_GetLastError(); + throw new Error(`FPDFText_LoadStandardFont failed (${_getErrorMessage(error)})`); + } + + const textHandle = Pdfium.wasmExports.FPDFPageObj_CreateTextObj(docHandle, fontHandle, fontSize); + if (!textHandle) { + const error = Pdfium.wasmExports.FPDF_GetLastError(); + throw new Error(`FPDFPageObj_CreateTextObj failed (${_getErrorMessage(error)})`); + } + + if (!Pdfium.wasmExports.FPDFText_SetText(textHandle, textUtf16)) { + const error = Pdfium.wasmExports.FPDF_GetLastError(); + throw new Error(`FPDFText_SetText failed (${_getErrorMessage(error)})`); + } + + const a = (textColor >> 24) & 0xFF; + const r = (textColor >> 16) & 0xFF; + const g = (textColor >> 8) & 0xFF; + const b = textColor & 0xFF; + + if (Pdfium.wasmExports.FPDFPageObj_SetFillColor(textHandle, r, g, b, a) == 0) { + throw PdfException('FPDFPageObj_SetFillColor failed.'); + } + + + let ax, ay; + + const boundsSize = 64; + const boundsWrite = Pdfium.wasmExports.malloc(boundsSize); + try { + if (Pdfium.wasmExports.FPDFPageObj_GetBounds(textHandle, boundsWrite, boundsWrite + 4, boundsWrite + 8, boundsWrite + 12) == 0) { + throw PdfException('could not determine text bounds'); + } + const boundsView = new Float32Array(Pdfium.memory.buffer, boundsWrite, 4); + + ax = (boundsView[2] - boundsView[0]) * anchorX; + ay = (boundsView[3] - boundsView[1]) * anchorY; + } finally { + Pdfium.wasmExports.free(boundsWrite); + } + + const radians = rotation * (Math.PI / 180.0); + const cosR = Math.cos(radians); + const sinR = Math.sin(radians); + + Pdfium.wasmExports.FPDFPageObj_Transform( + textHandle, + cosR, + sinR, + -sinR, + cosR, + x - ax * cosR + ay * sinR, + y - ax * sinR - ay * cosR, + ); + + Pdfium.wasmExports.FPDFPage_InsertObject(pageHandle, textHandle); + if (!Pdfium.wasmExports.FPDFPage_GenerateContent(pageHandle)) { + const error = Pdfium.wasmExports.FPDF_GetLastError(); + throw new Error(`FPDFPage_InsertObject failed (${_getErrorMessage(error)})`); + } + } finally { + if (fontHandle) { + Pdfium.wasmExports.FPDFFont_Close(fontHandle); + } + Pdfium.wasmExports.FPDF_ClosePage(pageHandle); + StringUtils.freeUTF8(fontNameUtf8); + StringUtils.freeUTF16(textUtf16); + } + return { message: 'Text inserted' }; +} + /** * Functions that can be called from the main thread */ @@ -2138,6 +2251,7 @@ const functions = { clearAllFontData, assemble, encodePdf, + insertText, }; /** @@ -2418,6 +2532,21 @@ class StringUtils { this.stringToUtf8Bytes(str, new Uint8Array(Pdfium.memory.buffer, ptr, size)); return ptr; } + /** + * Allocate memory for UTF-16 string + * @param {string} str + * @returns {number} Pointer to allocated buffer that contains UTF-16 string. The buffer should be released by calling [freeUTF16]. + */ + static allocateUTF16(str) { + if (str == null) return 0; + + const size = str.length * 2 + 1; + const ptr = Pdfium.wasmExports.malloc(size); + const view = new DataView(Pdfium.memory.buffer, ptr, size); + for (let i = 0; i < str.length; i++) view.setUint16(i * 2, str.charCodeAt(i), true); + view.setUint8(str.length * 2, 0); + return ptr; + } /** * Release memory allocated for UTF-8 string * @param {number} ptr Pointer to allocated buffer @@ -2425,4 +2554,11 @@ class StringUtils { static freeUTF8(ptr) { Pdfium.wasmExports.free(ptr); } + /** + * Release memory allocated for UTF-16 string + * @param {number} ptr Pointer to allocated buffer + */ + static freeUTF16(ptr) { + Pdfium.wasmExports.free(ptr); + } } diff --git a/packages/pdfrx/lib/src/wasm/pdfrx_wasm.dart b/packages/pdfrx/lib/src/wasm/pdfrx_wasm.dart index 326c1616..113b4d57 100644 --- a/packages/pdfrx/lib/src/wasm/pdfrx_wasm.dart +++ b/packages/pdfrx/lib/src/wasm/pdfrx_wasm.dart @@ -853,6 +853,38 @@ class _PdfPageWasm extends PdfPage { return PdfImageWeb(width: width, height: height, pixels: pixels); } + + @override + Future insertText({ + required String text, + required double fontSize, + double x = 0, + double y = 0, + double anchorX = 0.5, + double anchorY = 0.5, + double rotation = 0, + int textColor = 0xFF000000, + String fontName = 'Helvetica', + }) async { + if (document.isDisposed) return; + + await _sendCommand( + 'insertText', + parameters: { + 'docHandle': document.document['docHandle'], + 'pageIndex': pageNumber - 1, + 'text': text, + 'fontSize': fontSize, + 'x': x, + 'y': y, + 'anchorX': anchorX, + 'anchorY': anchorY, + 'rotation': rotation, + 'textColor': textColor, + 'fontName': fontName, + }, + ); + } } class PdfImageWeb extends PdfImage { diff --git a/packages/pdfrx_coregraphics/lib/pdfrx_coregraphics.dart b/packages/pdfrx_coregraphics/lib/pdfrx_coregraphics.dart index ffe78c3e..c93afe9c 100644 --- a/packages/pdfrx_coregraphics/lib/pdfrx_coregraphics.dart +++ b/packages/pdfrx_coregraphics/lib/pdfrx_coregraphics.dart @@ -759,6 +759,19 @@ class _CoreGraphicsPdfPage extends PdfPage { return const []; } } + + @override + Future insertText({ + required String text, + required double fontSize, + double x = 0, + double y = 0, + double anchorX = 0.5, + double anchorY = 0.5, + double rotation = 0, + int textColor = 0xFF000000, + String fontName = 'Helvetica', + }) => throw PdfException('insertText not implemented for CoreGraphics'); } class _CoreGraphicsPdfImage implements PdfImage { diff --git a/packages/pdfrx_engine/lib/src/native/pdfrx_pdfium.dart b/packages/pdfrx_engine/lib/src/native/pdfrx_pdfium.dart index af1a420b..137c649c 100644 --- a/packages/pdfrx_engine/lib/src/native/pdfrx_pdfium.dart +++ b/packages/pdfrx_engine/lib/src/native/pdfrx_pdfium.dart @@ -1290,6 +1290,86 @@ class _PdfPagePdfium extends PdfPage { @override PdfPageRenderCancellationTokenPdfium createCancellationToken() => PdfPageRenderCancellationTokenPdfium(this); + @override + Future insertText({ + required String text, + required double fontSize, + double x = 0, + double y = 0, + double anchorX = 0.5, + double anchorY = 0.5, + double rotation = 0, + int textColor = 0xFF000000, + String fontName = 'Helvetica', + }) async { + if (document.isDisposed || !isLoaded) return; + return await BackgroundWorker.computeWithArena((arena, params) { + final doc = pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.docHandle); + final page = pdfium.FPDF_LoadPage(doc, params.pageNumber - 1); + + try { + final font = pdfium.FPDFText_LoadStandardFont(doc, fontName.toUtf8(arena)); + if (font == nullptr) { + throw PdfException('FPDFText_LoadStandardFont failed.'); + } + try { + final textObj = pdfium.FPDFPageObj_CreateTextObj(doc, font, fontSize); + if (textObj == nullptr) { + throw PdfException('FPDFPageObj_CreateTextObj failed.'); + } + + if (pdfium.FPDFText_SetText(textObj, text.toNativeUtf16(allocator: arena).cast()) == 0) { + throw PdfException('FPDFText_SetText failed.'); + } + + final a = (textColor >> 24) & 0xFF; + final r = (textColor >> 16) & 0xFF; + final g = (textColor >> 8) & 0xFF; + final b = textColor & 0xFF; + + if (pdfium.FPDFPageObj_SetFillColor(textObj, r, g, b, a) == 0) { + throw PdfException('FPDFPageObj_SetFillColor failed.'); + } + + final left = arena(); + final bottom = arena(); + final right = arena(); + final top = arena(); + + if (pdfium.FPDFPageObj_GetBounds(textObj, left, bottom, right, top) == 0) { + throw PdfException('could not determine text bounds'); + } + + final ax = (right.value - left.value) * anchorX; + final ay = (top.value - bottom.value) * anchorY; + + final radians = rotation * (pi / 180.0); + final cosR = cos(radians); + final sinR = sin(radians); + + pdfium.FPDFPageObj_Transform( + textObj, + cosR, + sinR, + -sinR, + cosR, + x - ax * cosR + ay * sinR, + y - ax * sinR - ay * cosR, + ); + + pdfium.FPDFPage_InsertObject(page, textObj); + if (pdfium.FPDFPage_GenerateContent(page) == 0) { + throw PdfException('FPDFPage_GenerateContent failed.'); + } + } finally { + pdfium.FPDFFont_Close(font); + } + } finally { + pdfium.FPDF_ClosePage(page); + } + }, (docHandle: document.document.address, pageNumber: pageNumber, bbLeft: bbLeft, bbBottom: bbBottom)); + } + @override Future loadText() async { if (document.isDisposed || !isLoaded) return null; diff --git a/packages/pdfrx_engine/lib/src/pdf_page.dart b/packages/pdfrx_engine/lib/src/pdf_page.dart index 92714722..b9fd93dc 100644 --- a/packages/pdfrx_engine/lib/src/pdf_page.dart +++ b/packages/pdfrx_engine/lib/src/pdf_page.dart @@ -89,6 +89,26 @@ abstract class PdfPage { /// If the page is not loaded yet (progressive loading case only), this function returns null. Future loadText(); + /// Insert a text element + /// + /// [text] is the text to insert + /// [textColor] is `AARRGGBB` integer color notation + /// [fontSize] is the font size + /// [rotation] is the rotation in degrees + /// [anchorX] and [anchorY] are the anchor point position in the range 0..1 + /// [x] and [y] are the position of the anchor point in page coordinates + Future insertText({ + required String text, + required double fontSize, + double x = 0, + double y = 0, + double anchorX = 0.5, + double anchorY = 0.5, + double rotation = 0, + int textColor = 0xFF000000, + String fontName = 'Helvetica', + }); + /// Load links. /// /// If [compact] is true, it tries to reduce memory usage by compacting the link data. diff --git a/packages/pdfrx_engine/lib/src/pdf_page_proxies.dart b/packages/pdfrx_engine/lib/src/pdf_page_proxies.dart index eb0fa5d6..e8ba1649 100644 --- a/packages/pdfrx_engine/lib/src/pdf_page_proxies.dart +++ b/packages/pdfrx_engine/lib/src/pdf_page_proxies.dart @@ -103,6 +103,29 @@ class PdfPageRenumbered implements PdfPageProxy { cancellationToken: cancellationToken, rotationOverride: rotationOverride, ); + + @override + Future insertText({ + required String text, + required double fontSize, + double x = 0, + double y = 0, + double anchorX = 0.5, + double anchorY = 0.5, + double rotation = 0, + int textColor = 0xFF000000, + String fontName = 'Helvetica', + }) => basePage.insertText( + text: text, + x: x, + y: y, + anchorX: anchorX, + anchorY: anchorY, + rotation: rotation, + textColor: textColor, + fontSize: fontSize, + fontName: fontName, + ); } /// PDF page wrapper that applies an absolute rotation to the base page. @@ -179,4 +202,27 @@ class PdfPageRotated implements PdfPageProxy { @override Future> loadLinks({bool compact = false, bool enableAutoLinkDetection = true}) => basePage.loadLinks(compact: compact, enableAutoLinkDetection: enableAutoLinkDetection); + + @override + Future insertText({ + required String text, + required double fontSize, + double x = 0, + double y = 0, + double anchorX = 0.5, + double anchorY = 0.5, + double rotation = 0, + int textColor = 0xFF000000, + String fontName = 'Helvetica', + }) => basePage.insertText( + text: text, + x: x, + y: y, + anchorX: anchorX, + anchorY: anchorY, + rotation: rotation, + textColor: textColor, + fontSize: fontSize, + fontName: fontName, + ); }