diff --git a/CHANGELOG.md b/CHANGELOG.md index 06ce309a..c10d2d22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ ### 고침 +- 쪽 수 추정(실험, `estimate_pages`)이 곁에 자리를 남기는 종이·쪽 기준 어울림(SQUARE) 개체의 띠에 닿는 줄을, 그 줄이 + 줄 캐시를 가질 때 따른다. 한/글은 그런 줄을 같은 높이·간격으로 개체 곁에 좁혀 둔다(양옆에 자리가 있으면 한 높이에 + 두 조각). 캐시 없는 줄·표·개체가 닿으면 전처럼 지원 밖이다. - 쪽 수 추정(실험, `estimate_pages`)이 같은 폭의 여러 단 쪽에서 종이 기준으로 둔 위아래 배치 개체가 본문 너비를 다 덮지 않으면 지원 밖(`objects placed on the paper`)으로 두던 것을 고친다. 한/글은 개체가 가로로 걸치는 단에서만 줄을 개체 아래로 보내고, 걸치지 않는 단의 줄은 개체가 없을 때 자리에 둔다. 이제 종이의 왼쪽·가운데· diff --git a/src/hwpx/layout/pages.py b/src/hwpx/layout/pages.py index 34c6d993..9b7ea7db 100644 --- a/src/hwpx/layout/pages.py +++ b/src/hwpx/layout/pages.py @@ -40,7 +40,8 @@ reaches over from the paper's left, centre or right, the others running past it), 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 @@ -930,6 +931,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 @@ -2005,8 +2008,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 @@ -2081,7 +2085,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) @@ -2108,7 +2113,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: @@ -2422,7 +2428,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) @@ -2442,6 +2449,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 00000000..88cc09a6 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_square_picture_on_paper_lines_beside_it.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_square_picture_on_paper_lines_beside_it_on_page_2.hwpx b/tests/fixtures/hancom_saved/pages_square_picture_on_paper_lines_beside_it_on_page_2.hwpx new file mode 100644 index 00000000..eeda0a0b Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_square_picture_on_paper_lines_beside_it_on_page_2.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_square_picture_on_paper_lines_on_both_sides.hwpx b/tests/fixtures/hancom_saved/pages_square_picture_on_paper_lines_on_both_sides.hwpx new file mode 100644 index 00000000..7e27e4c9 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_square_picture_on_paper_lines_on_both_sides.hwpx differ diff --git a/tests/test_layout_page_estimate.py b/tests/test_layout_page_estimate.py index f2e8f558..e9883315 100644 --- a/tests/test_layout_page_estimate.py +++ b/tests/test_layout_page_estimate.py @@ -461,12 +461,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: