Skip to content

fix: #8149 빈 host 자리차지 표의 저장 상자·쪽 바닥 첫 조각 계약 (사양서 74→71쪽) - #8184

Open
planet6897 wants to merge 25 commits into
edwardkim:develfrom
planet6897:fix/8149-empty-host-float-outer-top
Open

planet6897 wants to merge 25 commits into
edwardkim:develfrom
planet6897:fix/8149-empty-host-float-outer-top

Conversation

@planet6897

@planet6897 planet6897 commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

요약

한컴 「한글문서파일형식 5.0 revision1.3」(samples/한글문서파일형식_5.0_revision1.3.hwp)이 devel 에서 74쪽(정본 PDF 71쪽)으로 나오던 문제를 고칩니다. 원인은 글 없는 host 문단의 문단 기준 자리차지 표(빈 host 표)를 둘러싼 여러 계약이 조판과 배치에서 어긋난 것입니다. 이 브랜치에서 71쪽 일치, 정본 대조 글줄 위치가 어긋난 쪽 48 → 10(남은 10쪽 중 실결함은 2쪽, 아래 「남은 범위」).

Closes #8149 · 관련 #7735(47·50쪽 부근 +1 두 건이 함께 해소)

고친 규칙 (커밋 순)

커밋 규칙 독립 근거
18d868dd2 아래 캡션이 있는 빈 host 표도 저장 사다리 상자로 받는다(캡션 = 캡션 간격 + 저장 캡션 줄). 다음 문단 앞 간격이 상자 안에 흡수된 사다리(델타 = 상자)도 인정 저장 사다리 등식, 합성 gen_captioned_host.py 한/글 2020
8be70c87a 흡수 등식은 바로 앞 조판 항목이 앞 문단의 빈 host 표일 때만 반례 issue_6797(글 host 띠 뒤)
fcdf1e7d5 인증 안 된 빈 host 표 뒤 문단도 앞 간격을 min(앞 간격, 바깥 아래 여백) 만큼 흡수, 저장 원점 대체(세로 오프셋 0 한정), 단독 표 host 줄간격은 캡션에 안 더함 정본 30·50쪽, 반례 issue_7379
89f600725 쪽 바닥에서 나뉘는 빈 host 표 첫 조각은 바깥 아래 여백 + 아래 캡션 을 남긴다. 선언 높이(#7147 첫 조각)가 행 경계에 맞으면 그 행이 상한 합성 gen_rowfit.py 16사례 + gen_reservefit.py 32사례(여백 200HU 간격, 예약 오차 0.1px)
6556f9713 재조판 빈 host 상자는 문단 윗변에서, 분할 진입 검사는 첫 줄 앞 간격 차감 정본 38·39쪽
37a73d648 · 634c88189 TAC 표 host 위 간격·아래 캡션 띠·TAC 도형 host 줄간격을 HWP5 저장 사다리대로 배치 정본 62쪽, 저장 vpos
1842eaa3c 앞 간격 흡수는 스냅 전 상자 바닥에서 잰다(이중 차감 제거) 정본 41·51쪽
d2ea85186 첫 조각 예약을 행 예산과 첫 행 이월 판정이 같은 helper 로 공유(선언이 모든 행을 덮을 때만 이월에 사용), 통째 첫 조각 흐름 = 끝 조각 점유식, 이월 뒤 단 첫머리는 앞 간격 없이 정본 32·33·42쪽, 합성 gen_headerrow.py 15사례(한/글도 제목 행만 남기는 분할 허용 — H03)
fdde0bcf2 저장 원점 상자 꼬리에서 단독 표 host 줄간격 제외, host 앞 간격은 표를 여는 자리에서 결정 저장 사다리(33쪽 202→203: 다음 줄 vpos 45542 = 상자 바닥), 정본 44~49쪽
27d782cd8 단 첫머리가 빈 host 표인 쪽의 저장 원점 = 다음 줄 − (위 여백 + 높이 + 아래 캡션 + 아래 여백). host 줄이 위 여백 띠 안일 때만, 세로 오프셋 0 한정 42쪽 270→271: 0+566+14476+1802+1984 = 18828, 반례 issue_4068·issue7198

조판과 배치가 같은 함수(stored_table_column_origin, empty_host_float_first_fragment_reserve_px, empty_host_float_successor_absorbed_spacing_px)를 소비합니다.

검증

  • 소스: head f1c7a4ec6c (base upstream/devel def947c). 정책 검사 base 도 def947c.
  • cargo nextest run --cargo-profile release-test: 11356 / 11356 통과(새 4건 포함).
  • Rust lint 묶음: native clippy · WASM clippy · workspace build · workspace all-targets clippy · rust-test-suite-manifest --check --base-ref def947c21 통과. rustfmt 는 커밋된 blob 기준 통과.
  • 새 회귀 tests/cases/issue_8149_empty_host_float_page_bottom.rs
    • spec_header_only_split_follows_declared_first_fragment — 사양서 71쪽, 표 199 는 33쪽에만, 표 334 는 51·52쪽에 나뉨. 수정 전(devel) 74쪽이라 실패.
    • 합성 3종(rowfit·reservefit·headerrow) 쪽별 행 소속 = 한/글 2020 PDF 본문 글자. 한/글 저장본은 저장 첫 프레임 경로로도 맞아 수정 전에도 통과 — 계약 보호용입니다.

Visual Sweep (전쪽, 판정값 silhouette_raw_match_percent, 96dpi, 로컬 Windows)

문서 · 기준 PDF 쪽수 rhwp/PDF Native 최저 fresh WASM 최저 90% 미만
사양서 · …-hancom-official.pdf(Hwp 2018) 71/71 (devel 74) 93.80 (16쪽) 94.14 (16쪽) 없음
issue8149/captioned-host-2020 3/3 100.00 100.00 없음
issue8149/rowfit-2020 33/33 100.00 100.00 없음
issue8149/headerrow-2020 31/31 100.00 100.00 없음
issue8149/reservefit-2020 65/65 100.00 100.00 없음

devel 은 사양서 74쪽이라 쪽별 대조가 불가(전체 쪽수 불일치). 실루엣 보조값은 2px 이웃 관용 내용 실루엣이며 이진화·색상 경계 원값을 함께 보존했습니다(TSV 는 로컬 output/pr-review/8149/).

대표 증적 (증적 head 567ff9d18, code head d878c2ad5, base 238de6de4 — 아래 「최신 devel rebase (238de6d)」)

33쪽 — 표 199 를 32쪽 바닥에 제목 행만 남기지 않고 통째로 넘김, 뒤 표 203 의 저장 상자(raw Native 99.25% / WASM 99.88%)

spec p33 review
spec p33 overlay

43쪽 — 표 273 첫 조각이 저장 프레임대로 11행까지(raw 99.78% / 99.78%)

spec p43 review
spec p43 overlay

47쪽 — 이월된 표 297 이 쪽 첫머리에서 앞 간격 없이(raw 99.59% / 99.59%)

spec p47 review
spec p47 overlay

WASM review PNG 는 같은 폴더의 spec-p0NN-wasm-review.png.

직접 판독

33·41·43·47쪽을 정본과 같은 배율로 대조: 표 상자 위치·쪽 소속·캡션·뒤 제목 위치 일치. 남은 차이는 표 바깥 테두리의 색·굵기(정본은 더 굵은 파란 외곽선)로 이번 변경 범위 밖입니다.

남은 범위 (미해결 · 미검증)

최신 devel rebase (2026-10-26, #8191 반영 devel)

기준 upstream/devel 9418ed0c7, code head ecb928662, 증적 head 5c80b5d48(이전 f1c7a4ec6). #8191 이 같은 축(빈 host 표 원점·각주 경계·물리 행 프레임)을 크게 바꿔, 단순 rebase 뒤 아래 회귀가 드러났고 각각 원인에서 고쳤습니다.

드러난 문제 원인(이 PR 의 커밋) 수정
컴파일 오류 devel 의 layout.rs 출력 폴백이 from_empty_stored_ladder_host 를 새 인자 없이 호출 9438ba4e9 — 폴백은 종전대로 true
hwpspec 54쪽 89.24 → 79.56 앞 간격 흡수(fcdf1e7d5)가 저장 줄이 앞 간격을 증언하는 테두리 문단(이어지는 마지막 조각 뒤 pi=341)도 13.3px 당김 a044f5eb9·0276b5ac6 — 다음 문단의 저장 첫 줄이 증언하는 소비량(0..앞 간격)을 쓴다. samples 흡수 사례 118곳 중 117곳은 종전 흡수량과 같다
issue_7470 visible_successor_uses_the_original_line_origin… 같은 흡수가 devel 의 stored_after_partial_table_shared_spacing_px 커서 맞춤을 1.9px 어긋나게 함(rowbreak-problem-pages 5쪽) 같은 수정 — 저장 줄이 앞 간격 전부의 소비를 증언
issue_4068 only_the_column_top_table_receives_outer_margin 통째 첫 조각 흐름 직접 합산(d2ea85186)이 host 간격 합의 뒤 몫을 버리고 단 맨 위 표의 바깥 위 여백을 흐름에 넣음 c14d69ee1 — 흐름 = 표 높이 + host 간격 합 + max(0, 끝 점유 − 합의 뒤 몫). 사양서 표 199 175.9(종전과 같음), 2912695 2쪽 표 3 779.6(devel 과 같음)
issue_5846 cut_fragment_defers_partial_nested_tail_row(75544 65쪽 표 619 넷째 행 소실) 첫 조각 바닥 예약(89f600725)과 devel 의 저장 되감김 꼬리말 여유(STORED_REWIND_FIRST_FRAGMENT_PAINT_FOOTER_GUARD_PX 4px)가 같은 쪽 바닥 띠를 겹쳐 뺌 ecb928662 — 예약에서 여유만큼 뺀다. 한/글은 그 쪽에서 본문 끝 5.9px 위에서 첫 조각을 닫는다(devel 렌더·한/글 PDF 괘선 1px 안 일치)

2c53026ab(예약을 첫 행 이월 판정에만)·d293089f3(흐름을 devel 식으로)는 원인을 좁히는 중간 단계였고, 각각 ecb928662·c14d69ee1 이 대체합니다(이력 보존).

바닥 예약의 한/글 오라클 확인

메인터너 권고대로 한/글을 직접 실행해 규칙을 확인했습니다. 한/글 2024(13.0.0.564)로 저장·Print 한 합성(빈 host 문단 기준 자리차지 표, 1열 12행, 행마다 세 줄)입니다.

  • 바깥 아래 여백 0/141/500/1000/1984HU × 아래 캡션 × 채움 3종(30사례): 1984HU(26.5px)·캡션이 있으면 행 안 컷의 마지막 줄을 하나 더 넘겨 여백(+캡션) 만큼 남긴다.
  • 바깥 아래 여백 0/141HU × 바깥 위 여백 566HU 부터 75HU(1px) 간격 24단계(48사례): 141HU 계열은 줄을 넘기는 문턱이 0 계열보다 약 2px 앞이다(위 여백 1841·1916HU 에서 0 계열은 행6 줄3 까지, 141 계열은 줄2 까지). 1.9px 여백도 행 안 컷에 남긴다.

그래서 바닥 예약은 첫 조각 예산 전체(통째 행·행 안 컷)에 둡니다. 합성 입력은 rhwp 가 같은 문턱의 두 사례씩(4쪽, 46~50%)에서 한/글과 한 줄 다르게 끊어 회귀 fixture 기준(전쪽 90% 이상)에 못 미치므로 저장소에 넣지 않았습니다. 두 여백 계열에서 똑같이 생기는 줄 단위 컷 문턱 차이(약 1px)이며 바닥 예약과 무관합니다(미해결).

검증 (code head ecb928662, base 9418ed0c7)

  • rustfmt 차이 0 · Clippy native / wasm32 / workspace all-targets / native-skia all-targets 경고 0(매 단계 소스 touch 후 실제 재컴파일) · manifest --check · unit tier --check 통과.
  • nextest(--features native-skia, release-test) 11,762건 중 11,758 통과 — 실패 4건은 devel 에서도 같은 이 host 환경 문제다(issue_7966_pdf_close_stroke 3건은 Windows 체크아웃 CRLF, fix(checkout): Windows CRLF·심볼릭 링크 체크아웃에서만 실패하던 로컬 검사를 고친다 #8193 · custom_font_paths_select_style_within_the_first_family_owner 는 글꼴 설치).
  • 새 회귀 successor_keeps_spacing_when_its_stored_line_says_so: hwpspec 54쪽 문단 341 첫 줄이 저장 줄 위치(본문 위끝 + vpos)에 있다. 반례로 흡수가 맞는 hwpspec 53쪽 문단 337·사양서 55쪽 문단 349 도 저장 줄에 있다. 수정 전 문단 341 assertion 에서 실패, 수정 후 통과.
문서 · 기준 PDF (Visual Sweep 전 쪽, raw 실루엣 TSV 4열) 쪽수 rhwp/PDF devel 9418ed0c7 Native / WASM 이 PR Native / WASM
사양서 · …-hancom-official.pdf (--compat 2024) 71/71 (devel 83) 쪽수 불일치로 대조 불가 최저 95.23 / 94.65, 90% 미만 0
samples/hwpspec.hwp · pdf/hwpspec-2024.pdf 178/178 90% 미만 8쪽(최저 63.14 / 62.50) 90% 미만 2쪽: 23쪽 86.84 / 86.78 · 54쪽 89.24 / 89.39 (둘 다 devel 과 같은 값)

위 대표 증적은 이 head 로 다시 캡처했습니다(같은 파일 이름). 33쪽 Native raw 99.20%. samples A/B·10k 코퍼스는 이번 base 에서 다시 돌리지 않았습니다.

최신 devel rebase (2026-10-26, 238de6de4 — #8228 통합 반영)

기준 upstream/devel 238de6de4, code head d878c2ad5, 증적 head 567ff9d18(이전 5c80b5d48). 위 9418ed0c7 rebase 의 커밋·보정은 그대로 두고 옮겼습니다.

드러난 문제 원인 수정
stored_empty_host_frame.rs 충돌 #8228 이 from_stored_ladder 를 저장본·재조판 공용 from_ladder 로 나눔(재조판은 높이 일치 검사 생략) 아래 캡션 높이를 더한 선언 높이로 from_ladder 의 검사를 저장본 경로에만 둔다
컴파일 오류(origin_closes_float_frame) 앞 간격 흡수 인자가 새 from_ladder 로 이어지지 않음 d878c2ad5 — 저장본 경로는 받은 값을, from_reflowed_ladder 는 앞 항목 상자 끝을 증언하는 호출자가 없어 false

검증 (code head d878c2ad5, base 238de6de4, macOS)

  • --prepare → rustfmt --check · Clippy native / wasm32 lib / workspace all-targets 경고 0 · workspace build · manifest --check --base-ref 238de6de4 · unit tier --check --base-ref 238de6de4 통과.
  • cargo test --locked --no-fail-fast --profile release-test --tests: 11,486 통과 / 0 실패(issue_7470·issue_4068·issue_5846·issue_8149 포함).
  • Visual Sweep(전쪽, --silhouette-only, 96dpi, RHWP_FONT_PATH=ttfs/opensource:ttfs/redistributable:rhwp-studio/public/fonts, 판정값 silhouette_raw_match_percent):
문서 · 기준 PDF 쪽수 rhwp/PDF devel 238de6de4 Native / WASM 이 PR Native / WASM TSV
사양서 · …-hancom-official.pdf (--compat 2024) 71/71 (devel 83) 쪽수 불일치로 대조 불가 최저 95.22 / 95.14(8쪽), 90% 미만 0 native · wasm
samples/hwpspec.hwp · pdf/hwpspec-2024.pdf (--compat 2024) 178/178 90% 미만 8쪽(최저 20쪽 61.07 / 61.51) 90% 미만 2쪽: 23쪽 87.21 / 87.05 · 54쪽 89.59 / 89.62 (devel 과 같은 값) native · wasm · devel native · wasm
issue8149/captioned-host-2020 3/3 – 100.00 / 100.00 native · wasm
issue8149/rowfit-2020 33/33 – 100.00 / 100.00 native · wasm
issue8149/headerrow-2020 31/31 – 100.00 / 100.00 native · wasm
issue8149/reservefit-2020 65/65 – 100.00 / 100.00 native · wasm

hwpspec 23·54쪽은 devel 에서도 같은 값으로 90% 미만이며 이 PR 이 바꾼 쪽이 아닙니다(위 「남은 범위」). 대표 PNG(33·43·47쪽)는 이 head 로 다시 캡처했고 직접 판독에서 표 상자·쪽 소속·뒤 제목 위치가 정본과 맞습니다. 표 캡션 번호(정본 「표 42」, rhwp 「표 1」)는 필드 번호 차이로 이번 범위 밖입니다.

🤖 Generated with Claude Code

…인정한다 (edwardkim#8149)

`from_stored_ladder` 는 다음 저장 줄이 닫는 빈 host 자리차지 표의 상자를 조판·배치에 공유한다
(`다음 − host = 위 여백 + 선언 높이 + 아래 여백 − host 앞 간격`). 그런데 캡션이 있는 표를 통째로
거절해, 아래 캡션이 붙은 표는 옛 통째 배치로 떨어졌다. 그 경로는 표를 바깥 위 여백 없이 그리고
표 뒤 흐름 끝을 다음 문단의 저장 줄에 둔 뒤 그 문단이 위 간격을 또 더해, 표마다 흐름이 6.7px
씩 내려갔다(한글문서파일형식 39쪽, 마지막 행이 다음 쪽으로 넘어가 74쪽).

- 아래 캡션은 `캡션 간격 + 저장 캡션 줄` 을 선언 높이에 더한다(표 235: 10545 = 566 + 6193 +
  852 + 950 + 1984). 위·왼쪽·오른쪽 캡션은 본체 원점·폭이 다른 기하라 계속 거절한다.
- 앞 항목이 자리차지 표 상자로 끝난 host 는 한/글이 앞 간격을 두지 않는다. 사다리가
  `다음 − host = 상자` 로 그것을 증명하면 같은 상자로 받는다(표 235: 앞은 표 234 조각).

두 경우 모두 다음 저장 줄이 상자 바닥이고, 후속 문단은 기존 `stored_frame_successor_shared_spacing`
계약으로 그 줄에 앞 간격을 한 번만 소비한다.

결과(devel 605ccfd → 이 커밋, 96dpi, 정본 PDF 좌표):
- 표 235 251.7 (244.2→, 정본 251.7) · pi=236 384.8 (391.4→, 384.6)
- 표 239 500.9 (500.0→, 500.5) · pi=240 606.2 (619.5→, 605.8)
- pi=246·254 가 쪽을 나누지 않고 74 → 73쪽(정본 71쪽, 47·50쪽 부근 +1 두 번은 edwardkim#7735)

남은 범위: 다음 문단에 저장 줄이 없는 경우(40쪽 표 250·254 뒤 「4.3.x」 제목)는 사다리로 상자를
인증할 수 없어 옛 경로에 남는다 — 정본 40쪽 끝 pi=256 은 아직 41쪽으로 넘어간다.
`델타 = 상자` 는 host 앞 간격이 흡수됐다는 사다리의 증언이지만, 원점(조판 커서)이 그 저장 줄에
있다는 증거는 아니다. 156160455 7쪽 pi=71 은 글이 있는 host(pi=70)의 자리차지 밴드 뒤라 커서가
host 글줄 끝(174.8)에 있고 저장 줄은 밴드 바닥(296.8)이다. 첫 커밋은 이 경우도 받아 표를 앞
표와 겹쳐 그렸다(issue_6797 회귀).

`from_empty_stored_ladder_host` 에 `origin_closes_float_frame` 을 두어, 바로 앞 조판 항목이 앞
문단(글 없는 host)의 자리차지 표 상자일 때만 흡수 등식을 받는다. 조판 블록 경로만 이를 확인할
수 있으므로 거기서 넘기고, 셀 안·lane 경로는 종전 등식만 쓴다.
…#8149)

한글문서파일형식 정본 PDF·저장 사다리로 확정한 한/글 규칙(빈 host 문단기준 자리차지 표):
표 원점 = host 문단 위끝 + 바깥 위 여백, 다음 문단의 앞 간격은 바깥 아래 여백 안에서 소비.
저장 사다리가 상자를 인증하지 못하는 경우(다음 문단에 저장 줄이 없는 「4.3.x」 제목 등)에도
같은 규칙을 공유 결과로 낸다.

- 후속 문단 앞 간격: `empty_host_float_successor_absorbed_spacing_px` — 바로 앞 조판 항목이
  글 없는 host 의 세로 오프셋 0 자리차지 표(끝 조각)이면 `min(앞 간격, 바깥 아래 여백)` 을
  조판(flow.rs)과 배치(layout.rs)가 같은 자리에서 소비한다. 인증 배치가 있으면 기존
  `stored_frame_successor_shared_spacing_px` 가 맡는다.
- 표 원점: `from_saved_whole_empty_host` 가 저장 단 원점이 없을 때 조판 커서(문단 위끝) +
  적용한 앞 간격을 원점으로 쓴다(세로 오프셋 0 한정 — 양수 오프셋은 edwardkim#4068 계약).
- 캡션 높이: 글자 없이 표 하나만 든 host 는 캡션에 host 줄간격을 더하지 않는다(저장 줄 폭이
  0 이 아니어도 — 30쪽 표 185 의 폭 39628).

결과(정본 쪽마다 글줄 위치 대조, |중앙값|>2 또는 |최대|>3px 인 쪽): 48 → 33, 전체 73 → 71쪽
(정본 71). 13쪽 표 51(오프셋 경로)·쪽 바닥 행 분할은 남는다.
…m#8149)

한/글은 쪽 바닥에서 나뉘는 빈 host 문단기준 자리차지 표의 첫 조각 아래에 그 표의 바깥 아래
여백과 (아래 캡션이면) 캡션 자리(간격 + 캡션 줄 높이)를 남긴다. 한/글 2020 저장·Print 합성
`samples/issue8149/gen_rowfit.py` 16사례(캡션 유무 × 아래 여백 1984/0 × 행 높이 4종)로 확정:
캡션 없음·여백 0 은 본문 끝까지, 여백만 있으면 여백, 아래 캡션이면 여백 + 캡션.

rhwp 는 저장 첫 조각 프레임(선언 개체 높이 = 첫 조각, edwardkim#7147)이 있으면 그 높이로 같은 컷을
얻었지만, 저장 근거가 없는 쪽(한글문서파일형식 38쪽 표 234 등)에서는 아무것도 남기지 않아
한 행씩 더 받았다.

- 저장 근거가 없을 때 첫 조각 예산에서 `바깥 아래 여백 + 아래 캡션` 을 뺀다(확정 배치가 예산을
  다시 정하는 갈래 포함).
- 저장 첫 조각 프레임의 앵커 대조: 저장 앵커는 host 줄 위치(앞 간격 뒤)이므로 흐름 원점(문단
  위끝)에 앞 간격을 더해 견준다 — 종전에는 이 차이(6.67px)로 프레임을 버렸다.
- 선언 개체 높이가 행 경계에 1px 안으로 적중하면 그 행 끝을 첫 조각 상한으로 쓰고(edwardkim#7147 판별),
  저장 프레임이 인정되면 위 계산 예약은 쓰지 않는다.

합성 16사례 모두 한/글과 같은 행에서 끊고, 한글문서파일형식 정본 대조 어긋난 쪽 33 → 28.
…ardkim#8149)

- `from_empty_reflow_host`: 저장 원점은 host 줄 위치(문단 앞 간격 뒤)다. 한/글은 문단 기준 표를
  문단 윗변 + 바깥 위 여백에 두므로(edwardkim#7620, `from_saved_whole_empty_host` 와 같은 규칙) 적용한
  앞 간격을 뺀 자리에서 상자를 세운다. 한글문서파일형식 13쪽 표 51(나누지 않음)이 +6.7px 아래였다.
- 분할 진입 검사: 첫 줄은 문단 앞 간격 뒤에 놓인다. 분할 루프 예산은 그 간격을 빼는데 진입
  검사만 빼지 않아, 루프가 면제하는 조각 첫 줄(edwardkim#6568)이 본문 바닥을 넘어도 그 쪽에 남았다
  (13쪽 pi=53: 남은 16.43 − 앞 간격 6.67 = 9.76 < 첫 줄 13.33 — 한/글은 다음 쪽). drift 안전
  여백은 여기서 빼지 않는다(edwardkim#6568 주석의 이중 차감 회귀).

한글문서파일형식 정본 대조 어긋난 쪽 28 → 27, 악화 0. 합성 rowfit 16사례 유지. nextest 11352 통과.
…dwardkim#8149)

앞 항목이 TAC 표면 배치는 저장 vpos 보정을 건너뛰고, 다음 TAC 표 host 의 위 간격은 edwardkim#5809
규칙으로 직접 더한다. 그 규칙이 HWPX 저장본(또는 저장 줄 없는 문단)에만 걸려 HWP5 저장본에서는
위 간격이 빠졌다. HWP5 저장 사다리도 그 간격을 둔다 — 한글문서파일형식 25쪽 pi=166→167:
5438 + 15368 + 600 + 500 = 21906. 조판은 이 간격을 이미 전진에 넣으므로(조판 커서 285.5 =
저장 문단 상단 285.4) 배치만 표마다 6.67px 씩 위로 누적됐다.

정본 대조: 25쪽 −19.4 → 0.7, 62쪽 −13.7 → −7.1, 악화 0(어긋난 쪽 27 → 26). nextest 11352 통과.
…im#8149)

- TAC 표: 배치가 표 상단을 «줄 상단 + om_top» 에 둘지 정하는 edwardkim#5729 갈래에서, 저장 밴드가
  `om_top + 선언 높이 + 아래 캡션(간격 + 저장 캡션 줄) + om_bottom` 인 경우도 바깥 상자로
  인정한다(`tac_stored_band_is_captioned_outer_box`). 한글문서파일형식 62쪽 표 397:
  9363 = 566 + 5011 + 1802 + 1984 — 종전 om_top 만큼 위(−7.1px). 조판의 후속 원점 판정
  (`tac_stored_band_is_outer_box`)은 캡션이 섞인 줄을 표 원점으로 쓰지 않는 계약이라 그대로
  둔다(넓히면 rowbreak_short_row_whole_carry 컷 14건이 바뀐다).
- TAC 도형 host: 다음 저장 줄이 `줄 + 줄간격` 만큼 떨어질 때만 꼬리 줄간격을 복원했다. 사다리
  경계는 `줄 + 줄간격 + 이 문단 아래 간격 + 다음 문단 위 간격` 이므로 그 등식도 받는다
  (21쪽 pi=140: 18340 + 15255 + 628 + 700 + 500 = 35423, 종전 다음 문단 −8.0px).

정본 대조 어긋난 쪽 26 → 24, 악화 0. nextest 11352 통과. `stored_bottom_caption_hu` 를 공용으로 연다.
저장 줄 스냅이 이미 같은 앞 간격을 되돌린 문단(한글문서파일형식 41쪽
문단 262, 51쪽 문단 328)에서 empty_host_float_successor_absorbed_spacing_px
를 다시 빼 6.7px 위로 그렸다. 소비량을 스냅 전 흐름 끝(상자 바닥)
기준으로 재어, 조판이 정한 문단 시작과 같게 한다.
- 빈 host 문단 기준 자리차지 표의 첫 조각 바닥 예약(바깥 아래 여백 +
  아래 캡션)을 empty_host_float_first_fragment_reserve_px 하나로 모아
  행 예산(budget)과 첫 행 이월 판정(prepare)이 함께 쓴다. 선언 개체
  높이가 첫 조각만 덮는 표(edwardkim#7147, 한/글이 거기서 나눈 저장 증거)는
  이월 판정에 쓰지 않는다. 한글문서파일형식 32쪽 표 199(선언 = 7행
  전체)는 제목 행만 남기지 않고 33쪽으로 통째로 넘어간다. 51쪽 표
  334(선언 = 제목 행)는 그대로 나뉜다.
- 통째 첫 조각 갈래가 흐름을 끝 조각과 같은 점유(아래 캡션·바깥 아래
  여백 포함)로 닫는다. 넘긴 표 199 뒤 제목이 캡션에 겹쳤다.
- 앞 쪽에서 포맷한 표를 이월해 새 단 첫머리에서 열면 host 앞 간격을
  빼고 바깥 위 여백만 쓴다(HostSpacing.column_top_before). 42쪽 표 270
  뒤 표 271 이 6.7px 내려갔다.

한/글 2020 합성(gen_headerrow.py 15사례: 제목 줄 반복 × 제목 칸 ×
채움)은 수정 전후 모두 일치하고, 제목 행만 남기는 분할 자체는 한/글도
허용함을 보였다(H03). 행 분할 합성 16사례 유지. 정본 대조 어긋난 쪽
22 → 19.
- 저장 원점으로 세우는 빈 host 표 상자(from_saved_whole_empty_host)의 꼬리에서
  글 없이 표 하나만 든 host 의 줄간격을 뺀다. 저장 사다리는 다음 빈 host 표의
  줄을 `표 + 캡션 + 바깥 아래 여백` 바닥에 둔다(한글문서파일형식 33쪽 표
  202→203: 다음 줄 vpos 45542 = 상자 바닥). 같은 문서의 저장 사다리 경로
  (52쪽 표 335~337)는 이미 이 상자를 썼다. 30쪽 표 185 캡션(5a2838e62)과
  같은 규칙이다.
- host 문단 앞 간격을 표를 실제로 여는 자리에서 정한다. 앞 쪽에서 포맷한
  표를 선언 높이 이월로 새 단 첫머리에 두면 바깥 위 여백만 쓴다(47쪽 표 297,
  6.7px). 캡션 RowBreak 단 배치·저장 사다리 없는 빈 앵커·재조판 host 상자가
  같은 값을 쓴다.

정본 대조 어긋난 쪽 19 → 12(33·44~49쪽 일치). 행 분할 합성 16사례 유지.
단 첫 항목이 글 없는 host 의 문단 기준 자리차지 표면 쪽의 저장 vpos 원점을
host 첫 줄 vpos 로 잡았다. 그 줄은 문단 앞 간격 뒤에 있는데 표 상자는 문단
위끝에서 바깥 위 여백만큼 내려 시작하므로, 그 쪽의 저장 앵커가 모두 앞 간격
만큼 위로 읽혔다. 한글문서파일형식 42쪽은 표 270 줄 500 을 원점으로 써 표 273
의 저장 첫 조각 프레임(선언 = 첫 조각 234.3px, edwardkim#7147)을 앵커 불일치로 버렸고,
한/글보다 한 행 적게 실어 43쪽부터 16px 밀렸다.

저장 사다리가 다음 줄을 `원점 + 위 여백 + 높이 + 아래 캡션 + 아래 여백` 에
두고(270→271: 0 + 566 + 14476 + 1802 + 1984 = 18828) host 줄이 표 위 바깥
여백 띠 안에서 시작하면 그 원점을 쓴다. 세로 오프셋이 있는 표(쪽을 넘기면
오프셋을 버림, edwardkim#4068)와 host 줄이 표 아래인 경우(issue7198)는 제외한다.
조판(stored_tac)과 배치(layout)가 같은 함수를 쓴다.

정본 대조 어긋난 쪽 12 → 10(42·43쪽 일치). nextest 11356 통과.
- 합성 3종(samples/issue8149/gen_rowfit·gen_reservefit·gen_headerrow, 한/글 2020 저장
  HWP 와 Print PDF): 사례마다 쪽별 행 소속을 PDF 본문 글자에서 읽은 기대값과 대조한다.
  한/글 저장본은 저장 첫 조각 프레임 경로로도 맞아 수정 전에도 통과한다 — 쪽 바닥
  예약(바깥 아래 여백 + 아래 캡션), 제목 행만 남기는 분할, 통째 이월 계약의 보호용이다.
- 사양서(한글문서파일형식 5.0, 정본 71쪽): 표 199 는 33쪽에만(선언 = 7행 전체),
  표 334 는 51·52쪽에 나뉜다(선언 = 제목 행). 수정 전은 74쪽으로 쪽수부터 다르다.

전쪽 실루엣(raw): 사양서 Native 최저 93.80 · WASM 94.14, 합성 4종 100%.
…im#8149)

devel 9418ed0 의 layout.rs 출력 폴백이 from_empty_stored_ladder_host 를 부른다.
이 PR 이 더한 origin_closes_float_frame 인자는 조판(entry.rs)이 앞 항목으로 정하므로
출력 폴백은 종전대로 true 를 넘긴다.
empty_host_float_successor_absorbed_spacing_px 가 저장 조판 프로필에서 다음 문단의 저장 첫 줄
vpos 를 본다. «상자 끝 + 앞 간격» 과 ±1px 로 맞고 «상자 끝 − 흡수 + 앞 간격» 과는 다르면
한/글 출력이 앞 간격을 소비하지 않은 것이므로 흡수하지 않는다. 조판(flow.rs)과 배치(layout.rs)
가 같은 커서(본문 위끝 기준)로 판정한다.

samples 에서 저장 줄과 맞출 수 있는 흡수 사례 118곳 중 117곳은 저장 줄이 흡수한 위치를
증언하고, hwpspec 54쪽 문단 341(이어지는 마지막 조각 뒤 테두리 문단)만 흡수하지 않은 위치를
증언한다. 그 쪽 Native 79.56 → 89.24(devel 과 같음).

회귀: successor_keeps_spacing_when_its_stored_line_says_so — 수정 전 문단 341 첫 줄이 저장
줄보다 13.3px 위라 실패, 수정 후 통과. 흡수가 맞는 반례 hwpspec 53쪽 문단 337·사양서 55쪽
문단 349 도 저장 줄에 있다.
저장 조판에서 다음 문단의 저장 첫 줄이 있으면 «커서 + 앞 간격 − 저장 위치» 를 소비량으로
쓴다(0..=앞 간격, ±1px 는 0·흡수량·앞 간격으로 맞춤). 증언 범위 밖의 vpos 면 종전 규칙
min(앞 간격, 바깥 아래 여백) 이다.

rowbreak-problem-pages 5쪽 pi=12: 저장 줄이 앞 간격 전부의 소비를 증언하는데 흡수량(1.9px)만
빼면 devel 의 stored_after_partial_table_shared_spacing_px 가 저장 줄과 맞추던 커서가 어긋나
문단이 11.5px 내려갔다(issue_7470 visible_successor_uses_the_original_line_origin… 실패).
이제 그 쪽도 저장 줄에 놓인다. hwpspec 54쪽 pi=341(소비 0)과 흡수 사례 117곳은 같다.
…#8149)

04b2cc3 의 바닥 예약(바깥 아래 여백 + 아래 캡션)은 한/글 2020 합성 gen_rowfit 16사례에서
얻었는데, 그 행은 모두 한 줄이라 통째로만 넘어간다. 첫 조각 행 예산에서 빼면 행 안에서
자르는 컷까지 쪽 끝을 못 쓴다 — 75544_pii_bunseok 65쪽 표 619(바깥 아래 여백 1.9px)는
한/글이 넷째 행을 쪽 끝까지 잘라 싣는데 rhwp 는 행 전체를 66쪽으로 넘겼다(issue_5846
cut_fragment_defers_partial_nested_tail_row 의 칸 (4,1) 소실).

첫 조각 예산(budget.rs)의 예약과 저장 프레임 인정 시 되돌림을 걷어내고, 첫 행이 통째로
들어가는지 보는 이월 판정(prepare.rs, 한글문서파일형식 32쪽 표 199)만 남긴다. 행 안 컷의
바닥 규칙은 한/글 오라클로 따로 확인한다.
7216d90 가 통째 첫 조각 갈래의 흐름 전진을 host_before_overhead + … 로 직접 합산했다.
그 합에는 단 맨 위 표의 바깥 위 여백이 들어가, paint 위치에만 쓰는 여백이 같은 쪽 다음
표의 흐름을 바꿨다(issue_4068 only_the_column_top_table_receives_outer_margin: 2912695 2쪽
첫 표 여백 0 이면 둘째 표가 1.88px 올라감). devel 의 partial_height + host_spacing_total 로
되돌린다. 이 갈래가 고치려던 한글문서파일형식 33쪽 표 199 뒤 제목은 Visual Sweep 으로 다시
확인하고, 필요하면 여백 계약을 지키는 방식으로 따로 다룬다.
planet6897 and others added 6 commits October 27, 2026 21:54
d293089 은 통째 첫 조각 흐름 전진을 devel 의 partial_height + host_spacing_total 로 되돌렸다.
그러면 한글문서파일형식 33쪽 표 199(뒤 몫 0)가 아래 캡션 24.0 + 바깥 아래 여백 26.5 를 흐름에
싣지 않아 제목 200 이 캡션과 겹친다(사양서 33쪽 Native 54.56, hwpspec 47쪽 44.12).
7216d90 의 직접 합산은 반대로 host 간격 합의 뒤 몫(host 줄간격 등)을 버리고 단 맨 위 표의
바깥 위 여백을 흐름에 넣어 issue_4068 계약을 깼다.

흐름 전진 = 표 높이 + host 간격 합 + max(0, 끝 점유 − 합의 뒤 몫). 끝 점유는 아래 캡션 +
바깥 아래 여백 + 뒤 간격이다. 표 199 는 175.9(직접 합산과 같음), 2912695 2쪽 표 3 은
779.6(devel 과 같음). 사양서 33쪽·hwpspec 47쪽 회복, hwpspec 90 미만 2쪽(devel 과 같은 쪽).
…m#8149)

2c53026 가 첫 조각 예산에서 뺀 바닥 예약(바깥 아래 여백 + 아래 캡션)을 되돌린다. 한/글 2024
합성(행마다 세 줄인 1열 12행 빈 host 자리차지 표, 바깥 아래 여백 0/141HU × 바깥 위 여백 75HU
간격 24단계로 쪽 바닥 남는 틈을 1px 씩 줄임)에서 141HU(1.9px) 여백도 행 안 컷의 마지막 줄을
넘긴다 — 줄을 넘기는 문턱이 여백 0 보다 약 2px 앞이다. 행 안 컷에도 예약이 맞다(한글문서파일형식
33→34쪽 표 203: 예약 없으면 한 행을 더 싣는다).

issue_5846(75544_pii_bunseok 65쪽 표 619)이 깨진 이유는 저장 되감김 첫 조각의 꼬리말 여유
(STORED_REWIND_FIRST_FRAGMENT_PAINT_FOOTER_GUARD_PX 4px)와 바닥 예약 1.88px 를 더해 뺀 것이다.
두 예약은 같은 쪽 바닥 띠를 남기므로 예약에서 여유만큼 뺀다. 한/글은 그 쪽에서 본문 끝 5.9px
위에서 첫 조각을 닫고 넷째 행(자식 첫 프레임 + 안 여백 29.87px)을 싣는다.

사양서 71쪽(정본 71) Native 최저 95.23 · fresh WASM 94.65, hwpspec 90 미만 2쪽(devel 과 같은
23·54쪽). issue_4068·5846·7470 과 edwardkim#8149 검사 통과.
devel 238de6d(edwardkim#8228)가 `from_stored_ladder` 를 저장본·재조판 공용 `from_ladder` 로
나눴다. 저장본 경로는 받은 `origin_closes_float_frame` 을 넘기고, 재조판 경로
(`from_reflowed_ladder`)는 앞 항목 상자 끝을 증언하는 호출자가 없으므로 `false` 다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 실루엣 TSV

head: 사양서·hwpspec·issue8149 합성 4종. devel: hwpspec(사양서는 devel 83쪽으로 쪽별 대조 불가).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@planet6897
planet6897 force-pushed the fix/8149-empty-host-float-outer-top branch from 567ff9d to ab8b419 Compare October 27, 2026 14:40
@planet6897

Copy link
Copy Markdown
Contributor Author

최신 devel 로 rebase

  • 새 head ab8b419cb (이전 567ff9d18), 기준 devel af6bb05b2
  • 충돌 해소: float_placement.rs: devel 의 reflow_block_cell 재수출을 유지하고 stored_empty_host_frame 을 pub(crate) mod 로 공개. 첫 lint 에서 컴파일 실패: 이 PR 의 3d7a1a7313 이 stored_table_column_origin 에 next 인자를 더했는데, devel 이 그 뒤 예전 형식의 새 호출부(typeset/state/stored_tac.rs)를 만들었다. 그 호출부도 다른 호출부처럼 다음 문단 next 를 넘기도록 3d7a1a7313 에 합쳤다.
  • 확인: --prepare → cargo fmt --all -- --check → native clippy → wasm32 clippy → workspace build → workspace all-targets clippy → manifest --check → unit-test-tiers --check 통과
  • 미실행(사용자 지시): 전체 integration nextest, 대표 쪽 Visual Sweep 재측정. 본문의 시험·시각 증적은 이전 head 기준이다.

같은 블록의 `first` 를 값으로 바인딩하는 변경(edwardkim#8205)과 함께 병합되면 `*first` 가
usize 역참조가 되어 컴파일되지 않는다. `&usize + usize` 도 usize 이므로 두 경우 모두
같은 값이다.
@planet6897

Copy link
Copy Markdown
Contributor Author

통합 검증(열린 PR 전체를 devel af6bb05b2 위에 순서대로 merge)에서 #8205 와 함께 병합하면 컴파일되지 않는 것을 찾아 6fe6d9fdb 를 더했습니다.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working layout 문서 레이아웃/페이지네이션: 표·그림 배치, float/anchor, 줄·행 높이, 겹침·클리핑 rendering SVG/Canvas/PDF 등 렌더 출력과 시각 표시: 그리기, 표시 누락, bbox, paint 정합성 table table rendering and edit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants