Created
March 20, 2026 01:08
-
-
Save xexi/23afe5c90a5493420c1ef3bc288efc88 to your computer and use it in GitHub Desktop.
python-hwpx-v2.8.3_styling.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # python-hwpx 스타일링 가이드 | |
| > python-hwpx v2.8.3 기준. HWPX 문서의 글꼴·크기·색상·정렬·간격 등 스타일 적용 방법. | |
| ## 1. 스타일 구조 개요 | |
| HWPX 문서의 스타일은 `Contents/header.xml`에 정의되고, `Contents/section*.xml`의 문단/런에서 ID로 참조한다. | |
| ``` | |
| header.xml | |
| ├── fontfaces → 글꼴 목록 (fontface > font) | |
| ├── charProperties → 글자 속성 (charPr: 크기, 색상, 굵기, 글꼴 참조) | |
| ├── paraProperties → 문단 속성 (paraPr: 정렬, 줄간격, 여백) | |
| └── styles → 스타일 세트 (charPr + paraPr 조합) | |
| section0.xml | |
| └── <hp:p paraPrIDRef="0"> ← 문단 속성 ID 참조 | |
| <hp:run charPrIDRef="5"> ← 글자 속성 ID 참조 | |
| <hp:t>텍스트</hp:t> | |
| </hp:run> | |
| </hp:p> | |
| ``` | |
| ## 2. 기본 문서(`new()`)에 포함된 스타일 | |
| ### charPr (글자 속성) — 7개 | |
| | id | 크기 | 글꼴 | 색상 | 용도 | | |
| |----|------|------|------|------| | |
| | 0 | 10pt | 함초롬바탕 | #000000 (검정) | 기본 본문 | | |
| | 1 | 10pt | 함초롬돋움 | #000000 | 고딕 본문 | | |
| | 2 | 9pt | 함초롬돋움 | #000000 | 작은 글씨 | | |
| | 3 | 9pt | 함초롬바탕 | #000000 | 작은 명조 | | |
| | 4 | 9pt | 함초롬돋움 | #000000 | 작은 고딕 | | |
| | **5** | **16pt** | 함초롬돋움 | **#2E74B5 (파랑)** | **제목용 (바로 사용 가능)** | | |
| | 6 | 11pt | 함초롬돋움 | #000000 | 중간 크기 | | |
| ### 기본 글꼴 — 2종 | |
| | id | 글꼴명 | 종류 | | |
| |----|--------|------| | |
| | 0 | 함초롬돋움 | 고딕 (sans-serif) | | |
| | 1 | 함초롬바탕 | 명조 (serif) | | |
| ### paraPr (문단 속성) — 20개 (id 0~19) | |
| 대부분 양쪽정렬(JUSTIFY), 줄간격 160%. | |
| ## 3. 단위 체계 | |
| | 항목 | 단위 | 환산 | | |
| |------|------|------| | |
| | 글자 크기 (charPr height) | 1/10 pt | 1000=10pt, 1600=16pt, 2400=24pt | | |
| | 여백/간격 (paraPr margin) | HWPUNIT | 283.46 HWPUNIT = 1mm | | |
| | 줄간격 (lineSpacing value) | 퍼센트 | 160 = 160% | | |
| ## 4. 커스텀 스타일 생성 방법 | |
| ### 4-1. header XML 접근 | |
| ```python | |
| from hwpx import HwpxDocument | |
| doc = HwpxDocument.new() | |
| # header XML 접근 | |
| hdr = doc.headers[0] # HwpxOxmlHeader 객체 | |
| ref_list = hdr.element.find('{http://www.hancom.co.kr/hwpml/2011/head}refList') | |
| ``` | |
| 네임스페이스 상수: | |
| ```python | |
| _HH = '{http://www.hancom.co.kr/hwpml/2011/head}' | |
| _HC = '{http://www.hancom.co.kr/hwpml/2011/core}' | |
| ``` | |
| ### 4-2. 글꼴 추가 | |
| ```python | |
| fontfaces = ref_list.find(f'{_HH}fontfaces') | |
| for fontface in fontfaces.findall(f'{_HH}fontface'): | |
| existing = fontface.findall(f'{_HH}font') | |
| max_id = max(int(f.get('id', '0')) for f in existing) | |
| new_id = str(max_id + 1) | |
| new_font = fontface.makeelement(f'{_HH}font', { | |
| 'id': new_id, | |
| 'face': 'NanumGothic', # 원하는 글꼴명 | |
| 'type': 'TTF', | |
| 'isEmbedded': '0' | |
| }) | |
| # typeInfo 자식 추가 (필수) | |
| type_info = new_font.makeelement(f'{_HH}typeInfo', { | |
| 'familyType': 'FCAT_GOTHIC', | |
| 'serif': 'SERIF_SANS_SERIF', | |
| 'weight': '6', | |
| 'proportion': '4', | |
| 'contrast': '3', | |
| 'strokeVariation': '1', | |
| 'armStyle': '1', | |
| 'letterform': '1', | |
| 'midline': '1', | |
| 'xHeight': '1' | |
| }) | |
| new_font.append(type_info) | |
| fontface.append(new_font) | |
| fontface.set('fontCnt', str(len(fontface.findall(f'{_HH}font')))) | |
| hdr.mark_dirty() | |
| ``` | |
| ### 4-3. 글자 속성(charPr) 추가 | |
| ```python | |
| char_props = ref_list.find(f'{_HH}charProperties') | |
| existing_cps = char_props.findall(f'{_HH}charPr') | |
| new_id = str(max(int(cp.get('id', '0')) for cp in existing_cps) + 1) | |
| # 기존 charPr을 복제하여 수정하는 것이 안전 | |
| import copy | |
| base_cp = existing_cps[0] # id=0 기반 | |
| new_cp = copy.deepcopy(base_cp) | |
| new_cp.set('id', new_id) | |
| new_cp.set('height', '1600') # 16pt | |
| new_cp.set('textColor', '#FF0000') # 빨강 | |
| # 굵게 추가 | |
| bold_elem = new_cp.find(f'{_HH}bold') | |
| if bold_elem is None: | |
| bold_elem = new_cp.makeelement(f'{_HH}bold', {}) | |
| new_cp.append(bold_elem) | |
| # 기울임 추가 | |
| # italic_elem = new_cp.makeelement(f'{_HH}italic', {}) | |
| # new_cp.append(italic_elem) | |
| # 글꼴 변경 (fontRef의 hangul, latin 등 속성을 글꼴 id로 설정) | |
| font_ref = new_cp.find(f'{_HH}fontRef') | |
| if font_ref is not None: | |
| font_ref.set('hangul', '2') # fontface에 추가한 글꼴 id | |
| font_ref.set('latin', '2') | |
| char_props.append(new_cp) | |
| char_props.set('itemCnt', str(len(char_props.findall(f'{_HH}charPr')))) | |
| hdr.mark_dirty() | |
| doc.oxml.invalidate_char_property_cache() # 캐시 갱신 | |
| ``` | |
| ### 4-4. 문단 속성(paraPr) 추가 | |
| ```python | |
| para_props = ref_list.find(f'{_HH}paraProperties') | |
| existing_pps = para_props.findall(f'{_HH}paraPr') | |
| new_pp_id = str(max(int(pp.get('id', '0')) for pp in existing_pps) + 1) | |
| base_pp = existing_pps[0] | |
| new_pp = copy.deepcopy(base_pp) | |
| new_pp.set('id', new_pp_id) | |
| # 정렬 변경 | |
| align = new_pp.find(f'{_HH}align') | |
| if align is not None: | |
| align.set('horizontal', 'CENTER') # CENTER, LEFT, RIGHT, JUSTIFY | |
| # 줄간격 변경 | |
| line_spacing = new_pp.find(f'{_HH}lineSpacing') | |
| if line_spacing is not None: | |
| line_spacing.set('type', 'PERCENT') | |
| line_spacing.set('value', '200') # 200% | |
| # 문단 앞 간격 (space before) | |
| margin = new_pp.find(f'{_HH}margin') | |
| if margin is not None: | |
| prev = margin.find(f'{_HC}prev') | |
| if prev is not None: | |
| prev.set('value', '400') # HWPUNIT 단위 | |
| prev.set('unit', 'HWPUNIT') | |
| para_props.append(new_pp) | |
| para_props.set('itemCnt', str(len(para_props.findall(f'{_HH}paraPr')))) | |
| hdr.mark_dirty() | |
| ``` | |
| ### 4-5. 커스텀 스타일을 문단에 적용 | |
| ```python | |
| # charPrIDRef, paraPrIDRef에 새로 만든 ID 지정 | |
| doc.add_paragraph("제목 텍스트", char_pr_id_ref=new_id, para_pr_id_ref=new_pp_id) | |
| doc.add_paragraph("본문 텍스트", char_pr_id_ref='0') # 기본 10pt | |
| ``` | |
| ## 5. 바로 사용 가능한 기존 스타일 활용 | |
| 커스텀 스타일 없이도 기본 id만으로 구분 가능: | |
| ```python | |
| doc = HwpxDocument.new() | |
| # 제목: id=5 (16pt, 파랑, 함초롬돋움) | |
| doc.add_paragraph("Ⅰ. 제목", char_pr_id_ref=5) | |
| # 본문: id=0 (10pt, 검정, 함초롬바탕) | |
| doc.add_paragraph("본문 내용", char_pr_id_ref=0) | |
| # 작은 글씨: id=2 (9pt, 함초롬돋움) | |
| doc.add_paragraph("※ 참고사항", char_pr_id_ref=2) | |
| ``` | |
| ## 6. 알려진 버그 및 우회 | |
| ### ensure_run_style() 버그 (v2.8.3) | |
| **증상**: `doc.ensure_run_style(bold=True)` 호출 시 TypeError 발생 | |
| ``` | |
| TypeError: SubElement() argument 1 must be xml.etree.ElementTree.Element, not lxml.etree._Element | |
| ``` | |
| **원인**: oxml/document.py 4458행에서 stdlib `ET.SubElement()`를 lxml element에 사용 | |
| **우회**: 위 4-3의 header XML 직접 조작 방식으로 bold charPr을 직접 생성 | |
| ### run.bold = True 버그 | |
| 내부적으로 `ensure_run_style()`을 호출하므로 동일하게 실패. 같은 우회 방법 적용. | |
| ## 7. 검증 | |
| ```bash | |
| # 패키지 구조 검증 | |
| hwpx-validate-package output.hwpx | |
| # XML 스키마 검증 (더 엄격) | |
| hwpx-validate output.hwpx | |
| ``` | |
| ## 8. 실전 예시: 보고서용 스타일 세트 생성 | |
| ```python | |
| import copy | |
| from hwpx import HwpxDocument | |
| _HH = '{http://www.hancom.co.kr/hwpml/2011/head}' | |
| _HC = '{http://www.hancom.co.kr/hwpml/2011/core}' | |
| doc = HwpxDocument.new() | |
| hdr = doc.headers[0] | |
| ref_list = hdr.element.find(f'{_HH}refList') | |
| char_props = ref_list.find(f'{_HH}charProperties') | |
| para_props = ref_list.find(f'{_HH}paraProperties') | |
| base_cp = char_props.findall(f'{_HH}charPr')[1] # id=1 (함초롬돋움 기반) | |
| base_pp = para_props.findall(f'{_HH}paraPr')[0] | |
| def make_char_style(cp_id, height, bold=False, color='#000000'): | |
| """커스텀 글자 속성 생성""" | |
| cp = copy.deepcopy(base_cp) | |
| cp.set('id', str(cp_id)) | |
| cp.set('height', str(height)) | |
| cp.set('textColor', color) | |
| # bold | |
| old_bold = cp.find(f'{_HH}bold') | |
| if bold and old_bold is None: | |
| cp.append(cp.makeelement(f'{_HH}bold', {})) | |
| elif not bold and old_bold is not None: | |
| cp.remove(old_bold) | |
| char_props.append(cp) | |
| return str(cp_id) | |
| def make_para_style(pp_id, align='JUSTIFY', space_before=0, line_spacing=160): | |
| """커스텀 문단 속성 생성""" | |
| pp = copy.deepcopy(base_pp) | |
| pp.set('id', str(pp_id)) | |
| al = pp.find(f'{_HH}align') | |
| if al is not None: | |
| al.set('horizontal', align) | |
| ls = pp.find(f'{_HH}lineSpacing') | |
| if ls is not None: | |
| ls.set('value', str(line_spacing)) | |
| if space_before > 0: | |
| margin = pp.find(f'{_HH}margin') | |
| if margin is not None: | |
| prev = margin.find(f'{_HC}prev') | |
| if prev is not None: | |
| prev.set('value', str(space_before)) | |
| para_props.append(pp) | |
| return str(pp_id) | |
| # 스타일 정의 | |
| TITLE = make_char_style(10, 2400, bold=True) # 24pt 굵게 | |
| HEADING = make_char_style(11, 1600, bold=True) # 16pt 굵게 | |
| SUBHEAD = make_char_style(12, 1400, bold=True) # 14pt 굵게 | |
| BODY = '0' # 10pt 기본 | |
| NOTE = make_char_style(13, 900, color='#666666') # 9pt 회색 | |
| CENTER = make_para_style(20, align='CENTER') | |
| SPACED = make_para_style(21, space_before=300) # 문단 앞 여백 | |
| # itemCnt 업데이트 | |
| char_props.set('itemCnt', str(len(char_props.findall(f'{_HH}charPr')))) | |
| para_props.set('itemCnt', str(len(para_props.findall(f'{_HH}paraPr')))) | |
| hdr.mark_dirty() | |
| doc.oxml.invalidate_char_property_cache() | |
| # 문서 작성 | |
| doc.add_paragraph("보고서 제목", char_pr_id_ref=TITLE, para_pr_id_ref=CENTER) | |
| doc.add_paragraph("") | |
| doc.add_paragraph("□ 대분류 제목", char_pr_id_ref=HEADING, para_pr_id_ref=SPACED) | |
| doc.add_paragraph(" ○ 본문 내용입니다.", char_pr_id_ref=BODY) | |
| doc.add_paragraph(" ※ 참고 사항", char_pr_id_ref=NOTE) | |
| doc.save_to_path("styled_report.hwpx") | |
| ``` | |
| ## 9. 요약 | |
| | 작업 | 방법 | 난이도 | | |
| |------|------|--------| | |
| | 기본 스타일 활용 (id 0~6) | `char_pr_id_ref=N` | 쉬움 | | |
| | 커스텀 크기/색상 | header XML에 charPr 추가 | 중간 | | |
| | 굵게/기울임 | charPr에 `<hh:bold/>` 추가 | 중간 | | |
| | 정렬/줄간격 변경 | header XML에 paraPr 추가 | 중간 | | |
| | 글꼴 추가 | fontface에 font 추가 + charPr fontRef 변경 | 어려움 | | |
| | 표 셀 스타일 | `set_cell_text()` 후 셀 내부 run의 charPrIDRef 변경 | 어려움 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment