Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

HwpForge

한글 문서(HWP/HWPX)를 프로그래밍으로 제어하는 Rust 라이브러리

crates.io docs.rs License: MIT OR Apache-2.0


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 호환)
참조책갈피, 상호참조, 필드, 메모, 찾아보기
MarkdownGFM 디코드, 손실/무손실 인코드, 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기본 포함설명
hwpxHWPX 인코더/디코더 (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 &section.paragraphs {
            for run in &paragraph.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>: 팬텀 타입을 이용한 브랜드 인덱스. CharShapeIndexParaShapeIndex를 혼용하면 컴파일 에러

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 프론트매터 → Core
  • MdEncoder — 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 (OOXML xmlns: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 — 임베드된 이미지 바이너리 데이터
}
필드타입설명
documentDocument<Draft>섹션, 문단, 런 트리
style_storeHwpxStyleStore폰트/글자모양/문단모양/스타일
image_storeImageStore이미지 바이너리 저장소

인코딩: 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(&registry);
}

라운드트립 예제 (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>를 반환합니다. HwpxErrorHwpxErrorCode와 메시지를 포함합니다.

#![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(&registry);
}

한컴 스타일셋: Classic / Modern / Latest

한글 프로그램은 버전에 따라 다른 기본 스타일 구성을 사용합니다.

스타일셋스타일 수설명
Classic18개한글 구버전 호환
Modern22개기본값 (한글 2018~)
Latest23개최신 버전

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(&registry);

// 문서 구성 (스타일 인덱스는 레지스트리에서 조회)
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 / Column3D3D 막대/세로 막대
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],
};
}

차트를 문단에 삽입하는 패턴

차트 ControlRun::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)

  1. 색상은 BGR 순서 (RGB 아님)

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로 저장
}
  1. HwpUnit은 정수 기반 단위

부동소수점 정밀도 오류를 피하기 위해 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)

  1. 선(Line) 도형의 좌표는 hc: 네임스페이스

기하 좌표에는 hp: (paragraph) 네임스페이스가 아닌 hc: (core) 네임스페이스를 사용해야 합니다. hp:를 쓰면 한글이 파일을 파싱하지 못합니다.

<!-- ❌ WRONG — 한글 parse error -->
<hp:startPt x="0" y="0"/>

<!-- ✅ CORRECT -->
<hc:startPt x="0" y="0"/>
  1. 다각형 꼭짓점도 hc: 네임스페이스

다각형(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)

  1. TextBox는 control 요소가 아님

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>
}
  1. 수식(Equation)에는 shape common 블록이 없음

수식은 일반 도형(선/타원/다각형)과 달리 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).

  1. 차트 XML을 content.hpf manifest에 등록하면 안 됨

