Metadata-Version: 2.5
Name: panel-compiler
Version: 0.1.11
Summary: Compose SVG figures and LaTeX equations into a panel template
Author-email: Matthias Függer <mfuegger@gmail.com>
License: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: pyyaml
Description-Content-Type: text/markdown

# pc — Panel Compiler

Compose scientific figures from SVG plots, PDF figures, standalone TeX figures, LaTeX equations, and raster images (PNG/JPEG) into a single SVG or PDF panel.
This is intended for the case that these subfigures are
generated by scripts and you automatically want to keep your panels up to date.

Define a panel template in Inkscape with named group (`<g>`) or rectangle (`<rect>`) placeholders (set via the layer/group label field), then write a small YAML config that maps each label to an SVG file, PDF, standalone `.tex` file, raster image (`.png`/`.jpg`), or a LaTeX string. `pc` scales each figure to fit its placeholder and writes the compiled panel. Raster images are embedded as-is at their pixel size and scaled to the placeholder, so a molecular render or photo can drop in directly:

```yaml
structure:
  file: laci_operator.png
  fit: contain
```
It is often convenient to let `pc` overwrite the old panel (labels are preserved for repeated execution).

## Example

An example is given in `example`.
Assume you would like to create a panel with 3 subfigures.
Two are figures created via `python` code, and one contains a LaTeX formula.

We start from the panel template `example/panel.svg` that was created in Inkscape.
Here, the template is simple and contains only three `rect` placeholders, each of which has set its `label` (via Inkscape) set to one of: `formula`, `logistic`, and `logistic_log`.

This is the panel:

![panel template](example/panel.svg)

The `python` script that generates the subfigures is `example/subfigures.py` with the following
subfigures being generated:

| `logistic.svg` | `logistic_log.svg` |
|:--------------:|:------------------:|
| ![logistic](example/logistic.svg) | ![logistic_log](example/logistic_log.svg) |

Next we prepared a configuration `example/pc.yaml` that tells the panel-compiler how to
compile the panel:
```yaml
panel: panel.svg
output:
  - out.svg
  - panel.pdf

formula:
  tex: $\frac{dN}{dt} = rN\!\left(1 - \frac{N}{K}\right)$
  size: 10pt

logistic:
  file: logistic.svg
  fit: contain

logistic_log:
  file: logistic_log.svg
  fit: contain
```

The panel is finally compiled via:

```bash
cd example
python subfigures.py
pc pc.yaml
```

Resulting in the generation of `example/out.svg`:

![compiled panel](example/out.svg)


## Installation

From PyPI:

```bash
uv tool install panel-compiler
```

or with pip:

```bash
pip install panel-compiler
```

From the repository:

```bash
uv tool install git+https://github.com/mfuegger/pc.git
```

or with pip:

```bash
pip install git+https://github.com/mfuegger/pc.git
```

For LaTeX rendering and standalone `.tex` figures, `pdflatex` must be on your `PATH`. For PDF/TeX figures and PDF output, `pdf2svg` and `inkscape` must be on your `PATH`. On macOS: `brew install pdf2svg`. SVG and raster (PNG/JPEG) figures need no external tool.

## Usage

```
pc [config.yaml]
```

| Argument | Default | Description |
|---|---|---|
| `config.yaml` | `pc.yaml` | YAML config (see below) |

The output path is taken from the `output` key in the config. If omitted, it defaults to the config filename with `.svg` extension (e.g. `pc.yaml` → `pc.svg`).

## Config format

### Single panel

```yaml
panel: panel.svg    # required — path to the panel SVG, relative to this config file
output: out.svg     # optional — defaults to <config-stem>.svg if omitted
content_style: "stroke: none; fill: initial;"  # optional — empty by default
```

`output` can also be a list to produce multiple formats in one run. Supported
formats are SVG, PDF, and PNG (chosen by the file extension); PDF and PNG are
exported through Inkscape:

```yaml
output:
  - out.svg
  - out.pdf
  - out.png
```

For raster output, set the resolution per entry with `dpi` (the panel's physical
`width`/`height` in mm then fix the pixel dimensions). Either nest the options
under the filename or use the explicit `file:` form:

```yaml
output:
  - out.svg
  - out.png:        # keyed form
      dpi: 600
  - file: thumb.png # file form
    dpi: 150
```

`dpi` is ignored for SVG; for PDF it sets the resolution at which Inkscape
rasterises filter effects (drop shadows, blurs).

