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.
Files changed (217) hide show
  1. hwpx_automation/__init__.py +61 -0
  2. hwpx_automation/__init__.pyi +27 -0
  3. hwpx_automation/__main__.py +8 -0
  4. hwpx_automation/agent_document.py +392 -0
  5. hwpx_automation/api.py +136 -0
  6. hwpx_automation/blind_eval.py +407 -0
  7. hwpx_automation/capabilities.py +110 -0
  8. hwpx_automation/compat.py +48 -0
  9. hwpx_automation/configuration.py +60 -0
  10. hwpx_automation/core/__init__.py +2 -0
  11. hwpx_automation/core/content.py +762 -0
  12. hwpx_automation/core/context.py +111 -0
  13. hwpx_automation/core/diff.py +53 -0
  14. hwpx_automation/core/document.py +37 -0
  15. hwpx_automation/core/formatting.py +513 -0
  16. hwpx_automation/core/handles.py +24 -0
  17. hwpx_automation/core/locations.py +205 -0
  18. hwpx_automation/core/locator.py +162 -0
  19. hwpx_automation/core/plan.py +680 -0
  20. hwpx_automation/core/resources.py +42 -0
  21. hwpx_automation/core/search.py +296 -0
  22. hwpx_automation/core/transactions.py +434 -0
  23. hwpx_automation/core/txn.py +48 -0
  24. hwpx_automation/document_state.py +95 -0
  25. hwpx_automation/errors.py +174 -0
  26. hwpx_automation/execution_lock.py +15 -0
  27. hwpx_automation/fastmcp_adapter.py +672 -0
  28. hwpx_automation/form_fill.py +1177 -0
  29. hwpx_automation/form_output_models.py +223 -0
  30. hwpx_automation/handlers/__init__.py +2 -0
  31. hwpx_automation/handlers/_shared.py +377 -0
  32. hwpx_automation/handlers/agent_document.py +257 -0
  33. hwpx_automation/handlers/authoring.py +750 -0
  34. hwpx_automation/handlers/content_edit.py +1078 -0
  35. hwpx_automation/handlers/form_fill.py +607 -0
  36. hwpx_automation/handlers/layout_style.py +660 -0
  37. hwpx_automation/handlers/quality_render.py +566 -0
  38. hwpx_automation/handlers/read_export.py +1295 -0
  39. hwpx_automation/handlers/specialized.py +624 -0
  40. hwpx_automation/handlers/tracked_changes.py +589 -0
  41. hwpx_automation/handlers/workflow.py +105 -0
  42. hwpx_automation/hwp_converter.py +227 -0
  43. hwpx_automation/hwp_support.py +94 -0
  44. hwpx_automation/hwpx_ops.py +1439 -0
  45. hwpx_automation/identity.json +263 -0
  46. hwpx_automation/identity.py +18 -0
  47. hwpx_automation/ingest_adapters.py +85 -0
  48. hwpx_automation/markdown_plan.py +216 -0
  49. hwpx_automation/mcp_cli.py +29 -0
  50. hwpx_automation/metadata/tools_meta.py +40 -0
  51. hwpx_automation/mixed_form.py +3007 -0
  52. hwpx_automation/mutation_models.py +401 -0
  53. hwpx_automation/network_policy.py +232 -0
  54. hwpx_automation/office/__init__.py +14 -0
  55. hwpx_automation/office/agent/__init__.py +125 -0
  56. hwpx_automation/office/agent/_batch_verification.py +383 -0
  57. hwpx_automation/office/agent/blueprint/__init__.py +58 -0
  58. hwpx_automation/office/agent/blueprint/bundle.py +282 -0
  59. hwpx_automation/office/agent/blueprint/catalog.py +136 -0
  60. hwpx_automation/office/agent/blueprint/dump.py +520 -0
  61. hwpx_automation/office/agent/blueprint/mapping.py +312 -0
  62. hwpx_automation/office/agent/blueprint/model.py +722 -0
  63. hwpx_automation/office/agent/blueprint/native.py +621 -0
  64. hwpx_automation/office/agent/blueprint/replay.py +622 -0
  65. hwpx_automation/office/agent/catalog.py +252 -0
  66. hwpx_automation/office/agent/cli.py +647 -0
  67. hwpx_automation/office/agent/commands.py +1383 -0
  68. hwpx_automation/office/agent/document.py +801 -0
  69. hwpx_automation/office/agent/form_plan.py +1760 -0
  70. hwpx_automation/office/agent/model.py +808 -0
  71. hwpx_automation/office/agent/path.py +155 -0
  72. hwpx_automation/office/agent/query.py +230 -0
  73. hwpx_automation/office/agent/story.py +207 -0
  74. hwpx_automation/office/authoring/__init__.py +3542 -0
  75. hwpx_automation/office/authoring/advanced_generators.py +154 -0
  76. hwpx_automation/office/authoring/builder/__init__.py +52 -0
  77. hwpx_automation/office/authoring/builder/core.py +996 -0
  78. hwpx_automation/office/authoring/builder/report.py +195 -0
  79. hwpx_automation/office/authoring/design/__init__.py +30 -0
  80. hwpx_automation/office/authoring/design/_support.py +144 -0
  81. hwpx_automation/office/authoring/design/composer.py +282 -0
  82. hwpx_automation/office/authoring/design/harvest.py +305 -0
  83. hwpx_automation/office/authoring/design/plan.py +69 -0
  84. hwpx_automation/office/authoring/design/profile.py +88 -0
  85. hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
  86. hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
  87. hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
  88. hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
  89. hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
  90. hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
  91. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
  92. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
  93. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
  94. hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
  95. hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
  96. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
  97. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
  98. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
  99. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
  100. hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
  101. hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
  102. hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
  103. hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
  104. hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
  105. hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
  106. hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
  107. hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
  108. hwpx_automation/office/authoring/design/validator.py +107 -0
  109. hwpx_automation/office/authoring/presets/__init__.py +22 -0
  110. hwpx_automation/office/authoring/presets/proposal.py +538 -0
  111. hwpx_automation/office/authoring/report_parser.py +141 -0
  112. hwpx_automation/office/authoring/style_profile.py +437 -0
  113. hwpx_automation/office/authoring/template_analyzer.py +657 -0
  114. hwpx_automation/office/compliance/__init__.py +38 -0
  115. hwpx_automation/office/compliance/official_lint.py +478 -0
  116. hwpx_automation/office/compliance/pii.py +388 -0
  117. hwpx_automation/office/document_ops/__init__.py +13 -0
  118. hwpx_automation/office/document_ops/comparison.py +62 -0
  119. hwpx_automation/office/document_ops/mail_merge.py +73 -0
  120. hwpx_automation/office/document_ops/redline.py +35 -0
  121. hwpx_automation/office/evalplan/__init__.py +36 -0
  122. hwpx_automation/office/evalplan/runtime.py +2762 -0
  123. hwpx_automation/office/exam/__init__.py +44 -0
  124. hwpx_automation/office/exam/compose.py +282 -0
  125. hwpx_automation/office/exam/ir.py +44 -0
  126. hwpx_automation/office/exam/measure.py +163 -0
  127. hwpx_automation/office/exam/parser.py +150 -0
  128. hwpx_automation/office/exam/profile.py +123 -0
  129. hwpx_automation/office/form_fill/__init__.py +66 -0
  130. hwpx_automation/office/form_fill/classification.py +108 -0
  131. hwpx_automation/office/form_fill/fill_residue.py +242 -0
  132. hwpx_automation/office/form_fill/fit/__init__.py +36 -0
  133. hwpx_automation/office/form_fill/fit/apply.py +24 -0
  134. hwpx_automation/office/form_fill/fit/engine.py +24 -0
  135. hwpx_automation/office/form_fill/fit/measure.py +50 -0
  136. hwpx_automation/office/form_fill/fit/policy.py +28 -0
  137. hwpx_automation/office/form_fill/fit/report.py +28 -0
  138. hwpx_automation/office/form_fill/fit/seal.py +457 -0
  139. hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
  140. hwpx_automation/office/form_fill/guidance.py +704 -0
  141. hwpx_automation/office/form_fill/quality.py +961 -0
  142. hwpx_automation/office/form_fill/split_run.py +333 -0
  143. hwpx_automation/office/form_fill/template_formfit.py +656 -0
  144. hwpx_automation/office/house_style/__init__.py +196 -0
  145. hwpx_automation/office/house_style/composition.py +68 -0
  146. hwpx_automation/office/house_style/data/bank.json +625 -0
  147. hwpx_automation/office/house_style/data/genres.json +43 -0
  148. hwpx_automation/office/quality/__init__.py +14 -0
  149. hwpx_automation/office/quality/page_guard.py +277 -0
  150. hwpx_automation/office/rendering/__init__.py +145 -0
  151. hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
  152. hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
  153. hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
  154. hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
  155. hwpx_automation/office/rendering/block_splits.py +76 -0
  156. hwpx_automation/office/rendering/detectors.py +151 -0
  157. hwpx_automation/office/rendering/diff.py +153 -0
  158. hwpx_automation/office/rendering/fixture_corpus.py +215 -0
  159. hwpx_automation/office/rendering/oracle.py +909 -0
  160. hwpx_automation/office/rendering/page_qa.py +245 -0
  161. hwpx_automation/office/rendering/qa_contracts.py +293 -0
  162. hwpx_automation/office/rendering/qa_metrics.py +241 -0
  163. hwpx_automation/office/rendering/worker.py +290 -0
  164. hwpx_automation/office/utilities/__init__.py +12 -0
  165. hwpx_automation/office/utilities/table_compute.py +477 -0
  166. hwpx_automation/ops_services/__init__.py +1 -0
  167. hwpx_automation/ops_services/_border_fill.py +283 -0
  168. hwpx_automation/ops_services/composition.py +55 -0
  169. hwpx_automation/ops_services/content_layout.py +322 -0
  170. hwpx_automation/ops_services/context.py +213 -0
  171. hwpx_automation/ops_services/form_fields.py +557 -0
  172. hwpx_automation/ops_services/media.py +178 -0
  173. hwpx_automation/ops_services/memo_style.py +477 -0
  174. hwpx_automation/ops_services/package_validation.py +166 -0
  175. hwpx_automation/ops_services/planning.py +201 -0
  176. hwpx_automation/ops_services/preview_export.py +585 -0
  177. hwpx_automation/ops_services/read_query.py +601 -0
  178. hwpx_automation/ops_services/save_policy.py +604 -0
  179. hwpx_automation/ops_services/tables.py +539 -0
  180. hwpx_automation/ops_services/transactions.py +616 -0
  181. hwpx_automation/preview_output_models.py +69 -0
  182. hwpx_automation/public-modules.json +206 -0
  183. hwpx_automation/py.typed +1 -0
  184. hwpx_automation/quality.py +351 -0
  185. hwpx_automation/quality_generation.py +725 -0
  186. hwpx_automation/runtime.py +321 -0
  187. hwpx_automation/runtime_services.py +100 -0
  188. hwpx_automation/server.py +259 -0
  189. hwpx_automation/storage.py +747 -0
  190. hwpx_automation/tool_bindings.py +170 -0
  191. hwpx_automation/tool_contract.py +982 -0
  192. hwpx_automation/upstream.py +755 -0
  193. hwpx_automation/utils/__init__.py +2 -0
  194. hwpx_automation/utils/helpers.py +29 -0
  195. hwpx_automation/visual_qa.py +667 -0
  196. hwpx_automation/workflow/__init__.py +55 -0
  197. hwpx_automation/workflow/adapters.py +482 -0
  198. hwpx_automation/workflow/dispatcher.py +213 -0
  199. hwpx_automation/workflow/models.py +243 -0
  200. hwpx_automation/workflow/policy.py +198 -0
  201. hwpx_automation/workflow/render_contracts.py +173 -0
  202. hwpx_automation/workflow/render_metrics.py +196 -0
  203. hwpx_automation/workflow/render_queue.py +482 -0
  204. hwpx_automation/workflow/render_security.py +172 -0
  205. hwpx_automation/workflow/render_transport.py +369 -0
  206. hwpx_automation/workflow/rendering.py +206 -0
  207. hwpx_automation/workflow/service.py +758 -0
  208. hwpx_automation/workflow/state_machine.py +65 -0
  209. hwpx_automation/workflow/store.py +747 -0
  210. hwpx_automation/workspace.py +1694 -0
  211. python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
  212. python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
  213. python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
  214. python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
  215. python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
  216. python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
  217. python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,249 @@
