scripts/build-notebook-gallery.py 1#!/usr/bin/env python32"""Run the real Python bridge for two reproducible gallery figures."""3from __future__ import annotations4 5import argparse6import os7import sys8import tempfile9import warnings10from pathlib import Path11 12ROOT = Path(__file__).resolve().parents[1]13sys.path.insert(0, str(ROOT / "python"))14os.environ.setdefault("SOURCE_DATE_EPOCH", "0")15os.environ.setdefault("LAYMESH_CLI", str(ROOT / "target/release" / ("laymesh.exe" if sys.platform == "win32" else "laymesh")))16 17import matplotlib as mpl # noqa: E40218mpl.use("Agg")19import matplotlib.pyplot as plt # noqa: E40220import numpy as np # noqa: E40221from PIL import Image # noqa: E40222from laymesh import render_source # noqa: E40223 24mpl.rcParams["svg.hashsalt"] = "laymesh-gallery"25 26SOURCE = 'page = canvas(size=(120 mm, 80 mm), background="#f7f9fc")\nplot = image(src={{fig}})\nplaced = page.add(plot,size=(104 mm, 58 mm), fit=contain,\n target=page.top_left, offset=(8 mm, 8 mm))\ncaption = text(content={{caption}}, font_family="DejaVu Sans",\n font_size=10 pt, color="#203864")\npage.add(caption, target=page.bottom_left, offset=(8 mm, -11 mm))\n'27 28 29def figure(kind: str):30 fig, ax = plt.subplots(figsize=(5.2, 2.9), layout="constrained")31 if kind == "vector":32 x = np.linspace(0, 2 * np.pi, 121)33 ax.plot(x, np.sin(x), color="#087f8c", linewidth=2.5, label="sin(x)")34 ax.plot(x, np.cos(x), color="#d66853", linewidth=2, label="cos(x)")35 ax.set(xlabel="x", ylabel="amplitude", title="Vector line plot")36 ax.legend(frameon=False)37 else:38 x = np.linspace(-2.5, 2.5, 46)39 y = np.linspace(-1.8, 1.8, 32)40 z = np.sin(x[None, :] * 2) * np.cos(y[:, None] * 2)41 ax.imshow(z, cmap="viridis", origin="lower", aspect="auto")42 ax.set(title="Heatmap PNG fallback", xlabel="column", ylabel="row")43 return fig44 45 46def build(target: Path, kind: str):47 target.mkdir(parents=True, exist_ok=True)48 fig = figure(kind)49 with warnings.catch_warnings(record=True) as captured:50 warnings.simplefilter("always")51 result = render_source(52 SOURCE, namespace={"fig": fig, "caption": "Vector plot" if kind == "vector" else "Heatmap fallback"},53 base_dir=target, output=target / f"{kind}-preview.png", dpi=150,54 plot_dpi=240, save_source=f"{kind}.lay",55 )56 plt.close(fig)57 messages = [str(item.message) for item in captured]58 assets = list((target / f"{kind}.assets").iterdir())59 extension = ".svg" if kind == "vector" else ".png"60 assert len(assets) == 1 and assets[0].suffix == extension, (kind, assets)61 if kind == "vector":62 assert not any("已改用" in message for message in messages), messages63 else:64 assert any("已改用 240 DPI PNG" in message for message in messages), messages65 with Image.open(result.output) as image:66 assert image.size == (709, 472), image.size67 return result, assets[0], messages68 69 70def main():71 parser = argparse.ArgumentParser()72 group = parser.add_mutually_exclusive_group(required=True)73 group.add_argument("--write", action="store_true")74 group.add_argument("--check", action="store_true")75 args = parser.parse_args()76 destination = ROOT / "examples/gallery/notebook"77 if args.write:78 for kind in ("vector", "heatmap"):79 _, asset, messages = build(destination, kind)80 print(f"{kind}: {asset.relative_to(ROOT)}, 120 × 80 mm, 709 × 472 px, warnings={messages or 'none'}")81 else:82 with tempfile.TemporaryDirectory(dir=ROOT / "examples/gallery") as folder:83 for kind in ("vector", "heatmap"):84 _, asset, messages = build(Path(folder), kind)85 committed = destination / f"{kind}-preview.png"86 assert committed.is_file(), committed87 with Image.open(committed) as image:88 assert image.size == (709, 472)89 saved = destination / f"{kind}.lay"90 assert saved.is_file() and f"{kind}.assets/" in saved.read_text()91 assert list((destination / f"{kind}.assets").glob(f"*{asset.suffix}"))92 print(f"{kind}: bridge checked, {asset.suffix} asset, warnings={messages or 'none'}")93 94 95if __name__ == "__main__":96 main()