aspose-cells-foss 25.12.1__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.
- aspose/__init__.py +14 -0
- aspose/cells/__init__.py +31 -0
- aspose/cells/cell.py +350 -0
- aspose/cells/constants.py +44 -0
- aspose/cells/converters/__init__.py +13 -0
- aspose/cells/converters/csv_converter.py +55 -0
- aspose/cells/converters/json_converter.py +46 -0
- aspose/cells/converters/markdown_converter.py +453 -0
- aspose/cells/drawing/__init__.py +17 -0
- aspose/cells/drawing/anchor.py +172 -0
- aspose/cells/drawing/collection.py +233 -0
- aspose/cells/drawing/image.py +338 -0
- aspose/cells/formats.py +80 -0
- aspose/cells/formula/__init__.py +10 -0
- aspose/cells/formula/evaluator.py +360 -0
- aspose/cells/formula/functions.py +433 -0
- aspose/cells/formula/tokenizer.py +340 -0
- aspose/cells/io/__init__.py +27 -0
- aspose/cells/io/csv/__init__.py +8 -0
- aspose/cells/io/csv/reader.py +88 -0
- aspose/cells/io/csv/writer.py +98 -0
- aspose/cells/io/factory.py +138 -0
- aspose/cells/io/interfaces.py +48 -0
- aspose/cells/io/json/__init__.py +8 -0
- aspose/cells/io/json/reader.py +126 -0
- aspose/cells/io/json/writer.py +119 -0
- aspose/cells/io/md/__init__.py +8 -0
- aspose/cells/io/md/reader.py +161 -0
- aspose/cells/io/md/writer.py +334 -0
- aspose/cells/io/models.py +64 -0
- aspose/cells/io/xlsx/__init__.py +9 -0
- aspose/cells/io/xlsx/constants.py +312 -0
- aspose/cells/io/xlsx/image_writer.py +311 -0
- aspose/cells/io/xlsx/reader.py +284 -0
- aspose/cells/io/xlsx/writer.py +931 -0
- aspose/cells/plugins/__init__.py +6 -0
- aspose/cells/plugins/docling_backend/__init__.py +7 -0
- aspose/cells/plugins/docling_backend/backend.py +535 -0
- aspose/cells/plugins/markitdown_plugin/__init__.py +15 -0
- aspose/cells/plugins/markitdown_plugin/plugin.py +128 -0
- aspose/cells/range.py +210 -0
- aspose/cells/style.py +287 -0
- aspose/cells/utils/__init__.py +54 -0
- aspose/cells/utils/coordinates.py +68 -0
- aspose/cells/utils/exceptions.py +43 -0
- aspose/cells/utils/validation.py +102 -0
- aspose/cells/workbook.py +352 -0
- aspose/cells/worksheet.py +670 -0
- aspose_cells_foss-25.12.1.dist-info/METADATA +189 -0
- aspose_cells_foss-25.12.1.dist-info/RECORD +53 -0
- aspose_cells_foss-25.12.1.dist-info/WHEEL +5 -0
- aspose_cells_foss-25.12.1.dist-info/entry_points.txt +2 -0
- aspose_cells_foss-25.12.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""XLSX format constants and XML templates."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class XlsxConstants:
|
|
7
|
+
"""Central place for all XLSX-related constants and XML templates."""
|
|
8
|
+
|
|
9
|
+
# XML Namespaces
|
|
10
|
+
NAMESPACES = {
|
|
11
|
+
'main': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
12
|
+
'r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
13
|
+
'pkg': 'http://schemas.openxmlformats.org/package/2006/relationships'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# Content Types
|
|
17
|
+
CONTENT_TYPES = {
|
|
18
|
+
'defaults': [
|
|
19
|
+
("rels", "application/vnd.openxmlformats-package.relationships+xml"),
|
|
20
|
+
("xml", "application/xml")
|
|
21
|
+
],
|
|
22
|
+
'overrides': {
|
|
23
|
+
'workbook': ("/xl/workbook.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"),
|
|
24
|
+
'styles': ("/xl/styles.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"),
|
|
25
|
+
'theme': ("/xl/theme/theme1.xml", "application/vnd.openxmlformats-officedocument.theme+xml"),
|
|
26
|
+
'core_props': ("/docProps/core.xml", "application/vnd.openxmlformats-package.core-properties+xml"),
|
|
27
|
+
'app_props': ("/docProps/app.xml", "application/vnd.openxmlformats-officedocument.extended-properties+xml"),
|
|
28
|
+
'shared_strings': ("/xl/sharedStrings.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"),
|
|
29
|
+
'worksheet': "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Relationship Types
|
|
34
|
+
REL_TYPES = {
|
|
35
|
+
'office_document': "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
|
|
36
|
+
'core_properties': "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
|
|
37
|
+
'extended_properties': "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",
|
|
38
|
+
'worksheet': "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
|
|
39
|
+
'styles': "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
|
|
40
|
+
'theme': "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme",
|
|
41
|
+
'shared_strings': "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
|
|
42
|
+
'hyperlink': "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Built-in Number Formats
|
|
46
|
+
BUILTIN_NUMBER_FORMATS = {
|
|
47
|
+
'General': 0,
|
|
48
|
+
'0': 1,
|
|
49
|
+
'0.00': 2,
|
|
50
|
+
'#,##0': 3,
|
|
51
|
+
'#,##0.00': 4,
|
|
52
|
+
'0%': 9,
|
|
53
|
+
'0.00%': 10,
|
|
54
|
+
'mm/dd/yyyy': 14,
|
|
55
|
+
'$#,##0': 164, # Custom format starting from 164
|
|
56
|
+
'$#,##0.00': 165,
|
|
57
|
+
'0.0%': 166
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Default Style Properties
|
|
61
|
+
DEFAULT_FONT = {
|
|
62
|
+
'name': 'Calibri',
|
|
63
|
+
'size': 11,
|
|
64
|
+
'bold': False,
|
|
65
|
+
'italic': False,
|
|
66
|
+
'color': '000000'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
DEFAULT_FILLS = [
|
|
70
|
+
{'pattern': 'none', 'color': None},
|
|
71
|
+
{'pattern': 'gray125', 'color': None}
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
DEFAULT_BORDER = {
|
|
75
|
+
'left': None, 'right': None, 'top': None, 'bottom': None
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
DEFAULT_CELL_FORMAT = {
|
|
79
|
+
'font_id': 0,
|
|
80
|
+
'fill_id': 0,
|
|
81
|
+
'border_id': 0,
|
|
82
|
+
'number_format_id': 0
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Color Mapping
|
|
86
|
+
COLOR_MAP = {
|
|
87
|
+
'black': '000000',
|
|
88
|
+
'white': 'FFFFFF',
|
|
89
|
+
'red': 'FF0000',
|
|
90
|
+
'green': '00FF00',
|
|
91
|
+
'blue': '0000FF',
|
|
92
|
+
'yellow': 'FFFF00',
|
|
93
|
+
'cyan': '00FFFF',
|
|
94
|
+
'magenta': 'FF00FF',
|
|
95
|
+
'darkblue': '000080',
|
|
96
|
+
'darkgreen': '008000',
|
|
97
|
+
'darkred': '800000',
|
|
98
|
+
'purple': '800080',
|
|
99
|
+
'orange': 'FFA500',
|
|
100
|
+
'gray': '808080',
|
|
101
|
+
'lightblue': 'ADD8E6',
|
|
102
|
+
'lightgreen': '90EE90',
|
|
103
|
+
'lightyellow': 'FFFFE0',
|
|
104
|
+
'lightgray': 'D3D3D3',
|
|
105
|
+
'lightcyan': 'E0FFFF',
|
|
106
|
+
'lightcoral': 'F08080',
|
|
107
|
+
'lightpink': 'FFB6C1',
|
|
108
|
+
'gold': 'FFD700',
|
|
109
|
+
'lavender': 'E6E6FA'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Default Sheet Properties
|
|
113
|
+
SHEET_DEFAULTS = {
|
|
114
|
+
'default_row_height': "15",
|
|
115
|
+
'default_col_width': "10",
|
|
116
|
+
'window_x': "0",
|
|
117
|
+
'window_y': "0",
|
|
118
|
+
'window_width': "14980",
|
|
119
|
+
'window_height': "8580"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Drawing and Image Constants
|
|
123
|
+
DRAWING_NAMESPACES = {
|
|
124
|
+
'xdr': 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing',
|
|
125
|
+
'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
|
|
126
|
+
'a16': 'http://schemas.microsoft.com/office/drawing/2014/main',
|
|
127
|
+
'a14': 'http://schemas.microsoft.com/office/drawing/2010/main'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Image-related constants
|
|
131
|
+
IMAGE_DEFAULTS = {
|
|
132
|
+
'width': 100,
|
|
133
|
+
'height': 100,
|
|
134
|
+
'name_prefix': 'Image',
|
|
135
|
+
'emu_per_pixel': 9525 # English Metric Units per pixel
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Extension URIs used in images
|
|
139
|
+
IMAGE_EXTENSIONS = {
|
|
140
|
+
'creation_id': '{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}',
|
|
141
|
+
'local_dpi': '{28A0092B-C50C-407E-A947-70E740481C1C}'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Image MIME types
|
|
145
|
+
IMAGE_CONTENT_TYPES = {
|
|
146
|
+
'jpg': 'image/jpeg',
|
|
147
|
+
'jpeg': 'image/jpeg',
|
|
148
|
+
'png': 'image/png',
|
|
149
|
+
'gif': 'image/gif'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# XML Attributes and values
|
|
153
|
+
XML_ATTRIBUTES = {
|
|
154
|
+
'edit_as': 'oneCell',
|
|
155
|
+
'no_change_aspect': '1',
|
|
156
|
+
'cstate': 'print',
|
|
157
|
+
'local_dpi_val': '0',
|
|
158
|
+
'transform_xy': '0',
|
|
159
|
+
'preset_geom': 'rect'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class XlsxTemplates:
|
|
164
|
+
"""XML templates for XLSX files."""
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def get_theme_xml() -> str:
|
|
168
|
+
"""Get the complete theme XML template."""
|
|
169
|
+
return '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
170
|
+
<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme">
|
|
171
|
+
<a:themeElements>
|
|
172
|
+
<a:clrScheme name="Office">
|
|
173
|
+
<a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
|
|
174
|
+
<a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
|
|
175
|
+
<a:dk2><a:srgbClr val="1F497D"/></a:dk2>
|
|
176
|
+
<a:lt2><a:srgbClr val="EEECE1"/></a:lt2>
|
|
177
|
+
<a:accent1><a:srgbClr val="4F81BD"/></a:accent1>
|
|
178
|
+
<a:accent2><a:srgbClr val="C0504D"/></a:accent2>
|
|
179
|
+
<a:accent3><a:srgbClr val="9BBB59"/></a:accent3>
|
|
180
|
+
<a:accent4><a:srgbClr val="8064A2"/></a:accent4>
|
|
181
|
+
<a:accent5><a:srgbClr val="4BACC6"/></a:accent5>
|
|
182
|
+
<a:accent6><a:srgbClr val="F79646"/></a:accent6>
|
|
183
|
+
<a:hlink><a:srgbClr val="0000FF"/></a:hlink>
|
|
184
|
+
<a:folHlink><a:srgbClr val="800080"/></a:folHlink>
|
|
185
|
+
</a:clrScheme>
|
|
186
|
+
<a:fontScheme name="Office">
|
|
187
|
+
<a:majorFont>
|
|
188
|
+
<a:latin typeface="Cambria"/>
|
|
189
|
+
<a:ea typeface=""/>
|
|
190
|
+
<a:cs typeface=""/>
|
|
191
|
+
</a:majorFont>
|
|
192
|
+
<a:minorFont>
|
|
193
|
+
<a:latin typeface="Calibri"/>
|
|
194
|
+
<a:ea typeface=""/>
|
|
195
|
+
<a:cs typeface=""/>
|
|
196
|
+
</a:minorFont>
|
|
197
|
+
</a:fontScheme>
|
|
198
|
+
<a:fmtScheme name="Office">
|
|
199
|
+
<a:fillStyleLst>
|
|
200
|
+
<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
|
|
201
|
+
<a:gradFill rotWithShape="1">
|
|
202
|
+
<a:gsLst>
|
|
203
|
+
<a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="50000"/><a:satMod val="300000"/></a:schemeClr></a:gs>
|
|
204
|
+
<a:gs pos="35000"><a:schemeClr val="phClr"><a:tint val="37000"/><a:satMod val="300000"/></a:schemeClr></a:gs>
|
|
205
|
+
<a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="15000"/><a:satMod val="350000"/></a:schemeClr></a:gs>
|
|
206
|
+
</a:gsLst>
|
|
207
|
+
<a:lin ang="16200000" scaled="1"/>
|
|
208
|
+
</a:gradFill>
|
|
209
|
+
<a:gradFill rotWithShape="1">
|
|
210
|
+
<a:gsLst>
|
|
211
|
+
<a:gs pos="0"><a:schemeClr val="phClr"><a:shade val="51000"/><a:satMod val="130000"/></a:schemeClr></a:gs>
|
|
212
|
+
<a:gs pos="80000"><a:schemeClr val="phClr"><a:shade val="93000"/><a:satMod val="130000"/></a:schemeClr></a:gs>
|
|
213
|
+
<a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="94000"/><a:satMod val="135000"/></a:schemeClr></a:gs>
|
|
214
|
+
</a:gsLst>
|
|
215
|
+
<a:lin ang="16200000" scaled="0"/>
|
|
216
|
+
</a:gradFill>
|
|
217
|
+
</a:fillStyleLst>
|
|
218
|
+
<a:lnStyleLst>
|
|
219
|
+
<a:ln w="9525" cap="flat" cmpd="sng" algn="ctr">
|
|
220
|
+
<a:solidFill><a:schemeClr val="phClr"><a:shade val="95000"/><a:satMod val="105000"/></a:schemeClr></a:solidFill>
|
|
221
|
+
<a:prstDash val="solid"/>
|
|
222
|
+
</a:ln>
|
|
223
|
+
<a:ln w="25400" cap="flat" cmpd="sng" algn="ctr">
|
|
224
|
+
<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
|
|
225
|
+
<a:prstDash val="solid"/>
|
|
226
|
+
</a:ln>
|
|
227
|
+
<a:ln w="38100" cap="flat" cmpd="sng" algn="ctr">
|
|
228
|
+
<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
|
|
229
|
+
<a:prstDash val="solid"/>
|
|
230
|
+
</a:ln>
|
|
231
|
+
</a:lnStyleLst>
|
|
232
|
+
<a:effectStyleLst>
|
|
233
|
+
<a:effectStyle>
|
|
234
|
+
<a:effectLst>
|
|
235
|
+
<a:outerShdw blurRad="40000" dist="20000" dir="5400000" rotWithShape="0">
|
|
236
|
+
<a:srgbClr val="000000"><a:alpha val="38000"/></a:srgbClr>
|
|
237
|
+
</a:outerShdw>
|
|
238
|
+
</a:effectLst>
|
|
239
|
+
</a:effectStyle>
|
|
240
|
+
<a:effectStyle>
|
|
241
|
+
<a:effectLst>
|
|
242
|
+
<a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0">
|
|
243
|
+
<a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr>
|
|
244
|
+
</a:outerShdw>
|
|
245
|
+
</a:effectLst>
|
|
246
|
+
</a:effectStyle>
|
|
247
|
+
<a:effectStyle>
|
|
248
|
+
<a:effectLst>
|
|
249
|
+
<a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0">
|
|
250
|
+
<a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr>
|
|
251
|
+
</a:outerShdw>
|
|
252
|
+
</a:effectLst>
|
|
253
|
+
<a:scene3d>
|
|
254
|
+
<a:camera prst="orthographicFront"><a:rot lat="0" lon="0" rev="0"/></a:camera>
|
|
255
|
+
<a:lightRig rig="threePt" dir="t"><a:rot lat="0" lon="0" rev="1200000"/></a:lightRig>
|
|
256
|
+
</a:scene3d>
|
|
257
|
+
<a:sp3d><a:bevelT w="63500" h="25400"/></a:sp3d>
|
|
258
|
+
</a:effectStyle>
|
|
259
|
+
</a:effectStyleLst>
|
|
260
|
+
<a:bgFillStyleLst>
|
|
261
|
+
<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
|
|
262
|
+
<a:gradFill rotWithShape="1">
|
|
263
|
+
<a:gsLst>
|
|
264
|
+
<a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="40000"/><a:satMod val="350000"/></a:schemeClr></a:gs>
|
|
265
|
+
<a:gs pos="40000"><a:schemeClr val="phClr"><a:tint val="45000"/><a:shade val="99000"/><a:satMod val="350000"/></a:schemeClr></a:gs>
|
|
266
|
+
<a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="20000"/><a:satMod val="255000"/></a:schemeClr></a:gs>
|
|
267
|
+
</a:gsLst>
|
|
268
|
+
<a:path path="circle"><a:fillToRect l="50000" t="-80000" r="50000" b="180000"/></a:path>
|
|
269
|
+
</a:gradFill>
|
|
270
|
+
<a:gradFill rotWithShape="1">
|
|
271
|
+
<a:gsLst>
|
|
272
|
+
<a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="80000"/><a:satMod val="300000"/></a:schemeClr></a:gs>
|
|
273
|
+
<a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="30000"/><a:satMod val="200000"/></a:schemeClr></a:gs>
|
|
274
|
+
</a:gsLst>
|
|
275
|
+
<a:path path="circle"><a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path>
|
|
276
|
+
</a:gradFill>
|
|
277
|
+
</a:bgFillStyleLst>
|
|
278
|
+
</a:fmtScheme>
|
|
279
|
+
</a:themeElements>
|
|
280
|
+
<a:objectDefaults/>
|
|
281
|
+
<a:extraClrSchemeLst/>
|
|
282
|
+
</a:theme>'''
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def get_app_properties_data() -> Dict[str, str]:
|
|
286
|
+
"""Get application properties data."""
|
|
287
|
+
return {
|
|
288
|
+
'application': 'Aspose.Cells.Python',
|
|
289
|
+
'doc_security': '0',
|
|
290
|
+
'links_up_to_date': 'false',
|
|
291
|
+
'shared_doc': 'false',
|
|
292
|
+
'hyperlinks_changed': 'false',
|
|
293
|
+
'app_version': '16.0300'
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def get_core_properties_data() -> Dict[str, str]:
|
|
298
|
+
"""Get core properties data."""
|
|
299
|
+
return {
|
|
300
|
+
'creator': 'Aspose.Cells.Python',
|
|
301
|
+
'modified': '2024-07-20T14:00:00Z',
|
|
302
|
+
'created': '2024-07-20T14:00:00Z'
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
@staticmethod
|
|
306
|
+
def get_rels_data():
|
|
307
|
+
"""Get main relationships data."""
|
|
308
|
+
return [
|
|
309
|
+
("rId1", XlsxConstants.REL_TYPES['office_document'], "xl/workbook.xml"),
|
|
310
|
+
("rId2", XlsxConstants.REL_TYPES['core_properties'], "docProps/core.xml"),
|
|
311
|
+
("rId3", XlsxConstants.REL_TYPES['extended_properties'], "docProps/app.xml")
|
|
312
|
+
]
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""
|
|
2
|
+
XLSX Image Writer - Handles image embedding in Excel files
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import xml.etree.ElementTree as ET
|
|
6
|
+
from typing import Dict, List, Optional, Tuple
|
|
7
|
+
import base64
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ...drawing import Image, ImageFormat, ImageCollection, Anchor, AnchorType
|
|
11
|
+
from ...utils import tuple_to_coordinate
|
|
12
|
+
from .constants import XlsxConstants
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ImageWriter:
|
|
16
|
+
"""Handles writing images to XLSX format following OOXML specifications."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.image_counter = 0
|
|
20
|
+
self.relationship_counter = 0
|
|
21
|
+
self.image_files: Dict[str, bytes] = {} # path -> data
|
|
22
|
+
self.image_relationships: List[Dict] = [] # relationship info
|
|
23
|
+
|
|
24
|
+
def add_image(self, image: Image) -> str:
|
|
25
|
+
"""Add image and return its relationship ID."""
|
|
26
|
+
self.image_counter += 1
|
|
27
|
+
self.relationship_counter += 1
|
|
28
|
+
|
|
29
|
+
# Generate paths
|
|
30
|
+
rel_id = f"rId{self.relationship_counter}"
|
|
31
|
+
image_filename = f"image{self.image_counter}.{image.format.value}"
|
|
32
|
+
image_path = f"../media/{image_filename}"
|
|
33
|
+
|
|
34
|
+
# Store image data - ensure it's bytes
|
|
35
|
+
image_data = image.data
|
|
36
|
+
if isinstance(image_data, str):
|
|
37
|
+
# If data is base64 string, decode it
|
|
38
|
+
try:
|
|
39
|
+
image_data = base64.b64decode(image_data)
|
|
40
|
+
except Exception:
|
|
41
|
+
# If not base64, treat as raw string and encode
|
|
42
|
+
image_data = image_data.encode('utf-8')
|
|
43
|
+
self.image_files[f"xl/media/{image_filename}"] = image_data
|
|
44
|
+
|
|
45
|
+
# Store relationship info
|
|
46
|
+
self.image_relationships.append({
|
|
47
|
+
'id': rel_id,
|
|
48
|
+
'type': f"{XlsxConstants.REL_TYPES['worksheet'].rsplit('/', 1)[0]}/image",
|
|
49
|
+
'target': image_path
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return rel_id
|
|
53
|
+
|
|
54
|
+
def create_drawing_xml(self, images: List[Image]) -> str:
|
|
55
|
+
"""Create drawing XML for worksheet images."""
|
|
56
|
+
if not images:
|
|
57
|
+
return ""
|
|
58
|
+
|
|
59
|
+
# Create drawing XML structure - match OpenCells/Excel standard exactly
|
|
60
|
+
drawing = ET.Element('xdr:wsDr')
|
|
61
|
+
drawing.set('xmlns:xdr', XlsxConstants.DRAWING_NAMESPACES['xdr'])
|
|
62
|
+
drawing.set('xmlns:a', XlsxConstants.DRAWING_NAMESPACES['a'])
|
|
63
|
+
# Note: r namespace declared locally in each blip element (OpenCells standard)
|
|
64
|
+
|
|
65
|
+
for image in images:
|
|
66
|
+
rel_id = self.add_image(image)
|
|
67
|
+
anchor_elem = self._create_anchor_element(image, rel_id)
|
|
68
|
+
drawing.append(anchor_elem)
|
|
69
|
+
|
|
70
|
+
# Format XML with proper declaration and indentation
|
|
71
|
+
self._indent_xml(drawing)
|
|
72
|
+
xml_str = ET.tostring(drawing, encoding='unicode')
|
|
73
|
+
return f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n{xml_str}'
|
|
74
|
+
|
|
75
|
+
def _create_anchor_element(self, image: Image, rel_id: str) -> ET.Element:
|
|
76
|
+
"""Create anchor element for image based on its positioning type."""
|
|
77
|
+
anchor = image.anchor
|
|
78
|
+
|
|
79
|
+
if anchor.type == AnchorType.ONE_CELL:
|
|
80
|
+
return self._create_one_cell_anchor(image, rel_id)
|
|
81
|
+
elif anchor.type == AnchorType.TWO_CELL:
|
|
82
|
+
return self._create_two_cell_anchor(image, rel_id)
|
|
83
|
+
elif anchor.type == AnchorType.ABSOLUTE:
|
|
84
|
+
return self._create_absolute_anchor(image, rel_id)
|
|
85
|
+
else:
|
|
86
|
+
# Default to two cell anchor (Excel standard for images)
|
|
87
|
+
return self._create_two_cell_anchor(image, rel_id)
|
|
88
|
+
|
|
89
|
+
def _create_one_cell_anchor(self, image: Image, rel_id: str) -> ET.Element:
|
|
90
|
+
"""Create one-cell anchor element."""
|
|
91
|
+
anchor_elem = ET.Element('xdr:oneCellAnchor')
|
|
92
|
+
|
|
93
|
+
# From position
|
|
94
|
+
from_elem = ET.SubElement(anchor_elem, 'xdr:from')
|
|
95
|
+
from_row, from_col = image.anchor.from_position
|
|
96
|
+
|
|
97
|
+
ET.SubElement(from_elem, 'xdr:col').text = str(from_col)
|
|
98
|
+
ET.SubElement(from_elem, 'xdr:colOff').text = str(getattr(image.anchor, 'from_offset', (0, 0))[0] * XlsxConstants.IMAGE_DEFAULTS['emu_per_pixel'])
|
|
99
|
+
ET.SubElement(from_elem, 'xdr:row').text = str(from_row)
|
|
100
|
+
ET.SubElement(from_elem, 'xdr:rowOff').text = str(getattr(image.anchor, 'from_offset', (0, 0))[1] * XlsxConstants.IMAGE_DEFAULTS['emu_per_pixel'])
|
|
101
|
+
|
|
102
|
+
# Extent (size) - EMU units (English Metric Units)
|
|
103
|
+
ext_elem = ET.SubElement(anchor_elem, 'xdr:ext')
|
|
104
|
+
width_emu = int((image.width or XlsxConstants.IMAGE_DEFAULTS['width']) * XlsxConstants.IMAGE_DEFAULTS['emu_per_pixel'])
|
|
105
|
+
height_emu = int((image.height or XlsxConstants.IMAGE_DEFAULTS['height']) * XlsxConstants.IMAGE_DEFAULTS['emu_per_pixel'])
|
|
106
|
+
ET.SubElement(ext_elem, 'xdr:cx').text = str(width_emu)
|
|
107
|
+
ET.SubElement(ext_elem, 'xdr:cy').text = str(height_emu)
|
|
108
|
+
|
|
109
|
+
# Picture
|
|
110
|
+
pic_elem = self._create_picture_element(image, rel_id)
|
|
111
|
+
anchor_elem.append(pic_elem)
|
|
112
|
+
|
|
113
|
+
# Client data
|
|
114
|
+
client_data = ET.SubElement(anchor_elem, 'xdr:clientData')
|
|
115
|
+
|
|
116
|
+
return anchor_elem
|
|
117
|
+
|
|
118
|
+
def _create_two_cell_anchor(self, image: Image, rel_id: str) -> ET.Element:
|
|
119
|
+
"""Create two-cell anchor element."""
|
|
120
|
+
anchor_elem = ET.Element('xdr:twoCellAnchor')
|
|
121
|
+
# Add editAs attribute like Excel standard
|
|
122
|
+
anchor_elem.set('editAs', XlsxConstants.XML_ATTRIBUTES['edit_as'])
|
|
123
|
+
|
|
124
|
+
# From position
|
|
125
|
+
from_elem = ET.SubElement(anchor_elem, 'xdr:from')
|
|
126
|
+
from_row, from_col = image.anchor.from_position
|
|
127
|
+
|
|
128
|
+
ET.SubElement(from_elem, 'xdr:col').text = str(from_col)
|
|
129
|
+
ET.SubElement(from_elem, 'xdr:colOff').text = str(getattr(image.anchor, 'from_offset', (0, 0))[0] * XlsxConstants.IMAGE_DEFAULTS['emu_per_pixel'])
|
|
130
|
+
ET.SubElement(from_elem, 'xdr:row').text = str(from_row)
|
|
131
|
+
ET.SubElement(from_elem, 'xdr:rowOff').text = str(getattr(image.anchor, 'from_offset', (0, 0))[1] * XlsxConstants.IMAGE_DEFAULTS['emu_per_pixel'])
|
|
132
|
+
|
|
133
|
+
# To position
|
|
134
|
+
to_elem = ET.SubElement(anchor_elem, 'xdr:to')
|
|
135
|
+
if image.anchor.to_position:
|
|
136
|
+
to_row, to_col = image.anchor.to_position
|
|
137
|
+
to_col_off, to_row_off = image.anchor.to_offset
|
|
138
|
+
else:
|
|
139
|
+
# Calculate end position based on size
|
|
140
|
+
to_row = from_row + 5 # Default span
|
|
141
|
+
to_col = from_col + 3
|
|
142
|
+
to_col_off, to_row_off = (0, 0)
|
|
143
|
+
|
|
144
|
+
ET.SubElement(to_elem, 'xdr:col').text = str(to_col)
|
|
145
|
+
ET.SubElement(to_elem, 'xdr:colOff').text = str(int(to_col_off * XlsxConstants.IMAGE_DEFAULTS['emu_per_pixel']))
|
|
146
|
+
ET.SubElement(to_elem, 'xdr:row').text = str(to_row)
|
|
147
|
+
ET.SubElement(to_elem, 'xdr:rowOff').text = str(int(to_row_off * XlsxConstants.IMAGE_DEFAULTS['emu_per_pixel']))
|
|
148
|
+
|
|
149
|
+
# Picture
|
|
150
|
+
pic_elem = self._create_picture_element(image, rel_id)
|
|
151
|
+
anchor_elem.append(pic_elem)
|
|
152
|
+
|
|
153
|
+
# Client data
|
|
154
|
+
client_data = ET.SubElement(anchor_elem, 'xdr:clientData')
|
|
155
|
+
|
|
156
|
+
return anchor_elem
|
|
157
|
+
|
|
158
|
+
def _create_absolute_anchor(self, image: Image, rel_id: str) -> ET.Element:
|
|
159
|
+
"""Create absolute anchor element."""
|
|
160
|
+
anchor_elem = ET.Element('xdr:absoluteAnchor')
|
|
161
|
+
|
|
162
|
+
# Position
|
|
163
|
+
pos_elem = ET.SubElement(anchor_elem, 'xdr:pos')
|
|
164
|
+
abs_pos = getattr(image.anchor, 'absolute_position', (0, 0)) or (0, 0)
|
|
165
|
+
ET.SubElement(pos_elem, 'xdr:x').text = str(int(abs_pos[0] * XlsxConstants.IMAGE_DEFAULTS['emu_per_pixel']))
|
|
166
|
+
ET.SubElement(pos_elem, 'xdr:y').text = str(int(abs_pos[1] * XlsxConstants.IMAGE_DEFAULTS['emu_per_pixel']))
|
|
167
|
+
|
|
168
|
+
# Extent (size)
|
|
169
|
+
ext_elem = ET.SubElement(anchor_elem, 'xdr:ext')
|
|
170
|
+
width_emu = int((image.width or XlsxConstants.IMAGE_DEFAULTS['width']) * XlsxConstants.IMAGE_DEFAULTS['emu_per_pixel'])
|
|
171
|
+
height_emu = int((image.height or XlsxConstants.IMAGE_DEFAULTS['height']) * XlsxConstants.IMAGE_DEFAULTS['emu_per_pixel'])
|
|
172
|
+
ET.SubElement(ext_elem, 'xdr:cx').text = str(width_emu)
|
|
173
|
+
ET.SubElement(ext_elem, 'xdr:cy').text = str(height_emu)
|
|
174
|
+
|
|
175
|
+
# Picture
|
|
176
|
+
pic_elem = self._create_picture_element(image, rel_id)
|
|
177
|
+
anchor_elem.append(pic_elem)
|
|
178
|
+
|
|
179
|
+
# Client data
|
|
180
|
+
client_data = ET.SubElement(anchor_elem, 'xdr:clientData')
|
|
181
|
+
|
|
182
|
+
return anchor_elem
|
|
183
|
+
|
|
184
|
+
def _create_picture_element(self, image: Image, rel_id: str) -> ET.Element:
|
|
185
|
+
"""Create picture element with all required sub-elements."""
|
|
186
|
+
pic_elem = ET.Element('xdr:pic')
|
|
187
|
+
|
|
188
|
+
# Non-visual picture properties
|
|
189
|
+
nv_pic_pr = ET.SubElement(pic_elem, 'xdr:nvPicPr')
|
|
190
|
+
|
|
191
|
+
# Non-visual drawing properties
|
|
192
|
+
c_nv_pr = ET.SubElement(nv_pic_pr, 'xdr:cNvPr')
|
|
193
|
+
c_nv_pr.set('id', str(self.image_counter))
|
|
194
|
+
c_nv_pr.set('name', image.name or f'{XlsxConstants.IMAGE_DEFAULTS["name_prefix"]} {self.image_counter}')
|
|
195
|
+
|
|
196
|
+
# CRITICAL: Add extension list with creation ID (required by Excel)
|
|
197
|
+
ext_lst = ET.SubElement(c_nv_pr, 'a:extLst')
|
|
198
|
+
ext = ET.SubElement(ext_lst, 'a:ext')
|
|
199
|
+
ext.set('uri', XlsxConstants.IMAGE_EXTENSIONS['creation_id'])
|
|
200
|
+
|
|
201
|
+
# Add creation ID
|
|
202
|
+
creation_id = ET.SubElement(ext, 'a16:creationId')
|
|
203
|
+
creation_id.set('xmlns:a16', XlsxConstants.DRAWING_NAMESPACES['a16'])
|
|
204
|
+
# Generate a unique GUID for this image
|
|
205
|
+
import uuid
|
|
206
|
+
creation_id.set('id', '{' + str(uuid.uuid4()).upper() + '}')
|
|
207
|
+
|
|
208
|
+
# Non-visual picture drawing properties
|
|
209
|
+
c_nv_pic_pr = ET.SubElement(nv_pic_pr, 'xdr:cNvPicPr')
|
|
210
|
+
# Add picture locks for image protection - minimal attributes for Excel compatibility
|
|
211
|
+
pic_locks = ET.SubElement(c_nv_pic_pr, 'a:picLocks')
|
|
212
|
+
pic_locks.set('noChangeAspect', XlsxConstants.XML_ATTRIBUTES['no_change_aspect'])
|
|
213
|
+
|
|
214
|
+
# Blip fill
|
|
215
|
+
blip_fill = ET.SubElement(pic_elem, 'xdr:blipFill')
|
|
216
|
+
blip = ET.SubElement(blip_fill, 'a:blip')
|
|
217
|
+
# CRITICAL: Use r:embed with local r namespace declaration like OpenCells
|
|
218
|
+
blip.set('xmlns:r', XlsxConstants.NAMESPACES['r'])
|
|
219
|
+
blip.set('r:embed', rel_id) # Reference to image relationship
|
|
220
|
+
blip.set('cstate', XlsxConstants.XML_ATTRIBUTES['cstate'])
|
|
221
|
+
|
|
222
|
+
# CRITICAL: Add extension list with useLocalDpi (required by Excel)
|
|
223
|
+
blip_ext_lst = ET.SubElement(blip, 'a:extLst')
|
|
224
|
+
blip_ext = ET.SubElement(blip_ext_lst, 'a:ext')
|
|
225
|
+
blip_ext.set('uri', XlsxConstants.IMAGE_EXTENSIONS['local_dpi'])
|
|
226
|
+
|
|
227
|
+
# Add useLocalDpi
|
|
228
|
+
use_local_dpi = ET.SubElement(blip_ext, 'a14:useLocalDpi')
|
|
229
|
+
use_local_dpi.set('xmlns:a14', XlsxConstants.DRAWING_NAMESPACES['a14'])
|
|
230
|
+
use_local_dpi.set('val', XlsxConstants.XML_ATTRIBUTES['local_dpi_val'])
|
|
231
|
+
|
|
232
|
+
# Stretch
|
|
233
|
+
stretch = ET.SubElement(blip_fill, 'a:stretch')
|
|
234
|
+
fill_rect = ET.SubElement(stretch, 'a:fillRect')
|
|
235
|
+
|
|
236
|
+
# Shape properties
|
|
237
|
+
sp_pr = ET.SubElement(pic_elem, 'xdr:spPr')
|
|
238
|
+
|
|
239
|
+
# Transform
|
|
240
|
+
xfrm = ET.SubElement(sp_pr, 'a:xfrm')
|
|
241
|
+
off = ET.SubElement(xfrm, 'a:off')
|
|
242
|
+
off.set('x', XlsxConstants.XML_ATTRIBUTES['transform_xy'])
|
|
243
|
+
off.set('y', XlsxConstants.XML_ATTRIBUTES['transform_xy'])
|
|
244
|
+
ext = ET.SubElement(xfrm, 'a:ext')
|
|
245
|
+
width_emu = int((image.width or XlsxConstants.IMAGE_DEFAULTS['width']) * XlsxConstants.IMAGE_DEFAULTS['emu_per_pixel'])
|
|
246
|
+
height_emu = int((image.height or XlsxConstants.IMAGE_DEFAULTS['height']) * XlsxConstants.IMAGE_DEFAULTS['emu_per_pixel'])
|
|
247
|
+
ext.set('cx', str(width_emu))
|
|
248
|
+
ext.set('cy', str(height_emu))
|
|
249
|
+
|
|
250
|
+
# Preset geometry - rectangle shape for image
|
|
251
|
+
prst_geom = ET.SubElement(sp_pr, 'a:prstGeom')
|
|
252
|
+
prst_geom.set('prst', XlsxConstants.XML_ATTRIBUTES['preset_geom'])
|
|
253
|
+
av_lst = ET.SubElement(prst_geom, 'a:avLst')
|
|
254
|
+
|
|
255
|
+
# Remove line element for images - can cause Excel validation issues
|
|
256
|
+
|
|
257
|
+
return pic_elem
|
|
258
|
+
|
|
259
|
+
def create_drawing_rels_xml(self) -> str:
|
|
260
|
+
"""Create drawing relationships XML."""
|
|
261
|
+
if not self.image_relationships:
|
|
262
|
+
return ""
|
|
263
|
+
|
|
264
|
+
relationships = ET.Element('Relationships')
|
|
265
|
+
relationships.set('xmlns', XlsxConstants.NAMESPACES['pkg'])
|
|
266
|
+
|
|
267
|
+
for rel in self.image_relationships:
|
|
268
|
+
relationship = ET.SubElement(relationships, 'Relationship')
|
|
269
|
+
relationship.set('Id', rel['id'])
|
|
270
|
+
relationship.set('Type', rel['type'])
|
|
271
|
+
relationship.set('Target', rel['target'])
|
|
272
|
+
|
|
273
|
+
# Format XML with proper declaration and indentation
|
|
274
|
+
self._indent_xml(relationships)
|
|
275
|
+
xml_str = ET.tostring(relationships, encoding='unicode')
|
|
276
|
+
return f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n{xml_str}'
|
|
277
|
+
|
|
278
|
+
def get_image_files(self) -> Dict[str, bytes]:
|
|
279
|
+
"""Get all image files that need to be written to the archive."""
|
|
280
|
+
return self.image_files.copy()
|
|
281
|
+
|
|
282
|
+
def get_content_types_entries(self) -> List[Tuple[str, str]]:
|
|
283
|
+
"""Get content types entries for images."""
|
|
284
|
+
entries = []
|
|
285
|
+
for path in self.image_files.keys():
|
|
286
|
+
filename = Path(path).name
|
|
287
|
+
if filename.endswith('.png'):
|
|
288
|
+
entries.append(('png', XlsxConstants.IMAGE_CONTENT_TYPES['png']))
|
|
289
|
+
elif filename.endswith(('.jpg', '.jpeg')):
|
|
290
|
+
entries.append(('jpeg', XlsxConstants.IMAGE_CONTENT_TYPES['jpeg']))
|
|
291
|
+
elif filename.endswith('.gif'):
|
|
292
|
+
entries.append(('gif', XlsxConstants.IMAGE_CONTENT_TYPES['gif']))
|
|
293
|
+
|
|
294
|
+
# Remove duplicates
|
|
295
|
+
return list(set(entries))
|
|
296
|
+
|
|
297
|
+
def _indent_xml(self, elem, level=0):
|
|
298
|
+
"""Add proper indentation to XML for readability."""
|
|
299
|
+
i = "\n" + level * " "
|
|
300
|
+
if len(elem):
|
|
301
|
+
if not elem.text or not elem.text.strip():
|
|
302
|
+
elem.text = i + " "
|
|
303
|
+
if not elem.tail or not elem.tail.strip():
|
|
304
|
+
elem.tail = i
|
|
305
|
+
for child in elem:
|
|
306
|
+
self._indent_xml(child, level + 1)
|
|
307
|
+
if not child.tail or not child.tail.strip():
|
|
308
|
+
child.tail = i
|
|
309
|
+
else:
|
|
310
|
+
if level and (not elem.tail or not elem.tail.strip()):
|
|
311
|
+
elem.tail = i
|