HWP/HWPX ↔ Markdown 양방향 변환기 (Rust)
한글(HWP/HWPX) 문서를 Markdown으로, Markdown을 HWPX 문서로 변환하는 CLI 도구 + 라이브러리. 모든 HWP 파싱/생성 코드를 자체 구현 — 라이선스 독립성 확보.
- 라이선스 독립: HWP 전용 크레이트(unhwp, hwpforge, hwpers 등) 미사용
- Generic 의존성만 사용: cfb, zip, quick-xml, flate2, comrak 등 범용 크레이트
- IR 기반 아키텍처: 포맷별 reader/writer가 공통 IR을 통해 변환
- 점진적 구현: 텍스트 추출 → 서식 → 테이블 → 이미지 → 수식 순
# 컨테이너/압축 (범용)
cfb = "0.14" # OLE2/CFB — HWP 5.0 컨테이너
zip = "2.0" # ZIP — HWPX 컨테이너
flate2 = "1.0" # Zlib — HWP 스트림 압축
# 파싱 (범용)
quick-xml = "0.37" # XML — HWPX 콘텐츠
byteorder = "1.5" # 바이트 오더 — HWP 레코드
encoding_rs = "0.8" # 문자 인코딩
# Markdown (범용)
comrak = "0.34" # GFM Markdown 파서/렌더러
# CLI/유틸 (범용)
clap = "4"
serde = "1"
thiserror = "2"
anyhow = "1"
tracing = "0.1"HWP 5.0 ──→ hwp::reader (CFB+zlib+record) ──→ IR ──→ md::writer ──→ Markdown
HWPX ──→ hwpx::reader (ZIP+XML) ──→ IR ──→ md::writer ──→ Markdown
Markdown ──→ md::parser (comrak) ──→ IR ──→ hwpx::writer ──→ HWPX
┌─────────────────────────────────────────┐
│ 4-byte Header (Little Endian) │
│ bits 0-9: tag_id (0x3FF mask) │
│ bits 10-19: level (0x3FF mask) │
│ bits 20-31: size (0xFFF mask) │
│ if size == 0xFFF → 4-byte extended │
├─────────────────────────────────────────┤
│ data[size] │
└─────────────────────────────────────────┘
Text: UTF-16LE, control chars 0x0000-0x001F (extended controls use 8 code units)
Document
├── Metadata (title, author, dates, keywords)
├── Sections[]
│ └── Blocks[]
│ ├── Heading (level, inlines)
│ ├── Paragraph (inlines)
│ ├── Table (rows → cells, col_count)
│ ├── CodeBlock (language, code)
│ ├── BlockQuote (nested blocks)
│ ├── List (ordered, start, items)
│ ├── Image (src, alt)
│ ├── HorizontalRule
│ ├── Footnote (id, content)
│ └── Math (display, tex)
└── Assets[] (name, data, mime_type)
src/
├── main.rs — CLI (to-md, to-hwpx, info)
├── lib.rs — 모듈 선언
├── convert.rs — 변환 오케스트레이터
├── error.rs — 에러 타입
├── ir.rs — 중간 표현
├── hwp/
│ ├── mod.rs — HWP 모듈 공개 인터페이스
│ ├── model.rs — HWP 내부 모델 (FileHeader, DocInfo, Section, Paragraph)
│ ├── record.rs — 레코드 파싱 (4-byte header, tag constants)
│ └── reader.rs — CFB → zlib → records → IR 변환
├── hwpx/
│ ├── mod.rs — HWPX 모듈 공개 인터페이스
│ ├── reader.rs — ZIP+XML → IR 변환
│ └── writer.rs — IR → HWPX (ZIP+XML) 생성
└── md/
├── mod.rs — Markdown 모듈 공개 인터페이스
├── writer.rs — IR → Markdown 렌더러
└── parser.rs — Markdown → IR 파서 (comrak)
자체 구현 기반 프로젝트 재구성.
- 1.1 Cargo.toml — HWP 전용 크레이트 제거, generic 의존성만
- 1.2 IR 설계 — Document/Section/Block/Inline 계층 + Asset
- 1.3 HWP reader — CFB + zlib + record parsing + UTF-16LE text
- 1.4 HWPX reader — ZIP + XML 파싱
- 1.5 MD writer — IR → Markdown (GFM 호환)
- 1.6 MD parser — Markdown → IR (comrak 기반)
- 1.7 HWPX writer — IR → HWPX (ZIP+XML 생성)
- 1.8 Convert 오케스트레이터 — to-md, to-hwpx, info 명령
HWP 5.0 파서 정확도 향상.
- 2.1 테이블 파싱 — CTRL_TABLE + LIST_HEADER + 셀 내 paragraphs
- 2.2 이미지/바이너리 — BinData 참조 + 이미지 추출
- 2.3 하이퍼링크 — CTRL 오브젝트에서 URL 추출
- 2.4 각주/미주 — CTRL_FOOTNOTE/CTRL_ENDNOTE 파싱
- 2.5 수식 — EQEDIT 스크립트 → LaTeX 변환
- 2.6 DRM/암호화 감지 — 명확한 에러 메시지
- 2.7 샘플 테스트 — 다양한 HWP 파일로 검증
HWPX XML 파서 정확도 향상.
- 3.1 스타일 상속 — header.xml 스타일 정의 파싱
- 3.2 테이블 — colspan/rowspan 처리
- 3.3 이미지 — BinData 참조 해결
- 3.4 각주/미주 — XML 요소 매핑
- 3.5 수식 — OWPML 수식 요소 → LaTeX
- 3.6 정부 공문서 HWPX 샘플 검증
- 4.1 GFM 호환 검증 — comrak 왕복 테스트
- 4.2 복잡한 테이블 — colspan/rowspan → HTML 폴백
- 4.3 이미지 참조 — 상대경로 / inline base64 옵션
- 4.4 frontmatter — YAML 메타데이터
- 4.5 info 명령 — 문서 통계 (페이지, 글자수, 스타일)
- 5.1 스타일 매핑 — heading/paragraph 스타일 정의
- 5.2 테이블 생성 — Markdown 테이블 → HWPX 표
- 5.3 이미지 삽입 — Asset → HWPX BinData
- 5.4 YAML 스타일 템플릿 — 커스텀 스타일 지원
- 5.5 한글 2022+ 호환 검증
- 5.6 라운드트립 테스트 — HWP→MD→HWPX 왕복 검증
- 6.1 에러 처리 — 사용자 친화적 에러 메시지
- 6.2 배치 변환 — 디렉토리 일괄 변환
- 6.3 CI/CD — GitHub Actions (build + test + clippy)
- 6.4 테스트 커버리지 80%+
- 6.5 crates.io 배포
- 6.6 README 업데이트
| HWP/HWPX 요소 | Markdown 매핑 | 비고 |
|---|---|---|
| 제목 (개요 1~6) | # ~ ###### |
수준 매핑 |
| 본문 | 일반 텍스트 | |
| 굵게/기울임 | **bold** / *italic* |
|
| 밑줄 | <u>text</u> |
HTML 폴백 |
| 취소선 | ~~text~~ |
GFM |
| 위첨자/아래첨자 | <sup>/<sub> |
HTML 폴백 |
| 하이퍼링크 | [text](url) |
|
| 표 | GFM table | colspan → HTML 폴백 |
| 이미지 |  |
파일 추출 |
| 코드 블록 | ```lang ``` |
고정폭 스타일 추론 |
| 인용 | > text |
|
| 순서/비순서 목록 | 1. / - |
중첩 지원 |
| 각주 | [^1] |
GFM footnotes |
| 수식 | $LaTeX$ |
한글 수식 → LaTeX |
| 머리글/바닥글 | <!-- header/footer --> 마커 |
양방향 round-trip |
| 다단 | 단일 단으로 평탄화 |
- HWP DRM (배포용) 문서는 지원하지 않음
- 다단 레이아웃은 단일 단으로 평탄화
- 복잡한 테이블 (colspan/rowspan)은 HTML 폴백
- 머리글/바닥글은
<!-- header/footer -->HTML 코멘트 마커로 round-trip - MD → HWP (바이너리)는 지원하지 않음 — HWPX만 출력
- 한글 수식 → LaTeX 변환은 기본적인 수준만 지원
Sprint 89 완료 (2026-05-30). strikeout text + color #span; H1/H2 순서+레벨 이중 핀; MD→HWPX→MD bold/heading 왕복 안정성.
- P1: 관(subsection) 감지 — 대형 법령 픽스처 확보 시 검토 (BLOCKER 유지) — 이월
- P2: HWPX inline link 통합 테스트 — ✅ multiple links (URL isolation), unsafe URL (writer gate pin)
- P3: 복합 인라인 서식 통합 테스트 — ✅ bold+underline →
<u>**text**</u>, bold+italic+color →<span>***text***</span> - P4: 긴 문서 통합 테스트 — ✅ H1/para/H2/code/para IR 위치 순서 + Markdown ATX 순서
Sprint 90 완료 (2026-05-30). 1527 tests. hyperlink URL isolation + unsafe gate pin; combined inline formatting; complex doc ordering.
- P1: 관(subsection) 감지 — BLOCKER 유지 — 이월
- P2: integration.rs 분할 — ✅
integration_formatting.rs분리 (3418 → 3075 lines, -343 lines) - P3: charPr 조합 테스트 — ✅ strikethrough+underline
<u>~~t~~</u>, bold+strikethrough~~**t**~~, italic+underline<u>*t*</u> - P4: CTRL_RUBY/BinData — 이미 충분히 커버됨, 스킵
Sprint 91 완료 (2026-05-30). 1530 tests. formatting split + 3 combination tests. integration.rs still 3075 lines.
- P1: 관(subsection) 감지 — BLOCKER 유지 — 이월
- P2: integration_hyperlink.rs 분리 — ✅ 4 hyperlink tests 이전 (3075 → 2913 lines)
- P3: 인라인 링크 + 포맷 조합 테스트 — ✅ bold+link
[**t**](url), italic+link[*t*](url), color+link[<span>t</span>](url) - P4: MD→HWPX→MD 리스트 왕복 — ✅ ordered list IR-structural assertion + text presence + ordering
Sprint 92 완료 (2026-05-30). 1534 tests. hyperlink split + formatted link + list roundtrip.
- P1: 관(subsection) 감지 — BLOCKER 유지 — 이월
- P2: integration_footnote.rs 분리 — ✅ 5 footnote/noteRef tests (2913 → 2719 lines)
- P3: 경계 테스트 — ✅ bold+italic+link
[***t***](url), paren URL[t](<url>)angle-bracket - P4: unordered list roundtrip — ✅
Block::List { ordered: false }structural pin
Sprint 93 완료 (2026-05-31). 1537 tests. footnote split + paren URL + unordered list roundtrip.
- P1: 관(subsection) 감지 — BLOCKER 유지 — 이월
- P2: integration_list.rs 분리 — ✅ 5 list tests 이전 (2719 → 2504 lines)
- P3: read_fixture → fixtures/mod.rs — ✅ pub fn 공유, 모든 4개 test file에서 local copy 제거
- P4: Table MD→HWPX→MD roundtrip — ✅ Block::Table 구조 + 2-column separator + Alice<90<Bob 조합
Sprint 94 완료 (2026-05-31). 1538 tests. list split + shared read_fixture + table roundtrip.
- P1: 관(subsection) 감지 — BLOCKER 유지 — 이월
- P2: integration_blocks.rs 분리 — ✅ 4 block tests 이전 (equation×2, HR, blockquote)
- P3: italic/strikethrough roundtrip — ✅ 3-phase 패턴
- P4: code block roundtrip — ✅ blockquote/HR 대체 (lossy encoding 발견)
Sprint 95 완료 (2026-05-31). 1541 tests. blocks split + italic/strikethrough/code roundtrip.
- P1: 관(subsection) 감지 — BLOCKER 유지 — 이월
- P2: integration_image.rs 분리 — ✅ 2 image tests 이전 (Sprint 84 P4)
- P3: roundtrip helpers — ✅
ir_hwpx_roundtrip+any_inline_in_doc(top-level paragraphs 한정) - P4: BlockQuote/HR negative assertion — ✅ 손실 계약 pre+!still 패턴으로 executable화
Sprint 96 완료 (2026-05-31). 1543 tests. image split + roundtrip helpers + lossy contracts.
- P1: 관(subsection) 감지 — BLOCKER 유지 — 이월
- P2: integration_table_blocks.rs 분리 — ✅ colspan + no-lang code block 2개 이전
- P3:
any_block_in_doc→ fixtures/mod.rs — ✅ lossy-contract tests 리팩 (-12 lines) - P4: 헤딩 roundtrip — ✅ H1/H2/H3 hp:styleIDRef numeric → parse_hwpx_style_ref lossless
Sprint 97 완료 (2026-05-31). 1544 tests. table/code split + heading roundtrip. integration.rs 2430 lines.
- P1: 관(subsection) 감지 — BLOCKER 계속 유지
- P2: integration.rs 추가 분할 — multi-section/roundtrip 섹션 분리 (Sprint 88 P4 + 89 P4)
- P3: 인라인 서식 추가 roundtrip — underline MD→HWPX→MD + superscript/subscript roundtrip
- P4: Heading H4-H6 roundtrip + 레벨 clamp 확인 (writer clamps to 1-6; H7→H6)
GPL-3.0-only
- HWP 5.0 스펙 — 한컴 공식
- OWPML (KS X 6101) — HWPX 포맷 설명
- rhwp — Rust HWP 뷰어/에디터 (참조용)
- hwpforge — HWPX 프로그래밍 제어 (참조용)
- hwpConverter — Java HWP↔HWPX 변환 (참조용)