diff --git a/README.md b/README.md index 96c44b9..1f6d400 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@

+

ํ•œ๊ตญ์–ด | English

+ --- ## ๐Ÿงฉ HWPX Stack (3์ข…) @@ -174,6 +176,7 @@ hwpx-analyze-template ๋ณด๊ณ ์„œ.hwpx - **[๐Ÿš€ ๋น ๋ฅธ ์‹œ์ž‘](docs/quickstart.md)** ยท **[๐Ÿ“š ์‚ฌ์šฉ ๊ฐ€์ด๋“œ](docs/usage.md)** โ€” ์ฒซ ํŒŒ์ผ ์—ด๊ธฐ๋ถ€ํ„ฐ ๋ฌธ๋‹จยทํ‘œยท๋ฉ”๋ชจยท์„น์…˜ ํŽธ์ง‘, ํ…์ŠคํŠธ ์ถ”์ถœยท๊ฒ€์ฆ๊นŒ์ง€ - **[๐Ÿ’ก ์˜ˆ์ œ ๋ชจ์Œ](docs/examples.md)** ยท [`examples/`](examples/) โ€” `build_release_checklist.py`(๋ฉ”๋ชจยท์Šคํƒ€์ผ ํŽธ์ง‘ HWPX ์ƒ์„ฑ), `extract_text.py`(CLI ํ…์ŠคํŠธ ์ถ”์ถœ), `find_objects.py`(OWPML ๋…ธ๋“œ ์ถ”์ ) ๋“ฑ - **[๐Ÿ“ ์Šคํ‚ค๋งˆ ๊ฐœ์š”](docs/schema-overview.md)** ยท **[๐Ÿ”ง ์„ค์น˜ ๊ฒ€์ฆ](docs/installation.md)** +- **[๐Ÿ”ฌ HWPX ๋‚ด๋ถ€ ์‹ค์ „ ๊ฐ€์ด๋“œ](docs/internals/)** โ€” ์‹ค์ œ ํ•œ/๊ธ€ ๋™์ž‘์—์„œ ํ™•์ธ๋œ HWPUNITยท์กฐํŒ ์บ์‹œยท๋ชฉ์ฐจ ํ•„๋“œยทOPC ์žฌํŒจํ‚นยท๋ฉ”๋ชจยท์˜ค๋ผํด ํ•œ๊ณ„ - **[๐Ÿ“– ์ „์ฒด ๋ฌธ์„œ (Sphinx)](https://airmang.github.io/python-hwpx/)** โ€” API ๋ ˆํผ๋Ÿฐ์Šคยท50+ ์‹ค์ „ ํŒจํ„ดยทFAQ - **[๐Ÿ“ CHANGELOG](CHANGELOG.md)** ยท **[๐Ÿค CONTRIBUTING](CONTRIBUTING.md)** ยท **[๐Ÿ‘ฅ CONTRIBUTORS](CONTRIBUTORS.md)** diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..e86d254 --- /dev/null +++ b/README_EN.md @@ -0,0 +1,217 @@ +

+

python-hwpx

+

+ Read, edit, generate, and structurally validate HWPX documents in Python โ€” without Hancom Office. +

+

+ PyPI + Python + License + Docs +

+

+ +

ํ•œ๊ตญ์–ด | English

