From 4e2c27acada714afd2eb97d74325f819fa62b21b Mon Sep 17 00:00:00 2001 From: overcuriousity Date: Thu, 16 Jul 2026 20:08:49 +0200 Subject: [PATCH] Add Dockerfile and publish workflow; fix XML escaping and platform bugs - Add CPU-only Dockerfile, .dockerignore, and a GHCR publish workflow triggered on version tags (closes #22) - Document the system header packages needed when Pillow builds from source on Python 3.14 (closes #23) - Escape title/authors in the cover page and TOC entries in TOC.xhtml/ toc.ncx; unescaped '&' or '<' produced invalid XHTML - Use torch.backends.mps.is_available() (available since torch 1.12) instead of torch.mps.is_available() - Fix opening the markdown file for review on macOS (open) and Windows (os.startfile; 'start' is not an executable) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TceCbVKCf5V8bjjXy7yUYz --- .dockerignore | 14 +++++++++++++ .github/workflows/docker.yml | 39 ++++++++++++++++++++++++++++++++++++ .gitignore | 1 + Dockerfile | 21 +++++++++++++++++++ README.md | 34 ++++++++++++++++++++++++++++++- main.py | 2 +- modules/mark2epub.py | 15 +++++++++----- 7 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker.yml create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7ea633c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.github +.gitignore +.ruff_cache +__pycache__ +**/__pycache__ +docs +input +*.md +!README.md +venv +.venv +Dockerfile +.dockerignore diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..3e2abd6 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,39 @@ +name: Docker + +on: + push: + tags: ["v*"] + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v7 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}} + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index 1a70eca..832f01e 100644 --- a/.gitignore +++ b/.gitignore @@ -176,3 +176,4 @@ config.yaml # macOS .DS_Store +.claude/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d9f7a46 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# CPU-only image. For GPU acceleration install a CUDA/ROCm PyTorch build +# instead (see README) or run natively. +FROM python:3.13-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY main.py . +COPY modules/ modules/ + +# marker-pdf downloads its models on first run; mount a volume at this path +# to cache them across container runs. +ENV HF_HOME=/models +VOLUME /models + +# PDFs are read from /data/input and results written next to them by default. +WORKDIR /data + +ENTRYPOINT ["python", "/app/main.py"] diff --git a/README.md b/README.md index d004cbd..16c6b4a 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,15 @@ Python 3.13 is recommended. `marker-pdf` constrains `Pillow<11.0.0`, and Pillow only ships Python 3.14 wheels from 11.3.0 onward. On Python 3.10–3.13 every dependency installs as a prebuilt wheel. On 3.14, pip has to build Pillow from source instead: this -works, but it is slower and requires a working C toolchain. +works, but it is slower and requires a working C toolchain plus the image +library headers Pillow links against. On Debian/Ubuntu install them first: + +```bash +sudo apt install libjpeg-dev zlib1g-dev libtiff-dev libfreetype6-dev libwebp-dev +``` + +Without these headers the install fails with +`RequiredDependencyException: The headers or library files could not be found for jpeg`. ## 💻 Installation @@ -74,6 +82,30 @@ print(torch.backends.mps.is_available()) # Should return True for Apple Silicon print(torch.version.hip) # Should print ROCm version for AMD ``` +### 🐳 Docker + +A CPU-only image can be built from the included `Dockerfile`: + +```bash +docker build -t pdf2epub . +``` + +Run it with your PDFs mounted at `/data` and a model cache volume (marker-pdf +downloads its models on first run): + +```bash +docker run -it --rm \ + -v "$(pwd)":/data \ + -v pdf2epub-models:/models \ + pdf2epub input.pdf +``` + +`-it` is required for EPUB generation because metadata is prompted +interactively; with `--skip-epub` it can run non-interactively. + +Tagged releases are also published to +`ghcr.io/overcuriousity/pdf2epub` by the Docker workflow. + ## 🚀 Usage ### Basic Usage diff --git a/main.py b/main.py index dbb2ea2..14a0f74 100755 --- a/main.py +++ b/main.py @@ -11,7 +11,7 @@ def main(): if torch.cuda.is_available(): print("CUDA is available. Using GPU for processing.") - elif torch.mps.is_available(): + elif torch.backends.mps.is_available(): print("MPS is available. Using Apple Silicon for processing.") else: print("CUDA is not available. Using CPU for processing.") diff --git a/modules/mark2epub.py b/modules/mark2epub.py index ee09f61..7a289aa 100644 --- a/modules/mark2epub.py +++ b/modules/mark2epub.py @@ -58,7 +58,12 @@ def review_markdown(markdown_path: Path) -> tuple[bool, str]: response = input("\nWould you like to review the markdown file before conversion? (y/n): ").lower() if response in ['y', 'yes']: try: - subprocess.run(['xdg-open' if os.name == 'posix' else 'start', str(markdown_path)], check=True) + if sys.platform == 'darwin': + subprocess.run(['open', str(markdown_path)], check=True) + elif os.name == 'posix': + subprocess.run(['xdg-open', str(markdown_path)], check=True) + else: + os.startfile(str(markdown_path)) while True: proceed = input("\nPress Enter when you're done editing (or 'q' to abort): ").lower() @@ -333,8 +338,8 @@ def get_coverpage_XML(title, authors):
-

{title}

-

{authors}

+

{xml_escape(title)}

+

{xml_escape(authors) if authors else ''}

""" @@ -355,7 +360,7 @@ def get_TOC_XML(default_css_filenames, markdown_filenames): for i,md_filename in enumerate(markdown_filenames): stem = md_filename.split(".")[0] href = quote("s{:05d}-{}.xhtml".format(i, stem)) - toc_xhtml += """
  • {}
  • """.format(href, stem) + toc_xhtml += """
  • {}
  • """.format(href, xml_escape(stem)) toc_xhtml += """\n\n\n""" return toc_xhtml @@ -377,7 +382,7 @@ def get_TOCNCX_XML(markdown_filenames, uid="", title=""): stem = md_filename.split(".")[0] src = quote("s{:05d}-{}.xhtml".format(i, stem)) toc_ncx += """\n""".format(i, i + 1) - toc_ncx += """\n{}\n""".format(stem) + toc_ncx += """\n{}\n""".format(xml_escape(stem)) toc_ncx += """""".format(src) toc_ncx += """ """ toc_ncx += """\n"""