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.
@@ -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