hwp-convert 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +185 -0
  2. package/LICENSE +25 -0
  3. package/NOTICE +23 -0
  4. package/README.md +338 -0
  5. package/dist/browser/hwp-convert.browser.mjs +20677 -0
  6. package/dist/browser/hwp-convert.browser.mjs.map +7 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +267 -0
  9. package/dist/index.d.ts +5 -0
  10. package/dist/index.js +5 -0
  11. package/dist/lib/errors.d.ts +9 -0
  12. package/dist/lib/errors.js +18 -0
  13. package/dist/lib/hwp/binData.d.ts +15 -0
  14. package/dist/lib/hwp/binData.js +64 -0
  15. package/dist/lib/hwp/bodyText.d.ts +31 -0
  16. package/dist/lib/hwp/bodyText.js +208 -0
  17. package/dist/lib/hwp/byteReader.d.ts +40 -0
  18. package/dist/lib/hwp/byteReader.js +116 -0
  19. package/dist/lib/hwp/cfbReader.d.ts +44 -0
  20. package/dist/lib/hwp/cfbReader.js +134 -0
  21. package/dist/lib/hwp/control.d.ts +17 -0
  22. package/dist/lib/hwp/control.js +290 -0
  23. package/dist/lib/hwp/converter.d.ts +22 -0
  24. package/dist/lib/hwp/converter.js +41 -0
  25. package/dist/lib/hwp/docInfo.d.ts +26 -0
  26. package/dist/lib/hwp/docInfo.js +396 -0
  27. package/dist/lib/hwp/fileHeader.d.ts +42 -0
  28. package/dist/lib/hwp/fileHeader.js +66 -0
  29. package/dist/lib/hwp/htmlReader.d.ts +17 -0
  30. package/dist/lib/hwp/htmlReader.js +602 -0
  31. package/dist/lib/hwp/hwpxBuilder.d.ts +19 -0
  32. package/dist/lib/hwp/hwpxBuilder.js +633 -0
  33. package/dist/lib/hwp/index.d.ts +68 -0
  34. package/dist/lib/hwp/index.js +149 -0
  35. package/dist/lib/hwp/mdReader.d.ts +16 -0
  36. package/dist/lib/hwp/mdReader.js +485 -0
  37. package/dist/lib/hwp/mdWriter.d.ts +23 -0
  38. package/dist/lib/hwp/mdWriter.js +182 -0
  39. package/dist/lib/hwp/owpml.d.ts +33 -0
  40. package/dist/lib/hwp/owpml.js +86 -0
  41. package/dist/lib/hwp/record.d.ts +24 -0
  42. package/dist/lib/hwp/record.js +59 -0
  43. package/dist/lib/hwp/tags.d.ts +115 -0
  44. package/dist/lib/hwp/tags.js +217 -0
  45. package/dist/lib/hwp/types.d.ts +214 -0
  46. package/dist/lib/hwp/types.js +5 -0
  47. package/dist/lib/hwpxReader.d.ts +60 -0
  48. package/dist/lib/hwpxReader.js +1104 -0
  49. package/dist/lib/types.d.ts +47 -0
  50. package/dist/lib/types.js +1 -0
  51. package/dist/lib/writer.d.ts +19 -0
  52. package/dist/lib/writer.js +149 -0
  53. package/package.json +94 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,185 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0 — 2026-06-21