Chart/*.xml 파일을 manifest에 등록하면 한글이 즉시 충돌합니다. ZIP 파일에만 존재해야 합니다.

<!-- ❌ WRONG — 한글 크래시 유발 -->
<opf:item id="chart1" href="Chart/chart1.xml" media-type="application/xml"/>

<!-- ✅ CORRECT — Chart/*.xml은 ZIP에만 존재, content.hpf에 등록하지 않음 -->
  1. 차트 데이터에 <c:f> formula 참조가 필수

<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 데이터 읽기의 전제조건으로 사용합니다.

  1. 차트 시리즈 이름 <c:tx>는 직접값만 허용

<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>
  1. hp:chart 요소에 dropcapstyle 속성 필수

dropcapstyle="None" 속성이 없으면 한글이 충돌합니다. 또한 horzRelTo"PARA"가 아닌 "COLUMN"이어야 합니다.

<!-- ✅ CORRECT -->
<hp:chart dropcapstyle="None" horzRelTo="COLUMN" .../>
  1. TextBox(hp:rect) 인코딩 6가지 핵심 규칙

글상자를 인코딩할 때 지켜야 할 6가지 규칙입니다.

  1. 모서리 좌표는 hc: 네임스페이스: <hc:pt0> ~ <hc:pt3> (hp:pt0 아님)
  2. 요소 순서: shape-common → drawText → caption → hc:pt0-3 → sz → pos → outMargin → shapeComment
  3. lastWidth = 전체 width: margin을 차감하지 않음
  4. Shadow alpha = 178: 기본값 0이 아님
  5. shapeComment 필수: <hp:shapeComment>사각형입니다.</hp:shapeComment>
  6. Shape run 후 <hp:t/> marker 필수: 모든 shape 포함 run에 빈 <hp:t/> 추가
  1. 다각형 꼭짓점은 첫 번째를 마지막에 반복해야 닫힘

한글은 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"/>
  1. VHLC/VOHLC 주식 차트는 4축 combo layout 필수

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)

  1. landscape 속성값이 스펙과 반대

실제 한글 파일의 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()
};
}
  1. colPr self-closing 태그와 ctrl 요소 순서

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

  1. Modern 스타일셋의 개요 8/9/10 paraPr 인덱스는 비순차

Modern(22) 스타일셋에서 개요 8/9/10의 paraPr 인덱스는 순차적이지 않습니다. 순차적이라고 가정하면 잘못된 스타일이 적용됩니다.

스타일Style IDparaPr 그룹
개요 8918
개요 91016
개요 101117

Modern 스타일셋에서 사용자 paraShape는 인덱스 20부터 시작합니다.

  1. MasterPage XML은 prefix 없는 루트 + 15개 xmlns 전체 선언 필수

<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가지 핵심 규칙:

  1. 루트 요소는 <masterPage> (prefix 없음, <hm:masterPage> 아님)
  2. header/section과 동일한 15개 namespace 전부 선언 필수
  3. <hp:subList> 사용 (<hm:subList> 아님)

필드/참조 (Fields & References)

  1. paraPr 당 switch가 여러 개일 수 있음

실제 한글 파일에서는 <hh:paraPr> 당 2개 이상의 <hp:switch>가 있습니다 (예: 제목용 하나, 여백/줄간격용 하나). 스키마는 Option<HxSwitch>가 아닌 Vec<HxSwitch>를 사용해야 합니다.

#![allow(unused)]
fn main() {
// ❌ WRONG
switches: Option<HxSwitch>

// ✅ CORRECT
switches: Vec<HxSwitch>
}
  1. 하이퍼링크는 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>
  1. 각주/미주는 같은 문단의 인라인 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),
));
}
  1. 날짜/시간 필드는 type=SUMMERY (오타 주의)

한글 내부에서 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).

  1. 본문 쪽번호는 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: 존재하지 않음
  1. page_break는 문단 속성으로 직접 인코딩

pageBreak 속성을 하드코딩된 0으로 설정하면 페이지 나누기가 동작하지 않습니다.

#![allow(unused)]
fn main() {
// ❌ WRONG (하드코딩)
page_break: 0,

// ✅ CORRECT — para.page_break 필드에서 읽기
page_break: u32::from(para.page_break),
}

encoder/section.rsbuild_paragraph()에서 pageBreak 속성을 para.page_break 필드로부터 읽어야 합니다.


스타일 (Styles)

  1. breakNonLatinWord는 반드시 KEEP_WORD

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()

  1. 화살표 도형은 반드시 EMPTY_* 형태 사용

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 속성 무관)

  1. DropCapStyle은 PascalCase (도형 레벨 속성)

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)

  1. HWP5 TagID에 +16 오프셋

Section 레코드는 공식 스펙보다 +16 오프셋을 가집니다.

항목스펙 값실제 값
PARA_HEADER0x32 (50)0x42 (66)

자세한 내용은 .docs/research/SPEC_VS_REALITY.md를 참조하세요.

  1. Foundation 의존성은 최소화

Foundation은 의존성 그래프의 루트입니다. Foundation을 수정하면 모든 crate가 재빌드됩니다. 불필요한 의존성을 추가하지 마세요.

Phase 0 Oracle 리뷰에서 사용하지 않는 의존성 3개를 제거한 사례가 있습니다.

  1. schemars 1.x: schema_name() 반환 타입 변경

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") }
}
  1. quick-xml 0.39: unescape() 제거

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 값 사용
  • breakNonLatinWordKEEP_WORD로 설정
  • 다각형 꼭짓점 목록 마지막에 첫 꼭짓점 반복
  • 각주/미주를 같은 문단의 Run에 인라인으로 삽입

API 레퍼런스

HwpForge의 전체 공개 API 문서는 rustdoc으로 자동 생성됩니다.

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-hwpxHWPX 인코더/디코더hwpforge_smithy_hwpx
hwpforge-smithy-mdMarkdown 인코더/디코더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 한글