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