Skip to content

About

Independent Python bindings for Boko: ebook conversion, inspection, and export.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

Repository files navigation

boko-python

Independent Python bindings for Boko, the native Rust ebook converter. This project depends on the published boko Rust crate; it does not fork or patch the converter and is not maintained by the upstream Boko project.

PyPI version

GitHub release GitHub Downloads CI

Made with Python License

GitHub Sponsors PayPal Liberapay

The Python distribution is boko-ebook, imported as boko. The unrelated PyPI distribution boko uses the same import name: do not install both together.

Caution

This project was developed with use of AI assistance.

Installation

pip install boko-ebook

Usage

from pathlib import Path
import boko

boko.convert_file(Path('input.epub'), Path('output.kfx'))

# Use bytes when the input is already in memory.
source = Path('input.epub').read_bytes()
output = boko.convert(source, 'epub', 'kfx')
Format Input Output
EPUB 2/3 (epub) yes yes
AZW3 (azw3) yes yes
MOBI (mobi, azw) yes no
KFX (kfx) yes yes
Markdown (markdown, md) no yes

Format names are case-insensitive. convert returns bytes (UTF-8 for Markdown).

convert() buffers input and output in memory. convert_file() infers formats from case-insensitive file extensions and writes directly to a same-filesystem temporary file, without buffering the entire output in the binding. It replaces an existing destination only after export and file synchronization succeed. The same input/output file, including symlink and hardlink aliases, raises ValueError. Output extensions are .epub, .azw3, .kfx, .md, and .markdown. POSIX outputs use private file permissions (0600).

For inspection, repeated reads, or export options, use the Book API below. Parsing, conversion, chapter loading/rendering, link resolution, and optimization release the GIL.

  • Invalid/unsupported formats and malformed books raise ValueError.
  • DRM-protected books raise PermissionError; no decryption is provided.
  • Missing resources raise KeyError; genuine I/O failures raise OSError.
  • Unrecognized future core errors fall back to RuntimeError.
  • Rust panics during detached native operations raise RuntimeError. After a panic, close and discard the affected book; continued use is not supported. Allocation aborts and OS process kills cannot be caught this way, so untrusted books still need worker limits.

Regular CPython 3.11+ is supported. Wheels use abi3-py311 and do not require Rust to install. Free-threaded Python, PyPy, and GraalPy wheels are not currently built.

Untrusted books

Parsing is not a sandbox. Input, decompressed chapters, parsed content, image processing, and exports can consume much more memory than the uploaded file. The core's 512 MiB limit per decompressed EPUB entry is not a whole-book memory budget; neither compressed file size nor spine size estimates provide that budget.

Process each untrusted book in a disposable worker process or container with an OS-enforced memory limit and a parent-enforced wall-clock timeout. Limit concurrent workers too. On Linux, resource.RLIMIT_AS bounds virtual address space, including libraries and thread stacks; choose the budget for your workload. Other platforms need their own process/container limits. A Rust allocation failure can abort the worker rather than raise a Python exception: treat nonzero exits and timeouts as failed jobs, not failures of the parent service.

These limits must be configured by the calling application; the library does not impose a whole-book memory budget.

Book API

with boko.Book.open(Path('input.epub')) as book:
    print(book.metadata['identifier'], book.metadata['is_rtl'])
    print(book.toc, book.landmarks, book.spine)
    cover = book.cover()
    first = book.chapter(book.spine[0]['id'])
    print(first.text)
    sections = book.sections()
    links = book.resolve_links()
    print(links.broken_links)
    output = book.export('epub', normalize=True, compression_level=6)

Use Book.from_bytes(data, format) for in-memory inputs. Book.open(path, format=None) accepts strings and PathLike objects, inferring formats from the file extension unless supplied explicitly. Use a with statement or close() to release the file and caches. closed reports the state; operations on a closed book raise ValueError.

Inspection and assets

  • metadata: every core metadata field, including contributor roles, collection/ series, sort names, cover path, reading direction, and inferred is_rtl.
  • toc: nested entries with title, href, play order, children, and optional resolved target. Call resolve_links() to populate targets.
  • landmarks: structural navigation entries, using snake-case type names.
  • spine: reading-order entries with id, source_path, and size_estimate.
  • assets: archive-relative asset names. load_asset(path) returns their bytes.
  • cover(): cover bytes, or None when no cover is identified. An identified but missing resource raises KeyError.
  • font_faces: family, numeric weight, style, and archive source path.
  • requires_normalized_export: whether source content needs normalized export.

Dictionaries and lists are independent snapshots: modifying them does not edit the book. Absent optional metadata uses None; absent collections use empty lists. boko.pyi describes these dictionaries; they are not runtime model classes.

