|
import graphviz |
|
import json |
|
from tempfile import NamedTemporaryFile |
|
import os |
|
from PIL import Image |
|
import platform |
|
import subprocess |
|
|
|
def setup_korean_font_env(): |
|
"""ํ๊ธ ํฐํธ ํ๊ฒฝ ์ค์ """ |
|
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) |
|
FONT_PATH = os.path.join(CURRENT_DIR, 'NanumGothic-Regular.ttf') |
|
|
|
|
|
if not os.path.exists(FONT_PATH): |
|
print(f"[๊ฒฝ๊ณ ] ํ๊ธ ํฐํธ ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค: {FONT_PATH}") |
|
return None |
|
|
|
|
|
os.environ['GDFONTPATH'] = CURRENT_DIR |
|
|
|
|
|
fonts_conf_path = os.path.join(CURRENT_DIR, 'fonts.conf') |
|
fonts_conf_content = f"""<?xml version="1.0"?> |
|
<!DOCTYPE fontconfig SYSTEM "fonts.dtd"> |
|
<fontconfig> |
|
<dir>{CURRENT_DIR}</dir> |
|
<cachedir>/tmp/fontconfig-cache</cachedir> |
|
|
|
<match target="pattern"> |
|
<test name="family"> |
|
<string>NanumGothic</string> |
|
</test> |
|
<edit name="family" mode="assign" binding="same"> |
|
<string>NanumGothic-Regular</string> |
|
</edit> |
|
</match> |
|
|
|
<match target="pattern"> |
|
<test name="family"> |
|
<string>NanumGothic-Regular</string> |
|
</test> |
|
<edit name="file" mode="assign" binding="same"> |
|
<string>{FONT_PATH}</string> |
|
</edit> |
|
</match> |
|
|
|
<alias binding="same"> |
|
<family>NanumGothic</family> |
|
<default> |
|
<family>NanumGothic-Regular</family> |
|
</default> |
|
</alias> |
|
</fontconfig>""" |
|
|
|
with open(fonts_conf_path, 'w', encoding='utf-8') as f: |
|
f.write(fonts_conf_content) |
|
|
|
os.environ['FONTCONFIG_FILE'] = fonts_conf_path |
|
os.environ['FONTCONFIG_PATH'] = CURRENT_DIR |
|
|
|
return FONT_PATH |
|
|
|
|
|
FONT_PATH = setup_korean_font_env() |
|
|
|
def generate_process_flow_diagram(json_input: str, output_format: str) -> str: |
|
""" |
|
Generates a Process Flow Diagram (Flowchart) from JSON input. |
|
""" |
|
try: |
|
if not json_input.strip(): |
|
return "Error: Empty input" |
|
|
|
data = json.loads(json_input) |
|
|
|
|
|
if 'start_node' not in data or 'nodes' not in data or 'connections' not in data: |
|
raise ValueError("Missing required fields: 'start_node', 'nodes', or 'connections'") |
|
|
|
|
|
node_shapes = { |
|
"process": "box", |
|
"decision": "diamond", |
|
"start": "oval", |
|
"end": "oval", |
|
"io": "parallelogram", |
|
"document": "note", |
|
"default": "box" |
|
} |
|
|
|
|
|
dot = graphviz.Digraph( |
|
name='ProcessFlowDiagram', |
|
format='png', |
|
encoding='utf-8', |
|
graph_attr={ |
|
'rankdir': 'TB', |
|
'splines': 'ortho', |
|
'bgcolor': 'white', |
|
'pad': '0.2', |
|
'margin': '0.2', |
|
'nodesep': '0.4', |
|
'ranksep': '0.6', |
|
'fontname': 'NanumGothic-Regular', |
|
'charset': 'UTF-8', |
|
'dpi': '96', |
|
'size': '10,7.5', |
|
'ratio': 'compress' |
|
}, |
|
node_attr={ |
|
'fontname': 'NanumGothic-Regular', |
|
'fontsize': '12', |
|
'charset': 'UTF-8', |
|
'height': '0.6', |
|
'width': '2.0', |
|
'margin': '0.2,0.1' |
|
}, |
|
edge_attr={ |
|
'fontname': 'NanumGothic-Regular', |
|
'fontsize': '9', |
|
'charset': 'UTF-8' |
|
} |
|
) |
|
|
|
base_color = '#19191a' |
|
fill_color_for_nodes = base_color |
|
font_color_for_nodes = 'white' if base_color == '#19191a' or base_color.lower() in ['#000000', '#19191a'] else 'black' |
|
|
|
|
|
all_defined_nodes = {node['id']: node for node in data['nodes']} |
|
|
|
|
|
start_node_id = data['start_node'] |
|
dot.node( |
|
start_node_id, |
|
start_node_id, |
|
shape=node_shapes['start'], |
|
style='filled,rounded', |
|
fillcolor='#2196F3', |
|
fontcolor='white', |
|
fontsize='12', |
|
height='0.5', |
|
width='1.8' |
|
) |
|
|
|
|
|
for node_id, node_info in all_defined_nodes.items(): |
|
if node_id == start_node_id: |
|
continue |
|
|
|
node_type = node_info.get("type", "default") |
|
shape = node_shapes.get(node_type, "box") |
|
node_label = node_info['label'] |
|
|
|
|
|
if node_type == 'decision': |
|
height = '0.8' |
|
width = '1.5' |
|
else: |
|
height = '0.6' |
|
width = '2.0' |
|
|
|
if node_type == 'end': |
|
dot.node( |
|
node_id, |
|
node_label, |
|
shape=shape, |
|
style='filled,rounded', |
|
fillcolor='#F44336', |
|
fontcolor='white', |
|
fontsize='12', |
|
height='0.5', |
|
width='1.8' |
|
) |
|
else: |
|
dot.node( |
|
node_id, |
|
node_label, |
|
shape=shape, |
|
style='filled,rounded', |
|
fillcolor=fill_color_for_nodes, |
|
fontcolor=font_color_for_nodes, |
|
fontsize='12', |
|
height=height, |
|
width=width |
|
) |
|
|
|
|
|
for connection in data['connections']: |
|
dot.edge( |
|
connection['from'], |
|
connection['to'], |
|
label=connection.get('label', ''), |
|
color='#4a4a4a', |
|
fontcolor='#4a4a4a', |
|
fontsize='9' |
|
) |
|
|
|
|
|
with NamedTemporaryFile(delete=False, suffix=f'.{output_format}') as tmp: |
|
dot.render(tmp.name, format=output_format, cleanup=True) |
|
png_path = f"{tmp.name}.{output_format}" |
|
|
|
|
|
from PIL import Image |
|
with Image.open(png_path) as img: |
|
width, height = img.size |
|
print(f"[ํ๋ก์ธ์ค ํ๋ก์ฐ] ์์ฑ๋ ์ด๋ฏธ์ง ํฌ๊ธฐ: {width}x{height}") |
|
|
|
|
|
max_width = 1200 |
|
max_height = 900 |
|
|
|
if width > max_width or height > max_height: |
|
|
|
ratio = min(max_width/width, max_height/height) |
|
new_width = int(width * ratio) |
|
new_height = int(height * ratio) |
|
|
|
img_resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS) |
|
|
|
|
|
final_img = Image.new('RGB', (max_width, max_height), 'white') |
|
x = (max_width - new_width) // 2 |
|
y = (max_height - new_height) // 2 |
|
final_img.paste(img_resized, (x, y)) |
|
|
|
|
|
new_path = png_path.replace('.png', '_resized.png') |
|
final_img.save(new_path) |
|
os.unlink(png_path) |
|
return new_path |
|
|
|
return png_path |
|
|
|
except json.JSONDecodeError: |
|
return "Error: Invalid JSON format" |
|
except Exception as e: |
|
return f"Error: {str(e)}" |
|
|
|
def generate_process_flow_for_ppt(topic: str, context: str, style: str = "Business Workflow") -> Image.Image: |
|
""" |
|
PPT ์์ฑ๊ธฐ์์ ์ฌ์ฉํ ํ๋ก์ธ์ค ํ๋ก์ฐ ๋ค์ด์ด๊ทธ๋จ ์์ฑ |
|
""" |
|
print(f"[ํ๋ก์ธ์ค ํ๋ก์ฐ] ์์ฑ ์์ - ์ฃผ์ : {topic}, ์ปจํ
์คํธ: {context}") |
|
|
|
|
|
setup_korean_font_env() |
|
|
|
|
|
context_lower = context.lower() |
|
|
|
|
|
if "ํ๋ก์ ํธ" in context or "project" in context_lower: |
|
flow_json = { |
|
"start_node": "์์", |
|
"nodes": [ |
|
{"id": "plan", "label": "๊ธฐํ", "type": "process"}, |
|
{"id": "design", "label": "์ค๊ณ", "type": "process"}, |
|
{"id": "develop", "label": "๊ฐ๋ฐ", "type": "process"}, |
|
{"id": "test", "label": "ํ
์คํธ", "type": "decision"}, |
|
{"id": "deploy", "label": "๋ฐฐํฌ", "type": "process"}, |
|
{"id": "end", "label": "์๋ฃ", "type": "end"} |
|
], |
|
"connections": [ |
|
{"from": "์์", "to": "plan", "label": ""}, |
|
{"from": "plan", "to": "design", "label": ""}, |
|
{"from": "design", "to": "develop", "label": ""}, |
|
{"from": "develop", "to": "test", "label": ""}, |
|
{"from": "test", "to": "deploy", "label": "ํต๊ณผ"}, |
|
{"from": "test", "to": "develop", "label": "์์ "}, |
|
{"from": "deploy", "to": "end", "label": ""} |
|
] |
|
} |
|
elif "์๋" in context or "๊ธฐ๋ฅ" in context: |
|
flow_json = { |
|
"start_node": "์์", |
|
"nodes": [ |
|
{"id": "input", "label": "์
๋ ฅ", "type": "io"}, |
|
{"id": "validate", "label": "๊ฒ์ฆ", "type": "decision"}, |
|
{"id": "process", "label": "์ฒ๋ฆฌ", "type": "process"}, |
|
{"id": "output", "label": "์ถ๋ ฅ", "type": "io"}, |
|
{"id": "end", "label": "์ข
๋ฃ", "type": "end"} |
|
], |
|
"connections": [ |
|
{"from": "์์", "to": "input", "label": ""}, |
|
{"from": "input", "to": "validate", "label": ""}, |
|
{"from": "validate", "to": "process", "label": "์ ํจ"}, |
|
{"from": "validate", "to": "input", "label": "์ฌ์
๋ ฅ"}, |
|
{"from": "process", "to": "output", "label": ""}, |
|
{"from": "output", "to": "end", "label": ""} |
|
] |
|
} |
|
else: |
|
|
|
flow_json = { |
|
"start_node": "์์", |
|
"nodes": [ |
|
{"id": "analyze", "label": "๋ถ์", "type": "process"}, |
|
{"id": "plan", "label": "๊ณํ", "type": "process"}, |
|
{"id": "execute", "label": "์คํ", "type": "process"}, |
|
{"id": "check", "label": "๊ฒํ ", "type": "decision"}, |
|
{"id": "complete", "label": "์๋ฃ", "type": "end"} |
|
], |
|
"connections": [ |
|
{"from": "์์", "to": "analyze", "label": ""}, |
|
{"from": "analyze", "to": "plan", "label": ""}, |
|
{"from": "plan", "to": "execute", "label": ""}, |
|
{"from": "execute", "to": "check", "label": ""}, |
|
{"from": "check", "to": "complete", "label": "์น์ธ"}, |
|
{"from": "check", "to": "plan", "label": "์์ "} |
|
] |
|
} |
|
|
|
|
|
json_str = json.dumps(flow_json, ensure_ascii=False) |
|
|
|
print(f"[ํ๋ก์ธ์ค ํ๋ก์ฐ] JSON ์์ฑ ์๋ฃ") |
|
|
|
|
|
png_path = generate_process_flow_diagram(json_str, 'png') |
|
|
|
if png_path.startswith("Error:"): |
|
print(f"[ํ๋ก์ธ์ค ํ๋ก์ฐ] ์์ฑ ์คํจ: {png_path}") |
|
|
|
from PIL import ImageDraw, ImageFont |
|
img = Image.new('RGB', (1200, 900), 'white') |
|
draw = ImageDraw.Draw(img) |
|
|
|
try: |
|
if FONT_PATH and os.path.exists(FONT_PATH): |
|
font = ImageFont.truetype(FONT_PATH, 20) |
|
else: |
|
font = ImageFont.load_default() |
|
except: |
|
font = ImageFont.load_default() |
|
|
|
draw.text((600, 450), "ํ๋ก์ธ์ค ํ๋ก์ฐ ์์ฑ ์คํจ", fill='red', anchor='mm', font=font) |
|
return img |
|
|
|
print(f"[ํ๋ก์ธ์ค ํ๋ก์ฐ] PNG ์์ฑ ์ฑ๊ณต: {png_path}") |
|
|
|
|
|
with Image.open(png_path) as img: |
|
img_copy = img.copy() |
|
|
|
|
|
try: |
|
os.unlink(png_path) |
|
except: |
|
pass |
|
|
|
return img_copy |