pywpdoc 0.44.0rc1__py3-none-any.whl
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.
- pywpdoc/__init__.py +27 -0
- pywpdoc/_engine/__init__.py +26 -0
- pywpdoc/_engine/_hwpx_section.py +86 -0
- pywpdoc/_engine/_hwpx_story.py +35 -0
- pywpdoc/_engine/_hwpx_table.py +37 -0
- pywpdoc/_engine/authoring.py +273 -0
- pywpdoc/_engine/common/__init__.py +3 -0
- pywpdoc/_engine/common/document.py +133 -0
- pywpdoc/_engine/common_api.py +9 -0
- pywpdoc/_engine/core/__init__.py +11 -0
- pywpdoc/_engine/core/contracts.py +153 -0
- pywpdoc/_engine/core/defaults.py +18 -0
- pywpdoc/_engine/core/enums.py +32 -0
- pywpdoc/_engine/core/errors.py +39 -0
- pywpdoc/_engine/core/proxy.py +22 -0
- pywpdoc/_engine/core/snapshot.py +27 -0
- pywpdoc/_engine/core/units.py +54 -0
- pywpdoc/_engine/data/default.hwpx +0 -0
- pywpdoc/_engine/data/default_2020.hwpx +0 -0
- pywpdoc/_engine/document.py +866 -0
- pywpdoc/_engine/docx/__init__.py +11 -0
- pywpdoc/_engine/docx/backend.py +166 -0
- pywpdoc/_engine/docx/comparison_safety.py +79 -0
- pywpdoc/_engine/docx/fragments.py +177 -0
- pywpdoc/_engine/docx/media.py +105 -0
- pywpdoc/_engine/docx/metadata.py +101 -0
- pywpdoc/_engine/docx/paragraph.py +126 -0
- pywpdoc/_engine/docx/revisions.py +747 -0
- pywpdoc/_engine/docx/section.py +111 -0
- pywpdoc/_engine/docx/snapshot.py +587 -0
- pywpdoc/_engine/docx/story.py +107 -0
- pywpdoc/_engine/docx/styles.py +241 -0
- pywpdoc/_engine/docx/table.py +303 -0
- pywpdoc/_engine/docx/table_geometry.py +85 -0
- pywpdoc/_engine/docx/text.py +148 -0
- pywpdoc/_engine/docx/traversal.py +81 -0
- pywpdoc/_engine/enum/__init__.py +10 -0
- pywpdoc/_engine/enum/section.py +3 -0
- pywpdoc/_engine/enum/style.py +3 -0
- pywpdoc/_engine/enum/table.py +3 -0
- pywpdoc/_engine/enum/text.py +40 -0
- pywpdoc/_engine/errors.py +36 -0
- pywpdoc/_engine/hwpx/__init__.py +16 -0
- pywpdoc/_engine/hwpx/backend.py +140 -0
- pywpdoc/_engine/hwpx/comparison_safety.py +60 -0
- pywpdoc/_engine/hwpx/media.py +72 -0
- pywpdoc/_engine/hwpx/metadata.py +92 -0
- pywpdoc/_engine/hwpx/paragraph.py +135 -0
- pywpdoc/_engine/hwpx/revisions.py +1474 -0
- pywpdoc/_engine/hwpx/section.py +53 -0
- pywpdoc/_engine/hwpx/snapshot.py +287 -0
- pywpdoc/_engine/hwpx/story.py +72 -0
- pywpdoc/_engine/hwpx/styles.py +120 -0
- pywpdoc/_engine/hwpx/table.py +246 -0
- pywpdoc/_engine/hwpx/table_geometry.py +18 -0
- pywpdoc/_engine/hwpx/text.py +26 -0
- pywpdoc/_engine/metadata.py +190 -0
- pywpdoc/_engine/model.py +2796 -0
- pywpdoc/_engine/package.py +84 -0
- pywpdoc/_engine/py.typed +0 -0
- pywpdoc/_engine/reverse.py +129 -0
- pywpdoc/_engine/shared.py +8 -0
- pywpdoc/_engine/styles.py +1697 -0
- pywpdoc/_engine/validation.py +377 -0
- pywpdoc/core/__init__.py +19 -0
- pywpdoc/core/drawing/__init__.py +18 -0
- pywpdoc/core/drawing/model.py +224 -0
- pywpdoc/core/structure/__init__.py +4 -0
- pywpdoc/core/structure/traversal.py +23 -0
- pywpdoc/core/text/__init__.py +13 -0
- pywpdoc/core/text/fragments.py +77 -0
- pywpdoc/core/text/map.py +90 -0
- pywpdoc/core/text/match.py +71 -0
- pywpdoc/core/text/mutation.py +63 -0
- pywpdoc/core/text/traversal.py +29 -0
- pywpdoc/document.py +189 -0
- pywpdoc/docx/__init__.py +8 -0
- pywpdoc/features/__init__.py +14 -0
- pywpdoc/features/compare/__init__.py +11 -0
- pywpdoc/features/compare/api.py +747 -0
- pywpdoc/features/compare/model.py +124 -0
- pywpdoc/features/find/__init__.py +5 -0
- pywpdoc/features/find/api.py +55 -0
- pywpdoc/features/find/model.py +19 -0
- pywpdoc/features/regex/__init__.py +4 -0
- pywpdoc/features/regex/api.py +132 -0
- pywpdoc/features/replace/__init__.py +5 -0
- pywpdoc/features/replace/api.py +72 -0
- pywpdoc/features/replace/preview.py +123 -0
- pywpdoc/features/template/__init__.py +16 -0
- pywpdoc/features/template/api.py +78 -0
- pywpdoc/features/template/check.py +308 -0
- pywpdoc/features/template/context.py +93 -0
- pywpdoc/features/template/model.py +21 -0
- pywpdoc/features/template/parser.py +52 -0
- pywpdoc/features/template/renderer.py +158 -0
- pywpdoc/features/template/sections.py +186 -0
- pywpdoc/features/template/structure/__init__.py +11 -0
- pywpdoc/features/template/structure/model.py +71 -0
- pywpdoc/features/template/structure/parser.py +93 -0
- pywpdoc/features/template/structure/rows.py +268 -0
- pywpdoc/hwpx/__init__.py +7 -0
- pywpdoc-0.44.0rc1.dist-info/METADATA +165 -0
- pywpdoc-0.44.0rc1.dist-info/RECORD +107 -0
- pywpdoc-0.44.0rc1.dist-info/WHEEL +5 -0
- pywpdoc-0.44.0rc1.dist-info/licenses/LICENSE +21 -0
- pywpdoc-0.44.0rc1.dist-info/top_level.txt +1 -0
pywpdoc/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""PyWPDoc: Python word-processing document automation API.
|
|
2
|
+
|
|
3
|
+
Use ``pywpdoc.Document`` for the high-level format-neutral API.
|
|
4
|
+
Use ``pywpdoc.hwpx`` or ``pywpdoc.docx`` for backend-specific APIs.
|
|
5
|
+
"""
|
|
6
|
+
from .document import Document, UnifiedDocument
|
|
7
|
+
from .core import (
|
|
8
|
+
Length, Pt, Mm, HwpUnit, Cm, Inches,
|
|
9
|
+
WD_CELL_VERTICAL_ALIGNMENT, WD_ALIGN_VERTICAL,
|
|
10
|
+
WD_ORIENTATION, WD_ORIENT, WD_STYLE_TYPE,
|
|
11
|
+
DocumentError, DocumentValueError, DocumentStateError, DocumentUnsupportedError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from importlib.metadata import PackageNotFoundError as _PackageNotFoundError, version as _pkg_version
|
|
15
|
+
try:
|
|
16
|
+
__version__ = _pkg_version("pywpdoc")
|
|
17
|
+
except _PackageNotFoundError:
|
|
18
|
+
__version__ = "0+unknown" # direct source import without installed package metadata
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Document", "UnifiedDocument",
|
|
22
|
+
"Length", "Pt", "Mm", "HwpUnit", "Cm", "Inches",
|
|
23
|
+
"WD_CELL_VERTICAL_ALIGNMENT", "WD_ALIGN_VERTICAL",
|
|
24
|
+
"WD_ORIENTATION", "WD_ORIENT", "WD_STYLE_TYPE",
|
|
25
|
+
"DocumentError", "DocumentValueError", "DocumentStateError", "DocumentUnsupportedError",
|
|
26
|
+
"__version__",
|
|
27
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .document import Document
|
|
2
|
+
from .reverse import reverse_hwpx
|
|
3
|
+
from .model import Paragraph, Run, Table, Row, Column, Cell, Shape, Rectangle, Picture, ElementModel, Section, PageSetup, HeaderFooter, Note, Hyperlink
|
|
4
|
+
from .styles import Styles, Style, Font, ParagraphFormat, CellBorder, TabStop, TabStops
|
|
5
|
+
from .shared import Length, Pt, Mm, Cm, Inches, HwpUnit
|
|
6
|
+
from .enum.text import WD_BREAK, WD_BREAK_TYPE, WD_TAB_ALIGNMENT, WD_TAB_LEADER
|
|
7
|
+
from .enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_ALIGN_VERTICAL
|
|
8
|
+
from .enum.section import WD_ORIENTATION, WD_ORIENT
|
|
9
|
+
from .enum.style import WD_STYLE_TYPE
|
|
10
|
+
from .metadata import CoreProperties, Settings, CaretPosition
|
|
11
|
+
from .validation import ValidationIssue, ValidationReport, validate_hwpx_file
|
|
12
|
+
from .errors import HwpxError, HwpxValueError, HwpxStateError, HwpxUnsupportedError, HwpxValidationError
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Document", "reverse_hwpx",
|
|
16
|
+
"Paragraph", "Run", "Table", "Row", "Column", "Cell", "Shape", "Rectangle", "Picture", "ElementModel",
|
|
17
|
+
"Section", "PageSetup", "HeaderFooter", "Note", "Hyperlink",
|
|
18
|
+
"Styles", "Style", "Font", "ParagraphFormat", "CellBorder", "TabStop", "TabStops",
|
|
19
|
+
"Length", "Pt", "Mm", "Cm", "Inches", "HwpUnit",
|
|
20
|
+
"WD_BREAK", "WD_BREAK_TYPE", "WD_TAB_ALIGNMENT", "WD_TAB_LEADER",
|
|
21
|
+
"WD_CELL_VERTICAL_ALIGNMENT", "WD_ALIGN_VERTICAL", "WD_ORIENTATION", "WD_ORIENT",
|
|
22
|
+
"WD_STYLE_TYPE",
|
|
23
|
+
"CoreProperties", "Settings", "CaretPosition",
|
|
24
|
+
"ValidationIssue", "ValidationReport", "validate_hwpx_file",
|
|
25
|
+
"HwpxError", "HwpxValueError", "HwpxStateError", "HwpxUnsupportedError", "HwpxValidationError",
|
|
26
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Low-level HWPX section/story authoring policy.
|
|
2
|
+
|
|
3
|
+
This module belongs to the native HWPX engine, below the standalone ``hwpx``
|
|
4
|
+
backend wrappers. It intentionally knows nothing about DOCX or the common
|
|
5
|
+
facade.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def invalidate_layout_cache(section: Any) -> int:
|
|
13
|
+
"""Drop stale paragraph line-layout caches for a section.
|
|
14
|
+
|
|
15
|
+
Hancom persists ``hp:linesegarray`` as a derived layout cache. Page size,
|
|
16
|
+
margin, orientation, and header/footer-area changes can invalidate every
|
|
17
|
+
cached line position in the section. Keeping those coordinates after a
|
|
18
|
+
geometry mutation can make Hancom lay out visible content against the old
|
|
19
|
+
page box, so native geometry setters clear them instead of trying to patch
|
|
20
|
+
opaque cached coordinates.
|
|
21
|
+
"""
|
|
22
|
+
from .model import local_name
|
|
23
|
+
|
|
24
|
+
removed = 0
|
|
25
|
+
for node in section.element_model.iter():
|
|
26
|
+
if local_name(node.tag) != "p":
|
|
27
|
+
continue
|
|
28
|
+
kept = []
|
|
29
|
+
for child in node.children:
|
|
30
|
+
if local_name(child.tag).lower() == "linesegarray":
|
|
31
|
+
removed += 1
|
|
32
|
+
else:
|
|
33
|
+
kept.append(child)
|
|
34
|
+
if len(kept) != len(node.children):
|
|
35
|
+
node.children[:] = kept
|
|
36
|
+
return removed
|
|
37
|
+
|
|
38
|
+
def content_width(section: Any) -> int:
|
|
39
|
+
"""Return the section's writable horizontal extent in HWPUNIT."""
|
|
40
|
+
page = section.page_setup
|
|
41
|
+
return max(int(page.width) - int(page.left_margin) - int(page.right_margin), 0)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def story_style_refs(section: Any, kind: str) -> tuple[str, str, str]:
|
|
45
|
+
"""Return native style/paragraph/character refs for a story's first line.
|
|
46
|
+
|
|
47
|
+
The bundled Hancom skeleton has a dedicated ``Header``/``머리말`` style
|
|
48
|
+
but no independent Footer style. Hancom uses the story-specific style as
|
|
49
|
+
the best native baseline for authored header/footer text; later paragraphs
|
|
50
|
+
added explicitly keep ordinary paragraph defaults unless a style is given.
|
|
51
|
+
"""
|
|
52
|
+
document = section.document
|
|
53
|
+
if document is not None:
|
|
54
|
+
style = document.styles.get("Header") or document.styles.get("머리말")
|
|
55
|
+
if style is not None:
|
|
56
|
+
return (str(style.id), str(style.para_pr_id_ref or "0"), str(style.char_pr_id_ref or "0"))
|
|
57
|
+
return ("0", "0", "0")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def story_sublist_attrs(section: Any, kind: str) -> list[tuple[str, str]]:
|
|
61
|
+
from .authoring import sublist_attrs
|
|
62
|
+
|
|
63
|
+
attrs = dict(sublist_attrs())
|
|
64
|
+
attrs["vertAlign"] = "TOP" if kind == "header" else "BOTTOM"
|
|
65
|
+
attrs["textWidth"] = str(content_width(section))
|
|
66
|
+
page = section.page_setup
|
|
67
|
+
attrs["textHeight"] = str(int(page.header_distance if kind == "header" else page.footer_distance))
|
|
68
|
+
return list(attrs.items())
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def simple_story(section: Any, kind: str, page_type: str, text: str):
|
|
72
|
+
from .authoring import object_id, paragraph_attrs
|
|
73
|
+
from .model import ElementModel, HP
|
|
74
|
+
|
|
75
|
+
style_ref, para_ref, char_ref = story_style_refs(section, kind)
|
|
76
|
+
story = ElementModel(HP + kind, attrs=[("id", object_id()), ("applyPageType", page_type)])
|
|
77
|
+
sub = ElementModel(HP + "subList", attrs=story_sublist_attrs(section, kind))
|
|
78
|
+
if "\n" in text:
|
|
79
|
+
sub.set("textHeight", "0")
|
|
80
|
+
p = ElementModel(HP + "p", attrs=paragraph_attrs(para_pr_id_ref=para_ref, style_id_ref=style_ref))
|
|
81
|
+
run = ElementModel(HP + "run", attrs=[("charPrIDRef", char_ref)])
|
|
82
|
+
run.children.append(ElementModel(HP + "t", text=text))
|
|
83
|
+
p.children.append(run)
|
|
84
|
+
sub.children.append(p)
|
|
85
|
+
story.children.append(sub)
|
|
86
|
+
return story
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Low-level HWPX header/footer content policy.
|
|
2
|
+
|
|
3
|
+
Kept below the standalone HWPX backend so ``hwpx.story`` can remain a thin
|
|
4
|
+
object adapter. No imports from ``common`` or ``docx`` are allowed here.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def paragraph_text(story: Any) -> str:
|
|
11
|
+
"""Return block paragraph text without leaking nested-table cell text."""
|
|
12
|
+
texts: list[str] = []
|
|
13
|
+
for item in story.iter_inner_content():
|
|
14
|
+
if type(item).__name__ == "Paragraph":
|
|
15
|
+
texts.append(item.text)
|
|
16
|
+
return "\n".join(texts)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def enable_auto_story_height(story: Any) -> None:
|
|
21
|
+
"""Let Hancom size a rich header/footer story from its block content.
|
|
22
|
+
|
|
23
|
+
``hp:subList/@textHeight`` is the story text-area height. A fixed value
|
|
24
|
+
equal to the header/footer distance is only suitable for a simple single
|
|
25
|
+
line story. Once additional paragraphs, tables, fields, or runs are
|
|
26
|
+
authored, keeping that fixed box can make content overflow into the body.
|
|
27
|
+
HWPX ParaList containers use ``0`` for content-sized text areas, so rich
|
|
28
|
+
authored stories switch to that native auto-size form.
|
|
29
|
+
"""
|
|
30
|
+
sub = story._ensure_sublist()
|
|
31
|
+
sub.set("textHeight", "0")
|
|
32
|
+
|
|
33
|
+
def normalize_story_table(table: Any) -> None:
|
|
34
|
+
from ._hwpx_table import normalize_authored_table
|
|
35
|
+
normalize_authored_table(table)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Native HWPX table authoring defaults shared by HWPX entry points only."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def normalize_authored_table(table: Any) -> None:
|
|
7
|
+
"""Apply the HWPX backend's authored-cell defaults to *table*."""
|
|
8
|
+
from .core import defaults
|
|
9
|
+
from .core.units import Mm
|
|
10
|
+
from .model import local_name
|
|
11
|
+
|
|
12
|
+
margin = next((c for c in table.element_model.children if local_name(c.tag) == "inMargin"), None)
|
|
13
|
+
if margin is not None:
|
|
14
|
+
margin.set("left", str(int(Mm(defaults.CELL_MARGIN_LEFT_MM))))
|
|
15
|
+
margin.set("right", str(int(Mm(defaults.CELL_MARGIN_RIGHT_MM))))
|
|
16
|
+
margin.set("top", str(int(Mm(defaults.CELL_MARGIN_TOP_MM))))
|
|
17
|
+
margin.set("bottom", str(int(Mm(defaults.CELL_MARGIN_BOTTOM_MM))))
|
|
18
|
+
for row in table.rows:
|
|
19
|
+
for cell in row.physical_cells:
|
|
20
|
+
cell.vertical_alignment = defaults.DEFAULT_CELL_VERTICAL_ALIGNMENT
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def fit_nested_table(table: Any, cell: Any, cols: int) -> None:
|
|
24
|
+
if cols <= 0:
|
|
25
|
+
return
|
|
26
|
+
from .authoring import distribute_size
|
|
27
|
+
from .core import defaults
|
|
28
|
+
from .core.units import Mm
|
|
29
|
+
|
|
30
|
+
target = max(
|
|
31
|
+
int(cell.width)
|
|
32
|
+
- int(Mm(defaults.CELL_MARGIN_LEFT_MM))
|
|
33
|
+
- int(Mm(defaults.CELL_MARGIN_RIGHT_MM)),
|
|
34
|
+
0,
|
|
35
|
+
)
|
|
36
|
+
for index, width in enumerate(distribute_size(target, cols)):
|
|
37
|
+
table.columns[index].width = width
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import zipfile
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .model import ElementModel, HP, HC, local_name
|
|
8
|
+
from .package import PackageEntryInfo
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .document import Document
|
|
12
|
+
|
|
13
|
+
OPF_NS = "http://www.idpf.org/2007/opf/"
|
|
14
|
+
OPF = f"{{{OPF_NS}}}"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def object_id() -> str:
|
|
18
|
+
return str(random.SystemRandom().randrange(1, 2**31 - 1))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def paragraph_id() -> str:
|
|
22
|
+
return object_id()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def distribute_size(total: int, parts: int) -> list[int]:
|
|
26
|
+
if parts <= 0:
|
|
27
|
+
return []
|
|
28
|
+
base, rem = divmod(max(int(total), 0), parts)
|
|
29
|
+
return [base + (1 if i < rem else 0) for i in range(parts)]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def sublist_attrs() -> list[tuple[str, str]]:
|
|
33
|
+
return [
|
|
34
|
+
("id", ""), ("textDirection", "HORIZONTAL"), ("lineWrap", "BREAK"),
|
|
35
|
+
("vertAlign", "CENTER"), ("linkListIDRef", "0"),
|
|
36
|
+
("linkListNextIDRef", "0"), ("textWidth", "0"), ("textHeight", "0"),
|
|
37
|
+
("hasTextRef", "0"), ("hasNumRef", "0"),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def paragraph_attrs(*, para_pr_id_ref: str | int = "0", style_id_ref: str | int = "0") -> list[tuple[str, str]]:
|
|
42
|
+
return [
|
|
43
|
+
("id", paragraph_id()), ("paraPrIDRef", str(para_pr_id_ref)),
|
|
44
|
+
("styleIDRef", str(style_id_ref)), ("pageBreak", "0"),
|
|
45
|
+
("columnBreak", "0"), ("merged", "0"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def cell_inner_margin() -> list[tuple[str, str]]:
|
|
50
|
+
return [("left", "510"), ("right", "510"), ("top", "141"), ("bottom", "141")]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def zero_margin() -> list[tuple[str, str]]:
|
|
54
|
+
return [("left", "0"), ("right", "0"), ("top", "0"), ("bottom", "0")]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def create_table(
|
|
58
|
+
rows: int,
|
|
59
|
+
cols: int,
|
|
60
|
+
*,
|
|
61
|
+
width: int | None,
|
|
62
|
+
height: int | None,
|
|
63
|
+
border_fill_id_ref: str | int,
|
|
64
|
+
) -> ElementModel:
|
|
65
|
+
if rows <= 0 or cols <= 0:
|
|
66
|
+
raise ValueError("rows and cols must be positive integers")
|
|
67
|
+
table_width = int(width if width is not None else cols * 7200)
|
|
68
|
+
table_height = int(height if height is not None else rows * 282)
|
|
69
|
+
border = str(border_fill_id_ref)
|
|
70
|
+
|
|
71
|
+
tbl = ElementModel(HP + "tbl", attrs=[
|
|
72
|
+
("id", object_id()), ("zOrder", "0"), ("numberingType", "TABLE"),
|
|
73
|
+
("textWrap", "TOP_AND_BOTTOM"), ("textFlow", "BOTH_SIDES"), ("lock", "0"),
|
|
74
|
+
("dropcapstyle", "None"), ("pageBreak", "CELL"), ("repeatHeader", "0"),
|
|
75
|
+
("rowCnt", str(rows)), ("colCnt", str(cols)), ("cellSpacing", "0"),
|
|
76
|
+
("borderFillIDRef", border), ("noAdjust", "0"),
|
|
77
|
+
])
|
|
78
|
+
tbl.children.extend([
|
|
79
|
+
ElementModel(HP + "sz", attrs=[
|
|
80
|
+
("width", str(table_width)), ("widthRelTo", "ABSOLUTE"),
|
|
81
|
+
("height", str(table_height)), ("heightRelTo", "ABSOLUTE"), ("protect", "0"),
|
|
82
|
+
]),
|
|
83
|
+
ElementModel(HP + "pos", attrs=[
|
|
84
|
+
("treatAsChar", "1"), ("affectLSpacing", "0"), ("flowWithText", "1"),
|
|
85
|
+
("allowOverlap", "0"), ("holdAnchorAndSO", "0"), ("vertRelTo", "PARA"),
|
|
86
|
+
("horzRelTo", "COLUMN"), ("vertAlign", "TOP"), ("horzAlign", "LEFT"),
|
|
87
|
+
("vertOffset", "0"), ("horzOffset", "0"),
|
|
88
|
+
]),
|
|
89
|
+
ElementModel(HP + "outMargin", attrs=zero_margin()),
|
|
90
|
+
ElementModel(HP + "inMargin", attrs=cell_inner_margin()),
|
|
91
|
+
])
|
|
92
|
+
col_widths = distribute_size(table_width, cols)
|
|
93
|
+
row_heights = distribute_size(table_height, rows)
|
|
94
|
+
for r in range(rows):
|
|
95
|
+
tr = ElementModel(HP + "tr")
|
|
96
|
+
for c in range(cols):
|
|
97
|
+
tc = ElementModel(HP + "tc", attrs=[
|
|
98
|
+
("name", ""), ("header", "0"), ("hasMargin", "0"), ("protect", "0"),
|
|
99
|
+
("editable", "0"), ("dirty", "0"), ("borderFillIDRef", border),
|
|
100
|
+
])
|
|
101
|
+
sub = ElementModel(HP + "subList", attrs=sublist_attrs())
|
|
102
|
+
p = ElementModel(HP + "p", attrs=paragraph_attrs())
|
|
103
|
+
run = ElementModel(HP + "run", attrs=[("charPrIDRef", "0")])
|
|
104
|
+
run.children.append(ElementModel(HP + "t", text=""))
|
|
105
|
+
p.children.append(run)
|
|
106
|
+
sub.children.append(p)
|
|
107
|
+
tc.children.extend([
|
|
108
|
+
sub,
|
|
109
|
+
ElementModel(HP + "cellAddr", attrs=[("colAddr", str(c)), ("rowAddr", str(r))]),
|
|
110
|
+
ElementModel(HP + "cellSpan", attrs=[("colSpan", "1"), ("rowSpan", "1")]),
|
|
111
|
+
ElementModel(HP + "cellSz", attrs=[("width", str(col_widths[c])), ("height", str(row_heights[r]))]),
|
|
112
|
+
ElementModel(HP + "cellMargin", attrs=cell_inner_margin()),
|
|
113
|
+
])
|
|
114
|
+
tr.children.append(tc)
|
|
115
|
+
tbl.children.append(tr)
|
|
116
|
+
return tbl
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
_IDENTITY = [("e1", "1"), ("e2", "0"), ("e3", "0"), ("e4", "0"), ("e5", "1"), ("e6", "0")]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _shape_common(parent: ElementModel, width: int, height: int, *, treat_as_char: bool) -> None:
|
|
123
|
+
ident = object_id()
|
|
124
|
+
for k, v in [
|
|
125
|
+
("id", ident), ("zOrder", "0"), ("numberingType", "NONE"), ("textWrap", "SQUARE"),
|
|
126
|
+
("textFlow", "BOTH_SIDES"), ("lock", "0"), ("dropcapstyle", "None"),
|
|
127
|
+
("href", ""), ("groupLevel", "0"), ("instid", ident),
|
|
128
|
+
]:
|
|
129
|
+
parent.set(k, v)
|
|
130
|
+
parent.children.extend([
|
|
131
|
+
ElementModel(HP + "offset", attrs=[("x", "0"), ("y", "0")]),
|
|
132
|
+
ElementModel(HP + "orgSz", attrs=[("width", str(width)), ("height", str(height))]),
|
|
133
|
+
ElementModel(HP + "curSz", attrs=[("width", str(width)), ("height", str(height))]),
|
|
134
|
+
ElementModel(HP + "flip", attrs=[("horizontal", "0"), ("vertical", "0")]),
|
|
135
|
+
ElementModel(HP + "rotationInfo", attrs=[
|
|
136
|
+
("angle", "0"), ("centerX", str(width // 2)), ("centerY", str(height // 2)), ("rotateimage", "1")
|
|
137
|
+
]),
|
|
138
|
+
])
|
|
139
|
+
rendering = ElementModel(HP + "renderingInfo")
|
|
140
|
+
rendering.children.extend([
|
|
141
|
+
ElementModel(HC + "transMatrix", attrs=list(_IDENTITY)),
|
|
142
|
+
ElementModel(HC + "scaMatrix", attrs=list(_IDENTITY)),
|
|
143
|
+
ElementModel(HC + "rotMatrix", attrs=list(_IDENTITY)),
|
|
144
|
+
])
|
|
145
|
+
parent.children.append(rendering)
|
|
146
|
+
parent.set("_treatAsChar", "1" if treat_as_char else "0")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _shape_drawing(parent: ElementModel, *, line_color: str, line_width: str, fill_color: str | None) -> None:
|
|
150
|
+
parent.children.append(ElementModel(HP + "lineShape", attrs=[
|
|
151
|
+
("color", line_color.upper()), ("width", str(line_width)), ("style", "SOLID"),
|
|
152
|
+
("endCap", "FLAT"), ("headStyle", "NORMAL"), ("tailStyle", "NORMAL"),
|
|
153
|
+
("headfill", "1"), ("tailfill", "1"), ("headSz", "SMALL_SMALL"),
|
|
154
|
+
("tailSz", "SMALL_SMALL"), ("outlineStyle", "NORMAL"), ("alpha", "0"),
|
|
155
|
+
]))
|
|
156
|
+
if fill_color is not None:
|
|
157
|
+
fill = ElementModel(HC + "fillBrush")
|
|
158
|
+
fill.children.append(ElementModel(HC + "winBrush", attrs=[
|
|
159
|
+
("faceColor", fill_color.upper()), ("hatchColor", "#FFFFFF"), ("alpha", "0")
|
|
160
|
+
]))
|
|
161
|
+
parent.children.append(fill)
|
|
162
|
+
parent.children.append(ElementModel(HP + "shadow", attrs=[
|
|
163
|
+
("type", "NONE"), ("color", "#B2B2B2"), ("offsetX", "0"), ("offsetY", "0"), ("alpha", "0")
|
|
164
|
+
]))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _shape_base(parent: ElementModel, width: int, height: int) -> None:
|
|
168
|
+
treat = parent.get("_treatAsChar", "1") == "1"
|
|
169
|
+
parent.remove_attr("_treatAsChar")
|
|
170
|
+
parent.children.extend([
|
|
171
|
+
ElementModel(HP + "sz", attrs=[
|
|
172
|
+
("width", str(width)), ("height", str(height)), ("widthRelTo", "ABSOLUTE"),
|
|
173
|
+
("heightRelTo", "ABSOLUTE"), ("protect", "0"),
|
|
174
|
+
]),
|
|
175
|
+
ElementModel(HP + "pos", attrs=[
|
|
176
|
+
("treatAsChar", "1" if treat else "0"), ("affectLSpacing", "0"),
|
|
177
|
+
("flowWithText", "1" if treat else "0"), ("allowOverlap", "0" if treat else "1"),
|
|
178
|
+
("holdAnchorAndSO", "0"), ("vertRelTo", "PARA"), ("horzRelTo", "COLUMN"),
|
|
179
|
+
("vertAlign", "TOP"), ("horzAlign", "LEFT"), ("vertOffset", "0"), ("horzOffset", "0"),
|
|
180
|
+
]),
|
|
181
|
+
ElementModel(HP + "outMargin", attrs=zero_margin()),
|
|
182
|
+
])
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def create_rectangle(
|
|
186
|
+
width: int,
|
|
187
|
+
height: int,
|
|
188
|
+
*,
|
|
189
|
+
line_color: str = "#000000",
|
|
190
|
+
line_width: str = "33",
|
|
191
|
+
fill_color: str | None = None,
|
|
192
|
+
treat_as_char: bool = True,
|
|
193
|
+
text: str | None = None,
|
|
194
|
+
para_pr_id_ref: str | int = "0",
|
|
195
|
+
char_pr_id_ref: str | int = "0",
|
|
196
|
+
) -> ElementModel:
|
|
197
|
+
rect = ElementModel(HP + "rect", attrs=[("ratio", "0")])
|
|
198
|
+
_shape_common(rect, int(width), int(height), treat_as_char=treat_as_char)
|
|
199
|
+
_shape_drawing(rect, line_color=line_color, line_width=line_width, fill_color=fill_color)
|
|
200
|
+
if text is not None:
|
|
201
|
+
draw = ElementModel(HP + "drawText", attrs=[("lastWidth", str(width)), ("name", ""), ("editable", "0")])
|
|
202
|
+
sub = ElementModel(HP + "subList", attrs=sublist_attrs())
|
|
203
|
+
p = ElementModel(HP + "p", attrs=paragraph_attrs(para_pr_id_ref=para_pr_id_ref))
|
|
204
|
+
run = ElementModel(HP + "run", attrs=[("charPrIDRef", str(char_pr_id_ref))])
|
|
205
|
+
run.children.append(ElementModel(HP + "t", text=text))
|
|
206
|
+
p.children.append(run)
|
|
207
|
+
sub.children.append(p)
|
|
208
|
+
draw.children.extend([sub, ElementModel(HP + "textMargin", attrs=[("left", "283"), ("right", "283"), ("top", "283"), ("bottom", "283")])])
|
|
209
|
+
rect.children.append(draw)
|
|
210
|
+
rect.children.extend([
|
|
211
|
+
ElementModel(HC + "pt0", attrs=[("x", "0"), ("y", "0")]),
|
|
212
|
+
ElementModel(HC + "pt1", attrs=[("x", str(width)), ("y", "0")]),
|
|
213
|
+
ElementModel(HC + "pt2", attrs=[("x", str(width)), ("y", str(height))]),
|
|
214
|
+
ElementModel(HC + "pt3", attrs=[("x", "0"), ("y", str(height))]),
|
|
215
|
+
])
|
|
216
|
+
_shape_base(rect, int(width), int(height))
|
|
217
|
+
return rect
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def create_picture(binary_item_id_ref: str, width: int, height: int, *, treat_as_char: bool = True) -> ElementModel:
|
|
221
|
+
pic = ElementModel(HP + "pic", attrs=[
|
|
222
|
+
("textWrap", "SQUARE"), ("textFlow", "BOTH_SIDES"), ("reverse", "0")
|
|
223
|
+
])
|
|
224
|
+
_shape_common(pic, int(width), int(height), treat_as_char=treat_as_char)
|
|
225
|
+
pic.set("numberingType", "PICTURE")
|
|
226
|
+
rect = ElementModel(HP + "imgRect")
|
|
227
|
+
rect.children.extend([
|
|
228
|
+
ElementModel(HC + "pt0", attrs=[("x", "0"), ("y", "0")]),
|
|
229
|
+
ElementModel(HC + "pt1", attrs=[("x", str(width)), ("y", "0")]),
|
|
230
|
+
ElementModel(HC + "pt2", attrs=[("x", str(width)), ("y", str(height))]),
|
|
231
|
+
ElementModel(HC + "pt3", attrs=[("x", "0"), ("y", str(height))]),
|
|
232
|
+
])
|
|
233
|
+
pic.children.extend([
|
|
234
|
+
rect,
|
|
235
|
+
ElementModel(HP + "imgClip", attrs=[("left", "0"), ("right", str(width)), ("top", "0"), ("bottom", str(height))]),
|
|
236
|
+
ElementModel(HP + "inMargin", attrs=zero_margin()),
|
|
237
|
+
ElementModel(HP + "imgDim", attrs=[("dimwidth", str(width)), ("dimheight", str(height))]),
|
|
238
|
+
ElementModel(HC + "img", attrs=[
|
|
239
|
+
("binaryItemIDRef", binary_item_id_ref), ("bright", "0"), ("contrast", "0"),
|
|
240
|
+
("effect", "REAL_PIC"), ("alpha", "0"),
|
|
241
|
+
]),
|
|
242
|
+
ElementModel(HP + "effects"),
|
|
243
|
+
])
|
|
244
|
+
_shape_base(pic, int(width), int(height))
|
|
245
|
+
return pic
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
_MEDIA_TYPES = {
|
|
249
|
+
"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", "gif": "image/gif", "bmp": "image/bmp",
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def add_binary_image(document: "Document", data: bytes, image_format: str) -> str:
|
|
254
|
+
fmt = image_format.lower().lstrip(".")
|
|
255
|
+
if fmt not in _MEDIA_TYPES:
|
|
256
|
+
raise ValueError(f"unsupported image format: {image_format}")
|
|
257
|
+
manifest = next((n for n in document.content_model.iter() if local_name(n.tag) == "manifest"), None)
|
|
258
|
+
if manifest is None:
|
|
259
|
+
raise ValueError("content.hpf has no manifest")
|
|
260
|
+
used = {n.get("id") for n in manifest.children if local_name(n.tag) == "item"}
|
|
261
|
+
index = 1
|
|
262
|
+
while f"image{index}" in used or f"BinData/image{index}.{fmt}" in document._package.parts:
|
|
263
|
+
index += 1
|
|
264
|
+
item_id = f"image{index}"
|
|
265
|
+
name = f"BinData/{item_id}.{fmt}"
|
|
266
|
+
manifest.children.append(ElementModel(OPF + "item", attrs=[
|
|
267
|
+
("id", item_id), ("href", name), ("media-type", _MEDIA_TYPES[fmt]), ("isEmbeded", "1")
|
|
268
|
+
]))
|
|
269
|
+
# Use the section entry's metadata conventions for the new ZIP member.
|
|
270
|
+
ref = document._package.infos.get("Contents/section0.xml")
|
|
271
|
+
date_time = ref.date_time if ref is not None else (2026, 1, 1, 0, 0, 0)
|
|
272
|
+
document._package.set(name, bytes(data), PackageEntryInfo(name, zipfile.ZIP_DEFLATED, date_time))
|
|
273
|
+
return item_id
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Thin optional facade over independently usable DOCX and HWPX backends.
|
|
2
|
+
|
|
3
|
+
The common layer deliberately owns no text-replacement algorithm, table
|
|
4
|
+
geometry, XML handling, or backend option normalization. It only selects a
|
|
5
|
+
backend and delegates the shared surface.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ..core.errors import DocumentUnsupportedError, DocumentValueError
|
|
12
|
+
from ..core.contracts import DocumentBackend
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Document:
|
|
16
|
+
"""Thin common wrapper around a standalone backend document."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, backend: DocumentBackend):
|
|
19
|
+
self.backend = backend
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def open(cls, source: str | Path) -> "Document":
|
|
23
|
+
path = Path(source)
|
|
24
|
+
suffix = path.suffix.lower()
|
|
25
|
+
if suffix == ".hwpx":
|
|
26
|
+
from ..hwpx import Document as HwpxDocument
|
|
27
|
+
return cls(HwpxDocument.open(path))
|
|
28
|
+
if suffix == ".docx":
|
|
29
|
+
from ..docx import Document as DocxDocument
|
|
30
|
+
return cls(DocxDocument.open(path))
|
|
31
|
+
raise DocumentValueError(
|
|
32
|
+
f"cannot infer document format from {path.name!r}",
|
|
33
|
+
code="common-format-unknown",
|
|
34
|
+
context={"suffix": suffix},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def new(cls, format: str = "hwpx", *, compatibility: str | None = None) -> "Document":
|
|
39
|
+
fmt = str(format).lower().lstrip(".")
|
|
40
|
+
if fmt == "hwpx":
|
|
41
|
+
from ..hwpx import Document as HwpxDocument
|
|
42
|
+
return cls(HwpxDocument.new(compatibility=compatibility or "hancom2020"))
|
|
43
|
+
if fmt == "docx":
|
|
44
|
+
if compatibility is not None:
|
|
45
|
+
raise DocumentValueError(
|
|
46
|
+
"compatibility profiles are only supported for HWPX",
|
|
47
|
+
code="common-compatibility-not-applicable",
|
|
48
|
+
context={"format": fmt, "compatibility": compatibility},
|
|
49
|
+
)
|
|
50
|
+
from ..docx import Document as DocxDocument
|
|
51
|
+
return cls(DocxDocument.new())
|
|
52
|
+
raise DocumentValueError("format must be 'hwpx' or 'docx'", code="common-format-invalid")
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def rebuild(
|
|
56
|
+
cls,
|
|
57
|
+
source: "Document | str | Path",
|
|
58
|
+
*,
|
|
59
|
+
format: str | None = None,
|
|
60
|
+
preserve_layout: bool = True,
|
|
61
|
+
) -> "Document":
|
|
62
|
+
"""Rebuild *source* through the selected backend.
|
|
63
|
+
|
|
64
|
+
Common only transports the backend-neutral snapshot. Export/import
|
|
65
|
+
semantics and all format fidelity rules remain backend-owned.
|
|
66
|
+
"""
|
|
67
|
+
src = source if isinstance(source, cls) else cls.open(source)
|
|
68
|
+
target = cls.new(format or src.format)
|
|
69
|
+
snapshot = src.backend.export_snapshot(preserve_layout=preserve_layout)
|
|
70
|
+
if src.format == "docx" and target.format == "hwpx":
|
|
71
|
+
raise DocumentUnsupportedError(
|
|
72
|
+
"DOCX to HWPX rebuild is not supported: no verified portable snapshot importer",
|
|
73
|
+
code="rebuild-docx-to-hwpx-unsupported",
|
|
74
|
+
)
|
|
75
|
+
target.backend.import_snapshot(snapshot, preserve_layout=preserve_layout)
|
|
76
|
+
return target
|
|
77
|
+
|
|
78
|
+
def export_snapshot(self, *, preserve_layout: bool = True):
|
|
79
|
+
return self.backend.export_snapshot(preserve_layout=preserve_layout)
|
|
80
|
+
|
|
81
|
+
def rebuild_from(self, source: "Document", *, preserve_layout: bool = True) -> "Document":
|
|
82
|
+
snapshot = source.backend.export_snapshot(preserve_layout=preserve_layout)
|
|
83
|
+
if source.format == "docx" and self.format == "hwpx":
|
|
84
|
+
raise DocumentUnsupportedError(
|
|
85
|
+
"DOCX to HWPX rebuild is not supported: no verified portable snapshot importer",
|
|
86
|
+
code="rebuild-docx-to-hwpx-unsupported",
|
|
87
|
+
)
|
|
88
|
+
self.backend.import_snapshot(snapshot, preserve_layout=preserve_layout)
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def format(self) -> str:
|
|
93
|
+
return self.backend.format
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def native(self) -> Any:
|
|
97
|
+
"""Deep native/expert object (legacy HWPX Document or python-docx Document)."""
|
|
98
|
+
return self.backend.native
|
|
99
|
+
|
|
100
|
+
def unwrap(self) -> Any:
|
|
101
|
+
"""Return the current backend/native object via an explicit escape hatch.
|
|
102
|
+
|
|
103
|
+
Code using this method is backend-specific and therefore has weaker
|
|
104
|
+
compatibility guarantees than the common or format-specific API.
|
|
105
|
+
"""
|
|
106
|
+
return self.native
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def hwpx(self):
|
|
110
|
+
if self.format != "hwpx":
|
|
111
|
+
raise DocumentUnsupportedError(
|
|
112
|
+
"this document uses the DOCX backend; HWPX expert API is unavailable",
|
|
113
|
+
code="common-hwpx-expert-unavailable",
|
|
114
|
+
)
|
|
115
|
+
return self.native
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def docx(self):
|
|
119
|
+
if self.format != "docx":
|
|
120
|
+
raise DocumentUnsupportedError(
|
|
121
|
+
"this document uses the HWPX backend; DOCX expert API is unavailable",
|
|
122
|
+
code="common-docx-expert-unavailable",
|
|
123
|
+
)
|
|
124
|
+
return self.native
|
|
125
|
+
|
|
126
|
+
def __getattr__(self, name: str):
|
|
127
|
+
# Backend implementation remains independent of the small common facade.
|
|
128
|
+
return getattr(self.backend, name)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
UnifiedDocument = Document
|
|
132
|
+
|
|
133
|
+
__all__ = ["Document", "UnifiedDocument"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Compatibility shim for the v0.12 experimental common API import path.
|
|
2
|
+
|
|
3
|
+
The implementation moved to :mod:`pywpdoc._engine.common` in v0.13. The
|
|
4
|
+
common layer is now intentionally thin; all real behavior lives in the
|
|
5
|
+
independently usable DOCX/HWPX backend modules.
|
|
6
|
+
"""
|
|
7
|
+
from .common import Document, UnifiedDocument
|
|
8
|
+
|
|
9
|
+
__all__ = ["Document", "UnifiedDocument"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .units import Length, Pt, Mm, HwpUnit, Cm, Inches
|
|
2
|
+
from .enums import WD_CELL_VERTICAL_ALIGNMENT, WD_ALIGN_VERTICAL, WD_ORIENTATION, WD_ORIENT, WD_STYLE_TYPE
|
|
3
|
+
from .errors import DocumentError, DocumentValueError, DocumentStateError, DocumentUnsupportedError
|
|
4
|
+
from .contracts import DocumentBackend, ParagraphBackend, TableBackend, CellBackend
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Length", "Pt", "Mm", "HwpUnit", "Cm", "Inches",
|
|
8
|
+
"WD_CELL_VERTICAL_ALIGNMENT", "WD_ALIGN_VERTICAL", "WD_ORIENTATION", "WD_ORIENT", "WD_STYLE_TYPE",
|
|
9
|
+
"DocumentError", "DocumentValueError", "DocumentStateError", "DocumentUnsupportedError",
|
|
10
|
+
"DocumentBackend", "ParagraphBackend", "TableBackend", "CellBackend",
|
|
11
|
+
]
|