python-hwpx 2.8__tar.gz → 2.8.3__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.
Files changed (67) hide show
  1. {python_hwpx-2.8/src/python_hwpx.egg-info → python_hwpx-2.8.3}/PKG-INFO +34 -63
  2. {python_hwpx-2.8 → python_hwpx-2.8.3}/README.md +31 -28
  3. {python_hwpx-2.8 → python_hwpx-2.8.3}/pyproject.toml +9 -4
  4. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/oxml/document.py +29 -16
  5. {python_hwpx-2.8 → python_hwpx-2.8.3/src/python_hwpx.egg-info}/PKG-INFO +34 -63
  6. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/python_hwpx.egg-info/SOURCES.txt +1 -0
  7. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/python_hwpx.egg-info/requires.txt +1 -0
  8. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_memo_and_style_editing.py +18 -0
  9. python_hwpx-2.8.3/tests/test_packaging_license_metadata.py +63 -0
  10. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_section_headers.py +17 -0
  11. {python_hwpx-2.8 → python_hwpx-2.8.3}/LICENSE +0 -0
  12. {python_hwpx-2.8 → python_hwpx-2.8.3}/setup.cfg +0 -0
  13. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/__init__.py +0 -0
  14. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/data/Skeleton.hwpx +0 -0
  15. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/document.py +0 -0
  16. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/opc/package.py +0 -0
  17. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/opc/relationships.py +0 -0
  18. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/opc/xml_utils.py +0 -0
  19. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/oxml/__init__.py +0 -0
  20. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/oxml/body.py +0 -0
  21. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/oxml/common.py +0 -0
  22. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/oxml/header.py +0 -0
  23. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/oxml/header_part.py +0 -0
  24. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/oxml/memo.py +0 -0
  25. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/oxml/namespaces.py +0 -0
  26. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/oxml/paragraph.py +0 -0
  27. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/oxml/parser.py +0 -0
  28. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/oxml/schema.py +0 -0
  29. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/oxml/section.py +0 -0
  30. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/oxml/table.py +0 -0
  31. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/oxml/utils.py +0 -0
  32. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/package.py +0 -0
  33. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/py.typed +0 -0
  34. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/templates.py +0 -0
  35. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/tools/__init__.py +0 -0
  36. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/tools/_schemas/header.xsd +0 -0
  37. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/tools/_schemas/section.xsd +0 -0
  38. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/tools/archive_cli.py +0 -0
  39. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/tools/exporter.py +0 -0
  40. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/tools/object_finder.py +0 -0
  41. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/tools/package_validator.py +0 -0
  42. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/tools/page_guard.py +0 -0
  43. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/tools/template_analyzer.py +0 -0
  44. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/tools/text_extract_cli.py +0 -0
  45. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/tools/text_extractor.py +0 -0
  46. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/hwpx/tools/validator.py +0 -0
  47. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  48. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  49. {python_hwpx-2.8 → python_hwpx-2.8.3}/src/python_hwpx.egg-info/top_level.txt +0 -0
  50. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_coverage_targets.py +0 -0
  51. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_document_context_manager.py +0 -0
  52. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_document_formatting.py +0 -0
  53. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_document_save_api.py +0 -0
  54. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_gap_closure_tools.py +0 -0
  55. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_inline_models.py +0 -0
  56. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_integration_hwpx_compatibility.py +0 -0
  57. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_integration_roundtrip.py +0 -0
  58. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_new_features.py +0 -0
  59. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_opc_package.py +0 -0
  60. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_oxml_parsing.py +0 -0
  61. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_packaging_py_typed.py +0 -0
  62. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_paragraph_section_management.py +0 -0
  63. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_repr_snapshots.py +0 -0
  64. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_split_merged_cell.py +0 -0
  65. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_tables_default_border.py +0 -0
  66. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_text_extractor_annotations.py +0 -0
  67. {python_hwpx-2.8 → python_hwpx-2.8.3}/tests/test_version_metadata.py +0 -0
@@ -1,48 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 2.8
3
+ Version: 2.8.3
4
4
  Summary: Hancom HWPX 패키지를 로드하고 편집하기 위한 Python 유틸리티 모음
5
5
  Author: python-hwpx Maintainers
6
- License: Non-Commercial License
7
-
8
- Copyright (c) 2024 python-hwpx Maintainers
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to use,
12
- copy, modify, merge, publish, distribute, and sublicense the Software only for
13
- non-commercial purposes, subject to the following conditions:
14
-
15
- 1. Non-Commercial Use Only. The Software may be used, copied, modified,
16
- merged, published, distributed, and sublicensed only for non-commercial
17
- purposes. "Non-Commercial" means use that is not primarily intended for or
18
- directed toward commercial advantage, monetary compensation, or any form of
19
- direct or indirect commercial exploitation.
20
-
21
- 2. Attribution. The above copyright notice and this permission notice shall be
22
- included in all copies or substantial portions of the Software.
23
-
24
- 3. No Warranty of Commercial Support. The maintainers are not obligated to
25
- provide commercial support, maintenance, or updates.
26
-
27
- THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
- FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. IN NO EVENT SHALL THE
30
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33
- SOFTWARE.
34
-
35
- If you require permissions to use this Software for commercial purposes,
36
- please contact the copyright holders to negotiate an alternative licensing
37
- arrangement.
38
-
6
+ License-Expression: LicenseRef-python-hwpx-NonCommercial
39
7
  Project-URL: Homepage, https://github.com/airmang/python-hwpx
40
8
  Project-URL: Documentation, https://github.com/airmang/python-hwpx/tree/main/docs
41
9
  Project-URL: Issues, https://github.com/airmang/python-hwpx/issues
42
10
  Keywords: hwp,hwpx,hancom,opc,xml
43
11
  Classifier: Development Status :: 3 - Alpha
44
12
  Classifier: Intended Audience :: Developers
45
- Classifier: License :: OSI Approved :: MIT License
46
13
  Classifier: Programming Language :: Python :: 3
47
14
  Classifier: Programming Language :: Python :: 3.10
48
15
  Classifier: Programming Language :: Python :: 3.11
@@ -58,6 +25,7 @@ Requires-Dist: build>=1.0; extra == "dev"
58
25
  Requires-Dist: twine>=4.0; extra == "dev"
59
26
  Requires-Dist: pytest>=7.4; extra == "dev"
60
27
  Provides-Extra: test
28
+ Requires-Dist: build>=1.0; extra == "test"
61
29
  Requires-Dist: pytest>=7.4; extra == "test"
62
30
  Requires-Dist: pytest-cov>=5.0; extra == "test"
63
31
  Provides-Extra: typecheck
@@ -73,15 +41,16 @@ Dynamic: license-file
73
41
  <p align="center">
74
42
  <a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/v/python-hwpx?color=blue&label=PyPI" alt="PyPI"></a>
75
43
  <a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/pyversions/python-hwpx" alt="Python"></a>
76
- <a href="https://github.com/airmang/python-hwpx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License"></a>
44
+ <a href="https://github.com/airmang/python-hwpx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Custom%20Noncommercial-orange" alt="License: Custom Non-Commercial"></a>
77
45
  <a href="https://airmang.github.io/python-hwpx/"><img src="https://img.shields.io/badge/docs-Sphinx-8CA1AF" alt="Docs"></a>
78
46
  </p>
79
47
  </p>
80
48
 
81
49
  ---
82
50
 
