From 19aef5e034e7280903a39cdce903282e3818cfd3 Mon Sep 17 00:00:00 2001 From: Ludy Date: Tue, 11 Nov 2025 22:44:18 +0100 Subject: [PATCH] feat(conversion): add eBook to PDF via Calibre (EPUB/MOBI/AZW3/FB2/TXT/DOCX) (#4644) This pull request adds support for converting common eBook formats (EPUB, MOBI, AZW3, FB2, TXT, DOCX) to PDF using Calibre. It introduces a new API endpoint and updates the configuration, dependency checks, and documentation to support this feature. Additionally, it includes related UI and localization changes. **New eBook to PDF conversion feature:** * Added `ConvertEbookToPDFController` with a new `/api/v1/convert/ebook/pdf` endpoint to handle eBook to PDF conversion using Calibre, supporting options like embedding fonts, including table of contents, and page numbers. * Introduced `ConvertEbookToPdfRequest` model for handling conversion requests and options. **Configuration and dependency management:** * Updated `RuntimePathConfig`, `ApplicationProperties`, and `ExternalAppDepConfig` to support Calibre's executable path configuration and dependency checking, ensuring Calibre is available and correctly integrated. [[1]](diffhunk://#diff-68c561052c2376c3d494bf11dd821958acd9917b1b2d33a7195ca2d6df7ec517R24) [[2]](diffhunk://#diff-68c561052c2376c3d494bf11dd821958acd9917b1b2d33a7195ca2d6df7ec517R61) [[3]](diffhunk://#diff-68c561052c2376c3d494bf11dd821958acd9917b1b2d33a7195ca2d6df7ec517R72-R74) [[4]](diffhunk://#diff-1c357db0a3e88cf5bedd4a5852415fadad83b8b3b9eb56e67059d8b9d8b10702R359) [[5]](diffhunk://#diff-8932df49d210349a062949da2ed43ce769b0f107354880a78103664f008f849eR26-R34) [[6]](diffhunk://#diff-8932df49d210349a062949da2ed43ce769b0f107354880a78103664f008f849eR48) [[7]](diffhunk://#diff-8932df49d210349a062949da2ed43ce769b0f107354880a78103664f008f849eR63-R68) [[8]](diffhunk://#diff-8932df49d210349a062949da2ed43ce769b0f107354880a78103664f008f849eR132) * Registered the new endpoint and tool group in `EndpointConfiguration`, including logic to enable/disable the feature based on Calibre's presence. [[1]](diffhunk://#diff-3cddb66d1cf93eeb8103ccd17cee8ed006e0c0ee006d0ee1cf42d512f177e437R260) [[2]](diffhunk://#diff-3cddb66d1cf93eeb8103ccd17cee8ed006e0c0ee006d0ee1cf42d512f177e437R440-R442) [[3]](diffhunk://#diff-3cddb66d1cf93eeb8103ccd17cee8ed006e0c0ee006d0ee1cf42d512f177e437L487-R492) **Documentation and localization:** * Updated the `README.md` to mention eBook to PDF conversion support. * Added UI route and form for eBook to PDF conversion in the web controller. * Added English and German localization strings for the new feature, including descriptions, labels, and error messages. [[1]](diffhunk://#diff-ee1c6999a33498cfa3abba4a384e73a8b8269856899438de80560c965079a9fdR617-R620) [[2]](diffhunk://#diff-482633b22866efc985222c4a14efc5b7d2487b59f39b953f038273a39d0362f7R617-R620) [[3]](diffhunk://#diff-482633b22866efc985222c4a14efc5b7d2487b59f39b953f038273a39d0362f7R1476-R1485) ## 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) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] 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. --------- Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> --- Dockerfile | 25 +- Dockerfile.dev | 8 +- Dockerfile.fat | 31 +- Dockerfile.ultra-lite | 8 +- README.md | 1 + .../configuration/RuntimePathConfig.java | 5 + .../common/model/ApplicationProperties.java | 1 + .../SPDF/config/EndpointConfiguration.java | 5 + .../SPDF/config/ExternalAppDepConfig.java | 3 + .../ConvertEbookToPDFController.java | 208 ++++++++++++++ .../web/ConverterWebController.java | 7 + .../converters/ConvertEbookToPdfRequest.java | 53 ++++ .../main/resources/messages_de_DE.properties | 15 + .../main/resources/messages_en_GB.properties | 15 + .../src/main/resources/settings.yml.template | 1 + .../templates/convert/ebook-to-pdf.html | 107 +++++++ .../templates/fragments/navElements.html | 6 + .../ConvertEbookToPDFControllerTest.java | 266 ++++++++++++++++++ devGuide/DeveloperGuide.md | 8 +- testing/allEndpointsRemovedSettings.yml | 2 +- testing/endpoints.txt | 1 + testing/webpage_urls_full.txt | 3 +- 22 files changed, 749 insertions(+), 30 deletions(-) create mode 100644 app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFController.java create mode 100644 app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertEbookToPdfRequest.java create mode 100644 app/core/src/main/resources/templates/convert/ebook-to-pdf.html create mode 100644 app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFControllerTest.java diff --git a/Dockerfile b/Dockerfile index d36ea60a9..bb8412cc9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,11 +38,12 @@ ENV DISABLE_ADDITIONAL_FEATURES=true \ TEMP=/tmp/stirling-pdf \ TMP=/tmp/stirling-pdf - # JDK for app -RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ - echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \ - echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \ +RUN printf '%s\n' \ + 'https://dl-cdn.alpinelinux.org/alpine/edge/main' \ + 'https://dl-cdn.alpinelinux.org/alpine/edge/community' \ + 'https://dl-cdn.alpinelinux.org/alpine/edge/testing' \ + > /etc/apk/repositories && \ apk upgrade --no-cache -a && \ apk add --no-cache \ ca-certificates \ @@ -65,19 +66,23 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a # OCR MY PDF (unpaper for descew and other advanced features) tesseract-ocr-data-eng \ tesseract-ocr-data-chi_sim \ - tesseract-ocr-data-deu \ - tesseract-ocr-data-fra \ - tesseract-ocr-data-por \ + tesseract-ocr-data-deu \ + tesseract-ocr-data-fra \ + tesseract-ocr-data-por \ unpaper \ - # CV + # CV / Python py3-opencv \ python3 \ ocrmypdf \ py3-pip \ - py3-pillow@testing \ - py3-pdf2image@testing \ + py3-pillow \ + py3-pdf2image \ + # Calibre + calibre \ # URW Base 35 fonts for better PDF rendering font-urw-base35 && \ + # Calibre fixes + apk fix --no-cache calibre && \ python3 -m venv /opt/venv && \ /opt/venv/bin/pip install --no-cache-dir --upgrade pip setuptools && \ /opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \ diff --git a/Dockerfile.dev b/Dockerfile.dev index 517e94b95..6098acd41 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -25,6 +25,12 @@ RUN apt-get update && apt-get install -y \ python3-venv \ # ss -tln iproute2 \ +# calibre requires these dependencies + wget \ + xz-utils \ + libopengl0 \ + libxcb-cursor0 \ + && wget -nv -O- https://download.calibre-ebook.com/linux-installer.sh | sh /dev/stdin \ && apt-get clean && rm -rf /var/lib/apt/lists/* # Setze die Environment Variable für setuptools @@ -38,7 +44,7 @@ ENV SETUPTOOLS_USE_DISTUTILS=local \ COPY .github/scripts/requirements_dev.txt /tmp/requirements_dev.txt RUN python3 -m venv --system-site-packages /opt/venv \ && . /opt/venv/bin/activate \ - && pip install --no-cache-dir --require-hashes -r /tmp/requirements_dev.txt + && pip install --no-cache-dir --only-binary=:all: --require-hashes -r /tmp/requirements_dev.txt # Füge den venv-Pfad zur globalen PATH-Variable hinzu, damit die Tools verfügbar sind ENV PATH="/opt/venv/bin:$PATH" diff --git a/Dockerfile.fat b/Dockerfile.fat index b6afec888..363c4b555 100644 --- a/Dockerfile.fat +++ b/Dockerfile.fat @@ -17,9 +17,9 @@ WORKDIR /app COPY . . # Build the application with DISABLE_ADDITIONAL_FEATURES=false -RUN DISABLE_ADDITIONAL_FEATURES=false \ - STIRLING_PDF_DESKTOP_UI=false \ - ./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube +ENV DISABLE_ADDITIONAL_FEATURES=false \ + STIRLING_PDF_DESKTOP_UI=false +RUN ./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube # Main stage FROM alpine:3.22.2@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412 @@ -52,11 +52,12 @@ ENV DISABLE_ADDITIONAL_FEATURES=true \ TEMP=/tmp/stirling-pdf \ TMP=/tmp/stirling-pdf - # JDK for app -RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ - echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \ - echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \ +RUN printf '%s\n' \ + 'https://dl-cdn.alpinelinux.org/alpine/edge/main' \ + 'https://dl-cdn.alpinelinux.org/alpine/edge/community' \ + 'https://dl-cdn.alpinelinux.org/alpine/edge/testing' \ + > /etc/apk/repositories && \ apk upgrade --no-cache -a && \ apk add --no-cache \ ca-certificates \ @@ -79,18 +80,22 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a # OCR MY PDF (unpaper for descew and other advanced featues) tesseract-ocr-data-eng \ tesseract-ocr-data-chi_sim \ - tesseract-ocr-data-deu \ - tesseract-ocr-data-fra \ - tesseract-ocr-data-por \ + tesseract-ocr-data-deu \ + tesseract-ocr-data-fra \ + tesseract-ocr-data-por \ unpaper \ font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra font-liberation font-linux-libertine font-urw-base35 \ - # CV + # CV / Python py3-opencv \ python3 \ ocrmypdf \ py3-pip \ - py3-pillow@testing \ - py3-pdf2image@testing && \ + py3-pillow \ + py3-pdf2image \ + # Calibre (musl-native) + QtWebEngine Runtime + calibre && \ + # Calibre fixes + apk fix --no-cache calibre && \ python3 -m venv /opt/venv && \ /opt/venv/bin/pip install --no-cache-dir --upgrade pip setuptools && \ /opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \ diff --git a/Dockerfile.ultra-lite b/Dockerfile.ultra-lite index 04ba3de15..3a7c67072 100644 --- a/Dockerfile.ultra-lite +++ b/Dockerfile.ultra-lite @@ -24,9 +24,11 @@ COPY scripts/installFonts.sh /scripts/installFonts.sh COPY app/core/build/libs/*.jar app.jar # Set up necessary directories and permissions -RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ - echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \ - echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \ +RUN printf '%s\n' \ + 'https://dl-cdn.alpinelinux.org/alpine/edge/main' \ + 'https://dl-cdn.alpinelinux.org/alpine/edge/community' \ + 'https://dl-cdn.alpinelinux.org/alpine/edge/testing' \ + > /etc/apk/repositories && \ apk upgrade --no-cache -a && \ apk add --no-cache \ ca-certificates \ diff --git a/README.md b/README.md index ef37d70fe..e901dd273 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir - **CBZ to PDF**: Convert comic book archives - **CBR to PDF**: Convert comic book rar archives - **Email to PDF**: Convert email files to PDF +- **eBook to PDF**: Convert eBook formats (EPUB, MOBI, AZW3, FB2, TXT, DOCX) to PDF (using Calibre) - **Vector Image to PDF**: Convert vector images (PS, EPS, EPSF) to PDF format #### Convert from PDF diff --git a/app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java b/app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java index 53fa97c25..f8bc38a6b 100644 --- a/app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java +++ b/app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java @@ -21,6 +21,7 @@ public class RuntimePathConfig { private final String basePath; private final String weasyPrintPath; private final String unoConvertPath; + private final String calibrePath; // Pipeline paths private final String pipelineWatchedFoldersPath; @@ -57,6 +58,7 @@ public class RuntimePathConfig { // Initialize Operation paths String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint"; String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert"; + String defaultCalibrePath = isDocker ? "/usr/bin/ebook-convert" : "ebook-convert"; Operations operations = properties.getSystem().getCustomPaths().getOperations(); this.weasyPrintPath = @@ -67,6 +69,9 @@ public class RuntimePathConfig { resolvePath( defaultUnoConvertPath, operations != null ? operations.getUnoconvert() : null); + this.calibrePath = + resolvePath( + defaultCalibrePath, operations != null ? operations.getCalibre() : null); } private String resolvePath(String defaultPath, String customPath) { diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index 2aba77b25..e8606b1f9 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -371,6 +371,7 @@ public class ApplicationProperties { public static class Operations { private String weasyprint; private String unoconvert; + private String calibre; } } diff --git a/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index d8b00b0e7..74d71825e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -257,6 +257,7 @@ public class EndpointConfiguration { addEndpointToGroup("Convert", "html-to-pdf"); addEndpointToGroup("Convert", "url-to-pdf"); addEndpointToGroup("Convert", "markdown-to-pdf"); + addEndpointToGroup("Convert", "ebook-to-pdf"); addEndpointToGroup("Convert", "pdf-to-csv"); addEndpointToGroup("Convert", "pdf-to-markdown"); addEndpointToGroup("Convert", "eml-to-pdf"); @@ -446,6 +447,9 @@ public class EndpointConfiguration { addEndpointToGroup("Weasyprint", "markdown-to-pdf"); addEndpointToGroup("Weasyprint", "eml-to-pdf"); + // Calibre dependent endpoints + addEndpointToGroup("Calibre", "ebook-to-pdf"); + // Pdftohtml dependent endpoints addEndpointToGroup("Pdftohtml", "pdf-to-html"); addEndpointToGroup("Pdftohtml", "pdf-to-markdown"); @@ -498,6 +502,7 @@ public class EndpointConfiguration { || "Javascript".equals(group) || "Weasyprint".equals(group) || "Pdftohtml".equals(group) + || "Calibre".equals(group) || "rar".equals(group) || "FFmpeg".equals(group); } diff --git a/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java index 9f8d7d17c..a703d1da3 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java @@ -40,6 +40,7 @@ public class ExternalAppDepConfig { private final String weasyprintPath; private final String unoconvPath; + private final String calibrePath; /** * Map of command(binary) -> affected groups (e.g. "gs" -> ["Ghostscript"]). Immutable to avoid @@ -56,6 +57,7 @@ public class ExternalAppDepConfig { this.endpointConfiguration = endpointConfiguration; this.weasyprintPath = runtimePathConfig.getWeasyPrintPath(); this.unoconvPath = runtimePathConfig.getUnoConvertPath(); + this.calibrePath = runtimePathConfig.getCalibrePath(); Map> tmp = new HashMap<>(); tmp.put("gs", List.of("Ghostscript")); @@ -67,6 +69,7 @@ public class ExternalAppDepConfig { tmp.put("qpdf", List.of("qpdf")); tmp.put("tesseract", List.of("tesseract")); tmp.put("rar", List.of("rar")); + tmp.put(calibrePath, List.of("Calibre")); tmp.put("ffmpeg", List.of("FFmpeg")); this.commandToGroupMapping = Collections.unmodifiableMap(tmp); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFController.java new file mode 100644 index 000000000..c1b59bb41 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFController.java @@ -0,0 +1,208 @@ +package stirling.software.SPDF.controller.api.converters; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import org.apache.commons.io.FilenameUtils; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import io.github.pixee.security.Filenames; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.SPDF.config.EndpointConfiguration; +import stirling.software.SPDF.model.api.converters.ConvertEbookToPdfRequest; +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.ProcessExecutor; +import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; +import stirling.software.common.util.TempFileManager; +import stirling.software.common.util.WebResponseUtils; + +@RestController +@RequestMapping("/api/v1/convert") +@Tag(name = "Convert", description = "Convert APIs") +@RequiredArgsConstructor +@Slf4j +public class ConvertEbookToPDFController { + + private static final Set SUPPORTED_EXTENSIONS = + Set.of("epub", "mobi", "azw3", "fb2", "txt", "docx"); + + private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; + private final EndpointConfiguration endpointConfiguration; + + private boolean isCalibreEnabled() { + return endpointConfiguration.isGroupEnabled("Calibre"); + } + + private boolean isGhostscriptEnabled() { + return endpointConfiguration.isGroupEnabled("Ghostscript"); + } + + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/ebook/pdf") + @Operation( + summary = "Convert an eBook file to PDF", + description = + "This endpoint converts common eBook formats (EPUB, MOBI, AZW3, FB2, TXT, DOCX)" + + " to PDF using Calibre. Input:BOOK Output:PDF Type:SISO") + public ResponseEntity convertEbookToPdf( + @ModelAttribute ConvertEbookToPdfRequest request) throws Exception { + if (!isCalibreEnabled()) { + throw new IllegalStateException("Calibre support is disabled"); + } + + MultipartFile inputFile = request.getFileInput(); + if (inputFile == null || inputFile.isEmpty()) { + throw new IllegalArgumentException("No input file provided"); + } + + boolean optimizeForEbook = Boolean.TRUE.equals(request.getOptimizeForEbook()); + if (optimizeForEbook && !isGhostscriptEnabled()) { + log.warn( + "Ghostscript optimization requested but Ghostscript is not enabled/available" + + " for ebook conversion"); + optimizeForEbook = false; + } + boolean embedAllFonts = Boolean.TRUE.equals(request.getEmbedAllFonts()); + boolean includeTableOfContents = Boolean.TRUE.equals(request.getIncludeTableOfContents()); + boolean includePageNumbers = Boolean.TRUE.equals(request.getIncludePageNumbers()); + + String originalFilename = Filenames.toSimpleFileName(inputFile.getOriginalFilename()); + if (originalFilename == null || originalFilename.isBlank()) { + originalFilename = "document"; + } + + String extension = FilenameUtils.getExtension(originalFilename); + if (extension == null || extension.isBlank()) { + throw new IllegalArgumentException("Unable to determine file type"); + } + + String lowerExtension = extension.toLowerCase(Locale.ROOT); + if (!SUPPORTED_EXTENSIONS.contains(lowerExtension)) { + throw new IllegalArgumentException("Unsupported eBook file extension: " + extension); + } + + String baseName = FilenameUtils.getBaseName(originalFilename); + if (baseName == null || baseName.isBlank()) { + baseName = "document"; + } + + Path workingDirectory = tempFileManager.createTempDirectory(); + Path inputPath = workingDirectory.resolve(baseName + "." + lowerExtension); + Path outputPath = workingDirectory.resolve(baseName + ".pdf"); + + try (InputStream inputStream = inputFile.getInputStream()) { + Files.copy(inputStream, inputPath, StandardCopyOption.REPLACE_EXISTING); + } + + List command = + buildCalibreCommand( + inputPath, + outputPath, + embedAllFonts, + includeTableOfContents, + includePageNumbers); + ProcessExecutorResult result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.CALIBRE) + .runCommandWithOutputHandling(command, workingDirectory.toFile()); + + if (result == null) { + throw new IllegalStateException("Calibre conversion returned no result"); + } + + if (result.getRc() != 0) { + String errorMessage = result.getMessages(); + if (errorMessage == null || errorMessage.isBlank()) { + errorMessage = "Calibre conversion failed"; + } + throw new IllegalStateException(errorMessage); + } + + if (!Files.exists(outputPath) || Files.size(outputPath) == 0L) { + throw new IllegalStateException("Calibre did not produce a PDF output"); + } + + String outputFilename = + GeneralUtils.generateFilename(originalFilename, "_convertedToPDF.pdf"); + + try { + if (optimizeForEbook) { + byte[] pdfBytes = Files.readAllBytes(outputPath); + try { + byte[] optimizedPdf = GeneralUtils.optimizePdfWithGhostscript(pdfBytes); + return WebResponseUtils.bytesToWebResponse(optimizedPdf, outputFilename); + } catch (IOException e) { + log.warn( + "Ghostscript optimization failed for ebook conversion, returning" + + " original PDF", + e); + return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); + } + } + + try (PDDocument document = pdfDocumentFactory.load(outputPath.toFile())) { + return WebResponseUtils.pdfDocToWebResponse(document, outputFilename); + } + } finally { + cleanupTempFiles(workingDirectory, inputPath, outputPath); + } + } + + private List buildCalibreCommand( + Path inputPath, + Path outputPath, + boolean embedAllFonts, + boolean includeTableOfContents, + boolean includePageNumbers) { + List command = new ArrayList<>(); + command.add("ebook-convert"); + command.add(inputPath.toString()); + command.add(outputPath.toString()); + + if (embedAllFonts) { + command.add("--embed-all-fonts"); + } + if (includeTableOfContents) { + command.add("--pdf-add-toc"); + } + if (includePageNumbers) { + command.add("--pdf-page-numbers"); + } + + return command; + } + + private void cleanupTempFiles(Path workingDirectory, Path inputPath, Path outputPath) { + List pathsToDelete = new ArrayList<>(); + pathsToDelete.add(inputPath); + pathsToDelete.add(outputPath); + + for (Path path : pathsToDelete) { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + log.warn("Failed to delete temporary file: {}", path, e); + } + } + tempFileManager.deleteTempDirectory(workingDirectory); + } +} diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java index db6c62bc4..ef0d840b2 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java @@ -47,6 +47,13 @@ public class ConverterWebController { return "convert/cbr-to-pdf"; } + @GetMapping("/ebook-to-pdf") + @Hidden + public String convertEbookToPdfForm(Model model) { + model.addAttribute("currentPage", "ebook-to-pdf"); + return "convert/ebook-to-pdf"; + } + @GetMapping("/pdf-to-cbr") @Hidden public String convertPdfToCbrForm(Model model) { diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertEbookToPdfRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertEbookToPdfRequest.java new file mode 100644 index 000000000..9461bbb15 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertEbookToPdfRequest.java @@ -0,0 +1,53 @@ +package stirling.software.SPDF.model.api.converters; + +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode +public class ConvertEbookToPdfRequest { + + @Schema( + description = + "The input eBook file to be converted to a PDF file (EPUB, MOBI, AZW3, FB2," + + " TXT, DOCX)", + contentMediaType = + "application/epub+zip, application/x-mobipocket-ebook, application/x-azw3," + + " text/xml, text/plain," + + " application/vnd.openxmlformats-officedocument.wordprocessingml.document", + requiredMode = Schema.RequiredMode.REQUIRED) + private MultipartFile fileInput; + + @Schema( + description = "Embed all fonts from the eBook into the generated PDF", + allowableValues = {"true", "false"}, + requiredMode = Schema.RequiredMode.REQUIRED, + defaultValue = "false") + private Boolean embedAllFonts; + + @Schema( + description = "Add a generated table of contents to the resulting PDF", + requiredMode = Schema.RequiredMode.REQUIRED, + allowableValues = {"true", "false"}, + defaultValue = "false") + private Boolean includeTableOfContents; + + @Schema( + description = "Add page numbers to the generated PDF", + requiredMode = Schema.RequiredMode.REQUIRED, + allowableValues = {"true", "false"}, + defaultValue = "false") + private Boolean includePageNumbers; + + @Schema( + description = + "Optimize the PDF for eBook reading (smaller file size, better rendering on" + + " eInk devices)", + allowableValues = {"true", "false"}, + defaultValue = "false") + private Boolean optimizeForEbook; +} diff --git a/app/core/src/main/resources/messages_de_DE.properties b/app/core/src/main/resources/messages_de_DE.properties index c97b32e1c..9625d8935 100644 --- a/app/core/src/main/resources/messages_de_DE.properties +++ b/app/core/src/main/resources/messages_de_DE.properties @@ -616,6 +616,10 @@ home.cbrToPdf.title=CBR zu PDF home.cbrToPdf.desc=CBR-Comicarchive in das PDF-Format konvertieren. cbrToPdf.tags=konvertierung,comic,buch,archiv,cbr,rar +home.ebookToPdf.title=E-Book zu PDF +home.ebookToPdf.desc=E-Book-Dateien (EPUB, MOBI, AZW3, FB2, TXT, DOCX) mit Calibre in PDF konvertieren. +ebookToPdf.tags=konvertierung,ebook,calibre,epub,mobi,azw3 + home.pdfToCbz.title=PDF zu CBZ home.pdfToCbz.desc=PDF-Dateien in CBZ-Comicarchive umwandeln. pdfToCbz.tags=konvertierung,comic,buch,archiv,cbz,pdf @@ -1490,6 +1494,17 @@ cbrToPDF.submit=Zu PDF konvertieren cbrToPDF.selectText=CBR-Datei auswählen cbrToPDF.optimizeForEbook=PDF für E-Book-Reader optimieren (verwendet Ghostscript) +#ebookToPDF +ebookToPDF.title=E-Book zu PDF +ebookToPDF.header=E-Book zu PDF +ebookToPDF.submit=Zu PDF konvertieren +ebookToPDF.selectText=E-Book-Datei auswählen +ebookToPDF.embedAllFonts=Alle Schriftarten in der erzeugten PDF einbetten (kann die Dateigröße erhöhen) +ebookToPDF.includeTableOfContents=Inhaltsverzeichnis zur erzeugten PDF hinzufügen +ebookToPDF.includePageNumbers=Seitenzahlen zur erzeugten PDF hinzufügen +ebookToPDF.optimizeForEbook=PDF für E-Book-Reader optimieren (verwendet Ghostscript) +ebookToPDF.calibreDisabled=Calibre-Unterstützung ist deaktiviert. Aktivieren Sie die Calibre-Werkzeuggruppe oder installieren Sie Calibre, um diese Funktion zu nutzen. + #pdfToCBR pdfToCBR.title=PDF zu CBR pdfToCBR.header=PDF zu CBR diff --git a/app/core/src/main/resources/messages_en_GB.properties b/app/core/src/main/resources/messages_en_GB.properties index 41e0ef4ee..e4c6378f1 100644 --- a/app/core/src/main/resources/messages_en_GB.properties +++ b/app/core/src/main/resources/messages_en_GB.properties @@ -616,6 +616,10 @@ home.cbrToPdf.title=CBR to PDF home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. cbrToPdf.tags=conversion,comic,book,archive,cbr,rar +home.ebookToPdf.title=eBook to PDF +home.ebookToPdf.desc=Convert eBook files (EPUB, MOBI, AZW3, FB2, TXT, DOCX) to PDF using Calibre. +ebookToPdf.tags=conversion,ebook,calibre,epub,mobi,azw3 + home.pdfToCbz.title=PDF to CBZ home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf @@ -1490,6 +1494,17 @@ cbrToPDF.submit=Convert to PDF cbrToPDF.selectText=Select CBR file cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) +#ebookToPDF +ebookToPDF.title=eBook to PDF +ebookToPDF.header=eBook to PDF +ebookToPDF.submit=Convert to PDF +ebookToPDF.selectText=Select eBook file +ebookToPDF.embedAllFonts=Embed all fonts in the output PDF (may increase file size) +ebookToPDF.includeTableOfContents=Add a generated table of contents to the PDF +ebookToPDF.includePageNumbers=Add page numbers to the generated PDF +ebookToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) +ebookToPDF.calibreDisabled=Calibre support is disabled. Enable the Calibre tool group or install Calibre to use this feature. + #pdfToCBR pdfToCBR.title=PDF to CBR pdfToCBR.header=PDF to CBR diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index f5bf4ebcd..734f3f793 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -149,6 +149,7 @@ system: operations: weasyprint: '' # Defaults to /opt/venv/bin/weasyprint unoconvert: '' # Defaults to /opt/venv/bin/unoconvert + calibre: '' # Defaults to /usr/bin/ebook-convert fileUploadLimit: '' # Defaults to "". No limit when string is empty. Set a number, between 0 and 999, followed by one of the following strings to set a limit. "KB", "MB", "GB". tempFileManagement: baseTmpDir: '' # Defaults to java.io.tmpdir/stirling-pdf diff --git a/app/core/src/main/resources/templates/convert/ebook-to-pdf.html b/app/core/src/main/resources/templates/convert/ebook-to-pdf.html new file mode 100644 index 000000000..9c2614fcf --- /dev/null +++ b/app/core/src/main/resources/templates/convert/ebook-to-pdf.html @@ -0,0 +1,107 @@ + + + + + + + + + +
+
+ +

+
+
+
+
+ menu_book + +
+

+ +
+ Calibre support is disabled. +
+ +
+
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/app/core/src/main/resources/templates/fragments/navElements.html b/app/core/src/main/resources/templates/fragments/navElements.html index bbf09985e..3bb9ea25c 100644 --- a/app/core/src/main/resources/templates/fragments/navElements.html +++ b/app/core/src/main/resources/templates/fragments/navElements.html @@ -53,6 +53,9 @@
+
+
@@ -132,6 +135,9 @@
+
+
diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFControllerTest.java new file mode 100644 index 000000000..95f0de648 --- /dev/null +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFControllerTest.java @@ -0,0 +1,266 @@ +package stirling.software.SPDF.controller.api.converters; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockMultipartFile; + +import stirling.software.SPDF.config.EndpointConfiguration; +import stirling.software.SPDF.model.api.converters.ConvertEbookToPdfRequest; +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.ProcessExecutor; +import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; +import stirling.software.common.util.ProcessExecutor.Processes; +import stirling.software.common.util.TempFileManager; +import stirling.software.common.util.WebResponseUtils; + +@ExtendWith(MockitoExtension.class) +class ConvertEbookToPDFControllerTest { + + @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; + @Mock private EndpointConfiguration endpointConfiguration; + + @InjectMocks private ConvertEbookToPDFController controller; + + @Test + void convertEbookToPdf_buildsCalibreCommandAndCleansUp() throws Exception { + when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true); + + MockMultipartFile ebookFile = + new MockMultipartFile( + "fileInput", "ebook.epub", "application/epub+zip", "content".getBytes()); + + ConvertEbookToPdfRequest request = new ConvertEbookToPdfRequest(); + request.setFileInput(ebookFile); + request.setEmbedAllFonts(true); + request.setIncludeTableOfContents(true); + request.setIncludePageNumbers(true); + + Path workingDir = Files.createTempDirectory("ebook-convert-test-"); + when(tempFileManager.createTempDirectory()).thenReturn(workingDir); + + AtomicReference deletedDir = new AtomicReference<>(); + Mockito.doAnswer( + invocation -> { + Path dir = invocation.getArgument(0); + deletedDir.set(dir); + if (Files.exists(dir)) { + try (Stream paths = Files.walk(dir)) { + paths.sorted(Comparator.reverseOrder()) + .forEach( + path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + } + }); + } + } + return null; + }) + .when(tempFileManager) + .deleteTempDirectory(any(Path.class)); + + PDDocument mockDocument = Mockito.mock(PDDocument.class); + when(pdfDocumentFactory.load(any(File.class))).thenReturn(mockDocument); + + try (MockedStatic pe = Mockito.mockStatic(ProcessExecutor.class); + MockedStatic wr = Mockito.mockStatic(WebResponseUtils.class); + MockedStatic gu = Mockito.mockStatic(GeneralUtils.class)) { + + ProcessExecutor executor = Mockito.mock(ProcessExecutor.class); + pe.when(() -> ProcessExecutor.getInstance(Processes.CALIBRE)).thenReturn(executor); + + ProcessExecutorResult execResult = Mockito.mock(ProcessExecutorResult.class); + when(execResult.getRc()).thenReturn(0); + + @SuppressWarnings("unchecked") + ArgumentCaptor> commandCaptor = ArgumentCaptor.forClass(List.class); + Path expectedInput = workingDir.resolve("ebook.epub"); + Path expectedOutput = workingDir.resolve("ebook.pdf"); + when(executor.runCommandWithOutputHandling( + commandCaptor.capture(), eq(workingDir.toFile()))) + .thenAnswer( + invocation -> { + Files.writeString(expectedOutput, "pdf"); + return execResult; + }); + + ResponseEntity expectedResponse = ResponseEntity.ok("result".getBytes()); + wr.when( + () -> + WebResponseUtils.pdfDocToWebResponse( + mockDocument, "ebook_convertedToPDF.pdf")) + .thenReturn(expectedResponse); + gu.when(() -> GeneralUtils.generateFilename("ebook.epub", "_convertedToPDF.pdf")) + .thenReturn("ebook_convertedToPDF.pdf"); + + ResponseEntity response = controller.convertEbookToPdf(request); + + assertSame(expectedResponse, response); + + List command = commandCaptor.getValue(); + assertEquals(6, command.size()); + assertEquals("ebook-convert", command.get(0)); + assertEquals(expectedInput.toString(), command.get(1)); + assertEquals(expectedOutput.toString(), command.get(2)); + assertEquals("--embed-all-fonts", command.get(3)); + assertEquals("--pdf-add-toc", command.get(4)); + assertEquals("--pdf-page-numbers", command.get(5)); + + assertFalse(Files.exists(expectedInput)); + assertFalse(Files.exists(expectedOutput)); + assertEquals(workingDir, deletedDir.get()); + Mockito.verify(tempFileManager).deleteTempDirectory(workingDir); + } + + if (Files.exists(workingDir)) { + try (Stream paths = Files.walk(workingDir)) { + paths.sorted(Comparator.reverseOrder()) + .forEach( + path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + } + }); + } + } + } + + @Test + void convertEbookToPdf_withUnsupportedExtensionThrows() { + when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true); + + MockMultipartFile unsupported = + new MockMultipartFile( + "fileInput", "ebook.exe", "application/octet-stream", new byte[] {1, 2, 3}); + + ConvertEbookToPdfRequest request = new ConvertEbookToPdfRequest(); + request.setFileInput(unsupported); + + assertThrows(IllegalArgumentException.class, () -> controller.convertEbookToPdf(request)); + } + + @Test + void convertEbookToPdf_withOptimizeForEbookUsesGhostscript() throws Exception { + when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true); + when(endpointConfiguration.isGroupEnabled("Ghostscript")).thenReturn(true); + + MockMultipartFile ebookFile = + new MockMultipartFile( + "fileInput", "ebook.epub", "application/epub+zip", "content".getBytes()); + + ConvertEbookToPdfRequest request = new ConvertEbookToPdfRequest(); + request.setFileInput(ebookFile); + request.setOptimizeForEbook(true); + + Path workingDir = Files.createTempDirectory("ebook-convert-opt-test-"); + when(tempFileManager.createTempDirectory()).thenReturn(workingDir); + + AtomicReference deletedDir = new AtomicReference<>(); + Mockito.doAnswer( + invocation -> { + Path dir = invocation.getArgument(0); + deletedDir.set(dir); + if (Files.exists(dir)) { + try (Stream paths = Files.walk(dir)) { + paths.sorted(Comparator.reverseOrder()) + .forEach( + path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + } + }); + } + } + return null; + }) + .when(tempFileManager) + .deleteTempDirectory(any(Path.class)); + + try (MockedStatic pe = Mockito.mockStatic(ProcessExecutor.class); + MockedStatic gu = Mockito.mockStatic(GeneralUtils.class); + MockedStatic wr = Mockito.mockStatic(WebResponseUtils.class)) { + + ProcessExecutor executor = Mockito.mock(ProcessExecutor.class); + pe.when(() -> ProcessExecutor.getInstance(Processes.CALIBRE)).thenReturn(executor); + + ProcessExecutorResult execResult = Mockito.mock(ProcessExecutorResult.class); + when(execResult.getRc()).thenReturn(0); + + Path expectedInput = workingDir.resolve("ebook.epub"); + Path expectedOutput = workingDir.resolve("ebook.pdf"); + when(executor.runCommandWithOutputHandling(any(List.class), eq(workingDir.toFile()))) + .thenAnswer( + invocation -> { + Files.writeString(expectedOutput, "pdf"); + return execResult; + }); + + gu.when(() -> GeneralUtils.generateFilename("ebook.epub", "_convertedToPDF.pdf")) + .thenReturn("ebook_convertedToPDF.pdf"); + byte[] optimizedBytes = "optimized".getBytes(StandardCharsets.UTF_8); + gu.when(() -> GeneralUtils.optimizePdfWithGhostscript(Mockito.any(byte[].class))) + .thenReturn(optimizedBytes); + + ResponseEntity expectedResponse = ResponseEntity.ok(optimizedBytes); + wr.when( + () -> + WebResponseUtils.bytesToWebResponse( + optimizedBytes, "ebook_convertedToPDF.pdf")) + .thenReturn(expectedResponse); + + ResponseEntity response = controller.convertEbookToPdf(request); + + assertSame(expectedResponse, response); + gu.verify(() -> GeneralUtils.optimizePdfWithGhostscript(Mockito.any(byte[].class))); + Mockito.verifyNoInteractions(pdfDocumentFactory); + Mockito.verify(tempFileManager).deleteTempDirectory(workingDir); + assertEquals(workingDir, deletedDir.get()); + assertFalse(Files.exists(expectedInput)); + assertFalse(Files.exists(expectedOutput)); + } + + if (Files.exists(workingDir)) { + try (Stream paths = Files.walk(workingDir)) { + paths.sorted(Comparator.reverseOrder()) + .forEach( + path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + } + }); + } + } + } +} diff --git a/devGuide/DeveloperGuide.md b/devGuide/DeveloperGuide.md index fb8911eaf..746e09e24 100644 --- a/devGuide/DeveloperGuide.md +++ b/devGuide/DeveloperGuide.md @@ -12,6 +12,7 @@ Stirling-PDF is built using: - PDFBox - LibreOffice - qpdf +- Calibre (`ebook-convert` CLI) for eBook conversions - HTML, CSS, JavaScript - Docker - PDF.js @@ -54,7 +55,12 @@ Stirling-PDF is built using: Stirling-PDF uses Lombok to reduce boilerplate code. Some IDEs, like Eclipse, don't support Lombok out of the box. To set up Lombok in your development environment: Visit the [Lombok website](https://projectlombok.org/setup/) for installation instructions specific to your IDE. -5. Add environment variable +5. Install Calibre CLI (optional but required for eBook conversions) + Ensure the `ebook-convert` binary from Calibre is available on your PATH when working on the + eBook to PDF feature. The Calibre tool group is automatically disabled when the binary is + missing, so having it installed locally allows you to exercise the full workflow. + +6. Add environment variable For local testing, you should generally be testing the full 'Security' version of Stirling PDF. To do this, you must add the environment flag DISABLE_ADDITIONAL_FEATURES=false to your system and/or IDE build/run step. ## 4. Project Structure diff --git a/testing/allEndpointsRemovedSettings.yml b/testing/allEndpointsRemovedSettings.yml index 014556fc0..58e7fd9f9 100644 --- a/testing/allEndpointsRemovedSettings.yml +++ b/testing/allEndpointsRemovedSettings.yml @@ -158,7 +158,7 @@ ui: languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled. endpoints: - toRemove: [crop, merge-pdfs, multi-page-layout, overlay-pdfs, pdf-to-single-page, rearrange-pages, remove-image-pdf, remove-pages, rotate-pdf, scale-pages, split-by-size-or-count, split-pages, split-pdf-by-chapters, split-pdf-by-sections, add-password, add-watermark, auto-redact, cert-sign, get-info-on-pdf, redact, remove-cert-sign, remove-password, sanitize-pdf, validate-signature, file-to-pdf, html-to-pdf, img-to-pdf, markdown-to-pdf, pdf-to-csv, pdf-to-html, pdf-to-img, pdf-to-markdown, pdf-to-pdfa, pdf-to-presentation, pdf-to-text, pdf-to-word, pdf-to-xml, url-to-pdf, add-image, add-page-numbers, add-stamp, auto-rename, auto-split-pdf, compress-pdf, decompress-pdf, extract-image-scans, extract-images, flatten, ocr-pdf, remove-blanks, repair, replace-invert-pdf, show-javascript, update-metadata, filter-contains-image, filter-contains-text, filter-file-size, filter-page-count, filter-page-rotation, filter-page-size, add-attachments] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages']) + toRemove: [ebook-to-pdf, crop, merge-pdfs, multi-page-layout, overlay-pdfs, pdf-to-single-page, rearrange-pages, remove-image-pdf, remove-pages, rotate-pdf, scale-pages, split-by-size-or-count, split-pages, split-pdf-by-chapters, split-pdf-by-sections, add-password, add-watermark, auto-redact, cert-sign, get-info-on-pdf, redact, remove-cert-sign, remove-password, sanitize-pdf, validate-signature, file-to-pdf, html-to-pdf, img-to-pdf, markdown-to-pdf, pdf-to-csv, pdf-to-html, pdf-to-img, pdf-to-markdown, pdf-to-pdfa, pdf-to-presentation, pdf-to-text, pdf-to-word, pdf-to-xml, url-to-pdf, add-image, add-page-numbers, add-stamp, auto-rename, auto-split-pdf, compress-pdf, decompress-pdf, extract-image-scans, extract-images, flatten, ocr-pdf, remove-blanks, repair, replace-invert-pdf, show-javascript, update-metadata, filter-contains-image, filter-contains-text, filter-file-size, filter-page-count, filter-page-rotation, filter-page-size, add-attachments] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages']) groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice']) metrics: diff --git a/testing/endpoints.txt b/testing/endpoints.txt index 149e3af3a..1df2e53e6 100644 --- a/testing/endpoints.txt +++ b/testing/endpoints.txt @@ -44,6 +44,7 @@ /api/v1/convert/markdown/pdf /api/v1/convert/img/pdf /api/v1/convert/html/pdf +/api/v1/convert/ebook/pdf /api/v1/convert/file/pdf /api/v1/general/split-pdf-by-sections /api/v1/general/split-pdf-by-chapters diff --git a/testing/webpage_urls_full.txt b/testing/webpage_urls_full.txt index 86b908720..6bba382e1 100644 --- a/testing/webpage_urls_full.txt +++ b/testing/webpage_urls_full.txt @@ -14,6 +14,7 @@ /compare /compress-pdf /crop +/ebook-to-pdf /extract-image-scans /extract-images /extract-page @@ -62,4 +63,4 @@ /stamp /validate-signature /view-pdf -/swagger-ui/index.html \ No newline at end of file +/swagger-ui/index.html