Skip to main content

Read, fill, and edit Korean HWP (Hancom Office) documents in Python — extract text for LLM/RAG, fill forms, and rewrite the binary without corrupting it.

Project description

hwpkit — open-source HWP editor in Python

PyPI CI License: MIT Python 3.9+

Read, fill, and edit Korean HWP / HWPX (Hancom Office) documents in Python. Extract text for LLM / RAG pipelines, fill government & university forms (text and seal / signature images), and rewrite the binary without corrupting it — one API across both .hwp and .hwpx.

Korean government, universities, and most Korean enterprises run on .hwp — the binary format Hancom Office uses. If you need to ingest Korean enterprise documents into an LLM, automate form filling at scale, or just edit an HWP file without manually clicking through Hancom, hwpkit is the missing piece.

Under the hood, HWP 5.0 is a Microsoft Compound File Binary (MS-CFB) container holding a DocInfo stream and one or more Section streams (raw deflate). The standard olefile library can only rewrite a stream if it stays the same byte length, which is rarely true when you're inserting Korean text. hwpkit rewrites the whole CFB container while preserving the directory tree topology Hancom validates on open.

Scope: both Hancom formats are supported — the binary .hwp (the format Office has shipped since 2010) and the XML-based .hwpx. Across both you can extract text, edit text (inject / replace / swap), and insert images, through the same API; the binary side additionally rewrites the container without corrupting it. .hwp and .hwpx are two serializations of one document model (docs/OBJECT_MODEL.md).

Install

Python 3.9 or newer.

pip install hwpkit            # binary .hwp read + edit (core)
pip install hwpkit[hwpx]      # + .hwpx text extraction (lxml)
pip install hwpkit[image]     # + image insertion (Pillow)
pip install hwpkit[full]      # everything

Quickstart

from hwpkit import fill_hwp, inject_text, swap_in_para_text, replace_text

def edit(records):
    inject_text(records, 24, "홍길동")                      # fill empty cell
    swap_in_para_text(records, 40, "□ 석사", "☑ 석사")      # tick checkbox
    replace_text(records, 75, "2026. 05. 19.")            # rewrite a cell

fill_hwp("template.hwp", "out.hwp", edit)

Finding paragraph indices

hwpkit-inspect template.hwp

Prints one line per record with a text preview, so you can identify which paragraph index is which form cell.

Extracting plain text

hwpkit-text file.hwp

Walks every section, strips inline controls (tables, images, footnote refs, etc.) and prints just the literal character content. From Python:

from hwpkit import extract_text_from_hwp
print(extract_text_from_hwp("file.hwp"))

Both formats, one call

.hwpx (the XML format) extracts too — and extract_text_from_file auto-detects which container it's looking at (by content, not just the extension), so a mixed corpus of .hwp and .hwpx needs no branching:

from hwpkit import extract_text_from_file   # .hwp or .hwpx
print(extract_text_from_file("doc.hwpx"))

from hwpkit import extract_text_from_hwpx    # .hwpx only (needs lxml)

The CLI dispatches the same way: hwpkit-text file.hwp or hwpkit-text file.hwpx. HWPX text includes table-cell content (one line per paragraph, document order).

For semantic HWP → XML (OWPML) conversion, use pyhwp — that's a much bigger job.

Editing .hwpx

.hwpx files edit through HwpxFile, mirroring the binary edit verbs — so the same calling pattern works on both formats. There's no layout cache to manage; Hancom recomputes from the XML.

from hwpkit import HwpxFile

doc = HwpxFile.open("form.hwpx")
print(doc.describe())                       # find paragraph indices
doc.inject_text(1, "홍길동")                 # fill an empty paragraph
doc.replace_text(12, "2026. 05. 19.")        # overwrite a cell
doc.swap_in_para_text(8, "□ 동의", "☑ 동의")  # tick a checkbox
doc.save("out.hwpx")

Or the one-call form, matching fill_hwp:

from hwpkit import fill_hwpx
fill_hwpx("form.hwpx", "out.hwpx", lambda d: d.replace_text(12, "홍길동"))

Paragraphs are indexed in document order across all sections (table-cell paragraphs included), the same convention as the binary side. Needs lxml (pip install hwpkit[hwpx]).

Images insert into .hwpx too — place_image adds the BinData/ part, registers it in content.hpf (isEmbeded="1"), and anchors an inline <hp:pic> in the target paragraph (extents = pixels × 75):

doc = HwpxFile.open("form.hwpx")
doc.place_image(42, "seal.png", width_mm=25)   # 25 mm wide, aspect preserved
doc.save("out.hwpx")

Needs Pillow (pip install hwpkit[image]).

For LLM / RAG pipelines

Korean enterprises ship contracts, policies, regulations, government notices, internal memos, and academic papers as .hwp. If your retrieval / RAG pipeline can't read HWP, it can't index Korean enterprise data. The standard text-extraction stack (pdfplumber, python-docx, unstructured) doesn't cover HWP — they all need a preprocessing step.

hwpkit is that step. The library has no LLM dependencies; it's just a clean Korean-text source you can plug into anything:

# Index every HWP in a directory tree as documents for a vector DB
import glob
from hwpkit import extract_text_from_hwp

for path in glob.glob("corpus/**/*.hwp", recursive=True):
    text = extract_text_from_hwp(path)
    vector_db.add(doc_id=path, content=text)
