diff --git a/CHANGELOG.md b/CHANGELOG.md index 184c06cf..e8adfa55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ ### 고침 +- 쪽 수 추정(실험, `estimate_pages`)이 세로쓰기 칸(`hp:subList@textDirection="VERTICAL"`)의 글을 가로 글처럼 + 칸 폭에서 나누어 행을 키우던 것을 고친다. + - 한/글은 세로쓰기 칸의 줄을 칸 높이를 따라 세로로 놓고 옆으로 늘어놓는다. 넘치는 글은 잘리고, 행은 선언 + 높이 그대로다. + - 세로쓰기 칸이 있는 흐르는 표는 쪽 끝에서 나뉘지 않고, 나누지 않게 둔 표처럼 통째로 다음 쪽으로 간다. + 기준 문단 줄과 그 뒤 글은 앞 쪽에 남는다. - 쪽 수 추정(실험, `estimate_pages`)이 줄 캐시 없는 표 칸에서 글자처럼 둔 표 옆에 글 앞·뒤로 둔 개체가 있는 문단을 지원 밖(`a nested table`)으로 두던 것을 고친다. - 그런 개체는 자리를 차지하지 않으므로, 그 문단은 표 혼자인 문단처럼 표 높이의 한 줄이다. diff --git a/src/hwpx/layout/pages.py b/src/hwpx/layout/pages.py index 71cee0b0..b97763a6 100644 --- a/src/hwpx/layout/pages.py +++ b/src/hwpx/layout/pages.py @@ -918,7 +918,30 @@ def _down(pos: Any) -> int: return 0 if offset < 0 or offset >= 1 << 31 else offset +def _vertical(cell: Any) -> bool: + """Whether *cell* holds vertical text (``hp:subList@textDirection``): Hancom lays its lines down the + cell's height, side by side across it, cuts what does not fit and never makes its row taller.""" + + sub_list = cell.find(f"{HP}subList") + return sub_list is not None and sub_list.get("textDirection", "HORIZONTAL") != "HORIZONTAL" + + +def _page_break(table: Any) -> str: + """How a flowing table goes over a page end (``hp:tbl@pageBreak``): one holding a cell of vertical text + does not split, as if set not to (NONE): it moves to the next page whole.""" + + if any(_vertical(cell) for row in table.findall(f"{HP}tr") for cell in row.findall(f"{HP}tc")): + return "NONE" + return table.get("pageBreak", "CELL") + + def _cell_row(measure: _Measure, table: Any, cell: Any) -> _Row: + if _vertical(cell): # its lines go across it, the row as declared: one block, not split + margins = cell_margins_of(cell, table) + vertical = margins.top + margins.bottom + height = max(int(cell.find(f"{HP}cellSz").get("height", 0)), vertical) + return _Row(height, 1, height - vertical, height - vertical, vertical, cell.get("header") == "1", + first=height - vertical) paragraphs = cell.findall(f"{HP}subList/{HP}p") nested = _holds_table(cell) drawn: tuple[tuple[int, int], ...] = () @@ -1660,7 +1683,7 @@ def _anchor(measure: _Measure, paragraph: Any, runs: list[Any], text: str, obj: top, bottom = (0, 0) if margin is None else (_margin(margin, "top"), _margin(margin, "bottom")) if _local(obj) == "tbl": rows, cells = _table_rows(measure, obj) - table = _FlowTable(rows, obj.get("pageBreak", "CELL"), obj.get("repeatHeader") == "1", (top, bottom), + table = _FlowTable(rows, _page_break(obj), obj.get("repeatHeader") == "1", (top, bottom), tuple(cells), caption=_caption(measure, obj), cut=_spare_cut(obj)) return _Anchor(line, table, 0) return _Anchor(line, None, _drawn_height(obj, measure) + top + bottom) @@ -1688,7 +1711,7 @@ def _object_line( if name == "tbl": rows, cells = _table_rows(measure, obj) offset = int(pos.get("vertOffset", 0)) # one up (a negative offset, kept unsigned) starts at the line - table = _FlowTable(rows, obj.get("pageBreak", "CELL"), obj.get("repeatHeader") == "1", (top, bottom), + table = _FlowTable(rows, _page_break(obj), obj.get("repeatHeader") == "1", (top, bottom), tuple(cells), 0 if offset < 0 or offset >= 1 << 31 else offset, _caption(measure, obj), _spare_cut(obj)) return count, size, pitch, table @@ -1928,7 +1951,7 @@ def _wrapped_paragraph(measure: _Measure, page: _Page, paragraph: Any, rows, cells = _table_rows(measure, pusher) margin = pusher.find(f"{HP}outMargin") ends = (0, 0) if margin is None else (_margin(margin, "top"), _margin(margin, "bottom")) - table = _FlowTable(rows, pusher.get("pageBreak", "CELL"), pusher.get("repeatHeader") == "1", ends, + table = _FlowTable(rows, _page_break(pusher), pusher.get("repeatHeader") == "1", ends, tuple(cells), caption=_caption(measure, pusher), cut=_spare_cut(pusher)) return replace(para, band=_Band(para.wrap_anchor, offset, table)), None top = para.span(0, para.wrap_anchor) + offset @@ -2029,7 +2052,7 @@ def _stack(measure: _Measure, objects: list[Any], column: int, shape: _Shape) -> rows, cells = _table_rows(measure, obj) margin = obj.find(f"{HP}outMargin") ends = (0, 0) if margin is None else (_margin(margin, "top"), _margin(margin, "bottom")) - table = _FlowTable(rows, obj.get("pageBreak", "CELL"), obj.get("repeatHeader") == "1", ends, + table = _FlowTable(rows, _page_break(obj), obj.get("repeatHeader") == "1", ends, tuple(cells), caption=_caption(measure, obj), cut=_spare_cut(obj)) stacked.append(_Stacked(int(pos.get("vertOffset", 0)), left, left + width, height, table)) return tuple(stacked) diff --git a/tests/fixtures/hancom_saved/pages_table_merged_cells_over_the_page_end.hwpx b/tests/fixtures/hancom_saved/pages_table_merged_cells_over_the_page_end.hwpx new file mode 100644 index 00000000..748a18f0 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_table_merged_cells_over_the_page_end.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_table_vertical_cell_in_every_row_over_the_page_end.hwpx b/tests/fixtures/hancom_saved/pages_table_vertical_cell_in_every_row_over_the_page_end.hwpx new file mode 100644 index 00000000..27862bd6 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_table_vertical_cell_in_every_row_over_the_page_end.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_table_vertical_cell_merged_over_rows.hwpx b/tests/fixtures/hancom_saved/pages_table_vertical_cell_merged_over_rows.hwpx new file mode 100644 index 00000000..7df185aa Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_table_vertical_cell_merged_over_rows.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_table_vertical_cell_of_all_rows_over_the_page_end.hwpx b/tests/fixtures/hancom_saved/pages_table_vertical_cell_of_all_rows_over_the_page_end.hwpx new file mode 100644 index 00000000..00f87455 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_table_vertical_cell_of_all_rows_over_the_page_end.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_table_vertical_cell_short_text.hwpx b/tests/fixtures/hancom_saved/pages_table_vertical_cell_short_text.hwpx new file mode 100644 index 00000000..b80c16fe Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_table_vertical_cell_short_text.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_table_vertical_cell_text_past_its_height.hwpx b/tests/fixtures/hancom_saved/pages_table_vertical_cell_text_past_its_height.hwpx new file mode 100644 index 00000000..fe597b65 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_table_vertical_cell_text_past_its_height.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_table_vertical_cell_text_past_its_width.hwpx b/tests/fixtures/hancom_saved/pages_table_vertical_cell_text_past_its_width.hwpx new file mode 100644 index 00000000..326b4c6a Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_table_vertical_cell_text_past_its_width.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_table_vertical_cells_over_the_page_end.hwpx b/tests/fixtures/hancom_saved/pages_table_vertical_cells_over_the_page_end.hwpx new file mode 100644 index 00000000..16ca3fda Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_table_vertical_cells_over_the_page_end.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_table_vertical_cells_taller_than_a_page.hwpx b/tests/fixtures/hancom_saved/pages_table_vertical_cells_taller_than_a_page.hwpx new file mode 100644 index 00000000..a71a8022 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_table_vertical_cells_taller_than_a_page.hwpx differ diff --git a/tests/test_layout_page_estimate.py b/tests/test_layout_page_estimate.py index 0ae4a5b3..041bf43f 100644 --- a/tests/test_layout_page_estimate.py +++ b/tests/test_layout_page_estimate.py @@ -305,6 +305,14 @@ "pages_table_nested_in_table_as_character": 1, # a table in a cell of a table set as a character "pages_table_nested_row_split": 2, # a row holding a table splits after its first line of text "pages_table_nested_row_moved": 2, # none of a row holding a table fits: it goes on whole + # A flowing table whose first column holds vertical text (10 pt, spaced 160%): Hancom lays its lines down + # the cell's height, side by side across it, and cuts what does not fit; the row stays as declared: + "pages_table_vertical_cell_short_text": 1, # three characters in a cell 3000 x 4000: one line + "pages_table_vertical_cell_text_past_its_height": 1, # thirty: ten lines, the row still 4000 tall + "pages_table_vertical_cell_text_past_its_width": 1, # sixty in a cell 1500 wide: twenty lines + "pages_table_vertical_cell_merged_over_rows": 1, # ten in a cell merged over four rows 2000 tall + "pages_table_merged_cells_over_the_page_end": 2, # twenty rows, cells of four merged, horizontal text: + # the table splits at the page end "pages_table_nested_top_and_bottom_alone_then_a_line": 1, # a table placed top and bottom alone in # a cell paragraph, then a line: right # below the table @@ -888,6 +896,27 @@ def test_a_cell_holding_a_table_among_text_or_top_and_bottom_is_as_tall_as_hanco assert estimate_pages(_without_caches(data)).unsupported == ("section 0: a nested table",) +@pytest.mark.parametrize("fixture", ["pages_table_vertical_cells_over_the_page_end", + "pages_table_vertical_cell_of_all_rows_over_the_page_end", + "pages_table_vertical_cell_in_every_row_over_the_page_end", + "pages_table_vertical_cells_taller_than_a_page"]) +def test_a_table_holding_vertical_text_moves_to_the_next_page_whole(fixture: str) -> None: + # pages_table_merged_cells_over_the_page_end with vertical text in its first column (cells of four rows + # merged, one of all twenty, or one in every row), and one of forty rows taller than a page: Hancom does + # not split it but moves it whole to the next page's top (running past the page's foot when taller), the + # anchor's line and the paragraphs after it staying on the first page. + data = (FIXTURES / f"{fixture}.hwpx").read_bytes() + hancom = _hancom_lines(data) + + for document in (data, _without_caches(data)): + estimate = estimate_pages(document) + estimated = [[line.vertpos for line in lines] for lines in estimate.lines] + assert estimate.unsupported == () + assert estimate.pages == 2 + assert [mine for mine, theirs in zip(estimated, hancom) if theirs] == [theirs for theirs in hancom if theirs] + assert {line.page for lines in estimate.lines for line in lines} == {0} + + def test_a_negative_outer_margin_counts_as_none() -> None: # A table over two pages whose outer margins were -500 (kept as unsigned 32-bit numbers): Hancom laid it # out as one without them and saved them as 0. With the margins back at -500 the estimate is the same.