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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@
지원 밖(`a document without a header part (Contents/header.xml)`)으로 알린다. 줄 배치 정보 쓰기도 그런 문서에는 쓰지 않는다.
- (실험) 줄 배치 정보 쓰기가 문단 모양이 한글·영문/숫자 자동 간격(`hh:autoSpacing`)을 켜고 한글 음절 바로 옆에 영문 글자나
숫자가 있는 문단을 건너뛴다. 한/글은 그 사이를 넓히는데 그 폭을 아직 재지 않아, 쓴 줄 나눔이 한/글과 다를 수 있었다.
- (실험) 줄 배치 정보 쓰기가 글 사이에 필드(하이퍼링크·누름틀·날짜·메일 머지)나 형광펜이 든 문단에도 쓴다. 필드의 시작·끝은
- (실험) 줄 배치 정보 쓰기가 글 사이에 필드(하이퍼링크·누름틀·날짜·메일 머지)나 형광펜이 든 문단에도 쓴다(구역 첫 문단의
필드와 메모 필드는 한/글이 배치 정보 없이 저장해 건너뛴다). 필드의 시작·끝은
줄에 자리를 차지하지 않고 줄 위치(textpos)에서 하나에 8글자씩이며, 줄이 나뉘는 자리에 있으면 다음 줄이 그것으로
시작한다. 형광펜 표시는 자리도 줄 위치도 차지하지 않는다. 한/글이 그렇게 놓는다. 전에는 그런 문단을 건너뛰었다.
- FormFit과 쪽 수 추정이 글꼴 폭 표에 없는 가나(히라가나·가타카나)를 그 글꼴의 한글 음절 폭으로 잡는다(함초롬바탕
972, 한컴 고딕 932). 한/글이 그렇게 놓는다. 전에는 전각(1 em)으로 셌다.
- 쪽 수 추정(실험, `estimate_pages`)이 탭 정의를 한 번만 읽는다. 전에는 글자 모양을 볼 때마다 머리 정보의 탭 정의 전체를
Expand Down
30 changes: 25 additions & 5 deletions src/hwpx/layout/line_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
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
first paragraph), the start and end of a field (a hyperlink, a click-here field, a date, a mail merge field) anywhere in the text but
in a section's first paragraph, a
highlight and a hanging bullet or number label in the paragraph's size are fine. Its runs may differ in
face, 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), and has no tab; a larger empty run ending the paragraph makes its last line's text as tall. A
Expand Down Expand Up @@ -115,6 +117,12 @@
#: The text children a cached paragraph may hold, as python-hwpx reads them into its text. With a cache Hancom
#: draws a tab as wide as its ``hp:tab`` says, so a tab gets the width Hancom lays it out at too.
_TEXT_MARKS = frozenset({"tab", "lineBreak", "nbSpace", "fwSpace"})
#: Marks in the text that take neither room on a line nor a text position: a highlight's start and end.
_EMPTY_MARKS = frozenset({"markpenBegin", "markpenEnd"})
#: The kinds of field whose start and end take no room on a line and eight characters each in a line's text position
#: wherever they stand (one standing where a line breaks starts the next line). A memo's field, or a field in a
#: section's first paragraph, Hancom was seen to save without a cache, so those are left for it.
_FIELD_KINDS = frozenset({"HYPERLINK", "CLICK_HERE", "DATE", "MAILMERGE"})
#: What Hancom writes on a tab that reaches a default tab stop: no leader, a left stop.
_DEFAULT_TAB = {"leader": "0", "type": "1"}

