Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,38 @@
복사해서, 그 칸의 그림·도형이 같은 번호로 겹쳤다.
- 객체 모델 `set_column_widths`·`equalize_column_widths`가 칸을 안쪽 표보다 좁게 만들면 바이트 편집처럼 거부하고 표를
그대로 둔다(`TableStructureError`).
- 폐기 경고와 `docs/migration-6.0.md`가 없는 이름으로 안내하던 두 곳을 바로잡았다. `doc.style(…)`의 이동 대상은
`doc.styles.get`, `doc.set_check_box(…)`의 이동 대상은 `doc.fields.set_check_box`다(전에는 없는
`doc.styles.style`·`doc.fields.check_box(...).checked`를 가리켰다). 이동 대상 79개가 실제 속성으로 풀리는지
테스트가 확인한다. `llms.txt`는 `.hwp`가 열린다고 적고, `examples/build_release_checklist.py`는 6.x 이름으로
끝까지 돈다(전에는 없는 `document.save()`를 불렀다).
- 공개 예외 7종(`EquationConversionError`, `UnsupportedLatexError`, `MathMLUnavailableError`, `DocumentIngestError`,
`UnsupportedDocumentFormat`, `RecoverError`, `WordBoxExtractorRequired`)이 `HwpxError`도 상속한다. 기존 부모
(`ValueError`·`RuntimeError`)는 그대로라 기존 `except`는 전처럼 잡는다. 각자 `code`가 생겼다
(`equation-conversion`, `equation-latex-unsupported`, `equation-mathml-unavailable`, `ingest-failed`,
`ingest-format-unsupported`, `recover-unsafe`, `toc-word-box-extractor-required`).
- 손상된 HWPX를 열 때 나던 날 예외를 `HwpxStructureError` 하위 클래스로 묶었다. ZIP이 아니거나 잘린 입력은
`HwpxArchiveError`, 압축 데이터 손상은 `HwpxCompressedDataError`, 깨진 XML 파트(암호 걸린 HWPX 포함)는
`HwpxXmlSyntaxError`(`part_name`에 파트 이름)다. 각 클래스는 전에 나던 `zipfile.BadZipFile`·`zlib.error`·
`lxml.etree.XMLSyntaxError`도 상속하므로 기존 `except`는 그대로 잡고, 원래 예외는 `__cause__`에 있다.
`HwpxDocument.open`의 독스트링과 `docs/stable-api.md`가 이 표를 적는다.
- `HwpxDocument.new()`로 만든 문서가 동봉 템플릿의 지은이(`synthetic-fixture-author`)와 만든 날(2025-09-17)을 물려받지
않는다. 지은이·마지막 저장한 이·`date`는 비고, `CreatedDate`·`ModifiedDate`는 만든 시각(UTC)이다
(`SOURCE_DATE_EPOCH`가 있으면 그 시각). 지은이는 `doc.package.set_document_metadata(creator=...)`로 넣는다.
- 버전 이력이 있는 한/글 문서에서 `doc.parts.histories`의 `historylastdoc.hml`(마지막 문서의 HWPML 스냅숏)이
`to_model()`에서 `HwpxValueError`를 내지 않고 항목 없는 `History`를 돌려준다. 버전 기록인지는 새 속성
`is_version_log`로 가린다.
- `doc.text.html()`이 병합된 표 칸에 `colspan`·`rowspan`을 쓴다. 전에는 병합 표의 뒤 칸이 왼쪽으로 밀렸다.
- `doc.tables.all`의 설명을 실제 범위(본문 문단에 바로 놓인 표)로 고쳤다. 동작은 그대로다.
- `hwpx.ingest`의 HWPX → Markdown 변환이 손실을 항상 `"low"`로 보고하지 않는다. Markdown에 글이 실리지 않은 요소
(머리말·꼬리말·메모·각주·미주·글상자·수식·덧말·글자 겹침·양식 개체 캡션·글맵시·바탕쪽)를 결과와 대조해 종류별로
세어 `warnings`와 `metadata["left_out"]`에 싣고, 빠진 것이 있으면 `lossiness`가 `"medium"`(빠진 글이 실린 글보다
많으면 `"high"`)이다. 변경 추적으로 지운 글 표시가 있으면 따로 경고한다. 빠진 것이 없으면 전처럼 `"low"`다.
- `describe_capabilities()`의 자리 안내를 실제와 맞췄다. 차례·상호참조(`hwpx.tools.toc_author`), 제목 차례 표시·색인
표시·날짜/교정 부호/파일 이름 필드·메일머지 표시 필드(`HwpxOxmlParagraph.add_*`)는 `doc.refs`·`doc.fields`에 없으므로
`namespace`가 `None`이고 진입점이 그 모듈·문단 클래스다. 지원 매트릭스의 같은 표와 arc·polygon·curve·connectLine 행,
차트 행(`add_chart(chart_xml)`)도 고쳤다.
- 커버리지 장부가 `.hwp` 변환기(`hwp5/`)가 써내는 태그를 읽기로 세지 않는다(읽기 266 → 256).

