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で見る