mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-01 20:10:35 +01:00
[V2] feat(convert): add eBook (EPUB, MOBI, AZW3, FB2) to PDF conversion options and UI (#5291)
# 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. <img width="366" height="997" alt="image" src="https://github.com/user-attachments/assets/44d3308c-ea49-4874-8f5e-c7d617a37489" /> <!-- 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:
parent
43a5f72b01
commit
1318604f32
@ -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<String> 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);
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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: <K extends keyof ConvertParameters>(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 (
|
||||
<Stack gap="sm">
|
||||
<Checkbox
|
||||
label={t("convert.ebookOptions.embedAllFonts", "Embed all fonts")}
|
||||
description={t("convert.ebookOptions.embedAllFontsDesc", "Embed all fonts from the eBook into the generated PDF")}
|
||||
checked={ebookOptions.embedAllFonts}
|
||||
onChange={(event) => handleEmbedAllFontsChange(event.currentTarget.checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t("convert.ebookOptions.includeTableOfContents", "Include table of contents")}
|
||||
description={t("convert.ebookOptions.includeTableOfContentsDesc", "Add a generated table of contents to the resulting PDF")}
|
||||
checked={ebookOptions.includeTableOfContents}
|
||||
onChange={(event) => handleIncludeTableOfContentsChange(event.currentTarget.checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t("convert.ebookOptions.includePageNumbers", "Include page numbers")}
|
||||
description={t("convert.ebookOptions.includePageNumbersDesc", "Add page numbers to the generated PDF")}
|
||||
checked={ebookOptions.includePageNumbers}
|
||||
onChange={(event) => handleIncludePageNumbersChange(event.currentTarget.checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t("convert.ebookOptions.optimizeForEbookPdf", "Optimize for ebook readers")}
|
||||
description={t("convert.ebookOptions.optimizeForEbookPdfDesc", "Optimize the PDF for eBook reading (smaller file size, better rendering on eInk devices)")}
|
||||
checked={ebookOptions.optimizeForEbook}
|
||||
onChange={(event) => handleOptimizeForEbookChange(event.currentTarget.checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConvertFromEbookSettings;
|
||||
@ -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' && (
|
||||
<>
|
||||
<Divider />
|
||||
<ConvertFromEbookSettings
|
||||
parameters={parameters}
|
||||
onParameterChange={onParameterChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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<string, string[]> = {
|
||||
'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<string, Record<string, string>> = {
|
||||
'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];
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user