diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cae9b4b..d8d19cba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ ### 고침 +- 쪽 수 추정(실험, `estimate_pages`)이 문단 글 뒤에 놓인 어울림(SQUARE) 개체를 지원 밖 + (`... wrapped square after text`)으로 두던 것을, 그 개체의 조판 부호가 문단 첫 줄에 있을 때 고친다. + - 한/글은 개체의 문단 기준 세로 자리를 부호가 놓인 줄(문단을 단 전체 폭으로 놓았을 때)의 윗선에서 잰다. + 부호가 첫 줄에 있으면 문단 윗선이라, 글 앞에 놓인 개체와 같다. 띠에 닿는 줄은 그 문단의 줄도 곁으로 간다. + - 첫 줄은 FormFit으로 문단을 단 전체 폭에서 나누어 가린다. 부호가 뒤 줄에 있으면 전처럼 지원 밖이다. - 쪽 수 추정(실험, `estimate_pages`)이 글과 함께 움직이고 나뉠 수 있는 어울림(SQUARE) 표가 본문 바닥을 넘을 때, 줄 캐시가 없으면 지원 밖으로 두던 것을 고친다. - 한/글은 그런 표를 흐르는 표처럼 쪽 끝에서 행 사이나 칸 줄 사이로 나눈다. 표가 걸친 쪽마다 곁 줄은 그 쪽 diff --git a/src/hwpx/layout/pages.py b/src/hwpx/layout/pages.py index ff81f213..4a9955f4 100644 --- a/src/hwpx/layout/pages.py +++ b/src/hwpx/layout/pages.py @@ -170,7 +170,7 @@ import copy import os -from collections.abc import Collection +from collections.abc import Callable, Collection from dataclasses import dataclass, field, replace from typing import TYPE_CHECKING, Any @@ -2007,9 +2007,12 @@ def _wraps_top_and_bottom(obj: Any, column: int) -> bool: return start < _MIN_SIDE and column - start - width < _MIN_SIDE -def _square_object(objects: list[Any], runs: list[Any], column: int) -> Any: +def _square_object(objects: list[Any], runs: list[Any], column: int, + on_first_line: Callable[[Any], bool] | None = None) -> Any: """The one object of a paragraph wrapped square across the column from the paragraph's top, before - any text, or ``None`` (:func:`_square_sides` says where the text goes beside it).""" + any text or after text that *on_first_line* says ends on the paragraph's first line (Hancom places it from + the top of the line its control falls on, the paragraph laid out the column's whole width), or ``None`` + (:func:`_square_sides` says where the text goes beside it).""" if len(objects) != 1: return None @@ -2025,10 +2028,29 @@ def _square_object(objects: list[Any], runs: list[Any], column: int) -> Any: if child is obj: break if _local(child) == "t" and _t_text(child): - raise _Unsupported(f"{_local(obj)} wrapped square after text") + if on_first_line is None or not on_first_line(obj): + raise _Unsupported(f"{_local(obj)} wrapped square after text") + break return obj +def _on_first_line(measure: _Measure, page: _Page, paragraph: Any, runs: list[Any], obj: Any) -> bool: + """Whether *obj*'s control falls on the paragraph's first line laid out the column's whole width (FormFit's + lines, the object taking no room in them).""" + + place = 0 + for child in (child for run in runs for child in run): + if child is obj: + break + if _local(child) == "t": + place += len(_t_text(child)) + size, refs, _ = _text_size(measure, runs) + style = measure.style(paragraph.get("paraPrIDRef"), refs, paragraph) + shape = measure.shape(paragraph.get("paraPrIDRef")) + starts = measure.line_starts(_run_text(runs), _line_widths(shape, page.column_width, style), size, style) + return all(start > place for start in starts[1:]) + + def _square_sides(obj: Any, page: _Page) -> tuple[int, int]: """How wide the text beside a square-wrapped object is on its left and on its right in the column: a side narrower than 1440, or one its text flow leaves out, takes none (0). Placed from the column, @@ -2082,7 +2104,8 @@ def _wrapped_paragraph(measure: _Measure, page: _Page, paragraph: Any, wrap: _Wr para = _paragraph(measure, page, paragraph, stacked=tuple(stacked)) shape = measure.shape(paragraph.get("paraPrIDRef")) return replace(para, stack=_stack(measure, stacked, page.column_width, shape)), None - square = _square_object(objects, runs, page.column_width) + square = _square_object(objects, runs, page.column_width, + lambda obj: _on_first_line(measure, page, paragraph, runs, obj)) pusher = _pushing_object(objects, _run_text(runs), page.column_width) if moved: # the object went on to the next page's top: the paragraph's lines as if it were not there # the object: wrapped square, pushing the lines below it, or alone below the empty line (a table that diff --git a/tests/fixtures/hancom_saved/pages_square_table_after_text.hwpx b/tests/fixtures/hancom_saved/pages_square_table_after_text.hwpx new file mode 100644 index 00000000..1372a3e6 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_square_table_after_text.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_square_table_after_text_2383_down.hwpx b/tests/fixtures/hancom_saved/pages_square_table_after_text_2383_down.hwpx new file mode 100644 index 00000000..6a3ad7e2 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_square_table_after_text_2383_down.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_square_table_after_text_on_the_second_line.hwpx b/tests/fixtures/hancom_saved/pages_square_table_after_text_on_the_second_line.hwpx new file mode 100644 index 00000000..8f2c6b0c Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_square_table_after_text_on_the_second_line.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_square_table_before_text.hwpx b/tests/fixtures/hancom_saved/pages_square_table_before_text.hwpx new file mode 100644 index 00000000..60ae213a Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_square_table_before_text.hwpx differ diff --git a/tests/test_layout_page_estimate.py b/tests/test_layout_page_estimate.py index 6a8f8e85..2fe9ddf4 100644 --- a/tests/test_layout_page_estimate.py +++ b/tests/test_layout_page_estimate.py @@ -62,6 +62,13 @@ "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 + # A table 30000 x 20000 wrapped square at the column's left from its paragraph 8000 down (10 pt text spaced + # 160%), its control after 16 characters, on the paragraph's first line laid out the column's whole width: + # Hancom places it from the paragraph's top as one before the text, the lines reaching it beside it: + "pages_square_table_after_text": 1, # the paragraph's own lines beside it, 12 characters a line + "pages_square_table_after_text_2383_down": 1, # placed 2383 down: the paragraph's line above it the + # whole width, the paragraphs after beside it + "pages_square_table_before_text": 1, # its control before the text: the same "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 @@ -1264,6 +1271,16 @@ def test_a_table_whose_row_addresses_skip_is_estimated_without_the_missing_rows( assert estimate.pages == 1 +def test_a_square_object_after_text_past_the_first_line_is_not_followed() -> None: + # The same table, its control after 60 characters: on the paragraph's second line laid out the column's + # whole width. Hancom places it from that line's top (9600), the lines from there on beside it. Not + # followed. + data = (FIXTURES / "pages_square_table_after_text_on_the_second_line.hwpx").read_bytes() + + assert estimate_pages(data).unsupported == ("section 0: tbl wrapped square after text",) + assert estimate_pages(_without_caches(data)).unsupported == ("section 0: tbl wrapped square after text",) + + def test_a_page_break_in_a_row_holding_a_table_beside_a_taller_cell_is_unsupported() -> None: # Row 1's cell (1, 0) holds more lines than the cell beside it holding a table; the page end falls # among them.