This commit is contained in:
Connor Yoh 2025-07-31 15:23:38 +01:00
parent 5b8eea686e
commit cd87eb18e8
7 changed files with 206 additions and 1 deletions

View File

@ -12,6 +12,7 @@ import ConvertToImageSettings from "./ConvertToImageSettings";
import ConvertFromImageSettings from "./ConvertFromImageSettings";
import ConvertFromWebSettings from "./ConvertFromWebSettings";
import ConvertFromEmailSettings from "./ConvertFromEmailSettings";
import ConvertToPdfaSettings from "./ConvertToPdfaSettings";
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
import {
FROM_FORMAT_OPTIONS,
@ -114,6 +115,9 @@ const ConvertSettings = ({
downloadHtml: false,
includeAllRecipients: false,
});
onParameterChange('pdfaOptions', {
outputFormat: 'pdfa-1',
});
// Disable smart detection when manually changing source format
onParameterChange('isSmartDetection', false);
onParameterChange('smartDetectionType', 'none');
@ -161,6 +165,9 @@ const ConvertSettings = ({
downloadHtml: false,
includeAllRecipients: false,
});
onParameterChange('pdfaOptions', {
outputFormat: 'pdfa-1',
});
};
@ -274,6 +281,19 @@ const ConvertSettings = ({
</>
)}
{/* PDF to PDF/A options */}
{parameters.fromExtension === 'pdf' && parameters.toExtension === 'pdfa' && (
<>
<Divider />
<ConvertToPdfaSettings
parameters={parameters}
onParameterChange={onParameterChange}
selectedFiles={selectedFiles}
disabled={disabled}
/>
</>
)}
</Stack>
);
};

View File

@ -0,0 +1,60 @@
import React from 'react';
import { Stack, Text, Select, Alert } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
import { usePdfSignatureDetection } from '../../../hooks/usePdfSignatureDetection';
interface ConvertToPdfaSettingsProps {
parameters: ConvertParameters;
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
selectedFiles: File[];
disabled?: boolean;
}
const ConvertToPdfaSettings = ({
parameters,
onParameterChange,
selectedFiles,
disabled = false
}: ConvertToPdfaSettingsProps) => {
const { t } = useTranslation();
const { hasDigitalSignatures, isChecking } = usePdfSignatureDetection(selectedFiles);
const pdfaFormatOptions = [
{ value: 'pdfa-1', label: 'PDF/A-1b' },
{ value: 'pdfa', label: 'PDF/A-2b' }
];
return (
<Stack gap="sm" data-testid="pdfa-settings">
<Text size="sm" fw={500}>{t("convert.pdfaOptions", "PDF/A Options")}:</Text>
{hasDigitalSignatures && (
<Alert color="yellow" size="sm">
<Text size="sm">
{t("convert.pdfaDigitalSignatureWarning", "The PDF contains a digital signature. This will be removed in the next step.")}
</Text>
</Alert>
)}
<Stack gap="xs">
<Text size="xs" fw={500}>{t("convert.outputFormat", "Output Format")}:</Text>
<Select
value={parameters.pdfaOptions.outputFormat}
onChange={(value) => onParameterChange('pdfaOptions', {
...parameters.pdfaOptions,
outputFormat: value || 'pdfa-1'
})}
data={pdfaFormatOptions}
disabled={disabled || isChecking}
data-testid="pdfa-output-format-select"
/>
<Text size="xs" c="dimmed">
{t("convert.pdfaNote", "PDF/A-1b is more compatible, PDF/A-2b supports more features.")}
</Text>
</Stack>
</Stack>
);
};
export default ConvertToPdfaSettings;

View File

