v8:문서유형 분석등록 및 추출_20260206

This commit is contained in:
2026-02-20 11:46:52 +09:00
parent db6532b33c
commit c3e9e29205
57 changed files with 22138 additions and 1421 deletions

0
domain/__init__.py Normal file
View File

0
domain/hwpx/__init__.py Normal file
View File

View File

@@ -0,0 +1,769 @@
# HWP/HWPX ↔ HTML/CSS 도메인 가이드
> **목적**: HWPX에서 문서 유형·스타일·템플릿을 추출하거나, HTML → HWPX → HWP 변환 시
> 하드코딩 없이 이 가이드를 참조하여 정확한 매핑을 수행한다.
> **출처**: 한글과컴퓨터 공식 "글 문서 파일 구조 5.0" (revision 1.3, 2018-11-08)
> **범위**: HWP 5.0 바이너리 스펙의 개념 체계 + HWPX XML 태그 + HTML/CSS 매핑
---
## 0. 문서 형식 관계
```
HWP (바이너리) HWPX (XML) HTML/CSS
───────────────── ───────────────────── ─────────────────
Compound File ZIP Archive 단일 HTML 파일
├─ FileHeader ├─ META-INF/ ├─ <head>
├─ DocInfo │ └─ manifest.xml │ ├─ <meta>
│ (글꼴, 스타일, ├─ Contents/ │ └─ <style>
│ 테두리/배경, │ ├─ header.xml └─ <body>
│ 글자모양 등) │ │ (DocInfo 대응) ├─ 헤더 영역
├─ BodyText/ │ ├─ section0.xml │ ├─ 본문
│ └─ Section0 │ │ (본문 대응) │ └─ 푸터 영역
├─ BinData/ │ └─ section1.xml └─ @page CSS
│ └─ 이미지 등 ├─ BinData/
└─ PrvImage │ └─ 이미지 파일
└─ version.xml
```
**핵심**: HWP 바이너리의 레코드 구조와 HWPX XML의 엘리먼트는 1:1 대응한다.
이 가이드는 두 형식의 공통 개념 체계를 기준으로, CSS 변환까지 연결한다.
---
## 1. 단위 체계
### 1.1 HWPUNIT (글 내부 단위)
HWP는 1/7200 인치를 기본 단위로 사용한다.
| 변환 대상 | 공식 | 예시 |
|-----------|------|------|
| HWPUNIT → mm | `hwpunit / 7200 * 25.4` | 7200 → 25.4mm (= 1인치) |
| HWPUNIT → pt | `hwpunit / 7200 * 72` | 7200 → 72pt |
| HWPUNIT → px (96dpi) | `hwpunit / 7200 * 96` | 7200 → 96px |
| mm → HWPUNIT | `mm / 25.4 * 7200` | 25.4mm → 7200 |
| pt → HWPUNIT | `pt / 72 * 7200` | 10pt → 1000 |
```python
def hwpunit_to_mm(hwpunit): return hwpunit / 7200 * 25.4
def hwpunit_to_pt(hwpunit): return hwpunit / 7200 * 72
def hwpunit_to_px(hwpunit): return hwpunit / 7200 * 96
def mm_to_hwpunit(mm): return mm / 25.4 * 7200
```
### 1.2 글자 크기 (CharShape)
HWP의 글자 크기는 HWPUNIT 단위이지만 100배 스케일이 적용되어 있다.
| HWP 값 | 실제 크기 | CSS |
|--------|----------|-----|
| 1000 | 10pt | `font-size: 10pt` |
| 1200 | 12pt | `font-size: 12pt` |
| 2400 | 24pt | `font-size: 24pt` |
```python
def charsize_to_pt(hwp_size): return hwp_size / 100 # 1000 → 10pt
```
### 1.3 COLORREF (색상)
HWP는 0x00BBGGRR 형식(리틀 엔디안 BGR). CSS는 #RRGGBB.
| HWP COLORREF | 분해 | CSS |
|-------------|------|-----|
| 0x00000000 | R=0, G=0, B=0 | `#000000` (검정) |
| 0x00FF0000 | R=0, G=0, B=255 | `#0000ff` (파랑) |
| 0x0000FF00 | R=0, G=255, B=0 | `#00ff00` (초록) |
| 0x000000FF | R=255, G=0, B=0 | `#ff0000` (빨강) |
| 0x00FFFFFF | R=255, G=255, B=255 | `#ffffff` (흰색) |
```python
def colorref_to_css(colorref):
r = colorref & 0xFF
g = (colorref >> 8) & 0xFF
b = (colorref >> 16) & 0xFF
return f'#{r:02x}{g:02x}{b:02x}'
```
**HWPX XML에서의 색상**: `#RRGGBB` 형식으로 직접 기록됨 (변환 불필요).
---
## 2. 테두리/배경 (BorderFill)
> HWP: `HWPTAG_BORDER_FILL` (DocInfo 레코드)
> HWPX: `<hh:borderFill>` (header.xml 내)
> 용도: 표 셀, 문단, 쪽 테두리/배경에 공통 적용
### 2.1 테두리선 종류
| HWP 값 | 이름 | HWPX type 속성 | CSS border-style |
|--------|------|---------------|-----------------|
| 0 | 실선 | `SOLID` | `solid` |
| 1 | 긴 점선 | `DASH` | `dashed` |
| 2 | 점선 | `DOT` | `dotted` |
| 3 | -.-.-. | `DASH_DOT` | `dashed` (근사) |
| 4 | -..-.. | `DASH_DOT_DOT` | `dashed` (근사) |
| 5 | 긴 Dash | `LONG_DASH` | `dashed` |
| 6 | 큰 동그라미 | `CIRCLE` | `dotted` (근사) |
| 7 | 2중선 | `DOUBLE` | `double` |
| 8 | 가는선+굵은선 | `THIN_THICK` | `double` (근사) |
| 9 | 굵은선+가는선 | `THICK_THIN` | `double` (근사) |
| 10 | 가는+굵은+가는 | `THIN_THICK_THIN` | `double` (근사) |
| 11 | 물결 | `WAVE` | `solid` (근사) |
| 12 | 물결 2중선 | `DOUBLE_WAVE` | `double` (근사) |
| 13 | 두꺼운 3D | `THICK_3D` | `ridge` |
| 14 | 두꺼운 3D(역) | `THICK_3D_REV` | `groove` |
| 15 | 3D 단선 | `3D` | `outset` |
| 16 | 3D 단선(역) | `3D_REV` | `inset` |
| — | 없음 | `NONE` | `none` |
### 2.2 테두리선 굵기
| HWP 값 | 실제 굵기 | HWPX width 속성 | CSS border-width |
|--------|----------|----------------|-----------------|
| 0 | 0.1 mm | `0.1mm` | `0.1mm``0.4px` |
| 1 | 0.12 mm | `0.12mm` | `0.12mm``0.5px` |
| 2 | 0.15 mm | `0.15mm` | `0.15mm``0.6px` |
| 3 | 0.2 mm | `0.2mm` | `0.2mm``0.8px` |
| 4 | 0.25 mm | `0.25mm` | `0.25mm``1px` |
| 5 | 0.3 mm | `0.3mm` | `0.3mm``1.1px` |
| 6 | 0.4 mm | `0.4mm` | `0.4mm``1.5px` |
| 7 | 0.5 mm | `0.5mm` | `0.5mm``1.9px` |
| 8 | 0.6 mm | `0.6mm` | `0.6mm``2.3px` |
| 9 | 0.7 mm | `0.7mm` | `0.7mm``2.6px` |
| 10 | 1.0 mm | `1.0mm` | `1mm``3.8px` |
| 11 | 1.5 mm | `1.5mm` | `1.5mm``5.7px` |
| 12 | 2.0 mm | `2.0mm` | `2mm``7.6px` |
| 13 | 3.0 mm | `3.0mm` | `3mm``11.3px` |
| 14 | 4.0 mm | `4.0mm` | `4mm``15.1px` |
| 15 | 5.0 mm | `5.0mm` | `5mm``18.9px` |
```python
BORDER_WIDTH_MAP = {
0: 0.1, 1: 0.12, 2: 0.15, 3: 0.2, 4: 0.25, 5: 0.3,
6: 0.4, 7: 0.5, 8: 0.6, 9: 0.7, 10: 1.0, 11: 1.5,
12: 2.0, 13: 3.0, 14: 4.0, 15: 5.0
}
def border_width_to_css(hwp_val):
mm = BORDER_WIDTH_MAP.get(hwp_val, 0.12)
return f'{mm}mm' # 또는 mm * 3.7795px
```
### 2.3 테두리 4방향 순서
| HWP 배열 인덱스 | HWPX 속성 | CSS 대응 |
|:---:|:---:|:---:|
| [0] | `<left>` / `<hh:left>` | `border-left` |
| [1] | `<right>` / `<hh:right>` | `border-right` |
| [2] | `<top>` / `<hh:top>` | `border-top` |
| [3] | `<bottom>` / `<hh:bottom>` | `border-bottom` |
### 2.4 채우기 (Fill) 정보
| 채우기 종류 (type 비트) | HWPX 엘리먼트 | CSS 대응 |
|:---:|:---:|:---:|
| 0x00 — 없음 | (없음) | `background: none` |
| 0x01 — 단색 | `<hh:windowBrush>` 또는 `<hh:colorFill>` | `background-color: #...` |
| 0x02 — 이미지 | `<hh:imgBrush>` | `background-image: url(...)` |
| 0x04 — 그러데이션 | `<hh:gradation>` | `background: linear-gradient(...)` |
**단색 채우기 구조** (가장 빈번):
```xml
<!-- HWPX header.xml -->
<hh:borderFill id="4">
<hh:slash .../>
<hh:backSlash .../>
<hh:left type="SOLID" width="0.12mm" color="#000000"/>
<hh:right type="SOLID" width="0.12mm" color="#000000"/>
<hh:top type="SOLID" width="0.12mm" color="#000000"/>
<hh:bottom type="SOLID" width="0.12mm" color="#000000"/>
<hh:diagonal .../>
<hh:fillBrush>
<hh:windowBrush faceColor="#E8F5E9" hatchColor="none" .../>
</hh:fillBrush>
</hh:borderFill>
```
```css
/* CSS 대응 */
.cell-bf4 {
border-left: 0.12mm solid #000000;
border-right: 0.12mm solid #000000;
border-top: 0.12mm solid #000000;
border-bottom: 0.12mm solid #000000;
background-color: #E8F5E9;
}
```
### 2.5 HWPX borderFill → CSS 변환 함수 (의사 코드)
```python
def borderfill_to_css(bf_element):
"""HWPX <hh:borderFill> 엘리먼트 → CSS 딕셔너리"""
css = {}
for side in ['left', 'right', 'top', 'bottom']:
el = bf_element.find(f'hh:{side}')
if el is None:
css[f'border-{side}'] = 'none'
continue
btype = el.get('type', 'NONE')
width = el.get('width', '0.12mm')
color = el.get('color', '#000000')
if btype == 'NONE':
css[f'border-{side}'] = 'none'
else:
css_style = BORDER_TYPE_MAP.get(btype, 'solid')
css[f'border-{side}'] = f'{width} {css_style} {color}'
# 배경
fill = bf_element.find('.//hh:windowBrush')
if fill is not None:
face = fill.get('faceColor', 'none')
if face and face != 'none':
css['background-color'] = face
return css
```
---
## 3. 글꼴 (FaceName)
> HWP: `HWPTAG_FACE_NAME` (DocInfo)
> HWPX: `<hh:fontface>` → `<hh:font>` (header.xml)
> CSS: `font-family`
### 3.1 언어별 글꼴 시스템
HWP는 한글·영문·한자·일어·기타·기호·사용자 총 7개 언어 슬롯에 각각 다른 글꼴을 지정한다.
| 언어 인덱스 | HWPX lang 속성 | 주요 글꼴 예시 |
|:---:|:---:|:---:|
| 0 | `HANGUL` | 맑은 고딕, 나눔고딕 |
| 1 | `LATIN` | Arial, Times New Roman |
| 2 | `HANJA` | (한글 글꼴 공유) |
| 3 | `JAPANESE` | MS Mincho |
| 4 | `OTHER` | — |
| 5 | `SYMBOL` | Symbol, Wingdings |
| 6 | `USER` | — |
**CSS 매핑**: 일반적으로 한글(0)과 영문(1) 글꼴을 `font-family` 스택으로 결합.
```css
/* HWPX: hangul="맑은 고딕" latin="Arial" */
font-family: "맑은 고딕", Arial, sans-serif;
```
### 3.2 글꼴 관련 HWPX 구조
```xml
<!-- header.xml -->
<hh:fontfaces>
<hh:fontface lang="HANGUL">
<hh:font face="맑은 고딕" type="TTF" id="0"/>
</hh:fontface>
<hh:fontface lang="LATIN">
<hh:font face="Arial" type="TTF" id="0"/>
</hh:fontface>
...
</hh:fontfaces>
```
---
## 4. 글자 모양 (CharShape)
> HWP: `HWPTAG_CHAR_SHAPE` (DocInfo, 72바이트)
> HWPX: `<hh:charPr>` (header.xml charProperties 내)
> CSS: font-*, color, text-decoration 등
### 4.1 주요 속성 매핑
| HWP 필드 | HWPX 속성 | CSS 속성 | 비고 |
|----------|----------|---------|------|
| 글꼴 ID [7] | `fontRef` | `font-family` | 언어별 참조 |
| 장평 [7] | `ratio` | `font-stretch` | 50%~200% |
| 자간 [7] | `spacing` | `letter-spacing` | -50%~50%, pt 변환 필요 |
| 기준 크기 | `height` | `font-size` | 값/100 = pt |
| 글자 색 | `color` 속성 | `color` | COLORREF → #RRGGBB |
| 밑줄 색 | `underline color` | `text-decoration-color` | |
| 진하게(bit 1) | `bold="true"` | `font-weight: bold` | |
| 기울임(bit 0) | `italic="true"` | `font-style: italic` | |
| 밑줄(bit 2-3) | `underline type` | `text-decoration: underline` | |
| 취소선(bit 18-20) | `strikeout type` | `text-decoration: line-through` | |
| 위첨자(bit 15) | `supscript` | `vertical-align: super; font-size: 70%` | |
| 아래첨자(bit 16) | `subscript` | `vertical-align: sub; font-size: 70%` | |
### 4.2 HWPX charPr 구조 예시
```xml
<hh:charPr id="1" height="1000" bold="false" italic="false"
underline="NONE" strikeout="NONE" color="#000000">
<hh:fontRef hangul="0" latin="0" hanja="0" japanese="0"
other="0" symbol="0" user="0"/>
<hh:ratio hangul="100" latin="100" .../>
<hh:spacing hangul="0" latin="0" .../>
<hh:relSz hangul="100" latin="100" .../>
<hh:offset hangul="0" latin="0" .../>
</hh:charPr>
```
---
## 5. 문단 모양 (ParaShape)
> HWP: `HWPTAG_PARA_SHAPE` (DocInfo, 54바이트)
> HWPX: `<hh:paraPr>` (header.xml paraProperties 내)
> CSS: text-align, margin, line-height, text-indent 등
### 5.1 정렬 방식
| HWP 값 (bit 2-4) | HWPX 속성값 | CSS text-align |
|:---:|:---:|:---:|
| 0 | `JUSTIFY` | `justify` |
| 1 | `LEFT` | `left` |
| 2 | `RIGHT` | `right` |
| 3 | `CENTER` | `center` |
| 4 | `DISTRIBUTE` | `justify` (근사) |
| 5 | `DISTRIBUTE_SPACE` | `justify` (근사) |
### 5.2 줄 간격 종류
| HWP 값 | HWPX 속성값 | CSS line-height | 비고 |
|:---:|:---:|:---:|:---:|
| 0 | `PERCENT` | `160%` (예) | 글자 크기 기준 % |
| 1 | `FIXED` | `24pt` (예) | 고정 pt |
| 2 | `BETWEEN_LINES` | — | 여백만 지정 |
| 3 | `AT_LEAST` | — | 최소값 |
### 5.3 주요 속성 매핑
| HWP 필드 | HWPX 속성 | CSS 속성 | 단위 |
|----------|----------|---------|------|
| 왼쪽 여백 | `margin left` | `margin-left` / `padding-left` | HWPUNIT → mm |
| 오른쪽 여백 | `margin right` | `margin-right` / `padding-right` | HWPUNIT → mm |
| 들여쓰기 | `indent` | `text-indent` | HWPUNIT → mm |
| 문단 간격 위 | `spacing before` | `margin-top` | HWPUNIT → mm |
| 문단 간격 아래 | `spacing after` | `margin-bottom` | HWPUNIT → mm |
| 줄 간격 | `lineSpacing` | `line-height` | 종류에 따라 다름 |
| BorderFill ID | `borderFillIDRef` | border + background | ID로 참조 |
### 5.4 HWPX paraPr 구조 예시
```xml
<hh:paraPr id="0" align="JUSTIFY">
<hh:margin left="0" right="0" indent="0"/>
<hh:spacing before="0" after="0"
lineSpacingType="PERCENT" lineSpacing="160"/>
<hh:border borderFillIDRef="1"
left="0" right="0" top="0" bottom="0"/>
<hh:autoSpacing eAsianEng="false" eAsianNum="false"/>
</hh:paraPr>
```
---
## 6. 표 (Table) 구조
> HWP: `HWPTAG_TABLE` (본문 레코드)
> HWPX: `<hp:tbl>` (section*.xml 내)
> HTML: `<table>`, `<tr>`, `<td>`/`<th>`
### 6.1 표 속성 매핑
| HWP 필드 | HWPX 속성 | HTML/CSS 대응 | 비고 |
|----------|----------|-------------|------|
| RowCount | `rowCnt` | (행 수) | |
| nCols | `colCnt` | (열 수) | `<colgroup>` 참조 |
| CellSpacing | `cellSpacing` | `border-spacing` | HWPUNIT16 |
| 안쪽 여백 | `cellMargin` left/right/top/bottom | `padding` | |
| BorderFill ID | `borderFillIDRef` | 표 전체 테두리 | |
| 쪽나눔(bit 0-1) | `pageBreak` | `page-break-inside` | 0=avoid, 1=auto |
| 제목줄 반복(bit 2) | `repeatHeader` | `<thead>` 출력 | |
### 6.2 열 너비
```xml
<!-- HWPX -->
<hp:tbl colCnt="3" rowCnt="5" ...>
<hp:colSz>
<hp:widthList>8504 8504 8504</hp:widthList> <!-- HWPUNIT -->
</hp:colSz>
...
</hp:tbl>
```
```html
<!-- HTML 변환 -->
<colgroup>
<col style="width: 33.33%"> <!-- 8504 / 총합 * 100 -->
<col style="width: 33.33%">
<col style="width: 33.33%">
</colgroup>
```
### 6.3 셀 (Cell) 속성
| HWP 필드 | HWPX 속성 | HTML 속성 | 비고 |
|----------|----------|----------|------|
| Column 주소 | `colAddr` | — | 0부터 시작 |
| Row 주소 | `rowAddr` | — | 0부터 시작 |
| 열 병합 개수 | `colSpan` | `colspan` | 1 = 병합 없음 |
| 행 병합 개수 | `rowSpan` | `rowspan` | 1 = 병합 없음 |
| 셀 폭 | `width` | `width` | HWPUNIT |
| 셀 높이 | `height` | `height` | HWPUNIT |
| 셀 여백 [4] | `cellMargin` | `padding` | HWPUNIT16 → mm |
| BorderFill ID | `borderFillIDRef` | `border` + `background` | 셀별 스타일 |
### 6.4 HWPX 셀 구조 예시
```xml
<hp:tc colAddr="0" rowAddr="0" colSpan="2" rowSpan="1"
width="17008" height="2400" borderFillIDRef="4">
<hp:cellMargin left="510" right="510" top="142" bottom="142"/>
<hp:cellAddr colAddr="0" rowAddr="0"/>
<hp:subList ...>
<hp:p ...>
<!-- 셀 내용 -->
</hp:p>
</hp:subList>
</hp:tc>
```
```html
<!-- HTML 변환 -->
<td colspan="2" style="
width: 60mm;
height: 8.5mm;
padding: 0.5mm 1.8mm;
border: 0.12mm solid #000;
background-color: #E8F5E9;
">셀 내용</td>
```
### 6.5 병합 셀 처리 규칙
HWP/HWPX에서 병합된 셀은 **왼쪽 위 셀만 존재**하고, 병합에 포함된 다른 셀은 아예 없다.
HTML에서는 colspan/rowspan으로 표현하고, 병합된 위치의 `<td>`를 생략한다.
```
HWPX: colSpan="2", rowSpan="3" at (col=0, row=0)
→ 이 셀이 col 0~1, row 0~2를 차지
→ col=1/row=0, col=0/row=1, col=1/row=1, col=0/row=2, col=1/row=2 셀은 없음
HTML: <td colspan="2" rowspan="3">...</td>
→ 해당 행/열 위치에서 <td> 생략
```
---
## 7. 용지 설정 (PageDef / SecPr)
> HWP: `HWPTAG_PAGE_DEF` (구역 정의 하위)
> HWPX: `<hp:secPr>` → `<hp:pageDef>` (section*.xml 내)
> CSS: `@page`, `@media print`
### 7.1 용지 크기 사전 정의
| 용지 이름 | 가로 (mm) | 세로 (mm) | HWPUNIT (가로×세로) |
|----------|----------|----------|:---:|
| A4 | 210 | 297 | 59528 × 84188 |
| A3 | 297 | 420 | 84188 × 119055 |
| B5 | 176 | 250 | 49896 × 70866 |
| Letter | 215.9 | 279.4 | 61200 × 79200 |
| Legal | 215.9 | 355.6 | 61200 × 100800 |
### 7.2 여백 매핑
```xml
<!-- HWPX section0.xml -->
<hp:secPr>
<hp:pageDef width="59528" height="84188"
landscape="NARROWLY"> <!-- 좁게 = 세로 -->
<hp:margin left="8504" right="8504"
top="5668" bottom="4252"
header="4252" footer="4252"
gutter="0"/>
</hp:pageDef>
</hp:secPr>
```
```css
/* CSS 변환 */
@page {
size: A4 portrait; /* 210mm × 297mm */
margin-top: 20mm; /* 5668 / 7200 * 25.4 ≈ 20mm */
margin-bottom: 15mm; /* 4252 → 15mm */
margin-left: 30mm; /* 8504 → 30mm */
margin-right: 30mm; /* 8504 → 30mm */
}
/* 머리말/꼬리말 여백은 CSS에서 body padding으로 근사 */
```
### 7.3 용지 방향
| HWP 값 (bit 0) | HWPX 속성값 | CSS |
|:---:|:---:|:---:|
| 0 | `NARROWLY` (좁게) | `portrait` |
| 1 | `WIDELY` (넓게) | `landscape` |
---
## 8. 머리말/꼬리말 (Header/Footer)
> HWP: `HWPTAG_CTRL_HEADER` → 컨트롤 ID `head` / `foot`
> HWPX: `<hp:headerFooter>` (section*.xml 내, 또는 별도 header/footer 영역)
> HTML: 페이지 상단/하단 고정 영역
### 8.1 머리말/꼬리말 적용 범위
| HWP/HWPX 설정 | 의미 |
|:---:|:---:|
| 양쪽 | 모든 쪽에 동일 |
| 짝수쪽만 | 짝수 페이지 |
| 홀수쪽만 | 홀수 페이지 |
### 8.2 HTML 근사 표현
```html
<!-- 머리말 -->
<div class="page-header" style="
position: absolute; top: 0; left: 0; right: 0;
height: 15mm; /* header margin 값 */
padding: 0 30mm; /* 좌우 본문 여백 */
">
<table class="header-table">...</table>
</div>
<!-- 꼬리말 -->
<div class="page-footer" style="
position: absolute; bottom: 0; left: 0; right: 0;
height: 15mm; /* footer margin 값 */
padding: 0 30mm;
">
<span class="footer-text">페이지 번호</span>
</div>
```
---
## 9. 구역 정의 (Section)
> HWP: 구역 정의 컨트롤 (`secd`)
> HWPX: `<hp:secPr>` (section*.xml 최상위)
### 9.1 구역 속성
| 속성 | HWPX | CSS/HTML 대응 | 비고 |
|------|------|-------------|------|
| 머리말 감춤 | `hideHeader` | header 영역 display:none | |
| 꼬리말 감춤 | `hideFooter` | footer 영역 display:none | |
| 텍스트 방향 | `textDirection` | `writing-mode` | 0=가로, 1=세로 |
| 단 정의 | `<hp:colDef>` | CSS `columns` / `column-count` | |
| 쪽 번호 | `pageStartNo` | 쪽 번호 출력 값 | 0=이어서 |
---
## 10. HTML → HWPX → HWP 변환 파이프라인
### 10.1 전체 흐름
```
[HTML (우리 출력)]
↓ (1) HTML 파싱 → CSS 속성 추출
[중간 표현 (JSON)]
↓ (2) 이 가이드의 역방향 매핑
[HWPX (XML ZIP)]
↓ (3) 한컴오피스 변환 도구
[HWP (바이너리)]
```
### 10.2 단계별 매핑 방향
| 단계 | 입력 | 출력 | 참조할 가이드 섹션 |
|:---:|:---:|:---:|:---:|
| HTML → HWPX | CSS border | `<hh:borderFill>` 생성 | §2 역방향 |
| HTML → HWPX | CSS font | `<hh:charPr>` + `<hh:fontface>` | §3, §4 역방향 |
| HTML → HWPX | CSS text-align 등 | `<hh:paraPr>` | §5 역방향 |
| HTML → HWPX | `<table>` | `<hp:tbl>` + `<hp:tc>` | §6 역방향 |
| HTML → HWPX | @page CSS | `<hp:pageDef>` | §7 역방향 |
| HTML → HWPX | header/footer div | `<hp:headerFooter>` | §8 역방향 |
### 10.3 CSS → HWPX 역변환 예시
```python
def css_border_to_hwpx(css_border):
"""'0.12mm solid #000000' → HWPX 속성"""
parts = css_border.split()
width = parts[0] # '0.12mm'
style = parts[1] # 'solid'
color = parts[2] # '#000000'
hwpx_type = CSS_TO_BORDER_TYPE.get(style, 'SOLID')
return {
'type': hwpx_type,
'width': width,
'color': color
}
CSS_TO_BORDER_TYPE = {
'solid': 'SOLID', 'dashed': 'DASH', 'dotted': 'DOT',
'double': 'DOUBLE', 'ridge': 'THICK_3D', 'groove': 'THICK_3D_REV',
'outset': '3D', 'inset': '3D_REV', 'none': 'NONE'
}
```
### 10.4 HWPX ZIP 구조 생성
```
output.hwpx (ZIP)
├── META-INF/
│ └── manifest.xml ← 파일 목록
├── Contents/
│ ├── header.xml ← DocInfo (글꼴, 스타일, borderFill)
│ ├── section0.xml ← 본문 (문단, 표, 머리말/꼬리말)
│ └── content.hpf ← 콘텐츠 메타
├── BinData/ ← 이미지 등
├── Preview/
│ └── PrvImage.png ← 미리보기
└── version.xml ← 버전 정보
```
**header.xml 필수 구조**:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<hh:head xmlns:hh="...">
<hh:beginNum .../>
<hh:refList>
<hh:fontfaces>...</hh:fontfaces> <!-- §3 -->
<hh:borderFills>...</hh:borderFills> <!-- §2 -->
<hh:charProperties>...</hh:charProperties> <!-- §4 -->
<hh:tabProperties>...</hh:tabProperties>
<hh:numberingProperties>...</hh:numberingProperties>
<hh:bulletProperties>...</hh:bulletProperties>
<hh:paraProperties>...</hh:paraProperties> <!-- §5 -->
<hh:styles>...</hh:styles>
</hh:refList>
</hh:head>
```
**section0.xml 필수 구조**:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<hp:sec xmlns:hp="...">
<hp:secPr>
<hp:pageDef .../> <!-- §7 -->
<hp:headerFooter .../> <!-- §8 -->
</hp:secPr>
<hp:p paraPrIDRef="0" styleIDRef="0"> <!-- 문단 -->
<hp:run charPrIDRef="0">
<hp:t>텍스트</hp:t>
</hp:run>
</hp:p>
<hp:p ...>
<hp:ctrl>
<hp:tbl ...>...</hp:tbl> <!-- §6 -->
</hp:ctrl>
</hp:p>
</hp:sec>
```
---
## 11. 시스템 적용 가이드
### 11.1 적용 대상 모듈
| 모듈 | 파일 | 이 가이드 활용 방식 |
|------|------|:---:|
| **doc_template_analyzer.py** | HWPX → HTML 템플릿 추출 | §2,6,7,8 정방향 (HWPX→CSS) |
| **template_manager.py** | 추출된 스타일 저장/로드 | §2 borderFill ID 매핑 |
| **custom_doc_type.py** | HTML 문서 생성 | §2,4,5 CSS 값 참조 |
| **hwpx_converter.py** (예정) | HTML → HWPX 변환 | §2~8 역방향 (CSS→HWPX) |
| **hwp_converter.py** (예정) | HWPX → HWP 변환 | §1 단위 변환 |
### 11.2 하드코딩 제거 전략
**현재 문제 (AS-IS)**:
```python
# doc_template_analyzer.py에 하드코딩됨
border_css = "2px solid var(--primary)"
header_bg = "#E8F5E9"
```
**해결 방향 (TO-BE)**:
```python
# style.json에서 추출된 borderFill 참조
bf = style['border_fills']['3'] # id=3
border_css = f"{bf['top']['width']} {bf['top']['css_style']} {bf['top']['color']}"
# → "0.12mm solid #000000"
header_bf = style['border_fills']['4'] # id=4 (헤더 배경 포함)
header_bg = header_bf.get('background', 'none')
# → "#E8F5E9"
```
### 11.3 이 가이드를 코드에서 참조하는 방식
이 문서(`hwpx_domain_guide.md`)는 다음과 같이 활용한다:
1. **변환 테이블을 JSON으로 추출**`hwpx_mappings.json`
- 테두리선 종류, 굵기, 색상 변환 등의 룩업 테이블
2. **변환 함수 라이브러리**`hwpx_utils.py`
- `hwpunit_to_mm()`, `borderfill_to_css()`, `css_border_to_hwpx()`
3. **AI 프롬프트 컨텍스트** → 문서 유형/구조 분석 시 참조
- "이 HWPX의 borderFill id=3은 실선 0.12mm 검정이므로 표 일반 셀에 해당"
4. **검증 기준** → 변환 결과물 검증 시 정확성 확인
- 추출된 CSS가 원본 HWPX의 스펙과 일치하는지
---
## 부록 A. 빠른 참조 — HWPX XML 태그 ↔ HWP 레코드 대응
| HWP 레코드 (Tag ID) | HWPX XML 엘리먼트 | 위치 |
|---------------------|------------------|------|
| HWPTAG_DOCUMENT_PROPERTIES | `<hh:beginNum>` 등 | header.xml |
| HWPTAG_ID_MAPPINGS | (암묵적) | header.xml |
| HWPTAG_FACE_NAME | `<hh:font>` | header.xml > fontfaces |
| HWPTAG_BORDER_FILL | `<hh:borderFill>` | header.xml > borderFills |
| HWPTAG_CHAR_SHAPE | `<hh:charPr>` | header.xml > charProperties |
| HWPTAG_TAB_DEF | `<hh:tabPr>` | header.xml > tabProperties |
| HWPTAG_NUMBERING | `<hh:numbering>` | header.xml > numberingProperties |
| HWPTAG_BULLET | `<hh:bullet>` | header.xml > bulletProperties |
| HWPTAG_PARA_SHAPE | `<hh:paraPr>` | header.xml > paraProperties |
| HWPTAG_STYLE | `<hh:style>` | header.xml > styles |
| HWPTAG_PARA_HEADER | `<hp:p>` | section*.xml |
| HWPTAG_TABLE | `<hp:tbl>` | section*.xml > p > ctrl |
| (셀 속성) | `<hp:tc>` | section*.xml > tbl > tr > tc |
| HWPTAG_PAGE_DEF | `<hp:pageDef>` | section*.xml > secPr |
| (머리말/꼬리말) | `<hp:headerFooter>` | section*.xml > secPr |
## 부록 B. 빠른 참조 — CSS → HWPX 역변환
| CSS 속성 | HWPX 대응 | 변환 공식 |
|----------|----------|----------|
| `font-family` | `<hh:font face="...">` | 첫 번째 값 → hangul, 두 번째 → latin |
| `font-size: 10pt` | `<hh:charPr height="1000">` | pt × 100 |
| `font-weight: bold` | `bold="true"` | |
| `font-style: italic` | `italic="true"` | |
| `color: #1a365d` | `color="#1a365d"` | 동일 |
| `text-align: center` | `align="CENTER"` | 대문자 |
| `margin-left: 30mm` | `left="8504"` | mm → HWPUNIT |
| `line-height: 160%` | `lineSpacing="160"` + `type="PERCENT"` | |
| `border: 1px solid #000` | `<hh:borderFill>` 내 각 방향 | §2 참조 |
| `background-color: #E8F5E9` | `<hh:windowBrush faceColor="...">` | |
| `padding: 2mm 5mm` | `<hp:cellMargin top="567" left="1417">` | mm → HWPUNIT |
| `width: 210mm` | `width="59528"` | mm → HWPUNIT |
| `@page { size: A4 }` | `<hp:pageDef width="59528" height="84188">` | |
---
*이 가이드는 한글과컴퓨터의 "글 문서 파일 구조 5.0 (revision 1.3)"을 참고하여 작성되었습니다.*

