python-hwpx 5.7.0__py3-none-any.whl → 6.0.2__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/_document/_legacy.py +1745 -0
- hwpx/_document/_resolve.py +173 -0
- hwpx/_document/fields.py +212 -62
- hwpx/_document/headings.py +187 -0
- hwpx/_document/layout.py +171 -59
- hwpx/_document/media.py +122 -38
- hwpx/_document/memos.py +49 -13
- hwpx/_document/ns/__init__.py +68 -0
- hwpx/_document/ns/_base.py +40 -0
- hwpx/_document/ns/fields.py +160 -0
- hwpx/_document/ns/media.py +92 -0
- hwpx/_document/ns/notes.py +210 -0
- hwpx/_document/ns/page.py +468 -0
- hwpx/_document/ns/parts.py +68 -0
- hwpx/_document/ns/refs.py +72 -0
- hwpx/_document/ns/shapes.py +250 -0
- hwpx/_document/ns/styles.py +371 -0
- hwpx/_document/ns/tables.py +80 -0
- hwpx/_document/ns/text.py +146 -0
- hwpx/_document/ns/tracking.py +151 -0
- hwpx/_document/persistence.py +11 -3
- hwpx/_document/shapes.py +44 -15
- hwpx/_document/tracked.py +108 -36
- hwpx/body_patch.py +78 -12
- hwpx/capabilities.py +275 -3
- hwpx/data/contract_docs/support-matrix.md +34 -2
- hwpx/document.py +239 -1557
- hwpx/errors.py +194 -1
- hwpx/layout/lint.py +8 -3
- hwpx/layout/report.py +11 -2
- hwpx/model.py +78 -0
- hwpx/objects/__init__.py +57 -0
- hwpx/objects/binary_item.py +58 -0
- hwpx/objects/checkbox.py +103 -0
- hwpx/objects/form_field.py +191 -0
- hwpx/objects/results.py +242 -0
- hwpx/objects/tracked.py +66 -0
- hwpx/oxml/_document_primitives.py +71 -0
- hwpx/oxml/document_parts.py +28 -12
- hwpx/oxml/header_part.py +114 -3
- hwpx/oxml/memo.py +78 -0
- hwpx/oxml/section_format.py +665 -0
- hwpx/patch.py +22 -1
- hwpx/quality/report.py +34 -2
- hwpx/quality/save_pipeline.py +21 -4
- hwpx/table_patch.py +1 -1
- hwpx/tools/idempotence.py +7 -5
- {python_hwpx-5.7.0.dist-info → python_hwpx-6.0.2.dist-info}/METADATA +19 -9
- {python_hwpx-5.7.0.dist-info → python_hwpx-6.0.2.dist-info}/RECORD +54 -31
- {python_hwpx-5.7.0.dist-info → python_hwpx-6.0.2.dist-info}/licenses/NOTICE +8 -0
- {python_hwpx-5.7.0.dist-info → python_hwpx-6.0.2.dist-info}/WHEEL +0 -0
- {python_hwpx-5.7.0.dist-info → python_hwpx-6.0.2.dist-info}/entry_points.txt +0 -0
- {python_hwpx-5.7.0.dist-info → python_hwpx-6.0.2.dist-info}/licenses/LICENSE +0 -0
- {python_hwpx-5.7.0.dist-info → python_hwpx-6.0.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1745 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""6.0 이주 창(migration window)을 여는 레거시 파사드.
|
|
3
|
+
|
|
4
|
+
`HwpxDocument`의 공개 표면은 5.x에서 102개였다. 6.0은 그것을 루트 34개로
|
|
5
|
+
줄이고 나머지를 도메인 네임스페이스(`doc.styles`, `doc.page`, `doc.fields` …)로
|
|
6
|
+
옮긴다. 이 모듈은 **옮겨 간 79개 이름을 루트에 그대로 남겨 두는 위임 shim**이며,
|
|
7
|
+
호출하면 행선지를 담은 `DeprecationWarning`을 낸다.
|
|
8
|
+
|
|
9
|
+
왜 별도 클래스인가. `tests/test_document_facade_surface.py`의 락 생성기는
|
|
10
|
+
`vars(HwpxDocument)` — **클래스 자기 `__dict__`만** 본다(상속 멤버는 안 잡힌다).
|
|
11
|
+
shim을 베이스 클래스에 두면 루트 락은 34개로 정직하게 줄고, shim은 사라지지
|
|
12
|
+
않고 `tests/data/document_legacy_shims.json`이라는 **별도 락**에 계속 세어진다.
|
|
13
|
+
감추는 게 아니라 분리해서 센다 — 그래서 두 번째 락은 감소만 허용하는
|
|
14
|
+
ratchet이고 `describe_capabilities()`가 `legacyShimCount`로 노출한다.
|
|
15
|
+
|
|
16
|
+
7.0에서 이 모듈 전체가 삭제된다(`docs/stable-api.md`의 최소 deprecation window
|
|
17
|
+
— 한 major에서 경고, 그다음 major에서 제거).
|
|
18
|
+
|
|
19
|
+
shim 본문은 5.8.0 `document.py`의 것을 **그대로** 옮긴 것이다. 위임 대상
|
|
20
|
+
(`_document/*.py` 소유 모듈)도 바뀌지 않았다. 6.0에서 이 79개 호출의
|
|
21
|
+
동작은 경고가 하나 더 붙는 것 말고는 5.x와 동일하다.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import functools
|
|
27
|
+
import inspect
|
|
28
|
+
import warnings
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
from typing import (
|
|
31
|
+
TYPE_CHECKING,
|
|
32
|
+
Any,
|
|
33
|
+
Callable,
|
|
34
|
+
Iterator,
|
|
35
|
+
Mapping,
|
|
36
|
+
Sequence,
|
|
37
|
+
TypeVar,
|
|
38
|
+
cast,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
from ..oxml import (
|
|
42
|
+
Bullet,
|
|
43
|
+
GenericElement,
|
|
44
|
+
HwpxOxmlInlineObject,
|
|
45
|
+
HwpxOxmlMemo,
|
|
46
|
+
HwpxOxmlNote,
|
|
47
|
+
HwpxOxmlParagraph,
|
|
48
|
+
HwpxOxmlRun,
|
|
49
|
+
HwpxOxmlSection,
|
|
50
|
+
HwpxOxmlSectionHeaderFooter,
|
|
51
|
+
HwpxOxmlShape,
|
|
52
|
+
HwpxOxmlTable,
|
|
53
|
+
MemoShape,
|
|
54
|
+
ParagraphProperty,
|
|
55
|
+
RunStyle,
|
|
56
|
+
Style,
|
|
57
|
+
TrackChange,
|
|
58
|
+
TrackChangeAuthor,
|
|
59
|
+
)
|
|
60
|
+
from ..errors import HwpxValueError
|
|
61
|
+
from . import _resolve
|
|
62
|
+
from . import fields as _fields
|
|
63
|
+
from . import layout as _layout
|
|
64
|
+
from . import media as _media
|
|
65
|
+
from . import memos as _memos
|
|
66
|
+
from . import persistence as _persistence
|
|
67
|
+
from . import shapes as _shapes
|
|
68
|
+
from . import tracked as _tracked
|
|
69
|
+
|
|
70
|
+
if TYPE_CHECKING:
|
|
71
|
+
from ..form_fit.policy import FitPolicy
|
|
72
|
+
from ..oxml import (
|
|
73
|
+
HwpxOxmlDocument,
|
|
74
|
+
HwpxOxmlHeader,
|
|
75
|
+
HwpxOxmlHistory,
|
|
76
|
+
HwpxOxmlMasterPage,
|
|
77
|
+
HwpxOxmlVersion,
|
|
78
|
+
)
|
|
79
|
+
from ..tools.table_navigation import (
|
|
80
|
+
SearchDirection,
|
|
81
|
+
TableFillResult,
|
|
82
|
+
TableLabelSearchResult,
|
|
83
|
+
TableMapResult,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
_F = TypeVar("_F", bound=Callable[..., Any])
|
|
87
|
+
|
|
88
|
+
#: shim이 가리키는 제거 예정 major. 락(`document_legacy_shims.json`)과
|
|
89
|
+
#: `describe_capabilities()`가 이 값을 읽는다.
|
|
90
|
+
LEGACY_REMOVED_IN = "7.0"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _takes_section_pair(func: Callable[..., Any]) -> bool:
|
|
94
|
+
"""`section` / `section_index` 쌍을 받는 서명인가.
|
|
95
|
+
|
|
96
|
+
`remove_section(section)` 처럼 짝이 없는 것은 제외한다 — 그쪽은 애초에
|
|
97
|
+
``HwpxOxmlSection | int`` 를 받는 다른 계약이다.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
params = inspect.signature(func).parameters
|
|
102
|
+
except (TypeError, ValueError): # pragma: no cover - 내장/C 함수 방어
|
|
103
|
+
return False
|
|
104
|
+
return "section" in params and "section_index" in params
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _moved(replacement: str, *, removed_in: str = LEGACY_REMOVED_IN) -> Callable[[_F], _F]:
|
|
108
|
+
"""호출 시 행선지를 알려 주는 위임 shim으로 감싼다.
|
|
109
|
+
|
|
110
|
+
`functools.wraps`가 `__wrapped__`를 남기므로 `inspect.signature`는 원본
|
|
111
|
+
시그니처를 그대로 보고한다 — 락이 기록하는 시그니처가 5.x와 한 글자도
|
|
112
|
+
달라지지 않는다.
|
|
113
|
+
|
|
114
|
+
부수적으로 **`section=` 관대 수용의 초크포인트**이기도 하다. 5.x에서
|
|
115
|
+
`doc.set_header_text("x", section=0)` 은 `AttributeError: 'int' object has
|
|
116
|
+
no attribute 'properties'` 로 터졌다. 여기서 한 번 정규화하면 소유 모듈
|
|
117
|
+
(`_document/layout.py` 등)은 항상 해석된 섹션 객체만 받는다. 경고와
|
|
118
|
+
정규화가 한 데코레이터에 있는 것은 결합이지만, 대안은 28개 shim 본문에
|
|
119
|
+
같은 두 줄을 복사하는 것이다 — 초크포인트가 하나인 쪽이 낫다.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def decorate(func: _F) -> _F:
|
|
123
|
+
normalizes_section = _takes_section_pair(func)
|
|
124
|
+
|
|
125
|
+
@functools.wraps(func)
|
|
126
|
+
def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
127
|
+
warnings.warn(
|
|
128
|
+
f"HwpxDocument.{func.__name__}은(는) python-hwpx 6.0에서 "
|
|
129
|
+
f"{replacement}(으)로 이동했습니다. {removed_in}에서 제거됩니다. "
|
|
130
|
+
f"자세한 내용: docs/migration-6.0.md",
|
|
131
|
+
DeprecationWarning,
|
|
132
|
+
stacklevel=2,
|
|
133
|
+
)
|
|
134
|
+
if normalizes_section and (
|
|
135
|
+
kwargs.get("section") is not None or kwargs.get("section_index") is not None
|
|
136
|
+
):
|
|
137
|
+
kwargs["section"] = _resolve.resolve_section(
|
|
138
|
+
self,
|
|
139
|
+
kwargs.get("section"),
|
|
140
|
+
kwargs.get("section_index"),
|
|
141
|
+
caller=func.__name__,
|
|
142
|
+
)
|
|
143
|
+
kwargs["section_index"] = None
|
|
144
|
+
return func(self, *args, **kwargs)
|
|
145
|
+
|
|
146
|
+
wrapper.__hwpx_moved_to__ = replacement # type: ignore[attr-defined]
|
|
147
|
+
wrapper.__hwpx_removed_in__ = removed_in # type: ignore[attr-defined]
|
|
148
|
+
wrapper.__hwpx_resolves_section__ = normalizes_section # type: ignore[attr-defined]
|
|
149
|
+
return wrapper # type: ignore[return-value]
|
|
150
|
+
|
|
151
|
+
return decorate
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class _LegacyFacade:
|
|
155
|
+
"""5.x 루트 표면 중 6.0에서 이동·강등된 79개 이름.
|
|
156
|
+
|
|
157
|
+
`HwpxDocument`가 이 클래스를 상속한다. 본문은 `self._root` 등 구현
|
|
158
|
+
상태를 쓰는데, 그 상태는 `HwpxDocument.__init__`이 만든다 — 이 클래스는
|
|
159
|
+
단독으로 인스턴스화되지 않는다.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
if TYPE_CHECKING:
|
|
163
|
+
# `HwpxDocument`가 소유하는 구현 상태·비공개 헬퍼. 타입 검사기가
|
|
164
|
+
# shim 본문을 검사할 수 있도록 여기서 선언만 한다.
|
|
165
|
+
_root: HwpxOxmlDocument
|
|
166
|
+
_package: Any
|
|
167
|
+
_FORMAT_TO_MEDIA_TYPE: dict[str, str]
|
|
168
|
+
|
|
169
|
+
def _iter_form_field_matches(self) -> list[dict[str, Any]]: ...
|
|
170
|
+
|
|
171
|
+
@_moved("doc.refs.add_bookmark")
|
|
172
|
+
def add_bookmark(
|
|
173
|
+
self,
|
|
174
|
+
name: str,
|
|
175
|
+
*,
|
|
176
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
177
|
+
section: HwpxOxmlSection | None = None,
|
|
178
|
+
section_index: int | None = None,
|
|
179
|
+
) -> HwpxOxmlInlineObject:
|
|
180
|
+
"""Insert a bookmark marker in the document.
|
|
181
|
+
|
|
182
|
+
Returns the ``<hp:ctrl>`` wrapper element.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
return _layout.add_bookmark(
|
|
186
|
+
self,
|
|
187
|
+
name=name,
|
|
188
|
+
paragraph=paragraph,
|
|
189
|
+
section=section,
|
|
190
|
+
section_index=section_index,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
@_moved("doc.shapes.add_chart")
|
|
194
|
+
def add_chart(
|
|
195
|
+
self,
|
|
196
|
+
chart_xml: bytes | str,
|
|
197
|
+
*,
|
|
198
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
199
|
+
section: HwpxOxmlSection | None = None,
|
|
200
|
+
section_index: int | None = None,
|
|
201
|
+
size: tuple[int, int] | None = None,
|
|
202
|
+
treat_as_char: bool = False,
|
|
203
|
+
char_pr_id_ref: str | int | None = None,
|
|
204
|
+
) -> HwpxOxmlInlineObject:
|
|
205
|
+
"""Insert a native chart from ECMA-376 chartML. **Experimental contract.**
|
|
206
|
+
|
|
207
|
+
Stores *chart_xml* as a ``Chart/chartN.xml`` package part and emits the
|
|
208
|
+
real-Hancom ``<hp:chart>`` anchor referencing it via ``chartIDRef``
|
|
209
|
+
(contract: ``specs/055-chart-authoring/evidence/p0/chart-contract.md``).
|
|
210
|
+
Hancom draws the chart from the chartML alone — no OLE fallback or
|
|
211
|
+
pre-rendered image is written. The chartML must parse and carry the
|
|
212
|
+
``c:chartSpace`` root (typed rejection otherwise), and the created
|
|
213
|
+
anchor is re-read through the standard section scan — creation fails
|
|
214
|
+
loudly if the standard consumer would not see it.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
chart_xml: ECMA-376 chartML document (``c:chartSpace``).
|
|
218
|
+
paragraph: Target paragraph (e.g. inside a table cell). When
|
|
219
|
+
omitted a new paragraph is appended to *section*.
|
|
220
|
+
size: Optional ``(width, height)`` HWPUNIT pair for the anchor.
|
|
221
|
+
treat_as_char: ``True`` places the chart inline in the text flow;
|
|
222
|
+
default mirrors the render-verified gold float placement.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
return _shapes.add_chart(
|
|
226
|
+
self,
|
|
227
|
+
chart_xml,
|
|
228
|
+
paragraph=paragraph,
|
|
229
|
+
section=section,
|
|
230
|
+
section_index=section_index,
|
|
231
|
+
size=size,
|
|
232
|
+
treat_as_char=treat_as_char,
|
|
233
|
+
char_pr_id_ref=char_pr_id_ref,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
@_moved("doc.fields.add_check_box")
|
|
237
|
+
def add_check_box(
|
|
238
|
+
self,
|
|
239
|
+
caption: str,
|
|
240
|
+
*,
|
|
241
|
+
checked: bool = False,
|
|
242
|
+
name: str | None = None,
|
|
243
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
244
|
+
section: HwpxOxmlSection | None = None,
|
|
245
|
+
section_index: int | None = None,
|
|
246
|
+
) -> dict[str, Any]:
|
|
247
|
+
"""Create a check-box form object (체크박스). **Experimental contract.**
|
|
248
|
+
|
|
249
|
+
Real Hancom draws ☑ when *checked* and □ otherwise, with *caption* beside
|
|
250
|
+
the box and present in the rendered text layer. The created object is
|
|
251
|
+
read back through :meth:`list_check_boxes` with no special-casing.
|
|
252
|
+
|
|
253
|
+
Note:
|
|
254
|
+
Korean government forms specify a text ``[ ]`` + √ convention rather
|
|
255
|
+
than this form object (시행규칙 별표 4 제10호), so this primitive is
|
|
256
|
+
for forms that genuinely use Hancom check boxes — not for 공문서.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
caption: Label drawn beside the box (non-empty).
|
|
260
|
+
checked: Initial state.
|
|
261
|
+
name: Object name; generated when omitted.
|
|
262
|
+
paragraph: Target paragraph (e.g. a table cell). A new paragraph is
|
|
263
|
+
appended to *section* when omitted.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
return _fields.add_check_box(
|
|
267
|
+
self,
|
|
268
|
+
caption,
|
|
269
|
+
checked=checked,
|
|
270
|
+
name=name,
|
|
271
|
+
paragraph=paragraph,
|
|
272
|
+
section=section,
|
|
273
|
+
section_index=section_index,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
@_moved("doc.shapes.add_control")
|
|
277
|
+
def add_control(
|
|
278
|
+
self,
|
|
279
|
+
*,
|
|
280
|
+
section: HwpxOxmlSection | None = None,
|
|
281
|
+
section_index: int | None = None,
|
|
282
|
+
attributes: dict[str, str] | None = None,
|
|
283
|
+
control_type: str | None = None,
|
|
284
|
+
para_pr_id_ref: str | int | None = None,
|
|
285
|
+
style_id_ref: str | int | None = None,
|
|
286
|
+
char_pr_id_ref: str | int | None = None,
|
|
287
|
+
run_attributes: dict[str, str] | None = None,
|
|
288
|
+
**extra_attrs: str,
|
|
289
|
+
) -> HwpxOxmlInlineObject:
|
|
290
|
+
"""Insert a control inline object into a new paragraph.
|
|
291
|
+
|
|
292
|
+
This is a low-level escape hatch: an ``<hp:ctrl>`` carries no meaning
|
|
293
|
+
of its own — the control it represents is its child element
|
|
294
|
+
(``colPr``, ``bookmark``, ``fieldBegin``, …). Until the caller
|
|
295
|
+
appends one, the element is empty and **Hancom refuses to open the
|
|
296
|
+
document**, so a :class:`UserWarning` is raised.
|
|
297
|
+
|
|
298
|
+
For the controls this package builds, prefer :meth:`set_columns`,
|
|
299
|
+
:meth:`add_bookmark`, and :meth:`add_hyperlink`, which write the full
|
|
300
|
+
child structure.
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
return _shapes.add_control(
|
|
304
|
+
self,
|
|
305
|
+
section=section,
|
|
306
|
+
section_index=section_index,
|
|
307
|
+
attributes=attributes,
|
|
308
|
+
control_type=control_type,
|
|
309
|
+
para_pr_id_ref=para_pr_id_ref,
|
|
310
|
+
style_id_ref=style_id_ref,
|
|
311
|
+
char_pr_id_ref=char_pr_id_ref,
|
|
312
|
+
run_attributes=run_attributes,
|
|
313
|
+
**extra_attrs,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
@_moved("doc.shapes.add_ellipse")
|
|
317
|
+
def add_ellipse(
|
|
318
|
+
self,
|
|
319
|
+
width: int = 14400,
|
|
320
|
+
height: int = 7200,
|
|
321
|
+
*,
|
|
322
|
+
line_color: str = "#000000",
|
|
323
|
+
line_width: str = "283",
|
|
324
|
+
fill_color: str | None = None,
|
|
325
|
+
treat_as_char: bool = True,
|
|
326
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
327
|
+
section: HwpxOxmlSection | None = None,
|
|
328
|
+
section_index: int | None = None,
|
|
329
|
+
) -> HwpxOxmlShape:
|
|
330
|
+
"""Insert an ellipse drawing shape.
|
|
331
|
+
|
|
332
|
+
Dimensions are in HWPUNIT.
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
return _shapes.add_ellipse(
|
|
336
|
+
self,
|
|
337
|
+
width=width,
|
|
338
|
+
height=height,
|
|
339
|
+
line_color=line_color,
|
|
340
|
+
line_width=line_width,
|
|
341
|
+
fill_color=fill_color,
|
|
342
|
+
treat_as_char=treat_as_char,
|
|
343
|
+
paragraph=paragraph,
|
|
344
|
+
section=section,
|
|
345
|
+
section_index=section_index,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
@_moved("doc.notes.add_endnote")
|
|
349
|
+
def add_endnote(
|
|
350
|
+
self,
|
|
351
|
+
text: str,
|
|
352
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
353
|
+
*,
|
|
354
|
+
section: HwpxOxmlSection | None = None,
|
|
355
|
+
section_index: int | None = None,
|
|
356
|
+
char_pr_id_ref: str | int | None = None,
|
|
357
|
+
) -> HwpxOxmlNote:
|
|
358
|
+
"""Add an endnote to an existing paragraph, or create a new one."""
|
|
359
|
+
|
|
360
|
+
return _shapes.add_endnote(
|
|
361
|
+
self,
|
|
362
|
+
text=text,
|
|
363
|
+
paragraph=paragraph,
|
|
364
|
+
section=section,
|
|
365
|
+
section_index=section_index,
|
|
366
|
+
char_pr_id_ref=char_pr_id_ref,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
@_moved("doc.shapes.add_equation")
|
|
370
|
+
def add_equation(
|
|
371
|
+
self,
|
|
372
|
+
script: str,
|
|
373
|
+
*,
|
|
374
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
375
|
+
section: HwpxOxmlSection | None = None,
|
|
376
|
+
section_index: int | None = None,
|
|
377
|
+
base_unit: int = 1100,
|
|
378
|
+
size: tuple[int, int] | None = None,
|
|
379
|
+
char_pr_id_ref: str | int | None = None,
|
|
380
|
+
) -> HwpxOxmlInlineObject:
|
|
381
|
+
"""Insert an inline equation from an EqEdit script. **Experimental contract.**
|
|
382
|
+
|
|
383
|
+
Emits the real-Hancom ``<hp:equation>`` shape (contract:
|
|
384
|
+
``specs/054-equation-authoring/evidence/p0/equation-contract.md``): the
|
|
385
|
+
EqEdit source is stored verbatim in ``<hp:script>``, no layout cache is
|
|
386
|
+
written (Hancom re-lays-out on open), and the shape is inline so it
|
|
387
|
+
renders in the page flow. The created element is immediately re-read
|
|
388
|
+
through the standard section scan — creation fails loudly if the
|
|
389
|
+
standard consumer would not see it (no special-casing by design).
|
|
390
|
+
|
|
391
|
+
To author from LaTeX, convert first (typed refusal outside the
|
|
392
|
+
verified token set)::
|
|
393
|
+
|
|
394
|
+
from hwpx.equation import latex_to_eqedit
|
|
395
|
+
doc.add_equation(latex_to_eqedit(r"\\frac{a}{b}"))
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
script: EqEdit script stored as-is (e.g. ``{a} over {b}``).
|
|
399
|
+
paragraph: Target paragraph (e.g. inside a table cell). When
|
|
400
|
+
omitted a new paragraph is appended to *section*.
|
|
401
|
+
base_unit: Equation base font size in 1/100 pt.
|
|
402
|
+
size: Optional explicit ``(width, height)`` HWPUNIT pair;
|
|
403
|
+
defaults to a proportional estimate (Hancom re-measures).
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
return _shapes.add_equation(
|
|
407
|
+
self,
|
|
408
|
+
script,
|
|
409
|
+
paragraph=paragraph,
|
|
410
|
+
section=section,
|
|
411
|
+
section_index=section_index,
|
|
412
|
+
base_unit=base_unit,
|
|
413
|
+
size=size,
|
|
414
|
+
char_pr_id_ref=char_pr_id_ref,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
@_moved("doc.notes.add_footnote")
|
|
418
|
+
def add_footnote(
|
|
419
|
+
self,
|
|
420
|
+
text: str,
|
|
421
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
422
|
+
*,
|
|
423
|
+
section: HwpxOxmlSection | None = None,
|
|
424
|
+
section_index: int | None = None,
|
|
425
|
+
char_pr_id_ref: str | int | None = None,
|
|
426
|
+
) -> HwpxOxmlNote:
|
|
427
|
+
"""Add a footnote to an existing paragraph, or create a new one.
|
|
428
|
+
|
|
429
|
+
When *paragraph* is ``None`` a new paragraph is appended to the given
|
|
430
|
+
(or last) section.
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
return _shapes.add_footnote(
|
|
434
|
+
self,
|
|
435
|
+
text=text,
|
|
436
|
+
paragraph=paragraph,
|
|
437
|
+
section=section,
|
|
438
|
+
section_index=section_index,
|
|
439
|
+
char_pr_id_ref=char_pr_id_ref,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
@_moved("doc.fields.add")
|
|
443
|
+
def add_form_field(
|
|
444
|
+
self,
|
|
445
|
+
name: str,
|
|
446
|
+
*,
|
|
447
|
+
prompt: str = "",
|
|
448
|
+
memo: str = "",
|
|
449
|
+
editable: bool = True,
|
|
450
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
451
|
+
section: HwpxOxmlSection | None = None,
|
|
452
|
+
section_index: int | None = None,
|
|
453
|
+
) -> dict[str, Any]:
|
|
454
|
+
"""Create a click-here (누름틀) form field. **Experimental contract.**
|
|
455
|
+
|
|
456
|
+
Emits the real-Hancom CLICKHERE shape (안내문 placeholder run included)
|
|
457
|
+
so the created field is indistinguishable from a Hancom-authored one:
|
|
458
|
+
``list_form_fields``/``fill_form_field`` recognize it with no
|
|
459
|
+
special-casing, and real Hancom Office enumerates and fills it.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
name: Field name (non-empty).
|
|
463
|
+
prompt: 안내문 shown while the field is empty. Screen-only —
|
|
464
|
+
Hancom does not print it.
|
|
465
|
+
memo: Help text (``HelpState``).
|
|
466
|
+
paragraph: Target paragraph (e.g. inside a table cell). When
|
|
467
|
+
omitted a new paragraph is appended to *section*.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
The created field's payload, same shape as a ``list_form_fields``
|
|
471
|
+
entry.
|
|
472
|
+
"""
|
|
473
|
+
|
|
474
|
+
return _fields.add_form_field(
|
|
475
|
+
self,
|
|
476
|
+
name,
|
|
477
|
+
prompt=prompt,
|
|
478
|
+
memo=memo,
|
|
479
|
+
editable=editable,
|
|
480
|
+
paragraph=paragraph,
|
|
481
|
+
section=section,
|
|
482
|
+
section_index=section_index,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
@_moved("doc.refs.add_hyperlink")
|
|
486
|
+
def add_hyperlink(
|
|
487
|
+
self,
|
|
488
|
+
url: str,
|
|
489
|
+
display_text: str,
|
|
490
|
+
*,
|
|
491
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
492
|
+
section: HwpxOxmlSection | None = None,
|
|
493
|
+
section_index: int | None = None,
|
|
494
|
+
char_pr_id_ref: str | int | None = None,
|
|
495
|
+
) -> HwpxOxmlInlineObject:
|
|
496
|
+
"""Insert a hyperlink (fieldBegin + text + fieldEnd).
|
|
497
|
+
|
|
498
|
+
The display text follows the Hancom convention (blue underlined)
|
|
499
|
+
unless ``char_pr_id_ref`` overrides it.
|
|
500
|
+
|
|
501
|
+
Returns the ``<hp:ctrl>`` wrapper containing the ``<hp:fieldBegin>``.
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
return _layout.add_hyperlink(
|
|
505
|
+
self,
|
|
506
|
+
url=url,
|
|
507
|
+
display_text=display_text,
|
|
508
|
+
paragraph=paragraph,
|
|
509
|
+
section=section,
|
|
510
|
+
section_index=section_index,
|
|
511
|
+
char_pr_id_ref=char_pr_id_ref,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
@_moved("doc.media.add_image")
|
|
515
|
+
def add_image(
|
|
516
|
+
self,
|
|
517
|
+
image_data: bytes,
|
|
518
|
+
image_format: str,
|
|
519
|
+
*,
|
|
520
|
+
item_id: str | None = None,
|
|
521
|
+
) -> str:
|
|
522
|
+
"""Embed an image file and return the manifest item id.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
image_data: Raw image bytes.
|
|
526
|
+
image_format: Image format extension (``jpg``, ``png``, …).
|
|
527
|
+
item_id: Optional explicit manifest item id. When omitted an
|
|
528
|
+
auto-generated ``BIN####`` id is used.
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
The manifest item id that can be passed to
|
|
532
|
+
``binaryItemIDRef`` when constructing a ``<hp:pic>`` element.
|
|
533
|
+
"""
|
|
534
|
+
|
|
535
|
+
return _media.add_image(
|
|
536
|
+
self,
|
|
537
|
+
image_data=image_data,
|
|
538
|
+
image_format=image_format,
|
|
539
|
+
item_id=item_id,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
@_moved("doc.shapes.add_line")
|
|
543
|
+
def add_line(
|
|
544
|
+
self,
|
|
545
|
+
start_x: int = 0,
|
|
546
|
+
start_y: int = 0,
|
|
547
|
+
end_x: int = 14400,
|
|
548
|
+
end_y: int = 0,
|
|
549
|
+
*,
|
|
550
|
+
line_color: str = "#000000",
|
|
551
|
+
line_width: str = "283",
|
|
552
|
+
treat_as_char: bool = True,
|
|
553
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
554
|
+
section: HwpxOxmlSection | None = None,
|
|
555
|
+
section_index: int | None = None,
|
|
556
|
+
) -> HwpxOxmlShape:
|
|
557
|
+
"""Insert a line drawing shape.
|
|
558
|
+
|
|
559
|
+
Coordinates are in HWPUNIT (7200 per inch).
|
|
560
|
+
"""
|
|
561
|
+
|
|
562
|
+
return _shapes.add_line(
|
|
563
|
+
self,
|
|
564
|
+
start_x=start_x,
|
|
565
|
+
start_y=start_y,
|
|
566
|
+
end_x=end_x,
|
|
567
|
+
end_y=end_y,
|
|
568
|
+
line_color=line_color,
|
|
569
|
+
line_width=line_width,
|
|
570
|
+
treat_as_char=treat_as_char,
|
|
571
|
+
paragraph=paragraph,
|
|
572
|
+
section=section,
|
|
573
|
+
section_index=section_index,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
@_moved("doc.notes.add_memo")
|
|
577
|
+
def add_memo(
|
|
578
|
+
self,
|
|
579
|
+
text: str = "",
|
|
580
|
+
*,
|
|
581
|
+
section: HwpxOxmlSection | None = None,
|
|
582
|
+
section_index: int | None = None,
|
|
583
|
+
memo_shape_id_ref: str | int | None = None,
|
|
584
|
+
memo_id: str | None = None,
|
|
585
|
+
char_pr_id_ref: str | int | None = None,
|
|
586
|
+
attributes: dict[str, str] | None = None,
|
|
587
|
+
) -> HwpxOxmlMemo:
|
|
588
|
+
"""Create a memo entry inside *section* (or the last section by default)."""
|
|
589
|
+
|
|
590
|
+
return _memos.add_memo(
|
|
591
|
+
self,
|
|
592
|
+
text,
|
|
593
|
+
section=section,
|
|
594
|
+
section_index=section_index,
|
|
595
|
+
memo_shape_id_ref=memo_shape_id_ref,
|
|
596
|
+
memo_id=memo_id,
|
|
597
|
+
char_pr_id_ref=char_pr_id_ref,
|
|
598
|
+
attributes=attributes,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
@_moved("doc.notes.add_memo(anchor=...)")
|
|
602
|
+
def add_memo_with_anchor(
|
|
603
|
+
self,
|
|
604
|
+
text: str = "",
|
|
605
|
+
*,
|
|
606
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
607
|
+
section: HwpxOxmlSection | None = None,
|
|
608
|
+
section_index: int | None = None,
|
|
609
|
+
paragraph_text: str | None = None,
|
|
610
|
+
memo_shape_id_ref: str | int | None = None,
|
|
611
|
+
memo_id: str | None = None,
|
|
612
|
+
char_pr_id_ref: str | int | None = None,
|
|
613
|
+
attributes: dict[str, str] | None = None,
|
|
614
|
+
field_id: str | None = None,
|
|
615
|
+
author: str | None = None,
|
|
616
|
+
created: datetime | str | None = None,
|
|
617
|
+
number: int = 1,
|
|
618
|
+
anchor_char_pr_id_ref: str | int | None = None,
|
|
619
|
+
) -> tuple[HwpxOxmlMemo, HwpxOxmlParagraph, str]:
|
|
620
|
+
"""Create a memo and ensure it is visible by anchoring a MEMO field."""
|
|
621
|
+
|
|
622
|
+
return _memos.add_memo_with_anchor(
|
|
623
|
+
self,
|
|
624
|
+
text,
|
|
625
|
+
paragraph=paragraph,
|
|
626
|
+
section=section,
|
|
627
|
+
section_index=section_index,
|
|
628
|
+
paragraph_text=paragraph_text,
|
|
629
|
+
memo_shape_id_ref=memo_shape_id_ref,
|
|
630
|
+
memo_id=memo_id,
|
|
631
|
+
char_pr_id_ref=char_pr_id_ref,
|
|
632
|
+
attributes=attributes,
|
|
633
|
+
field_id=field_id,
|
|
634
|
+
author=author,
|
|
635
|
+
created=created,
|
|
636
|
+
number=number,
|
|
637
|
+
anchor_char_pr_id_ref=anchor_char_pr_id_ref,
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
@_moved("doc.shapes.add_rectangle")
|
|
641
|
+
def add_rectangle(
|
|
642
|
+
self,
|
|
643
|
+
width: int = 14400,
|
|
644
|
+
height: int = 7200,
|
|
645
|
+
*,
|
|
646
|
+
ratio: int = 0,
|
|
647
|
+
line_color: str = "#000000",
|
|
648
|
+
line_width: str = "283",
|
|
649
|
+
fill_color: str | None = None,
|
|
650
|
+
treat_as_char: bool = True,
|
|
651
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
652
|
+
section: HwpxOxmlSection | None = None,
|
|
653
|
+
section_index: int | None = None,
|
|
654
|
+
) -> HwpxOxmlShape:
|
|
655
|
+
"""Insert a rectangle drawing shape.
|
|
656
|
+
|
|
657
|
+
Dimensions are in HWPUNIT. *ratio* controls corner roundness
|
|
658
|
+
(0 = sharp, 50 = semicircle).
|
|
659
|
+
"""
|
|
660
|
+
|
|
661
|
+
return _shapes.add_rectangle(
|
|
662
|
+
self,
|
|
663
|
+
width=width,
|
|
664
|
+
height=height,
|
|
665
|
+
ratio=ratio,
|
|
666
|
+
line_color=line_color,
|
|
667
|
+
line_width=line_width,
|
|
668
|
+
fill_color=fill_color,
|
|
669
|
+
treat_as_char=treat_as_char,
|
|
670
|
+
paragraph=paragraph,
|
|
671
|
+
section=section,
|
|
672
|
+
section_index=section_index,
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
@_moved("doc.shapes.add_raw")
|
|
676
|
+
def add_shape(
|
|
677
|
+
self,
|
|
678
|
+
shape_type: str,
|
|
679
|
+
*,
|
|
680
|
+
section: HwpxOxmlSection | None = None,
|
|
681
|
+
section_index: int | None = None,
|
|
682
|
+
attributes: dict[str, str] | None = None,
|
|
683
|
+
para_pr_id_ref: str | int | None = None,
|
|
684
|
+
style_id_ref: str | int | None = None,
|
|
685
|
+
char_pr_id_ref: str | int | None = None,
|
|
686
|
+
run_attributes: dict[str, str] | None = None,
|
|
687
|
+
**extra_attrs: str,
|
|
688
|
+
) -> HwpxOxmlInlineObject:
|
|
689
|
+
"""Insert an inline shape into a new paragraph.
|
|
690
|
+
|
|
691
|
+
This is a low-level escape hatch: it writes the element and the
|
|
692
|
+
attributes it is handed and nothing else, so the result is **not a
|
|
693
|
+
document Hancom can open** until the caller supplies the required
|
|
694
|
+
OWPML children (``offset``, ``orgSz``, ``curSz``, ``sz``, ``pos`` and
|
|
695
|
+
the type-specific geometry). A :class:`UserWarning` is raised while
|
|
696
|
+
they are missing.
|
|
697
|
+
|
|
698
|
+
For LINE / RECT / ELLIPSE shapes, prefer :meth:`add_line`,
|
|
699
|
+
:meth:`add_rectangle`, and :meth:`add_ellipse`, which build the full
|
|
700
|
+
child structure.
|
|
701
|
+
"""
|
|
702
|
+
|
|
703
|
+
return _shapes.add_shape(
|
|
704
|
+
self,
|
|
705
|
+
shape_type=shape_type,
|
|
706
|
+
section=section,
|
|
707
|
+
section_index=section_index,
|
|
708
|
+
attributes=attributes,
|
|
709
|
+
para_pr_id_ref=para_pr_id_ref,
|
|
710
|
+
style_id_ref=style_id_ref,
|
|
711
|
+
char_pr_id_ref=char_pr_id_ref,
|
|
712
|
+
run_attributes=run_attributes,
|
|
713
|
+
**extra_attrs,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
@_moved("doc.tracking.add_change")
|
|
717
|
+
def add_track_change(
|
|
718
|
+
self,
|
|
719
|
+
change_type: str,
|
|
720
|
+
*,
|
|
721
|
+
author_name: str = "AI Agent",
|
|
722
|
+
date: str | None = None,
|
|
723
|
+
) -> int:
|
|
724
|
+
"""Add tracked-change header metadata and return the new change id."""
|
|
725
|
+
|
|
726
|
+
return _tracked.add_track_change(
|
|
727
|
+
self,
|
|
728
|
+
change_type,
|
|
729
|
+
author_name=author_name,
|
|
730
|
+
date=date,
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
@_moved("doc.tracking.delete")
|
|
734
|
+
def add_tracked_delete(
|
|
735
|
+
self,
|
|
736
|
+
paragraph: HwpxOxmlParagraph,
|
|
737
|
+
*,
|
|
738
|
+
match: str | None = None,
|
|
739
|
+
author: str = "AI Agent",
|
|
740
|
+
date: str | None = None,
|
|
741
|
+
) -> int:
|
|
742
|
+
"""Wrap paragraph text or the first matching substring in delete marks."""
|
|
743
|
+
|
|
744
|
+
return _tracked.add_tracked_delete(
|
|
745
|
+
self,
|
|
746
|
+
paragraph,
|
|
747
|
+
match=match,
|
|
748
|
+
author=author,
|
|
749
|
+
date=date,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
@_moved("doc.tracking.insert")
|
|
753
|
+
def add_tracked_insert(
|
|
754
|
+
self,
|
|
755
|
+
paragraph: HwpxOxmlParagraph,
|
|
756
|
+
text: str,
|
|
757
|
+
*,
|
|
758
|
+
author: str = "AI Agent",
|
|
759
|
+
date: str | None = None,
|
|
760
|
+
char_pr_id_ref: str | int | None = None,
|
|
761
|
+
) -> int:
|
|
762
|
+
"""Append tracked inserted *text* to *paragraph* and return its change id."""
|
|
763
|
+
|
|
764
|
+
return _tracked.add_tracked_insert(
|
|
765
|
+
self,
|
|
766
|
+
paragraph,
|
|
767
|
+
text,
|
|
768
|
+
author=author,
|
|
769
|
+
date=date,
|
|
770
|
+
char_pr_id_ref=char_pr_id_ref,
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
@_moved("doc.tracking.replace")
|
|
774
|
+
def add_tracked_replace(
|
|
775
|
+
self,
|
|
776
|
+
paragraph: HwpxOxmlParagraph,
|
|
777
|
+
old: str,
|
|
778
|
+
new: str,
|
|
779
|
+
*,
|
|
780
|
+
author: str = "AI Agent",
|
|
781
|
+
date: str | None = None,
|
|
782
|
+
) -> tuple[int, int]:
|
|
783
|
+
"""Represent a replacement as tracked delete of *old* plus tracked insert of *new*."""
|
|
784
|
+
|
|
785
|
+
return _tracked.add_tracked_replace(
|
|
786
|
+
self,
|
|
787
|
+
paragraph,
|
|
788
|
+
old,
|
|
789
|
+
new,
|
|
790
|
+
author=author,
|
|
791
|
+
date=date,
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
@_moved("doc.notes.attach")
|
|
795
|
+
def attach_memo_field(
|
|
796
|
+
self,
|
|
797
|
+
paragraph: HwpxOxmlParagraph,
|
|
798
|
+
memo: HwpxOxmlMemo,
|
|
799
|
+
*,
|
|
800
|
+
field_id: str | None = None,
|
|
801
|
+
author: str | None = None,
|
|
802
|
+
created: datetime | str | None = None,
|
|
803
|
+
number: int = 1,
|
|
804
|
+
char_pr_id_ref: str | int | None = None,
|
|
805
|
+
) -> str:
|
|
806
|
+
"""Attach a MEMO field control to *paragraph* so Hangul shows *memo*."""
|
|
807
|
+
|
|
808
|
+
return _memos.attach_memo_field(
|
|
809
|
+
self,
|
|
810
|
+
paragraph,
|
|
811
|
+
memo,
|
|
812
|
+
field_id=field_id,
|
|
813
|
+
author=author,
|
|
814
|
+
created=created,
|
|
815
|
+
number=number,
|
|
816
|
+
char_pr_id_ref=char_pr_id_ref,
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
@_moved("doc.styles.border_fill")
|
|
820
|
+
def border_fill(self, border_fill_id_ref: int | str | None) -> GenericElement | None:
|
|
821
|
+
"""Return the border fill definition referenced by *border_fill_id_ref*."""
|
|
822
|
+
|
|
823
|
+
return self._root.border_fill(border_fill_id_ref)
|
|
824
|
+
|
|
825
|
+
@property
|
|
826
|
+
@_moved("doc.styles.border_fills")
|
|
827
|
+
def border_fills(self) -> dict[str, GenericElement]:
|
|
828
|
+
"""Return border fill definitions declared in the headers."""
|
|
829
|
+
|
|
830
|
+
return self._root.border_fills
|
|
831
|
+
|
|
832
|
+
@_moved("doc.styles.bullet")
|
|
833
|
+
def bullet(self, bullet_id_ref: int | str | None) -> Bullet | None:
|
|
834
|
+
"""Return the bullet definition referenced by *bullet_id_ref*."""
|
|
835
|
+
|
|
836
|
+
return self._root.bullet(bullet_id_ref)
|
|
837
|
+
|
|
838
|
+
@property
|
|
839
|
+
@_moved("doc.styles.bullets")
|
|
840
|
+
def bullets(self) -> dict[str, Bullet]:
|
|
841
|
+
"""Return bullet definitions declared in header reference lists."""
|
|
842
|
+
|
|
843
|
+
return self._root.bullets
|
|
844
|
+
|
|
845
|
+
@property
|
|
846
|
+
@_moved("doc.styles.char_properties")
|
|
847
|
+
def char_properties(self) -> dict[str, RunStyle]:
|
|
848
|
+
"""Return the resolved character style definitions available to the document."""
|
|
849
|
+
|
|
850
|
+
return self._root.char_properties
|
|
851
|
+
|
|
852
|
+
@_moved("doc.styles.char_property")
|
|
853
|
+
def char_property(self, char_pr_id_ref: int | str | None) -> RunStyle | None:
|
|
854
|
+
"""Return the style referenced by *char_pr_id_ref* if known."""
|
|
855
|
+
|
|
856
|
+
return self._root.char_property(char_pr_id_ref)
|
|
857
|
+
|
|
858
|
+
@_moved("doc.styles.ensure_border_fill")
|
|
859
|
+
def ensure_border_fill(
|
|
860
|
+
self,
|
|
861
|
+
*,
|
|
862
|
+
border_color: str = "#BFBFBF",
|
|
863
|
+
border_width: str = "0.12 mm",
|
|
864
|
+
fill_color: str | None = None,
|
|
865
|
+
active_borders: Sequence[str] | None = None,
|
|
866
|
+
border_type: str = "SOLID",
|
|
867
|
+
) -> str:
|
|
868
|
+
"""Return a borderFill id matching the requested border/fill attributes.
|
|
869
|
+
|
|
870
|
+
``border_type`` selects the OWPML line style (``SOLID``, ``DASH``,
|
|
871
|
+
``DOT``, ``DOUBLE_SLIM``, ``WAVE``, …); values outside the OWPML
|
|
872
|
+
vocabulary are rejected.
|
|
873
|
+
"""
|
|
874
|
+
|
|
875
|
+
return self._root.ensure_border_fill(
|
|
876
|
+
border_color=border_color,
|
|
877
|
+
border_width=border_width,
|
|
878
|
+
fill_color=fill_color,
|
|
879
|
+
active_borders=active_borders,
|
|
880
|
+
border_type=border_type,
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
@_moved("doc.styles.ensure_numbering")
|
|
884
|
+
def ensure_numbering(
|
|
885
|
+
self,
|
|
886
|
+
*,
|
|
887
|
+
kind: str,
|
|
888
|
+
levels: Sequence[dict[str, str]] | None = None,
|
|
889
|
+
) -> list[str]:
|
|
890
|
+
"""Return paragraph property ids for bullet or numbered-list levels."""
|
|
891
|
+
|
|
892
|
+
return self._root.ensure_numbering(kind=kind, levels=levels)
|
|
893
|
+
|
|
894
|
+
@_moved("doc.styles.ensure_run")
|
|
895
|
+
def ensure_run_style(
|
|
896
|
+
self,
|
|
897
|
+
*,
|
|
898
|
+
bold: bool = False,
|
|
899
|
+
italic: bool = False,
|
|
900
|
+
underline: bool = False,
|
|
901
|
+
color: str | None = None,
|
|
902
|
+
font: str | None = None,
|
|
903
|
+
size: int | float | None = None,
|
|
904
|
+
highlight: str | None = None,
|
|
905
|
+
strike: bool | None = None,
|
|
906
|
+
underline_shape: str | None = None,
|
|
907
|
+
underline_color: str | None = None,
|
|
908
|
+
strike_shape: str | None = None,
|
|
909
|
+
ratio: int | None = None,
|
|
910
|
+
letter_spacing: int | None = None,
|
|
911
|
+
shadow: str | None = None,
|
|
912
|
+
script: str | None = None,
|
|
913
|
+
base_char_pr_id: str | int | None = None,
|
|
914
|
+
) -> str:
|
|
915
|
+
"""Return a ``charPr`` identifier matching the requested flags.
|
|
916
|
+
|
|
917
|
+
5.4.0 additions (render-verified vocabulary; invalid values are
|
|
918
|
+
rejected): ``underline_shape``/``underline_color``, ``strike_shape``,
|
|
919
|
+
``ratio`` (장평 %), ``letter_spacing`` (자간 %), ``shadow`` (drop
|
|
920
|
+
shadow colour), ``script`` (``"sup"``/``"sub"``).
|
|
921
|
+
"""
|
|
922
|
+
|
|
923
|
+
return self._root.ensure_run_style(
|
|
924
|
+
bold=bold,
|
|
925
|
+
italic=italic,
|
|
926
|
+
underline=underline,
|
|
927
|
+
color=color,
|
|
928
|
+
font=font,
|
|
929
|
+
size=size,
|
|
930
|
+
highlight=highlight,
|
|
931
|
+
strike=strike,
|
|
932
|
+
underline_shape=underline_shape,
|
|
933
|
+
underline_color=underline_color,
|
|
934
|
+
strike_shape=strike_shape,
|
|
935
|
+
ratio=ratio,
|
|
936
|
+
letter_spacing=letter_spacing,
|
|
937
|
+
shadow=shadow,
|
|
938
|
+
script=script,
|
|
939
|
+
base_char_pr_id=base_char_pr_id,
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
@_moved("doc.text.html")
|
|
943
|
+
def export_html(self, **kwargs: object) -> str:
|
|
944
|
+
"""Export content as HTML. Keyword args forwarded to :func:`~hwpx.tools.exporter.export_html`."""
|
|
945
|
+
|
|
946
|
+
return _persistence.export_html(
|
|
947
|
+
self,
|
|
948
|
+
**kwargs,
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
@_moved("doc.text.markdown")
|
|
952
|
+
def export_markdown(self, **kwargs: object) -> str:
|
|
953
|
+
"""Export content as Markdown. Keyword args forwarded to :func:`~hwpx.tools.exporter.export_markdown`."""
|
|
954
|
+
|
|
955
|
+
return _persistence.export_markdown(
|
|
956
|
+
self,
|
|
957
|
+
**kwargs,
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
@_moved("doc.text.markdown(rich=True)")
|
|
961
|
+
def export_rich_markdown(self, **kwargs: object) -> str:
|
|
962
|
+
"""Export rich Markdown preserving inline styles, tables, footnotes, hyperlinks, images, and shape text.
|
|
963
|
+
|
|
964
|
+
Keyword args forwarded to :func:`~hwpx.tools.markdown_export.export_markdown`.
|
|
965
|
+
"""
|
|
966
|
+
|
|
967
|
+
return _persistence.export_rich_markdown(
|
|
968
|
+
self,
|
|
969
|
+
**kwargs,
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
@_moved("doc.text.plain")
|
|
973
|
+
def export_text(self, **kwargs: object) -> str:
|
|
974
|
+
"""Export content as plain text. Keyword args forwarded to :func:`~hwpx.tools.exporter.export_text`."""
|
|
975
|
+
|
|
976
|
+
return _persistence.export_text(
|
|
977
|
+
self,
|
|
978
|
+
**kwargs,
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
@_moved("doc.tables.fill_by_path")
|
|
982
|
+
def fill_by_path(
|
|
983
|
+
self,
|
|
984
|
+
mappings: Mapping[str, str],
|
|
985
|
+
) -> TableFillResult:
|
|
986
|
+
"""Fill table cells using ``label > direction > ...`` navigation paths."""
|
|
987
|
+
|
|
988
|
+
return _fields.fill_by_path(self, mappings)
|
|
989
|
+
|
|
990
|
+
@_moved("doc.fields.fill")
|
|
991
|
+
def fill_form_field(
|
|
992
|
+
self,
|
|
993
|
+
value: str,
|
|
994
|
+
*,
|
|
995
|
+
field_index: int | None = None,
|
|
996
|
+
field_id: str | None = None,
|
|
997
|
+
name: str | None = None,
|
|
998
|
+
fit_policy: "FitPolicy | None" = None,
|
|
999
|
+
box_width: int | None = None,
|
|
1000
|
+
font_pt: float | None = None,
|
|
1001
|
+
) -> dict[str, Any]:
|
|
1002
|
+
"""Fill a native form/click-here field while preserving surrounding runs.
|
|
1003
|
+
|
|
1004
|
+
When *fit_policy* and *box_width* (the field's usable width, in HWPUNIT)
|
|
1005
|
+
are supplied, the value is run through the FormFit engine (plan §2 C): it
|
|
1006
|
+
is measured against the box and may be shrunk/truncated, the inserted run
|
|
1007
|
+
is re-pointed at a smaller ``charPr`` for a real (oracle-visible) shrink,
|
|
1008
|
+
and the response carries a ``fit`` verdict with ``ok`` propagated from it.
|
|
1009
|
+
Without a box width a native field has no reliable geometry, so the fit is
|
|
1010
|
+
reported low-confidence and never hard-fails (measurement honesty).
|
|
1011
|
+
"""
|
|
1012
|
+
|
|
1013
|
+
return _fields.fill_form_field(
|
|
1014
|
+
self,
|
|
1015
|
+
value,
|
|
1016
|
+
field_index=field_index,
|
|
1017
|
+
field_id=field_id,
|
|
1018
|
+
name=name,
|
|
1019
|
+
fit_policy=fit_policy,
|
|
1020
|
+
box_width=box_width,
|
|
1021
|
+
font_pt=font_pt,
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
@_moved("doc.tables.find_cell_by_label")
|
|
1025
|
+
def find_cell_by_label(
|
|
1026
|
+
self,
|
|
1027
|
+
label_text: str,
|
|
1028
|
+
direction: str = "right",
|
|
1029
|
+
) -> TableLabelSearchResult:
|
|
1030
|
+
"""Return every label/target cell pair that matches *label_text*."""
|
|
1031
|
+
|
|
1032
|
+
from ..tools.table_navigation import find_cell_by_label
|
|
1033
|
+
|
|
1034
|
+
return find_cell_by_label(
|
|
1035
|
+
self,
|
|
1036
|
+
label_text,
|
|
1037
|
+
direction=cast("SearchDirection", direction),
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
@_moved("doc.text.find_runs")
|
|
1041
|
+
def find_runs_by_style(
|
|
1042
|
+
self,
|
|
1043
|
+
*,
|
|
1044
|
+
text_color: str | None = None,
|
|
1045
|
+
underline_type: str | None = None,
|
|
1046
|
+
underline_color: str | None = None,
|
|
1047
|
+
char_pr_id_ref: str | int | None = None,
|
|
1048
|
+
) -> list[HwpxOxmlRun]:
|
|
1049
|
+
"""Return runs matching the requested style criteria."""
|
|
1050
|
+
|
|
1051
|
+
matches: list[HwpxOxmlRun] = []
|
|
1052
|
+
target_char = str(char_pr_id_ref).strip() if char_pr_id_ref is not None else None
|
|
1053
|
+
|
|
1054
|
+
for run in self.iter_runs():
|
|
1055
|
+
if target_char is not None:
|
|
1056
|
+
run_char = (run.char_pr_id_ref or "").strip()
|
|
1057
|
+
if run_char != target_char:
|
|
1058
|
+
continue
|
|
1059
|
+
style = run.style
|
|
1060
|
+
if text_color is not None:
|
|
1061
|
+
if style is None or style.text_color() != text_color:
|
|
1062
|
+
continue
|
|
1063
|
+
if underline_type is not None:
|
|
1064
|
+
if style is None or style.underline_type() != underline_type:
|
|
1065
|
+
continue
|
|
1066
|
+
if underline_color is not None:
|
|
1067
|
+
if style is None or style.underline_color() != underline_color:
|
|
1068
|
+
continue
|
|
1069
|
+
matches.append(run)
|
|
1070
|
+
return matches
|
|
1071
|
+
|
|
1072
|
+
@_moved("doc.tables.map")
|
|
1073
|
+
def get_table_map(self) -> TableMapResult:
|
|
1074
|
+
"""Return compact metadata for every table in document order."""
|
|
1075
|
+
|
|
1076
|
+
from ..tools.table_navigation import get_table_map
|
|
1077
|
+
|
|
1078
|
+
return get_table_map(self)
|
|
1079
|
+
|
|
1080
|
+
@property
|
|
1081
|
+
@_moved("doc.parts.headers")
|
|
1082
|
+
def headers(self) -> list[HwpxOxmlHeader]:
|
|
1083
|
+
"""Return the header parts referenced by the document."""
|
|
1084
|
+
return self._root.headers
|
|
1085
|
+
|
|
1086
|
+
@property
|
|
1087
|
+
@_moved("doc.parts.histories")
|
|
1088
|
+
def histories(self) -> list[HwpxOxmlHistory]:
|
|
1089
|
+
"""Return document history parts referenced by the manifest."""
|
|
1090
|
+
return self._root.histories
|
|
1091
|
+
|
|
1092
|
+
@_moved("doc.text.runs")
|
|
1093
|
+
def iter_runs(self) -> Iterator[HwpxOxmlRun]:
|
|
1094
|
+
"""Yield every run element contained in the document."""
|
|
1095
|
+
|
|
1096
|
+
for paragraph in self.paragraphs:
|
|
1097
|
+
for run in paragraph.runs:
|
|
1098
|
+
yield run
|
|
1099
|
+
|
|
1100
|
+
@_moved("doc.fields.check_boxes")
|
|
1101
|
+
def list_check_boxes(self) -> list[dict[str, Any]]:
|
|
1102
|
+
"""Return check-box form objects (체크박스) in document order.
|
|
1103
|
+
|
|
1104
|
+
Each entry carries ``index``/``name``/``caption``/``checked``.
|
|
1105
|
+
"""
|
|
1106
|
+
|
|
1107
|
+
return _fields.list_check_boxes(self)
|
|
1108
|
+
|
|
1109
|
+
@_moved("doc.fields.all")
|
|
1110
|
+
def list_form_fields(self) -> list[dict[str, Any]]:
|
|
1111
|
+
"""Return native form/click-here fields in document order.
|
|
1112
|
+
|
|
1113
|
+
The result intentionally excludes memo and hyperlink fields because
|
|
1114
|
+
those are annotation/navigation mechanisms rather than fillable form
|
|
1115
|
+
slots.
|
|
1116
|
+
"""
|
|
1117
|
+
|
|
1118
|
+
return _fields.list_form_fields(self)
|
|
1119
|
+
|
|
1120
|
+
@_moved("doc.media.images")
|
|
1121
|
+
def list_images(self) -> list[dict[str, str]]:
|
|
1122
|
+
"""Return metadata dicts for all embedded binary data items.
|
|
1123
|
+
|
|
1124
|
+
Each dict contains the ``<hh:binItem>`` attributes (``id``, ``Type``,
|
|
1125
|
+
``BinData``, ``Format``, …).
|
|
1126
|
+
"""
|
|
1127
|
+
|
|
1128
|
+
return _media.list_images(self)
|
|
1129
|
+
|
|
1130
|
+
@property
|
|
1131
|
+
@_moved("doc.parts.master_pages")
|
|
1132
|
+
def master_pages(self) -> list[HwpxOxmlMasterPage]:
|
|
1133
|
+
"""Return the master-page parts declared in the manifest."""
|
|
1134
|
+
return self._root.master_pages
|
|
1135
|
+
|
|
1136
|
+
@_moved("doc.styles.memo_shape")
|
|
1137
|
+
def memo_shape(self, memo_shape_id_ref: int | str | None) -> MemoShape | None:
|
|
1138
|
+
"""Return the memo shape definition referenced by *memo_shape_id_ref*."""
|
|
1139
|
+
|
|
1140
|
+
return self._root.memo_shape(memo_shape_id_ref)
|
|
1141
|
+
|
|
1142
|
+
@property
|
|
1143
|
+
@_moved("doc.styles.memo_shapes")
|
|
1144
|
+
def memo_shapes(self) -> dict[str, MemoShape]:
|
|
1145
|
+
"""Return memo shapes available in the header reference lists."""
|
|
1146
|
+
|
|
1147
|
+
return self._root.memo_shapes
|
|
1148
|
+
|
|
1149
|
+
@property
|
|
1150
|
+
@_moved("doc.notes.memos")
|
|
1151
|
+
def memos(self) -> list[HwpxOxmlMemo]:
|
|
1152
|
+
"""Return all memo entries declared in every section."""
|
|
1153
|
+
|
|
1154
|
+
memos: list[HwpxOxmlMemo] = []
|
|
1155
|
+
for section in self._root.sections:
|
|
1156
|
+
memos.extend(section.memos)
|
|
1157
|
+
return memos
|
|
1158
|
+
|
|
1159
|
+
@_moved("doc.tables.merge_cells")
|
|
1160
|
+
def merge_table_cells(
|
|
1161
|
+
self,
|
|
1162
|
+
table: HwpxOxmlTable,
|
|
1163
|
+
cell_range: str,
|
|
1164
|
+
) -> Any:
|
|
1165
|
+
"""Merge a table cell range using spreadsheet notation such as ``A1:C1``."""
|
|
1166
|
+
|
|
1167
|
+
return table.merge_cells(cell_range)
|
|
1168
|
+
|
|
1169
|
+
@property
|
|
1170
|
+
@_moved("doc.styles.paragraph_properties")
|
|
1171
|
+
def paragraph_properties(self) -> dict[str, ParagraphProperty]:
|
|
1172
|
+
"""Return paragraph property definitions declared in headers."""
|
|
1173
|
+
|
|
1174
|
+
return self._root.paragraph_properties
|
|
1175
|
+
|
|
1176
|
+
@_moved("doc.styles.paragraph_property")
|
|
1177
|
+
def paragraph_property(
|
|
1178
|
+
self, para_pr_id_ref: int | str | None
|
|
1179
|
+
) -> ParagraphProperty | None:
|
|
1180
|
+
"""Return the paragraph property referenced by *para_pr_id_ref*."""
|
|
1181
|
+
|
|
1182
|
+
return self._root.paragraph_property(para_pr_id_ref)
|
|
1183
|
+
|
|
1184
|
+
@_moved("doc.media.picture_references")
|
|
1185
|
+
def picture_references(self) -> list[dict[str, Any]]:
|
|
1186
|
+
"""Return body picture references in document order."""
|
|
1187
|
+
|
|
1188
|
+
return _media.picture_references(self)
|
|
1189
|
+
|
|
1190
|
+
@_moved("doc.page.remove_footer")
|
|
1191
|
+
def remove_footer(
|
|
1192
|
+
self,
|
|
1193
|
+
*,
|
|
1194
|
+
section: HwpxOxmlSection | None = None,
|
|
1195
|
+
section_index: int | None = None,
|
|
1196
|
+
page_type: str = "BOTH",
|
|
1197
|
+
) -> None:
|
|
1198
|
+
"""Remove the footer linked to *page_type* from the requested section if present."""
|
|
1199
|
+
|
|
1200
|
+
return _layout.remove_footer(
|
|
1201
|
+
self,
|
|
1202
|
+
section=section,
|
|
1203
|
+
section_index=section_index,
|
|
1204
|
+
page_type=page_type,
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1207
|
+
@_moved("doc.page.remove_header")
|
|
1208
|
+
def remove_header(
|
|
1209
|
+
self,
|
|
1210
|
+
*,
|
|
1211
|
+
section: HwpxOxmlSection | None = None,
|
|
1212
|
+
section_index: int | None = None,
|
|
1213
|
+
page_type: str = "BOTH",
|
|
1214
|
+
) -> None:
|
|
1215
|
+
"""Remove the header linked to *page_type* from the requested section if present."""
|
|
1216
|
+
|
|
1217
|
+
return _layout.remove_header(
|
|
1218
|
+
self,
|
|
1219
|
+
section=section,
|
|
1220
|
+
section_index=section_index,
|
|
1221
|
+
page_type=page_type,
|
|
1222
|
+
)
|
|
1223
|
+
|
|
1224
|
+
@_moved("doc.media.remove_image")
|
|
1225
|
+
def remove_image(self, item_id: str) -> bool:
|
|
1226
|
+
"""Remove an embedded image by its manifest item id.
|
|
1227
|
+
|
|
1228
|
+
This removes the binary data from the ZIP, the manifest entry, and
|
|
1229
|
+
the header binItem entry.
|
|
1230
|
+
|
|
1231
|
+
Returns:
|
|
1232
|
+
``True`` if any component was removed.
|
|
1233
|
+
"""
|
|
1234
|
+
|
|
1235
|
+
return _media.remove_image(
|
|
1236
|
+
self,
|
|
1237
|
+
item_id=item_id,
|
|
1238
|
+
)
|
|
1239
|
+
|
|
1240
|
+
@_moved("doc.notes.remove_memo")
|
|
1241
|
+
def remove_memo(self, memo: HwpxOxmlMemo) -> None:
|
|
1242
|
+
"""Remove *memo* from the section it belongs to."""
|
|
1243
|
+
|
|
1244
|
+
return _memos.remove_memo(self, memo)
|
|
1245
|
+
|
|
1246
|
+
@_moved("paragraph.remove()")
|
|
1247
|
+
def remove_paragraph(
|
|
1248
|
+
self,
|
|
1249
|
+
paragraph: HwpxOxmlParagraph | int,
|
|
1250
|
+
*,
|
|
1251
|
+
section: HwpxOxmlSection | None = None,
|
|
1252
|
+
section_index: int | None = None,
|
|
1253
|
+
) -> None:
|
|
1254
|
+
"""Remove a paragraph from the document.
|
|
1255
|
+
|
|
1256
|
+
*paragraph* may be a :class:`HwpxOxmlParagraph` instance or an
|
|
1257
|
+
integer index into the paragraphs of the specified (or last)
|
|
1258
|
+
section.
|
|
1259
|
+
|
|
1260
|
+
Raises ``ValueError`` if the target section would become empty.
|
|
1261
|
+
"""
|
|
1262
|
+
self._root.remove_paragraph(
|
|
1263
|
+
paragraph,
|
|
1264
|
+
section=section,
|
|
1265
|
+
section_index=section_index,
|
|
1266
|
+
)
|
|
1267
|
+
|
|
1268
|
+
@_moved("doc.media.replace_picture")
|
|
1269
|
+
def replace_picture(
|
|
1270
|
+
self,
|
|
1271
|
+
image_data: bytes,
|
|
1272
|
+
image_format: str,
|
|
1273
|
+
*,
|
|
1274
|
+
picture_index: int = 0,
|
|
1275
|
+
binary_item_id_ref: str | None = None,
|
|
1276
|
+
remove_orphaned: bool = True,
|
|
1277
|
+
item_id: str | None = None,
|
|
1278
|
+
) -> dict[str, Any]:
|
|
1279
|
+
"""Replace a body picture's image asset while preserving its geometry.
|
|
1280
|
+
|
|
1281
|
+
The existing ``<hp:pic>`` element is left in place. Only the child
|
|
1282
|
+
``<hc:img>`` ``binaryItemIDRef`` is changed, so size, position, crop,
|
|
1283
|
+
rotation, and wrapping geometry remain untouched.
|
|
1284
|
+
"""
|
|
1285
|
+
|
|
1286
|
+
return _media.replace_picture(
|
|
1287
|
+
self,
|
|
1288
|
+
image_data=image_data,
|
|
1289
|
+
image_format=image_format,
|
|
1290
|
+
picture_index=picture_index,
|
|
1291
|
+
binary_item_id_ref=binary_item_id_ref,
|
|
1292
|
+
remove_orphaned=remove_orphaned,
|
|
1293
|
+
item_id=item_id,
|
|
1294
|
+
)
|
|
1295
|
+
|
|
1296
|
+
@_moved("doc.text.replace")
|
|
1297
|
+
def replace_text_in_runs(
|
|
1298
|
+
self,
|
|
1299
|
+
search: str,
|
|
1300
|
+
replacement: str,
|
|
1301
|
+
*,
|
|
1302
|
+
text_color: str | None = None,
|
|
1303
|
+
underline_type: str | None = None,
|
|
1304
|
+
underline_color: str | None = None,
|
|
1305
|
+
char_pr_id_ref: str | int | None = None,
|
|
1306
|
+
limit: int | None = None,
|
|
1307
|
+
) -> int:
|
|
1308
|
+
"""Replace occurrences of *search* in runs matching the provided style filters."""
|
|
1309
|
+
|
|
1310
|
+
if not search:
|
|
1311
|
+
raise HwpxValueError(
|
|
1312
|
+
"search must be a non-empty string",
|
|
1313
|
+
code="text-search-empty",
|
|
1314
|
+
context={"search": search},
|
|
1315
|
+
suggestion="Pass the substring to replace.",
|
|
1316
|
+
)
|
|
1317
|
+
|
|
1318
|
+
replacements = 0
|
|
1319
|
+
runs = self.find_runs_by_style(
|
|
1320
|
+
text_color=text_color,
|
|
1321
|
+
underline_type=underline_type,
|
|
1322
|
+
underline_color=underline_color,
|
|
1323
|
+
char_pr_id_ref=char_pr_id_ref,
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
for run in runs:
|
|
1327
|
+
remaining = None
|
|
1328
|
+
if limit is not None:
|
|
1329
|
+
remaining = limit - replacements
|
|
1330
|
+
if remaining <= 0:
|
|
1331
|
+
break
|
|
1332
|
+
original_char_pr = run.char_pr_id_ref
|
|
1333
|
+
replaced_here = run.replace_text(
|
|
1334
|
+
search,
|
|
1335
|
+
replacement,
|
|
1336
|
+
count=remaining,
|
|
1337
|
+
)
|
|
1338
|
+
if replaced_here and original_char_pr is not None:
|
|
1339
|
+
# Ensure the run retains its original formatting reference even
|
|
1340
|
+
# if XML nodes were rewritten during substitution.
|
|
1341
|
+
run.char_pr_id_ref = original_char_pr
|
|
1342
|
+
replacements += replaced_here
|
|
1343
|
+
if limit is not None and replacements >= limit:
|
|
1344
|
+
break
|
|
1345
|
+
return replacements
|
|
1346
|
+
|
|
1347
|
+
@_moved("doc.fields.check_box(...).checked")
|
|
1348
|
+
def set_check_box(
|
|
1349
|
+
self,
|
|
1350
|
+
checked: bool,
|
|
1351
|
+
*,
|
|
1352
|
+
index: int | None = None,
|
|
1353
|
+
name: str | None = None,
|
|
1354
|
+
) -> dict[str, Any]:
|
|
1355
|
+
"""Set a check box's state, selecting it by ``index`` or ``name``.
|
|
1356
|
+
|
|
1357
|
+
Exactly one selector is required; an ambiguous name is refused rather
|
|
1358
|
+
than guessed.
|
|
1359
|
+
"""
|
|
1360
|
+
|
|
1361
|
+
return _fields.set_check_box(self, checked, index=index, name=name)
|
|
1362
|
+
|
|
1363
|
+
@_moved("doc.page.set_columns")
|
|
1364
|
+
def set_columns(
|
|
1365
|
+
self,
|
|
1366
|
+
col_count: int = 2,
|
|
1367
|
+
*,
|
|
1368
|
+
col_type: str = "NEWSPAPER",
|
|
1369
|
+
layout: str = "LEFT",
|
|
1370
|
+
same_size: bool = True,
|
|
1371
|
+
same_gap: int = 1200,
|
|
1372
|
+
column_widths: "Sequence[tuple[int, int]] | None" = None,
|
|
1373
|
+
separator_type: str | None = None,
|
|
1374
|
+
separator_width: str | None = None,
|
|
1375
|
+
separator_color: str | None = None,
|
|
1376
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
1377
|
+
section: HwpxOxmlSection | None = None,
|
|
1378
|
+
section_index: int | None = None,
|
|
1379
|
+
) -> HwpxOxmlInlineObject:
|
|
1380
|
+
"""Insert a column definition control.
|
|
1381
|
+
|
|
1382
|
+
This adds a ``<hp:ctrl><hp:colPr>`` element to the specified paragraph.
|
|
1383
|
+
Text that follows will be laid out in the specified number of columns.
|
|
1384
|
+
|
|
1385
|
+
Args:
|
|
1386
|
+
col_count: Number of columns (1–255).
|
|
1387
|
+
col_type: ``NEWSPAPER``, ``BALANCED_NEWSPAPER``, or ``PARALLEL``.
|
|
1388
|
+
same_gap: Gap in HWPUNIT (7200 = 1 inch).
|
|
1389
|
+
separator_type: Optional column separator line type (e.g. ``SOLID``).
|
|
1390
|
+
"""
|
|
1391
|
+
|
|
1392
|
+
return _layout.set_columns(
|
|
1393
|
+
self,
|
|
1394
|
+
col_count=col_count,
|
|
1395
|
+
col_type=col_type,
|
|
1396
|
+
layout=layout,
|
|
1397
|
+
same_size=same_size,
|
|
1398
|
+
same_gap=same_gap,
|
|
1399
|
+
column_widths=column_widths,
|
|
1400
|
+
separator_type=separator_type,
|
|
1401
|
+
separator_width=separator_width,
|
|
1402
|
+
separator_color=separator_color,
|
|
1403
|
+
paragraph=paragraph,
|
|
1404
|
+
section=section,
|
|
1405
|
+
section_index=section_index,
|
|
1406
|
+
)
|
|
1407
|
+
|
|
1408
|
+
@_moved("doc.page.set_footer(content=...)")
|
|
1409
|
+
def set_footer_content(
|
|
1410
|
+
self,
|
|
1411
|
+
content: Sequence[Mapping[str, Any]],
|
|
1412
|
+
*,
|
|
1413
|
+
section: HwpxOxmlSection | None = None,
|
|
1414
|
+
section_index: int | None = None,
|
|
1415
|
+
page_type: str = "BOTH",
|
|
1416
|
+
) -> HwpxOxmlSectionHeaderFooter:
|
|
1417
|
+
"""Ensure the requested section contains a rich footer for *page_type*."""
|
|
1418
|
+
|
|
1419
|
+
return _layout.set_footer_content(
|
|
1420
|
+
self,
|
|
1421
|
+
content=content,
|
|
1422
|
+
section=section,
|
|
1423
|
+
section_index=section_index,
|
|
1424
|
+
page_type=page_type,
|
|
1425
|
+
)
|
|
1426
|
+
|
|
1427
|
+
@_moved("doc.page.set_footer(text=...)")
|
|
1428
|
+
def set_footer_text(
|
|
1429
|
+
self,
|
|
1430
|
+
text: str,
|
|
1431
|
+
*,
|
|
1432
|
+
section: HwpxOxmlSection | None = None,
|
|
1433
|
+
section_index: int | None = None,
|
|
1434
|
+
page_type: str = "BOTH",
|
|
1435
|
+
) -> HwpxOxmlSectionHeaderFooter:
|
|
1436
|
+
"""Ensure the requested section contains a footer for *page_type* and set its text."""
|
|
1437
|
+
|
|
1438
|
+
return _layout.set_footer_text(
|
|
1439
|
+
self,
|
|
1440
|
+
text=text,
|
|
1441
|
+
section=section,
|
|
1442
|
+
section_index=section_index,
|
|
1443
|
+
page_type=page_type,
|
|
1444
|
+
)
|
|
1445
|
+
|
|
1446
|
+
@_moved("doc.page.set_header(content=...)")
|
|
1447
|
+
def set_header_content(
|
|
1448
|
+
self,
|
|
1449
|
+
content: Sequence[Mapping[str, Any]],
|
|
1450
|
+
*,
|
|
1451
|
+
section: HwpxOxmlSection | None = None,
|
|
1452
|
+
section_index: int | None = None,
|
|
1453
|
+
page_type: str = "BOTH",
|
|
1454
|
+
) -> HwpxOxmlSectionHeaderFooter:
|
|
1455
|
+
"""Ensure the requested section contains a rich header for *page_type*."""
|
|
1456
|
+
|
|
1457
|
+
return _layout.set_header_content(
|
|
1458
|
+
self,
|
|
1459
|
+
content=content,
|
|
1460
|
+
section=section,
|
|
1461
|
+
section_index=section_index,
|
|
1462
|
+
page_type=page_type,
|
|
1463
|
+
)
|
|
1464
|
+
|
|
1465
|
+
@_moved("doc.page.set_header / doc.page.set_footer")
|
|
1466
|
+
def set_header_footer(
|
|
1467
|
+
self,
|
|
1468
|
+
*,
|
|
1469
|
+
kind: str,
|
|
1470
|
+
text: str | None = None,
|
|
1471
|
+
content: Sequence[Mapping[str, Any]] | None = None,
|
|
1472
|
+
section: HwpxOxmlSection | None = None,
|
|
1473
|
+
section_index: int | None = None,
|
|
1474
|
+
page_type: str = "BOTH",
|
|
1475
|
+
) -> HwpxOxmlSectionHeaderFooter:
|
|
1476
|
+
"""Set a header or footer using plain text or rich content specs."""
|
|
1477
|
+
|
|
1478
|
+
return _layout.set_header_footer(
|
|
1479
|
+
self,
|
|
1480
|
+
kind=kind,
|
|
1481
|
+
text=text,
|
|
1482
|
+
content=content,
|
|
1483
|
+
section=section,
|
|
1484
|
+
section_index=section_index,
|
|
1485
|
+
page_type=page_type,
|
|
1486
|
+
)
|
|
1487
|
+
|
|
1488
|
+
@_moved("doc.page.set_header(text=...)")
|
|
1489
|
+
def set_header_text(
|
|
1490
|
+
self,
|
|
1491
|
+
text: str,
|
|
1492
|
+
*,
|
|
1493
|
+
section: HwpxOxmlSection | None = None,
|
|
1494
|
+
section_index: int | None = None,
|
|
1495
|
+
page_type: str = "BOTH",
|
|
1496
|
+
) -> HwpxOxmlSectionHeaderFooter:
|
|
1497
|
+
"""Ensure the requested section contains a header for *page_type* and set its text."""
|
|
1498
|
+
|
|
1499
|
+
return _layout.set_header_text(
|
|
1500
|
+
self,
|
|
1501
|
+
text=text,
|
|
1502
|
+
section=section,
|
|
1503
|
+
section_index=section_index,
|
|
1504
|
+
page_type=page_type,
|
|
1505
|
+
)
|
|
1506
|
+
|
|
1507
|
+
@_moved("doc.styles.apply_list_format")
|
|
1508
|
+
def set_list_format(
|
|
1509
|
+
self,
|
|
1510
|
+
*,
|
|
1511
|
+
paragraph_index: int | None = None,
|
|
1512
|
+
paragraph_indexes: Sequence[int] | None = None,
|
|
1513
|
+
kind: str = "bullet",
|
|
1514
|
+
level: int = 1,
|
|
1515
|
+
bullet_char: str | None = None,
|
|
1516
|
+
number_format: str | None = None,
|
|
1517
|
+
start: int | None = None,
|
|
1518
|
+
) -> dict[str, Any]:
|
|
1519
|
+
"""Apply bullet or numbered-list paragraph properties to paragraphs."""
|
|
1520
|
+
|
|
1521
|
+
return _layout.set_list_format(
|
|
1522
|
+
self,
|
|
1523
|
+
paragraph_index=paragraph_index,
|
|
1524
|
+
paragraph_indexes=paragraph_indexes,
|
|
1525
|
+
kind=kind,
|
|
1526
|
+
level=level,
|
|
1527
|
+
bullet_char=bullet_char,
|
|
1528
|
+
number_format=number_format,
|
|
1529
|
+
start=start,
|
|
1530
|
+
)
|
|
1531
|
+
|
|
1532
|
+
@_moved("doc.page.set_margins")
|
|
1533
|
+
def set_page_margins(
|
|
1534
|
+
self,
|
|
1535
|
+
*,
|
|
1536
|
+
left: int | None = None,
|
|
1537
|
+
right: int | None = None,
|
|
1538
|
+
top: int | None = None,
|
|
1539
|
+
bottom: int | None = None,
|
|
1540
|
+
header: int | None = None,
|
|
1541
|
+
footer: int | None = None,
|
|
1542
|
+
gutter: int | None = None,
|
|
1543
|
+
section: HwpxOxmlSection | None = None,
|
|
1544
|
+
section_index: int | None = None,
|
|
1545
|
+
) -> None:
|
|
1546
|
+
"""Set page margins on the requested section through the public facade."""
|
|
1547
|
+
|
|
1548
|
+
return _layout.set_page_margins(
|
|
1549
|
+
self,
|
|
1550
|
+
left=left,
|
|
1551
|
+
right=right,
|
|
1552
|
+
top=top,
|
|
1553
|
+
bottom=bottom,
|
|
1554
|
+
header=header,
|
|
1555
|
+
footer=footer,
|
|
1556
|
+
gutter=gutter,
|
|
1557
|
+
section=section,
|
|
1558
|
+
section_index=section_index,
|
|
1559
|
+
)
|
|
1560
|
+
|
|
1561
|
+
@_moved("doc.page.set_page_number")
|
|
1562
|
+
def set_page_number(
|
|
1563
|
+
self,
|
|
1564
|
+
*,
|
|
1565
|
+
target: str = "footer",
|
|
1566
|
+
page_type: str = "BOTH",
|
|
1567
|
+
format: str = "page",
|
|
1568
|
+
align: str = "CENTER",
|
|
1569
|
+
position: str = "BOTTOM_CENTER",
|
|
1570
|
+
prefix: str = "",
|
|
1571
|
+
suffix: str = "",
|
|
1572
|
+
format_type: str | None = None,
|
|
1573
|
+
section: HwpxOxmlSection | None = None,
|
|
1574
|
+
section_index: int | None = None,
|
|
1575
|
+
) -> HwpxOxmlSectionHeaderFooter:
|
|
1576
|
+
"""Replace header/footer content with an automatic page-number field."""
|
|
1577
|
+
|
|
1578
|
+
return _layout.set_page_number(
|
|
1579
|
+
self,
|
|
1580
|
+
target=target,
|
|
1581
|
+
page_type=page_type,
|
|
1582
|
+
format=format,
|
|
1583
|
+
align=align,
|
|
1584
|
+
position=position,
|
|
1585
|
+
prefix=prefix,
|
|
1586
|
+
suffix=suffix,
|
|
1587
|
+
format_type=format_type,
|
|
1588
|
+
section=section,
|
|
1589
|
+
section_index=section_index,
|
|
1590
|
+
)
|
|
1591
|
+
|
|
1592
|
+
@_moved("doc.page.setup")
|
|
1593
|
+
def set_page_setup(
|
|
1594
|
+
self,
|
|
1595
|
+
*,
|
|
1596
|
+
paper_size: str | None = None,
|
|
1597
|
+
width_mm: float | None = None,
|
|
1598
|
+
height_mm: float | None = None,
|
|
1599
|
+
orientation: str | None = None,
|
|
1600
|
+
margins_mm: Mapping[str, float] | None = None,
|
|
1601
|
+
margin_left_mm: float | None = None,
|
|
1602
|
+
margin_right_mm: float | None = None,
|
|
1603
|
+
margin_top_mm: float | None = None,
|
|
1604
|
+
margin_bottom_mm: float | None = None,
|
|
1605
|
+
header_margin_mm: float | None = None,
|
|
1606
|
+
footer_margin_mm: float | None = None,
|
|
1607
|
+
gutter_mm: float | None = None,
|
|
1608
|
+
columns: int | None = None,
|
|
1609
|
+
column_gap_mm: float | None = None,
|
|
1610
|
+
section: HwpxOxmlSection | None = None,
|
|
1611
|
+
section_index: int | None = None,
|
|
1612
|
+
) -> dict[str, Any]:
|
|
1613
|
+
"""Set page size, margins, orientation, and optional columns in human units."""
|
|
1614
|
+
|
|
1615
|
+
return _layout.set_page_setup(
|
|
1616
|
+
self,
|
|
1617
|
+
paper_size=paper_size,
|
|
1618
|
+
width_mm=width_mm,
|
|
1619
|
+
height_mm=height_mm,
|
|
1620
|
+
orientation=orientation,
|
|
1621
|
+
margins_mm=margins_mm,
|
|
1622
|
+
margin_left_mm=margin_left_mm,
|
|
1623
|
+
margin_right_mm=margin_right_mm,
|
|
1624
|
+
margin_top_mm=margin_top_mm,
|
|
1625
|
+
margin_bottom_mm=margin_bottom_mm,
|
|
1626
|
+
header_margin_mm=header_margin_mm,
|
|
1627
|
+
footer_margin_mm=footer_margin_mm,
|
|
1628
|
+
gutter_mm=gutter_mm,
|
|
1629
|
+
columns=columns,
|
|
1630
|
+
column_gap_mm=column_gap_mm,
|
|
1631
|
+
section=section,
|
|
1632
|
+
section_index=section_index,
|
|
1633
|
+
)
|
|
1634
|
+
|
|
1635
|
+
@_moved("doc.page.set_size")
|
|
1636
|
+
def set_page_size(
|
|
1637
|
+
self,
|
|
1638
|
+
*,
|
|
1639
|
+
width: int | None = None,
|
|
1640
|
+
height: int | None = None,
|
|
1641
|
+
orientation: str | None = None,
|
|
1642
|
+
gutter_type: str | None = None,
|
|
1643
|
+
section: HwpxOxmlSection | None = None,
|
|
1644
|
+
section_index: int | None = None,
|
|
1645
|
+
) -> None:
|
|
1646
|
+
"""Set page dimensions on the requested section through the public facade."""
|
|
1647
|
+
|
|
1648
|
+
return _layout.set_page_size(
|
|
1649
|
+
self,
|
|
1650
|
+
width=width,
|
|
1651
|
+
height=height,
|
|
1652
|
+
orientation=orientation,
|
|
1653
|
+
gutter_type=gutter_type,
|
|
1654
|
+
section=section,
|
|
1655
|
+
section_index=section_index,
|
|
1656
|
+
)
|
|
1657
|
+
|
|
1658
|
+
@_moved("doc.styles.apply_paragraph_format")
|
|
1659
|
+
def set_paragraph_format(
|
|
1660
|
+
self,
|
|
1661
|
+
*,
|
|
1662
|
+
paragraph_index: int | None = None,
|
|
1663
|
+
paragraph_indexes: Sequence[int] | None = None,
|
|
1664
|
+
alignment: str | None = None,
|
|
1665
|
+
line_spacing_percent: int | float | None = None,
|
|
1666
|
+
indent_left_mm: float | None = None,
|
|
1667
|
+
indent_right_mm: float | None = None,
|
|
1668
|
+
first_line_indent_mm: float | None = None,
|
|
1669
|
+
spacing_before_pt: float | None = None,
|
|
1670
|
+
spacing_after_pt: float | None = None,
|
|
1671
|
+
outline_level: int | None = None,
|
|
1672
|
+
keep_with_next: bool | None = None,
|
|
1673
|
+
keep_lines: bool | None = None,
|
|
1674
|
+
page_break_before: bool | None = None,
|
|
1675
|
+
bottom_border: bool = False,
|
|
1676
|
+
border_color: str = "#BFBFBF",
|
|
1677
|
+
border_width: str = "0.12 mm",
|
|
1678
|
+
) -> dict[str, Any]:
|
|
1679
|
+
"""Apply paragraph-level formatting using human units.
|
|
1680
|
+
|
|
1681
|
+
Millimetre inputs are converted to HWP units; paragraph spacing uses
|
|
1682
|
+
points; line spacing is stored as a percent value. ``keep_with_next`` /
|
|
1683
|
+
``keep_lines`` / ``page_break_before`` set the paragraph's keep-together
|
|
1684
|
+
(``<hh:breakSetting>``) flags via a freshly minted paraPr.
|
|
1685
|
+
"""
|
|
1686
|
+
|
|
1687
|
+
return _layout.set_paragraph_format(
|
|
1688
|
+
self,
|
|
1689
|
+
paragraph_index=paragraph_index,
|
|
1690
|
+
paragraph_indexes=paragraph_indexes,
|
|
1691
|
+
alignment=alignment,
|
|
1692
|
+
line_spacing_percent=line_spacing_percent,
|
|
1693
|
+
indent_left_mm=indent_left_mm,
|
|
1694
|
+
indent_right_mm=indent_right_mm,
|
|
1695
|
+
first_line_indent_mm=first_line_indent_mm,
|
|
1696
|
+
spacing_before_pt=spacing_before_pt,
|
|
1697
|
+
spacing_after_pt=spacing_after_pt,
|
|
1698
|
+
outline_level=outline_level,
|
|
1699
|
+
keep_with_next=keep_with_next,
|
|
1700
|
+
keep_lines=keep_lines,
|
|
1701
|
+
page_break_before=page_break_before,
|
|
1702
|
+
bottom_border=bottom_border,
|
|
1703
|
+
border_color=border_color,
|
|
1704
|
+
border_width=border_width,
|
|
1705
|
+
)
|
|
1706
|
+
|
|
1707
|
+
@_moved("doc.styles.style")
|
|
1708
|
+
def style(self, style_id_ref: int | str | None) -> Style | None:
|
|
1709
|
+
"""Return the style definition referenced by *style_id_ref*."""
|
|
1710
|
+
|
|
1711
|
+
return self._root.style(style_id_ref)
|
|
1712
|
+
|
|
1713
|
+
@_moved("doc.tracking.change")
|
|
1714
|
+
def track_change(self, change_id_ref: int | str | None) -> TrackChange | None:
|
|
1715
|
+
"""Return tracked change metadata referenced by *change_id_ref*."""
|
|
1716
|
+
|
|
1717
|
+
return self._root.track_change(change_id_ref)
|
|
1718
|
+
|
|
1719
|
+
@_moved("doc.tracking.author")
|
|
1720
|
+
def track_change_author(
|
|
1721
|
+
self, author_id_ref: int | str | None
|
|
1722
|
+
) -> TrackChangeAuthor | None:
|
|
1723
|
+
"""Return tracked change author details referenced by *author_id_ref*."""
|
|
1724
|
+
|
|
1725
|
+
return self._root.track_change_author(author_id_ref)
|
|
1726
|
+
|
|
1727
|
+
@property
|
|
1728
|
+
@_moved("doc.tracking.authors")
|
|
1729
|
+
def track_change_authors(self) -> dict[str, TrackChangeAuthor]:
|
|
1730
|
+
"""Return tracked change author metadata declared in the headers."""
|
|
1731
|
+
|
|
1732
|
+
return self._root.track_change_authors
|
|
1733
|
+
|
|
1734
|
+
@property
|
|
1735
|
+
@_moved("doc.tracking.changes")
|
|
1736
|
+
def track_changes(self) -> dict[str, TrackChange]:
|
|
1737
|
+
"""Return tracked change metadata declared in the headers."""
|
|
1738
|
+
|
|
1739
|
+
return self._root.track_changes
|
|
1740
|
+
|
|
1741
|
+
@property
|
|
1742
|
+
@_moved("doc.parts.version")
|
|
1743
|
+
def version(self) -> HwpxOxmlVersion | None:
|
|
1744
|
+
"""Return the version metadata part if present."""
|
|
1745
|
+
return self._root.version
|