From 1318604f3248a0de4ed73c924d4776bc60eda3f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= <127139797+balazs-szucs@users.noreply.github.com> Date: Thu, 25 Dec 2025 15:58:10 +0100 Subject: [PATCH] [V2] feat(convert): add eBook (EPUB, MOBI, AZW3, FB2) to PDF conversion options and UI (#5291) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes This pull request adds support for converting eBook formats (EPUB, MOBI, AZW3, FB2) to PDF, including a new set of user-configurable options for the conversion process. It introduces frontend UI components, updates configuration and constants, and ensures that the backend can recognize and handle these new formats and options. **eBook to PDF Conversion Support** - Added support for converting eBook formats (`epub`, `mobi`, `azw3`, `fb2`) to PDF, including updates to conversion endpoints, extension mappings, and conversion matrices in `convertConstants.ts` to route these conversions through the new `ebook-to-pdf` endpoint. - Updated the file processing logic so that eBook-to-PDF conversions are processed separately for each file, similar to web file conversions. **Frontend: User Options for eBook Conversion** - Added a new UI component `ConvertFromEbookSettings.tsx` that allows users to configure options for eBook-to-PDF conversion: embedding all fonts, including a table of contents, adding page numbers, and optimizing for eBook readers. This component is conditionally rendered in the conversion settings when an eBook format is selected as the source and PDF as the target. - Integrated the new eBook options into the conversion parameters and ensured they are initialized/reset appropriately in the conversion settings. **Form Data and Backend Integration** - Modified the form data builder to append the new eBook options to the request payload when performing eBook-to-PDF conversions, ensuring these settings are sent to the backend. **Localization and Configuration** - Added localization strings for all new eBook conversion options in the English translation file, providing user-friendly labels and descriptions in the UI. - Updated backend configuration logic to recognize `Calibre` and `FFmpeg` as tool groups, ensuring correct enablement/disablement behavior for these tools. image --- ## 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 --- .../SPDF/config/EndpointConfiguration.java | 15 +-- .../public/locales/en-GB/translation.toml | 12 +++ .../convert/ConvertFromEbookSettings.tsx | 96 +++++++++++++++++++ .../tools/convert/ConvertSettings.tsx | 19 ++++ .../src/core/constants/convertConstants.ts | 12 ++- .../tools/convert/useConvertOperation.ts | 9 +- .../tools/convert/useConvertParameters.ts | 12 +++ 7 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 frontend/src/core/components/tools/convert/ConvertFromEbookSettings.tsx diff --git a/app/common/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/app/common/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index bffd00fda..c752485f0 100644 --- a/app/common/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/app/common/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -156,19 +156,19 @@ public class EndpointConfiguration { return false; } + // Rule 2: For tool groups, they're enabled unless explicitly disabled (handled above) + if (isToolGroup(group)) { + log.debug("isGroupEnabled('{}') -> true (tool group not disabled)", group); + return true; + } + + // Rule 3: For functional groups, check if all endpoints are enabled Set endpoints = endpointGroups.get(group); if (endpoints == null || endpoints.isEmpty()) { log.debug("isGroupEnabled('{}') -> false (no endpoints)", group); return false; } - // Rule 2: For functional groups, check if all endpoints are enabled - // Rule 3: For tool groups, they're enabled unless explicitly disabled (handled above) - if (isToolGroup(group)) { - log.debug("isGroupEnabled('{}') -> true (tool group not disabled)", group); - return true; - } - // For functional groups, check each endpoint individually for (String endpoint : endpoints) { if (!isEndpointEnabledDirectly(endpoint)) { @@ -592,6 +592,7 @@ public class EndpointConfiguration { || "Pdftohtml".equals(group) || "ImageMagick".equals(group) || "rar".equals(group) + || "Calibre".equals(group) || "FFmpeg".equals(group) || "veraPDF".equals(group); } diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 3bb44b13e..f47d45d5d 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -1279,6 +1279,18 @@ optimizeForEbook = "Optimize PDF for ebook readers (uses Ghostscript)" cbzOutputOptions = "PDF to CBZ Options" cbzDpi = "DPI for image rendering" +[convert.ebookOptions] +ebookOptions = "eBook to PDF Options" +ebookOptionsDesc = "Options for converting eBooks to PDF" +embedAllFonts = "Embed all fonts" +embedAllFontsDesc = "Embed all fonts from the eBook into the generated PDF" +includeTableOfContents = "Include table of contents" +includeTableOfContentsDesc = "Add a generated table of contents to the resulting PDF" +includePageNumbers = "Include page numbers" +includePageNumbersDesc = "Add page numbers to the generated PDF" +optimizeForEbookPdf = "Optimize for ebook readers" +optimizeForEbookPdfDesc = "Optimize the PDF for eBook reading (smaller file size, better rendering on eInk devices)" + [imageToPdf] tags = "conversion,img,jpg,picture,photo" diff --git a/frontend/src/core/components/tools/convert/ConvertFromEbookSettings.tsx b/frontend/src/core/components/tools/convert/ConvertFromEbookSettings.tsx new file mode 100644 index 000000000..5d8d2b28b --- /dev/null +++ b/frontend/src/core/components/tools/convert/ConvertFromEbookSettings.tsx @@ -0,0 +1,96 @@ +import { Stack, Checkbox } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { ConvertParameters } from "@app/hooks/tools/convert/useConvertParameters"; + +interface ConvertFromEbookSettingsProps { + parameters: ConvertParameters; + onParameterChange: (key: K, value: ConvertParameters[K]) => void; + disabled?: boolean; +} + +const ConvertFromEbookSettings = ({ + parameters, + onParameterChange, + disabled = false +}: ConvertFromEbookSettingsProps) => { + const { t } = useTranslation(); + + const handleEmbedAllFontsChange = (value: boolean) => { + onParameterChange('ebookOptions', { + embedAllFonts: value, + includeTableOfContents: parameters.ebookOptions?.includeTableOfContents ?? false, + includePageNumbers: parameters.ebookOptions?.includePageNumbers ?? false, + optimizeForEbook: parameters.ebookOptions?.optimizeForEbook ?? false, + }); + }; + + const handleIncludeTableOfContentsChange = (value: boolean) => { + onParameterChange('ebookOptions', { + embedAllFonts: parameters.ebookOptions?.embedAllFonts ?? false, + includeTableOfContents: value, + includePageNumbers: parameters.ebookOptions?.includePageNumbers ?? false, + optimizeForEbook: parameters.ebookOptions?.optimizeForEbook ?? false, + }); + }; + + const handleIncludePageNumbersChange = (value: boolean) => { + onParameterChange('ebookOptions', { + embedAllFonts: parameters.ebookOptions?.embedAllFonts ?? false, + includeTableOfContents: parameters.ebookOptions?.includeTableOfContents ?? false, + includePageNumbers: value, + optimizeForEbook: parameters.ebookOptions?.optimizeForEbook ?? false, + }); + }; + + const handleOptimizeForEbookChange = (value: boolean) => { + onParameterChange('ebookOptions', { + embedAllFonts: parameters.ebookOptions?.embedAllFonts ?? false, + includeTableOfContents: parameters.ebookOptions?.includeTableOfContents ?? false, + includePageNumbers: parameters.ebookOptions?.includePageNumbers ?? false, + optimizeForEbook: value, + }); + }; + + // Initialize ebookOptions if not present + const ebookOptions = parameters.ebookOptions || { + embedAllFonts: false, + includeTableOfContents: false, + includePageNumbers: false, + optimizeForEbook: false, + }; + + return ( + + handleEmbedAllFontsChange(event.currentTarget.checked)} + disabled={disabled} + /> + handleIncludeTableOfContentsChange(event.currentTarget.checked)} + disabled={disabled} + /> + handleIncludePageNumbersChange(event.currentTarget.checked)} + disabled={disabled} + /> + handleOptimizeForEbookChange(event.currentTarget.checked)} + disabled={disabled} + /> + + ); +}; + +export default ConvertFromEbookSettings; diff --git a/frontend/src/core/components/tools/convert/ConvertSettings.tsx b/frontend/src/core/components/tools/convert/ConvertSettings.tsx index 50c01365b..c31928112 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 ConvertFromEbookSettings from "@app/components/tools/convert/ConvertFromEbookSettings"; import { ConvertParameters } from "@app/hooks/tools/convert/useConvertParameters"; import { FROM_FORMAT_OPTIONS, @@ -148,6 +149,12 @@ const ConvertSettings = ({ onParameterChange('cbzOutputOptions', { dpi: 150, }); + onParameterChange('ebookOptions', { + embedAllFonts: false, + includeTableOfContents: false, + includePageNumbers: false, + optimizeForEbook: false, + }); onParameterChange('isSmartDetection', false); onParameterChange('smartDetectionType', 'none'); }; @@ -366,6 +373,18 @@ const ConvertSettings = ({ )} + {/* eBook to PDF options */} + {['epub', 'mobi', 'azw3', 'fb2'].includes(parameters.fromExtension) && parameters.toExtension === 'pdf' && ( + <> + + + + )} + ); }; diff --git a/frontend/src/core/constants/convertConstants.ts b/frontend/src/core/constants/convertConstants.ts index 3e25a737e..80e205967 100644 --- a/frontend/src/core/constants/convertConstants.ts +++ b/frontend/src/core/constants/convertConstants.ts @@ -34,6 +34,7 @@ export const CONVERSION_ENDPOINTS = { 'html-pdf': '/api/v1/convert/html/pdf', 'markdown-pdf': '/api/v1/convert/markdown/pdf', 'eml-pdf': '/api/v1/convert/eml/pdf', + 'ebook-pdf': '/api/v1/convert/ebook/pdf', 'pdf-text-editor': '/api/v1/convert/pdf/text-editor', 'text-editor-pdf': '/api/v1/convert/text-editor/pdf' } as const; @@ -55,6 +56,7 @@ export const ENDPOINT_NAMES = { 'html-pdf': 'html-to-pdf', 'markdown-pdf': 'markdown-to-pdf', 'eml-pdf': 'eml-to-pdf', + 'ebook-pdf': 'ebook-to-pdf', 'pdf-text-editor': 'pdf-to-text-editor', 'text-editor-pdf': 'text-editor-to-pdf' } as const; @@ -89,6 +91,10 @@ export const FROM_FORMAT_OPTIONS = [ { value: 'txt', label: 'TXT', group: 'Text' }, { value: 'rtf', label: 'RTF', group: 'Text' }, { value: 'eml', label: 'EML', group: 'Email' }, + { value: 'epub', label: 'EPUB', group: 'eBook' }, + { value: 'mobi', label: 'MOBI', group: 'eBook' }, + { value: 'azw3', label: 'AZW3', group: 'eBook' }, + { value: 'fb2', label: 'FB2', group: 'eBook' }, ]; export const TO_FORMAT_OPTIONS = [ @@ -127,7 +133,8 @@ export const CONVERSION_MATRIX: Record = { 'zip': ['pdf'], 'md': ['pdf'], 'txt': ['pdf'], 'rtf': ['pdf'], - 'eml': ['pdf'] + 'eml': ['pdf'], + 'epub': ['pdf'], 'mobi': ['pdf'], 'azw3': ['pdf'], 'fb2': ['pdf'] }; // Map extensions to endpoint keys @@ -154,7 +161,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' }, + 'epub': { 'pdf': 'ebook-to-pdf' }, 'mobi': { 'pdf': 'ebook-to-pdf' }, 'azw3': { 'pdf': 'ebook-to-pdf' }, 'fb2': { 'pdf': 'ebook-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 9650ac0e2..d5867422f 100644 --- a/frontend/src/core/hooks/tools/convert/useConvertOperation.ts +++ b/frontend/src/core/hooks/tools/convert/useConvertOperation.ts @@ -28,6 +28,8 @@ export const shouldProcessFilesSeparately = ( // Web files to PDF conversions (each web file should generate its own PDF) ((isWebFormat(parameters.fromExtension) || parameters.fromExtension === 'web') && parameters.toExtension === 'pdf') || + // eBook files to PDF conversions (each file should be processed separately via Calibre) + (['epub', 'mobi', 'azw3', 'fb2'].includes(parameters.fromExtension) && parameters.toExtension === 'pdf') || // Web files smart detection (parameters.isSmartDetection && parameters.smartDetectionType === 'web') || // Mixed file types (smart detection) @@ -43,7 +45,7 @@ export const buildConvertFormData = (parameters: ConvertParameters, selectedFile formData.append("fileInput", file); }); - const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions, pdfaOptions, cbzOptions, cbzOutputOptions } = parameters; + const { fromExtension, toExtension, imageOptions, htmlOptions, emailOptions, pdfaOptions, cbzOptions, cbzOutputOptions, ebookOptions } = parameters; if (isImageFormat(toExtension)) { formData.append("imageFormat", toExtension); @@ -75,6 +77,11 @@ export const buildConvertFormData = (parameters: ConvertParameters, selectedFile formData.append("optimizeForEbook", (cbzOptions?.optimizeForEbook ?? false).toString()); } else if (fromExtension === 'pdf' && toExtension === 'cbz') { formData.append("dpi", (cbzOutputOptions?.dpi ?? 150).toString()); + } else if (['epub', 'mobi', 'azw3', 'fb2'].includes(fromExtension) && toExtension === 'pdf') { + formData.append("embedAllFonts", (ebookOptions?.embedAllFonts ?? false).toString()); + formData.append("includeTableOfContents", (ebookOptions?.includeTableOfContents ?? false).toString()); + formData.append("includePageNumbers", (ebookOptions?.includePageNumbers ?? false).toString()); + formData.append("optimizeForEbook", (ebookOptions?.optimizeForEbook ?? false).toString()); } return formData; diff --git a/frontend/src/core/hooks/tools/convert/useConvertParameters.ts b/frontend/src/core/hooks/tools/convert/useConvertParameters.ts index eaa05d571..ce2ae0fd7 100644 --- a/frontend/src/core/hooks/tools/convert/useConvertParameters.ts +++ b/frontend/src/core/hooks/tools/convert/useConvertParameters.ts @@ -42,6 +42,12 @@ export interface ConvertParameters extends BaseParameters { cbzOutputOptions: { dpi: number; }; + ebookOptions?: { + embedAllFonts: boolean; + includeTableOfContents: boolean; + includePageNumbers: boolean; + optimizeForEbook: boolean; + }; isSmartDetection: boolean; smartDetectionType: 'mixed' | 'images' | 'web' | 'none'; } @@ -81,6 +87,12 @@ export const defaultParameters: ConvertParameters = { cbzOutputOptions: { dpi: 150, }, + ebookOptions: { + embedAllFonts: false, + includeTableOfContents: false, + includePageNumbers: false, + optimizeForEbook: false, + }, isSmartDetection: false, smartDetectionType: 'none', };