kordoc 1.7.1 → 1.7.2

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.
package/README.md CHANGED
@@ -1,127 +1,132 @@
1
1
  # kordoc
2
2
 
3
- **모두 파싱해버리겠다** — The Korean Document Platform.
3
+ **모두 파싱해버리겠다.**
4
4
 
5
- [![npm version](https://img.shields.io/badge/npm-v1.7.0-cb3837.svg)](https://www.npmjs.com/package/kordoc)
5
+ [![npm version](https://img.shields.io/badge/npm-v1.7.1-cb3837.svg)](https://www.npmjs.com/package/kordoc)
6
6
  [![license](https://img.shields.io/npm/l/kordoc.svg)](https://github.com/chrisryugj/kordoc/blob/main/LICENSE)
7
- [![node](https://img.shields.io/node/v/kordoc.svg)](https://nodejs.org)
8
7
 
9
- > *Parse, compare, extract, and generate Korean documents. HWP, HWPX, PDF — all of them.*
8
+ > *대한민국에서 둘째가라면 서러울 문서지옥. 거기서 7년 버틴 공무원이 만들었습니다.*
10
9
 
11
- [한국어](./README-KR.md)
10
+ HWP, HWPX, PDF — 관공서에서 쏟아지는 모든 문서를 파싱하고, 비교하고, 분석하고, 생성합니다.
12
11
 
13
- ![kordoc demo](./demo.gif)
12
+ [English](./README-EN.md)
13
+
14
+ ![kordoc 데모](./demo.gif)
14
15
 
15
16
  ---
16
17
 
17
- ## What's New in v1.7.0
18
+ ## 💡 kordoc으로 무엇을 할 수 있나요?
18
19
 
19
- - **Image Extraction (HWP/HWPX)** Binary image extraction from ZIP entries and HWP5 BinData streams. Rendered as `![image](...)` in markdown output.
20
- - **Partial Parsing (Graceful Degradation)** — Single page failures no longer abort the whole document. Failed pages emit `PARTIAL_PARSE` warnings and parsing continues.
21
- - **Progress Callbacks** — `onProgress` callback in `ParseOptions`. CLI shows `[3/15 pages]` progress. Batch mode shows `[2/10 files]`.
22
- - **File Path Input** — `parse("path/to/file.hwp")` string overload. Auto-reads file, detects format, returns result.
23
- - **PDF Header/Footer Filtering** — `removeHeaderFooter: true` option removes repeated text at page edges. Removed elements recorded in `ParseWarning`.
24
- - **Security Hardening** — ZIP bomb cumulative-size tracking across all file types, SSRF prevention on webhook URLs, XSS-safe hyperlink rendering (javascript: URLs stripped), null-byte path traversal detection, Levenshtein length guard (O(m×n) DoS prevention), 30s PDF load timeout.
25
- - **Bug Fixes** — HWPX generator separator logic, XML recursion depth limit (MAX_XML_DEPTH=200), PDF table row merge protection, CLI `--format` validation, variable shadowing in PDF parser.
26
- - **UX Improvements** — KV table false-positive reduction (time/URL/number patterns excluded), MCP `parse_metadata` uses 50MB limit with header-only format detection, Watch debounce increased to 1000ms with stable-size check.
20
+ 단순한 텍스트 추출을 넘어, **공문서 처리를 위한 모든 과정**을 자동화합니다.
27
21
 
28
- <details>
29
- <summary>v1.6.1 fixes</summary>
22
+ * **📄 어떤 문서든 마크다운으로**: `HWP`, `HWPX`, `PDF` 파일을 즉시 `Markdown`으로 변환합니다. AI(LLM)가 문서를 읽고 분석하기 가장 좋은 상태로 만들어줍니다.
23
+ * **📊 복잡한 표(Table) 완벽 재현**: 선이 없는 PDF나 복잡하게 병합된 HWP 표도 구조를 분석하여 정확한 마크다운 테이블로 복원합니다.
24
+ * **🔍 신구대조표 자동 생성**: 두 문서의 차이점을 분석하여 무엇이 바뀌었는지 한눈에 보여줍니다. (HWP와 HWPX 간의 비교도 가능!)
25
+ * **📝 마크다운을 다시 HWPX로**: AI가 작성한 내용을 다시 보고서 양식(`HWPX`)으로 되돌려줍니다. 이제 복사-붙여넣기 노가다에서 해방되세요.
26
+ * **🤖 AI 에이전트 연동 (MCP)**: `Claude`, `Cursor`와 같은 도구에서 직접 `kordoc`을 호출해 문서를 읽고 코딩할 수 있습니다.
27
+
28
+ ---
30
29
 
31
- - **HWP5 Table Cell Offset Fix** — Fixed critical 2-byte offset misalignment in LIST_HEADER parsing. Row address was incorrectly read as colSpan, causing 3-column tables to explode into 6+ columns with misaligned content. Tables now use colAddr/rowAddr-based direct placement for accurate cell positioning.
32
- - **HWP5 TAB Control Character Fix** — TAB (0x0009) inline control's 14-byte extension data was not skipped, producing garbage characters (`࣐Ā`) after every tab in the output. Fixed by adding the required 14-byte skip.
30
+ ## v1.7.1 변경사항
33
31
 
34
- </details>
32
+ - **이미지 추출 (HWP/HWPX)** — ZIP 엔트리와 HWP5 BinData 스트림에서 바이너리 이미지 추출. 마크다운 `![image](...)` 형태로 출력.
33
+ - **부분 파싱 (Graceful Degradation)** — 개별 페이지 실패가 전체 파싱을 중단하지 않음. 실패 페이지는 `PARTIAL_PARSE` 경고 기록 후 계속 진행.
34
+ - **진행률 콜백** — `ParseOptions`에 `onProgress` 콜백 추가. CLI에서 `[3/15 pages]` 형태 표시. 배치 모드는 `[2/10 files]`.
35
+ - **파일 경로 직접 입력** — `parse("path/to/file.hwp")` 문자열 오버로드. 파일 읽기 + 포맷 감지 자동.
36
+ - **PDF 머리글/바닥글 필터링** — `removeHeaderFooter: true` 옵션으로 페이지 상하단 반복 텍스트 제거. 제거 항목은 `ParseWarning` 기록.
37
+ - **보안 강화** — ZIP bomb 누적크기 추적 전체 파일 타입 적용, Webhook SSRF 방지, 하이퍼링크 XSS 방어(javascript: URL 제거), 널바이트 경로 탐색 감지, Levenshtein 길이 가드, PDF 30초 로딩 타임아웃.
38
+ - **버그 수정** — HWPX generator separator 로직, XML 재귀 깊이 제한(MAX_XML_DEPTH=200), PDF 테이블 행 병합 보호, CLI `--format` 검증, PDF 변수 섀도잉.
39
+ - **UX 개선** — KV 테이블 오탐 개선(시간/URL/숫자 패턴 제외), MCP `parse_metadata` 50MB 제한 + 헤더만 포맷 감지, Watch 디바운스 1000ms + 파일 크기 안정성 체크.
35
40
 
36
41
  <details>
37
- <summary>v1.6.0 features</summary>
42
+ <summary>v1.6.1 수정사항</summary>
38
43
 
39
- - **Cluster-Based Table Detection (PDF)**Detects borderless tables by analyzing text alignment patterns. Baseline grouping + X-coordinate clustering identifies 2+ column tables that line-based detection misses. Sort-and-split clustering for order-independent results.
40
- - **Korean Special Table Detection**Automatically detects `구분/항목/종류`-style key-value patterns common in Korean government documents and converts them to structured 2-column tables.
41
- - **Korean Word-Break Recovery** — Improved merging of broken Korean words in PDF table cells. Handles character-level PDF rendering (micro-gaps between Hangul characters) and cell line-break artifacts up to 8 characters.
42
- - **Empty Table Filtering** — Tables with all-empty cells (from line detection of decorative borders) are now automatically removed.
44
+ - **HWP5 테이블 오프셋 수정** LIST_HEADER 파싱 2바이트 오프셋 밀림으로 rowAddr를 colSpan으로 잘못 읽던 치명적 버그 수정. 3열 테이블이 6열로 뻥튀기되던 문제 해결. colAddr/rowAddr 기반 직접 배치로 병합 테이블 정확도 향상.
45
+ - **HWP5 TAB 제어문자 수정**TAB(0x0009) 인라인 컨트롤의 14바이트 확장 데이터 스킵 누락으로 `࣐Ā` 쓰레기 문자가 출력되던 버그 수정.
43
46
 
44
47
  </details>
45
48
 
46
49
  <details>
47
- <summary>v1.5.0 features</summary>
50
+ <summary>v1.6.0 기능</summary>
48
51
 
49
- - **Line-Based Table Detection (PDF)** — Ported from OpenDataLoader. Extracts horizontal/vertical lines from PDF graphics commands, builds grid via intersection vertices, maps text to cells by bbox overlap. Proper colspan/rowspan detection. Falls back to heuristic for line-free PDFs.
50
- - **IRBlock v2** 6 block types: `heading`, `paragraph`, `table`, `list`, `image`, `separator`. New fields: `bbox`, `style`, `pageNumber`, `level`, `href`, `footnoteText`.
51
- - **ParseResult v2**`outline` (document structure) and `warnings` (skipped elements, hidden text) fields.
52
- - **PDF Enhancements**XY-Cut reading order, heading detection (font-size ratio), hidden text filtering (prompt injection defense), bounding box on every block.
53
- - **HWP5 Enhancements** — CHAR_SHAPE parsing, style-based heading detection, warnings for skipped OLE/images.
54
- - **HWPX Enhancements** — Style parsing from header.xml, hyperlink/footnote extraction.
55
- - **List Detection** — Numbered paragraphs after tables auto-converted to ordered list blocks.
56
- - **MCP Server** — Now returns `outline` and `warnings` in parse_document responses.
52
+ - **클러스터 기반 테이블 감지 (PDF)** — 없는 PDF에서 텍스트 정렬 패턴으로 테이블 구조 추론. baseline 그룹핑 + X좌표 클러스터링으로 2열 이상 테이블 감지. 기반 감지가 실패한 경우의 중간 계층 fallback.
53
+ - **한국어 특수 테이블 감지** `구분/항목/종류/기준` 한국 공문서 key-value 패턴을 자동으로 2열 테이블로 변환.
54
+ - **한국어 어절 끊김 복원** PDF 한글 문자별 렌더링으로 인한 미세 처리 개선. 셀 줄바꿈 병합 임계값 8자로 확장, 1글자 조사 자동 연결.
55
+ - **빈 테이블 필터링** 장식용 선에서 생긴 테이블 자동 제거.
57
56
 
58
57
  </details>
59
58
 
60
59
  <details>
61
- <summary>v1.4.x features</summary>
62
-
63
- - **Document Compare** — Diff two documents at IR level. Cross-format (HWP vs HWPX) supported.
64
- - **Form Field Recognition** — Extract label-value pairs from government forms automatically.
65
- - **Structured Parsing** — Access `IRBlock[]` and `DocumentMetadata` directly, not just markdown.
66
- - **Page Range Parsing** — Parse only pages 1-3: `parse(buffer, { pages: "1-3" })`.
67
- - **Markdown to HWPX** — Reverse conversion. Generate valid HWPX files from markdown.
68
- - **OCR Integration** — Pluggable OCR for image-based PDFs (bring your own provider).
69
- - **Watch Mode** — `kordoc watch ./incoming --webhook https://...` for auto-conversion.
70
- - **7 MCP Tools** — parse_document, detect_format, parse_metadata, parse_pages, parse_table, compare_documents, parse_form.
71
- - **Error Codes** — Structured `code` field: `"ENCRYPTED"`, `"ZIP_BOMB"`, `"IMAGE_BASED_PDF"`, etc.
60
+ <summary>v1.5.0 기능</summary>
72
61
 
73
- </details>
62
+ - **선 기반 테이블 감지 (PDF)** — OpenDataLoader 핵심 알고리즘 포팅. PDF 그래픽 명령에서 수평/수직 선을 추출하고, 교차점으로 그리드 구성, bbox overlap으로 텍스트→셀 매핑. colspan/rowspan 자동 감지. 선 없는 PDF는 기존 휴리스틱 fallback.
63
+ - **IRBlock v2** — 6가지 블록 타입: `heading`, `paragraph`, `table`, `list`, `image`, `separator`. 새 필드: `bbox`, `style`, `pageNumber`, `level`, `href`, `footnoteText`.
64
+ - **ParseResult v2** — `outline` (문서 구조), `warnings` (스킵된 요소, 숨김 텍스트) 필드 추가.
65
+ - **PDF 개선** — XY-Cut 읽기 순서, 폰트 크기 기반 헤딩 감지, hidden text 필터링 (프롬프트 인젝션 방어), 모든 블록에 바운딩 박스.
66
+ - **HWP5 개선** — CHAR_SHAPE 파싱, 스타일 기반 헤딩 감지, OLE/이미지 스킵 경고.
67
+ - **HWPX 개선** — header.xml 스타일 파싱, 하이퍼링크/각주 추출.
68
+ - **리스트 감지** — 테이블 뒤 번호 문단을 ordered list 블록으로 자동 변환.
69
+ - **MCP 서버** — parse_document 응답에 `outline`, `warnings` 포함.
74
70
 
75
- ---
76
-
77
- ## Why kordoc?
71
+ </details>
78
72
 
79
- South Korea's government runs on **HWP** — a proprietary word processor the rest of the world has never heard of. Every day, 243 local governments and thousands of public institutions produce mountains of `.hwp` files. Extracting text from them has always been a nightmare.
73
+ <details>
74
+ <summary>v1.4.x 기능</summary>
75
+
76
+ - **문서 비교 (Diff)** — IR 레벨 블록 비교로 신구대조표 생성. HWP↔HWPX 크로스 포맷 지원.
77
+ - **양식 인식** — 공문서 테이블에서 label-value 쌍 자동 추출. 성명, 소속, 전화번호 등.
78
+ - **구조화 파싱** — `IRBlock[]`과 `DocumentMetadata`에 직접 접근. 마크다운 넘어선 데이터 활용.
79
+ - **페이지 범위** — `parse(buffer, { pages: "1-3" })` — 필요한 페이지만 빠르게.
80
+ - **Markdown → HWPX** — 역변환. AI가 생성한 내용을 바로 공문서로.
81
+ - **OCR 연동** — 이미지 기반 PDF도 텍스트 추출 (Tesseract, Claude Vision 등 프로바이더 직접 제공).
82
+ - **Watch 모드** — `kordoc watch ./수신함 -d ./변환결과 --webhook https://...`
83
+ - **MCP 7개 도구** — parse_document, detect_format, parse_metadata, parse_pages, parse_table, compare_documents, parse_form
84
+ - **에러 코드** — `"ENCRYPTED"`, `"ZIP_BOMB"`, `"IMAGE_BASED_PDF"` 등 구조화된 에러 핸들링
80
85
 
81
- **kordoc** was born from that document hell. Built by a Korean civil servant who spent **7 years** buried under HWP files. Battle-tested across 5 real government projects. If a Korean public servant wrote it, kordoc can parse it.
86
+ </details>
82
87
 
83
88
  ---
84
89
 
85
- ## Installation
90
+ ## 설치
86
91
 
87
92
  ```bash
88
93
  npm install kordoc
89
94
 
90
- # PDF support (optional)
95
+ # PDF 파싱이 필요하면 (선택)
91
96
  npm install pdfjs-dist
92
97
  ```
93
98
 
94
- ## Quick Start
99
+ ## 빠른 시작
95
100
 
96
- ### Parse Any Document
101
+ ### 문서 파싱
97
102
 
98
103
  ```typescript
99
104
  import { parse } from "kordoc"
100
105
  import { readFileSync } from "fs"
101
106
 
102
- const buffer = readFileSync("document.hwpx")
107
+ const buffer = readFileSync("사업계획서.hwpx")
103
108
  const result = await parse(buffer.buffer)
104
109
 
105
110
  if (result.success) {
106
- console.log(result.markdown) // Markdown text
107
- console.log(result.blocks) // IRBlock[] structured data
111
+ console.log(result.markdown) // 마크다운 텍스트
112
+ console.log(result.blocks) // IRBlock[] 구조화 데이터
108
113
  console.log(result.metadata) // { title, author, createdAt, ... }
109
114
  }
110
115
  ```
111
116
 
112
- ### Compare Two Documents
117
+ ### 문서 비교 (신구대조표)
113
118
 
114
119
  ```typescript
115
120
  import { compare } from "kordoc"
116
121
 
117
- const diff = await compare(bufferA, bufferB)
122
+ const diff = await compare(구버전Buffer, 신버전Buffer)
118
123
  // diff.stats → { added: 3, removed: 1, modified: 5, unchanged: 42 }
119
- // diff.diffs → BlockDiff[] with cell-level table diffs
124
+ // diff.diffs → BlockDiff[] (테이블은 단위 diff 포함)
120
125
  ```
121
126
 
122
- Cross-format supported: compare HWP against HWPX of the same document.
127
+ HWP vs HWPX 크로스 포맷 비교도 가능합니다.
123
128
 
124
- ### Extract Form Fields
129
+ ### 양식 필드 추출
125
130
 
126
131
  ```typescript
127
132
  import { parse, extractFormFields } from "kordoc"
@@ -134,28 +139,28 @@ if (result.success) {
134
139
  }
135
140
  ```
136
141
 
137
- ### Generate HWPX from Markdown
142
+ ### HWPX 생성 (역변환)
138
143
 
139
144
  ```typescript
140
145
  import { markdownToHwpx } from "kordoc"
141
146
 
142
- const hwpxBuffer = await markdownToHwpx("# Title\n\nParagraph text\n\n| A | B |\n| --- | --- |\n| 1 | 2 |")
143
- writeFileSync("output.hwpx", Buffer.from(hwpxBuffer))
147
+ const hwpxBuffer = await markdownToHwpx("# 제목\n\n본문 텍스트\n\n| 이름 | 직급 |\n| --- | --- |\n| 홍길동 | 과장 |")
148
+ writeFileSync("출력.hwpx", Buffer.from(hwpxBuffer))
144
149
  ```
145
150
 
146
- ### Parse Specific Pages
151
+ ### 페이지 범위 지정
147
152
 
148
153
  ```typescript
149
- const result = await parse(buffer, { pages: "1-3" }) // pages 1-3 only
150
- const result = await parse(buffer, { pages: [1, 5, 10] }) // specific pages
154
+ const result = await parse(buffer, { pages: "1-3" }) // 1~3 페이지만
155
+ const result = await parse(buffer, { pages: [1, 5, 10] }) // 특정 페이지
151
156
  ```
152
157
 
153
- ### OCR for Image-Based PDFs
158
+ ### OCR (이미지 PDF)
154
159
 
155
160
  ```typescript
156
161
  const result = await parse(buffer, {
157
162
  ocr: async (pageImage, pageNumber, mimeType) => {
158
- return await myOcrService.recognize(pageImage) // Tesseract, Claude Vision, etc.
163
+ return await myOcrService.recognize(pageImage)
159
164
  }
160
165
  })
161
166
  ```
@@ -163,16 +168,16 @@ const result = await parse(buffer, {
163
168
  ## CLI
164
169
 
165
170
  ```bash
166
- npx kordoc document.hwpx # stdout
167
- npx kordoc document.hwp -o output.md # save to file
168
- npx kordoc *.pdf -d ./converted/ # batch convert
169
- npx kordoc report.hwpx --format json # JSON with blocks + metadata
170
- npx kordoc report.hwpx --pages 1-3 # page range
171
- npx kordoc watch ./incoming -d ./output # watch mode
172
- npx kordoc watch ./docs --webhook https://api/hook # webhook notification
171
+ npx kordoc 사업계획서.hwpx # 터미널 출력
172
+ npx kordoc 보고서.hwp -o 보고서.md # 파일 저장
173
+ npx kordoc *.pdf -d ./변환결과/ # 일괄 변환
174
+ npx kordoc 검토서.hwpx --format json # JSON (blocks + metadata 포함)
175
+ npx kordoc 보고서.hwpx --pages 1-3 # 페이지 범위
176
+ npx kordoc watch ./수신함 -d ./변환결과 # 폴더 감시 모드
177
+ npx kordoc watch ./문서 --webhook https://api/hook # 웹훅 알림
173
178
  ```
174
179
 
175
- ## MCP Server (Claude / Cursor / Windsurf)
180
+ ## MCP 서버 (Claude / Cursor / Windsurf)
176
181
 
177
182
  ```json
178
183
  {
@@ -185,46 +190,45 @@ npx kordoc watch ./docs --webhook https://api/hook # webhook notification
185
190
  }
186
191
  ```
187
192
 
188
- **7 Tools:**
193
+ **7 도구:**
189
194
 
190
- | Tool | Description |
191
- |------|-------------|
192
- | `parse_document` | Parse HWP/HWPX/PDF → Markdown with metadata |
193
- | `detect_format` | Detect file format via magic bytes |
194
- | `parse_metadata` | Extract metadata only (fast, no full parse) |
195
- | `parse_pages` | Parse specific page range |
196
- | `parse_table` | Extract Nth table from document |
197
- | `compare_documents` | Diff two documents (cross-format) |
198
- | `parse_form` | Extract form fields as structured JSON |
195
+ | 도구 | 설명 |
196
+ |------|------|
197
+ | `parse_document` | HWP/HWPX/PDF → 마크다운 (메타데이터 포함) |
198
+ | `detect_format` | 매직 바이트로 포맷 감지 |
199
+ | `parse_metadata` | 메타데이터만 빠르게 추출 |
200
+ | `parse_pages` | 특정 페이지 범위만 파싱 |
201
+ | `parse_table` | N번째 테이블만 추출 |
202
+ | `compare_documents` | 문서 비교 (크로스 포맷) |
203
+ | `parse_form` | 양식 필드를 JSON으로 추출 |
199
204
 
200
- ## API Reference
205
+ ## API
201
206
 
202
- ### Core
207
+ ### 핵심 함수
203
208
 
204
- | Function | Description |
205
- |----------|-------------|
206
- | `parse(buffer, options?)` | Auto-detect format, parse to Markdown + IRBlock[] |
207
- | `parseHwpx(buffer, options?)` | HWPX only |
208
- | `parseHwp(buffer, options?)` | HWP 5.x only |
209
- | `parsePdf(buffer, options?)` | PDF only |
210
- | `detectFormat(buffer)` | Returns `"hwpx" \| "hwp" \| "pdf" \| "unknown"` |
209
+ | 함수 | 설명 |
210
+ |------|------|
211
+ | `parse(buffer, options?)` | 포맷 자동 감지 Markdown + IRBlock[] |
212
+ | `parseHwpx(buffer, options?)` | HWPX 전용 |
213
+ | `parseHwp(buffer, options?)` | HWP 5.x 전용 |
214
+ | `parsePdf(buffer, options?)` | PDF 전용 |
215
+ | `detectFormat(buffer)` | `"hwpx" \| "hwp" \| "pdf" \| "unknown"` |
211
216
 
212
- ### Advanced
217
+ ### 고급 함수
213
218
 
214
- | Function | Description |
215
- |----------|-------------|
216
- | `compare(bufferA, bufferB, options?)` | Document diff at IR level |
217
- | `extractFormFields(blocks)` | Form field recognition from IRBlock[] |
218
- | `markdownToHwpx(markdown)` | Markdown → HWPX reverse conversion |
219
- | `blocksToMarkdown(blocks)` | IRBlock[] → Markdown string |
219
+ | 함수 | 설명 |
220
+ |------|------|
221
+ | `compare(bufferA, bufferB, options?)` | IR 레벨 문서 비교 |
222
+ | `extractFormFields(blocks)` | IRBlock[]에서 양식 필드 인식 |
223
+ | `markdownToHwpx(markdown)` | Markdown → HWPX 역변환 |
224
+ | `blocksToMarkdown(blocks)` | IRBlock[] → Markdown 문자열 |
220
225
 
221
- ### Types
226
+ ### 타입
222
227
 
223
228
  ```typescript
224
229
  import type {
225
230
  ParseResult, ParseSuccess, ParseFailure, FileType,
226
- IRBlock, IRBlockType, IRTable, IRCell, CellContext,
227
- BoundingBox, InlineStyle, OutlineItem, ParseWarning, WarningCode,
231
+ IRBlock, IRTable, IRCell, CellContext,
228
232
  DocumentMetadata, ParseOptions, ErrorCode,
229
233
  DiffResult, BlockDiff, CellDiff, DiffChangeType,
230
234
  FormField, FormResult,
@@ -232,22 +236,23 @@ import type {
232
236
  } from "kordoc"
233
237
  ```
234
238
 
235
- ## Supported Formats
239
+ ## 지원 포맷
236
240
 
237
- | Format | Engine | Features |
238
- |--------|--------|----------|
239
- | **HWPX** (한컴 2020+) | ZIP + XML DOM | Manifest, nested tables, merged cells, broken ZIP recovery |
240
- | **HWP 5.x** (한컴 Legacy) | OLE2 + CFB | 21 control chars, zlib decompression, DRM detection, colAddr-based table cell placement |
241
- | **PDF** | pdfjs-dist | Line-based table detection, XY-Cut reading order, heading detection, hidden text filter, OCR |
241
+ | 포맷 | 엔진 | 특징 |
242
+ |------|------|------|
243
+ | **HWPX** (한컴 2020+) | ZIP + XML DOM | 매니페스트, 중첩 테이블, 병합 셀, 손상 ZIP 복구 |
244
+ | **HWP 5.x** (한컴 레거시) | OLE2 + CFB | 21 제어문자, zlib 압축 해제, DRM 감지, colAddr 기반 배치 |
245
+ | **PDF** | pdfjs-dist | 라인 그룹핑, 테이블 감지, 이미지 PDF + OCR |
242
246
 
243
- ## Security
247
+ ## 보안
244
248
 
245
- Production-grade hardening: ZIP bomb protection, XXE/Billion Laughs prevention, decompression bomb guard, path traversal guard, MCP error sanitization, file size limits (500MB). See [SECURITY.md](./SECURITY.md) for details.
249
+ 프로덕션급 보안 강화: ZIP bomb 방지, XXE/Billion Laughs 방지, 압축 폭탄 방지, 경로 순회 차단, MCP 에러 정제, 파일 크기 제한(500MB). 자세한 내용은 [SECURITY.md](./SECURITY.md) 참조.
246
250
 
247
- ## Credits
251
+ ## 만든 사람
248
252
 
249
- Production-tested across 5 Korean government projects: school curriculum plans, facility inspection reports, legal document annexes, municipal newsletters, and public data extraction tools. Thousands of real government documents parsed.
253
+ 대한민국 지방공무원. 광진구청에서 7년간 HWP 파일과 싸우다가 이걸 만들었습니다.
254
+ 5개 공공 프로젝트에서 수천 건의 실제 관공서 문서를 파싱하며 검증했습니다.
250
255
 
251
- ## License
256
+ ## 라이선스
252
257
 
253
258
  [MIT](./LICENSE)
@@ -196,7 +196,7 @@ function tableToMarkdown(table) {
196
196
  }
197
197
 
198
198
  // src/utils.ts
199
- var VERSION = true ? "1.7.1" : "0.0.0-dev";
199
+ var VERSION = true ? "1.7.2" : "0.0.0-dev";
200
200
  function toArrayBuffer(buf) {
201
201
  if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) {
202
202
  return buf.buffer;
@@ -1551,6 +1551,20 @@ function extractLines(fnArray, argsArray) {
1551
1551
  let currentPath = [];
1552
1552
  let pathStartX = 0, pathStartY = 0;
1553
1553
  let curX = 0, curY = 0;
1554
+ function pushRectangle(path, rx, ry, rw, rh) {
1555
+ if (Math.abs(rh) < ORIENTATION_TOL * 2) {
1556
+ path.push({ x1: rx, y1: ry + rh / 2, x2: rx + rw, y2: ry + rh / 2 });
1557
+ } else if (Math.abs(rw) < ORIENTATION_TOL * 2) {
1558
+ path.push({ x1: rx + rw / 2, y1: ry, x2: rx + rw / 2, y2: ry + rh });
1559
+ } else {
1560
+ path.push(
1561
+ { x1: rx, y1: ry, x2: rx + rw, y2: ry },
1562
+ { x1: rx + rw, y1: ry, x2: rx + rw, y2: ry + rh },
1563
+ { x1: rx + rw, y1: ry + rh, x2: rx, y2: ry + rh },
1564
+ { x1: rx, y1: ry + rh, x2: rx, y2: ry }
1565
+ );
1566
+ }
1567
+ }
1554
1568
  function flushPath(isStroke) {
1555
1569
  if (!isStroke) {
1556
1570
  currentPath = [];
@@ -1569,49 +1583,78 @@ function extractLines(fnArray, argsArray) {
1569
1583
  lineWidth = args[0] || 1;
1570
1584
  break;
1571
1585
  case OPS.constructPath: {
1572
- const subOps = args[0];
1573
- const coords = args[1];
1574
- let ci = 0;
1575
- for (const subOp of subOps) {
1576
- if (subOp === OPS.moveTo) {
1577
- curX = coords[ci++];
1578
- curY = coords[ci++];
1579
- pathStartX = curX;
1580
- pathStartY = curY;
1581
- } else if (subOp === OPS.lineTo) {
1582
- const x2 = coords[ci++], y2 = coords[ci++];
1583
- currentPath.push({ x1: curX, y1: curY, x2, y2 });
1584
- curX = x2;
1585
- curY = y2;
1586
- } else if (subOp === OPS.rectangle) {
1587
- const rx = coords[ci++], ry = coords[ci++];
1588
- const rw = coords[ci++], rh = coords[ci++];
1589
- if (Math.abs(rh) < ORIENTATION_TOL * 2) {
1590
- currentPath.push({ x1: rx, y1: ry + rh / 2, x2: rx + rw, y2: ry + rh / 2 });
1591
- } else if (Math.abs(rw) < ORIENTATION_TOL * 2) {
1592
- currentPath.push({ x1: rx + rw / 2, y1: ry, x2: rx + rw / 2, y2: ry + rh });
1593
- } else {
1594
- currentPath.push(
1595
- { x1: rx, y1: ry, x2: rx + rw, y2: ry },
1596
- // bottom
1597
- { x1: rx + rw, y1: ry, x2: rx + rw, y2: ry + rh },
1598
- // right
1599
- { x1: rx + rw, y1: ry + rh, x2: rx, y2: ry + rh },
1600
- // top
1601
- { x1: rx, y1: ry + rh, x2: rx, y2: ry }
1602
- // left
1603
- );
1586
+ const arg0 = args[0];
1587
+ if (Array.isArray(arg0)) {
1588
+ const subOps = arg0;
1589
+ const coords = args[1];
1590
+ let ci = 0;
1591
+ for (const subOp of subOps) {
1592
+ if (subOp === OPS.moveTo) {
1593
+ curX = coords[ci++];
1594
+ curY = coords[ci++];
1595
+ pathStartX = curX;
1596
+ pathStartY = curY;
1597
+ } else if (subOp === OPS.lineTo) {
1598
+ const x2 = coords[ci++], y2 = coords[ci++];
1599
+ currentPath.push({ x1: curX, y1: curY, x2, y2 });
1600
+ curX = x2;
1601
+ curY = y2;
1602
+ } else if (subOp === OPS.rectangle) {
1603
+ const rx = coords[ci++], ry = coords[ci++];
1604
+ const rw = coords[ci++], rh = coords[ci++];
1605
+ pushRectangle(currentPath, rx, ry, rw, rh);
1606
+ } else if (subOp === OPS.closePath) {
1607
+ if (curX !== pathStartX || curY !== pathStartY) {
1608
+ currentPath.push({ x1: curX, y1: curY, x2: pathStartX, y2: pathStartY });
1609
+ }
1610
+ curX = pathStartX;
1611
+ curY = pathStartY;
1612
+ } else if (subOp === OPS.curveTo) {
1613
+ ci += 6;
1614
+ } else if (subOp === OPS.curveTo2 || subOp === OPS.curveTo3) {
1615
+ ci += 4;
1604
1616
  }
1605
- } else if (subOp === OPS.closePath) {
1606
- if (curX !== pathStartX || curY !== pathStartY) {
1607
- currentPath.push({ x1: curX, y1: curY, x2: pathStartX, y2: pathStartY });
1617
+ }
1618
+ } else {
1619
+ const afterOp = arg0;
1620
+ const dataArr = args[1];
1621
+ const pathData = dataArr?.[0];
1622
+ if (pathData && typeof pathData === "object") {
1623
+ const len = Object.keys(pathData).length;
1624
+ let di = 0;
1625
+ while (di < len) {
1626
+ const drawOp = pathData[di++];
1627
+ if (drawOp === 0 /* moveTo */) {
1628
+ curX = pathData[di++];
1629
+ curY = pathData[di++];
1630
+ pathStartX = curX;
1631
+ pathStartY = curY;
1632
+ } else if (drawOp === 1 /* lineTo */) {
1633
+ const x2 = pathData[di++], y2 = pathData[di++];
1634
+ currentPath.push({ x1: curX, y1: curY, x2, y2 });
1635
+ curX = x2;
1636
+ curY = y2;
1637
+ } else if (drawOp === 2 /* curveTo */) {
1638
+ di += 6;
1639
+ } else if (drawOp === 3 /* quadraticCurveTo */) {
1640
+ di += 4;
1641
+ } else if (drawOp === 4 /* closePath */) {
1642
+ if (curX !== pathStartX || curY !== pathStartY) {
1643
+ currentPath.push({ x1: curX, y1: curY, x2: pathStartX, y2: pathStartY });
1644
+ }
1645
+ curX = pathStartX;
1646
+ curY = pathStartY;
1647
+ } else {
1648
+ break;
1649
+ }
1608
1650
  }
1609
- curX = pathStartX;
1610
- curY = pathStartY;
1611
- } else if (subOp === OPS.curveTo) {
1612
- ci += 6;
1613
- } else if (subOp === OPS.curveTo2 || subOp === OPS.curveTo3) {
1614
- ci += 4;
1651
+ }
1652
+ if (afterOp === OPS.stroke || afterOp === OPS.closeStroke) {
1653
+ flushPath(true);
1654
+ } else if (afterOp === OPS.fill || afterOp === OPS.eoFill || afterOp === OPS.fillStroke || afterOp === OPS.eoFillStroke || afterOp === OPS.closeFillStroke || afterOp === OPS.closeEOFillStroke) {
1655
+ flushPath(true);
1656
+ } else if (afterOp === OPS.endPath) {
1657
+ flushPath(false);
1615
1658
  }
1616
1659
  }
1617
1660
  break;
@@ -3328,4 +3371,4 @@ export {
3328
3371
  extractFormFields,
3329
3372
  parse
3330
3373
  };
3331
- //# sourceMappingURL=chunk-VOMMXHNQ.js.map
3374
+ //# sourceMappingURL=chunk-NJ3R7LNR.js.map