Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
끝은 줄에 자리를 차지하지 않는다. 전에는 필드 끝을 다음 줄로 보내 빈 줄을 하나 더 셌다.
- 쪽 수 추정이 글 문단 첫 줄 아래로 놓인 나누지 않는 위아래 표가 쪽에 들어가지 않을 때, 한/글처럼 문단 줄은 들어가는 쪽에
두고 표만 다음 쪽 맨 위로 보낸다. 전에는 문단 줄까지 다음 쪽 표 아래로 보내 쪽 수가 하나 늘었다.
- 쪽 수 추정이 글자 모양의 위·아래 테두리를 줄 높이에 넣는다. 한/글은 테두리가 있는 글자(문단 끝 빈 run 포함)가 선 줄을 그 두께만큼
위아래로 키우고 줄 간격도 그 높이로 잰다. 그런 문단의 줄 캐시는 쓰지 않는다.

## [6.11.0] - 2026-11-02

Expand Down
2 changes: 2 additions & 0 deletions src/hwpx/layout/line_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 27 additions & 2 deletions src/hwpx/layout/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve the new border metrics when paragraphs reflow

The added border height lives only in held, but _paragraph replaces its computed metrics through _Reflow.at for page.unequal, and _Reflow neither stores held nor forwards it to _line_metrics. The new height therefore disappears on this supported layout path. Reproduction using this PR's pages_character_border_line_height.hwpx: remove line caches and encode two identical 21260-unit columns either as sameSz="1", sameGap="0", colCount="2", or as sameSz="0", colCount="2" with two colSz width="16384" gap="0" children. Both resolve to exactly the same actual column widths, but estimate_pages reports 66 different paragraph positions: paragraph 13 is at 3928 with equal-column encoding and 3840 with explicit-column encoding. The 10pt/0.1mm bordered line falls from height/advance 1056/1688 to 1000/1600 during same-width reflow. Carry border/held metrics through _Reflow.at and its banded path (rebasing offsets), or fail closed for combinations that cannot preserve them, and add a public regression for equivalent column-width encodings.

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
Expand Down
Binary file not shown.
4 changes: 4 additions & 0 deletions tests/test_layout_page_estimate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading