- spreadsheet_print_html: 통합문서 → 인쇄 HTML(DOM 없음 · 화면 엔진 값 · formatValue · 병합 · colgroup 폭 · 쪽 나눔 덩이 · thead 머리행 반복 · 그림 img · 시트마다 @page) - 서식 → CSS 를 클립보드에서 떼어 공용(medium · thick · double 굵기 반영) - 서버 Node 진입 prints · 파이썬 print_htmls · workbook_pdf(공용 PDF 엔진 page_size=None) - common_util_workbook_xlsx: openpyxl 로 값 · 식 · 서식 · 병합 · 폭 · 높이 · 틀 고정 · 인쇄 설정 · 쪽 나눔 · 그림(두 칸 닻) - common_util_svg_png: Playwright 로 SVG → PNG(PyMuPDF 안 씀) - 시험 test_spreadsheet_print(인쇄 HTML · xlsx 되읽기 · PNG 크기 · PDF 쪽 수) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
4.0 KiB
Python
96 lines
4.0 KiB
Python
"""SVG → PNG(요구 347-5 · 요구 350 — PyMuPDF 안 씀) — 헤드리스 Edge(없으면 Chrome) Playwright 찍기.
|
||
|
||
svgs_to_pngs([svg 글], scale=2) → [PNG 바이트] 브라우저 한 번으로 여러 장(차례 그대로)
|
||
|
||
크기 = SVG 의 width · height(없으면 viewBox) × scale. 보고서 그림(단면 · 평면 위험 띠)을
|
||
내장 엑셀 그림(xlsx · 화면)으로 넣을 때 씀 — PDF 는 인쇄 HTML 이 data: 주소로 그대로 씀.
|
||
브라우저 띄우는 규칙 · 잠금 · 까닭 글은 공용 PDF 엔진(`common_util_print_pdf`)과 한 벌 —
|
||
자식 파이썬 프로세스(서버 스레드에선 Playwright 드라이버가 못 뜸) · 스크립트 끔 · 바깥 요청 막음.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import base64
|
||
import json
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
from common_util.common_util_print_pdf import CHANNELS, NO_BROWSER, TIMEOUT_S, PrintError, _lock
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
|
||
|
||
def _shoot(svgs: list[str], scale: float) -> list[bytes]:
|
||
"""자식 프로세스 안 — SVG 마다 <svg> 요소만 찍음. 실패 = PrintError."""
|
||
try:
|
||
from playwright.sync_api import Error as PwError
|
||
from playwright.sync_api import sync_playwright
|
||
except ImportError as error:
|
||
raise PrintError("서버에 Playwright 가 없습니다(pip install playwright)") from error
|
||
try:
|
||
with sync_playwright() as p:
|
||
browser = None
|
||
for channel in CHANNELS:
|
||
try:
|
||
browser = p.chromium.launch(channel=channel, headless=True)
|
||
break
|
||
except PwError:
|
||
continue
|
||
if browser is None:
|
||
raise PrintError(NO_BROWSER)
|
||
try:
|
||
context = browser.new_context(java_script_enabled=False, device_scale_factor=scale)
|
||
context.route("**/*", lambda route: route.abort())
|
||
page = context.new_page()
|
||
out = []
|
||
for svg in svgs:
|
||
page.set_content(
|
||
f'<html><body style="margin:0;background:#fff">{svg}</body></html>'
|
||
)
|
||
out.append(page.locator("svg").first.screenshot())
|
||
return out
|
||
finally:
|
||
browser.close()
|
||
except PrintError:
|
||
raise
|
||
except Exception as error: # 드라이버 못 뜸 등 — 예외 글은 내지 않음
|
||
raise PrintError(NO_BROWSER) from error
|
||
|
||
|
||
def svgs_to_pngs(svgs: list[str], scale: float = 2, timeout_s: float = TIMEOUT_S) -> list[bytes]:
|
||
"""SVG 여러 장 → PNG 여러 장(자식 프로세스 한 번)."""
|
||
if not svgs:
|
||
return []
|
||
with _lock:
|
||
try:
|
||
done = subprocess.run(
|
||
[sys.executable, "-m", "common_util.common_util_svg_png"], # 묶음 import 되게
|
||
input=json.dumps({"svgs": svgs, "scale": scale}).encode(),
|
||
cwd=ROOT,
|
||
capture_output=True,
|
||
timeout=timeout_s,
|
||
)
|
||
except subprocess.TimeoutExpired as error:
|
||
raise PrintError(f"그림 만들기가 {timeout_s:.0f}초 안에 안 끝남") from error
|
||
except OSError as error:
|
||
raise PrintError(NO_BROWSER) from error
|
||
try:
|
||
result: dict[str, Any] = json.loads(done.stdout.decode().strip().splitlines()[-1])
|
||
except (ValueError, IndexError): # 자식이 죽음 — stderr 는 내지 않음
|
||
result = {"reason": NO_BROWSER}
|
||
if result.get("reason"):
|
||
raise PrintError(result["reason"])
|
||
return [base64.b64decode(png) for png in result["pngs"]]
|
||
|
||
|
||
if __name__ == "__main__": # 자식 프로세스 몸통
|
||
job = json.loads(sys.stdin.buffer.read())
|
||
try:
|
||
pngs = _shoot(job["svgs"], job["scale"])
|
||
print(json.dumps({"reason": None, "pngs": [base64.b64encode(p).decode() for p in pngs]}))
|
||
except PrintError as error:
|
||
print(json.dumps({"reason": str(error)}, ensure_ascii=False))
|
||
sys.exit(0)
|