mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 18:45:21 +00:00
pdf-pdfa
This commit is contained in:
parent
5b8eea686e
commit
cd87eb18e8
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
@ -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
|
||||
}
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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',
|
||||
};
|
||||
|
66
frontend/src/hooks/usePdfSignatureDetection.ts
Normal file
66
frontend/src/hooks/usePdfSignatureDetection.ts
Normal 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
|
||||
};
|
||||
};
|
@ -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', () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user