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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@
- 쪽 수 추정에서 글이 없는 문단의 줄은 컨트롤이나 개체를 든 run과 마지막 run(문단 끝) 가운데 가장 큰 글자 크기로
잰다. 한/글은 그 앞의 빈 글 run은 세지 않는다. 전에는 빈 글 run의 가장 작은 크기로 재서, 구역 정의나 표를 든 큰 글자
run과 작은 빈 run이 섞인 문단(예: 문서 첫 문단)의 줄이 낮았다.
- 쪽 수 추정과 배치 검사(`hwpx.layout`)가 음수 쪽 여백을 음수로 읽는다. 머리말·꼬리말 여백이 음수인 문서(.hwp를 HWPX로
바꾼 문서에 있다)는 그 값을 부호 없는 32비트(-1400이면 4294965896)로 남기는데, 전에는 이를 큰 양수로 읽어 본문 높이가
음수가 되고 쪽 수 추정이 끝나지 않았다. 한/글은 그 값을 음수로 읽어 본문을 그만큼 늘린다. 반면 마이너스 기호로 쓴
여백(`header="-1400"`)은 한/글이 읽지 않고 새 쪽의 여백(위 5668, 아래 4252, 왼쪽·오른쪽 8504, 머리말·꼬리말 4252,
제본 0)으로 놓은 뒤 그 값으로 저장하므로, 추정도 그 값을 쓴다. 여백 때문에 본문 높이나 글 너비가 0 이하이면 추정은
끝없이 돌지 않고 지원 밖(`page margins leaving no room for the body`)을 돌려준다.
- FormFit과 쪽 수 추정이 탭을 한/글처럼 놓는다.
- 탭은 단(셀이면 셀 글 영역)의 왼쪽 끝에서 4000 HWPUNIT마다 있는 기본 탭 위치 가운데 지금 위치 다음 것까지 간다.
줄은 문단 왼쪽 여백에서, 첫 줄은 들여쓰기만큼 더 들어가 시작한다(내어쓰기는 둘째 줄부터).
Expand Down
4 changes: 2 additions & 2 deletions src/hwpx/layout/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
from .report import LayoutFinding, LayoutLintReport
from ..opc.security import guard_zip_file, read_member
from ..oxml.header import parse_char_property, parse_paragraph_property
from ..oxml.section_format import _drawn_page_size
from ..oxml.section_format import _drawn_page_size, _page_margin
from ..oxml.table_sizes import cell_margins_of

if TYPE_CHECKING:
Expand Down Expand Up @@ -526,7 +526,7 @@ def _page_heights(root: ET.Element) -> tuple[int, int] | None:
int(page.get("width", "")), int(page.get("height", "")), page.get("landscape")
)
top, bottom, header, footer = (
int(margin.get(key, "0")) for key in ("top", "bottom", "header", "footer")
_page_margin(margin.get(key, "0"), key) for key in ("top", "bottom", "header", "footer")
)
except ValueError:
return None
Expand Down
15 changes: 10 additions & 5 deletions src/hwpx/layout/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@
from ..oxml.paragraph_heading import paragraph_heading
from ..oxml.section import _remove_short_paragraph_layout_cache
from ..oxml.header_part import HwpxOxmlHeader
from ..oxml.section_format import COLUMN_SHARES, _drawn_page_size
from ..oxml.section_format import COLUMN_SHARES, _drawn_page_size, _page_margin
from ..oxml.table_sizes import cell_margins_of, grid_widths_of

