mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
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>
This commit is contained in:
parent
e932ca01f3
commit
19aef5e034
25
Dockerfile
25
Dockerfile
@ -38,11 +38,12 @@ ENV DISABLE_ADDITIONAL_FEATURES=true \
|
|||||||
TEMP=/tmp/stirling-pdf \
|
TEMP=/tmp/stirling-pdf \
|
||||||
TMP=/tmp/stirling-pdf
|
TMP=/tmp/stirling-pdf
|
||||||
|
|
||||||
|
|
||||||
# JDK for app
|
# JDK for app
|
||||||
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
RUN printf '%s\n' \
|
||||||
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
'https://dl-cdn.alpinelinux.org/alpine/edge/main' \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
'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 upgrade --no-cache -a && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
ca-certificates \
|
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)
|
# OCR MY PDF (unpaper for descew and other advanced features)
|
||||||
tesseract-ocr-data-eng \
|
tesseract-ocr-data-eng \
|
||||||
tesseract-ocr-data-chi_sim \
|
tesseract-ocr-data-chi_sim \
|
||||||
tesseract-ocr-data-deu \
|
tesseract-ocr-data-deu \
|
||||||
tesseract-ocr-data-fra \
|
tesseract-ocr-data-fra \
|
||||||
tesseract-ocr-data-por \
|
tesseract-ocr-data-por \
|
||||||
unpaper \
|
unpaper \
|
||||||
# CV
|
# CV / Python
|
||||||
py3-opencv \
|
py3-opencv \
|
||||||
python3 \
|
python3 \
|
||||||
ocrmypdf \
|
ocrmypdf \
|
||||||
py3-pip \
|
py3-pip \
|
||||||
py3-pillow@testing \
|
py3-pillow \
|
||||||
py3-pdf2image@testing \
|
py3-pdf2image \
|
||||||
|
# Calibre
|
||||||
|
calibre \
|
||||||
# URW Base 35 fonts for better PDF rendering
|
# URW Base 35 fonts for better PDF rendering
|
||||||
font-urw-base35 && \
|
font-urw-base35 && \
|
||||||
|
# Calibre fixes
|
||||||
|
apk fix --no-cache calibre && \
|
||||||
python3 -m venv /opt/venv && \
|
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 pip setuptools && \
|
||||||
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
||||||
|
|||||||
@ -25,6 +25,12 @@ RUN apt-get update && apt-get install -y \
|
|||||||
python3-venv \
|
python3-venv \
|
||||||
# ss -tln
|
# ss -tln
|
||||||
iproute2 \
|
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/*
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Setze die Environment Variable für setuptools
|
# 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
|
COPY .github/scripts/requirements_dev.txt /tmp/requirements_dev.txt
|
||||||
RUN python3 -m venv --system-site-packages /opt/venv \
|
RUN python3 -m venv --system-site-packages /opt/venv \
|
||||||
&& . /opt/venv/bin/activate \
|
&& . /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
|
# Füge den venv-Pfad zur globalen PATH-Variable hinzu, damit die Tools verfügbar sind
|
||||||
ENV PATH="/opt/venv/bin:$PATH"
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
|||||||
@ -17,9 +17,9 @@ WORKDIR /app
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build the application with DISABLE_ADDITIONAL_FEATURES=false
|
# Build the application with DISABLE_ADDITIONAL_FEATURES=false
|
||||||
RUN DISABLE_ADDITIONAL_FEATURES=false \
|
ENV DISABLE_ADDITIONAL_FEATURES=false \
|
||||||
STIRLING_PDF_DESKTOP_UI=false \
|
STIRLING_PDF_DESKTOP_UI=false
|
||||||
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
RUN ./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||||
|
|
||||||
# Main stage
|
# Main stage
|
||||||
FROM alpine:3.22.2@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412
|
FROM alpine:3.22.2@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412
|
||||||
@ -52,11 +52,12 @@ ENV DISABLE_ADDITIONAL_FEATURES=true \
|
|||||||
TEMP=/tmp/stirling-pdf \
|
TEMP=/tmp/stirling-pdf \
|
||||||
TMP=/tmp/stirling-pdf
|
TMP=/tmp/stirling-pdf
|
||||||
|
|
||||||
|
|
||||||
# JDK for app
|
# JDK for app
|
||||||
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
RUN printf '%s\n' \
|
||||||
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
'https://dl-cdn.alpinelinux.org/alpine/edge/main' \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
'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 upgrade --no-cache -a && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
ca-certificates \
|
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)
|
# OCR MY PDF (unpaper for descew and other advanced featues)
|
||||||
tesseract-ocr-data-eng \
|
tesseract-ocr-data-eng \
|
||||||
tesseract-ocr-data-chi_sim \
|
tesseract-ocr-data-chi_sim \
|
||||||
tesseract-ocr-data-deu \
|
tesseract-ocr-data-deu \
|
||||||
tesseract-ocr-data-fra \
|
tesseract-ocr-data-fra \
|
||||||
tesseract-ocr-data-por \
|
tesseract-ocr-data-por \
|
||||||
unpaper \
|
unpaper \
|
||||||
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra font-liberation font-linux-libertine font-urw-base35 \
|
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 \
|
py3-opencv \
|
||||||
python3 \
|
python3 \
|
||||||
ocrmypdf \
|
ocrmypdf \
|
||||||
py3-pip \
|
py3-pip \
|
||||||
py3-pillow@testing \
|
py3-pillow \
|
||||||
py3-pdf2image@testing && \
|
py3-pdf2image \
|
||||||
|
# Calibre (musl-native) + QtWebEngine Runtime
|
||||||
|
calibre && \
|
||||||
|
# Calibre fixes
|
||||||
|
apk fix --no-cache calibre && \
|
||||||
python3 -m venv /opt/venv && \
|
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 pip setuptools && \
|
||||||
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
||||||
|
|||||||
@ -24,9 +24,11 @@ COPY scripts/installFonts.sh /scripts/installFonts.sh
|
|||||||
COPY app/core/build/libs/*.jar app.jar
|
COPY app/core/build/libs/*.jar app.jar
|
||||||
|
|
||||||
# Set up necessary directories and permissions
|
# Set up necessary directories and permissions
|
||||||
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
RUN printf '%s\n' \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
'https://dl-cdn.alpinelinux.org/alpine/edge/main' \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
'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 upgrade --no-cache -a && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
|||||||
@ -52,6 +52,7 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
|
|||||||
- **CBZ to PDF**: Convert comic book archives
|
- **CBZ to PDF**: Convert comic book archives
|
||||||
- **CBR to PDF**: Convert comic book rar archives
|
- **CBR to PDF**: Convert comic book rar archives
|
||||||
- **Email to PDF**: Convert email files to PDF
|
- **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
|
- **Vector Image to PDF**: Convert vector images (PS, EPS, EPSF) to PDF format
|
||||||
|
|
||||||
#### Convert from PDF
|
#### Convert from PDF
|
||||||
|
|||||||
@ -21,6 +21,7 @@ public class RuntimePathConfig {
|
|||||||
private final String basePath;
|
private final String basePath;
|
||||||
private final String weasyPrintPath;
|
private final String weasyPrintPath;
|
||||||
private final String unoConvertPath;
|
private final String unoConvertPath;
|
||||||
|
private final String calibrePath;
|
||||||
|
|
||||||
// Pipeline paths
|
// Pipeline paths
|
||||||
private final String pipelineWatchedFoldersPath;
|
private final String pipelineWatchedFoldersPath;
|
||||||
@ -57,6 +58,7 @@ public class RuntimePathConfig {
|
|||||||
// Initialize Operation paths
|
// Initialize Operation paths
|
||||||
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
|
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
|
||||||
String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
|
String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
|
||||||
|
String defaultCalibrePath = isDocker ? "/usr/bin/ebook-convert" : "ebook-convert";
|
||||||
|
|
||||||
Operations operations = properties.getSystem().getCustomPaths().getOperations();
|
Operations operations = properties.getSystem().getCustomPaths().getOperations();
|
||||||
this.weasyPrintPath =
|
this.weasyPrintPath =
|
||||||
@ -67,6 +69,9 @@ public class RuntimePathConfig {
|
|||||||
resolvePath(
|
resolvePath(
|
||||||
defaultUnoConvertPath,
|
defaultUnoConvertPath,
|
||||||
operations != null ? operations.getUnoconvert() : null);
|
operations != null ? operations.getUnoconvert() : null);
|
||||||
|
this.calibrePath =
|
||||||
|
resolvePath(
|
||||||
|
defaultCalibrePath, operations != null ? operations.getCalibre() : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolvePath(String defaultPath, String customPath) {
|
private String resolvePath(String defaultPath, String customPath) {
|
||||||
|
|||||||
@ -371,6 +371,7 @@ public class ApplicationProperties {
|
|||||||
public static class Operations {
|
public static class Operations {
|
||||||
private String weasyprint;
|
private String weasyprint;
|
||||||
private String unoconvert;
|
private String unoconvert;
|
||||||
|
private String calibre;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -257,6 +257,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Convert", "html-to-pdf");
|
addEndpointToGroup("Convert", "html-to-pdf");
|
||||||
addEndpointToGroup("Convert", "url-to-pdf");
|
addEndpointToGroup("Convert", "url-to-pdf");
|
||||||
addEndpointToGroup("Convert", "markdown-to-pdf");
|
addEndpointToGroup("Convert", "markdown-to-pdf");
|
||||||
|
addEndpointToGroup("Convert", "ebook-to-pdf");
|
||||||
addEndpointToGroup("Convert", "pdf-to-csv");
|
addEndpointToGroup("Convert", "pdf-to-csv");
|
||||||
addEndpointToGroup("Convert", "pdf-to-markdown");
|
addEndpointToGroup("Convert", "pdf-to-markdown");
|
||||||
addEndpointToGroup("Convert", "eml-to-pdf");
|
addEndpointToGroup("Convert", "eml-to-pdf");
|
||||||
@ -446,6 +447,9 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Weasyprint", "markdown-to-pdf");
|
addEndpointToGroup("Weasyprint", "markdown-to-pdf");
|
||||||
addEndpointToGroup("Weasyprint", "eml-to-pdf");
|
addEndpointToGroup("Weasyprint", "eml-to-pdf");
|
||||||
|
|
||||||
|
// Calibre dependent endpoints
|
||||||
|
addEndpointToGroup("Calibre", "ebook-to-pdf");
|
||||||
|
|
||||||
// Pdftohtml dependent endpoints
|
// Pdftohtml dependent endpoints
|
||||||
addEndpointToGroup("Pdftohtml", "pdf-to-html");
|
addEndpointToGroup("Pdftohtml", "pdf-to-html");
|
||||||
addEndpointToGroup("Pdftohtml", "pdf-to-markdown");
|
addEndpointToGroup("Pdftohtml", "pdf-to-markdown");
|
||||||
@ -498,6 +502,7 @@ public class EndpointConfiguration {
|
|||||||
|| "Javascript".equals(group)
|
|| "Javascript".equals(group)
|
||||||
|| "Weasyprint".equals(group)
|
|| "Weasyprint".equals(group)
|
||||||
|| "Pdftohtml".equals(group)
|
|| "Pdftohtml".equals(group)
|
||||||
|
|| "Calibre".equals(group)
|
||||||
|| "rar".equals(group)
|
|| "rar".equals(group)
|
||||||
|| "FFmpeg".equals(group);
|
|| "FFmpeg".equals(group);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,7 @@ public class ExternalAppDepConfig {
|
|||||||
|
|
||||||
private final String weasyprintPath;
|
private final String weasyprintPath;
|
||||||
private final String unoconvPath;
|
private final String unoconvPath;
|
||||||
|
private final String calibrePath;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map of command(binary) -> affected groups (e.g. "gs" -> ["Ghostscript"]). Immutable to avoid
|
* Map of command(binary) -> affected groups (e.g. "gs" -> ["Ghostscript"]). Immutable to avoid
|
||||||
@ -56,6 +57,7 @@ public class ExternalAppDepConfig {
|
|||||||
this.endpointConfiguration = endpointConfiguration;
|
this.endpointConfiguration = endpointConfiguration;
|
||||||
this.weasyprintPath = runtimePathConfig.getWeasyPrintPath();
|
this.weasyprintPath = runtimePathConfig.getWeasyPrintPath();
|
||||||
this.unoconvPath = runtimePathConfig.getUnoConvertPath();
|
this.unoconvPath = runtimePathConfig.getUnoConvertPath();
|
||||||
|
this.calibrePath = runtimePathConfig.getCalibrePath();
|
||||||
|
|
||||||
Map<String, List<String>> tmp = new HashMap<>();
|
Map<String, List<String>> tmp = new HashMap<>();
|
||||||
tmp.put("gs", List.of("Ghostscript"));
|
tmp.put("gs", List.of("Ghostscript"));
|
||||||
@ -67,6 +69,7 @@ public class ExternalAppDepConfig {
|
|||||||
tmp.put("qpdf", List.of("qpdf"));
|
tmp.put("qpdf", List.of("qpdf"));
|
||||||
tmp.put("tesseract", List.of("tesseract"));
|
tmp.put("tesseract", List.of("tesseract"));
|
||||||
tmp.put("rar", List.of("rar"));
|
tmp.put("rar", List.of("rar"));
|
||||||
|
tmp.put(calibrePath, List.of("Calibre"));
|
||||||
tmp.put("ffmpeg", List.of("FFmpeg"));
|
tmp.put("ffmpeg", List.of("FFmpeg"));
|
||||||
this.commandToGroupMapping = Collections.unmodifiableMap(tmp);
|
this.commandToGroupMapping = Collections.unmodifiableMap(tmp);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<String> 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<byte[]> 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<String> 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<String> buildCalibreCommand(
|
||||||
|
Path inputPath,
|
||||||
|
Path outputPath,
|
||||||
|
boolean embedAllFonts,
|
||||||
|
boolean includeTableOfContents,
|
||||||
|
boolean includePageNumbers) {
|
||||||
|
List<String> 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<Path> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -47,6 +47,13 @@ public class ConverterWebController {
|
|||||||
return "convert/cbr-to-pdf";
|
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")
|
@GetMapping("/pdf-to-cbr")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String convertPdfToCbrForm(Model model) {
|
public String convertPdfToCbrForm(Model model) {
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
@ -616,6 +616,10 @@ home.cbrToPdf.title=CBR zu PDF
|
|||||||
home.cbrToPdf.desc=CBR-Comicarchive in das PDF-Format konvertieren.
|
home.cbrToPdf.desc=CBR-Comicarchive in das PDF-Format konvertieren.
|
||||||
cbrToPdf.tags=konvertierung,comic,buch,archiv,cbr,rar
|
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.title=PDF zu CBZ
|
||||||
home.pdfToCbz.desc=PDF-Dateien in CBZ-Comicarchive umwandeln.
|
home.pdfToCbz.desc=PDF-Dateien in CBZ-Comicarchive umwandeln.
|
||||||
pdfToCbz.tags=konvertierung,comic,buch,archiv,cbz,pdf
|
pdfToCbz.tags=konvertierung,comic,buch,archiv,cbz,pdf
|
||||||
@ -1490,6 +1494,17 @@ cbrToPDF.submit=Zu PDF konvertieren
|
|||||||
cbrToPDF.selectText=CBR-Datei auswählen
|
cbrToPDF.selectText=CBR-Datei auswählen
|
||||||
cbrToPDF.optimizeForEbook=PDF für E-Book-Reader optimieren (verwendet Ghostscript)
|
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
|
||||||
pdfToCBR.title=PDF zu CBR
|
pdfToCBR.title=PDF zu CBR
|
||||||
pdfToCBR.header=PDF zu CBR
|
pdfToCBR.header=PDF zu CBR
|
||||||
|
|||||||
@ -616,6 +616,10 @@ home.cbrToPdf.title=CBR to PDF
|
|||||||
home.cbrToPdf.desc=Convert CBR comic book archives to PDF format.
|
home.cbrToPdf.desc=Convert CBR comic book archives to PDF format.
|
||||||
cbrToPdf.tags=conversion,comic,book,archive,cbr,rar
|
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.title=PDF to CBZ
|
||||||
home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives.
|
home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives.
|
||||||
pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf
|
pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf
|
||||||
@ -1490,6 +1494,17 @@ cbrToPDF.submit=Convert to PDF
|
|||||||
cbrToPDF.selectText=Select CBR file
|
cbrToPDF.selectText=Select CBR file
|
||||||
cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript)
|
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
|
||||||
pdfToCBR.title=PDF to CBR
|
pdfToCBR.title=PDF to CBR
|
||||||
pdfToCBR.header=PDF to CBR
|
pdfToCBR.header=PDF to CBR
|
||||||
|
|||||||
@ -149,6 +149,7 @@ system:
|
|||||||
operations:
|
operations:
|
||||||
weasyprint: '' # Defaults to /opt/venv/bin/weasyprint
|
weasyprint: '' # Defaults to /opt/venv/bin/weasyprint
|
||||||
unoconvert: '' # Defaults to /opt/venv/bin/unoconvert
|
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".
|
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:
|
tempFileManagement:
|
||||||
baseTmpDir: '' # Defaults to java.io.tmpdir/stirling-pdf
|
baseTmpDir: '' # Defaults to java.io.tmpdir/stirling-pdf
|
||||||
|
|||||||
107
app/core/src/main/resources/templates/convert/ebook-to-pdf.html
Normal file
107
app/core/src/main/resources/templates/convert/ebook-to-pdf.html
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html th:data-language="${#locale.toString()}"
|
||||||
|
th:dir="#{language.direction}"
|
||||||
|
th:lang="${#locale.language}"
|
||||||
|
xmlns:th="https://www.thymeleaf.org">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<th:block th:insert="~{fragments/common :: head(title=#{ebookToPDF.title}, header=#{ebookToPDF.header})}"></th:block>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||||
|
<div id="page-container">
|
||||||
|
<div id="content-wrap">
|
||||||
|
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||||
|
<br><br>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 bg-card">
|
||||||
|
<div class="tool-header">
|
||||||
|
<span class="material-symbols-rounded tool-header-icon convertto">menu_book</span>
|
||||||
|
<span class="tool-header-text"
|
||||||
|
th:text="#{ebookToPDF.header}"></span>
|
||||||
|
</div>
|
||||||
|
<p th:text="#{processTimeWarning}"></p>
|
||||||
|
|
||||||
|
<div class="alert alert-warning"
|
||||||
|
th:if="${!@endpointConfiguration.isGroupEnabled('Calibre')}">
|
||||||
|
<span th:text="#{ebookToPDF.calibreDisabled}">Calibre support is disabled.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form enctype="multipart/form-data"
|
||||||
|
id="ebookToPDFForm"
|
||||||
|
method="post"
|
||||||
|
th:action="@{'/api/v1/convert/ebook/pdf'}"
|
||||||
|
th:if="${@endpointConfiguration.isGroupEnabled('Calibre')}">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.epub,.mobi,.azw3,.fb2,.txt,.docx', inputText=#{ebookToPDF.selectText})}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input class="form-check-input"
|
||||||
|
id="embedAllFonts"
|
||||||
|
name="embedAllFonts"
|
||||||
|
type="checkbox"
|
||||||
|
value="true">
|
||||||
|
<label for="embedAllFonts"
|
||||||
|
th:text="#{ebookToPDF.embedAllFonts}">
|
||||||
|
Embed all fonts in PDF
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input class="form-check-input"
|
||||||
|
id="includeTableOfContents"
|
||||||
|
name="includeTableOfContents"
|
||||||
|
type="checkbox"
|
||||||
|
value="true">
|
||||||
|
<label
|
||||||
|
for="includeTableOfContents"
|
||||||
|
th:text="#{ebookToPDF.includeTableOfContents}">
|
||||||
|
Add table of contents
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input class="form-check-input"
|
||||||
|
id="includePageNumbers"
|
||||||
|
name="includePageNumbers"
|
||||||
|
type="checkbox"
|
||||||
|
value="true">
|
||||||
|
<label
|
||||||
|
for="includePageNumbers"
|
||||||
|
th:text="#{ebookToPDF.includePageNumbers}">
|
||||||
|
Add page numbers
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-3"
|
||||||
|
th:if="${@endpointConfiguration.isGroupEnabled('Ghostscript')}">
|
||||||
|
<input class="form-check-input"
|
||||||
|
id="optimizeForEbook"
|
||||||
|
name="optimizeForEbook"
|
||||||
|
type="checkbox"
|
||||||
|
value="true">
|
||||||
|
<label
|
||||||
|
for="optimizeForEbook"
|
||||||
|
th:text="#{ebookToPDF.optimizeForEbook}">
|
||||||
|
Optimize PDF for ebook readers (uses Ghostscript)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
id="submitBtn"
|
||||||
|
th:text="#{ebookToPDF.submit}"
|
||||||
|
type="submit">Convert to
|
||||||
|
PDF</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -53,6 +53,9 @@
|
|||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry('cbr-to-pdf', 'auto_stories', 'home.cbrToPdf.title', 'home.cbrToPdf.desc', 'cbrToPdf.tags', 'convertto')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry('cbr-to-pdf', 'auto_stories', 'home.cbrToPdf.title', 'home.cbrToPdf.desc', 'cbrToPdf.tags', 'convertto')}">
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/navbarEntry :: navbarEntry('ebook-to-pdf', 'menu_book', 'home.ebookToPdf.title', 'home.ebookToPdf.desc', 'ebookToPdf.tags', 'convertto')}">
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
|
||||||
</div>
|
</div>
|
||||||
@ -132,6 +135,9 @@
|
|||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry('cbr-to-pdf', 'auto_stories', 'home.cbrToPdf.title', 'home.cbrToPdf.desc', 'cbrToPdf.tags', 'convertto')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry('cbr-to-pdf', 'auto_stories', 'home.cbrToPdf.title', 'home.cbrToPdf.desc', 'cbrToPdf.tags', 'convertto')}">
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/navbarEntry :: navbarEntry('ebook-to-pdf', 'menu_book', 'home.ebookToPdf.title', 'home.ebookToPdf.desc', 'ebookToPdf.tags', 'convertto')}">
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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<Path> deletedDir = new AtomicReference<>();
|
||||||
|
Mockito.doAnswer(
|
||||||
|
invocation -> {
|
||||||
|
Path dir = invocation.getArgument(0);
|
||||||
|
deletedDir.set(dir);
|
||||||
|
if (Files.exists(dir)) {
|
||||||
|
try (Stream<Path> 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<ProcessExecutor> pe = Mockito.mockStatic(ProcessExecutor.class);
|
||||||
|
MockedStatic<WebResponseUtils> wr = Mockito.mockStatic(WebResponseUtils.class);
|
||||||
|
MockedStatic<GeneralUtils> 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<List<String>> 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<byte[]> 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<byte[]> response = controller.convertEbookToPdf(request);
|
||||||
|
|
||||||
|
assertSame(expectedResponse, response);
|
||||||
|
|
||||||
|
List<String> 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<Path> 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<Path> deletedDir = new AtomicReference<>();
|
||||||
|
Mockito.doAnswer(
|
||||||
|
invocation -> {
|
||||||
|
Path dir = invocation.getArgument(0);
|
||||||
|
deletedDir.set(dir);
|
||||||
|
if (Files.exists(dir)) {
|
||||||
|
try (Stream<Path> 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<ProcessExecutor> pe = Mockito.mockStatic(ProcessExecutor.class);
|
||||||
|
MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class);
|
||||||
|
MockedStatic<WebResponseUtils> 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<byte[]> expectedResponse = ResponseEntity.ok(optimizedBytes);
|
||||||
|
wr.when(
|
||||||
|
() ->
|
||||||
|
WebResponseUtils.bytesToWebResponse(
|
||||||
|
optimizedBytes, "ebook_convertedToPDF.pdf"))
|
||||||
|
.thenReturn(expectedResponse);
|
||||||
|
|
||||||
|
ResponseEntity<byte[]> 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<Path> paths = Files.walk(workingDir)) {
|
||||||
|
paths.sorted(Comparator.reverseOrder())
|
||||||
|
.forEach(
|
||||||
|
path -> {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(path);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ Stirling-PDF is built using:
|
|||||||
- PDFBox
|
- PDFBox
|
||||||
- LibreOffice
|
- LibreOffice
|
||||||
- qpdf
|
- qpdf
|
||||||
|
- Calibre (`ebook-convert` CLI) for eBook conversions
|
||||||
- HTML, CSS, JavaScript
|
- HTML, CSS, JavaScript
|
||||||
- Docker
|
- Docker
|
||||||
- PDF.js
|
- 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:
|
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.
|
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.
|
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
|
## 4. Project Structure
|
||||||
|
|||||||
@ -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.
|
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
|
||||||
|
|
||||||
endpoints:
|
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'])
|
groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice'])
|
||||||
|
|
||||||
metrics:
|
metrics:
|
||||||
|
|||||||
@ -44,6 +44,7 @@
|
|||||||
/api/v1/convert/markdown/pdf
|
/api/v1/convert/markdown/pdf
|
||||||
/api/v1/convert/img/pdf
|
/api/v1/convert/img/pdf
|
||||||
/api/v1/convert/html/pdf
|
/api/v1/convert/html/pdf
|
||||||
|
/api/v1/convert/ebook/pdf
|
||||||
/api/v1/convert/file/pdf
|
/api/v1/convert/file/pdf
|
||||||
/api/v1/general/split-pdf-by-sections
|
/api/v1/general/split-pdf-by-sections
|
||||||
/api/v1/general/split-pdf-by-chapters
|
/api/v1/general/split-pdf-by-chapters
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
/compare
|
/compare
|
||||||
/compress-pdf
|
/compress-pdf
|
||||||
/crop
|
/crop
|
||||||
|
/ebook-to-pdf
|
||||||
/extract-image-scans
|
/extract-image-scans
|
||||||
/extract-images
|
/extract-images
|
||||||
/extract-page
|
/extract-page
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user