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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

### 고침

- 쪽 수 추정(실험, `estimate_pages`)이 줄 캐시 없는 표 칸에서 위아래 배치 표 하나가 든 문단을 지원 밖
(`a nested table`)으로 두던 것을 고친다. 이제 그림·도형처럼 칸이 그 표의 아래 끝(행으로 잰 표 높이와 바깥 여백)까지
닿고, 칸의 다음 줄은 그 아래에서 이어진다. 위로 띄운 표는 문단 윗선에 선다. 그런 행이 쪽을 넘어 줄 사이에서
나뉠 때도 표는 그 높이의 한 줄로 센다. 글 앞에 둔 그런 표는 거기 닿는 첫 줄부터 표 아래로 보낸다. 한 문단에 표가
둘이면 한/글은 빈 줄을 아래 표 밑으로 보내므로 전처럼 캐시가 있어야 한다.
- 쪽 수 추정(실험, `estimate_pages`)이 칸 단위로 나누는 흐르는 표(`pageBreak="CELL"`)에서 병합 칸으로 이어진
행들이 한 쪽보다 길면 "rows merged together taller than a page"로 거부하던 것을 고친다. 한/글은 쪽 끝마다
다시 나눈다. 칸마다 그 쪽에 들어가는 줄까지 두고 나머지는 다음 쪽 머리 행 아래로 넘기며, 넘긴 부분이 다시
Expand Down
112 changes: 96 additions & 16 deletions src/hwpx/layout/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@
where it would stand in the unsplit cell, a first line below such a table with the room above it,
so the row splits again where it would). A picture or drawing placed top and bottom or wrapped
square from a cell paragraph holding no text makes the cell reach its foot (outer margins
included). A flowing table's rows use
included), and so does a table placed top and bottom from a cell paragraph holding nothing else,
without such caches, as tall as its rows (one placed up from the paragraph's top stands at that
top); what follows in the cell goes on below that foot. Before the paragraph's text, the first
line reaching the table and the lines after it go below it. A flowing table's rows use
the body only down to just above the page's foot (101 above it, or 2 in a table set not to be
adjusted), less the table's bottom outer margin: a row, or a cell line of a row split between its
lines, ending lower goes on to the next page (where the table goes on below its top outer margin;
Expand Down Expand Up @@ -135,7 +138,8 @@
starting past their
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
a nested table among text, or not set as a character but one top and bottom in its paragraph,
before any text (two there are laid out otherwise), 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 @@ -542,17 +546,17 @@ def stack(self, paragraphs: list[Any], width: int, caches: bool) -> tuple[int, i
count, size, pitch = 1, tall, tall + pitch - size
else:
cached = _cached_metrics(paragraph) if caches else ()
cached = cached or self.marked_lines(paragraph, runs, width) \
or self.mixed_lines(paragraph, runs, shape, width)
cached = cached or self.pushed_lines(paragraph, runs, width) \
or self.marked_lines(paragraph, runs, width) or self.mixed_lines(paragraph, runs, shape, width)
if cached: # the lines Hancom laid out, each as tall as it drew it
if pending is not None:
height += pending + shape.prev
top = height
height += sum(advance for _, advance in cached[:-1]) + cached[-1][0]
size, pitch = cached[-1]
pending = pitch - size + shape.next
if top + _objects_reach(runs) > height: # an object placed from the paragraph reaches lower
height, pending = top + _objects_reach(runs), shape.next
if top + _objects_reach(runs, self) > height: # an object placed from it reaches lower
height, pending = top + _objects_reach(runs, self), shape.next
lines += len(cached)
continue
style = self.style(paragraph.get("paraPrIDRef"), refs, paragraph)
Expand All @@ -564,8 +568,8 @@ def stack(self, paragraphs: list[Any], width: int, caches: bool) -> tuple[int, i
top = height
height += (count - 1) * pitch + size
pending = pitch - size + shape.next
if top + _objects_reach(runs) > height: # an object placed from the paragraph reaches lower
height, pending = top + _objects_reach(runs), shape.next
if top + _objects_reach(runs, self) > height: # an object placed from the paragraph reaches lower
height, pending = top + _objects_reach(runs, self), shape.next
lines += count
return height, lines, pitch, size

Expand Down Expand Up @@ -596,14 +600,47 @@ def stack_lines(self, paragraphs: list[Any], width: int, caches: bool) -> tuple[
runs = paragraph.findall(f"{HP}run")
cached = () if _table_alone(runs) is not None or not caches else _cached_metrics(paragraph)
if not cached and _table_alone(runs) is None: # each line as tall as stack makes it
cached = self.marked_lines(paragraph, runs, width) or self.mixed_lines(paragraph, runs, shape, width)
cached = self.pushed_lines(paragraph, runs, width) or self.marked_lines(paragraph, runs, width) \
or self.mixed_lines(paragraph, runs, shape, width)
if cached:
metrics += list(cached[:-1]) + [(cached[-1][0], cached[-1][1] + shape.next)]
continue
height, count, pitch, size = self.stack([paragraph], width, caches)
metrics += [(size, pitch)] * (count - 1) + [(height - (count - 1) * pitch, pitch + shape.next)]
last = height - (count - 1) * pitch # down to the foot of an object placed from it: the next below
metrics += [(size, pitch)] * (count - 1) + [(last, (last if last > size else pitch) + shape.next)]
return tuple(metrics)

def pushed_lines(self, paragraph: Any, runs: list[Any], width: int) -> tuple[tuple[int, int], ...]:
"""(height, advance) of each line of a cell paragraph without a layout cache whose text follows a table
placed top and bottom from its top: the first line reaching the table and the lines after it go below
it, and the paragraph reaches down to the table's foot at least; empty for any other paragraph."""

table = _table_before_text(runs)
if table is None:
return ()
top = _down(table.find(f"{HP}pos"))
foot = top + _inline_table_height(self, table) + _extent_margins(table)
shape = self.shape(paragraph.get("paraPrIDRef"))
lines = list(self.marked_lines(paragraph, runs, width) or self.mixed_lines(paragraph, runs, shape, width))
if not lines:
size, refs, _ = _text_size(self, runs)
style = self.style(paragraph.get("paraPrIDRef"), refs, paragraph)
count = self.lines(_run_text(runs), _line_widths(shape, width, style), size, style)
lines = [(size, _pitch(shape.kind, shape.value, size))] * count
y = 0
for index, (height, advance) in enumerate(lines):
if y < foot and y + height > top: # it reaches the table: below it, and the lines after it
if index:
lines[index - 1] = (lines[index - 1][0], lines[index - 1][1] + foot - y)
else: # the first line takes the room above it
lines[0] = (height + foot - y, advance + foot - y)
break
y += advance
above = sum(advance for _, advance in lines[:-1])
if above + lines[-1][0] < foot: # every line above the table: the paragraph holds it
lines[-1] = (foot - above, foot - above)
return tuple(lines)

def marked_lines(self, paragraph: Any, runs: list[Any], width: int) -> tuple[tuple[int, int], ...]:
"""(height, advance) of each line of a paragraph holding composed characters, ruby text, or objects
set as characters among its text or several of them, laid out at *width* as in the body; empty for
Expand Down Expand Up @@ -826,7 +863,11 @@ def _cell_row(measure: _Measure, table: Any, cell: Any) -> _Row:
if nested and any(paragraph.find(f".//{HP}tbl") is not None
and _table_alone(paragraph.findall(f"{HP}run")) is None for paragraph in paragraphs):
drawn = _drawn_lines(measure, paragraphs) # among text, or not set as a character: as Hancom drew it
if not drawn:
if not drawn and not all(_table_on_its_own(paragraph)
or _table_before_text(paragraph.findall(f"{HP}run")) is not None
for paragraph in paragraphs
if paragraph.find(f".//{HP}tbl") is not None
and _table_alone(paragraph.findall(f"{HP}run")) is None):
raise _Unsupported("a nested table")
size = cell.find(f"{HP}cellSz")
margins = cell_margins_of(cell, table)
Expand Down Expand Up @@ -1058,9 +1099,10 @@ def _caption(measure: _Measure, table: Any) -> tuple[int, int]:


def _inline_table_height(measure: _Measure, table: Any) -> int:
"""A table set as a character: its rows as the estimate measures them. When the row model does not
follow the table (merged rows, a nested table) but every paragraph in it keeps a valid layout cache,
Hancom laid it out as it is, and the height it saved (hp:sz) is the height it draws."""
"""A table set as a character (or placed top and bottom in a cell): its rows as the estimate measures
them. When the row model does not follow the table (merged rows, a nested table) but every paragraph in
it keeps a valid layout cache, Hancom laid it out as it is, and the height it saved (hp:sz) is the height
it draws."""

if table.find(f".//{HP}tbl") is not None or any(_row_span(tc) != 1 for tc in table.iter(f"{HP}tc")):
paragraphs = list(table.iter(f"{HP}p"))
Expand Down Expand Up @@ -1108,16 +1150,20 @@ def _check_section(section: Any) -> None:
raise _Unsupported("section settings (hp:secPr) after the first paragraph")


def _objects_reach(runs: list[Any]) -> int:
def _objects_reach(runs: list[Any], measure: _Measure) -> int:
"""How far below the top of their paragraph in a cell, holding no text, the pictures and drawings placed
from it reach: top and bottom or square, not set as a character, their outer margins included (the cell
holds them, and its row grows to). Tables in a cell are laid out on their own."""
holds them, and its row grows to). So do the tables placed top and bottom from it, as tall as their rows
(one placed up stands at the paragraph's top)."""

reach = 0
if _run_text(runs).strip(): # text goes on below such an object: not followed here
return reach
for obj in (child for run in runs for child in run):
name, pos = _local(obj), obj.find(f"{HP}pos")
if name == "tbl" and _placed_top_and_bottom(obj):
reach = max(reach, _down(pos) + _inline_table_height(measure, obj) + _extent_margins(obj))
continue
if name not in _OBJECTS or name == "tbl" or pos is None or obj.find(f"{HP}sz") is None \
or pos.get("treatAsChar") == "1" or obj.get("textWrap") not in ("TOP_AND_BOTTOM", "SQUARE") \
or pos.get("vertRelTo") != "PARA" or pos.get("vertAlign", "TOP") != "TOP":
Expand All @@ -1129,6 +1175,40 @@ def _objects_reach(runs: list[Any]) -> int:
return reach


def _placed_top_and_bottom(obj: Any) -> bool:
"""An object placed top and bottom from its paragraph's top, not set as a character."""

pos = obj.find(f"{HP}pos")
return pos is not None and pos.get("treatAsChar") != "1" and obj.get("textWrap") == "TOP_AND_BOTTOM" \
and pos.get("vertRelTo") == "PARA" and pos.get("vertAlign", "TOP") == "TOP"


def _table_on_its_own(paragraph: Any) -> bool:
"""Whether a cell paragraph holds no text and nothing but one table placed top and bottom from its top
(Hancom sets the empty line below two of them, not beside them)."""

runs = paragraph.findall(f"{HP}run")
objects = [child for run in runs for child in run if _local(child) in _OBJECTS]
return not _run_text(runs).strip() and len(objects) == 1 and _local(objects[0]) == "tbl" \
and _placed_top_and_bottom(objects[0])


def _table_before_text(runs: list[Any]) -> Any:
"""The table placed top and bottom from its paragraph's top that is all the runs hold but text after it
(text), or ``None``."""

objects = [child for run in runs for child in run if _local(child) in _OBJECTS]
if len(objects) != 1 or _local(objects[0]) != "tbl" or not _placed_top_and_bottom(objects[0]) \
or not _run_text(runs).strip():
return None
for child in (child for run in runs for child in run):
if child is objects[0]:
return child
if _local(child) == "t" and _t_text(child).strip(): # text before it
return None
return None


def _placed_objects(runs: list[Any]) -> list[Any]:
"""The objects of the runs."""

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading
Loading