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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@
빈칸이 여러 개 이어진 자리에서 줄이 한 글자 일찍 나뉘었다.
- FormFit과 쪽 수 추정이 탭 바로 뒤의 닫는 문장 부호(`)`·`.`·`!`·`」` 등)로 줄을 시작할 수 있게 한다. 빈칸 뒤처럼 탭은 앞 줄에
남고 부호가 다음 줄을 시작한다. 한/글이 그렇게 놓는다. 전에는 탭까지 다음 줄로 보냈다.
- 쪽 수 추정(실험, `estimate_pages`)이 머리 정보 파트(`Contents/header.xml`)가 없는 문서에서 `IndexError`로 멈추던 것을
지원 밖(`a document without a header part (Contents/header.xml)`)으로 알린다. 줄 배치 정보 쓰기도 그런 문서에는 쓰지 않는다.
- (실험) 줄 배치 정보 쓰기가 문단 모양이 한글·영문/숫자 자동 간격(`hh:autoSpacing`)을 켜고 한글 음절 바로 옆에 영문 글자나
숫자가 있는 문단을 건너뛴다. 한/글은 그 사이를 넓히는데 그 폭을 아직 재지 않아, 쓴 줄 나눔이 한/글과 다를 수 있었다.
- FormFit과 쪽 수 추정이 글꼴 폭 표에 없는 가나(히라가나·가타카나)를 그 글꼴의 한글 음절 폭으로 잡는다(함초롬바탕
972, 한컴 고딕 932). 한/글이 그렇게 놓는다. 전에는 전각(1 em)으로 셌다.
- 쪽 수 추정(실험, `estimate_pages`)이 탭 정의를 한 번만 읽는다. 전에는 글자 모양을 볼 때마다 머리 정보의 탭 정의 전체를
Expand Down
24 changes: 24 additions & 0 deletions src/hwpx/layout/line_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ def _lines(measure: Any, paragraph: Any, width: int, *, first: bool,
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)
if _auto_spaced(measure, paragraph.get("paraPrIDRef"), text):
return None # the gap Hancom adds between Hangul and Latin or digits was not measured
style = measure.style(paragraph.get("paraPrIDRef"), refs, paragraph)
if style is not None and style.head and not (headed and style.head_hangs):
return None
Expand Down Expand Up @@ -383,6 +385,28 @@ def _sure_of(text: str, starts: list[int], style: Any, looks: list[Any] | None =
return all(start in breaks or text[start - 1] == "\n" for start in starts[1:])


def _auto_spaced(measure: Any, para_pr_id: str | None, text: str) -> bool:
"""Whether the paragraph shape turns on Hancom's automatic spacing between Hangul and Latin letters or
digits (``hh:autoSpacing``) and the text puts a Hangul syllable right next to one of those."""

spacing = getattr(measure, "_line_cache_auto_spacing", None)
if spacing is None:
spacing = {}
for shape in measure._header.iter(f"{HH}paraPr"):
auto = next(shape.iter(f"{HH}autoSpacing"), None)
if auto is not None:
spacing[shape.get("id")] = (auto.get("eAsianEng") in ("1", "true"), auto.get("eAsianNum") in ("1", "true"))
measure._line_cache_auto_spacing = spacing
latin, digits = spacing.get(para_pr_id, (False, False))
if not latin and not digits:
return False
for before, after in zip(text, text[1:]):
for hangul, other in ((before, after), (after, before)):
if "가" <= hangul <= "힣" and other.isascii() and (latin and other.isalpha() or digits and other.isdigit()):
return True
return False


def _bold_sure(measure: Any, paragraph: Any, runs: list[Any]) -> bool:
"""Whether every bold run of *paragraph* holding text is set in faces of :data:`_BOLD_SURE_FACES`."""

Expand Down
2 changes: 2 additions & 0 deletions src/hwpx/layout/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@ def _estimate(document: Any, *, first_reason: bool = False) -> PageEstimate:
one given) instead of laying every section out to name them all."""

root = getattr(_open(document), "_root")
if not root.headers: # the shapes every paragraph names live there
return PageEstimate(None, (), ("a document without a header part (Contents/header.xml)",))
measure = _Measure(root)
layouts: list[_SectionLayout] = []
reasons: list[str] = []
Expand Down
23 changes: 23 additions & 0 deletions tests/test_layout_line_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,3 +403,26 @@ def test_a_tab_among_runs_of_several_faces_gets_its_width() -> None:
assert len(tabbed) >= 2
for mine, theirs in tabbed:
assert [dict(tab.attrib) for tab in mine.iter(f"{HP}tab")] == [dict(tab.attrib) for tab in theirs.iter(f"{HP}tab")]


@pytest.mark.parametrize(("eng", "num", "text", "written"), [
("1", "0", "가나 ABC 다라마바사 " * 3, True), # Latin only beside spaces
("1", "0", "가나ABC다라마바사 " * 3, False), # a Hangul syllable right next to a Latin letter
("0", "1", "제15호에 따른 정의 " * 3, False), # ... and next to a digit
("0", "0", "제15호에 따른 정의 " * 3, True), # automatic spacing off
])
def test_a_paragraph_with_automatic_spacing_beside_hangul_is_left_for_hancom(
eng: str, num: str, text: str, written: bool) -> None:
# Hancom widens the gap between Hangul and Latin letters or digits when the paragraph shape turns that on;
# how much was not measured, so such a paragraph keeps no cache.
document = HwpxDocument.new()
paragraph = document.add_paragraph(text)
for shape in document.oxml.headers[0].element.iter(f"{HH}paraPr"):
if shape.get("id") == paragraph.element.get("paraPrIDRef"):
auto = next(shape.iter(f"{HH}autoSpacing"))
auto.set("eAsianEng", eng)
auto.set("eAsianNum", num)

write_line_caches(document)

assert bool(_segments(paragraph.element)) is written
29 changes: 29 additions & 0 deletions tests/test_layout_page_estimate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2277,3 +2277,32 @@ def test_footnotes_beneath_text_remain_unsupported_with_endnote_support(written:

assert estimate.pages is None
assert estimate.unsupported == ("section 0: footnotes beneath the text",)


def _without_header(data: bytes) -> bytes:
out = io.BytesIO()
with zipfile.ZipFile(io.BytesIO(data)) as src, zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as dst:
for item in src.infolist():
if item.filename == "Contents/header.xml":
continue
payload = src.read(item.filename)
if item.filename == "Contents/content.hpf":
payload = re.sub(rb"<opf:item [^>]*href=\"Contents/header.xml\"[^>]*/>", b"", payload)
dst.writestr(item, payload)
return out.getvalue()


def test_a_document_without_a_header_part_is_left_unsupported() -> None:
# The paragraph and character shapes every paragraph names live in the header part; without it there is
# nothing to lay the text out by, so the estimate says so instead of failing.
document = HwpxDocument.new()
document.add_paragraph("머리 정보가 없는 문서")
data = _without_header(document.to_bytes())

estimate = estimate_pages(data)

assert estimate.pages is None and estimate.lines == ()
assert estimate.unsupported == ("a document without a header part (Contents/header.xml)",)
from hwpx.layout.line_cache import write_line_caches
report = write_line_caches(HwpxDocument.open(io.BytesIO(data)))
assert report.written == 0 and report.unsupported == estimate.unsupported
Loading