Skip to content
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@
것을 고친다. 전에는 칸 문단에 혼자 든 표만 그 높이로 셌고, 다른 개체는 없는 것처럼 줄을 세어 행이 한/글보다
낮았다. 이제 개체 하나만 든 문단은 개체 높이의 한 줄이고 줄 간격은 글자 크기로 센다. 글 사이에 있거나 여럿이면
본문처럼 개체의 폭과 높이로 줄 안에 놓는다.
- 쪽 수 추정(실험, `estimate_pages`)이 빈 문단에 문단 위 기준으로 둔 위아래 개체 여럿(표·그림)을 지원 밖
(`an object with text or other objects in its paragraph`)으로 두던 것을 고친다.
- 한/글은 개체를 차례로, 문단의 쪽부터 그 쪽에서 가로로 겹치는 앞 개체 아래에 들어가는 첫 쪽에 둔다.
문단의 쪽에서는 띄움과 앞 개체 아래 가운데 낮은 곳이다. 그래서 뒤 개체가 다음 쪽으로 간 앞 개체를 건너
문단의 쪽에 들어가기도 한다.
- 어느 쪽에도 들어가지 않는 개체는 마지막으로 쓴 쪽의 다음 쪽 맨 위에서 시작한다. 다만 첫 개체는 문단 자리에서
시작해 흐른다. 쪽보다 긴 표는 그 뒤 쪽들로 나뉜다(CELL은 줄 사이에서, TABLE은 행째).
- 이런 문단이 이어지면 뒤 문단의 개체도 앞 문단 개체를 비켜 들어가는 첫 쪽에 놓인다. 다만 쪽을 넘어 이어지는 표의
나머지는 다음 쪽 맨 위에서 이어지고, 그 쪽의 다른 개체와 겹쳐도 비키지 않는다.
- 문단의 줄과 뒤 줄은 개체를 비킨 첫 자리에 놓이고, 그 쪽에 자리가 없으면 다음 쪽 맨 위로 간다. 개체가 혼자
차지한 쪽으로 넘어간 줄(글자처럼 둔 표가 든 줄도)은 개체 아래 남은 자리에 들어가지 않으면 그 쪽을 건너뛴다.
- 여러 단, 쪽 바닥을 넘는 그림·도형은 전처럼 지원 밖이다.
- 쪽 수 추정(실험, `estimate_pages`)이 `hp:t` 글 안에 글자로 든 전각 공백(U+3000)을 고정폭 빈칸(`hp:fwSpace`)으로
보던 것을 고친다(python-hwpx는 둘을 모두 U+3000으로 읽는다). 글자 전각 공백은 글꼴의 폭(대개 글자 크기만큼)을
차지하고, 그 뒤에서 줄이 나뉘며, 줄 끝에서는 몇 개가 이어져도 모두 매달린다. 전에는 1/4 폭의 고정폭 빈칸으로
Expand Down
176 changes: 166 additions & 10 deletions src/hwpx/layout/pages.py

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
79 changes: 79 additions & 0 deletions tests/test_layout_page_estimate.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,85 @@ def test_without_line_caches_formfit_breaks_the_lines_the_same(name: str) -> Non
_assert_like_hancom(estimate_pages(_without_caches(data)), data, HANCOM_PAGES[name])


