변경 로그
모든 중요한 변경 사항은 이 문서에 기록됩니다. 형식은 Keep a Changelog과 Semantic Versioning을 따릅니다.
[Unreleased]
추가
표 구조 편집(
hwpx.table_patch.apply_table_ops)에 열 삽입insert_column_by_clone을 더한다. 격자 열ref_col의 오른쪽(side: "left"면 왼쪽)에 그 열을 복제한 열count개를 넣는다.새 열은 기준 열과 같은 폭이고, 표는 그만큼 넓어진다. 본문 폭을 넘어도 줄이지 않는다.
새 칸은 같은 행 기준 칸의 테두리·문단 모양·글자 모양을 따른다. 글은 행 삽입(
insert_row_by_clone)처럼 복제하고,blank: true면 한/글처럼 빈 문단 하나만 둔다.개체 ID·필드·책갈피·로컬 참조가 있는 내용을 그대로 복제하면 거부한다. ID를 중복시키지 않으며, 빈 칸 삽입은 사용할 수 있다. 기존 행 복제도 같은 거부 계약을 따른다.
새 열을 가로지르는 합친 칸은 그 열만큼 colSpan과 폭이 는다. 기준 열에서 끝나는(왼쪽이면 시작하는) 합친 칸 옆에는 그 칸 서식의 빈 칸이 들어가고, 기준 열에서 세로로 합친 칸은 같은 행들에 걸쳐 복제된다.
칸 영역(
hp:cellzone)의 열 주소도 함께 옮긴다.
행 삽입
insert_row_by_clone이side: "above"(기준 행 위)와blank: true(한/글처럼 빈 문단 하나)를 받는다.새 행은 기준 행과 같은 높이이고, 새 칸은 같은 열 기준 칸(열을 합친 칸 포함)의 서식을 따른다.
위쪽 삽입에서 새 행을 가로지르는 합친 칸은 rowSpan이 는다. 기준 행에서 시작하는 세로 합친 칸 위에는 그 칸 서식의 빈 칸이 들어간다(아래쪽 삽입에서 기준 행에서 끝나는 칸과 같은 규칙).
칸 나누기
split_cell(한/글의 셀 나누기)을 더한다. 칸의 주소(row,col)와rows·cols(2~63)를 받는다.세로만 나누는 경우에도 정확한 앵커 주소가 필요하다. 합친 칸의 피복 주소·범위 밖 주소는 문서를 바꾸지 않고 거부한다.
열로: 칸 폭을 똑같이 나누고(나머지는 마지막 칸), 새 격자 선을 넣는다. 다른 칸은 폭은 그대로 두고, 덮는 열만큼 colSpan이 는다.
줄로: 칸이 덮는 행이 똑같이 나뉘면 그 행들에 나누고, 아니면 한 행씩 나누고 모자라는 행을 칸의 마지막 행 뒤에 더한다. 그 행 옆 칸은 새 행까지 덮는다. 칸의 저장 높이는 똑같이 나눈다.
글은 첫 칸에 남고, 나머지 칸에는 그 칸 서식의 빈 문단 하나를 둔다. 칸 영역은 덮던 자리를 그대로 덮는다.
표 객체(
hwpx.model.Table)에 행·열 구조 편집을 더한다:insert_rows(위·아래),insert_columns(왼쪽·오른쪽),delete_rows,delete_columns,split_cell.doc.tables에도 같은 이름이 있다(첫 인자로 표).apply_table_ops의 같은 편집과 같은 규칙(한/글의 줄/칸 추가하기·지우기, 셀 나누기)을 따른다. 새 칸은 기준 칸의 글을 복제하고,blank=True면 그 칸 서식의 빈 문단 하나만 둔다.편집하면 표를 담은 문단의 줄 캐시를 지운다. 편집 전에 얻은 셀·행 객체는 표에 속하지 않으니 다시 얻는다.
고침
문단 모양 조회와 글꼴·테두리 조회가 큰 머리말에서 느리던 것을 고친다. FormFit과 쪽 수 추정(실험)이 문단·글자 모양마다 이 조회를 불러, 문단 모양이 많은 문서에서는 거의 모든 시간을 여기에 썼다.
문서의
paragraph_property()가 부를 때마다 모든 머리말의 문단 모양을 모두 읽었다. 이제 머리말마다 찾는 모양만 읽는다(뒤 머리말이 이기는 것은 그대로).font_face()·border_fill_info()가 부를 때마다 머리말 전체를 훑었다. 이제hh:refList의 글꼴·글자 모양· 테두리 목록만 보고, 그런 목록이 없는 머리말만 전체를 훑는다.
표 구조 편집(
apply_table_ops)이hp:cellAddr·hp:cellSz가 없는 칸(다른 프로그램이 쓴 표)에서AssertionError로 멈추거나 엉뚱한 이유로 거부하던 것을 고친다. 행·열 삭제와 삽입, 열 폭 설정과 맞춤, 표 나누기, 행 순서 바꾸기가 이제 그런 표를 표 안의 표처럼 거부한다.표의 열을 모두 지우는
delete_column(1열 표의 열 포함)이ZeroDivisionError로 멈추던 것을 고친다. 행을 모두 지우는delete_row도 남는 격자 탓으로만 거부했다. 한/글도 마지막 행·열은 지우지 않는다. 이제 둘 다 거부하고delete_table을 쓰라고 알린다.행 삽입(
insert_row_by_clone)과 행·열 삭제(delete_row,delete_column)가 칸 영역(hp:cellzone)을 옮기지 않던 것을 고친다. 영역은 그려지므로, 남은 영역이 다른 칸에 테두리를 그렸다. 이제 영역이 칸과 함께 움직인다.삽입: 새 행 뒤의 영역은 밀고, 새 행을 가로지르는 영역은 넓힌다.
삭제: 영역은 남는 줄만큼 줄고, 줄이 모두 지워진 영역은 없어진다(마지막 영역이면 목록째).
글머리표·번호 목록(
doc.styles.apply_list_format)이 10보다 깊은 수준을 받던 것을 고친다.한/글 목록은 1~10수준이다. 11수준 이상을 쓴 문서는 한/글이 다시 저장하지 못하고 멈춘다.
이제 수준이 1 이상 10 이하의 int가 아니면 무엇도 바꾸기 전에
style-list-level-invalid오류로 거부한다(전에는 1 미만만 거부했다).
[6.8.0] - 2026-10-06
import hwpx가 HWP 5.0 모듈과 hwpx.tools를 처음 쓸 때 읽도록 바꿔 가벼워진 릴리스입니다. FormFit은 글꼴
38종을 글꼴 파일의 설계 폭으로 재고, 수식 상자 폭은 한/글 저장값과의 평균 오차가 10.8%에서 3.6%로 줄었습니다.
그래서 FormFit이 고르는 글자 크기와 줄 수, add_equation이 쓰는 수식 상자 크기가 6.7.0과 달라질 수 있습니다.
save_report는 .hwp 경로를 HWP 5.0으로 씁니다. 쪽 수 추정(실험)은 개체·표·칸의 배치를 한/글 저장본에 맞춰
넓게 고쳤습니다. 코어는 6.7.0보다 약 6천 줄(7.7%) 늘었는데, 대부분 FormFit의 글꼴 폭 표(약 4,300줄)와 쪽 수
추정(약 1,300줄)입니다. import hwpx가 읽는 hwpx 모듈은 117개에서 97개로 줄었습니다. 저장 성공만으로 시각 품질을
보증하지 않습니다.
바꿈
import hwpx가 HWP 5.0 모듈(hwpx.hwp5)을 읽지 않는다..hwp를 열거나 HWP 5.0으로 저장할 때 처음 읽는다.Hwp5Error·Hwp5ConversionWarning은 이제hwpx.errors에 있고,hwpx와hwpx.hwp5.errors는 같은 클래스를 내보낸다.import hwpx가hwpx.tools(텍스트 추출·문서 비교·메일 머지·패키지 검증·내보내기 등)를 읽지 않는다.hwpx.TextExtractor·hwpx.doc_diff·hwpx.validate_package처럼hwpx.tools에 사는 이름은 처음 쓸 때 경고 없이 읽고, 같은 객체다. 위hwpx.hwp5와 합쳐import hwpx가 읽는 hwpx 모듈은 117개에서 97개로 준다. 저장은 여전히 처음 저장할 때 패키지 검증 도구를 읽는다.FormFit이 글꼴 12종(바탕·바탕체·돋움·돋움체·굴림·굴림체·궁서·궁서체·한컴 고딕·함초롬바탕·함초롬돋움·맑은 고딕)에서 KS X 1001 특수 기호와 전각 문자, 라틴-1·라틴 확장-A·일반 문장 부호·통화 기호·수학 연산자·도형·기타 기호·딩뱃· 반각/전각 형태 블록의 글자를 글꼴의 설계 폭으로 잰다. 전에는 흔한 문장 부호와 기호 몇십 자 말고는 글자 종류의 평균 폭을 썼다. 글꼴에 없는 글자는 한/글이 대신 그리는 글꼴(바탕·궁서와 -체 글꼴은 함초롬바탕, 굴림·돋움·맑은 고딕·한컴 고딕은 함초롬돋움, 함초롬 글꼴은 Segoe UI Symbol)의 폭을 그 글꼴 단위로 옮겨 쓴다. 이런 글자가 든 칸은 채우기 결과(글자 크기, 줄 수)가 달라질 수 있다.
기호 글꼴 Wingdings·Symbol도 문서가 글자를 두는 사용자 영역(U+F020–F0FF)의 설계 폭으로 잰다. 실제 너비를 쓰는 Wingdings 글머리표(U+F09F)는 10 pt에서 456을 차지한다.
HY헤드라인M도 글꼴의 설계 폭으로 잰다. 글꼴에 없는 글자와
`는 함초롬바탕의 폭을 쓴다.휴먼명조도 글꼴의 설계 폭으로 잰다. 글꼴에 없는 글자는 함초롬바탕의 폭을 쓴다.
FormFit이 한/글이나 Windows와 함께 오는 글꼴 26종도 글꼴의 설계 폭으로 잰다. 전에는 글자 종류의 평균 폭을 썼다. 이 글꼴로 쓴 칸은 채우기 결과(글자 크기, 줄 수)가 달라질 수 있다.
글꼴: 한컴산뜻돋움, 휴먼고딕, 한컴돋움, HY울릉도M·B, HY중고딕, HY견고딕, HY신명조, HY태백B, HY그래픽, 한컴바탕, 한컴 윤고딕 230·240·720·760, 한컴 윤체 L, 한컴 소망 M·B, 한컴 백제 B·M, 한컴 바겐세일 B·M, 새굴림, 한컴 말랑말랑 Regular·Bold, MD개성체
글꼴에 없는 글자는 한컴산뜻돋움·새굴림이 함초롬돋움의 폭을, HY중고딕·HY견고딕·HY신명조·한컴 말랑말랑 Regular가 함초롬바탕의 폭을 그 글꼴 단위로 옮겨 쓴다. 나머지 글꼴에 없는 글자는 전처럼 평균 폭이다.
한글 음절마다 폭이 다른 글꼴은 음절마다 그 폭을 쓴다(
_glyph_table.HANGUL_ADVANCES). 예를 들어 한컴 말랑말랑은 가로 모음(ㅗ·ㅛ·ㅜ·ㅠ·ㅡ)이 든 음절이 820/1000이고, 나머지는 880이다.
고침
쪽 수 추정(실험,
estimate_pages)이 글자처럼 둔 도형의 그림자를 줄 높이에 넣지 않던 것을 고친다.한/글은 그런 도형의 줄을 도형과 평행 그림자의 세로 이동만큼 높게 놓는다. 이동은 아래쪽 그림자면
offsetY + 600, 위쪽 그림자면offsetY - 600이다(아래가 양수). 그림자가 위로 가면 도형이 그만큼 아래에 그려진다. 선 굵기는 줄 높이에 더하지 않는다.전에는 도형 크기만 세어, 그런 도형 뒤 줄들이 그만큼 위에 놓였다.
쪽 수 추정(실험,
estimate_pages)이 글꼴에서 줄 높이를 정하는 문단 모양(fontLineHeight)을 무시하던 것을 고친다.한/글은 그런 문단의 줄을 글자 크기가 아니라 글꼴의 오름과 내림(OS/2 usWinAscent·usWinDescent)만큼 높게 놓고, 줄 간격도 그 높이에서 센다. 함초롬 글꼴과 한컴산뜻돋움은 1.3배, 맑은 고딕은 약 1.33배다. 바탕·굴림·HY 글꼴처럼 오름과 내림을 더하면 em인 글꼴은 크기 그대로다.
이제 줄 캐시가 없는 문단은 FormFit 글꼴 표에 더한 오름·내림으로 줄 높이를 잡는다. 오름과 내림을 각각 올림해 더하며, 한/글은 크기에 따라 1~2 더 높게 놓기도 한다. 표에 없는 글꼴은 전처럼 글자 크기다.
FormFit도 칸의 첫 문단이 그런 모양이면 칸에 들어가는 줄 수를 그 줄 높이로 센다(
SlotMetrics.font_line,line_size).
hwpx.tools도 공개 이름과 하위 모듈을 처음 쓸 때 읽는다. 첫 저장은 패키지·문서 검증기만 읽고, 내보내기·뷰어·문서 비교·메일 머지 등은 읽지 않는다. 기존 import·객체·타입 정보와doc_diff함수 이름을 유지한다. HWPX Markdown 변환기의 배포판 버전 조회도 변환 시점으로 미뤄,import hwpx가importlib.metadata를 읽지 않는다(#386).수식 상자 측정(
measure_equation,estimate_equation_size)의 폭을 한/글이 저장한 상자에 더 가깝게 고친다. 합성 수식 말뭉치(132식 × 기준 크기 1000·1100·1300, macOS 한/글 저장값)에 대한 폭의 평균 절대 오차가 10.8%에서 3.6%로 준다(1000: 8.4→2.8%, 1100: 15.0→6.1%, 1300: 8.9→1.9%). 높이(2.5%)와 기준선(평균 0.9)은 그대로다.글자 폭을 글리프별로 잰다. 숫자·기울인 글자는 더 좁고(
x0.52em, 숫자 0.47em),i·j·l·t는 좁고m·w는 넓다. 연산자·관계 기호는 기호마다 폭이 있고 양옆에 같은 간격을 둔다. 앞에 받을 것이 없는+·-는 부호라 뒤에만 간격을 둔다. 상자 끝의 여백은 없앤다.rm뒤의 글자는it이나 묶음 끝까지 바로 선 글자 폭으로, 함수 이름(sin·log등)도 바로 선 글자 폭의 합으로 잰다. 따옴표 안 글자는 그때의 글꼴 모양을 따른다.첨자의 폭은 본 크기의 0.55배가 아니라 0.66배로 잰다(높이는 그대로).
LEFT/RIGHT와 행렬이 늘이는 괄호는 괄호 모양마다((·[·|·{) 폭이 다르다.choose·binom은 한/글이pmatrix {n # r}과 같은 상자로 저장하므로, 늘인 괄호 안의 두 줄로 잰다.atop은 전처럼 가로줄 없는 분수다.상자는 여전히
baseUnit에 비례해 커진다. 한/글(macOS)은 글자 폭을 크기에 꼭 비례해 두지 않아(1100에서 글자가 1000과 같은 폭) 1100의 오차가 가장 크다. Windows 한/글에서의 비례는 아직 확인하지 않았다.
도형·그림·수식을 넣을 때 음수 크기와 사각형의 음수 모서리 곡률을 그대로 쓰던 것을 고친다.
한/글은 음수 크기를 0으로 읽고 0으로 저장한다. 그 크기에서 쓴 원래 크기(
orgSz)·회전 중심·그림의 이미지 크기(imgDim)도 0이 된다. 음수 곡률(ratio)도 0으로 읽는다.사각형·타원·호(
doc.shapes.add_rectangle·add_ellipse·add_arc), 그림(doc.add_picture), 수식 상자 (doc.shapes.add_equation의size), 도형 크기 바꾸기(HwpxOxmlShape.resize)는 이제 0 이상 2**31 미만의 크기만 받는다. 그 밖의 값은 문단·런·그림을 더하기 전에shape-size-value오류로 거부한다. 곡률은shape-rect-ratio-value로 거부한다.수식의
base_unit이 0 이하일 때 내던ValueError를shape-equation-base-unit-value(HwpxValueError)로 바꾸고 문단을 더하기 전에 낸다. 231 이상도 거부한다. 아주 큰base_unit으로 잰 상자가 231 이상이면shape-size-value로 거부한다. 한/글은 그런 상자를 2**32 - 1로 줄여 저장한다.HwpxOxmlParagraph.add_picture는 그림 요소를 만든 뒤에 런을 더한다. 값이 거부되면 빈 런이 남지 않는다.그림의 HWPUNIT 크기는 정수 변환 전에 검사한다. bool·소수·문자열을 정수로 바꾸어 받아들이지 않는다.
캡션 간격, 같은 폭 단의 간격, 새 번호를 한/글이 그대로 두지 않는 값도 그대로 쓰던 것을 고친다.
한/글은 캡션 간격(
hp:caption@gap)과 단 간격(hp:colPr@sameGap)을 부호 있는 16비트 수로, 새 번호 (hp:newNum@num)를 부호 없는 16비트 수로 읽는다. 범위 밖 값은 감싸 읽는다. 캡션 간격 32768은 −32768, 새 번호 −1은 65535, 65536은 0이 된다. 단 간격 32768은 −32768로 읽어 둘째 단이 첫째 단과 겹친다. 음수 글로 쓴 단 간격은 0으로 읽는다.이제 캡션 간격(
set_caption의gap)은 −32768 이상 32767 이하, 단 간격(doc.page.set_columns의same_gap,doc.page.setup의column_gap_mm)은 0 이상 32767 이하, 새 번호(doc.page.restart_page_number의number)는 0 이상 65535 이하만 받는다. 그 밖의 값은 무엇도 바꾸기 전에shape-caption-gap-value,page-column-gap-value,page-new-num-value오류로 거부한다.doc.page.setup은 쪽 크기와 여백을 바꾸기 전에 단 간격을 검사한다.
너비가 다른 단(
doc.page.set_columns(same_size=False, column_widths=...))의 너비와 간격을 한/글이 읽는 꼴로 쓴다. 한/글은hp:colSz의 너비와 간격을 HWPUNIT이 아니라 본문 폭을 32768로 나눈 몫으로 읽고, 단을 너비 × 본문 폭 / 32768로 놓는다. 전에는 받은 값을 그대로 써서, 본문 폭에 맞춰 HWPUNIT으로 준 단(A4에서[(20000, 1000), (21520, 0)])이 1.3배 넓게 놓여 둘째 단이 종이 밖으로 나갔다.이제
column_widths를 비율로 받아 합이 32768이 되게 맞춰 쓴다. 본문 폭에 맞춘 HWPUNIT 값은 그 너비 그대로 나오고, 합이 32768인 값은 그대로 쓴다.음수, 정수가 아닌 값, 모두 0인 값, 두 값이 아닌 쌍은 무엇도 바꾸기 전에
page-column-widths-value오류로 거부한다.쪽 수 추정(실험,
estimate_pages)도 단 너비를 너비 × 본문 폭 / 32768로 센다. 전에는 너비들의 합으로 나눴다. 한/글이 만든 문서는 합이 32768이라 결과가 같다.
쪽·구역·목록 설정에서 한/글이 그대로 두지 않는 값과, 음수를 조용히 0으로 쓰던 값을 거부한다.
한/글은 줄 번호의 시작·간격(
doc.page.set_line_numbers의start_number·count_by), 구역 시작 번호 (set_start_numbering), 목록 시작 번호(doc.styles.apply_list_format의start)를 부호 없는 16비트 수로, 쪽 테두리 오프셋을 부호 있는 16비트 수로 둔다. 그 밖의 값은 감싸 읽는다(예: 65536은 0). 모르는 제본 방식 (gutter_type)과 쪽 테두리 채울 영역(fill_area)은 기본값(LEFT_ONLY,PAPER)으로 읽는다.이제 번호는 0 이상 65535 이하, 줄 번호 거리와 재시작은 0 이상 231 미만, 쪽 테두리 오프셋은 0 이상 32767 이하, 쪽 크기와 여백은 0 이상 231 미만만 받는다. 쪽 테두리 종류(
page_type)는BOTH·EVEN·ODD, 채울 영역은PAPER·PAGE·BORDER, 제본 방식은LEFT_ONLY·LEFT_RIGHT·TOP_BOTTOM만 받는다. 음수 글자 크기(ensure_run의size)도 거부한다.그 밖의 값은 무엇도 바꾸기 전에 형식 있는 오류(
page-number-value,page-border-fill-invalid,page-gutter-type-invalid,page-size-value,style-list-start-value,style-run-size-value)로 거부한다. 전에는 음수를 0으로 쓰고, 범위 밖 값은 그대로 썼다.doc.page.setup은 쪽을 바꾸기 전에 크기와 여백을 검사한다.목록 시작 번호 0은 그대로 쓴다(한/글이 그대로 두고 1처럼 그린다). 전에는 1로 썼다.
구역에 없던 설정(쪽 테두리, 시작 번호, 줄 번호, 격자, 보이기 등)을 더할 때
TypeError가 나던 것을 고친다. 예를 들어 새 종류의 쪽 테두리를 만들면 그랬다.
새 표(
doc.add_table, 칸 안의add_table)와 열 너비 바꾸기(HwpxOxmlTable.set_column_widths)가, 한/글이 그리는 것보다 좁은 열을 그대로 쓰던 것을 고친다.한/글은 표의 열을 적어도 그 칸들의 왼쪽·오른쪽 여백과 283을 더한 폭(새 표는 510 + 510 + 283 = 1303)으로 그린다. 칸에 적힌 좁은 폭은 그대로 두고, 표는 그만큼 넓게 저장한다. 예를 들어 폭 0으로 쓴 2×2 표는 2606이 되고, 42520 폭 표에 열 비율 0과 1을 주면 43823이 된다.
이제 새 표는 적어도 열마다 그 폭으로,
set_column_widths는 그보다 좁은 열을 그 폭으로 쓰고 표 폭을 그만큼 늘린다. 그래서 쓴 표가 한/글이 그리고 저장하는 표와 같다. 폭을 주지 않은 좁은 칸 안의 표도 그렇다.새 표의 폭이 0 초과 2**31 미만의 int가 아니면, 문단이나 테두리 채우기를 더하기 전에
table-width-value오류로 거부한다. 전에는 0이나 음수 폭을 0으로 썼다.
문단에서 새 단을 시작하는 단 정의(
doc.page.set_columns(..., paragraph=...),add_column_definition)를 문단의 글 뒤 런에 쓰던 것을 고친다. 한/글은 문단의 글보다 앞에 있는 단 정의로만 새 단을 시작하고(한/글 문서는 그것을 문단의 첫 런에, 구역 설정 다음에 둔다), 글 뒤에 있으면 새 영역만 열고 단은 적용하지 않아 뒤 글이 본문 폭으로 놓였다. 이제 단 정의의 런을 문단의 글보다 앞에 쓴다.구역의 첫 문단에서는 구역 설정 옆 단 정의를 그 자리에서 고쳐 쓴다(문단을 주지 않은
set_columns와 같다). 첫 문단에 단 정의가 하나 더 있으면 한/글이 문서를 열지 못한다.
쪽 수 추정(실험,
estimate_pages)이 구역 안에서 단이 바뀌는 문서를 지원 밖(the columns change inside the section)으로 두던 것을 고친다. 이제 한/글처럼 단 영역별로 놓는다.구역 첫 문단 뒤에서 단 정의를 가진 문단은 새 단 영역을 시작한다.
영역은 앞 영역이 끝난 쪽에서, 앞 영역의 가장 낮은 줄 아래끝보다 1134(4mm) 아래에 시작한다. 줄 간격이나 문단 간격과 상관없다. 자리가 없으면 다음 쪽 위에서 시작한다.
영역의 줄 위치는 그 쪽에서 영역 위쪽부터 센다(한/글의 줄 캐시와 같다).
다음 영역이 오는 영역은 마지막 쪽의 단을 높이로 균형 맞춘다. 모든 줄이 그 쪽에 남는 가장 짧은 단이다. 균형 다단(
BALANCED_NEWSPAPER)의 단 나누기도 새 균형 영역을 연다.문단의 글 뒤에 있는 단 정의는 영역만 새로 열고 단은 바꾸지 않는다.
단이 바뀌는 구역의 각주와 쪽·종이 기준 개체는 여전히 지원 밖이다.
쪽 수 추정(실험,
estimate_pages)이 쪽 번호 조판 부호(hp:ctrl/hp:pageNum)와 줄보다 넓은 글자처럼 둔 표만 있는 문단을 표 높이의 한 줄로 세던 것을 고친다.한/글은 둘을 한 줄에 두지 않는다. 부호의 줄은 문단 글의 빈 줄이다.
부호가 앞이면 빈 줄 다음에 표의 줄이 오고, 표 뒤면 표의 줄 다음에 빈 줄이 온다.
표가 줄에 들어가거나, 앞에 구역·단 정의 부호만 있으면 전처럼 한 줄이다.
쪽 수 추정(실험,
estimate_pages)이 줄보다 넓은 글자처럼 둔 표 곁의 다른 조판 부호(감출 쪽 번호, 새 번호, 자동 번호, 머리말·꼬리말, 쪽 번호 위치, 책갈피, 누름틀, 찾아보기 표시 등)를 표의 줄에 함께 세던 것을 고친다.한/글은 단 정의(
hp:colPr)를 뺀 모든 조판 부호를 쪽 번호처럼 표와 다른 줄에 둔다. 표 앞의 부호는 표 위의 빈 줄, 표 뒤의 부호는 표 아래의 빈 줄이다.그때 표의 줄 간격은 표가 든 런의 글자 크기로, 부호 줄은 그 부호들이 든 런의 글자 크기로 센다. 전에는 문단 모든 런 가운데 가장 큰 글자로 셌다.
쪽 수 추정(실험,
estimate_pages)이 문단 글 뒤에 놓인 어울림(SQUARE) 개체를 지원 밖 (... wrapped square after text)으로 두던 것을, 그 개체의 조판 부호가 문단 첫 줄에 있을 때 고친다.한/글은 개체의 문단 기준 세로 자리를 부호가 놓인 줄(문단을 단 전체 폭으로 놓았을 때)의 윗선에서 잰다. 부호가 첫 줄에 있으면 문단 윗선이라, 글 앞에 놓인 개체와 같다. 띠에 닿는 줄은 그 문단의 줄도 곁으로 간다.
첫 줄은 FormFit으로 문단을 단 전체 폭에서 나누어 가린다. 부호가 뒤 줄에 있으면 전처럼 지원 밖이다.
쪽 수 추정(실험,
estimate_pages)이 문단 맨 위(세로 위치 0)에 둔 위아래 배치 개체가 본문 바닥을 넘을 때 문단째 다음 쪽으로 넘기거나(빈 문단) 지원 밖(... anchored in text at a page end)으로 두던 것을 고친다.한/글은 글과 함께 움직이는 개체(
flowWithText)면 개체만 다음 쪽 본문 맨 위로 보낸다. 문단의 줄은 빈 줄이든 글 줄이든 제자리에 전체 폭으로 남고, 다음 쪽 내용은 개체 아래에서 시작한다. 문단 위치에서 아래로 내려 둔 개체(세로 위치 > 0)에 쓰던 규칙과 같다.글과 함께 움직이지 않는 개체는 그 쪽에 남아 바닥을 넘는다. 이때 들어가야 하는 것은 문단의 줄뿐이고, 다음 문단은 개체 아래(대개 다음 쪽 맨 위)에서 시작한다.
개체만 넘기는 것은 문단 자기 줄이 제자리에 들어갈 때뿐이다. 줄도 안 들어가면 문단째 넘어간다(문단 위치에서 내려 둔 개체도 같다). 개체가 혼자 넘어간 쪽을 따르지 못하는 구역(그 띠를 흐르는 표가 지나가는 등)은 예전처럼 개체를 문단과 함께 넘겨 다시 놓는다.
개체가 넘어간 쪽에서, 통째로 넘어간 위아래 배치 표는 개체 아래에서 시작하고 그 문단 줄은 앞 쪽에 남는다. 앞 쪽에서 나뉘어 이어지는 표는 그 쪽 맨 위에서 개체와 겹쳐 이어지고, 뒤 글은 둘 아래에서 시작한다. 그 쪽으로 넘어간 표 문단은 개체 아래에서 쪽 맨 위처럼 놓인다.
쪽 수 추정(실험,
estimate_pages)이 줄을 나누지 않는 표 칸(hp:subList@lineWrap이 SQUEEZE나 KEEP)의 글을 칸 폭에서 나누던 것을 고친다.한/글은 그런 칸의 문단을 한 줄로 두고, 그릴 때 칸 폭에 눌러 넣는다. 줄 바꿈과 문단 나눔만 새 줄을 연다.
이제 캐시 없는 그런 칸의 줄을 줄 바꿈마다 한 줄로 센다.
쪽 수 추정(실험,
estimate_pages)이 표 칸 문단에 홀로 둔 위아래 배치 표(중첩 표)를 바깥 칸의 한 줄로 세어, 바깥 행이 쪽 끝에 걸리면 그 행을 쪽 바닥에서 그대로 자르던 것을 고친다.한/글은 그런 표를 행 사이에서 나눈다(쪽 나눔 CELL·TABLE). CELL이면 여러 줄인 행도 줄 사이에서 나누고, 나뉜 행의 각 부분에 칸 여백을 둔다. 머리행 반복이면 다음 쪽 부분 위에 머리행을 다시 둔다.
이제 그 표를 행(또는 줄) 조각으로 펼쳐 바깥 칸의 줄처럼 나눈다. 글자처럼 둔 중첩 표는 전처럼 나뉘지 않고 통째로 넘어간다. 나누지 않는 표(NONE), 아래로 내려 둔 표, 캡션이 있거나 행이 병합된 표는 전처럼 한 덩어리다.
쪽 수 추정(실험,
estimate_pages)이 글과 함께 움직이고 나뉠 수 있는 어울림(SQUARE) 표가 본문 바닥을 넘을 때, 줄 캐시가 없으면 지원 밖으로 두던 것을 고친다.한/글은 그런 표를 흐르는 표처럼 쪽 끝에서 행 사이나 칸 줄 사이로 나눈다. 표가 걸친 쪽마다 곁 줄은 그 쪽 부분 곁으로 간다. 첫 쪽은 표 위부터 바닥까지, 표가 쪽을 다 덮으면 그 쪽 모든 줄, 마지막 쪽은 맨 위부터 표 끝까지다.
이제 그 표를 흐르는 표처럼 놓아 쪽마다 띠를 두고, 띠에 닿는 줄을 다시 나눈다.
한 쪽에 띠가 둘이면(앞 표의 끝과 다음 표의 시작) 둘 다 둔다. 한 문단이 두 띠에 걸치면 지원 밖이다.
쪽 수 추정(실험,
estimate_pages)이 문단 글 앞에서 조판 부호만 든 런(구역·단 정의, 쪽 번호, 자동 번호, 꼬리말 등)의 글자 크기를 줄 높이에서 빼던 것을 고친다.한/글은 그런 런의 글자를 첫 줄의 글자로 센다. 그 런이 글보다 크면 첫 줄이 그만큼 높다. 머리표·번호와 같다.
부호도 글도 없는 빈 런은 전처럼 자리를 차지하지 않는다.
쪽 수 추정(실험,
estimate_pages)이 앞 문단의 어울림(SQUARE) 개체 띠가 남은 채 위아래 배치 개체(표 등)를 글 앞에 둔 문단을 지원 밖(... beside a square-wrapped object)으로 두던 것을, 그 문단의 줄이 띠에 닿지 않을 때 고친다.한/글은 그 개체를 띠와 상관없이 자기 위치(문단 윗선 + 세로 위치)에 놓고, 겹치면 띠의 개체 위에 그린다. 문단의 글 줄은 개체 아래에서 시작한다.
이제 개체가 첫 줄에 닿아 줄을 자기 아래로 밀고 그 발이 띠 끝보다 낮으면, 그 문단을 띠 없이 놓는다. 개체 아래 줄이 아직 띠에 닿는 경우와 어울림 개체가 둘인 경우는 전처럼 지원 밖이다.
쪽 수 추정(실험,
estimate_pages)이 글과 함께 움직이는 어울림(SQUARE) 그림·도형이 본문 바닥을 넘을 때, 줄 캐시가 없으면 지원 밖으로 두던 것을 고친다.한/글은 그 개체를 띄움과 상관없이 다음 쪽 본문 맨 위로 옮긴다. 개체가 있던 쪽의 줄은 모두 전체 폭이다. 다음 쪽에서 줄 위가 개체 아래 끝보다 위인 줄은 개체 곁으로 간다.
이제 그 쪽들을 다시 놓는다. 개체 문단과 뒤 문단을 전체 폭으로 나누고, 다음 쪽 맨 위의 개체 곁에서 줄을 다시 나눈다. 그 자리에 개체·각주가 든 문단이나 표가 오면 지원 밖이다.
나누지 않게 둔 어울림 표도 그렇게 옮겨 간다. 나뉘는 어울림 표는 쪽 끝에서 나뉜다(아래 항목).
문단 줄 아래로 띄워 위아래로 둔 그림·도형도, 문단이 놓인 자리에서 개체 발이 바닥을 넘으면 다음 쪽 맨 위로 옮겨 간다. 문단 줄은 제자리에 남고, 다음 쪽에서 개체에 닿는 줄은 그 아래로 간다. 캐시가 있을 때도 전에는 문단째 다음 쪽으로 보냈다.
양옆에 글이 가는 개체 곁의 줄은, 글이 첫 조각에서 끝나거나 글이 없어도 두 조각이다(둘째는 빈 조각). 캐시 없는 추정도 이제 그렇게 센다.
문단 첫 줄을 그림 아래로 밀어낸 위아래 그림은, 밀린 줄 자리에 그림 높이를 한 번 더 더해 바닥을 넘는다고 보아 지원 밖으로 두었다. 이제 밀리기 전 줄 자리에서 잰다.
쪽 수 추정(실험,
estimate_pages)이 세로쓰기 칸(hp:subList@textDirection="VERTICAL")의 글을 가로 글처럼 칸 폭에서 나누어 행을 키우던 것을 고친다.한/글은 세로쓰기 칸의 줄을 칸 높이를 따라 세로로 놓고 옆으로 늘어놓는다. 넘치는 글은 잘리고, 행은 선언 높이 그대로다.
세로쓰기 칸이 있는 흐르는 표는 쪽 끝에서 나뉘지 않고, 나누지 않게 둔 표처럼 통째로 다음 쪽으로 간다. 기준 문단 줄과 그 뒤 글은 앞 쪽에 남는다.
쪽 수 추정(실험,
estimate_pages)이 표 칸의 글을 칸 자신의 폭(hp:cellSz)에서 나누던 것을 고친다. 같은 열의 칸들이 폭이 다르면 한/글은 칸을 표 격자의 폭으로 놓는다.열은 왼쪽부터 쌓는다. 열마다 그 열에서 끝나는 칸들의 시작 + 폭(+ 칸 간격
cellSpacing) 가운데 가장 먼 곳에서 끝난다. 그래서 열은 그 열 낱칸 가운데 가장 넓은 것만큼 넓고, 여러 열을 합친 칸이 그 열들보다 넓으면 넘침이 마지막 열에 붙는다.칸은 첫 열의 시작부터 마지막 열의 끝까지다. 다른 행 때문에 자기 폭보다 넓어질 수 있다.
줄 캐시가 없는 칸의 줄 나눔과 행 높이에 쓴다(
hwpx.oxml.table_sizes.grid_widths_of).FormFit(
resolve_slot_metrics)도 칸의 자리를 그 폭에서 잰다(SlotMetrics.raw_width). 전에는 칸 자신의hp:cellSz폭이었다.
쪽 수 추정(실험,
estimate_pages)이 표의 칸 간격(hp:tbl@cellSpacing)을 표 높이에서 빠뜨리던 것을 고친다.한/글은 행마다 위에 칸 간격을 두고, 마지막 행 아래에도 하나 둔다. 쪽을 넘는 표는 바깥 여백처럼 쪽마다 그 쪽 조각의 마지막 행 아래에 간격을 다시 두고, 다음 쪽 첫 행 위에도 간격이 있다.
글자처럼 둔 표의 줄은 행들과 그 위아래 간격만큼 높다.
쪽 수 추정(실험,
estimate_pages)이 줄 캐시 없는 표 칸에서 글자처럼 둔 표 옆에 글 앞·뒤로 둔 개체가 있는 문단을 지원 밖(a nested table)으로 두던 것을 고친다.그런 개체는 자리를 차지하지 않으므로, 그 문단은 표 혼자인 문단처럼 표 높이의 한 줄이다.
위아래로 둔 표 옆에 그런 개체가 있으면 한/글은 빈 줄을 표 아래에 둔다(표 둘일 때처럼). 이제 캐시 없이도 따른다(아래 항목).
쪽 수 추정(실험,
estimate_pages)이 글 없는 문단에 문단 위에서 위아래로 둔 개체(표·그림)와 글 앞·뒤로 둔 개체가 함께 있을 때, 빈 줄을 위아래 개체의 윗선에 두던 것을 고친다.한/글은 공백이나 머리표가 있을 때처럼 빈 줄을 그 개체 아래에 전체 폭으로 둔다. 뜬 개체가 종이 기준이든 문단 기준이든, 글 앞이든 뒤든 같다.
위아래 개체만 있으면 전처럼 윗선에 폭 0이다.
본문과 표 칸 모두 그렇다. 표 칸에서는 전에 캐시 없이 지원 밖(
a nested table)이었다.
쪽 수 추정(실험,
estimate_pages)이 위아래 배치 개체와 글자처럼 둔 개체가 함께 있는 문단 일부를 지원 밖 (an object with text or other objects in its paragraph)으로 두던 것을 고친다.글 없는 문단에 문단 위에서 위아래로 둔 개체가 여럿이면, 한/글은 그 개체들을 쌓고 글자처럼 둔 개체의 줄을 그 아래에 둔다. 런 순서와 상관없다. 전에는 위아래 개체가 하나일 때만 셌다.
글 문단의 줄 아래로 띄워 둔 위아래 개체는 글 속에 글자처럼 둔 개체가 있어도 같다. 문단 줄은 제자리이고, 개체 띠에 닿는 줄은 개체 발 아래로 간다. 그 줄이 다음 쪽으로 가도 개체는 첫 줄의 쪽에 남는다.
글자처럼 둔 개체가 있는 문단에서 문단 위보다 위로 둔(세로 위치가 음수인) 위아래 표는 문단 윗선에 놓는다. 한/글은 표의 음수 세로 위치(부호 없는 32비트로 적힌다)를 0으로 보고 그 자리에 그린다.
공백만 든 문단도 글 문단이다. 공백 하나라도 있으면, 문단 줄 아래로 띄운 위아래 개체에 닿는 그 줄은 개체 발 아래로 간다. 빈 문단의 줄만 전처럼 문단 윗선에 남는다.
쪽 수 추정(실험,
estimate_pages)이 글자처럼 둔 개체 하나와 공백·줄 바꿈만 있는 문단을 언제나 한 줄로 세던 것을 고친다.한/글은 개체 바로 뒤 공백 둘을 여백을 넘어도 그 줄에 두고, 그다음 공백이 여백이나 그 너머에서 시작하면 다음 줄로 넘긴다. 개체 앞에 공백이 있고 그 뒤에 개체가 들어가지 않으면 개체가 다음 줄로 간다.
개체 앞뒤의 줄 바꿈도 새 줄을 연다. 개체 앞의 빈 줄은 글 한 줄만큼 내려간다.
이제 그런 문단을 FormFit 줄 나눔으로 나누고, 줄마다 높이를 센다(본문과 표 칸). 넘치지 않으면 전처럼 개체 높이의 한 줄이다.
유효한 줄 캐시가 있으면 다른 문단처럼 캐시의 줄(한/글이 파일을 열 때 쓰는 줄)을 그대로 쓰고, 캐시가 없을 때만 FormFit으로 나눈다.
FormFit
hancom_line_starts가 개체 자리(objects)를 받아, 개체 바로 뒤 둘째 공백도 여백 밖에 매단다.
쪽 수 추정(실험,
estimate_pages)이 글자처럼 둔 개체만 있는 문단의 줄 간격을, 문단 줄 간격의 종류와 상관없이 글자 크기에서 세던 것을 고친다.고정 줄 간격이면 다음 줄은 개체가 더 높아도 그 값만큼만 내려간다(개체는 겹쳐 그려진다).
최소 줄 간격이면 줄 사이는 개체 높이와 그 값 가운데 큰 쪽이다.
비율 줄 간격은 전처럼 글자 크기에서 센다. 개체를 줄 간격에 영향을 주게 둔 것(
hp:pos@affectLSpacing) 이면 줄 높이(개체 높이)에서 센다.본문과 표 칸 모두 그렇다.
쪽 수 추정(실험,
estimate_pages)이 줄 캐시 없는 표 칸에서 글자처럼 둔 표가 글 사이에 있거나 여럿인 문단을 지원 밖(a nested table)으로 두던 것을 고친다.이제 본문처럼 그 문단의 줄에 놓는다. 줄은 그 줄의 표와 글 가운데 큰 쪽만큼 높고, 들어가지 않는 표는 다음 줄로 간다.
칸 문단에 둔 머리말·꼬리말·각주 같은 컨트롤 안의 표는 그 칸이 아니라 컨트롤 자리에 놓이므로, 칸 안 표로 세지 않는다.
저장할 때, 줄이 개체 양옆 두 조각으로 나뉘고 오른쪽 조각이 비어 있는 한/글 줄 캐시를 낡은 것으로 보고 지우던 것을 고친다.
한/글은 그런 빈 조각(
hp:lineseg@flags의 16번 비트)을 글 끝 바로 다음 자리(textpos= 글 길이 + 1)에서 시작한다.전에는
textpos가 글 길이를 넘으면 문단의 줄 캐시를 통째로 지워, 무편집 저장에서도 캐시가 사라졌다.패키지 검사(
validate_package)와 레이아웃 검사도 이를 낡은 캐시로 보고했다.이제 글 끝 바로 다음의 빈 조각만 받아들인다. 그보다 뒤이거나 비지 않은 조각은 전처럼 낡은 캐시다.
쪽 수 추정(실험,
estimate_pages)이 글과 함께 움직이지 않게 둔(flowWithText="0") 어울림(SQUARE) 개체가 본문 바닥을 넘으면 지원 밖으로 두던 것을 고친다.한/글은 그런 개체를 다음 쪽으로 옮기지 않고 제 쪽에 둔다(바닥을 넘어 그린다).
곁 줄은 그 쪽 끝까지 좁게 이어지고, 다음 쪽 줄은 개체 없이 전체 폭이다.
이제 그 개체를 옮기지 않는다. 곁 문단이 다음 쪽으로 넘어가도, 그 줄이 캐시를 지키거나 글이 없으면 따른다. 글과 함께 움직이는 개체는 전과 같다.
쪽 수 추정(실험,
estimate_pages)이 문단 줄 아래로 띄워 둔 흐르는 위아래 표 바로 다음 문단에 또 그런 표가 있으면 지원 밖(a page break or another object beside a top-and-bottom table's band)으로 두던 것을 고친다.한/글은 뒤 문단을 앞 표의 발(쪽을 넘었으면 다음 쪽 조각의 발)에서 시작한다. 뒤 문단의 표는 그 문단 윗선
세로 위치에 놓이고, 그 뒤 글은 그 표의 발에서 시작한다.
뒤 문단의 첫 줄이 앞 표에 닿지 않으면 전처럼 지원 밖이다.
쪽 수 추정(실험,
estimate_pages)이 개체 하나만 든 빈 문단에 위아래로 띄워 둔 표나 그림을, 문단을 ‘띄운 거리개체 높이’만큼 높은 한 줄로 세어 뒤의 모든 줄을 개체 아래로 보내던 것을 고친다.
한/글은 빈 문단의 줄을 제자리에 둔다(개체의 띠가 그 줄 아래끝보다 위에서 시작하면 폭 0인 빈 조각이 된다).
뒤 줄은 보통 간격으로 놓이다가, 띠와 겹치는 첫 줄부터 띠 아래끝으로 간다(줄 간격은 겹침에 넣지 않는다).
흐르는 표는 홀로 둔 표처럼 앞 간격 위 문단 위쪽부터 띄운 거리를 잰다.
쪽 수 추정(실험,
estimate_pages)이 표 칸 첫 문단의 앞 간격을 칸 높이에서 빠뜨리던 것을 고친다.한/글은 칸의 첫 줄을 그 간격만큼 아래에서 시작한다. 글이 선언 높이보다 길면 행이 그만큼 더 늘어난다.
행이 쪽을 넘어 줄 사이에서 나뉘면 다음 쪽 부분에는 붙지 않는다(문단 중간 줄이므로). 행째 넘어가는 표는 행마다 붙는다.
글자처럼 둔 표도 그렇다.
쪽 수 추정(실험,
estimate_pages)과 FormFit이, 들여쓰기나 내어쓰기가 줄에 1440보다 좁은 폭을 남기는 문단의 그 줄을 1440 폭으로 놓고 나누던 것을 고친다.한/글은 그런 들여쓰기를 통째로 무시하고 문단의 모든 줄을 전체 폭으로 놓는다. 글자 크기와 상관없다.
1440 이상을 남기면 전처럼 들여쓰기를 둔다. 글자 하나가 들어가지 않을 만큼 좁아도 줄마다 한 글자씩 둔다.
글머리표·번호가 있는 문단은 전과 같다.
쪽 수 추정(실험,
estimate_pages)이 글상자(hp:drawText)가 든 도형의 높이를hp:sz로만 세던 것을 고친다.한/글은 글상자의 글이 넘치면 도형을 글과 위아래 글 여백만큼 늘려 그린다. 늘어난 크기는
hp:curSz에만 적고 (hp:sz는 그대로 둔다), 가끔 1 작게 적는다.이제 글상자의 글(캐시가 있으면 그 줄, 없으면 배치한 줄)과 위아래 글 여백을 더한 높이, 그리고
hp:curSz가운데 큰 쪽이hp:sz보다 높으면 그 높이로 센다.글자처럼 둔 것과 그렇지 않은 것, 본문과 표 칸 모두 그렇다. 글이 넘치지 않으면 전처럼
hp:sz다.위아래로 뒤집은 도형은
hp:curSz가 음수로 적힌다. 그 크기를 쓴다.
HwpxDocument.save_report가.hwp경로에도 HWPX 패키지(ZIP)를 써서, 확장자와 다른 형식의 파일이 경고 없이 생기던 것을 고친다(#375). 이제save_to_path처럼 HWP 5.0으로 쓴다. HWP 5.0으로 담을 수 없는 내용이면 아무것도 쓰기 전에Hwp5Error(hwp5-write-unsupported)를 낸다. 저장 관문은 전처럼 HWPX 부분을 검사하고, 통과했을 때만 쓴다.도형 위치(
HwpxOxmlShape.set_position)와 새 연결선(doc.shapes.add_connector)이 기준 위나 왼쪽으로 둔 음수 오프셋을"-2000"처럼 부호 있는 수로 쓰던 것을 고친다.한/글은 음수 오프셋을 부호 없는 32비트(
4294965296)로 적고 읽는다. 부호 있는 수는 0으로 읽어, 도형이 기준 가장자리에 그려지고 다시 저장하면 0이 된다. 스키마도 음 아닌 정수다.이제 둘 다 부호 없는 32비트로 쓴다. 연결선은 잇는 도형의 오프셋을 부호 있는 수로 다시 읽는다.
쪽 수 추정(실험,
estimate_pages)이 흐르는 표가 그 문단 줄보다 뒤 쪽에서 끝날 때, 다음 문단을 한/글처럼 둔다.한/글은 다음 문단의 줄을 개체 띠 아래로 밀린 줄처럼 표 끝 바로 아래에 둔다. 그 문단의 앞 간격은 자리를 차지하지 않는다. 전에는 앞 간격을 더했다.
그 문단이 단 나누기로 시작하면 앞 문단 줄의 다음 단에 놓인다. 1단이면 표가 끝난 쪽의 표 아래다. 전에는 그 다음 쪽으로 보냈다.
쪽 나누기는 전처럼 표가 끝난 쪽의 다음 쪽 맨 위다.
쪽 수 추정(실험,
estimate_pages)이 쪽이나 용지 기준으로 세로 가운데에 둔 위아래 개체를 지원 밖 (... placed TOP_AND_BOTTOM relative to PAGE·PAPER)으로 두던 것을 고친다.한/글은 그 개체를 본문(쪽 기준)이나 용지(용지 기준)의 세로 가운데에 놓고, 세로 위치만큼 내린다. 음수면 올린다(부호 없는 32비트로 적힌다). 그 쪽에서 개체 띠에 닿는 줄은 띠 아래로 간다.
위·아래 기준은 전과 같다.
쪽 수 추정(실험,
estimate_pages)이 표 칸 문단에서 위아래로 둔 그림·도형을 한/글처럼 센다.캐시가 있으면 표가 든 칸처럼 칸 줄을 한/글이 둔 자리대로 센다. 개체 아래로 밀린 줄과, 개체 끝까지 닿는 칸이 그대로 들어간다.
위로 띄운 개체(음수 띄움)는 그 문단 윗선에 선다. 칸 위나 앞 문단으로 올라가지 않는다. 전에는 띄움만큼 위로 셈해 행 높이에서 빠졌다.
개체 뒤에 글이 있으면(공백 하나나 글머리표·번호도) 그 줄은 개체 아래로 간다. 캐시가 없을 때 표 뒤 글만 그렇게 셌던 것을 그림·도형으로 넓힌다.
쪽 수 추정(실험,
estimate_pages)이 표 위·아래 캡션의 높이에서 캡션 첫 문단의 앞 간격을 빠뜨리던 것을 고친다. 한/글은 캡션의 첫 줄을 그 간격만큼 아래에서 시작한다. 캡션이 차지하는 자리는 그 앞 간격과 줄들과 gap을 더한 높이다. 마지막 문단의 뒤 간격은 들지 않는다. 글자처럼 둔 표와 흐르는 표 모두 그렇다.쪽 수 추정(실험,
estimate_pages)이 위아래 배치 개체(표·그림) 뒤에 공백만 있는 문단, 그리고 글머리표·번호가 붙은 문단을 글 없는 문단으로 보던 것을 고친다.한/글은 공백과 머리표·번호도 글로 본다. 그 줄을 개체 아래에 전체 폭으로 두고, 다음 문단은 그 줄 아래에 둔다.
공백도 머리도 없을 때만 줄을 개체 윗선에 폭 0으로 둔다.
공백은 본문과 표 칸 모두, 머리표·번호는 본문에서 따른다.
공백만 있는 한 줄 문단의 표가 그 자리에서 시작하지 못하면 표 혼자인 문단처럼 놓는다. 표는 다음 쪽 맨 위에서 시작하고, 문단 줄은 제자리에 남으며, 뒤 글은 그 줄 아래에 이어진다. 나누지 않게 둔 표도 같다.
쪽 수 추정(실험,
estimate_pages)이 줄 캐시 없는 표 칸에서 위아래 배치 표 하나가 든 문단을 지원 밖 (a nested table)으로 두던 것을 고친다. 이제 그림·도형처럼 칸이 그 표의 아래 끝(행으로 잰 표 높이와 바깥 여백)까지 닿고, 칸의 다음 줄은 그 아래에서 이어진다. 위로 띄운 표는 문단 윗선에 선다. 그런 행이 쪽을 넘어 줄 사이에서 나뉠 때도 표는 그 높이의 한 줄로 센다. 글 앞에 둔 그런 표는 거기 닿는 첫 줄부터 표 아래로 보낸다. 한 문단에 표가 둘이면 한/글은 빈 줄을 아래 표 밑으로 보내므로 전처럼 캐시가 있어야 한다.쪽 수 추정(실험,
estimate_pages)이 위아래 개체의 띠에 닿는 줄에 글자처럼 둔 개체가 있으면 지원 밖 (a page break or another object beside a square-wrapped object)으로 두던 것을 고친다. 한/글은 그런 줄도 글 줄처럼 띠 아래로 보낸다. 글자처럼 둔 개체가 줄 높이를 키우지 않으면 줄 높이는 글 높이 그대로다.쪽 수 추정(실험,
estimate_pages)이 어울림(SQUARE) 개체 곁 띠에 글자처럼 둔 개체 하나만 든 빈 문단을 지원 밖 (an object beside a square-wrapped object)으로 두던 것을 고친다. 한/글은 개체가 곁 자리보다 넓으면 그 줄을 띠 아래로 보내 전체 폭에 두고, 좁으면 곁에 둔다.쪽 수 추정(실험,
estimate_pages)이 어울림 개체 곁 띠에서 글 사이에 글자처럼 둔 개체가 든 문단을 띠 없이 세던 것을 고친다. 이제 띠 안에서 시작하는 줄은 곁 자리 폭으로 나눈다. 곁 자리보다 넓은 개체가 든 첫 줄은 한/글처럼 띠 아래로 보내 전체 폭에 두고, 곁의 남은 자리는 비운다. 글이 개체 양쪽에 놓이는 띠에서는 지원 밖 (objects set as characters among text beside a square-wrapped object)이다.쪽 수 추정(실험,
estimate_pages)이 칸 단위로 나누는 흐르는 표(pageBreak="CELL")에서 병합 칸으로 이어진 행들이 한 쪽보다 길면 “rows merged together taller than a page”로 거부하던 것을 고친다. 한/글은 쪽 끝마다 다시 나눈다. 칸마다 그 쪽에 들어가는 줄까지 두고 나머지는 다음 쪽 머리 행 아래로 넘기며, 넘긴 부분이 다시 쪽보다 길면 그 쪽 끝에서 또 나눈다. 행들보다 긴 병합 칸은 행이 끝난 뒤에도 혼자 다음 쪽들로 이어진다. 글보다 높게 선언된 칸의 남는 높이도(글이 다 들어간 뒤에도) 쪽 끝마다 행처럼 잘려 이어진다. 어느 쪽에도 한 줄도 들어가지 않는 칸만 전처럼 거부한다. 줄 사이에서 나뉜 선언 행의 다음 쪽 부분은 선언 높이의 나머지와, 이어지는 줄에 칸 위아래 여백을 더한 높이 가운데 큰 쪽이다. 이어지는 줄이 있으면 남은 높이가 낮아도 버리지 않는다.쪽 수 추정(실험,
estimate_pages)이 곁에 자리를 남기는 종이·쪽 기준 어울림(SQUARE) 개체의 띠에 닿는 줄을, 그 줄이 줄 캐시를 가질 때 따른다. 한/글은 그런 줄을 같은 높이·간격으로 개체 곁에 좁혀 둔다(양옆에 자리가 있으면 한 높이에 두 조각). 캐시 없는 줄·표·개체가 닿으면 전처럼 지원 밖이다.쪽 수 추정(실험,
estimate_pages)이 같은 폭의 여러 단 쪽에서 종이 기준으로 둔 위아래 배치 개체가 본문 너비를 다 덮지 않으면 지원 밖(objects placed on the paper)으로 두던 것을 고친다. 한/글은 개체가 가로로 걸치는 단에서만 줄을 개체 아래로 보내고, 걸치지 않는 단의 줄은 개체가 없을 때 자리에 둔다. 이제 종이의 왼쪽·가운데· 오른쪽을 기준으로 둔 개체는 바깥 여백까지 쳐서 걸치는 단마다 그렇게 놓는다.hwpx.experimental.latex_to_eqedit(실험)가e^{it},x_{rm}처럼 글자가rm·it·bold가 되는 식별자를 그대로 쓰던 것을 고친다. 한/글은 이 낱말을 글꼴 전환으로 읽어서, 첨자에 홀로 오면 아무것도 그리지 않는다. 이제 다른 예약어처럼 따옴표로 감싸 쓰고(e ^{"it"}),eqedit_to_latex는 그것을 다시 글자로 읽는다.latex_to_eqedit가\lbrace·\rbrace를\{·\}처럼LBRACE·RBRACE로 옮긴다(\left·\right뒤에서도). 전에는 지원하지 않는 명령이었다.eqedit_to_latex는LEFT·RIGHT밖의 맨LBRACE·RBRACE를\{·\}로 읽는다. 전에는 낱말 그대로 남았다.수식 상자 설명에, Windows의 한/글은 저장할 때 상자(
hp:sz)와baseLine을 그대로 두고 그 상자로 쪽을 배치한다는 점을 더한다.쪽 수 추정(실험,
estimate_pages)이 줄 캐시 없는 표 칸 문단에서 글자처럼 둔 개체(그림·도형·수식 등)를 세지 않던 것을 고친다. 전에는 칸 문단에 혼자 든 표만 그 높이로 셌고, 다른 개체는 없는 것처럼 줄을 세어 행이 한/글보다 낮았다. 이제 개체 하나만 든 문단은 개체 높이의 한 줄이고 줄 간격은 글자 크기로 센다. 글 사이에 있거나 여럿이면 본문처럼 개체의 폭과 높이로 줄 안에 놓는다.쪽 수 추정(실험,
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 폭의 고정폭 빈칸으로 재어 줄을 늦게 나누었다.FormFit은 고정폭 빈칸이 낱말 바로 뒤라도 여백에서(또는 넘어서) 시작하면 다음 줄로 넘긴다. 전에는 낱말이 여백에 딱 맞을 때 그 뒤 고정폭 빈칸을 줄 끝에 매달았다.
쪽 수 추정(실험,
estimate_pages)이 글 문단의 첫 줄에서 아래로 띄운 흐르는 위아래 표를 쪽 끝에서도 따른다.그 줄이 쪽 바닥 위에 들어가지 않으면 줄과 표가 함께 다음 쪽으로 가고, 표는 거기서 띄운 만큼 아래에서 흐른다.
줄은 들어가는데 표 윗선이 바닥을 넘거나, 행 단위로 옮기는(TABLE) 표의 첫 행이 들어가지 않으면, 표만 다음 쪽 맨 위에서 시작한다. 문단의 글은 그 줄 아래로 이어지고, 표가 차지하는 쪽에서는 표 아래로 간다.
전에는 모두 지원 밖(
a top-and-bottom table offset down from a line at a page end)이었다. 첫 줄이 아닌 줄에서 띄운 표는 전처럼 지원 밖이다.
쪽 수 추정(실험,
estimate_pages)이 표 칸 안에 위아래나 어울림으로 둔 그림·도형의 높이를 행 높이에 넣는다. 글 없는 칸 문단에서 문단 위를 기준으로 둔 것(글자처럼 두지 않은 것)이면, 칸이 그 개체의 아래 끝(바깥 여백 포함)까지 온다. 전에는 그 개체를 세지 않아 행을 한 줄 높이로 잡았다.쪽 수 추정(실험,
estimate_pages)이 병합 칸으로 묶인 행들이 쪽 바닥을 빈 높이 안에서만 넘을 때, 그 행들을 그 쪽 바닥에서 끝낸다. 모든 칸의 글이 바닥 위에 들어가고 남는 높이가 1282 이하면 다음 쪽으로 넘길 것이 없다. 전에는 표 윗 여백만 있는 빈 쪽이 하나 더 생겼다.쪽 수 추정(실험,
estimate_pages)이 띠가 본문 아래 끝을 넘는 어울림(SQUARE) 개체를, 그 문단과 곁 문단이 줄 캐시를 가질 때 받는다.한/글은 이런 개체만 다음 쪽 맨 위로 보낸다. 개체는 줄 높이를 차지하지 않으므로 캐시를 가진 줄은 캐시 자리에 그대로 있다.
캐시 없는 문단이 그 곁에 있으면 전처럼 지원 밖이다. 줄을 어디서 나눌지가 개체의 쪽에 달려 있기 때문이다.
쪽 수 추정(실험,
estimate_pages)이 곁에 줄 하나 들어갈 자리를 남기는 종이·쪽 기준 어울림(SQUARE) 개체를 받는다(한 단일 때).그 쪽의 줄·표·개체가 개체의 띠에 닿지 않으면 개체가 없을 때와 같다. 띠 위 끝에 바닥이 맞닿은 줄도 그렇다.
닿는 줄은 한/글이 개체 곁에 좁혀 둔다. 이것은 따르지 않고 지원 밖으로 둔다(
text beside an object placed on the paper).
쪽 수 추정(실험,
estimate_pages)이 시험지 첫 쪽 모양을 따른다. 종이 위에서 본문 머리에 걸친 머리 표 아래에 문단 위에서 위아래로 둔 문항 표가 서는 모양이다.종이·쪽 기준 어울림(SQUARE) 개체가 본문 양옆에 줄 하나 들어갈 자리(1440)를 남기지 않으면, 위아래 개체처럼 그 쪽 줄을 띠 아래로 민다. 곁에 자리가 남으면 전처럼 지원 밖이다.
문단 첫 줄 위에 앵커한 위아래 개체는 그 줄이 띠에 밀리면 띠 바로 아래에 서고, 그 줄은 개체 아래로 간다. 흐르는 표만 든 문단도 줄이 띠에 밀리면 표가 띠 바로 아래에 서고 빈 줄은 표 아래로 간다. 밀리지 않으면 줄은 전처럼 표 윗선에 있다.
띠 끝에 맞닿아 시작하는 이런 표는 받는다. 띠에 닿는 흐르는 표는 전처럼 지원 밖이다.
쪽 수 추정(실험,
estimate_pages)이 구역의 ‘빈 줄 감추기’(hp:visibility@hideFirstEmptyLine)를 따르지 않던 것을 고친다. 이 설정이 켜진 구역에서 한/글은 다음 쪽이나 단을 시작할 빈 문단을 둘까지 앞 쪽 바닥 밑, 그 문단이 설 자리에 겹쳐 두어 감춘다. 감춘 문단은 자리를 차지하지 않고 쪽을 만들지 않으며, 다음 글은 다음 쪽 맨 위에서 시작한다. 셋째 빈 문단부터, 쪽을 나누는 문단, 빈칸이 든 문단은 감추지 않는다. 전에는 그런 빈 문단을 다음 쪽으로 넘겨, 문서가 빈 문단으로 끝나면 빈 쪽을 하나 더 세기도 했다.그림 넣기(
doc.add_picture)가align에 받은 값을 검사 없이hp:pos@horzAlign에 쓰던 것을 고친다.한/글은 모르는 정렬 값을 왼쪽(LEFT)으로 읽는다. 정수 같은 값은 그림과 문단을 더한 뒤에야 오류가 났다.
이제 대소문자와 상관없이
LEFT·CENTER·RIGHT·INSIDE·OUTSIDE만 받는다. 그 밖의 값은 아무것도 더하기 전에shape-position-frame오류로 거부한다.
쪽 수 추정(실험,
estimate_pages)이 칸 단위로 나누는 흐르는 표에서 문단이 여럿인 칸을 줄 간격 하나로만 세어 나누던 것을 고친다. 한/글은 문단 사이 간격(앞 문단의 아래 간격과 다음 문단의 위 간격)도 세고, 다음 쪽 부분이 문단의 첫 줄로 시작하면 그 문단의 위 간격을 그대로 둔다. 이제 그런 칸은 줄마다 제 자리로 나눈다. 전에는 문단 간격이 있으면 한 쪽에 줄을 더 넣어, 표가 한/글보다 짧게 끝날 수 있었다.문단 모양 적용(
doc.styles.apply_paragraph_format)이 퍼센트 줄 간격의hh:lineSpacing@unit을PERCENT로 쓰던 것을 고친다.스키마의 단위는
CHAR·HWPUNIT이고, 한/글은 퍼센트 줄 간격도HWPUNIT으로 적는다.PERCENT로 쓴 것도 같게 배치하지만, 다시 저장하면 바꾼다. 이제 처음부터 그렇게 쓴다.그래서 같은 줄 간격의 문단 모양이 이미 있으면, 단위만 다른 모양을 새로 만들지 않고 그것을 쓴다.
[6.7.0] - 2026-10-01
본문 쪽 수 추정(실험, hwpx.experimental.estimate_pages)을 더하고, 곡선·연결선, 바탕쪽 정리, 글상자 채우기,
쓰지 않는 그림·차트 정리 같은 저작·정리 API를 넓힌 릴리스입니다. FormFit은 글꼴마다 한/글이 배치하는 폭과
칸을 늘리는 식을 따르도록 다시 맞춰서, 고르는 글자 크기와 줄 수가 6.6.0과 달라질 수 있습니다. 누름틀 채우기도
한/글에 맞췄습니다. 여러 문단에 걸친 누름틀은 걸친 문단을 지우고, 끝(hp:fieldEnd)이 없는 누름틀은
field-end-missing 오류를 냅니다. 새 도형의 기본 선 굵기는 33(0.12 mm)이고, 각주·미주의 인스턴스 ID는
instId로 씁니다. 저장 성공만으로 시각 품질을 보증하지 않습니다.
추가
hwpx.experimental.latex_to_eqedit(실험)가 고등학교 수학에서 흔한 표기 몇 가지를 더 옮긴다(#347). 전에는UnsupportedLatexError로 거부했다.\,→`(작은 공백),\overrightarrow{AB}→vec {AB}(두 글자 위로 늘어난 화살표),\overleftrightarrow{AB}→dyad {AB}(↔),\therefore→therefore(∴),\because→because(∵).\mathbf{X}→{rm {bold {X}} it}.bold만으로는 영문자가 기울임으로 남고,rm은 중괄호 밖으로 번지므로 곧은 굵은 글꼴로 그린 뒤it으로 기울임을 되돌린다.eqedit_to_latex는 이 철자들을 되읽는다(`→\,,dyad→\overleftrightarrow,therefore·THEREFORE→\therefore,because→\because,{rm {bold X} it}→\mathbf{X}). 수식 크기 추정은dyad를vec처럼 꾸밈으로 잰다.
hwpx.experimental.latex_to_eqedit(실험)가\mid와\binom도 옮긴다(#347). 전에는UnsupportedLatexError로 거부했다. 철자는 한/글에서 그려 본 결과(#347 댓글)를 따른다.\mid→~|~. 맨|는 앞뒤 간격 없이 붙어 그려지고,~|~가\mid의 간격에 가장 가깝다.\binom{n}{r}→{n} choose {r}.binom {n} {r}·pmatrix {n # r}와 같은 모양이고,( {n} atop {r} )는 괄호가 늘어나지 않는다.eqedit_to_latex는~|~를\mid로,{n} choose {r}와binom {n} {r}를\binom{n}{r}로 되읽는다. 수식 크기 추정은 둘을atop처럼 두 줄로 잰다.choose·binom은 예약어가 되어, LaTeX의 맨 글자choose는 따옴표로 감싼다.LaTeX
~(띄움)를 EqEdit의 보통 띄움~로 옮긴다. 전에는 거부해서,eqedit_to_latex가 내보낸~가 든 LaTeX를 다시 EqEdit로 옮기지 못했다. 이제 EqEdit의 작은 띄움`(\,)과 보통 띄움~가 모두 왕복한다.
doc.shapes.add_curve()·add_connector()를 더한다(곡선과 붙은 연결선 저작).곡선은 앵커를 지나는
CURVE구간으로 쓴다(닫으면 끝에서 첫 앵커로). 한/글은 곡선의 크기 상자를 다시 계산하지 않으므로, 한/글 곡선이 갖는 상자(앵커를 지나는 Catmull-Rom 곡선을 구간마다 16단계로 근사한 상자)를 쓰고 앵커는 그 상자 기준으로 둔다. 직선 구간이 섞인 곡선과 묶음 안 곡선은 쓰지 않는다.연결선은 글자처럼 두지 않고 같은 기준에 놓인 두 도형의 변 가운데(위·오른쪽·아래·왼쪽)에 붙인 직선(
STRAIGHT)이나 꺾인 선(STROKE)이다. 한/글은 붙은 연결선을 도형 상자로 다시 그리므로 도형을 옮기거나 키워도 선이 따라간다. 호(ARC) 연결선은 제어점이 필요해 거부한다(shape-connector-kind-unsupported).오류 코드
shape-curve-too-few-points,shape-connector-target-inline,shape-connector-frame,shape-connector-side-invalid,shape-connector-kind-unsupported를 더한다.
doc.shapes.add_drop_cap(style="DoubleLine")을 지원한다(전에는TripleLine만 받고 나머지는shape-drop-cap-style-unsupported로 거부했다). 한/글은 같은hp:rect+hp:drawText구조에hp:parameterset값만 1(TripleLine은 2)인 첫 글자 장식을 읽고, 그리고, 그대로 저장한다. 상자 크기가 같으면 두 스타일의 화면이 같다. 상자 옆으로 가는 줄 수는 상자 높이가 정한다.Margin은 상자를 여백 쪽 어디에 두는지 근거가 없어 계속 거부한다.쪽 수 추정(실험,
estimate_pages)이 문단 위에서 위아래로 둔 개체와 글자처럼 둔 개체가 함께 있는 문단을 따른다(글이 없어도). 한/글은 글자처럼 둔 개체의 줄을 위아래 개체 아래(바깥 여백 포함)에 두고, 흐르는 표가 쪽 끝에서 나뉘면 표의 끝 아래에 둔다. 전에는 글이 있는 문단만 따랐고, 글자처럼 둔 개체가 함께 있으면 지원 밖이었다.문단 위에서 아래로 띄운 위아래 개체도 글 없이 글자처럼 둔 개체만 있는 문단에서 따른다. 글자처럼 둔 개체들이 한 줄이고, 글 문단의 줄처럼 개체에 닿으면 개체 아래로 가고 닿지 않으면 제자리이며, 다음 문단은 보통 자리와 개체 아래 가운데 낮은 쪽에 온다. 그런 줄의 높이는 글자처럼 둔 개체와 글자 크기 가운데 큰 쪽이다.
쪽 수 추정(실험,
estimate_pages)이 중첩 표를 품은 행이 쪽을 넘어 나뉜 흐르는 표를 따른다. 한/글은 그런 칸의 줄 캐시를 다음 쪽 부분에서 맨 위부터 다시 적는데, 전에는 그 칸을 읽지 못해 지원 밖이었다. 이제 위로 되돌아간 줄을 나뉘지 않은 칸에서 설 자리로 이어 읽고(앞 줄의 높이와 간격 아래, 문단의 첫 줄이면 문단 위), 행을 다시 나눈다. 문단 위에서 놓은 개체 아래에 있는 첫 줄은 그 위 공간을 함께 차지해, 개체와 함께 다음 쪽으로 간다.쪽 수 추정(실험,
estimate_pages)이 폭이 다른 다단 구역을 따른다. 모든 문단이 저장 때 남는 줄 캐시를 가지고 개체를 글자처럼만 두면, 줄은 캐시대로 두고 단마다 같은 본문 높이로 흐른다(전에는 지원 밖이었다). 글자처럼 두지 않은 개체가 있으면 지원 밖이다.쪽 수 추정(실험)이 폭이 다른 다단에서 줄 캐시가 없는 문단도 따른다. 단의 폭은 본문 폭을 단 너비·간격 (
hp:colSz)의 비율로 나눠 반올림한 값이다. 단에 든 줄은 그 단의 폭으로 나누고, 문단이 폭이 다른 단으로 넘어가면 남은 글을 그 단의 첫 글자부터 그 단의 폭으로 다시 나눈다. 전에는 줄 캐시가 없는 문단이 있으면 지원 밖이었다. 그런 문단에 개체·글자 겹치기·덧말·각주가 있으면 여전히 지원 밖이다.쪽 수 추정(실험)
hwpx.experimental.estimate_pages(document)를 더한다. 한/글 없이 본문 문단의 줄마다 쪽·단· 세로 위치(EstimatedLine)와 쪽 수(PageEstimate.pages)를 추정한다.줄 나눔: 문단의 줄 캐시가 저장 때 남는 것이면 그 줄(한/글이 그린 줄)을 쓰고, 아니면 FormFit처럼 줄을 나눈다. 캐시의 줄은 한/글이 준 높이와 간격(
vertsize,spacing)대로 놓는다. 캐시 없이 글자 크기가 여럿인 문단은 글자마다 제 크기로 줄을 나누고, 줄마다 그 줄에서 가장 큰 글자만큼 높으며 줄 간격도 그 크기로 센다. 빈 run은 자리를 차지하지 않는다. 글이 든 run마다 글꼴·장평·자간이 다르면 글자마다 제 run의 것으로 줄을 나눈다(전에는 첫 run의 것을 모든 글자에 썼다). FormFit의hancom_line_starts()에 글자마다 크기와 글자 모양을 주는sizes·styles를 더한다.세로: 줄 간격 네 종류, 문단 앞뒤 간격, 쪽·단 나눔, 문단 보호·다음 문단과 함께·외톨이줄, 같은 폭 다단, 글자처럼 취급한 표·그림, 문단에 붙은 위아래 배치 개체, 본문과 함께 흐르는 표(칸·행 단위로 나눔, 나누지 않음, 머리행 반복), 각주(다음 쪽으로 이어짐 포함)를 따른다. 줄은 끝이 본문 끝보다 위일 때만 그 쪽에 남는다. 끝이 본문 끝과 같은 줄(빈 줄도)은 다음 쪽으로 가고(전에는 남았다), 1 위에서 끝나는 줄은 남는다.
글 사이에 글자처럼 둔 개체(그림·표·도형·수식, 글 앞·가운데·뒤)는 그 자리에서 글자처럼 제 폭을 차지하고, 그 줄을 개체 높이까지 높인다. 줄 간격은 글의 것이고, 고정 줄 간격이면 다음 줄은 그만큼 아래에 온다. 줄 간격이 퍼센트·고정이 아니면 따르지 않는다.
글 앞으로·글 뒤로 둔 개체(글자처럼 두지 않은 것)는 자리를 차지하지 않는다. 문단·종이 어느 기준에 놓이든, 쪽 끝을 넘어도 줄은 그 개체가 없을 때 자리에 놓인다. 전에는 그런 개체가 글이나 다른 개체와 한 문단에 있거나 문단에 혼자 있으면 따르지 않았다.
종이나 쪽(본문)의 위 끝이나 아래 끝을 기준으로 놓은 위아래 배치 개체(글자처럼 두지 않은 것)는 앵커 문단이 놓인 쪽 전체에 작용한다. 그 쪽에서 개체 자리(기준 끝에서 오프셋만큼 안쪽, 개체 높이만큼)에 닿는 줄은 앵커 문단의 줄이든 그 앞뒤 문단의 줄이든 개체 아래로 내려가고(자리가 본문 끝까지 닿으면 다음 쪽으로), 뒤 줄이 따라간다. 표도 같다. 다단이면 본문 너비를 다 덮는 개체가 그 쪽의 모든 단에 작용한다. 개체는 문단 안에서 자리를 차지하지 않는다. 같은 쪽의 흐르는 표가 앵커 줄부터 끝까지 개체 자리에 닿거나 그 바로 아래에서 시작하면 따르지 않는다(전에는 이런 개체를 따르지 않았다).
글이 든 문단에 앵커한 위아래 배치 개체(문단 위 기준, 오프셋 0)는 그 자리가 든 줄의 위에 놓이고, 그 줄과 문단의 나머지는 개체 아래에 온다. 흐르는 표면 행 단위로 나뉜다. 오프셋이 있으면 개체는 그 줄의 위에서 오프셋만큼 아래에 놓이고, 개체에 닿는 첫 줄(그 문단이나 다음 문단의)과 그 뒤 줄들이 개체 아래로 내려간다. 흐르는 표(칸·행 단위로 나눔)는 그 자리에서 쪽 끝을 넘어 이어지고, 표에 닿는 첫 줄은 표가 끝나는 쪽의 표 아래로 간다(전에는 따르지 않았다). 그 밖의 개체나 그 위의 줄이 쪽을 넘으면 따르지 않는다.
글 앞에(또는 문단에 혼자) 둔 어울림(SQUARE) 개체(문단 위 기준, 단·문단·종이의 왼쪽이나 오른쪽에서 오프셋만큼 — 첫 글자 장식도 그렇다)는 곁의 두 쪽 가운데 폭이 1440 이상이고 글 흐름(양쪽·넓은 쪽·왼쪽만· 오른쪽만)이 허락하는 쪽에 글을 둔다. 개체 높이 안에 줄의 위가 드는 줄은 그 문단과 다음 문단들에서 그만큼 좁아지고, 양쪽에 글이 가면 한 줄이 같은 높이의 두 조각(왼쪽 먼저)이 된다. 전에는 단 끝에 붙고 양쪽으로 흐르는 개체만 따랐다. 표는 행 높이의 합을 높이로 쓴다. 단보다 넓어 곁에 자리가 없는 어울림 개체는 위아래 배치처럼 글을 아래로 민다. 개체나 그 곁의 줄이 쪽을 넘으면 따르지 않는다. 줄 캐시에서도 같은 높이에 나란한 두 조각은 한 줄로 센다.
글자 겹치기·덧말이 든 문단은 저장 때 남는 줄 캐시가 있으면 그 줄(한/글이 그린 줄)을 따른다. 캐시가 없으면 (표 칸 안 문단도) 글자 겹치기·덧말을 그 run의 글자처럼 제자리에 두고 줄을 나눈다. 글자 겹치기는 틀(원·사각형 등)이 있거나 펼침(
SPREAD)이면 한글 한 글자의 폭, 틀 없이 겹치면 가장 넓은 글자의 폭이고, 줄 높이를 바꾸지 않는다. 덧말은 본말의 폭이다(덧말이 더 길어도). 덧말이 든 줄은 본말 위에 덧말(크기 비율, 0이면 절반)을 얹은 높이, 아래 덧말이면 본말 높이의 91/100에 덧말을 더한 높이이고(둘 다 1/1800인치 단위로 셈), 줄 간격도 그 높이로 센다. 전에는 캐시가 없으면 본문 문단은 따르지 않았고, 칸 안 문단은 글자 겹치기·덧말을 빼고 셌다.줄 캐시를 쓸 때 줄의 높이와 다음 줄까지의 거리는
textheight와spacing으로 센다. 글자처럼 둔 개체가 뒤따르는 줄은vertsize가 개체 높이일 때가 있지만, 한/글은 그 줄의 글 높이만큼만 내려간다. 표 칸 안 문단도 저장 때 남는 줄 캐시가 있으면 그 줄 높이와 간격을 쓴다(전에는 줄 수만 쓰고 높이는 첫 글자 크기로 셌다). 높이가 0인 줄로 된 캐시는 없는 것으로 본다.흐르는 표의 앵커 줄이 쪽 끝에 들어가지 않으면, 앵커 줄과 표가 다음 쪽 위에서 시작한다.
행 단위로 나누는 흐르는 표의 첫 행이 앵커 문단 아래에 들어가지 않으면, 표는 다음 쪽에서 시작한다. 표 뒤의 글은 앵커 아래에 이어지다가, 표가 차지한 쪽에서는 표 아래에 놓인다.
칸 안에 글자처럼 둔 표가 문단에 혼자 있으면, 그 줄은 표 높이(행 높이의 합과 바깥 여백)만큼 높고 줄 간격은 글의 것이다. 칸 단위로 나누는 표에서 그런 칸이 든 행은 칸 줄 사이에서 나뉜다. 줄마다 제 높이이고(안쪽 표는 한 줄), 쪽 끝에 들어가는 줄만 남고 나머지는 다음 쪽에서 이어진다. 행 단위로 나누는 표에서는 행째 다음 쪽으로 간다. 칸 안 표가 글과 한 문단에 있거나 글자처럼 두지 않은 것(위아래 배치나 어울림)이어도, 칸의 모든 문단이 저장 때 남는 줄 캐시를 가지면 따른다. 칸은 한/글이 그린 줄 위치대로 높고, 위아래 배치나 어울림 표가 마지막 줄보다 아래로 내려가면 그 표의 아래 끝까지 높다. 문단 위에서 위로 둔(음수 오프셋) 표는 문단 위에 선다 (전에는 따르지 않았다). 간다. 선언 높이가 글보다 큰 그런 행은 여느 선언 높이 행처럼 첫 줄이 들어가면 쪽 끝 바로 위에서 잘린다(아래). 간다.
흐르는 표의 행은 쪽 본문 끝에서 101 HWPUNIT 위까지만 쓴다. 그보다 아래에서 끝나는 행은 다음 쪽으로 가고, 칸 단위로 나누는 표에서는 그 아래로 내려가는 칸 줄부터 다음 쪽에서 이어진다(전에는 쪽 끝까지 썼다). 행 단위로 나누는 표의 첫 행이 앵커 아래에 들어가는지도 이 자리로 따진다.
칸 단위로 나누는 흐르는 표에서 선언 높이가 글보다 큰 행은 쪽 끝 바로 위(101 HWPUNIT)에서 잘리고, 남은 높이는 다음 쪽으로 넘어간다. 남은 높이가 1282(기본 칸 여백의 10pt 한 줄) 이하이면 넘기지 않는다. 칸 여백, 세로 정렬, 글자 크기는 자르는 자리와 이 문턱을 바꾸지 않는다.
글자처럼 취급한 표에 병합 행이나 중첩 표가 있어도, 표 안 모든 문단이 저장 때 남는 줄 캐시를 가지면 (한/글이 배치한 그대로면) 한/글이 저장한 표 높이(
hp:sz)를 쓴다. 그 밖의 병합 행 표(본문과 함께 흐르는 표 포함)는 행마다 한 행짜리 칸 가운데 가장 높은 칸의 높이이고, 병합된 칸이 걸친 행들보다 높으면 모자란 만큼 그 마지막 행이 높아진다. 병합 칸이 여럿이면 끝 행이 먼저인 것부터 차례로 따진다(걸친 행이 같거나, 엇갈리거나, 한쪽이 다른 쪽을 품어도). 한 행짜리 칸이 없는 행은 높이 0에서 시작한다. 행 단위로 나누는 흐르는 표에서는 병합 칸으로 이어진 행들이 한 덩어리로 다음 쪽으로 간다. 칸 단위로 나누는 표에서는 그 행들의 칸마다 쪽 끝까지 들어가는 줄만 남고 나머지 줄이 다음 쪽에서 이어진다. 쪽 끝이 걸린 행부터는 칸의 남은 줄만큼 높고, 병합 칸의 남은 높이는 모자란 만큼 그 마지막 행에 더한다. 그 행들에서 선언 높이가 글보다 큰 칸은 글의 첫 줄이 쪽 끝에 들어가면(글이 다 들어가지 않아도) 선언 높이를 행처럼 쪽 끝 바로 위에서 자르고, 남은 높이가 1282 이하이면 버린다. 첫 줄도 들어가지 않으면 칸째 다음 쪽으로 간다. 글이 든 문단에 앵커한 위아래 배치 표도 같다. 칸·글상자 안 문단의 단 설정은 그 목록의 것이라 구역의 단 변경으로 보지 않는다.그 밖의 것(미주, 구역 안 단 변경, 첫 문단 뒤의 구역 설정, 격자, 글과 개체가 한 문단(위의 글자처럼 둔 개체는 빼고)과 그런 문단의 각주, 앵커 아래나 한 쪽에 들어가지 않는 병합 행 덩어리, 한/글이 배치한 그대로가 아닌 표에서, 줄 캐시가 없는 칸의 글과 한 문단에 있거나 글자처럼 두지 않은 중첩 표, 표를 품은 칸 곁에 더 높은 칸이 있는 흐르는 표 행의 쪽 나눔, 다음 쪽에서 시작하는 흐르는 표 둘이 한 쪽에 놓임, 쪽·종이 기준 개체(글 앞·뒤 개체와, 흐르는 표가 없는 쪽의 종이 위 기준 위아래 개체는 빼고), 줄 캐시 없는 문단에서 위·아래가 아닌 자리나 퍼센트가 아닌 줄 간격의 덧말, 글자처럼 두지 않은 개체 곁의 글자 겹치기·덧말)이 있으면
pages는None이고unsupported가 구역마다 까닭을 적는다.
doc.shapes.remove_unused_charts()를 더한다. 어떤 차트도 가리키지 않는 차트 파트(Chart/chartN.xml)를 모두 지우고 지운 파트 이름을 돌려준다. 본문에서 차트를 지우면(section.clear_body(), 문단 삭제) 그 파트가 차트의 항목 이름·값과 함께 파일에 남았다. 한/글은 문서를 저장할 때 이런 파트를 지운다.docs/known-traps.md와clear_body()설명에도 적는다.export_text()·export_html()·export_markdown()에notes를 더한다.True면 각주·미주를 본문·칸·글상자 안의 제자리에 한/글 텍스트 저장처럼 쓴다. 참조 번호(1)) 뒤에 각주의 문단마다 그 번호로 시작하는 글과 줄바꿈이 온다. 번호는 각주·미주를 따로 문서 순서로 매기고, 구역의 번호 모양(autoNumFormat)과 번호 매기기(ON_SECTION은 구역마다newNum부터, 새 번호 지정hp:newNum)를 따른다. 쪽마다 새로 매기는 번호는 쪽을 나누지 않으므로 이어 매긴다. 기본값은 전처럼 각주·미주를 쓰지 않는다.doc.styles.apply_list_format()에continue_list를 더한다.True면 대상 문단 앞의 마지막 번호 목록(개요는 개요 목록)과 같은 번호 정의를 써서 한/글에서 번호가 이어진다. 전에는 부를 때마다 번호 정의를 새로 만들어, 문단마다 따로 부르면 목록이 매번 1부터 다시 시작했다(기본 동작은 그대로다). 앞에 그런 목록이 없으면 새 목록을 만든다. 앞 목록의 번호 정의에 없는 더 깊은 수준으로 이어 붙이면 그 수준의 모양을 새 목록과 같은 꼴(^1.^2.)로 정의에 더한다. 한/글은 번호 정의에 없는 수준에는 번호를 그리지 않는다.number_format·start와 함께 주면style-list-continue-conflict로 거부한다.doc.media.remove_unused_images()를 더한다. 문서가 어디서도 가리키지 않는 이진 항목(매니페스트 항목·파트· header의binItem)을 모두 지우고 지운 항목을 돌려준다. 한/글은 문서를 저장할 때 이런 항목을 지운다. 본문에서 그림을 지우면(section.clear_body(), 문단 삭제) 그 그림의 이진 항목이 남으므로 이 호출로 정리한다.remove_image가 확인하는 참조(그림, 채우기 그림·그림 글머리표, 바탕쪽, 동영상, OLE, 내장 글꼴)가 있는 항목은 남긴다.doc.fields.text_boxes와doc.fields.fill_text_box()를 더한다. 한/글은 이름이 있는 글상자 (hp:drawText@name)를 필드로 보고, 필드 목록에 넣고 이름으로 채운다.text_boxes는 이름 붙은 글상자를 문서 순서로 돌려준다(이름 없는 글상자는 뺀다).fill_text_box(value, name=, index=)는 한/글처럼 글상자의 글을 값 한 문단으로 바꾼다. 첫 문단과 첫 run의 모양은 그대로이고, 줄바꿈은hp:lineBreak로 쓴다. 같은 이름의 글상자는 모두 채우고, 없는 이름은field-text-box-not-found로 거부한다.set_draw_text(name=)의 설명도 고쳤다(이름이 글상자를 필드로 만든다).doc.parts.remove_master_page(id)를 더한다. 어느 구역도 참조하지 않는 바탕쪽의 매니페스트 항목과 파트를 지우고, 저장된 적이 있어 패키지에 있는 파트 파일도 지운다. 구역이 참조하는 바탕쪽은master-page-in-use, 없는 id는master-page-not-found로 거부하고 아무것도 바꾸지 않는다.doc.parts.add_master_page()에section을 더한다. 주면 만든 바탕쪽을 그 구역에 바로 연결한다. 그 구역에 같은 쪽의 바탕쪽이 이미 있으면 파트를 만들기 전에master-page-pages-taken으로 거부하므로, 거부된 호출은 문서에 아무것도 남기지 않는다.add_rectangle()·add_ellipse()(doc.shapes와 문단)에original_size=(w, h)를 더한다.hp:orgSz를 그리는 크기와 따로 쓴다. 도형과 기하는 원래 크기로 만들고,curSz·sz는width×height,scaMatrix는 두 크기의 비율, 회전 중심은curSz의 절반이다. 한/글이 저장한 코퍼스 파일의 크기 바뀐 도형과 같은 배치다. 주지 않으면 출력은 전과 같다. 양의 정수 두 개가 아니면shape-original-size-invalid로 거부한다. 한/글에서 렌더는 확인하지 않았다.doc.styles.replace_font()에font_type(REP/TTF/HFT)을 더한다. 이 호출이 새로 선언하는 글꼴의type을 정한다. 이미 있던 글꼴은 바꾸지 않는다.hwpx.opc.relationships.ManifestItemRef에 매니페스트 항목의isEmbeded값을 담는is_embeded필드를 더한다.
바꿈
hwpx.experimental.latex_to_eqedit(실험)가\text{…}·\mathrm{…}·\textrm{…}·\mbox{…}를{rm "…" it}로 옮긴다(#347). 전에는 따옴표 글"…"로 옮겼는데, 한/글은 따옴표 글을 기울여 그린다.rm은 곧게 그리되 중괄호 밖으로 번지므로it으로 뒤 글자를 기울임으로 되돌린다(예:\mathrm{d}x→{rm "d" it} x).eqedit_to_latex는 이 묶음을\text{…}로 되읽는다. 따옴표 글은 전처럼\text{…}로 읽되, 낱말 하나가 EqEdit 예약어인 것(쓰는 쪽이 기호로 바뀌지 않게 감싼T _{"int"}같은 것)은 맨 글자로 읽는다. 그래서 쓰기 → 읽기 → 쓰기가 전처럼 고정점이다.
FormFit이 칸의 높이 예산으로 저장된 안쪽 높이를 그대로 쓴다. 한/글은 그 높이까지 줄을 넣어도 행을 키우지 않고, 줄 간격은 한/글과 같게 센다. 폭의 안전 여유(0.93)는 그대로다. 전에는 높이에도 0.93을 곱해, 칸에 들어가는 값을 줄이거나 거부했다(예: 안쪽 2047에 1939를 쓰는 두 줄 값).
FormFit이 화살표·도형 기호·그 밖의 기호(U+2190–21FF, U+25A0–25FF, U+2600–26FF: → ○ □ △ ☆ ☎ 등)를 글꼴 폭 표에 없는 글꼴에서 전각(1 em)으로 센다. 표에 있는 한국어 글꼴에서 이 기호들은 0.89~1 em이다. 전에는 문장 부호 폭으로 세어, 표에 없는 글꼴의 ○ 다섯 개를 한 줄로 예측했는데 한/글은 두 줄로 나눴다.
탭 정의를 한/글처럼 읽고 쓴다. 한/글이 저장한
hh:tabPr은 탭마다hp:switch로 감싸hp:case에 HWPUNIT 위치 (unit="HWPUNIT")를,hp:default에 그 두 배를 쓴다.doc.styles.tab_properties·tab_property()의tab_stops가 이제 모든 스위치의hp:case위치를 읽는다. 전에는 첫 스위치의hp:default만 읽어서, 탭이 여럿인 정의를 탭 하나로, 위치는 두 배로 보고했다. 스위치 없는hh:tabItem은 전처럼 그 값 그대로다(한/글도 HWPUNIT으로 읽는다).ensure_tab_definition()(apply_paragraph_format(tab_stops=…), 차례 만들기)도 탭마다 같은 스위치로 쓰고, 같은 정의를 찾을 때hp:case위치로 비교한다.FormFit이 저장 높이가 한 줄보다 낮은 칸(글에 따라 늘어나는 칸)에도 높이 예산을 쓴다. 한/글은 이런 행을 그 행에서 가장 높은 칸만큼 그린다. 그 높이에서 이 칸의 위아래 여백을 뺀 것이 예산이다. 칸의 높이는 한/글이 배치한 줄(줄 캐시)의 끝에 여백을 더한 것이고, 줄 캐시가 없으면 그 칸의 글을 잰 줄 수로 계산한다. 저장 높이가 더 크면 저장 높이다. 전에는 이런 칸을 너비로만 맞춰서, 행이 담는 것보다 줄이 많은 값을 쓰면 행이 커졌다. 이제 다른 행과 같이 정책을 따른다. 기본 정책은 그런 값을 거부하고,
allow_row_expand를 주면 전처럼 행이 커진다.FormFit이 줄이 이미 저장 높이를 넘친 행에서는 한/글이 그린 높이를 예산으로 쓴다. 한/글은 칸의 줄이 저장 높이를 넘으면 행을 그 줄만큼 그린다. 전에는 저장 높이로만 예산을 잡아, 원래 두 줄이던 칸에 두 줄 값을 넣어도 한 줄로 줄이거나 거부했다.
doc.fields.fill()이 값을 누름틀 시작이 든 런의 글자 모양으로 쓴다. 한/글은 값이 이미 든 누름틀을 채울 때도 옛 값의 글자 모양(파란 글자, 굵게 등)을 버리고 시작 런의 모양으로 쓴다. 전에는 안내문(자리표시)을 바꿀 때만 시작 런의 모양으로 바꾸고, 값이 든 누름틀에서는 옛 값 런의 모양을 지켰다. 그런 누름틀을 채운 결과의style_preserved는 이제False다.셀 글 쓰기(
table.set_cell_text(),cell.text = …,doc.fields.fill_cell())가 값을 받은 문단 밖의 빈 문단을 모두 지운다. 한/글은 칸을 채우면 값을 칸의 한 문단으로 만든다. 전에는 이번 쓰기로 비운 문단만 지우고 원래 비어 있던 문단(빈 줄)은 남겼다. 그래서 칸이 빈 줄만큼 높았고,cell.text가 쓴 값과 다르게 읽히기도 했다. 안쪽 표·개체를 담은 문단은 전처럼 남는다.add_paragraph(style=…)가 문단에 그 스타일의 문단 모양과 글자 모양을 쓴다. 한/글이 스타일을 적용할 때처럼 문단의paraPrIDRef와 첫 run의charPrIDRef가 스타일의 것이 된다. 전에는styleIDRef만 바꾸고 모양은 앞 문단에서 물려받아, 한/글에서 개요 스타일 문단이 본문처럼 보이거나 앞 제목의 번호가 붙었다.para_pr_id_ref·char_pr_id_ref나run_attributes의charPrIDRef를 함께 주면 그 값을 쓴다. 숫자 id를 쓰는style_id_ref는 전처럼 스타일 이름만 붙인다. 개요 스타일 문단에는 한/글이 개요 번호를 붙이므로 README 예시에서 글에 쓴 번호를 뺐다.FormFit이 로마 숫자(Ⅰ–Ⅻ, ⅰ–ⅻ)와 원 숫자·괄호 숫자(①–⑳, ⑴–⒇)를 글꼴에서 한/글이 배치하는 폭으로 센다. 함초롬바탕·함초롬돋움은 0.972 em, 맑은 고딕은 로마 숫자 0.952 em·원 숫자 1 em이다. 글꼴 폭 표에 없는 글꼴이나 글자도 전각(1 em)으로 본다. 전에는 평균 폭(0.62 em)으로 세어, 이런 글자가 든 칸을 한 줄로 예측했는데 한/글은 두 줄로 나눴다.
FormFit이 한글 한 자를 글자 모양의 한글 글꼴에서 한/글이 배치하는 폭으로 센다. 함초롬바탕·함초롬돋움은 0.972 em, 한컴 고딕은 0.932 em이고, 다른 글꼴(맑은 고딕·바탕· 돋움·굴림·궁서 등)은 전처럼 1 em이다. 이 세 글꼴의 셀과 누름틀에는 전보다 한 줄에 글자가 조금 더 들어간다고 본다.
TextStyle.hangul_advance(em, 기본 1.0)를 더했다. 글자별 폭을 모르는 글꼴의 한글 폭이다.
FormFit이 한글이 아닌 글자(숫자, 영문, 문장 부호, 자주 쓰는 기호)도 함초롬바탕·함초롬돋움·맑은 고딕에서는 한/글이 배치하는 글자별 폭으로 센다. 전에는 글자 종류별 평균(숫자 0.55 em, 문장 부호 0.42 em 등)을 썼다. 한글·영문·기타·기호 글꼴이 모두 같은 글자 모양일 때 쓰고, 다른 글꼴이나 표에 없는 글자는 전처럼 평균을 쓴다(
TextStyle.glyph_face,hwpx.form_fit.measure.glyph_advance_em).FormFit의 세로 예산(
SlotMetrics.height_lines)이 한/글이 칸을 늘리는 식을 따른다. n줄은 (n − 1) × 줄 간격 + 글자 크기를 차지하고 마지막 줄의 줄 간격은 들어가지 않는다. 전에는 n × 줄 간격으로 셌다. 줄 간격은 비율(PERCENT)뿐 아니라 고정(FIXED= 값), 여백만(BETWEEN_LINES= 크기값), 최소(
AT_LEAST= 크기와 값 중 큰 것)도 읽는다(SlotMetrics.line_spacing). 전에는 비율이 아니면 160%로 봤다. 비율 줄 간격에서 크기를 넘는 부분은 한/글처럼 1/1800인치(4 HWPUNIT) 단위로 센다(크기를 그 단위로 내린 em에 비율을 곱해 반올림, 10.5pt 160%는 630이 아니라 628).
FormFit이 글자 폭을 한/글처럼 1/1800인치(4 HWPUNIT) 단위로 센다. 글꼴의 설계 폭을 글자 크기에 맞춘 뒤 장평이 100%면 반올림하고 다른 장평이면 내린다. 자간은 그 폭에 비율을 곱해 0에서 먼 쪽으로 반올림해 더한다. 반각 공백은 em의 절반(내림)에 장평을 곱해 반올림한다. 전에는 10pt에서 본 비율을 크기에 곱하기만 해서, 10pt가 아닌 크기나 장평·자간이 있는 칸에서 줄 끝이 한/글과 어긋날 수 있었다.
글자별 폭을 아는 글꼴에 바탕·바탕체·돋움·돋움체·굴림·굴림체·궁서·궁서체·한컴 고딕을 더했다(모두 12개). 굵은 글자도 같은 폭을 쓴다.
TextStyle.hangul_face를 더했다. 셀·누름틀 슬롯은 첫 run 글자 모양의 한글 글꼴 이름으로 채우고, 표에 있는 글꼴이면hangul_advance대신 그 글꼴의 한글 폭을 쓴다.glyph_advance_em()은 글꼴의 설계 폭(em)을 돌려준다.
FormFit이 칸 줄 폭을 한/글처럼 센다. 1440 HWPUNIT 하한을 들여쓰기를 뺀 뒤 줄마다 적용하고(좁은 칸의 내어쓰기 줄도 1440), 칸보다 넓은 인라인 개체 다음 줄들은 칸 폭으로 센다(
SlotMetrics.line_width,SlotMetrics.min_line_width).FormFit이 칸 첫 줄의 인라인 개체(체크 상자·그림 등) 뒤에 글의 첫 글자도 들어가지 않으면, 한/글처럼 글이 다음 줄에서 시작한다고 센다. 전에는 첫 줄에 한 글자를 억지로 넣어 한 줄 적게 세거나, 개체 뒤 폭이 0이면 들어갈 수 없다고 보았다.
한 줄에 좁혀 넣는 칸(
lineWrap="SQUEEZE")에 글을 넣으면(cell.text = …,set_cell_text()), 값이 칸 줄 폭의 1.1배 이하일 때는 SQUEEZE를 그대로 둔다. 전에는 글이 바뀌면 늘 BREAK로 바꿔서, 줄 폭을 조금만 넘어도 줄이 바뀌고 행이 커져 쪽이 밀렸다. 한/글은 SQUEEZE 칸의 글을 한 줄에 두고 글자 사이만 좁히며, 1.1배까지는 읽히고 그보다 길면 글자가 닿거나 겹친다. 그래서 더 긴 값은 전처럼 BREAK로 바꾼다. 폭은 칸의 글자 모양으로 잰다.도형의 기본 선 굵기를 283(1 mm)에서 33(0.12 mm)으로 바꾼다. 한/글이 새 도형에 쓰는 굵기다.
doc.shapes.add_line()·add_rectangle()·add_ellipse()·add_arc()·add_polygon(), 문단의 같은 이름 메서드, 묶음 도형의 멤버가 이 기본값을 쓴다.line_width를 주지 않은 도형은 전보다 가늘게 그려진다. 옛 5.x 이름(doc.add_rectangle()등)은 시그니처를 그대로 두기로 한 호환 계층이라 전처럼 283이다.set_footnote_numbering()·set_endnote_numbering()의 설명과docs/known-traps.md에, 시작 번호(new_num)는 번호 방식이ON_SECTION일 때만 한/글에 보인다고 적는다.CONTINUOUS·ON_PAGE는 1부터 매긴다.FormFit이 칸을 채울 때 기본으로 행 높이를 지킨다. 값이 칸 높이가 담는 줄 수보다 조금 많으면 전에는 줄여 넣지 못한 경우 경고(
overflow_detected)만 남기고 값을 넣어 행이 늘었고, 그 뒤 쪽이 밀렸다. 이제 넘는 정도와 관계없이 글자를 줄여(min_font_pt까지) 칸 안에 넣고, 그래도 안 되면FitPolicy.overflow를 따른다. 기본fail은 채우지 않는다(doc.fields.fill()은field-fit-failed,set_cell_text(fit=...)는 칸을 그대로 두고ok=False인 결과). 행을 늘려도 되면FitPolicy(allow_row_expand=True), 경고만 받고 넣으려면FitPolicy(overflow="warn")을 준다. 칸 높이를 잴 수 없는 칸(병합·자동 늘어남)은 전처럼 폭만 본다.docs/known-traps.md와section.clear_body()설명에, 본문에서 지운 그림(clear_body(), 문단 삭제)의 이미지가 파일에 남는다고 적는다. 파일에서도 없애려면 저장 전에doc.media.remove_unused_images()를 부른다. 한/글은 저장할 때 아무것도 가리키지 않는 이미지를 지운다.
고침
쪽 수 추정(실험,
estimate_pages)이 줄 사이에서 나뉘는(CELL) 흐르는 표의 아래 캡션을 한/글처럼 둔다. 마지막 행이 글보다 높게 선언돼 있으면, 그 행과 캡션을 함께 다음 쪽으로 보내지 않고 행의 빈 높이를 캡션 위에서 자른다. 쪽 끝에서 빈 높이를 자르는 규칙과 같다. 남은 높이가 1282 이하면 버리고 캡션은 그 쪽에 남는다. 넘으면 남은 부분과 캡션이 다음 쪽으로 간다. 빈 높이로 모자라면 전처럼 행이 캡션과 함께 넘어간다.쪽 수 추정(실험,
estimate_pages)이 문단 위에서 단이나 문단의 왼쪽·오른쪽 끝을 기준으로 둔 어울림(SQUARE) 개체 가운데, 글이 갈 수 있는 양옆에 1440보다 좁은 자리만 남기는 것을 위아래 개체처럼 받는다. 전에는 단보다 좁으면 지원 밖(wrapped square leaving little room for text)이었다.글은 개체 아래(문단 윗선 + 개체 높이)로 간다. 표만 든 문단의 빈 줄은 표 윗선에 있고, 다음 문단은 표 아래에서 시작한다.
쪽 수 추정(실험,
estimate_pages)이 줄 캐시 없는 문단의 글머리표·번호 표지가 자기 글자 모양을 가질 때 그 크기를 세지 않던 것을 고친다. 한/글은 그런 표지를 문단 첫 줄에 놓인 그 크기의 글자로 두어, 표지가 글보다 크면 첫 줄이 표지만큼 높아지고 줄 간격도 그 크기로 센다(고정 간격이면 다음 줄은 그 간격만큼 아래). 표지가 글보다 작으면 그대로다. 본문 문단과 표 칸 문단 모두 그렇다(_head_size,_line_metrics의head).쪽 수 추정(실험,
estimate_pages)이 줄 캐시 없는 표 칸 문단의 글자 크기를 문단의 첫 run에서 읽던 것을 고친다. 칸 문단이 다른 크기의 빈 run으로 시작하면(빈 문단에 run을 더해 쓴 문단처럼) 그 빈 run의 크기로 줄 높이와 간격을 셌다. 이제 본문 문단처럼 글이 든 run의 크기를 쓰고, 빈 run은 자리를 차지하지 않는다.수식 상자 측정(
measure_equation,estimate_equation_size)이 기호를 뜻하는 수식 명령어를 그 철자 글자들로 재던 것을 고친다.lbrace·rbrace(대소문자 무관)는 중괄호 한 글자로,LINE·vert는|,VERT는 겹세로줄로 잰다.CHOOSE·BINOM처럼 대문자로 쓴 이항계수도choose·binom처럼 두 줄로 잰다. 전에는lbrace x | x > 0 rbrace가 한/글이 저장한 상자보다 2.5배 넓었다. 다음 폭도 맞춘다:prime과'는 좁은 표시,DEG는 도 기호 하나,theta등 그리스 글자는 글자별 폭,root의 짧은 지수는 근호 왼쪽 홈에 들어가 폭을 더하지 않고, 근호와 행렬 괄호는 더 좁다.atop(과 같은 모양의choose·binom)은 가로줄이 없어over보다 낮고,LEFT/RIGHT괄호는 높이를 더하지 않으며, 그 묶음(과 분수)에 붙은 첨자는 묶음의 위아래를 따라 밖으로 나간다. 따옴표 안의 대문자·숫자는 글자 종류대로 잰다.수식 측정 모듈 설명에서 “한컴은 문서를 열 때 수식을 다시 재지 않는다”는 문장을 관찰한 대로 고친다. macOS 한/글은 저장할 때 저장된 크기와 상관없이 수식을 다시 배치해
<hp:sz>를 다시 쓰는 것이 관찰되었다. 파일을 읽어 쪽을 배치하는 다른 프로그램에는 저장된 상자가 그대로 쓰인다.FormFit(과 줄 캐시 없는 문단의 쪽 수 추정)이 글머리표·문단 번호·개요 번호가 붙은 문단의 줄을 표지 없이 나누던 것을 고친다. 이제 표지 폭과 너비 조정(
widthAdjust), 본문과의 간격(textOffset: 표지 글자 크기의 %나 HWPUNIT)만큼 첫 줄이 좁아진다. 자동 내어 쓰기(autoIndent)면 둘째 줄부터도 좁아지고, 문단의 들여쓰기는 둘째 줄부터만 든다(내어쓰기면 더 좁고, 첫 줄 들여쓰기면 그만큼 넓다). 표지 폭은 실제 너비 사용 (useInstWidth)이면 표지 글자들의 폭이고, 아니면 ASCII 글자는 반 em, 다른 글자는 1 em이다(번호는 그 형식의 글자 하나로 센다). 쪽 수 추정은 번호 문단마다 한/글이 그리는 번호를 읽는 순서대로 세어 쓰고, FormFit은 그 수준의 첫 번호를 쓴다(TextStyle.head·head_hangs,indented_widths(),label_room(),paragraph_label()).쪽 수 추정(실험,
estimate_pages)이 줄 캐시 없는 문단 끝의 빈 run을 세지 않던 것을 고친다. 한/글은 문단 끝을 마지막 run의 글자 모양으로 두어, 빈 run이 문단을 끝내면 마지막 줄이 그 글자 크기만큼 높아진다(빈칸만 든 줄도, 글이 든 줄도). 견주는 것은 마지막 줄의 글자다. 앞 줄에 더 큰 글자가 있어도 그렇다. 글 앞의 빈 run은 전처럼 자리를 차지하지 않는다.FormFit이 묶음 빈칸(U+00A0,
hp:nbSpace)과 고정폭 빈칸(U+3000,hp:fwSpace)을 한/글과 다르게 세던 것을 고친다. 묶음 빈칸은 반각 빈칸만큼 넓고, 줄 끝에 걸리지 않으며 그 자리에서 줄이 나뉘지 않는다(앞뒤 낱말이 한 낱말이 된다). 전에는 줄 끝에 걸리는 빈칸으로 보았다. 고정폭 빈칸은 장평·자간·최소 공백과 상관없이 글자 크기의 1/4(1/1800인치 단위로 버림)이고, 빈칸처럼 줄 끝에 걸리며 그 뒤에서 줄이 나뉜다. 전에는 온폭 글자로 보았다. 줄 캐시 없는 문단의 쪽 수 추정은 둘을 글에서 빼고 줄을 나누었는데, 이제 글자로 센다.쪽 수 추정(실험,
estimate_pages)이 나누지 않게 한 흐르는 표(hp:tbl@pageBreak="NONE")의 높이를 세지 않던 것을 고친다(다음 쪽으로 넘어갈 때 쪽 수만 하나 더했다). 한/글은 그런 표의 행이 앵커 줄 아래에 다 들어가면 여느 흐르는 표처럼 그 자리를 차지하고, 다음 문단은 표 끝 아래에 온다. 다 들어가지 않으면 표만 다음 쪽 맨 위로 옮기고, 뒤 글은 앵커 줄 아래에 이어지며, 다음 쪽의 줄은 표 아래에서 시작한다(쪽보다 큰 표도 다음 쪽으로 간다). 글이 든 문단 맨 앞에 둔 표는 그 아래 줄까지 들어가지 않으면 문단째 다음 쪽으로 간다.FormFit(과 줄 캐시 없는 문단의 쪽 수 추정)이 최소 공백(
condense)을 한/글과 다르게 세던 것을 고친다. 빈칸은 자간을 뺀 빈칸 폭의 그 비율만큼 줄어든다(전에는 자간을 넣은 폭의 비율). 빈칸이 줄어드는 것은 여백을 넘는 낱말을 넣을 때뿐이다. 줄의 글이 자간을 넣어 여백에 닿으면 그 뒤 빈칸은 모두 줄 끝에 걸리고 다음 낱말은 다음 줄에서 시작한다. 전에는 빈칸을 줄여 다음 낱말도 넣었다.FormFit이 모든 글자에 한글의 장평·자간을 쓰던 것을 고친다. 글자 모양이 영문·기호·한자에 따로 준 값이 있으면 그 글자에 쓴다(
glyph_script(),TextStyle.scripts). 영문은 U+024F까지(숫자, 문장 부호, 가운뎃점, °, ×)와 ‘’“”다. 기호는 일반 문장 부호(줄표, 줄임표, ‰, ※), 글자꼴 기호(℃), 숫자 꼴(Ⅰ, ①), 화살표, 도형, CJK 기호와 괄호, 전각 문자다. 한자는 CJK 한자다. 반각 빈칸은 한글 장평에 영문 자간을 쓴다.쪽 수 추정(실험,
estimate_pages)이 줄 캐시 없는 표 칸 문단의 줄을 칸 안쪽 폭 그대로, 첫 run의 글자 크기와 모양 하나로 나누던 것을 고친다. 이제 본문 문단처럼 문단 여백과 첫 줄 들여쓰기(내어쓰기면 둘째 줄부터)를 빼고, 글자 크기나 모양이 섞이면 글자마다 제 크기와 모양으로 나누며, 줄마다 그 줄의 가장 큰 글자만큼 높다.묶음(
hp:container)의resize()가 원래 크기(orgSz)를 새 크기로 덮고 배율 행렬과 구성원은 그대로 두던 것을 고친다. 한/글은 묶음을sz크기로 그리고, 크기를 바꾼 묶음을 저장할 때orgSz를 두고sz·curSz를 새 크기로(바뀌지 않은 축의curSz는 0), 묶음의scaMatrix를 새 크기/원래 크기로 쓴다. 구성원마다(묶음 안 묶음의 구성원도) 첫scaMatrix에 같은 배율과 오프셋 × (배율 − 1)만큼의 이동을,curSz에orgSz× 들어 있는 묶음들의 배율(버림)을 쓴다. 이제resize()가 그 모양을 쓴다. 전에도 그림은 새 크기였지만, 한/글이 다시 저장하면curSz가 그린 크기의 두 배로 기록됐다.메모 API(
doc.notes.memos,doc.notes.remove_memo())가 MEMO 필드에만 든 메모를 보지 못하던 것을 고친다. 한/글은 메모를hp:memogroup없이 MEMO 필드(hp:fieldBegin type="MEMO")의hp:subList에 두며, HWP를 HWPX로 읽은 문서도 그렇다. 이제notes.memos가 그런 메모도 문서 순서대로 돌려준다(HwpxOxmlFieldMemo: id·번호·작성자는 필드의 매개변수, 글은hp:subList). 메모 그룹과 필드에 함께 있는 메모(python-hwpx가 앵커를 건 메모)는 한 번만 센다.remove_memo()는 필드의 두 제어와 메모 글을 지우고 필드가 걸친 글은 남긴다. python-hwpx가 앵커를 건 메모를 지우면 그 MEMO 필드도 함께 지운다(전에는 필드가 남아 한/글에 메모가 그대로 보였다).쪽 수 추정(실험,
estimate_pages)이 칸 줄 사이에서 나누는 흐르는 표 행을 한 칸(가장 높은 칸)의 줄로만 나누던 것을 고친다. 한/글은 모든 칸의 첫 줄이 쪽 끝 바로 위까지 들어갈 때만 행을 나누고, 그렇지 않으면 행째 다음 쪽으로 보낸다.쪽 수 추정(실험,
estimate_pages)이 글자처럼 둔 개체가 든 줄의 줄 간격을 개체가 든 런의 글자 크기 없이 세던 것을 고친다. 한/글은 그 줄의 가장 큰 글자로 줄 간격을 세며, 개체는 그 런의 글자 크기로 센다. 개체 곁에 공백만 있으면(또는 아무것도 없으면) 그 문단의 어느 런이든 가장 큰 글자 크기로 센다(전에는 가장 작은 글자).쪽 수 추정(실험,
estimate_pages)이 표의 캡션을 세지 않던 것을 고친다. 한/글은 표 위나 아래의 캡션에 그 줄과 간격만큼 자리를 둔다(글자처럼 둔 표는 그 줄이 그만큼 높다). 아래 캡션이 표의 마지막 행과 함께 쪽 끝 바로 위까지 들어가지 않으면 그 행과 함께 다음 쪽으로 간다. 표 곁의 캡션은 자리를 두지 않는다.쪽 수 추정(실험,
estimate_pages)이 문단에 혼자 있는 흐르는 표의 오프셋(문단 위에서 아래로 둔 거리)을 무시하던 것을 고친다. 한/글은 그 표를 문단 위(문단 위 간격보다 위)에서 오프셋만큼 아래에서 시작한다(위로 둔 표는 문단 위에서). 다음 문단은 표 끝과, 앵커 문단의 줄 뒤에 제 문단 위 간격을 더한 자리 가운데 더 아래에 놓인다. 오프셋을 두고 흐르는 표 뒤에 그런 문단이 오면, 그 줄은 앞 표가 끝난 자리로 내려가고 표는 거기서 흐른다(전에는 지원 밖이었다).쪽 수 추정(실험,
estimate_pages)이 선언 높이가 글보다 큰 흐르는 표 행을 쪽 끝에서 자를 때, 들어가지 않은 글 줄을 잃던 것을 고친다. 남은 선언 높이가 1282 이하이면 버렸지만, 한/글은 들어가지 않은 줄을 다음 쪽으로 보내고 남은 높이를 그 줄과 칸 여백만큼은 둔다. 같은 높이의 칸이 여럿이면 줄이 가장 많은 칸으로 행을 나눈다(전에는 첫 칸).쪽 수 추정(실험,
estimate_pages)이 제목 줄을 반복하는 흐르는 표에서 행의 제목 여부를 가장 높은 칸 하나로만 보던 것을 고친다. 그 칸이 제목 칸이 아니면 행을 다음 쪽에 되풀이하지 않았지만, 한/글은 제목 칸이 하나라도 있는 행을 되풀이한다.쪽 수 추정(실험,
estimate_pages)이 크기를 자동으로 조절하지 않게 한 흐르는 표(hp:tbl@noAdjust="1")의 쪽 끝을 여느 표처럼 보던 것을 고친다. 한/글은 여느 표의 행과 칸 줄을 본문 끝 101 위까지만 두지만, 그런 표는 2 위까지 둔다(끝이 1 위면 다음 쪽). 선언 높이가 글보다 큰 행도 거기서 자른다.쪽 수 추정(실험,
estimate_pages)이 음수인 바깥 여백(파일에는 부호 없는 32비트 수로 남는다)을 그 큰 수 그대로 세던 것을 고친다. 한/글은 그런 여백을 없는 것으로 배치하고, 저장하면 0으로 쓴다.쪽 수 추정(실험,
estimate_pages)이 여러 쪽에 걸친 흐르는 표의 바깥 여백을 쪽 끝과 다음 쪽에서 따르지 않던 것을 고친다. 한/글은 표의 행과 칸 줄을 본문 끝에서 바깥 아래 여백을 뺀 자리의 바로 위까지만 두고, 다음 쪽에서는 표를 바깥 위 여백만큼 내려 다시 시작한다. 전에는 두 여백을 표의 처음과 끝에서만 셌다(여백이 0인 표는 그대로).쪽 수 추정(실험,
estimate_pages)이 흐르는 표(글자처럼 두지 않은 표)를 품은 문단의 쪽 나누기를 무시하던 것을 고친다. 그 표는 앞 글이나 앞 표 바로 뒤에 놓였지만, 한/글은 새 쪽에서 시작한다. 앞 표가 다음 쪽까지 이어지면 그 표가 끝난 쪽의 다음 쪽이다.편집기 안전 검사가 한/글이 여는 중에 멈추는 바탕쪽을 통과시키던 것을 고친다. 이제
hp:subList가 없는 바탕쪽(masterPage)과 바탕쪽 파트의 문단 없는hp:subList는 오류다(문단 없는hp:subList검사는 전에는 구역 파트만 보았다).doc.parts.add_master_page(paragraphs=[])는 문단 없는 바탕쪽을 쓰던 것을 고쳐, 이제 빈 문단 하나를 쓴다.문단 모양 조회(
paragraph_property())가 부를 때마다 헤더의 문단 모양을 모두 다시 읽던 것을 고친다. 이제 찾는 문단 모양 하나만 읽고, 찾는 규칙(id, 숫자로 고친 id)은paragraph_properties와 같다. FormFit 칸 채우기처럼 칸이나 문단마다 문단 모양을 찾는 일이, 문단 모양이 많은 문서에서 크게 빨라진다.remove_section()이 지운 구역의 파트(Contents/sectionN.xml)를 패키지에 남기던 것을 고친다. 매니페스트는 그 파트를 더 가리키지 않지만 저장한 파일 안에 지운 구역의 글이 그대로 남았다(한/글은 읽지 않고, 다시 저장하면 없어진다). 이제 구역을 지우면 그 파트도 지운다.편집기 안전 검사가 한컴이 열지 못하거나 여는 중에 멈추는 찾아보기 표시·도형 매개변수 묶음을 통과시키던 것을 고친다. 이제 다음은 오류다:
hp:firstKey가 없는hp:indexmark,hp:parameterset안의name없는hp:listParam·hp:unsignedintegerParam,cnt로 매개변수가 있다고 하면서 하나도 없는hp:parameterset·hp:listParam.cnt="0"인 빈 묶음과, 매개변수가 있는데cnt만 다른 묶음은 한컴이 열므로 그대로 둔다.doc.page.set_header()·set_footer()·set_page_number()가 구역 첫 문단의 첫 run에서 시작하는 필드(누름틀 등) 뒤에 머리말·꼬리말 컨트롤을 넣던 것을 고친다. 그 자리는 필드 범위 안이라 한/글이 저장하면서 머리말·꼬리말을 버렸다. 이제 필드가 시작하는 컨트롤 앞에 넣는다. 머리말·꼬리말 글을 고치며hp:ctrl사본을 새로 만들 때도 같다.편집기 안전 검사(
validate_package·validate_editor_open_safety)가 한컴이 열지 못하거나(거부) 멈추거나 배치를 끝내지 못하는 문서를 통과시키던 것을 고친다. 이제 다음은 오류이고, 공개 저장 경로는 이런 문서를 쓰지 않는다.패키지:
rootfile@media-type,opf:item의id·href·media-type,opf:itemref@idref,opf:meta@name이 없거나opf:manifest에 항목이 없음hh:head@secCnt없음(예전에는 경고),HCFVersion@tagetApplication없음표:
hp:tbl의rowCnt·colCnt,hp:tr/hp:tc,hp:cellAddr의colAddr·rowAddr,hp:cellSpan의colSpan·rowSpan이 없음hp:secPr에hp:startNum·hp:visibility없음,hp:lineseg@textpos없음필드:
hp:fieldBegin@id,hp:fieldEnd@beginIDRef가 없거나, 없는fieldBegin을 가리키는fieldEnd가 자기hp:run의 맨 앞에 있거나, 필드 종류와fieldid가 둘 다 한컴이 모르는 값임. run 안에서 글·빈hp:t·다른 컨트롤 뒤에 오는 그런fieldEnd는 한컴이 버리고 문서를 연다도형: 필수 하위 요소가 없는 도형과 빈
hp:ctrl(예전에는 경고),hp:renderingInfo행렬이 없는 도형, 모서리 점(hc:pt0~pt3)이 없는 사각형,hc:img가 없는 그림구역 설정의
hp:parameterset안hp:booleanParam, 패키지에 없는 바탕쪽을 가리키는hp:masterPage,hp:subList가 없는 머리말·꼬리말 컨트롤, 문단 없는hp:subList머리말·구역 항목의
opf:item@href가 패키지 루트에서 본 파트 이름이 아님 (content.hpf폴더 기준 상대 경로,/로 시작하는 경로). 한컴은 href를 패키지 루트에서 그대로 찾는다. 그림 항목이 그러면 경고다(한컴은 그 그림을 빼고 연다)구역 항목의
opf:item@id가section0부터 번호가 이어지지 않음. 한컴은 구역을 이 id의 번호 순서로 찾고(spine이나 파일 이름이 아니라) 하나라도 없으면 열지 않는다. 번호는 이어지지만 spine 순서와 다르면 경고다(한컴은 id 순서로 읽는다). id는 spine이 가리키는 것을 보고, 같은 구역 파트를 가리키는 다른 항목은 세지 않는다.repair_repack은 구역 id를 spine 순서대로 다시 매긴다
add_heading(level=8~10)과apply_paragraph_format(outline_level=8~10)이 문단 모양의 개요를 한/글과 다른 모양으로 쓰던 것을 고친다. 개요 8~10 수준은 2016 문단 이름공간에만 있어서, 한/글은hp:switch의hp:case에 개요를 두고hp:default에NONE을 둔다(새 문서의 개요 8~10 스타일도 이렇다). 전에는 이hp:switch를 비우고 개요를 문단 모양에 바로 썼다. 이제 한/글과 같은 모양으로 쓰고, 새 문서에서는 개요 8~10 스타일의 문단 모양을 그대로 쓴다. 문단 모양을 읽을 때도hp:switch안의 개요를 읽는다(paragraph_property().heading, 목록 글자 내보내기).글자 겹치기(
add_composed_character())와 덧말(add_dutmal())을 한/글이 쓰는 모양으로 쓴다. 글자 겹치기는 늘hp:charPr열 칸을 쓰고(쓰지 않는 칸은4294967295), 주지 않은 속성은 한/글이 새 글자 겹치기에 주는 값 (circleType="SHAPE_CIRCLE",charSz="-4",composeType="SPREAD")으로 쓴다. 덧말은 스타일을 주지 않으면styleIDRef="0"을 쓴다. 전에는 준 칸만 쓰고 속성을 뺐으며, 한/글은 저장할 때 이 값들을 채웠다.새 도형(
add_rectangle()·add_ellipse()·add_line()·add_arc()·add_polygon()·add_container())에textWrap·textFlow를 쓴다. 한/글은 모든 도형에 이 둘을 쓰고, 없으면SQUARE·BOTH_SIDES로 읽는다. 그래서 새 도형은textWrap="SQUARE" textFlow="BOTH_SIDES"이고, 묶음(hp:container) 안 도형은 한/글처럼TOP_AND_BOTTOM이다. 채운 도형의hc:winBrush에도 한/글처럼alpha="0"을 쓴다. 한/글에서 보이는 모양은 그대로다.FormFit이 공백이 이어진 글을 한/글처럼 줄 나눈다. 낱말 바로 뒤의 공백은 전처럼 줄 끝에 매달리지만, 그 뒤로 이어지는 공백이 줄 끝이나 그 너머에서 시작하면 다음 줄로 간다. 최소 공백(
condense)은 줄의 첫 글자 뒤에 있는 공백만 줄이고, 줄 앞의 공백은 줄이지 않는다. 전에는 이어진 공백을 모두 줄 끝에 매달고 줄 앞 공백까지 줄여서, 공백으로 칸을 채운 값을 한 줄로 예측했는데 한/글은 두 줄로 나눴다.누름틀 안에 다른 누름틀이 있을 때 바깥 누름틀을
doc.fields.fill()로 채우면 안쪽 누름틀의 시작과 끝을 지운다. 한/글은 바깥 누름틀의 내용 전체를 값으로 바꾸므로 안쪽 누름틀도 함께 없어진다. 전에는 안쪽 누름틀의 시작과 끝이 빈 채로 남아 필드 목록에 그대로 있었다. 안쪽 누름틀만 채우면 바깥 누름틀은 그대로다.필드를 쓸 때
fieldid에 그 필드 종류의 제어 id를 쓴다. 한/글은 종류마다 모든 필드에 같은fieldid를 쓴다. 제어 id 네 글자를 수로 읽은 값이다(누름틀%clk627272811, 날짜%dte, 경로%pat, 교정 부호%sig, 메일 머지%mmg, 차례%toc, 하이퍼링크%hlk, 상호 참조%xrf). 전에는 누름틀·날짜·경로·교정 부호·메일 머지· 차례·상호 참조 필드에 임의 수를 썼고, 차례 도구가 쓴 필드 끝에는fieldid가 없었다. 문서 합치기 (insert_document()·append_document())도 가져온 필드의fieldid를 임의 수 대신 종류의 제어 id로 쓴다.누름틀의 끝을 찾을 때
beginIDRef가 있는 끝은 그 id의 누름틀 끝으로만 본다. 같은 종류의 필드는fieldid를 함께 쓰기 때문에, 한/글이 저장한 문서에서 끝이 없는 누름틀이 다음 누름틀의 끝을 제 끝으로 잡았다. 그러면 채울 때 그 사이 내용을 지웠다. 이제 이런 누름틀은field-end-missing으로 거부한다.FormFit이 문단의 왼쪽·오른쪽 여백을 줄 폭에서 빼고, 문단 위 간격을 칸의 높이 예산에서 뺀다. 한/글은 줄을 문단 왼쪽 여백에서 시작하고, 줄 폭은 칸 안쪽에서 두 여백을 뺀 값이다. 첫 줄은 위 간격 아래에서 시작하고, 칸 마지막 문단의 아래 간격은 자리를 차지하지 않는다. 전에는 두 여백과 위 간격을 무시해서, 여백이 있는 칸에서 줄 수를 적게 세고 칸에 들어가는 줄 수를 많게 셌다.
메모를 여럿 달면 모두
Number1로 쓰던 것을 고친다. 한/글은 번호가 같은 메모를 한 메모로 보아, 문서를 다시 저장할 때 둘째 메모부터 메모 범위의 끝(hp:fieldEnd)을 지웠다. 이제doc.notes.add_memo()·doc.notes.attach()에number를 주지 않으면 메모는 문서의 다음 번호(zorder와 같은 값)를 쓴다. 메모 필드도 한/글이 쓰는 모양으로 쓴다. 필드 id는 숫자이고,fieldid는 메모 필드의 종류 코드(%%me)다.name="",editable="1",dirty="0"을 쓰고, 메모 글subList에는 한/글과 같은 속성을 쓴다.doc.fields.fill()이 내용이 여러 문단에 걸친 누름틀을 채우면 값만 시작 뒤에 넣고 걸친 문단의 원래 글을 그대로 두던 것을 고친다. 누름틀 끝을 시작과 같은 문단에서만 찾았기 때문이다. 이제 끝을 뒤 문단에서도 찾고, 한/글처럼 걸친 문단(그 안의 표 포함)을 지운 뒤 값과 끝을 시작 문단에 둔다. 끝 문단에서 끝 뒤에 있던 글은 시작 문단으로 합치고, 시작 문단의 문단 모양은 그대로다. 이런 누름틀의FormField.value는 걸친 문단의 글을 한 줄에 한 문단씩 읽는다.끝(
hp:fieldEnd)이 없는 누름틀을doc.fields.fill()로 채우면HwpxValueError(code="field-end-missing")를 낸다. 바꿀 내용이 없기 때문이다(한/글도 이런 누름틀은 채우지 않는다). 전에는 값을 시작 뒤에 넣었다.안내문을
Command문자열(Clickhere:set:…:Direction:wstring:<길이>:<안내문> …) 안에만 둔 누름틀을 자리표시로 알아보지 못하던 것을 고친다. 그래서doc.fields.fill()이 값을 안내문의 글자 모양(보통 빨강)으로 남겼다. 이제 안내문을 그 문자열에서도 읽고(FormField.prompt,is_placeholder), 값은 누름틀의 글자 모양으로 쓴다. 누름틀의 매개변수는 바꾸지 않는다.doc.parts.add_master_page(section=...)가 내는master-page-pages-taken을 오류 코드 목록 (hwpx.errors.ERROR_CODES,docs/error-codes.md)에 올린다. 전에는 목록에 없었다.doc.styles.replace_font()가 dst_face를 새로 선언하는 블록에 늘type="TTF"를 쓰던 것을 고친다. 다른 블록이 같은 글꼴을HFT로 선언해도 그랬다. 이제 다른 블록에 dst_face 선언이 있으면 그hh:font를 id만 바꿔 복사한다. 그래서type·isEmbedded·hh:typeInfo가 그 선언과 같다. 어느 블록에도 없을 때만 전처럼TTF로 선언한다.check_id_integrity()가 동영상이 가리키는 항목(포스터imageIDRef, 파일fileIDRef)을 고아 BinData로 보고해ok=False를 내던 것을 고친다.binaryItemIDRef만 참조로 셌기 때문이다. 이제imageIDRef·fileIDRef·soundIDRef도 참조로 세고, 패키지 밖 파일을 잇는 항목(isEmbeded="0"이고 href가BinData/밖)은 고아 목록에 넣지 않는다. 이진 항목 판정·이름 비교·참조 속성은doc.media.remove_image()·doc.media.images·doc.validate()와 같은 규칙(hwpx.opc.relationships)을 쓴다. 참조 무결성 게이트(require_reference_integrity)가 고아 항목 때문에 실패하면 메시지에 그 항목을 적는다. 전에는 메시지가ID/reference integrity failed:로 끝났다.doc.media.images가 한/글이BinData/에 두는 OLE 항목을 빼던 것을 고친다.isEmbeded="0"이면 모두 바깥 파일로 봤기 때문이다. 이제 href가BinData/밖인isEmbeded="0"항목(패키지 밖 파일)만 뺀다.표 칸의 안쪽 여백을 읽는 곳마다 규칙이 달라 같은 칸에 서로 다른 값을 쓰던 것을 고친다. 이제 양식 맞춤(
resolve_slot_metrics), 칸 안 중첩 표의 기본 너비, 새 표의 쪽 넘김 판단, 레이아웃 미리보기의 칸 안쪽 여백, 템플릿 분석(analyze_template)의CellSummary.margin, 바이트 보존 칸 채우기(fill_cells)의 글자 줄이기가 모두cell.margins와 같은 규칙을 따른다. 칸의hasMargin이 켜져 있을 때만 칸의hp:cellMargin을 쓰고, 꺼져 있거나 없으면 표의hp:inMargin을 쓴다. 전에는 양식 맞춤이hasMargin이 없는 칸에 칸 여백을 썼고, 나머지는hasMargin을 보지 않고 칸 여백만 읽었다. 한/글이 저장한 실문서의 줄 너비(lineseg@horzsize)가 이 규칙과 맞는다.hasMargin="0"이고 두 여백이 다른 칸이 있는 문서는 중첩 표 기본 너비, 미리보기 안쪽 여백, 템플릿 분석 값,fill_cells의 글자 줄이기 판단이 바뀐다. 양식 맞춤 결과는 테스트 실문서에서 바뀌지 않는다.tests/data/model_surface.json의internalCount(stable 목록 밖 공개 멤버 수)를 확인하는 테스트가 없어 19개 클래스 중 12개 값이 실제와 어긋나 있던 것을 고친다. 값을 실제에 맞추고(합 82 → 154), 표면 테스트가 모든 클래스의 값과docs/stable-api.md의 수를 확인한다. 공개 API는 바뀌지 않는다.5.0에서
python-hwpx-automation으로 옮긴 모듈(hwpx.builder,hwpx.authoring,hwpx.exam,hwpx.tools.pii등 21개)을 import하면No module named ...만 나오던 것을 고친다. 이제ModuleNotFoundError메시지가 새 모듈 경로(예:hwpx_automation.office.authoring.builder),pip install python-hwpx-automation, 이전 가이드 주소를 알려 준다. 배포본에서 뺀 저장소 전용 패키지(hwpx.benchmark,hwpx.conformance,hwpx.tools.fuzz)는 소스 체크아웃에서 쓰라고 알려 준다. core는 여전히 companion 패키지를 import하지 않고, 옮긴 경로를 파일로 되살리지 않는다. 모듈별 대체표는docs/migration-5.0.md의 “Module paths”에 있고 메시지와 같은 표에서 나온다.validate_package()(hwpx-validate-package)가 패키지 밖 파일을 잇는 매니페스트 항목(isEmbeded="0"이고 href가BinData/밖, 예: 연결한 동영상)을 파트가 없다는 오류로 보고해ok=False를 내던 것을 고친다. 이 때문에 참조 무결성 게이트(require_reference_integrity)로 저장하면OPC/package validation failed로 실패했다. 이제doc.validate()·check_id_integrity()·doc.media.images와 같은 규칙(is_linked_file)으로 이런 항목을 건너뛴다.BinData/에 파일을 두는 OLE 항목은isEmbeded="0"이어도 계속 검사한다.doc.validate()와validate_document()(hwpx-validate)가 같은 패키지에 다른 답을 하던 것을 고친다. 매니페스트 불일치 경고(파트가 없는 매니페스트 항목, 매니페스트 항목이 없는BinData/파트)를doc.validate()만 냈기 때문이다. 이제 이 검사를validate_document()가 하고doc.validate()는 그 결과를 그대로 돌려준다. 파트가 없는 매니페스트 항목을 오류로 보는 것은 여전히 패키지 검사(validate_package())다.doc.validate()의 docstring에 무엇을 검사하는지 적는다.lint_layout()이 글을 채워 쪽 본문보다 높아진 글자처럼 취급한 표를 알리지 못하던 것을 고친다. 행 높이를 칸에 저장된 높이(hp:cellSz)로만 셌기 때문이다. 글을 채워도 저장된 높이는 그대로이고, 한/글은 칸의 줄에 맞춰 행을 늘려 그린다. 이제 칸마다 문단과 줄바꿈(hp:lineBreak, 글 안의 줄바꿈 문자) 수만큼 줄을 센다. n줄은 (n − 1) × 줄 간격 + 글자 크기를 차지하고, 여기에 칸 위아래 여백을 더한 값을 행 높이의 하한으로 쓴다. 글자 모양이 하나인 문단은 긴 글이 칸 폭에서 접혀 생긴 줄도 센다. 줄 나눔은 FormFit과 같고, 글자마다 측정 오차만큼 좁게 재므로 한/글이 그리는 줄보다 많이 세지 않는다. 탭·개체가 있거나 글자 모양이 여럿인 문단은 문단과 줄바꿈 수만 센다.문단이나 표 칸에 이미 가진 글을 다시 넣으면(
paragraph.text = paragraph.text,cell.text = cell.text) 줄 배치 캐시(hp:linesegarray)를 지우던 것을 고친다. 한/글은 캐시가 없는 문단을 다시 배치하는데, 다른 판의 한/글이 만든 캐시와 결과가 달라 글이 바뀌지 않았는데도 쪽 수가 달라질 수 있었다. 여러 런 문단은 런까지 첫 글자 모양으로 합쳐졌고, 여러 문단 칸은 한 문단이 됐다. 이제 같은 글이면 아무것도 바꾸지 않는다(set_text_preserving_runs와 같다). 칸의set_text(..., preserve_format=False)는 같은 글이어도 전처럼 글자 모양을 되돌린다.lint_layout()이pageBreak="TABLE"표를 검사하지 않던 것을 고친다. 한/글은 이 표를 행 사이에서만 나눈다. 그래서 쪽 본문보다 높은 행은 한 쪽에 그리고, 종이 아래 끝을 넘는 줄은 그리지 않는다. 이제 이런 행을TABLE_TALLER_THAN_PAGE로 알린다(detail["row"]는 행 번호). 행 안에서도 나누는pageBreak="CELL"표는 전처럼 검사하지 않는다.각주·미주의 인스턴스 ID를
instid속성으로 쓰던 것을 고친다. 스키마(NoteType)와 한/글이 쓰는 이름은instId이고, 소문자instid는 그림·도형 같은 개체의 철자다. 한/글은 이런 파일을 저장하며instid를 버리고instId를 새로 매겼다. HWP로 저장하면 각주·미주의 인스턴스 ID가 0이 됐다. 이제add_footnote()·add_endnote()는instId를 쓰고, 읽기는instId를 먼저 보고 전에 쓴instid도 읽는다. 문단 복제는 어느 철자든 값만 새로 매긴다.doc.fields.fill()이 값의 줄바꿈 문자(CRLF·LF·CR)를 글 안에 그대로 쓰던 것을 고친다. 한/글은 이 문자를 열 때 같은 문단 안 줄바꿈(hp:lineBreak)으로 바꿔 보여 줬다. 이제 줄바꿈마다hp:lineBreak를 쓴다. 누름틀 값을 읽을 때도hp:lineBreak를 줄바꿈 문자로,hp:tab을 탭으로 읽는다(전에는 빠졌다). 채울 때 옛 값에 있던 줄바꿈·탭 요소가 새 값 뒤에 남던 것도 함께 고친다.이름이 빈 누름틀(
name="")을doc.fields.all이id를 이름으로 삼아 보여 주던 것을 고친다. 한/글은 이런 누름틀을 이름 없는 필드로 보고 필드 목록과 이름으로 채우기에서 뺀다. 이제name속성이 있으면 빈 값이라도 그 값을 이름으로 쓴다. 이름 없는 누름틀은doc.fields.all·list_form_fields()에 빈 이름으로 나오고(field_id로 구별한다),fill(name=)으로는 찾지 않는다(안내문으로도 찾지 않는다).fill(field_index=)·fill(field_id=)로는 전처럼 채울 수 있다.안에 표가 든 칸에 글을 넣으면(
cell.text = …,set_cell_text()) 안쪽 표의 칸 글까지 모두 지워 빈 표가 남던 것을 고친다. 이제 칸 자신의 문단 글만 바꾸고, 칸 안의 표·개체는 글을 그대로 둔다. 누름틀 칸 채우기 (doc.fields.fill_cell())도 같은 방식으로 쓴다. 그래서fill_cell()도 이번 쓰기로 비워진 칸 문단을 지운다(전에는 빈 줄로 남아 칸이 높아졌다).
[6.6.0] - 2026-09-28
HWP 5.0(.hwp) 읽기·쓰기를 더하고, 템플릿·양식을 공개 API만으로 다루는 기능(칸 안쪽 여백,
글꼴·테두리 읽기와 글꼴 교체, 떠 있는 도형의 기준 틀, 본문 비우기, 메타데이터·미리보기·그림
정리)을 공개한 릴리스입니다. 한/글과 결과를 맞추느라 출력이 바뀌는 고침이 여럿 있습니다.
특히 equalize_column_widths()는 한/글처럼 표 폭을 행별 칸 수의 공배수로 올립니다. 전의 분배가
필요하면 set_column_widths([1] * column_count)를 씁니다. 저장 성공만으로 시각 품질을
보증하지 않습니다.
바꿈
FormFit(
hwpx.form_fit)이 셀과 누름틀 값의 줄을 한컴의 줄 배치 규칙으로 센다. 글꼴 파일은 쓰지 않는다. 공백은 0.5 em이고, 폭에 장평과 (1 + 자간)을 곱한다(줄 끝 글자에는 자간을 붙이지 않는다). 칸 안 폭이 1440 HWPUNIT보다 좁아도 줄 폭은 1440이다. 한글·영어 나눔 설정을 따른다(한글BREAK_WORD는 어절 단위,KEEP_WORD는 글자 단위). 줄 끝 공백은 여백 밖으로 걸치고, 첫 줄 들여쓰기와 내어쓰기, 최소 공백, 닫는 부호의 줄 머리 금칙을 반영한다. 영어 단어 안의/ - . @뒤에서는 나누지 않는다. 같은 줄의 체크박스 같은 인라인 개체는 첫 줄 폭만 줄인다.셀 슬롯(
resolve_slot_metrics)은 셀 첫 문단의 문단 모양과 글자 모양에서, 누름틀 슬롯은 필드 문단과 글자 모양에서 이 설정(TextStyle)을 읽는다.새
TextStyle과hancom_line_starts()를 공개한다.estimate_text_width와estimate_lines는style을 선택 인자로 받고,style없이 부르면 전과 같다.문단 모양 읽기(
ParagraphProperty.margin)가 한컴 표기의 여백 값 (<hc:intent value="1000" unit="HWPUNIT"/>)을 읽는다. 전에는 요소 글자에서만 값을 찾아 한컴 문서의 문단 여백이 모두None이었고, FormFit도 들여쓰기를 0으로 봤다.
추가
Section.clear_body()로 문서를 양식 틀로 비운다. 첫 문단 첫 run의hp:secPr(쪽 설정)와 첫 문단 모든 run의hp:ctrl(단·머리말·꼬리말·쪽 번호 — 한/글은 이것들을 흔히 둘째 run에 쓴다)을 남기고, 나머지 문단, 첫 문단 run의 다른 자식, 컨트롤이 없는 뒤쪽 run, 첫 문단의 줄 배치 캐시를 지운다. 머리말·꼬리말처럼 남길 컨트롤 안에 글·표·개체·양식 개체·덧말·글자 겹침이 있으면 기본으로section-clear-control-content로 거부하고,on_control_content="keep"은 남기며,"strip"은 그 컨트롤과hp:secPr안의 머리말·꼬리말 사본을 지운다."strip"으로도 지울 수 없는 내용이 있으면 거부한다.hp:t안 자식 뒤의 글(<hp:t><hp:tab/>이름</hp:t>)도 내용으로 본다. 어느 쪽이든 찾은 태그를ClearBodyReport.control_content에 담는다. 거부는 바꾸기 전에 일어난다.Run.content_kinds()가 run이 담은 것을 닫힌 어휘(RUN_CONTENT_KINDS:text·table·picture·shape·equation·ole·chart·video·form·control·section_properties·other)로 알려 준다. 빈hp:t는 글로 세지 않는다.doc.text.plain()·markdown()·html()(hwpx.tools.exporter)에list_labels인자(기본False): 번호·개요·글머리표 문단 앞에 한/글이 그리는 글자(1.·가.·①·●등)와 공백 하나를 붙인다. 번호는 번호 정의와 수준마다 문서 순서로 세고(윗수준이 오면 아랫수준은 새로 시작, 건너뛴 윗수준은 시작값), 개요 문단은 구역의 개요 번호를 쓴다. 표 칸 문단도 읽는 순서로 센다(include_tables=False로 표를 빼도 세기는 한다). 쓸 수 있는 번호 모양은 숫자·한글·자모· 원 숫자·원 한글·로마 숫자·라틴 문자이고, 다른 모양(갑·일·一 등)의 문단에는 라벨을 붙이지 않는다. 센다(include_tables=False로 표를 빼도 세기는 한다).^N은 1수준부터 그 수준까지의 번호를 숫자로 잇고(1.2.), 정의가 없는 번호는 모든 수준을 그렇게 쓰며, 정의가 없는 글머리표는●로 쓴다. 글자가 기호 글꼴 문자(사용자 정의 영역의 U+F09F·U+F0A7 등) 하나뿐이면 한/글의 텍스트 저장처럼●·■등 유니코드 기호로 쓴다. 쓸 수 있는 번호 모양은 숫자·한글·자모·원 숫자· 원 한글·로마 숫자·라틴 문자이고, 다른 모양(갑·일·一 등)의 문단에는 라벨을 붙이지 않는다.셀 필드(이름 붙은 표 칸,
hp:tc@name):doc.fields.cells가 문서 순서로 돌려주고(본문 표와 칸 안의 표),doc.fields.fill_cell(value, name=..., index=None)이 한/글PutFieldText처럼 같은 이름의 칸을 모두 채운다.index(0부터)를 주면 그 하나만 채운다. 칸의 이름은cell.field_name으로 읽고 쓴다. 채우면 칸 자신의 문단 글만 바뀌고, 칸 안에 든 표나 개체의 글(그 안의 셀 필드 값 포함)은 그대로다. 새 오류 코드field-cell-not-found.doc.text.replace(search, replacement, everywhere=True): 한/글 “모두 바꾸기”처럼 표 칸(칸 안 표 포함)·글상자·캡션·머리말·꼬리말·각주·미주·바탕쪽의 문단도 바꾸고, 서식이 다른 런에 걸친 말도 바꾼다. 바꿀 글의 글자는 같은 자리의 찾은 글자가 있던 런의 서식을 따르고, 남는 글자는 찾은 글의 마지막 글자 뒤에 붙는다. 탭·줄 바꿈이나 컨트롤을 사이에 둔 글은 한 말로 보지 않고, 메모 본문은 바꾸지 않는다. python-hwpx가 두 벌로 쓰는 머리말·꼬리말은 두 곳을 같이 바꾸고 한 번으로 센다. 기본값(False)은 전처럼 본문 문단의 런 안만 바꾼다.docs/ai-assistants.md의 양식 채우기 예제는 이 인자를 쓴다.doc.styles.apply_paragraph_format(paragraphs=[...]): 본문 인덱스 대신 문단 객체를 받아 표 셀(중첩 표 포함)·머리말·꼬리말 문단에도 사람 단위(mm·pt·%) 문단 서식을 적용한다(예:paragraphs=[table.cell(0, 0).paragraphs[0]]). 같은 문단 모양을 쓰던 다른 문단은 그대로다. 이 문서에 속하지 않은 문단(다른 문서의 문단, 지운 문단)이 섞이면 아무것도 바꾸기 전에paragraph-not-in-document로 거부하고, 인덱스 인자와 함께 주면paragraph-argument-conflict로 거부한다. 결과의paragraphs에는 본문 문단 인덱스만 담기고formatted는 대상 수다. 기존paragraph_index·paragraph_indexes의 뜻은 그대로다. 머리말·꼬리말 객체에paragraphs속성 (cell.paragraphs처럼 문단 객체 목록)을 더한다.apply_list_format()의 기본 글머리표·번호 모양이 한/글 새 목록과 다른 점(글머리표-·○대●, 번호 2수준1.1.대가., 3수준1.1.1.대1))과 글머리표를 맞추는 법 (bullet_char="●")을 설명과 known-traps에 적는다. 한/글이 새 목록에 쓰는 모양은 한/글이 저장한 목록 fixture로 테스트에 고정한다.HWP 5.0(
.hwp)을 읽고 쓴다.HwpxDocument.open이.hwp를 같은 문서 모델로 열고,save_to_path("x.hwp")·save_to_stream(..., format="hwp")·to_bytes(format="hwp")가 HWP 5.0으로 쓴다. 본문·글자와 문단 모양·스타일·표·그림·그리기 개체·글맵시·수식·차트·OLE· 동영상·필드·누름틀과 양식 개체·머리말과 꼬리말·각주와 미주·메모·바탕쪽·변경 추적· 문서 정보·인쇄 설정·스크립트를 옮긴다. 옮기지 못한 내용은document.conversion_report(unconverted·dropped)에 세고Hwp5ConversionWarning으로 알린다. HWP 5.0으로 쓸 수 없는 내용은 쓰기 전에Hwp5Error(hwp5-write-unsupported)로 거부하고, 암호·배포용·DRM 문서도Hwp5Error로 거부한다(hwp5-*코드).HwpxPackage.open은 그대로 HWPX 전용이며.hwp에는HwpxDocument.open을 안내한다. 새 의존성은 없다.쪽 번호·줄 번호 설명을 보강한다(
page.set_page_number,set_visibility,hide_page_elements,set_line_numbers와 known-traps).set_page_number()의 번호는 머리말/꼬리말 글이라 한컴의 쪽 번호 감추기(hide_first_page_num,hide_page_elements(page_num=True))로 숨지 않으니 머리말/꼬리말째 감춘다.hide_page_elements()는 문단이 있는 쪽에서만 감춘다(다음 쪽부터 다시 보임, 설명을 바로잡음). 줄 번호는set_visibility(show_line_number=True)일 때만 그려진다.새 문서가 빈 줄로 시작하는 까닭(
HwpxDocument.new()의 첫 문단은 구역 설정을 담은 빈 문단이고add_paragraph()는 그 뒤에 붙는다)과 첫 줄부터 쓰는 법 (doc.paragraphs[0].text = ...)을HwpxDocument.new()설명과 known-traps에 적는다.hwpx.layout.lint_layout()이 쪽에서 나뉘지 않는 본문 표(글자처럼 취급한 표,pageBreak="NONE"표)가 쪽 본문보다 높으면TABLE_TALLER_THAN_PAGE로 알린다. 한컴은 이런 표를 한 쪽에 그린다. 쪽 첫머리가 아니면 다음 쪽으로 옮기고, 아래 여백까지 내려 그리며, 종이 아래 끝을 넘는 행은 그리지 않는다. 높이는 행마다 가장 높은 셀 높이의 합(아래 한계)으로 잰다. 행이 잘리면overflow_policy="fail"에서 오류, 아니면 경고다. 여러 쪽에 걸칠 표는Table.set_treat_as_char(False)로 흐르게 한다.styles.apply_paragraph_format(border={...})로 문단 테두리를 만든다. 키는sides(기본 네 면),color(#000000),width(0.12 mm),type(SOLID),offset_mm(글과의 간격 mm, 수 하나 또는 왼쪽·오른쪽·위·아래),connect,ignore_margin이다.connect=True면 같은 문단 모양을 쓰는 연속 문단이 단과 쪽을 넘는 상자 하나로 그려진다(한컴의 “문단 테두리 연결”). 상자 안의 빈 문단에도 같은 서식을 주면 상자가 끊기지 않는다. 기존bottom_border=True는 그대로 아래 한 면만 켠다. 잘못된 지정은paragraph-border-invalid로 거부한다.템플릿을 넘기기 전에 패키지를 정리하는 기본 동작을 더한다. 무엇을 지울지는 호출자가 정한다.
doc.parts.clear_document_metadata(keep=("title", "language"), timestamp="1970-01-01T00:00:00Z")(HwpxPackage에도 같은 이름)는opf:metadata에서keep밖의 필드를 모두 비우고 비운 키를 문서 순서로 돌려준다. 이 라이브러리가 모르는opf:meta이름도 비운다.CreatedDate·ModifiedDate처럼 이름에Date가 든 필드는timestamp로 바꾸고, 자유형식date를 비롯한 나머지는 요소와 속성을 두고 내용만 비운다.doc.parts.clear_preview()는Preview/PrvText.txt를 비우고Preview/PrvImage.png를 1×1 흰 PNG로 바꾼다. 두 파트를 지우지는 않는다.doc.validate()가 파트가 없는 매니페스트 항목과, 매니페스트 항목이 없는BinData/파트를 경고로 알린다. 경고라서ok는 그대로다.BinData/밖의 파일을 잇는isEmbeded="0"항목(동영상 링크 등)은 알리지 않는다. 한/글은 OLE 개체에도isEmbeded="0"을 쓰지만 그 파일은BinData/안에 두므로, 빠지면 알린다.
Shape.set_position()이 기준 프레임과 정렬도 정한다.horz_rel_to·vert_rel_to·horz_align·vert_align이hp:pos의horzRelTo·vertRelTo·horzAlign·vertAlign을 쓴다. 값은 OWPML 스키마 철자 그대로다(vert_rel_to에는COLUMN이 없다).PAPER로 두면 확인 도장 같은 떠 있는 도형을 쪽 여백에 고정할 수 있다. 주지 않은 값은 그대로 두므로 오프셋만 주는 기존 호출은 결과가 같다. 스키마 밖의 값은 아무것도 바꾸기 전에shape-position-frame으로 거부한다.Shape.set_draw_text()에para_pr_id_ref(도형 안 문단의paraPrIDRef)와vert_align(hp:subList/@vertAlign,TOP/CENTER/BOTTOM)을 더한다. 둘 다 생략하면 XML이 전과 같다. 잘못된 정렬 값은shape-draw-text-vert-align으로 거부한다.Shape.draw_text·set_draw_text·remove_draw_text를 stable 표면에 올리고, 이들이 돌려주는 글상자를hwpx.model.DrawText라는 계약 이름으로 낸다(name·editable·text_margin·paragraphs·text·add_paragraph가 stable). 반환 객체 계약은 19개 클래스 / 189개 멤버가 된다.글꼴 표와 테두리를 값으로 읽는다.
doc.styles.fonts(lang="HANGUL")는 그 lang 블록의 글꼴을 id 문자열 →Font로,doc.styles.font_face(char_pr_id_ref, lang="HANGUL")는 글자 모양이 그 lang에서 가리키는 글꼴 이름을 돌려준다.doc.styles.border_fill_info(id)는hh:borderFill하나를BorderFillInfo(네 변과 대각선의BorderLine(type, width_mm, color), 단색 채우기fill)로 돌려준다. 값은 저장된 그대로이고, 그라데이션·그림 채우기는 설명하지 않는다(fill은None). 너비를 읽을 수 없으면style-border-fill-width-invalid다. 기존border_fill()은 그대로GenericElement를 돌려준다. 모두 메모리의 헤더를 읽어ensure_font()·ensure_border_fill()직후에도 맞는다.doc.styles.replace_font(src_face, dst_face, *, langs=None)로 글꼴 하나를 문서 전체에서 다른 글꼴로 바꾼다(읽는 쪽에 없는 장식 글꼴을 흔한 글꼴로 바꿀 때). lang 블록마다 바꿀 글꼴을 지우고 남은 글꼴 id를 0부터 다시 매기며, 모든 글자 모양의fontRef를 옮겨 다른 참조가 가리키는 글꼴은 바뀌지 않는다. 결과는FontReplaceReport(바뀐 블록langs, 새로 선언한 블록declared, 옮긴 참조 수repointed)다. 같은 글꼴 둘은style-font-replace-same-face로 거부한다.표 셀 안쪽 여백을 읽고 쓴다.
cell.margins는 한/글이 셀을 배치하는 여백을hwpx.objects.CellMargins(HWPUNIT,left·right·top·bottom)로 돌려준다. 셀의hasMargin이 꺼져 있으면 표의hp:inMargin, 켜져 있으면 셀의hp:cellMargin이다.add_table()이 만든 셀은CellMargins(510, 510, 141, 141)이다.cell.set_margins(left=, right=, top=, bottom=)는 주지 않은 면을 지금 여백으로 채워 네 면을 셀 여백에 쓰고hasMargin="1"로 켠 뒤 새 여백을 돌려준다. 여백이 실제로 바뀌면 그 셀에 바로 든 문단의 줄 배치 캐시를 지운다(안쪽 표 문단은 그대로). 인자가 없으면 아무것도 바꾸지 않는다.0 <= v < 2**31인int가 아닌 값(bool포함)은 바꾸기 전에HwpxValueError(cell-margin-value)로 거부한다.편집 의미론 문서에
table.set_cell_text()가 남기는 것과 다시 만드는 것(기본은 첫hp:t에 쓰고 나머지hp:t를 비우며 그렇게 비워진 문단만 지운다,split_paragraphs=True는 문단을 다시 만든다),table.set_column_widths([1] * column_count)가 나누는 규칙(전의equalize_column_widths()나눔: 표 너비 유지, 앞n-1열round(W / n), 마지막 열 나머지)과Paragraph.char_pr_id_ref가 그 문단의 run 전부에 들어간다는 것을 적는다.
바꿈
마지막 문단을 지우려는
paragraph.remove()가 맨ValueError대신HwpxValueError(ValueError하위라except ValueError는 그대로 잡는다)를 낸다.code는paragraph-remove-last,context["container"]는 문단을 담은 곳 (section·cell·header·footer)이다. 메시지는 같다.HwpxOxmlTable.equalize_column_widths()가 한/글 “셀 너비를 같게”처럼 행마다 칸을 같은 너비로 나눈다. 합친 칸은 그 행의 한 칸으로 세고, 모든 행이 같은 자리에서 끝나도록 표 너비를 행마다의 칸 수로 모두 나누어떨어지는 값까지 올린 뒤(예: 3칸 행과 4칸 행이면 12의 배수) 열 격자(colCnt·cellAddr·cellSpan)를 새 칸 경계로 다시 짠다. 한/글과 같아서 칸 수가 다른 행이 많으면 표가 크게 넓어질 수 있다. 칸 영역(hp:cellzone)의 열 주소도 새 격자로 옮기고, 같은 칸을 덮을 수 없으면 표를 그대로 두고HwpxValueError를 낸다. 합친 칸 아래에 크기 0인 자리 표시 칸이 남아 있는 표에서는 자리 표시 칸마다 덮는 칸의 새 열을 하나씩 주고, 열이 모자라면 남는 빈 자리 표시 칸은 없앤다(한/글은 합친 칸 아래에 칸을 쓰지 않는다). 전에는 격자 열을 같게 해서, 칸 경계가 행마다 다른 표에서는 칸 너비가 고르지 않았다. 세로로 합친 칸이 행마다 다른 너비를 받아야 하는 표는 한/글처럼 바꾸지 않고HwpxValueError를 낸다. 격자 열마다 같은 너비가 필요하면set_column_widths([1] * column_count)를 쓴다.validate_document()가 구역·머리 XML을 번들된 전체 OWPML 스키마(DevDoc/OWPML SCHEMA의 Body·ParaList·Core·Header)로도 검사한다(#118). 한/글 2011 네임스페이스를 2024로 바꿔 끼운 사본을 보고, 한/글이 여는 문서에도 있는 편차(2011 관행)는 빼고 나머지를 경고(OWPML schema: …)로 낸다.ok는 전처럼 하드 오류만 본다. 끄려면full_schema=False. 이 검사가 돌지 못하면 멈추지 않고 그 까닭을 경고로 남긴다.hp:secPr안의 주석·처리 지시는 건너뛴다.문단 객체의
add_hyperlink(url, text)도doc.add_hyperlink·doc.refs.add_hyperlink처럼 링크 글을 한/글 관례(파랑#0000FF+ 파랑 밑줄)로 쓴다. 전에는 문단의 글자 모양을 그대로 써서 링크가 보통 글처럼 보였다. 글꼴·크기·굵기 같은 나머지 모양은 링크가 들어가는 문단의 글자 모양을 따른다. 두 경로 모두 같고, 모양이 같은 문단의 링크는 글자 모양 하나를 함께 쓴다.char_pr_id_ref를 주면 그 모양을 쓴다. 각주·미주 본문의add_hyperlink도 같다. Markdown 내보내기는 링크 글에 밑줄·글자색 장식을 붙이지 않는다 (링크 표기가 이미 그 모양이다).doc.text.plain()·markdown()·html()(hwpx.tools.exporter)이 본문 글상자(도형 안 글)의 글을 내보낸다. 글상자의 문단은 그 글상자가 놓인 자리에, 그 문단의 표와 함께 문서 순서로 온다. 쪽 미리보기(render_layout_preview, 문서 뷰어)는⟦도형⟧표시 옆에 글상자 글을 쓴다. 전에는 둘 다 글상자 글을 버렸다(칸 안 글상자는 전부터 칸 글에 들어갔다). 머리말·꼬리말·각주·미주·메모 안의 글상자는 전처럼 본문으로 치지 않는다. 칸 안 글상자의 글은 미리보기에 한 번만 나온다. 메일 머지(merge_template_rows)는 글상자 속 자리표시자도 바꾼다 (본문·칸·다른 글상자 안의 글상자 모두). 전에는 찾기만 하고 바꾸지 않았다.doc.text.plain()·markdown()·html()(hwpx.tools.exporter)이 문단 안의 표·글상자· 캡션을 그 문단의 글 뒤에 모아 쓰지 않고, 한/글의 텍스트 저장처럼 놓인 자리에서 문단 글을 나눠 쓴다(개체 앞 글, 개체, 개체 뒤 글).list_labels의 번호와 글머리표는 첫 조각에 붙는다. 표 칸 안에서도 같아서, 칸 안의 글상자와 표는 칸 글 속 제자리에 오고 칸 안 표의 캡션은 그 표 뒤에 온다(전에는 칸 문단의 글 뒤에 왔고 캡션은 표 앞에 왔다).apply_table_ops의insert_row_by_clone이 합친 칸이 걸린 행도 한/글이 줄을 넣듯 처리한다. 기준 행 아래로 이어지는 합친 칸은 복제하지 않고 새 행 수만큼rowSpan을 늘리며, 윗행에서 내려와 기준 행에서 끝나는 합친 칸 밑에는 새 행마다 그 칸의 서식·너비로 빈 칸(기준 행 높이)을 넣는다. 전에는 기준 행에 여러 행에 걸친 칸이 있으면 거부했고(“clone source row must have rowSpan==1 cells”), 합친 칸이 기준 행에서 끝나면 새 행에 칸이 비어 거부했다. 모든 칸이 아래로 이어지는 행은 복제할 칸이 없어 거부한다.doc.parts.add_master_page()(doc.oxml.add_master_page())가page_number를 받지 않으면OPTIONAL_PAGE는 1, 다른 종류(BOTH·ODD·EVEN·LAST_PAGE)는 한/글처럼 0을 쓴다. 전에는 종류와 상관없이 1을 썼다.한 구역에 같은 쪽의 바탕쪽을 둘 연결하면(
add_master_page_reference·doc.page.set_master_page)HwpxValueError(master-page-pages-taken)를 낸다. 양쪽·홀수·짝수·마지막 쪽은 하나씩, 한 쪽 바탕쪽은 쪽 번호마다 하나다. 전에는 그대로 연결되어.hwp저장이 실패했다.add_table()이 만드는 표의 행 높이 기본값을 12.7 mm(3600)에서 한컴이 새 표에 쓰는 282로 바꾼다. 행은 셀 글 높이에 맞춰 자라서, 한 줄이면 약 4.6 mm다. 전처럼 높은 행이 필요하면height=를 준다.add_table()은 행들만으로 쪽 본문보다 높은 표를 흐르는 표로 만든다(글자처럼 취급을 끔). 행 높이는 셀 높이와 셀 글 한 줄 + 셀 위아래 여백 중 큰 값으로 잰다. 한컴은 글자처럼 취급한 표를 쪽에서 나누지 않아, 이런 표는 종이 끝에서 잘렸다. 흐르는 표는 행 사이에서 쪽을 넘는다. 쪽 크기는 그려지는 쪽(가로면 돌린 높이)으로 잰다.doc.media.remove_image()가 문서가 아직 가리키는 이진 항목을 지우지 않고HwpxValueError(media-item-in-use)를 낸다. 가리키는 곳은 그림, header의 채우기 그림·그림 글머리표, 바탕쪽, 동영상 파일·포스터, OLE, 내장 글꼴이고,context["references"]에"파트: 요소@속성"으로 담긴다. 거부는 바꾸기 전에 일어난다. 전에는 그대로 지워서 쪽 테두리 채우기 그림 같은 참조가 끊긴 채 저장되었다. 그래도 지우려면force=True를 준다. 구역·header처럼 이진 항목이 아닌 매니페스트 항목은 지우지 않고False를 돌려준다(전에는remove_image("section0")이 구역 파트를 지웠다).
고침
doc.styles.replace_font()가 한 언어 블록에 같은 이름으로 두 번 선언된 글꼴을 하나만 바꾸던 것을 고친다. 한/글이 저장한 문서에 흔한 모양으로, 둘째 선언을 가리키는 글자 모양은 옛 글꼴로 남는데 보고서는 그 블록이 바뀌었다고 알렸다. 이제 블록 안의 같은 이름 선언을 모두 지우고 그 참조를 모두 새 글꼴로 옮긴다.MS Word처럼 배치하는 문서(
hh:compatibleDocument의targetProgram="MS_WORD")를.hwp로 열고 저장할 때 배치 호환 설정이 사라지던 것을 고친다. 열 때hh:layoutCompatibility를 비워 두고 저장할 때 0으로 써서, Word 호환 배치가 모두 꺼졌다. 이제 켜진 설정마다hh:layoutCompatibility의 자식 하나로 한/글이 쓰는 순서대로 열고, 저장할 때 다시 쓴다. 한/글처럼HWP201X문서는 설정 없이 저장하고, 한/글이 모르는 자식이 있으면 한/글의 기본 설정으로 저장한다. 한/글의 일부 판이 쓰는 묶음 요소 (hh:char·hh:paragraph·hh:section·hh:object·hh:field)와 그 안의 것은 한/글처럼 건너뛴다.applyFontspaceToLatin은 한/글이 HWPX로 쓰지는 않지만 읽는 설정이라, 문서의 배치가 그대로 남도록 열어 둔다..hwp로 저장할 때 줄 간격AT_LEAST(최소)를 옛 줄 간격 필드에 한/글처럼 100%로 쓴다. 전에는 옛 필드에BETWEEN_LINES와 새 값을 썼다.표
merge_cells()(doc.tables.merge_cells)가 가려지는 칸의 글과 개체를 버리던 것을 고친다. 이제 병합 칸이 글이나 개체가 있는 칸의 문단을 읽기 순서(행마다 왼쪽에서 오른쪽, 위 행부터)로 모두 받고, 빈 칸(빈 문단 하나)은 아무것도 보태지 않는다. 병합 칸이 비어 있었으면 그 빈 줄 대신 옮겨 온 문단만 남는다. 칸 크기가 바뀌므로 병합 칸의 줄 배치 캐시는 비운다. 병합한 뒤split_merged_cell()로 나누면 옮겨 온 글은 첫 칸에 남는다. 병합할 영역의 칸 격자는 한 번만 만들어서 큰 표의 병합이 빨라졌다. 병합으로 칸이 하나도 남지 않은 행과, 어느 칸도 시작하지 않는 열은 한/글처럼 없앤다(행 전체를 합치면 그 높이의 한 행이 되고, 표 전체를 합치면 한 칸짜리 표가 된다). 빈hp:tr이 남은 표는 한/글이 열지 못했다.표 칸의
.text =(set_text)가 글을 지운 문단을 빈 줄로 남기던 것을 고친다. 글이 지워지고 남은 것이 없는 칸 문단은 없애서, 넣은 글이 그대로 읽힌다(병합한 칸에 글을 넣을 때도 같다). 원래 빈 줄이던 문단과 개체가 든 문단은 그대로 둔다.번호 형식을 받은 그대로(대문자로만 바꿔) 쓰던 것을 고친다. 목록(
styles.apply_list_format의number_format,ensure_numbering레벨의format), 각주·미주(set_footnote_auto_num_format·set_endnote_auto_num_format의type), 쪽 번호(page.set_page_number의format_type)가 그렇다. 한컴은 모르는 형식을 거부하지 않고 아라비아 숫자로 매겨서,number_format="decimal"같은 값이 조용히1. 2. 3.이 되었다. 이제 목록·개요 머리와 쪽 번호는 한컴 번호 모양 15개(hc:NumberType1:DIGIT·ROMAN_SMALL·ROMAN_CAPITAL·LATIN_SMALL·CIRCLED_DIGIT·HANGUL_SYLLABLE등,hwpx.oxml.numbering_kinds.NUMBER_FORMATS)를, 각주·미주 번호는 여기에DECAGON_CIRCLE·DECAGON_CIRCLE_HANJA·SYMBOL·USER_CHAR를 더한 19개(hc:NumberType2,NOTE_NUMBER_FORMATS)를 받는다. 쪽 번호가 먼저 받던 짧은 이름(roman→ROMAN_CAPITAL,roman_lower→ROMAN_SMALL,alpha→LATIN_CAPITAL등)도 받고, 그 밖의 값은HwpxValueError(style-number-format-invalid)로 거부한다.색 인자가
#RRGGBB가 아니어도 그대로 쓰던 것을 고친다. 한컴은 색 값을 16진 수 하나로 읽고 나머지를 거부하지 않아서,#ABC는 CSS의#AABBCC가 아니라#000ABC로,red는 검정으로,#12345는#012345로 보였다. 이제styles.ensure_run(color·highlight·underline_color·shadow), 칸·테두리 채우기와 그러데이션 색, 메모 모양, 도형 선·채우기, 단 구분선,body_patch의restyle_text글자색은#RRGGBB와#AARRGGBB(#생략·소문자 허용)만 받고(none을 받던 자리는none도), 그 밖의 값은HwpxValueError(style-color-invalid)로 거부한다.#AARRGGBB는 한컴이 무늬·테두리·선·그림자 색에 쓰는 꼴(알파 먼저)이라, 문서에서 읽은 색을 넘겨도 그대로 쓴다. 거부하면 아무것도 바꾸지 않는다: 머리말·꼬리말set_content는 내용을 비우기 전에 색을 검사하고, 그 뒤에 값이 거부되어도 원래 내용으로 되돌린다.set_columns도 구분선 값을 먼저 검사한다.테두리 굵기(
styles.ensure_border_fill의border_width,apply_paragraph_format의 문단 테두리 굵기)와 단 구분선 굵기(page.set_columns의separator_width)를 받은 그대로 쓰던 것을 고친다. 한컴은 선 굵기 목록(0.1 mm~5.0 mm)에 글자 그대로 있는 값만 읽고,1 mm·2 mm·0.12mm처럼 목록 밖의 값은 가장 가는0.1 mm로 그렸다. 이제"1 mm"·"1mm"·1은"1.0 mm"로 쓰고, 목록에 없는 굵기는 가장 가까운 목록 값으로 쓴다(한가운데면 굵은 쪽:"0.45 mm"는"0.5 mm","6 mm"는"5.0 mm"). 0보다 큰 mm 값이 아니면 ("thick"·"1 cm"·0)HwpxValueError(style-line-width-invalid)로 거부한다.hwpx.oxml.utils.LINE_WIDTHS가 그 목록이다.styles.ensure_run(ratio=...)(장평)가 256~400을 받던 것을 고친다. 한컴은 장평을 한 바이트로 두어 256 이상은 256을 뺀 값이 된다(300은 44%로 그려짐). 이제 범위는 10~255다.테두리 종류(
styles.ensure_border_fill의border_type, 문단 테두리, 표 칸 테두리)의THICK_3D·THICK_3D_REVERSE_LIGHTING·SLIM_3D·SLIM_3D_REVERSE_LIGHTING을 그대로 써서, 한컴이 모르는 표기라 테두리가 없는 것(NONE)으로 그려지던 것을 고친다. 이제 한컴 표기THICK3D·THICKREV3D·3D·REV3D로 바꿔 쓰고, 한컴 표기도 그대로 받는다..hwp로 저장할 때 그림 효과PATTERN8x8을 한/글이 읽지 않는 값으로 쓰던 것을 고친다. 한/글처럼 원래 그림(REAL_PIC)으로 쓰고,.hwp에서 이름이 없는 효과 값은effect없이 연다..hwp의 선 종류 13~17을 한/글이 쓰는 이름(DOUBLEWAVE·THICK3D·THICKREV3D·3D·REV3D)으로 열고, 저장할 때는 이 이름과THICK_3D같은 다른 철자를 모두 받는다. 문단 번호의 번호 형식을 4비트로만 써서DECAGON_CIRCLE_HANJA가 다른 형식으로 바뀌던 것도 고친다.그림자가 있는 도형을
.hwp로 저장하고 열 때 바깥 여백을 한/글처럼 셈한다. 기울임·원근·확대·축소 그림자가 도형 밖으로 나간 만큼을 더하고 빼며, 왼쪽이나 위로 민 그림자도 그쪽 여백에 더한다. 큰 도형에서 여백이 16비트를 넘으면 한/글처럼 아래 16비트로 저장하고, 열 때 원래 여백으로 되돌린다 (전에는 32비트를 넘는 값으로 열렸다).HWPX에 없는 배치 속성을
.hwp에 쓸 때 한/글이 주는 값을 쓴다.textWrap은 도형이SQUARE, 양식 개체가TOP_AND_BOTTOM이고,vertRelTo·horzRelTo는PAPER,widthRelTo·heightRelTo는PAGE, 캡션side는LEFT다. 채운 화살표(FILLED_*)와 덧말 위치CENTER처럼.hwp에 없는 값도 한/글처럼 기본값으로 쓴다..hwp의 틀 없는 글자 겹치기를 한/글처럼OVERLAP으로 연다. 전에는SPREAD로 열어 저장할 때 빈칸을 붙였다. 수식 버전이 빈.hwp수식은 빈 채로 열고 저장한다.export_text()·export_markdown()·export_html()이 표 칸 글의 앞뒤 공백을 지우던 것을 고친다. 들여 맞춘용 역 명이용 역 명으로,협조자가협조자로 나왔다. 한/글이 저장한 문서의 미리보기 글(Preview/PrvText.txt)도 칸 글의 공백을 그대로 둔다. 칸 끝의 빈 문단은 전처럼 빼고, 글에 든 공백만 지킨다.apply_paragraph_format()가 여백·들여쓰기·문단 간격을hp:switch안에 쓸 때hp:default에는hp:case값의 두 배를 쓴다. 한컴 문서와 같은 표기다. 전에는 두 곳에 같은 값을 써서,hp:case를 모르고hp:default를 읽는 프로그램에서는 여백이 절반으로 보였다.hp:switch밖의 여백은 준 값 그대로다.표 칸 하나의 테두리를 바꾸면(
table.set_cell_borders) 이웃 칸의 맞닿은 변이 옛 선으로 남아, 한컴에서 두 선이 겹쳐 그려지던 것을 고친다. 칸마다 네 변을 따로 가지고 맞닿은 변은 두 칸의 선이 다 그려지므로, 한컴의 칸 테두리 명령은 이웃 칸의 맞닿은 변도 같은 선으로 바꾼다. 이제set_cell_borders()도 이웃 칸의 맞닿은 변만 같은 선으로 바꾼 테두리/배경 정의를 이웃에 준다. 이웃의 다른 변과 배경은 그대로다. 한컴처럼, 맞닿은 변이 바꾼 칸의 변보다 긴 이웃(한 칸 옆의 병합 칸)은 바꾸지 않는다. 칸 하나가 가리킬 정의를 정하는set_cell_border_fill()은 전처럼 그 칸만 바꾼다.apply_paragraph_format(alignment=...)가 한컴의 나눔 정렬DISTRIBUTE_SPACE를 받는다. 전에는 배분 정렬DISTRIBUTE만 받고DISTRIBUTE_SPACE는 알 수 없는 정렬이라며 거부했다(문서를 읽을 때는 이 값을 그대로 둔다).add_heading()이 제목 문단을 그 개요 스타일의 문단 모양·글자 모양에서 만든다. 전에는 문단 모양 0(바탕글)에 개요 수준만 더하고 글자 모양도 0을 써서, 한컴에서 개요 번호는 붙지만 스타일의 들여쓰기·간격·글자 모양이 빠진 제목이 됐다. 한컴은 문단을 스타일이 아니라 문단에 달린 모양으로 그린다.char_pr_id_ref를 주면 그 글자 모양을 쓴다.수식 상자 크기(
hp:sz)와 기준선(baseLine)을 수식 구조로 잰다(add_equation,estimate_equation_size). 한컴은 문서를 열 때 수식을 다시 재지 않고 저장된 상자로 줄을 배치한다. 이전 추정(보이는 글자 수 × 0.45, 높이는 기준 크기의 1.6~2.5배)은 한 줄짜리 수식도 상자를 크게 잡아 수식 뒤에 빈틈이 생기고 줄이 높아졌으며, 좁으면 뒤 글자와 겹쳤다. 기준선도 기준 크기에서 계산한 값이라 수식이 위아래로 어긋났다. 이제 글자 종류별 너비, 분수(over), 위·아래 첨자, 근호, 큰 연산자와 그 범위,lim아래 범위, 행렬과pile, 줄 나눔(#)을 상자로 쌓아 크기를 재고, 기준선은 상자 높이에서 기준선 위 부분의 비율(한 줄 수식 약 86)로 쓴다. 설명의 ‘한컴이 열 때 다시 잰다’도 바로잡았다.구역에 XML 주석이나 처리 명령이 있는 문서를 편집한 뒤 저장하면
TypeError: cannot serialize <cyfunction Comment>로 멈추던 것을 고친다(6.5.0부터 있던 결함). 실문서 가운데 표 행 안에 주석을 둔 문서가 있었고, 편집하지 않고 저장하면 원본 바이트를 그대로 써서 드러나지 않았다. 이제 편집한 파트를 쓸 때 주석과 처리 명령은 내용이 아니므로 빼고, 그 뒤의 글은 남긴다. 주석이 없는 문서의 출력은 그대로다.doc.text.replace(..., everywhere=True)가hp:t안 XML 주석의 글까지 찾아 바꾸던 것을 고친다. 주석은 건너뛰고, 주석 양쪽의 글은 이어진 글로 찾는다.doc.text.plain()·markdown()·html()(hwpx.tools.exporter)이 표·그림·도형의 캡션과 덧말을 내보내지 않던 것을 고친다. 한/글의 텍스트 저장처럼 표 캡션은 표 뒤에, 그림·도형 캡션은 그 자리에 오고, 덧말은 본말 뒤에(덧말:<덧말 글>)로 붙는다. 칸 안 표의 캡션은 전처럼 칸 글에 한 번 들어간다. 캡션의 자동 번호는 담긴 번호로 쓴다(그림 1,표 1).hp:switch에 두 번 쓰인 개체 (차트와 그 OLE 대체본)는 한 번만 읽고,include_tables=False면 표와 함께 그 캡션도 뺀다. 메일 머지(merge_template_rows)는 자리표시자를 한/글 “모두 바꾸기”가 닿는 곳(표 칸·글상자· 캡션·머리말·꼬리말·각주·미주)과 덧말에서도 바꾼다. 전에는 내보내기로 찾기만 하고 바꾸지 못해 그 행을 실패로 보고했다.글자 모양을 바꾸는 API가 기준 글자 모양의 나머지 서식을 지키게 고친다. 런의
bold·italic·underline을 바꾸면 글자 크기·색·글꼴이 첫 글자 모양의 것으로 돌아갔다.styles.ensure_run()과add_run(size=...)같은 글자 서식 인자는 요청한 값만 같은 아무 글자 모양이나 돌려줘, 새 문서에서add_run("글", size=16)이 파란 제목 글자가 되고base_char_pr_id를 주어도 다른 글자 모양이 나왔다. 양식 칸에 맞춰 글자를 줄이면 굵게·기울임·밑줄이 풀렸고, 새로 만든 글자 모양에서는 기준의 취소선이 빠졌다. 이제 요청한 글자 모양은 기준(base_char_pr_id, 없으면 첫 글자 모양)에서 요청한 값만 바꾼 것이고, 내용이 같은 글자 모양이 있을 때만 그것을 다시 쓴다.ensure_run()에서 주지 않은bold·italic·underline은 기준의 것을 따른다(기준을 주지 않으면 끔).끝이 아닌 구역을
remove_section()으로 지운 문서를 한/글이 열지 못하고,add_section(after=...)로 가운데 넣은 구역이 한/글에서 맨 뒤에 나오던 것을 고친다. 한/글은 구역을content.hpf항목 idsection0,section1, …의 번호 순서로 찾고(spine 순서나 파일 이름이 아니라), 번호가 하나라도 빠지면 문서를 열지 않는다. 이제 구역을 넣거나 지우거나 문서를 저장할 때 구역 항목의 id를 문서 순서대로 다시 매긴다(파일 이름은 그대로). 그래서 id가 어긋난 채 저장된 파일도 다시 저장하면 바로잡힌다. 새 구역은 다른 구역 파일이나 항목이 쓰는 번호를 받지 않는다.doc.text.replace()(과run.replace_text()를 쓰는 메일 병합)가hp:t안의 탭·줄 바꿈을 사이에 둔 글을 이어 읽어,사⇥과를사과로 찾아 바꾸던 것을 고친다(탭은 남고 글자만 합쳐졌다). 한/글처럼 탭·줄 바꿈 같은 글자 요소와 컨트롤은 말을 끊는다. 형광펜 표시는 전처럼 끊지 않는다.표 셀·머리말·꼬리말 문단의
remove()가 아무 일도 하지 않던 것을 고친다. 문단을 구역 바로 아래에서만 찾아, 셀 문단(cell.paragraphs[i].remove())은 오류 없이 그대로 남았다. 이제 문단을 담은 구역이나hp:subList에서 지운다. 담은 곳의 마지막 문단은 지우지 않고ValueError를 낸다(문단이 없는 셀·머리말·꼬리말은 한/글이 열지 못한다). 이미 지운 문단의remove()는 조용히 넘어간다. 같은 까닭으로 셀 문단의apply_model()이ValueError를 내던 것도 고친다.그림 같은 이진 항목이 많은 문서를 여는 데 오래 걸리던 것을 고친다.
content.hpf의 항목마다 패키지의 모든 파트 이름을 다시 정규화해서, 항목·파트 수의 제곱에 비례했다 (파트 1,400개 문서에서 열기 1분 넘게). 이제 목록을 읽을 때 파트 이름을 한 번만 정규화한다.resolve_part_name()의 동작은 그대로다.저장이 한컴이 쓴 문단 줄 배치 캐시(
hp:linesegarray)를 멀쩡한데도 지우던 것을 고친다. 저장 때 캐시가 글보다 뒤를 가리키는지 보는 검사가hp:t안의 줄 바꿈·고정 폭 빈칸·탭을 글자 수에서 빼고 셌다. 한컴의 줄 시작 위치(textpos)는 이것들을 센다(탭 8, 나머지 1). 그래서 구역을 다시 쓸 때(그 구역의 다른 문단만 고쳐도) 이런 문단의 캐시가 사라져, 한컴이 그 문단을 새로 배치하면서 쪽 수가 바뀔 수 있었다. 이제 한컴의 글자 위치로 센다.doc.text.markdown(rich=True)(hwpx.tools.markdown_export)가 도형(사각형·타원· 다각형)을 품은 문단의 글을 잃던 것을 고친다.도형을 품은 문단은 제 글을 통째로 버렸다(도형 글이 흘러나온 것으로 보았다). 이제 문단 자신의 글이 도형 안의 글과 같을 때만 뺀다.
도형 안 문단을 이미 쓴 것인지
id()로 가렸다. 풀린 lxml 요소 프록시의 번호를 다른 문단이 다시 받으면, 처음 보는 도형 문단을 건너뛰었다(칸 안 도형도 같음). 이제 요소 자체로 가린다.
hwpx.table_patch.apply_table_ops의delete_table·clone_table·split_table·merge_table이 표를 담은 문단을 잘못 찾던 것을 고친다. 문단 안에서 표 앞에hp:pagePr·hp:pageBorderFill·hp:pic처럼 이름이p로 시작하는 요소가 있으면 (구역 설정이 든 구역 첫 문단의 표 등) 그 요소를 문단 시작으로 알아, 잘린 XML이 저장 전 검사에 걸려ValueError로 멈추거나merge_table이 두 표 사이에 글이 있다고 잘못 거부했다. 이제 표를 감싸는 가장 안쪽 문단을 찾는다. 그 문단에 구역 설정·글·다른 표가 함께 있으면 문단 대신 표만 다룬다.delete_table은 표만 지우고,clone_table·split_table의 새 표는 그 표만 담은 새 문단(같은 문단 모양, 쪽·단 나눔 없음)에 들어간다. 한 문단에 든 두 표를merge_table하면 표만 합치고, 두 표 사이에 글이 있으면 거부한다.autofit_columns가 표 안에 표가 있는 표에서KeyError를 내던 것을, 다른 구조 편집처럼TableStructureError로 거부하게 한다.delete_column이 한 칸짜리 칸만 있는 행이 표의 모든 열을 덮지 않을 때(오른쪽 열이 윗행에서 내려온 합친 칸에 덮인 행) 그 행을 열 폭의 근거로 써서KeyError를 내던 것을 고친다. 이런 행은 건너뛰고, 없으면 합친 칸 격자에서 폭을 구한다.delete_row가 세로로 합친 칸의 첫 행을 지우면 합친 칸까지 지워 표에 빈 자리가 생겨 거부되던 것을 고친다. 이제 합친 칸은 다음 행으로 내려가 한 행 줄어들고, 글과 서식은 그대로다(한컴 편집기에서 그 행을 지운 결과와 같다).doc.media.images가content.hpf매니페스트에만 있는 이진 항목을 보지 못하던 것을 고친다. 한컴이 저장한 파일은 보통 header에binDataList가 없어 그림이 있어도 빈 목록이 나왔다. 이제 header 항목 뒤에 매니페스트에만 있는 항목(href가BinData/아래이거나 media-type이image/*)을 붙인다.isEmbeded="0"으로 바깥 파일을 잇는 항목은 넣지 않고, 파트가 없는 내장 항목은size=0으로 넣는다.doc.media.remove_image()는 매니페스트 id·파트 경로 ("BinData/image1.png")·BinaryItem을 모두 받아 이런 항목도 지운다.doc.media.remove_image("image1")이 header에image10.png가 먼저 있으면 그binItem과 파트를 대신 지우던 것을 고친다. header 항목을 파일 이름 앞부분으로 맞췄기 때문이다. 이제 확장자를 뺀 파일 이름(또는 파일 이름 전체)이 같아야 맞는다.doc.media.replace_picture()가 바꾼 그림의 옛 이진 항목을 header의 채우기 그림이나 바탕쪽 같은 다른 곳이 아직 쓰는데도 지우던 것을 고친다. 본문 그림만 보고 고아로 판단했기 때문이다. 이제 문서 어디서도 가리키지 않을 때만 지우고, 아니면 남긴다(removed_orphans가 빈다).HwpxPackage.remove_manifest_item()에 파트 경로를 넘기면False만 돌려주고 항목을 남기던 것을 고친다. 이 인자는 매니페스트id다. 그 id의 항목이 없고 값에/가 있으면 href가 같은 파트를 가리키는 항목을 지운다.HWP 5.0으로 저장(
to_bytes(format="hwp")·save_to_path("*.hwp"))할 때 원본 구역에 XML 주석이 있으면ValueError: Invalid input tag로 멈추던 것을 고친다. 실문서 가운데 표 행 안에 주석을 둔 문서가 있었다. 이제 주석과 처리 명령은 내용이 아니므로 건너뛴다.머리말·꼬리말 객체로 고친 내용이 한/글에 나타나지 않던 것을 고친다. python-hwpx가 만든 머리말·꼬리말은 구역 설정(
hp:secPr) 안과 본문hp:ctrl안에 두 벌로 쓰이고, 한/글은hp:ctrl쪽만 읽는다.set_header()·set_footer()가 돌려준 객체의text설정,add_run(),add_paragraph(),add_page_number_field(),set_content(),clear_content()와 그 문단을 고친 내용은 구역 설정 쪽에만 들어가, 한/글에서는 처음 글이 그대로 보였다. 이제 고친 구역을 저장할 때 두 벌을 맞춘다. 두 벌이 마지막으로 같았던 때(문서를 열 때, 머리말·꼬리말을 설정할 때, 저장할 때) 뒤에 바뀐 쪽을 다른 쪽에 옮기고, 알 수 없거나 둘 다 바뀌었으면 구역 설정 쪽을 따른다. 두 요소는 제자리에 남아서 저장한 뒤에도 머리말 객체를 계속 쓸 수 있다. 두 벌로 쓰인 머리말·꼬리말의 누름틀은doc.fields에 한 번만 나온다.id·apply_page_type을 바꾸면hp:ctrl쪽도 함께 바꾸고(홀수 쪽으로 바꾼 머리말이 모든 쪽에 보이던 것),BOTH가 되면 홀수·짝수 쪽 것보다 앞에 둔다(hp:ctrl쪽만 있는 머리말·꼬리말도 같다). 한/글이 저장한 문서처럼hp:ctrl쪽만 있는 머리말·꼬리말의 내용은 저장할 때 건드리지 않는다.clear_content()와 빈set_content([])는 빈 문단 하나를 남긴다. 전에는hp:subList까지 지웠는데, 실제 문서의 머리말·꼬리말은 모두hp:subList와 문단을 하나 이상 가진다.칸·테두리 그림 채우기(
set_cell_fill_image(mode=),ensure_border_fill(fill_image={"mode": ...}))가LEFT_TOP·LEFT_BOTTOM·RIGHT_CENTER·RIGHT_TOP도 받는다. 한/글이 쓰는 그림 위치인데 OWPML 스키마의 목록(12개)에 없어ValueError를 냈다.TextExtractor.extract_text()가 안쪽 문단을 두 번 쓰던 것을 고친다.include_nested=True(기본)에서 각주를footnote="inline"으로, 컨트롤을control="nested"로, 개체를object_behavior="nested"로 글에 넣으면 그 안의 문단이 따로 한 번 더 나왔다.object_behavior="nested"는 칸 안의 표처럼 개체 안의 개체 글을 문단 하나 안에서도 거듭 썼다. 이제 앞 문단의 글에 이미 들어간 문단은 따로 쓰지 않고, 개체는 자기 칸·글상자의 문단만 돌며 그 안의 개체는 그 문단이 쓴다.평문·HTML·Markdown 내보내기(
doc.text.plain()·html()·markdown(),hwpx.tools.exporter)의 표 글을 고친다. 칸 안의 표는 바깥 칸의 글에 들어 있는데도 따로 한 번 더 나왔다. 칸에 단 각주·미주·메모의 본문이 칸 글에 섞였다(본문 문단에 단 것은 빠졌다). 머리말·꼬리말 안의 표가 본문 첫 문단 자리에 나왔다. 이제 표는 바깥 표만 한 번 나오고, 각주·미주·메모·숨은 설명·머리말·꼬리말의 글은 본문 내보내기에 넣지 않는다. Markdown 표 칸은 문단 나눔을<br>로,|를\|로 써서 한 줄에 둔다(전에는 칸 안의 줄 나눔이 표 행을 깨뜨렸다).doc.text.markdown()이 첫 행보다 칸이 많은 행의 뒤쪽 칸을 버리던 것을 고친다. 첫 행이 합친 칸(제목 행 등)이면 그 아래 행은 첫 칸만 남았다. 이제 칸을hp:cellAddr자리에 놓고 가장 넓은 행만큼 열을 두며, 합친 칸이 덮는 자리는 비운다.doc.tracking.delete(문단)로 글만 있는 문단을 통째로 지우면, 한컴에서 그 자리에 빈 줄이 남던 것을 고친다. 문단 나눔은 삭제로 표시되지 않았다. 이제 뒤에 문단이 이어지면 마지막 삭제 표시에paraend="1"을 써서 문단 나눔도 지운다. 구역 설정·컨트롤·개체가 있는 문단과 마지막 문단은 전처럼 나눔을 남긴다.append_document/insert_document가 원본 첫 문단의 단 설정(hp:colPr)을 뺄 때, 같은hp:ctrl안에 함께 있던 다른 컨트롤(누름틀 시작, 쪽 번호 새로 시작 등)까지 지우던 것을 고친다. 누름틀 끝만 남아 짝이 끊긴 문서는 한컴에서 열리지 않았다. 이제hp:colPr만 빼고,hp:ctrl은 비었을 때만 지운다.insert_document(after_paragraph_index=-1)가 대상의 구역 설정을 새 첫 문단으로 옮길 때도hp:colPr만 옮긴다.문서를 저장할 때 같은 패키지 바이트에 열기 안전 검사(
validate_editor_open_safety)를 두 번 돌리던 것을 한 번으로 줄인다. 패키지 저장이 검사를 통과시킨 바이트의 SHA-256을 기억하고, 문서 쪽 검사는 같은 바이트면 건너뛴다. 패키지가 검사하지 않은 바이트는 전처럼 검사한다. 긴 문서의 저장이 빨라진다.styles.ensure_run(font=...)가 머리말에 없는 글꼴을 조용히 무시하던 것을 고친다(글자는 기본 글꼴로 그려졌다). 이제 한컴처럼 그 글꼴을 일곱 언어 모두에 선언하고 적용한다(ensure_font의 기본값과 같다).일부 언어에만 선언된 글꼴을
ensure_run(font=...)로 쓰면, 선언되지 않은 언어의fontRef에 다른 언어의 글꼴 번호가 들어가던 것을 고친다.fontRef는 언어마다 번호를 따로 매겨서, 그 언어에 글꼴이 더 있으면 엉뚱한 글꼴이 됐다. 이제 글꼴이 선언된 언어만 바꾸고, 나머지 언어는 바탕 글자 모양의 글꼴을 그대로 둔다(font_ref_for_face는 선언된 언어만 돌려준다).긴 문서를 열고 저장하는 시간을 줄인다. 네임스페이스 정규화(
normalize_hwpml_namespaces)가 XML 파트마다 모든 태그와 속성을 파이썬 콜백으로 다시 훑었는데, 이제 2016 네임스페이스 URI가 든 태그만 찾아 고친다. 결과 바이트는 같다. 저장은 열기 안전 검사에서 패키지를 다시 읽으므로 함께 빨라진다.page.set_page_number(format_type=...)로 고른 쪽 번호 모양(로마 숫자, 원 숫자 등)이 한컴에서 늘 아라비아 숫자로 그려지던 것을 고친다. 한컴은 머리말·꼬리말의 쪽 번호를hp:autoNum으로 그리고 그 모양을 자식hp:autoNumFormat에서 읽는데, 모양은 옆의hp:pageNum@formatType에만 적혀 있었다. 이제hp:autoNum이 한컴처럼 모양을 가진다. “쪽/전체 쪽”(format="page/total")의 전체 쪽 번호도 같은 모양을 쓴다.표 셀에 넣은 차트(
shapes.add_chart(..., paragraph=셀 문단))가 셀보다 넓게 만들어져 한컴에서 셀 경계에서 잘리던 것을 고친다.size를 주지 않으면 기본 크기(32250×18750)를 넣을 곳(표 셀이나 본문)의 쓸 수 있는 폭에 맞게 같은 비율로 줄인다. 중첩 표의 기본 폭과 같은 기준이다. 본문의 기본 크기와size로 준 크기는 그대로다.tools.toc_author.add_native_toc()가 만든 차례 항목에서 쪽 번호가 제목 바로 뒤에 붙던 것을 고친다. 한컴은 탭을 문단의 탭 정지로 배치하고hp:tab속성은 그 결과의 캐시로만 쓰는데, 항목 문단(기본 문단 모양)에는 탭 정지가 없었다. 이제 항목 문단은 첫 구역 본문 오른쪽 끝(쪽 폭에서 왼쪽·오른쪽·제본 여백을 뺀 곳)에leader채움의 오른쪽 탭 정지를 가진다. 기본값 3은 한컴 차례와 같은 점선이다.leader는 탭 채움 코드 0–11만 받고, 그 밖의 값은paragraph-tab-leader-invalid로 거부한다.탭이 든 문단의 멀쩡한 줄 배치 캐시를 오래됐다고 보고 지우던 것을 고친다. 그래서 한컴 문서를 편집 없이 저장하기만 해도 쪽 경계에서 줄이 옮겨 갈 수 있었다. 캐시의 줄 시작 위치 (
hp:lineseg@textpos)는hp:t안의 인라인 요소도 센다. 탭은 8칸, 줄바꿈·하이픈·고정 빈칸은 1칸이다. 저장 때 캐시 정리와 편집 안전 검사의 “줄 시작이 글 길이를 넘는 캐시” 규칙이 이 길이 (oxml.utils.hancom_text_length)를 쓴다. 글을 넘어서 시작하는 캐시는 전처럼 지운다.문서에 이미 들어 있던 줄 배치 캐시가 문단 글을 다 담지 못하면(여러 줄 글에 한 줄 캐시) python-hwpx가 편집하지 않은 문서도 저장하지 못하던 것을 고친다. 편집 안전 검사는 이런 캐시를 거부하는데(한컴이 꼬리 글을 겹쳐 그린다), 저장 때 모든 구역에 도는 캐시 정리(
HwpxOxmlSection.remove_stale_layout_caches())는 글 길이를 넘는 줄 시작만 지웠다. 이제 그 검사가 거부할 캐시도 지운다. 한컴은 그 문단을 다시 배치한다.머리말·꼬리말 글(
page.set_header(text=...)·set_footer(text=...), 머리말·꼬리말 객체의text와add_run())과 각주·미주 글(notes.add_footnote()등, 주석 객체의text)이 탭을 지우던 것을 고친다. 이제 탭을hp:t안의hp:tab으로 쓴다 (page.set_header(text="학교명\t날짜")). 두text는hp:t안 요소 뒤의 글과 탭까지 읽는다. 머리말·꼬리말의set_simple_text_preserving()은 전처럼 탭을 받지 않는다.셀의
set_text()(Table.set_cell_text())와run.text = ...가 옛 글 일부를 남기던 것을 고친다. 한컴이 쓴 셀과 run에는hp:t안에 줄바꿈·탭·공백 요소와 형광펜 표시가 있고, 그 뒤에도 글이 있다. 두 설정자는hp:t의 앞 글만 바꿔서 요소와 그 뒤 옛 글이 새 값 뒤에 남았다(run.text는 새 값을 옛 글 뒤에 붙였다). 이제paragraph.text처럼 옛 글을 요소까지 모두 지우고 새 값을 쓴다.run.text는 첫hp:t자리에 쓴다.같은 두 설정자가 값의 탭을 지우던 것을 고친다. 이제 탭을
hp:t안의hp:tab으로 쓴다(split_paragraphs=True도 같다).탭 문자가 든 글을 쓴 문서를 한컴이 끝내 열지 못하던 것을 고친다.
paragraph.add_run("이름\t홍길동"),doc.text.replace(),fill_cells(),paragraph_patch(),apply_body_ops()의replace_text와 삽입 op는 탭을 문자 그대로hp:t에 넣었다. 한컴은 탭을hp:t안의hp:tab요소로만 읽고, 탭 문자가 있으면 문단 배치를 끝내지 못한다. 이제 모든 쓰기 경로가 탭을hp:tab요소로 쓴다. 메모리의 문서 트리는 그대로이고, 저장하는 XML만 바뀐다.라이브러리 안의 코드가 6.0에서 옮긴
HwpxDocument이름을 불러, 지금 API만 쓰는 사용자에게도DeprecationWarning이 나던 것을 고친다. 셀 맞춤(set_cell_text(fit=...), 메일 머지)의 글자·문단 모양 읽기와 글자 줄이기,lint_layout(required_fields=...), 변경 추적 점검(inspect_redline_structure), HWPX→Markdown 수집기가 이제 새 이름 (doc.fields.all,doc.tracking.*,doc.parts.headers,doc.text.markdown(rich=True))이나 문서 루트를 부른다.shapes.add_chart()가 계열 축이 없는 3D 차트(세 번째c:axId가0)를 축이 없다고 거부하던 것을 고친다.0은 축이 없다는 표시라, 이제 나머지 두 축 id만 정의돼 있으면 받아들인다. 한컴은 이런 차트를 축 id 두 개짜리와 같게 그린다.shapes.add_drop_cap(paragraph=...)과 문단의add_drop_cap()이 문단 첫 글자 장식을 문단 텍스트 뒤에 붙여, 한컴이 장식을 문단 마지막 줄에 그리던 것을 고친다. 이제 장식은 한컴처럼 문단 첫 글이 든 run 안, 그 글(hp:t) 바로 앞(앞선 컨트롤 뒤)에 들어가 첫 줄부터 그려진다. 구역 첫 문단의 구역 설정은 그대로 맨 앞에 남는다. 장식을 담은 run은 첫 글자의 글자 모양(char_pr_id_ref) 대신 문단 글의 글자 모양을 쓴다. 큰 글자 모양이 그 run에 있으면 한컴이 첫 줄을 그 크기로 잡아 글이 장식을 감싸지 못했다.shapes.add_polygon()과 그룹 멤버ContainerMember.polygon()이 만든 다각형이 한컴에서 마지막 변 없이 열린 선으로 그려지던 것을 고친다. 한컴은 첫 꼭짓점을 끝에 한 번 더 써서 다각형을 닫는다. 이제 두 API도 그렇게 쓴다. 열린 선이 필요하면closed=False를 넘긴다.문단 글에 넣은 탭이 한컴에서 사라지던 것을 고친다.
add_paragraph("이름\t홍길동")같은 탭을hp:t와 나란한 run 자식hp:tab으로 썼는데, 한컴은 이런 탭을 열 때 버려서 글이 붙고 탭 정지도 효과가 없었다. 이제 한컴처럼 탭을hp:t안에 쓴다(<hp:t>이름<hp:tab/>홍길동</hp:t>).shapes.add_line()로 그린 가로선과 세로선이 한컴에서 보이지 않던 것을 고친다. 선의 외접 상자(orgSz·curSz)에 0인 변이 있으면 한컴이 선을 그리지 않는다. 이제 한컴처럼 두 크기는 각 변을 최소 1로 쓰고(가로선은 폭 × 1),sz와 시작·끝점은 그대로 둔다.Shape.resize()도 같게 쓴다.add_picture(align="CENTER")(와"LEFT"·"RIGHT")로 넣은 그림이 한컴에서 늘 왼쪽에 놓이던 것을 고친다. 글자처럼 취급하는 그림은 문단 정렬을 따르는데,hp:pos@horzAlign만 바꾸고 그림 문단은 양쪽 정렬로 두었다. 이제 그림 문단의 정렬도 같은 값으로 맞춘다.hwpx.tools.read_fidelity.resolve_run_spans()(와 이를 쓰는 판독 표면)가 위·아래 첨자를offset부호만 보고 판정하던 것을 고친다. 부호가 거꾸로여서(음수가 위로 올린다, DEV-028) 예전 python-hwpx 위첨자를 아래첨자로 보고했고,hh:supscript/hh:subscript요소를 보지 않아 한컴 자신의 첨자(offset 0)와 지금ensure_run(script=...)이 만드는 첨자는 아예 첨자로 보지 않았다. 이제CharProperty.is_superscript()/is_subscript()처럼 요소로 판정한다. 요소 없이offset만 있는 글자는 한컴의 글자 위치(올림·내림) 설정이라 첨자로 보지 않는다.hwpx.tools.toc_author.add_native_toc(headings=...)로 제목을 직접 준 목차가 한컴에서 열 때 항목과 링크를 잃던 것을 고친다.dirty기본값이 늘True라, 한컴이 목차를 다시 만들면서 개요 문단만 모았다. 이제dirty의 기본값은 제목을 자동으로 찾을 때True,headings를 직접 줄 때False다. 직접 넘기면 그 값을 쓴다.page.setup(columns=N)과page.set_columns()가 섹션의 단 정의는 그대로 두고 섹션 끝에 빈 문단과 단 컨트롤을 덧붙여, 이미 있는 본문이 1단으로 남던 것을 고친다. 이제hp:secPr옆의 섹션 단 정의(hp:colPr)를 제자리에서 고쳐 섹션 전체가 N단이 된다. 구분선(separator_type등)도 이 정의에 들어가고, 빠진 값은 한컴 기본값(SOLID,0.12 mm,#000000)으로 채운다. 특정 문단부터 단을 바꾸려면page.set_columns(paragraph=...)를 쓴다.page.setup(columns=...)은 같은 폭의 단 N개와 간격만 쓴다. 있던 구분선은 지운다.page.set_columns는 1~255 밖의 단 수를page-columns-invalid로 거부한다.문단에 넣는 단 컨트롤도 한컴 표기대로
id=""와sameSz="1"/"0"을 쓴다.
용지 방향을 한컴 표기로 쓴다.
page.setup(orientation="PORTRAIT")는 스키마 밖 값landscape="PORTRAIT"를 써서 한컴이 가로로 그렸고,set_page_size(orientation= "LANDSCAPE")에 가로 치수를 주면 세로로 그려졌다. 이제 세로는WIDELY, 가로는NARROWLY이고, 치수는 두 방향 모두 용지의 세로 치수(너비 ≤ 높이)로 저장한다 (NARROWLY는 한컴이 돌려서 그린다).page.setup,page.set_size,section.properties.set_page_size가 모두 같다.WIDELY/NARROWLY를 직접 넘기면 한컴 뜻 그대로(WIDELY가 세로) 읽는다. 예전에는WIDELY를 가로로 읽었다.page.set_size는 모르는 방향 값을page-orientation-unsupported로 거부한다.landscape가 없으면page_size.orientation은 스키마 기본값NARROWLY를 돌려준다. 한컴도 그렇게 읽는다.새
PageSize.drawn_width/drawn_height는 한컴이 그리는 쪽 크기다. 표 기본 폭, 머리말·꼬리말 폭,template_analyzer의 쪽·본문 폭,layout_preview의 쪽 상자가 이 크기를 쓴다. 그래서NARROWLY와 세로 치수로 된 가로 문서도 가로 폭으로 계산한다.
styles.ensure_run(script="sup"/"sub")가 만든 위·아래 첨자가 한컴에서 두 번 줄어들던 것을 고친다.hh:supscript/hh:subscript요소와 함께relSz 67·offset -30/+30도 썼는데, 한컴은 요소만으로 글자를 줄이고 올리거나 내린다. 이제 한컴 자신의 첨자처럼 요소만 쓴다(relSz 100·offset 0). 예전 모양의 글자 모양은 다시 쓰지 않고, 기준 글자 모양으로 받으면 기본값으로 되돌린다.한컴이
hp:t안에 넣는 탭·줄 바꿈·특수 공백(<hp:t>성<hp:fwSpace/>명<hp:tab/>홍길동</hp:t>) 뒤의 글자를paragraph.text·run.text·doc.text.plain()·Markdown 내보내기· 표 이름표 찾기가 버리던 것을 고친다. 이들은hp:t의 첫 글자 묶음만 읽어 위 예에서 “성”만 돌려줬다. 이제 모두hwpx.tools.text_extractor와 같은 규칙으로 읽는다: 탭\t, 줄 바꿈\n, 줄 안 공백 U+00A0, 전각 공백 U+3000, 하이픈 U+00AD, 그 밖의 자식 요소는 제 글자와 뒤 글자(형광펜·변경 추적 표시는 빈 요소라 뒤 글자만). 텍스트 추출기도hp:t안의 탭을 버리던 것을 고친다(목차 항목의 점선 탭 뒤 쪽 번호가 붙어 나왔다). 변경 추적으로 삽입한 글(hp:insertBegin뒤 글자)도 이제paragraph.text와 평문에 나온다.doc.styles.ensure_run()이 만든 글자 모양(hh:charPr)의 자식 순서가 OWPML 스키마(CharShapeType)를 어기던 것을 고친다.bold·italic·underline·strikeout을outline·shadow뒤에 덧붙였고,bold를italic보다 먼저 썼다. 이제 서식을 적용한 뒤 자식을 스키마 순서(fontRef, ratio, spacing, relSz, offset, italic, bold, underline, strikeout, outline, shadow, emboss, engrave, supscript, subscript)로 맞춘다. 한컴이 쓰는 순서와 같다.doc.refs.add_hyperlink()로 만든 링크가 한컴에서 아무 데도 가지 않던 것을 고친다. 대상 주소를fieldBegin@name에만 썼는데, 한컴은 링크 대상을 필드 매개변수에서 읽는다. 한컴에서 다시 저장하면 링크는 남지만Category=HWPHYPERLINK_TYPE_HWP만 붙고Command가 없는(대상 없는) 문서 안 링크가 됐다. 이제 한컴이 쓰는 모양 그대로Command(: ? ; #백슬래시 이스케이프 +;1;0;0;, 메일;2;0;0, 책갈피?이름;0;0;0;)·Path·Category·TargetType·DocOpenType을 쓰고,fieldid는 한컴 하이퍼링크 컨트롤 id(627600491)로 둔다.#이름을 주면 그 책갈피로 가는 링크가 된다. 옛 판독기를 위해@name에도 주소를 그대로 둔다.링크 대상을 읽는 곳이 서로 달랐던 것을 맞춘다.
paragraph.hyperlinks와 Markdown 내보내기는@name만 읽어 한컴이 만든 링크의 주소가 빈 값이었고, 주석 달린 텍스트 추출은Command를 이스케이프와;1;0;0;꼬리가 붙은 채로 돌려줬다. 이제 셋 다Path→Command(꼬리·툴팁 제거, 이스케이프 풀기) →@name순으로 같은 값을 돌려준다.doc.page.set_page_number(format="page/total")이 “쪽/전체 쪽수” 대신 “쪽/쪽”을 그리던 것을 고친다. 두 번째 번호도 현재 쪽(numType="PAGE")으로 써서, 3쪽 문서가 1/1, 2/2, 3/3으로 나왔다. 이제 한컴 문서의 쪽/전체 머리말·꼬리말처럼/뒤에hp:autoNum numType="TOTAL_PAGE"를 쓰고(1/3, 2/3, 3/3), 쪽 번호 위치 컨트롤(hp:pageNum)은 한 번만 넣는다.hwpx.tools.toc_author.add_native_toc()를 기본값(at_index=0)으로 부르면 목차가 구역 설정(hp:secPr)을 가진 첫 문단 앞에 들어가, 한컴이 목차를 별도 구역으로 읽던 것을 고친다(목차 뒤에 쪽 나눔이 생겼다). 이제hp:secPr와 구역 처음부터 적용되는 조판 부호(단·머리말·꼬리말·쪽 번호)는 새 첫 문단의 맨 앞 run으로 옮겨 가고, 목차는 그대로 문서 맨 앞에 선다.doc.styles.apply_paragraph_format()·apply_list_format()처럼 문단 모양 (paraPrIDRef)을 바꾸는 편집이 그 문단의 줄배치 캐시(hp:linesegarray)를 남기던 것을 고친다. 한컴은 줄 높이를 이 캐시에서 가져오므로, 행간을 130%로 바꿔도 예전 160% 줄 높이로 그려졌다. 이제 문단 모양이 바뀐 문단의 캐시만 지우고, 글자 모양을 바꿀 때처럼 다른 문단의 캐시는 그대로 둔다.add_chart가 축(c:axId·c:catAx·c:valAx)이 필요한 차트 종류(막대·꺾은선· 영역·분산형·방사형·거품형, 3D·주식형·표면형 포함)에 축이 없으면HwpxValueError(shape-chart-axes-missing)로 거부하고 축을 넣는 방법을 안내한다. 한컴은 축 없는 막대 차트를 막대 없이 빈 차트로 그리고, 나머지 종류는 렌더하거나 저장하다 멈췄다. 같은 차트에 축만 넣으면 정상이었다. 축이 없는 원형·도넛 차트는 전처럼 받는다.암호가 걸린 HWPX를 열면
Start tag expected, '<' not found같은 파일 손상처럼 보이는 lxml 오류만 나오던 것을 고친다.META-INF/manifest.xml이 파트를encryption-data로 암호화 선언한 경우 예외 타입(XMLSyntaxError)은 그대로 두고 메시지가 문서 암호 해제 후 재저장을 안내하며, 원래 lxml 오류는__cause__로 남는다(HWP v5 안내와 같은 방식). 계기: HWPX 1,395건(실업무 문서· 공개 픽스처·저작 산출) 열기 기준선에서 유일하게 열리지 않던 파일이 한컴오피스 한글 13.0.0.711이 저장한 암호 문서였다.쪽 종류가 다른 머리말·꼬리말을 함께 쓰면 한컴에서 일부가 사라지던 문제를 고친다. 머리말·꼬리말을 설정하면 한컴이 읽는
hp:ctrl사본을 같은 쪽 종류만 바꾸도록 했다. 예전에는 그 태그의 사본을 모두 지웠다. 그래서 홀수 쪽 꼬리말 뒤에set_page_number(양쪽 꼬리말)를 부르면 홀수 쪽 글자가 한컴에서 사라졌다. python-hwpx만 읽는hp:secPr사본에는 글자가 남아 있었다. 또BOTH사본을ODD·EVEN보다 앞에 둔다. 한컴은 쪽마다 문서 순서상 마지막으로 해당하는 컨트롤을 그려서, 양쪽 꼬리말이 뒤에 있으면 홀수 쪽 꼬리말은 한 번도 그려지지 않았다.remove_header·remove_footer도 이제 지정한 쪽 종류의 사본만 지운다.메모 필드가 한컴에서 저장된 뒤 범위 끝과 작성 시각을 잃던 문제를 고친다.
attach_memo_field는 MEMOfieldBegin에 메모마다 양의zorder를 주고,CreateDateTime을 한컴과 같은 UTC ISO 8601(2024-12-02T09:00:00Z)로 쓴다. 시간대 없는 값은 로컬 시각으로 보고 UTC로 바꾼다. 한컴에서는zorder가 없거나-1이면 저장 때fieldEnd가 빠졌고,2024-12-02 09:00:00형식은 열 때 1601년 값으로 바뀌었다. 매개변수 개수 속성도 다른 필드처럼 한컴 표기cnt로 쓴다(한컴은 저장하며count를cnt로 바꿔 썼다).insert_document(..., after_paragraph_index=-1)는 대상 절의hp:secPr을 둘째 이후 문단에 남겼다. 한컴은 이를 새 절의 시작으로 읽어, 끼운 문단을 기본 쪽 설정의 별도 절로 열고 쪽을 넘겼다. 이제 절 설정(hp:secPr,hp:ctrl/hp:colPr)을 새 첫 문단으로 옮기고, 병합 보고서에sectionPropertiesRelocated를 남긴다.문서 병합은 여러 문단에 걸친 필드(한컴 양식의 누름틀 등)의
fieldEnd를 옛 id에 남겨 짝을 끊었다. 이런 병합 결과는 한컴이 열지 못하거나 저장할 때fieldEnd를 버렸다. 이제 id 대응을 복사한 문단 전체에서 맞추고,fieldEnd의fieldid는 자기fieldBegin을 따른다.
[6.5.0] - 2026-09-21
기존 문서 편집과 저작 표의 보존 범위를 넓힌 릴리스입니다. 저장 성공만으로 시각 품질을 보증하지 않으며, 과거 셀 채움 페이지 흐름 사례 두 건의 글자 가림은 이번 버전에도 남아 있습니다.
추가
표 높이와 텍스트 흐름 설정을 공개해 긴 저작 표가 다음 페이지로 이어지도록 한다.
혼합 run·필드·여러 story의 요청 범위를 보존하면서 편집한다.
고침
EqEdit의 공식
TRIANGLE토큰과 실문서에서 관측된 소문자triangle토큰을\triangle로 변환한다. 미리보기 MathML에서도 삼각형을 식별자 문자열이 아닌 수학 연산자 기호로 보존하며, 혼합 대소문자나 더 긴 식별자는 해석하지 않는다. 따옴표 리터럴(\text{...}) 안의\triangle은 그대로 문자로 남기고, 저작 방향(latex_to_eqedit)은 실한컴 검증 전까지 문서화된 대문자TRIANGLE만 낸다. (@kilomanyo, #93)
[6.4.0] - 2026-09-09
기존 문서 편집 보존 트레인. 구조 검증은 한컴 화면의 잘림·겹침·페이지 수 보존을 뜻하지 않는다.
고침
네이티브 머리글/바닥글과 빈 필드, 복제 행 문단 ID, XML 네임스페이스 정규화, 상속 셀 여백을 보존하도록 수정했다.
표 라벨 탐색에서 병합 셀을 물리 셀 하나로 계산하고, 방향 이동이 병합 영역을 건너뛰도록 수정했다. 실제로 다른 셀에 반복된 라벨은 계속 모호한 대상으로 처리한다.
주요 사용 문서와 AI 참조 문서를 현재 네임스페이스 API에 맞췄다. 저장 성공, 요청한 내용의 반영, 화면 검증의 의미를 구분한다.
바꿈
ChatGPT 안내를 wheel 업로드 경로로 교체 — ChatGPT 채팅의 파이썬 환경은 PyPI 접근이 막혀
pip install python-hwpx프롬프트가 더 이상 동작하지 않는다. README(한/영)의 따라 하기 프롬프트와 “어디서 쓰나” 표를 GitHub Release에 첨부된py3-none-anywheel을 문서와 함께 올려 오프라인 설치하는 경로로 바꾸고,docs/ai-assistants.md(사람용 3단계·에이전트용 지시문·lxml 의존성·최소 예제)를 신설했다.docs/installation.md에 wheel 설치 절을,llms.txt에 PyPI 없는 샌드박스의 설치 문장을 추가했다. Release 자산 첨부는 기존release.yml이 이미 수행하므로 CI 변경은 없다.에이전트 학습 표면을 6.0 API로 현행화 —
llms.txt와 llms-full 원천 (quickstart·recipes-traversal·mutation-semantics)이 6.0에서 이동돼DeprecationWarning을 내고 7.0에서 제거되는 5.x 평면 이름 (replace_text_in_runs·iter_runs·add_memo_with_anchor·add_footnote·remove_paragraph·list_form_fields·add_form_field·fill_form_field·add_equation·find_runs_by_style·memos·memo_shapes)을 안내하고 있었다.doc.text/doc.notes/doc.fields/doc.shapes네임스페이스와paragraph.remove()로 바꾸고,add_heading을 python-docx 습관으로 오기한 문장을 정정했다. 문서 예제 원장과 게이트 해시를 갱신했다(블록 수 117 불변). 동봉 계약 문서(src/hwpx/data/contract_docs/)도 같이 재동기화했다.
더함
Shape.set_position과Table.set_cell_borders를 안정 API에 추가했다. 기존 개체 속성·셀 배경·테두리 굵기를 보존하며 모호한 구조를 거부한다.기존 양식을 임시 파일에 편집한 뒤 재개봉·내용 확인 후 출력하는
examples/edit_existing_form.py와 외부 문서 편집 회귀 검증. 기존 값 사전조건으로 잘못된 대상이나 변경된 양식을 거부할 수 있다.wheel을 GitHub Actions artifact로도 유지 — ChatGPT의 GitHub 커넥터가 Release 자산 바이너리는 못 받고 Actions artifact는 받을 수 있는 것으로 확인되어, 릴리스 워크플로가 wheel과
SHA256SUMS를 artifactpython-hwpx-wheel로 올리고, 신설wheel-artifact.yml이 매달·릴리스마다 최신 Release의 wheel을 해시 검증 후 같은 이름으로 다시 올린다(artifact 90일 만료 대응). 빌드하지 않고 발행된 bytes만 재게시하며, 채팅 런타임까지의 전 과정 실측은 아직이라 안내에 실험 경로로 표시했다.
[6.3.0] - 2026-08-19
더함
왕복 충실도 공개 지표 (
docs/corpus-metrics.md신규 절 +docs/roundtrip/영수증) — 실한컴 저장본 76건(provenance 심사, 정직 제외 3건) 무편집 재직렬화에서 실질 구조 변화 0(byte-identical 22 · zip-container-only 54), 실한컴(Mac 12.30.0 build 6446) 재개봉 판정 가능 73/73 무손상, 음성 대조 2/2 정상 거부. 분모 루트를tests/datagold pair·패키지 동봉 Skeleton까지 확장 (scripts/roundtrip_fidelity.py). 상주 회귀tests/test_roundtrip_fidelity_corpus.py가 “substantive 0”을 매 스위트 게이트로 강제한다.stale lineseg 꼬리 커버리지 판정 — 기존 범위 검사(textpos>len)가 못 보던 방향(편집으로 문단이 캐시 줄 수 너머로 자란 경우 = 변경추적 겹침 렌더의 실물 방향)을 layout lint/패키지 검증기가 검출한다. 꼬리 폭을 overflow lint와 같은 보수 추정으로 재고 줄 폭 2배 초과만 error(실한컴 저장본 76건 캘리브레이션 오검출 0). 실한컴 렌더 겹침 재검은 opt-in 스모크
tests/test_tracked_overlap_render_smoke.py(HWPX_MAC_ORACLE_SMOKE=1).reorder_rows표 구조 op (apply_table_ops) — 물리 행 재배열(완전 순열rowAddr 재기입). 편집기 [표>정렬…] 역매핑 갭의 엔진 프리미티브. 수직 병합(rowSpan>1) 표는 typed refuse(바이트 무변경). 정렬 키 비교는 호출자 몫.
고침
instid 속성명 카멜케이스 불일치 (#88) — 스키마·실한컴 산출물의 정본은 소문자
instid(픽스처 전수 744 vs 0)인데 코드 6곳이instId를 읽거나 썼다. 문단 복제 시 인스턴스 ID 재발급 분기가 영원히 안 타던 사문 수리(중복 instid 복제 해소), 각주/미주 저작이 스키마에 없는 속성명을 방출하던 것을 정본으로 교정, 리더 4곳은instid우선 + 과거 자사 산출물(instId) 폴백. JSON 투영 키(instId)는 출력 계약이라 불변.hp:audio인라인 분류 정리 (#89) — 실한컴(Windows 한글 2024)의 소리 삽입 실물은hp:ole(EMBEDDED/ICON)이고hp:audio는 생성되지 않으며 실코퍼스 표본 0건. 빈도 0 요소는 읽기측 불투명 보존만 한다는 원칙에 따라INLINE_OBJECT_NAMES에서 제외(GenericElement 보존, 무손실 불변).
[6.2.1] - 2026-08-19
고침
v6.2.0은 보존된 실패 태그입니다 — 워크스페이스 편의용
work심볼릭 링크(정본 체크아웃의work/를 가리키는 상대 링크)가 릴리스 트레인에 추적돼 들어갔고, CI 러너의 경로 구조에서는 그 상대 경로가 자기 자신을 가리키는 순환 링크가 되어test_emit_loss_inventory가OSError: Too many levels of symbolic links로 실패했다(1 failed/3107 — prepublish 게이트가 발행을 정상 차단)..gitignore의work/패턴은 디렉터리만 매칭해 링크 파일을 못 막던 것이 근본 원인 — 링크를 언트랙하고 패턴에work(슬래시 없음)를 보강했다. 태그는 불변 원칙대로 보존한다(v2.29.0 전례). 6.2.1은 그 외 6.2.0과 동일하다.
[6.2.0] - 2026-08-19 (보존된 실패 태그 — PyPI 미발행)
더함
HwpxOxmlParagraph.add_index_mark(first, second=)— 색인 표시 (hp:indexmark) 저작. 실한컴 GUI 프로브 gold 2건 역설계.add_title_mark와 타겟팅 계약은 같으나(호출자가 대상 문단을 직접 지정 = 편집기의 캐럿 문단) 배치가 다르다 — titleMark는hp:t안, indexmark는 같은 run 안의hp:ctrl형제로 텍스트 앞. 스키마는firstKey/secondKey를 둘 다 필수 시퀀스로 선언하지만 실물은 1단계 색인에서secondKey를 생략하므로 실측을 따랐다(add_new_num의autoNumFormat생략과 같은 부류).HwpxOxmlParagraph.add_mail_merge_field(name, cached_text=)— 메일머지 표시 필드(hp:fieldBegin type="MAILMERGE") 저작. 기존hwpx.tools.mail_merge.merge_template_rows가 기존 텍스트를 치환하는 Edit인 것과 달리 필드 컨트롤 자체를 만드는 Create다. 파라미터 이름Fiexde는 실물·한컴 공식 문서 공통의 오타 철자라 그대로 쓴다. 캐시 텍스트 기본값{{이름}}은 그 배치 생성기가 인식하는 플레이스홀더 문법이라 이 필드로 저작한 템플릿이 곧바로 물린다.doc.parts.set_license_mark(...)/.remove_license_mark()— 문서 수준 라이선스 레코드(hh:licensemark) 저작. “입력 > CCL 넣기…”가 남기는 그 레코드이며, 눈에 보이는 배지는 별개(라이선스 증서 URL을href로 단 그림).openrate v22 (
docs/openrate/report-v22.json) — 위 세 표면의 실한컴 렌더 배치: 3 스트라텀 × 5건, macOS Hancom GUI 오라클 15/15 render_checked·0 failed, 음성 대조 3/3 정상 거부. 각 스트라텀에 관측 밖 경계 프로브 1건씩(한 문단 색인 표식 2개·한글 필드명·미관측 라이선스 type 문자열"KOGL")을 일부러 실었고 전부 수용됐다 — 실한컴의 수용 경계가 관측 분포보다 넓다는 기존 v10/v11 관찰의 재확인. 원장은hp:indexmark/hp:firstKey/hp:secondKey/hh:licensemark를by-openrate-corpus로 승격했고,hp:fieldBegin은 공유 저수준 요소 원칙(누름틀 v15 전례)대로 매트릭스 산문 근거만 갱신했다.
고침
LicenseMark.type이int→str— 스키마(Header XML schema.xml의DocOptionType)가xs:unsignedInt use="required"로 선언해 그대로 믿었으나 실한컴이 실제로 쓰는 값은 문자열"CCL"이다. 그래서 실한컴이 만든 CCL 문서를to_model()로 읽으면ValueError: Invalid integer value: 'CCL'로 터졌다 — 위 저작 표면을 열면서 실측으로 드러났다.flag/lang은 관측값이 정수라 그대로다.
[6.1.0] - 2026-08-15
15개 작업 사이클(6.1~6.15)을 하나의 트레인으로 묶어 발행한다. 완전성 감사 (2026-08-04)가 지목한 갭을 순서대로 닫아나가면서, 그 과정에서 두 개의 새 계측기(편집기 표면 인벤토리·편집기 메뉴 역방향 지도)를 만들어 남은 갭을 스스로 찾아내는 체제로 전환했다. 저작 표면 하나하나는 실코퍼스 리버스가 먼저이고, 실한컴 오라클(Windows COM 또는 macOS GUI) 렌더 검증이 뒤따르는 원칙을 전 트레인에서 유지했다 — 아래 각 항목은 그 순서를 따라 서술한다. 이 트레인은 이 브랜치가 갈라져 나온 뒤 별도로 발행된 6.0.3의 보안 수정 (GHSA-8g8m-xpm4-wxvx, 아래 [6.0.3] 항목)을 그대로 포함한다 — 병합으로 합류했을 뿐 내용은 손대지 않았다.
더함
doc.styles.ensure_font— 폰트 선언·치환 저작. 완전성 감사가 지목한 최대 갭(#1): 모든 실문서가hh:fontfaces를 갖는데도 이제까지 그 블록은 frozen 스켈레톤 출력이었다. 언어 블록 7종을 기본 등록하고ensure_style과 같은 dedupe 관용구를 따르며,substFont는 실코퍼스 그대로 — 임베딩이 아니면binaryItemIDRef속성 자체를 쓰지 않고(1682/1682 관측)substFont값은 항상 빈 문자열(284/284),isEmbedded는 “0”/”1” 스펠링 (6741 관측).fontfaceid 번호가 언어 블록마다 독립적으로 매겨진다는 실측도 고정했다.탭 정지 저작·읽기 —
apply_paragraph_format(tab_stops=...). 감사 갭 #2. 스키마는tabPr당tabItem최대 1개를 암시하지만 실 정부 문서는 위치정렬 최대 4개를 한tabPr에 담는다 — 코퍼스가 스키마를 이겼고 저작은 문서를 따른다. auto-tab 불리언도 같은 “0”/”1” 스펠링.layoutCompatibility·compatibleDocument·settings.xml읽기 표면 — 감사의 R1 반박(모든 실문서에 있는데도 코드가 전혀 못 읽던hh: layoutCompatibility)을 닫는다.settings.xml은 벤더드 스키마 어디에도 선언이 없어 177개 실문서로 구조를 역설계했고(config:config-item어휘는 OASIS ODF 1.0을 그대로 재사용),layoutCompatibility는 스키마가 48개 플래그를 선언하지만 실문서 전부 비어 있어 관찰된 자식 이름 집합만 보존한다. 의도적으로 읽기 전용 — 쓰기 API가 없으므로 OPC 계층의 무손실 바이트 보존이 그대로 적용된다(3개 실픽스처로 확인).도형 텍스트(draw-text)·개체 캡션 — 감사 갭 #3·#4.
.draw_text/.set_draw_text(rect·ellipse 호스트, 스키마가 도형 개체로만 제한)와 표·그림·도형이 공유하는.caption/.set_caption/.remove_caption. 실코퍼스는 스키마와 반대로shadow가drawText보다 먼저 오고, 캡션 배치는 호스트에 관계없이 “outMargin 다음”으로 일관됐다.하이라이트(형광펜) —
doc.text.highlight/.highlights.markpenBegin/End를 LIFO로 페어링해hp:t내부에 삽입한다. 벤더드 코퍼스의 유일한 실 예시가 스키마상 잘못된 위치(hwpxlib 오류-회귀 사례)이므로 그 기형 원본은 “읽기-비어있음”으로 그대로 두고 신규 저작 경로만 정상 계약으로 검증했다.이미지·그라디언트 채우기 —
ensure_border_fill이 스키마의fillBrush선택지(winBrush/gradation/imgBrush) 전부를 커버하게 됐다.fill_image=(doc.media경유, 실측 전량TOTAL모드)·fill_gradient=(LINEAR기본, alpha “0” 관측)가fill_color=와 상호배타 옵션으로 합류했고, 표 셀 배경set_cell_fill_image/set_cell_fill_gradient까지 실제 소비 경로가 이어진다.메모 도형 저작 —
ensure_memo_shape.hh:memoPr을 find-or-create dedupe로 등록한다. 실코퍼스 다수 기본값(폭 15591·SOLID·공유 3색 프로필)을 따르고, id는 1부터 시작(borderFill의 0과 다름),memoType은 “NOMAL” 스펠링을 스키마·실문서 그대로 유지한다.페이지 번호 재시작·페이지별 요소 숨기기 —
restart_page_number(hp:newNum)·hide_page_elements(hp:pageHiding).newNum은 스키마가 요구하는autoNumFormat자식 없이 자기닫힘으로 실코퍼스 그대로 방출한다(편차 등재).위·아래 첨자 실요소, 양각·음각, 외곽선 —
ensure_run확장. hwpxlib 실코퍼스(오류 문서 charPr id=513)에서 실한컴이 위첨자를relSz/offset수치가 아니라hh:supscript플래그 요소만으로 판정함을 확인해, 기존 offset 부호 계약(실한컴 렌더 검증으로 이미 확정됨)은 그대로 두고 실요소를 병행 방출한다. emboss·engrave·outline은 스키마 근거로 추가.필드 매개변수 일반 모델(
ParameterList)·타입 있는 콤보박스listItem—hp:parameters(필드 클릭 액션)와hp:parameterset(도형 확장 속성)가 스키마상 같은 재사용 타입임을 실코퍼스로 확인하고 범용 파서·직렬화기로 승격했다(불리언·정수·실수·문자열·리스트 매개변수 재귀 포함). 처음엔 읽기 모델만 있고 실제 문서 dispatch에 연결이 안 돼 있던 진짜 갭이었음이 이후 사이클에서 드러나 바로 이어붙였다(아래 고침 참조).hp:compose(글자 겹치기·원문자)·재귀InlineObject승격 —ComposedCharacter가GenericElement대신 타입 있는 필드로 노출된다. 실코퍼스로 위치가 스키마와 다름을 확정했고(hp:t자식이 아니라hp:run직속), 컨테이너 등 내부에 중첩된 도형도 재귀 깊이와 무관하게InlineObject로 인식되도록 확장했다.doc.shapes.add_polygon— 다각형 저작. 실코퍼스(hwpxlib_corpus/ reader_writer__SimplePolygon.hwpx) 리버스: 꼭짓점은hc:pt(core 네임스페이스, rect·ellipse와 같은 기하 네임스페이스 계약)이고 자기 bbox 좌상단 원점 로컬 좌표계에 산다(정점 목록이 그 자기orgSz와 정확히 일치, 실측) —points_mm로 받은 페이지-스페이스 좌표를 그 로컬 좌표계로 평행이동한다. 기존resize()는 이미 범용(pt로컬명 스캔)이라 변경 없이 다각형에도 적용된다. curve(hp:curve/hp:seg)는 정직 보류: 유일한 실 예시에서 곡선의orgSz가 앵커점 bbox보다 뚜렷이 크다(스플라인이 앵커점 밖으로 부풀어 오르는 실측) — 한컴의 정확한 곡선 적합 알고리즘 근거 없이 bbox를 추정하면 침묵 오류가 되므로 미룬다.doc.shapes.add_arc— 사분원(호) 저작. 실코퍼스(hwpxlib_corpus/ reader_writer__SimpleArc.hwpx) 리버스: 스키마상hp:arc는 좌표 3점 (center/ax1/ax2)뿐 각도 필드가 없다. 유일한 실 예시는center가 자기 bbox 모서리에 앉고ax1이 바로 아래,ax2가 바로 오른쪽인 사분원 하나뿐이라 그 배치만 점 단위로 검증됐고, 나머지 세 모서리는 다른 도형이 이미 쓰는hp:flip미러링으로 얻는다(corner인자 —TOP_LEFT만 실측, 나머지 3개는 유도).arc_type(NORMAL/PIE/CHORD)은 스키마 열거값 그대로 통과한다. connectLine은 정직 보류: 유일한 정본 예시(SimpleConnectLine. hwpx)는 자유선이 아니라subjectIDRef로 다른 두 도형을 잇는 “스마트 연결선”이고,offset이 음수·curSz≠orgSz·scaMatrix가 평행이동까지 얹은 비항등 행렬이라 관계식을 재현할 근거가 없다.파트 계층 읽기 모델 —
version.xml(HcfVersion)·masterpage.xml(MasterPage)·history.xml(History) 읽기 승격(감사 갭 #15).settings.xml관용구 그대로:doc.parts.*가 돌려주는HwpxOxml {Version,MasterPage,History}에.to_model()추가. 실 산출물 47/47(version)·다수(masterpage, 서로 다른 두 문서 계열 110건) 전수가 벤더드 2024 초안 스키마와 루트 이름·네임스페이스가 다르다는 걸 재확인했다(스키마가 아니라 실코퍼스가 진실 원천 —version.xml루트는hv:HCFVersion이고 오탈자tagetApplication을 그대로 쓴다,masterpage.xml루트masterPage는 네임스페이스가 없다).history.xml은 실 예시가 하나도 없어(코퍼스+개인 실문서 6,262건 전수) 스키마 전용으로 표기 — 평평한 리비전 메타데이터만 타입 있는 필드로 옮기고, 재귀 중첩되는 diff 본문은DiffNode로 원문 구조 그대로 보존한다.그룹 개체(컨테이너) 저작 —
doc.shapes.add_container. 실코퍼스 74개 컨테이너(벤더드 3개 + 오류-회귀 픽스처 71개) 리버스: 멤버는 독립 도형과 동일한 offset/orgSz/curSz/flip/renderingInfo 계약을 갖되AbstractShapeObjectType꼬리(sz/pos/outMargin/shapeComment)를 그룹이 대신 갖고groupLevel="1"이 된다. 컨테이너 자신의orgSz는 멤버들의 합집합 bbox. 알려진 한계도 그대로 문서화했다:resize()가 컨테이너 자신의 크기만 갱신하고 멤버 좌표는 안 건드린다.특수 인라인 텍스트 원자(줄바꿈·전각공백·고정폭공백) —
add_run(expand_special_characters=True). 세 마커 모두hp:t내부 혼합 콘텐츠로 중첩되는 실코퍼스 관용구를 그대로 재현한다(스키마가 허용하는hp:run형제 배치는 실사용 0건). 저작과 별개로 읽기 쪽 실버그도 같은 트레인에서 발견·수리했다(아래 고침 참조).hp:switch/case/default읽기 모델 — 스키마 선언이 전혀 없는데도 실코퍼스 236/237이 쓰는 버전-호환 래퍼.paraPr의margin/lineSpacing이 이 래퍼 안에 있으면None으로 읽히던 실버그를hp:case-우선/hp:default- 폴백 읽기로 수리했다(아래 고침 참조).tabPr쪽은 두 분기가 서로 다른 값(정확히 2배 관계)을 가져 정반대로hp:default를 우선해야 한다는 것도 별도로 확정했다.저수준 도형·컨트롤 이스케이프 해치의 정직한 검증 신호 —
add_shape/add_control로 필수 자식이 빠진 도형이나 빈hp:ctrl을 만들면 실한컴이 열지 못하는데도validate_package/validate_editor_open_safety둘 다 통과시키던 갭. 경고 레벨 신호를 추가했다(오류로 만들지 않음 — 여러 호출에 걸친 점진적 조립이라는 이 이스케이프 해치의 문서화된 사용법을 깨지 않기 위해서다). 컨테이너 멤버는 이 5개 자식이 없는 게 정상이라 스캔에서 제외한다.문서 옵션·호환성 설정 저작 —
hh:compatibleDocument/@targetProgram(실측 47/47 “HWP201X” 고정)·layoutCompatibility플래그·hh:docOption/ linkinfo·hh:paraPr/autoSpacing까지 쓰기 경로를 열었다. 모든 불리언은 “0”/”1” 관용구(다른 OWPML 불리언 계열과 동일).hp:label(양식 라벨·명패) 읽기+쓰기 — 개인 실문서(학교 행정서식) 75건 리버스: 항상hp:tbl의 마지막 자식, 스키마 11개 속성 전부 사용, 실측은 정확히 2가지 조합(2×9 소형 라벨시트 325건/1×2 대형 명패 111건)으로 수렴하지만 저작은 그 두 조합에 값을 제한하지 않는다(실증 없는 값을 기계적으로 금지하지 않는다는 원칙).문서 삽입·병합(
append_document/insert_document) — 다른 HWPX 문서의 본문을 열린 문서에 복사하면서 헤더 소유 공유 자원(charPr/paraPr/ style/borderFill/tabPr/numbering/bullet/memoPr/fontfaces)마다 새 대상 id로 재매핑해, 대상이 이미 그 번호에 갖고 있던 값에 조용히 앨리어싱되지 않도록 한다. v2에서 실측된 한컴 자신의 “문서 끼워 넣기” 4축 정책(글자 모양·스타일·문단 모양·쪽 모양 유지)을 named parameter로 노출한다 (기본값=v1의 이미 실한컴 검증된 동작, 비기본값은 타입 오류로 거부) — KeepStyle 축이 동일-이름 스타일 충돌 해소 규칙임을, KeepSection 축이 별도 섹션 파트 생성 여부임을 실 골드 2라운드로 확정했다. MEMO 병합 지원도 추가됐다(hp:memogroup이 문단이 아니라 섹션의 형제임을 확인 후). 이 저작 표면을 만드는 과정에서 여러 실결함을 발견해 고쳤다(아래 고침 참조).덧말·글자 겹치기(
add_dutmal/add_composed_character) 정식 완결 — 구현 자체는 앞서 들어와 있었고, 이번에 capability 영역 등록·편차 등재· 정식 테스트로 마무리했다.문서정보(document metadata) —
doc.parts.set_document_metadata/document_metadata(). 실문서 67픽스처 전수 조사: title/creator/subject/ keyword/lastsaveby는 요소는 있지만 대개 비어 있고,CreatedDate/ModifiedDate는 65/65 단일 ISO-8601 포맷(저작 대상)인 반면 자유형식date필드는 한컴 버전·로캘에 따라 5가지 이상 서로 다른 포맷이 관측돼 정직하게 저작을 보류한다(불투명 문자열 보존만, curve·connectLine과 같은 원칙).드롭캡 —
doc.shapes.add_drop_cap.dropcapstyle이hh:paraPr이 아니라AbstractShapeObjectType(모든 임베드 가능 도형의 공유 속성)에 있다는 걸 스키마 대조로 발견했다. 67개 픽스처 중 유일한 실사용 예시 (TripleLine)를 구조까지 리버스했고(투명hp:rect가 확대 글자를 감싼drawText/subList,horzRelTo=PARA/flowWithText=1,curSz가 항상 0/0인 센티널), DoubleLine·Margin은 표본이 없어 타입 오류로 거부한다.글자 방향(세로쓰기) —
doc.page.text_direction/set_text_direction.hp:secPr의textDirection(HORIZONTAL/VERTICAL/VERTICALALL)·textVerticalWidthHead를 노출한다. 67픽스처 74개secPr전부 HORIZONTAL이라 VERTICAL 계열은 실사용 전례가 없지만, 이 프로젝트 최초로 VERTICAL/VERTICALALL을 실제로 실한컴 렌더 검증까지 통과시켰다.단 나누기 —
apply_paragraph_format(column_break=...).hp:p자신의columnBreak속성으로, 기존page_break_before(hh:breakSetting이라는 공유 스타일 속성)와는 메커니즘이 다른 별개의 문단-인스턴스 속성임을 확인한 뒤 저작했다(실 73건pageBreak="1"계열로 동일 어휘 계약 확인).표 나누기·붙이기 —
apply_table_ops의split_table·merge_table. 전용 OWPML 어휘가 없어hp:tr/hp:tc/cellAddr/cellSpan순수 구조 편집으로 구현했고, 병합 셀이 분할 경계를 가로지르면 실패 폐쇄로 거부한다. 표 뒤집기는 정의가 불가능하다는 판단으로(병합 셀이 있을 때 어느 쪽이 내용을 갖는지 정할 근거가 스키마·코퍼스 어디에도 없음) 정직 보류한다.셀 균등화(행 높이·열 너비) —
equalize_column_widths()/equalize_row_heights(). 이미 있던set_column_widths/셀별set_size를 “균등화”라는 이름 있는 연산으로 노출했다. 병합 셀은 자신이 걸친 행·열의 합을 받는다.개요 번호매기기 —
ensure_numbering/apply_list_format의 세 번째 kind,"outline".hh:heading type="OUTLINE"이 리스트 서식과 같은hh:numbering/hh:paraHeadid-공간을 공유한다는 사실을 활용해 커스텀 번호 형식·시작값을 지정할 수 있게 했다.바탕쪽(master page) 쓰기 경로 —
doc.parts.add_master_page+doc.page.set_master_page/.master_page_refs. 유일한 실 샘플에서 파트 파일명·매니페스트opf:itemid·masterPage루트 자신의 id·섹션의hp:masterPage/@idRef가 모두 같은 문자열(“masterpageN”)을 공유함을 직접 확인해 재현했다. 마스터 페이지는opf:item은 받지만 spineitemref는 받지 않는다(읽기-순서 파트가 아니므로).날짜·교정 부호 필드 —
add_date_field(type="DATE")·add_proofreading_mark(type="PROOFREADING_MARKS_SIGN")(DEV-043). 실한컴 macOS GUI 프로브 골드(date_and_proofreading_mark.hwpx) 리버스. 스키마는PROOFREADING_MARKS를 선언하지만 실측은 열거형에 아예 없는 값PROOFREADING_MARKS_SIGN이다.PATH 필드 —
add_path_field(type="PATH"). GUI 프로브 없이 기존 실코퍼스 샘플(파일 이름 필드,Command=Format="$F")만으로 계약을 확보했다. 이 저작을 붙이며fieldEnd의@fieldid갱신 누락이라는 실결함을 함께 발견해 DATE·교정 부호·PATH 세 필드 전부 소급 수리했다 (아래 고침 참조).titleMark(차례 숨기기·제목 차례 표시) —
add_title_mark(*, in_toc: bool)(DEV-044). macOS GUI 자동화로는 캐럿을 임의 문단에 놓을 수 없어 한 사이클 넘게 저작이 보류돼 있었는데, Windows 박스 COMSetPos3- variant 프로브로 캐럿-문단 타겟팅 규칙이 확정되며 열렸다.in_toc=True→ignore="1",False→ignore="0"(이름의 직관과 반대인 극성을 macOS GUI·Windows COM 양쪽에서 독립적으로 확인). 필드 래퍼가 전혀 없는 유일한 마커 계열이라(DATE/교정 부호/PATH의ctrl/fieldBegin메커니즘과 다름) 새 run을 만들지 않고 기존 run의 첫hp:t안에 바로 삽입한다.
고침
문서 병합의
linkListIDRef과잉 거부 + 저장 경로 무결성 훼손 —hp:subList가 관용적으로 갖는 센티널 값(“0”, 벤더드 코퍼스 5,891/5,891) 을 실제 연결된 텍스트박스 체인으로 오판해, 표를 가진 모든 문서(이 라이브러리 자신이 생성한 문서 포함)를 병합 불가로 만들던 결함. 더 심각한 두 번째 결함이 같은 트레인에서 함께 드러났다: 병합이 대상 헤더 트리를 직접 변형하면서도target_header.mark_dirty()를 호출하지 않아, 그림이 껴 있지 않은 모든 병합이 메모리상 참조 무결성 검사는 통과하면서 실제로는 손상된 파일을 저장하고 있었다(재오픈 전까지 안 보임). 세 번째:hp:secPr제거 로직이 같은 run에 얹혀 있던 표·텍스트 까지 통째로 삭제하던 결함(빈 문서 스켈레톤으로만 테스트돼 있었다).문서 병합의
fieldEnd/@fieldid미갱신 —hp:fieldBegin·hp:fieldEnd둘 다 생성 시 같은fieldid를 갖지만, 병합 리프레시가fieldBegin쪽 id/fieldid와fieldEnd의beginIDRef만 재발급하고fieldEnd자신의fieldid는 건드리지 않아 원본uuid4값이 복사된 콘텐츠에 그대로 남아있던 결함. DATE·교정 부호·PATH 필드 저작에도 같은 결함이 있어 함께 소급 수리했다.문서 병합의 속성-클론 중첩 참조 앨리어싱 —
hh:paraPr내부 테두리의borderFillIDRef/tabPrIDRef,hh:charPr의 글자-테두리borderFillIDRef가 본문이 아니라 방금 복사된 헤더 항목 안에 살아있어 임포트 스캔에 안 잡히던 결함(참조가 매달린 상태가 아니라서 참조 무결성 검사도 통과했다). 무테두리 문서를 표 스타일(SOLID) 대상에 병합하면 표지 전체가 박스로 렌더되는 것으로 실증(실한컴 A/B 대조)한 뒤 수리했다.변경추적(추적된 삽입·삭제·교체) 줄 겹침 렌더 — run 교체 경로 (
apply_model)가 문단의 lineseg 캐시를 지우지 않아 한컴이 옛 줄배치를 재사용, 추가된 텍스트가 기존 줄 위에 겹쳐 렌더되던 실사용 제보를 재현·수리했다. 실한컴 오라클로 수리 전후 렌더를 직접 대조했다. 후속 감사로 문단 복제 경로 7종을 전수 지도화해 무방비 2종(document-merge·공개insert_paragraphs/copy_paragraph_range)의 lineseg 캐시도 클론 시점에 제거하도록 정리했다(항상 새 위치의 새 내용이므로 원본 절대좌표를 들고 있을 이유가 없다).hh:tabPr탭 정지가hp:switch로 감싸이면 통째로 안 읽히던 결함 (DEV-022) —parse_tab_definition이 직계 자식만 스캔해hp:switch안의hh:tabItem(실코퍼스 449건)을 전부 놓치던 결함.hp:case/hp:default두 분기가 실제로는 다른 값(정확히 2배 관계)을 갖는다는 걸 확인해hp:default를 우선하는 읽기 모델을 세웠다(같은 값을 공유하는paraPr의 case-우선 규칙과는 정반대). 저작 쪽의 자매 결함(ensure_ tab_definition의 dedupe 비교가 같은 맹점으로 중복tabPr을 만들던 문제)도 같은 커밋에서 함께 수리했다.add_section()이 메모가 앵커된 첫 문단에서 깨지던 결함 —hp:secPr이 항상 첫 문단의 물리적으로 첫 run에 있다고 가정하던 코드가, 메모(나 다른 필드)가 앞에 붙으면 실제로 존재하는secPr을 못 찾고 “양수 페이지 크기를 가진 섹션이 없다”는 오류를 내던 결함.paraPr비중복화 —ensure_paragraph_format이 호출마다 동일 내용의paraPr을 새 id로 계속 발급하던 결함(실한컴은 저장 시점에 중복을 자체 병합하지만 이 라이브러리는 하지 않고 있었다). 구조 비교 기반 find-or-existing으로 수리했다.특수 인라인 텍스트 원자 읽기 버그 —
TextExtractor가lineBreak/nbSpace/fwSpace를 재귀 순회에서 건너뛰기만 하고 대체 문자를 넣지 않아, 줄바꿈이 조용히 사라지고 앞뒤 텍스트가 그대로 붙어버리던 결함. 각 마커를 유니코드 대응(줄바꿈은 개행, 전각·고정폭 공백은 각각의 전용 공백 문자, 하이픈은 소프트 하이픈)으로 치환하도록 고쳤다.커버리지 원장(coverage ledger) 계측기 자체의 오분류 4종 — 독립 완전성 감사(2026-08-04)가 판정한 오분류(주석·문서화 문자열을 코드로 오인, 함수 인자로 전달된 태그를 못 따라감, 런타임 조립 마크에 근거 없는 화이트리스트 등)를 tokenize+ast 기반 스캐너·고정점 인자 추적기· 근거-필수 화이트리스트로 수리했다. 감사가 제시한 4개 오분류 사례 전부 수리 전/후 재현으로 확인했다.
정리 (측정 인프라)
census 모집단 재구성 — 커버리지 원장의 census를 재현 불가능한 옛 166파일 모집단에서 물러나, 벤더드 hwpxlib 코퍼스(47, 누구나 재현 가능) + 관리자 개인 실문서 집계(190, 경로·이름·내용 없이 개수만) = 237건으로 재구성했다. 두 모집단의 비대칭성을 census 문서 자체에 명시한다.
편집기 표면 인벤토리(신규 계측기) — coverage ledger가 재는 “요소 축”과 별개로 “기능 축”(사용자가 실제로 에디터에서 클릭하는 모든 기능)을 재는 새 문서(
docs/editor-surface-inventory.md). capabilities 등록 영역과 support-matrix 근거를 자동 교차해[엔진 상태]×[증거]× [실한컴 검증]을 도출한다. 이 인벤토리 자체가 자기 스캔으로 8개 실동작·미등록 기능(doc.page의 19개 메서드 전체 등)을 찾아냈고, 사이클 진행에 따라 미실측 행을 하나씩 닫아 이번 릴리스 시점에는 구조적으로 검증 불가능한shape-escape-hatch한 줄만 남았다(실한컴이 설계상 그 입력을 거부하므로 “렌더 검증”이라는 개념 자체가 성립하지 않는다).편집기 메뉴 역방향 지도(신규 계측기) — “한컴 메뉴 항목 → 우리 엔진 대응”이라는 반대 방향 질문에 답하는 새 문서(
docs/editor-menu-reverse- map.md). macOS 9개 메뉴 128개 항목을 전수 판정했다([대응 영역]/ [부분 대응]/[대응 없음=신규 갭]/[스코프 밖]/[미확인]). 여기서 나온 “대응 없음” 목록이 이번 릴리스의 신규 저작 표면(문서정보·드롭캡· 글자방향·단나누기·표나누기/붙이기·개요·바탕쪽·날짜/교정/PATH 필드· titleMark 등) 대다수의 최초 입력이 됐다.openrate 코퍼스 v5~v21 — 사이클마다 새로 연 저작 표면을 실한컴 오라클로(초기엔 Windows COM, v6부터는 macOS GUI) 배치 측정하는 시리즈를 17회 더 이어갔다. 매 배치마다 결정론(독립 재생성 2회 바이트 동일)· 정적 오픈-세이프티 사전필터·부정 대조군 거부를 확인했고, 6.11 사이클 종료 시점에 인벤토리의 미실측 행을 한 차례 0으로 닫았다(이후 신규 저작마다 다시 쌓이고 다시 닫히는 순환이 이어졌다).
편차 등재 DEV-002~044 — 스키마-대-실측 괴리를 재실행 가능한 프로브와 함께 43건 추가 기록했다(예:
tabItem카디널리티, connectLine의subjectIDRef가id가 아닌instid로 해석되는 점,hp:label의 실측 2-클러스터 수렴,hp:switch/case/default의 스키마 완전 부재, titleMark가 스키마에는 선언돼 있으나 문서화가 전무한 점). 별도로, 이전 조사 회차에서 미병합 상태로 남아 있던 편차 16건도 재검증을 거쳐 정식 등재했다.
[6.0.3] - 2026-08-11
보안
신뢰할 수 없는 HWPX를 읽을 때의 자원 고갈을 수정합니다(GHSA-8g8m-xpm4-wxvx, HIGH). 405 KB짜리 조작 파일 하나로
HwpxDocument.open()이 830 MB를 할당할 수 있었고, 비율은 공격자가 정합니다. 원인은 두 가지였습니다 — 멤버를 읽는 대부분의 경로가guard_zip_file()을 아예 호출하지 않았고, 가드가 도는 자리에서도 중앙 디렉터리에 적힌 크기만 검사해 실제 할당량을 제한하지 못했습니다.read_member()/read_zip_members()를 도입해 적힌 크기가 아니라 실제로 읽힌 바이트를 64 KB 단위로 세며 한도를 넘으면 중단합니다. 이제 할당량이 저장량이 아니라 선언 크기에 비례합니다.압축 데이터가 적힌 크기보다 큰 멤버(위조 크기), 데이터를 실은 디렉터리 엔트리, 드라이브 경로(
C:/·D:\·//server/share), STORED·DEFLATED 외 압축 방식 (bzip2·lzma는 CPython이 청크 읽기로 bound하지 못함)을 거부합니다.패치 계열·아카이브 CLI·진단 도구 등 신뢰할 수 없는 아카이브를 읽는 모든 경로가 첫 멤버를 읽기 전에
guard_zip_file()을 통과하도록 배선했습니다.
동작 변경: 위 경로들에는 지금까지 크기 제한이 없었습니다. 그래서
HwpxDocument.open()이 이미 거부하던 문서(멤버 128 MB 초과, 멤버 4096개 초과, 압축비 1000:1 초과)가 이제paragraph_patch()·hwpx-unpack등에서도 거부됩니다. 128 MB가 넘는 동영상·이미지를 담은 문서를 다루던 사용자는 영향을 받을 수 있으며,guard_zip_file(limits=...)로 조정할 수 있습니다.신고 및 수정: @Nekonic.
[6.0.2] - 2026-08-04
v6.0.1도 보존된 실패 태그입니다 — 아무것도 게시되지 않았습니다. prepublish의
mypy 게이트가 Q3b 구역 서식 setter의 루프 변수 재사용 타입 충돌 4건을 적발해
업로드 전에 중단했습니다(로컬 태그-전 점검이 core에서 타입체커를 생략한 것이
원인 — 체크리스트에 core mypy+pyright를 명문화). 변수 분리로 고친 동일 내용이
6.0.2입니다. 삭제·이동·재사용하지 않습니다.
[6.0.1] - 2026-08-04 (preserved failed tag — nothing published)
v6.0.0은 보존된 실패 태그입니다 — 아무것도 게시되지 않았습니다. prepublish의
공개 위생 게이트가 이번 트레인 신규 파일 3개(코퍼스 v4 생성기·왕복 하니스·
감사 판정문)에 유입된 내부 작업 코드네임을 적발해 업로드 전에 트레인을
중단했습니다. 코드네임을 공개 서술로 치환하고 동결 인벤토리를 75→74로 전진
(6.0 표면 재편으로 삭제된 파일의 역사 항목 1건 소멸)한 동일 내용이
6.0.1입니다. 삭제·이동·재사용하지 않습니다.
[6.0.0] - 2026-08-04 (preserved failed tag — nothing published)
엔진 완전성 트레인의 major 경계입니다 — 표면을 도메인 네임스페이스로 재편하고,
완전성을 측정할 구조물(커버리지 원장·편차 레지스트리·왕복 하니스·코퍼스 v4)을
세웁니다. 독립 감사 판정: “완전성에 도달한 버전”이 아니라 “완전성을 측정하고
채워 나갈 구조물”이 정확한 서사입니다(docs/2026-08-04-completeness-audit-verdict.md).
바뀜 (파괴적 — major 근거)
루트 표면 102 → 34. 이동한 79개 이름은 도메인 네임스페이스로:
doc.notes(각주·미주·메모) ·doc.fields(누름틀·체크박스·하이퍼링크) ·doc.shapes(도형·그림·수식·차트) ·doc.styles(문자·문단 서식, 스타일) ·doc.page(용지·구역) ·doc.tracking(변경추적) ·doc.text(검색·치환) ·doc.toc·doc.plan·doc.media·doc.parts. 5.x 루트 이름 전부가 행선지를 안내하는DeprecationWarningshim으로 7.0까지 동작합니다.반환 규약 단일화: 저작 메서드는 라이브 도메인 객체를, 결과 페이로드는
frozen dataclass(+to_dict())를 반환합니다 — dict/tuple/스칼라 반환 0.typed error: 공개 경로 100%가 kebab
code를 가진HwpxValueError/TypeError/LookupError/StateError로 던집니다(기존 builtin except 절과 이중상속 호환). 스타일 이름 오타는 호출 시점에style-not-found(+근접 제안)로, 없는 숫자 id도 저장 전에 거부됩니다.
추가됨
add_paragraph(style="개요 1")스타일 이름 해석과add_heading(level=)(styleIDRef +hh:headingOUTLINE 이중 방출).Q3b 요소 개방:
pageBorderFill(BOTH/EVEN/ODD 질의·저작), footnote/endnote 모양 5블록 부분 갱신,ensure_style(이름 기반 신규 스타일),set_visibility/set_line_numbers/set_grid.커버리지 원장(
docs/coverage-ledger.md, 345 요소 기계 재산출 — ⚠알려진 측정 오차 절 필독) · OWPML 편차 레지스트리 17항목 (specs/063-owpml-deviations/) · 왕복 충실도 하니스 · 코퍼스 v4 (실한컴 12.0.0.3288에서 170/170 개봉·170/170 렌더 검증,docs/openrate/report-v4.json).실측 비교표
docs/comparison-python-docx.md(대조군 리플렉션 실측, 지는 칸 5행 포함) · 이주 가이드docs/migration-6.0.md· 문서 예제 자립 실행 71/117.
제거 예고
legacy shim 79종과
section_index=파라미터는 7.0.0에서 제거됩니다.python -m hwpx.capabilities의surfaceShape가 shim 수와 제거 시점을 기계 판독 가능하게 노출합니다.
[5.8.0] - 2026-08-03
정직성 트레인입니다 — 새 저작 표면 없이, 라이브러리가 하는 말을 전부 참으로 되돌립니다.
고쳐짐 (영수증 무결성)
SavePipeline의 레이아웃 게이트가 꺼져 있거나 내부 예외로 못 돌았을 때passed를 돌려주던 것을 중단합니다.LayoutReport에 명시적status(passed/warned/failed/unverified)와unverified_reason이 추가되어, 끈 게이트·터진 게이트·진짜 통과가 영수증에서 구분됩니다.ok의 의미(저장 차단 여부)는 그대로라 동작은 바뀌지 않습니다.ID/참조 무결성 검사가 내부 예외를 삼킨 채 통과로 남던 것을 수리했습니다.
require_reference_integrity=True인데 검사가 실행되지 못하면 이제 실패입니다 — “확인 못 했음”을 “확인했고 정상”으로 세탁하지 않습니다.paragraph_patch가 전량 폐기된 편집을applied에 싣던 것을 수리했습니다. 이 계열은 all-or-nothing이므로, 매칭됐지만 버려진 편집은 신설된discarded로 보고되고applied는 비웁니다.output_path가 원본 바이트 사본을 받는 문서화된 경우는 신설outputIsSourceCopy가 명시합니다(두 키 모두 additive, 기본값 빈/false).
고쳐짐 (자기서술)
동봉 지원 매트릭스가 이미 출하된 기능을 부정하던 3곳을 정정했습니다: 차트(“생성 API 없음” →
add_chart는 5.3.0부터 실재), 표 기본값(“수리 예정” → 5.4.0에서 수리 완료), 체크박스(행 부재 → 실한컴 계약과 함께 신설). 이 문서는 휠에 동봉되고 MCP 리소스로 서빙되므로, 이를 읽는 에이전트가 실재하는 기능을 회피하고 있었습니다.능력 레지스트리 드리프트 가드를 문서↔문서 대조에서 레지스트리↔실코드 대조로 뒤집었습니다.
HwpxDocument의 모든add_*메서드는 정확히 한 능력 영역에 귀속되어야 하며, 등재 없이는 새 저작 메서드를 출하할 수 없습니다 (직전 릴리스에서 체크박스가 양쪽 문서에 다 없어 옛 가드를 통과한 실패의 구조적 차단).
추가됨
hwpx.capabilities.verify_self_description()과python -m hwpx.capabilities --verify— 설치된 휠에서 자기서술이 실표면과 일치하는지 누구나 직접 검사할 수 있습니다. 선언 대신 검증기를 동봉합니다.실측 코퍼스 v3 발행: 현행 스택이 만든 120문서(기준 스트라텀 + 각주·누름틀· 수식·차트·체크박스·편집계획 6표면)를 실한컴 12.0.0.3288이 전건 개봉·렌더 (120/120, rule-of-three 하한 97.5%). 영수증(
docs/openrate/verdicts_v3.jsonl) 은 행마다bucket을 가져 분모 분해가jq한 줄로 재현됩니다. 축소 범위 (497 전수 아님)는 매트릭스·README에 명시합니다.
문서
README 헤드라인 수치에 측정 스택·일자를 병기합니다(측정 없는 “continuously verified” 문구 폐기).
NOTICE에 한컴 파일 형식 공개 문서가 요구하는 고지 문구를 원문 그대로 추가했습니다. 낡은 사내 라이선스 메타데이터 정책 문서(재라이선스 이전 잔재)를 제거했습니다.
[5.7.0] - 2026-08-03
추가
체크박스 양식개체 저작(experimental):
add_check_box(caption, checked=…)·list_check_boxes()·set_check_box(checked, index=|name=). 방출 형상은 실한컴 실측 계약을 따릅니다 —value="CHECKED"가 ☑,UNCHECKED가 □로 그려지고 caption은 렌더 텍스트 레이어에 나옵니다.<hp:formCharPr>는 필수 자식이며(없으면 한컴이 문서를 거부합니다 — 단일 변인 프로브로 확정)<hp:checkBtn>은 누름틀과 달리<hp:ctrl>래핑 없이 run 직속입니다. 표 셀 안 배치도 지원하며, 선택자 없는/모호한set_check_box와 빈 caption은 typed 거부입니다.참고: 한국 정부 공문서 서식은 이 양식개체가 아니라 텍스트
[ ]+√ 관례를 규정합니다(시행규칙 별표 4 제10호). 이 프리미티브는 실제로 한컴 체크박스를 쓰는 서식용입니다.
[5.6.0] - 2026-08-03
추가
hwpx.plan편집 계획 실행기 (experimental): 바이트-스플라이스 편집 op 7종(paragraph_patch·fill_cells·apply_table_ops·apply_body_ops·recolor_runs_by_color·strip_runs_by_color·strip_trailing_table_captions) 을 선언적 계획 1파일(hwpx.edit-plan/v1)로 합성해 정적 선검증 → 전 체인 인메모리 실행 → 최종 open-safety 검증 → 단 1회 원자 쓰기로 실행합니다. 중간 step이 실패하면 output·source가 바이트 불변임이 테스트로 증명되는 all-or-nothing 계약이며, 결과는 step별 + 원본→최종 실측hwpx.mutation-report/v1사영을 실은hwpx.plan-report/v1입니다. dryRun은 동일 체인을 전부 실행하되 쓰기만 생략하고,journalPath지정 시 JSONL 저널(진단용, resume 계약 아님)을 남깁니다.hwpx.capabilities기계가독 자기서술 (experimental):describe_capabilities()가 설치 버전·extras 실측 프로브·라이브 표면 census(stable/experimental)·편집 계획 op 어휘·능력 영역 레지스트리를 보고합니다.contract_document()는 패키지 동봉 계약 문서 4종 (support-matrix·recipes-traversal·mutation-semantics·known-traps 신규 저작) 을,contract_json_schema()는 계약 JSON Schema 4종(edit-plan·plan-report· mutation-report·capabilities)을 서빙합니다. 드리프트 가드 테스트가 레지스트리 진입점 해석·op 어휘 3자 일치·support-matrix 행 대조를 강제합니다(core는 렌더 오라클 가용성을 주장하지 않습니다 — 그 보고는 automation 계층 소유).
수정
support-matrix 각주/미주 행의 낡은 서술 정정: 5.5.0이 각주 방출 계약을 수리하고도 매트릭스 행은
Render-unverified·honest-defer(2026-08-01 문구) 로 남아 출하됐습니다. 행을 실상태(Render-verified, 5.5.0 수리 근거)로 갱신했고, 같은 부류의 드리프트를hwpx.capabilities가드 테스트가 이제 구조적으로 막습니다.docs/stable-api.md의 experimental 목록도 5.2.0 수식 3종 누락을 함께 정정(12→23).
[5.5.0] - 2026-08-02
수정
각주/미주가 실한컴에서 렌더되도록 방출 계약 수리 (실한컴 gold 리버스): 각주는 본문 run 안
<hp:ctrl>래핑으로 삽입되고number(유형별 연속 번호)·suffixChar를 실으며, 각주 본문 문단은 각주/미주 스타일 (헤더 이름 조회, 기본 15/16·paraPr 10·charPr 3)과 선행<hp:autoNum>(FOOTNOTE/ENDNOTE)을 갖습니다 — 기존 방출(run 직속·번호 없음)은 실한컴이 각주를 그리지 않던 실결함(감사 실결함 3번). 리더 (markdown [^fn] 부록·텍스트 추출 주석)는 실한컴 ctrl-래핑 형상과 구식 자사 형상을 모두 읽습니다 — 실한컴산 문서의 진짜 각주를 그동안 리더가 놓치고 있던 갭도 함께 수리.note.textsetter는 gold 본문(autoNum· 스타일)을 보존합니다.
[5.4.0] - 2026-08-01
추가
ensure_border_fill(border_type=…): OWPML 선 종류 어휘(SOLID·DASH· DOT·DASH_DOT 등)를 검증해 테두리 모양을 지정합니다. 어휘 밖 값은 typed 거부(ValueError).HwpxOxmlTable.set_cell_border_fill(row, col, border_fill_id_ref): 개별 셀의 테두리/배경 참조를 교체하는 표면(저작 충실도 감사에서 확인된 B 표면갭 수리).ensure_run_style확장 7종:underline_shape/underline_color(물결 등 밑줄 모양·색),strike_shape(취소선 모양),ratio(장평 10–400),letter_spacing(자간 −50–100),shadow(그림자 색),script(sup/sub위·아래 첨자 — 실한컴 계약 relSz 67·offset ±30). 전부 감사에서 실한컴 렌더로 검증된 charPr 자식 어휘이며, 범위·어휘 밖 값은 typed 거부.
수정
add_hyperlink표시 텍스트가 실한컴 관례를 따름: 파랑(#0000FF) 텍스트 + 파랑 BOTTOM 밑줄(실코퍼스 하이퍼링크 최빈 서식). 서식은 표시 런에만 실리며(fieldBegin/fieldEnd 런은 주변 서식 유지 — gold 계약) 다음 문단으로 전염되지 않습니다.char_pr_id_ref=로 override 가능.첨자 offset 부호 정정: 실한컴 렌더 실측 결과 offset 양수=아래· 음수=위 —
script="sup"가 위로,"sub"가 아래로 정확히 그려지도록 부호를 맞췄습니다.밑줄 type 어휘 gold 일치:
underline=True가 방출하던type="SOLID"(비표준)를 실한컴 관례 위치 어휘BOTTOM으로 수정.
변경
add_table기본값이 실한컴 계약과 일치: 셀 안여백 기본 510/510/141/141(기존 0 — 한컴에서 만든 표와 여백이 달랐음), 기본 표 폭은 본문 폭(용지 − 여백)으로, 셀 안에 만드는 중첩 표는 부모 셀 사용 폭에 맞춤(기존 고정 7200/셀 기본값이 부모 셀보다 넓어 내용이 소리 없이 잘려 보이던 감사 실결함 수리). 명시width=지정 시 동작 불변.목록 문단 비상속: 글머리표/번호 문단 뒤에
add_paragraph로 추가하는 일반 문단이 목록 paraPr를 상속하지 않습니다(실한컴에서 프로그램적 추가 문단은 본문 — 감사에서 모든 후속 문단이 목록화되던 실결함 수리). 명시para_pr_id_ref지정 시 목록 계속 사용 가능.
[5.3.0] - 2026-07-31
추가
차트 저작
HwpxDocument.add_chart(experimental 계약): ECMA-376 chartML(c:chartSpace)을Chart/chartN.xml파트로 저장하고 실한컴<hp:chart chartIDRef=…>앵커를 방출합니다 — 실한컴 계약 그대로 파트는 어느 manifest에도 등록하지 않으며(실측), 한컴은 chartML만으로 차트를 그립니다(OLE 폴백·사전렌더 이미지 불요, 실한컴 렌더 픽셀 실측: 막대· 꺾은선·원). chartML은 파싱·루트 검증 후에만 기록(typed 거부), 표 셀 배치·인라인 배치 지원, 생성 직후 표준 섹션 스캔 재인식 실패 시 즉시 실패합니다. 기존 차트 파트와의 경로 충돌 없이 순차 할당하며 무관 파트는 바이트 보존됩니다.
[5.2.0] - 2026-07-31
추가
수식 저작
HwpxDocument.add_equation(experimental 계약): 실한컴이 만드는<hp:equation>형상 그대로(EqEdit script는<hp:script>자식,treatAsChar="1"인라인, lineseg 캐시 없이 — 한컴이 열 때 재조판) 수식을 삽입합니다. 표 셀 내부 삽입을 지원하고, 생성 직후 표준 섹션 스캔으로 재인식되지 않으면 즉시 실패합니다(특수분기 0). 계약은 실한컴 gold 리버스(수식 계약 문서) 기준입니다.LaTeX → EqEdit 변환기
hwpx.equation.latex_to_eqedit(experimental): 리더 방향(EqEdit → LaTeX)과 같은 토큰맵의 역방향입니다. 분수·근호/n제곱근· 첨자·그리스·관계/이항연산·합/적분/극한 상하한·행렬(pmatrix계열·cases)·\left/\right·accent·\text리터럴·예약어 따옴표 보호(T_{int}→T _{"int"})를 지원하며, 검증된 토큰셋 밖의 LaTeX는UnsupportedLatexError로 typed 거부합니다(무음 근사 없음). 왕복 property 테스트가 저작 EqEdit를 기존 리더로 복원해 고정점을 증명합니다.estimate_equation_size휴리스틱이<hp:sz>자리표시 크기를 계산합니다 (한컴이 열 때 실측 재계산).
[5.1.1]
v5.1.0은 보존된 실패 태그입니다 — 아무것도 게시되지 않았습니다. 태그
워크플로의 공개 위생 게이트가 실한컴 gold 픽스처 2종의 OPF 메타데이터에
남은 사적 출처 마커(creator/lastsaveby)를 적발해 중단했고, 마커를 제거한
동일 내용이 5.1.1로 복구되었습니다. v5.1.0은 삭제·이동·재사용하지 않습니다.
[5.1.0]
추가
누름틀 필드 저작
HwpxDocument.add_form_field(experimental 계약): 실한컴이 만드는 CLICKHERE 형상 그대로(필드 속성·Prop/Command/Direction/HelpState파라미터·화면 전용 안내문 런·fieldEnd쌍) 필드를 생성합니다. 안내문 길이 직렬화는 문자 수 기준이며, 만든 필드는 기존list_form_fields/fill_form_field가 특수분기 없이 그대로 소비하고 실제 한컴오피스가 열거·채움을 수행합니다(실한컴 gold pair 리버스 계약). 표 셀 내부(중첩 포함) 삽입을 지원합니다.필드 페이로드 확장:
list_form_fields결과에memo(HelpState)·dirty·is_placeholder가 추가되고, 실한컴 파라미터 이름Direction/HelpState가prompt/memo로 매핑됩니다.dirty != "1"인 동안 보이는 값은 사용자 값이 아니라 안내문입니다.
수정
채움이 한컴과 같은 흔적을 남깁니다:
fill_form_field가 값 기록 시dirty="1"을 설정하고, 안내문(placeholder) 상태의 필드를 채울 때 안내문 전용 스타일(빨강·이탤릭)을 주변 스타일로 교체합니다. 종전에는 값이 안내문 스타일을 그대로 물려받아 빨간 이탤릭으로 남는 무음 시각 결함이 있었습니다.
[5.0.2]
수정
첫 실행이 조용해졌습니다: 기본 템플릿으로
new()→save_to_path()만 해도manifest에서 masterPage/history/version …경고가 15줄 출력되던 문제를 고쳤습니다. 빈 문서에 masterPage/history가 없는 것과 version.xml이 manifest 선언 없이 고정 경로에 있는 것은 실제 한컴 산출물과 동일한 정상 상태입니다(실한컴 gold fixture 6종 대조로 확인). 이제 manifest가 실재 파일을 놓친 경우에만 경고하며, 반환값은 변하지 않습니다.릴리스 해시 검증기가 태그에서 버전을 유도합니다: 종전에는 PyPI 조회 URL의 버전이 스크립트 상수로 고정되어, v5.0.1 릴리스에서 존재할 수 없는 5.0.0 JSON을 조회하다 404로 빨간 표시를 냈습니다(아티팩트는 건전, 독립 readback으로 종결했던 그 건입니다). 이제
--tag에서 버전을 유도하고, 매니페스트 파일명이 태그 버전을 담는지 교차 검증하며, 인덱스 전파 지연 대비 재시도 예산을 60초에서 300초로 늘렸습니다.
추가
5분 퀵스타트 개편: 읽기(텍스트 추출)와 저장 영수증(
MutationReport) 단계를 추가하고, 편집 예제를 복붙만으로 이어지는 완결 스크립트로 재구성했습니다. 문서 예제 ledger 재생성 스크립트 (scripts/regenerate_example_ledger.py)도 함께 추가했습니다.과업 축 매뉴얼 2종: 순회 레시피(문단·중첩 표·각주·런·메모/누름틀)와 편집 의미론(반환형· 실패 모드·재실행 성질·저장 보증 — 전부 실측 확인) — 실행 ledger 게이트에 편입.
llms.txt 발행: 훈련 데이터에 이 라이브러리가 없는 AI 도구가 정확한 API를 배우도록
https://airmang.github.io/python-hwpx/llms.txt(+ 매뉴얼 병합본llms-full.txt, 빌드 시 자동 생성)를 사이트 루트에 제공합니다. 내용 계약은 테스트로 고정(버전 문자열 금지 포함).채택·응답 지표 공개: 채택 지표 페이지와 기계 판독 이력(
adoption-metrics-history.json, 사이트 루트)을 추가했습니다. 수집은scripts/snapshot_adoption_metrics.py(멱등 — 같은 날 재실행은 교체)이며, 각 지표에 “이 숫자가 말해주지 않는 것” 캐비앗을 함께 둡니다.Python 3.13·3.14 공식 지원: CI 매트릭스와 trove classifier에 3.13/3.14를 추가했습니다(전체 스위트 1376 passed를 양 버전에서 확인한 뒤 반영). 지원 정책은 설치 가이드에 명문화했습니다.
[5.0.1]
v5.0.0 is a preserved failed tag: its release run stopped in prepublish
(the installed-wheel docs gate requires uv, which the workflow never
installed) and nothing was published to PyPI or GitHub. 5.0.1 is the same
train content plus the workflow fixes — uv is now bootstrapped in both
the test and release jobs, and the test job’s tee pipe no longer masks
pytest’s exit code. Never delete, move, or reuse v5.0.0.
[5.0.0]
Removed — BREAKING
python-hwpx is now the HWPX object model, OPC/OXML, and the format-native
primitives built on them. The application workflows that had grown inside the
library moved to python-hwpx-automation, which has owned their canonical
implementation since the 4.x line. Nothing is discontinued — every removed
import has a named replacement in
the migration guide, and 4.x keeps all of it.
hwpx.agentand thehwpxconsole command →hwpx_automation.office.agent. The automation package declares thehwpxname in the train that raises its core floor to 5.0, so no valid install ever has two declarers of it.hwpx.authoring,hwpx.builder,hwpx.design,hwpx.presets→hwpx_automation.office.authoringhwpx.exam→hwpx_automation.office.examhwpx.evalplan_fill→hwpx_automation.office.evalplanhwpx.form_fill,hwpx.formfill_quality,hwpx.fill_residue,hwpx.guidance_scan,hwpx.template_formfit→hwpx_automation.office.form_fillhwpx.tools.official_lint,hwpx.tools.pii,hwpx.tools.table_compute,hwpx.tools.style_profile,hwpx.tools.advanced_generators,hwpx.tools.report_parser→hwpx_automation.office.compliance/office.quality/office.utilities/office.authoringhwpx.tools.mail_merge.mail_merge→merge_template_rows, which is now public. The removed wrapper masked by default and core no longer carries the detection rules; the generic function takes avalue_sanitizerso the caller decides, and the decision is visible at the call site instead of implied by an omitted argument.hwpx.tools.doc_diff.build_comparison_table_plan→hwpx_automation.office.document_ops. Generic diff and reference-consistency stay here.The four
template_formfitnames that 4.x warned would go “in the next major” are gone. That was this major.hwpx-conformanceconsole script: its package is repository QA and no longer ships, and a command that cannot start is worse than no command.
Changed
HwpxDocument.add_table()now creates a neutral table-anchor paragraph by default, so a preceding numbered heading/list no longer renders a stray number beside the table in Hancom. Passinherit_style=Trueor explicit paragraph/style references when anchor-style inheritance is intentional.HwpxDocument.add_tracked_replace()now writes the inserted replacement at the deleted text’s source position, preserves that run’s character style, and leaves later tracked inserts in their original order.The product-boundary lazy-loader fingerprint now canonicalizes parser-only empty AST fields, so the same guarded source passes consistently on Python 3.10–3.13 while real syntax changes still fail closed.
verify_redlineandverify_filltake an injectedhwpx.quality.rendering.RenderBackend. Core does not look for a Hancom installation any more; without a backend the report isrender_checked=Falserather than a visual verdict it never earned.toc_fidelity.heading_rendered_pagestakes a word-box extractor, andheading_pages_from_word_boxesis public for callers that already have boxes. Reading a PDF needs an imaging stack; matching headings does not.hwpx.oxml.color.color_familyis public. Both core’s run rewriting and the application-side guidance scanner classify colour the same way, so there is one implementation of what counts as red.
Packaging
hwpx.benchmarkandhwpx.conformanceare excluded from the wheel. They are things this project runs on itself, not things a library user installs.The fuzz harness moved to
scripts/fuzz/, outside the package, because it builds documents through the MCP owner’s builder and core must never import a companion layer.
Migration
pip install python-hwpx-automation and follow docs/migration-5.0.md.
pip install "python-hwpx<5" rolls back; the 4.x line is not a dead end.
Documentation
4.x compatibility/deprecation 관찰 정책을 공개했습니다. 8개 runtime family, 3개 CLI, 공개 스키마/리포트 projection은 2026-10-31까지 모두
extend하며 제거는 0건입니다. 신규 코드를 위한 MCP canonical 경로, side-by-side 이행, rollback, 별도 next-major 승인 조건을 4.x 호환 표면 관찰 정책에 기록했습니다.
[4.2.0] - 2026-07-22
Added — 평가계획(교수학습운영·평가계획) 실채움 엔진 확장
fill_evalplan에phase="clean"을 추가했습니다 — 기존"all"(채움) 뒤에 core의 결정론적 양식 정리(제목·교사·정의적 표 채움, 양식 지시문·외래 샘플 소제목·고아 헤딩 prune, 빨강 지시 run 제거, 슬롯-파랑→본문 검정 재색, 표 꼬리 캡션 strip)를 한 경로로 수행해, 별도 정리 스크립트 없이 실제 도교육청 양식을 잔존물 없이 채웁니다. 기본값"all"은 기존 동작 그대로입니다(비파괴).finalize_evalplan(위 정리 오케스트레이션)과 프리미티브 2종 (table_patch.strip_trailing_table_captions,guidance_scan.is_form_instruction)을 추가했습니다. 전부 패턴·구조 기반이며 과목·학년 문자열 하드코딩이 없습니다.
Fixed — 평가계획 표 파싱·채움
성취기준별 §4가 성취수준 표(A~E 또는 상/중/하)를 파싱·채우지 못하던 문제를 고쳤습니다. 콘텐츠의 성취수준 단계 수를 읽어 표 구조에 맞춰 채웁니다.
도너 스케줄 표의 병합이 일부 주차 행을 흡수하던 문제를 고쳤습니다(병합 분리).
§7 수행평가 세부기준 상세 배점 루브릭(평가요소·수행수준·배점, 세부 영역 가/나/다)을 파싱·채우도록 확장했습니다.
[4.1.1] - 2026-07-21
Fixed
4.1.0 태그의 발행이 공개 위생 게이트(워크스테이션 경로 검사)에 걸려 중단됐습니다 — 내부 개발용 rhwp 트리아지 스크립트가 공개 레포에 포함돼 있었고, 해당 도구는 공개 표면이 아니므로 저장소에서 제거했습니다. 라이브러리 코드는 4.1.0과 동일합니다. (
v4.1.0은 보존된 실패 태그이며 PyPI 산출물이 없습니다.)
[4.1.0] - 2026-07-21
Fixed — form-fill 차등 판정기 교정 (verify 신호 신뢰성)
hwpx.form_fit.wordbox의 표 구조 지문(extract_layout_signature)과 overflow 클립(extract_cell_clips)이 fitzfind_tables()기본 전략(글리프 스냅) 대신 그려진 테두리 전용(lines_strict)을 쓰도록 교정했습니다. 종전에는 빈 셀을 채우면 그려진 격자가 그대로인데도 유령 표·허위 overflow가 검출되어verify_form_fill계열 게이트가 허위 실패를 냈습니다. wild 코퍼스 실측: 허위 shape 실패 6건·허위 overflow 3건 해소, 회귀 0, 산출분 pass 17/28→23/28. 제품 산출물(채움 바이트)은 변경되지 않았습니다 — 판정만 정확해졌습니다.
Docs — 정직 실측 갱신
실측 코퍼스 메트릭에 「구조결함 2차 실측」 추가: 잔존은 페이지 리플 5건(4건 typed 경고 동반·완전 무음 1건)이며, typed 거부 35건 전수 감별 결과 과잉거부 0. 지원 매트릭스 양식 채움 행 동기화.
[4.0.0] - 2026-07-21
Removed — deprecation window 준수 정리 (major 경계)
HwpxDocument.save()제거: v2.6(2026-02-19)부터 5개월간DeprecationWarning을 달고 있던 호환 래퍼를 제거했습니다. 목적지별 명시 메서드를 쓰세요 — 경로save_to_path(path)· 스트림save_to_stream(stream)· 바이트to_bytes(). 이 셋은 안전한 쓰기 계약(mode/fallback등급 +return_report)을 지원하며, 제거된save()에는 없던 기능입니다.hwpx.package모듈(→hwpx.opc.package)은 경고 이력이 있지만 stableHwpxPackage의 역사적 import 경로라 유지합니다(shim, 비용 0). 전체 표·이유는 4.0.0 마이그레이션.
Added — 구조화 예외 베이스 (오류 계약 통일)
hwpx.errors.HwpxError(최상위hwpx.HwpxError로도 import) 도입 — fail-closed 공개 경로 예외의 베이스. 사람용str(exc)는 그대로 두고 기계가 읽는code(kebab 안정 식별자)·context(실측 값 dict)·suggestion(다음 행동)을 얹으며to_dict()로 봉투를 냅니다. 공개 계약 경로부터 이행:PreservationDowngradeError(preservation-downgrade), 신규SaveError(대표 저장 경로의 사전검증·open-safety· 품질 게이트 실패),TableStructureError(table-structure),RenderCheckRequired(render-check-required). 기존except타입(ValueError/RuntimeError/Exception)은 상속으로 그대로 동작합니다. 전체 표는 stable-api.md 오류 계약.HwpxError가 stable 표면에 추가되어hwpx.__all__은 stable 67개가 됩니다.
Changed — 스키마 동결 + mutation 반환형 통일
스키마 동결:
hwpx.mutation-report/v1·hwpx.document_plan.v1/v2·hwpx.agent-batch/v1·hwpx.mixed-form-plan/v1의 required 필드 집합을 동결하고 계약 테스트(tests/test_schema_freeze.py)로 고정했습니다. 정책은 additive-only — 신규 필드는 Optional, 파괴 변경은 새 major + 새 스키마 버전 문자열 (schema-freeze.md).mutation 반환형 통일: 공개 mutation-write 경로가 전부
hwpx.mutation-report/v1로 사영 가능함을 계약 테스트(tests/test_mutation_contract_unification.py)로 고정 했습니다(byte-write 결과모델 4종 + 네이티브save_to_path영수증). 사영 없는 신규 byte-write 모델이 추가되면 구조 가드가 실패합니다.save_to_path/save_to_stream/to_bytes이름을 장기 확정으로 명문화(개명 재론 종결, safe-write-contract.md).
Changed — 최상위 API 표면 3계층화 (major 경계 준비)
from hwpx import ...표면을 stable / experimental / deprecated 3계층으로 분류했습니다.hwpx.__all__에는 stable 67개만 남깁니다(P1의 66 + P2HwpxError). 계층·정책 전수 목록은docs/stable-api.md.experimental(12) — ingestion 프레임워크·레이아웃 프리뷰·문서 프리뷰 뷰어.
from hwpx.experimental import ...로 사용하세요. 최상위 재내보내기는 하위 호환을 위해 유지하되 접근 시DeprecationWarning이 나며 다음 major에서 최상위 경로가 제거될 예정입니다(구현 모듈·hwpx.experimental경로는 유지).deprecated(4) —
analyze_template_formfit/apply_template_formfit와TEMPLATE_FORMFIT_*_SCHEMA_VERSION상수 2개. 대체 = 구조적 form-fill 경로 (hwpx.table_patch.fill_cells계열 + MCPanalyze_form_fill/apply_form_fill/verify_form_fill).최상위에서 제거된 이름 0개: 기존 최상위 이름 82개는 전부 계속 import 가능합니다 (최소 deprecation window 준수 — 경고 없는 즉시 제거 금지).
HwpxDocument.save()는 최상위 표면 이름이 아니라 메서드로, 위 Removed 절에 따라 제거됐습니다.
[3.8.0] - 2026-07-21
Added — 문서 프리뷰 뷰어
render_document_viewer: 한컴 없이 문서를 스크롤 통독하는 self-contained HTML 뷰어(상단바·현재 페이지 표시·키보드 탐색·외부 리소스 0). 충실도 배지가 한계를 정직하게 표시합니다(텍스트 근사·페이지네이션은 한컴과 다를 수 있음).수식 실제 렌더:
hwpx.equation—<hp:equation>의 EqEdit 스크립트를 clean-room 토큰맵으로 LaTeX로 변환해 MathML로 렌더합니다(선택 extrapython-hwpx[preview]= latex2mathml). 실한컴 ground-truth와 시각 대조로 검증(specs 증거). 변환 불가/라이브러리 부재 시 빈칸 대신 원문·LaTeX 코드블록으로 정직 표시하고, 그림·도형은 자리표시 마커로 보입니다 — 기존 프리뷰가 수식을 조용히 빈 문단으로 떨구던 동작은 제거됐습니다.
Internal
MCP 경계 타이핑 정밀화를 위한 evalplan 경로 Path/str 유니온(순수 어노테이션).
복잡도 3물결: 최상위 15개 함수 분해(C901 115→100), characterization 39건 추가.
[3.7.0] - 2026-07-21
Added — Safe Write Contract
일반 저장 경로가 명시적 쓰기 모드를 받습니다:
save_to_path(path, mode="patch"|"rebuild"|"auto", fallback="error"|"rebuild", return_report=True). 반환 기본값은 기존과 동일(경로)이며return_report=True일 때만 스키마버전드hwpx.mutation-report/v1영수증을 반환합니다 — 요청/실제 모드, 변경 part 목록(+가능한 경우 실제 범위, coordinate space 명시), 보존 보증 3계층(미수정 part payload / zip local record / whole package), 수행한 검증의 3값 기록(passed/failed/not_performed).무음 강등 없음:
mode="patch"에서 요청 보존 등급을 만족하지 못하면 출력 파일을 쓰지 않고 typedPreservationDowngradeError를 던집니다.fallback="rebuild"로 명시 동의한 경우에만 진행하며 영수증에fallbackUsed: true가 남습니다. 보존은 단언이 아니라 open-시점 기준선 대비 실측입니다(발행 성공 시 기준선 전진).기존 쓰기 결과 4종(BytePreservingPatchResult·CellFillResult·BodyOpsResult· AgentBatchResult)에
as_mutation_report()사영 추가 — byte-splice 계열은source=(bytes/str/Path) 제공 시 보존 3계층과 실제 스플라이스 범위를 실측 보고, 미제공 시 정직 강등. 기존 필드·to_dict 출력은 불변(additive).
Docs
README 포지셔닝 개정(§안전 자동화 계층+검증된 저작), 지원 매트릭스 공개(docs/support-matrix.md — Parse/Preserve/Edit/Create/Render-verified/ Unsupported-but-preserved/Unsupported-and-rejected, 셀별 증거 링크), docs/safe-write-contract.md 신설, 헤드라인 지표에 corpus·분모·측정일· assurance 병기(오픈 수용률 하한 표기를 rule-of-three 99.37%로 정밀화).
[3.6.0] - 2026-07-20
Fixed
양식 채움 무음 서식파괴의 지배 원인 픽스: fit 슬롯이 셀 안 인라인 treat-as-char 객체(체크박스·폼컨트롤·그림)의 선언 폭(
hp:sz/@width)을 가용폭에서 차감합니다. 이전에는 이를 비모델링해 “맞음”으로 오판한 채움이 실한컴에서 줄바꿈·행 성장을 일으켜 다중 페이지 양식 전체를 밀었습니다 (실측: 3글자 채움이 10페이지 양식을 11페이지로). 이제 그런 채움은 typed FIELD_OVERFLOW 거부(컨트롤 동거 경고 포함)가 됩니다. 실한컴 재측정에서 판정 조합 전체의 무음 서식파괴가 47%→16.7%로 감소했습니다.레이아웃 캐시 무효화를 편집 스코프로 축소: 편집한 문단의 캐시만 제거하고 저장 시 전량 제거(2개 레이어) 대신 증명 가능한 stale만 걷어냅니다. 채움 바이트 델타가 −133,651자에서 −170자로 줄어 바이트 보존 계열과 정합합니다.
Added
SlotMetrics.inline_object_width/inline_object_count— 슬롯 진단 표면.
[3.5.0] - 2026-07-20
Added
양식 채움 fit 판정에 수직(행 높이) 예산을 도입합니다. 이전에는 셀의 가로 폭만 측정해 세로로 100줄까지 “맞음”으로 판정, 축소도 거부도 발동하지 않고 행이 조용히 자라 페이지를 밀었습니다. 이제
available_height(셀 높이 − 상하 마진)와 글꼴별 라인하이트로 세로 예산을 재서,wrap_then_shrink는 값을 예산 안으로 축소하고, 최소 글꼴에서도 안 들어가는 과대 입력은overflow="fail"에서 typed 거부(FIELD_OVERFLOW)로 돌려줍니다.allow_row_expand/expand_row는 명시적 opt-out입니다.FitPolicy.keep()과 폭-only 경로는 바이트 불변입니다.
Measured (정직 발견)
wild 공개 양식 전수 재측정(실한컴): 양식 채움 차등이 fit-on 32/63 = 50.8% (fit-off 31/63 = 49.2%). 기대-통과 층(short+medium)은 61.4%로 이동 없음 — fit은 wild 실패의 지배 레버가 아니며, 지배 원인은 구조적 3부류(채움-겹침·다중 페이지 경계 밀림·표 구조 민감도)임이 측정됐습니다. 상세는
docs/corpus-metrics.md및 leap 데모demo/S-085-wild-formfill/.
[3.4.1] - 2026-07-19
Note
복구 릴리스.
v3.4.0태그는 prepublish 공개 위생 게이트에서 실패했고 (테스트 픽스처의 워크스테이션형 경로 문자열 —/home/...패턴), PyPI 아티팩트나 GitHub Release는 만들어지지 않았습니다. 실패 태그는 이력으로 보존하며 3.4.1이 실제 공개 릴리스입니다. 기능 내용은 아래 3.4.0 항목과 동일합니다.
[3.4.0] - 2026-07-19 (tag preserved; not published)
Added
선언형 문서 계획(plan-v2)에 네이티브 자동 목차 옵션(
native: true)을 추가합니다. 정적 목차를 내려쓴 뒤 한컴이 열기 시점에 재계산하는 TABLEOFCONTENTS 필드로 승격하며, 본문 스타일 수집 가드(바탕글-수집 함정)를 포함합니다.M9 출판 코퍼스 도구를 추가합니다: 코퍼스 v2 additive 생성기, 무인 실한컴 박스 판정(오픈/파싱·스케일 렌더 배치·redline InitScan 프로브), 축별 검증 드라이버 (byte-identity·PII 0-leak·저작 품질·양식 채움 차등·TOC 검증)와 결합 집계기 (provenance 이중 발행·redline-aware parsed·렌더 티어).
실측 결과를 문서로 발행합니다:
docs/corpus-metrics.md(축 주석 포함)와 릴리스별 기계 판독 히스토리docs/corpus-metrics-history.json.
Fixed
오픈레이트 박스 러너의 재개 키를 basename에서 전체 경로로 바꿔, 중복 파일명을 가진 코퍼스에서 파일이 조용히 건너뛰어지던 문제를 고칩니다.
Windows PowerShell 5.1에서 렌더 잡 배열이 1개 객체로 접히던 열거 함정을 고칩니다.
Internal
HwpxDocumentfacade를 도메인 owner 8모듈로 행동 보존 분해합니다 (2,961→1,777줄; 공개 표면 97멤버 characterization 스냅샷으로 동결).복잡도 상위 함수 5종+1을 분해해 C901 초과를 120→114로 낮추고, owner 모듈을 mypy/pyright 점진 게이트에 편입합니다(40파일 clean).
[3.3.1] - 2026-07-18
Internal
최악 복잡도 검증기 2종을 행동 보존 분해합니다:
validate_blueprint_manifest(C901 60→1, 헬퍼 14개)와validate_package(53→10, 헬퍼 12개). 에러 문자열·검사 순서·manifest 불변성은 기준선과 verbatim 동일하며 테스트 델타 0으로 실증했습니다.공개 표면·계약 변화는 없습니다. S-083 릴리스 트레인(hwpx-mcp-server 4.3.0 · hwpx-plugin 0.6.0)과 좌표를 맞추는 릴리스입니다.
[3.3.0] - 2026-07-18
추가
선택적 Mac GUI 오라클(
MacHancomOracle)에 도달성 프로브를 추가했습니다. 한컴 앱이 설치돼 있어도 GUI 세션·Automation(TCC) 권한이 없으면 5초 이내에available() == False로 정직하게 강등되며(프로세스 수명 캐시), 기존 구조 검증 경로가 그대로 동작합니다.렌더 오라클에 단일 외부 예산 전파를 추가했습니다.
budget_seconds(생성자·resolve_oracle)가 지정되면 내부의 모든 subprocess 타임아웃이 남은 예산으로 clamp되고, 예산 소진 시 subprocess를 생성하지 않고 즉시 강등합니다.HWPX_ORACLE_STRUCTURAL_ONLY환경변수를 추가했습니다. 설정 시resolve_oracle()은NullOracle을 반환하고 Mac GUI 백엔드는 어떤 경로로도 GUI 자동화에 진입하지 않습니다(구조 판정 전용 모드).HWPX_ORACLE_BUDGET_SECONDS환경변수를 추가했습니다. 호스팅 프로세스가 한 번 선언한 외부 deadline이resolve_oracle()의 모든 호출 지점(코어 verify·MCP 핸들러)에 자동 전파됩니다. 명시적budget_seconds파라미터가 env보다 우선합니다.
변경
src/hwpx/visual/oracle.py를 mypy·pyright 점진적 게이트에 편입하고 기존 타입 오류 2건(textLength 협소화, 검증 항목 변수 재사용)을 수정했습니다.
[3.2.0] - 2026-07-17
추가
기존 섹션 머리글을 네이티브 ID 또는 페이지 유형으로 지정해 본문·표 셀과 같은
hwpx.agent-batch/v1트랜잭션에서 편집하는 타입드 story 경로를 추가했습니다. dry-run, revision 및 idempotency 검사, 오류 시 rollback, 저장 후 story identity·구조 보존 확인을 동일한 단일 직렬화 경로에서 수행합니다.
변경
비대해진 OXML 문서 구현을 문서 프리미티브, 패키지/구역 관리, 구역 서식과 story 등 책임별 런타임 모듈로 분리했습니다. 기존 공개 facade와 import 경로는 유지하면서 수정된 소유 모듈 전부를 Ruff·mypy·pyright 점진적 게이트에 포함했습니다.
수정
add_section()이 인접 구역의 렌더 가능한 페이지·단 설정을 복제하되 머리글·바닥글 story는 복제하지 않고, manifest/spine 순서와hh:head/@secCnt를 함께 갱신하도록 수정했습니다. 음수after인덱스, 범위 오류, 불완전 패키지, 상대 manifest href를 원자적으로 처리하며remove_section()과 패키지 열기 안전성 검증도 같은 구역 수 불변식을 확인합니다.
검증
2개 구역 합성 fixture의 결정적 재생성, 저장·재열기, 패키지 바이트 보존, 오류 주입 rollback 및 한컴오피스 2/2쪽·2/2구역 경고 없는 열기 회귀를 고정했습니다.
[3.1.0] - 2026-07-16
추가
타입드 혼합 양식 계획기:
hwpx.mixed-form-plan/v1이 네이티브 필드, 고유 라벨 셀, revision-bound canonical path, 본문 직접 단일-run anchor를 하나의 엄격한 계획으로 받습니다. 모든 대상을 먼저 canonical path로 해석한 뒤 기존hwpx.agent-batch/v1한 건으로 컴파일하며,apply_document_commands만 실행기로 사용해 dry-run·rollback·idempotency·단일 저장 게이트를 그대로 보장합니다.병합 셀의 논리 좌표를 실제
cellAddr앵커로 정규화하는 비변형resolve_cell_target계획 프리미티브와 공개 plan/compiled-plan JSON Schema를 추가했습니다.
변경
에이전트 batch JSON Schema를 정본 카탈로그에 추가하고 quality 객체의 enum·boolean 필드를 엄격하게 검증해 알 수 없는 값이 실행 단계까지 전달되지 않도록 했습니다.
혼합 양식 출력이 입력과 동일한 resolved path·symlink·hardlink인 경우 계획과 적용 직전에 거부하고, 셀 라벨은 정규화된 정확 일치만 허용합니다. 컴파일 계획은 locator별 node/path/ section/좌표 불변식을 검증하며, 멱등성 식별자에 공개 locator 요청 hash를 포함합니다.
검증
합성 1쪽 한국어 혼합 양식으로 네 locator 동시 적용, 드라이런 무출력, 주입 실패 rollback, idempotent replay/conflict, 미변경 OPC member 바이트 보존, 재열기와
openSafety.ok를 고정했습니다.evalplan회귀는 유지하며exam모듈은 혼합 양식 경로에 가져오지 않습니다.
[3.0.0] - 2026-07-16
변경
공개 배포에서 내부 품질 검증용
hwpx.practice런타임과 관련 Python API를 제거했습니다. 문서 작성·편집·검증, 시험지, 평가계획, 범용 양식 채움 기능과 공개 conformance 흐름은 그대로 유지됩니다.
수정
높이가 서로 다른 물리 행을 삭제할 때 병합 셀 높이를 평균값으로 줄이던 문제를 수정했습니다. 이제 삭제 행의 실제 최대 높이를 사용해 평가계획 표를 다시 배치해도 병합 높이 합과 한컴 편집기 열기 안전성이 유지됩니다.
보안
소스·wheel·sdist·import 표면에 내부 품질 검증 런타임이 다시 포함되지 않도록 공개 위생과 패키징 회귀 게이트를 추가했습니다.
[2.29.2] - 2026-07-15
수정
공개 저장소의 운영 기록·생성 산출물·workstation 경로를 제거하고, 실제 회귀 입력은 명시적인 합성 fixture 디렉터리로 이동했습니다. 패키지에 포함되는 HWPX 예제와 conformance corpus의 작성자 메타데이터도 합성 값으로 정규화했습니다.
치명적 정적 오류를 차단하는 Ruff
E9,F및 텍스트/HWPX/wheel 공개 위생 게이트를 추가하고, 기존에 이름이 겹쳐 실행되지 않던 builder 회귀 테스트를 고유 이름으로 복구했습니다.
보안
GitHub Actions를 불변 커밋으로 고정하고 CodeQL, dependency review, Dependabot, CycloneDX SBOM, SECURITY/CODEOWNERS 정책을 릴리스 표면에 추가했습니다.
[2.29.1] - 2026-07-15
수정
릴리스 prepublish가 시각 fixture 테스트를 실행하면서도 Pillow와 NumPy를 설치하지 않아
v2.29.0태그의 게시 작업이 중단된 문제를 수정했습니다.testextra에 두 의존성을 포함해 clean CI에서도 동일한 테스트 계약을 결정적으로 재현합니다.
비고
v2.29.0태그는 prepublish 실패 이력으로 보존되며 PyPI 패키지와 GitHub Release는 생성되지 않았습니다. 기능 내용은 아래 2.29.0 항목과 동일하고, 실제 공개 패키지는 2.29.1입니다.
[2.29.0] - 2026-07-15
추가
타입드 에이전트 문서 인터페이스와 블루프린트 재생: 안정적인 semantic path/query, revision-bound atomic command, 통합
hwpxCLI, deterministic.hwpxbpdump와 strict portable/source-bound replay를 추가했습니다. 스타일·번호·리소스·참조는 typed semantic signature로 매핑하고, raw XML 없이 fidelity/identity/dependency/lossless/openSafety 영수증을 반환합니다.내구성 렌더·시각 QA: supervised Hancom render worker, resumable queue/poison-session 처리와 fixture 기반 page QA·blind evaluation 계약을 추가했습니다.
수정
블루프린트 replay가 병합 표 grid와 한컴 네이티브 control을 보존하도록 보강했습니다.
공개 2.24.1의 안전 동작을 유지해, 실제로 변경된 비어 있지 않은
lineWrap="SQUEEZE"셀만BREAK로 전환하고 no-op·지우기·미편집 셀은 원래 모드를 보존합니다.
비고
2.25.0–2.28.0은 공개 배포가 아니라 단계별 로컬 후보였으며, 그 누적 변경을 이 2.29.0 공개 항목으로 통합합니다.
[2.24.1] - 2026-07-14
수정
표 셀의
lineWrap="SQUEEZE"가 긴 신규 값을 한 줄 폭에 강제 압축해 글자가 포개지던 문제를 수정했습니다. 바이트보존fill_cells/apply_table_ops(fill_cell)와 일반set_cell_text모두 실제로 변경된 비어 있지 않은 셀만BREAK로 전환하고, 미편집 셀·no-op·지우기 작업의 원래 모드는 보존합니다.
[2.24.0] - 2026-07-08
추가
Stage 3 범용 form-fill 프리미티브 (universal form-fill goal): 임의 양식을 제출본급으로 채우는 동적 파이프라인의 손.
hwpx.guidance_scan.scan_form_guidance(비변형 정찰: 셀·캡션 포함 색 신호/범례/placeholder/빈 셀 후보).hwpx.body_patch(표 밖 문단 바이트보존 op: replace_text·delete_paragraph·insert_paragraph_by_clone·reorder_paragraphs·restyle_text·set_paragraph_text + 문서전체strip_runs_by_color(범례 “이 색=삭제” 구동)·recolor_runs_by_color(슬롯색→본문색)).hwpx.table_patch신규 op:split_cell_vertical(병합 셀 N그룹 분할)·clone_table(표 복제)·set_row_heights·set_cell_line_spacing+apply_table_ops(dry_run=)transcript.hwpx.fill_residue.inspect_fill_residue(채움본 잔존물 zero-체크 게이트).
비고
평가계획 3학년 실양식을 처음부터 끝까지(삭제·재구성·채움·청소·recolor) 범용 프리미티브만으로 완성, 실한컴 렌더 + 오너 실물 검수 PASS로 검증(도메인 전용 코드 0줄).
[2.23.0] - 2026-07-03
추가
폰트 shrink-to-fit (M10 후속, S-064):
hwpx.table_patch.fill_cells에fit_max_lines(+ 셀별max_lines) — 셀 텍스트가 템플릿 폰트로 목표 줄수를 넘겨 wrap되면form_fitFitEngine이 확신을 갖고 들어가는 가장 큰 폰트(≥min_font_pt)를 골라 실제<hh:charPr>로 재료화(base charPr 복제·height 변경)하고 셀 run을 그 charPr로 재지정. byte-preserving(header.xml의 새 charPr + 해당 섹션만 변경, opt-in이라 목표 없는 채움은 바이트 동일). FitEngine 정직 게이트가 borderline shrink는 거부(확실히 들어갈 때만 축소).
비고
오라클 실증: 실제 3학년 양식 성취기준 셀을 긴 텍스트 +
max_lines=4로 채우니 9pt→6.5pt 축소, 실한컴 clean 렌더(나머지 표는 9pt 유지). 정직: 도교육청 폼은 base 9pt라 축소 여지(→8pt)가 작아autofit_columns(가로)가 주력이고 폰트 축소는 보조; base 폰트 큰 폼엔 효과적. README 3스택 정비(python-hwpx 425→171·mcp 599→184·skill 471→178줄) 동반.
[2.22.0] - 2026-07-03
추가
열 너비 조정 (M10 후속, S-064):
hwpx.table_patch.apply_table_ops새 op 2종 —set_column_widths(table_index, widths)(명시적 논리 열너비; 각 셀 cellSz.width = 걸친 열들의 합, 병합 인식)·autofit_columns(table_index)(내용에 맞춰 열너비 재균형: demand = 최장 단일-span 셀 텍스트폭[form_fit어드밴스 모델], sqrt-damped로 문단 열 폭주 방지, 열별 최소폭 floor, 표 총폭 보존). 둘 다 byte-preserving(cellSz만 편집, charPr/header 불변)이며 grid 검증. 배경: 텍스트가 길어지면 한컴이 행 높이를 자동으로 늘려 넘침은 없으나 좁은 열은 촘촘히 wrap됨 — autofit이 내용 많은 열을 넓혀 완화한다(오라클 실증: 운영계획 성취기준 열 14186→16441, wrap 약 16→9줄, 총폭 보존).
[2.21.0] - 2026-07-03
추가
M10 바이트보존 구조적 양식채움 (S-064):
hwpx.table_patch— 2026-07-03 실전 실패(도교육청 평가계획 양식을 재생성으로 채워 서식 파괴)를 드라이버로, S-052 바이트 코어 위에 “양식 채움 층”을 완성.fill_cells(source, cells)—(table_index, row, col)주소로 셀 텍스트를 바이트보존 splice(빈/self-closing 셀 삽입, 다중 문단 셀 전체 교체, 병합 앵커 해석). 미변경 셀·표·섹션은 바이트 동일(원칙 VII), no-op=바이트동일, 미해결 주소는 mutate 없이skipped.표 구조 프리미티브
apply_table_ops(source, ops):delete_column(자유폭 재분배 + 열삭제로 빈 행 생기면 캐스케이드 삭제·rowSpan 붕괴)·delete_row·delete_table·insert_row_by_clone(rowSpan==1 참조행 복제, 서식보존·문단 id 리프레시 — 균등 재생성 금지). 각 편집 후build_grid검증(overlap/hole/oob)으로 무효면 거부(fail-closed, 원칙 VI). 중첩표 거부.실한컴 오라클 게이트
verify_fill(before, after, require=):resolve_oracle+visual_check로 before/after를 실제 한컴 렌더 대조 →render_checked·overflow·overlap(글자겹침)·page_count. 오라클 없으면 정직 degrade(render_checked=False),require=True면 fail-closed. open-safety/HTML 프리뷰를 한컴 수용으로 오인 금지.
비고
오라클 실증: 실제 3학년 양식에서
delete_column(반영비율 7→5열 캐스케이드)·insert_row_by_clone(세부기준 +행, 85병합 표)·content-complete 운영계획 채움이 실한컴에서 서식보존·clean 렌더. MCP 표면(apply_table_ops·verify_form_fill)은 hwpx-mcp-server 2.13.0에서 합류.
[2.20.0] - 2026-07-02
추가
M7 네이티브 자동 차례·상호참조 (S-062):
hwpx.tools.toc_author—add_native_toc(한컴 네이티브TABLEOFCONTENTS필드영역 + Command DSL,dirty=1기본 = 한컴이 처음 여는 순간 항목·차례 스타일·쪽번호를 재계산),add_page_crossref(쪽 번호CROSSREF필드 + 캐시 결과런 — 한컴이 편집/저장 시 자동 재계산),mark_toc_dirty(편집 후 재번호 재트리거),ensure_paragraph_anchor_id/outline_heading_paragraphs. 계약은 실제 한컴 저작 gold pair에서 리버스엔지니어링(tests/fixtures/m7_toc_gold/).차례 충실도 하니스:
hwpx.tools.toc_fidelity—parse_toc_model(하이퍼링크·평문 재생성 항목 모두),structural_report(오라클 없이도 CROSSREF↔차례 캐시 모순으로 stale 탐지),toc_verify(한컴 렌더 대조toc_correctness_ratio, 무오라클 시 정직unverified),grow_paragraph.Mac 오라클 새로고침 레그:
MacHancomOracle.refresh_document(+_refresh_hwpx_mac.applescript) — 열기→dirty 필드 재생성→제자리 저장→닫기. dirty-재생성 직후 같은 세션 PDF export가 이 한컴 빌드를 크래시시키는 실측 때문에 refresh와 render는 의도적으로 별도 세션.
수정
Mac 렌더 스크립트
waitForFile이%%EOF트레일러를 요구 — size>0만으로는 비동기 export 도중의 잘린 PDF를 캡처했다(실측).
비고
E2E 오라클 증명: 저작→새로고침→ratio 1.0(2/5/8쪽) → 재페이지네이션+
mark_toc_dirty→새로고침→ratio 1.0 + 페이지 SHIFT(2/7/10). 실측 수집 규칙:ContentsStyles:0:이 바탕글(스타일 0) 문단도 차례 항목으로 수집 — 본문은 본문(스타일 1) 등 비수집 스타일 권장. MCP 표면(add_toc·add_cross_reference·verify_toc)은 hwpx-mcp-server 2.12.0에서 합류.
[2.19.0] - 2026-07-02
추가
M6 런서식 충실 읽기 하니스 (S-060):
hwpx.tools.read_fidelity—resolve_run_spans(런별 bold/italic/underline/strikeout/color/size_pt/font/super-subscript를 charPr+fontface 해석),collect_notes(각주/미주 본문 + 본문 서식),roundtrip_fidelity/corpus_fidelity(콘텐츠-레벨 라운드트립 충실도),spans_fidelity/notes_fidelity비교기, 공개fontface_maps/run_span. 요소-카운트만 재던roundtrip_diff와 달리 charPr-해석 런-스팬 및 각주 본문의 무손실을 측정한다.strikeout은 shape 속성으로 정규화(항상 존재하는<hh:strikeout shape="NONE"/>가 상시-on으로 오독되던 문제 회피),underlinetypeNONE→None정규화.
비고
코퍼스 런서식 라운드트립 충실도 1.0(4075 런 / hwpxlib 47편). reading 차원 4→5(구조적 corpus-scale, 오라클 불요). 설치 MCP 표면 노출은 hwpx-mcp-server 2.11.0에서 합류.
[2.18.0] - 2026-07-01
추가
M5 개인정보(PII) 마스킹 엔진 (S-059):
hwpx.tools.pii—detect_pii/mask_pii/mask_value/PIIPolicy. 기계검증 세트(주민등록번호·휴대폰·이메일·카드+Luhn)는 항상-on high-confidence, 맥락형(계좌·주소·이름)은 라벨 게이트 low-confidence(과마스킹 방지). 필드 최소화minimize_fields, 가명Pseudonymizer(결정적 토큰맵), 비식별deidentify(불가역 salted-SHA256), 로그 위생PiiLogFilter/scrub_exception_message.메일머지·추출 경로 마스킹:
mail_merge(masking_policy=DEFAULT_POLICY)기본 ON — 명부 산출물의 기계검증 PII 자동 마스킹(마스킹 길이로 FitPolicy 재측정).export_text/export_html/export_markdown(masking_policy=...)opt-in 추출 마스킹(기본None= 내부 placeholder 탐지 보존).
비고
폼필(form-fill) 경로 마스킹·
scan_personal_info·전 경로 0-누출 게이트는 MCP 표면(hwpx-mcp-server) 단계에서 합류합니다.
[2.17.0] - 2026-06-30
추가
M4 변경추적(redline) 저작 (S-058):
HwpxDocument.add_tracked_insert/add_tracked_delete/add_tracked_replace— 에이전트가 변경추적(삽입/삭제/치환)을 작성자·일자와 함께 저작하고, 사람이 한컴 검토 리본에서 개별 수락/거부할 수 있습니다. 헤더trackChanges/trackChangeAuthorssurgical splice(작성자 dedup·표시 플래그) + 본문insertBegin/End·deleteBegin/End마크(charPrIDRef 상속, paraend=0). 한컴 수용성은 measure-first 스파이크로 입증(실 Windows 한컴 COMIsTrackChange=1·opens-clean·roundtrip + 검토 리본 수락→반영/거부→취소 확인).hwpx.tools.redline.verify_redline(before, after, *, oracle=None)— 구조 검증(변경 수·TcId 마크 연결·표시 플래그·opens-clean) +visual_checkrender_checked를 정직하게 fold(오라클 없으면unverified, 거짓 통과 없음).
수정
메모(코멘트) 본문이 숫자로 표시되던 버그:
attach_memo_field가 MEMO 필드 subList에 코멘트 내용 대신 메모 ID(숫자)를 넣어, 한컴이 메모 박스에 숫자를 렌더했습니다. 한컴 오라클 구조에 맞춰 subList에 코멘트 텍스트를 넣고MemoShapeIDRef(기본 65535)로 박스를 연결하도록 수정했습니다(실 Windows 한컴 검증).
비고
수락/거부는 사람이 한컴 검토 리본에서 수행합니다(COM accept 액션 미노출 — 정석 워크플로).
byte-identity: 미수정 part(ZIP 엔트리)는 byte-identical. 수정 섹션 내부의 문단단위 완전 byte-identical(surgical splice)은 stretch로 연기(한컴 렌더·수용엔 무영향).
[2.16.0] - 2026-06-29
추가
M3 문서 작성 (S-057):
create_document_from_plan이document_type(공문/보고서/가정통신문)을 보고 실제 한컴-harvest 프로파일(hwpx.design.compose)로 라우팅합니다. 미매칭 유형은 기존 제로베이스 경로를 유지하고-> HwpxDocument반환 계약을 보존합니다. 공문은 결문 메타document_plan.gyeolmun = {issuer, productionNumber, enforcementDate, disclosure}를 지원합니다.hwpx.design.profiles.home_notice— 실제 가정통신문에서 harvest한 디자인 프로파일.공문 구조 hard-gate:
inspect_official_document_style(source, *, document_type="공문")이 시행문 척추(수신·발신명의·시행·공개구분·끝.)를 ERROR 심각도로 검사하고structure_pass를 반환합니다. 표 셀까지 읽는 table-aware 텍스트 추출(실 시행문의 두문/결문은 표 안에 있음)을 추가했고, 진짜 시행문(tests/fixtures/m3_gongmun_gold/seoul_sihaengmun.hwpx)을 앵커로 삼습니다.inspect_document_authoring_quality에korean_proofing_status(정직unverified/llm_proofed_not_oracle_verified, 거짓 통과 없음)와verify_render=True시 실제 한컴 렌더 영수증render_checked/visual_complete를 추가했습니다.
비고
각주(footnote) 작성은 한컴 렌더가 확인되지 않아 honest-deferred(
unverified) 상태입니다.
[2.15.0] - 2026-06-27
추가
HwpxDocument.set_paragraph_format(keep_with_next=, keep_lines=, page_break_before=)— 문단 keep-together 플래그를 엔진ensure_paragraph_format(break_setting=)로 전달한다(새 paraPr 발행, 기존 paraPr 미수정 = 무손실). 시험지 조판 등에서 한 문항이 단/쪽 경계에서 잘리지 않게 묶을 때 쓴다.hwpx.exam: re-typeset an authored exam (Markdown) into a school form.hwpx— Exam IR + strict md parser, form profiler (role→existing form style), keep-together body composition (insert into the form’s body region, never append; 관리박스 + footer preserved byte-identical), and an oracle convergence drivercompose_exam_into_form. The driver renders via Hancom and, when the composed 문항 are in the extractable text layer, verifies 문항-split / overflow / placeholder integrity (inserting column/page breaks to converge); when they are not (forms whose body Hancom exports as vector curves) or no oracle is available, it returnsrender_checked/splits=None+needs_reviewrather than a silent pass (Constitution V — honest unverified).find_seal_anchor— 발신명의가 좁은 표 셀에서 여러 줄로 wrap된 경우도 앵커를 찾는 fallback(연속 줄 윈도우, 최대 3줄). 단일 줄 매칭이 우선이라 기존 동작 불변; spurious 다중줄 매칭은 윈도우·동일페이지로 차단.
수정
paragraph.add_picture—treat_as_char=True(inline)인데pos_overrides(PAPER relTo/offset)를 주면 모순된 inline/floating<hp:pos>를 방출하던 것을ValueError로 fail-fast. floating 배치는treat_as_char=False에서만.
[2.14.0] - 2026-06-25
추가
hwpx.form_fit.seal— 직인/관인 배치 + 규정 검사(M2 P3).find_seal_anchor(발신명의 줄의 끝글자=도장 중심),check_seal_placement(중심 tol·가림 글자 차별 pass/fail),seal_pos_offsets(PDF pt 앵커→PAPER HWPUNIT offset),place_seal(발신명의 소스 문단—표 셀까지 탐색—에 직인을 floating 스탬프; 오라클 검증 0.12pt, fail-closed, page/clamp 정직신호).hwpx.form_fit.wordbox.extract_image_boxes— 렌더된 PDF에서 임베디드 이미지(직인) rect 추출. 직인은 글자가 아니라 그림이라get_text로 안 잡힘.add_picture(treat_as_char=False, pos_overrides=, text_wrap=)— floating 그림 경로. PAPER 상대<hp:pos>(offset은 xs:nonNegativeInteger로 coerce) +textWrap(직인은IN_FRONT_OF_TEXT로 텍스트 안 밀고 위에 스탬프).mail_merge(fit_policy=, max_lines=)— fit-aware 배치(M2 P4 / FR-004). 각 placeholder 슬롯을 템플릿에서 한 번 측정(template-once-measure, advance-model·오라클 불필요)하고 레코드별로 fit. 넘침/결측 행을needsReview[]/skipped[](reason 코드 + retry advice)로 격리—자동 truncate 없음.[xlsx]extra(openpyxl)로 Excel/명부(.xlsx/.xlsm) 수용.
수정
임베디드 이미지 manifest
<opf:item>에isEmbeded="1"(OWPML 단일-d 철자) 방출 — 없으면 한컴이add_picture로 넣은 모든 그림을 렌더 드롭하던 잠복 버그(한컴 GUI 렌더로 확정).mail_merge가 표 셀 안 placeholder도 치환 —replace_text_in_runs(본문 전용)가 셀 런에 안 닿아 발신·결재/안내 표 안{{토큰}}이 미치환으로 남던 버그.
[2.13.0] - 2026-06-24
추가
hwpx.conformance— VisualComplete 적합성 코퍼스 + 배지 등급(plan §2 Phase G).hwpx-conformance run이 코퍼스를 4개 배지 등급(Open-Safe/Semantic-Safe/Form-Safe/VisualComplete)으로 채점하고 등급별 통과율을 산출합니다. 임계값은 엄격 기본값(구조 등급 100%, 폼셋 overflow 0%, VisualComplete ≥95%). golden 베이스라인(tests/conformance/golden/structural.json) 대비 회귀를 숫자로 감지하며(--check), CI가 구조 등급을 추적합니다. 어슈어런스 등급은 절대 섞지 않습니다(§0.0): 한컴이 없는 구조 실행은 VisualComplete를unverified로 보고하고, 오라클 실행(도달 가능한 한컴 백엔드)만 VisualComplete를 검증합니다. 케이스에before(+선택적editMask)를 선언하면 VisualComplete가 오라클의 before/after diff 경로로 게이트됩니다(마스크 밖 변경·글자 겹침을 잡음).expectVisualDefect는 일부러 깨뜨린 쌍을 positive control로 삼아 게이트가 결함을 실제로 잡는지 검증합니다. (실측: 실제 한컴-저장 코퍼스에서 clean 쌍은 통과, out-of-slot 변경은 catch.)
[2.12.0] - 2026-06-24
추가
hwpx.quality— 단일 저장 게이트SavePipeline,QualityPolicy,VisualCompleteReport. 모든 직렬화 출력이 이 게이트를 통과합니다(무결성·XML·OPC/ID·열림안전·시각 오라클 → 단일 리포트).HwpxDocument.save_report(...)로 노출.hwpx.form_fit— FormFit 엔진(FitPolicy/FitResult). 폼 값이 셀/필드 박스에 맞는지 측정(한글=1.0em, 한컴 실측 보정)해 wrap/shrink/truncate/fail 처리.set_cell_text(fit=...)/fill_form_field(fit_policy=...)로 연결.hwpx.layout— 렌더러 없는 구조적 시각 스모크lint_layout(stale lineseg·dirty/lineseg·overflow risk·표 구조).QualityPolicy.layout_lint로 SavePipeline 하드 게이트로 연결.hwpx.design— 검증된 한컴 저장 템플릿 + harvest 프래그먼트로 새 문서를 생성하는compose/DocumentPlan/profile 빌더.official_notice/report/application_form프로파일 동봉.시각 오라클
hwpx.visual에 Mac 한컴 백엔드(MacHancomOracle) 추가.
[2.11.1] - 2026-06-12
수정
create_document_from_plan()의headingblock과 builderHeading이 기본 템플릿의개요 N/Outline N문단 스타일을 실제로 적용하도록 수정했습니다. 생성 문서가 한컴 개요/문서 탐색과 MCP outline readback에서 구조화된 제목으로 인식됩니다.document-plan 기본 스타일 preset에 제목 18pt, 부제 12pt, 장 제목 14pt 글자 크기와 함초롬바탕 폰트를 적용해 보고서 생성 시 제목/본문 시각 위계가 명확하게 보이도록 했습니다.
[2.11.0] - 2026-06-12
추가
시드 결정적 퍼징 수렴 루프
hwpx.tools.fuzz(시나리오 카탈로그·생성기·3중 오라클 러너·최소화)와tests/fixtures/fuzz_regressions회귀 박제 수트를 추가했습니다.레이아웃 근사 프리뷰 렌더러
hwpx.tools.layout_preview를 추가했습니다(페이지 박스·표·여백 근사 HTML/PNG — 에이전트 자기검증용).section XML 바이트 splice 기반 문단 패치 경로
hwpx.patch를 추가했습니다(미수정 영역 바이트 보존).그림 자산 안전 삽입·치환 API(
add_picture및 치환 워크플로)와 manifest 검증을 추가했습니다.기존 문서 서식 편집 API를 추가했습니다: 문단 정렬·줄간격·들여쓰기·문단 간격, 용지·여백·방향, 머리말/꼬리말·쪽번호, 불릿/번호 형식.
누름틀(클릭히어 필드) 1급 조회·채움 API를 추가했습니다.
공문서 작성규정 lint
hwpx.tools.official_lint(항목기호 위계·”끝.” 표시·붙임·날짜 표기)와 결재란 프리셋을 추가했습니다.고급 생성기
hwpx.tools.advanced_generators를 추가했습니다: 사진대지(build_image_grid)·회의 명패(build_meeting_nameplates)·표 기반 조직도.신구대조 문단 diff와 참조 정합 lint
hwpx.tools.doc_diff를 추가했습니다.메일머지 대량 생성과 표 합계·평균 계산 유틸
hwpx.tools.mail_merge를 추가했습니다.참조 문서 서식 프로파일 추출·적용과 템플릿 레지스트리
hwpx.tools.style_profile을 추가했습니다.template analyzer 리포트를 강화했습니다(열너비 재구성·cell margin·vertAlign).
변경
hwpx.oxml.document모놀리스(5,700여 줄)를 요소별 모듈(_document_impl외 18개)로 분할했습니다. 공개 API는 변하지 않습니다.
수정
신뢰할 수 없는 입력 파싱을 강건화했습니다(
hwpx.opc.security): XML entity 선언 거부와 깊이/크기 한도, ZIP 압축비·멤버 수 한도를 적용해 entity 폭탄·압축 폭탄 입력을 안전하게 거부합니다.
[2.10.3] - 2026-06-09
추가
hwpx.tools.validate_editor_open_safety()와EditorOpenSafetyReport를 추가해 package validation, document validation, 재오픈 검증을 한 곳에서 확인할 수 있게 했습니다.
수정
텍스트를 줄이는 저수준 편집 뒤 stale
hp:linesegarray가 남아 한컴 편집기에서 열리지 않을 수 있는 문제를 막기 위해, 저장 직전 plain-text 문단의 무효한 layout cache를 제거합니다.편집된 section과 public 저수준 section write 경로는 모든
hp:lineSegArraylayout cache를 제거해, 복합 문단처럼 stale 여부를 안전하게 계산하기 어려운 경우도 편집기가 다시 계산하도록 했습니다.public 저수준 section/header XML write 경로도 Hancom-compatible root namespace 선언과
standalone="yes"XML declaration을 보정해 generic XML serializer 출력이 그대로 저장되지 않도록 했습니다.HwpxDocument.to_bytes(),save_to_path(),save_to_stream()이 생성된 패키지의 editor-open safety를 확인한 뒤에만 결과를 반환하거나 쓰도록 보강했습니다.save_to_path()는 safety 실패 시 기존 대상 파일을 교체하지 않습니다.HwpxPackage.save()도 editor-open safety를 기본 검증해 저수준 package 직접 편집이 unsafe HWPX를 bytes/path/stream으로 내보내지 않도록 막습니다.public
HwpxPackage.save()에서 editor-open safety 검증을 우회하는 파라미터를 제공하지 않도록 정리했습니다. 검증 실패 상태의 bytes snapshot은 package 내부 진단 토큰이 있는 경로에서만 생성하며, unchecked 경로는 caller-provided file path/stream에 직접 쓸 수 없습니다.HwpxDocument._to_bytes_raw()의 open-safety bypass 인자를 제거해 document 객체에서 unchecked bytes를 얻는 실수성 우회 경로를 더 좁혔습니다.private archive writer도 save 내부 컨텍스트에서만 동작하게 해,
_write_archive()/_write_zip_entry()직접 호출로 editor-open safety 검증을 건너뛴 ZIP을 만드는 실수성 우회 경로를 막았습니다.문서/package 저장 중 open-safety, 실제 파일 쓰기, stream short write가 실패하면 dirty 상태를 성공처럼 정리하지 않도록 보강했습니다. seek 가능하고 안전하게 복원 가능한 stream은 쓰기 실패 시 원래 내용으로 rollback합니다.
archive pack CLI가 재패킹 결과를 editor-open safety 리포트로 재검증하고, 실패 시 기존 output을 보존합니다. 성공 시
PackResult.open_safety와 CLIopen_safety_ok=true출력으로 handoff evidence를 제공합니다.repair/recover 출력도 CRC와 package validation 뒤에 editor-open safety를 재검증하고, section의
hp:lineSegArraylayout cache 제거와 section/header root namespace 및standalone="yes"보정을 적용합니다. 실패 시 기존 output은 보존하고, 성공 시RepairResult.open_safety로 handoff evidence를 제공합니다.template form-fit apply가 최종 목적지에 먼저 복사하지 않고 temp 파일에서 저장 및 editor-open safety 검증을 끝낸 뒤에만 교체하도록 보강했습니다.
builder
Document.save_to_path()리포트에editor_open_safetyhard gate와 세부 리포트를 포함합니다.template form-fit paragraph clone 경로가 텍스트를 직접 바꿀 때도 layout cache를 제거합니다.
package validator가 실제 텍스트 길이를 넘어서는
lineseg/@textpos를 hard error로 보고하도록 보강했습니다.EditorOpenSafetyReport.ok가 package/reopen뿐 아니라 document validation 실행 실패와 hard error도 반영하도록 보강했습니다.저장 직전 paragraph의
styleIDRef가Normal/본문처럼 header style 이름으로 잘못 들어간 경우, 일치하는 numeric style id로 정규화해 저수준 편집 산출물이 document validation에서 차단되거나 편집기 오픈 리스크를 만들지 않도록 했습니다.HwpxPackage.save(updates=...)같은 순수 package 저수준 저장 경로도 header style 이름으로 된 paragraphstyleIDRef를 numeric id로 정규화해, MCP를 거치지 않는 직접 ZIP/XML 편집 산출물도 같은 safety 보정을 받도록 했습니다.
[2.10.2] - 2026-06-06
추가
hwpx.tools.markdown_export.export_markdown()와HwpxDocument.export_rich_markdown()을 추가해 풍부한 Markdown 변환을 지원합니다. 인라인 서식(굵게/기울임/취소선/색상/하이라이트), 표 병합 셀(colspan/rowspan HTML), 중첩 표 재귀,rect/ellipse/polygon도형 내부 paragraph, BinData 이미지 추출,Ⅰ./1.패턴 기반 헤딩 감지(#/##), 각주·미주(정확 위치 마커 +fn1/en1일련번호 + 본문 인라인 서식), 하이퍼링크([text](url)) 보존을 한 번에 처리합니다. 기존HwpxDocument.export_markdown()은 그대로 유지됩니다.HwpxOxmlNote에 본문 paragraph 접근/편집 helper를 추가했습니다:body_paragraphproperty,add_run(text, *, char_pr_id_ref=..., bold=..., italic=..., underline=..., color=..., font=..., size=..., highlight=..., strike=..., attributes=...),add_hyperlink(url, display_text, *, char_pr_id_ref=...). XML 직접 조작 없이 각주 본문에 혼합 서식 run과 하이퍼링크를 추가할 수 있습니다.get_table_map()결과에 본문 표 anchorlocation, 셀 문단별table_cell_paragraphlocation,caption_text,preceding_paragraph_text를 추가했습니다.새 컨버터와 helper에 대한 회귀 테스트를
tests/test_markdown_export.py에 추가했습니다.
변경
HwpxOxmlTableCell.text가 셀 내부 여러 문단을 줄바꿈으로 보존하고,set_text(..., preserve_format=True, split_paragraphs=True)경로에서 기존 runcharPrIDRef를 유지하도록 개선했습니다.
수정
HwpxOxmlParagraph.add_footnote()/add_endnote()의char_pr_id_ref인자가 외부 호스팅 run에만 적용되고 각주 본문 run은 항상charPrIDRef="0"으로 하드코딩되던 문제를 수정했습니다. 인자가 사용자 의도대로 본문 run에도 적용됩니다.
[2.10.1] - 2026-06-04
추가
document_planauthoring을 builder lowering 중심으로 확장하고 v2 builder node, TOC, government_report preset을 지원합니다.정부보고서 계산/파싱 유틸리티(
hwpx.tools.report_utils,hwpx.tools.report_parser)와 computed field 치환을 추가했습니다.generic element coverage inventory, table cleanup, table profile/caption/unit preservation, id reference integrity checker를 추가했습니다.
linesegarray,transMatrix,scaMatrix,rotMatrix, edit/combo box control을 first-class OXML 모델로 승격했습니다.
변경
builder save report의 hard gate가 id integrity를 실제 검사 결과로 반영하도록 강화했습니다.
패키지 rewrite 시
mimetype엔트리를 보존하도록 OPC 저장 경로를 정리했습니다.
[2.10.0] - 2026-06-02
추가
hwpx.builder공개 패키지를 추가했습니다.Document,Section,Paragraph,Run,Heading,Bullet,NumberedList,Table,Image,Header,Footer,PageNumber,PageBreak,Metadata,PageSize,Margins노드로 조립형 HWPX 생성을 지원합니다.BuilderSaveReport와ReopenReport를 추가해 builder 저장 후 package validation, document error/lint, reopen, feature flags, visual review 필요 여부를 확인할 수 있게 했습니다.머리글/바닥글 리치 content, 자동 쪽번호, 리치 런 서식(color/font/size/highlight/strike), 다단계 목록, 표 병합/음영/열너비, 이미지 배치를 위한
HwpxDocumentfacade 및 OXML wrapper 메서드를 추가했습니다.hwpx.document_plan.v1, 운영 계획서 품질 프로필, template form-fit authoring, proposal/form-fill 품질 검증 흐름을 강화했습니다.hwpxlib sample corpus 기반 oracle fixture와 builder vertical slice 통합 테스트를 추가했습니다.
src/hwpx/tools/_schemas/owpml/에 2011 Hancom 네임스페이스용 subset XSD 번들을 추가했습니다 (header.xsd,body.xsd,paralist.xsd,core.xsd,xml.xsd,NOTICE).hwpx.oxml.load_compound_schema()와SchemaImportError를 추가해 offline compound XSD 로딩을 지원합니다.fixture matrix 기반 Phase 1 validation 리포트(
shared/hwpx/HWPX_STACK_VALIDATION_2026-04-20_pre-phase1.md,..._post-phase1.md)와 회귀 테스트를 추가했습니다.
변경
validate_document().ok는 error 기준으로 유지하고 schema warning은 lint/warning으로 분리해 가시화합니다.HwpxDocument.save_to_path()기반 저장/재오픈 검증 경로를 builder와 authoring workflow에서 일관되게 사용하도록 정리했습니다.hwpx-validate는 이제 기본 strict 모드로 Phase 1 subset schema bundle을 사용합니다.--no-strict로 warning-only 분류를 지원합니다.HwpxDocument.validate()는 기본strict=False로 동작하며,validate_on_save_strict옵션으로 저장 시 strict 검증을 제어할 수 있습니다.패키지 배포물(sdist/wheel)에 OWPML subset schema bundle이 포함되도록 package-data를 확장했습니다.
수정
split-run placeholder, template form-fit, proposal/document-plan 생성 경로의 회귀를 보강했습니다.
builder vertical slice에서 Hancom Office HWP 재오픈과 구조 hard gate가 통과하도록 머리글/바닥글 lowering과 page number control 배치를 정렬했습니다.
[2.9.1] - 2026-04-27
상호운용성(interop) 버그 묶음 릴리즈입니다. 외부 기여자들이 보고하고 수정한 세 가지 문제를 정리합니다.
수정
HwpxOxmlTableCell._ensure_text_element와ensure_run_style내 modifier가 lxml 엘리먼트 상에서 또한ET.SubElement를 호출해TypeError를 발생시키던 경로를 기본 헬퍼_append_child로 정리했습니다. 이제cell.text = ...와paragraph.add_run(..., bold=True)가 monkey-patch 없이 정상 동작합니다 (#30, @hhy827)._paragraph_id/_object_id/_memo_id가uuid4().int & 0xFFFFFFFF로부터 signed int32 범위를 벗어나는 값을 약 50% 확률로 생성하던 문제를 수정했습니다. id 값을 signed 32-bit 양수 범위(0 <= x < 2^31)로 클램프해 downstream 소비자와의 상호운용성을 확보했습니다 (#34, @seonghoony).HwpxDocument.new()의 seed로 쓰이는 번들Skeleton.hwpx에 signed int32 범위를 벗어나는<hp:p id="3121190098">가 포함돼 있던 문제를 수정했습니다 (#35, @seonghoony).pyproject.toml에 PEP 639licenseexpression과 같이 남아 있던 legacyLicense :: OSI Approved :: Apache Software Licenseclassifier를 제거해setuptools>=77에서의 소스 설치/바이너리 빌드 실패를 해소했습니다.
추가
위 세 버그에 대한 회귀 테스트를 추가했습니다 (
tests/test_document_formatting.py,tests/test_id_generator_range.py,tests/test_skeleton_template_ids.py).머지된 기여를 인정하는
CONTRIBUTORS.md를 추가하고README.md/CONTRIBUTING.md에서 연결했습니다.
변경
License relicensed to Apache-2.0 (sole author, full consent). Previous license terms no longer apply to future releases.
[2.9.0] - 2026-04-02
추가
HwpxDocument.get_table_map(),find_cell_by_label(),fill_by_path()를 추가해 HWPX 양식/템플릿 표를 문서 순서 기반으로 탐색하고 채울 수 있게 했습니다.hwpx.tools.table_navigation모듈을 추가해 엔진 레벨에서 재사용 가능한 표 탐색, 라벨 정규화, 방향 이동, 배치 채우기 helper를 공개했습니다.
변경
라벨 매칭이 공백 축약, 대소문자 무시, 후행 콜론 허용 규칙을 따르도록 정규화 로직을 추가했습니다.
표 자동화 API에 대한 회귀 테스트와 README/API 레퍼런스 문서를 추가했습니다.
[2.8.3] - 2026-03-10
변경
저장소와 배포 메타데이터의 라이선스 표기를 실제
LICENSE파일과 일치하도록 정렬했습니다.pyproject.toml을 PEP 639 방식의LicenseRef-python-hwpx-NonCommercial+license-files구성으로 갱신하고, 잘못된 MIT 분류자를 제거했습니다.README 라이선스 배지/섹션을 커스텀 비상업적 라이선스 기준으로 수정하고, wheel/sdist 산출물의 라이선스 메타데이터를 검증하는 회귀 테스트를 추가했습니다.
[2.8.2] - 2026-03-08
변경
README를 현재 공개 API와 CLI 범위에 맞춰 정리했습니다. Quick start, 텍스트 추출, 객체 검색 예시를 실제 호출 방식 기준으로 수정했습니다.
add_memo()/add_memo_with_anchor()가HwpxDocument.new()로 만든 실제lxml기반 문서에서도 동작하도록 memo XML 생성 경로를 엔진 호환 방식으로 정리했습니다.실제 빈 문서 템플릿에서 메모 추가 후 roundtrip 되는 회귀 테스트를 추가했습니다.
[2.8.1] - 2026-03-08
추가
템플릿 자동화 회귀 스위트를 추가했습니다 (
tests/template_automation/). 단순 토큰, 반복 토큰, split-run, 공백 정규화, 표/머리글/바닥글/다중 섹션, 체크박스 토글, extract-repack, 비표준 rootfile 패턴을 대표 fixture + 시나리오 계약으로 점검합니다.DevDoc/template-automation-regression-suite.md를 추가해 스위트의 보장 범위, 한계, fixture 추가 절차를 문서화했습니다.
변경
실제
lxml기반 문서에서set_header_text()/set_footer_text()가 동작하도록 header/footer 생성 경로를 XML 엔진 호환 방식으로 정리했습니다.섹션 속성(
secPr)이 비어 있을 때 보강 생성하는 경로를 XML 엔진 호환 방식으로 정리했습니다.add_section()이 새 섹션을 잘못된 네임스페이스로 만들던 문제를 수정했습니다.mypy/pyright gradual scope에 이번에 추가한 template automation helper/generator 모듈을 포함했습니다.
[2.8] - 2026-03-08
변경
HwpxPackage와 OXML 로딩/저장이 rootfile/manifest-relative 경로를 실제로 따르도록 정렬했습니다.hwpx-analyze-template --extract-dir가 재구성에 바로 쓸 수 있는 작업 디렉터리와.hwpx-pack-metadata.json을 생성하도록 확장했습니다.hwpx-validate-package를 엔진 정합 기준으로 재작성해 dynamic rootfile/manifest 관계, CRC, fallback warning을 구분하도록 했습니다.hwpx-unpack기본값을 raw-byte preserving으로 바꾸고--pretty-xmlopt-in을 추가했습니다.tooling/OPC 회귀 테스트를 확대하고, coverage threshold를 60으로 올렸으며, pyright는 touched OPC/tooling 범위에서
basic으로 상향했습니다.
[2.7.1] - 2026-03-08
변경
공개 저장소와 배포 산출물에서 내부 감사 문서를 제거했습니다.
[2.7] - 2026-03-08
추가
hwpx-unpack,hwpx-pack,hwpx-analyze-templateCLI를 추가했습니다.src/hwpx/tools/archive_cli.py를 추가해 unpack/pack 워크플로를 패키지 레벨 도구로 승격했습니다.unpack 시
.hwpx-pack-metadata.json을 기록하고, pack 시 이를 사용해 원본 ZIP 엔트리 순서/압축 방식을 가능한 범위에서 보존하도록 했습니다.src/hwpx/tools/template_analyzer.py를 추가했습니다.
변경
scripts/office/unpack.py,scripts/office/pack.py,scripts/analyze_template.py를 패키지 도구 래퍼로 정리했습니다.page_guard에 shape/control count 및 히스토그램 비교를 추가하고, 실제 페이지 수 계산기가 아니라 구조 변화 징후 점검 도구임을 문서와 CLI 설명에 명시했습니다.README와
docs/usage.md에 새 CLI 사용 예시를 추가했습니다.새 tooling에 대한 CLI/추출/overwrite/page-guard 회귀 테스트를 강화했습니다.
[2.6] - 2026-03-08
추가
hwpx-validate-packageCLI와hwpx.tools.package_validator를 추가해 ZIP/OPC/HWPX 패키지 구조,mimetype,container.xml, manifest/spine 참조, XML well-formedness를 점검할 수 있게 했습니다.hwpx-page-guardCLI와hwpx.tools.page_guard를 추가해 섹션 수, 단락 수, page/column break, 표 구조, 텍스트 길이 변화량을 기준으로 문서 드리프트를 비교할 수 있게 했습니다.hwpx-text-extractCLI를 추가해 기존TextExtractor기능을 plain/markdown 형태로 바로 사용할 수 있게 했습니다.scripts/office/unpack.py,scripts/office/pack.py,scripts/analyze_template.py를 추가해 XML-first HWPX 작업 흐름을 지원합니다.gap-closure 반영분에 대한 테스트를 추가했습니다 (
tests/test_gap_closure_tools.py).
수정
HwpxDocument.validate()가 내부 직렬화 과정에서 dirty 상태를 지워 버리던 부작용을 제거해, 검증 이후에도 저장 필요 상태가 유지되도록 수정했습니다.
[2.3.1] - 2026-02-28
추가
단락 삭제 API:
paragraph.remove(),section.remove_paragraph(),document.remove_paragraph()메서드를 추가했습니다. 마지막 단락 삭제 시ValueError가 발생합니다.섹션 추가/삭제 API:
document.add_section(after=),document.remove_section()메서드를 추가했습니다. 새 섹션은 manifest/spine에 자동 등록되며, 마지막 섹션 삭제 시ValueError가 발생합니다.네임스페이스 상수 모듈:
hwpx.oxml.namespaces모듈을 추가하여 HP, HH, HC 등 공유 네임스페이스 상수를 제공합니다.새 API에 대한 16개 테스트 케이스를 추가했습니다 (
test_paragraph_section_management.py).
수정
import hwpx만으로DeprecationWarning이 발생하던 문제를 수정했습니다.hwpx.package경고는 이제 사용자가 직접 해당 모듈을 import할 때만 표시됩니다.HwpxOxmlTableCell.text가 셀에 여러 단락이 있을 때 첫 번째 텍스트만 반환하던 버그를 수정했습니다. 모든<hp:t>요소의 텍스트를 결합하여 반환합니다.add_hyperlink()메서드에서 사용되지 않는field_inst_id변수를 제거했습니다.deprecated
save()호출을 사용하던 테스트 코드를save_to_path()/save_to_stream()으로 업데이트했습니다.
[1.9] - 2026-02-18
변경
hwpx.__version__하드코딩 값을 제거하고importlib.metadata.version("python-hwpx")기반으로 노출하도록 정리했습니다.editable/로컬 소스 실행처럼 배포 메타데이터가 없는 환경에서도 동작하도록
PackageNotFoundErrorfallback(0+unknown)을 추가했습니다.
[0.1.0] - 2025-09-17
추가
hwpx.opc.package.HwpxPackage와hwpx.document.HwpxDocument를 포함한 핵심 API를 공개했습니다.텍스트 추출, 객체 탐색, 문서 유효성 검사 등 도구 모듈과
hwpx-validateCLI를 제공합니다.HWPX 스키마 리소스와 예제 스크립트를 번들링해 바로 사용할 수 있도록 했습니다.
설치 가이드, 사용 예제, 스키마 개요 등 배포 문서를 정리했습니다.