diff --git a/CHANGELOG.md b/CHANGELOG.md index 318d6426..fc68f0ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,9 @@ ### 고침 +- FormFit과 쪽 수 추정이 한양 글꼴(한양신명조·한양중고딕·한양견고딕)의 굵은 글자를 em의 1/20만큼 넓게 잡는다. 한/글은 + 그 폭을 더한 뒤 배치 단위로 반올림한다(14pt "가"가 1,400이 아니라 1,472). 전에는 굵은 글자도 보통 폭으로 셌다. 다른 + 글꼴의 굵은 글자는 전처럼 보통 폭이다. `TextStyle.bold`가 글자 모양의 굵게를 담는다. - FormFit과 쪽 수 추정에서 `!`의 반 em을 공백처럼 배치 단위(1/1800인치)로 내림한다. 9·11·13pt처럼 글자 크기의 4분의 1이 홀수인 크기에서 `!`가 4 넓게 잡혀, 그 뒤 탭 폭이 한/글보다 4 좁았다. - `.hwp`를 HWPX로 읽을 때 변경 추적 설정(`hh:trackchageConfig@flags`)을 설정 레코드의 첫 값 그대로 쓴다. 한컴오피스 2024가 diff --git a/src/hwpx/form_fit/measure.py b/src/hwpx/form_fit/measure.py index ac9db630..dea4043d 100644 --- a/src/hwpx/form_fit/measure.py +++ b/src/hwpx/form_fit/measure.py @@ -121,6 +121,9 @@ #: The glyphs Hancom lays out at half an em in every face, whatever the font's own advance (a glyph all the #: same: rounded, \uc7a5\ud3c9 and \uc790\uac04 applied as any other is). _HALF_EM_GLYPHS = frozenset("!") +#: The faces Hancom lays a bold glyph out wider in: by a twentieth of the em, added before the advance is rounded +#: to whole layout units. In the other faces a bold glyph keeps its regular advance. +_BOLD_WIDER_FACES = frozenset({"한양중고딕", "한양신명조", "한양견고딕"}) #: Pictographs Hancom draws as color emoji: 1.373 em in every face (a design advance of 1373 in 1000), however #: the face draws its other symbols. The pictograph planes past the BMP, and the BMP symbols that take the emoji #: presentation there (⚫ ⚡ ⌛ ☎ ✔ ➡ and other symbols stay the face's own). The regional indicators of a flag @@ -1212,12 +1215,15 @@ def font_line_height(face: str | None, height: int) -> int | None: @lru_cache(maxsize=4096) -def _laid_out(units: int, upem: int, height: int, ratio: float, spacing: float) -> float: +def _laid_out(units: int, upem: int, height: int, ratio: float, spacing: float, bold: bool = False) -> float: """HWPUNIT a glyph of design advance *units* (of *upem*) takes at a character - height of *height* HWPUNIT, its 자간 included.""" + height of *height* HWPUNIT, its 자간 included; *bold* in a face of + ``_BOLD_WIDER_FACES``, a twentieth of the em wider (seen at 장평 100 %).""" scaled = Fraction(units * (height // _LAYOUT_UNIT), upem) if ratio == 100: + if bold: + scaled += Fraction(height // _LAYOUT_UNIT, 20) advance = math.floor(scaled + Fraction(1, 2)) else: advance = math.floor(scaled * Fraction(ratio) / 100) @@ -1286,7 +1292,9 @@ class TextStyle: and the other glyphs take, laid out as Hancom rounds them (see :func:`char_advance`); an empty or unlisted face, or a glyph it does not list, falls back to ``hangul_advance`` (a Hangul syllable in em) and to - the class averages. + the class averages. ``bold`` glyphs in a face of ``_BOLD_WIDER_FACES`` + (the 한양 faces) are a twentieth of the em wider; in the others bold + keeps the regular advance. """ ratio: float = 100.0 @@ -1309,6 +1317,7 @@ class TextStyle: tab_stops: tuple[tuple[int, str], ...] = () auto_tab_left: bool = False auto_tab_right: bool = False + bold: bool = False def _line_origin(style: TextStyle, first: bool) -> float: @@ -1491,6 +1500,7 @@ def char_advance(ch: str, font_pt: float, style: TextStyle | None = None) -> flo ratio, spacing = _scaling(ch, style) if ch in _HALF_EM_SPACES and not style.use_font_space: return _laid_out_space(height, ratio, spacing) + face = style.hangul_face if cls == "hangul" else style.glyph_face if emoji: design, base = _EMOJI_UNITS, _ADVANCE_EM[cls] elif cls == "hangul": @@ -1500,7 +1510,8 @@ def char_advance(ch: str, font_pt: float, style: TextStyle | None = None) -> flo else: design, base = _design_units(_laid_out_face(style.glyph_face), ch), _ADVANCE_EM[cls] if design is not None: - return _laid_out(design[0], design[1], height, ratio, spacing) + bold = style.bold and not emoji and face in _BOLD_WIDER_FACES + return _laid_out(design[0], design[1], height, ratio, spacing, bold) return base * font_pt * 100.0 * ratio / 100.0 * (1 + spacing / 100.0) @@ -2302,7 +2313,7 @@ def text_style_from_refs( last run): a bullet or number label without a character shape of its own takes that one, else the text's.""" - ratio, spacing, use_font_space, hangul_face, glyph_face = 100.0, 0.0, False, "", "" + ratio, spacing, use_font_space, hangul_face, glyph_face, bold = 100.0, 0.0, False, "", "", False scripts: tuple[tuple[str, float, float], ...] = () root = _document_root(document) height, used = 1000, None @@ -2323,6 +2334,7 @@ def text_style_from_refs( if (_style_number(ratios.get(name), ratio), _style_number(spacings.get(name), spacing)) != (ratio, spacing) ) use_font_space = (getattr(run_style, "attributes", {}) or {}).get("useFontSpace") in {"1", "true"} + bold = "bold" in children hangul_face, glyph_face = _face_names(root, ref) height = int(_style_number((getattr(run_style, "attributes", {}) or {}).get("height"), 1000.0)) used = ref @@ -2339,6 +2351,7 @@ def text_style_from_refs( glyph_face=glyph_face, hangul_face=hangul_face, scripts=scripts, + bold=bold, ) breaks = getattr(prop, "break_setting", None) try: @@ -2372,6 +2385,7 @@ def text_style_from_refs( tab_stops=tab_stops, auto_tab_left=bool(getattr(tabs, "auto_tab_left", False)), auto_tab_right=bool(getattr(tabs, "auto_tab_right", False)), + bold=bold, ) heading = paragraph_label(root, para_pr_id_ref, label) if heading is None: diff --git a/tests/fixtures/hancom_saved/formfit_bold_advances.hwpx b/tests/fixtures/hancom_saved/formfit_bold_advances.hwpx new file mode 100644 index 00000000..3b056f87 Binary files /dev/null and b/tests/fixtures/hancom_saved/formfit_bold_advances.hwpx differ diff --git a/tests/test_form_fit_hancom_rules.py b/tests/test_form_fit_hancom_rules.py index 3ecbf05d..04df723a 100644 --- a/tests/test_form_fit_hancom_rules.py +++ b/tests/test_form_fit_hancom_rules.py @@ -57,6 +57,7 @@ FONT_LINE_HEIGHTS = Path(__file__).parent / "fixtures" / "hancom_saved" / "formfit_font_line_heights.hwpx" 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" CHARS = TextStyle(break_non_latin_word="KEEP_WORD") # Hancom 글자 단위 SPACE_RUNS = Path(__file__).parent / "fixtures" / "hancom_saved" / "formfit_space_runs.hwpx" @@ -387,6 +388,35 @@ def test_faces_in_hancoms_own_folder_take_the_design_advances_of_their_fonts() - assert glyph_advance_em("한컴 윤고딕 230", "(") == 380 / 1000 +def test_a_bold_glyph_in_a_hanyang_face_is_a_twentieth_of_the_em_wider() -> None: + # Hancom adds a twentieth of the em to a bold glyph's advance in the 한양 faces before rounding it to whole + # layout units: "가" at 14 pt is 350 + 17.5 units, 368 (1472), against 350 (1400) regular. Bold glyphs in the + # other faces keep their regular advances. + hanyang = TextStyle(hangul_face="한양신명조", glyph_face="한양신명조") + assert char_advance("가", 14, hanyang) == 1400 + assert char_advance("가", 14, replace(hanyang, bold=True)) == 1472 + assert char_advance("가", 9, replace(hanyang, bold=True)) == 900 + 44 + malgun = TextStyle(hangul_face="맑은 고딕", glyph_face="맑은 고딕") + assert char_advance("가", 14, replace(malgun, bold=True)) == char_advance("가", 14, malgun) + + +def test_bold_glyphs_take_the_advances_hancom_lays_them_out_at() -> None: + # Hancom laid out three glyphs then a tab, regular and bold, in 한양신명조 at 9 to 20 pt: each tab reaches + # the next default stop past the glyphs, so its width shows how wide they are. + document = HwpxDocument.open(BOLD_ADVANCES) + paragraphs = document.oxml.sections[0].element.findall(f"{HP}p")[1:] + bold_seen = False + for paragraph in paragraphs: + runs = paragraph.findall(f"{HP}run") + style = text_style_from_refs(document, paragraph.get("paraPrIDRef"), [runs[0].get("charPrIDRef")]) + bold_seen = bold_seen or style.bold + text = "".join(t.text or "" for t in paragraph.iter(f"{HP}t")) + size = int(document.oxml.char_property(runs[0].get("charPrIDRef")).attributes["height"]) / 100 + x = sum(char_advance(ch, size, style) for ch in text[:3]) + assert round((x // 4000 + 1) * 4000 - x) == int(paragraph.find(f".//{HP}tab").get("width")) + assert bold_seen + + 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.