{"id":9063,"date":"2026-07-09T10:37:25","date_gmt":"2026-07-09T01:37:25","guid":{"rendered":"https:\/\/hasu0707.duckdns.org\/blog\/?p=9063"},"modified":"2026-07-09T10:48:08","modified_gmt":"2026-07-09T01:48:08","slug":"ttf-to-png","status":"publish","type":"post","link":"https:\/\/hasu0707.duckdns.org\/blog\/?p=9063","title":{"rendered":"TTF to PNG (Segoe Fluent Icons)"},"content":{"rendered":"\n<pre class=\"wp-block-preformatted\"><strong>1. \ud3f0\ud2b8 \ub2e4\uc6b4\ub85c\ub4dc<\/strong><br><a href=\"https:\/\/learn.microsoft.com\/ko-kr\/windows\/apps\/design\/downloads\/#fonts\">https:\/\/learn.microsoft.com\/ko-kr\/windows\/apps\/design\/downloads\/#fonts<\/a><br><br><strong>2. Python \ud328\ud0a4\uc9c0 \uc124\uce58<\/strong><br>PYTHON -m pip install --upgrade pip<br>PYTHON -m pip install fonttools cairosvg pillow<br><br><strong>3. MSYS2 Cairo \uc124\uce58<\/strong><br>CD \/D c:\\PortableApps\\cmd_msys64\\usr\\bin<br>bash<br>-----<br>pacman -Syu --noconfirm<br>pacman -S --noconfirm --needed mingw-w64-x86_64-cairo<br>-----<br>DIR c:\\PortableApps\\cmd_msys64\\mingw64\\bin\\libcairo-2.dll<br><br><strong>4. \uc544\ub798 \uc2a4\ud06c\ub9bd\ud2b8\ub85c \uc2e4\ud589<\/strong><br>PYTHON export_glyph_png.py ^<br>--font \"C:\\Windows\\Fonts\\Segoe Fluent Icons.ttf\" ^<br>--out \"d:\\downloads\\segoe_fluent_png\" ^<br>--size 256 ^<br>--padding 16 ^<br>--oversample 4 ^<br>--save-svg ^<br>--cairo-dll-dir \"c:\\PortableApps\\cmd_msys64\\mingw64\\bin\"<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">from __future__ import annotations\n\nimport argparse\nimport csv\nimport importlib\nimport io\nimport os\nimport re\nimport sys\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Tuple\nfrom xml.sax.saxutils import escape\n\nfrom fontTools.ttLib import TTFont\nfrom fontTools.pens.svgPathPen import SVGPathPen\nfrom fontTools.pens.boundsPen import BoundsPen\nfrom PIL import Image\n\n\nBounds = Tuple[float, float, float, float]\n\n# os.add_dll_directory()\uac00 \ubc18\ud658\ud558\ub294 \ud578\ub4e4\uc744 \uc720\uc9c0\ud574\uc57c DLL \uac80\uc0c9 \uacbd\ub85c\uac00 \uacc4\uc18d \uc720\ud6a8\ud568\n_DLL_DIRECTORY_HANDLES = []\n\n\ntry:\n    RESAMPLE_LANCZOS = Image.Resampling.LANCZOS\nexcept AttributeError:\n    RESAMPLE_LANCZOS = Image.LANCZOS\n\n\ndef sanitize_filename(value: str, max_len: int = 120) -> str:\n    \"\"\"\n    Windows \ud30c\uc77c\uba85\uc73c\ub85c \uc0ac\uc6a9\ud560 \uc218 \uc5c6\ub294 \ubb38\uc790\ub97c \uc81c\uac70\ud55c\ub2e4.\n    \"\"\"\n    value = re.sub(r'[&lt;>:\"\/\\\\|?*\\x00-\\x1F]', \"_\", value)\n    value = value.strip().strip(\".\")\n\n    if not value:\n        value = \"glyph\"\n\n    return value[:max_len]\n\n\ndef get_windows_font_dirs() -> List[Path]:\n    \"\"\"\n    Windows \uc2dc\uc2a4\ud15c \ud3f0\ud2b8 \ub514\ub809\ud130\ub9ac\uc640 \uc0ac\uc6a9\uc790 \ud3f0\ud2b8 \ub514\ub809\ud130\ub9ac\ub97c \ubc18\ud658\ud55c\ub2e4.\n    \"\"\"\n    font_dirs = [\n        Path(r\"C:\\Windows\\Fonts\"),\n    ]\n\n    local_app_data = os.environ.get(\"LOCALAPPDATA\")\n    if local_app_data:\n        font_dirs.append(Path(local_app_data) \/ \"Microsoft\" \/ \"Windows\" \/ \"Fonts\")\n\n    return font_dirs\n\n\ndef resolve_font_path(font_arg: str) -> Path:\n    \"\"\"\n    \uc0ac\uc6a9\uc790\uac00 \uc9c0\uc815\ud55c font \uacbd\ub85c\uac00 \uc2e4\uc81c \ud30c\uc77c\uc774\uba74 \uadf8\ub300\ub85c \uc0ac\uc6a9\ud55c\ub2e4.\n\n    Windows\uc758 Fonts \ud3f4\ub354\ub294 Explorer\uc5d0\uc11c \ubcf4\uc774\ub294 \ud3f0\ud2b8 \ud45c\uc2dc\uba85\uacfc \uc2e4\uc81c \ud30c\uc77c\uba85\uc774 \ub2e4\ub97c \uc218 \uc788\ub2e4.\n    \uc608:\n      \ud45c\uc2dc\uba85: Segoe Fluent Icons\n      \uc2e4\uc81c \ud30c\uc77c\uba85: SegoeIcons.ttf\n\n    \ub530\ub77c\uc11c \uacbd\ub85c\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc73c\uba74 \uc77c\ubc18\uc801\uc778 \ud6c4\ubcf4\ub97c \uc790\ub3d9 \uac80\uc0c9\ud55c\ub2e4.\n    \"\"\"\n    input_path = Path(font_arg)\n\n    if input_path.exists():\n        return input_path.resolve()\n\n    font_dirs = get_windows_font_dirs()\n\n    # Windows 11 Segoe Fluent Icons\uc758 \uc77c\ubc18\uc801\uc778 \uc2e4\uc81c \ud30c\uc77c\uba85 \ud6c4\ubcf4\n    known_candidates = [\n        \"SegoeIcons.ttf\",\n        \"segmdl2.ttf\",\n    ]\n\n    requested_name = input_path.name.lower()\n    requested_stem = input_path.stem.lower()\n\n    # 1\ucc28: \uc54c\ub824\uc9c4 \ud6c4\ubcf4 \ud30c\uc77c\uba85 \uc9c1\uc811 \ud655\uc778\n    for font_dir in font_dirs:\n        if not font_dir.exists():\n            continue\n\n        for candidate_name in known_candidates:\n            candidate = font_dir \/ candidate_name\n            if candidate.exists():\n                if (\n                    \"segoe fluent icons\" in requested_stem\n                    or \"fluent\" in requested_stem\n                    or \"icon\" in requested_stem\n                ):\n                    print(f\"[INFO] Requested font path not found: {font_arg}\")\n                    print(f\"[INFO] Auto-selected candidate font: {candidate}\")\n                    return candidate.resolve()\n\n    # 2\ucc28: \ud30c\uc77c\uba85 \uae30\uc900 \ud6c4\ubcf4 \uac80\uc0c9\n    candidate_files: List[Path] = []\n\n    search_words = []\n    for token in re.split(r\"[\\s._-]+\", requested_stem):\n        token = token.strip().lower()\n        if len(token) >= 3:\n            search_words.append(token)\n\n    # Segoe Fluent Icons \uc694\uccad\uc778 \uacbd\uc6b0 \ud3ed\ub113\uac8c \uac80\uc0c9\n    if \"segoe\" in requested_stem or \"fluent\" in requested_stem or \"icon\" in requested_stem:\n        search_words.extend([\"segoe\", \"icon\", \"icons\", \"fluent\"])\n\n    search_words = sorted(set(search_words))\n\n    for font_dir in font_dirs:\n        if not font_dir.exists():\n            continue\n\n        for file in font_dir.glob(\"*\"):\n            if file.suffix.lower() not in [\".ttf\", \".otf\", \".ttc\"]:\n                continue\n\n            file_name_lower = file.name.lower()\n\n            if any(word in file_name_lower for word in search_words):\n                candidate_files.append(file)\n\n    candidate_files = sorted(set(candidate_files), key=lambda p: p.name.lower())\n\n    if candidate_files:\n        print(\"[ERROR] Font file was not found exactly.\")\n        print(f\"[ERROR] Requested: {font_arg}\")\n        print()\n        print(\"[INFO] Candidate font files:\")\n\n        for idx, candidate in enumerate(candidate_files[:30], start=1):\n            print(f\"  {idx:02d}. {candidate}\")\n\n        print()\n        print(\"[ACTION] --font \uac12\uc5d0 \uc704 \ud6c4\ubcf4 \uc911 \uc2e4\uc81c \uc0ac\uc6a9\ud560 \ud30c\uc77c\uc758 \uc804\uccb4 \uacbd\ub85c\ub97c \ub123\uc73c\uc138\uc694.\")\n        print('[HINT] Windows 11 Segoe Fluent Icons\ub294 \ubcf4\ud1b5 \"C:\\\\Windows\\\\Fonts\\\\SegoeIcons.ttf\" \uc785\ub2c8\ub2e4.')\n\n    raise FileNotFoundError(\n        f\"Font file not found: {font_arg}\"\n    )\n\n\ndef load_cairosvg(cairo_dll_dir: Optional[str]):\n    \"\"\"\n    Windows\uc5d0\uc11c Cairo DLL \uacbd\ub85c\uac00 \ud544\uc694\ud55c \uacbd\uc6b0 \uba3c\uc800 \ub4f1\ub85d\ud55c \ub4a4 CairoSVG\ub97c import\ud55c\ub2e4.\n    \"\"\"\n    if cairo_dll_dir:\n        cairo_path = Path(cairo_dll_dir).resolve()\n\n        if not cairo_path.exists():\n            raise FileNotFoundError(\n                f\"Cairo DLL directory does not exist: {cairo_path}\"\n            )\n\n        expected_dll = cairo_path \/ \"libcairo-2.dll\"\n\n        if not expected_dll.exists():\n            print(f\"[WARN] libcairo-2.dll not found in: {cairo_path}\")\n            print(\"[WARN] CairoSVG import may fail.\")\n\n        os.environ[\"CAIROCFFI_DLL_DIRECTORIES\"] = str(cairo_path)\n        os.environ[\"PATH\"] = str(cairo_path) + os.pathsep + os.environ.get(\"PATH\", \"\")\n\n        add_dll_directory = getattr(os, \"add_dll_directory\", None)\n\n        if callable(add_dll_directory):\n            handle = add_dll_directory(str(cairo_path))\n            _DLL_DIRECTORY_HANDLES.append(handle)\n\n    try:\n        return importlib.import_module(\"cairosvg\")\n    except OSError as exc:\n        raise RuntimeError(\n            \"CairoSVG import failed because native Cairo DLL was not found.\\n\\n\"\n            \"Windows \ud574\uacb0 \ubc29\ubc95:\\n\"\n            \"1) Cairo DLL\uc774 \uc788\ub294 \ub514\ub809\ud130\ub9ac\ub97c \ud655\uc778\ud569\ub2c8\ub2e4.\\n\"\n            \"2) \ubcf4\ud1b5 MSYS2 \uc0ac\uc6a9 \uc2dc \uacbd\ub85c\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.\\n\"\n            \"   C:\\\\msys64\\\\mingw64\\\\bin\\n\"\n            \"3) \ud604\uc7ac \ud658\uacbd\uc5d0\uc11c\ub294 \uc608\ub97c \ub4e4\uc5b4 \ub2e4\uc74c\ucc98\ub7fc \uc2e4\ud589\ud569\ub2c8\ub2e4.\\n\"\n            \"   --cairo-dll-dir \\\"c:\\\\PortableApps\\\\cmd_msys64\\\\mingw64\\\\bin\\\"\\n\\n\"\n            f\"Original error:\\n{exc}\"\n        ) from exc\n\n\ndef build_unicode_map(font: TTFont) -> Dict[str, List[int]]:\n    \"\"\"\n    glyph_name -> [unicode codepoint...] \ud615\ud0dc\uc758 \ub9e4\ud551\uc744 \ub9cc\ub4e0\ub2e4.\n\n    getBestCmap() \ud558\ub098\ub9cc \uc4f0\uc9c0 \uc54a\uace0 Unicode cmap subtable \uc804\uccb4\ub97c \ud655\uc778\ud55c\ub2e4.\n    \uc544\uc774\ucf58 \ud3f0\ud2b8\ub294 Private Use Area, Symbol cmap \ub4f1\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc73c\ubbc0\ub85c\n    \uac00\ub2a5\ud55c Unicode \ub9e4\ud551\uc744 \ubaa8\ub450 \uc218\uc9d1\ud55c\ub2e4.\n    \"\"\"\n    result: Dict[str, set[int]] = {}\n\n    if \"cmap\" not in font:\n        return {}\n\n    for table in font[\"cmap\"].tables:\n        if not table.isUnicode():\n            continue\n\n        for codepoint, glyph_name in table.cmap.items():\n            result.setdefault(glyph_name, set()).add(int(codepoint))\n\n    return {\n        glyph_name: sorted(codepoints)\n        for glyph_name, codepoints in result.items()\n    }\n\n\ndef get_glyph_path_and_bounds(\n    font: TTFont,\n    glyph_name: str,\n) -> Tuple[str, Optional[Bounds]]:\n    \"\"\"\n    \uc2e4\uc81c glyph outline\uc744 SVG path command\uc640 bbox\ub85c \ucd94\ucd9c\ud55c\ub2e4.\n    \"\"\"\n    glyph_set = font.getGlyphSet()\n\n    if glyph_name not in glyph_set:\n        return \"\", None\n\n    # SVG path \ucd94\ucd9c\n    path_pen = SVGPathPen(glyph_set)\n    glyph_set[glyph_name].draw(path_pen)\n    path_data = path_pen.getCommands()\n\n    # \uc2e4\uc81c outline bbox \ucd94\ucd9c\n    bounds_pen = BoundsPen(glyph_set)\n    glyph_set[glyph_name].draw(bounds_pen)\n    bounds = bounds_pen.bounds\n\n    return path_data, bounds\n\n\ndef get_advance_width(font: TTFont, glyph_name: str) -> Optional[int]:\n    \"\"\"\n    hmtx \ud14c\uc774\ube14\uc5d0\uc11c glyph advance width\ub97c \uac00\uc838\uc628\ub2e4.\n    \"\"\"\n    if \"hmtx\" not in font:\n        return None\n\n    metrics = font[\"hmtx\"].metrics\n\n    if glyph_name not in metrics:\n        return None\n\n    advance_width, _left_side_bearing = metrics[glyph_name]\n    return int(advance_width)\n\n\ndef get_vertical_metrics(font: TTFont) -> Tuple[int, int]:\n    \"\"\"\n    metrics layout\uc6a9 vertical box\ub97c \uad6c\ud55c\ub2e4.\n\n    \uc6b0\uc120\uc21c\uc704:\n    1. OS\/2 sTypoAscender \/ sTypoDescender\n    2. hhea ascent \/ descent\n    3. unitsPerEm fallback\n    \"\"\"\n    if \"OS\/2\" in font:\n        os2 = font[\"OS\/2\"]\n        ascender = int(getattr(os2, \"sTypoAscender\", 0))\n        descender = int(getattr(os2, \"sTypoDescender\", 0))\n\n        if ascender != 0 or descender != 0:\n            return ascender, descender\n\n    if \"hhea\" in font:\n        hhea = font[\"hhea\"]\n        return int(hhea.ascent), int(hhea.descent)\n\n    units_per_em = int(font[\"head\"].unitsPerEm)\n    return units_per_em, 0\n\n\ndef make_transform_fit(\n    bounds: Bounds,\n    size: int,\n    padding: int,\n) -> Tuple[str, float]:\n    \"\"\"\n    glyph outline bbox \uae30\uc900\uc73c\ub85c \uc544\uc774\ucf58\uc744 \uc815\ud655\ud788 \uc911\uc559 \ubc30\uce58\ud558\uace0 \ucd5c\ub300 \ud06c\uae30\ub85c \ub9de\ucd98\ub2e4.\n\n    \ud2b9\uc9d5:\n    - \uc2e4\uc81c outline\uc758 bbox\ub97c \uae30\uc900\uc73c\ub85c \ud568\n    - \uc544\uc774\ucf58\ubcc4\ub85c 256x256 \uc548\uc5d0\uc11c \ucd5c\ub300\ud55c \ud06c\uac8c \ubc30\uce58\n    - PNG \uc544\uc774\ucf58 \ucd94\ucd9c\uc6a9 \uae30\ubcf8 \ucd94\ucc9c\n    \"\"\"\n    x_min, y_min, x_max, y_max = bounds\n\n    glyph_w = x_max - x_min\n    glyph_h = y_max - y_min\n\n    if glyph_w &lt;= 0 or glyph_h &lt;= 0:\n        raise ValueError(f\"Invalid glyph bounds: {bounds}\")\n\n    usable = max(1, size - padding * 2)\n    scale = usable \/ max(glyph_w, glyph_h)\n\n    target_w = glyph_w * scale\n    target_h = glyph_h * scale\n\n    # Font \uc88c\ud45c\uacc4: Y \uc704\ucabd \uc99d\uac00\n    # SVG \uc88c\ud45c\uacc4: Y \uc544\ub798\ucabd \uc99d\uac00\n    # matrix(a b c d e f):\n    #   X = a*x + c*y + e\n    #   Y = b*x + d*y + f\n    tx = (size - target_w) \/ 2.0 - x_min * scale\n    ty = (size - target_h) \/ 2.0 + y_max * scale\n\n    transform = f\"matrix({scale:.10f} 0 0 {-scale:.10f} {tx:.10f} {ty:.10f})\"\n    return transform, scale\n\n\ndef make_transform_metrics(\n    font: TTFont,\n    glyph_name: str,\n    size: int,\n    padding: int,\n) -> Tuple[str, float]:\n    \"\"\"\n    font metrics \uae30\uc900\uc73c\ub85c \uc6d0\ub798 font cell \ub0b4 \uc704\uce58\uc640 \ud06c\uae30\ub97c \ubcf4\uc874\ud55c\ub2e4.\n\n    \ud2b9\uc9d5:\n    - glyph\ubcc4 bbox\ub85c \uaf49 \ucc44\uc6b0\uc9c0 \uc54a\uc74c\n    - advance width, ascender, descender \uae30\uc900\n    - \ud3f0\ud2b8 \ub0b4\ubd80 \ub808\uc774\uc544\uc6c3 \ube44\uad50\uc6a9\n    \"\"\"\n    units_per_em = int(font[\"head\"].unitsPerEm)\n\n    advance_width = get_advance_width(font, glyph_name)\n    if not advance_width:\n        advance_width = units_per_em\n\n    ascender, descender = get_vertical_metrics(font)\n\n    cell_w = max(1, int(advance_width))\n    cell_h = max(1, int(ascender - descender))\n\n    usable = max(1, size - padding * 2)\n    scale = min(usable \/ cell_w, usable \/ cell_h)\n\n    target_w = cell_w * scale\n    target_h = cell_h * scale\n\n    tx = (size - target_w) \/ 2.0\n    ty = (size - target_h) \/ 2.0 + ascender * scale\n\n    transform = f\"matrix({scale:.10f} 0 0 {-scale:.10f} {tx:.10f} {ty:.10f})\"\n    return transform, scale\n\n\ndef build_svg(\n    path_data: str,\n    transform: str,\n    size: int,\n    fill: str,\n    fill_rule: str,\n) -> str:\n    \"\"\"\n    \ud22c\uba85 \ubc30\uacbd SVG\ub97c \uc0dd\uc131\ud55c\ub2e4.\n\n    \ubc30\uacbd rect\ub97c \ub123\uc9c0 \uc54a\uc73c\ubbc0\ub85c PNG \ubcc0\ud658 \ud6c4\uc5d0\ub3c4 alpha\uac00 \uc720\uc9c0\ub41c\ub2e4.\n    \"\"\"\n    return f\"\"\"&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?>\n&lt;svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\"\n     width=\"{size}\"\n     height=\"{size}\"\n     viewBox=\"0 0 {size} {size}\">\n  &lt;g transform=\"{escape(transform)}\">\n    &lt;path d=\"{escape(path_data)}\"\n          fill=\"{escape(fill)}\"\n          fill-rule=\"{escape(fill_rule)}\"\/>\n  &lt;\/g>\n&lt;\/svg>\n\"\"\"\n\n\ndef render_svg_to_png(\n    cairosvg_module,\n    svg_text: str,\n    output_png: Path,\n    size: int,\n    oversample: int,\n) -> None:\n    \"\"\"\n    SVG\ub97c \uace0\ud574\uc0c1\ub3c4\ub85c \uba3c\uc800 \ub80c\ub354\ub9c1\ud55c \ub4a4 LANCZOS\ub85c 256x256\uc5d0 \ub2e4\uc6b4\uc0d8\ud50c\ub9c1\ud55c\ub2e4.\n\n    oversample=4, size=256\uc774\uba74:\n      1. 1024x1024 PNG\ub85c 1\ucc28 \ub80c\ub354\ub9c1\n      2. 256x256\uc73c\ub85c \uace0\ud488\uc9c8 \ucd95\uc18c\n    \"\"\"\n    render_size = size * oversample\n\n    png_bytes = cairosvg_module.svg2png(\n        bytestring=svg_text.encode(\"utf-8\"),\n        output_width=render_size,\n        output_height=render_size,\n    )\n\n    image = Image.open(io.BytesIO(png_bytes)).convert(\"RGBA\")\n\n    if oversample > 1:\n        image = image.resize((size, size), RESAMPLE_LANCZOS)\n\n    image.save(output_png)\n\n\ndef make_output_filename(\n    glyph_index: int,\n    glyph_name: str,\n    codepoints: List[int],\n) -> str:\n    \"\"\"\n    \ucd9c\ub825 PNG \ud30c\uc77c\uba85\uc744 \ub9cc\ub4e0\ub2e4.\n    \"\"\"\n    if codepoints:\n        unicode_part = \"_\".join(f\"U+{cp:04X}\" for cp in codepoints[:6])\n    else:\n        unicode_part = \"UNENCODED\"\n\n    safe_glyph_name = sanitize_filename(glyph_name)\n\n    return f\"{glyph_index:05d}_{unicode_part}_{safe_glyph_name}.png\"\n\n\ndef write_empty_png(path: Path, size: int) -> None:\n    \"\"\"\n    outline\uc774 \uc5c6\ub294 glyph\uc6a9 \ud22c\uba85 PNG \uc0dd\uc131.\n    \"\"\"\n    image = Image.new(\"RGBA\", (size, size), (0, 0, 0, 0))\n    image.save(path)\n\n\ndef export_glyphs(args: argparse.Namespace) -> None:\n    \"\"\"\n    \uc804\uccb4 glyph\ub97c PNG\ub85c export\ud55c\ub2e4.\n    \"\"\"\n    font_path = resolve_font_path(args.font)\n\n    output_dir = Path(args.out).resolve()\n    svg_dir = output_dir \/ \"_svg\"\n\n    output_dir.mkdir(parents=True, exist_ok=True)\n\n    if args.save_svg:\n        svg_dir.mkdir(parents=True, exist_ok=True)\n\n    print(f\"[INFO] Font   : {font_path}\")\n    print(f\"[INFO] Output : {output_dir}\")\n\n    font = TTFont(str(font_path), lazy=False, fontNumber=args.font_number)\n\n    try:\n        cairosvg_module = load_cairosvg(args.cairo_dll_dir)\n\n        unicode_by_glyph = build_unicode_map(font)\n\n        glyph_order = list(font.getGlyphOrder())\n\n        if args.encoded_only:\n            glyph_names = [\n                glyph_name\n                for glyph_name in glyph_order\n                if glyph_name in unicode_by_glyph\n            ]\n        else:\n            glyph_names = glyph_order\n\n        rows = []\n        exported_count = 0\n        skipped_count = 0\n        error_count = 0\n\n        for glyph_index, glyph_name in enumerate(glyph_names):\n            if glyph_name == \".notdef\" and not args.include_notdef:\n                skipped_count += 1\n                continue\n\n            codepoints = unicode_by_glyph.get(glyph_name, [])\n\n            try:\n                path_data, bounds = get_glyph_path_and_bounds(font, glyph_name)\n\n                if not path_data or bounds is None:\n                    if args.keep_empty:\n                        file_name = make_output_filename(\n                            glyph_index,\n                            glyph_name,\n                            codepoints,\n                        )\n                        output_png = output_dir \/ file_name\n\n                        write_empty_png(output_png, args.size)\n\n                        rows.append({\n                            \"glyph_index\": glyph_index,\n                            \"glyph_name\": glyph_name,\n                            \"unicode\": \" \".join(f\"U+{cp:04X}\" for cp in codepoints),\n                            \"output_file\": file_name,\n                            \"bbox_x_min\": \"\",\n                            \"bbox_y_min\": \"\",\n                            \"bbox_x_max\": \"\",\n                            \"bbox_y_max\": \"\",\n                            \"advance_width\": get_advance_width(font, glyph_name) or \"\",\n                            \"layout\": args.layout,\n                            \"scale\": \"\",\n                            \"status\": \"empty\",\n                            \"error\": \"\",\n                        })\n\n                        exported_count += 1\n                    else:\n                        skipped_count += 1\n\n                    continue\n\n                if args.layout == \"fit\":\n                    transform, scale = make_transform_fit(\n                        bounds=bounds,\n                        size=args.size,\n                        padding=args.padding,\n                    )\n                elif args.layout == \"metrics\":\n                    transform, scale = make_transform_metrics(\n                        font=font,\n                        glyph_name=glyph_name,\n                        size=args.size,\n                        padding=args.padding,\n                    )\n                else:\n                    raise ValueError(f\"Unknown layout: {args.layout}\")\n\n                svg_text = build_svg(\n                    path_data=path_data,\n                    transform=transform,\n                    size=args.size,\n                    fill=args.fill,\n                    fill_rule=args.fill_rule,\n                )\n\n                file_name = make_output_filename(\n                    glyph_index,\n                    glyph_name,\n                    codepoints,\n                )\n\n                output_png = output_dir \/ file_name\n\n                render_svg_to_png(\n                    cairosvg_module=cairosvg_module,\n                    svg_text=svg_text,\n                    output_png=output_png,\n                    size=args.size,\n                    oversample=args.oversample,\n                )\n\n                if args.save_svg:\n                    svg_file = svg_dir \/ file_name.replace(\".png\", \".svg\")\n                    svg_file.write_text(svg_text, encoding=\"utf-8\")\n\n                x_min, y_min, x_max, y_max = bounds\n\n                rows.append({\n                    \"glyph_index\": glyph_index,\n                    \"glyph_name\": glyph_name,\n                    \"unicode\": \" \".join(f\"U+{cp:04X}\" for cp in codepoints),\n                    \"output_file\": file_name,\n                    \"bbox_x_min\": x_min,\n                    \"bbox_y_min\": y_min,\n                    \"bbox_x_max\": x_max,\n                    \"bbox_y_max\": y_max,\n                    \"advance_width\": get_advance_width(font, glyph_name) or \"\",\n                    \"layout\": args.layout,\n                    \"scale\": f\"{scale:.10f}\",\n                    \"status\": \"exported\",\n                    \"error\": \"\",\n                })\n\n                exported_count += 1\n\n                if exported_count % args.progress_interval == 0:\n                    print(\n                        f\"[INFO] exported={exported_count}, \"\n                        f\"skipped={skipped_count}, \"\n                        f\"errors={error_count}\"\n                    )\n\n            except Exception as exc:\n                error_count += 1\n\n                rows.append({\n                    \"glyph_index\": glyph_index,\n                    \"glyph_name\": glyph_name,\n                    \"unicode\": \" \".join(f\"U+{cp:04X}\" for cp in codepoints),\n                    \"output_file\": \"\",\n                    \"bbox_x_min\": \"\",\n                    \"bbox_y_min\": \"\",\n                    \"bbox_x_max\": \"\",\n                    \"bbox_y_max\": \"\",\n                    \"advance_width\": get_advance_width(font, glyph_name) or \"\",\n                    \"layout\": args.layout,\n                    \"scale\": \"\",\n                    \"status\": \"error\",\n                    \"error\": str(exc),\n                })\n\n                print(f\"[WARN] Failed glyph={glyph_name}, index={glyph_index}: {exc}\")\n\n                if not args.continue_on_error:\n                    raise\n\n        index_csv = output_dir \/ \"glyph_index.csv\"\n\n        with index_csv.open(\"w\", newline=\"\", encoding=\"utf-8-sig\") as fp:\n            writer = csv.DictWriter(\n                fp,\n                fieldnames=[\n                    \"glyph_index\",\n                    \"glyph_name\",\n                    \"unicode\",\n                    \"output_file\",\n                    \"bbox_x_min\",\n                    \"bbox_y_min\",\n                    \"bbox_x_max\",\n                    \"bbox_y_max\",\n                    \"advance_width\",\n                    \"layout\",\n                    \"scale\",\n                    \"status\",\n                    \"error\",\n                ],\n            )\n            writer.writeheader()\n            writer.writerows(rows)\n\n        print()\n        print(\"[DONE]\")\n        print(f\"Font        : {font_path}\")\n        print(f\"Output      : {output_dir}\")\n        print(f\"Index CSV   : {index_csv}\")\n        print(f\"Exported    : {exported_count}\")\n        print(f\"Skipped     : {skipped_count}\")\n        print(f\"Errors      : {error_count}\")\n        print(f\"Image size  : {args.size}x{args.size}\")\n        print(f\"Layout      : {args.layout}\")\n        print(f\"Oversample  : {args.oversample}\")\n\n    finally:\n        font.close()\n\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description=(\n            \"Export TTF\/OTF glyph outlines to transparent PNG files. \"\n            \"The glyph outline is converted to SVG path and then rendered to PNG.\"\n        )\n    )\n\n    parser.add_argument(\n        \"--font\",\n        required=True,\n        help=(\n            \"Font file path. \"\n            'Example: \"C:\\\\Windows\\\\Fonts\\\\SegoeIcons.ttf\"'\n        ),\n    )\n\n    parser.add_argument(\n        \"--font-number\",\n        type=int,\n        default=0,\n        help=(\n            \"Font index for TTC\/collection fonts. \"\n            \"For normal TTF files, use 0. Default: 0\"\n        ),\n    )\n\n    parser.add_argument(\n        \"--out\",\n        default=\"glyph_png\",\n        help=\"Output directory. Default: glyph_png\",\n    )\n\n    parser.add_argument(\n        \"--size\",\n        type=int,\n        default=256,\n        help=\"PNG canvas size. Default: 256\",\n    )\n\n    parser.add_argument(\n        \"--padding\",\n        type=int,\n        default=16,\n        help=\"Canvas padding in pixels. Default: 16\",\n    )\n\n    parser.add_argument(\n        \"--oversample\",\n        type=int,\n        default=4,\n        help=(\n            \"Render at size*N and downsample for smoother antialiasing. \"\n            \"Default: 4\"\n        ),\n    )\n\n    parser.add_argument(\n        \"--fill\",\n        default=\"#000000\",\n        help=\"Icon fill color. Default: #000000\",\n    )\n\n    parser.add_argument(\n        \"--fill-rule\",\n        choices=[\"nonzero\", \"evenodd\"],\n        default=\"nonzero\",\n        help=\"SVG fill-rule. Default: nonzero\",\n    )\n\n    parser.add_argument(\n        \"--layout\",\n        choices=[\"fit\", \"metrics\"],\n        default=\"fit\",\n        help=(\n            \"fit: actual outline bbox \uae30\uc900\uc73c\ub85c \uc911\uc559 \ubc30\uce58\/\ucd5c\ub300\ud654. \"\n            \"metrics: font metrics \uae30\uc900\uc73c\ub85c \uc6d0\ub798 font cell \uc704\uce58\/\ud06c\uae30 \ubcf4\uc874. \"\n            \"Default: fit\"\n        ),\n    )\n\n    parser.add_argument(\n        \"--encoded-only\",\n        action=\"store_true\",\n        help=\"Unicode cmap\uc5d0 \ub9e4\ud551\ub41c glyph\ub9cc export\ud55c\ub2e4. \uae30\ubcf8\uac12\uc740 \uc804\uccb4 glyph order export.\",\n    )\n\n    parser.add_argument(\n        \"--include-notdef\",\n        action=\"store_true\",\n        help=\".notdef glyph\ub3c4 export\ud55c\ub2e4.\",\n    )\n\n    parser.add_argument(\n        \"--keep-empty\",\n        action=\"store_true\",\n        help=\"outline\uc774 \uc5c6\ub294 glyph\ub3c4 \ud22c\uba85 PNG\ub85c \uc0dd\uc131\ud55c\ub2e4.\",\n    )\n\n    parser.add_argument(\n        \"--save-svg\",\n        action=\"store_true\",\n        help=\"\uc911\uac04 SVG \ud30c\uc77c\ub3c4 _svg \ub514\ub809\ud130\ub9ac\uc5d0 \uc800\uc7a5\ud55c\ub2e4.\",\n    )\n\n    parser.add_argument(\n        \"--cairo-dll-dir\",\n        default=None,\n        help=(\n            \"Windows\uc5d0\uc11c Cairo DLL\uc744 \ucc3e\uc9c0 \ubabb\ud560 \ub54c GTK\/Cairo\/MSYS2 bin \ub514\ub809\ud130\ub9ac \uc9c0\uc815. \"\n            'Example: \"C:\\\\msys64\\\\mingw64\\\\bin\"'\n        ),\n    )\n\n    parser.add_argument(\n        \"--continue-on-error\",\n        action=\"store_true\",\n        default=True,\n        help=\"\uac1c\ubcc4 glyph \ubcc0\ud658 \uc2e4\ud328 \uc2dc \uacc4\uc18d \uc9c4\ud589\ud55c\ub2e4. \uae30\ubcf8\uac12: enabled\",\n    )\n\n    parser.add_argument(\n        \"--stop-on-error\",\n        action=\"store_false\",\n        dest=\"continue_on_error\",\n        help=\"\uac1c\ubcc4 glyph \ubcc0\ud658 \uc2e4\ud328 \uc2dc \uc989\uc2dc \uc911\ub2e8\ud55c\ub2e4.\",\n    )\n\n    parser.add_argument(\n        \"--progress-interval\",\n        type=int,\n        default=100,\n        help=\"\uba87 \uac1c glyph\ub9c8\ub2e4 \uc9c4\ud589\ub960\uc744 \ucd9c\ub825\ud560\uc9c0 \uc9c0\uc815. Default: 100\",\n    )\n\n    args = parser.parse_args()\n\n    if args.size &lt;= 0:\n        raise ValueError(\"--size must be positive\")\n\n    if args.padding &lt; 0:\n        raise ValueError(\"--padding must be zero or positive\")\n\n    if args.padding * 2 >= args.size:\n        raise ValueError(\"--padding is too large for --size\")\n\n    if args.oversample &lt;= 0:\n        raise ValueError(\"--oversample must be positive\")\n\n    if args.font_number &lt; 0:\n        raise ValueError(\"--font-number must be zero or positive\")\n\n    if args.progress_interval &lt;= 0:\n        raise ValueError(\"--progress-interval must be positive\")\n\n    return args\n\n\ndef main() -> None:\n    try:\n        args = parse_args()\n        export_glyphs(args)\n    except Exception as exc:\n        print()\n        print(\"[FAILED]\")\n        print(str(exc))\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>1. \ud3f0\ud2b8 \ub2e4\uc6b4\ub85c\ub4dchttps:\/\/learn.microsoft.com\/ko-kr\/windows\/apps\/design\/downloads\/#fonts2. Python \ud328\ud0a4\uc9c0 \uc124\uce58PYTHON -m pip install &#8211;upgrade pipPYTHON -m pip install fonttools cairosvg pillow3. MSYS2 Cairo \uc124\uce58CD \/D c:\\PortableApps\\cmd_msys64\\usr\\binbash&#8212;&#8211;pacman -Syu &#8211;noconfirmpacman -S &#8211;noconfirm &#8211;needed mingw-w64-x86_64-cairo&#8212;&#8211;DIR c:\\PortableApps\\cmd_msys64\\mingw64\\bin\\libcairo-2.dll4. \uc544\ub798 \uc2a4\ud06c\ub9bd\ud2b8\ub85c \uc2e4\ud589PYTHON export_glyph_png.py ^&#8211;font &#8220;C:\\Windows\\Fonts\\Segoe Fluent Icons.ttf&#8221; ^&#8211;out &#8220;d:\\downloads\\segoe_fluent_png&#8221; ^&#8211;size 256 ^&#8211;padding 16 ^&#8211;oversample 4 ^&#8211;save-svg ^&#8211;cairo-dll-dir &#8220;c:\\PortableApps\\cmd_msys64\\mingw64\\bin&#8221;<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"set","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[23,19],"tags":[],"class_list":["post-9063","post","type-post","status-publish","format-standard","hentry","category-development_web","category-development_lib"],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=\/wp\/v2\/posts\/9063","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=9063"}],"version-history":[{"count":0,"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=\/wp\/v2\/posts\/9063\/revisions"}],"wp:attachment":[{"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=9063"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=9063"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=9063"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}