Skip to content

Commit 94bbf7e

Browse files
authored
Merge pull request #23 from DanMeon/feature/v0.7.0
v0.7.0 — HWPX writeback baseline
2 parents 3692ffd + f1ebb73 commit 94bbf7e

12 files changed

Lines changed: 565 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.7.0] — 2026-06-04
11+
12+
MINOR release. parse 한 `Document` 를 다시 HWPX 로 저장하는 첫 역방향 표면을 추가한다 — v0.2.0 ~ v0.6.0 의 모든 산출물 (IR / SVG / PDF / PNG) 이 read-only 였던 것에 writeback 을 연다. 직렬화는 상류 `serialize_hwpx` 에 위임한다. 추가만 있고 기존 IR / 렌더 / MCP 표면은 모두 보존 (additive only) — IR schema (`"1.1"`) 변경 0.
13+
14+
### Added
15+
16+
- `Document.to_hwpx_bytes() -> bytes` — 문서를 HWPX (ZIP+XML) 바이트로 직렬화. 출력은 ZIP magic `b"PK\x03\x04"` 으로 시작하고 첫 엔트리가 STORED `mimetype` = `application/hwp+zip`. `Document` IR 이 포맷 독립이라 HWP5 로 parse 한 문서도 HWPX 로 출력된다 (HWP5 → HWPX 포맷 변환). 직렬화 실패 (참조 무결성 위반 — BinData 누락 등) 는 `ValueError`.
17+
- `Document.export_hwpx(path) -> int` — 문서를 HWPX 파일로 저장하고 작성 바이트 수 (> 0) 를 반환. 파일 쓰기 실패 (부모 디렉토리 부재 등) 는 `OSError`. `render_pdf` / `export_pdf` 의 메모리 / 파일 분리 패턴과 대칭.
18+
- README § "HWPX 저장 (writeback)" 신설 — `to_hwpx_bytes` / `export_hwpx` 사용 예 + HWP5 → HWPX 변환 + 보존 범위 / 에러 계약. PNG 렌더 섹션과 LangChain 통합 섹션 사이 배치.
19+
20+
보존 범위: 텍스트·문단은 round-trip 의미를 보존한다 (parse → 저장 → 재파싱 시 섹션 수 / 문단 수 / 문단 텍스트 동등). 표·그림·수식은 상류 serializer 의 현 보존 범위에 위임 — 의미 보존 미보장 (예외 없는 직렬화만 보장). round-trip 은 의미적 동등성 기준이며 byte 단위 동일은 보장하지 않는다. 표·그림 round-trip 의미 보존은 v0.8.0, HWP5 binary 출력 (`export_hwp`) 은 별도 minor.
21+
22+
### Build
23+
24+
- `external/rhwp` submodule pin `1899ef9b` (v0.7.12) → `ce45231c` (v0.7.12 + 394 commit, 2026-05-27 상류). spec·feat 는 `1899ef9b` 기준 작성됐고 (2026-05-20) GA 직전 재동기화 후 그 위에서 회귀를 재검증했다. **본 binding 관점 회귀 0**`serialize_hwpx` 시그니처 불변, `maturin develop --release` clean, `pytest -m "not slow"` 599 passed / 2 skipped (IR baseline byte-equal 포함). 흡수한 상류 변경: serializer +2092 (16 commit 거의 전부 HWP5 binary writeback `serialize_hwp` 한컴 호환 — Form 컨트롤 byte-perfect / 각주 contract / 표 셀 배경 / EQEDIT errata), model +1267, rendering +1320 — 모두 직렬화·렌더 내부라 binding 이 소비하는 IR schema (`"1.1"`) / IR·렌더 출력은 불변. 상류 HWPX round-trip IrDiff 는 여전히 Stage 0 (카운트만) 라 본 baseline 의 텍스트·문단 round-trip 보장은 binding 자체 회귀 가드가 책임.
25+
- `Cargo.toml``version` `0.6.1``0.7.0`. `pyproject.toml``dynamic = ["version"]` 으로 자동 추종.
26+
1027
## [0.6.1] — 2026-05-18
1128

