Metadata-Version: 2.4
Name: SVGraft
Version: 0.0.6
Summary: Split SVG files to multiple pages, with margins and guides
Author-email: Derek Baier <derek.m.baier@gmail.com>
License-Expression: GPL-3.0-or-later
Project-URL: Homepage, https://github.com/deekb/SVGraft
Project-URL: Issues, https://github.com/deekb/SVGraft/issues
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cairocffi==1.7.1
Requires-Dist: CairoSVG==2.9.1
Requires-Dist: cffi==2.1.1
Requires-Dist: cssselect2==0.10.1
Requires-Dist: defusedxml==0.7.1
Requires-Dist: lxml==6.1.3
Requires-Dist: markdown-it-py==4.2.0
Requires-Dist: mdurl==0.1.2
Requires-Dist: pdf2image==1.17.0
Requires-Dist: pillow==13.0.0
Requires-Dist: pycparser==3.12
Requires-Dist: Pygments==2.21.0
Requires-Dist: pypdf==6.21.2
Requires-Dist: rich==15.0.0
Requires-Dist: tinycss2==1.5.1
Requires-Dist: webencodings==0.6.1
Provides-Extra: pymupdf
Requires-Dist: pymupdf==2.0; extra == "pymupdf"
Dynamic: license-file

# SVGraft

**SVGraft** splits a large SVG file into a multipage PDF, tiling it across standard print pages with configurable margins and alignment corner marks. It is designed for printing oversized vector graphics (such as posters, banners, or blueprints) on ordinary home or office printers.

I developed the first version at an FRC competition to print a 25x30 poster for Engineering Inspiration judging on letter paper.

---

## Features

- Splits any SVG (with a viewBox defined in mm or in) into a grid of tiles that each fit on a single page
- Outputs a single merged PDF ready to print and assemble
- Configurable page size, margins, and tile orientation
- Automatic orientation optimization — chooses portrait or landscape to minimize page count
- Corner alignment marks and a dashed cut border on every tile to aid physical assembly
- Progress display and tile summary table via `rich`
- **Scale control** — scale the output up or down before tiling, using percentage, target page count, or target physical dimensions
- **PDF input** *(optional)* — accepts PDF files directly as input (first page is converted and tiled)

---

## Requirements

