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