323
domain/hwpx/hwpx_utils.py Normal file
View File

@@ -0,0 +1,323 @@
# -*- coding: utf-8 -*-
"""
HWP/HWPX ↔ HTML/CSS 변환 유틸리티
hwpx_domain_guide.md의 매핑 테이블을 코드화.
하드코딩 없이 이 모듈의 함수/상수를 참조하여 정확한 변환을 수행한다.
참조: 한글과컴퓨터 "글 문서 파일 구조 5.0" (revision 1.3, 2018-11-08)
"""
# ================================================================
# §1. 단위 변환
# ================================================================
def hwpunit_to_mm(hwpunit):
"""HWPUNIT → mm (1 HWPUNIT = 1/7200 inch)"""
return hwpunit / 7200 * 25.4
def hwpunit_to_pt(hwpunit):
"""HWPUNIT → pt"""
return hwpunit / 7200 * 72
def hwpunit_to_px(hwpunit, dpi=96):
"""HWPUNIT → px (기본 96dpi)"""
return hwpunit / 7200 * dpi
def mm_to_hwpunit(mm):
"""mm → HWPUNIT"""
return mm / 25.4 * 7200
def pt_to_hwpunit(pt):
"""pt → HWPUNIT"""
return pt / 72 * 7200
def px_to_hwpunit(px, dpi=96):
"""px → HWPUNIT"""
return px / dpi * 7200
def charsize_to_pt(hwp_size):
"""HWP 글자 크기 → pt (100 스케일 제거)
예: 1000 → 10pt, 2400 → 24pt
"""
return hwp_size / 100
def pt_to_charsize(pt):
"""pt → HWP 글자 크기
예: 10pt → 1000, 24pt → 2400
"""
return int(pt * 100)
# ================================================================
# §1.3 색상 변환
# ================================================================
def colorref_to_css(colorref):
"""HWP COLORREF (0x00BBGGRR) → CSS #RRGGBB
HWP는 리틀 엔디안 BGR 순서:
- 0x00FF0000 → B=255,G=0,R=0 → #0000ff (파랑)
- 0x000000FF → B=0,G=0,R=255 → #ff0000 (빨강)
"""
r = colorref & 0xFF
g = (colorref >> 8) & 0xFF
b = (colorref >> 16) & 0xFF
return f'#{r:02x}{g:02x}{b:02x}'
def css_to_colorref(css_color):
"""CSS #RRGGBB → HWP COLORREF (0x00BBGGRR)"""
css_color = css_color.lstrip('#')
if len(css_color) == 3: # 단축형 #rgb → #rrggbb
css_color = ''.join(c * 2 for c in css_color)
r = int(css_color[0:2], 16)
g = int(css_color[2:4], 16)
b = int(css_color[4:6], 16)
return (b << 16) | (g << 8) | r
# ================================================================
# §2. 테두리/배경 (BorderFill) 매핑
# ================================================================
# §2.1 테두리선 종류: HWPX type → CSS border-style
BORDER_TYPE_TO_CSS = {
'NONE': 'none',
'SOLID': 'solid',
'DASH': 'dashed',
'DOT': 'dotted',
'DASH_DOT': 'dashed', # CSS 근사
'DASH_DOT_DOT': 'dashed', # CSS 근사
'LONG_DASH': 'dashed',
'CIRCLE': 'dotted', # CSS 근사 (큰 동그라미 → dot)
'DOUBLE': 'double',
'THIN_THICK': 'double', # CSS 근사
'THICK_THIN': 'double', # CSS 근사
'THIN_THICK_THIN':'double', # CSS 근사
'WAVE': 'solid', # CSS 근사 (물결 → 실선)
'DOUBLE_WAVE': 'double', # CSS 근사
'THICK_3D': 'ridge',
'THICK_3D_REV': 'groove',
'3D': 'outset',
'3D_REV': 'inset',
}
# CSS border-style → HWPX type (역방향)
CSS_TO_BORDER_TYPE = {
'none': 'NONE',
'solid': 'SOLID',
'dashed': 'DASH',
'dotted': 'DOT',
'double': 'DOUBLE',
'ridge': 'THICK_3D',
'groove': 'THICK_3D_REV',
'outset': '3D',
'inset': '3D_REV',
}
# §2.2 HWP 바이너리 테두리 굵기 값 → 실제 mm
BORDER_WIDTH_HWP_TO_MM = {
0: 0.1, 1: 0.12, 2: 0.15, 3: 0.2, 4: 0.25, 5: 0.3,
6: 0.4, 7: 0.5, 8: 0.6, 9: 0.7, 10: 1.0, 11: 1.5,
12: 2.0, 13: 3.0, 14: 4.0, 15: 5.0,
}
# ================================================================
# §5. 정렬 매핑
# ================================================================
# §5.1 HWPX align → CSS text-align
ALIGN_TO_CSS = {
'JUSTIFY': 'justify',
'LEFT': 'left',
'RIGHT': 'right',
'CENTER': 'center',
'DISTRIBUTE': 'justify', # CSS 근사
'DISTRIBUTE_SPACE': 'justify', # CSS 근사
}
# CSS text-align → HWPX align (역방향)
CSS_TO_ALIGN = {
'justify': 'JUSTIFY',
'left': 'LEFT',
'right': 'RIGHT',
'center': 'CENTER',
}
# ================================================================
# §5.2 줄 간격 매핑
# ================================================================
LINE_SPACING_TYPE_TO_CSS = {
'PERCENT': 'percent', # 글자에 따라 (%) → line-height: 160%
'FIXED': 'fixed', # 고정값 → line-height: 24pt
'BETWEEN_LINES': 'between', # 여백만 지정
'AT_LEAST': 'at_least', # 최소
}
# ================================================================
# 종합 변환 함수
# ================================================================
def hwpx_border_to_css(border_attrs):
"""HWPX 테두리 속성 dict → CSS border 문자열
Args:
border_attrs: {'type': 'SOLID', 'width': '0.12mm', 'color': '#000000'}
Returns:
'0.12mm solid #000000' 또는 'none'
"""
btype = border_attrs.get('type', 'NONE')
if btype == 'NONE' or btype is None:
return 'none'
width = border_attrs.get('width', '0.12mm')
color = border_attrs.get('color', '#000000')
css_style = BORDER_TYPE_TO_CSS.get(btype, 'solid')
return f'{width} {css_style} {color}'
def css_border_to_hwpx(css_border):
"""CSS border 문자열 → HWPX 속성 dict
Args:
css_border: '0.12mm solid #000000' 또는 'none'
Returns:
{'type': 'SOLID', 'width': '0.12mm', 'color': '#000000'}
"""
if not css_border or css_border.strip() == 'none':
return {'type': 'NONE', 'width': '0mm', 'color': '#000000'}
parts = css_border.strip().split()
width = parts[0] if len(parts) > 0 else '0.12mm'
style = parts[1] if len(parts) > 1 else 'solid'
color = parts[2] if len(parts) > 2 else '#000000'
return {
'type': CSS_TO_BORDER_TYPE.get(style, 'SOLID'),
'width': width,
'color': color,
}
def hwpx_borderfill_to_css(bf_element_attrs):
"""HWPX <hh:borderFill> 전체 속성 → CSS dict
Args:
bf_element_attrs: {
'left': {'type': 'SOLID', 'width': '0.12mm', 'color': '#000000'},
'right': {'type': 'SOLID', 'width': '0.12mm', 'color': '#000000'},
'top': {'type': 'SOLID', 'width': '0.12mm', 'color': '#000000'},
'bottom': {'type': 'SOLID', 'width': '0.12mm', 'color': '#000000'},
'background': '#E8F5E9' or None,
}
Returns:
{
'border-left': '0.12mm solid #000000',
'border-right': '0.12mm solid #000000',
'border-top': '0.12mm solid #000000',
'border-bottom': '0.12mm solid #000000',
'background-color': '#E8F5E9',
}
"""
css = {}
for side in ['left', 'right', 'top', 'bottom']:
border = bf_element_attrs.get(side, {})
css[f'border-{side}'] = hwpx_border_to_css(border)
bg = bf_element_attrs.get('background')
if bg and bg != 'none':
css['background-color'] = bg
return css
def hwpx_align_to_css(hwpx_align):
"""HWPX 정렬 값 → CSS text-align"""
return ALIGN_TO_CSS.get(hwpx_align, 'left')
def css_align_to_hwpx(css_align):
"""CSS text-align → HWPX 정렬 값"""
return CSS_TO_ALIGN.get(css_align, 'LEFT')
def hwpx_line_spacing_to_css(spacing_type, spacing_value):
"""HWPX 줄 간격 → CSS line-height
Args:
spacing_type: 'PERCENT' | 'FIXED' | 'BETWEEN_LINES' | 'AT_LEAST'
spacing_value: 숫자값
Returns:
CSS line-height 문자열 (예: '160%', '24pt')
"""
if spacing_type == 'PERCENT':
return f'{spacing_value}%'
elif spacing_type == 'FIXED':
pt = hwpunit_to_pt(spacing_value)
return f'{pt:.1f}pt'
else:
return f'{spacing_value}%' # 기본 근사
# ================================================================
# 용지 크기 사전 정의 (§7.1)
# ================================================================
PAPER_SIZES = {
'A4': {'width_mm': 210, 'height_mm': 297, 'width_hu': 59528, 'height_hu': 84188},
'A3': {'width_mm': 297, 'height_mm': 420, 'width_hu': 84188, 'height_hu': 119055},
'B5': {'width_mm': 176, 'height_mm': 250, 'width_hu': 49896, 'height_hu': 70866},
'Letter': {'width_mm': 215.9, 'height_mm': 279.4, 'width_hu': 61200, 'height_hu': 79200},
'Legal': {'width_mm': 215.9, 'height_mm': 355.6, 'width_hu': 61200, 'height_hu': 100800},
}
def detect_paper_size(width_hu, height_hu, tolerance=200):
"""HWPUNIT 용지 크기 → 용지 이름 추정
Args:
width_hu: 가로 크기 (HWPUNIT)
height_hu: 세로 크기 (HWPUNIT)
tolerance: 허용 오차 (HWPUNIT)
Returns:
'A4', 'A3', 'Letter' 등 또는 'custom'
"""
for name, size in PAPER_SIZES.items():
if (abs(width_hu - size['width_hu']) <= tolerance and
abs(height_hu - size['height_hu']) <= tolerance):
return name
# landscape 체크
if (abs(width_hu - size['height_hu']) <= tolerance and
abs(height_hu - size['width_hu']) <= tolerance):
return f'{name}_landscape'
return 'custom'
# ================================================================
# 편의 함수
# ================================================================
def css_style_string(css_dict):
"""CSS dict → CSS style 문자열
예: {'border-left': '1px solid #000', 'padding': '5mm'}
'border-left: 1px solid #000; padding: 5mm;'
"""
return ' '.join(f'{k}: {v};' for k, v in css_dict.items() if v)
def mm_format(hwpunit, decimal=1):
"""HWPUNIT → 'Xmm' 포맷 문자열"""
mm = hwpunit_to_mm(hwpunit)
return f'{mm:.{decimal}f}mm'