diff --git a/CHANGELOG.md b/CHANGELOG.md index f2b30eda..06ce309a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ ### 고침 +- 쪽 수 추정(실험, `estimate_pages`)이 같은 폭의 여러 단 쪽에서 종이 기준으로 둔 위아래 배치 개체가 본문 너비를 + 다 덮지 않으면 지원 밖(`objects placed on the paper`)으로 두던 것을 고친다. 한/글은 개체가 가로로 걸치는 + 단에서만 줄을 개체 아래로 보내고, 걸치지 않는 단의 줄은 개체가 없을 때 자리에 둔다. 이제 종이의 왼쪽·가운데· + 오른쪽을 기준으로 둔 개체는 바깥 여백까지 쳐서 걸치는 단마다 그렇게 놓는다. - `hwpx.experimental.latex_to_eqedit`(실험)가 `e^{it}`, `x_{rm}`처럼 글자가 `rm`·`it`·`bold`가 되는 식별자를 그대로 쓰던 것을 고친다. 한/글은 이 낱말을 글꼴 전환으로 읽어서, 첨자에 홀로 오면 아무것도 그리지 않는다. 이제 다른 예약어처럼 따옴표로 감싸 쓰고(`e ^{"it"}`), `eqedit_to_latex`는 그것을 다시 글자로 읽는다. diff --git a/src/hwpx/layout/pages.py b/src/hwpx/layout/pages.py index 20faacbb..34c6d993 100644 --- a/src/hwpx/layout/pages.py +++ b/src/hwpx/layout/pages.py @@ -36,8 +36,9 @@ without it, wherever it stands; one placed top and bottom from the top or the bottom of the paper or of the page (its body) keeps every line of its page out of its band (a line reaching it, in any paragraph on that page, goes below it -- on to the next page when the band reaches the body's - foot -- and the lines after follow; with several columns, in each of them when it covers the - text's whole width), and so does one wrapped square there that leaves less than a line's room + foot -- and the lines after follow; with several columns of equal width, in each column it + reaches over from the paper's left, centre or right, the others running past it), and so does one + wrapped square there that leaves less than a line's room (1440) on either side of it across the text; one that leaves more (in one column) changes nothing when no line, table or object of its page reaches its band, and is not followed otherwise. An object anchored at the top of a paragraph's first line that such a band pushes down stands right @@ -922,9 +923,9 @@ class _Para: #: the line the object laid out around the text (the caller's) stands on wrap_anchor: int = 0 band: _Band | None = None - #: (top, bottom) in the body of an object anchored here and placed on the paper or the page, and whether - #: it leaves a line's room beside it (see :func:`_paper_band`) - paper: tuple[int, int, bool] | None = None + #: (top, bottom) in the body of an object anchored here and placed on the paper or the page, whether it + #: leaves a line's room beside it and the columns it reaches over (see :func:`_paper_band`) + paper: tuple[int, int, bool, tuple[int, ...]] | None = None #: in columns of unequal width, a paragraph without a layout cache: its text, to break again reflow: _Reflow | None = None #: holding nothing, in a section hiding a page's first empty lines (see :meth:`_Paginator._paragraph`) @@ -993,6 +994,7 @@ class _Page: widths: tuple[int, ...] = () # then each column's width paper_height: int = 0 hide_empty: bool = False # hp:visibility@hideFirstEmptyLine: a page's first empty lines are hidden + gap: int = 0 # between columns of equal width def _page(section: Any) -> _Page: @@ -1003,17 +1005,17 @@ def _page(section: Any) -> _Page: width, height = _drawn_page_size(int(page.get("width", 0)), int(page.get("height", 0)), page.get("landscape")) body = height - sum(int(margin.get(key, 0)) for key in ("top", "bottom", "header", "footer")) text_width = width - sum(int(margin.get(key, 0)) for key in ("left", "right", "gutter")) - columns, column_width, widths = _columns(section, text_width) + columns, column_width, widths, gap = _columns(section, text_width) visibility = next(section.iter(f"{HP}visibility"), None) return _Page(body, column_width, columns, int(margin.get("top", 0)) + int(margin.get("header", 0)), int(margin.get("left", 0)) + int(margin.get("gutter", 0)), text_width, width, bool(widths), widths, height, - visibility is not None and visibility.get("hideFirstEmptyLine") in ("1", "true")) + visibility is not None and visibility.get("hideFirstEmptyLine") in ("1", "true"), gap) -def _columns(section: Any, text_width: int) -> tuple[int, int, tuple[int, ...]]: - """(count, width, widths): the section's columns, their width (the narrowest when they differ) and, - when they differ, each one's.""" +def _columns(section: Any, text_width: int) -> tuple[int, int, tuple[int, ...], int]: + """(count, width, widths, gap): the section's columns, their width (the narrowest when they differ), + when they differ each one's, and when they do not the gap between them.""" # Column settings in a cell or a text box (an hp:subList) belong to that list, not the section. settings = [cols for cols in section.iter(f"{HP}colPr") if not _in_sub_list(cols)] @@ -1021,16 +1023,16 @@ def _columns(section: Any, text_width: int) -> tuple[int, int, tuple[int, ...]]: raise _Unsupported("the columns change inside the section") count = int(settings[0].get("colCount", "1")) if settings else 1 if count <= 1: - return 1, text_width, () + return 1, text_width, (), 0 if settings[0].get("sameSz") != "1": # each column takes its share of the width with the gaps (hp:colSz), sizes = settings[0].findall(f"{HP}colSz") # rounded total = sum(int(size.get("width", 0)) + int(size.get("gap", 0)) for size in sizes) if len(sizes) != count or total <= 0: raise _Unsupported("columns of unequal width") widths = tuple((2 * int(size.get("width", 0)) * text_width + total) // (2 * total) for size in sizes) - return count, min(widths), widths + return count, min(widths), widths, 0 gap = int(settings[0].get("sameGap", 0)) - return count, (text_width - (count - 1) * gap) // count // 4 * 4, () + return count, (text_width - (count - 1) * gap) // count // 4 * 4, (), gap def _in_sub_list(element: Any) -> bool: @@ -1182,10 +1184,11 @@ def _on_paper(obj: Any) -> bool: and pos.get("vertRelTo") in ("PAPER", "PAGE") and pos.get("vertAlign", "TOP") in ("TOP", "BOTTOM")) -def _paper_band(measure: _Measure, page: _Page, paragraph: Any) -> tuple[int, int, bool] | None: - """(top, bottom) in the body of the object of *paragraph* placed on the paper, if any, and whether it - is wrapped square leaving a line's room beside it: Hancom sets the text reaching it beside it, which is - not followed, and the text above or below it as if it were not there.""" +def _paper_band(measure: _Measure, page: _Page, paragraph: Any) -> tuple[int, int, bool, tuple[int, ...]] | None: + """(top, bottom) in the body of the object of *paragraph* placed on the paper, if any, whether it is + wrapped square leaving a line's room beside it (Hancom sets the text reaching it beside it, which is + not followed, and the text above or below it as if it were not there), and the columns it keeps the + lines out of.""" placed = [child for run in paragraph.findall(f"{HP}run") for child in run if _local(child) in _OBJECTS and _on_paper(child)] @@ -1193,7 +1196,9 @@ def _paper_band(measure: _Measure, page: _Page, paragraph: Any) -> tuple[int, in return None square = placed[0].get("textWrap") == "SQUARE" # no line beside it: less than a line's room on each side across = _across_the_text(placed[0], page, _MIN_SIDE - 1 if square else 0) - if len(placed) > 1 or (page.columns > 1 and not across): + columns = tuple(range(page.columns)) if across or page.columns == 1 else () if square \ + else _columns_reached(placed[0], page) + if len(placed) > 1 or not columns: raise _Unsupported("objects placed on the paper") obj = placed[0] tall = _extent(obj, "height") @@ -1205,20 +1210,40 @@ def _paper_band(measure: _Measure, page: _Page, paragraph: Any) -> tuple[int, in top = offset if pos.get("vertAlign", "TOP") == "TOP" else page.body - offset - tall else: # from the paper's top or bottom top = (offset if pos.get("vertAlign", "TOP") == "TOP" else page.paper_height - offset - tall) - page.top - return top, top + tall, square and not across + return top, top + tall, square and not across, columns def _across_the_text(obj: Any, page: _Page, slack: int = 0) -> bool: """Whether an object placed from the paper's left covers the text's whole width (every column), but for *slack* on either side.""" + left = _paper_left(obj, page) + return left is not None and left <= page.left + slack \ + and left + _extent(obj, "width") >= page.left + page.text_width - slack + + +def _columns_reached(obj: Any, page: _Page) -> tuple[int, ...]: + """The columns of equal width that an object placed from the paper's left reaches over, its outer margins + included.""" + + left = _paper_left(obj, page) + if left is None or page.unequal: + return () + right = left + _extent(obj, "width") + starts = [page.left + index * (page.column_width + page.gap) for index in range(page.columns)] + return tuple(index for index, start in enumerate(starts) if left < start + page.column_width and right > start) + + +def _paper_left(obj: Any, page: _Page) -> int | None: + """How far right of the paper's left edge an object placed from the paper's left, centre or right stands; + ``None`` for one placed otherwise.""" + pos = obj.find(f"{HP}pos") if pos.get("horzRelTo") != "PAPER": - return False + return None width, offset = _extent(obj, "width"), int(pos.get("horzOffset", 0)) - left = {"LEFT": offset, "CENTER": (page.paper_width - width) // 2 + offset, + return {"LEFT": offset, "CENTER": (page.paper_width - width) // 2 + offset, "RIGHT": page.paper_width - width - offset}.get(pos.get("horzAlign", "LEFT")) - return left is not None and left <= page.left + slack and left + width >= page.left + page.text_width - slack def _floating(obj: Any) -> bool: @@ -2429,10 +2454,10 @@ def _lay_section(measure: _Measure, section: Any) -> _SectionLayout: firsts.append(firsts[-1] + count) placed: dict[int, list[tuple[int, int]]] = {} beside: dict[int, list[tuple[int, int]]] = {} - for index, (top, bottom, room) in paper.items(): # on every column of the page + for index, (top, bottom, room, columns) in paper.items(): # on the columns of the page it reaches over first = paginator.out[firsts[index]][0] // page.columns * page.columns - for frame in range(first, first + page.columns): - (beside if room else placed).setdefault(frame, []).append((top, bottom)) + for column in columns: + (beside if room else placed).setdefault(first + column, []).append((top, bottom)) if placed == bands and beside == sides: break bands, sides = placed, beside diff --git a/tests/fixtures/hancom_saved/pages_rectangle_on_paper_100_into_the_second_column.hwpx b/tests/fixtures/hancom_saved/pages_rectangle_on_paper_100_into_the_second_column.hwpx new file mode 100644 index 00000000..24878ea7 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_rectangle_on_paper_100_into_the_second_column.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_rectangle_on_paper_400_narrower_than_two_columns.hwpx b/tests/fixtures/hancom_saved/pages_rectangle_on_paper_400_narrower_than_two_columns.hwpx new file mode 100644 index 00000000..50c191c0 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_rectangle_on_paper_400_narrower_than_two_columns.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_rectangle_on_paper_ending_in_the_column_gap.hwpx b/tests/fixtures/hancom_saved/pages_rectangle_on_paper_ending_in_the_column_gap.hwpx new file mode 100644 index 00000000..97fa34cc Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_rectangle_on_paper_ending_in_the_column_gap.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_rectangle_on_paper_over_the_left_column.hwpx b/tests/fixtures/hancom_saved/pages_rectangle_on_paper_over_the_left_column.hwpx new file mode 100644 index 00000000..9cf6dcd3 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_rectangle_on_paper_over_the_left_column.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_rectangle_on_paper_over_the_left_column_from_the_right.hwpx b/tests/fixtures/hancom_saved/pages_rectangle_on_paper_over_the_left_column_from_the_right.hwpx new file mode 100644 index 00000000..9152a181 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_rectangle_on_paper_over_the_left_column_from_the_right.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_rectangle_on_paper_over_the_middle_of_three_columns.hwpx b/tests/fixtures/hancom_saved/pages_rectangle_on_paper_over_the_middle_of_three_columns.hwpx new file mode 100644 index 00000000..48d7ecf1 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_rectangle_on_paper_over_the_middle_of_three_columns.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_rectangle_on_paper_over_the_right_column.hwpx b/tests/fixtures/hancom_saved/pages_rectangle_on_paper_over_the_right_column.hwpx new file mode 100644 index 00000000..b8031df0 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_rectangle_on_paper_over_the_right_column.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_rectangle_on_paper_partly_over_two_columns.hwpx b/tests/fixtures/hancom_saved/pages_rectangle_on_paper_partly_over_two_columns.hwpx new file mode 100644 index 00000000..bef6dbe5 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_rectangle_on_paper_partly_over_two_columns.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_table_on_paper_400_narrower_than_two_columns.hwpx b/tests/fixtures/hancom_saved/pages_table_on_paper_400_narrower_than_two_columns.hwpx new file mode 100644 index 00000000..3cb0da2c Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_table_on_paper_400_narrower_than_two_columns.hwpx differ diff --git a/tests/test_layout_page_estimate.py b/tests/test_layout_page_estimate.py index 4cb78ff1..f2e8f558 100644 --- a/tests/test_layout_page_estimate.py +++ b/tests/test_layout_page_estimate.py @@ -293,6 +293,18 @@ "pages_table_on_paper_pushes_a_later_line": 1, # a table 40000 below it: a line of a later paragraph "pages_rectangle_on_paper_across_two_columns": 2, # across both columns, anchored in the second: # the lines of both columns go below it + "pages_rectangle_on_paper_over_the_left_column": 2, # 15000 wide over the first of two columns: its + # lines go below it, the second's start at the top + "pages_rectangle_on_paper_over_the_right_column": 2, # over the second only + "pages_rectangle_on_paper_partly_over_two_columns": 2, # 30000 wide, centred: partly over each, the + # lines of both go below it + "pages_rectangle_on_paper_400_narrower_than_two_columns": 2, # 400 narrower than the text, centred + "pages_table_on_paper_400_narrower_than_two_columns": 2, # a table so + "pages_rectangle_on_paper_over_the_left_column_from_the_right": 2, # anchored in the second column + "pages_rectangle_on_paper_over_the_middle_of_three_columns": 2, # the middle one of three only + "pages_rectangle_on_paper_100_into_the_second_column": 2, # its right edge 100 into the second: both + "pages_rectangle_on_paper_ending_in_the_column_gap": 2, # its right edge 100 short of the second, in + # the gap: the first only "pages_rectangle_on_page_5000_below_its_top": 2, # top and bottom 5000 below the body's top "pages_rectangle_on_page_5000_above_its_foot": 2, # 5000 above the body's foot: the lines reaching it # go below it @@ -607,7 +619,7 @@ def test_columns_of_unequal_width_take_their_share_of_the_text_width_rounded() - with zipfile.ZipFile(FIXTURES / "pages_columns_unequal_three.hwpx") as package: section = etree.fromstring(package.read("Contents/section0.xml")) - assert page_layout._columns(section, 42520) == (3, 8502, (8502, 15875, 15878)) + assert page_layout._columns(section, 42520) == (3, 8502, (8502, 15875, 15878), 0) # no gap: unequal widths assert {int(seg.get("horzsize")) for seg in section.iter(f"{HP}lineseg")} == {8502, 15875, 15878}