[V2] feat(convert): add support for CBZ to PDF and PDF to CBZ conversion (#4831)

# Description of Changes


TLDR:
- Implemented `ConvertFromCbzSettings` component for CBZ to PDF options
- Added `ConvertToCbzSettings` component for PDF to CBZ options
- Extended conversion constants with new CBZ-related formats and
mappings
- Updated `ConvertSettings` to include CBZ-specific configuration forms
- Updated `useConvertOperation` to handle CBZ conversion parameters

For backend reference see this PR: #4472

This pull request adds support for converting between CBZ (Comic Book
Zip) and PDF formats in the frontend conversion tool. It introduces new
UI components for CBZ-to-PDF and PDF-to-CBZ conversion options, updates
the conversion parameters and form data logic, and integrates these
formats into the available conversion options and endpoints.

**CBZ/PDF Conversion Support:**

* Added `ConvertFromCbzSettings` and `ConvertToCbzSettings` components
to provide user options for CBZ-to-PDF and PDF-to-CBZ conversions,
including "Optimize for ebook" and DPI settings.

**Conversion Logic and Parameters:**

* Extended the `ConvertParameters` interface and default parameters to
include `cbzOptions` (for CBZ-to-PDF) and `cbzOutputOptions` (for
PDF-to-CBZ).

**Conversion Matrix and Endpoint Integration:**

* Added CBZ to the supported "from" and "to" format lists, updated the
conversion matrix to allow CBZ-to-PDF and PDF-to-CBZ, and mapped the new
conversions to appropriate API endpoints.
* Updated URL mapping to recognize `/cbz-to-pdf` and `/pdf-to-cbz` as
conversion tool routes.

### Front-end

<img width="1852" height="994" alt="image"
src="https://github.com/user-attachments/assets/b902438a-8ce7-4eed-a4f8-d80677c0c796"
/>
<img width="270" height="994" alt="image"
src="https://github.com/user-attachments/assets/87556380-e0c5-4974-93c2-d36f86268b49"
/>
<img width="417" height="389" alt="image"
src="https://github.com/user-attachments/assets/1889a3ec-0c06-442a-b3bb-71e1a7c34e4c"
/>
<img width="1853" height="994" alt="image"
src="https://github.com/user-attachments/assets/d6d68f5f-ed70-4be9-89b1-2e4b7cd90921"
/>
<img width="265" height="994" alt="image"
src="https://github.com/user-attachments/assets/36a05dd5-164f-46a7-b8af-aa815d6c0767"
/>




<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
This commit is contained in:
Balázs Szücs 2025-11-12 16:27:41 +01:00 committed by GitHub
parent 74a1438c21
commit a05c5a53c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 229 additions and 6 deletions

View File

@ -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"

View File

@ -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',

View File

@ -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: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
disabled?: boolean;
}
const ConvertFromCbzSettings = ({
parameters,
onParameterChange,
disabled = false
}: ConvertFromCbzSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="sm" data-testid="cbz-settings">
<Text size="sm" fw={500}>{t('convert.cbzOptions', 'CBZ to PDF Options')}:</Text>
<Checkbox
label={t('convert.optimizeForEbook', 'Optimize PDF for ebook readers (uses Ghostscript)')}
checked={parameters.cbzOptions.optimizeForEbook}
onChange={(event) => onParameterChange('cbzOptions', {
...parameters.cbzOptions,
optimizeForEbook: event.currentTarget.checked
})}
disabled={disabled}
data-testid="optimize-ebook-checkbox"
/>
</Stack>
);
};
export default ConvertFromCbzSettings;

View File

@ -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' && (
<>
<Divider />
<ConvertFromCbzSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
</>
)}
{/* PDF to CBZ options */}
{parameters.fromExtension === 'pdf' && parameters.toExtension === 'cbz' && (
<>
<Divider />
<ConvertToCbzSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
</>
)}
{/* PDF to PDF/A options */}
{parameters.fromExtension === 'pdf' && parameters.toExtension === 'pdfa' && (
<>

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 ConvertToCbzSettingsProps {
parameters: ConvertParameters;
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
disabled?: boolean;
}
const ConvertToCbzSettings = ({
parameters,
onParameterChange,
disabled = false
}: ConvertToCbzSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="sm" data-testid="cbz-output-settings">
<Text size="sm" fw={500}>{t('convert.cbzOutputOptions', 'PDF to CBZ Options')}:</Text>
<NumberInput
data-testid="cbz-dpi-input"
label={t('convert.cbzDpi', 'DPI for image rendering')}
value={parameters.cbzOutputOptions.dpi}
onChange={(val) => typeof val === 'number' && onParameterChange('cbzOutputOptions', {
...parameters.cbzOutputOptions,
dpi: val
})}
min={72}
max={600}
step={1}
disabled={disabled}
/>
</Stack>
);
};
export default ConvertToCbzSettings;

View File

@ -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<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', '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<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',
'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' },

View File

@ -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',
];

View File

@ -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;

View File

@ -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',
};

View File

@ -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
}
};

View File

@ -28,6 +28,8 @@ export const URL_TO_TOOL_MAP: Record<string, ToolId> = {
'/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',