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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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가
Expand Down
24 changes: 19 additions & 5 deletions src/hwpx/form_fit/measure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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":
Expand All @@ -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)


Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
Binary file not shown.
30 changes: 30 additions & 0 deletions tests/test_form_fit_hancom_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down
Loading