rename tool

This commit is contained in:
Anthony Stirling 2025-11-12 10:16:36 +00:00
parent 246d7a7b16
commit aad1b06324
11 changed files with 109 additions and 109 deletions

View File

@ -1,4 +1,4 @@
# PDF JSON Editor Backlog # PDF Text Editor Backlog
- **Type3 Font Support (Text Additions)** - **Type3 Font Support (Text Additions)**
- Parse Type3 charprocs to extract glyph outlines, build a synthetic TrueType/OpenType font (FontTools, Ghostscript `ps2ttf`, etc.), and store it in `webProgram` / `pdfProgram` for client use. - Parse Type3 charprocs to extract glyph outlines, build a synthetic TrueType/OpenType font (FontTools, Ghostscript `ps2ttf`, etc.), and store it in `webProgram` / `pdfProgram` for client use.
@ -15,7 +15,7 @@
- **Editor UX Safeguards** - **Editor UX Safeguards**
- Mark groups using fallback glyphs so the UI can warn about possible appearance shifts. Font family matching is now implemented (Liberation fonts), but weight matching is still TODO, so bold/italic text using fallbacks may appear lighter than original. - Mark groups using fallback glyphs so the UI can warn about possible appearance shifts. Font family matching is now implemented (Liberation fonts), but weight matching is still TODO, so bold/italic text using fallbacks may appear lighter than original.
- Surface when Type3 conversion was downgraded (e.g., rasterized glyphs) and limit editing to operations that keep the PDF stable. - Surface when Type3 conversion was downgraded (e.g., rasterized glyphs) and limit editing to operations that keep the PDF stable.
- Reference: `frontend/src/proprietary/components/tools/pdfJsonEditor/PdfJsonEditorView.tsx:1260-1287` - Reference: `frontend/src/proprietary/components/tools/pdfTextEditor/PdfTextEditorView.tsx:1260-1287`
- **Canonical Font Sharing** - **Canonical Font Sharing**
- Emit fonts once per unique embedded program. Add a `canonicalFonts` array containing the full payload (program, ToUnicode, metadata) and a compact `fontAliases` mapping `{pageNumber, fontId, canonicalUid}` so text elements can still reference per-page IDs. - Emit fonts once per unique embedded program. Add a `canonicalFonts` array containing the full payload (program, ToUnicode, metadata) and a compact `fontAliases` mapping `{pageNumber, fontId, canonicalUid}` so text elements can still reference per-page IDs.

View File

@ -2,7 +2,7 @@
## Overview ## Overview
The PDF JSON editor needs to handle **Type3 fonts** - custom vector fonts embedded in PDFs that don't follow standard font formats. These are common in PDFs generated by Matplotlib, LaTeX, scientific papers, and presentation tools. The PDF Text editor needs to handle **Type3 fonts** - custom vector fonts embedded in PDFs that don't follow standard font formats. These are common in PDFs generated by Matplotlib, LaTeX, scientific papers, and presentation tools.
When converting a PDF to JSON for editing, Type3 fonts present two challenges: When converting a PDF to JSON for editing, Type3 fonts present two challenges:
1. **No Unicode mapping** - Character codes don't map to standard Unicode characters 1. **No Unicode mapping** - Character codes don't map to standard Unicode characters
@ -430,8 +430,8 @@ Output shows all Type3 fonts with their signatures and glyph coverage.
- `PdfJsonFontType3Glyph.java` - Model for Type3 glyph data - `PdfJsonFontType3Glyph.java` - Model for Type3 glyph data
### Frontend (TypeScript) ### Frontend (TypeScript)
- `pdfJsonEditorTypes.ts` - Type definitions for JSON structure - `pdfTextEditorTypes.ts` - Type definitions for JSON structure
- `pdfJsonEditorUtils.ts` - Font handling utilities - `pdfTextEditorUtils.ts` - Font handling utilities
### Resources ### Resources
- `type3/library/index.json` - Font library metadata - `type3/library/index.json` - Font library metadata

View File

@ -765,9 +765,9 @@
"title": "Automate", "title": "Automate",
"desc": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks." "desc": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."
}, },
"pdfJsonEditor": { "pdfTextEditor": {
"tags": "edit,text,modify", "tags": "edit,text,modify",
"title": "PDF Editor", "title": "PDF Text Editor",
"desc": "Edit text content in PDF documents" "desc": "Edit text content in PDF documents"
}, },
"mobile": { "mobile": {
@ -4455,8 +4455,8 @@
"startTour": "Start Tour", "startTour": "Start Tour",
"startTourDescription": "Take a guided tour of Stirling PDF's key features" "startTourDescription": "Take a guided tour of Stirling PDF's key features"
}, },
"pdfJsonEditor": { "pdfTextEditor": {
"viewLabel": "PDF Editor", "viewLabel": "PDF Text Editor",
"title": "PDF Editor", "title": "PDF Editor",
"badges": { "badges": {
"unsaved": "Unsaved changes", "unsaved": "Unsaved changes",

View File

@ -21,14 +21,14 @@ import FontDownloadIcon from '@mui/icons-material/FontDownload';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import { PdfJsonDocument } from '@app/tools/pdfJsonEditor/pdfJsonEditorTypes'; import { PdfJsonDocument } from '@app/tools/pdfTextEditor/pdfTextEditorTypes';
import { import {
analyzeDocumentFonts, analyzeDocumentFonts,
DocumentFontAnalysis, DocumentFontAnalysis,
FontAnalysis, FontAnalysis,
getFontStatusColor, getFontStatusColor,
getFontStatusDescription, getFontStatusDescription,
} from '@app/tools/pdfJsonEditor/fontAnalysis'; } from '@app/tools/pdfTextEditor/fontAnalysis';
interface FontStatusPanelProps { interface FontStatusPanelProps {
document: PdfJsonDocument | null; document: PdfJsonDocument | null;
@ -99,19 +99,19 @@ const FontDetailItem = ({ analysis }: { analysis: FontAnalysis }) => {
{/* Font Details */} {/* Font Details */}
<Box> <Box>
<Text size="xs" c="dimmed" mb={2}> <Text size="xs" c="dimmed" mb={2}>
{t('pdfJsonEditor.fontAnalysis.details', 'Font Details')}: {t('pdfTextEditor.fontAnalysis.details', 'Font Details')}:
</Text> </Text>
<Stack gap={2}> <Stack gap={2}>
<Group gap={4}> <Group gap={4}>
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
{t('pdfJsonEditor.fontAnalysis.embedded', 'Embedded')}: {t('pdfTextEditor.fontAnalysis.embedded', 'Embedded')}:
</Text> </Text>
<Code style={{ fontSize: '0.65rem', padding: '0 4px' }}>{analysis.embedded ? 'Yes' : 'No'}</Code> <Code style={{ fontSize: '0.65rem', padding: '0 4px' }}>{analysis.embedded ? 'Yes' : 'No'}</Code>
</Group> </Group>
{analysis.subtype && ( {analysis.subtype && (
<Group gap={4}> <Group gap={4}>
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
{t('pdfJsonEditor.fontAnalysis.type', 'Type')}: {t('pdfTextEditor.fontAnalysis.type', 'Type')}:
</Text> </Text>
<Code style={{ fontSize: '0.65rem', padding: '0 4px' }}>{analysis.subtype}</Code> <Code style={{ fontSize: '0.65rem', padding: '0 4px' }}>{analysis.subtype}</Code>
</Group> </Group>
@ -119,7 +119,7 @@ const FontDetailItem = ({ analysis }: { analysis: FontAnalysis }) => {
{analysis.webFormat && ( {analysis.webFormat && (
<Group gap={4}> <Group gap={4}>
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
{t('pdfJsonEditor.fontAnalysis.webFormat', 'Web Format')}: {t('pdfTextEditor.fontAnalysis.webFormat', 'Web Format')}:
</Text> </Text>
<Code style={{ fontSize: '0.65rem', padding: '0 4px' }}>{analysis.webFormat}</Code> <Code style={{ fontSize: '0.65rem', padding: '0 4px' }}>{analysis.webFormat}</Code>
</Group> </Group>
@ -131,7 +131,7 @@ const FontDetailItem = ({ analysis }: { analysis: FontAnalysis }) => {
{analysis.warnings.length > 0 && ( {analysis.warnings.length > 0 && (
<Box> <Box>
<Text size="xs" c="orange" fw={500}> <Text size="xs" c="orange" fw={500}>
{t('pdfJsonEditor.fontAnalysis.warnings', 'Warnings')}: {t('pdfTextEditor.fontAnalysis.warnings', 'Warnings')}:
</Text> </Text>
<List size="xs" spacing={2} withPadding> <List size="xs" spacing={2} withPadding>
{analysis.warnings.map((warning, index) => ( {analysis.warnings.map((warning, index) => (
@ -147,7 +147,7 @@ const FontDetailItem = ({ analysis }: { analysis: FontAnalysis }) => {
{analysis.suggestions.length > 0 && ( {analysis.suggestions.length > 0 && (
<Box> <Box>
<Text size="xs" c="blue" fw={500}> <Text size="xs" c="blue" fw={500}>
{t('pdfJsonEditor.fontAnalysis.suggestions', 'Notes')}: {t('pdfTextEditor.fontAnalysis.suggestions', 'Notes')}:
</Text> </Text>
<List size="xs" spacing={2} withPadding> <List size="xs" spacing={2} withPadding>
{analysis.suggestions.map((suggestion, index) => ( {analysis.suggestions.map((suggestion, index) => (
@ -192,8 +192,8 @@ const FontStatusPanel: React.FC<FontStatusPanelProps> = ({ document, pageIndex }
const statusColor = canReproducePerfectly ? 'green' : hasWarnings ? 'yellow' : 'blue'; const statusColor = canReproducePerfectly ? 'green' : hasWarnings ? 'yellow' : 'blue';
const pageLabel = pageIndex !== undefined const pageLabel = pageIndex !== undefined
? t('pdfJsonEditor.fontAnalysis.currentPageFonts', 'Fonts on this page') ? t('pdfTextEditor.fontAnalysis.currentPageFonts', 'Fonts on this page')
: t('pdfJsonEditor.fontAnalysis.allFonts', 'All fonts'); : t('pdfTextEditor.fontAnalysis.allFonts', 'All fonts');
return ( return (
<Accordion variant="contained" defaultValue={hasWarnings ? 'fonts' : undefined}> <Accordion variant="contained" defaultValue={hasWarnings ? 'fonts' : undefined}>
@ -215,16 +215,16 @@ const FontStatusPanel: React.FC<FontStatusPanelProps> = ({ document, pageIndex }
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
{canReproducePerfectly {canReproducePerfectly
? t( ? t(
'pdfJsonEditor.fontAnalysis.perfectMessage', 'pdfTextEditor.fontAnalysis.perfectMessage',
'All fonts can be reproduced perfectly.' 'All fonts can be reproduced perfectly.'
) )
: hasWarnings : hasWarnings
? t( ? t(
'pdfJsonEditor.fontAnalysis.warningMessage', 'pdfTextEditor.fontAnalysis.warningMessage',
'Some fonts may not render correctly.' 'Some fonts may not render correctly.'
) )
: t( : t(
'pdfJsonEditor.fontAnalysis.infoMessage', 'pdfTextEditor.fontAnalysis.infoMessage',
'Font reproduction information available.' 'Font reproduction information available.'
)} )}
</Text> </Text>
@ -233,22 +233,22 @@ const FontStatusPanel: React.FC<FontStatusPanelProps> = ({ document, pageIndex }
<Group gap={4} wrap="wrap"> <Group gap={4} wrap="wrap">
{summary.perfect > 0 && ( {summary.perfect > 0 && (
<Badge size="xs" color="green" variant="light" leftSection={<CheckCircleIcon sx={{ fontSize: 12 }} />}> <Badge size="xs" color="green" variant="light" leftSection={<CheckCircleIcon sx={{ fontSize: 12 }} />}>
{summary.perfect} {t('pdfJsonEditor.fontAnalysis.perfect', 'perfect')} {summary.perfect} {t('pdfTextEditor.fontAnalysis.perfect', 'perfect')}
</Badge> </Badge>
)} )}
{summary.embeddedSubset > 0 && ( {summary.embeddedSubset > 0 && (
<Badge size="xs" color="blue" variant="light" leftSection={<InfoIcon sx={{ fontSize: 12 }} />}> <Badge size="xs" color="blue" variant="light" leftSection={<InfoIcon sx={{ fontSize: 12 }} />}>
{summary.embeddedSubset} {t('pdfJsonEditor.fontAnalysis.subset', 'subset')} {summary.embeddedSubset} {t('pdfTextEditor.fontAnalysis.subset', 'subset')}
</Badge> </Badge>
)} )}
{summary.systemFallback > 0 && ( {summary.systemFallback > 0 && (
<Badge size="xs" color="yellow" variant="light" leftSection={<WarningIcon sx={{ fontSize: 12 }} />}> <Badge size="xs" color="yellow" variant="light" leftSection={<WarningIcon sx={{ fontSize: 12 }} />}>
{summary.systemFallback} {t('pdfJsonEditor.fontAnalysis.fallback', 'fallback')} {summary.systemFallback} {t('pdfTextEditor.fontAnalysis.fallback', 'fallback')}
</Badge> </Badge>
)} )}
{summary.missing > 0 && ( {summary.missing > 0 && (
<Badge size="xs" color="red" variant="light" leftSection={<ErrorIcon sx={{ fontSize: 12 }} />}> <Badge size="xs" color="red" variant="light" leftSection={<ErrorIcon sx={{ fontSize: 12 }} />}>
{summary.missing} {t('pdfJsonEditor.fontAnalysis.missing', 'missing')} {summary.missing} {t('pdfTextEditor.fontAnalysis.missing', 'missing')}
</Badge> </Badge>
)} )}
</Group> </Group>

View File

@ -32,13 +32,13 @@ import CloseIcon from '@mui/icons-material/Close';
import { Rnd } from 'react-rnd'; import { Rnd } from 'react-rnd';
import { import {
PdfJsonEditorViewData, PdfTextEditorViewData,
PdfJsonFont, PdfJsonFont,
PdfJsonPage, PdfJsonPage,
ConversionProgress, ConversionProgress,
} from '@app/tools/pdfJsonEditor/pdfJsonEditorTypes'; } from '@app/tools/pdfTextEditor/pdfTextEditorTypes';
import { getImageBounds, pageDimensions } from '@app/tools/pdfJsonEditor/pdfJsonEditorUtils'; import { getImageBounds, pageDimensions } from '@app/tools/pdfTextEditor/pdfTextEditorUtils';
import FontStatusPanel from '@app/components/tools/pdfJsonEditor/FontStatusPanel'; import FontStatusPanel from '@app/components/tools/pdfTextEditor/FontStatusPanel';
const MAX_RENDER_WIDTH = 820; const MAX_RENDER_WIDTH = 820;
const MIN_BOX_SIZE = 18; const MIN_BOX_SIZE = 18;
@ -190,8 +190,8 @@ const extractTextWithSoftBreaks = (element: HTMLElement): { text: string; insert
}; };
}; };
interface PdfJsonEditorViewProps { interface PdfTextEditorViewProps {
data: PdfJsonEditorViewData; data: PdfTextEditorViewData;
} }
const toCssBounds = ( const toCssBounds = (
@ -313,7 +313,7 @@ const analyzePageContentType = (groups: TextGroup[]): boolean => {
type GroupingMode = 'auto' | 'paragraph' | 'singleLine'; type GroupingMode = 'auto' | 'paragraph' | 'singleLine';
const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [activeGroupId, setActiveGroupId] = useState<string | null>(null); const [activeGroupId, setActiveGroupId] = useState<string | null>(null);
const [editingGroupId, setEditingGroupId] = useState<string | null>(null); const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
@ -1066,8 +1066,8 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<DescriptionIcon fontSize="small" /> <DescriptionIcon fontSize="small" />
<Title order={3}>{t('pdfJsonEditor.title', 'PDF JSON Editor')}</Title> <Title order={3}>{t('pdfTextEditor.title', 'PDF JSON Editor')}</Title>
{hasChanges && <Badge color="orange" variant="light" size="sm">{t('pdfJsonEditor.badges.unsaved', 'Edited')}</Badge>} {hasChanges && <Badge color="orange" variant="light" size="sm">{t('pdfTextEditor.badges.unsaved', 'Edited')}</Badge>}
</Group> </Group>
</Group> </Group>
@ -1081,7 +1081,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
fullWidth fullWidth
{...props} {...props}
> >
{t('pdfJsonEditor.actions.load', 'Load File')} {t('pdfTextEditor.actions.load', 'Load File')}
</Button> </Button>
)} )}
</FileButton> </FileButton>
@ -1092,7 +1092,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
disabled={!hasDocument || isConverting} disabled={!hasDocument || isConverting}
fullWidth fullWidth
> >
{t('pdfJsonEditor.actions.reset', 'Reset Changes')} {t('pdfTextEditor.actions.reset', 'Reset Changes')}
</Button> </Button>
<Button <Button
variant="default" variant="default"
@ -1101,7 +1101,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
disabled={!hasDocument || isConverting} disabled={!hasDocument || isConverting}
fullWidth fullWidth
> >
{t('pdfJsonEditor.actions.downloadJson', 'Download JSON')} {t('pdfTextEditor.actions.downloadJson', 'Download JSON')}
</Button> </Button>
<Button <Button
leftSection={<PictureAsPdfIcon fontSize="small" />} leftSection={<PictureAsPdfIcon fontSize="small" />}
@ -1110,13 +1110,13 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
disabled={!hasDocument || !hasChanges || isConverting} disabled={!hasDocument || !hasChanges || isConverting}
fullWidth fullWidth
> >
{t('pdfJsonEditor.actions.generatePdf', 'Generate PDF')} {t('pdfTextEditor.actions.generatePdf', 'Generate PDF')}
</Button> </Button>
</Stack> </Stack>
{fileName && ( {fileName && (
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
{t('pdfJsonEditor.currentFile', 'Current file: {{name}}', { name: fileName })} {t('pdfTextEditor.currentFile', 'Current file: {{name}}', { name: fileName })}
</Text> </Text>
)} )}
@ -1125,11 +1125,11 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<div> <div>
<Text fw={500} size="sm"> <Text fw={500} size="sm">
{t('pdfJsonEditor.options.autoScaleText.title', 'Auto-scale text to fit boxes')} {t('pdfTextEditor.options.autoScaleText.title', 'Auto-scale text to fit boxes')}
</Text> </Text>
<Text size="xs" c="dimmed" mt={4}> <Text size="xs" c="dimmed" mt={4}>
{t( {t(
'pdfJsonEditor.options.autoScaleText.description', 'pdfTextEditor.options.autoScaleText.description',
'Automatically scales text horizontally to fit within its original bounding box when font rendering differs from PDF.' 'Automatically scales text horizontally to fit within its original bounding box when font rendering differs from PDF.'
)} )}
</Text> </Text>
@ -1144,32 +1144,32 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
<Stack gap="xs"> <Stack gap="xs">
<Group gap={4} align="center"> <Group gap={4} align="center">
<Text fw={500} size="sm"> <Text fw={500} size="sm">
{t('pdfJsonEditor.options.groupingMode.title', 'Text Grouping Mode')} {t('pdfTextEditor.options.groupingMode.title', 'Text Grouping Mode')}
</Text> </Text>
{externalGroupingMode === 'auto' && isParagraphPage && ( {externalGroupingMode === 'auto' && isParagraphPage && (
<Badge size="xs" color="blue" variant="light"> <Badge size="xs" color="blue" variant="light">
{t('pdfJsonEditor.pageType.paragraph', 'Paragraph page')} {t('pdfTextEditor.pageType.paragraph', 'Paragraph page')}
</Badge> </Badge>
)} )}
{externalGroupingMode === 'auto' && !isParagraphPage && hasDocument && ( {externalGroupingMode === 'auto' && !isParagraphPage && hasDocument && (
<Badge size="xs" color="gray" variant="light"> <Badge size="xs" color="gray" variant="light">
{t('pdfJsonEditor.pageType.sparse', 'Sparse text')} {t('pdfTextEditor.pageType.sparse', 'Sparse text')}
</Badge> </Badge>
)} )}
</Group> </Group>
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
{externalGroupingMode === 'auto' {externalGroupingMode === 'auto'
? t( ? t(
'pdfJsonEditor.options.groupingMode.autoDescription', 'pdfTextEditor.options.groupingMode.autoDescription',
'Automatically detects page type and groups text appropriately.' 'Automatically detects page type and groups text appropriately.'
) )
: externalGroupingMode === 'paragraph' : externalGroupingMode === 'paragraph'
? t( ? t(
'pdfJsonEditor.options.groupingMode.paragraphDescription', 'pdfTextEditor.options.groupingMode.paragraphDescription',
'Groups aligned lines into multi-line paragraph text boxes.' 'Groups aligned lines into multi-line paragraph text boxes.'
) )
: t( : t(
'pdfJsonEditor.options.groupingMode.singleLineDescription', 'pdfTextEditor.options.groupingMode.singleLineDescription',
'Keeps each PDF text line as a separate text box.' 'Keeps each PDF text line as a separate text box.'
)} )}
</Text> </Text>
@ -1177,9 +1177,9 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
value={externalGroupingMode} value={externalGroupingMode}
onChange={(value) => handleModeChangeRequest(value as GroupingMode)} onChange={(value) => handleModeChangeRequest(value as GroupingMode)}
data={[ data={[
{ label: t('pdfJsonEditor.groupingMode.auto', 'Auto'), value: 'auto' }, { label: t('pdfTextEditor.groupingMode.auto', 'Auto'), value: 'auto' },
{ label: t('pdfJsonEditor.groupingMode.paragraph', 'Paragraph'), value: 'paragraph' }, { label: t('pdfTextEditor.groupingMode.paragraph', 'Paragraph'), value: 'paragraph' },
{ label: t('pdfJsonEditor.groupingMode.singleLine', 'Single Line'), value: 'singleLine' }, { label: t('pdfTextEditor.groupingMode.singleLine', 'Single Line'), value: 'singleLine' },
]} ]}
fullWidth fullWidth
/> />
@ -1188,11 +1188,11 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<div> <div>
<Text fw={500} size="sm"> <Text fw={500} size="sm">
{t('pdfJsonEditor.options.forceSingleElement.title', 'Lock edited text to a single PDF element')} {t('pdfTextEditor.options.forceSingleElement.title', 'Lock edited text to a single PDF element')}
</Text> </Text>
<Text size="xs" c="dimmed" mt={4}> <Text size="xs" c="dimmed" mt={4}>
{t( {t(
'pdfJsonEditor.options.forceSingleElement.description', '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.' 'When enabled, the editor exports each edited text box as one PDF text element to avoid overlapping glyphs or mixed fonts.'
)} )}
</Text> </Text>
@ -1212,7 +1212,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
<Group gap="xs" wrap="nowrap"> <Group gap="xs" wrap="nowrap">
<InfoOutlinedIcon fontSize="small" /> <InfoOutlinedIcon fontSize="small" />
<Text size="sm" fw={500}> <Text size="sm" fw={500}>
{t('pdfJsonEditor.disclaimer.heading', 'Preview Limitations')} {t('pdfTextEditor.disclaimer.heading', 'Preview Limitations')}
</Text> </Text>
</Group> </Group>
</Accordion.Control> </Accordion.Control>
@ -1220,19 +1220,19 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
<Stack gap={4}> <Stack gap={4}>
<Text size="xs"> <Text size="xs">
{t( {t(
'pdfJsonEditor.disclaimer.textFocus', '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.' '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>
<Text size="xs"> <Text size="xs">
{t( {t(
'pdfJsonEditor.disclaimer.previewVariance', '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.' '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>
<Text size="xs"> <Text size="xs">
{t( {t(
'pdfJsonEditor.disclaimer.alpha', '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.' '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> </Text>
@ -1262,10 +1262,10 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
<Stack align="center" gap="md"> <Stack align="center" gap="md">
<DescriptionIcon sx={{ fontSize: 48 }} /> <DescriptionIcon sx={{ fontSize: 48 }} />
<Text size="lg" fw={600}> <Text size="lg" fw={600}>
{t('pdfJsonEditor.empty.title', 'No document loaded')} {t('pdfTextEditor.empty.title', 'No document loaded')}
</Text> </Text>
<Text size="sm" c="dimmed" ta="center" maw={420}> <Text size="sm" c="dimmed" ta="center" maw={420}>
{t('pdfJsonEditor.empty.subtitle', 'Load a PDF or JSON file to begin editing text content.')} {t('pdfTextEditor.empty.subtitle', 'Load a PDF or JSON file to begin editing text content.')}
</Text> </Text>
</Stack> </Stack>
</Card> </Card>
@ -1279,12 +1279,12 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
<Text size="lg" fw={600} mb="xs"> <Text size="lg" fw={600} mb="xs">
{conversionProgress {conversionProgress
? conversionProgress.message ? conversionProgress.message
: t('pdfJsonEditor.converting', 'Converting PDF to editable format...')} : t('pdfTextEditor.converting', 'Converting PDF to editable format...')}
</Text> </Text>
{conversionProgress && ( {conversionProgress && (
<Group gap="xs"> <Group gap="xs">
<Text size="sm" c="dimmed" tt="capitalize"> <Text size="sm" c="dimmed" tt="capitalize">
{t(`pdfJsonEditor.stages.${conversionProgress.stage}`, conversionProgress.stage)} {t(`pdfTextEditor.stages.${conversionProgress.stage}`, conversionProgress.stage)}
</Text> </Text>
{conversionProgress.current !== undefined && {conversionProgress.current !== undefined &&
conversionProgress.total !== undefined && ( conversionProgress.total !== undefined && (
@ -1317,18 +1317,18 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<Group gap="sm"> <Group gap="sm">
<Text fw={500}> <Text fw={500}>
{t('pdfJsonEditor.pageSummary', 'Page {{number}} of {{total}}', { {t('pdfTextEditor.pageSummary', 'Page {{number}} of {{total}}', {
number: selectedPage + 1, number: selectedPage + 1,
total: pages.length, total: pages.length,
})} })}
</Text> </Text>
{dirtyPages[selectedPage] && ( {dirtyPages[selectedPage] && (
<Badge color="yellow" size="xs"> <Badge color="yellow" size="xs">
{t('pdfJsonEditor.badges.modified', 'Edited')} {t('pdfTextEditor.badges.modified', 'Edited')}
</Badge> </Badge>
)} )}
<Badge color="blue" variant="dot" size="xs"> <Badge color="blue" variant="dot" size="xs">
{t('pdfJsonEditor.badges.earlyAccess', 'Early Access')} {t('pdfTextEditor.badges.earlyAccess', 'Early Access')}
</Badge> </Badge>
</Group> </Group>
{pages.length > 1 && ( {pages.length > 1 && (
@ -1381,7 +1381,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
{pagePreview && ( {pagePreview && (
<img <img
src={pagePreview} src={pagePreview}
alt={t('pdfJsonEditor.pagePreviewAlt', 'Page preview')} alt={t('pdfTextEditor.pagePreviewAlt', 'Page preview')}
style={{ style={{
position: 'absolute', position: 'absolute',
inset: 0, inset: 0,
@ -1493,7 +1493,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
> >
<img <img
src={src} src={src}
alt={t('pdfJsonEditor.imageLabel', 'Placed image')} alt={t('pdfTextEditor.imageLabel', 'Placed image')}
style={{ style={{
width: '100%', width: '100%',
height: '100%', height: '100%',
@ -1510,7 +1510,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
<Group justify="center" align="center" style={{ height: '100%' }}> <Group justify="center" align="center" style={{ height: '100%' }}>
<Stack gap={4} align="center"> <Stack gap={4} align="center">
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
{t('pdfJsonEditor.noTextOnPage', 'No editable text was detected on this page.')} {t('pdfTextEditor.noTextOnPage', 'No editable text was detected on this page.')}
</Text> </Text>
</Stack> </Stack>
</Group> </Group>
@ -1785,22 +1785,22 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
<Modal <Modal
opened={pendingModeChange !== null} opened={pendingModeChange !== null}
onClose={handleCancelModeChange} onClose={handleCancelModeChange}
title={t('pdfJsonEditor.modeChange.title', 'Confirm Mode Change')} title={t('pdfTextEditor.modeChange.title', 'Confirm Mode Change')}
centered centered
> >
<Stack gap="md"> <Stack gap="md">
<Text> <Text>
{t( {t(
'pdfJsonEditor.modeChange.warning', 'pdfTextEditor.modeChange.warning',
'Changing the text grouping mode will reset all unsaved changes. Are you sure you want to continue?' 'Changing the text grouping mode will reset all unsaved changes. Are you sure you want to continue?'
)} )}
</Text> </Text>
<Group justify="flex-end" gap="sm"> <Group justify="flex-end" gap="sm">
<Button variant="default" onClick={handleCancelModeChange}> <Button variant="default" onClick={handleCancelModeChange}>
{t('pdfJsonEditor.modeChange.cancel', 'Cancel')} {t('pdfTextEditor.modeChange.cancel', 'Cancel')}
</Button> </Button>
<Button color="red" onClick={handleConfirmModeChange}> <Button color="red" onClick={handleConfirmModeChange}>
{t('pdfJsonEditor.modeChange.confirm', 'Reset and Change Mode')} {t('pdfTextEditor.modeChange.confirm', 'Reset and Change Mode')}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
@ -1809,4 +1809,4 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
); );
}; };
export default PdfJsonEditorView; export default PdfTextEditorView;

View File

@ -2,7 +2,7 @@ import { useMemo } from "react";
import LocalIcon from "@app/components/shared/LocalIcon"; import LocalIcon from "@app/components/shared/LocalIcon";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { getSynonyms } from "@app/utils/toolSynonyms"; import { getSynonyms } from "@app/utils/toolSynonyms";
import PdfJsonEditor from "@app/tools/pdfJsonEditor/PdfJsonEditor"; import PdfTextEditor from "@app/tools/pdfTextEditor/PdfTextEditor";
import { import {
SubcategoryId, SubcategoryId,
ToolCategoryId, ToolCategoryId,
@ -19,19 +19,19 @@ export function useProprietaryToolRegistry(): ProprietaryToolRegistry {
const { t } = useTranslation(); const { t } = useTranslation();
return useMemo<ProprietaryToolRegistry>(() => ({ return useMemo<ProprietaryToolRegistry>(() => ({
pdfJsonEditor: { pdfTextEditor: {
icon: <LocalIcon icon="code-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="code-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.pdfJsonEditor.title", "PDF JSON Editor"), name: t("home.pdfTextEditor.title", "PDF Text Editor"),
component: PdfJsonEditor, component: PdfTextEditor,
description: t( description: t(
"home.pdfJsonEditor.desc", "home.pdfTextEditor.desc",
"Review and edit Stirling PDF JSON exports with grouped text editing and PDF regeneration" "Review and edit Stirling PDF JSON exports with grouped text editing and PDF regeneration"
), ),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL, subcategoryId: SubcategoryId.GENERAL,
workbench: "custom:pdfJsonEditor", workbench: "custom:pdfTextEditor",
endpoints: ["json-pdf"], endpoints: ["json-pdf"],
synonyms: getSynonyms(t, "pdfJsonEditor"), synonyms: getSynonyms(t, "pdfTextEditor"),
supportsAutomate: false, supportsAutomate: false,
automationSettings: null, automationSettings: null,
}, },

View File

@ -17,8 +17,8 @@ import {
PdfJsonImageElement, PdfJsonImageElement,
PdfJsonPage, PdfJsonPage,
TextGroup, TextGroup,
PdfJsonEditorViewData, PdfTextEditorViewData,
} from './pdfJsonEditorTypes'; } from './pdfTextEditorTypes';
import { import {
deepCloneDocument, deepCloneDocument,
getDirtyPages, getDirtyPages,
@ -27,12 +27,12 @@ import {
extractDocumentImages, extractDocumentImages,
cloneImageElement, cloneImageElement,
valueOr, valueOr,
} from './pdfJsonEditorUtils'; } from './pdfTextEditorUtils';
import PdfJsonEditorView from '@app/components/tools/pdfJsonEditor/PdfJsonEditorView'; import PdfTextEditorView from '@app/components/tools/pdfTextEditor/PdfTextEditorView';
import type { PDFDocumentProxy } from 'pdfjs-dist'; import type { PDFDocumentProxy } from 'pdfjs-dist';
const VIEW_ID = 'pdfJsonEditorView'; const VIEW_ID = 'pdfTextEditorView';
const WORKBENCH_ID = 'custom:pdfJsonEditor' as const; const WORKBENCH_ID = 'custom:pdfTextEditor' as const;
const sanitizeBaseName = (name?: string | null): string => { const sanitizeBaseName = (name?: string | null): string => {
if (!name || name.trim().length === 0) { if (!name || name.trim().length === 0) {
@ -52,7 +52,7 @@ const getAutoLoadKey = (file: File): string => {
return `${file.name}|${file.size}|${file.lastModified}`; return `${file.name}|${file.size}|${file.lastModified}`;
}; };
const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => { const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
registerCustomWorkbenchView, registerCustomWorkbenchView,
@ -136,7 +136,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
); );
const hasChanges = useMemo(() => dirtyPages.some(Boolean), [dirtyPages]); const hasChanges = useMemo(() => dirtyPages.some(Boolean), [dirtyPages]);
const hasDocument = loadedDocument !== null; const hasDocument = loadedDocument !== null;
const viewLabel = useMemo(() => t('pdfJsonEditor.viewLabel', 'PDF Editor'), [t]); const viewLabel = useMemo(() => t('pdfTextEditor.viewLabel', 'PDF Editor'), [t]);
const { selectedFiles } = useFileSelection(); const { selectedFiles } = useFileSelection();
const resetToDocument = useCallback((document: PdfJsonDocument | null, mode: 'auto' | 'paragraph' | 'singleLine') => { const resetToDocument = useCallback((document: PdfJsonDocument | null, mode: 'auto' | 'paragraph' | 'singleLine') => {
@ -191,9 +191,9 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
if (!jobId) { if (!jobId) {
return; return;
} }
console.log(`[PdfJsonEditor] Cleaning up cached document for jobId: ${jobId}`); console.log(`[PdfTextEditor] Cleaning up cached document for jobId: ${jobId}`);
apiClient.post(`/api/v1/convert/pdf/json/clear-cache/${jobId}`).catch((error) => { apiClient.post(`/api/v1/convert/pdf/json/clear-cache/${jobId}`).catch((error) => {
console.warn('[PdfJsonEditor] Failed to clear cache:', error); console.warn('[PdfTextEditor] Failed to clear cache:', error);
}); });
}, []); }, []);
@ -227,7 +227,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
setHasVectorPreview(true); setHasVectorPreview(true);
} catch (error) { } catch (error) {
if (previewRequestIdRef.current === requestId) { if (previewRequestIdRef.current === requestId) {
console.warn('[PdfJsonEditor] Failed to initialise PDF preview:', error); console.warn('[PdfTextEditor] Failed to initialise PDF preview:', error);
clearPdfPreview(); clearPdfPreview();
} }
} }
@ -522,7 +522,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
} }
console.log( console.log(
`[PdfJsonEditor] Document loaded. Lazy image mode: ${shouldUseLazyMode}, Pages: ${ `[PdfTextEditor] Document loaded. Lazy image mode: ${shouldUseLazyMode}, Pages: ${
parsed.pages?.length || 0 parsed.pages?.length || 0
}`, }`,
); );
@ -559,13 +559,13 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
if (isPdf) { if (isPdf) {
const errorMsg = const errorMsg =
error?.message || error?.message ||
t('pdfJsonEditor.conversionFailed', 'Failed to convert PDF. Please try again.'); t('pdfTextEditor.conversionFailed', 'Failed to convert PDF. Please try again.');
setErrorMessage(errorMsg); setErrorMessage(errorMsg);
console.error('Setting error message:', errorMsg); console.error('Setting error message:', errorMsg);
} else { } else {
setErrorMessage( setErrorMessage(
t( t(
'pdfJsonEditor.errors.invalidJson', 'pdfTextEditor.errors.invalidJson',
'Unable to read the JSON file. Ensure it was generated by the PDF to JSON tool.', 'Unable to read the JSON file. Ensure it was generated by the PDF to JSON tool.',
), ),
); );
@ -891,7 +891,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
const message = const message =
error?.response?.data || error?.response?.data ||
error?.message || error?.message ||
t('pdfJsonEditor.errors.pdfConversion', 'Unable to convert the edited JSON back into a PDF.'); t('pdfTextEditor.errors.pdfConversion', 'Unable to convert the edited JSON back into a PDF.');
const msgString = typeof message === 'string' ? message : String(message); const msgString = typeof message === 'string' ? message : String(message);
setErrorMessage(msgString); setErrorMessage(msgString);
if (onError) { if (onError) {
@ -974,7 +974,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
} }
context.restore(); context.restore();
} catch (textError) { } catch (textError) {
console.warn('[PdfJsonEditor] Failed to strip text from preview', textError); console.warn('[PdfTextEditor] Failed to strip text from preview', textError);
} }
const dataUrl = canvas.toDataURL('image/png'); const dataUrl = canvas.toDataURL('image/png');
page.cleanup(); page.cleanup();
@ -988,7 +988,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
return next; return next;
}); });
} catch (error) { } catch (error) {
console.warn('[PdfJsonEditor] Failed to render page preview', error); console.warn('[PdfTextEditor] Failed to render page preview', error);
} finally { } finally {
previewRenderingRef.current.delete(pageIndex); previewRenderingRef.current.delete(pageIndex);
} }
@ -1004,7 +1004,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
} }
}, [groupingMode, resetToDocument]); }, [groupingMode, resetToDocument]);
const viewData = useMemo<PdfJsonEditorViewData>(() => ({ const viewData = useMemo<PdfTextEditorViewData>(() => ({
document: loadedDocument, document: loadedDocument,
groupsByPage, groupsByPage,
imagesByPage, imagesByPage,
@ -1063,7 +1063,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
setForceSingleTextElement, setForceSingleTextElement,
]); ]);
const latestViewDataRef = useRef<PdfJsonEditorViewData>(viewData); const latestViewDataRef = useRef<PdfTextEditorViewData>(viewData);
latestViewDataRef.current = viewData; latestViewDataRef.current = viewData;
// Trigger initial image loading in lazy mode // Trigger initial image loading in lazy mode
@ -1079,7 +1079,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
return; return;
} }
if (navigationState.selectedTool !== 'pdfJsonEditor') { if (navigationState.selectedTool !== 'pdfTextEditor') {
return; return;
} }
@ -1103,7 +1103,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
workbenchId: WORKBENCH_ID, workbenchId: WORKBENCH_ID,
label: viewLabel, label: viewLabel,
icon: <DescriptionIcon fontSize="small" />, icon: <DescriptionIcon fontSize="small" />,
component: PdfJsonEditorView, component: PdfTextEditorView,
}); });
setLeftPanelView('hidden'); setLeftPanelView('hidden');
setCustomWorkbenchViewData(VIEW_ID, latestViewDataRef.current); setCustomWorkbenchViewData(VIEW_ID, latestViewDataRef.current);
@ -1127,14 +1127,14 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
useEffect(() => { useEffect(() => {
if ( if (
navigationState.selectedTool === 'pdfJsonEditor' && navigationState.selectedTool === 'pdfTextEditor' &&
navigationState.workbench !== WORKBENCH_ID navigationState.workbench !== WORKBENCH_ID
) { ) {
navigationActions.setWorkbench(WORKBENCH_ID); navigationActions.setWorkbench(WORKBENCH_ID);
} }
}, [navigationActions, navigationState.selectedTool, navigationState.workbench]); }, [navigationActions, navigationState.selectedTool, navigationState.workbench]);
const lastSentViewDataRef = useRef<PdfJsonEditorViewData | null>(null); const lastSentViewDataRef = useRef<PdfTextEditorViewData | null>(null);
useEffect(() => { useEffect(() => {
if (lastSentViewDataRef.current === viewData) { if (lastSentViewDataRef.current === viewData) {
@ -1148,12 +1148,12 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
return null; return null;
}; };
(PdfJsonEditor as ToolComponent).tool = () => { (PdfTextEditor as ToolComponent).tool = () => {
throw new Error('PDF JSON Editor does not support automation operations.'); throw new Error('PDF JSON Editor does not support automation operations.');
}; };
(PdfJsonEditor as ToolComponent).getDefaultParameters = () => ({ (PdfTextEditor as ToolComponent).getDefaultParameters = () => ({
groups: [], groups: [],
}); });
export default PdfJsonEditor as ToolComponent; export default PdfTextEditor as ToolComponent;

View File

@ -1,4 +1,4 @@
import { PdfJsonDocument, PdfJsonFont } from './pdfJsonEditorTypes'; import { PdfJsonDocument, PdfJsonFont } from './pdfTextEditorTypes';
export type FontStatus = 'perfect' | 'embedded-subset' | 'system-fallback' | 'missing' | 'unknown'; export type FontStatus = 'perfect' | 'embedded-subset' | 'system-fallback' | 'missing' | 'unknown';

View File

@ -181,7 +181,7 @@ export interface ConversionProgress {
total?: number; total?: number;
} }
export interface PdfJsonEditorViewData { export interface PdfTextEditorViewData {
document: PdfJsonDocument | null; document: PdfJsonDocument | null;
groupsByPage: TextGroup[][]; groupsByPage: TextGroup[][];
imagesByPage: PdfJsonImageElement[][]; imagesByPage: PdfJsonImageElement[][];

View File

@ -7,7 +7,7 @@ import {
TextGroup, TextGroup,
DEFAULT_PAGE_HEIGHT, DEFAULT_PAGE_HEIGHT,
DEFAULT_PAGE_WIDTH, DEFAULT_PAGE_WIDTH,
} from './pdfJsonEditorTypes'; } from './pdfTextEditorTypes';
const LINE_TOLERANCE = 2; const LINE_TOLERANCE = 2;
const GAP_FACTOR = 0.6; const GAP_FACTOR = 0.6;

View File

@ -5,7 +5,7 @@
*/ */
export const PROPRIETARY_REGULAR_TOOL_IDS = [ export const PROPRIETARY_REGULAR_TOOL_IDS = [
'pdfJsonEditor', 'pdfTextEditor',
] as const; ] as const;
export const PROPRIETARY_SUPER_TOOL_IDS = [ export const PROPRIETARY_SUPER_TOOL_IDS = [