diff --git a/CHANGELOG.md b/CHANGELOG.md index aca8cc54..a780abb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ ### 고침 +- 쪽 수 추정(실험, `estimate_pages`)이 줄 캐시 없는 표 칸 문단에서 글자처럼 둔 개체(그림·도형·수식 등)를 세지 않던 + 것을 고친다. 전에는 칸 문단에 혼자 든 표만 그 높이로 셌고, 다른 개체는 없는 것처럼 줄을 세어 행이 한/글보다 + 낮았다. 이제 개체 하나만 든 문단은 개체 높이의 한 줄이고 줄 간격은 글자 크기로 센다. 글 사이에 있거나 여럿이면 + 본문처럼 개체의 폭과 높이로 줄 안에 놓는다. - 쪽 수 추정(실험, `estimate_pages`)이 `hp:t` 글 안에 글자로 든 전각 공백(U+3000)을 고정폭 빈칸(`hp:fwSpace`)으로 보던 것을 고친다(python-hwpx는 둘을 모두 U+3000으로 읽는다). 글자 전각 공백은 글꼴의 폭(대개 글자 크기만큼)을 차지하고, 그 뒤에서 줄이 나뉘며, 줄 끝에서는 몇 개가 이어져도 모두 매달린다. 전에는 1/4 폭의 고정폭 빈칸으로 diff --git a/src/hwpx/layout/pages.py b/src/hwpx/layout/pages.py index 1468ce81..20faacbb 100644 --- a/src/hwpx/layout/pages.py +++ b/src/hwpx/layout/pages.py @@ -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 @@ -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) \ @@ -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) @@ -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``.""" diff --git a/tests/fixtures/hancom_saved/pages_cell_equation_alone.hwpx b/tests/fixtures/hancom_saved/pages_cell_equation_alone.hwpx new file mode 100644 index 00000000..b4b37255 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_cell_equation_alone.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_cell_picture_after_text.hwpx b/tests/fixtures/hancom_saved/pages_cell_picture_after_text.hwpx new file mode 100644 index 00000000..05040ad7 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_cell_picture_after_text.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_cell_picture_alone_fixed_spacing.hwpx b/tests/fixtures/hancom_saved/pages_cell_picture_alone_fixed_spacing.hwpx new file mode 100644 index 00000000..cf98b46b Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_cell_picture_alone_fixed_spacing.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_cell_picture_among_text.hwpx b/tests/fixtures/hancom_saved/pages_cell_picture_among_text.hwpx new file mode 100644 index 00000000..7ee1cbdb Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_cell_picture_among_text.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_cell_picture_as_character_alone.hwpx b/tests/fixtures/hancom_saved/pages_cell_picture_as_character_alone.hwpx new file mode 100644 index 00000000..c21cd3d8 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_cell_picture_as_character_alone.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_cell_picture_before_text.hwpx b/tests/fixtures/hancom_saved/pages_cell_picture_before_text.hwpx new file mode 100644 index 00000000..eddb7e90 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_cell_picture_before_text.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_cell_picture_in_a_table_as_character.hwpx b/tests/fixtures/hancom_saved/pages_cell_picture_in_a_table_as_character.hwpx new file mode 100644 index 00000000..1fe3cee4 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_cell_picture_in_a_table_as_character.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_cell_picture_then_a_line.hwpx b/tests/fixtures/hancom_saved/pages_cell_picture_then_a_line.hwpx new file mode 100644 index 00000000..b7838b12 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_cell_picture_then_a_line.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_cell_rectangle_as_character_alone.hwpx b/tests/fixtures/hancom_saved/pages_cell_rectangle_as_character_alone.hwpx new file mode 100644 index 00000000..be459035 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_cell_rectangle_as_character_alone.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_cell_two_pictures_on_one_line.hwpx b/tests/fixtures/hancom_saved/pages_cell_two_pictures_on_one_line.hwpx new file mode 100644 index 00000000..522d4896 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_cell_two_pictures_on_one_line.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_cell_two_pictures_on_two_lines.hwpx b/tests/fixtures/hancom_saved/pages_cell_two_pictures_on_two_lines.hwpx new file mode 100644 index 00000000..034cddcc Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_cell_two_pictures_on_two_lines.hwpx differ diff --git a/tests/test_layout_page_estimate.py b/tests/test_layout_page_estimate.py index a5395122..4cb78ff1 100644 --- a/tests/test_layout_page_estimate.py +++ b/tests/test_layout_page_estimate.py @@ -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