hwp2markdown 0.1.0__py3-none-any.whl

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.
Files changed (66) hide show
  1. hwp2markdown-0.1.0.dist-info/METADATA +337 -0
  2. hwp2markdown-0.1.0.dist-info/RECORD +66 -0
  3. hwp2markdown-0.1.0.dist-info/WHEEL +4 -0
  4. hwp2markdown-0.1.0.dist-info/entry_points.txt +2 -0
  5. hwp2markdown-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. hwp2markdown-0.1.0.dist-info/licenses/NOTICE +64 -0
  7. hwp2md/__init__.py +44 -0
  8. hwp2md/blocks.py +160 -0
  9. hwp2md/cli.py +94 -0
  10. hwp2md/convert.py +104 -0
  11. hwp2md/detect.py +68 -0
  12. hwp2md/equation/__init__.py +22 -0
  13. hwp2md/equation/eqedit.py +380 -0
  14. hwp2md/equation/tokens.py +292 -0
  15. hwp2md/errors.py +29 -0
  16. hwp2md/hwp5/__init__.py +1 -0
  17. hwp2md/hwp5/aes.py +167 -0
  18. hwp2md/hwp5/body.py +714 -0
  19. hwp2md/hwp5/distdoc.py +55 -0
  20. hwp2md/hwp5/docinfo.py +227 -0
  21. hwp2md/hwp5/reader.py +264 -0
  22. hwp2md/hwp5/records.py +165 -0
  23. hwp2md/hwpml/__init__.py +1 -0
  24. hwp2md/hwpml/body.py +523 -0
  25. hwp2md/hwpml/reader.py +350 -0
  26. hwp2md/hwpx/__init__.py +1 -0
  27. hwp2md/hwpx/bindata.py +89 -0
  28. hwp2md/hwpx/body.py +554 -0
  29. hwp2md/hwpx/header.py +161 -0
  30. hwp2md/hwpx/reader.py +163 -0
  31. hwp2md/images.py +45 -0
  32. hwp2md/memory.py +185 -0
  33. hwp2md/numbering.py +138 -0
  34. hwp2md/ole.py +184 -0
  35. hwp2md/py.typed +0 -0
  36. hwp2md/render.py +188 -0
  37. hwp2md/skips.py +45 -0
  38. hwp2md/source.py +25 -0
  39. hwp2md/web/__init__.py +1 -0
  40. hwp2md/web/app.py +134 -0
  41. hwp2md/web/server.py +48 -0
  42. hwp2md/web/static/about.html +483 -0
  43. hwp2md/web/static/css/app.css +252 -0
  44. hwp2md/web/static/css/base.css +277 -0
  45. hwp2md/web/static/css/landing.css +415 -0
  46. hwp2md/web/static/index.html +156 -0
  47. hwp2md/web/static/js/app.js +432 -0
  48. hwp2md/web/static/js/effects.js +130 -0
  49. hwp2md/web/static/js/landing.js +515 -0
  50. hwp2md/web/static/js/theme.js +55 -0
  51. hwp2md/web/static/pyodide-runner.js +113 -0
  52. hwp2md/web/static/vendor/README.md +23 -0
  53. hwp2md/web/static/vendor/fonts/JetBrainsMono-Medium.woff2 +0 -0
  54. hwp2md/web/static/vendor/fonts/JetBrainsMono-Regular.woff2 +0 -0
  55. hwp2md/web/static/vendor/fonts/JetBrainsMono-SemiBold.woff2 +0 -0
  56. hwp2md/web/static/vendor/fonts/OFL-JetBrainsMono.txt +93 -0
  57. hwp2md/web/static/vendor/fonts/OFL-Pretendard.txt +94 -0
  58. hwp2md/web/static/vendor/fonts/PretendardVariable.woff2 +0 -0
  59. hwp2md/web/static/vendor/fonts/fonts.css +30 -0
  60. hwp2md/web/static/vendor/gsap/LICENSE.md +15 -0
  61. hwp2md/web/static/vendor/gsap/ScrambleTextPlugin.min.js +11 -0
  62. hwp2md/web/static/vendor/gsap/ScrollTrigger.min.js +11 -0
  63. hwp2md/web/static/vendor/gsap/SplitText.min.js +11 -0
  64. hwp2md/web/static/vendor/gsap/gsap.min.js +11 -0
  65. hwp2md/web/static/vendor/marked.LICENSE.md +44 -0
  66. hwp2md/web/static/vendor/marked.min.js +6 -0
