diff --git a/app/proprietary/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java b/app/proprietary/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java index f72df4033..b0869dc16 100644 --- a/app/proprietary/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java +++ b/app/proprietary/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java @@ -2647,8 +2647,18 @@ public class PdfJsonConversionService { pageNumber); continue; } - contentStream.showText( - new String(encoded, StandardCharsets.ISO_8859_1)); + try { + contentStream.showText( + new String(encoded, StandardCharsets.ISO_8859_1)); + } catch (IllegalArgumentException ex) { + log.warn( + "Failed to render text '{}' with font {} on page {}: {}", + run.text(), + run.font().getName(), + pageNumber, + ex.getMessage()); + continue; + } } } } @@ -3380,8 +3390,18 @@ public class PdfJsonConversionService { // or return null to trigger fallback font } else if (!isType3Font || fontModel == null) { // For non-Type3 fonts without Type3 metadata, use standard encoding - byte[] encoded = font.encode(text); - return sanitizeEncoded(encoded); + try { + byte[] encoded = font.encode(text); + return sanitizeEncoded(encoded); + } catch (IllegalArgumentException ex) { + log.info( + "[FONT-DEBUG] Font {} cannot encode text '{}': {}", + font.getName(), + text, + ex.getMessage()); + // Return null to trigger fallback font mechanism + return null; + } } // Type3 glyph mapping logic (for actual Type3 fonts AND normalized Type3 fonts) diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index d44661a23..c972fd919 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -4471,7 +4471,8 @@ "title": "PDF Editor", "badges": { "unsaved": "Unsaved changes", - "modified": "Edited" + "modified": "Edited", + "earlyAccess": "Early Access - Pre-Alpha" }, "controls": { "title": "Document Controls" @@ -4526,6 +4527,12 @@ "paragraph": "Paragraph", "singleLine": "Single Line" }, + "modeChange": { + "title": "Confirm Mode Change", + "warning": "Changing the text grouping mode will reset all unsaved changes. Are you sure you want to continue?", + "cancel": "Cancel", + "confirm": "Reset and Change Mode" + }, "disclaimer": { "heading": "Preview limitations", "textFocus": "This workspace focuses on editing text and repositioning embedded images. Complex page artwork, form widgets, and layered graphics are preserved for export but are not fully editable here.", diff --git a/frontend/src/proprietary/components/tools/pdfTextEditor/FontStatusPanel.tsx b/frontend/src/proprietary/components/tools/pdfTextEditor/FontStatusPanel.tsx index 244e31494..4bb8ce65c 100644 --- a/frontend/src/proprietary/components/tools/pdfTextEditor/FontStatusPanel.tsx +++ b/frontend/src/proprietary/components/tools/pdfTextEditor/FontStatusPanel.tsx @@ -255,8 +255,8 @@ const FontStatusPanel: React.FC = ({ document, pageIndex } {/* Font List */} - {fonts.map((font) => ( - + {fonts.map((font, index) => ( + ))} diff --git a/frontend/src/proprietary/components/tools/pdfTextEditor/PdfTextEditorView.tsx b/frontend/src/proprietary/components/tools/pdfTextEditor/PdfTextEditorView.tsx index fc008f6a5..9d78e5a2e 100644 --- a/frontend/src/proprietary/components/tools/pdfTextEditor/PdfTextEditorView.tsx +++ b/frontend/src/proprietary/components/tools/pdfTextEditor/PdfTextEditorView.tsx @@ -318,6 +318,9 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => { const [activeGroupId, setActiveGroupId] = useState(null); const [editingGroupId, setEditingGroupId] = useState(null); const [activeImageId, setActiveImageId] = useState(null); + const draggingImageRef = useRef(null); + const rndRefs = useRef>(new Map()); + const pendingDragUpdateRef = useRef(null); const [fontFamilies, setFontFamilies] = useState>(new Map()); const [autoScaleText, setAutoScaleText] = useState(true); const [textScales, setTextScales] = useState>(new Map()); @@ -930,6 +933,41 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => { } }, [editingGroupId]); + // Sync image positions when not dragging (handles stutters/re-renders) + useLayoutEffect(() => { + const isDragging = draggingImageRef.current !== null; + if (isDragging) { + return; // Don't sync during drag + } + + pageImages.forEach((image) => { + if (!image?.id) return; + + const imageId = image.id; + const rndRef = rndRefs.current.get(imageId); + if (!rndRef || !rndRef.updatePosition) return; + + const bounds = getImageBounds(image); + const width = Math.max(bounds.right - bounds.left, 1); + const height = Math.max(bounds.top - bounds.bottom, 1); + const cssLeft = bounds.left * scale; + const cssTop = (pageHeight - bounds.top) * scale; + + // Get current position from Rnd component + const currentState = rndRef.state || {}; + const currentX = currentState.x ?? 0; + const currentY = currentState.y ?? 0; + + // Calculate drift + const drift = Math.abs(currentX - cssLeft) + Math.abs(currentY - cssTop); + + // Only sync if drift is significant (more than 3px) + if (drift > 3) { + rndRef.updatePosition({ x: cssLeft, y: cssTop }); + } + }); + }, [pageImages, scale, pageHeight]); + const handlePageChange = (pageNumber: number) => { setActiveGroupId(null); setEditingGroupId(null); @@ -1413,25 +1451,43 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => { return ( { + if (ref) { + rndRefs.current.set(imageId, ref); + } else { + rndRefs.current.delete(imageId); + } + }} key={`image-${imageId}`} bounds="parent" size={{ width: cssWidth, height: cssHeight }} position={{ x: cssLeft, y: cssTop }} - onDragStart={() => { + onDragStart={(_event, data) => { setActiveGroupId(null); setEditingGroupId(null); setActiveImageId(imageId); + draggingImageRef.current = imageId; }} onDrag={(_event, data) => { - emitImageTransform( - imageId, - data.x, - data.y, - cssWidth, - cssHeight, - ); + // Cancel any pending update + if (pendingDragUpdateRef.current) { + cancelAnimationFrame(pendingDragUpdateRef.current); + } + + // Schedule update on next frame to batch rapid drag events + pendingDragUpdateRef.current = requestAnimationFrame(() => { + const rndRef = rndRefs.current.get(imageId); + if (rndRef && rndRef.updatePosition) { + rndRef.updatePosition({ x: data.x, y: data.y }); + } + }); }} onDragStop={(_event, data) => { + if (pendingDragUpdateRef.current) { + cancelAnimationFrame(pendingDragUpdateRef.current); + pendingDragUpdateRef.current = null; + } + draggingImageRef.current = null; emitImageTransform( imageId, data.x, @@ -1444,19 +1500,10 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => { setActiveImageId(imageId); setActiveGroupId(null); setEditingGroupId(null); - }} - onResize={(_event, _direction, ref, _delta, position) => { - const nextWidth = parseFloat(ref.style.width); - const nextHeight = parseFloat(ref.style.height); - emitImageTransform( - imageId, - position.x, - position.y, - nextWidth, - nextHeight, - ); + draggingImageRef.current = imageId; }} onResizeStop={(_event, _direction, ref, _delta, position) => { + draggingImageRef.current = null; const nextWidth = parseFloat(ref.style.width); const nextHeight = parseFloat(ref.style.height); emitImageTransform( diff --git a/frontend/src/proprietary/tools/pdfTextEditor/PdfTextEditor.tsx b/frontend/src/proprietary/tools/pdfTextEditor/PdfTextEditor.tsx index 42b26384b..8ef040338 100644 --- a/frontend/src/proprietary/tools/pdfTextEditor/PdfTextEditor.tsx +++ b/frontend/src/proprietary/tools/pdfTextEditor/PdfTextEditor.tsx @@ -976,6 +976,36 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => { } catch (textError) { console.warn('[PdfTextEditor] Failed to strip text from preview', textError); } + + // Also mask out images to prevent ghost/shadow images when they're moved + try { + const pageImages = imagesByPage[pageIndex] ?? []; + if (pageImages.length > 0) { + context.save(); + context.globalCompositeOperation = 'destination-out'; + context.fillStyle = '#000000'; + for (const image of pageImages) { + if (!image) continue; + // Get image bounds in PDF coordinates + const left = image.left ?? image.x ?? 0; + const bottom = image.bottom ?? image.y ?? 0; + const width = image.width ?? Math.max((image.right ?? left) - left, 0); + const height = image.height ?? Math.max((image.top ?? bottom) - bottom, 0); + const right = left + width; + const top = bottom + height; + + // Convert to canvas coordinates (PDF origin is bottom-left, canvas is top-left) + const canvasX = left * scale; + const canvasY = canvas.height - top * scale; + const canvasWidth = width * scale; + const canvasHeight = height * scale; + context.fillRect(canvasX, canvasY, canvasWidth, canvasHeight); + } + context.restore(); + } + } catch (imageError) { + console.warn('[PdfTextEditor] Failed to strip images from preview', imageError); + } const dataUrl = canvas.toDataURL('image/png'); page.cleanup(); if (previewRequestIdRef.current !== currentToken) { @@ -993,7 +1023,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => { previewRenderingRef.current.delete(pageIndex); } }, - [hasVectorPreview], + [hasVectorPreview, imagesByPage], ); // Re-group text when grouping mode changes without forcing a full reload