Repository navigation
[renderer/layout] 쪽을 넘긴 자리차지 표의 마지막 조각 뒤 host 글줄을 typeset 이 예약하지 않고 layout 만 그리며 문단 앞 간격을 다시 더한다 — 정본은 바깥 아래 여백만 #7677
Copy link
Copy link
Closed
Description
사용 경로
CLI (export-svg / dump-pages), 렌더 트리
rhwp 버전 또는 접속 URL
devel f0e7228f6 (stream/devel, 2026-10-08)
운영체제
Linux
문서 형식
HWP (HWP5) — 한/글 2020 이 저장한 공개 합성 입력
표시 문제와 기대 결과
문단(PARA) 기준 자리차지(TOP_AND_BOTTOM) RowBreak 표가 쪽을 넘어 이어질 때, 마지막 표 조각 뒤의 host 글줄에 문단 앞 간격이 다시 더해지고, 조판(typeset)은 이 글줄을 아예 예약하지 않는다.
- 기대(한/글 2020 Print PDF): host 첫 글줄은 마지막 표 조각 하단 + 표 바깥 아래 여백(852HU = 11.4px)에 선다. 앞 간격(1400HU = 18.7px)은 문단 상단에서 재는 거리라 앞 쪽에서 이미 소비됐다.
- 실제(devel): 표 조각 하단 + 앞 간격(18.4px)에 선다(바깥 아래 여백 대신 앞 간격).
| 6쪽 (96dpi px, 쪽 원점) | 한/글 정본 | devel |
|---|---|---|
| 마지막 표 조각 하단 | 362.5 | 361.4 |
| host 첫 글줄 「S. 넘어간 표 조각 뒤 host 의 첫 글줄」 상단 | 373.8 (+11.3) | 379.8 (+18.4) |
| 다음 문단 「S 뒤 문단이다.」 | 395.2 | 401.1 |
측정·배치도 같은 결과를 쓰지 않는다. dump-pages 6쪽 항목은 PartialTable rows=10..20 cont=true 와 다음 문단뿐이고(used 288.0px = 표 조각 266.7 + 다음 문단 21.3), host 글줄은 typeset 항목에 없다. layout 만 표 조각 경로에서 이 글줄을 그린다.
재현 순서·문제 페이지
- 아래 생성기로
python3 make_float_push_spacing_before_boundaries.py --continuation out.hwpx(저장소 루트, 뼈대는 공개 샘플samples/issue2527_empty_linesegs.hwpx). - 한/글 2020(hwp2024Convert MCP engine 2020)으로 HWP 저장(sha256
56f74734327cd664…)하고 같은 엔진으로 Print PDF 출력(sha2561839db526f608552…). 저장 LineSeg 는 한/글이 쓴 값이다. rhwp dump-pages out.hwp·rhwp export-render-tree out.hwp6쪽: 위 표의 좌표.
S 형상: 앞 간격 1400HU host 문단(채움 문단 30개 뒤)이 20행(행 2000HU) PARA 기준 자리차지 표(바깥 여백 위 140/아래 852HU)를 품고 표 뒤에 글 한 줄이 있다. 표가 5쪽 아래에서 시작해 6쪽으로 넘어간다.
자료
- 코드 경로(devel
f0e7228f6):src/renderer/layout.rs:14375defer_visible_rowbreak_host_text→:14399render_deferred_rowbreak_host_text_after→:14609para_start_y.insert(para_index, y_offset)후:14610layout_partial_paragraph(.., start_line, ..)— 표 조각 끝 y 를 문단 상단으로 삼아start_line == 0첫 줄에paragraph_layout.rs:4843의 앞 간격 분기가 전량 더한다(is_column_top아님).- typeset 쪽은
src/renderer/typeset/table/continuation/의 마지막 조각 방출 뒤 host 글줄 항목·높이를 만들지 않는다(위dump-pages).
- 의심 계층: 이어받은 표 조각 뒤 host 글줄의 소유자가 layout 단독(측정 미예약) + 앞 간격 원점을 이어받은 쪽에서 새로 만든다.
- 관계: [layout] 쪼갤 내용 없는 host 문단이 PartialParagraph 로 나가 저장 vpos 스냅을 잃는다 — 같은 문단의 표만 제자리 (#4599 하위, 백로그 30건) #7330 / PR fix(조판): 자리차지 표가 민 첫 글줄에 문단 앞 간격을 다시 더하지 않는다 (Refs #7330) #7617 작업 중 발견. PR fix(조판): 자리차지 표가 민 첫 글줄에 문단 앞 간격을 다시 더하지 않는다 (Refs #7330) #7617(자리차지 표가 민 첫 글줄의 앞 간격을 측정이 확정하고 배치가 소비)은 이 경로를 바꾸지 않는다(devel·PR head 같은 좌표). 지연 렌더 경로 자체는 지연 자리차지 표의 host 제목 줄이 마지막 fragment 뒤에 렌더 — 이월 전 쪽 잔류(한글 정합) 필요 #1755 에서 도입됐다.
- 생성기는 PR fix(조판): 자리차지 표가 민 첫 글줄에 문단 앞 간격을 다시 더하지 않는다 (Refs #7330) #7617 브랜치에 있다: samples/float_push_spacing_before/make_float_push_spacing_before_boundaries.py @ 85ea9a90b. 아래에도 그대로 붙인다.
공개 합성 입력 생성기 (make_float_push_spacing_before_boundaries.py, 저장소 루트에서 실행 — 뼈대는 공개 샘플 samples/issue2527_empty_linesegs.hwpx)
#!/usr/bin/env python3
"""공개 합성 입력 생성기 (#7330 float-sb 경계 재현용).
같은 문단의 문단(PARA) 기준 자리차지(TOP_AND_BOTTOM) 비-글자취급 표가 host 첫 글줄을 표 아래로
미는 형상의 **경계**를 쪽마다 하나씩 둔다. 기대 위치는 이 생성물을 한/글(hwp2024Convert MCP
engine 2020)로 HWP 저장한 저장 LineSeg 와 같은 엔진의 Print PDF 가 정한다.
- 1쪽 M: 앞 간격(4000HU)이 표 점유(한 행 1000HU, 바깥 여백 0)보다 큰 host — 글줄 원점이
`문단 상단 + 앞 간격` 쪽으로 정해지는 경계. 대조: 같은 앞 간격의 표 없는 문단, 앞 간격 없는 host.
- 2쪽 T: 같은 형상의 host 가 쪽 나눔 뒤 쪽 맨 위에 있다(단 맨 위). T 뒤를 채움 문단으로
채워, 측정이 첫 글줄 위치를 과소 계상하면 채움 문단의 쪽 소속이 달라지게 한다.
- 4쪽 L: 앞 간격(1400HU) host 의 표 뒤 글이 여러 줄이다 — 첫 글줄만 앞 간격 경계이고
둘째 줄부터는 문단 안의 줄이다.
`--continuation` 을 주면 이어받기 경계 두 쪽을 더 붙인다(회귀 시험 입력이 아니라 기존 결함의
공개 재현용이다).
- S: 앞 간격(1400HU) host 의 여러 행 표가 쪽 아래에서 시작해 다음 쪽으로 넘어간다 —
이어받은 표 조각 뒤의 host 글줄.
- C: L 과 같은 host 가 쪽 아래에 있어 표 뒤 글줄이 다음 쪽으로 이어진다(이어받은 줄).
뼈대(secPr·colPr·header·패키지 파일)는 공개 샘플 samples/issue2527_empty_linesegs.hwpx 에서
가져오고 rhwp 출처 표식(META-INF/rhwp-hwp5-origin)은 넣지 않는다. 생성물에는 linesegarray 가 없다.
사용: python3 samples/float_push_spacing_before/make_float_push_spacing_before_boundaries.py [--continuation] <출력.hwpx>
(저장소 루트에서)
"""
import re
import sys
import zipfile
src = zipfile.ZipFile('samples/issue2527_empty_linesegs.hwpx')
continuation = '--continuation' in sys.argv[1:]
out = [a for a in sys.argv[1:] if not a.startswith('--')][0]
hdr = src.read('Contents/header.xml').decode('utf8')
pp0 = re.search(r'<hh:paraPr id="0".*?</hh:paraPr>', hdr, re.S).group(0)
pp1 = pp0.replace('id="0"', 'id="1"').replace('<hc:prev value="0"', '<hc:prev value="1400"')
pp2 = pp0.replace('id="0"', 'id="2"').replace('<hc:prev value="0"', '<hc:prev value="4000"')
hdr = hdr.replace('<hh:paraProperties itemCnt="1">' + pp0,
'<hh:paraProperties itemCnt="3">' + pp0 + pp1 + pp2)
assert 'itemCnt="3">' in hdr
sec = src.read('Contents/section0.xml').decode('utf8')
head = sec[:sec.find('<hp:bookmark')] # 첫 문단의 secPr·colPr 까지
def para(pid, runs, pp=0, page_break=False):
return (f'<hp:p id="{pid}" paraPrIDRef="{pp}" styleIDRef="0" pageBreak="{1 if page_break else 0}" '
f'columnBreak="0" merged="0">{runs}</hp:p>\n')
def t(text):
return f'<hp:run charPrIDRef="0"><hp:t>{text}</hp:t></hp:run>'
def cell(c, r, w, h, text):
return (f'<hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="2">'
f'<hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" '
f'linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0">'
+ para(100000 + r * 10 + c, t(text)) +
f'</hp:subList><hp:cellAddr colAddr="{c}" rowAddr="{r}"/><hp:cellSpan colSpan="1" rowSpan="1"/>'
f'<hp:cellSz width="{w}" height="{h}"/><hp:cellMargin left="510" right="510" top="0" bottom="0"/></hp:tc>')
def table(tid, rows, row_h, margin):
w = 48000 // 3
top, bottom = margin
trs = ''.join('<hp:tr>' + ''.join(cell(c, r, w, row_h, rows[r][c]) for c in range(3)) + '</hp:tr>'
for r in range(len(rows)))
return (f'<hp:tbl id="{tid}" zOrder="{tid}" numberingType="TABLE" textWrap="TOP_AND_BOTTOM" textFlow="BOTH_SIDES" '
f'lock="0" dropcapstyle="None" pageBreak="CELL" repeatHeader="0" rowCnt="{len(rows)}" colCnt="3" '
f'cellSpacing="0" borderFillIDRef="2" noAdjust="0">'
f'<hp:sz width="{w * 3}" widthRelTo="ABSOLUTE" height="{row_h * len(rows)}" heightRelTo="ABSOLUTE" protect="0"/>'
'<hp:pos treatAsChar="0" affectLSpacing="0" flowWithText="1" allowOverlap="0" holdAnchorAndSO="0" '
'vertRelTo="PARA" horzRelTo="PARA" vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/>'
f'<hp:outMargin left="140" right="140" top="{top}" bottom="{bottom}"/>'
'<hp:inMargin left="510" right="510" top="0" bottom="0"/>'
+ trs + '</hp:tbl>')
def host(pid, tid, rows, row_h, margin, text, pp, page_break=False):
return para(pid, '<hp:run charPrIDRef="0">' + table(tid, rows, row_h, margin) + '</hp:run>' + t(text),
pp=pp, page_break=page_break)
small = [['가', '1,000', '-']]
body = [head + '</hp:run>' + t('문단 앞 간격·자리차지 표 경계 합성 입력 (#7330)') + '</hp:p>\n',
para(10, t('1. 표 앞 문단이다.')),
host(20, 301, small, 1000, (0, 0), 'M. 앞 간격이 표 점유보다 큰 host 의 첫 글줄', pp=2),
para(21, t('M 뒤 문단이다.')),
para(22, t('대조: 같은 앞 간격의 표 없는 문단'), pp=2),
para(23, t('대조 뒤 문단이다.')),
host(24, 302, small, 1000, (0, 0), '대조: 앞 간격 없는 host 의 첫 글줄', pp=0),
para(25, t('대조 host 뒤 문단이다.')),
host(30, 303, small, 1000, (0, 0), 'T. 쪽 맨 위 host 의 첫 글줄 — 앞 간격이 표 점유보다 크다', pp=2,
page_break=True),
para(31, t('T 뒤 문단이다.'))]
# 2쪽: T 뒤를 채움 문단으로 채운다 — 첫 글줄 위치를 측정이 과소 계상하면 이 쪽에 문단이 더 남는다.
for i in range(42):
body.append(para(200 + i, t(f'T 채움 {i + 1}')))
# L 쪽: 표 뒤 글이 여러 줄이다.
long_text = ('L. 표 뒤 글이 여러 줄인 host 문단이다. ' * 6).strip()
body.append(para(90, t('L 쪽 첫 문단이다.'), page_break=True))
body.append(host(91, 305, [['구분', '금액', '비고'], ['가', '1,000', '-']], 2000, (140, 852), long_text, pp=1))
body.append(para(92, t('L 뒤 문단이다.')))
body.append(para(93, t('끝.')))
if continuation:
# S 쪽: 쪽 아래에서 시작해 넘어가는 표 — 이어받은 표 조각 뒤의 host 글줄.
body.append(para(40, t('S 쪽 첫 문단이다.'), page_break=True))
for i in range(30):
body.append(para(41 + i, t(f'채움 문단 {i + 1}')))
rows = [[f'{r + 1}행', f'{(r + 1) * 1000:,}', '-'] for r in range(20)]
body.append(host(80, 304, rows, 2000, (140, 852), 'S. 넘어간 표 조각 뒤 host 의 첫 글줄', pp=1))
body.append(para(81, t('S 뒤 문단이다.')))
# C 쪽: 표 뒤 여러 줄 글이 쪽 끝을 넘어 다음 쪽으로 이어진다(이어받은 줄).
body.append(para(300, t('C 쪽 첫 문단이다.'), page_break=True))
for i in range(37):
body.append(para(301 + i, t(f'C 채움 {i + 1}')))
c_text = ('C. 쪽 끝을 넘는 host 문단이다. ' * 12).strip()
body.append(host(340, 306, [['구분', '금액', '비고'], ['가', '1,000', '-']], 2000, (140, 852), c_text, pp=1))
body.append(para(341, t('C 뒤 문단이다.')))
body.append(para(342, t('끝.')))
sec_new = ''.join(body) + '</hs:sec>'
def put(z, name, data, stored=False):
info = zipfile.ZipInfo(name, date_time=(2026, 10, 8, 0, 0, 0))
info.compress_type = zipfile.ZIP_STORED if stored else zipfile.ZIP_DEFLATED
z.writestr(info, data)
with zipfile.ZipFile(out, 'w') as z:
put(z, 'mimetype', src.read('mimetype'), stored=True)
for n in ('version.xml', 'Contents/content.hpf', 'META-INF/container.xml', 'META-INF/manifest.xml'):
put(z, n, src.read(n))
put(z, 'Contents/header.xml', hdr.encode('utf8'))
put(z, 'Contents/section0.xml', sec_new.encode('utf8'))Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
planet6897 commentedon Oct 17, 2026
devel
6ed1682c3재현 확인 — 착수한다저장소 생성기
make_float_push_spacing_before_boundaries.py --continuation으로 HWPX 를 만들고(저장 줄 없음), 로컬 한/글 2024(13.0.0.564)로 HWP 저장(저장 줄 있음) + 같은 한/글 Print PDF 를 만들었다. 본문의 2020 MCP 수치와 정본 좌표가 같다.6ed1682c3dump-pages6쪽 항목은 여전히PartialTable rows=10..20 cont=true다음 바로FullParagraph(다음 문단) 이라 host 줄은 layout 만 그린다. 저장 줄 없는 HWPX 경로도 같은 증상(+18.5)이다.수정 방향: typeset 이 마지막 조각을 커밋한 직후(흐름이 조각 하단 + 바깥 아래 여백에 선 시점)에 host 글줄을
PartialParagraph로 예약하고 앞 간격 소비량 0 을 기록, layout 은 그 항목이 있으면 지연 host 렌더(앞 간격 재가산)를 하지 않는다. 쪽에 안 들어가는 경우(C 형, #7678)는 범위 밖으로 둔다.planet6897 commentedon Oct 18, 2026
PR #8007 로 제출했다 (head
5413d4f7d, base6ed1682c3). 이 이슈를 닫는다.PartialParagraph로 예약하고, 흐름이 조각 하단보다 더 내려간 몫을 [layout] 쪼갤 내용 없는 host 문단이 PartialParagraph 로 나가 저장 vpos 스냅을 잃는다 — 같은 문단의 표만 제자리 (#4599 하위, 백로그 30건) #7330 의 확정 앞 간격 계약(기록 전용)으로 배치에 넘긴다. 배치는 그 항목이 있으면 지연 host 렌더(앞 간격 재가산)를 하지 않는다.dump-pages6쪽에PartialParagraph pi=87 lines=0..1이 생겼다. 저장 줄 없는 HWPX 경로도 같은 규칙(조각 +11.2).--continuation-s(S 쪽만) 플래그를 더해 만든 HWPX + 한/글 2024 저장 HWP + 같은 한/글 Print PDF(samples/issue7677,pdf/issue7677; 정본 좌표는 본문의 2020 MCP 수치와 동일). 전쪽 실루엣 6쪽 base 92.79 → Native/fresh WASM 99.88, 나머지 ≥99.3. 회귀 3건(수정 전 2 FAIL).--continuation의 C 쪽(표 뒤 여러 줄 글이 쪽 끝을 줄 단위로 못 넘김)은 [renderer/typeset] 자리차지 표 뒤 host 글이 쪽 끝에 걸려도 줄 단위로 나누지 않아 본문 하단을 넘는다 — 정본은 넘친 줄을 다음 쪽 맨 위로 이어받는다 #7678 의 증상이라 base 와 같게 남아 있다.수정(조판): 쪽을 넘긴 자리차지 표의 마지막 조각 뒤 host 글줄을 조판이 예약하고 배치는 바깥 아래 여백 자리에 둔다 (e…
planet6897 commentedon Oct 22, 2026
devel
605ccfd92재측정 — 이슈 규칙은 해소, 문서 잔여는 #8100 에서 추적하며 닫습니다fc32ca502(쪽을 넘긴 자리차지 표의 마지막 조각 뒤 host 글줄을 조판이 예약하고 배치는 바깥 아래 여백 자리에 둔다)issue_7677_rowbreak_host_line_after_last_fragment(regression_suite_006, release-test): 3/3 통과host_line_after_the_last_fragment_sits_at_outer_bottom_not_spacing_beforeokhwpx_without_stored_lines_follows_the_same_outer_bottom_ruleokhost_text_is_not_drawn_twice_across_the_splitok남은 범위: 원본 입력은 #7445 이관 자산(
deferred-20261020)으로 옮겨졌고, 연속 표 7677·7678 의 전체 피델리티 잔여는 #8100 에서 추적합니다.