From e8290caf23a06ac8d9e40764b5eb9e69bfb792aa Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:28:23 +0900 Subject: [PATCH 1/2] fix(layout): read a column gap as a signed 32-bit number The page estimate reads hp:colPr@sameGap as a signed 32-bit number: a value of 2**31 or more is a negative gap, and Hancom Office widens the columns by it so that they overlap. Before, such a gap was read as a huge one, leaving the columns next to no width. The columns_gap_wrapped fixture is re-saved by Hancom Office 2024. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 ++ src/hwpx/layout/pages.py | 2 ++ .../hancom_saved/columns_gap_wrapped.hwpx | Bin 8317 -> 22614 bytes tests/test_layout_page_estimate.py | 3 +++ 4 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70a51509..8b0f5157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,8 @@ - 탭 정의에 자기 탭 위치나 자동 탭이 있는 문단은 전처럼 잰다. - 쪽 수 추정에서 강제 줄 바꿈 다음 줄은 첫 줄이 아니다. 들여쓰기를 빼지 않고 다른 줄의 폭으로 잰다. 전에는 첫 줄 폭으로 재서, 들여쓴 문단에서 줄이 하나 더 생길 수 있었다. +- 쪽 수 추정이 단 간격(`hp:colPr@sameGap`)을 부호 있는 32비트 수로 읽는다. 2³¹ 이상으로 쓰인 값은 음수 간격이고, 한/글은 + 단이 겹치도록 단 폭을 그만큼 넓힌다. 전에는 아주 큰 간격으로 읽어 단 폭이 거의 없었다. - `.hwp`를 열 때 그림의 투명도와 반전을 한/글이 읽는 자리에서 읽는다. 한/글은 그림 레코드의 투명도를 그림 원래 크기 뒤 마지막 바이트에서 읽고, 바이너리 항목 번호 뒤 바이트는 읽지 않는다(저장할 때 0으로 쓴다). 전에는 그 바이트를 투명도로 옮겨, 거기에 255가 든 문서의 그림이 HWPX에서 보이지 않았다. 반전(`hp:pic@reverse`)은 그림 개체 속성의 diff --git a/src/hwpx/layout/pages.py b/src/hwpx/layout/pages.py index 63844fe3..5f69d08e 100644 --- a/src/hwpx/layout/pages.py +++ b/src/hwpx/layout/pages.py @@ -1442,6 +1442,8 @@ def _column_layout(settings: Any, text_width: int) -> tuple[int, int, tuple[int, for size in sizes) return count, min(widths), widths, 0 gap = int(settings.get("sameGap", 0)) + if gap >= 1 << 31: # a signed 32-bit number written unsigned: Hancom takes a negative gap so + gap -= 1 << 32 return count, (text_width - (count - 1) * gap) // count // 4 * 4, (), gap diff --git a/tests/fixtures/hancom_saved/columns_gap_wrapped.hwpx b/tests/fixtures/hancom_saved/columns_gap_wrapped.hwpx index c54b86c642160af798d336cd5f398e947aea23d6..b5965ffac96f05c18b8939a315ae0fb7d871ab52 100644 GIT binary patch delta 15329 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 diff --git a/tests/test_layout_page_estimate.py b/tests/test_layout_page_estimate.py index f3ac879d..b50e8c32 100644 --- a/tests/test_layout_page_estimate.py +++ b/tests/test_layout_page_estimate.py @@ -23,6 +23,9 @@ HH = "{http://www.hancom.co.kr/hwpml/2011/head}" FIXTURES = Path(__file__).parent / "fixtures" / "hancom_saved" HANCOM_PAGES = { + # Two columns whose gap is written 2**32 - 2**15: a signed 32-bit number, -32768, so the columns overlap and + # each is (42520 + 32768) / 2 wide, as Hancom lays them out. + "columns_gap_wrapped": 1, # A picture (or a rectangle) placed top and bottom alone in an empty paragraph whose spacing after is 1000, # the next paragraph's spacing before 600, the empty line 1600 tall with its spacing: unless the object # moves with the text, may overlap and starts above the line's foot, the empty line stays at the paragraph's From 52c2ad0d24c6bdb2d14bb80bb5dfed93acc1285b Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Fri, 16 Oct 2026 22:37:28 +0900 Subject: [PATCH 2/2] Keep independent review additions at distinct changelog and test locations --- CHANGELOG.md | 4 ++-- tests/test_layout_page_estimate.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 657f1089..36575785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,8 +107,6 @@ 않으면 탭은 폭이 없다. 내어쓰기 자동 탭은 둘째 줄이 시작하는 곳에 왼쪽 위치를 둔다. - 쪽 수 추정에서 강제 줄 바꿈 다음 줄은 첫 줄이 아니다. 들여쓰기를 빼지 않고 다른 줄의 폭으로 잰다. 전에는 첫 줄 폭으로 재서, 들여쓴 문단에서 줄이 하나 더 생길 수 있었다. -- 쪽 수 추정이 단 간격(`hp:colPr@sameGap`)을 부호 있는 32비트 수로 읽는다. 2³¹ 이상으로 쓰인 값은 음수 간격이고, 한/글은 - 단이 겹치도록 단 폭을 그만큼 넓힌다. 전에는 아주 큰 간격으로 읽어 단 폭이 거의 없었다. - FormFit과 쪽 수 추정이 텍스트 내보내기가 쓰지 않는 번호 형식(`KOREAN_DIGIT`, `HANGUL` 등)의 문단 번호를 만나도 멈추지 않는다. 한/글이 재는 번호(`useInstWidth`)이면 그 숫자를 한글 한 글자 폭으로 잰다. 전에는 번호가 자기 글자 모양을 가진 문단에서 `KeyError`가 났다. @@ -146,6 +144,8 @@ 다음 줄이 쪽 끝을 넘으면 다음 쪽 맨 위에서 시작한다(개체 아래 간격은 그 쪽으로 가져가지 않는다). 개체의 앵커 줄이 다음 쪽으로 넘어가면 개체도 그 줄과 함께 넘어가, 그 줄 위에서 오프셋만큼 아래에 선다. 전에는 앞의 경우를 지원하지 않았고, 뒤의 경우 개체를 다음 쪽 맨 위에, 앵커 줄을 그 아래에 놓았다. +- 쪽 수 추정이 단 간격(`hp:colPr@sameGap`)을 부호 있는 32비트 수로 읽는다. 2³¹ 이상으로 쓰인 값은 음수 간격이고, 한/글은 + 단이 겹치도록 단 폭을 그만큼 넓힌다. 전에는 아주 큰 간격으로 읽어 단 폭이 거의 없었다. - 쪽 수 추정이 빈 문단에 홀로 든 위아래 배치 개체(그림·도형) 뒤의 다음 문단을 한/글처럼 놓는다. 개체가 글과 함께 움직이고 겹침을 허용하며 빈 줄의 글자 높이 안에서 시작할 때만 빈 줄이 개체 아래로 내려간다. 그 밖에는 빈 줄이 문단 위에 그대로 있고, 다음 문단은 개체의 아래끝과 빈 줄에 두 문단 간격(앞 문단 아래·다음 diff --git a/tests/test_layout_page_estimate.py b/tests/test_layout_page_estimate.py index 42a7fa7a..d623bc26 100644 --- a/tests/test_layout_page_estimate.py +++ b/tests/test_layout_page_estimate.py @@ -23,9 +23,6 @@ HH = "{http://www.hancom.co.kr/hwpml/2011/head}" FIXTURES = Path(__file__).parent / "fixtures" / "hancom_saved" HANCOM_PAGES = { - # Two columns whose gap is written 2**32 - 2**15: a signed 32-bit number, -32768, so the columns overlap and - # each is (42520 + 32768) / 2 wide, as Hancom lays them out. - "columns_gap_wrapped": 1, # A picture (or a rectangle) placed top and bottom alone in an empty paragraph whose spacing after is 1000, # the next paragraph's spacing before 600, the empty line 1600 tall with its spacing: unless the object # moves with the text, may overlap and starts above the line's foot, the empty line stays at the paragraph's @@ -243,6 +240,9 @@ "pages_line_ends_1_above_page_foot": 2, # and one ending 1 above it "pages_line_ends_100_above_page_foot": 2, # and 100 above it: no room is kept below lines "pages_keep_keep_with_next": 3, # keep with next + # Two columns whose gap is written 2**32 - 2**15: a signed 32-bit number, -32768, so the columns overlap and + # each is (42520 + 32768) / 2 wide, as Hancom lays them out. + "columns_gap_wrapped": 1, "pages_columns_2_break": 4, # two columns and a column break # Columns changing inside the section (10 pt lines spaced 160%): three lines in one column, a paragraph # starting two columns (gap 1200) and the lines after it, then one starting one column and three lines.