Introduces a new directory containing a small standalone CLI tool () that converts PDFs to Markdown using PyMuPDF and pymupdf4llm. When installed, it replaces the slower pdftohtml-based converter with a significantly faster path. The tool is invoked as an OS subprocess, following the same pattern already used for LibreOffice and Tesseract. A new service handles discovery (PATH probe, cached 30 s), temp file I/O, and process execution via the existing infrastructure. Auto-detected at startup via — no configuration flag required. Falls back transparently to the bundled pdftohtml converter if not found. is baked into the Docker base image via the existing Python venv build stage and symlinked onto PATH, so all Docker users get the faster path automatically with no manual steps. AGPL notice: is licensed under AGPL-3.0-or-later because it depends on PyMuPDF (AGPL-3.0 / Artifex commercial). It is a SEPARATE program from the rest of Stirling PDF and shares no source code with the MIT-licensed core or the proprietary engine. The subprocess boundary ensures the AGPL copyleft attaches to this tool alone and does not extend to Stirling PDF. The LICENSE file and pymupdf-worker/LICENSE document this separation explicitly. Do not replace the subprocess invocation with an in-process import of PyMuPDF.
673 lines
31 KiB
Docker
673 lines
31 KiB
Docker
# Stirling-PDF Base Image
|
|
# Pre-built tools: Calibre, Ghostscript, QPDF, ImageMagick, Python venv with AI/OCR packages
|
|
# This image is reused by Dockerfile and Dockerfile.fat to avoid rebuilding tools on every app release
|
|
|
|
ARG TARGETPLATFORM
|
|
|
|
# Stage 1: Build and strip Calibre
|
|
FROM ubuntu:noble@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b AS calibre-build
|
|
ARG TARGETPLATFORM
|
|
ARG CALIBRE_VERSION=9.4.0
|
|
ARG CALIBRE_STRIP_WEBENGINE=false
|
|
|
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
|
set -eux; \
|
|
apt-get update; \
|
|
apt-get install -y --no-install-recommends \
|
|
ca-certificates curl xz-utils libnss3 libfontconfig1 \
|
|
libgl1 libegl1 libdbus-1-3 libasound2t64 libxcomposite1 \
|
|
libxrandr2 libxkbcommon0 libxi6 libxtst6 libopengl0 \
|
|
poppler-utils; \
|
|
rm -rf /var/lib/apt/lists/*; \
|
|
\
|
|
case "$(uname -m)" in \
|
|
x86_64) CALIBRE_ARCH="x86_64" ;; \
|
|
aarch64) CALIBRE_ARCH="arm64" ;; \
|
|
*) echo "Unsupported arch: $(uname -m)"; exit 1 ;; \
|
|
esac; \
|
|
\
|
|
for attempt in 1 2 3; do \
|
|
echo "Downloading Calibre (attempt $attempt/3)..."; \
|
|
if curl -fsSL --max-time 300 --retry 3 --retry-delay 5 --retry-max-time 900 \
|
|
"https://download.calibre-ebook.com/${CALIBRE_VERSION}/calibre-${CALIBRE_VERSION}-${CALIBRE_ARCH}.txz" \
|
|
-o /tmp/calibre.txz; then \
|
|
break; \
|
|
fi; \
|
|
if [ $attempt -lt 3 ]; then sleep 10; fi; \
|
|
done; \
|
|
mkdir -p /opt/calibre; \
|
|
tar xJf /tmp/calibre.txz -C /opt/calibre; \
|
|
rm /tmp/calibre.txz; \
|
|
\
|
|
# We only need Qt6 WebEngine (Chromium) for ebook->PDF output.
|
|
# PDF INPUT now uses the pdftohtml engine (poppler), not Qt.
|
|
rm -f /opt/calibre/lib/libQt6Designer* \
|
|
/opt/calibre/lib/libQt6Multimedia* \
|
|
/opt/calibre/lib/libQt6SpatialAudio.so.* \
|
|
/opt/calibre/lib/libQt6NetworkAuth.so.* \
|
|
/opt/calibre/lib/libQt6Concurrent.so.* \
|
|
/opt/calibre/lib/libQt6OpenGLWidgets.so.* \
|
|
/opt/calibre/lib/libQt6QuickWidgets.so.* \
|
|
/opt/calibre/lib/libQt6Svg.so.* \
|
|
/opt/calibre/lib/libQt6SvgWidgets.so.* \
|
|
/opt/calibre/lib/libQt6Pdf*.so.* \
|
|
/opt/calibre/lib/libQt6ShaderTools.so.* \
|
|
/opt/calibre/lib/libQt6SerialPort.so.* \
|
|
/opt/calibre/lib/libQt6Sensors.so.* \
|
|
/opt/calibre/lib/libQt6Test.so.* \
|
|
/opt/calibre/lib/libQt6Sql.so.* \
|
|
/opt/calibre/lib/libQt6RemoteObjects.so.* \
|
|
/opt/calibre/lib/libQt6Help.so.* \
|
|
/opt/calibre/lib/libQt6VirtualKeyboard.so.* \
|
|
/opt/calibre/lib/libQt6WaylandClient.so.* \
|
|
/opt/calibre/lib/libQt6WaylandCompositor.so.* \
|
|
/opt/calibre/lib/libQt6Bluetooth.so.* \
|
|
/opt/calibre/lib/libQt6Nfc.so.* \
|
|
/opt/calibre/lib/libQt6Charts.so.* \
|
|
/opt/calibre/lib/libQt6DataVisualization.so.* \
|
|
/opt/calibre/lib/libQt6Scxml.so.* \
|
|
/opt/calibre/lib/libQt6StateMachine.so.* \
|
|
/opt/calibre/lib/libQt6TextToSpeech.so.* \
|
|
/opt/calibre/lib/libQt63D*.so.* \
|
|
/opt/calibre/lib/libavcodec.so.* \
|
|
/opt/calibre/lib/libavfilter.so.* \
|
|
/opt/calibre/lib/libavformat.so.* \
|
|
/opt/calibre/lib/libavutil.so.* \
|
|
/opt/calibre/lib/libavdevice.so.* \
|
|
/opt/calibre/lib/libpostproc.so.* \
|
|
/opt/calibre/lib/libswresample.so.* \
|
|
/opt/calibre/lib/libswscale.so.* \
|
|
/opt/calibre/lib/libspeex.so.* \
|
|
/opt/calibre/lib/libFLAC.so.* \
|
|
/opt/calibre/lib/libopus.so.* \
|
|
/opt/calibre/lib/libvorbis*.so.* \
|
|
/opt/calibre/lib/libasyncns.so.* \
|
|
/opt/calibre/lib/libspeechd.so.* \
|
|
/opt/calibre/lib/libespeak-ng.so.* \
|
|
/opt/calibre/lib/libonnxruntime.so.* \
|
|
/opt/calibre/lib/libgio-2.0.so.* \
|
|
/opt/calibre/lib/libzstd.so.* \
|
|
/opt/calibre/lib/libhunspell-1.7.so.* \
|
|
/opt/calibre/lib/libbrotlienc.so.* \
|
|
/opt/calibre/lib/libbrotlicommon.so.* \
|
|
/opt/calibre/lib/libbrotlidec.so.* \
|
|
/opt/calibre/lib/libstemmer.so.* \
|
|
/opt/calibre/lib/libmtp.so.* \
|
|
/opt/calibre/lib/libncursesw.so.* \
|
|
/opt/calibre/lib/libchm.so.* \
|
|
/opt/calibre/lib/libgcrypt.so.* \
|
|
/opt/calibre/lib/libgpg-error.so.* \
|
|
/opt/calibre/lib/libicuio.so.* \
|
|
/opt/calibre/lib/libreadline.so.* \
|
|
/opt/calibre/lib/libusb-1.0.so.* \
|
|
/opt/calibre/lib/libpulse*.so.* \
|
|
/opt/calibre/lib/libsndfile.so.* \
|
|
/opt/calibre/lib/libmpv.so.* \
|
|
/opt/calibre/lib/libass.so.* \
|
|
/opt/calibre/lib/librubberband.so.* \
|
|
/opt/calibre/lib/libsamplerate.so.*; \
|
|
rm -rf /opt/calibre/lib/qt6/plugins/platformthemes \
|
|
/opt/calibre/lib/qt6/plugins/multimedia \
|
|
/opt/calibre/lib/qt6/plugins/designer \
|
|
/opt/calibre/lib/qt6/plugins/virtualkeyboard \
|
|
/opt/calibre/lib/qt6/plugins/wayland* \
|
|
/opt/calibre/lib/qt6/plugins/texttospeech \
|
|
/opt/calibre/lib/qt6/plugins/position \
|
|
/opt/calibre/lib/qt6/plugins/sensors \
|
|
/opt/calibre/lib/qt6/plugins/sqldrivers \
|
|
/opt/calibre/lib/qt6/plugins/canbus \
|
|
/opt/calibre/lib/qt6/plugins/sceneparsers \
|
|
/opt/calibre/lib/qt6/plugins/renderers \
|
|
/opt/calibre/lib/qt6/plugins/geometryloaders \
|
|
/opt/calibre/lib/qt6/plugins/generic \
|
|
/opt/calibre/lib/qt6/plugins/qmltooling \
|
|
/opt/calibre/lib/qt6/libexec/QtWebEngineProcess.bak; \
|
|
rm -rf /opt/calibre/plugins/sqldrivers \
|
|
/opt/calibre/plugins/multimedia \
|
|
/opt/calibre/plugins/wayland-shell-integration \
|
|
/opt/calibre/plugins/wayland-graphics-integration-client \
|
|
/opt/calibre/plugins/wayland-decoration-client \
|
|
/opt/calibre/plugins/texttospeech \
|
|
/opt/calibre/plugins/platformthemes \
|
|
/opt/calibre/plugins/platforminputcontexts \
|
|
/opt/calibre/plugins/egldeviceintegrations \
|
|
/opt/calibre/plugins/iconengines; \
|
|
\
|
|
# Remove GUI executables but keep ebook-convert, ebook-meta, and calibre-parallel.
|
|
rm -f /opt/calibre/calibre \
|
|
/opt/calibre/calibre-server \
|
|
/opt/calibre/calibre-smtp \
|
|
/opt/calibre/calibre-debug \
|
|
/opt/calibre/calibre-customize \
|
|
/opt/calibre/calibredb \
|
|
/opt/calibre/ebook-viewer \
|
|
/opt/calibre/ebook-edit \
|
|
/opt/calibre/ebook-polish \
|
|
/opt/calibre/ebook-device \
|
|
/opt/calibre/fetch-ebook-metadata \
|
|
/opt/calibre/lrf2lrs \
|
|
/opt/calibre/lrs2lrf \
|
|
/opt/calibre/markdown-calibre \
|
|
/opt/calibre/web2disk; \
|
|
\
|
|
# Remove Python modules not needed for conversion.
|
|
rm -rf /opt/calibre/lib/calibre/gui2 \
|
|
/opt/calibre/lib/calibre/devices \
|
|
/opt/calibre/lib/calibre/library \
|
|
/opt/calibre/lib/calibre/db \
|
|
/opt/calibre/lib/calibre/srv \
|
|
/opt/calibre/lib/calibre/spell \
|
|
/opt/calibre/lib/calibre/live \
|
|
/opt/calibre/lib/calibre/utils/piper \
|
|
/opt/calibre/lib/calibre/utils/certgen.so \
|
|
/opt/calibre/lib/calibre/utils/https \
|
|
/opt/calibre/lib/calibre/utils/mdns; \
|
|
\
|
|
# Remove resources not needed for CLI conversion.
|
|
rm -rf /opt/calibre/resources/images \
|
|
/opt/calibre/resources/icons \
|
|
/opt/calibre/resources/icons.rcc \
|
|
/opt/calibre/resources/content-server \
|
|
/opt/calibre/resources/editor* \
|
|
/opt/calibre/resources/viewer \
|
|
/opt/calibre/resources/viewer.js \
|
|
/opt/calibre/resources/viewer.html \
|
|
/opt/calibre/resources/recipes \
|
|
/opt/calibre/resources/dictionaries \
|
|
/opt/calibre/resources/hyphenation \
|
|
/opt/calibre/resources/catalog \
|
|
/opt/calibre/resources/calibre-mimetypes.xml \
|
|
/opt/calibre/resources/changelog.json \
|
|
/opt/calibre/resources/user-agent-data.json \
|
|
/opt/calibre/resources/builtin_recipes.zip \
|
|
/opt/calibre/resources/builtin_recipes.xml \
|
|
/opt/calibre/resources/stylelint-bundle.min.js \
|
|
/opt/calibre/resources/stylelint.js \
|
|
/opt/calibre/resources/rapydscript \
|
|
/opt/calibre/resources/quick_start \
|
|
/opt/calibre/resources/piper-voices.json \
|
|
/opt/calibre/resources/images.qrc \
|
|
/opt/calibre/resources/mozilla-ca-certs.pem \
|
|
/opt/calibre/resources/ebook-convert-complete.calibre_msgpack \
|
|
/opt/calibre/resources/mathjax \
|
|
/opt/calibre/resources/common-english-words.txt \
|
|
/opt/calibre/resources/calibre-portable.sh \
|
|
/opt/calibre/resources/calibre-portable.bat \
|
|
/opt/calibre/resources/metadata_sqlite.sql \
|
|
/opt/calibre/resources/notes_sqlite.sql \
|
|
/opt/calibre/resources/fts_sqlite.sql \
|
|
/opt/calibre/resources/fts_triggers.sql \
|
|
/opt/calibre/resources/jacket \
|
|
/opt/calibre/resources/editor-functions.json \
|
|
/opt/calibre/resources/calibre-ebook-root-CA.crt \
|
|
/opt/calibre/resources/csscolorparser.js \
|
|
/opt/calibre/resources/lookup.js \
|
|
/opt/calibre/resources/pdf-mathjax-loader.js \
|
|
/opt/calibre/resources/scraper.js \
|
|
/opt/calibre/resources/toc.js \
|
|
/opt/calibre/resources/user-manual-translation-stats.json \
|
|
/opt/calibre/resources/pin-template.svg \
|
|
/opt/calibre/resources/scripts.calibre_msgpack \
|
|
/opt/calibre/resources/fonts \
|
|
/opt/calibre/resources/qtwebengine_devtools_resources.pak \
|
|
/opt/calibre/lib/calibre/ebooks/docx/images \
|
|
/opt/calibre/share \
|
|
/opt/calibre/man; \
|
|
\
|
|
# Remove translations and localization while keeping required libraries.
|
|
# Keep iso639.calibre_msgpack (required)
|
|
# Keep only en-US.pak from qtwebengine_locales (required for WebEngine)
|
|
rm -rf /opt/calibre/lib/qt6/translations; \
|
|
find /opt/calibre/translations -mindepth 1 -maxdepth 1 ! -name 'qtwebengine_locales' -exec rm -rf {} +; \
|
|
find /opt/calibre/translations/qtwebengine_locales -type f ! -name 'en-US.pak' -delete 2>/dev/null || true; \
|
|
if [ -d /opt/calibre/resources/localization ]; then \
|
|
rm -rf /opt/calibre/resources/localization/locales.zip \
|
|
/opt/calibre/resources/localization/stats.calibre_msgpack \
|
|
/opt/calibre/resources/localization/website-languages.txt; \
|
|
find /opt/calibre/resources/localization -mindepth 1 -maxdepth 1 ! -name 'iso639.calibre_msgpack' -exec rm -rf {} +; \
|
|
fi; \
|
|
\
|
|
# Strip debug symbols from calibre extension modules.
|
|
# Exclude Qt6 libs: libQt6WebEngineCore and friends embed Chromium V8 JIT code
|
|
# and internal resource blobs that strip corrupts, causing segfaults at render time.
|
|
find /opt/calibre/lib -name '*.so*' \
|
|
! -name 'libQt6*' \
|
|
-exec strip --strip-unneeded {} + 2>/dev/null || true; \
|
|
\
|
|
# Remove Python bytecode caches.
|
|
find /opt/calibre -type d -name __pycache__ \
|
|
-exec rm -rf {} + 2>/dev/null || true; \
|
|
find /opt/calibre -name '*.pyc' -delete 2>/dev/null || true; \
|
|
\
|
|
# Verify conversion still works
|
|
# NOTE: txt->epub used intentionally NOT txt->pdf.
|
|
# Calibre 7+ uses WebEngine (Chromium) for PDF output, which requires kernel
|
|
# capabilities unavailable in Docker RUN steps and segfaults under QEMU.
|
|
# epub output exercises the same Python/plugin stack without touching WebEngine.
|
|
/opt/calibre/ebook-convert --version; \
|
|
echo "Hello" > /tmp/test.txt; \
|
|
/opt/calibre/ebook-convert /tmp/test.txt /tmp/test.epub; \
|
|
rm -f /tmp/test.txt /tmp/test.epub; \
|
|
\
|
|
# Verify pdftohtml (poppler) is available for the pdftohtml PDF engine.
|
|
pdftohtml -v >/dev/null 2>&1 && echo "pdftohtml OK" || { echo "ERROR: pdftohtml not found"; exit 1; }; \
|
|
echo "=== Calibre stripped successfully ==="
|
|
|
|
# Optional: strip Chromium/WebEngine (~80 MB savings) when PDF output via Calibre is not needed.
|
|
# Build with --build-arg CALIBRE_STRIP_WEBENGINE=true to enable.
|
|
RUN if [ "${CALIBRE_STRIP_WEBENGINE}" = "true" ]; then \
|
|
echo "Stripping Calibre WebEngine (Chromium), PDF output via Calibre will be disabled"; \
|
|
rm -rf /opt/calibre/lib/qt6/libexec/QtWebEngineProcess \
|
|
/opt/calibre/lib/qt6/resources \
|
|
/opt/calibre/lib/libQt6WebEngine*.so.* \
|
|
/opt/calibre/lib/libQt6Quick*.so.* \
|
|
/opt/calibre/lib/libQt6Qml*.so.* \
|
|
/opt/calibre/translations/qtwebengine_locales 2>/dev/null || true; \
|
|
echo "WebEngine stripped, Calibre PDF output disabled"; \
|
|
else \
|
|
echo "CALIBRE_STRIP_WEBENGINE=false, keeping WebEngine for PDF output"; \
|
|
fi
|
|
|
|
|
|
# Stage 2: Build Ghostscript from source
|
|
FROM ubuntu:noble@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b AS gs-build
|
|
ARG TARGETPLATFORM
|
|
ARG GS_VERSION=10.06.0
|
|
|
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
|
--mount=type=cache,target=/tmp/gs-build,id=gs-build-${TARGETPLATFORM:-local} \
|
|
apt-get update && apt-get install -y --no-install-recommends \
|
|
build-essential curl ca-certificates libfontconfig1-dev && rm -rf /var/lib/apt/lists/* && \
|
|
GS_TAG="gs$(printf '%s' "${GS_VERSION}" | tr -d '.')" && \
|
|
cd /tmp/gs-build && \
|
|
rm -rf ghostscript-* && \
|
|
(test -d "ghostscript-${GS_VERSION}" || curl -fsSL --max-time 300 --retry 3 --retry-delay 5 "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/${GS_TAG}/ghostscript-${GS_VERSION}.tar.gz" | tar xz) && \
|
|
cd "ghostscript-${GS_VERSION}" && \
|
|
./configure \
|
|
--prefix=/usr/local \
|
|
--without-x \
|
|
--disable-cups \
|
|
--disable-gtk && \
|
|
make -j"$(nproc)" && \
|
|
make install && \
|
|
cd ..
|
|
|
|
|
|
# Stage 3: Build PDF Tools (QPDF and ImageMagick 7)
|
|
FROM ubuntu:noble@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b AS pdf-tools-build
|
|
ARG TARGETPLATFORM
|
|
ARG QPDF_VERSION=12.3.2
|
|
ARG IM_VERSION=7.1.2-13
|
|
|
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
|
--mount=type=cache,target=/tmp/pdf-tools-build,id=pdf-tools-${TARGETPLATFORM:-local} \
|
|
apt-get update && apt-get install -y --no-install-recommends \
|
|
build-essential cmake libssl-dev libjpeg-dev zlib1g-dev curl ca-certificates pkg-config \
|
|
libpng-dev libtiff-dev libwebp-dev libxml2-dev libfreetype6-dev liblcms2-dev libzip-dev liblqr-1-0-dev \
|
|
libltdl-dev libtool && rm -rf /var/lib/apt/lists/* && \
|
|
cd /tmp/pdf-tools-build && \
|
|
rm -rf qpdf-* ImageMagick-* && \
|
|
# Build QPDF
|
|
(test -d "qpdf-${QPDF_VERSION}" || curl -fsSL --max-time 300 --retry 3 --retry-delay 5 "https://github.com/qpdf/qpdf/releases/download/v${QPDF_VERSION}/qpdf-${QPDF_VERSION}.tar.gz" | tar xz) && \
|
|
cd "qpdf-${QPDF_VERSION}" && \
|
|
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DALLOW_CRYPTO_OPENSSL=ON -DDEFAULT_CRYPTO=openssl && \
|
|
cmake --build build --parallel "$(nproc)" && \
|
|
cmake --install build --strip && \
|
|
cd .. && \
|
|
# Build ImageMagick 7
|
|
(test -d "ImageMagick-${IM_VERSION}" || curl -fsSL --max-time 300 --retry 3 --retry-delay 5 "https://github.com/ImageMagick/ImageMagick/archive/refs/tags/${IM_VERSION}.tar.gz" | tar xz) && \
|
|
cd "ImageMagick-${IM_VERSION}" && \
|
|
./configure --prefix=/usr/local --with-modules --with-perl=no --with-magick-plus-plus=no --with-quantum-depth=16 --disable-static --enable-shared && \
|
|
make -j"$(nproc)" && \
|
|
make install-strip && \
|
|
# Enable PDF/PS/EPS in policy
|
|
sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /usr/local/etc/ImageMagick-7/policy.xml && \
|
|
sed -i 's/rights="none" pattern="PS"/rights="read|write" pattern="PS"/' /usr/local/etc/ImageMagick-7/policy.xml && \
|
|
sed -i 's/rights="none" pattern="EPS"/rights="read|write" pattern="EPS"/' /usr/local/etc/ImageMagick-7/policy.xml && \
|
|
cd .. && \
|
|
ldconfig /usr/local/lib
|
|
|
|
# Stage ImageMagick outputs into a single directory so runtime can import them with one COPY
|
|
# (reduces 4 separate COPY layers to 1 independent --link layer).
|
|
RUN mkdir -p /magick-export/usr/bin \
|
|
/magick-export/usr/local/lib \
|
|
/magick-export/usr/local/etc && \
|
|
cp /usr/local/bin/magick /magick-export/usr/bin/ && \
|
|
cp -a /usr/local/lib/libMagick*.so* /magick-export/usr/local/lib/ && \
|
|
cp -a /usr/local/lib/ImageMagick-7* /magick-export/usr/local/lib/ && \
|
|
cp -a /usr/local/etc/ImageMagick-7 /magick-export/usr/local/etc/
|
|
|
|
|
|
# Stage 4: Build Python venv
|
|
FROM ubuntu:noble@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b AS python-venv-build
|
|
ARG TARGETPLATFORM
|
|
ARG UNOSERVER_VERSION=3.6
|
|
|
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
|
apt-get update && apt-get install -y --no-install-recommends \
|
|
python3 python3-venv ca-certificates binutils && \
|
|
rm -rf /var/lib/apt/lists/*
|
|
|
|
# Copy the AGPL-3.0 pymupdf-convert CLI source so it can be installed into the venv.
|
|
# This is a separate program; installing it here does not affect the license of the
|
|
# rest of the image. See pymupdf-worker/LICENSE.
|
|
COPY pymupdf-worker/ /tmp/pymupdf-worker/
|
|
|
|
RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
|
|
python3 -m venv /opt/venv --system-site-packages && \
|
|
/opt/venv/bin/pip install --no-cache-dir --prefer-binary --only-binary=:all: \
|
|
weasyprint pdf2image opencv-python-headless ocrmypdf \
|
|
cryptography \
|
|
"unoserver==${UNOSERVER_VERSION}" \
|
|
pymupdf pymupdf4llm && \
|
|
# Install the pymupdf-convert CLI entry point (AGPL-3.0 separate program)
|
|
/opt/venv/bin/pip install --no-cache-dir /tmp/pymupdf-worker && \
|
|
rm -rf /tmp/pymupdf-worker && \
|
|
find /opt/venv -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true && \
|
|
find /opt/venv \( -name '*.pyc' -o -name '*.pyi' \) -delete 2>/dev/null || true && \
|
|
rm -rf /opt/venv/lib/python*/site-packages/pip \
|
|
/opt/venv/lib/python*/site-packages/pip-*.dist-info \
|
|
/opt/venv/lib/python*/site-packages/setuptools \
|
|
/opt/venv/lib/python*/site-packages/setuptools-*.dist-info && \
|
|
find /opt/venv -name '*.so' -exec strip --strip-unneeded {} + 2>/dev/null || true
|
|
|
|
|
|
# Final runtime image - the actual base image
|
|
FROM eclipse-temurin:25-jre-noble@sha256:b27ca47660a8fa837e47a8533b9b1a3a430295cf29ca28d91af4fd121572dc29 AS runtime
|
|
|
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
|
|
ENV DEBIAN_FRONTEND=noninteractive \
|
|
LANG=C.UTF-8 \
|
|
LC_ALL=C.UTF-8 \
|
|
TESS_BASE_PATH=/usr/share/tesseract-ocr/5/tessdata
|
|
|
|
ARG UNOSERVER_VERSION=3.6
|
|
|
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
|
set -eux; \
|
|
apt-get update; \
|
|
# Add LibreOffice Fresh PPA for latest version (26.2.x)
|
|
apt-get install -y --no-install-recommends software-properties-common; \
|
|
add-apt-repository -y ppa:libreoffice/ppa; \
|
|
apt-get update; \
|
|
apt-get install -y --no-install-recommends \
|
|
# Core tools
|
|
ca-certificates tzdata tini bash fontconfig curl \
|
|
# ffmpeg disabled due to raised CVEs
|
|
# ffmpeg \
|
|
poppler-utils fontforge \
|
|
unpaper pngquant \
|
|
# Fonts: full coverage for standard + fat variants
|
|
fonts-dejavu \
|
|
fonts-liberation2 \
|
|
fonts-crosextra-caladea fonts-crosextra-carlito \
|
|
fonts-noto-core fonts-noto-mono fonts-noto-extra \
|
|
fonts-noto-cjk poppler-data \
|
|
fonts-freefont-ttf fonts-terminus \
|
|
# Python runtime & UNO bridge
|
|
python3 python3-uno python3-packaging \
|
|
# OCR
|
|
tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu tesseract-ocr-fra \
|
|
tesseract-ocr-por tesseract-ocr-chi-sim \
|
|
# Tesseract OSD for orientation detection
|
|
tesseract-ocr-osd \
|
|
# Graphics / AWT headless
|
|
libcairo2 libpango-1.0-0 libpangoft2-1.0-0 libgdk-pixbuf-2.0-0 \
|
|
libfreetype6 libfontconfig1 libx11-6 libxt6 libxext6 libxrender1 \
|
|
libxtst6 libxi6 libxinerama1 libxkbcommon0 libsm6 libice6 \
|
|
# Qt/EGL for Calibre CLI
|
|
libegl1 libgl1 libopengl0 libdbus-1-3 libglib2.0-0 libnss3 \
|
|
libasound2t64 libxcomposite1 libxrandr2 \
|
|
# Virtual framebuffer (required for headless LibreOffice Impress/Draw)
|
|
xvfb x11-utils coreutils \
|
|
libreoffice-writer-nogui libreoffice-calc-nogui \
|
|
libreoffice-impress-nogui libreoffice-draw-nogui \
|
|
libreoffice-base-nogui libreoffice-java-common \
|
|
; \
|
|
\
|
|
# Verify and fix LibreOffice
|
|
libreoffice --version; \
|
|
soffice --version 2>/dev/null || true; \
|
|
# Rebuild UNO bridge type database
|
|
/usr/lib/libreoffice/program/soffice.bin --headless --convert-to pdf /dev/null 2>/dev/null || true; \
|
|
# Force font cache rebuild
|
|
fc-cache -f -v 2>&1 | awk 'NR <= 20'; \
|
|
\
|
|
# Cleanup stage.
|
|
\
|
|
# Remove PPA helper, no longer needed after apt-get update
|
|
apt-get remove --purge -y software-properties-common || true; \
|
|
apt-get autoremove --purge -y || true; \
|
|
rm -rf /var/lib/apt/lists/*; \
|
|
\
|
|
# Docs / man / info / icons / themes / GUI assets (headless server)
|
|
rm -rf /usr/share/doc/* /usr/share/man/* /usr/share/info/* \
|
|
/usr/share/lintian/* /usr/share/linda/* \
|
|
/usr/share/icons/* /usr/share/themes/* \
|
|
/usr/share/javascript/* \
|
|
/usr/share/gtk-3.0/* \
|
|
/usr/share/fontforge/pixmaps \
|
|
/usr/share/fontforge/osx \
|
|
/usr/share/fontforge/cidmap \
|
|
/usr/share/fontforge/prefs \
|
|
/usr/share/liblangtag/* \
|
|
/usr/share/tcltk/* \
|
|
/usr/share/python-wheels/* \
|
|
/usr/share/glib-2.0/schemas/* \
|
|
/usr/share/mime/* \
|
|
/usr/share/xml/iso-codes \
|
|
/usr/share/GConf \
|
|
/usr/share/bash-completion \
|
|
/usr/share/zsh \
|
|
/usr/share/libmysofa \
|
|
/usr/share/alsa \
|
|
/usr/share/iso-codes \
|
|
/usr/share/perl5 \
|
|
/usr/share/libthai \
|
|
/usr/share/libexttextcat \
|
|
/usr/share/openal \
|
|
/usr/share/gcc; \
|
|
\
|
|
find /usr/share/locale -mindepth 1 -maxdepth 1 -type d \
|
|
! -name 'en*' -exec rm -rf {} + 2>/dev/null || true; \
|
|
rm -rf /usr/share/i18n/locales /usr/share/i18n/charmaps; \
|
|
\
|
|
rm -rf /usr/lib/libreoffice/share/gallery \
|
|
/usr/lib/libreoffice/share/template \
|
|
/usr/lib/libreoffice/share/wizards \
|
|
/usr/lib/libreoffice/share/autotext \
|
|
/usr/lib/libreoffice/help \
|
|
/usr/lib/libreoffice/share/config/images_*.zip \
|
|
/usr/lib/libreoffice/share/basic \
|
|
/usr/lib/libreoffice/share/Scripts \
|
|
/usr/lib/libreoffice/share/autocorr \
|
|
/usr/lib/libreoffice/share/classification \
|
|
/usr/lib/libreoffice/share/wordbook \
|
|
/usr/lib/libreoffice/share/fingerprint \
|
|
/usr/lib/libreoffice/share/xdg \
|
|
/usr/lib/libreoffice/share/numbertext \
|
|
/usr/lib/libreoffice/share/shell \
|
|
/usr/lib/libreoffice/share/palette \
|
|
/usr/lib/libreoffice/share/theme_definitions \
|
|
/usr/lib/libreoffice/share/xslt \
|
|
/usr/lib/libreoffice/share/labels \
|
|
/usr/lib/libreoffice/share/dtd \
|
|
/usr/lib/libreoffice/share/tipoftheday \
|
|
/usr/lib/libreoffice/share/toolbarmode \
|
|
/usr/lib/libreoffice/share/psprint \
|
|
/usr/lib/libreoffice/CREDITS.fodt \
|
|
/usr/lib/libreoffice/LICENSE.html; \
|
|
\
|
|
# Remove unused LO extensions (GUI-only; not needed for document conversion)
|
|
rm -rf /usr/lib/libreoffice/share/extensions/wiki-publisher \
|
|
/usr/lib/libreoffice/share/extensions/nlpsolver \
|
|
/usr/lib/libreoffice/share/extensions/dict-* 2>/dev/null || true; \
|
|
# Remove LO database components (LO Base; not needed for Writer/Calc/Impress conversion)
|
|
rm -rf /usr/lib/libreoffice/program/libdba* \
|
|
/usr/lib/libreoffice/program/libdbahsql* \
|
|
/usr/lib/libreoffice/program/libdbu* \
|
|
/usr/lib/libreoffice/program/libreport* 2>/dev/null || true; \
|
|
\
|
|
rm -rf /usr/lib/python3.12/test \
|
|
/usr/lib/python3.12/idlelib \
|
|
/usr/lib/python3.12/tkinter \
|
|
/usr/lib/python3.12/lib2to3 \
|
|
/usr/lib/python3.12/pydoc_data; \
|
|
\
|
|
rm -rf /usr/lib/python3/dist-packages/scipy \
|
|
/usr/lib/python3/dist-packages/sympy \
|
|
/usr/lib/python3/dist-packages/mpmath; \
|
|
\
|
|
rm -rf \
|
|
/usr/lib/python3/dist-packages/cffi \
|
|
/usr/lib/python3/dist-packages/cffi-*.dist-info \
|
|
/usr/lib/python3/dist-packages/_cffi_backend*.so \
|
|
/usr/lib/python3/dist-packages/_cffi_backend*.cpython*.so \
|
|
2>/dev/null || true; \
|
|
\
|
|
# Strip debug symbols from ALL shared libraries
|
|
find /usr/lib -name '*.so*' -type f \
|
|
-not -path '*/jvm/*' \
|
|
-not -path '*/libreoffice/*' \
|
|
-exec strip --strip-unneeded {} + 2>/dev/null || true; \
|
|
\
|
|
# Remove GPU backends not needed for headless operation.
|
|
MULTIARCH_LIBDIR=$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null \
|
|
|| find /usr/lib -maxdepth 1 -type d -name '*-linux-gnu' | head -1); \
|
|
rm -f \
|
|
"${MULTIARCH_LIBDIR}"/libLLVM*.so* \
|
|
"${MULTIARCH_LIBDIR}"/libgallium*.so* \
|
|
2>/dev/null || true; \
|
|
\
|
|
find /usr/lib/python3* -type d -name __pycache__ \
|
|
-exec rm -rf {} + 2>/dev/null || true; \
|
|
find /usr/lib/python3* \( -name '*.pyc' -o -name '*.pyi' \) \
|
|
-delete 2>/dev/null || true; \
|
|
\
|
|
rm -rf /usr/share/bug /usr/share/lintian /usr/share/linda \
|
|
/var/lib/dpkg/info/*.list \
|
|
/var/lib/dpkg/info/*.md5sums \
|
|
/var/lib/dpkg/info/*.conffiles \
|
|
/var/lib/dpkg/info/*.postinst \
|
|
/var/lib/dpkg/info/*.preinst \
|
|
/var/lib/dpkg/info/*.prerm \
|
|
/var/lib/dpkg/info/*.postrm \
|
|
/var/lib/dpkg/info/*.triggers \
|
|
/var/lib/dpkg/info/*.shlibs \
|
|
/var/lib/dpkg/info/*.symbols \
|
|
/var/lib/dpkg/info/*.templates \
|
|
/var/log/dpkg.log /var/log/apt/* \
|
|
/usr/local/share/ghostscript/*/doc \
|
|
/usr/local/share/ghostscript/*/examples \
|
|
/usr/share/ImageMagick-*/doc \
|
|
/usr/share/ImageMagick-*/www; \
|
|
\
|
|
# Tesseract training configs (not needed for OCR, but keep configs/ for hocr/txt output)
|
|
rm -rf /usr/share/tesseract-ocr/*/tessdata/tessconfigs; \
|
|
\
|
|
# Noto fonts: keep Regular weight only (~370MB savings)
|
|
find /usr/share/fonts/truetype/noto -type f \
|
|
! -name '*Regular*' -delete 2>/dev/null || true; \
|
|
find /usr/share/fonts/opentype -type f \
|
|
! -name '*Regular*' -delete 2>/dev/null || true; \
|
|
# DejaVu: keep Regular and Bold only
|
|
find /usr/share/fonts/truetype -name '*DejaVu*' \
|
|
! -name '*-Regular*' ! -name '*-Bold*' ! -name '*Bold.ttf' \
|
|
! -name 'DejaVuSans.ttf' ! -name 'DejaVuSerif.ttf' ! -name 'DejaVuSansMono.ttf' \
|
|
-type f -delete 2>/dev/null || true; \
|
|
# Remove empty font directories after cleanup
|
|
find /usr/share/fonts -type d -empty -delete 2>/dev/null || true; \
|
|
\
|
|
# gconv: keep only essential charset conversion modules
|
|
GCONV_DIR=$(find /usr/lib -type d -name gconv 2>/dev/null | head -1); \
|
|
if [ -n "$GCONV_DIR" ] && [ -d "$GCONV_DIR" ]; then \
|
|
mkdir -p /tmp/gconv-keep; \
|
|
for mod in UTF-8.so UTF-16.so UTF-32.so UTF-7.so \
|
|
ISO8859-1.so ISO8859-15.so ISO8859-2.so ISO8859-9.so \
|
|
UNICODE.so CP1252.so CP1251.so CP1250.so \
|
|
EUC-JP.so EUC-KR.so EUC-CN.so \
|
|
SHIFT_JIS.so GB18030.so BIG5.so \
|
|
gconv-modules gconv-modules.d gconv-modules.cache; do \
|
|
[ -e "$GCONV_DIR/$mod" ] && \
|
|
cp -a "$GCONV_DIR/$mod" /tmp/gconv-keep/ 2>/dev/null || true; \
|
|
done; \
|
|
rm -rf "$GCONV_DIR"/*; \
|
|
cp -a /tmp/gconv-keep/* "$GCONV_DIR/" 2>/dev/null || true; \
|
|
rm -rf /tmp/gconv-keep; \
|
|
fi; \
|
|
\
|
|
# Misc caches
|
|
rm -rf /var/cache/fontconfig/* /tmp/*
|
|
|
|
# External tool layers, all use --link for independent layer caching and parallel pulls.
|
|
COPY --link --from=calibre-build /opt/calibre /opt/calibre
|
|
COPY --link --from=pdf-tools-build /usr/local/bin/qpdf /usr/bin/qpdf
|
|
# ImageMagick: 4 layers collapsed to 1 via the magick-export staging dir in pdf-tools-build
|
|
COPY --link --from=pdf-tools-build /magick-export/ /
|
|
COPY --link --from=gs-build /usr/local/bin/gs /usr/local/bin/gs
|
|
COPY --link --from=gs-build /usr/local/share/ghostscript /usr/local/share/ghostscript
|
|
# Python venv pre-built (no pip install at runtime, no build tools needed)
|
|
COPY --link --from=python-venv-build /opt/venv /opt/venv
|
|
|
|
RUN ldconfig /usr/local/lib && \
|
|
PYTHONDONTWRITEBYTECODE=1 \
|
|
/opt/venv/bin/python -c "import cffi; print('cffi OK:', cffi.__version__)" && \
|
|
PYTHONDONTWRITEBYTECODE=1 \
|
|
/opt/venv/bin/python -c "import cv2; print('OpenCV', cv2.__version__)" && \
|
|
PYTHONDONTWRITEBYTECODE=1 \
|
|
/opt/venv/bin/python -c "import ocrmypdf; print('ocrmypdf OK')" && \
|
|
PYTHONDONTWRITEBYTECODE=1 \
|
|
/opt/venv/bin/python -c "import pymupdf4llm; print('pymupdf4llm OK')" && \
|
|
find /opt/venv -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
|
|
|
# Non-root user
|
|
ARG PUID=1000
|
|
ARG PGID=1000
|
|
|
|
RUN set -eux; \
|
|
if ! getent group stirlingpdfgroup >/dev/null 2>&1; then \
|
|
groupadd -g "${PGID}" stirlingpdfgroup 2>/dev/null \
|
|
|| groupadd stirlingpdfgroup; \
|
|
fi; \
|
|
if ! id -u stirlingpdfuser >/dev/null 2>&1; then \
|
|
useradd -m -u "${PUID}" -g stirlingpdfgroup \
|
|
-d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser 2>/dev/null \
|
|
|| useradd -m -g stirlingpdfgroup \
|
|
-d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser; \
|
|
fi
|
|
|
|
# Application directories
|
|
RUN set -eux; \
|
|
mkdir -p /configs /configs/cache /configs/heap_dumps /logs /customFiles \
|
|
/pipeline/watchedFolders /pipeline/finishedFolders \
|
|
/storage \
|
|
/tmp/stirling-pdf/heap_dumps; \
|
|
chown -R stirlingpdfuser:stirlingpdfgroup \
|
|
/home/stirlingpdfuser /configs /logs /customFiles /pipeline /storage \
|
|
/tmp/stirling-pdf; \
|
|
chmod 750 /tmp/stirling-pdf; \
|
|
chmod 750 /tmp/stirling-pdf/heap_dumps
|
|
|
|
# Tool symlinks
|
|
RUN set -eux; \
|
|
ln -sf /opt/calibre/ebook-convert /usr/bin/ebook-convert; \
|
|
ln -sf /opt/venv/bin/unoconvert /usr/local/bin/unoconvert; \
|
|
ln -sf /opt/venv/bin/unoserver /usr/local/bin/unoserver; \
|
|
ln -sf /opt/venv/bin/ocrmypdf /usr/local/bin/ocrmypdf; \
|
|
ln -sf /opt/venv/bin/weasyprint /usr/local/bin/weasyprint; \
|
|
ln -sf /opt/venv/bin/unoping /usr/local/bin/unoping; \
|
|
ln -sf /opt/venv/bin/pymupdf-convert /usr/local/bin/pymupdf-convert; \
|
|
fc-cache -f
|
|
|
|
# Metadata labels - base image
|
|
LABEL org.opencontainers.image.title="Stirling-PDF Base" \
|
|
org.opencontainers.image.description="Pre-built base image with Calibre, Ghostscript, QPDF, ImageMagick, LibreOffice, OCRmyPDF and dependencies" \
|
|
org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF" \
|
|
org.opencontainers.image.licenses="MIT" \
|
|
org.opencontainers.image.vendor="Stirling-Tools" \
|
|
org.opencontainers.image.url="https://www.stirlingpdf.com" \
|
|
org.opencontainers.image.documentation="https://docs.stirlingpdf.com" \
|
|
maintainer="Stirling-Tools" \
|
|
org.opencontainers.image.authors="Stirling-Tools"
|