diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 5d9f08467..8b16244ac 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -6160,7 +6160,7 @@ title = "Authentication Failed" message = "Authentication was not successful. You can close this window and try again." [pdfTextEditor] -title = "PDF JSON Editor" +title = "PDF Text Editor" viewLabel = "PDF Editor" converting = "Converting PDF to editable format..." conversionFailed = "Failed to convert PDF. Please try again." @@ -6189,6 +6189,8 @@ reset = "Reset Changes" downloadJson = "Download JSON" generatePdf = "Generate PDF" saveChanges = "Save Changes" +applyChanges = "Apply Changes" +downloadCopy = "Download Copy" [pdfTextEditor.options.autoScaleText] title = "Auto-scale text to fit boxes" @@ -6207,6 +6209,24 @@ descriptionInline = "Tip: Hold Ctrl (Cmd) or Shift to multi-select text boxes. A 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." +[pdfTextEditor.options.advanced] +title = "Advanced Settings" + +[pdfTextEditor.tooltip.header] +title = "Preview Limitations" + +[pdfTextEditor.tooltip.textFocus] +title = "Text and Image Focus" +text = "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." + +[pdfTextEditor.tooltip.previewVariance] +title = "Preview Variance" +text = "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." + +[pdfTextEditor.tooltip.alpha] +title = "Alpha Viewer" +text = "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." + [pdfTextEditor.manual] mergeTooltip = "Merge selected boxes" merge = "Merge selection" @@ -6226,8 +6246,8 @@ alpha = "This alpha viewer is still evolving—certain fonts, colours, transpare [pdfTextEditor.empty] title = "No document loaded" subtitle = "Load a PDF or JSON file to begin editing text content." -dropzone = "Drag and drop a PDF or JSON file here, or click to browse" -dropzoneWithFiles = "Select a file from the Files tab, or drag and drop a PDF or JSON file here, or click to browse" +dropzone = "Drag and drop a PDF here, or click to browse" +dropzoneWithFiles = "Select a file from the Files tab, or drag and drop a PDF here, or click to browse" [pdfTextEditor.welcomeBanner] title = "Welcome to PDF Text Editor (Early Access)" diff --git a/frontend/src/core/components/tools/pdfTextEditor/FontStatusPanel.tsx b/frontend/src/core/components/tools/pdfTextEditor/FontStatusPanel.tsx index 0ba9a4665..2c600c231 100644 --- a/frontend/src/core/components/tools/pdfTextEditor/FontStatusPanel.tsx +++ b/frontend/src/core/components/tools/pdfTextEditor/FontStatusPanel.tsx @@ -1,10 +1,11 @@ import React, { useMemo, useState } from 'react'; import { - Accordion, Badge, Box, Code, Collapse, + Divider, + Flex, Group, List, Paper, @@ -29,10 +30,14 @@ import { getFontStatusColor, getFontStatusDescription, } from '@app/tools/pdfTextEditor/fontAnalysis'; +import LocalIcon from '@app/components/shared/LocalIcon'; +import { Tooltip as CustomTooltip } from '@app/components/shared/Tooltip'; interface FontStatusPanelProps { document: PdfJsonDocument | null; pageIndex?: number; + isCollapsed?: boolean; + onCollapsedChange?: (collapsed: boolean) => void; } const FontStatusBadge = ({ analysis }: { analysis: FontAnalysis }) => { @@ -74,25 +79,31 @@ const FontDetailItem = ({ analysis }: { analysis: FontAnalysis }) => { const [expanded, setExpanded] = useState(false); return ( - setExpanded(!expanded)}> + setExpanded(!expanded)}> - - - - - {analysis.baseName} - + + + + + + {analysis.baseName} + + {analysis.isSubset && ( - + subset )} - + {expanded ? : } - + @@ -165,7 +176,12 @@ const FontDetailItem = ({ analysis }: { analysis: FontAnalysis }) => { ); }; -const FontStatusPanel: React.FC = ({ document, pageIndex }) => { +const FontStatusPanel: React.FC = ({ + document, + pageIndex, + isCollapsed = false, + onCollapsedChange +}) => { const { t } = useTranslation(); const fontAnalysis: DocumentFontAnalysis = useMemo( @@ -175,16 +191,6 @@ const FontStatusPanel: React.FC = ({ document, pageIndex } const { canReproducePerfectly, hasWarnings, summary, fonts } = fontAnalysis; - const statusIcon = useMemo(() => { - if (canReproducePerfectly) { - return ; - } - if (hasWarnings) { - return ; - } - return ; - }, [canReproducePerfectly, hasWarnings]); - // Early return AFTER all hooks are declared if (!document || fontAnalysis.fonts.length === 0) { return null; @@ -197,37 +203,46 @@ const FontStatusPanel: React.FC = ({ document, pageIndex } : t('pdfTextEditor.fontAnalysis.allFonts', 'All fonts'); return ( - - - - - - {statusIcon} - - {pageLabel} - - - {fonts.length} - - +
+
+ {/* Header - matches ToolStep style */} + onCollapsedChange?.(!isCollapsed)} + > + + + {pageLabel} + + + {fonts.length} + + - {/* Warning badges BEFORE expansion */} - - {summary.systemFallback > 0 && ( - }> - {summary.systemFallback} {t('pdfTextEditor.fontAnalysis.fallback', 'fallback')} - - )} - {summary.missing > 0 && ( - }> - {summary.missing} {t('pdfTextEditor.fontAnalysis.missing', 'missing')} - - )} - - - - - + {isCollapsed ? ( + + ) : ( + + )} + + + {/* Content */} + {!isCollapsed && ( + {/* Overall Status Message */} {canReproducePerfectly @@ -277,9 +292,10 @@ const FontStatusPanel: React.FC = ({ document, pageIndex } ))} - - - + )} +
+ +
); }; diff --git a/frontend/src/core/components/tools/pdfTextEditor/PdfTextEditorSidebar.tsx b/frontend/src/core/components/tools/pdfTextEditor/PdfTextEditorSidebar.tsx new file mode 100644 index 000000000..6ad1fec33 --- /dev/null +++ b/frontend/src/core/components/tools/pdfTextEditor/PdfTextEditorSidebar.tsx @@ -0,0 +1,340 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { + ActionIcon, + Badge, + Button, + Divider, + Flex, + Group, + Menu, + Modal, + ScrollArea, + SegmentedControl, + Stack, + Switch, + Text, +} from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import AutorenewIcon from '@mui/icons-material/Autorenew'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; +import FileDownloadIcon from '@mui/icons-material/FileDownloadOutlined'; + +import { PdfTextEditorViewData, TextGroup } from '@app/tools/pdfTextEditor/pdfTextEditorTypes'; +import { pageDimensions } from '@app/tools/pdfTextEditor/pdfTextEditorUtils'; +import FontStatusPanel from '@app/components/tools/pdfTextEditor/FontStatusPanel'; +import ToolStep from '@app/components/tools/shared/ToolStep'; +import { usePdfTextEditorTips } from '@app/components/tooltips/usePdfTextEditorTips'; +import { Tooltip } from '@app/components/shared/Tooltip'; +import LocalIcon from '@app/components/shared/LocalIcon'; + +type GroupingMode = 'auto' | 'paragraph' | 'singleLine'; + +interface PdfTextEditorSidebarProps { + data: PdfTextEditorViewData; +} + +// Analyze page content to determine if it's paragraph-heavy +const analyzePageContentType = ( + groups: TextGroup[], + pageWidth: number, +): boolean => { + if (groups.length < 3) { + return false; + } + + const widths = groups.map((g) => Math.max(g.bounds.right - g.bounds.left, 1)); + const avgWidth = widths.reduce((sum, w) => sum + w, 0) / widths.length; + const stdDev = Math.sqrt( + widths.reduce((sum, w) => sum + Math.pow(w - avgWidth, 2), 0) / widths.length, + ); + const coefficientOfVariation = avgWidth > 0 ? stdDev / avgWidth : 0; + const fullWidthRatio = widths.filter((w) => w > pageWidth * 0.65).length / widths.length; + + const criterion1 = groups.length >= 3; + const criterion2 = avgWidth > pageWidth * 0.3; + const criterion3 = coefficientOfVariation > 0.5 || fullWidthRatio > 0.6; + + return criterion1 && criterion2 && criterion3; +}; + +const PdfTextEditorSidebar = ({ data }: PdfTextEditorSidebarProps) => { + const { t } = useTranslation(); + const [pendingModeChange, setPendingModeChange] = useState(null); + const [advancedSettingsCollapsed, setAdvancedSettingsCollapsed] = useState(false); + const [fontsCollapsed, setFontsCollapsed] = useState(false); + const pdfTextEditorTips = usePdfTextEditorTips(); + + const { + document: pdfDocument, + groupsByPage, + hasDocument, + hasChanges, + fileName, + isGeneratingPdf, + isSavingToWorkbench, + isConverting, + forceSingleTextElement, + groupingMode: externalGroupingMode, + autoScaleText, + selectedPage, + onReset, + onGeneratePdf, + onSaveToWorkbench, + onForceSingleTextElementChange, + onGroupingModeChange, + onAutoScaleTextChange, + } = data; + + // Get page dimensions + const pages = pdfDocument?.pages ?? []; + const currentPage = pages[selectedPage] ?? null; + const { width: pageWidth } = pageDimensions(currentPage); + const pageGroups = groupsByPage[selectedPage] ?? []; + + // Detect if current page contains paragraph-heavy content + const isParagraphPage = useMemo(() => { + return analyzePageContentType(pageGroups, pageWidth); + }, [pageGroups, pageWidth]); + + const handleModeChangeRequest = useCallback((newMode: GroupingMode) => { + if (hasChanges && newMode !== externalGroupingMode) { + setPendingModeChange(newMode); + } else { + onGroupingModeChange(newMode); + } + }, [hasChanges, externalGroupingMode, onGroupingModeChange]); + + const handleConfirmModeChange = useCallback(() => { + if (pendingModeChange) { + onGroupingModeChange(pendingModeChange); + setPendingModeChange(null); + } + }, [pendingModeChange, onGroupingModeChange]); + + const handleCancelModeChange = useCallback(() => { + setPendingModeChange(null); + }, []); + + return ( + <> + + + + + {/* Title row with ALPHA badge and info tooltip */} + + + + {t('pdfTextEditor.title', 'PDF Text Editor')} + + + {t('toolPanel.alpha', 'Alpha')} + + + + + + + + + + {fileName && ( + + {t('pdfTextEditor.currentFile', 'Current file: {{name}}', { name: fileName })} + + )} + + + setAdvancedSettingsCollapsed(!advancedSettingsCollapsed)} + > + + + + + + + + + + + {t('pdfTextEditor.options.autoScaleText.title', 'Auto-scale text to fit boxes')} + + + onAutoScaleTextChange(event.currentTarget.checked)} + /> + + + + + + + + {t('pdfTextEditor.options.groupingMode.title', 'Text Grouping Mode')} + + {externalGroupingMode === 'auto' && isParagraphPage && ( + + {t('pdfTextEditor.pageType.paragraph', 'Paragraph page')} + + )} + {externalGroupingMode === 'auto' && !isParagraphPage && hasDocument && ( + + {t('pdfTextEditor.pageType.sparse', 'Sparse text')} + + )} + + + {externalGroupingMode === 'auto' + ? t( + 'pdfTextEditor.options.groupingMode.autoDescription', + 'Automatically detects page type and groups text appropriately.' + ) + : externalGroupingMode === 'paragraph' + ? t( + 'pdfTextEditor.options.groupingMode.paragraphDescription', + 'Groups aligned lines into multi-line paragraph text boxes.' + ) + : t( + 'pdfTextEditor.options.groupingMode.singleLineDescription', + 'Keeps each PDF text line as a separate text box.' + )} + + handleModeChangeRequest(value as GroupingMode)} + data={[ + { label: t('pdfTextEditor.groupingMode.auto', 'Auto'), value: 'auto' }, + { label: t('pdfTextEditor.groupingMode.paragraph', 'Paragraph'), value: 'paragraph' }, + { label: t('pdfTextEditor.groupingMode.singleLine', 'Single Line'), value: 'singleLine' }, + ]} + fullWidth + /> + + + + + + + + + + + + + {t('pdfTextEditor.options.forceSingleElement.title', 'Lock edited text to a single PDF element')} + + + onForceSingleTextElementChange(event.currentTarget.checked)} + /> + + + + + {hasDocument && ( + + )} + + + + + + + + + + + + + } + onClick={() => onGeneratePdf()} + disabled={!hasChanges || isGeneratingPdf} + > + {t('pdfTextEditor.actions.downloadCopy', 'Download Copy')} + + } + onClick={onReset} + color="red" + > + {t('pdfTextEditor.actions.reset', 'Reset Changes')} + + + + + + + {/* Mode Change Confirmation Modal */} + + + + {t( + 'pdfTextEditor.modeChange.warning', + 'Changing the text grouping mode will reset all unsaved changes. Are you sure you want to continue?' + )} + + + + + + + + + ); +}; + +export default PdfTextEditorSidebar; diff --git a/frontend/src/core/components/tools/pdfTextEditor/PdfTextEditorView.tsx b/frontend/src/core/components/tools/pdfTextEditor/PdfTextEditorView.tsx index 3c5b45e0e..99cd6ad99 100644 --- a/frontend/src/core/components/tools/pdfTextEditor/PdfTextEditorView.tsx +++ b/frontend/src/core/components/tools/pdfTextEditor/PdfTextEditorView.tsx @@ -1,6 +1,5 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { - Accordion, ActionIcon, Alert, Badge, @@ -14,18 +13,12 @@ import { Pagination, Progress, ScrollArea, - SegmentedControl, Stack, - Switch, Text, - Title, Tooltip, } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { useTranslation } from 'react-i18next'; -import DescriptionIcon from '@mui/icons-material/DescriptionOutlined'; -import FileDownloadIcon from '@mui/icons-material/FileDownloadOutlined'; -import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdfOutlined'; import AutorenewIcon from '@mui/icons-material/Autorenew'; import WarningAmberIcon from '@mui/icons-material/WarningAmber'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; @@ -34,7 +27,6 @@ import MergeTypeIcon from '@mui/icons-material/MergeType'; import CallSplitIcon from '@mui/icons-material/CallSplit'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import UploadFileIcon from '@mui/icons-material/UploadFileOutlined'; -import SaveIcon from '@mui/icons-material/SaveOutlined'; import { Rnd } from 'react-rnd'; import NavigationWarningModal from '@app/components/shared/NavigationWarningModal'; @@ -46,7 +38,6 @@ import { TextGroup, } from '@app/tools/pdfTextEditor/pdfTextEditorTypes'; import { getImageBounds, pageDimensions } from '@app/tools/pdfTextEditor/pdfTextEditorUtils'; -import FontStatusPanel from '@app/components/tools/pdfTextEditor/FontStatusPanel'; const MAX_RENDER_WIDTH = 820; const MIN_BOX_SIZE = 18; @@ -313,8 +304,6 @@ const analyzePageContentType = (groups: TextGroup[], pageWidth: number): boolean return isParagraphPage; }; -type GroupingMode = 'auto' | 'paragraph' | 'singleLine'; - const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => { const { t } = useTranslation(); const { activeFiles } = useFileContext(); @@ -327,9 +316,7 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => { 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()); - const [pendingModeChange, setPendingModeChange] = useState(null); const measurementKeyRef = useRef(''); const containerRef = useRef(null); const editorRefs = useRef>(new Map()); @@ -378,27 +365,27 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => { dirtyPages, hasDocument, hasVectorPreview, - fileName, + fileName: _fileName, errorMessage, - isGeneratingPdf, - isSavingToWorkbench, + isGeneratingPdf: _isGeneratingPdf, + isSavingToWorkbench: _isSavingToWorkbench, isConverting, conversionProgress, - hasChanges, - forceSingleTextElement, + hasChanges: _hasChanges, + forceSingleTextElement: _forceSingleTextElement, groupingMode: externalGroupingMode, + autoScaleText, requestPagePreview, onSelectPage, onGroupEdit, onGroupDelete, onImageTransform, onImageReset, - onReset, - onDownloadJson, - onGeneratePdf, + onReset: _onReset, + onGeneratePdf: _onGeneratePdf, onSaveToWorkbench, - onForceSingleTextElementChange, - onGroupingModeChange, + onForceSingleTextElementChange: _onForceSingleTextElementChange, + onGroupingModeChange: _onGroupingModeChange, onMergeGroups, onUngroupGroup, onLoadFile, @@ -428,27 +415,6 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => { } : null, }); - const handleModeChangeRequest = useCallback((newMode: GroupingMode) => { - if (hasChanges && newMode !== externalGroupingMode) { - // Show confirmation dialog - setPendingModeChange(newMode); - } else { - // No changes, switch immediately - onGroupingModeChange(newMode); - } - }, [hasChanges, externalGroupingMode, onGroupingModeChange]); - - const handleConfirmModeChange = useCallback(() => { - if (pendingModeChange) { - onGroupingModeChange(pendingModeChange); - setPendingModeChange(null); - } - }, [pendingModeChange, onGroupingModeChange]); - - const handleCancelModeChange = useCallback(() => { - setPendingModeChange(null); - }, []); - const clearSelection = useCallback(() => { setSelectedGroupIds(new Set()); lastSelectedGroupIdRef.current = null; @@ -1448,223 +1414,16 @@ const selectionToolbarPosition = useMemo(() => { padding: '1.5rem', overflow: 'hidden', height: '100%', - display: 'grid', - gridTemplateColumns: 'minmax(0, 1fr) 320px', - gridTemplateRows: '1fr', - alignItems: hasDocument ? 'start' : 'stretch', - gap: '1.5rem', + display: 'flex', + flexDirection: 'column', }} > - - - - - - - {t('pdfTextEditor.title', 'PDF JSON Editor')} - {hasChanges && {t('pdfTextEditor.badges.unsaved', 'Edited')}} - - - - - - - - - - - {fileName && ( - - {t('pdfTextEditor.currentFile', 'Current file: {{name}}', { name: fileName })} - - )} - - - - -
- - {t('pdfTextEditor.options.autoScaleText.title', 'Auto-scale text to fit boxes')} - - - {t( - 'pdfTextEditor.options.autoScaleText.description', - 'Automatically scales text horizontally to fit within its original bounding box when font rendering differs from PDF.' - )} - -
- setAutoScaleText(event.currentTarget.checked)} - /> -
- - - - - {t('pdfTextEditor.options.groupingMode.title', 'Text Grouping Mode')} - - {externalGroupingMode === 'auto' && isParagraphPage && ( - - {t('pdfTextEditor.pageType.paragraph', 'Paragraph page')} - - )} - {externalGroupingMode === 'auto' && !isParagraphPage && hasDocument && ( - - {t('pdfTextEditor.pageType.sparse', 'Sparse text')} - - )} - - - {externalGroupingMode === 'auto' - ? t( - 'pdfTextEditor.options.groupingMode.autoDescription', - 'Automatically detects page type and groups text appropriately.' - ) - : externalGroupingMode === 'paragraph' - ? t( - 'pdfTextEditor.options.groupingMode.paragraphDescription', - 'Groups aligned lines into multi-line paragraph text boxes.' - ) - : t( - 'pdfTextEditor.options.groupingMode.singleLineDescription', - 'Keeps each PDF text line as a separate text box.' - )} - - handleModeChangeRequest(value as GroupingMode)} - data={[ - { label: t('pdfTextEditor.groupingMode.auto', 'Auto'), value: 'auto' }, - { label: t('pdfTextEditor.groupingMode.paragraph', 'Paragraph'), value: 'paragraph' }, - { label: t('pdfTextEditor.groupingMode.singleLine', 'Single Line'), value: 'singleLine' }, - ]} - fullWidth - /> - - - - {t( - 'pdfTextEditor.options.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.', - )} - - - -
- - {t('pdfTextEditor.options.forceSingleElement.title', 'Lock edited text to a single PDF element')} - - - {t( - 'pdfTextEditor.options.forceSingleElement.description', - 'When enabled, the editor exports each edited text box as one PDF text element to avoid overlapping glyphs or mixed fonts.' - )} - -
- onForceSingleTextElementChange(event.currentTarget.checked)} - /> -
- - - - - - - - - - {t('pdfTextEditor.disclaimer.heading', 'Preview Limitations')} - - - - - - - {t( - 'pdfTextEditor.disclaimer.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.' - )} - - - {t( - 'pdfTextEditor.disclaimer.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.' - )} - - - {t( - 'pdfTextEditor.disclaimer.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.' - )} - - - - - - - {hasDocument && } -
-
-
- {errorMessage && ( } color="red" radius="md" - style={{ gridColumn: '2 / 3' }} + mb="md" > {errorMessage} @@ -1674,7 +1433,7 @@ const selectionToolbarPosition = useMemo(() => { { @@ -1704,8 +1463,8 @@ const selectionToolbarPosition = useMemo(() => { {activeFiles.length > 0 - ? t('pdfTextEditor.empty.dropzoneWithFiles', 'Select a file from the Files tab, or drag and drop a PDF or JSON file here, or click to browse') - : t('pdfTextEditor.empty.dropzone', 'Drag and drop a PDF or JSON file here, or click to browse')} + ? t('pdfTextEditor.empty.dropzoneWithFiles', 'Select a file from the Files tab, or drag and drop a PDF here, or click to browse') + : t('pdfTextEditor.empty.dropzone', 'Drag and drop a PDF here, or click to browse')} @@ -1713,7 +1472,7 @@ const selectionToolbarPosition = useMemo(() => { )} {isConverting && ( - +
@@ -1748,10 +1507,8 @@ const selectionToolbarPosition = useMemo(() => { gap="lg" className="flex-1" style={{ - gridColumn: '1 / 2', - gridRow: 1, minHeight: 0, - height: 'calc(100vh - 3rem)', + flex: 1, overflow: 'hidden', }} > @@ -1788,65 +1545,208 @@ const selectionToolbarPosition = useMemo(() => { title={ - {t('pdfTextEditor.welcomeBanner.title', 'Welcome to PDF Text Editor (Early Access)')} + + {t( + 'pdfTextEditor.welcomeBanner.title', + 'Welcome to PDF Text Editor (Early Access)', + )} + } centered size="lg" + scrollAreaComponent={Box} > - - +
+ {/* Header (fixed) */} +
- {t('pdfTextEditor.welcomeBanner.experimental', 'This is an experimental feature in active development. Expect some instability and issues during use.')} + {t( + 'pdfTextEditor.welcomeBanner.experimental', + 'This is an experimental feature in active development. Expect some instability and issues during use.', + )} - {t('pdfTextEditor.welcomeBanner.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.')} + {t( + 'pdfTextEditor.welcomeBanner.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.', + )} +
+ + {/* Body (scrollable) */} +
+
+ + + {t('pdfTextEditor.welcomeBanner.bestFor', 'Works Best With:')} + + +
  • + {t( + 'pdfTextEditor.welcomeBanner.bestFor1', + 'Simple PDFs containing primarily text and images', + )} +
  • +
  • + {t( + 'pdfTextEditor.welcomeBanner.bestFor2', + 'Documents with standard paragraph formatting', + )} +
  • +
  • + {t( + 'pdfTextEditor.welcomeBanner.bestFor3', + 'Letters, essays, reports, and basic documents', + )} +
  • +
    + + + {t('pdfTextEditor.welcomeBanner.notIdealFor', 'Not Ideal For:')} + + +
  • + {t( + 'pdfTextEditor.welcomeBanner.notIdealFor1', + 'PDFs with special formatting like bullet points, tables, or multi-column layouts', + )} +
  • +
  • + {t( + 'pdfTextEditor.welcomeBanner.notIdealFor2', + 'Magazines, brochures, or heavily designed documents', + )} +
  • +
  • + {t( + 'pdfTextEditor.welcomeBanner.notIdealFor3', + 'Instruction manuals with complex layouts', + )} +
  • +
    + + + {t('pdfTextEditor.welcomeBanner.limitations', 'Current Limitations:')} + + +
  • + {t( + 'pdfTextEditor.welcomeBanner.limitation1', + 'Font rendering may differ slightly from the original PDF', + )} +
  • +
  • + {t( + 'pdfTextEditor.welcomeBanner.limitation2', + 'Complex graphics, form fields, and annotations are preserved but not editable', + )} +
  • +
  • + {t( + 'pdfTextEditor.welcomeBanner.limitation3', + 'Large files may take time to convert and process', + )} +
  • +
    + + + {t('pdfTextEditor.welcomeBanner.knownIssues', 'Known Issues (Being Fixed):')} + + +
  • + {t( + 'pdfTextEditor.welcomeBanner.issue1', + 'Text colour is not currently preserved (will be added soon)', + )} +
  • +
  • + {t( + 'pdfTextEditor.welcomeBanner.issue2', + 'Paragraph mode has more alignment and spacing issues - Single Line mode recommended', + )} +
  • +
  • + {t( + 'pdfTextEditor.welcomeBanner.issue3', + 'The preview display differs from the exported PDF - exported PDFs are closer to the original', + )} +
  • +
  • + {t( + 'pdfTextEditor.welcomeBanner.issue4', + 'Rotated text alignment may need manual adjustment', + )} +
  • +
  • + {t( + 'pdfTextEditor.welcomeBanner.issue5', + 'Transparency and layering effects may vary from original', + )} +
  • +
    +
    +
    + + {/* Footer (fixed) */} +
    - - {t('pdfTextEditor.welcomeBanner.bestFor', 'Works Best With:')} + + {t( + 'pdfTextEditor.welcomeBanner.feedback', + 'This is an early access feature. Please report any issues you encounter to help us improve!', + )} - -
  • {t('pdfTextEditor.welcomeBanner.bestFor1', 'Simple PDFs containing primarily text and images')}
  • -
  • {t('pdfTextEditor.welcomeBanner.bestFor2', 'Documents with standard paragraph formatting')}
  • -
  • {t('pdfTextEditor.welcomeBanner.bestFor3', 'Letters, essays, reports, and basic documents')}
  • -
    - - - {t('pdfTextEditor.welcomeBanner.notIdealFor', 'Not Ideal For:')} - - -
  • {t('pdfTextEditor.welcomeBanner.notIdealFor1', 'PDFs with special formatting like bullet points, tables, or multi-column layouts')}
  • -
  • {t('pdfTextEditor.welcomeBanner.notIdealFor2', 'Magazines, brochures, or heavily designed documents')}
  • -
  • {t('pdfTextEditor.welcomeBanner.notIdealFor3', 'Instruction manuals with complex layouts')}
  • -
    - - - {t('pdfTextEditor.welcomeBanner.limitations', 'Current Limitations:')} - - -
  • {t('pdfTextEditor.welcomeBanner.limitation1', 'Font rendering may differ slightly from the original PDF')}
  • -
  • {t('pdfTextEditor.welcomeBanner.limitation2', 'Complex graphics, form fields, and annotations are preserved but not editable')}
  • -
  • {t('pdfTextEditor.welcomeBanner.limitation3', 'Large files may take time to convert and process')}
  • -
    - - - {t('pdfTextEditor.welcomeBanner.knownIssues', 'Known Issues (Being Fixed):')} - - -
  • {t('pdfTextEditor.welcomeBanner.issue1', 'Text colour is not currently preserved (will be added soon)')}
  • -
  • {t('pdfTextEditor.welcomeBanner.issue2', 'Paragraph mode has more alignment and spacing issues - Single Line mode recommended')}
  • -
  • {t('pdfTextEditor.welcomeBanner.issue3', 'The preview display differs from the exported PDF - exported PDFs are closer to the original')}
  • -
  • {t('pdfTextEditor.welcomeBanner.issue4', 'Rotated text alignment may need manual adjustment')}
  • -
  • {t('pdfTextEditor.welcomeBanner.issue5', 'Transparency and layering effects may vary from original')}
  • -
    - - - {t('pdfTextEditor.welcomeBanner.feedback', 'This is an early access feature. Please report any issues you encounter to help us improve!')} - - + @@ -1854,8 +1754,8 @@ const selectionToolbarPosition = useMemo(() => { {t('pdfTextEditor.welcomeBanner.dontShowAgain', "Don't show again")} - - +
    +
    {
    )} - {/* Mode Change Confirmation Modal */} - - - - {t( - 'pdfTextEditor.modeChange.warning', - 'Changing the text grouping mode will reset all unsaved changes. Are you sure you want to continue?' - )} - - - - - - - - {/* Navigation Warning Modal */} { + const { t } = useTranslation(); + + return { + header: { + title: t('pdfTextEditor.tooltip.header.title', 'Preview Limitations'), + }, + tips: [ + { + title: t('pdfTextEditor.tooltip.textFocus.title', 'Text and Image Focus'), + description: t( + 'pdfTextEditor.tooltip.textFocus.text', + '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.' + ), + }, + { + title: t('pdfTextEditor.tooltip.previewVariance.title', 'Preview Variance'), + description: t( + 'pdfTextEditor.tooltip.previewVariance.text', + '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.' + ), + }, + { + title: t('pdfTextEditor.tooltip.alpha.title', 'Alpha Viewer'), + description: t( + 'pdfTextEditor.tooltip.alpha.text', + '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.' + ), + }, + ], + }; +}; + diff --git a/frontend/src/core/tools/pdfTextEditor/PdfTextEditor.tsx b/frontend/src/core/tools/pdfTextEditor/PdfTextEditor.tsx index 422b06603..25309c0cf 100644 --- a/frontend/src/core/tools/pdfTextEditor/PdfTextEditor.tsx +++ b/frontend/src/core/tools/pdfTextEditor/PdfTextEditor.tsx @@ -34,6 +34,7 @@ import { valueOr, } from '@app/tools/pdfTextEditor/pdfTextEditorUtils'; import PdfTextEditorView from '@app/components/tools/pdfTextEditor/PdfTextEditorView'; +import PdfTextEditorSidebar from '@app/components/tools/pdfTextEditor/PdfTextEditorSidebar'; import type { PDFDocumentProxy } from 'pdfjs-dist'; const WORKBENCH_VIEW_ID = 'pdfTextEditorWorkbench'; @@ -228,6 +229,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => { const [groupingMode, setGroupingMode] = useState<'auto' | 'paragraph' | 'singleLine'>('auto'); const [hasVectorPreview, setHasVectorPreview] = useState(false); const [pagePreviews, setPagePreviews] = useState>(new Map()); + const [autoScaleText, setAutoScaleText] = useState(true); // Lazy loading state const [isLazyMode, setIsLazyMode] = useState(false); @@ -1565,6 +1567,8 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => { hasChanges, forceSingleTextElement, groupingMode, + autoScaleText, + onAutoScaleTextChange: setAutoScaleText, requestPagePreview, onSelectPage: handleSelectPage, onGroupEdit: handleGroupTextChange, @@ -1613,6 +1617,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => { selectedPage, forceSingleTextElement, groupingMode, + autoScaleText, requestPagePreview, setForceSingleTextElement, handleLoadFileFromDropzone, @@ -1683,12 +1688,17 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => { icon: , component: PdfTextEditorView, }); - setLeftPanelView('hidden'); setCustomWorkbenchViewData(WORKBENCH_VIEW_ID, latestViewDataRef.current); + + return () => { + // Clear backend cache if we were using lazy loading + clearCachedJob(cachedJobIdRef.current); + clearCustomWorkbenchViewData(WORKBENCH_VIEW_ID); + unregisterCustomWorkbenchView(WORKBENCH_VIEW_ID); + }; }, [ registerCustomWorkbenchView, setCustomWorkbenchViewData, - setLeftPanelView, viewLabel, ]); @@ -1723,12 +1733,12 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => { setCustomWorkbenchViewData(WORKBENCH_VIEW_ID, viewData); }, [setCustomWorkbenchViewData, viewData]); - // All editing happens in the custom workbench view. - return null; + // Render the sidebar with settings while editing happens in the custom workbench view. + return ; }; (PdfTextEditor as ToolComponent).tool = () => { - throw new Error('PDF JSON Editor does not support automation operations.'); + throw new Error('PDF Text Editor does not support automation operations.'); }; (PdfTextEditor as ToolComponent).getDefaultParameters = () => ({ diff --git a/frontend/src/core/tools/pdfTextEditor/pdfTextEditorTypes.ts b/frontend/src/core/tools/pdfTextEditor/pdfTextEditorTypes.ts index 3dd45a465..aab065a11 100644 --- a/frontend/src/core/tools/pdfTextEditor/pdfTextEditorTypes.ts +++ b/frontend/src/core/tools/pdfTextEditor/pdfTextEditorTypes.ts @@ -201,6 +201,8 @@ export interface PdfTextEditorViewData { hasChanges: boolean; forceSingleTextElement: boolean; groupingMode: 'auto' | 'paragraph' | 'singleLine'; + autoScaleText: boolean; + onAutoScaleTextChange: (value: boolean) => void; requestPagePreview: (pageIndex: number, scale: number) => void; onSelectPage: (pageIndex: number) => void; onGroupEdit: (pageIndex: number, groupId: string, value: string) => void;