# Objects placed top and bottom from the top of an empty paragraph after four lines (10 pt, spaced 160%), the
# lines after it of text, laid out and saved by Hancom: its pages, and the page of the paragraph's line and
# the lines after when not the first:
STACKED_PAGES: dict[str, int | tuple[int, int]] = {
"pages_stacked_two_tables": 1, # both 0 down: one right below the other, the line below both
"pages_stacked_three_tables": 1,
"pages_stacked_second_table_1230_down": 1, # still right below the first
"pages_stacked_second_table_5000_down": 1, # below a gap, which takes the paragraph's line
"pages_stacked_first_table_2000_down": 1, # the line above it, the next line below both
"pages_stacked_four_tables_with_margins": 1, # outer margins 140
"pages_stacked_two_pictures": 1,
"pages_stacked_pictures_side_by_side": 1, # each at its own offset, the lines above them staying there
"pages_stacked_picture_and_table": 1,
"pages_stacked_two_pictures_past_the_foot": 2, # the second alone on the next page, the text on the first
"pages_stacked_two_tables_past_the_foot": 2,
"pages_stacked_middle_table_past_the_foot_cell": 2, # the second on the next page, the third below the first
"pages_stacked_middle_table_past_the_foot_table": 2, # (split row by row, the same)
"pages_stacked_table_taller_than_a_page_cell": 3, # split between lines over the two pages after the first
"pages_stacked_table_taller_than_a_page_table": 3, # row by row
"pages_stacked_four_tables_over_four_pages": 4, # the third below the first, 1230 down buried; the fourth
# past the second page, row by row over the last two
"pages_stacked_tables_filling_the_second_page": 2, # the third below the second, on the second page
"pages_stacked_second_table_on_the_next_page": 2, # outer margins 141: the second from the next page's top
# The paragraph low on its page, the first table not fitting under it: it flows from there.
"pages_stacked_low_two_tables_cell": (2, 1), # split at the foot, the second below its end, then the line
"pages_stacked_low_two_tables_table": (2, 1), # its first row alone on the first page
"pages_stacked_low_small_second_table": (2, 1), # the small second one not back on the first page
"pages_stacked_low_table_taller_than_a_page": (3, 2), # no room left for the line: the next page's top
# After an empty paragraph whose second table of two went on to the next page:
"pages_stacked_in_two_paragraphs": 2, # another such paragraph: its tables below the first's line
"pages_stacked_in_two_paragraphs_over_four_pages": (4, 3), # four tables, then two more from where the
# first's line leaves off: the first of them split
# over the next page's top across the other's, the
# second below it, the line on the fourth page
"pages_stacked_then_a_flowing_table": 2, # a paragraph holding one flowing table: under its line
"pages_stacked_then_text_over_two_pages": (3, 2), # 60 lines, going on below the second table
# After an empty paragraph whose second table went alone to the top of the next page, filling it but for 1500:
"pages_stacked_then_text_below_a_nearly_filled_page": (3, 2), # 60 lines: one in those 1500, the rest after
"pages_stacked_then_text_past_a_filled_page": (3, 2), # 900 left, less than a line: the lines skip that page
"pages_stacked_then_a_table_as_character_past_a_filled_page": (3, 2), # one 55000 tall skips it too
"pages_stacked_then_a_table_as_character_below_a_short_one": (2, 1), # under a second table 5762 tall
}


@pytest.mark.parametrize("name", sorted(STACKED_PAGES))
def test_objects_stacked_in_an_empty_paragraph_go_where_hancom_put_them(name: str) -> None:
# Each goes to the first page, from the paragraph's on, where it fits below the earlier ones it overlaps
# across; one fitting on none starts at the top of the page after the last one used. The lines take the
# first places clear of them. A last page holding only objects has no line, hence no HANCOM_PAGES entry.
data = (FIXTURES / f"{name}.hwpx").read_bytes()
hancom = _hancom_lines(data)
pages, lines_page = STACKED_PAGES[name] if isinstance(STACKED_PAGES[name], tuple) else (STACKED_PAGES[name], 0)

for source in (data, _without_caches(data)):
estimate = estimate_pages(source)
estimated = [[line.vertpos for line in lines] for lines in estimate.lines]

assert (estimate.unsupported, estimate.pages) == ((), pages)
assert [lines[-1].page for lines in estimate.lines][-1] == lines_page
assert [mine for mine, theirs in zip(estimated, hancom) if theirs] == [theirs for theirs in hancom if theirs]


@pytest.mark.parametrize(("index", "height"), [(0, 60000), (1, 70000)]) # the first not fitting, a later on no page
def test_a_stacked_picture_past_the_page_foot_is_not_followed(index: int, height: int) -> None:
out = io.BytesIO()
data = (FIXTURES / "pages_stacked_two_pictures.hwpx").read_bytes()
with zipfile.ZipFile(io.BytesIO(data)) as source, zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as target:
for info in source.infolist():
payload = source.read(info.filename)
if info.filename.startswith("Contents/section"):
root = etree.fromstring(payload)
list(root.iter(f"{HP}pic"))[index].find(f"{HP}sz").set("height", str(height))
payload = etree.tostring(root, xml_declaration=True, encoding="UTF-8", standalone=True)
target.writestr(info, payload)

assert estimate_pages(out.getvalue()).unsupported == (
"section 0: an object placed top and bottom past the page's foot",)


def test_objects_set_as_characters_among_text_on_both_sides_of_a_square_wrapped_object_are_not_followed() -> None:
# The picture of pages_square_band_table_as_character_among_text moved 3000 from the column's left: text goes
# on both sides of it, each line in two pieces, and an object among it is not followed there.
Expand Down
Loading