From b4197c4f1ea704e6c714ae673b36cebbc4436495 Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Sat, 17 Oct 2026 04:20:09 +0900 Subject: [PATCH] feat(layout): write line caches for paragraphs whose runs differ in face MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit write_line_caches now also writes the cache of a paragraph whose runs differ in face, 장평 or 자간: each character breaks the lines at its own run's advance, as the page estimate lays such a paragraph out and as Hancom Office does. Its line breaks count as sure only when every character's advance comes from a glyph table for its own run's face. A tab in such a paragraph still leaves it for Hancom. A Hancom-saved fixture holds paragraphs of runs in several faces. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- src/hwpx/layout/line_cache.py | 51 +++++++++--------- .../hancom_saved/line_cache_mixed_faces.hwpx | Bin 0 -> 28552 bytes tests/test_layout_line_cache.py | 20 +++++++ 4 files changed, 47 insertions(+), 26 deletions(-) create mode 100644 tests/fixtures/hancom_saved/line_cache_mixed_faces.hwpx diff --git a/CHANGELOG.md b/CHANGELOG.md index dca42406..f9fb09e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ (`hp:linesegarray`)를 쓴다. 줄 높이·글 높이·기준선·줄 간격은 한/글이 쓰는 값 그대로, 줄 시작·위치·폭은 쪽 수 추정이 놓는 대로이고, 탭에는 한/글이 배치한 폭을 쓴다. 글자 크기와 모양(글꼴·장평·자간)이 하나이고 개체·사용자 탭 정의가 없는 문단만(내어 쓰는 번호·글머리표는 된다), 단이 하나이고 글이 개체 옆으로 흐르지 않는 구역에서만 쓰고, 나머지 문단은 전처럼 한/글이 열 때 배치한다. 한/글은 줄이 어디서 나뉘는지도 배치 정보를 따르므로, 글자 폭 표에 없는 글자나 한 줄보다 긴 낱말이 든 문단도 쓰지 않는다. 이미 배치 정보가 있는 문단은 그대로 두고, 쪽 수 추정이 문서를 따르지 못하면 아무것도 쓰지 않는다. - 놓는 대로이고, 탭에는 한/글이 배치한 폭을 쓴다. 글자 모양(글꼴·장평·자간)이 하나이고 개체·사용자 탭 정의가 없는 문단만(머리말·꼬리말·쪽 번호처럼 줄에 자리를 차지하지 않는 컨트롤은 된다)(글자 크기가 섞이면 줄마다 가장 큰 글자의 높이로 쓴다)(내어 쓰는 번호·글머리표는 된다), 단이 하나이고 글이 개체 옆으로 흐르지 않는 구역에서만 쓰고, 나머지 문단은 전처럼 + 놓는 대로이고, 탭에는 한/글이 배치한 폭을 쓴다. 개체·사용자 탭 정의가 없는 문단만(run마다 글꼴·장평·자간이 달라도 된다)(머리말·꼬리말·쪽 번호처럼 줄에 자리를 차지하지 않는 컨트롤은 된다)(글자 크기가 섞이면 줄마다 가장 큰 글자의 높이로 쓴다)(내어 쓰는 번호·글머리표는 된다), 단이 하나이고 글이 개체 옆으로 흐르지 않는 구역에서만 쓰고, 나머지 문단은 전처럼 놓는 대로이고, 탭에는 한/글이 배치한 폭을 쓴다. 글자 모양(글꼴·장평·자간)이 하나이고 개체·사용자 탭 정의가 없는 문단만(글자 크기가 섞이면 줄마다 가장 큰 글자의 높이로 쓴다)(내어 쓰는 번호·글머리표는 된다), 단이 하나이고 글이 개체 옆으로 흐르지 않는 구역에서만 쓰고(그 구역 표의 칸도 칸 안 문단이 모두 그런 문단이면 쓰고, 글자처럼 놓인 표만 든 문단에는 표 높이의 한 줄을 쓰며 표의 `hp:sz` 높이도 행 높이의 합으로 쓴다), 나머지 문단은 전처럼 한/글이 열 때 배치한다. 이미 배치 정보가 있는 문단은 그대로 두고, 쪽 수 추정이 문서를 따르지 못하면 아무것도 쓰지 않는다. 기본 저장은 바뀌지 않는다. diff --git a/src/hwpx/layout/line_cache.py b/src/hwpx/layout/line_cache.py index d8d2ff78..f2313637 100644 --- a/src/hwpx/layout/line_cache.py +++ b/src/hwpx/layout/line_cache.py @@ -13,20 +13,23 @@ the baseline and the line spacing past it. The others come from :func:`hwpx.layout.pages.estimate_pages`. Hancom also draws a tab as wide as its ``hp:tab`` says once the paragraph has a cache, so each tab gets its width. -Only a paragraph Hancom would lay out from its text alone gets a cache: a body paragraph of text runs in one -look (face, 장평, 자간) -- tabs to the default tab stops, line breaks, controls that take no room on a line (a -header, a footer, a page number, the section's own definitions in its first paragraph), and a hanging bullet or -number label in the paragraph's size included -- in a section of one column -where no object lets text run beside it. Its runs may differ in size: each line is then as tall as its largest -character, unless a larger one past the line's end may be the one that did not fit on it (Hancom writes the line -as tall as that one), and the paragraph has no tab. The cells of a table in such a section get caches the -same way when every paragraph of the cell is one of these: their lines go down from the top of the cell's text -area, as wide as that area in whole layout units and never narrower than Hancom's narrowest line. A paragraph -holding nothing but a table set as a character gets its one line, as tall as the table, and the table its rows' -height (``hp:sz``), as Hancom saves them. A paragraph is -also left when its line breaks are less sure: a character no glyph table gives an advance for, or a word too long -for a line. A paragraph that has a cache keeps it. The others are left for Hancom; -a document mixing both is laid out as one. Nothing is written when the estimate does not follow the document. +Only a paragraph Hancom would lay out from its text alone gets a cache: a body paragraph of text runs in a +section of one column where no object lets text run beside it. Tabs to the default tab stops, line breaks, +controls that take no room on a line (a header, a footer, a page number, the section's own definitions in its +first paragraph) and a hanging bullet or number label in the paragraph's size are fine. Its runs may differ in +face, 장평 or 자간, each character taking its own run's advance, and in size: each line is then as tall as its +largest character, unless a larger one past the line's end may be the one that did not fit on it (Hancom writes +the line as tall as that one); such a paragraph has no tab. + +The cells of a table in such a section get caches the same way when every paragraph of the cell is one of +these: their lines go down from the top of the cell's text area, as wide as that area in whole layout units and +never narrower than Hancom's narrowest line. A paragraph holding nothing but a table set as a character gets its +one line, as tall as the table, and the table its rows' height (``hp:sz``), as Hancom saves them, when every +paragraph of its cells is laid out surely. + +A paragraph is left for Hancom when its line breaks are less sure: a character no glyph table gives an advance +for, or a word too long for a line. A paragraph that has a cache keeps it. A document mixing paragraphs with and +without a cache is laid out as one. Nothing is written when the estimate does not follow the document. """ from __future__ import annotations @@ -249,8 +252,7 @@ def _lines(measure: Any, paragraph: Any, width: int, *, first: bool, size, refs, sizes = _pages._text_size(measure, runs) end = _pages._end_size(measure, runs, sizes) mixed = len(set(sizes)) > 1 or bool(end) # each line as tall as its largest character - if _pages._char_styles(measure, paragraph, runs) is not None: # runs of another face, 장평 or 자간 - return None + looks = _pages._char_styles(measure, paragraph, runs) # runs of another face, 장평 or 자간: each its own if headed and (_pages._head_size(measure, paragraph, sizes) or shape.indent): # a label of its own size return None text = _pages._run_text(runs) @@ -259,11 +261,11 @@ def _lines(measure: Any, paragraph: Any, width: int, *, first: bool, return None room = width - shape.left - shape.right widths = _pages._line_widths(shape, width, style) if cell else _pages._indented(room, shape, style) - if mixed: + if mixed or looks is not None: if "\t" in text: return None - starts = _pages._metric_starts(measure, text, widths, sizes, style, {}) - metrics = _pages._line_metrics(measure, text, widths, sizes, style, shape, {}, end=end) + starts = _pages._metric_starts(measure, text, widths, sizes, style, {}, looks) + metrics = _pages._line_metrics(measure, text, widths, sizes, style, shape, {}, looks, end=end) else: starts = measure.line_starts(text, widths, size, style) if text else [0] metrics = ((size, _pages._pitch(shape.kind, shape.value, size)),) * len(starts) @@ -271,7 +273,7 @@ def _lines(measure: Any, paragraph: Any, width: int, *, first: bool, return None if mixed and _taller_past_a_break(text, sizes, starts, metrics): return None - if not _sure_of(text, starts, style) or any(place in starts[1:] for place in controls): + if not _sure_of(text, starts, style, looks) or any(place in starts[1:] for place in controls): return None # a line starting at a control: on which side Hancom starts it is not known tabs = _tab_widths(text, starts, widths, size, style) if "\t" in text else [] if tabs is None: @@ -302,7 +304,7 @@ def _lines(measure: Any, paragraph: Any, width: int, *, first: bool, return segments, tabs -def _sure_of(text: str, starts: list[int], style: Any) -> bool: +def _sure_of(text: str, starts: list[int], style: Any, looks: list[Any] | None = None) -> bool: """Whether the line breaks at *starts* can be trusted to be Hancom's: every character takes an advance a glyph table gives (not a class average), and no line starts inside a word that did not fit on a line of its own. Hancom takes the line breaks from a cache as written (a line short of a break is drawn with its text @@ -310,14 +312,13 @@ def _sure_of(text: str, starts: list[int], style: Any) -> bool: if style is None: return False - face, hangul_face = _laid_out_face(style.glyph_face), _laid_out_face(style.hangul_face) - for ch in set(text): + for ch, look in set(zip(text, looks or [style] * len(text))): if ch in "\t\n" or ch in _HALF_EM_SPACES or ch in _HALF_EM_GLYPHS or ch in (_FIXED_SPACE, _GLYPH_SPACE): continue if classify_char(ch) == "hangul": - if _hangul_units(hangul_face, ch) is None: + if _hangul_units(_laid_out_face(look.hangul_face), ch) is None: return False - elif _design_units(face, ch) is None: + elif _design_units(_laid_out_face(look.glyph_face), ch) is None: return False breaks = _hancom_break_opportunities(text, style) return all(start in breaks or text[start - 1] == "\n" for start in starts[1:]) diff --git a/tests/fixtures/hancom_saved/line_cache_mixed_faces.hwpx b/tests/fixtures/hancom_saved/line_cache_mixed_faces.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..72764bd83d87dd184b2966576da64ff6dd320e5c GIT binary patch literal 28552 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%0A{&60xD zXBG?>AwgwF4GanRjqorAywk%|2jIGJbis+`;4up%D;a5oN_IvurS^?g8wNl3MPxWd znVbTBB~3F7pvihP;_re;S#n-n)}vz69Sti6j%K zXHX(&gkPpZ78ujc9)58~(mHwiX4GJ+v70A5#0kzh&4H(7vIqea1FFfQ4WV|wtA2D8 zq!qRKG(LWPLL=051|7FstFl~Kz6X;z9J6ag3R!3s!Cji(=UUjmw)8h0kSN>Q+zik@ zL?JNhxff~j+=5U|KVXXbl=hWtcXVWGFaQfog`izaAIfV>{w|k9jO_)@%_IEb@`ulV z2KhjimJ4ea35_iLxRBg6OVW^Ys?~bwwlx1Gi>9lSZ>Od8ru)=+mYV5O6@C^yOCxmthUmku!YP7Cxm9GzY?D;h;n->ERRjn&Ie3g~v_9K0ARW%Es%VmBjd6umH z1gHV&v3s#!cQn5JvSz|oQm>^tU$U?9{4DpX#g?*tenJD*^V|ciSFC^DUh`e3X4AS2P7KpJOabo z+G52boxy49Fi#LYjO)kz7*A08$PRe@z#~K+k!n@;RL93iVkvj2i1`rAz@*bqcH$5V zGc4QsHXjHbqPIkuqZHs*!&zpCXZLyo#Lm?iR z=b_2-x$%t zJI-YTU(thGVC{B{-LP^;jmNpngrSvJU$t*qTB&jIap)c}w=ds$`a7Jx?#Lexn4J+A zaA@2DbJhBYx9$d&=Hdm;O7tDi5}$BOD8du=m@-y|Ig`2hY2F%@M%Pe+Nwh2p&XsEy ztkPGF3G(6h;%#v^jDiEO%q$;v@%hnD_W}Z(wr|HLalOpB;Fh6P;00LuTxdKV3Q-fo z8CAkyMreN!<6PmauA|EiO`l5eq>O;CE^4syMirBH_&NvpidZlAqTyO)>k*glZP`IG z9S&w80usu)dhbK1$m+U9@y%r4mPA%xJh_1r&dSk$AA-kRpIYeLzv+#Dqz1QK2V~y9 zyvAP5+(k`pvHsZYTo`kCyBxz{=)lc1_!Md@zYJO*W@J)_1Rva*&0EcQo%@Yxj+w!I{~?iu?X`h>v6x%KA&dBn0?c)wMY;RmzaF3baO7(>>pY}x*qOh$@CcGMqet$FS!ixMZ_jzQ@c-Qtgcs*- zVkFQPvVGZpCnu<4b?hSJmKDRni8o-{ws_IMZMoehd%#J$M~Vy4{-%mu1|R@Iji9q5 zkE^cN`AAMMiNmdc<)gS2n2FrLkncqcIhw|&eFlM$s8_?9HU_u`ZI3%zP38l zsoZ}3l+KBX_4}?4-%Q+rgNZFnc|Gzvq{!#WTC>DaA)Ua! z#DLo0?ttYXRmdjVBlVnnNjUJ`#SK`pP)m=VstW=Q_!9IgE6|O)@no)+Gej3NTh)T6 zxi@V>?BQsEzP-2sxE4n^;fNh2}EJg+zHDWuSxUUB7+kCRvXFR?I$Ro?^v` zzgvI2@*0NJ(CaDZrioCC=p&xxmdE-$wmQ)e&rN3jQfO&Azuk~BOM+dD})dUNr4JjaWX!8@l zleWHA>H5!ZpggEh;ZKOyFA1|gfp=L}4almX>OK+QgF&3oyM-0oV9p$}a^|6JR+?%u zX|J|rwm7A3OvCk>D0Uekk2;qElX*`Z^p71b94+rgT@>|&Uxy3@2P)^xPOPRLSs7MN zZwl~7bdw7n!o4w~M;Kr#_9~|pDHLz?CI$Nkl97$&FF1j5cy-H;`p&iGX3K{n5aNW% z&L8vdl?{E}M5a8ezq_&^)aL~V*?QLc)|q;ko0Rqj9F#Ezi>J_-O1kwoT+a1LES3v%7iG(U$FLRfstWb9XT`2!9G{fkeQQYh#H>fvutU9EFfzb6A z9DjYkn(tZrR8xNZkIBS%4kIbnJIa-5eWi8HE+@mDqP^i=ca zc4p7?nD9$OpB$D?{K;fyc^53s>cC`aXhp_p{#pDw`lIr~c$t&z!V$FL$j0nV5&h!; z=9w9A8u>K^ zyn_e`wV83bxHZJW>H+_)l5W=y5%bhM!&^6eHR#!=*uOH}%0Atz4$F1kA7-NBIZkRB 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 P@U8<)a5>QM$dUg6Jju+k literal 0 HcmV?d00001 diff --git a/tests/test_layout_line_cache.py b/tests/test_layout_line_cache.py index 3b270f5a..c5d252ba 100644 --- a/tests/test_layout_line_cache.py +++ b/tests/test_layout_line_cache.py @@ -185,3 +185,23 @@ def test_a_paragraph_holding_page_controls_gets_a_cache() -> None: if child.tag.endswith("}ctrl") and any(not grand.tag.endswith("}colPr") for grand in child)] assert controls and _segments(first) assert report.written == 2 + + +def test_a_paragraph_of_several_faces_gets_a_cache() -> None: + # Runs of other faces, 장평 or 자간 break their lines with their own advances, as Hancom lays them out. + document = HwpxDocument.open(FIXTURES / "line_cache_mixed_faces.hwpx") + hancom = HwpxDocument.open(FIXTURES / "line_cache_mixed_faces.hwpx") + for paragraph in _body(document): + for cache in paragraph.findall(f"{HP}linesegarray"): + paragraph.remove(cache) + + write_line_caches(document) + + faces = [ + (mine, theirs) + for mine, theirs in zip(_body(document), _body(hancom)) + if _segments(mine) and len({run.get("charPrIDRef") for run in mine.findall(f"{HP}run")}) > 1 + ] + assert len(faces) >= 3 + for mine, theirs in faces: + assert _segments(mine) == _segments(theirs)