`content_style` is an optional CSS declaration block applied to all paths, circles, and polygons inside embedded SVG figures. It is **empty by default**, so each figure keeps its own strokes and fills. Set it only when the panel template ships type-selector rules (e.g. a `<style>` with `path { stroke: ... }`) that would otherwise bleed into embedded content; the value then overrides them (e.g. `stroke: none; fill: initial;`). LaTeX labels always get `stroke: none` regardless of this setting.

Each remaining key is a **label** that must match an element in the panel SVG. `pc` looks it up by `inkscape:label` first, then `label`, then `id`. The element can be a `<g>` group or a `<rect>` placeholder — a `<rect>` is automatically replaced by a `<g>` positioned at the rect's `x`/`y`. A warning (including the panel filename) is emitted for any label not found; compilation of the remaining entries continues.

### Figure entry (SVG, PDF, or TeX)

```yaml
plot:
  file: results.svg   # path relative to this config file; .svg, .pdf, or .tex  (alias: svg:)
  fit: contain        # contain | height | width  (default: contain)
  width: 200          # optional — fallback target width  (SVG user units)
  height: 100         # optional — fallback target height (SVG user units)
```

Fit strategies:
- `contain` — scale uniformly to fit within the placeholder box (default)
- `height` — scale to match the placeholder height exactly
- `width`  — scale to match the placeholder width exactly

Target dimensions come from `width`/`height` attributes on the group element in the panel SVG. The config `width`/`height` are used as a fallback when the group has no such attributes.

PDF files are converted via `pdf2svg` before embedding. TeX files are compiled with `pdflatex`, converted via `pdf2svg`, then embedded and scaled like SVG/PDF figures.

Use `.tex` files for complete documents, not snippets. In practice these are often `standalone` documents containing TikZ, algorithms, proof trees, tables, or anything else that is easier to draw in LaTeX than in Inkscape:

```yaml
algorithm:
  file: algorithm.tex
  fit: contain
```

For example:

```tex
\documentclass[tikz,border=2pt]{standalone}
\usepackage{tikz}

\begin{document}
\begin{tikzpicture}
  \draw (0,0) rectangle (2,1);
  \node at (1,.5) {example};
\end{tikzpicture}
\end{document}
```

If the TeX file does not compile, `pc` prints the `pdflatex` command, the working directory, the exit code, and the tail of the LaTeX log. That usually includes the familiar `file.tex:line: error` message, so the first thing to check is the reported line in the `.tex` file. The file is compiled from its own directory, so relative `\input{...}` and image paths should be written as they would be when running `pdflatex` next to that file.

### LaTeX entry

```yaml
label:
  tex: $y = \sin(x)$   # any LaTeX — math mode, text, amsmath, …
  size: 12pt           # font size (default: 10pt); scales the rendered output
```

Rendered via `pdflatex` + `inkscape`. No fit scaling — the output is sized by `size` alone.

### Shorthand

A plain string value is treated as an SVG file path with `fit: contain`:

```yaml
other: path/to/fig.svg
```

### Multiple panels

Use a YAML list; each entry must have its own `output`. A plain mapping (non-list) with duplicate keys — the easy mistake when copy-pasting a second panel block — triggers a warning and the second value silently wins; use the list form to avoid this.

```yaml
- panel: panel1.svg
  output: out1.svg
  plot:
    file: results.svg

- panel: panel2.svg
  output:
    - out2.svg
    - out2.pdf
  label:
    tex: $E = mc^2$
    size: 12pt
```

## Development and tests

Run the full test suite with coverage:

```bash
uv run pytest
# or: make test
```

Every run measures line and branch coverage for `panel_compiler` and the legacy
`pc.py` module. The terminal report lists uncovered lines and branches; the
combined coverage must be at least 95% or the command fails. The settings live
in `pyproject.toml` and use `pytest-cov` with coverage.py.

Open `htmlcov/index.html` for the annotated source report. `coverage.xml` is also
generated for CI tools. These generated files are ignored by Git.

GitHub Actions runs the same checks on every push and pull request with Python
3.12 and 3.14, adds coverage to the job summary, and saves the HTML/XML reports
as downloadable artifacts. External converter commands are mocked in the test
suite, so running it does not require Inkscape or a LaTeX installation.

For a focused regression test without the full-suite coverage threshold:

```bash
uv run pytest --no-cov tests/test_scaling.py
```

Coverage records which code the tests exercise; the regression assertions check
the resulting SVG content, dimensions, references, and error handling.

## License

Apache 2.0 — see [LICENSE](LICENSE).
