Skip to main content

hwp

Use this skill whenever the user wants to do anything with HWP or HWPX files. This includes reading or extracting text/tables/images from HWP files, converting HWP to markdown or other formats, creating HWPX files from markdown/text, parsing HWP binary (OLE2/CFB) or HWPX (ZIP/XML) formats, and handling Korean document processing. If the user mentions a .hwp or .hwpx file, or asks about 한글 문서 processing, use this skill.

소스 정보

저장소
revfactory/skills
최근 소스 활동
2026년 4월 12일 04:07
감지된 SKILL.md 언어
영어
스타
160
포크
45

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
2 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
hwp
description
Use this skill whenever the user wants to do anything with HWP or HWPX files. This includes reading or extracting text/tables/images from HWP files, converting HWP to markdown or other formats, creating HWPX files from markdown/text, parsing HWP binary (OLE2/CFB) or HWPX (ZIP/XML) formats, and handling Korean document processing. If the user mentions a .hwp or .hwpx file, or asks about 한글 문서 processing, use this skill.
# HWP/HWPX Processing Guide ## Overview HWP is the native format for Hancom's "한글" word processor, widely used in South Korea. Two formats exist: | Format | Container | Detection | Extension | |--------|-----------|-----------|-----------| | **HWP (Binary)** | OLE2/CFB compound file | Magic bytes `0xD0 0xCF 0x11 0xE0` | `.hwp` | | **HWPX** | ZIP archive with XML | Magic bytes `0x50 0x4B` (PK) | `.hwpx` | For advanced patterns used in the MDView project (Web Workers, React hooks, progress tracking), see REFERENCE.md. ## Quick Start ### Node.js / TypeScript — Read HWP Binary ```typescript import * as CFB from 'cfb'; import pako from 'pako'; // Read HWP binary file const data = fs.readFileSync('document.hwp'); const cfb = CFB.read(data); // Check encryption const fileHeader = CFB.find(cfb, '/FileHeader'); if (fileHeader) { const flags = fileHeader.content[36] | (fileHeader.content[37] << 8); const isEncrypted = (flags & 0x02) !== 0; if (isEncrypted) throw new Error('Encrypted HWP files are not supported'); } // Check compression flag const isCompressed = (fileHeader.content[36] & 0x01) !== 0; // Read body sections const section = CFB.find(cfb, '/BodyText/Section0'); if (section && section.content) { let content = section.content; if (isCompressed) { content = pako.inflate(new Uint8Array(content)); } // Parse binary records from content... } ``` ### Node.js / TypeScript — Read HWPX ```typescript import JSZip from 'jszip'; const zip = await JSZip.loadAsync(fileBuffer); // Find section XML files const sectionFiles = Object.keys(zip.files) .filter(f => /Contents\/section\d+\.xml/i.test(f)) .sort(); for (const sectionFile of sectionFiles) { const xml = await zip.file(sectionFile)!.async('string'); // Parse XML to extract text from <hp:t> tags const textMatches = xml.match(/<hp:t[^>]*>([\s\S]*?)<\/hp:t>/g); // ... } // Extract images from BinData/ const imageFiles = Object.keys(zip.files) .filter(f => /BinData\//i.test(f) && !f.endsWith('/')); ``` ### Python — Read HWP Binary ```python import olefile import zlib import struct def read_hwp(filepath): ole = olefile.OleFileIO(filepath) # Check encryption header = ole.openstream('FileHeader').read() flags = struct.unpack_from('<I', header, 36)[0] if flags & 0x02: raise ValueError('Encrypted HWP files are not supported') is_compressed = bool(flags & 0x01) # Read body sections sections = [] for entry in ole.listdir(): path = '/'.join(entry) if path.startswith('BodyText/Section'): data = ole.openstream(path).read() if is_compressed: data = zlib.decompress(data, -15) sections.append(data) # Extract images from BinData images = {} for entry in ole.listdir(): path = '/'.join(entry) if path.startswith('BinData/'): img_data = ole.openstream(path).read() try: img_data = zlib.decompress(img_data, -15) except zlib.error: pass # Not compressed images[path] = img_data ole.close() return sections, images # Inline extended control chars: each occupies 8 UTF-16 code units (16 bytes). # After reading the 2-byte char code, skip 14 more bytes. EXTENDED_CTRL_CHARS = frozenset({ 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23 }) def extract_para_text(data): """Extract UTF-16 LE text from HWPTAG_PARA_TEXT record data.""" text = '' i = 0 while i < len(data) - 1: ch = struct.unpack_from('<H', data, i)[0] i += 2 if ch == 0x000D: break elif ch == 0x0009: text += '\t' elif ch == 0x000A: text += '\n' elif ch in EXTENDED_CTRL_CHARS: i += 14 # Skip 14 bytes of inline control data elif ch < 0x0020: continue else: text += chr(ch) return text def parse_records(data): """Parse binary records and extract text from HWPTAG_PARA_TEXT (tag_id=67).""" texts = [] offset = 0 while offset < len(data) - 4: header = struct.unpack_from('<I', data, offset)[0] offset += 4 tag_id = header & 0x3FF size = (header >> 20) & 0xFFF if size == 0xFFF: if offset + 4 > len(data): break size = struct.unpack_from('<I', data, offset)[0] offset += 4 if offset + size > len(data): break record_data = data[offset:offset + size] offset += size if tag_id == 67 and size > 0: text = extract_para_text(record_data) if text.strip(): texts.append(text) return texts ``` ### Python — Read HWPX ```python import zipfile import xml.etree.ElementTree as ET def read_hwpx(filepath): with zipfile.ZipFile(filepath, 'r') as z: # List section files section_files = sorted([ f for f in z.namelist() if f.startswith('Contents/section') and f.endswith('.xml') ]) text_content = [] for sf in section_files: xml_data = z.read(sf).decode('utf-8') root = ET.fromstring(xml_data) # Extract text from hp:t elements ns = {'hp': 'http://www.hancom.co.kr/hwpml/2011/paragraph'} for t in root.iter('{http://www.hancom.co.kr/hwpml/2011/paragraph}t'): if t.text: text_content.append(t.text) # Extract images images = { f: z.read(f) for f in z.namelist() if f.startswith('BinData/') and not f.endswith('/') } return text_content, images ``` ## HWP Binary Record Format HWP binary files store content as sequential records: ``` Record Header (4 bytes): - Tag ID: bits 0-9 (10 bits) — identifies record type - Level: bits 10-19 (10 bits) — nesting depth - Size: bits 20-31 (12 bits) — data size (if 0xFFF, next 4 bytes = actual size) ``` ### Key Record Tag IDs | Constant | Tag ID | Description | |----------|--------|-------------| | `HWPTAG_PARA_HEADER` | 66 | Paragraph header | | `HWPTAG_PARA_TEXT` | 67 | Paragraph text (UTF-16 LE) | | `HWPTAG_CTRL_HEADER` | 71 | Control header (tables, images) | | `HWPTAG_LIST_HEADER` | 72 | List header (cell content) | | `HWPTAG_TABLE` | 77 | Table definition | ### Text Encoding Paragraph text is encoded as **UTF-16 LE** with special control characters: | Char Code | Meaning | Type | |-----------|---------|------| | `0x0001-0x0008` | Inline extended controls (reserved, section, field start/end, etc.) | **Extended** — 8 UTF-16 code units total (16 bytes). Skip 14 bytes after the 2-byte char code. | | `0x0009` | Tab | Simple | | `0x000A` | Line break | Simple | | `0x000B` | Drawing/table placeholder | **Extended** — skip 14 bytes after char code | | `0x000C` | Reserved | **Extended** — skip 14 bytes after char code | | `0x000D` | Paragraph end | Simple (terminates text) | | `0x000E-0x0017` | Extended controls (hidden comment, header/footer, footnote, auto number, bookmark, etc.) | **Extended** — skip 14 bytes after char code | | `0x0020` | Space | Simple | | `0x00A0` | Non-breaking space | Simple | | `0x00AD` | Soft hyphen | Simple | > **CRITICAL**: Inline extended control characters (codes 1-8, 11-12, 14-23) each occupy **8 UTF-16 code units** (16 bytes total). After reading the 2-byte character code, you MUST skip the next **14 bytes** of control data. Failing to do so causes binary metadata to leak into extracted text as garbage characters (e.g., `捤獥汤捯`, `汫╨`, `ȃ`). ### Parsing Records (TypeScript) ```typescript interface HwpRecord { tagId: number; level: number; size: number; data: Uint8Array; } function parseRecords(buffer: Uint8Array): HwpRecord[] { const records: HwpRecord[] = []; let offset = 0; const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); while (offset < buffer.length - 4) { const header = view.getUint32(offset, true); offset += 4; const tagId = header & 0x3FF; const level = (header >> 10) & 0x3FF; let size = (header >> 20) & 0xFFF; if (size === 0xFFF) { size = view.getUint32(offset, true); offset += 4; } const data = buffer.slice(offset, offset + size); offset += size; records.push({ tagId, level, size, data }); } return records; } ``` ### Extracting Text from Records (TypeScript) ```typescript // Inline extended control chars: each occupies 8 UTF-16 code units (16 bytes). // After reading the 2-byte char code, skip 14 more bytes. const EXTENDED_CTRL_CHARS = new Set([ 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23 ]); function extractParaText(data: Uint8Array): string { const view = new DataView(data.buffer, data.byteOffset, data.byteLength); let text = ''; let i = 0; while (i < data.length - 1) { const ch = view.getUint16(i, true); i += 2; if (ch === 0x000D) break; // Paragraph end if (ch === 0x0009) { text += '\t'; continue; } if (ch === 0x000A) { text += '\n'; continue; } if (EXTENDED_CTRL_CHARS.has(ch)) { i += 14; // Skip 14 bytes of inline control data continue; } if (ch < 0x0020) continue; // Other control chars text += String.fromCharCode(ch); } return text; } ``` ## Creating HWPX Files HWPX is a ZIP archive with the following structure: ``` document.hwpx ├── mimetype (uncompressed, "application/hwp+zip") ├── META-INF/ │ └── manifest.xml (file entries) ├── Contents/ │ ├── content.hpf (package manifest) │ ├── header.xml (page layout, fonts, styles) │ └── section0.xml (main content) └── BinData/ ├── image1.jpg (embedded images) └── image2.png ``` ### Generate HWPX (TypeScript) ```typescript import JSZip from 'jszip'; async function generateHwpx( markdown: string, options: { paperSize?: 'A4' | 'Letter' | 'B5'; orientation?: 'portrait' | 'landscape'; margins?: { top: number; bottom: number; left: number; right: number }; // mm fontSize?: number; // pt fontFamily?: string; } = {} ): Promise<Blob> { const { paperSize = 'A4', orientation = 'portrait', margins = { top: 20, bottom: 20, left: 20, right: 20 }, fontSize = 10, fontFamily = '맑은 고딕', } = options; // Paper sizes in 1/100mm (HWPX unit) const paperSizes = { A4: { w: 21000, h: 29700 }, Letter: { w: 21590, h: 27940 }, B5: { w: 17600, h: 25000 }, }; const paper = paperSizes[paperSize]; const [w, h] = orientation === 'portrait' ? [paper.w, paper.h] : [paper.h, paper.w]; // Convert margins mm → 1/100mm const m = { top: margins.top * 100, bottom: margins.bottom * 100, left: margins.left * 100, right: margins.right * 100, };
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기