From f63cc36f625788fcd04faa38f6bb058ebf4abd7b Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:29:28 +0900 Subject: [PATCH 1/2] fix(oxml): give a new section Hancom's namespace prefixes in memory add_section() built the new section's root without a namespace map, so in memory its elements took prefixes like ns0:sec and ns4:p (a saved file was set right), and structure edits of a table added there (split_cell and the like) stopped with a TableStructureError. The new section now takes the prefixes Hancom writes (hs, hp, hc, ...). The error's wording, which said the reverse of the check, is fixed too. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +++ src/hwpx/oxml/document_parts.py | 8 ++++++-- src/hwpx/oxml/table_structure.py | 2 +- tests/test_paragraph_section_management.py | 11 +++++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70a51509..167d5917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,9 @@ - 탭 정의에 자기 탭 위치나 자동 탭이 있는 문단은 전처럼 잰다. - 쪽 수 추정에서 강제 줄 바꿈 다음 줄은 첫 줄이 아니다. 들여쓰기를 빼지 않고 다른 줄의 폭으로 잰다. 전에는 첫 줄 폭으로 재서, 들여쓴 문단에서 줄이 하나 더 생길 수 있었다. +- `add_section()`이 만든 구역은 메모리에서도 한/글의 이름공간 접두사(`hs:sec`, `hp:p` 등)를 쓴다. 전에는 `ns0:sec`, + `ns4:p`처럼 붙어(저장할 때는 바로잡혔다), 그 구역에 넣은 표의 셀 나누기 같은 구조 편집이 `TableStructureError`로 멈췄다. + 그 오류 문구도 바로잡았다. - `.hwp`를 열 때 그림의 투명도와 반전을 한/글이 읽는 자리에서 읽는다. 한/글은 그림 레코드의 투명도를 그림 원래 크기 뒤 마지막 바이트에서 읽고, 바이너리 항목 번호 뒤 바이트는 읽지 않는다(저장할 때 0으로 쓴다). 전에는 그 바이트를 투명도로 옮겨, 거기에 255가 든 문서의 그림이 HWPX에서 보이지 않았다. 반전(`hp:pic@reverse`)은 그림 개체 속성의 diff --git a/src/hwpx/oxml/document_parts.py b/src/hwpx/oxml/document_parts.py index fb447147..5940db01 100644 --- a/src/hwpx/oxml/document_parts.py +++ b/src/hwpx/oxml/document_parts.py @@ -6,7 +6,7 @@ from copy import deepcopy from dataclasses import dataclass import logging -from typing import TYPE_CHECKING, Any, Callable, Iterable, Mapping, Sequence, TypeVar +from typing import TYPE_CHECKING, Any, Callable, Iterable, Mapping, Sequence, TypeVar, cast import xml.etree.ElementTree as ET from hwpx.opc.relationships import resolve_part_name @@ -1293,7 +1293,11 @@ def add_section(self, *, after: int | None = None) -> HwpxOxmlSection: # Build a renderable empty section. ``secPr`` and ``colPr`` must # precede body text in the first paragraph's first run. - section_element = section_properties.makeelement(f"{_HS}sec", {}) + # with the namespace prefixes Hancom writes (hs, hp, hc, ...): structure edits read the hp prefix + from .namespaces import HWPML_COMPAT_ROOT_NAMESPACES + + section_element = cast(Any, section_properties).makeelement( + f"{_HS}sec", {}, nsmap=HWPML_COMPAT_ROOT_NAMESPACES) para_attrs = {"id": _paragraph_id(), **_DEFAULT_PARAGRAPH_ATTRS} para = _append_child(section_element, f"{_HP}p", para_attrs) run = _append_child(para, f"{_HP}run", {"charPrIDRef": "0"}) diff --git a/src/hwpx/oxml/table_structure.py b/src/hwpx/oxml/table_structure.py index 899486cc..b8f9bee4 100644 --- a/src/hwpx/oxml/table_structure.py +++ b/src/hwpx/oxml/table_structure.py @@ -117,7 +117,7 @@ def _restructure(self, op: dict[str, Any]) -> None: element = self.element if element.prefix != "hp": - raise TableStructureError("structure edits read a table whose paragraph namespace is prefixed hp") + raise TableStructureError("structure edits need a table whose paragraph namespace is prefixed hp") section = self.paragraph.section sections = section.document.sections if section.document is not None else [section] used = { diff --git a/tests/test_paragraph_section_management.py b/tests/test_paragraph_section_management.py index 55df6b95..96340fb2 100644 --- a/tests/test_paragraph_section_management.py +++ b/tests/test_paragraph_section_management.py @@ -183,6 +183,17 @@ def test_add_section_appends(self): "t", ] + def test_add_section_writes_the_documents_namespace_prefixes(self): + # The new section's XML takes the prefixes of the document's sections (hs:sec, hp:p, ...), so a + # table added there can take structure edits like any other. + doc = _new() + new_section = doc.add_section() + table = doc.add_table(3, 3, section=new_section) + + assert (new_section.element.prefix, table.element.prefix) == ("hs", "hp") + table.split_cell(2, 2, rows=1, cols=2) + assert table.column_count >= 3 + def test_add_section_after(self, tmp_path): doc = _new() second = doc.add_section() From 36ecb8f5102df97a65140ac8225867d1c2596208 Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Fri, 16 Oct 2026 22:37:27 +0900 Subject: [PATCH 2/2] Keep independent review additions at distinct changelog and test locations --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5c8298d..f6a008e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,9 +107,6 @@ 않으면 탭은 폭이 없다. 내어쓰기 자동 탭은 둘째 줄이 시작하는 곳에 왼쪽 위치를 둔다. - 쪽 수 추정에서 강제 줄 바꿈 다음 줄은 첫 줄이 아니다. 들여쓰기를 빼지 않고 다른 줄의 폭으로 잰다. 전에는 첫 줄 폭으로 재서, 들여쓴 문단에서 줄이 하나 더 생길 수 있었다. -- `add_section()`이 만든 구역은 메모리에서도 한/글의 이름공간 접두사(`hs:sec`, `hp:p` 등)를 쓴다. 전에는 `ns0:sec`, - `ns4:p`처럼 붙어(저장할 때는 바로잡혔다), 그 구역에 넣은 표의 셀 나누기 같은 구조 편집이 `TableStructureError`로 멈췄다. - 그 오류 문구도 바로잡았다. - FormFit과 쪽 수 추정이 텍스트 내보내기가 쓰지 않는 번호 형식(`KOREAN_DIGIT`, `HANGUL` 등)의 문단 번호를 만나도 멈추지 않는다. 한/글이 재는 번호(`useInstWidth`)이면 그 숫자를 한글 한 글자 폭으로 잰다. 전에는 번호가 자기 글자 모양을 가진 문단에서 `KeyError`가 났다. @@ -138,6 +135,9 @@ 픽셀(96dpi, 75 HWPUNIT) 단위로 올려 세고(반경 400은 450), 뻗는 길이는 HWPUNIT 아래를 버린다. 반사는 대략 그 몫만큼의 그림(빛남 포함) 높이와 거리만큼 아래로 자리를 차지한다(한/글과 한 픽셀 안팎 차이). 부드러운 가장자리는 자리를 차지하지 않는다. +- `add_section()`이 만든 구역은 메모리에서도 한/글의 이름공간 접두사(`hs:sec`, `hp:p` 등)를 쓴다. 전에는 `ns0:sec`, + `ns4:p`처럼 붙어(저장할 때는 바로잡혔다), 그 구역에 넣은 표의 셀 나누기 같은 구조 편집이 `TableStructureError`로 멈췄다. + 그 오류 문구도 바로잡았다. - 쪽 수 추정이 문단 맨 위에 둔 위아래 배치 표 아래로 문단 첫 줄이 그 쪽에 들어가지 않을 때 한/글처럼 놓는다. 그 쪽에서 끝나는 표는 첫 줄과 함께 다음 쪽 맨 위로 간다. 행 단위로 나누는 표는 그 쪽에 다 들어갈 때, 셀 단위로 나누는 표는 마지막 행의 남는 높이를 잘라 그 쪽에서 끝날 때도 그렇다. 나누지 않는 표가 그 자리에 들어가지 않으면