"""
HWPX 파일 생성기
StyleAnalyzer 결과를 받아 스타일이 적용된 HWPX 파일 생성
"""
import os
import zipfile
import xml.etree.ElementTree as ET
from typing import List, Dict, Optional
from dataclasses import dataclass
from pathlib import Path
from style_analyzer import StyleAnalyzer, StyledElement
from hwp_style_mapping import HwpStyleMapper, HwpStyle, ROLE_TO_STYLE_NAME
@dataclass
class HwpxConfig:
"""HWPX 생성 설정"""
paper_width: int = 59528 # A4 너비 (hwpunit, 1/7200 inch)
paper_height: int = 84188 # A4 높이
margin_left: int = 8504
margin_right: int = 8504
margin_top: int = 5668
margin_bottom: int = 4252
default_font: str = "함초롬바탕"
default_font_size: int = 1000 # 10pt (hwpunit)
class HwpxGenerator:
"""HWPX 파일 생성기"""
def __init__(self, config: Optional[HwpxConfig] = None):
self.config = config or HwpxConfig()
self.mapper = HwpStyleMapper()
self.used_styles: set = set()
def generate(self, elements: List[StyledElement], output_path: str) -> str:
"""
StyledElement 리스트로부터 HWPX 파일 생성
Args:
elements: StyleAnalyzer로 분류된 요소 리스트
output_path: 출력 파일 경로 (.hwpx)
Returns:
생성된 파일 경로
"""
# 사용된 스타일 수집
self.used_styles = {e.role for e in elements}
# 임시 디렉토리 생성
temp_dir = Path(output_path).with_suffix('.temp')
temp_dir.mkdir(parents=True, exist_ok=True)
try:
# HWPX 구조 생성
self._create_mimetype(temp_dir)
self._create_meta_inf(temp_dir)
self._create_version(temp_dir)
self._create_header(temp_dir)
self._create_content(temp_dir, elements)
self._create_settings(temp_dir)
# ZIP으로 압축
self._create_hwpx(temp_dir, output_path)
return output_path
finally:
# 임시 파일 정리
import shutil
if temp_dir.exists():
shutil.rmtree(temp_dir)
def _create_mimetype(self, temp_dir: Path):
"""mimetype 파일 생성"""
mimetype_path = temp_dir / "mimetype"
mimetype_path.write_text("application/hwp+zip")
def _create_meta_inf(self, temp_dir: Path):
"""META-INF/manifest.xml 생성"""
meta_dir = temp_dir / "META-INF"
meta_dir.mkdir(exist_ok=True)
manifest = """
"""
(meta_dir / "manifest.xml").write_text(manifest, encoding='utf-8')
def _create_version(self, temp_dir: Path):
"""version.xml 생성"""
version = """
"""
(temp_dir / "version.xml").write_text(version, encoding='utf-8')
def _create_header(self, temp_dir: Path):
"""Contents/header.xml 생성 (스타일 정의 포함)"""
contents_dir = temp_dir / "Contents"
contents_dir.mkdir(exist_ok=True)
# 스타일별 속성 생성
char_props_xml = self._generate_char_properties()
para_props_xml = self._generate_para_properties()
styles_xml = self._generate_styles_xml()
header = f"""
{char_props_xml}
{para_props_xml}
{styles_xml}
"""
(contents_dir / "header.xml").write_text(header, encoding='utf-8')
def _generate_char_properties(self) -> str:
"""글자 속성 XML 생성"""
lines = [f' ']
# 기본 글자 속성 (id=0)
lines.append('''
''')
# 역할별 글자 속성
for idx, role in enumerate(sorted(self.used_styles), start=1):
style = self.mapper.get_style(role)
height = int(style.font_size * 100) # pt → hwpunit
color = style.font_color.lstrip('#')
font_id = "1" if style.font_bold else "0" # 굵게면 함초롬돋움
lines.append(f'''
''')
lines.append(' ')
return '\n'.join(lines)
def _generate_para_properties(self) -> str:
"""문단 속성 XML 생성"""
lines = [f' ']
# 기본 문단 속성 (id=0)
lines.append('''
''')
# 역할별 문단 속성
align_map = {"left": "LEFT", "center": "CENTER", "right": "RIGHT", "justify": "JUSTIFY"}
for idx, role in enumerate(sorted(self.used_styles), start=1):
style = self.mapper.get_style(role)
align_val = align_map.get(style.align, "JUSTIFY")
line_spacing = int(style.line_spacing)
left_margin = int(style.indent_left * 100)
indent = int(style.indent_first * 100)
space_before = int(style.space_before * 100)
space_after = int(style.space_after * 100)
lines.append(f'''
''')
lines.append(' ')
return '\n'.join(lines)
def _generate_styles_xml(self) -> str:
"""스타일 정의 XML 생성 (charPrIDRef, paraPrIDRef 참조)"""
lines = [f' ']
# 기본 스타일 (id=0, 바탕글)
lines.append(' ')
# 역할별 스타일 (charPrIDRef, paraPrIDRef 참조)
for idx, role in enumerate(sorted(self.used_styles), start=1):
style = self.mapper.get_style(role)
style_name = style.name.replace('<', '<').replace('>', '>')
lines.append(f' ')
lines.append(' ')
return '\n'.join(lines)
def _create_content(self, temp_dir: Path, elements: List[StyledElement]):
"""Contents/section0.xml 생성 (본문 + 스타일 참조)"""
contents_dir = temp_dir / "Contents"
# 문단 XML 생성
paragraphs = []
current_table = None
# 역할 → 스타일 인덱스 매핑 생성
role_to_idx = {role: idx for idx, role in enumerate(sorted(self.used_styles), start=1)}
for elem in elements:
style = self.mapper.get_style(elem.role)
style_idx = role_to_idx.get(elem.role, 0)
# 테이블 요소는 특수 처리
if elem.role in ["TH", "TD", "TABLE_CAPTION", "TABLE", "FIGURE"]:
continue # 테이블/그림은 별도 처리 필요
# 일반 문단
para_xml = self._create_paragraph(elem.text, style, style_idx)
paragraphs.append(para_xml)
section = f"""
{"".join(paragraphs)}
"""
(contents_dir / "section0.xml").write_text(section, encoding='utf-8')
def _create_paragraph(self, text: str, style: HwpStyle, style_idx: int) -> str:
"""단일 문단 XML 생성"""
text = self._escape_xml(text)
return f'''
{text}
'''
def _escape_xml(self, text: str) -> str:
"""XML 특수문자 이스케이프"""
return (text
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace('"', """)
.replace("'", "'"))
def _create_settings(self, temp_dir: Path):
"""settings.xml 생성"""
settings = """
"""
(temp_dir / "settings.xml").write_text(settings, encoding='utf-8')
def _create_hwpx(self, temp_dir: Path, output_path: str):
"""HWPX 파일 생성 (ZIP 압축)"""
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
# mimetype은 압축하지 않고 첫 번째로
mimetype_path = temp_dir / "mimetype"
zf.write(mimetype_path, "mimetype", compress_type=zipfile.ZIP_STORED)
# 나머지 파일들
for root, dirs, files in os.walk(temp_dir):
for file in files:
if file == "mimetype":
continue
file_path = Path(root) / file
arcname = file_path.relative_to(temp_dir)
zf.write(file_path, arcname)
def convert_html_to_hwpx(html: str, output_path: str) -> str:
"""
HTML → HWPX 변환 메인 함수
Args:
html: HTML 문자열
output_path: 출력 파일 경로
Returns:
생성된 파일 경로
"""
# 1. HTML 분석 → 역할 분류
analyzer = StyleAnalyzer()
elements = analyzer.analyze(html)
print(f"📊 분석 완료: {len(elements)}개 요소")
for role, count in analyzer.get_role_summary().items():
print(f" {role}: {count}")
# 2. HWPX 생성
generator = HwpxGenerator()
result_path = generator.generate(elements, output_path)
print(f"✅ 생성 완료: {result_path}")
return result_path
if __name__ == "__main__":
# 테스트
test_html = """
건설·토목 측량 DX 실무지침
드론/UAV·GIS·지형/지반 모델 기반
2024년 1월
1. 개요
본 보고서는 건설 및 토목 분야의 측량 디지털 전환에 대한 실무 지침을 제공합니다.
1.1 배경
최근 드론과 GIS 기술의 발전으로 측량 업무가 크게 변화하고 있습니다.
1.1.1 기술 동향
1) 드론 측량의 발전
드론을 활용한 측량은 기존 방식 대비 효율성이 크게 향상되었습니다.
(1) RTK 드론
실시간 보정 기능을 갖춘 RTK 드론이 보급되고 있습니다.
- 고정밀 GPS 수신기 내장
- 센티미터 단위 정확도
"""
output = "/home/claude/test_output.hwpx"
convert_html_to_hwpx(test_html, output)