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,123 @@
|
|
|
1
|
+
"""Profile a school form: resolve role->style and classify the body region.
|
|
2
|
+
|
|
3
|
+
Grounded by the measure-first A_form probe (scripts/exam_profile_a_form.py).
|
|
4
|
+
No style-by-name API exists, so we build a name->Style index ourselves."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
from hwpx.document import HwpxDocument
|
|
12
|
+
from hwpx.oxml import Style
|
|
13
|
+
|
|
14
|
+
DEFAULT_ROLE_STYLE_NAMES: dict[str, str] = {
|
|
15
|
+
"normal": "바탕글",
|
|
16
|
+
"number": "문항자동번호넣기",
|
|
17
|
+
"choice1": "1행답항",
|
|
18
|
+
"choice2": "2행답항",
|
|
19
|
+
"choice3": "3행답항",
|
|
20
|
+
"choice5": "5행답항",
|
|
21
|
+
"box_guide": "(보기)박스안내용",
|
|
22
|
+
"box": "박스안내용",
|
|
23
|
+
}
|
|
24
|
+
# Roles that MUST exist for composition (others are optional conveniences).
|
|
25
|
+
REQUIRED_ROLES = ("normal", "number", "choice1", "choice5")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FormProfileError(ValueError):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class ResolvedStyle:
|
|
34
|
+
name: str
|
|
35
|
+
style_id: str
|
|
36
|
+
para_pr_id: str | None
|
|
37
|
+
char_pr_id: str | None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class FormProfile:
|
|
42
|
+
role_styles: dict[str, ResolvedStyle]
|
|
43
|
+
admin_box_index: int
|
|
44
|
+
body_start: int
|
|
45
|
+
body_end: int
|
|
46
|
+
replaceable_indices: tuple[int, ...]
|
|
47
|
+
structural_indices: tuple[int, ...]
|
|
48
|
+
ambiguous_indices: tuple[int, ...]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _name_index(doc: HwpxDocument) -> dict[str, Style]:
|
|
52
|
+
index: dict[str, Style] = {}
|
|
53
|
+
for style in doc.styles.values():
|
|
54
|
+
if style.name:
|
|
55
|
+
index.setdefault(style.name, style)
|
|
56
|
+
return index
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def profile_form(
|
|
60
|
+
doc: HwpxDocument,
|
|
61
|
+
*,
|
|
62
|
+
role_style_names: Mapping[str, str] | None = None,
|
|
63
|
+
) -> FormProfile:
|
|
64
|
+
names = dict(DEFAULT_ROLE_STYLE_NAMES)
|
|
65
|
+
if role_style_names:
|
|
66
|
+
names.update(role_style_names)
|
|
67
|
+
|
|
68
|
+
by_name = _name_index(doc)
|
|
69
|
+
role_styles: dict[str, ResolvedStyle] = {}
|
|
70
|
+
for role, style_name in names.items():
|
|
71
|
+
style = by_name.get(style_name)
|
|
72
|
+
if style is None:
|
|
73
|
+
if role in REQUIRED_ROLES:
|
|
74
|
+
raise FormProfileError(
|
|
75
|
+
f"required role '{role}' style {style_name!r} not found in form"
|
|
76
|
+
)
|
|
77
|
+
continue
|
|
78
|
+
role_styles[role] = ResolvedStyle(
|
|
79
|
+
name=style_name,
|
|
80
|
+
style_id=str(style.id if style.id is not None else style.raw_id),
|
|
81
|
+
para_pr_id=None
|
|
82
|
+
if style.para_pr_id_ref is None
|
|
83
|
+
else str(style.para_pr_id_ref),
|
|
84
|
+
char_pr_id=None
|
|
85
|
+
if style.char_pr_id_ref is None
|
|
86
|
+
else str(style.char_pr_id_ref),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Anchor the body region on the QUESTION/ANSWER styles ONLY. 바탕글(normal)
|
|
90
|
+
# also clothes the 관리박스 [0] and the trailing footer/essay zone, so it
|
|
91
|
+
# cannot delimit the body (measured: A_form [0] and [71..100] are all 바탕글;
|
|
92
|
+
# the question/answer slots span [1..70], footer marker sits at [99]).
|
|
93
|
+
anchor_names = {rs.name for role, rs in role_styles.items() if role != "normal"}
|
|
94
|
+
section = doc.sections[0]
|
|
95
|
+
paragraphs = section.paragraphs
|
|
96
|
+
|
|
97
|
+
anchors: list[int] = []
|
|
98
|
+
for idx, para in enumerate(paragraphs):
|
|
99
|
+
if idx == 0: # 관리박스 (never replaceable)
|
|
100
|
+
continue
|
|
101
|
+
sid = para.style_id_ref
|
|
102
|
+
style = doc.style(sid) if sid is not None else None
|
|
103
|
+
if style is not None and style.name in anchor_names:
|
|
104
|
+
anchors.append(idx)
|
|
105
|
+
|
|
106
|
+
if not anchors:
|
|
107
|
+
raise FormProfileError(
|
|
108
|
+
"no question/answer-style body paragraphs found (form profile failed)"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
body_start, body_end = anchors[0], anchors[-1]
|
|
112
|
+
# The whole [body_start..body_end] span is recomposed from the exam IR
|
|
113
|
+
# (inline 바탕글 lines + 논술형 scaffolding within it included); 관리박스 [0]
|
|
114
|
+
# and the tail [body_end+1..] (footer / essay answer space) are preserved.
|
|
115
|
+
return FormProfile(
|
|
116
|
+
role_styles=role_styles,
|
|
117
|
+
admin_box_index=0,
|
|
118
|
+
body_start=body_start,
|
|
119
|
+
body_end=body_end,
|
|
120
|
+
replaceable_indices=tuple(range(body_start, body_end + 1)),
|
|
121
|
+
structural_indices=(),
|
|
122
|
+
ambiguous_indices=(), # v1: the body span is recomposed wholesale from the IR
|
|
123
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Canonical automation owner for application-level form-fill behavior."""
|
|
3
|
+
|
|
4
|
+
import hwpx_automation.office.form_fill.fit as fit
|
|
5
|
+
from .fill_residue import (
|
|
6
|
+
ResidueFinding,
|
|
7
|
+
ResidueReport,
|
|
8
|
+
inspect_fill_residue,
|
|
9
|
+
)
|
|
10
|
+
from .guidance import (
|
|
11
|
+
Candidate,
|
|
12
|
+
CellLoc,
|
|
13
|
+
GuidanceReport,
|
|
14
|
+
LegendBinding,
|
|
15
|
+
ScannedParagraph,
|
|
16
|
+
is_form_instruction,
|
|
17
|
+
scan_form_guidance,
|
|
18
|
+
)
|
|
19
|
+
from .quality import (
|
|
20
|
+
NEEDS_REVIEW,
|
|
21
|
+
UNVERIFIED,
|
|
22
|
+
AxisScore,
|
|
23
|
+
ScoreCard,
|
|
24
|
+
score_form_fill,
|
|
25
|
+
)
|
|
26
|
+
from .split_run import (
|
|
27
|
+
FillReport,
|
|
28
|
+
HeterogeneousWarning,
|
|
29
|
+
Placeholder,
|
|
30
|
+
TextFragment,
|
|
31
|
+
fill_section_bytes,
|
|
32
|
+
find_split_placeholders,
|
|
33
|
+
heterogeneous_warnings,
|
|
34
|
+
)
|
|
35
|
+
from .template_formfit import (
|
|
36
|
+
analyze_template_formfit,
|
|
37
|
+
apply_template_formfit,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"NEEDS_REVIEW",
|
|
42
|
+
"UNVERIFIED",
|
|
43
|
+
"AxisScore",
|
|
44
|
+
"Candidate",
|
|
45
|
+
"CellLoc",
|
|
46
|
+
"FillReport",
|
|
47
|
+
"GuidanceReport",
|
|
48
|
+
"HeterogeneousWarning",
|
|
49
|
+
"LegendBinding",
|
|
50
|
+
"Placeholder",
|
|
51
|
+
"ResidueFinding",
|
|
52
|
+
"ResidueReport",
|
|
53
|
+
"ScannedParagraph",
|
|
54
|
+
"ScoreCard",
|
|
55
|
+
"TextFragment",
|
|
56
|
+
"analyze_template_formfit",
|
|
57
|
+
"apply_template_formfit",
|
|
58
|
+
"fill_section_bytes",
|
|
59
|
+
"find_split_placeholders",
|
|
60
|
+
"fit",
|
|
61
|
+
"heterogeneous_warnings",
|
|
62
|
+
"inspect_fill_residue",
|
|
63
|
+
"is_form_instruction",
|
|
64
|
+
"scan_form_guidance",
|
|
65
|
+
"score_form_fill",
|
|
66
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Leaf table extraction and classification policy shared by form workflows."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from hwpx.table_patch import (
|
|
10
|
+
build_grid,
|
|
11
|
+
iter_table_spans,
|
|
12
|
+
read_source_bytes,
|
|
13
|
+
section_parts,
|
|
14
|
+
table_text,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class _Table:
|
|
20
|
+
section: str
|
|
21
|
+
index: int
|
|
22
|
+
bytes: bytes
|
|
23
|
+
rows: int
|
|
24
|
+
cols: int
|
|
25
|
+
first_row: list[str]
|
|
26
|
+
heading: str
|
|
27
|
+
text: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _tables(source: str | Path | bytes) -> list[_Table]:
|
|
31
|
+
data = read_source_bytes(source)
|
|
32
|
+
out: list[_Table] = []
|
|
33
|
+
for section_path, section in sorted(section_parts(data).items()):
|
|
34
|
+
spans = iter_table_spans(section)
|
|
35
|
+
for table_index, (start, end) in enumerate(spans):
|
|
36
|
+
table = section[start:end]
|
|
37
|
+
grid, report = build_grid(table)
|
|
38
|
+
first: list[str] = []
|
|
39
|
+
for column in range(report.col_count):
|
|
40
|
+
cell = grid.get((0, column))
|
|
41
|
+
first.append(
|
|
42
|
+
" ".join(
|
|
43
|
+
table_text(table[cell.start : cell.end]).split()
|
|
44
|
+
)
|
|
45
|
+
if cell
|
|
46
|
+
else ""
|
|
47
|
+
)
|
|
48
|
+
deduplicated: list[str] = []
|
|
49
|
+
for cell_text in first:
|
|
50
|
+
if not deduplicated or deduplicated[-1] != cell_text:
|
|
51
|
+
deduplicated.append(cell_text)
|
|
52
|
+
window_start = max(0, start - 8000)
|
|
53
|
+
heading = " ".join(
|
|
54
|
+
table_text(section[window_start:start]).split()
|
|
55
|
+
)[-80:]
|
|
56
|
+
out.append(
|
|
57
|
+
_Table(
|
|
58
|
+
section=section_path,
|
|
59
|
+
index=table_index,
|
|
60
|
+
bytes=table,
|
|
61
|
+
rows=report.row_count,
|
|
62
|
+
cols=report.col_count,
|
|
63
|
+
first_row=deduplicated,
|
|
64
|
+
heading=heading,
|
|
65
|
+
text=" ".join(table_text(table).split()),
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
return out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _classify(table: _Table) -> str:
|
|
72
|
+
"""Coarse table type from its first-row and heading signature."""
|
|
73
|
+
|
|
74
|
+
first_row = " | ".join(table.first_row)
|
|
75
|
+
text = table.text
|
|
76
|
+
if (
|
|
77
|
+
table.rows == 1
|
|
78
|
+
and table.first_row
|
|
79
|
+
and table.first_row[0].strip().isdigit()
|
|
80
|
+
):
|
|
81
|
+
return "other"
|
|
82
|
+
if "석차등급" in first_row and "원점수" in first_row:
|
|
83
|
+
return "seokcha"
|
|
84
|
+
if "제출" in first_row and ("모아찍기" in text or "인쇄물" in text):
|
|
85
|
+
return "submit"
|
|
86
|
+
if table.rows == 1 and table.cols == 1 and table.text.startswith("★유의"):
|
|
87
|
+
return "notice_star"
|
|
88
|
+
if first_row.startswith("교육과정 성취기준") and "평가기준" in first_row:
|
|
89
|
+
return "achievement"
|
|
90
|
+
if first_row.startswith("성취수준") and "일반적 특성" in first_row:
|
|
91
|
+
return "level"
|
|
92
|
+
if first_row.startswith("교육과정성취기준"):
|
|
93
|
+
return "rubric"
|
|
94
|
+
if "최소 능력" in first_row or "최소 성취수준" in first_row:
|
|
95
|
+
return "minlevel"
|
|
96
|
+
if "성취기준별 성취수준" in first_row and "최소" not in first_row:
|
|
97
|
+
return "achievement"
|
|
98
|
+
if "학기 단위 성취수준" in first_row:
|
|
99
|
+
return "level"
|
|
100
|
+
if first_row.startswith("평가 영역명") and "영역 만점" in first_row:
|
|
101
|
+
return "rubric"
|
|
102
|
+
if "평가 종류" in first_row or (
|
|
103
|
+
"수행평가" in first_row and "합계" in first_row
|
|
104
|
+
):
|
|
105
|
+
return "ratio"
|
|
106
|
+
if "성취율" in first_row and "성취도" in first_row:
|
|
107
|
+
return "achieve_rate"
|
|
108
|
+
return "other"
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""채움본 잔존물 zero-체크 — 범용 form-fill goal Stage 3의 기계 게이트.
|
|
2
|
+
|
|
3
|
+
2026-07-06 평가계획 거짓 수렴(빨간 지시문 ~15곳·타과목 샘플·placeholder 잔존이
|
|
4
|
+
스코어 94.9를 통과)의 결함 유형을 **양식 불문 신호**로 검사한다. 도메인 어휘 없음:
|
|
5
|
+
|
|
6
|
+
- **삭제색 잔존**(ERROR): blank의 범례가 "삭제"로 선언한 색의 런 텍스트가
|
|
7
|
+
produced에 남아 있음(예: 빨간 지시문).
|
|
8
|
+
- **미수정 샘플**(ERROR): 범례가 "수정"으로 선언한 색의 blank 런 텍스트가
|
|
9
|
+
produced에 **그대로** 남아 있음 — 코드 없는 prose 샘플(음악 '연주', 한국사
|
|
10
|
+
'전근대사' 등)을 잡는 일반 신호.
|
|
11
|
+
- **placeholder 잔존**: ◯◯◯·□□□(2연속 이상)=ERROR. 리터럴 ``**``는 각주
|
|
12
|
+
표식("경비** 집행 불가")과 중의적이라 NEEDS_REVIEW(2026-07-07 신청서 실측).
|
|
13
|
+
- **고아 마커**(NEEDS_REVIEW): 텍스트가 목록 마커('-', '•', '가.' 류)뿐인 문단 —
|
|
14
|
+
의도적으로 남긴 하위 항목 자리일 수 있어 사람 판단으로 넘긴다.
|
|
15
|
+
|
|
16
|
+
이 게이트는 필요조건이다 — 통과해도 "제출 가능" 확언은 렌더 PDF를 사람이 전
|
|
17
|
+
페이지 본 뒤에만 한다(철칙).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from hwpx.document import HwpxDocument
|
|
27
|
+
|
|
28
|
+
from .guidance import _Walker, _color_family, scan_form_guidance
|
|
29
|
+
|
|
30
|
+
__all__ = ["ResidueFinding", "ResidueReport", "inspect_fill_residue"]
|
|
31
|
+
|
|
32
|
+
_PLACEHOLDER_RES = {
|
|
33
|
+
"circle_blank": re.compile(r"[◯○〇]{2,}"),
|
|
34
|
+
"square_blank": re.compile(r"□{2,}"),
|
|
35
|
+
}
|
|
36
|
+
# ``**``는 placeholder(**과목)일 수도, 각주 표식(경비**)일 수도 있어 사람 판단으로.
|
|
37
|
+
_AMBIGUOUS_RES = {
|
|
38
|
+
"double_star": re.compile(r"\*\*"),
|
|
39
|
+
}
|
|
40
|
+
_MARKER_ONLY = re.compile(
|
|
41
|
+
r"^\s*(?:[-•‧∙·]|[가나다라마바사아자차카타파하]\.|\(\d+\)|\d+\))\s*$"
|
|
42
|
+
)
|
|
43
|
+
_MIN_SAMPLE_LEN = 4 # 이보다 짧은 수정색 텍스트(숫자·기호 등)는 미수정 판정에서 제외
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(slots=True)
|
|
47
|
+
class ResidueFinding:
|
|
48
|
+
kind: str # delete_color_residue / unmodified_sample / placeholder / marker_only
|
|
49
|
+
severity: str # error / needs_review
|
|
50
|
+
location: str
|
|
51
|
+
text_preview: str
|
|
52
|
+
signal: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(slots=True)
|
|
56
|
+
class ResidueReport:
|
|
57
|
+
produced: str
|
|
58
|
+
errors: list[ResidueFinding] = field(default_factory=list)
|
|
59
|
+
needs_review: list[ResidueFinding] = field(default_factory=list)
|
|
60
|
+
stats: dict = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def ok(self) -> bool:
|
|
64
|
+
return not self.errors
|
|
65
|
+
|
|
66
|
+
def to_dict(self) -> dict:
|
|
67
|
+
def d(f: ResidueFinding) -> dict:
|
|
68
|
+
return {
|
|
69
|
+
"kind": f.kind,
|
|
70
|
+
"severity": f.severity,
|
|
71
|
+
"location": f.location,
|
|
72
|
+
"textPreview": f.text_preview,
|
|
73
|
+
"signal": f.signal,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
"ok": self.ok,
|
|
78
|
+
"errors": [d(f) for f in self.errors],
|
|
79
|
+
"needsReview": [d(f) for f in self.needs_review],
|
|
80
|
+
"stats": self.stats,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _scan_paragraphs(source: str | Path | HwpxDocument):
|
|
85
|
+
doc = source if isinstance(source, HwpxDocument) else HwpxDocument.open(str(source))
|
|
86
|
+
chars = doc._root.char_properties
|
|
87
|
+
out = []
|
|
88
|
+
for s_idx, section in enumerate(doc.sections):
|
|
89
|
+
walker = _Walker(chars)
|
|
90
|
+
for b_idx, p in enumerate(section.paragraphs):
|
|
91
|
+
walker.walk_paragraph(p.element, s_idx, b_idx, None)
|
|
92
|
+
out.extend(walker.out)
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _collect_blank_residue_texts(
|
|
97
|
+
blank: str | Path,
|
|
98
|
+
) -> tuple[list[tuple[str, str]], list[tuple[str, str]]]:
|
|
99
|
+
"""(blank_delete_texts, blank_modify_texts) -- (text, 위치 힌트) pairs from the
|
|
100
|
+
blank's legend-declared delete/modify color families."""
|
|
101
|
+
|
|
102
|
+
blank_report = scan_form_guidance(blank)
|
|
103
|
+
delete_families = {b.family for b in blank_report.legend if b.action == "delete"}
|
|
104
|
+
modify_families = {b.family for b in blank_report.legend if b.action == "modify"}
|
|
105
|
+
blank_delete_texts: list[tuple[str, str]] = []
|
|
106
|
+
blank_modify_texts: list[tuple[str, str]] = []
|
|
107
|
+
for bp in _scan_paragraphs(blank):
|
|
108
|
+
for _, hex_color, text in bp.spans:
|
|
109
|
+
t = text.strip()
|
|
110
|
+
if not t:
|
|
111
|
+
continue
|
|
112
|
+
fam = _color_family(hex_color)
|
|
113
|
+
if fam in delete_families:
|
|
114
|
+
blank_delete_texts.append((t, bp.location()))
|
|
115
|
+
elif fam in modify_families and len(t) >= _MIN_SAMPLE_LEN:
|
|
116
|
+
blank_modify_texts.append((t, bp.location()))
|
|
117
|
+
return blank_delete_texts, blank_modify_texts
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _check_delete_color_residue(
|
|
121
|
+
blank_delete_texts: list[tuple[str, str]], produced_text: str
|
|
122
|
+
) -> list[ResidueFinding]:
|
|
123
|
+
"""삭제색 잔존: blank의 삭제색 텍스트가 produced에 남음."""
|
|
124
|
+
|
|
125
|
+
findings: list[ResidueFinding] = []
|
|
126
|
+
seen: set[str] = set()
|
|
127
|
+
for t, loc in blank_delete_texts:
|
|
128
|
+
if t in seen:
|
|
129
|
+
continue
|
|
130
|
+
seen.add(t)
|
|
131
|
+
if t in produced_text:
|
|
132
|
+
findings.append(
|
|
133
|
+
ResidueFinding(
|
|
134
|
+
"delete_color_residue",
|
|
135
|
+
"error",
|
|
136
|
+
loc,
|
|
137
|
+
t[:80],
|
|
138
|
+
"blank 범례가 '삭제'로 선언한 색의 텍스트가 잔존",
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
return findings
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _check_unmodified_sample(
|
|
145
|
+
blank_modify_texts: list[tuple[str, str]], produced_text: str
|
|
146
|
+
) -> list[ResidueFinding]:
|
|
147
|
+
"""미수정 샘플: 수정색 텍스트가 blank와 동일하게 잔존."""
|
|
148
|
+
|
|
149
|
+
findings: list[ResidueFinding] = []
|
|
150
|
+
seen: set[str] = set()
|
|
151
|
+
for t, loc in blank_modify_texts:
|
|
152
|
+
if t in seen:
|
|
153
|
+
continue
|
|
154
|
+
seen.add(t)
|
|
155
|
+
if t in produced_text:
|
|
156
|
+
findings.append(
|
|
157
|
+
ResidueFinding(
|
|
158
|
+
"unmodified_sample",
|
|
159
|
+
"error",
|
|
160
|
+
loc,
|
|
161
|
+
t[:80],
|
|
162
|
+
"blank 범례가 '수정'으로 선언한 텍스트가 그대로 잔존(샘플 미교체)",
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
return findings
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _check_placeholder_residue(
|
|
169
|
+
paras: list,
|
|
170
|
+
) -> tuple[list[ResidueFinding], list[ResidueFinding]]:
|
|
171
|
+
"""placeholder 잔존 (errors, needs_review)."""
|
|
172
|
+
|
|
173
|
+
errors: list[ResidueFinding] = []
|
|
174
|
+
needs_review: list[ResidueFinding] = []
|
|
175
|
+
for p in paras:
|
|
176
|
+
text = p.text
|
|
177
|
+
if not text.strip():
|
|
178
|
+
continue
|
|
179
|
+
for name, pat in _PLACEHOLDER_RES.items():
|
|
180
|
+
if pat.search(text):
|
|
181
|
+
errors.append(
|
|
182
|
+
ResidueFinding(
|
|
183
|
+
"placeholder",
|
|
184
|
+
"error",
|
|
185
|
+
p.location(),
|
|
186
|
+
text.strip()[:80],
|
|
187
|
+
f"placeholder:{name}",
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
for name, pat in _AMBIGUOUS_RES.items():
|
|
191
|
+
if pat.search(text):
|
|
192
|
+
needs_review.append(
|
|
193
|
+
ResidueFinding(
|
|
194
|
+
"placeholder_ambiguous",
|
|
195
|
+
"needs_review",
|
|
196
|
+
p.location(),
|
|
197
|
+
text.strip()[:80],
|
|
198
|
+
f"placeholder?:{name} — 각주 표식일 수 있음",
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
if _MARKER_ONLY.match(text):
|
|
202
|
+
needs_review.append(
|
|
203
|
+
ResidueFinding(
|
|
204
|
+
"marker_only",
|
|
205
|
+
"needs_review",
|
|
206
|
+
p.location(),
|
|
207
|
+
text.strip()[:20],
|
|
208
|
+
"목록 마커만 있는 문단 — 의도된 빈 자리인지 사람 판단",
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
return errors, needs_review
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def inspect_fill_residue(
|
|
215
|
+
produced: str | Path,
|
|
216
|
+
blank: str | Path | None = None,
|
|
217
|
+
) -> ResidueReport:
|
|
218
|
+
"""채움본(produced)의 잔존물을 검사한다. blank가 있으면 범례 기반 신호까지."""
|
|
219
|
+
report = ResidueReport(produced=str(produced))
|
|
220
|
+
paras = _scan_paragraphs(produced)
|
|
221
|
+
produced_text = "\n".join(p.text for p in paras)
|
|
222
|
+
|
|
223
|
+
blank_delete_texts: list[tuple[str, str]] = []
|
|
224
|
+
blank_modify_texts: list[tuple[str, str]] = []
|
|
225
|
+
if blank is not None:
|
|
226
|
+
blank_delete_texts, blank_modify_texts = _collect_blank_residue_texts(blank)
|
|
227
|
+
|
|
228
|
+
report.errors.extend(_check_delete_color_residue(blank_delete_texts, produced_text))
|
|
229
|
+
report.errors.extend(_check_unmodified_sample(blank_modify_texts, produced_text))
|
|
230
|
+
|
|
231
|
+
placeholder_errors, placeholder_needs_review = _check_placeholder_residue(paras)
|
|
232
|
+
report.errors.extend(placeholder_errors)
|
|
233
|
+
report.needs_review.extend(placeholder_needs_review)
|
|
234
|
+
|
|
235
|
+
report.stats = {
|
|
236
|
+
"paragraphs": len(paras),
|
|
237
|
+
"blankDeleteTexts": len({t for t, _ in blank_delete_texts}),
|
|
238
|
+
"blankModifyTexts": len({t for t, _ in blank_modify_texts}),
|
|
239
|
+
"errors": len(report.errors),
|
|
240
|
+
"needsReview": len(report.needs_review),
|
|
241
|
+
}
|
|
242
|
+
return report
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Form-fit decision, reporting, and geometry owned by the automation layer."""
|
|
3
|
+
|
|
4
|
+
import hwpx_automation.office.form_fill.fit.seal as seal
|
|
5
|
+
import hwpx_automation.office.form_fill.fit.wordbox as wordbox
|
|
6
|
+
from .engine import FitEngine
|
|
7
|
+
from .measure import (
|
|
8
|
+
DEFAULT_SAFETY,
|
|
9
|
+
Measurement,
|
|
10
|
+
SlotMetrics,
|
|
11
|
+
estimate_lines,
|
|
12
|
+
estimate_text_width,
|
|
13
|
+
measure,
|
|
14
|
+
resolve_slot_metrics,
|
|
15
|
+
)
|
|
16
|
+
from .policy import FitMode, FitPolicy, OverflowAction
|
|
17
|
+
from .report import FIELD_OVERFLOW, FitResult, to_form_report
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"DEFAULT_SAFETY",
|
|
21
|
+
"FIELD_OVERFLOW",
|
|
22
|
+
"FitEngine",
|
|
23
|
+
"FitMode",
|
|
24
|
+
"FitPolicy",
|
|
25
|
+
"FitResult",
|
|
26
|
+
"Measurement",
|
|
27
|
+
"OverflowAction",
|
|
28
|
+
"SlotMetrics",
|
|
29
|
+
"estimate_lines",
|
|
30
|
+
"estimate_text_width",
|
|
31
|
+
"measure",
|
|
32
|
+
"resolve_slot_metrics",
|
|
33
|
+
"seal",
|
|
34
|
+
"to_form_report",
|
|
35
|
+
"wordbox",
|
|
36
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Re-export of the neutral fit cell application helper that python-hwpx owns.
|
|
3
|
+
|
|
4
|
+
This module used to be a copy. The two versions were behaviourally identical —
|
|
5
|
+
comparing normalised syntax trees showed no semantic difference across the five
|
|
6
|
+
fit-contract modules, only comment alignment and whether a forward reference was
|
|
7
|
+
quoted — so maintaining both bought nothing and cost a drift risk that had
|
|
8
|
+
already come due elsewhere in this stack.
|
|
9
|
+
|
|
10
|
+
Slot measurement and the keep/wrap/shrink ladder are format reasoning, not
|
|
11
|
+
application policy: core's own table and field APIs call them. What this layer
|
|
12
|
+
owns is the application half — seal placement, PDF extraction, institutional
|
|
13
|
+
rules — which stays here and is not re-exported from core.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from hwpx.form_fit.apply import ( # noqa: F401 - re-exported public surface
|
|
19
|
+
fit_cell_text,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"fit_cell_text",
|
|
24
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Re-export of the neutral fit keep/wrap/shrink ladder that python-hwpx owns.
|
|
3
|
+
|
|
4
|
+
This module used to be a copy. The two versions were behaviourally identical —
|
|
5
|
+
comparing normalised syntax trees showed no semantic difference across the five
|
|
6
|
+
fit-contract modules, only comment alignment and whether a forward reference was
|
|
7
|
+
quoted — so maintaining both bought nothing and cost a drift risk that had
|
|
8
|
+
already come due elsewhere in this stack.
|
|
9
|
+
|
|
10
|
+
Slot measurement and the keep/wrap/shrink ladder are format reasoning, not
|
|
11
|
+
application policy: core's own table and field APIs call them. What this layer
|
|
12
|
+
owns is the application half — seal placement, PDF extraction, institutional
|
|
13
|
+
rules — which stays here and is not re-exported from core.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from hwpx.form_fit.engine import ( # noqa: F401 - re-exported public surface
|
|
19
|
+
FitEngine,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"FitEngine",
|
|
24
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Re-export of the neutral fit measurement model that python-hwpx owns.
|
|
3
|
+
|
|
4
|
+
This module used to be a copy. The two versions were behaviourally identical —
|
|
5
|
+
comparing normalised syntax trees showed no semantic difference across the five
|
|
6
|
+
fit-contract modules, only comment alignment and whether a forward reference was
|
|
7
|
+
quoted — so maintaining both bought nothing and cost a drift risk that had
|
|
8
|
+
already come due elsewhere in this stack.
|
|
9
|
+
|
|
10
|
+
Slot measurement and the keep/wrap/shrink ladder are format reasoning, not
|
|
11
|
+
application policy: core's own table and field APIs call them. What this layer
|
|
12
|
+
owns is the application half — seal placement, PDF extraction, institutional
|
|
13
|
+
rules — which stays here and is not re-exported from core.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from hwpx.form_fit.measure import ( # noqa: F401 - re-exported public surface
|
|
19
|
+
Confidence,
|
|
20
|
+
DEFAULT_LINE_SPACING_RATIO,
|
|
21
|
+
DEFAULT_SAFETY,
|
|
22
|
+
GROSS_ROW_GROWTH_FACTOR,
|
|
23
|
+
MIN_LINE_SPACING_RATIO,
|
|
24
|
+
MIN_ROW_GROWTH_LINES,
|
|
25
|
+
Measurement,
|
|
26
|
+
SlotMetrics,
|
|
27
|
+
char_advance,
|
|
28
|
+
classify_char,
|
|
29
|
+
estimate_lines,
|
|
30
|
+
estimate_text_width,
|
|
31
|
+
measure,
|
|
32
|
+
resolve_slot_metrics,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"Confidence",
|
|
37
|
+
"DEFAULT_LINE_SPACING_RATIO",
|
|
38
|
+
"DEFAULT_SAFETY",
|
|
39
|
+
"GROSS_ROW_GROWTH_FACTOR",
|
|
40
|
+
"MIN_LINE_SPACING_RATIO",
|
|
41
|
+
"MIN_ROW_GROWTH_LINES",
|
|
42
|
+
"Measurement",
|
|
43
|
+
"SlotMetrics",
|
|
44
|
+
"char_advance",
|
|
45
|
+
"classify_char",
|
|
46
|
+
"estimate_lines",
|
|
47
|
+
"estimate_text_width",
|
|
48
|
+
"measure",
|
|
49
|
+
"resolve_slot_metrics",
|
|
50
|
+
]
|