From 83169ed0f4a9912918af22c75d4d604bf01a4e2a Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:35:35 +0000 Subject: [PATCH] Move Forms location (#5769) # Description of Changes --- ## 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. --- .../common}/util/FormFieldTypeSupport.java | 2 +- .../software/common}/util/FormUtils.java | 5 +- .../software/common}/util/FormUtilsTest.java | 2 +- .../api/form/FormFillController.java | 4 +- .../api/form/FormPayloadParser.java | 4 +- .../core/data/useTranslatedToolRegistry.tsx | 14 + .../tools/formFill/FieldInput.tsx | 4 +- .../core/tools/formFill/FormFieldOverlay.tsx | 469 +++++++++++++++- .../core/tools/formFill/FormFieldSidebar.tsx | 205 ++++++- .../tools/formFill/FormFill.module.css | 0 .../tools/formFill/FormFill.tsx | 10 +- .../core/tools/formFill/FormFillContext.tsx | 530 ++++++++++++++++-- .../src/core/tools/formFill/FormSaveBar.tsx | 179 +++++- .../tools/formFill/fieldMeta.tsx | 2 +- .../tools/formFill/formApi.ts | 2 +- frontend/src/core/tools/formFill/index.ts | 11 + .../formFill/providers/PdfBoxFormProvider.ts | 6 +- .../formFill/providers/PdfLibFormProvider.ts | 4 +- .../core/tools/formFill/providers/index.ts | 3 + .../tools/formFill/providers/types.ts | 2 +- .../tools/formFill/types.ts | 0 frontend/src/core/types/toolId.ts | 1 + .../data/useProprietaryToolRegistry.tsx | 23 +- .../tools/formFill/FormFieldOverlay.tsx | 473 ---------------- .../tools/formFill/FormFieldSidebar.tsx | 210 ------- .../tools/formFill/FormFillContext.tsx | 507 ----------------- .../tools/formFill/FormSaveBar.tsx | 186 ------ .../src/proprietary/tools/formFill/index.ts | 11 - .../tools/formFill/providers/index.ts | 3 - .../proprietary/types/proprietaryToolId.ts | 1 - 30 files changed, 1362 insertions(+), 1511 deletions(-) rename app/{proprietary/src/main/java/stirling/software/proprietary => common/src/main/java/stirling/software/common}/util/FormFieldTypeSupport.java (99%) rename app/{proprietary/src/main/java/stirling/software/proprietary => common/src/main/java/stirling/software/common}/util/FormUtils.java (99%) rename app/{proprietary/src/test/java/stirling/software/proprietary => common/src/test/java/stirling/software/common}/util/FormUtilsTest.java (99%) rename app/{proprietary/src/main/java/stirling/software/proprietary => core/src/main/java/stirling/software/SPDF}/controller/api/form/FormFillController.java (99%) rename app/{proprietary/src/main/java/stirling/software/proprietary => core/src/main/java/stirling/software/SPDF}/controller/api/form/FormPayloadParser.java (98%) rename frontend/src/{proprietary => core}/tools/formFill/FieldInput.tsx (97%) rename frontend/src/{proprietary => core}/tools/formFill/FormFill.module.css (100%) rename frontend/src/{proprietary => core}/tools/formFill/FormFill.tsx (98%) rename frontend/src/{proprietary => core}/tools/formFill/fieldMeta.tsx (94%) rename frontend/src/{proprietary => core}/tools/formFill/formApi.ts (94%) create mode 100644 frontend/src/core/tools/formFill/index.ts rename frontend/src/{proprietary => core}/tools/formFill/providers/PdfBoxFormProvider.ts (80%) rename frontend/src/{proprietary => core}/tools/formFill/providers/PdfLibFormProvider.ts (99%) create mode 100644 frontend/src/core/tools/formFill/providers/index.ts rename frontend/src/{proprietary => core}/tools/formFill/providers/types.ts (95%) rename frontend/src/{proprietary => core}/tools/formFill/types.ts (100%) delete mode 100644 frontend/src/proprietary/tools/formFill/FormFieldOverlay.tsx delete mode 100644 frontend/src/proprietary/tools/formFill/FormFieldSidebar.tsx delete mode 100644 frontend/src/proprietary/tools/formFill/FormFillContext.tsx delete mode 100644 frontend/src/proprietary/tools/formFill/FormSaveBar.tsx delete mode 100644 frontend/src/proprietary/tools/formFill/index.ts delete mode 100644 frontend/src/proprietary/tools/formFill/providers/index.ts diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/util/FormFieldTypeSupport.java b/app/common/src/main/java/stirling/software/common/util/FormFieldTypeSupport.java similarity index 99% rename from app/proprietary/src/main/java/stirling/software/proprietary/util/FormFieldTypeSupport.java rename to app/common/src/main/java/stirling/software/common/util/FormFieldTypeSupport.java index 4f1c1e0d8..3018a26fd 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/util/FormFieldTypeSupport.java +++ b/app/common/src/main/java/stirling/software/common/util/FormFieldTypeSupport.java @@ -1,4 +1,4 @@ -package stirling.software.proprietary.util; +package stirling.software.common.util; import java.io.IOException; import java.util.Arrays; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/util/FormUtils.java b/app/common/src/main/java/stirling/software/common/util/FormUtils.java similarity index 99% rename from app/proprietary/src/main/java/stirling/software/proprietary/util/FormUtils.java rename to app/common/src/main/java/stirling/software/common/util/FormUtils.java index b5d412f98..fa8020a23 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/util/FormUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/FormUtils.java @@ -1,4 +1,4 @@ -package stirling.software.proprietary.util; +package stirling.software.common.util; import java.awt.image.BufferedImage; import java.io.IOException; @@ -49,9 +49,6 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.model.FormFieldWithCoordinates; -import stirling.software.common.util.ApplicationContextProvider; -import stirling.software.common.util.ExceptionUtils; -import stirling.software.common.util.RegexPatternUtils; @Slf4j @UtilityClass diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/util/FormUtilsTest.java b/app/common/src/test/java/stirling/software/common/util/FormUtilsTest.java similarity index 99% rename from app/proprietary/src/test/java/stirling/software/proprietary/util/FormUtilsTest.java rename to app/common/src/test/java/stirling/software/common/util/FormUtilsTest.java index 6416e82bf..592402b88 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/util/FormUtilsTest.java +++ b/app/common/src/test/java/stirling/software/common/util/FormUtilsTest.java @@ -1,4 +1,4 @@ -package stirling.software.proprietary.util; +package stirling.software.common.util; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/form/FormFillController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/form/FormFillController.java similarity index 99% rename from app/proprietary/src/main/java/stirling/software/proprietary/controller/api/form/FormFillController.java rename to app/core/src/main/java/stirling/software/SPDF/controller/api/form/FormFillController.java index 1b584af2e..ce8f77faa 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/form/FormFillController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/form/FormFillController.java @@ -1,4 +1,4 @@ -package stirling.software.proprietary.controller.api.form; +package stirling.software.SPDF.controller.api.form; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -30,8 +30,8 @@ import lombok.RequiredArgsConstructor; import stirling.software.common.model.FormFieldWithCoordinates; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.FormUtils; import stirling.software.common.util.WebResponseUtils; -import stirling.software.proprietary.util.FormUtils; @RestController @RequestMapping("/api/v1/form") diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/form/FormPayloadParser.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/form/FormPayloadParser.java similarity index 98% rename from app/proprietary/src/main/java/stirling/software/proprietary/controller/api/form/FormPayloadParser.java rename to app/core/src/main/java/stirling/software/SPDF/controller/api/form/FormPayloadParser.java index 2efffb21d..f9cd97811 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/form/FormPayloadParser.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/form/FormPayloadParser.java @@ -1,4 +1,4 @@ -package stirling.software.proprietary.controller.api.form; +package stirling.software.SPDF.controller.api.form; import java.io.IOException; import java.util.ArrayList; @@ -13,7 +13,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import stirling.software.common.util.ExceptionUtils; -import stirling.software.proprietary.util.FormUtils; +import stirling.software.common.util.FormUtils; final class FormPayloadParser { diff --git a/frontend/src/core/data/useTranslatedToolRegistry.tsx b/frontend/src/core/data/useTranslatedToolRegistry.tsx index 0bfb6c9f1..f055f9c2c 100644 --- a/frontend/src/core/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/core/data/useTranslatedToolRegistry.tsx @@ -39,6 +39,7 @@ import AutoRename from "@app/tools/AutoRename"; import SingleLargePage from "@app/tools/SingleLargePage"; import PageLayout from "@app/tools/PageLayout"; import UnlockPdfForms from "@app/tools/UnlockPdfForms"; +import FormFill from "@app/tools/formFill/FormFill"; import RemoveCertificateSign from "@app/tools/RemoveCertificateSign"; import RemoveImage from "@app/tools/RemoveImage"; import CertSign from "@app/tools/CertSign"; @@ -346,6 +347,19 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { synonyms: getSynonyms(t, "unlockPDFForms"), automationSettings: null }, + formFill: { + icon: , + name: t('home.formFill.title', 'Fill Form'), + component: FormFill, + description: t('home.formFill.desc', 'Fill PDF form fields interactively with a visual editor'), + categoryId: ToolCategoryId.STANDARD_TOOLS, + subcategoryId: SubcategoryId.GENERAL, + workbench: 'viewer' as const, + endpoints: ['form-fill'], + automationSettings: null, + supportsAutomate: false, + synonyms: ['form', 'fill', 'fillable', 'input', 'field', 'acroform'], + }, changePermissions: { icon: , name: t("home.changePermissions.title", "Change Permissions"), diff --git a/frontend/src/proprietary/tools/formFill/FieldInput.tsx b/frontend/src/core/tools/formFill/FieldInput.tsx similarity index 97% rename from frontend/src/proprietary/tools/formFill/FieldInput.tsx rename to frontend/src/core/tools/formFill/FieldInput.tsx index a2355ec39..d310a63bc 100644 --- a/frontend/src/proprietary/tools/formFill/FieldInput.tsx +++ b/frontend/src/core/tools/formFill/FieldInput.tsx @@ -15,8 +15,8 @@ import { MultiSelect, Stack, } from '@mantine/core'; -import { useFieldValue } from '@proprietary/tools/formFill/FormFillContext'; -import type { FormField } from '@proprietary/tools/formFill/types'; +import { useFieldValue } from '@app/tools/formFill/FormFillContext'; +import type { FormField } from '@app/tools/formFill/types'; function FieldInputInner({ field, diff --git a/frontend/src/core/tools/formFill/FormFieldOverlay.tsx b/frontend/src/core/tools/formFill/FormFieldOverlay.tsx index 9f673a003..22698b905 100644 --- a/frontend/src/core/tools/formFill/FormFieldOverlay.tsx +++ b/frontend/src/core/tools/formFill/FormFieldOverlay.tsx @@ -1,20 +1,473 @@ /** - * FormFieldOverlay stub for the core build. - * This file is overridden in src/proprietary/tools/formFill/FormFieldOverlay.tsx - * when building the proprietary variant. + * FormFieldOverlay — Renders interactive HTML form widgets on top of a PDF page. + * + * This layer is placed inside the renderPage callback of the EmbedPDF Scroller, + * similar to how AnnotationLayer, RedactionLayer, and LinkLayer work. + * + * It reads the form field coordinates (in un-rotated CSS space, top-left origin) + * and scales them using the document scale from EmbedPDF. + * + * Each widget renders an appropriate HTML input (text, checkbox, dropdown, etc.) + * that synchronises bidirectionally with FormFillContext values. + * + * Coordinate handling: + * Both providers (PdfLibFormProvider and PdfBoxFormProvider) output widget + * coordinates in un-rotated PDF space (y-flipped to CSS upper-left origin). + * The component (which wraps this overlay along with page tiles) + * handles visual rotation via CSS transforms — same as TilingLayer, + * AnnotationLayer, and LinkLayer. */ +import React, { useCallback, useMemo, memo } from 'react'; +import { useDocumentState } from '@embedpdf/core/react'; +import { useFormFill, useFieldValue } from '@app/tools/formFill/FormFillContext'; +import type { FormField, WidgetCoordinates } from '@app/tools/formFill/types'; + +interface WidgetInputProps { + field: FormField; + widget: WidgetCoordinates; + isActive: boolean; + error?: string; + scaleX: number; + scaleY: number; + onFocus: (fieldName: string) => void; + onChange: (fieldName: string, value: string) => void; +} + +/** + * WidgetInput subscribes to its own field value via useSyncExternalStore, + * so it only re-renders when its specific value changes — not when ANY + * form value in the entire document changes. + */ +function WidgetInputInner({ + field, + widget, + isActive, + error, + scaleX, + scaleY, + onFocus, + onChange, +}: WidgetInputProps) { + // Per-field value subscription — only this widget re-renders when its value changes + const value = useFieldValue(field.name); + + // Coordinates are in visual CSS space (top-left origin). + // Multiply by per-axis scale to get rendered pixel coordinates. + const left = widget.x * scaleX; + const top = widget.y * scaleY; + const width = widget.width * scaleX; + const height = widget.height * scaleY; + + const borderColor = error ? '#f44336' : (isActive ? '#2196F3' : 'rgba(33, 150, 243, 0.4)'); + const bgColor = error + ? '#FFEBEE' // Red 50 (Opaque) + : (isActive ? '#E3F2FD' : '#FFFFFF'); // Blue 50 (Opaque) : White (Opaque) + + const commonStyle: React.CSSProperties = { + position: 'absolute', + left, + top, + width, + height, + zIndex: 10, + boxSizing: 'border-box', + border: `1px solid ${borderColor}`, + borderRadius: 1, + background: isActive ? bgColor : 'transparent', + transition: 'border-color 0.15s, background 0.15s, box-shadow 0.15s', + boxShadow: + isActive && field.type !== 'radio' && field.type !== 'checkbox' + ? `0 0 0 2px ${error ? 'rgba(244, 67, 54, 0.25)' : 'rgba(33, 150, 243, 0.25)'}` + : 'none', + cursor: field.readOnly ? 'default' : 'text', + pointerEvents: 'auto', + display: 'flex', + alignItems: field.multiline ? 'stretch' : 'center', + }; + + const stopPropagation = (e: React.SyntheticEvent) => { + e.stopPropagation(); + // Also stop immediate propagation to native listeners to block non-React subscribers + if (e.nativeEvent) { + e.nativeEvent.stopImmediatePropagation?.(); + } + }; + + const commonProps = { + style: commonStyle, + onPointerDown: stopPropagation, + onPointerUp: stopPropagation, + onMouseDown: stopPropagation, + onMouseUp: stopPropagation, + onClick: stopPropagation, + onDoubleClick: stopPropagation, + onKeyDown: stopPropagation, + onKeyUp: stopPropagation, + onKeyPress: stopPropagation, + onDragStart: stopPropagation, + onSelect: stopPropagation, + onContextMenu: stopPropagation, + }; + + const captureStopProps = { + onPointerDownCapture: stopPropagation, + onPointerUpCapture: stopPropagation, + onMouseDownCapture: stopPropagation, + onMouseUpCapture: stopPropagation, + onClickCapture: stopPropagation, + onKeyDownCapture: stopPropagation, + onKeyUpCapture: stopPropagation, + onKeyPressCapture: stopPropagation, + }; + + const fontSize = widget.fontSize + ? widget.fontSize * scaleY + : field.multiline + ? Math.max(6, Math.min(height * 0.60, 14)) + : Math.max(6, height * 0.65); + + const inputBaseStyle: React.CSSProperties = { + width: '100%', + height: '100%', + border: 'none', + outline: 'none', + background: 'transparent', + padding: 0, + paddingLeft: `${Math.max(2, 4 * scaleX)}px`, + paddingRight: `${Math.max(2, 4 * scaleX)}px`, + fontSize: `${fontSize}px`, + fontFamily: 'Helvetica, Arial, sans-serif', + color: '#000', + boxSizing: 'border-box', + lineHeight: 'normal', + }; + + const handleFocus = () => onFocus(field.name); + + switch (field.type) { + case 'text': + return ( +
+ {field.multiline ? ( +