1
+ -- SPDX-License-Identifier: Apache-2.0
2
+ -- Mac Hancom (한컴오피스 한글) render backend for the VisualComplete oracle.
3
+ --
4
+ -- This build of Hancom Office HWP (com.hancom.office.hwp12.mac.general) ships
5
+ -- NO AppleScript dictionary (sdef) and NO headless convert CLI, so PDF export is
6
+ -- GUI-only. This script drives that GUI deterministically through System Events
7
+ -- (UI scripting), the Mac analogue of the Windows COM backend (_render_hwpx.ps1).
8
+ --
9
+ -- Flow, anchored on STABLE MENU-ITEM NAMES (prefix match), not pixels:
10
+ -- open <input> -> 파일 (File) > "PDF로 저장하기..." -> NSSavePanel
11
+ -- -> Return (= the default 저장 button) -> 파일 > "문서 닫기"
12
+ --
13
+ -- Why no typing is needed: the save panel is document-relative — it pre-fills
14
+ -- 위치 (location) = the input file's directory and the name field = the input
15
+ -- stem. The Python caller therefore STAGES the input as <out_dir>/<out_stem>.hwpx,
16
+ -- so pressing 저장 writes exactly <out_dir>/<out_stem>.pdf. The caller also
17
+ -- pre-deletes the target, so the "대치(replace)?" sheet never appears in normal
18
+ -- operation; it is still dismissed defensively here. A "문서 닫기" save-changes
19
+ -- prompt is likewise discarded (we only exported; the doc is unmodified).
20
+ --
21
+ -- Usage: osascript _render_hwpx_mac.applescript <input.hwpx> <out.pdf> [timeoutSecs]
22
+ -- Output: prints "OK" on success; prints "ERR: <reason>" and exits 1 otherwise.
23
+
24
+ property procName : "Hancom Office HWP"
25
+ property appName : "Hancom Office HWP"
26
+ property pdfDialogTitle : "PDF로 저장하기"
27
+ property fileMenuName : "파일"
28
+ property savePdfPrefix : "PDF로 저장하기"
29
+ property closeDocPrefix : "문서 닫기"
30
+
31
+ on run argv
32
+ if (count of argv) < 2 then return "ERR: usage: <input.hwpx> <out.pdf> [timeoutSecs]"
33
+ set inputPath to item 1 of argv
34
+ set outPdf to item 2 of argv
35
+ set timeoutSecs to 90
36
+ if (count of argv) ≥ 3 then
37
+ try
38
+ set timeoutSecs to (item 3 of argv) as integer
39
+ end try
40
+ end if
41
+
42
+ set inputBase to do shell script "basename " & quoted form of inputPath
43
+
44
+ try
45
+ -- 1) Open the staged input. LaunchServices focuses Hancom (launching it
46
+ -- if needed) and opens the document.
47
+ do shell script "open -a " & quoted form of appName & " " & quoted form of inputPath
48
+
49
+ -- 2) Wait for the document window to exist.
50
+ if not (waitForWindowNamed(inputBase, timeoutSecs)) then
51
+ return "ERR: document window did not open: " & inputBase
52
+ end if
53
+ -- A prior timed-out render can leave a same-named window behind, and
54
+ -- LaunchServices does not guarantee that the newly opened document is
55
+ -- frontmost. Raise the exact staged document before using its File menu.
56
+ raiseWindowNamed(inputBase)
57
+ delay 0.3
58
+
59
+ -- 3) 파일 > "PDF로 저장하기..." (open the export save panel)
60
+ clickFileMenuItemByPrefix(savePdfPrefix)
61
+
62
+ -- 4) Wait for the export dialog.
63
+ if not (waitForWindowNamed(pdfDialogTitle, 30)) then
64
+ return "ERR: PDF save dialog did not appear"
65
+ end if
66
+ delay 1.0
67
+
68
+ -- 5) Press Return = the default 저장 button (no typing: panel is
69
+ -- pre-filled with 위치=out_dir, name=out_stem from the staged input).
70
+ tell application "System Events" to tell process procName
71
+ set frontmost to true
72
+ delay 0.3
73
+ key code 36 -- Return
74
+ end tell
75
+ -- Some Hancom builds expose the dialog before its default button is
76
+ -- ready. Retry Return once only while the same dialog is still present.
77
+ delay 1.0
78
+ if listContains(windowNames(), pdfDialogTitle) then
79
+ tell application "System Events" to tell process procName
80
+ set frontmost to true
81
+ key code 36
82
+ end tell
83
+ end if
84
+
85
+ -- 6) Defensive: a "대치(replace)?" sheet only appears if the target still
86
+ -- exists. Dismiss it (대치) if present, then wait for the file.
87
+ dismissOverwriteSheetIfPresent()
88
+
89
+ -- 7) The render is asynchronous — wait until the PDF lands at out_pdf and
90
+ -- the dialog has closed.
91
+ set wrote to waitForFile(outPdf, timeoutSecs)
92
+ waitForWindowGone(pdfDialogTitle, 15)
93
+
94
+ -- 8) Always close the document so the next render starts from a clean
95
+ -- session, even if the wait above was noisy. Discard any save-changes
96
+ -- prompt (we only exported; the doc content is unmodified).
97
+ try
98
+ clickFileMenuItemByPrefix(closeDocPrefix)
99
+ dismissCloseSheetIfPresent()
100
+ end try
101
+
102
+ if not wrote then return "ERR: PDF not written to " & outPdf
103
+ return "OK"
104
+ on error errMsg number errNum
105
+ -- Best-effort cleanup so a mid-flow error doesn't leak an open document.
106
+ try
107
+ clickFileMenuItemByPrefix(closeDocPrefix)
108
+ dismissCloseSheetIfPresent()
109
+ end try
110
+ return "ERR: " & errMsg & " (" & errNum & ")"
111
+ end try
112
+ end run
113
+
114
+ -- Find the 파일 menu by name, then click the first menu item whose name starts
115
+ -- with `prefix`. Name-based so it survives index drift across Hancom versions.
116
+ on clickFileMenuItemByPrefix(prefix)
117
+ tell application "System Events" to tell process procName
118
+ set frontmost to true
119
+ delay 0.2
120
+ set fileMenu to missing value
121
+ repeat with mbi in menu bar items of menu bar 1
122
+ if (name of mbi as string) is fileMenuName then
123
+ set fileMenu to menu 1 of mbi
124
+ exit repeat
125
+ end if
126
+ end repeat
127
+ if fileMenu is missing value then error "menu '" & fileMenuName & "' not found"
128
+ repeat with mi in menu items of fileMenu
129
+ try
130
+ if (name of mi as string) starts with prefix then
131
+ click mi
132
+ return
133
+ end if
134
+ end try
135
+ end repeat
136
+ error "menu item not found: " & prefix
137
+ end tell
138
+ end clickFileMenuItemByPrefix
139
+
140
+ on raiseWindowNamed(winName)
141
+ tell application "System Events" to tell process procName
142
+ set frontmost to true
143
+ try
144
+ perform action "AXRaise" of (first window whose name is winName)
145
+ end try
146
+ end tell
147
+ end raiseWindowNamed
148
+
149
+ -- Window polling uses the ATOMIC ``name of windows`` string list (never a held
150
+ -- ``repeat with w in windows`` element reference): the save dialog appears and
151
+ -- disappears mid-flow, so a lazily-resolved ``item N of every window`` can become
152
+ -- an invalid index (-1719). Snapshotting names and guarding with ``try`` is
153
+ -- race-safe.
154
+ on windowNames()
155
+ try
156
+ tell application "System Events" to tell process procName
157
+ return (name of windows) as list
158
+ end tell
159
+ on error
160
+ return {}
161
+ end try
162
+ end windowNames
163
+
164
+ on listContains(theList, theValue)
165
+ repeat with x in theList
166
+ if (x as string) is theValue then return true
167
+ end repeat
168
+ return false
169
+ end listContains
170
+
171
+ on waitForWindowNamed(winName, secs)
172
+ repeat (secs * 2) times
173
+ if listContains(windowNames(), winName) then return true
174
+ delay 0.5
175
+ end repeat
176
+ return false
177
+ end waitForWindowNamed
178
+
179
+ on waitForWindowGone(winName, secs)
180
+ repeat (secs * 2) times
181
+ if not (listContains(windowNames(), winName)) then return true
182
+ delay 0.5
183
+ end repeat
184
+ return false
185
+ end waitForWindowGone
186
+
187
+ on waitForFile(p, secs)
188
+ -- size>0 alone is NOT completion: Hancom streams the PDF asynchronously and
189
+ -- closing the document mid-write truncates it (measured: a TOC-regenerating
190
+ -- document produced a deterministic %%EOF-less torso). Require the PDF
191
+ -- trailer marker so the export has actually finished before we move on.
192
+ repeat (secs * 2) times
193
+ if (do shell script "test -s " & quoted form of p & " && tail -c 64 " & quoted form of p & " | grep -q '%%EOF' && echo 1 || echo 0") is "1" then
194
+ return true
195
+ end if
196
+ delay 0.5
197
+ end repeat
198
+ return false
199
+ end waitForFile
200
+
201
+ -- The overwrite confirmation is a SHEET of the PDF dialog window; its buttons are
202
+ -- directly accessible (unlike the save panel's own nested buttons). Re-fetch the
203
+ -- window by name each pass and guard, since it is being torn down concurrently.
204
+ on dismissOverwriteSheetIfPresent()
205
+ repeat 6 times
206
+ try
207
+ tell application "System Events" to tell process procName
208
+ set dlg to (first window whose name is pdfDialogTitle)
209
+ if (count of sheets of dlg) > 0 then
210
+ tell sheet 1 of dlg
211
+ if (exists button "대치") then
212
+ click button "대치"
213
+ return true
214
+ end if
215
+ end tell
216
+ end if
217
+ end tell
218
+ end try
219
+ delay 0.3
220
+ end repeat
221
+ return false
222
+ end dismissOverwriteSheetIfPresent
223
+
224
+ -- If closing surfaces a save-changes sheet, discard (저장 안 함 / 안 함). We only
225
+ -- exported a PDF, so the document content is unchanged.
226
+ on dismissCloseSheetIfPresent()
227
+ repeat 6 times
228
+ set handled to false
229
+ try
230
+ tell application "System Events" to tell process procName
231
+ repeat with wn in windowNames()
232
+ set w to (first window whose name is (wn as string))
233
+ if (count of sheets of w) > 0 then
234
+ repeat with b in buttons of (sheet 1 of w)
235
+ set bn to (name of b as string)
236
+ if bn contains "안 함" or bn contains "안함" then
237
+ click b
238
+ set handled to true
239
+ end if
240
+ end repeat
241
+ end if
242
+ end repeat
243
+ end tell
244
+ end try
245
+ if handled then return true
246
+ delay 0.3
247
+ end repeat
248
+ return false
249
+ end dismissCloseSheetIfPresent
@@ -0,0 +1,76 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Renderer-neutral block split geometry.
3
+
4
+ This module deliberately knows nothing about Hancom discovery, subprocesses,
5
+ GUI automation, MCP, or form-fill policy. Callers provide glyph-like objects
6
+ with ``page``, ``x0`` and ``x1`` attributes and explicit column boundaries.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Protocol
13
+
14
+
15
+ class PositionedGlyph(Protocol):
16
+ """Minimum geometry consumed by :func:`detect_block_splits`."""
17
+
18
+ page: int
19
+ x0: float
20
+ x1: float
21
+
22
+
23
+ @dataclass
24
+ class Block:
25
+ """One logical render block and its positioned glyphs."""
26
+
27
+ id: str
28
+ glyphs: list
29
+
30
+
31
+ @dataclass
32
+ class BlockSplit:
33
+ """A block that straddles a column or page boundary."""
34
+
35
+ block_id: str
36
+ kind: str
37
+
38
+
39
+ def _column_index(x_center: float, column_x_bounds: list[tuple[float, float]]) -> int:
40
+ for index, (x0, x1) in enumerate(column_x_bounds):
41
+ if x0 <= x_center <= x1:
42
+ return index
43
+ return -1
44
+
45
+
46
+ def detect_block_splits(
47
+ blocks: list,
48
+ column_x_bounds: list,
49
+ page_height: float,
50
+ ) -> list:
51
+ """Return blocks whose glyphs span more than one page or column.
52
+
53
+ ``page_height`` remains accepted for compatibility with the established
54
+ contract. Page membership comes from each glyph's explicit ``page`` field.
55
+ """
56
+
57
+ del page_height
58
+ splits: list[BlockSplit] = []
59
+ for block in blocks:
60
+ if not block.glyphs:
61
+ continue
62
+ pages = {glyph.page for glyph in block.glyphs}
63
+ if len(pages) > 1:
64
+ splits.append(BlockSplit(block_id=block.id, kind="page"))
65
+ continue
66
+ columns = {
67
+ _column_index((glyph.x0 + glyph.x1) / 2.0, column_x_bounds)
68
+ for glyph in block.glyphs
69
+ }
70
+ columns.discard(-1)
71
+ if len(columns) > 1:
72
+ splits.append(BlockSplit(block_id=block.id, kind="column"))
73
+ return splits
74
+
75
+
76
+ __all__ = ["Block", "BlockSplit", "PositionedGlyph", "detect_block_splits"]
@@ -0,0 +1,151 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Pixel-level visual detectors (ported from the baseline measurement harness).
3
+
4
+ ``diff_ratio`` and ``overlap_score`` are byte-for-byte the same heuristics the
5
+ one-off ``scripts/visualcomplete-baseline/overlap_detect.py`` used, so the gate
6
+ reproduces the harness verdicts on the measured corpus. Added here:
7
+
8
+ * mask-aware variants (diff / new-ink restricted to outside the edit mask),
9
+ * a diff-image generator for report artifacts.
10
+
11
+ ``numpy`` and ``Pillow`` are imported lazily so importing this module (and the
12
+ whole :mod:`hwpx.visual` package) never requires the imaging stack; callers must
13
+ gate on :func:`imaging_available` and degrade otherwise.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from typing import TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING: # pragma: no cover - typing only
20
+ from PIL import Image
21
+
22
+ # Pixels darker than this (0-255 grayscale) count as "ink".
23
+ _INK_THRESHOLD = 160
24
+ # Row is "inked" when at least this fraction of its pixels are ink.
25
+ _ROW_INK_FRACTION = 0.01
26
+
27
+
28
+ def imaging_available() -> bool:
29
+ """True when numpy + Pillow can be imported (required by every detector)."""
30
+
31
+ try: # pragma: no cover - trivial import probe
32
+ import numpy # noqa: F401
33
+ from PIL import Image # noqa: F401
34
+ except Exception:
35
+ return False
36
+ return True
37
+
38
+
39
+ def _ink_mask(img: "Image.Image", threshold: int = _INK_THRESHOLD):
40
+ import numpy as np
41
+
42
+ gray = np.asarray(img.convert("L"))
43
+ return gray < threshold
44
+
45
+
46
+ def _match_size(a: "Image.Image", b: "Image.Image"):
47
+ if a.size != b.size:
48
+ width = min(a.size[0], b.size[0])
49
+ height = min(a.size[1], b.size[1])
50
+ a = a.crop((0, 0, width, height))
51
+ b = b.crop((0, 0, width, height))
52
+ return a, b
53
+
54
+
55
+ def _zero_rects(mask, rects) -> None:
56
+ """In-place: clear the given pixel rectangles in a boolean ink/diff mask."""
57
+
58
+ for x0, y0, x1, y1 in rects:
59
+ mask[y0:y1, x0:x1] = False
60
+
61
+
62
+ def diff_ratio(a: "Image.Image", b: "Image.Image") -> float:
63
+ """Fraction of pixels whose ink/no-ink state differs between ``a`` and ``b``."""
64
+
65
+ import numpy as np
66
+
67
+ a, b = _match_size(a, b)
68
+ diff = np.logical_xor(_ink_mask(a), _ink_mask(b))
69
+ return float(diff.sum()) / float(diff.size or 1)
70
+
71
+
72
+ def diff_ratio_outside_mask(a: "Image.Image", b: "Image.Image", rects) -> float:
73
+ """Like :func:`diff_ratio` but ignoring pixels inside ``rects`` (allowed region)."""
74
+
75
+ import numpy as np
76
+
77
+ a, b = _match_size(a, b)
78
+ diff = np.logical_xor(_ink_mask(a), _ink_mask(b))
79
+ if rects:
80
+ _zero_rects(diff, rects)
81
+ return float(diff.sum()) / float(diff.size or 1)
82
+
83
+
84
+ def new_ink_ratio_outside_mask(before: "Image.Image", after: "Image.Image", rects) -> float:
85
+ """Fraction of pixels that gained ink in ``after`` *outside* the mask.
86
+
87
+ This is the "the fill spilled out of its slot" signal: ink present in the
88
+ output but not the original, in a region the edit was not allowed to paint.
89
+ """
90
+
91
+
92
+ before, after = _match_size(before, after)
93
+ new_ink = _ink_mask(after) & ~_ink_mask(before)
94
+ if rects:
95
+ _zero_rects(new_ink, rects)
96
+ return float(new_ink.sum()) / float(new_ink.size or 1)
97
+
98
+
99
+ def overlap_score(img: "Image.Image") -> dict[str, float]:
100
+ """Row-band analysis. A band far taller than the median line suggests
101
+ collapsed / overlapping lines (글자 겹침)."""
102
+
103
+ import numpy as np
104
+
105
+ mask = _ink_mask(img)
106
+ row_density = mask.mean(axis=1)
107
+ inked_rows = row_density > _ROW_INK_FRACTION
108
+
109
+ bands: list[tuple[int, int]] = []
110
+ start: int | None = None
111
+ for i, inked in enumerate(inked_rows):
112
+ if inked and start is None:
113
+ start = i
114
+ elif not inked and start is not None:
115
+ bands.append((start, i))
116
+ start = None
117
+ if start is not None:
118
+ bands.append((start, len(inked_rows)))
119
+
120
+ heights = [end - begin for begin, end in bands]
121
+ median_h = float(np.median(heights)) if heights else 0.0
122
+ max_h = float(max(heights)) if heights else 0.0
123
+ return {
124
+ "ink_ratio": float(mask.mean()),
125
+ "num_bands": float(len(bands)),
126
+ "median_band_height": median_h,
127
+ "max_band_height": max_h,
128
+ "tall_band_ratio": (max_h / median_h) if median_h else 0.0,
129
+ }
130
+
131
+
132
+ def make_diff_image(a: "Image.Image", b: "Image.Image") -> "Image.Image":
133
+ """Render the ink XOR of two pages as a black-on-white diff image."""
134
+
135
+ import numpy as np
136
+ from PIL import Image
137
+
138
+ a, b = _match_size(a, b)
139
+ diff = np.logical_xor(_ink_mask(a), _ink_mask(b))
140
+ out = np.where(diff, 0, 255).astype("uint8")
141
+ return Image.fromarray(out) # 2-D uint8 -> mode "L" inferred
142
+
143
+
144
+ __all__ = [
145
+ "imaging_available",
146
+ "diff_ratio",
147
+ "diff_ratio_outside_mask",
148
+ "new_ink_ratio_outside_mask",
149
+ "overlap_score",
150
+ "make_diff_image",
151
+ ]
@@ -0,0 +1,153 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Rasterize Hancom-exported PDFs and turn page pairs into visual signals.
3
+
4
+ This is the scoring half of the gate: render PDF pages to images (``pymupdf``,
5
+ imported lazily) and run the :mod:`.detectors` over them. The
6
+ before/after comparison reproduces the baseline harness's verdict logic
7
+ (``diff_ratio`` ≥ eps, relative tall-band overlap, page-count change), extended
8
+ with edit-mask awareness for FormFit.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ import hwpx_automation.office.rendering.detectors as detectors
16
+ from hwpx.quality.rendering import EditMask
17
+
18
+ if TYPE_CHECKING: # pragma: no cover - typing only
19
+ from PIL import Image
20
+
21
+ # A page whose tallest ink band is this many times the median line height is a
22
+ # collapse signature; only flagged when the *after* page is meaningfully worse
23
+ # than the *before* page (relative, like the baseline harness).
24
+ _TALL_BAND_ABS = 3.0
25
+ _TALL_BAND_REL = 1.0
26
+
27
+
28
+ def pymupdf_available() -> bool:
29
+ try: # pragma: no cover - trivial import probe
30
+ import fitz # noqa: F401
31
+ except Exception:
32
+ return False
33
+ return True
34
+
35
+
36
+ def render_pdf_to_images(pdf_path: str | Path, dpi: int = 150) -> list["Image.Image"]:
37
+ """Rasterize every page of ``pdf_path`` to a list of RGB ``PIL.Image``."""
38
+
39
+ import fitz # pymupdf
40
+ from PIL import Image
41
+
42
+ pages: list[Image.Image] = []
43
+ with fitz.open(str(pdf_path)) as doc:
44
+ for page in doc:
45
+ pix = page.get_pixmap(dpi=dpi)
46
+ pages.append(Image.frombytes("RGB", (pix.width, pix.height), pix.samples))
47
+ return pages
48
+
49
+
50
+ def _page_overlap(before_ov: dict[str, float], after_ov: dict[str, float]) -> bool:
51
+ return (
52
+ after_ov["tall_band_ratio"] >= _TALL_BAND_ABS
53
+ and after_ov["tall_band_ratio"] > before_ov["tall_band_ratio"] + _TALL_BAND_REL
54
+ )
55
+
56
+
57
+ def compare_renders(
58
+ before_pdf: str | Path,
59
+ after_pdf: str | Path,
60
+ *,
61
+ edit_mask: EditMask | None = None,
62
+ diff_eps: float = 0.005,
63
+ dpi: int = 150,
64
+ diff_image_path: str | Path | None = None,
65
+ ) -> dict[str, Any]:
66
+ """Compare two rendered PDFs and return the visual signal dictionary."""
67
+
68
+ before_pages = render_pdf_to_images(before_pdf, dpi)
69
+ after_pages = render_pdf_to_images(after_pdf, dpi)
70
+ page_count = min(len(before_pages), len(after_pages))
71
+
72
+ max_full_diff = 0.0
73
+ max_outside_diff = 0.0
74
+ max_new_ink_outside = 0.0
75
+ overlap_flag = False
76
+ worst_page = 0
77
+
78
+ for i in range(page_count):
79
+ before_img, after_img = before_pages[i], after_pages[i]
80
+ full = detectors.diff_ratio(before_img, after_img)
81
+ if full > max_full_diff:
82
+ max_full_diff = full
83
+ worst_page = i
84
+
85
+ if edit_mask is not None and not edit_mask.is_empty:
86
+ rects = edit_mask.rects_for(i, before_img.size[0], before_img.size[1])
87
+ max_outside_diff = max(
88
+ max_outside_diff, detectors.diff_ratio_outside_mask(before_img, after_img, rects)
89
+ )
90
+ max_new_ink_outside = max(
91
+ max_new_ink_outside,
92
+ detectors.new_ink_ratio_outside_mask(before_img, after_img, rects),
93
+ )
94
+
95
+ if _page_overlap(detectors.overlap_score(before_img), detectors.overlap_score(after_img)):
96
+ overlap_flag = True
97
+
98
+ page_count_changed = len(before_pages) != len(after_pages)
99
+ has_mask = edit_mask is not None and not edit_mask.is_empty
100
+
101
+ effective_diff = max_outside_diff if has_mask else max_full_diff
102
+ unexpected_diff_outside_mask = effective_diff >= diff_eps
103
+ # Overflow: the fill painted ink outside its slot, or content spilled onto
104
+ # extra pages. Without a mask, the page-growth proxy is the available signal.
105
+ overflow_detected = len(after_pages) > len(before_pages)
106
+ if has_mask:
107
+ overflow_detected = overflow_detected or (max_new_ink_outside >= diff_eps)
108
+
109
+ diff_image_out: str | None = None
110
+ if diff_image_path is not None and page_count > 0:
111
+ image = detectors.make_diff_image(before_pages[worst_page], after_pages[worst_page])
112
+ image.save(str(diff_image_path))
113
+ diff_image_out = str(diff_image_path)
114
+
115
+ return {
116
+ "render_checked": True,
117
+ "page_count": page_count,
118
+ "before_page_count": len(before_pages),
119
+ "after_page_count": len(after_pages),
120
+ "page_count_changed": page_count_changed,
121
+ "max_diff_ratio": round(max_full_diff, 6),
122
+ "max_diff_outside_mask": round(max_outside_diff, 6),
123
+ "max_new_ink_outside_mask": round(max_new_ink_outside, 6),
124
+ "unexpected_diff_outside_mask": unexpected_diff_outside_mask,
125
+ "overlap_detected": overlap_flag,
126
+ "overflow_detected": overflow_detected,
127
+ "diff_image": diff_image_out,
128
+ }
129
+
130
+
131
+ def analyze_single(after_pdf: str | Path, *, dpi: int = 150) -> dict[str, Any]:
132
+ """Single-render structural-visual pass (new-doc generation, no before).
133
+
134
+ Without a reference there is no faithful overlap baseline, so this only
135
+ confirms the doc rasterizes and reports its page count. Overlap/overflow
136
+ judgement for new docs is a Phase-E concern; here it stays conservative.
137
+ """
138
+
139
+ after_pages = render_pdf_to_images(after_pdf, dpi)
140
+ return {
141
+ "render_checked": True,
142
+ "before_page_count": None,
143
+ "after_page_count": len(after_pages),
144
+ "page_count_changed": None,
145
+ "max_diff_ratio": None,
146
+ "unexpected_diff_outside_mask": False,
147
+ "overlap_detected": False,
148
+ "overflow_detected": False,
149
+ "diff_image": None,
150
+ }
151
+
152
+
153
+ __all__ = ["pymupdf_available", "render_pdf_to_images", "compare_renders", "analyze_single"]