434 lines
15 KiB
Python
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}") |