feat(convert): add support for CBR to PDF and PDF to CBR conversions

- Introduced `ConvertFromCbrSettings` and `ConvertToCbrSettings` components for handling conversion-specific settings.
- Updated `ConvertSettings` to include CBR-related configurations.
- Enhanced `useConvertParameters` with new parameters: `cbrOptions` and `pdfToCbrOptions`.
- Updated locales with CBR-related translations.
- Added endpoints and updated conversion matrix for CBR formats in `convertConstants`.
- Modified `useConvertOperation` to handle CBR-specific parameters during form data preparation.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
This commit is contained in:
Balázs Szücs 2025-11-06 09:34:49 +01:00
parent 4c0c9b28ef
commit 2122b1a323
8 changed files with 156 additions and 8 deletions

View File

@ -1079,7 +1079,11 @@
"markdown": "Markdown",
"textRtf": "Text/RTF",
"grayscale": "Greyscale",
"errorConversion": "An error occurred while converting the file."
"errorConversion": "An error occurred while converting the file.",
"cbrOptions": "CBR to PDF Options",
"optimizeForEbook": "Optimize PDF for ebook readers (uses Ghostscript)",
"cbrOutputOptions": "PDF to CBR Options",
"cbrDpi": "DPI for image rendering"
},
"imageToPdf": {
"tags": "conversion,img,jpg,picture,photo"

View File

@ -2021,6 +2021,10 @@
"outputFormat": "Output Format",
"pdfaNote": "PDF/A-1b is more compatible, PDF/A-2b supports more features.",
"pdfaDigitalSignatureWarning": "The PDF contains a digital signature. This will be removed in the next step.",
"cbrOptions": "CBR to PDF Options",
"optimizeForEbook": "Optimize PDF for ebook readers (uses Ghostscript)",
"cbrOutputOptions": "PDF to CBR Options",
"cbrDpi": "DPI for image rendering",
"sanitize": {
"submit": "Sanitize PDF",
"completed": "Sanitization completed successfully",

View File

@ -0,0 +1,36 @@
import {Checkbox, Stack, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ConvertParameters } from "@app/hooks/tools/convert/useConvertParameters";
interface ConvertFromCbrSettingsProps {
parameters: ConvertParameters;
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
disabled?: boolean;
}
const ConvertFromCbrSettings = ({
parameters,
onParameterChange,
disabled = false
}: ConvertFromCbrSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="sm" data-testid="cbr-options-section">
<Text size="sm" fw={500}>{t("convert.cbrOptions", "CBR Options")}:</Text>
<Checkbox
label={t('convert.optimizeForEbook', 'Optimize PDF for ebook readers (uses Ghostscript)')}
checked={parameters.cbrOptions.optimizeForEbook}
onChange={(event) => onParameterChange('cbrOptions', {
...parameters.cbrOptions,
optimizeForEbook: event.currentTarget.checked
})}
disabled={disabled}
data-testid="optimize-ebook-checkbox"
/>
</Stack>
);
};
export default ConvertFromCbrSettings;

View File

@ -14,6 +14,8 @@ import ConvertFromImageSettings from "@app/components/tools/convert/ConvertFromI
import ConvertFromWebSettings from "@app/components/tools/convert/ConvertFromWebSettings";
import ConvertFromEmailSettings from "@app/components/tools/convert/ConvertFromEmailSettings";
import ConvertToPdfaSettings from "@app/components/tools/convert/ConvertToPdfaSettings";
import ConvertFromCbrSettings from "@app/components/tools/convert/ConvertFromCbrSettings";
import ConvertToCbrSettings from "@app/components/tools/convert/ConvertToCbrSettings";
import { ConvertParameters } from "@app/hooks/tools/convert/useConvertParameters";
import {
FROM_FORMAT_OPTIONS,
@ -118,6 +120,12 @@ const ConvertSettings = ({
onParameterChange('pdfaOptions', {
outputFormat: 'pdfa-1',
});
onParameterChange('cbrOptions', {
optimizeForEbook: false,
});
onParameterChange('pdfToCbrOptions', {
dpi: 150,
});
onParameterChange('isSmartDetection', false);
onParameterChange('smartDetectionType', 'none');
};
@ -180,6 +188,12 @@ const ConvertSettings = ({
onParameterChange('pdfaOptions', {
outputFormat: 'pdfa-1',
});
onParameterChange('cbrOptions', {
optimizeForEbook: false,
});
onParameterChange('pdfToCbrOptions', {
dpi: 150,
});
};
@ -306,6 +320,30 @@ const ConvertSettings = ({
</>
)}
{/* CBR to PDF options */}
{parameters.fromExtension === 'cbr' && parameters.toExtension === 'pdf' && (
<>
<Divider />
<ConvertFromCbrSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
</>
)}
{/* PDF to CBR options */}
{parameters.fromExtension === 'pdf' && parameters.toExtension === 'cbr' && (
<>
<Divider />
<ConvertToCbrSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
</>
)}
</Stack>
);
};

View File

@ -0,0 +1,39 @@
import { Stack, Text, NumberInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ConvertParameters } from "@app/hooks/tools/convert/useConvertParameters";
interface ConvertToCbrSettingsProps {
parameters: ConvertParameters;
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
disabled?: boolean;
}
const ConvertToCbrSettings = ({
parameters,
onParameterChange,
disabled = false
}: ConvertToCbrSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="sm" data-testid="cbr-output-options-section">
<Text size="sm" fw={500}>{t("convert.cbrOutputOptions", "PDF to CBR Options")}:</Text>
<NumberInput
data-testid="cbr-dpi-input"
label={t("convert.cbrDpi", "DPI for image rendering")}
value={parameters.pdfToCbrOptions.dpi}
onChange={(value) => onParameterChange('pdfToCbrOptions', {
...parameters.pdfToCbrOptions,
dpi: typeof value === 'number' ? value : 150
})}
min={72}
max={600}
step={50}
disabled={disabled}
/>
</Stack>
);
};
export default ConvertToCbrSettings;

View File

@ -31,7 +31,9 @@ export const CONVERSION_ENDPOINTS = {
'pdf-pdfa': '/api/v1/convert/pdf/pdfa',
'html-pdf': '/api/v1/convert/html/pdf',
'markdown-pdf': '/api/v1/convert/markdown/pdf',
'eml-pdf': '/api/v1/convert/eml/pdf'
'eml-pdf': '/api/v1/convert/eml/pdf',
'cbr-pdf': '/api/v1/convert/cbr/pdf',
'pdf-cbr': '/api/v1/convert/pdf/cbr'
} as const;
export const ENDPOINT_NAMES = {
@ -48,7 +50,9 @@ export const ENDPOINT_NAMES = {
'pdf-pdfa': 'pdf-to-pdfa',
'html-pdf': 'html-to-pdf',
'markdown-pdf': 'markdown-to-pdf',
'eml-pdf': 'eml-to-pdf'
'eml-pdf': 'eml-to-pdf',
'cbr-pdf': 'cbr-to-pdf',
'pdf-cbr': 'pdf-to-cbr'
} as const;
@ -80,6 +84,7 @@ export const FROM_FORMAT_OPTIONS = [
{ value: 'txt', label: 'TXT', group: 'Text' },
{ value: 'rtf', label: 'RTF', group: 'Text' },
{ value: 'eml', label: 'EML', group: 'Email' },
{ value: 'cbr', label: 'CBR', group: 'Archive' },
];
export const TO_FORMAT_OPTIONS = [
@ -101,13 +106,14 @@ export const TO_FORMAT_OPTIONS = [
{ value: 'webp', label: 'WEBP', group: 'Image' },
{ value: 'html', label: 'HTML', group: 'Web' },
{ value: 'xml', label: 'XML', group: 'Web' },
{ value: 'cbr', label: 'CBR', group: 'Archive' },
];
// Conversion matrix - what each source format can convert to
export const CONVERSION_MATRIX: Record<string, string[]> = {
'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'],
'pdf': ['png', 'jpg', 'gif', 'tiff', 'bmp', 'webp', 'docx', 'odt', 'pptx', 'odp', 'csv', 'txt', 'rtf', 'md', 'html', 'xml', 'pdfa', 'cbr'],
'docx': ['pdf'], 'doc': ['pdf'], 'odt': ['pdf'],
'xlsx': ['pdf'], 'xls': ['pdf'], 'ods': ['pdf'],
'pptx': ['pdf'], 'ppt': ['pdf'], 'odp': ['pdf'],
@ -116,7 +122,8 @@ export const CONVERSION_MATRIX: Record<string, string[]> = {
'zip': ['pdf'],
'md': ['pdf'],
'txt': ['pdf'], 'rtf': ['pdf'],
'eml': ['pdf']
'eml': ['pdf'],
'cbr': ['pdf']
};
// Map extensions to endpoint keys
@ -130,7 +137,8 @@ export const EXTENSION_TO_ENDPOINT: Record<string, Record<string, string>> = {
'csv': 'pdf-to-csv',
'txt': 'pdf-to-text', 'rtf': 'pdf-to-text', 'md': 'pdf-to-markdown',
'html': 'pdf-to-html', 'xml': 'pdf-to-xml',
'pdfa': 'pdf-to-pdfa'
'pdfa': 'pdf-to-pdfa',
'cbr': 'pdf-to-cbr'
},
'docx': { 'pdf': 'file-to-pdf' }, 'doc': { 'pdf': 'file-to-pdf' }, 'odt': { 'pdf': 'file-to-pdf' },
'xlsx': { 'pdf': 'file-to-pdf' }, 'xls': { 'pdf': 'file-to-pdf' }, 'ods': { 'pdf': 'file-to-pdf' },
@ -141,7 +149,8 @@ export const EXTENSION_TO_ENDPOINT: Record<string, Record<string, string>> = {
'zip': { 'pdf': 'html-to-pdf' },
'md': { 'pdf': 'markdown-to-pdf' },
'txt': { 'pdf': 'file-to-pdf' }, 'rtf': { 'pdf': 'file-to-pdf' },
'eml': { 'pdf': 'eml-to-pdf' }
'eml': { 'pdf': 'eml-to-pdf' },
'cbr': { 'pdf': 'cbr-to-pdf' }
};
export type ColorType = typeof COLOR_TYPES[keyof typeof COLOR_TYPES];

View File

@ -21,6 +21,8 @@ export const shouldProcessFilesSeparately = (
(parameters.fromExtension === 'pdf' && parameters.toExtension === 'pdfa') ||
// 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)
(parameters.fromExtension === 'pdf' && parameters.toExtension === 'cbr') ||
// Web files to PDF conversions (each web file should generate its own PDF)
((isWebFormat(parameters.fromExtension) || parameters.fromExtension === 'web') &&
parameters.toExtension === 'pdf') ||
@ -39,7 +41,7 @@ export const buildConvertFormData = (parameters: ConvertParameters, selectedFile
formData.append("fileInput", file);
});
const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions, pdfaOptions } = parameters;
const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions, pdfaOptions, cbrOptions, pdfToCbrOptions } = parameters;
if (isImageFormat(toExtension)) {
formData.append("imageFormat", toExtension);
@ -67,6 +69,10 @@ export const buildConvertFormData = (parameters: ConvertParameters, selectedFile
formData.append("outputFormat", pdfaOptions.outputFormat);
} else if (fromExtension === 'pdf' && toExtension === 'csv') {
formData.append("pageNumbers", "all");
} else if (fromExtension === 'cbr' && toExtension === 'pdf') {
formData.append("optimizeForEbook", cbrOptions.optimizeForEbook.toString());
} else if (fromExtension === 'pdf' && toExtension === 'cbr') {
formData.append("dpi", pdfToCbrOptions.dpi.toString());
}
return formData;

View File

@ -36,6 +36,12 @@ export interface ConvertParameters extends BaseParameters {
pdfaOptions: {
outputFormat: string;
};
cbrOptions: {
optimizeForEbook: boolean;
};
pdfToCbrOptions: {
dpi: number;
};
isSmartDetection: boolean;
smartDetectionType: 'mixed' | 'images' | 'web' | 'none';
}
@ -69,6 +75,12 @@ export const defaultParameters: ConvertParameters = {
pdfaOptions: {
outputFormat: 'pdfa-1',
},
cbrOptions: {
optimizeForEbook: false,
},
pdfToCbrOptions: {
dpi: 150,
},
isSmartDetection: false,
smartDetectionType: 'none',
};