@ -46,6 +46,8 @@ const shouldProcessFilesSeparately = (
parameters.toExtension === 'pdf' && !parameters.imageOptions.combineImages) ||
// PDF to image conversions (each PDF should generate its own image file)
(parameters.fromExtension === 'pdf' && isImageFormat(parameters.toExtension)) ||
// PDF to PDF/A conversions (each PDF should be processed separately)
(parameters.fromExtension === 'pdf' && parameters.toExtension === 'pdfa') ||
// Web files to PDF conversions (each web file should generate its own PDF)
((isWebFormat(parameters.fromExtension) || parameters.fromExtension === 'web') &&
parameters.toExtension === 'pdf') ||
@ -143,7 +145,7 @@ export const useConvertOperation = (): ConvertOperationHook => {
formData.append("fileInput", file);
});
const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions } = parameters;
const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions, pdfaOptions } = parameters;
// Add conversion-specific parameters
if (isImageFormat(toExtension)) {
@ -170,6 +172,9 @@ export const useConvertOperation = (): ConvertOperationHook => {
formData.append("maxAttachmentSizeMB", emailOptions.maxAttachmentSizeMB.toString());
formData.append("downloadHtml", emailOptions.downloadHtml.toString());
formData.append("includeAllRecipients", emailOptions.includeAllRecipients.toString());
} else if (fromExtension === 'pdf' && toExtension === 'pdfa') {
// PDF to PDF/A conversion with output format
formData.append("outputFormat", pdfaOptions.outputFormat);
} else if (fromExtension === 'pdf' && toExtension === 'csv') {
// CSV extraction - always process all pages for simplified workflow
formData.append("pageNumbers", "all");
@ -199,6 +204,7 @@ export const useConvertOperation = (): ConvertOperationHook => {
imageOptions: parameters.imageOptions,
htmlOptions: parameters.htmlOptions,
emailOptions: parameters.emailOptions,
pdfaOptions: parameters.pdfaOptions,
},
fileSize: selectedFiles[0].size
}

View File

@ -23,6 +23,7 @@ describe('useConvertParameters', () => {
expect(result.current.parameters.emailOptions.maxAttachmentSizeMB).toBe(10);
expect(result.current.parameters.emailOptions.downloadHtml).toBe(false);
expect(result.current.parameters.emailOptions.includeAllRecipients).toBe(false);
expect(result.current.parameters.pdfaOptions.outputFormat).toBe('pdfa-1');
});
test('should update individual parameters', () => {
@ -82,6 +83,18 @@ describe('useConvertParameters', () => {
expect(result.current.parameters.emailOptions.includeAllRecipients).toBe(true);
});
test('should update nested PDF/A options', () => {
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('pdfaOptions', {
outputFormat: 'pdfa'
});
});
expect(result.current.parameters.pdfaOptions.outputFormat).toBe('pdfa');
});
test('should reset parameters to defaults', () => {
const { result } = renderHook(() => useConvertParameters());

View File

@ -32,6 +32,9 @@ export interface ConvertParameters {
downloadHtml: boolean;
includeAllRecipients: boolean;
};
pdfaOptions: {
outputFormat: string;
};
isSmartDetection: boolean;
smartDetectionType: 'mixed' | 'images' | 'web' | 'none';
}
@ -67,6 +70,9 @@ const initialParameters: ConvertParameters = {
downloadHtml: false,
includeAllRecipients: false,
},
pdfaOptions: {
outputFormat: 'pdfa-1',
},
isSmartDetection: false,
smartDetectionType: 'none',
};

View File

@ -0,0 +1,66 @@
import { useState, useEffect } from 'react';
import * as pdfjsLib from 'pdfjs-dist';
export interface PdfSignatureDetectionResult {
hasDigitalSignatures: boolean;
isChecking: boolean;
}
export const usePdfSignatureDetection = (files: File[]): PdfSignatureDetectionResult => {
const [hasDigitalSignatures, setHasDigitalSignatures] = useState(false);
const [isChecking, setIsChecking] = useState(false);
useEffect(() => {
const checkForDigitalSignatures = async () => {
if (files.length === 0) {
setHasDigitalSignatures(false);
return;
}
setIsChecking(true);
let foundSignature = false;
try {
// Set up PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdfjs-legacy/pdf.worker.mjs';
for (const file of files) {
const arrayBuffer = await file.arrayBuffer();
try {
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const annotations = await page.getAnnotations({ intent: 'display' });
annotations.forEach(annotation => {
if (annotation.subtype === 'Widget' && annotation.fieldType === 'Sig') {
foundSignature = true;
}
});
if (foundSignature) break;
}
} catch (error) {
console.warn('Error analyzing PDF for signatures:', error);
}
if (foundSignature) break;
}
} catch (error) {
console.warn('Error checking for digital signatures:', error);
}
setHasDigitalSignatures(foundSignature);
setIsChecking(false);
};
checkForDigitalSignatures();
}, [files]);
return {
hasDigitalSignatures,
isChecking
};
};

View File

@ -322,6 +322,40 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
expect(formData.get('downloadHtml')).toBe('true');
expect(formData.get('includeAllRecipients')).toBe('true');
});
test('should send correct PDF/A parameters for pdf-to-pdfa conversion', async () => {
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
});
const { result: operationResult } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
});
const pdfFile = new File(['pdf content'], 'document.pdf', { type: 'application/pdf' });
// Set up PDF/A conversion parameters
act(() => {
paramsResult.current.updateParameter('fromExtension', 'pdf');
paramsResult.current.updateParameter('toExtension', 'pdfa');
paramsResult.current.updateParameter('pdfaOptions', {
outputFormat: 'pdfa'
});
});
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
[pdfFile]
);
});
const formData = mockedAxios.post.mock.calls[0][1] as FormData;
expect(formData.get('outputFormat')).toBe('pdfa');
expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/pdf/pdfa', expect.any(FormData), {
responseType: 'blob'
});
});
});
describe('Image Conversion Options Integration', () => {