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,277 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Proxy checks for layout drift between a reference and an output HWPX.
|
|
3
|
+
|
|
4
|
+
This module does not calculate rendered page counts. It compares structural and
|
|
5
|
+
textual metrics that often correlate with page-layout drift.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
from dataclasses import asdict, dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import BinaryIO, Iterable, Sequence
|
|
15
|
+
|
|
16
|
+
from hwpx import HwpxPackage
|
|
17
|
+
from lxml import etree # pyright: ignore[reportAttributeAccessIssue]
|
|
18
|
+
|
|
19
|
+
NS = {
|
|
20
|
+
"hp": "http://www.hancom.co.kr/hwpml/2011/paragraph",
|
|
21
|
+
"hs": "http://www.hancom.co.kr/hwpml/2011/section",
|
|
22
|
+
"opf": "http://www.idpf.org/2007/opf/",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_SHAPE_TAGS = {
|
|
26
|
+
"line",
|
|
27
|
+
"rect",
|
|
28
|
+
"ellipse",
|
|
29
|
+
"arc",
|
|
30
|
+
"polygon",
|
|
31
|
+
"curve",
|
|
32
|
+
"connectLine",
|
|
33
|
+
"textart",
|
|
34
|
+
"pic",
|
|
35
|
+
"compose",
|
|
36
|
+
"equation",
|
|
37
|
+
"ole",
|
|
38
|
+
"container",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"DocumentMetrics",
|
|
43
|
+
"collect_metrics",
|
|
44
|
+
"compare_metrics",
|
|
45
|
+
"main",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class DocumentMetrics:
|
|
51
|
+
section_count: int
|
|
52
|
+
paragraph_count: int
|
|
53
|
+
page_break_count: int
|
|
54
|
+
column_break_count: int
|
|
55
|
+
table_count: int
|
|
56
|
+
shape_count: int
|
|
57
|
+
control_count: int
|
|
58
|
+
table_shapes: list[tuple[str, str, str, str, str, str]]
|
|
59
|
+
shape_types: list[tuple[str, int]]
|
|
60
|
+
control_types: list[tuple[str, int]]
|
|
61
|
+
text_char_total: int
|
|
62
|
+
text_char_total_nospace: int
|
|
63
|
+
paragraph_text_lengths: list[int]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _text_of_t_node(node: etree._Element) -> str:
|
|
67
|
+
return "".join(node.itertext())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _local_name(tag: str) -> str:
|
|
71
|
+
if "}" in tag:
|
|
72
|
+
return tag.split("}", 1)[1]
|
|
73
|
+
return tag
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _iter_section_roots(source: str | Path | bytes | BinaryIO) -> Iterable[etree._Element]:
|
|
77
|
+
package = HwpxPackage.open(source)
|
|
78
|
+
for name in package.section_paths():
|
|
79
|
+
yield package.get_xml(name)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def collect_metrics(source: str | Path | bytes | BinaryIO) -> DocumentMetrics:
|
|
83
|
+
section_roots = list(_iter_section_roots(source))
|
|
84
|
+
|
|
85
|
+
paragraphs: list[etree._Element] = []
|
|
86
|
+
tables: list[etree._Element] = []
|
|
87
|
+
table_shapes: list[tuple[str, str, str, str, str, str]] = []
|
|
88
|
+
shape_types: dict[str, int] = {}
|
|
89
|
+
control_types: dict[str, int] = {}
|
|
90
|
+
paragraph_text_lengths: list[int] = []
|
|
91
|
+
text_char_total = 0
|
|
92
|
+
text_char_total_nospace = 0
|
|
93
|
+
page_break_count = 0
|
|
94
|
+
column_break_count = 0
|
|
95
|
+
|
|
96
|
+
for root in section_roots:
|
|
97
|
+
section_paragraphs = root.xpath(".//hs:sec/hp:p", namespaces=NS)
|
|
98
|
+
if not section_paragraphs:
|
|
99
|
+
section_paragraphs = root.xpath(".//hp:p", namespaces=NS)
|
|
100
|
+
paragraphs.extend(section_paragraphs)
|
|
101
|
+
|
|
102
|
+
section_tables = root.xpath(".//hp:tbl", namespaces=NS)
|
|
103
|
+
tables.extend(section_tables)
|
|
104
|
+
|
|
105
|
+
for element in root.iter():
|
|
106
|
+
name = _local_name(element.tag)
|
|
107
|
+
if name in _SHAPE_TAGS:
|
|
108
|
+
shape_types[name] = shape_types.get(name, 0) + 1
|
|
109
|
+
if name == "ctrl":
|
|
110
|
+
control_counted = False
|
|
111
|
+
for child in element:
|
|
112
|
+
child_name = _local_name(child.tag)
|
|
113
|
+
control_types[child_name] = control_types.get(child_name, 0) + 1
|
|
114
|
+
control_counted = True
|
|
115
|
+
if not control_counted:
|
|
116
|
+
control_types["ctrl"] = control_types.get("ctrl", 0) + 1
|
|
117
|
+
|
|
118
|
+
for table in section_tables:
|
|
119
|
+
size = table.find("hp:sz", namespaces=NS)
|
|
120
|
+
table_shapes.append(
|
|
121
|
+
(
|
|
122
|
+
table.get("rowCnt", ""),
|
|
123
|
+
table.get("colCnt", ""),
|
|
124
|
+
size.get("width", "") if size is not None else "",
|
|
125
|
+
size.get("height", "") if size is not None else "",
|
|
126
|
+
table.get("repeatHeader", ""),
|
|
127
|
+
table.get("pageBreak", ""),
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
for paragraph in section_paragraphs:
|
|
132
|
+
if paragraph.get("pageBreak") == "1":
|
|
133
|
+
page_break_count += 1
|
|
134
|
+
if paragraph.get("columnBreak") == "1":
|
|
135
|
+
column_break_count += 1
|
|
136
|
+
paragraph_length = 0
|
|
137
|
+
for text_node in paragraph.xpath(".//hp:t", namespaces=NS):
|
|
138
|
+
text = _text_of_t_node(text_node)
|
|
139
|
+
paragraph_length += len(text)
|
|
140
|
+
text_char_total += len(text)
|
|
141
|
+
text_char_total_nospace += len("".join(text.split()))
|
|
142
|
+
paragraph_text_lengths.append(paragraph_length)
|
|
143
|
+
|
|
144
|
+
return DocumentMetrics(
|
|
145
|
+
section_count=len(section_roots),
|
|
146
|
+
paragraph_count=len(paragraphs),
|
|
147
|
+
page_break_count=page_break_count,
|
|
148
|
+
column_break_count=column_break_count,
|
|
149
|
+
table_count=len(tables),
|
|
150
|
+
shape_count=sum(shape_types.values()),
|
|
151
|
+
control_count=sum(control_types.values()),
|
|
152
|
+
table_shapes=table_shapes,
|
|
153
|
+
shape_types=sorted(shape_types.items()),
|
|
154
|
+
control_types=sorted(control_types.items()),
|
|
155
|
+
text_char_total=text_char_total,
|
|
156
|
+
text_char_total_nospace=text_char_total_nospace,
|
|
157
|
+
paragraph_text_lengths=paragraph_text_lengths,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _ratio_delta(reference_value: int, output_value: int) -> float:
|
|
162
|
+
base = max(reference_value, 1)
|
|
163
|
+
return abs(output_value - reference_value) / base
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def compare_metrics(
|
|
167
|
+
reference: DocumentMetrics,
|
|
168
|
+
output: DocumentMetrics,
|
|
169
|
+
*,
|
|
170
|
+
max_text_delta_ratio: float = 0.15,
|
|
171
|
+
max_paragraph_delta_ratio: float = 0.25,
|
|
172
|
+
) -> list[str]:
|
|
173
|
+
errors: list[str] = []
|
|
174
|
+
|
|
175
|
+
if reference.section_count != output.section_count:
|
|
176
|
+
errors.append(
|
|
177
|
+
f"section count mismatch: ref={reference.section_count}, out={output.section_count}"
|
|
178
|
+
)
|
|
179
|
+
if reference.paragraph_count != output.paragraph_count:
|
|
180
|
+
errors.append(
|
|
181
|
+
f"paragraph count mismatch: ref={reference.paragraph_count}, out={output.paragraph_count}"
|
|
182
|
+
)
|
|
183
|
+
if reference.page_break_count != output.page_break_count:
|
|
184
|
+
errors.append(
|
|
185
|
+
"pageBreak count mismatch: "
|
|
186
|
+
f"ref={reference.page_break_count}, out={output.page_break_count}"
|
|
187
|
+
)
|
|
188
|
+
if reference.column_break_count != output.column_break_count:
|
|
189
|
+
errors.append(
|
|
190
|
+
"columnBreak count mismatch: "
|
|
191
|
+
f"ref={reference.column_break_count}, out={output.column_break_count}"
|
|
192
|
+
)
|
|
193
|
+
if reference.table_count != output.table_count:
|
|
194
|
+
errors.append(f"table count mismatch: ref={reference.table_count}, out={output.table_count}")
|
|
195
|
+
if reference.shape_count != output.shape_count:
|
|
196
|
+
errors.append(f"shape count mismatch: ref={reference.shape_count}, out={output.shape_count}")
|
|
197
|
+
if reference.control_count != output.control_count:
|
|
198
|
+
errors.append(
|
|
199
|
+
f"control count mismatch: ref={reference.control_count}, out={output.control_count}"
|
|
200
|
+
)
|
|
201
|
+
if reference.table_shapes != output.table_shapes:
|
|
202
|
+
errors.append("table shape mismatch (rowCnt/colCnt/width/height/repeatHeader/pageBreak)")
|
|
203
|
+
if reference.shape_types != output.shape_types:
|
|
204
|
+
errors.append("shape type histogram mismatch")
|
|
205
|
+
if reference.control_types != output.control_types:
|
|
206
|
+
errors.append("control type histogram mismatch")
|
|
207
|
+
|
|
208
|
+
text_delta = _ratio_delta(reference.text_char_total_nospace, output.text_char_total_nospace)
|
|
209
|
+
if text_delta > max_text_delta_ratio:
|
|
210
|
+
errors.append(
|
|
211
|
+
"total text length drift exceeded: "
|
|
212
|
+
f"ref={reference.text_char_total_nospace}, out={output.text_char_total_nospace}, "
|
|
213
|
+
f"delta={text_delta:.2%}, limit={max_text_delta_ratio:.2%}"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if len(reference.paragraph_text_lengths) == len(output.paragraph_text_lengths):
|
|
217
|
+
for index, (ref_len, out_len) in enumerate(
|
|
218
|
+
zip(reference.paragraph_text_lengths, output.paragraph_text_lengths),
|
|
219
|
+
start=1,
|
|
220
|
+
):
|
|
221
|
+
if ref_len == 0 and out_len == 0:
|
|
222
|
+
continue
|
|
223
|
+
delta = _ratio_delta(ref_len, out_len)
|
|
224
|
+
if delta > max_paragraph_delta_ratio:
|
|
225
|
+
errors.append(
|
|
226
|
+
f"paragraph {index} text drift exceeded: "
|
|
227
|
+
f"ref={ref_len}, out={out_len}, delta={delta:.2%}, "
|
|
228
|
+
f"limit={max_paragraph_delta_ratio:.2%}"
|
|
229
|
+
)
|
|
230
|
+
return errors
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
234
|
+
parser = argparse.ArgumentParser(
|
|
235
|
+
description="Reference-vs-output HWPX layout-drift proxy checker"
|
|
236
|
+
)
|
|
237
|
+
parser.add_argument("--reference", "-r", required=True, help="Reference HWPX path")
|
|
238
|
+
parser.add_argument("--output", "-o", required=True, help="Output HWPX path")
|
|
239
|
+
parser.add_argument("--max-text-delta-ratio", type=float, default=0.15)
|
|
240
|
+
parser.add_argument("--max-paragraph-delta-ratio", type=float, default=0.25)
|
|
241
|
+
parser.add_argument("--json", action="store_true", help="Print collected metrics as JSON")
|
|
242
|
+
args = parser.parse_args(argv)
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
reference = collect_metrics(args.reference)
|
|
246
|
+
output = collect_metrics(args.output)
|
|
247
|
+
except Exception as exc:
|
|
248
|
+
print(f"ERROR: {exc}")
|
|
249
|
+
return 1
|
|
250
|
+
|
|
251
|
+
if args.json:
|
|
252
|
+
print(
|
|
253
|
+
json.dumps(
|
|
254
|
+
{"reference": asdict(reference), "output": asdict(output)},
|
|
255
|
+
ensure_ascii=False,
|
|
256
|
+
indent=2,
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
errors = compare_metrics(
|
|
261
|
+
reference,
|
|
262
|
+
output,
|
|
263
|
+
max_text_delta_ratio=args.max_text_delta_ratio,
|
|
264
|
+
max_paragraph_delta_ratio=args.max_paragraph_delta_ratio,
|
|
265
|
+
)
|
|
266
|
+
if errors:
|
|
267
|
+
print("FAIL: page guard")
|
|
268
|
+
for error in errors:
|
|
269
|
+
print(f" - {error}")
|
|
270
|
+
return 1
|
|
271
|
+
|
|
272
|
+
print("PASS: page guard")
|
|
273
|
+
return 0
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
if __name__ == "__main__": # pragma: no cover - CLI convenience
|
|
277
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Canonical Hancom rendering owner for the automation application layer.
|
|
3
|
+
|
|
4
|
+
The package owns runtime discovery, renderer execution, serialized worker
|
|
5
|
+
policy, fixture orchestration, and visual-QA measurement. ``python-hwpx``
|
|
6
|
+
supplies only renderer-neutral contracts and deterministic geometry helpers.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Iterator
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from hwpx.quality import SavePipeline
|
|
16
|
+
from hwpx.quality.rendering import EditMask, VisualReport
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .oracle import (
|
|
20
|
+
MacHancomOracle,
|
|
21
|
+
NullOracle,
|
|
22
|
+
RenderBackend,
|
|
23
|
+
RenderOracle,
|
|
24
|
+
WindowsComOracle,
|
|
25
|
+
resolve_oracle,
|
|
26
|
+
visual_check,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_ORACLE_EXPORTS = (
|
|
30
|
+
"MacHancomOracle",
|
|
31
|
+
"NullOracle",
|
|
32
|
+
"RenderBackend",
|
|
33
|
+
"RenderOracle",
|
|
34
|
+
"WindowsComOracle",
|
|
35
|
+
"resolve_oracle",
|
|
36
|
+
"visual_check",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def __getattr__(name: str) -> Any:
|
|
41
|
+
"""Load the Hancom side only when someone asks for it.
|
|
42
|
+
|
|
43
|
+
Importing this package eagerly would drag the oracle in behind any import of
|
|
44
|
+
a neutral contract — ``block_splits``, ``detectors``, ``diff`` — because a
|
|
45
|
+
submodule import initialises its package first. Those contracts exist so
|
|
46
|
+
exam typesetting can compute question splits with no renderer present, and
|
|
47
|
+
that promise is worth nothing if reaching them starts one.
|
|
48
|
+
|
|
49
|
+
Core's ``hwpx.visual`` used the same lazy hook for the same reason. The move
|
|
50
|
+
to this owner dropped it, and the boundary test caught it.
|
|
51
|
+
"""
|
|
52
|
+
if name in _ORACLE_EXPORTS:
|
|
53
|
+
import importlib
|
|
54
|
+
|
|
55
|
+
return getattr(importlib.import_module(f"{__name__}.oracle"), name)
|
|
56
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def __dir__() -> list[str]:
|
|
60
|
+
return sorted({*globals(), *_ORACLE_EXPORTS})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _runtime_export(name: str) -> Any:
|
|
64
|
+
"""Resolve a lazy export while preserving an explicit runtime patch."""
|
|
65
|
+
|
|
66
|
+
value = globals().get(name)
|
|
67
|
+
return value if value is not None else __getattr__(name)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class HancomRenderBackend:
|
|
71
|
+
"""Adapt a discovered Hancom render transport to the core check protocol."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, oracle: Any) -> None:
|
|
74
|
+
self._oracle = oracle
|
|
75
|
+
|
|
76
|
+
def available(self) -> bool:
|
|
77
|
+
return bool(self._oracle is not None and self._oracle.available())
|
|
78
|
+
|
|
79
|
+
def check(
|
|
80
|
+
self,
|
|
81
|
+
before_hwpx: str | None,
|
|
82
|
+
after_hwpx: str,
|
|
83
|
+
*,
|
|
84
|
+
edit_mask: EditMask | None = None,
|
|
85
|
+
diff_eps: float = 0.005,
|
|
86
|
+
dpi: int = 150,
|
|
87
|
+
work_dir: str | None = None,
|
|
88
|
+
keep_artifacts: bool = False,
|
|
89
|
+
) -> VisualReport:
|
|
90
|
+
# Module-dict lookup, not a direct submodule import: a test that
|
|
91
|
+
# patches ``rendering.visual_check`` must still win, and __getattr__
|
|
92
|
+
# only runs when the name is absent from the module dict.
|
|
93
|
+
return _runtime_export("visual_check")(
|
|
94
|
+
before_hwpx,
|
|
95
|
+
after_hwpx,
|
|
96
|
+
oracle=self._oracle,
|
|
97
|
+
edit_mask=edit_mask,
|
|
98
|
+
diff_eps=diff_eps,
|
|
99
|
+
dpi=dpi,
|
|
100
|
+
work_dir=work_dir,
|
|
101
|
+
keep_artifacts=keep_artifacts,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def resolve_hancom_backend(*, dpi: int = 150, **_options: Any) -> HancomRenderBackend:
|
|
106
|
+
"""Discover Hancom at the application boundary and return a core backend."""
|
|
107
|
+
|
|
108
|
+
return HancomRenderBackend(resolve_hancom_oracle(dpi=dpi))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def resolve_hancom_oracle(*, dpi: int = 150, **_options: Any) -> Any:
|
|
112
|
+
"""Return the canonical Hancom transport at the automation boundary."""
|
|
113
|
+
|
|
114
|
+
return _runtime_export("resolve_oracle")(dpi=dpi)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@contextmanager
|
|
118
|
+
def bind_document_rendering(document: Any) -> Iterator[None]:
|
|
119
|
+
"""Temporarily bind automation-owned rendering to one core document save."""
|
|
120
|
+
|
|
121
|
+
sentinel = object()
|
|
122
|
+
previous = getattr(document, "_save_pipeline", sentinel)
|
|
123
|
+
document._save_pipeline = SavePipeline(oracle_factory=resolve_hancom_backend)
|
|
124
|
+
try:
|
|
125
|
+
yield
|
|
126
|
+
finally:
|
|
127
|
+
if previous is sentinel:
|
|
128
|
+
delattr(document, "_save_pipeline")
|
|
129
|
+
else:
|
|
130
|
+
document._save_pipeline = previous
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
__all__ = [
|
|
134
|
+
"HancomRenderBackend",
|
|
135
|
+
"MacHancomOracle",
|
|
136
|
+
"NullOracle",
|
|
137
|
+
"RenderBackend",
|
|
138
|
+
"RenderOracle",
|
|
139
|
+
"WindowsComOracle",
|
|
140
|
+
"bind_document_rendering",
|
|
141
|
+
"resolve_hancom_backend",
|
|
142
|
+
"resolve_hancom_oracle",
|
|
143
|
+
"resolve_oracle",
|
|
144
|
+
"visual_check",
|
|
145
|
+
]
|