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

HWP5와 HWPX: 이중 포맷 파이프라인

HwpForge는 한국의 두 가지 주요 문서 포맷 — 바이너리 OLE 기반 HWP5와 XML 기반 HWPX — 을 하나의 통합 파이프라인으로 처리할 수 있도록 설계되었습니다.

포맷 비교

특성HWP5 (.hwp)HWPX (.hwpx)
컨테이너OLE2/CFB (Compound File Binary)ZIP
내부 데이터바이너리 레코드 스트림XML 파일 (KS X 6101 OWPML)
표준한컴 독자 포맷 (공개 스펙)국가 표준 KS X 6101
역사1990년대~현재 (레거시)2014년~ (현대)
파일 시그니처D0 CF 11 E0 A1 B1 1A E1 (OLE)50 4B 03 04 (ZIP/PK)
스트림 구조FileHeader, DocInfo, BodyText/Section0mimetype, Contents/header.xml, Contents/section0.xml
압축zlib (스트림 단위)ZIP deflate (파일 단위)
암호화지원 (스트림 암호화)지원 (ZIP 암호화)
한글 호환성한글 97~최신한글 2014~최신

Core DOM: 포맷 독립 중간 표현 (IR)

HwpForge의 핵심 설계 원칙은 Core DOM이 포맷에 독립적이라는 것입니다. Document<Draft>는 HWP5든 HWPX든 Markdown이든 동일한 구조체로 표현됩니다.

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  HWP5 파일  │     │  HWPX 파일  │     │  Markdown   │
│ (OLE/CFB)   │     │ (ZIP/XML)   │     │ (GFM+YAML)  │
└──────┬──────┘     └──────┬──────┘     └──────┬──────┘
       │ decode            │ decode            │ decode
       ▼                   ▼                   ▼
┌──────────────────────────────────────────────────────┐
│              Document<Draft>  (Core DOM)              │
│  ┌──────────────────────────────────────────────┐    │
│  │ Sections → Paragraphs → Runs → Text/Control  │    │
│  │ + Metadata (title, author, created, ...)      │    │
│  │ + Tables, Images, Shapes, Charts, ...         │    │
│  └──────────────────────────────────────────────┘    │
│              포맷 독립 중간 표현 (IR)                  │
└──────────┬───────────────┬───────────────┬───────────┘
           │ encode        │ encode        │ encode
           ▼               ▼               ▼
    ┌──────────┐    ┌──────────┐    ┌──────────┐
    │ HWP5     │    │ HWPX     │    │ Markdown │
    │ CLI/전용 │    │ ✅ 구현   │    │ ✅ 구현   │
    └──────────┘    └──────────┘    └──────────┘

이 설계 덕분에:

  • 하나의 문서 모델로 모든 포맷을 처리합니다
  • 포맷 간 변환이 Core DOM을 경유하여 자연스럽게 이루어집니다
  • 새 포맷 추가 시 기존 코드 수정 없이 Smithy 크레이트만 추가하면 됩니다
  • 비즈니스 로직은 Core DOM에만 의존하므로 포맷 변경에 영향을 받지 않습니다

포맷 감지

파일의 첫 바이트(매직 바이트)로 포맷을 판별합니다.

#![allow(unused)]
fn main() {
/// 파일 포맷 감지
enum DocumentFormat {
    Hwp5,     // OLE2/CFB 바이너리
    Hwpx,     // ZIP + XML
    Markdown, // 텍스트
    Unknown,
}

fn detect_format(bytes: &[u8]) -> DocumentFormat {
    if bytes.len() < 4 {
        return DocumentFormat::Unknown;
    }

    // OLE2 Compound File Binary: D0 CF 11 E0
    if bytes.starts_with(&[0xD0, 0xCF, 0x11, 0xE0]) {
        return DocumentFormat::Hwp5;
    }

    // ZIP (PK\x03\x04)
    if bytes.starts_with(&[0x50, 0x4B, 0x03, 0x04]) {
        return DocumentFormat::Hwpx;
    }

    // UTF-8 텍스트로 시작하면 Markdown 후보
    if std::str::from_utf8(bytes).is_ok() {
        return DocumentFormat::Markdown;
    }

    DocumentFormat::Unknown
}
}

포맷 독립 문서 처리

Core DOM을 활용하면 입력 포맷에 관계없이 동일한 코드로 문서를 처리할 수 있습니다.

현재 지원되는 파이프라인

#![allow(unused)]
fn main() {
use hwpforge::hwpx::{HwpxDecoder, HwpxEncoder, HwpxRegistryBridge};
use hwpforge::md::{MdDecoder, MdDocument, MdEncoder};
use hwpforge::core::{Document, Draft, ImageStore};

// === 1. HWPX → Core DOM ===
let hwpx_result = HwpxDecoder::decode_file("input.hwpx").unwrap();
let doc_from_hwpx: Document<Draft> = hwpx_result.document;

// === 2. Markdown → Core DOM ===
let markdown = "# 제목\n\n본문 내용입니다.";
let MdDocument { document: doc_from_md, style_registry } =
    MdDecoder::decode_with_default(markdown).unwrap();

// === 3. 포맷 독립 처리 (어느 소스에서 왔든 동일) ===
fn process_document(doc: &Document<Draft>) {
    // 메타데이터 접근
    let meta = doc.metadata();
    println!("제목: {:?}", meta.title);
    println!("작성자: {:?}", meta.author);

    // 섹션/문단 순회
    for section in doc.sections() {
        println!("문단 수: {}", section.paragraphs.len());
        let counts = section.content_counts();
        println!("표: {}, 이미지: {}", counts.tables, counts.images);
    }
}

process_document(&doc_from_hwpx);
process_document(&doc_from_md);

// === 4. Core DOM → 다른 포맷으로 출력 ===
// HWPX로 저장
let bridge = HwpxRegistryBridge::from_registry(&style_registry).unwrap();
let rebound = bridge.rebind_draft_document(doc_from_md).unwrap();
let validated = rebound.validate().unwrap();
let bytes = HwpxEncoder::encode(&validated, bridge.style_store(), &ImageStore::new()).unwrap();
std::fs::write("output.hwpx", &bytes).unwrap();

// Markdown으로 저장
let markdown_out = MdEncoder::encode_lossy(&validated).unwrap();
std::fs::write("output.md", &markdown_out).unwrap();
}

