diff --git a/CHANGELOG.md b/CHANGELOG.md index 45347ef9..fd88f6e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,6 +107,9 @@ 않으면 탭은 폭이 없다. 내어쓰기 자동 탭은 둘째 줄이 시작하는 곳에 왼쪽 위치를 둔다. - 쪽 수 추정에서 강제 줄 바꿈 다음 줄은 첫 줄이 아니다. 들여쓰기를 빼지 않고 다른 줄의 폭으로 잰다. 전에는 첫 줄 폭으로 재서, 들여쓴 문단에서 줄이 하나 더 생길 수 있었다. +- 쪽 수 추정에서 자기 글자 모양이 없는 글머리표·번호(`hh:paraHead@charPrIDRef` 없음)는 문단 끝(마지막 run)의 글자 + 모양으로 잰다. 그 크기로 머리 폭을 재고, 그 크기가 첫 줄 높이에도 들어간다. 전에는 문단 첫 글자의 모양으로 재서, + 크기가 섞인 개요·번호 문단의 줄 나눔과 첫 줄 높이가 한/글과 달랐다. - FormFit과 쪽 수 추정이 텍스트 내보내기가 쓰지 않는 번호 형식(`KOREAN_DIGIT`, `HANGUL` 등)의 문단 번호를 만나도 멈추지 않는다. 한/글이 재는 번호(`useInstWidth`)이면 그 숫자를 한글 한 글자 폭으로 잰다. 전에는 번호가 자기 글자 모양을 가진 문단에서 `KeyError`가 났다. diff --git a/src/hwpx/form_fit/measure.py b/src/hwpx/form_fit/measure.py index 4319fa2c..24a51eb7 100644 --- a/src/hwpx/form_fit/measure.py +++ b/src/hwpx/form_fit/measure.py @@ -2197,11 +2197,18 @@ def _face_names(root: Any, char_pr_id_ref: object) -> tuple[str, str]: def text_style_from_refs( - document: object, para_pr_id_ref: object, char_pr_id_refs: "list[object]", label: str | None = None + document: object, + para_pr_id_ref: object, + char_pr_id_refs: "list[object]", + label: str | None = None, + end_char_pr_id_ref: object = None, ) -> TextStyle: """Hancom layout settings of a paragraph shape and the first resolvable character shape among *char_pr_id_refs*; *label*, when given, is the number - Hancom draws before the paragraph (see :func:`paragraph_label`).""" + Hancom draws before the paragraph (see :func:`paragraph_label`). + *end_char_pr_id_ref* is the character shape of the paragraph's end (its + 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, "", "" scripts: tuple[tuple[str, float, float], ...] = () @@ -2278,6 +2285,8 @@ def text_style_from_refs( text, head = heading own = head.get("charPrIDRef") looks, em = style, height + if not (own and own != "4294967295" and root.char_property(own) is not None) and end_char_pr_id_ref is not None: + own = str(end_char_pr_id_ref) # a label without a shape of its own takes the paragraph end's if own and own != "4294967295" and root.char_property(own) is not None and own != used: looks = text_style_from_refs(document, None, [own]) em = int(_style_number((root.char_property(own).attributes or {}).get("height"), 1000.0)) diff --git a/src/hwpx/layout/pages.py b/src/hwpx/layout/pages.py index e31c6cf0..18d95d8e 100644 --- a/src/hwpx/layout/pages.py +++ b/src/hwpx/layout/pages.py @@ -538,29 +538,33 @@ def headed(self, paragraph: Any) -> bool: def style(self, para_pr_id: Any, char_pr_ids: list[Any], paragraph: Any = None) -> Any: """FormFit's text style of a paragraph shape and its characters; with *paragraph*, a number's label - takes the room of the label Hancom draws for that paragraph.""" + takes the room of the label Hancom draws for that paragraph, and a label without a character shape of + its own that of the paragraph's end.""" drawn = None if paragraph is not None and self.numbered(para_pr_id): if self._drawn is None: self._drawn = _drawn_labels(self._root) drawn = self._drawn.get(paragraph) - key = (str(para_pr_id), tuple(str(ref) for ref in char_pr_ids), drawn) + end = _end_ref(paragraph) if paragraph is not None else None + key = (str(para_pr_id), tuple(str(ref) for ref in char_pr_ids), drawn, end) if key not in self._styles: - self._styles[key] = text_style_from_refs(self._lookups, para_pr_id, char_pr_ids, drawn) + self._styles[key] = text_style_from_refs(self._lookups, para_pr_id, char_pr_ids, drawn, end) return self._styles[key] - def label_size(self, para_pr_id: Any) -> int: - """The character size of a paragraph shape's bullet or number label in its own character shape - (``hh:paraHead@charPrIDRef``); 0 when it has none or takes its paragraph's.""" + def label_size(self, para_pr_id: Any, end: Any = None) -> int: + """The character size of a paragraph shape's bullet or number label: in its own character shape + (``hh:paraHead@charPrIDRef``), or without one in that of the paragraph's end (*end*); 0 when it has + none, or no shape of its own and no *end*.""" key = str(para_pr_id) if key not in self._label_sizes: label = paragraph_label(self._root, para_pr_id) ref = label[1].get("charPrIDRef") if label is not None else None own = ref is not None and ref != "4294967295" and self._root.char_property(ref) is not None - self._label_sizes[key] = self.char_height(ref) if own else 0 - return self._label_sizes[key] + self._label_sizes[key] = self.char_height(ref) if own else -1 if label is not None else 0 + size = self._label_sizes[key] + return size if size >= 0 else self.char_height(end) if end is not None else 0 def numbered(self, para_pr_id: Any) -> bool: """Whether a paragraph shape heads its paragraphs with a number (``NUMBER`` or ``OUTLINE``).""" @@ -1794,15 +1798,22 @@ def _end_size(measure: _Measure, runs: list[Any], sizes: list[int]) -> int: return size if size > min(sizes) else 0 +def _end_ref(paragraph: Any) -> Any: + """The character shape of a paragraph's end: its last run's, or None without runs.""" + + runs = paragraph.findall(f"{HP}run") + return runs[-1].get("charPrIDRef") if runs else None + + def _head_size(measure: _Measure, paragraph: Any, sizes: list[int]) -> int: - """The size a bullet or number label in its own character shape, or a run holding nothing but controls - before the text (a section's or columns' definition, a page number: an empty run takes no room), gives its - paragraph's first line: a character of that size there (it counts when it is larger than the smallest - character); 0 when not.""" + """The size a bullet or number label (in its own character shape, or without one in that of the + paragraph's end), or a run holding nothing but controls before the text (a section's or columns' + definition, a page number: an empty run takes no room), gives its paragraph's first line: a character of + that size there (it counts when it is larger than the smallest character); 0 when not.""" if not sizes: return 0 - size = measure.label_size(paragraph.get("paraPrIDRef")) + size = measure.label_size(paragraph.get("paraPrIDRef"), _end_ref(paragraph)) for run in paragraph.findall(f"{HP}run"): if any(child.text or len(child) for child in run.findall(f"{HP}t")): # the text starts break diff --git a/tests/fixtures/hancom_saved/pages_outline_labels_paragraph_end_shape.hwpx b/tests/fixtures/hancom_saved/pages_outline_labels_paragraph_end_shape.hwpx new file mode 100644 index 00000000..218e18c7 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_outline_labels_paragraph_end_shape.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_outline_labels_paragraph_end_shape_empty_run.hwpx b/tests/fixtures/hancom_saved/pages_outline_labels_paragraph_end_shape_empty_run.hwpx new file mode 100644 index 00000000..cff54612 Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_outline_labels_paragraph_end_shape_empty_run.hwpx differ diff --git a/tests/fixtures/hancom_saved/pages_outline_labels_paragraph_end_shape_sizes.hwpx b/tests/fixtures/hancom_saved/pages_outline_labels_paragraph_end_shape_sizes.hwpx new file mode 100644 index 00000000..05e2cd9e Binary files /dev/null and b/tests/fixtures/hancom_saved/pages_outline_labels_paragraph_end_shape_sizes.hwpx differ diff --git a/tests/test_form_fit_hancom_rules.py b/tests/test_form_fit_hancom_rules.py index ef436488..dbdd9299 100644 --- a/tests/test_form_fit_hancom_rules.py +++ b/tests/test_form_fit_hancom_rules.py @@ -1000,6 +1000,21 @@ def test_a_label_takes_its_room_off_the_lines() -> None: assert indented_widths(10000, replace(labelled, head_hangs=False, indent=-1000)) == (8500, 9000) +def test_a_label_without_a_shape_of_its_own_takes_the_paragraph_ends() -> None: + # An outline paragraph (the template's level 2, "가." with 자동 내어 쓰기) whose text starts at 10 pt and ends + # at 20 pt: Hancom measures the label in the paragraph end's shape, about twice as wide as in the text's. + doc = HwpxDocument.new() + small = doc.ensure_run_style(font="함초롬바탕", size=10) + large = doc.ensure_run_style(font="함초롬바탕", size=20) + + text = text_style_from_refs(doc.oxml, "3", [small]) + end = text_style_from_refs(doc.oxml, "3", [small], None, large) + + assert (text.head, text.head_hangs) == (1792, True) + assert (end.head, end.head_hangs) == (3580, True) + assert text_style_from_refs(doc.oxml, "3", [small], None, small) == text + + def test_a_label_takes_its_glyphs_its_adjustment_and_its_gap() -> None: head = {"useInstWidth": "0", "textOffsetType": "PERCENT", "textOffset": "50", "widthAdjust": "0"} face = TextStyle(glyph_face="함초롬돋움", hangul_face="함초롬돋움") diff --git a/tests/test_layout_page_estimate.py b/tests/test_layout_page_estimate.py index 7a5707d2..6f290607 100644 --- a/tests/test_layout_page_estimate.py +++ b/tests/test_layout_page_estimate.py @@ -813,6 +813,12 @@ # it, the character's line below its end "pages_bullet_and_number_labels": 16, # fourteen blocks of rows under bullets and numbers of every # label setting: each label takes its room off the lines + # Outline paragraphs (the template's levels 1 to 7, labels without a character shape of their own) of two + # to four runs, the first and the last of different sizes, some ending in an empty run: Hancom measures the + # label in the paragraph end's shape, and that size counts in the first line's height. + "pages_outline_labels_paragraph_end_shape": 2, + "pages_outline_labels_paragraph_end_shape_sizes": 2, + "pages_outline_labels_paragraph_end_shape_empty_run": 2, "pages_bullet_in_its_own_20pt_shape": 1, # three-line paragraphs of 10 pt text under a bullet in its own # 20 pt shape, at 160 %: the first line as tall as the bullet "pages_bullet_in_its_own_8pt_shape": 1, # an 8 pt one: no line taller