Metadata-Version: 2.4
Name: imagespec
Version: 1.0.1
Summary: Render images from a declarative YAML/dict spec — shapes, text, charts, QR/barcodes — for e-paper ESL tags and label printers.
Author: eigger
License-Expression: MIT AND Apache-2.0
Project-URL: Homepage, https://github.com/eigger/imagespec
Project-URL: Repository, https://github.com/eigger/imagespec
Project-URL: Changelog, https://github.com/eigger/imagespec/blob/main/CHANGELOG.md
Project-URL: Documentation, https://github.com/eigger/imagespec/blob/main/docs/integrating.md
Project-URL: Issues, https://github.com/eigger/imagespec/issues
Keywords: image,render,yaml,spec,e-paper,esl,label,pillow
Classifier: Development Status :: 5 - Production/Stable
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Multimedia :: Graphics
Classifier: Operating System :: OS Independent
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: LICENSE-APACHE-2.0
License-File: NOTICE
Requires-Dist: pillow>=10.4
Requires-Dist: qrcode[pil]>=7.4
Requires-Dist: python-barcode>=0.15
Requires-Dist: requests>=2.25
Provides-Extra: datamatrix
Requires-Dist: pyStrich>=0.8; extra == "datamatrix"
Provides-Extra: yaml
Requires-Dist: PyYAML>=6.0.1; extra == "yaml"
Provides-Extra: fast
Requires-Dist: numpy>=2.1; extra == "fast"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: numpy>=2.1; extra == "dev"
Requires-Dist: mypy>=1.14; extra == "dev"
Requires-Dist: types-requests>=2.32; extra == "dev"
Requires-Dist: types-qrcode>=8.0; extra == "dev"
Requires-Dist: jsonschema>=4.18; extra == "dev"
Dynamic: license-file

# imagespec

