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: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
(`hp:linesegarray`)를 쓴다. 줄 높이·글 높이·기준선·줄 간격은 한/글이 쓰는 값 그대로, 줄 시작·위치·폭은 쪽 수 추정이
놓는 대로이고, 탭에는 한/글이 배치한 폭을 쓴다. 글자 크기와 모양(글꼴·장평·자간)이 하나이고 개체·사용자 탭 정의가 없는 문단만(내어 쓰는 번호·글머리표는 된다), 단이 하나이고 글이 개체 옆으로 흐르지 않는 구역에서만 쓰고, 나머지 문단은 전처럼
한/글이 열 때 배치한다. 한/글은 줄이 어디서 나뉘는지도 배치 정보를 따르므로, 글자 폭 표에 없는 글자나 한 줄보다 긴 낱말이 든 문단도 쓰지 않는다. 이미 배치 정보가 있는 문단은 그대로 두고, 쪽 수 추정이 문서를 따르지 못하면 아무것도 쓰지 않는다.
놓는 대로이고, 탭에는 한/글이 배치한 폭을 쓴다. 글자 모양(글꼴·장평·자간)이 하나이고 개체·사용자 탭 정의가 없는 문단만(머리말·꼬리말·쪽 번호처럼 줄에 자리를 차지하지 않는 컨트롤은 된다)(글자 크기가 섞이면 줄마다 가장 큰 글자의 높이로 쓴다)(내어 쓰는 번호·글머리표는 된다), 단이 하나이고 글이 개체 옆으로 흐르지 않는 구역에서만 쓰고, 나머지 문단은 전처럼
놓는 대로이고, 탭에는 한/글이 배치한 폭을 쓴다. 개체·사용자 탭 정의가 없는 문단만(run마다 글꼴·장평·자간이 달라도 된다)(머리말·꼬리말·쪽 번호처럼 줄에 자리를 차지하지 않는 컨트롤은 된다)(글자 크기가 섞이면 줄마다 가장 큰 글자의 높이로 쓴다)(내어 쓰는 번호·글머리표는 된다), 단이 하나이고 글이 개체 옆으로 흐르지 않는 구역에서만 쓰고, 나머지 문단은 전처럼
놓는 대로이고, 탭에는 한/글이 배치한 폭을 쓴다. 글자 모양(글꼴·장평·자간)이 하나이고 개체·사용자 탭 정의가 없는 문단만(글자 크기가 섞이면 줄마다 가장 큰 글자의 높이로 쓴다)(내어 쓰는 번호·글머리표는 된다), 단이 하나이고 글이 개체 옆으로 흐르지 않는 구역에서만 쓰고(그 구역 표의 칸도 칸 안 문단이 모두 그런 문단이면 쓰고, 글자처럼 놓인 표만 든 문단에는 표 높이의 한 줄을 쓰며 표의 `hp:sz` 높이도 행 높이의 합으로 쓴다), 나머지 문단은 전처럼
한/글이 열 때 배치한다. 이미 배치 정보가 있는 문단은 그대로 두고, 쪽 수 추정이 문서를 따르지 못하면 아무것도 쓰지 않는다.
기본 저장은 바뀌지 않는다.
Expand Down
51 changes: 26 additions & 25 deletions src/hwpx/layout/line_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,23 @@
the baseline and the line spacing past it. The others come from :func:`hwpx.layout.pages.estimate_pages`. Hancom
also draws a tab as wide as its ``hp:tab`` says once the paragraph has a cache, so each tab gets its width.

Only a paragraph Hancom would lay out from its text alone gets a cache: a body paragraph of text runs in one
look (face, 장평, 자간) -- tabs to the default tab stops, line breaks, controls that take no room on a line (a
header, a footer, a page number, the section's own definitions in its first paragraph), and a hanging bullet or
number label in the paragraph's size included -- in a section of one column
where no object lets text run beside it. Its runs may differ in size: each line is then as tall as its largest
character, unless a larger one past the line's end may be the one that did not fit on it (Hancom writes the line
as tall as that one), and the paragraph has no tab. The cells of a table in such a section get caches the
same way when every paragraph of the cell is one of these: their lines go down from the top of the cell's text
area, as wide as that area in whole layout units and never narrower than Hancom's narrowest line. A paragraph
holding nothing but a table set as a character gets its one line, as tall as the table, and the table its rows'
height (``hp:sz``), as Hancom saves them. A paragraph is
also left when its line breaks are less sure: a character no glyph table gives an advance for, or a word too long
for a line. A paragraph that has a cache keeps it. The others are left for Hancom;
a document mixing both is laid out as one. Nothing is written when the estimate does not follow the document.
Only a paragraph Hancom would lay out from its text alone gets a cache: a body paragraph of text runs in a
section of one column where no object lets text run beside it. Tabs to the default tab stops, line breaks,
controls that take no room on a line (a header, a footer, a page number, the section's own definitions in its
first paragraph) and a hanging bullet or number label in the paragraph's size are fine. Its runs may differ in
face, 장평 or 자간, each character taking its own run's advance, and in size: each line is then as tall as its
largest character, unless a larger one past the line's end may be the one that did not fit on it (Hancom writes
the line as tall as that one); such a paragraph has no tab.

The cells of a table in such a section get caches the same way when every paragraph of the cell is one of
these: their lines go down from the top of the cell's text area, as wide as that area in whole layout units and
never narrower than Hancom's narrowest line. A paragraph holding nothing but a table set as a character gets its
one line, as tall as the table, and the table its rows' height (``hp:sz``), as Hancom saves them, when every
paragraph of its cells is laid out surely.

A paragraph is left for Hancom when its line breaks are less sure: a character no glyph table gives an advance
for, or a word too long for a line. A paragraph that has a cache keeps it. A document mixing paragraphs with and
without a cache is laid out as one. Nothing is written when the estimate does not follow the document.
"""

from __future__ import annotations
Expand Down Expand Up @@ -249,8 +252,7 @@ def _lines(measure: Any, paragraph: Any, width: int, *, first: bool,
size, refs, sizes = _pages._text_size(measure, runs)
end = _pages._end_size(measure, runs, sizes)
mixed = len(set(sizes)) > 1 or bool(end) # each line as tall as its largest character
if _pages._char_styles(measure, paragraph, runs) is not None: # runs of another face, 장평 or 자간
return None
looks = _pages._char_styles(measure, paragraph, runs) # runs of another face, 장평 or 자간: each its own
if headed and (_pages._head_size(measure, paragraph, sizes) or shape.indent): # a label of its own size
return None
text = _pages._run_text(runs)
Expand All @@ -259,19 +261,19 @@ def _lines(measure: Any, paragraph: Any, width: int, *, first: bool,
return None
room = width - shape.left - shape.right
widths = _pages._line_widths(shape, width, style) if cell else _pages._indented(room, shape, style)
if mixed:
if mixed or looks is not None:
if "\t" in text:
return None
starts = _pages._metric_starts(measure, text, widths, sizes, style, {})
metrics = _pages._line_metrics(measure, text, widths, sizes, style, shape, {}, end=end)
starts = _pages._metric_starts(measure, text, widths, sizes, style, {}, looks)
metrics = _pages._line_metrics(measure, text, widths, sizes, style, shape, {}, looks, end=end)
else:
starts = measure.line_starts(text, widths, size, style) if text else [0]
metrics = ((size, _pages._pitch(shape.kind, shape.value, size)),) * len(starts)
if len(metrics) != len(starts):
return None
if mixed and _taller_past_a_break(text, sizes, starts, metrics):
return None
if not _sure_of(text, starts, style) or any(place in starts[1:] for place in controls):
if not _sure_of(text, starts, style, looks) or any(place in starts[1:] for place in controls):
return None # a line starting at a control: on which side Hancom starts it is not known
tabs = _tab_widths(text, starts, widths, size, style) if "\t" in text else []
if tabs is None:
Expand Down Expand Up @@ -302,22 +304,21 @@ def _lines(measure: Any, paragraph: Any, width: int, *, first: bool,
return segments, tabs


def _sure_of(text: str, starts: list[int], style: Any) -> bool:
def _sure_of(text: str, starts: list[int], style: Any, looks: list[Any] | None = None) -> bool:
"""Whether the line breaks at *starts* can be trusted to be Hancom's: every character takes an advance a
glyph table gives (not a class average), and no line starts inside a word that did not fit on a line of its
own. Hancom takes the line breaks from a cache as written (a line short of a break is drawn with its text
pressed together), so a paragraph laid out less surely is left for Hancom."""

if style is None:
return False
face, hangul_face = _laid_out_face(style.glyph_face), _laid_out_face(style.hangul_face)
for ch in set(text):
for ch, look in set(zip(text, looks or [style] * len(text))):
if ch in "\t\n" or ch in _HALF_EM_SPACES or ch in _HALF_EM_GLYPHS or ch in (_FIXED_SPACE, _GLYPH_SPACE):
continue
if classify_char(ch) == "hangul":
if _hangul_units(hangul_face, ch) is None:
if _hangul_units(_laid_out_face(look.hangul_face), ch) is None:
return False
elif _design_units(face, ch) is None:
elif _design_units(_laid_out_face(look.glyph_face), ch) is None:
return False
breaks = _hancom_break_opportunities(text, style)
return all(start in breaks or text[start - 1] == "\n" for start in starts[1:])
Expand Down
Binary file not shown.
20 changes: 20 additions & 0 deletions tests/test_layout_line_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,23 @@ def test_a_paragraph_holding_page_controls_gets_a_cache() -> None:
if child.tag.endswith("}ctrl") and any(not grand.tag.endswith("}colPr") for grand in child)]
assert controls and _segments(first)
assert report.written == 2


def test_a_paragraph_of_several_faces_gets_a_cache() -> None:
# Runs of other faces, 장평 or 자간 break their lines with their own advances, as Hancom lays them out.
document = HwpxDocument.open(FIXTURES / "line_cache_mixed_faces.hwpx")
hancom = HwpxDocument.open(FIXTURES / "line_cache_mixed_faces.hwpx")
for paragraph in _body(document):
for cache in paragraph.findall(f"{HP}linesegarray"):
paragraph.remove(cache)

write_line_caches(document)

faces = [
(mine, theirs)
for mine, theirs in zip(_body(document), _body(hancom))
if _segments(mine) and len({run.get("charPrIDRef") for run in mine.findall(f"{HP}run")}) > 1
]
assert len(faces) >= 3
for mine, theirs in faces:
assert _segments(mine) == _segments(theirs)
Loading