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
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@

### 고침

- 쪽 수 추정(실험, `estimate_pages`)이 글과 함께 움직이고 나뉠 수 있는 어울림(SQUARE) 표가 본문 바닥을 넘을 때,
줄 캐시가 없으면 지원 밖으로 두던 것을 고친다.
- 한/글은 그런 표를 흐르는 표처럼 쪽 끝에서 행 사이나 칸 줄 사이로 나눈다. 표가 걸친 쪽마다 곁 줄은 그 쪽
부분 곁으로 간다. 첫 쪽은 표 위부터 바닥까지, 표가 쪽을 다 덮으면 그 쪽 모든 줄, 마지막 쪽은 맨 위부터 표
끝까지다.
- 이제 그 표를 흐르는 표처럼 놓아 쪽마다 띠를 두고, 띠에 닿는 줄을 다시 나눈다.
- 한 쪽에 띠가 둘이면(앞 표의 끝과 다음 표의 시작) 둘 다 둔다. 한 문단이 두 띠에 걸치면 지원 밖이다.
- 쪽 수 추정(실험, `estimate_pages`)이 글과 함께 움직이는 어울림(SQUARE) 그림·도형이 본문 바닥을 넘을 때, 줄 캐시가
없으면 지원 밖으로 두던 것을 고친다.
- 한/글은 그 개체를 띄움과 상관없이 다음 쪽 본문 맨 위로 옮긴다. 개체가 있던 쪽의 줄은 모두 전체 폭이다. 다음
쪽에서 줄 위가 개체 아래 끝보다 위인 줄은 개체 곁으로 간다.
- 이제 그 쪽들을 다시 놓는다. 개체 문단과 뒤 문단을 전체 폭으로 나누고, 다음 쪽 맨 위의 개체 곁에서 줄을 다시
나눈다. 그 자리에 개체·각주가 든 문단이나 표가 오면 지원 밖이다.
- 나누지 않게 둔 어울림 표도 그렇게 옮겨 간다. 나뉘는 어울림 표는 쪽 끝에서 나뉘므로 전처럼 캐시가 있을 때만
따른다.
- 나누지 않게 둔 어울림 표도 그렇게 옮겨 간다. 나뉘는 어울림 표는 쪽 끝에서 나뉜다(아래 항목).
- 문단 줄 아래로 띄워 위아래로 둔 그림·도형도, 문단이 놓인 자리에서 개체 발이 바닥을 넘으면 다음 쪽 맨 위로
옮겨 간다. 문단 줄은 제자리에 남고, 다음 쪽에서 개체에 닿는 줄은 그 아래로 간다. 캐시가 있을 때도 전에는
문단째 다음 쪽으로 보냈다.
Expand Down
79 changes: 66 additions & 13 deletions src/hwpx/layout/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,12 @@
and its outer margins take (a drop cap is such an object), or, with text on both sides, is two
pieces at one height, the left one first -- a table by the height
of its rows; a picture or drawing, or a table set not to split, whose band passes the body's foot
goes on alone to the next page's body top, whatever its offset: the lines of its own page are the whole width, and the
lines on the next page whose top is above its foot go beside it (without layout caches, broken
again there; a paragraph holding objects or a table there is not followed), while such a table
is followed only through the layout caches; a line beside an object with text on both sides is
goes on alone to the next page's body top, whatever its offset: the lines of its own page are the
whole width, and the lines on the next page whose top is above its foot go beside it (without
layout caches, broken again there; a paragraph holding objects or a table there is not
followed), while a table that may split is split over the page end as a flowing table, the lines
on each page it reaches beside its part there (on a page it fills, all of them); a line beside an
object with text on both sides is
two pieces even when its text ends in the first or it has none (the second empty) -- and wrapped
square with no room beside it (less than 1440 on each side its text flow
allows), it pushes the text below it like a top-and-bottom object; a table flowing with the text is laid out row by row -- split between cell lines, moved row
Expand Down Expand Up @@ -1121,6 +1123,10 @@ class _Para:
#: how far such an object pushed the paragraph's first line down (its band's foot is measured from where
#: that line stood before)
wrap_shift: int = 0
#: a table wrapped square anchored here, flowing with the text and allowed to split, that goes on over the
#: page end: it as it flows, how far below the paragraph's first line it starts, and its band beside the
#: text (see :meth:`_Paginator._span_bands`)
spans: tuple[_FlowTable, int, _Wrap] | None = None
#: such an object alone in this empty paragraph, its foot this far below the paragraph's top (the line is
#: as tall as that): past the body's foot it goes on to the next page's top, the empty line staying
moves: int = 0
Expand Down Expand Up @@ -1998,6 +2004,22 @@ def _square_sides(obj: Any, page: _Page) -> tuple[int, int]:
return left, right


def _spans(measure: _Measure, page: _Page, table: Any) -> tuple[_FlowTable, int, _Wrap]:
"""A table wrapped square that splits over the page end: it as a flowing table, its offset below its
paragraph's first line, and its band beside the text (top and bottom set page by page)."""

offset = int(table.find(f"{HP}pos").get("vertOffset", 0))
if offset < 0 or offset >= 1 << 31:
raise _Unsupported("a square-wrapped table placed up from its paragraph past the page foot")
rows, cells = _table_rows(measure, table)
margin = table.find(f"{HP}outMargin")
ends = (0, 0) if margin is None else (_margin(margin, "top"), _margin(margin, "bottom"))
flowing = _FlowTable(rows, table.get("pageBreak", "CELL"), table.get("repeatHeader") == "1", ends,
tuple(cells), caption=_caption(measure, table), cut=_spare_cut(table))
left, right = _square_sides(table, page)
return flowing, offset, _Wrap(0, 0, page.column_width - left - right, split=left if left and right else 0)


def _wrapped_paragraph(measure: _Measure, page: _Page, paragraph: Any, wrap: _Wrap | None, moved: bool = False) -> tuple[_Para, _Wrap | None]:
"""The paragraph with the lines beside a square-wrapped object's band narrower, and the band as the
next paragraph sees it. With *moved*, the object went on to the next page's top: none here."""
Expand All @@ -2019,6 +2041,9 @@ def _wrapped_paragraph(measure: _Measure, page: _Page, paragraph: Any, wrap: _Wr
# followed)
alone = objects[0] if len(objects) == 1 and objects[0].find(f"{HP}pos").get("treatAsChar") != "1" else None
obj = square if square is not None else pusher if pusher is not None else alone
if obj is not None and obj is square and wrap is None and _local(obj) == "tbl" \
and obj.get("pageBreak", "CELL") != "NONE": # it splits over the page end instead
return replace(_paragraph(measure, page, paragraph, None, obj), spans=_spans(measure, page, obj)), None
if obj is None or wrap is not None or _local(obj) == "tbl" and (obj is not square
or obj.get("pageBreak", "CELL") != "NONE"):
raise _Unsupported("a square-wrapped or offset top-and-bottom object past the page foot")
Expand Down Expand Up @@ -2567,8 +2592,9 @@ def __init__(self, body: int, columns: int, notes: _NoteShape,
self.reserved: dict[int, int] = {} # frames a table starting past its anchor takes: where text starts
self.wrap_frame = -1 # the frame a square-wrapped object's band is on
self.wrap_moved = False # that band passed the body's foot: the object went on to the next page
#: frame -> the band of a square-wrapped object that went on to its top (see :attr:`_Para.moved`)
self.squares: dict[int, _Wrap] = {}
#: frame -> the bands of square-wrapped objects there that the text there is broken beside: one that
#: went on to its top (see :attr:`_Para.moved`), the parts of tables split over its ends
self.squares: dict[int, list[_Wrap]] = {}
#: a flowing table's band no line has reached yet: its frame, top, bottom there (None when it goes
#: on over the page end), and the frame and position where the lines after it go on
self.band: tuple[int, int, int | None, int, int] | None = None
Expand Down Expand Up @@ -2955,6 +2981,9 @@ def _breaks(self, para: _Para, start: int) -> tuple[int, bool]:
def _lay(self, index: int, paras: list[_Para], para: _Para, start: int, broke: bool) -> bool:
"""Place the lines of *para*; False when its notes ended the page after it."""

if para.spans is not None: # a table wrapped square going on over the page end: its bands, page by page
self._span_bands(para.spans, start)
para = replace(para, spans=None)
para = self._at_band(self._at_width(para, 0), 0, start)
remaining, first_chunk = para.lines, True
fresh = self.last_vp is None or broke
Expand Down Expand Up @@ -3015,14 +3044,38 @@ def _at_band(self, para: _Para, line: int, start: int) -> _Para:
"""*para* with its lines from *line* on, laid from *start* down, broken again beside the band of a
square-wrapped object set at the current frame's top when they reach it."""

band = self.squares.get(self.frame)
if band is None or start >= band.bottom:
bands = sorted((band for band in self.squares.get(self.frame, ()) if band.bottom > start),
key=lambda band: band.top)
if not bands:
return para
if para.blank: # an empty line: two empty pieces beside an object with text on both sides
return _in_two_pieces(para) if band.split and para.lines == 1 and not para.cached else para
band = bands[0]
if para.blank: # an empty line reaching it: two empty pieces beside an object with text on both sides
reaches = start + para.height(line) > band.top
return _in_two_pieces(para) if reaches and band.split and para.lines == 1 and not para.cached else para
if para.reflow is None:
raise _Unsupported("objects, notes or a layout cache beside a square-wrapped object moved to the next page")
return para.reflow.banded(para, line, replace(band, top=band.top - start, bottom=band.bottom - start))
again = para.reflow.banded(para, line, replace(band, top=band.top - start, bottom=band.bottom - start))
if len(bands) > 1 and start + again.span(0, again.lines) > bands[1].top:
raise _Unsupported("a paragraph beside two square-wrapped objects on a page")
return again

def _span_bands(self, spans: tuple[_FlowTable, int, _Wrap], start: int) -> None:
"""The bands of a table wrapped square, anchored in the paragraph being laid from *start*, that goes
on over the page end: it flows as a flowing table from its top, and on each page it reaches the lines
go beside its part there -- on its first page from its top to the foot, on a page it fills the whole
page, on its last page from the top down to its end."""

table, offset, band = spans
top = start + offset
last, end = _flow_table(table, self.frame, top + table.above, self.body)
for frame in range(self.frame, last + 1):
part = replace(band, top=top if frame == self.frame else 0,
bottom=end + table.below if frame == last else self.body)
if self.bands.get(frame) or self.sides.get(frame) or frame in self.reserved \
or any(other.top < part.bottom and part.top < other.bottom for other in self.squares.get(frame, ())):
raise _Unsupported("a square-wrapped table going on over the page end beside other objects")
self.squares.setdefault(frame, []).append(part)
self.table_end = max(self.table_end, last)

def _move_band(self, band: _Wrap) -> None:
"""A square-wrapped object anchored in the paragraph being laid, flowing with the text, went past the
Expand All @@ -3031,12 +3084,12 @@ def _move_band(self, band: _Wrap) -> None:

target = self.frame + 1
if band.bottom > self.body or self.bands.get(target) or self.sides.get(target) or target in self.reserved \
or target in self.squares:
or any(other.top < band.bottom for other in self.squares.get(target, ())):
raise _Unsupported("an object going on to the next page's top beside other objects")
if band.push: # a top-and-bottom one: the lines reaching it go below it
self.bands[target] = [(band.top, band.bottom)]
else:
self.squares[target] = band
self.squares.setdefault(target, []).append(band)
self.table_end = max(self.table_end, target)

def _next_frame(self, para: _Para, count: int, first_chunk: bool) -> 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.
32 changes: 19 additions & 13 deletions tests/test_layout_page_estimate.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@
"pages_square_picture_moved_from_8000_down": 3, # placed 8000 down from its paragraph: at the top too
"pages_square_table_not_split_moved_to_the_next_page": 3, # a table of six rows at the column's left,
# set not to split: the same
# A table wrapped square at the column's left (rows of two columns, 10 pt text spaced 160%), flowing with
# the text and allowed to split, its band past the body's foot: Hancom splits it over the page end as a
# flowing table, and the lines on each page it reaches go beside its part there:
"pages_square_table_split_between_cell_lines": 3, # 15000 wide, split between the lines of a row of four
"pages_square_table_split_in_a_merged_cell": 3, # the same through a cell merged over three rows
"pages_square_table_split_over_three_pages": 3, # 25 rows: all the lines of the page it fills beside it
"pages_square_table_1548_down_split": 3, # placed 1548 down: its paragraph's line above it the whole width
"pages_square_table_split_text_on_both_sides": 3, # in the middle: the lines in two pieces
"pages_square_table_wide_split_text_beside_it": 2, # 45626 wide, 2562 left beside it: two characters a
# line there
"pages_picture_1319_down_moved_to_the_next_page": 3, # a picture 8000 tall placed top and bottom 1319
# below its empty paragraph's line: alone at the
# next page's top, the line staying, the lines
Expand Down Expand Up @@ -1197,27 +1207,23 @@ def test_a_page_break_in_a_row_holding_a_table_beside_a_taller_cell_is_unsupport


@pytest.mark.parametrize(
("name", "followed"),
"name",
[
("pages_picture_square_past_the_page_foot", True), # its paragraph's three lines the page's last
("pages_picture_square_past_the_page_foot_mid_paragraph", True), # its first line the page's last
("pages_table_square_past_the_page_foot", False), # a table wrapped square alone in its paragraph
"pages_picture_square_past_the_page_foot", # its paragraph's three lines the page's last
"pages_picture_square_past_the_page_foot_mid_paragraph", # its first line the page's last
"pages_table_square_past_the_page_foot", # a table wrapped square alone in its paragraph
],
)
def test_a_square_object_past_the_page_foot_goes_on_at_the_next_page(name: str, followed: bool) -> None:
def test_a_square_object_past_the_page_foot_goes_on_at_the_next_page(name: str) -> None:
# A picture wrapped square from its paragraph's top, 8000 tall, its band past the body's foot: Hancom
# sets it alone at the next page's top, the lines beside it there narrower and the lines left above the
# foot as wide as the column. With the caches the lines stay where the caches put them; without them
# they are broken again beside it there. A table wrapped square is split over the page end instead, its
# band going on at the next page's top: not followed without the caches.
# foot as wide as the column. A table wrapped square is split over the page end instead, its band going
# on at the next page's top as tall as what is left of it. With the caches the lines stay where the
# caches put them; without them they are broken again beside it there.
data = (FIXTURES / f"{name}.hwpx").read_bytes()

_assert_like_hancom(estimate_pages(data), data, 2)
if followed:
_assert_like_hancom(estimate_pages(_without_caches(data)), data, 2)
else:
assert estimate_pages(_without_caches(data)).unsupported == (
"section 0: a square-wrapped or offset top-and-bottom object past the page foot",)
_assert_like_hancom(estimate_pages(_without_caches(data)), data, 2)


def test_a_top_and_bottom_table_not_split_offset_past_the_page_foot_is_unsupported() -> None:
Expand Down
Loading