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
0.1.0 (first release): 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.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 localisationmalight.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 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).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; newpen.svg_image()to place an SVG (vector, with editable text and colours); newimport_svg_as_groupimported group (movable / rotatable / scalable, filters supported); new coloured console message helpers (print_red/print_green… inmalight.tools).
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.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.
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;
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.
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 |
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.
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:
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.
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):
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:
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. |
| svggroup | SvgGroupElement: an SVG file imported as an editable group. |
| 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.2.1
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.2.1.tar.gz | 1.5 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| malight-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.7 MB
Release files / malight-0.2.1.tar.gz
| Download URL | malight-0.2.1.tar.gz |
|---|---|
| Size | 1.5 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3bea7302ffdca33d01dc7a2cd025b044753a1128134a04b39b41b6813c139f89
|
|
BLAKE2b-256 checksum How to use checksums |
f67ee3a2e5cf0e848ed6602d95e36909c90a0e7aaec8a2a768f45af7bbfbb919
|
| 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.2.1-py3-none-any.whl
| Download URL | malight-0.2.1-py3-none-any.whl |
|---|---|
| Size | 1.3 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c310f110fe6490025f50bab578075ae2f319483b90918cfb4fbef63e30c6a111
|
|
BLAKE2b-256 checksum How to use checksums |
2e55af4a23b7fe2d9479aec9146c04a809349d344eb47ff35c9e263e183b77fb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|