Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

### 고침

- 쪽 수 추정(실험, `estimate_pages`)이 줄 캐시 없는 표 칸 문단에서 글자처럼 둔 개체(그림·도형·수식 등)를 세지 않던
것을 고친다. 전에는 칸 문단에 혼자 든 표만 그 높이로 셌고, 다른 개체는 없는 것처럼 줄을 세어 행이 한/글보다
낮았다. 이제 개체 하나만 든 문단은 개체 높이의 한 줄이고 줄 간격은 글자 크기로 센다. 글 사이에 있거나 여럿이면
본문처럼 개체의 폭과 높이로 줄 안에 놓는다.
- 쪽 수 추정(실험, `estimate_pages`)이 `hp:t` 글 안에 글자로 든 전각 공백(U+3000)을 고정폭 빈칸(`hp:fwSpace`)으로
보던 것을 고친다(python-hwpx는 둘을 모두 U+3000으로 읽는다). 글자 전각 공백은 글꼴의 폭(대개 글자 크기만큼)을
차지하고, 그 뒤에서 줄이 나뉘며, 줄 끝에서는 몇 개가 이어져도 모두 매달린다. 전에는 1/4 폭의 고정폭 빈칸으로
Expand Down
33 changes: 29 additions & 4 deletions src/hwpx/layout/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@
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
there, spaced like the text, and a row holding one splits between its cell's lines, each as tall
as it is; a nested table among text, placed top and bottom or wrapped square is followed through
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
the body; a nested table among text, placed top and bottom or wrapped square is followed through
the layout caches of its cell, as tall as Hancom drew it (down to such a table's foot; one placed
up from its paragraph's top stands at that top; the caches of a row Hancom split over a page end
start over at the next page's top, and are read as one run of lines, each line that goes back up
Expand Down Expand Up @@ -527,9 +529,13 @@ def stack(self, paragraphs: list[Any], width: int, caches: bool) -> tuple[int, i
shape = self.shape(paragraph.get("paraPrIDRef"))
pitch = _pitch(shape.kind, shape.value, size)
table = _table_alone(runs)
alone = None if table is not None or caches and _cached_metrics(paragraph) else _object_alone(runs)
if table is not None: # one line as tall as the table, spaced like the text
tall = _inline_table_height(self, table) + _extent_margins(table)
count, size, pitch = 1, tall, tall + pitch - size
elif alone is not None: # so with another object set as a character
tall = _object_extent(alone, self)[1]
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) \
Expand Down Expand Up @@ -595,10 +601,11 @@ def stack_lines(self, paragraphs: list[Any], width: int, caches: bool) -> tuple[
return tuple(metrics)

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 or ruby text, laid
out at *width* as in the body; empty for a paragraph holding neither."""
"""(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
a paragraph holding none of them."""

if not _marks(runs):
if not _marks(runs) and not _objects_among(runs):
return ()
shape = self.shape(paragraph.get("paraPrIDRef"))
_check_ruby_spacing(runs, shape)
Expand Down Expand Up @@ -712,6 +719,24 @@ def _row_span(cell: Any) -> int:
return 1 if span is None else int(span.get("rowSpan", 1))


def _object_alone(runs: list[Any]) -> Any:
"""The object set as a character, not a table, that is all a paragraph holds (but objects in front of or
behind the text), or ``None``."""

objects = _placed_objects(runs)
if len(objects) != 1 or _local(objects[0]) == "tbl" or _run_text(runs).strip():
return None
return objects[0] if objects[0].find(f"{HP}pos").get("treatAsChar") == "1" else None


def _objects_among(runs: list[Any]) -> bool:
"""Whether the runs hold objects set as characters, and nothing else placed, among text or several."""

objects = _placed_objects(runs)
return bool(objects) and (len(objects) > 1 or bool(_run_text(runs).strip())) and all(
obj.find(f"{HP}pos").get("treatAsChar") == "1" for obj in objects)


def _table_alone(runs: list[Any]) -> Any:
"""The table set as a character that is all a paragraph holds, or ``None``."""

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.
Binary file not shown.
12 changes: 12 additions & 0 deletions tests/test_layout_page_estimate.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@
# line goes below it, as a line of text would
"pages_object_pushing_characters_off1398": 1, # the same 1398 down
"pages_object_pushing_characters_off1398_char3000": 1, # with a rectangle 3000 tall set as a character
# Objects set as characters in a paragraph of a cell of a flowing table, 10 pt text spaced 160%:
"pages_cell_picture_as_character_alone": 1, # a picture 3000 x 4000 alone: one line 4000 tall
"pages_cell_rectangle_as_character_alone": 1, # a rectangle 3000 x 2000
"pages_cell_equation_alone": 1, # an equation
"pages_cell_picture_before_text": 1, # the picture before two lines of text: the first line as tall
"pages_cell_picture_among_text": 1, # among them
"pages_cell_picture_after_text": 1, # after them: the second line as tall
"pages_cell_two_pictures_on_one_line": 1, # two that fit side by side
"pages_cell_two_pictures_on_two_lines": 1, # two that do not: a line each
"pages_cell_picture_alone_fixed_spacing": 1, # alone, line spacing fixed at 1600
"pages_cell_picture_in_a_table_as_character": 1, # alone, in a table set as a character
"pages_cell_picture_then_a_line": 1, # alone, then a paragraph of a line: spaced from the text size
# A section hiding a page's first empty lines (hp:visibility@hideFirstEmptyLine), its first page full:
"pages_hide_empty_lines_one": 2, # an empty paragraph past the foot stays there, the text after it
# starts the next page
Expand Down
Loading