From 0309194bd3ed6a2944bb684abb955a499275de2b Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Thu, 1 Oct 2026 02:11:54 +0900 Subject: [PATCH] feat(layout): follow cached lines beside a square object on the paper A line reaching the band of a square-wrapped object placed on the paper with room beside it is set beside it, narrower, at the same height and spacing (two pieces at one height with room on both sides), in the paragraph before its anchor too. A line keeping its layout cache is therefore followed where it is; a line without one, a table or an object reaching the band is still not followed. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 ++ src/hwpx/layout/pages.py | 20 ++++++++---- ...uare_picture_on_paper_lines_beside_it.hwpx | Bin 0 -> 8708 bytes ...re_on_paper_lines_beside_it_on_page_2.hwpx | Bin 0 -> 9908 bytes ..._picture_on_paper_lines_on_both_sides.hwpx | Bin 0 -> 8764 bytes tests/test_layout_page_estimate.py | 30 +++++++++++++++--- 6 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 tests/fixtures/hancom_saved/pages_square_picture_on_paper_lines_beside_it.hwpx create mode 100644 tests/fixtures/hancom_saved/pages_square_picture_on_paper_lines_beside_it_on_page_2.hwpx create mode 100644 tests/fixtures/hancom_saved/pages_square_picture_on_paper_lines_on_both_sides.hwpx diff --git a/CHANGELOG.md b/CHANGELOG.md index b2443654..a0fdd923 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ ### 고침 +- 쪽 수 추정(실험, `estimate_pages`)이 곁에 자리를 남기는 종이·쪽 기준 어울림(SQUARE) 개체의 띠에 닿는 줄을, 그 줄이 + 줄 캐시를 가질 때 따른다. 한/글은 그런 줄을 같은 높이·간격으로 개체 곁에 좁혀 둔다(양옆에 자리가 있으면 한 높이에 + 두 조각). 캐시 없는 줄·표·개체가 닿으면 전처럼 지원 밖이다. - 쪽 수 추정(실험, `estimate_pages`)이 띠가 본문 아래 끝을 넘는 어울림(SQUARE) 개체를, 그 문단과 곁 문단이 줄 캐시를 가질 때 받는다. - 한/글은 이런 개체만 다음 쪽 맨 위로 보낸다. 개체는 줄 높이를 차지하지 않으므로 캐시를 가진 줄은 캐시 자리에 diff --git a/src/hwpx/layout/pages.py b/src/hwpx/layout/pages.py index b0d5ffbf..50325934 100644 --- a/src/hwpx/layout/pages.py +++ b/src/hwpx/layout/pages.py @@ -39,7 +39,8 @@ foot -- and the lines after follow; with several columns, in each of them when it covers the text's whole width), and so does one wrapped square there that leaves less than a line's room (1440) on either side of it across the text; one that leaves more (in one column) changes nothing - when no line, table or object of its page reaches its band, and is not followed otherwise. An + when no line, table or object of its page reaches its band, and a line keeping its layout cache + that reaches it stays where it is (beside it); anything else reaching it is not followed. An object anchored at the top of a paragraph's first line that such a band pushes down stands right below the band, the line below the object; a table flowing with the text alone in its paragraph stands there too, its paragraph's line below it. A table or picture set as a character is one @@ -891,6 +892,8 @@ class _Para: reflow: _Reflow | None = None #: holding nothing, in a section hiding a page's first empty lines (see :meth:`_Paginator._paragraph`) hides: bool = False + #: its lines are Hancom's own, from the paragraph's valid layout cache + kept: bool = False def height(self, line: int) -> int: return self.cached[line][0] if self.cached else self.size @@ -1917,8 +1920,9 @@ def __init__(self, body: int, columns: int, notes: _NoteShape, self.widths = widths # each column's width when they differ #: frame -> (top, bottom) of the objects placed on the paper there: no line in them self.bands = bands or {} - #: frame -> (top, bottom) of the ones leaving a line's room beside them, and whether a line, a table - #: or an object reaches one of them (set beside it by Hancom, which is not followed) + #: frame -> (top, bottom) of the ones leaving a line's room beside them, and whether a line without + #: a layout cache, a table or an object reaches one of them (set beside it by Hancom, which is not + #: followed: a line keeping its cache stays where it is) self.sides = sides or {} self.beside = False self.columns = columns @@ -1993,7 +1997,8 @@ def _flow(self, para: _Para, table: _FlowTable, start: int) -> None: self._flow_below_band(para, table, self._below_bands(start)) return self.out.append((self.frame, start)) # the anchor paragraph's line, under the table's top - self._reach(self.frame, start, start + para.height(0)) + if not para.kept: + self._reach(self.frame, start, start + para.height(0)) before = self.frame top = start - para.prev + table.offset + table.above # from the paragraph's top, above its spacing frame, end = _flow_table(table, self.frame, top, self.body) @@ -2020,7 +2025,8 @@ def _flow_below_band(self, para: _Para, table: _FlowTable, top: int) -> None: self.page_notes = [0, 0] if frame != self.frame else self.page_notes self.frame, self.table_end = frame, max(self.table_end, frame) self.out.append((self.frame, line)) - self._reach(self.frame, line, line + para.height(0)) + if not para.kept: + self._reach(self.frame, line, line + para.height(0)) self.last_vp, self.last_pitch, self.pending_next = line, para.advance(0), para.next def _anchored(self, index: int, paras: list[_Para], para: _Para, start: int) -> None: @@ -2311,7 +2317,8 @@ def _place(self, para: _Para, first: int, count: int) -> None: note_height, notes, _ = para.notes.get(line, (0, 0, 0)) self.page_notes[0] += note_height self.page_notes[1] += notes - self._reach(frame, top, top + para.height(line)) + if not para.kept: + self._reach(frame, top, top + para.height(line)) @dataclass(frozen=True) @@ -2331,6 +2338,7 @@ def _lay_section(measure: _Measure, section: Any) -> _SectionLayout: for paragraph in section.findall(f"{HP}p"): para, wrap = _wrapped_paragraph(measure, page, paragraph, wrap) band = _paper_band(measure, page, paragraph) + para = replace(para, kept=bool(_cached_metrics(paragraph))) paras.append(para if band is None else replace(para, paper=band)) paper = {index: para.paper for index, para in enumerate(paras) if para.paper is not None} bands: dict[int, list[tuple[int, int]]] = {} diff --git a/tests/fixtures/hancom_saved/pages_square_picture_on_paper_lines_beside_it.hwpx b/tests/fixtures/hancom_saved/pages_square_picture_on_paper_lines_beside_it.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..88cc09a68911ba8ba8de60680f368c0c1fed2293 GIT binary patch literal 8708 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 N&TvA3@0aulQqoS#oFA( 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 literal 0 HcmV?d00001 diff --git a/tests/test_layout_page_estimate.py b/tests/test_layout_page_estimate.py index b8e34ae4..af6c5621 100644 --- a/tests/test_layout_page_estimate.py +++ b/tests/test_layout_page_estimate.py @@ -387,12 +387,34 @@ def test_a_header_wrapped_square_with_room_beside_it_is_not_followed() -> None: assert estimate_pages(out.getvalue()).unsupported == ("section 0: text beside an object placed on the paper",) -def test_a_line_reaching_a_picture_on_the_paper_with_room_beside_it_is_not_followed() -> None: - # The picture's top 100 above the fifth line's foot: Hancom sets that line beside it, narrower. +def test_a_line_reaching_a_picture_on_the_paper_with_room_beside_it_keeps_its_cached_place() -> None: + # The picture's top 100 above the fifth line's foot: Hancom sets that line beside it, narrower, at the + # same height. Keeping its cache, it is followed; without it, where it breaks is not. data = (FIXTURES / "pages_square_picture_on_paper_reaching_the_last_line.hwpx").read_bytes() - for source in (data, _without_caches(data)): - assert estimate_pages(source).unsupported == ("section 0: text beside an object placed on the paper",) + _assert_like_hancom(estimate_pages(data), data, 1) + assert estimate_pages(_without_caches(data)).unsupported == ( + "section 0: text beside an object placed on the paper",) + + +@pytest.mark.parametrize( + ("name", "pages"), + [ + ("pages_square_picture_on_paper_lines_beside_it", 2), # at the text's left, 800 below its top + ("pages_square_picture_on_paper_lines_on_both_sides", 2), # at its middle: two pieces a line + ("pages_square_picture_on_paper_lines_beside_it_on_page_2", 5), # anchored after a page break + ], +) +def test_lines_keeping_their_caches_beside_a_picture_on_the_paper_stay_where_hancom_put_them(name: str, + pages: int) -> None: + # A picture wrapped square from the paper's top with room beside it, long paragraphs flowing beside + # it (the line of the paragraph before its anchor too): each line keeps its height and spacing there, + # narrower. Without the caches, where the lines beside it break is not followed. + data = (FIXTURES / f"{name}.hwpx").read_bytes() + + _assert_like_hancom(estimate_pages(data), data, pages) + assert estimate_pages(_without_caches(data)).unsupported == ( + "section 0: text beside an object placed on the paper",) def _with_empty_runs_in_cells(data: bytes) -> bytes: