codex/grapheme-cluster-codepoints.py

grapheme-cluster-codepoints.py
"""Compare canonical encodings of one accented letter; write PNG to cwd."""

from pathlib import Path
import unicodedata

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch


def box(ax, x, y, width, label, fill, fontsize=12):
    ax.add_patch(FancyBboxPatch(
        (x, y), width, 44, boxstyle="round,pad=0.02,rounding_size=5",
        facecolor=fill, edgecolor="#627183", linewidth=1.1,
    ))
    ax.text(x + width / 2, y + 22, label, ha="center", va="center",
            fontsize=fontsize, family="DejaVu Sans Mono", color="#172330")


def main():
    precomposed = "\u00e9"
    decomposed = "e\u0301"
    assert unicodedata.normalize("NFC", decomposed) == precomposed
    assert unicodedata.normalize("NFD", precomposed) == decomposed
    assert [len(s.encode("utf-8")) for s in [precomposed, decomposed]] == [2, 3]

    fig = plt.figure(figsize=(9, 3.6), dpi=100, facecolor="white")
    ax = fig.add_axes([0, 0, 1, 1])
    ax.set(xlim=(0, 900), ylim=(0, 360))
    ax.axis("off")
    ax.text(30, 332, "One grapheme cluster, different stored representations",
            fontsize=17, weight="bold", color="#172330")
    for x, text in [(30, "Representation"), (230, "Unicode code points"),
                    (530, "UTF-8 bytes (hex)"), (790, "Letter")]:
        ax.text(x, 286, text, fontsize=12, color="#34465b")

    for title, text, y in [("Precomposed", precomposed, 205),
                            ("Decomposed", decomposed, 103)]:
        ax.text(30, y + 28, title, fontsize=13, va="center", color="#172330")
        count = len(text)
        ax.add_patch(FancyBboxPatch(
            (216, y - 12), 276, 68, boxstyle="round,pad=0.02,rounding_size=8",
            facecolor="#edf5ff", edgecolor="#3c6e9b", linewidth=1.1,
        ))
        start = 230 if count == 2 else 300
        for i, char in enumerate(text):
            box(ax, start + i * 128, y, 116, f"U+{ord(char):04X}", "white")
        for i, byte in enumerate(text.encode("utf-8")):
            box(ax, 530 + i * 67, y, 57, f"{byte:02X}", "#f8f1df")
        # Show the abstract letter, not a benchmark of Matplotlib's shaping.
        ax.text(812, y + 22, precomposed, fontsize=34, ha="center", va="center",
                color="#172330")
        ax.text(350, y - 29, "one extended grapheme cluster", ha="center",
                fontsize=10, color="#3c6e9b")

    ax.text(30, 28, "Canonical normalization relates the rows; neither byte count nor code-point count counts the clusters.",
            fontsize=10.5, color="#34465b")
    fig.savefig(Path.cwd() / "grapheme-cluster-codepoints.png", dpi=100,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()