# -*- coding: utf-8 -*- """ HWP 스타일 매핑 모듈 v2.0 HTML 역할(Role) → HWP 스타일 매핑 ✅ v2.0 변경사항: - pyhwpx API에 맞게 apply_to_hwp() 재작성 - CharShape/ParaShape 직접 설정 방식 - 역할 → 개요 스타일 매핑 """ from dataclasses import dataclass from typing import Dict, Optional from enum import Enum class HwpStyleType(Enum): """HWP 스타일 유형""" PARAGRAPH = "paragraph" CHARACTER = "character" @dataclass class HwpStyle: """HWP 스타일 정의""" id: int name: str type: HwpStyleType font_size: float font_bold: bool = False font_color: str = "000000" align: str = "justify" line_spacing: float = 160 space_before: float = 0 space_after: float = 0 indent_left: float = 0 indent_first: float = 0 bg_color: Optional[str] = None # ============================================================================= # 기본 스타일 템플릿 # ============================================================================= DEFAULT_STYLES: Dict[str, HwpStyle] = { # 표지 "COVER_TITLE": HwpStyle( id=100, name="표지제목", type=HwpStyleType.PARAGRAPH, font_size=32, font_bold=True, align="center", space_before=20, space_after=10, font_color="1a365d" ), "COVER_SUBTITLE": HwpStyle( id=101, name="표지부제", type=HwpStyleType.PARAGRAPH, font_size=18, font_bold=False, align="center", font_color="555555" ), "COVER_INFO": HwpStyle( id=102, name="표지정보", type=HwpStyleType.PARAGRAPH, font_size=12, align="center", font_color="666666" ), # 목차 "TOC_H1": HwpStyle( id=110, name="목차1수준", type=HwpStyleType.PARAGRAPH, font_size=12, font_bold=True, indent_left=0 ), "TOC_H2": HwpStyle( id=111, name="목차2수준", type=HwpStyleType.PARAGRAPH, font_size=11, indent_left=20 ), "TOC_H3": HwpStyle( id=112, name="목차3수준", type=HwpStyleType.PARAGRAPH, font_size=10, indent_left=40, font_color="666666" ), # 제목 계층 (개요 1~7 매핑) "H1": HwpStyle( id=1, name="개요 1", type=HwpStyleType.PARAGRAPH, font_size=20, font_bold=True, align="left", space_before=30, space_after=15, font_color="1a365d" ), "H2": HwpStyle( id=2, name="개요 2", type=HwpStyleType.PARAGRAPH, font_size=16, font_bold=True, align="left", space_before=20, space_after=10, font_color="2c5282" ), "H3": HwpStyle( id=3, name="개요 3", type=HwpStyleType.PARAGRAPH, font_size=14, font_bold=True, align="left", space_before=15, space_after=8, font_color="2b6cb0" ), "H4": HwpStyle( id=4, name="개요 4", type=HwpStyleType.PARAGRAPH, font_size=12, font_bold=True, align="left", space_before=10, space_after=5, indent_left=10 ), "H5": HwpStyle( id=5, name="개요 5", type=HwpStyleType.PARAGRAPH, font_size=11, font_bold=True, align="left", space_before=8, space_after=4, indent_left=20 ), "H6": HwpStyle( id=6, name="개요 6", type=HwpStyleType.PARAGRAPH, font_size=11, font_bold=False, align="left", indent_left=30 ), "H7": HwpStyle( id=7, name="개요 7", type=HwpStyleType.PARAGRAPH, font_size=10.5, font_bold=False, align="left", indent_left=40 ), # 본문 "BODY": HwpStyle( id=20, name="바탕글", type=HwpStyleType.PARAGRAPH, font_size=11, align="justify", line_spacing=180, indent_first=10 ), "LIST_ITEM": HwpStyle( id=8, name="개요 8", type=HwpStyleType.PARAGRAPH, font_size=11, align="left", indent_left=15, line_spacing=160 ), "HIGHLIGHT_BOX": HwpStyle( id=21, name="강조박스", type=HwpStyleType.PARAGRAPH, font_size=10.5, align="left", bg_color="f7fafc", indent_left=10, indent_first=0 ), # 표 "TABLE": HwpStyle( id=30, name="표", type=HwpStyleType.PARAGRAPH, font_size=10, align="center" ), "TH": HwpStyle( id=11, name="표제목", type=HwpStyleType.PARAGRAPH, font_size=10, font_bold=True, align="center", bg_color="e2e8f0" ), "TD": HwpStyle( id=31, name="표내용", type=HwpStyleType.PARAGRAPH, font_size=10, align="left" ), "TABLE_CAPTION": HwpStyle( id=19, name="표캡션", type=HwpStyleType.PARAGRAPH, font_size=10, font_bold=True, align="center", space_before=5, space_after=3 ), # 그림 "FIGURE": HwpStyle( id=32, name="그림", type=HwpStyleType.PARAGRAPH, font_size=10, align="center" ), "FIGURE_CAPTION": HwpStyle( id=18, name="그림캡션", type=HwpStyleType.PARAGRAPH, font_size=9.5, align="center", font_color="666666", space_before=5 ), # 기타 "UNKNOWN": HwpStyle( id=0, name="바탕글", type=HwpStyleType.PARAGRAPH, font_size=10, align="left" ), } # 역할 → 개요 번호 매핑 (StyleShortcut 용) ROLE_TO_OUTLINE_NUM = { "H1": 1, "H2": 2, "H3": 3, "H4": 4, "H5": 5, "H6": 6, "H7": 7, "LIST_ITEM": 8, "BODY": 0, # 바탕글 "COVER_TITLE": 0, "COVER_SUBTITLE": 0, "COVER_INFO": 0, } # 역할 → HWP 스타일 이름 매핑 ROLE_TO_STYLE_NAME = { "H1": "개요 1", "H2": "개요 2", "H3": "개요 3", "H4": "개요 4", "H5": "개요 5", "H6": "개요 6", "H7": "개요 7", "LIST_ITEM": "개요 8", "BODY": "바탕글", "COVER_TITLE": "표지제목", "COVER_SUBTITLE": "표지부제", "TH": "표제목", "TD": "표내용", "TABLE_CAPTION": "표캡션", "FIGURE_CAPTION": "그림캡션", "UNKNOWN": "바탕글", } class HwpStyleMapper: """HTML 역할 → HWP 스타일 매퍼""" def __init__(self, custom_styles: Optional[Dict[str, HwpStyle]] = None): self.styles = DEFAULT_STYLES.copy() if custom_styles: self.styles.update(custom_styles) def get_style(self, role: str) -> HwpStyle: return self.styles.get(role, self.styles["UNKNOWN"]) def get_style_id(self, role: str) -> int: return self.get_style(role).id def get_all_styles(self) -> Dict[str, HwpStyle]: return self.styles class HwpStyGenerator: """ HTML 스타일 → HWP 스타일 적용기 pyhwpx API를 사용하여: 1. 역할별 스타일 정보 저장 2. 텍스트 삽입 시 CharShape/ParaShape 직접 적용 3. 개요 스타일 번호 매핑 반환 """ def __init__(self): self.styles: Dict[str, HwpStyle] = {} self.hwp = None def update_from_html(self, html_styles: Dict[str, Dict]): """HTML에서 추출한 스타일로 업데이트""" for role, style_dict in html_styles.items(): if role in DEFAULT_STYLES: base = DEFAULT_STYLES[role] # color 처리 - # 제거 color = style_dict.get('color', base.font_color) if isinstance(color, str): color = color.lstrip('#') self.styles[role] = HwpStyle( id=base.id, name=base.name, type=base.type, font_size=style_dict.get('font_size', base.font_size), font_bold=style_dict.get('bold', base.font_bold), font_color=color, align=style_dict.get('align', base.align), line_spacing=style_dict.get('line_spacing', base.line_spacing), space_before=style_dict.get('space_before', base.space_before), space_after=style_dict.get('space_after', base.space_after), indent_left=style_dict.get('indent_left', base.indent_left), indent_first=style_dict.get('indent_first', base.indent_first), bg_color=style_dict.get('bg_color', base.bg_color), ) else: # 기본 스타일 사용 self.styles[role] = DEFAULT_STYLES.get('UNKNOWN') # 누락된 역할은 기본값으로 채움 for role in DEFAULT_STYLES: if role not in self.styles: self.styles[role] = DEFAULT_STYLES[role] def apply_to_hwp(self, hwp) -> Dict[str, HwpStyle]: """역할 → HwpStyle 매핑 반환""" self.hwp = hwp # 🚫 스타일 생성 비활성화 (API 문제) # for role, style in self.styles.items(): # self._create_or_update_style(hwp, role, style) if not self.styles: self.styles = DEFAULT_STYLES.copy() print(f" ✅ 스타일 매핑 완료: {len(self.styles)}개") return self.styles def _create_or_update_style(self, hwp, role: str, style: HwpStyle): """HWP에 스타일 생성 또는 수정""" try: # 1. 스타일 편집 모드 hwp.HAction.GetDefault("ModifyStyle", hwp.HParameterSet.HStyle.HSet) hwp.HParameterSet.HStyle.StyleName = style.name # 2. 글자 모양 color_hex = style.font_color.lstrip('#') if len(color_hex) == 6: r, g, b = int(color_hex[0:2], 16), int(color_hex[2:4], 16), int(color_hex[4:6], 16) text_color = hwp.RGBColor(r, g, b) else: text_color = hwp.RGBColor(0, 0, 0) hwp.HParameterSet.HStyle.CharShape.Height = hwp.PointToHwpUnit(style.font_size) hwp.HParameterSet.HStyle.CharShape.Bold = style.font_bold hwp.HParameterSet.HStyle.CharShape.TextColor = text_color # 3. 문단 모양 align_map = {'left': 0, 'center': 1, 'right': 2, 'justify': 3} hwp.HParameterSet.HStyle.ParaShape.Align = align_map.get(style.align, 3) hwp.HParameterSet.HStyle.ParaShape.LineSpacing = int(style.line_spacing) hwp.HParameterSet.HStyle.ParaShape.SpaceBeforePara = hwp.PointToHwpUnit(style.space_before) hwp.HParameterSet.HStyle.ParaShape.SpaceAfterPara = hwp.PointToHwpUnit(style.space_after) # 4. 실행 hwp.HAction.Execute("ModifyStyle", hwp.HParameterSet.HStyle.HSet) print(f" ✓ 스타일 '{style.name}' 정의됨") except Exception as e: print(f" [경고] 스타일 '{style.name}' 생성 실패: {e}") def get_style(self, role: str) -> HwpStyle: """역할에 해당하는 스타일 반환""" return self.styles.get(role, DEFAULT_STYLES.get('UNKNOWN')) def apply_char_shape(self, hwp, role: str): """현재 선택 영역에 글자 모양 적용""" style = self.get_style(role) try: # RGB 색상 변환 color_hex = style.font_color.lstrip('#') if style.font_color else '000000' if len(color_hex) == 6: r = int(color_hex[0:2], 16) g = int(color_hex[2:4], 16) b = int(color_hex[4:6], 16) text_color = hwp.RGBColor(r, g, b) else: text_color = hwp.RGBColor(0, 0, 0) # 글자 모양 설정 hwp.HAction.GetDefault("CharShape", hwp.HParameterSet.HCharShape.HSet) hwp.HParameterSet.HCharShape.Height = hwp.PointToHwpUnit(style.font_size) hwp.HParameterSet.HCharShape.Bold = style.font_bold hwp.HParameterSet.HCharShape.TextColor = text_color hwp.HAction.Execute("CharShape", hwp.HParameterSet.HCharShape.HSet) except Exception as e: print(f" [경고] 글자 모양 적용 실패 ({role}): {e}") def apply_para_shape(self, hwp, role: str): """현재 문단에 문단 모양 적용""" style = self.get_style(role) try: # 정렬 align_actions = { 'left': "ParagraphShapeAlignLeft", 'center': "ParagraphShapeAlignCenter", 'right': "ParagraphShapeAlignRight", 'justify': "ParagraphShapeAlignJustify" } if style.align in align_actions: hwp.HAction.Run(align_actions[style.align]) # 문단 모양 상세 설정 hwp.HAction.GetDefault("ParagraphShape", hwp.HParameterSet.HParaShape.HSet) p = hwp.HParameterSet.HParaShape p.LineSpaceType = 0 # 퍼센트 p.LineSpacing = int(style.line_spacing) p.LeftMargin = hwp.MiliToHwpUnit(style.indent_left) p.IndentMargin = hwp.MiliToHwpUnit(style.indent_first) p.SpaceBeforePara = hwp.PointToHwpUnit(style.space_before) p.SpaceAfterPara = hwp.PointToHwpUnit(style.space_after) hwp.HAction.Execute("ParagraphShape", p.HSet) except Exception as e: print(f" [경고] 문단 모양 적용 실패 ({role}): {e}") def apply_style(self, hwp, role: str): """역할에 맞는 전체 스타일 적용 (글자 + 문단)""" self.apply_char_shape(hwp, role) self.apply_para_shape(hwp, role) def export_sty(self, hwp, output_path: str) -> bool: """스타일 파일 내보내기 (현재 미지원)""" print(f" [알림] .sty 내보내기는 현재 미지원") return False # ============================================================================= # 번호 제거 유틸리티 # ============================================================================= import re NUMBERING_PATTERNS = { 'H1': re.compile(r'^(\d+)\.\s*'), # "1. " → "" 'H2': re.compile(r'^(\d+)\.(\d+)\s*'), # "1.1 " → "" 'H3': re.compile(r'^(\d+)\.(\d+)\.(\d+)\s*'), # "1.1.1 " → "" 'H4': re.compile(r'^[가-하]\.\s*'), # "가. " → "" 'H5': re.compile(r'^(\d+)\)\s*'), # "1) " → "" 'H6': re.compile(r'^\((\d+)\)\s*'), # "(1) " → "" 'H7': re.compile(r'^[①②③④⑤⑥⑦⑧⑨⑩]\s*'), # "① " → "" 'LIST_ITEM': re.compile(r'^[•\-○]\s*'), # "• " → "" } def strip_numbering(text: str, role: str) -> str: """ 역할에 따라 텍스트 앞의 번호/기호 제거 HWP 개요 기능이 번호를 자동 생성하므로 중복 방지 """ if not text: return text pattern = NUMBERING_PATTERNS.get(role) if pattern: return pattern.sub('', text).strip() return text.strip() if __name__ == "__main__": # 테스트 print("=== 스타일 매핑 테스트 ===") gen = HwpStyGenerator() # HTML 스타일 시뮬레이션 html_styles = { 'H1': {'font_size': 20, 'color': '#1a365d', 'bold': True}, 'H2': {'font_size': 16, 'color': '#2c5282', 'bold': True}, 'BODY': {'font_size': 11, 'align': 'justify'}, } gen.update_from_html(html_styles) for role, style in gen.styles.items(): print(f"{role:15} → size={style.font_size}pt, bold={style.font_bold}, color=#{style.font_color}")