1229
PATCH release. v0.6.0 (Frozen, 2026-05-10) 의 GitHub Release / PyPI publish 가 누락된 상태에서 발견된 release 인프라 정합화 + 후속 polish 를 한 묶음 PATCH 로 발행한다. 사용자 영향: PyPI 첫 게시 패키지가 `v0.5.1` 다음 `v0.6.1` 로 점프 — v0.6.0 의 모든 표면 (페이지 PNG 렌더링 + 문서 시스템 개편) 은 변경 없이 그대로 포함하며 `[0.6.0]` 섹션은 historical record 로 보존. 외부 공개 API / IR schema (`"1.1"`) 변경 0.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rhwp-python"
3-
version = "0.6.1"
3+
version = "0.7.0"
44
edition = "2021"
55
# ^ rust-version 미명시 — 상위 rhwp crate 정책(stable Rust, MSRV unclaimed) 준수.
66
# PyO3 0.28 이 Rust 1.83+ 요구하지만, 이는 README 에 문서로 안내

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,37 @@ print(message.content[0].text)
124124
`ValueError("raster pixel count out of range: ...")`. 사용자가 명시 override
125125
가능 — 예: `doc.render_png(0, max_pixels=200_000_000)`.
126126

127+
## HWPX 저장 (writeback)
128+
129+
parse 한 문서를 다시 HWPX 파일로 저장하는 역방향 표면. `Document` IR 이 포맷
130+
독립이라 HWP5 로 parse 한 문서도 HWPX 로 출력된다 (HWP5 → HWPX 포맷 변환).
131+
직렬화는 상류 `rhwp``serialize_hwpx` (PR #170) 에 위임한다.
132+
133+
```python
134+
import rhwp
135+
136+
doc = rhwp.parse("report.hwp")
137+
138+
# 메모리 bytes — ZIP magic 으로 시작 (첫 엔트리 STORED mimetype = application/hwp+zip)
139+
data: bytes = doc.to_hwpx_bytes()
140+
141+
# 디스크 저장 — 작성 바이트 수 반환
142+
written: int = doc.export_hwpx("out.hwpx")
143+
144+
# HWP5 → HWPX 변환도 동일 (입력 포맷 무관)
145+
rhwp.parse("legacy.hwp").export_hwpx("converted.hwpx")
146+
```
147+
148+
**보존 범위** — 텍스트·문단은 round-trip 의미를 보존한다 (parse → 저장 →
149+
재파싱 시 섹션 수 / 문단 수 / 문단 텍스트 동등). 표·그림·수식은 상류 serializer
150+
의 현 보존 범위에 위임하며 의미 보존을 보장하지 않는다 (예외 없는 직렬화만
151+
보장). 표·그림의 round-trip 의미 보존은 후속 버전 과제다. round-trip 은 의미적
152+
동등성 기준이며 byte 단위 동일은 보장하지 않는다 (ZIP 압축 / canonical default
153+
주입 등).
154+
155+
직렬화 실패 (참조 무결성 위반 — BinData 누락 등) 는 `ValueError`, 파일 쓰기
156+
실패 (부모 디렉토리 부재 등) 는 `OSError`.
157+
127158
## LangChain 통합
128159

129160
```bash
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
---
2+
status: Frozen
3+
description: "v0.7.0 hwpx-writeback-baseline ADR — 직렬화 source(상류 위임) / API 명명 / GIL 전략 / 보존 boundary 4개 결정의 근거"
4+
ga: v0.7.0
5+
last_updated: 2026-06-04
6+
---
7+
8+
# v0.7.0 hwpx-writeback-baseline — 설계 의사결정 리서치 요약
9+
10+
[v0.7.0/hwpx-writeback-baseline.md](../../roadmap/v0.7.0/hwpx-writeback-baseline.md) §결정 사항 중 외부 독자가 "왜?" 를 던질 만한 **4**건의 업계 선례·대안·실패 시나리오를 기록한다. spec 본문이 최종 결정을 기술하고, 본 문서는 그 결정의 근거를 담는다.
11+
12+
## 결정 매트릭스
13+
14+
| # | 항목 | 옵션 비교 | 채택 | 1차 근거 |
15+
|---|---|---|---|---|
16+
| 1 | 직렬화 source | A: 자체 직렬화 구현 / B: 상류 `serialize_hwpx` 위임 | B | upstream-first — 결함은 상류 이슈, binding 은 표면만 |
17+
| 2 | API 표면·명명 | A: `render_hwpx` / B: `to_hwpx_bytes` + `export_hwpx` / C: `save_hwpx` | B | `render_*` 는 raster 전용, `save_*` 는 mutable 시맨틱 연상 |
18+
| 3 | GIL 전략 | A: GIL 보유 / B: `Document` clone 후 `py.detach` | A | `DocumentCore` `!Sync` — clone 비용 미측정, 정확성 우선 |
19+
| 4 | 보존 boundary | A: 전체 요소 보존 보장 / B: 텍스트·문단만 보장 + 상류 위임 | B | 상류 IrDiff 검증이 점진 확장 중 — 미검증 요소 보장은 거짓 약속 |
20+
21+
## 1. 직렬화 source
22+
23+
### 팩트
24+
25+
- 상류 `external/rhwp/src/serializer/hwpx/mod.rs:40``serialize_hwpx(doc: &Document) -> Result<Vec<u8>, SerializeError>` 를 공개 API 로 export 한다.
26+
- `PyDocument``inner: DocumentCore` 를 보관하고 (`src/document.rs:15`), `self.inner.document()``&Document` 를 반환한다 (`src/document.rs:75` 등에서 사용). 상류 시그니처에 그대로 전달 가능.
27+
- 본 프로젝트의 운영 원칙: `external/rhwp` 는 upstream-owned, 로컬 수정 금지 — 결함/누락은 상류 GitHub 이슈로 보고 (자체 patch / 알고리즘 복사 금지).
28+
29+
### 검증자 반박
30+
31+
- "상류 serializer 에 버그가 있으면 binding 에서 못 고치나?" → 못 고친다 (의도적). 상류 이슈/PR 로 해결한다. 자체 patch 는 유지보수 부채 + 상류와 발산하는 fork 를 만든다.
32+
- "직렬화를 위임만 하면 binding 의 가치가 없는 것 아닌가?" → binding 의 가치는 직렬화 알고리즘 재구현이 아니라 Python 표면 / 타입 스텁 / 에러 변환 (`SerializeError``PyValueError` / `OSError`) / GIL 관리 / round-trip 회귀 테스트다.
33+
34+
### 최종 결정
35+
36+
B 채택. 상류 `serialize_hwpx` 를 그대로 위임 호출하고 자체 직렬화 구현은 하지 않는다. HWPX writer 결함은 상류 이슈로 보고한다.
37+
38+
### 1차 소스
39+
40+
- 상류 serializer 모듈: `external/rhwp/src/serializer/hwpx/mod.rs`, `external/rhwp/src/serializer/mod.rs`
41+
- 상류 PR #170 (HWPX Serializer 구현 — Document IR → HWPX 저장)
42+
- HWP5-origin Document 수용 증거 (결정 사항 §4 입력 포맷 backing): 상류 test `equation_roundtrip_from_hancom_origin_hwp_sample` (`external/rhwp/src/serializer/hwpx/mod.rs`) 가 `parse_hwp` 결과를 `serialize_hwpx` 에 직접 투입. `SerializeError::UnsupportedInput` 은 enum 에 선언만 되고 `serialize_hwpx` 경로에서 생성되지 않음 (입력 포맷 게이트 부재)
43+
44+
## 2. API 표면·명명
45+
46+
### 팩트
47+
48+
- 기존 `Document` 표면의 동사 관용: `render_svg` / `render_pdf` / `render_png` (시각 raster·view 산출물), `export_svg` / `export_pdf` / `export_png` (파일 저장), `to_ir` / `to_ir_json` (데이터 구조 변환) — `python/rhwp/document.py`.
49+
-`render_*` = 픽셀/뷰 렌더, `to_*` = 데이터 변환 (메모리), `export_*` = 파일 저장의 3분 패턴이 이미 확립돼 있다.
50+
51+
### 검증자 반박
52+
53+
- "`save_hwpx` 가 사용자에게 더 직관적이지 않나?" → `save` 는 "편집한 것을 저장" 하는 mutable 시맨틱을 연상시킨다. baseline 은 편집 없는 변환이라 부정확하고, v1.0 의 mutable IR 빌더 API 와 명명이 충돌한다.
54+
- "`render_hwpx` 는?" → `render_*` 는 픽셀/뷰 산출물 전용 의미라 ZIP+XML 포맷 직렬화에 부적합. `to_hwpx_bytes``to_ir_json` 과 같은 "구조 → 직렬 형식" 결을 따른다.
55+
56+
### 최종 결정
57+
58+
B 채택. `to_hwpx_bytes() -> bytes` (메모리) + `export_hwpx(path) -> int` (파일). 기존 `to_ir` / `export_pdf` 패턴과 정합.
59+
60+
### 1차 소스
61+
62+
- 기존 API 표면: `python/rhwp/document.py` (`render_*` / `export_*` / `to_*` 메서드군)
63+
64+
## 3. GIL 전략
65+
66+
### 팩트
67+
68+
- `src/document.rs:240-243` `to_ir` 주석: "GIL 해제 불가: `self.inner` (DocumentCore) 가 RefCell 캐시로 `!Sync` — closure 가 `&self` 를 캡처하면 `py.detach` 의 Ungil 바운드 불만족. parse (`from_bytes` — owned bytes) 와 `render_pdf` / `export_pdf` (owned svgs) 만 GIL 해제 가능."
69+
- `serialize_hwpx(self.inner.document())``&self.inner` 를 캡처한다 — 위 제약에 해당해 클로저 이동 불가.
70+
- 프로젝트 GIL 가이드: ≥1 ms Rust-side 작업은 `py.detach` 권장하되, 불확실하면 `benches/bench_gil.py` 패턴으로 측정 후 결정.
71+
72+
### 검증자 반박
73+
74+
- "ZIP 압축은 ≥1 ms 일 텐데 GIL 보유면 멀티스레드 처리량 손해 아닌가?" → 맞다. 단 `detach` 하려면 `self.inner.document().clone()` 으로 owned `Document` 를 만들어 클로저로 이동해야 한다. clone 비용 vs GIL 보유 비용은 미측정.
75+
- "clone 후 detach 가 항상 이득인가?" → 아니다. clone 비용은 `Document` 크기에 비례 — 대형 문서면 clone 이 GIL 보유보다 비쌀 수 있다. 측정 없이 단정 불가.
76+
77+
### 최종 결정
78+
79+
A 채택. baseline 은 GIL 보유로 정확성을 우선한다. clone-후-detach 최적화는 `bench_gil.py` 측정이 순이득을 보이면 후속 patch (v0.7.x) 로 분리.
80+
81+
### 1차 소스
82+
83+
- `src/document.rs` (`to_ir` GIL 주석, `render_pdf` / `export_pdf` detach 패턴)
84+
- 프로젝트 GIL 정책 (`AGENTS.md` § Rust + Python hybrid build)
85+
86+
## 4. 보존 boundary
87+
88+
### 팩트
89+
90+
- 상류 `external/rhwp/src/serializer/hwpx/section.rs:292` `render_control_slot``Table` / `Picture` / `Equation` / `Shape` 컨트롤을 emit 한다 — 표 직렬화 실패 시 `eprintln!` 후 계속 진행 (graceful degradation).
91+
- 상류 `external/rhwp/src/serializer/hwpx/roundtrip.rs:7` IrDiff 하네스 주석: "누적 확장 — Stage 0 에선 뼈대 필드 (섹션 수·문단 수·리소스 카운트) 만 비교하고, Stage 1~5 진행 시 비교 대상 필드를 누적 확장." 즉 직렬화 코드 존재와 round-trip 검증 완료는 별개.
92+
- `serialize_hwpx` 는 per-control 만 graceful (`section.rs``eprintln!`) 하고, 참조 무결성은 hard-error 다: `BinDataContent 누락` (`hwpx/mod.rs:86-93`) / `assert_all_refs_resolved()` / `assert_bin_data_3way()``Err(SerializeError)` 를 반환 → binding 에서 `ValueError` 로 전파. 보존 boundary 는 "무조건 crash-free" 가 아니라 "per-control graceful + 무결성 hard-error" 모델.
93+
94+
### 검증자 반박
95+
96+
- "표가 직렬화되는데 왜 보존을 보장하지 않나?" → 직렬화 코드 존재 ≠ round-trip 의미 보존 검증 완료. 상류가 IrDiff 로 검증한 범위 밖을 우리가 보장하면 거짓 약속이 된다.
97+
- "그럼 baseline 의 실용 가치가 텍스트뿐인가?" → 텍스트·문단 round-trip + HWP5 → HWPX 포맷 변환 + 표·그림 포함 실문서 crash-free 직렬화. 메타 정정 / 평문 교정 / 포맷 마이그레이션 시나리오를 커버한다.
98+
99+
### 최종 결정
100+
101+
B 채택. 텍스트·문단 round-trip 을 회귀로 보장하고, 표·그림 등은 상류 보존 범위에 위임 (crash-free 만 보장). 의미 보존 확장은 상류 IrDiff 진척에 맞춰 v0.8.0.
102+
103+
### 1차 소스
104+
105+
- 상류 직렬화/검증: `external/rhwp/src/serializer/hwpx/section.rs`, `external/rhwp/src/serializer/hwpx/roundtrip.rs`
106+
107+
## 참조
108+
109+
- 짝 페어 (spec): [roadmap/v0.7.0/hwpx-writeback-baseline.md](../../roadmap/v0.7.0/hwpx-writeback-baseline.md)
110+
- 상류 PR #170 (HWPX Serializer) / `edwardkim/rhwp` `serializer/hwpx/` 모듈

0 commit comments

Comments
 (0)