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.
Files changed (54) hide show
  1. hwpx/_document/_legacy.py +1745 -0
  2. hwpx/_document/_resolve.py +173 -0
  3. hwpx/_document/fields.py +212 -62
  4. hwpx/_document/headings.py +187 -0
  5. hwpx/_document/layout.py +171 -59
  6. hwpx/_document/media.py +122 -38
  7. hwpx/_document/memos.py +49 -13
  8. hwpx/_document/ns/__init__.py +68 -0
  9. hwpx/_document/ns/_base.py +40 -0
  10. hwpx/_document/ns/fields.py +160 -0
  11. hwpx/_document/ns/media.py +92 -0
  12. hwpx/_document/ns/notes.py +210 -0
  13. hwpx/_document/ns/page.py +468 -0
  14. hwpx/_document/ns/parts.py +68 -0
  15. hwpx/_document/ns/refs.py +72 -0
  16. hwpx/_document/ns/shapes.py +250 -0
  17. hwpx/_document/ns/styles.py +371 -0
  18. hwpx/_document/ns/tables.py +80 -0
  19. hwpx/_document/ns/text.py +146 -0
  20. hwpx/_document/ns/tracking.py +151 -0
  21. hwpx/_document/persistence.py +11 -3
  22. hwpx/_document/shapes.py +44 -15
  23. hwpx/_document/tracked.py +108 -36
  24. hwpx/body_patch.py +78 -12
  25. hwpx/capabilities.py +275 -3
  26. hwpx/data/contract_docs/support-matrix.md +34 -2
  27. hwpx/document.py +239 -1557
  28. hwpx/errors.py +194 -1
  29. hwpx/layout/lint.py +8 -3
  30. hwpx/layout/report.py +11 -2
  31. hwpx/model.py +78 -0
  32. hwpx/objects/__init__.py +57 -0
  33. hwpx/objects/binary_item.py +58 -0
  34. hwpx/objects/checkbox.py +103 -0
  35. hwpx/objects/form_field.py +191 -0
  36. hwpx/objects/results.py +242 -0
  37. hwpx/objects/tracked.py +66 -0
  38. hwpx/oxml/_document_primitives.py +71 -0
  39. hwpx/oxml/document_parts.py +28 -12
  40. hwpx/oxml/header_part.py +114 -3
  41. hwpx/oxml/memo.py +78 -0
  42. hwpx/oxml/section_format.py +665 -0
  43. hwpx/patch.py +22 -1
  44. hwpx/quality/report.py +34 -2
  45. hwpx/quality/save_pipeline.py +21 -4
  46. hwpx/table_patch.py +1 -1
  47. hwpx/tools/idempotence.py +7 -5
  48. {python_hwpx-5.7.0.dist-info → python_hwpx-6.0.2.dist-info}/METADATA +19 -9
  49. {python_hwpx-5.7.0.dist-info → python_hwpx-6.0.2.dist-info}/RECORD +54 -31
  50. {python_hwpx-5.7.0.dist-info → python_hwpx-6.0.2.dist-info}/licenses/NOTICE +8 -0
  51. {python_hwpx-5.7.0.dist-info → python_hwpx-6.0.2.dist-info}/WHEEL +0 -0
  52. {python_hwpx-5.7.0.dist-info → python_hwpx-6.0.2.dist-info}/entry_points.txt +0 -0
  53. {python_hwpx-5.7.0.dist-info → python_hwpx-6.0.2.dist-info}/licenses/LICENSE +0 -0
  54. {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