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.
Files changed (107) hide show
  1. pywpdoc/__init__.py +27 -0
  2. pywpdoc/_engine/__init__.py +26 -0
  3. pywpdoc/_engine/_hwpx_section.py +86 -0
  4. pywpdoc/_engine/_hwpx_story.py +35 -0
  5. pywpdoc/_engine/_hwpx_table.py +37 -0
  6. pywpdoc/_engine/authoring.py +273 -0
  7. pywpdoc/_engine/common/__init__.py +3 -0
  8. pywpdoc/_engine/common/document.py +133 -0
  9. pywpdoc/_engine/common_api.py +9 -0
  10. pywpdoc/_engine/core/__init__.py +11 -0
  11. pywpdoc/_engine/core/contracts.py +153 -0
  12. pywpdoc/_engine/core/defaults.py +18 -0
  13. pywpdoc/_engine/core/enums.py +32 -0
  14. pywpdoc/_engine/core/errors.py +39 -0
  15. pywpdoc/_engine/core/proxy.py +22 -0
  16. pywpdoc/_engine/core/snapshot.py +27 -0
  17. pywpdoc/_engine/core/units.py +54 -0
  18. pywpdoc/_engine/data/default.hwpx +0 -0
  19. pywpdoc/_engine/data/default_2020.hwpx +0 -0
  20. pywpdoc/_engine/document.py +866 -0
  21. pywpdoc/_engine/docx/__init__.py +11 -0
  22. pywpdoc/_engine/docx/backend.py +166 -0
  23. pywpdoc/_engine/docx/comparison_safety.py +79 -0
  24. pywpdoc/_engine/docx/fragments.py +177 -0
  25. pywpdoc/_engine/docx/media.py +105 -0
  26. pywpdoc/_engine/docx/metadata.py +101 -0
  27. pywpdoc/_engine/docx/paragraph.py +126 -0
  28. pywpdoc/_engine/docx/revisions.py +747 -0
  29. pywpdoc/_engine/docx/section.py +111 -0
  30. pywpdoc/_engine/docx/snapshot.py +587 -0
  31. pywpdoc/_engine/docx/story.py +107 -0
  32. pywpdoc/_engine/docx/styles.py +241 -0
  33. pywpdoc/_engine/docx/table.py +303 -0
  34. pywpdoc/_engine/docx/table_geometry.py +85 -0
  35. pywpdoc/_engine/docx/text.py +148 -0
  36. pywpdoc/_engine/docx/traversal.py +81 -0
  37. pywpdoc/_engine/enum/__init__.py +10 -0
  38. pywpdoc/_engine/enum/section.py +3 -0
  39. pywpdoc/_engine/enum/style.py +3 -0
  40. pywpdoc/_engine/enum/table.py +3 -0
  41. pywpdoc/_engine/enum/text.py +40 -0
  42. pywpdoc/_engine/errors.py +36 -0
  43. pywpdoc/_engine/hwpx/__init__.py +16 -0
  44. pywpdoc/_engine/hwpx/backend.py +140 -0
  45. pywpdoc/_engine/hwpx/comparison_safety.py +60 -0
  46. pywpdoc/_engine/hwpx/media.py +72 -0
  47. pywpdoc/_engine/hwpx/metadata.py +92 -0
  48. pywpdoc/_engine/hwpx/paragraph.py +135 -0
  49. pywpdoc/_engine/hwpx/revisions.py +1474 -0
  50. pywpdoc/_engine/hwpx/section.py +53 -0
  51. pywpdoc/_engine/hwpx/snapshot.py +287 -0
  52. pywpdoc/_engine/hwpx/story.py +72 -0
  53. pywpdoc/_engine/hwpx/styles.py +120 -0
  54. pywpdoc/_engine/hwpx/table.py +246 -0
  55. pywpdoc/_engine/hwpx/table_geometry.py +18 -0
  56. pywpdoc/_engine/hwpx/text.py +26 -0
  57. pywpdoc/_engine/metadata.py +190 -0
  58. pywpdoc/_engine/model.py +2796 -0
  59. pywpdoc/_engine/package.py +84 -0
  60. pywpdoc/_engine/py.typed +0 -0
  61. pywpdoc/_engine/reverse.py +129 -0
  62. pywpdoc/_engine/shared.py +8 -0
  63. pywpdoc/_engine/styles.py +1697 -0
  64. pywpdoc/_engine/validation.py +377 -0
  65. pywpdoc/core/__init__.py +19 -0
  66. pywpdoc/core/drawing/__init__.py +18 -0
  67. pywpdoc/core/drawing/model.py +224 -0
  68. pywpdoc/core/structure/__init__.py +4 -0
  69. pywpdoc/core/structure/traversal.py +23 -0
  70. pywpdoc/core/text/__init__.py +13 -0
  71. pywpdoc/core/text/fragments.py +77 -0
  72. pywpdoc/core/text/map.py +90 -0
  73. pywpdoc/core/text/match.py +71 -0
  74. pywpdoc/core/text/mutation.py +63 -0
  75. pywpdoc/core/text/traversal.py +29 -0
  76. pywpdoc/document.py +189 -0
  77. pywpdoc/docx/__init__.py +8 -0
  78. pywpdoc/features/__init__.py +14 -0
  79. pywpdoc/features/compare/__init__.py +11 -0
  80. pywpdoc/features/compare/api.py +747 -0
  81. pywpdoc/features/compare/model.py +124 -0
  82. pywpdoc/features/find/__init__.py +5 -0
  83. pywpdoc/features/find/api.py +55 -0
  84. pywpdoc/features/find/model.py +19 -0
  85. pywpdoc/features/regex/__init__.py +4 -0
  86. pywpdoc/features/regex/api.py +132 -0
  87. pywpdoc/features/replace/__init__.py +5 -0
  88. pywpdoc/features/replace/api.py +72 -0
  89. pywpdoc/features/replace/preview.py +123 -0
  90. pywpdoc/features/template/__init__.py +16 -0
  91. pywpdoc/features/template/api.py +78 -0
  92. pywpdoc/features/template/check.py +308 -0
  93. pywpdoc/features/template/context.py +93 -0
  94. pywpdoc/features/template/model.py +21 -0
  95. pywpdoc/features/template/parser.py +52 -0
  96. pywpdoc/features/template/renderer.py +158 -0
  97. pywpdoc/features/template/sections.py +186 -0
  98. pywpdoc/features/template/structure/__init__.py +11 -0
  99. pywpdoc/features/template/structure/model.py +71 -0
  100. pywpdoc/features/template/structure/parser.py +93 -0
  101. pywpdoc/features/template/structure/rows.py +268 -0
  102. pywpdoc/hwpx/__init__.py +7 -0
  103. pywpdoc-0.44.0rc1.dist-info/METADATA +165 -0
  104. pywpdoc-0.44.0rc1.dist-info/RECORD +107 -0
  105. pywpdoc-0.44.0rc1.dist-info/WHEEL +5 -0
  106. pywpdoc-0.44.0rc1.dist-info/licenses/LICENSE +21 -0
  107. 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,3 @@
1
+ from .document import Document, UnifiedDocument
2
+
3
+ __all__ = ["Document", "UnifiedDocument"]
@@ -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
+ ]