python-hwpx 1.4__tar.gz → 1.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python-hwpx-1.4/src/python_hwpx.egg-info → python_hwpx-1.7}/PKG-INFO +41 -3
- {python-hwpx-1.4 → python_hwpx-1.7}/README.md +6 -1
- {python-hwpx-1.4 → python_hwpx-1.7}/pyproject.toml +3 -4
- python_hwpx-1.7/src/hwpx/data/Skeleton.hwpx +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/document.py +18 -2
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/oxml/document.py +206 -3
- {python-hwpx-1.4 → python_hwpx-1.7/src/python_hwpx.egg-info}/PKG-INFO +41 -3
- {python-hwpx-1.4 → python_hwpx-1.7}/src/python_hwpx.egg-info/SOURCES.txt +1 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/tests/test_document_formatting.py +25 -0
- python_hwpx-1.7/tests/test_tables_default_border.py +81 -0
- python-hwpx-1.4/src/hwpx/data/Skeleton.hwpx +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/LICENSE +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/setup.cfg +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/__init__.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/opc/package.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/oxml/__init__.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/oxml/body.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/oxml/common.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/oxml/header.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/oxml/parser.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/oxml/schema.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/oxml/utils.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/package.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/templates.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/tools/__init__.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/tools/object_finder.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/tools/text_extractor.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/hwpx/tools/validator.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/tests/test_inline_models.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/tests/test_memo_and_style_editing.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/tests/test_oxml_parsing.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/tests/test_section_headers.py +0 -0
- {python-hwpx-1.4 → python_hwpx-1.7}/tests/test_text_extractor_annotations.py +0 -0
|
@@ -1,9 +1,41 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hwpx
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7
|
|
4
4
|
Summary: Hancom HWPX 패키지를 로드하고 편집하기 위한 Python 유틸리티 모음
|
|
5
5
|
Author: python-hwpx Maintainers
|
|
6
6
|
License: Non-Commercial License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 python-hwpx Maintainers
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to use,
|
|
12
|
+
copy, modify, merge, publish, distribute, and sublicense the Software only for
|
|
13
|
+
non-commercial purposes, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
1. Non-Commercial Use Only. The Software may be used, copied, modified,
|
|
16
|
+
merged, published, distributed, and sublicensed only for non-commercial
|
|
17
|
+
purposes. "Non-Commercial" means use that is not primarily intended for or
|
|
18
|
+
directed toward commercial advantage, monetary compensation, or any form of
|
|
19
|
+
direct or indirect commercial exploitation.
|
|
20
|
+
|
|
21
|
+
2. Attribution. The above copyright notice and this permission notice shall be
|
|
22
|
+
included in all copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
3. No Warranty of Commercial Support. The maintainers are not obligated to
|
|
25
|
+
provide commercial support, maintenance, or updates.
|
|
26
|
+
|
|
27
|
+
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
28
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
29
|
+
FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
|
30
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
31
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
32
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
33
|
+
SOFTWARE.
|
|
34
|
+
|
|
35
|
+
If you require permissions to use this Software for commercial purposes,
|
|
36
|
+
please contact the copyright holders to negotiate an alternative licensing
|
|
37
|
+
arrangement.
|
|
38
|
+
|
|
7
39
|
Project-URL: Homepage, https://github.com/airmang/python-hwpx
|
|
8
40
|
Project-URL: Documentation, https://github.com/airmang/python-hwpx/tree/main/docs
|
|
9
41
|
Project-URL: Issues, https://github.com/airmang/python-hwpx/issues
|
|
@@ -27,6 +59,7 @@ Requires-Dist: twine>=4.0; extra == "dev"
|
|
|
27
59
|
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
28
60
|
Provides-Extra: test
|
|
29
61
|
Requires-Dist: pytest>=7.4; extra == "test"
|
|
62
|
+
Dynamic: license-file
|
|
30
63
|
|
|
31
64
|
# python-hwpx
|
|
32
65
|
|
|
@@ -38,7 +71,7 @@ Requires-Dist: pytest>=7.4; extra == "test"
|
|
|
38
71
|
- **문서 편집 API** – `hwpx.document.HwpxDocument`는 문단과 표, 메모, 헤더 속성을 파이썬 객체로 노출하고 새 콘텐츠를 손쉽게 추가합니다. 섹션 머리말·꼬리말을 수정하면 `<hp:headerApply>`/`<hp:footerApply>`와 마스터 페이지 링크도 함께 갱신합니다.
|
|
39
72
|
- **타입이 지정된 본문 모델** – `hwpx.oxml.body`는 표·컨트롤·인라인 도형·변경 추적 태그를 데이터 클래스에 매핑하고, `HwpxOxmlParagraph.model`/`HwpxOxmlRun.model`로 이를 조회·수정한 뒤 XML로 되돌릴 수 있도록 지원합니다.
|
|
40
73
|
- **메모와 필드 앵커** – `add_memo_with_anchor()`로 메모를 생성하면서 MEMO 필드 컨트롤을 자동 삽입해 한/글에서 바로 표시되도록 합니다.
|
|
41
|
-
- **헤더 참조 목록 탐색** – 글머리표, 문단 속성, 스타일, 변경 추적 항목, 작성자 정보를 데이터클래스로 파싱하고 `document.bullets`·`document.styles` 같은 조회 헬퍼로 ID 기반 검색을 단순화했습니다.
|
|
74
|
+
- **헤더 참조 목록 탐색** – 글머리표, 문단 속성, 테두리 채우기, 스타일, 변경 추적 항목, 작성자 정보를 데이터클래스로 파싱하고 `document.border_fills`·`document.bullets`·`document.styles` 같은 조회 헬퍼로 ID 기반 검색을 단순화했습니다.
|
|
42
75
|
- **바탕쪽·이력·버전 파트 제어** – 매니페스트에 포함된 master-page/history/version 파트를 `document.master_pages`, `document.histories`, `document.version`으로 직접 편집하고 저장합니다.
|
|
43
76
|
- **스타일 기반 텍스트 치환** – 런 서식(색상, 밑줄, `charPrIDRef`)으로 필터링해 텍스트를 선택적으로 교체하거나 삭제합니다. 하이라이트
|
|
44
77
|
마커나 태그로 분리된 문자열도 서식을 유지한 채 치환합니다.
|
|
@@ -79,6 +112,7 @@ print("sections:", len(document.sections))
|
|
|
79
112
|
# 2) 문단과 표, 메모 추가
|
|
80
113
|
section = document.sections[0]
|
|
81
114
|
paragraph = document.add_paragraph("자동 생성한 문단", section=section)
|
|
115
|
+
# 표에 사용할 기본 실선 테두리 채우기가 없으면 add_table()이 자동으로 생성합니다.
|
|
82
116
|
table = document.add_table(rows=2, cols=2, section=section)
|
|
83
117
|
table.set_cell_text(0, 0, "항목")
|
|
84
118
|
table.set_cell_text(0, 1, "값")
|
|
@@ -90,6 +124,10 @@ document.add_memo_with_anchor("배포 전 검토", paragraph=paragraph, memo_sha
|
|
|
90
124
|
document.save("output/example.hwpx")
|
|
91
125
|
```
|
|
92
126
|
|
|
127
|
+
`HwpxDocument.add_table()`은 문서에 정의된 테두리 채우기가 없으면 헤더 참조 목록에 "기본 실선" `borderFill`을 만들어 표와 모든 셀에 참조를 연결합니다.
|
|
128
|
+
|
|
129
|
+
표 셀 텍스트를 편집하는 `table.set_cell_text()`는 기존 단락에 남아 있는 `lineSegArray`와 같은 줄 배치 캐시를 제거하여 한/글이 문서를 다시 열 때 줄바꿈을 새로 계산하도록 합니다.
|
|
130
|
+
|
|
93
131
|
더 많은 실전 패턴은 [빠른 시작](docs/quickstart.md)과 [사용 가이드](docs/usage.md)의 "빠른 예제 모음"에서 확인할 수 있습니다.
|
|
94
132
|
|
|
95
133
|
## 문서
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- **문서 편집 API** – `hwpx.document.HwpxDocument`는 문단과 표, 메모, 헤더 속성을 파이썬 객체로 노출하고 새 콘텐츠를 손쉽게 추가합니다. 섹션 머리말·꼬리말을 수정하면 `<hp:headerApply>`/`<hp:footerApply>`와 마스터 페이지 링크도 함께 갱신합니다.
|
|
9
9
|
- **타입이 지정된 본문 모델** – `hwpx.oxml.body`는 표·컨트롤·인라인 도형·변경 추적 태그를 데이터 클래스에 매핑하고, `HwpxOxmlParagraph.model`/`HwpxOxmlRun.model`로 이를 조회·수정한 뒤 XML로 되돌릴 수 있도록 지원합니다.
|
|
10
10
|
- **메모와 필드 앵커** – `add_memo_with_anchor()`로 메모를 생성하면서 MEMO 필드 컨트롤을 자동 삽입해 한/글에서 바로 표시되도록 합니다.
|
|
11
|
-
- **헤더 참조 목록 탐색** – 글머리표, 문단 속성, 스타일, 변경 추적 항목, 작성자 정보를 데이터클래스로 파싱하고 `document.bullets`·`document.styles` 같은 조회 헬퍼로 ID 기반 검색을 단순화했습니다.
|
|
11
|
+
- **헤더 참조 목록 탐색** – 글머리표, 문단 속성, 테두리 채우기, 스타일, 변경 추적 항목, 작성자 정보를 데이터클래스로 파싱하고 `document.border_fills`·`document.bullets`·`document.styles` 같은 조회 헬퍼로 ID 기반 검색을 단순화했습니다.
|
|
12
12
|
- **바탕쪽·이력·버전 파트 제어** – 매니페스트에 포함된 master-page/history/version 파트를 `document.master_pages`, `document.histories`, `document.version`으로 직접 편집하고 저장합니다.
|
|
13
13
|
- **스타일 기반 텍스트 치환** – 런 서식(색상, 밑줄, `charPrIDRef`)으로 필터링해 텍스트를 선택적으로 교체하거나 삭제합니다. 하이라이트
|
|
14
14
|
마커나 태그로 분리된 문자열도 서식을 유지한 채 치환합니다.
|
|
@@ -49,6 +49,7 @@ print("sections:", len(document.sections))
|
|
|
49
49
|
# 2) 문단과 표, 메모 추가
|
|
50
50
|
section = document.sections[0]
|
|
51
51
|
paragraph = document.add_paragraph("자동 생성한 문단", section=section)
|
|
52
|
+
# 표에 사용할 기본 실선 테두리 채우기가 없으면 add_table()이 자동으로 생성합니다.
|
|
52
53
|
table = document.add_table(rows=2, cols=2, section=section)
|
|
53
54
|
table.set_cell_text(0, 0, "항목")
|
|
54
55
|
table.set_cell_text(0, 1, "값")
|
|
@@ -60,6 +61,10 @@ document.add_memo_with_anchor("배포 전 검토", paragraph=paragraph, memo_sha
|
|
|
60
61
|
document.save("output/example.hwpx")
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
`HwpxDocument.add_table()`은 문서에 정의된 테두리 채우기가 없으면 헤더 참조 목록에 "기본 실선" `borderFill`을 만들어 표와 모든 셀에 참조를 연결합니다.
|
|
65
|
+
|
|
66
|
+
표 셀 텍스트를 편집하는 `table.set_cell_text()`는 기존 단락에 남아 있는 `lineSegArray`와 같은 줄 배치 캐시를 제거하여 한/글이 문서를 다시 열 때 줄바꿈을 새로 계산하도록 합니다.
|
|
67
|
+
|
|
63
68
|
더 많은 실전 패턴은 [빠른 시작](docs/quickstart.md)과 [사용 가이드](docs/usage.md)의 "빠른 예제 모음"에서 확인할 수 있습니다.
|
|
64
69
|
|
|
65
70
|
## 문서
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-hwpx"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.7"
|
|
8
8
|
description = "Hancom HWPX 패키지를 로드하고 편집하기 위한 Python 유틸리티 모음"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
-
license = {
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
12
|
authors = [
|
|
13
13
|
{ name = "python-hwpx Maintainers" },
|
|
@@ -49,7 +49,6 @@ hwpx-validate = "hwpx.tools.validator:main"
|
|
|
49
49
|
[tool.setuptools]
|
|
50
50
|
package-dir = { "" = "src" }
|
|
51
51
|
include-package-data = true
|
|
52
|
-
license-files = ["LICENSE"]
|
|
53
52
|
|
|
54
53
|
[tool.setuptools.packages.find]
|
|
55
54
|
where = ["src"]
|
|
Binary file
|
|
@@ -11,6 +11,7 @@ from typing import BinaryIO, Iterator, List, Tuple
|
|
|
11
11
|
|
|
12
12
|
from .oxml import (
|
|
13
13
|
Bullet,
|
|
14
|
+
GenericElement,
|
|
14
15
|
HwpxOxmlDocument,
|
|
15
16
|
HwpxOxmlHeader,
|
|
16
17
|
HwpxOxmlHistory,
|
|
@@ -107,6 +108,17 @@ class HwpxDocument:
|
|
|
107
108
|
"""Return the version metadata part if present."""
|
|
108
109
|
return self._root.version
|
|
109
110
|
|
|
111
|
+
@property
|
|
112
|
+
def border_fills(self) -> dict[str, GenericElement]:
|
|
113
|
+
"""Return border fill definitions declared in the headers."""
|
|
114
|
+
|
|
115
|
+
return self._root.border_fills
|
|
116
|
+
|
|
117
|
+
def border_fill(self, border_fill_id_ref: int | str | None) -> GenericElement | None:
|
|
118
|
+
"""Return the border fill definition referenced by *border_fill_id_ref*."""
|
|
119
|
+
|
|
120
|
+
return self._root.border_fill(border_fill_id_ref)
|
|
121
|
+
|
|
110
122
|
@property
|
|
111
123
|
def memo_shapes(self) -> dict[str, MemoShape]:
|
|
112
124
|
"""Return memo shapes available in the header reference lists."""
|
|
@@ -529,7 +541,7 @@ class HwpxDocument:
|
|
|
529
541
|
section_index: int | None = None,
|
|
530
542
|
width: int | None = None,
|
|
531
543
|
height: int | None = None,
|
|
532
|
-
border_fill_id_ref: str | int =
|
|
544
|
+
border_fill_id_ref: str | int | None = None,
|
|
533
545
|
para_pr_id_ref: str | int | None = None,
|
|
534
546
|
style_id_ref: str | int | None = None,
|
|
535
547
|
char_pr_id_ref: str | int | None = None,
|
|
@@ -538,6 +550,10 @@ class HwpxDocument:
|
|
|
538
550
|
) -> HwpxOxmlTable:
|
|
539
551
|
"""Create a table in a new paragraph and return it."""
|
|
540
552
|
|
|
553
|
+
resolved_border_fill: str | int | None = border_fill_id_ref
|
|
554
|
+
if resolved_border_fill is None:
|
|
555
|
+
resolved_border_fill = self._root.ensure_basic_border_fill()
|
|
556
|
+
|
|
541
557
|
paragraph = self.add_paragraph(
|
|
542
558
|
"",
|
|
543
559
|
section=section,
|
|
@@ -553,7 +569,7 @@ class HwpxDocument:
|
|
|
553
569
|
cols,
|
|
554
570
|
width=width,
|
|
555
571
|
height=height,
|
|
556
|
-
border_fill_id_ref=
|
|
572
|
+
border_fill_id_ref=resolved_border_fill,
|
|
557
573
|
run_attributes=run_attributes,
|
|
558
574
|
char_pr_id_ref=char_pr_id_ref,
|
|
559
575
|
)
|
|
@@ -11,6 +11,7 @@ import xml.etree.ElementTree as ET
|
|
|
11
11
|
from lxml import etree as LET
|
|
12
12
|
|
|
13
13
|
from . import body
|
|
14
|
+
from .common import GenericElement
|
|
14
15
|
from .header import (
|
|
15
16
|
Bullet,
|
|
16
17
|
MemoProperties,
|
|
@@ -21,6 +22,7 @@ from .header import (
|
|
|
21
22
|
TrackChangeAuthor,
|
|
22
23
|
memo_shape_from_attributes,
|
|
23
24
|
parse_bullets,
|
|
25
|
+
parse_border_fills,
|
|
24
26
|
parse_paragraph_properties,
|
|
25
27
|
parse_styles,
|
|
26
28
|
parse_track_change_authors,
|
|
@@ -44,6 +46,23 @@ _DEFAULT_PARAGRAPH_ATTRS = {
|
|
|
44
46
|
_DEFAULT_CELL_WIDTH = 7200
|
|
45
47
|
_DEFAULT_CELL_HEIGHT = 3600
|
|
46
48
|
|
|
49
|
+
_BASIC_BORDER_FILL_ATTRIBUTES = {
|
|
50
|
+
"threeD": "0",
|
|
51
|
+
"shadow": "0",
|
|
52
|
+
"centerLine": "NONE",
|
|
53
|
+
"breakCellSeparateLine": "0",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_BASIC_BORDER_CHILDREN: Tuple[Tuple[str, dict[str, str]], ...] = (
|
|
57
|
+
("slash", {"type": "NONE", "Crooked": "0", "isCounter": "0"}),
|
|
58
|
+
("backSlash", {"type": "NONE", "Crooked": "0", "isCounter": "0"}),
|
|
59
|
+
("leftBorder", {"type": "SOLID", "width": "0.12 mm", "color": "#000000"}),
|
|
60
|
+
("rightBorder", {"type": "SOLID", "width": "0.12 mm", "color": "#000000"}),
|
|
61
|
+
("topBorder", {"type": "SOLID", "width": "0.12 mm", "color": "#000000"}),
|
|
62
|
+
("bottomBorder", {"type": "SOLID", "width": "0.12 mm", "color": "#000000"}),
|
|
63
|
+
("diagonal", {"type": "SOLID", "width": "0.1 mm", "color": "#000000"}),
|
|
64
|
+
)
|
|
65
|
+
|
|
47
66
|
T = TypeVar("T")
|
|
48
67
|
|
|
49
68
|
|
|
@@ -100,6 +119,17 @@ def _create_paragraph_element(
|
|
|
100
119
|
return paragraph
|
|
101
120
|
|
|
102
121
|
|
|
122
|
+
_LAYOUT_CACHE_ELEMENT_NAMES = {"lineSegArray"}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _clear_paragraph_layout_cache(paragraph: ET.Element) -> None:
|
|
126
|
+
"""Remove cached layout metadata such as ``<hp:lineSegArray>``."""
|
|
127
|
+
|
|
128
|
+
for child in list(paragraph):
|
|
129
|
+
if _element_local_name(child) in _LAYOUT_CACHE_ELEMENT_NAMES:
|
|
130
|
+
paragraph.remove(child)
|
|
131
|
+
|
|
132
|
+
|
|
103
133
|
def _element_local_name(node: ET.Element) -> str:
|
|
104
134
|
tag = node.tag
|
|
105
135
|
if "}" in tag:
|
|
@@ -107,6 +137,59 @@ def _element_local_name(node: ET.Element) -> str:
|
|
|
107
137
|
return tag
|
|
108
138
|
|
|
109
139
|
|
|
140
|
+
def _normalize_length(value: str | None) -> str:
|
|
141
|
+
if value is None:
|
|
142
|
+
return ""
|
|
143
|
+
return value.replace(" ", "").lower()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _border_fill_is_basic_solid_line(element: ET.Element) -> bool:
|
|
147
|
+
if _element_local_name(element) != "borderFill":
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
for attr, expected in _BASIC_BORDER_FILL_ATTRIBUTES.items():
|
|
151
|
+
actual = element.get(attr)
|
|
152
|
+
if attr == "centerLine":
|
|
153
|
+
if (actual or "").upper() != expected:
|
|
154
|
+
return False
|
|
155
|
+
else:
|
|
156
|
+
if actual != expected:
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
for child_name, child_attrs in _BASIC_BORDER_CHILDREN:
|
|
160
|
+
child = element.find(f"{_HH}{child_name}")
|
|
161
|
+
if child is None:
|
|
162
|
+
return False
|
|
163
|
+
for attr, expected in child_attrs.items():
|
|
164
|
+
actual = child.get(attr)
|
|
165
|
+
if attr == "type":
|
|
166
|
+
if (actual or "").upper() != expected:
|
|
167
|
+
return False
|
|
168
|
+
elif attr == "width":
|
|
169
|
+
if _normalize_length(actual) != _normalize_length(expected):
|
|
170
|
+
return False
|
|
171
|
+
elif attr == "color":
|
|
172
|
+
if (actual or "").upper() != expected.upper():
|
|
173
|
+
return False
|
|
174
|
+
else:
|
|
175
|
+
if actual != expected:
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
for child in element:
|
|
179
|
+
if _element_local_name(child) == "fillBrush":
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _create_basic_border_fill_element(border_id: str) -> ET.Element:
|
|
186
|
+
attrs = {"id": border_id, **_BASIC_BORDER_FILL_ATTRIBUTES}
|
|
187
|
+
element = ET.Element(f"{_HH}borderFill", attrs)
|
|
188
|
+
for child_name, child_attrs in _BASIC_BORDER_CHILDREN:
|
|
189
|
+
ET.SubElement(element, f"{_HH}{child_name}", dict(child_attrs))
|
|
190
|
+
return element
|
|
191
|
+
|
|
192
|
+
|
|
110
193
|
def _distribute_size(total: int, parts: int) -> List[int]:
|
|
111
194
|
"""Return *parts* integers that sum to *total* and are as even as possible."""
|
|
112
195
|
|
|
@@ -1429,6 +1512,7 @@ class HwpxOxmlTableCell:
|
|
|
1429
1512
|
paragraph = sublist.find(f"{_HP}p")
|
|
1430
1513
|
if paragraph is None:
|
|
1431
1514
|
paragraph = ET.SubElement(sublist, f"{_HP}p", _default_cell_paragraph_attributes())
|
|
1515
|
+
_clear_paragraph_layout_cache(paragraph)
|
|
1432
1516
|
run = paragraph.find(f"{_HP}run")
|
|
1433
1517
|
if run is None:
|
|
1434
1518
|
run = ET.SubElement(paragraph, f"{_HP}run", {"charPrIDRef": "0"})
|
|
@@ -1525,13 +1609,15 @@ class HwpxOxmlTable:
|
|
|
1525
1609
|
*,
|
|
1526
1610
|
width: int | None = None,
|
|
1527
1611
|
height: int | None = None,
|
|
1528
|
-
border_fill_id_ref: str | int =
|
|
1612
|
+
border_fill_id_ref: str | int | None = None,
|
|
1529
1613
|
) -> ET.Element:
|
|
1530
1614
|
if rows <= 0 or cols <= 0:
|
|
1531
1615
|
raise ValueError("rows and cols must be positive integers")
|
|
1532
1616
|
|
|
1533
1617
|
table_width = width if width is not None else cols * _DEFAULT_CELL_WIDTH
|
|
1534
1618
|
table_height = height if height is not None else rows * _DEFAULT_CELL_HEIGHT
|
|
1619
|
+
if border_fill_id_ref is None:
|
|
1620
|
+
raise ValueError("border_fill_id_ref must be provided")
|
|
1535
1621
|
border_fill = str(border_fill_id_ref)
|
|
1536
1622
|
|
|
1537
1623
|
table_attrs = {
|
|
@@ -1864,10 +1950,19 @@ class HwpxOxmlParagraph:
|
|
|
1864
1950
|
*,
|
|
1865
1951
|
width: int | None = None,
|
|
1866
1952
|
height: int | None = None,
|
|
1867
|
-
border_fill_id_ref: str | int =
|
|
1953
|
+
border_fill_id_ref: str | int | None = None,
|
|
1868
1954
|
run_attributes: dict[str, str] | None = None,
|
|
1869
1955
|
char_pr_id_ref: str | int | None = None,
|
|
1870
1956
|
) -> HwpxOxmlTable:
|
|
1957
|
+
if border_fill_id_ref is None:
|
|
1958
|
+
document = self.section.document
|
|
1959
|
+
if document is not None:
|
|
1960
|
+
resolved_border_fill: str | int = document.ensure_basic_border_fill()
|
|
1961
|
+
else:
|
|
1962
|
+
resolved_border_fill = "0"
|
|
1963
|
+
else:
|
|
1964
|
+
resolved_border_fill = border_fill_id_ref
|
|
1965
|
+
|
|
1871
1966
|
run = self._create_run_for_object(
|
|
1872
1967
|
run_attributes,
|
|
1873
1968
|
char_pr_id_ref=char_pr_id_ref,
|
|
@@ -1877,7 +1972,7 @@ class HwpxOxmlParagraph:
|
|
|
1877
1972
|
cols,
|
|
1878
1973
|
width=width,
|
|
1879
1974
|
height=height,
|
|
1880
|
-
border_fill_id_ref=
|
|
1975
|
+
border_fill_id_ref=resolved_border_fill,
|
|
1881
1976
|
)
|
|
1882
1977
|
run.append(table_element)
|
|
1883
1978
|
self.section.mark_dirty()
|
|
@@ -2253,6 +2348,16 @@ class HwpxOxmlHeader:
|
|
|
2253
2348
|
self.mark_dirty()
|
|
2254
2349
|
return element
|
|
2255
2350
|
|
|
2351
|
+
def _border_fills_element(self, create: bool = False) -> ET.Element | None:
|
|
2352
|
+
ref_list = self._ref_list_element(create=create)
|
|
2353
|
+
if ref_list is None:
|
|
2354
|
+
return None
|
|
2355
|
+
element = ref_list.find(f"{_HH}borderFills")
|
|
2356
|
+
if element is None and create:
|
|
2357
|
+
element = ET.SubElement(ref_list, f"{_HH}borderFills", {"itemCnt": "0"})
|
|
2358
|
+
self.mark_dirty()
|
|
2359
|
+
return element
|
|
2360
|
+
|
|
2256
2361
|
def _char_properties_element(self, create: bool = False) -> ET.Element | None:
|
|
2257
2362
|
ref_list = self._ref_list_element(create=create)
|
|
2258
2363
|
if ref_list is None:
|
|
@@ -2267,6 +2372,10 @@ class HwpxOxmlHeader:
|
|
|
2267
2372
|
count = len(list(element.findall(f"{_HH}charPr")))
|
|
2268
2373
|
element.set("itemCnt", str(count))
|
|
2269
2374
|
|
|
2375
|
+
def _update_border_fills_item_count(self, element: ET.Element) -> None:
|
|
2376
|
+
count = len(list(element.findall(f"{_HH}borderFill")))
|
|
2377
|
+
element.set("itemCnt", str(count))
|
|
2378
|
+
|
|
2270
2379
|
def _allocate_char_property_id(
|
|
2271
2380
|
self,
|
|
2272
2381
|
element: ET.Element,
|
|
@@ -2297,6 +2406,26 @@ class HwpxOxmlHeader:
|
|
|
2297
2406
|
candidate = str(next_id)
|
|
2298
2407
|
return candidate
|
|
2299
2408
|
|
|
2409
|
+
def _allocate_border_fill_id(self, element: ET.Element) -> str:
|
|
2410
|
+
existing: set[str] = {
|
|
2411
|
+
child.get("id") or ""
|
|
2412
|
+
for child in element.findall(f"{_HH}borderFill")
|
|
2413
|
+
}
|
|
2414
|
+
existing.discard("")
|
|
2415
|
+
|
|
2416
|
+
numeric_ids: List[int] = []
|
|
2417
|
+
for value in existing:
|
|
2418
|
+
try:
|
|
2419
|
+
numeric_ids.append(int(value))
|
|
2420
|
+
except ValueError:
|
|
2421
|
+
continue
|
|
2422
|
+
next_id = 0 if not numeric_ids else max(numeric_ids) + 1
|
|
2423
|
+
candidate = str(next_id)
|
|
2424
|
+
while candidate in existing:
|
|
2425
|
+
next_id += 1
|
|
2426
|
+
candidate = str(next_id)
|
|
2427
|
+
return candidate
|
|
2428
|
+
|
|
2300
2429
|
def ensure_char_property(
|
|
2301
2430
|
self,
|
|
2302
2431
|
*,
|
|
@@ -2386,6 +2515,59 @@ class HwpxOxmlHeader:
|
|
|
2386
2515
|
return None
|
|
2387
2516
|
return ref_list.find(f"{_HH}trackChangeAuthors")
|
|
2388
2517
|
|
|
2518
|
+
def find_basic_border_fill_id(self) -> str | None:
|
|
2519
|
+
element = self._border_fills_element()
|
|
2520
|
+
if element is None:
|
|
2521
|
+
return None
|
|
2522
|
+
for child in element.findall(f"{_HH}borderFill"):
|
|
2523
|
+
if _border_fill_is_basic_solid_line(child):
|
|
2524
|
+
identifier = child.get("id")
|
|
2525
|
+
if identifier:
|
|
2526
|
+
return identifier
|
|
2527
|
+
return None
|
|
2528
|
+
|
|
2529
|
+
def ensure_basic_border_fill(self) -> str:
|
|
2530
|
+
element = self._border_fills_element(create=True)
|
|
2531
|
+
if element is None: # pragma: no cover - defensive branch
|
|
2532
|
+
raise RuntimeError("failed to create <borderFills> element")
|
|
2533
|
+
|
|
2534
|
+
existing = self.find_basic_border_fill_id()
|
|
2535
|
+
if existing is not None:
|
|
2536
|
+
return existing
|
|
2537
|
+
|
|
2538
|
+
new_id = self._allocate_border_fill_id(element)
|
|
2539
|
+
element.append(_create_basic_border_fill_element(new_id))
|
|
2540
|
+
self._update_border_fills_item_count(element)
|
|
2541
|
+
self.mark_dirty()
|
|
2542
|
+
return new_id
|
|
2543
|
+
|
|
2544
|
+
@property
|
|
2545
|
+
def border_fills(self) -> dict[str, GenericElement]:
|
|
2546
|
+
element = self._border_fills_element()
|
|
2547
|
+
if element is None:
|
|
2548
|
+
return {}
|
|
2549
|
+
|
|
2550
|
+
fill_list = parse_border_fills(self._convert_to_lxml(element))
|
|
2551
|
+
mapping: dict[str, GenericElement] = {}
|
|
2552
|
+
for border_fill in fill_list.fills:
|
|
2553
|
+
raw_id = border_fill.attributes.get("id")
|
|
2554
|
+
keys: List[str] = []
|
|
2555
|
+
if raw_id:
|
|
2556
|
+
keys.append(raw_id)
|
|
2557
|
+
try:
|
|
2558
|
+
normalized = str(int(raw_id))
|
|
2559
|
+
except ValueError:
|
|
2560
|
+
normalized = None
|
|
2561
|
+
if normalized and normalized not in keys:
|
|
2562
|
+
keys.append(normalized)
|
|
2563
|
+
for key in keys:
|
|
2564
|
+
if key not in mapping:
|
|
2565
|
+
mapping[key] = border_fill
|
|
2566
|
+
return mapping
|
|
2567
|
+
|
|
2568
|
+
def border_fill(self, border_fill_id_ref: int | str | None) -> GenericElement | None:
|
|
2569
|
+
return self._lookup_by_id(self.border_fills, border_fill_id_ref)
|
|
2570
|
+
|
|
2389
2571
|
@staticmethod
|
|
2390
2572
|
def _convert_to_lxml(element: ET.Element) -> LET._Element:
|
|
2391
2573
|
return LET.fromstring(ET.tostring(element, encoding="utf-8"))
|
|
@@ -2788,6 +2970,27 @@ class HwpxOxmlDocument:
|
|
|
2788
2970
|
raise RuntimeError("charPr element is missing an id")
|
|
2789
2971
|
return char_id
|
|
2790
2972
|
|
|
2973
|
+
@property
|
|
2974
|
+
def border_fills(self) -> dict[str, GenericElement]:
|
|
2975
|
+
mapping: dict[str, GenericElement] = {}
|
|
2976
|
+
for header in self._headers:
|
|
2977
|
+
mapping.update(header.border_fills)
|
|
2978
|
+
return mapping
|
|
2979
|
+
|
|
2980
|
+
def border_fill(self, border_fill_id_ref: int | str | None) -> GenericElement | None:
|
|
2981
|
+
return HwpxOxmlHeader._lookup_by_id(self.border_fills, border_fill_id_ref)
|
|
2982
|
+
|
|
2983
|
+
def ensure_basic_border_fill(self) -> str:
|
|
2984
|
+
if not self._headers:
|
|
2985
|
+
return "0"
|
|
2986
|
+
|
|
2987
|
+
for header in self._headers:
|
|
2988
|
+
existing = header.find_basic_border_fill_id()
|
|
2989
|
+
if existing is not None:
|
|
2990
|
+
return existing
|
|
2991
|
+
|
|
2992
|
+
return self._headers[0].ensure_basic_border_fill()
|
|
2993
|
+
|
|
2791
2994
|
@property
|
|
2792
2995
|
def memo_shapes(self) -> dict[str, MemoShape]:
|
|
2793
2996
|
shapes: dict[str, MemoShape] = {}
|
|
@@ -1,9 +1,41 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hwpx
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7
|
|
4
4
|
Summary: Hancom HWPX 패키지를 로드하고 편집하기 위한 Python 유틸리티 모음
|
|
5
5
|
Author: python-hwpx Maintainers
|
|
6
6
|
License: Non-Commercial License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 python-hwpx Maintainers
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to use,
|
|
12
|
+
copy, modify, merge, publish, distribute, and sublicense the Software only for
|
|
13
|
+
non-commercial purposes, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
1. Non-Commercial Use Only. The Software may be used, copied, modified,
|
|
16
|
+
merged, published, distributed, and sublicensed only for non-commercial
|
|
17
|
+
purposes. "Non-Commercial" means use that is not primarily intended for or
|
|
18
|
+
directed toward commercial advantage, monetary compensation, or any form of
|
|
19
|
+
direct or indirect commercial exploitation.
|
|
20
|
+
|
|
21
|
+
2. Attribution. The above copyright notice and this permission notice shall be
|
|
22
|
+
included in all copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
3. No Warranty of Commercial Support. The maintainers are not obligated to
|
|
25
|
+
provide commercial support, maintenance, or updates.
|
|
26
|
+
|
|
27
|
+
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
28
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
29
|
+
FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
|
30
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
31
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
32
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
33
|
+
SOFTWARE.
|
|
34
|
+
|
|
35
|
+
If you require permissions to use this Software for commercial purposes,
|
|
36
|
+
please contact the copyright holders to negotiate an alternative licensing
|
|
37
|
+
arrangement.
|
|
38
|
+
|
|
7
39
|
Project-URL: Homepage, https://github.com/airmang/python-hwpx
|
|
8
40
|
Project-URL: Documentation, https://github.com/airmang/python-hwpx/tree/main/docs
|
|
9
41
|
Project-URL: Issues, https://github.com/airmang/python-hwpx/issues
|
|
@@ -27,6 +59,7 @@ Requires-Dist: twine>=4.0; extra == "dev"
|
|
|
27
59
|
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
28
60
|
Provides-Extra: test
|
|
29
61
|
Requires-Dist: pytest>=7.4; extra == "test"
|
|
62
|
+
Dynamic: license-file
|
|
30
63
|
|
|
31
64
|
# python-hwpx
|
|
32
65
|
|
|
@@ -38,7 +71,7 @@ Requires-Dist: pytest>=7.4; extra == "test"
|
|
|
38
71
|
- **문서 편집 API** – `hwpx.document.HwpxDocument`는 문단과 표, 메모, 헤더 속성을 파이썬 객체로 노출하고 새 콘텐츠를 손쉽게 추가합니다. 섹션 머리말·꼬리말을 수정하면 `<hp:headerApply>`/`<hp:footerApply>`와 마스터 페이지 링크도 함께 갱신합니다.
|
|
39
72
|
- **타입이 지정된 본문 모델** – `hwpx.oxml.body`는 표·컨트롤·인라인 도형·변경 추적 태그를 데이터 클래스에 매핑하고, `HwpxOxmlParagraph.model`/`HwpxOxmlRun.model`로 이를 조회·수정한 뒤 XML로 되돌릴 수 있도록 지원합니다.
|
|
40
73
|
- **메모와 필드 앵커** – `add_memo_with_anchor()`로 메모를 생성하면서 MEMO 필드 컨트롤을 자동 삽입해 한/글에서 바로 표시되도록 합니다.
|
|
41
|
-
- **헤더 참조 목록 탐색** – 글머리표, 문단 속성, 스타일, 변경 추적 항목, 작성자 정보를 데이터클래스로 파싱하고 `document.bullets`·`document.styles` 같은 조회 헬퍼로 ID 기반 검색을 단순화했습니다.
|
|
74
|
+
- **헤더 참조 목록 탐색** – 글머리표, 문단 속성, 테두리 채우기, 스타일, 변경 추적 항목, 작성자 정보를 데이터클래스로 파싱하고 `document.border_fills`·`document.bullets`·`document.styles` 같은 조회 헬퍼로 ID 기반 검색을 단순화했습니다.
|
|
42
75
|
- **바탕쪽·이력·버전 파트 제어** – 매니페스트에 포함된 master-page/history/version 파트를 `document.master_pages`, `document.histories`, `document.version`으로 직접 편집하고 저장합니다.
|
|
43
76
|
- **스타일 기반 텍스트 치환** – 런 서식(색상, 밑줄, `charPrIDRef`)으로 필터링해 텍스트를 선택적으로 교체하거나 삭제합니다. 하이라이트
|
|
44
77
|
마커나 태그로 분리된 문자열도 서식을 유지한 채 치환합니다.
|
|
@@ -79,6 +112,7 @@ print("sections:", len(document.sections))
|
|
|
79
112
|
# 2) 문단과 표, 메모 추가
|
|
80
113
|
section = document.sections[0]
|
|
81
114
|
paragraph = document.add_paragraph("자동 생성한 문단", section=section)
|
|
115
|
+
# 표에 사용할 기본 실선 테두리 채우기가 없으면 add_table()이 자동으로 생성합니다.
|
|
82
116
|
table = document.add_table(rows=2, cols=2, section=section)
|
|
83
117
|
table.set_cell_text(0, 0, "항목")
|
|
84
118
|
table.set_cell_text(0, 1, "값")
|
|
@@ -90,6 +124,10 @@ document.add_memo_with_anchor("배포 전 검토", paragraph=paragraph, memo_sha
|
|
|
90
124
|
document.save("output/example.hwpx")
|
|
91
125
|
```
|
|
92
126
|
|
|
127
|
+
`HwpxDocument.add_table()`은 문서에 정의된 테두리 채우기가 없으면 헤더 참조 목록에 "기본 실선" `borderFill`을 만들어 표와 모든 셀에 참조를 연결합니다.
|
|
128
|
+
|
|
129
|
+
표 셀 텍스트를 편집하는 `table.set_cell_text()`는 기존 단락에 남아 있는 `lineSegArray`와 같은 줄 배치 캐시를 제거하여 한/글이 문서를 다시 열 때 줄바꿈을 새로 계산하도록 합니다.
|
|
130
|
+
|
|
93
131
|
더 많은 실전 패턴은 [빠른 시작](docs/quickstart.md)과 [사용 가이드](docs/usage.md)의 "빠른 예제 모음"에서 확인할 수 있습니다.
|
|
94
132
|
|
|
95
133
|
## 문서
|
|
@@ -295,6 +295,31 @@ def test_document_add_table_creates_table_structure() -> None:
|
|
|
295
295
|
assert section.dirty is True
|
|
296
296
|
|
|
297
297
|
|
|
298
|
+
def test_table_set_cell_text_removes_layout_cache() -> None:
|
|
299
|
+
section_element = ET.Element(f"{HS}sec")
|
|
300
|
+
section = HwpxOxmlSection("section0.xml", section_element)
|
|
301
|
+
manifest = ET.Element("manifest")
|
|
302
|
+
root = HwpxOxmlDocument(manifest, [section], [])
|
|
303
|
+
document = HwpxDocument(cast(HwpxPackage, object()), root)
|
|
304
|
+
|
|
305
|
+
table = document.add_table(1, 1, section=section)
|
|
306
|
+
cell = table.cell(0, 0)
|
|
307
|
+
sublist = cell.element.find(f"{HP}subList")
|
|
308
|
+
assert sublist is not None
|
|
309
|
+
paragraph = sublist.find(f"{HP}p")
|
|
310
|
+
assert paragraph is not None
|
|
311
|
+
ET.SubElement(paragraph, f"{HP}lineSegArray")
|
|
312
|
+
assert paragraph.find(f"{HP}lineSegArray") is not None
|
|
313
|
+
text_element = paragraph.find(f".//{HP}t")
|
|
314
|
+
assert text_element is not None
|
|
315
|
+
text_element.text = "Cached"
|
|
316
|
+
|
|
317
|
+
table.set_cell_text(0, 0, "Updated")
|
|
318
|
+
|
|
319
|
+
assert table.cell(0, 0).text == "Updated"
|
|
320
|
+
assert paragraph.find(f"{HP}lineSegArray") is None
|
|
321
|
+
|
|
322
|
+
|
|
298
323
|
def test_table_merge_cells_updates_spans_and_structure() -> None:
|
|
299
324
|
section_element = ET.Element(f"{HS}sec")
|
|
300
325
|
section = HwpxOxmlSection("section0.xml", section_element)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Integration tests for automatic table border fills."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import xml.etree.ElementTree as ET
|
|
6
|
+
|
|
7
|
+
from hwpx.document import HwpxDocument
|
|
8
|
+
|
|
9
|
+
HP_NS = "http://www.hancom.co.kr/hwpml/2011/paragraph"
|
|
10
|
+
HH_NS = "http://www.hancom.co.kr/hwpml/2011/head"
|
|
11
|
+
HP = f"{{{HP_NS}}}"
|
|
12
|
+
HH = f"{{{HH_NS}}}"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_add_table_injects_basic_border_fill_when_missing() -> None:
|
|
16
|
+
document = HwpxDocument.new()
|
|
17
|
+
|
|
18
|
+
header = document.headers[0]
|
|
19
|
+
ref_list = header.element.find(f"{HH}refList")
|
|
20
|
+
if ref_list is not None:
|
|
21
|
+
existing = ref_list.find(f"{HH}borderFills")
|
|
22
|
+
if existing is not None:
|
|
23
|
+
ref_list.remove(existing)
|
|
24
|
+
header.reset_dirty()
|
|
25
|
+
|
|
26
|
+
section = document.sections[0]
|
|
27
|
+
section.reset_dirty()
|
|
28
|
+
|
|
29
|
+
table = document.add_table(2, 2, section=section)
|
|
30
|
+
assert table is not None
|
|
31
|
+
|
|
32
|
+
updates = document.oxml.serialize()
|
|
33
|
+
|
|
34
|
+
assert header.part_name in updates
|
|
35
|
+
header_root = ET.fromstring(updates[header.part_name])
|
|
36
|
+
|
|
37
|
+
border_fills_element = header_root.find(f".//{HH}borderFills")
|
|
38
|
+
assert border_fills_element is not None
|
|
39
|
+
assert border_fills_element.get("itemCnt") == "1"
|
|
40
|
+
|
|
41
|
+
border_fill_element = border_fills_element.find(f"{HH}borderFill")
|
|
42
|
+
assert border_fill_element is not None
|
|
43
|
+
border_id = border_fill_element.get("id")
|
|
44
|
+
assert border_id == "0"
|
|
45
|
+
assert border_fill_element.get("threeD") == "0"
|
|
46
|
+
assert border_fill_element.get("shadow") == "0"
|
|
47
|
+
assert border_fill_element.get("centerLine") == "NONE"
|
|
48
|
+
assert border_fill_element.get("breakCellSeparateLine") == "0"
|
|
49
|
+
|
|
50
|
+
slash = border_fill_element.find(f"{HH}slash")
|
|
51
|
+
assert slash is not None
|
|
52
|
+
assert slash.get("type") == "NONE"
|
|
53
|
+
|
|
54
|
+
back_slash = border_fill_element.find(f"{HH}backSlash")
|
|
55
|
+
assert back_slash is not None
|
|
56
|
+
assert back_slash.get("type") == "NONE"
|
|
57
|
+
|
|
58
|
+
for child_name in ("leftBorder", "rightBorder", "topBorder", "bottomBorder"):
|
|
59
|
+
child = border_fill_element.find(f"{HH}{child_name}")
|
|
60
|
+
assert child is not None
|
|
61
|
+
assert child.get("type") == "SOLID"
|
|
62
|
+
assert child.get("width") == "0.12 mm"
|
|
63
|
+
assert child.get("color") == "#000000"
|
|
64
|
+
|
|
65
|
+
diagonal = border_fill_element.find(f"{HH}diagonal")
|
|
66
|
+
assert diagonal is not None
|
|
67
|
+
assert diagonal.get("type") == "SOLID"
|
|
68
|
+
assert diagonal.get("width") == "0.1 mm"
|
|
69
|
+
assert diagonal.get("color") == "#000000"
|
|
70
|
+
|
|
71
|
+
assert section.part_name in updates
|
|
72
|
+
section_root = ET.fromstring(updates[section.part_name])
|
|
73
|
+
|
|
74
|
+
table_element = section_root.find(f".//{HP}tbl")
|
|
75
|
+
assert table_element is not None
|
|
76
|
+
assert table_element.get("borderFillIDRef") == border_id
|
|
77
|
+
|
|
78
|
+
cells = section_root.findall(f".//{HP}tc")
|
|
79
|
+
assert cells
|
|
80
|
+
for cell in cells:
|
|
81
|
+
assert cell.get("borderFillIDRef") == border_id
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|