diff --git a/CHANGELOG.md b/CHANGELOG.md index 22f7af94..7ffb4d97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,13 @@ 높이 그대로다. - 세로쓰기 칸이 있는 흐르는 표는 쪽 끝에서 나뉘지 않고, 나누지 않게 둔 표처럼 통째로 다음 쪽으로 간다. 기준 문단 줄과 그 뒤 글은 앞 쪽에 남는다. +- 쪽 수 추정(실험, `estimate_pages`)이 표 칸의 글을 칸 자신의 폭(`hp:cellSz`)에서 나누던 것을 고친다. 같은 열의 + 칸들이 폭이 다르면 한/글은 칸을 표 격자의 폭으로 놓는다. + - 열은 왼쪽부터 쌓는다. 열마다 그 열에서 끝나는 칸들의 시작 + 폭(+ 칸 간격 `cellSpacing`) 가운데 가장 먼 곳에서 + 끝난다. 그래서 열은 그 열 낱칸 가운데 가장 넓은 것만큼 넓고, 여러 열을 합친 칸이 그 열들보다 넓으면 넘침이 + 마지막 열에 붙는다. + - 칸은 첫 열의 시작부터 마지막 열의 끝까지다. 다른 행 때문에 자기 폭보다 넓어질 수 있다. + - 줄 캐시가 없는 칸의 줄 나눔과 행 높이에 쓴다(`_grid_widths`). - 쪽 수 추정(실험, `estimate_pages`)이 줄 캐시 없는 표 칸에서 글자처럼 둔 표 옆에 글 앞·뒤로 둔 개체가 있는 문단을 지원 밖(`a nested table`)으로 두던 것을 고친다. - 그런 개체는 자리를 차지하지 않으므로, 그 문단은 표 혼자인 문단처럼 표 높이의 한 줄이다. diff --git a/src/hwpx/layout/pages.py b/src/hwpx/layout/pages.py index e92dcb9a..5f2ac633 100644 --- a/src/hwpx/layout/pages.py +++ b/src/hwpx/layout/pages.py @@ -782,8 +782,9 @@ def _table_rows(measure: _Measure, table: Any) -> tuple[list[_Row], list[tuple[i nested: set[int] = set() # rows with a cell holding a table headers: set[int] = set() # rows with a header cell of their own firsts: dict[int, int] = {} # each row's tallest first line of a cell of its own + widths = _grid_widths(table) for tc in (tc for tr in table.findall(f"{HP}tr") for tc in tr.findall(f"{HP}tc")): - row, span = _cell_row(measure, table, tc), _row_span(tc) + row, span = _cell_row(measure, table, tc, widths.get(tc)), _row_span(tc) address = tc.find(f"{HP}cellAddr") first = int(address.get("rowAddr", 0)) if address is not None else len(rows) cells.append((first, span, row)) @@ -818,6 +819,33 @@ def _table_rows(measure: _Measure, table: Any) -> tuple[list[_Row], list[tuple[i [(place[first], span, cell) for first, span, cell in cells]) +def _grid_widths(table: Any) -> dict[Any, int]: + """Each cell's width on the table's column grid, as Hancom lays its text out: from the left, each column + starts at the farthest of the ends its cells to the left reach (a cell's start plus its ``hp:cellSz`` width + and the table's cell spacing) -- a column as wide as its widest cell of its own, and a merged cell wider + than its columns widening the last of them -- and a cell reaches from its first column's start to its last + column's end, wider than its own width when another row's cells push those ends apart. Empty for a table + whose cells give no column starts to stand on.""" + + spacing = int(table.get("cellSpacing", 0) or 0) + places: dict[Any, tuple[int, int, int]] = {} # cell: (first column, column after its last, width) + for tc in (tc for tr in table.findall(f"{HP}tr") for tc in tr.findall(f"{HP}tc")): + address, span = tc.find(f"{HP}cellAddr"), tc.find(f"{HP}cellSpan") + if address is None: + return {} + first = int(address.get("colAddr", 0)) + places[tc] = (first, first + (1 if span is None else int(span.get("colSpan", 1))), + int(tc.find(f"{HP}cellSz").get("width", 0))) + starts = {0: 0} + for end in sorted({stop for _, stop, _ in places.values()}): + reach = [starts[first] + width + spacing for first, stop, width in places.values() + if stop == end and first in starts] + if not reach: + return {} + starts[end] = max(reach) + return {tc: starts[stop] - starts[first] - spacing for tc, (first, stop, _) in places.items() if first in starts} + + def _row_span(cell: Any) -> int: span = cell.find(f"{HP}cellSpan") return 1 if span is None else int(span.get("rowSpan", 1)) @@ -953,7 +981,7 @@ def _page_break(table: Any) -> str: return table.get("pageBreak", "CELL") -def _cell_row(measure: _Measure, table: Any, cell: Any) -> _Row: +def _cell_row(measure: _Measure, table: Any, cell: Any, width: int | None = None) -> _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 @@ -977,7 +1005,7 @@ def _cell_row(measure: _Measure, table: Any, cell: Any) -> _Row: drawn = _drawn_lines(measure, paragraphs) # lines as Hancom drew them, below it or beside it size = cell.find(f"{HP}cellSz") margins = cell_margins_of(cell, table) - inner = int(size.get("width", 0)) - margins.left - margins.right + inner = (int(size.get("width", 0)) if width is None else width) - margins.left - margins.right content, lines, pitch, char_size = measure.stack(paragraphs, inner, caches=True) # Hancom starts the cell's first line below its first paragraph's spacing before (the lines as drawn # take that room already) diff --git a/tests/fixtures/hancom_saved/pages_table_column_as_wide_as_its_widest_cell.hwpx b/tests/fixtures/hancom_saved/pages_table_column_as_wide_as_its_widest_cell.hwpx new file mode 100644 index 00000000..8762a90f Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_table_column_as_wide_as_its_widest_cell.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_table_column_of_equal_cells.hwpx b/tests/fixtures/hancom_saved/pages_table_column_of_equal_cells.hwpx new file mode 100644 index 00000000..266ab6d1 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_table_column_of_equal_cells.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_table_columns_stacked_from_the_left.hwpx b/tests/fixtures/hancom_saved/pages_table_columns_stacked_from_the_left.hwpx new file mode 100644 index 00000000..0f6fc829 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_table_columns_stacked_from_the_left.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_table_merged_cell_widening_its_last_column.hwpx b/tests/fixtures/hancom_saved/pages_table_merged_cell_widening_its_last_column.hwpx new file mode 100644 index 00000000..c21cab35 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_table_merged_cell_widening_its_last_column.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_table_with_cell_spacing.hwpx b/tests/fixtures/hancom_saved/pages_table_with_cell_spacing.hwpx new file mode 100644 index 00000000..3145ad00 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_table_with_cell_spacing.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_table_without_cell_spacing.hwpx b/tests/fixtures/hancom_saved/pages_table_without_cell_spacing.hwpx new file mode 100644 index 00000000..1008a4d1 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_table_without_cell_spacing.hwpx differ diff --git a/tests/test_layout_page_estimate.py b/tests/test_layout_page_estimate.py index 6745dcd4..2ad305a4 100644 --- a/tests/test_layout_page_estimate.py +++ b/tests/test_layout_page_estimate.py @@ -153,6 +153,17 @@ # rows: its room cut at each page end, 10 rows after "pages_joined_rows_over_pages_empty_declared_cell": 3, # an empty cell over 8 rows declared 75000 tall # after 36 lines: cut over three pages + # A table whose cells of a column are not all as wide (``hp:cellSz``), every cell declared 282 tall so that + # its lines make its row (10 pt 함초롬바탕, "가" 972 wide), then three lines of text: Hancom lays a cell out + # on the table's grid, its columns stacked from the left, each as wide as its widest cell of its own, and a + # merged cell wider than its columns widening the last of them: + "pages_table_column_as_wide_as_its_widest_cell": 1, # 30000, 32000 and 31000 wide: the first's 31 + # characters on one line + "pages_table_column_of_equal_cells": 1, # all 30000: on two lines + "pages_table_columns_stacked_from_the_left": 1, # rows of 8000 and 30000, 7683 and 30500: both second + # cells 30500 wide, 31 characters on one line + "pages_table_merged_cell_widening_its_last_column": 1, # 10000 and 20000 below a merged 31000: the + # second 21000 wide, 21 characters on one line # Rows 1-5 joined by a cell merged down them (4 lines), declared taller than their text, the page end # falling among them: "pages_joined_rows_declared_rest_with_a_line": 2, # in row 1: its rest is the line going on with the @@ -890,6 +901,22 @@ def test_the_evaluation_plan_form_lays_out_like_hancom_without_caches() -> None: _assert_like_hancom(estimate_pages(_without_caches(data)), data, 15) +@pytest.mark.parametrize("name", ["pages_table_with_cell_spacing", "pages_table_without_cell_spacing"]) +def test_a_table_with_cell_spacing_lays_its_cells_out_at_their_own_widths(name: str) -> None: + # A table 46196 wide: one cell over its three columns, then cells of 15000, 21000 and 9800 with a cell + # spacing of 198 (10196 without it), the last holding ten "가" (9720), every cell declared 282 tall. With + # the spacing the second row's cells add up to the first row's, and Hancom lays each cell out at its own + # width: the ten characters on two lines (one line without the spacing). The height it saved is its rows' + # and the spacing above, between and below them. + doc = HwpxDocument.open(_without_caches((FIXTURES / f"{name}.hwpx").read_bytes())) + table = next(doc.oxml.sections[0].element.iter(f"{HP}tbl")) + rows = page_layout._rows(page_layout._Measure(getattr(doc, "_root")), table) + spacing = int(table.get("cellSpacing")) + + assert [row.lines for row in rows] == [1, 2 if spacing else 1] + assert sum(row.height for row in rows) + (len(rows) + 1) * spacing == int(table.find(f"{HP}sz").get("height")) + + def test_a_table_set_not_to_split_taller_than_a_page_takes_the_next_one() -> None: # 50 rows under its anchor line: it moves on to the next page, which holds nothing else, and the text # after it stays under its anchor line on the first page.