done anotations update

This commit is contained in:
azeeee05
2026-06-11 11:46:15 +05:30
parent ab88a05d36
commit c410f787f7
6 changed files with 204 additions and 10 deletions
+86
View File
@@ -2250,6 +2250,92 @@ std::expected<void, EngineError> 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<uint8_t> buf(len);
FPDFAnnot_GetStringValue(annot, "NM", reinterpret_cast<FPDF_WCHAR*>(buf.data()), len);
id = utf16le_to_utf8(reinterpret_cast<const char16_t*>(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<float>(data["x"].get<double>());
float y = static_cast<float>(data["y"].get<double>());
float width = static_cast<float>(data["width"].get<double>());
float height = static_cast<float>(data["height"].get<double>());
FS_RECTF rect;
rect.left = x;
rect.right = x + width;
rect.top = static_cast<float>(pageHeight - y);
rect.bottom = static_cast<float>(pageHeight - (y + height));
FPDFAnnot_SetRect(targetAnnot, &rect);
}
// Update Color
if (data.contains("color")) {
std::string colorStr = data["color"].get<std::string>();
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<float>(data["thickness"].get<double>());
FPDFAnnot_SetBorder(targetAnnot, 0.0f, 0.0f, thickness);
}
// Update Text
if (data.contains("text")) {
std::string text = data["text"].get<std::string>();
auto utf16 = utf8_to_utf16le(text);
FPDFAnnot_SetStringValue(targetAnnot, "Contents", reinterpret_cast<FPDF_WIDESTRING>(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);
}
+24
View File
@@ -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 (
<div className="flex h-full w-full flex-col overflow-hidden bg-[#f1f2f4] text-[#18212e] antialiased">
@@ -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}
+27 -2
View File
@@ -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<InspectorPanelProps> = (p) => {
{/* Body */}
<div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto">
{p.activeTab === 'pages' && <PagesTab {...p} />}
{p.activeTab === 'notes' && <NotesTab annotations={p.annotations.filter((a) => a.type !== 'widget')} onNavigate={p.onNavigateAnnotation} onDelete={p.onDeleteAnnotation} />}
{p.activeTab === 'notes' && <NotesTab annotations={p.annotations.filter((a) => a.type !== 'widget')} onNavigate={p.onNavigateAnnotation} onDelete={p.onDeleteAnnotation} onUpdate={p.onUpdateAnnotation} />}
{p.activeTab === 'search' && <SearchTab {...p} />}
{p.activeTab === 'properties' && <PropertiesTab metadata={p.metadata} sizeBytes={p.sizeBytes} totalPages={p.totalPages} filename={selectedDoc?.filename} />}
{p.activeTab === 'fonts' && <FontsTab fonts={p.fonts} />}
@@ -191,7 +192,7 @@ const PagesTab: React.FC<InspectorPanelProps> = (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 <EmptyState icon={<NotesIcon size={30} />} 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 && <p className="line-clamp-3 text-[12px] italic text-[#5b6573]">{a.content}</p>}
<span className="text-[10px] font-medium text-[#98a1ad]">{a.author}</span>
</CustomButton>
{onUpdate && (a.type === 'highlight' || a.type === 'ink' || a.type === 'comment') && (
<div className="flex items-center gap-2 mt-1 px-1" onClick={e => e.stopPropagation()}>
<input
type="color"
value={a.color || (a.type === 'highlight' ? '#ffeb3b' : '#000000')}
onChange={e => 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' && (
<input
type="range"
min="1"
max="10"
value={a.thickness || 2}
onChange={e => onUpdate({ ...a, thickness: parseFloat(e.target.value) })}
className="w-16"
title="Change Thickness"
/>
)}
</div>
)}
<button
title="Delete annotation"
aria-label="Delete annotation"
+12
View File
@@ -184,12 +184,24 @@ export type EditOperationDataMap = {
page_deletion: PageDeletionData;
page_reorder: PageReorderData;
delete_annotation: DeleteAnnotationData;
update_annotation: UpdateAnnotationData;
};
export interface DeleteAnnotationData {
annotationId: string;
}
export interface UpdateAnnotationData {
annotationId: string;
x?: number;
y?: number;
width?: number;
height?: number;
color?: string;
thickness?: number;
text?: string;
}
export interface OutlineItem {
title: string;
pageIndex: number;
+49 -7
View File
@@ -29,6 +29,7 @@ interface AnnotationLayerProps {
zoom: number;
annotations: Annotation[];
onAnnotationClick?: (annotation: Annotation) => void;
onAnnotationUpdate?: (annotation: Annotation) => void;
onFieldChange?: (id: string, value: string | boolean, pageIndex: number) => void;
}
@@ -39,8 +40,47 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
zoom,
annotations,
onAnnotationClick,
onAnnotationUpdate,
onFieldChange,
}) => {
const [draggingAnno, setDraggingAnno] = React.useState<string | null>(null);
const [dragStartPos, setDragStartPos] = React.useState({ x: 0, y: 0 });
const [dragOffset, setDragOffset] = React.useState({ x: 0, y: 0 });
const handlePointerDown = (e: React.PointerEvent, id: string) => {
e.stopPropagation();
setDraggingAnno(id);
setDragStartPos({ x: e.clientX, y: e.clientY });
setDragOffset({ x: 0, y: 0 });
(e.target as HTMLElement).setPointerCapture(e.pointerId);
};
const handlePointerMove = (e: React.PointerEvent, id: string) => {
if (draggingAnno !== id) return;
setDragOffset({
x: (e.clientX - dragStartPos.x) / zoom,
y: (e.clientY - dragStartPos.y) / zoom,
});
};
const handlePointerUp = (e: React.PointerEvent, anno: Annotation) => {
if (draggingAnno !== anno.id) return;
setDraggingAnno(null);
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
if (dragOffset.x !== 0 || dragOffset.y !== 0) {
onAnnotationUpdate?.({
...anno,
bbox: {
...anno.bbox,
x: anno.bbox.x + dragOffset.x,
y: anno.bbox.y + dragOffset.y,
}
});
} else {
onAnnotationClick?.(anno);
}
};
return (
<div
className="absolute top-0 left-0 z-30"
@@ -62,17 +102,19 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
? `${anno.author}${formattedDate ? ` (${formattedDate})` : ''}\n${anno.content || ''}`
: `${anno.author}: ${anno.content || ''}`;
const isDragging = draggingAnno === anno.id;
const currentOffset = isDragging ? dragOffset : { x: 0, y: 0 };
return (
<div
key={anno.id}
onClick={(e) => {
e.stopPropagation();
onAnnotationClick?.(anno);
}}
className={`absolute cursor-pointer rounded-[2px] transition-[opacity,box-shadow] duration-150 hover:shadow-[0_2px_8px_rgba(16,24,40,0.18)] type-${anno.type}`}
onPointerDown={(e) => handlePointerDown(e, anno.id)}
onPointerMove={(e) => handlePointerMove(e, anno.id)}
onPointerUp={(e) => handlePointerUp(e, anno)}
className={`absolute cursor-pointer rounded-[2px] transition-[opacity,box-shadow] duration-150 hover:shadow-[0_2px_8px_rgba(16,24,40,0.18)] type-${anno.type} ${isDragging ? 'shadow-lg z-50' : ''}`}
style={{
left: `${scaledBbox.x}px`,
top: `${scaledBbox.y}px`,
left: `${scaledBbox.x + currentOffset.x * zoom}px`,
top: `${scaledBbox.y + currentOffset.y * zoom}px`,
width: `${scaledBbox.width}px`,
height: `${scaledBbox.height}px`,
backgroundColor: anno.type === 'highlight' ? (anno.color || '#ffeb3b') : undefined,
+6 -1
View File
@@ -28,6 +28,8 @@ interface PDFViewerProps {
searchResults?: SearchResult[];
searchCurrentMatch?: number;
onAnnotationAdded?: (anno: Annotation) => void;
onAnnotationUpdate?: (anno: Annotation) => void;
onAnnotationClick?: (anno: Annotation) => void;
onPageVisible?: (pageIndex: number) => void;
onRedactArea?: (pageIndex: number, bounds: Rect) => void;
onPlaceText?: (pageIndex: number, rectPts: Rect, text: string) => void;
@@ -65,6 +67,8 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
searchResults,
searchCurrentMatch,
onAnnotationAdded,
onAnnotationUpdate,
onAnnotationClick,
onPageVisible,
onRedactArea,
onPlaceText,
@@ -79,7 +83,6 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
const [containerHeight, setContainerHeight] = useState(800);
const [pageTexts, setPageTexts] = useState<Record<number, Glyph[]>>({});
const verifiedPagesRef = useRef<Set<number>>(new Set());
// Reset cached page renders when switching documents
useEffect(() => {
setRenderedPages([]);
@@ -387,6 +390,8 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
height={page.height}
zoom={zoom}
annotations={annotations}
onAnnotationClick={onAnnotationClick}
onAnnotationUpdate={onAnnotationUpdate}
onFieldChange={onFieldChange}
/>