Metadata-Version: 2.4
Name: fop2
Version: 0.1.0
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Summary: Apache FOP - XSL-FO to PDF converter (Rust implementation)
Keywords: xsl-fo,pdf,python,bindings
Home-Page: https://github.com/cool-japan/fop
Author: COOLJAPAN OU (Team Kitasan)
License: Apache-2.0
Requires-Python: >=3.8
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Source Code, https://github.com/cool-japan/fop

# fop-python

[![Crates.io](https://img.shields.io/crates/v/fop-python.svg)](https://crates.io/crates/fop-python)
[![docs.rs](https://docs.rs/fop-python/badge.svg)](https://docs.rs/fop-python)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](https://github.com/cool-japan/fop/blob/main/LICENSE)
[![Python](https://img.shields.io/badge/python-3.8%2B-blue.svg)](https://www.python.org/)
[![PyO3](https://img.shields.io/badge/PyO3-0.24-orange.svg)](https://pyo3.rs/)

Python bindings for [FOP](https://github.com/cool-japan/fop) — a high-performance, pure-Rust XSL-FO processor that converts XSL-FO (Extensible Stylesheet Language Formatting Objects) documents to PDF, SVG, and plain text output.

This crate is part of the **COOLJAPAN FOP ecosystem** and is built with [PyO3](https://pyo3.rs/), providing a native Python extension that runs 10–1200x faster than the original Java Apache FOP.

---

## Description

`fop-python` exposes the full FOP conversion pipeline to Python via a native extension module. It compiles to a shared library (`fop.so` / `fop.pyd`) that can be imported directly from Python. The bindings cover:

- XSL-FO string to PDF bytes
- XSL-FO string to SVG string
- XSL-FO string to plain text string
- File-to-file conversion with automatic format detection
- XSL-FO document validation
- Stateful `FopConverter` class with configurable options
- Module-level one-shot convenience functions

The implementation delegates to the same Rust pipeline used throughout the FOP workspace:

```
XSL-FO XML  -->  FO Tree  -->  Area Tree  -->  PDF / SVG / Text
 (fop-core)    (fop-layout)   (fop-render)
```

---

## Features

| Feature | Description |
|---------|-------------|
| `FopConverter` class | Stateful converter object — instantiate once, convert many documents |
| `convert_to_pdf(fo_xml)` | Convert an XSL-FO string to raw PDF bytes |
| `convert_to_svg(fo_xml)` | Convert an XSL-FO string to an SVG string |
| `convert_to_text(fo_xml)` | Convert an XSL-FO string to plain text |
| `convert_file(input, output)` | Convert a `.fo` file to PDF/SVG/TXT (format inferred from extension) |
| `validate(fo_xml)` | Parse and validate an XSL-FO document; returns `(valid, node_count, error)` |
| `version()` | Return the crate version string |
| Module-level one-shot functions | `fop.convert_to_pdf()`, `fop.convert_to_svg()`, `fop.version()` |
| `verbose` property | Enable/disable verbose logging on the converter instance |

---

## Installation

### Via pip (maturin-built wheel)

If a pre-built wheel is available on PyPI:

```bash
pip install fop2
```

To build and install from source using [maturin](https://www.maturin.rs/):

```bash
# Install maturin
pip install maturin

# Clone the repository
git clone https://github.com/cool-japan/fop.git
cd fop/crates/fop-python

# Build and install into the active virtual environment
maturin develop --features extension-module
```

### As a Rust dependency

Add the following to your `Cargo.toml`:

```toml
[dependencies]
fop-python = "0.1.0"
```

---

## Python Usage

### Importing the module

```python
import fop
```

### Using the `FopConverter` class

`FopConverter` is the primary API. Instantiate it once and reuse it for multiple conversions:

```python
import fop

# Create a converter instance
converter = fop.FopConverter()

# Inspect the object
print(repr(converter))  # FopConverter(verbose=False)

# Enable verbose mode
converter.verbose = True
```

### Converting XSL-FO to PDF

```python
import fop

fo_xml = """<?xml version="1.0" encoding="UTF-8"?>
<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
  <fo:layout-master-set>
    <fo:simple-page-master master-name="A4"
                           page-width="210mm" page-height="297mm"
                           margin-top="20mm" margin-bottom="20mm"
                           margin-left="25mm" margin-right="25mm">
      <fo:region-body/>
    </fo:simple-page-master>
  </fo:layout-master-set>
  <fo:page-sequence master-reference="A4">
    <fo:flow flow-name="xsl-region-body">
      <fo:block font-size="18pt" font-weight="bold">Hello, FOP!</fo:block>
      <fo:block font-size="12pt" space-before="6pt">
        Generated by fop-python — a pure-Rust XSL-FO processor.
      </fo:block>
    </fo:flow>
  </fo:page-sequence>
</fo:root>"""

converter = fop.FopConverter()
pdf_bytes = converter.convert_to_pdf(fo_xml)

with open("output.pdf", "wb") as f:
    f.write(pdf_bytes)

print(f"PDF generated: {len(pdf_bytes)} bytes")
```

### Converting XSL-FO to SVG

```python
import fop

converter = fop.FopConverter()
svg_string = converter.convert_to_svg(fo_xml)

with open("output.svg", "w", encoding="utf-8") as f:
    f.write(svg_string)
```

### Converting XSL-FO to plain text

```python
import fop

converter = fop.FopConverter()
text = converter.convert_to_text(fo_xml)
print(text)
```

### File-to-file conversion

The output format is automatically detected from the file extension:
- `.pdf` — PDF output (default)
- `.svg` — SVG output
- `.txt` — Plain text output

```python
import fop

converter = fop.FopConverter()

# Convert to PDF
converter.convert_file("document.fo", "document.pdf")

# Convert to SVG
converter.convert_file("document.fo", "document.svg")

# Convert to plain text
converter.convert_file("document.fo", "document.txt")
```

### Validating an XSL-FO document

`validate()` always returns `Ok` at the Python level; parse errors are reported in the tuple rather than raised as exceptions:

```python
import fop

converter = fop.FopConverter()
valid, node_count, error = converter.validate(fo_xml)

if valid:
    print(f"Document is valid ({node_count} nodes)")
else:
    print(f"Validation failed: {error}")
```

### Module-level convenience functions

For one-shot conversions without creating a `FopConverter` instance:

```python
import fop

# Convert directly
pdf_bytes = fop.convert_to_pdf(fo_xml)
svg_string = fop.convert_to_svg(fo_xml)

# Get version
print(fop.version())  # fop-python 0.1.0
```

### Error handling

Conversion errors are raised as Python `RuntimeError` exceptions:

```python
import fop

converter = fop.FopConverter()

try:
    pdf_bytes = converter.convert_to_pdf("<<<not valid XSL-FO>>>")
except RuntimeError as e:
    print(f"Conversion failed: {e}")

try:
    converter.convert_file("/nonexistent/path.fo", "/tmp/out.pdf")
except IOError as e:
    print(f"File error: {e}")
```

---

## Building from Source

### Prerequisites

- Rust toolchain (1.70+) — install via [rustup](https://rustup.rs/)
- Python 3.8+ with a virtual environment
- [maturin](https://www.maturin.rs/) (`pip install maturin`)

### Development build (editable install)

```bash
git clone https://github.com/cool-japan/fop.git
cd fop/crates/fop-python

# Activate your virtual environment first, then:
maturin develop --features extension-module
```

This builds the native extension and installs it directly into the active Python environment, making `import fop` available immediately.

### Release wheel

```bash
# Build a release wheel for the current platform
maturin build --release --features extension-module

# The wheel is placed in target/wheels/
pip install target/wheels/fop_python-*.whl
```

### Cargo build (library only, no Python install)

```bash
# Build as a cdylib (shared library) from the workspace root
cargo build --release -p fop-python

# Or from the crate directory
cargo build --release
```

---

## Rust Usage

You can also use `fop-python` as a Rust library to access the `FopConverter` struct directly (e.g., in tests or when embedding):

```rust
use fop_python::converter::FopConverter;

fn main() {
    let converter = FopConverter::new();

    let fo_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
  <fo:layout-master-set>
    <fo:simple-page-master master-name="A4"
                           page-width="210mm" page-height="297mm">
      <fo:region-body/>
    </fo:simple-page-master>
  </fo:layout-master-set>
  <fo:page-sequence master-reference="A4">
    <fo:flow flow-name="xsl-region-body">
      <fo:block>Hello from Rust!</fo:block>
    </fo:flow>
  </fo:page-sequence>
</fo:root>"#;

    // Returns PyResult<Vec<u8>> — use pyo3::Python::with_gil to call from Rust
    // For pure Rust usage, prefer the fop-core / fop-render crates directly.
    println!("FopConverter created: {:?}", converter.version());
}
```

For pure Rust embedding without PyO3 overhead, use the underlying crates directly:

```rust
use fop_core::FoTreeBuilder;
use fop_layout::LayoutEngine;
use fop_render::PdfRenderer;
use std::io::Cursor;

let fo_tree = FoTreeBuilder::new().parse(Cursor::new(fo_xml)).unwrap();
let area_tree = LayoutEngine::new().layout(&fo_tree).unwrap();
let pdf_doc = PdfRenderer::new().render(&area_tree).unwrap();
let pdf_bytes = pdf_doc.to_bytes().unwrap();
```

---

## Feature Flags

| Feature | Default | Description |
|---------|---------|-------------|
| `extension-module` | No | Enables PyO3's `extension-module` feature, required when building a Python native extension (`.so`/`.pyd`). Must be enabled when using `maturin build` or `maturin develop`. Do **not** enable this when using the crate as a pure Rust library dependency, as it links against the Python interpreter in a way that is incompatible with standalone binaries. |

```toml
# In Cargo.toml when building as a Python extension:
[features]
extension-module = ["pyo3/extension-module"]
```

Maturin activates this feature automatically when invoked as a build backend.

---

## Architecture

`fop-python` is a thin PyO3 binding layer over the FOP Rust workspace crates:

```
fop-python  (PyO3 bindings)
    |
    +-- fop-render  (PdfRenderer, SvgRenderer, TextRenderer)
    |       |
    |       +-- fop-layout  (LayoutEngine, area tree)
    |               |
    |               +-- fop-core   (FoTreeBuilder, XML parsing)
    |                       |
    |                       +-- fop-types  (Length, Color, FopError, ...)
```

Error propagation: `FopError` (Rust) is mapped to `RuntimeError` (Python) via `fop_error_to_py`. File I/O errors are mapped to `IOError`.

---

## Supported XSL-FO Elements

The underlying rendering engine supports the following XSL-FO 1.1 elements:

- `fo:root`, `fo:layout-master-set`, `fo:simple-page-master`
- `fo:region-body`, `fo:region-before`, `fo:region-after`
- `fo:page-sequence`, `fo:flow`, `fo:static-content`
- `fo:block`, `fo:inline`
- `fo:table`, `fo:table-body`, `fo:table-row`, `fo:table-cell`, `fo:table-column`
- `fo:list-block`, `fo:list-item`, `fo:list-item-label`, `fo:list-item-body`
- `fo:external-graphic`, `fo:basic-link`

294 XSL-FO 1.1 properties are supported with full inheritance semantics.

---

## Performance

| Metric | Java Apache FOP | fop-python (Rust) |
|--------|-----------------|-------------------|
| Startup time | ~2000ms (JVM) | <10ms |
| Parse 1000-page doc | ~500ms | <50ms |
| Memory per page | ~50KB | <5KB |
| Throughput vs Java | 1x | **10–1200x faster** |

---

## Repository

- Source: [https://github.com/cool-japan/fop](https://github.com/cool-japan/fop)
- Documentation: [https://docs.rs/fop-python](https://docs.rs/fop-python)
- Issues: [https://github.com/cool-japan/fop/issues](https://github.com/cool-japan/fop/issues)

---

## License

Copyright 2024 COOLJAPAN OU (Team Kitasan)

Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0).

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

