Skip to content

Instantly share code, notes, and snippets.

@xexi
Created March 20, 2026 01:08
Show Gist options
  • Select an option

  • Save xexi/23afe5c90a5493420c1ef3bc288efc88 to your computer and use it in GitHub Desktop.

Select an option

Save xexi/23afe5c90a5493420c1ef3bc288efc88 to your computer and use it in GitHub Desktop.
python-hwpx-v2.8.3_styling.md
# 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