done React: form field viewing (Widget annotations) with saving issues fixed
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user