diff --git a/.github/workflows/push-docker-demo.yml b/.github/workflows/push-docker-demo.yml index 5ee6d640a..b5a76a9a6 100644 --- a/.github/workflows/push-docker-demo.yml +++ b/.github/workflows/push-docker-demo.yml @@ -76,6 +76,6 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }} - platforms: linux/amd64,linux/arm64/v8 + platforms: linux/amd64 provenance: true sbom: true diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index a0f7a2666..55a5adf4a 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -5285,5 +5285,111 @@ "offline": "Backend Offline", "starting": "Backend starting up...", "wait": "Please wait for the backend to finish launching and try again." + }, + "pdfTextEditor": { + "title": "PDF JSON Editor", + "imageLabel": "Placed image", + "pageSummary": "Page {{number}} of {{total}}", + "currentFile": "Current file: {{name}}", + "pagePreviewAlt": "Page preview", + "noTextOnPage": "No editable text was detected on this page.", + "empty": { + "title": "No document loaded", + "subtitle": "Load a PDF or JSON file to begin editing text content." + }, + "pageType": { + "sparse": "Sparse text" + }, + "badges": { + "unsaved": "Edited", + "modified": "Edited", + "earlyAccess": "Early Access" + }, + "actions": { + "reset": "Reset Changes", + "downloadJson": "Download JSON", + "generatePdf": "Generate PDF" + }, + "fontAnalysis": { + "details": "Font Details", + "embedded": "Embedded", + "type": "Type", + "webFormat": "Web Format", + "warnings": "Warnings", + "suggestions": "Notes", + "currentPageFonts": "Fonts on this page", + "allFonts": "All fonts", + "fallback": "fallback", + "missing": "missing", + "perfect": "perfect", + "subset": "subset", + "perfectMessage": "All fonts are perfectly matched", + "warningMessage": "Some fonts may render differently", + "infoMessage": "Font information" + }, + "manual": { + "resizeHandle": "Adjust text width" + }, + "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" + }, + "options": { + "autoScaleText": { + "title": "Auto-scale text to fit boxes", + "description": "Automatically scales text horizontally to fit within its original bounding box when font rendering differs from PDF." + }, + "groupingMode": { + "title": "Text Grouping Mode", + "autoDescription": "Automatically detects page type and groups text appropriately.", + "paragraphDescription": "Groups aligned lines into multi-line paragraph text boxes." + }, + "manualGrouping": { + "descriptionInline": "Tip: Hold Ctrl (Cmd) or Shift to multi-select text boxes. A floating toolbar will appear above the selection so you can merge, ungroup, or adjust widths." + }, + "forceSingleElement": { + "title": "Lock edited text to a single PDF element", + "description": "When enabled, the editor exports each edited text box as one PDF text element to avoid overlapping glyphs or mixed fonts." + } + }, + "groupingMode": { + "auto": "Auto", + "paragraph": "Paragraph", + "singleLine": "Single Line" + }, + "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.", + "previewVariance": "Some visuals (such as table borders, shapes, or annotation appearances) may not display exactly in the preview. The exported PDF keeps the original drawing commands whenever possible.", + "alpha": "This alpha viewer is still evolving—certain fonts, colours, transparency effects, and layout details may shift slightly. Please double-check the generated PDF before sharing." + }, + "welcomeBanner": { + "title": "Welcome to PDF Text Editor (Early Access)", + "experimental": "This is an experimental feature in active development. Expect some instability and issues during use.", + "howItWorks": "This tool converts your PDF to an editable format where you can modify text content and reposition images. Changes are saved back as a new PDF.", + "bestFor": "Works Best With:", + "bestFor1": "Simple PDFs containing primarily text and images", + "bestFor2": "Documents with standard paragraph formatting", + "bestFor3": "Letters, essays, reports, and basic documents", + "notIdealFor": "Not Ideal For:", + "notIdealFor1": "PDFs with special formatting like bullet points, tables, or multi-column layouts", + "notIdealFor2": "Magazines, brochures, or heavily designed documents", + "notIdealFor3": "Instruction manuals with complex layouts", + "limitations": "Current Limitations:", + "limitation1": "Font rendering may differ slightly from the original PDF", + "limitation2": "Complex graphics, form fields, and annotations are preserved but not editable", + "limitation3": "Large files may take time to convert and process", + "knownIssues": "Known Issues (Being Fixed):", + "issue1": "Text colour is not currently preserved (will be added soon)", + "issue2": "Paragraph mode has more alignment and spacing issues - Single Line mode recommended", + "issue3": "The preview display differs from the exported PDF - exported PDFs are closer to the original", + "issue4": "Rotated text alignment may need manual adjustment", + "issue5": "Transparency and layering effects may vary from original", + "feedback": "This is an early access feature. Please report any issues you encounter to help us improve!", + "gotIt": "Got it", + "dontShowAgain": "Don't show again" + } } } diff --git a/frontend/src/proprietary/components/tools/pdfTextEditor/PdfTextEditorView.tsx b/frontend/src/proprietary/components/tools/pdfTextEditor/PdfTextEditorView.tsx index 3b0d1b8e3..131d66a88 100644 --- a/frontend/src/proprietary/components/tools/pdfTextEditor/PdfTextEditorView.tsx +++ b/frontend/src/proprietary/components/tools/pdfTextEditor/PdfTextEditorView.tsx @@ -2084,7 +2084,8 @@ const selectionToolbarPosition = useMemo(() => { const isActive = activeGroupId === group.id || editingGroupId === group.id; const isEditing = editingGroupId === group.id; const baseFontSize = group.fontMatrixSize ?? group.fontSize ?? 12; - const fontSizePx = Math.max(baseFontSize * scale, 6); + // Reduce font size by ~2% to account for browser rendering differences and prevent wrapping + const fontSizePx = Math.max(baseFontSize * scale * 0.98, 6); const effectiveFontId = resolveFontIdForIndex(pageGroupIndex) ?? group.fontId; const fontFamily = getFontFamily(effectiveFontId, group.pageIndex); let lineHeightPx = getLineHeightPx(effectiveFontId, group.pageIndex, fontSizePx); @@ -2154,9 +2155,12 @@ const selectionToolbarPosition = useMemo(() => { const fontWeight = group.fontWeight || getFontWeight(effectiveFontId, group.pageIndex); // Determine text wrapping behavior based on whether text has been changed + // Only wrap if: it's a multi-line paragraph, width extended, or text changed + // Don't wrap single lines just because we're in paragraph mode const hasChanges = changed; const widthExtended = resolvedWidth - baseWidth > 0.5; - const enableWrap = isParagraphLayout || widthExtended || isEditing || hasChanges; + const isMultiLineParagraph = isParagraphLayout && lineCount > 1; + const enableWrap = isMultiLineParagraph || widthExtended || hasChanges; const whiteSpace = enableWrap ? 'pre-wrap' : 'pre'; const wordBreak = enableWrap ? 'break-word' : 'normal'; const overflowWrap = enableWrap ? 'break-word' : 'normal'; @@ -2169,7 +2173,7 @@ const selectionToolbarPosition = useMemo(() => { // We need to add this to the container width to compensate, so the inner content // has the full PDF-defined width available for text // Add extra padding to prevent text from being too tight and wrapping prematurely - const WRAPPER_HORIZONTAL_PADDING = 10; + const WRAPPER_HORIZONTAL_PADDING = 15; const containerStyle: React.CSSProperties = { position: 'absolute', @@ -2301,6 +2305,7 @@ const selectionToolbarPosition = useMemo(() => { } const textScale = textScales.get(group.id) ?? 1; + // Apply scale when needed to prevent text overflow, even when editing const shouldScale = autoScaleText && textScale < 0.98; return ( @@ -2333,7 +2338,8 @@ const selectionToolbarPosition = useMemo(() => { data-text-content style={{ pointerEvents: 'none', - display: enableWrap ? 'inline' : 'inline-block', + // Keep inline-block even when wrapping to maintain scaleX transform + display: 'inline-block', transform: shouldScale ? `scaleX(${textScale})` : 'none', transformOrigin: 'left center', whiteSpace,