syhwp 0.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- syhwp-0.0.5/.github/workflows/ci.yml +23 -0
- syhwp-0.0.5/.github/workflows/publish.yml +41 -0
- syhwp-0.0.5/.gitignore +26 -0
- syhwp-0.0.5/CLAUDE.md +76 -0
- syhwp-0.0.5/DESIGN.md +139 -0
- syhwp-0.0.5/LICENSE +21 -0
- syhwp-0.0.5/PKG-INFO +103 -0
- syhwp-0.0.5/README.md +75 -0
- syhwp-0.0.5/pyproject.toml +44 -0
- syhwp-0.0.5/src/syhwp/__init__.py +80 -0
- syhwp-0.0.5/src/syhwp/__main__.py +52 -0
- syhwp-0.0.5/src/syhwp/_hwp5.py +319 -0
- syhwp-0.0.5/src/syhwp/_hwpx.py +136 -0
- syhwp-0.0.5/src/syhwp/_markdown.py +36 -0
- syhwp-0.0.5/src/syhwp/_records.py +44 -0
- syhwp-0.0.5/src/syhwp/exceptions.py +21 -0
- syhwp-0.0.5/src/syhwp/models.py +144 -0
- syhwp-0.0.5/src/syhwp/py.typed +0 -0
- syhwp-0.0.5/tests/conftest.py +34 -0
- syhwp-0.0.5/tests/data/.gitkeep +2 -0
- syhwp-0.0.5/tests/test_corpus.py +29 -0
- syhwp-0.0.5/tests/test_document.py +48 -0
- syhwp-0.0.5/tests/test_fuzz.py +63 -0
- syhwp-0.0.5/tests/test_hwp5.py +29 -0
- syhwp-0.0.5/tests/test_hwpx.py +30 -0
- syhwp-0.0.5/tests/test_models.py +37 -0
- syhwp-0.0.5/tests/test_output.py +40 -0
- syhwp-0.0.5/tests/test_records.py +47 -0
- syhwp-0.0.5/uv.lock +214 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- name: Install
|
|
21
|
+
run: pip install -e ".[test]"
|
|
22
|
+
- name: Test
|
|
23
|
+
run: pytest -q
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a GitHub Release is published, using PyPI Trusted
|
|
4
|
+
# Publishing (OIDC) — no API token or secret is stored anywhere.
|
|
5
|
+
#
|
|
6
|
+
# One-time setup on PyPI (https://pypi.org/manage/account/publishing/):
|
|
7
|
+
# Add a pending publisher with
|
|
8
|
+
# PyPI project name: syhwp
|
|
9
|
+
# Owner: sysphere
|
|
10
|
+
# Repository name: syhwp
|
|
11
|
+
# Workflow name: publish.yml
|
|
12
|
+
# Environment name: pypi
|
|
13
|
+
# Then create a GitHub Release (tag e.g. v0.0.5) to trigger this workflow.
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
release:
|
|
17
|
+
types: [published]
|
|
18
|
+
push:
|
|
19
|
+
tags:
|
|
20
|
+
- "v*"
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
publish:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
environment:
|
|
26
|
+
name: pypi
|
|
27
|
+
url: https://pypi.org/p/syhwp
|
|
28
|
+
permissions:
|
|
29
|
+
id-token: write # OIDC token for PyPI trusted publishing
|
|
30
|
+
contents: read # allow actions/checkout to read the (private) repo
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
- uses: actions/setup-python@v5
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.12"
|
|
36
|
+
- name: Build sdist and wheel
|
|
37
|
+
run: |
|
|
38
|
+
python -m pip install --upgrade build
|
|
39
|
+
python -m build
|
|
40
|
+
- name: Publish to PyPI
|
|
41
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
syhwp-0.0.5/.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
env/
|
|
11
|
+
|
|
12
|
+
# Test / tooling
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
htmlcov/
|
|
17
|
+
|
|
18
|
+
# Local sample documents (do not commit copyrighted files)
|
|
19
|
+
tests/data/*.hwp
|
|
20
|
+
tests/data/*.hwpx
|
|
21
|
+
!tests/data/.gitkeep
|
|
22
|
+
|
|
23
|
+
# Editors / OS
|
|
24
|
+
.idea/
|
|
25
|
+
.vscode/
|
|
26
|
+
.DS_Store
|
syhwp-0.0.5/CLAUDE.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# syhwp
|
|
2
|
+
|
|
3
|
+
Korean **HWP 5.x** / **HWPX** 문서를 텍스트·마크다운으로 추출하는 순수 파이썬 라이브러리.
|
|
4
|
+
permissive(MIT) 라이선스로, AGPL(`pyhwp`)·미유지 바인딩(`libhwp`)의 공백을 메운다.
|
|
5
|
+
|
|
6
|
+
## ⚠️ 최우선 규칙: 클린룸 provenance
|
|
7
|
+
|
|
8
|
+
**이 프로젝트의 존재 이유가 permissive 라이선스다. 절대 깨지 말 것:**
|
|
9
|
+
|
|
10
|
+
- 구현은 **오직 Hancom 공개 스펙**(HWP 5.0 바이너리 포맷, OWPML)에서만 도출한다.
|
|
11
|
+
- **AGPL인 `pyhwp` 소스를 참고/복사하지 않는다.** (참고하는 순간 MIT 공개가 위반)
|
|
12
|
+
- Apache인 `hwp.js`/`hwp-rs`는 동작 교차검증용으로만 볼 수 있고, 실질 복제 시 Apache-2.0
|
|
13
|
+
+ NOTICE 승계 의무가 생기므로 코드 이식은 피한다. 스펙에서 직접 구현.
|
|
14
|
+
- 새 레코드/구조를 추가할 때 근거(스펙 절/필드)를 커밋 메시지나 주석에 남긴다.
|
|
15
|
+
|
|
16
|
+
## 개발 명령어
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install -e ".[test]" # 개발 설치 (의존성: olefile 하나)
|
|
20
|
+
pytest -q # 전체 테스트
|
|
21
|
+
pytest tests/test_records.py # 단위 테스트만
|
|
22
|
+
python -c "import syhwp; print(syhwp.extract_markdown('sample.hwpx'))"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
로컬 실측용 실제 문서는 `tests/data/`에 두면 되지만 **커밋 금지**(.gitignore 처리,
|
|
26
|
+
저작권). 합성 샘플 기반 테스트만 리포에 포함한다.
|
|
27
|
+
|
|
28
|
+
## 아키텍처
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
src/syhwp/
|
|
32
|
+
__init__.py 공개 API: detect_format / extract_text / extract_markdown (포맷 자동 감지)
|
|
33
|
+
exceptions.py SyhwpError ← UnsupportedFormatError / InvalidHwpError / EncryptedDocumentError
|
|
34
|
+
_records.py HWP5 레코드 순회 (헤더 uint32: tag10b/level10b/size12b, 0xFFF 확장)
|
|
35
|
+
_hwp5.py HWP5(OLE) 리더 — FileHeader 플래그, zlib(-15) 해제, PARA_TEXT(67) 디코드
|
|
36
|
+
_hwpx.py HWPX(OWPML) 리더 — zipfile+ElementTree, local-name 매칭, 표→GFM
|
|
37
|
+
_markdown.py GFM 표 렌더 공용 (normalize / escape_cell / grid_to_markdown)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 핵심 포맷 사실 (자주 참조)
|
|
41
|
+
|
|
42
|
+
- **HWP5 = OLE 복합파일**(magic `D0CF11E0`). 스트림: `FileHeader`, `BodyText/Section{N}`,
|
|
43
|
+
`DocInfo`(스타일 — **텍스트/표 추출엔 불필요, 파싱 안 함** → libhwp가 죽는 지점을 우회),
|
|
44
|
+
`PrvText`(무압축 UTF-16LE 미리보기 지름길).
|
|
45
|
+
- FileHeader offset 36 properties 플래그: bit0 압축, bit1 암호, bit2 배포용. bit1/2면
|
|
46
|
+
본문 암호화 → `EncryptedDocumentError`.
|
|
47
|
+
- 압축 섹션은 raw DEFLATE: `zlib.decompress(data, -15)`.
|
|
48
|
+
- PARA_TEXT 컨트롤 문자: 8 code-unit `{1-9,11,12,14-23}`, 1 unit `{0,10,13,24-31}`(10/13→개행).
|
|
49
|
+
- **HWPX = ZIP**(magic `PK\x03\x04`, mimetype `application/hwp+zip`). 본문
|
|
50
|
+
`Contents/section{N}.xml`, 요소 로컬네임 `p/t/tbl/tr/tc`.
|
|
51
|
+
|
|
52
|
+
## 코딩 컨벤션
|
|
53
|
+
|
|
54
|
+
- 순수 파이썬, 외부 의존성은 **`olefile`(BSD)** 만. HWPX는 stdlib만.
|
|
55
|
+
- **견고성 우선**: 미지 레코드/요소는 raise 말고 skip. 잘린 스트림도 graceful.
|
|
56
|
+
(패닉/크래시 없는 게 libhwp 대비 핵심 차별점)
|
|
57
|
+
- 예외는 전부 `SyhwpError` 하위. 사용자에게 명확한 사유 메시지.
|
|
58
|
+
- 공개 API는 `__init__.py`의 `__all__`만. 내부 모듈은 `_` 접두.
|
|
59
|
+
- Python 3.9+ 호환 (타입힌트 `List`/`Iterator` from typing, `X | Y` 지양).
|
|
60
|
+
|
|
61
|
+
## 로드맵 (DESIGN.md 상세)
|
|
62
|
+
|
|
63
|
+
- **v0 (완료)**: 포맷감지 + HWP5 텍스트 + HWPX 텍스트/표 + 암호감지.
|
|
64
|
+
- **v0.1 (완료)**: HWP5 **표 그리드 재구성** — 레코드 level 트리 + CTRL_HEADER(`tbl `,
|
|
65
|
+
파일에선 `b" lbt"`) + TABLE(77: nRows@4/nCols@6) + 셀 LIST_HEADER(72: nPara@0/
|
|
66
|
+
col@8/row@10/span@12,14) 그룹핑 → GFM. 중첩표는 셀 텍스트로 linearize.
|
|
67
|
+
- **v0.2 (완료)**: 구조화 API `open() → Document`(.paragraphs/.tables),
|
|
68
|
+
Table(n_rows/n_cols/cells)·Cell(row/col/row_span/col_span/text). 병합셀 span은
|
|
69
|
+
모델에 포착(GFM 출력은 병합 불가라 빈칸 유지).
|
|
70
|
+
- **v0.3 (진행)**: ✅ 버전 캡처(Document.version) ✅ 인라인 개체(수식 스크립트 `[수식:]`,
|
|
71
|
+
그림 `[그림]`, 셀 내부 포함) ✅ 퍼즈/방어 하드닝(OLE/zip 오류·압축폭탄→SyhwpError).
|
|
72
|
+
⏳ 각주/미주(샘플 없어 보류) ⏳ 문자서식→마크다운 강조 ⏳ 실 코퍼스.
|
|
73
|
+
|
|
74
|
+
## 비목표
|
|
75
|
+
|
|
76
|
+
쓰기/편집, 레이아웃 픽셀 충실도, 암호문서 복호화, HWP 3.x(구포맷).
|
syhwp-0.0.5/DESIGN.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# syhwp — Design
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
A pure-Python, permissively licensed reader for Korean HWP 5.x (legacy binary)
|
|
6
|
+
and HWPX (OWPML) documents, producing plain text and GFM markdown (tables as
|
|
7
|
+
pipe tables). Primary use case: feeding Korean office documents into RAG /
|
|
8
|
+
search / LLM pipelines where AGPL (`pyhwp`) and brittle/unmaintained bindings
|
|
9
|
+
(`libhwp`) are not acceptable.
|
|
10
|
+
|
|
11
|
+
Design priorities, in order: **(1) permissive license, (2) robustness on
|
|
12
|
+
real-world files, (3) zero-friction deployment (pure Python, one BSD dep),
|
|
13
|
+
(4) fidelity (tables).**
|
|
14
|
+
|
|
15
|
+
## Clean-room provenance
|
|
16
|
+
|
|
17
|
+
Implemented solely from HANCOM's published specifications:
|
|
18
|
+
- *한글 문서 파일 형식 5.0* (HWP 5.0 binary record format) — OLE/CFBF container,
|
|
19
|
+
record stream layout, control characters.
|
|
20
|
+
- *OWPML* (HWPX) — OPC/ZIP package, section XML.
|
|
21
|
+
|
|
22
|
+
No code or structure is taken from the AGPL `pyhwp`. Apache-licensed references
|
|
23
|
+
(`hwp.js`, `hwp-rs`) may be consulted for cross-checking behaviour only; any
|
|
24
|
+
substantial reuse would require carrying Apache-2.0 + NOTICE, which we avoid by
|
|
25
|
+
working from the spec. This provenance is what permits the MIT license.
|
|
26
|
+
|
|
27
|
+
## Formats
|
|
28
|
+
|
|
29
|
+
### HWP 5.x (`_hwp5.py`)
|
|
30
|
+
OLE compound file (magic `D0CF11E0…`). Relevant streams:
|
|
31
|
+
- `FileHeader` — 32-byte signature `"HWP Document File"`, then version (uint32)
|
|
32
|
+
and properties (uint32). Property bit 0 = compressed, bit 1 = password,
|
|
33
|
+
bit 2 = distribution (copy-protected). Bits 1/2 ⇒ body is encrypted ⇒ we raise
|
|
34
|
+
`EncryptedDocumentError`.
|
|
35
|
+
- `BodyText/Section{N}` — the content. If compressed, raw DEFLATE
|
|
36
|
+
(`zlib.decompress(data, -15)`).
|
|
37
|
+
- `DocInfo` — styles / char shapes / bindata map. **Not required for text or
|
|
38
|
+
table extraction**, so we do not parse it. (This is a robustness win: `libhwp`
|
|
39
|
+
panics inside DocInfo style parsing on files we read fine.)
|
|
40
|
+
|
|
41
|
+
**Records** (`_records.py`): each record is a uint32 header —
|
|
42
|
+
`tag = bits 0–9`, `level = bits 10–19`, `size = bits 20–31`; if `size == 0xFFF`
|
|
43
|
+
the real size is the following uint32 — then `size` bytes of payload. Unknown
|
|
44
|
+
tags are skipped.
|
|
45
|
+
|
|
46
|
+
**Text** (`HWPTAG_PARA_TEXT`, tag 67): UTF-16LE code units with inline control
|
|
47
|
+
characters. Control chars occupying 8 code units (extended/inline objects):
|
|
48
|
+
`{1–9, 11, 12, 14–23}`; occupying 1 unit (char controls): `{0, 10, 13, 24–31}`,
|
|
49
|
+
of which 10/13 map to a newline. Everything else is literal text.
|
|
50
|
+
|
|
51
|
+
**Tables** are reconstructed from the record tree (built from each record's
|
|
52
|
+
`level`). A table is a `HWPTAG_CTRL_HEADER` (tag 71) whose first 4 payload bytes
|
|
53
|
+
are the little-endian control id `"tbl "` (i.e. `b" lbt"`). Its children are:
|
|
54
|
+
- `HWPTAG_TABLE` (tag 77): `n_rows` (uint16 @4), `n_cols` (uint16 @6).
|
|
55
|
+
- repeated `HWPTAG_LIST_HEADER` (tag 72) — one per cell: `n_paragraphs`
|
|
56
|
+
(uint16 @0), then `col`/`row`/`col_span`/`row_span` (uint16 @8/@10/@12/@14) —
|
|
57
|
+
each followed by its `n_paragraphs` `HWPTAG_PARA_HEADER` (tag 66) subtrees,
|
|
58
|
+
which hold the cell's text. Cells are placed into a `n_rows × n_cols` grid and
|
|
59
|
+
rendered as GFM. Nested tables (a table inside a cell) linearize into that
|
|
60
|
+
cell's text. (Offsets were derived empirically from the public format, not
|
|
61
|
+
from `pyhwp` — see the clean-room note.)
|
|
62
|
+
|
|
63
|
+
### HWPX (`_hwpx.py`)
|
|
64
|
+
ZIP package (magic `PK\x03\x04`, mimetype `application/hwp+zip`). Text and tables
|
|
65
|
+
live in `Contents/section{N}.xml` as OWPML. Parsed with the standard library
|
|
66
|
+
(`zipfile` + `xml.etree.ElementTree`), matching elements by local name
|
|
67
|
+
(namespace-agnostic): `p` (paragraph), `t` (text run), `tbl`/`tr`/`tc`
|
|
68
|
+
(table/row/cell), `equation` (with `script`), `pic` (image). Tables render to
|
|
69
|
+
GFM via `_markdown.py`; version comes from `version.xml`
|
|
70
|
+
(major.minor.micro.buildNumber).
|
|
71
|
+
|
|
72
|
+
## Public API (`__init__.py`)
|
|
73
|
+
- `detect_format(path) -> "hwp5" | "hwpx"`
|
|
74
|
+
- `extract_text(path) -> str`
|
|
75
|
+
- `extract_markdown(path) -> str`
|
|
76
|
+
|
|
77
|
+
## Roadmap
|
|
78
|
+
- **v0 (done):** format detection; HWP5 text extraction; HWPX text + tables;
|
|
79
|
+
encryption detection. Verified on real government documents, including files
|
|
80
|
+
that crash `libhwp`.
|
|
81
|
+
- **v0.1 (done):** HWP5 **table grid reconstruction** — walk the record tree by
|
|
82
|
+
`level`, detect `CTRL_HEADER` with ctrl-id `tbl `, read the `TABLE` record
|
|
83
|
+
(rows/cols) and per-cell `LIST_HEADER`, group cell paragraphs into a grid → GFM.
|
|
84
|
+
- **v0.2 (done):** structured API — `open() -> Document` with `.paragraphs` /
|
|
85
|
+
`.tables`; `Table` (`n_rows`, `n_cols`, `cells`), `Cell` (`row`, `col`,
|
|
86
|
+
`row_span`, `col_span`, `text`). `col_span`/`row_span` are captured in the
|
|
87
|
+
model (GFM output still leaves merged slots blank — GFM cannot merge cells).
|
|
88
|
+
### Enhancement roadmap (prioritized for RAG / document ingestion)
|
|
89
|
+
|
|
90
|
+
`syhwp` is an *extraction* library, not a full-fidelity converter like pyhwp's
|
|
91
|
+
ODT path. The gaps that matter for the RAG use case, in priority order:
|
|
92
|
+
|
|
93
|
+
**Tier 1 — coverage & robustness**
|
|
94
|
+
- ✅ **Version-aware parsing** — FileHeader version captured on `Document.version`
|
|
95
|
+
(field-offset branching per sub-version to follow as older files surface).
|
|
96
|
+
- ✅ **Inline objects** — equations → their script (`[수식: …]`), images/drawing
|
|
97
|
+
objects → `[그림]`, at top level and inside table cells.
|
|
98
|
+
- ✅ **Fuzz / defensive hardening** — corrupt OLE/zip, decompression bombs, and
|
|
99
|
+
truncated streams raise `SyhwpError` instead of crashing (fuzz-tested).
|
|
100
|
+
- ⏳ **Footnotes / endnotes & captions** — deferred: no sample document with
|
|
101
|
+
footnotes on hand to verify against; emit `[^n]` markers once one is obtained.
|
|
102
|
+
- ⏳ **Char-shape → markdown emphasis** — parse DocInfo char shapes to emit
|
|
103
|
+
`**bold**` / `*italic*`.
|
|
104
|
+
- ✅ **Corpus harness** — `tests/test_corpus.py` runs over real documents placed
|
|
105
|
+
in `tests/data/` (gitignored); skips in CI. Exercised on 22 varied HWP/HWPX
|
|
106
|
+
files (multiple versions incl. 5.0.2.x, equations, images, tables, and two
|
|
107
|
+
distribution-protected files that correctly raise `EncryptedDocumentError`).
|
|
108
|
+
|
|
109
|
+
**Tier 2 — high value, higher effort**
|
|
110
|
+
- **Distribution (배포용) document decoding** — *deferred, not guessed.* Confirmed
|
|
111
|
+
two protected samples (FileHeader distribution bit set; the section stream is
|
|
112
|
+
obfuscated from byte 0, seed at bytes[0:4]). Correctly rejected today with
|
|
113
|
+
`EncryptedDocumentError` (no crash). Real decoding needs the authoritative
|
|
114
|
+
HANCOM distribution-doc spec (seed → LCG de-obfuscation → AES-128 key) plus an
|
|
115
|
+
AES backend — implemented clean-room from the spec and verified against the
|
|
116
|
+
samples, not reverse-engineered by guesswork. AES would be an optional extra
|
|
117
|
+
(`syhwp[crypto]`) to keep the core dependency-free.
|
|
118
|
+
- Nested-table rendering (HTML / indented), HWPX cell spans & images.
|
|
119
|
+
|
|
120
|
+
**Tier 3 — convenience / fidelity**
|
|
121
|
+
- ✅ `extract_html()` / `Document.html`; ✅ CLI (`syhwp` / `python -m syhwp`,
|
|
122
|
+
`--text/--markdown/--html`); ✅ `py.typed`.
|
|
123
|
+
- ⏳ hyperlinks → markdown links, streaming.
|
|
124
|
+
|
|
125
|
+
**Quality (feature-independent):** decompression-bomb & recursion-depth guards,
|
|
126
|
+
benchmarks vs pyhwp / libhwp (speed + coverage on a corpus).
|
|
127
|
+
|
|
128
|
+
### vs pyhwp — where syhwp already differs
|
|
129
|
+
Ahead: MIT (vs AGPL); HWPX support (pyhwp is HWP5-only); markdown output;
|
|
130
|
+
robustness (skips DocInfo styling, the area that crashes libhwp; unknown records
|
|
131
|
+
are skipped, not fatal); pure Python, one BSD dep, 3.9–3.13.
|
|
132
|
+
Behind: distribution-doc decoding, rich styles/images/equations, footnotes,
|
|
133
|
+
version-specific coverage, and 15 years of real-file maturity.
|
|
134
|
+
|
|
135
|
+
## Non-goals
|
|
136
|
+
- Writing/editing HWP files (read-only).
|
|
137
|
+
- Rendering/layout fidelity (we target content extraction, not pixel fidelity).
|
|
138
|
+
- Decrypting password/distribution-protected documents.
|
|
139
|
+
- HWP 3.x and earlier (different, pre-5.0 format).
|
syhwp-0.0.5/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sysphere
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
syhwp-0.0.5/PKG-INFO
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: syhwp
|
|
3
|
+
Version: 0.0.5
|
|
4
|
+
Summary: Pure-Python reader for Korean HWP 5.x and HWPX documents — text and tables. Clean-room, permissively licensed.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sysphere/syhwp
|
|
6
|
+
Project-URL: Source, https://github.com/sysphere/syhwp
|
|
7
|
+
Project-URL: Issues, https://github.com/sysphere/syhwp/issues
|
|
8
|
+
Author: sysphere
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: hancom,hwp,hwpx,korean,parser,rag,text-extraction
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Natural Language :: Korean
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: olefile>=0.46
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# syhwp
|
|
30
|
+
|
|
31
|
+
Pure-Python reader for Korean **HWP 5.x** and **HWPX** documents — extracts text
|
|
32
|
+
and tables, with no external services and a permissive license.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import syhwp
|
|
36
|
+
|
|
37
|
+
# convenience
|
|
38
|
+
text = syhwp.extract_text("report.hwp") # plain text
|
|
39
|
+
md = syhwp.extract_markdown("report.hwpx") # GFM markdown (tables as pipe tables)
|
|
40
|
+
|
|
41
|
+
# structured access
|
|
42
|
+
doc = syhwp.open("report.hwp") # -> Document
|
|
43
|
+
for table in doc.tables: # Table: n_rows, n_cols, cells[...]
|
|
44
|
+
print(table.to_markdown())
|
|
45
|
+
for para in doc.paragraphs: # Paragraph: text
|
|
46
|
+
print(para.text)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Format (HWP vs HWPX) is auto-detected. Works the same for `.hwp` and `.hwpx`.
|
|
50
|
+
|
|
51
|
+
Also available as a command line tool:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
syhwp report.hwp # markdown (default); also --text / --html
|
|
55
|
+
python -m syhwp report.hwpx
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Why
|
|
59
|
+
|
|
60
|
+
The existing Python options each have a blocking flaw for use in commercial or
|
|
61
|
+
SaaS software:
|
|
62
|
+
|
|
63
|
+
| Library | License | Issue |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `pyhwp` | **AGPL-3.0** | Strong network copyleft — unusable in closed/SaaS products |
|
|
66
|
+
| `libhwp` (hwp-rs) | Apache-2.0 | Unmaintained (0.2.0), panics on real files, no 3.12+ wheels |
|
|
67
|
+
| `pyhwpx` | — | Windows-only (COM automation) |
|
|
68
|
+
|
|
69
|
+
`syhwp` fills the gap: **pure Python, permissive (MIT), maintained, robust by
|
|
70
|
+
design** (unknown/edge records are skipped rather than crashing).
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install syhwp
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Only dependency: [`olefile`](https://pypi.org/project/olefile/) (BSD). HWPX
|
|
79
|
+
parsing uses the standard library only.
|
|
80
|
+
|
|
81
|
+
## Status
|
|
82
|
+
|
|
83
|
+
Alpha. See [DESIGN.md](DESIGN.md) for the architecture and roadmap.
|
|
84
|
+
|
|
85
|
+
- **HWP 5.x** — text and table extraction (tables reconstructed into GFM pipe
|
|
86
|
+
tables); equations surfaced as their script, images/drawings as `[그림]`;
|
|
87
|
+
document version exposed on `Document.version`.
|
|
88
|
+
- **HWPX** — text and table extraction.
|
|
89
|
+
- Robust by design: unknown records are skipped, and malformed / corrupt files
|
|
90
|
+
raise a `SyhwpError` subclass rather than crashing (fuzz-tested).
|
|
91
|
+
- Password-protected / distribution (copy-protected) documents raise
|
|
92
|
+
`EncryptedDocumentError` (their body streams are encrypted and cannot be read).
|
|
93
|
+
|
|
94
|
+
## Provenance / license note
|
|
95
|
+
|
|
96
|
+
`syhwp` is a **clean-room implementation** written from the publicly published
|
|
97
|
+
HANCOM *HWP 5.0 binary format* and *OWPML (HWPX)* specifications. It does **not**
|
|
98
|
+
derive from, or incorporate code from, the AGPL-licensed `pyhwp`. This is what
|
|
99
|
+
allows `syhwp` to be offered under the permissive MIT license.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT © 2026 sysphere
|
syhwp-0.0.5/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# syhwp
|
|
2
|
+
|
|
3
|
+
Pure-Python reader for Korean **HWP 5.x** and **HWPX** documents — extracts text
|
|
4
|
+
and tables, with no external services and a permissive license.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
import syhwp
|
|
8
|
+
|
|
9
|
+
# convenience
|
|
10
|
+
text = syhwp.extract_text("report.hwp") # plain text
|
|
11
|
+
md = syhwp.extract_markdown("report.hwpx") # GFM markdown (tables as pipe tables)
|
|
12
|
+
|
|
13
|
+
# structured access
|
|
14
|
+
doc = syhwp.open("report.hwp") # -> Document
|
|
15
|
+
for table in doc.tables: # Table: n_rows, n_cols, cells[...]
|
|
16
|
+
print(table.to_markdown())
|
|
17
|
+
for para in doc.paragraphs: # Paragraph: text
|
|
18
|
+
print(para.text)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Format (HWP vs HWPX) is auto-detected. Works the same for `.hwp` and `.hwpx`.
|
|
22
|
+
|
|
23
|
+
Also available as a command line tool:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
syhwp report.hwp # markdown (default); also --text / --html
|
|
27
|
+
python -m syhwp report.hwpx
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Why
|
|
31
|
+
|
|
32
|
+
The existing Python options each have a blocking flaw for use in commercial or
|
|
33
|
+
SaaS software:
|
|
34
|
+
|
|
35
|
+
| Library | License | Issue |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `pyhwp` | **AGPL-3.0** | Strong network copyleft — unusable in closed/SaaS products |
|
|
38
|
+
| `libhwp` (hwp-rs) | Apache-2.0 | Unmaintained (0.2.0), panics on real files, no 3.12+ wheels |
|
|
39
|
+
| `pyhwpx` | — | Windows-only (COM automation) |
|
|
40
|
+
|
|
41
|
+
`syhwp` fills the gap: **pure Python, permissive (MIT), maintained, robust by
|
|
42
|
+
design** (unknown/edge records are skipped rather than crashing).
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install syhwp
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Only dependency: [`olefile`](https://pypi.org/project/olefile/) (BSD). HWPX
|
|
51
|
+
parsing uses the standard library only.
|
|
52
|
+
|
|
53
|
+
## Status
|
|
54
|
+
|
|
55
|
+
Alpha. See [DESIGN.md](DESIGN.md) for the architecture and roadmap.
|
|
56
|
+
|
|
57
|
+
- **HWP 5.x** — text and table extraction (tables reconstructed into GFM pipe
|
|
58
|
+
tables); equations surfaced as their script, images/drawings as `[그림]`;
|
|
59
|
+
document version exposed on `Document.version`.
|
|
60
|
+
- **HWPX** — text and table extraction.
|
|
61
|
+
- Robust by design: unknown records are skipped, and malformed / corrupt files
|
|
62
|
+
raise a `SyhwpError` subclass rather than crashing (fuzz-tested).
|
|
63
|
+
- Password-protected / distribution (copy-protected) documents raise
|
|
64
|
+
`EncryptedDocumentError` (their body streams are encrypted and cannot be read).
|
|
65
|
+
|
|
66
|
+
## Provenance / license note
|
|
67
|
+
|
|
68
|
+
`syhwp` is a **clean-room implementation** written from the publicly published
|
|
69
|
+
HANCOM *HWP 5.0 binary format* and *OWPML (HWPX)* specifications. It does **not**
|
|
70
|
+
derive from, or incorporate code from, the AGPL-licensed `pyhwp`. This is what
|
|
71
|
+
allows `syhwp` to be offered under the permissive MIT license.
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT © 2026 sysphere
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "syhwp"
|
|
7
|
+
version = "0.0.5"
|
|
8
|
+
description = "Pure-Python reader for Korean HWP 5.x and HWPX documents — text and tables. Clean-room, permissively licensed."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "sysphere" }]
|
|
13
|
+
keywords = ["hwp", "hwpx", "hancom", "korean", "parser", "text-extraction", "rag"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Natural Language :: Korean",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Text Processing :: Markup",
|
|
26
|
+
]
|
|
27
|
+
dependencies = ["olefile>=0.46"]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
test = ["pytest>=7"]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
syhwp = "syhwp.__main__:main"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/sysphere/syhwp"
|
|
37
|
+
Source = "https://github.com/sysphere/syhwp"
|
|
38
|
+
Issues = "https://github.com/sysphere/syhwp/issues"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/syhwp"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""syhwp — pure-Python reader for Korean HWP 5.x and HWPX documents.
|
|
2
|
+
|
|
3
|
+
Clean-room implementation from the public HANCOM HWP 5.0 / OWPML specifications.
|
|
4
|
+
No AGPL, no external services. See DESIGN.md for architecture and provenance.
|
|
5
|
+
|
|
6
|
+
import syhwp
|
|
7
|
+
doc = syhwp.open("report.hwp") # -> Document (paragraphs, tables)
|
|
8
|
+
text = syhwp.extract_text("report.hwp")
|
|
9
|
+
md = syhwp.extract_markdown("report.hwpx")
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import io
|
|
13
|
+
|
|
14
|
+
from .exceptions import (
|
|
15
|
+
EncryptedDocumentError,
|
|
16
|
+
InvalidHwpError,
|
|
17
|
+
SyhwpError,
|
|
18
|
+
UnsupportedFormatError,
|
|
19
|
+
)
|
|
20
|
+
from .models import Cell, Document, Equation, Image, Paragraph, Table
|
|
21
|
+
|
|
22
|
+
__version__ = "0.0.5"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"open",
|
|
26
|
+
"detect_format",
|
|
27
|
+
"extract_text",
|
|
28
|
+
"extract_markdown",
|
|
29
|
+
"extract_html",
|
|
30
|
+
"Document",
|
|
31
|
+
"Paragraph",
|
|
32
|
+
"Table",
|
|
33
|
+
"Cell",
|
|
34
|
+
"Equation",
|
|
35
|
+
"Image",
|
|
36
|
+
"SyhwpError",
|
|
37
|
+
"UnsupportedFormatError",
|
|
38
|
+
"InvalidHwpError",
|
|
39
|
+
"EncryptedDocumentError",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
_OLE_MAGIC = b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1"
|
|
43
|
+
_ZIP_MAGIC = b"PK\x03\x04"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def detect_format(path) -> str:
|
|
47
|
+
"""Return ``"hwp5"`` or ``"hwpx"`` by inspecting the file's magic bytes."""
|
|
48
|
+
with io.open(path, "rb") as f: # io.open — module-level `open` is our API below
|
|
49
|
+
head = f.read(8)
|
|
50
|
+
if head.startswith(_OLE_MAGIC):
|
|
51
|
+
return "hwp5"
|
|
52
|
+
if head.startswith(_ZIP_MAGIC):
|
|
53
|
+
return "hwpx"
|
|
54
|
+
raise UnsupportedFormatError(f"Not an HWP or HWPX file: {path!r}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def open(path) -> Document: # noqa: A001 - intentional module-level API (cf. tarfile.open)
|
|
58
|
+
"""Parse an HWP 5.x or HWPX document into a :class:`Document`."""
|
|
59
|
+
if detect_format(path) == "hwp5":
|
|
60
|
+
from ._hwp5 import read_document_hwp5
|
|
61
|
+
|
|
62
|
+
return read_document_hwp5(path)
|
|
63
|
+
from ._hwpx import read_document_hwpx
|
|
64
|
+
|
|
65
|
+
return read_document_hwpx(path)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def extract_text(path) -> str:
|
|
69
|
+
"""Extract plain text from an HWP 5.x or HWPX document."""
|
|
70
|
+
return open(path).text
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def extract_markdown(path) -> str:
|
|
74
|
+
"""Extract GFM markdown (tables as pipe tables) from HWP 5.x or HWPX."""
|
|
75
|
+
return open(path).markdown
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def extract_html(path) -> str:
|
|
79
|
+
"""Extract a standalone HTML document (tables as ``<table>``) from HWP/HWPX."""
|
|
80
|
+
return open(path).html
|