83
- `python-hwpx`는 한컴오피스의 [HWPX 포맷](https://www.hancom.com/)을 순수 Python으로 다루는 라이브러리입니다.
51
+ `python-hwpx`는 한컴오피스의 [HWPX 포맷](https://www.hancom.com/)을 순수 Python으로 다루는 라이브러리이자 CLI 도구 모음입니다.
84
52
  한/글 설치 없이, OS에 관계없이 HWPX 문서의 구조를 파싱하고 콘텐츠를 조작할 수 있습니다.
53
+ 문서 편집 API뿐 아니라 스키마/패키지 검증, unpack/pack, 템플릿 분석 같은 XML-first 워크플로도 함께 제공합니다.
85
54
 
86
55
  > **pyhwpx / pyhwp와 다른 점?**
87
56
  > | | python-hwpx | pyhwpx | pyhwp |
@@ -93,7 +62,7 @@ Dynamic: license-file
93
62
 
94
63
  ## 🌍 크로스 플랫폼 지원
95
64
 
96
- HWPX 파일은 **ZIP + XML** 구조이므로, 한/글 프로그램 없이 Python만으로 완벽하게 읽고 수 있습니다.
65
+ HWPX 파일은 **ZIP + XML** 구조이므로, 한/글 프로그램 없이 Python만으로 읽고 편집하는 워크플로를 구성할 수 있습니다.
97
66
 
98
67
  | 플랫폼 | 읽기 | 쓰기 | 비고 |
99
68
  |--------|------|------|------|
@@ -115,14 +84,14 @@ pip install python-hwpx
115
84
  ```python
116
85
  from hwpx import HwpxDocument
117
86
 
118
- # 기존 문서 열기
119
- doc = HwpxDocument.open("보고서.hwpx")
120
-
121
87
  # 빈 문서 새로 만들기
122
88
  doc = HwpxDocument.new()
123
89
 
90
+ # 기존 문서를 수정하려면:
91
+ # doc = HwpxDocument.open("보고서.hwpx")
92
+
124
93
  # 문단 추가
125
- doc.add_paragraph("python-hwpx로 생성한 문단입니다.")
94
+ paragraph = doc.add_paragraph("python-hwpx로 생성한 문단입니다.")
126
95
 
127
96
  # 표 추가 (2×3)
128
97
  table = doc.add_table(rows=2, cols=3)
@@ -130,9 +99,8 @@ table.set_cell_text(0, 0, "이름")
130
99
  table.set_cell_text(0, 1, "부서")
131
100
  table.set_cell_text(0, 2, "연락처")
132
101
 
133
- # 메모 추가 (한/글에서 바로 표시)
134
- paragraph = doc.paragraphs[0]
135
- doc.add_memo_with_anchor("검토 필요", paragraph=paragraph)
102
+ # 메모 추가 (기본 템플릿의 memo shape 사용)
103
+ doc.add_memo_with_anchor("검토 필요", paragraph=paragraph, memo_shape_id_ref="0")
136
104
 
137
105
  # 저장
138
106
  doc.save_to_path("결과물.hwpx")
@@ -166,7 +134,7 @@ doc.save_to_path("결과물.hwpx")
166
134
  | 🎨 **스타일 치환** | 서식 기반 필터 | 색상/밑줄/charPrIDRef 기반 Run 검색 및 교체 |
167
135
  | 📤 **내보내기** | 텍스트/HTML/Markdown | 문서 변환 출력 |
168
136
  | ✅ **유효성 검사** | XSD + 패키지 구조 | CLI(`hwpx-validate`, `hwpx-validate-package`) 및 API |
169
- | 🧰 **워크플로 도구** | unpack/pack/template analyze/page guard | 템플릿 보존형 XML-first 작업 보조 |
137
+ | 🧰 **작업 도구** | unpack/pack/분석/비교 | pack-ready 작업 디렉터리 추출과 재구성 점검 |
170
138
  | 🏗️ **저수준 XML** | 데이터클래스 매핑 | OWPML 스키마 ↔ Python 객체 직접 조작 |
171
139
  | 🔄 **네임스페이스 호환** | 자동 정규화 | HWPML 2016 → 2011 자동 변환 |
172
140
 
@@ -174,7 +142,7 @@ doc.save_to_path("결과물.hwpx")
174
142
 
175
143
  ### 📄 문서 편집
176
144
 
177
- 문단, 표, 메모, 머리말/꼬리말을 Python 객체로 다룹니다.
145
+ 문단, 표, 메모, 머리글/바닥글을 Python 객체로 다룹니다.
178
146
 
179
147
  ```python
180
148
  # 단락 추가·삭제
@@ -186,9 +154,9 @@ new_sec = doc.add_section() # 문서 끝에 섹션 추가
186
154
  new_sec.add_paragraph("두 번째 섹션 내용")
187
155
  doc.remove_section(1) # 인덱스로 섹션 삭제
188
156
 
189
- # 머리말·꼬리말
157
+ # 머리글·바닥글
190
158
  doc.set_header_text("기밀 문서", page_type="BOTH")
191
- doc.set_footer_text("1 ", page_type="BOTH")
159
+ doc.set_footer_text("1 / 10", page_type="BOTH")
192
160
 
193
161
  # 표 셀 병합·분할
194
162
  table.merge_cells(0, 0, 1, 1) # (0,0)~(1,1) 병합
@@ -201,13 +169,14 @@ table.set_cell_text(0, 0, "병합된 셀", logical=True, split_merged=True)
201
169
  from hwpx import TextExtractor, ObjectFinder
202
170
 
203
171
  # 텍스트 추출
204
- for section in TextExtractor("문서.hwpx"):
205
- for para in section.paragraphs:
206
- print(para.text)
172
+ with TextExtractor("문서.hwpx") as extractor:
173
+ for section in extractor.iter_sections():
174
+ for para in extractor.iter_paragraphs(section):
175
+ print(para.text())
207
176
 
208
177
  # 특정 객체 탐색
209
- for obj in ObjectFinder("문서.hwpx").find("tbl"):
210
- print(obj.tag, obj.attributes)
178
+ for obj in ObjectFinder("문서.hwpx").find_all(tag="tbl"):
179
+ print(obj.tag, obj.path)
211
180
  ```
212
181
 
213
182
  ### 🎨 스타일 기반 텍스트 치환
@@ -270,7 +239,7 @@ python-hwpx
270
239
  │ ├── exporter # 텍스트/HTML/Markdown 내보내기
271
240
  │ ├── validator # 스키마 유효성 검사 (hwpx-validate CLI)
272
241
  │ ├── package_validator# ZIP/OPC/HWPX 구조 검사
273
- │ ├── page_guard # layout-drift proxy
242
+ │ ├── page_guard # 구조 변화 징후 점검
274
243
  │ └── template_analyzer# 레퍼런스 문서 분석/추출
275
244
  └── hwpx.templates # 내장 빈 문서 템플릿
276
245
  ```
@@ -289,7 +258,7 @@ hwpx-unpack 문서.hwpx ./unpacked
289
258
  hwpx-unpack 문서.hwpx ./pretty-unpacked --pretty-xml
290
259
  hwpx-pack ./unpacked ./repacked.hwpx
291
260
 
292
- # 레퍼런스 템플릿 분석과 pack-ready 추출
261
+ # 레퍼런스 문서 분석과 작업 디렉터리 추출
293
262
  hwpx-analyze-template 문서.hwpx --extract-dir ./template-parts --json
294
263
  hwpx-pack ./template-parts ./template-roundtrip.hwpx
295
264
  hwpx-validate-package ./template-roundtrip.hwpx
@@ -297,15 +266,15 @@ hwpx-validate-package ./template-roundtrip.hwpx
297
266
  # plain / markdown 텍스트 추출
298
267
  hwpx-text-extract 문서.hwpx --format markdown --output 문서.md
299
268
 
300
- # 레이아웃 드리프트 프록시 비교
269
+ # 문서 구조 변화 징후 비교
301
270
  hwpx-page-guard --reference 원본.hwpx --output 결과.hwpx
302
271
  ```
303
272
 
304
- `hwpx-page-guard`는 렌더된 실제 쪽수를 계산하지 않습니다. 대신 단락 수, 표 수, shape/control 수, 명시적 page/column break, 텍스트 길이 통계를 비교해 레이아웃 드리프트 위험을 탐지하는 프록시 도구입니다.
273
+ `hwpx-page-guard`는 렌더된 실제 쪽수를 계산하지 않습니다. 대신 단락 수, 표 수, shape/control 수, 명시적 page/column break, 텍스트 길이 같은 구조 지표를 비교해 편집 전후 변화 징후를 빠르게 점검합니다.
305
274
 
306
- `hwpx-validate-package`는 `Contents/content.hpf` 같은 고정 경로를 강제하지 않고, `META-INF/container.xml`과 선택된 rootfile/manifest 관계를 따라가며 검사합니다. 엔진이 fallback으로 열 수 있는 비표준 패키지는 가능한 경우 경고로 구분합니다.
275
+ `hwpx-validate-package`는 `Contents/content.hpf` 같은 고정 경로를 전제로 두지 않고, `META-INF/container.xml`과 실제 rootfile/manifest 선언을 따라가며 패키지 구조를 확인합니다. 엔진이 열 수 있는 비표준 패키지는 가능한 경우 경고로 분리해 보여줍니다.
307
276
 
308
- `hwpx-analyze-template --extract-dir`는 covered fixture 기준으로 `hwpx-pack`과 `hwpx-validate-package`, 그리고 엔진 open 경로에 다시 투입할 있는 pack-ready 작업 디렉터리를 만듭니다. 이건 재패킹 가능성을 목표로 한 것이지, 렌더링 fidelity를 보장한다는 뜻은 아닙니다.
277
+ `hwpx-analyze-template --extract-dir`는 다시 묶고 점검하기 쉬운 작업 디렉터리를 만듭니다. 재구성과 구조 검증에 필요한 파일을 함께 꺼내는 용도이며, 편집기에서의 최종 렌더링 결과까지 보장한다는 뜻은 아닙니다.
309
278
 
310
279
  ## 문서
311
280
 
@@ -352,13 +321,15 @@ pytest
352
321
 
353
322
  ## License
354
323
 
355
- [MIT](LICENSE) © 고규현 (Kyuhyun Koh)
324
+ [Custom Non-Commercial License](LICENSE) © python-hwpx Maintainers
325
+
326
+ Commercial use requires separate permission from the copyright holders.
356
327
 
357
328
  <br>
358
329
 
359
- ## Author
330
+ ## Maintainer
360
331
 
361
- **고규현** — 광교고등학교 정보·컴퓨터 교사
332
+ Primary maintainer/contact: **고규현** — 광교고등학교 정보·컴퓨터 교사
362
333
 
363
334
  - ✉️ [kokyuhyun@hotmail.com](mailto:kokyuhyun@hotmail.com)
364
335
  - 🐙 [@airmang](https://github.com/airmang)
@@ -6,15 +6,16 @@
6
6
  <p align="center">
7
7
  <a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/v/python-hwpx?color=blue&label=PyPI" alt="PyPI"></a>
8
8
  <a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/pyversions/python-hwpx" alt="Python"></a>
9
- <a href="https://github.com/airmang/python-hwpx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License"></a>
9
+ <a href="https://github.com/airmang/python-hwpx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Custom%20Noncommercial-orange" alt="License: Custom Non-Commercial"></a>
10
10
  <a href="https://airmang.github.io/python-hwpx/"><img src="https://img.shields.io/badge/docs-Sphinx-8CA1AF" alt="Docs"></a>
11
11
  </p>
12
12
  </p>
13
13
 
14
14
  ---
15
15
 
16
- `python-hwpx`는 한컴오피스의 [HWPX 포맷](https://www.hancom.com/)을 순수 Python으로 다루는 라이브러리입니다.
16
+ `python-hwpx`는 한컴오피스의 [HWPX 포맷](https://www.hancom.com/)을 순수 Python으로 다루는 라이브러리이자 CLI 도구 모음입니다.
17
17
  한/글 설치 없이, OS에 관계없이 HWPX 문서의 구조를 파싱하고 콘텐츠를 조작할 수 있습니다.
18
+ 문서 편집 API뿐 아니라 스키마/패키지 검증, unpack/pack, 템플릿 분석 같은 XML-first 워크플로도 함께 제공합니다.
18
19
 
19
20
  > **pyhwpx / pyhwp와 다른 점?**
20
21
  > | | python-hwpx | pyhwpx | pyhwp |
@@ -26,7 +27,7 @@
26
27
 
27
28
  ## 🌍 크로스 플랫폼 지원
28
29
 
29
- HWPX 파일은 **ZIP + XML** 구조이므로, 한/글 프로그램 없이 Python만으로 완벽하게 읽고 수 있습니다.
30
+ HWPX 파일은 **ZIP + XML** 구조이므로, 한/글 프로그램 없이 Python만으로 읽고 편집하는 워크플로를 구성할 수 있습니다.
30
31
 
31
32
  | 플랫폼 | 읽기 | 쓰기 | 비고 |
32
33
  |--------|------|------|------|
@@ -48,14 +49,14 @@ pip install python-hwpx
48
49
  ```python
49
50
  from hwpx import HwpxDocument
50
51
 
51
- # 기존 문서 열기
52
- doc = HwpxDocument.open("보고서.hwpx")
53
-
54
52
  # 빈 문서 새로 만들기
55
53
  doc = HwpxDocument.new()
56
54
 
55
+ # 기존 문서를 수정하려면:
56
+ # doc = HwpxDocument.open("보고서.hwpx")
57
+
57
58
  # 문단 추가
58
- doc.add_paragraph("python-hwpx로 생성한 문단입니다.")
59
+ paragraph = doc.add_paragraph("python-hwpx로 생성한 문단입니다.")
59
60
 
60
61
  # 표 추가 (2×3)
61
62
  table = doc.add_table(rows=2, cols=3)
@@ -63,9 +64,8 @@ table.set_cell_text(0, 0, "이름")
63
64
  table.set_cell_text(0, 1, "부서")
64
65
  table.set_cell_text(0, 2, "연락처")
65
66
 
66
- # 메모 추가 (한/글에서 바로 표시)
67
- paragraph = doc.paragraphs[0]
68
- doc.add_memo_with_anchor("검토 필요", paragraph=paragraph)
67
+ # 메모 추가 (기본 템플릿의 memo shape 사용)
68
+ doc.add_memo_with_anchor("검토 필요", paragraph=paragraph, memo_shape_id_ref="0")
69
69
 
70
70
  # 저장
71
71
  doc.save_to_path("결과물.hwpx")
@@ -99,7 +99,7 @@ doc.save_to_path("결과물.hwpx")
99
99
  | 🎨 **스타일 치환** | 서식 기반 필터 | 색상/밑줄/charPrIDRef 기반 Run 검색 및 교체 |
100
100
  | 📤 **내보내기** | 텍스트/HTML/Markdown | 문서 변환 출력 |
101
101
  | ✅ **유효성 검사** | XSD + 패키지 구조 | CLI(`hwpx-validate`, `hwpx-validate-package`) 및 API |
102
- | 🧰 **워크플로 도구** | unpack/pack/template analyze/page guard | 템플릿 보존형 XML-first 작업 보조 |
102
+ | 🧰 **작업 도구** | unpack/pack/분석/비교 | pack-ready 작업 디렉터리 추출과 재구성 점검 |
103
103
  | 🏗️ **저수준 XML** | 데이터클래스 매핑 | OWPML 스키마 ↔ Python 객체 직접 조작 |
104
104
  | 🔄 **네임스페이스 호환** | 자동 정규화 | HWPML 2016 → 2011 자동 변환 |
105
105
 
@@ -107,7 +107,7 @@ doc.save_to_path("결과물.hwpx")
107
107
 
108
108
  ### 📄 문서 편집
109
109
 
110
- 문단, 표, 메모, 머리말/꼬리말을 Python 객체로 다룹니다.
110
+ 문단, 표, 메모, 머리글/바닥글을 Python 객체로 다룹니다.
111
111
 
112
112
  ```python
113
113
  # 단락 추가·삭제
@@ -119,9 +119,9 @@ new_sec = doc.add_section() # 문서 끝에 섹션 추가
119
119
  new_sec.add_paragraph("두 번째 섹션 내용")
120
120
  doc.remove_section(1) # 인덱스로 섹션 삭제
121
121
 
122
- # 머리말·꼬리말
122
+ # 머리글·바닥글
123
123
  doc.set_header_text("기밀 문서", page_type="BOTH")
124
- doc.set_footer_text("1 ", page_type="BOTH")
124
+ doc.set_footer_text("1 / 10", page_type="BOTH")
125
125
 
126
126
  # 표 셀 병합·분할
127
127
  table.merge_cells(0, 0, 1, 1) # (0,0)~(1,1) 병합
@@ -134,13 +134,14 @@ table.set_cell_text(0, 0, "병합된 셀", logical=True, split_merged=True)
134
134
  from hwpx import TextExtractor, ObjectFinder
135
135
 
136
136
  # 텍스트 추출
137
- for section in TextExtractor("문서.hwpx"):
138
- for para in section.paragraphs:
139
- print(para.text)
137
+ with TextExtractor("문서.hwpx") as extractor:
138
+ for section in extractor.iter_sections():
139
+ for para in extractor.iter_paragraphs(section):
140
+ print(para.text())
140
141
 
141
142
  # 특정 객체 탐색
142
- for obj in ObjectFinder("문서.hwpx").find("tbl"):
143
- print(obj.tag, obj.attributes)
143
+ for obj in ObjectFinder("문서.hwpx").find_all(tag="tbl"):
144
+ print(obj.tag, obj.path)
144
145
  ```
145
146
 
146
147
  ### 🎨 스타일 기반 텍스트 치환
@@ -203,7 +204,7 @@ python-hwpx
203
204
  │ ├── exporter # 텍스트/HTML/Markdown 내보내기
204
205
  │ ├── validator # 스키마 유효성 검사 (hwpx-validate CLI)
205
206
  │ ├── package_validator# ZIP/OPC/HWPX 구조 검사
206
- │ ├── page_guard # layout-drift proxy
207
+ │ ├── page_guard # 구조 변화 징후 점검
207
208
  │ └── template_analyzer# 레퍼런스 문서 분석/추출
208
209
  └── hwpx.templates # 내장 빈 문서 템플릿
209
210
  ```
@@ -222,7 +223,7 @@ hwpx-unpack 문서.hwpx ./unpacked
222
223
  hwpx-unpack 문서.hwpx ./pretty-unpacked --pretty-xml
223
224
  hwpx-pack ./unpacked ./repacked.hwpx
224
225
 
225
- # 레퍼런스 템플릿 분석과 pack-ready 추출
226
+ # 레퍼런스 문서 분석과 작업 디렉터리 추출
226
227
  hwpx-analyze-template 문서.hwpx --extract-dir ./template-parts --json
227
228
  hwpx-pack ./template-parts ./template-roundtrip.hwpx
228
229
  hwpx-validate-package ./template-roundtrip.hwpx
@@ -230,15 +231,15 @@ hwpx-validate-package ./template-roundtrip.hwpx
230
231
  # plain / markdown 텍스트 추출
231
232
  hwpx-text-extract 문서.hwpx --format markdown --output 문서.md
232
233
 
233
- # 레이아웃 드리프트 프록시 비교
234
+ # 문서 구조 변화 징후 비교
234
235
  hwpx-page-guard --reference 원본.hwpx --output 결과.hwpx
235
236
  ```
236
237
 
237
- `hwpx-page-guard`는 렌더된 실제 쪽수를 계산하지 않습니다. 대신 단락 수, 표 수, shape/control 수, 명시적 page/column break, 텍스트 길이 통계를 비교해 레이아웃 드리프트 위험을 탐지하는 프록시 도구입니다.
238
+ `hwpx-page-guard`는 렌더된 실제 쪽수를 계산하지 않습니다. 대신 단락 수, 표 수, shape/control 수, 명시적 page/column break, 텍스트 길이 같은 구조 지표를 비교해 편집 전후 변화 징후를 빠르게 점검합니다.
238
239
 
239
- `hwpx-validate-package`는 `Contents/content.hpf` 같은 고정 경로를 강제하지 않고, `META-INF/container.xml`과 선택된 rootfile/manifest 관계를 따라가며 검사합니다. 엔진이 fallback으로 열 수 있는 비표준 패키지는 가능한 경우 경고로 구분합니다.
240
+ `hwpx-validate-package`는 `Contents/content.hpf` 같은 고정 경로를 전제로 두지 않고, `META-INF/container.xml`과 실제 rootfile/manifest 선언을 따라가며 패키지 구조를 확인합니다. 엔진이 열 수 있는 비표준 패키지는 가능한 경우 경고로 분리해 보여줍니다.
240
241
 
241
- `hwpx-analyze-template --extract-dir`는 covered fixture 기준으로 `hwpx-pack`과 `hwpx-validate-package`, 그리고 엔진 open 경로에 다시 투입할 있는 pack-ready 작업 디렉터리를 만듭니다. 이건 재패킹 가능성을 목표로 한 것이지, 렌더링 fidelity를 보장한다는 뜻은 아닙니다.
242
+ `hwpx-analyze-template --extract-dir`는 다시 묶고 점검하기 쉬운 작업 디렉터리를 만듭니다. 재구성과 구조 검증에 필요한 파일을 함께 꺼내는 용도이며, 편집기에서의 최종 렌더링 결과까지 보장한다는 뜻은 아닙니다.
242
243
 
243
244
  ## 문서
244
245
 
@@ -285,13 +286,15 @@ pytest
285
286
 
286
287
  ## License
287
288
 
288
- [MIT](LICENSE) © 고규현 (Kyuhyun Koh)
289
+ [Custom Non-Commercial License](LICENSE) © python-hwpx Maintainers
290
+
291
+ Commercial use requires separate permission from the copyright holders.
289
292
 
290
293
  <br>
291
294
 
292
- ## Author
295
+ ## Maintainer
293
296
 
294
- **고규현** — 광교고등학교 정보·컴퓨터 교사
297
+ Primary maintainer/contact: **고규현** — 광교고등학교 정보·컴퓨터 교사
295
298
 
296
299
  - ✉️ [kokyuhyun@hotmail.com](mailto:kokyuhyun@hotmail.com)
297
300
  - 🐙 [@airmang](https://github.com/airmang)
@@ -1,13 +1,14 @@
1
1
  [build-system]
2
- requires = ["setuptools", "wheel"]
2
+ requires = ["setuptools>=77.0.0", "wheel"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-hwpx"
7
- version = "2.8"
7
+ version = "2.8.3"
8
8
  description = "Hancom HWPX 패키지를 로드하고 편집하기 위한 Python 유틸리티 모음"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
- license = { file = "LICENSE" }
10
+ license = "LicenseRef-python-hwpx-NonCommercial"
11
+ license-files = ["LICENSE"]
11
12
  requires-python = ">=3.10"
12
13
  authors = [
13
14
  { name = "python-hwpx Maintainers" },
@@ -16,7 +17,6 @@ keywords = ["hwp", "hwpx", "hancom", "opc", "xml"]
16
17
  classifiers = [
17
18
  "Development Status :: 3 - Alpha",
18
19
  "Intended Audience :: Developers",
19
- "License :: OSI Approved :: MIT License",
20
20
  "Programming Language :: Python :: 3",
21
21
  "Programming Language :: Python :: 3.10",
22
22
  "Programming Language :: Python :: 3.11",
@@ -35,6 +35,7 @@ dev = [
35
35
  "pytest>=7.4",
36
36
  ]
37
37
  test = [
38
+ "build>=1.0",
38
39
  "pytest>=7.4",
39
40
  "pytest-cov>=5.0",
40
41
  ]
@@ -89,6 +90,8 @@ files = [
89
90
  "src/hwpx/tools/template_analyzer.py",
90
91
  "src/hwpx/tools/text_extract_cli.py",
91
92
  "src/hwpx/tools/text_extractor.py",
93
+ "tests/template_automation/helpers.py",
94
+ "tests/template_automation/generate_fixtures.py",
92
95
  ]
93
96
  ignore_missing_imports = true
94
97
 
@@ -108,6 +111,8 @@ include = [
108
111
  "src/hwpx/tools/template_analyzer.py",
109
112
  "src/hwpx/tools/text_extract_cli.py",
110
113
  "src/hwpx/tools/text_extractor.py",
114
+ "tests/template_automation/helpers.py",
115
+ "tests/template_automation/generate_fixtures.py",
111
116
  ]
112
117
  pythonVersion = "3.10"
113
118
  typeCheckingMode = "basic"
@@ -47,6 +47,8 @@ logger = logging.getLogger(__name__)
47
47
 
48
48
  _HP_NS = "http://www.hancom.co.kr/hwpml/2011/paragraph"
49
49
  _HP = f"{{{_HP_NS}}}"
50
+ _HS_NS = "http://www.hancom.co.kr/hwpml/2011/section"
51
+ _HS = f"{{{_HS_NS}}}"
50
52
  _HH_NS = "http://www.hancom.co.kr/hwpml/2011/head"
51
53
  _HH = f"{{{_HH_NS}}}"
52
54
 
@@ -127,6 +129,7 @@ def _create_paragraph_element(
127
129
  style_id_ref: str | int | None = None,
128
130
  paragraph_attributes: Optional[dict[str, str]] = None,
129
131
  run_attributes: Optional[dict[str, str]] = None,
132
+ parent: ET.Element | None = None,
130
133
  ) -> ET.Element:
131
134
  """Return a paragraph element populated with a single run and text node."""
132
135
 
@@ -138,7 +141,10 @@ def _create_paragraph_element(
138
141
  if style_id_ref is not None:
139
142
  attrs["styleIDRef"] = str(style_id_ref)
140
143
 
141
- paragraph = ET.Element(f"{_HP}p", attrs)
144
+ if parent is None:
145
+ paragraph = ET.Element(f"{_HP}p", attrs)
146
+ else:
147
+ paragraph = parent.makeelement(f"{_HP}p", attrs)
142
148
 
143
149
  run_attrs: dict[str, str] = dict(run_attributes or {})
144
150
  if char_pr_id_ref is not None:
@@ -146,8 +152,10 @@ def _create_paragraph_element(
146
152
  else:
147
153
  run_attrs.setdefault("charPrIDRef", "0")
148
154
 
149
- run = ET.SubElement(paragraph, f"{_HP}run", run_attrs)
150
- text_element = ET.SubElement(run, f"{_HP}t")
155
+ run = paragraph.makeelement(f"{_HP}run", run_attrs)
156
+ paragraph.append(run)
157
+ text_element = run.makeelement(f"{_HP}t", {})
158
+ run.append(text_element)
151
159
  text_element.text = text
152
160
  return paragraph
153
161
 
@@ -535,18 +543,22 @@ class HwpxOxmlSectionHeaderFooter:
535
543
  def _ensure_text_element(self) -> ET.Element:
536
544
  sublist = self.element.find(f"{_HP}subList")
537
545
  if sublist is None:
538
- sublist = ET.SubElement(self.element, f"{_HP}subList", self._initial_sublist_attributes())
546
+ sublist = _append_child(
547
+ self.element,
548
+ f"{_HP}subList",
549
+ self._initial_sublist_attributes(),
550
+ )
539
551
  paragraph = sublist.find(f"{_HP}p")
540
552
  if paragraph is None:
541
553
  paragraph_attrs = dict(_DEFAULT_PARAGRAPH_ATTRS)
542
554
  paragraph_attrs["id"] = _paragraph_id()
543
- paragraph = ET.SubElement(sublist, f"{_HP}p", paragraph_attrs)
555
+ paragraph = _append_child(sublist, f"{_HP}p", paragraph_attrs)
544
556
  run = paragraph.find(f"{_HP}run")
545
557
  if run is None:
546
- run = ET.SubElement(paragraph, f"{_HP}run", {"charPrIDRef": "0"})
558
+ run = _append_child(paragraph, f"{_HP}run", {"charPrIDRef": "0"})
547
559
  text = run.find(f"{_HP}t")
548
560
  if text is None:
549
- text = ET.SubElement(run, f"{_HP}t")
561
+ text = _append_child(run, f"{_HP}t")
550
562
  return text
551
563
 
552
564
  @property
@@ -851,7 +863,7 @@ class HwpxOxmlSectionProperties:
851
863
  attrs = {"applyPageType": page_type}
852
864
  if header_id is not None:
853
865
  attrs[self._apply_id_attributes(tag)[0]] = header_id
854
- apply = ET.SubElement(self.element, f"{_HP}{tag}Apply", attrs)
866
+ apply = _append_child(self.element, f"{_HP}{tag}Apply", attrs)
855
867
  changed = True
856
868
  else:
857
869
  if apply.get("applyPageType") != page_type:
@@ -897,7 +909,7 @@ class HwpxOxmlSectionProperties:
897
909
  element = self._find_header_footer(tag, page_type)
898
910
  changed = False
899
911
  if element is None:
900
- element = ET.SubElement(
912
+ element = _append_child(
901
913
  self.element,
902
914
  f"{_HP}{tag}",
903
915
  {"id": _object_id(), "applyPageType": page_type},
@@ -1362,7 +1374,7 @@ class HwpxOxmlMemoGroup:
1362
1374
  memo_attrs.setdefault("id", memo_id or _memo_id())
1363
1375
  if memo_shape_id_ref is not None:
1364
1376
  memo_attrs.setdefault("memoShapeIDRef", str(memo_shape_id_ref))
1365
- memo_element = ET.SubElement(self.element, f"{_HP}memo", memo_attrs)
1377
+ memo_element = _append_child(self.element, f"{_HP}memo", memo_attrs)
1366
1378
  memo = HwpxOxmlMemo(memo_element, self)
1367
1379
  memo.set_text(text, char_pr_id_ref=char_pr_id_ref)
1368
1380
  self.section.mark_dirty()
@@ -1466,10 +1478,11 @@ class HwpxOxmlMemo:
1466
1478
  for child in list(self.element):
1467
1479
  if _element_local_name(child) in {"paraList", "p"}:
1468
1480
  self.element.remove(child)
1469
- para_list = ET.SubElement(self.element, f"{_HP}paraList")
1481
+ para_list = _append_child(self.element, f"{_HP}paraList", {})
1470
1482
  paragraph = _create_paragraph_element(
1471
1483
  desired,
1472
1484
  char_pr_id_ref=existing_char if existing_char is not None else "0",
1485
+ parent=para_list,
1473
1486
  )
1474
1487
  para_list.append(paragraph)
1475
1488
  self.group.section.mark_dirty()
@@ -3493,11 +3506,11 @@ class HwpxOxmlSection:
3493
3506
  if paragraph is None:
3494
3507
  paragraph_attrs = dict(_DEFAULT_PARAGRAPH_ATTRS)
3495
3508
  paragraph_attrs["id"] = _paragraph_id()
3496
- paragraph = ET.SubElement(self._element, f"{_HP}p", paragraph_attrs)
3509
+ paragraph = _append_child(self._element, f"{_HP}p", paragraph_attrs)
3497
3510
  run = paragraph.find(f"{_HP}run")
3498
3511
  if run is None:
3499
- run = ET.SubElement(paragraph, f"{_HP}run", {"charPrIDRef": "0"})
3500
- element = ET.SubElement(run, f"{_HP}secPr")
3512
+ run = _append_child(paragraph, f"{_HP}run", {"charPrIDRef": "0"})
3513
+ element = _append_child(run, f"{_HP}secPr")
3501
3514
  self._properties_cache = None
3502
3515
  self.mark_dirty()
3503
3516
  return element
@@ -3536,7 +3549,7 @@ class HwpxOxmlSection:
3536
3549
  def _memo_group_element(self, create: bool = False) -> ET.Element | None:
3537
3550
  element = self._element.find(f"{_HP}memogroup")
3538
3551
  if element is None and create:
3539
- element = ET.SubElement(self._element, f"{_HP}memogroup")
3552
+ element = _append_child(self._element, f"{_HP}memogroup", {})
3540
3553
  self.mark_dirty()
3541
3554
  return element
3542
3555
 
@@ -4660,7 +4673,7 @@ class HwpxOxmlDocument:
4660
4673
  part_name = f"Contents/{section_id}.xml"
4661
4674
 
4662
4675
  # Build minimal section XML
4663
- section_element = ET.Element(f"{_HP}sec")
4676
+ section_element = ET.Element(f"{_HS}sec")
4664
4677
  para_attrs = {"id": _paragraph_id(), **_DEFAULT_PARAGRAPH_ATTRS}
4665
4678
  para = ET.SubElement(section_element, f"{_HP}p", para_attrs)
4666
4679
  run = ET.SubElement(para, f"{_HP}run", {"charPrIDRef": "0"})
@@ -1,48 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 2.8
3
+ Version: 2.8.3
4
4
  Summary: Hancom HWPX 패키지를 로드하고 편집하기 위한 Python 유틸리티 모음
5
5
  Author: python-hwpx Maintainers
6
- License: Non-Commercial License
7
-
8
- Copyright (c) 2024 python-hwpx Maintainers
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to use,
12
- copy, modify, merge, publish, distribute, and sublicense the Software only for
13
- non-commercial purposes, subject to the following conditions:
14
-
15
- 1. Non-Commercial Use Only. The Software may be used, copied, modified,
16
- merged, published, distributed, and sublicensed only for non-commercial
17
- purposes. "Non-Commercial" means use that is not primarily intended for or
18
- directed toward commercial advantage, monetary compensation, or any form of
19
- direct or indirect commercial exploitation.
20
-
21
- 2. Attribution. The above copyright notice and this permission notice shall be
22
- included in all copies or substantial portions of the Software.
23
-
24
- 3. No Warranty of Commercial Support. The maintainers are not obligated to
25
- provide commercial support, maintenance, or updates.
26
-
27
- THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
- FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. IN NO EVENT SHALL THE
30
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33
- SOFTWARE.
34
-
35
- If you require permissions to use this Software for commercial purposes,
36
- please contact the copyright holders to negotiate an alternative licensing
37
- arrangement.
38
-
6
+ License-Expression: LicenseRef-python-hwpx-NonCommercial
39
7
  Project-URL: Homepage, https://github.com/airmang/python-hwpx
40
8
  Project-URL: Documentation, https://github.com/airmang/python-hwpx/tree/main/docs
41
9
  Project-URL: Issues, https://github.com/airmang/python-hwpx/issues
42
10
  Keywords: hwp,hwpx,hancom,opc,xml
43
11
  Classifier: Development Status :: 3 - Alpha
44
12
  Classifier: Intended Audience :: Developers
45
- Classifier: License :: OSI Approved :: MIT License
46
13
  Classifier: Programming Language :: Python :: 3
47
14
  Classifier: Programming Language :: Python :: 3.10
48
15
  Classifier: Programming Language :: Python :: 3.11
@@ -58,6 +25,7 @@ Requires-Dist: build>=1.0; extra == "dev"
58
25
  Requires-Dist: twine>=4.0; extra == "dev"
59
26
  Requires-Dist: pytest>=7.4; extra == "dev"
60
27
  Provides-Extra: test
28
+ Requires-Dist: build>=1.0; extra == "test"
61
29
  Requires-Dist: pytest>=7.4; extra == "test"
62
30
  Requires-Dist: pytest-cov>=5.0; extra == "test"
63
31
  Provides-Extra: typecheck
@@ -73,15 +41,16 @@ Dynamic: license-file
73
41
  <p align="center">
74
42
  <a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/v/python-hwpx?color=blue&label=PyPI" alt="PyPI"></a>
75
43
  <a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/pyversions/python-hwpx" alt="Python"></a>
76
- <a href="https://github.com/airmang/python-hwpx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License"></a>
44
+ <a href="https://github.com/airmang/python-hwpx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Custom%20Noncommercial-orange" alt="License: Custom Non-Commercial"></a>
77
45
  <a href="https://airmang.github.io/python-hwpx/"><img src="https://img.shields.io/badge/docs-Sphinx-8CA1AF" alt="Docs"></a>
78
46
  </p>
79
47
  </p>
80
48
 
81
49
  ---
82
50
 
83
- `python-hwpx`는 한컴오피스의 [HWPX 포맷](https://www.hancom.com/)을 순수 Python으로 다루는 라이브러리입니다.
51
+ `python-hwpx`는 한컴오피스의 [HWPX 포맷](https://www.hancom.com/)을 순수 Python으로 다루는 라이브러리이자 CLI 도구 모음입니다.
84
52
  한/글 설치 없이, OS에 관계없이 HWPX 문서의 구조를 파싱하고 콘텐츠를 조작할 수 있습니다.
53
+ 문서 편집 API뿐 아니라 스키마/패키지 검증, unpack/pack, 템플릿 분석 같은 XML-first 워크플로도 함께 제공합니다.
85
54
 
86
55
  > **pyhwpx / pyhwp와 다른 점?**
87
56
  > | | python-hwpx | pyhwpx | pyhwp |
@@ -93,7 +62,7 @@ Dynamic: license-file
93
62
 
94
63
  ## 🌍 크로스 플랫폼 지원
95
64
 
96
- HWPX 파일은 **ZIP + XML** 구조이므로, 한/글 프로그램 없이 Python만으로 완벽하게 읽고 수 있습니다.
65
+ HWPX 파일은 **ZIP + XML** 구조이므로, 한/글 프로그램 없이 Python만으로 읽고 편집하는 워크플로를 구성할 수 있습니다.
97
66
 
98
67
  | 플랫폼 | 읽기 | 쓰기 | 비고 |
99
68
  |--------|------|------|------|
@@ -115,14 +84,14 @@ pip install python-hwpx
115
84
  ```python
116
85
  from hwpx import HwpxDocument
117
86
 
118
- # 기존 문서 열기
119
- doc = HwpxDocument.open("보고서.hwpx")
120
-
121
87
  # 빈 문서 새로 만들기
122
88
  doc = HwpxDocument.new()
123
89
 
90
+ # 기존 문서를 수정하려면:
91
+ # doc = HwpxDocument.open("보고서.hwpx")
92
+
124
93
  # 문단 추가
125
- doc.add_paragraph("python-hwpx로 생성한 문단입니다.")
94
+ paragraph = doc.add_paragraph("python-hwpx로 생성한 문단입니다.")
126
95
 
127
96
  # 표 추가 (2×3)
128
97
  table = doc.add_table(rows=2, cols=3)
@@ -130,9 +99,8 @@ table.set_cell_text(0, 0, "이름")
130
99
  table.set_cell_text(0, 1, "부서")
131
100
  table.set_cell_text(0, 2, "연락처")
132
101
 
133
- # 메모 추가 (한/글에서 바로 표시)
134
- paragraph = doc.paragraphs[0]
135
- doc.add_memo_with_anchor("검토 필요", paragraph=paragraph)
102
+ # 메모 추가 (기본 템플릿의 memo shape 사용)
103
+ doc.add_memo_with_anchor("검토 필요", paragraph=paragraph, memo_shape_id_ref="0")
136
104
 
137
105
  # 저장
138
106
  doc.save_to_path("결과물.hwpx")
@@ -166,7 +134,7 @@ doc.save_to_path("결과물.hwpx")
166
134
  | 🎨 **스타일 치환** | 서식 기반 필터 | 색상/밑줄/charPrIDRef 기반 Run 검색 및 교체 |
167
135
  | 📤 **내보내기** | 텍스트/HTML/Markdown | 문서 변환 출력 |
168
136
  | ✅ **유효성 검사** | XSD + 패키지 구조 | CLI(`hwpx-validate`, `hwpx-validate-package`) 및 API |
169
- | 🧰 **워크플로 도구** | unpack/pack/template analyze/page guard | 템플릿 보존형 XML-first 작업 보조 |
137
+ | 🧰 **작업 도구** | unpack/pack/분석/비교 | pack-ready 작업 디렉터리 추출과 재구성 점검 |
170
138
  | 🏗️ **저수준 XML** | 데이터클래스 매핑 | OWPML 스키마 ↔ Python 객체 직접 조작 |
171
139
  | 🔄 **네임스페이스 호환** | 자동 정규화 | HWPML 2016 → 2011 자동 변환 |
172
140
 
@@ -174,7 +142,7 @@ doc.save_to_path("결과물.hwpx")
174
142
 
175
143
  ### 📄 문서 편집
176
144
 
177
- 문단, 표, 메모, 머리말/꼬리말을 Python 객체로 다룹니다.
145
+ 문단, 표, 메모, 머리글/바닥글을 Python 객체로 다룹니다.
178
146
 
179
147
  ```python
180
148
  # 단락 추가·삭제
@@ -186,9 +154,9 @@ new_sec = doc.add_section() # 문서 끝에 섹션 추가
186
154
  new_sec.add_paragraph("두 번째 섹션 내용")
187
155
  doc.remove_section(1) # 인덱스로 섹션 삭제
188
156
 
189
- # 머리말·꼬리말
157
+ # 머리글·바닥글
190
158
  doc.set_header_text("기밀 문서", page_type="BOTH")
191
- doc.set_footer_text("1 ", page_type="BOTH")
159
+ doc.set_footer_text("1 / 10", page_type="BOTH")
192
160
 
193
161
  # 표 셀 병합·분할
194
162
  table.merge_cells(0, 0, 1, 1) # (0,0)~(1,1) 병합
@@ -201,13 +169,14 @@ table.set_cell_text(0, 0, "병합된 셀", logical=True, split_merged=True)
201
169
  from hwpx import TextExtractor, ObjectFinder
202
170
 
203
171
  # 텍스트 추출
204
- for section in TextExtractor("문서.hwpx"):
205
- for para in section.paragraphs:
206
- print(para.text)
172
+ with TextExtractor("문서.hwpx") as extractor:
173
+ for section in extractor.iter_sections():
174
+ for para in extractor.iter_paragraphs(section):
175
+ print(para.text())
207
176
 
208
177
  # 특정 객체 탐색
209
- for obj in ObjectFinder("문서.hwpx").find("tbl"):
210
- print(obj.tag, obj.attributes)
178
+ for obj in ObjectFinder("문서.hwpx").find_all(tag="tbl"):
179
+ print(obj.tag, obj.path)
211
180
  ```
212
181
 
213
182
  ### 🎨 스타일 기반 텍스트 치환
@@ -270,7 +239,7 @@ python-hwpx
270
239
  │ ├── exporter # 텍스트/HTML/Markdown 내보내기
271
240
  │ ├── validator # 스키마 유효성 검사 (hwpx-validate CLI)
272
241
  │ ├── package_validator# ZIP/OPC/HWPX 구조 검사
273
- │ ├── page_guard # layout-drift proxy
242
+ │ ├── page_guard # 구조 변화 징후 점검
274
243
  │ └── template_analyzer# 레퍼런스 문서 분석/추출
275
244
  └── hwpx.templates # 내장 빈 문서 템플릿
276
245
  ```
@@ -289,7 +258,7 @@ hwpx-unpack 문서.hwpx ./unpacked
289
258
  hwpx-unpack 문서.hwpx ./pretty-unpacked --pretty-xml
290
259
  hwpx-pack ./unpacked ./repacked.hwpx
291
260
 
292
- # 레퍼런스 템플릿 분석과 pack-ready 추출
261
+ # 레퍼런스 문서 분석과 작업 디렉터리 추출
293
262
  hwpx-analyze-template 문서.hwpx --extract-dir ./template-parts --json
294
263
  hwpx-pack ./template-parts ./template-roundtrip.hwpx
295
264
  hwpx-validate-package ./template-roundtrip.hwpx
@@ -297,15 +266,15 @@ hwpx-validate-package ./template-roundtrip.hwpx
297
266
  # plain / markdown 텍스트 추출
298
267
  hwpx-text-extract 문서.hwpx --format markdown --output 문서.md
299
268
 
300
- # 레이아웃 드리프트 프록시 비교
269
+ # 문서 구조 변화 징후 비교
301
270
  hwpx-page-guard --reference 원본.hwpx --output 결과.hwpx
302
271
  ```
303
272
 
304
- `hwpx-page-guard`는 렌더된 실제 쪽수를 계산하지 않습니다. 대신 단락 수, 표 수, shape/control 수, 명시적 page/column break, 텍스트 길이 통계를 비교해 레이아웃 드리프트 위험을 탐지하는 프록시 도구입니다.
273
+ `hwpx-page-guard`는 렌더된 실제 쪽수를 계산하지 않습니다. 대신 단락 수, 표 수, shape/control 수, 명시적 page/column break, 텍스트 길이 같은 구조 지표를 비교해 편집 전후 변화 징후를 빠르게 점검합니다.
305
274
 
306
- `hwpx-validate-package`는 `Contents/content.hpf` 같은 고정 경로를 강제하지 않고, `META-INF/container.xml`과 선택된 rootfile/manifest 관계를 따라가며 검사합니다. 엔진이 fallback으로 열 수 있는 비표준 패키지는 가능한 경우 경고로 구분합니다.
275
+ `hwpx-validate-package`는 `Contents/content.hpf` 같은 고정 경로를 전제로 두지 않고, `META-INF/container.xml`과 실제 rootfile/manifest 선언을 따라가며 패키지 구조를 확인합니다. 엔진이 열 수 있는 비표준 패키지는 가능한 경우 경고로 분리해 보여줍니다.
307
276
 
308
- `hwpx-analyze-template --extract-dir`는 covered fixture 기준으로 `hwpx-pack`과 `hwpx-validate-package`, 그리고 엔진 open 경로에 다시 투입할 있는 pack-ready 작업 디렉터리를 만듭니다. 이건 재패킹 가능성을 목표로 한 것이지, 렌더링 fidelity를 보장한다는 뜻은 아닙니다.
277
+ `hwpx-analyze-template --extract-dir`는 다시 묶고 점검하기 쉬운 작업 디렉터리를 만듭니다. 재구성과 구조 검증에 필요한 파일을 함께 꺼내는 용도이며, 편집기에서의 최종 렌더링 결과까지 보장한다는 뜻은 아닙니다.
309
278
 
310
279
  ## 문서
311
280
 
@@ -352,13 +321,15 @@ pytest
352
321
 
353
322
  ## License
354
323
 
355
- [MIT](LICENSE) © 고규현 (Kyuhyun Koh)
324
+ [Custom Non-Commercial License](LICENSE) © python-hwpx Maintainers
325
+
326
+ Commercial use requires separate permission from the copyright holders.
356
327
 
357
328
  <br>
358
329
 
359
- ## Author
330
+ ## Maintainer
360
331
 
361
- **고규현** — 광교고등학교 정보·컴퓨터 교사
332
+ Primary maintainer/contact: **고규현** — 광교고등학교 정보·컴퓨터 교사
362
333
 
363
334
  - ✉️ [kokyuhyun@hotmail.com](mailto:kokyuhyun@hotmail.com)
364
335
  - 🐙 [@airmang](https://github.com/airmang)
@@ -54,6 +54,7 @@ tests/test_memo_and_style_editing.py
54
54
  tests/test_new_features.py
55
55
  tests/test_opc_package.py
56
56
  tests/test_oxml_parsing.py
57
+ tests/test_packaging_license_metadata.py
57
58
  tests/test_packaging_py_typed.py
58
59
  tests/test_paragraph_section_management.py
59
60
  tests/test_repr_snapshots.py
@@ -6,6 +6,7 @@ twine>=4.0
6
6
  pytest>=7.4
7
7
 
8
8
  [test]
9
+ build>=1.0
9
10
  pytest>=7.4
10
11
  pytest-cov>=5.0
11
12
 
@@ -238,6 +238,24 @@ def test_add_memo_with_anchor_creates_paragraph_when_missing() -> None:
238
238
  assert field_end.get("beginIDRef") == "field-02"
239
239
 
240
240
 
241
+ def test_add_memo_with_anchor_roundtrips_on_real_document() -> None:
242
+ document = HwpxDocument.new()
243
+ paragraph = document.add_paragraph("Quick start anchor")
244
+
245
+ memo, anchored, field_id = document.add_memo_with_anchor(
246
+ "Quick start memo",
247
+ paragraph=paragraph,
248
+ memo_shape_id_ref="0",
249
+ )
250
+
251
+ assert anchored is paragraph
252
+ assert memo.text == "Quick start memo"
253
+ assert field_id
254
+
255
+ reopened = HwpxDocument.open(document.to_bytes())
256
+ assert any(item.text == "Quick start memo" for item in reopened.memos)
257
+
258
+
241
259
  def test_document_ensure_run_style_creates_bold_entry() -> None:
242
260
  document, _, header = _build_document()
243
261
 
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ import sys
5
+ import tarfile
6
+ from pathlib import Path
7
+ from zipfile import ZipFile
8
+
9
+ import pytest
10
+
11
+
12
+ LICENSE_EXPRESSION = "LicenseRef-python-hwpx-NonCommercial"
13
+
14
+
15
+ def _build_distribution(tmp_path: Path, distribution: str) -> Path:
16
+ pytest.importorskip("build")
17
+
18
+ project_root = Path(__file__).resolve().parents[1]
19
+ build_args = [
20
+ sys.executable,
21
+ "-m",
22
+ "build",
23
+ f"--{distribution}",
24
+ "--outdir",
25
+ str(tmp_path),
26
+ ]
27
+ subprocess.run(build_args, cwd=project_root, check=True)
28
+
29
+ pattern = "*.whl" if distribution == "wheel" else "*.tar.gz"
30
+ return next(tmp_path.glob(pattern))
31
+
32
+
33
+ @pytest.mark.parametrize("distribution", ["wheel", "sdist"])
34
+ def test_built_distributions_expose_custom_license_metadata(
35
+ tmp_path: Path, distribution: str
36
+ ) -> None:
37
+ artifact = _build_distribution(tmp_path, distribution)
38
+
39
+ if distribution == "wheel":
40
+ with ZipFile(artifact) as wheel_archive:
41
+ members = set(wheel_archive.namelist())
42
+ metadata_name = next(
43
+ name for name in members if name.endswith(".dist-info/METADATA")
44
+ )
45
+ metadata = wheel_archive.read(metadata_name).decode("utf-8")
46
+
47
+ assert f"License-Expression: {LICENSE_EXPRESSION}" in metadata
48
+ assert "License-File: LICENSE" in metadata
49
+ assert "Classifier: License ::" not in metadata
50
+ assert any(name.endswith(".dist-info/licenses/LICENSE") for name in members)
51
+ return
52
+
53
+ with tarfile.open(artifact, "r:gz") as sdist_archive:
54
+ members = sdist_archive.getnames()
55
+ pkg_info_name = next(name for name in members if name.endswith("/PKG-INFO"))
56
+ pkg_info_member = sdist_archive.extractfile(pkg_info_name)
57
+ assert pkg_info_member is not None
58
+ metadata = pkg_info_member.read().decode("utf-8")
59
+
60
+ assert f"License-Expression: {LICENSE_EXPRESSION}" in metadata
61
+ assert "License-File: LICENSE" in metadata
62
+ assert "Classifier: License ::" not in metadata
63
+ assert any(name.endswith("/LICENSE") for name in members)
@@ -141,3 +141,20 @@ def test_document_helpers_manage_header_apply_nodes() -> None:
141
141
 
142
142
  document.remove_header(section=section)
143
143
  assert sec_pr.find(f"{HP}headerApply") is None
144
+
145
+
146
+ def test_header_footer_helpers_work_on_real_hwpx_document() -> None:
147
+ document = HwpxDocument.new()
148
+
149
+ document.set_header_text("Header {{HDR1}}")
150
+ document.set_footer_text("Footer {{FTR1}}")
151
+
152
+ reopened = HwpxDocument.open(document.to_bytes())
153
+
154
+ header = reopened.sections[0].properties.get_header()
155
+ footer = reopened.sections[0].properties.get_footer()
156
+
157
+ assert header is not None
158
+ assert footer is not None
159
+ assert header.text == "Header {{HDR1}}"
160
+ assert footer.text == "Footer {{FTR1}}"
File without changes
File without changes
File without changes