geny-svgforge 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ name: publish
2
+
3
+ # PyPI Trusted Publishing (OIDC). Configured publisher:
4
+ # project: geny-svgforge · owner: CocoRoF · repo: geny-svgforge
5
+ # workflow: workflow.yml · environment: pypi
6
+ # A published GitHub Release (or manual dispatch) builds and uploads to PyPI.
7
+
8
+ on:
9
+ release:
10
+ types: [published]
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ build:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: "3.12"
24
+ - name: Build sdist and wheel
25
+ run: |
26
+ python -m pip install --upgrade build
27
+ python -m build
28
+ - name: Smoke-check metadata
29
+ run: |
30
+ python -m pip install --upgrade twine
31
+ python -m twine check dist/*
32
+ - uses: actions/upload-artifact@v4
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+
37
+ publish:
38
+ needs: build
39
+ runs-on: ubuntu-latest
40
+ environment: pypi
41
+ permissions:
42
+ id-token: write # OIDC token for trusted publishing
43
+ steps:
44
+ - uses: actions/download-artifact@v4
45
+ with:
46
+ name: dist
47
+ path: dist/
48
+ - name: Publish to PyPI
49
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,22 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.egg-info/
5
+ .eggs/
6
+ dist/
7
+ build/
8
+ .venv/
9
+ .uv/
10
+
11
+ # caches
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+ .mypy_cache/
15
+
16
+ # generated demo output
17
+ examples/out.svg
18
+ examples/out.png
19
+ examples/probe.png
20
+
21
+ # OS
22
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jang HaRyeom (CocoRoF)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.4
2
+ Name: geny-svgforge
3
+ Version: 0.1.0
4
+ Summary: AI가 좌표 대신 의미(spec)만 내면 결정론적 레이아웃으로 깨끗한 다이어그램 SVG를 만드는 라이브러리
5
+ Project-URL: Homepage, https://github.com/CocoRoF/geny-svgforge
6
+ Project-URL: Repository, https://github.com/CocoRoF/geny-svgforge
7
+ Project-URL: Issues, https://github.com/CocoRoF/geny-svgforge/issues
8
+ Author: Jang HaRyeom (CocoRoF)
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,diagram,layout,llm,mcp,svg
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Multimedia :: Graphics
20
+ Classifier: Topic :: Scientific/Engineering :: Visualization
21
+ Classifier: Topic :: Text Processing :: Markup
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: fonttools>=4.40
24
+ Requires-Dist: pydantic>=2.5
25
+ Provides-Extra: dev
26
+ Requires-Dist: cairosvg>=2.7; extra == 'dev'
27
+ Requires-Dist: pytest>=8; extra == 'dev'
28
+ Provides-Extra: mcp
29
+ Requires-Dist: mcp>=1.2; extra == 'mcp'
30
+ Provides-Extra: png
31
+ Requires-Dist: cairosvg>=2.7; extra == 'png'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # geny-svgforge
35
+
36
+ **Give an AI a semantic spec instead of raw coordinates, and a deterministic layout engine produces a clean diagram SVG with zero overlaps or clipping.**
37
+
38
+ When an LLM writes `<svg>` by hand, it can't reliably reason about text widths, box bounds, curve paths, or the viewBox — so elements overlap and captions get clipped. geny-svgforge inserts a layout layer between the AI and the SVG. The AI emits only a JSON **spec** (*"two rows of labeled tokens, posN labels, connect A's token 2 to B's token 3 with a curve, a side note, a caption"*) — no coordinates. The library measures text with real font metrics, sizes every box, routes connectors around obstacles, and fits the viewBox to the content. **Overlap and overflow become structurally impossible.**
39
+
40
+ > Same lineage as edit2ppt (AI emits PPT structure, engine renders) and Contextifier (structure-preserving document parsing).
41
+
42
+ ---
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install geny-svgforge # core (SVG)
48
+ pip install 'geny-svgforge[png]' # + PNG export (cairosvg)
49
+ pip install 'geny-svgforge[mcp]' # + MCP server
50
+ ```
51
+
52
+ ## Quickstart (Python)
53
+
54
+ ```python
55
+ from geny_svgforge import render
56
+
57
+ spec = {
58
+ "type": "token-sequence",
59
+ "title": "Absolute position changes, relative pattern remains",
60
+ "rows": [
61
+ {"label": "sentence A", "tokens": [
62
+ {"text": "나는", "pos": "pos 0"},
63
+ {"text": "오늘", "pos": "pos 1"},
64
+ {"text": "밥을", "pos": "pos 2", "id": "a2"},
65
+ {"text": "먹었다", "pos": "pos 3", "variant": "highlight"},
66
+ ]},
67
+ {"label": "sentence B", "tokens": [
68
+ {"text": "나는", "pos": "pos 0"},
69
+ {"text": "정말", "pos": "pos 1", "variant": "accent"},
70
+ {"text": "오늘", "pos": "pos 2"},
71
+ {"text": "밥을", "pos": "pos 3", "id": "b3"},
72
+ {"text": "먹었다", "pos": "pos 4", "variant": "highlight"},
73
+ ]},
74
+ ],
75
+ "connectors": [{"from": "a2", "to": "b3", "color": "accent"}],
76
+ "note": {"title": "What the model must learn",
77
+ "lines": ["Memorizing absolute positions is brittle to length changes.",
78
+ "Relative distance and surrounding patterns are handled in attention."]},
79
+ "caption": "Same relative token relationship survives an absolute shift",
80
+ }
81
+
82
+ result = render(spec) # portable SVG with the used glyphs embedded
83
+ print(result.warnings) # [] ← no overlap / no clipping (lint passed)
84
+ open("out.svg", "w").write(result.svg)
85
+ ```
86
+
87
+ ## CLI
88
+
89
+ ```bash
90
+ geny-svgforge render spec.json -o out.svg
91
+ geny-svgforge render spec.json -o out.png # PNG (requires [png])
92
+ geny-svgforge validate spec.json # validate before rendering
93
+ geny-svgforge schema -o schema.json # dump the JSON Schema
94
+ ```
95
+
96
+ ## MCP server
97
+
98
+ geny-svgforge ships an [MCP](https://modelcontextprotocol.io) server over **stdio** so any MCP-compatible agent can request diagrams. After `pip install 'geny-svgforge[mcp]'` the server is launched with:
99
+
100
+ ```bash
101
+ geny-svgforge-mcp # console script
102
+ # or
103
+ python -m geny_svgforge.mcp_server
104
+ ```
105
+
106
+ ### Tools exposed
107
+
108
+ | Tool | Input | Returns |
109
+ |---|---|---|
110
+ | `get_diagram_schema` | – | JSON Schema describing the spec (the agent learns the format from this) |
111
+ | `validate_diagram_spec` | `spec` | `{ ok, errors[], warnings[] }` — check before rendering |
112
+ | `render_diagram` | `spec` | `{ svg, width, height, warnings[] }` — if `warnings` is non-empty, fix the spec and call again |
113
+
114
+ ### Client configuration
115
+
116
+ Add the server to your MCP client config. The standard shape is an `mcpServers` map keyed by a server name.
117
+
118
+ **Claude Desktop** (`claude_desktop_config.json`), **Cursor** (`~/.cursor/mcp.json`), or **Claude Code** (`.mcp.json`):
119
+
120
+ ```json
121
+ {
122
+ "mcpServers": {
123
+ "geny-svgforge": {
124
+ "command": "geny-svgforge-mcp"
125
+ }
126
+ }
127
+ }
128
+ ```
129
+
130
+ Zero-install with [uv](https://docs.astral.sh/uv/) (no prior `pip install` needed):
131
+
132
+ ```json
133
+ {
134
+ "mcpServers": {
135
+ "geny-svgforge": {
136
+ "command": "uvx",
137
+ "args": ["--from", "geny-svgforge[mcp]", "geny-svgforge-mcp"]
138
+ }
139
+ }
140
+ }
141
+ ```
142
+
143
+ Claude Code can also add it from the CLI:
144
+
145
+ ```bash
146
+ claude mcp add geny-svgforge -- uvx --from 'geny-svgforge[mcp]' geny-svgforge-mcp
147
+ ```
148
+
149
+ A typical agent flow: call `get_diagram_schema` once to learn the format → emit a `spec` → call `render_diagram` → if `warnings` is non-empty, repair the spec and retry.
150
+
151
+ ---
152
+
153
+ ## How it works
154
+
155
+ Three layers: **Spec (JSON Schema) → Layout Engine → Renderer**.
156
+
157
+ - **Real font metrics** — text width is computed in pixels by summing glyph advances via `fontTools` (no browser, no headless engine). The same font used for measurement is embedded into the SVG, so **measured layout == rendered output**.
158
+ - **Deterministic layout** — boxes are sized to their text, connectors are routed through the inter-row band away from boxes, and the viewBox/padding is derived from the bounding box of every element — so nothing can clip.
159
+ - **Font embedding** — only the glyphs actually used are subset and inlined as a base64 `@font-face`, so the SVG renders identically everywhere (browsers, resvg). `to_png()` renders via the installed font (`raster_safe`) because cairosvg ignores embedded `@font-face`.
160
+ - **Lint** — a post-layout pass flags box overlaps and canvas overflow. It should always be empty; if not, the warnings are returned to the agent so it can fix the spec.
161
+
162
+ ## Diagram types
163
+
164
+ | Type | Description |
165
+ |---|---|
166
+ | `token-sequence` | Rows of position-labeled token boxes, with inter-row connectors, a side note, and a caption |
167
+
168
+ (`flow`, `grid`, `stack`, `callout`, … are planned — the spec is extensible via the `type` field.)
169
+
170
+ ## Roadmap
171
+
172
+ - Automatic collision resolution (constraint / force based)
173
+ - Visual self-repair loop: render → rasterize → multimodal critique → fix spec → re-render
174
+ - More diagram types, themes, templates, accessibility (`<title>`/`<desc>`/aria)
175
+
176
+ ## License
177
+
178
+ MIT. Text is measured against — and a subset is embedded from — a system font (e.g. SIL OFL Noto Sans CJK). When embedding, the embedded font's own license applies.
@@ -0,0 +1,145 @@
1
+ # geny-svgforge
2
+
3
+ **Give an AI a semantic spec instead of raw coordinates, and a deterministic layout engine produces a clean diagram SVG with zero overlaps or clipping.**
4
+
5
+ When an LLM writes `<svg>` by hand, it can't reliably reason about text widths, box bounds, curve paths, or the viewBox — so elements overlap and captions get clipped. geny-svgforge inserts a layout layer between the AI and the SVG. The AI emits only a JSON **spec** (*"two rows of labeled tokens, posN labels, connect A's token 2 to B's token 3 with a curve, a side note, a caption"*) — no coordinates. The library measures text with real font metrics, sizes every box, routes connectors around obstacles, and fits the viewBox to the content. **Overlap and overflow become structurally impossible.**
6
+
7
+ > Same lineage as edit2ppt (AI emits PPT structure, engine renders) and Contextifier (structure-preserving document parsing).
8
+
9
+ ---
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install geny-svgforge # core (SVG)
15
+ pip install 'geny-svgforge[png]' # + PNG export (cairosvg)
16
+ pip install 'geny-svgforge[mcp]' # + MCP server
17
+ ```
18
+
19
+ ## Quickstart (Python)
20
+
21
+ ```python
22
+ from geny_svgforge import render
23
+
24
+ spec = {
25
+ "type": "token-sequence",
26
+ "title": "Absolute position changes, relative pattern remains",
27
+ "rows": [
28
+ {"label": "sentence A", "tokens": [
29
+ {"text": "나는", "pos": "pos 0"},
30
+ {"text": "오늘", "pos": "pos 1"},
31
+ {"text": "밥을", "pos": "pos 2", "id": "a2"},
32
+ {"text": "먹었다", "pos": "pos 3", "variant": "highlight"},
33
+ ]},
34
+ {"label": "sentence B", "tokens": [
35
+ {"text": "나는", "pos": "pos 0"},
36
+ {"text": "정말", "pos": "pos 1", "variant": "accent"},
37
+ {"text": "오늘", "pos": "pos 2"},
38
+ {"text": "밥을", "pos": "pos 3", "id": "b3"},
39
+ {"text": "먹었다", "pos": "pos 4", "variant": "highlight"},
40
+ ]},
41
+ ],
42
+ "connectors": [{"from": "a2", "to": "b3", "color": "accent"}],
43
+ "note": {"title": "What the model must learn",
44
+ "lines": ["Memorizing absolute positions is brittle to length changes.",
45
+ "Relative distance and surrounding patterns are handled in attention."]},
46
+ "caption": "Same relative token relationship survives an absolute shift",
47
+ }
48
+
49
+ result = render(spec) # portable SVG with the used glyphs embedded
50
+ print(result.warnings) # [] ← no overlap / no clipping (lint passed)
51
+ open("out.svg", "w").write(result.svg)
52
+ ```
53
+
54
+ ## CLI
55
+
56
+ ```bash
57
+ geny-svgforge render spec.json -o out.svg
58
+ geny-svgforge render spec.json -o out.png # PNG (requires [png])
59
+ geny-svgforge validate spec.json # validate before rendering
60
+ geny-svgforge schema -o schema.json # dump the JSON Schema
61
+ ```
62
+
63
+ ## MCP server
64
+
65
+ geny-svgforge ships an [MCP](https://modelcontextprotocol.io) server over **stdio** so any MCP-compatible agent can request diagrams. After `pip install 'geny-svgforge[mcp]'` the server is launched with:
66
+
67
+ ```bash
68
+ geny-svgforge-mcp # console script
69
+ # or
70
+ python -m geny_svgforge.mcp_server
71
+ ```
72
+
73
+ ### Tools exposed
74
+
75
+ | Tool | Input | Returns |
76
+ |---|---|---|
77
+ | `get_diagram_schema` | – | JSON Schema describing the spec (the agent learns the format from this) |
78
+ | `validate_diagram_spec` | `spec` | `{ ok, errors[], warnings[] }` — check before rendering |
79
+ | `render_diagram` | `spec` | `{ svg, width, height, warnings[] }` — if `warnings` is non-empty, fix the spec and call again |
80
+
81
+ ### Client configuration
82
+
83
+ Add the server to your MCP client config. The standard shape is an `mcpServers` map keyed by a server name.
84
+
85
+ **Claude Desktop** (`claude_desktop_config.json`), **Cursor** (`~/.cursor/mcp.json`), or **Claude Code** (`.mcp.json`):
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "geny-svgforge": {
91
+ "command": "geny-svgforge-mcp"
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ Zero-install with [uv](https://docs.astral.sh/uv/) (no prior `pip install` needed):
98
+
99
+ ```json
100
+ {
101
+ "mcpServers": {
102
+ "geny-svgforge": {
103
+ "command": "uvx",
104
+ "args": ["--from", "geny-svgforge[mcp]", "geny-svgforge-mcp"]
105
+ }
106
+ }
107
+ }
108
+ ```
109
+
110
+ Claude Code can also add it from the CLI:
111
+
112
+ ```bash
113
+ claude mcp add geny-svgforge -- uvx --from 'geny-svgforge[mcp]' geny-svgforge-mcp
114
+ ```
115
+
116
+ A typical agent flow: call `get_diagram_schema` once to learn the format → emit a `spec` → call `render_diagram` → if `warnings` is non-empty, repair the spec and retry.
117
+
118
+ ---
119
+
120
+ ## How it works
121
+
122
+ Three layers: **Spec (JSON Schema) → Layout Engine → Renderer**.
123
+
124
+ - **Real font metrics** — text width is computed in pixels by summing glyph advances via `fontTools` (no browser, no headless engine). The same font used for measurement is embedded into the SVG, so **measured layout == rendered output**.
125
+ - **Deterministic layout** — boxes are sized to their text, connectors are routed through the inter-row band away from boxes, and the viewBox/padding is derived from the bounding box of every element — so nothing can clip.
126
+ - **Font embedding** — only the glyphs actually used are subset and inlined as a base64 `@font-face`, so the SVG renders identically everywhere (browsers, resvg). `to_png()` renders via the installed font (`raster_safe`) because cairosvg ignores embedded `@font-face`.
127
+ - **Lint** — a post-layout pass flags box overlaps and canvas overflow. It should always be empty; if not, the warnings are returned to the agent so it can fix the spec.
128
+
129
+ ## Diagram types
130
+
131
+ | Type | Description |
132
+ |---|---|
133
+ | `token-sequence` | Rows of position-labeled token boxes, with inter-row connectors, a side note, and a caption |
134
+
135
+ (`flow`, `grid`, `stack`, `callout`, … are planned — the spec is extensible via the `type` field.)
136
+
137
+ ## Roadmap
138
+
139
+ - Automatic collision resolution (constraint / force based)
140
+ - Visual self-repair loop: render → rasterize → multimodal critique → fix spec → re-render
141
+ - More diagram types, themes, templates, accessibility (`<title>`/`<desc>`/aria)
142
+
143
+ ## License
144
+
145
+ MIT. Text is measured against — and a subset is embedded from — a system font (e.g. SIL OFL Noto Sans CJK). When embedding, the embedded font's own license applies.
@@ -0,0 +1,28 @@
1
+ """예시 spec 을 깨끗한 SVG 로 렌더하고(PNG 도) 린트 결과를 출력."""
2
+ import json
3
+ import pathlib
4
+ import sys
5
+
6
+ from geny_svgforge import render
7
+ from geny_svgforge.api import to_png
8
+
9
+ HERE = pathlib.Path(__file__).parent
10
+ spec_path = HERE / (sys.argv[1] if len(sys.argv) > 1 else "token_sequence_positions.json")
11
+ spec = json.loads(spec_path.read_text(encoding="utf-8"))
12
+
13
+ # 프로덕션 SVG (폰트 임베드 — 브라우저/resvg 에서 완전 이식)
14
+ result = render(spec)
15
+ out_svg = HERE / "out.svg"
16
+ out_svg.write_text(result.svg, encoding="utf-8")
17
+ print(f"canvas: {result.width:.0f} x {result.height:.0f}")
18
+ print(f"lint warnings ({len(result.warnings)}):")
19
+ for w in result.warnings:
20
+ print(" -", w)
21
+ print(f"wrote {out_svg} ({len(result.svg)} bytes)")
22
+
23
+ try:
24
+ out_png = HERE / "out.png"
25
+ out_png.write_bytes(to_png(spec, scale=2.0))
26
+ print(f"wrote {out_png}")
27
+ except Exception as e: # noqa: BLE001
28
+ print(f"(png skip: {e})")
@@ -0,0 +1,38 @@
1
+ {
2
+ "type": "token-sequence",
3
+ "title": "Absolute position changes, relative pattern remains",
4
+ "subtitle": "중간에 토큰이 추가되면 뒤쪽 토큰의 절대 위치는 밀린다. 모델은 이런 이동에도 유지되는 관계를 배워야 한다.",
5
+ "theme": "light",
6
+ "rows": [
7
+ {
8
+ "label": "sentence A",
9
+ "tokens": [
10
+ { "text": "나는", "pos": "pos 0" },
11
+ { "text": "오늘", "pos": "pos 1" },
12
+ { "text": "밥을", "pos": "pos 2", "id": "a2" },
13
+ { "text": "먹었다", "pos": "pos 3", "id": "a3", "variant": "highlight" }
14
+ ]
15
+ },
16
+ {
17
+ "label": "sentence B",
18
+ "tokens": [
19
+ { "text": "나는", "pos": "pos 0" },
20
+ { "text": "정말", "pos": "pos 1", "variant": "accent" },
21
+ { "text": "오늘", "pos": "pos 2" },
22
+ { "text": "밥을", "pos": "pos 3", "id": "b3" },
23
+ { "text": "먹었다", "pos": "pos 4", "id": "b4", "variant": "highlight" }
24
+ ]
25
+ }
26
+ ],
27
+ "connectors": [
28
+ { "from": "a2", "to": "b3", "style": "arc", "color": "accent" }
29
+ ],
30
+ "note": {
31
+ "title": "모델이 배워야 하는 것",
32
+ "lines": [
33
+ "절대 position만 외우면 문장 길이 변화에 약하다.",
34
+ "상대 거리와 주변 패턴을 attention에서 같이 다룬다."
35
+ ]
36
+ },
37
+ "caption": "절대 위치가 바뀌어도 상대 토큰 관계가 유지되는 문장 예시"
38
+ }
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "geny-svgforge"
7
+ version = "0.1.0"
8
+ description = "AI가 좌표 대신 의미(spec)만 내면 결정론적 레이아웃으로 깨끗한 다이어그램 SVG를 만드는 라이브러리"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Jang HaRyeom (CocoRoF)" }]
13
+ keywords = ["svg", "diagram", "ai", "mcp", "layout", "llm"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Multimedia :: Graphics",
23
+ "Topic :: Scientific/Engineering :: Visualization",
24
+ "Topic :: Text Processing :: Markup",
25
+ ]
26
+ dependencies = [
27
+ "pydantic>=2.5",
28
+ "fonttools>=4.40",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/CocoRoF/geny-svgforge"
33
+ Repository = "https://github.com/CocoRoF/geny-svgforge"
34
+ Issues = "https://github.com/CocoRoF/geny-svgforge/issues"
35
+
36
+ [project.optional-dependencies]
37
+ png = ["cairosvg>=2.7"]
38
+ mcp = ["mcp>=1.2"]
39
+ dev = ["pytest>=8", "cairosvg>=2.7"]
40
+
41
+ [project.scripts]
42
+ geny-svgforge = "geny_svgforge.cli:main"
43
+ geny-svgforge-mcp = "geny_svgforge.mcp_server:main"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/geny_svgforge"]
@@ -0,0 +1,25 @@
1
+ """geny-svgforge — AI가 의미(spec)만 내면 결정론적 레이아웃으로 깨끗한 다이어그램 SVG를 만든다."""
2
+ from __future__ import annotations
3
+
4
+ from .api import RenderResult, render, validate_spec
5
+ from .spec import (
6
+ Connector,
7
+ Note,
8
+ Row,
9
+ Token,
10
+ TokenSequenceSpec,
11
+ json_schema,
12
+ )
13
+
14
+ __version__ = "0.1.0"
15
+ __all__ = [
16
+ "render",
17
+ "validate_spec",
18
+ "RenderResult",
19
+ "json_schema",
20
+ "TokenSequenceSpec",
21
+ "Row",
22
+ "Token",
23
+ "Connector",
24
+ "Note",
25
+ ]
@@ -0,0 +1,103 @@
1
+ """공개 API. AI 에이전트/서버/CLI 가 쓰는 진입점."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, Union
6
+
7
+ from .fonts import EMBED_FAMILY, FONT_FAMILY, default_fonts, subset_b64
8
+ from .geometry import TextEl
9
+ from .layout import build_scene
10
+ from .lint import lint
11
+ from .render import render_svg
12
+ from .spec import TokenSequenceSpec
13
+
14
+
15
+ @dataclass
16
+ class RenderResult:
17
+ svg: str
18
+ width: float
19
+ height: float
20
+ warnings: list[str] = field(default_factory=list)
21
+
22
+
23
+ def _coerce(spec: Union[dict, TokenSequenceSpec]) -> TokenSequenceSpec:
24
+ if isinstance(spec, TokenSequenceSpec):
25
+ return spec
26
+ return TokenSequenceSpec.model_validate(spec)
27
+
28
+
29
+ def _embed_css(scene) -> tuple[str, str]:
30
+ """사용된 글자만 subset 해 base64 @font-face 로 임베드.
31
+
32
+ 측정에 쓴 폰트를 그대로 임베드하므로 어떤 렌더러에서도 측정==렌더가 성립한다(완전 이식성).
33
+ 반환: (font_faces_css, font_family).
34
+ """
35
+ reg, bold = default_fonts()
36
+ reg_chars: set[str] = set()
37
+ bold_chars: set[str] = set()
38
+ for el in scene.elements:
39
+ if isinstance(el, TextEl):
40
+ (bold_chars if el.weight == "bold" else reg_chars).update(el.text)
41
+ css = ""
42
+ if reg_chars:
43
+ b = subset_b64(reg.path, reg.ttc_index, reg_chars)
44
+ if b:
45
+ css += (f"@font-face{{font-family:'{EMBED_FAMILY}';font-weight:400;"
46
+ f"src:url(data:font/ttf;base64,{b}) format('truetype');}}")
47
+ if bold_chars:
48
+ b = subset_b64(bold.path, bold.ttc_index, bold_chars)
49
+ if b:
50
+ css += (f"@font-face{{font-family:'{EMBED_FAMILY}';font-weight:700;"
51
+ f"src:url(data:font/ttf;base64,{b}) format('truetype');}}")
52
+ real = default_fonts()[0].family_name
53
+ family = f"'{EMBED_FAMILY}', '{real}', {FONT_FAMILY}" if css else FONT_FAMILY
54
+ return css, family
55
+
56
+
57
+ def render(
58
+ spec: Union[dict, TokenSequenceSpec],
59
+ embed_font: bool = True,
60
+ raster_safe: bool = False,
61
+ ) -> RenderResult:
62
+ """spec(dict or model) -> 깨끗한 SVG + 린트 경고.
63
+
64
+ 잘못된 spec 은 pydantic ValidationError 를 던진다(렌더 전 차단).
65
+ - embed_font=True: 사용된 글자를 subset 해 @font-face 로 임베드(브라우저·resvg 에서 완전 이식).
66
+ - raster_safe=True: 임베드 대신 실제 설치 폰트 family 를 우선 지정(cairosvg 등 @font-face 미지원
67
+ 래스터라이저에서 CJK 가 깨지지 않게). PNG 내보내기에 사용.
68
+ """
69
+ model = _coerce(spec)
70
+ scene = build_scene(model)
71
+ warnings = lint(scene)
72
+ if raster_safe:
73
+ real = default_fonts()[0].family_name
74
+ svg = render_svg(scene, font_family=f"'{real}', {FONT_FAMILY}")
75
+ elif embed_font:
76
+ css, family = _embed_css(scene)
77
+ svg = render_svg(scene, font_family=family, font_faces_css=css)
78
+ else:
79
+ svg = render_svg(scene)
80
+ return RenderResult(svg=svg, width=scene.width, height=scene.height, warnings=warnings)
81
+
82
+
83
+ def to_png(spec: Union[dict, TokenSequenceSpec], scale: float = 2.0) -> bytes:
84
+ """PNG 바이트. cairosvg 필요(optional dep 'png'). @font-face 미지원 래스터라이저를 위해
85
+ 실제 설치 폰트로 렌더한다(설치 폰트가 없으면 resvg + 임베드 SVG 사용 권장)."""
86
+ import cairosvg
87
+
88
+ res = render(spec, raster_safe=True)
89
+ return cairosvg.svg2png(
90
+ bytestring=res.svg.encode("utf-8"), output_width=int(res.width * scale)
91
+ )
92
+
93
+
94
+ def validate_spec(spec: dict) -> dict[str, Any]:
95
+ """렌더 없이 spec 만 검증. {ok, errors[], warnings[]}."""
96
+ from pydantic import ValidationError
97
+
98
+ try:
99
+ model = _coerce(spec)
100
+ except ValidationError as e:
101
+ return {"ok": False, "errors": e.errors(include_url=False), "warnings": []}
102
+ scene = build_scene(model)
103
+ return {"ok": True, "errors": [], "warnings": lint(scene)}