+ +--- + +## ๐Ÿงฉ HWPX Stack (3 components) + +| Layer | Repo | Role | +|---|---|---| +| ๐Ÿ“ฆ Library | **[`python-hwpx`](https://github.com/airmang/python-hwpx)** | Pure-Python HWPX parsing / editing / generation core | +| ๐Ÿ”Œ MCP server | [`hwpx-mcp-server`](https://github.com/airmang/hwpx-mcp-server) | Manipulate HWPX from MCP clients (Claude Desktop, VS Code, etc.) | +| ๐ŸŽฏ Agent skill | [`hwpx-plugin`](https://github.com/airmang/hwpx-plugins) | First-party plugin / skill bundle that lets agents use HWPX directly | + +`python-hwpx` is the core library providing HWPX parsing, editing, and generation, +while `hwpx-mcp-server` and `hwpx-plugin` are first-party integration components +maintained directly by the same project. +"First-party" refers to the project's maintenance relationship; it does not imply +official certification by Hancom or any third party. + +The current public PyPI release is `python-hwpx 3.6.0`. A plain +`pip install python-hwpx` installs this release. +The current package classifier is `Development Status :: 3 - Alpha`. This classifier +reflects the maturity of the API and product; it does not stand in for the public +version or the minimum compatible version of the plugin. + +--- + +## We speak in measurements โ€” Published Corpus + +The outputs of this stack are verified by **exhaustive measurement against real +Hancom Office**, not by claims (frozen corpus N=497, 2026-07-19; details and caveats +in the +[measured corpus metrics](https://airmang.github.io/python-hwpx/corpus-metrics.html)): + +- **Hancom open-acceptance rate 100%** (476/476 all-pass, lower bound โ‰ฅ99.4%) ยท parsing 96.2% +- **Byte preservation of untouched regions 100%** (497/497, patch path) ยท **personal-info 0-leak** +- 416 render-verified cases + an honesty bucket (PDF export of tracked-change documents is refused by Hancom itself โ€” published as a measured limitation) +- Form-fill differential is 49.2% on wild public forms โ€” **we publish the low number as-is** and record it as remaining work + +> These numbers are on the *output acceptance* axis (does real Hancom accept the files we produce). +> This is a different axis from document *parsing recall*, so do not compare it side by side with parser-project figures. + +--- + +## Why python-hwpx + +- **No Hancom Office required for core editing** โ€” HWPX is a ZIP+XML (OWPML/OPC) structure, so pure Python reads and writes it anywhere: Windows, macOS, Linux, CI. +- **From reading to generation in one core** โ€” text/format extraction, paragraph/table/form editing, new-document generation, and XSD schema validation are all handled by one API. +- **Agent- and automation-friendly** โ€” `hwpx-mcp-server` and `hwpx-plugin`, maintained by the same project, connect to the core. + +Document parsing, editing, and generation can be done in pure Python. However, to +assert final visual quality โ€” page breaks, table overflow, font substitution, and so +on โ€” you separately use a real Hancom render oracle as needed. + +## Quick start + +```bash +pip install python-hwpx # Python 3.10+ ยท lxml โ‰ฅ 4.9 +``` + +```python +from hwpx import HwpxDocument + +# Open an existing document โ†’ edit โ†’ save +doc = HwpxDocument.open("๋ณด๊ณ ์„œ.hwpx") +doc.add_paragraph("์ž๋™ํ™”๋กœ ์ถ”๊ฐ€ํ•œ ๋ฌธ๋‹จ์ž…๋‹ˆ๋‹ค.") +doc.save_to_path("๋ณด๊ณ ์„œ-์ˆ˜์ •.hwpx") + +# Create a new document +new = HwpxDocument.new() +new.add_paragraph("python-hwpx๋กœ ๋งŒ๋“  ์ƒˆ ๋ฌธ์„œ") +new.save_to_path("์ƒˆ๋ฌธ์„œ.hwpx") +``` + +> ๐Ÿ’ก A context manager is also supported โ€” resources are cleaned up automatically on leaving the `with` block: +> ```python +> with HwpxDocument.open("๋ณด๊ณ ์„œ.hwpx") as doc: +> doc.add_paragraph("์ž๋™์œผ๋กœ ๋ฆฌ์†Œ์Šค๊ฐ€ ์ •๋ฆฌ๋ฉ๋‹ˆ๋‹ค.") +> doc.save_to_path("๊ฒฐ๊ณผ๋ฌผ.hwpx") +> ``` + +Once you have the `open`/`new` โ†’ `edit`/`extract` โ†’ `save_to_path` flow, you can expand into the rest as needed. + +## What it does + +### ๐Ÿ” Read ยท Extract +- Text/HTML/Markdown export โ€” `export_text()` ยท `export_html()` ยท `export_markdown()` +- **Rich Markdown** โ€” `export_rich_markdown()` preserves inline formatting (`**bold**` ยท `*italic*` ยท `~~strikethrough~~`), nested tables (colspan/rowspan safe), shape text, images, footnotes/endnotes, hyperlinks, and heading auto-detection (`#`/`##`) +- **Document ingest gateway** โ€” `hwpx.ingest.DocumentIngestor` detects HWPX and normalizes it into rich Markdown plus section/table metadata +- `TextExtractor` / `ObjectFinder` โ€” iterate sections/paragraphs, find objects by tag/attribute/XPath (`hp:tab` is preserved as `\t`, roundtrip-safe) + +```python +doc = HwpxDocument.open("๋ณด๊ณ ์„œ.hwpx") +md = doc.export_rich_markdown( + image_dir="out/images", # extract BinData images to disk + image_ref_prefix="images/", # prefix for ![](images/...) paths in the markdown + detect_headings=True, # auto #/## based on โ… ./1. patterns +) +``` + +### โœ๏ธ Edit +- Add/remove/format paragraphs, run-level bold/italic/underline/color +- Add/remove sections (`add_section(after=)` ยท `remove_section()`, manifest managed automatically) +- Create tables, cell text, merge/split, nested tables, image embedding, headers/footers, memos (anchor-based), footnotes/endnotes, bookmarks/hyperlinks, multi-column editing +- **Edit formatting of existing documents** โ€” alignment ยท line spacing ยท indentation ยท paragraph spacing, paper ยท margins ยท orientation, page numbers, bullets/numbering +- **Style-based replacement** โ€” filter runs by color ยท underline ยท `charPrIDRef` and replace selectively (`replace_text_in_runs` ยท `find_runs_by_style`) + +```python +# Find and replace only red text +doc.replace_text_in_runs("์ž„์‹œ", "ํ™•์ •", text_color="#FF0000") +``` + +### ๐Ÿ–Š๏ธ Form filling (byte-preserving) +- Query and format-preserving fill of click-here (๋ˆ„๋ฆ„ํ‹€) fields, label-based cell lookup (`find_cell_by_label`) ยท path-based fill (`fill_by_path`) +- **Byte-preserving structural editing** โ€” cell filling / rowยทcolumnยทtable deleteยทinsert / column-width autofit / shrink-to-fit fonts performed without reassembling the document, preserving the form's formatting exactly. Untouched regions are left byte-for-byte intact by `hwpx.patch`, which splices section XML bytes + +```python +doc = HwpxDocument.open("์‹ ์ฒญ์„œ.hwpx") +result = doc.fill_by_path({ + "์„ฑ๋ช… > right": "ํ™๊ธธ๋™", + "์†Œ์† > right": "ํ”Œ๋žซํผํŒ€", +}) +doc.save_to_path("์‹ ์ฒญ์„œ-์ž‘์„ฑ์™„๋ฃŒ.hwpx") +print(result["applied_count"], result["failed_count"]) +``` + +### ๐Ÿ—๏ธ Generation ยท Official-document tools +- `hwpx.builder` โ€” assembly-style generation of Section/Heading/Table/Image/Header + a hard-gated save report +- Official-document tools โ€” `official_lint` (item-marker hierarchy ยท "๋." marker ยท attachments ยท date lint), approval-block presets +- `advanced_generators` โ€” photo boards (image_grid) ยท meeting nameplates ยท table-based org charts +- `mail_merge` โ€” bulk generation of N copies from template + data, table sum/average computation +- `doc_diff` โ€” paragraph LCS diff ยท old/new comparison tables ยท reference-consistency lint +- `style_profile` โ€” extract and apply reference-document profiles, template registry + +### โœ… Validation ยท Safety ยท Low-level +- XSD schema + package structure validation โ€” CLI `hwpx-validate` ยท `hwpx-validate-package`, `hwpx-analyze-template` +- `validate_editor_open_safety` โ€” gate for save/pack/repair/builder output, returns `openSafety` evidence +- `hwpx.tools.fuzz` (seeded deterministic scenarios ยท triple oracle) ยท `hwpx.tools.layout_preview` (page-box approximation HTML/PNG, self-verifying) ยท `opc.security` (XML entity ยท ZIP compression-bomb guards) +- Directly manipulate OWPML schema โ†” Python objects via `hwpx.oxml` dataclasses, with automatic HWPML 2016โ†’2011 namespace normalization + +```bash +hwpx-validate-package ๋ณด๊ณ ์„œ.hwpx +hwpx-analyze-template ๋ณด๊ณ ์„œ.hwpx +``` + +> For the full list of features, classes, and methods, see the [usage guide](docs/usage.md) and the [API reference](https://airmang.github.io/python-hwpx/api_reference.html). + +## Comparison with competing libraries + +| | python-hwpx | pyhwpx | pyhwp | +|---|---|---|---| +| **Target format** | `.hwpx` (OWPML/OPC) | `.hwpx` | `.hwp` (v5 binary) | +| **Hancom install** | Not required | Required (Windows COM) | Not required | +| **Cross-platform** | โœ… Linux / macOS / Windows / CI | โŒ Windows only | โœ… | +| **Edit/generate API** | โœ… | โœ… (COM) | โŒ mostly read-only | +| **Schema validation** | โœ… | โŒ | โŒ | +| **AI agent integration (MCP)** | โœ… `hwpx-mcp-server` | โŒ | โŒ | + +> HWP (v5 binary) files are not supported. Convert to HWPX in Hancom Office first. + +## Known limitations + +- `add_shape()` / `add_control()` do not generate every child element Hancom requires. When adding complex objects, verify by opening in Hancom. +- Image binary embedding is supported, but full automatic generation of the `` element is not provided. +- Encryption/decryption of encrypted HWPX files is not supported. + +## More + +- **[๐Ÿš€ Quick start](docs/quickstart.md)** ยท **[๐Ÿ“š Usage guide](docs/usage.md)** โ€” from opening your first file to editing paragraphs/tables/memos/sections and text extraction/validation +- **[๐Ÿ’ก Examples](docs/examples.md)** ยท [`examples/`](examples/) โ€” `build_release_checklist.py` (generate HWPX with memo/style edits), `extract_text.py` (CLI text extraction), `find_objects.py` (trace OWPML nodes), and more +- **[๐Ÿ“ Schema overview](docs/schema-overview.md)** ยท **[๐Ÿ”ง Install verification](docs/installation.md)** +- **[๐Ÿ”ฌ HWPX internals field guide](docs/internals/)** โ€” HWPUNIT ยท layout cache ยท TOC fields ยท OPC repacking ยท memos ยท oracle limits, verified against real Hancom behavior +- **[๐Ÿ“– Full docs (Sphinx)](https://airmang.github.io/python-hwpx/)** โ€” API reference ยท 50+ practical patterns ยท FAQ +- **[๐Ÿ“ CHANGELOG](CHANGELOG.md)** ยท **[๐Ÿค CONTRIBUTING](CONTRIBUTING.md)** ยท **[๐Ÿ‘ฅ CONTRIBUTORS](CONTRIBUTORS.md)** + +## Contributing + +Bug reports, feature proposals, and PRs are all welcome. + +```bash +git clone https://github.com/airmang/python-hwpx.git +cd python-hwpx +pip install -e ".[dev]" +pytest +``` + +## Acknowledgements + +This project is indebted to the following open standards and projects. + +- **[OWPML โ€” Open Word-processor Markup Language (KS X 6101)](https://www.kssn.net/search/stddetail.do?itemNo=K001010119985)** โ€” the Korean industrial standard HWPX is based on +- **[hancom-io/hwpx-owpml-model](https://github.com/hancom-io/hwpx-owpml-model)** โ€” reference model for OWPML element structure ยท **[neolord0/hwpxlib](https://github.com/neolord0/hwpxlib)** โ€” oracle sample corpus +- **[edwardkim/rhwp](https://github.com/edwardkim/rhwp)** โ€” inspiration for idempotence / verification-gate design +- **๋ฒ”์ •๋ถ€์˜คํ”ผ์Šค (Whole-of-government Office)** โ€” ideas for official-document editing workflows + +## License + +Apache License 2.0. See LICENSE and NOTICE. + +## Maintainer + +Primary maintainer/contact: **Kohkyuhyun** ([@airmang](https://github.com/airmang)) + +- โœ‰๏ธ [kokyuhyun@hotmail.com](mailto:kokyuhyun@hotmail.com) +- ๐Ÿ™ [@airmang](https://github.com/airmang) diff --git a/docs/index.md b/docs/index.md index 5c21c73..fcbd6cb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,6 +20,7 @@ examples corpus-metrics schema-overview +internals/README faq changelog ``` diff --git a/docs/internals/README.md b/docs/internals/README.md new file mode 100644 index 0000000..1b9b7c7 --- /dev/null +++ b/docs/internals/README.md @@ -0,0 +1,40 @@ +# HWPX/OWPML ๋‚ด๋ถ€ ์‹ค์ „ ๊ฐ€์ด๋“œ + +```{toctree} +:maxdepth: 1 +:hidden: + +units +lineseg +toc-dirty +opc-packaging +memo-structure +oracle-limits +``` + +์ฝ”๋“œ๋งŒ์œผ๋กœ๋Š” ์•Œ ์ˆ˜ ์—†๋Š”, **์‹ค์ œ ํ•œ/๊ธ€ ๋™์ž‘์—์„œ ํ™•์ธ๋œ** HWPX/OWPML ์‹ค์ „ ์ง€์‹์„ ๋ชจ์€ ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค. + +HWPX๋Š” [KS X 6101 OWPML](https://www.kssn.net/search/stddetail.do?itemNo=K001010119985) ํ‘œ์ค€ ์œ„์— ์„ธ์›Œ์ง„ ZIP+XML ํฌ๋งท์ด์ง€๋งŒ, ํ‘œ์ค€ ๋ฌธ์„œ์—๋„ ํ•œ์ปด ๊ณต์‹ ๋ฌธ์„œ์—๋„ ์ ํ˜€ ์žˆ์ง€ ์•Š์€ ๋™์ž‘์ด ๋งŽ์Šต๋‹ˆ๋‹ค. ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด ์‹ค์ œ ํ•œ/๊ธ€์—์„œ ์—ด์–ด ๋ณด๋ฉด์„œ๋งŒ ์•Œ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ๋“ค โ€” ์ €์žฅ ์‹œ์ ์— ์บ์‹œ๋˜๋Š” ์กฐํŒ ๊ฒฐ๊ณผ, ํ•„๋“œ ์žฌ๊ณ„์‚ฐ ํŠธ๋ฆฌ๊ฑฐ, OPC ์žฌํŒจํ‚น ๊ทœ์น™, ๋ฉ”๋ชจ์˜ ์ฐธ์กฐ ๊ตฌ์กฐ, ๋ Œ๋” ๊ธฐ๋ฐ˜ ๊ฒ€์ฆ์˜ ํ•œ๊ณ„ ๊ฐ™์€ ๊ฒƒ๋“ค์ด ๊ทธ๋ ‡์Šต๋‹ˆ๋‹ค. + +์ด ๊ฐ€์ด๋“œ๋Š” ๊ทธ๋Ÿฐ ์ง€์‹์„ ์ •๋ฆฌํ•ด, ๊ธฐ์—ฌ์ž๊ฐ€ "์™œ ์ด๋ ‡๊ฒŒ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š”๊ฐ€"๋ฅผ ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ์ดํ•ดํ•˜๋„๋ก ๋•๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ๋ฌธ์„œ์˜ ๋ชจ๋“  ์ฃผ์žฅ์€ ์ด ์ €์žฅ์†Œ์˜ ์ฝ”๋“œยทํ…Œ์ŠคํŠธ(`src/hwpx/...`, `tests/...`)๋‚˜ ๊ณต๊ฐœ ํ‘œ์ค€์œผ๋กœ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ๋งŒ ๋‹ด์•˜๊ณ , ์‹ค์ธก์œผ๋กœ ๊ด€์ฐฐํ•œ ํ•œ/๊ธ€ ๋™์ž‘์€ "์‹ค์ œ ํ•œ/๊ธ€์—์„œ ํ™•์ธ๋œ ๋™์ž‘"์œผ๋กœ ๋ช…์‹œํ–ˆ์Šต๋‹ˆ๋‹ค. + +## ๋ฌธ์„œ ๋ชฉ๋ก + +| ์ฃผ์ œ | ๋ฌธ์„œ | ํ•œ ์ค„ ์š”์•ฝ | +|---|---|---| +| ์ขŒํ‘œ ๋‹จ์œ„ | [units.md](units.md) | HWPUNIT ์ขŒํ‘œ๊ณ„(1 inch = 7,200)์™€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ pt/mm/%๋ฅผ ๋‚ด๋ถ€์—์„œ ๋ณ€ํ™˜ํ•˜๋Š” ์ด์œ  | +| ์กฐํŒ ์บ์‹œ | [lineseg.md](lineseg.md) | `hp:linesegarray`๋Š” ํ•œ/๊ธ€์ด ์ €์žฅ ์‹œ์ ์— ์บ์‹œํ•œ ์ค„๋‚˜๋ˆ” ๊ฒฐ๊ณผ โ€” ํŽธ์ง‘ ํ›„ stale๋กœ ๋‚จ๊ธฐ๋ฉด ๊ธ€์ž ๊ฒน์นจ | +| ๋ชฉ์ฐจ ํ•„๋“œ | [toc-dirty.md](toc-dirty.md) | `TABLEOFCONTENTS` ํ•„๋“œ์˜ `dirty="1"`์ด ์—ฌ๋Š” ์‹œ์  ์žฌ๊ณ„์‚ฐ์„ ํŠธ๋ฆฌ๊ฑฐํ•˜๋Š” ๋ฉ”์ปค๋‹ˆ์ฆ˜ | +| OPC ํŒจํ‚ค์ง• | [opc-packaging.md](opc-packaging.md) | `mimetype` ์ฒซ ์—”ํŠธ๋ฆฌยทSTORED ๊ทœ์น™, version.xml/manifest, 2016โ†’2011 ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์ •๊ทœํ™” | +| ๋ฉ”๋ชจ ๊ตฌ์กฐ | [memo-structure.md](memo-structure.md) | ๋ฉ”๋ชจ ๋ณธ๋ฌธยทMEMO ํ•„๋“œยท`MemoShapeIDRef` ์ฐธ์กฐ๊ฐ€ ๋งž์•„์•ผ ํ•œ/๊ธ€์ด ๋ฉ”๋ชจ๋ฅผ ํ‘œ์‹œํ•˜๋Š” ์ด์œ  | +| ์˜ค๋ผํด ํ•œ๊ณ„ | [oracle-limits.md](oracle-limits.md) | ํ•œ/๊ธ€ export ๊ธฐ๋ฐ˜ ๊ฒ€์ฆ์ด ์นจ๋ฌต ์‹คํŒจํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ์™€ ํ”ฝ์…€ ๊ฒ€์ฆ์ด ํ•„์š”ํ•œ ์ด์œ  | + +## ์ฝ๋Š” ์ˆœ์„œ + +์ฒ˜์Œ์ด๋ผ๋ฉด [units.md](units.md) โ†’ [opc-packaging.md](opc-packaging.md) โ†’ [lineseg.md](lineseg.md) ์ˆœ์„œ๋ฅผ ๊ถŒํ•ฉ๋‹ˆ๋‹ค. ์ขŒํ‘œ ๋‹จ์œ„์™€ ์ปจํ…Œ์ด๋„ˆ ๊ตฌ์กฐ๋ฅผ ๋จผ์ € ์žก์œผ๋ฉด ๋‚˜๋จธ์ง€ ์ฃผ์ œ์˜ ์ฝ”๋“œ๊ฐ€ ํ›จ์”ฌ ์ž˜ ์ฝํž™๋‹ˆ๋‹ค. + +## ์ „์ œ + +- ์ด ๊ฐ€์ด๋“œ๋Š” HWPX(OWPML/OPC) ํฌ๋งท์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค. HWP v5 ๋ฐ”์ด๋„ˆ๋ฆฌ ํฌ๋งท์€ ๋Œ€์ƒ์ด ์•„๋‹™๋‹ˆ๋‹ค. +- ์ธ์šฉ๋œ ์ฝ”๋“œ ๊ฒฝ๋กœ๋Š” ์ด ์ €์žฅ์†Œ ๊ธฐ์ค€์ž…๋‹ˆ๋‹ค(`src/hwpx/...`). ๋ฒ„์ „์— ๋”ฐ๋ผ ์ค„ ๋ฒˆํ˜ธ๋Š” ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์œผ๋‹ˆ ํ•จ์ˆ˜ยทํด๋ž˜์Šค ์ด๋ฆ„์œผ๋กœ ์ฐพ์œผ์„ธ์š”. +- "์‹ค์ œ ํ•œ/๊ธ€์—์„œ ํ™•์ธ๋œ ๋™์ž‘"์€ ์‹ค์ œ ํ•œ์ปด์˜คํ”ผ์Šค๋กœ ํŒŒ์ผ์„ ์—ด๊ฑฐ๋‚˜ ์ €์žฅํ•ด ๊ด€์ฐฐํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋œปํ•ฉ๋‹ˆ๋‹ค. ํ‘œ์ค€์— ๋ช…๋ฌธํ™”๋˜์–ด ์žˆ์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. diff --git a/docs/internals/lineseg.md b/docs/internals/lineseg.md new file mode 100644 index 0000000..9c1562a --- /dev/null +++ b/docs/internals/lineseg.md @@ -0,0 +1,83 @@ +# hp:linesegarray โ€” ์กฐํŒ(์ค„๋‚˜๋ˆ”) ์บ์‹œ + +`hp:linesegarray`๋Š” HWPX๋ฅผ ํŽธ์ง‘ํ•  ๋•Œ ๊ฐ€์žฅ ์กฐ์šฉํžˆ, ๊ทธ๋Ÿฌ๋‚˜ ํ™•์‹คํ•˜๊ฒŒ ๋ฌธ์„œ๋ฅผ ๊นจ๋œจ๋ฆด ์ˆ˜ ์žˆ๋Š” ์š”์†Œ์ž…๋‹ˆ๋‹ค. ํ…์ŠคํŠธ๋ฅผ ๊ณ ์นœ ๋’ค ์ด ์บ์‹œ๋ฅผ ๋ฐฉ์น˜ํ•˜๋ฉด ํ•œ/๊ธ€ ๋ Œ๋”์—์„œ **๊ธ€์ž๊ฐ€ ๊ฒน์ณ** ๋ณด์ž…๋‹ˆ๋‹ค. ์ด ๋ฌธ์„œ๋Š” ๊ทธ๊ฒƒ์ด ๋ฌด์—‡์ด๊ณ , ์–ธ์ œ stale์ด ๋˜๋ฉฐ, ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋Š”์ง€ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. + +## ๋ฌด์—‡์ธ๊ฐ€ + +``๋Š” ๋ฌธ๋‹จ(``) ์•ˆ์— ๋“ค์–ด๊ฐ€๋Š” **์กฐํŒ ๊ฒฐ๊ณผ ์บ์‹œ**์ž…๋‹ˆ๋‹ค. ์ž์‹์œผ๋กœ `` ์š”์†Œ๋“ค์„ ๊ฐ€์ง€๋ฉฐ, ๊ฐ `lineseg`๋Š” ํ•œ ์ค„์˜ ์„ธ๋กœ ์œ„์น˜ยท๋†’์ดยท๋ฒ ์ด์Šค๋ผ์ธยท์‹œ์ž‘ ํ…์ŠคํŠธ ์œ„์น˜(`textpos`)ยท๊ฐ€๋กœ ์œ„์น˜/ํฌ๊ธฐ ๋“ฑ ํ•œ/๊ธ€์ด **์ €์žฅ ์‹œ์ ์— ๊ณ„์‚ฐํ•ด ๋‘” ์ค„๋‚˜๋ˆ” ๊ธฐํ•˜**๋ฅผ ๋‹ด์Šต๋‹ˆ๋‹ค. ์ €์ˆ˜์ค€ ๋ชจ๋ธ์ด ์ด ํ•„๋“œ๋ฅผ ์ „๋ถ€ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค โ€” `src/hwpx/oxml/body.py`์˜ `class LineSeg`(text_pos, vert_pos, vert_size, text_height, baseline, ... ํ•„๋“œ)์™€ `class LineSegArray`(linesegs ๋ฆฌ์ŠคํŠธ). + +ํ•ต์‹ฌ์€ ์ด๊ฒƒ์ด **์˜๋ฏธ ์žˆ๋Š” ์ฝ˜ํ…์ธ ๊ฐ€ ์•„๋‹ˆ๋ผ ํŒŒ์ƒ๋œ ๋ ˆ์ด์•„์›ƒ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ**๋ผ๋Š” ์ ์ž…๋‹ˆ๋‹ค. `src/hwpx/oxml/section_story.py`์˜ ์ฃผ์„์ด ์ด๋ฅผ ๋ช…ํ™•ํžˆ ํ•ฉ๋‹ˆ๋‹ค: + +> A cached `lineSegArray` is layout metadata and may be discarded after a text edit; every other child is semantic content and therefore fails closed before mutation. + +์ฆ‰ ํ…์ŠคํŠธยท์„œ์‹์€ ์ง€์šฐ๋ฉด ์•ˆ ๋˜์ง€๋งŒ, ์กฐํŒ ์บ์‹œ๋Š” ์ง€์›Œ๋„ ๋ฉ๋‹ˆ๋‹ค. ์ง€์šฐ๋ฉด ํ•œ/๊ธ€์ด ๋ฌธ์„œ๋ฅผ ์—ด ๋•Œ ์Šค์Šค๋กœ ๋‹ค์‹œ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + +## ์–ธ์ œ stale์ด ๋˜๋‚˜ + +`lineseg`์˜ `textpos`๋Š” "์ด ์ค„์ด ๋ฌธ๋‹จ ํ…์ŠคํŠธ์˜ ๋ช‡ ๋ฒˆ์งธ ๊ธ€์ž์—์„œ ์‹œ์ž‘ํ•˜๋Š”๊ฐ€"๋ฅผ ๊ฐ€๋ฆฌํ‚ต๋‹ˆ๋‹ค. ๋ฌธ๋‹จ์˜ ํ…์ŠคํŠธ ๊ธธ์ด๋ฅผ ํŽธ์ง‘์œผ๋กœ ๋ฐ”๊พธ๋ฉด, ์ด์ „์— ์บ์‹œ๋œ `textpos` ๊ฐ’๋“ค์ด ์ƒˆ ํ…์ŠคํŠธ์™€ ์–ด๊ธ‹๋‚ฉ๋‹ˆ๋‹ค. ํŠนํžˆ `textpos`๊ฐ€ ์ƒˆ ๋ฌธ๋‹จ์˜ ํ…์ŠคํŠธ ๊ธธ์ด๋ฅผ ๋„˜์–ด์„œ๋ฉด ๊ทธ ์บ์‹œ๋Š” ๋ช…๋ฐฑํžˆ **stale**์ž…๋‹ˆ๋‹ค. + +๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์ด ์ •์˜๋ฅผ ๊ทธ๋Œ€๋กœ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค โ€” `src/hwpx/tools/package_validator.py`: + +```python +if textpos > text_length: + _error( + issues, + part_name, + f"paragraph {paragraph_index} has stale lineseg textpos={textpos} " + f"beyond text length {text_length}", + ) +``` + +`src/hwpx/layout/lint.py`๋„ ๊ฐ™์€ ์กฐ๊ฑด์„ `STALE_LINESEG_DETECTED`๋กœ ์žก์•„๋ƒ…๋‹ˆ๋‹ค(๋ Œ๋”๋Ÿฌ ์—†์ด๋„ ํƒ์ง€ ๊ฐ€๋Šฅํ•œ ํ•˜๋“œ ์—๋Ÿฌ). + +**stale์„ ๋‚จ๊ธฐ๋ฉด ๋ฌด์Šจ ์ผ์ด ์ƒ๊ธฐ๋‚˜.** ์บ์‹œ๋œ ์ค„ ๊ธฐํ•˜๋Š” *์˜›* ํ…์ŠคํŠธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํ•˜๋ฏ€๋กœ, ํ•œ/๊ธ€์€ ์ƒˆ(๋” ๊ธด) ํ…์ŠคํŠธ๋ฅผ ์˜› ์ค„ ์Šฌ๋กฏ์— ๋ฐ€์–ด ๋„ฃ์–ด ๋ Œ๋”ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ๊ฒฐ๊ณผ๊ฐ€ ๊ธ€์ž ๊ฒน์นจ์ž…๋‹ˆ๋‹ค. `src/hwpx/patch.py`์˜ ์ฃผ์„์ด ์ด ์ธ๊ณผ๋ฅผ ๋ชป ๋ฐ•์•„ ๋‘ก๋‹ˆ๋‹ค: + +> Splicing new text into a paragraph invalidates its cached line geometry, so the byte path must drop the cache; otherwise Hangul renders the new text into the stale line slots and overlapping glyphs result. + +์ด ๋™์ž‘์€ ์‹ค์ œ ํ•œ์ปด ๋ Œ๋” ์˜ค๋ผํด๋กœ ํ™•์ธ๋œ ๊ฒƒ์ž…๋‹ˆ๋‹ค: ํ…์ŠคํŠธ๋ฅผ ๊ต์ฒดํ•œ ๋ฌธ๋‹จ์€ ์ค„์ด ๊ฒน์น˜๊ณ , ์บ์‹œ๋ฅผ ์ œ๊ฑฐํ•œ ๋™์ผ ๋ฌธ๋‹จ์€ ์ •์ƒ ๋ Œ๋”๋ฉ๋‹ˆ๋‹ค. + +## ๋‘ ๊ฐ€์ง€ ์ „๋žต + +ํŽธ์ง‘ ํ›„ ์บ์‹œ๋ฅผ ๋‹ค๋ฃฐ ๋ฐฉ๋ฒ•์€ ๋‘ ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค. + +1. **์ง€์šด๋‹ค โ†’ ํ•œ/๊ธ€์ด ์žฌ๊ณ„์‚ฐ.** ํŽธ์ง‘ํ•œ ๋ฌธ๋‹จ์˜ `linesegarray`๋ฅผ ์ œ๊ฑฐํ•˜๋ฉด ํ•œ/๊ธ€์ด ์—ด ๋•Œ ๊ทธ ๋ฌธ๋‹จ๋งŒ ๋‹ค์‹œ ์กฐํŒํ•ฉ๋‹ˆ๋‹ค. ์•ˆ์ „ํ•˜์ง€๋งŒ, ์ œ๊ฑฐ๋œ ๋ฐ”์ดํŠธ๋งŒํผ ํŒŒ์ผ์ด ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค. +2. **๋ณด์กดํ•œ๋‹ค โ†’ ๋ฐ”์ดํŠธ ๋ณด์กด์— ์œ ๋ฆฌ, ๊ทธ๋Ÿฌ๋‚˜ stale ์œ„ํ—˜.** ์บ์‹œ๋ฅผ ๊ทธ๋Œ€๋กœ ๋‘๋ฉด ๋ฏธํŽธ์ง‘ ์˜์—ญ์˜ ๋ฐ”์ดํŠธ๊ฐ€ ์›๋ณธ๊ณผ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. ํŽธ์ง‘ํ•œ ๋ฌธ๋‹จ์˜ ์บ์‹œ๊นŒ์ง€ ๋‚จ๊ธฐ๋ฉด ์œ„ํ—˜ํ•ฉ๋‹ˆ๋‹ค. + +์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์„ค๊ณ„ ์›์น™์€ **๋‘˜์˜ ์กฐํ•ฉ โ€” "์†๋Œ„ ๋ฌธ๋‹จ๋งŒ" ๋ฌดํšจํ™”**์ž…๋‹ˆ๋‹ค. ํŽธ์ง‘ํ•œ ๋ฌธ๋‹จ์˜ ์บ์‹œ๋Š” ์ง€์šฐ๊ณ (์ „๋žต 1), ์†๋Œ€์ง€ ์•Š์€ ๋ฌธ๋‹จ์˜ ์บ์‹œ๋Š” ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค(์ „๋žต 2). + +## ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์‹ค์ œ ์ฒ˜๋ฆฌ: ๋ฌธ๋‹จ ๋‹จ์œ„ ์Šค์ฝ”ํ”„ ๋ฌดํšจํ™” + +๋ฌธ์„œ ์ „์ฒด์˜ ์บ์‹œ๋ฅผ ๋ชฝ๋•… ์ง€์šฐ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, **ํŽธ์ง‘ํ•œ ๋ฌธ๋‹จ(๋“ค)์˜ ์บ์‹œ๋งŒ** ์ง€์›๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ถˆ๋ณ€์‹์ž…๋‹ˆ๋‹ค. + +ํŽธ์ง‘ ์ง„์ž…์ ๋งˆ๋‹ค ์Šค์ฝ”ํ”„ ๋ฌดํšจํ™”๊ฐ€ ๊ฑธ๋ ค ์žˆ์Šต๋‹ˆ๋‹ค. + +- **๋ฐ”์ดํŠธ ์Šคํ”Œ๋ผ์ด์Šค ๊ฒฝ๋กœ** (`src/hwpx/patch.py`): `_strip_paragraph_layout_cache()`๊ฐ€ ์ •๊ทœ์‹์œผ๋กœ ํ•œ ๋ฌธ๋‹จ์˜ ``๋งŒ ์ œ๊ฑฐํ•˜๊ณ , ์Šคํ”Œ๋ผ์ด์Šคํ•œ ๋ฌธ๋‹จ์—๋งŒ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ฏธํŽธ์ง‘ span์€ ์บ์‹œ๊นŒ์ง€ ์›๋ณธ ๋ฐ”์ดํŠธ ๊ทธ๋Œ€๋กœ round-trip๋ฉ๋‹ˆ๋‹ค. +- **๋ณธ๋ฌธ ํ…์ŠคํŠธ ๊ต์ฒด** (`src/hwpx/body_patch.py`): `replace_text`๊ฐ€ ํŽธ์ง‘๋œ ๋ฌธ๋‹จ์„ **๊ฐ€์žฅ ์•ˆ์ชฝ `` ๋‹จ์œ„**๋กœ ๋ฌถ์–ด ๊ทธ ๋ธ”๋ก์˜ ์บ์‹œ๋งŒ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. +- **ํ‘œ ์…€ ์ฑ„์›€** (`src/hwpx/table_patch.py`): `fill_cells`ยท`_blank_cell_text`๊ฐ€ ์ฑ„์šด(๋˜๋Š” ๋น„์šด) ์…€ ๋ฌธ๋‹จ์˜ ์บ์‹œ๋งŒ ์ œ๊ฑฐํ•˜๊ณ , ์†๋Œ€์ง€ ์•Š์€ ํ‘œ๋Š” ๋ฐ”์ดํŠธ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. +- **๋จธ๋ฆฌ๊ธ€/๋ฐ”๋‹ฅ๊ธ€ ์Šคํ† ๋ฆฌ** (`src/hwpx/oxml/section_story.py`): ํ…์ŠคํŠธ ์„ธํ„ฐ๊ฐ€ ๋Œ€์ƒ ๋ฌธ๋‹จ์˜ ์บ์‹œ๋ฅผ `_clear_paragraph_layout_cache()`๋กœ ์ง€์›๋‹ˆ๋‹ค(์ฃผ์„: `Clear cached lineseg so Hangul recalculates layout.`). +- **์˜ค๋ธŒ์ ํŠธ ๋ชจ๋ธ ์„ธํ„ฐ** (`src/hwpx/oxml/run.py`, `paragraph.py`, `table.py`): run/๋ฌธ๋‹จ/์…€ ํ…์ŠคํŠธยท์Šคํƒ€์ผ์„ ๋ฐ”๊พธ๋Š” API๊ฐ€ ์ž์‹ ์ด ๊ฑด๋“œ๋ฆฐ ๋ฌธ๋‹จ์˜ ์บ์‹œ๋ฅผ ๊ทธ ์ž๋ฆฌ์—์„œ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + +### ์™œ "์ „๋ถ€ ์ง€์šฐ๊ธฐ"๊ฐ€ ์•„๋‹ˆ๋ผ "์Šค์ฝ”ํ”„"์ธ๊ฐ€ + +๋ชจ๋ธ ์ „์ฒด๋ฅผ ์žฌ์ง๋ ฌํ™”ํ•  ๋•Œ์กฐ์ฐจ ์บ์‹œ๋ฅผ ์ „๋ถ€ ์ง€์šฐ์ง€ ์•Š๊ณ , **๋ช…๋ฐฑํžˆ staleํ•œ ๊ฒƒ๋งŒ** ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค(์•ˆ์ „๋ง). `src/hwpx/oxml/document_parts.py`์˜ ์ฃผ์„์ด ๊ทธ ์ด์œ ๋ฅผ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค: + +> The mutating APIs clear the caches of exactly the paragraphs they touch, so even a dirty section only needs the stale sweep as a safety net. Nuking every cache here forced Hancom to re-lay-out untouched pages of multi-page forms, which is what stacked glyphs and shifted page counts. + +์ฆ‰ **์ „๋ถ€ ์ง€์šฐ๊ธฐ๊ฐ€ ์˜คํžˆ๋ ค ๋ฒ„๊ทธ**์˜€์Šต๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€์งœ๋ฆฌ ์–‘์‹์—์„œ ์บ์‹œ๋ฅผ ํ†ต์งธ๋กœ ๋‚ ๋ฆฌ๋ฉด, ํ•œ/๊ธ€์ด ์†๋Œ€์ง€ ์•Š์€ ํŽ˜์ด์ง€๊นŒ์ง€ ์žฌ์กฐํŒํ•˜๋ฉด์„œ ํŽ˜์ด์ง€ ์ˆ˜๊ฐ€ ๋ฐ”๋€Œ๊ณ  ๊ธ€์ž๊ฐ€ ๊ฒน์ณค์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ €์žฅ ๊ฒฝ๊ณ„์˜ sweep์€ staleํ•œ ๊ฒƒ๋งŒ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค โ€” `src/hwpx/opc/package.py`์˜ `_strip_section_layout_caches`๋Š” `textpos > text length`์ธ ์บ์‹œ๋งŒ ์ง€์›๋‹ˆ๋‹ค. + +`src/hwpx/oxml/section.py`์—๋Š” ์ „๋Ÿ‰ ์ œ๊ฑฐ ๋ฉ”์„œ๋“œ `remove_layout_caches()`๊ฐ€ ์กด์žฌํ•˜์ง€๋งŒ, ์ €์žฅ ํŒŒ์ดํ”„๋ผ์ธ์— ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ๋ฐฐ์„ ๋œ ๊ฒƒ์€ stale-only์ธ `remove_stale_layout_caches()`๋ฟ์ž…๋‹ˆ๋‹ค. + +## ํ…Œ์ŠคํŠธ๋กœ ๊ณ ์ •๋œ ๊ณ„์•ฝ + +์ด ๋™์ž‘์€ ์—ฌ๋Ÿฌ ํ…Œ์ŠคํŠธ๋กœ ๋ชป ๋ฐ•ํ˜€ ์žˆ์Šต๋‹ˆ๋‹ค. + +- `tests/test_layout_cache_scope.py` โ€” ๋‹จ์ผ ์…€ ์ฑ„์›€์ด "๊ทธ ์…€ ๋ฌธ๋‹จ์˜ ์บ์‹œ๋งŒ" ๋ฌดํšจํ™”ํ•˜๊ณ  ๋‚˜๋จธ์ง€๋Š” ๋ณด์กดํ•จ์„ ๊ฒ€์ฆ. no-op ์ €์žฅ์€ ๋ชจ๋“  ์บ์‹œ๋ฅผ ๋ณด์กด(`test_noop_save_preserves_every_layout_cache`). +- `tests/test_kordoc_absorption.py::test_byte_preserving_patch_strips_only_patched_paragraph_layout_cache` โ€” ์Šคํ”Œ๋ผ์ด์Šค๊ฐ€ ๋Œ€์ƒ ๋ฌธ๋‹จ๋งŒ ์ œ๊ฑฐํ•˜๊ณ  ์ด์›ƒ ๋ฌธ๋‹จ ์บ์‹œ๋Š” ์‚ด์•„ ์žˆ์Œ์„ ๊ฒ€์ฆ. +- `tests/test_layout_lint.py`, `tests/test_gap_closure_tools.py` โ€” seeded stale ์บ์‹œ๊ฐ€ ๋ Œ๋”๋Ÿฌ ์—†์ด๋„ ํ•˜๋“œ ์—๋Ÿฌ/`ValueError`๋กœ ์žกํžˆ๋Š”์ง€ ๊ฒ€์ฆ. +- `tests/test_coverage_promotion.py` โ€” ๋ฏธํŽธ์ง‘ ๋ฌธ๋‹จ์˜ `LineSeg`/`LineSegArray`๊ฐ€ ์ €์žฅยท์žฌํŒŒ์‹ฑ์„ ๊ฑฐ์ณ ์†์‹ค ์—†์ด round-trip๋จ์„ ๊ฒ€์ฆ. + +## ์‹ค์ „ ์š”์•ฝ + +- HWPX ํ…์ŠคํŠธ๋ฅผ ์ง์ ‘ ํŽธ์ง‘ํ•œ๋‹ค๋ฉด(๋ฌธ์ž์—ด ์น˜ํ™˜, XML ์กฐ์ž‘), **๊ทธ ๋ฌธ๋‹จ์˜ ``๋ฅผ ๋ฐ˜๋“œ์‹œ ์ œ๊ฑฐ**ํ•˜์„ธ์š”. ์•ˆ ๊ทธ๋Ÿฌ๋ฉด ํ•œ/๊ธ€์—์„œ ๊ธ€์ž๊ฐ€ ๊ฒน์นฉ๋‹ˆ๋‹ค. +- ๋‹จ, **์†๋Œ€์ง€ ์•Š์€ ๋ฌธ๋‹จ์˜ ์บ์‹œ๋Š” ๋‚จ๊ฒจ ๋‘์„ธ์š”.** ์ „๋Ÿ‰ ์‚ญ์ œ๋Š” ๋ฏธํŽธ์ง‘ ํŽ˜์ด์ง€์˜ ์žฌ์กฐํŒ์„ ์œ ๋ฐœํ•ด ํŽ˜์ด์ง€ ์ˆ˜/๋ ˆ์ด์•„์›ƒ์„ ํํŠธ๋Ÿฌ๋œจ๋ฆฝ๋‹ˆ๋‹ค. +- ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๊ณ ์ˆ˜์ค€ยท๋ฐ”์ดํŠธ ๋ณด์กด API๋ฅผ ์“ฐ๋ฉด ์ด ์ฒ˜๋ฆฌ๋Š” ์ž๋™์ž…๋‹ˆ๋‹ค. ์ €์ˆ˜์ค€์œผ๋กœ ๋‚ด๋ ค๊ฐˆ ๋•Œ๋งŒ ์ง์ ‘ ์‹ ๊ฒฝ ์“ฐ๋ฉด ๋ฉ๋‹ˆ๋‹ค. diff --git a/docs/internals/memo-structure.md b/docs/internals/memo-structure.md new file mode 100644 index 0000000..9c87c7d --- /dev/null +++ b/docs/internals/memo-structure.md @@ -0,0 +1,85 @@ +# ๋ฉ”๋ชจ(hp:memo) ๊ตฌ์กฐ + +ํ•œ/๊ธ€์˜ ๋ฉ”๋ชจ(์ฃผ์„)๋ฅผ ํ”„๋กœ๊ทธ๋žจ์œผ๋กœ ๋ถ™์ผ ๋•Œ ํ”ํžˆ ๊ฒช๋Š” ํ•จ์ •์€ "์š”์†Œ๋Š” ๋งŒ๋“ค์—ˆ๋Š”๋ฐ ํ•œ/๊ธ€์—์„œ ๋ฉ”๋ชจ๊ฐ€ ์•ˆ ๋ณด์ธ๋‹ค"์ž…๋‹ˆ๋‹ค. ๋ฉ”๋ชจ๊ฐ€ ํ‘œ์‹œ๋˜๋ ค๋ฉด **์„ธ ์กฐ๊ฐ โ€” ๋ฉ”๋ชจ ๋ณธ์ฒด, ๋ณธ๋ฌธ์˜ MEMO ํ•„๋“œ, ๊ทธ๋ฆฌ๊ณ  ๋‘˜์„ ์ž‡๋Š” ์ฐธ์กฐ โ€” ๊ฐ€ ๋ชจ๋‘ ๋งž์•„์•ผ** ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด ๋ฌธ์„œ๋Š” ์‹ค์ œ๋กœ ํ‘œ์‹œ๋˜๋Š” ๋ฉ”๋ชจ์˜ ๊ตฌ์กฐ๋ฅผ ์ฝ”๋“œ๋กœ ์งš์Šต๋‹ˆ๋‹ค. + +## ์„ธ ์กฐ๊ฐ + +### 1. ๋ฉ”๋ชจ ๋ณธ์ฒด: `` ์•ˆ์˜ `` + +๋ฉ”๋ชจ ๋ณธ์ฒด๋Š” ์„น์…˜์˜ `` ์ปจํ…Œ์ด๋„ˆ ์•ˆ์— ``๋กœ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค(`src/hwpx/oxml/memo.py`์˜ `HwpxOxmlMemoGroup`, `HwpxOxmlMemo`). ๊ฐ ``๋Š” `id`์™€ `memoShapeIDRef` ์†์„ฑ์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค. + +๋ฉ”๋ชจ์˜ **๋ณธ๋ฌธ ํ…์ŠคํŠธ**๋Š” `` ์•ˆ์˜ `` โ†’ `` โ†’ `` โ†’ ``์— ๋†“์ž…๋‹ˆ๋‹ค(`HwpxOxmlMemo.set_text`). ์ฆ‰ ๋ฉ”๋ชจ ๋ณธ์ฒด๊ฐ€ ๋งŒ๋“ค์–ด๋‚ด๋Š” ๊ตฌ์กฐ๋Š”: + +``` + + + + ๋ฉ”๋ชจ ๋‚ด์šฉ + + + +``` + +> ์ฃผ์˜: ์—ฌ๊ธฐ์„œ ๋ณธ๋ฌธ์„ ๋‹ด๋Š” ์ปจํ…Œ์ด๋„ˆ๋Š” `paraList`์ž…๋‹ˆ๋‹ค. ์•„๋ž˜์— ๋‚˜์˜ค๋Š” MEMO **ํ•„๋“œ**์˜ `subList`์™€ ํ˜ผ๋™ํ•˜๊ธฐ ์‰ฌ์šด๋ฐ, ๋‘˜์€ ์„œ๋กœ ๋‹ค๋ฅธ ์œ„์น˜์ž…๋‹ˆ๋‹ค. (์ฐธ๊ณ ๋กœ ๊ฐ์ฃผ/๋ฏธ์ฃผ ``/``๋Š” ๋˜ ๋ณ„๊ฐœ๋กœ `subList`๋ฅผ ์”๋‹ˆ๋‹ค.) + +### 2. ๋ณธ๋ฌธ์˜ MEMO ํ•„๋“œ: `` + +๋ฉ”๋ชจ๊ฐ€ ํ•œ/๊ธ€ ํŽธ์ง‘๊ธฐ์˜ ์—ฌ๋ฐฑ์— **ํ’์„ ์œผ๋กœ ํ‘œ์‹œ๋˜๋ ค๋ฉด**, ๋ณธ๋ฌธ ๋ฌธ๋‹จ์— ๋Œ€์‘ํ•˜๋Š” MEMO ํ•„๋“œ ์ปจํŠธ๋กค์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. `docs/usage.md`๊ฐ€ ์ด๋ฅผ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค: + +> ํ•œ๊ธ€ ํŽธ์ง‘๊ธฐ์—์„œ ๋ฉ”๋ชจ ํ’์„ ์„ ํ‘œ์‹œํ•˜๋ ค๋ฉด ๋ณธ๋ฌธ ๋ฌธ๋‹จ์— ๋Œ€์‘๋˜๋Š” MEMO ํ•„๋“œ ์ปจํŠธ๋กค(`hp:fieldBegin`/`hp:fieldEnd`)์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + +์ด ํ•„๋“œ๋Š” `src/hwpx/_document/memos.py`์˜ `attach_memo_field`๊ฐ€ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ๋ฌธ๋‹จ์˜ ์•ž๋’ค์— ``์™€ `` run์„ ์‚ฝ์ž…ํ•ด, ํ•„๋“œ๊ฐ€ ๋ฌธ๋‹จ ๋‚ด์šฉ์„ ๊ฐ์‹ธ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. `fieldBegin`์˜ `id`์™€ `fieldEnd`์˜ `beginIDRef`๊ฐ€ ์ง์„ ์ด๋ฃน๋‹ˆ๋‹ค. + +**์ค‘์š”ํ•œ ์‹ค์ธก ํ•จ์ •**: ์—ฌ๋ฐฑ์— ์‹ค์ œ๋กœ ๋ณด์ด๋Š” ์ฝ”๋ฉ˜ํŠธ ํ…์ŠคํŠธ๋Š” ์ด MEMO **ํ•„๋“œ์˜ `subList`** ์— ๋‹ด๊น๋‹ˆ๋‹ค. `memos.py`์˜ ์ฃผ์„์ด ๊ณผ๊ฑฐ ํšŒ๊ท€๋ฅผ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค: + +> The MEMO field's subList holds the comment TEXT โ€” this is what Hancom shows in the margin memo box. (Previously this emitted `memo.id`, so Hancom rendered the numeric id instead of the comment.) + +์ฆ‰ ์ด subList์— ํ…์ŠคํŠธ๊ฐ€ ์•„๋‹ˆ๋ผ id๋ฅผ ๋„ฃ์œผ๋ฉด, ํ•œ/๊ธ€์€ ์ฝ”๋ฉ˜ํŠธ ๋Œ€์‹  ์ˆซ์ž๋ฅผ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค. ํ‘œ์‹œ๋Š” ๋˜์ง€๋งŒ ๋‚ด์šฉ์ด ํ‹€๋ฆฐ ์…ˆ์ž…๋‹ˆ๋‹ค. + +### 3. ์ž‡๋Š” ์ฐธ์กฐ: MemoShapeIDRef + +๋ฉ”๋ชจ ๋ณธ์ฒด์™€ ๊ทธ ์‹œ๊ฐ์  ์†์„ฑ(๋ฉ”๋ชจ ์ƒ์ž์˜ ์ƒ‰ยทํ…Œ๋‘๋ฆฌ ๋“ฑ)์„ ์ž‡๋Š” ๊ฒƒ์ด **MemoShapeIDRef**์ž…๋‹ˆ๋‹ค. MEMO ํ•„๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค(`attach_memo_field`): + +```python +parameters = _append_element(field_begin, f"{_HP}parameters", {"count": "5", "name": ""}) +_append_element(parameters, f"{_HP}stringParam", {"name": "ID"}).text = memo.id or "" +... +# Hancom's own files use MemoShapeIDRef (65535 = the built-in default memo +# shape) โ€” an empty/absent ref leaves the memo box unlinked. +_append_element(parameters, f"{_HP}stringParam", {"name": "MemoShapeIDRef"}).text = ( + memo_shape_id or "65535" +) +``` + +์—ฌ๊ธฐ์„œ ๋‘ ๊ฐ€์ง€๋ฅผ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +- **`65535`๋Š” ๊ธฐ๋ณธ ๋ฉ”๋ชจ ์ƒ์ž๋ฅผ ๋œปํ•˜๋Š” ์„ผํ‹ฐ๋„ฌ**์ž…๋‹ˆ๋‹ค. ๋ฉ”๋ชจ์— ๊ณ ์œ ํ•œ `memoShapeIDRef`๊ฐ€ ์—†์œผ๋ฉด ์ด ๊ฐ’์„ ์”๋‹ˆ๋‹ค. +- **์ฐธ์กฐ๊ฐ€ ๋น„์–ด ์žˆ๊ฑฐ๋‚˜ ์—†์œผ๋ฉด ๋ฉ”๋ชจ ์ƒ์ž๊ฐ€ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค**("leaves the memo box unlinked"). ์ฆ‰ ์œ ํšจํ•œ ์ฐธ์กฐ๊ฐ€ ์žˆ์–ด์•ผ ํ•œ/๊ธ€์ด ๋ฉ”๋ชจ๋ฅผ ์ œ๋Œ€๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. + +ํ•œํŽธ ๋ฉ”๋ชจ ๋ณธ์ฒด์˜ `memoShapeIDRef`๋Š” ํ—ค๋”์— ์ •์˜๋œ ๋ฉ”๋ชจ ๋„ํ˜• ์†์„ฑ(``, ๋ชจ๋ธ์ƒ `MemoShape`; `src/hwpx/oxml/header.py`)์„ ๊ฐ€๋ฆฌํ‚ต๋‹ˆ๋‹ค. `document.memo_shape(id)`๋กœ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +> ํ‘œ๊ธฐ ์ฃผ์˜: ์†์„ฑ ์ด๋ฆ„์€ ์œ„์น˜์— ๋”ฐ๋ผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ๋ฉ”๋ชจ ๋ณธ์ฒด ์š”์†Œ์˜ ์†์„ฑ์€ ์†Œ๋ฌธ์ž `memoShapeIDRef`์ด๊ณ , MEMO ํ•„๋“œ ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ์ด๋ฆ„์€ ๋Œ€๋ฌธ์ž `MemoShapeIDRef`์ž…๋‹ˆ๋‹ค. ์ฝ”๋“œ๊ฐ€ ๋‘˜์„ ๊ตฌ๋ถ„ํ•ด ์”๋‹ˆ๋‹ค. + +## ๊ณ ์ˆ˜์ค€ API + +์ด ์„ธ ์กฐ๊ฐ์„ ์†์œผ๋กœ ๋งž์ถ”์ง€ ์•Š๋„๋ก, ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์•ต์ปค ๊ธฐ๋ฐ˜ API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + +- `document.add_memo(text, memo_shape_id_ref=...)` โ€” ๋ฉ”๋ชจ ๋ณธ์ฒด๋งŒ ์ถ”๊ฐ€. +- `document.add_memo_with_anchor(text, paragraph=...)` โ€” ๋ฉ”๋ชจ ๋ณธ์ฒด ์ƒ์„ฑ + ๋Œ€์ƒ ๋ฌธ๋‹จ(์—†์œผ๋ฉด ์ƒˆ๋กœ ์ƒ์„ฑ)์— MEMO ํ•„๋“œ๋ฅผ ๋ถ™์—ฌ **ํ‘œ์‹œ๊นŒ์ง€** ๋ณด์žฅ(`src/hwpx/_document/memos.py`). ๋ฌธ๋‹จ์„ ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด `anchor_char_pr_id_ref`/`char_pr_id_ref`๋กœ ์ƒˆ ๋ฌธ๋‹จ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. + +ํ•„๋“œ run์˜ `charPrIDRef`๋Š” ์ธ์ž โ†’ ๋ฌธ๋‹จ์˜ `char_pr_id_ref` โ†’ ๋ฉ”๋ชจ๊ฐ€ ์ถ”๋ก ํ•œ ๊ฐ’ โ†’ `"0"` ์ˆœ์œผ๋กœ ํ•ด๊ฒฐ๋ฉ๋‹ˆ๋‹ค(`attach_memo_field`). + +> ์ฐธ๊ณ : ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์•ต์ปค API ์ด๋ฆ„์€ `add_memo_with_anchor`์ž…๋‹ˆ๋‹ค. (MCP ์„œ๋ฒ„ ์ชฝ์—๋Š” `add_memo_by_anchor`๋ผ๋Š” ๋„๊ตฌ ์ด๋ฆ„์ด ์žˆ์ง€๋งŒ, ๊ทธ๊ฒƒ์€ ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ•จ์ˆ˜๋ฅผ ๊ฐ์‹ผ ๋ž˜ํผ์ž…๋‹ˆ๋‹ค.) + +## ํ…Œ์ŠคํŠธ๋กœ ๊ณ ์ •๋œ ๊ณ„์•ฝ + +- `tests/test_memo_and_style_editing.py::test_attach_memo_field_inserts_control_runs` โ€” ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐœ์ˆ˜, `MemoShapeIDRef`๊ฐ€ ๋ฉ”๋ชจ์˜ ์ฐธ์กฐ์™€ ์ผ์น˜, ๊ทธ๋ฆฌ๊ณ  (ํšŒ๊ท€ ๋ฐฉ์ง€) ํ•„๋“œ subList๊ฐ€ **id๊ฐ€ ์•„๋‹ˆ๋ผ ์ฝ”๋ฉ˜ํŠธ ํ…์ŠคํŠธ**๋ฅผ ๋‹ด๋Š”์ง€ ๊ฒ€์ฆ. +- ๊ฐ™์€ ํŒŒ์ผ์˜ `test_section_memo_parsing_exposes_text_and_shape`, `test_document_add_edit_and_remove_memos`, `test_add_memo_with_anchor_roundtrips_on_real_document` โ€” ํŒŒ์‹ฑยท์ถ”๊ฐ€ยทํŽธ์ง‘ยท์‚ญ์ œยท์™•๋ณต. +- `tests/test_oxml_parsing.py` โ€” ํ—ค๋”์˜ `` ํŒŒ์‹ฑ๊ณผ id ์ •๊ทœํ™” ์กฐํšŒ(`memo_shape("07") == shapes["7"]`). + +## ์‹ค์ „ ์š”์•ฝ + +- ๋ฉ”๋ชจ๋ฅผ "๋ณด์ด๊ฒŒ" ํ•˜๋ ค๋ฉด ์„ธ ์กฐ๊ฐ์ด ๋‹ค ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค: ๋ณธ์ฒด(`` + `paraList` ๋ณธ๋ฌธ), ๋ณธ๋ฌธ์˜ MEMO ํ•„๋“œ(`fieldBegin`/`fieldEnd`), ๊ทธ๋ฆฌ๊ณ  ๋‘˜์„ ์ž‡๋Š” ์œ ํšจํ•œ `MemoShapeIDRef`. +- ์—ฌ๋ฐฑ ํ’์„ ์— ๋œจ๋Š” ํ…์ŠคํŠธ๋Š” **MEMO ํ•„๋“œ์˜ `subList`** ์—์„œ ์˜ต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์— ํ…์ŠคํŠธ๊ฐ€ ์•„๋‹Œ ๊ฐ’์„ ๋„ฃ์œผ๋ฉด ์—‰๋šฑํ•œ ๋‚ด์šฉ์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. +- ์ฐธ์กฐ๊ฐ€ ๋น„๋ฉด ์ƒ์ž๊ฐ€ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ณ ์œ  ๋„ํ˜•์ด ์—†์œผ๋ฉด `65535`(๊ธฐ๋ณธ ๋„ํ˜•)๋ฅผ ์“ฐ์„ธ์š”. +- ์ง์ ‘ XML์„ ์กฐ๋ฆฝํ•˜์ง€ ๋ง๊ณ  `add_memo_with_anchor`๋ฅผ ์“ฐ๋ฉด ์ด ๋ฐฐ์„ ์ด ์ž๋™์ž…๋‹ˆ๋‹ค. diff --git a/docs/internals/opc-packaging.md b/docs/internals/opc-packaging.md new file mode 100644 index 0000000..e30d8d0 --- /dev/null +++ b/docs/internals/opc-packaging.md @@ -0,0 +1,84 @@ +# OPC/ZIP ์ปจํ…Œ์ด๋„ˆ ์žฌํŒจํ‚น + +HWPX๋Š” OPC(Open Packaging Conventions) ๊ทœ์•ฝ์„ ๋”ฐ๋ฅด๋Š” ZIP ์ปจํ…Œ์ด๋„ˆ์ž…๋‹ˆ๋‹ค โ€” ์•ˆ์— ์—ฌ๋Ÿฌ XML ํŒŒํŠธ(๋ณธ๋ฌธ ์„น์…˜, ํ—ค๋”, ๋งค๋‹ˆํŽ˜์ŠคํŠธ, ๋ฒ„์ „ ์ •๋ณด)์™€ ๋ฐ”์ด๋„ˆ๋ฆฌ(์ด๋ฏธ์ง€ ๋“ฑ)๊ฐ€ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค. ๊ฒ‰๋ณด๊ธฐ์—” ํ‰๋ฒ”ํ•œ ZIP์ด์ง€๋งŒ, **ํ•œ/๊ธ€์ด ํŒŒ์ผ์„ ์ธ์‹ํ•˜๋ ค๋ฉด ์ง€์ผœ์•ผ ํ•˜๋Š” ์žฌํŒจํ‚น ๊ทœ์น™**์ด ๋ช‡ ๊ฐ€์ง€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ทœ์น™์„ ์–ด๊ธฐ๋ฉด ํ•œ/๊ธ€์ด ํŒŒ์ผ์„ ์—ด์ง€ ๋ชปํ•˜๊ฑฐ๋‚˜ ์†์ƒ์œผ๋กœ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค. + +## mimetype๋Š” ์ฒซ ์—”ํŠธ๋ฆฌ, ๊ทธ๋ฆฌ๊ณ  STORED + +๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ทœ์น™์ž…๋‹ˆ๋‹ค. **`mimetype` ํŒŒํŠธ๋Š” ZIP์˜ ์ฒซ ๋ฒˆ์งธ ์—”ํŠธ๋ฆฌ์—ฌ์•ผ ํ•˜๊ณ , ์••์ถ•ํ•˜์ง€ ์•Š๊ณ (STORED) ์ €์žฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.** OPC ๊ณ„์—ด ํฌ๋งท(ODF, EPUB ๋“ฑ)์ด ๊ณต์œ ํ•˜๋Š” ๊ด€๋ก€๋กœ, ZIP ํ—ค๋”๋งŒ ์ฝ์–ด๋„ ํŒŒ์ผ ์ข…๋ฅ˜๋ฅผ ํŒ๋ณ„ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค. + +๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ƒ์ˆ˜์™€ ์“ฐ๊ธฐ ๊ฒฝ๋กœ๊ฐ€ ์ด๋ฅผ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค โ€” `src/hwpx/opc/package.py`: + +```python +MIMETYPE_PATH = "mimetype" +DEFAULT_MIMETYPE = "application/hwp+zip" +``` + +`_write_archive`๊ฐ€ mimetype์„ ํ•ญ์ƒ ๋จผ์ € ์“ฐ๊ณ , ๋‚˜๋จธ์ง€๋Š” ์›๋ž˜ ์ˆœ์„œ๋ฅผ ๋ณด์กดํ•œ ๋’ค ์ƒˆ ํŒŒํŠธ๋ฅผ ๋’ค์— ๋ถ™์ž…๋‹ˆ๋‹ค. mimetype๋งŒ `ZIP_STORED`, ๋‚˜๋จธ์ง€๋Š” `ZIP_DEFLATED`๋กœ ์”๋‹ˆ๋‹ค: + +```python +def _write_archive(self, zf: ZipFile) -> None: + self._write_mimetype(zf) # ํ•ญ์ƒ ์ฒซ ์—”ํŠธ๋ฆฌ + ... + for name in [*ordered_names, *new_names]: + self._write_zip_entry(zf, name, self._files[name], ZIP_DEFLATED) +``` + +์›๋ณธ์ด mimetype์„ ์••์ถ•ํ•ด์„œ ์ €์žฅํ–ˆ๋”๋ผ๋„, ์ €์žฅ ์‹œ ๋ฌด์กฐ๊ฑด STORED๋กœ ๋‹ค์‹œ ์”๋‹ˆ๋‹ค. ๊ฒ€์ฆ๊ธฐ `src/hwpx/tools/package_validator.py`๋Š” ์„ธ ์กฐ๊ฑด์„ ํ•˜๋“œ ์—๋Ÿฌ๋กœ ์žก์Šต๋‹ˆ๋‹ค: mimetype ๊ฐ’์ด `application/hwp+zip`์ธ๊ฐ€, mimetype์ด ์ฒซ ์—”ํŠธ๋ฆฌ์ธ๊ฐ€, `ZIP_STORED`์ธ๊ฐ€. + +์ˆ˜๋ฆฌ ๊ฒฝ๋กœ๋„ ๊ฐ™์Šต๋‹ˆ๋‹ค โ€” `src/hwpx/tools/repair.py`์˜ `_ordered_entries`๊ฐ€ mimetype์„ ๋งจ ์•ž์œผ๋กœ ์˜ฎ๊ธฐ๊ณ , ์—†๊ฑฐ๋‚˜ ์ค‘๋ณต์ด๋ฉด ๊ฑฐ๋ถ€ํ•ฉ๋‹ˆ๋‹ค. `repair_repack`์€ mimetype์ด STORED๊ฐ€ ์•„๋‹ˆ์—ˆ์œผ๋ฉด "์žฌ์ •๋ ฌ๋จ"์œผ๋กœ ๋ฆฌํฌํŠธํ•ฉ๋‹ˆ๋‹ค. + +์ด ๊ณ„์•ฝ์€ `tests/test_opc_package.py`, `tests/test_repair_repack.py`๊ฐ€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค(์˜ˆ: `test_save_rewrites_mimetype_as_stored_even_when_source_was_compressed`). + +## ๋ฏธํŽธ์ง‘ ๋ฐ”์ดํŠธ๋ฅผ ์ง€ํ‚ค๋Š” ๋ถ€๋ถ„ ํŒจ์น˜ + +ํ•œ/๊ธ€์ด ํŒŒ์ผ์„ "๋ณ€์กฐ๋˜์ง€ ์•Š์•˜๋‹ค"๊ณ  ์ธ์‹ํ•˜๊ฒŒ ํ•˜๋ ค๋ฉด, ์†๋Œ€์ง€ ์•Š์€ ํŒŒํŠธ๋Š” ์›๋ณธ ๊ทธ๋Œ€๋กœ ๋‘๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. ์ €์žฅ ์‹œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” **์›๋ž˜ ZIP ์—”ํŠธ๋ฆฌ ์ˆœ์„œ์™€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(์••์ถ• ๋ฐฉ์‹, create_system, external_attr ๋“ฑ)๋ฅผ ๋ณด์กด**ํ•˜๊ณ , ํŽธ์ง‘ํ•œ ํŒŒํŠธ๋งŒ ๋‹ค์‹œ ์”๋‹ˆ๋‹ค(`test_save_preserves_existing_archive_order_and_entry_metadata`). ๋ฐ”์ดํŠธ ๋ณด์กด ํŽธ์ง‘ ๊ฒฝ๋กœ๋Š” ์—ฌ๊ธฐ์— ๋”ํ•ด, ์„น์…˜ XML์˜ ๋ฐ”์ดํŠธ๋ฅผ ๋ถ€๋ถ„ ์Šคํ”Œ๋ผ์ด์Šคํ•ด ๋ฏธํŽธ์ง‘ ์˜์—ญ์„ ํ•œ ๋ฐ”์ดํŠธ๋„ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + +## version.xml ๊ด€๋ฆฌ + +`version.xml`์€ ํ•œ/๊ธ€ ๋ฒ„์ „ ์ •๋ณด๋ฅผ ๋‹ด๋Š” ์„ ํƒ์ ์ด์ง€๋งŒ ์ค‘์š”ํ•œ ํŒŒํŠธ์ž…๋‹ˆ๋‹ค(`src/hwpx/opc/package.py`์˜ `VersionInfo`). ์‹ค์ „์—์„œ ์•Œ์•„ ๋‘˜ ์ : + +- **์—†์–ด๋„ ์—ด๋ฆฐ๋‹ค.** ํŒŒ์‹ฑ ์‹œ version.xml์ด ์—†์œผ๋ฉด ์—๋Ÿฌ๊ฐ€ ์•„๋‹ˆ๋ผ ๊ฒฝ๊ณ ๋ฅผ ๋‚ด๊ณ  ๊ธฐ๋ณธ๊ฐ’์„ ์”๋‹ˆ๋‹ค(`_parse_version`). ๋‹จ, ํ•œ๋ฒˆ ์—ด๋ฆฐ ๋ฌธ์„œ์—์„œ๋Š” `mimetype`ยท`container.xml`ยท`version.xml`์„ ํ•„์ˆ˜ยท์‚ญ์ œ ๋ถˆ๊ฐ€ ํŒŒํŠธ๋กœ ์ทจ๊ธ‰ํ•ฉ๋‹ˆ๋‹ค. +- **dirty์ผ ๋•Œ๋งŒ ๋‹ค์‹œ ์“ด๋‹ค.** ์ €์žฅ ์‹œ version ์ •๋ณด๊ฐ€ ๋ณ€๊ฒฝ(dirty)๋œ ๊ฒฝ์šฐ์—๋งŒ ์žฌ์ง๋ ฌํ™”ํ•˜๊ณ , XML ์„ ์–ธ(``)์€ ์›๋ณธ์„ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. +- **ํ•œ์ปด์˜ ์˜คํƒ€๊นŒ์ง€ ๋ณด์กด.** ๊ธฐ๋ณธ version.xml์˜ ๋ฃจํŠธ ์†์„ฑ ์ด๋ฆ„์€ `tagetApplication`์ž…๋‹ˆ๋‹ค(`targetApplication`์˜ ์˜คํƒ€). ํ•œ/๊ธ€์ด ์‹ค์ œ๋กœ ๊ทธ๋ ‡๊ฒŒ ์“ฐ๋ฏ€๋กœ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋„ ๊ทธ๋Œ€๋กœ ์žฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. + +## manifest์™€ container ๊ด€๊ณ„ + +HWPX์˜ ๋งค๋‹ˆํŽ˜์ŠคํŠธ๋Š” ODF์‹ `META-INF/manifest.xml`์ด ์•„๋‹ˆ๋ผ **OPF ํ˜•์‹์˜ `Contents/content.hpf`** ์ž…๋‹ˆ๋‹ค(`src/hwpx/opc/relationships.py`์˜ `MAIN_ROOTFILE_MEDIA_TYPE = "application/hwpml-package+xml"`). + +- `META-INF/container.xml`์ด rootfile์„ ์„ ์–ธํ•ฉ๋‹ˆ๋‹ค. ์—†์œผ๋ฉด ํ•˜๋“œ ์—๋Ÿฌ(`_parse_container`), rootfile์ด ํ•˜๋‚˜๋„ ์—†์–ด๋„ ์—๋Ÿฌ์ž…๋‹ˆ๋‹ค. +- ๋ฉ”์ธ rootfile(`content.hpf`)์˜ `/`๋“ค์ด ๊ฐ ํŒŒํŠธ๋ฅผ id๋กœ ๋งคํ•‘ํ•˜๊ณ , `/`๊ฐ€ ๋ณธ๋ฌธ ํŒŒํŠธ ์ˆœ์„œ๋ฅผ ์ •ํ•ฉ๋‹ˆ๋‹ค. `parse_manifest_relationships`๊ฐ€ ํ—ค๋”/๋งˆ์Šคํ„ฐํŽ˜์ด์ง€/ํžˆ์Šคํ† ๋ฆฌ/๋ฒ„์ „ ํŒŒํŠธ๋ฅผ ์—ฌ๊ธฐ์„œ ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค. + +## ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์ •๊ทœํ™”: 2016 โ†’ 2011 + +HWPML ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋Š” ์—ฐ๋„๋ณ„ ๋ณ€์ข…์ด ์žˆ์Šต๋‹ˆ๋‹ค(2011, 2016, 2024). ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ๋‹ค์–‘ํ•œ ์—ฐ๋„๋กœ ์ €์ž‘๋œ ๋ฌธ์„œ๋ฅผ ํ•˜๋‚˜์˜ ์ƒ์ˆ˜ ์ง‘ํ•ฉ์œผ๋กœ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•ด, **ํŒŒ์‹ฑ ์ง์ „์— 2016 ๋„ค์ž„์ŠคํŽ˜์ด์Šค URI๋ฅผ 2011 ๋“ฑ๊ฐ€๋ฌผ๋กœ ์น˜ํ™˜**ํ•ฉ๋‹ˆ๋‹ค โ€” `src/hwpx/opc/xml_utils.py`: + +```python +_HWPML_2016_TO_2011 = ( + (b".../hwpml/2016/paragraph", b".../hwpml/2011/paragraph"), + (b".../hwpml/2016/head", b".../hwpml/2011/head"), + (b".../hwpml/2016/section", b".../hwpml/2011/section"), + (b".../hwpml/2016/core", b".../hwpml/2011/core"), + (b".../hwpml/2016/master-page", b".../hwpml/2011/master-page"), + (b".../hwpml/2016/history", b".../hwpml/2011/history"), + (b".../hwpml/2016/app", b".../hwpml/2011/app"), +) + +def normalize_hwpml_namespaces(data: bytes) -> bytes: + for old, new in _HWPML_2016_TO_2011: + if old in data: + data = data.replace(old, new) + return data +``` + +7๊ฐœ ๊ณ„์—ด(paragraph, head, section, core, master-page, history, app)์ด ๋Œ€์ƒ์ž…๋‹ˆ๋‹ค. ๋•๋ถ„์— ํ•˜์œ„ ์ฝ”๋“œ๋Š” 2011 URI ํ•˜๋‚˜๋กœ๋งŒ ์š”์†Œ๋ฅผ ์ฐพ์œผ๋ฉด ๋ฉ๋‹ˆ๋‹ค. + +**์ฃผ์˜ํ•  ์ •์งํ•œ ํ•œ๊ณ„**: ์ด ์ •๊ทœํ™”๋Š” *ํŒŒ์‹ฑํ•œ ํŠธ๋ฆฌ*์—๋งŒ ์˜ํ–ฅ์„ ์ค๋‹ˆ๋‹ค. ํŽธ์ง‘ํ•˜์ง€ ์•Š์€ ํŒŒํŠธ๋Š” ์›๋ณธ ์ €์žฅ ๋ฐ”์ดํŠธ ๊ทธ๋Œ€๋กœ ์ €์žฅ๋˜๋ฏ€๋กœ, ์˜ˆ์ปจ๋Œ€ 2024 ๋„ค์ž„์ŠคํŽ˜์ด์Šค ๋ฌธ์„œ๋Š” ํŽธ์ง‘ ํ›„์—๋„ 2024 URI๊ฐ€ ๋ณด์กด๋ฉ๋‹ˆ๋‹ค(`tests/test_namespace_handling.py::test_open_to_bytes_preserves_source_namespace_after_edit`). ์—ด๊ธฐ๋Š” 2011/2016/2024๋ฅผ ๋ชจ๋‘ ์ˆ˜์šฉํ•ฉ๋‹ˆ๋‹ค. + +์ˆ˜๋ฆฌ ๊ฒฝ๋กœ๋Š” ์„น์…˜/ํ—ค๋” ๋ฃจํŠธ๋ฅผ ํ•œ/๊ธ€์ด ์“ฐ๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ ํญ๋„“์€ ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์„ ์–ธ(`hp10`์ด 2016 paragraph URI๋ฅผ ๊ฐ€๋ฆฌํ‚ค๋Š” ๊ฒƒ ํฌํ•จ)์œผ๋กœ ๋‹ค์‹œ ๊ฐ์‹ธ๊ณ , `standalone="yes"` ์„ ์–ธ์„ ์žฌ๋ฐœํ–‰ํ•ฉ๋‹ˆ๋‹ค(`src/hwpx/tools/repair.py`์˜ `_serialize_hwpml_compat_root`). ์ด๋Š” read-modify-save ์™•๋ณต์ด "๋ณ€์กฐ๋œ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ด์ง€ ์•Š๊ฒŒ" ํ•˜๋ ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. + +## ์‹ค์ „ ์š”์•ฝ + +- HWPX ZIP์„ ์ง์ ‘ ์žฌํŒจํ‚นํ•œ๋‹ค๋ฉด **mimetype์„ ์ฒซ ์—”ํŠธ๋ฆฌยทSTORED**๋กœ ๋„ฃ์œผ์„ธ์š”. ์ด ํ•˜๋‚˜๋งŒ ์–ด๊ฒจ๋„ ํ•œ/๊ธ€์ด ์ธ์‹ํ•˜์ง€ ๋ชปํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +- ๋ฏธํŽธ์ง‘ ํŒŒํŠธ๋Š” ์ˆœ์„œยท๋ฉ”ํƒ€๋ฐ์ดํ„ฐยท๋ฐ”์ดํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณด์กดํ•˜๋Š” ํŽธ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. +- ๋งค๋‹ˆํŽ˜์ŠคํŠธ๋Š” `Contents/content.hpf`(OPF)์ด๊ณ , `META-INF/container.xml`์ด ๊ทธ๊ฒƒ์„ rootfile๋กœ ์„ ์–ธํ•ฉ๋‹ˆ๋‹ค. +- ์†์ƒ๋œ ํŒŒ์ผ์€ `hwpx.tools.repair`(CLI `hwpx-validate-package`์™€ ํ•จ๊ป˜)๋กœ ์žฌ์ •๋ ฌยท์ •๊ทœํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. diff --git a/docs/internals/oracle-limits.md b/docs/internals/oracle-limits.md new file mode 100644 index 0000000..a09c33f --- /dev/null +++ b/docs/internals/oracle-limits.md @@ -0,0 +1,50 @@ +# ํ•œ/๊ธ€ export ๊ธฐ๋ฐ˜ ๊ฒ€์ฆ์˜ ํ•œ๊ณ„ + +์ด ํ”„๋กœ์ ํŠธ๊ฐ€ ์‚ฐ์ถœ๋ฌผ์„ "ํ•œ/๊ธ€์ด ์‹ค์ œ๋กœ ๋ฐ›์•„๋“ค์ด๋Š”๊ฐ€"๋กœ ๊ฒ€์ฆํ•˜๋Š” ๋ฐฉ์‹์€ ๊ฐ•๋ ฅํ•˜์ง€๋งŒ, ๋งŒ๋Šฅ์€ ์•„๋‹™๋‹ˆ๋‹ค. ํŠนํžˆ **ํ•œ/๊ธ€์ด exportํ•œ PDF์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•ด ๊ฒ€์‚ฌํ•˜๋Š” ๋ฐฉ์‹**์—๋Š” ์กฐ์šฉํžˆ ๋ฌด๋„ˆ์งˆ ์ˆ˜ ์žˆ๋Š” ์‚ฌ๊ฐ์ง€๋Œ€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฌธ์„œ๋Š” ๊ทธ ํ•œ๊ณ„์™€, ์™œ ํ”ฝ์…€(์‹œ๊ฐ) ๊ฒ€์ฆ์ด ํ•„์š”ํ•œ์ง€๋ฅผ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. + +## ๋‘ ์ข…๋ฅ˜์˜ ์˜ค๋ผํด: ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜๊ณผ ํ”ฝ์…€ ๊ธฐ๋ฐ˜ + +ํ•œ/๊ธ€๋กœ ๋ฌธ์„œ๋ฅผ ๋ Œ๋”ํ•ด ๊ฒ€์ฆํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ํฌ๊ฒŒ ๋‘˜์ž…๋‹ˆ๋‹ค. + +1. **ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜**: ํ•œ/๊ธ€์ด PDF๋กœ export โ†’ PDF์—์„œ ๋‹จ์–ด/์ขŒํ‘œ๋ฅผ ์ถ”์ถœ(์˜ˆ: fitz `get_text`/words) โ†’ ๊ธฐ๋Œ€๊ฐ’๊ณผ ๋น„๊ต. ์˜ˆ์ปจ๋Œ€ ๋ชฉ์ฐจ ์ชฝ๋ฒˆํ˜ธ ๊ฒ€์ฆ(`src/hwpx/tools/toc_fidelity.py`)์ด "ํ•œ/๊ธ€ render โ†’ fitz words"๋กœ ์บ์‹œ๋œ ์ชฝ๋ฒˆํ˜ธ์™€ ์‹ค์ œ ๋ Œ๋” ์ชฝ๋ฒˆํ˜ธ๋ฅผ ๋Œ€์กฐํ•ฉ๋‹ˆ๋‹ค. +2. **ํ”ฝ์…€ ๊ธฐ๋ฐ˜**: ํ•œ/๊ธ€์ด PDF๋กœ export โ†’ PDF๋ฅผ ์ด๋ฏธ์ง€๋กœ ๋ž˜์Šคํ„ฐํ™” โ†’ ํ”ฝ์…€(์ž‰ํฌ) ์ˆ˜์ค€์—์„œ ๊ฒ€์‚ฌ. `src/hwpx/visual/`์˜ ๋ Œ๋” ๊ฒŒ์ดํŠธ๊ฐ€ ์ด ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. + +๋‘ ๋ฐฉ์‹์€ ์žก์•„๋‚ด๋Š” ๊ฒƒ์ด ๋‹ค๋ฅด๊ณ , ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ•œ๊ณ„๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. + +## ํ…์ŠคํŠธ ์ถ”์ถœ์ด ์นจ๋ฌต ์‹คํŒจํ•  ์ˆ˜ ์žˆ๋‹ค + +**์‹ค์ œ ํ•œ/๊ธ€์—์„œ ํ™•์ธ๋œ ๋™์ž‘**: ํŠน์ • ๋ฌธ์„œ์—์„œ๋Š” ํ•œ/๊ธ€์ด ๋ณธ๋ฌธ ํ…์ŠคํŠธ๋ฅผ **๋ฌธ์ž(๊ธ€๋ฆฌํ”„)๊ฐ€ ์•„๋‹ˆ๋ผ ๋ฒกํ„ฐ ์ปค๋ธŒ๋กœ** PDF์— exportํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ๋˜๋ฉด PDF์—๋Š” ์‹œ๊ฐ์ ์œผ๋กœ ๊ธ€์ž๊ฐ€ ๊ทธ๋ ค์ ธ ์žˆ์ง€๋งŒ, **ํ…์ŠคํŠธ ์ถ”์ถœ์€ ์•„๋ฌด ๋‹จ์–ด๋„ ๋Œ๋ ค์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค**(์ถ”์ถœ ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด ์žˆ์Œ). + +์ด๊ฒƒ์ด ์œ„ํ—˜ํ•œ ์ด์œ ๋Š” ์‹คํŒจ๊ฐ€ **์กฐ์šฉํ•˜๊ธฐ** ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ๊ฒŒ์ดํŠธ๋Š” "์ถ”์ถœ๋œ ํ…์ŠคํŠธ vs ๊ธฐ๋Œ€ ํ…์ŠคํŠธ"๋ฅผ ๋น„๊ตํ•˜๋Š”๋ฐ, ์ถ”์ถœ ๊ฒฐ๊ณผ๊ฐ€ ๋น„๋ฉด "๋น„๊ตํ•  ๊ฒƒ์ด ์—†์Œ"์ด ๋˜์–ด โ€” ๊ฒŒ์ดํŠธ๋ฅผ ์–ด๋–ป๊ฒŒ ์„ค๊ณ„ํ–ˆ๋А๋ƒ์— ๋”ฐ๋ผ โ€” **์•„๋ฌด ๊ฒฐํ•จ๋„ ์—†๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ํ†ต๊ณผ**ํ•ด ๋ฒ„๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ๋Š” ๋ Œ๋”๊ฐ€ ์ •์ƒ์ธ์ง€์กฐ์ฐจ ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ๋Š”๋ฐ ๋ง์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด "์นจ๋ฌต ์‹คํŒจ(silent pass)"์ž…๋‹ˆ๋‹ค. + +## ๊ทธ๋ž˜์„œ ํ”ฝ์…€ ๊ฒ€์ฆ์ด ํ•„์š”ํ•˜๋‹ค + +๋ฒกํ„ฐ ์ปค๋ธŒ๋กœ ๊ทธ๋ ค์กŒ๋“  ๊ธ€๋ฆฌํ”„๋กœ ๊ทธ๋ ค์กŒ๋“ , **๋ž˜์Šคํ„ฐํ™”ํ•œ ์ด๋ฏธ์ง€์—๋Š” ์ž‰ํฌ๊ฐ€ ๋‚จ์Šต๋‹ˆ๋‹ค**. ํ”ฝ์…€ ๊ธฐ๋ฐ˜ ๊ฒ€์ฆ์€ ํ…์ŠคํŠธ ์ถ”์ถœ์— ์˜์กดํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ด ์‚ฌ๊ฐ์ง€๋Œ€๋ฅผ ํ”ผํ•ฉ๋‹ˆ๋‹ค. `src/hwpx/visual/page_qa.py`๊ฐ€ ์ž‰ํฌ(ํ”ฝ์…€) ์ˆ˜์ค€์—์„œ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค: + +- **๋นˆ ํŽ˜์ด์ง€ ํƒ์ง€**: ์ž‰ํฌ ๋น„์œจ์ด ๋„ˆ๋ฌด ๋‚ฎ์œผ๋ฉด "์˜๋ฏธ ์žˆ๋Š” ๋ Œ๋” ์ž‰ํฌ ์—†์Œ"์œผ๋กœ ์‹คํŒจ โ€” ์ฆ‰ ํ…์ŠคํŠธ๊ฐ€ ์žˆ์–ด์•ผ ํ•  ์ž๋ฆฌ์— ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ๊ทธ๋ ค์กŒ์œผ๋ฉด ์žก์Šต๋‹ˆ๋‹ค. +- **๊ฒน์นจ ํƒ์ง€**: ๋น„์ •์ƒ์ ์œผ๋กœ ๋†’๊ณ  ๋นฝ๋นฝํ•œ ์ž‰ํฌ ๋ ๋ฅผ "๋ถ•๊ดด๋˜๊ฑฐ๋‚˜ ๊ฒน์นœ ํ…์ŠคํŠธ"๋กœ ํŒ์ •ํ•ฉ๋‹ˆ๋‹ค(์ฃผ์„: `Abnormally tall dense ink band suggests collapsed or overlapping text`). ๊ธ€์ž ๊ฒน์นจ(โ†’ [lineseg.md](lineseg.md) ์ฐธ๊ณ )์€ ํ…์ŠคํŠธ ์ถ”์ถœ๋กœ๋Š” ์•ˆ ๋ณด์—ฌ๋„ ํ”ฝ์…€๋กœ๋Š” ๋“œ๋Ÿฌ๋‚ฉ๋‹ˆ๋‹ค. +- **ํด๋ฆฌํ•‘ ํƒ์ง€**: ์ž‰ํฌ๊ฐ€ ํŽ˜์ด์ง€ ๊ฐ€์žฅ์ž๋ฆฌ์— ๋‹ฟ์œผ๋ฉด ์ž˜๋ ธ์„ ๊ฐ€๋Šฅ์„ฑ์œผ๋กœ ๊ฒฝ๊ณ . + +`src/hwpx/visual/detectors.py`๋Š” before/after ๋‘ ๋ Œ๋”์˜ ์ž‰ํฌ ๋งˆ์Šคํฌ ์ฐจ์ด(`diff_ratio`)๋ฅผ ํ”ฝ์…€ ๋‹จ์œ„๋กœ ๊ณ„์‚ฐํ•ด, ๋งˆ์Šคํฌ ๋ฐ–์—์„œ ์ž‰ํฌ๊ฐ€ ๋Š˜์–ด๋‚œ ์˜์—ญ์„ ์žก์Šต๋‹ˆ๋‹ค. ์ „๋ถ€ ํ…์ŠคํŠธ ๋‚ด์šฉ์ด ์•„๋‹ˆ๋ผ **ํ”ฝ์…€์˜ ์œ ๋ฌด**๋กœ ํŒ์ •ํ•˜๋ฏ€๋กœ ์ปค๋ธŒ export์— ํ”๋“ค๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + +## ์ •์ง ๊ทœ์œจ: ๊ฒ€์ฆํ•˜์ง€ ๋ชปํ•œ ๊ฒƒ์„ ํ†ต๊ณผ๋กœ ์„ธ์ง€ ์•Š๋Š”๋‹ค + +์ด ํ”„๋กœ์ ํŠธ์˜ ๋ Œ๋” ๊ฒŒ์ดํŠธ๋Š” "๋ฌด์—‡์„ ๊ฒ€์‚ฌํ–ˆ๋Š”๊ฐ€"์™€ "ํŒ์ •์ด ๋ฌด์—‡์ธ๊ฐ€"๋ฅผ ์˜๋„์ ์œผ๋กœ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค. `src/hwpx/visual/report.py`์˜ `VisualReport`: + +> `render_checked` is `True` only when a Hancom render diff actually ran. Off-oracle (no Hancom / missing imaging deps) it is `False` and the report is a *structural degrade*, never a silent visual pass. +> +> When `render_checked` is `False` it means "nothing could be verified" (optimistic-but-labelled), not "verified clean". + +์ฆ‰ ํ•œ/๊ธ€ ์˜ค๋ผํด์ด ์—†๊ฑฐ๋‚˜ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์˜์กด์„ฑ์ด ์—†์œผ๋ฉด, ๊ฒฐ๊ณผ๋Š” "๊ตฌ์กฐ๋งŒ ๋ดค์Œ(`render_checked=False`)"์œผ๋กœ **๋‚ฎ์ถฐ ๋ผ๋ฒจ๋ง**๋˜์ง€, "์‹œ๊ฐ์ ์œผ๋กœ ๊นจ๋—ํ•จ"์œผ๋กœ ์œ„์žฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ๋ชฉ์ฐจ ๊ฒ€์ฆ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ, ์˜ค๋ผํด์ด ์—†์œผ๋ฉด ๊ตฌ์กฐ ๊ฒ€์ฆ์œผ๋กœ degradeํ•ฉ๋‹ˆ๋‹ค(`toc_fidelity.py`). + +์ด ๊ทœ์œจ์€ ์‹ค์ธก ์ฝ”ํผ์Šค ๋ฐœํ–‰์—๋„ ๊ทธ๋Œ€๋กœ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค(`docs/corpus-metrics.md`). ์˜ˆ์ปจ๋Œ€ ๋ณ€๊ฒฝ์ถ”์  ๋ฌธ์„œ์˜ PDF export๋Š” **ํ•œ/๊ธ€ ์ž์ฒด๊ฐ€ ๊ฑฐ๋ถ€**ํ•˜๋Š”๋ฐ, ์ด๋Ÿฐ ๊ฑด์€ `render_unavailable` ๋ฒ„ํ‚ท์œผ๋กœ ๋ถ„๋ฆฌํ•ด ๋ฐœํ–‰ํ•˜๊ณ  **์ ˆ๋Œ€ pass๋กœ ์ง‘๊ณ„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค**. "๋‚ฎ์€ ์ˆซ์ž๋„ ๊ทธ๋Œ€๋กœ ๋ฐœํ–‰"ํ•˜๋Š” ๊ฒƒ์ด ์ด ํ”„๋กœ์ ํŠธ์˜ ์›์น™์ž…๋‹ˆ๋‹ค. + +## ํ•จ์ • ํ•˜๋‚˜ ๋”: refresh์™€ render์˜ ์„ธ์…˜ ๋ถ„๋ฆฌ + +๊ด€๋ จํ•ด์„œ ์‹ค์ธก๋œ ํ•จ์ •: ํ•„๋“œ(๋ชฉ์ฐจ ๋“ฑ)๋ฅผ ์žฌ์ƒ์„ฑ ์ค‘์ธ ํ•œ/๊ธ€ ์„ธ์…˜์—์„œ ๊ณง๋ฐ”๋กœ PDF export๋ฅผ ์‹œ๋„ํ•˜๋ฉด ์ด ํ•œ/๊ธ€ ๋นŒ๋“œ๊ฐ€ ํฌ๋ž˜์‹œํ•ฉ๋‹ˆ๋‹ค(์ž˜๋ฆฐ PDF ํ›„ ํ”„๋กœ์„ธ์Šค ์‚ฌ๋ง). ๊ทธ๋ž˜์„œ "ํ•„๋“œ ์žฌ๊ณ„์‚ฐ"๊ณผ "๋ Œ๋”"๋Š” ๋ณ„๊ฐœ ์„ธ์…˜์œผ๋กœ ๋ถ„๋ฆฌ๋ฉ๋‹ˆ๋‹ค(โ†’ [toc-dirty.md](toc-dirty.md) ์ฐธ๊ณ ). ์˜ค๋ผํด์„ ์ž๋™ํ™”ํ•  ๋•Œ ์•Œ์•„ ๋‘๋ฉด ์ข‹์€ ์ œ์•ฝ์ž…๋‹ˆ๋‹ค. + +## ์‹ค์ „ ์š”์•ฝ + +- ํ•œ/๊ธ€ export์˜ ํ…์ŠคํŠธ ์ถ”์ถœ ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด ์žˆ๋‹ค๊ณ  ํ•ด์„œ "๋ฌธ์„œ์— ๊ธ€์ž๊ฐ€ ์—†๋‹ค"๋Š” ๋œป์ด ์•„๋‹™๋‹ˆ๋‹ค โ€” ๋ฒกํ„ฐ ์ปค๋ธŒ๋กœ ๊ทธ๋ ค์กŒ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ๊ฒ€์‚ฌ๋งŒ ๋ฏฟ์œผ๋ฉด ์นจ๋ฌต ์‹คํŒจ์— ๋น ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +- ๋ Œ๋” ์ •์ƒ ์—ฌ๋ถ€๋Š” **ํ”ฝ์…€(์ž‰ํฌ) ์ˆ˜์ค€**์œผ๋กœ ๊ฒ€์ฆํ•˜์„ธ์š”(๋นˆ ํŽ˜์ด์ง€ยท๊ฒน์นจยทํด๋ฆฌํ•‘). ์ด ๋ฐฉ์‹์€ ๊ธ€๋ฆฌํ”„/์ปค๋ธŒ ์—ฌ๋ถ€์™€ ๋ฌด๊ด€ํ•ฉ๋‹ˆ๋‹ค. +- ๊ฒ€์ฆํ•˜์ง€ ๋ชปํ•œ ๊ฒƒ์€ ํ†ต๊ณผ๊ฐ€ ์•„๋‹ˆ๋ผ "๋ฏธ๊ฒ€์ฆ(`render_checked=False`)"์œผ๋กœ ๋ผ๋ฒจ๋งํ•˜๋Š” ๊ฒƒ์ด ์ •์งํ•ฉ๋‹ˆ๋‹ค. ์ด ํ”„๋กœ์ ํŠธ์˜ ๊ฒŒ์ดํŠธ์™€ ์ฝ”ํผ์Šค ๋ฆฌํฌํŠธ๊ฐ€ ๊ทธ ๊ทœ์œจ์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. diff --git a/docs/internals/toc-dirty.md b/docs/internals/toc-dirty.md new file mode 100644 index 0000000..95f0812 --- /dev/null +++ b/docs/internals/toc-dirty.md @@ -0,0 +1,58 @@ +# ๋ชฉ์ฐจ ํ•„๋“œ(TABLEOFCONTENTS)์™€ dirty="1" + +ํ•œ/๊ธ€์˜ ์ž๋™ ๋ชฉ์ฐจ๋Š” `` ํ•„๋“œ๋กœ ํ‘œํ˜„๋ฉ๋‹ˆ๋‹ค. ์ด ํ•„๋“œ์— ๋Œ€ํ•ด "์–ธ์ œ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๊ฐ€ ๋‹ค์‹œ ๊ณ„์‚ฐ๋˜๋Š”๊ฐ€"๋Š” ์ž๋™ํ™” ํ™˜๊ฒฝ์—์„œ ํŠนํžˆ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค โ€” ๋ฉ”๋‰ด ์—†์ด ๋ชฉ์ฐจ๋ฅผ ๊ฐฑ์‹ ํ•  ๋ฐฉ๋ฒ•์ด ๋”ฑ ํ•˜๋‚˜๋ฟ์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด ๋ฌธ์„œ๋Š” `dirty="1"` ํ”Œ๋ž˜๊ทธ์˜ ์‹ค์ œ ๋™์ž‘๊ณผ ๊ทธ ํ•œ๊ณ„๋ฅผ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. + +## ๋ชฉ์ฐจ ํ•„๋“œ์˜ ๊ตฌ์กฐ + +์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ํ•œ/๊ธ€์ด ์ง์ ‘ ์ €์žฅํ•œ ๋ฌธ์„œ๋ฅผ ๋ฆฌ๋ฒ„์Šค ์—”์ง€๋‹ˆ์–ด๋งํ•ด ๋ชฉ์ฐจ ํ•„๋“œ ๊ณ„์•ฝ์„ ์žฌํ˜„ํ•ฉ๋‹ˆ๋‹ค(`src/hwpx/tools/toc_author.py`). ๊ณจ์ž๋Š”: + +- ``๊ฐ€ `TableOfContents:set:...` Command ๋ฌธ์ž์—ด๊ณผ ํ•จ๊ป˜ ๋ชฉ์ฐจ ์˜์—ญ์„ ์—ฝ๋‹ˆ๋‹ค. +- ๊ฐ ๋ชฉ์ฐจ ํ•ญ๋ชฉ์€ HYPERLINK ํ•„๋“œ๋กœ, ํ•˜๋‚˜์˜ `` ์•ˆ์— "์ œ๋ชฉํ…์ŠคํŠธ + ์ ์„ (dot-leader) `` + ์ชฝ๋ฒˆํ˜ธ" ํ˜•ํƒœ๋ฅผ ๋‹ด์Šต๋‹ˆ๋‹ค. ์ชฝ๋ฒˆํ˜ธ๋Š” ์ค‘์ฒฉ๋œ `hp:tab`์˜ tail์— ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค. +- ํ•ญ๋ชฉ์˜ ์•ต์ปค๋Š” ๋Œ€์ƒ ๋ฌธ๋‹จ์˜ `id` ์†์„ฑ์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋ชฉ์ฐจ๊ฐ€ ๊ฐ€๋ฆฌํ‚ค๋Š” ์ œ๋ชฉ ๋ฌธ๋‹จ์€ **๋ฌธ์„œ ์ „์ฒด์—์„œ ์œ ์ผํ•œ id**๋ฅผ ๊ฐ€์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค(`ensure_paragraph_anchor_id`๊ฐ€ ์ด๋ฅผ ๋ณด์žฅ). ํ•œ/๊ธ€์ด ์ƒˆ๋กœ ์ƒ์„ฑํ•œ ๋ฌธ๋‹จ์€ ์ƒ์ˆ˜ id `2147483648`์„ ๊ณต์œ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์–ด ์•ต์ปค๋กœ๋Š” ์“ธ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค(`src/hwpx/tools/toc_fidelity.py`์˜ `NON_UNIQUE_PARA_ID`). + +## dirty="1"์ด ํ•˜๋Š” ์ผ + +๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋ชฉ์ฐจ๋ฅผ ์‚ฝ์ž…ํ•  ๋•Œ(`add_native_toc`), ํ•ญ๋ชฉ์˜ ์ชฝ๋ฒˆํ˜ธ๋Š” **naiveํ•œ ์ถ”์ •์น˜**๋กœ ์ฑ„์›Œ์ง‘๋‹ˆ๋‹ค. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ํ•œ/๊ธ€์ด ์•„๋‹ˆ๋ฏ€๋กœ ์‹ค์ œ ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋Œ€์‹  ํ•„๋“œ์— `dirty="1"`์„ ์„ธํŒ…ํ•ฉ๋‹ˆ๋‹ค. + +**์‹ค์ œ ํ•œ/๊ธ€์—์„œ ํ™•์ธ๋œ ๋™์ž‘**: `dirty="1"`์ธ TABLEOFCONTENTS ํ•„๋“œ๋Š” ํ•œ/๊ธ€์ด **๋ฌธ์„œ๋ฅผ ์—ฌ๋Š” ์‹œ์ ์—** ํ†ต์งธ๋กœ ์žฌ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค โ€” ํ•ญ๋ชฉ, ์Šคํƒ€์ผ, ์ชฝ๋ฒˆํ˜ธ ์ „๋ถ€๋ฅผ ํ•œ/๊ธ€์ด ์ง์ ‘ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. `src/hwpx/visual/oracle.py`์˜ `refresh_document` ์ฃผ์„์ด ์ด ํŠธ๋ฆฌ๊ฑฐ๋ฅผ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค: + +> The measured native-TOC re-number trigger: a `dirty="1"` TABLEOFCONTENTS is rebuilt on open โ€” Hancom itself computes entries and page numbers โ€” and CROSSREF caches recompute automatically. + +`src/hwpx/tools/toc_author.py`์˜ `mark_toc_dirty()`๊ฐ€ ๋ฐ”๋กœ ์ด ์šฉ๋„์ž…๋‹ˆ๋‹ค: ๋ชจ๋“  TABLEOFCONTENTS ํ•„๋“œ์— `dirty="1"`์„ ์„ธํŒ…ํ•ด, ๋‹ค์Œ ์—ด๊ธฐ ๋•Œ ํ•œ/๊ธ€์ด ์žฌ๊ณ„์‚ฐํ•˜๋„๋ก ๋งŒ๋“ญ๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ๋ฐ”๊พธ๋Š” ํŽธ์ง‘ ํ›„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. + +```python +def mark_toc_dirty(doc: HwpxDocument) -> int: + """Set dirty="1" on every TABLEOFCONTENTS field โ€” the measured + re-number trigger: Hancom regenerates a dirty TOC (entries, styles, page + numbers) when it next opens the document.""" +``` + +๊ฒฐ๊ณผ์ ์œผ๋กœ, ์‚ฌ์šฉ์ž๊ฐ€ ํŒŒ์ผ์„ ์ฒ˜์Œ ์—ด์—ˆ์„ ๋•Œ ๋ณด๋Š” ๋ชฉ์ฐจ๋Š” **ํ•œ/๊ธ€์ด ์Šค์Šค๋กœ ๊ณ„์‚ฐํ•œ** ๋ชฉ์ฐจ์ž…๋‹ˆ๋‹ค. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ถ”์ • ์ชฝ๋ฒˆํ˜ธ๊ฐ€ ์•„๋‹ˆ๋ผ. + +## ์™œ ์ด๊ฒƒ์ด ์œ ์ผํ•˜๊ฒŒ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐฑ์‹  ๊ฒฝ๋กœ์ธ๊ฐ€ + +ํ•œ/๊ธ€ ๋ฐ์Šคํฌํ†ฑ์—์„œ๋Š” ๋ชฉ์ฐจ๋ฅผ ์šฐํด๋ฆญํ•ด "์ฐจ๋ก€ ์ƒˆ๋กœ ๊ณ ์นจ"์œผ๋กœ ๊ฐฑ์‹ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ **๋ฉ”๋‰ด๊ฐ€ ์—†๋Š” ์ž๋™ํ™” ํ™˜๊ฒฝ**(์„œ๋ฒ„, CI, ํ—ค๋“œ๋ฆฌ์Šค ํŒŒ์ดํ”„๋ผ์ธ)์—์„œ๋Š” ๊ทธ UI ์กฐ์ž‘์ด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๋‚จ๋Š” ๊ฒƒ์€ XML์— `dirty="1"`์„ ์‹ฌ์–ด ๋‘๊ณ , ํ•œ/๊ธ€์ด ๋ฌธ์„œ๋ฅผ ์—ฌ๋Š” ์ˆœ๊ฐ„ ์žฌ๊ณ„์‚ฐ์— ๋งก๊ธฐ๋Š” ๋ฐฉ๋ฒ•๋ฟ์ž…๋‹ˆ๋‹ค. + +์ด ํ”„๋กœ์ ํŠธ์˜ ๋ Œ๋” ์˜ค๋ผํด์€ ์ •ํ™•ํžˆ ์ด ๊ฒฝ๋กœ๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค โ€” `refresh_document`๋Š” ๋ฌธ์„œ๋ฅผ ์—ด์–ด dirty ํ•„๋“œ๊ฐ€ ์žฌ์ƒ์„ฑ๋˜๊ฒŒ ํ•˜๊ณ , ์ œ์ž๋ฆฌ ์ €์žฅ ํ›„ ๋‹ซ์Šต๋‹ˆ๋‹ค. ํ•œ ๊ฐ€์ง€ ์‹ค์ธก๋œ ํ•จ์ •: **์žฌ์ƒ์„ฑ ์ค‘์ธ ์„ธ์…˜์—์„œ ๊ณง๋ฐ”๋กœ PDF export๋ฅผ ์‹œ๋„ํ•˜๋ฉด ์ด ํ•œ/๊ธ€ ๋นŒ๋“œ๊ฐ€ ํฌ๋ž˜์‹œ**ํ•ฉ๋‹ˆ๋‹ค(์ž˜๋ฆฐ PDF ํ›„ ํ”„๋กœ์„ธ์Šค ์‚ฌ๋ง). ๊ทธ๋ž˜์„œ refresh์™€ render๋Š” ์˜๋„์ ์œผ๋กœ ๋ณ„๊ฐœ์˜ ์„ธ์…˜์œผ๋กœ ๋ถ„๋ฆฌ๋ฉ๋‹ˆ๋‹ค. + +## ์ค‘์š”ํ•œ ํ•œ๊ณ„: dirty๋Š” "์‹ ์„ ๋„ ํ‘œ์‹œ"๊ฐ€ ์•„๋‹ˆ๋‹ค + +`dirty="1"`์€ **์žฌ๊ณ„์‚ฐ ํŠธ๋ฆฌ๊ฑฐ**์ด์ง€, ์บ์‹œ๋œ ์ชฝ๋ฒˆํ˜ธ๊ฐ€ ๋งž๋Š”์ง€ ํ‹€๋ฆฐ์ง€๋ฅผ ์•Œ๋ ค์ฃผ๋Š” **์‹ ์„ ๋„(staleness) ํ‘œ์‹œ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค**. `src/hwpx/tools/toc_fidelity.py`๊ฐ€ ์ด ๊ตฌ๋ถ„์„ ๋ช…ํ™•ํžˆ ํ•ฉ๋‹ˆ๋‹ค: + +> the TOC block only recomputes after an explicit ์ฐจ๋ก€ ์ƒˆ๋กœ ๊ณ ์นจ โ€” so `dirty` is NOT a reliable staleness marker and the only honest verdict comes from comparing cached page numbers against rendered ones (Hancom render -> fitz words). + +์ฆ‰: + +- ํŒŒ์ผ ์•ˆ์˜ ๋ชฉ์ฐจ ์ชฝ๋ฒˆํ˜ธ๋ฅผ ๋ณด๊ณ  "dirty๊ฐ€ ๊บผ์ ธ ์žˆ์œผ๋‹ˆ ๋งž๋‹ค"๊ณ  ๋‹จ์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ํ•œ/๊ธ€์ด ์‹ค์ œ๋กœ ๋‹ค์‹œ ์—ด์–ด ์žฌ๊ณ„์‚ฐํ•˜๊ธฐ ์ „๊นŒ์ง€๋Š”, ๊ทธ ์ˆซ์ž๋Š” ๊ทธ์ € ์บ์‹œ์ผ ๋ฟ์ž…๋‹ˆ๋‹ค. +- ๋ชฉ์ฐจ ์ชฝ๋ฒˆํ˜ธ๊ฐ€ ์ •๋ง ๋งž๋Š”์ง€ **์ •์งํ•˜๊ฒŒ ๊ฒ€์ฆ**ํ•˜๋ ค๋ฉด, ํ•œ/๊ธ€๋กœ ๋ Œ๋”ํ•œ ๋’ค ์‹ค์ œ ๋ Œ๋”๋œ ์ชฝ๋ฒˆํ˜ธ์™€ ์บ์‹œ ๊ฐ’์„ ๋น„๊ตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค(`toc_fidelity`๊ฐ€ ํ•˜๋Š” ์ผ). ์˜ค๋ผํด์ด ์—†์œผ๋ฉด ์ด ๊ฒ€์‚ฌ๋Š” ๊ตฌ์กฐ ๊ฒ€์ฆ(`render_checked=False`)์œผ๋กœ ๋‚ฎ์ถฐ์ง€๋ฉฐ, ํ†ต๊ณผ๋กœ ์œ„์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + +## CROSSREF๋Š” ๋‹ค๋ฅด๋‹ค + +๊ฐ™์€ ํ•„๋“œ ๊ณ„์—ด์ด์ง€๋งŒ ํŽ˜์ด์ง€ ์ƒํ˜ธ์ฐธ์กฐ(``)๋Š” ๋™์ž‘์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค. **์‹ค์ œ ํ•œ/๊ธ€์—์„œ ํ™•์ธ๋œ ๋™์ž‘**: CROSSREF ์บ์‹œ๋Š” ํŽธ์ง‘/์ €์žฅ ์‹œ **์ž๋™์œผ๋กœ** ์žฌ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค(`src/hwpx/tools/toc_fidelity.py`ยท`toc_author.py`์— measured๋กœ ๊ธฐ๋ก). ๋ชฉ์ฐจ ๋ธ”๋ก์ฒ˜๋Ÿผ ๋ช…์‹œ์  ์žฌ์ƒ์„ฑ ํŠธ๋ฆฌ๊ฑฐ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + +## ์‹ค์ „ ์š”์•ฝ + +- ๋ชฉ์ฐจ๋ฅผ ์‚ฝ์ž…/ํŽธ์ง‘ํ•œ ๋’ค์—๋Š” `mark_toc_dirty()`(๋˜๋Š” `dirty=True`)๋กœ TABLEOFCONTENTS ํ•„๋“œ๋ฅผ dirty ์ƒํƒœ๋กœ ๋‚จ๊ธฐ์„ธ์š”. ํ•œ/๊ธ€์ด ๋‹ค์Œ ์—ด๊ธฐ ๋•Œ ์ชฝ๋ฒˆํ˜ธ๊นŒ์ง€ ์žฌ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. +- ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์ฑ„์šด ์ชฝ๋ฒˆํ˜ธ๋Š” ์ถ”์ •์น˜์ž…๋‹ˆ๋‹ค. ์ตœ์ข… ๊ฐ’์€ ํ•œ/๊ธ€์ด ๋งŒ๋“ญ๋‹ˆ๋‹ค. +- ๋ชฉ์ฐจ ์ •ํ•ฉ์„ **๊ฒ€์ฆ**ํ•˜๋ ค๋ฉด ํ•œ/๊ธ€ ๋ Œ๋”์™€ ๋น„๊ตํ•˜์„ธ์š”. `dirty` ๊ฐ’๋งŒ์œผ๋กœ ์‹ ์„ ๋„๋ฅผ ํŒ๋‹จํ•˜์ง€ ๋งˆ์„ธ์š”. +- ๋ชฉ์ฐจ๊ฐ€ ๊ฐ€๋ฆฌํ‚ค๋Š” ์ œ๋ชฉ ๋ฌธ๋‹จ์€ ์œ ์ผํ•œ `id`๋ฅผ ๊ฐ€์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค(์ƒ์ˆ˜ id `2147483648`์€ ์•ต์ปค๋กœ ๋ถ€์ ํ•ฉ). diff --git a/docs/internals/units.md b/docs/internals/units.md new file mode 100644 index 0000000..b40440c --- /dev/null +++ b/docs/internals/units.md @@ -0,0 +1,52 @@ +# HWPUNIT ์ขŒํ‘œ๊ณ„ + +HWPX ๋ฌธ์„œ์˜ ๊ฑฐ์˜ ๋ชจ๋“  ๊ธฐํ•˜ ๊ฐ’ โ€” ์šฉ์ง€ ํฌ๊ธฐ, ์—ฌ๋ฐฑ, ํ‘œ ์…€ ๋„ˆ๋น„, ๊ธ€์ž ํฌ๊ธฐ, ๊ฐœ์ฒด ์œ„์น˜ โ€” ์€ **HWPUNIT**์ด๋ผ๋Š” ๋‹จ์ผ ์ •์ˆ˜ ๋‹จ์œ„๋กœ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. ์ด ๋‹จ์œ„๋ฅผ ์ดํ•ดํ•˜๋ฉด XML์„ ์ง์ ‘ ๋“ค์—ฌ๋‹ค๋ณผ ๋•Œ ์ˆซ์ž๊ฐ€ ๋ฌด์—‡์„ ๋œปํ•˜๋Š”์ง€ ๋ฐ”๋กœ ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +## ๊ธฐ๋ณธ ํ™˜์‚ฐ + +HWPUNIT์€ ์ธ์น˜๋ฅผ 7,200๋“ฑ๋ถ„ํ•œ ๋‹จ์œ„์ž…๋‹ˆ๋‹ค. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๋ณ€ํ™˜ ์ƒ์ˆ˜๊ฐ€ ์ด๋ฅผ ๊ทธ๋Œ€๋กœ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค (`src/hwpx/_document/_units.py`): + +```python +_HWP_UNITS_PER_MM = 7200 / 25.4 +_HWP_UNITS_PER_PT = 100 +``` + +์—ฌ๊ธฐ์„œ ์„ธ ๊ฐ€์ง€ ํ™˜์‚ฐ์ด ๋„์ถœ๋ฉ๋‹ˆ๋‹ค. + +| ๋‹จ์œ„ | HWPUNIT | ์œ ๋ž˜ | +|---|---|---| +| 1 inch | 7,200 | ์ •์˜ | +| 1 pt | 100 | 7,200 / 72 (1 inch = 72 pt) | +| 1 mm | ์•ฝ 283.46 | 7,200 / 25.4 | + +`src/hwpx/form_fit/measure.py`, `src/hwpx/form_fit/seal.py`์˜ ํ—ค๋” ์ฃผ์„๋„ ๊ฐ™์€ ๊ด€๊ณ„๋ฅผ ๋ชป ๋ฐ•์•„ ๋‘ก๋‹ˆ๋‹ค: `1 pt = 100 HWPUNIT, 1 inch = 7200 HWPUNIT`, `1 PDF point = 7200/72 = 100 HWPUNIT`. PDF ์ขŒํ‘œ(1 pt)์™€ HWPUNIT์ด ์ •ํ™•ํžˆ 100:1๋กœ ๋Œ€์‘ํ•˜๋ฏ€๋กœ, ํ•œ/๊ธ€์ด exportํ•œ PDF์˜ ์ขŒํ‘œ๋ฅผ HWPUNIT์œผ๋กœ ๋˜๋Œ๋ฆด ๋•Œ ์‹ค์ˆ˜ ์˜ค์ฐจ ์—†์ด ์ •์ˆ˜๋ฐฐ๋กœ ํ™˜์‚ฐ๋ฉ๋‹ˆ๋‹ค. + +## HWPUNIT์ด ๋‚˜ํƒ€๋‚˜๋Š” ๊ณณ + +๊ฐ™์€ ๋‹จ์œ„๊ฐ€ ์—ฌ๋Ÿฌ ์š”์†Œ์— ๊ฑธ์ณ ์žฌ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + +- **์šฉ์ง€ยท์—ฌ๋ฐฑ**: ์„น์…˜ ์ •์˜์˜ ``, ์—ฌ๋ฐฑ ๊ฐ’. `src/hwpx/_document/layout.py`๊ฐ€ mm ์ž…๋ ฅ์„ `_mm_to_hwp_units()`๋กœ ๋ณ€ํ™˜ํ•ด ์ฑ„์›๋‹ˆ๋‹ค. +- **ํ‘œ ์…€ ๋„ˆ๋น„/๋†’์ด**: ``, ``. ๊ธฐ๋ณธ ์…€ ๋„ˆ๋น„ ์ƒ์ˆ˜๋„ HWPUNIT์ž…๋‹ˆ๋‹ค โ€” `src/hwpx/oxml/_document_primitives.py`์˜ `_DEFAULT_CELL_WIDTH = 7200`(= 1 inch). +- **๊ธ€์ž ํฌ๊ธฐ**: ๋ฌธ์ž ์†์„ฑ ``. ๊ธ€์ž ๋†’์ด๋„ ๊ฐ™์€ ์Šค์ผ€์ผ์ด๋ผ **100 HWPUNIT = 1 pt**์ž…๋‹ˆ๋‹ค. ์‹ค์ œ ํ•œ์ปด์ด ์ €์žฅํ•œ ๋ฌธ์„œ๋ฅผ ๋ณด๋ฉด 10 pt ๊ธ€์ž๋Š” `height="1000"`, 9 pt๋Š” `height="900"`, 11 pt๋Š” `height="1100"`์œผ๋กœ ๋‚˜์˜ต๋‹ˆ๋‹ค. `src/hwpx/form_fit/measure.py` ํ—ค๋”๊ฐ€ ์ง€์ ํ•˜๋“ฏ "๊ธ€์ž ๋†’์ด์™€ ์…€ ๋„ˆ๋น„๊ฐ€ ๊ฐ™์€ ๋‹จ์œ„๋ฅผ ๊ณต์œ "ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ๊ธ€์ž์˜ ์ง„ํ–‰ํญ(advance)์„ ๊ธ€์ž ๋†’์ด์˜ ๋ถ„์ˆ˜(em)๋กœ ๋ฐ”๋กœ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๊ณ  ๋ณ„๋„์˜ DPI/ํฌ์ธํŠธ ๋ณ€ํ™˜์ด ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค. +- **๊ฐœ์ฒด ์œ„์น˜ยทํฌ๊ธฐ**: ๊ทธ๋ฆผยท๋„ํ˜•์˜ ``, `` ์ขŒํ‘œ. `src/hwpx/document.py`, `src/hwpx/_document/shapes.py`์˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜๊ฐ€ `height: int = 7200`์ฒ˜๋Ÿผ HWPUNIT ๊ธฐ๋ณธ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค(์ฃผ์„: `Coordinates are in HWPUNIT (7200 per inch)`). +- **์ด๋ฏธ์ง€ ํฌ๊ธฐ**: `src/hwpx/_document/media.py`๊ฐ€ mm ์ž…๋ ฅ์„ `_mm_to_hwp_units()`๋กœ ๋ณ€ํ™˜ํ•˜๊ณ , ๊ธฐ๋ณธ๊ฐ’์€ `14400`(= 2 inch) ๊ฐ™์€ HWPUNIT ์ƒ์ˆ˜์ž…๋‹ˆ๋‹ค. + +## ์˜ˆ์™ธ: ์ค„ ๊ฐ„๊ฒฉ์€ ํผ์„ผํŠธ + +๋ชจ๋“  ๊ฒƒ์ด HWPUNIT์€ ์•„๋‹™๋‹ˆ๋‹ค. **์ค„ ๊ฐ„๊ฒฉ(line spacing)** ์€ ํผ์„ผํŠธ ๊ฐ’์œผ๋กœ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. `src/hwpx/_document/layout.py`์˜ ๋ฌธ๋‹จ ์„œ์‹ ํ•จ์ˆ˜ ์ฃผ์„์ด ์ด๋ฅผ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค: + +> Millimetre inputs are converted to HWP units; paragraph spacing uses points; line spacing is stored as a percent value. + +์ฆ‰ ๋ฌธ๋‹จ ์œ„/์•„๋ž˜ ๊ฐ„๊ฒฉ(`spacing_before_pt`, `spacing_after_pt`)์€ pt โ†’ HWPUNIT์œผ๋กœ ๋ณ€ํ™˜๋˜์ง€๋งŒ, `line_spacing_percent`๋Š” ``๋กœ ํผ์„ผํŠธ ๊ทธ๋Œ€๋กœ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค. ๋‹จ์œ„๋ฅผ ํ•˜๋‚˜๋กœ ๋ญ‰๋šฑ๊ทธ๋ฆฌ๋ฉด ํ‹€๋ฆฌ๋Š” ์ง€์ ์ž…๋‹ˆ๋‹ค. + +## ๊ณต๊ฐœ API๊ฐ€ ์‚ฌ๋žŒ ๋‹จ์œ„๋ฅผ ์“ฐ๋Š” ์ด์œ  + +HWPUNIT์€ XML ์ €์žฅ ํ˜•์‹์œผ๋กœ๋Š” ์ •ํ™•ํ•˜์ง€๋งŒ, ์‚ฌ๋žŒ์ด ์ง์ ‘ ๋‹ค๋ฃจ๊ธฐ์—” ๋ถˆํŽธํ•ฉ๋‹ˆ๋‹ค("10 pt ๊ธ€์ž"๋ฅผ ๋งค๋ฒˆ 1000์œผ๋กœ ํ™˜์‚ฐํ•ด์•ผ ํ•จ). ๊ทธ๋ž˜์„œ ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๊ณ ์ˆ˜์ค€ API๋Š” **์‚ฌ๋žŒ ๋‹จ์œ„(pt/mm/%)๋ฅผ ์ž…๋ ฅ๋ฐ›์•„ ๋‚ด๋ถ€์—์„œ HWPUNIT์œผ๋กœ ๋ณ€ํ™˜**ํ•ฉ๋‹ˆ๋‹ค. + +- ๋ฌธ๋‹จ ์„œ์‹: `indent_left_mm`, `first_line_indent_mm`, `spacing_before_pt`, `line_spacing_percent` (`src/hwpx/_document/layout.py`) +- ์ด๋ฏธ์ง€: `width_mm`, `height_mm` (`src/hwpx/_document/media.py`) +- ํŽ˜์ด์ง€ยทํ‘œ: `pageWidthMm`, `heightMm` ๋“ฑ (`src/hwpx/agent/commands.py`) + +๋ณ€ํ™˜์€ ํ•ญ์ƒ `round()`๋กœ ์ •์ˆ˜ํ™”๋ฉ๋‹ˆ๋‹ค(HWPUNIT์€ ์ •์ˆ˜). ๋ฐ˜๋Œ€๋กœ ๋ฌธ์„œ๋ฅผ ๋ถ„์„ํ•ด ์‚ฌ๋žŒ์—๊ฒŒ ๋ณด์—ฌ์ค„ ๋•Œ๋Š” HWPUNIT โ†’ mm๋กœ ๋˜๋Œ๋ฆฝ๋‹ˆ๋‹ค โ€” `src/hwpx/tools/style_profile.py`, `src/hwpx/tools/layout_preview.py`๊ฐ€ `value / _HWP_UNITS_PER_MM`์œผ๋กœ ์—ญ๋ณ€ํ™˜ํ•ด ๋ฆฌํฌํŠธํ•ฉ๋‹ˆ๋‹ค. + +์ •๋ฆฌํ•˜๋ฉด: **์ €์žฅ ํ˜•์‹์€ HWPUNIT, ์‚ฌ์šฉ์ž ํ‘œ๋ฉด์€ pt/mm/%.** ์ €์ˆ˜์ค€ `hwpx.oxml` ๋ฐ์ดํ„ฐํด๋ž˜์Šค๋ฅผ ์ง์ ‘ ์กฐ์ž‘ํ•  ๋•Œ๋Š” HWPUNIT ์ •์ˆ˜๋ฅผ ์ง์ ‘ ๋‹ค๋ฃจ๊ฒŒ ๋˜๋ฏ€๋กœ, ์œ„ ํ™˜์‚ฐํ‘œ๋ฅผ ๊ณ์— ๋‘๊ณ  ์ž‘์—…ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.