This commit is contained in:
EthanHealy01 2025-12-18 17:21:49 +00:00 committed by GitHub
commit 57a983238c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 696 additions and 397 deletions

View File

@ -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)"

View File

@ -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 (
<Paper withBorder p="xs" style={{ cursor: 'pointer' }} onClick={() => setExpanded(!expanded)}>
<Paper withBorder px="sm" py="md" style={{ cursor: 'pointer' }} onClick={() => setExpanded(!expanded)}>
<Stack gap={4}>
<Group justify="space-between">
<Group gap={4}>
<FontDownloadIcon sx={{ fontSize: 16 }} />
<Text size="xs" fw={500} lineClamp={1}>
{analysis.baseName}
</Text>
<Flex align="center" justify="space-between" wrap="nowrap">
<Group gap={4} wrap="nowrap" style={{ flex: 1, minWidth: 0 }}>
<FontDownloadIcon sx={{ fontSize: 16, flexShrink: 0 }} />
<CustomTooltip
sidebarTooltip={false}
content={analysis.baseName}
position="top"
>
<Text size="xs" fw={500} lineClamp={1} style={{ flex: 1, minWidth: 0 }}>
{analysis.baseName}
</Text>
</CustomTooltip>
{analysis.isSubset && (
<Badge size="xs" color="gray" variant="outline">
<Badge size="xs" color="gray" variant="outline" style={{ flexShrink: 0 }}>
subset
</Badge>
)}
</Group>
<Group gap={4}>
<Group gap={4} wrap="nowrap" style={{ flexShrink: 0 }}>
<FontStatusBadge analysis={analysis} />
{expanded ? <ExpandLessIcon sx={{ fontSize: 16 }} /> : <ExpandMoreIcon sx={{ fontSize: 16 }} />}
</Group>
</Group>
</Flex>
<Collapse in={expanded}>
<Stack gap={4} mt={4}>
@ -165,7 +176,12 @@ const FontDetailItem = ({ analysis }: { analysis: FontAnalysis }) => {
);
};
const FontStatusPanel: React.FC<FontStatusPanelProps> = ({ document, pageIndex }) => {
const FontStatusPanel: React.FC<FontStatusPanelProps> = ({
document,
pageIndex,
isCollapsed = false,
onCollapsedChange
}) => {
const { t } = useTranslation();
const fontAnalysis: DocumentFontAnalysis = useMemo(
@ -175,16 +191,6 @@ const FontStatusPanel: React.FC<FontStatusPanelProps> = ({ document, pageIndex }
const { canReproducePerfectly, hasWarnings, summary, fonts } = fontAnalysis;
const statusIcon = useMemo(() => {
if (canReproducePerfectly) {
return <CheckCircleIcon sx={{ fontSize: 16 }} />;
}
if (hasWarnings) {
return <WarningIcon sx={{ fontSize: 16 }} />;
}
return <InfoIcon sx={{ fontSize: 16 }} />;
}, [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<FontStatusPanelProps> = ({ document, pageIndex }
: t('pdfTextEditor.fontAnalysis.allFonts', 'All fonts');
return (
<Accordion variant="contained" defaultValue={hasWarnings ? 'fonts' : undefined}>
<Accordion.Item value="fonts">
<Accordion.Control>
<Group gap="xs" wrap="wrap" style={{ flex: 1 }}>
<Group gap="xs" wrap="nowrap">
{statusIcon}
<Text size="sm" fw={500}>
{pageLabel}
</Text>
<Badge size="xs" color={statusColor} variant="dot">
{fonts.length}
</Badge>
</Group>
<div>
<div
style={{
padding: '0.5rem',
opacity: isCollapsed ? 0.8 : 1,
color: isCollapsed ? 'var(--mantine-color-dimmed)' : 'inherit',
transition: 'opacity 0.2s ease, color 0.2s ease'
}}
>
{/* Header - matches ToolStep style */}
<Flex
align="center"
justify="space-between"
mb={isCollapsed ? 0 : 'sm'}
style={{ cursor: 'pointer' }}
onClick={() => onCollapsedChange?.(!isCollapsed)}
>
<Flex align="center" gap="xs">
<Text fw={500} size="sm">
{pageLabel}
</Text>
<Badge size="xs" color={statusColor} variant="dot">
{fonts.length}
</Badge>
</Flex>
{/* Warning badges BEFORE expansion */}
<Group gap={4} wrap="wrap">
{summary.systemFallback > 0 && (
<Badge size="xs" color="yellow" variant="filled" leftSection={<WarningIcon sx={{ fontSize: 12 }} />}>
{summary.systemFallback} {t('pdfTextEditor.fontAnalysis.fallback', 'fallback')}
</Badge>
)}
{summary.missing > 0 && (
<Badge size="xs" color="red" variant="filled" leftSection={<ErrorIcon sx={{ fontSize: 12 }} />}>
{summary.missing} {t('pdfTextEditor.fontAnalysis.missing', 'missing')}
</Badge>
)}
</Group>
</Group>
</Accordion.Control>
<Accordion.Panel>
<Stack gap="xs">
{isCollapsed ? (
<LocalIcon icon="chevron-right-rounded" width="1.2rem" height="1.2rem" style={{
color: 'var(--mantine-color-dimmed)'
}} />
) : (
<LocalIcon icon="expand-more-rounded" width="1.2rem" height="1.2rem" style={{
color: 'var(--mantine-color-dimmed)'
}} />
)}
</Flex>
{/* Content */}
{!isCollapsed && (
<Stack gap="xs" pl="sm">
{/* Overall Status Message */}
<Text size="xs" c="dimmed">
{canReproducePerfectly
@ -277,9 +292,10 @@ const FontStatusPanel: React.FC<FontStatusPanelProps> = ({ document, pageIndex }
))}
</Stack>
</Stack>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
)}
</div>
<Divider style={{ color: '#E2E8F0', marginLeft: '1rem', marginRight: '-0.5rem' }} />
</div>
);
};

View File

@ -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<GroupingMode | null>(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 (
<>
<Stack style={{ height: '100%', display: 'flex' }} gap={0}>
<ScrollArea style={{ flex: 1 }} offsetScrollbars>
<Stack gap="md">
<Stack gap="xs" pl="md" pr={0} pt="md">
{/* Title row with ALPHA badge and info tooltip */}
<Flex align="center" justify="space-between">
<Flex align="center" gap="xs">
<Text fw={600} size="sm">
{t('pdfTextEditor.title', 'PDF Text Editor')}
</Text>
<Badge size="xs" variant="light" color="orange">
{t('toolPanel.alpha', 'Alpha')}
</Badge>
</Flex>
<Tooltip
sidebarTooltip={true}
tips={pdfTextEditorTips.tips}
header={pdfTextEditorTips.header}
pinOnClick
>
<ActionIcon variant="subtle" color="blue" size="sm">
<LocalIcon icon="info-outline-rounded" width="1.25rem" height="1.25rem" />
</ActionIcon>
</Tooltip>
</Flex>
{fileName && (
<Text size="sm" c="dimmed">
{t('pdfTextEditor.currentFile', 'Current file: {{name}}', { name: fileName })}
</Text>
)}
</Stack>
<ToolStep
title={t('pdfTextEditor.options.advanced.title', 'Advanced Settings')}
isCollapsed={advancedSettingsCollapsed}
onCollapsedClick={() => setAdvancedSettingsCollapsed(!advancedSettingsCollapsed)}
>
<Stack gap="md">
<Divider />
<Group justify="space-between" align="center">
<Group gap={4} align="center" style={{ flex: 1, minWidth: 0 }}>
<Tooltip
sidebarTooltip={false}
content={t(
'pdfTextEditor.options.autoScaleText.description',
'Automatically scales text horizontally to fit within its original bounding box when font rendering differs from PDF.'
)}
position="top"
>
<ActionIcon variant="subtle" color="gray" size="sm" style={{ flexShrink: 0 }}>
<InfoOutlinedIcon fontSize="small" />
</ActionIcon>
</Tooltip>
<Text fw={500} size="sm" style={{ flex: 1 }}>
{t('pdfTextEditor.options.autoScaleText.title', 'Auto-scale text to fit boxes')}
</Text>
</Group>
<Switch
size="md"
checked={autoScaleText}
onChange={(event) => onAutoScaleTextChange(event.currentTarget.checked)}
/>
</Group>
<Divider />
<Stack gap="xs">
<Group gap={4} align="center">
<Text fw={500} size="sm">
{t('pdfTextEditor.options.groupingMode.title', 'Text Grouping Mode')}
</Text>
{externalGroupingMode === 'auto' && isParagraphPage && (
<Badge size="xs" color="blue" variant="light" key={`para-${selectedPage}`}>
{t('pdfTextEditor.pageType.paragraph', 'Paragraph page')}
</Badge>
)}
{externalGroupingMode === 'auto' && !isParagraphPage && hasDocument && (
<Badge size="xs" color="gray" variant="light" key={`sparse-${selectedPage}`}>
{t('pdfTextEditor.pageType.sparse', 'Sparse text')}
</Badge>
)}
</Group>
<Text size="xs" c="dimmed">
{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.'
)}
</Text>
<SegmentedControl
value={externalGroupingMode}
onChange={(value) => 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
/>
</Stack>
<Divider />
<Group justify="space-between" align="center">
<Group gap={4} align="center" style={{ flex: 1, minWidth: 0 }}>
<Tooltip
sidebarTooltip={false}
content={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.'
)}
position="top"
>
<ActionIcon variant="subtle" color="gray" size="sm" style={{ flexShrink: 0 }}>
<InfoOutlinedIcon fontSize="small" />
</ActionIcon>
</Tooltip>
<Text fw={500} size="sm" style={{ flex: 1 }}>
{t('pdfTextEditor.options.forceSingleElement.title', 'Lock edited text to a single PDF element')}
</Text>
</Group>
<Switch
size="md"
checked={forceSingleTextElement}
onChange={(event) => onForceSingleTextElementChange(event.currentTarget.checked)}
/>
</Group>
</Stack>
</ToolStep>
{hasDocument && (
<FontStatusPanel
document={pdfDocument}
pageIndex={selectedPage}
isCollapsed={fontsCollapsed}
onCollapsedChange={setFontsCollapsed}
/>
)}
</Stack>
</ScrollArea>
<Group gap="xs" wrap="nowrap" p="md">
<Button
variant="filled"
onClick={onSaveToWorkbench}
loading={isSavingToWorkbench}
disabled={!hasDocument || !hasChanges || isConverting}
style={{ flex: 1 }}
>
{t('pdfTextEditor.actions.applyChanges', 'Apply Changes')}
</Button>
<Menu position="bottom-end" withinPortal>
<Menu.Target>
<ActionIcon
variant="default"
size="lg"
disabled={!hasDocument || isConverting}
>
<MoreHorizIcon fontSize="small" />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<FileDownloadIcon fontSize="small" />}
onClick={() => onGeneratePdf()}
disabled={!hasChanges || isGeneratingPdf}
>
{t('pdfTextEditor.actions.downloadCopy', 'Download Copy')}
</Menu.Item>
<Menu.Item
leftSection={<AutorenewIcon fontSize="small" />}
onClick={onReset}
color="red"
>
{t('pdfTextEditor.actions.reset', 'Reset Changes')}
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
</Stack>
{/* Mode Change Confirmation Modal */}
<Modal
opened={pendingModeChange !== null}
onClose={handleCancelModeChange}
title={t('pdfTextEditor.modeChange.title', 'Confirm Mode Change')}
centered
>
<Stack gap="md">
<Text>
{t(
'pdfTextEditor.modeChange.warning',
'Changing the text grouping mode will reset all unsaved changes. Are you sure you want to continue?'
)}
</Text>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={handleCancelModeChange}>
{t('pdfTextEditor.modeChange.cancel', 'Cancel')}
</Button>
<Button color="red" onClick={handleConfirmModeChange}>
{t('pdfTextEditor.modeChange.confirm', 'Reset and Change Mode')}
</Button>
</Group>
</Stack>
</Modal>
</>
);
};
export default PdfTextEditorSidebar;

View File

@ -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<Map<string, any>>(new Map());
const pendingDragUpdateRef = useRef<number | null>(null);
const [fontFamilies, setFontFamilies] = useState<Map<string, string>>(new Map());
const [autoScaleText, setAutoScaleText] = useState(true);
const [textScales, setTextScales] = useState<Map<string, number>>(new Map());
const [pendingModeChange, setPendingModeChange] = useState<GroupingMode | null>(null);
const measurementKeyRef = useRef<string>('');
const containerRef = useRef<HTMLDivElement | null>(null);
const editorRefs = useRef<Map<string, HTMLDivElement>>(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',
}}
>
<Card
withBorder
radius="md"
shadow="xs"
padding="md"
style={{
gridColumn: '2 / 3',
gridRow: 1,
maxHeight: 'calc(100vh - 3rem)',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden'
}}
>
<ScrollArea style={{ flex: 1 }} offsetScrollbars>
<Stack gap="sm" pb="md">
<Group justify="space-between" align="center">
<Group gap="xs" align="center">
<DescriptionIcon fontSize="small" />
<Title order={3}>{t('pdfTextEditor.title', 'PDF JSON Editor')}</Title>
{hasChanges && <Badge color="orange" variant="light" size="sm">{t('pdfTextEditor.badges.unsaved', 'Edited')}</Badge>}
</Group>
</Group>
<Stack gap="xs">
<Button
variant="subtle"
leftSection={<AutorenewIcon fontSize="small" />}
onClick={onReset}
disabled={!hasDocument || isConverting}
fullWidth
>
{t('pdfTextEditor.actions.reset', 'Reset Changes')}
</Button>
<Button
variant="default"
leftSection={<FileDownloadIcon fontSize="small" />}
onClick={onDownloadJson}
disabled={!hasDocument || isConverting}
fullWidth
>
{t('pdfTextEditor.actions.downloadJson', 'Download JSON')}
</Button>
<Button
leftSection={<PictureAsPdfIcon fontSize="small" />}
onClick={onGeneratePdf}
loading={isGeneratingPdf}
disabled={!hasDocument || !hasChanges || isConverting}
fullWidth
>
{t('pdfTextEditor.actions.generatePdf', 'Generate PDF')}
</Button>
<Button
variant="filled"
color="green"
leftSection={<SaveIcon fontSize="small" />}
onClick={onSaveToWorkbench}
loading={isSavingToWorkbench}
disabled={!hasDocument || !hasChanges || isConverting}
fullWidth
>
{t('pdfTextEditor.actions.saveChanges', 'Save Changes')}
</Button>
</Stack>
{fileName && (
<Text size="sm" c="dimmed">
{t('pdfTextEditor.currentFile', 'Current file: {{name}}', { name: fileName })}
</Text>
)}
<Divider my="xs" />
<Group justify="space-between" align="center">
<div>
<Text fw={500} size="sm">
{t('pdfTextEditor.options.autoScaleText.title', 'Auto-scale text to fit boxes')}
</Text>
<Text size="xs" c="dimmed" mt={4}>
{t(
'pdfTextEditor.options.autoScaleText.description',
'Automatically scales text horizontally to fit within its original bounding box when font rendering differs from PDF.'
)}
</Text>
</div>
<Switch
size="md"
checked={autoScaleText}
onChange={(event) => setAutoScaleText(event.currentTarget.checked)}
/>
</Group>
<Stack gap="xs">
<Group gap={4} align="center">
<Text fw={500} size="sm">
{t('pdfTextEditor.options.groupingMode.title', 'Text Grouping Mode')}
</Text>
{externalGroupingMode === 'auto' && isParagraphPage && (
<Badge size="xs" color="blue" variant="light" key={`para-${selectedPage}`}>
{t('pdfTextEditor.pageType.paragraph', 'Paragraph page')}
</Badge>
)}
{externalGroupingMode === 'auto' && !isParagraphPage && hasDocument && (
<Badge size="xs" color="gray" variant="light" key={`sparse-${selectedPage}`}>
{t('pdfTextEditor.pageType.sparse', 'Sparse text')}
</Badge>
)}
</Group>
<Text size="xs" c="dimmed">
{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.'
)}
</Text>
<SegmentedControl
value={externalGroupingMode}
onChange={(value) => 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
/>
</Stack>
<Text size="xs" c="dimmed">
{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.',
)}
</Text>
<Group justify="space-between" align="center">
<div>
<Text fw={500} size="sm">
{t('pdfTextEditor.options.forceSingleElement.title', 'Lock edited text to a single PDF element')}
</Text>
<Text size="xs" c="dimmed" mt={4}>
{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.'
)}
</Text>
</div>
<Switch
size="md"
checked={forceSingleTextElement}
onChange={(event) => onForceSingleTextElementChange(event.currentTarget.checked)}
/>
</Group>
<Divider my="xs" />
<Accordion variant="contained">
<Accordion.Item value="disclaimer">
<Accordion.Control>
<Group gap="xs" wrap="nowrap">
<InfoOutlinedIcon fontSize="small" />
<Text size="sm" fw={500}>
{t('pdfTextEditor.disclaimer.heading', 'Preview Limitations')}
</Text>
</Group>
</Accordion.Control>
<Accordion.Panel>
<Stack gap={4}>
<Text size="xs">
{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.'
)}
</Text>
<Text size="xs">
{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.'
)}
</Text>
<Text size="xs">
{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.'
)}
</Text>
</Stack>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
{hasDocument && <FontStatusPanel document={pdfDocument} pageIndex={selectedPage} />}
</Stack>
</ScrollArea>
</Card>
{errorMessage && (
<Alert
icon={<WarningAmberIcon fontSize="small" />}
color="red"
radius="md"
style={{ gridColumn: '2 / 3' }}
mb="md"
>
{errorMessage}
</Alert>
@ -1674,7 +1433,7 @@ const selectionToolbarPosition = useMemo(() => {
<Stack
align="center"
justify="center"
style={{ gridColumn: '1 / 2', gridRow: 1, height: '100%' }}
style={{ flex: 1, height: '100%' }}
>
<Dropzone
onDrop={(files) => {
@ -1704,8 +1463,8 @@ const selectionToolbarPosition = useMemo(() => {
</Text>
<Text size="sm" c="dimmed" ta="center" maw={420}>
{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')}
</Text>
</Stack>
</Dropzone>
@ -1713,7 +1472,7 @@ const selectionToolbarPosition = useMemo(() => {
)}
{isConverting && (
<Card withBorder radius="md" padding="xl" style={{ gridColumn: '1 / 2', gridRow: 1 }}>
<Card withBorder radius="md" padding="xl" style={{ flex: 1 }}>
<Stack gap="md">
<Group justify="space-between" align="flex-start">
<div style={{ flex: 1 }}>
@ -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={
<Group gap="xs">
<InfoOutlinedIcon fontSize="small" />
<Text fw={600}>{t('pdfTextEditor.welcomeBanner.title', 'Welcome to PDF Text Editor (Early Access)')}</Text>
<Text fw={600}>
{t(
'pdfTextEditor.welcomeBanner.title',
'Welcome to PDF Text Editor (Early Access)',
)}
</Text>
</Group>
}
centered
size="lg"
scrollAreaComponent={Box}
>
<ScrollArea style={{ maxHeight: '70vh' }} offsetScrollbars>
<Stack gap="sm">
<div
style={{
display: 'flex',
flexDirection: 'column',
maxHeight: '70vh',
width: '100%',
}}
>
{/* Header (fixed) */}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
marginBottom: '0.75rem',
}}
>
<Alert color="orange" variant="light" radius="md">
<Text size="sm" fw={500}>
{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.',
)}
</Text>
</Alert>
<Text size="sm">
{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.',
)}
</Text>
</div>
{/* Body (scrollable) */}
<div
style={{
flex: 1,
minHeight: 0,
overflowY: 'auto',
paddingRight: '0.25rem',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
}}
>
<Divider />
<Text size="sm" fw={500} c="green.7">
{t('pdfTextEditor.welcomeBanner.bestFor', 'Works Best With:')}
</Text>
<Text
size="sm"
component="ul"
style={{ marginLeft: '1rem', marginTop: '0.25rem' }}
>
<li>
{t(
'pdfTextEditor.welcomeBanner.bestFor1',
'Simple PDFs containing primarily text and images',
)}
</li>
<li>
{t(
'pdfTextEditor.welcomeBanner.bestFor2',
'Documents with standard paragraph formatting',
)}
</li>
<li>
{t(
'pdfTextEditor.welcomeBanner.bestFor3',
'Letters, essays, reports, and basic documents',
)}
</li>
</Text>
<Divider />
<Text size="sm" fw={500} c="orange.7">
{t('pdfTextEditor.welcomeBanner.notIdealFor', 'Not Ideal For:')}
</Text>
<Text
size="sm"
component="ul"
style={{ marginLeft: '1rem', marginTop: '0.25rem' }}
>
<li>
{t(
'pdfTextEditor.welcomeBanner.notIdealFor1',
'PDFs with special formatting like bullet points, tables, or multi-column layouts',
)}
</li>
<li>
{t(
'pdfTextEditor.welcomeBanner.notIdealFor2',
'Magazines, brochures, or heavily designed documents',
)}
</li>
<li>
{t(
'pdfTextEditor.welcomeBanner.notIdealFor3',
'Instruction manuals with complex layouts',
)}
</li>
</Text>
<Divider />
<Text size="sm" fw={500}>
{t('pdfTextEditor.welcomeBanner.limitations', 'Current Limitations:')}
</Text>
<Text
size="sm"
component="ul"
style={{ marginLeft: '1rem', marginTop: '0.25rem' }}
>
<li>
{t(
'pdfTextEditor.welcomeBanner.limitation1',
'Font rendering may differ slightly from the original PDF',
)}
</li>
<li>
{t(
'pdfTextEditor.welcomeBanner.limitation2',
'Complex graphics, form fields, and annotations are preserved but not editable',
)}
</li>
<li>
{t(
'pdfTextEditor.welcomeBanner.limitation3',
'Large files may take time to convert and process',
)}
</li>
</Text>
<Divider />
<Text size="sm" fw={500}>
{t('pdfTextEditor.welcomeBanner.knownIssues', 'Known Issues (Being Fixed):')}
</Text>
<Text
size="sm"
component="ul"
style={{ marginLeft: '1rem', marginTop: '0.25rem' }}
>
<li>
{t(
'pdfTextEditor.welcomeBanner.issue1',
'Text colour is not currently preserved (will be added soon)',
)}
</li>
<li>
{t(
'pdfTextEditor.welcomeBanner.issue2',
'Paragraph mode has more alignment and spacing issues - Single Line mode recommended',
)}
</li>
<li>
{t(
'pdfTextEditor.welcomeBanner.issue3',
'The preview display differs from the exported PDF - exported PDFs are closer to the original',
)}
</li>
<li>
{t(
'pdfTextEditor.welcomeBanner.issue4',
'Rotated text alignment may need manual adjustment',
)}
</li>
<li>
{t(
'pdfTextEditor.welcomeBanner.issue5',
'Transparency and layering effects may vary from original',
)}
</li>
</Text>
</div>
</div>
{/* Footer (fixed) */}
<div
style={{
marginTop: '0.75rem',
}}
>
<Divider />
<Text size="sm" fw={500} c="green.7">
{t('pdfTextEditor.welcomeBanner.bestFor', 'Works Best With:')}
<Text size="xs" c="dimmed" mt="xs">
{t(
'pdfTextEditor.welcomeBanner.feedback',
'This is an early access feature. Please report any issues you encounter to help us improve!',
)}
</Text>
<Text size="sm" component="ul" style={{ marginLeft: '1rem', marginTop: '0.25rem' }}>
<li>{t('pdfTextEditor.welcomeBanner.bestFor1', 'Simple PDFs containing primarily text and images')}</li>
<li>{t('pdfTextEditor.welcomeBanner.bestFor2', 'Documents with standard paragraph formatting')}</li>
<li>{t('pdfTextEditor.welcomeBanner.bestFor3', 'Letters, essays, reports, and basic documents')}</li>
</Text>
<Divider />
<Text size="sm" fw={500} c="orange.7">
{t('pdfTextEditor.welcomeBanner.notIdealFor', 'Not Ideal For:')}
</Text>
<Text size="sm" component="ul" style={{ marginLeft: '1rem', marginTop: '0.25rem' }}>
<li>{t('pdfTextEditor.welcomeBanner.notIdealFor1', 'PDFs with special formatting like bullet points, tables, or multi-column layouts')}</li>
<li>{t('pdfTextEditor.welcomeBanner.notIdealFor2', 'Magazines, brochures, or heavily designed documents')}</li>
<li>{t('pdfTextEditor.welcomeBanner.notIdealFor3', 'Instruction manuals with complex layouts')}</li>
</Text>
<Divider />
<Text size="sm" fw={500}>
{t('pdfTextEditor.welcomeBanner.limitations', 'Current Limitations:')}
</Text>
<Text size="sm" component="ul" style={{ marginLeft: '1rem', marginTop: '0.25rem' }}>
<li>{t('pdfTextEditor.welcomeBanner.limitation1', 'Font rendering may differ slightly from the original PDF')}</li>
<li>{t('pdfTextEditor.welcomeBanner.limitation2', 'Complex graphics, form fields, and annotations are preserved but not editable')}</li>
<li>{t('pdfTextEditor.welcomeBanner.limitation3', 'Large files may take time to convert and process')}</li>
</Text>
<Divider />
<Text size="sm" fw={500}>
{t('pdfTextEditor.welcomeBanner.knownIssues', 'Known Issues (Being Fixed):')}
</Text>
<Text size="sm" component="ul" style={{ marginLeft: '1rem', marginTop: '0.25rem' }}>
<li>{t('pdfTextEditor.welcomeBanner.issue1', 'Text colour is not currently preserved (will be added soon)')}</li>
<li>{t('pdfTextEditor.welcomeBanner.issue2', 'Paragraph mode has more alignment and spacing issues - Single Line mode recommended')}</li>
<li>{t('pdfTextEditor.welcomeBanner.issue3', 'The preview display differs from the exported PDF - exported PDFs are closer to the original')}</li>
<li>{t('pdfTextEditor.welcomeBanner.issue4', 'Rotated text alignment may need manual adjustment')}</li>
<li>{t('pdfTextEditor.welcomeBanner.issue5', 'Transparency and layering effects may vary from original')}</li>
</Text>
<Divider />
<Text size="xs" c="dimmed">
{t('pdfTextEditor.welcomeBanner.feedback', 'This is an early access feature. Please report any issues you encounter to help us improve!')}
</Text>
<Group justify="flex-end" gap="sm" mt="md">
<Group justify="flex-end" gap="sm" mt="xs">
<Button variant="default" onClick={handleDismissWelcomeBanner}>
{t('pdfTextEditor.welcomeBanner.gotIt', 'Got it')}
</Button>
@ -1854,8 +1754,8 @@ const selectionToolbarPosition = useMemo(() => {
{t('pdfTextEditor.welcomeBanner.dontShowAgain', "Don't show again")}
</Button>
</Group>
</Stack>
</ScrollArea>
</div>
</div>
</Modal>
<Card
@ -2485,31 +2385,6 @@ const selectionToolbarPosition = useMemo(() => {
</Stack>
)}
{/* Mode Change Confirmation Modal */}
<Modal
opened={pendingModeChange !== null}
onClose={handleCancelModeChange}
title={t('pdfTextEditor.modeChange.title', 'Confirm Mode Change')}
centered
>
<Stack gap="md">
<Text>
{t(
'pdfTextEditor.modeChange.warning',
'Changing the text grouping mode will reset all unsaved changes. Are you sure you want to continue?'
)}
</Text>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={handleCancelModeChange}>
{t('pdfTextEditor.modeChange.cancel', 'Cancel')}
</Button>
<Button color="red" onClick={handleConfirmModeChange}>
{t('pdfTextEditor.modeChange.confirm', 'Reset and Change Mode')}
</Button>
</Group>
</Stack>
</Modal>
{/* Navigation Warning Modal */}
<NavigationWarningModal
onApplyAndContinue={onSaveToWorkbench}

View File

@ -0,0 +1,36 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '@app/types/tips';
export const usePdfTextEditorTips = (): TooltipContent => {
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.'
),
},
],
};
};

View File

@ -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<Map<number, string>>(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: <DescriptionIcon fontSize="small" />,
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 <PdfTextEditorSidebar data={viewData} />;
};
(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 = () => ({

View File

@ -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;