Metadata-Version: 2.4
Name: malight
Version: 0.2.0
Summary: malight - SVG drawing toolkit ported and enhanced from Shenbi Maliang, with PS/AI-style filters and a plugin extension mechanism
Author-email: weigang <gang.wei@qq.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/weigang75/malight
Project-URL: Repository, https://github.com/weigang75/malight
Project-URL: Issues, https://github.com/weigang75/malight/issues
Keywords: svg,drawing,vector,2d,graphics,filter,magicpen,malight
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: export
Requires-Dist: cairosvg; extra == "export"
Provides-Extra: fontpath
Requires-Dist: fontTools; extra == "fontpath"
Requires-Dist: svgpathtools; extra == "fontpath"
Provides-Extra: full
Requires-Dist: cairosvg; extra == "full"
Requires-Dist: fontTools; extra == "full"
Requires-Dist: svgpathtools; extra == "full"
Dynamic: license-file

<!-- Generated by tools/pypi_readme.py from README.en.md for PyPI.
     Do not edit by hand: edit README.en.md (or fix the module pages) and re-run. -->

# MaLight — an English drawing toolkit (the English edition of Shenbi Maliang)

[简体中文](https://github.com/weigang75/malight/blob/main/README.md) ｜ **English**

A complete English rewrite of *Shenbi Maliang* ("Magic Light"). The product and
class name is **MaLight**; the package name **malight** = **Ma**(gic) + **light**
(lower-case by Python convention, used for `import`): it echoes the original
Chinese name and means "light of magic" while staying shorter than `magiclight`.

English-only API, **bilingual comments and examples in the source**, and
**one method name per SVG element** (`circle` / `rect` / `text` / `path` / `g` /
`clipPath` / `linearGradient` …), one `.py` file per class. Drawing needs **zero
third-party dependencies** thanks to a tiny self-contained SVG backend
(PNG/PDF export optionally uses `cairosvg`; text-to-path optionally uses
`fontTools` and `svgpathtools`).

```bash
pip install malight                 # core: no dependencies
pip install malight[export]         # add PNG / PDF export (cairosvg)
pip install malight[full]           # everything optional
```

> **0.1.0 (first release)**: package renamed magicpen → **malight**; new filter
> factory `pen.fx` (Photoshop/Illustrator-style filters, chainable); new extension
> mechanism `malight.ext`. Old scripts with `import magicpen` still run (a
> compatibility shim ships in the repo), and
> `python -m malight.compat old.py new.py` migrates them.
>
> Also in 0.1.0: the "enum + string" dual style everywhere (fonts / colours /
> option values, so typos can't slip through); new path helper package
> `malight.pathkit` (inspect the structure, drag anchors and control points,
> insert/delete points, save/load point data); every class file ships a runnable
> example; exports print the **full path** so you can copy it straight into a
> shell; runtime localisation `malight.i18n` (English by default, one line to
> switch to Chinese); a bilingual doc pair `xxx.zh.md` / `xxx.en.md` is generated
> next to every module — **docs travel with the code**; code comments and docstrings
> are bilingual; **English pages contain no Chinese at all**.
> Elements can be **tweaked after creation**: `el.set_font_size(36)` or
> `el.font_size(36)` (no-arg reads, one-arg sets, chainable; parameter names
> match the creating call, `get_font_size()` reads back); plus
> `paint_order=PaintOrder.STROKE` (stroke-first outlined text) and the new
> `fx_engrave()` carved-in filter; plus `PageSetup` for PDF paper/margins
> (`pen.export_pdf(page=PageSetup("A4", margin=24))` — **Chrome engine only**;
> PNG is a screen screenshot and has no notion of paper). Filter / text-style
> module docs now embed rendered **effect preview images**
> (generated by `tools/gen_previews.py`).
>
> **0.2.0 (current)**: element templates `el.to_template()` + `clone()` (turn an
> element into a `<symbol>` template in place, stamp out `<use>` copies);
> **asset embedding** — fonts are subsetted automatically by default (only the
> glyphs actually drawn are packaged), with whole-file embed and local-link
> modes; images can be embedded or linked; new `pen.svg_image()` to place an SVG
> (vector, with editable text and colours); new `import_svg_as_group` imported
> group (movable / rotatable / scalable, filters supported); new coloured console
> message helpers (`print_red` / `print_green` … in `malight.tools`).

## Quick start

```python
from malight import Malight, Color

pen = Malight("hello", width=800, height=600)
pen.set_background_color("#f8f9fa")
pen.circle(200, 150, 80, fill_color=Color.RGB(30, 144, 255),   # <circle>
           stroke_color="navy", stroke_width=3)
pen.rect(300, 60, 160, 110, corner_radius=12, fill_color="gold")
pen.text(400, 50, "Hello malight", font_size=32, bold=True)
pen.circle(600, 300, 70, fill_color="#e63946",
           filter=pen.fx.shadow(6, 8, 6))          # a filter in one line
pen.finish()          # write the SVG
pen.export_png()      # optional PNG (needs cairosvg; filters need a browser)
```

`Malight` and `MagicPen` are two names for the same class — use either.
Deeper usage lives in [board/core.en.md](https://github.com/weigang75/malight/blob/main/malight/board/core.en.md).

## Method name = SVG element name

| malight method | SVG element | Notes |
|---|---|---|
| `pen.circle(x, y, r)` | `<circle>` | circle |
| `pen.ellipse(x, y, (rx,ry), rotate=)` | `<ellipse>` | ellipse (with rotation) |
| `pen.rect(x, y, w, h)` | `<rect>` | rectangle |
| `pen.line(p1, p2)` | `<line>` | line segment |
| `pen.polyline(pts)` / `pen.polygon(pts)` | `<polyline>` / `<polygon>` | polyline / polygon |
| `pen.text(x, y, s)` | `<text>` | text |
| `pen.textPath(pts, s)` | `<textPath>` | text along a path |
| `pen.path(...)` | `<path>` | paths (Bézier / arcs / turtle drawing / boolean ops) |
| `pen.image(file, x, y)` | `<image>` | bitmap (inlined as base64) |
| `pen.svg_image(file, x, y)` | `<image>` | SVG file: vector, and its text/colours can be edited |
| `pen.g()` | `<g>` | group |
| `pen.symbol(id)` + `pen.use(id, x, y)` | `<symbol>` + `<use>` | templates and reuse |
| `pen.pattern(...)` | `<pattern>` | tiling fill |
| `pen.marker(...)` | `<marker>` | arrowheads / line-end markers |
| `pen.clipPath(shape, targets)` | `<clipPath>` | clipping (alias `pen.clip`) |
| `pen.mask(shape, targets)` | `<mask>` | masking |
| `pen.a(el, url)` | `<a>` | hyperlink |
| `pen.linearGradient(...)` / `pen.radialGradient(...)` | `<linearGradient>` / `<radialGradient>` | gradients |

Every legacy long name (`draw_circle`, `write_text`, `create_linear_gradient`, …)
is kept as an alias, so old scripts keep working.

## Enum + string, both accepted

Font names, colour names, option values with only a handful of choices — **all have
enums**. Each enum subclasses `str`, so `f"{Font.SIMHEI}"` gives `"SimHei"` and
comparison against a plain string works, which makes enums and strings fully
interchangeable.

```python
from malight import (Malight, Font, Color, ColorName, FontWeight,
                     StrokeCap, FillRule)

pen = Malight("demo")

# Fonts: three equivalent forms — enum / font name / font file
pen.text(50, 60, "enum",  font=Font.SIMHEI)                  # enum
pen.text(50, 100, "name", font="KaiTi")                      # any installed font name
pen.text(50, 140, "bold", font=Font.MS_YAHEI, weight=FontWeight.BOLD)
# A font file (.ttf/.otf/.ttc) embeds only the glyphs actually drawn by default
# (automatic subsetting), so text survives elsewhere and the SVG stays small;
# see "Fonts and images" below to embed the whole file or link it instead:
# pen.text(50, 180, "file", font="C:/myfonts/MyFont.ttf")

# Colours: enum (72 common names) + string + RGB all work
pen.circle(200, 200, 60, fill_color=ColorName.TOMATO)
pen.circle(340, 200, 60, fill_color="tomato")             # same thing
pen.circle(480, 200, 60, fill_color=Color.RGB(30, 144, 255))

# Options with few values: enums make typos impossible
pen.path(fill_color="none", stroke_color="#333", stroke_width=6,
         stroke_cap=StrokeCap.ROUND,       # line cap: ROUND / BUTT / SQUARE
         stroke_join="round")              # line join: strings accepted too
pen.rect(0, 0, 10, 10, fill_rule=FillRule.EVENODD)
pen.finish()
```

The full enum list (`Font` / `ColorName` / `BlendMode` / `FontWeight` / `DashStyle` /
`StrokeCap` / `StrokeJoin` / `FillRule` / `ArrowStyle` / `TextHAlign` / `PDFMode` …),
their members and their meaning: [definitions.en.md](https://github.com/weigang75/malight/blob/main/malight/definitions.en.md);
font lookup and embedding: [fonts.en.md](https://github.com/weigang75/malight/blob/main/malight/fonts.en.md).

General yes/no values also have lowercase constants: `YES` `NO` `ON` `OFF`.
Your IDE completes them, so there is nothing to memorise.

## Fonts and images: embed or link (`pen.set_embed`)

Local font files and local images are **written into the SVG** by default (they
survive on any machine), but how much gets written is your choice:

| Asset | Default | Alternatives |
|---|---|---|
| Font (`font=<file>`) | `FontEmbed.SUBSET` - only the glyphs actually drawn | `EMBED` the whole file / `LINK` the local path |
| Image (`pen.image(...)`) | `ImageEmbed.EMBED` - base64 inlined | `LINK` a relative path |

```python
from malight import Malight, FontEmbed, ImageEmbed, find_font_file

pen = Malight("poster", fonts="link", images="link")   # smallest files
pen.set_embed(fonts=FontEmbed.SUBSET)                  # back to the default

# No need to repeat the text: at finish() the library scans the glyphs used
pen.text(300, 100, "MaLight", font=find_font_file("MyFont"),
         font_size=40, h_align="middle")
pen.image("assets/bg.jpg", 0, 0, width=600, height=400)   # LINK writes a path
```

Measured on a 5.6 MB font (`Android.ttf`) drawing 12 characters:

| Mode | Resulting SVG |
|---|---|
| `SUBSET` (default) | **4.5 KB** |
| `EMBED` | 7.5 MB |
| `LINK` | 0.7 KB |

The price of `LINK`: the font / image file must stay where it is (same machine,
same folder layout). Copy the SVG alone and you lose the glyphs or the picture.
Keep the defaults when the file has to travel. Images in `LINK` mode are
referenced **relative to the SVG folder**, so PNG/PDF export still resolves them.

Full walk-through: `examples/demo_embed.py`; bundled fonts:
[assets/fonts/README.md](https://github.com/weigang75/malight/blob/main/malight/assets/fonts/README.md).

## Recolouring one SVG file (`pen.svg_image`)

`pen.image()` pastes an SVG as a **bitmap**: the contents sit inside base64 and
cannot be touched. `pen.svg_image()` reads the source text into the element, so
you can edit it — colours included:

```python
icon = pen.svg_image("assets/icons/mark.svg", x=40, y=40, width=60)
icon.svg_colors()                           # ['#ffffff', '#4dabf7']: what the file really uses
icon.replace_color("#ffffff", "#ff0000")    # white -> red (#fff / white all match)
icon.replace_text("circle", "ellipse")      # plain text replacement

# one file, three colours: each element edits its own copy
for i, color in enumerate(("#e63946", "#2a9d8f", "#1d3557")):
    pen.svg_image("assets/icons/mark.svg", x=40 + i * 90, y=200,
                  width=70).replace_color("white", color)
```

`replace_color` swaps by **colour**, not by raw text: `#ffffff` / `#FFF` /
`rgb(255,255,255)` / `white` all mean the same thing and match at once, while
identifiers such as `id="orange"` stay untouched. A colour the file does not
actually use is reported together with the colours it does use, instead of
silently changing nothing.

### Taking an SVG apart (`import_svg_as_group` / `import_svg_as_symbol`)

Recolouring belongs to the SVG image element above, the one that carries its own
text. When you want to **take an SVG apart** and edit it shape by shape, use
these two entry points instead: they hand you **nodes**.

```python
g = pen.import_svg_as_group("assets/icons/mark.svg", x=40, y=40, scale=0.5)
g.bbox()                              # it is a real group: geometry included
g.translate(10, 0)
for node in g.walk():                 # walk every node and edit any attribute
    print(node.tag, node.attribs.get("fill"))
```

They carry **no recolour methods**: colour is a property of a graphic, not of
the idea of grouping. Recolour through the tools functions on the node instead
(`<style>` blocks included):

```python
from malight.tools import replace_svg_node_color, svg_node_colors

svg_node_colors(g.node)               # ['#ffffff', '#4dabf7']
replace_svg_node_color(g.node, "white", "#ff0000")

tpl = pen.import_svg_as_symbol("assets/icons/mark.svg", id_="mark")
replace_svg_node_color(tpl.node, "white", "#ff0000")   # one edit, every <use> changes
pen.use("mark", x=200, y=40, width=80, height=60)
```

When each copy needs its own colours, keep using `pen.svg_image()` — a template
is by definition "edit once, change everywhere".

## Switchable language at runtime (English by default)

Library errors, notices and export messages are **English by default**. Switching to
Chinese takes one line and no changes to your own code:

```python
from malight import set_language, use_language, get_language, t

print(get_language())                # "en" (default)

set_language("zh")                   # switch everything to Chinese
set_language("zh-TW")                # region codes work; falls back to zh
set_language("auto")                 # follow the system locale

with use_language("en"):             # scope it to one block; restored on exit
    pen.export_png()

print(t("export.ok", kind="SVG", tail="", path="a.svg", size="1 KB"))
```

- The `MALIGHT_LANG=zh` environment variable sets the language before the process
  starts — handy in containers and CI.
- Lookup order is *exact language code → same language family → English*, so a
  missing message never raises.
- Add your own language, or override individual keys, with
  `malight.i18n.add_messages("ja", {...})`.

The complete API and every message key: [i18n.en.md](https://github.com/weigang75/malight/blob/main/malight/i18n.en.md).

## Docs travel with the code

Every module has its own bilingual documentation right next to it — **change the API,
change the page beside it**:

| File | Contents |
|---|---|
| `malight/elements/path.py` | the source (its docstrings are the single source of truth) |
| `malight/elements/path.zh.md` | Chinese page: summary + class/method tables + full runnable example |
| `malight/elements/path.en.md` | the English page, with a one-click switch back to Chinese |

Pages are **generated**, never hand-written:

```bash
python tools/gen_docs.py            # refresh all 46 modules' bilingual pages from docstrings
python tools/gen_docs.py --check    # for CI: exit code 1 when a page is out of date
python tools/gen_docs.py path       # refresh only modules whose name matches
```

That is why the pages can't drift from the code; the module index in the README below
is rebuilt by the same script.

## Module index

<!-- DOCS-INDEX:START -->
### malight (top-level)

| Module | Description |
|---|---|
| [compat](https://github.com/weigang75/malight/blob/main/malight/compat.en.md) | Mapping tables and a script migrator from the Chinese API to the English one. |
| [definitions](https://github.com/weigang75/malight/blob/main/malight/definitions.en.md) | Colors, fonts, paper sizes and every option enum. |
| [ext](https://github.com/weigang75/malight/blob/main/malight/ext.en.md) | The extension mechanism: register a third-party toolkit with @toolkit and mount it on a board. |
| [fonts](https://github.com/weigang75/malight/blob/main/malight/fonts.en.md) | Font enum plus font lookup, embedding and subsetting helpers. |
| [gradients](https://github.com/weigang75/malight/blob/main/malight/gradients.en.md) | Linear and radial gradient definitions. |
| [i18n](https://github.com/weigang75/malight/blob/main/malight/i18n.en.md) | Runtime message localisation. |
| [page](https://github.com/weigang75/malight/blob/main/malight/page.en.md) | Page setup for printing and PDF export: paper, margins and scaling. |
| [svg_backend](https://github.com/weigang75/malight/blob/main/malight/svg_backend.en.md) | A tiny SVG element and serialisation backend (internal). |
| [tools](https://github.com/weigang75/malight/blob/main/malight/tools.en.md) | Image, export, geometry, filter and font helpers. |

### malight/board

| Module | Description |
|---|---|
| [containers](https://github.com/weigang75/malight/blob/main/malight/board/containers.en.md) | Container elements: g, symbol, use, pattern, marker and a. |
| [core](https://github.com/weigang75/malight/blob/main/malight/board/core.en.md) | BoardCore: canvas setup, element registry, page config, backgrounds, lifecycle hooks and finish(). |
| [debug](https://github.com/weigang75/malight/blob/main/malight/board/debug.en.md) | Debugging helpers: grids, frames, distance measurements and key points. |
| [effects](https://github.com/weigang75/malight/blob/main/malight/board/effects.en.md) | Clipping and masking with SVG clipPath and mask. |
| [filters](https://github.com/weigang75/malight/blob/main/malight/board/filters.en.md) | The filter factory behind pen.fx and pen.filter. |
| [fx](https://github.com/weigang75/malight/blob/main/malight/board/fx.en.md) | FilterChain, a chainable builder for Photoshop-style filter stacks. |
| [gradients_mixin](https://github.com/weigang75/malight/blob/main/malight/board/gradients_mixin.en.md) | Gradients: SVG linearGradient and radialGradient, plus rainbow and gold shortcuts. |
| [images](https://github.com/weigang75/malight/blob/main/malight/board/images.en.md) | Images: bitmap `<image>` placement and SVG import. |
| [layout](https://github.com/weigang75/malight/blob/main/malight/board/layout.en.md) | Arranging elements horizontally, vertically, in a grid or around a circle. |
| [paths](https://github.com/weigang75/malight/blob/main/malight/board/paths.en.md) | Paths and connectors: path, polyline, tables, arrows and wave lines. |
| [repeat](https://github.com/weigang75/malight/blob/main/malight/board/repeat.en.md) | Repetition: grid, circular and linear. |
| [shapes](https://github.com/weigang75/malight/blob/main/malight/board/shapes.en.md) | Basic shapes, each method named after its SVG element. |
| [style](https://github.com/weigang75/malight/blob/main/malight/board/style.en.md) | The style helper (pen.style) for CSS classes, the global stylesheet and font embedding. |
| [text_board](https://github.com/weigang75/malight/blob/main/malight/board/text_board.en.md) | Text: SVG text, textPath and text-to-path conversion. |

### malight/elements

| Module | Description |
|---|---|
| [base](https://github.com/weigang75/malight/blob/main/malight/elements/base.en.md) | The Element base class and the helpers shared by every element. |
| [circle](https://github.com/weigang75/malight/blob/main/malight/elements/circle.en.md) | CircleElement, created by pen.circle. |
| [clippath](https://github.com/weigang75/malight/blob/main/malight/elements/clippath.en.md) | ClipPathElement: clip-path definitions. |
| [ellipse](https://github.com/weigang75/malight/blob/main/malight/elements/ellipse.en.md) | EllipseElement, created by pen.ellipse. |
| [group](https://github.com/weigang75/malight/blob/main/malight/elements/group.en.md) | GroupElement: bundle elements so they transform and animate together. |
| [image](https://github.com/weigang75/malight/blob/main/malight/elements/image.en.md) | ImageElement: a bitmap image. |
| [line](https://github.com/weigang75/malight/blob/main/malight/elements/line.en.md) | LineElement, created by pen.line. |
| [link](https://github.com/weigang75/malight/blob/main/malight/elements/link.en.md) | LinkElement: click the element to open a URL. |
| [marker](https://github.com/weigang75/malight/blob/main/malight/elements/marker.en.md) | MarkerElement: line-end decorations such as arrowheads. |
| [mask](https://github.com/weigang75/malight/blob/main/malight/elements/mask.en.md) | MaskElement: control visibility by luminance. |
| [path](https://github.com/weigang75/malight/blob/main/malight/elements/path.en.md) | PathElement: move / line / curve / arc commands, turtle drawing and boolean operations. |
| [pattern](https://github.com/weigang75/malight/blob/main/malight/elements/pattern.en.md) | PatternElement: tiling fill textures. |
| [polygon](https://github.com/weigang75/malight/blob/main/malight/elements/polygon.en.md) | PolygonElement, created by pen.polygon. |
| [polyline](https://github.com/weigang75/malight/blob/main/malight/elements/polyline.en.md) | PolylineElement, created by pen.polyline. |
| [rect](https://github.com/weigang75/malight/blob/main/malight/elements/rect.en.md) | RectElement, created by pen.rect. |
| [svggroup](https://github.com/weigang75/malight/blob/main/malight/elements/svggroup.en.md) | SvgGroupElement: an SVG file imported as an editable group. |
| [svgimage](https://github.com/weigang75/malight/blob/main/malight/elements/svgimage.en.md) | SVGImageElement: embed another SVG file as an image. |
| [symbol](https://github.com/weigang75/malight/blob/main/malight/elements/symbol.en.md) | TemplateElement: define a reusable symbol. |
| [text](https://github.com/weigang75/malight/blob/main/malight/elements/text.en.md) | TextElement, created by pen.text. |
| [textpath](https://github.com/weigang75/malight/blob/main/malight/elements/textpath.en.md) | TextPathElement: text laid out along a path. |
| [use](https://github.com/weigang75/malight/blob/main/malight/elements/use.en.md) | UseElement: reference a symbol or an already defined shape. |

### malight/pathkit

| Module | Description |
|---|---|
| [editor](https://github.com/weigang75/malight/blob/main/malight/pathkit/editor.en.md) | PathEditor: read the structure, drag anchors and control points, insert or drop points, save point data. |
| [htmleditor](https://github.com/weigang75/malight/blob/main/malight/pathkit/htmleditor.en.md) | PathHTMLEditor: export a path as one self-contained HTML file you drag points in. |
| [parser](https://github.com/weigang75/malight/blob/main/malight/pathkit/parser.en.md) | Parse an SVG path d string into structured segments, and render segments back to d. |
| [point](https://github.com/weigang75/malight/blob/main/malight/pathkit/point.en.md) | PathPoint: a single draggable anchor or control point. |
| [segment](https://github.com/weigang75/malight/blob/main/malight/pathkit/segment.en.md) | PathSegment: one SVG path command in absolute coordinates, with geometry helpers. |
<!-- DOCS-INDEX:END -->

## Going deeper

| Topic | In one line | Documentation |
|---|---|---|
| Path helper `pathkit` | Turn a path into **visible points**: inspect the structure, drag anchors/controls, insert or delete points, reverse, scale, save point data | [pathkit/editor.md](https://github.com/weigang75/malight/blob/main/malight/pathkit/editor.en.md) ｜ [point.md](https://github.com/weigang75/malight/blob/main/malight/pathkit/point.en.md) ｜ [segment.md](https://github.com/weigang75/malight/blob/main/malight/pathkit/segment.en.md) |
| Filters `pen.fx` | Photoshop/Illustrator-style filters, chainable, only visually stable effects | [board/fx.md](https://github.com/weigang75/malight/blob/main/malight/board/fx.en.md) ｜ [board/filters.md](https://github.com/weigang75/malight/blob/main/malight/board/filters.en.md) |
| Extensions `malight.ext` | Add toolkits (charts, icons, effects) to the board without touching the source | [ext.md](https://github.com/weigang75/malight/blob/main/malight/ext.en.md) |
| Export engines | Chrome and cairo back ends for PNG and PDF, full path printed | [tools.md](https://github.com/weigang75/malight/blob/main/malight/tools.en.md) |
| Enums and constants | Colours, fonts, paper sizes and every option enum | [definitions.md](https://github.com/weigang75/malight/blob/main/malight/definitions.en.md) |
| Compatibility | Chinese-API → English-API mapping tables and a one-command migrator | [compat.md](https://github.com/weigang75/malight/blob/main/malight/compat.en.md) |
| Internal backend | The tiny SVG element tree and serialiser (rarely needed directly) | [svg_backend.md](https://github.com/weigang75/malight/blob/main/malight/svg_backend.en.md) |

### Path helper `pathkit` (see it, drag it)

`PathElement` has many commands and is hard to reason about visually. `malight.pathkit`
turns a path into **visible points**: how many points each curve has, where they are,
and what happens when you drag them — just like a pen tool.

```python
from malight import Malight
from malight.pathkit import PathEditor

pen = Malight("path_demo", width=660, height=400)
p = pen.path(fill_color="none", stroke_color="#e63946", stroke_width=3)
p.move_to(60, 300)
p.cubic_to((120, 80), (260, 80), (320, 300))
p.quad_to((420, 120), (520, 300))

print(p.describe())                   # 1) inspect: start / control / end of each segment
ed = p.editor()                       # 2) get the editor (same as PathEditor(p))
ed.anchors[1].move_to(200, 60)        #    drag an anchor (writes back immediately)
ed.controls[0].move_by(0, -30)        #    drag a control point
ed.insert_anchor(ed.curve_indices()[0], 0.5)   # 3) insert a point, shape unchanged
ed.scale_all(0.9, 0.9, cx=300, cy=200)
ed.show(labels=True)                  # 4) visualise: squares = anchors, dots = controls
ed.save_json("points.json")           # 5) save points, compute elsewhere, load back
pen.finish()
```

`PathElement` also exposes convenience passthroughs: `path.describe()` /
`path.anchor_points()` / `path.control_points()` / `path.move_anchor(i, x, y)` /
`path.move_control(seg, i, x, y)` / `path.show_points()` / `path.editor()`.

### Filters `pen.fx` (Photoshop / Illustrator style)

**8 ways to use them** (they stack and can be shared; illustrated in
[assets/images/filters_preview.png](https://github.com/weigang75/malight/blob/main/malight/assets/images/filters_preview.png) -
code on the left, rendered result on the right):

| # | Style | Code |
|---|---|---|
| 1 | Element method chain (`fx_*` returns the element, chains forever) | `el.fx_inner_shadow(1, 1, 2, "#ffffff", 0.6).fx_emboss().fx_blur(1.5)` |
| 2 | Stack by name (same as `el.fx_blur(3)`) | `el.fx("blur", 3)` |
| 3 | Append any raw SVG filter primitive | `el.fx("custom", "feBlend", mode="screen", in2="SourceGraphic")` |
| 4 | Continue the element's own chain | `el.fx_chain().blur(2).saturate(1.4)` |
| 5 | Factory, one call (pass `filter=` straight in) | `filter=pen.fx.shadow(6, 8, 6)` |
| 6 | Factory chain, bind later (one chain, many elements) | `f = pen.fx.chain().blur(1).shadow(5, 5, 4)` → `a.set_filter(f)` / `f.apply(b)` |
| 7 | Stack / replace / clear (`set_filter` stacks by default) | repeated `set_filter` stacks; `merge=False` replaces the chain; `set_filter(None)` clears |
| 8 | Low-level helpers (`tools.create_*_filter`) | `fid = create_glow_filter(pen, 6)` + `extra={"filter": "url(#%s)" % fid}` |

```python
fx = pen.fx          # pen.filter is an alias

# stack them like Photoshop layer styles (inner shadow + emboss + slight blur)
pen.text(60, 200, "Ma", font_size=44, fill_color="#c23b22") \
   .fx_inner_shadow(1, 1, 2, "#ffffff", 0.6).fx_emboss().fx_blur(1.5)

# build one chain, share it with several elements
f = fx.chain().outline(3, "#fff").shadow(8, 10, 6).saturate(1.4)
pen.star(500, 400, 140, fill_color="#f4a261").set_filter(f)
pen.circle(620, 400, 90, fill_color="#2a9d8f").set_filter(f)
```

| Category | Methods | Photoshop / Illustrator equivalent |
|---|---|---|
| Layer styles | `shadow` `inner_shadow` `glow` `inner_glow` `bevel` `engrave` `outline` `color_overlay` | drop shadow / inner shadow / outer glow / inner glow / bevel & emboss / engrave / stroke / colour overlay |
| Blur & sharpen | `blur` `sharpen` `motion_blur` | Gaussian blur / unsharp mask / motion blur |
| Stylise | `roughen` `noise` `emboss` `edge_detect(width)` | roughen / add noise / emboss / find edges (width &gt; 1 thickens strokes) |
| Colour | `saturate` `hue_rotate` `brightness` `contrast` `gamma` `grayscale` `sepia` `invert` `posterize` | hue & saturation / brightness & contrast / curves / desaturate / invert / posterize |

Every method returns a `FilterChain`, so chains keep building; `f.apply(el)`,
`el.set_filter(f)` (stacks by default, `merge=False` replaces the whole chain)
and `el.set_filter(None)` (clear) are supported, and
`f.custom("feBlend", mode="screen", ...)` appends any raw SVG primitive.
Factory methods map one-to-one onto the element shortcuts
(`pen.fx.blur(x)` is the same as `el.fx_blur(x)`).
Filters render in browsers and any SVG-filter-aware viewer (cairosvg ignores filters
when exporting PNG).

## Type hints (PyCharm / VS Code completion)

All **314 public methods carry return-type annotations**, so typing `pen.` shows what
each method returns, and chained calls keep their hints:

```python
p = pen.path(fill_color="none", stroke_width=2)   # p: PathElement
p.move_to(50, 50).line_to(200, 80).close()        # chainable: returns itself

c = pen.circle(100, 100, 50)                      # c: CircleElement
c.set_filter(pen.fx.shadow(6, 6, 5)).translate(10, 0)

g = pen.g()                                       # g: GroupElement
pen.linearGradient((0, 0), (1, 0), "red", "blue") # -> LinearGradient
pen.repeat_grid(tile, 3, 40, 2, 40)               # -> list[Element]
```

- Element methods (`move_to` / `translate` / `set_filter` …) are annotated with
  `TypeVar("_Self")`, so subclasses infer **their own** type
  (`PathElement.move_to()` returns `PathElement`).
- Board methods that return the board (`resize` / `set_background_color` / `add_js`)
  are annotated `_Pen`.
- The package ships `py.typed` (PEP 561), so hints survive `pip install malight`.
- `examples/test_types.py` checks for missing annotations, evaluability and runtime
  type agreement: forget an annotation on a new method and the test fails.

## Export: Chrome and cairo engines

```python
pen.finish()

pen.export_pdf()                                  # AUTO: Chrome if present, else cairo
pen.export_pdf("out.pdf", engine=PDFMode.CHROME)  # force Chrome (browser-identical, SVG filters included)
pen.export_pdf("out.pdf", engine=PDFMode.CAIROSVG)# force cairo (no browser needed)
pen.export_png(scale=2)                           # PNG AUTO: cairosvg if installed, else Chrome
pen.export_png(scale=2, mode=PNGMode.CHROME)      # PNG via Chrome (filters render correctly)
```

**Exports print the full path**, ready to copy (the return value is the same absolute
path, so scripts can chain on it):

```
[malight] SVG exported (800x500) -> C:\proj\output\demo_basic.svg  [7.3 KB]
[malight] cairosvg not found, falling back to headless Chrome for PNG (better SVG filter support)
[malight] PNG exported (Chrome engine, 2x, filters intact) -> C:\proj\output\demo_basic.png  [45.9 KB]
[malight] PDF exported (Chrome engine) -> C:\proj\output\demo_basic.pdf  [88.3 KB]
```

| Engine | Requirement | SVG filters (shadow/glow/blur/emboss) | Best for |
|---|---|---|---|
| **Chrome** (headless) | local Chrome/Edge | full rendering | filter-heavy artwork, browser-identical PDF |
| **cairo** | `cairosvg` | ignored | pure vector work, no browser, fastest |

- Chrome is located automatically (registry → common install dirs → `PATH` → Edge).
  For portable builds set `MALIGHT_CHROME=D:\chrome\chrome.exe`.
- PDF pages have zero margin and map 1:1 to canvas pixels; PNG honours `scale`
  exactly (a 480×320 canvas at `scale=2` yields 960×640).
- **If neither engine is available you get a clear error with two installation
  suggestions**, not a bare `ModuleNotFoundError`.
  The full comparison is in `examples/demo_export.py` and [tools.md](https://github.com/weigang75/malight/blob/main/malight/tools.en.md).

## Compatibility and migration

- **Legacy Chinese scripts**: `python -m malight.compat old.py new.py`
  (Chinese API → English short names, including `import magicpen` → `import malight`).
- **v1 English scripts**: `import magicpen` still works (the shim at the repo root
  forwards automatically), but moving to `from malight import Malight` is recommended.
  The shim ships only in the source repository, not in the wheel.

Mapping tables and all rewrite rules: [compat.md](https://github.com/weigang75/malight/blob/main/malight/compat.en.md).

## Examples

`examples/` holds 13 runnable demos: `demo_basic` `demo_path` `demo_effects`
`demo_animation` `demo_arrange` `demo_advanced` `demo_fx` (the whole filter family)
`demo_ext` (extensions) `demo_export` (Chrome vs cairo). The `board_games/`
subdirectory collects board-game samples: `chinese_chess.py` (Xiangqi) and
`international_chess.py` (chess). Plus two tests:
`test_fx_smoke.py` (filter smoke test) and `test_types.py` (annotation audit).

**Every class file also contains a runnable example** (`if __name__ == "__main__":`).
Click the green triangle in PyCharm, or run it from the command line, and it draws
its result; the example covers every method of that class with step-by-step comments:

```bash
python malight/elements/circle.py     # circle: styles / transforms / animation / clone
python malight/elements/path.py       # path: every command + pathkit point dragging
python malight/elements/group.py      # group: nesting / transforms / bbox / clone
python malight/pathkit/editor.py      # path editor: inspect / drag / insert / save
python malight/pathkit/point.py       # draggable point: properties / move / unpack / eq
python malight/pathkit/segment.py     # path segment: parse / sample / measure / split
# ... or equivalently `python -m malight.elements.circle`
```

Runnable modules: `malight/elements/*.py` (20 element classes) plus
`malight/pathkit/*.py` (parser / editor / point / segment).
The "Full example" section of each module page is exactly this code.

## Building the wheel

The repo ships `build_release.py` (run it from the project root, where
`pyproject.toml` lives):

```bash
python build_release.py            # build dist/malight-x.y.z-py3-none-any.whl + .tar.gz and self-test
python build_release.py --check    # also run twine check on the metadata
python build_release.py --upload   # build, verify, then upload to PyPI (needs twine)
python build_release.py --offline  # don't auto-install build/twine; fall back to pip wheel
```

The full PyPI flow (wired into the script, or run by hand):

```bash
pip install -U twine
python build_release.py --check
twine upload dist/*                          # production
twine upload --repository testpypi dist/*    # verify on TestPyPI first
```

- The single source of truth for the version is `__version__` in
  `malight/__init__.py`; `pyproject.toml` reads it dynamically.
- Core drawing has **no dependencies**; optional extras:
  `pip install malight[export]` (PNG/PDF), `malight[fontpath]` (text to path),
  `malight[full]`.
- Before releasing, the script creates a clean venv, installs the wheel and exercises
  drawing, filters and extensions to make sure it works out of the box.
- The bilingual module `.md` files ship inside the sdist (for offline reading) but are
  excluded from the wheel.

## A note on the bilingual docs

Both languages are first class. Every module ships two pages next to it —
`xxx.zh.md` in Chinese and `xxx.en.md` in English — each with a one-click switch at
the top, and the same content on both: summary, class and method tables, module
notes and a full runnable example.

The English pages are **entirely English**: summaries, prose, tables and the example
code. The one exception is the switch label naming the other language. A regression
check fails the build if Chinese ever leaks into an English page.

The sources serve both languages at once: every docstring and every example comment
carries both halves, separated by ` / ` — Chinese first, English second. The docs
generator splits each pair per page, which is why the two pages can never drift apart.
Runtime messages follow the language you set with `malight.set_language(...)`.

## License

See [LICENSE](https://github.com/weigang75/malight/blob/main/LICENSE).
