From a9f9d6c6b0772c1599e38e3e0732533806d213c8 Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:25:35 +0900 Subject: [PATCH 01/15] fix(media): refuse a picture alignment Hancom does not read add_picture wrote any align it was given into hp:pos@horzAlign ("BOGUS", " RIGHT ", "TOP"), which Hancom reads as LEFT; a non-string failed only after the image and its paragraph were added. The alignment is now checked first, in any case, against the schema's horizontal alignments, and anything else is refused with shape-position-frame before anything is stored. Co-Authored-By: Claude Opus 5.5 --- src/hwpx/_document/media.py | 2 ++ src/hwpx/oxml/objects.py | 4 ++-- src/hwpx/oxml/shape_position.py | 10 ++++++++++ tests/test_image_object_workflow.py | 20 ++++++++++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/hwpx/_document/media.py b/src/hwpx/_document/media.py index 815c091a..274c76d5 100644 --- a/src/hwpx/_document/media.py +++ b/src/hwpx/_document/media.py @@ -19,6 +19,7 @@ ) from ..oxml import HwpxOxmlInlineObject, HwpxOxmlParagraph from ..oxml.namespaces import HC, HP +from ..oxml.shape_position import validate_picture_align from ._units import _mm_to_hwp_units if TYPE_CHECKING: @@ -114,6 +115,7 @@ def add_picture( ) -> HwpxOxmlInlineObject: """Embed image data and place a picture object in a new paragraph.""" + validate_picture_align(align) # before anything is stored: Hancom reads an unknown one as LEFT # Call the local primitive directly rather than `doc.add_image` — that # facade name moved in 6.0 (design table row 33), and going through it # would fire a DeprecationWarning on every `add_picture` call even though diff --git a/src/hwpx/oxml/objects.py b/src/hwpx/oxml/objects.py index 596b8fb5..ec2e4913 100644 --- a/src/hwpx/oxml/objects.py +++ b/src/hwpx/oxml/objects.py @@ -25,7 +25,7 @@ _paragraph_id, ) from .shape_position import ( - _shape_set_position, build_at_original_size, resize_group, validate_draw_text_vert_align, + _shape_set_position, build_at_original_size, resize_group, validate_draw_text_vert_align, validate_picture_align, ) if TYPE_CHECKING: @@ -746,7 +746,7 @@ def _create_picture_element( if align: pos = el.find(f"{_HP}pos") if pos is not None: - pos.set("horzAlign", align.upper()) + pos.set("horzAlign", validate_picture_align(align) or "LEFT") if pos_overrides: pos = el.find(f"{_HP}pos") if pos is not None: diff --git a/src/hwpx/oxml/shape_position.py b/src/hwpx/oxml/shape_position.py index ea841689..7305fc58 100644 --- a/src/hwpx/oxml/shape_position.py +++ b/src/hwpx/oxml/shape_position.py @@ -61,6 +61,16 @@ def _require_member( ) +def validate_picture_align(align: object) -> str | None: + """A new picture's *align* as its ``hp:pos@horzAlign``, any case (``"left"`` is ``LEFT``); ``None`` when it is + not given. Hancom reads a value outside :data:`POS_HORZ_ALIGN` as ``LEFT``, so one is refused.""" + + if align is None or align == "": + return None + return _require_member(align.upper() if isinstance(align, str) else align, POS_HORZ_ALIGN, argument="align", + code="shape-position-frame") + + def validate_draw_text_vert_align(vert_align: str | None) -> str | None: """Check ``set_draw_text``'s *vert_align* (``hp:subList/@vertAlign``).""" diff --git a/tests/test_image_object_workflow.py b/tests/test_image_object_workflow.py index 8703252f..b4385351 100644 --- a/tests/test_image_object_workflow.py +++ b/tests/test_image_object_workflow.py @@ -228,6 +228,26 @@ def test_an_aligned_inline_picture_takes_the_paragraph_alignment() -> None: assert _picture_paragraph_alignment(document) == align.upper() +@pytest.mark.parametrize("align", ["BOGUS", "TOP", " right ", 1]) +def test_a_picture_alignment_outside_the_schema_is_refused_before_anything_is_added(align: object) -> None: + # Hancom reads a horizontal alignment it does not know as LEFT; the picture was written with it as given. + document = HwpxDocument.new() + paragraphs, images = len(document.paragraphs), len(document.list_images()) + + with pytest.raises(HwpxValueError) as caught: + document.add_picture(PNG_1X1, "png", width=7200, height=7200, align=align) # type: ignore[arg-type] + + assert caught.value.code == "shape-position-frame" + assert (len(document.paragraphs), len(document.list_images())) == (paragraphs, images) + + +def test_a_picture_takes_the_inside_and_outside_alignments_too() -> None: + for align in ("INSIDE", "outside"): + document = HwpxDocument.new() + document.add_picture(PNG_1X1, "png", width=7200, height=7200, align=align) + assert _first_picture(document).find(f"{HP}pos").get("horzAlign") == align.upper() + + def test_a_picture_without_align_keeps_the_paragraph_alignment() -> None: document = HwpxDocument.new() document.add_picture(PNG_1X1, "png", width=7200, height=7200) From ca46d844f3f40ae2022c06181ca36f14f685639f Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:25:53 +0900 Subject: [PATCH 02/15] docs(changelog): note the fix Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07bd7d57..d3aa8a1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -257,6 +257,10 @@ 자리에 겹쳐 두어 감춘다. 감춘 문단은 자리를 차지하지 않고 쪽을 만들지 않으며, 다음 글은 다음 쪽 맨 위에서 시작한다. 셋째 빈 문단부터, 쪽을 나누는 문단, 빈칸이 든 문단은 감추지 않는다. 전에는 그런 빈 문단을 다음 쪽으로 넘겨, 문서가 빈 문단으로 끝나면 빈 쪽을 하나 더 세기도 했다. +- 그림 넣기(`doc.add_picture`)가 `align`에 받은 값을 검사 없이 `hp:pos@horzAlign`에 쓰던 것을 고친다. + - 한/글은 모르는 정렬 값을 왼쪽(LEFT)으로 읽는다. 정수 같은 값은 그림과 문단을 더한 뒤에야 오류가 났다. + - 이제 대소문자와 상관없이 `LEFT`·`CENTER`·`RIGHT`·`INSIDE`·`OUTSIDE`만 받는다. 그 밖의 값은 아무것도 더하기 + 전에 `shape-position-frame` 오류로 거부한다. - 쪽 수 추정(실험, `estimate_pages`)이 칸 단위로 나누는 흐르는 표에서 문단이 여럿인 칸을 줄 간격 하나로만 세어 나누던 것을 고친다. 한/글은 문단 사이 간격(앞 문단의 아래 간격과 다음 문단의 위 간격)도 세고, 다음 쪽 부분이 문단의 첫 줄로 시작하면 그 문단의 위 간격을 그대로 둔다. 이제 그런 칸은 줄마다 제 자리로 나눈다. 전에는 문단 From 6fa9e6de1edfbf5d9cad4e41fad2540d51756a0f Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:43:08 +0900 Subject: [PATCH 03/15] fix(shapes): refuse sizes and corner ratios Hancom reads as 0 A negative width or height of a rectangle, ellipse, arc, picture or equation box, or a negative rectangle corner ratio, was written as given; Hancom reads it as 0. Such values, and sizes of 2**31 or more, are now refused with typed errors before any paragraph, run or image is added (shape-size-value, shape-rect-ratio-value). An equation's base_unit outside 1..2**31-1 raises shape-equation-base-unit-value (a ValueError, as before) before its paragraph is added. HwpxOxmlShape.resize refuses a negative size too, and HwpxOxmlParagraph.add_picture builds the picture before adding its run. Co-Authored-By: Claude Opus 5.5 --- docs/error-codes.md | 5 +- src/hwpx/_document/media.py | 15 +- src/hwpx/_document/shapes.py | 15 +- src/hwpx/errors.py | 3 + src/hwpx/oxml/objects.py | 7 +- src/hwpx/oxml/paragraph.py | 15 +- src/hwpx/oxml/shape_position.py | 46 +++++ ...equation_base_unit_largest_given_size.hwpx | Bin 0 -> 7647 bytes .../equation_base_unit_largest_measured.hwpx | Bin 0 -> 7577 bytes .../hancom_saved/equation_size_negative.hwpx | Bin 0 -> 7557 bytes .../hancom_saved/picture_size_negative.hwpx | Bin 0 -> 8208 bytes .../shape_rect_ratio_negative.hwpx | Bin 0 -> 7755 bytes .../shape_rect_size_negative.hwpx | Bin 0 -> 7729 bytes tests/test_shape_size_values.py | 193 ++++++++++++++++++ 14 files changed, 277 insertions(+), 22 deletions(-) create mode 100644 tests/fixtures/hancom_saved/equation_base_unit_largest_given_size.hwpx create mode 100644 tests/fixtures/hancom_saved/equation_base_unit_largest_measured.hwpx create mode 100644 tests/fixtures/hancom_saved/equation_size_negative.hwpx create mode 100644 tests/fixtures/hancom_saved/picture_size_negative.hwpx create mode 100644 tests/fixtures/hancom_saved/shape_rect_ratio_negative.hwpx create mode 100644 tests/fixtures/hancom_saved/shape_rect_size_negative.hwpx create mode 100644 tests/test_shape_size_values.py diff --git a/docs/error-codes.md b/docs/error-codes.md index fdd53181..c110bf2b 100644 --- a/docs/error-codes.md +++ b/docs/error-codes.md @@ -30,7 +30,7 @@ except HwpxError as exc: | 형태 | `style-not-found` | `VISUAL_COMPLETE_FAILED` | | 쓰임 | 예외 분기 | **발행된 영수증 스키마의 필드값** | | 관리 | major 경계 | 영수증 스키마 버전 | -| 개수 | 176 | 11 | +| 개수 | 179 | 11 | 통합하지 않는 이유: quality 코드는 `hwpx.mutation-report/v1` 과 `VisualCompleteReport` 에 이미 실려 나간 값이다. 이름을 바꾸면 영수증을 읽는 @@ -285,6 +285,7 @@ except HwpxError as exc: | `shape-drop-cap-character-empty` | 드롭캡으로 키울 문자가 비어 있다. | | `shape-drop-cap-not-created` | 만든 드롭캡을 표준 섹션 스캔이 다시 찾지 못했다. | | `shape-drop-cap-style-unsupported` | 드롭캡 dropcapstyle 값이 실증된 어휘(DoubleLine, TripleLine) 밖이다. | +| `shape-equation-base-unit-value` | 수식 기준 크기(base_unit)가 1 이상 2**31 미만의 int(1/100 pt)가 아니다(bool 도 거부). | | `shape-equation-not-created` | 만든 수식을 표준 스캔이 다시 찾지 못했다. | | `shape-equation-not-verbatim` | 만든 수식이 스크립트를 그대로 담지 않았다. | | `shape-equation-script-empty` | 수식 스크립트가 비어 있다. | @@ -294,6 +295,8 @@ except HwpxError as exc: | `shape-position-frame` | 도형 기준 프레임·정렬 값이 OWPML 어휘(hp:pos 의 vertRelTo/horzRelTo/vertAlign/horzAlign) 밖이다. | | `shape-position-unsupported` | 위치를 바꿀 도형이 떠 있는 개체가 아니거나 hp:pos 가 없다. | | `shape-position-value` | 도형 오프셋이 signed 32-bit 정수(HWPUNIT)가 아니다. | +| `shape-rect-ratio-value` | 사각형 모서리 곡률(ratio)이 0 이상 2**31 미만의 int(%)가 아니다(bool 도 거부). | +| `shape-size-value` | 도형·그림·수식 크기가 0 이상 2**31 미만의 int(HWPUNIT)가 아니다(bool 도 거부). | ### `story-*` diff --git a/src/hwpx/_document/media.py b/src/hwpx/_document/media.py index 274c76d5..20be1c9c 100644 --- a/src/hwpx/_document/media.py +++ b/src/hwpx/_document/media.py @@ -19,7 +19,7 @@ ) from ..oxml import HwpxOxmlInlineObject, HwpxOxmlParagraph from ..oxml.namespaces import HC, HP -from ..oxml.shape_position import validate_picture_align +from ..oxml.shape_position import validate_picture_align, validate_shape_size from ._units import _mm_to_hwp_units if TYPE_CHECKING: @@ -116,12 +116,6 @@ def add_picture( """Embed image data and place a picture object in a new paragraph.""" validate_picture_align(align) # before anything is stored: Hancom reads an unknown one as LEFT - # Call the local primitive directly rather than `doc.add_image` — that - # facade name moved in 6.0 (design table row 33), and going through it - # would fire a DeprecationWarning on every `add_picture` call even though - # `add_picture` itself is a kept (unmoved) root method. - binary_item_id_ref = str(add_image(doc, image_data, image_format)) - resolved_width = width if resolved_width is None: resolved_width = _mm_to_hwp_units(width_mm) if width_mm is not None else 14400 @@ -137,6 +131,13 @@ def add_picture( resolved_height = round(resolved_width * source_height / source_width) else: resolved_height = resolved_width + validate_shape_size(int(resolved_width), int(resolved_height)) # before anything is stored too + + # Call the local primitive directly rather than `doc.add_image` — that + # facade name moved in 6.0 (design table row 33), and going through it + # would fire a DeprecationWarning on every `add_picture` call even though + # `add_picture` itself is a kept (unmoved) root method. + binary_item_id_ref = str(add_image(doc, image_data, image_format)) paragraph = doc.add_paragraph( "", diff --git a/src/hwpx/_document/shapes.py b/src/hwpx/_document/shapes.py index b60198e1..95464b97 100644 --- a/src/hwpx/_document/shapes.py +++ b/src/hwpx/_document/shapes.py @@ -8,7 +8,9 @@ from ._units import _mm_to_hwp_units from ..errors import HwpxStateError, HwpxValueError from ..oxml.objects import _closed_points -from ..oxml.shape_position import validate_original_size +from ..oxml.shape_position import ( + validate_equation_base_unit, validate_original_size, validate_rect_ratio, validate_shape_size, +) if TYPE_CHECKING: from hwpx.document import HwpxDocument @@ -265,6 +267,8 @@ def add_rectangle( Dimensions are in HWPUNIT. *ratio* controls corner roundness (0 = sharp, 50 = semicircle). """ + validate_shape_size(width, height) + validate_rect_ratio(ratio) validate_original_size(original_size) if paragraph is None: paragraph = doc.add_paragraph( @@ -297,6 +301,7 @@ def add_ellipse( Dimensions are in HWPUNIT. """ + validate_shape_size(width, height) validate_original_size(original_size) if paragraph is None: paragraph = doc.add_paragraph( @@ -334,6 +339,7 @@ def add_arc( mechanism every other shape here already uses. *arc_type* is the schema's own ``NORMAL``/``PIE``/``CHORD`` passthrough. """ + validate_shape_size(width, height) if paragraph is None: paragraph = doc.add_paragraph( "", section=section, section_index=section_index, @@ -522,15 +528,16 @@ def add_equation( context={"length": len(text), "limit": MAX_SOURCE_LENGTH}, suggestion="Split the equation.", ) + validate_equation_base_unit(base_unit) + width, height = size if size is not None else estimate_equation_size(text, base_unit=base_unit) + validate_shape_size(width, height) # one measured at a huge base_unit too if paragraph is None: paragraph = doc.add_paragraph( "", section=section, section_index=section_index, include_run=False, ) - if size is None: - size = estimate_equation_size(text, base_unit=base_unit) inline_object = paragraph.add_equation( - text, base_unit=base_unit, size=size, char_pr_id_ref=char_pr_id_ref, + text, base_unit=base_unit, size=(width, height), char_pr_id_ref=char_pr_id_ref, ) created_id = inline_object.element.get("id", "") diff --git a/src/hwpx/errors.py b/src/hwpx/errors.py index be00a160..e3c68fd1 100644 --- a/src/hwpx/errors.py +++ b/src/hwpx/errors.py @@ -344,6 +344,9 @@ class Hwp5ConversionWarning(UserWarning): "shape-caption-side-invalid": "캡션 side 값이 OWPML 어휘(LEFT/RIGHT/TOP/BOTTOM) 밖이다.", "shape-draw-text-vert-align": "도형 글상자 vert_align 값이 OWPML 어휘(hp:subList/@vertAlign, TOP/CENTER/BOTTOM) 밖이다.", "shape-original-size-invalid": "add_rectangle/add_ellipse 의 original_size 가 양의 정수 두 개(HWPUNIT)가 아니다.", + "shape-size-value": "도형·그림·수식 크기가 0 이상 2**31 미만의 int(HWPUNIT)가 아니다(bool 도 거부).", + "shape-rect-ratio-value": "사각형 모서리 곡률(ratio)이 0 이상 2**31 미만의 int(%)가 아니다(bool 도 거부).", + "shape-equation-base-unit-value": "수식 기준 크기(base_unit)가 1 이상 2**31 미만의 int(1/100 pt)가 아니다(bool 도 거부).", "shape-position-value": "도형 오프셋이 signed 32-bit 정수(HWPUNIT)가 아니다.", "shape-position-unsupported": "위치를 바꿀 도형이 떠 있는 개체가 아니거나 hp:pos 가 없다.", "shape-position-frame": "도형 기준 프레임·정렬 값이 OWPML 어휘(hp:pos 의 vertRelTo/horzRelTo/vertAlign/horzAlign) 밖이다.", diff --git a/src/hwpx/oxml/objects.py b/src/hwpx/oxml/objects.py index ec2e4913..1ad1e44f 100644 --- a/src/hwpx/oxml/objects.py +++ b/src/hwpx/oxml/objects.py @@ -26,6 +26,7 @@ ) from .shape_position import ( _shape_set_position, build_at_original_size, resize_group, validate_draw_text_vert_align, validate_picture_align, + validate_rect_ratio, validate_shape_size, ) if TYPE_CHECKING: @@ -178,8 +179,7 @@ def _build_shape_common_children( AbstractShapeObjectType children (last, via ``_build_shape_base_children``): sz, pos, outMargin """ - w = str(width) - h = str(height) + w, h = map(str, validate_shape_size(width, height)) # Hancom reads a negative size as 0 the_id = inst_id or _object_id() parent.set("id", the_id) @@ -344,7 +344,7 @@ def _create_rectangle_element( treat_as_char: bool = True, ) -> ET.Element: """Build a complete ```` element matching real HWPX output.""" - el = ET.Element(f"{_HP}rect", {"ratio": str(ratio)}) + el = ET.Element(f"{_HP}rect", {"ratio": str(validate_rect_ratio(ratio))}) _build_shape_common_children(el, width, height, treat_as_char=treat_as_char) _build_drawing_object_children( el, line_color=line_color, line_width=line_width, @@ -1383,6 +1383,7 @@ def resize(self, width: int, height: int) -> None: :class:`UserWarning` is raised, because the drawn shape cannot follow the requested size. """ + validate_shape_size(width, height) if self.shape_type == "container" and resize_group(self, width, height): return # a group is drawn at its sz: see shape_position.resize_group old_width, old_height = self._geometry_size() diff --git a/src/hwpx/oxml/paragraph.py b/src/hwpx/oxml/paragraph.py index 55fca391..a37d73ed 100644 --- a/src/hwpx/oxml/paragraph.py +++ b/src/hwpx/oxml/paragraph.py @@ -65,6 +65,7 @@ ) from .curves import _paragraph_add_connector, _paragraph_add_curve from .run import HwpxOxmlRun +from .shape_position import validate_equation_base_unit, validate_shape_size from .table import HwpxOxmlTable from .table_sizes import cell_margins_of @@ -676,11 +677,7 @@ def add_picture( "(a PAPER-relative on an inline pic is contradictory)" ) - run = self._create_run_for_object( - run_attributes, - char_pr_id_ref=char_pr_id_ref, - ) - element = _create_picture_element( + element = _create_picture_element( # checked before its run is added str(binary_item_id_ref), int(width), int(height), @@ -689,6 +686,10 @@ def add_picture( pos_overrides=pos_overrides, text_wrap=text_wrap, ) + run = self._create_run_for_object( + run_attributes, + char_pr_id_ref=char_pr_id_ref, + ) if type(element) is not type(run): element = LET.fromstring(ET.tostring(element, encoding="utf-8")) run.append(element) @@ -1184,12 +1185,12 @@ def add_equation( text = script.strip() if not text: raise ValueError("equation script must be a non-empty string") - if base_unit <= 0: - raise ValueError("base_unit must be positive") + validate_equation_base_unit(base_unit) from ..equation.measure import measure_equation measured = measure_equation(text, base_unit=base_unit) width, height = size if size is not None else (measured.width, measured.height) + validate_shape_size(width, height) run = self._create_run_for_object( run_attributes, char_pr_id_ref=char_pr_id_ref ) diff --git a/src/hwpx/oxml/shape_position.py b/src/hwpx/oxml/shape_position.py index 7305fc58..f32066c2 100644 --- a/src/hwpx/oxml/shape_position.py +++ b/src/hwpx/oxml/shape_position.py @@ -151,6 +151,48 @@ def _matrix_number(value: float) -> str: return "0" if text in ("-0", "") else text +def validate_shape_size(width: object, height: object) -> tuple[int, int]: + """*width* and *height* of a shape, picture or equation, checked to be ints in ``0 <= value < 2**31`` + (HWPUNIT): Hancom reads a negative size as 0, and the original size and rotation centre written from it.""" + + for argument, value in (("width", width), ("height", height)): + if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value < 2**31: + raise HwpxValueError( + f"{argument} must be an int in 0 <= {argument} < 2**31 (HWPUNIT); got {value!r}", + code="shape-size-value", + context={"argument": argument, "value": repr(value)}, + suggestion="Pass a size from 0 to 2**31 - 1 HWP units (an equation's is measured at its base_unit).", + ) + return width, height # type: ignore[return-value] + + +def validate_rect_ratio(ratio: object) -> int: + """A rectangle's corner *ratio* (per cent), checked to be an int in ``0 <= ratio < 2**31``: Hancom reads a + negative one as 0.""" + + if isinstance(ratio, bool) or not isinstance(ratio, int) or not 0 <= ratio < 2**31: + raise HwpxValueError( + f"ratio must be an int in 0 <= ratio < 2**31 (per cent); got {ratio!r}", + code="shape-rect-ratio-value", + context={"value": repr(ratio)}, + suggestion="Pass the corner roundness in per cent: 0 sharp, 50 a semicircle.", + ) + return ratio + + +def validate_equation_base_unit(base_unit: object) -> int: + """An equation's *base_unit* (1/100 pt), checked to be an int in ``1 <= base_unit < 2**31``.""" + + if isinstance(base_unit, bool) or not isinstance(base_unit, int) or not 0 < base_unit < 2**31: + raise HwpxValueError( + f"base_unit must be positive: an int in 1 <= base_unit < 2**31 (1/100 pt); got {base_unit!r}", + code="shape-equation-base-unit-value", + context={"value": repr(base_unit)}, + suggestion="Pass the equation's base font size in 1/100 pt, e.g. 1000 for 10 pt.", + ) + return base_unit + + def validate_original_size(original_size: object) -> tuple[int, int] | None: """Check *original_size* is ``None`` or two positive integers.""" @@ -191,6 +233,7 @@ def build_at_original_size( """ checked = validate_original_size(original_size) + validate_shape_size(width, height) if checked is None: return factory(width, height, **options) org_width, org_height = checked @@ -288,5 +331,8 @@ def _set_scale(matrix: "ET.Element", factors: tuple[float, float], offset: tuple "build_at_original_size", "resize_group", "validate_draw_text_vert_align", + "validate_equation_base_unit", "validate_original_size", + "validate_rect_ratio", + "validate_shape_size", ] diff --git a/tests/fixtures/hancom_saved/equation_base_unit_largest_given_size.hwpx b/tests/fixtures/hancom_saved/equation_base_unit_largest_given_size.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..5307ebb23ad3d0f9bc82e584496e5c8d4e2a56ab GIT binary patch literal 7647 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 K(svXg0Pufw>?Obe literal 0 HcmV?d00001 diff --git a/tests/fixtures/hancom_saved/picture_size_negative.hwpx b/tests/fixtures/hancom_saved/picture_size_negative.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..0b995ec6ab6a84d600e6cdd651bcc12b006ff944 GIT binary patch literal 8208 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/hancom_saved/shape_rect_size_negative.hwpx b/tests/fixtures/hancom_saved/shape_rect_size_negative.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..611a7776d3d3065c6f03af0b1641e7e94154d300 GIT binary patch literal 7729 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 literal 0 HcmV?d00001 diff --git a/tests/test_shape_size_values.py b/tests/test_shape_size_values.py new file mode 100644 index 00000000..78be42ec --- /dev/null +++ b/tests/test_shape_size_values.py @@ -0,0 +1,193 @@ +"""Shape, picture and equation sizes, rectangle ratios and equation base units Hancom does not keep are refused. + +Hancom reads a negative size or corner ratio as 0 (and saves 0), so they are refused before anything is added: +no paragraph, run or image is left behind. ``tests/fixtures/hancom_saved/*_negative.hwpx`` are Hancom's saves of +a rectangle, a picture and an equation written 1 unit below 0 and of a rectangle with ratio -1. +""" + +from __future__ import annotations + +import base64 +import zipfile +from collections.abc import Callable +from pathlib import Path + +import pytest +from lxml import etree + +from hwpx import HwpxDocument +from hwpx.errors import HwpxValueError + +HP = "{http://www.hancom.co.kr/hwpml/2011/paragraph}" +FIXTURES = Path(__file__).parent / "fixtures" / "hancom_saved" +PNG_1X1 = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMB/axwAqkAAAAASUVORK5CYII=" +) + + +def _saved(name: str, tag: str) -> etree._Element: + """The first ``hp:`` object in the Hancom-saved fixture *name*.""" + + with zipfile.ZipFile(FIXTURES / name) as archive: + root = etree.fromstring(archive.read("Contents/section0.xml")) + return next(root.iter(f"{HP}{tag}")) + + +def _size(element: etree._Element, child: str = "sz") -> tuple[str | None, str | None]: + node = element.find(f"{HP}{child}") + return node.get("width"), node.get("height") + + +def _state(document: HwpxDocument) -> tuple[int, int, list[bytes]]: + """What a refused object must leave as it was: the paragraphs, the stored images and every section's XML.""" + + return ( + len(document.paragraphs), + len(document.media.images), + [etree.tostring(section.element) for section in document.sections], + ) + + +def test_hancom_reads_a_negative_size_or_corner_ratio_as_0() -> None: + # Written as -1 by -1, Hancom saved 0 for the size, the original size and the rotation centre written from + # it, and a picture's image size; a corner ratio of -1 it saved as 0. + rectangle = _saved("shape_rect_size_negative.hwpx", "rect") + assert {_size(rectangle, child) for child in ("sz", "orgSz")} == {("0", "0")} + rotation = rectangle.find(f"{HP}rotationInfo") + assert (rotation.get("centerX"), rotation.get("centerY")) == ("0", "0") + + assert _saved("shape_rect_ratio_negative.hwpx", "rect").get("ratio") == "0" + + picture = _saved("picture_size_negative.hwpx", "pic") + assert {_size(picture, child) for child in ("sz", "orgSz")} == {("0", "0")} + image = picture.find(f"{HP}imgDim") + assert (image.get("dimwidth"), image.get("dimheight")) == ("0", "0") + + assert _size(_saved("equation_size_negative.hwpx", "equation")) == ("0", "0") + + +_SHAPES: dict[str, Callable[[HwpxDocument, object, object], object]] = { + "rectangle": lambda document, width, height: document.shapes.add_rectangle(width=width, height=height), + "ellipse": lambda document, width, height: document.shapes.add_ellipse(width=width, height=height), + "arc": lambda document, width, height: document.shapes.add_arc(width=width, height=height), + "equation": lambda document, width, height: document.shapes.add_equation("x + 1 over 2", size=(width, height)), +} + + +@pytest.mark.parametrize("kind", sorted(_SHAPES)) +@pytest.mark.parametrize("width, height", [(-1, 7200), (7200, -1), (2**31, 7200), (7200.0, 7200), (True, 7200)]) +def test_a_size_hancom_does_not_keep_is_refused_before_anything_is_added( + kind: str, width: object, height: object +) -> None: + document = HwpxDocument.new() + before = _state(document) + + with pytest.raises(HwpxValueError) as caught: + _SHAPES[kind](document, width, height) + + assert caught.value.code == "shape-size-value" + assert _state(document) == before + + +@pytest.mark.parametrize( + "size", [{"width": -1, "height": 7200}, {"width": 7200, "height": -1}, {"width": 2**31}, {"width_mm": -1.0}] +) +def test_a_picture_size_hancom_does_not_keep_is_refused_before_the_image_is_stored(size: dict[str, object]) -> None: + document = HwpxDocument.new() + before = _state(document) + + with pytest.raises(HwpxValueError) as caught: + document.add_picture(PNG_1X1, "png", **size) # type: ignore[arg-type] + + assert caught.value.code == "shape-size-value" + assert _state(document) == before + + +@pytest.mark.parametrize("ratio", [-1, 2**31, 12.5, True]) +def test_a_corner_ratio_hancom_does_not_keep_is_refused_before_anything_is_added(ratio: object) -> None: + document = HwpxDocument.new() + before = _state(document) + + with pytest.raises(HwpxValueError) as caught: + document.shapes.add_rectangle(width=14400, height=7200, ratio=ratio) # type: ignore[arg-type] + + assert caught.value.code == "shape-rect-ratio-value" + assert _state(document) == before + + +def test_the_smallest_and_largest_sizes_are_written_as_given() -> None: + document = HwpxDocument.new() + + rectangle = document.shapes.add_rectangle(width=0, height=2**31 - 1, ratio=50) + equation = document.shapes.add_equation("x + 1 over 2", size=(0, 2**31 - 1)) + + assert _size(rectangle.element) == ("0", str(2**31 - 1)) + assert rectangle.element.get("ratio") == "50" + assert _size(equation.element) == ("0", str(2**31 - 1)) + + +@pytest.mark.parametrize("base_unit", [0, -1100, 2**31, 1100.0, True]) +def test_an_equation_base_unit_outside_1_to_2_31_is_refused_before_anything_is_added(base_unit: object) -> None: + document = HwpxDocument.new() + before = _state(document) + + with pytest.raises(HwpxValueError) as caught: + document.shapes.add_equation("x + 1 over 2", base_unit=base_unit) # type: ignore[arg-type] + + assert caught.value.code == "shape-equation-base-unit-value" + assert isinstance(caught.value, ValueError) # what it raised before + assert _state(document) == before + + +def test_an_equation_box_measured_past_31_bits_is_refused_and_a_given_box_keeps_the_largest_base_unit() -> None: + # Hancom keeps base_unit 2**31 - 1 and lays the page out with the stored box. The box measured at it is past + # 32 bits: Hancom saved 2**32 - 1 for it. With a box given, it kept both. + measured = _saved("equation_base_unit_largest_measured.hwpx", "equation") + assert measured.get("baseUnit") == "2147483647" + assert _size(measured) == ("4294967295", "4294967295") + given = _saved("equation_base_unit_largest_given_size.hwpx", "equation") + assert given.get("baseUnit") == "2147483647" + assert _size(given) == ("1342", "1089") + + document = HwpxDocument.new() + before = _state(document) + with pytest.raises(HwpxValueError) as caught: + document.shapes.add_equation("x + 1 over 2", base_unit=2**31 - 1) + assert caught.value.code == "shape-size-value" + assert _state(document) == before + + equation = document.shapes.add_equation("(x)", base_unit=2**31 - 1, size=(1342, 1089)) + assert equation.element.get("baseUnit") == "2147483647" + assert _size(equation.element) == ("1342", "1089") + + +def test_the_paragraph_writers_check_before_adding_a_run() -> None: + document = HwpxDocument.new() + paragraph = document.paragraphs[0] + runs = len(paragraph.element.findall(f"{HP}run")) + + for add, code in ( + (lambda: paragraph.add_rectangle(width=-1, height=7200), "shape-size-value"), + (lambda: paragraph.add_rectangle(width=-1, height=7200, original_size=(100, 100)), "shape-size-value"), + (lambda: paragraph.add_rectangle(width=14400, height=7200, ratio=-1), "shape-rect-ratio-value"), + (lambda: paragraph.add_picture("image1", width=-1, height=7200), "shape-size-value"), + (lambda: paragraph.add_equation("x", size=(-1, 7200)), "shape-size-value"), + (lambda: paragraph.add_equation("x", base_unit=0), "shape-equation-base-unit-value"), + ): + with pytest.raises(HwpxValueError) as caught: + add() + assert caught.value.code == code + + assert len(paragraph.element.findall(f"{HP}run")) == runs + + +def test_resizing_a_shape_to_a_negative_size_is_refused_and_leaves_it_as_it_was() -> None: + document = HwpxDocument.new() + shape = document.shapes.add_rectangle(width=14400, height=7200, treat_as_char=False) + before = etree.tostring(shape.element) + + with pytest.raises(HwpxValueError) as caught: + shape.resize(-1, 7200) + + assert caught.value.code == "shape-size-value" + assert etree.tostring(shape.element) == before From 89d1855299b3f3fb651227824bf07d7d6a2c5d75 Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:43:08 +0900 Subject: [PATCH 04/15] docs(changelog): note the fix Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3aa8a1c..87a5af1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,17 @@ 전처럼 가로줄 없는 분수다. - 상자는 여전히 `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`)로 + 바꾸고 문단을 더하기 전에 낸다. 2**31 이상도 거부한다. 아주 큰 `base_unit`으로 잰 상자가 2**31 이상이면 + `shape-size-value`로 거부한다. 한/글은 그런 상자를 2**32 - 1로 줄여 저장한다. + - `HwpxOxmlParagraph.add_picture`는 그림 요소를 만든 뒤에 런을 더한다. 값이 거부되면 빈 런이 남지 않는다. - 쪽 수 추정(실험, `estimate_pages`)이 쪽 번호 조판 부호(`hp:ctrl/hp:pageNum`)와 줄보다 넓은 글자처럼 둔 표만 있는 문단을 표 높이의 한 줄로 세던 것을 고친다. - 한/글은 둘을 한 줄에 두지 않는다. 부호의 줄은 문단 글의 빈 줄이다. From 8c8fae481ba4a6dcf5b7f37113bdb3ba7ad57581 Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:03:19 +0900 Subject: [PATCH 05/15] fix(captions,page): refuse gaps and new numbers Hancom does not keep Hancom reads a caption's gap and the gap between columns of the same width as signed 16-bit numbers and a new number as an unsigned one, wrapping anything else (a column gap of 32768 makes the columns overlap; a negative one written as text is read as 0). set_caption's gap outside -32768..32767, a same_gap (or page setup column_gap_mm) outside 0..32767 and a restart number outside 0..65535 are now refused before anything changes (shape-caption-gap-value, page-column-gap-value, page-new-num-value). The caption side check moves to shape_position unchanged. Co-Authored-By: Claude Opus 5.5 --- docs/error-codes.md | 5 +- src/hwpx/_document/layout.py | 6 +- src/hwpx/errors.py | 3 + src/hwpx/oxml/objects.py | 18 +-- src/hwpx/oxml/paragraph.py | 6 +- src/hwpx/oxml/section_format.py | 36 +++++ src/hwpx/oxml/shape_position.py | 34 +++++ .../caption_gap_largest_kept.hwpx | Bin 0 -> 7986 bytes .../caption_gap_negative_kept.hwpx | Bin 0 -> 7981 bytes .../hancom_saved/caption_gap_wrapped.hwpx | Bin 0 -> 7962 bytes .../columns_gap_largest_kept.hwpx | Bin 0 -> 8351 bytes .../columns_gap_negative_read_as_0.hwpx | Bin 0 -> 7337 bytes .../hancom_saved/columns_gap_wrapped.hwpx | Bin 0 -> 8317 bytes .../new_page_number_largest_kept.hwpx | Bin 0 -> 7664 bytes .../new_page_number_negative_wrapped.hwpx | Bin 0 -> 7659 bytes .../new_page_number_smallest_kept.hwpx | Bin 0 -> 7658 bytes .../hancom_saved/new_page_number_wrapped.hwpx | Bin 0 -> 7666 bytes tests/test_gap_and_new_number_values.py | 143 ++++++++++++++++++ 18 files changed, 233 insertions(+), 18 deletions(-) create mode 100644 tests/fixtures/hancom_saved/caption_gap_largest_kept.hwpx create mode 100644 tests/fixtures/hancom_saved/caption_gap_negative_kept.hwpx create mode 100644 tests/fixtures/hancom_saved/caption_gap_wrapped.hwpx create mode 100644 tests/fixtures/hancom_saved/columns_gap_largest_kept.hwpx create mode 100644 tests/fixtures/hancom_saved/columns_gap_negative_read_as_0.hwpx create mode 100644 tests/fixtures/hancom_saved/columns_gap_wrapped.hwpx create mode 100644 tests/fixtures/hancom_saved/new_page_number_largest_kept.hwpx create mode 100644 tests/fixtures/hancom_saved/new_page_number_negative_wrapped.hwpx create mode 100644 tests/fixtures/hancom_saved/new_page_number_smallest_kept.hwpx create mode 100644 tests/fixtures/hancom_saved/new_page_number_wrapped.hwpx create mode 100644 tests/test_gap_and_new_number_values.py diff --git a/docs/error-codes.md b/docs/error-codes.md index c110bf2b..5d93cb40 100644 --- a/docs/error-codes.md +++ b/docs/error-codes.md @@ -30,7 +30,7 @@ except HwpxError as exc: | 형태 | `style-not-found` | `VISUAL_COMPLETE_FAILED` | | 쓰임 | 예외 분기 | **발행된 영수증 스키마의 필드값** | | 관리 | major 경계 | 영수증 스키마 버전 | -| 개수 | 179 | 11 | +| 개수 | 182 | 11 | 통합하지 않는 이유: quality 코드는 `hwpx.mutation-report/v1` 과 `VisualCompleteReport` 에 이미 실려 나간 값이다. 이름을 바꾸면 영수증을 읽는 @@ -183,9 +183,11 @@ except HwpxError as exc: |---|---| | `page-argument-conflict` | text 와 content 를 동시에 지정했다. | | `page-argument-missing` | text 또는 content 중 하나는 있어야 한다. | +| `page-column-gap-value` | 같은 폭 단의 간격(same_gap)이 0 이상 32767 이하의 int(HWPUNIT)가 아니다(bool 도 거부). | | `page-columns-invalid` | 단 수는 1 이상이어야 한다. | | `page-kind-invalid` | kind 는 'header' 또는 'footer' 여야 한다. | | `page-new-num-kind-invalid` | 쪽번호 재시작 kind 값이 OWPML 어휘(hp:AutoNumNewNumType/@numType) 밖이다. | +| `page-new-num-value` | 새 번호(hp:newNum/@num)가 0 이상 65535 이하가 아니다. | | `page-orientation-unsupported` | 지원하지 않는 용지 방향이다. | | `page-paper-size-unsupported` | 지원하지 않는 용지 규격이다. | | `page-text-direction-unsupported` | 글자 방향 값이 OWPML 어휘(hp:secPr/@textDirection: HORIZONTAL/VERTICAL/VERTICALALL) 밖이다. | @@ -267,6 +269,7 @@ except HwpxError as exc: |---|---| | `shape-arc-corner-invalid` | add_arc 의 corner 인자가 지원하는 모서리(TOP_LEFT 등) 밖이다. | | `shape-arc-type-invalid` | add_arc 의 arc_type 인자가 OWPML 어휘(NORMAL/PIE/CHORD) 밖이다. | +| `shape-caption-gap-value` | 캡션 간격(gap)이 -32768 이상 32767 이하의 int(HWPUNIT)가 아니다(bool 도 거부). | | `shape-caption-side-invalid` | 캡션 side 값이 OWPML 어휘(LEFT/RIGHT/TOP/BOTTOM) 밖이다. | | `shape-chart-anchor-detached` | 만든 차트 앵커가 자기 파트를 가리키지 않는다. | | `shape-chart-axes-missing` | 축이 필요한 차트(막대·꺾은선·영역·분산형·방사형·거품형 등)에 두 축(c:axId와 정의된 축)이 없다(한컴이 빈 차트를 그리거나 멈춘다). | diff --git a/src/hwpx/_document/layout.py b/src/hwpx/_document/layout.py index 98799ec4..1dd5e789 100644 --- a/src/hwpx/_document/layout.py +++ b/src/hwpx/_document/layout.py @@ -19,7 +19,7 @@ from ..oxml.namespaces import HH, HP from ..oxml.numbering_kinds import ensure_numbering_levels from ..oxml.objects import HwpxOxmlInlineObject -from ..oxml.section_format import _PAGE_LANDSCAPE, _PAGE_PORTRAIT, _page_orientation_value +from ..oxml.section_format import _PAGE_LANDSCAPE, _PAGE_PORTRAIT, _page_orientation_value, validate_column_gap from ..oxml.table_sizes import cell_margins_of from ._units import _mm_to_hwp_units, _pt_to_hwp_units @@ -609,6 +609,8 @@ def set_page_setup( """ normalized_orientation = _normalize_page_orientation(orientation) + if columns is not None: # the gap checked before the page or its margins change + validate_column_gap(_mm_to_hwp_units(float(column_gap_mm or 0))) target_width_mm = width_mm target_height_mm = height_mm if paper_size: @@ -741,6 +743,8 @@ def set_columns( context={"requested": col_count}, suggestion="Use columns=1 to remove columns.", ) + if same_size: + validate_column_gap(same_gap) # before a paragraph is added if paragraph is None: target_section = _resolve_section(doc, section=section, section_index=section_index) ctrl = target_section.properties.set_columns( diff --git a/src/hwpx/errors.py b/src/hwpx/errors.py index e3c68fd1..8178c3ae 100644 --- a/src/hwpx/errors.py +++ b/src/hwpx/errors.py @@ -307,9 +307,11 @@ class Hwp5ConversionWarning(UserWarning): "page-argument-missing": "text 또는 content 중 하나는 있어야 한다.", "page-kind-invalid": "kind 는 'header' 또는 'footer' 여야 한다.", "page-columns-invalid": "단 수는 1 이상이어야 한다.", + "page-column-gap-value": "같은 폭 단의 간격(same_gap)이 0 이상 32767 이하의 int(HWPUNIT)가 아니다(bool 도 거부).", "page-orientation-unsupported": "지원하지 않는 용지 방향이다.", "page-paper-size-unsupported": "지원하지 않는 용지 규격이다.", "page-new-num-kind-invalid": "쪽번호 재시작 kind 값이 OWPML 어휘(hp:AutoNumNewNumType/@numType) 밖이다.", + "page-new-num-value": "새 번호(hp:newNum/@num)가 0 이상 65535 이하가 아니다.", "page-text-direction-unsupported": "글자 방향 값이 OWPML 어휘(hp:secPr/@textDirection: HORIZONTAL/VERTICAL/VERTICALALL) 밖이다.", # -- 양식개체 -------------------------------------------------------- "field-name-empty": "누름틀 이름이 비어 있다.", @@ -342,6 +344,7 @@ class Hwp5ConversionWarning(UserWarning): "shape-chart-anchor-detached": "만든 차트 앵커가 자기 파트를 가리키지 않는다.", "shape-chart-not-created": "만든 차트를 표준 스캔이 다시 찾지 못했다.", "shape-caption-side-invalid": "캡션 side 값이 OWPML 어휘(LEFT/RIGHT/TOP/BOTTOM) 밖이다.", + "shape-caption-gap-value": "캡션 간격(gap)이 -32768 이상 32767 이하의 int(HWPUNIT)가 아니다(bool 도 거부).", "shape-draw-text-vert-align": "도형 글상자 vert_align 값이 OWPML 어휘(hp:subList/@vertAlign, TOP/CENTER/BOTTOM) 밖이다.", "shape-original-size-invalid": "add_rectangle/add_ellipse 의 original_size 가 양의 정수 두 개(HWPUNIT)가 아니다.", "shape-size-value": "도형·그림·수식 크기가 0 이상 2**31 미만의 int(HWPUNIT)가 아니다(bool 도 거부).", diff --git a/src/hwpx/oxml/objects.py b/src/hwpx/oxml/objects.py index 1ad1e44f..a670ad0c 100644 --- a/src/hwpx/oxml/objects.py +++ b/src/hwpx/oxml/objects.py @@ -26,7 +26,7 @@ ) from .shape_position import ( _shape_set_position, build_at_original_size, resize_group, validate_draw_text_vert_align, validate_picture_align, - validate_rect_ratio, validate_shape_size, + validate_caption_gap, validate_caption_side, validate_rect_ratio, validate_shape_size, ) if TYPE_CHECKING: @@ -1023,10 +1023,6 @@ def _paragraph_shapes(self: "HwpxOxmlParagraph") -> list["HwpxOxmlShape"]: # reused by ``HwpxOxmlTable`` (table.py), ``HwpxOxmlShape``, and # ``HwpxOxmlInlineObject`` (this module) rather than duplicated per host. -#: ``hp:caption/@side`` 어휘(스키마 기본값은 LEFT). 실코퍼스 15건 전수는 -#: TOP 14 · BOTTOM 1 — LEFT/RIGHT 관측 0(테두리 옆 캡션은 실무에서 안 쓴다). -_CAPTION_SIDES = frozenset({"LEFT", "RIGHT", "TOP", "BOTTOM"}) - #: 실코퍼스 15건 전수: fullSz="0"(전부) · width="8504"(전부, 호스트 크기와 #: 무관한 고정값) · gap="850"(11) 또는 "566"(4, 스키마 기본은 850). _CAPTION_DEFAULT_WIDTH = "8504" @@ -1227,16 +1223,8 @@ def _write_caption( gap: int, char_pr_id_ref: str | int | None, ) -> Caption: - normalized_side = side.strip().upper() - if normalized_side not in _CAPTION_SIDES: - from ..errors import HwpxValueError - - raise HwpxValueError( - f"unsupported caption side {side!r}", - code="shape-caption-side-invalid", - context={"requested": side, "available": sorted(_CAPTION_SIDES)}, - suggestion=f"side 는 {sorted(_CAPTION_SIDES)} 중 하나여야 합니다.", - ) + normalized_side = validate_caption_side(side) + gap = validate_caption_gap(gap) # before anything changes: Hancom keeps a signed 16-bit gap element = host.find(f"{_HP}caption") if element is None: diff --git a/src/hwpx/oxml/paragraph.py b/src/hwpx/oxml/paragraph.py index a37d73ed..8e289369 100644 --- a/src/hwpx/oxml/paragraph.py +++ b/src/hwpx/oxml/paragraph.py @@ -65,6 +65,7 @@ ) from .curves import _paragraph_add_connector, _paragraph_add_curve from .run import HwpxOxmlRun +from .section_format import validate_column_gap, validate_new_number from .shape_position import validate_equation_base_unit, validate_shape_size from .table import HwpxOxmlTable from .table_sizes import cell_margins_of @@ -803,6 +804,8 @@ def add_column_definition( """ if not 1 <= col_count <= 255: raise ValueError("col_count must be between 1 and 255") + if same_size: + validate_column_gap(same_gap) # before its run is added run = self._create_run_for_object( run_attributes, char_pr_id_ref=char_pr_id_ref, @@ -1037,10 +1040,11 @@ def add_new_num( """ normalized_kind = _normalize_enum_attr(kind or "PAGE", NEW_NUM_KINDS, label="kind") + number = validate_new_number(number) # before its run is added run = self._create_run_for_object(run_attributes, char_pr_id_ref=char_pr_id_ref) ctrl = _append_child(run, f"{_HP}ctrl", {}) _append_child( - ctrl, f"{_HP}newNum", {"num": str(int(number)), "numType": normalized_kind}, + ctrl, f"{_HP}newNum", {"num": str(number), "numType": normalized_kind}, ) self.section.mark_dirty() return HwpxOxmlInlineObject(ctrl, self) diff --git a/src/hwpx/oxml/section_format.py b/src/hwpx/oxml/section_format.py index 58aa1875..18824dba 100644 --- a/src/hwpx/oxml/section_format.py +++ b/src/hwpx/oxml/section_format.py @@ -248,6 +248,40 @@ class NoteShape: placement: NotePlacement +def validate_new_number(number: object) -> int: + """A ``hp:newNum/@num``: *number* as an int, checked to be in ``0 <= number <= 65535``. Hancom keeps it as + an unsigned 16-bit number, reading -1 as 65535 and 65536 as 0.""" + + from ..errors import HwpxValueError + + value = int(number) # type: ignore[call-overload] + if not 0 <= value < 2**16: + raise HwpxValueError( + f"number must be in 0 <= number <= 65535; got {number!r}", + code="page-new-num-value", + context={"value": repr(number)}, + suggestion="Pass the number the count restarts at, from 0 to 65535.", + ) + return value + + +def validate_column_gap(gap: object) -> int: + """The gap between columns of the same width (``hp:colPr/@sameGap``, HWPUNIT), checked to be an int in + ``0 <= gap <= 32767``: Hancom reads it as a signed 16-bit number (32768 as -32768, the columns then + overlapping; 65536 as 0) and a negative one written as text as 0.""" + + from ..errors import HwpxValueError + + if isinstance(gap, bool) or not isinstance(gap, int) or not 0 <= gap < 2**15: + raise HwpxValueError( + f"same_gap must be an int in 0 <= same_gap <= 32767 (HWPUNIT); got {gap!r}", + code="page-column-gap-value", + context={"value": repr(gap)}, + suggestion="Pass the gap between columns in HWP units, e.g. 1200 (about 4 mm).", + ) + return gap + + class HwpxOxmlSectionProperties: """Provides convenient access to ```` configuration.""" @@ -437,6 +471,8 @@ def set_columns( ``hp:secPr``. """ + if same_size: + validate_column_gap(same_gap) # checked before anything changes, as the separator below line = None if separator_type or separator_width or separator_color: # checked before anything changes: a refused value leaves the columns as they were diff --git a/src/hwpx/oxml/shape_position.py b/src/hwpx/oxml/shape_position.py index f32066c2..0825b435 100644 --- a/src/hwpx/oxml/shape_position.py +++ b/src/hwpx/oxml/shape_position.py @@ -44,6 +44,9 @@ POS_VERT_ALIGN = ("TOP", "CENTER", "BOTTOM", "INSIDE", "OUTSIDE") POS_HORZ_ALIGN = ("LEFT", "CENTER", "RIGHT", "INSIDE", "OUTSIDE") SUBLIST_VERT_ALIGN = ("TOP", "CENTER", "BOTTOM") +#: ``hp:caption/@side`` 어휘(스키마 기본값은 LEFT). 실코퍼스 15건 전수는 +#: TOP 14 · BOTTOM 1 — LEFT/RIGHT 관측 0(테두리 옆 캡션은 실무에서 안 쓴다). +CAPTION_SIDES = frozenset({"LEFT", "RIGHT", "TOP", "BOTTOM"}) def _require_member( @@ -81,6 +84,34 @@ def validate_draw_text_vert_align(vert_align: str | None) -> str | None: ) +def validate_caption_side(side: str) -> str: + """A caption's *side* as ``hp:caption/@side``: any case, surrounding spaces dropped.""" + + normalized_side = side.strip().upper() + if normalized_side not in CAPTION_SIDES: + raise HwpxValueError( + f"unsupported caption side {side!r}", + code="shape-caption-side-invalid", + context={"requested": side, "available": sorted(CAPTION_SIDES)}, + suggestion=f"side 는 {sorted(CAPTION_SIDES)} 중 하나여야 합니다.", + ) + return normalized_side + + +def validate_caption_gap(gap: object) -> int: + """A caption's *gap* from its object (HWPUNIT), checked to be an int in ``-32768 <= gap <= 32767``: Hancom + keeps it as a signed 16-bit number, reading 32768 as -32768 and 65536 as 0.""" + + if isinstance(gap, bool) or not isinstance(gap, int) or not -(2**15) <= gap < 2**15: + raise HwpxValueError( + f"gap must be an int in -32768 <= gap <= 32767 (HWPUNIT); got {gap!r}", + code="shape-caption-gap-value", + context={"value": repr(gap)}, + suggestion="Pass the caption's distance from its object in HWP units, e.g. 850 (3 mm).", + ) + return gap + + def _shape_set_position( self: "HwpxOxmlShape", *, @@ -323,6 +354,7 @@ def _set_scale(matrix: "ET.Element", factors: tuple[float, float], offset: tuple __all__ = [ + "CAPTION_SIDES", "POS_HORZ_ALIGN", "POS_HORZ_REL_TO", "POS_VERT_ALIGN", @@ -330,6 +362,8 @@ def _set_scale(matrix: "ET.Element", factors: tuple[float, float], offset: tuple "SUBLIST_VERT_ALIGN", "build_at_original_size", "resize_group", + "validate_caption_gap", + "validate_caption_side", "validate_draw_text_vert_align", "validate_equation_base_unit", "validate_original_size", diff --git a/tests/fixtures/hancom_saved/caption_gap_largest_kept.hwpx b/tests/fixtures/hancom_saved/caption_gap_largest_kept.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..8ec91f998fbd076e4669225ec71d5c5128e35979 GIT binary patch literal 7986 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 LgiT7Lyl?$Ka?=$L literal 0 HcmV?d00001 diff --git a/tests/fixtures/hancom_saved/caption_gap_wrapped.hwpx b/tests/fixtures/hancom_saved/caption_gap_wrapped.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..692dbbce12c6d32672d184f384deeb3dacbd8016 GIT binary patch literal 7962 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/hancom_saved/columns_gap_largest_kept.hwpx b/tests/fixtures/hancom_saved/columns_gap_largest_kept.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..c1a6a86e29ad374a05db449fade0839817da1045 GIT binary patch literal 8351 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/hancom_saved/columns_gap_negative_read_as_0.hwpx b/tests/fixtures/hancom_saved/columns_gap_negative_read_as_0.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..e2d4429202a7d413c8667dcafc6cb202ef902be2 GIT binary patch literal 7337 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/hancom_saved/columns_gap_wrapped.hwpx b/tests/fixtures/hancom_saved/columns_gap_wrapped.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..c54b86c642160af798d336cd5f398e947aea23d6 GIT binary patch literal 8317 zcmZ{J1z1#D_ctAaAPv$(Ne$gY=gvz^(v-jF7)RmA?2oe7N&>{pKOUe_#Zyp*3ygAxALZDtQ5OWt7 z2Rlo1sGYMDhpndztGAtt`o9?Y?>_t@Mnph(XjpJY4~VN9oEQjmbO4FNe`x+QAPV3D zasp@|PL|GAc1|{;03B^Pc0mA*8`Rv%%G|-(2_g#cg17-_#Kl0i9>NOJa=L#b(ZE5R z+=OjCL;Y8fOvRYbdngAL{^B2ypq5wX4&(7(g$Mw*&bano#XW?$=V5RErXaRvgfy?de zCJNvNJjn7Nu^#3F=lzF%L;(ta2}Pr3ZEa@>p@HAoxI6qs@?RbRhuHtA??KD72;~Q! z@8z)(5CRJR*1fc|6BObEb>pywm|HA80i;Qk&Ui!Y z87%Y5*CV6x`0!I0gs_EvcoPsR#1UhROZ&J4WQ}A?M$U3!Puu!%Mx0~Gem@$&l-49v zQOcn^QkHcmaVhaVU+?j<;9IU;lW1v@=`5 z=0!S>Yz81d9C|+(PQB8atmyUka7_mgHA4&>I1oSQW-e*}M;p|e(j?B!D8#oCk-s5L) zrA4rzs|tXkz7DAm8z>GUQH30`qem5xW~?pBimHSXANxQ<0*B3BJhItO73rtkL^Hri z9S`C^cO8s*hqafZf8Em^t%1%Y*ZU(ygE&r=GQI5KZW;33`-T`%7-jb8RGxMYhiti*U8`6Eb$bll2Y38{#)z72VXI?WC6BqeM=xk zeZQdjoV3+C?RXXw8L2_pPz2~yh8ywm&vk?A^XHN0_LjOT8M^UN9j(gRSu+=#b1Py^ zR~G{D)F#S)_f4hZzOrwPgf~1YJ?WJ~=Bln-I0KXe_#{5?AJ76Olgv%d^*3|9%P)33 z3O&D;4Nao*$_ejQD<`s?kTX7^NjaM{qa?jR|d*; zM(RW7R`tN7d})XO_4=pb6BP4xZJ6&+^Rlmt>o^|SQhmd18LNfpx@v3^NX%d65TQ{% z#7>UUyY+kF7}B-b(wpPA!808Q##Ktu!-m<57V2Lz_`w8MK$LBF+f-u(JuS`+T;E_X z#$)J8QM2EYtc;sjKXzqIZ>^-kx6?_n@zEx`H_C$D=*q!l#aNaB?wa=Uacq&o?J_bu z!Jun`;oxHC6%As#ZAyk~3`?t)+~alxIxFNUl6?<@kuC;$Hy$U|AB`-{nL?G{zbwD~ zb<4vT3TTMy3<%L9%TJA61Ql9dz+$h)g$(p zh8~n$)!I#sencU<$`@hWd5l>xw%66W!M0P49*Z&LPARwT<*^QgI@a^5FaXO5Rx?A_ z`Mc*ppf_D9deqDWw{rMQ^!RSJ5MHPbLgH@zHyJh zi8Bb_(J$S3R-pxTIBZ(iW!?`qxiTIfvcs+2@RZ;PKHueY&NK?AdEQ6EEW1U$2}>x7 zRy@g~&yi`CuIB>{#vQA7E6Kd>s}Jov6V|`kjNH!pP)lL-woom2#*DMK2>ps(uzTVM zbL>YKJ~2WK3&5ZSp1TIe8c99b*D@j>ho9L>y-qTZ{d28ZzOt9q%MB+BOUjr z9lrE7L{U`{rn>XoLvZMcRBrtZ)hR1MwmN?42 z!yjvQjW=THxOz73DA4cL8?!DomRt7)JESiyyXZEWVj3kB4a{uVq7@U6VlWwJ|4Pyu zp&QY2w6SGt0@;HKjr?hgyZU#2gAq=x{OuguAR-l=Xi0>{3(L<(K~B26Qc=YythKf! zejkPl&NrK)dt2l$L<&Ri3X=Fg(y8I9lGioeS$=~+Ctgy%TdFY`rG*}n>pK%fcam$n zSMZAFyGa;=0;D%w2_|c=Td^qG%&DmkJly5@a@GO<8@(^Z96? zXL0_er{th=}0F^>syQ4qVxhm3fHlkzhL8+v08SBrBo@PVtA+B|0&Q3eE( z#vcn}l~hIZW9_}AJ%vdUr6t2m5{(F3TIEM5ND0T}vR1-0Rh>e}6Q6q`gqk^6n5VRJ zXG^+Hn3L{bf(_D-nZnjKB1pP+lT8n4nKXsX((;hbtFwDc@gry?p`E-Z!L%eF_0szZ zC9PfpB0)qTekOxNO)(zCCRY~LjT+1J5py2*q>+SYX~p&`HW@WkK8&OXjAZQ6l1;91 zT0B!5g`B}z7xkP`DTEIt%{vHjV86+!Dr(Jiz$xY<$w?*x+o$IlE zt|+ZLYTHD{J-WqdAQT*>OZY&nJE%Thkv@^|Dx|DNC_#6CCO;7+-jr`n0Q4s#n{+$kt4l4kc z51o-SGb%$ct)c$olqJofJ^tr5!5EH?s*jF?tp!G{0USfN=hXm~MaChAC(-3&c!(XA zh69$+>Q^t@iSb#>Pa_s0hI5JEbE#eWrr89OvU|u#5QV4E)bc5bvn&o;E6vM+8JiC_ zKIW`)cR-HLT2vc)$K2E8ZT1Y~RXc2t-cKk7O`znK`}R9g`;j>=uLamc zYGqCN^~kErE%^oo;U14DwwOzu%9TSpM~qs+^^Xc#%=R#dXLu@i z{bOTuYR=UEdaATif%Ho*f74unc$WF(d_jwhE%z1q(>!J0$@~#D=ahCTKHOdGG6e?< zz(M-m)4qh<0^0-`-g|F8uM-xeO@zllONoRYi3DDWX?hy*mYci1STzO)r2O^!-ZcEK zTwrf*L5Egyu)IopZtpQ6@j7!}hgN!U;oE@1CtX@bQH9B9YIZo87D$;E*lKoMnVL^Q zn?f)}Oqe1UXtM`4Xm_l=waU!2%G`IXO}t*5*HI&auEv0<#t>rT$(6ap3oCIu-e+80 zh&~;|4=5>4=8Q}Bm6<`2nGwuk2zmQ1CY@kGh^FTiKI_(cRuZYaB1?|0R6zyxoGP-q z-q<~p%H8G2_oJN3XdimkMA(bgRlb6ouXFmec+;l-r;N{1c{$McY}yP7gnWMN&ulnz zF#6w1qY&ISR0ynUTe?i6jOFJzY_g}{elKmi5kT6)pmm_bUZ-Pr%9KLN)J`a;cRk;r zuCzYk-S24X1ozA<0w8LBuY|qs*pRkS44%a>WMUH$ROMONGsIO?nuw*{94A75ZXYp? z`A13Yx$jMKeaP*N>zT^YMEa=RTuzVwDONc=FoMZW5yZ;E)DG_X@&aIg?J`Z84vQ)1 zj?Y06d{OdNkAQ43R4c3_ywFw=#H1J$4=h$4E~&gb?V53Jiwr16Mknu&=?W)M*HmnI zRyg{`4d8dsX+CuPi&sxN3At)QV2yUNmpEhFf_o__dAL{(ST+AjeT4PH59S0+-~{QM zFKrj^asEn2+GDOj#7SZ0YBPYh)4a4yLY}xs|D1AaJ&rN*HLGg+1>oZ5+=l!k4VozD z)i0%#RITOsP16%_rT>T74&eA`_=#Lq%zif4yAVcFuRDMHl+*VKVt3;IjyozdXDvR% zLlB5D0s;{{68Uf3;RdmM2=+K1B9A4b5!dz4cz#60ZiD7*H^|!MHbd(ls!Kud>>XlR z8$Lh7#81gr2+5OM7upp1L0FvbI)LSPAf1P}H`XL#Si$`$a>PhkVh8Pt?&%UW#<1s# z|8wFAo26PW9jd-x7(H>o!WlLWlD?DMr5Gz7*oS{4uTm*T%CrhN96D zjSivDy(w&r^>ERHe|q~S;YnEHxKT z_4q~oyjjdS;GG(RPFaes3=W^GPnQ-5mZbKW@l)@qHz3s92yNq>1b&9A*nUdFGf`n%WP*QDzR?huz3#I}sj>CH+9-C0QMJ@yhEep8i?2lHQ0VlNf3$wF<<0 zFB8Jh&5XRbzz%Ue&(^Z0!tc%db#>3{_)%^7cSR1-jrRB0jx}=G2|d;7mWPXU7Fv8h zj@5iEJDiH{d~7jROO|KGNc05xi`@%tkWog4={;(Iz|o=L7ADgZ2JQ?F?f_G+G8qbX zT}u7s3ol>4mluLmfl)J`mD6NRk1D(+^gNU2vjN+W^@3o|KmFfx3owmD68v-xBqFeVK`Zy;&qDnLEAnVXK^J;m|X+132yuq!z)r_**|Eq8gJEh<^zppF& zn>SOJJNMlQCkqSI!|yuyAytEg##4sqqcvlIGP~T<(8ZDd$TLg}zqV#WaS^^i)LG_Z zQ-w&UHiP()_3K*xNViV>LFKA>?3^R(aT~6-lo4p~_JMMk{ZD{n>e+5pCz6p~3pH}N z*ZJ2i(+-5Mg`DF~qV)v-`vn(=DpJj7h3as*Kv3ddw+MNc7HQB{!BmbC%Iytx`+0XeLxR!dV5t5OLhb| zeYJqg{b?-5+FTbH9OjaP!m8Tg)6xkx!DZ4y>;ylF$6Qm!sC1c}>tfaqaB(euYXJtd zd zm`>lV-p!03M7K|(N@B-^#YHH&9M1KtaeFuAGk6`jJ6=7BQ(>!E4aDUdP>9X_6fE~f zLX*1^CcG*uh{^R79Fzb^zkV}?$Px4s9fJu`kLfj5WdpuPdn5)X_fzvu6dk1No+(5g z09&_hH}e|4`F^6U1^O_C5sQSNxrY>Ir{-lwCQcDv94#7M!4oagoH#u}N_Dy%DQr1R z5+r;Ex#rH@fTtRS7Mz;g=1;!9Sy|W(AWNaVJYMZie-u-EtjHUVY$0R!wmR&&MBG_q zVI-~`s9XPactAbMHAJo(kq7DhBRn-W!CVEd4I(x4>9CR~xpAu$bf?8#tm!z(c+#|T zbp0Cyy;Uo?sc>owNh;H~;aM`j+}+V@!`^to0?h|?f^H9K3DBFvP`$h*xn*?#w@xg0 zf8Qc1A?5@rwAmLTTXYA3+Oj@(Qe2%VE*AfN+a3r}WV<~KQil_Vdc6wNOfoyXFao$s zM8e+okzfGA3E%p9k0cf$8*~SQ+TK2)r>$qLY@(S)Wd8;i!z>6ZoZuVZX&#O(9`5NG zQa&bBDREBJYA=B7ghq`cpvE$hbJKGwUf4_y(7%p_ot~l4+no8X5GOefKXGEDXpwefWtU)Crg1#QoN6=$4^&f3O zCBlQg9$4v{Ct$R#aHT{p%1R!31jVx=b!JY{lgvfJNnZmQG{aYT2YNPhXaq#6@sq#3 zzP=qFH8ui1a{EM}LGwMx=eEAX*d>S4Qv_8j*=ICV?_9Cer2X>11F35b z0RfnP^R-vB+*ddfoR&da--V&YkL?@Ki3GJ_F zu*T0Ogd2|RQG^10)-8v|j+n4oi4~+}zFyo*H+xvaX9Cath{rBJ1DdItvR> z)R7Eii%@^BLruM!%A6Gt4JvruR!Q&?OsJ!n2E0vni>=)FX+7t;kHuamX9L2gwJAz0 zHZZoixh==05~O2tXAsHdTIa#HNqgm3I(C--EQNY5;Yz2Md^?2DER~UK3DeR(>>Yg| zXNVxqO>}_u!3J==_8V4~gl0Wu`k3`~-3@2%&}Z=(!h+~L8@na0&ZBf0-bwp&DYELi zab@WhNea1(@!WV$hwnta@%-1he^4I0x&X9wvChyLdY8uez8bV~f%5?z<8cYXM~7WOaV^YDPId*X0Y}WrTqpqxqX1uPKqGAB7ezTdNdl zq`>gC_O6a#d*bCr1v9_KE^II{a)iQ3@^e5qLs?(SB)!H{lm_+5bbIZv*$>vKsup$_ zc(;QZtx24-_XTJZSs|X06`6vrm~XznN;C9d)pT*t$PEvyJmvdPb*^X%*86~|CVP@H zbr@88M)desa>6XC7>MCCy*UO(dO17~*@QM2bwkuEdF6NsgoI|k>ML~5#Ihk8xU@J zd^JX1AaOnMOMGj;5OuB{RH{%sE**(}jX*2X*l`m#e7Lvh*{xfp?i@ex%AY54h8f0X zQ4H+S9i35r6vgijq%ydyTjb@?)aI_{5tDV;Br7E>-2^PO)HA<(i5F@1PM`0>Kq#Ro zzPMt~7D-n{w=k+&$peEFY%1@pei?kgW9gGqhFD;y*b##`49e}otcQGE3n$Fj$xQi-wcs`=&r9vxL-i)^~uXIgSA*w4OA`$*&3g9BatB!wG zdmly-5NHqr0w1pbSNr{w>(5f}1MS~jLh$QoF-FM#p@DxG_&*W;+|a)f@F@PL6#wIo{Bw2x#!#UA2gX0wy1Ei7 V{1ie!cm)6Yz|W+8I3WVU{{sz2vLXNg literal 0 HcmV?d00001 diff --git a/tests/fixtures/hancom_saved/new_page_number_largest_kept.hwpx b/tests/fixtures/hancom_saved/new_page_number_largest_kept.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..969ab5f1343b894b42c4e7ce2c0c170d87108121 GIT binary patch literal 7664 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
bFC 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 HH>3XpLX}sF literal 0 HcmV?d00001 diff --git a/tests/fixtures/hancom_saved/new_page_number_negative_wrapped.hwpx b/tests/fixtures/hancom_saved/new_page_number_negative_wrapped.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..c362fb256a0f2b014f8d92877325c96c86c0501b GIT binary patch literal 7659 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 A=l}o! literal 0 HcmV?d00001 diff --git a/tests/fixtures/hancom_saved/new_page_number_smallest_kept.hwpx b/tests/fixtures/hancom_saved/new_page_number_smallest_kept.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..bad718839c1c4664b2b4d9d03991d40dc595b32f GIT binary patch literal 7658 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 A4FCWD literal 0 HcmV?d00001 diff --git a/tests/fixtures/hancom_saved/new_page_number_wrapped.hwpx b/tests/fixtures/hancom_saved/new_page_number_wrapped.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..ff0f619ae28acd75a8d11fb007b651d200b4ec30 GIT binary patch literal 7666 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 LD4S$LecSp!4qQ`X literal 0 HcmV?d00001 diff --git a/tests/test_gap_and_new_number_values.py b/tests/test_gap_and_new_number_values.py new file mode 100644 index 00000000..389a4d50 --- /dev/null +++ b/tests/test_gap_and_new_number_values.py @@ -0,0 +1,143 @@ +"""Caption gaps, column gaps and new numbers Hancom does not keep are refused before anything changes. + +Hancom keeps a caption's gap and the gap between columns of the same width as signed 16-bit numbers and a new +number (``hp:newNum/@num``) as an unsigned one, reading anything else wrapped (and a column gap written as negative +text as 0). ``tests/fixtures/hancom_saved/caption_gap_*.hwpx``, ``columns_gap_*.hwpx`` and ``new_page_number_*.hwpx`` +are its saves of a table caption written 32767, -1 and 32768 from the table, of two columns written 32767, 32768 +and -1 apart and of a page number restarted at 0, 65535, 65536 and -1. +""" + +from __future__ import annotations + +import zipfile +from pathlib import Path + +import pytest +from lxml import etree + +from hwpx import HwpxDocument +from hwpx.errors import HwpxValueError + +HP = "{http://www.hancom.co.kr/hwpml/2011/paragraph}" +FIXTURES = Path(__file__).parent / "fixtures" / "hancom_saved" + + +_SAVED_VALUES = {"caption": "gap", "colPr": "sameGap", "newNum": "num"} + + +def _saved(name: str, tag: str) -> str | None: + """The first ``hp:``'s saved value in the Hancom-saved fixture *name*: a caption's gap, the columns' gap + or a newNum's num.""" + + with zipfile.ZipFile(FIXTURES / name) as archive: + root = etree.fromstring(archive.read("Contents/section0.xml")) + return next(root.iter(f"{HP}{tag}")).get(_SAVED_VALUES[tag]) + + +def _sections(document: HwpxDocument) -> list[bytes]: + return [etree.tostring(section.element) for section in document.sections] + + +def test_hancom_keeps_a_caption_gap_as_a_signed_16_bit_number() -> None: + assert _saved("caption_gap_largest_kept.hwpx", "caption") == "32767" + assert _saved("caption_gap_negative_kept.hwpx", "caption") == "-1" + assert _saved("caption_gap_wrapped.hwpx", "caption") == "-32768" # written 32768 + + +@pytest.mark.parametrize("gap", [32768, -32769, 65536, 850.0, True]) +def test_a_caption_gap_hancom_does_not_keep_is_refused_before_anything_changes(gap: object) -> None: + document = HwpxDocument.new() + table = document.add_table(2, 2) + shape = document.shapes.add_rectangle(width=14400, height=7200) + shape.set_caption("그림 1", side="BOTTOM", gap=850) + before = etree.tostring(table.element), etree.tostring(shape.element) + + for host in (table, shape): + with pytest.raises(HwpxValueError) as caught: + host.set_caption("새 캡션", side="BOTTOM", gap=gap) # type: ignore[arg-type] + assert caught.value.code == "shape-caption-gap-value" + + assert (etree.tostring(table.element), etree.tostring(shape.element)) == before + + +@pytest.mark.parametrize("gap", [-32768, -1, 0, 32767]) +def test_the_caption_gaps_hancom_keeps_are_written_as_given(gap: int) -> None: + document = HwpxDocument.new() + table = document.add_table(2, 2) + + table.set_caption("표 1", side="BOTTOM", gap=gap) + + assert table.element.find(f"{HP}caption").get("gap") == str(gap) + + +def test_hancom_reads_the_gap_between_columns_as_a_signed_16_bit_number() -> None: + assert _saved("columns_gap_largest_kept.hwpx", "colPr") == "32767" + # written 32768: read as -32768 (the second column then starts inside the first), saved unsigned 32-bit + assert _saved("columns_gap_wrapped.hwpx", "colPr") == str(2**32 - 2**15) + assert _saved("columns_gap_negative_read_as_0.hwpx", "colPr") == "0" # written "-1" + + +@pytest.mark.parametrize("gap", [-1, 32768, 65536, 1200.0, True]) +def test_a_column_gap_hancom_does_not_keep_is_refused_before_anything_changes(gap: object) -> None: + document = HwpxDocument.new() + paragraph = document.add_paragraph("단 앞 글") + before = (len(document.paragraphs), _sections(document)) + + for where in ({}, {"paragraph": paragraph}): # the section's own columns, new columns at a paragraph + with pytest.raises(HwpxValueError) as caught: + document.page.set_columns(col_count=2, same_gap=gap, **where) # type: ignore[arg-type] + assert caught.value.code == "page-column-gap-value" + + assert (len(document.paragraphs), _sections(document)) == before + + +@pytest.mark.parametrize("gap_mm", [-1.0, 200.0]) +def test_page_setup_checks_the_column_gap_before_the_page_changes(gap_mm: float) -> None: + document = HwpxDocument.new() + before = _sections(document) + + with pytest.raises(HwpxValueError) as caught: + document.page.setup(paper_size="A4", orientation="LANDSCAPE", columns=2, column_gap_mm=gap_mm) + + assert caught.value.code == "page-column-gap-value" + assert _sections(document) == before + + +@pytest.mark.parametrize("gap", [0, 32767]) +def test_the_column_gaps_hancom_keeps_are_written_as_given(gap: int) -> None: + document = HwpxDocument.new() + + document.page.set_columns(col_count=2, same_gap=gap) + + assert next(document.sections[0].element.iter(f"{HP}colPr")).get("sameGap") == str(gap) + + +def test_hancom_keeps_a_new_number_as_an_unsigned_16_bit_number() -> None: + assert _saved("new_page_number_smallest_kept.hwpx", "newNum") == "0" + assert _saved("new_page_number_largest_kept.hwpx", "newNum") == "65535" + assert _saved("new_page_number_wrapped.hwpx", "newNum") == "0" # written 65536 + assert _saved("new_page_number_negative_wrapped.hwpx", "newNum") == "65535" # written -1 + + +@pytest.mark.parametrize("number", [-1, 65536, 2**31]) +@pytest.mark.parametrize("kind", ["PAGE", "TABLE"]) +def test_a_new_number_hancom_does_not_keep_is_refused_before_anything_is_added(number: int, kind: str) -> None: + document = HwpxDocument.new() + paragraph = document.add_paragraph("2쪽 첫 글") + before = etree.tostring(paragraph.element) + + with pytest.raises(HwpxValueError) as caught: + document.page.restart_page_number(paragraph, number=number, kind=kind) + + assert caught.value.code == "page-new-num-value" + assert etree.tostring(paragraph.element) == before + + +@pytest.mark.parametrize("number", [0, 65535]) +def test_the_new_numbers_hancom_keeps_are_written_as_given(number: int) -> None: + document = HwpxDocument.new() + paragraph = document.add_paragraph("2쪽 첫 글") + + document.page.restart_page_number(paragraph, number=number) + + assert next(paragraph.element.iter(f"{HP}newNum")).get("num") == str(number) From ac07e6b723b04fb6fb11f03e0b54805493c75d0c Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:03:20 +0900 Subject: [PATCH 06/15] docs(changelog): note the fix Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a5af1e..198b3736 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,16 @@ 바꾸고 문단을 더하기 전에 낸다. 2**31 이상도 거부한다. 아주 큰 `base_unit`으로 잰 상자가 2**31 이상이면 `shape-size-value`로 거부한다. 한/글은 그런 상자를 2**32 - 1로 줄여 저장한다. - `HwpxOxmlParagraph.add_picture`는 그림 요소를 만든 뒤에 런을 더한다. 값이 거부되면 빈 런이 남지 않는다. +- 캡션 간격, 같은 폭 단의 간격, 새 번호를 한/글이 그대로 두지 않는 값도 그대로 쓰던 것을 고친다. + - 한/글은 캡션 간격(`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`은 쪽 크기와 여백을 바꾸기 + 전에 단 간격을 검사한다. - 쪽 수 추정(실험, `estimate_pages`)이 쪽 번호 조판 부호(`hp:ctrl/hp:pageNum`)와 줄보다 넓은 글자처럼 둔 표만 있는 문단을 표 높이의 한 줄로 세던 것을 고친다. - 한/글은 둘을 한 줄에 두지 않는다. 부호의 줄은 문단 글의 빈 줄이다. From f898027ac2e09f6b8f1c0e12058cdea00bf3cef2 Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:10:47 +0900 Subject: [PATCH 07/15] fix(page): write unequal column widths as shares of 32768 Hancom reads a column's width and gap (hp:colSz) as a share of the text width out of 32768 and lays the column out width * text width / 32768 wide, whatever the shares add up to. column_widths were written as given, so widths in HWP units adding up to the text width came out about 1.3 times as wide and ran off the paper. They are now taken as proportions and written as shares adding up to 32768 (column_shares); widths that already add up to 32768 are written unchanged. Negative, non-int, all-zero or malformed pairs are refused before anything changes (page-column-widths-value). The page estimate takes a column's width as its share of 32768 of the text width. Co-Authored-By: Claude Opus 5.5 --- docs/error-codes.md | 3 +- src/hwpx/_document/layout.py | 9 +- src/hwpx/errors.py | 1 + src/hwpx/layout/pages.py | 9 +- src/hwpx/oxml/paragraph.py | 20 ++-- src/hwpx/oxml/section_format.py | 37 +++++- .../columns_unequal_widths_in_hwpunit.hwpx | Bin 0 -> 8400 bytes .../columns_unequal_widths_in_shares.hwpx | Bin 0 -> 8335 bytes tests/test_column_widths_as_shares.py | 110 ++++++++++++++++++ tests/test_section_columns.py | 3 +- 10 files changed, 175 insertions(+), 17 deletions(-) create mode 100644 tests/fixtures/hancom_saved/columns_unequal_widths_in_hwpunit.hwpx create mode 100644 tests/fixtures/hancom_saved/columns_unequal_widths_in_shares.hwpx create mode 100644 tests/test_column_widths_as_shares.py diff --git a/docs/error-codes.md b/docs/error-codes.md index 5d93cb40..5596420a 100644 --- a/docs/error-codes.md +++ b/docs/error-codes.md @@ -30,7 +30,7 @@ except HwpxError as exc: | 형태 | `style-not-found` | `VISUAL_COMPLETE_FAILED` | | 쓰임 | 예외 분기 | **발행된 영수증 스키마의 필드값** | | 관리 | major 경계 | 영수증 스키마 버전 | -| 개수 | 182 | 11 | +| 개수 | 183 | 11 | 통합하지 않는 이유: quality 코드는 `hwpx.mutation-report/v1` 과 `VisualCompleteReport` 에 이미 실려 나간 값이다. 이름을 바꾸면 영수증을 읽는 @@ -184,6 +184,7 @@ except HwpxError as exc: | `page-argument-conflict` | text 와 content 를 동시에 지정했다. | | `page-argument-missing` | text 또는 content 중 하나는 있어야 한다. | | `page-column-gap-value` | 같은 폭 단의 간격(same_gap)이 0 이상 32767 이하의 int(HWPUNIT)가 아니다(bool 도 거부). | +| `page-column-widths-value` | 단 너비(column_widths)가 0 이상의 int 로 된 (너비, 간격) 쌍이 아니거나 모두 0이다. | | `page-columns-invalid` | 단 수는 1 이상이어야 한다. | | `page-kind-invalid` | kind 는 'header' 또는 'footer' 여야 한다. | | `page-new-num-kind-invalid` | 쪽번호 재시작 kind 값이 OWPML 어휘(hp:AutoNumNewNumType/@numType) 밖이다. | diff --git a/src/hwpx/_document/layout.py b/src/hwpx/_document/layout.py index 1dd5e789..0f309b6f 100644 --- a/src/hwpx/_document/layout.py +++ b/src/hwpx/_document/layout.py @@ -19,7 +19,9 @@ from ..oxml.namespaces import HH, HP from ..oxml.numbering_kinds import ensure_numbering_levels from ..oxml.objects import HwpxOxmlInlineObject -from ..oxml.section_format import _PAGE_LANDSCAPE, _PAGE_PORTRAIT, _page_orientation_value, validate_column_gap +from ..oxml.section_format import ( + _PAGE_LANDSCAPE, _PAGE_PORTRAIT, _page_orientation_value, column_shares, validate_column_gap, +) from ..oxml.table_sizes import cell_margins_of from ._units import _mm_to_hwp_units, _pt_to_hwp_units @@ -734,6 +736,9 @@ def set_columns( col_count: Number of columns (1–255). col_type: ``NEWSPAPER``, ``BALANCED_NEWSPAPER``, or ``PARALLEL``. same_gap: Gap in HWPUNIT (7200 = 1 inch). + column_widths: With ``same_size=False``, a ``(width, gap)`` pair per + column, in HWPUNIT adding up to the text width or in plain + proportions; Hancom keeps them as shares of 32768 of the text width. separator_type: Optional column separator line type (e.g. ``SOLID``). """ if not 1 <= col_count <= 255: @@ -745,6 +750,8 @@ def set_columns( ) if same_size: validate_column_gap(same_gap) # before a paragraph is added + elif column_widths: + column_shares(column_widths) # checked before a paragraph is added too if paragraph is None: target_section = _resolve_section(doc, section=section, section_index=section_index) ctrl = target_section.properties.set_columns( diff --git a/src/hwpx/errors.py b/src/hwpx/errors.py index 8178c3ae..fb78f463 100644 --- a/src/hwpx/errors.py +++ b/src/hwpx/errors.py @@ -308,6 +308,7 @@ class Hwp5ConversionWarning(UserWarning): "page-kind-invalid": "kind 는 'header' 또는 'footer' 여야 한다.", "page-columns-invalid": "단 수는 1 이상이어야 한다.", "page-column-gap-value": "같은 폭 단의 간격(same_gap)이 0 이상 32767 이하의 int(HWPUNIT)가 아니다(bool 도 거부).", + "page-column-widths-value": "단 너비(column_widths)가 0 이상의 int 로 된 (너비, 간격) 쌍이 아니거나 모두 0이다.", "page-orientation-unsupported": "지원하지 않는 용지 방향이다.", "page-paper-size-unsupported": "지원하지 않는 용지 규격이다.", "page-new-num-kind-invalid": "쪽번호 재시작 kind 값이 OWPML 어휘(hp:AutoNumNewNumType/@numType) 밖이다.", diff --git a/src/hwpx/layout/pages.py b/src/hwpx/layout/pages.py index b186304c..4052c174 100644 --- a/src/hwpx/layout/pages.py +++ b/src/hwpx/layout/pages.py @@ -181,7 +181,7 @@ from ..oxml.paragraph_heading import paragraph_heading from ..oxml.section import _remove_short_paragraph_layout_cache from ..oxml.header_part import HwpxOxmlHeader -from ..oxml.section_format import _drawn_page_size +from ..oxml.section_format import COLUMN_SHARES, _drawn_page_size from ..oxml.table_sizes import cell_margins_of, grid_widths_of if TYPE_CHECKING: @@ -1274,12 +1274,13 @@ def _columns(section: Any, text_width: int) -> tuple[int, int, tuple[int, ...], count = int(settings[0].get("colCount", "1")) if settings else 1 if count <= 1: return 1, text_width, (), 0 - if settings[0].get("sameSz") != "1": # each column takes its share of the width with the gaps (hp:colSz), - sizes = settings[0].findall(f"{HP}colSz") # rounded + if settings[0].get("sameSz") != "1": # each column takes its share of 32768 of the text width (hp:colSz), + sizes = settings[0].findall(f"{HP}colSz") # rounded, whatever the shares add up to total = sum(int(size.get("width", 0)) + int(size.get("gap", 0)) for size in sizes) if len(sizes) != count or total <= 0: raise _Unsupported("columns of unequal width") - widths = tuple((2 * int(size.get("width", 0)) * text_width + total) // (2 * total) for size in sizes) + widths = tuple((2 * int(size.get("width", 0)) * text_width + COLUMN_SHARES) // (2 * COLUMN_SHARES) + for size in sizes) return count, min(widths), widths, 0 gap = int(settings[0].get("sameGap", 0)) return count, (text_width - (count - 1) * gap) // count // 4 * 4, (), gap diff --git a/src/hwpx/oxml/paragraph.py b/src/hwpx/oxml/paragraph.py index 8e289369..715d2b58 100644 --- a/src/hwpx/oxml/paragraph.py +++ b/src/hwpx/oxml/paragraph.py @@ -65,7 +65,7 @@ ) from .curves import _paragraph_add_connector, _paragraph_add_curve from .run import HwpxOxmlRun -from .section_format import validate_column_gap, validate_new_number +from .section_format import column_shares, validate_column_gap, validate_new_number from .shape_position import validate_equation_base_unit, validate_shape_size from .table import HwpxOxmlTable from .table_sizes import cell_margins_of @@ -796,8 +796,10 @@ def add_column_definition( layout: ``LEFT``, ``RIGHT``, or ``MIRROR``. same_size: If ``True`` all columns have equal width. same_gap: Gap between columns when *same_size* is ``True`` (HWPUNIT). - column_widths: When *same_size* is ``False``, a sequence of - ``(width, gap)`` tuples – one per column. + column_widths: When *same_size* is ``False``, a ``(width, gap)`` + pair per column, in HWP units adding up to the text width or + in plain proportions: they are written as the shares of 32768 + Hancom keeps (see :func:`~hwpx.oxml.section_format.column_shares`). separator_type: Line type for the column separator (e.g. ``SOLID``). separator_width: Line width (e.g. ``0.12 mm``). separator_color: Line colour (e.g. ``#000000``). @@ -806,6 +808,7 @@ def add_column_definition( raise ValueError("col_count must be between 1 and 255") if same_size: validate_column_gap(same_gap) # before its run is added + sizes = column_shares(column_widths) if column_widths and not same_size else [] run = self._create_run_for_object( run_attributes, char_pr_id_ref=char_pr_id_ref, @@ -832,12 +835,11 @@ def add_column_definition( line_attrs["color"] = separator_color _append_child(col_pr, f"{_HP}colLine", line_attrs) - # Individual column sizes when same_size=False - if not same_size and column_widths: - for w, g in column_widths: - _append_child(col_pr, f"{_HP}colSz", { - "width": str(w), "gap": str(g), - }) + # Individual column sizes when same_size=False, as shares of 32768 + for w, g in sizes: + _append_child(col_pr, f"{_HP}colSz", { + "width": str(w), "gap": str(g), + }) self.section.mark_dirty() return HwpxOxmlInlineObject(ctrl, self) diff --git a/src/hwpx/oxml/section_format.py b/src/hwpx/oxml/section_format.py index 18824dba..eb31334f 100644 --- a/src/hwpx/oxml/section_format.py +++ b/src/hwpx/oxml/section_format.py @@ -282,6 +282,40 @@ def validate_column_gap(gap: object) -> int: return gap +#: Hancom keeps each column's width and gap (``hp:colSz``) as a share of the text width out of 32768 and lays a +#: column out ``width * text width / 32768`` wide, whatever the shares add up to; its own add up to 32768. +COLUMN_SHARES = 32768 + + +def column_shares(column_widths: Sequence[tuple[int, int]]) -> list[tuple[int, int]]: + """*column_widths*, a ``(width, gap)`` pair per column in any one unit (HWP units adding up to the text width, + or plain proportions), as the ``hp:colSz`` shares Hancom keeps: in the same proportions, adding up to + :data:`COLUMN_SHARES`, so that the columns and their gaps fill the text width. Every value must be an int of + 0 or more, not all of them 0.""" + + from ..errors import HwpxValueError + + pairs = [tuple(pair) for pair in column_widths] + values = [value for pair in pairs for value in pair] + if ( + any(len(pair) != 2 for pair in pairs) + or not all(isinstance(value, int) and not isinstance(value, bool) and value >= 0 for value in values) + or sum(values) <= 0 + ): + raise HwpxValueError( + f"column_widths must be (width, gap) pairs of ints, 0 or more and not all 0; got {column_widths!r}", + code="page-column-widths-value", + context={"value": repr(column_widths)}, + suggestion="Pass a (width, gap) pair per column, e.g. [(20000, 1000), (21520, 0)] in HWP units.", + ) + total, run, edges = sum(values), 0, [0] + for value in values: # rounded at each edge, so that the shares add up to COLUMN_SHARES + run += value + edges.append((2 * run * COLUMN_SHARES + total) // (2 * total)) + shares = [end - start for start, end in zip(edges, edges[1:])] + return list(zip(shares[::2], shares[1::2])) + + class HwpxOxmlSectionProperties: """Provides convenient access to ```` configuration.""" @@ -473,6 +507,7 @@ def set_columns( if same_size: validate_column_gap(same_gap) # checked before anything changes, as the separator below + sizes = column_shares(column_widths) if column_widths and not same_size else [] line = None if separator_type or separator_width or separator_color: # checked before anything changes: a refused value leaves the columns as they were @@ -494,7 +529,7 @@ def set_columns( col_pr.remove(child) if line is not None: _append_child(col_pr, f"{_HP}colLine", line) - for width, gap in () if same_size else (column_widths or ()): + for width, gap in sizes: _append_child(col_pr, f"{_HP}colSz", {"width": str(width), "gap": str(gap)}) self.section.mark_dirty() return ctrl diff --git a/tests/fixtures/hancom_saved/columns_unequal_widths_in_hwpunit.hwpx b/tests/fixtures/hancom_saved/columns_unequal_widths_in_hwpunit.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..feead24f945f56bd4f75e281e70ff49051c6b280 GIT binary patch literal 8400 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

aho1l7b;&V~qBQtI$tbf$vFVfYK2GsC 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 literal 0 HcmV?d00001 diff --git a/tests/test_column_widths_as_shares.py b/tests/test_column_widths_as_shares.py new file mode 100644 index 00000000..dfb146ae --- /dev/null +++ b/tests/test_column_widths_as_shares.py @@ -0,0 +1,110 @@ +"""Columns of unequal width are written as the shares of the text width out of 32768 Hancom keeps (``hp:colSz``). + +Hancom lays a column out ``width * text width / 32768`` wide, whatever the shares add up to. +``tests/fixtures/hancom_saved/columns_unequal_widths_in_*.hwpx`` are its saves of two columns written +``[(20000, 1000), (21520, 0)]`` (HWP units adding up to the text width, 42520: the second column then runs off +the paper) and ``[(16000, 768), (16000, 0)]`` (shares adding up to 32768). +""" + +from __future__ import annotations + +import zipfile +from pathlib import Path + +import pytest +from lxml import etree + +from hwpx import HwpxDocument +from hwpx.errors import HwpxValueError +from hwpx.layout import pages as page_layout + +HP = "{http://www.hancom.co.kr/hwpml/2011/paragraph}" +FIXTURES = Path(__file__).parent / "fixtures" / "hancom_saved" + + +def _saved_section(name: str) -> etree._Element: + with zipfile.ZipFile(FIXTURES / name) as archive: + return etree.fromstring(archive.read("Contents/section0.xml")) + + +def _sizes(section: etree._Element) -> list[tuple[str | None, str | None]]: + return [(size.get("width"), size.get("gap")) for size in section.iter(f"{HP}colSz")] + + +def _sections(document: HwpxDocument) -> list[bytes]: + return [etree.tostring(section.element) for section in document.sections] + + +@pytest.mark.parametrize( + ("fixture", "widths"), + [("columns_unequal_widths_in_hwpunit.hwpx", (25952, 27925)), ("columns_unequal_widths_in_shares.hwpx", (20762,) * 2)], +) +def test_hancom_lays_a_column_out_its_share_of_32768_of_the_text_width(fixture: str, widths: tuple[int, ...]) -> None: + # 20000 and 21520 of 32768 over a text width of 42520 are 25952 and 27925 wide (not 20000 and 21520, their + # share of what the sizes add up to); 16000 of 32768 is 20762. + section = _saved_section(fixture) + + assert {int(line.get("horzsize")) for line in section.iter(f"{HP}lineseg")} == set(widths) + assert page_layout._columns(section, 42520) == (2, min(widths), widths, 0) + + +@pytest.mark.parametrize("where", ["section", "paragraph"]) +def test_column_widths_in_hwp_units_are_written_as_shares(where: str) -> None: + document = HwpxDocument.new() + paragraph = document.add_paragraph("단 앞 글") + + document.page.set_columns(2, same_size=False, column_widths=[(20000, 1000), (21520, 0)], + **({"paragraph": paragraph} if where == "paragraph" else {})) + + written = paragraph.element if where == "paragraph" else document.sections[0].element + assert _sizes(written) == [("15413", "771"), ("16584", "0")] # 20000, 1000 and 21520 of 42520 + + +def test_column_widths_in_hwp_units_adding_up_to_the_text_width_come_out_as_wide_as_given() -> None: + document = HwpxDocument.new() + + document.page.set_columns(2, same_size=False, column_widths=[(20000, 1000), (21520, 0)]) + + assert page_layout._columns(document.sections[0].element, 42520)[2] == (20000, 21520) + + +@pytest.mark.parametrize( + ("given", "written"), + [ + ([(10632, 873), (21263, 0)], [(10632, 873), (21263, 0)]), # shares already + ([(6552, 873), (12234, 873), (12236, 0)], [(6552, 873), (12234, 873), (12236, 0)]), + ([(1, 0), (2, 0)], [(10923, 0), (21845, 0)]), # plain proportions + ], +) +def test_column_widths_are_written_in_their_proportions_adding_up_to_32768( + given: list[tuple[int, int]], written: list[tuple[int, int]] +) -> None: + document = HwpxDocument.new() + + document.page.set_columns(len(given), same_size=False, column_widths=given) + + assert _sizes(document.sections[0].element) == [(str(width), str(gap)) for width, gap in written] + + +@pytest.mark.parametrize( + "column_widths", + [ + [(-1, 0), (20000, 0)], + [(20000, -1000), (20000, 0)], + [(0, 0), (0, 0)], + [(20000.0, 0), (20000, 0)], + [(True, 0), (1, 0)], + [(20000,), (20000, 0)], + ], +) +def test_column_widths_that_are_no_shares_are_refused_before_anything_changes(column_widths: list[tuple]) -> None: + document = HwpxDocument.new() + paragraph = document.add_paragraph("단 앞 글") + before = (len(document.paragraphs), _sections(document)) + + for where in ({}, {"paragraph": paragraph}): # the section's own columns, new columns at a paragraph + with pytest.raises(HwpxValueError) as caught: + document.page.set_columns(2, same_size=False, column_widths=column_widths, **where) + assert caught.value.code == "page-column-widths-value" + + assert (len(document.paragraphs), _sections(document)) == before diff --git a/tests/test_section_columns.py b/tests/test_section_columns.py index a85f9b09..ed3313e5 100644 --- a/tests/test_section_columns.py +++ b/tests/test_section_columns.py @@ -86,7 +86,8 @@ def test_unequal_columns_write_their_sizes() -> None: col_pr = _section_column_definition(_section_xml(document)) assert (col_pr.get("sameSz"), col_pr.get("sameGap")) == ("0", "0") sizes = [(size.get("width"), size.get("gap")) for size in col_pr.findall(f"{HP}colSz")] - assert sizes == [("20000", "1000"), ("21520", "0")] + # as the shares of 32768 of the text width Hancom keeps: 20000, 1000 and 21520 of 42520 + assert sizes == [("15413", "771"), ("16584", "0")] def test_setting_one_column_clears_the_previous_layout() -> None: From 86b12167166a67ae2635af662c267920ea8dabd9 Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:10:47 +0900 Subject: [PATCH 08/15] docs(changelog): note the fix Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 198b3736..180f11a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,16 @@ `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이라 결과가 같다. - 쪽 수 추정(실험, `estimate_pages`)이 쪽 번호 조판 부호(`hp:ctrl/hp:pageNum`)와 줄보다 넓은 글자처럼 둔 표만 있는 문단을 표 높이의 한 줄로 세던 것을 고친다. - 한/글은 둘을 한 줄에 두지 않는다. 부호의 줄은 문단 글의 빈 줄이다. From f7458fbe7760e51f41cd34cf06cdcf17f42cb007 Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:27:45 +0900 Subject: [PATCH 09/15] docs(layout): state the share of 32768 in the page estimate's notes Co-Authored-By: Claude Opus 5.5 --- src/hwpx/layout/pages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hwpx/layout/pages.py b/src/hwpx/layout/pages.py index 4052c174..c9062fb8 100644 --- a/src/hwpx/layout/pages.py +++ b/src/hwpx/layout/pages.py @@ -28,7 +28,7 @@ other, and take no room). Page and column breaks, page break before, keep lines together, keep with next and widow/orphan control; columns of equal width, and columns of - unequal width (each its share of the text width with the gaps, rounded) holding objects only as + unequal width (each its share of 32768 of the text width, ``hp:colSz``, rounded) holding objects only as characters: a paragraph without a valid layout cache breaks the lines a column holds at that column's width, and going on into a column of another width breaks its rest there again, from the first character that column holds. From c8a06be9afa6ac6897874a82c6ccdcd0302aab60 Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:37:17 +0900 Subject: [PATCH 10/15] test(page): Hancom lays the written shares out at the widths given Co-Authored-By: Claude Opus 5.5 --- ...columns_unequal_widths_written_as_shares.hwpx | Bin 0 -> 8408 bytes tests/test_column_widths_as_shares.py | 11 ++++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/hancom_saved/columns_unequal_widths_written_as_shares.hwpx diff --git a/tests/fixtures/hancom_saved/columns_unequal_widths_written_as_shares.hwpx b/tests/fixtures/hancom_saved/columns_unequal_widths_written_as_shares.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..a5accd72544214dee924c08d0ce23b76a257decb GIT binary patch literal 8408 zcmZ{J1z40_^EWLB0!w!+9nvANG%VdINV9ZFhmuNncgi9i0xG?LbV{johcrmXw>;1L ze|;XW_uIYB-gD-h`Q3A7PtA+Hm7oljyCw|Ga!wdyP1=vnS-+vL=@-)aRbtbi-BxBh2^Eex_=|lz(Jhc zgl#-Ufi~{$F2bCgUS3`tHfC@jM-B^T4tv)J#T*?tdAPW_IsfxEAdS13HN;)=xAvky zJvB`kbxk#CSuHI!O(2b4;M&AO><*S+W2uro?*)VJQ%x_)+A6- zDx^D5oc#94tb}IRwJQsWdh{)=?vOef<1jYr2Zv zhvXZwDUf`BFl{WUSWWCse&*aA9Sslgu~n3EcTl)z#F)eD<;&V4z)o$9z_1U>b1TJTNRnRDfgw7;yK|;sxsH*Xh-A;KcxnjSIgN6 zS00+;Hb}y&cP~B}t7}Q0jnO1L55vTDQ$7T($=5%S_~~+kbP6%f`g*5 z@0r<*wB;)8SUwXei9y9+B#CGrIJXn|i-&5Y0V*Nc2B&$m2F zyyhwfzoPPig|};!lid& zb$zN^A01@-?Pfg5q8-PoD7$*5B6uL{aoek_tDnw-2Cx_16ADhwQ@+EEhgYdSIVjgT znGeLR>Yj1s{FcC*)sI6*C}ykL-meE+7GAr!jsZyL8$aJxu$hals>Y>)!~$gY5t`&e zZNZGbZ9hv!k*-bW-yX(?Ou-P0YLsGz3=3w>)qms&DBxdlplo{BWE#oqX>qOLybkeU zJak`t-{Lz<0jUYT%qcpKNjLI z&O0dmgxQQp1lYKcDo(Bh>I&Gm5$u}+HbYpirRDqGx0))&gRRJ-aNd9C*`#P%@{BvA zp$Dbcv~^QII-n3;;*T`yJjAFP-RbICW8bQM6o)?OK?&aU@m%F_cWmTSVc@95U&;+z z73iJ;f!=my=uxxalYcAXHsR&P;B_-+6ck6^-QitAAotA<159tbXzMe*?k<);cd|^0 zsk^S%D3cmvZ^b5q&Llu>xotpsaeR5BVU%xHJLxj%#O<+=(U9hC*eIj)#qRb@wT@wfmrl1NO~QXI0S`FbQF92xp)laXG_f}Qei(4YB1 zzPWb{!dwmF#i{6zar9xEHdOdWhw9U;AYJ}G8Sn5zeFg9L6Nm33w{XX43+c!x-+Dx1 zCm2L*>6dRYS82IB>^HCKvh0Q!Um1-J+Tzr&c}Z}FoNe>m;(pQ0UjEh%fyYOa}|Vs4!;VNdYojQ1Qc0aFn6q*HM&jR3rmToQ%pFH z-#MN&9B{8CQJ9j^@q`=16j70rtIT$tUjo~!)swIQ3#2Bu)lbC8uT2P9w%ya!F4Fq_ zVkUfx=A*=DoLw~&Xh2d`sPacOKFtbtE+&`tn%d=G>^>LjDxD%ujz})9nRrpLiVRT& zVPg_#I!n;DDg4qnmn_#2vEfhEQ&A~4FuQ%Nb0JIiHZp6i+?;hv(f*(uhdNc$R(pZQ z`t?Okw&KTuCtJGCxzmO*U2-ff;J!OEh8W{!!S^#Fm$7!AvcO$C!!ZLQ4M`()bUYuo z__Ny)MO8(Z8qSLL6)L9;rbYan@SxVu!O=5z0K@HR$_USv zK%D6{VA#TO>2%Cdu+Ob0c2#P$sNn}zXm3`*``Z|bNt7@&1s20rt=PZ}gRcqpuOxjD zx)Hrbnp!u;kv*x<$jMqg)PD*X40CA}Zx-4F6RPOMNFvOhTYNeQcGBIJiY`TAtG6lh z&l`fCtvAQ?w91`}l!V~j%!L&fh`8O#SHNUcYk^Lx|36{CTe z69SAbF2A`fFA<4Yb#ErJj6UP0Aaaol9rlbM5nv24^u-#g73XH)SE!k9_nKxz=@(2L zd-4pktR_YPb0?nm#9NXuE8W{T zfu!rW8OiRYfToi1R;j&;bp9 zbT6D)P#HpK4fUU7%xezr2t2nAL3eaieS8>VB{*UQW#;jvYoM1GGPDk4hbR=a$iWgSBD)Kf-+Fd~Ddo?l6vb#}l?=^I#qv1MX9X9yyU8o1tBcS?hot+0GVu5;E3kH$E!Ebl)_L2!^6HT z17Ozn_n*DIWZ5E~gG37}>tDPFY!9_JmROjUSb7h&iB?PB!9IvQQe!|=V+ghO;?AAt^Dc8c++|$) z9&<8^7g$!B&Xthz%@Vs{I;6GItEeC`75GfvWy#qPxa?ROvw37g_# zARts4As`UKJ(2(V9c~bd2WOA#!Sk4ZG4Hh61n?(>weEcBSrLb1?@9`YXCXWn(PtvB z7YuTkX!NyINLl;EsJAI}MIc-4J3-8Pg-fY&Ax%g1Iad8i# zytR%qRi5XDhtX-%{FI#sPn=}eie+}*jkf1zxp{);%W!o;j>r*kp?8uDAnm9kI_gpt z`>k{Q+C8|=S1XRIlq~<7V$32v|AY8In7L!g++Nfdem#lY0FLla~ zjR2#b7VOYP;>sSkhVU6tqKTLx%SW=c>=Q{X*&a-*RfpShl_F6o7ODE({o$W+jnld6`_7Zp(G zi>cAlAB4ZMuGGsen6$_?w_OwV1~7F-lE*EQQwQ0{k`^bdcVO`ciW6o`@25Uq0a?f= z#P(j`h%MgukF|?8zpaY$t{71vHVr`sJRV*huNi?!Lr6krSJIw|?v34&#n9RKwfJo< z&UppDdS0Rc4L$kvO&M)_ndA7jtcG-iSZBKbnJRUXf@nLMiOq(U@-OU-fZVCs(xCxM zJew)%LH5~|x%HMVgJtwGw-VdGLO$r=1`^0VXww%1kCw$GvUQndY1n$c+eb#-9pz`02v zu?CfyRH;$CG!yPQXD*ta8ga-;m|EKH^#SaLOb?%E2+0*Ss;6nzil5xE+zYeX7$ROV zlhyp8d2lqRp?BVru!6}78Z)t{7Z}Nl&QA`Qcqx{nTV^Z*ER=VktYsmQbD$E8wNdno z{X~9#UgCPWUUwhBlLGtl<^G#;Q&uvJnR*7oE8>!CL}Uh?Zhu8lC70VXihGi^K7aB3 zGd6pTYS6uu0pBaz;Kv~U=qN|NFwd^Em^2_5z;4HT>qjeacie$WxvRZ?x()zDbUdoPz6_ybxT@S4Ia^9w-^`*J;&ls?PJOQij+Ost1naA z{RW>@=q+zuQs?y}CNl~|IXsSW83FW!LvDfsTwNL}ypIuhbrKQkYZmO0%q7_vtw#H} zs+ig6VPc^`74`qI@YjMorM56{Fj6!xu&>ew-b{bWO`;L(&Xwr2 zT;CCOYSj|pQpojn1RJrRAy+_*5U>K_T6qBmSiqZ)D#Lw9qorsZ(6uD9S8Rhw0s9tQ zQhWpeT7cUCUoEPg%)7X8W^;*HTq0FL(TBP;XHmi%ZR5^>j~li@WEz}m933kWwFD$0 z#hKrb1PhIGjU-6i@RxG-+08KL!hL;wC7s{O2-AO7Nn?+v+u1}!Q{|3(*5kCAfa)o8 z3xd(0!V;BMk{$6dcf+rI-q0qqn*+SE{P8eP!+m^0gTl1Iv|dqARM+kn5G*i$N%L|3 zf$CAcL~MGR90>_Ho`^TBB#}Rxs1I;*j4s_Rv=L7%GXI#C=o8@N1PeJe@esFpKAq{I zdW?m@mO}@mKwe1ct?n$GZZb4D-Qrqgq(Z!0?b} zk>dSHB!0UmO5KNM`iiO=eett#6q7x+O+?7oFdJ|@R#(TI)zM@RQxQ?u70d>RWfl%5 z`22PPiN(&X_r&xW>Pon5^e5Q~Bqlqz_7j)YXxueG_taC!pNC0HTn=$KDIfn2LHw2%u!zSNEvcWwR~VS=g6UWK9;1Eo7?^fv|9~* z;DoIz$jYbVI-RImZO3tVhnLU*iVPN5as~7?vaWOF-q7WEnk(e^KQ$-v)+qOC#O_Jr ziA85}itFbkPmBIBw+{lQJZjJm@OO(Wi7^B}Xa$G`M!YAWcmJEQ??u&C{suiYT~75g z;KR$o_rpqtVOdNGc(g#^$ML?Nr$qwJj+D_%y%ciVP4mSTM130NX^pY+Vx)wC<=7-O z@|ox#Ee#%Ax$!4iC}0JzN1j;#8E7xp71971Nuu zsvafOde*TgM1dxeV}`KkvmCtm>Ex_yU++~2bGp^ zFrBM$^giZi(r}XSCcSmG%Wphc8wa%=94JP>3Qi9(BTnOYUe86#VH&?9shshk1f7lW zeF9OGpV=#Dm^k-nSg;(Xxn$rJIOnovML*^1N`FuKl970Yp;-$^Dpvu9j8}NrO%yT)46UUtFiX0~K{7gV-b0({!k*moi!M17kqYH|^E< zbqWMJidh`D>27h=Yrm{!ymm34Hh`@`c(m55VnJ=N)NSQrq zb=`2oRW$fXe2M@X^Um6Kp1boPTZZqe{h1VLZNr$d^r9pM_S6an&gPyTH@P}EKb9ufjV0~!LtL-y#ue#3+D;E4+k8yBk_ zox#K`F5DYJMK8LRFnU?i16Zjx$7hJRDf<&Jx33J|;MDa~nuJ=cZv*9!>!TQ9!dZ23%x%paGSw{9#^WRY-0EO&IlLhOkan&eIWo4T+Rh>#=Y zkJ6t5BN!@rGrrPmkfD55|C(*D9X_3Bm8oiOiw?LQ&}d8Ln!bmkjpv7YMOEcKbH#Z3 z^Hr8%z>=nmgGNzAQ1uCaUd@@JiGp4phMMeA#>9Sb{VCy-i}d7aR59veuN;k+`bwG? z3(drx2k7Do`9II7o5&g8)j)M8DRTsi-{wo%N?0%#<{omq)d%n2 z?Yk7y)9O@?;Y2BZeRh`GgN_G`{_x&B>eY=ij6zs(^~^@_ruuD0`V(v&Wp5N#!qfzu zgk5d+6+_D#G?J!UPS(p_i3myW^_~rH6sOWPq?VPtQ<7%#7qXWrp}kAg?xGrfc;h@6 z9^G2btu4rFAGgO$wPR>wc?mW0v;^IGH9wKZXLdQRr$t> zD9v?Q(_g)|;9Rf*cKLBZ-A}S@g_E?#r>42`Q4U9y^o2t_V}=l$TCEKR7vK7!(_wSp zdb*yzzgdt;i{R`Zz9)4$!z40%0nWn9|7QWhcb?Z8!*)pAxc({e_TQt=w1dm#OUI<6 z9$h2QiZsD)5{CA7X1%&~Yt)^S`d`+$Em5! zQ_CwR>#$B*PEfuM{NCEglK2u3WtynZe{LX@{649)YR3jiS4FoZx>m^(okYPz&RP93 zWRKUvudo6UYOC35MlAXLT`X$Z9yf`1?(k+u$T6#lW?}1z{z0C(xc8!^^lLVg=b^G} z-|P!GYydTG&;qv&5To@$1#K+OZE_c?ZQMqn+OAUP{p4(LQ5QxdWNtZvAZIH#;}hn0 zw*fgmYUgH;le^DBvtaFQD(|&X6YE233>UAYb-{lxJ!iJ17cb#UE)-rKT=xI&TaIQ< zwpI`~_lNzOKSt?s7Z(<4HL(7`nOG)!*EjeKp+`58)}J*&>&GlpvHNv;#!TFS@PraE zXJ|*-YKB4rJ241p(RoI?ckL;#_j`Vj_>S`Gm|^ZaaI!+{_(I^I7^*RGxGX0}xQ>wl zo*!#naRY1S%~(6jV%J0s{3t<0BKT`7fU5*gX#SZ#c`Jv{#}t^B9dpIPLG zzJE&z!Jq$0>OPF`x8VQIpZ+EH*Xz9Dv+KXR)9-BRp8$UbLVg1rKKTd0Kf@w_Li~9V z_zl5J{6DV+e**k@0RINKfdl;WIQ~=e&$H~exuyTcU?%&A1^%(Y z|B3MDg8q%*K=D7d_#c1dpR@ZnMiJ#dF#b8$)s;};rw{_dWBAt(ekRFLJ=Fdm+M>;A literal 0 HcmV?d00001 diff --git a/tests/test_column_widths_as_shares.py b/tests/test_column_widths_as_shares.py index dfb146ae..020b6f5c 100644 --- a/tests/test_column_widths_as_shares.py +++ b/tests/test_column_widths_as_shares.py @@ -3,7 +3,8 @@ Hancom lays a column out ``width * text width / 32768`` wide, whatever the shares add up to. ``tests/fixtures/hancom_saved/columns_unequal_widths_in_*.hwpx`` are its saves of two columns written ``[(20000, 1000), (21520, 0)]`` (HWP units adding up to the text width, 42520: the second column then runs off -the paper) and ``[(16000, 768), (16000, 0)]`` (shares adding up to 32768). +the paper) and ``[(16000, 768), (16000, 0)]`` (shares adding up to 32768); +``columns_unequal_widths_written_as_shares.hwpx`` of the shares now written for the first. """ from __future__ import annotations @@ -48,6 +49,14 @@ def test_hancom_lays_a_column_out_its_share_of_32768_of_the_text_width(fixture: assert page_layout._columns(section, 42520) == (2, min(widths), widths, 0) +def test_hancom_lays_the_shares_written_for_widths_in_hwp_units_out_at_those_widths() -> None: + # [(20000, 1000), (21520, 0)] written as shares, saved by Hancom: its lines are 20000 and 21520 wide. + section = _saved_section("columns_unequal_widths_written_as_shares.hwpx") + + assert _sizes(section) == [("15413", "771"), ("16584", "0")] + assert {int(line.get("horzsize")) for line in section.iter(f"{HP}lineseg")} == {20000, 21520} + + @pytest.mark.parametrize("where", ["section", "paragraph"]) def test_column_widths_in_hwp_units_are_written_as_shares(where: str) -> None: document = HwpxDocument.new() From d42a366b396fdeea010d14b9e870d574059c289a Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:24:13 +0900 Subject: [PATCH 11/15] fix(table): write columns no narrower than Hancom draws them Hancom draws a table column at least its cells' left and right margins and 283 wide (1303 in a new table), keeps the narrower width written in the cells and saves the table as wide as it draws the columns. A new table now gives each column at least that floor (a nested table without a width in a narrow cell too), and set_column_widths writes a column below its floor at the floor and widens the table by as much. A new table width that is no int in 0 < width < 2**31 is refused before any paragraph, run or border fill is added (table-width-value). Co-Authored-By: Claude Opus 5.5 --- docs/error-codes.md | 3 +- src/hwpx/document.py | 2 + src/hwpx/errors.py | 1 + src/hwpx/oxml/paragraph.py | 8 +- src/hwpx/oxml/table.py | 3 +- src/hwpx/oxml/table_sizes.py | 48 +++++++ .../hancom_saved/table_column_floor_1302.hwpx | Bin 0 -> 7806 bytes .../hancom_saved/table_column_floor_1304.hwpx | Bin 0 -> 7805 bytes .../table_column_floor_m0_282.hwpx | Bin 0 -> 7813 bytes .../table_column_floor_m1000_2282.hwpx | Bin 0 -> 7831 bytes .../table_column_weights_zero_one.hwpx | Bin 0 -> 7824 bytes .../hancom_saved/table_width_zero.hwpx | Bin 0 -> 7838 bytes tests/test_set_column_widths_uniform_split.py | 11 +- tests/test_table_column_floor.py | 119 ++++++++++++++++++ 14 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 tests/fixtures/hancom_saved/table_column_floor_1302.hwpx create mode 100644 tests/fixtures/hancom_saved/table_column_floor_1304.hwpx create mode 100644 tests/fixtures/hancom_saved/table_column_floor_m0_282.hwpx create mode 100644 tests/fixtures/hancom_saved/table_column_floor_m1000_2282.hwpx create mode 100644 tests/fixtures/hancom_saved/table_column_weights_zero_one.hwpx create mode 100644 tests/fixtures/hancom_saved/table_width_zero.hwpx create mode 100644 tests/test_table_column_floor.py diff --git a/docs/error-codes.md b/docs/error-codes.md index 5596420a..28a40780 100644 --- a/docs/error-codes.md +++ b/docs/error-codes.md @@ -30,7 +30,7 @@ except HwpxError as exc: | 형태 | `style-not-found` | `VISUAL_COMPLETE_FAILED` | | 쓰임 | 예외 분기 | **발행된 영수증 스키마의 필드값** | | 관리 | major 경계 | 영수증 스키마 버전 | -| 개수 | 183 | 11 | +| 개수 | 184 | 11 | 통합하지 않는 이유: quality 코드는 `hwpx.mutation-report/v1` 과 `VisualCompleteReport` 에 이미 실려 나간 값이다. 이름을 바꾸면 영수증을 읽는 @@ -347,6 +347,7 @@ except HwpxError as exc: |---|---| | `table-cell-zone-grid-mismatch` | 셀 영역(hp:cellzone)이 새 열 격자에서 같은 셀들을 덮을 수 없어 열 너비를 바꾸지 않았다. | | `table-position-missing` | 표에 hp:pos 가 없어 글자처럼 취급을 바꿀 수 없다. | +| `table-width-value` | 새 표 폭(width)이 0 초과 2**31 미만의 int(HWPUNIT)가 아니다(bool 도 거부). | ### `text-*` diff --git a/src/hwpx/document.py b/src/hwpx/document.py index 18bc8275..29147f42 100644 --- a/src/hwpx/document.py +++ b/src/hwpx/document.py @@ -29,6 +29,7 @@ HwpxPackage, ) from .oxml.namespaces import register_owpml_namespaces +from .oxml.table_sizes import new_table_width from .mutation_report import Fallback, Mode, MutationReport from .quality import QualityPolicy, SavePipeline, VisualCompleteReport from .templates import blank_document_bytes @@ -585,6 +586,7 @@ def add_table( self, section, section_index, caller="add_table" ) section_index = None + new_table_width(width, cols) # checked before a paragraph or a border fill is added resolved_border_fill: str | int | None = border_fill_id_ref if resolved_border_fill is None: resolved_border_fill = self._root.ensure_basic_border_fill() diff --git a/src/hwpx/errors.py b/src/hwpx/errors.py index fb78f463..775b4923 100644 --- a/src/hwpx/errors.py +++ b/src/hwpx/errors.py @@ -252,6 +252,7 @@ class Hwp5ConversionWarning(UserWarning): "cell-margin-value": "셀 여백 값이 0 이상 2**31 미만의 int(HWPUNIT)가 아니다(bool 도 거부).", "cell-border-edit-unsupported": "셀 테두리 편집 대상 스타일이 없거나 모호하거나 네 변이 온전하지 않다(또는 색·선 종류가 잘못됐다).", "table-position-missing": "표에 hp:pos 가 없어 글자처럼 취급을 바꿀 수 없다.", + "table-width-value": "새 표 폭(width)이 0 초과 2**31 미만의 int(HWPUNIT)가 아니다(bool 도 거부).", "table-cell-zone-grid-mismatch": "셀 영역(hp:cellzone)이 새 열 격자에서 같은 셀들을 덮을 수 없어 열 너비를 바꾸지 않았다.", # -- 머리말·꼬리말 story ------------------------------------------------ "story-ambiguous": "그 쪽 종류·id 에 맞는 머리말/꼬리말 story 가 여럿이거나 식별이 모호하다.", diff --git a/src/hwpx/oxml/paragraph.py b/src/hwpx/oxml/paragraph.py index 715d2b58..1301a5d1 100644 --- a/src/hwpx/oxml/paragraph.py +++ b/src/hwpx/oxml/paragraph.py @@ -68,7 +68,7 @@ from .section_format import column_shares, validate_column_gap, validate_new_number from .shape_position import validate_equation_base_unit, validate_shape_size from .table import HwpxOxmlTable -from .table_sizes import cell_margins_of +from .table_sizes import NEW_TABLE_COLUMN_FLOOR, cell_margins_of, new_table_width if TYPE_CHECKING: from .section import HwpxOxmlSection @@ -579,8 +579,10 @@ def add_table( run_attributes: dict[str, str] | None = None, char_pr_id_ref: str | int | None = None, ) -> HwpxOxmlTable: - if width is None: - width = self._context_table_width() + if width is not None: + width = new_table_width(width, cols) # checked, and floored as Hancom draws it, before its run is added + elif (context := self._context_table_width()) is not None: # a narrow cell's may be below the floor + width = max(context, cols * NEW_TABLE_COLUMN_FLOOR) if border_fill_id_ref is None: document = self.section.document if document is not None: diff --git a/src/hwpx/oxml/table.py b/src/hwpx/oxml/table.py index 6dea1cc8..1774a184 100644 --- a/src/hwpx/oxml/table.py +++ b/src/hwpx/oxml/table.py @@ -521,6 +521,7 @@ def add_table( The table is created inside a new paragraph within the cell's ````. """ + _table_sizes.new_table_width(width, cols) # checked before a paragraph is added # Resolve border fill ID if border_fill_id_ref is None: document = self.table.paragraph.section.document @@ -751,7 +752,7 @@ def create( if rows <= 0 or cols <= 0: raise ValueError("rows and cols must be positive integers") - table_width = width if width is not None else cols * _DEFAULT_CELL_WIDTH + table_width = _table_sizes.new_table_width(width if width is not None else cols * _DEFAULT_CELL_WIDTH, cols) table_height = height if height is not None else rows * _DEFAULT_CELL_HEIGHT if border_fill_id_ref is None: raise ValueError("border_fill_id_ref must be provided") diff --git a/src/hwpx/oxml/table_sizes.py b/src/hwpx/oxml/table_sizes.py index 839987aa..3ae038d3 100644 --- a/src/hwpx/oxml/table_sizes.py +++ b/src/hwpx/oxml/table_sizes.py @@ -18,6 +18,7 @@ from ._document_primitives import ( _HP, _clear_paragraph_layout_cache, + _default_cell_inner_margin_attributes, _distribute_size, _element_local_name, ) @@ -34,10 +35,53 @@ "effective_cell_margin_source", "set_column_widths", "grid_widths_of", + "COLUMN_FLOOR_EXTRA", + "NEW_TABLE_COLUMN_FLOOR", + "new_table_width", ] _MARGIN_SIDES = ("left", "right", "top", "bottom") +#: Hancom draws a table column at least its cells' left and right margins and this wide, the table as much +#: wider (it keeps the narrower width written in the cells). +COLUMN_FLOOR_EXTRA = 283 +#: The floor of a new table's column: its cells take the new table's margins (510 left and right). +NEW_TABLE_COLUMN_FLOOR = sum( + int(_default_cell_inner_margin_attributes()[side]) for side in ("left", "right") +) + COLUMN_FLOOR_EXTRA + + +def new_table_width(width: object, cols: int) -> int | None: + """The width a new table of *cols* columns is written at for *width* (``None`` when not given): at least + :data:`NEW_TABLE_COLUMN_FLOOR` a column, as Hancom draws it. A width that is not an int in + ``0 < width < 2**31`` is refused.""" + + from ..errors import HwpxValueError + + if width is None: + return None + if isinstance(width, bool) or not isinstance(width, int) or not 0 < width < 2**31: + raise HwpxValueError( + f"width must be an int in 0 < width < 2**31 (HWPUNIT); got {width!r}", + code="table-width-value", + context={"value": repr(width)}, + suggestion="Pass the table's width in HWP units, or leave it out to fit the text width.", + ) + return max(width, cols * NEW_TABLE_COLUMN_FLOOR) + + +def _column_floors(table: "HwpxOxmlTable") -> list[int]: + """The narrowest Hancom draws each grid column: the widest left and right margins and + :data:`COLUMN_FLOOR_EXTRA` of its cells one column wide.""" + + floors = [0] * table.column_count + for entry in table.iter_grid(): + if entry.is_anchor and entry.span[1] == 1: + margins = cell_margins_of(entry.cell.element, table.element) + sides = 0 if margins is None else margins.left + margins.right + floors[entry.column] = max(floors[entry.column], sides + COLUMN_FLOOR_EXTRA) + return floors + def set_column_widths(table: "HwpxOxmlTable", weights: Sequence[int | float]) -> None: if len(weights) != table.column_count: @@ -61,6 +105,10 @@ def set_column_widths(table: "HwpxOxmlTable", weights: Sequence[int | float]) -> width = round(total_width * weight / weight_total) allocated += width column_widths.append(width) + # A column narrower than its floor is drawn at the floor and the table that much wider: written so too. + column_widths = [max(width, floor) for width, floor in zip(column_widths, _column_floors(table))] + if sz is not None and sum(column_widths) > total_width: + sz.set("width", str(sum(column_widths))) updated_cells: set[int] = set() for entry in table.iter_grid(): diff --git a/tests/fixtures/hancom_saved/table_column_floor_1302.hwpx b/tests/fixtures/hancom_saved/table_column_floor_1302.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..9783d04c206e66e5348839db45ac7fdd8d9b5623 GIT binary patch literal 7806 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 A&Hw-a literal 0 HcmV?d00001 diff --git a/tests/fixtures/hancom_saved/table_column_floor_1304.hwpx b/tests/fixtures/hancom_saved/table_column_floor_1304.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..1d95d2e0914f42cf685724447f0d9d3bd9737cfd GIT binary patch literal 7805 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 AH2?qr literal 0 HcmV?d00001 diff --git a/tests/fixtures/hancom_saved/table_column_floor_m0_282.hwpx b/tests/fixtures/hancom_saved/table_column_floor_m0_282.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..8afe610076b7ccef33d82dab33f709509cb68aab GIT binary patch literal 7813 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 H0Pz0+W*V~M literal 0 HcmV?d00001 diff --git a/tests/fixtures/hancom_saved/table_column_floor_m1000_2282.hwpx b/tests/fixtures/hancom_saved/table_column_floor_m1000_2282.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..e399c798cb5ec4ae2a352f30fc5f96f4202044e1 GIT binary patch literal 7831 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 OcKX45(oYy60Puf0J)vd* literal 0 HcmV?d00001 diff --git a/tests/fixtures/hancom_saved/table_width_zero.hwpx b/tests/fixtures/hancom_saved/table_width_zero.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..331b1314136298152252d3721a46d02868509bdd GIT binary patch literal 7838 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 literal 0 HcmV?d00001 diff --git a/tests/test_set_column_widths_uniform_split.py b/tests/test_set_column_widths_uniform_split.py index 20a9132f..1f4ef3bd 100644 --- a/tests/test_set_column_widths_uniform_split.py +++ b/tests/test_set_column_widths_uniform_split.py @@ -34,13 +34,14 @@ def test_uniform_weights_give_the_remainder_to_the_last_column() -> None: def test_uniform_weights_round_half_to_even() -> None: + # 2609 / 2 = 1304.5 rounds to 1304 (each column above its 1303 floor) document = HwpxDocument.new() - table = document.add_table(rows=1, cols=2, width=13) - assert _row_widths(table) == [7, 6] + table = document.add_table(rows=1, cols=2, width=2609) + assert _row_widths(table) == [1305, 1304] _uniform(table) - assert _row_widths(table) == [6, 7] + assert _row_widths(table) == [1304, 1305] def test_uniform_weights_give_a_merged_cell_the_sum_of_its_columns() -> None: @@ -56,9 +57,9 @@ def test_uniform_weights_give_a_merged_cell_the_sum_of_its_columns() -> None: def test_uniform_weights_fall_back_to_the_row_zero_sum_without_a_table_width() -> None: document = HwpxDocument.new() - table = document.add_table(rows=1, cols=2, width=13) + table = document.add_table(rows=1, cols=2, width=2609) table.element.find(f"{HP}sz").attrib.pop("width") _uniform(table) - assert _row_widths(table) == [6, 7] + assert _row_widths(table) == [1304, 1305] diff --git a/tests/test_table_column_floor.py b/tests/test_table_column_floor.py new file mode 100644 index 00000000..3d596fac --- /dev/null +++ b/tests/test_table_column_floor.py @@ -0,0 +1,119 @@ +"""New tables and ``set_column_widths`` write a column narrower than Hancom draws it at its floor. + +Hancom draws a table column at least its cells' left and right margins and 283 wide, keeps the narrower width +written in the cells, and saves the table as wide as it draws the columns. +``tests/fixtures/hancom_saved/table_column_floor_*.hwpx`` are its saves of two-column tables whose first column +is written just below or above the floor (cell margins 510 + 510, 0 + 0 and 1000 + 1000) and the second 20000 +wide; ``table_width_zero.hwpx`` of a 2x2 table written 0 wide, ``table_column_weights_zero_one.hwpx`` of a table +42520 wide given column weights 0 and 1. +""" + +from __future__ import annotations + +import zipfile +from pathlib import Path + +import pytest +from lxml import etree + +from hwpx import HwpxDocument +from hwpx.errors import HwpxValueError + +HP = "{http://www.hancom.co.kr/hwpml/2011/paragraph}" +FIXTURES = Path(__file__).parent / "fixtures" / "hancom_saved" + + +def _saved_table(name: str) -> etree._Element: + with zipfile.ZipFile(FIXTURES / name) as archive: + return next(etree.fromstring(archive.read("Contents/section0.xml")).iter(f"{HP}tbl")) + + +def _table_width(table: etree._Element) -> int: + return int(table.find(f"{HP}sz").get("width")) + + +def _sections(document: HwpxDocument) -> list[bytes]: + return [etree.tostring(section.element) for section in document.sections] + + +@pytest.mark.parametrize( + ("fixture", "written", "drawn"), + [ + ("table_column_floor_1302.hwpx", 1302, 1303), # the table's margins, 510 + 510 + ("table_column_floor_1304.hwpx", 1304, 1304), + ("table_column_floor_m0_282.hwpx", 282, 283), # the cells' own margins, 0 + 0 + ("table_column_floor_m1000_2282.hwpx", 2282, 2283), # 1000 + 1000 + ], +) +def test_hancom_draws_a_column_at_least_its_cell_margins_and_283_wide(fixture: str, written: int, drawn: int) -> None: + table = _saved_table(fixture) + + assert [int(size.get("width")) for size in table.iter(f"{HP}cellSz")] == [written, 20000] # kept + assert _table_width(table) == drawn + 20000 + + +def test_hancom_saves_a_table_as_wide_as_it_draws_its_columns() -> None: + assert _table_width(_saved_table("table_width_zero.hwpx")) == 2 * 1303 # written 0 wide + assert _table_width(_saved_table("table_column_weights_zero_one.hwpx")) == 1303 + 42520 # weights 0 and 1 + + +@pytest.mark.parametrize(("width", "written"), [(1, 2606), (2605, 2606), (2606, 2606), (2609, 2609)]) +def test_a_new_table_is_written_as_wide_as_hancom_draws_its_columns(width: int, written: int) -> None: + document = HwpxDocument.new() + + table = document.add_table(2, 2, width=width) + + assert _table_width(table.element) == written + assert sum(table.cell(0, column).width for column in range(2)) == written + assert min(table.cell(row, column).width for row in range(2) for column in range(2)) >= 1303 + + +def test_a_nested_table_in_a_narrow_cell_is_written_as_wide_as_hancom_draws_its_columns() -> None: + document = HwpxDocument.new() + outer = document.add_table(1, 2, width=2606) # 1303-wide cells: 283 inside their margins + + nested = outer.cell(0, 0).add_table(1, 2) + + assert _table_width(nested.element) == 2606 + + +@pytest.mark.parametrize("width", [0, -1, 2**31, 2606.0, True]) +def test_a_table_width_that_is_no_positive_int_is_refused_before_anything_is_added(width: object) -> None: + document = HwpxDocument.new() + outer = document.add_table(1, 1) + paragraph = document.add_paragraph("표 앞 글") + before = (len(document.paragraphs), _sections(document)) + + for add in ( + lambda: document.add_table(2, 2, width=width), + lambda: outer.cell(0, 0).add_table(2, 2, width=width), + lambda: paragraph.add_table(2, 2, width=width), + ): + with pytest.raises(HwpxValueError) as caught: + add() # type: ignore[no-untyped-call] + assert caught.value.code == "table-width-value" + + assert (len(document.paragraphs), _sections(document)) == before + + +def test_set_column_widths_writes_a_column_below_its_floor_at_the_floor_and_the_table_wider() -> None: + # As Hancom saved the same table and weights: 1303 and 42520 wide, 43823 in all. + document = HwpxDocument.new() + table = document.add_table(2, 2) + + table.set_column_widths([0, 1]) + + assert [[table.cell(row, column).width for column in range(2)] for row in range(2)] == [[1303, 42520]] * 2 + assert _table_width(table.element) == _table_width(_saved_table("table_column_weights_zero_one.hwpx")) + + +def test_a_column_takes_the_floor_of_its_own_cell_margins() -> None: + # As Hancom saved the table: cell margins 0 + 0, the column 282 written and 283 drawn, 20283 in all. + document = HwpxDocument.new() + table = document.add_table(1, 2, width=20282) + table.cell(0, 0).set_margins(left=0, right=0) + + table.set_column_widths([282, 20000]) + + assert [table.cell(0, column).width for column in range(2)] == [283, 20000] + assert _table_width(table.element) == _table_width(_saved_table("table_column_floor_m0_282.hwpx")) From 59be394d385dad3ed3dc6f9309a78b13897a7d05 Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:24:16 +0900 Subject: [PATCH 12/15] docs(changelog): note the fix Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 180f11a9..85549d21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,15 @@ 거부한다. - 쪽 수 추정(실험, `estimate_pages`)도 단 너비를 너비 × 본문 폭 / 32768로 센다. 전에는 너비들의 합으로 나눴다. 한/글이 만든 문서는 합이 32768이라 결과가 같다. +- 새 표(`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으로 썼다. - 쪽 수 추정(실험, `estimate_pages`)이 쪽 번호 조판 부호(`hp:ctrl/hp:pageNum`)와 줄보다 넓은 글자처럼 둔 표만 있는 문단을 표 높이의 한 줄로 세던 것을 고친다. - 한/글은 둘을 한 줄에 두지 않는다. 부호의 줄은 문단 글의 빈 줄이다. From d88818651c69a20746e722671e967937b1f1370e Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:27:20 +0900 Subject: [PATCH 13/15] test(errors): lower the census lock for the base unit check add_equation's base unit check now raises a typed error, so oxml/paragraph.py raises one untyped error fewer. The census lock records 202 untyped raises (paragraph.py 18), as the ratchet's self-test expects the lock to match the tree. Co-Authored-By: Claude Opus 5.5 --- tests/data/error_census.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/error_census.json b/tests/data/error_census.json index 3a5e94ad..27a3285c 100644 --- a/tests/data/error_census.json +++ b/tests/data/error_census.json @@ -479,7 +479,7 @@ "observed": { "other": 215, "typed": 216, - "untyped": 203, + "untyped": 202, "untypedByFile": { "body_patch.py": 20, "equation/mathml.py": 1, @@ -494,7 +494,7 @@ "oxml/header_part.py": 16, "oxml/memo.py": 1, "oxml/numbering_kinds.py": 1, - "oxml/paragraph.py": 19, + "oxml/paragraph.py": 18, "oxml/parser.py": 2, "oxml/run.py": 2, "oxml/section.py": 1, From 44bbd3f5b3d2790e85b196ccb906fcfdea5df757 Mon Sep 17 00:00:00 2001 From: airmang <38392618+airmang@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:31:28 +0900 Subject: [PATCH 14/15] fix(table): type new_table_width by its argument new_table_width returns None only when no width is given. Overloads now say so, so the width HwpxOxmlTable.create builds the table with is an int for mypy and pyright. Co-Authored-By: Claude Opus 5.5 --- src/hwpx/oxml/table_sizes.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/hwpx/oxml/table_sizes.py b/src/hwpx/oxml/table_sizes.py index 3ae038d3..28916283 100644 --- a/src/hwpx/oxml/table_sizes.py +++ b/src/hwpx/oxml/table_sizes.py @@ -12,7 +12,7 @@ from dataclasses import replace from math import lcm -from typing import TYPE_CHECKING, Any, Iterator, Mapping, Sequence +from typing import TYPE_CHECKING, Any, Iterator, Mapping, Sequence, overload from ..objects.results import CellMargins from ._document_primitives import ( @@ -51,6 +51,10 @@ ) + COLUMN_FLOOR_EXTRA +@overload +def new_table_width(width: None, cols: int) -> None: ... +@overload +def new_table_width(width: int, cols: int) -> int: ... def new_table_width(width: object, cols: int) -> int | None: """The width a new table of *cols* columns is written at for *width* (``None`` when not given): at least :data:`NEW_TABLE_COLUMN_FLOOR` a column, as Hancom draws it. A width that is not an int in From 879043730b15e5b12f6b8b8451af7ef01fa6d591 Mon Sep 17 00:00:00 2001 From: kokyuhyun Date: Wed, 7 Oct 2026 08:48:46 +0900 Subject: [PATCH 15/15] fix: validate picture units before coercion Refs #435. --- CHANGELOG.md | 1 + src/hwpx/_document/media.py | 3 ++- src/hwpx/oxml/paragraph.py | 4 ++-- tests/test_shape_size_values.py | 19 +++++++++++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a5af1e..e42e3676 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ 바꾸고 문단을 더하기 전에 낸다. 2**31 이상도 거부한다. 아주 큰 `base_unit`으로 잰 상자가 2**31 이상이면 `shape-size-value`로 거부한다. 한/글은 그런 상자를 2**32 - 1로 줄여 저장한다. - `HwpxOxmlParagraph.add_picture`는 그림 요소를 만든 뒤에 런을 더한다. 값이 거부되면 빈 런이 남지 않는다. + - 그림의 HWPUNIT 크기는 정수 변환 전에 검사한다. bool·소수·문자열을 정수로 바꾸어 받아들이지 않는다. - 쪽 수 추정(실험, `estimate_pages`)이 쪽 번호 조판 부호(`hp:ctrl/hp:pageNum`)와 줄보다 넓은 글자처럼 둔 표만 있는 문단을 표 높이의 한 줄로 세던 것을 고친다. - 한/글은 둘을 한 줄에 두지 않는다. 부호의 줄은 문단 글의 빈 줄이다. diff --git a/src/hwpx/_document/media.py b/src/hwpx/_document/media.py index 20be1c9c..541615e4 100644 --- a/src/hwpx/_document/media.py +++ b/src/hwpx/_document/media.py @@ -119,6 +119,7 @@ def add_picture( resolved_width = width if resolved_width is None: resolved_width = _mm_to_hwp_units(width_mm) if width_mm is not None else 14400 + validate_shape_size(resolved_width, 0) # validate before aspect-ratio arithmetic too resolved_height = height if resolved_height is None: @@ -131,7 +132,7 @@ def add_picture( resolved_height = round(resolved_width * source_height / source_width) else: resolved_height = resolved_width - validate_shape_size(int(resolved_width), int(resolved_height)) # before anything is stored too + validate_shape_size(resolved_width, resolved_height) # before anything is stored too # Call the local primitive directly rather than `doc.add_image` — that # facade name moved in 6.0 (design table row 33), and going through it diff --git a/src/hwpx/oxml/paragraph.py b/src/hwpx/oxml/paragraph.py index a37d73ed..bfa6c3bb 100644 --- a/src/hwpx/oxml/paragraph.py +++ b/src/hwpx/oxml/paragraph.py @@ -679,8 +679,8 @@ def add_picture( element = _create_picture_element( # checked before its run is added str(binary_item_id_ref), - int(width), - int(height), + width, + height, align=align, treat_as_char=treat_as_char, pos_overrides=pos_overrides, diff --git a/tests/test_shape_size_values.py b/tests/test_shape_size_values.py index 78be42ec..4464fd2a 100644 --- a/tests/test_shape_size_values.py +++ b/tests/test_shape_size_values.py @@ -103,6 +103,25 @@ def test_a_picture_size_hancom_does_not_keep_is_refused_before_the_image_is_stor assert _state(document) == before +@pytest.mark.parametrize("value", [True, False, -0.5, 14400.5, "14400"]) +@pytest.mark.parametrize("dimension", ["width", "height"]) +@pytest.mark.parametrize("direct", [False, True]) +def test_picture_units_are_validated_without_coercion(value: object, dimension: str, direct: bool) -> None: + document = HwpxDocument.new() + paragraph = document.paragraphs[0] + before = _state(document) + sizes = {dimension: value} + + with pytest.raises(HwpxValueError) as caught: + if direct: + paragraph.add_picture("image1", **sizes) # type: ignore[arg-type] + else: + document.add_picture(PNG_1X1, "png", **sizes) # type: ignore[arg-type] + + assert caught.value.code == "shape-size-value" + assert _state(document) == before + + @pytest.mark.parametrize("ratio", [-1, 2**31, 12.5, True]) def test_a_corner_ratio_hancom_does_not_keep_is_refused_before_anything_is_added(ratio: object) -> None: document = HwpxDocument.new()