From 2b51f8bf9069d6eb6f8a86977a4c650b8f3f7ba3 Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:11:33 +0900 Subject: [PATCH 1/2] fix(layout): read xs:boolean true in column and footnote settings The page estimate reads hp:colPr@sameSz and the footnotes' hp:placement@beneathText as xs:boolean, true as well as 1. Before, columns of the same size written sameSz=true were taken for columns of unequal width without their sizes, and the document was not estimated. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 ++ src/hwpx/layout/pages.py | 10 ++++++++-- tests/test_layout_page_estimate.py | 21 +++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70a51509..a555ef7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,8 @@ - 탭 정의에 자기 탭 위치나 자동 탭이 있는 문단은 전처럼 잰다. - 쪽 수 추정에서 강제 줄 바꿈 다음 줄은 첫 줄이 아니다. 들여쓰기를 빼지 않고 다른 줄의 폭으로 잰다. 전에는 첫 줄 폭으로 재서, 들여쓴 문단에서 줄이 하나 더 생길 수 있었다. +- 쪽 수 추정이 `hp:colPr@sameSz`와 각주 `hp:placement@beneathText`의 `true`를 `1`과 같이 읽는다(xs:boolean). 전에는 + `sameSz="true"`인 같은 폭 다단을 폭이 다른 다단으로 보고 추정하지 않았다. - `.hwp`를 열 때 그림의 투명도와 반전을 한/글이 읽는 자리에서 읽는다. 한/글은 그림 레코드의 투명도를 그림 원래 크기 뒤 마지막 바이트에서 읽고, 바이너리 항목 번호 뒤 바이트는 읽지 않는다(저장할 때 0으로 쓴다). 전에는 그 바이트를 투명도로 옮겨, 거기에 255가 든 문서의 그림이 HWPX에서 보이지 않았다. 반전(`hp:pic@reverse`)은 그림 개체 속성의 diff --git a/src/hwpx/layout/pages.py b/src/hwpx/layout/pages.py index 63844fe3..5f39d0fd 100644 --- a/src/hwpx/layout/pages.py +++ b/src/hwpx/layout/pages.py @@ -1427,13 +1427,19 @@ def _first_columns(section: Any) -> Any: return next((cols for cols in section.iter(f"{HP}colPr") if not _in_sub_list(cols)), None) +def _true(value: Any) -> bool: + """Whether an ``xs:boolean`` attribute *value* is true: ``1`` or ``true`` (Hancom writes ``1``).""" + + return value in ("1", "true") + + def _column_layout(settings: Any, text_width: int) -> tuple[int, int, tuple[int, ...], int]: """:func:`_columns` of the column settings *settings* (``hp:colPr``; ``None``: one column).""" count = int(settings.get("colCount", "1")) if settings is not None else 1 if count <= 1: return 1, text_width, (), 0 - if settings.get("sameSz") != "1": # each column takes its share of 32768 of the text width (hp:colSz), + if not _true(settings.get("sameSz")): # each column takes its share of 32768 of the text width (hp:colSz), sizes = settings.findall(f"{HP}colSz") # rounded, whatever the shares add up to total = sum(int(size.get("width", 0)) + int(size.get("gap", 0)) for size in sizes) if len(sizes) != count or total <= 0: @@ -1502,7 +1508,7 @@ def _note_shape(section: Any) -> _NoteShape: if settings is None: return _NoteShape() placement = settings.find(f"{HP}placement") - if placement is not None and placement.get("beneathText") == "1": + if placement is not None and _true(placement.get("beneathText")): raise _Unsupported("footnotes beneath the text") spacing = settings.find(f"{HP}noteSpacing") rule = settings.find(f"{HP}noteLine") diff --git a/tests/test_layout_page_estimate.py b/tests/test_layout_page_estimate.py index f3ac879d..ef7bae5d 100644 --- a/tests/test_layout_page_estimate.py +++ b/tests/test_layout_page_estimate.py @@ -1561,6 +1561,27 @@ def test_the_multi_column_sample_breaks_its_lines_at_each_column_width_without_c _assert_like_hancom(estimate_pages(_without_caches(data)), data, 2) +def test_columns_of_the_same_size_written_as_true_are_of_the_same_size() -> None: + # hp:colPr@sameSz is an xs:boolean: "true" is "1" (Hancom writes "1"; other writers "true"). + document = HwpxDocument.new() + document.page.set_columns(2) + for number in range(80): + document.add_paragraph(f"{number} 단 나누기 문단입니다") + data = document.to_bytes() + out = io.BytesIO() + with zipfile.ZipFile(io.BytesIO(data)) as source, zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as target: + for info in source.infolist(): + payload = source.read(info.filename) + if info.filename.startswith("Contents/section"): + payload = re.sub(rb'(colCount="2"[^>]*)sameSz="1"', rb'\1sameSz="true"', payload) + target.writestr(info, payload) + assert b'sameSz="true"' in zipfile.ZipFile(out).read("Contents/section0.xml") + + ones, trues = estimate_pages(data), estimate_pages(out.getvalue()) + + assert (trues.unsupported, trues.pages, trues.lines) == ((), ones.pages, ones.lines) + + def test_a_footnote_in_a_paragraph_without_a_cache_in_columns_of_unequal_width_is_not_followed() -> None: document = HwpxDocument.new() document.set_columns(2, same_size=False, column_widths=[(10632, 873), (21263, 0)]) From 42199bbdb905e73481b0b0b1984da05361ba606f Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Fri, 16 Oct 2026 22:37:25 +0900 Subject: [PATCH 2/2] Keep independent review additions at distinct changelog and test locations --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f89d0367..46fdb47d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,11 +107,11 @@ 않으면 탭은 폭이 없다. 내어쓰기 자동 탭은 둘째 줄이 시작하는 곳에 왼쪽 위치를 둔다. - 쪽 수 추정에서 강제 줄 바꿈 다음 줄은 첫 줄이 아니다. 들여쓰기를 빼지 않고 다른 줄의 폭으로 잰다. 전에는 첫 줄 폭으로 재서, 들여쓴 문단에서 줄이 하나 더 생길 수 있었다. -- 쪽 수 추정이 `hp:colPr@sameSz`와 각주 `hp:placement@beneathText`의 `true`를 `1`과 같이 읽는다(xs:boolean). 전에는 - `sameSz="true"`인 같은 폭 다단을 폭이 다른 다단으로 보고 추정하지 않았다. - FormFit과 쪽 수 추정이 텍스트 내보내기가 쓰지 않는 번호 형식(`KOREAN_DIGIT`, `HANGUL` 등)의 문단 번호를 만나도 멈추지 않는다. 한/글이 재는 번호(`useInstWidth`)이면 그 숫자를 한글 한 글자 폭으로 잰다. 전에는 번호가 자기 글자 모양을 가진 문단에서 `KeyError`가 났다. +- 쪽 수 추정이 `hp:colPr@sameSz`와 각주 `hp:placement@beneathText`의 `true`를 `1`과 같이 읽는다(xs:boolean). 전에는 + `sameSz="true"`인 같은 폭 다단을 폭이 다른 다단으로 보고 추정하지 않았다. - `.hwp`를 열 때 그림의 투명도와 반전을 한/글이 읽는 자리에서 읽는다. 한/글은 그림 레코드의 투명도를 그림 원래 크기 뒤 마지막 바이트에서 읽고, 바이너리 항목 번호 뒤 바이트는 읽지 않는다(저장할 때 0으로 쓴다). 전에는 그 바이트를 투명도로 옮겨, 거기에 255가 든 문서의 그림이 HWPX에서 보이지 않았다. 반전(`hp:pic@reverse`)은 그림 개체 속성의