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 @@ -6,6 +6,13 @@

### 고침

- 쪽 수 추정(실험, `estimate_pages`)이 칸 단위로 나누는 흐르는 표(`pageBreak="CELL"`)에서 병합 칸으로 이어진
행들이 한 쪽보다 길면 "rows merged together taller than a page"로 거부하던 것을 고친다. 한/글은 쪽 끝마다
다시 나눈다. 칸마다 그 쪽에 들어가는 줄까지 두고 나머지는 다음 쪽 머리 행 아래로 넘기며, 넘긴 부분이 다시
쪽보다 길면 그 쪽 끝에서 또 나눈다. 행들보다 긴 병합 칸은 행이 끝난 뒤에도 혼자 다음 쪽들로 이어진다. 글보다
높게 선언된 칸의 남는 높이도(글이 다 들어간 뒤에도) 쪽 끝마다 행처럼 잘려 이어진다. 어느 쪽에도 한 줄도
들어가지 않는 칸만 전처럼 거부한다. 줄 사이에서 나뉜 선언 행의 다음 쪽 부분은 선언 높이의 나머지와, 이어지는
줄에 칸 위아래 여백을 더한 높이 가운데 큰 쪽이다. 이어지는 줄이 있으면 남은 높이가 낮아도 버리지 않는다.
- 쪽 수 추정(실험, `estimate_pages`)이 곁에 자리를 남기는 종이·쪽 기준 어울림(SQUARE) 개체의 띠에 닿는 줄을, 그 줄이
줄 캐시를 가질 때 따른다. 한/글은 그런 줄을 같은 높이·간격으로 개체 곁에 좁혀 둔다(양옆에 자리가 있으면 한 높이에
두 조각). 캐시 없는 줄·표·개체가 닿으면 전처럼 지원 밖이다.
Expand Down
95 changes: 64 additions & 31 deletions src/hwpx/layout/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@
move to the next page as one, and in one split between cell lines each of their cells keeps the
lines that fit and the rest go on, the rows from the one the page end falls in as tall as their
cells' rest (a cell declared taller than its text, whose first line fits, is cut like such a row,
below). A table set as a character alone in a paragraph of a cell is one line as tall as it
below), a rest taller than a page split again at each page end the same way. A table set as a
character alone in a paragraph of a cell is one line as tall as it
there, spaced like the text, and a row holding one splits between its cell's lines, each as tall
as it is; so is another object set as a character alone in a cell paragraph without a layout
cache, and such objects among its text, or several of them, take their place in its lines as in
Expand Down Expand Up @@ -132,8 +133,9 @@
across the column before any text; an object offset down, but a flowing table, or wrapped square
stays on one page with the lines above or beside it), footnotes in such a paragraph, two tables
starting past their
anchors on one page, rows merged together that do not fit under their table's anchor or on a
page, a nested table among text or not set as a character in a cell without such caches (in a table
anchors on one page, rows merged together that do not fit under their table's anchor, or on a
page in a table moved row by row (split between cell lines: a cell no line of which fits a page),
a nested table among text or not set as a character in a cell without such caches (in a table
Hancom has not laid out as it is), a page break in a flowing row holding a table beside a taller
cell, other objects placed on the page or the paper (but top and bottom from its top or bottom, a
flowing table on their page keeping clear of them), and in a paragraph without such a cache ruby text
Expand Down Expand Up @@ -1853,13 +1855,7 @@ def _flow_table(table: _FlowTable, frame: int, y: int, body: int) -> tuple[int,
continue
if table.mode == "CELL" and end > index and table.cells \
and y + sum(row.height for row in rows[index:end + 1]) > foot: # split cell by cell
rest = _block_rest(table, index, end, y, body)
if not rest: # every cell done above the page end, what it declares below dropped: they end there
y, index = body, end + 1
continue
frame, y = frame + 1, header + rest
if y > foot:
raise _Unsupported("rows merged together taller than a page")
frame, y = _split_block(table, index, end, frame, y, body, header)
index = end + 1
continue
before, start = frame, y
Expand All @@ -1876,12 +1872,38 @@ def _flow_table(table: _FlowTable, frame: int, y: int, body: int) -> tuple[int,
return frame, y


def _block_rest(table: _FlowTable, first: int, last: int, top: int, body: int) -> int:
"""How tall rows *first*..*last*, joined by merged cells, are on the next page when the page end
def _split_block(table: _FlowTable, first: int, last: int, frame: int, y: int, body: int,
header: int) -> tuple[int, int]:
"""Rows *first*..*last*, joined by merged cells and starting at *y*, in a table split between cell
lines when the page end falls among them: each page keeps the lines that fit, and what is left goes on
below the next page's *header*, split again at that page's end until it fits. The frame and position
where they end."""

foot, before = body - table.cut, None
while True:
cut, heights, cells = _block_rest(table, first, last, y, body)
rest = sum(heights.values())
if not rest: # every cell done above the page end, what it declares below dropped: they end there
return frame, body
if header + rest <= foot:
return frame + 1, header + rest
if before is not None and rest >= before: # nothing of it fits a page
raise _Unsupported("rows merged together taller than a page")
rows = list(table.rows)
for index, height in heights.items():
rows[index] = replace(rows[index], height=height)
table, first, frame, y, before = replace(table, rows=rows, cells=tuple(cells)), cut, frame + 1, header, rest


def _block_rest(table: _FlowTable, first: int, last: int, top: int,
body: int) -> tuple[int, dict[int, int], list[tuple[int, int, _Row]]]:
"""What goes on to the next page of rows *first*..*last*, joined by merged cells, when the page end
falls among them in a table split between cell lines: every cell keeps the lines that fit above
the page end and the rest go on. From the row the page end falls in, each row is as tall as the
rest of its cells of one row (the rows after it whole), then each merged cell's rest, the one
ending first first, adds what its rows lack to the last of them."""
ending first first, adds what its rows lack to the last of them. The row the page end falls in,
each row's height from it on, and the cells that go on, each as tall as its rest with the lines
it has left."""

rows, tops, y, foot = table.rows, {}, top, body - table.cut
for index in range(first, last + 1):
Expand All @@ -1890,34 +1912,45 @@ def _block_rest(table: _FlowTable, first: int, last: int, top: int, body: int) -
cut = next(index for index in range(first, last + 1) if tops[index] + rows[index].height > foot)
heights = dict.fromkeys(range(cut, last + 1), 0)
rests: list[tuple[int, int, int]] = []
cells: list[tuple[int, int, _Row]] = []
for start, span, cell in table.cells:
end = start + span - 1
if start < first or start > last or end < cut:
continue # another row, or done above the page end
if start > cut: # wholly on the next page
rest = cell.height
else:
fitting = 0
while fitting < cell.lines and tops[start] + cell.margins + fitting * cell.pitch + cell.size <= foot:
fitting += 1
if cell.spare and fitting: # its text starts above the page end: the declared room is cut like a row's
rest = tops[start] + cell.height - foot
if rest <= _SPARE_DROPPED:
continue
elif cell.spare: # none of it fits: it goes on whole
rest = cell.height
elif fitting == cell.lines:
continue
else:
rest = cell.margins + (cell.lines - fitting - 1) * cell.pitch + cell.size
part = (cell.height, cell.lines) if start > cut else _cell_rest(cell, tops[start], foot)
if part is None:
continue
rest, begin = part[0], max(start, cut)
cells.append((begin, end - begin + 1, replace(cell, height=rest, lines=part[1])))
if span == 1:
heights[start] = max(heights[start], rest)
else:
rests.append((max(start, cut), end, rest))
rests.append((begin, end, rest))
for start, end, rest in sorted(rests, key=lambda item: (item[1], item[0])):
lacking = rest - sum(heights[index] for index in range(start, end + 1))
heights[end] += max(lacking, 0)
return sum(heights.values())
return cut, heights, cells


def _cell_rest(cell: _Row, top: int, foot: int) -> tuple[int, int] | None:
"""How tall the part of a cell starting at *top* that goes on past the page end *foot* is, and how many
lines it holds; ``None`` when nothing of it goes on."""

fitting = 0
while fitting < cell.lines and top + cell.margins + fitting * cell.pitch + cell.size <= foot:
fitting += 1
if cell.spare and (fitting or not cell.lines): # its text (or what is left: none) starts above the page end:
# the declared room is cut like a row's, and the lines that do not fit go on with it, which is then
# at least as tall as they are with the cell's margins
rest, left = top + cell.height - foot, cell.lines - fitting
if left:
return max(rest, cell.margins + (left - 1) * cell.pitch + cell.size), left
return (rest, 0) if rest > _SPARE_DROPPED else None
if cell.spare: # none of it fits: it goes on whole
return cell.height, cell.lines
if fitting == cell.lines:
return None
return cell.margins + (cell.lines - fitting - 1) * cell.pitch + cell.size, cell.lines - fitting


def _flow_row(mode: str, row: _Row, frame: int, y: int, body: int, header: int,
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
43 changes: 31 additions & 12 deletions tests/test_layout_page_estimate.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,24 @@
# empty one spaced 562 before it
"pages_hide_empty_lines_ending_the_document": 1, # two ending the document: no second page
"pages_hide_empty_lines_off": 2, # two, the setting off: they start the next page
"pages_joined_rows_over_pages_n100": 3, # 100 one-line rows joined by a cell merged down all of them,
# split between cell lines: what is left is split again at
# each page end
"pages_joined_rows_over_pages_label150": 4, # 60 such rows and a merged cell of 150 lines: the cell
# goes on alone over the last two pages
"pages_joined_rows_over_pages_mixed": 3, # rows of one and three lines: a row split between its lines
"pages_joined_rows_over_pages_declared_cell": 3, # 8 rows joined by a cell declared 80000 tall, after 20
# 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
# 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
# cell margins, taller than the declared rest
"pages_joined_rows_declared_cut_in_a_later_row": 2, # in row 2: its declared rest, taller than the line
"pages_joined_rows_declared_first_line_not_fitting": 2, # in row 3, before its first line: whole on the
# next page
"pages_joined_rows_text_height_split": 2, # in row 1, the rows as tall as their text: lines and margins
"pages_table_row_split_a_first_line_not_fitting": 2, # the first 8 pt line of one cell fits, the 16 pt
# line of the other does not: it goes on whole
"pages_table_row_split_every_first_line_fitting": 2, # room for both: split after two 8 pt lines
Expand Down Expand Up @@ -782,20 +800,21 @@ def _flowing_table_after(paragraphs: int, rows: int) -> tuple[HwpxDocument, obje
return document, table


def test_rows_merged_together_taller_than_a_page_are_unsupported() -> None:
# The cell merged over rows 1-4 is declared 150000 tall: what is left of it after the page end
# does not fit on the next page either.
document, table = _flowing_table_after(38, 6)
table.merge_cells(1, 0, 4, 0)
for cell in table.element.iter(f"{HP}tc"):
span = cell.find(f"{HP}cellSpan")
if span is not None and span.get("rowSpan", "1") != "1":
cell.find(f"{HP}cellSz").set("height", "150000")
def test_a_merged_cell_declared_taller_than_two_pages_is_cut_at_each_page_end() -> None:
# After 38 lines, a flowing table of 6 rows whose cell merged over rows 1-4 is declared 150000 tall, laid
# out and saved by Hancom: the cell's room is cut 101 above each page's foot and the rest goes on, page
# after page, the last row ending the table (and the document) on page 4. No line of text is on that
# page, so the pages are checked here rather than in HANCOM_PAGES.
data = (FIXTURES / "pages_joined_rows_over_pages_declared_150000.hwpx").read_bytes()
hancom = _hancom_lines(data)

estimate = estimate_pages(document)
for source in (data, _without_caches(data)):
estimate = estimate_pages(source)
estimated = [[line.vertpos for line in lines] for lines in estimate.lines]

assert estimate.pages is None
assert estimate.unsupported == ("section 0: rows merged together taller than a page",)
assert (estimate.unsupported, estimate.pages) == ((), 4)
assert len(estimated) == len(hancom)
assert [mine for mine, theirs in zip(estimated, hancom) if theirs] == [theirs for theirs in hancom if theirs]


@pytest.mark.parametrize(("height", "rest"), [(20000, 13221), (8029, 0), (8061, 0), (8062, 1283), (8079, 1300)])
Expand Down
Loading