diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java index f7e5d6efd..830efd511 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java @@ -479,15 +479,22 @@ public class ConvertPDFToPDFA { command.add("-dCompatibilityLevel=" + profile.getCompatibilityLevel()); command.add("-sDEVICE=pdfwrite"); command.add("-sColorConversionStrategy=RGB"); - command.add("-dProcessColorModel=/DeviceRGB"); command.add("-sOutputICCProfile=" + colorProfiles.rgb().toAbsolutePath()); command.add("-sDefaultRGBProfile=" + colorProfiles.rgb().toAbsolutePath()); command.add("-sDefaultGrayProfile=" + colorProfiles.gray().toAbsolutePath()); command.add("-dEmbedAllFonts=true"); - command.add("-dSubsetFonts=false"); // Embed complete fonts to avoid incomplete glyphs + command.add("-dSubsetFonts=true"); command.add("-dCompressFonts=true"); command.add("-dNOSUBSTFONTS=false"); // Allow font substitution for problematic fonts - command.add("-dPDFSETTINGS=/prepress"); + + // Explicitly tune downsampling/compression for high-quality print + command.add("-dColorImageDownsampleType=/Bicubic"); + command.add("-dColorImageResolution=300"); + command.add("-dGrayImageDownsampleType=/Bicubic"); + command.add("-dGrayImageResolution=300"); + command.add("-dMonoImageDownsampleType=/Bicubic"); + command.add("-dMonoImageResolution=1200"); + command.add("-dNOPAUSE"); command.add("-dBATCH"); command.add("-dNOOUTERSAVE"); @@ -2445,9 +2452,7 @@ public class ConvertPDFToPDFA { @Getter private enum PdfXProfile { - PDF_X_1("PDF/X-1", "_PDFX-1.pdf", "1.3", "2001", "pdfx-1", "pdfx"), - PDF_X_3("PDF/X-3", "_PDFX-3.pdf", "1.3", "2003", "pdfx-3"), - PDF_X_4("PDF/X-4", "_PDFX-4.pdf", "1.4", "2008", "pdfx-4"); + PDF_X("PDF/X", "_PDFX.pdf", "1.6", "2008", "pdfx"); private final String displayName; private final String suffix; @@ -2473,7 +2478,7 @@ public class ConvertPDFToPDFA { static PdfXProfile fromRequest(String requestToken) { if (requestToken == null) { - return PDF_X_4; + return PDF_X; } String normalized = requestToken.trim().toLowerCase(Locale.ROOT); Optional match = @@ -2481,7 +2486,7 @@ public class ConvertPDFToPDFA { .filter(profile -> profile.requestTokens.contains(normalized)) .findFirst(); - return match.orElse(PDF_X_4); + return match.orElse(PDF_X); } String outputSuffix() { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java index 3df1eff9e..aa453bc74 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java @@ -273,7 +273,9 @@ public class StampController { // Split the stampText into multiple lines String[] lines = - RegexPatternUtils.getInstance().getEscapedNewlinePattern().split(processedStampText); + RegexPatternUtils.getInstance() + .getEscapedNewlinePattern() + .split(processedStampText); // Calculate dynamic line height based on font ascent and descent float ascent = font.getFontDescriptor().getAscent(); diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPdfARequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPdfARequest.java index 4bda52cc7..7cbd3d19b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPdfARequest.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPdfARequest.java @@ -14,9 +14,6 @@ public class PdfToPdfARequest extends PDFFile { @Schema( description = "The output format type (PDF/A or PDF/X)", requiredMode = Schema.RequiredMode.REQUIRED, - allowableValues = { - "pdfa", "pdfa-1", "pdfa-2", "pdfa-2b", "pdfa-3", "pdfa-3b", "pdfx", "pdfx-1", - "pdfx-3", "pdfx-4" - }) + allowableValues = {"pdfa", "pdfa-1", "pdfa-2", "pdfa-2b", "pdfa-3", "pdfa-3b", "pdfx"}) private String outputFormat; } diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index a42565ae9..95e59c062 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -1264,7 +1264,10 @@ downloadHtml = "Download HTML intermediate file instead of PDF" pdfaOptions = "PDF/A Options" outputFormat = "Output Format" pdfaNote = "PDF/A-1b is more compatible, PDF/A-2b supports more features, PDF/A-3b supports embedded files." +pdfaFormat = "PDF/A Format" pdfaDigitalSignatureWarning = "The PDF contains a digital signature. This will be removed in the next step." +pdfxDigitalSignatureWarning = "The PDF contains a digital signature. This will be removed in the next step." +pdfxDescription = "PDF/X is an ISO standard PDF subset for reliable printing and graphics exchange." fileFormat = "File Format" wordDoc = "Word Document" wordDocExt = "Word Document (.docx)" @@ -2197,6 +2200,16 @@ tip = "Currently does not work for multiple inputs at once" outputFormat = "Output format" pdfWithDigitalSignature = "The PDF contains a digital signature. This will be removed in the next step." +[pdfToPDFX] +tags = "print,standard,conversion,production,prepress,archive" +title = "PDF To PDF/X" +header = "PDF To PDF/X" +credit = "This service uses Ghostscript for PDF/X conversion" +submit = "Convert" +tip = "Currently does not work for multiple inputs at once" +outputFormat = "Output format" +pdfWithDigitalSignature = "The PDF contains a digital signature. This will be removed in the next step." + [PDFToWord] tags = "doc,docx,odt,word,transformation,format,conversion,office,microsoft,docfile" title = "PDF to Word" diff --git a/frontend/src/core/components/tools/convert/ConvertSettings.tsx b/frontend/src/core/components/tools/convert/ConvertSettings.tsx index 7ce07c8f6..5905ae4b9 100644 --- a/frontend/src/core/components/tools/convert/ConvertSettings.tsx +++ b/frontend/src/core/components/tools/convert/ConvertSettings.tsx @@ -17,6 +17,7 @@ import ConvertFromEmailSettings from "@app/components/tools/convert/ConvertFromE import ConvertFromCbzSettings from "@app/components/tools/convert/ConvertFromCbzSettings"; import ConvertToCbzSettings from "@app/components/tools/convert/ConvertToCbzSettings"; import ConvertToPdfaSettings from "@app/components/tools/convert/ConvertToPdfaSettings"; +import ConvertToPdfxSettings from "@app/components/tools/convert/ConvertToPdfxSettings"; import ConvertFromCbrSettings from "@app/components/tools/convert/ConvertFromCbrSettings"; import ConvertToCbrSettings from "@app/components/tools/convert/ConvertToCbrSettings"; import ConvertFromEbookSettings from "@app/components/tools/convert/ConvertFromEbookSettings"; @@ -147,6 +148,9 @@ const ConvertSettings = ({ onParameterChange('pdfaOptions', { outputFormat: 'pdfa-1', }); + onParameterChange('pdfxOptions', { + outputFormat: 'pdfx', + }); onParameterChange('cbrOptions', { optimizeForEbook: false, }); @@ -232,6 +236,9 @@ const ConvertSettings = ({ onParameterChange('pdfaOptions', { outputFormat: 'pdfa-1', }); + onParameterChange('pdfxOptions', { + outputFormat: 'pdfx', + }); onParameterChange('cbrOptions', { optimizeForEbook: false, }); @@ -406,6 +413,19 @@ const ConvertSettings = ({ )} + {/* PDF to PDF/X options */} + {parameters.fromExtension === 'pdf' && parameters.toExtension === 'pdfx' && ( + <> + + + + )} + {/* eBook to PDF options */} {['epub', 'mobi', 'azw3', 'fb2'].includes(parameters.fromExtension) && parameters.toExtension === 'pdf' && ( <> diff --git a/frontend/src/core/components/tools/convert/ConvertToPdfxSettings.tsx b/frontend/src/core/components/tools/convert/ConvertToPdfxSettings.tsx new file mode 100644 index 000000000..6d2d3ce63 --- /dev/null +++ b/frontend/src/core/components/tools/convert/ConvertToPdfxSettings.tsx @@ -0,0 +1,31 @@ +import { useEffect } from 'react'; +import { ConvertParameters } from '@app/hooks/tools/convert/useConvertParameters'; +import { StirlingFile } from '@app/types/fileContext'; + +interface ConvertToPdfxSettingsProps { + parameters: ConvertParameters; + onParameterChange: (key: K, value: ConvertParameters[K]) => void; + selectedFiles: StirlingFile[]; + disabled?: boolean; +} + +const ConvertToPdfxSettings = ({ + parameters, + onParameterChange, + selectedFiles: _selectedFiles, + disabled: _disabled = false +}: ConvertToPdfxSettingsProps) => { + // Automatically set PDF/X-3 format when this component is rendered + useEffect(() => { + if (parameters.pdfxOptions.outputFormat !== 'pdfx-3') { + onParameterChange('pdfxOptions', { + ...parameters.pdfxOptions, + outputFormat: 'pdfx-3' + }); + } + }, [parameters.pdfxOptions.outputFormat, onParameterChange]); + + return null; +}; + +export default ConvertToPdfxSettings; diff --git a/frontend/src/core/constants/convertConstants.ts b/frontend/src/core/constants/convertConstants.ts index 6e10fb557..bdb9caa6b 100644 --- a/frontend/src/core/constants/convertConstants.ts +++ b/frontend/src/core/constants/convertConstants.ts @@ -110,6 +110,7 @@ export const FROM_FORMAT_OPTIONS = [ export const TO_FORMAT_OPTIONS = [ { value: 'pdf', label: 'PDF', group: 'Document' }, { value: 'pdfa', label: 'PDF/A', group: 'Document' }, + { value: 'pdfx', label: 'PDF/X', group: 'Document' }, { value: 'docx', label: 'DOCX', group: 'Document' }, { value: 'odt', label: 'ODT', group: 'Document' }, { value: 'cbz', label: 'CBZ', group: 'Archive' }, @@ -136,7 +137,7 @@ export const TO_FORMAT_OPTIONS = [ export const CONVERSION_MATRIX: Record = { 'any': ['pdf'], // Mixed files always convert to PDF 'image': ['pdf'], // Multiple images always convert to PDF - 'pdf': ['png', 'jpg', 'gif', 'tiff', 'bmp', 'webp', 'docx', 'odt', 'pptx', 'odp', 'csv', 'txt', 'rtf', 'md', 'html', 'xml', 'pdfa', 'cbz', 'cbr', 'epub', 'azw3'], + 'pdf': ['png', 'jpg', 'gif', 'tiff', 'bmp', 'webp', 'docx', 'odt', 'pptx', 'odp', 'csv', 'txt', 'rtf', 'md', 'html', 'xml', 'pdfa', 'pdfx', 'cbz', 'cbr', 'epub', 'azw3'], 'cbz': ['pdf'], 'docx': ['pdf'], 'doc': ['pdf'], 'odt': ['pdf'], 'xlsx': ['pdf'], 'xls': ['pdf'], 'ods': ['pdf'], @@ -164,6 +165,7 @@ export const EXTENSION_TO_ENDPOINT: Record> = { 'txt': 'pdf-to-text', 'rtf': 'pdf-to-text', 'md': 'pdf-to-markdown', 'html': 'pdf-to-html', 'xml': 'pdf-to-xml', 'pdfa': 'pdf-to-pdfa', + 'pdfx': 'pdf-to-pdfa', // PDF/X uses the same endpoint as PDF/A 'cbr': 'pdf-to-cbr', 'cbz': 'pdf-to-cbz', 'epub': 'pdf-to-epub', 'azw3': 'pdf-to-epub' diff --git a/frontend/src/core/hooks/tools/convert/useConvertOperation.ts b/frontend/src/core/hooks/tools/convert/useConvertOperation.ts index 0f976ce38..0f78cdbb0 100644 --- a/frontend/src/core/hooks/tools/convert/useConvertOperation.ts +++ b/frontend/src/core/hooks/tools/convert/useConvertOperation.ts @@ -19,8 +19,8 @@ export const shouldProcessFilesSeparately = ( (parameters.fromExtension === 'svg' && 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') || + // PDF to PDF/A and PDF/X conversions (each PDF should be processed separately) + (parameters.fromExtension === 'pdf' && (parameters.toExtension === 'pdfa' || parameters.toExtension === 'pdfx')) || // PDF to text-like formats should be one output per input (parameters.fromExtension === 'pdf' && ['txt', 'rtf', 'csv'].includes(parameters.toExtension)) || // PDF to CBR conversions (each PDF should generate its own archive) @@ -46,7 +46,7 @@ export const shouldProcessFilesSeparately = ( // Static function that can be used by both the hook and automation executor export const buildConvertFormData = (parameters: ConvertParameters, selectedFiles: File[]): FormData => { const formData = new FormData(); - const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions, pdfaOptions, cbrOptions, pdfToCbrOptions, cbzOptions, cbzOutputOptions, ebookOptions, epubOptions } = parameters; + const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions, pdfaOptions, pdfxOptions, cbrOptions, pdfToCbrOptions, cbzOptions, cbzOutputOptions, ebookOptions, epubOptions } = parameters; selectedFiles.forEach(file => { formData.append("fileInput", file); @@ -79,6 +79,9 @@ export const buildConvertFormData = (parameters: ConvertParameters, selectedFile formData.append("includeAllRecipients", emailOptions.includeAllRecipients.toString()); } else if (fromExtension === 'pdf' && toExtension === 'pdfa') { formData.append("outputFormat", pdfaOptions.outputFormat); + } else if (fromExtension === 'pdf' && toExtension === 'pdfx') { + // Use PDF/A endpoint with PDF/X format parameter + formData.append("outputFormat", pdfxOptions?.outputFormat || 'pdfx'); } else if (fromExtension === 'pdf' && toExtension === 'csv') { formData.append("pageNumbers", "all"); } else if (fromExtension === 'cbr' && toExtension === 'pdf') { @@ -112,7 +115,8 @@ export const createFileFromResponse = ( ): File => { const originalName = originalFileName.split('.')[0]; - if (targetExtension == 'pdfa') { + // Map both pdfa and pdfx to pdf since they both result in PDF files + if (targetExtension == 'pdfa' || targetExtension == 'pdfx') { targetExtension = 'pdf'; } @@ -127,14 +131,21 @@ export const convertProcessor = async ( selectedFiles: File[] ): Promise => { const processedFiles: File[] = []; - const endpoint = getEndpointUrl(parameters.fromExtension, parameters.toExtension); + + // Map PDF/X to use PDF/A endpoint + const actualToExtension = parameters.toExtension === 'pdfx' ? 'pdfa' : parameters.toExtension; + const endpoint = getEndpointUrl(parameters.fromExtension, actualToExtension); if (!endpoint) { throw new Error('Unsupported conversion format'); } // Convert-specific routing logic: decide batch vs individual processing - const isSeparateProcessing = shouldProcessFilesSeparately(selectedFiles, parameters); + // For PDF/X, we want to treat it similar to PDF/A (separate processing) + const isSeparateProcessing = shouldProcessFilesSeparately(selectedFiles, { + ...parameters, + toExtension: actualToExtension // Use the mapped extension for decision logic + }); if (isSeparateProcessing) { // Individual processing for complex cases (PDF→image, smart detection, etc.) @@ -143,7 +154,7 @@ export const convertProcessor = async ( const formData = buildConvertFormData(parameters, [file]); const response = await apiClient.post(endpoint, formData, { responseType: 'blob' }); - const convertedFile = createFileFromResponse(response.data, response.headers, file.name, parameters.toExtension); + const convertedFile = createFileFromResponse(response.data, response.headers, file.name, actualToExtension === 'pdfa' ? 'pdfx' : parameters.toExtension); processedFiles.push(convertedFile); } catch (error) { @@ -159,7 +170,7 @@ export const convertProcessor = async ( ? selectedFiles[0].name : 'converted_files'; - const convertedFile = createFileFromResponse(response.data, response.headers, baseFilename, parameters.toExtension); + const convertedFile = createFileFromResponse(response.data, response.headers, baseFilename, actualToExtension === 'pdfa' ? 'pdfx' : parameters.toExtension); processedFiles.push(convertedFile); } diff --git a/frontend/src/core/hooks/tools/convert/useConvertParameters.ts b/frontend/src/core/hooks/tools/convert/useConvertParameters.ts index 59557465f..8a065df20 100644 --- a/frontend/src/core/hooks/tools/convert/useConvertParameters.ts +++ b/frontend/src/core/hooks/tools/convert/useConvertParameters.ts @@ -36,6 +36,9 @@ export interface ConvertParameters extends BaseParameters { pdfaOptions: { outputFormat: string; }; + pdfxOptions: { + outputFormat: string; + }; cbrOptions: { optimizeForEbook: boolean; }; @@ -92,6 +95,9 @@ export const defaultParameters: ConvertParameters = { pdfaOptions: { outputFormat: 'pdfa-1', }, + pdfxOptions: { + outputFormat: 'pdfx', + }, cbrOptions: { optimizeForEbook: false, }, diff --git a/frontend/src/core/tests/convert/ConvertIntegration.test.tsx b/frontend/src/core/tests/convert/ConvertIntegration.test.tsx index 73e776c32..cacb1ab37 100644 --- a/frontend/src/core/tests/convert/ConvertIntegration.test.tsx +++ b/frontend/src/core/tests/convert/ConvertIntegration.test.tsx @@ -152,6 +152,9 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + pdfxOptions: { + outputFormat: 'pdfx' + }, cbrOptions: { optimizeForEbook: false }, @@ -231,6 +234,9 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + pdfxOptions: { + outputFormat: 'pdfx' + }, cbrOptions: { optimizeForEbook: false }, @@ -288,6 +294,9 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + pdfxOptions: { + outputFormat: 'pdfx' + }, cbrOptions: { optimizeForEbook: false }, @@ -354,6 +363,9 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + pdfxOptions: { + outputFormat: 'pdfx' + }, cbrOptions: { optimizeForEbook: false }, @@ -424,6 +436,9 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + pdfxOptions: { + outputFormat: 'pdfx' + }, cbrOptions: { optimizeForEbook: false }, @@ -492,6 +507,9 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + pdfxOptions: { + outputFormat: 'pdfx' + }, cbrOptions: { optimizeForEbook: false }, @@ -556,6 +574,9 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + pdfxOptions: { + outputFormat: 'pdfx' + }, cbrOptions: { optimizeForEbook: false }, @@ -617,6 +638,9 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + pdfxOptions: { + outputFormat: 'pdfx' + }, cbrOptions: { optimizeForEbook: false }, @@ -680,6 +704,9 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + pdfxOptions: { + outputFormat: 'pdfx' + }, cbrOptions: { optimizeForEbook: false }, @@ -740,6 +767,9 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + pdfxOptions: { + outputFormat: 'pdfx' + }, cbrOptions: { optimizeForEbook: false }, @@ -806,6 +836,9 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + pdfxOptions: { + outputFormat: 'pdfx' + }, cbrOptions: { optimizeForEbook: false }, @@ -871,6 +904,9 @@ describe('Convert Tool Integration Tests', () => { pdfaOptions: { outputFormat: '' }, + pdfxOptions: { + outputFormat: 'pdfx' + }, cbrOptions: { optimizeForEbook: false },