python-hwpx 3.8.0__py3-none-any.whl → 4.1.1__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.
- hwpx/__init__.py +98 -43
- hwpx/_document/persistence.py +27 -31
- hwpx/document.py +0 -25
- hwpx/errors.py +70 -0
- hwpx/experimental.py +48 -0
- hwpx/form_fit/wordbox.py +30 -4
- hwpx/mutation_report.py +22 -4
- hwpx/table_patch.py +19 -4
- python_hwpx-4.1.1.dist-info/METADATA +181 -0
- {python_hwpx-3.8.0.dist-info → python_hwpx-4.1.1.dist-info}/RECORD +15 -13
- python_hwpx-3.8.0.dist-info/METADATA +0 -344
- {python_hwpx-3.8.0.dist-info → python_hwpx-4.1.1.dist-info}/WHEEL +0 -0
- {python_hwpx-3.8.0.dist-info → python_hwpx-4.1.1.dist-info}/entry_points.txt +0 -0
- {python_hwpx-3.8.0.dist-info → python_hwpx-4.1.1.dist-info}/licenses/LICENSE +0 -0
- {python_hwpx-3.8.0.dist-info → python_hwpx-4.1.1.dist-info}/licenses/NOTICE +0 -0
- {python_hwpx-3.8.0.dist-info → python_hwpx-4.1.1.dist-info}/top_level.txt +0 -0
hwpx/__init__.py
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
"""High-level utilities for working with HWPX documents.
|
|
2
|
+
"""High-level utilities for working with HWPX documents.
|
|
3
3
|
|
|
4
|
+
최상위 ``from hwpx import ...`` 표면은 세 계층으로 나뉩니다(정책·전수 목록:
|
|
5
|
+
``docs/stable-api.md``):
|
|
6
|
+
|
|
7
|
+
- **stable** — ``__all__``에 있는 이름. 계약이 굳었고 major 경계에서만 깨질 수 있음.
|
|
8
|
+
- **experimental** — 계약이 유동적. ``from hwpx.experimental import ...``로 쓰세요.
|
|
9
|
+
최상위 재내보내기는 하위 호환을 위해 유지하되 접근 시 ``DeprecationWarning``이 나며
|
|
10
|
+
다음 major에서 제거될 예정입니다.
|
|
11
|
+
- **deprecated** — 대체 경로로 이전하세요. 접근 시 ``DeprecationWarning``이 납니다.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import importlib
|
|
15
|
+
import warnings
|
|
4
16
|
from importlib.metadata import PackageNotFoundError, version as _metadata_version
|
|
5
17
|
|
|
6
18
|
|
|
@@ -11,13 +23,94 @@ def _resolve_version() -> str:
|
|
|
11
23
|
except PackageNotFoundError:
|
|
12
24
|
return "0+unknown"
|
|
13
25
|
|
|
26
|
+
|
|
27
|
+
# --- experimental / deprecated 최상위 표면 (지연 접근, 접근 시 경고) ---------------
|
|
28
|
+
#
|
|
29
|
+
# stable 이름은 아래에서 eager import 되어 모듈 전역에 존재하므로 ``__getattr__``이
|
|
30
|
+
# 호출되지 않습니다(=경고 없음). 여기 등록된 이름만 지연 해석되어 경고를 냅니다.
|
|
31
|
+
# 4.0.0에서 제거되는 이름은 0개 — 모두 계속 import 가능합니다.
|
|
32
|
+
|
|
33
|
+
_EXPERIMENTAL_EXPORTS = {
|
|
34
|
+
# 문서 ingestion 프레임워크(임의 포맷 -> HWPX). 계약 유동.
|
|
35
|
+
"ConversionAttempt": "hwpx.ingest",
|
|
36
|
+
"DocumentConverter": "hwpx.ingest",
|
|
37
|
+
"DocumentIngestError": "hwpx.ingest",
|
|
38
|
+
"DocumentIngestResult": "hwpx.ingest",
|
|
39
|
+
"DocumentIngestor": "hwpx.ingest",
|
|
40
|
+
"DocumentSourceInfo": "hwpx.ingest",
|
|
41
|
+
"UnsupportedDocumentFormat": "hwpx.ingest",
|
|
42
|
+
# 레이아웃 프리뷰(한컴 없는 정직 근사). 계약 유동.
|
|
43
|
+
"LayoutPreview": "hwpx.tools.layout_preview",
|
|
44
|
+
"PreviewPage": "hwpx.tools.layout_preview",
|
|
45
|
+
"render_layout_preview": "hwpx.tools.layout_preview",
|
|
46
|
+
# 문서 프리뷰 뷰어(3.8.0 신규). 계약 유동.
|
|
47
|
+
"DocumentViewer": "hwpx.tools.document_viewer",
|
|
48
|
+
"render_document_viewer": "hwpx.tools.document_viewer",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_DEPRECATED_EXPORTS = {
|
|
52
|
+
"analyze_template_formfit": "hwpx.template_formfit",
|
|
53
|
+
"apply_template_formfit": "hwpx.template_formfit",
|
|
54
|
+
"TEMPLATE_FORMFIT_BASELINE_SCHEMA_VERSION": "hwpx.template_formfit",
|
|
55
|
+
"TEMPLATE_FORMFIT_PLAN_SCHEMA_VERSION": "hwpx.template_formfit",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# deprecated formfit 표면의 공통 대체 경로 안내.
|
|
59
|
+
_FORMFIT_REPLACEMENT = (
|
|
60
|
+
"구조적 form-fill 경로를 사용하세요: 라이브러리는 "
|
|
61
|
+
"hwpx.table_patch.fill_cells 계열, MCP는 analyze_form_fill/apply_form_fill/"
|
|
62
|
+
"verify_form_fill."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _experimental_message(name: str) -> str:
|
|
67
|
+
return (
|
|
68
|
+
f"'hwpx.{name}'는 실험적(experimental) 표면입니다. 계약이 유동적이므로 "
|
|
69
|
+
f"'from hwpx.experimental import {name}'로 import하세요. 최상위 재내보내기는 "
|
|
70
|
+
f"다음 major에서 제거될 예정입니다."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _deprecated_message(name: str) -> str:
|
|
75
|
+
return (
|
|
76
|
+
f"'hwpx.{name}'는 deprecated입니다. {_FORMFIT_REPLACEMENT} 이 이름은 "
|
|
77
|
+
f"다음 major에서 제거될 예정입니다."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
14
81
|
def __getattr__(name: str) -> object:
|
|
15
|
-
"""Resolve dynamic module attributes.
|
|
82
|
+
"""Resolve dynamic module attributes.
|
|
83
|
+
|
|
84
|
+
``__version__``은 경고 없이 지연 해석하고, experimental/deprecated 이름은
|
|
85
|
+
해석 시 ``DeprecationWarning``을 냅니다.
|
|
86
|
+
"""
|
|
16
87
|
|
|
17
88
|
if name == "__version__":
|
|
18
89
|
return _resolve_version()
|
|
90
|
+
|
|
91
|
+
module_name = _EXPERIMENTAL_EXPORTS.get(name)
|
|
92
|
+
if module_name is not None:
|
|
93
|
+
warnings.warn(_experimental_message(name), DeprecationWarning, stacklevel=2)
|
|
94
|
+
return getattr(importlib.import_module(module_name), name)
|
|
95
|
+
|
|
96
|
+
module_name = _DEPRECATED_EXPORTS.get(name)
|
|
97
|
+
if module_name is not None:
|
|
98
|
+
warnings.warn(_deprecated_message(name), DeprecationWarning, stacklevel=2)
|
|
99
|
+
return getattr(importlib.import_module(module_name), name)
|
|
100
|
+
|
|
19
101
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
20
102
|
|
|
103
|
+
|
|
104
|
+
def __dir__() -> list[str]:
|
|
105
|
+
return sorted(
|
|
106
|
+
set(globals())
|
|
107
|
+
| set(__all__)
|
|
108
|
+
| set(_EXPERIMENTAL_EXPORTS)
|
|
109
|
+
| set(_DEPRECATED_EXPORTS)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# --- stable 최상위 표면 (eager import) ------------------------------------------
|
|
21
114
|
from .tools.text_extractor import (
|
|
22
115
|
DEFAULT_NAMESPACES,
|
|
23
116
|
ParagraphInfo,
|
|
@@ -70,25 +163,8 @@ from .tools.style_profile import (
|
|
|
70
163
|
placeholder_fill_report,
|
|
71
164
|
register_template,
|
|
72
165
|
)
|
|
73
|
-
from .
|
|
74
|
-
|
|
75
|
-
PreviewPage,
|
|
76
|
-
render_layout_preview,
|
|
77
|
-
)
|
|
78
|
-
from .tools.document_viewer import (
|
|
79
|
-
DocumentViewer,
|
|
80
|
-
render_document_viewer,
|
|
81
|
-
)
|
|
82
|
-
from .ingest import (
|
|
83
|
-
ConversionAttempt,
|
|
84
|
-
DocumentConverter,
|
|
85
|
-
DocumentIngestError,
|
|
86
|
-
DocumentIngestResult,
|
|
87
|
-
DocumentIngestor,
|
|
88
|
-
DocumentSourceInfo,
|
|
89
|
-
HwpxMarkdownConverter,
|
|
90
|
-
UnsupportedDocumentFormat,
|
|
91
|
-
)
|
|
166
|
+
from .ingest import HwpxMarkdownConverter
|
|
167
|
+
from .errors import HwpxError
|
|
92
168
|
from .mutation_report import (
|
|
93
169
|
MutationReport,
|
|
94
170
|
PreservationDowngradeError,
|
|
@@ -124,12 +200,6 @@ from .quality import (
|
|
|
124
200
|
SavePipeline,
|
|
125
201
|
VisualCompleteReport,
|
|
126
202
|
)
|
|
127
|
-
from .template_formfit import (
|
|
128
|
-
TEMPLATE_FORMFIT_BASELINE_SCHEMA_VERSION,
|
|
129
|
-
TEMPLATE_FORMFIT_PLAN_SCHEMA_VERSION,
|
|
130
|
-
analyze_template_formfit,
|
|
131
|
-
apply_template_formfit,
|
|
132
|
-
)
|
|
133
203
|
|
|
134
204
|
__all__ = [
|
|
135
205
|
"QualityPolicy",
|
|
@@ -141,30 +211,21 @@ __all__ = [
|
|
|
141
211
|
"DEFAULT_STYLE_PRESET",
|
|
142
212
|
"DOCUMENT_PLAN_SCHEMA_VERSION",
|
|
143
213
|
"get_document_plan_schema",
|
|
144
|
-
"ConversionAttempt",
|
|
145
214
|
"DocumentBlock",
|
|
146
|
-
"DocumentConverter",
|
|
147
|
-
"DocumentIngestError",
|
|
148
|
-
"DocumentIngestResult",
|
|
149
|
-
"DocumentIngestor",
|
|
150
215
|
"DocumentPlan",
|
|
151
|
-
"DocumentSourceInfo",
|
|
152
216
|
"DocumentStylePreset",
|
|
153
217
|
"EditorOpenSafetyReport",
|
|
154
218
|
"ParagraphInfo",
|
|
155
219
|
"PackageValidationReport",
|
|
156
220
|
"BytePreservingPatchResult",
|
|
221
|
+
"HwpxError",
|
|
157
222
|
"MutationReport",
|
|
158
223
|
"PreservationDowngradeError",
|
|
159
224
|
"PlanValidationReport",
|
|
160
225
|
"ParagraphTextPatch",
|
|
161
226
|
"PatchApplied",
|
|
162
227
|
"PatchSkipped",
|
|
163
|
-
"LayoutPreview",
|
|
164
|
-
"PreviewPage",
|
|
165
228
|
"SectionInfo",
|
|
166
|
-
"TEMPLATE_FORMFIT_BASELINE_SCHEMA_VERSION",
|
|
167
|
-
"TEMPLATE_FORMFIT_PLAN_SCHEMA_VERSION",
|
|
168
229
|
"TextExtractor",
|
|
169
230
|
"FoundElement",
|
|
170
231
|
"HwpxMarkdownConverter",
|
|
@@ -177,7 +238,6 @@ __all__ = [
|
|
|
177
238
|
"STYLE_PROFILE_SCHEMA_VERSION",
|
|
178
239
|
"TEMPLATE_REGISTRY_SCHEMA_VERSION",
|
|
179
240
|
"TABLE_COMPUTE_REPORT_VERSION",
|
|
180
|
-
"UnsupportedDocumentFormat",
|
|
181
241
|
"apply_style_profile_to_plan",
|
|
182
242
|
"build_comparison_table_plan",
|
|
183
243
|
"build_image_grid",
|
|
@@ -190,8 +250,6 @@ __all__ = [
|
|
|
190
250
|
"HwpxDocument",
|
|
191
251
|
"HwpxPackage",
|
|
192
252
|
"create_document_from_plan",
|
|
193
|
-
"analyze_template_formfit",
|
|
194
|
-
"apply_template_formfit",
|
|
195
253
|
"approval_box",
|
|
196
254
|
"describe_template",
|
|
197
255
|
"extract_style_profile",
|
|
@@ -209,9 +267,6 @@ __all__ = [
|
|
|
209
267
|
"validate_editor_open_safety",
|
|
210
268
|
"validate_package",
|
|
211
269
|
"paragraph_patch",
|
|
212
|
-
"render_layout_preview",
|
|
213
|
-
"render_document_viewer",
|
|
214
|
-
"DocumentViewer",
|
|
215
270
|
"register_template",
|
|
216
271
|
"table_compute",
|
|
217
272
|
]
|
hwpx/_document/persistence.py
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
-
import warnings
|
|
7
6
|
from os import PathLike
|
|
8
7
|
from typing import TYPE_CHECKING, Any, BinaryIO, Literal, Sequence
|
|
9
8
|
|
|
9
|
+
from ..errors import SaveError
|
|
10
10
|
from ..mutation_report import (
|
|
11
11
|
Fallback,
|
|
12
12
|
Mode,
|
|
@@ -84,7 +84,15 @@ def _run_pre_save_validation(doc: "HwpxDocument") -> None:
|
|
|
84
84
|
report = doc.validate()
|
|
85
85
|
if not report.ok:
|
|
86
86
|
msgs = _summarize_validation_issues(report.issues)
|
|
87
|
-
raise
|
|
87
|
+
raise SaveError(
|
|
88
|
+
f"Document validation failed: {msgs}",
|
|
89
|
+
code="document-validation-failed",
|
|
90
|
+
context={"issueCount": len(report.issues), "issues": msgs},
|
|
91
|
+
suggestion=(
|
|
92
|
+
"Fix the reported schema issues, or disable validate_on_save if "
|
|
93
|
+
"the validation is a false positive for your document."
|
|
94
|
+
),
|
|
95
|
+
)
|
|
88
96
|
|
|
89
97
|
|
|
90
98
|
def _run_open_safety_validation(doc: "HwpxDocument", archive_bytes: bytes) -> None:
|
|
@@ -94,9 +102,15 @@ def _run_open_safety_validation(doc: "HwpxDocument", archive_bytes: bytes) -> No
|
|
|
94
102
|
|
|
95
103
|
report = validate_editor_open_safety(archive_bytes)
|
|
96
104
|
if not report.ok:
|
|
97
|
-
raise
|
|
105
|
+
raise SaveError(
|
|
98
106
|
"Generated HWPX package failed open-safety validation: "
|
|
99
|
-
+ report.summary
|
|
107
|
+
+ report.summary,
|
|
108
|
+
code="open-safety-failed",
|
|
109
|
+
context={"summary": report.summary},
|
|
110
|
+
suggestion=(
|
|
111
|
+
"The serialized package would not reopen in an HWPX editor; "
|
|
112
|
+
"inspect validate_editor_open_safety(...) for the failing checks."
|
|
113
|
+
),
|
|
100
114
|
)
|
|
101
115
|
|
|
102
116
|
|
|
@@ -126,7 +140,15 @@ def _gate_and_write(
|
|
|
126
140
|
)
|
|
127
141
|
if not report.ok:
|
|
128
142
|
detail = "; ".join(str(error) for error in report.errors)
|
|
129
|
-
raise
|
|
143
|
+
raise SaveError(
|
|
144
|
+
f"Document save failed the quality gate: {detail}",
|
|
145
|
+
code="quality-gate-failed",
|
|
146
|
+
context={"errors": [str(error) for error in report.errors]},
|
|
147
|
+
suggestion=(
|
|
148
|
+
"The SavePipeline quality gate rejected the archive; inspect the "
|
|
149
|
+
"returned report's errors before retrying."
|
|
150
|
+
),
|
|
151
|
+
)
|
|
130
152
|
return report
|
|
131
153
|
|
|
132
154
|
|
|
@@ -420,29 +442,3 @@ def _mark_save_clean(doc: "HwpxDocument") -> None:
|
|
|
420
442
|
package = doc._package
|
|
421
443
|
package._opened_members = dict(package._files)
|
|
422
444
|
package._opened_zip_infos = dict(package._zip_infos)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
def save(
|
|
426
|
-
doc: "HwpxDocument",
|
|
427
|
-
path_or_stream: str | PathLike[str] | BinaryIO | None = None,
|
|
428
|
-
) -> str | PathLike[str] | BinaryIO | bytes:
|
|
429
|
-
"""Deprecated compatibility wrapper around save_to_path/save_to_stream/to_bytes.
|
|
430
|
-
|
|
431
|
-
Deprecated:
|
|
432
|
-
``save()``는 하위 호환을 위해 유지되며 향후 제거될 수 있습니다.
|
|
433
|
-
- 경로 저장: ``save_to_path(path)``
|
|
434
|
-
- 스트림 저장: ``save_to_stream(stream)``
|
|
435
|
-
- 바이트 반환: ``to_bytes()``
|
|
436
|
-
"""
|
|
437
|
-
|
|
438
|
-
warnings.warn(
|
|
439
|
-
"HwpxDocument.save()는 deprecated 예정입니다. "
|
|
440
|
-
"save_to_path()/save_to_stream()/to_bytes() 사용을 권장합니다.",
|
|
441
|
-
DeprecationWarning,
|
|
442
|
-
stacklevel=2,
|
|
443
|
-
)
|
|
444
|
-
if path_or_stream is None:
|
|
445
|
-
return doc.to_bytes()
|
|
446
|
-
if isinstance(path_or_stream, (str, PathLike)):
|
|
447
|
-
return doc.save_to_path(path_or_stream)
|
|
448
|
-
return doc.save_to_stream(path_or_stream)
|
hwpx/document.py
CHANGED
|
@@ -1844,28 +1844,3 @@ class HwpxDocument:
|
|
|
1844
1844
|
self,
|
|
1845
1845
|
reset_dirty=reset_dirty,
|
|
1846
1846
|
)
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
@overload
|
|
1850
|
-
def save(self, path_or_stream: None = None) -> bytes: ...
|
|
1851
|
-
|
|
1852
|
-
@overload
|
|
1853
|
-
def save(self, path_or_stream: str | PathLike[str]) -> str | PathLike[str]: ...
|
|
1854
|
-
|
|
1855
|
-
@overload
|
|
1856
|
-
def save(self, path_or_stream: BinaryIO) -> BinaryIO: ...
|
|
1857
|
-
|
|
1858
|
-
def save(
|
|
1859
|
-
self,
|
|
1860
|
-
path_or_stream: str | PathLike[str] | BinaryIO | None = None,
|
|
1861
|
-
) -> str | PathLike[str] | BinaryIO | bytes:
|
|
1862
|
-
"""Deprecated compatibility wrapper around save_to_path/save_to_stream/to_bytes.
|
|
1863
|
-
|
|
1864
|
-
Deprecated:
|
|
1865
|
-
``save()``는 하위 호환을 위해 유지되며 향후 제거될 수 있습니다.
|
|
1866
|
-
- 경로 저장: ``save_to_path(path)``
|
|
1867
|
-
- 스트림 저장: ``save_to_stream(stream)``
|
|
1868
|
-
- 바이트 반환: ``to_bytes()``
|
|
1869
|
-
"""
|
|
1870
|
-
|
|
1871
|
-
return _persistence.save(self, path_or_stream=path_or_stream)
|
hwpx/errors.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Structured exception base for python-hwpx (S-091 P2).
|
|
3
|
+
|
|
4
|
+
Every fail-closed public contract raises a :class:`HwpxError` (or a subclass).
|
|
5
|
+
The base carries three machine-readable fields on top of the human-readable
|
|
6
|
+
message, so a caller can branch on a stable ``code``, read the measured
|
|
7
|
+
``context``, and surface an actionable ``suggestion`` without parsing prose:
|
|
8
|
+
|
|
9
|
+
- ``code`` — a stable, kebab-case identifier for the failure class. Callers may
|
|
10
|
+
switch on it; it is part of the contract and changes only on a major boundary.
|
|
11
|
+
- ``context`` — a JSON-serialisable dict of the measured values that triggered
|
|
12
|
+
the failure (offending parts, indices, counts…). Empty when there is nothing
|
|
13
|
+
to measure.
|
|
14
|
+
- ``suggestion`` — one actionable next step, or ``None`` when there is nothing
|
|
15
|
+
specific to advise.
|
|
16
|
+
|
|
17
|
+
``str(exc)`` stays the human sentence (the ``message``), so existing ``except``
|
|
18
|
+
handlers and log lines are unchanged. Subclasses set :attr:`default_code`, which
|
|
19
|
+
lets a historical ``raise Subclass("message")`` site keep working while gaining
|
|
20
|
+
the structured fields with no raise-site churn (§11 — no bulk raise rewrites).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Any, Mapping
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class HwpxError(Exception):
|
|
29
|
+
"""Base for structured, fail-closed python-hwpx errors."""
|
|
30
|
+
|
|
31
|
+
#: Stable ``code`` used when a raise site does not pass an explicit ``code``.
|
|
32
|
+
default_code: str = "hwpx-error"
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
message: str,
|
|
37
|
+
*,
|
|
38
|
+
code: str | None = None,
|
|
39
|
+
context: Mapping[str, Any] | None = None,
|
|
40
|
+
suggestion: str | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
super().__init__(message)
|
|
43
|
+
self.message = message
|
|
44
|
+
self.code = code if code is not None else self.default_code
|
|
45
|
+
self.context: dict[str, Any] = dict(context) if context else {}
|
|
46
|
+
self.suggestion = suggestion
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> dict[str, Any]:
|
|
49
|
+
"""The structured envelope: ``code`` / ``message`` / ``context`` / ``suggestion``."""
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
"code": self.code,
|
|
53
|
+
"message": self.message,
|
|
54
|
+
"context": dict(self.context),
|
|
55
|
+
"suggestion": self.suggestion,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SaveError(HwpxError, ValueError):
|
|
60
|
+
"""A representative save path (``save_to_path`` / ``save_to_stream`` /
|
|
61
|
+
``to_bytes``) failed closed before writing any output.
|
|
62
|
+
|
|
63
|
+
Subclasses :class:`ValueError` for backward compatibility: pre-4.0 callers
|
|
64
|
+
caught ``ValueError`` from these paths and must keep working.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
default_code = "save-failed"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = ["HwpxError", "SaveError"]
|
hwpx/experimental.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""실험적(experimental) 공개 표면.
|
|
3
|
+
|
|
4
|
+
여기 노출되는 이름들은 **계약이 유동적**입니다 — minor 릴리스에서 시그니처,
|
|
5
|
+
동작, 반환 스키마가 예고 없이 바뀔 수 있습니다. 안정 계약이 필요하면 stable 표면
|
|
6
|
+
(:mod:`hwpx` 최상위 ``__all__`` / ``docs/stable-api.md``)만 사용하세요.
|
|
7
|
+
|
|
8
|
+
이 모듈은 실제 구현 모듈에서 이름을 **재내보내기(re-export)** 만 합니다(모듈을
|
|
9
|
+
옮기지 않음). 최상위 ``from hwpx import ...`` 경로도 하위 호환을 위해 당분간
|
|
10
|
+
동작하지만 ``DeprecationWarning``을 내며 다음 major에서 제거될 예정입니다 —
|
|
11
|
+
새 코드는 ``from hwpx.experimental import ...``를 쓰세요.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from .ingest import (
|
|
17
|
+
ConversionAttempt,
|
|
18
|
+
DocumentConverter,
|
|
19
|
+
DocumentIngestError,
|
|
20
|
+
DocumentIngestResult,
|
|
21
|
+
DocumentIngestor,
|
|
22
|
+
DocumentSourceInfo,
|
|
23
|
+
UnsupportedDocumentFormat,
|
|
24
|
+
)
|
|
25
|
+
from .tools.document_viewer import (
|
|
26
|
+
DocumentViewer,
|
|
27
|
+
render_document_viewer,
|
|
28
|
+
)
|
|
29
|
+
from .tools.layout_preview import (
|
|
30
|
+
LayoutPreview,
|
|
31
|
+
PreviewPage,
|
|
32
|
+
render_layout_preview,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"ConversionAttempt",
|
|
37
|
+
"DocumentConverter",
|
|
38
|
+
"DocumentIngestError",
|
|
39
|
+
"DocumentIngestResult",
|
|
40
|
+
"DocumentIngestor",
|
|
41
|
+
"DocumentSourceInfo",
|
|
42
|
+
"UnsupportedDocumentFormat",
|
|
43
|
+
"LayoutPreview",
|
|
44
|
+
"PreviewPage",
|
|
45
|
+
"render_layout_preview",
|
|
46
|
+
"DocumentViewer",
|
|
47
|
+
"render_document_viewer",
|
|
48
|
+
]
|
hwpx/form_fit/wordbox.py
CHANGED
|
@@ -681,6 +681,14 @@ def extract_cell_clips(pdf_path: str, *, page: int | None = None) -> list[Rect]:
|
|
|
681
681
|
These are the overflow clips — same coordinate space as the glyph boxes — so
|
|
682
682
|
no HWPX→PDF transform is needed. ``find_tables`` can raise on odd pages; such
|
|
683
683
|
a page contributes no clips rather than crashing.
|
|
684
|
+
|
|
685
|
+
Borders-only (``strategy="lines_strict"``): a cell rectangle is defined by its
|
|
686
|
+
*drawn borders*, not by the text inside it. The ``find_tables`` default snaps
|
|
687
|
+
edges to glyph positions, so a filled value can appear to *escape* a clip that
|
|
688
|
+
was mis-sized around the very text it holds — a false overflow. S-094 P3
|
|
689
|
+
measured this: the only 3 corpus pairs that flagged ``newOverflow`` (the nts
|
|
690
|
+
forms) all had the fill value escaping a text-snapped clip under the default,
|
|
691
|
+
and 0 escapes under ``lines_strict``; there were no real escapes to mask.
|
|
684
692
|
"""
|
|
685
693
|
|
|
686
694
|
if not fitz_available():
|
|
@@ -695,7 +703,7 @@ def extract_cell_clips(pdf_path: str, *, page: int | None = None) -> list[Rect]:
|
|
|
695
703
|
with doc:
|
|
696
704
|
for pno in _page_indices(doc, page):
|
|
697
705
|
try:
|
|
698
|
-
tables = doc[pno].find_tables().tables
|
|
706
|
+
tables = doc[pno].find_tables(strategy="lines_strict").tables
|
|
699
707
|
except Exception:
|
|
700
708
|
continue # layout analysis failed on this page -> no clips, no crash
|
|
701
709
|
for ti, table in enumerate(tables):
|
|
@@ -839,8 +847,21 @@ class LayoutSignature:
|
|
|
839
847
|
lesson that absolute glyph-overlap is unusable on real forms). Page count is
|
|
840
848
|
the dominant, high-confidence signal — a fill that spills onto a new page is
|
|
841
849
|
the canonical layout collapse. Per-table ``(rows, cols)`` is the finer
|
|
842
|
-
row/column signal
|
|
843
|
-
|
|
850
|
+
row/column signal, extracted from *drawn table borders only*
|
|
851
|
+
(``find_tables(strategy="lines_strict")``): a structural fingerprint must be
|
|
852
|
+
text-independent, so it is judged as a multiset *delta* (blank vs filled),
|
|
853
|
+
not an absolute count.
|
|
854
|
+
|
|
855
|
+
Why ``lines_strict`` and not the ``find_tables`` default (``"lines"``): the
|
|
856
|
+
default snaps table edges to *text* positions, so injecting a value into a
|
|
857
|
+
previously-empty cell makes it hallucinate a phantom ``(2,2)``/``(1,2)`` table
|
|
858
|
+
or wobble a real one by ±1 row/col even when the drawn grid and page count are
|
|
859
|
+
unchanged. S-094 P3 measured this on the frozen corpus (all 6 "table-shape"
|
|
860
|
+
differential failures were page-stable phantoms; under ``lines_strict`` blank
|
|
861
|
+
and filled agree, 0 regressions across 31 pairs; ``sen-24`` has byte-identical
|
|
862
|
+
drawn rectangles yet the default strategy still invented a ``(1,2)`` table from
|
|
863
|
+
the 3 added glyphs). Borders-only detection measures the grid the fill did not
|
|
864
|
+
touch.
|
|
844
865
|
"""
|
|
845
866
|
|
|
846
867
|
page_count: int
|
|
@@ -879,7 +900,12 @@ def extract_layout_signature(pdf_path: str) -> LayoutSignature:
|
|
|
879
900
|
for page in doc:
|
|
880
901
|
sizes.append((float(page.rect.width), float(page.rect.height)))
|
|
881
902
|
try:
|
|
882
|
-
|
|
903
|
+
# Borders-only ("lines_strict"): a *structural* fingerprint must
|
|
904
|
+
# not react to cell text. The default strategy snaps to glyph
|
|
905
|
+
# positions and invents phantom tables from a fill's added text
|
|
906
|
+
# (S-094 P3). Bordered form grids are detected identically either
|
|
907
|
+
# way; the text-sensitivity is the only difference we drop.
|
|
908
|
+
tables = page.find_tables(strategy="lines_strict").tables
|
|
883
909
|
except Exception:
|
|
884
910
|
continue # layout analysis failed on this page -> no tables, no crash
|
|
885
911
|
for table in tables:
|
hwpx/mutation_report.py
CHANGED
|
@@ -22,6 +22,8 @@ from io import BytesIO
|
|
|
22
22
|
from typing import Any, Literal, Mapping, Sequence
|
|
23
23
|
from zipfile import ZipInfo
|
|
24
24
|
|
|
25
|
+
from .errors import HwpxError
|
|
26
|
+
|
|
25
27
|
MUTATION_REPORT_SCHEMA = "hwpx.mutation-report/v1"
|
|
26
28
|
COORDINATE_SPACE = "uncompressed-part-bytes"
|
|
27
29
|
|
|
@@ -229,9 +231,19 @@ class MutationReport:
|
|
|
229
231
|
}
|
|
230
232
|
|
|
231
233
|
|
|
232
|
-
class PreservationDowngradeError(
|
|
234
|
+
class PreservationDowngradeError(HwpxError):
|
|
233
235
|
"""Raised when a requested preservation grade is not achieved and
|
|
234
|
-
``fallback="error"`` — before any output is written (specs/032 §1).
|
|
236
|
+
``fallback="error"`` — before any output is written (specs/032 §1).
|
|
237
|
+
|
|
238
|
+
Structured on the :class:`~hwpx.errors.HwpxError` base: ``code`` is the stable
|
|
239
|
+
``"preservation-downgrade"`` identifier, ``context`` carries the measured
|
|
240
|
+
modes and offending parts, and ``suggestion`` is the actionable next step.
|
|
241
|
+
The historical positional attributes (``requested_mode``/``achieved_grade``/
|
|
242
|
+
``offending_parts``/``suggestion``) are preserved so existing handlers still
|
|
243
|
+
read them.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
default_code = "preservation-downgrade"
|
|
235
247
|
|
|
236
248
|
def __init__(
|
|
237
249
|
self,
|
|
@@ -244,12 +256,18 @@ class PreservationDowngradeError(Exception):
|
|
|
244
256
|
self.requested_mode = requested_mode
|
|
245
257
|
self.achieved_grade = achieved_grade
|
|
246
258
|
self.offending_parts = offending_parts
|
|
247
|
-
self.suggestion = suggestion
|
|
248
259
|
parts = ", ".join(offending_parts) if offending_parts else "(none)"
|
|
249
260
|
super().__init__(
|
|
250
261
|
f"requested mode {requested_mode!r} needs patch-grade preservation but "
|
|
251
262
|
f"the save achieved {achieved_grade!r}; offending parts: {parts}. "
|
|
252
|
-
f"{suggestion}"
|
|
263
|
+
f"{suggestion}",
|
|
264
|
+
code=self.default_code,
|
|
265
|
+
context={
|
|
266
|
+
"requestedMode": requested_mode,
|
|
267
|
+
"achievedGrade": achieved_grade,
|
|
268
|
+
"offendingParts": list(offending_parts),
|
|
269
|
+
},
|
|
270
|
+
suggestion=suggestion,
|
|
253
271
|
)
|
|
254
272
|
|
|
255
273
|
|
hwpx/table_patch.py
CHANGED
|
@@ -28,6 +28,7 @@ from dataclasses import dataclass
|
|
|
28
28
|
from pathlib import Path
|
|
29
29
|
from typing import Any, Iterable, Mapping, Sequence
|
|
30
30
|
|
|
31
|
+
from .errors import HwpxError
|
|
31
32
|
from .mutation_report import MutationReport, project_byte_splice
|
|
32
33
|
from .patch import (
|
|
33
34
|
_apply_edits,
|
|
@@ -803,8 +804,16 @@ _S_TR = re.compile(r"<hp:tr\b.*?</hp:tr>", re.DOTALL)
|
|
|
803
804
|
_S_TC = re.compile(r"<hp:tc\b.*?</hp:tc>", re.DOTALL)
|
|
804
805
|
|
|
805
806
|
|
|
806
|
-
class TableStructureError(ValueError):
|
|
807
|
-
"""A structure edit was refused (fail-closed) or is unsupported.
|
|
807
|
+
class TableStructureError(HwpxError, ValueError):
|
|
808
|
+
"""A structure edit was refused (fail-closed) or is unsupported.
|
|
809
|
+
|
|
810
|
+
Structured on :class:`~hwpx.errors.HwpxError` (``code="table-structure"``)
|
|
811
|
+
while still subclassing :class:`ValueError` for backward compatibility. The
|
|
812
|
+
many string-only raise sites keep working and gain the ``code``/``context``/
|
|
813
|
+
``suggestion`` fields via the base default.
|
|
814
|
+
"""
|
|
815
|
+
|
|
816
|
+
default_code = "table-structure"
|
|
808
817
|
|
|
809
818
|
|
|
810
819
|
def _si(chunk: str, tag: str, attr: str) -> int | None:
|
|
@@ -1645,8 +1654,14 @@ def apply_table_ops(
|
|
|
1645
1654
|
|
|
1646
1655
|
# --- P3: real-Hancom oracle gate for form-fill (FR-005) -----------------------
|
|
1647
1656
|
|
|
1648
|
-
class RenderCheckRequired(RuntimeError):
|
|
1649
|
-
"""``verify_fill(require=True)`` but no real Hancom oracle rendered.
|
|
1657
|
+
class RenderCheckRequired(HwpxError, RuntimeError):
|
|
1658
|
+
"""``verify_fill(require=True)`` but no real Hancom oracle rendered.
|
|
1659
|
+
|
|
1660
|
+
Structured on :class:`~hwpx.errors.HwpxError` (``code="render-check-required"``)
|
|
1661
|
+
while still subclassing :class:`RuntimeError` for backward compatibility.
|
|
1662
|
+
"""
|
|
1663
|
+
|
|
1664
|
+
default_code = "render-check-required"
|
|
1650
1665
|
|
|
1651
1666
|
|
|
1652
1667
|
def verify_fill(
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-hwpx
|
|
3
|
+
Version: 4.1.1
|
|
4
|
+
Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리
|
|
5
|
+
Author: python-hwpx Maintainers
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/airmang/python-hwpx
|
|
8
|
+
Project-URL: Documentation, https://airmang.github.io/python-hwpx/
|
|
9
|
+
Project-URL: Issues, https://github.com/airmang/python-hwpx/issues
|
|
10
|
+
Keywords: hwp,hwpx,hancom,opc,xml,document-automation,validation,template
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Classifier: Topic :: Text Processing :: Markup :: XML
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
License-File: NOTICE
|
|
23
|
+
Requires-Dist: lxml<7,>=4.9
|
|
24
|
+
Provides-Extra: visual
|
|
25
|
+
Requires-Dist: pymupdf>=1.24; extra == "visual"
|
|
26
|
+
Requires-Dist: pillow>=10.0; extra == "visual"
|
|
27
|
+
Requires-Dist: numpy>=1.26; extra == "visual"
|
|
28
|
+
Provides-Extra: xlsx
|
|
29
|
+
Requires-Dist: openpyxl>=3.1; extra == "xlsx"
|
|
30
|
+
Provides-Extra: preview
|
|
31
|
+
Requires-Dist: latex2mathml>=3.77; extra == "preview"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
34
|
+
Requires-Dist: twine>=4.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
36
|
+
Requires-Dist: latex2mathml>=3.77; extra == "dev"
|
|
37
|
+
Provides-Extra: test
|
|
38
|
+
Requires-Dist: build>=1.0; extra == "test"
|
|
39
|
+
Requires-Dist: numpy>=1.26; extra == "test"
|
|
40
|
+
Requires-Dist: pillow>=10.0; extra == "test"
|
|
41
|
+
Requires-Dist: pytest>=7.4; extra == "test"
|
|
42
|
+
Requires-Dist: pytest-cov>=5.0; extra == "test"
|
|
43
|
+
Requires-Dist: ruff>=0.12; extra == "test"
|
|
44
|
+
Requires-Dist: latex2mathml>=3.77; extra == "test"
|
|
45
|
+
Provides-Extra: typecheck
|
|
46
|
+
Requires-Dist: mypy>=1.10; extra == "typecheck"
|
|
47
|
+
Requires-Dist: pyright>=1.1.390; extra == "typecheck"
|
|
48
|
+
Dynamic: license-file
|
|
49
|
+
|
|
50
|
+
<p align="center">
|
|
51
|
+
<h1 align="center">python-hwpx</h1>
|
|
52
|
+
<p align="center">
|
|
53
|
+
<strong>한컴 없이 HWPX를 읽고, 고치고, 만드는 순수 파이썬 라이브러리</strong>
|
|
54
|
+
</p>
|
|
55
|
+
<p align="center">
|
|
56
|
+
<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>
|
|
57
|
+
<a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/pyversions/python-hwpx" alt="Python"></a>
|
|
58
|
+
<a href="https://github.com/airmang/python-hwpx/actions/workflows/tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/airmang/python-hwpx/tests.yml?branch=main&label=tests" alt="Tests"></a>
|
|
59
|
+
<a href="https://airmang.github.io/python-hwpx/corpus-metrics.html"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fairmang.github.io%2Fpython-hwpx%2F_static%2Fbadge-hancom-open.json" alt="Hancom open"></a>
|
|
60
|
+
<a href="https://github.com/airmang/python-hwpx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="License"></a>
|
|
61
|
+
</p>
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
<p align="center">한국어 | <a href="README_EN.md">English</a></p>
|
|
65
|
+
|
|
66
|
+
기존 문서는 손댄 곳만 고치고(미수정 영역은 바이트 그대로), 새 문서는 실제
|
|
67
|
+
한컴오피스가 받아들이는 형태로 만듭니다. HWPX는 ZIP+XML(OWPML/OPC) 구조라
|
|
68
|
+
Windows·macOS·Linux·CI 어디서든 순수 파이썬으로 동작합니다.
|
|
69
|
+
|
|
70
|
+
| | 레포 | 역할 |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| 📦 | **`python-hwpx`** | 순수 파이썬 HWPX 코어 (이 레포) |
|
|
73
|
+
| 🔌 | [`hwpx-mcp-server`](https://github.com/airmang/hwpx-mcp-server) | MCP 클라이언트(Claude Desktop 등)에서 HWPX 조작 |
|
|
74
|
+
| 🎯 | [`hwpx-plugin`](https://github.com/airmang/hwpx-plugins) | 에이전트용 플러그인·스킬 번들 |
|
|
75
|
+
|
|
76
|
+
## 시작하기
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install python-hwpx # Python 3.10+
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from hwpx import HwpxDocument
|
|
84
|
+
|
|
85
|
+
doc = HwpxDocument.open("보고서.hwpx")
|
|
86
|
+
doc.add_paragraph("자동화로 추가한 문단입니다.")
|
|
87
|
+
doc.save_to_path("보고서-수정.hwpx")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 무엇을 하나
|
|
91
|
+
|
|
92
|
+
- **읽기·추출** — 텍스트/HTML/rich Markdown 내보내기(서식·중첩 표·각주 보존), XPath 객체 탐색
|
|
93
|
+
- **편집** — 문단·표·이미지·머리글/바닥글·메모·각주, 줄간격·여백·쪽번호 등 서식
|
|
94
|
+
- **양식 채우기** — 라벨·경로 기반 셀 채움, 바이트 보존 구조 편집(행·열·오토핏·shrink-to-fit)
|
|
95
|
+
- **생성** — 조립형 builder, 공문 lint·결재란, 사진대지·명패·조직도, mail merge, 신구대조표
|
|
96
|
+
- **변경추적·목차** — redline 저작, 네이티브 목차·상호참조
|
|
97
|
+
- **검증·안전** — XSD·패키지 검증 CLI, 열림 안전 게이트, 모든 쓰기에 영수증(`MutationReport`)
|
|
98
|
+
|
|
99
|
+
자세한 내용: [사용 가이드](docs/usage.md) · [API 레퍼런스](https://airmang.github.io/python-hwpx/) · [안정 API 표면](docs/stable-api.md) · [예제](docs/examples.md)
|
|
100
|
+
|
|
101
|
+
### 양식 채우기 — 서식은 그대로, 값만
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
doc = HwpxDocument.open("신청서.hwpx")
|
|
105
|
+
result = doc.fill_by_path({
|
|
106
|
+
"성명 > right": "홍길동",
|
|
107
|
+
"소속 > right": "플랫폼팀",
|
|
108
|
+
})
|
|
109
|
+
doc.save_to_path("신청서-작성완료.hwpx")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
라벨 기준으로 셀을 찾아 채우고, 손대지 않은 영역은 원본 바이트가 그대로 유지됩니다.
|
|
113
|
+
|
|
114
|
+
### 저장에는 영수증이 따라옵니다
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
report = doc.save_to_path("결과.hwpx", return_report=True)
|
|
118
|
+
print(report.actual_mode) # "patch" — 문서 재조립 없이 저장됨
|
|
119
|
+
print(report.preservation.untouched_part_payloads.to_dict())
|
|
120
|
+
# {"verified": 17, "changed": 0}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
요청한 보존 등급을 지킬 수 없으면 아무것도 쓰지 않고 실패합니다(fail-closed).
|
|
124
|
+
전체 규칙은 [안전한 쓰기 계약](docs/safe-write-contract.md)에 있습니다.
|
|
125
|
+
|
|
126
|
+
## 실측으로 말합니다
|
|
127
|
+
|
|
128
|
+
산출물 전수를 실제 한컴오피스로 측정해 그대로 공개합니다(동결 코퍼스 N=497):
|
|
129
|
+
|
|
130
|
+
- **한컴 오픈 476/476 all-pass** — 우리가 만든 파일을 실한컴이 전부 엽니다
|
|
131
|
+
- **미수정 영역 바이트 보존 497/497** · 개인정보 0-leak
|
|
132
|
+
- **렌더 검증 416/476** + 정직 버킷 43 — 한컴 자체가 PDF export를 거부한 케이스도 숨기지 않고 집계
|
|
133
|
+
- 낮은 숫자도 그대로 발행합니다 — 전체 수치·주의사항: [실측 코퍼스 메트릭](https://airmang.github.io/python-hwpx/corpus-metrics.html)
|
|
134
|
+
|
|
135
|
+
기능별로 되는 것과 안 되는 것은 [지원 매트릭스](docs/support-matrix.md)에 등급으로
|
|
136
|
+
명시되어 있습니다. 현재 개발 상태는 Alpha입니다 — API는 바뀔 수 있습니다.
|
|
137
|
+
|
|
138
|
+
> 위 수치는 *생성물 수용률* 축입니다(만든 파일을 실한컴이 받는가). 문서 *파싱 recall*과는
|
|
139
|
+
> 다른 축이므로 파서 프로젝트 수치와 병치 비교하지 마세요.
|
|
140
|
+
|
|
141
|
+
## 비교
|
|
142
|
+
|
|
143
|
+
| | python-hwpx | pyhwpx | pyhwp |
|
|
144
|
+
|---|---|---|---|
|
|
145
|
+
| **대상 포맷** | `.hwpx` (OWPML/OPC) | `.hwpx` | `.hwp` (v5 바이너리) |
|
|
146
|
+
| **한/글 설치** | 불필요 | 필요 (Windows COM) | 불필요 |
|
|
147
|
+
| **크로스 플랫폼** | ✅ Linux / macOS / Windows / CI | ❌ Windows 전용 | ✅ |
|
|
148
|
+
| **편집/생성 API** | ✅ | ✅ (COM) | ❌ 대부분 읽기 |
|
|
149
|
+
| **AI 에이전트 연동 (MCP)** | ✅ | ❌ | ❌ |
|
|
150
|
+
|
|
151
|
+
> HWP(v5 바이너리)는 지원하지 않습니다. 한컴오피스에서 HWPX로 변환 후 사용하세요.
|
|
152
|
+
|
|
153
|
+
## 알려진 제약
|
|
154
|
+
|
|
155
|
+
- `add_shape()` / `add_control()`은 한/글이 요구하는 모든 하위 요소를 생성하지 않습니다.
|
|
156
|
+
- `<hp:pic>` 그림 개체의 완전 자동 생성은 제공하지 않습니다.
|
|
157
|
+
- 암호화된 HWPX는 지원하지 않습니다.
|
|
158
|
+
|
|
159
|
+
## 기여하기
|
|
160
|
+
|
|
161
|
+
[help wanted](https://github.com/airmang/python-hwpx/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) ·
|
|
162
|
+
[로드맵](https://github.com/airmang/python-hwpx/milestones) ·
|
|
163
|
+
[Discussions](https://github.com/airmang/python-hwpx/discussions) ·
|
|
164
|
+
[내부 실전 가이드](docs/internals/) ·
|
|
165
|
+
[CONTRIBUTING](CONTRIBUTING.md)
|
|
166
|
+
|
|
167
|
+
HWPX 내부 구조가 처음이라면 [내부 실전 가이드](docs/internals/)부터 — 실제 한/글
|
|
168
|
+
동작에서 확인된 조판 캐시·목차 필드·OPC 재패킹 같은 실전 지식을 정리해 두었습니다.
|
|
169
|
+
|
|
170
|
+
## 감사의 말
|
|
171
|
+
|
|
172
|
+
아래 공개 표준·프로젝트에 빚지고 있습니다.
|
|
173
|
+
|
|
174
|
+
- **[OWPML — 개방형 워드프로세서 마크업 언어 (KS X 6101)](https://www.kssn.net/search/stddetail.do?itemNo=K001010119985)** — HWPX가 기반하는 한국 산업 표준
|
|
175
|
+
- **[hancom-io/hwpx-owpml-model](https://github.com/hancom-io/hwpx-owpml-model)** — OWPML 요소 구조 참조 모델 · **[neolord0/hwpxlib](https://github.com/neolord0/hwpxlib)** — 오라클 샘플 코퍼스
|
|
176
|
+
- **[edwardkim/rhwp](https://github.com/edwardkim/rhwp)** — 멱등성·검증 게이트 설계 영감
|
|
177
|
+
- **범정부오피스** — 공무 문서 편집 워크플로 아이디어
|
|
178
|
+
|
|
179
|
+
## License · Maintainer
|
|
180
|
+
|
|
181
|
+
Apache-2.0 ([LICENSE](LICENSE) · [NOTICE](NOTICE)) — **Kohkyuhyun** [@airmang](https://github.com/airmang) · [kokyuhyun@hotmail.com](mailto:kokyuhyun@hotmail.com)
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
hwpx/__init__.py,sha256=
|
|
1
|
+
hwpx/__init__.py,sha256=7o5kxSumBqqFnhLrYojfCrvRbq3I0cE5DxxIp3cFeB8,8600
|
|
2
2
|
hwpx/authoring.py,sha256=BoWGNc50ALMSuyjFUGl1iMCLYb1rFllCKVcuugvsaOw,129001
|
|
3
3
|
hwpx/body_patch.py,sha256=yB3X0L3Jns38CpRFOvjvPMnovJ5a2SoVefh6FIy3uYQ,25736
|
|
4
|
-
hwpx/document.py,sha256=
|
|
4
|
+
hwpx/document.py,sha256=_a0snPKzdsdIL9AmzvQzvOloqXB0CVq89vx6GjnKeHw,59186
|
|
5
|
+
hwpx/errors.py,sha256=BgWhEFHiiiz9DoCNRwovHt95dz8HVzQx1C29PGfXMwQ,2619
|
|
5
6
|
hwpx/evalplan_fill.py,sha256=UmXj6uo63Bo1iBI9lB3_e2G-z8dT_8nQBywwvfrepfM,83578
|
|
7
|
+
hwpx/experimental.py,sha256=H8fBrmGf0BzoDS6dqwBpUChUestUgOpO7XjMEBXsssg,1499
|
|
6
8
|
hwpx/fill_residue.py,sha256=Uq4DbwFNlbHrO3FQA7Yyk1I0gAsvPw6O8mPD82rmBws,8286
|
|
7
9
|
hwpx/form_fill.py,sha256=VUIU53Qa9Ho2aP72biDvJwnDW7ngdAzu3PSd5A7d1JM,9908
|
|
8
10
|
hwpx/formfill_quality.py,sha256=pRxZYb1cg95F2O78va5glMJ3UqPh1rewJB1Qz-LOlsc,38521
|
|
9
11
|
hwpx/guidance_scan.py,sha256=62ltZsuaebWaDxDaMbbq870J5PQVZoz0ZCSZ39otBkE,25193
|
|
10
|
-
hwpx/mutation_report.py,sha256=
|
|
12
|
+
hwpx/mutation_report.py,sha256=6hurhDdgGiONeLIeWZJT8lwLtWJZ1VHp1l6G0zy0mQo,19409
|
|
11
13
|
hwpx/package.py,sha256=0rKjGCJbPQvrVBIy07Jpjsu3fI7HhbqFCGWTiTDsJpo,1141
|
|
12
14
|
hwpx/patch.py,sha256=6MEVGAjpaWKK7Oyxydfvk52-VzSJfHi6I-eUCGJHZQM,24846
|
|
13
15
|
hwpx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
hwpx/table_patch.py,sha256=
|
|
16
|
+
hwpx/table_patch.py,sha256=R5DBCgImIU8e53nq9X3wqGPJlZ7pbZoiretgU2E__a8,78805
|
|
15
17
|
hwpx/template_formfit.py,sha256=dlxf98FatT5Nl4hN15TuMXqj5NlDTGSnLaqXVo_PoBo,23424
|
|
16
18
|
hwpx/templates.py,sha256=28bYqeJVeDb1Cq8G9NZG9Mhnu4K2GamAKC4QhxvUZyA,1187
|
|
17
19
|
hwpx/_document/__init__.py,sha256=gJ7N5W4EANv_341NJCtbxSh0QrNIBtArRAnjJUe_PE4,98
|
|
@@ -20,7 +22,7 @@ hwpx/_document/fields.py,sha256=YvrbWyjcDtZDrWSMQkR31maCpYvhwYEWkNtxTFVxXGY,1969
|
|
|
20
22
|
hwpx/_document/layout.py,sha256=2Y3JUnN5-_zZHg0CxfXq4nl8MjqFoqQbSlgszCCzJ6g,23097
|
|
21
23
|
hwpx/_document/media.py,sha256=F3iquwS7V-luiBVg7H8Aq1DpNXNClPDmO1ug99dLyhw,11600
|
|
22
24
|
hwpx/_document/memos.py,sha256=LBr5OPKLQMm4cHFIg9HohcMyKbf8DPpdmR3jeiyRNMg,7489
|
|
23
|
-
hwpx/_document/persistence.py,sha256=
|
|
25
|
+
hwpx/_document/persistence.py,sha256=gPxo7alBywBPBu3iACeQs_gK3w3ruFd2K7J90cr4Keo,16011
|
|
24
26
|
hwpx/_document/shapes.py,sha256=RoDtvlGvLshIKMsqDbiKfGz0pin63qkLHVtWGbdzb-0,6165
|
|
25
27
|
hwpx/_document/tracked.py,sha256=ni4tglO1LuukTnRz6I__9kW0P8uYCSy9BEuxZMrinqc,3423
|
|
26
28
|
hwpx/agent/__init__.py,sha256=jMMxl9cvGrRtdX669yNmLzrk_CK3UlhXGjXQHy4ERpo,3400
|
|
@@ -106,7 +108,7 @@ hwpx/form_fit/measure.py,sha256=gnSfqccm7_3oTKv3HH6Zs77r2bToxzmfvUWN2zshDE8,2232
|
|
|
106
108
|
hwpx/form_fit/policy.py,sha256=iXAatjS7ZycOdKgMCL4Q_kYg0qnogFPdRNlr_ecOvjc,3106
|
|
107
109
|
hwpx/form_fit/report.py,sha256=h1NzQfbj1JYvZtKUi0DckJNWGnUHMZtQ3iCzbUxUjHA,3305
|
|
108
110
|
hwpx/form_fit/seal.py,sha256=QsPzFagaEi7UKAXS-iFwiZs8rLG-_Fo9RixLc2OaqbU,17640
|
|
109
|
-
hwpx/form_fit/wordbox.py,sha256=
|
|
111
|
+
hwpx/form_fit/wordbox.py,sha256=n86BegyBZMybf6Qxol6hWCG1r0EWiaJRyZEno_YkpIE,50170
|
|
110
112
|
hwpx/ingest/__init__.py,sha256=cjDQwMdaqbq2xiD32zmaFAyzZbENga_6MLbgo0sLGZE,636
|
|
111
113
|
hwpx/ingest/base.py,sha256=95jXJ70Hkay70BgyryGOzI3UVTixUpR-6f71iF3eqn4,8134
|
|
112
114
|
hwpx/ingest/hwpx_converter.py,sha256=HOUCFYO-HK4WRpWiN5sA_kZIvdv439mkZ6PMCFU3hLg,4269
|
|
@@ -207,10 +209,10 @@ hwpx/visual/page_qa.py,sha256=AuJCySfLIdXjPeekhAc3YnrIWOsN0sI35Pnz8BZ6Pro,7278
|
|
|
207
209
|
hwpx/visual/qa_contracts.py,sha256=1cjoiRTAWgi7NgE8SPc6bKxdoUfKx9nyou1mgC47TxY,10506
|
|
208
210
|
hwpx/visual/qa_metrics.py,sha256=I7RdybN-f1Fvcv2j-ceDPoek51nOkMui1Zrd7TEX_C8,9838
|
|
209
211
|
hwpx/visual/report.py,sha256=2RhXN1KBYOZTim9FNpeUhaaDHR7oFxI6Z2DLUkDIiwE,1717
|
|
210
|
-
python_hwpx-
|
|
211
|
-
python_hwpx-
|
|
212
|
-
python_hwpx-
|
|
213
|
-
python_hwpx-
|
|
214
|
-
python_hwpx-
|
|
215
|
-
python_hwpx-
|
|
216
|
-
python_hwpx-
|
|
212
|
+
python_hwpx-4.1.1.dist-info/licenses/LICENSE,sha256=_ubz4wv-BkkT3l3gu-QuH7JGeVjuRYGZoZK95eNsCHU,9688
|
|
213
|
+
python_hwpx-4.1.1.dist-info/licenses/NOTICE,sha256=KJgtwIIzrXoA0j3PUhwp78ZtnucocEoB7jiuwoL4i7o,3060
|
|
214
|
+
python_hwpx-4.1.1.dist-info/METADATA,sha256=5R5nvsB-mwhZxnwU0KQG3wXscXzEBa6TjdvA4iknZhg,9164
|
|
215
|
+
python_hwpx-4.1.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
216
|
+
python_hwpx-4.1.1.dist-info/entry_points.txt,sha256=PVUBTBQtBHiflQ9XBjfd004w-LrDibgZjwzxgu8Tx7w,480
|
|
217
|
+
python_hwpx-4.1.1.dist-info/top_level.txt,sha256=R1iToqDh80Nf2oQhRjTN0rbN2X6kyDUizIocZjkhuxc,5
|
|
218
|
+
python_hwpx-4.1.1.dist-info/RECORD,,
|
|
@@ -1,344 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: python-hwpx
|
|
3
|
-
Version: 3.8.0
|
|
4
|
-
Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리
|
|
5
|
-
Author: python-hwpx Maintainers
|
|
6
|
-
License-Expression: Apache-2.0
|
|
7
|
-
Project-URL: Homepage, https://github.com/airmang/python-hwpx
|
|
8
|
-
Project-URL: Documentation, https://airmang.github.io/python-hwpx/
|
|
9
|
-
Project-URL: Issues, https://github.com/airmang/python-hwpx/issues
|
|
10
|
-
Keywords: hwp,hwpx,hancom,opc,xml,document-automation,validation,template
|
|
11
|
-
Classifier: Development Status :: 3 - Alpha
|
|
12
|
-
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
-
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
-
Classifier: Topic :: Text Processing :: Markup :: XML
|
|
19
|
-
Requires-Python: >=3.10
|
|
20
|
-
Description-Content-Type: text/markdown
|
|
21
|
-
License-File: LICENSE
|
|
22
|
-
License-File: NOTICE
|
|
23
|
-
Requires-Dist: lxml<7,>=4.9
|
|
24
|
-
Provides-Extra: visual
|
|
25
|
-
Requires-Dist: pymupdf>=1.24; extra == "visual"
|
|
26
|
-
Requires-Dist: pillow>=10.0; extra == "visual"
|
|
27
|
-
Requires-Dist: numpy>=1.26; extra == "visual"
|
|
28
|
-
Provides-Extra: xlsx
|
|
29
|
-
Requires-Dist: openpyxl>=3.1; extra == "xlsx"
|
|
30
|
-
Provides-Extra: preview
|
|
31
|
-
Requires-Dist: latex2mathml>=3.77; extra == "preview"
|
|
32
|
-
Provides-Extra: dev
|
|
33
|
-
Requires-Dist: build>=1.0; extra == "dev"
|
|
34
|
-
Requires-Dist: twine>=4.0; extra == "dev"
|
|
35
|
-
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
36
|
-
Requires-Dist: latex2mathml>=3.77; extra == "dev"
|
|
37
|
-
Provides-Extra: test
|
|
38
|
-
Requires-Dist: build>=1.0; extra == "test"
|
|
39
|
-
Requires-Dist: numpy>=1.26; extra == "test"
|
|
40
|
-
Requires-Dist: pillow>=10.0; extra == "test"
|
|
41
|
-
Requires-Dist: pytest>=7.4; extra == "test"
|
|
42
|
-
Requires-Dist: pytest-cov>=5.0; extra == "test"
|
|
43
|
-
Requires-Dist: ruff>=0.12; extra == "test"
|
|
44
|
-
Requires-Dist: latex2mathml>=3.77; extra == "test"
|
|
45
|
-
Provides-Extra: typecheck
|
|
46
|
-
Requires-Dist: mypy>=1.10; extra == "typecheck"
|
|
47
|
-
Requires-Dist: pyright>=1.1.390; extra == "typecheck"
|
|
48
|
-
Dynamic: license-file
|
|
49
|
-
|
|
50
|
-
<p align="center">
|
|
51
|
-
<h1 align="center">python-hwpx</h1>
|
|
52
|
-
<p align="center">
|
|
53
|
-
<strong>한컴 없이 HWPX를 안전하게 자동화하는 Python 계층 — 최소 범위 편집, 검증된 저작, 모든 쓰기에 영수증.</strong>
|
|
54
|
-
</p>
|
|
55
|
-
<p align="center">
|
|
56
|
-
<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>
|
|
57
|
-
<a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/pyversions/python-hwpx" alt="Python"></a>
|
|
58
|
-
<a href="https://github.com/airmang/python-hwpx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="License"></a>
|
|
59
|
-
<a href="https://airmang.github.io/python-hwpx/"><img src="https://img.shields.io/badge/docs-Sphinx-8CA1AF" alt="Docs"></a>
|
|
60
|
-
<a href="https://github.com/airmang/python-hwpx/actions/workflows/tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/airmang/python-hwpx/tests.yml?branch=main&label=tests" alt="Tests"></a>
|
|
61
|
-
<a href="https://airmang.github.io/python-hwpx/corpus-metrics.html"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fairmang.github.io%2Fpython-hwpx%2F_static%2Fbadge-hancom-open.json" alt="Hancom open"></a>
|
|
62
|
-
</p>
|
|
63
|
-
</p>
|
|
64
|
-
|
|
65
|
-
<p align="center">한국어 | <a href="README_EN.md">English</a></p>
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
> **python-hwpx는 한컴 없이 HWPX를 안전하게 자동화하는 Python 계층입니다.** 기존
|
|
70
|
-
> 문서는 최소 범위만 수정하고, 새 문서는 실제 한컴 수용이 검증된 형태로 생성하며,
|
|
71
|
-
> 모든 쓰기에 변경·보존·검증 영수증을 남기고, 완전한 해석과 렌더링은 전문 백엔드에
|
|
72
|
-
> 위임할 수 있습니다.
|
|
73
|
-
|
|
74
|
-
- **최소 범위 편집** — 미수정 part는 저장 시 바이트 그대로 유지됩니다(patch 경로
|
|
75
|
-
바이트 보존 497/497, 동결 코퍼스 v2 · 2026-07-19).
|
|
76
|
-
- **검증된 저작** — 밑바닥 생성도 실제 한컴이 받아들이는 형태로 냅니다(산출물 한컴
|
|
77
|
-
오픈 476/476 all-pass, 실저작 품질 게이트 58/58).
|
|
78
|
-
- **모든 쓰기에 영수증** — 대표 저장 경로는 [Safe Write Contract](docs/safe-write-contract.md)의
|
|
79
|
-
`MutationReport`(`hwpx.mutation-report/v1`)로 실제 쓰기 모드·보존 등급·검증 결과를
|
|
80
|
-
**측정해** 반환합니다.
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## 🧩 HWPX Stack (3종)
|
|
85
|
-
|
|
86
|
-
| 계층 | 레포 | 역할 |
|
|
87
|
-
|---|---|---|
|
|
88
|
-
| 📦 라이브러리 | **[`python-hwpx`](https://github.com/airmang/python-hwpx)** | 순수 파이썬 HWPX 파싱·편집·생성 코어 |
|
|
89
|
-
| 🔌 MCP 서버 | [`hwpx-mcp-server`](https://github.com/airmang/hwpx-mcp-server) | MCP 클라이언트(Claude Desktop, VS Code 등)에서 HWPX 조작 |
|
|
90
|
-
| 🎯 에이전트 스킬 | [`hwpx-plugin`](https://github.com/airmang/hwpx-plugins) | 에이전트가 HWPX를 바로 쓰게 해주는 first-party 플러그인·스킬 번들 |
|
|
91
|
-
|
|
92
|
-
`hwpx-mcp-server`와 `hwpx-plugin`은 같은 프로젝트가 직접 유지보수하는 first-party 연동
|
|
93
|
-
구성요소입니다(유지보수 관계를 뜻하며, 한컴 등 외부 기관의 인증을 뜻하지 않습니다).
|
|
94
|
-
|
|
95
|
-
현재 PyPI 공개 릴리스는 `python-hwpx 3.8.0`입니다. 일반
|
|
96
|
-
`pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
|
|
97
|
-
현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
|
|
98
|
-
성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
## 실측으로 말합니다 — Published Corpus
|
|
103
|
-
|
|
104
|
-
이 스택의 산출물은 주장 대신 **실제 한컴오피스 전수 측정**으로 검증됩니다
|
|
105
|
-
(동결 코퍼스 v2, N=497 산출물, 2026-07-19, 실한컴 12.0.0.3288 COM/GUI 오라클;
|
|
106
|
-
상세·주의사항은
|
|
107
|
-
[실측 코퍼스 메트릭](https://airmang.github.io/python-hwpx/corpus-metrics.html)):
|
|
108
|
-
|
|
109
|
-
- **한컴 오픈 수용률 476/476 all-pass** (동결 코퍼스 v2 · 2026-07-19 · 실한컴 COM
|
|
110
|
-
`Open()` 판정 · rule-of-three 하한 99.37%) · 파싱 96.2%(458/476)
|
|
111
|
-
- **미수정 영역 바이트 보존 497/497** (patch 경로 한정, zip-part diff · 오라클 불요)
|
|
112
|
-
· **개인정보 0-leak** (35문서/합성 140값)
|
|
113
|
-
- 렌더 검증 416/476 (실한컴 `SaveAs("PDF")`) + 정직 버킷 43건(변경추적 문서의 PDF
|
|
114
|
-
export는 한컴 자체가 거부 — 실측 한계로 발행) + 미검증 17건
|
|
115
|
-
- wild 공개 양식 채움은 구조결함 픽스 후 **무음 서식파괴 16.7%**(판정 66조합, 못 담는 타깃은 typed 거부 35건·산출분 pass 17/28) — **낮은 숫자도 그대로 발행**하고 잔여(페이지 리플·표 shape)를 명명합니다
|
|
116
|
-
|
|
117
|
-
<p align="center">
|
|
118
|
-
<img src="https://raw.githubusercontent.com/airmang/python-hwpx/main/docs/images/redline-hancom.png" alt="python-hwpx가 남긴 변경추적과 AI 에이전트 메모가 실제 한/글에서 열린 화면" width="720">
|
|
119
|
-
</p>
|
|
120
|
-
<p align="center"><sub>python-hwpx로 작성한 변경추적(취소선·삽입)과 AI 에이전트 메모 — 실제 한/글에서 연 화면입니다.</sub></p>
|
|
121
|
-
|
|
122
|
-
> 이 숫자들은 *생성물 수용률* 축입니다(우리가 만든 파일을 실제 한컴이 받아들이는가).
|
|
123
|
-
> 문서 *파싱 recall*과는 다른 축이므로 파서 프로젝트 수치와 병치 비교하지 마세요.
|
|
124
|
-
|
|
125
|
-
---
|
|
126
|
-
|
|
127
|
-
## 왜 python-hwpx인가
|
|
128
|
-
|
|
129
|
-
- **코어 편집에 한컴오피스 설치 불필요** — HWPX는 ZIP+XML(OWPML/OPC) 구조라, 순수 파이썬으로 Windows·macOS·Linux·CI 어디서나 읽고 씁니다.
|
|
130
|
-
- **읽기부터 생성까지 한 코어** — 텍스트/서식 추출, 문단·표·양식 편집, 새 문서 생성, XSD 스키마 검증을 하나의 API로 처리합니다.
|
|
131
|
-
- **에이전트·자동화 친화** — 같은 프로젝트가 유지보수하는 `hwpx-mcp-server`와 `hwpx-plugin`이 코어에 연결됩니다.
|
|
132
|
-
|
|
133
|
-
문서 파싱·편집·생성은 순수 Python으로 수행할 수 있습니다. 다만 페이지 나눔, 표 넘침,
|
|
134
|
-
글꼴 대체 등 최종 시각 품질을 확언하려면 필요에 따라 실제 한컴 렌더 오라클을 별도로 사용합니다.
|
|
135
|
-
|
|
136
|
-
## 빠른 시작
|
|
137
|
-
|
|
138
|
-
```bash
|
|
139
|
-
pip install python-hwpx # Python 3.10+ · lxml ≥ 4.9
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
```python
|
|
143
|
-
from hwpx import HwpxDocument
|
|
144
|
-
|
|
145
|
-
# 기존 문서 열기 → 편집 → 저장
|
|
146
|
-
doc = HwpxDocument.open("보고서.hwpx")
|
|
147
|
-
doc.add_paragraph("자동화로 추가한 문단입니다.")
|
|
148
|
-
doc.save_to_path("보고서-수정.hwpx")
|
|
149
|
-
|
|
150
|
-
# 새 문서 만들기
|
|
151
|
-
new = HwpxDocument.new()
|
|
152
|
-
new.add_paragraph("python-hwpx로 만든 새 문서")
|
|
153
|
-
new.save_to_path("새문서.hwpx")
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
> 💡 컨텍스트 매니저도 지원합니다 — `with` 블록을 벗어나면 리소스가 자동 정리됩니다:
|
|
157
|
-
> ```python
|
|
158
|
-
> with HwpxDocument.open("보고서.hwpx") as doc:
|
|
159
|
-
> doc.add_paragraph("자동으로 리소스가 정리됩니다.")
|
|
160
|
-
> doc.save_to_path("결과물.hwpx")
|
|
161
|
-
> ```
|
|
162
|
-
|
|
163
|
-
`open`/`new` → `edit`/`extract` → `save_to_path` 흐름만 잡으면 나머지는 필요할 때 확장하면 됩니다.
|
|
164
|
-
|
|
165
|
-
## 무엇을 하나
|
|
166
|
-
|
|
167
|
-
### 🔍 읽기 · 추출
|
|
168
|
-
- 텍스트/HTML/Markdown 내보내기 — `export_text()` · `export_html()` · `export_markdown()`
|
|
169
|
-
- **풍부한 Markdown** — `export_rich_markdown()`은 인라인 서식(`**굵게**`·`*기울임*`·`~~취소선~~`), 중첩 표(colspan/rowspan 안전), 도형 텍스트, 이미지, 각주/미주, 하이퍼링크, 제목(`#`/`##`) 자동 감지까지 보존
|
|
170
|
-
- **문서 ingest 게이트웨이** — `hwpx.ingest.DocumentIngestor`가 HWPX를 감지해 rich Markdown과 섹션/표 메타데이터로 정규화
|
|
171
|
-
- `TextExtractor` / `ObjectFinder` — 섹션·문단 순회, 태그·속성·XPath로 객체 탐색 (`hp:tab`은 `\t`로 보존, roundtrip 안전)
|
|
172
|
-
|
|
173
|
-
```python
|
|
174
|
-
doc = HwpxDocument.open("보고서.hwpx")
|
|
175
|
-
md = doc.export_rich_markdown(
|
|
176
|
-
image_dir="out/images", # BinData 이미지를 디스크에 추출
|
|
177
|
-
image_ref_prefix="images/", # 마크다운 내  경로 접두
|
|
178
|
-
detect_headings=True, # Ⅰ./1. 패턴 기반 #/## 자동
|
|
179
|
-
)
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
### ✏️ 편집
|
|
183
|
-
- 문단 추가/삭제/서식, Run 단위 볼드·이탤릭·밑줄·색상
|
|
184
|
-
- 섹션 추가/삭제(`add_section(after=)`·`remove_section()`, manifest 자동 관리)
|
|
185
|
-
- 표 생성·셀 텍스트·병합/분할·중첩 테이블, 이미지 임베드, 머리글/바닥글, 메모(앵커 기반), 각주/미주, 북마크/하이퍼링크, 다단 편집
|
|
186
|
-
- **기존 문서 서식 편집** — 정렬·줄간격·들여쓰기·문단 간격, 용지·여백·방향, 쪽번호, 불릿/번호
|
|
187
|
-
- **스타일 기반 치환** — 색상·밑줄·`charPrIDRef`로 Run을 필터링해 선택 교체(`replace_text_in_runs`·`find_runs_by_style`)
|
|
188
|
-
|
|
189
|
-
```python
|
|
190
|
-
# 빨간색 텍스트만 찾아서 치환
|
|
191
|
-
doc.replace_text_in_runs("임시", "확정", text_color="#FF0000")
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
### 🖊️ 양식 채우기 (byte-preserving)
|
|
195
|
-
- 누름틀(클릭히어) 필드 조회·서식 보존 채움, 라벨 기반 셀 탐색(`find_cell_by_label`)·경로 채우기(`fill_by_path`)
|
|
196
|
-
- **바이트 보존 구조 편집** — 셀 채우기 / 행·열·표 삭제·삽입 / 열 너비 오토핏 / 폰트 shrink-to-fit 을 문서 재조립 없이 수행해 양식 서식을 그대로 보존. 미수정 영역은 `hwpx.patch`가 section XML 바이트를 splice해 손대지 않음
|
|
197
|
-
|
|
198
|
-
```python
|
|
199
|
-
doc = HwpxDocument.open("신청서.hwpx")
|
|
200
|
-
result = doc.fill_by_path({
|
|
201
|
-
"성명 > right": "홍길동",
|
|
202
|
-
"소속 > right": "플랫폼팀",
|
|
203
|
-
})
|
|
204
|
-
doc.save_to_path("신청서-작성완료.hwpx")
|
|
205
|
-
print(result["applied_count"], result["failed_count"])
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
### 🏗️ 생성 · 공문서 도구
|
|
209
|
-
- `hwpx.builder` — Section/Heading/Table/Image/Header 조립형 생성 + 하드게이트 저장 리포트
|
|
210
|
-
- 공문서 도구 — `official_lint`(항목기호 위계·"끝." 표시·붙임·날짜 lint), 결재란 프리셋
|
|
211
|
-
- `advanced_generators` — 사진대지(image_grid)·회의 명패·표 기반 조직도
|
|
212
|
-
- `mail_merge` — 템플릿+데이터 N부 대량 생성, 표 합계·평균 계산
|
|
213
|
-
- `doc_diff` — 문단 LCS diff·신구대조표·참조 정합 lint
|
|
214
|
-
- `style_profile` — 참조 문서 프로파일 추출·적용, 템플릿 레지스트리
|
|
215
|
-
|
|
216
|
-
### ✅ 검증 · 안전 · 저수준
|
|
217
|
-
- XSD 스키마 + 패키지 구조 검증 — CLI `hwpx-validate` · `hwpx-validate-package`, `hwpx-analyze-template`
|
|
218
|
-
- `validate_editor_open_safety` — 저장/팩/리페어/빌더 출력 게이트, `openSafety` 증거 반환
|
|
219
|
-
- `hwpx.tools.fuzz`(시드 결정적 시나리오·3중 오라클) · `hwpx.tools.layout_preview`(페이지 박스 근사 HTML/PNG 자기검증) · `opc.security`(XML entity·ZIP 압축 폭탄 가드)
|
|
220
|
-
- `hwpx.oxml` 데이터클래스로 OWPML 스키마 ↔ Python 객체 직접 조작, HWPML 2016→2011 네임스페이스 자동 정규화
|
|
221
|
-
|
|
222
|
-
```bash
|
|
223
|
-
hwpx-validate-package 보고서.hwpx
|
|
224
|
-
hwpx-analyze-template 보고서.hwpx
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
> 전체 기능·클래스·메서드 목록은 [사용 가이드](docs/usage.md)와 [API 레퍼런스](https://airmang.github.io/python-hwpx/api_reference.html)를 참고하세요.
|
|
228
|
-
|
|
229
|
-
## 안전한 쓰기 계약 (Safe Write Contract)
|
|
230
|
-
|
|
231
|
-
대표 저장 경로(`save_to_path` · `save_to_stream` · `to_bytes`)는 **요청한 보존 등급을
|
|
232
|
-
쓰기 전에 판정하고, 실제로 무엇을 바꿨는지 측정한 영수증**을 돌려줍니다.
|
|
233
|
-
|
|
234
|
-
```python
|
|
235
|
-
from hwpx.mutation_report import PreservationDowngradeError
|
|
236
|
-
|
|
237
|
-
# 영수증과 함께 저장 — 달성 가능한 가장 강한 보존 등급 자동 선택(mode="auto" 기본)
|
|
238
|
-
report = doc.save_to_path("결과.hwpx", return_report=True)
|
|
239
|
-
print(report.actual_mode) # "patch" | "rebuild"
|
|
240
|
-
print(report.preservation.untouched_part_payloads.to_dict()) # {"verified": 17, "changed": 0}
|
|
241
|
-
|
|
242
|
-
# patch 등급 강제 — 미달이면 아무것도 쓰지 않고 예외(fail-closed)
|
|
243
|
-
try:
|
|
244
|
-
doc.save_to_path("결과.hwpx", mode="patch", fallback="error")
|
|
245
|
-
except PreservationDowngradeError as exc:
|
|
246
|
-
print(exc.offending_parts, exc.suggestion)
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
- `mode="patch" | "rebuild" | "auto"`(기본 `auto`) · `fallback="error" | "rebuild"`(기본 `error`)
|
|
250
|
-
- `mode="patch"` + `fallback="error"`에서 미수정 part의 바이트 동일성을 지킬 수 없으면
|
|
251
|
-
**아무것도 쓰지 않고** `PreservationDowngradeError`를 던집니다(무음 rebuild 없음).
|
|
252
|
-
- `MutationReport`는 `requestedMode`/`actualMode`/`fallbackUsed`, 변경 part와 좌표 명시
|
|
253
|
-
범위, 보존 3층(part 페이로드·ZIP 레코드·전체 패키지), 검증 3항목(`passed`/`failed`/`not_performed`)을
|
|
254
|
-
**측정해** 반환합니다.
|
|
255
|
-
|
|
256
|
-
> 파라미터 전체와 `MutationReport` 스키마는 [안전한 쓰기 계약 문서](docs/safe-write-contract.md)를 참고하세요.
|
|
257
|
-
|
|
258
|
-
## 지원 매트릭스
|
|
259
|
-
|
|
260
|
-
능력 영역별 실제 등급입니다(동결 코퍼스 v2 · 2026-07-19 · 실한컴 12.0.0.3288 오라클).
|
|
261
|
-
등급 어휘: **Parse / Preserve / Edit / Create / Render-verified /
|
|
262
|
-
Unsupported-but-preserved / Unsupported-and-rejected**.
|
|
263
|
-
|
|
264
|
-
| 능력 영역 | 상태 | 증거 |
|
|
265
|
-
|---|---|---|
|
|
266
|
-
| 문단·표 저작/편집 | Parse·Preserve·Edit·Create·Render-verified | 오픈 476/476 · 실저작 게이트 58/58 · 렌더 416 |
|
|
267
|
-
| 표 구조 변경(행·열·표, 오토핏) | Preserve·Edit | `hwpx.table_patch` · 바이트 보존 497/497 |
|
|
268
|
-
| 양식 채움(byte-splice) | Preserve·Edit | `hwpx.patch`·`table_patch`·`body_patch` · 보존 497/497 (wild 무음 서식파괴 16.7%·typed 거부 35/66, 잔여 명명) |
|
|
269
|
-
| 그림 삽입/치환 | Edit·Create | `add_picture`·`replace_picture` (복잡 개체는 한컴 확인 권장) |
|
|
270
|
-
| 차트 | Unsupported-but-preserved | 생성 API 없음 · 기존 차트 part는 patch 보존 |
|
|
271
|
-
| 수식 | Parse·Unsupported-but-preserved | 저작 API 없음 · 기존 수식 파싱·patch 보존 |
|
|
272
|
-
| 변경추적(redline) | Edit·Create | `add_tracked_*` · 실한컴 `IsTrackChange=1` (한컴이 PDF export 거부 → `render_unavailable` 정직 집계) |
|
|
273
|
-
| 메모(코멘트) | Edit·Create·Render-verified | `add_memo*` · 실 Windows 한컴 검증 |
|
|
274
|
-
| 각주/미주 | Edit·Create | `add_footnote`·`add_endnote` (렌더 독립 게이트 미측정) |
|
|
275
|
-
| 네이티브 목차/상호참조 | Create·Render-verified | `add_native_toc`·`toc_verify` · 구조 15/15 · 페이지 정합 5/5 |
|
|
276
|
-
| 암호화 HWPX | Unsupported-and-rejected | 복호화 없음 · 암호화 part는 파싱 단계 예외로 거부 |
|
|
277
|
-
| HWP 5.x 바이너리 | Unsupported-and-rejected | ZIP 아님 → 열기 시 `BadZipFile` (HWPX로 변환 후 사용) |
|
|
278
|
-
| 누름틀(form field) 생성 | Parse·Edit | 기존 필드 조회·서식보존 채움 · **신규 누름틀 생성 도구는 미제공** |
|
|
279
|
-
|
|
280
|
-
> 각 등급의 판정 근거와 상세 증거 포인터는 [지원 매트릭스 문서](docs/support-matrix.md)를 참고하세요.
|
|
281
|
-
|
|
282
|
-
## 대항 라이브러리 비교
|
|
283
|
-
|
|
284
|
-
| | python-hwpx | pyhwpx | pyhwp |
|
|
285
|
-
|---|---|---|---|
|
|
286
|
-
| **대상 포맷** | `.hwpx` (OWPML/OPC) | `.hwpx` | `.hwp` (v5 바이너리) |
|
|
287
|
-
| **한/글 설치** | 불필요 | 필요 (Windows COM) | 불필요 |
|
|
288
|
-
| **크로스 플랫폼** | ✅ Linux / macOS / Windows / CI | ❌ Windows 전용 | ✅ |
|
|
289
|
-
| **편집/생성 API** | ✅ | ✅ (COM) | ❌ 대부분 읽기 |
|
|
290
|
-
| **스키마 검증** | ✅ | ❌ | ❌ |
|
|
291
|
-
| **AI 에이전트 연동 (MCP)** | ✅ `hwpx-mcp-server` | ❌ | ❌ |
|
|
292
|
-
|
|
293
|
-
> HWP(v5 바이너리) 파일은 지원하지 않습니다. 한컴오피스에서 HWPX로 변환 후 사용하세요.
|
|
294
|
-
|
|
295
|
-
## 알려진 제약
|
|
296
|
-
|
|
297
|
-
- `add_shape()` / `add_control()`은 한/글이 요구하는 모든 하위 요소를 생성하지 않습니다. 복잡한 개체 추가 시 한/글에서 열어 검증하세요.
|
|
298
|
-
- 이미지 바이너리 임베드는 지원하지만 `<hp:pic>` 요소의 완전 자동 생성은 제공하지 않습니다.
|
|
299
|
-
- 암호화된 HWPX 파일의 암복호화는 지원하지 않습니다.
|
|
300
|
-
|
|
301
|
-
## 더 보기
|
|
302
|
-
|
|
303
|
-
- **[🚀 빠른 시작](docs/quickstart.md)** · **[📚 사용 가이드](docs/usage.md)** — 첫 파일 열기부터 문단·표·메모·섹션 편집, 텍스트 추출·검증까지
|
|
304
|
-
- **[💡 예제 모음](docs/examples.md)** · [`examples/`](examples/) — `build_release_checklist.py`(메모·스타일 편집 HWPX 생성), `extract_text.py`(CLI 텍스트 추출), `find_objects.py`(OWPML 노드 추적) 등
|
|
305
|
-
- **[📐 스키마 개요](docs/schema-overview.md)** · **[🔧 설치 검증](docs/installation.md)**
|
|
306
|
-
- **[🔬 HWPX 내부 실전 가이드](docs/internals/)** — 실제 한/글 동작에서 확인된 HWPUNIT·조판 캐시·목차 필드·OPC 재패킹·메모·오라클 한계
|
|
307
|
-
- **[📖 전체 문서 (Sphinx)](https://airmang.github.io/python-hwpx/)** — API 레퍼런스·50+ 실전 패턴·FAQ
|
|
308
|
-
- **[📝 CHANGELOG](CHANGELOG.md)** · **[🤝 CONTRIBUTING](CONTRIBUTING.md)** · **[👥 CONTRIBUTORS](CONTRIBUTORS.md)**
|
|
309
|
-
|
|
310
|
-
## 기여하기
|
|
311
|
-
|
|
312
|
-
버그 리포트, 기능 제안, PR 모두 환영합니다.
|
|
313
|
-
|
|
314
|
-
- **[help wanted 이슈](https://github.com/airmang/python-hwpx/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)** — 지금 들어오기 좋은 입구입니다. 시작 전에 이슈에 코멘트로 방향을 남겨주세요.
|
|
315
|
-
- **[마일스톤](https://github.com/airmang/python-hwpx/milestones)** — 공개 로드맵입니다. 프로젝트가 어디로 가는지 여기서 보입니다.
|
|
316
|
-
- **[Discussions](https://github.com/airmang/python-hwpx/discussions)** — 질문·아이디어는 이슈 대신 여기로.
|
|
317
|
-
- **[내부 실전 가이드](docs/internals/)** — HWPX 내부 구조가 처음이라면 여기부터. 개발 흐름은 [CONTRIBUTING.md](CONTRIBUTING.md)에 있습니다.
|
|
318
|
-
|
|
319
|
-
```bash
|
|
320
|
-
git clone https://github.com/airmang/python-hwpx.git
|
|
321
|
-
cd python-hwpx
|
|
322
|
-
pip install -e ".[dev]"
|
|
323
|
-
pytest
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
## 감사의 말
|
|
327
|
-
|
|
328
|
-
아래 공개 표준·프로젝트에 빚지고 있습니다.
|
|
329
|
-
|
|
330
|
-
- **[OWPML — 개방형 워드프로세서 마크업 언어 (KS X 6101)](https://www.kssn.net/search/stddetail.do?itemNo=K001010119985)** — HWPX가 기반하는 한국 산업 표준
|
|
331
|
-
- **[hancom-io/hwpx-owpml-model](https://github.com/hancom-io/hwpx-owpml-model)** — OWPML 요소 구조 참조 모델 · **[neolord0/hwpxlib](https://github.com/neolord0/hwpxlib)** — 오라클 샘플 코퍼스
|
|
332
|
-
- **[edwardkim/rhwp](https://github.com/edwardkim/rhwp)** — 멱등성·검증 게이트 설계 영감
|
|
333
|
-
- **범정부오피스** — 공무 문서 편집 워크플로 아이디어
|
|
334
|
-
|
|
335
|
-
## License
|
|
336
|
-
|
|
337
|
-
Apache License 2.0. See LICENSE and NOTICE.
|
|
338
|
-
|
|
339
|
-
## Maintainer
|
|
340
|
-
|
|
341
|
-
Primary maintainer/contact: **Kohkyuhyun** ([@airmang](https://github.com/airmang))
|
|
342
|
-
|
|
343
|
-
- ✉️ [kokyuhyun@hotmail.com](mailto:kokyuhyun@hotmail.com)
|
|
344
|
-
- 🐙 [@airmang](https://github.com/airmang)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|