# One-shot: pipe an HWP into any LLM CLI
hwpkit-text contract.hwp | llm "Summarize the key obligations in Korean"

# Bulk: convert a folder of HWPs to .txt for downstream tooling
for f in *.hwp; do hwpkit-text "$f" > "${f%.hwp}.txt"; done

The extractor walks every Section* stream, decodes UTF-16LE, and strips inline controls (tables, images, footnote refs, autonumbers, page-number ctrls, bookmarks) so what you get is clean text — usable directly as input to chunkers, embeddings, or any LLM context.

Edit operations

Function When to use What it does
inject_text(records, i, text) The paragraph is empty (cell on a blank template) Adds a PARA_TEXT record, updates the char count, and dummies the cached layout
swap_in_para_text(records, i, old, new) Same-length substring swap (checkboxes □ → ☑, single-char rewrites) Pure byte replace; keeps the cached layout intact
replace_text(records, i, text) Paragraph has existing text you want to overwrite entirely Rewrites PARA_TEXT, updates char count, dummies layout if length changed
charshape.flatten_to_face(rec, face_id) Mixed-script paragraph (Korean + English) won't pick up font changes Sets all 7 per-script CharShape slots to the same face — see GOTCHAS §3
place_image(in, out, img, paragraph_index, width_mm=…) Stamp a seal / signature / stamp image into a form Adds the image stream to the CFB, registers it in DocInfo, and anchors a picture object in the target paragraph — see below

Inserting an image (seal / signature / stamp)

Form filling often needs a 도장/직인/서명 image, not just text. place_image embeds a PNG/JPG and anchors it in a paragraph:

from hwpkit import place_image

place_image("form.hwp", "out.hwp", "seal.png",
            paragraph_index=42, width_mm=30)   # 30 mm wide, aspect preserved

Under the hood this does the three things a picture needs — add a BinData/BIN%04d.<ext> stream to the container (via a red-black-tree node insert), bump the DocInfo BinData count + add a BIN_DATA record, and anchor a gso picture object in the paragraph. Image extents use HWPUNIT (pixels × 75); see GOTCHAS §5 and RECORD_FORMAT.md.

What's tricky about HWP

See docs/GOTCHAS.md. The short version:

  • PARA_LINE_SEG cache — when a paragraph grows, the cached layout record must be replaced with 36 zero bytes. Anything else (keep, delete, fake multi-segment) either trips Hancom's corruption check or makes text render on a single smashed line.
  • CharShape has seven font slots — Hangul / Latin / Hanja / Japanese / Symbol / User / Other. Hancom's font dropdown typically only changes the Hangul slot, so mixed-script paragraphs need explicit per-slot control via hwpkit.charshape.
  • replace_text("") corrupts the file — wiping a paragraph to empty produces a (chars=1, PARA_TEXT="\r") state that opens fine alone but fails Hancom's checks when combined with other edits. Use a space or em-dash placeholder.
  • Naive CFB writers fail RB-tree validation — Hancom validates the red-black-tree directory invariants on open. hwpkit.cfb reads the original tree pointers byte-for-byte and reuses them.

Comparison

pyhwp olefile hwpkit
Extract plain text (.hwp)
Extract plain text (.hwpx)
Convert HWP → XML / OWPML (semantic)
Read raw streams
Rewrite same-size stream
Rewrite stream that grew/shrank
Insert an image (seal / signature)
Hancom accepts the output n/a only if same-size

See also

  • docs/OBJECT_MODEL.md — how the binary .hwp records map onto their HWPX (OWPML) twins. .hwp and .hwpx are two serializations of one document model, so CharShape/BorderFill/ HWPUNIT knowledge transfers between them.
  • pyhwp — comprehensive HWP→XML converter. hwpkit learned the record format and the dummy-LineSeg trick from reading its source.
  • olefile — read-side dependency.
  • The HWP 5.0 spec from Hancom (Korean).

License

MIT — see LICENSE.


Made by Ebenworks

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hwpkit-0.2.0.tar.gz (43.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hwpkit-0.2.0-py3-none-any.whl (32.7 kB view details)

Uploaded Python 3

File details

Details for the file hwpkit-0.2.0.tar.gz.

File metadata

  • Download URL: hwpkit-0.2.0.tar.gz
  • Upload date:
  • Size: 43.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for hwpkit-0.2.0.tar.gz
Algorithm Hash digest
SHA256 eb954d6a5239a8a950afb207bed218ba0fe4678029b3de0a62b772380680a22b
MD5 202bc2e6c9ccefa7c5789036a1044dec
BLAKE2b-256 52dafcdb523cfd11ed39dd16326bd0b5bc6e328836b6d2513215254a6701dd00

See more details on using hashes here.

Provenance

The following attestation bundles were made for hwpkit-0.2.0.tar.gz:

Publisher: publish.yml on psychofict/hwpkit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file hwpkit-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: hwpkit-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 32.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for hwpkit-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7418597083081aa1c716cf47f50d1e336251ee722c548f44e3002cae558113cb
MD5 f4166da3156fdb1ec464610b40e499ff
BLAKE2b-256 fde927066450559536c9f6b99c275268e2457b99626a8ece95669c67a05590a3

See more details on using hashes here.

Provenance

The following attestation bundles were made for hwpkit-0.2.0-py3-none-any.whl:

Publisher: publish.yml on psychofict/hwpkit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page