Metadata-Version: 2.4
Name: malight
Version: 0.1.0
Summary: malight - SVG drawing toolkit ported and enhanced from Shenbi Maliang, with PS/AI-style filters and a plugin extension mechanism
Author: malight contributors
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
```

> **v2.0**: 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.
>
> **v2.1**: 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.
>
> **v2.2**: 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 now 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`).

## 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.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) is base64-inlined via @font-face, so text
# survives on machines that lack the font:
# 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.

## 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. |
| [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).
