python-hwpx 6.7.0__py3-none-any.whl → 6.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hwpx/__init__.py +84 -34
- hwpx/_document/fields.py +1 -1
- hwpx/_document/layout.py +46 -25
- hwpx/_document/media.py +45 -8
- hwpx/_document/memos.py +3 -1
- hwpx/_document/ns/shapes.py +2 -2
- hwpx/_document/ns/tables.py +49 -3
- hwpx/_document/persistence.py +14 -2
- hwpx/_document/shapes.py +15 -7
- hwpx/body_patch.py +27 -12
- hwpx/capabilities.py +1 -1
- hwpx/data/contract_docs/support-matrix.md +4 -4
- hwpx/document.py +39 -3
- hwpx/equation/authoring.py +6 -3
- hwpx/equation/eqedit.py +7 -2
- hwpx/equation/measure.py +169 -73
- hwpx/errors.py +48 -1
- hwpx/form_fit/_glyph_table.py +6951 -0
- hwpx/form_fit/measure.py +1392 -85
- hwpx/hwp5/docinfo_writer.py +4 -3
- hwpx/hwp5/errors.py +12 -22
- hwpx/hwp5/header_xml.py +4 -3
- hwpx/hwp5/package.py +70 -3
- hwpx/hwp5/records.py +2 -0
- hwpx/hwp5/section_writer.py +6 -5
- hwpx/hwp5/section_xml.py +69 -6
- hwpx/hwp5/shape_xml.py +6 -4
- hwpx/hwp5/shapes.py +22 -16
- hwpx/ingest/hwpx_converter.py +2 -1
- hwpx/layout/line_cache.py +636 -0
- hwpx/layout/lint.py +4 -3
- hwpx/layout/pages.py +2516 -349
- hwpx/oxml/__init__.py +4 -0
- hwpx/oxml/_document_primitives.py +31 -20
- hwpx/oxml/_paragraph_text_edit.py +5 -11
- hwpx/oxml/body.py +3 -3
- hwpx/oxml/container.py +377 -0
- hwpx/oxml/curves.py +10 -3
- hwpx/oxml/document_parts.py +38 -5
- hwpx/oxml/dutmal_compose.py +1 -1
- hwpx/oxml/header.py +4 -0
- hwpx/oxml/header_compat.py +6 -0
- hwpx/oxml/header_fonts.py +75 -4
- hwpx/oxml/header_part.py +1 -1
- hwpx/oxml/memo.py +4 -1
- hwpx/oxml/note_authoring.py +2 -0
- hwpx/oxml/objects.py +145 -215
- hwpx/oxml/paragraph.py +54 -81
- hwpx/oxml/run.py +1 -0
- hwpx/oxml/section.py +69 -15
- hwpx/oxml/section_format.py +185 -54
- hwpx/oxml/shape_position.py +320 -2
- hwpx/oxml/table.py +6 -4
- hwpx/oxml/table_sizes.py +82 -1
- hwpx/oxml/table_structure.py +166 -0
- hwpx/oxml/utils.py +46 -1
- hwpx/patch.py +11 -8
- hwpx/table_patch.py +428 -56
- hwpx/tools/__init__.py +220 -74
- hwpx/tools/package_validator.py +29 -6
- hwpx/tools/toc_author.py +1 -1
- {python_hwpx-6.7.0.dist-info → python_hwpx-6.9.0.dist-info}/METADATA +9 -4
- {python_hwpx-6.7.0.dist-info → python_hwpx-6.9.0.dist-info}/RECORD +68 -64
- {python_hwpx-6.7.0.dist-info → python_hwpx-6.9.0.dist-info}/WHEEL +0 -0
- {python_hwpx-6.7.0.dist-info → python_hwpx-6.9.0.dist-info}/entry_points.txt +0 -0
- {python_hwpx-6.7.0.dist-info → python_hwpx-6.9.0.dist-info}/licenses/LICENSE +0 -0
- {python_hwpx-6.7.0.dist-info → python_hwpx-6.9.0.dist-info}/licenses/NOTICE +0 -0
- {python_hwpx-6.7.0.dist-info → python_hwpx-6.9.0.dist-info}/top_level.txt +0 -0
hwpx/__init__.py
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import importlib
|
|
15
15
|
import warnings
|
|
16
16
|
from dataclasses import dataclass
|
|
17
|
-
from typing import Literal
|
|
17
|
+
from typing import TYPE_CHECKING, Literal
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _resolve_version() -> str:
|
|
@@ -28,9 +28,9 @@ def _resolve_version() -> str:
|
|
|
28
28
|
|
|
29
29
|
# --- experimental / deprecated 최상위 표면 (지연 접근, 접근 시 경고) ---------------
|
|
30
30
|
#
|
|
31
|
-
# stable 이름은 아래에서 eager import
|
|
32
|
-
#
|
|
33
|
-
# 4.0.0에서 제거되는 이름은 0개 — 모두 계속 import 가능합니다.
|
|
31
|
+
# stable 이름은 경고 없이 해석됩니다. 대부분 아래에서 eager import 되고, ``hwpx.tools``의
|
|
32
|
+
# 이름은 ``_TOOLS_STABLE_EXPORTS``가 지연 해석합니다. 여기 등록된 이름만 지연 해석되어
|
|
33
|
+
# 경고를 냅니다. 4.0.0에서 제거되는 이름은 0개 — 모두 계속 import 가능합니다.
|
|
34
34
|
|
|
35
35
|
_EXPERIMENTAL_EXPORTS = {
|
|
36
36
|
# 문서 ingestion 프레임워크(임의 포맷 -> HWPX). 계약 유동.
|
|
@@ -293,8 +293,8 @@ class RetiredSurface(ImportError):
|
|
|
293
293
|
def __getattr__(name: str) -> object:
|
|
294
294
|
"""Resolve dynamic module attributes.
|
|
295
295
|
|
|
296
|
-
``__version__
|
|
297
|
-
해석 시 ``DeprecationWarning``을 냅니다.
|
|
296
|
+
``__version__``과 ``hwpx.tools``의 stable 이름은 경고 없이 지연 해석하고,
|
|
297
|
+
experimental/deprecated 이름은 해석 시 ``DeprecationWarning``을 냅니다.
|
|
298
298
|
"""
|
|
299
299
|
|
|
300
300
|
if name == "__version__":
|
|
@@ -318,6 +318,17 @@ def __getattr__(name: str) -> object:
|
|
|
318
318
|
warnings.warn(_deprecated_message(name), DeprecationWarning, stacklevel=2)
|
|
319
319
|
return getattr(importlib.import_module(module_name), name)
|
|
320
320
|
|
|
321
|
+
if name in _TOOLS_STABLE_EXPORTS or name == "tools":
|
|
322
|
+
# Not ``from . import tools``: that asks this module for ``tools`` first and
|
|
323
|
+
# re-enters this function.
|
|
324
|
+
import hwpx.tools as tools
|
|
325
|
+
|
|
326
|
+
if name == "tools":
|
|
327
|
+
return tools
|
|
328
|
+
value = getattr(tools, name)
|
|
329
|
+
globals()[name] = value
|
|
330
|
+
return value
|
|
331
|
+
|
|
321
332
|
moved = _MOVED_TO_COMPANION.get(name)
|
|
322
333
|
if moved is not None:
|
|
323
334
|
statement = moved.import_statement(name)
|
|
@@ -346,11 +357,76 @@ def __dir__() -> list[str]:
|
|
|
346
357
|
return sorted(
|
|
347
358
|
set(globals())
|
|
348
359
|
| set(__all__)
|
|
360
|
+
| {"tools"}
|
|
349
361
|
| set(_EXPERIMENTAL_EXPORTS)
|
|
350
362
|
| set(_DEPRECATED_EXPORTS)
|
|
351
363
|
)
|
|
352
364
|
|
|
353
365
|
|
|
366
|
+
# --- 지연 stable 최상위 표면 (첫 접근 때 해석, 경고 없음) ---------------------------
|
|
367
|
+
#
|
|
368
|
+
# ``hwpx.tools``에 사는 stable 이름은 첫 접근 때 ``__getattr__``이 그 모듈을 읽어
|
|
369
|
+
# 같은 객체를 돌려주고 모듈 전역에 둡니다(경고 없음). ``import hwpx``가 도구 모듈
|
|
370
|
+
# 전부(내보내기·비교·검증 등)를 읽지 않게 하려는 것입니다. ``__all__``·``dir()``·
|
|
371
|
+
# ``from hwpx import *``에는 그대로 있습니다.
|
|
372
|
+
|
|
373
|
+
#: ``hwpx.tools``의 하위 모듈이 정의하고 ``hwpx.tools``가 다시 내보내는 stable 이름(텍스트 추출·개체
|
|
374
|
+
#: 찾기·문서 비교·메일 머지·패키지 검증). 정적 import로 해석하므로 동적 import 지점은
|
|
375
|
+
#: 늘지 않는다.
|
|
376
|
+
_TOOLS_STABLE_EXPORTS = frozenset(
|
|
377
|
+
{
|
|
378
|
+
"DEFAULT_NAMESPACES",
|
|
379
|
+
"ParagraphInfo",
|
|
380
|
+
"SectionInfo",
|
|
381
|
+
"TextExtractor",
|
|
382
|
+
"FoundElement",
|
|
383
|
+
"ObjectFinder",
|
|
384
|
+
"DOC_DIFF_REPORT_VERSION",
|
|
385
|
+
"REFERENCE_CONSISTENCY_REPORT_VERSION",
|
|
386
|
+
"diff_paragraphs",
|
|
387
|
+
"doc_diff",
|
|
388
|
+
"inspect_reference_consistency",
|
|
389
|
+
"MAIL_MERGE_REPORT_VERSION",
|
|
390
|
+
"inspect_mail_merge_placeholders",
|
|
391
|
+
"load_mail_merge_rows",
|
|
392
|
+
"merge_template_rows",
|
|
393
|
+
"EditorOpenSafetyReport",
|
|
394
|
+
"PackageValidationReport",
|
|
395
|
+
"validate_editor_open_safety",
|
|
396
|
+
"validate_package",
|
|
397
|
+
}
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
if TYPE_CHECKING: # type checkers see the real objects; at runtime they load lazily
|
|
402
|
+
from .tools.doc_diff import (
|
|
403
|
+
DOC_DIFF_REPORT_VERSION,
|
|
404
|
+
REFERENCE_CONSISTENCY_REPORT_VERSION,
|
|
405
|
+
diff_paragraphs,
|
|
406
|
+
doc_diff,
|
|
407
|
+
inspect_reference_consistency,
|
|
408
|
+
)
|
|
409
|
+
from .tools.mail_merge import (
|
|
410
|
+
MAIL_MERGE_REPORT_VERSION,
|
|
411
|
+
inspect_mail_merge_placeholders,
|
|
412
|
+
load_mail_merge_rows,
|
|
413
|
+
merge_template_rows,
|
|
414
|
+
)
|
|
415
|
+
from .tools.object_finder import FoundElement, ObjectFinder
|
|
416
|
+
from .tools.package_validator import (
|
|
417
|
+
EditorOpenSafetyReport,
|
|
418
|
+
PackageValidationReport,
|
|
419
|
+
validate_editor_open_safety,
|
|
420
|
+
validate_package,
|
|
421
|
+
)
|
|
422
|
+
from .tools.text_extractor import (
|
|
423
|
+
DEFAULT_NAMESPACES,
|
|
424
|
+
ParagraphInfo,
|
|
425
|
+
SectionInfo,
|
|
426
|
+
TextExtractor,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
354
430
|
# ``import hwpx.builder`` goes through the import system, not ``__getattr__``,
|
|
355
431
|
# so the moved module names get their destination from a finder instead.
|
|
356
432
|
from . import _moved_modules
|
|
@@ -359,35 +435,9 @@ _moved_modules.install(_MOVED_TO_COMPANION)
|
|
|
359
435
|
|
|
360
436
|
|
|
361
437
|
# --- stable 최상위 표면 (eager import) ------------------------------------------
|
|
362
|
-
|
|
363
|
-
DEFAULT_NAMESPACES,
|
|
364
|
-
ParagraphInfo,
|
|
365
|
-
SectionInfo,
|
|
366
|
-
TextExtractor,
|
|
367
|
-
)
|
|
368
|
-
from .tools.object_finder import FoundElement, ObjectFinder
|
|
369
|
-
from .tools.doc_diff import (
|
|
370
|
-
DOC_DIFF_REPORT_VERSION,
|
|
371
|
-
REFERENCE_CONSISTENCY_REPORT_VERSION,
|
|
372
|
-
diff_paragraphs,
|
|
373
|
-
doc_diff,
|
|
374
|
-
inspect_reference_consistency,
|
|
375
|
-
)
|
|
376
|
-
from .tools.mail_merge import (
|
|
377
|
-
MAIL_MERGE_REPORT_VERSION,
|
|
378
|
-
inspect_mail_merge_placeholders,
|
|
379
|
-
load_mail_merge_rows,
|
|
380
|
-
merge_template_rows,
|
|
381
|
-
)
|
|
382
|
-
from .tools.package_validator import (
|
|
383
|
-
EditorOpenSafetyReport,
|
|
384
|
-
PackageValidationReport,
|
|
385
|
-
validate_editor_open_safety,
|
|
386
|
-
validate_package,
|
|
387
|
-
)
|
|
438
|
+
# ``hwpx.tools``의 stable 이름은 위 ``_TOOLS_STABLE_EXPORTS``가 지연 해석한다.
|
|
388
439
|
from .ingest import HwpxMarkdownConverter
|
|
389
|
-
from .errors import HwpxError
|
|
390
|
-
from .hwp5.errors import Hwp5ConversionWarning, Hwp5Error
|
|
440
|
+
from .errors import Hwp5ConversionWarning, Hwp5Error, HwpxError
|
|
391
441
|
from .mutation_report import (
|
|
392
442
|
MutationReport,
|
|
393
443
|
PreservationDowngradeError,
|
hwpx/_document/fields.py
CHANGED
|
@@ -663,7 +663,7 @@ def add_form_field(
|
|
|
663
663
|
"""Create a click-here (누름틀) form field and return it as a :class:`FormField`.
|
|
664
664
|
|
|
665
665
|
The emitted XML follows the real-Hancom CLICKHERE contract
|
|
666
|
-
(
|
|
666
|
+
(inferred from Hancom Office 12.0.0.3288 gold documents). The
|
|
667
667
|
prompt (안내문) is materialized as a screen-only red-italic run, exactly as
|
|
668
668
|
Hancom authors it. The created field is immediately re-read through the
|
|
669
669
|
standard ``list_form_fields`` matcher — creation fails loudly if the
|
hwpx/_document/layout.py
CHANGED
|
@@ -19,7 +19,9 @@ from ..oxml._document_primitives import NEW_NUM_KINDS
|
|
|
19
19
|
from ..oxml.namespaces import HH, HP
|
|
20
20
|
from ..oxml.numbering_kinds import ensure_numbering_levels
|
|
21
21
|
from ..oxml.objects import HwpxOxmlInlineObject
|
|
22
|
-
from ..oxml.section_format import
|
|
22
|
+
from ..oxml.section_format import (
|
|
23
|
+
_PAGE_LANDSCAPE, _PAGE_PORTRAIT, _checked, _page_orientation_value, column_shares, validate_column_gap,
|
|
24
|
+
)
|
|
23
25
|
from ..oxml.table_sizes import cell_margins_of
|
|
24
26
|
from ._units import _mm_to_hwp_units, _pt_to_hwp_units
|
|
25
27
|
|
|
@@ -512,12 +514,12 @@ def set_list_format(
|
|
|
512
514
|
suggestion="A continued list numbers on in the format of the list before it: "
|
|
513
515
|
"leave out number_format and start, or continue_list to start a new list.",
|
|
514
516
|
)
|
|
515
|
-
if level
|
|
517
|
+
if isinstance(level, bool) or not isinstance(level, int) or not 1 <= level <= 10:
|
|
516
518
|
raise HwpxValueError(
|
|
517
|
-
"level must be 1
|
|
519
|
+
f"level must be an int from 1 to 10; got {level!r}",
|
|
518
520
|
code="style-list-level-invalid",
|
|
519
|
-
context={"requested": level},
|
|
520
|
-
suggestion="
|
|
521
|
+
context={"requested": repr(level)},
|
|
522
|
+
suggestion="Hancom lists have ten levels, 1 to 10; it cannot save a document using a deeper one.",
|
|
521
523
|
)
|
|
522
524
|
if not doc._root.headers:
|
|
523
525
|
raise HwpxValueError(
|
|
@@ -525,6 +527,13 @@ def set_list_format(
|
|
|
525
527
|
code="document-header-missing",
|
|
526
528
|
suggestion="Check that this is an intact HWPX package.",
|
|
527
529
|
)
|
|
530
|
+
if start is not None and (isinstance(start, bool) or not isinstance(start, int) or not 0 <= start <= 0xFFFF):
|
|
531
|
+
raise HwpxValueError( # Hancom keeps a list's start as 16 bits unsigned (0 drawn as 1)
|
|
532
|
+
f"start must be an int from 0 to 65535; got {start!r}",
|
|
533
|
+
code="style-list-start-value",
|
|
534
|
+
context={"requested": repr(start)},
|
|
535
|
+
suggestion="Pass the number the list starts at, from 0 to 65535.",
|
|
536
|
+
)
|
|
528
537
|
|
|
529
538
|
level_specs: list[dict[str, str]] = [{} for _ in range(level)]
|
|
530
539
|
if bullet_char:
|
|
@@ -532,7 +541,7 @@ def set_list_format(
|
|
|
532
541
|
if number_format:
|
|
533
542
|
level_specs[level - 1]["format"] = str(number_format).upper()
|
|
534
543
|
if start is not None:
|
|
535
|
-
level_specs[level - 1]["start"] = str(
|
|
544
|
+
level_specs[level - 1]["start"] = str(start)
|
|
536
545
|
|
|
537
546
|
targets = _resolve_paragraph_targets(doc,
|
|
538
547
|
paragraph_index=paragraph_index,
|
|
@@ -609,6 +618,8 @@ def set_page_setup(
|
|
|
609
618
|
"""
|
|
610
619
|
|
|
611
620
|
normalized_orientation = _normalize_page_orientation(orientation)
|
|
621
|
+
if columns is not None: # the gap checked before the page or its margins change
|
|
622
|
+
validate_column_gap(_mm_to_hwp_units(float(column_gap_mm or 0)))
|
|
612
623
|
target_width_mm = width_mm
|
|
613
624
|
target_height_mm = height_mm
|
|
614
625
|
if paper_size:
|
|
@@ -633,6 +644,24 @@ def set_page_setup(
|
|
|
633
644
|
|
|
634
645
|
width = _mm_to_hwp_units(float(target_width_mm)) if target_width_mm is not None else None
|
|
635
646
|
height = _mm_to_hwp_units(float(target_height_mm)) if target_height_mm is not None else None
|
|
647
|
+
margin_source = dict(margins_mm or {})
|
|
648
|
+
margin_values = {
|
|
649
|
+
"left": margin_left_mm if margin_left_mm is not None else margin_source.get("left"),
|
|
650
|
+
"right": margin_right_mm if margin_right_mm is not None else margin_source.get("right"),
|
|
651
|
+
"top": margin_top_mm if margin_top_mm is not None else margin_source.get("top"),
|
|
652
|
+
"bottom": margin_bottom_mm if margin_bottom_mm is not None else margin_source.get("bottom"),
|
|
653
|
+
"header": header_margin_mm if header_margin_mm is not None else margin_source.get("header"),
|
|
654
|
+
"footer": footer_margin_mm if footer_margin_mm is not None else margin_source.get("footer"),
|
|
655
|
+
"gutter": gutter_mm if gutter_mm is not None else margin_source.get("gutter"),
|
|
656
|
+
}
|
|
657
|
+
hwp_margins = {
|
|
658
|
+
name: _mm_to_hwp_units(float(value))
|
|
659
|
+
for name, value in margin_values.items()
|
|
660
|
+
if value is not None
|
|
661
|
+
}
|
|
662
|
+
for name, value in (("width", width), ("height", height), *hwp_margins.items()):
|
|
663
|
+
if value is not None: # checked before the page or its margins change
|
|
664
|
+
_checked(name, value, 2**31 - 1, "page-size-value")
|
|
636
665
|
if width is not None or height is not None or normalized_orientation is not None:
|
|
637
666
|
# Call the local primitives directly rather than `doc.set_page_size`/
|
|
638
667
|
# `doc.set_page_margins`/`doc.set_columns` below — all three names
|
|
@@ -649,21 +678,6 @@ def set_page_setup(
|
|
|
649
678
|
section_index=section_index,
|
|
650
679
|
)
|
|
651
680
|
|
|
652
|
-
margin_source = dict(margins_mm or {})
|
|
653
|
-
margin_values = {
|
|
654
|
-
"left": margin_left_mm if margin_left_mm is not None else margin_source.get("left"),
|
|
655
|
-
"right": margin_right_mm if margin_right_mm is not None else margin_source.get("right"),
|
|
656
|
-
"top": margin_top_mm if margin_top_mm is not None else margin_source.get("top"),
|
|
657
|
-
"bottom": margin_bottom_mm if margin_bottom_mm is not None else margin_source.get("bottom"),
|
|
658
|
-
"header": header_margin_mm if header_margin_mm is not None else margin_source.get("header"),
|
|
659
|
-
"footer": footer_margin_mm if footer_margin_mm is not None else margin_source.get("footer"),
|
|
660
|
-
"gutter": gutter_mm if gutter_mm is not None else margin_source.get("gutter"),
|
|
661
|
-
}
|
|
662
|
-
hwp_margins = {
|
|
663
|
-
name: _mm_to_hwp_units(float(value))
|
|
664
|
-
for name, value in margin_values.items()
|
|
665
|
-
if value is not None
|
|
666
|
-
}
|
|
667
681
|
if hwp_margins:
|
|
668
682
|
set_page_margins(
|
|
669
683
|
doc,
|
|
@@ -723,15 +737,18 @@ def set_columns(
|
|
|
723
737
|
) -> HwpxOxmlInlineObject:
|
|
724
738
|
"""Set the columns of a section, or start new columns at a paragraph.
|
|
725
739
|
|
|
726
|
-
Without ``paragraph
|
|
727
|
-
``hp:colPr`` next to ``hp:secPr``) in
|
|
728
|
-
out in ``col_count`` columns. With
|
|
729
|
-
|
|
740
|
+
Without ``paragraph``, or with a section's first one, this rewrites the
|
|
741
|
+
section's own column layout (the ``hp:colPr`` next to ``hp:secPr``) in
|
|
742
|
+
place, so the whole section is laid out in ``col_count`` columns. With a
|
|
743
|
+
later ``paragraph`` it adds a column definition there, used from it on.
|
|
730
744
|
|
|
731
745
|
Args:
|
|
732
746
|
col_count: Number of columns (1–255).
|
|
733
747
|
col_type: ``NEWSPAPER``, ``BALANCED_NEWSPAPER``, or ``PARALLEL``.
|
|
734
748
|
same_gap: Gap in HWPUNIT (7200 = 1 inch).
|
|
749
|
+
column_widths: With ``same_size=False``, a ``(width, gap)`` pair per
|
|
750
|
+
column, in HWPUNIT adding up to the text width or in plain
|
|
751
|
+
proportions; Hancom keeps them as shares of 32768 of the text width.
|
|
735
752
|
separator_type: Optional column separator line type (e.g. ``SOLID``).
|
|
736
753
|
"""
|
|
737
754
|
if not 1 <= col_count <= 255:
|
|
@@ -741,6 +758,10 @@ def set_columns(
|
|
|
741
758
|
context={"requested": col_count},
|
|
742
759
|
suggestion="Use columns=1 to remove columns.",
|
|
743
760
|
)
|
|
761
|
+
if same_size:
|
|
762
|
+
validate_column_gap(same_gap) # before a paragraph is added
|
|
763
|
+
elif column_widths:
|
|
764
|
+
column_shares(column_widths) # checked before a paragraph is added too
|
|
744
765
|
if paragraph is None:
|
|
745
766
|
target_section = _resolve_section(doc, section=section, section_index=section_index)
|
|
746
767
|
ctrl = target_section.properties.set_columns(
|
hwpx/_document/media.py
CHANGED
|
@@ -19,6 +19,11 @@ from ..opc.relationships import (
|
|
|
19
19
|
)
|
|
20
20
|
from ..oxml import HwpxOxmlInlineObject, HwpxOxmlParagraph
|
|
21
21
|
from ..oxml.namespaces import HC, HP
|
|
22
|
+
from ..oxml.color import normalize_color
|
|
23
|
+
from ..oxml.shape_position import PictureGlow, PictureReflection, PictureShadow, validate_picture_effects
|
|
24
|
+
from ..oxml.shape_position import (
|
|
25
|
+
validate_picture_align, validate_picture_border, validate_picture_crop, validate_picture_image, validate_shape_size,
|
|
26
|
+
)
|
|
22
27
|
from ._units import _mm_to_hwp_units
|
|
23
28
|
|
|
24
29
|
if TYPE_CHECKING:
|
|
@@ -106,23 +111,35 @@ def add_picture(
|
|
|
106
111
|
width_mm: float | None = None,
|
|
107
112
|
height_mm: float | None = None,
|
|
108
113
|
align: str | None = None,
|
|
114
|
+
brightness: int = 0,
|
|
115
|
+
contrast: int = 0,
|
|
116
|
+
effect: str = "REAL_PIC",
|
|
117
|
+
alpha: int = 0,
|
|
118
|
+
line_color: str | None = None,
|
|
119
|
+
line_width: int = 33,
|
|
120
|
+
crop: tuple[int, int, int, int] | None = None,
|
|
121
|
+
shadow: PictureShadow | None = None,
|
|
122
|
+
glow: PictureGlow | None = None,
|
|
123
|
+
soft_edge: int | None = None,
|
|
124
|
+
reflection: PictureReflection | None = None,
|
|
109
125
|
para_pr_id_ref: str | int | None = None,
|
|
110
126
|
style_id_ref: str | int | None = None,
|
|
111
127
|
char_pr_id_ref: str | int | None = None,
|
|
112
128
|
run_attributes: dict[str, str] | None = None,
|
|
113
129
|
**extra_attrs: str,
|
|
114
130
|
) -> HwpxOxmlInlineObject:
|
|
115
|
-
"""Embed image data and place a picture object in a new paragraph
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
131
|
+
"""Embed image data and place a picture object in a new paragraph, the image adjusted and bordered as
|
|
132
|
+
:meth:`HwpxOxmlParagraph.add_picture` takes it."""
|
|
133
|
+
|
|
134
|
+
validate_picture_align(align) # before anything is stored: Hancom reads an unknown one as LEFT
|
|
135
|
+
validate_picture_image(brightness, contrast, effect, alpha) # ... and draws no other adjustment
|
|
136
|
+
validate_picture_border(line_width)
|
|
137
|
+
if line_color is not None:
|
|
138
|
+
normalize_color(line_color)
|
|
123
139
|
resolved_width = width
|
|
124
140
|
if resolved_width is None:
|
|
125
141
|
resolved_width = _mm_to_hwp_units(width_mm) if width_mm is not None else 14400
|
|
142
|
+
validate_shape_size(resolved_width, 0) # validate before aspect-ratio arithmetic too
|
|
126
143
|
|
|
127
144
|
resolved_height = height
|
|
128
145
|
if resolved_height is None:
|
|
@@ -135,6 +152,15 @@ def add_picture(
|
|
|
135
152
|
resolved_height = round(resolved_width * source_height / source_width)
|
|
136
153
|
else:
|
|
137
154
|
resolved_height = resolved_width
|
|
155
|
+
validate_shape_size(resolved_width, resolved_height) # before anything is stored too
|
|
156
|
+
validate_picture_crop(crop, resolved_width, resolved_height)
|
|
157
|
+
validate_picture_effects(shadow, glow, soft_edge, reflection)
|
|
158
|
+
|
|
159
|
+
# Call the local primitive directly rather than `doc.add_image` — that
|
|
160
|
+
# facade name moved in 6.0 (design table row 33), and going through it
|
|
161
|
+
# would fire a DeprecationWarning on every `add_picture` call even though
|
|
162
|
+
# `add_picture` itself is a kept (unmoved) root method.
|
|
163
|
+
binary_item_id_ref = str(add_image(doc, image_data, image_format))
|
|
138
164
|
|
|
139
165
|
paragraph = doc.add_paragraph(
|
|
140
166
|
"",
|
|
@@ -158,6 +184,17 @@ def add_picture(
|
|
|
158
184
|
width=resolved_width,
|
|
159
185
|
height=resolved_height,
|
|
160
186
|
align=align,
|
|
187
|
+
brightness=brightness,
|
|
188
|
+
contrast=contrast,
|
|
189
|
+
effect=effect,
|
|
190
|
+
alpha=alpha,
|
|
191
|
+
line_color=line_color,
|
|
192
|
+
line_width=line_width,
|
|
193
|
+
crop=crop,
|
|
194
|
+
shadow=shadow,
|
|
195
|
+
glow=glow,
|
|
196
|
+
soft_edge=soft_edge,
|
|
197
|
+
reflection=reflection,
|
|
161
198
|
run_attributes=run_attributes,
|
|
162
199
|
char_pr_id_ref=char_pr_id_ref,
|
|
163
200
|
)
|
hwpx/_document/memos.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
8
8
|
|
|
9
9
|
from ..errors import HwpxValueError
|
|
10
10
|
|
|
11
|
-
from ..oxml._document_primitives import _object_id
|
|
11
|
+
from ..oxml._document_primitives import _clear_paragraph_layout_cache, _object_id
|
|
12
12
|
from ..oxml.namespaces import HP
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
@@ -189,6 +189,7 @@ def attach_memo_field(
|
|
|
189
189
|
"dirty": "0",
|
|
190
190
|
"zorder": order,
|
|
191
191
|
"fieldid": MEMO_FIELD_ID,
|
|
192
|
+
"metaTag": "",
|
|
192
193
|
},
|
|
193
194
|
)
|
|
194
195
|
|
|
@@ -247,6 +248,7 @@ def attach_memo_field(
|
|
|
247
248
|
|
|
248
249
|
paragraph.element.insert(0, run_begin)
|
|
249
250
|
paragraph.element.append(run_end)
|
|
251
|
+
_clear_paragraph_layout_cache(paragraph.element) # the field's controls move its text: laid out again
|
|
250
252
|
paragraph.section.mark_dirty()
|
|
251
253
|
|
|
252
254
|
return memo
|
hwpx/_document/ns/shapes.py
CHANGED
|
@@ -345,8 +345,8 @@ class ShapesNamespace(_Namespace):
|
|
|
345
345
|
section: "int | Section | None" = None,
|
|
346
346
|
section_index: int | None = None,
|
|
347
347
|
) -> "Shape":
|
|
348
|
-
"""도형을 그룹으로 묶는다(`ContainerMember.rect`/`.ellipse`/`.polygon
|
|
349
|
-
각 부재를 그룹 로컬 좌표로 만들어 넘긴다)."""
|
|
348
|
+
"""도형을 그룹으로 묶는다(`ContainerMember.rect`/`.ellipse`/`.polygon`/`.line`/
|
|
349
|
+
`.text_box`/`.picture`/`.group`으로 각 부재를 그룹 로컬 좌표로 만들어 넘긴다)."""
|
|
350
350
|
|
|
351
351
|
from .. import shapes as _shapes
|
|
352
352
|
|
hwpx/_document/ns/tables.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
"""`doc.tables` — 표
|
|
2
|
+
"""`doc.tables` — 표 탐색·매핑·병합·구조 편집·경로 채움.
|
|
3
3
|
|
|
4
4
|
표를 **만드는** 것은 루트에 남았다(`doc.add_table` — python-docx 대응이자
|
|
5
5
|
automation 최다 사용 저작 동사 중 하나). 만든 다음에 하는 일들이 여기 모인다.
|
|
@@ -11,7 +11,7 @@ automation 최다 사용 저작 동사 중 하나). 만든 다음에 하는 일
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
-
from typing import TYPE_CHECKING, Mapping
|
|
14
|
+
from typing import TYPE_CHECKING, Iterable, Mapping, overload
|
|
15
15
|
|
|
16
16
|
from ._base import _Namespace
|
|
17
17
|
|
|
@@ -28,7 +28,7 @@ __all__ = ["TablesNamespace"]
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class TablesNamespace(_Namespace):
|
|
31
|
-
"""표
|
|
31
|
+
"""표 탐색·매핑·병합·구조 편집·경로 채움."""
|
|
32
32
|
|
|
33
33
|
__slots__ = ()
|
|
34
34
|
_path = "doc.tables"
|
|
@@ -48,6 +48,20 @@ class TablesNamespace(_Namespace):
|
|
|
48
48
|
def __iter__(self):
|
|
49
49
|
return iter(self.all)
|
|
50
50
|
|
|
51
|
+
@overload
|
|
52
|
+
def __getitem__(self, index: int) -> "Table": ...
|
|
53
|
+
|
|
54
|
+
@overload
|
|
55
|
+
def __getitem__(self, index: slice) -> list["Table"]: ...
|
|
56
|
+
|
|
57
|
+
def __getitem__(self, index: int | slice) -> "Table | list[Table]":
|
|
58
|
+
"""문서 순서의 ``index`` 번째 표(``doc.tables[0]``, ``doc.tables[-1]``, ``doc.tables[1:3]``).
|
|
59
|
+
|
|
60
|
+
``doc.tables.all[index]``와 같다. 범위 밖이면 ``IndexError``.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
return self.all[index]
|
|
64
|
+
|
|
51
65
|
def map(self) -> "TableMapResult":
|
|
52
66
|
"""모든 표의 위치·모양·헤더 미리보기를 담은 간결 지도."""
|
|
53
67
|
|
|
@@ -78,3 +92,35 @@ class TablesNamespace(_Namespace):
|
|
|
78
92
|
"""
|
|
79
93
|
|
|
80
94
|
return table.merge_cells(cell_range)
|
|
95
|
+
|
|
96
|
+
def insert_rows(
|
|
97
|
+
self, table: "Table", ref_row: int, count: int = 1, *, side: str = "below", blank: bool = False
|
|
98
|
+
) -> None:
|
|
99
|
+
"""``ref_row`` 행을 복제해 그 아래(``side="above"``면 위)에 ``count`` 행을 넣는다.
|
|
100
|
+
|
|
101
|
+
새 칸은 같은 열 기준 칸의 서식과 글을 받고, ``blank=True``면 한/글처럼 빈 문단 하나만 둔다.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
table.insert_rows(ref_row, count, side=side, blank=blank)
|
|
105
|
+
|
|
106
|
+
def insert_columns(
|
|
107
|
+
self, table: "Table", ref_col: int, count: int = 1, *, side: str = "right", blank: bool = False
|
|
108
|
+
) -> None:
|
|
109
|
+
"""``ref_col`` 열을 복제해 그 오른쪽(``side="left"``면 왼쪽)에 ``count`` 열을 넣는다(표가 넓어진다)."""
|
|
110
|
+
|
|
111
|
+
table.insert_columns(ref_col, count, side=side, blank=blank)
|
|
112
|
+
|
|
113
|
+
def delete_rows(self, table: "Table", rows: int | Iterable[int]) -> None:
|
|
114
|
+
"""행을 지운다. 모든 행은 지울 수 없다(표 삭제를 쓴다)."""
|
|
115
|
+
|
|
116
|
+
table.delete_rows(rows)
|
|
117
|
+
|
|
118
|
+
def delete_columns(self, table: "Table", cols: int | Iterable[int]) -> None:
|
|
119
|
+
"""열을 지우고 그 폭을 남은 열에 나눈다. 모든 열은 지울 수 없다."""
|
|
120
|
+
|
|
121
|
+
table.delete_columns(cols)
|
|
122
|
+
|
|
123
|
+
def split_cell(self, table: "Table", row: int, col: int, *, rows: int = 1, cols: int = 1) -> None:
|
|
124
|
+
"""(``row``, ``col``) 칸을 ``rows`` 줄 × ``cols`` 칸으로 나눈다(한/글의 셀 나누기)."""
|
|
125
|
+
|
|
126
|
+
table.split_cell(row, col, rows=rows, cols=cols)
|
hwpx/_document/persistence.py
CHANGED
|
@@ -408,7 +408,8 @@ def save_report(
|
|
|
408
408
|
oracle-verified ``visual_complete`` tier (plan §0.0).
|
|
409
409
|
|
|
410
410
|
Like the legacy savers it raises only if the serialize step's open-safety
|
|
411
|
-
check fails; all other gate outcomes are returned in the report.
|
|
411
|
+
check fails; all other gate outcomes are returned in the report. An ``.hwp``
|
|
412
|
+
path is written as HWP 5.0, as :func:`save_to_path` writes it.
|
|
412
413
|
"""
|
|
413
414
|
|
|
414
415
|
_run_pre_save_validation(doc)
|
|
@@ -420,10 +421,15 @@ def save_report(
|
|
|
420
421
|
output_path = path_or_stream
|
|
421
422
|
elif path_or_stream is not None:
|
|
422
423
|
output_stream = path_or_stream
|
|
424
|
+
# An .hwp target gets HWP 5.0 made from the HWPX parts the gate checks; it raises before anything is
|
|
425
|
+
# written when the HWP 5.0 writer cannot express the document.
|
|
426
|
+
hwp5 = None
|
|
427
|
+
if output_path is not None and str(output_path).lower().endswith(".hwp"):
|
|
428
|
+
hwp5 = _hwp5_bytes(archive_bytes)
|
|
423
429
|
|
|
424
430
|
report = doc._save_pipeline.run(
|
|
425
431
|
archive_bytes,
|
|
426
|
-
output_path=output_path,
|
|
432
|
+
output_path=None if hwp5 is not None else output_path,
|
|
427
433
|
output_stream=output_stream,
|
|
428
434
|
quality=quality or QualityPolicy.transparent(),
|
|
429
435
|
before=before,
|
|
@@ -432,6 +438,12 @@ def save_report(
|
|
|
432
438
|
reference_document=doc,
|
|
433
439
|
source_label="document.save_report",
|
|
434
440
|
)
|
|
441
|
+
if hwp5 is not None and report.ok: # the gate passed: publish the HWP 5.0 file in its place
|
|
442
|
+
from ..quality.save_pipeline import write_bytes_atomically
|
|
443
|
+
|
|
444
|
+
assert output_path is not None
|
|
445
|
+
write_bytes_atomically(output_path, hwp5)
|
|
446
|
+
report.output_path = str(output_path)
|
|
435
447
|
if report.ok:
|
|
436
448
|
_mark_save_clean(doc)
|
|
437
449
|
return report
|
hwpx/_document/shapes.py
CHANGED
|
@@ -8,7 +8,9 @@ from typing import TYPE_CHECKING, Any, Sequence, cast
|
|
|
8
8
|
from ._units import _mm_to_hwp_units
|
|
9
9
|
from ..errors import HwpxStateError, HwpxValueError
|
|
10
10
|
from ..oxml.objects import _closed_points
|
|
11
|
-
from ..oxml.shape_position import
|
|
11
|
+
from ..oxml.shape_position import (
|
|
12
|
+
validate_equation_base_unit, validate_original_size, validate_rect_ratio, validate_shape_size,
|
|
13
|
+
)
|
|
12
14
|
|
|
13
15
|
if TYPE_CHECKING:
|
|
14
16
|
from hwpx.document import HwpxDocument
|
|
@@ -265,6 +267,8 @@ def add_rectangle(
|
|
|
265
267
|
Dimensions are in HWPUNIT. *ratio* controls corner roundness
|
|
266
268
|
(0 = sharp, 50 = semicircle).
|
|
267
269
|
"""
|
|
270
|
+
validate_shape_size(width, height)
|
|
271
|
+
validate_rect_ratio(ratio)
|
|
268
272
|
validate_original_size(original_size)
|
|
269
273
|
if paragraph is None:
|
|
270
274
|
paragraph = doc.add_paragraph(
|
|
@@ -297,6 +301,7 @@ def add_ellipse(
|
|
|
297
301
|
|
|
298
302
|
Dimensions are in HWPUNIT.
|
|
299
303
|
"""
|
|
304
|
+
validate_shape_size(width, height)
|
|
300
305
|
validate_original_size(original_size)
|
|
301
306
|
if paragraph is None:
|
|
302
307
|
paragraph = doc.add_paragraph(
|
|
@@ -334,6 +339,7 @@ def add_arc(
|
|
|
334
339
|
mechanism every other shape here already uses. *arc_type* is the
|
|
335
340
|
schema's own ``NORMAL``/``PIE``/``CHORD`` passthrough.
|
|
336
341
|
"""
|
|
342
|
+
validate_shape_size(width, height)
|
|
337
343
|
if paragraph is None:
|
|
338
344
|
paragraph = doc.add_paragraph(
|
|
339
345
|
"", section=section, section_index=section_index,
|
|
@@ -461,7 +467,8 @@ def add_container(
|
|
|
461
467
|
"""Insert a group (``<hp:container>``) wrapping *members*.
|
|
462
468
|
|
|
463
469
|
Each member is built with :class:`hwpx.oxml.ContainerMember`'s
|
|
464
|
-
``rect``/``ellipse``/``polygon``
|
|
470
|
+
``rect``/``ellipse``/``polygon``/``line``/``text_box``/``picture``/``group``
|
|
471
|
+
classmethods, which take the member's
|
|
465
472
|
position in the group's own local coordinate space (HWPUNIT, top-left
|
|
466
473
|
anchored) alongside its usual shape parameters::
|
|
467
474
|
|
|
@@ -522,15 +529,16 @@ def add_equation(
|
|
|
522
529
|
context={"length": len(text), "limit": MAX_SOURCE_LENGTH},
|
|
523
530
|
suggestion="Split the equation.",
|
|
524
531
|
)
|
|
532
|
+
validate_equation_base_unit(base_unit)
|
|
533
|
+
width, height = size if size is not None else estimate_equation_size(text, base_unit=base_unit)
|
|
534
|
+
validate_shape_size(width, height) # one measured at a huge base_unit too
|
|
525
535
|
if paragraph is None:
|
|
526
536
|
paragraph = doc.add_paragraph(
|
|
527
537
|
"", section=section, section_index=section_index,
|
|
528
538
|
include_run=False,
|
|
529
539
|
)
|
|
530
|
-
if size is None:
|
|
531
|
-
size = estimate_equation_size(text, base_unit=base_unit)
|
|
532
540
|
inline_object = paragraph.add_equation(
|
|
533
|
-
text, base_unit=base_unit, size=
|
|
541
|
+
text, base_unit=base_unit, size=(width, height), char_pr_id_ref=char_pr_id_ref,
|
|
534
542
|
)
|
|
535
543
|
|
|
536
544
|
created_id = inline_object.element.get("id", "")
|
|
@@ -756,10 +764,10 @@ def add_drop_cap(
|
|
|
756
764
|
char_pr_id_ref: str | int | None = None,
|
|
757
765
|
para_pr_id_ref: str | int | None = None,
|
|
758
766
|
) -> HwpxOxmlInlineObject:
|
|
759
|
-
"""Insert a drop cap (문단 첫 글자 장식),
|
|
767
|
+
"""Insert a drop cap (문단 첫 글자 장식), inferred from the one
|
|
760
768
|
real-corpus example that carries a non-default ``dropcapstyle``
|
|
761
769
|
(``error__20230809__test.hwpx`` -- see ``oxml.drop_cap``'s own
|
|
762
|
-
docstring for the full structural
|
|
770
|
+
docstring for the full structural format inference and why it
|
|
763
771
|
supports ``style="TripleLine"`` and ``"DoubleLine"`` only).
|
|
764
772
|
|
|
765
773
|
Element construction (element-building validation included) happens in
|