MaLight — an English drawing toolkit (the English edition of Shenbi Maliang)
简体中文 | 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).
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 mechanismmalight.ext. Old scripts withimport magicpenstill run (a compatibility shim ships in the repo), andpython -m malight.compat old.py new.pymigrates 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 pairxxx.zh.md/xxx.en.mdis 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)orel.font_size(36)(no-arg reads, one-arg sets, chainable; parameter names match the creating call,get_font_size()reads back); pluspaint_order=PaintOrder.STROKE(stroke-first outlined text) and the newfx_engrave()carved-in filter; plusPageSetupfor 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 bytools/gen_previews.py).
Quick start
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.
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.
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;
font lookup and embedding: 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:
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=zhenvironment 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.
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:
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
malight (top-level)
| Module | Description |
|---|---|
| compat | Mapping tables and a script migrator from the Chinese API to the English one. |
| definitions | Colors, fonts, paper sizes and every option enum. |
| ext | The extension mechanism: register a third-party toolkit with @toolkit and mount it on a board. |
| fonts | Font enum plus font lookup, embedding and subsetting helpers. |
| gradients | Linear and radial gradient definitions. |
| i18n | Runtime message localisation. |
| page | Page setup for printing and PDF export: paper, margins and scaling. |
| svg_backend | A tiny SVG element and serialisation backend (internal). |
| tools | Image, export, geometry, filter and font helpers. |
malight/board
| Module | Description |
|---|---|
| containers | Container elements: g, symbol, use, pattern, marker and a. |
| core | BoardCore: canvas setup, element registry, page config, backgrounds, lifecycle hooks and finish(). |
| debug | Debugging helpers: grids, frames, distance measurements and key points. |
| effects | Clipping and masking with SVG clipPath and mask. |
| filters | The filter factory behind pen.fx and pen.filter. |
| fx | FilterChain, a chainable builder for Photoshop-style filter stacks. |
| gradients_mixin | Gradients: SVG linearGradient and radialGradient, plus rainbow and gold shortcuts. |
| images | Images: bitmap <image> placement and SVG import. |
| layout | Arranging elements horizontally, vertically, in a grid or around a circle. |
| paths | Paths and connectors: path, polyline, tables, arrows and wave lines. |
| repeat | Repetition: grid, circular and linear. |
| shapes | Basic shapes, each method named after its SVG element. |
| style | The style helper (pen.style) for CSS classes, the global stylesheet and font embedding. |
| text_board | Text: SVG text, textPath and text-to-path conversion. |
malight/elements
| Module | Description |
|---|---|
| base | The Element base class and the helpers shared by every element. |
| circle | CircleElement, created by pen.circle. |
| clippath | ClipPathElement: clip-path definitions. |
| ellipse | EllipseElement, created by pen.ellipse. |
| group | GroupElement: bundle elements so they transform and animate together. |
| image | ImageElement: a bitmap image. |
| line | LineElement, created by pen.line. |
| link | LinkElement: click the element to open a URL. |
| marker | MarkerElement: line-end decorations such as arrowheads. |
| mask | MaskElement: control visibility by luminance. |
| path | PathElement: move / line / curve / arc commands, turtle drawing and boolean operations. |
| pattern | PatternElement: tiling fill textures. |
| polygon | PolygonElement, created by pen.polygon. |
| polyline | PolylineElement, created by pen.polyline. |
| rect | RectElement, created by pen.rect. |
| svgimage | SVGImageElement: embed another SVG file as an image. |
| symbol | TemplateElement: define a reusable symbol. |
| text | TextElement, created by pen.text. |
| textpath | TextPathElement: text laid out along a path. |
| use | UseElement: reference a symbol or an already defined shape. |
malight/pathkit
| Module | Description |
|---|---|
| editor | PathEditor: read the structure, drag anchors and control points, insert or drop points, save point data. |
| htmleditor | PathHTMLEditor: export a path as one self-contained HTML file you drag points in. |
| parser | Parse an SVG path d string into structured segments, and render segments back to d. |
| point | PathPoint: a single draggable anchor or control point. |
| segment | PathSegment: one SVG path command in absolute coordinates, with geometry helpers. |
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 | point.md | segment.md |
Filters pen.fx |
Photoshop/Illustrator-style filters, chainable, only visually stable effects | board/fx.md | board/filters.md |
Extensions malight.ext |
Add toolkits (charts, icons, effects) to the board without touching the source | ext.md |
| Export engines | Chrome and cairo back ends for PNG and PDF, full path printed | tools.md |
| Enums and constants | Colours, fonts, paper sizes and every option enum | definitions.md |
| Compatibility | Chinese-API → English-API mapping tables and a one-command migrator | compat.md |
| Internal backend | The tiny SVG element tree and serialiser (rarely needed directly) | svg_backend.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.
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 - 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} |
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 > 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:
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 withTypeVar("_Self"), so subclasses infer their own type (PathElement.move_to()returnsPathElement). - Board methods that return the board (
resize/set_background_color/add_js) are annotated_Pen. - The package ships
py.typed(PEP 561), so hints survivepip install malight. examples/test_types.pychecks for missing annotations, evaluability and runtime type agreement: forget an annotation on a new method and the test fails.
Export: Chrome and cairo engines
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 setMALIGHT_CHROME=D:\chrome\chrome.exe. - PDF pages have zero margin and map 1:1 to canvas pixels; PNG honours
scaleexactly (a 480×320 canvas atscale=2yields 960×640). - If neither engine is available you get a clear error with two installation
suggestions, not a bare
ModuleNotFoundError. The full comparison is inexamples/demo_export.pyand tools.md.
Compatibility and migration
- Legacy Chinese scripts:
python -m malight.compat old.py new.py(Chinese API → English short names, includingimport magicpen→import malight). - v1 English scripts:
import magicpenstill works (the shim at the repo root forwards automatically), but moving tofrom malight import Malightis recommended. The shim ships only in the source repository, not in the wheel.
Mapping tables and all rewrite rules: compat.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:
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):
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):
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__inmalight/__init__.py;pyproject.tomlreads 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
.mdfiles 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.
Release files for malight 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| malight-0.1.0.tar.gz | 1.4 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| malight-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.6 MB
Release files / malight-0.1.0.tar.gz
| Download URL | malight-0.1.0.tar.gz |
|---|---|
| Size | 1.4 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a6c0ca7744c8f397976756062b3e47a35c6db2e65ffdcdc29fb464d75bb5661d
|
|
BLAKE2b-256 checksum How to use checksums |
69bc4faf957145eeefeb9beb2e0dbb1aff78ecf1de3c5c4557109b99bf0161ef
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / malight-0.1.0-py3-none-any.whl
| Download URL | malight-0.1.0-py3-none-any.whl |
|---|---|
| Size | 1.2 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0f70714f5de0cc8d79ca0eb1445632bfef163c539123b4925503c48e26d266fe
|
|
BLAKE2b-256 checksum How to use checksums |
412b7a019aedfde2612e6d12190ecd5b0bcff0f9d6b015ef7473e3dc8cbc15f0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|