Hangul (Hancom Office) HWPX files Writing XML directly -centered skill for creating, editing, and reading. HWPX is a ZIP-based XML container (OWPML standard). It completely bypasses the formatting bugs of the python-hwpx API and enables fine-grained formatting control.
Environment
# SKILL_DIR는 이 SKILL.md가 위치한 디렉토리의 절대 경로로 설정
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)" # 스크립트 내에서
# 또는 Claude Code가 자동으로 주입하는 base directory 경로를 사용
# Python 가상환경 (프로젝트에 맞게 설정)
VENV="<프로젝트>/.venv/bin/activate"
For all Python executions:
# 프로젝트의 .venv를 활성화 (pip install lxml 필요)
source "$VENV"
Directory Structure
.claude/skills/hwpx/
├── SKILL.md # 이 파일
├── scripts/
│ ├── office/
│ │ ├── unpack.py # HWPX → 디렉토리 (XML pretty-print)
│ │ └── pack.py # 디렉토리 → HWPX
│ ├── build_hwpx.py # 템플릿 + XML → .hwpx 조립 (핵심)
│ ├── analyze_template.py # HWPX 심층 분석 (레퍼런스 기반 생성용)
│ ├── validate.py # HWPX 구조 검증
│ └── text_extract.py # 텍스트 추출
├── templates/
│ ├── base/ # 베이스 템플릿 (Skeleton 기반)
│ │ ├── mimetype, META-INF/*, version.xml, settings.xml, Preview/*
│ │ └── Contents/ (header.xml, section0.xml, content.hpf)
│ ├── gonmun/ # 공문 오버레이 (header.xml, section0.xml)
│ ├── report/ # 보고서 오버레이
│ ├── minutes/ # 회의록 오버레이
│ └── proposal/ # 제안서/사업개요 오버레이 (색상 헤더바, 번호 배지)
├── examples/
│ ├── 01_basic_document.sh # XML로 기본 문서 빌드
│ ├── 02_gonmun_example.sh # 공문 템플릿 사용
│ ├── 03_report_with_table.sh # 표 포함 보고서
│ ├── 04_read_and_extract.py # 기존 문서 읽기/추출
│ ├── 05_edit_existing.sh # unpack→편집→pack
│ ├── sample_section0.xml # 주석 달린 section0 예제
│ └── sample_header.xml # 주석 달린 header 예제
└── references/
└── hwpx-format.md # OWPML XML 요소 레퍼런스
Workflow 1: XML-first Document Creation (Main Workflow)
Flow
- Select Template (base/gonmun/report/minutes/proposal)
- Write section0.xml (body content)
- (Optional) Modify header.xml (when new styles need to be added)
- Build with build_hwpx.py
- Validate with validate.py
Basic Usage
source "$VENV"
# 빈 문서 (base 템플릿)
python3 "$SKILL_DIR/scripts/build_hwpx.py" --output result.hwpx
# 템플릿 사용
python3 "$SKILL_DIR/scripts/build_hwpx.py" --template gonmun --output result.hwpx
# 커스텀 section0.xml 오버라이드
python3 "$SKILL_DIR/scripts/build_hwpx.py" --template gonmun --section my_section0.xml --output result.hwpx
# header도 오버라이드
python3 "$SKILL_DIR/scripts/build_hwpx.py" --header my_header.xml --section my_section0.xml --output result.hwpx
# 메타데이터 설정
python3 "$SKILL_DIR/scripts/build_hwpx.py" --template report --section my.xml \
--title "제목" --creator "작성자" --output result.hwpx
Practical Pattern: Write section0.xml Inline → Build
# 1. section0.xml을 임시파일로 작성
SECTION=$(mktemp /tmp/section0_XXXX.xml)
cat > "$SECTION" << 'XMLEOF'
<?xml version='1.0' encoding='UTF-8'?>
<hs:sec xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph"
xmlns:hs="http://www.hancom.co.kr/hwpml/2011/section">
<!-- secPr 포함 첫 문단 (base/section0.xml에서 복사) -->
<!-- ... -->
<hp:p id="1000000002" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">
<hp:run charPrIDRef="0">
<hp:t>본문 내용</hp:t>
</hp:run>
</hp:p>
</hs:sec>
XMLEOF
# 2. 빌드
python3 "$SKILL_DIR/scripts/build_hwpx.py" --section "$SECTION" --output result.hwpx
# 3. 정리
rm -f "$SECTION"
section0.xml Writing Guide
Required Structure
The first paragraph of section0.xml (<hp:p>)의 첫 런(<hp:run>) must <hp:secPr>과 <hp:colPr> include:
<hp:p id="1000000001" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">
<hp:run charPrIDRef="0">
<hp:secPr ...>
<!-- 페이지 크기, 여백, 각주/미주 설정 등 -->
</hp:secPr>
<hp:ctrl>
<hp:colPr id="" type="NEWSPAPER" layout="LEFT" colCount="1" sameSz="1" sameGap="0"/>
</hp:ctrl>
</hp:run>
<hp:run charPrIDRef="0"><hp:t/></hp:run>
</hp:p>
Tip: templates/base/Contents/section0.xml You can simply copy the first paragraph of .
Paragraph
<hp:p id="고유ID" paraPrIDRef="문단스타일ID" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">
<hp:run charPrIDRef="글자스타일ID">
<hp:t>텍스트 내용</hp:t>
</hp:run>
</hp:p>
빈 줄
<hp:p id="고유ID" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">
<hp:run charPrIDRef="0"><hp:t/></hp:run>
</hp:p>
Mixed-formatting run (multiple styles in one paragraph)
<hp:p id="고유ID" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">
<hp:run charPrIDRef="0"><hp:t>일반 텍스트 </hp:t></hp:run>
<hp:run charPrIDRef="7"><hp:t>볼드 텍스트</hp:t></hp:run>
<hp:run charPrIDRef="0"><hp:t> 다시 일반</hp:t></hp:run>
</hp:p>
How to Create Tables
<hp:p id="고유ID" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">
<hp:run charPrIDRef="0">
<hp:tbl id="고유ID" zOrder="0" numberingType="TABLE" textWrap="TOP_AND_BOTTOM"
textFlow="BOTH_SIDES" lock="0" dropcapstyle="None" pageBreak="CELL"
repeatHeader="0" rowCnt="행수" colCnt="열수" cellSpacing="0"
borderFillIDRef="3" noAdjust="0">
<hp:sz width="42520" widthRelTo="ABSOLUTE" height="전체높이" heightRelTo="ABSOLUTE" protect="0"/>
<hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="1" allowOverlap="0"
holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="COLUMN" vertAlign="TOP"
horzAlign="LEFT" vertOffset="0" horzOffset="0"/>
<hp:outMargin left="0" right="0" top="0" bottom="0"/>
<hp:inMargin left="0" right="0" top="0" bottom="0"/>
<hp:tr>
<hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="1" borderFillIDRef="4">
<hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER"
linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0"
hasTextRef="0" hasNumRef="0">
<hp:p paraPrIDRef="21" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0" id="고유ID">
<hp:run charPrIDRef="9"><hp:t>헤더 셀</hp:t></hp:run>
</hp:p>
</hp:subList>
<hp:cellAddr colAddr="0" rowAddr="0"/>
<hp:cellSpan colSpan="1" rowSpan="1"/>
<hp:cellSz width="열너비" height="행높이"/>
<hp:cellMargin left="0" right="0" top="0" bottom="0"/>
</hp:tc>
<!-- 나머지 셀... -->
</hp:tr>
</hp:tbl>
</hp:run>
</hp:p>
Table Size Calculation
- A4 body width: 42520 HWPUNIT = 59528 (paper) - 8504×2 (left/right margins)
- Sum of column widths = body width (42520)
- Example: 3 equal columns → 14173 + 14173 + 14174 = 42520
- Example: 2 columns (label:content = 1:4) → 8504 + 34016 = 42520
- Row height: usually 2400~3600 HWPUNIT per cell
ID Rules
- Paragraph id:
1000000001increment sequentially from - 표 id:
1000000099It is recommended to use separate ranges such as - All ids must be unique within the document
header.xml Modification Guide
How to Add Custom Styles
templates/base/Contents/header.xmlCopy- Add the necessary charPr/paraPr/borderFill
- For each group,
itemCntupdate the attribute
Example of adding charPr (bold 14pt)
<hh:charPr id="8" height="1400" textColor="#000000" shadeColor="none"
useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="2">
<hh:fontRef hangul="1" latin="1" hanja="1" japanese="1" other="1" symbol="1" user="1"/>
<hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
<hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
<hh:bold/>
<hh:underline type="NONE" shape="SOLID" color="#000000"/>
<hh:strikeout shape="NONE" color="#000000"/>
<hh:outline type="NONE"/>
<hh:shadow type="NONE" color="#C0C0C0" offsetX="10" offsetY="10"/>
</hh:charPr>
Font Reference System
fontRefThe value isfontfacesthe font id defined inhangul="0"→ Hamchorom Dotum (Gothic)hangul="1"→ Hamchorom Batang (Myeongjo)- Set all 7 languages identically
Cautions When Adding paraPr
- Must
hp:switchinclude the structure (hp:case+hp:default) hp:case와hp:defaultThe values of are usually the same (or default is 2x)borderFillIDRef="2"maintain
Style ID Map per Template
base (default)
| ID | Type | Description |
|---|---|---|
| charPr 0 | Character | 10pt Hamchorom Batang, default |
| charPr 1 | Character | 10pt Hamchorom Dotum |
| charPr 2~6 | Character | Skeleton default style |
| paraPr 0 | Paragraph | JUSTIFY, 160% line spacing |
| paraPr 1~19 | Paragraph | Skeleton default (outline, footnotes, etc.) |
| borderFill 1 | Border | None (page border) |
| borderFill 2 | Border | None + transparent background (for reference) |
gonmun (official document) — base + additions
| ID | Type | Description |
|---|---|---|
| charPr 7 | Text | 22pt bold Hamchoreum Batang (institution name/title) |
| charPr 8 | Text | 16pt bold Hamchoreum Batang (signer) |
| charPr 9 | Text | 8pt Hamchoreum Batang (bottom contact info) |
| charPr 10 | Text | 10pt bold Hamchoreum Batang (table header) |
| paraPr 20 | Paragraph | CENTER, 160% line spacing |
| paraPr 21 | Paragraph | CENTER, 130% (표 셀) |
| paraPr 22 | Paragraph | JUSTIFY, 130% (표 셀) |
| borderFill 3 | Border | SOLID 0.12mm 4면 |
| borderFill 4 | Border | SOLID 0.12mm + #D6DCE4 background |
report — base + additions
| ID | Type | Description |
|---|---|---|
| charPr 7 | Text | 20pt bold (document title) |
| charPr 8 | Text | 14pt bold (subheading) |
| charPr 9 | Text | 10pt bold (table header) |
| charPr 10 | Text | 10pt bold + underline (emphasized text) |
| charPr 11 | Text | 9pt Hamchoreum Batang (small/footnote) |
| charPr 12 | Text | 16pt bold Hamchoreum Batang (single-line title) |
| charPr 13 | Text | 12pt bold Hamchoreum Dotum (section header) |
| paraPr 20~22 | Paragraph | CENTER/JUSTIFY variants |
| paraPr 23 | Paragraph | RIGHT alignment, 160% line spacing |
| paraPr 24 | Paragraph | JUSTIFY, left 600 (□ checklist item indent) |
| paraPr 25 | Paragraph | JUSTIFY, left 1200 (sub-item ①②③ indent) |
| paraPr 26 | Paragraph | JUSTIFY, left 1800 (deep sub-item - indent) |
| paraPr 27 | Paragraph | LEFT, top and bottom border lines (for section header), prev 400 |
| borderFill 3 | Border | SOLID 0.12mm 4면 |
| borderFill 4 | Border | SOLID 0.12mm + #DAEEF3 background |
| borderFill 5 | Border | Top 0.4mm thick line + bottom 0.12mm thin line (section header) |
Indentation rules: Must use paraPr's left margin, not space characters. □ items use paraPr 24, sub-items ①②③ use paraPr 25, deep - items use paraPr 26.
Section header rules: paraPr 27 + charPr 13 combination. Paragraph border (borderFillIDRef="5") automatically displays top thick line + bottom thin line.
minutes — base + additions
| ID | Type | Description |
|---|---|---|
| charPr 7 | Text | 18pt bold (title) |
| charPr 8 | Text | 12pt bold (section label) |
| charPr 9 | Text | 10pt bold (table header) |
| paraPr 20~22 | Paragraph | CENTER/JUSTIFY variants |
| borderFill 3 | Border | SOLID 0.12mm 4면 |
| borderFill 4 | Border | SOLID 0.12mm + #E2EFDA background |
proposal — base + additions
For official documents requiring visual distinction. Color background header bar and number badge implemented with a table-based layout.
| ID | Type | Description |
|---|---|---|
| charPr 7 | Text | 20pt bold HamchoRomBatang (document title) |
| charPr 8 | Text | 14pt bold HamchoRomBatang (subheading) |
| charPr 9 | Text | 10pt bold HamchoRomBatang (table header) |
| charPr 10 | Text | 14pt bold white HamchoRomDotum (major item number, green background) |
| charPr 11 | Text | 11pt bold white HamchoRomDotum (minor item number, blue background) |
| paraPr 20 | Paragraph | CENTER, 160% line spacing |
| paraPr 21 | Paragraph | CENTER, 130% (표 셀) |
| paraPr 22 | Paragraph | JUSTIFY, 130% (표 셀) |
| borderFill 3 | Border | SOLID 0.12mm 4면 |
| borderFill 4 | Border | SOLID 0.12mm + #DAEEF3 background |
| borderFill 5 | Border | Olive green background #7B8B3D (major item number cell) |
| borderFill 6 | Border | Light gray background #F2F2F2 + gray border (major item title cell) |
| borderFill 7 | Border | Blue background #4472C4 (minor item number badge) |
| borderFill 8 | Border | Bottom border only #D0D0D0 (minor item title area) |
proposal layout pattern
Major item header (2-cell table: number + title):
<!-- borderFillIDRef="5" + charPrIDRef="10" → 녹색배경 흰색 로마숫자 -->
<!-- borderFillIDRef="6" + charPrIDRef="8" → 회색배경 검정 볼드 제목 -->
Minor item header (2-cell table: number badge + title):
<!-- borderFillIDRef="7" + charPrIDRef="11" → 파란배경 흰색 아라비아숫자 -->
<!-- borderFillIDRef="8" + charPrIDRef="8" → 하단선만 검정 볼드 제목 -->
Workflow 2: Editing an existing document (unpack → Edit → pack)
source "$VENV"
# 1. HWPX → 디렉토리 (XML pretty-print)
python3 "$SKILL_DIR/scripts/office/unpack.py" document.hwpx ./unpacked/
# 2. XML 직접 편집 (Claude가 Read/Edit 도구로)
# 본문: ./unpacked/Contents/section0.xml
# 스타일: ./unpacked/Contents/header.xml
# 3. 다시 HWPX로 패키징
python3 "$SKILL_DIR/scripts/office/pack.py" ./unpacked/ edited.hwpx
# 4. 검증
python3 "$SKILL_DIR/scripts/validate.py" edited.hwpx
Workflow 3: Reading/text extraction
source "$VENV"
# 순수 텍스트
python3 "$SKILL_DIR/scripts/text_extract.py" document.hwpx
# 테이블 포함
python3 "$SKILL_DIR/scripts/text_extract.py" document.hwpx --include-tables
# 마크다운 형식
python3 "$SKILL_DIR/scripts/text_extract.py" document.hwpx --format markdown
Python API
from hwpx import TextExtractor
with TextExtractor("document.hwpx") as ext:
text = ext.extract_text(include_nested=True, object_behavior="nested")
print(text)
Workflow 4: Validation
source "$VENV"
python3 "$SKILL_DIR/scripts/validate.py" document.hwpx
Validation items: ZIP validity, required file existence, mimetype content/location/compression method, XML well-formedness
Workflow 5: Reference-based document generation
A workflow that analyzes a user-provided HWPX file and generates a document with the same layout.
Flow
- Analysis —
analyze_template.pyfor in-depth analysis of the reference document - Extract header.xml — use the reference's style definitions as-is
- Write section0.xml — write new content following the structure of the analysis results
- Build — build with the extracted header.xml + new section0.xml
- Validation
Usage
source "$VENV"
# 1. 심층 분석 (구조 청사진 출력)
python3 "$SKILL_DIR/scripts/analyze_template.py" reference.hwpx
# 2. header.xml과 section0.xml을 추출하여 참고용으로 보관
python3 "$SKILL_DIR/scripts/analyze_template.py" reference.hwpx \
--extract-header /tmp/ref_header.xml \
--extract-section /tmp/ref_section.xml
# 3. 분석 결과를 보고 새 section0.xml 작성
# - 동일한 charPrIDRef, paraPrIDRef 사용
# - 동일한 테이블 구조 (열 수, 열 너비, 행 수, rowSpan/colSpan)
# - 동일한 borderFillIDRef, cellMargin
# 4. 추출한 header.xml + 새 section0.xml로 빌드
python3 "$SKILL_DIR/scripts/build_hwpx.py" \
--header /tmp/ref_header.xml \
--section /tmp/new_section0.xml \
--output result.hwpx
# 5. 검증
python3 "$SKILL_DIR/scripts/validate.py" result.hwpx
Analysis output items
| Item | Description |
|---|---|
| Font definitions | hangul/latin font mapping |
| borderFill | Border type/thickness + background color (details per side) |
| charPr | Font size (pt), font name, color, bold/italic/underline/strikethrough, fontRef |
| paraPr | Alignment, line spacing, margins (left/right/prev/next/intent), heading, borderFillIDRef |
| Document structure | Page size, margins, page border, body width |
| Body details | id/paraPr/charPr of all paragraphs + text content |
| Table details | rows×cols, column width array, per-cell span/margin/borderFill/vertAlign + content |
Core Principles
- Use charPrIDRef/paraPrIDRef as-is: Do not change the style IDs in the extracted header.xml
- Sum of column widths = body width: Replicate the column width array from the analysis result exactly
- Preserve rowSpan/colSpan patterns: Precisely reproduce the analyzed cell merge structure
- Preserve cellMargin: Apply the analyzed cell margin values identically
Script Summary
| Script | Purpose |
|---|---|
scripts/build_hwpx.py |
Key — Template + XML → HWPX assembly |
scripts/analyze_template.py |
HWPX in-depth analysis (blueprint for reference-based generation) |
scripts/office/unpack.py |
HWPX → directory (XML pretty-print) |
scripts/office/pack.py |
Directory → HWPX (mimetype first) |
scripts/validate.py |
Validate HWPX file structure |
scripts/text_extract.py |
Extract HWPX text |
Unit conversion
| 값 | HWPUNIT | Meaning |
|---|---|---|
| 1pt | 100 | Base unit |
| 10pt | 1000 | Base font size |
| 1mm | 283.5 | Millimeter |
| 1cm | 2835 | Centimeter |
| A4 폭 | 59528 | 210mm |
| A4 height | 84186 | 297mm |
| Left/right margins | 8504 | 30mm |
| Body width | 42520 | 150mm (A4 - left/right margins) |
Critical Rules
- Only HWPX is supported:
.hwp(binary) files are not supported. If the user provides a.hwpfile, in Hancom Office,.hwpxsave again asinstruct them to do so. (File → Save As → File type: HWPX) - secPr required: The first run of the first paragraph in section0.xml must include secPr + colPr
- mimetype order: When packaging HWPX, mimetype must be the first ZIP entry, ZIP_STORED
- Preserve namespaces: When editing XML,
hp:,hs:,hh:,hc:keep the prefix - itemCnt consistency: The charProperties/paraProperties/borderFills itemCnt in header.xml must match the actual number of children
- ID reference consistency: The charPrIDRef/paraPrIDRef in section0.xml must match the definitions in header.xml
- Use venv: The project's
.venv/bin/python3(lxml package required) - Validation: After generation, always
validate.pyverify integrity with - Reference: For detailed XML structure, see
$SKILL_DIR/references/hwpx-format.mdrefer to - build_hwpx.py first: Use build_hwpx.py for creating new documents (avoid calling the python-hwpx API directly)
- 빈 줄:
<hp:t/>use (self-closing tag)