Merge branch 'dev' into furqan, resolving OverlayLayer conflict
This commit is contained in:
@@ -1588,7 +1588,68 @@ std::expected<void, EngineError> PdfiumDocument::applyEdits(const std::string& e
|
||||
FPDFBitmap_Destroy(bitmap);
|
||||
FPDF_ClosePage(page);
|
||||
} else if (type == "highlight") {
|
||||
spdlog::info("Parsed highlight edit operation (stub)");
|
||||
if (!op.contains("data") || !op["data"].is_object()) {
|
||||
spdlog::error("highlight operation missing 'data' object");
|
||||
return std::unexpected(EngineError::InvalidFormat);
|
||||
}
|
||||
auto data = op["data"];
|
||||
std::string color = data.value("color", "#ffeb3b");
|
||||
std::string author = data.value("author", "");
|
||||
std::string content = data.value("content", "");
|
||||
|
||||
if (!data.contains("quadPoints") || !data["quadPoints"].is_array()) {
|
||||
spdlog::error("highlight operation missing 'quadPoints' array");
|
||||
return std::unexpected(EngineError::InvalidFormat);
|
||||
}
|
||||
auto quadPoints = data["quadPoints"];
|
||||
|
||||
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
|
||||
if (!page) {
|
||||
spdlog::error("Failed to load page index {} for highlight", pageIndex);
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
|
||||
FPDF_ANNOTATION annot = FPDFPage_CreateAnnot(page, FPDF_ANNOT_HIGHLIGHT);
|
||||
if (!annot) {
|
||||
spdlog::error("Failed to create highlight annotation");
|
||||
FPDF_ClosePage(page);
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
|
||||
float minX = 999999, minY = 999999, maxX = -999999, maxY = -999999;
|
||||
for (const auto& qp : quadPoints) {
|
||||
FS_QUADPOINTSF quad;
|
||||
quad.x1 = qp.value("x1", 0.0f); quad.y1 = qp.value("y1", 0.0f);
|
||||
quad.x2 = qp.value("x2", 0.0f); quad.y2 = qp.value("y2", 0.0f);
|
||||
quad.x3 = qp.value("x3", 0.0f); quad.y3 = qp.value("y3", 0.0f);
|
||||
quad.x4 = qp.value("x4", 0.0f); quad.y4 = qp.value("y4", 0.0f);
|
||||
FPDFAnnot_AppendAttachmentPoints(annot, &quad);
|
||||
|
||||
minX = (std::min)({minX, quad.x1, quad.x2, quad.x3, quad.x4});
|
||||
minY = (std::min)({minY, quad.y1, quad.y2, quad.y3, quad.y4});
|
||||
maxX = (std::max)({maxX, quad.x1, quad.x2, quad.x3, quad.x4});
|
||||
maxY = (std::max)({maxY, quad.y1, quad.y2, quad.y3, quad.y4});
|
||||
}
|
||||
|
||||
FS_RECTF rect;
|
||||
rect.left = minX; rect.bottom = minY; rect.right = maxX; rect.top = maxY;
|
||||
FPDFAnnot_SetRect(annot, &rect);
|
||||
|
||||
unsigned int r = 0, g = 0, b = 0;
|
||||
parseHexColor(color, r, g, b);
|
||||
FPDFAnnot_SetColor(annot, FPDFANNOT_COLORTYPE_Color, r, g, b, 255);
|
||||
|
||||
if (!content.empty()) {
|
||||
auto utf16 = utf8_to_utf16le(content);
|
||||
FPDFAnnot_SetStringValue(annot, "Contents", reinterpret_cast<FPDF_WIDESTRING>(utf16.data()));
|
||||
}
|
||||
if (!author.empty()) {
|
||||
auto utf16 = utf8_to_utf16le(author);
|
||||
FPDFAnnot_SetStringValue(annot, "T", reinterpret_cast<FPDF_WIDESTRING>(utf16.data()));
|
||||
}
|
||||
|
||||
FPDFPage_CloseAnnot(annot);
|
||||
FPDF_ClosePage(page);
|
||||
} else if (type == "free_text") {
|
||||
spdlog::info("Parsed free_text edit operation (stub)");
|
||||
} else if (type == "comment") {
|
||||
|
||||
+65
-1
@@ -19,6 +19,7 @@ function App() {
|
||||
// Settings
|
||||
const [zoom, setZoom] = useState<number>(1.0);
|
||||
const [activeTool, setActiveTool] = useState<string>('select');
|
||||
const [highlightColor, setHighlightColor] = useState<string>('#ffeb3b');
|
||||
const [currentPage, setCurrentPage] = useState<number>(0);
|
||||
|
||||
// States
|
||||
@@ -167,7 +168,65 @@ function App() {
|
||||
setSidebarTab('annotations');
|
||||
};
|
||||
|
||||
const handleRotateClick = async (_newRotValue?: number) => {
|
||||
const handleSaveEdits = async () => {
|
||||
if (!selectedDocId || !activeDoc || annotations.length === 0) return;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const operations: any[] = annotations.map((anno) => {
|
||||
if (anno.type === 'highlight') {
|
||||
return {
|
||||
id: anno.id,
|
||||
type: 'highlight',
|
||||
pageIndex: anno.pageIndex || 0,
|
||||
data: {
|
||||
quadPoints: [{
|
||||
x1: anno.bbox.x, y1: anno.bbox.y,
|
||||
x2: anno.bbox.x + anno.bbox.width, y2: anno.bbox.y,
|
||||
x3: anno.bbox.x, y3: anno.bbox.y + anno.bbox.height,
|
||||
x4: anno.bbox.x + anno.bbox.width, y4: anno.bbox.y + anno.bbox.height,
|
||||
}],
|
||||
color: anno.color || '#ffeb3b',
|
||||
opacity: 0.5,
|
||||
author: anno.author,
|
||||
content: anno.content
|
||||
}
|
||||
};
|
||||
} else if (anno.type === 'ink') {
|
||||
return {
|
||||
id: anno.id,
|
||||
type: 'freehand',
|
||||
pageIndex: anno.pageIndex || 0,
|
||||
data: {
|
||||
paths: anno.paths || [],
|
||||
color: anno.color || '#3b82f6',
|
||||
thickness: 2.0
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}).filter(Boolean);
|
||||
|
||||
if (operations.length === 0) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await gatewayService.applyEdits(selectedDocId, operations);
|
||||
if (result.success) {
|
||||
setAnnotations([]);
|
||||
const docs = await gatewayService.listDocuments();
|
||||
setDocuments(docs);
|
||||
setSelectedDocId(result.newDocumentId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to save edits:', err);
|
||||
alert('Failed to save edits. Make sure the gateway is connected.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRotateClick = async () => {
|
||||
if (!selectedDocId || !activeDoc) return;
|
||||
const pageIndex = currentPage;
|
||||
try {
|
||||
@@ -353,6 +412,8 @@ function App() {
|
||||
onRotationChange={handleRotateClick}
|
||||
activeTool={activeTool}
|
||||
onActiveToolChange={setActiveTool}
|
||||
highlightColor={highlightColor}
|
||||
onHighlightColorChange={setHighlightColor}
|
||||
currentPage={currentPage}
|
||||
totalPages={activeDoc?.totalPages || 1}
|
||||
onUploadStart={handleUploadStart}
|
||||
@@ -365,6 +426,8 @@ function App() {
|
||||
onExport={handleExport}
|
||||
onWasmInspectToggle={() => setWasmInspectorOpen(!wasmInspectorOpen)}
|
||||
wasmInspectorOpen={wasmInspectorOpen}
|
||||
onSaveEdits={handleSaveEdits}
|
||||
hasAnnotations={annotations.length > 0}
|
||||
/>
|
||||
|
||||
<div className="flex-1 w-full flex overflow-hidden">
|
||||
@@ -403,6 +466,7 @@ function App() {
|
||||
zoom={zoom}
|
||||
pagesInfo={activeDoc.pages}
|
||||
activeTool={activeTool}
|
||||
highlightColor={highlightColor}
|
||||
annotations={annotations}
|
||||
searchQuery={searchQuery}
|
||||
searchResults={searchResults}
|
||||
|
||||
@@ -8,6 +8,8 @@ interface ToolbarProps {
|
||||
onRotationChange: (rot: number) => void;
|
||||
activeTool: string;
|
||||
onActiveToolChange: (tool: string) => void;
|
||||
highlightColor: string;
|
||||
onHighlightColorChange: (color: string) => void;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onUploadStart?: (file: File) => void;
|
||||
@@ -22,6 +24,8 @@ interface ToolbarProps {
|
||||
onExport?: () => void;
|
||||
onWasmInspectToggle?: () => void;
|
||||
wasmInspectorOpen?: boolean;
|
||||
onSaveEdits?: () => void;
|
||||
hasAnnotations?: boolean;
|
||||
}
|
||||
|
||||
export const Toolbar: React.FC<ToolbarProps> = ({
|
||||
@@ -31,6 +35,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
||||
onRotationChange,
|
||||
activeTool,
|
||||
onActiveToolChange,
|
||||
highlightColor,
|
||||
onHighlightColorChange,
|
||||
currentPage,
|
||||
totalPages,
|
||||
onUploadStart,
|
||||
@@ -43,6 +49,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
||||
onExport,
|
||||
onWasmInspectToggle,
|
||||
wasmInspectorOpen = false,
|
||||
onSaveEdits,
|
||||
hasAnnotations,
|
||||
}) => {
|
||||
|
||||
const handleZoomPercentSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
@@ -165,6 +173,20 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
||||
>
|
||||
Highlight
|
||||
</button>
|
||||
|
||||
{activeTool === 'highlight' && (
|
||||
<div className="flex items-center gap-1 ml-1 pl-2 border-l border-slate-700/50">
|
||||
{['#ffeb3b', '#4caf50', '#2196f3', '#e91e63', '#ff9800'].map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
className={`w-5 h-5 rounded-full border-2 transition-transform ${highlightColor === color ? 'border-white scale-110' : 'border-transparent hover:scale-105'}`}
|
||||
style={{ backgroundColor: color }}
|
||||
onClick={() => onHighlightColorChange(color)}
|
||||
title={`Set color to ${color}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => onActiveToolChange('draw')}
|
||||
@@ -224,6 +246,20 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{onSaveEdits && (
|
||||
<button
|
||||
onClick={onSaveEdits}
|
||||
className="save-btn font-semibold bg-indigo-600 text-white px-3 py-1.5 rounded-md text-sm hover:bg-indigo-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
|
||||
disabled={!hasAnnotations}
|
||||
title="Save Annotations to PDF"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
|
||||
</svg>
|
||||
Save Edits
|
||||
</button>
|
||||
)}
|
||||
|
||||
<label className="upload-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Rect } from '../lib/coordinateMapping';
|
||||
export interface Annotation {
|
||||
id: string;
|
||||
type: 'highlight' | 'signature' | 'strikeout' | 'comment' | 'ink';
|
||||
pageIndex: number;
|
||||
bbox: Rect;
|
||||
color?: string;
|
||||
author: string;
|
||||
@@ -21,6 +22,7 @@ interface AnnotationLayerProps {
|
||||
}
|
||||
|
||||
export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
|
||||
pageIndex,
|
||||
width,
|
||||
height,
|
||||
zoom,
|
||||
@@ -30,10 +32,10 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
|
||||
return (
|
||||
<div
|
||||
className="annotation-layer"
|
||||
style={{ width: `${width}px`, height: `${height}px` }}
|
||||
style={{ width: `${width}px`, height: `${height}px`, pointerEvents: 'none' }}
|
||||
>
|
||||
{annotations
|
||||
.filter((anno) => anno.type === 'highlight' || anno.type === 'comment')
|
||||
.filter((anno) => (anno.type === 'highlight' || anno.type === 'comment') && anno.pageIndex === pageIndex)
|
||||
.map((anno) => {
|
||||
const scaledBbox = {
|
||||
x: anno.bbox.x * zoom,
|
||||
@@ -55,6 +57,10 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
|
||||
top: `${scaledBbox.y}px`,
|
||||
width: `${scaledBbox.width}px`,
|
||||
height: `${scaledBbox.height}px`,
|
||||
backgroundColor: anno.type === 'highlight' ? (anno.color || '#ffeb3b') : undefined,
|
||||
opacity: anno.type === 'highlight' ? 0.4 : undefined,
|
||||
mixBlendMode: anno.type === 'highlight' ? 'multiply' : undefined,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
title={`${anno.author}: ${anno.content || ''}`}
|
||||
/>
|
||||
@@ -66,7 +72,7 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
|
||||
style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}
|
||||
>
|
||||
{annotations
|
||||
.filter((anno) => anno.type === 'ink' && anno.paths)
|
||||
.filter((anno) => anno.type === 'ink' && anno.paths && anno.pageIndex === pageIndex)
|
||||
.map((anno) => (
|
||||
<g key={anno.id} stroke={anno.color || '#3b82f6'} strokeWidth={2 * zoom} fill="none" strokeLinecap="round" strokeLinejoin="round">
|
||||
{anno.paths!.map((path, i) => {
|
||||
|
||||
@@ -35,7 +35,7 @@ export const OverlayLayer: React.FC<OverlayLayerProps> = ({
|
||||
if (activeTool !== 'draw') return;
|
||||
setIsDrawing(true);
|
||||
if (e.target instanceof Element) {
|
||||
e.target.setPointerCapture(e.pointerId);
|
||||
(e.target as Element).setPointerCapture(e.pointerId);
|
||||
}
|
||||
setCurrentPath([getCoordinates(e)]);
|
||||
};
|
||||
@@ -64,6 +64,7 @@ export const OverlayLayer: React.FC<OverlayLayerProps> = ({
|
||||
const newAnno: Annotation = {
|
||||
id: `anno_${Math.random().toString(36).substring(2, 11)}`,
|
||||
type: 'ink',
|
||||
pageIndex,
|
||||
bbox: {
|
||||
x: minX,
|
||||
y: minY,
|
||||
|
||||
@@ -18,6 +18,7 @@ interface PDFViewerProps {
|
||||
zoom: number;
|
||||
pagesInfo?: PageInfo[];
|
||||
activeTool: string;
|
||||
highlightColor: string;
|
||||
annotations: Annotation[];
|
||||
searchQuery?: string;
|
||||
searchResults?: SearchResult[];
|
||||
@@ -48,6 +49,7 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||
zoom,
|
||||
pagesInfo,
|
||||
activeTool,
|
||||
highlightColor,
|
||||
annotations,
|
||||
searchQuery,
|
||||
searchResults,
|
||||
@@ -206,10 +208,15 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||
};
|
||||
}, [visiblePages, documentId, zoom, renderedPages]);
|
||||
|
||||
const verifiedPages = useRef<Set<number>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
const verifyPageModel = async () => {
|
||||
if (visiblePages.length > 0 && documentId && !verifiedPages[visiblePages[0].index]) {
|
||||
const pageIndex = visiblePages[0].index;
|
||||
if (verifiedPages.current.has(pageIndex)) return;
|
||||
|
||||
verifiedPages.current.add(pageIndex);
|
||||
try {
|
||||
const model = await gatewayService.getPageModel(documentId, pageIndex);
|
||||
console.log(`--- Verification for Page ${pageIndex} ---`);
|
||||
@@ -227,24 +234,26 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||
[pageIndex]: true,
|
||||
}));
|
||||
} catch (e) {
|
||||
// ignore or log
|
||||
verifiedPages.current.delete(pageIndex);
|
||||
}
|
||||
}
|
||||
};
|
||||
verifyPageModel();
|
||||
}, [visiblePages, documentId, verifiedPages]);
|
||||
|
||||
const handleTextSelection = (text: string, bbox: Rect) => {
|
||||
const handleTextSelection = (text: string, bbox: Rect, pageIndex: number) => {
|
||||
if (activeTool === 'highlight') {
|
||||
const newAnno: Annotation = {
|
||||
id: generateUniqueId(),
|
||||
type: 'highlight',
|
||||
pageIndex,
|
||||
bbox: {
|
||||
x: bbox.x / zoom,
|
||||
y: bbox.y / zoom,
|
||||
width: bbox.width / zoom,
|
||||
height: bbox.height / zoom,
|
||||
},
|
||||
color: highlightColor,
|
||||
author: 'Current User',
|
||||
content: text,
|
||||
};
|
||||
@@ -337,7 +346,7 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||
width={page.width}
|
||||
height={page.height}
|
||||
zoom={zoom}
|
||||
onTextSelected={(text, bbox) => handleTextSelection(text, bbox)}
|
||||
onTextSelected={(text, bbox) => handleTextSelection(text, bbox, page.index)}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user