현재: HWP5 전용 crate / CLI 경로

#![allow(unused)]
fn main() {
use hwpforge_smithy_hwp5::Hwp5Decoder;
use hwpforge::hwpx::{HwpxEncoder, HwpxStyleStore};
use hwpforge::core::ImageStore;

let hwp5_result = Hwp5Decoder::decode_file("legacy.hwp").unwrap();
let doc: Document<Draft> = hwp5_result.document;

// Core DOM을 경유하여 HWP5 → HWPX 변환
let validated = doc.validate().unwrap();
let style_store = HwpxStyleStore::with_default_fonts("함초롬바탕");
let bytes = HwpxEncoder::encode(&validated, &style_store, &ImageStore::new()).unwrap();
std::fs::write("converted.hwpx", &bytes).unwrap();
}

CLI만 필요하다면 전용 명령도 이미 있습니다.

hwpforge convert-hwp5 legacy.hwp -o converted.hwpx
hwpforge audit-hwp5 legacy.hwp converted.hwpx
hwpforge census-hwp5 legacy.hwp --json

CLI에서 포맷 처리

현재 CLI는 HWPX와 Markdown을 지원합니다.

# Markdown → HWPX 변환
hwpforge convert report.md -o report.hwpx

# HWPX 문서 검사 (메타데이터 포함)
hwpforge inspect report.hwpx --json

# HWPX → JSON → 편집 → HWPX 라운드트립
hwpforge to-json report.hwpx -o report.json
# (AI 에이전트가 JSON 편집)
hwpforge from-json report.json -o updated.hwpx

# HWPX → Markdown (읽기용)
# Rust API: MdEncoder::encode_lossy(&validated)

크레이트 역할 분담

크레이트역할포맷 의존성
hwpforge-foundation원시 타입 (HwpUnit, Color, Index)없음
hwpforge-core포맷 독립 문서 모델 (IR)없음
hwpforge-blueprintYAML 스타일 템플릿없음
hwpforge-smithy-hwpxHWPX ↔ Core 코덱HWPX (ZIP+XML)
hwpforge-smithy-hwp5HWP5 decode/projectionHWP5 (OLE/CFB)
hwpforge-smithy-mdMarkdown ↔ Core 코덱Markdown (텍스트)
hwpforge-convertHWP5 → HWPX 변환 오케스트레이터 + audit helpersHWP5 + HWPX (smithy 경유)

핵심 원칙: Core 이하 계층은 어떤 파일 포맷도 모릅니다. Smithy 계층만 특정 포맷을 이해하고, convert는 두 Smithy를 엮어 포맷 간 변환을 지휘합니다(자체 포맷 파싱 없음).

HWP5 포맷 구조 (참고)

HWP5 파일은 OLE2 Compound File Binary (CFB) 컨테이너 안에 바이너리 레코드 스트림을 저장합니다.

HWP5 파일 (OLE2 CFB)
├── FileHeader          — 파일 인식 정보, 버전, 플래그
├── DocInfo             — 문서 설정 (스타일, 폰트, 탭, 번호)
├── BodyText/
│   ├── Section0        — 첫 번째 섹션 (바이너리 레코드)
│   ├── Section1        — 두 번째 섹션
│   └── ...
├── BinData/            — 이미지 등 바이너리 데이터
├── DocOptions/         — 추가 옵션
├── Scripts/            — 매크로 스크립트
└── PrvText             — 미리보기 텍스트

각 섹션은 Tag-Length-Value (TLV) 구조의 레코드 체인으로 구성됩니다:

레코드 = TagID (10bit) + Level (10bit) + Size (12bit) + Data (Size bytes)

주의: HWP5의 TagID에는 +16 오프셋이 있습니다. PARA_HEADER = 0x42 (66), 공식 스펙의 0x32 (50)가 아닙니다.

현재 지원 상태

기능HWPXHWP5Markdown
읽기 (Decode)✅ 완전 지원🟡 전용 crate/CLI 경로✅ 완전 지원
쓰기 (Encode)✅ 완전 지원✅ 완전 지원
메타데이터 추출✅ Core DOM🟡 inspect/census summary✅ YAML Frontmatter
이미지 추출✅ ImageStore🟡 decode/projection path
스타일 보존✅ HwpxStyleStore🟡 warning-first projection + HWPX re-emission✅ StyleRegistry
JSON 라운드트립✅ to-json/from-json

HWP5 읽기 자체는 더 이상 future tense가 아니다. 지금도 hwpforge-smithy-hwp5와 CLI를 통해 decode / inspect / convert / audit 경로를 사용할 수 있다. 다만 umbrella crate와 일부 top-level guide는 여전히 HWPX/Markdown 중심이며, HWP5 parity는 warning-first로 점진적으로 넓혀 가는 중이다.