Metadata-Version: 2.4
Name: pixtxt
Version: 0.1.4
Summary: Python SDK for reading and writing PixTXT documents
Author: PixTXT Maintainers
License-Expression: MIT
Project-URL: Homepage, https://github.com/Fantety/PixTXT
Project-URL: Repository, https://github.com/Fantety/PixTXT
Project-URL: Issues, https://github.com/Fantety/PixTXT/issues
Keywords: pixel-art,animation,sprite-sheet,llm,text-format
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pillow>=10.0
Provides-Extra: dev
Requires-Dist: build>=1.4.2; extra == "dev"
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: twine>=6.2.0; extra == "dev"
Dynamic: license-file

<div align="center">
  <h1>PixTXT Python SDK</h1>
  <p>
    <img src="icon-black.svg" alt="PixTXT Logo" width="220" />
  </p>
  <p><i>Python SDK for parsing, validating, optimizing, and exporting PixTXT documents.</i></p>
  <p>
    <a href="README_CN.md">🇨🇳 中文</a> |
    <a href="README.md">🇺🇸 English</a>
  </p>
</div>

---

## What is PixTXT?

PixTXT is a text format for pixel art designed for human readability, low token cost, and reliable LLM generation.

- Readable: `rows` encoding is easy to inspect and edit by eye.
- Token-efficient: `rle` encoding compresses repeated runs.
- Structured: supports `@meta`, `@palette`, `@image`, `@anim`, and `@sheet`.
- Engineering-friendly: works well with git diff, CI validation, and automation.

Minimal example:

```txt
PixTXT 1

@palette global
A:#111111
B:#22AAFF

@image dot 2x2 rows
AB
B.
```

## Documentation

- PixTXT text format (English): `docs/pixtxt-format.md`
- PixTXT text format (中文): `docs/pixtxt-format.zh-CN.md`
- PyPI release guide: `docs/pypi-release-guide.md`

## Install

```bash
python -m pip install -e .
```

For local development and tests:

```bash
python -m pip install -e .[dev]
```

## Quickstart

```python
from pixtxt import loads, validate

doc = loads("""PixTXT 1

@palette global
A:#111111

@image dot 1x1 rows
A
""")
issues = validate(doc)

print(f"images={len(doc.images)} issues={len(issues)}")
```

## Typical Workflow

1. Parse/load PixTXT.
2. Validate for issues.
3. Optimize (`rows`/`rle`) if needed.
4. Dump `.pxt` output.
5. Export PNG/GIF assets.

## Print PixTXT in Code

Use `dumps(doc)` when you want PixTXT text in memory (for logging, prompting an LLM, or sending over an API).

```python
from pixtxt import dumps, from_image

doc = from_image("assets/source.png")
text = dumps(doc)
print(text)
```

Use `dump(doc, path)` when you want to save a `.pxt` file.

```python
from pixtxt import dump, from_image

doc = from_image("assets/source.png")
dump(doc, "out/source.pxt")
```

Print optimized PixTXT:

```python
from pixtxt import dumps, from_image, optimize

doc = from_image("assets/source.png")
optimized = optimize(doc, mode="token")
print(dumps(optimized))
```

## SDK Guide

### Core Data Model

Main objects in `pixtxt.models`:

- `PixTXTDocument`
  - `meta: dict[str, str]`
  - `palettes: list[PaletteDef]`
  - `images: list[ImageDef]`
  - `anims: list[AnimDef]`
  - `sheets: list[SheetDef]`
  - `extensions: list[tuple[str, list[str]]]`
- `PaletteDef(name, colors)`
- `ImageDef(image_id, width, height, encoding, payload, pixels, palette_name)`
- `AnimDef(anim_id, fps, loop, frames)`
- `SheetDef(sheet_id, cell_width, cell_height, grid_width, grid_height, items)`
- `SheetItem(image_id, x, y)`

`ImageDef.pixels` is the canonical internal matrix representation.

### Parse and Validate

```python
from pixtxt import loads, validate

text = """PixTXT 1
@palette global
A:#000000
@image dot 1x1 rows
A
"""

doc = loads(text, strict=False)
issues = validate(doc, strict=False)

if issues:
    for issue in issues:
        print(issue.code, issue.location, issue.message)
```

Strict mode behavior:

- `loads(..., strict=True)`: rejects unknown sections and unknown `@meta` keys.
- `validate(..., strict=True)`: reports strict-only semantic issues.

### Read and Write Files

