diff --git a/app/core/src/main/resources/application.properties b/app/core/src/main/resources/application.properties index cb96934c3..7cc321b2d 100644 --- a/app/core/src/main/resources/application.properties +++ b/app/core/src/main/resources/application.properties @@ -7,9 +7,9 @@ logging.level.org.eclipse.jetty=WARN #logging.level.org.opensaml=DEBUG #logging.level.stirling.software.proprietary.security=DEBUG logging.level.com.zaxxer.hikari=WARN -logging.level.stirling.software.SPDF.service.PdfJsonConversionService=TRACE -logging.level.stirling.software.common.service.JobExecutorService=DEBUG -logging.level.stirling.software.common.service.TaskManager=DEBUG +logging.level.stirling.software.SPDF.service.PdfJsonConversionService=INFO +logging.level.stirling.software.common.service.JobExecutorService=INFO +logging.level.stirling.software.common.service.TaskManager=INFO spring.jpa.open-in-view=false server.forward-headers-strategy=NATIVE server.error.path=/error diff --git a/app/proprietary/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java b/app/proprietary/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java index b86c118dc..fa91cfee4 100644 --- a/app/proprietary/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java +++ b/app/proprietary/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java @@ -500,7 +500,8 @@ public class PdfJsonConversionService { rewriteSucceeded = false; } else if (!preservedStreams.isEmpty()) { log.info("Attempting token rewrite for page {}", pageNumberValue); - rewriteSucceeded = rewriteTextOperators(document, page, elements, false); + rewriteSucceeded = + rewriteTextOperators(document, page, elements, false, false); if (!rewriteSucceeded) { log.info( "Token rewrite failed for page {}, regenerating text stream", @@ -2209,7 +2210,12 @@ public class PdfJsonConversionService { PDDocument document, PDPage page, List 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()) { return true; } @@ -2226,6 +2232,8 @@ public class PdfJsonConversionService { PDFont currentFont = null; String currentFontName = null; + boolean encounteredModifiedFont = false; + for (int i = 0; i < tokens.size(); i++) { Object token = tokens.get(i); if (!(token instanceof Operator operator)) { @@ -2240,6 +2248,9 @@ public class PdfJsonConversionService { log.trace( "Encountered Tf operator; switching to font resource {}", currentFontName); + if (forceRegenerate) { + encounteredModifiedFont = true; + } } else { currentFont = null; currentFontName = null; @@ -2253,7 +2264,11 @@ public class PdfJsonConversionService { "Encountered Tj without preceding string operand; aborting rewrite"); 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( cosString, currentFont, currentFontName, cursor, removeOnly)) { 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"); 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( array, currentFont, currentFontName, cursor, removeOnly)) { log.debug("Failed to rewrite TJ operator; aborting rewrite"); @@ -2282,6 +2301,12 @@ public class PdfJsonConversionService { return false; } + if (forceRegenerate && encounteredModifiedFont) { + log.debug( + "Rewrite succeeded but forceRegenerate=true, returning false to trigger rebuild"); + return false; + } + PDStream newStream = new PDStream(document); try (OutputStream outputStream = newStream.createOutputStream(COSName.FLATE_DECODE)) { new ContentStreamWriter(outputStream).writeTokens(tokens); @@ -2303,11 +2328,24 @@ public class PdfJsonConversionService { boolean removeOnly) throws IOException { if (font == null) { + log.debug( + "rewriteShowText aborted: no active font for expected resource {}", + expectedFontName); return false; } int glyphCount = countGlyphs(cosString, font); + log.trace( + "rewriteShowText consuming {} glyphs at cursor index {} for font {}", + glyphCount, + cursor.index, + expectedFontName); List consumed = cursor.consume(expectedFontName, glyphCount); if (consumed == null) { + log.debug( + "Failed to consume {} glyphs for font {} (cursor remaining {})", + glyphCount, + expectedFontName, + cursor.remaining()); return false; } if (removeOnly) { @@ -2320,7 +2358,10 @@ public class PdfJsonConversionService { cosString.setValue(encoded); return true; } 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; } } @@ -2333,6 +2374,9 @@ public class PdfJsonConversionService { boolean removeOnly) throws IOException { if (font == null) { + log.debug( + "rewriteShowTextArray aborted: no active font for expected resource {}", + expectedFontName); return false; } for (int i = 0; i < array.size(); i++) { @@ -2341,6 +2385,12 @@ public class PdfJsonConversionService { int glyphCount = countGlyphs(cosString, font); List consumed = cursor.consume(expectedFontName, glyphCount); if (consumed == null) { + log.debug( + "Failed to consume {} glyphs for font {} in TJ segment {} (cursor remaining {})", + glyphCount, + expectedFontName, + i, + cursor.remaining()); return false; } if (removeOnly) { @@ -2354,7 +2404,11 @@ public class PdfJsonConversionService { } catch (IOException | IllegalArgumentException | 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; } } @@ -2417,6 +2471,11 @@ public class PdfJsonConversionService { while (remaining > 0 && index < elements.size()) { PdfJsonTextElement element = elements.get(index); if (!fontMatches(expectedFontName, element.getFontId())) { + log.debug( + "Cursor consume failed: font mismatch (expected={}, actual={}) at element {}", + expectedFontName, + element.getFontId(), + index); return null; } consumed.add(element); @@ -2424,6 +2483,11 @@ public class PdfJsonConversionService { index++; } if (remaining > 0) { + log.debug( + "Cursor consume failed: ran out of elements (remaining={}, currentIndex={}, total={})", + remaining, + index, + elements.size()); return null; } return consumed; @@ -3995,7 +4059,8 @@ public class PdfJsonConversionService { } if (hasText && !preflightResult.usesFallback()) { - boolean rewriteSucceeded = rewriteTextOperators(document, page, textElements, false); + boolean rewriteSucceeded = + rewriteTextOperators(document, page, textElements, false, true); if (rewriteSucceeded) { return RegenerateMode.REUSE_EXISTING; } diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 63677052b..a8aa15173 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -4412,9 +4412,12 @@ "viewLabel": "PDF Editor", "title": "PDF Editor", "badges": { - "unsaved": "Edited", + "unsaved": "Unsaved changes", "modified": "Edited" }, + "controls": { + "title": "Document Controls" + }, "actions": { "load": "Load File", "reset": "Reset Changes", @@ -4439,9 +4442,14 @@ "pdfConversion": "Unable to convert the edited JSON back into a PDF." }, "options": { + "title": "Viewing options", "autoScaleText": { "title": "Auto-scale text to fit boxes", "description": "Automatically scales text horizontally to fit within its original bounding box when font rendering differs from PDF." + }, + "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": { diff --git a/frontend/src/proprietary/components/tools/pdfJsonEditor/PdfJsonEditorView.tsx b/frontend/src/proprietary/components/tools/pdfJsonEditor/PdfJsonEditorView.tsx index 9aadb26a7..c3ae8cd1e 100644 --- a/frontend/src/proprietary/components/tools/pdfJsonEditor/PdfJsonEditorView.tsx +++ b/frontend/src/proprietary/components/tools/pdfJsonEditor/PdfJsonEditorView.tsx @@ -228,6 +228,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { isConverting, conversionProgress, hasChanges, + forceSingleTextElement, onLoadJson, onSelectPage, onGroupEdit, @@ -236,6 +237,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { onReset, onDownloadJson, onGeneratePdf, + onForceSingleTextElementChange, } = data; 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 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 metrics = new Map(); pdfDocument?.fonts?.forEach((font) => { @@ -545,11 +597,15 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { }, [pdfDocument?.fonts]); const visibleGroups = useMemo( () => - pageGroups.filter((group) => { - const hasContent = ((group.text ?? '').trim().length > 0) || ((group.originalText ?? '').trim().length > 0); - return hasContent || editingGroupId === group.id; - }), - [editingGroupId, pageGroups] + pageGroups + .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; + }), + [editingGroupId, pageGroups], ); const orderedImages = useMemo( @@ -590,7 +646,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { const measureTextScales = () => { const newScales = new Map(); - visibleGroups.forEach((group) => { + visibleGroups.forEach(({ group }) => { // Skip groups that are being edited if (editingGroupId === group.id) { return; @@ -644,7 +700,10 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { if (!editor) { return; } - const offset = caretOffsetsRef.current.get(editingGroupId) ?? editor.innerText.length; + const offset = caretOffsetsRef.current.get(editingGroupId); + if (offset === undefined) { + return; + } setCaretOffset(editor, offset); }, [editingGroupId, groupsByPage, imagesByPage]); @@ -654,14 +713,8 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { } const editor = document.querySelector(`[data-editor-group="${editingGroupId}"]`); if (editor) { - editor.focus(); - const selection = window.getSelection(); - if (selection) { - selection.removeAllRanges(); - const range = document.createRange(); - range.selectNodeContents(editor); - range.collapse(false); - selection.addRange(range); + if (document.activeElement !== editor) { + editor.focus(); } } }, [editingGroupId]); @@ -744,8 +797,25 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { ); return ( - - + + @@ -753,13 +823,14 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { {t('pdfJsonEditor.title', 'PDF JSON Editor')} {hasChanges && {t('pdfJsonEditor.badges.unsaved', 'Edited')}} - + {(props) => ( @@ -779,6 +851,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { leftSection={} onClick={onDownloadJson} disabled={!hasDocument || isConverting} + fullWidth > {t('pdfJsonEditor.actions.downloadJson', 'Download JSON')} @@ -787,10 +860,11 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { onClick={onGeneratePdf} loading={isGeneratingPdf} disabled={!hasDocument || !hasChanges || isConverting} + fullWidth > {t('pdfJsonEditor.actions.generatePdf', 'Generate PDF')} - + {fileName && ( @@ -819,6 +893,25 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { onChange={(event) => setAutoScaleText(event.currentTarget.checked)} /> + + +
+ + {t('pdfJsonEditor.options.forceSingleElement.title', 'Lock edited text to a single PDF element')} + + + {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.' + )} + +
+ onForceSingleTextElementChange(event.currentTarget.checked)} + /> +
@@ -827,6 +920,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { color="yellow" radius="md" variant="light" + style={{ gridColumn: '2 / 3' }} > @@ -853,14 +947,82 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { + {hasDocument && ( + + + + {t('pdfJsonEditor.groupList', 'Detected Text Groups')} + setTextGroupsExpanded(!textGroupsExpanded)} + aria-label={textGroupsExpanded ? 'Collapse' : 'Expand'} + > + {textGroupsExpanded ? : } + + + + + + {visibleGroups.map(({ group }) => { + const changed = group.text !== group.originalText; + return ( + setActiveGroupId(group.id)} + onMouseLeave={() => setActiveGroupId((current) => (current === group.id ? null : current))} + style={{ cursor: 'pointer' }} + onClick={() => { + setActiveGroupId(group.id); + setEditingGroupId(group.id); + }} + > + + + {changed && {t('pdfJsonEditor.badges.modified', 'Edited')}} + {group.fontId && {group.fontId}} + {group.fontSize && ( + + {t('pdfJsonEditor.fontSizeValue', '{{size}}pt', { size: group.fontSize.toFixed(1) })} + + )} + + + {group.text || t('pdfJsonEditor.emptyGroup', '[Empty Group]')} + + + + ); + })} + + + + + + )} + {errorMessage && ( - } color="red" radius="md"> + } + color="red" + radius="md" + style={{ gridColumn: '2 / 3' }} + > {errorMessage} )} {!hasDocument && !isConverting && ( - + @@ -874,7 +1036,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { )} {isConverting && ( - +
@@ -899,22 +1061,13 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
- - - {conversionProgress?.percent || 0}% complete - +
)} {hasDocument && ( - + @@ -1091,20 +1244,21 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { ) : ( - visibleGroups.map((group) => { + visibleGroups.map(({ group, pageGroupIndex }) => { const bounds = toCssBounds(currentPage, pageHeight, scale, group.bounds); const changed = group.text !== group.originalText; const isActive = activeGroupId === group.id || editingGroupId === group.id; const isEditing = editingGroupId === group.id; const baseFontSize = group.fontMatrixSize ?? group.fontSize ?? 12; const fontSizePx = Math.max(baseFontSize * scale, 6); - const fontFamily = getFontFamily(group.fontId, group.pageIndex); - let lineHeightPx = getLineHeightPx(group.fontId, group.pageIndex, fontSizePx); + const effectiveFontId = resolveFontIdForIndex(pageGroupIndex) ?? group.fontId; + 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; const rotation = group.rotation ?? 0; const hasRotation = Math.abs(rotation) > 0.5; 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 descentPx = geometry ? Math.max(fontSizePx * geometry.descentRatio, fontSizePx * 0.2) : fontSizePx * 0.22; lineHeightPx = Math.max(lineHeightPx, ascentPx + descentPx); @@ -1143,7 +1297,7 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { // Extract styling from group 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 = { position: 'absolute', @@ -1179,6 +1333,22 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => { contentEditable suppressContentEditableWarning 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) => { const value = event.currentTarget.innerText.replace(/\u00A0/g, ' '); caretOffsetsRef.current.delete(group.id); @@ -1280,65 +1450,6 @@ const PdfJsonEditorView = ({ data }: PdfJsonEditorViewProps) => {
- - - - {t('pdfJsonEditor.groupList', 'Detected Text Groups')} - setTextGroupsExpanded(!textGroupsExpanded)} - aria-label={textGroupsExpanded ? 'Collapse' : 'Expand'} - > - {textGroupsExpanded ? : } - - - - - - - - {visibleGroups.map((group) => { - const changed = group.text !== group.originalText; - return ( - setActiveGroupId(group.id)} - onMouseLeave={() => setActiveGroupId((current) => (current === group.id ? null : current))} - style={{ cursor: 'pointer' }} - onClick={() => { - setActiveGroupId(group.id); - setEditingGroupId(group.id); - }} - > - - - {changed && {t('pdfJsonEditor.badges.modified', 'Edited')}} - {group.fontId && ( - {group.fontId} - )} - {group.fontSize && ( - - {t('pdfJsonEditor.fontSizeValue', '{{size}}pt', { size: group.fontSize.toFixed(1) })} - - )} - - - {group.text || t('pdfJsonEditor.emptyGroup', '[Empty Group]')} - - - - ); - })} - - - - - -
)}
diff --git a/frontend/src/proprietary/tools/pdfJsonEditor/PdfJsonEditor.tsx b/frontend/src/proprietary/tools/pdfJsonEditor/PdfJsonEditor.tsx index 366db7d45..b236de5a8 100644 --- a/frontend/src/proprietary/tools/pdfJsonEditor/PdfJsonEditor.tsx +++ b/frontend/src/proprietary/tools/pdfJsonEditor/PdfJsonEditor.tsx @@ -74,6 +74,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => { stage: string; message: string; } | null>(null); + const [forceSingleTextElement, setForceSingleTextElement] = useState(false); // Lazy loading state const [isLazyMode, setIsLazyMode] = useState(false); @@ -615,13 +616,14 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => { groupsByPage, imagesByPageRef.current, originalImagesRef.current, + forceSingleTextElement, ); const baseName = sanitizeBaseName(fileName || loadedDocument.metadata?.title || undefined); return { document: updatedDocument, filename: `${baseName}.json`, }; - }, [fileName, groupsByPage, loadedDocument]); + }, [fileName, forceSingleTextElement, groupsByPage, loadedDocument]); const handleDownloadJson = useCallback(() => { const payload = buildPayload(); @@ -817,6 +819,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => { isConverting, conversionProgress, hasChanges, + forceSingleTextElement, onLoadJson: handleLoadFile, onSelectPage: handleSelectPage, onGroupEdit: handleGroupTextChange, @@ -825,6 +828,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => { onReset: handleResetEdits, onDownloadJson: handleDownloadJson, onGeneratePdf: handleGeneratePdf, + onForceSingleTextElementChange: setForceSingleTextElement, }), [ handleImageTransform, imagesByPage, @@ -846,6 +850,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => { conversionProgress, loadedDocument, selectedPage, + forceSingleTextElement, ]); const latestViewDataRef = useRef(viewData); diff --git a/frontend/src/proprietary/tools/pdfJsonEditor/pdfJsonEditorTypes.ts b/frontend/src/proprietary/tools/pdfJsonEditor/pdfJsonEditorTypes.ts index 98323f161..2733509c6 100644 --- a/frontend/src/proprietary/tools/pdfJsonEditor/pdfJsonEditorTypes.ts +++ b/frontend/src/proprietary/tools/pdfJsonEditor/pdfJsonEditorTypes.ts @@ -191,6 +191,7 @@ export interface PdfJsonEditorViewData { isConverting: boolean; conversionProgress: ConversionProgress | null; hasChanges: boolean; + forceSingleTextElement: boolean; onLoadJson: (file: File | null) => Promise | void; onSelectPage: (pageIndex: number) => void; onGroupEdit: (pageIndex: number, groupId: string, value: string) => void; @@ -209,4 +210,5 @@ export interface PdfJsonEditorViewData { onReset: () => void; onDownloadJson: () => void; onGeneratePdf: () => void; + onForceSingleTextElementChange: (value: boolean) => void; } diff --git a/frontend/src/proprietary/tools/pdfJsonEditor/pdfJsonEditorUtils.ts b/frontend/src/proprietary/tools/pdfJsonEditor/pdfJsonEditorUtils.ts index 685929e30..cb6f16338 100644 --- a/frontend/src/proprietary/tools/pdfJsonEditor/pdfJsonEditorUtils.ts +++ b/frontend/src/proprietary/tools/pdfJsonEditor/pdfJsonEditorUtils.ts @@ -689,6 +689,7 @@ export const restoreGlyphElements = ( groupsByPage: TextGroup[][], imagesByPage: PdfJsonImageElement[][], originalImagesByPage: PdfJsonImageElement[][], + forceMergedGroups: boolean = false, ): PdfJsonDocument => { const updated = deepCloneDocument(source); const pages = updated.pages ?? []; @@ -709,6 +710,10 @@ export const restoreGlyphElements = ( groups.forEach((group) => { if (group.text !== group.originalText) { + if (forceMergedGroups) { + rebuiltElements.push(createMergedElement(group)); + return; + } const originalGlyphCount = group.originalElements.reduce( (sum, element) => sum + countGraphemes(element.text ?? ''), 0,