hwpx-kit-cli 0.3.0__tar.gz → 0.4.0__tar.gz

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 (93) hide show
  1. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/.claude-plugin/plugin.json +1 -1
  2. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/CLAUDE.md +1 -1
  3. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/PKG-INFO +6 -4
  4. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/README.md +5 -3
  5. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/pyproject.toml +1 -1
  6. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/skills/hwpx-form/SKILL.md +17 -1
  7. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/adapter/hwpx_engine.py +65 -0
  8. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/cli.py +35 -0
  9. hwpx_kit_cli-0.4.0/src/hwpx_kit/commands/header_footer.py +13 -0
  10. hwpx_kit_cli-0.4.0/src/hwpx_kit/commands/image_add.py +25 -0
  11. hwpx_kit_cli-0.4.0/tests/test_image_header.py +152 -0
  12. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/.claude-plugin/marketplace.json +0 -0
  13. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  14. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  15. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/.github/workflows/tests.yml +0 -0
  16. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/.gitignore +0 -0
  17. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/LICENSE +0 -0
  18. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/commands/hwpx.md +0 -0
  19. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/docs/images/After.png +0 -0
  20. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/docs/images/Before.png +0 -0
  21. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/docs/images/Tasking.png +0 -0
  22. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/install.ps1 +0 -0
  23. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/install.sh +0 -0
  24. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/sample-values.json +0 -0
  25. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/scripts/build_skill_package.py +0 -0
  26. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/scripts/check_stdout_purity.py +0 -0
  27. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/scripts/make_sample.py +0 -0
  28. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/skills/doc-create/SKILL.md +0 -0
  29. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/skills/format-convert/SKILL.md +0 -0
  30. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/skills/gongmun-format/SKILL.md +0 -0
  31. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/skills/office-export/SKILL.md +0 -0
  32. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/skills/table-calc/SKILL.md +0 -0
  33. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/__init__.py +0 -0
  34. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/adapter/__init__.py +0 -0
  35. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/adapter/base.py +0 -0
  36. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/adapter/kordoc_engine.py +0 -0
  37. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/adapter/office_readers.py +0 -0
  38. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/__init__.py +0 -0
  39. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/analyze.py +0 -0
  40. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/convert.py +0 -0
  41. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/export.py +0 -0
  42. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/fill.py +0 -0
  43. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/fill_batch.py +0 -0
  44. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/fmt.py +0 -0
  45. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/generate.py +0 -0
  46. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/inspect_doc.py +0 -0
  47. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/outline.py +0 -0
  48. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/page_break.py +0 -0
  49. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/read.py +0 -0
  50. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/render.py +0 -0
  51. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/row_height.py +0 -0
  52. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/table_clear.py +0 -0
  53. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/table_copy.py +0 -0
  54. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/table_map_cmd.py +0 -0
  55. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/table_new.py +0 -0
  56. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/table_rows.py +0 -0
  57. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/table_set.py +0 -0
  58. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/validate.py +0 -0
  59. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/format.py +0 -0
  60. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/inspect_rules.py +0 -0
  61. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/output.py +0 -0
  62. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/templates/Default_Template.hwpx +0 -0
  63. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/__init__.py +0 -0
  64. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/conftest.py +0 -0
  65. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/fixtures/real/legacy-sample.hwp +0 -0
  66. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/fixtures/real/seoul-attachment.hwpx +0 -0
  67. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/fixtures/real/seoul-body.hwpx +0 -0
  68. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/fixtures/real/seoul-report-brief.hwpx +0 -0
  69. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/fixtures/real/seoul-report-cover.hwpx +0 -0
  70. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_adapter.py +0 -0
  71. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_analyze.py +0 -0
  72. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_cli.py +0 -0
  73. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_convert.py +0 -0
  74. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_export.py +0 -0
  75. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_fill.py +0 -0
  76. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_fill_batch.py +0 -0
  77. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_format.py +0 -0
  78. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_inspect.py +0 -0
  79. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_integration.py +0 -0
  80. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_kordoc.py +0 -0
  81. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_office_readers.py +0 -0
  82. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_output.py +0 -0
  83. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_page_break.py +0 -0
  84. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_read_validate.py +0 -0
  85. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_real_forms.py +0 -0
  86. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_row_height.py +0 -0
  87. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_skill_package.py +0 -0
  88. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_stdout_purity.py +0 -0
  89. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_table_clear.py +0 -0
  90. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_table_copy.py +0 -0
  91. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_table_map_new.py +0 -0
  92. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_table_rows.py +0 -0
  93. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_table_set.py +0 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hwpx-kit",