### 수정

Expand Down
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
3. **개발 도구를 설치**합니다. 배포 메타데이터가 `pyproject.toml`에 정의되어 있어
편집 가능한 설치로 개발 의존성을 한 번에 구성할 수 있습니다:
```bash
python -m pip install -e .[dev]
python -m pip install -e ".[dev]"
```
`dev`에는 CI가 설치하는 `test`·`typecheck`(pytest, ruff, mypy, pyright, 테스트가
쓰는 pillow·openpyxl·pyyaml 등)와 배포 점검용 twine이 들어 있습니다. CI와 똑같이
맞추려면 `python -m pip install -e ".[test,typecheck]"`를 써도 됩니다.
4. **임포트 확인**. `pip install -e`를 실행하면 `hwpx` 패키지가 가상 환경에
설치되어 추가적인 `PYTHONPATH` 설정이 필요 없습니다. 인터프리터에서
`import hwpx`가 정상적으로 동작하는지 확인하세요.
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ doc.save_to_path("계획.hwpx")

## 할 수 있는 것

- 읽기·추출: 텍스트·HTML·Markdown으로 내보냅니다. 서식, 표 안의 표, 각주를 살립니다.
- 읽기·추출: 텍스트·HTML·Markdown으로 내보냅니다. 서식과 표 안의 표를 살립니다. 각주·미주는 `notes=True`를 주거나
`markdown(rich=True)`·`TextExtractor`를 쓸 때 나오고, 머리말·꼬리말·메모는 `TextExtractor`에서만 나옵니다.
수식·글자 겹침·양식 개체 캡션은 아직 어느 경로에서도 나오지 않습니다. Markdown 변환(`hwpx.ingest`)은 빠진
종류와 개수를 `warnings`와 `lossiness`로 알립니다.
- 편집: 문단·표·그림·머리말/꼬리말·메모·각주, 줄 간격·여백·쪽 번호 같은 서식을 고칩니다.
- 양식 채우기: 라벨로 칸을 찾아 값만 넣고 손대지 않은 부분은 바이트 그대로 둡니다. FormFit은 값이
칸에 들어가도록 줄을 바꾸거나 글자를 줄일지 한/글의 글자 폭으로 재서 고릅니다.
Expand Down
5 changes: 4 additions & 1 deletion README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ For higher-level workflows (document authoring, form filling, exam typesetting),

## What it does

- Read and extract: text, HTML and Markdown export that keeps formatting, nested tables and footnotes.
- Read and extract: text, HTML and Markdown export that keeps formatting and nested tables. Footnotes and endnotes
come out with `notes=True`, `markdown(rich=True)` or `TextExtractor`; headers, footers and memos only from
`TextExtractor`. Equations, composed characters and form-object captions are not extracted by any path yet.
The Markdown converter (`hwpx.ingest`) reports what it left out in `warnings` and `lossiness`.
- Edit: paragraphs, tables, pictures, headers and footers, memos, footnotes; line spacing, margins, page numbers.
- Fill forms: find a cell by its label and change only the value; parts you did not touch keep their bytes.
FormFit decides whether a value should wrap or shrink to fit its cell, measuring with Hangul's glyph widths.
Expand Down
8 changes: 6 additions & 2 deletions docs/_extra/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,12 @@ Use the API paths below rather than inferring names from python-docx.
- Wrong: `Document()` / `doc.paragraphs[0].runs` — those are python-docx
idioms. Use `HwpxDocument.new()` / `document.text.runs()`. (`add_paragraph`
and `add_heading` do exist.)
- `.hwp` (HWP 5.x binary) is a different format: opening raises `BadZipFile`
with guidance. Encrypted HWPX is rejected fail-closed.
- `.hwp` (HWP 5.x binary) opens too: `HwpxDocument.open("file.hwp")` converts it
into the same document model. `document.conversion_report` counts what the
conversion could not carry over (`None` for a document opened from HWPX), and
a `Hwp5ConversionWarning` is raised once when something was skipped.
Password-protected, distribution and DRM `.hwp` files raise `Hwp5Error`.
Encrypted HWPX is rejected fail-closed.
- All `add_*` calls append (not idempotent); check existence first by
traversal when you need "add once".
- Report content verification, untouched-part preservation, package/open-safety
Expand Down
8 changes: 5 additions & 3 deletions docs/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,7 @@
- `conversion_report -> Hwp5ConversionReport | None`
- `.hwp`를 열 때 문서 모델로 옮기지 못한 내용의 개수(`unconverted`·`dropped`, 읽기 전용).
`.hwp`로 열지 않은 문서에서는 `None`입니다.
- `save(path_or_stream=None) -> str | PathLike[str] | BinaryIO | bytes`
- 하위 호환용 래퍼입니다. 내부에서 `save_to_path()`/`save_to_stream()`/`to_bytes()`를 호출하며 `DeprecationWarning`을 발생시킵니다.
- `save()`는 없습니다. 4.0에서 없앴고, 부르면 `AttributeError`가 납니다. 경로에는 `save_to_path()`, 스트림에는 `save_to_stream()`, 바이트에는 `to_bytes()`를 씁니다.

