HwpForge
한글 문서(HWP/HWPX)를 프로그래밍으로 제어하는 Rust 라이브러리
HwpForge란?
HwpForge는 한컴 한글의 HWPX 문서(ZIP + XML, KS X 6101)를 Rust로 읽고, 쓰고, 변환할 수 있는 라이브러리입니다.
주요 기능
- HWPX 풀 코덱 — HWPX 파일 디코딩/인코딩 + 무손실 라운드트립
- Markdown 브릿지 — GFM Markdown ↔ HWPX 변환
- YAML 스타일 템플릿 — 재사용 가능한 디자인 토큰 (Figma 패턴)
- 타입 안전 API — 브랜드 인덱스, 타입스테이트 검증, unsafe 코드 0
지원 콘텐츠
| 카테고리 | 요소 |
|---|---|
| 텍스트 | 런, 문자 모양, 문단 모양, 스타일 (한컴 기본 22종) |
| 구조 | 표 (중첩), 이미지, 글상자, 캡션 |
| 레이아웃 | 다단, 페이지 설정, 가로/세로, 여백, 마스터페이지 |
| 머리글/바닥글 | 머리글, 바닥글, 쪽번호 (autoNum) |
| 주석 | 각주, 미주 |
| 도형 | 선, 타원, 다각형, 호, 곡선, 연결선 (채움/회전/화살표) |
| 수식 | HancomEQN 스크립트 |
| 차트 | 18종 차트 (OOXML 호환) |
| 참조 | 책갈피, 상호참조, 필드, 메모, 찾아보기 |
| Markdown | GFM 디코드, 손실/무손실 인코드, YAML 프론트매터 |
누구를 위한 라이브러리인가?
- LLM/AI 에이전트 — 자연어로 한글 문서 자동 생성
- 백엔드 개발자 — 서버에서 한글 문서 프로그래밍 생성
- 자동화 도구 — CI/CD 파이프라인에서 보고서 자동 생성
- 데이터 파이프라인 — HWPX 문서에서 텍스트/표 추출
빠른 맛보기
#![allow(unused)] fn main() { use hwpforge::core::{Document, Draft, Paragraph, Run, Section, PageSettings}; use hwpforge::foundation::{CharShapeIndex, ParaShapeIndex}; use hwpforge::hwpx::{HwpxEncoder, HwpxStyleStore}; use hwpforge::core::ImageStore; // 1. 문서 생성 let mut doc = Document::<Draft>::new(); doc.add_section(Section::with_paragraphs( vec![Paragraph::with_runs( vec![Run::text("안녕하세요, HwpForge!", CharShapeIndex::new(0))], ParaShapeIndex::new(0), )], PageSettings::a4(), )); // 2. 검증 + 인코딩 let validated = doc.validate().unwrap(); let style_store = HwpxStyleStore::with_default_fonts("함초롬바탕"); let image_store = ImageStore::new(); let bytes = HwpxEncoder::encode(&validated, &style_store, &image_store).unwrap(); // 3. 파일 저장 std::fs::write("output.hwpx", &bytes).unwrap(); }
다음 단계
설치
HwpForge는 순수 Rust로 작성된 라이브러리입니다. 별도의 시스템 의존성 없이 Cargo.toml에 추가하는 것만으로 사용할 수 있습니다.
최소 지원 Rust 버전 (MSRV)
Rust 1.88 이상이 필요합니다. 현재 버전을 확인하려면:
rustc --version
버전이 낮다면 rustup으로 업데이트합니다:
rustup update stable
의존성 추가
Cargo.toml의 [dependencies] 섹션에 추가합니다:
[dependencies]
hwpforge = "0.1"
기본 설치에는 HWPX 인코더/디코더가 포함됩니다.
Feature Flags
HwpForge는 필요한 기능만 선택적으로 활성화할 수 있습니다.
| Feature | 기본 포함 | 설명 |
|---|---|---|
hwpx | 예 | HWPX 인코더/디코더 (ZIP + XML, KS X 6101) |
md | 아니오 | Markdown(GFM) ↔ HWPX 변환 |
full | 아니오 | 모든 기능 활성화 |
HWPX만 사용 (기본)
[dependencies]
hwpforge = "0.1"
Markdown 변환 포함
[dependencies]
hwpforge = { version = "0.1", features = ["md"] }
모든 기능 활성화
[dependencies]
hwpforge = { version = "0.1", features = ["full"] }
빌드 확인
의존성을 추가한 후 빌드가 정상적으로 되는지 확인합니다:
cargo build
다음과 같이 컴파일이 성공하면 설치가 완료된 것입니다:
Compiling hwpforge v0.1.0
Finished `dev` profile [unoptimized + debuginfo] target(s) in ...
다음 단계
설치가 완료되었습니다. 빠른 시작으로 이동하여 첫 번째 HWPX 문서를 생성해 보세요.
빠른 시작
이 페이지에서는 HwpForge의 세 가지 핵심 사용 패턴을 코드 예제와 함께 설명합니다.
예제 1: 텍스트 문서 생성 후 HWPX로 저장
가장 기본적인 사용 패턴입니다. 문서 구조를 직접 조립하고 HWPX 파일로 출력합니다.
use hwpforge::core::{Document, Draft, ImageStore, PageSettings, Paragraph, Run, Section}; use hwpforge::foundation::{CharShapeIndex, ParaShapeIndex}; use hwpforge::hwpx::{HwpxEncoder, HwpxStyleStore}; fn main() -> anyhow::Result<()> { // 1. Draft 상태의 문서 생성 let mut doc = Document::<Draft>::new(); // 2. 텍스트 Run 구성 — CharShapeIndex(0)은 기본 글자 스타일을 참조 let run = Run::text("안녕하세요, HwpForge입니다!", CharShapeIndex::new(0)); // 3. 문단 생성 — ParaShapeIndex(0)은 기본 문단 스타일을 참조 let paragraph = Paragraph::with_runs(vec![run], ParaShapeIndex::new(0)); // 4. 섹션(= 쪽 단위 컨테이너)에 문단 추가, A4 용지 설정 let section = Section::with_paragraphs(vec![paragraph], PageSettings::a4()); doc.add_section(section); // 5. 유효성 검증 — Draft → Validated 상태 전이 (타입스테이트) let validated = doc.validate()?; // 6. 스타일 스토어: 한컴 Modern(22종) 기본 스타일 사용 let style_store = HwpxStyleStore::with_default_fonts("함초롬바탕"); // 7. 이미지 스토어: 이미지가 없으므로 빈 스토어 사용 let image_store = ImageStore::new(); // 8. HWPX 바이트 인코딩 후 파일 저장 let bytes = HwpxEncoder::encode(&validated, &style_store, &image_store)?; std::fs::write("output.hwpx", &bytes)?; println!("output.hwpx 저장 완료 ({} bytes)", bytes.len()); Ok(()) }
참고:
CharShapeIndex::new(0)과ParaShapeIndex::new(0)은HwpxStyleStore::with_default_fonts()이 제공하는 기본 스타일(본문)을 가리킵니다. 커스텀 스타일을 사용하려면 스타일 템플릿 문서를 참고하세요.
예제 2: HWPX 파일 디코딩
기존 HWPX 파일을 읽어서 Core 문서 모델로 변환합니다.
use hwpforge::hwpx::HwpxDecoder; use hwpforge::core::run::RunContent; fn main() -> anyhow::Result<()> { // 파일 경로를 받아 HWPX를 디코딩 let result = HwpxDecoder::decode_file("input.hwpx")?; let doc = &result.document; // 섹션 수 출력 println!("섹션 수: {}", doc.sections().len()); // 각 섹션의 문단과 텍스트 출력 for (sec_idx, section) in doc.sections().iter().enumerate() { println!("--- 섹션 {} ---", sec_idx + 1); for paragraph in §ion.paragraphs { for run in ¶graph.runs { if let RunContent::Text(ref text) = run.content { print!("{}", text); } } println!(); // 문단 끝 줄바꿈 } } Ok(()) }
HwpxDecoder::decode_file은 경로를 받아 ZIP을 열고 XML을 파싱합니다.
반환값에는 document(문서 구조)와 style_store(글꼴/문단 스타일)가 포함됩니다.
예제 3: Markdown → HWPX 변환
GFM(GitHub Flavored Markdown) 텍스트를 HWPX 파일로 변환합니다.
features = ["md"] 또는 features = ["full"]이 필요합니다.
use hwpforge::core::ImageStore; use hwpforge::hwpx::{HwpxEncoder, HwpxStyleStore}; use hwpforge::md::MdDecoder; fn main() -> anyhow::Result<()> { // 1. GFM Markdown 텍스트 (YAML 프론트매터 지원) let markdown = r#"--- title: 보고서 제목 author: 홍길동 date: 2026-03-06 --- 1장. 서론 본 보고서는 HwpForge를 이용한 **자동 문서 생성** 예시입니다. # 1.1 배경 - 항목 A - 항목 B - 항목 C # 1.2 결론 > HwpForge는 LLM 에이전트가 한글 문서를 생성할 때 사용할 수 있습니다. "#; // 2. Markdown → Core 문서 모델 변환 // MdDecoder::decode_with_default는 document + style_registry(스타일 매핑)를 반환 let md_doc = MdDecoder::decode_with_default(markdown)?; // 3. Draft → Validated 상태 전이 let validated = md_doc.document.validate()?; // 4. Markdown 헤딩(H1-H6)이 한컴 개요 1-6 스타일로 자동 매핑된 스타일 스토어 생성 let style_store = HwpxStyleStore::from_registry(&md_doc.style_registry); // 5. 이미지 없음 let image_store = ImageStore::new(); // 6. HWPX 인코딩 후 저장 let bytes = HwpxEncoder::encode(&validated, &style_store, &image_store)?; std::fs::write("report.hwpx", &bytes)?; println!("report.hwpx 저장 완료"); Ok(()) }
Markdown 변환 시 자동으로 처리되는 항목:
| Markdown 요소 | 변환 결과 |
|---|---|
# H1 ~ ###### H6 | 한컴 개요 1 ~ 6 스타일 |
**굵게** | 글자 진하게 |
*기울임* | 글자 기울임 |
`코드` | 고정폭 글꼴 |
> 인용문 | 들여쓰기 문단 |
- 목록 | 글머리 기호 목록 |
| YAML 프론트매터 | 문서 메타데이터 |
다음 단계
아키텍처 개요
HwpForge는 대장간(Forge) 메타포를 기반으로 설계된 계층형 크레이트 구조를 갖습니다. 각 계층은 명확한 역할을 가지며, 상위 계층은 하위 계층에만 의존합니다.
Forge 메타포
| 계층 | 역할 | 비유 |
|---|---|---|
| Foundation (기반) | 원시 타입, 단위, 인덱스 | 쇠못과 금속 소재 |
| Core (핵심) | 형식 독립 문서 모델 | 도면 위의 설계도 |
| Blueprint (청사진) | YAML 스타일 템플릿 | 피그마 디자인 토큰 |
| Smithy (대장간) | 형식별 인코더/디코더 | 용광로와 망치 |
| Bindings (바인딩) | Python, CLI 인터페이스 | 완성된 제품 포장 |
크레이트 의존성 그래프
graph TD
F[hwpforge-foundation<br/>원시 타입] --> C[hwpforge-core<br/>문서 모델]
C --> B[hwpforge-blueprint<br/>스타일 템플릿]
B --> SH[hwpforge-smithy-hwpx<br/>HWPX 코덱]
B --> SM[hwpforge-smithy-md<br/>Markdown 코덱]
B --> S5[hwpforge-smithy-hwp5<br/>HWP5 디코더 (예정)]
SH --> U[hwpforge<br/>umbrella crate]
SM --> U
S5 --> U
U --> PY[hwpforge-bindings-py<br/>Python (예정)]
U --> CLI[hwpforge-bindings-cli<br/>CLI (예정)]
규칙: 의존성은 위에서 아래로만 흐릅니다.
foundation을 수정하면 모든 크레이트가 재빌드됩니다. 따라서foundation은 최소한으로 유지합니다.
핵심 원칙: 구조와 스타일의 분리
HwpForge는 HTML + CSS의 관계처럼 문서 구조와 스타일 정의를 완전히 분리합니다.
Core (구조) Blueprint (스타일)
───────────── ──────────────────
Paragraph font: "맑은 고딕"
style_id: 2 ──▶ size: 10pt
runs: [...] color: #000000
- Core는 스타일 ID(인덱스)만 보유합니다. 실제 글꼴 이름이나 크기를 모릅니다.
- Blueprint는 스타일 정의를 YAML 템플릿으로 관리합니다.
- Smithy 컴파일러가 Core + Blueprint를 조합해 최종 형식을 생성합니다.
이 구조 덕분에 하나의 YAML 템플릿을 여러 문서에 재사용하거나, 동일한 문서를 HWPX/Markdown 등 다른 형식으로 내보낼 수 있습니다.
타입스테이트 패턴: Document → Document
Document는 컴파일 타임에 상태를 추적하는 타입스테이트 패턴을 사용합니다.
#![allow(unused)] fn main() { use hwpforge::core::{Document, Draft}; // Draft 상태: 편집 가능, 저장 불가 let mut doc = Document::<Draft>::new(); doc.add_section(/* ... */); // validate()를 호출해야만 Validated 상태로 전이 let validated = doc.validate().unwrap(); // Validated 상태에서만 인코딩 가능 // doc.validate()를 건너뛰면 컴파일 에러 발생 let bytes = hwpforge::hwpx::HwpxEncoder::encode( &validated, &hwpforge::hwpx::HwpxStyleStore::with_default_fonts("함초롬바탕"), &hwpforge::core::ImageStore::new(), ).unwrap(); }
잘못된 상태에서 저장을 시도하면 런타임 에러가 아닌 컴파일 에러가 발생합니다.
각 크레이트 설명
hwpforge-foundation
의존성이 없는 루트 크레이트입니다. 모든 크레이트가 공유하는 원시 타입을 정의합니다.
HwpUnit: 정수 기반 HWP 단위 (1pt = 100 HWPUNIT). 부동소수점 오차 없음Color: BGR 바이트 순서 색상 타입.Color::from_rgb(r, g, b)로 생성Index<T>: 팬텀 타입을 이용한 브랜드 인덱스.CharShapeIndex와ParaShapeIndex를 혼용하면 컴파일 에러
hwpforge-core
형식에 독립적인 문서 모델입니다. 한글/Markdown/PDF 어디에도 종속되지 않습니다.
Document<S>,Section,Paragraph,Run— 기본 문서 구조Table,Control,Shape— 복합 요소PageSettings— 용지 크기, 여백, 가로/세로 방향
hwpforge-blueprint
YAML로 작성하는 스타일 템플릿 시스템입니다. 피그마의 디자인 토큰 개념과 유사합니다.
- 상속(
extends)과 병합(merge)을 지원하는PartialCharShape/CharShape두 타입 구조 StyleRegistry— 파싱 후 인덱스를 할당한 최종 스타일 집합
hwpforge-smithy-hwpx
HWPX ↔ Core 변환을 담당하는 핵심 코덱입니다. KS X 6101(OWPML) 국가 표준을 구현합니다.
HwpxDecoder— ZIP + XML 파싱 → Core 문서 모델HwpxEncoder— Core 문서 모델 → ZIP + XML 바이트HwpxStyleStore— 한컴 기본 스타일 22종(Modern) 내장
hwpforge-smithy-md
GFM(GitHub Flavored Markdown) ↔ Core 변환을 담당합니다.
MdDecoder— Markdown + YAML 프론트매터 → CoreMdEncoder— Core → Markdown (손실/무손실 모드)
hwpforge (umbrella crate)
모든 공개 크레이트를 재내보내기(re-export)하는 진입점 크레이트입니다. 사용자는 이 크레이트 하나만 의존성에 추가하면 됩니다.
다음 단계
HWPX 인코딩/디코딩
HWPX 포맷 소개
HWPX는 한글과컴퓨터의 공개 문서 표준(KS X 6101, OWPML)입니다. 내부 구조는 ZIP 컨테이너 안에 XML 파일들이 담긴 형태로, Microsoft DOCX와 유사합니다.
주요 구성 파일:
mimetype— 포맷 식별자Contents/header.xml— 스타일 정의 (폰트, 문단 모양, 글자 모양)Contents/section0.xml,section1.xml, … — 본문 내용BinData/— 이미지 등 바이너리 파일들Chart/— 차트 XML (OOXMLxmlns:c형식)
hwpforge-smithy-hwpx 크레이트가 이 포맷의 인코드/디코드를 담당합니다.
디코딩: HWPX 파일 읽기
HwpxDecoder::decode_file()로 .hwpx 파일을 HwpxDocument로 읽습니다.
#![allow(unused)] fn main() { use hwpforge_smithy_hwpx::HwpxDecoder; let result = HwpxDecoder::decode_file("document.hwpx").unwrap(); // 섹션 수 확인 println!("섹션 수: {}", result.document.sections().len()); // 첫 번째 섹션의 문단 수 let section = &result.document.sections()[0]; println!("문단 수: {}", section.paragraphs.len()); }
HwpxDocument 결과 구조
HwpxDecoder::decode_file()은 HwpxDocument를 반환합니다. 세 가지 필드로 구성됩니다.
#![allow(unused)] fn main() { use hwpforge_smithy_hwpx::{HwpxDecoder, HwpxDocument}; let HwpxDocument { document, style_store, image_store } = HwpxDecoder::decode_file("document.hwpx").unwrap(); // document: Document<Draft> — 문서 DOM (섹션/문단/런 트리) // style_store: HwpxStyleStore — 폰트, 글자 모양, 문단 모양, 스타일 // image_store: ImageStore — 임베드된 이미지 바이너리 데이터 }
| 필드 | 타입 | 설명 |
|---|---|---|
document | Document<Draft> | 섹션, 문단, 런 트리 |
style_store | HwpxStyleStore | 폰트/글자모양/문단모양/스타일 |
image_store | ImageStore | 이미지 바이너리 저장소 |
인코딩: Core → HWPX
HwpxEncoder::encode()로 Document<Validated>를 HWPX 바이트 벡터로 직렬화합니다.
#![allow(unused)] fn main() { use hwpforge_smithy_hwpx::{HwpxDecoder, HwpxEncoder, HwpxStyleStore}; use hwpforge_core::{Document, Section, Paragraph, PageSettings}; use hwpforge_core::run::Run; use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex}; // 새 문서 생성 let mut doc = Document::new(); doc.add_section(Section::with_paragraphs( vec![Paragraph::with_runs( vec![Run::text("안녕하세요, HwpForge!", CharShapeIndex::new(0))], ParaShapeIndex::new(0), )], PageSettings::a4(), )); let validated = doc.validate().unwrap(); let style_store = HwpxStyleStore::with_default_fonts("함초롬바탕"); let image_store = Default::default(); let bytes = HwpxEncoder::encode(&validated, &style_store, &image_store).unwrap(); std::fs::write("output.hwpx", &bytes).unwrap(); }
HwpxStyleStore 생성 방법
with_default_fonts() — 간단한 기본 스타일
단일 글꼴 이름으로 빠르게 스타일 스토어를 생성합니다. 가장 간단한 방법입니다.
#![allow(unused)] fn main() { use hwpforge::hwpx::HwpxStyleStore; let style_store = HwpxStyleStore::with_default_fonts("함초롬바탕"); }
from_registry() — Blueprint 템플릿에서 변환
커스텀 YAML 스타일 템플릿을 적용할 때 사용합니다. 자세한 내용은 스타일 템플릿 참조.
#![allow(unused)] fn main() { use hwpforge::blueprint::builtins::builtin_default; use hwpforge::blueprint::registry::StyleRegistry; use hwpforge::hwpx::HwpxStyleStore; let template = builtin_default().unwrap(); let registry = StyleRegistry::from_template(&template).unwrap(); let style_store = HwpxStyleStore::from_registry(®istry); }
라운드트립 예제 (decode → modify → encode)
기존 HWPX 파일을 읽어서 수정한 뒤 다시 저장하는 전형적인 패턴입니다.
#![allow(unused)] fn main() { use hwpforge_smithy_hwpx::{HwpxDecoder, HwpxEncoder}; use hwpforge_core::run::Run; use hwpforge_core::paragraph::Paragraph; use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex}; // 1. 기존 파일 디코딩 let mut result = HwpxDecoder::decode_file("original.hwpx").unwrap(); // 2. 문서 수정: 새 문단 추가 let new_para = Paragraph::with_runs( vec![Run::text("추가된 문단입니다.", CharShapeIndex::new(0))], ParaShapeIndex::new(0), ); // Draft 상태이므로 sections 직접 접근 가능 result.document.sections_mut()[0].paragraphs.push(new_para); // 3. 검증 후 인코딩 let validated = result.document.validate().unwrap(); let bytes = HwpxEncoder::encode( &validated, &result.style_store, &result.image_store, ).unwrap(); std::fs::write("modified.hwpx", &bytes).unwrap(); }
오류 처리
모든 함수는 HwpxResult<T>를 반환합니다. HwpxError는 HwpxErrorCode와 메시지를 포함합니다.
#![allow(unused)] fn main() { use hwpforge::hwpx::HwpxDecoder; match HwpxDecoder::decode_file("missing.hwpx") { Ok(_result) => println!("디코딩 성공"), Err(e) => eprintln!("디코딩 실패: {e}"), } }
Markdown에서 HWPX로
HwpForge는 Markdown을 HWPX로 변환하는 완전한 파이프라인을 제공합니다. LLM이 Markdown을 생성하면 HwpForge가 이를 한글 문서로 자동 변환합니다.
MD → Core → HWPX 파이프라인
Markdown 문자열
|
v (MdDecoder::decode)
Document<Draft> + StyleRegistry
|
v (doc.validate())
Document<Validated>
|
v (HwpxEncoder::encode)
HWPX 바이트 → .hwpx 파일
각 단계는 독립적이므로, 중간 Core DOM을 직접 조작하거나 검사할 수 있습니다.
MdDecoder::decode() 사용법
#![allow(unused)] fn main() { use hwpforge::md::{MdDecoder, MdDocument}; let markdown = r#" --- title: 사업 제안서 author: 홍길동 date: 2026-03-06 --- 개요 본 제안서는 신규 사업 기회를 설명합니다. # 배경 시장 분석에 따르면 성장 가능성이 높습니다. "#; let MdDocument { document, style_registry } = MdDecoder::decode_with_default(markdown).unwrap(); println!("섹션 수: {}", document.sections().len()); }
MdDocument에는 document: Document<Draft>와 style_registry: StyleRegistry가 포함됩니다.
YAML Frontmatter
Markdown 파일 상단에 --- 블록으로 문서 메타데이터를 지정합니다.
---
title: 문서 제목 # Metadata.title
author: 작성자 이름 # Metadata.author
date: 2026-03-06 # Metadata.date (ISO 8601)
template: government # 사용할 스타일 템플릿 이름 (옵션)
---
지원 필드:
| 필드 | 설명 |
|---|---|
title | 문서 제목 |
author | 작성자 |
date | 작성일 (ISO 8601) |
template | 스타일 템플릿 이름 |
Frontmatter 없이도 디코딩이 가능하며, 메타데이터 필드는 빈 값으로 처리됩니다.
섹션 마커
<!-- hwpforge:section --> 주석으로 HWPX 섹션을 분리합니다. 한 Markdown 파일에서 여러 섹션(페이지 설정이 다른 구역)을 만들 때 유용합니다.
# 1장 개요
첫 번째 섹션 내용.
<!-- hwpforge:section -->
# 2장 본론
두 번째 섹션 — 다른 페이지 설정 가능.
H1-H6 → 개요 1-6 자동 매핑
Markdown 헤딩은 한글의 개요 스타일로 자동 변환됩니다.
| Markdown | 한글 스타일 |
|---|---|
# H1 | 개요 1 (style ID 2) |
## H2 | 개요 2 (style ID 3) |
### H3 | 개요 3 (style ID 4) |
#### H4 | 개요 4 (style ID 5) |
##### H5 | 개요 5 (style ID 6) |
###### H6 | 개요 6 (style ID 7) |
| 일반 문단 | 본문 (style ID 0) |
MdEncoder — Core → Markdown
반대 방향(HWPX → Markdown) 변환도 지원합니다. 두 가지 모드가 있습니다.
#![allow(unused)] fn main() { use hwpforge::md::MdEncoder; use hwpforge::hwpx::HwpxDecoder; let result = HwpxDecoder::decode_file("document.hwpx").unwrap(); let validated = result.document.validate().unwrap(); // Lossy 모드: 읽기 좋은 GFM (표, 이미지 등 일부 정보 손실) let gfm = MdEncoder::encode_lossy(&validated).unwrap(); // Lossless 모드: YAML frontmatter + HTML-like 마크업 (정보 보존) let lossless = MdEncoder::encode_lossless(&validated).unwrap(); }
| 모드 | 특징 | 용도 |
|---|---|---|
encode_lossy | 읽기 좋은 GFM | 사람이 읽는 문서 미리보기 |
encode_lossless | 구조 완전 보존 | 라운드트립, 백업 |
전체 파이프라인 예제 (MD string → HWPX file)
use hwpforge::md::{MdDecoder, MdDocument}; use hwpforge::hwpx::{HwpxEncoder, HwpxStyleStore}; fn markdown_to_hwpx(markdown: &str, output_path: &str) { // 1. Markdown 파싱 → Core DOM let MdDocument { document, .. } = MdDecoder::decode_with_default(markdown).unwrap(); // 2. 문서 검증 let validated = document.validate().unwrap(); // 3. 한컴 기본 스타일 적용 후 HWPX 인코딩 let style_store = HwpxStyleStore::with_default_fonts("함초롬바탕"); let image_store = Default::default(); let bytes = HwpxEncoder::encode(&validated, &style_store, &image_store).unwrap(); // 4. 파일 저장 std::fs::write(output_path, &bytes).unwrap(); println!("저장 완료: {output_path}"); } fn main() { let md = r#" --- title: AI 활용 정책 제안서 author: 정책팀 date: 2026-03-06 --- 제안 배경 인공지능 기술의 급속한 발전에 대응하여 정책 수립이 필요합니다. # 현황 분석 국내외 AI 활용 사례를 분석하였습니다. # 정책 방향 단계적 도입과 윤리적 기준 마련을 제안합니다. "#; markdown_to_hwpx(md, "proposal.hwpx"); }
스타일 템플릿 (YAML)
Blueprint 개념: 구조와 스타일 분리
HwpForge는 HTML+CSS와 동일한 철학으로 **구조(Core)**와 **스타일(Blueprint)**을 분리합니다.
Core (Document, Section, Paragraph, Run)
= HTML — "무엇이 있는가"
Blueprint (Template, StyleRegistry, CharShape, ParaShape)
= CSS — "어떻게 보이는가"
Core의 문단과 런은 스타일 인덱스(ParaShapeIndex, CharShapeIndex)만 참조합니다. 실제 폰트 이름이나 크기는 Blueprint에 정의됩니다. 덕분에 동일한 문서 구조에 다른 템플릿을 적용해 전혀 다른 외관의 HWPX를 생성할 수 있습니다.
Template YAML 구조
meta:
name: my-template
version: "1.0"
description: "커스텀 스타일 템플릿"
styles:
body:
font: "한컴바탕"
size: 10pt
line_spacing: 160%
alignment: justify
heading1:
inherits: body # body에서 상속
font: "한컴고딕"
size: 16pt
bold: true
heading2:
inherits: heading1
size: 14pt
상속 (Inheritance)
inherits 키로 다른 스타일을 상속받습니다. 상속 체인은 DFS로 해결되며, 자식 스타일의 값이 부모를 덮어씁니다. Option 필드(PartialCharShape, PartialParaShape)를 병합하는 two-type 패턴으로 구현됩니다.
StyleRegistry: from_template() 사용법
#![allow(unused)] fn main() { use hwpforge_blueprint::template::Template; use hwpforge_blueprint::registry::StyleRegistry; let yaml = r#" meta: name: custom version: "1.0" styles: body: font: "나눔명조" size: 11pt "#; // YAML → Template → StyleRegistry let template = Template::from_yaml(yaml).unwrap(); let registry = StyleRegistry::from_template(&template).unwrap(); // 인덱스 기반 접근 (브랜드 타입으로 혼용 방지) let body_entry = registry.get_style("body").unwrap(); let char_shape = registry.char_shape(body_entry.char_shape_id).unwrap(); println!("폰트: {}", char_shape.font); // "나눔명조" println!("크기: {:?}", char_shape.size); // HwpUnit }
내장 템플릿: builtin_default()
별도 YAML 없이 즉시 사용 가능한 기본 템플릿입니다.
#![allow(unused)] fn main() { use hwpforge_blueprint::builtins::builtin_default; use hwpforge_blueprint::registry::StyleRegistry; let template = builtin_default().unwrap(); assert_eq!(template.meta.name, "default"); let registry = StyleRegistry::from_template(&template).unwrap(); let body = registry.get_style("body").unwrap(); let cs = registry.char_shape(body.char_shape_id).unwrap(); assert_eq!(cs.font, "한컴바탕"); }
HwpxStyleStore 변환: from_registry()
Blueprint의 StyleRegistry를 HWPX 인코더가 사용하는 HwpxStyleStore로 변환합니다.
#![allow(unused)] fn main() { use hwpforge_blueprint::builtins::builtin_default; use hwpforge_blueprint::registry::StyleRegistry; use hwpforge_smithy_hwpx::HwpxStyleStore; let template = builtin_default().unwrap(); let registry = StyleRegistry::from_template(&template).unwrap(); // Blueprint StyleRegistry → HWPX 전용 스타일 저장소 let style_store = HwpxStyleStore::from_registry(®istry); }
한컴 스타일셋: Classic / Modern / Latest
한글 프로그램은 버전에 따라 다른 기본 스타일 구성을 사용합니다.
| 스타일셋 | 스타일 수 | 설명 |
|---|---|---|
Classic | 18개 | 한글 구버전 호환 |
Modern | 22개 | 기본값 (한글 2018~) |
Latest | 23개 | 최신 버전 |
Modern은 개요 8/9/10을 스타일 ID 9-11에 삽입하므로 인덱스가 Classic과 다릅니다.
#![allow(unused)] fn main() { use hwpforge_smithy_hwpx::{HwpxStyleStore, HancomStyleSet}; // 기본값 (간단한 방법) let modern = HwpxStyleStore::with_default_fonts("함초롬바탕"); // 특정 스타일셋 지정 // from_registry_with()로 커스텀 레지스트리 + 스타일셋 조합 가능 }
예제: 커스텀 스타일로 문서 생성
#![allow(unused)] fn main() { use hwpforge_blueprint::template::Template; use hwpforge_blueprint::registry::StyleRegistry; use hwpforge_smithy_hwpx::{HwpxEncoder, HwpxStyleStore}; use hwpforge_core::{Document, Section, Paragraph, PageSettings}; use hwpforge_core::run::Run; use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex}; let yaml = r#" meta: name: report version: "1.0" styles: body: font: "맑은 고딕" size: 10pt line_spacing: 150% title: inherits: body font: "맑은 고딕" size: 20pt bold: true alignment: center "#; // 스타일 빌드 let template = Template::from_yaml(yaml).unwrap(); let registry = StyleRegistry::from_template(&template).unwrap(); let style_store = HwpxStyleStore::from_registry(®istry); // 문서 구성 (스타일 인덱스는 레지스트리에서 조회) let mut doc = Document::new(); doc.add_section(Section::with_paragraphs( vec![ // 제목 문단 (ParaShapeIndex 0 = title) Paragraph::with_runs( vec![Run::text("분기 보고서", CharShapeIndex::new(0))], ParaShapeIndex::new(0), ), // 본문 문단 (ParaShapeIndex 1 = body) Paragraph::with_runs( vec![Run::text("1분기 실적은 목표를 초과 달성했습니다.", CharShapeIndex::new(1))], ParaShapeIndex::new(1), ), ], PageSettings::a4(), )); let validated = doc.validate().unwrap(); let image_store = Default::default(); let bytes = HwpxEncoder::encode(&validated, &style_store, &image_store).unwrap(); std::fs::write("report.hwpx", &bytes).unwrap(); }
차트 생성
HwpForge는 OOXML 차트 형식(xmlns:c)을 사용해 18종의 차트를 HWPX 문서에 삽입할 수 있습니다.
지원 차트 종류 (18종)
| 변형 | 설명 |
|---|---|
Bar | 가로 막대 차트 |
Column | 세로 막대 차트 |
Bar3D / Column3D | 3D 막대/세로 막대 |
Line / Line3D | 꺾은선 / 3D 꺾은선 |
Pie / Pie3D | 원형 / 3D 원형 |
Doughnut | 도넛 차트 |
OfPie | 원형-of-원형 / 막대-of-원형 |
Area / Area3D | 영역 / 3D 영역 |
Scatter | 분산형 (XY) |
Bubble | 버블 차트 |
Radar | 방사형 차트 |
Surface / Surface3D | 표면 / 3D 표면 |
Stock | 주식 차트 (HLC/OHLC/VHLC/VOHLC) |
Control::Chart 생성 방법
차트는 Control::Chart 변형으로 표현됩니다. Run::control()로 런에 삽입하고, 그 런을 문단에 넣습니다.
#![allow(unused)] fn main() { use hwpforge_core::control::Control; use hwpforge_core::chart::{ChartType, ChartData, ChartGrouping, LegendPosition}; use hwpforge_foundation::HwpUnit; let chart = Control::Chart { chart_type: ChartType::Column, data: ChartData::category( &["1월", "2월", "3월", "4월"], &[("매출", &[1200.0, 1500.0, 1350.0, 1800.0])], ), title: Some("월별 매출".to_string()), legend: LegendPosition::Bottom, grouping: ChartGrouping::Clustered, width: HwpUnit::from_mm(120.0).unwrap(), height: HwpUnit::from_mm(80.0).unwrap(), }; }
ChartData: Category vs Xy 방식
Category 방식 (막대, 꺾은선, 원형, 영역, 방사형 등)
카테고리 레이블(X축)과 여러 시리즈로 구성됩니다. 대부분의 차트 종류에 사용합니다.
#![allow(unused)] fn main() { use hwpforge_core::chart::ChartData; // 편의 생성자: cats 슬라이스 + (이름, 값 슬라이스) 튜플 배열 let data = ChartData::category( &["1분기", "2분기", "3분기", "4분기"], &[ ("매출액", &[4200.0, 5100.0, 4800.0, 6200.0]), ("비용", &[3100.0, 3400.0, 3200.0, 3900.0]), ], ); }
Xy 방식 (분산형, 버블)
X값과 Y값 쌍으로 구성됩니다. 두 변수 간의 관계를 나타낼 때 사용합니다.
#![allow(unused)] fn main() { use hwpforge_core::chart::ChartData; // (이름, x값 슬라이스, y값 슬라이스) 튜플 배열 let data = ChartData::xy(&[ ("데이터셋 A", &[1.0, 2.0, 3.0, 4.0], &[2.1, 3.9, 6.2, 7.8]), ("데이터셋 B", &[1.0, 2.0, 3.0, 4.0], &[1.5, 3.0, 5.0, 6.5]), ]); }
ChartSeries, XySeries 구조
시리즈를 직접 구성할 때는 구조체를 사용합니다.
#![allow(unused)] fn main() { use hwpforge_core::chart::{ChartData, ChartSeries, XySeries}; // Category용 시리즈 let series = ChartSeries { name: "판매량".to_string(), values: vec![100.0, 150.0, 200.0], }; let data = ChartData::Category { categories: vec!["A".to_string(), "B".to_string(), "C".to_string()], series: vec![series], }; // XY용 시리즈 let xy_series = XySeries { name: "측정값".to_string(), x_values: vec![0.0, 1.0, 2.0], y_values: vec![0.0, 1.0, 4.0], }; }
차트를 문단에 삽입하는 패턴
차트 Control을 Run::control()로 감싼 뒤, Paragraph::with_runs()에 포함시킵니다.
#![allow(unused)] fn main() { use hwpforge_core::control::Control; use hwpforge_core::chart::{ChartType, ChartData, ChartGrouping, LegendPosition}; use hwpforge_core::run::Run; use hwpforge_core::paragraph::Paragraph; use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex, HwpUnit}; let chart_control = Control::Chart { chart_type: ChartType::Column, data: ChartData::category( &["A", "B", "C"], &[("값", &[10.0, 20.0, 30.0])], ), title: None, legend: LegendPosition::Right, grouping: ChartGrouping::Clustered, width: HwpUnit::from_mm(100.0).unwrap(), height: HwpUnit::from_mm(70.0).unwrap(), }; let para = Paragraph::with_runs( vec![Run::control(chart_control, CharShapeIndex::new(0))], ParaShapeIndex::new(0), ); }
예제: 막대 차트 (Column)
#![allow(unused)] fn main() { use hwpforge_core::control::Control; use hwpforge_core::chart::{ChartType, ChartData, ChartGrouping, LegendPosition}; use hwpforge_core::run::Run; use hwpforge_core::paragraph::Paragraph; use hwpforge_core::{Document, Section, PageSettings}; use hwpforge_smithy_hwpx::{HwpxEncoder, HwpxStyleStore}; use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex, HwpUnit}; let data = ChartData::category( &["2022", "2023", "2024", "2025"], &[ ("국내 매출", &[3200.0, 4100.0, 5300.0, 6800.0]), ("해외 매출", &[1100.0, 1800.0, 2700.0, 3900.0]), ], ); let chart = Control::Chart { chart_type: ChartType::Column, data, title: Some("연도별 매출 현황 (단위: 백만원)".to_string()), legend: LegendPosition::Bottom, grouping: ChartGrouping::Clustered, width: HwpUnit::from_mm(140.0).unwrap(), height: HwpUnit::from_mm(90.0).unwrap(), }; let mut doc = Document::new(); doc.add_section(Section::with_paragraphs( vec![Paragraph::with_runs( vec![Run::control(chart, CharShapeIndex::new(0))], ParaShapeIndex::new(0), )], PageSettings::a4(), )); let validated = doc.validate().unwrap(); let bytes = HwpxEncoder::encode( &validated, &HwpxStyleStore::with_default_fonts("함초롬바탕"), &Default::default(), ).unwrap(); std::fs::write("bar_chart.hwpx", &bytes).unwrap(); }
예제: 원형 차트 (Pie)
#![allow(unused)] fn main() { use hwpforge_core::control::Control; use hwpforge_core::chart::{ChartType, ChartData, ChartGrouping, LegendPosition}; use hwpforge_foundation::{HwpUnit}; // 원형 차트는 단일 시리즈 사용 let chart = Control::Chart { chart_type: ChartType::Pie, data: ChartData::category( &["서울", "경기", "부산", "기타"], &[("비율", &[38.5, 25.2, 12.8, 23.5])], ), title: Some("지역별 매출 비중".to_string()), legend: LegendPosition::Right, grouping: ChartGrouping::Standard, // Pie는 Standard 사용 width: HwpUnit::from_mm(100.0).unwrap(), height: HwpUnit::from_mm(80.0).unwrap(), }; }
주의: 차트 XML은 ZIP에 포함되지만
content.hpf매니페스트에는 등록하지 않습니다. 매니페스트에 등록하면 한글이 크래시합니다.
HWPX 포맷 주의사항 (Gotchas)
HWPX 포맷은 KS X 6101 스펙과 한글 실제 동작 사이에 차이가 있습니다. 이 페이지는 HwpForge를 사용하거나 HWPX 파일을 직접 생성할 때 반드시 알아야 할 30가지 함정을 정리한 참고 문서입니다. 각 항목은 실제 개발 과정에서 발견된 버그와 충돌 사례를 기반으로 합니다.
색상/단위 (Color & Unit)
HWP 포맷은 BGR(Blue-Green-Red) 바이트 순서를 사용합니다. 원시 16진수 값을 그대로 쓰면 색이 반전됩니다.
#![allow(unused)] fn main() { // ❌ WRONG — 0xFF0000은 HWP에서 파란색! let red_bgr = 0xFF0000; // ✅ CORRECT — from_rgb() 생성자를 항상 사용 Color::from_rgb(255, 0, 0) // 내부적으로 0x0000FF로 저장 }
부동소수점 정밀도 오류를 피하기 위해 HwpUnit은 정수 기반입니다. 1pt = 100 HWPUNIT, 1mm ≈ 283 HWPUNIT.
#![allow(unused)] fn main() { HwpUnit::from_pt(12.0) // 12pt → HwpUnit(1200) HwpUnit::from_mm(10.0) // 10mm → HwpUnit(2834) // 유효 범위: ±100M }
네임스페이스 (Namespace)
기하 좌표에는 hp: (paragraph) 네임스페이스가 아닌 hc: (core) 네임스페이스를 사용해야 합니다. hp:를 쓰면 한글이 파일을 파싱하지 못합니다.
<!-- ❌ WRONG — 한글 parse error -->
<hp:startPt x="0" y="0"/>
<!-- ✅ CORRECT -->
<hc:startPt x="0" y="0"/>
다각형(polygon)의 꼭짓점도 동일한 규칙이 적용됩니다. hp:pt를 쓰면 한글에서 “파일을 읽거나 저장하는데 오류“가 발생합니다.
<!-- ❌ WRONG — 한글 파일 오류 -->
<hp:pt x="0" y="0"/>
<!-- ✅ CORRECT (KS X 6101: type="hc:PointType") -->
<hc:pt x="0" y="0"/>
모든 기하 요소(선, 타원, 다각형, 글상자 모서리)는 hc: 네임스페이스를 사용합니다.
도형/차트/수식 (Shapes, Charts, Equations)
HWPX에서 글상자(TextBox)는 Control 요소가 아닙니다. <hp:rect> 도형 안에 <hp:drawText>를 내포한 구조입니다.
#![allow(unused)] fn main() { // ❌ WRONG (HWPX는 control 요소가 아님) Control::TextBox(...) // ✅ CORRECT (HWPX 실제 구조) // <hp:rect ...><hp:drawText>...</hp:drawText></hp:rect> }
수식은 일반 도형(선/타원/다각형)과 달리 offset, orgSz, curSz, flip, rotation, lineShape, fillBrush, shadow 요소가 없습니다.
<!-- ❌ WRONG — equation은 shape common이 없음 -->
<hp:equation><hp:offset .../><hp:orgSz .../></hp:equation>
<!-- ✅ CORRECT — sz + pos + outMargin + script만 -->
<hp:equation>
<hp:sz .../>
<hp:pos .../>
<hp:outMargin .../>
<hp:script>...</hp:script>
</hp:equation>
flowWithText="1" (도형은 0), outMargin left/right=56 (도형은 0 또는 283).
Chart/*.xml 파일을 manifest에 등록하면 한글이 즉시 충돌합니다. ZIP 파일에만 존재해야 합니다.
<!-- ❌ WRONG — 한글 크래시 유발 -->
<opf:item id="chart1" href="Chart/chart1.xml" media-type="application/xml"/>
<!-- ✅ CORRECT — Chart/*.xml은 ZIP에만 존재, content.hpf에 등록하지 않음 -->
<c:f> 요소가 없으면 차트가 열리지만 데이터가 표시되지 않습니다(빈 차트). 더미 수식이라도 반드시 포함해야 합니다.
<!-- ❌ WRONG — 차트 열리지만 데이터 표시 안 됨 -->
<c:cat><c:strRef><c:strCache>...</c:strCache></c:strRef></c:cat>
<!-- ✅ CORRECT — 더미 formula라도 반드시 포함 -->
<c:cat>
<c:strRef>
<c:f>Sheet1!$A$2:$A$5</c:f>
<c:strCache>...</c:strCache>
</c:strRef>
</c:cat>
한글은 <c:f> 존재 여부를 cache 데이터 읽기의 전제조건으로 사용합니다.
<c:strRef> 방식으로 시리즈 이름을 지정하면 한글이 충돌합니다. <c:v>로 직접 값을 지정해야 합니다.
<!-- ❌ WRONG — 한글 크래시 -->
<c:tx><c:strRef><c:strCache>...</c:strCache></c:strRef></c:tx>
<!-- ✅ CORRECT -->
<c:tx><c:v>시리즈명</c:v></c:tx>
- hp:chart 요소에 dropcapstyle 속성 필수
dropcapstyle="None" 속성이 없으면 한글이 충돌합니다. 또한 horzRelTo는 "PARA"가 아닌 "COLUMN"이어야 합니다.
<!-- ✅ CORRECT -->
<hp:chart dropcapstyle="None" horzRelTo="COLUMN" .../>
글상자를 인코딩할 때 지켜야 할 6가지 규칙입니다.
- 모서리 좌표는
hc:네임스페이스:<hc:pt0>~<hc:pt3>(hp:pt0아님) - 요소 순서: shape-common → drawText → caption → hc:pt0-3 → sz → pos → outMargin → shapeComment
- lastWidth = 전체 width: margin을 차감하지 않음
- Shadow alpha = 178: 기본값 0이 아님
- shapeComment 필수:
<hp:shapeComment>사각형입니다.</hp:shapeComment> - Shape run 후
<hp:t/>marker 필수: 모든 shape 포함 run에 빈<hp:t/>추가
한글은 path를 자동으로 닫지 않습니다. 첫 꼭짓점을 마지막에 반복하지 않으면 삼각형이 2변만 표시됩니다.
<!-- ❌ WRONG — 삼각형이 2변만 표시됨 -->
<hc:pt x="0" y="100"/>
<hc:pt x="50" y="0"/>
<hc:pt x="100" y="100"/>
<!-- ✅ CORRECT — 첫 꼭짓점을 마지막에 반복 -->
<hc:pt x="0" y="100"/>
<hc:pt x="50" y="0"/>
<hc:pt x="100" y="100"/>
<hc:pt x="0" y="100"/>
barChart와 stockChart가 catAx를 공유하는 3축 레이아웃을 사용하면 한글 렌더링이 깨집니다. 각 차트 타입은 자체 축 쌍(catAx+valAx)을 가져야 합니다.
<!-- ❌ WRONG — 3축 layout (catAx 공유) → 한글 렌더링 깨짐 -->
<c:barChart>...<c:axId val="1"/><c:axId val="3"/></c:barChart>
<c:stockChart>...<c:axId val="1"/><c:axId val="2"/></c:stockChart>
<c:catAx><c:axId val="1"/>...</c:catAx>
<!-- ✅ CORRECT — OOXML 표준 4축 combo layout -->
<c:barChart>...<c:axId val="3"/><c:axId val="4"/></c:barChart>
<c:stockChart>...<c:axId val="1"/><c:axId val="2"/></c:stockChart>
<c:catAx><c:axId val="1"/><c:crossAx val="2"/>...</c:catAx>
<c:valAx><c:axId val="2"/><c:crossAx val="1"/>...</c:valAx>
<c:catAx><c:axId val="3"/><c:crossAx val="4"/><c:delete val="1"/>...</c:catAx>
<c:valAx><c:axId val="4"/><c:crossAx val="3"/><c:crosses val="max"/>...</c:valAx>
secondary catAx는 delete="1"로 숨깁니다.
페이지/레이아웃 (Page & Layout)
실제 한글 파일의 landscape 속성값은 KS X 6101 스펙과 반대입니다. width/height 비교로 가로/세로를 추론하지 마세요.
| 값 | KS X 6101 스펙 | 한글 실제 동작 |
|---|---|---|
WIDELY | 가로(landscape) | 세로(portrait) |
NARROWLY | 세로(portrait) | 가로(landscape) |
또한 width/height는 항상 세로 기준으로 유지해야 합니다 (예: A4 = 210x297). 한글이 내부적으로 회전 처리합니다.
#![allow(unused)] fn main() { // ❌ WRONG — width/height 교환 시 이중 회전 발생 let landscape = PageSettings { width: HwpUnit::from_mm(297.0).unwrap(), height: HwpUnit::from_mm(210.0).unwrap(), ..PageSettings::a4() }; // ✅ CORRECT — landscape: true, 치수는 세로 기준 유지 let landscape = PageSettings { landscape: true, ..PageSettings::a4() }; }
build_col_pr_xml은 self-closing <hp:colPr ... />를 생성합니다. </hp:colPr>를 검색하면 매칭에 실패합니다.
#![allow(unused)] fn main() { // ❌ WRONG — self-closing 태그를 놓침 xml.find("</hp:colPr>") // ✅ CORRECT — 양쪽 형태 모두 매칭 xml.find("<hp:colPr") }
secPr 내 ctrl 요소 순서: secPr → colPr → header → footer → pageNum
Modern(22) 스타일셋에서 개요 8/9/10의 paraPr 인덱스는 순차적이지 않습니다. 순차적이라고 가정하면 잘못된 스타일이 적용됩니다.
| 스타일 | Style ID | paraPr 그룹 |
|---|---|---|
| 개요 8 | 9 | 18 |
| 개요 9 | 10 | 16 |
| 개요 10 | 11 | 17 |
Modern 스타일셋에서 사용자 paraShape는 인덱스 20부터 시작합니다.
<hm:masterPage> 형태로 prefix를 쓰거나 xmlns 선언이 누락되면 한글이 즉시 충돌합니다.
<!-- ❌ WRONG — 한글 크래시 (예기치 않게 종료) -->
<hm:masterPage xmlns:hp="..." xmlns:hm="...">
<hm:subList>...</hm:subList>
</hm:masterPage>
<!-- ✅ CORRECT — prefix 없는 루트 + 15개 xmlns 전체 선언 + hp:subList -->
<masterPage xmlns="http://www.hancom.co.kr/hwpml/2011/master"
xmlns:hp="..." xmlns:hh="..." xmlns:hc="..." ...>
<hp:subList id="" textDirection="HORIZONTAL" ...>
...
</hp:subList>
</masterPage>
3가지 핵심 규칙:
- 루트 요소는
<masterPage>(prefix 없음,<hm:masterPage>아님) - header/section과 동일한 15개 namespace 전부 선언 필수
<hp:subList>사용 (<hm:subList>아님)
필드/참조 (Fields & References)
실제 한글 파일에서는 <hh:paraPr> 당 2개 이상의 <hp:switch>가 있습니다 (예: 제목용 하나, 여백/줄간격용 하나). 스키마는 Option<HxSwitch>가 아닌 Vec<HxSwitch>를 사용해야 합니다.
#![allow(unused)] fn main() { // ❌ WRONG switches: Option<HxSwitch> // ✅ CORRECT switches: Vec<HxSwitch> }
- 하이퍼링크는 fieldBegin/fieldEnd 패턴 (hp:hyperlink 없음)
<hp:hyperlink> 요소는 HWPX에 존재하지 않습니다. KS X 6101의 field pair 패턴을 사용해야 합니다.
<!-- ❌ WRONG — 이런 요소 없음 -->
<hp:hyperlink href="...">...</hp:hyperlink>
<!-- ✅ CORRECT — KS X 6101 field pair -->
<hp:run charPrIDRef="0">
<hp:ctrl>
<hp:fieldBegin type="HYPERLINK" fieldid="0">
<hp:parameters cnt="4">
<hp:stringParam name="Path">https://url.com</hp:stringParam>
<hp:stringParam name="Category">HWPHYPERLINK_TYPE_URL</hp:stringParam>
<hp:stringParam name="TargetType">HWPHYPERLINK_TARGET_DOCUMENT_DONTCARE</hp:stringParam>
<hp:stringParam name="DocOpenType">HWPHYPERLINK_JUMP_NEWTAB</hp:stringParam>
</hp:parameters>
</hp:fieldBegin>
</hp:ctrl>
<hp:t>링크 텍스트</hp:t>
<hp:ctrl><hp:fieldEnd beginIDRef="0" fieldid="0"/></hp:ctrl>
</hp:run>
각주/미주를 별도 문단으로 만들면 각주 번호가 단독 줄에 표시됩니다. 반드시 같은 문단의 Run에 포함해야 합니다.
#![allow(unused)] fn main() { // ❌ WRONG — 별도 문단으로 만들면 "1)"이 단독 줄에 표시됨 paras.push(p("본문 텍스트.")); paras.push(ctrl_para(Control::footnote(notes), CS_NORMAL, PS_JUSTIFY)); // ✅ CORRECT — 같은 문단의 Run에 포함 paras.push(Paragraph::with_runs( vec![ Run::text("본문 텍스트.", CharShapeIndex::new(0)), Run::control(Control::footnote(notes), CharShapeIndex::new(0)), ], ParaShapeIndex::new(0), )); }
한글 내부에서 14년간 유지된 오타입니다. "DATE" 또는 "TIME"을 쓰면 아무것도 표시되지 않습니다.
<!-- ❌ WRONG — 한글이 인식하지 않음 (빈 필드) -->
<hp:fieldBegin type="DATE" ...>
<hp:fieldBegin type="TIME" ...>
<!-- ✅ CORRECT — "Summary"의 오타 "SUMMERY" 사용 -->
<hp:fieldBegin type="SUMMERY" fieldid="628321650" ...>
<hp:parameters cnt="3" name="">
<hp:integerParam name="Prop">8</hp:integerParam>
<hp:stringParam name="Command">$modifiedtime</hp:stringParam>
<hp:stringParam name="Property">$modifiedtime</hp:stringParam>
</hp:parameters>
</hp:fieldBegin>
Command 매핑: $modifiedtime=날짜, $createtime=시간, $author=작성자, $lastsaveby=최종수정자.
CLICK_HERE와의 차이: Prop=8 (CLICK_HERE는 9), fieldid=628321650 (CLICK_HERE는 627272811).
- 본문 쪽번호는 hp:autoNum 사용 (fieldBegin 아님)
type="PAGE_NUM"은 유효한 fieldBegin 타입이 아닙니다. 본문에 쪽번호를 삽입할 때는 autoNum 메커니즘을 사용해야 합니다.
<!-- ❌ WRONG — PAGE_NUM은 존재하지 않는 타입 -->
<hp:fieldBegin type="PAGE_NUM" ...>
<!-- ✅ CORRECT — autoNum 메커니즘 사용 -->
<hp:ctrl>
<hp:autoNum num="1" numType="PAGE">
<hp:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar="" supscript="0"/>
</hp:autoNum>
</hp:ctrl>
3가지 쪽번호 메커니즘 혼동 주의:
<hp:pageNum>: secPr 내 ctrl (머리글/바닥글 자동 배치)<hp:autoNum numType="PAGE">: 본문 텍스트 인라인 삽입: 존재하지 않음type="PAGE_NUM"fieldBegin
pageBreak 속성을 하드코딩된 0으로 설정하면 페이지 나누기가 동작하지 않습니다.
#![allow(unused)] fn main() { // ❌ WRONG (하드코딩) page_break: 0, // ✅ CORRECT — para.page_break 필드에서 읽기 page_break: u32::from(para.page_break), }
encoder/section.rs의 build_paragraph()에서 pageBreak 속성을 para.page_break 필드로부터 읽어야 합니다.
스타일 (Styles)
BREAK_WORD를 사용하면 양쪽 정렬(justify) 텍스트에서 글자 사이 공간이 균등 분배되어 글자가 비정상적으로 퍼집니다. 한글 기본값인 KEEP_WORD를 사용해야 합니다.
#![allow(unused)] fn main() { // ❌ WRONG — 양쪽 정렬 시 글자 사이 공간 균등 분배 → 퍼짐 현상 break_non_latin_word: "BREAK_WORD" // ✅ CORRECT — 한글 기본값, 단어 단위 공간 분배 → 자연스러운 정렬 break_non_latin_word: "KEEP_WORD" }
위치: crates/hwpforge-smithy-hwpx/src/encoder/header.rs build_para_pr()
KS X 6101 스키마에는 FILLED_DIAMOND, FILLED_CIRCLE, FILLED_BOX가 유효한 값으로 정의되어 있지만, 실제 한글은 이를 인식하지 못합니다. EMPTY_* 형태와 headfill/tailfill 속성(0 또는 1)으로 채움 여부를 제어해야 합니다.
<!-- ❌ WRONG — 한글이 인식하지 않음 (화살촉 안 보임) -->
<hp:lineShape headStyle="FILLED_DIAMOND" headfill="1" .../>
<!-- ✅ CORRECT — EMPTY_* + headfill="1" = 채워진 다이아몬드 -->
<hp:lineShape headStyle="EMPTY_DIAMOND" headfill="1" .../>
<!-- ✅ CORRECT — EMPTY_* + headfill="0" = 빈 다이아몬드 -->
<hp:lineShape headStyle="EMPTY_DIAMOND" headfill="0" .../>
적용 대상: EMPTY_DIAMOND, EMPTY_CIRCLE, EMPTY_BOX
비기하 도형은 그대로: NORMAL, ARROW, SPEAR, CONCAVE_ARROW (fill 속성 무관)
DropCapStyle은 문단 속성이 아니라 도형(AbstractShapeObjectType)의 속성입니다. 값은 SCREAMING_SNAKE_CASE가 아닌 PascalCase를 사용해야 합니다.
<!-- ❌ WRONG — SCREAMING_SNAKE_CASE -->
dropcapstyle="DOUBLE_LINE"
<!-- ✅ CORRECT — PascalCase (KS X 6101 XSD 준수) -->
dropcapstyle="DoubleLine"
유효한 값: None, DoubleLine, TripleLine, Margin
라이브러리 호환성 (Library Compatibility)
Section 레코드는 공식 스펙보다 +16 오프셋을 가집니다.
| 항목 | 스펙 값 | 실제 값 |
|---|---|---|
PARA_HEADER | 0x32 (50) | 0x42 (66) |
자세한 내용은 .docs/research/SPEC_VS_REALITY.md를 참조하세요.
Foundation은 의존성 그래프의 루트입니다. Foundation을 수정하면 모든 crate가 재빌드됩니다. 불필요한 의존성을 추가하지 마세요.
Phase 0 Oracle 리뷰에서 사용하지 않는 의존성 3개를 제거한 사례가 있습니다.
schemars 0.8에서 1.x로 업그레이드하면 schema_name()의 반환 타입이 변경되었습니다.
#![allow(unused)] fn main() { // ❌ WRONG (schemars 0.8 API) fn schema_name() -> String { "MyType".to_owned() } // ✅ CORRECT (schemars 1.x API) fn schema_name() -> Cow<'static, str> { Cow::Borrowed("MyType") } }
quick-xml 0.36에서 0.39로 업그레이드하면 unescape() API가 제거되었습니다. 또한 Event::GeneralRef variant가 추가되어 exhaustive match에서 처리해야 합니다.
#![allow(unused)] fn main() { // ❌ WRONG (quick-xml 0.36 API — 0.39에서 제거됨) let text = event.unescape()?; // ✅ CORRECT (quick-xml 0.39) let text = reader.decoder().decode(event.as_ref())?; }
요약 체크리스트
구현 전에 확인하세요:
-
색상 값에
Color::from_rgb()사용 (BGR 혼동 방지) -
기하 좌표에
hc:네임스페이스 사용 (선/다각형/글상자 모서리) -
차트 XML을
content.hpf에 등록하지 않음 -
차트 데이터에
<c:f>더미 formula 포함 -
차트 시리즈 이름에
<c:v>직접값 사용 - 주식 차트에 4축 combo layout 사용
-
가로 방향에
landscape: true플래그 사용 (width/height 교환 금지) - MasterPage XML에 prefix 없는 루트 + 15개 xmlns 선언
-
날짜/시간 필드에
type="SUMMERY"(오타 포함) -
본문 쪽번호에
<hp:autoNum>사용 (fieldBegin 아님) -
화살표에
EMPTY_*+headfill/tailfill조합 사용 -
DropCapStyle에 PascalCase 값 사용 -
breakNonLatinWord를KEEP_WORD로 설정 - 다각형 꼭짓점 목록 마지막에 첫 꼭짓점 반복
- 각주/미주를 같은 문단의 Run에 인라인으로 삽입
API 레퍼런스
HwpForge의 전체 공개 API 문서는 rustdoc으로 자동 생성됩니다.
크레이트 구조
| 크레이트 | 역할 | rustdoc |
|---|---|---|
hwpforge | 우산 크레이트 (re-export) | hwpforge |
hwpforge-foundation | 기본 타입 (HwpUnit, Color) | hwpforge_foundation |
hwpforge-core | 문서 구조 (Document, Section) | hwpforge_core |
hwpforge-blueprint | 스타일 템플릿 (YAML) | hwpforge_blueprint |
hwpforge-smithy-hwpx | HWPX 인코더/디코더 | hwpforge_smithy_hwpx |
hwpforge-smithy-md | Markdown 인코더/디코더 | hwpforge_smithy_md |
로컬에서 보기
cargo doc --open --no-deps --all-features
crates.io 퍼블리시 후
퍼블리시 이후에는 docs.rs/hwpforge에서도 확인할 수 있습니다.
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.1.0 - 2026-03-06
Added
- hwpforge: Umbrella crate with feature flags (
hwpx,md,full) - hwpforge-foundation: Primitive types (HwpUnit, Color BGR, branded Index
, enums, error codes) - hwpforge-core: Format-independent document model with typestate validation (Draft/Validated)
- Document, Section, Paragraph, Run, Table, Image
- Controls: TextBox, Footnote, Endnote, Equation, Chart (18 types)
- Shapes: Line, Ellipse, Polygon, Arc, Curve, ConnectLine
- References: Bookmark, CrossRef, Field, Memo, IndexMark
- Layout: Multi-column, captions, headers/footers, page numbers, master pages
- Annotations: Dutmal, compose characters
- hwpforge-blueprint: YAML-based style template system
- Template inheritance with DFS merge
- StyleRegistry with deduplicated fonts, char shapes, para shapes
- Built-in default template (Hancom 한컴바탕)
- BorderFill support
- hwpforge-smithy-hwpx: Full HWPX codec (KS X 6101)
- Decoder: HWPX ZIP+XML -> Core Document
- Encoder: Core Document -> HWPX ZIP+XML
- Lossless roundtrip for all supported content
- HancomStyleSet support (Classic/Modern/Latest)
- 22 default styles with per-style charPr/paraPr
- ZIP bomb defense (50MB/500MB/10k limits)
- OOXML chart generation (18 chart types)
- Golden fixture tests with real Hancom 한글 files
- hwpforge-smithy-md: Markdown codec
- GFM decoder (pulldown-cmark) with YAML frontmatter
- Lossy encoder (readable GFM) and lossless encoder (HTML+YAML)
- Full pipeline: MD -> Core -> HWPX verified in Hancom 한글