```python
from pixtxt import load, dump

doc = load("assets/hero.pxt", strict=False)
dump(doc, "out/hero_copy.pxt", encoding_preference="auto")
```

`encoding_preference`:

- `"auto"`: preserve image encoding preference per image.
- `"rows"`: force rows output.
- `"rle"`: force rle output.

### Optimize for Token Cost

```python
from pixtxt import load, optimize, dump

doc = load("assets/anim.pxt")
optimized = optimize(doc, mode="token")
dump(optimized, "out/anim_optimized.pxt")
```

Optimizer guarantees matrix-equivalent output.

### Render and Export Assets

Single image to PNG:

```python
from pixtxt import load, to_png

doc = load("assets/hero.pxt")
to_png(doc, "hero_idle", "out/hero_idle.png")
```

Sheet to PNG:

```python
from pixtxt import load, sheet_to_png

doc = load("assets/hero.pxt")
sheet_to_png(doc, "atlas", "out/atlas.png")
```

Animation to GIF:

```python
from pixtxt import load, anim_to_gif

doc = load("assets/hero.pxt")
anim_to_gif(doc, "walk", "out/walk.gif")
```

### Import Existing Images

Single static image:

```python
from pixtxt import from_image, dump

doc = from_image("assets/source.png", palette_mode="global")
dump(doc, "out/source.pxt")
```

Animated GIF/APNG (import all frames and auto-create `@anim`):

```python
from pixtxt import from_image, dump

doc = from_image(
    "assets/blink.gif",
    frame_mode="all",
    anim_id="blink",
    frame_fps=12,  # fallback when source timing is unavailable
    frame_loop=1,
    use_source_timing=True,
)
dump(doc, "out/blink_anim.pxt")
```

Sprite sheet slicing (grid -> images + `@sheet`):

```python
from pixtxt import from_image, dump

doc = from_image(
    "assets/atlas.png",
    cell=(16, 16),
    grid=(8, 4),
    sheet_id="actors",
    naming="rowcol",  # or "index"
)
dump(doc, "out/atlas_sheet.pxt")
```

Create `@anim` in code (common after sheet slicing):

```python
from pixtxt import AnimDef, anim_to_gif, dump, from_image

doc = from_image(
    "assets/run-Sheet.png",
    cell=(34, 50),
    grid=(8, 1),
    sheet_id="actors",
    naming="rowcol",  # run-Sheet_r0_c0 ... run-Sheet_r0_c7
)

doc.anims.append(
    AnimDef(
        anim_id="walk",
        fps=12,
        loop=1,
        frames=[
            "run-Sheet_r0_c0",
            "run-Sheet_r0_c1",
            "run-Sheet_r0_c2",
            "run-Sheet_r0_c3",
            "run-Sheet_r0_c4",
            "run-Sheet_r0_c5",
            "run-Sheet_r0_c6",
            "run-Sheet_r0_c7",
        ],
    )
)

dump(doc, "out/run_sheet_with_anim.pxt")
anim_to_gif(doc, "walk", "out/walk.gif")
```

### Errors

The SDK exposes typed errors:

- `PixTXTParseError`: text parsing errors.
- `PixTXTSemanticError`: semantic consistency errors.
- `PixTXTValidationError`: validation aggregation category.

For reporting-style checks, prefer `validate(doc)` which returns `list[Issue]`.

## End-to-End Recipes

### A) Static image -> PixTXT -> PNG

```python
from pixtxt import from_image, dump, load, to_png

doc = from_image("assets/hero.png")
dump(doc, "out/hero.pxt")

doc2 = load("out/hero.pxt")
to_png(doc2, doc2.images[0].image_id, "out/hero_roundtrip.png")
```

### B) Animated GIF -> PixTXT anim -> GIF

```python
from pixtxt import from_image, dump, anim_to_gif

doc = from_image(
    "assets/run.gif",
    frame_mode="all",
    anim_id="run",
    use_source_timing=True,
    frame_fps=12,
    frame_loop=1,
)

dump(doc, "out/run_anim.pxt")
anim_to_gif(doc, "run", "out/run_rebuilt.gif")
```

### C) Sprite sheet -> Images + Sheet -> Manual Anim -> GIF

