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 ConvertFromImageSettings from "./ConvertFromImageSettings";
|
||||||
import ConvertFromWebSettings from "./ConvertFromWebSettings";
|
import ConvertFromWebSettings from "./ConvertFromWebSettings";
|
||||||
import ConvertFromEmailSettings from "./ConvertFromEmailSettings";
|
import ConvertFromEmailSettings from "./ConvertFromEmailSettings";
|
||||||
|
import ConvertToPdfaSettings from "./ConvertToPdfaSettings";
|
||||||
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParameters";
|
||||||
import {
|
import {
|
||||||
FROM_FORMAT_OPTIONS,
|
FROM_FORMAT_OPTIONS,
|
||||||
@ -114,6 +115,9 @@ const ConvertSettings = ({
|
|||||||
downloadHtml: false,
|
downloadHtml: false,
|
||||||
includeAllRecipients: false,
|
includeAllRecipients: false,
|
||||||
});
|
});
|
||||||
|
onParameterChange('pdfaOptions', {
|
||||||
|
outputFormat: 'pdfa-1',
|
||||||
|
});
|
||||||
// Disable smart detection when manually changing source format
|
// Disable smart detection when manually changing source format
|
||||||
onParameterChange('isSmartDetection', false);
|
onParameterChange('isSmartDetection', false);
|
||||||
onParameterChange('smartDetectionType', 'none');
|
onParameterChange('smartDetectionType', 'none');
|
||||||
@ -161,6 +165,9 @@ const ConvertSettings = ({
|
|||||||
downloadHtml: false,
|
downloadHtml: false,
|
||||||
includeAllRecipients: 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>
|
</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) ||
|
parameters.toExtension === 'pdf' && !parameters.imageOptions.combineImages) ||
|
||||||
// PDF to image conversions (each PDF should generate its own image file)
|
// PDF to image conversions (each PDF should generate its own image file)
|
||||||
(parameters.fromExtension === 'pdf' && isImageFormat(parameters.toExtension)) ||
|
(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)
|
// Web files to PDF conversions (each web file should generate its own PDF)
|
||||||
((isWebFormat(parameters.fromExtension) || parameters.fromExtension === 'web') &&
|
((isWebFormat(parameters.fromExtension) || parameters.fromExtension === 'web') &&
|
||||||
parameters.toExtension === 'pdf') ||
|
parameters.toExtension === 'pdf') ||
|
||||||
@ -143,7 +145,7 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
formData.append("fileInput", file);
|
formData.append("fileInput", file);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions } = parameters;
|
const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions, pdfaOptions } = parameters;
|
||||||
|
|
||||||
// Add conversion-specific parameters
|
// Add conversion-specific parameters
|
||||||
if (isImageFormat(toExtension)) {
|
if (isImageFormat(toExtension)) {
|
||||||
@ -170,6 +172,9 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
formData.append("maxAttachmentSizeMB", emailOptions.maxAttachmentSizeMB.toString());
|
formData.append("maxAttachmentSizeMB", emailOptions.maxAttachmentSizeMB.toString());
|
||||||
formData.append("downloadHtml", emailOptions.downloadHtml.toString());
|
formData.append("downloadHtml", emailOptions.downloadHtml.toString());
|
||||||
formData.append("includeAllRecipients", emailOptions.includeAllRecipients.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') {
|
} else if (fromExtension === 'pdf' && toExtension === 'csv') {
|
||||||
// CSV extraction - always process all pages for simplified workflow
|
// CSV extraction - always process all pages for simplified workflow
|
||||||
formData.append("pageNumbers", "all");
|
formData.append("pageNumbers", "all");
|
||||||
@ -199,6 +204,7 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
imageOptions: parameters.imageOptions,
|
imageOptions: parameters.imageOptions,
|
||||||
htmlOptions: parameters.htmlOptions,
|
htmlOptions: parameters.htmlOptions,
|
||||||
emailOptions: parameters.emailOptions,
|
emailOptions: parameters.emailOptions,
|
||||||
|
pdfaOptions: parameters.pdfaOptions,
|
||||||
},
|
},
|
||||||
fileSize: selectedFiles[0].size
|
fileSize: selectedFiles[0].size
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ describe('useConvertParameters', () => {
|
|||||||
expect(result.current.parameters.emailOptions.maxAttachmentSizeMB).toBe(10);
|
expect(result.current.parameters.emailOptions.maxAttachmentSizeMB).toBe(10);
|
||||||
expect(result.current.parameters.emailOptions.downloadHtml).toBe(false);
|
expect(result.current.parameters.emailOptions.downloadHtml).toBe(false);
|
||||||
expect(result.current.parameters.emailOptions.includeAllRecipients).toBe(false);
|
expect(result.current.parameters.emailOptions.includeAllRecipients).toBe(false);
|
||||||
|
expect(result.current.parameters.pdfaOptions.outputFormat).toBe('pdfa-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should update individual parameters', () => {
|
test('should update individual parameters', () => {
|
||||||
@ -82,6 +83,18 @@ describe('useConvertParameters', () => {
|
|||||||
expect(result.current.parameters.emailOptions.includeAllRecipients).toBe(true);
|
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', () => {
|
test('should reset parameters to defaults', () => {
|
||||||
const { result } = renderHook(() => useConvertParameters());
|
const { result } = renderHook(() => useConvertParameters());
|
||||||
|
|
||||||
|
@ -32,6 +32,9 @@ export interface ConvertParameters {
|
|||||||
downloadHtml: boolean;
|
downloadHtml: boolean;
|
||||||
includeAllRecipients: boolean;
|
includeAllRecipients: boolean;
|
||||||
};
|
};
|
||||||
|
pdfaOptions: {
|
||||||
|
outputFormat: string;
|
||||||
|
};
|
||||||
isSmartDetection: boolean;
|
isSmartDetection: boolean;
|
||||||
smartDetectionType: 'mixed' | 'images' | 'web' | 'none';
|
smartDetectionType: 'mixed' | 'images' | 'web' | 'none';
|
||||||
}
|
}
|
||||||
@ -67,6 +70,9 @@ const initialParameters: ConvertParameters = {
|
|||||||
downloadHtml: false,
|
downloadHtml: false,
|
||||||
includeAllRecipients: false,
|
includeAllRecipients: false,
|
||||||
},
|
},
|
||||||
|
pdfaOptions: {
|
||||||
|
outputFormat: 'pdfa-1',
|
||||||
|
},
|
||||||
isSmartDetection: false,
|
isSmartDetection: false,
|
||||||
smartDetectionType: 'none',
|
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('downloadHtml')).toBe('true');
|
||||||
expect(formData.get('includeAllRecipients')).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', () => {
|
describe('Image Conversion Options Integration', () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user