4
+
5
+ [hwpxjs](https://github.com/ssabro/hwpxjs)(MIT, ssabro) 를 fork·확장한 독립 패키지 `hwp-convert` 의 첫 릴리스. HWP 바이너리 파서 구조는 [rhwp](https://github.com/edwardkim/rhwp)(MIT) 포팅. 자세한 출처는 [NOTICE](./NOTICE) 참조.
6
+
7
+ ### 한컴 호환 HWPX 생성 (핵심)
8
+
9
+ 원본은 생성한 HWPX 가 한컴오피스(한글)에서 열리지 않았다. 이를 실제 한글로 재현·수정:
10
+
11
+ - mimetype `application/owpml` → `application/hwp+zip`
12
+ - OWPML 속성의 네임스페이스 prefix 제거 (요소만 prefix, 속성 무prefix)
13
+ - 첫 문단 `<hp:secPr>`(페이지 설정) 추가 + 풀 네임스페이스/version/secCnt
14
+ - 폰트 `face`, `tabProperties`, `<hp:p>` 고유 id, spine `header` itemref
15
+ - 표 한컴 구조(`cellAddr`/`cellSpan`/`cellSz` + `sz`/`pos`), HTML 병합표 좌표 occupancy-grid
16
+ - reader: `hwp+zip` mimetype 허용 + 표 span 구형/신형 모두 읽기
17
+
18
+ ### 로컬 파일 이미지 임베드
19
+
20
+ - `imageResolver` 옵션: `data:` URI 외에 `file://`·로컬/상대 경로 이미지를 임베드. 코어는 주입만 받아 브라우저 안전, CLI 는 fs resolver 자동 주입.
21
+ - 이미지 `<hp:pic>` 풀 구조(orgSz/curSz/renderingInfo/imgRect/sz/pos)
22
+
23
+ ### 기타
24
+
25
+ - 변환 정합성 테스트 추가 (owpml-compat / e2e / image-resolver), 총 109개 통과
26
+ - CLI 명령 `hwpconvert` / `hwp-convert`, 브라우저 번들 `dist/browser/hwp-convert.browser.mjs`
27
+
28
+ 상세 패치 노트: [docs/2026-06-21-hancom-hwpx-compat.md](./docs/2026-06-21-hancom-hwpx-compat.md)
29
+
30
+ ---
31
+
32
+ 아래는 fork 이전 hwpxjs 의 이력입니다.
33
+
34
+ ## 0.4.0
35
+
36
+ Markdown / HTML 양방향 변환과 BorderFill 풀 보존을 추가한 릴리스. 0.3.0 대비 사용 시나리오가 크게 확장됐고 새 의존성(`marked`, `htmlparser2`)이 추가됐기 때문에 minor 버전 bump.
37
+
38
+ ### Markdown / HTML 양방향 변환
39
+
40
+ - **HWP/HWPX → Markdown** (`hwpToMarkdown`, `HwpxReader.extractMarkdown`):
41
+ - 본문/표/이미지/굵게/기울임 보존
42
+ - 표는 마크다운 테이블 (병합 셀은 평탄화)
43
+ - 이미지 `![](BinData/imageN.ext)` 또는 `embedImages: true` 시 base64 data URI
44
+ - **Markdown → HWPX** (`markdownToHwpx`):
45
+ - `marked` lexer 토큰 트리 → HwpDocument IR → hwpxBuilder
46
+ - 헤딩 1~6 (큰 사이즈 + 굵게), 인라인 `**bold**`/`*em*`/`` `code` ``, 리스트(ordered/unordered), blockquote, 표, 이미지(data URI 만 보존), 코드 블록(모노스페이스)
47
+ - **HTML → HWPX** (`htmlToHwpx`):
48
+ - `htmlparser2` SAX → DOM 트리 → IR → hwpxBuilder
49
+ - p/h1~6/strong/em/b/i/code/pre/ul/ol/li/blockquote/table(rowspan,colspan)/img(data URI)/br/hr 처리
50
+ - script/style/head 무시, HTML 엔티티 디코딩
51
+ - **CLI 신규 명령**: `md` / `hwp:md` / `md:hwpx` / `html:hwpx`
52
+
53
+ ### BorderFill 실 데이터 보존
54
+
55
+ - 4면 테두리(line type/width/color), 대각선 슬래시 방향(attr 비트필드), 단색 채우기(`<hh:fillBrush><hh:winBrush>`) 모두 HWPX 로 옮김
56
+ - 너비 인덱스 → mm 매핑 (HWP 5.0 스펙 16종)
57
+ - 라인 종류 18종 (NONE/SOLID/DASH/DOT/DOUBLE/THIN_THICK_DOUBLE/THICK_3D 등)
58
+ - `attr` 비트 2~4 = slash, 5~7 = backSlash 방향 정확히 디코딩
59
+
60
+ ### 머리말/꼬리말/각주 인라인 출력
61
+
62
+ - 본문 흐름에 paragraph 형태로 평탄 출력해서 텍스트 가시화 (master page 매핑은 향후)
63
+
64
+ ### 변경
65
+
66
+ - `HwpBorderFill` 타입 확장: `borders` (4면), `diagonal`, `fill` 추가
67
+ - `HwpDiagonalLine`, `HwpBorderLine`, `HwpSolidFill` 신규 타입
68
+ - `HwpxReader.parseXml` 가 `trimValues: false` — run 사이 공백 보존
69
+ - 의존성 추가: `marked` (Markdown 파서), `htmlparser2` (HTML SAX 파서)
70
+ - 브라우저 번들 크기: 540KB → 733KB (marked + htmlparser2 인라인)
71
+
72
+ ### 테스트
73
+
74
+ - 신규 `test/markdownHtml.test.ts` — 15 개 (MD/HTML → HWPX, MD writer)
75
+ - 전체: **85 tests / 9 files**
76
+
77
+ ## 0.3.0
78
+
79
+ HWP 5.0 바이너리 포맷 지원과 풀 충실도 HWPX 변환을 추가한 대규모 업데이트.
80
+
81
+ ### 추가된 기능
82
+
83
+ #### HWP 5.0 바이너리 파서
84
+
85
+ - CFB(OLE2) 컨테이너 리더 (`cfb` 의존, raw deflate 는 `pako`)
86
+ - `FileHeader` (시그니처/버전/11종 플래그)
87
+ - `DocInfo`: 7개 언어 그룹별 폰트(`FACE_NAME`), `CHAR_SHAPE`, `PARA_SHAPE`, `STYLE`, `BORDER_FILL`, `NUMBERING`, `BULLET`, `TAB_DEF`, `BIN_DATA` 레코드
88
+ - `BodyText`: `PARA_HEADER` / `PARA_TEXT` / `PARA_CHAR_SHAPE` 텍스트 + run 분할
89
+ - 표/도형 안의 nested `PARA_HEADER`(level≥2)도 본문 문단으로 평탄 추출
90
+ - 컨트롤 문자 (탭/줄끝/문단끝/NBSP/하이픈/figure space) 처리
91
+ - 인라인 컨트롤: 표 (rowSpan/colSpan + 셀 문단 재귀), 그림(GSO+SHAPE_PICTURE, `binDataId` 추출), 머리말/꼬리말/각주, 필드, 도형(SHAPE_LINE 좌표 보존, 사각형/타원/호/다각형/곡선은 종류만), 수식(EQEDIT 스크립트 UTF-16 추출)
92
+ - 임베디드 BinData 추출 (이미지/OLE storage prefix 보정)
93
+
94
+ #### 공개 API
95
+
96
+ - `parseHwp(bytes): HwpDocument` — IR 직접 접근
97
+ - `hwpToText(bytes, options?): string` — 평문 추출
98
+ - `hwpToHwpx(bytes, options?): Uint8Array` — HWPX 변환
99
+ - `detectFormat(bytes)` — `"hwp" | "hwpx" | "hwp3" | "unknown"`
100
+ - 에러: `HwpEncryptedError`, `HwpUnsupportedError`, `HwpInvalidFormatError`
101
+
102
+ #### CLI
103
+
104
+ - `hwp-convert hwp:txt <file.hwp>` — HWP 평문 출력
105
+ - `hwp-convert convert:hwp <file.hwp> <out.hwpx>` — HWP → HWPX
106
+ - `hwp-convert txt` 가 입력 확장자 보고 자동 라우팅 (`.hwp` 는 바이너리 파서)
107
+ - 기존: `inspect`, `txt`, `html`, `html:tpl`, `batch`, `batch:tpl`, `write:txt`
108
+
109
+ #### HWPX 변환 충실도 (HWP → HWPX)
110
+
111
+ - 표 → `<hp:tbl>` `<hp:tr>` `<hp:tc>` `<hp:subList>` (rowSpan/colSpan 보존)
112
+ - 이미지 → `BinData/imageN.{ext}` 패키징 + 매니페스트 등록 + `<hp:pic>` + `<hc:img binaryItemIDRef>` 인라인
113
+ - 다중 섹션 출력
114
+ - `header.xml refList` 풀 출력:
115
+ - `<hh:fontfaces>` 7 언어 그룹별 — 실제 사용 폰트(굴림/맑은 고딕/한컴 솔잎 B/함초롬돋움/휴먼명조 등) 정확히 보존
116
+ - `<hh:charProperties>` charShape 별 fontRef·textColor·shadeColor·height + bold/italic/underline/strikeout 요소
117
+ - `<hh:paraProperties>` paraShape 별 정렬(LEFT/RIGHT/CENTER/JUSTIFY/DISTRIBUTE) + margin + lineSpacing
118
+ - `<hh:styles>` 정의 + `paraPrIDRef`/`charPrIDRef`
119
+ - `<hh:borderFills>` / `<hh:numberings>` / `<hh:bullets>` / `<hh:tabPrs>` 카운트만큼 슬롯 출력 → paraShape 의 참조가 더 이상 dangle 하지 않음
120
+ - 번호 매기기 수준별 형식 문자열 (`^1.`, `^1)`) 보존
121
+ - 글머리표 문자(●/○/■ …) 보존
122
+ - section.xml 의 paragraph/run 이 실제 `paraShapeId`/`styleId`/`charShapeId` 를 참조 (이전엔 모두 0)
123
+ - 색상 변환: HWP `0xAABBGGRR` → CSS `#RRGGBB` (`colorBgrToHex`)
124
+ - `Preview/PrvText.txt` 자동 생성 (한컴 호환 형식, 표 셀은 `<셀텍스트 >` 패턴) — 다른 HWP 뷰어/탐색기에서 썸네일/미리보기 지원
125
+
126
+ #### HwpxReader / HwpxWriter 개선
127
+
128
+ - `HwpxWriter.createFromPlainText` 가 OWPML 패키지 규칙을 따르는 spec 호환 .hwpx 를 생성:
129
+ - mimetype 첫 STORED 엔트리, 정확히 `application/owpml`
130
+ - `META-INF/container.xml` + `META-INF/manifest.xml`
131
+ - 적절한 xmlns 선언 (hp/hh/hc/ha/opf/dc)
132
+ - 최소 충실도의 header.xml (charPr/paraPr/style 1세트)
133
+ - `HwpxReader.extractText` / `extractHtml` 이 `<hp:p><hp:run><hp:tbl>` 패턴의 paragraph-내장 표를 재귀 탐색 — `<hp:p>` 직속 텍스트가 비어도 셀 안 텍스트를 추출 (이전엔 PrvText 폴백 의존)
134
+ - `extractCellText` 가 `<hp:subList>` 안의 paragraph 도 정확히 추출
135
+ - `parseTagValue: false` — `<hp:t>1</hp:t>` 의 "1" 같은 숫자형 텍스트가 number 로 자동 변환되는 버그 수정
136
+ - 표 셀 rowSpan/colSpan 보존 (변환 → HTML 라운드트립 검증)
137
+ - 디버그용 `console.log`/`console.warn` 제거
138
+
139
+ #### 브라우저 ESM 번들
140
+
141
+ - `dist/browser/hwp-convert.browser.mjs` (esbuild, 약 540KB)
142
+ - 모든 의존성(jszip, cfb, fast-xml-parser, pako) 인라인
143
+ - `package.json` `exports.browser` 조건부 경로 + `./browser` 서브패스
144
+ - 사용 예: `<script type="module">import { hwpToText } from "hwp-convert/browser";</script>` 또는 jsdelivr CDN 직접 로드
145
+
146
+ #### 테스트 (vitest 도입)
147
+
148
+ 총 59 tests / 7 files. `npm test`, `npm run test:watch` 스크립트.
149
+
150
+ - 단위 (28 tests): byteReader, record 헤더(비트필드·확장 크기), fileHeader(11종 플래그), format detection, BGR→#RRGGBB
151
+ - 통합 (17 tests, 픽스처 자동 탐지): HwpxWriter spec 검증, 1.hwp 표 라운드트립, 여름휴가 안내문.hwp 이미지 BinData 매니페스트, 공고문(안) 셀 병합 + 스타일 정확성
152
+ - 견고성 (14 tests): 손상 입력(빈 버퍼/1바이트/잘린 CFB), HWP 3.0 친화적 에러, 미로드 reader, 487KB HWP 파싱 < 5초/변환 < 10초, HTML rowspan 라운드트립, BinData 매니페스트 mime 정확성
153
+
154
+ ### 변경
155
+
156
+ - `package.json`: description/keywords 갱신, `exports` 의 `browser` 조건 + `./browser` 서브패스 추가
157
+ - README 를 실제 공개 API 와 정합화 (아키텍처 개요·브라우저 사용법·rhwp 출처 강화)
158
+
159
+ ### 제거 / 깨진 약속 정리
160
+
161
+ - `HwpReader` / `HwpConverter` 더미 구현 제거 (하드코딩 샘플 텍스트 반환하던 자리)
162
+ - README 의 존재하지 않는 API (`loadFromFile`, `HwpxError`, `processingTime`, `inputPath` 등) 정리
163
+
164
+ ### 검증된 샘플
165
+
166
+ - `1.hwp` (24KB, 22행×9열 표 + secd/cold/gso) — 표 구조/텍스트 정상
167
+ - `여름휴가 안내문.hwp` (487KB, PNG+JPG 이미지 2개) — `BinData/image1.png` + `image2.jpg` 패키징, HTML base64 인라인 OK
168
+ - `공고문(안)(26.4.24.).hwp` (80KB, 6×6 셀 병합 표) — 라운드트립: 22 paragraphs / 30 table cells / rowspan 3개(rs=2/3/5) 보존, 숫자 셀("1"~"5") · 영문 차대번호 · 한글 차종 · ☎ 특수문자 모두 정상
169
+
170
+ ### 알려진 제한 (0.3.0 시점)
171
+
172
+ - 암호화 HWP 미지원 (`HwpEncryptedError`)
173
+ - 배포용 ViewText 미지원 (`HwpUnsupportedError`)
174
+ - HWP 3.0 미지원
175
+ - 머리말/꼬리말/각주는 파싱되나 본문 흐름 안에 출력하지 않음 (별도 master page 매핑은 다음 단계)
176
+ - BorderFill 정의는 ID 슬롯만 채움 — 실제 테두리 색/굵기/대각선/그라데이션 미보존
177
+ - 번호 매기기 수준별 시작번호는 기본 1
178
+ - 차트(CHART_DATA) / OLE / 글맵시 미지원
179
+ - 도형은 `line` 좌표만 보존, 사각형/타원/호/다각형/곡선은 종류만
180
+
181
+ ## 0.2.0
182
+
183
+ - 초기 공개 릴리스 (HWPX 한정)
184
+ - 텍스트/HTML 추출, 표/이미지 일부 지원, 템플릿/배치 변환
185
+ - CLI: `inspect` / `txt` / `html` / `html:tpl` / `batch` / `batch:tpl` / `write:txt`
package/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 shyang (hwp-convert)
4
+ Copyright (c) 2025 ssabro (hwpxjs — original work this project is forked from; https://github.com/ssabro/hwpxjs)
5
+ Copyright (c) 2025-2026 Edward Kim (rhwp — HWP 5.0 binary parser structure ported in src/lib/hwp/*; https://github.com/edwardkim/rhwp)
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
24
+
25
+
package/NOTICE ADDED
@@ -0,0 +1,23 @@
1
+ hwp-convert
2
+ Copyright (c) 2026 shyang
3
+
4
+ 이 프로젝트는 아래 MIT 라이선스 저작물을 기반으로 fork·확장한 것입니다.
5
+
6
+ 1. hwpxjs (https://github.com/ssabro/hwpxjs)
7
+ Copyright (c) 2025 ssabro — MIT License
8
+ 본 프로젝트의 원본. HWPX(OWPML) 읽기/쓰기, HWP→HWPX 변환, Markdown·HTML
9
+ 양방향 변환의 골격을 제공.
10
+
11
+ 2. rhwp (https://github.com/edwardkim/rhwp)
12
+ Copyright (c) 2025-2026 Edward Kim — MIT License
13
+ HWP 5.0(CFB/OLE2) 바이너리 파서 구조의 원작(Rust). src/lib/hwp/ 의 파서는
14
+ 이를 TypeScript 로 포팅한 것이며, 각 포팅 파일 헤더에 출처를 명시함.
15
+
16
+ ## hwp-convert 가 추가/변경한 것
17
+
18
+ - 생성되는 HWPX 가 한컴오피스(한글)에서 실제로 열리도록 OWPML 컨벤션 정합
19
+ (mimetype, 속성 네임스페이스, secPr, 표 셀 구조 등 — docs/ 참고)
20
+ - data URI + Node 로컬 파일(file://·경로) 이미지 임베드(imageResolver)
21
+ - 변환 정합성 테스트(owpml-compat / e2e / image-resolver)
22
+
23
+ 원본·포팅 저작물의 저작권 고지와 MIT 라이선스 조건을 모두 유지합니다. 전문은 LICENSE 참조.
package/README.md ADDED
@@ -0,0 +1,338 @@
1
+ # hwp-convert
2
+
3
+ HWP 5.0(바이너리/CFB)·HWPX(OWPML/ZIP+XML) ↔ Markdown/HTML **변환** 라이브러리.
4
+ Node.js 와 브라우저(ESM) 양쪽에서 동작하며, 별도의 한글(HWP) 클라이언트 의존성 없이 순수 TypeScript 만으로 처리합니다.
5
+
6
+ > **핵심**: 생성한 HWPX 가 **한컴오피스(한글)에서 실제로 열립니다.** Markdown·HTML 을 변환해도 표·이미지·서식이 보존된 채 정상 표시됩니다.
7
+
8
+ - HWPX 는 ZIP 패키지 내부에 OWPML XML 로 본문을 저장하는 개방형 포맷입니다. (참고: [HWPX 포맷 구조 살펴보기](https://tech.hancom.com/hwpxformat/))
9
+ - HWP 바이너리(CFB/OLE2) 파서는 [rhwp](https://github.com/edwardkim/rhwp) (Rust, MIT, Copyright (c) 2025-2026 Edward Kim) 의 구조를 TypeScript 로 포팅한 것입니다. 각 포팅 파일 헤더에 출처를 명시합니다.
10
+ - 이 프로젝트는 [hwpxjs](https://github.com/ssabro/hwpxjs) (MIT, Copyright (c) 2025 ssabro) 에서 **fork·확장**한 것입니다. 한컴 호환 HWPX 생성(OWPML 컨벤션 정합) 과 로컬 파일 이미지 임베드를 더했습니다. 출처·라이선스는 [LICENSE](./LICENSE) 와 [NOTICE](./NOTICE) 참조. 본 저장소도 동일한 MIT 라이선스를 따릅니다.
11
+
12
+ ## 주요 기능
13
+
14
+ - **HWPX 읽기**: 패키지 inspect, 평문/HTML/Markdown 추출 (이미지 인라인/리졸버 지원)
15
+ - **HWPX 쓰기**: OWPML 패키지 규칙(첫 STORED `mimetype`, `META-INF/container.xml`/`manifest.xml`, OPF spine) 호환 .hwpx 생성
16
+ - **HWP 5.0 파싱**: CFB(OLE2) + raw deflate 컨테이너 → FileHeader / DocInfo / BodyText 풀 파서
17
+ - **HWP → HWPX 변환**: 표(셀 병합 포함), 이미지(BinData 패키징), 폰트/문단/스타일 정의 보존, `Preview/PrvText.txt` 자동 생성
18
+ - **Markdown ↔ HWPX 양방향**: MD lexer → IR / IR → MD writer
19
+ - **HTML → HWPX**: `htmlparser2` 기반 DOM → IR
20
+ - **표·이미지·스타일 보존**: 글자 모양(굵게/기울임/밑줄/색/크기), 문단 모양(정렬/들여쓰기/줄간격), 7개 언어별 폰트 그룹, 번호/글머리표 형식 문자열까지 OWPML refList 로 옮겨 라운드트립 가능
21
+ - **CLI**: `inspect` / `txt` / `html` / `md` / `hwp:txt` / `hwp:md` / `html:tpl` / `batch` / `batch:tpl` / `write:txt` / `md:hwpx` / `html:hwpx` / `convert:hwp`
22
+ - **템플릿 처리**: `{{key}}` 텍스트 치환 (CLI / 라이브러리)
23
+ - **브라우저 ESM 번들**: `dist/browser/hwp-convert.browser.mjs` (esbuild, 약 730KB, 모든 의존성 인라인)
24
+
25
+ ### 변환 매트릭스
26
+
27
+ | 출발 \ 도착 | text | HTML | Markdown | HWPX | HWP |
28
+ | --- | --- | --- | --- | --- | --- |
29
+ | **HWP** | ✅ | ⚠️ HWPX 거쳐서 | ✅ `hwpToMarkdown` | ✅ `hwpToHwpx` | ─ |
30
+ | **HWPX** | ✅ `extractText` | ✅ `extractHtml` | ✅ `extractMarkdown` | ─ | ❌ |
31
+ | **Markdown** | ─ | ─ | ─ | ✅ `markdownToHwpx` | ❌ |
32
+ | **HTML** | ─ | ─ | ─ | ✅ `htmlToHwpx` | ❌ |
33
+ | **plain text** | ─ | ─ | ─ | ✅ `HwpxWriter` | ❌ |
34
+ | **PDF** | ❌ | ❌ | ❌ | ❌ | ❌ |
35
+
36
+ PDF 와 HWPX → HWP 역변환은 별도 도메인이라 미지원. PDF 가 필요하면 변환된 HWPX 를 LibreOffice (`libreoffice --headless --convert-to pdf`) 또는 헤드리스 Chrome 인쇄로 변환하시면 됩니다.
37
+
38
+ ## 설치
39
+
40
+ ```bash
41
+ pnpm add hwp-convert
42
+ # or
43
+ npm i hwp-convert
44
+ ```
45
+
46
+ 런타임 의존성: `cfb`, `fast-xml-parser`, `jszip`, `pako`. Node.js 18+ 권장.
47
+
48
+ ## 라이브러리 사용
49
+
50
+ ### HWPX 파일 읽기
51
+
52
+ ```ts
53
+ import HwpxReader from "hwp-convert";
54
+ import { readFile } from "node:fs/promises";
55
+
56
+ const buf = await readFile("./document.hwpx");
57
+ const reader = new HwpxReader();
58
+ await reader.loadFromArrayBuffer(
59
+ buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)
60
+ );
61
+
62
+ // 패키지 메타/매니페스트 정보
63
+ const info = await reader.getDocumentInfo();
64
+ // { metadata: { title?, creator?, created?, modified?, version?, caretPosition? },
65
+ // summary: { hasEncryptionInfo, contentsFiles, manifest?, spine? } }
66
+
67
+ // 텍스트 추출 (표 셀까지 재귀 탐색)
68
+ const text = await reader.extractText();
69
+
70
+ // 이미지 경로 목록 (BinData/ 내 파일 경로)
71
+ const images = await reader.listImages();
72
+ // → ["BinData/image1.png", "BinData/image2.jpg", ...]
73
+ ```
74
+
75
+ ### HTML 변환
76
+
77
+ ```ts
78
+ const html = await reader.extractHtml({
79
+ paragraphTag: "p", // default "p"
80
+ tableClassName: "hwpx-table", // default "hwpx-table"
81
+ renderImages: true, // default true
82
+ renderTables: true, // default true (rowSpan/colSpan 보존)
83
+ renderStyles: true, // default true (charProperties 기반 굵게/기울임/색/크기)
84
+ embedImages: false, // default false. true 시 data: URL 인라인
85
+ tableHeaderFirstRow: false, // default false. true 시 첫 행을 <th>
86
+ imageSrcResolver: (binPath) => `/static/images/${binPath}`,
87
+ });
88
+ ```
89
+
90
+ ### HWP 바이너리 파싱 / 변환
91
+
92
+ ```ts
93
+ import {
94
+ parseHwp,
95
+ hwpToText,
96
+ hwpToHwpx,
97
+ detectFormat,
98
+ HwpEncryptedError,
99
+ HwpUnsupportedError,
100
+ HwpInvalidFormatError,
101
+ } from "hwp-convert";
102
+ import { readFile, writeFile } from "node:fs/promises";
103
+
104
+ const bytes = new Uint8Array(await readFile("./input.hwp"));
105
+
106
+ // 포맷 자동 감지: "hwp" | "hwpx" | "hwp3" | "unknown"
107
+ const fmt = detectFormat(bytes);
108
+
109
+ // 평문 추출
110
+ const text = await hwpToText(bytes);
111
+
112
+ // HwpDocument IR (header / docInfo / sections / binData) 직접 접근
113
+ const doc = parseHwp(bytes);
114
+ console.log(doc.docInfo.styles, doc.sections.length);
115
+
116
+ // HWPX 로 변환 (표·이미지·스타일·미리보기 모두 포함)
117
+ const hwpxBytes = await hwpToHwpx(bytes, { title: "변환본", creator: "hwp-convert" });
118
+ await writeFile("./output.hwpx", hwpxBytes);
119
+ ```
120
+
121
+ `hwpToHwpx` 가 만든 HWPX 는 다음을 보존합니다:
122
+
123
+ - 표(`<hp:tbl>`/`<hp:tr>`/`<hp:tc>`/`<hp:subList>`) — `colSpan`/`rowSpan` 그대로
124
+ - 이미지 — `BinData/imageN.{png|jpg|...}` 패키징 + 매니페스트 등록 + `<hp:pic>`/`<hc:img binaryItemIDRef>` 인라인
125
+ - 폰트 — 7개 언어 그룹별(`HANGUL/LATIN/HANJA/JAPANESE/OTHER/SYMBOL/USER`) 실제 사용 폰트
126
+ - 글자 모양 — `<hh:charPr>` 의 `fontRef`/`textColor`/`shadeColor`/`height` + `<hh:bold>`/`<hh:italic>`/`<hh:underline>`/`<hh:strikeout>` 요소
127
+ - 문단 모양 — 정렬(`LEFT|RIGHT|CENTER|JUSTIFY|DISTRIBUTE`), 좌/우 여백, 들여쓰기, 줄간격
128
+ - 스타일 정의 — `<hh:style>` + `paraPrIDRef`/`charPrIDRef`
129
+ - 번호 매기기 — 수준별 형식 문자열(예: `^1.`, `^1)`)
130
+ - 글머리표 — 글머리 문자(●/○/■ 등)
131
+ - 미리보기 — `Preview/PrvText.txt` 자동 생성 (한컴 호환 형식, 다른 뷰어/탐색기에서 썸네일/미리보기 지원)
132
+
133
+ > 한계: `BorderFill` 의 그라데이션/이미지 채우기와 도형의 사각형/타원/호/다각형/곡선 좌표·스타일은 종류만 표시합니다(직선만 좌표 보존). 차트·OLE·글맵시는 미지원이고, 머리말/꼬리말/각주는 본문 흐름에 평탄 출력되며 별도 master page 매핑은 향후 지원 예정입니다.
134
+
135
+ ### Markdown / HTML ↔ HWPX
136
+
137
+ ```ts
138
+ import {
139
+ hwpToMarkdown,
140
+ markdownToHwpx,
141
+ htmlToHwpx,
142
+ } from "hwp-convert";
143
+ import { readFile, writeFile } from "node:fs/promises";
144
+
145
+ // HWP → Markdown
146
+ const md = await hwpToMarkdown(new Uint8Array(await readFile("./input.hwp")));
147
+
148
+ // HWPX → Markdown
149
+ import HwpxReader from "hwp-convert";
150
+ const reader = new HwpxReader();
151
+ await reader.loadFromArrayBuffer(/* ArrayBuffer */);
152
+ const md2 = await reader.extractMarkdown({ embedImages: true });
153
+
154
+ // Markdown → HWPX (heading/bold/italic/list/table/image-data-URI)
155
+ const mdSrc = `# 제목\n\n**굵게** 그리고 *기울임*\n\n| A | B |\n| --- | --- |\n| 1 | 2 |`;
156
+ const hwpxFromMd = await markdownToHwpx(mdSrc, { title: "from-md", creator: "me" });
157
+ await writeFile("./from-md.hwpx", hwpxFromMd);
158
+
159
+ // HTML → HWPX (p/h1-6/strong/em/ul/ol/li/table/blockquote/pre/img-data-URI)
160
+ const html = `<h1>제목</h1><p>본문 <strong>강조</strong></p>`;
161
+ const hwpxFromHtml = await htmlToHwpx(html);
162
+ await writeFile("./from-html.hwpx", hwpxFromHtml);
163
+ ```
164
+
165
+ 이미지는 `data:` URI 인 경우만 BinData 로 임베드됩니다. 외부 URL/상대경로 이미지는 스킵 (런타임 fetch 가 필요해서 1차 포팅 범위 밖).
166
+
167
+ ### 평문 → HWPX 작성
168
+
169
+ ```ts
170
+ import { HwpxWriter } from "hwp-convert";
171
+ import { writeFile } from "node:fs/promises";
172
+
173
+ const writer = new HwpxWriter();
174
+ const bytes = await writer.createFromPlainText("첫 문단\n두 번째 문단", {
175
+ title: "예시",
176
+ creator: "홍길동",
177
+ });
178
+ await writeFile("./output.hwpx", bytes);
179
+ ```
180
+
181
+ `HwpxWriter` 는 OWPML 패키지 규칙(첫 STORED `mimetype`, `META-INF/container.xml`/`manifest.xml`, OPF 매니페스트+spine, 최소 충실도의 `header.xml` 1세트)을 따르는 spec 호환 .hwpx 를 생성합니다. 한컴오피스 / LibreOffice 에서 그대로 열립니다.
182
+
183
+ ### 오류 처리
184
+
185
+ ```ts
186
+ import HwpxReader, {
187
+ HwpxNotLoadedError,
188
+ HwpxEncryptedDocumentError,
189
+ InvalidHwpxFormatError,
190
+ HwpEncryptedError,
191
+ HwpUnsupportedError,
192
+ HwpInvalidFormatError,
193
+ } from "hwp-convert";
194
+
195
+ try {
196
+ const reader = new HwpxReader();
197
+ await reader.loadFromArrayBuffer(buffer);
198
+ await reader.extractText();
199
+ } catch (e) {
200
+ if (e instanceof HwpxEncryptedDocumentError) {
201
+ // 암호화 HWPX — 미지원
202
+ } else if (e instanceof InvalidHwpxFormatError) {
203
+ // 유효하지 않은 HWPX
204
+ } else if (e instanceof HwpxNotLoadedError) {
205
+ // loadFromArrayBuffer 미호출
206
+ } else if (e instanceof HwpEncryptedError) {
207
+ // 암호화 HWP — 미지원
208
+ } else if (e instanceof HwpUnsupportedError) {
209
+ // 배포용 ViewText / HWP 3.0 등
210
+ } else if (e instanceof HwpInvalidFormatError) {
211
+ // CFB 시그니처 아님
212
+ } else {
213
+ throw e;
214
+ }
215
+ }
216
+ ```
217
+
218
+ ### 브라우저(ESM) 사용
219
+
220
+ `dist/browser/hwp-convert.browser.mjs` 는 모든 의존성을 인라인한 단일 ESM 번들입니다. `package.json` 의 `exports.browser` 조건과 `./browser` 서브패스로 매핑되어 있습니다.
221
+
222
+ 번들러(Vite/webpack) 사용 시:
223
+
224
+ ```ts
225
+ // 번들러가 browser 조건을 자동 선택
226
+ import { hwpToText, parseHwp } from "hwp-convert";
227
+
228
+ // 명시적으로 브라우저 빌드 지정
229
+ import { hwpToText } from "hwp-convert/browser";
230
+ ```
231
+
232
+ `<script type="module">` 직접 로드:
233
+
234
+ ```html
235
+ <script type="module">
236
+ import { hwpToText, hwpToHwpx } from "https://cdn.jsdelivr.net/npm/hwp-convert/dist/browser/hwp-convert.browser.mjs";
237
+
238
+ document.querySelector("#file").addEventListener("change", async (e) => {
239
+ const file = e.target.files[0];
240
+ const bytes = new Uint8Array(await file.arrayBuffer());
241
+ const text = await hwpToText(bytes);
242
+ document.querySelector("#out").textContent = text;
243
+ });
244
+ </script>
245
+ ```
246
+
247
+ ## CLI
248
+
249
+ 설치 후 `hwpconvert` 또는 `hwp-convert` 명령으로 사용할 수 있습니다.
250
+
251
+ ```bash
252
+ # HWPX 검사 / 추출
253
+ hwp-convert inspect document.hwpx
254
+ hwp-convert txt document.hwpx
255
+ hwp-convert html document.hwpx > out.html
256
+ hwp-convert md document.hwpx > out.md
257
+
258
+ # HWP 바이너리 (자동 라우팅: txt/md 명령은 .hwp 확장자 인식)
259
+ hwp-convert txt document.hwp
260
+ hwp-convert md document.hwp
261
+ hwp-convert hwp:txt document.hwp
262
+ hwp-convert hwp:md document.hwp > out.md
263
+ hwp-convert convert:hwp document.hwp converted.hwpx
264
+
265
+ # 작성 / 변환
266
+ hwp-convert write:txt notes.txt out.hwpx # 평문 → HWPX
267
+ hwp-convert md:hwpx notes.md out.hwpx # Markdown → HWPX
268
+ hwp-convert html:hwpx page.html out.hwpx # HTML → HWPX
269
+
270
+ # 일괄 처리: HWPX → HTML
271
+ hwp-convert batch ./input ./output
272
+
273
+ # 템플릿 ({{key}} 치환)
274
+ hwp-convert html:tpl template.hwpx data.json > result.html
275
+ hwp-convert batch:tpl ./templates ./data ./output
276
+ ```
277
+
278
+ ## 아키텍처 개요
279
+
280
+ `src/lib/` 아래 모듈 구성:
281
+
282
+ | 모듈 | 역할 |
283
+ | --- | --- |
284
+ | `hwpxReader.ts` | HWPX(ZIP+XML) 읽기 — 매니페스트/spine 으로 섹션 순서 결정, `extractText`/`extractHtml`/`listImages` |
285
+ | `writer.ts` | 평문 → HWPX (`HwpxWriter.createFromPlainText`) |
286
+ | `errors.ts` | HWPX 측 에러 클래스 (`HwpxNotLoadedError` 등) |
287
+ | `types.ts` | HWPX 측 공개 타입 |
288
+ | `hwp/index.ts` | HWP 바이너리 진입점 — `detectFormat`/`parseHwp`/`hwpToText`/`hwpToHwpx` |
289
+ | `hwp/cfbReader.ts` | CFB(OLE2) 컨테이너 + raw deflate 압축 해제 (cfb + pako) |
290
+ | `hwp/fileHeader.ts` | FileHeader 256B — 시그니처/버전/11종 플래그 |
291
+ | `hwp/record.ts` | DocInfo/BodyText 공통 4바이트 레코드 헤더(tag/level/size + 확장 size) |
292
+ | `hwp/docInfo.ts` | DocInfo 스트림 — FACE_NAME/CHAR_SHAPE/PARA_SHAPE/STYLE/BORDER_FILL/NUMBERING/BULLET/TAB_DEF/BIN_DATA |
293
+ | `hwp/bodyText.ts` | BodyText 섹션 — PARA_HEADER/PARA_TEXT/PARA_CHAR_SHAPE 와 컨트롤 문자, 계층적 nested 문단 |
294
+ | `hwp/control.ts` | 인라인 컨트롤 — 표(rowSpan/colSpan) / 그림(GSO+SHAPE_PICTURE) / 머리말·꼬리말·각주 / 필드 / 도형 / 수식(EQEDIT) |
295
+ | `hwp/binData.ts` | 임베디드 바이너리(이미지/OLE) 추출 — OLE storage prefix 보정 |
296
+ | `hwp/converter.ts` | `HwpDocument` IR → 텍스트 / HWPX 라우팅 |
297
+ | `hwp/hwpxBuilder.ts` | `HwpDocument` → HWPX 패키지 합성 (header.xml refList 풀 출력, section.xml 실 ID 참조, BinData 패키징, PrvText 생성) |
298
+ | `hwp/types.ts` | HWP IR 타입 (`HwpDocument`, `HwpSection`, `HwpParagraph`, `HwpControl` …) |
299
+ | `hwp/tags.ts` | HWPTAG_* 상수 |
300
+ | `hwp/byteReader.ts` | LE u8/u16/u32, signed, UTF-16, HWP 문자열, 바운드 체크 |
301
+
302
+ `src/cli.ts` 는 위 라이브러리를 얇게 감싼 CLI 진입점이고, `scripts/build-browser.mjs` 는 esbuild 로 브라우저 번들을 만듭니다.
303
+
304
+ ## 개발
305
+
306
+ ```bash
307
+ # 빌드 (tsc → dist/, esbuild → dist/browser/hwp-convert.browser.mjs)
308
+ npm run build
309
+
310
+ # 테스트 (vitest, 59 tests / 7 files: 단위 + 통합 + 견고성)
311
+ npm test
312
+ npm run test:watch
313
+ ```
314
+
315
+ 테스트 픽스처는 `test/fixtures/` 또는 사용자의 `~/Documents/` 에서 자동 탐지합니다. 다음 파일들이 있으면 통합 테스트가 활성화됩니다:
316
+
317
+ - `1.hwp`, `1.hwpx` — 폼 양식 (다중 셀 표)
318
+ - `여름휴가 안내문.hwp` — 이미지 임베드 (PNG + JPG)
319
+ - `공고문(안)(26.4.24.).hwp` — 표 셀 병합 (rowSpan 2/3/5)
320
+
321
+ ## 알려진 제한 (0.3.0)
322
+
323
+ - 암호화 HWP / 배포용 ViewText / HWP 3.0 미지원 — 명시적 에러 발생
324
+ - 머리말/꼬리말/각주: 본문 흐름에 평탄 paragraph 로 출력 (별도 master page 매핑은 향후)
325
+ - BorderFill 그라데이션/이미지 채우기 미보존 (단색 채우기·4면 테두리·대각선은 보존)
326
+ - 번호 매기기 수준별 시작번호: 기본 1
327
+ - 차트(CHART_DATA) / OLE / 글맵시: 미지원
328
+ - 도형: `line` 의 좌표만 보존, 사각형/타원/호/다각형/곡선은 종류만 보존
329
+
330
+ ## 구현 노트
331
+
332
+ - `HwpxReader` 는 `Contents/content.hpf` 의 manifest+spine 으로 섹션 순서를 결정하며, 실패 시 `Contents/section*.xml` 알파벳 순서로 폴백합니다.
333
+ - 암호화 감지는 `META-INF/manifest.xml` 의 `encrypt|cipher` 마커에 의존합니다(휴리스틱). 복호화는 미지원입니다.
334
+ - HWP 5.0 바이너리 파서는 [rhwp](https://github.com/edwardkim/rhwp) (MIT) 의 구조를 TypeScript 로 포팅 중이며, 각 포팅 파일 헤더에 출처가 명시되어 있습니다.
335
+
336
+ ## 라이센스
337
+
338
+ MIT. HWP 바이너리 파서 부분은 rhwp (MIT, Copyright (c) 2025-2026 Edward Kim) 의 코드 구조를 TypeScript 로 포팅한 것이며 동일 MIT 라이센스에서 재배포됩니다. 자세한 내용은 [LICENSE](./LICENSE) 와 각 포팅 파일의 헤더 주석을 참조해 주세요.