***

Expand Down Expand Up @@ -573,7 +572,10 @@
### 예외

- `HwpxPackageError`: 패키지 조작 오류에 대한 기본 예외입니다.
- `HwpxStructureError`: HWPX 패키지의 필수 구조 구성 요소가 없거나 일치하지 않을 때 발생합니다.
- `HwpxStructureError`: HWPX 패키지의 필수 구조 구성 요소가 없거나 일치하지 않을 때 발생합니다. 아래 셋의 부모입니다.
- `HwpxArchiveError`: ZIP이 아니거나 잘린 입력. `zipfile.BadZipFile`도 상속합니다.
- `HwpxCompressedDataError`: 압축 데이터가 손상된 입력. `zlib.error`도 상속합니다.
- `HwpxXmlSyntaxError`: XML 파트가 깨진 입력. `lxml.etree.XMLSyntaxError`도 상속하고 `part_name`에 파트 이름을 담습니다.

### 데이터 클래스

Expand Down
25 changes: 13 additions & 12 deletions docs/coverage-ledger.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"docs/openrate/report-v22.json"
],
"skeleton": "src/hwpx/data/Skeleton.hwpx",
"sourceScanned": "src/hwpx/**/*.py"
"sourceScanned": "src/hwpx/**/*.py",
"readScanExcludes": "src/hwpx/hwp5/**/*.py"
},
"corpusTotalFiles": 237,
"corpusUnknownFiles": {
Expand Down Expand Up @@ -58,7 +59,7 @@
"schemaDeclared": 307,
"corpusOnly": 38,
"corpusObserved": 229,
"codeRead": 266,
"codeRead": 256,
"codeWriteApi": 209,
"codeWriteFrozenTemplate": 10,
"codeWriteNone": 126,
Expand Down Expand Up @@ -228,7 +229,7 @@
"schemaSource": null,
"corpusFrequency": 0.0084,
"corpusFileCount": 2,
"codeRead": true,
"codeRead": false,
"codeWrite": "none",
"capabilityArea": null,
"capabilityStatus": null,
Expand Down Expand Up @@ -3246,7 +3247,7 @@
"schemaSource": "ParaList XML schema.xml",
"corpusFrequency": 0.0042,
"corpusFileCount": 1,
"codeRead": true,
"codeRead": false,
"codeWrite": "none",
"capabilityArea": null,
"capabilityStatus": null,
Expand Down Expand Up @@ -3428,7 +3429,7 @@
"schemaSource": "ParaList XML schema.xml",
"corpusFrequency": 0.0042,
"corpusFileCount": 1,
"codeRead": true,
"codeRead": false,
"codeWrite": "none",
"capabilityArea": null,
"capabilityStatus": null,
Expand Down Expand Up @@ -3554,7 +3555,7 @@
"schemaSource": "ParaList XML schema.xml",
"corpusFrequency": 0.0084,
"corpusFileCount": 2,
"codeRead": true,
"codeRead": false,
"codeWrite": "none",
"capabilityArea": null,
"capabilityStatus": null,
Expand Down Expand Up @@ -3827,7 +3828,7 @@
"schemaSource": "ParaList XML schema.xml",
"corpusFrequency": 0.0042,
"corpusFileCount": 1,
"codeRead": true,
"codeRead": false,
"codeWrite": "none",
"capabilityArea": null,
"capabilityStatus": null,
Expand Down Expand Up @@ -4464,7 +4465,7 @@
"schemaSource": "ParaList XML schema.xml",
"corpusFrequency": 0.0,
"corpusFileCount": 0,
"codeRead": true,
"codeRead": false,
"codeWrite": "none",
"capabilityArea": null,
"capabilityStatus": null,
Expand Down Expand Up @@ -4672,7 +4673,7 @@
"schemaSource": "ParaList XML schema.xml",
"corpusFrequency": 0.0042,
"corpusFileCount": 1,
"codeRead": true,
"codeRead": false,
"codeWrite": "none",
"capabilityArea": null,
"capabilityStatus": null,
Expand Down Expand Up @@ -4740,7 +4741,7 @@
"schemaSource": "ParaList XML schema.xml",
"corpusFrequency": 0.0042,
"corpusFileCount": 1,
"codeRead": true,
"codeRead": false,
"codeWrite": "none",
"capabilityArea": null,
"capabilityStatus": null,
Expand Down Expand Up @@ -5215,7 +5216,7 @@
"schemaSource": "ParaList XML schema.xml",
"corpusFrequency": 0.0084,
"corpusFileCount": 2,
"codeRead": true,
"codeRead": false,
"codeWrite": "none",
"capabilityArea": null,
"capabilityStatus": null,
Expand Down Expand Up @@ -5469,7 +5470,7 @@
"schemaSource": "ParaList XML schema.xml",
"corpusFrequency": 0.0042,
"corpusFileCount": 1,
"codeRead": true,
"codeRead": false,
"codeWrite": "none",
"capabilityArea": null,
"capabilityStatus": null,
Expand Down
26 changes: 13 additions & 13 deletions docs/coverage-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
**5) 속성 축.** 요소별 관측 속성 **이름** 집합을 census가 함께 기록한다(`observedAttributes` 컬럼, 값 빈도까지는 이번 사이클 범위 밖 — 생성기 독스트링에 명시).
**6) openrate 코퍼스 환류(v4~v21).** `docs/openrate/report-v{4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21}.json`의 스트라타별 실한컴 수용(`render_checked>0`·`render_failed==0`, 구세대 스키마는 `opened==requested>0`도 함께)을 `verificationBasis`로 환류한다(`by-openrate-corpus`/`by-capability-area+openrate-corpus`) — 2026-08-04 감사 R4가 지목한 v4 배선(원 이름 `by-v4-corpus`)을 2026-08 사이클 6.5 트레인⑰에서 v8까지, 사이클 6.6 트레인⑳에서 v9까지, 사이클 6.7 트레인㉔에서 v10까지 확장하며 버전-중립 이름으로 바꿨다. 두 경로로 매핑한다: 이미 등록된 capabilityArea와 1:1 대응하는 스트라타는 `_OPENRATE_STRATUM_TO_CAPABILITY_AREA`(차트·체크박스·수식·각주 2종·테두리채우기·하이라이트·메모·그룹컨테이너·문서옵션호환성), capabilityArea가 아직 없거나 있어도 혼합 지원 영역이라 요소만 지목해야 하는 신규 능력(글꼴·탭·도형텍스트·캡션·쪽번호제어·문자서식·필드파라미터·arc/polygon·인라인 원자 3종)은 `_OPENRATE_STRATUM_TO_ELEMENTS`로 요소를 직접 지목한다 — 둘 다 근거는 각 생성기 스크립트 독스트링이 실제로 부른다고 명시하는 것뿐(무근거 매핑 금지, `fieldBegin`을 일부러 안 매핑한 것과 같은 원칙).

