""" 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 드론이 보급되고 있습니다.

""" output = "/home/claude/test_output.hwpx" convert_html_to_hwpx(test_html, output)