Expand Down Expand Up @@ -320,8 +328,8 @@ def lay_out(room: list[float]) -> list[int]:
return None
if mixed and _taller_past_a_break(text, sizes, starts, metrics):
return None
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
if not _sure_of(text, starts, style, looks):
return None
if any(lay_out([room + shift for room in widths]) != starts for shift in (-_MARGIN, _MARGIN)):
return None # a line break this close to a line's end may fall elsewhere in Hancom
tabs = _tab_widths(text, starts, widths, size, style, looks) if "\t" in text else []
Expand Down Expand Up @@ -533,26 +541,38 @@ def _wraps_text(section: Any) -> bool:
def _control_places(runs: list[Any], first: bool) -> list[int] | None:
"""Where in the paragraph's text each of its controls stands, or None when the paragraph holds anything but
text and controls that take no room on a line: a header, a footer, a page number and the other page controls
(and in a section's first paragraph, the section's own definitions), all of them before the text."""
(and in a section's first paragraph, the section's own definitions), all of them before the text, and the start
and end of a field anywhere."""

places: list[int] = []
length = 0
for run in runs:
for child in run:
name = _pages._local(child)
if name == "t":
if any(_pages._local(mark) not in _TEXT_MARKS for mark in child):
if any(_pages._local(mark) not in _TEXT_MARKS | _EMPTY_MARKS for mark in child):
return None
if "\n" in (child.text or "") or any("\n" in (mark.tail or "") for mark in child):
return None # a line break typed into the text, which Hancom writes as hp:lineBreak
length += len(_pages._t_text(child))
elif (name == "secPr" and first or name == "ctrl" and _takes_no_room(child, first)) and not length:
places.append(length) # before the text only: one among it was seen to break the line elsewhere
elif name == "ctrl" and not first and _field_mark(child):
places.append(length)

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.

[P1] Count each accepted field mark rather than each ctrl wrapper. _field_mark accepts multiple fieldBegin/fieldEnd children, and the OWPML ctrl choice has maxOccurs='unbounded', but this branch appends only one position. Each mark takes eight logical positions (the HWP writer also emits each ctrl child individually). Reproduced on this exact head: add a non-first paragraph with '가나다 '*40 and one ctrl containing DATE fieldBegin plus its fieldEnd before the text; write_line_caches writes starts [0,56,104,152] with counted places [0]. With two semantic field marks the starts must be [0,64,112,160]. Extend by one position per mark (or reject grouped wrappers), and test a wrapper with multiple marks, including marks at a line boundary.

else:
return None
return places


def _field_mark(ctrl: Any) -> bool:
"""Whether *ctrl* holds only the start of a field of :data:`_FIELD_KINDS` or a field's end."""

marks = list(ctrl)
return bool(marks) and all(
_pages._local(mark) == "fieldEnd" or _pages._local(mark) == "fieldBegin" and mark.get("type") in _FIELD_KINDS
for mark in marks)


def _takes_no_room(ctrl: Any, first: bool) -> bool:
kinds = {_pages._local(child) for child in ctrl}
return bool(kinds) and kinds <= (_PAGE_CONTROLS | ({"colPr"} if first else set()))
Expand Down
Binary file not shown.
22 changes: 21 additions & 1 deletion tests/test_layout_line_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
HANCOM_LAID_OUT = ["line_cache_tabs.hwpx", "line_cache_line_breaks.hwpx", "line_cache_hanging_indents.hwpx",
"line_cache_indents.hwpx", "line_cache_labels.hwpx",
"line_cache_mixed_sizes.hwpx", "line_cache_taller_past_a_break.hwpx", "line_cache_table_cells.hwpx",
"line_cache_mixed_faces_tabs.hwpx", "line_cache_space_runs.hwpx", "line_cache_tab_punctuation.hwpx", "line_cache_script_faces.hwpx"]
"line_cache_mixed_faces_tabs.hwpx", "line_cache_space_runs.hwpx", "line_cache_tab_punctuation.hwpx", "line_cache_script_faces.hwpx", "line_cache_fields.hwpx"]


def _body(document: HwpxDocument) -> list:
Expand Down Expand Up @@ -444,3 +444,23 @@ def test_glyphs_in_a_shape_of_several_script_faces_get_a_cache() -> None:
assert any(ch.isascii() and ch.isalnum() for ch in texts) and "※" in texts
for mine, theirs in written:
assert _segments(mine) == _segments(theirs)


def test_a_paragraph_holding_fields_or_a_highlight_gets_a_cache() -> None:
# A field's start and end take no room on a line and eight characters each in its text position, wherever
# they stand (one where a line breaks starts the next line); a highlight's marks take neither.
document = HwpxDocument.open(FIXTURES / "line_cache_fields.hwpx")
hancom = HwpxDocument.open(FIXTURES / "line_cache_fields.hwpx")
for paragraph in _body(document):
for cache in paragraph.findall(f"{HP}linesegarray"):
paragraph.remove(cache)

write_line_caches(document)

written = [(mine, theirs) for mine, theirs in zip(_body(document), _body(hancom)) if _segments(mine)]
with_fields = [mine for mine, _ in written if mine.find(f".//{HP}fieldBegin") is not None]
with_highlights = [mine for mine, _ in written if mine.find(f".//{HP}markpenBegin") is not None]
assert len(with_fields) >= 10 and with_highlights
assert any(len(_segments(mine)) > 1 for mine in with_fields)
for mine, theirs in written:
assert _segments(mine) == _segments(theirs)
Loading