**전 vs 후 (감사가 하한을 인용한 것과 같은 슬라이스 — corpusFileCount>0인 요소만)**: 감사 인용 하한은 관측 228건 중 write=none **70** · read=none **56** · frozen-template **28**([감사 판정문](2026-08-04-completeness-audit-verdict.md) 요약표). 이 원장 재생성 기준으로는 관측 229건 중 write=none **21** · read=none **2** · frozen-template **10**. **주의**: 두 population이 다르다(모집단을 재정의했다 — 위 4항목) — 이 비교는 "같은 잣대로 다시 잰 정확한 델타"가 아니라 분류기 수리가 방향대로 움직였는지의 참고 신호다. 분류기 수리 자체의 정확도 증거는 위 1~3항의 요소별 재현 로그가 1차 근거다.
**전 vs 후 (감사가 하한을 인용한 것과 같은 슬라이스 — corpusFileCount>0인 요소만)**: 감사 인용 하한은 관측 228건 중 write=none **70** · read=none **56** · frozen-template **28**([감사 판정문](2026-08-04-completeness-audit-verdict.md) 요약표). 이 원장 재생성 기준으로는 관측 229건 중 write=none **21** · read=none **11** · frozen-template **10**. **주의**: 두 population이 다르다(모집단을 재정의했다 — 위 4항목) — 이 비교는 "같은 잣대로 다시 잰 정확한 델타"가 아니라 분류기 수리가 방향대로 움직였는지의 참고 신호다. 분류기 수리 자체의 정확도 증거는 위 1~3항의 요소별 재현 로그가 1차 근거다.

