diff --git a/CHANGELOG.md b/CHANGELOG.md index 8852b181..778fc932 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ 끝은 줄에 자리를 차지하지 않는다. 전에는 필드 끝을 다음 줄로 보내 빈 줄을 하나 더 셌다. - 쪽 수 추정이 글 문단 첫 줄 아래로 놓인 나누지 않는 위아래 표가 쪽에 들어가지 않을 때, 한/글처럼 문단 줄은 들어가는 쪽에 두고 표만 다음 쪽 맨 위로 보낸다. 전에는 문단 줄까지 다음 쪽 표 아래로 보내 쪽 수가 하나 늘었다. +- 쪽 수 추정이 글자 모양의 위·아래 테두리를 줄 높이에 넣는다. 한/글은 테두리가 있는 글자(문단 끝 빈 run 포함)가 선 줄을 그 두께만큼 + 위아래로 키우고 줄 간격도 그 높이로 잰다. 그런 문단의 줄 캐시는 쓰지 않는다. ## [6.11.0] - 2026-11-02 diff --git a/src/hwpx/layout/line_cache.py b/src/hwpx/layout/line_cache.py index 7fcd6340..cd547d39 100644 --- a/src/hwpx/layout/line_cache.py +++ b/src/hwpx/layout/line_cache.py @@ -468,6 +468,8 @@ def _lines(measure: Any, paragraph: Any, width: int, *, first: bool, size, refs, sizes = _pages._text_size(measure, runs) if headed and not sizes and _pages._text_size(measure, runs, paragraph)[0] > size: return None # an empty paragraph under a taller number label (its line as tall as that): left for Hancom + if any(measure.char_box(run.get("charPrIDRef")) > measure.char_height(run.get("charPrIDRef")) for run in runs): + return None # a character border above or below: Hancom writes its lines' heights and baselines otherwise end = _pages._end_size(measure, runs, sizes) if sizes and end > sizes[-1] and _pages._run_text(runs).endswith("\n"): return None # a larger empty end after a line break: Hancom writes the line before it as tall as the end diff --git a/src/hwpx/layout/pages.py b/src/hwpx/layout/pages.py index e1246cc2..adb8ad62 100644 --- a/src/hwpx/layout/pages.py +++ b/src/hwpx/layout/pages.py @@ -625,6 +625,8 @@ def __init__(self, root: Any) -> None: self._numbered: dict[str, bool] = {} self._label_sizes: dict[str, int] = {} self._pictured: dict[str, bool] = {} + self._boxes: dict[str, int] = {} + self._edges: dict[str, int] | None = None #: how far apart the default tab stops of the section at hand are (see :func:`_tab_stop`) self.tab_stop = _DEFAULT_TAB_STOP @@ -638,6 +640,24 @@ def char_height(self, char_pr_id: Any) -> int: style = self._root.char_property(char_pr_id) return int(style.attributes.get("height", 1000)) if style is not None else 1000 + def char_box(self, char_pr_id: Any) -> int: + """How tall a character of *char_pr_id* makes its line: its height, and the width of a border its shape + draws above and below it (Hancom: the line that much taller, spaced from that height).""" + + if self._edges is None: # each border fill's width above and below, read once from the header + self._edges = {} + for fill in self._header.iter(f"{HH}borderFill"): + self._edges[fill.get("id")] = sum( + round(float(rule.get("width", "0 mm").split()[0]) * 7200 / 25.4) + for rule in (fill.find(f"{HH}topBorder"), fill.find(f"{HH}bottomBorder")) + if rule is not None and rule.get("type", "NONE") != "NONE") + key = str(char_pr_id) + if key not in self._boxes: + style = self._root.char_property(char_pr_id) + fill = style.attributes.get("borderFillIDRef") if style is not None else None + self._boxes[key] = self.char_height(char_pr_id) + self._edges.get(fill, 0) + return self._boxes[key] + def font_line(self, size: int, style: Any) -> int: """How tall a line of characters *size* tall in *style* is in a paragraph taking its line height from the font (see :func:`~hwpx.form_fit.measure.font_line_height`); *size* for a face the glyph table does @@ -2745,7 +2765,7 @@ def _end_size(measure: _Measure, runs: list[Any], sizes: list[int]) -> int: if not runs or not sizes or any(len(child) or child.tag != f"{HP}t" or child.text for child in runs[-1]): return 0 - size = measure.char_height(runs[-1].get("charPrIDRef")) + size = measure.char_box(runs[-1].get("charPrIDRef")) if _run_text(runs).endswith("\n"): # after a line break the empty last line is the run alone: as tall as return size if size != sizes[-1] else 0 # itself, smaller or larger (see _line_metrics) return size if size > min(sizes) else 0 @@ -2783,15 +2803,20 @@ def _held_sizes(measure: _Measure, runs: list[Any], sizes: list[int]) -> dict[in """Where in the text a run holding a control shown on a line (see :func:`_shown_control`; not a note) or an object not set as a character stands after the first character -> that run's size, when it is larger than the smallest character: Hancom sets it as a character of no width, the line it falls on as tall as its run's - characters (those before the text, see :func:`_head_size`).""" + characters (those before the text, see :func:`_head_size`). Each character whose shape draws a border above + or below it is held too, its line as tall as the character and the border (see :meth:`_Measure.char_box`).""" if not sizes: return {} place, held = 0, {} for run in runs: + box = measure.char_box(run.get("charPrIDRef")) + boxed = box > measure.char_height(run.get("charPrIDRef")) # a border above or below its characters for child in run: name = _local(child) if name == "t": + for offset in range(len(_t_text(child))) if boxed else (): + held[place + offset + 1] = max(held.get(place + offset + 1, 0), box) place += len(_t_text(child)) elif place and (_shown_control(child) and not any(_local(kind) in ("footNote", "endNote") for kind in child) or name in _OBJECTS and (child.find(f"{HP}pos") is None diff --git a/tests/fixtures/hancom_saved/pages_character_border_line_height.hwpx b/tests/fixtures/hancom_saved/pages_character_border_line_height.hwpx new file mode 100644 index 00000000..98b96785 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_character_border_line_height.hwpx differ diff --git a/tests/test_layout_page_estimate.py b/tests/test_layout_page_estimate.py index 56b2393d..8bc0b3db 100644 --- a/tests/test_layout_page_estimate.py +++ b/tests/test_layout_page_estimate.py @@ -246,6 +246,10 @@ # empty one at 10 or 20 pt: after the break the empty last line is that run alone, as tall as itself smaller # or larger, the line before it spaced from its own characters "pages_empty_end_run_after_a_line_break": 12, + # Twenty-six times on a page of its own: ten characters at 10 or 14 pt, an empty run ending them, all of them + # or two amid them in a character shape with a border 0.1 or 0.4 mm on every side, above and below or left + # and right: a border above or below makes the line that much taller on each side, spaced from that height + "pages_character_border_line_height": 26, # A run of 14 pt before a line of 10 pt text (함초롬바탕, spaced 160%): one holding nothing but controls # makes the line as tall as itself (1400, spaced 840), an empty one takes no room: "pages_section_controls_run_taller_than_the_text": 1, # the section's and the columns' definitions