ownhwpx
v0.2.1
Published
HWPX file format reader/writer - TypeScript port of hwpxlib. Read, write, and manipulate HWPX (Hancom Word) documents.
Maintainers
Readme
ownhwpx
HWPX file format reader/writer — TypeScript port of hwpxlib (Java) by Park.sungkyon, with extended features for Node.js environments. Built with reference to Hancom's public HWPX specification.
Read, write, and manipulate HWPX (Hancom Word) documents in Node.js.
Features
Core (from hwpxlib)
- ✅ Read HWPX files — from disk (
fromFilepath), from URL (fromUrl), or from Buffer - ✅ Write HWPX files — to disk (
toFilepath), to Buffer (toBytes), or to any Writable stream (toStream) - ✅ Roundtrip — Read → Modify → Write preserves document structure
- ✅ Tables — Rows, cells, nested paragraphs inside table cells
- ✅ Drawing objects — Picture, Rectangle, Ellipse, Line, Arc, Curve, Polygon, ConnectLine, Equation, TextArt, OLE, Video, Container
- ✅ Form controls — Edit, ComboBox, ListBox, ScrollBar, Button, CheckButton, RadioButton
- ✅ Ctrl items — Header/Footer, FootNote/EndNote, FieldBegin/FieldEnd, Bookmark, PageNum, Indexmark, HiddenComment, AutoNum/NewNum, PageHiding, ColPr
Extended (TypeScript-only additions)
- ✅ Text extraction — Extract plain text from documents, with optional paragraph numbering
- ✅ Object search — Find any element by type or field name throughout the document
- ✅ Field finder — Locate field begin/end pairs by field name
- ✅ Blank document creation — Create new empty HWPX documents from scratch
- ✅ Template engine — Variable substitution (
{{name}}), conditionals ({{#if}}), loops ({{#each}}), date formatting - ✅ Table of contents — Auto-generate TOC from heading paragraphs
- ✅ Table from array — Programmatically create tables from 2D data with colspan/rowspan support
- ✅ Horizontal rules — Insert line separator drawing objects
- ✅ Section breaks — Create and insert new sections programmatically
- ✅ Paragraph numbering — Auto-number headings (Outline/Bullet/Numbering)
- ✅ TypeScript — Full type definitions, strict mode, 825+ source files
- ✅ Zero compilation errors — Complete type safety
- ✅ Unit tested — 121 tests across 3 test suites (Vitest)
Installation
npm install ownhwpxQuick Start
import { HWPXReader, HWPXWriter, BlankFileMaker, TextExtractor, TextExtractMethod } from 'ownhwpx';
// Read an existing HWPX file
const doc = HWPXReader.fromFilepath('document.hwpx');
console.log(`Sections: ${doc.sectionXMLFileList.length}`);
// Access paragraphs
const section = doc.sectionXMLFileList.get(0);
for (let i = 0; i < section.paraListCore.count; i++) {
const para = section.paraListCore.getPara(i);
console.log(`Paragraph ${i}: ${para.runList.length} runs`);
}
// Write back to a new file
await HWPXWriter.toFilepath(doc, 'output.hwpx');
// Create a blank document
const blank = BlankFileMaker.make();
await HWPXWriter.toFilepath(blank, 'blank.hwpx');
// Extract text with paragraph headings
const text = TextExtractor.extract(doc, TextExtractMethod.AppendControlTextAfterParagraphText, true, null);
console.log(text);API Overview
Reader
import { HWPXReader } from 'ownhwpx';
// From disk
const doc = HWPXReader.fromFilepath('document.hwpx');
// From URL (Node.js 18+)
// const doc = await HWPXReader.fromUrl('https://example.com/doc.hwpx');
// Document structure
doc.sectionXMLFileList // sections
doc.headerXMLFile // document header (styles, fonts, etc.)
doc.settingsXMLFile // document settings
doc.containerXMLFile // OCF container
doc.manifestXMLFile // file manifest
doc.contentHPFFile // content package
// Section content
const section = doc.sectionXMLFileList.get(0);
for (const para of section.paraListCore) {
for (const run of para.runList) {
for (const item of run.itemList) {
// item can be T (text), Ctrl (control), Table, Picture, etc.
console.log(item.objectType());
}
}
}Writer
import { HWPXWriter } from 'ownhwpx';
import * as fs from 'fs';
// Write to file
await HWPXWriter.toFilepath(doc, 'output.hwpx');
// Write to buffer
const buffer = await HWPXWriter.toBytes(doc);
// Write to any Writable stream (HTTP response, S3 upload, etc.)
await HWPXWriter.toStream(doc, fs.createWriteStream('output.hwpx'));ObjectFinder
import { ObjectFinder } from 'ownhwpx';
import { ObjectType } from 'ownhwpx';
// Find all fieldBegin elements
const results = ObjectFinder.find(doc, {
isMatched: obj => obj.objectType() === ObjectType.hp_fieldBegin
}, false);
for (const r of results) {
console.log(r.thisObject);
}TextExtractor
import { TextExtractor, TextExtractMethod } from 'ownhwpx';
// Basic text extraction
const text = TextExtractor.extract(doc, TextExtractMethod.AppendControlTextAfterParagraphText);
// With paragraph headings
const textWithHeads = TextExtractor.extract(doc, TextExtractMethod.AppendControlTextAfterParagraphText, true, null);BlankFileMaker
import { BlankFileMaker } from 'ownhwpx';
const blank = BlankFileMaker.make();
await HWPXWriter.toFilepath(blank, 'new_document.hwpx');TemplateEngine
import { TemplateEngine } from 'ownhwpx';
// Create a document with template markers
const blank = BlankFileMaker.make();
const section = blank.sectionXMLFileList.get(0);
const para = section.paraListCore.getPara(0);
para.addNewRun().addNewT().addText('Hello, {{name}}!');
// Render with context
TemplateEngine.render(blank, { name: 'World' });
// → "Hello, World!"TocMaker
import { TocMaker } from 'ownhwpx';
// Auto-generate TOC from heading paragraphs
const entries = TocMaker.generate(doc);
console.log(entries); // [{ level: 0, number: "1", text: "Introduction" }, ...]
// Insert TOC paragraphs at the start of section 0
TocMaker.insertToc(doc, { tocTitle: 'Table of Contents', maxLevel: 3 });TableFromArray
import { TableFromArray } from 'ownhwpx';
const table = TableFromArray.make([
['Name', 'Age', 'City'],
['Alice', '30', 'Seoul'],
['Bob', '25', 'Busan'],
], { headerRows: 1 });
// Insert into a paragraph
const para = section.paraListCore.getPara(0);
para.addNewRun().addItem(table);HrMaker
import { HrMaker } from 'ownhwpx';
import { LineWidth, LineType2 } from 'ownhwpx'; // re-exported from enums
const doc = BlankFileMaker.make();
const section = doc.sectionXMLFileList.get(0);
const para = section.paraListCore.getPara(0);
// Insert a horizontal rule
HrMaker.insertAfter(para, { thickness: LineWidth.MM_0_4, color: '#CCCCCC' });SectionBreakMaker
import { SectionBreakMaker } from 'ownhwpx';
// Append a new section with landscape layout
SectionBreakMaker.append(doc, { landscape: true });
// Insert a section at a specific position
SectionBreakMaker.insertAt(doc, 1, { marginTop: 3000 });FieldFinder
import { FieldFinder } from 'ownhwpx';
// Find all fields named "CustomerName"
const results = FieldFinder.find(doc, 'CustomerName', false);
for (const r of results) {
console.log(r.beginField?.name, r.endField?.beginIDRef);
}Test Reference
| 기능 | 테스트 파일 | 테스트 범위 |
|------|------------|------------|
| Reader/Writer 기초 | src/reader/HWPXReader.test.ts | 파일 읽기, 쓰기, 라운드트립, BlankFileMaker, TextExtractor, ObjectFinder |
| TocMaker | src/tool/toc/TocMaker.test.ts | TOC 생성, 제목 찾기, maxLevel, TOC 삽입 |
| 종합 테스트 | src/test/comprehensive.test.ts | 아래 전 범위 포함 |
| ├ 기본 텍스트 | BasicText | 텍스트 삽입, 멀티라인, 유니코드, Tab, 라운드트립 |
| ├ 문자 서식 | CharFormatting | charPrIDRef, charProperties 참조 |
| ├ 표 | Tables | 표 생성/속성/셀병합, CellAddr/CellSpan, SimpleTable 읽기 |
| ├ 그리기 개체 | DrawingObjects | Rectangle, Ellipse, Line, Arc, Curve, Polygon, ConnectLine, Picture, Equation, TextArt, Container, OLE, Video |
| ├ Run 아이템 | RunItems | Compose, Dutmal |
| ├ 폼 개체 | FormObjects | Buttons, ComboBox, Edit |
| ├ 페이지 기능 | PageFeatures | HeaderFooter, MultiColumn, PageFunctions, PageSize/Margin, ChangeTrack |
| ├ 라운드트립 | Roundtrip | 25개 reader_writer 테스트 파일 전체 |
| ├ 에러 파일 | ErrorFiles | 20개 에러 케이스 |
| ├ BlankFileMaker | BlankFileMaker | 생성, 문단/스타일/글꼴 확인, 라운드트립 |
| ├ TextExtractor | TextExtractor | sample1 텍스트 추출 |
| ├ ObjectFinder | ObjectFinder | 문단/Run/텍스트/섹션 검색, 다중 타입 |
| ├ ValueConvertor | ValueConvertor | boolean/정수/실수 변환 |
| └ TocMaker | TocMaker | 제목 찾기, maxLevel, TOC 삽입 |
# 특정 기능만 테스트하려면
npx vitest run -t "Tables"
npx vitest run -t "DrawingObjects"
npx vitest run -t "BlankFileMaker"
npx vitest run src/reader/HWPXReader.test.ts
# 전체 테스트
npm testPerformance
| Operation | Small (4KB) | Medium (138KB) | |-----------|-------------|-----------------| | Read | ~30ms | ~160ms | | Write | ~60ms | ~70ms | | Re-read | ~20ms | ~30ms |
Uses saxes SAX parser for efficient streaming XML parsing (4.2x faster than DOM-based parsing).
Document Model
HWPXFile
├── versionXMLFile
├── containerXMLFile
├── manifestXMLFile
├── contentHPFFile
├── headerXMLFile
│ ├── beginNum
│ ├── refList (fontfaces, styles, borderFills, etc.)
│ └── compatibleDocument
├── sectionXMLFileList[]
│ ├── paraListCore
│ │ └── Para[]
│ │ └── runList
│ │ └── Run[]
│ │ └── itemList
│ │ ├── T (text)
│ │ ├── Ctrl (fieldBegin, bookmark, etc.)
│ │ ├── Table (with rows/cells/nested paragraphs)
│ │ ├── Picture / Rectangle / Ellipse / Arc / ...
│ │ └── ComboBox / Edit / Button / ...
│ └── secPr (section properties: grid, page layout, notes, etc.)
├── settingsXMLFile
└── masterPageXMLFileList[]Limitations
HWPX 포맷 스펙상 근본적인 제약이 있어 완전한 구현이 불가능한 항목들입니다.
자동 목차 생성
TocMaker로 Heading 목록 수집 및 TOC paragraph 삽입은 가능하나, HWPX의 TOC 페이지 번호는 HWP Viewer가 렌더링 시 계산합니다. 라이브러리 레벨에서 정확한 페이지 번호를 알 수 없습니다.
수평선(구분선)
HWPX 스펙에 <hr> 개념이 없습니다. Line 벡터 도형으로 선을 삽입하거나 문단 테두리로 근사할 수 있으나, 완전한 <hr> 동작과는 차이가 있습니다.
구역 나누기
HWPX 구역은 별도 XML 파일 단위로 관리됩니다. MS Word처럼 문단 중간에 inline section break를 삽입하는 개념이 없습니다.
Project Status
| Layer | Files | Status | |-------|-------|--------| | Object Model | 329 | ✅ Complete | | Reader | 220 | ✅ Complete | | Writer | 108 | ✅ Complete | | Tool | 143 | ✅ Complete (Finder, TextExtractor, BlankMaker, Template, TOC, TableFromArray, Hr, SectionBreak) | | Total | 825+ | ✅ 0 compilation errors |
Requirements
- Node.js >= 18
- TypeScript >= 5.4 (for development)
Dependencies
saxes— SAX XML parser (ISC)archiver— ZIP creation (MIT)
Development
# Install dependencies
npm install
# Build
npm run build
# Test
npm test
# Watch mode
npm run test:watch
# Type check
npx tsc --noEmitLicense
Apache-2.0
This project is a TypeScript port of hwpxlib by Park.sungkyon (박성견), which is also Apache-2.0 licensed. See LICENSE for details.
