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.
Files changed (53) hide show
  1. aspose/__init__.py +14 -0
  2. aspose/cells/__init__.py +31 -0
  3. aspose/cells/cell.py +350 -0
  4. aspose/cells/constants.py +44 -0
  5. aspose/cells/converters/__init__.py +13 -0
  6. aspose/cells/converters/csv_converter.py +55 -0
  7. aspose/cells/converters/json_converter.py +46 -0
  8. aspose/cells/converters/markdown_converter.py +453 -0
  9. aspose/cells/drawing/__init__.py +17 -0
  10. aspose/cells/drawing/anchor.py +172 -0
  11. aspose/cells/drawing/collection.py +233 -0
  12. aspose/cells/drawing/image.py +338 -0
  13. aspose/cells/formats.py +80 -0
  14. aspose/cells/formula/__init__.py +10 -0
  15. aspose/cells/formula/evaluator.py +360 -0
  16. aspose/cells/formula/functions.py +433 -0
  17. aspose/cells/formula/tokenizer.py +340 -0
  18. aspose/cells/io/__init__.py +27 -0
  19. aspose/cells/io/csv/__init__.py +8 -0
  20. aspose/cells/io/csv/reader.py +88 -0
  21. aspose/cells/io/csv/writer.py +98 -0
  22. aspose/cells/io/factory.py +138 -0
  23. aspose/cells/io/interfaces.py +48 -0
  24. aspose/cells/io/json/__init__.py +8 -0
  25. aspose/cells/io/json/reader.py +126 -0
  26. aspose/cells/io/json/writer.py +119 -0
  27. aspose/cells/io/md/__init__.py +8 -0
  28. aspose/cells/io/md/reader.py +161 -0
  29. aspose/cells/io/md/writer.py +334 -0
  30. aspose/cells/io/models.py +64 -0
  31. aspose/cells/io/xlsx/__init__.py +9 -0
  32. aspose/cells/io/xlsx/constants.py +312 -0
  33. aspose/cells/io/xlsx/image_writer.py +311 -0
  34. aspose/cells/io/xlsx/reader.py +284 -0
  35. aspose/cells/io/xlsx/writer.py +931 -0
  36. aspose/cells/plugins/__init__.py +6 -0
  37. aspose/cells/plugins/docling_backend/__init__.py +7 -0
  38. aspose/cells/plugins/docling_backend/backend.py +535 -0
  39. aspose/cells/plugins/markitdown_plugin/__init__.py +15 -0
  40. aspose/cells/plugins/markitdown_plugin/plugin.py +128 -0
  41. aspose/cells/range.py +210 -0
  42. aspose/cells/style.py +287 -0
  43. aspose/cells/utils/__init__.py +54 -0
  44. aspose/cells/utils/coordinates.py +68 -0
  45. aspose/cells/utils/exceptions.py +43 -0
  46. aspose/cells/utils/validation.py +102 -0
  47. aspose/cells/workbook.py +352 -0
  48. aspose/cells/worksheet.py +670 -0
  49. aspose_cells_foss-25.12.1.dist-info/METADATA +189 -0
  50. aspose_cells_foss-25.12.1.dist-info/RECORD +53 -0
  51. aspose_cells_foss-25.12.1.dist-info/WHEEL +5 -0
  52. aspose_cells_foss-25.12.1.dist-info/entry_points.txt +2 -0
  53. 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