```python
from pixtxt import AnimDef, anim_to_gif, dump, from_image

doc = from_image(
    "assets/run-Sheet.png",
    cell=(34, 50),
    grid=(8, 1),
    sheet_id="actors",
    naming="rowcol",
)

doc.anims.append(
    AnimDef(
        anim_id="walk",
        fps=12,
        loop=1,
        frames=[
            "run-Sheet_r0_c0",
            "run-Sheet_r0_c1",
            "run-Sheet_r0_c2",
            "run-Sheet_r0_c3",
            "run-Sheet_r0_c4",
            "run-Sheet_r0_c5",
            "run-Sheet_r0_c6",
            "run-Sheet_r0_c7",
        ],
    )
)

dump(doc, "out/run_sheet_with_anim.pxt")
anim_to_gif(doc, "walk", "out/walk.gif")
```

## Troubleshooting

### `Animation '<id>' not found`

Reason: your document does not contain a matching `@anim` section.

Fix:

- Inspect available animation IDs:

```python
print([a.anim_id for a in doc.anims])
```

- Add `AnimDef(...)` programmatically if needed (see recipe C).

### `image size WxH does not match cell*grid size ...`

Reason: `cell[0] * grid[0] != image_width` or `cell[1] * grid[1] != image_height`.

Fix: adjust `cell` and `grid` so they multiply exactly to source image dimensions.

### TestPyPI install fails with dependency resolution

Use both indexes so dependencies can come from PyPI:

```bash
uv add --index https://test.pypi.org/simple/ --index https://pypi.org/simple pixtxt
```

### Upload fails: `filename was previously used`

Bump package version (`pyproject.toml`) and re-build/re-upload.

## Parse / Validate / Dump

```python
from pathlib import Path

from pixtxt import dump, dumps, loads, validate

text = """PixTXT 1

@meta
title: Demo

@palette global
A:#111111
B:#22AAFF

@image hero 2x2 rows
AB
B.
"""

doc = loads(text)
issues = validate(doc, strict=True)
assert issues == []

serialized = dumps(doc)
Path("out").mkdir(parents=True, exist_ok=True)
dump(doc, "out/demo.pxt", encoding_preference="auto")

print(serialized.splitlines()[0])  # PixTXT 1
```

## PNG / GIF Export

```python
from pathlib import Path

from pixtxt import anim_to_gif, loads, sheet_to_png, to_png

doc = loads("""PixTXT 1

@palette global
A:#111111
B:#22AAFF

@image hero_idle 2x2 rows
AB
B.

@image hero_blink 2x2 rows
BA
.A

@anim blink fps=8 loop=1
use: hero_idle, hero_blink

@sheet atlas
cell:2x2
grid:2x1
item: hero_idle at 0,0
item: hero_blink at 1,0
""")

Path("out").mkdir(parents=True, exist_ok=True)
to_png(doc, "hero_idle", "out/hero_idle.png")
sheet_to_png(doc, "atlas", "out/atlas.png")
anim_to_gif(doc, "blink", "out/blink.gif")
```

## Strict Mode Notes

- `loads(..., strict=True)` rejects unknown section headers and unknown `@meta` keys.
- `validate(..., strict=True)` reports `META_UNKNOWN_KEY` for non-standard `@meta` keys.
- In non-strict mode, unknown sections are preserved as `doc.extensions`.

## API Signatures

| Function | Signature |
| --- | --- |
| `load` | `load(path: str | pathlib.Path, strict: bool = False) -> PixTXTDocument` |
| `loads` | `loads(text: str, strict: bool = False) -> PixTXTDocument` |
| `dump` | `dump(doc: PixTXTDocument, path: str | pathlib.Path, encoding_preference: str = "auto") -> None` |
| `dumps` | `dumps(doc: PixTXTDocument, encoding_preference: str = "auto") -> str` |
| `validate` | `validate(doc: PixTXTDocument, strict: bool = False) -> list[Issue]` |
| `optimize` | `optimize(doc: PixTXTDocument, mode: str = "token") -> PixTXTDocument` |
| `from_image` | `from_image(path, palette_mode="global", frame_mode="first", cell=None, grid=None, sheet_id=None, anim_id=None, frame_fps=8, frame_loop=1, naming="index", use_source_timing=False) -> PixTXTDocument` |
| `to_png` | `to_png(doc: PixTXTDocument, image_id: str, out_path: str | pathlib.Path) -> None` |
| `sheet_to_png` | `sheet_to_png(doc: PixTXTDocument, sheet_id: str, out_path: str | pathlib.Path) -> None` |
| `anim_to_gif` | `anim_to_gif(doc: PixTXTDocument, anim_id: str, out_path: str | pathlib.Path) -> None` |
