Local Excalidraw API with Python
- 1. 로컬 Excalidraw 설치 및 실행
- 2. Python으로 로컬 Excalidraw API 사용하기
- 3. 커스텀 로컬 엔드포인트 구성 (Excalidraw API 서버)
- 4. FastAPI를 사용한 Python 기반 Excalidraw API 서버
- 5. Python으로 주어진 JSON으로부터 SVG 생성하기
- 6. Selenium을 사용한 웹 브라우저 자동화 방식
- 추가 팁과 참고사항
- Keywords
로컬에서 실행 중인 Excalidraw API를 Python으로 사용하는 방법을 설명해 드리겠습니다. 로컬 Excalidraw 인스턴스를 사용하면 인터넷 연결이 없거나 개인 정보 보호가 필요한 경우 유용합니다.
1. 로컬 Excalidraw 설치 및 실행
먼저 로컬에 Excalidraw를 설치하고 실행해야 합니다:
# Excalidraw 저장소 클론
git clone https://github.com/excalidraw/excalidraw.git
# 디렉토리 이동
cd excalidraw
# 의존성 설치
npm install
# 로컬 서버 실행
npm start
일반적으로 로컬 Excalidraw는 http://localhost:3000에서 실행됩니다.
2. Python으로 로컬 Excalidraw API 사용하기
import requests
import json
import base64
import os
from io import BytesIO
from PIL import Image
def generate_diagram_from_local_excalidraw(json_file_path, output_file_path, local_url="http://localhost:3000"):
"""
로컬에서 실행 중인 Excalidraw API를 사용하여 다이어그램 생성
:param json_file_path: Excalidraw JSON 파일 경로
:param output_file_path: 생성될 이미지 파일 경로
:param local_url: 로컬 Excalidraw 서버 URL
:return: 성공 여부
"""
try:
# 1. JSON 파일 읽기
with open(json_file_path, 'r', encoding='utf-8') as file:
excalidraw_data = json.load(file)
# 2. API 엔드포인트 구성 (로컬 인스턴스)
# 로컬 Excalidraw 서버의 generate API 엔드포인트
api_endpoint = f"{local_url}/api/v2/scene/export"
# 3. 요청 파라미터 구성
payload = {
"elements": excalidraw_data.get("elements", []),
"appState": excalidraw_data.get("appState", {}),
"files": excalidraw_data.get("files", {}),
"exportAs": "png", # 'png', 'svg', 'json' 등 선택 가능
"quality": 2 # 이미지 품질 (1-4)
}
# 4. API 요청 보내기
response = requests.post(
api_endpoint,
json=payload,
headers={'Content-Type': 'application/json'}
)
# 5. 응답 확인
response.raise_for_status()
# 6. 응답 처리 (응답 형식에 따라 달라질 수 있음)
result = response.json()
# 만약 base64 인코딩된 이미지가 반환된 경우
if "image" in result and result["image"].startswith("data:image/png;base64,"):
# Base64 디코딩
base64_data = result["image"].split(",")[1]
image_data = base64.b64decode(base64_data)
# 이미지 저장
with open(output_file_path, 'wb') as f:
f.write(image_data)
print(f"다이어그램이 성공적으로 생성되었습니다: {output_file_path}")
return True
else:
# 다른 형식의 응답인 경우 직접 저장
with open(output_file_path, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"결과가 저장되었습니다: {output_file_path}")
return True
except FileNotFoundError:
print(f"JSON 파일을 찾을 수 없습니다: {json_file_path}")
return False
except json.JSONDecodeError:
print("유효하지 않은 JSON 파일입니다.")
return False
except requests.exceptions.RequestException as e:
print(f"API 요청 중 오류 발생: {e}")
# 로컬 서버 연결 오류일 경우
if isinstance(e, requests.exceptions.ConnectionError):
print("로컬 Excalidraw 서버가 실행 중인지 확인하세요.")
return False
if __name__ == "__main__":
# 사용 예시
json_file = "your-excalidraw-diagram.json"
output_file = "generated-diagram.png"
# 로컬 Excalidraw 서버 URL (기본값은 http://localhost:3000)
local_excalidraw_url = "http://localhost:3000"
generate_diagram_from_local_excalidraw(json_file, output_file, local_excalidraw_url)
3. 커스텀 로컬 엔드포인트 구성 (Excalidraw API 서버)
만약 Excalidraw API 서버를 따로 구성하고 싶다면, 다음과 같이 Express.js를 사용하여 간단한 API 서버를 만들 수 있습니다:
const express = require('express');
const bodyParser = require('body-parser');
const { exportToSvg, exportToBlob } = require('@excalidraw/excalidraw');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3001;
// JSON 파싱을 위한 미들웨어
app.use(bodyParser.json({ limit: '50mb' }));
// API 라우트 설정
app.post('/generate', async (req, res) => {
try {
const { elements, appState, files } = req.body;
// SVG로 내보내기
const svgOutput = await exportToSvg({
elements,
appState,
files,
exportPadding: 10
});
// SVG 문자열 얻기
const svgString = new XMLSerializer().serializeToString(svgOutput);
// PNG로 내보내기
const blob = await exportToBlob({
elements,
appState,
files,
mimeType: 'image/png',
quality: 2
});
// Blob을 Buffer로 변환
const arrayBuffer = await blob.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
// 응답 설정
res.set('Content-Type', 'image/png');
res.send(buffer);
} catch (error) {
console.error('Error generating diagram:', error);
res.status(500).json({ error: 'Failed to generate diagram' });
}
});
// 서버 시작
app.listen(PORT, () => {
console.log(`Excalidraw API server running at http://localhost:${PORT}`);
});
4. FastAPI를 사용한 Python 기반 Excalidraw API 서버
Python만 사용하려면 FastAPI를 이용해 로컬 API 서버를 구현할 수 있습니다:
from fastapi import FastAPI, HTTPException, Body
from fastapi.responses import FileResponse, JSONResponse
from pydantic import BaseModel
from typing import Dict, List, Any, Optional
import json
import subprocess
import tempfile
import os
import uuid
from pathlib import Path
app = FastAPI(title="로컬 Excalidraw API 서버")
class ExcalidrawData(BaseModel):
elements: List[Dict[str, Any]]
appState: Optional[Dict[str, Any]] = {}
files: Optional[Dict[str, Any]] = {}
@app.post("/generate")
async def generate_diagram(data: ExcalidrawData = Body(...)):
"""
Excalidraw JSON을 받아 이미지로 변환
"""
try:
# 임시 파일 생성
temp_dir = tempfile.gettempdir()
json_file = Path(temp_dir) / f"{uuid.uuid4()}.json"
output_file = Path(temp_dir) / f"{uuid.uuid4()}.png"
# JSON 데이터 저장
with open(json_file, "w") as f:
json.dump({
"type": "excalidraw",
"version": 2,
"source": "http://localhost:8000",
"elements": data.elements,
"appState": data.appState,
"files": data.files
}, f)
# 이미지 변환 (Puppeteer와 같은 도구 필요)
# 여기서는 예시로 가정. 실제로는 외부 도구가 필요할 수 있음
try:
# Puppeteer를 사용한 변환 (Node.js 필요)
script_path = Path(__file__).parent / "convert_excalidraw.js"
# 스크립트가 없는 경우 생성
if not script_path.exists():
with open(script_path, "w") as f:
f.write("""
const puppeteer = require('puppeteer');
const fs = require('fs');
const path = require('path');
(async () => {
const jsonPath = process.argv[2];
const outputPath = process.argv[3];
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Excalidraw 웹 페이지 열기
await page.goto('http://localhost:3000', {waitUntil: 'networkidle0'});
// JSON 로드
const excalidrawData = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
// 페이지에 데이터 주입
await page.evaluate((data) => {
window.EXCALIDRAW_DATA = data;
// Excalidraw API 호출 (페이지에 맞게 조정 필요)
window.app.loadFromJSON(data);
}, excalidrawData);
// 스크린샷 저장
await page.screenshot({ path: outputPath });
await browser.close();
})();
""")
# 스크립트 실행
result = subprocess.run([
"node",
str(script_path),
str(json_file),
str(output_file)
], capture_output=True, text=True)
if result.returncode != 0:
raise Exception(f"변환 실패: {result.stderr}")
# 이미지 반환
return FileResponse(
path=output_file,
media_type="image/png",
filename="excalidraw-diagram.png"
)
except Exception as e:
# Puppeteer 사용 실패 시 대체 방법
# JSON 파일만 반환 (실제 변환 로직은 환경에 따라 다름)
return JSONResponse(
content={"message": "이미지 변환 불가", "data": data.dict()},
status_code=200
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"다이어그램 생성 오류: {str(e)}")
finally:
# 임시 파일 정리
if 'json_file' in locals() and os.path.exists(json_file):
os.remove(json_file)
if 'output_file' in locals() and os.path.exists(output_file):
os.remove(output_file)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
5. Python으로 주어진 JSON으로부터 SVG 생성하기
Excalidraw API 서버 없이 직접 JSON을 SVG로 변환하는 예제:
import json
import svgwrite
import math
from typing import Dict, List, Any
def create_svg_from_excalidraw(json_file_path: str, output_svg_path: str) -> bool:
"""
Excalidraw JSON 파일을 읽어 SVG로 변환
:param json_file_path: Excalidraw JSON 파일 경로
:param output_svg_path: 출력할 SVG 파일 경로
:return: 성공 여부
"""
try:
# JSON 파일 읽기
with open(json_file_path, 'r', encoding='utf-8') as file:
data = json.load(file)
# 요소 추출
elements = data.get('elements', [])
if not elements:
print("변환할 요소가 없습니다.")
return False
# 캔버스 경계 계산
min_x = min_y = float('inf')
max_x = max_y = float('-inf')
for element in elements:
x = element.get('x', 0)
y = element.get('y', 0)
width = element.get('width', 0)
height = element.get('height', 0)
min_x = min(min_x, x)
min_y = min(min_y, y)
max_x = max(max_x, x + width)
max_y = max(max_y, y + height)
# 페딩 추가
padding = 20
min_x -= padding
min_y -= padding
max_x += padding
max_y += padding
width = max_x - min_x
height = max_y - min_y
# SVG 생성
dwg = svgwrite.Drawing(output_svg_path, size=(f"{width}px", f"{height}px"), profile='tiny')
# 배경색 설정
bg_color = data.get('appState', {}).get('viewBackgroundColor', '#ffffff')
dwg.add(dwg.rect(insert=(0, 0), size=('100%', '100%'), fill=bg_color))
# 요소 변환
for element in elements:
element_type = element.get('type', '')
x = element.get('x', 0) - min_x
y = element.get('y', 0) - min_y
width = element.get('width', 0)
height = element.get('height', 0)
angle = element.get('angle', 0)
stroke_color = element.get('strokeColor', '#000000')
bg_color = element.get('backgroundColor', 'transparent')
stroke_width = element.get('strokeWidth', 1)
opacity = element.get('opacity', 100) / 100
# 요소 유형에 따른 처리
if element_type == 'rectangle':
rect = dwg.rect(
insert=(x, y),
size=(width, height),
fill=bg_color,
stroke=stroke_color,
stroke_width=stroke_width,
opacity=opacity
)
if angle != 0:
# 회전 변환
center_x = x + width / 2
center_y = y + height / 2
rect.translate(tx=center_x, ty=center_y)
rect.rotate(angle * 180 / math.pi)
rect.translate(tx=-center_x, ty=-center_y)
dwg.add(rect)
elif element_type == 'ellipse':
rx = width / 2
ry = height / 2
center_x = x + rx
center_y = y + ry
ellipse = dwg.ellipse(
center=(center_x, center_y),
r=(rx, ry),
fill=bg_color,
stroke=stroke_color,
stroke_width=stroke_width,
opacity=opacity
)
if angle != 0:
ellipse.translate(tx=center_x, ty=center_y)
ellipse.rotate(angle * 180 / math.pi)
ellipse.translate(tx=-center_x, ty=-center_y)
dwg.add(ellipse)
elif element_type == 'text':
text_content = element.get('text', '')
font_family = element.get('fontFamily', 1)
font_size = element.get('fontSize', 20)
# 폰트 패밀리 매핑
font_mapping = {
1: 'Arial',
2: 'Virgil',
3: 'Helvetica'
}
font = font_mapping.get(font_family, 'Arial')
text = dwg.text(
text_content,
insert=(x, y + font_size), # 폰트 베이스라인 조정
fill=stroke_color,
font_family=font,
font_size=font_size,
opacity=opacity
)
if angle != 0:
center_x = x + width / 2
center_y = y + height / 2
text.translate(tx=center_x, ty=center_y)
text.rotate(angle * 180 / math.pi)
text.translate(tx=-center_x, ty=-center_y)
dwg.add(text)
elif element_type == 'line':
points = element.get('points', [])
if points:
path_data = 'M'
for i, point in enumerate(points):
px = x + point[0]
py = y + point[1]
if i == 0:
path_data += f" {px},{py}"
else:
path_data += f" L{px},{py}"
path = dwg.path(
d=path_data,
fill='none',
stroke=stroke_color,
stroke_width=stroke_width,
opacity=opacity
)
dwg.add(path)
# 기타 요소 타입들은 필요에 따라 추가 가능
# SVG 저장
dwg.save()
print(f"SVG가 성공적으로 생성되었습니다: {output_svg_path}")
return True
except Exception as e:
print(f"SVG 생성 중 오류 발생: {e}")
return False
if __name__ == "__main__":
# 사용 예시
json_file = "your-excalidraw-diagram.json"
output_svg = "generated-diagram.svg"
create_svg_from_excalidraw(json_file, output_svg)
6. Selenium을 사용한 웹 브라우저 자동화 방식
웹 브라우저를 자동화하여 로컬 Excalidraw에 접근하는 방법:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager
import json
import time
import base64
import os
def generate_diagram_with_selenium(json_file_path, output_file_path, excalidraw_url="http://localhost:3000"):
"""
Selenium을 사용하여 웹 브라우저를 자동화하고 로컬 Excalidraw에 접근
:param json_file_path: Excalidraw JSON 파일 경로
:param output_file_path: 출력 이미지 파일 경로
:param excalidraw_url: 로컬 Excalidraw URL
:return: 성공 여부
"""
try:
# JSON 파일 읽기
with open(json_file_path, 'r', encoding='utf-8') as file:
excalidraw_data = json.load(file)
# 브라우저 옵션 설정
chrome_options = Options()
chrome_options.add_argument("--headless") # 헤드리스 모드
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
# WebDriver 초기화
driver = webdriver.Chrome(
service=Service(ChromeDriverManager().install()),
options=chrome_options
)
try:
# Excalidraw 웹페이지 로드
driver.get(excalidraw_url)
# 페이지 로드 대기
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.TAG_NAME, "canvas"))
)
# JSON 데이터 로드하는 스크립트 실행
load_script = f"""
try {{
const data = {json.dumps(excalidraw_data)};
window.excalidrawAPI.updateScene(data);
return true;
}} catch (error) {{
console.error('데이터 로드 오류:', error);
return false;
}}
"""
result = driver.execute_script(load_script)
if not result:
raise Exception("Excalidraw에 데이터 로드 실패")
# 로드된 다이어그램 렌더링을 위한 대기
time.sleep(2)
# 스크린샷 캡처
screenshot_script = """
try {
return window.excalidrawAPI.exportToBlob({
mimeType: 'image/png',
quality: 2,
exportWithDarkMode: false
});
} catch (error) {
console.error('스크린샷 오류:', error);
return null;
}
"""
# PNG 블롭 가져오기
blob = driver.execute_script(screenshot_script)
if not blob:
# 대체 방법으로 전체 화면 스크린샷 사용
driver.save_screenshot(output_file_path)
print(f"전체 화면 스크린샷이 저장되었습니다: {output_file_path}")
return True
# Blob 데이터 디코드 및 저장
image_data = base64.b64decode(blob)
with open(output_file_path, 'wb') as f:
f.write(image_data)
print(f"다이어그램이 성공적으로 생성되었습니다: {output_file_path}")
return True
finally:
# 브라우저 종료
driver.quit()
except Exception as e:
print(f"다이어그램 생성 중 오류 발생: {e}")
return False
if __name__ == "__main__":
# 사용 예시
json_file = "your-excalidraw-diagram.json"
output_file = "generated-diagram.png"
local_url = "http://localhost:3000"
generate_diagram_with_selenium(json_file, output_file, local_url)
추가 팁과 참고사항
로컬 Excalidraw API 경로: 로컬 Excalidraw의 API 경로는 버전에 따라 다를 수 있습니다. 최신 버전의 문서를 확인하세요.
필요한 패키지 설치:
pip install requests pillow svgwrite selenium webdriver-manager fastapi uvicornAPI 문서화: FastAPI를 사용하는 경우 자동으로 API 문서가 생성됩니다.
http://localhost:8000/docs에서 확인 가능합니다.보안 고려사항: 로컬 API 서버를 외부에 노출할 경우 인증 기능을 추가하는 것이 좋습니다.
Headless 브라우저 대안: Selenium 외에 Puppeteer(Node.js)나 Playwright(Python 지원)도 고려해볼 수 있습니다.
에러 처리: 로컬 Excalidraw API는 공식 API와 다른 응답 형식을 가질 수 있으므로 적절한 에러 처리 코드를 추가하세요.
SVG 최적화: 생성된 SVG 파일이 너무 큰 경우 SVGO와 같은 도구를 사용하여 최적화할 수 있습니다.
Keywords
Excalidraw, API, Python, FastAPI, Selenium, JSON, SVG, Local Server, Automation, Diagram
'IT Best Practise > Linux' 카테고리의 다른 글
| SOPS and GPG 암복호화 (2) | 2025.03.23 |
|---|---|
| Systemd Timer: 주기적 Log 압축 실무 예제 (0) | 2025.03.13 |
| Systemd Timer 모범사례: 디스크 용량 모니터링과 자동화된 실행 체계 (0) | 2025.03.12 |
| Systemd Timer: 리눅스 시스템 작업 스케줄링의 현대적 접근 (0) | 2025.03.12 |
| Systemd Usage: 리눅스 시스템 및 서비스 관리자의 완벽한 이해 (0) | 2025.03.12 |
