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 5e7123c93..d86046dd2 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 @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -2002,17 +2003,27 @@ public class PdfJsonConversionService { if (stream == null) { return null; } - return serializeStream(stream.getCOSObject()); + return serializeStream( + stream.getCOSObject(), Collections.newSetFromMap(new IdentityHashMap<>())); } private PdfJsonStream serializeStream(COSStream cosStream) throws IOException { if (cosStream == null) { return null; } + return serializeStream( + cosStream, Collections.newSetFromMap(new IdentityHashMap<>())); + } + + private PdfJsonStream serializeStream(COSStream cosStream, Set visited) + throws IOException { + if (cosStream == null) { + return null; + } Map dictionary = new LinkedHashMap<>(); for (COSName key : cosStream.keySet()) { COSBase value = cosStream.getDictionaryObject(key); - PdfJsonCosValue serialized = serializeCosValue(value); + PdfJsonCosValue serialized = serializeCosValue(value, visited); if (serialized != null) { dictionary.put(key.getName(), serialized); } @@ -2032,6 +2043,11 @@ public class PdfJsonConversionService { } private PdfJsonCosValue serializeCosValue(COSBase base) throws IOException { + return serializeCosValue( + base, Collections.newSetFromMap(new IdentityHashMap<>())); + } + + private PdfJsonCosValue serializeCosValue(COSBase base, Set visited) throws IOException { if (base == null) { return null; } @@ -2041,55 +2057,76 @@ public class PdfJsonConversionService { return null; } } - PdfJsonCosValue.PdfJsonCosValueBuilder builder = PdfJsonCosValue.builder(); - if (base instanceof COSNull) { - builder.type(PdfJsonCosValue.Type.NULL); - return builder.build(); - } - if (base instanceof COSBoolean booleanValue) { - builder.type(PdfJsonCosValue.Type.BOOLEAN).value(booleanValue.getValue()); - return builder.build(); - } - if (base instanceof COSInteger integer) { - builder.type(PdfJsonCosValue.Type.INTEGER).value(integer.longValue()); - return builder.build(); - } - if (base instanceof COSFloat floatValue) { - builder.type(PdfJsonCosValue.Type.FLOAT).value(floatValue.floatValue()); - return builder.build(); - } - if (base instanceof COSName name) { - builder.type(PdfJsonCosValue.Type.NAME).value(name.getName()); - return builder.build(); - } - if (base instanceof COSString cosString) { - builder.type(PdfJsonCosValue.Type.STRING) - .value(Base64.getEncoder().encodeToString(cosString.getBytes())); - return builder.build(); - } - if (base instanceof COSArray array) { - List items = new ArrayList<>(array.size()); - for (COSBase item : array) { - PdfJsonCosValue serialized = serializeCosValue(item); - items.add(serialized); + + boolean complex = + base instanceof COSDictionary + || base instanceof COSArray + || base instanceof COSStream; + if (complex) { + if (!visited.add(base)) { + return PdfJsonCosValue.builder() + .type(PdfJsonCosValue.Type.NAME) + .value("__circular__") + .build(); } - builder.type(PdfJsonCosValue.Type.ARRAY).items(items); - return builder.build(); } - if (base instanceof COSStream stream) { - builder.type(PdfJsonCosValue.Type.STREAM).stream(serializeStream(stream)); - return builder.build(); - } - if (base instanceof COSDictionary dictionary) { - Map entries = new LinkedHashMap<>(); - for (COSName key : dictionary.keySet()) { - PdfJsonCosValue serialized = serializeCosValue(dictionary.getDictionaryObject(key)); - entries.put(key.getName(), serialized); + + try { + PdfJsonCosValue.PdfJsonCosValueBuilder builder = PdfJsonCosValue.builder(); + if (base instanceof COSNull) { + builder.type(PdfJsonCosValue.Type.NULL); + return builder.build(); + } + if (base instanceof COSBoolean booleanValue) { + builder.type(PdfJsonCosValue.Type.BOOLEAN).value(booleanValue.getValue()); + return builder.build(); + } + if (base instanceof COSInteger integer) { + builder.type(PdfJsonCosValue.Type.INTEGER).value(integer.longValue()); + return builder.build(); + } + if (base instanceof COSFloat floatValue) { + builder.type(PdfJsonCosValue.Type.FLOAT).value(floatValue.floatValue()); + return builder.build(); + } + if (base instanceof COSName name) { + builder.type(PdfJsonCosValue.Type.NAME).value(name.getName()); + return builder.build(); + } + if (base instanceof COSString cosString) { + builder.type(PdfJsonCosValue.Type.STRING) + .value(Base64.getEncoder().encodeToString(cosString.getBytes())); + return builder.build(); + } + if (base instanceof COSArray array) { + List items = new ArrayList<>(array.size()); + for (COSBase item : array) { + PdfJsonCosValue serialized = serializeCosValue(item, visited); + items.add(serialized); + } + builder.type(PdfJsonCosValue.Type.ARRAY).items(items); + return builder.build(); + } + if (base instanceof COSStream stream) { + builder.type(PdfJsonCosValue.Type.STREAM).stream(serializeStream(stream, visited)); + return builder.build(); + } + if (base instanceof COSDictionary dictionary) { + Map entries = new LinkedHashMap<>(); + for (COSName key : dictionary.keySet()) { + PdfJsonCosValue serialized = + serializeCosValue(dictionary.getDictionaryObject(key), visited); + entries.put(key.getName(), serialized); + } + builder.type(PdfJsonCosValue.Type.DICTIONARY).entries(entries); + return builder.build(); + } + return null; + } finally { + if (complex) { + visited.remove(base); } - builder.type(PdfJsonCosValue.Type.DICTIONARY).entries(entries); - return builder.build(); } - return null; } private COSBase deserializeCosValue(PdfJsonCosValue value, PDDocument document) diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 37c130605..43e4c542a 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -4628,6 +4628,5 @@ "passwordMustBeDifferent": "New password must be different from current password", "passwordChangedSuccess": "Password changed successfully! Please log in again.", "passwordChangeFailed": "Failed to change password. Please check your current password." ->>>>>>> refs/remotes/origin/V2 } } diff --git a/frontend/src/proprietary/tools/pdfJsonEditor/PdfJsonEditor.tsx b/frontend/src/proprietary/tools/pdfJsonEditor/PdfJsonEditor.tsx index 61de7adc6..419cab1c9 100644 --- a/frontend/src/proprietary/tools/pdfJsonEditor/PdfJsonEditor.tsx +++ b/frontend/src/proprietary/tools/pdfJsonEditor/PdfJsonEditor.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import DescriptionIcon from '@mui/icons-material/DescriptionOutlined'; import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; +import { useFileSelection } from '@app/contexts/FileContext'; import { useNavigationActions, useNavigationState } from '@app/contexts/NavigationContext'; import { BaseToolProps, ToolComponent } from '@app/types/tool'; import { CONVERSION_ENDPOINTS } from '@app/constants/convertConstants'; @@ -36,6 +37,17 @@ const sanitizeBaseName = (name?: string | null): string => { return name.replace(/\.[^.]+$/u, ''); }; +const getAutoLoadKey = (file: File): string => { + const withId = file as File & { fileId?: string; quickKey?: string }; + if (withId.fileId && typeof withId.fileId === 'string') { + return withId.fileId; + } + if (withId.quickKey && typeof withId.quickKey === 'string') { + return withId.quickKey; + } + return `${file.name}|${file.size}|${file.lastModified}`; +}; + const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); const { @@ -58,6 +70,9 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => { const [isConverting, setIsConverting] = useState(false); const originalImagesRef = useRef([]); + const autoLoadKeyRef = useRef(null); + const loadRequestIdRef = useRef(0); + const latestPdfRequestIdRef = useRef(null); const dirtyPages = useMemo( () => getDirtyPages(groupsByPage, imagesByPage, originalImagesRef.current), @@ -66,6 +81,7 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => { const hasChanges = useMemo(() => dirtyPages.some(Boolean), [dirtyPages]); const hasDocument = loadedDocument !== null; const viewLabel = useMemo(() => t('pdfJsonEditor.viewLabel', 'PDF Editor'), [t]); + const { selectedFiles } = useFileSelection(); const resetToDocument = useCallback((document: PdfJsonDocument | null) => { if (!document) { @@ -90,15 +106,20 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => { return; } + const requestId = loadRequestIdRef.current + 1; + loadRequestIdRef.current = requestId; + + const fileKey = getAutoLoadKey(file); const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf'); try { let parsed: PdfJsonDocument; + setErrorMessage(null); + if (isPdf) { - // Convert PDF to JSON first + latestPdfRequestIdRef.current = requestId; setIsConverting(true); - setErrorMessage(null); const formData = new FormData(); formData.append('fileInput', file); @@ -110,21 +131,28 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => { const jsonText = await response.data.text(); parsed = JSON.parse(jsonText) as PdfJsonDocument; } else { - // Load JSON directly const content = await file.text(); parsed = JSON.parse(content) as PdfJsonDocument; } + if (loadRequestIdRef.current !== requestId) { + return; + } + setLoadedDocument(parsed); resetToDocument(parsed); setFileName(file.name); setErrorMessage(null); + autoLoadKeyRef.current = fileKey; } catch (error) { console.error('Failed to load file', error); + + if (loadRequestIdRef.current !== requestId) { + return; + } + setLoadedDocument(null); - setGroupsByPage([]); - setImagesByPage([]); - originalImagesRef.current = []; + resetToDocument(null); if (isPdf) { setErrorMessage( @@ -139,7 +167,9 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => { ); } } finally { - setIsConverting(false); + if (isPdf && latestPdfRequestIdRef.current === requestId) { + setIsConverting(false); + } } }, [resetToDocument, t] @@ -367,6 +397,30 @@ const PdfJsonEditor = ({ onComplete, onError }: BaseToolProps) => { const latestViewDataRef = useRef(viewData); latestViewDataRef.current = viewData; + useEffect(() => { + if (selectedFiles.length === 0) { + autoLoadKeyRef.current = null; + return; + } + + if (navigationState.selectedTool !== 'pdfJsonEditor') { + return; + } + + const file = selectedFiles[0]; + if (!file) { + return; + } + + const fileKey = getAutoLoadKey(file); + if (autoLoadKeyRef.current === fileKey) { + return; + } + + autoLoadKeyRef.current = fileKey; + void handleLoadFile(file); + }, [selectedFiles, navigationState.selectedTool, handleLoadFile]); + useEffect(() => { registerCustomWorkbenchView({ id: VIEW_ID,