Files
_Geulbeot/03. Code/geulbeot_10th/converters/hwp_style_mapping.py

434 lines
15 KiB
Python

# -*- 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}")