@@ -0,0 +1,337 @@
1
+ Metadata-Version: 2.5
2
+ Name: hwp2markdown
3
+ Version: 0.1.0
4
+ Summary: Convert HWP 5.0 documents to Markdown without Hancom Office
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ License-File: NOTICE
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: lxml>=5
10
+ Requires-Dist: olefile>=0.46
11
+ Provides-Extra: web
12
+ Requires-Dist: starlette>=0.37; extra == 'web'
13
+ Requires-Dist: uvicorn>=0.30; extra == 'web'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # hwp2md
17
+
18
+ 한컴오피스 없이 HWP·HWPX·HWPML 문서를 제목 구조·표·이미지·각주·수식이 살아 있는 Markdown 으로 바꾸는 순수 파이썬 CLI·라이브러리·로컬 웹 미리보기.
19
+
20
+ 본 제품은 한글과컴퓨터의 한/글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다.
21
+
22
+ ![hwp2md 웹 미리보기 — 왼쪽은 렌더링된 미리보기, 오른쪽은 Markdown 원문](docs/images/web-server-mode.png)
23
+
24
+ > 화면의 문서는 시연용으로 코드에서 만든 가상의 공고문이다. 실제 기관·문서와 관계없다.
25
+
26
+ ## 왜 필요한가
27
+
28
+ 한국의 공공·기업 문서는 HWP 안에 갇혀 있다. LLM 시대에 가장 먼저 필요한 건 그 문을 여는 열쇠다.
29
+
30
+ - HWP/HWPX 는 RAG 파이프라인, LLM 입력, 버전 관리 어디에도 바로 쓸 수 없다.
31
+ - 기존 변환 도구는 표·이미지 위치를 잃거나, 한컴 설치를 요구한다.
32
+ - 개발자들이 매번 같은 파싱 코드를 다시 짠다.
33
+
34
+ hwp2md 는 이 문제를 이렇게 푼다.
35
+
36
+ - 한컴 없이 동작하는 순수 파이썬 (의존성 `olefile`, `lxml` 두 개).
37
+ - 표는 Markdown 표로, 이미지는 파일로 추출해 상대 경로로 링크한다.
38
+ - 문서의 제목 계층(개요 수준)을 `#` 제목으로 보존해 청킹에 유리하게 만든다.
39
+ - 같은 엔진을 CLI·라이브러리·웹 미리보기가 함께 쓴다. 세 곳의 출력은 바이트 단위로 같다.
40
+
41
+ ## 빠른 시작
42
+
43
+ ### 설치
44
+
45
+ PyPI 배포 이름은 `hwp2markdown` 이다. 설치 후 명령과 import 이름은 `hwp2md` 다. Python 3.11 이상이 필요하다.
46
+
47
+ ```bash
48
+ pip install 'hwp2markdown[web]' # 또는: uv tool install 'hwp2markdown[web]'
49
+ hwp2md --version
50
+ ```
51
+
52
+ PyPI 의 `hwp2md` 는 이 저장소와 무관한 다른 프로젝트다. 설치할 때는 `hwp2markdown` 을 쓴다.
53
+
54
+ 웹 미리보기 의존성(Starlette·uvicorn)은 extra `web` 에만 있다. 기본 설치(`pip install hwp2markdown`, CLI·라이브러리)는 `olefile`·`lxml` 만 쓴다. extra 없이 `hwp2md serve` 를 실행하면 `pip install 'hwp2markdown[web]'` 안내와 함께 종료 코드 1로 끝난다.
55
+
56
+ 소스에서 설치(개발용):
57
+
58
+ ```bash
59
+ git clone <이 저장소> hwp2md && cd hwp2md
60
+
61
+ # 방법 1: uv (웹 미리보기·테스트 도구까지 함께 설치)
62
+ uv sync
63
+ uv run hwp2md --version
64
+
65
+ # 방법 2: pip (웹 미리보기가 필요 없으면 '[web]' 을 빼도 된다)
66
+ pip install '.[web]'
67
+ hwp2md --version
68
+ ```
69
+
70
+ 아래 예시는 `uv run` 을 생략했다. 소스에서 uv 로 설치했다면 앞에 `uv run` 을 붙인다.
71
+
72
+ ### ① CLI
73
+
74
+ ```text
75
+ usage: hwp2md [-h] [-o OUT_DIR] [--version] INPUT
76
+ usage: hwp2md serve [-h] [--host HOST] [--port PORT] [--no-browser]
77
+ ```
78
+
79
+ 시연용 합성 문서(제목 5개·본문 5단락·표 1개·그림 1개)를 변환한 실제 출력이다.
80
+
81
+ ```console
82
+ $ hwp2md demo.hwpx -o out/
83
+ ✓ demo.hwpx → out/demo.md
84
+ 단락 5개 · 표 1개 · 제목 5개 · 이미지 1개
85
+ 이미지: out/demo_images/ (1 files)
86
+ ```
87
+
88
+ ```text
89
+ out/
90
+ ├── demo.md
91
+ └── demo_images/
92
+ └── img_001.png
93
+ ```
94
+
95
+ `out/demo.md`:
96
+
97
+ ```markdown
98
+ # 2026년도 가상 문서 디지털 전환 지원사업 공고
99
+
100
+ 이 문서는 hwp2md 시연을 위해 만든 가상의 공고문입니다. 실제 기관·사업과 관계없습니다.
101
+
102
+ ## 1. 사업 개요
103
+
104
+ 종이와 한글 문서로 관리하던 업무 자료를 검색 가능한 형태로 바꾸는 기관을 지원합니다.
105
+
106
+ | 구분 | 내용 |
107
+ |---|---|
108
+ | 지원 규모 | 총 10억 원 (기관당 최대 5천만 원) |
109
+ | 지원 기간 | 2026. 3. ~ 2026. 12. |
110
+ | 신청 방법 | 온라인 접수 (붙임 서식 작성 후 제출) |
111
+
112
+ ## 2. 추진 절차
113
+
114
+ 신청서 접수 후 서면 심사를 거쳐 지원 대상을 선정합니다.
115
+
116
+ ![그림 1](demo_images/img_001.png)
117
+
118
+ ### 2.1 심사 기준
119
+
120
+ 사업 필요성, 추진 계획의 구체성, 성과 활용 방안을 평가합니다.
121
+
122
+ ## 3. 문의
123
+
124
+ 문의는 가상 사업 운영 사무국으로 해 주십시오.
125
+ ```
126
+
127
+ - `-o` 를 생략하면 현재 폴더에 쓴다. 출력 폴더가 없으면 만든다.
128
+ - HWP 5.0·HWPX·HWPML 은 같은 명령이다. 포맷은 확장자가 아니라 파일 내용으로 판별한다.
129
+ - 이미지가 없는 문서는 `_images` 폴더를 만들지 않고, 요약 줄에서 0개인 제목·이미지 항목을 생략한다.
130
+ - 종료 코드: 0 성공 · 1 변환 오류 · 2 파일 없음 · 3 HWP 가 아니거나 지원하지 않는 문서(DRM·비밀번호 문서 등). 오류 줄은 `unsupported HWP file (DRM protected): …`, `unsupported HWP file (password protected): …` 처럼 거부 이유를 구분한다.
131
+ - 변환하지 못한 요소가 있으면 마지막에 ` 건너뜀: 머리말·꼬리말 2개 · 메모 1개` 같은 줄이 붙는다(아래 "건너뜀 보고").
132
+ - 변환할 파일 이름이 `serve` 이면 `hwp2md ./serve` 처럼 경로로 적는다.
133
+
134
+ ### ② 웹 미리보기
135
+
136
+ ```bash
137
+ hwp2md serve # http://localhost:8770 , 브라우저가 자동으로 열린다
138
+ hwp2md serve --port 8771 # 8770 이 이미 쓰이고 있을 때
139
+ hwp2md serve --no-browser # 브라우저 자동 열기 끔
140
+ ```
141
+
142
+ `/` 은 변환 화면, `/about` 은 서비스 소개(기능, 사용 장면, 추출 한계 `/about#limits`)다. 두 화면에 쓰는 글꼴·GSAP·marked 는 패키지 안에 동봉돼 있어 서버 모드는 인터넷 없이 동작한다.
143
+
144
+ 1. HWP·HWPX 파일을 끌어다 놓거나 클릭해서 고른다 (최대 50MB).
145
+ 2. 왼쪽에 렌더링된 미리보기, 오른쪽에 Markdown 원문이 나온다.
146
+ 3. "zip 다운로드" 로 `.md` 와 이미지 폴더를 한 번에 받는다.
147
+
148
+ 오른쪽 위 "브라우저 안에서만 처리" 스위치로 변환 위치를 고른다.
149
+
150
+ | | 서버 모드 (기본) | 브라우저 안에서만 처리 |
151
+ |---|---|---|
152
+ | 변환 위치 | 로컬 `hwp2md serve` 프로세스 (메모리에서 변환) | 브라우저 안의 Pyodide (WebAssembly 파이썬) |
153
+ | 파일 바이트 전송 | 로컬 서버로 `POST /api/convert` | 보내지 않음 |
154
+ | 첫 변환까지 | 즉시 | 첫 로딩 약 1분 (Pyodide·lxml 다운로드, 측정 약 65초) |
155
+ | 인터넷 | 필요 없음 (화면 자원 동봉) | 필요 (Pyodide 를 jsDelivr CDN 에서 받음) |
156
+ | 결과 | 같은 Markdown | 같은 Markdown (E2E 로 바이트 비교) |
157
+
158
+ ![서버 모드 화면](docs/images/web-server-mode.png)
159
+
160
+ ![브라우저 안에서만 처리 모드 화면 — 상태 표시에 "파일이 서버로 전송되지 않습니다"](docs/images/web-browser-mode.png)
161
+
162
+ ### ③ 라이브러리
163
+
164
+ 파일로 쓰기 (`convert`):
165
+
166
+ ```python
167
+ from hwp2md import convert
168
+
169
+ result = convert("input.hwp", "out/") # out/input.md (+ out/input_images/)
170
+ print(result.output) # Path("out/input.md")
171
+ print(result.heading_count, result.table_count, result.image_count)
172
+ print(result.markdown) # 파일에 쓴 것과 같은 내용
173
+ ```
174
+
175
+ 메모리에서만 변환하기 (`convert_bytes`, 디스크에 아무것도 쓰지 않는다):
176
+
177
+ ```python
178
+ from pathlib import Path
179
+ from hwp2md import convert_bytes
180
+
181
+ data = Path("input.hwp").read_bytes()
182
+ result = convert_bytes(data, filename="input.hwp") # filename 은 이미지 폴더 이름에만 쓰인다
183
+ print(result.markdown) # convert() 가 쓰는 .md 와 바이트 동일
184
+ for img in result.images: # 추출된 이미지 (고유 파일만)
185
+ print(img.file_name, img.mime, len(img.data))
186
+ ```
187
+
188
+ - 시그니처: `convert(path: str | Path, out_dir: str | Path = ".") -> ConvertResult`, `convert_bytes(data: bytes, filename: str = "document.hwp") -> MemoryResult`.
189
+ - 예외: `NotHwpFileError`(HWP 아님), `UnsupportedHwpError`(지원하지 않는 문서)와 그 하위 `DrmProtectedError`(DRM 적용 문서)·`EncryptedDocumentError`(비밀번호 문서), 모두 `Hwp2mdError` 하위.
190
+ - 건너뛴 요소: `result.skipped` 는 `(("header_footer", 2), …)` 처럼 종류와 개수의 튜플이다(0개는 빠진다). `convert`·`convert_bytes` 결과 모두에 있다. `convert` 는 파일이 없으면 `FileNotFoundError`, `convert_bytes` 는 빈 입력·50MB 초과도 `Hwp2mdError` 하위 예외로 거부한다.
191
+
192
+ ## 지원 범위
193
+
194
+ | 구분 | 항목 | 설명 |
195
+ |---|---|---|
196
+ | 지원 | 포맷 | HWP 5.0 바이너리(OLE), HWPX(zip/XML), HWPML 2.x(단일 XML). 확장자가 아니라 내용으로 판별 |
197
+ | 지원 | 본문 단락 | 탭은 공백, 줄바꿈 유지 |
198
+ | 지원 | 제목 계층 | 문단 모양의 개요 1~6수준 → `#`~`######`, 7수준은 `######` |
199
+ | 지원 | 표 | GFM 표 (첫 행이 헤더), 셀 안 여러 단락은 `<br>`, `\|` 이스케이프, 중첩 표는 셀 텍스트로 평탄화 |
200
+ | 지원 | 이미지 | 본문 그림을 `<이름>_images/img_001.png` … 로 추출(원본 바이트 그대로)하고 제자리에 `![그림 N](…)` 링크. 같은 그림을 다시 참조하면 파일 1개 |
201
+ | 지원 | 표 칸 안 그림 | 칸 안에 `![그림 N](…)` 링크. 문서 순서대로 그림 번호를 쓴다 |
202
+ | 지원 | 캡션 | 표·그림 캡션을 개체 뒤 단락으로 출력 |
203
+ | 지원 | 글상자·도형·묶음 | 안쪽 글과 그림을 개체 자리에 출력 |
204
+ | 지원 | 하이퍼링크 | `[글자](주소)`. http·https·mailto 만 링크로 쓰고 그 밖의 주소는 글자만 남긴다 |
205
+ | 지원 | 각주·미주 | 본문에 `[^1]`·`[^e1]` 참조, 문서 끝에 정의 |
206
+ | 지원 | 수식 | 한글 수식 스크립트 → LaTeX `$…$` (단락에 수식만 있으면 `$$…$$`). 변환에 실패하면 원문 스크립트를 코드 스팬으로 |
207
+ | 지원 | 번호·글머리표 목록 | 글머리표는 `-`, `1.`·`1)` 꼴 번호는 Markdown 목록 문법, 그 밖의 번호(`가.`·`①` 등)는 단락 앞 글자로. 번호는 문단 번호 정의로 근사 계산 |
208
+ | 지원 | OOXML 차트 | 차트에 저장된 값으로 `차트: 제목` 단락 + 표 |
209
+ | 지원 | 배포용 문서 | 공개된 절차로 복호화해 변환 (아래 "배포용 문서 고지") |
210
+ | 부분 | OLE 개체 | 원본이 그림(PNG·JPEG·GIF·BMP)이면 그림으로 추출, 그 밖은 건너뜀 보고 |
211
+ | 제한 | 병합 셀 | 원 셀에 텍스트, 덮인 칸은 빈 칸 (GFM 이 병합을 표현하지 못함) |
212
+ | 제외 | 머리말·꼬리말 | 본문 흐름에 넣지 않고(청킹 잡음 방지) 건너뜀으로 보고. 세 포맷 동일 |
213
+ | 제외 | 메모·숨은 설명 | 작성자 정보가 있을 수 있어 내용은 출력하지 않고 개수만 보고 |
214
+ | 제한 | 서식 | 글꼴·색상·정렬은 버린다 ("하지 않는 것" 참조) |
215
+ | 거부 | DRM 적용 문서 | `DrmProtectedError`, CLI 종료 코드 3, API 오류 코드 `drm` |
216
+ | 거부 | 비밀번호 문서 | `EncryptedDocumentError`, CLI 종료 코드 3, API 오류 코드 `encrypted`. 비밀번호 입력 기능은 없다 |
217
+ | 미지원 | HWP 3.0 이하 | HWP 파일이 아닌 것으로 판정 (종료 코드 3) |
218
+
219
+ 요소별 상태(추출·부분·불가·제외)와 이유, 대안은 [docs/extraction-limits.md](docs/extraction-limits.md) 에 있다.
220
+
221
+ ### 건너뜀 보고
222
+
223
+ 변환하지 못한 요소는 조용히 버리지 않고 종류별 개수를 알려 준다. 종류는 머리말·꼬리말, 메모, 숨은 설명, 데이터를 꺼낼 수 없는 차트, OLE 개체, 글맵시, 양식 개체, 외부 연결 그림, 본체 없는 그림 아홉 가지다.
224
+
225
+ | 어디서 | 모양 |
226
+ |---|---|
227
+ | CLI | 요약 끝 줄 ` 건너뜀: 머리말·꼬리말 2개 · OLE 개체 1개` |
228
+ | 라이브러리 | `ConvertResult.skipped`·`MemoryResult.skipped` (종류, 개수) 튜플 |
229
+ | 웹 API | 응답 JSON 의 `skipped`: `[{"kind": "header_footer", "label": "머리말·꼬리말", "count": 2}]` (항상 있음, 없으면 빈 배열) |
230
+ | 웹 화면 | 미리보기 위 "변환하지 못한 요소: …" 경고 |
231
+
232
+ ### 배포용 문서 고지
233
+
234
+ 배포용 문서(편집·복사·인쇄를 제한해 배포한 HWP 5.0 문서)는 기본으로 변환한다. 편집·복사 제한 문서를 텍스트로 꺼내므로 작성자의 의도와 사용 권한을 확인하세요. hwp2md 는 공개된 복호화 절차만 쓰며, DRM 적용 문서와 비밀번호 문서는 풀지 않고 거부한다.
235
+
236
+ ## 구조
237
+
238
+ ```mermaid
239
+ flowchart LR
240
+ subgraph entry["진입점"]
241
+ CLI["CLI<br>hwp2md INPUT -o OUT"]
242
+ LIB["라이브러리<br>convert / convert_bytes"]
243
+ WEB["웹 서버<br>hwp2md serve"]
244
+ PYO["브라우저 Pyodide<br>같은 소스를 브라우저에서 실행"]
245
+ end
246
+ IN[".hwp / .hwpx / .hml<br>경로 또는 bytes"] --> DET["detect<br>내용으로 포맷 판별"]
247
+ CLI --> IN
248
+ LIB --> IN
249
+ WEB --> IN
250
+ PYO --> IN
251
+ DET -->|"OLE"| H5["hwp5 리더<br>레코드 파싱"]
252
+ DET -->|"zip + mimetype"| HX["hwpx 리더<br>XML 파싱"]
253
+ DET -->|"루트 HWPML"| HM["hwpml 리더<br>XML 파싱"]
254
+ H5 --> BL["Block 리스트<br>Paragraph / Table / Image / Note"]
255
+ HX --> BL
256
+ HM --> BL
257
+ BL --> RD["render<br>Markdown"]
258
+ RD --> OUT["문서.md + 문서_images/<br>(CLI) 또는 메모리 결과(웹·API)"]
259
+ ```
260
+
261
+ 세 포맷의 리더는 같은 블록 모델을 돌려주고, 렌더러는 포맷을 모른다. 그래서 CLI·웹 서버·브라우저 모드가 같은 Markdown 을 낸다. 모듈별 역할, 웹 엔드포인트, 브라우저 모드 시퀀스, 확장 방법은 [docs/system-overview.md](docs/system-overview.md) 에 있다.
262
+
263
+ ## 개인정보·보안 설계
264
+
265
+ - **업로드는 디스크에 쓰지 않는다.** 웹 서버는 요청 본문을 메모리에서 바로 변환한다(`convert_bytes`). 임시 파일도 만들지 않는다. E2E `test_no_files_written` 이 변환·zip·오류 요청 뒤 서버의 작업 폴더와 임시 폴더가 비어 있는지 확인한다.
266
+ - **브라우저 모드는 파일 바이트를 서버로 보내지 않는다.** 네트워크 요청은 Pyodide·lxml 다운로드(jsDelivr CDN)와 hwp2md 소스 zip(`GET /pkg/hwp2md.zip`)뿐이다. E2E `test_browser_mode_sends_no_file` 이 변환 중 GET 이외 요청 0건, `/api/` 요청 0건을 확인한다.
267
+ - **미리보기 XSS 방어.** 문서 내용은 외부 입력으로 본다. 미리보기 렌더러는 원시 HTML 중 표 셀 줄바꿈용 `<br>` 만 살리고 나머지는 이스케이프한다. 링크 주소는 http·https·mailto 와 상대 경로만, 이미지 주소는 상대 경로와 `data:image/*;base64` 만 허용한다.
268
+ - **XML·zip 방어.** HWPX XML 은 외부 엔티티·네트워크를 끈 파서로 읽고, 50MB 를 넘는 섹션은 거부한다. 웹 입력은 50MB 상한이며 `Content-Length` 가 넘으면 본문을 읽기 전에 413 으로 거절한다.
269
+ - **로컬 전용 도구다.** 기본 바인드는 `localhost` 이고 인증이 없다. `--host` 로 외부에 열 수는 있지만 외부 공개용으로 설계하지 않았다.
270
+
271
+ ## 품질·테스트
272
+
273
+ | 종류 | 위치 | 수 | 무엇을 보나 |
274
+ |---|---|---|---|
275
+ | 단위 | `tests/unit` | 813 passed (`uv run pytest tests/unit -q`, 2026-10-23 직접 실행) | 레코드·XML 파서, 제목 판정, 표 조립, 이미지 추출, 렌더러, 포맷 판별, 메모리 API, 웹 앱(TestClient) |
276
+ | E2E | `tests/e2e` | 120 passed (`uv run pytest tests/e2e -q`, 2026-10-23 직접 실행, 실제 샘플 요소별 18개 포함) | 실제 콘솔 스크립트 실행, 실제 서버 + Playwright(Chromium) 화면 |
277
+
278
+ E2E 가 검증하는 핵심:
279
+ - 서버 모드 · 브라우저 모드 · CLI 의 Markdown 이 같다 (`test_modes_produce_identical_markdown`, `test_convert_matches_cli`).
280
+ - zip 다운로드 내용이 CLI 출력과 같다.
281
+ - 브라우저 모드에서 파일이 서버로 가지 않는다, 서버 모드에서 업로드가 디스크에 남지 않는다.
282
+ - 잘못된 입력(HWP 아님·빈 파일·50MB 초과)은 API 가 각각 415·400·413 으로 거절하고, HWP 가 아닌 파일의 오류 문구는 두 모드에서 같다.
283
+ - 포트 충돌·web extra 미설치 시 안내와 종료 코드.
284
+
285
+ 테스트 문서는 `tests/synth_hwp5.py`·`tests/synth_hwpx.py` 가 명세 기준으로 코드에서 만든다(제목·표·병합 셀·이미지·암호화 플래그 등 변형을 바이트 단위로 통제). 실제 공개 샘플은 라이선스 확인 전이라 저장소에 넣지 않았고, `tests/fixtures/` 에 넣으면 E2E 가 자동으로 함께 돌린다.
286
+
287
+ 개발 명령:
288
+
289
+ ```bash
290
+ uv sync && uv run playwright install chromium # 개발 환경 (E2E 용 브라우저 포함)
291
+ uv run pytest tests/unit -q # 단위
292
+ uv run pytest tests/e2e -q # E2E (브라우저 모드 케이스는 인터넷 필요)
293
+ uv run ruff check src tests && uv run ruff format --check src tests
294
+ uv run mypy src # strict
295
+ ```
296
+
297
+ ## 한계와 알려진 이슈
298
+
299
+ - 브라우저 모드 첫 로딩이 약 1분 걸리고 인터넷(jsDelivr)이 필요하다. 오프라인 번들은 없다.
300
+ - 공공기관 샘플 20개 변환 품질 비교표는 아직 작성하지 않았다.
301
+ - CI 는 Python 3.11·3.13 에서 검증한다. 3.14 에서는 손상된 zip 의 오류 문구가 다르게 나오는 알려진 차이가 있다.
302
+ - 기본 포트 8770 이 다른 프로그램과 겹치면 `--port` 로 바꾼다 (자동으로 다음 포트를 찾지 않는다).
303
+ - 이미지가 있는 실제 HWP 5.0 샘플이 없어, HWP 5.0 이미지 경로는 합성 문서와 공개 스펙 교차 확인으로만 검증했다.
304
+ - 이미지 포맷은 변환하지 않는다. WMF/EMF 같은 원본은 그대로 저장되어 브라우저에서 안 보일 수 있다.
305
+ - 구형 차트(차트 값이 OOXML 로 저장되지 않은 것)는 데이터를 꺼내지 못하고 건너뜀으로 보고한다.
306
+ - 쪽 번호·날짜 같은 필드 값은 다시 계산하지 않는다. 목록 번호는 근사값이다.
307
+ - 단락 중간에 놓인 그림의 위치가 HWP 5.0 과 HWPX 에서 다를 수 있다.
308
+ - 요소별 전체 목록과 대안은 [docs/extraction-limits.md](docs/extraction-limits.md) 에 있다.
309
+
310
+ ## 로드맵
311
+
312
+ - [x] 각주·미주 → Markdown 하단 참조(`[^1]`)
313
+ - [ ] RAG 옵션: `--split section` (제목 단위 분할), `--meta` (목차·표 수·이미지 경로·원본 해시 JSON)
314
+ - [ ] 폴더 일괄 변환
315
+ - [x] 배포용 문서 지원
316
+ - [ ] 병합 셀 개선 (`--table html` 로 rowspan/colspan 보존)
317
+ - [ ] 공공기관 샘플 20개 품질 비교표
318
+ - [x] PyPI 배포(hwp2markdown) + GitHub Actions CI
319
+
320
+ ## 문서
321
+
322
+ - [docs/vision.md](docs/vision.md) — 완성된 모습 (사용자 시나리오)
323
+ - [docs/system-overview.md](docs/system-overview.md) — 내부 구조 (모듈·데이터 흐름·웹·보안·테스트·확장)
324
+ - [docs/decisions.md](docs/decisions.md) — 결정 기록 (날짜와 이유)
325
+ - [docs/releasing.md](docs/releasing.md) — 릴리스 절차 (PyPI 게시 설정·태그·실패 대응)
326
+ - [docs/extraction-limits.md](docs/extraction-limits.md) — 추출 한계 (요소별 상태·이유·대안)
327
+
328
+ ## 하지 않는 것
329
+
330
+ - 역변환 (md → hwp)
331
+ - 서식 완벽 재현 (글꼴·색상)
332
+
333
+ ## 라이선스
334
+
335
+ - hwp2md 는 [MIT 라이선스](LICENSE)로 배포한다.
336
+ - `src/hwp2md/equation/eqedit.py`·`tokens.py` 는 python-hwpx 에서 가져온 코드로 Apache License 2.0 을 따른다. 출처·커밋·변경 사실은 [NOTICE](NOTICE) 에 있다.
337
+ - 본 제품은 한글과컴퓨터의 한/글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다.
@@ -0,0 +1,66 @@
1
+ hwp2md/__init__.py,sha256=uhV7fPCl2Gy-PPlW3ZB_52DeaQGoV_oaryX-b7rDG8Y,1023
2
+ hwp2md/blocks.py,sha256=hfsD9gTMZguXi_HPOdirmrYLUS-TrBAS4Zma-qBP6YA,5220
3
+ hwp2md/cli.py,sha256=OUW6r09E187CK37ezGRmnYqCtB3DJ8cOw0GAPam_FNQ,3488
4
+ hwp2md/convert.py,sha256=lX3V_tGi2HR_4m18F3oNmB56RcvdkLrWg7-Kh-OWvHY,4215
5
+ hwp2md/detect.py,sha256=SrmXrJF1J2Psm4EbkBWUlBg521MTFERLwSaDQO93u4k,2726
6
+ hwp2md/errors.py,sha256=CiJyF4yEt0sm9fFeJHrzh7xspxlKWa9zXh8MpxXX7a8,770
7
+ hwp2md/images.py,sha256=cElesHex6RcodoR9uczLkn5Y5MhMdEodkEecKq4ziRs,1259
8
+ hwp2md/memory.py,sha256=ZCj6uXDj24mYLUBaGtRg9ckTlGxvyp6g2r47BQlRe-Y,6398
9
+ hwp2md/numbering.py,sha256=FntJ09xn3JDrYCbL3FB0tlLrfJx-48PcECdxKACCjvw,4324
10
+ hwp2md/ole.py,sha256=Ncu6JzTAVeeUbb9mNt2pZBAb4YtoujwcbzZc7G85QoI,6146
11
+ hwp2md/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ hwp2md/render.py,sha256=9dnNxtWN64TqNMy-Y2KGvLtE7u5qfG-qtA71OGNP-yc,7171
13
+ hwp2md/skips.py,sha256=3nfts11mQ7BnmZYjtawpiIPTsAlzHE0hlwXQIKmVF7s,1257
14
+ hwp2md/source.py,sha256=io7at1ZunruLp7nLJq48e7ubbBI50X_5GEJouod4ZLU,740
15
+ hwp2md/equation/__init__.py,sha256=s7i4-8tcENUQOrAYStFvHREYyRNcUoYhBRZ2iW77Hyk,770
16
+ hwp2md/equation/eqedit.py,sha256=MjeQvapiuY_n22mbkhu7ZB3L-X2vqspEgd6V7gHchYo,14202
17
+ hwp2md/equation/tokens.py,sha256=Zh8L0_OgPumNxezAQwUqRwFbtVClyyviH_bJ3CHX3g4,8259
18
+ hwp2md/hwp5/__init__.py,sha256=PmoSaSJI9UiM1pwWl-jel2VX0RCJxR-cg1TEpn4v7YU,37
19
+ hwp2md/hwp5/aes.py,sha256=g0-zaCF9zgpUJJHpAhQLupfxjCOYANTXrfju8XF42sQ,4888
20
+ hwp2md/hwp5/body.py,sha256=joWTdcz7NHaH1ITVpAqle_W_p7cJSeeeKdtHiBWpxZU,24897
21
+ hwp2md/hwp5/distdoc.py,sha256=eHJhSnCoPZv_qrA62yM8Icc4S4FQsu1rh1I244iSLDA,2216
22
+ hwp2md/hwp5/docinfo.py,sha256=fuD-pMswrvVEVhwdWNuJVmhLFOWgIfWmZ8HWbRJLdmY,8776
23
+ hwp2md/hwp5/reader.py,sha256=AJJ35vYfvOzYyd6_Zj8AoD3A7_qlh9KkogZ6c2SBgA0,10408
24
+ hwp2md/hwp5/records.py,sha256=0Khsgu6XPSDdSTMHa0EQOLlS39prL3m_C6A4UkjQULQ,5947
25
+ hwp2md/hwpml/__init__.py,sha256=sxng8-cqxlTq59Hq-hGpMt2A_BFHCwsFPFnScqC_QAw,62
26
+ hwp2md/hwpml/body.py,sha256=z9kb5S4NXOZqDYAItkqJ2QQr1aPTDFYaaCbMObaWe7Q,19068
27
+ hwp2md/hwpml/reader.py,sha256=331QoGFKGma9uUdxa5VOl0iAC2MtcEDF7aArQTmy5v8,13008
28
+ hwp2md/hwpx/__init__.py,sha256=JJyvPHrxZnt1WemXOHkIf5hZygWoyDer_7rtjMUy-8E,43
29
+ hwp2md/hwpx/bindata.py,sha256=PlyIJQxNg2sfwO7f9NjslFDdZIGobUNBeX98Sv1o1o4,3484
30
+ hwp2md/hwpx/body.py,sha256=gxh1mPqYSg7xw3hgwCT1e2QRC-6fGjVxBBWNUdSTLEQ,20070
31
+ hwp2md/hwpx/header.py,sha256=EF1S543CmGGeuPisynZLDPZZCCJye4zeinB6gSF0Hk8,5821
32
+ hwp2md/hwpx/reader.py,sha256=oxwjbZYGUaZBMe87bF3ODHAnK_T61SNzE11dLG70R7A,6385
33
+ hwp2md/web/__init__.py,sha256=JEgKC2YtgfEMSoGMc0Blsa2nnAS3SBdXFabLWsUaFGg,69
34
+ hwp2md/web/app.py,sha256=ynEKAHTXjPgGZnH876auigdfpWCjCcUCL3kdRrpmt7o,4510
35
+ hwp2md/web/server.py,sha256=9Ido9-RkhFfHvqIXYvaeUySpS6ngurh-HPx5O2nF3co,1431
36
+ hwp2md/web/static/about.html,sha256=Fn0RiBQh7mHQIPT2fTEfQOiuRRBmSJOxUs5PZXqmweo,37655
37
+ hwp2md/web/static/index.html,sha256=eHCVhnG6rkVMoCj8PmTSpWKOgW9EwpCpRph2iVT2Hlc,9034
38
+ hwp2md/web/static/pyodide-runner.js,sha256=HuqnBV-bXcp3f1LhFzX1qSiINJZaYZJpEeriAHTqymc,3353
39
+ hwp2md/web/static/css/app.css,sha256=deHgu_1ysj8IZE9tlNrkGXrQUmNcIUBJGc-BfHfTiD0,15929
40
+ hwp2md/web/static/css/base.css,sha256=GqIDUF_RbxQ07eVqdoOK6JPlXoRLURAX3eIlY73VoqM,10687
41
+ hwp2md/web/static/css/landing.css,sha256=E8UTfl0ERptzRcsQrN2jIpmKMyyLO4FFWmfjcWQRuIE,26501
42
+ hwp2md/web/static/js/app.js,sha256=QcF-QxvyDsUxsw4HZ9GH_z0wxdztTh0YCOv-eHuaR0Y,16675
43
+ hwp2md/web/static/js/effects.js,sha256=c5xiEwo7PhHxhsf9QXhnivT8VYdvInl2ny_9ixDMm6s,5315
44
+ hwp2md/web/static/js/landing.js,sha256=hppRCRlmN9gDkTUbBWTKzjr9zgz2YBdIq-v2LhNdpLA,24129
45
+ hwp2md/web/static/js/theme.js,sha256=u1DNXb7uRFEJbiWxevnjbwX3baIXmmWWKwGl8gWn-U4,2151
46
+ hwp2md/web/static/vendor/README.md,sha256=nhOHtSdn2UjaDxrB4TVxDONsc9lC21MDw30tydzHeNY,3278
47
+ hwp2md/web/static/vendor/marked.LICENSE.md,sha256=jjo_gvWaYJWPVsoI9EVkfDKkcz3HymwsRvbriYRxq5w,2942
48
+ hwp2md/web/static/vendor/marked.min.js,sha256=Ffq85bZYmLMrA_XtJen4kacprUwNbYdxEKd0SqhHqJQ,35479
49
+ hwp2md/web/static/vendor/fonts/JetBrainsMono-Medium.woff2,sha256=CGxI376p3a_xMg9-CTmbjikk6IzmdFNyElXbO9u1o1M,93824
50
+ hwp2md/web/static/vendor/fonts/JetBrainsMono-Regular.woff2,sha256=qcsc2CMysjpH46EjnSXRPIbRbEIgaV40skPv-pmfRfI,92164
51
+ hwp2md/web/static/vendor/fonts/JetBrainsMono-SemiBold.woff2,sha256=kY7a1UKh2mCP0rqNrrr_msgCMJED_nYO7UZbi05H-vE,94472
52
+ hwp2md/web/static/vendor/fonts/OFL-JetBrainsMono.txt,sha256=MPDBNuPIjkItB5Gs2XI4hw-QVKlym8NM8v8NTtjKxK0,4399
53
+ hwp2md/web/static/vendor/fonts/OFL-Pretendard.txt,sha256=sEU4yavsOaPbdRCM8K8P2cdwMv6Kos84NFtNJQ6Y444,4419
54
+ hwp2md/web/static/vendor/fonts/PretendardVariable.woff2,sha256=lZnxL9QvwLzhzVC0egwCLhCNeqZN0NG7DtRPMoLZALQ,2057688
55
+ hwp2md/web/static/vendor/fonts/fonts.css,sha256=rHnYGgkHcsrit6MTqON4KBKDMOwF-BmzC0CUSRM-4mI,957
56
+ hwp2md/web/static/vendor/gsap/LICENSE.md,sha256=FY3l5oC4A5MgqkLH0HleSQqabD-G87rXcFo1g69O-a8,798
57
+ hwp2md/web/static/vendor/gsap/ScrambleTextPlugin.min.js,sha256=6c4EoH2TLYUG8hoNsOOUzlJvabnxqTxwtk3juCPapFI,11687
58
+ hwp2md/web/static/vendor/gsap/ScrollTrigger.min.js,sha256=MIIZOQ5eO4TNoMSB5wyqmCCIOuEL2kTm6aFJqBqsSz8,44157
59
+ hwp2md/web/static/vendor/gsap/SplitText.min.js,sha256=XlGeokcPqhXKTU8n6SY-WQbAfANKm0dxFk3uEIE2Vjo,7247
60
+ hwp2md/web/static/vendor/gsap/gsap.min.js,sha256=lsAbgfRKMpDitFMvVeLJU0sq3EMnOhnzdWsstB8P0LY,72435
61
+ hwp2markdown-0.1.0.dist-info/METADATA,sha256=fZ33SQhYT2VF1chfcEnUnw8rDMH-OquVF0WcejTB8ro,19663
62
+ hwp2markdown-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
63
+ hwp2markdown-0.1.0.dist-info/entry_points.txt,sha256=1bjMnU9VrkuMAFzq5QEaDwEEUs0i6bbl3BZU-f2D56I,43
64
+ hwp2markdown-0.1.0.dist-info/licenses/LICENSE,sha256=6rO0ayMmAIt7_oXEqEx2bAsQe_1KChyHpREcOpODVLc,1076
65
+ hwp2markdown-0.1.0.dist-info/licenses/NOTICE,sha256=50Fb_Nt2Z1QEDALdhSLUJVWjXVrr1j6p85O6R7I05-I,2906
66
+ hwp2markdown-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ hwp2md = hwp2md.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hwp2md contributors
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.
@@ -0,0 +1,64 @@
1
+ hwp2md
2
+ Third-party notices
3
+
4
+ hwp2md is licensed under the MIT License (see LICENSE). The vendored files
5
+ listed below remain under their original licenses.
6
+
7
+ ------------------------------------------------------------------------
8
+ HWP document format public specification
9
+
10
+ 본 제품은 한글과컴퓨터의 한/글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다.
11
+
12
+ ------------------------------------------------------------------------
13
+ python-hwpx — equation converter (vendored)
14
+
15
+ Source repository: https://github.com/airmang/python-hwpx
16
+ Commit: 7ca45822076b9feb7cf3a7cfb2c924d121162b4b
17
+ License: Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
18
+ Copyright 2025-2026 airmang
19
+
20
+ Vendored files:
21
+ src/hwpx/equation/eqedit.py -> src/hwp2md/equation/eqedit.py
22
+ src/hwpx/equation/tokens.py -> src/hwp2md/equation/tokens.py
23
+
24
+ Changes made:
25
+ - Moved into the package hwp2md.equation (relative imports unchanged).
26
+ - eqedit.py: added a "# ruff: noqa: E501" line after the SPDX header so the
27
+ project linter accepts the original long comment lines. No logic changes.
28
+ - Added src/hwp2md/equation/__init__.py (new, not from python-hwpx) with a
29
+ to_latex() wrapper that returns None on empty input or conversion failure.
30
+
31
+ Upstream NOTICE excerpt (python-hwpx):
32
+ This product includes software developed by airmang.
33
+ Licensed under the Apache License, Version 2.0.
34
+ - OpenBapul/hml-equation-parser (Apache-2.0): HULK-style EqEdit <-> LaTeX
35
+ token mapping, referenced for reader-direction <hp:equation> script
36
+ rendering. The token maps are a clean-room re-derivation from the public
37
+ EqEdit grammar; no source code was copied, translated, or vendored.
38
+
39
+ ------------------------------------------------------------------------
40
+ Web UI assets (vendored, unmodified) — src/hwp2md/web/static/vendor/
41
+ Versions, source URLs and SHA-256: src/hwp2md/web/static/vendor/README.md
42
+
43
+ marked 12.0.2
44
+ Files: vendor/marked.min.js
45
+ License: MIT (vendor/marked.LICENSE.md)
46
+ Source: https://github.com/markedjs/marked
47
+
48
+ GSAP 3.13.0 (gsap, ScrollTrigger, SplitText, ScrambleTextPlugin)
49
+ Files: vendor/gsap/gsap.min.js, ScrollTrigger.min.js, SplitText.min.js,
50
+ ScrambleTextPlugin.min.js
51
+ License: GSAP Standard "no charge" license (vendor/gsap/LICENSE.md)
52
+ License URL: https://gsap.com/standard-license
53
+ Source: https://www.npmjs.com/package/gsap
54
+
55
+ Pretendard 1.3.9 (Pretendard Variable)
56
+ Files: vendor/fonts/PretendardVariable.woff2
57
+ License: SIL Open Font License 1.1 (vendor/fonts/OFL-Pretendard.txt)
58
+ Source: https://github.com/orioncactus/pretendard
59
+
60
+ JetBrains Mono 2.304 (Regular, Medium, SemiBold)
61
+ Files: vendor/fonts/JetBrainsMono-Regular.woff2, JetBrainsMono-Medium.woff2,
62
+ JetBrainsMono-SemiBold.woff2
63
+ License: SIL Open Font License 1.1 (vendor/fonts/OFL-JetBrainsMono.txt)
64
+ Source: https://github.com/JetBrains/JetBrainsMono
hwp2md/__init__.py ADDED
@@ -0,0 +1,44 @@
1
+ """hwp2md — HWP 5.0·HWPX 문서를 Markdown 으로 변환한다.
2
+
3
+ 본 제품은 한글과컴퓨터의 한/글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다.
4
+ """
5
+
6
+ from .blocks import Block, Image, Link, ListInfo, Math, Note, NoteRef, Paragraph, Table
7
+ from .convert import ConvertResult, convert
8
+ from .detect import DocFormat, detect_format
9
+ from .errors import (
10
+ DrmProtectedError,
11
+ EncryptedDocumentError,
12
+ Hwp2mdError,
13
+ NotHwpFileError,
14
+ UnsupportedHwpError,
15
+ )
16
+ from .memory import MemoryResult, convert_bytes
17
+ from .skips import SkipCounter
18
+
19
+ __version__ = "0.1.0"
20
+
21
+ __all__ = [
22
+ "Block",
23
+ "ConvertResult",
24
+ "DocFormat",
25
+ "DrmProtectedError",
26
+ "EncryptedDocumentError",
27
+ "Hwp2mdError",
28
+ "Image",
29
+ "Link",
30
+ "ListInfo",
31
+ "Math",
32
+ "MemoryResult",
33
+ "NotHwpFileError",
34
+ "Note",
35
+ "NoteRef",
36
+ "Paragraph",
37
+ "SkipCounter",
38
+ "Table",
39
+ "UnsupportedHwpError",
40
+ "__version__",
41
+ "convert",
42
+ "convert_bytes",
43
+ "detect_format",
44
+ ]