[![PyPI](https://img.shields.io/pypi/v/imagespec.svg)](https://pypi.org/project/imagespec/)
[![Python versions](https://img.shields.io/pypi/pyversions/imagespec.svg)](https://pypi.org/project/imagespec/)
[![CI](https://github.com/eigger/imagespec/actions/workflows/ci.yml/badge.svg)](https://github.com/eigger/imagespec/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/License-MIT%20AND%20Apache--2.0-yellow.svg)](LICENSE)

Render images from a declarative **YAML/dict spec** — shapes, text, charts,
QR/barcodes — for e-paper ESL tags and label printers.

![imagespec Showcase](https://raw.githubusercontent.com/eigger/imagespec/main/examples/showcase.png)

`imagespec` is the rendering core behind
[`hass-ble-esl`](https://github.com/eigger/hass-ble-esl) (BLE e-paper shelf
labels) and [`hass-niimbot`](https://github.com/eigger/hass-niimbot) (label
printers): both Home Assistant integrations depend on it from PyPI and keep
only a thin adapter of their own (see
[`docs/integrating.md`](docs/integrating.md)). The library has no
framework dependency, so it can be used and tested standalone. The rendering
engine was originally adapted from
[OpenEPaperLink's Home Assistant Integration](https://github.com/OpenEPaperLink/Home_Assistant_Integration)
(`imagegen` module, Apache License 2.0) and has since been substantially
rewritten and extended — see [`NOTICE`](https://github.com/eigger/imagespec/blob/main/NOTICE) for the full attribution.

## Status

Stable 1.0.0 release, used in production by the two integrations above. The
public Python API and payload contract are stable within the 1.x series. Every
registered element is pinned by a golden-image test (the README previews below
double as the goldens), alongside unit tests for palettes, rotation, dithering,
template-string coercion and error handling; CI runs the suite on Python
3.13/3.14 and against the lowest supported dependency versions.

## Design

- **No framework dependency.** The core never imports Home Assistant. Anything
  host-specific is injected through `RenderContext`:
  - `font_resolver(name) -> path | None` — e.g. an integration's
    `hass.config.path("www/fonts")` lookup.
  - `history_provider(entity_ids, start, end) -> states` — for the `plot`
    element (HA recorder). Optional.
  - `palette` — the device's supported colors (see below).
- **Registry dispatch.** Each element `type` is a handler registered with
  `@element("type")` in `imagespec/elements/`, replacing the original giant
  `if/elif` chain. Adding an element = adding a function.
- **`RenderState`.** Threaded through handlers; holds the (reassignable) `img`
  and the `pos_y` flow cursor.
- **Device-dependent palette** (`RenderContext.palette`), *not* unified — panels
  support different colors. Define it as a **list of the colors the device
  supports** (names, HEX, or RGBA tuples):

  ```python
  RenderContext(palette=["black", "white", "red"])  # names
  RenderContext(palette=["#000000", "#ffffff", "#ff0000"])  # HEX
  RenderContext(palette=[(0, 0, 0), (255, 255, 255)])  # RGBA tuples
  ```

  Shorthand names are optional convenience for common panels: `"2"`/`"bw"`,
  `"3"`/`"bwr"`, `"4"`, `"7"`/`"acep"`. Any requested color in a payload is then
  quantized to the nearest color in this list — on a 2-color device `red`
  becomes black; on 4-color a blue `#1e90ff` becomes white; on 7-color it stays
  blue. Elements are drawn in full color and this mapping is applied to the whole
  image once at the end of `render()` (dithered or flat, per `dither` — see
  *Dithering*).
- **Device-dependent rotation** (`rotate_mode`), *not* unified — the two
  behaviours are physically different:
  - `"canvas"` (fixed-resolution e-ink panels, e.g. ble-esl): the drawing
    surface rotates; output stays `width×height`.
  - `"image"` (variable-size label printers, e.g. niimbot): the drawing rotates;
    output dimensions swap.

## Usage

```python
from imagespec import render, validate, RenderContext, RenderError

ctx = RenderContext(
    font_resolver=my_font_lookup,  # optional
    history_provider=my_history_lookup,  # optional, only for `plot`
)

# Optional: lint the payload first — typos, unknown colours, missing required keys (with "did you mean" hints)
for issue in validate(payload):
    print(issue)  # e.g. "[2].colr: unknown key for 'text' (did you mean 'color'?)"

try:
    image = render(payload, width=296, height=128, rotate=0, background="white", context=ctx)  # -> PIL.Image (RGB)
    # render(..., strict=True) runs the same validation first and raises one RenderError listing every issue
except RenderError as err:
    print(err)  # "[1].elements[0]: <message>" when one element failed (err.path / err.message separately)
```

Run the smoke test (no fonts required):

```bash
pip install -e .
python examples/smoke_test.py
```

## Development & testing

```bash
pip install -e ".[dev,datamatrix]"   # dev pulls in numpy, so both dither paths are exercised
pytest                 # unit + golden-image tests: every element, palettes, rotation, dither, errors
pytest --update-golden # rewrite the golden PNGs after an intentional rendering change
ruff check . && ruff format --check .   # lint + format
mypy                   # type-check src/ (the package ships py.typed)
python -m build        # build sdist + wheel (bundles fonts/icons)
```

CI runs on every push/PR (`.github/workflows/ci.yml`): ruff lint+format, mypy, the test
suite on Python 3.13/3.14 (plus a lowest-pinned-dependencies job), and a build that asserts the bundled fonts/icons
are present in the wheel.

**Releasing**: add a version section to [`CHANGELOG.md`](CHANGELOG.md), bump
`version` in `pyproject.toml`, update `schema_version` when the payload
contract changes, and merge. Then create and publish a GitHub Release for the
matching `v<version>` tag. The release workflow checks the tag against project
metadata before building and publishing to PyPI with trusted publishing.

The test matrix (`tests/test_elements.py`) asserts it covers *every* registered
element type, so adding a new `@element(...)` without a sample fails the suite —
keeping coverage exhaustive by construction.

**Golden images** (`tests/test_golden.py`) pin the actual pixels: each element
preview in `examples/elements/` is re-rendered from
`examples/generate_element_previews.py` and compared exactly, and
`tests/golden/` holds targeted scenes (every dither method, per-element dither
phase, rotation modes, less-common handler options). A rendering change that is
intended is committed by running `pytest --update-golden` and checking in the
PNGs; an unintended one fails CI with the pixel count and a diff image.
Text in the goldens is laid out with Pillow's `BASIC` engine
(`RenderContext(layout_engine=...)`) so the same Pillow release renders them
identically on every OS; set `IMAGESPEC_GOLDEN_TOLERANCE=0.05` to allow 5 % of
pixels to differ under a different Pillow/FreeType or python-barcode release.

**Robustness built in:**

- Each handler error is wrapped with element context — you get
  `error rendering element #3 (type 'text'): ...`, not a raw PIL traceback.
- `render()` validates `rotate`/`rotate_mode`/size and rejects non-dict elements;
  unknown element types are warned-and-skipped. Unknown *keys* are ignored
  unless you pass `strict=True` (or call `imagespec.validate()` yourself).
- `dlimg` only allows `http(s)`/`data:` URLs by default; local paths require
  `RenderContext(allow_local_images=True)`. Network failures become `RenderError`.
  Downloads are streamed and abort past `max_image_bytes` (20 MB default);
  `image_cache_ttl=<seconds>` reuses a fetched image across renders (off by
  default so camera snapshots are never served stale), and `image_fetcher`
  lets the host supply its own `url -> bytes`.
- Clear errors for missing required args, invalid barcode symbology, malformed
  `polygon` points, and a `diagram` too small for its bars.
- Template-friendly input: numeric keys (`x`, `size`, `progress`, ...) accept
  strings (`"42"`, `"3.5"`) and boolean flags (`visible`, `show_percentage`, ...)
  accept `"False"`/`"off"`/`"0"`, as Home Assistant templates produce them. A
  non-numeric string fails with `'x' must be a number, got 'oops'` naming the
  element.

## Elements

> [!TIP]
> **Copy-paste examples for every element:** [`docs/elements.md`](docs/elements.md)

| Preview | Element | Module | Notes |
|:---:|---|---|---|
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/line.png) | `line` | shapes | + dashed lines |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/rectangle.png) | `rectangle` | shapes | |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/rectangle_pattern.png) | `rectangle_pattern` | shapes | |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/circle.png) | `circle` | shapes | |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/ellipse.png) | `ellipse` | shapes | |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/arc.png) | `arc` | shapes | |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/polygon.png) | `polygon` | shapes | |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/gauge.png) | `gauge` | shapes | |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/text.png) | `text` | text | + rotation, background box |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/text_box.png) | `text_box` | text | |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/multiline.png) | `multiline` | text | |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/new_multiline.png) | `new_multiline` | text | fit-to-width/height autosize |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/text_fit.png) | `text_fit` | text | fit text into a fixed box: shrink font / ellipsis / wrap |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/table.png) | `table` | text | |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/qrcode.png) | `qrcode` | codes | `eclevel`, pixel `width`/`height` sizing |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/barcode.png) | `barcode` | codes | |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/datamatrix.png) | `datamatrix` | codes | optional dep `pyStrich` (`imagespec[datamatrix]`) |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/icon.png) | `icon` | media | Material Design Icons (default) **+ Font Awesome Free** (`fa:`/`fas:`/`far:`/`fab:`); needs bundled `icons/` assets |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/dlimg.png) | `dlimg` | media | + fit modes (stretch/fit/fill/contain) |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/diagram.png) | `diagram` | charts | bar chart |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/plot.png) | `plot` | charts | needs `history_provider`; + area_fill, xlegend |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/progress_bar.png) | `progress_bar` | charts | + rounded corners |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/pie.png) | `pie` | charts | pie / donut (`inner_radius`) |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/sparkline.png) | `sparkline` | charts | compact axis-less line from inline values |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/rich_text.png) | `rich_text` | text | inline spans: icon + text + color on one line |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/group.png) | `group` | layout | container: child elements at an offset, clipped, optionally rotated |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/column.png) | `stack` / `row` / `column` | layout | auto-layout: packs children along an axis (gap/padding/justify/align), flexbox-style, with optional Tailwind-like `class` shorthand |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/legend.png) | `legend` | widgets | color-swatch ↔ label rows (vertical/horizontal) for `pie`/`plot` |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/star_rating.png) | `star_rating` | widgets | full/half/empty stars for rating labels |
| ![](https://raw.githubusercontent.com/eigger/imagespec/main/examples/elements/battery.png) | `battery` | widgets | vector battery gauge with proportional fill |

`render(..., dither=True|"atkinson"|…)` dithers the whole output (15 algorithms
+ `none`; see [`docs/dithering.md`](docs/dithering.md)) and **any element** can
carry its own `dither` bool/method to override it just for itself.

## Payload specification

Payloads are specified as a list (sequence) of dictionary elements, which can be easily authored in YAML or JSON. Each element requires a `type` string and varying geometric/styling attributes.

> [!TIP]
> **Element-by-element YAML examples:** [`docs/elements.md`](docs/elements.md)
>
> **Generating payloads with an LLM?** See [`docs/authoring.md`](docs/authoring.md) — a
> **self-contained authoring guide** you can paste straight into an AI's context. It
> covers the output contract, the layout decision model (`stack`/`row`/`column` vs
> `group` vs absolute coordinates), the Tailwind-like `class` shorthand, common
> pitfalls (e.g. the YAML "one key per line" trap), device canvas sizes, and full
> worked examples — every example verified by actually rendering it.

### Element reference & JSON Schema

Every element's keys, types and defaults are declared once, next to its handler
(`@element("circle", fields=[num("x", required=True), ...])`), and everything
else is generated from those declarations:

- [`docs/reference.md`](docs/reference.md) — the full key reference
- [`schema/elements.json`](schema/elements.json) — JSON Schema (draft 2020-12)
  for a whole payload; use it in an editor or validator to catch typos, missing
  required keys and bad enum values before rendering
- template-string coercion (`"42"` → `42`, `"False"` → `False`) — driven by the
  declared key types
- `imagespec.validate(payload)` — the same checks at runtime, without a JSON
  Schema library: returns `[Issue(path, message), ...]` such as
  `[2].fil: unknown key for 'circle' (did you mean 'fill'?)` (typos get a
  "did you mean" hint), tolerating template strings and `null` exactly as
  `render()` does. `render(..., strict=True)` runs it first and
  raises `RenderError` listing every issue.

`python scripts/export_schema.py` regenerates the two files; CI fails if they
drift from the declarations, and a guard test fails if a handler reads a key it
did not declare. The web payload editor in
[`eigger.github.io`](https://github.com/eigger/eigger.github.io) keeps its
supported subset in `schema/editor_types.json`.

### Common attributes
- **Colors**: names (`black`, `white`, `red`, `green`, `blue`, `orange`, `yellow`, any CSS name) or HEX (`#FF0000`, `#f00`); quantized to the device palette at the end of `render()`.
- **Coordinates**: pixels from the top-left corner `(0, 0)`.
- **Numbers and booleans** also accept the string forms Home Assistant templates produce.
- **`visible`**, **`dither`**, **`class`** / **`layout`** are accepted by every element (see the reference).

### Dithering

`imagespec` can dither full-color content onto limited palettes (2-color B/W,
3-color BWR, 7-color ACeP, …). Besides flat nearest-color mapping (`none`), it
ships **15 e-ink-oriented dither algorithms** (Floyd–Steinberg, Atkinson,
Jarvis, Stucki, Burkes, Sierra family, Stevenson–Arce, Bayer 2/4/8/16,
clustered-dot 4/8).

- `dither=False` / `"none"` (default) → flat nearest color
- `dither=True` / `"floyd"` → Floyd–Steinberg (backward compatible)
- `dither="atkinson"` / `"bayer8"` / … → any name in `DITHER_METHODS`

Per-element overrides accept the same bool or method string. Full gallery,
API notes, and per-algorithm tiles: **[`docs/dithering.md`](docs/dithering.md)**.

```bash
python examples/generate_dither_methods.py   # all-method grids + tiles
```

![All dither methods on B/W](https://raw.githubusercontent.com/eigger/imagespec/main/examples/dither_methods_bw.png)

Without dithering, colors snap to the nearest palette entry and gradients band
badly. Dithering trades spatial resolution for perceived depth.

#### 1. Gradient & 3D Shading
Dithering creates a natural halftone pattern that simulates smooth shading and eliminates color banding.
![Gradient Dithering Comparison](https://raw.githubusercontent.com/eigger/imagespec/main/examples/dither_comparison_gradient.png)

#### 2. Font Rendering (Anti-aliasing vs. Dithering)
> [!IMPORTANT]
> **Guidelines for Text:** Avoid dithering on text layers. Dithering anti-aliased font edges creates tiny dot noise, which severely degrades readability on low-resolution e-ink screens. For sharp text, use direct quantization or disable anti-aliasing (`fontmode = "1"`). The built-in `text` element enforces `fontmode = "1"` for this reason.
![Font Dithering Comparison](https://raw.githubusercontent.com/eigger/imagespec/main/examples/dither_comparison_font.png)

#### 3. Charts & Solid Fills
Dithering is useful when you have solid color regions (like pie slices or bar diagrams) in colors outside your device palette (e.g. orange on a black/white screen). Dithering simulates these colors with dot patterns to help distinguish segments, though it introduces some edge noise.
![Chart Dithering Comparison](https://raw.githubusercontent.com/eigger/imagespec/main/examples/dither_comparison_chart.png)

#### How palette mapping works
Every element is drawn in **full color**, and the whole image is mapped to
`context.palette` **once at the end** of `render()`. The `dither` flag only picks
*how* that single mapping happens:

- `dither=True` / `"floyd"` → Floyd–Steinberg (or pass another method name)
- `dither=False` / `"none"` (default) → flat nearest color

Either way the output is **strictly on-palette**. Because mapping is deferred,
in-palette colors (e.g. black text on white) stay crisp under dithering —
error diffusion spreads no error when a pixel already equals a palette color —
while the text guidance above still applies to *off-palette* text you choose to
dither.

```python
ctx = RenderContext(palette="bw")
img = render(payload, 296, 128, dither=True, context=ctx)
img = render(payload, 296, 128, dither="atkinson", context=ctx)
# orange/green/blue pie slices -> different dot patterns, not one black blob
```

#### Per-element `dither` override
Any element may carry its own `dither: true`/`false`/method name to override the
global flag just for itself — so you can dither only the parts that benefit
(photos, charts) and keep the rest flat (labels, QR codes), in a single render:

```yaml
- type: dlimg          # this photo -> halftone
  url: "https://…/photo.png"
  xsize: 100
  ysize: 100
  dither: atkinson
- type: pie            # this chart -> ordered screen
  x: 60
  y: 60
  radius: 40
  values: "Gas,30,orange;Water,25,blue;Elec,45,red"
  dither: bayer8
- type: text           # left flat regardless of the global flag
  x: 10
  y: 110
  value: "Energy mix"
```

An element with an explicit `dither` is rendered in isolation and mapped to the
palette immediately (then composited in payload order), so its choice survives
the final whole-image pass. Elements without the key follow the global `dither`
argument. (QR/barcode and black text are pure palette colors, so they stay crisp
under the global flag anyway — set `dither: false` only for *off-palette* content
you want kept solid.)

#### Device samples
Both labels below mix crisp content (text, QR, barcode) with charts authored in
off-palette colors and marked `dither: true` — the charts become halftones so
their segments stay distinguishable, while everything else stays sharp.

**Electronic shelf label — 3-color (black / white / red):**

![ESL 3-color dithering sample](https://raw.githubusercontent.com/eigger/imagespec/main/examples/dither_esl_3color.png)

**Label printer — 2-color (black / white):**

![Label printer 2-color dithering sample](https://raw.githubusercontent.com/eigger/imagespec/main/examples/dither_label_2color.png)

Regenerate them with:
```bash
python examples/generate_dither_labels.py
```

Older Floyd-vs-none comparison boards:
```bash
python examples/compare_dither.py
```

## Fonts & assets

Bundled in the package (offline baseline, ~12 MB total):

- `icons/materialdesignicons-webfont.ttf` + `_meta.json` — `icon`'s default set
  (`mdi:` prefix, or no prefix).
- `icons/fontawesome-free-{solid,regular,brands}.otf` + trimmed metadata —
  `icon`'s second set (`fa:`/`fas:`/`far:`/`fab:` prefix).
- `fonts/NotoSansKR-Regular.ttf` — the only bundled font, and the default for
  every payload (`RenderContext.default_font`).

Anything else is resolved at runtime, in order: `font_resolver` (host) →
bundled font of the same basename → bundled default. Helpers in
`imagespec.resolvers`:

- `directory_resolver(dir)` — look up fonts in a host directory (e.g. `www/fonts`).
- `caching_resolver(cache_dir, sources)` — **download on first use, cache to
  disk, reuse offline** (internet needed only once per font).
- `google_fonts_resolver(cache_dir, families=None)` — a `caching_resolver`
  preset over verified-license Google Fonts (`ofl/` directory → all SIL OFL
  1.1), covering scripts the bundled Noto Sans KR doesn't: Japanese, Simplified/
  Traditional Chinese, Arabic, Thai, plus a broader Latin/Cyrillic/Greek family.
  See `GOOGLE_FONTS_SOURCES` for the exact list.
- `chain_resolvers(a, b, ...)` — try several in order.

The package bundles only this baseline: decorative or other-script fonts are
better downloaded-and-cached (`google_fonts_resolver`/`caching_resolver`) or
served by the host (e.g. Home Assistant's `www/fonts`) through `font_resolver`.
Only fonts with a verifiable license are bundled — see *Licensing & attribution*
below.

### Licensing & attribution

`imagespec` is **MIT AND Apache-2.0** (see the `license` field in
`pyproject.toml`) — not pure MIT — because it's a combined work:

| Component | License | Source |
|---|---|---|
| imagespec's own code/modifications | MIT | [`LICENSE`](https://github.com/eigger/imagespec/blob/main/LICENSE) |
| Rendering engine origin (registry dispatch, element handlers) | Apache License 2.0 | [OpenEPaperLink Home Assistant Integration](https://github.com/OpenEPaperLink/Home_Assistant_Integration) — see [`NOTICE`](https://github.com/eigger/imagespec/blob/main/NOTICE) |
| `icons/materialdesignicons-webfont.ttf` (+ metadata) | Apache License 2.0 | [Pictogrammers / Templarian MaterialDesign-Webfont](https://github.com/Templarian/MaterialDesign-Webfont) |
| `icons/fontawesome-free-*.otf` (+ metadata) | SIL OFL 1.1 (fonts) / CC BY 4.0 (icons) | [Font Awesome Free](https://github.com/FortAwesome/Font-Awesome) |
| `fonts/NotoSansKR-Regular.ttf` | SIL Open Font License 1.1 | [Google Noto Fonts](https://fonts.google.com/noto/specimen/Noto+Sans+KR) |

The engine was originally adapted from OpenEPaperLink's code and has since been
substantially rewritten and extended (palette/color model, device-aware
rotation, dithering, new elements, ...); [`NOTICE`](https://github.com/eigger/imagespec/blob/main/NOTICE) documents this per
the Apache License's redistribution terms. Full license texts ship in the
package: [`LICENSE-APACHE-2.0`](https://github.com/eigger/imagespec/blob/main/LICENSE-APACHE-2.0) (covers the engine origin
and the MDI font), [`icons/LICENSE`](https://github.com/eigger/imagespec/blob/main/src/imagespec/icons/LICENSE) (a co-located
copy for the icons directory), [`icons/LICENSE-FONTAWESOME`](https://github.com/eigger/imagespec/blob/main/src/imagespec/icons/LICENSE-FONTAWESOME)
(Font Awesome Free — also notes brand-icon trademark restrictions), and
[`fonts/OFL.txt`](https://github.com/eigger/imagespec/blob/main/src/imagespec/fonts/OFL.txt).

## Integrating into a host

Build a `RenderContext` (palette, font lookup, history provider), call
`render()`, translate `RenderError` into the host's error type — about 60 lines.
[`docs/integrating.md`](docs/integrating.md) walks through it with a Home
Assistant adapter.
