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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@
끝난다. 그래서 열은 그 열 낱칸 가운데 가장 넓은 것만큼 넓고, 여러 열을 합친 칸이 그 열들보다 넓으면 넘침이
마지막 열에 붙는다.
- 칸은 첫 열의 시작부터 마지막 열의 끝까지다. 다른 행 때문에 자기 폭보다 넓어질 수 있다.
- 줄 캐시가 없는 칸의 줄 나눔과 행 높이에 쓴다(`_grid_widths`).
- 줄 캐시가 없는 칸의 줄 나눔과 행 높이에 쓴다(`hwpx.oxml.table_sizes.grid_widths_of`).
- FormFit(`resolve_slot_metrics`)도 칸의 자리를 그 폭에서 잰다(`SlotMetrics.raw_width`). 전에는 칸 자신의
`hp:cellSz` 폭이었다.
- 쪽 수 추정(실험, `estimate_pages`)이 줄 캐시 없는 표 칸에서 글자처럼 둔 표 옆에 글 앞·뒤로 둔 개체가 있는
문단을 지원 밖(`a nested table`)으로 두던 것을 고친다.
- 그런 개체는 자리를 차지하지 않으므로, 그 문단은 표 혼자인 문단처럼 표 높이의 한 줄이다.
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/module-ownership.json
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@
"path": "src/hwpx/oxml/table_sizes.py",
"disposition": "core",
"approvedBy": "hancom-distribute-cell-width",
"rationale": "How a table shares out its width and height among its cells: the bodies of HwpxOxmlTable.set_column_widths, equalize_column_widths (Hancom's distribute-cell-width rule: each row splits the table width among its own cells, the width rounded up to a common multiple of every row's cell count, the column grid rebuilt from the new cell edges) and equalize_row_heights; and the bodies of HwpxOxmlTableCell.margins/set_margins (the effective inner margins: the table's hp:inMargin unless the cell's hasMargin is on, then its hp:cellMargin), assigned onto the cell class as attributes. That margin rule (effective_cell_margin_source / cell_margins_of) is the one every other reader uses -- form fit, the nested-table default width, the table height bound, the layout preview, the template analyzer and the byte-level cell fill -- so the engine reports one value per cell. A new module rather than more table.py lines: the open table fixes together would take that owner file past the 1,600-line cap; the table methods keep their documentation and delegate. Format-level only."
"rationale": "How a table shares out its width and height among its cells: the bodies of HwpxOxmlTable.set_column_widths, equalize_column_widths (Hancom's distribute-cell-width rule: each row splits the table width among its own cells, the width rounded up to a common multiple of every row's cell count, the column grid rebuilt from the new cell edges) and equalize_row_heights; and the bodies of HwpxOxmlTableCell.margins/set_margins (the effective inner margins: the table's hp:inMargin unless the cell's hasMargin is on, then its hp:cellMargin), assigned onto the cell class as attributes. That margin rule (effective_cell_margin_source / cell_margins_of) is the one every other reader uses -- form fit, the nested-table default width, the table height bound, the layout preview, the template analyzer and the byte-level cell fill -- so the engine reports one value per cell. grid_widths_of gives each cell's width on the column grid Hancom lays text out on (columns stacked from the left, each ending at the farthest of its cells' starts plus widths and the cell spacing), read by the page estimate and form fit. A new module rather than more table.py lines: the open table fixes together would take that owner file past the 1,600-line cap; the table methods keep their documentation and delegate. Format-level only."
},
{
"path": "src/hwpx/oxml/paragraph_heading.py",
Expand Down
23 changes: 19 additions & 4 deletions src/hwpx/form_fit/measure.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from functools import lru_cache
from typing import Any, Literal

from ..oxml.table_sizes import cell_margins_of
from ..oxml.table_sizes import cell_margins_of, grid_widths_of

# Advance width as a fraction of the em (font height in HWPUNIT). Hangul/wide are
# exact (full-width cells); the Latin/digit/punct values are conservative class
Expand Down Expand Up @@ -825,7 +825,7 @@ class SlotMetrics:
available_width: float # usable inner width after margins + safety
font_pt: float
max_lines: int = 1
raw_width: float | None = None # cellSz.width before margins (diagnostics)
raw_width: float | None = None # the cell's width on the table grid before margins (diagnostics)
source: str = "cell"
# Vertical budget. ``available_height`` is the usable inner height (HWPUNIT)
# after top/bottom cell margins + the safety inset. ``None`` means the vertical
Expand Down Expand Up @@ -1141,6 +1141,19 @@ def _local_name(tag: object) -> str:
return str(tag).rsplit("}", 1)[-1]


def _grid_width(cell: object) -> float:
"""The cell's width on its table's column grid, as Hancom lays its text out: wider than its own
``hp:cellSz`` when another row's cells push its columns' ends apart (see
:func:`hwpx.oxml.table_sizes.grid_widths_of`); its own width without a table to read."""

own = float(getattr(cell, "width", 0) or 0)
element = getattr(cell, "element", None)
table_element = getattr(getattr(cell, "table", None), "element", None)
if element is None or table_element is None:
return own
return float(grid_widths_of(table_element).get(element, own))


def _effective_cell_margins(cell: object) -> tuple[int, int, int, int]:
"""(left, right, top, bottom) the cell is laid out with, as ``cell.margins`` reads them.

Expand Down Expand Up @@ -1434,7 +1447,9 @@ def resolve_slot_metrics(
) -> SlotMetrics:
"""Build :class:`SlotMetrics` from a live table cell.

``available_width = max(cellSz.width - margin.L - margin.R, MIN_LINE_WIDTH) * safety``,
``available_width = max(width - margin.L - margin.R, MIN_LINE_WIDTH) * safety``, the width being the
cell's on the table's column grid (its ``cellSz.width`` unless another row's cells push its columns' ends
apart, see :func:`hwpx.oxml.table_sizes.grid_widths_of`),
where the margins are the cell's effective ones (``cell.margins``: the
table's ``hp:inMargin`` unless ``hasMargin`` is on) —
verified against Hancom's own ``lineSeg/@horzsize`` (±10 HWPUNIT on 82% of
Expand Down Expand Up @@ -1486,7 +1501,7 @@ def _cell_slot(
) -> SlotMetrics:
"""The width, font and text style of a cell's slot, without a height budget."""

raw_width = float(getattr(cell, "width", 0) or 0)
raw_width = _grid_width(cell)
element = getattr(cell, "element", None)
left, right, _top, _bottom = _effective_cell_margins(cell)
line = max(raw_width - left - right, 0.0) * safety
Expand Down
31 changes: 2 additions & 29 deletions src/hwpx/layout/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@
from ..oxml.section import _remove_short_paragraph_layout_cache
from ..oxml.header_part import HwpxOxmlHeader
from ..oxml.section_format import _drawn_page_size
from ..oxml.table_sizes import cell_margins_of
from ..oxml.table_sizes import cell_margins_of, grid_widths_of

if TYPE_CHECKING:
from ..document import HwpxDocument
Expand Down Expand Up @@ -786,7 +786,7 @@ 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)
widths = grid_widths_of(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, widths.get(tc)), _row_span(tc)
address = tc.find(f"{HP}cellAddr")
Expand Down Expand Up @@ -823,33 +823,6 @@ 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
29 changes: 29 additions & 0 deletions src/hwpx/oxml/table_sizes.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"set_cell_margins",
"effective_cell_margin_source",
"set_column_widths",
"grid_widths_of",
]

