diff --git a/CHANGELOG.md b/CHANGELOG.md index 318d6426..4be50a1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,9 @@ - FormFit과 쪽 수 추정에서 `!`의 반 em을 공백처럼 배치 단위(1/1800인치)로 내림한다. 9·11·13pt처럼 글자 크기의 4분의 1이 홀수인 크기에서 `!`가 4 넓게 잡혀, 그 뒤 탭 폭이 한/글보다 4 좁았다. +- FormFit과 쪽 수 추정이 공백 뒤의 닫는 문장부호(`!`·`?`·`)`·`%`·`」`·`·` 등 줄 머리에 오지 않는 문자 모두)로 줄을 + 시작한다. 한/글이 그렇게 놓는다. 전에는 말줄임표(`…`)만 그랬고, 다른 문자는 공백 앞 낱말까지 다음 줄로 넘겨 한 줄에 + 낱말 하나가 덜 들어갔다. 앞 글자에 붙은 문장부호는 전처럼 그 글자와 함께 다음 줄로 간다. - `.hwp`를 HWPX로 읽을 때 변경 추적 설정(`hh:trackchageConfig@flags`)을 설정 레코드의 첫 값 그대로 쓴다. 한컴오피스 2024가 쓰는 값이다(대개 56). 전에는 레코드가 있으면 늘 4를 썼다. `.hwp`로 쓸 때도 HWPX의 flags를 그 자리에 쓴다(없으면 56). - 메모 필드(`notes.add_memo`)와 `.hwp`에서 읽은 필드의 `hp:fieldBegin`에 한/글처럼 `metaTag=""`를 단다(메일 머지 diff --git a/src/hwpx/form_fit/measure.py b/src/hwpx/form_fit/measure.py index ac9db630..9230a7e4 100644 --- a/src/hwpx/form_fit/measure.py +++ b/src/hwpx/form_fit/measure.py @@ -129,8 +129,8 @@ _EMOJI_BMP = frozenset("✅❌❎❗❓❔❕➕➖➗⏰⏳⛔✋✨⭐⬛⬜") #: The variation selectors and the zero-width joiner: no advance. _ZERO_WIDTH = frozenset("\ufe0e\ufe0f\u200d") -#: Closing punctuation that never starts a line; it moves down with the -#: character before it; but the ellipsis starts a line after a space. +#: Closing punctuation that never starts a line right after a character: it +#: moves down with that character. After a space it may start a line. _NO_LINE_START = frozenset("!%),.:;?]}¢°’”‰′″℃〉》」』】〕…·、。") #: Opening punctuation that never ends a line. _NO_LINE_END = frozenset("([{‘“〈《「『【〔") @@ -1608,9 +1608,9 @@ def hancom_line_starts( do. Once the line's text reaches the margin (its 자간 included), its spaces hang and the next word starts the next line however far the spaces could shrink: only the word that crosses the margin shrinks them. The line then - ends at the last break opportunity that fits — never before a closing or - after an opening punctuation mark — or mid-word when no opportunity is - left. *sizes*, when given, holds each character's size in pt (runs of + ends at the last break opportunity that fits — never before a closing + punctuation mark attached to the character before it, nor after an opening + one — or mid-word when no opportunity is left. *sizes*, when given, holds each character's size in pt (runs of several sizes): every character, and every space that shrinks, then takes its own size. *styles*, when given, holds each character's own style (runs of several @@ -1678,7 +1678,7 @@ def hancom_line_starts( options = [ index for index in breaks if start < index <= end - and (text[index] not in _NO_LINE_START or text[index] == "…" and text[index - 1] in _HANGING_SPACES) + and (text[index] not in _NO_LINE_START or text[index - 1] in _HANGING_SPACES) and text[index - 1] not in _NO_LINE_END ] start = max(options) if options else end diff --git a/tests/fixtures/hancom_saved/pages_closing_bracket_after_a_space_starts_a_line.hwpx b/tests/fixtures/hancom_saved/pages_closing_bracket_after_a_space_starts_a_line.hwpx new file mode 100644 index 00000000..531cfe74 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_closing_bracket_after_a_space_starts_a_line.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_percent_after_a_space_starts_a_line.hwpx b/tests/fixtures/hancom_saved/pages_percent_after_a_space_starts_a_line.hwpx new file mode 100644 index 00000000..841b7b9f Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_percent_after_a_space_starts_a_line.hwpx differ diff --git a/tests/test_layout_page_estimate.py b/tests/test_layout_page_estimate.py index 85ad606a..0bd6e187 100644 --- a/tests/test_layout_page_estimate.py +++ b/tests/test_layout_page_estimate.py @@ -212,6 +212,10 @@ # Tables of short sentences, one cell holding "… " among its words: Hancom starts a line with the ellipsis after # a space (attached to the character before it, it keeps to that one). "pages_ellipsis_starts_a_cell_line": 4, + # Paragraphs of sentences among which " 」" or " %" stands after a space: Hancom starts a line with the mark + # there, as with any closing punctuation after a space (attached to a character, it moves down with it). + "pages_closing_bracket_after_a_space_starts_a_line": 2, + "pages_percent_after_a_space_starts_a_line": 2, "pages_no_break_spaces": 2, # rows of "가나" and a no-break space: each is half an em and keeps the row # one word "pages_typed_ideographic_spaces": 5, # cells of "가나다라마" and a U+3000 typed in the text, a full em @@ -1079,6 +1083,26 @@ def test_without_line_caches_formfit_breaks_the_lines_the_same(name: str) -> Non _assert_like_hancom(estimate_pages(_without_caches(data)), data, HANCOM_PAGES[name]) +@pytest.mark.parametrize("name", ["pages_closing_bracket_after_a_space_starts_a_line", + "pages_percent_after_a_space_starts_a_line"]) +def test_a_closing_mark_after_a_space_may_start_a_line(name: str) -> None: + # The line count alone does not show where a line starts: each paragraph's lines start where Hancom's do. + root = HwpxDocument.open(io.BytesIO((FIXTURES / f"{name}.hwpx").read_bytes()))._root + measure = page_layout._Measure(root) + page = page_layout._page(root.sections[0].element) + + for paragraph in root.sections[0].element.findall(f"{HP}p")[1:]: + runs = paragraph.findall(f"{HP}run") + size, refs, _ = page_layout._text_size(measure, runs) + style = measure.style(paragraph.get("paraPrIDRef"), refs, paragraph) + shape = measure.shape(paragraph.get("paraPrIDRef")) + widths = page_layout._indented(page.column_width - shape.left - shape.right, shape, style) + + starts = measure.line_starts(page_layout._run_text(runs), widths, size, style) + + assert starts == [int(segment.get("textpos")) for segment in paragraph.findall(f"{HP}linesegarray/{HP}lineseg")] + + @pytest.mark.parametrize( "name", [