python-hwpx-automation 6.0.3__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_automation/__init__.py +61 -0
- hwpx_automation/__init__.pyi +27 -0
- hwpx_automation/__main__.py +8 -0
- hwpx_automation/agent_document.py +392 -0
- hwpx_automation/api.py +136 -0
- hwpx_automation/blind_eval.py +407 -0
- hwpx_automation/capabilities.py +110 -0
- hwpx_automation/compat.py +48 -0
- hwpx_automation/configuration.py +60 -0
- hwpx_automation/core/__init__.py +2 -0
- hwpx_automation/core/content.py +762 -0
- hwpx_automation/core/context.py +111 -0
- hwpx_automation/core/diff.py +53 -0
- hwpx_automation/core/document.py +37 -0
- hwpx_automation/core/formatting.py +513 -0
- hwpx_automation/core/handles.py +24 -0
- hwpx_automation/core/locations.py +205 -0
- hwpx_automation/core/locator.py +162 -0
- hwpx_automation/core/plan.py +680 -0
- hwpx_automation/core/resources.py +42 -0
- hwpx_automation/core/search.py +296 -0
- hwpx_automation/core/transactions.py +434 -0
- hwpx_automation/core/txn.py +48 -0
- hwpx_automation/document_state.py +95 -0
- hwpx_automation/errors.py +174 -0
- hwpx_automation/execution_lock.py +15 -0
- hwpx_automation/fastmcp_adapter.py +672 -0
- hwpx_automation/form_fill.py +1177 -0
- hwpx_automation/form_output_models.py +223 -0
- hwpx_automation/handlers/__init__.py +2 -0
- hwpx_automation/handlers/_shared.py +377 -0
- hwpx_automation/handlers/agent_document.py +257 -0
- hwpx_automation/handlers/authoring.py +750 -0
- hwpx_automation/handlers/content_edit.py +1078 -0
- hwpx_automation/handlers/form_fill.py +607 -0
- hwpx_automation/handlers/layout_style.py +660 -0
- hwpx_automation/handlers/quality_render.py +566 -0
- hwpx_automation/handlers/read_export.py +1295 -0
- hwpx_automation/handlers/specialized.py +624 -0
- hwpx_automation/handlers/tracked_changes.py +589 -0
- hwpx_automation/handlers/workflow.py +105 -0
- hwpx_automation/hwp_converter.py +227 -0
- hwpx_automation/hwp_support.py +94 -0
- hwpx_automation/hwpx_ops.py +1439 -0
- hwpx_automation/identity.json +263 -0
- hwpx_automation/identity.py +18 -0
- hwpx_automation/ingest_adapters.py +85 -0
- hwpx_automation/markdown_plan.py +216 -0
- hwpx_automation/mcp_cli.py +29 -0
- hwpx_automation/metadata/tools_meta.py +40 -0
- hwpx_automation/mixed_form.py +3007 -0
- hwpx_automation/mutation_models.py +401 -0
- hwpx_automation/network_policy.py +232 -0
- hwpx_automation/office/__init__.py +14 -0
- hwpx_automation/office/agent/__init__.py +125 -0
- hwpx_automation/office/agent/_batch_verification.py +383 -0
- hwpx_automation/office/agent/blueprint/__init__.py +58 -0
- hwpx_automation/office/agent/blueprint/bundle.py +282 -0
- hwpx_automation/office/agent/blueprint/catalog.py +136 -0
- hwpx_automation/office/agent/blueprint/dump.py +520 -0
- hwpx_automation/office/agent/blueprint/mapping.py +312 -0
- hwpx_automation/office/agent/blueprint/model.py +722 -0
- hwpx_automation/office/agent/blueprint/native.py +621 -0
- hwpx_automation/office/agent/blueprint/replay.py +622 -0
- hwpx_automation/office/agent/catalog.py +252 -0
- hwpx_automation/office/agent/cli.py +647 -0
- hwpx_automation/office/agent/commands.py +1383 -0
- hwpx_automation/office/agent/document.py +801 -0
- hwpx_automation/office/agent/form_plan.py +1760 -0
- hwpx_automation/office/agent/model.py +808 -0
- hwpx_automation/office/agent/path.py +155 -0
- hwpx_automation/office/agent/query.py +230 -0
- hwpx_automation/office/agent/story.py +207 -0
- hwpx_automation/office/authoring/__init__.py +3542 -0
- hwpx_automation/office/authoring/advanced_generators.py +154 -0
- hwpx_automation/office/authoring/builder/__init__.py +52 -0
- hwpx_automation/office/authoring/builder/core.py +996 -0
- hwpx_automation/office/authoring/builder/report.py +195 -0
- hwpx_automation/office/authoring/design/__init__.py +30 -0
- hwpx_automation/office/authoring/design/_support.py +144 -0
- hwpx_automation/office/authoring/design/composer.py +282 -0
- hwpx_automation/office/authoring/design/harvest.py +305 -0
- hwpx_automation/office/authoring/design/plan.py +69 -0
- hwpx_automation/office/authoring/design/profile.py +88 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/validator.py +107 -0
- hwpx_automation/office/authoring/presets/__init__.py +22 -0
- hwpx_automation/office/authoring/presets/proposal.py +538 -0
- hwpx_automation/office/authoring/report_parser.py +141 -0
- hwpx_automation/office/authoring/style_profile.py +437 -0
- hwpx_automation/office/authoring/template_analyzer.py +657 -0
- hwpx_automation/office/compliance/__init__.py +38 -0
- hwpx_automation/office/compliance/official_lint.py +478 -0
- hwpx_automation/office/compliance/pii.py +388 -0
- hwpx_automation/office/document_ops/__init__.py +13 -0
- hwpx_automation/office/document_ops/comparison.py +62 -0
- hwpx_automation/office/document_ops/mail_merge.py +73 -0
- hwpx_automation/office/document_ops/redline.py +35 -0
- hwpx_automation/office/evalplan/__init__.py +36 -0
- hwpx_automation/office/evalplan/runtime.py +2762 -0
- hwpx_automation/office/exam/__init__.py +44 -0
- hwpx_automation/office/exam/compose.py +282 -0
- hwpx_automation/office/exam/ir.py +44 -0
- hwpx_automation/office/exam/measure.py +163 -0
- hwpx_automation/office/exam/parser.py +150 -0
- hwpx_automation/office/exam/profile.py +123 -0
- hwpx_automation/office/form_fill/__init__.py +66 -0
- hwpx_automation/office/form_fill/classification.py +108 -0
- hwpx_automation/office/form_fill/fill_residue.py +242 -0
- hwpx_automation/office/form_fill/fit/__init__.py +36 -0
- hwpx_automation/office/form_fill/fit/apply.py +24 -0
- hwpx_automation/office/form_fill/fit/engine.py +24 -0
- hwpx_automation/office/form_fill/fit/measure.py +50 -0
- hwpx_automation/office/form_fill/fit/policy.py +28 -0
- hwpx_automation/office/form_fill/fit/report.py +28 -0
- hwpx_automation/office/form_fill/fit/seal.py +457 -0
- hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
- hwpx_automation/office/form_fill/guidance.py +704 -0
- hwpx_automation/office/form_fill/quality.py +961 -0
- hwpx_automation/office/form_fill/split_run.py +333 -0
- hwpx_automation/office/form_fill/template_formfit.py +656 -0
- hwpx_automation/office/house_style/__init__.py +196 -0
- hwpx_automation/office/house_style/composition.py +68 -0
- hwpx_automation/office/house_style/data/bank.json +625 -0
- hwpx_automation/office/house_style/data/genres.json +43 -0
- hwpx_automation/office/quality/__init__.py +14 -0
- hwpx_automation/office/quality/page_guard.py +277 -0
- hwpx_automation/office/rendering/__init__.py +145 -0
- hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
- hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
- hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
- hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
- hwpx_automation/office/rendering/block_splits.py +76 -0
- hwpx_automation/office/rendering/detectors.py +151 -0
- hwpx_automation/office/rendering/diff.py +153 -0
- hwpx_automation/office/rendering/fixture_corpus.py +215 -0
- hwpx_automation/office/rendering/oracle.py +909 -0
- hwpx_automation/office/rendering/page_qa.py +245 -0
- hwpx_automation/office/rendering/qa_contracts.py +293 -0
- hwpx_automation/office/rendering/qa_metrics.py +241 -0
- hwpx_automation/office/rendering/worker.py +290 -0
- hwpx_automation/office/utilities/__init__.py +12 -0
- hwpx_automation/office/utilities/table_compute.py +477 -0
- hwpx_automation/ops_services/__init__.py +1 -0
- hwpx_automation/ops_services/_border_fill.py +283 -0
- hwpx_automation/ops_services/composition.py +55 -0
- hwpx_automation/ops_services/content_layout.py +322 -0
- hwpx_automation/ops_services/context.py +213 -0
- hwpx_automation/ops_services/form_fields.py +557 -0
- hwpx_automation/ops_services/media.py +178 -0
- hwpx_automation/ops_services/memo_style.py +477 -0
- hwpx_automation/ops_services/package_validation.py +166 -0
- hwpx_automation/ops_services/planning.py +201 -0
- hwpx_automation/ops_services/preview_export.py +585 -0
- hwpx_automation/ops_services/read_query.py +601 -0
- hwpx_automation/ops_services/save_policy.py +604 -0
- hwpx_automation/ops_services/tables.py +539 -0
- hwpx_automation/ops_services/transactions.py +616 -0
- hwpx_automation/preview_output_models.py +69 -0
- hwpx_automation/public-modules.json +206 -0
- hwpx_automation/py.typed +1 -0
- hwpx_automation/quality.py +351 -0
- hwpx_automation/quality_generation.py +725 -0
- hwpx_automation/runtime.py +321 -0
- hwpx_automation/runtime_services.py +100 -0
- hwpx_automation/server.py +259 -0
- hwpx_automation/storage.py +747 -0
- hwpx_automation/tool_bindings.py +170 -0
- hwpx_automation/tool_contract.py +982 -0
- hwpx_automation/upstream.py +755 -0
- hwpx_automation/utils/__init__.py +2 -0
- hwpx_automation/utils/helpers.py +29 -0
- hwpx_automation/visual_qa.py +667 -0
- hwpx_automation/workflow/__init__.py +55 -0
- hwpx_automation/workflow/adapters.py +482 -0
- hwpx_automation/workflow/dispatcher.py +213 -0
- hwpx_automation/workflow/models.py +243 -0
- hwpx_automation/workflow/policy.py +198 -0
- hwpx_automation/workflow/render_contracts.py +173 -0
- hwpx_automation/workflow/render_metrics.py +196 -0
- hwpx_automation/workflow/render_queue.py +482 -0
- hwpx_automation/workflow/render_security.py +172 -0
- hwpx_automation/workflow/render_transport.py +369 -0
- hwpx_automation/workflow/rendering.py +206 -0
- hwpx_automation/workflow/service.py +758 -0
- hwpx_automation/workflow/state_machine.py +65 -0
- hwpx_automation/workflow/store.py +747 -0
- hwpx_automation/workspace.py +1694 -0
- python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
- python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
- python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
- python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
- python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# ruff: noqa: E402
|
|
3
|
+
"""Thin integration helpers for the upstream ``python-hwpx`` library.
|
|
4
|
+
|
|
5
|
+
Keep version-sensitive imports and non-obvious upstream calls here so the rest
|
|
6
|
+
of the codebase has fewer direct dependencies on internal upstream details.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from copy import deepcopy
|
|
12
|
+
from os import PathLike
|
|
13
|
+
from typing import Any, Iterable, Literal
|
|
14
|
+
from xml.etree import ElementTree as ET
|
|
15
|
+
|
|
16
|
+
from .compat import patch_python_hwpx
|
|
17
|
+
|
|
18
|
+
# Compatibility patching must precede version-sensitive python-hwpx imports.
|
|
19
|
+
patch_python_hwpx()
|
|
20
|
+
|
|
21
|
+
from hwpx import ObjectFinder
|
|
22
|
+
from hwpx.document import (
|
|
23
|
+
HwpxDocument,
|
|
24
|
+
)
|
|
25
|
+
from hwpx.oxml.memo import HwpxOxmlMemo
|
|
26
|
+
from hwpx.oxml.paragraph import HwpxOxmlParagraph
|
|
27
|
+
from hwpx.oxml.run import HwpxOxmlRun
|
|
28
|
+
from hwpx.oxml.table import HwpxOxmlTable
|
|
29
|
+
from hwpx.opc.package import HwpxPackage
|
|
30
|
+
from hwpx.templates import blank_document_bytes
|
|
31
|
+
from hwpx.tools.text_extractor import AnnotationOptions, TextExtractor
|
|
32
|
+
from hwpx.tools.validator import ValidationReport, validate_document
|
|
33
|
+
from hwpx.oxml.namespaces import HH as HH_NS, HP as HP_NS
|
|
34
|
+
|
|
35
|
+
_FONT_REF_KEYS = (
|
|
36
|
+
"hangul",
|
|
37
|
+
"latin",
|
|
38
|
+
"hanja",
|
|
39
|
+
"japanese",
|
|
40
|
+
"other",
|
|
41
|
+
"symbol",
|
|
42
|
+
"user",
|
|
43
|
+
)
|
|
44
|
+
_FONT_FACE_LANGS = {
|
|
45
|
+
"HANGUL": "hangul",
|
|
46
|
+
"LATIN": "latin",
|
|
47
|
+
"HANJA": "hanja",
|
|
48
|
+
"JAPANESE": "japanese",
|
|
49
|
+
"OTHER": "other",
|
|
50
|
+
"SYMBOL": "symbol",
|
|
51
|
+
"USER": "user",
|
|
52
|
+
}
|
|
53
|
+
_DEFAULT_TEXT_COLOR = "#000000"
|
|
54
|
+
_DEFAULT_CHAR_HEIGHT = "1000"
|
|
55
|
+
_PATHOLOGICAL_CHAR_SPACING_LIMIT = -40
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def open_document(source: str | PathLike[str] | Any) -> HwpxDocument:
|
|
59
|
+
return HwpxDocument.open(source)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def new_document() -> HwpxDocument:
|
|
63
|
+
return HwpxDocument.new()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def blank_document_template_bytes() -> bytes:
|
|
67
|
+
return blank_document_bytes()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def open_package(path: str | PathLike[str]) -> HwpxPackage:
|
|
71
|
+
return HwpxPackage.open(path)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def create_text_extractor(path: str | PathLike[str]) -> TextExtractor:
|
|
75
|
+
return TextExtractor(path)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def create_object_finder(path: str | PathLike[str]) -> ObjectFinder:
|
|
79
|
+
return ObjectFinder(path)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def validate_document_path(path: str | PathLike[str]) -> ValidationReport:
|
|
83
|
+
return validate_document(path)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def export_document(document: HwpxDocument, output_format: Literal["text", "html", "markdown"]) -> str:
|
|
87
|
+
if output_format == "text":
|
|
88
|
+
return document.export_text()
|
|
89
|
+
if output_format == "html":
|
|
90
|
+
return document.export_html()
|
|
91
|
+
if output_format == "markdown":
|
|
92
|
+
return document.export_markdown()
|
|
93
|
+
raise ValueError(f"unsupported export format: {output_format}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def default_cell_width() -> int:
|
|
97
|
+
"""Return the upstream default table-cell width.
|
|
98
|
+
|
|
99
|
+
``python-hwpx`` currently stores this as the private module constant
|
|
100
|
+
``hwpx.oxml.document._DEFAULT_CELL_WIDTH``. Centralize that lookup here so a
|
|
101
|
+
future upstream change only touches one place downstream.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
try: # pragma: no branch - single lookup with defensive fallback
|
|
105
|
+
from hwpx.oxml import document as hwpx_document_module
|
|
106
|
+
|
|
107
|
+
return int(getattr(hwpx_document_module, "_DEFAULT_CELL_WIDTH", 7200))
|
|
108
|
+
except Exception: # pragma: no cover - fallback only used if upstream internals move
|
|
109
|
+
return 7200
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def append_xml_child(parent: ET.Element, tag: str, attrs: dict[str, str] | None = None) -> ET.Element:
|
|
113
|
+
payload = dict(attrs or {})
|
|
114
|
+
try:
|
|
115
|
+
return ET.SubElement(parent, tag, payload)
|
|
116
|
+
except TypeError:
|
|
117
|
+
maker = getattr(parent, "makeelement", None)
|
|
118
|
+
if not callable(maker):
|
|
119
|
+
raise
|
|
120
|
+
child = maker(tag, payload)
|
|
121
|
+
parent.append(child)
|
|
122
|
+
return child
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def primary_header(document: HwpxDocument) -> Any:
|
|
126
|
+
if not document.headers:
|
|
127
|
+
raise RuntimeError("document does not contain any headers to host styles")
|
|
128
|
+
return document.headers[0]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def style_identifier(style: Any) -> str | None:
|
|
132
|
+
raw_id = getattr(style, "raw_id", None)
|
|
133
|
+
if raw_id:
|
|
134
|
+
return raw_id
|
|
135
|
+
identifier = getattr(style, "id", None)
|
|
136
|
+
if identifier is not None:
|
|
137
|
+
return str(identifier)
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def iter_unique_styles(document: HwpxDocument):
|
|
142
|
+
seen: set[str] = set()
|
|
143
|
+
for style in document.styles.values():
|
|
144
|
+
style_id = style_identifier(style)
|
|
145
|
+
if style_id is None or style_id in seen:
|
|
146
|
+
continue
|
|
147
|
+
seen.add(style_id)
|
|
148
|
+
yield style
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def resolve_style_id(document: HwpxDocument, style: str | None) -> str | None:
|
|
152
|
+
if style is None:
|
|
153
|
+
return None
|
|
154
|
+
value = style.strip()
|
|
155
|
+
if not value:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
resolved = document.style(value)
|
|
159
|
+
if resolved is not None:
|
|
160
|
+
style_id = style_identifier(resolved)
|
|
161
|
+
if style_id is not None:
|
|
162
|
+
return style_id
|
|
163
|
+
|
|
164
|
+
for candidate in iter_unique_styles(document):
|
|
165
|
+
if value not in {candidate.name, candidate.eng_name}:
|
|
166
|
+
continue
|
|
167
|
+
style_id = style_identifier(candidate)
|
|
168
|
+
if style_id is not None:
|
|
169
|
+
return style_id
|
|
170
|
+
|
|
171
|
+
raise ValueError(f"unknown style reference: {value}")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def normalize_hex_color(color: str | None, *, field_name: str = "color") -> str | None:
|
|
175
|
+
if color is None:
|
|
176
|
+
return None
|
|
177
|
+
value = color.strip()
|
|
178
|
+
if not value:
|
|
179
|
+
return None
|
|
180
|
+
if not value.startswith("#"):
|
|
181
|
+
value = "#" + value
|
|
182
|
+
if len(value) != 7 or any(ch not in "0123456789abcdefABCDEF#" for ch in value):
|
|
183
|
+
raise ValueError(f"{field_name} must be a 6-digit hexadecimal value")
|
|
184
|
+
return value.upper()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def normalize_font_size(font_size: float | None) -> str | None:
|
|
188
|
+
if font_size is None:
|
|
189
|
+
return None
|
|
190
|
+
if font_size <= 0:
|
|
191
|
+
raise ValueError("font_size must be greater than 0")
|
|
192
|
+
return str(int(round(font_size * 100)))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def run_style_flags(run_style: Any) -> tuple[bool, bool, bool]:
|
|
196
|
+
if run_style is None:
|
|
197
|
+
return False, False, False
|
|
198
|
+
children = getattr(run_style, "child_attributes", {})
|
|
199
|
+
underline_attrs = children.get("underline")
|
|
200
|
+
underline = False
|
|
201
|
+
if underline_attrs is not None:
|
|
202
|
+
underline = underline_attrs.get("type", "").upper() != "NONE"
|
|
203
|
+
return "bold" in children, "italic" in children, underline
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def run_style_height(run_style: Any) -> str | None:
|
|
207
|
+
if run_style is None:
|
|
208
|
+
return None
|
|
209
|
+
return getattr(run_style, "attributes", {}).get("height")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def run_style_font_refs(run_style: Any) -> dict[str, str]:
|
|
213
|
+
if run_style is None:
|
|
214
|
+
return {}
|
|
215
|
+
refs = dict(getattr(run_style, "child_attributes", {}).get("fontRef", {}))
|
|
216
|
+
return {key: str(value) for key, value in refs.items() if key in _FONT_REF_KEYS}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def element_style_flags(element: ET.Element) -> tuple[bool, bool, bool]:
|
|
220
|
+
underline_node = element.find(f"{HH_NS}underline")
|
|
221
|
+
underline = False
|
|
222
|
+
if underline_node is not None:
|
|
223
|
+
underline = underline_node.get("type", "").upper() != "NONE"
|
|
224
|
+
return (
|
|
225
|
+
element.find(f"{HH_NS}bold") is not None,
|
|
226
|
+
element.find(f"{HH_NS}italic") is not None,
|
|
227
|
+
underline,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def element_font_refs(element: ET.Element) -> dict[str, str]:
|
|
232
|
+
font_ref = element.find(f"{HH_NS}fontRef")
|
|
233
|
+
if font_ref is None:
|
|
234
|
+
return {}
|
|
235
|
+
return {key: str(value) for key, value in font_ref.attrib.items() if key in _FONT_REF_KEYS}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def char_style_matches(
|
|
239
|
+
run_style: Any,
|
|
240
|
+
*,
|
|
241
|
+
flags: tuple[bool, bool, bool],
|
|
242
|
+
color: str,
|
|
243
|
+
height: str,
|
|
244
|
+
font_refs: dict[str, str],
|
|
245
|
+
) -> bool:
|
|
246
|
+
if run_style_flags(run_style) != flags:
|
|
247
|
+
return False
|
|
248
|
+
if ((run_style.text_color() if run_style is not None else None) or _DEFAULT_TEXT_COLOR).upper() != color:
|
|
249
|
+
return False
|
|
250
|
+
if (run_style_height(run_style) or _DEFAULT_CHAR_HEIGHT) != height:
|
|
251
|
+
return False
|
|
252
|
+
current_font_refs = run_style_font_refs(run_style)
|
|
253
|
+
for key in _FONT_REF_KEYS:
|
|
254
|
+
if current_font_refs.get(key) != font_refs.get(key):
|
|
255
|
+
return False
|
|
256
|
+
return True
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _ref_list_element(header: Any) -> ET.Element:
|
|
260
|
+
element = header.element.find(f"{HH_NS}refList")
|
|
261
|
+
if element is None:
|
|
262
|
+
raise RuntimeError("document header does not expose a refList element")
|
|
263
|
+
return element
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _fontfaces_element(header: Any) -> ET.Element:
|
|
267
|
+
ref_list = _ref_list_element(header)
|
|
268
|
+
fontfaces = ref_list.find(f"{HH_NS}fontfaces")
|
|
269
|
+
if fontfaces is None:
|
|
270
|
+
raise RuntimeError("document header does not expose fontfaces")
|
|
271
|
+
return fontfaces
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _find_fontface_bucket(fontfaces: ET.Element, lang: str) -> ET.Element | None:
|
|
275
|
+
for bucket in fontfaces.findall(f"{HH_NS}fontface"):
|
|
276
|
+
if (bucket.get("lang") or "").upper() == lang:
|
|
277
|
+
return bucket
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _next_font_id(bucket: ET.Element) -> str:
|
|
282
|
+
numeric_ids: list[int] = []
|
|
283
|
+
for child in bucket.findall(f"{HH_NS}font"):
|
|
284
|
+
raw_id = child.get("id")
|
|
285
|
+
if raw_id is None:
|
|
286
|
+
continue
|
|
287
|
+
try:
|
|
288
|
+
numeric_ids.append(int(raw_id))
|
|
289
|
+
except ValueError:
|
|
290
|
+
continue
|
|
291
|
+
return "0" if not numeric_ids else str(max(numeric_ids) + 1)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def ensure_font_face_refs(header: Any, font_name: str) -> dict[str, str]:
|
|
295
|
+
name = (font_name or "").strip()
|
|
296
|
+
if not name:
|
|
297
|
+
raise ValueError("font_name cannot be empty")
|
|
298
|
+
|
|
299
|
+
fontfaces = _fontfaces_element(header)
|
|
300
|
+
refs: dict[str, str] = {}
|
|
301
|
+
changed = False
|
|
302
|
+
|
|
303
|
+
for lang, ref_key in _FONT_FACE_LANGS.items():
|
|
304
|
+
bucket = _find_fontface_bucket(fontfaces, lang)
|
|
305
|
+
if bucket is None:
|
|
306
|
+
bucket = append_xml_child(fontfaces, f"{HH_NS}fontface", {"lang": lang, "fontCnt": "0"})
|
|
307
|
+
changed = True
|
|
308
|
+
|
|
309
|
+
font_element = None
|
|
310
|
+
for candidate in bucket.findall(f"{HH_NS}font"):
|
|
311
|
+
if (candidate.get("face") or "").strip() == name:
|
|
312
|
+
font_element = candidate
|
|
313
|
+
break
|
|
314
|
+
|
|
315
|
+
if font_element is None:
|
|
316
|
+
font_element = append_xml_child(
|
|
317
|
+
bucket,
|
|
318
|
+
f"{HH_NS}font",
|
|
319
|
+
{
|
|
320
|
+
"id": _next_font_id(bucket),
|
|
321
|
+
"face": name,
|
|
322
|
+
"type": "TTF",
|
|
323
|
+
"isEmbedded": "0",
|
|
324
|
+
},
|
|
325
|
+
)
|
|
326
|
+
bucket.set("fontCnt", str(len(bucket.findall(f"{HH_NS}font"))))
|
|
327
|
+
changed = True
|
|
328
|
+
|
|
329
|
+
font_id = font_element.get("id")
|
|
330
|
+
if not font_id:
|
|
331
|
+
raise RuntimeError("fontface entry is missing an id")
|
|
332
|
+
refs[ref_key] = font_id
|
|
333
|
+
|
|
334
|
+
if changed:
|
|
335
|
+
header.mark_dirty()
|
|
336
|
+
return refs
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def ensure_char_style(
|
|
340
|
+
document: HwpxDocument,
|
|
341
|
+
*,
|
|
342
|
+
base_char_pr_id: str | int | None,
|
|
343
|
+
bold: bool | None = None,
|
|
344
|
+
italic: bool | None = None,
|
|
345
|
+
underline: bool | None = None,
|
|
346
|
+
font_size: float | None = None,
|
|
347
|
+
font_name: str | None = None,
|
|
348
|
+
color: str | None = None,
|
|
349
|
+
) -> str:
|
|
350
|
+
"""Return a ``charPr`` id matching the requested formatting.
|
|
351
|
+
|
|
352
|
+
This prefers the public-ish upstream helper ``document.ensure_run_style()``
|
|
353
|
+
when only bold/italic/underline flags are involved. When color, font size,
|
|
354
|
+
or font family overrides are requested, we fall back to the header-level
|
|
355
|
+
``ensure_char_property()`` hook so downstream behavior stays aligned with the
|
|
356
|
+
current upstream document engine.
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
base_style = document.char_property(base_char_pr_id)
|
|
360
|
+
base_flags = run_style_flags(base_style)
|
|
361
|
+
target_flags = (
|
|
362
|
+
base_flags[0] if bold is None else bool(bold),
|
|
363
|
+
base_flags[1] if italic is None else bool(italic),
|
|
364
|
+
base_flags[2] if underline is None else bool(underline),
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
if font_size is None and font_name is None and color is None:
|
|
368
|
+
return document.ensure_run_style(
|
|
369
|
+
bold=target_flags[0],
|
|
370
|
+
italic=target_flags[1],
|
|
371
|
+
underline=target_flags[2],
|
|
372
|
+
base_char_pr_id=base_char_pr_id,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
header = primary_header(document)
|
|
376
|
+
target_color = normalize_hex_color(color) or (
|
|
377
|
+
(base_style.text_color() if base_style is not None else None) or _DEFAULT_TEXT_COLOR
|
|
378
|
+
).upper()
|
|
379
|
+
target_height = normalize_font_size(font_size) or run_style_height(base_style) or _DEFAULT_CHAR_HEIGHT
|
|
380
|
+
target_font_refs = {
|
|
381
|
+
key: value for key, value in run_style_font_refs(base_style).items() if key in _FONT_REF_KEYS
|
|
382
|
+
}
|
|
383
|
+
if not target_font_refs:
|
|
384
|
+
target_font_refs = {key: "0" for key in _FONT_REF_KEYS}
|
|
385
|
+
if font_name is not None:
|
|
386
|
+
target_font_refs.update(ensure_font_face_refs(header, font_name))
|
|
387
|
+
|
|
388
|
+
if base_char_pr_id is not None and char_style_matches(
|
|
389
|
+
base_style,
|
|
390
|
+
flags=target_flags,
|
|
391
|
+
color=target_color,
|
|
392
|
+
height=target_height,
|
|
393
|
+
font_refs=target_font_refs,
|
|
394
|
+
):
|
|
395
|
+
return str(base_char_pr_id)
|
|
396
|
+
|
|
397
|
+
def predicate(element: ET.Element) -> bool:
|
|
398
|
+
if element_style_flags(element) != target_flags:
|
|
399
|
+
return False
|
|
400
|
+
if (element.get("textColor", _DEFAULT_TEXT_COLOR) or _DEFAULT_TEXT_COLOR).upper() != target_color:
|
|
401
|
+
return False
|
|
402
|
+
if element.get("height", _DEFAULT_CHAR_HEIGHT) != target_height:
|
|
403
|
+
return False
|
|
404
|
+
existing_font_refs = element_font_refs(element)
|
|
405
|
+
for key in _FONT_REF_KEYS:
|
|
406
|
+
if existing_font_refs.get(key) != target_font_refs.get(key):
|
|
407
|
+
return False
|
|
408
|
+
return True
|
|
409
|
+
|
|
410
|
+
def modifier(element: ET.Element) -> None:
|
|
411
|
+
underline_nodes = list(element.findall(f"{HH_NS}underline"))
|
|
412
|
+
existing_underline = dict(underline_nodes[0].attrib) if underline_nodes else {}
|
|
413
|
+
|
|
414
|
+
for child in list(element.findall(f"{HH_NS}bold")):
|
|
415
|
+
element.remove(child)
|
|
416
|
+
for child in list(element.findall(f"{HH_NS}italic")):
|
|
417
|
+
element.remove(child)
|
|
418
|
+
for child in underline_nodes:
|
|
419
|
+
element.remove(child)
|
|
420
|
+
|
|
421
|
+
if target_flags[0]:
|
|
422
|
+
append_xml_child(element, f"{HH_NS}bold")
|
|
423
|
+
if target_flags[1]:
|
|
424
|
+
append_xml_child(element, f"{HH_NS}italic")
|
|
425
|
+
|
|
426
|
+
underline_attrs = dict(existing_underline)
|
|
427
|
+
underline_attrs.setdefault("shape", existing_underline.get("shape", "SOLID"))
|
|
428
|
+
underline_attrs["color"] = underline_attrs.get("color", target_color) or target_color
|
|
429
|
+
if target_flags[2]:
|
|
430
|
+
underline_attrs["type"] = underline_attrs.get("type", "SOLID") or "SOLID"
|
|
431
|
+
if underline_attrs["type"].upper() == "NONE":
|
|
432
|
+
underline_attrs["type"] = "SOLID"
|
|
433
|
+
underline_attrs["color"] = target_color
|
|
434
|
+
else:
|
|
435
|
+
underline_attrs["type"] = "NONE"
|
|
436
|
+
append_xml_child(element, f"{HH_NS}underline", underline_attrs)
|
|
437
|
+
|
|
438
|
+
element.set("textColor", target_color)
|
|
439
|
+
element.set("height", target_height)
|
|
440
|
+
|
|
441
|
+
font_ref = element.find(f"{HH_NS}fontRef")
|
|
442
|
+
if font_ref is None:
|
|
443
|
+
font_ref = append_xml_child(element, f"{HH_NS}fontRef")
|
|
444
|
+
for key in _FONT_REF_KEYS:
|
|
445
|
+
value = target_font_refs.get(key)
|
|
446
|
+
if value is not None:
|
|
447
|
+
font_ref.set(key, str(value))
|
|
448
|
+
|
|
449
|
+
char_element = header.ensure_char_property(
|
|
450
|
+
predicate=predicate,
|
|
451
|
+
modifier=modifier,
|
|
452
|
+
base_char_pr_id=base_char_pr_id,
|
|
453
|
+
)
|
|
454
|
+
char_id = char_element.get("id")
|
|
455
|
+
if not char_id:
|
|
456
|
+
raise RuntimeError("char property does not expose an identifier")
|
|
457
|
+
return char_id
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _char_property_element(
|
|
461
|
+
document: HwpxDocument,
|
|
462
|
+
char_pr_id_ref: str | int | None,
|
|
463
|
+
) -> tuple[Any, ET.Element] | None:
|
|
464
|
+
if char_pr_id_ref is None:
|
|
465
|
+
return None
|
|
466
|
+
target = str(char_pr_id_ref).strip()
|
|
467
|
+
if not target:
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
for header in document.headers:
|
|
471
|
+
ref_list = header.element.find(f"{HH_NS}refList")
|
|
472
|
+
if ref_list is None:
|
|
473
|
+
continue
|
|
474
|
+
char_properties = ref_list.find(f"{HH_NS}charProperties")
|
|
475
|
+
if char_properties is None:
|
|
476
|
+
continue
|
|
477
|
+
for element in char_properties.findall(f"{HH_NS}charPr"):
|
|
478
|
+
if (element.get("id") or "").strip() == target:
|
|
479
|
+
return header, element
|
|
480
|
+
return None
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _spacing_values(element: ET.Element | None) -> dict[str, int]:
|
|
484
|
+
if element is None:
|
|
485
|
+
return {}
|
|
486
|
+
spacing = element.find(f"{HH_NS}spacing")
|
|
487
|
+
if spacing is None:
|
|
488
|
+
return {}
|
|
489
|
+
|
|
490
|
+
values: dict[str, int] = {}
|
|
491
|
+
for key in _FONT_REF_KEYS:
|
|
492
|
+
raw = spacing.get(key)
|
|
493
|
+
if raw is None:
|
|
494
|
+
continue
|
|
495
|
+
try:
|
|
496
|
+
values[key] = int(raw)
|
|
497
|
+
except ValueError:
|
|
498
|
+
continue
|
|
499
|
+
return values
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _safe_spacing_fallback_refs(
|
|
503
|
+
document: HwpxDocument,
|
|
504
|
+
paragraph: Any | None,
|
|
505
|
+
explicit_ref: str | int | None,
|
|
506
|
+
) -> list[str]:
|
|
507
|
+
refs: list[str] = []
|
|
508
|
+
|
|
509
|
+
def append(value: str | int | None) -> None:
|
|
510
|
+
if value is None:
|
|
511
|
+
return
|
|
512
|
+
normalized = str(value).strip()
|
|
513
|
+
if normalized and normalized not in refs:
|
|
514
|
+
refs.append(normalized)
|
|
515
|
+
|
|
516
|
+
append(explicit_ref)
|
|
517
|
+
if paragraph is not None:
|
|
518
|
+
style = document.style(getattr(paragraph, "style_id_ref", None))
|
|
519
|
+
append(getattr(style, "char_pr_id_ref", None))
|
|
520
|
+
append(getattr(paragraph, "char_pr_id_ref", None))
|
|
521
|
+
|
|
522
|
+
style_zero_ref: str | int | None = None
|
|
523
|
+
for style in iter_unique_styles(document):
|
|
524
|
+
style_id = style_identifier(style)
|
|
525
|
+
name = str(getattr(style, "name", None) or "").strip()
|
|
526
|
+
eng_name = str(getattr(style, "eng_name", None) or "").strip().lower()
|
|
527
|
+
char_ref = getattr(style, "char_pr_id_ref", None)
|
|
528
|
+
if name in {"바탕글", "본문"} or eng_name in {"normal", "body"}:
|
|
529
|
+
append(char_ref)
|
|
530
|
+
if style_id == "0":
|
|
531
|
+
style_zero_ref = char_ref
|
|
532
|
+
append(style_zero_ref)
|
|
533
|
+
append("0")
|
|
534
|
+
return refs
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _element_signature(element: ET.Element) -> bytes:
|
|
538
|
+
clone = deepcopy(element)
|
|
539
|
+
clone.attrib.pop("id", None)
|
|
540
|
+
return ET.tostring(clone, encoding="utf-8")
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _run_element(run: Any) -> ET.Element | None:
|
|
544
|
+
element = getattr(run, "element", None)
|
|
545
|
+
if element is not None:
|
|
546
|
+
return element
|
|
547
|
+
if hasattr(run, "get") and hasattr(run, "set"):
|
|
548
|
+
return run
|
|
549
|
+
return None
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def repair_pathological_text_spacing(
|
|
553
|
+
document: HwpxDocument,
|
|
554
|
+
*,
|
|
555
|
+
paragraph: Any | None = None,
|
|
556
|
+
runs: Iterable[Any] | None = None,
|
|
557
|
+
fallback_char_pr_id: str | int | None = None,
|
|
558
|
+
) -> int:
|
|
559
|
+
"""Remap touched text runs whose character spacing would over-print glyphs.
|
|
560
|
+
|
|
561
|
+
Some form templates use ``hh:spacing=-50`` on short placeholder runs. If a
|
|
562
|
+
replacement or newly-added paragraph blindly keeps that ``charPr``, Hancom
|
|
563
|
+
renders normal text with roughly half-width glyph advance and the letters
|
|
564
|
+
overlap. Only the touched runs are remapped: the source ``charPr`` remains
|
|
565
|
+
byte-for-byte intact, while a deduplicated clone changes only pathological
|
|
566
|
+
spacing values (``<= -40``) to a safe paragraph/body-style fallback.
|
|
567
|
+
"""
|
|
568
|
+
|
|
569
|
+
selected_runs = list(runs if runs is not None else (getattr(paragraph, "runs", None) or []))
|
|
570
|
+
if not selected_runs:
|
|
571
|
+
return 0
|
|
572
|
+
|
|
573
|
+
fallback_values: list[dict[str, int]] = []
|
|
574
|
+
for fallback_ref in _safe_spacing_fallback_refs(document, paragraph, fallback_char_pr_id):
|
|
575
|
+
found = _char_property_element(document, fallback_ref)
|
|
576
|
+
if found is not None:
|
|
577
|
+
fallback_values.append(_spacing_values(found[1]))
|
|
578
|
+
|
|
579
|
+
remapped = 0
|
|
580
|
+
replacements_by_ref: dict[str, str] = {}
|
|
581
|
+
for run in selected_runs:
|
|
582
|
+
element = _run_element(run)
|
|
583
|
+
if element is None:
|
|
584
|
+
continue
|
|
585
|
+
base_ref = (element.get("charPrIDRef") or "").strip()
|
|
586
|
+
if not base_ref:
|
|
587
|
+
continue
|
|
588
|
+
|
|
589
|
+
found = _char_property_element(document, base_ref)
|
|
590
|
+
if found is None:
|
|
591
|
+
continue
|
|
592
|
+
header, base_element = found
|
|
593
|
+
base_spacing = _spacing_values(base_element)
|
|
594
|
+
unsafe_keys = [
|
|
595
|
+
key
|
|
596
|
+
for key, value in base_spacing.items()
|
|
597
|
+
if value <= _PATHOLOGICAL_CHAR_SPACING_LIMIT
|
|
598
|
+
]
|
|
599
|
+
if not unsafe_keys:
|
|
600
|
+
continue
|
|
601
|
+
|
|
602
|
+
target_ref = replacements_by_ref.get(base_ref)
|
|
603
|
+
if target_ref is None:
|
|
604
|
+
target_values: dict[str, int] = {}
|
|
605
|
+
for key in unsafe_keys:
|
|
606
|
+
safe_value = 0
|
|
607
|
+
for candidate in fallback_values:
|
|
608
|
+
value = candidate.get(key, 0)
|
|
609
|
+
if value > _PATHOLOGICAL_CHAR_SPACING_LIMIT:
|
|
610
|
+
safe_value = value
|
|
611
|
+
break
|
|
612
|
+
target_values[key] = safe_value
|
|
613
|
+
|
|
614
|
+
target_element = deepcopy(base_element)
|
|
615
|
+
target_element.attrib.pop("id", None)
|
|
616
|
+
target_spacing = target_element.find(f"{HH_NS}spacing")
|
|
617
|
+
if target_spacing is None: # pragma: no cover - unsafe keys imply spacing exists
|
|
618
|
+
target_spacing = append_xml_child(target_element, f"{HH_NS}spacing")
|
|
619
|
+
for key, value in target_values.items():
|
|
620
|
+
target_spacing.set(key, str(value))
|
|
621
|
+
target_signature = _element_signature(target_element)
|
|
622
|
+
|
|
623
|
+
def predicate(candidate: ET.Element) -> bool:
|
|
624
|
+
return _element_signature(candidate) == target_signature
|
|
625
|
+
|
|
626
|
+
def modifier(candidate: ET.Element) -> None:
|
|
627
|
+
spacing = candidate.find(f"{HH_NS}spacing")
|
|
628
|
+
if spacing is None: # pragma: no cover - cloned unsafe style has spacing
|
|
629
|
+
spacing = append_xml_child(candidate, f"{HH_NS}spacing")
|
|
630
|
+
for key, value in target_values.items():
|
|
631
|
+
spacing.set(key, str(value))
|
|
632
|
+
|
|
633
|
+
safe_element = header.ensure_char_property(
|
|
634
|
+
predicate=predicate,
|
|
635
|
+
modifier=modifier,
|
|
636
|
+
base_char_pr_id=base_ref,
|
|
637
|
+
)
|
|
638
|
+
target_ref = safe_element.get("id")
|
|
639
|
+
if not target_ref: # pragma: no cover - upstream guarantees an id
|
|
640
|
+
raise RuntimeError("safe character property does not expose an identifier")
|
|
641
|
+
replacements_by_ref[base_ref] = target_ref
|
|
642
|
+
|
|
643
|
+
if hasattr(run, "char_pr_id_ref"):
|
|
644
|
+
run.char_pr_id_ref = target_ref
|
|
645
|
+
else:
|
|
646
|
+
element.set("charPrIDRef", target_ref)
|
|
647
|
+
section = getattr(paragraph, "section", None)
|
|
648
|
+
if section is not None and hasattr(section, "mark_dirty"):
|
|
649
|
+
section.mark_dirty()
|
|
650
|
+
remapped += 1
|
|
651
|
+
|
|
652
|
+
return remapped
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def document_styles_element(document: HwpxDocument) -> ET.Element:
|
|
656
|
+
"""Return the raw ``<hh:styles>`` element for the primary header.
|
|
657
|
+
|
|
658
|
+
This currently relies on the private upstream helper ``_styles_element()``
|
|
659
|
+
because ``python-hwpx`` does not expose a public style-creation API yet.
|
|
660
|
+
"""
|
|
661
|
+
|
|
662
|
+
header = primary_header(document)
|
|
663
|
+
styles_element = header._styles_element()
|
|
664
|
+
if styles_element is None:
|
|
665
|
+
raise RuntimeError("document header does not expose styles")
|
|
666
|
+
return styles_element
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def style_element_by_id(styles_element: ET.Element, style_id: str) -> ET.Element | None:
|
|
670
|
+
for style_element in styles_element.findall(f"{HH_NS}style"):
|
|
671
|
+
raw_id = style_element.get("id")
|
|
672
|
+
if raw_id == style_id:
|
|
673
|
+
return style_element
|
|
674
|
+
try:
|
|
675
|
+
if raw_id is not None and str(int(raw_id)) == style_id:
|
|
676
|
+
return style_element
|
|
677
|
+
except ValueError:
|
|
678
|
+
continue
|
|
679
|
+
return None
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def next_numeric_style_id(styles_element: ET.Element) -> str:
|
|
683
|
+
numeric_ids: list[int] = []
|
|
684
|
+
for style_element in styles_element.findall(f"{HH_NS}style"):
|
|
685
|
+
raw_id = style_element.get("id")
|
|
686
|
+
if raw_id is None:
|
|
687
|
+
continue
|
|
688
|
+
try:
|
|
689
|
+
numeric_ids.append(int(raw_id))
|
|
690
|
+
except ValueError:
|
|
691
|
+
continue
|
|
692
|
+
return "0" if not numeric_ids else str(max(numeric_ids) + 1)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def update_styles_item_count(styles_element: ET.Element) -> None:
|
|
696
|
+
styles_element.set("itemCnt", str(len(styles_element.findall(f"{HH_NS}style"))))
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def default_base_style_id(document: HwpxDocument) -> str:
|
|
700
|
+
for candidate in ("1", "본문", "Body", "0", "바탕글", "Normal"):
|
|
701
|
+
try:
|
|
702
|
+
resolved = resolve_style_id(document, candidate)
|
|
703
|
+
except ValueError:
|
|
704
|
+
continue
|
|
705
|
+
if resolved is not None:
|
|
706
|
+
return resolved
|
|
707
|
+
|
|
708
|
+
for style in iter_unique_styles(document):
|
|
709
|
+
style_id = style_identifier(style)
|
|
710
|
+
if style_id is not None:
|
|
711
|
+
return style_id
|
|
712
|
+
|
|
713
|
+
raise RuntimeError("document does not contain any styles")
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
__all__ = [
|
|
717
|
+
"AnnotationOptions",
|
|
718
|
+
"HH_NS",
|
|
719
|
+
"HP_NS",
|
|
720
|
+
"HwpxDocument",
|
|
721
|
+
"HwpxOxmlMemo",
|
|
722
|
+
"HwpxOxmlParagraph",
|
|
723
|
+
"HwpxOxmlRun",
|
|
724
|
+
"HwpxOxmlTable",
|
|
725
|
+
"ValidationReport",
|
|
726
|
+
"append_xml_child",
|
|
727
|
+
"blank_document_template_bytes",
|
|
728
|
+
"char_style_matches",
|
|
729
|
+
"create_object_finder",
|
|
730
|
+
"create_text_extractor",
|
|
731
|
+
"default_base_style_id",
|
|
732
|
+
"default_cell_width",
|
|
733
|
+
"document_styles_element",
|
|
734
|
+
"element_font_refs",
|
|
735
|
+
"element_style_flags",
|
|
736
|
+
"ensure_char_style",
|
|
737
|
+
"ensure_font_face_refs",
|
|
738
|
+
"export_document",
|
|
739
|
+
"iter_unique_styles",
|
|
740
|
+
"new_document",
|
|
741
|
+
"normalize_font_size",
|
|
742
|
+
"normalize_hex_color",
|
|
743
|
+
"open_document",
|
|
744
|
+
"open_package",
|
|
745
|
+
"primary_header",
|
|
746
|
+
"resolve_style_id",
|
|
747
|
+
"run_style_flags",
|
|
748
|
+
"run_style_font_refs",
|
|
749
|
+
"run_style_height",
|
|
750
|
+
"style_element_by_id",
|
|
751
|
+
"style_identifier",
|
|
752
|
+
"update_styles_item_count",
|
|
753
|
+
"validate_document_path",
|
|
754
|
+
"next_numeric_style_id",
|
|
755
|
+
]
|