_MARGIN_SIDES = ("left", "right", "top", "bottom")
Expand Down Expand Up @@ -304,6 +305,34 @@ def cell_margins_of(cell_element: Any, table_element: Any | None) -> CellMargins
return CellMargins(*(_margin_value(source.get(side)) for side in _MARGIN_SIDES))


def grid_widths_of(table_element: Any) -> dict[Any, int]:
"""Each ``hp:tc`` of a raw ``hp:tbl`` element and its width on the table's column grid, as Hancom lays its
text out (the page estimate and form fit read it). 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 is as wide as its widest cell of its own, and a merged cell wider than its columns widens the last
of them. 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_element.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_element.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 _margin_value(raw: str | None) -> int:
try:
return int(raw or 0)
Expand Down
24 changes: 24 additions & 0 deletions tests/test_form_fit_hancom_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,30 @@ def test_a_cell_holds_lines_up_to_its_stored_height() -> None:
assert budgets == [1, 2, 2]


@pytest.mark.parametrize(
("fixture", "row", "column", "width"),
[
("pages_table_column_as_wide_as_its_widest_cell", 0, 1, 32000), # 30000 in a column of 30000, 32000,
# 31000
("pages_table_columns_stacked_from_the_left", 0, 1, 30500), # 30000 after 8000, 30500 after 7683 below
("pages_table_merged_cell_widening_its_last_column", 0, 2, 21000), # 20000 after 10000 under a cell
# merged over both, 31000
],
)
def test_a_cell_slot_is_as_wide_as_its_columns_on_the_table_grid(fixture: str, row: int, column: int,
width: int) -> None:
# Hancom laid these cells' lines out at the width of their columns on the table grid, not their own
# hp:cellSz: the slot is measured there too, as wide as Hancom's lines (within its rounding of the margins).
doc = HwpxDocument.open((Path(__file__).parent / "fixtures" / "hancom_saved" / f"{fixture}.hwpx").read_bytes())
table = next(table for paragraph in doc.paragraphs for table in paragraph.tables)
cell = table.cell(row, column)
slot = resolve_slot_metrics(cell, doc, safety=1.0)
segment = cell.element.find(f"{HP}subList/{HP}p/{HP}linesegarray/{HP}lineseg")

assert slot.raw_width == width
assert abs(slot.available_width - int(segment.get("horzsize"))) <= 2


def test_each_line_spacing_type_advances_a_line_as_hancom_does() -> None:
def pitch(kind: str, value: float) -> float:
return SlotMetrics(available_width=5000.0, font_pt=10.0, line_spacing=(kind, value)).line_height()
Expand Down
Loading