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 @@ -105,6 +105,12 @@
글을 줄 끝에서 끝낸다. 줄 끝에 닿은 탭은 다음 줄로 간다. 오른쪽 끝 자동 탭은 줄 끝의 오른쪽
위치로 함께 센다. 그다음은 기본 탭 위치다. 맞출 수 없어 지나친 위치가 있는데 기본 탭 위치에서 글이 줄에 들어가지
않으면 탭은 폭이 없다. 내어쓰기 자동 탭은 둘째 줄이 시작하는 곳에 왼쪽 위치를 둔다.

- .hwp를 열 때 높이가 음수인 표 칸(파일에는 부호 없는 32비트로 남는다, -633이면 4294966663)을 한/글처럼 그 칸의
위·아래 여백 합(칸 자체 여백이 있으면 그것, 없으면 표 안 여백) 높이로 바꾼다. 전에는 그 값을 그대로 써서 큰 양수
높이로 읽혔다. 한/글은 그 높이에서 글에 맞게 칸을 키운다.
- 쪽 수 추정과 배치 검사가 HWPX의 음수 칸 높이(부호 없는 32비트로 남은 값)를 없는 높이로 센다. 한/글은 그 칸을 위·아래
여백 높이로 놓고 글에 맞게 키우는데, 전에는 40억이 넘는 높이로 읽어 .hwp를 바꾼 문서의 쪽 수가 수만 쪽으로 나왔다.
- 쪽 수 추정에서 강제 줄 바꿈 다음 줄은 첫 줄이 아니다. 들여쓰기를 빼지 않고 다른 줄의 폭으로 잰다. 전에는 첫 줄 폭으로
재서, 들여쓴 문단에서 줄이 하나 더 생길 수 있었다.
- FormFit과 쪽 수 추정이 텍스트 내보내기가 쓰지 않는 번호 형식(`KOREAN_DIGIT`, `HANGUL` 등)의 문단 번호를 만나도
Expand Down
18 changes: 15 additions & 3 deletions src/hwpx/hwp5/section_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -1336,7 +1336,7 @@ def table(self, run: etree._Element, ctrl: rec.Record) -> etree._Element:
for row_size in tp.row_sizes or [len(cells)]:
tr = sub(table, "hp:tr")
for header, paragraphs in cells[position : position + row_size]:
self.cell(tr, header, paragraphs)
self.cell(tr, header, paragraphs, tp.inner)
position += row_size
for data in (r for r in ctrl.children if r.tag == rec.CTRL_DATA):
if data.payload.startswith(ct.NAME_SET_PREFIX):
Expand Down Expand Up @@ -1371,9 +1371,21 @@ def caption(self, parent: etree._Element, header: rec.Record, paragraphs: list[r
sub_list = sub(element, "hp:subList", list_attrs(cap.list_props))
self.paragraphs(sub_list, paragraphs)

def cell(self, tr: etree._Element, header: rec.Record, paragraphs: list[rec.Record]) -> None:
def cell(
self,
tr: etree._Element,
header: rec.Record,
paragraphs: list[rec.Record],
table_inner: tuple[int, int, int, int] = (510, 510, 141, 141),
) -> None:
cell = ct.CellHeader.decode(header.payload)
flags = cell.flags
height = cell.height
if height >= 1 << 31:
# A negative height, kept unsigned: Hancom gives the cell its top and bottom margins (its own, or
# the table's inner ones), which it then grows to fit the text.
_left, _right, top, bottom = cell.margins if flags & 0x1 else table_inner
height = top + bottom
tc = sub(
tr,
"hp:tc",
Expand All @@ -1391,7 +1403,7 @@ def cell(self, tr: etree._Element, header: rec.Record, paragraphs: list[rec.Reco
self.paragraphs(sub_list, paragraphs)
sub(tc, "hp:cellAddr", (("colAddr", cell.col), ("rowAddr", cell.row)))
sub(tc, "hp:cellSpan", (("colSpan", cell.col_span), ("rowSpan", cell.row_span)))
sub(tc, "hp:cellSz", (("width", cell.width), ("height", cell.height)))
sub(tc, "hp:cellSz", (("width", cell.width), ("height", height)))
left, right, top, bottom = cell.margins
sub(
tc,
Expand Down
3 changes: 2 additions & 1 deletion src/hwpx/layout/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,8 @@ def _row_min_heights(table: ET.Element, lines: "_LineHeights") -> list[int]:
heights = [0]
for cell in row:
if _local_name(cell) == "tc" and _cell_int(cell, "cellSpan", "rowSpan", 1) == 1:
heights.append(max(_cell_int(cell, "cellSz", "height", 0), lines.cell_height(cell, table)))
stored = _cell_int(cell, "cellSz", "height", 0) # a negative one is kept unsigned: none
heights.append(max(0 if stored >= 1 << 31 else stored, lines.cell_height(cell, table)))
heights_by_row.append(max(heights))
return heights_by_row

Expand Down
12 changes: 10 additions & 2 deletions src/hwpx/layout/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,14 @@ def _table_alone(runs: list[Any]) -> Any:
return children[0] if pos is not None and pos.get("treatAsChar") == "1" else None


def _cell_height(size: Any) -> int:
"""A cell's stored height (hp:cellSz): a negative one, which the file keeps as an unsigned 32-bit number,
counts as none -- Hancom gives that cell its top and bottom margins and grows it to fit its text."""

height = int(size.get("height", 0)) if size is not None else 0
return 0 if height >= 1 << 31 else height


def _margin(margin: Any, side: str) -> int:
"""One side of an object's outer margins (hp:outMargin): a negative one, which the file keeps as an
unsigned 32-bit number, counts as none, as Hancom lays it out (and saves it)."""
Expand Down Expand Up @@ -1046,7 +1054,7 @@ def _cell_row(measure: _Measure, table: Any, cell: Any, width: int | None = None
if _vertical(cell): # its lines go across it, the row as declared: one block, not split
margins = cell_margins_of(cell, table)
vertical = margins.top + margins.bottom
height = max(int(cell.find(f"{HP}cellSz").get("height", 0)), vertical)
height = max(_cell_height(cell.find(f"{HP}cellSz")), vertical)
return _Row(height, 1, height - vertical, height - vertical, vertical, cell.get("header") == "1",
first=height - vertical)
paragraphs = cell.findall(f"{HP}subList/{HP}p")
Expand Down Expand Up @@ -1081,7 +1089,7 @@ def _cell_row(measure: _Measure, table: Any, cell: Any, width: int | None = None
if drawn:
content, lines = sum(advance for _, advance in drawn[:-1]) + drawn[-1][0], len(drawn)
vertical = margins.top + margins.bottom
height = max(int(size.get("height", 0)), vertical + content)
height = max(_cell_height(size), vertical + content)
first = drawn[0][0] if drawn else \
before + (measure.stack_lines(paragraphs[:1], inner, caches=True, pieces=pieces) or ((0, 0),))[0][0]
several = not nested and len(paragraphs) > 1 # its lines split at their own places, spacing included
Expand Down
Binary file not shown.
Binary file added tests/fixtures/hwp5/cell_height_negative.hwp
Binary file not shown.
Binary file not shown.
23 changes: 23 additions & 0 deletions tests/test_hwp5_open.py
Original file line number Diff line number Diff line change
Expand Up @@ -1214,3 +1214,26 @@ def test_the_converted_package_saves_as_hwpx(tmp_path: Path) -> None:
document.save_to_path(target)
reopened = HwpxDocument.open(target)
assert [p.text for p in reopened.paragraphs] == [p.text for p in document.paragraphs]


@pytest.mark.parametrize(
("name", "margins"),
[("cell_height_negative", ("0", "141", "141")), ("cell_height_negative_own_margins", ("1", "300", "500"))],
)
def test_a_negative_cell_height_takes_the_cells_top_and_bottom_margins(
name: str, margins: tuple[str, str, str]
) -> None:
# Row 2, column 1 of a 3x2 table holds the height -633 (-1000 in the second, whose cell has margins of its
# own, top 300 and bottom 500), kept unsigned as the file has it. Hancom converts such a cell with the height
# of its top and bottom margins -- the table's inner ones (141 + 141) or its own (300 + 500) -- and grows it
# to fit its text; the other cells keep theirs.
data = (Path(__file__).parent / "fixtures" / "hwp5" / f"{name}.hwp").read_bytes()
root = HwpxDocument.open(data).sections[0].element

cells = [
(tc.get("hasMargin"), tc.find(f"{HP}cellSz").get("height"), tc.find(f"{HP}cellMargin")) for tc in root.iter(f"{HP}tc")
]
has_margin, height, margin = cells[2]
assert (has_margin, margin.get("top"), margin.get("bottom")) == margins
assert int(height) == int(margins[1]) + int(margins[2])
assert all(int(other) < 1 << 31 for _, other, _ in cells)
24 changes: 24 additions & 0 deletions tests/test_layout_page_estimate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1468,6 +1468,30 @@ def test_the_evaluation_plan_form_lays_out_like_hancom_without_caches() -> None:
_assert_like_hancom(estimate_pages(_without_caches(data)), data, 15)


@pytest.mark.parametrize("height", [str((1 << 32) - 633), str((1 << 32) - 1)])
def test_a_negative_cell_height_kept_unsigned_counts_as_none(height: str) -> None:
# A 3x2 table whose row-2 cell was written with the height -633 kept unsigned (4294966663), saved by Hancom:
# it gives the cell its top and bottom margins (282) and grows it to fit the text, one page. Written back
# unsigned, the cell is not four billion tall: the estimate gives Hancom's page.
data = (FIXTURES / "pages_cell_height_negative.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)
cell = list(root.iter(f"{HP}tc"))[2]
assert cell.find(f"{HP}cellSz").get("height") == "282"
cell.find(f"{HP}cellSz").set("height", height)
payload = etree.tostring(root, xml_declaration=True, encoding="UTF-8", standalone=True)
target.writestr(info, payload)
negative = out.getvalue()

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


@pytest.mark.parametrize("name", ["pages_table_with_cell_spacing", "pages_table_without_cell_spacing"])
def test_a_table_with_cell_spacing_lays_its_cells_out_at_their_own_widths(name: str) -> None:
# A table 46196 wide: one cell over its three columns, then cells of 15000, 21000 and 9800 with a cell
Expand Down
Loading