HWPX Editing
HWPX is a zip of XML (HWPML). The traps that corrupt a file aren't obvious
from the outside, so read the relevant section of references/hwpx-guide.md
before editing, and run the scripts in scripts/ to repack and verify —
don't hand-roll the zip or eyeball correctness.
HWPX only. This handles .hwpx (zip + XML) exclusively. A legacy .hwp
(OLE binary, signature D0CF11E0) must be converted first. The scripts detect
.hwp and say so. For one file, 한글의 다른 이름으로 저장 → HWPX; for a folder,
scripts/hwp_to_hwpx.py FOLDER converts in batch (needs Windows + 한글 +
pywin32), including password-protected files — see "Legacy .hwp" below.
Read-only text/image extraction from .hwp doesn't need conversion at all: use
the hwp5-reading skill.
The one rule that matters most
Never re-zip an HWPX with a normal zip writer. 한글 rejects a file whose
unchanged entries were re-deflated. Use the raw-preserving repacker
(scripts/hwpxlib.py:repack_preserve): it byte-copies every entry you didn't
touch and re-deflates only what you changed, so a no-op repack is byte-identical
to the source — meaning "if the original opens in 한글, your edit opens too."
Workflow
- Inspect first.
python scripts/inspect_hwpx.py FILE.hwpx --breaks — see
per-section counts (paragraphs, tables, pic, equation, fields) and, crucially,
which paragraphs carry a hidden pageBreak/columnBreak. If a heading is split
from its content or a page/column is blank, hunt those breaks first (§6-A) —
don't reach for keepWithNext. Body-paragraph breaks are usually leftover cruft.
- Read the matching guide section in
references/hwpx-guide.md (map below).
The XML ids/refs (charPrIDRef, paraPrIDRef, borderFillIDRef, …) differ per
file — always read them from the actual file, never assume.
- Edit the XML with lxml, following the invariants:
- After editing or creating any paragraph, remove its
<hp:linesegarray>
(cached line layout goes stale → broken spacing). After structural edits,
strip linesegarray from the whole section so 한글 fully re-lays-out.
Use hwpxlib.strip_linesegarray.
- When you clone a node (endnote, table, equation, image), it inherits the
original's
id/instId → duplicates → instability. Reassign fresh ids with
hwpxlib.make_uid, including nested subList>p / tbl/tc/p ids.
- Reuse existing
charPr/paraPr definitions instead of adding new ones;
if you must add, update the itemCnt or 한글 rejects the file.
- Repack with
hwpxlib.repack_preserve(src, changed, out, added):
changed = edited entries (keep the XML declaration on top), added = new
entries like BinData/imageN.png or a new sectionN.xml (also register these in
content.hpf).
- Verify every build:
python scripts/verify.py EDITED.hwpx --orig ORIG.hwpx.
All hard checks must pass (byte-identity self-check, well-formed XML incl.
content.hpf, zero duplicate ids, IDRef/itemCnt integrity, table cell widths,
no 각주/미주 nested inside another 주석, zip integrity + mimetype first/STORED).
It also prints a minimal-change diff so you can confirm only intended changes
are present.
- Render, then look —
python scripts/audit_layout.py FILE.hwpx. Steps 1–5 all
pass on defects that only a render shows: a number broken across two lines
because its column got one character wider, a table footnote stranded alone on
the next page, a table-of-contents number that no longer matches. Any change to
a table's values, widths, or footnote length needs this step — the earlier
checks cannot see layout.
- Round-trip in 한글. LibreOffice can't render HWPX, so render-dependent
judgments (which heading orphans, whether spacing looks right) need the user to
open the file in 한글 and, for equations/TOC, run 도구→차례 새로 고침 or
double-click→close to finalize. Say so.
재분석 후 원고의 표를 갱신할 때는 값을 손으로 옮기지 말고 fill_table()로 소스에서
채운다 — 그래야 재실행 한 번이 전수 갱신이고, 다시 읽어 소스와 비교하는 것이 그대로
검증기가 된다 (§4 표 다시 채우기).
Scripts (scripts/) — run these, don't reinvent
| Script |
Purpose |
inspect_hwpx.py FILE [--text] [--breaks] |
Structure dump; find hidden page/column breaks. |
verify.py EDITED [--orig ORIG] |
The §7 checklist; non-zero exit on failure (CI-gateable). |
audit_layout.py FILE [--pdf X.pdf] |
렌더 기반 감사 — 구조검사가 통과하는 결함만 노린다(열 폭을 넘겨 두 줄로 쪼개진 숫자, 각주만 남은 희박 페이지, 목차 쪽번호 불일치). --pdf 없으면 한글 COM으로 렌더. |
audit_typography.py FILE [--expect-face 이름] [--expect-body-pt N] |
글꼴 혼재와 JUSTIFY+KEEP_WORD 자간 벌어짐을 잡는다. --expect-*가 어긋나면 종료코드 1. |
remerge_check.py MASTER CHAPTER |
떼어낸 장이 다시 합쳐지는지 실증 — 실제로 끼워 넣고 렌더해 번호를 읽는다(§6-E). |
crossref_check.py FILE [--baseline BEFORE] [--fix-cache OUT] |
상호참조(재인용) 무결성 — 필드 페어링·고아 참조·캐시 번호·_PAGE 오설정·리터럴로 남은 인용번호. --baseline으로 편집 전후 «미주↔재인용 대응표» 기계 대조(§4-상호참조). |
hwp_to_hwpx.py FOLDER [--password PW] [--scan] [--to pdf] |
레거시 .hwp → .hwpx 일괄 변환(한글 COM). 암호 걸린 파일도 처리하고, 산출물이 실제로 파싱되는지까지 확인한다. --scan은 한글을 띄우지 않고 «어느 파일이 잠겼는지»만 보고. |
selftest.py |
Prove the repacker is lossless without a real file. |
tables_to_xlsx.py · hwpx_to_markdown.py · hwpx_to_docx.py · data_to_hwpx_table.py |
변환: 표→Excel, 문서→Markdown(LLM이 읽기용), →Word, Excel/CSV→한글 표. 병합셀 보존. 각각 -h. |
hwpxlib.py — import it, don't hand-roll. 손으로 짜면 틀리는 자리마다 헬퍼가 있다:
재압축 repack_preserve(+drop/rename) · 본문 읽기 own · 본문 편집 replace_text/
insert_ctrls_after/find_para(.tail-safe) · 문단 복제 pick_template/clone_para ·
표 table_grid/cell_text/set_cell_text/fill_table/delete_row/delete_column/
set_column_width/table_width_ok · 그림 find_pic/replace_image · 주석
add_endnotes/clone_endnote/nested_notes · 상호참조(재인용) read_crossrefs/
crossref_template/clone_crossref/add_crossrefs/sync_crossref_cache · 장 추출
extract_section · 메모·변경추적 read_memos/delete_memo/read_track_changes · 위생
make_uid/strip_linesegarray/find_duplicate_ids/structural_counts.
각 함수의 함정은 docstring과 가이드에 있다.
Scripts need lxml; table→Excel also needs openpyxl. Python 3.10+.
Where to read in the guide (references/hwpx-guide.md)
Load only the section you need — the guide opens with a "흔한 실패 TOP" (top
failure modes); skim that first, then jump to:
- §1 네임스페이스 ·
own() · 섹션 전수 확인 · 셀 단위 텍스트 추출
- §2 raw-preserving 재압축 (가장 중요)
- §3
linesegarray 제거 · 클론 후 id 중복 제거
- §4 문단 · 표(셀 폭 합 = 표 폭) · 그림(
orgSz·imgDim·재채우기) ·
서식 · 각주/미주 · 상호참조(재인용) · 메모 · 오타감사 스코핑 — 가장 크니 소절만 골라 읽을 것
- §5 다단 · 자동 목차 · 한컴 수식 스크립트(LaTeX 아님)
- §6 숨은 break → 제목 고아 → 빈 페이지 → 넓은 표/구역 이동 → E. 장 추출·재병합
- §7
verify.py가 자동화하는 것과 한글 왕복 주의
Legacy .hwp → .hwpx, and the 한글 COM traps
scripts/hwp_to_hwpx.py exists because driving 한글 through COM has no timeouts:
when it wants a dialog answered, Open() simply never returns — no exception, no
False. Four different causes look identical from the outside, so diagnose by
enabling one condition at a time rather than guessing.
- Security module. Register
FilePathCheckerModule — not
FilePathCheckerModuleExample, the name in 한컴's sample code. Get it wrong and an
invisible "파일 접근 허용" dialog (HNC_DIALOG, IsWindowVisible == 0) blocks
every open forever. The script self-heals the registration.
- Password-protected
.hwp is convertible. 한컴 provides no API to pass a
password to Open() (official forum answer) — but that is not the same as
impossible: the dialog can be driven. Three things must all hold, and missing
any one produces the same "it hangs" symptom: ① the security module is registered
(so the dialog is even visible), ② the password goes into the edit field and is
submitted with {ENTER} — the 확인 button's invoke()/click do nothing — and
the UIA calls run on the main thread (from a worker they find the window but
silently fail), ③ SetMessageBoxMode(0x00011011) before Open(), or every
call after the document opens hangs on another unnamed modal.
- ⚠️ Protection follows the document into
.hwpx. Exporting a locked .hwp
"succeeds", but Contents/section0.xml comes out encrypted, so every tool here
sees binary instead of markup. The script strips it by copying the body into a new
document, then verifies the output parses (hwpx_is_readable) rather than
trusting the exit status. PDF export is unaffected.
- Detecting locks costs nothing: HWP 5.0's
FileHeader stream, DWORD at offset
36, bit 0x02 = password set. No 한글, no opening — thousands of files scan in
minutes (--scan).
- Batch hygiene: one COM object per file (a COM object is bound to the thread
that made it), a per-file timeout so one bad document can't stall the run, and
taskkill on timeout. Run only one instance at a time — the cleanup kills every
Hwp.exe, including one a person is using.
Guardrails
- This edits documents only; it never needs the user's credentials, and it doesn't
fetch or execute remote content. Work on a copy and keep the original.
- Preserve the author's content: only change what the user asked for; the
minimal-change diff in
verify.py is your proof.
1---2name: hwpx-editing3description: Safely read, edit, and convert HWPX (Hangul / 한글 .hwpx) word-processor files with Python + lxml without corrupting them. Use this whenever a task involves a .hwpx file, a 한글 / Hangul / 한컴 (Hancom Office) document, HWPML, or a Korean government / academic / 논문 / 보고서 document — including reading or extracting text and tables (e.g. exporting complex merged tables to Excel / .xlsx), editing paragraphs, tables, images, equations, footnotes/endnotes, or memos, adding or positioning captions, fixing layout (orphaned headings, blank pages, columns), building a table of contents, or repackaging the zip. Also trigger when a 한글 file "won't open" / "is corrupted" (파일이 깨졌다 / 한글에서 안 열린다), or when the user hands you a legacy .hwp (this skill detects it and tells them to convert to .hwpx first). Trigger even if the user only says "edit this 한글 file" or ".hwpx" and doesn't mention the internals — naive edits (re-zipping, stale line caches, cloned ids) make 한글 refuse to open the file.4license: MIT5---67# HWPX Editing89HWPX is a **zip of XML (HWPML)**. The traps that corrupt a file aren't obvious10from the outside, so **read the relevant section of `references/hwpx-guide.md`11before editing**, and **run the scripts in `scripts/` to repack and verify** —12don't hand-roll the zip or eyeball correctness.1314> **HWPX only.** This handles `.hwpx` (zip + XML) exclusively. A legacy `.hwp`15> (OLE binary, signature `D0CF11E0`) must be converted first. The scripts detect16> `.hwp` and say so. For one file, 한글의 **다른 이름으로 저장 → HWPX**; for a folder,17> **`scripts/hwp_to_hwpx.py FOLDER`** converts in batch (needs Windows + 한글 +18> `pywin32`), including **password-protected files** — see "Legacy `.hwp`" below.19> Read-only text/image extraction from `.hwp` doesn't need conversion at all: use20> the `hwp5-reading` skill.2122## The one rule that matters most2324**Never re-zip an HWPX with a normal zip writer.** 한글 rejects a file whose25unchanged entries were re-deflated. Use the raw-preserving repacker26(`scripts/hwpxlib.py:repack_preserve`): it byte-copies every entry you didn't27touch and re-deflates only what you changed, so a no-op repack is **byte-identical28to the source** — meaning "if the original opens in 한글, your edit opens too."2930## Workflow31321. **Inspect first.** `python scripts/inspect_hwpx.py FILE.hwpx --breaks` — see33 per-section counts (paragraphs, tables, `pic`, `equation`, fields) and, crucially,34 which paragraphs carry a hidden `pageBreak`/`columnBreak`. If a heading is split35 from its content or a page/column is blank, **hunt those breaks first** (§6-A) —36 don't reach for `keepWithNext`. Body-paragraph breaks are usually leftover cruft.372. **Read the matching guide section** in `references/hwpx-guide.md` (map below).38 The XML ids/refs (`charPrIDRef`, `paraPrIDRef`, `borderFillIDRef`, …) differ per39 file — always read them from the actual file, never assume.403. **Edit the XML with lxml**, following the invariants:41 - After editing or creating any paragraph, **remove its `<hp:linesegarray>`**42 (cached line layout goes stale → broken spacing). After *structural* edits,43 strip linesegarray from the **whole section** so 한글 fully re-lays-out.44 Use `hwpxlib.strip_linesegarray`.45 - When you **clone** a node (endnote, table, equation, image), it inherits the46 original's `id`/`instId` → duplicates → instability. Reassign fresh ids with47 `hwpxlib.make_uid`, including nested `subList>p` / `tbl`/`tc`/`p` ids.48 - **Reuse existing `charPr`/`paraPr` definitions** instead of adding new ones;49 if you must add, update the `itemCnt` or 한글 rejects the file.504. **Repack** with `hwpxlib.repack_preserve(src, changed, out, added)`:51 `changed` = edited entries (keep the XML declaration on top), `added` = new52 entries like `BinData/imageN.png` or a new `sectionN.xml` (also register these in53 `content.hpf`).545. **Verify every build**: `python scripts/verify.py EDITED.hwpx --orig ORIG.hwpx`.55 All hard checks must pass (byte-identity self-check, well-formed XML incl.56 `content.hpf`, zero duplicate ids, IDRef/itemCnt integrity, table cell widths,57 **no 각주/미주 nested inside another 주석**, zip integrity + mimetype first/STORED).58 It also prints a **minimal-change diff** so you can confirm *only intended changes*59 are present.606. **Render, then look** — `python scripts/audit_layout.py FILE.hwpx`. Steps 1–5 all61 pass on defects that only a render shows: a number broken across two lines62 because its column got one character wider, a table footnote stranded alone on63 the next page, a table-of-contents number that no longer matches. **Any change to64 a table's values, widths, or footnote length needs this step** — the earlier65 checks cannot see layout.667. **Round-trip in 한글.** LibreOffice can't render HWPX, so render-dependent67 judgments (which heading orphans, whether spacing looks right) need the user to68 open the file in 한글 and, for equations/TOC, run 도구→차례 새로 고침 or69 double-click→close to finalize. Say so.7071> 재분석 후 원고의 표를 갱신할 때는 **값을 손으로 옮기지 말고 `fill_table()`로 소스에서72> 채운다** — 그래야 재실행 한 번이 전수 갱신이고, 다시 읽어 소스와 비교하는 것이 그대로73> 검증기가 된다 (§4 표 다시 채우기).7475## Scripts (`scripts/`) — run these, don't reinvent7677| Script | Purpose |78|---|---|79| `inspect_hwpx.py FILE [--text] [--breaks]` | Structure dump; find hidden page/column breaks. |80| `verify.py EDITED [--orig ORIG]` | The §7 checklist; non-zero exit on failure (CI-gateable). |81| `audit_layout.py FILE [--pdf X.pdf]` | **렌더 기반** 감사 — 구조검사가 통과하는 결함만 노린다(열 폭을 넘겨 두 줄로 쪼개진 숫자, 각주만 남은 희박 페이지, 목차 쪽번호 불일치). `--pdf` 없으면 한글 COM으로 렌더. |82| `audit_typography.py FILE [--expect-face 이름] [--expect-body-pt N]` | 글꼴 혼재와 JUSTIFY+KEEP_WORD 자간 벌어짐을 잡는다. `--expect-*`가 어긋나면 종료코드 1. |83| `remerge_check.py MASTER CHAPTER` | 떼어낸 장이 다시 합쳐지는지 실증 — 실제로 끼워 넣고 렌더해 번호를 읽는다(§6-E). |84| `crossref_check.py FILE [--baseline BEFORE] [--fix-cache OUT]` | **상호참조(재인용) 무결성** — 필드 페어링·고아 참조·캐시 번호·`_PAGE` 오설정·리터럴로 남은 인용번호. `--baseline`으로 편집 전후 «미주↔재인용 대응표» 기계 대조(§4-상호참조). |85| `hwp_to_hwpx.py FOLDER [--password PW] [--scan] [--to pdf]` | **레거시 `.hwp` → `.hwpx` 일괄 변환**(한글 COM). 암호 걸린 파일도 처리하고, 산출물이 실제로 파싱되는지까지 확인한다. `--scan`은 한글을 띄우지 않고 «어느 파일이 잠겼는지»만 보고. |86| `selftest.py` | Prove the repacker is lossless without a real file. |87| `tables_to_xlsx.py` · `hwpx_to_markdown.py` · `hwpx_to_docx.py` · `data_to_hwpx_table.py` | 변환: 표→Excel, 문서→Markdown(LLM이 읽기용), →Word, Excel/CSV→한글 표. 병합셀 보존. 각각 `-h`. |8889**`hwpxlib.py` — import it, don't hand-roll.** 손으로 짜면 틀리는 자리마다 헬퍼가 있다:90재압축 `repack_preserve`(+`drop`/`rename`) · 본문 읽기 `own` · 본문 편집 `replace_text`/91`insert_ctrls_after`/`find_para`(`.tail`-safe) · 문단 복제 `pick_template`/`clone_para` ·92표 `table_grid`/`cell_text`/`set_cell_text`/`fill_table`/`delete_row`/`delete_column`/93`set_column_width`/`table_width_ok` · 그림 `find_pic`/`replace_image` · 주석94`add_endnotes`/`clone_endnote`/`nested_notes` · **상호참조(재인용)** `read_crossrefs`/95`crossref_template`/`clone_crossref`/`add_crossrefs`/`sync_crossref_cache` · 장 추출96`extract_section` · 메모·변경추적 `read_memos`/`delete_memo`/`read_track_changes` · 위생97`make_uid`/`strip_linesegarray`/`find_duplicate_ids`/`structural_counts`.98각 함수의 함정은 docstring과 가이드에 있다.99100Scripts need **lxml**; table→Excel also needs **openpyxl**. Python 3.10+.101102## Where to read in the guide (`references/hwpx-guide.md`)103104Load only the section you need — the guide opens with a **"흔한 실패 TOP" (top105failure modes)**; skim that first, then jump to:106107- **§1** 네임스페이스 · `own()` · 섹션 전수 확인 · 셀 단위 텍스트 추출108- **§2** raw-preserving 재압축 (가장 중요)109- **§3** `linesegarray` 제거 · 클론 후 id 중복 제거110- **§4** 문단 · **표**(셀 폭 합 = 표 폭) · **그림**(`orgSz`·`imgDim`·재채우기) ·111 서식 · **각주/미주** · **상호참조(재인용)** · 메모 · 오타감사 스코핑 — 가장 크니 소절만 골라 읽을 것112- **§5** 다단 · 자동 목차 · 한컴 수식 스크립트(LaTeX 아님)113- **§6** 숨은 break → 제목 고아 → 빈 페이지 → 넓은 표/구역 이동 → **E. 장 추출·재병합**114- **§7** `verify.py`가 자동화하는 것과 한글 왕복 주의115116## Legacy `.hwp` → `.hwpx`, and the 한글 COM traps117118`scripts/hwp_to_hwpx.py` exists because driving 한글 through COM has **no timeouts**:119when it wants a dialog answered, `Open()` simply never returns — no exception, no120`False`. Four different causes look identical from the outside, so diagnose by121enabling one condition at a time rather than guessing.122123- **Security module.** Register `FilePathCheckerModule` — **not**124 `FilePathCheckerModuleExample`, the name in 한컴's sample code. Get it wrong and an125 **invisible** "파일 접근 허용" dialog (`HNC_DIALOG`, `IsWindowVisible == 0`) blocks126 every open forever. The script self-heals the registration.127- **Password-protected `.hwp` is convertible.** 한컴 provides no API to pass a128 password to `Open()` (official forum answer) — but that is not the same as129 impossible: the *dialog* can be driven. Three things must all hold, and missing130 any one produces the same "it hangs" symptom: ① the security module is registered131 (so the dialog is even visible), ② the password goes into the edit field and is132 submitted with **`{ENTER}`** — the 확인 button's `invoke()`/`click` do nothing — and133 the UIA calls run on the **main thread** (from a worker they find the window but134 silently fail), ③ **`SetMessageBoxMode(0x00011011)` before `Open()`**, or every135 call *after* the document opens hangs on another unnamed modal.136- ⚠️ **Protection follows the document into `.hwpx`.** Exporting a locked `.hwp`137 "succeeds", but `Contents/section0.xml` comes out **encrypted**, so every tool here138 sees binary instead of markup. The script strips it by copying the body into a new139 document, then **verifies the output parses** (`hwpx_is_readable`) rather than140 trusting the exit status. PDF export is unaffected.141- **Detecting locks costs nothing:** HWP 5.0's `FileHeader` stream, DWORD at offset142 36, **bit `0x02`** = password set. No 한글, no opening — thousands of files scan in143 minutes (`--scan`).144- **Batch hygiene:** one COM object per file (a COM object is bound to the thread145 that made it), a per-file timeout so one bad document can't stall the run, and146 `taskkill` on timeout. Run only one instance at a time — the cleanup kills *every*147 `Hwp.exe`, including one a person is using.148149## Guardrails150151- This edits documents only; it never needs the user's credentials, and it doesn't152 fetch or execute remote content. Work on a **copy** and keep the original.153- Preserve the author's content: only change what the user asked for; the154 minimal-change diff in `verify.py` is your proof.