Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@
높이 그대로다.
- 세로쓰기 칸이 있는 흐르는 표는 쪽 끝에서 나뉘지 않고, 나누지 않게 둔 표처럼 통째로 다음 쪽으로 간다.
기준 문단 줄과 그 뒤 글은 앞 쪽에 남는다.
- 쪽 수 추정(실험, `estimate_pages`)이 표 칸의 글을 칸 자신의 폭(`hp:cellSz`)에서 나누던 것을 고친다. 같은 열의
칸들이 폭이 다르면 한/글은 칸을 표 격자의 폭으로 놓는다.
- 열은 왼쪽부터 쌓는다. 열마다 그 열에서 끝나는 칸들의 시작 + 폭(+ 칸 간격 `cellSpacing`) 가운데 가장 먼 곳에서
끝난다. 그래서 열은 그 열 낱칸 가운데 가장 넓은 것만큼 넓고, 여러 열을 합친 칸이 그 열들보다 넓으면 넘침이
마지막 열에 붙는다.
- 칸은 첫 열의 시작부터 마지막 열의 끝까지다. 다른 행 때문에 자기 폭보다 넓어질 수 있다.
- 줄 캐시가 없는 칸의 줄 나눔과 행 높이에 쓴다(`_grid_widths`).
- 쪽 수 추정(실험, `estimate_pages`)이 줄 캐시 없는 표 칸에서 글자처럼 둔 표 옆에 글 앞·뒤로 둔 개체가 있는
문단을 지원 밖(`a nested table`)으로 두던 것을 고친다.
- 그런 개체는 자리를 차지하지 않으므로, 그 문단은 표 혼자인 문단처럼 표 높이의 한 줄이다.
Expand Down
34 changes: 31 additions & 3 deletions src/hwpx/layout/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
27 changes: 27 additions & 0 deletions tests/test_layout_page_estimate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading