diff --git a/engine/src/parser/pdfium_document.cpp b/engine/src/parser/pdfium_document.cpp index bb3dff0..cc4c9af 100644 --- a/engine/src/parser/pdfium_document.cpp +++ b/engine/src/parser/pdfium_document.cpp @@ -2250,6 +2250,92 @@ std::expected PdfiumDocument::applyEdits(const std::string& e spdlog::warn("delete_annotation: annotation '{}' not found on page {}", targetId, pageIndex); } FPDF_ClosePage(page); + } else if (type == "update_annotation") { + if (!op.contains("data") || !op["data"].is_object()) { + spdlog::error("update_annotation operation missing 'data' object"); + return std::unexpected(EngineError::InvalidFormat); + } + auto data = op["data"]; + std::string targetId = data.value("annotationId", ""); + if (targetId.empty()) { + spdlog::error("update_annotation missing 'annotationId'"); + return std::unexpected(EngineError::InvalidFormat); + } + + FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex); + if (!page) { + spdlog::error("Failed to load page index {} for update_annotation", pageIndex); + return std::unexpected(EngineError::Unknown); + } + + int count = FPDFPage_GetAnnotCount(page); + FPDF_ANNOTATION targetAnnot = nullptr; + for (int i = 0; i < count; ++i) { + FPDF_ANNOTATION annot = FPDFPage_GetAnnot(page, i); + if (!annot) continue; + + std::string id; + unsigned long len = FPDFAnnot_GetStringValue(annot, "NM", nullptr, 0); + if (len > 2) { + std::vector buf(len); + FPDFAnnot_GetStringValue(annot, "NM", reinterpret_cast(buf.data()), len); + id = utf16le_to_utf8(reinterpret_cast(buf.data()), len / sizeof(char16_t)); + while (!id.empty() && id.back() == '\0') id.pop_back(); + } + if (id.empty()) { + id = "anno_" + std::to_string(pageIndex) + "_" + std::to_string(i); + } + + if (id == targetId) { + targetAnnot = annot; + break; + } + FPDFPage_CloseAnnot(annot); + } + + if (targetAnnot) { + // Update Rect + if (data.contains("x") && data.contains("y") && data.contains("width") && data.contains("height")) { + double pageHeight = FPDF_GetPageHeightF(page); + float x = static_cast(data["x"].get()); + float y = static_cast(data["y"].get()); + float width = static_cast(data["width"].get()); + float height = static_cast(data["height"].get()); + + FS_RECTF rect; + rect.left = x; + rect.right = x + width; + rect.top = static_cast(pageHeight - y); + rect.bottom = static_cast(pageHeight - (y + height)); + FPDFAnnot_SetRect(targetAnnot, &rect); + } + + // Update Color + if (data.contains("color")) { + std::string colorStr = data["color"].get(); + unsigned int r = 0, g = 0, b = 0; + parseHexColor(colorStr, r, g, b); + FPDFAnnot_SetColor(targetAnnot, FPDFANNOT_COLORTYPE_Color, r, g, b, 255); + } + + // Update Thickness + if (data.contains("thickness")) { + float thickness = static_cast(data["thickness"].get()); + FPDFAnnot_SetBorder(targetAnnot, 0.0f, 0.0f, thickness); + } + + // Update Text + if (data.contains("text")) { + std::string text = data["text"].get(); + auto utf16 = utf8_to_utf16le(text); + FPDFAnnot_SetStringValue(targetAnnot, "Contents", reinterpret_cast(utf16.data())); + } + + FPDFPage_CloseAnnot(targetAnnot); + } else { + spdlog::warn("update_annotation: annotation '{}' not found on page {}", targetId, pageIndex); + } + FPDF_ClosePage(page); } else { spdlog::warn("Unsupported edit operation type: {}", type); } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 810d2a0..1315da9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -425,6 +425,24 @@ function App() { }], 'Annotation deleted'); }; + const handleUpdateAnnotation = (a: Annotation) => { + // Optimistically update + setAnnotations((prev) => prev.map((x) => x.id === a.id ? a : x)); + applyOps([{ + id: rid('updanno'), type: 'update_annotation', pageIndex: a.pageIndex ?? currentPage, + data: { + annotationId: a.id, + x: a.bbox.x, + y: a.bbox.y, + width: a.bbox.width, + height: a.bbox.height, + color: a.color, + thickness: a.thickness, + text: a.content, + }, + }], 'Annotation updated'); + }; + /* -------------------------------------------------------------- render */ return (
@@ -504,6 +522,11 @@ function App() { searchResults={searchResults} searchCurrentMatch={searchCurrentMatch} onAnnotationAdded={handleAnnotationAdded} + onAnnotationUpdate={handleUpdateAnnotation} + onAnnotationClick={(a) => { + setInspectorTab('notes'); + if (!isInspectorOpen) setIsInspectorOpen(true); + }} onPageVisible={setCurrentPage} onRedactArea={handleRedactArea} onPlaceText={handlePlaceText} @@ -546,6 +569,7 @@ function App() { annotations={annotations} onNavigateAnnotation={navigateToAnnotation} onDeleteAnnotation={handleDeleteAnnotation} + onUpdateAnnotation={handleUpdateAnnotation} outline={outline} onNavigateOutline={(p) => viewerRef.current?.scrollToPage(p)} searchQuery={searchQuery} diff --git a/frontend/src/components/InspectorPanel.tsx b/frontend/src/components/InspectorPanel.tsx index e852cc2..7d17e82 100644 --- a/frontend/src/components/InspectorPanel.tsx +++ b/frontend/src/components/InspectorPanel.tsx @@ -30,6 +30,7 @@ interface InspectorPanelProps { annotations: Annotation[]; onNavigateAnnotation: (a: Annotation) => void; onDeleteAnnotation: (a: Annotation) => void; + onUpdateAnnotation?: (a: Annotation) => void; outline: OutlineItem[]; onNavigateOutline: (pageIndex: number) => void; @@ -137,7 +138,7 @@ export const InspectorPanel: React.FC = (p) => { {/* Body */}
{p.activeTab === 'pages' && } - {p.activeTab === 'notes' && a.type !== 'widget')} onNavigate={p.onNavigateAnnotation} onDelete={p.onDeleteAnnotation} />} + {p.activeTab === 'notes' && a.type !== 'widget')} onNavigate={p.onNavigateAnnotation} onDelete={p.onDeleteAnnotation} onUpdate={p.onUpdateAnnotation} />} {p.activeTab === 'search' && } {p.activeTab === 'properties' && } {p.activeTab === 'fonts' && } @@ -191,7 +192,7 @@ const PagesTab: React.FC = (p) => { ); }; -const NotesTab: React.FC<{ annotations: Annotation[]; onNavigate: (a: Annotation) => void; onDelete: (a: Annotation) => void }> = ({ annotations, onNavigate, onDelete }) => { +const NotesTab: React.FC<{ annotations: Annotation[]; onNavigate: (a: Annotation) => void; onDelete: (a: Annotation) => void; onUpdate?: (a: Annotation) => void }> = ({ annotations, onNavigate, onDelete, onUpdate }) => { if (annotations.length === 0) { return } title="No annotations yet" hint="Highlights, ink, and comments you add appear here." />; @@ -209,6 +210,30 @@ const NotesTab: React.FC<{ annotations: Annotation[]; onNavigate: (a: Annotation {a.content &&

“{a.content}”

} {a.author} + + {onUpdate && (a.type === 'highlight' || a.type === 'ink' || a.type === 'comment') && ( +
e.stopPropagation()}> + onUpdate({ ...a, color: e.target.value })} + className="h-5 w-5 cursor-pointer rounded border border-gray-300 p-0" + title="Change Color" + /> + {a.type === 'ink' && ( + onUpdate({ ...a, thickness: parseFloat(e.target.value) })} + className="w-16" + title="Change Thickness" + /> + )} +
+ )} +