diff --git a/.changeset/weak-pugs-hear.md b/.changeset/weak-pugs-hear.md new file mode 100644 index 000000000..cefa200de --- /dev/null +++ b/.changeset/weak-pugs-hear.md @@ -0,0 +1,25 @@ +--- +'@embedpdf/engines': minor +'@embedpdf/models': minor +'@embedpdf/plugin-render': minor +--- + +Add optional **form widget rendering** to the render pipeline. + +### What changed + +- **@embedpdf/models** + + - `PdfRenderPageOptions` now supports `withForms?: boolean` to request drawing interactive form widgets. + +- **@embedpdf/engines** + + - `PdfiumEngine.renderPage` and `renderPageRect` honor `withForms`. + When enabled, the engine initializes the page form handle and calls `FPDF_FFLDraw` with the correct device transform. + - New helper `computeFormDrawParams(matrix, rect, pageSize, rotation)` calculates start offsets and sizes for `FPDF_FFLDraw`. + +- **@embedpdf/plugin-render** + - New plugin config flags: + - `withForms?: boolean` (default `false`) + - `withAnnotations?: boolean` (default `false`) + - The plugin merges per-call options with plugin defaults so callers can set once at init or override per call. diff --git a/packages/engines/src/lib/pdfium/engine.ts b/packages/engines/src/lib/pdfium/engine.ts index a9c4cee17..452117f11 100644 --- a/packages/engines/src/lib/pdfium/engine.ts +++ b/packages/engines/src/lib/pdfium/engine.ts @@ -24,6 +24,7 @@ import { PdfWidgetAnnoOption, PdfFileAttachmentAnnoObject, Rect, + Size, PdfAttachmentObject, PdfUnsupportedAnnoObject, PdfTextAnnoObject, @@ -114,13 +115,14 @@ import { PdfAnnotationsProgress, ConvertToBlobOptions, buildUserToDeviceMatrix, + Matrix, PdfMetadataObject, PdfPrintOptions, PdfTrappedStatus, PdfStampFit, PdfAddAttachmentParams, } from '@embedpdf/models'; -import { isValidCustomKey, readArrayBuffer, readString } from './helper'; +import { computeFormDrawParams, isValidCustomKey, readArrayBuffer, readString } from './helper'; import { WrappedPdfiumModule } from '@embedpdf/pdfium'; import { DocumentContext, PageContext, PdfCache } from './cache'; import { ImageDataConverter, LazyImageData } from '../converters/types'; @@ -6956,6 +6958,8 @@ export class PdfiumEngine implements PdfEngine { const bytes = stride * hDev; const pageCtx = ctx.acquirePage(page.index); + const shouldRenderForms = options?.withForms ?? false; + const formHandle = shouldRenderForms ? pageCtx.getFormHandle() : undefined; // ---- 2) allocate a BGRA bitmap in WASM const heapPtr = this.memoryManager.malloc(bytes); @@ -6992,6 +6996,24 @@ export class PdfiumEngine implements PdfEngine { clipPtr, flags, ); + + if (formHandle !== undefined) { + const formParams = computeFormDrawParams(M, rect, page.size, rotation); + const { startX, startY, formsWidth, formsHeight, scaleX, scaleY } = formParams; + + // Draw form elements using the same effective transform as the page bitmap. + this.pdfiumModule.FPDF_FFLDraw( + formHandle, + bitmapPtr, + pageCtx.pagePtr, + startX, + startY, + formsWidth, + formsHeight, + rotation, + flags, + ); + } } finally { pageCtx.release(); this.memoryManager.free(mPtr); diff --git a/packages/engines/src/lib/pdfium/helper.ts b/packages/engines/src/lib/pdfium/helper.ts index b2056572a..fbaa616c9 100644 --- a/packages/engines/src/lib/pdfium/helper.ts +++ b/packages/engines/src/lib/pdfium/helper.ts @@ -1,3 +1,4 @@ +import { Matrix, Rotation, Rect, Size } from '@embedpdf/models'; import { PdfiumRuntimeMethods, PdfiumModule } from '@embedpdf/pdfium'; /** @@ -92,3 +93,65 @@ export function isValidCustomKey(key: string): boolean { } return true; } + +interface FormDrawParams { + startX: number; + startY: number; + formsWidth: number; + formsHeight: number; + scaleX: number; + scaleY: number; +} + +export function computeFormDrawParams( + matrix: Matrix, + rect: Rect, + pageSize: Size, + rotation: Rotation, +): FormDrawParams { + const rectLeft = rect.origin.x; + const rectBottom = rect.origin.y; + const rectRight = rectLeft + rect.size.width; + const rectTop = rectBottom + rect.size.height; + const pageWidth = pageSize.width; + const pageHeight = pageSize.height; + + // Extract the per-axis scale that the render matrix applies. + const scaleX = Math.hypot(matrix.a, matrix.b); + const scaleY = Math.hypot(matrix.c, matrix.d); + const swap = (rotation & 1) === 1; + + const formsWidth = swap + ? Math.max(1, Math.round(pageHeight * scaleX)) + : Math.max(1, Math.round(pageWidth * scaleX)); + const formsHeight = swap + ? Math.max(1, Math.round(pageWidth * scaleY)) + : Math.max(1, Math.round(pageHeight * scaleY)); + + let startX: number; + let startY: number; + switch (rotation) { + case Rotation.Degree0: + startX = -Math.round(rectLeft * scaleX); + startY = -Math.round(rectBottom * scaleY); + break; + case Rotation.Degree90: + startX = Math.round((rectTop - pageHeight) * scaleX); + startY = -Math.round(rectLeft * scaleY); + break; + case Rotation.Degree180: + startX = Math.round((rectRight - pageWidth) * scaleX); + startY = Math.round((rectTop - pageHeight) * scaleY); + break; + case Rotation.Degree270: + startX = -Math.round(rectBottom * scaleX); + startY = Math.round((rectRight - pageWidth) * scaleY); + break; + default: + startX = -Math.round(rectLeft * scaleX); + startY = -Math.round(rectBottom * scaleY); + break; + } + + return { startX, startY, formsWidth, formsHeight, scaleX, scaleY }; +} diff --git a/packages/models/src/pdf.ts b/packages/models/src/pdf.ts index 7dec7988e..108bf9263 100644 --- a/packages/models/src/pdf.ts +++ b/packages/models/src/pdf.ts @@ -2512,6 +2512,10 @@ export interface PdfRenderPageOptions extends PdfRenderOptions { * Whether to render annotations */ withAnnotations?: boolean; + /** + * Whether to render interactive form widgets + */ + withForms?: boolean; } export interface PdfRenderPageAnnotationOptions extends PdfRenderOptions { diff --git a/packages/plugin-render/src/lib/render-plugin.ts b/packages/plugin-render/src/lib/render-plugin.ts index e8e466637..266262432 100644 --- a/packages/plugin-render/src/lib/render-plugin.ts +++ b/packages/plugin-render/src/lib/render-plugin.ts @@ -16,6 +16,8 @@ export class RenderPlugin extends BasePlugin(); + private withForms = false; + private withAnnotations = false; constructor(id: string, registry: PluginRegistry) { super(id, registry); @@ -25,7 +27,10 @@ export class RenderPlugin extends BasePlugin {} + async initialize(config: RenderPluginConfig): Promise { + this.withForms = config.withForms ?? false; + this.withAnnotations = config.withAnnotations ?? false; + } protected buildCapability(): RenderCapability { return { @@ -50,7 +55,13 @@ export class RenderPlugin extends BasePlugin