3
3
  "description": "한글(HWPX/HWP) 문서 자동화 스킬 모음 — 양식 분석·채우기·백지 생성·표기 변환·공문 규약·표 계산·오피스 출력. CLI는 첫 사용 시 스킬이 설치를 안내 (Python 3.10+ 필요).",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "author": {
6
6
  "name": "6aneffy",
7
7
  "email": "tkdgus990809@gmail.com"
@@ -26,7 +26,7 @@ skills/*/SKILL.md → Claude Code 플러그인 스킬들 (.claude-plugin/plugin.
26
26
  └─ office-export → hwpx→docx(변환)·pptx/xlsx(재구성, officecli 선택 의존)
27
27
 
28
28
  CLI (cli.py) — JSON 봉투, 종료코드 0/1/2
29
- └─ commands/ (analyze·fill·fill-batch·inspect·read·validate·convert·export·render·generate·fmt·row-add·row-del·row-height·table-clear·table-set·table-copy·table-map·table-new·page-break·outline)
29
+ └─ commands/ (analyze·fill·fill-batch·inspect·image-add·header-footer·read·validate·convert·export·render·generate·fmt·row-add·row-del·row-height·table-clear·table-set·table-copy·table-map·table-new·page-break·outline)
30
30
  ├─ format.py ← 순수함수 (금액한글화·요일·만나이) — 엔진 임포트 금지, fmt는 파일 인수 없음(FILE_NOT_FOUND 가드 우회)
31
31
  └─ adapter/ ← 명령 계층은 어댑터 인터페이스만 사용 (엔진 직접 import 금지)
32
32
  ├─ hwpx_engine.py → python-hwpx (hwpx 분석·채우기·검증·표 조작, 순수 Python)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hwpx-kit-cli
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: 한글(HWPX) 문서 자동화 CLI — Korean HWPX document automation for AI agents: analyze, fill, read, validate, table ops
5
5
  Project-URL: Homepage, https://github.com/6aneffy/hwpx-kit
6
6
  Project-URL: Repository, https://github.com/6aneffy/hwpx-kit
@@ -33,7 +33,7 @@ Description-Content-Type: text/markdown
33
33
  [![PyPI](https://img.shields.io/pypi/v/hwpx-kit-cli.svg)](https://pypi.org/project/hwpx-kit-cli/)
34
34
  [![Python](https://img.shields.io/badge/python-3.10%2B-3776AB.svg)](https://www.python.org/)
35
35
  [![Claude Code](https://img.shields.io/badge/Claude%20Code-plugin-d97757.svg)](https://claude.com/claude-code)
36
- [![Tests](https://img.shields.io/badge/tests-176%20passing-brightgreen.svg)](tests/)
36
+ [![Tests](https://img.shields.io/badge/tests-186%20passing-brightgreen.svg)](tests/)
37
37
 
38
38
  </div>
39
39
 
@@ -154,6 +154,8 @@ hwpx-kit row-add 문서.hwpx --table 3 --like 2 --count 3 --out 결과.hwpx
154
154
  hwpx-kit row-del 문서.hwpx --table 3 --rows 5-7 --out 결과.hwpx # 행 삭제
155
155
  hwpx-kit row-height 문서.hwpx --table 3 --like 1 --rows 2-5 --out 결과.hwpx # 행 높이 정돈
156
156
  hwpx-kit page-break 문서.hwpx --table 5 --out 결과.hwpx # 쪽나눔 (새 장 시작)
157
+ hwpx-kit image-add 문서.hwpx --image 도장.png --at-text "(인)" --width-mm 20 --out 결과.hwpx # 직인·사진
158
+ hwpx-kit header-footer 문서.hwpx --footer "대외비" --page-number center --out 결과.hwpx
157
159
 
158
160
  # 변환·생성
159
161
  hwpx-kit fmt --amount 12340 --json # 금12,340원(금일만이천삼백사십원)
@@ -190,7 +192,7 @@ hwpx-kit render 문서.hwpx --out p.svg # SVG 미리보기 (kordoc)
190
192
  ## 아키텍처
191
193
 
192
194
  ```
193
- Claude Code 스킬 6종 ──→ hwpx-kit CLI (23 명령, JSON 봉투)
195
+ Claude Code 스킬 6종 ──→ hwpx-kit CLI (25 명령, JSON 봉투)
194
196
  └─ adapter/ ← 엔진 격리 계층
195
197
  ├─ python-hwpx hwpx 분석·채우기·표 조작
196
198
  ├─ pypdf·python-docx·openpyxl 타 포맷 읽기
@@ -206,7 +208,7 @@ templates/ 기본 계획서 템플릿 (표지·장 헤더·표 골격, 마커
206
208
 
207
209
  ## 검증
208
210
 
209
- - 자동 테스트 176개 (pytest)
211
+ - 자동 테스트 186개 (pytest)
210
212
  - 실서식 수렴 테스트: 보도자료·신청서·계획서·회의록·공고문 5계열,
211
213
  실제 공공기관 문서 30여 종으로 analyze→fill→validate 왕복
212
214
  - 실사용 시나리오 테스트 3회 (백지 생성 12분 완주, 장·표 동적 추가, 보도자료 변환)
@@ -10,7 +10,7 @@
10
10
  [![PyPI](https://img.shields.io/pypi/v/hwpx-kit-cli.svg)](https://pypi.org/project/hwpx-kit-cli/)
11
11
  [![Python](https://img.shields.io/badge/python-3.10%2B-3776AB.svg)](https://www.python.org/)
12
12
  [![Claude Code](https://img.shields.io/badge/Claude%20Code-plugin-d97757.svg)](https://claude.com/claude-code)
13
- [![Tests](https://img.shields.io/badge/tests-176%20passing-brightgreen.svg)](tests/)
13
+ [![Tests](https://img.shields.io/badge/tests-186%20passing-brightgreen.svg)](tests/)
14
14
 
15
15
  </div>
16
16
 
@@ -131,6 +131,8 @@ hwpx-kit row-add 문서.hwpx --table 3 --like 2 --count 3 --out 결과.hwpx
131
131
  hwpx-kit row-del 문서.hwpx --table 3 --rows 5-7 --out 결과.hwpx # 행 삭제
132
132
  hwpx-kit row-height 문서.hwpx --table 3 --like 1 --rows 2-5 --out 결과.hwpx # 행 높이 정돈
133
133
  hwpx-kit page-break 문서.hwpx --table 5 --out 결과.hwpx # 쪽나눔 (새 장 시작)
134
+ hwpx-kit image-add 문서.hwpx --image 도장.png --at-text "(인)" --width-mm 20 --out 결과.hwpx # 직인·사진
135
+ hwpx-kit header-footer 문서.hwpx --footer "대외비" --page-number center --out 결과.hwpx
134
136
 
135
137
  # 변환·생성
136
138
  hwpx-kit fmt --amount 12340 --json # 금12,340원(금일만이천삼백사십원)
@@ -167,7 +169,7 @@ hwpx-kit render 문서.hwpx --out p.svg # SVG 미리보기 (kordoc)
167
169
  ## 아키텍처
168
170
 
169
171
  ```
170
- Claude Code 스킬 6종 ──→ hwpx-kit CLI (23 명령, JSON 봉투)
172
+ Claude Code 스킬 6종 ──→ hwpx-kit CLI (25 명령, JSON 봉투)
171
173
  └─ adapter/ ← 엔진 격리 계층
172
174
  ├─ python-hwpx hwpx 분석·채우기·표 조작
173
175
  ├─ pypdf·python-docx·openpyxl 타 포맷 읽기
@@ -183,7 +185,7 @@ templates/ 기본 계획서 템플릿 (표지·장 헤더·표 골격, 마커
183
185
 
184
186
  ## 검증
185
187
 
186
- - 자동 테스트 176개 (pytest)
188
+ - 자동 테스트 186개 (pytest)
187
189
  - 실서식 수렴 테스트: 보도자료·신청서·계획서·회의록·공고문 5계열,
188
190
  실제 공공기관 문서 30여 종으로 analyze→fill→validate 왕복
189
191
  - 실사용 시나리오 테스트 3회 (백지 생성 12분 완주, 장·표 동적 추가, 보도자료 변환)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hwpx-kit-cli"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "한글(HWPX) 문서 자동화 CLI — Korean HWPX document automation for AI agents: analyze, fill, read, validate, table ops"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: hwpx-kit
3
- description: HWPX 양식 자동화 — 한글(HWPX/HWP) 양식 분석, 내용 채우기, 본문 추출, 구조 검증, .hwp→.hwpx 변환. 사용자가 hwpx/hwp 파일을 주며 "이 양식에 채워줘", "여기에 써줘", "이 명단으로 30명분 만들어줘"(일괄 생성), "이 엑셀 표 넣어줘", "이 문서 읽어줘/요약해줘", "hwp 변환해줘" 할 때 사용. Triggers: fill this Korean form, mail merge certificates, read this hwp/hwpx file, Hangul document automation.
3
+ description: HWPX 양식 자동화 — 한글(HWPX/HWP) 양식 분석, 내용 채우기, 본문 추출, 구조 검증, .hwp→.hwpx 변환. 사용자가 hwpx/hwp 파일을 주며 "이 양식에 채워줘", "여기에 써줘", "이 명단으로 30명분 만들어줘"(일괄 생성), "이 엑셀 표 넣어줘", "도장 찍어줘/사진 붙여줘", "쪽번호 넣어줘", "이 문서 읽어줘/요약해줘", "hwp 변환해줘" 할 때 사용. Triggers: fill this Korean form, mail merge certificates, read this hwp/hwpx file, Hangul document automation.
4
4
  ---
5
5
 
6
6
  # hwpx-kit: HWPX 양식 자동화
@@ -157,6 +157,22 @@ CLI `hwpx-kit`으로 hwpx 문서를 다룬다. 항상 `--json`을 붙여 실행
157
157
  금지문자는 `_` 치환. 원본 불변은 fill과 동일
158
158
  5. 개인정보 든 명단 파일은 사용자 문서와 동일한 기밀 취급 — 커밋·업로드 금지
159
159
 
160
+ ## 사진·직인 넣기 — "여기 도장 찍어줘", "사진 붙여줘"
161
+
162
+ `hwpx-kit image-add <문서.hwpx> --image <파일.png> ... --out <결과.hwpx>`
163
+ - **직인/서명**: 대상 문단 원문으로 — `--at-text "서명: (인)" --width-mm 20`
164
+ (원문은 read 출력에서 복사. 이미지 파일은 사용자에게 받는다 — 도장을 그려주지 않는다)
165
+ - **사진 첨부 칸**(표 안): `--table N --cell R,C --width-mm 35 --height-mm 45`
166
+ (증명사진 규격 예시. 좌표는 table-map으로 확인)
167
+ - 글자처럼취급으로 들어가므로 셀 크기가 이미지에 맞을수록 깔끔 — 크게 넣으면
168
+ 행이 부푼다. 삽입 후 validate + 한글 육안 확인 안내
169
+ - 사용자 제공 사진·도장 이미지도 문서와 동일한 기밀 취급
170
+
171
+ ## 머리말·꼬리말·쪽번호 — "쪽번호 넣어줘"
172
+
173
+ `hwpx-kit header-footer <문서.hwpx> [--header "텍스트"] [--footer "텍스트"] [--page-number center] --out <결과.hwpx>`
174
+ — 쪽번호 위치는 left/center/right. 문서관리번호·대외비 표기 등에 사용.
175
+
160
176
  ## 엑셀 표를 한글 문서에 넣기 — "이 엑셀 표 문서에 넣어줘"
161
177
 
162
178
  복붙(서식 깨짐) 대신 도구 조합으로:
@@ -671,6 +671,71 @@ class HwpxEngineAdapter:
671
671
  self._mark_sections_dirty()
672
672
  return len(targets)
673
673
 
674
+ _HWPUNIT_PER_MM = 7200 / 25.4 # 1mm ≈ 283.46 hwpunit
675
+
676
+ def insert_image(self, image_path: str, *, at_text: str | None = None,
677
+ table_index: int | None = None, cell: tuple[int, int] | None = None,
678
+ width_mm: float = 20.0, height_mm: float | None = None) -> dict:
679
+ """이미지를 문단(원문 앵커) 또는 표 셀에 글자처럼취급으로 삽입.
680
+
681
+ 엔진 2단계: add_image(바이너리 등록) → paragraph.add_picture(배치).
682
+ height_mm 생략 시 정사각(width와 동일) — 비율 유지가 필요하면 호출자가 지정.
683
+ """
684
+ import os as _os
685
+
686
+ ext = _os.path.splitext(image_path)[1].lower().lstrip(".")
687
+ if ext == "jpeg":
688
+ ext = "jpg"
689
+ if ext not in ("png", "jpg", "bmp", "gif"):
690
+ raise ValueError(f"지원하지 않는 이미지 형식: .{ext} (png/jpg/bmp/gif)")
691
+ with open(image_path, "rb") as f:
692
+ image_data = f.read()
693
+
694
+ width = int(width_mm * self._HWPUNIT_PER_MM)
695
+ height = int((height_mm if height_mm is not None else width_mm) * self._HWPUNIT_PER_MM)
696
+
697
+ with quiet_engine():
698
+ if cell is not None:
699
+ if table_index is None:
700
+ raise ValueError("--cell은 --table과 함께 지정하세요.")
701
+ from hwpx.tools import table_navigation as tn
702
+
703
+ tables = tn._collect_document_tables(self._doc)
704
+ if not 0 <= table_index < len(tables):
705
+ raise ValueError(f"표 인덱스 범위 밖: {table_index} (표 {len(tables)}개)")
706
+ r, c = cell
707
+ paragraphs = tables[table_index].table.cell(r, c).paragraphs
708
+ target = list(paragraphs)[0]
709
+ else:
710
+ target = self._find_anchor_paragraph(anchor_text=at_text,
711
+ after_table=table_index)
712
+ item_id = self._doc.add_image(image_data, ext)
713
+ target.add_picture(item_id, width=width, height=height, treat_as_char=True)
714
+ return {"binary_item_id": item_id, "width": width, "height": height}
715
+
716
+ def set_header_footer(self, *, header: str | None = None,
717
+ footer: str | None = None,
718
+ page_number: str | None = None) -> list[str]:
719
+ """머리말/꼬리말 텍스트·쪽번호 설정. 적용 항목 이름 목록 반환."""
720
+ applied: list[str] = []
721
+ align_map = {"left": "LEFT", "center": "CENTER", "right": "RIGHT"}
722
+ with quiet_engine():
723
+ if header is not None:
724
+ self._doc.set_header_text(header)
725
+ applied.append("header")
726
+ if footer is not None:
727
+ self._doc.set_footer_text(footer)
728
+ applied.append("footer")
729
+ if page_number is not None:
730
+ align = align_map.get(page_number.lower())
731
+ if align is None:
732
+ raise ValueError(f"쪽번호 위치는 left/center/right 중 하나: {page_number}")
733
+ self._doc.set_page_number(align=align)
734
+ applied.append("page_number")
735
+ if not applied:
736
+ raise ValueError("--header/--footer/--page-number 중 하나는 지정하세요.")
737
+ return applied
738
+
674
739
  def save_copy(self, out_path: str) -> str:
675
740
  out_abs = os.path.abspath(out_path)
676
741
  if out_abs == self._source_path:
@@ -104,6 +104,25 @@ def _build_parser() -> argparse.ArgumentParser:
104
104
  pin.add_argument("--checks", help="쉼표 구분: residue,gongmun,pii (생략 시 전부)")
105
105
  pin.add_argument("--json", action="store_true")
106
106
 
107
+ pim = sub.add_parser("image-add", help="이미지(사진·직인) 삽입 — 문단 앵커 또는 표 셀에 글자처럼취급")
108
+ pim.add_argument("file")
109
+ pim.add_argument("--image", required=True, help="이미지 파일 (png/jpg/bmp/gif)")
110
+ pim.add_argument("--at-text", help="이 원문 문단에 삽입 (공백 정규화 전체 일치)")
111
+ pim.add_argument("--table", type=int, help="표 인덱스 (--cell과 함께)")
112
+ pim.add_argument("--cell", help="셀 좌표 'R,C' (0-기준)")
113
+ pim.add_argument("--width-mm", type=float, default=20.0, help="가로 크기 mm (기본 20)")
114
+ pim.add_argument("--height-mm", type=float, help="세로 크기 mm (생략 시 가로와 동일)")
115
+ pim.add_argument("--out", required=True, help="출력 hwpx 경로 (원본 불변)")
116
+ pim.add_argument("--json", action="store_true")
117
+
118
+ phf = sub.add_parser("header-footer", help="머리말/꼬리말 텍스트·쪽번호 설정")
119
+ phf.add_argument("file")
120
+ phf.add_argument("--header", help="머리말 텍스트")
121
+ phf.add_argument("--footer", help="꼬리말 텍스트")
122
+ phf.add_argument("--page-number", help="쪽번호 위치: left/center/right")
123
+ phf.add_argument("--out", required=True, help="출력 hwpx 경로 (원본 불변)")
124
+ phf.add_argument("--json", action="store_true")
125
+
107
126
  pra = sub.add_parser("row-add", help="표 행 추가 — 기준 행 서식·높이 승계, 내용은 비움 (세로 병합 걸리면 거부)")
108
127
  pra.add_argument("file")
109
128
  pra.add_argument("--table", type=int, required=True, help="표 인덱스 (0-기준)")
@@ -245,6 +264,22 @@ def main(argv: list[str] | None = None) -> int:
245
264
  )
246
265
  if not data["clean"]:
247
266
  exit_code = 2
267
+ elif args.command == "image-add":
268
+ from hwpx_kit.commands.image_add import run_image_add
269
+
270
+ data = run_image_add(
271
+ args.file, image_path=args.image, at_text=args.at_text,
272
+ table=args.table, cell=args.cell,
273
+ width_mm=args.width_mm, height_mm=args.height_mm,
274
+ out_path=args.out,
275
+ )
276
+ elif args.command == "header-footer":
277
+ from hwpx_kit.commands.header_footer import run_header_footer
278
+
279
+ data = run_header_footer(
280
+ args.file, header=args.header, footer=args.footer,
281
+ page_number=args.page_number, out_path=args.out,
282
+ )
248
283
  elif args.command == "row-add":
249
284
  from hwpx_kit.commands.table_rows import run_row_add
250
285
 
@@ -0,0 +1,13 @@
1
+ """header-footer 명령 — 머리말/꼬리말 텍스트·쪽번호 설정."""
2
+ from __future__ import annotations
3
+
4
+ from hwpx_kit.adapter.hwpx_engine import HwpxEngineAdapter
5
+
6
+
7
+ def run_header_footer(path: str, header: str | None, footer: str | None,
8
+ page_number: str | None, out_path: str) -> dict:
9
+ ad = HwpxEngineAdapter.open(path)
10
+ applied = ad.set_header_footer(header=header, footer=footer,
11
+ page_number=page_number)
12
+ out = ad.save_copy(out_path)
13
+ return {"file": path, "out": out, "applied": applied}
@@ -0,0 +1,25 @@
1
+ """image-add 명령 — 이미지(사진·직인)를 문단 앵커 또는 표 셀에 삽입."""
2
+ from __future__ import annotations
3
+
4
+ from hwpx_kit.adapter.hwpx_engine import HwpxEngineAdapter
5
+
6
+
7
+ def run_image_add(path: str, image_path: str, at_text: str | None,
8
+ table: int | None, cell: str | None,
9
+ width_mm: float, height_mm: float | None, out_path: str) -> dict:
10
+ if (at_text is None) == (cell is None):
11
+ raise ValueError("--at-text 또는 (--table + --cell) 중 정확히 하나를 지정하세요.")
12
+ cell_rc = None
13
+ if cell is not None:
14
+ try:
15
+ r, c = cell.split(",")
16
+ cell_rc = (int(r), int(c))
17
+ except ValueError:
18
+ raise ValueError(f"--cell 형식은 'R,C': {cell!r}") from None
19
+
20
+ ad = HwpxEngineAdapter.open(path)
21
+ info = ad.insert_image(image_path, at_text=at_text, table_index=table,
22
+ cell=cell_rc, width_mm=width_mm, height_mm=height_mm)
23
+ out = ad.save_copy(out_path)
24
+ return {"file": path, "out": out, "image": image_path,
25
+ "inserted": True, **info}
@@ -0,0 +1,152 @@
1
+ """image-add / header-footer — 이미지 삽입·머리말/꼬리말/쪽번호.
2
+
3
+ 이미지는 엔진 add_image(바이너리 등록)+add_picture(문단 배치) 2단계 —
4
+ 저장 왕복 생존과 원본 불변이 계약. mm 크기는 hwpx 단위로 환산된다.
5
+ """
6
+ import json
7
+
8
+ import pytest
9
+
10
+ # 1x1 투명 PNG
11
+ PNG_1PX = bytes.fromhex(
12
+ "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
13
+ "0000000d4944415478da63f8ffff3f0300050001ff5cccd90000000049454e44ae426082"
14
+ )
15
+
16
+ from hwpx_kit.cli import main
17
+ from hwpx_kit.commands.header_footer import run_header_footer
18
+ from hwpx_kit.commands.image_add import run_image_add
19
+
20
+
21
+ @pytest.fixture()
22
+ def doc_with_anchor(tmp_path):
23
+ from hwpx.document import HwpxDocument
24
+
25
+ from hwpx_kit.output import quiet_engine
26
+
27
+ with quiet_engine():
28
+ doc = HwpxDocument.new()
29
+ doc.add_paragraph("신청서")
30
+ doc.add_paragraph("서명: (인)")
31
+ t = doc.add_table(2, 2)
32
+ t.set_cell_text(0, 0, "사진")
33
+ path = str(tmp_path / "form.hwpx")
34
+ doc.save_to_path(path)
35
+ return path
36
+
37
+
38
+ @pytest.fixture()
39
+ def png_file(tmp_path):
40
+ p = tmp_path / "stamp.png"
41
+ p.write_bytes(PNG_1PX)
42
+ return str(p)
43
+
44
+
45
+ def _image_count(path):
46
+ from hwpx.document import HwpxDocument
47
+
48
+ from hwpx_kit.output import quiet_engine
49
+
50
+ with quiet_engine():
51
+ return len(HwpxDocument.open(path).list_images())
52
+
53
+
54
+ def test_image_add_at_text(doc_with_anchor, png_file, tmp_path):
55
+ out = str(tmp_path / "o.hwpx")
56
+ result = run_image_add(doc_with_anchor, image_path=png_file,
57
+ at_text="서명: (인)", table=None, cell=None,
58
+ width_mm=20, height_mm=None, out_path=out)
59
+ assert result["inserted"] is True
60
+ assert _image_count(out) == 1
61
+ assert _image_count(doc_with_anchor) == 0 # 원본 불변
62
+
63
+
64
+ def test_image_add_into_table_cell(doc_with_anchor, png_file, tmp_path):
65
+ out = str(tmp_path / "o.hwpx")
66
+ result = run_image_add(doc_with_anchor, image_path=png_file,
67
+ at_text=None, table=0, cell="1,0",
68
+ width_mm=25, height_mm=30, out_path=out)
69
+ assert result["inserted"] is True
70
+ assert _image_count(out) == 1
71
+
72
+
73
+ def test_image_add_validates_roundtrip(doc_with_anchor, png_file, tmp_path):
74
+ from hwpx_kit.commands.validate import run_validate
75
+
76
+ out = str(tmp_path / "o.hwpx")
77
+ run_image_add(doc_with_anchor, image_path=png_file,
78
+ at_text="서명: (인)", table=None, cell=None,
79
+ width_mm=20, height_mm=None, out_path=out)
80
+ assert run_validate(out)["valid"] is True
81
+
82
+
83
+ def test_image_add_rejects_missing_anchor(doc_with_anchor, png_file, tmp_path):
84
+ with pytest.raises(ValueError, match="문단"):
85
+ run_image_add(doc_with_anchor, image_path=png_file,
86
+ at_text="없는 문장", table=None, cell=None,
87
+ width_mm=20, height_mm=None, out_path=str(tmp_path / "o.hwpx"))
88
+
89
+
90
+ def test_image_add_rejects_bad_format(doc_with_anchor, tmp_path):
91
+ bad = tmp_path / "note.txt"
92
+ bad.write_text("텍스트")
93
+ with pytest.raises(ValueError, match="형식"):
94
+ run_image_add(doc_with_anchor, image_path=str(bad),
95
+ at_text="서명: (인)", table=None, cell=None,
96
+ width_mm=20, height_mm=None, out_path=str(tmp_path / "o.hwpx"))
97
+
98
+
99
+ def test_cli_image_add(doc_with_anchor, png_file, tmp_path, capsys):
100
+ out = str(tmp_path / "o.hwpx")
101
+ code = main(["image-add", doc_with_anchor, "--image", png_file,
102
+ "--at-text", "서명: (인)", "--width-mm", "20",
103
+ "--out", out, "--json"])
104
+ env = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
105
+ assert code == 0 and env["ok"] is True
106
+ assert _image_count(out) == 1
107
+
108
+
109
+ # ── header-footer ─────────────────────────────────────────────
110
+
111
+ def _headers_footers(path):
112
+ from hwpx.document import HwpxDocument
113
+
114
+ from hwpx_kit.output import quiet_engine
115
+
116
+ with quiet_engine():
117
+ d = HwpxDocument.open(path)
118
+ return d.headers
119
+
120
+
121
+ def test_header_footer_set(doc_with_anchor, tmp_path):
122
+ out = str(tmp_path / "o.hwpx")
123
+ result = run_header_footer(doc_with_anchor, header="문서관리번호 2026-01",
124
+ footer=None, page_number=None, out_path=out)
125
+ assert "header" in result["applied"]
126
+ # 재열기 후에도 구조 유효 + 헤더 존재 (증발 회귀 감지)
127
+ from hwpx_kit.commands.validate import run_validate
128
+
129
+ assert run_validate(out)["valid"] is True
130
+ assert len(_headers_footers(out)) >= 1
131
+
132
+
133
+ def test_page_number_set(doc_with_anchor, tmp_path):
134
+ out = str(tmp_path / "o.hwpx")
135
+ result = run_header_footer(doc_with_anchor, header=None, footer=None,
136
+ page_number="center", out_path=out)
137
+ assert "page_number" in result["applied"]
138
+
139
+
140
+ def test_header_footer_requires_one(doc_with_anchor, tmp_path):
141
+ with pytest.raises(ValueError):
142
+ run_header_footer(doc_with_anchor, header=None, footer=None,
143
+ page_number=None, out_path=str(tmp_path / "o.hwpx"))
144
+
145
+
146
+ def test_cli_header_footer(doc_with_anchor, tmp_path, capsys):
147
+ out = str(tmp_path / "o.hwpx")
148
+ code = main(["header-footer", doc_with_anchor, "--footer", "대외비",
149
+ "--page-number", "center", "--out", out, "--json"])
150
+ env = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
151
+ assert code == 0 and env["ok"] is True
152
+ assert set(env["data"]["applied"]) == {"footer", "page_number"}
File without changes
File without changes
File without changes
File without changes