if TYPE_CHECKING:
Expand Down Expand Up @@ -1404,12 +1404,17 @@ def _page(section: Any) -> _Page:
if margin is None:
raise _Unsupported("no page settings")
width, height = _drawn_page_size(int(page.get("width", 0)), int(page.get("height", 0)), page.get("landscape"))
body = height - sum(int(margin.get(key, 0)) for key in ("top", "bottom", "header", "footer"))
text_width = width - sum(int(margin.get(key, 0)) for key in ("left", "right", "gutter"))
# A negative margin is kept as an unsigned 32-bit number: Hancom reads it signed, the body growing by it.
sides = {key: _page_margin(margin.get(key, 0), key) for key in ("top", "bottom", "header", "footer", "left",
"right", "gutter")}
body = height - sides["top"] - sides["bottom"] - sides["header"] - sides["footer"]
text_width = width - sides["left"] - sides["right"] - sides["gutter"]
if body <= 0 or text_width <= 0:
raise _Unsupported("page margins leaving no room for the body")
columns, column_width, widths, gap = _columns(section, text_width)
visibility = next(section.iter(f"{HP}visibility"), None)
return _Page(body, column_width, columns, int(margin.get("top", 0)) + int(margin.get("header", 0)),
int(margin.get("left", 0)) + int(margin.get("gutter", 0)), text_width, width, bool(widths),
return _Page(body, column_width, columns, sides["top"] + sides["header"],
sides["left"] + sides["gutter"], text_width, width, bool(widths),
widths, height,
visibility is not None and visibility.get("hideFirstEmptyLine") in ("1", "true"), gap)

Expand Down
16 changes: 16 additions & 0 deletions src/hwpx/oxml/section_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,22 @@ def _drawn_page_size(width: int, height: int, landscape: str | None) -> tuple[in
return (width, height) if landscape == _PAGE_PORTRAIT else (height, width)


#: What Hancom reads a page margin written with a minus sign as (it saves these back): the new-page margins.
_PAGE_MARGIN_FOR_MINUS = {"top": 5668, "bottom": 4252, "left": 8504, "right": 8504, "header": 4252, "footer": 4252,
"gutter": 0}


def _page_margin(value: str | int, side: str) -> int:
"""The *side* page margin (``hp:margin``) as Hancom reads it. A negative one is kept as an unsigned 32-bit
number (a header margin of -1400 as 4294965896, as a converted .hwp has it) and counts as negative; one
written with a minus sign is not a margin Hancom reads, and it takes the new-page margin for that side."""

if isinstance(value, str) and value.strip().startswith("-"):
return _PAGE_MARGIN_FOR_MINUS[side]
margin = int(value)
return margin - (1 << 32) if margin >= 1 << 31 else margin


def _hancom_page_orientation(
page_pr: ET.Element, orientation: str, width: int | None, height: int | None
) -> tuple[str, int | None, int | None]:
Expand Down
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.
66 changes: 66 additions & 0 deletions tests/test_layout_page_estimate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,72 @@ def test_a_shape_placed_up_from_its_paragraph_takes_no_more_than_its_foot() -> N
assert estimate.pages == 1


@pytest.mark.parametrize(
("name", "pages"),
[
("pages_margin_negative_header_one_page", 1),
("pages_margin_negative_header_two_pages", 2),
("pages_margin_negative_footer_one_page", 1),
("pages_margin_negative_footer_two_pages", 2),
("pages_margin_negative_both_one_page", 1),
("pages_margin_negative_both_two_pages", 2),
],
)
def test_a_negative_page_margin_kept_unsigned_counts_as_negative(name: str, pages: int) -> None:
# Header margin -1400, footer margin -3600 and both (-4000 and -3600), kept as unsigned 32-bit numbers
# (4294965896 ...) as a converted .hwp has them, saved by Hancom: the body grows by them, so the one-page
# files hold just as many one-line paragraphs as fit it and the two-page ones one more. Read as unsigned,
# the body was negative and the estimate never ended.
data = (FIXTURES / f"{name}.hwpx").read_bytes()

for estimate in (estimate_pages(data), estimate_pages(_without_caches(data))):
assert estimate.unsupported == ()
assert estimate.pages == pages


@pytest.mark.parametrize(("side", "written"), [("header", "-1400"), ("header", "-4000"), ("top", "-1000")])
def test_a_page_margin_written_with_a_minus_sign_takes_the_new_page_margin(side: str, written: str) -> None:
# Header margin written "-1400" (not the unsigned form), 46 one-line paragraphs: Hancom reads the minus sign as
# no margin it knows, lays the page with the new-page header margin 4252 and saves that -- two pages, where
# -1400 would have held them on one. The fixture is that save; written back with the minus sign (on the header
# or, with the new-page top 5668 it saves, the top), the estimate gives Hancom's two pages.
data = (FIXTURES / "pages_margin_minus_sign_header.hwpx").read_bytes()
out = io.BytesIO()
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 == "Contents/section0.xml":
root = etree.fromstring(payload)
(margin,) = [m for m in root.iter(f"{HP}margin") if m.getparent().tag == f"{HP}pagePr"]
assert margin.get(side) == {"header": "4252", "top": "5668"}[side]
margin.set(side, written)
payload = etree.tostring(root, xml_declaration=True, encoding="UTF-8", standalone=True)
target.writestr(info, payload)
minus = out.getvalue()

for estimate in (estimate_pages(data), estimate_pages(minus), estimate_pages(_without_caches(minus))):
assert estimate.unsupported == ()
assert estimate.pages == 2


def test_page_margins_leaving_no_body_are_unsupported() -> None:
data = (FIXTURES / "pages_margin_negative_header_one_page.hwpx").read_bytes()
out = io.BytesIO()
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 == "Contents/section0.xml":
root = etree.fromstring(payload)
(margin,) = [m for m in root.iter(f"{HP}margin") if m.getparent().tag == f"{HP}pagePr"]
margin.set("top", "90000")
payload = etree.tostring(root, xml_declaration=True, encoding="UTF-8", standalone=True)
target.writestr(info, payload)

estimate = estimate_pages(out.getvalue())
assert estimate.pages is None
assert any("no room for the body" in reason for reason in estimate.unsupported)


def test_an_offset_written_with_a_minus_sign_is_read_as_zero() -> None:
# The same rectangle written vertOffset="-1500" (not the unsigned form): Hancom reads it as 0, draws the
# rectangle at the paragraph's top and saves 0. The fixture is that save; written back with the minus sign,
Expand Down
Loading