## 전체 통계

Expand All @@ -28,7 +28,7 @@
| 스키마 선언 | 307 | 89.0% |
| 코퍼스에만 있음(스키마 미대응) | 38 | 11.0% |
| 실코퍼스에서 관측(빈도>0) | 229 | 66.4% |
| 코드 읽기 | 266 | 77.1% |
| 코드 읽기 | 256 | 74.2% |
| 코드 쓰기(api) | 209 | 60.6% |
| 쓰기 frozen-template | 10 | 2.9% |
| 쓰기 none | 126 | 36.5% |
Expand Down Expand Up @@ -63,36 +63,36 @@
| `hh:metaTag` | 0.0422 | 10 | True | frozen-template | — |
| `hh:forbiddenWord` | 0.0127 | 3 | True | none | — |
| `hh:forbiddenWordList` | 0.0127 | 3 | True | none | — |
| `hc:extent` | 0.0084 | 2 | True | none | — |
| `hp:endPt` | 0.0084 | 2 | True | none | — |
| `hc:extent` | 0.0084 | 2 | False | none | — |
| `hp:endPt` | 0.0084 | 2 | False | none | — |
| `hp:ole` | 0.0084 | 2 | True | none | — |
| `hp:startPt` | 0.0084 | 2 | True | none | — |
| `hp:startPt` | 0.0084 | 2 | False | none | — |
| `hp:btn` | 0.0042 | 1 | False | none | 체크박스 양식개체 |
| `hp:comboBox` | 0.0042 | 1 | True | none | — |
| `hp:controlPoints` | 0.0042 | 1 | True | none | — |
| `hp:controlPoints` | 0.0042 | 1 | False | none | — |
| `hp:edit` | 0.0042 | 1 | True | none | — |
| `hp:effect` | 0.0042 | 1 | True | none | — |
| `hp:hiddenComment` | 0.0042 | 1 | True | none | — |
| `hp:effect` | 0.0042 | 1 | False | none | — |
| `hp:hiddenComment` | 0.0042 | 1 | False | none | — |
| `hp:listItem` | 0.0042 | 1 | True | none | — |
| `hp:metaTag` | 0.0042 | 1 | True | none | — |
| `hp:point` | 0.0042 | 1 | True | none | — |
| `hp:presentation` | 0.0042 | 1 | True | none | — |
| `hp:point` | 0.0042 | 1 | False | none | — |
| `hp:presentation` | 0.0042 | 1 | False | none | — |
| `hp:radioBtn` | 0.0042 | 1 | False | none | 체크박스 양식개체 |
| `hp:text` | 0.0042 | 1 | True | none | — |
| `hp:textart` | 0.0042 | 1 | True | none | — |
| `hp:textartPr` | 0.0042 | 1 | True | none | — |
| `hp:textartPr` | 0.0042 | 1 | False | none | — |
| `hp:video` | 0.0042 | 1 | True | none | — |

## 네임스페이스별 표

| 네임스페이스 | 요소 수 | 스키마 선언 | 코퍼스 관측 | 읽기 | 쓰기 api | frozen-template | 쓰기 none |
|---|---|---|---|---|---|---|---|
| `ha` | 2 | 0 | 2 | 2 | 0 | 2 | 0 |
| `hc` | 31 | 7 | 29 | 31 | 23 | 5 | 3 |
| `hc` | 31 | 7 | 29 | 30 | 23 | 5 | 3 |
| `hh` | 126 | 125 | 64 | 72 | 63 | 3 | 60 |
| `hhs` | 10 | 10 | 0 | 10 | 0 | 0 | 10 |
| `hm` | 2 | 2 | 0 | 2 | 0 | 0 | 2 |
| `hp` | 171 | 161 | 132 | 146 | 121 | 0 | 50 |
| `hp` | 171 | 161 | 132 | 137 | 121 | 0 | 50 |
| `hs` | 1 | 1 | 1 | 1 | 1 | 0 | 0 |
| `hv` | 2 | 1 | 1 | 2 | 1 | 0 | 1 |

Expand Down
Loading
Loading