From 2122b1a32393506f5e336df28c12164edf58698c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= Date: Thu, 6 Nov 2025 09:34:49 +0100 Subject: [PATCH] feat(convert): add support for CBR to PDF and PDF to CBR conversions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../public/locales/en-GB/translation.json | 6 ++- .../public/locales/en-US/translation.json | 4 ++ .../tools/convert/ConvertFromCbrSettings.tsx | 36 +++++++++++++++++ .../tools/convert/ConvertSettings.tsx | 38 ++++++++++++++++++ .../tools/convert/ConvertToCbrSettings.tsx | 39 +++++++++++++++++++ .../src/core/constants/convertConstants.ts | 21 +++++++--- .../tools/convert/useConvertOperation.ts | 8 +++- .../tools/convert/useConvertParameters.ts | 12 ++++++ 8 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 frontend/src/core/components/tools/convert/ConvertFromCbrSettings.tsx create mode 100644 frontend/src/core/components/tools/convert/ConvertToCbrSettings.tsx diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 669c9f505..f2cf03a39 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -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" diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index c92c4c56e..70b5379f1 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -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", diff --git a/frontend/src/core/components/tools/convert/ConvertFromCbrSettings.tsx b/frontend/src/core/components/tools/convert/ConvertFromCbrSettings.tsx new file mode 100644 index 000000000..c5faa8c8a --- /dev/null +++ b/frontend/src/core/components/tools/convert/ConvertFromCbrSettings.tsx @@ -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: (key: K, value: ConvertParameters[K]) => void; + disabled?: boolean; +} + +const ConvertFromCbrSettings = ({ + parameters, + onParameterChange, + disabled = false +}: ConvertFromCbrSettingsProps) => { + const { t } = useTranslation(); + + return ( + + {t("convert.cbrOptions", "CBR Options")}: + + onParameterChange('cbrOptions', { + ...parameters.cbrOptions, + optimizeForEbook: event.currentTarget.checked + })} + disabled={disabled} + data-testid="optimize-ebook-checkbox" + /> + + ); +}; + +export default ConvertFromCbrSettings; diff --git a/frontend/src/core/components/tools/convert/ConvertSettings.tsx b/frontend/src/core/components/tools/convert/ConvertSettings.tsx index d332f888e..25ef868c8 100644 --- a/frontend/src/core/components/tools/convert/ConvertSettings.tsx +++ b/frontend/src/core/components/tools/convert/ConvertSettings.tsx @@ -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' && ( + <> + + + + )} + + {/* PDF to CBR options */} + {parameters.fromExtension === 'pdf' && parameters.toExtension === 'cbr' && ( + <> + + + + )} + ); }; diff --git a/frontend/src/core/components/tools/convert/ConvertToCbrSettings.tsx b/frontend/src/core/components/tools/convert/ConvertToCbrSettings.tsx new file mode 100644 index 000000000..df7a41a3f --- /dev/null +++ b/frontend/src/core/components/tools/convert/ConvertToCbrSettings.tsx @@ -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: (key: K, value: ConvertParameters[K]) => void; + disabled?: boolean; +} + +const ConvertToCbrSettings = ({ + parameters, + onParameterChange, + disabled = false +}: ConvertToCbrSettingsProps) => { + const { t } = useTranslation(); + + return ( + + {t("convert.cbrOutputOptions", "PDF to CBR Options")}: + + onParameterChange('pdfToCbrOptions', { + ...parameters.pdfToCbrOptions, + dpi: typeof value === 'number' ? value : 150 + })} + min={72} + max={600} + step={50} + disabled={disabled} + /> + + ); +}; + +export default ConvertToCbrSettings; diff --git a/frontend/src/core/constants/convertConstants.ts b/frontend/src/core/constants/convertConstants.ts index 5978d523b..a2e3bd698 100644 --- a/frontend/src/core/constants/convertConstants.ts +++ b/frontend/src/core/constants/convertConstants.ts @@ -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 = { '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 = { '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> = { '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> = { '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]; diff --git a/frontend/src/core/hooks/tools/convert/useConvertOperation.ts b/frontend/src/core/hooks/tools/convert/useConvertOperation.ts index 28080407d..baa4c10e8 100644 --- a/frontend/src/core/hooks/tools/convert/useConvertOperation.ts +++ b/frontend/src/core/hooks/tools/convert/useConvertOperation.ts @@ -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; diff --git a/frontend/src/core/hooks/tools/convert/useConvertParameters.ts b/frontend/src/core/hooks/tools/convert/useConvertParameters.ts index c9c19176e..83ef1e524 100644 --- a/frontend/src/core/hooks/tools/convert/useConvertParameters.ts +++ b/frontend/src/core/hooks/tools/convert/useConvertParameters.ts @@ -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', };