From 9593e6795f83469e7733b9dd8da183f0a0b15718 Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:47:31 +0900 Subject: [PATCH] =?UTF-8?q?fix(form=5Ffit):=20give=20a=20no-break=20space?= =?UTF-8?q?=20its=20=EC=9E=A5=ED=8F=89=20and=20no=20=EC=9E=90=EA=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FormFit and the page estimate laid a no-break space (hp:nbSpace) out like a space, its 장평 and its 자간 both applied. Hancom Office applies the 장평 but not the 자간: at 9 pt, 장평 120 % and 자간 -12 % one is 536, not 472. A Hancom-saved fixture holds no-break, fixed-width and plain spaces before tabs under several 장평 and 자간. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 ++ src/hwpx/form_fit/measure.py | 6 ++++-- .../hancom_saved/formfit_space_scaling.hwpx | Bin 0 -> 19415 bytes tests/test_form_fit_hancom_rules.py | 19 ++++++++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/hancom_saved/formfit_space_scaling.hwpx diff --git a/CHANGELOG.md b/CHANGELOG.md index b9f926af..19c512b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,6 +128,8 @@ - 쪽 수 추정(실험, `estimate_pages`)이 run의 `hp:switch` 안에 든 개체(차트와 그것을 대신하는 OLE 개체 등)가 있는 구역을 지원 밖으로 둔다(`an object in a switch (hp:switch)`). 한/글은 그런 개체도 자리를 차지해 곁의 줄을 좁히는데, 전에는 개체가 없는 것처럼 추정했다. +- FormFit과 쪽 수 추정이 묶음 빈칸(`hp:nbSpace`)에 장평만 적용하고 자간은 적용하지 않는다. 한/글이 그렇게 놓는다(9pt + 장평 120 %·자간 −12 %에서 536, 전에는 472). 공백과 고정폭 빈칸은 전과 같다. - FormFit과 쪽 수 추정에서 `!`의 반 em을 공백처럼 배치 단위(1/1800인치)로 내림한다. 9·11·13pt처럼 글자 크기의 4분의 1이 홀수인 크기에서 `!`가 4 넓게 잡혀, 그 뒤 탭 폭이 한/글보다 4 좁았다. - FormFit과 쪽 수 추정이 공백 뒤의 닫는 문장부호(`!`·`?`·`)`·`%`·`」`·`·` 등 줄 머리에 오지 않는 문자 모두)로 줄을 diff --git a/src/hwpx/form_fit/measure.py b/src/hwpx/form_fit/measure.py index 8646eb7b..479076b7 100644 --- a/src/hwpx/form_fit/measure.py +++ b/src/hwpx/form_fit/measure.py @@ -1269,7 +1269,8 @@ class TextStyle: the Hangul script's; ``scripts`` holds ``(script, 장평, 자간)`` for the Latin, symbol and Hanja scripts when the character shape gives them values of their own, and each glyph takes its script's (see :func:`glyph_script`). - A space keeps the Hangul 장평 and takes the Latin 자간. + A space keeps the Hangul 장평 and takes the Latin 자간; a no-break space + takes the 장평 and no 자간. ``break_non_latin_word`` works the reverse of its name in Hancom: ``BREAK_WORD`` (the default) keeps Hangul words whole and ``KEEP_WORD`` breaks between any two syllables; ``break_latin_word`` works as named. @@ -1499,7 +1500,8 @@ def char_advance(ch: str, font_pt: float, style: TextStyle | None = None) -> flo return float(height // _LAYOUT_UNIT // 4 * _LAYOUT_UNIT) ratio, spacing = _scaling(ch, style) if ch in _HALF_EM_SPACES and not style.use_font_space: - return _laid_out_space(height, ratio, spacing) + # a no-break space takes its 장평 but no 자간 + return _laid_out_space(height, ratio, 0.0 if ch == " " else spacing) face = style.hangul_face if cls == "hangul" else style.glyph_face if emoji: design, base = _EMOJI_UNITS, _ADVANCE_EM[cls] diff --git a/tests/fixtures/hancom_saved/formfit_space_scaling.hwpx b/tests/fixtures/hancom_saved/formfit_space_scaling.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..40aa9015e74bd6fbd2cdfbd0abfb08661a4cafbd GIT binary patch literal 19415 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 UFJW9{!v%kv!I+7zeDUu80}v9Si2wiq literal 0 HcmV?d00001 diff --git a/tests/test_form_fit_hancom_rules.py b/tests/test_form_fit_hancom_rules.py index 04df723a..a255ac14 100644 --- a/tests/test_form_fit_hancom_rules.py +++ b/tests/test_form_fit_hancom_rules.py @@ -20,6 +20,7 @@ import pytest from hwpx.document import HwpxDocument +from hwpx.layout import pages as page_layout from hwpx.form_fit import ( FitEngine, FitPolicy, @@ -58,6 +59,7 @@ PARAGRAPH_MARGINS = Path(__file__).parent / "fixtures" / "hancom_saved" / "formfit_paragraph_margins.hwpx" PARAGRAPH_SPACING = Path(__file__).parent / "fixtures" / "hancom_saved" / "formfit_paragraph_spacing.hwpx" BOLD_ADVANCES = Path(__file__).parent / "fixtures" / "hancom_saved" / "formfit_bold_advances.hwpx" +SPACE_SCALING = Path(__file__).parent / "fixtures" / "hancom_saved" / "formfit_space_scaling.hwpx" CHARS = TextStyle(break_non_latin_word="KEEP_WORD") # Hancom 글자 단위 SPACE_RUNS = Path(__file__).parent / "fixtures" / "hancom_saved" / "formfit_space_runs.hwpx" @@ -417,6 +419,23 @@ def test_bold_glyphs_take_the_advances_hancom_lays_them_out_at() -> None: assert bold_seen +def test_a_no_break_space_takes_its_ratio_and_no_spacing() -> None: + # Hancom laid "가", three no-break spaces (hp:nbSpace), fixed-width spaces or spaces and a tab out at 9, 10 + # and 14 pt in 맑은 고딕 under several 장평 and 자간: the tab's width shows how wide the spaces are. A + # no-break space takes the 장평 but no 자간 (at 120 % and -12 % a 9 pt one is 536, not 472). + assert char_advance(" ", 9, TextStyle(ratio=120, spacing=-12)) == 536 + assert char_advance(" ", 9, TextStyle(ratio=120, spacing=-12)) == 472 + document = HwpxDocument.open(SPACE_SCALING) + for paragraph in document.oxml.sections[0].element.findall(f"{HP}p")[1:]: + runs = paragraph.findall(f"{HP}run") + ref = runs[0].get("charPrIDRef") + style = text_style_from_refs(document, paragraph.get("paraPrIDRef"), [ref]) + size = int(document.oxml.char_property(ref).attributes["height"]) / 100 + text = page_layout._run_text(runs) + x = sum(char_advance(ch, size, style) for ch in text[:text.index(" ")]) + assert round((x // 4000 + 1) * 4000 - x) == int(paragraph.find(f".//{HP}tab").get("width")) + + def test_a_hangul_syllable_takes_its_own_advance_where_the_font_gives_it_one() -> None: # 한컴 말랑말랑 Regular's font draws the syllables with a horizontal vowel (ㅗ ㅛ ㅜ ㅠ ㅡ) 820 of 1000 units # wide and the others 880; the faces whose syllables are all one width keep it.