- Python 3.9+
- [CairoSVG](https://cairosvg.org/) and its native Cairo library
- See `requirements.txt` for the full list of Python dependencies
- *Optional:* [PyMuPDF](https://pymupdf.readthedocs.io/) for PDF input (installed with the `pymupdf` extra, see below)
- *Tests only:* Poppler (`pdf2image` uses it to render the output PDF)

Install system dependencies (Debian/Ubuntu):

```bash
sudo apt-get install libcairo2-dev          # required
sudo apt-get install poppler-utils          # only needed to run the tests
```

---

## Installation

```bash
pip install SVGraft
```

To also enable PDF input:

```bash
pip install "SVGraft[pymupdf]"
```

> PyMuPDF is a large download and is licensed under the AGPL-3.0, so it is an opt-in extra rather than a required dependency. Without it, SVG input works normally and PDF input prints a message telling you to install the extra.

Or install it from source:

```bash
git clone <repository-url>
cd SVGraft
pip install .                 # or: pip install -e ".[pymupdf]" for development
```

---

## Usage

### Command Line

```bash
svgraft input_file [options]
```

| Argument                    | Default               | Description                                                 |
|-----------------------------|-----------------------|-------------------------------------------------------------|
| `input_file`                | *(required)*          | Path to the input SVG or PDF file                           |
| `-o`, `--output`            | `./output_merged.pdf` | Path for the output PDF file                                |
| `--page-width`              | `8.5`                 | Page width in inches                                        |
| `--page-height`             | `11.0`                | Page height in inches                                       |
| `--margin`                  | `7.0`                 | Margin on each page in millimeters                          |
| `--scale`                   | —                     | Scale factor as a multiplier (`1.5`) or percentage (`150%`) |
| `--output-width`            | —                     | Target assembled width (e.g. `48in`, `1200mm`)              |
| `--output-height`           | —                     | Target assembled height (e.g. `36in`, `900mm`)              |
| `--pages-wide`              | —                     | Target number of tiles across                               |
| `--pages-tall`              | —                     | Target number of tiles tall                                 |
| `--no-optimize-orientation` | —                     | Disable automatic portrait/landscape optimization           |
| `--border-stroke`           | `0.1`                 | Border stroke width in millimeters                          |
| `--corner-stroke`           | `0.1`                 | Corner triangle stroke width in millimeters                 |

Running `svgraft` with no input file, or with a file that does not exist, prints the usage text and exits with status 2.

**Examples:**

Split a poster onto US Letter pages with default settings:
```bash
svgraft my_poster.svg
```

Use A4 pages with a 10 mm margin and a custom output path:
```bash
svgraft my_poster.svg --page-width 8.27 --page-height 11.69 --margin 10 -o poster_tiled.pdf
```

Scale the SVG to 150% of its native size before tiling:
```bash
svgraft my_poster.svg --scale 150%
```

Scale so the assembled output is exactly 4 feet wide:
```bash
svgraft my_poster.svg --output-width 48in
```

Tile the SVG to span exactly 3 pages wide:
```bash
svgraft my_poster.svg --pages-wide 3
```

Tile the first page of a PDF (requires `SVGraft[pymupdf]`):
```bash
svgraft document.pdf --scale 200%
```

### Python API

```python
from svgraft.cli import split_svg
from svgraft.config import ProcessConfig

config = ProcessConfig(
    input_file="my_poster.svg",
    page_width_in=8.5,
    page_height_in=11.0,
    margin_mm=6.35,
    output_file="output.pdf",
    optimize_tile_orientation=True,
    scale_factor=1.5,      # optional: scale to 150%
    total_width_in=48.0,   # optional: target assembled width
    pages_wide=3,          # optional: target tile columns
)

with open(config.input_file, "rb") as f:
    split_svg(f, config)
```

---

## SVG Requirements

The input SVG **must** have:

- A `viewBox` attribute (e.g. `viewBox="0 0 1200 900"`)
- A `width` attribute defined in **inches** (`in`) or **millimeters** (`mm`)

Example SVG header:

```xml
<svg xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 2400 1800"
     width="24in" height="18in"/>
```

---

## Scale Control

SVGraft can scale your SVG before tiling using any of three modes. All modes account for per-page margins when computing the final tile grid.

### Percentage / multiplier scaling

Scale the SVG up or down relative to its native size:

```bash
svgraft poster.svg --scale 150%
svgraft poster.svg --scale 0.75
```

### Target physical dimensions

Specify the desired real-world width or height of the assembled output. SVGraft derives the required scale factor from the SVG's native physical size:

```bash
svgraft poster.svg --output-width 48in
svgraft poster.svg --output-height 1200mm
```

Supported units: `in`, `mm`, `m`.

### Target page count

Specify how many tiles wide or tall the output should span. SVGraft back-calculates the scale factor to hit that grid exactly. Orientation optimization is disabled in this mode since you have expressed an explicit layout intent:

```bash
svgraft poster.svg --pages-wide 3
svgraft poster.svg --pages-tall 2
```

---

## PDF Input

SVGraft accepts PDF files as input in addition to SVG, provided the optional PyMuPDF dependency is installed (`pip install "SVGraft[pymupdf]"`). The first page of the PDF is converted to SVG internally, with its size converted from PDF points to inches, and run through the standard tiling pipeline:

```bash
svgraft document.pdf
svgraft document.pdf --scale 200% -o tiled_output.pdf
```

> **Note:** Only the first page of a multipage PDF is currently processed.

---

## How It Works

1. **Parse** the input file. SVG files are parsed directly; PDF inputs have their first page converted to SVG via PyMuPDF before continuing.
2. **Apply scaling** — if a scale mode is specified (`--scale`, `--output-width`, `--output-height`, `--pages-wide`, or `--pages-tall`), the units-per-inch value is adjusted so the scaled content maps correctly onto the tile grid.
3. **Compute tiles** — calculate the grid of columns and rows needed so each tile fits within the usable page area (page size minus margins).
4. **Optimize orientation** — if rotating tiles from portrait to landscape (or vice versa) produces fewer total pages, SVGraft automatically swaps the dimensions and re-tiles. Disabled when `--pages-wide` or `--pages-tall` is used.
5. **Render tiles** — for each grid cell, clip and translate the SVG, add corner alignment triangles and a dashed border, then render to PDF via CairoSVG.
6. **Merge** — all per-tile PDFs are merged into a single output PDF using pypdf.

---

## Assembly

Each page includes:

- **Corner triangles** at the four content corners for precise cut-and-align assembly
- A **dashed border** showing the exact content boundary

Pages are ordered left to right, then top to bottom. To assemble a print: trim half of the pages to their dashed border, then align them to the corner marks of the adjacent tiles.

---

## Configuration Defaults

Defaults are defined in the `DEFAULTS` dictionary in `src/svgraft/config.py`:

| Setting                     | Default               | Description                                 |
|-----------------------------|-----------------------|---------------------------------------------|
| `page_width_in`             | `8.5`                 | Page width (inches)                         |
| `page_height_in`            | `11`                  | Page height (inches)                        |
| `margin_mm`                 | `7.0`                 | Per-page margin (mm)                        |
| `corner_triangle_size_mm`   | `2.0`                 | Size of corner alignment triangles (mm)     |
| `corner_triangle_stroke`    | `0.1`                 | Stroke width of corner marks (mm)           |
| `border_stroke_mm`          | `0.1`                 | Stroke width of the dashed border (mm)      |
| `optimize_tile_orientation` | `True`                | Auto-flip orientation for fewer pages       |
| `output_file`               | `./output_merged.pdf` | Where the merged PDF is written             |

---

## Running Tests

```bash
pip install -e ".[pymupdf]"
python -m unittest discover -s tests -p "test*.py" -v
```

Install the package in editable mode first. If an older copy of SVGraft is installed in your environment, the tests will import that instead of your working tree, so run `pip uninstall -y svgraft` before the editable install.

- `tests/test_core.py` covers unit parsing, tiling math, config defaults, page layout, rendering helpers, PDF merging, end-to-end splits (page counts, page sizes, scaling, orientation), the sample posters, and the command line.
- `tests/testSVGraft.py` splits `tests/data/test_poster_inches.svg` onto US Letter pages and checks the output. It opens a preview of the first page on machines with a display and skips the preview on headless machines and in CI.
- The PDF input tests are skipped automatically if PyMuPDF is not installed.

A few tests are marked as expected failures. They document a known bug in `count_not_none` (`src/svgraft/util.py`), which stops the "only one size option allowed" check from firing.

### Continuous integration

Gitea Actions workflows live in `.gitea/workflows/`:

| Workflow           | Trigger                  | What it does                                        |
|--------------------|--------------------------|-----------------------------------------------------|
| `test.yml`         | every push / pull request | Installs the package and runs the unit tests        |
| `publish.yml`      | push of a `v*` tag       | Builds and uploads to PyPI                          |
| `publish-test.yml` | manual (`workflow_dispatch`) | Builds and uploads to TestPyPI                  |

The runner needs `libcairo2` and `poppler-utils` available for the test workflow.

---

## Releasing

1. Bump `__version__` in `src/svgraft/__init__.py`.
2. Commit and push.
3. Tag the release with the same version, prefixed by `v`, and push the tag:

   ```bash
   git tag v0.0.6
   git push origin v0.0.6
   ```

`publish.yml` checks that the tag matches `__version__`, builds the sdist and wheel, and uploads them with `twine`. PyPI never accepts the same version twice, so always bump the version for a new release.

Repository secrets (Settings → Actions → Secrets):

| Secret                | Used by            | Where to get it                                     |
|-----------------------|--------------------|-----------------------------------------------------|
| `PYPI_API_TOKEN`      | `publish.yml`      | pypi.org → Account settings → API tokens            |
| `TEST_PYPI_API_TOKEN` | `publish-test.yml` | test.pypi.org → Account settings → API tokens       |

To try a TestPyPI release, run the `publish-test.yml` workflow from the repository's Actions tab, then install it with:

```bash
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ SVGraft
```

---

## Project Structure

```
SVGraft/
├── .gitea/workflows/
│   ├── test.yml          # Unit tests on every push
│   ├── publish.yml       # Publish to PyPI on v* tags
│   └── publish-test.yml  # Manual publish to TestPyPI
├── src/svgraft/
│   ├── cli.py          # Entry point: argument parsing and split_svg()
│   ├── config.py       # Default settings and ProcessConfig
│   ├── tiling.py       # Tile grid computation
│   ├── rendering.py    # Per-tile SVG manipulation and PDF rendering
│   ├── pdf.py          # In-memory PDF merging and PDF-to-SVG conversion
│   ├── svg_io.py       # SVG unit parsing and physical dimension conversion
│   ├── geometry.py     # Unit conversion utilities
│   ├── util.py         # Small helpers
│   └── debug.py        # Debug/console helpers
├── tests/
│   ├── data/           # Sample posters (inches, millimeters, US Letter)
│   ├── test_core.py    # Unit and end-to-end tests
│   └── testSVGraft.py  # Split test with optional preview
├── gui.py              # Experimental GUI (requires dearpygui)
├── pyproject.toml
└── requirements.txt
```

---

## License

This project is licensed under the **GNU General Public License v3.0 or later**. See [LICENSE](LICENSE) for details.

---

## Author

Derek Baier — [derek.m.baier@gmail.com](mailto:derek.m.baier@gmail.com)
