diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 4b33f6034..aae5bbc9e 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1105,7 +1105,11 @@ "markdown": "Markdown", "textRtf": "Text/RTF", "grayscale": "Greyscale", - "errorConversion": "An error occurred while converting the file." + "errorConversion": "An error occurred while converting the file.", + "cbzOptions": "CBZ to PDF Options", + "optimizeForEbook": "Optimize PDF for ebook readers (uses Ghostscript)", + "cbzOutputOptions": "PDF to CBZ Options", + "cbzDpi": "DPI for image rendering" }, "imageToPdf": { "tags": "conversion,img,jpg,picture,photo" diff --git a/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx b/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx index b51152af1..e268b941c 100644 --- a/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx +++ b/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx @@ -93,6 +93,13 @@ const FileEditorThumbnail = ({ return (m?.[1] || '').toUpperCase(); }, [file.name]); + const extLower = useMemo(() => { + const m = /\.([a-z0-9]+)$/i.exec(file.name ?? ''); + return (m?.[1] || '').toLowerCase(); + }, [file.name]); + + const isCBZ = extLower === 'cbz'; + const pageLabel = useMemo( () => pageCount > 0 @@ -206,7 +213,7 @@ const FileEditorThumbnail = ({ alert({ alertType: 'success', title: `Unzipping ${file.name}`, expandable: false, durationMs: 2500 }); } }, - hidden: !isZipFile || !onUnzipFile, + hidden: !isZipFile || !onUnzipFile || isCBZ, }, { id: 'close', diff --git a/frontend/src/core/components/tools/convert/ConvertFromCbzSettings.tsx b/frontend/src/core/components/tools/convert/ConvertFromCbzSettings.tsx new file mode 100644 index 000000000..babb20255 --- /dev/null +++ b/frontend/src/core/components/tools/convert/ConvertFromCbzSettings.tsx @@ -0,0 +1,36 @@ +import { Stack, Text, Checkbox } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { ConvertParameters } from '@app/hooks/tools/convert/useConvertParameters'; + +interface ConvertFromCbzSettingsProps { + parameters: ConvertParameters; + onParameterChange: (key: K, value: ConvertParameters[K]) => void; + disabled?: boolean; +} + +const ConvertFromCbzSettings = ({ + parameters, + onParameterChange, + disabled = false +}: ConvertFromCbzSettingsProps) => { + const { t } = useTranslation(); + + return ( + + {t('convert.cbzOptions', 'CBZ to PDF Options')}: + + onParameterChange('cbzOptions', { + ...parameters.cbzOptions, + optimizeForEbook: event.currentTarget.checked + })} + disabled={disabled} + data-testid="optimize-ebook-checkbox" + /> + + ); +}; + +export default ConvertFromCbzSettings; diff --git a/frontend/src/core/components/tools/convert/ConvertSettings.tsx b/frontend/src/core/components/tools/convert/ConvertSettings.tsx index d332f888e..d9727b078 100644 --- a/frontend/src/core/components/tools/convert/ConvertSettings.tsx +++ b/frontend/src/core/components/tools/convert/ConvertSettings.tsx @@ -13,6 +13,8 @@ import ConvertToImageSettings from "@app/components/tools/convert/ConvertToImage import ConvertFromImageSettings from "@app/components/tools/convert/ConvertFromImageSettings"; import ConvertFromWebSettings from "@app/components/tools/convert/ConvertFromWebSettings"; import ConvertFromEmailSettings from "@app/components/tools/convert/ConvertFromEmailSettings"; +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 { ConvertParameters } from "@app/hooks/tools/convert/useConvertParameters"; import { @@ -118,6 +120,12 @@ const ConvertSettings = ({ onParameterChange('pdfaOptions', { outputFormat: 'pdfa-1', }); + onParameterChange('cbzOptions', { + optimizeForEbook: false, + }); + onParameterChange('cbzOutputOptions', { + dpi: 150, + }); onParameterChange('isSmartDetection', false); onParameterChange('smartDetectionType', 'none'); }; @@ -180,6 +188,12 @@ const ConvertSettings = ({ onParameterChange('pdfaOptions', { outputFormat: 'pdfa-1', }); + onParameterChange('cbzOptions', { + optimizeForEbook: false, + }); + onParameterChange('cbzOutputOptions', { + dpi: 150, + }); }; @@ -293,6 +307,30 @@ const ConvertSettings = ({ )} + {/* CBZ to PDF options */} + {parameters.fromExtension === 'cbz' && parameters.toExtension === 'pdf' && ( + <> + + + + )} + + {/* PDF to CBZ options */} + {parameters.fromExtension === 'pdf' && parameters.toExtension === 'cbz' && ( + <> + + + + )} + {/* PDF to PDF/A options */} {parameters.fromExtension === 'pdf' && parameters.toExtension === 'pdfa' && ( <> diff --git a/frontend/src/core/components/tools/convert/ConvertToCbzSettings.tsx b/frontend/src/core/components/tools/convert/ConvertToCbzSettings.tsx new file mode 100644 index 000000000..774219ea5 --- /dev/null +++ b/frontend/src/core/components/tools/convert/ConvertToCbzSettings.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 ConvertToCbzSettingsProps { + parameters: ConvertParameters; + onParameterChange: (key: K, value: ConvertParameters[K]) => void; + disabled?: boolean; +} + +const ConvertToCbzSettings = ({ + parameters, + onParameterChange, + disabled = false +}: ConvertToCbzSettingsProps) => { + const { t } = useTranslation(); + + return ( + + {t('convert.cbzOutputOptions', 'PDF to CBZ Options')}: + + typeof val === 'number' && onParameterChange('cbzOutputOptions', { + ...parameters.cbzOutputOptions, + dpi: val + })} + min={72} + max={600} + step={1} + disabled={disabled} + /> + + ); +}; + +export default ConvertToCbzSettings; diff --git a/frontend/src/core/constants/convertConstants.ts b/frontend/src/core/constants/convertConstants.ts index 5978d523b..0dc6e3726 100644 --- a/frontend/src/core/constants/convertConstants.ts +++ b/frontend/src/core/constants/convertConstants.ts @@ -21,6 +21,8 @@ export const CONVERSION_ENDPOINTS = { 'office-pdf': '/api/v1/convert/file/pdf', 'pdf-image': '/api/v1/convert/pdf/img', 'image-pdf': '/api/v1/convert/img/pdf', + 'cbz-pdf': '/api/v1/convert/cbz/pdf', + 'pdf-cbz': '/api/v1/convert/pdf/cbz', 'pdf-office-word': '/api/v1/convert/pdf/word', 'pdf-office-presentation': '/api/v1/convert/pdf/presentation', 'pdf-office-text': '/api/v1/convert/pdf/text', @@ -38,6 +40,8 @@ export const ENDPOINT_NAMES = { 'office-pdf': 'file-to-pdf', 'pdf-image': 'pdf-to-img', 'image-pdf': 'img-to-pdf', + 'cbz-pdf': 'cbz-to-pdf', + 'pdf-cbz': 'pdf-to-cbz', 'pdf-office-word': 'pdf-to-word', 'pdf-office-presentation': 'pdf-to-presentation', 'pdf-office-text': 'pdf-to-text', @@ -57,6 +61,7 @@ export const FROM_FORMAT_OPTIONS = [ { value: 'any', label: 'Any', group: 'Multiple Files' }, { value: 'image', label: 'Images', group: 'Multiple Files' }, { value: 'pdf', label: 'PDF', group: 'Document' }, + { value: 'cbz', label: 'CBZ', group: 'Archive' }, { value: 'docx', label: 'DOCX', group: 'Document' }, { value: 'doc', label: 'DOC', group: 'Document' }, { value: 'odt', label: 'ODT', group: 'Document' }, @@ -87,6 +92,7 @@ export const TO_FORMAT_OPTIONS = [ { value: 'pdfa', label: 'PDF/A', group: 'Document' }, { value: 'docx', label: 'DOCX', group: 'Document' }, { value: 'odt', label: 'ODT', group: 'Document' }, + { value: 'cbz', label: 'CBZ', group: 'Archive' }, { value: 'csv', label: 'CSV', group: 'Spreadsheet' }, { value: 'pptx', label: 'PPTX', group: 'Presentation' }, { value: 'odp', label: 'ODP', group: 'Presentation' }, @@ -107,7 +113,8 @@ 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'], + 'pdf': ['png', 'jpg', 'gif', 'tiff', 'bmp', 'webp', 'docx', 'odt', 'pptx', 'odp', 'csv', 'txt', 'rtf', 'md', 'html', 'xml', 'pdfa', 'cbz'], + 'cbz': ['pdf'], 'docx': ['pdf'], 'doc': ['pdf'], 'odt': ['pdf'], 'xlsx': ['pdf'], 'xls': ['pdf'], 'ods': ['pdf'], 'pptx': ['pdf'], 'ppt': ['pdf'], 'odp': ['pdf'], @@ -130,8 +137,10 @@ 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', + 'cbz': 'pdf-to-cbz' }, + 'cbz': { 'pdf': 'cbz-to-pdf' }, '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' }, 'pptx': { 'pdf': 'file-to-pdf' }, 'ppt': { 'pdf': 'file-to-pdf' }, 'odp': { 'pdf': 'file-to-pdf' }, diff --git a/frontend/src/core/constants/convertSupportedFornats.ts b/frontend/src/core/constants/convertSupportedFornats.ts index b9bea3227..86138c4e6 100644 --- a/frontend/src/core/constants/convertSupportedFornats.ts +++ b/frontend/src/core/constants/convertSupportedFornats.ts @@ -13,7 +13,7 @@ export const CONVERT_SUPPORTED_FORMATS = [ // Email formats 'eml', // Archive formats - 'zip', + 'zip', 'cbz', // Other 'dbf', 'fods', 'vsd', 'vor', 'vor3', 'vor4', 'uop', 'pct', 'ps', 'pdf', ]; diff --git a/frontend/src/core/hooks/tools/convert/useConvertOperation.ts b/frontend/src/core/hooks/tools/convert/useConvertOperation.ts index 28080407d..9134c9db4 100644 --- a/frontend/src/core/hooks/tools/convert/useConvertOperation.ts +++ b/frontend/src/core/hooks/tools/convert/useConvertOperation.ts @@ -39,7 +39,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, cbzOptions, cbzOutputOptions } = parameters; if (isImageFormat(toExtension)) { formData.append("imageFormat", toExtension); @@ -67,6 +67,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 === 'cbz' && toExtension === 'pdf') { + formData.append("optimizeForEbook", (cbzOptions?.optimizeForEbook ?? false).toString()); + } else if (fromExtension === 'pdf' && toExtension === 'cbz') { + formData.append("dpi", (cbzOutputOptions?.dpi ?? 150).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..eaa05d571 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; }; + cbzOptions: { + optimizeForEbook: boolean; + }; + cbzOutputOptions: { + dpi: number; + }; isSmartDetection: boolean; smartDetectionType: 'mixed' | 'images' | 'web' | 'none'; } @@ -69,6 +75,12 @@ export const defaultParameters: ConvertParameters = { pdfaOptions: { outputFormat: 'pdfa-1', }, + cbzOptions: { + optimizeForEbook: false, + }, + cbzOutputOptions: { + dpi: 150, + }, isSmartDetection: false, smartDetectionType: 'none', }; diff --git a/frontend/src/core/tests/convert/ConvertIntegration.test.tsx b/frontend/src/core/tests/convert/ConvertIntegration.test.tsx index 80e8760c7..bb2f0bcf1 100644 --- a/frontend/src/core/tests/convert/ConvertIntegration.test.tsx +++ b/frontend/src/core/tests/convert/ConvertIntegration.test.tsx @@ -151,6 +151,12 @@ describe('Convert Tool Integration Tests', () => { }, pdfaOptions: { outputFormat: '' + }, + cbzOptions: { + optimizeForEbook: false + }, + cbzOutputOptions: { + dpi: 150 } }; @@ -218,6 +224,12 @@ describe('Convert Tool Integration Tests', () => { }, pdfaOptions: { outputFormat: '' + }, + cbzOptions: { + optimizeForEbook: false + }, + cbzOutputOptions: { + dpi: 150 } }; @@ -263,6 +275,12 @@ describe('Convert Tool Integration Tests', () => { }, pdfaOptions: { outputFormat: '' + }, + cbzOptions: { + optimizeForEbook: false + }, + cbzOutputOptions: { + dpi: 150 } }; @@ -317,6 +335,12 @@ describe('Convert Tool Integration Tests', () => { }, pdfaOptions: { outputFormat: '' + }, + cbzOptions: { + optimizeForEbook: false + }, + cbzOutputOptions: { + dpi: 150 } }; @@ -375,6 +399,12 @@ describe('Convert Tool Integration Tests', () => { }, pdfaOptions: { outputFormat: '' + }, + cbzOptions: { + optimizeForEbook: false + }, + cbzOutputOptions: { + dpi: 150 } }; @@ -431,6 +461,12 @@ describe('Convert Tool Integration Tests', () => { }, pdfaOptions: { outputFormat: '' + }, + cbzOptions: { + optimizeForEbook: false + }, + cbzOutputOptions: { + dpi: 150 } }; @@ -483,6 +519,12 @@ describe('Convert Tool Integration Tests', () => { }, pdfaOptions: { outputFormat: '' + }, + cbzOptions: { + optimizeForEbook: false + }, + cbzOutputOptions: { + dpi: 150 } }; @@ -532,6 +574,12 @@ describe('Convert Tool Integration Tests', () => { }, pdfaOptions: { outputFormat: '' + }, + cbzOptions: { + optimizeForEbook: false + }, + cbzOutputOptions: { + dpi: 150 } }; @@ -583,6 +631,12 @@ describe('Convert Tool Integration Tests', () => { }, pdfaOptions: { outputFormat: '' + }, + cbzOptions: { + optimizeForEbook: false + }, + cbzOutputOptions: { + dpi: 150 } }; @@ -631,6 +685,12 @@ describe('Convert Tool Integration Tests', () => { }, pdfaOptions: { outputFormat: '' + }, + cbzOptions: { + optimizeForEbook: false + }, + cbzOutputOptions: { + dpi: 150 } }; @@ -685,6 +745,12 @@ describe('Convert Tool Integration Tests', () => { }, pdfaOptions: { outputFormat: '' + }, + cbzOptions: { + optimizeForEbook: false + }, + cbzOutputOptions: { + dpi: 150 } }; @@ -738,6 +804,12 @@ describe('Convert Tool Integration Tests', () => { }, pdfaOptions: { outputFormat: '' + }, + cbzOptions: { + optimizeForEbook: false + }, + cbzOutputOptions: { + dpi: 150 } }; diff --git a/frontend/src/core/utils/urlMapping.ts b/frontend/src/core/utils/urlMapping.ts index c76adf856..ba6fe1aaa 100644 --- a/frontend/src/core/utils/urlMapping.ts +++ b/frontend/src/core/utils/urlMapping.ts @@ -28,6 +28,8 @@ export const URL_TO_TOOL_MAP: Record = { '/pdf-to-pdfa': 'convert', '/pdf-to-word': 'convert', '/pdf-to-xml': 'convert', + '/cbz-to-pdf': 'convert', + '/pdf-to-cbz': 'convert', // Security tools '/add-password': 'addPassword',