Chapters and sections

chapter(id) loads a read-only Chapter snapshot. chapters(ids=None) loads all chapters in spine order, or the supplied IDs in that order, using the core's parallel/cache support. Chapter IDs come from spine, not TOC indexes.

Chapter property/method Result
id, source_path Chapter identifier and original archive path, if available
raw Format-specific bytes; KFX raw content is binary, not HTML
text Readable live-node text, including footnotes, code whitespace retained by the core, image alt text, and math fallback text
xhtml Normalized XHTML, linking its stylesheet as chapter.css
css The CSS matching the XHTML's generated class names
node_text(node_id) Readable text of a normalized node, for link navigation

XHTML/CSS are normalized renderings, not source-preserving edits. The generated CSS may include unused styles from the chapter's style pool. They do not copy or embed image/font assets: retrieve those separately with load_asset().

sections() returns title, authors, language, preamble, and nested sections. Each section contains level, title, content, and children; content blocks are tagged by type: paragraph, code_block, block_quote, list, table, image, or rule. This is a reading/search representation: upstream extraction omits nodes classified as footnotes by the core and drops inline formatting. Footnote classification varies by importer, so some EPUB notes may instead appear as ordinary blocks. Use chapter text/XHTML when footnote content is required.

clear_cache() clears parsed core chapters and requires exclusive access to the book. Existing chapter and link snapshots remain usable after cache clearing, optimization, or closing their book, and keep their own memory alive until released.

Links

resolve_links() returns a read-only ResolvedLinks snapshot:

  • links: ordered {source, target} entries; len(result) counts resolved links.
  • broken_links: ordered {source, href} entries.
  • get(chapter_id, node_id): the target of a source link, or None.
  • links_to(chapter_id, node_id=None): source nodes linking to a specific node, or to the chapter start when node_id is omitted (not all nodes in the chapter).

Sources contain chapter_id and node_id. Targets are discriminated by kind: internal includes both IDs; chapter includes chapter_id; external includes url. IDs refer to normalized nodes, not source-file byte offsets.

Export and optimization

export(format, *, normalize=None, compression_level=None) returns bytes. normalize applies only to EPUB/AZW3; compression_level applies only to EPUB and must be 1–9 (the current ZIP backend rejects level 0). Incompatible options raise ValueError. KFX input is normalized automatically where needed.

optimize() explicitly mutates the book using Boko's default image pass and returns passes, assets_changed, and bytes_saved. This is lossy image optimization: the core may downscale and re-encode images as JPEG and rename assets, including the cover path. JPEG quality (80), dimension limit (1236), and minimum savings are fixed by the core. Inspect fresh metadata/assets after it runs. Previously returned snapshots still describe the pre-optimization state.

Read-only operations may run concurrently on one book. clear_cache(), close(), sections(), and optimize() require exclusive access: do not run them concurrently with other operations on the same book. PyO3 rejects conflicting borrows rather than waiting.

Development

Requires CPython 3.11+, Rust 1.91+, and platform native build tools. Development uses Python 3.11, the oldest supported minor version. Rust, Python, and uv versions are pinned in mise.toml. With mise activated in your shell:

mise run dev                                        # uv sync: editable install
uv run python -m unittest discover -s tests -v       # run tests
mise run build                                      # uv build: wheel + source archive

uv manages .venv and builds the extension with maturin; no manual activation or maturin installation is needed.

Additional Rust checks: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, and cargo test --locked --lib. For Rust's embedded-interpreter test, relocatable Python installations may need PYTHONHOME set to Python's base prefix. On Linux, also add $PYTHONHOME/lib to LD_LIBRARY_PATH so the test executable can load the Python shared library. Importing the extension does not require these settings.

Build artifacts go to dist/. Use release builds when measuring performance. Local Linux wheels are not necessarily portable: use manylinux containers or maturin's compatible Zig support for distribution.

Releases

CI checks Rust, tests wheels for Linux x86_64/ARM64, macOS x86_64/ARM64, and Windows x86_64 on Python 3.11 and 3.15, and rebuilds/tests the source distribution. Pushing a v<version> tag matching Cargo.toml publishes the tested artifacts to PyPI and GitHub.

The PyPI Trusted Publisher must use workflow publish.yml and environment pypi. Protect that GitHub environment with required reviewers and tag restrictions before publishing.

License

GPL-3.0-or-later, matching the linked Boko core. See LICENSE. Boko's Rust implementation and original authorship remain with the upstream project.

About

Independent Python bindings for Boko: ebook conversion, inspection, and export.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages