mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
cleaner UI
This commit is contained in:
parent
bbcb23ca11
commit
d4e95a6ed7
@ -7,9 +7,9 @@ logging.level.org.eclipse.jetty=WARN
|
|||||||
#logging.level.org.opensaml=DEBUG
|
#logging.level.org.opensaml=DEBUG
|
||||||
#logging.level.stirling.software.proprietary.security=DEBUG
|
#logging.level.stirling.software.proprietary.security=DEBUG
|
||||||
logging.level.com.zaxxer.hikari=WARN
|
logging.level.com.zaxxer.hikari=WARN
|
||||||
logging.level.stirling.software.SPDF.service.PdfJsonConversionService=TRACE
|
logging.level.stirling.software.SPDF.service.PdfJsonConversionService=INFO
|
||||||
logging.level.stirling.software.common.service.JobExecutorService=DEBUG
|
logging.level.stirling.software.common.service.JobExecutorService=INFO
|
||||||
logging.level.stirling.software.common.service.TaskManager=DEBUG
|
logging.level.stirling.software.common.service.TaskManager=INFO
|
||||||
spring.jpa.open-in-view=false
|
spring.jpa.open-in-view=false
|
||||||
server.forward-headers-strategy=NATIVE
|
server.forward-headers-strategy=NATIVE
|
||||||
server.error.path=/error
|
server.error.path=/error
|
||||||
|
|||||||
@ -500,7 +500,8 @@ public class PdfJsonConversionService {
|
|||||||
rewriteSucceeded = false;
|
rewriteSucceeded = false;
|
||||||
} else if (!preservedStreams.isEmpty()) {
|
} else if (!preservedStreams.isEmpty()) {
|
||||||
log.info("Attempting token rewrite for page {}", pageNumberValue);
|
log.info("Attempting token rewrite for page {}", pageNumberValue);
|
||||||
rewriteSucceeded = rewriteTextOperators(document, page, elements, false);
|
rewriteSucceeded =
|
||||||
|
rewriteTextOperators(document, page, elements, false, false);
|
||||||
if (!rewriteSucceeded) {
|
if (!rewriteSucceeded) {
|
||||||
log.info(
|
log.info(
|
||||||
"Token rewrite failed for page {}, regenerating text stream",
|
"Token rewrite failed for page {}, regenerating text stream",
|
||||||
@ -2209,7 +2210,12 @@ public class PdfJsonConversionService {
|
|||||||
PDDocument document,
|
PDDocument document,
|
||||||
PDPage page,
|
PDPage page,
|
||||||
List<PdfJsonTextElement> elements,
|
List<PdfJsonTextElement> elements,
|
||||||
boolean removeOnly) {
|
boolean removeOnly,
|
||||||
|
boolean forceRegenerate) {
|
||||||
|
if (forceRegenerate) {
|
||||||
|
log.debug("forceRegenerate flag set; skipping token rewrite for page");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (elements == null || elements.isEmpty()) {
|
if (elements == null || elements.isEmpty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -2226,6 +2232,8 @@ public class PdfJsonConversionService {
|
|||||||
PDFont currentFont = null;
|
PDFont currentFont = null;
|
||||||
String currentFontName = null;
|
String currentFontName = null;
|
||||||
|
|
||||||
|
boolean encounteredModifiedFont = false;
|
||||||
|
|
||||||
for (int i = 0; i < tokens.size(); i++) {
|
for (int i = 0; i < tokens.size(); i++) {
|
||||||
Object token = tokens.get(i);
|
Object token = tokens.get(i);
|
||||||
if (!(token instanceof Operator operator)) {
|
if (!(token instanceof Operator operator)) {
|
||||||
@ -2240,6 +2248,9 @@ public class PdfJsonConversionService {
|
|||||||
log.trace(
|
log.trace(
|
||||||
"Encountered Tf operator; switching to font resource {}",
|
"Encountered Tf operator; switching to font resource {}",
|
||||||
currentFontName);
|
currentFontName);
|
||||||
|
if (forceRegenerate) {
|
||||||
|
encounteredModifiedFont = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
currentFont = null;
|
currentFont = null;
|
||||||
currentFontName = null;
|
currentFontName = null;
|
||||||
@ -2253,7 +2264,11 @@ public class PdfJsonConversionService {
|
|||||||
"Encountered Tj without preceding string operand; aborting rewrite");
|
"Encountered Tj without preceding string operand; aborting rewrite");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
log.trace("Rewriting Tj operator using font {}", currentFontName);
|
log.trace(
|
||||||
|
"Rewriting Tj operator using font {} (token index {}, cursor remaining {})",
|
||||||
|
currentFontName,
|
||||||
|
i,
|
||||||
|
cursor.remaining());
|
||||||
if (!rewriteShowText(
|
if (!rewriteShowText(
|
||||||
cosString, currentFont, currentFontName, cursor, removeOnly)) {
|
cosString, currentFont, currentFontName, cursor, removeOnly)) {
|
||||||
log.debug("Failed to rewrite Tj operator; aborting rewrite");
|
log.debug("Failed to rewrite Tj operator; aborting rewrite");
|
||||||
@ -2265,7 +2280,11 @@ public class PdfJsonConversionService {
|
|||||||
log.debug("Encountered TJ without array operand; aborting rewrite");
|
log.debug("Encountered TJ without array operand; aborting rewrite");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
log.trace("Rewriting TJ operator using font {}", currentFontName);
|
log.trace(
|
||||||
|
"Rewriting TJ operator using font {} (token index {}, cursor remaining {})",
|
||||||
|
currentFontName,
|
||||||
|
i,
|
||||||
|
cursor.remaining());
|
||||||
if (!rewriteShowTextArray(
|
if (!rewriteShowTextArray(
|
||||||
array, currentFont, currentFontName, cursor, removeOnly)) {
|
array, currentFont, currentFontName, cursor, removeOnly)) {
|
||||||
log.debug("Failed to rewrite TJ operator; aborting rewrite");
|
log.debug("Failed to rewrite TJ operator; aborting rewrite");
|
||||||
@ -2282,6 +2301,12 @@ public class PdfJsonConversionService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (forceRegenerate && encounteredModifiedFont) {
|
||||||
|
log.debug(
|
||||||
|
"Rewrite succeeded but forceRegenerate=true, returning false to trigger rebuild");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
PDStream newStream = new PDStream(document);
|
PDStream newStream = new PDStream(document);
|
||||||
try (OutputStream outputStream = newStream.createOutputStream(COSName.FLATE_DECODE)) {
|
try (OutputStream outputStream = newStream.createOutputStream(COSName.FLATE_DECODE)) {
|
||||||
new ContentStreamWriter(outputStream).writeTokens(tokens);
|
new ContentStreamWriter(outputStream).writeTokens(tokens);
|
||||||
@ -2303,11 +2328,24 @@ public class PdfJsonConversionService {
|
|||||||
boolean removeOnly)
|
boolean removeOnly)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
if (font == null) {
|
if (font == null) {
|
||||||
|
log.debug(
|
||||||
|
"rewriteShowText aborted: no active font for expected resource {}",
|
||||||
|
expectedFontName);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
int glyphCount = countGlyphs(cosString, font);
|
int glyphCount = countGlyphs(cosString, font);
|
||||||
|
log.trace(
|
||||||
|
"rewriteShowText consuming {} glyphs at cursor index {} for font {}",
|
||||||
|
glyphCount,
|
||||||
|
cursor.index,
|
||||||
|
expectedFontName);
|
||||||
List<PdfJsonTextElement> consumed = cursor.consume(expectedFontName, glyphCount);
|
List<PdfJsonTextElement> consumed = cursor.consume(expectedFontName, glyphCount);
|
||||||
if (consumed == null) {
|
if (consumed == null) {
|
||||||
|
log.debug(
|
||||||
|
"Failed to consume {} glyphs for font {} (cursor remaining {})",
|
||||||
|
glyphCount,
|
||||||
|
expectedFontName,
|
||||||
|
cursor.remaining());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (removeOnly) {
|
if (removeOnly) {
|
||||||
@ -2320,7 +2358,10 @@ public class PdfJsonConversionService {
|
|||||||
cosString.setValue(encoded);
|
cosString.setValue(encoded);
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException | IllegalArgumentException | UnsupportedOperationException ex) {
|
} catch (IOException | IllegalArgumentException | UnsupportedOperationException ex) {
|
||||||
log.debug("Failed to encode replacement text: {}", ex.getMessage());
|
log.debug(
|
||||||
|
"Failed to encode replacement text with font {}: {}",
|
||||||
|
expectedFontName,
|
||||||
|
ex.getMessage());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2333,6 +2374,9 @@ public class PdfJsonConversionService {
|
|||||||
boolean removeOnly)
|
boolean removeOnly)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
if (font == null) {
|
if (font == null) {
|
||||||
|
log.debug(
|
||||||
|
"rewriteShowTextArray aborted: no active font for expected resource {}",
|
||||||
|
expectedFontName);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < array.size(); i++) {
|
for (int i = 0; i < array.size(); i++) {
|
||||||
@ -2341,6 +2385,12 @@ public class PdfJsonConversionService {
|
|||||||
int glyphCount = countGlyphs(cosString, font);
|
int glyphCount = countGlyphs(cosString, font);
|
||||||
List<PdfJsonTextElement> consumed = cursor.consume(expectedFontName, glyphCount);
|
List<PdfJsonTextElement> consumed = cursor.consume(expectedFontName, glyphCount);
|
||||||
if (consumed == null) {
|
if (consumed == null) {
|
||||||
|
log.debug(
|
||||||
|
"Failed to consume {} glyphs for font {} in TJ segment {} (cursor remaining {})",
|
||||||
|
glyphCount,
|
||||||
|
expectedFontName,
|
||||||
|
i,
|
||||||
|
cursor.remaining());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (removeOnly) {
|
if (removeOnly) {
|
||||||
@ -2354,7 +2404,11 @@ public class PdfJsonConversionService {
|
|||||||
} catch (IOException
|
} catch (IOException
|
||||||
| IllegalArgumentException
|
| IllegalArgumentException
|
||||||
| UnsupportedOperationException ex) {
|
| UnsupportedOperationException ex) {
|
||||||
log.debug("Failed to encode replacement text in TJ array: {}", ex.getMessage());
|
log.debug(
|
||||||
|
"Failed to encode replacement text in TJ array for font {} segment {}: {}",
|
||||||
|
expectedFontName,
|
||||||
|
i,
|
||||||
|
ex.getMessage());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2417,6 +2471,11 @@ public class PdfJsonConversionService {
|
|||||||
while (remaining > 0 && index < elements.size()) {
|
while (remaining > 0 && index < elements.size()) {
|
||||||
PdfJsonTextElement element = elements.get(index);
|
PdfJsonTextElement element = elements.get(index);
|
||||||
if (!fontMatches(expectedFontName, element.getFontId())) {
|
if (!fontMatches(expectedFontName, element.getFontId())) {
|
||||||
|
log.debug(
|
||||||
|
"Cursor consume failed: font mismatch (expected={}, actual={}) at element {}",
|
||||||
|
expectedFontName,
|
||||||
|
element.getFontId(),
|
||||||
|
index);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
consumed.add(element);
|
consumed.add(element);
|
||||||
@ -2424,6 +2483,11 @@ public class PdfJsonConversionService {
|
|||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
if (remaining > 0) {
|
if (remaining > 0) {
|
||||||
|
log.debug(
|
||||||
|
"Cursor consume failed: ran out of elements (remaining={}, currentIndex={}, total={})",
|
||||||
|
remaining,
|
||||||
|
index,
|
||||||
|
elements.size());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return consumed;
|
return consumed;
|
||||||
@ -3995,7 +4059,8 @@ public class PdfJsonConversionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasText && !preflightResult.usesFallback()) {
|
if (hasText && !preflightResult.usesFallback()) {
|
||||||
boolean rewriteSucceeded = rewriteTextOperators(document, page, textElements, false);
|
boolean rewriteSucceeded =
|
||||||
|
rewriteTextOperators(document, page, textElements, false, true);
|
||||||
if (rewriteSucceeded) {
|
if (rewriteSucceeded) {
|
||||||
return RegenerateMode.REUSE_EXISTING;
|
return RegenerateMode.REUSE_EXISTING;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4412,9 +4412,12 @@
|
|||||||
"viewLabel": "PDF Editor",
|
"viewLabel": "PDF Editor",
|
||||||
"title": "PDF Editor",
|
"title": "PDF Editor",
|
||||||
"badges": {
|
"badges": {
|
||||||
"unsaved": "Edited",
|
"unsaved": "Unsaved changes",
|
||||||
"modified": "Edited"
|
"modified": "Edited"
|
||||||
},
|
},
|
||||||
|
"controls": {
|
||||||
|
"title": "Document Controls"
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"load": "Load File",
|
"load": "Load File",
|
||||||
"reset": "Reset Changes",
|
"reset": "Reset Changes",
|
||||||
@ -4439,9 +4442,14 @@
|
|||||||
"pdfConversion": "Unable to convert the edited JSON back into a PDF."
|
"pdfConversion": "Unable to convert the edited JSON back into a PDF."
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
"title": "Viewing options",
|
||||||
"autoScaleText": {
|
"autoScaleText": {
|
||||||
"title": "Auto-scale text to fit boxes",
|
"title": "Auto-scale text to fit boxes",
|
||||||
"description": "Automatically scales text horizontally to fit within its original bounding box when font rendering differs from PDF."
|
"description": "Automatically scales text horizontally to fit within its original bounding box when font rendering differs from PDF."
|
||||||
|
},
|
||||||
|
"forceSingleElement": {
|
||||||
|
"title": "Lock edited text to a single PDF element",
|
||||||
|
"description": "When enabled, the editor exports each edited text box as one PDF text element to avoid overlapping glyphs or mixed fonts."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"disclaimer": {
|
"disclaimer": {
|
||||||
|
|||||||
@ -228,6 +228,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
isConverting,
|
isConverting,
|
||||||
conversionProgress,
|
conversionProgress,
|
||||||
hasChanges,
|
hasChanges,
|
||||||
|
forceSingleTextElement,
|
||||||
onLoadJson,
|
onLoadJson,
|
||||||
onSelectPage,
|
onSelectPage,
|
||||||
onGroupEdit,
|
onGroupEdit,
|
||||||
@ -236,6 +237,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
onReset,
|
onReset,
|
||||||
onDownloadJson,
|
onDownloadJson,
|
||||||
onGeneratePdf,
|
onGeneratePdf,
|
||||||
|
onForceSingleTextElementChange,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
const resolveFont = (fontId: string | null | undefined, pageIndex: number | null | undefined): PdfJsonFont | null => {
|
const resolveFont = (fontId: string | null | undefined, pageIndex: number | null | undefined): PdfJsonFont | null => {
|
||||||
@ -402,6 +404,56 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
const pageGroups = groupsByPage[selectedPage] ?? [];
|
const pageGroups = groupsByPage[selectedPage] ?? [];
|
||||||
const pageImages = imagesByPage[selectedPage] ?? [];
|
const pageImages = imagesByPage[selectedPage] ?? [];
|
||||||
|
|
||||||
|
const extractPreferredFontId = useCallback((target?: TextGroup | null) => {
|
||||||
|
if (!target) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (target.fontId) {
|
||||||
|
return target.fontId;
|
||||||
|
}
|
||||||
|
for (const element of target.originalElements ?? []) {
|
||||||
|
if (element.fontId) {
|
||||||
|
return element.fontId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const element of target.elements ?? []) {
|
||||||
|
if (element.fontId) {
|
||||||
|
return element.fontId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resolveFontIdForIndex = useCallback(
|
||||||
|
(index: number): string | null | undefined => {
|
||||||
|
if (index < 0 || index >= pageGroups.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const direct = extractPreferredFontId(pageGroups[index]);
|
||||||
|
if (direct) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
for (let offset = 1; offset < pageGroups.length; offset += 1) {
|
||||||
|
const prevIndex = index - offset;
|
||||||
|
if (prevIndex >= 0) {
|
||||||
|
const candidate = extractPreferredFontId(pageGroups[prevIndex]);
|
||||||
|
if (candidate) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nextIndex = index + offset;
|
||||||
|
if (nextIndex < pageGroups.length) {
|
||||||
|
const candidate = extractPreferredFontId(pageGroups[nextIndex]);
|
||||||
|
if (candidate) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
[extractPreferredFontId, pageGroups],
|
||||||
|
);
|
||||||
|
|
||||||
const fontMetrics = useMemo(() => {
|
const fontMetrics = useMemo(() => {
|
||||||
const metrics = new Map<string, { unitsPerEm: number; ascent: number; descent: number }>();
|
const metrics = new Map<string, { unitsPerEm: number; ascent: number; descent: number }>();
|
||||||
pdfDocument?.fonts?.forEach((font) => {
|
pdfDocument?.fonts?.forEach((font) => {
|
||||||
@ -545,11 +597,15 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
}, [pdfDocument?.fonts]);
|
}, [pdfDocument?.fonts]);
|
||||||
const visibleGroups = useMemo(
|
const visibleGroups = useMemo(
|
||||||
() =>
|
() =>
|
||||||
pageGroups.filter((group) => {
|
pageGroups
|
||||||
const hasContent = ((group.text ?? '').trim().length > 0) || ((group.originalText ?? '').trim().length > 0);
|
.map((group, index) => ({ group, pageGroupIndex: index }))
|
||||||
|
.filter(({ group }) => {
|
||||||
|
const hasContent =
|
||||||
|
((group.text ?? '').trim().length > 0) ||
|
||||||
|
((group.originalText ?? '').trim().length > 0);
|
||||||
return hasContent || editingGroupId === group.id;
|
return hasContent || editingGroupId === group.id;
|
||||||
}),
|
}),
|
||||||
[editingGroupId, pageGroups]
|
[editingGroupId, pageGroups],
|
||||||
);
|
);
|
||||||
|
|
||||||
const orderedImages = useMemo(
|
const orderedImages = useMemo(
|
||||||
@ -590,7 +646,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
const measureTextScales = () => {
|
const measureTextScales = () => {
|
||||||
const newScales = new Map<string, number>();
|
const newScales = new Map<string, number>();
|
||||||
|
|
||||||
visibleGroups.forEach((group) => {
|
visibleGroups.forEach(({ group }) => {
|
||||||
// Skip groups that are being edited
|
// Skip groups that are being edited
|
||||||
if (editingGroupId === group.id) {
|
if (editingGroupId === group.id) {
|
||||||
return;
|
return;
|
||||||
@ -644,7 +700,10 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
if (!editor) {
|
if (!editor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const offset = caretOffsetsRef.current.get(editingGroupId) ?? editor.innerText.length;
|
const offset = caretOffsetsRef.current.get(editingGroupId);
|
||||||
|
if (offset === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setCaretOffset(editor, offset);
|
setCaretOffset(editor, offset);
|
||||||
}, [editingGroupId, groupsByPage, imagesByPage]);
|
}, [editingGroupId, groupsByPage, imagesByPage]);
|
||||||
|
|
||||||
@ -654,14 +713,8 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
}
|
}
|
||||||
const editor = document.querySelector<HTMLElement>(`[data-editor-group="${editingGroupId}"]`);
|
const editor = document.querySelector<HTMLElement>(`[data-editor-group="${editingGroupId}"]`);
|
||||||
if (editor) {
|
if (editor) {
|
||||||
|
if (document.activeElement !== editor) {
|
||||||
editor.focus();
|
editor.focus();
|
||||||
const selection = window.getSelection();
|
|
||||||
if (selection) {
|
|
||||||
selection.removeAllRanges();
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(editor);
|
|
||||||
range.collapse(false);
|
|
||||||
selection.addRange(range);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [editingGroupId]);
|
}, [editingGroupId]);
|
||||||
@ -744,8 +797,25 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="xl" className="h-full" style={{ padding: '1.5rem', overflow: 'auto' }}>
|
<Stack
|
||||||
<Card withBorder radius="md" shadow="xs" padding="lg">
|
gap="xl"
|
||||||
|
className="h-full"
|
||||||
|
style={{
|
||||||
|
padding: '1.5rem',
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'minmax(0, 1fr) 320px',
|
||||||
|
alignItems: 'start',
|
||||||
|
gap: '1.5rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
withBorder
|
||||||
|
radius="md"
|
||||||
|
shadow="xs"
|
||||||
|
padding="lg"
|
||||||
|
style={{ gridColumn: '2 / 3', gridRow: 1, position: 'sticky', top: '1.5rem', zIndex: 2 }}
|
||||||
|
>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
@ -753,13 +823,14 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
<Title order={3}>{t('pdfJsonEditor.title', 'PDF JSON Editor')}</Title>
|
<Title order={3}>{t('pdfJsonEditor.title', 'PDF JSON Editor')}</Title>
|
||||||
{hasChanges && <Badge color="yellow" size="sm">{t('pdfJsonEditor.badges.unsaved', 'Edited')}</Badge>}
|
{hasChanges && <Badge color="yellow" size="sm">{t('pdfJsonEditor.badges.unsaved', 'Edited')}</Badge>}
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="sm">
|
<Stack gap="sm">
|
||||||
<FileButton onChange={onLoadJson} accept="application/pdf,application/json,.pdf,.json">
|
<FileButton onChange={onLoadJson} accept="application/pdf,application/json,.pdf,.json">
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
leftSection={<UploadIcon fontSize="small" />}
|
leftSection={<UploadIcon fontSize="small" />}
|
||||||
loading={isConverting}
|
loading={isConverting}
|
||||||
|
fullWidth
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{t('pdfJsonEditor.actions.load', 'Load File')}
|
{t('pdfJsonEditor.actions.load', 'Load File')}
|
||||||
@ -771,6 +842,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
leftSection={<AutorenewIcon fontSize="small" />}
|
leftSection={<AutorenewIcon fontSize="small" />}
|
||||||
onClick={onReset}
|
onClick={onReset}
|
||||||
disabled={!hasDocument || isConverting}
|
disabled={!hasDocument || isConverting}
|
||||||
|
fullWidth
|
||||||
>
|
>
|
||||||
{t('pdfJsonEditor.actions.reset', 'Reset Changes')}
|
{t('pdfJsonEditor.actions.reset', 'Reset Changes')}
|
||||||
</Button>
|
</Button>
|
||||||
@ -779,6 +851,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
leftSection={<FileDownloadIcon fontSize="small" />}
|
leftSection={<FileDownloadIcon fontSize="small" />}
|
||||||
onClick={onDownloadJson}
|
onClick={onDownloadJson}
|
||||||
disabled={!hasDocument || isConverting}
|
disabled={!hasDocument || isConverting}
|
||||||
|
fullWidth
|
||||||
>
|
>
|
||||||
{t('pdfJsonEditor.actions.downloadJson', 'Download JSON')}
|
{t('pdfJsonEditor.actions.downloadJson', 'Download JSON')}
|
||||||
</Button>
|
</Button>
|
||||||
@ -787,10 +860,11 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
onClick={onGeneratePdf}
|
onClick={onGeneratePdf}
|
||||||
loading={isGeneratingPdf}
|
loading={isGeneratingPdf}
|
||||||
disabled={!hasDocument || !hasChanges || isConverting}
|
disabled={!hasDocument || !hasChanges || isConverting}
|
||||||
|
fullWidth
|
||||||
>
|
>
|
||||||
{t('pdfJsonEditor.actions.generatePdf', 'Generate PDF')}
|
{t('pdfJsonEditor.actions.generatePdf', 'Generate PDF')}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{fileName && (
|
{fileName && (
|
||||||
@ -819,6 +893,25 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
onChange={(event) => setAutoScaleText(event.currentTarget.checked)}
|
onChange={(event) => setAutoScaleText(event.currentTarget.checked)}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<div>
|
||||||
|
<Text fw={500} size="sm">
|
||||||
|
{t('pdfJsonEditor.options.forceSingleElement.title', 'Lock edited text to a single PDF element')}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed" mt={4}>
|
||||||
|
{t(
|
||||||
|
'pdfJsonEditor.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>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -827,6 +920,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
color="yellow"
|
color="yellow"
|
||||||
radius="md"
|
radius="md"
|
||||||
variant="light"
|
variant="light"
|
||||||
|
style={{ gridColumn: '2 / 3' }}
|
||||||
>
|
>
|
||||||
<Stack gap={4}>
|
<Stack gap={4}>
|
||||||
<Text fw={600}>
|
<Text fw={600}>
|
||||||
@ -853,14 +947,82 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
|
{hasDocument && (
|
||||||
|
<Card
|
||||||
|
withBorder
|
||||||
|
radius="md"
|
||||||
|
padding="md"
|
||||||
|
shadow="xs"
|
||||||
|
style={{ gridColumn: '2 / 3' }}
|
||||||
|
>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Text fw={500}>{t('pdfJsonEditor.groupList', 'Detected Text Groups')}</Text>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => setTextGroupsExpanded(!textGroupsExpanded)}
|
||||||
|
aria-label={textGroupsExpanded ? 'Collapse' : 'Expand'}
|
||||||
|
>
|
||||||
|
{textGroupsExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
<Collapse in={textGroupsExpanded}>
|
||||||
|
<ScrollArea h={240} offsetScrollbars>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{visibleGroups.map(({ group }) => {
|
||||||
|
const changed = group.text !== group.originalText;
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={`list-${group.id}`}
|
||||||
|
padding="sm"
|
||||||
|
radius="md"
|
||||||
|
withBorder
|
||||||
|
shadow={changed ? 'sm' : 'none'}
|
||||||
|
onMouseEnter={() => setActiveGroupId(group.id)}
|
||||||
|
onMouseLeave={() => setActiveGroupId((current) => (current === group.id ? null : current))}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveGroupId(group.id);
|
||||||
|
setEditingGroupId(group.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Group gap="xs">
|
||||||
|
{changed && <Badge color="yellow" size="xs">{t('pdfJsonEditor.badges.modified', 'Edited')}</Badge>}
|
||||||
|
{group.fontId && <Badge size="xs" variant="outline">{group.fontId}</Badge>}
|
||||||
|
{group.fontSize && (
|
||||||
|
<Badge size="xs" variant="light">
|
||||||
|
{t('pdfJsonEditor.fontSizeValue', '{{size}}pt', { size: group.fontSize.toFixed(1) })}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
<Text size="sm" c="dimmed" lineClamp={2}>
|
||||||
|
{group.text || t('pdfJsonEditor.emptyGroup', '[Empty Group]')}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea>
|
||||||
|
</Collapse>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<Alert icon={<WarningAmberIcon fontSize="small" />} color="red" radius="md">
|
<Alert
|
||||||
|
icon={<WarningAmberIcon fontSize="small" />}
|
||||||
|
color="red"
|
||||||
|
radius="md"
|
||||||
|
style={{ gridColumn: '2 / 3' }}
|
||||||
|
>
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasDocument && !isConverting && (
|
{!hasDocument && !isConverting && (
|
||||||
<Card withBorder radius="md" padding="xl">
|
<Card withBorder radius="md" padding="xl" style={{ gridColumn: '1 / 2', gridRow: 1 }}>
|
||||||
<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}>
|
||||||
@ -874,7 +1036,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isConverting && (
|
{isConverting && (
|
||||||
<Card withBorder radius="md" padding="xl">
|
<Card withBorder radius="md" padding="xl" style={{ gridColumn: '1 / 2', gridRow: 1 }}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Group justify="space-between" align="flex-start">
|
<Group justify="space-between" align="flex-start">
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
@ -899,22 +1061,13 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<AutorenewIcon sx={{ fontSize: 36 }} className="animate-spin" />
|
<AutorenewIcon sx={{ fontSize: 36 }} className="animate-spin" />
|
||||||
</Group>
|
</Group>
|
||||||
<Progress
|
<Progress value={conversionProgress?.percent || 0} size="lg" radius="md" />
|
||||||
value={conversionProgress?.percent || 0}
|
|
||||||
size="lg"
|
|
||||||
radius="md"
|
|
||||||
animated
|
|
||||||
striped
|
|
||||||
/>
|
|
||||||
<Text size="sm" c="dimmed" ta="right">
|
|
||||||
{conversionProgress?.percent || 0}% complete
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasDocument && (
|
{hasDocument && (
|
||||||
<Stack gap="lg" className="flex-1" style={{ minHeight: 0 }}>
|
<Stack gap="lg" className="flex-1" style={{ gridColumn: '1 / 2', gridRow: 1, minHeight: 0 }}>
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Text fw={500}>
|
<Text fw={500}>
|
||||||
@ -1091,20 +1244,21 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
) : (
|
) : (
|
||||||
visibleGroups.map((group) => {
|
visibleGroups.map(({ group, pageGroupIndex }) => {
|
||||||
const bounds = toCssBounds(currentPage, pageHeight, scale, group.bounds);
|
const bounds = toCssBounds(currentPage, pageHeight, scale, group.bounds);
|
||||||
const changed = group.text !== group.originalText;
|
const changed = group.text !== group.originalText;
|
||||||
const isActive = activeGroupId === group.id || editingGroupId === group.id;
|
const isActive = activeGroupId === group.id || editingGroupId === group.id;
|
||||||
const isEditing = editingGroupId === group.id;
|
const isEditing = editingGroupId === group.id;
|
||||||
const baseFontSize = group.fontMatrixSize ?? group.fontSize ?? 12;
|
const baseFontSize = group.fontMatrixSize ?? group.fontSize ?? 12;
|
||||||
const fontSizePx = Math.max(baseFontSize * scale, 6);
|
const fontSizePx = Math.max(baseFontSize * scale, 6);
|
||||||
const fontFamily = getFontFamily(group.fontId, group.pageIndex);
|
const effectiveFontId = resolveFontIdForIndex(pageGroupIndex) ?? group.fontId;
|
||||||
let lineHeightPx = getLineHeightPx(group.fontId, group.pageIndex, fontSizePx);
|
const fontFamily = getFontFamily(effectiveFontId, group.pageIndex);
|
||||||
|
let lineHeightPx = getLineHeightPx(effectiveFontId, group.pageIndex, fontSizePx);
|
||||||
let lineHeightRatio = fontSizePx > 0 ? Math.max(lineHeightPx / fontSizePx, 1.05) : 1.2;
|
let lineHeightRatio = fontSizePx > 0 ? Math.max(lineHeightPx / fontSizePx, 1.05) : 1.2;
|
||||||
const rotation = group.rotation ?? 0;
|
const rotation = group.rotation ?? 0;
|
||||||
const hasRotation = Math.abs(rotation) > 0.5;
|
const hasRotation = Math.abs(rotation) > 0.5;
|
||||||
const baselineLength = group.baselineLength ?? Math.max(group.bounds.right - group.bounds.left, 0);
|
const baselineLength = group.baselineLength ?? Math.max(group.bounds.right - group.bounds.left, 0);
|
||||||
const geometry = getFontGeometry(group.fontId, group.pageIndex);
|
const geometry = getFontGeometry(effectiveFontId, group.pageIndex);
|
||||||
const ascentPx = geometry ? Math.max(fontSizePx * geometry.ascentRatio, fontSizePx * 0.7) : fontSizePx * 0.82;
|
const ascentPx = geometry ? Math.max(fontSizePx * geometry.ascentRatio, fontSizePx * 0.7) : fontSizePx * 0.82;
|
||||||
const descentPx = geometry ? Math.max(fontSizePx * geometry.descentRatio, fontSizePx * 0.2) : fontSizePx * 0.22;
|
const descentPx = geometry ? Math.max(fontSizePx * geometry.descentRatio, fontSizePx * 0.2) : fontSizePx * 0.22;
|
||||||
lineHeightPx = Math.max(lineHeightPx, ascentPx + descentPx);
|
lineHeightPx = Math.max(lineHeightPx, ascentPx + descentPx);
|
||||||
@ -1143,7 +1297,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
|
|
||||||
// Extract styling from group
|
// Extract styling from group
|
||||||
const textColor = group.color || '#111827';
|
const textColor = group.color || '#111827';
|
||||||
const fontWeight = group.fontWeight || getFontWeight(group.fontId, group.pageIndex);
|
const fontWeight = group.fontWeight || getFontWeight(effectiveFontId, group.pageIndex);
|
||||||
|
|
||||||
const containerStyle: React.CSSProperties = {
|
const containerStyle: React.CSSProperties = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@ -1179,6 +1333,22 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
contentEditable
|
contentEditable
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
data-editor-group={group.id}
|
data-editor-group={group.id}
|
||||||
|
onFocus={(event) => {
|
||||||
|
const primaryFont = fontFamily.split(',')[0]?.replace(/['"]/g, '').trim();
|
||||||
|
if (primaryFont && typeof document !== 'undefined') {
|
||||||
|
try {
|
||||||
|
if (document.queryCommandSupported?.('styleWithCSS')) {
|
||||||
|
document.execCommand('styleWithCSS', false, true);
|
||||||
|
}
|
||||||
|
if (document.queryCommandSupported?.('fontName')) {
|
||||||
|
document.execCommand('fontName', false, primaryFont);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore execCommand failures; inline style already enforces font
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.currentTarget.style.fontFamily = fontFamily;
|
||||||
|
}}
|
||||||
onBlur={(event) => {
|
onBlur={(event) => {
|
||||||
const value = event.currentTarget.innerText.replace(/\u00A0/g, ' ');
|
const value = event.currentTarget.innerText.replace(/\u00A0/g, ' ');
|
||||||
caretOffsetsRef.current.delete(group.id);
|
caretOffsetsRef.current.delete(group.id);
|
||||||
@ -1280,65 +1450,6 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card padding="md" withBorder radius="md">
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Text fw={500}>{t('pdfJsonEditor.groupList', 'Detected Text Groups')}</Text>
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
onClick={() => setTextGroupsExpanded(!textGroupsExpanded)}
|
|
||||||
aria-label={textGroupsExpanded ? 'Collapse' : 'Expand'}
|
|
||||||
>
|
|
||||||
{textGroupsExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
<Collapse in={textGroupsExpanded}>
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Divider />
|
|
||||||
<ScrollArea h={180} offsetScrollbars>
|
|
||||||
<Stack gap="sm">
|
|
||||||
{visibleGroups.map((group) => {
|
|
||||||
const changed = group.text !== group.originalText;
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={`list-${group.id}`}
|
|
||||||
padding="sm"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
shadow={changed ? 'sm' : 'none'}
|
|
||||||
onMouseEnter={() => setActiveGroupId(group.id)}
|
|
||||||
onMouseLeave={() => setActiveGroupId((current) => (current === group.id ? null : current))}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
onClick={() => {
|
|
||||||
setActiveGroupId(group.id);
|
|
||||||
setEditingGroupId(group.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack gap={4}>
|
|
||||||
<Group gap="xs">
|
|
||||||
{changed && <Badge color="yellow" size="xs">{t('pdfJsonEditor.badges.modified', 'Edited')}</Badge>}
|
|
||||||
{group.fontId && (
|
|
||||||
<Badge size="xs" variant="outline">{group.fontId}</Badge>
|
|
||||||
)}
|
|
||||||
{group.fontSize && (
|
|
||||||
<Badge size="xs" variant="light">
|
|
||||||
{t('pdfJsonEditor.fontSizeValue', '{{size}}pt', { size: group.fontSize.toFixed(1) })}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
<Text size="sm" c="dimmed" lineClamp={2}>
|
|
||||||
{group.text || t('pdfJsonEditor.emptyGroup', '[Empty Group]')}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
|
||||||
</ScrollArea>
|
|
||||||
</Stack>
|
|
||||||
</Collapse>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@ -74,6 +74,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
stage: string;
|
stage: string;
|
||||||
message: string;
|
message: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [forceSingleTextElement, setForceSingleTextElement] = useState(false);
|
||||||
|
|
||||||
// Lazy loading state
|
// Lazy loading state
|
||||||
const [isLazyMode, setIsLazyMode] = useState(false);
|
const [isLazyMode, setIsLazyMode] = useState(false);
|
||||||
@ -615,13 +616,14 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
groupsByPage,
|
groupsByPage,
|
||||||
imagesByPageRef.current,
|
imagesByPageRef.current,
|
||||||
originalImagesRef.current,
|
originalImagesRef.current,
|
||||||
|
forceSingleTextElement,
|
||||||
);
|
);
|
||||||
const baseName = sanitizeBaseName(fileName || loadedDocument.metadata?.title || undefined);
|
const baseName = sanitizeBaseName(fileName || loadedDocument.metadata?.title || undefined);
|
||||||
return {
|
return {
|
||||||
document: updatedDocument,
|
document: updatedDocument,
|
||||||
filename: `${baseName}.json`,
|
filename: `${baseName}.json`,
|
||||||
};
|
};
|
||||||
}, [fileName, groupsByPage, loadedDocument]);
|
}, [fileName, forceSingleTextElement, groupsByPage, loadedDocument]);
|
||||||
|
|
||||||
const handleDownloadJson = useCallback(() => {
|
const handleDownloadJson = useCallback(() => {
|
||||||
const payload = buildPayload();
|
const payload = buildPayload();
|
||||||
@ -817,6 +819,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
isConverting,
|
isConverting,
|
||||||
conversionProgress,
|
conversionProgress,
|
||||||
hasChanges,
|
hasChanges,
|
||||||
|
forceSingleTextElement,
|
||||||
onLoadJson: handleLoadFile,
|
onLoadJson: handleLoadFile,
|
||||||
onSelectPage: handleSelectPage,
|
onSelectPage: handleSelectPage,
|
||||||
onGroupEdit: handleGroupTextChange,
|
onGroupEdit: handleGroupTextChange,
|
||||||
@ -825,6 +828,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
onReset: handleResetEdits,
|
onReset: handleResetEdits,
|
||||||
onDownloadJson: handleDownloadJson,
|
onDownloadJson: handleDownloadJson,
|
||||||
onGeneratePdf: handleGeneratePdf,
|
onGeneratePdf: handleGeneratePdf,
|
||||||
|
onForceSingleTextElementChange: setForceSingleTextElement,
|
||||||
}), [
|
}), [
|
||||||
handleImageTransform,
|
handleImageTransform,
|
||||||
imagesByPage,
|
imagesByPage,
|
||||||
@ -846,6 +850,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
conversionProgress,
|
conversionProgress,
|
||||||
loadedDocument,
|
loadedDocument,
|
||||||
selectedPage,
|
selectedPage,
|
||||||
|
forceSingleTextElement,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const latestViewDataRef = useRef<PdfJsonEditorViewData>(viewData);
|
const latestViewDataRef = useRef<PdfJsonEditorViewData>(viewData);
|
||||||
|
|||||||
@ -191,6 +191,7 @@ export interface PdfJsonEditorViewData {
|
|||||||
isConverting: boolean;
|
isConverting: boolean;
|
||||||
conversionProgress: ConversionProgress | null;
|
conversionProgress: ConversionProgress | null;
|
||||||
hasChanges: boolean;
|
hasChanges: boolean;
|
||||||
|
forceSingleTextElement: boolean;
|
||||||
onLoadJson: (file: File | null) => Promise<void> | void;
|
onLoadJson: (file: File | null) => Promise<void> | void;
|
||||||
onSelectPage: (pageIndex: number) => void;
|
onSelectPage: (pageIndex: number) => void;
|
||||||
onGroupEdit: (pageIndex: number, groupId: string, value: string) => void;
|
onGroupEdit: (pageIndex: number, groupId: string, value: string) => void;
|
||||||
@ -209,4 +210,5 @@ export interface PdfJsonEditorViewData {
|
|||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
onDownloadJson: () => void;
|
onDownloadJson: () => void;
|
||||||
onGeneratePdf: () => void;
|
onGeneratePdf: () => void;
|
||||||
|
onForceSingleTextElementChange: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -689,6 +689,7 @@ export const restoreGlyphElements = (
|
|||||||
groupsByPage: TextGroup[][],
|
groupsByPage: TextGroup[][],
|
||||||
imagesByPage: PdfJsonImageElement[][],
|
imagesByPage: PdfJsonImageElement[][],
|
||||||
originalImagesByPage: PdfJsonImageElement[][],
|
originalImagesByPage: PdfJsonImageElement[][],
|
||||||
|
forceMergedGroups: boolean = false,
|
||||||
): PdfJsonDocument => {
|
): PdfJsonDocument => {
|
||||||
const updated = deepCloneDocument(source);
|
const updated = deepCloneDocument(source);
|
||||||
const pages = updated.pages ?? [];
|
const pages = updated.pages ?? [];
|
||||||
@ -709,6 +710,10 @@ export const restoreGlyphElements = (
|
|||||||
|
|
||||||
groups.forEach((group) => {
|
groups.forEach((group) => {
|
||||||
if (group.text !== group.originalText) {
|
if (group.text !== group.originalText) {
|
||||||
|
if (forceMergedGroups) {
|
||||||
|
rebuiltElements.push(createMergedElement(group));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const originalGlyphCount = group.originalElements.reduce(
|
const originalGlyphCount = group.originalElements.reduce(
|
||||||
(sum, element) => sum + countGraphemes(element.text ?? ''),
|
(sum, element) => sum + countGraphemes(element.text ?? ''),
|
||||||
0,
|
0,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user