# Pdf To Hwp

> PDF/이미지/MD/DOCX 문서를 한컴오피스에서 편집 가능한 HWPX 파일로 변환합니다. 시험지(문제·선택지·지문)와 일반 문서(보고서·공문·텍스트 문서) 두 모드를 지원합니다. Claude가 문서를 직접 읽어(Vision) 구조를 인식하고, rhwp 오픈소스(build-from-ingest)로 HWPX를 생성합니다. 트리거 - "PDF를 HWP로 변환", "한글 파일로 만들어줘", "HWPX로 바꿔줘", "시험지 변환", "/pdf-to-hwp".

- **Type:** Skill
- **Install:** `agentstack add skill-yoonsoli-pdf-to-hwp-pdf-to-hwp`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [yoonsoli](https://agentstack.voostack.com/s/yoonsoli)
- **Installs:** 0
- **Category:** [Content & Media](https://agentstack.voostack.com/c/content-and-media)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [yoonsoli](https://github.com/yoonsoli)
- **Source:** https://github.com/yoonsoli/pdf-to-hwp/tree/main/skills/pdf-to-hwp

## Install

```sh
agentstack add skill-yoonsoli-pdf-to-hwp-pdf-to-hwp
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# pdf-to-hwp — PDF → HWPX 변환 Skill

## 필수 경로

```bash
RHWP_BIN="$HOME/workspace/rhwp/target/release/rhwp"   # rhwp 바이너리
RHWP_REPO="$HOME/workspace/rhwp"                      # rhwp 소스 (스키마/샘플 참조용)
HELPERS="/helpers"          # 스킬 파일 위치 기준 상대 경로
```

`HELPERS`는 설치 방식에 따라 달라진다 — 이 SKILL.md 파일이 위치한 디렉토리의 `helpers/` 하위를 쓰면 된다 (전역 설치: `~/.claude/skills/pdf-to-hwp/helpers`, 플러그인 설치: 플러그인 디렉토리의 `skills/pdf-to-hwp/helpers`).

바이너리가 없으면: `cd ~/workspace/rhwp && PATH="$HOME/.cargo/bin:$PATH" cargo build --release` (rust-toolchain.toml이 Rust 1.93.1 자동 설치). 저장소가 없으면 `git clone https://github.com/edwardkim/rhwp.git ~/workspace/rhwp` 먼저. 전체 환경 셋업은 스킬 저장소의 `install.sh` 참조.

의존성 점검: `bash $HELPERS/check_deps.sh`

## 작동 원리

```
[입력: PDF/PNG/JPG/MD/DOCX]
        ▼ helpers/ 스크립트로 입력 정규화 (PDF→페이지 PNG + 텍스트 layer)
        ▼ Claude가 Read tool로 페이지 PNG를 1장씩 Vision 분석
        ▼ 모드 판별: 시험지(모드 A) vs 일반 문서(모드 B)
        ▼ ingest.json 작성 (ingest_schema_v1)
        ▼ helpers/crop_image.sh로 이미지 영역 bbox 자르기
        ▼ $RHWP_BIN build-from-ingest → out.hwpx
```

**중요**: OCR/레이아웃 분석은 Claude 본인의 멀티모달 능력으로 직접 수행한다. 외부 OCR 엔진이나 별도 API 호출 없음. `pdftotext` 출력(`text.txt`)이 있으면 Vision 분석 결과와 교차 검증해 오탈자를 줄인다.

## 공통 파이프라인

### Step 1: 입력 정규화

```bash
TMP=$(mktemp -d /tmp/pdf-to-hwp.XXXXXX)
MEDIA_DIR="$TMP/media"
bash $HELPERS/pdf_to_pngs.sh input.pdf "$TMP" 300   # → page_001.png, ... + text.txt(텍스트 layer 있으면)
```

- 이미지 입력(PNG/JPG): 그대로 Read로 분석.
- DOCX: `python3 $HELPERS/extract_docx.py input.docx "$TMP"` → `text.txt` + `img/*.png`.
- MD: 직접 읽기. `` 이미지는 media 항목으로.

### Step 2: Vision 분석 + 모드 판별

각 `page_NNN.png`를 Read로 읽고 구조를 파악한다.

- **모드 A (시험지)**: 문제 번호("1.", "2."), 선택지 ①~⑤, 지문/보기 구조가 보이면.
- **모드 B (일반 문서)**: 그 외 전부 — 보고서, 공문, 안내문, 논문, 계약서 등.
- 애매하면 사용자에게 확인.

### Step 3: ingest.json 작성

스키마: `$RHWP_REPO/tools/rhwp-ingest/schema/ingest_schema_v1.json`
샘플: 같은 디렉토리의 `sample_minimal.json`, `sample_structured.json`

공통 최상위 필드:

```jsonc
{
  "version": "1",
  "page_size": {"width_mm": 210, "height_mm": 297},
  "default_font": "함초롬바탕",
  "header_text": "…",     // 반복 머리말 (선택)
  "footer_text": "…",     // 반복 꼬리말 (선택)
  "passages": [...],       // 모드 A 공유 지문
  "questions": [...]       // 필수 (모드 B에서도 사용 — 아래 참조)
}
```

### Step 4: 이미지 자르기

Step 2에서 추정한 bbox(픽셀, 좌상단 기준)마다:

```bash
bash $HELPERS/crop_image.sh "$TMP/page_001.png"     "$MEDIA_DIR/img/name.png"
```

### Step 5: HWPX 빌드 + 후처리 (후처리 2단계 모두 필수!)

```bash
$RHWP_BIN build-from-ingest "$TMP/ingest.json" --media-dir "$MEDIA_DIR" -o out.hwpx
python3 $HELPERS/embed_images.py out.hwpx "$TMP/ingest.json" "$MEDIA_DIR"   # 이미지 실제 임베드
python3 $HELPERS/embed_tables.py out.hwpx "$TMP/tables.json"               # #tbl: → 편집 가능한 실제 표
python3 $HELPERS/apply_styles.py out.hwpx                                  # 마커 → 제목/볼드 스타일 + 꼬리말 정규화
python3 $HELPERS/postprocess_hwpx.py out.hwpx --margins 20,20,20,15        # lineseg/테두리 정정 + 여백
```

**여백(--margins L,R,T,B[,머리말,꼬리말], mm 단위)**: 원본 PDF의 여백을 **측정해서 그대로** 적용한다. 페이지 PNG에서 본문 텍스트의 시작 x/y와 끝 위치를 읽고 `mm = px ÷ (이미지폭px ÷ 210)`으로 환산. (빌더 기본값은 좌우 30mm 한컴 기본 — 원본과 다르면 반드시 교정. 시험지 모드는 생략 가능.)

**후처리가 필수인 이유** (rhwp v0.7.18 기준 build-from-ingest 산출물 문제, E2E 검증됨):
1. image 블록이 `[이미지: ref]` 플레이스홀더 텍스트로만 출력되고 BinData 미포함(이슈 #182) → `embed_images.py`가 플레이스홀더를 인라인 ``으로 치환하고 PNG를 BinData/에 추가.
2. 모든 문단이 단일 스타일(10pt JUSTIFY)로 출력됨 → `apply_styles.py`가 텍스트 마커(아래 참조)를 해석해 제목/부제/캡션/볼드 charPr·paraPr를 주입.
3. 문단마다 lineseg 캐시가 1개로 저장돼 긴 문단이 줄바꿈 없이 한 줄로 압축 렌더링됨 → linesegarray 제거로 reflow 강제.
4. 일반 문단 borderFill(id=1)이 SOLID로 생성돼 모든 문단에 테두리 박스가 그려짐 → NONE 처리. boxed 블록(id=2)은 유지됨.

순서 주의: 반드시 `embed_images.py` → `embed_tables.py` → `apply_styles.py` → `postprocess_hwpx.py` 순서로 실행 (표가 없으면 embed_tables 생략 가능).
(구현 참고: rhwp 파서는 charPr/paraPr id를 무시하고 배열 인덱스로 참조하므로 apply_styles.py는 연속 인덱스 id를 사용한다.)

### Step 6: 검증

```bash
$RHWP_BIN dump out.hwpx        # IR 구조 확인
unzip -l out.hwpx              # BinData/ 이미지 포함 여부
$RHWP_BIN export-pdf out.hwpx -o "$TMP/roundtrip.pdf"   # 렌더링 시각 확인 (한컴 불필요)
```

roundtrip.pdf를 `pdftoppm`으로 PNG 변환 후 Read로 직접 보고 원본과 비교한다.

성공 시 결과 파일 경로 + 통계(문제/문단 수, 바이트)를 보고한다.

---

## 모드 A: 시험지 (수능/모의고사/학원 시험지)

페이지별로 다음을 인식해 `questions[]`로 구조화:

- **문제 번호**: "1.", "2.", "1)" 등 stem 시작 마커.
- **지문(stem)**: 문제 번호 다음 본문. `stem_blocks`로 블록 시퀀스 표현.
- **선택지(choices)**: ①~⑤로 시작하는 줄. label 그대로 포함.
- **이미지/도형/그래프**: bbox=[x,y,w,h] 추정, 의미적 placement 결정:
  - `between`(지문↔선택지 사이, 가장 흔함) / `above` / `below` / `inline`

```jsonc
{
  "number": 1,
  "passage_ref": "p1-3",            // 공유 지문 참조 (선택)
  "stem": "다음 글의 주제로 가장 적절한 것은?",
  "auto_number": true,               // 빌더가 "1. " prefix 자동 추가
  "stem_blocks": [
    {"type": "text", "text": "다음 글의 주제로 가장 적절한 것은?"},
    {"type": "boxed", "title": "", "blocks": [{"type": "text", "text": "보기 본문"}]},
    {"type": "image", "ref": "img/q1.png", "placement": "between"}
  ],
  "choices": [
    {"label": "①", "text": "…"}, {"label": "②", "text": "…"},
    {"label": "③", "text": "…"}, {"label": "④", "text": "…"}, {"label": "⑤", "text": "…"}
  ],
  "media": [{"id": "img/q1.png", "natural_w": 800, "natural_h": 600, "target_w_mm": 80, "placement": "between"}]
}
```

규칙:
- **공유 지문**: 여러 문제가 같은 지문을 쓰면 top-level `passages[]`에 한 번 작성, 각 문제에서 `passage_ref` 참조. 빌더가 첫 참조 위치에 한 번만 출력.
- **`[1~3] 다음 글을 읽고…` 그룹 지시문**: `passages[].blocks`에 작성.
- **``/`[보기]` 박스**: `stem_blocks`에 `{"type":"boxed","title":"","blocks":[...]}`.
- **`auto_number`**: 기본 `true`(빌더가 `{number}. ` prefix 자동). stem 텍스트에 번호를 이미 썼다면 `false`로 중복 방지.
- `form_label` 필드로 "홀수형/짝수형" 표기 전달 가능.

## 모드 B: 일반 문서 (보고서/공문/안내문 등)

ingest 스키마는 시험지 형태지만, **문단을 번호 없는 question으로 매핑**하면 일반 문서도 변환된다 (Rust 수정 불필요, 검증됨):

- 각 논리 블록(제목, 문단, 이미지 그룹) = question 1개:
  ```jsonc
  {
    "number": 1,                 // 순번 (출력 안 됨)
    "stem": "",                  // stem_blocks 사용 시 fallback일 뿐
    "auto_number": false,        // 번호 prefix 끔 — 필수!
    "choices": [],               // 빈 배열 — 선택지 출력 없음
    "stem_blocks": [
      {"type": "text", "text": "1. 개요"},
      {"type": "text", "text": "본 문서는 …"},
      {"type": "image", "ref": "img/fig1.png", "placement": "below"}
    ],
    "media": [{"id": "img/fig1.png", "natural_w": 1200, "natural_h": 800, "target_w_mm": 120, "placement": "below"}]
  }
  ```
- question 사이에는 빌더가 빈 문단을 자동 삽입 → 문단 간격이 된다. 촘촘한 연속 문단은 한 question의 `stem_blocks`에 여러 text 블록으로 몰아넣는다.
- 문서 머리말/꼬리말 → 최상위 `header_text`/`footer_text`.
- 원본 목차 번호("1.", "가.", "(1)")는 텍스트에 그대로 포함 (`auto_number: false`이므로 중복 없음).

### 스타일 마커 — 원본 타이포그래피를 그대로 재현 (모드 B 품질의 핵심!)

**철학: PDF를 재구성하지 않는다. 원본의 크기·굵기·색·정렬·들여쓰기를 측정해서 그대로 옮긴다.**

**범용 마커 `#s:토큰,...# ` (기본 도구 — 이것을 우선 사용):**

| 토큰 | 의미 | 예 |
|------|------|-----|
| 숫자 | 글자 크기 pt (소수 허용) | `14`, `11.5` |
| `b` | 굵게 | |
| `#RRGGBB` | 글자 색 | `#1F4E79` |
| `c` / `r` / `l` | 가운데/오른쪽/왼쪽 정렬 (기본: 양쪽) | |
| `i` | 왼쪽 들여쓰기 mm | `i7` |
| `sb` / `sa` | 문단 위/아래 여백 pt | `sb8`, `sa4` |

작성 예 (정부 문서의 전형적 위계):
```jsonc
{"type": "text", "text": "#s:14,b,sa4# □ 교육 대상 발굴 및 콘텐츠 개발"},
{"type": "text", "text": "#s:12,i7# ○ 공공언어 환경에 영향이 큰 교육 대상 발굴(7회, 213명)"},
{"type": "text", "text": "#s:10,i11# * 국제한국어교육학회, 이중언어학회"},
{"type": "text", "text": "#s:20,b,c# 표지 제목"},
{"type": "text", "text": "#s:9,#595959,c# [그림 1] 캡션"}
```

인라인 `**굵게**`는 어떤 문단에서든 동작한다 (#s: 문단이면 같은 크기/색의 굵게 변형이 자동 생성됨).

**페이지 나눔 `#pb#`**: 문단 텍스트 맨 앞에 붙이면 그 문단부터 새 페이지에서 시작한다. `#s:`와 조합 가능 (`"#pb##s:14,b# 2페이지 첫 제목"`). 원본 PDF의 페이지 경계를 그대로 유지할 때 각 원본 페이지의 첫 블록에 붙인다.

프리셋 마커(빠른 지정용, 원본 측정이 애매할 때만): `#t#` `#st#` `# ` `## ` `### ` `#c#` `#li# ` `#li2# ` `#cap#` — 크기·여백은 helpers/apply_styles.py 문서 참조.

꼬리말은 apply_styles.py가 자동으로 9pt 가운데 정렬로 정규화한다 (빌더의 댕글링 charPr 참조 문제 정정).

**이미지 문단도 자동 정규화된다**: ``이 든 문단은 줄간격 100%·가운데 정렬 전용 paraPr로 교체된다. 본문 줄간격(160%)이 이미지 라인에 적용되면 한컴에서 이미지 높이의 60%만큼 위아래 여백이 생기기 때문 — 이 여백 문제로 이미지 크기를 줄이려 하지 말 것.

### 원본 재현 규칙 (최대한 똑같게)

1. **기호·불릿 보존**: □·■·○·※·\* 등 원본의 모든 기호를 텍스트에 그대로 유지한다. 마커는 크기/굵기/들여쓰기만 지정한다. (예: "□ 교육 대상 발굴" → `#s:14,b# □ 교육 대상 발굴` — □를 절대 지우지 않는다)
2. **크기·색은 측정**: 페이지 PNG에서 원본 글자 높이를 본문 대비 비율로 가늠해 pt를 정한다 (본문 10~12pt 기준). 색상 텍스트는 `#RRGGBB`로 재현.
3. **여백도 원본에서 측정**: 페이지 PNG에서 본문 시작/끝 위치를 재서 mm로 환산 (`px ÷ (이미지폭px/210)`) → `postprocess_hwpx.py --margins L,R,T,B`. 임의의 표준값을 강요하지 않는다.
4. **들여쓰기 측정**: 각 위계의 들여쓰기를 제목 기준선 대비 mm로 재서 `i`으로 지정.
5. **문단 단위만 병합**: PDF 렌더링상의 줄바꿈은 문단 병합하되(한 문장 = 한 블록), 원본이 의도한 줄 구분(주소·연락처 줄 등)은 유지.
6. **표는 실제 편집 가능한 표로**: 표 위치에 `{"type":"text","text":"#tbl:#"}` 블록을 넣고, `$TMP/tables.json`에 표 정의를 작성한다 (스키마는 `helpers/embed_tables.py` 상단 문서 참조). **색감도 원본에서 측정**해 지정: 헤더 행 배경(`bg`)·글자색(`color`)·굵게, 라벨 열 배경, 격자선 색(`border_color`). 셀 안 `\n`(셀 내 다중 문단)과 `**굵게**`, `colspan` 병합 지원. rowspan·셀별 개별 테두리·대각선은 미지원 — 그런 표만 이미지 캡처로 fallback.
7. **카드·콜아웃 박스는 텍스트 개요로 (기본)**: 로드맵/역할 카드 그리드, "💡 이렇게 해결합니다" 류의 강조 박스 모두 박스 모양을 유지하지 말고 **제목 + 들여쓰기 텍스트**로 푼다 (사용자 확인된 선호 — boxed 블록도 모드 B에서는 사용하지 않는다):
   ```jsonc
   {"type": "text", "text": "#s:11,b,#1F3864,sb4# 1차년도 (2026) — 기초 모델 설계"},
   {"type": "text", "text": "#s:10,i5# • 유사 데이터셋으로 정상 패턴 기준 수립"},
   ```
   원본 카드 모양을 꼭 유지해야 할 때만 색 배경 셀 표 사용: 5열 `[카드, 3mm 스페이서, 카드, 스페이서, 카드]`, 표 `border_color:"#FFFFFF"`, 카드 셀만 `bg`/`border_color`, **제목 행 + 본문 행 2행 구조** (⚠️ 한 셀에 크기가 다른 줄 혼합 시 rhwp 렌더러가 마지막 글자를 다음 줄로 떨어뜨림 — 셀당 단일 크기 필수).
8. **재현 불가 장식은 이미지로**: 그라데이션 배너, 아이콘·일러스트가 섞인 다이어그램, 수식·도형은 bbox로 잘라 원본 그대로 이미지 삽입.
9. **반복 머리말/꼬리말**: 원본의 머리말/꼬리말/페이지 번호는 `header_text`/`footer_text`로 옮긴다 (본문에 중복 배치 금지).
10. **이미지 폭도 원본 비율**: 원본에서 그림이 차지하는 폭을 mm로 재서 `target_w_mm`에 그대로 사용.
11. **페이지 매핑 (다페이지 문서)**: 원본 1페이지 = question 1개로 병합하고(빌더가 question 사이에 빈 문단 스페이서를 넣어 페이지가 넘치는 것을 방지), 2페이지째부터는 각 페이지 첫 블록에 `#pb#`를 붙여 원본 페이지 경계를 유지한다. 섹션 사이 간격은 빈 문단 대신 헤딩의 `sb`(위 여백)로 준다 (페이지 중간 헤딩은 sb8~10 권장).
12. **페이지 하단 여유 필수 (한컴 리플로우 대비)**: 한컴은 rhwp 미리보기와 글자 폭·줄 높이 계산이 미세하게 달라, 미리보기에서 페이지에 꽉 찬 콘텐츠는 한컴에서 다음 페이지로 밀린다. `#pb#`와 결합되면 "거의 빈 페이지 + 꼬리말만 두 번 보이는" 증상이 생긴다. **각 페이지 마지막 콘텐츠와 꼬리말 사이에 최소 15~20mm 여유**를 확보할 것 — 미리보기 PNG에서 여유가 안 보이면 그 페이지의 이미지 폭을 줄여서 맞춘다.

**모드 B 한계 (사용자에게 미리 고지):**
- 지원 서식: 글자 크기·굵게·색상, 정렬(양쪽/가운데/좌/우), 들여쓰기, 문단 여백, 인라인 굵게. 미지원: 밑줄, 기울임, 문단 배경색, 글머리표 자동 번호.
- 표는 `#tbl:` + tables.json으로 **편집 가능한 실제 표**로 삽입 (colspan·셀 배경/글자색·굵게 지원 / rowspan·대각선 미지원 → 그 경우만 이미지). 수식·색 배경 배너·다이어그램은 이미지 캡처.
- 다단(2단 조판) 원본은 읽기 순서대로 1단 흐름으로 재배치.

## Vision 분석 정확도 팁

- 스캔본이 흐리면 더 선명한 원본/PDF를 요청.
- 한 페이지에 내용이 빽빽하면 페이지를 절반/사분면으로 crop해서 나눠 분석.
- `text.txt`(pdftotext 결과)와 교차 검증해 숫자·고유명사 오탈자 방지.
- bbox는 300 DPI 기준 픽셀 좌표. A4 300DPI = 2480×3508px 기준으로 비례 추정.

## 알려진 한계

- 이미지는 `embed_images.py` 후처리로 임베드된다(rhwp 자체 직렬화는 #182로 미지원). 생성 후 `unzip -l`로 BinData 포함 여부와 `export-pdf` 렌더링을 반드시 확인.
- 복잡한 수식(LaTeX 수준)은 이미지 캡처가 안전. HWP Equation 매핑은 rhwp 후속 마일스톤.
- 큰 인라인 이미지가 페이지 경계에 걸리면 rhwp export-pdf 미리보기에서 꼬리말과 살짝 겹쳐 보일 수 있음 — 한컴에서는 리플로우되므로 실제 파일은 정상.

## 의존성

- `$RHWP_BIN` (필수) — `cargo build --release` 산출물
- `pdftoppm`/`pdftotext` (poppler) — PDF 입력
- `magick` (ImageMagick) — 이미지 자르기
- `python3` + `python-docx` (선택) — DOCX 입력

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [yoonsoli](https://github.com/yoonsoli)
- **Source:** [yoonsoli/pdf-to-hwp](https://github.com/yoonsoli/pdf-to-hwp)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-yoonsoli-pdf-to-hwp-pdf-to-hwp
- Seller: https://agentstack.voostack.com/s/yoonsoli
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
