Install
$ agentstack add skill-yoonsoli-pdf-to-hwp-pdf-to-hwp ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
pdf-to-hwp — PDF → HWPX 변환 Skill
필수 경로
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: 입력 정규화
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
공통 최상위 필드:
{
"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 $HELPERS/crop_image.sh "$TMP/page_001.png" "$MEDIA_DIR/img/name.png"
Step 5: HWPX 빌드 + 후처리 (후처리 2단계 모두 필수!)
$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 검증됨):
- image 블록이
[이미지: ref]플레이스홀더 텍스트로만 출력되고 BinData 미포함(이슈 #182) →embed_images.py가 플레이스홀더를 인라인 ``으로 치환하고 PNG를 BinData/에 추가. - 모든 문단이 단일 스타일(10pt JUSTIFY)로 출력됨 →
apply_styles.py가 텍스트 마커(아래 참조)를 해석해 제목/부제/캡션/볼드 charPr·paraPr를 주입. - 문단마다 lineseg 캐시가 1개로 저장돼 긴 문단이 줄바꿈 없이 한 줄로 압축 렌더링됨 → linesegarray 제거로 reflow 강제.
- 일반 문단 borderFill(id=1)이 SOLID로 생성돼 모든 문단에 테두리 박스가 그려짐 → NONE 처리. boxed 블록(id=2)은 유지됨.
순서 주의: 반드시 embed_images.py → embed_tables.py → apply_styles.py → postprocess_hwpx.py 순서로 실행 (표가 없으면 embedtables 생략 가능). (구현 참고: rhwp 파서는 charPr/paraPr id를 무시하고 배열 인덱스로 참조하므로 applystyles.py는 연속 인덱스 id를 사용한다.)
Step 6: 검증
$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
{
"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 |
작성 예 (정부 문서의 전형적 위계):
{"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%만큼 위아래 여백이 생기기 때문 — 이 여백 문제로 이미지 크기를 줄이려 하지 말 것.
원본 재현 규칙 (최대한 똑같게)
- 기호·불릿 보존: □·■·○·※·\* 등 원본의 모든 기호를 텍스트에 그대로 유지한다. 마커는 크기/굵기/들여쓰기만 지정한다. (예: "□ 교육 대상 발굴" →
#s:14,b# □ 교육 대상 발굴— □를 절대 지우지 않는다) - 크기·색은 측정: 페이지 PNG에서 원본 글자 높이를 본문 대비 비율로 가늠해 pt를 정한다 (본문 10~12pt 기준). 색상 텍스트는
#RRGGBB로 재현. - 여백도 원본에서 측정: 페이지 PNG에서 본문 시작/끝 위치를 재서 mm로 환산 (
px ÷ (이미지폭px/210)) →postprocess_hwpx.py --margins L,R,T,B. 임의의 표준값을 강요하지 않는다. - 들여쓰기 측정: 각 위계의 들여쓰기를 제목 기준선 대비 mm로 재서
i으로 지정. - 문단 단위만 병합: PDF 렌더링상의 줄바꿈은 문단 병합하되(한 문장 = 한 블록), 원본이 의도한 줄 구분(주소·연락처 줄 등)은 유지.
- 표는 실제 편집 가능한 표로: 표 위치에
{"type":"text","text":"#tbl:#"}블록을 넣고,$TMP/tables.json에 표 정의를 작성한다 (스키마는helpers/embed_tables.py상단 문서 참조). 색감도 원본에서 측정해 지정: 헤더 행 배경(bg)·글자색(color)·굵게, 라벨 열 배경, 격자선 색(border_color). 셀 안\n(셀 내 다중 문단)과**굵게**,colspan병합 지원. rowspan·셀별 개별 테두리·대각선은 미지원 — 그런 표만 이미지 캡처로 fallback. - 카드·콜아웃 박스는 텍스트 개요로 (기본): 로드맵/역할 카드 그리드, "💡 이렇게 해결합니다" 류의 강조 박스 모두 박스 모양을 유지하지 말고 제목 + 들여쓰기 텍스트로 푼다 (사용자 확인된 선호 — boxed 블록도 모드 B에서는 사용하지 않는다):
``jsonc {"type": "text", "text": "#s:11,b,#1F3864,sb4# 1차년도 (2026) — 기초 모델 설계"}, {"type": "text", "text": "#s:10,i5# • 유사 데이터셋으로 정상 패턴 기준 수립"}, ` 원본 카드 모양을 꼭 유지해야 할 때만 색 배경 셀 표 사용: 5열 [카드, 3mm 스페이서, 카드, 스페이서, 카드], 표 bordercolor:"#FFFFFF", 카드 셀만 bg/bordercolor`, 제목 행 + 본문 행 2행 구조 (⚠️ 한 셀에 크기가 다른 줄 혼합 시 rhwp 렌더러가 마지막 글자를 다음 줄로 떨어뜨림 — 셀당 단일 크기 필수).
- 재현 불가 장식은 이미지로: 그라데이션 배너, 아이콘·일러스트가 섞인 다이어그램, 수식·도형은 bbox로 잘라 원본 그대로 이미지 삽입.
- 반복 머리말/꼬리말: 원본의 머리말/꼬리말/페이지 번호는
header_text/footer_text로 옮긴다 (본문에 중복 배치 금지). - 이미지 폭도 원본 비율: 원본에서 그림이 차지하는 폭을 mm로 재서
target_w_mm에 그대로 사용. - 페이지 매핑 (다페이지 문서): 원본 1페이지 = question 1개로 병합하고(빌더가 question 사이에 빈 문단 스페이서를 넣어 페이지가 넘치는 것을 방지), 2페이지째부터는 각 페이지 첫 블록에
#pb#를 붙여 원본 페이지 경계를 유지한다. 섹션 사이 간격은 빈 문단 대신 헤딩의sb(위 여백)로 준다 (페이지 중간 헤딩은 sb8~10 권장). - 페이지 하단 여유 필수 (한컴 리플로우 대비): 한컴은 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
- Source: yoonsoli/pdf-to-hwp
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.