done React: form field viewing (Widget annotations) with saving issues fixed

This commit is contained in:
azeeee05
2026-06-10 19:29:11 +05:30
parent 58b39b6780
commit d2c429f047
6 changed files with 103 additions and 2 deletions
+43
View File
@@ -1695,6 +1695,49 @@ std::expected<void, EngineError> PdfiumDocument::applyEdits(const std::string& e
}
FPDF_ClosePage(page);
} else if (type == "update_field") {
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("update_field operation missing 'data' object");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
std::string value;
if (data["value"].is_boolean()) {
value = data["value"].get<bool>() ? "Yes" : "Off";
} else if (data["value"].is_string()) {
value = data["value"].get<std::string>();
} else {
spdlog::error("update_field value must be a string or boolean");
return std::unexpected(EngineError::InvalidFormat);
}
std::string id = op.value("id", "");
int annotIndex = -1;
size_t lastUnderscore = id.find_last_of('_');
if (lastUnderscore != std::string::npos) {
try {
annotIndex = std::stoi(id.substr(lastUnderscore + 1));
} catch (...) {
annotIndex = -1;
}
}
if (annotIndex >= 0) {
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for field update", pageIndex);
return std::unexpected(EngineError::Unknown);
}
FPDF_ANNOTATION annot = FPDFPage_GetAnnot(page, annotIndex);
if (annot) {
auto utf16 = utf8_to_utf16le(value);
FPDFAnnot_SetStringValue(annot, "V", reinterpret_cast<FPDF_WIDESTRING>(utf16.data()));
FPDFPage_GenerateContent(page);
FPDFPage_CloseAnnot(annot);
}
FPDF_ClosePage(page);
}
} else if (type == "image_overlay") {
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("image_overlay operation missing 'data' object");
+8
View File
@@ -480,6 +480,14 @@ function App() {
hasSignature={!!pendingSignature}
activeStamp={activeStamp?.label ?? null}
annotations={annotations}
onFieldChange={(id, value, i) => {
applyOps([{
id,
type: 'update_field',
pageIndex: i,
data: { value }
} as any]);
}}
searchQuery={searchQuery}
searchResults={searchResults}
searchCurrentMatch={searchCurrentMatch}
+9
View File
@@ -29,6 +29,7 @@ interface AnnotationLayerProps {
zoom: number;
annotations: Annotation[];
onAnnotationClick?: (annotation: Annotation) => void;
onFieldChange?: (id: string, value: string | boolean, pageIndex: number) => void;
}
export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
@@ -38,6 +39,7 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
zoom,
annotations,
onAnnotationClick,
onFieldChange,
}) => {
return (
<div
@@ -101,6 +103,11 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
<input
type="text"
defaultValue={anno.fieldValue || ''}
onBlur={(e) => {
if (e.target.value !== (anno.fieldValue || '')) {
onFieldChange?.(anno.id, e.target.value, pageIndex);
}
}}
className="w-full h-full bg-blue-50/50 border border-blue-400/50 text-sm px-1 focus:outline-none focus:ring-1 focus:ring-blue-500 rounded-sm"
/>
)}
@@ -109,12 +116,14 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
<input
type={(anno.fieldFlags ?? 0) & 32768 ? 'radio' : 'checkbox'}
defaultChecked={anno.fieldValue === 'Yes' || anno.fieldValue === 'On'}
onChange={(e) => onFieldChange?.(anno.id, e.target.checked, pageIndex)}
className="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
/>
)}
{anno.fieldType === 'Ch' && (
<select
defaultValue={anno.fieldValue || ''}
onChange={(e) => onFieldChange?.(anno.id, e.target.value, pageIndex)}
className="w-full h-full bg-blue-50/50 border border-blue-400/50 text-sm px-1 rounded-sm appearance-none"
>
{anno.fieldOptions?.map((opt: string, i: number) => (
+3
View File
@@ -33,6 +33,7 @@ interface PDFViewerProps {
onPlaceText?: (pageIndex: number, rectPts: Rect, text: string) => void;
onPlaceStamp?: (pageIndex: number, pointPts: { x: number; y: number }) => void;
onPlaceSignature?: (pageIndex: number, pointPts: { x: number; y: number }) => void;
onFieldChange?: (id: string, value: string | boolean, pageIndex: number) => void;
}
interface PageLayout {
@@ -69,6 +70,7 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
onPlaceText,
onPlaceStamp,
onPlaceSignature,
onFieldChange,
}, ref) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const [scrollPosition, setScrollPosition] = useState({ scrollLeft: 0, scrollTop: 0 });
@@ -385,6 +387,7 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
height={page.height}
zoom={zoom}
annotations={annotations}
onFieldChange={onFieldChange}
/>
{/* Text Selection Dragging Layer */}
+25
View File
@@ -545,6 +545,31 @@ def export_document(document_id: str):
filename = d["filename"]
if not filename.endswith(".pdf"):
filename += ".pdf"
# pyrefly: ignore [missing-import]
import sys
import os
roaming_path = os.path.join(os.environ.get("APPDATA", "C:\\Users\\azeem\\AppData\\Roaming"), "Python", "Python312", "site-packages")
if roaming_path not in sys.path:
sys.path.append(roaming_path)
import pypdf
import io
# Parse the raw bytes and force NeedAppearances so the viewer
# actually renders the filled values.
reader = pypdf.PdfReader(io.BytesIO(bytes_data))
writer = pypdf.PdfWriter()
writer.append(reader)
acro_form = writer.root_object.get("/AcroForm")
if acro_form is not None:
acro_form_dict = acro_form.get_object()
acro_form_dict[pypdf.generic.NameObject("/NeedAppearances")] = pypdf.generic.BooleanObject(True)
out_stream = io.BytesIO()
writer.write(out_stream)
bytes_data = out_stream.getvalue()
return Response(
content=bytes_data,
+15 -2
View File
@@ -168,6 +168,17 @@ class PageReorderOperation(BaseModel):
data: PageReorderData
class UpdateFieldData(BaseModel):
value: str | bool
class UpdateFieldOperation(BaseModel):
id: str
type: Literal["update_field"]
pageIndex: int
data: UpdateFieldData
EditOperation = Annotated[
TextOverlayOperation
| RedactionOperation
@@ -178,7 +189,8 @@ EditOperation = Annotated[
| FreehandOperation
| PageRotationOperation
| PageDeletionOperation
| PageReorderOperation,
| PageReorderOperation
| UpdateFieldOperation,
Field(discriminator="type"),
]
@@ -214,7 +226,8 @@ def apply_edits_impl(document_id: str, request: EditsRequest):
import os
import tempfile
from PIL import Image
# pyrefly: ignore [missing-import]
from PIL import Image
# Remove data URI header if present
if "," in img_data_str: