lybic-guiagents 0.1.0__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.

Potentially problematic release.


This version of lybic-guiagents might be problematic. Click here for more details.

Files changed (85) hide show
  1. desktop_env/__init__.py +1 -0
  2. desktop_env/actions.py +203 -0
  3. desktop_env/controllers/__init__.py +0 -0
  4. desktop_env/controllers/python.py +471 -0
  5. desktop_env/controllers/setup.py +882 -0
  6. desktop_env/desktop_env.py +509 -0
  7. desktop_env/evaluators/__init__.py +5 -0
  8. desktop_env/evaluators/getters/__init__.py +41 -0
  9. desktop_env/evaluators/getters/calc.py +15 -0
  10. desktop_env/evaluators/getters/chrome.py +1774 -0
  11. desktop_env/evaluators/getters/file.py +154 -0
  12. desktop_env/evaluators/getters/general.py +42 -0
  13. desktop_env/evaluators/getters/gimp.py +38 -0
  14. desktop_env/evaluators/getters/impress.py +126 -0
  15. desktop_env/evaluators/getters/info.py +24 -0
  16. desktop_env/evaluators/getters/misc.py +406 -0
  17. desktop_env/evaluators/getters/replay.py +20 -0
  18. desktop_env/evaluators/getters/vlc.py +86 -0
  19. desktop_env/evaluators/getters/vscode.py +35 -0
  20. desktop_env/evaluators/metrics/__init__.py +160 -0
  21. desktop_env/evaluators/metrics/basic_os.py +68 -0
  22. desktop_env/evaluators/metrics/chrome.py +493 -0
  23. desktop_env/evaluators/metrics/docs.py +1011 -0
  24. desktop_env/evaluators/metrics/general.py +665 -0
  25. desktop_env/evaluators/metrics/gimp.py +637 -0
  26. desktop_env/evaluators/metrics/libreoffice.py +28 -0
  27. desktop_env/evaluators/metrics/others.py +92 -0
  28. desktop_env/evaluators/metrics/pdf.py +31 -0
  29. desktop_env/evaluators/metrics/slides.py +957 -0
  30. desktop_env/evaluators/metrics/table.py +585 -0
  31. desktop_env/evaluators/metrics/thunderbird.py +176 -0
  32. desktop_env/evaluators/metrics/utils.py +719 -0
  33. desktop_env/evaluators/metrics/vlc.py +524 -0
  34. desktop_env/evaluators/metrics/vscode.py +283 -0
  35. desktop_env/providers/__init__.py +35 -0
  36. desktop_env/providers/aws/__init__.py +0 -0
  37. desktop_env/providers/aws/manager.py +278 -0
  38. desktop_env/providers/aws/provider.py +186 -0
  39. desktop_env/providers/aws/provider_with_proxy.py +315 -0
  40. desktop_env/providers/aws/proxy_pool.py +193 -0
  41. desktop_env/providers/azure/__init__.py +0 -0
  42. desktop_env/providers/azure/manager.py +87 -0
  43. desktop_env/providers/azure/provider.py +207 -0
  44. desktop_env/providers/base.py +97 -0
  45. desktop_env/providers/gcp/__init__.py +0 -0
  46. desktop_env/providers/gcp/manager.py +0 -0
  47. desktop_env/providers/gcp/provider.py +0 -0
  48. desktop_env/providers/virtualbox/__init__.py +0 -0
  49. desktop_env/providers/virtualbox/manager.py +463 -0
  50. desktop_env/providers/virtualbox/provider.py +124 -0
  51. desktop_env/providers/vmware/__init__.py +0 -0
  52. desktop_env/providers/vmware/manager.py +455 -0
  53. desktop_env/providers/vmware/provider.py +105 -0
  54. gui_agents/__init__.py +0 -0
  55. gui_agents/agents/Action.py +209 -0
  56. gui_agents/agents/__init__.py +0 -0
  57. gui_agents/agents/agent_s.py +832 -0
  58. gui_agents/agents/global_state.py +610 -0
  59. gui_agents/agents/grounding.py +651 -0
  60. gui_agents/agents/hardware_interface.py +129 -0
  61. gui_agents/agents/manager.py +568 -0
  62. gui_agents/agents/translator.py +132 -0
  63. gui_agents/agents/worker.py +355 -0
  64. gui_agents/cli_app.py +560 -0
  65. gui_agents/core/__init__.py +0 -0
  66. gui_agents/core/engine.py +1496 -0
  67. gui_agents/core/knowledge.py +449 -0
  68. gui_agents/core/mllm.py +555 -0
  69. gui_agents/tools/__init__.py +0 -0
  70. gui_agents/tools/tools.py +727 -0
  71. gui_agents/unit_test/__init__.py +0 -0
  72. gui_agents/unit_test/run_tests.py +65 -0
  73. gui_agents/unit_test/test_manager.py +330 -0
  74. gui_agents/unit_test/test_worker.py +269 -0
  75. gui_agents/utils/__init__.py +0 -0
  76. gui_agents/utils/analyze_display.py +301 -0
  77. gui_agents/utils/common_utils.py +263 -0
  78. gui_agents/utils/display_viewer.py +281 -0
  79. gui_agents/utils/embedding_manager.py +53 -0
  80. gui_agents/utils/image_axis_utils.py +27 -0
  81. lybic_guiagents-0.1.0.dist-info/METADATA +416 -0
  82. lybic_guiagents-0.1.0.dist-info/RECORD +85 -0
  83. lybic_guiagents-0.1.0.dist-info/WHEEL +5 -0
  84. lybic_guiagents-0.1.0.dist-info/licenses/LICENSE +201 -0
  85. lybic_guiagents-0.1.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,957 @@
1
+ import logging
2
+ import xml.etree.ElementTree as ET
3
+ import zipfile
4
+ from math import sqrt
5
+
6
+ from pptx import Presentation
7
+ from pptx.util import Inches
8
+ from pptx.enum.shapes import MSO_SHAPE_TYPE
9
+
10
+ logger = logging.getLogger("desktopenv.metric.slides")
11
+
12
+ # Add a new logger specifically for debugging PPTX comparisons
13
+ debug_logger = logging.getLogger("desktopenv.metric.slides.debug")
14
+
15
+ def enable_debug_logging():
16
+ """Enable debug logging for PPTX comparison"""
17
+ debug_logger.setLevel(logging.DEBUG)
18
+ if not debug_logger.handlers:
19
+ handler = logging.StreamHandler()
20
+ handler.setLevel(logging.DEBUG)
21
+ formatter = logging.Formatter('[PPTX_DEBUG] %(message)s')
22
+ handler.setFormatter(formatter)
23
+ debug_logger.addHandler(handler)
24
+
25
+ # Add debug logger for detailed comparison output
26
+ debug_logger = logging.getLogger("desktopenv.metric.slides.debug")
27
+
28
+ def enable_debug_logging():
29
+ """Enable detailed debug logging for PPTX comparison"""
30
+ debug_logger.setLevel(logging.DEBUG)
31
+ if not debug_logger.handlers:
32
+ handler = logging.StreamHandler()
33
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
34
+ handler.setFormatter(formatter)
35
+ debug_logger.addHandler(handler)
36
+
37
+
38
+ def check_presenter_console_disable(config_file_path):
39
+ try:
40
+ tree = ET.parse(config_file_path)
41
+ root = tree.getroot()
42
+
43
+ namespaces = {
44
+ 'oor': 'http://openoffice.org/2001/registry'
45
+ }
46
+
47
+ for item in root.findall(
48
+ ".//item[@oor:path='/org.openoffice.Office.Impress/Misc/Start']/prop[@oor:name='EnablePresenterScreen']",
49
+ namespaces):
50
+ # Check if the value of the configuration item indicates that the presenter console has been disabled
51
+ presenter_screen_enabled = item.find('value').text
52
+ if presenter_screen_enabled.lower() == 'false':
53
+ return 1.
54
+ else:
55
+ return 0.
56
+ return 0.
57
+ except Exception as e:
58
+ logger.error(f"Error: {e}")
59
+ return 0.
60
+
61
+
62
+ def check_image_stretch_and_center(modified_ppt, original_ppt):
63
+ # fixme: this func is overfit to this example libreoffice_impress
64
+ # Load the presentations
65
+ original_pres = Presentation(original_ppt)
66
+ modified_pres = Presentation(modified_ppt)
67
+
68
+ # Get the first slide of each presentation
69
+ original_slide = original_pres.slides[0]
70
+ modified_slide = modified_pres.slides[0]
71
+
72
+ # Get the image on the first slide of each presentation
73
+ original_slide_images = [shape for shape in original_slide.shapes if shape.shape_type == 13]
74
+ modified_slide_images = [shape for shape in modified_slide.shapes if shape.shape_type == 13]
75
+
76
+ if not original_slide_images:
77
+ return 0.
78
+
79
+ the_image = original_slide_images[0]
80
+
81
+ the_modified_image = None
82
+
83
+ # Get the images that modified in width and height
84
+ for modified_image in modified_slide_images:
85
+ if the_image.image.blob == modified_image.image.blob:
86
+ the_modified_image = modified_image
87
+
88
+ if the_modified_image is None:
89
+ return 0.
90
+
91
+ if (abs(the_modified_image.width - original_pres.slide_width) > Inches(0.5) or
92
+ abs(the_modified_image.height - original_pres.slide_height) > Inches(0.5) or
93
+ abs(the_modified_image.left - (original_pres.slide_width - the_modified_image.width) / 2) > Inches(0.5) or
94
+ abs(the_modified_image.top - (original_pres.slide_height - the_modified_image.height) / 2) > Inches(0.5)):
95
+ return 0.
96
+
97
+ return 1.
98
+
99
+
100
+ def is_red_color(color):
101
+ # judge if the color is red
102
+ return color and color.rgb == (255, 0, 0)
103
+
104
+
105
+ def get_master_placeholder_color(prs):
106
+ # get the color of the placeholder
107
+ masters = prs.slide_masters
108
+ for idx, master in enumerate(masters):
109
+ for placeholder in master.placeholders:
110
+ if placeholder.has_text_frame and placeholder.text == "<number>":
111
+ text_frame = placeholder.text_frame
112
+
113
+ if text_frame.paragraphs:
114
+ first_paragraph = text_frame.paragraphs[0]
115
+ return first_paragraph.font.color
116
+ return None
117
+
118
+
119
+ def check_slide_numbers_color(pptx_file_path):
120
+ presentation = Presentation(pptx_file_path)
121
+
122
+ for i, slide in enumerate(presentation.slides):
123
+ for shape in slide.shapes:
124
+ # check if the shape is a text box
125
+ if hasattr(shape, "text"):
126
+ if shape.text.isdigit():
127
+ # "SlidePlaceholder" is the name of the placeholder in the master slide
128
+ page_number_text = shape.text
129
+ font_color = get_master_placeholder_color(presentation)
130
+ return 1 if font_color is not None and is_red_color(font_color) else 0
131
+
132
+
133
+ # import numpy as np
134
+ # from PIL import Image
135
+ # from skimage.metrics import structural_similarity as ssim
136
+
137
+ # def compare_images(image1_path, image2_path):
138
+ # # You would call this function with the paths to the two images you want to compare:
139
+ # # score = compare_images('path_to_image1', 'path_to_image2')
140
+ # # print("Similarity score:", score)
141
+
142
+ # if not image1_path or not image2_path:
143
+ # return 0
144
+
145
+ # # Open the images and convert to grayscale
146
+ # image1 = Image.open(image1_path).convert('L')
147
+ # image2 = Image.open(image2_path).convert('L')
148
+
149
+ # # Resize images to the smaller one's size for comparison
150
+ # image1_size = image1.size
151
+ # image2_size = image2.size
152
+ # new_size = min(image1_size, image2_size)
153
+
154
+ # image1 = image1.resize(new_size, Image.Resampling.LANCZOS)
155
+ # image2 = image2.resize(new_size, Image.Resampling.LANCZOS)
156
+
157
+ # # Convert images to numpy arrays
158
+ # image1_array = np.array(image1)
159
+ # image2_array = np.array(image2)
160
+
161
+ # # Calculate SSIM between two images
162
+ # similarity_index = ssim(image1_array, image2_array)
163
+
164
+ # return similarity_index
165
+
166
+ def get_all_text_shapes(slide):
167
+ """递归获取slide中所有包含文本的shapes,包括GROUP内部的"""
168
+
169
+ def extract_text_shapes(shape):
170
+ results = []
171
+
172
+ # 检查当前shape是否有文本
173
+ if hasattr(shape, "text") and hasattr(shape, "text_frame"):
174
+ results.append(shape)
175
+
176
+ # 如果是GROUP,递归检查内部shapes
177
+ if hasattr(shape, 'shapes'):
178
+ for sub_shape in shape.shapes:
179
+ results.extend(extract_text_shapes(sub_shape))
180
+
181
+ return results
182
+
183
+ all_text_shapes = []
184
+ for shape in slide.shapes:
185
+ all_text_shapes.extend(extract_text_shapes(shape))
186
+
187
+ return all_text_shapes
188
+
189
+
190
+ def compare_pptx_files(file1_path, file2_path, **options):
191
+ # todo: not strictly match since not all information is compared because we cannot get the info through pptx
192
+ prs1 = Presentation(file1_path)
193
+ prs2 = Presentation(file2_path)
194
+
195
+ # Enable debug logging if requested
196
+ enable_debug = options.get("enable_debug", True)
197
+ if enable_debug:
198
+ enable_debug_logging()
199
+ debug_logger.debug(f"=== COMPARING PPTX FILES ===")
200
+ debug_logger.debug(f"File 1: {file1_path}")
201
+ debug_logger.debug(f"File 2: {file2_path}")
202
+ debug_logger.debug(f"File 1 slides: {len(prs1.slides)}")
203
+ debug_logger.debug(f"File 2 slides: {len(prs2.slides)}")
204
+
205
+ approximately_tolerance = options.get("approximately_tolerance", 0.005)
206
+ def is_approximately_equal(val1, val2, tolerance=approximately_tolerance):
207
+ """Compare two values with a tolerance of 0.1% (0.005)"""
208
+ if val1 == val2:
209
+ return True
210
+ if val1 == 0 and val2 == 0:
211
+ return True
212
+ if val1 == 0 or val2 == 0:
213
+ return False
214
+ return abs(val1 - val2) / max(abs(val1), abs(val2)) <= tolerance
215
+
216
+ examine_number_of_slides = options.get("examine_number_of_slides", True)
217
+ examine_shape = options.get("examine_shape", True)
218
+ examine_text = options.get("examine_text", True)
219
+ examine_indent = options.get("examine_indent", True)
220
+ examine_font_name = options.get("examine_font_name", True)
221
+ examine_font_size = options.get("examine_font_size", True)
222
+ examine_font_bold = options.get("examine_font_bold", True)
223
+ examine_font_italic = options.get("examine_font_italic", True)
224
+ examine_color_rgb = options.get("examine_color_rgb", True)
225
+ examine_font_underline = options.get("examine_font_underline", True)
226
+ examine_strike_through = options.get("examine_strike_through", True)
227
+ examine_alignment = options.get("examine_alignment", True)
228
+ examine_title_bottom_position = options.get("examine_title_bottom_position", False)
229
+ examine_table_bottom_position = options.get("examine_table_bottom_position", False)
230
+ examine_right_position = options.get("examine_right_position", False)
231
+ examine_top_position = options.get("examine_top_position", False)
232
+ examine_shape_for_shift_size = options.get("examine_shape_for_shift_size", False)
233
+ examine_image_size = options.get("examine_image_size", False)
234
+ examine_modify_height = options.get("examine_modify_height", False)
235
+ examine_bullets = options.get("examine_bullets", True)
236
+ examine_background_color = options.get("examine_background_color", True)
237
+ examine_note = options.get("examine_note", True)
238
+
239
+ # compare the number of slides
240
+ if len(prs1.slides) != len(prs2.slides) and examine_number_of_slides:
241
+ if enable_debug:
242
+ debug_logger.debug(f"MISMATCH: Number of slides differ - File1: {len(prs1.slides)}, File2: {len(prs2.slides)}")
243
+ return 0
244
+
245
+ slide_idx = 0
246
+ # compare the content of each slide
247
+ for slide1, slide2 in zip(prs1.slides, prs2.slides):
248
+ slide_idx += 1
249
+ if enable_debug:
250
+ debug_logger.debug(f"--- Comparing Slide {slide_idx} ---")
251
+ debug_logger.debug(f"Slide {slide_idx} - Shapes count: File1={len(slide1.shapes)}, File2={len(slide2.shapes)}")
252
+
253
+ def get_slide_background_color(slide):
254
+ # background = slide.background
255
+ # if background.fill.background():
256
+ # return background.fill.fore_color.rgb
257
+ # else:
258
+ # return None
259
+ fill = slide.background.fill
260
+ if fill.type == 1:
261
+ return fill.fore_color.rgb
262
+ elif fill.type == 5:
263
+ master_fill = slide.slide_layout.slide_master.background.fill
264
+ if master_fill.type == 1:
265
+ return master_fill.fore_color.rgb
266
+ else:
267
+ return None
268
+ else:
269
+ return None
270
+
271
+ if get_slide_background_color(slide1) != get_slide_background_color(slide2) and examine_background_color:
272
+ return 0
273
+
274
+ def get_slide_notes(slide):
275
+ notes_slide = slide.notes_slide
276
+ if notes_slide:
277
+ return notes_slide.notes_text_frame.text
278
+ else:
279
+ return None
280
+
281
+ if get_slide_notes(slide1).strip() != get_slide_notes(slide2).strip() and examine_note:
282
+ if enable_debug:
283
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx} - Notes differ:")
284
+ debug_logger.debug(f" Notes1: '{get_slide_notes(slide1).strip()}'")
285
+ debug_logger.debug(f" Notes2: '{get_slide_notes(slide2).strip()}'")
286
+ return 0
287
+
288
+ # Get all text shapes including those inside GROUPs
289
+ text_shapes1 = get_all_text_shapes(slide1)
290
+ text_shapes2 = get_all_text_shapes(slide2)
291
+
292
+ if enable_debug:
293
+ debug_logger.debug(f"Slide {slide_idx} - Text shapes found: File1={len(text_shapes1)}, File2={len(text_shapes2)}")
294
+
295
+ # check if the number of slides is the same
296
+ if len(slide1.shapes) != len(slide2.shapes):
297
+ if enable_debug:
298
+ debug_logger.debug(f"MISMATCH: Slide {slide_idx} - Different number of shapes: File1={len(slide1.shapes)}, File2={len(slide2.shapes)}")
299
+ return 0
300
+
301
+ # check if the shapes are the same
302
+ shape_idx = 0
303
+ for shape1, shape2 in zip(slide1.shapes, slide2.shapes):
304
+ shape_idx += 1
305
+ if enable_debug:
306
+ debug_logger.debug(f" Shape {shape_idx} - Type: {shape1.shape_type} vs {shape2.shape_type}")
307
+ if hasattr(shape1, "text") and hasattr(shape2, "text"):
308
+ debug_logger.debug(f" Shape {shape_idx} - Text: '{shape1.text.strip()}' vs '{shape2.text.strip()}'")
309
+ debug_logger.debug(f" Shape {shape_idx} - Position: ({shape1.left}, {shape1.top}) vs ({shape2.left}, {shape2.top})")
310
+ debug_logger.debug(f" Shape {shape_idx} - Size: ({shape1.width}, {shape1.height}) vs ({shape2.width}, {shape2.height})")
311
+ if examine_title_bottom_position:
312
+ if hasattr(shape1, "text") and hasattr(shape2, "text") and shape1.text == shape2.text:
313
+ if shape1.text == "Product Comparison" and (shape1.top <= shape2.top or shape1.top < 3600000):
314
+ return 0
315
+ elif (not is_approximately_equal(shape1.left, shape2.left) or
316
+ not is_approximately_equal(shape1.top, shape2.top) or
317
+ not is_approximately_equal(shape1.width, shape2.width) or
318
+ not is_approximately_equal(shape1.height, shape2.height)):
319
+ return 0
320
+
321
+ if examine_table_bottom_position:
322
+ if slide_idx == 3 and shape1.shape_type == 19 and shape2.shape_type == 19:
323
+ if shape1.top <= shape2.top or shape1.top < 3600000:
324
+ return 0
325
+ elif (not is_approximately_equal(shape1.left, shape2.left) or
326
+ not is_approximately_equal(shape1.top, shape2.top) or
327
+ not is_approximately_equal(shape1.width, shape2.width) or
328
+ not is_approximately_equal(shape1.height, shape2.height)):
329
+ return 0
330
+
331
+ if examine_right_position:
332
+ if slide_idx == 2 and not hasattr(shape1, "text") and not hasattr(shape2, "text"):
333
+ if shape1.left <= shape2.left or shape1.left < 4320000:
334
+ return 0
335
+
336
+ if examine_top_position:
337
+ if slide_idx == 2 and shape1.shape_type == 13 and shape2.shape_type == 13:
338
+ if shape1.top >= shape2.top or shape1.top > 1980000:
339
+ return 0
340
+
341
+
342
+ if examine_shape_for_shift_size:
343
+ if (not is_approximately_equal(shape1.left, shape2.left) or
344
+ not is_approximately_equal(shape1.top, shape2.top) or
345
+ not is_approximately_equal(shape1.width, shape2.width) or
346
+ not is_approximately_equal(shape1.height, shape2.height)):
347
+ if not (hasattr(shape1, "text") and hasattr(shape2,
348
+ "text") and shape1.text == shape2.text and shape1.text == "Elaborate on what you want to discuss."):
349
+ return 0
350
+
351
+ # CRITICAL: examine_shape check happens BEFORE examine_modify_height!
352
+ # If examine_shape=True (default), any shape dimension mismatch will cause immediate return 0,
353
+ # preventing examine_modify_height from ever being executed.
354
+ # For height modification tasks, you MUST set examine_shape=False to allow examine_modify_height to work.
355
+ if (
356
+ not is_approximately_equal(shape1.left, shape2.left) or
357
+ not is_approximately_equal(shape1.top, shape2.top) or
358
+ not is_approximately_equal(shape1.width, shape2.width) or
359
+ not is_approximately_equal(shape1.height, shape2.height)) and examine_shape:
360
+ if enable_debug:
361
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx} - Shape dimensions differ:")
362
+ debug_logger.debug(f" Left: {shape1.left} vs {shape2.left} (equal: {is_approximately_equal(shape1.left, shape2.left)})")
363
+ debug_logger.debug(f" Top: {shape1.top} vs {shape2.top} (equal: {is_approximately_equal(shape1.top, shape2.top)})")
364
+ debug_logger.debug(f" Width: {shape1.width} vs {shape2.width} (equal: {is_approximately_equal(shape1.width, shape2.width)})")
365
+ debug_logger.debug(f" Height: {shape1.height} vs {shape2.height} (equal: {is_approximately_equal(shape1.height, shape2.height)})")
366
+ if hasattr(shape1, "text") and hasattr(shape2, "text"):
367
+ debug_logger.debug(f" Shape text: '{shape1.text.strip()}' vs '{shape2.text.strip()}'")
368
+ return 0
369
+
370
+ if examine_image_size:
371
+ if shape1.shape_type == 13 and shape2.shape_type == 13:
372
+ if not is_approximately_equal(shape1.width, shape2.width) or not is_approximately_equal(shape1.height, shape2.height):
373
+ return 0
374
+ elif (not is_approximately_equal(shape1.left, shape2.left) or
375
+ not is_approximately_equal(shape1.top, shape2.top) or
376
+ not is_approximately_equal(shape1.width, shape2.width) or
377
+ not is_approximately_equal(shape1.height, shape2.height)):
378
+ return 0
379
+
380
+ # examine_modify_height: Special logic for height modification tasks
381
+ # - For non-text shapes and FREEFORM shapes (type 5): Only check height differences
382
+ # - For other shapes: Check all dimensions (left, top, width, height)
383
+ # WARNING: This check only works if examine_shape=False, otherwise examine_shape will
384
+ # terminate the comparison before this code is reached!
385
+ if examine_modify_height:
386
+ if not hasattr(shape1, "text") and not hasattr(shape2,
387
+ "text") or shape1.shape_type == 5 and shape2.shape_type == 5:
388
+ if not is_approximately_equal(shape1.height, shape2.height):
389
+ return 0
390
+ elif (not is_approximately_equal(shape1.left, shape2.left) or
391
+ not is_approximately_equal(shape1.top, shape2.top) or
392
+ not is_approximately_equal(shape1.width, shape2.width) or
393
+ not is_approximately_equal(shape1.height, shape2.height)):
394
+ return 0
395
+
396
+ if shape1.shape_type == MSO_SHAPE_TYPE.TABLE:
397
+ table1 = shape1.table
398
+ table2 = shape2.table
399
+ if enable_debug:
400
+ debug_logger.debug(f" Shape {shape_idx} - Comparing TABLE with {len(table1.rows)} rows and {len(table1.columns)} columns")
401
+ debug_logger.debug(f" Shape {shape_idx} - Table2 has {len(table2.rows)} rows and {len(table2.columns)} columns")
402
+
403
+ # Check if tables have the same dimensions
404
+ if len(table1.rows) != len(table2.rows) or len(table1.columns) != len(table2.columns):
405
+ if enable_debug:
406
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx} (TABLE) - Table dimensions differ:")
407
+ debug_logger.debug(f" Table1: {len(table1.rows)} rows x {len(table1.columns)} columns")
408
+ debug_logger.debug(f" Table2: {len(table2.rows)} rows x {len(table2.columns)} columns")
409
+ return 0
410
+
411
+ for row_idx in range(len(table1.rows)):
412
+ for col_idx in range(len(table1.columns)):
413
+ cell1 = table1.cell(row_idx, col_idx)
414
+ cell2 = table2.cell(row_idx, col_idx)
415
+
416
+ # Check if cells have the same number of paragraphs
417
+ if len(cell1.text_frame.paragraphs) != len(cell2.text_frame.paragraphs):
418
+ if enable_debug:
419
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx} (TABLE) - Cell [{row_idx},{col_idx}] - Different number of paragraphs:")
420
+ debug_logger.debug(f" Cell1 paragraphs: {len(cell1.text_frame.paragraphs)}")
421
+ debug_logger.debug(f" Cell2 paragraphs: {len(cell2.text_frame.paragraphs)}")
422
+ return 0
423
+
424
+ for para_idx, (para1, para2) in enumerate(zip(cell1.text_frame.paragraphs, cell2.text_frame.paragraphs)):
425
+ # Check if paragraphs have the same number of runs
426
+ if len(para1.runs) != len(para2.runs):
427
+ if enable_debug:
428
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx} (TABLE) - Cell [{row_idx},{col_idx}], Para {para_idx} - Different number of runs:")
429
+ debug_logger.debug(f" Para1 runs: {len(para1.runs)}")
430
+ debug_logger.debug(f" Para2 runs: {len(para2.runs)}")
431
+ return 0
432
+
433
+ for run_idx, (run1, run2) in enumerate(zip(para1.runs, para2.runs)):
434
+ # Check font color
435
+ if hasattr(run1.font.color, "rgb") and hasattr(run2.font.color, "rgb"):
436
+ if run1.font.color.rgb != run2.font.color.rgb and examine_color_rgb:
437
+ if enable_debug:
438
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx} (TABLE) - Cell [{row_idx},{col_idx}], Para {para_idx}, Run {run_idx} - Font color differs:")
439
+ debug_logger.debug(f" Color1: {run1.font.color.rgb} vs Color2: {run2.font.color.rgb}")
440
+ debug_logger.debug(f" Cell text: '{cell1.text.strip()}' vs '{cell2.text.strip()}'")
441
+ debug_logger.debug(f" Run text: '{run1.text}' vs '{run2.text}'")
442
+ return 0
443
+
444
+ # Check font bold
445
+ if run1.font.bold != run2.font.bold:
446
+ if not ((run1.font.bold is None or run1.font.bold is False) and
447
+ (run2.font.bold is None or run2.font.bold is False)):
448
+ if enable_debug:
449
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx} (TABLE) - Cell [{row_idx},{col_idx}], Para {para_idx}, Run {run_idx} - Font bold differs:")
450
+ debug_logger.debug(f" Bold1: {run1.font.bold} vs Bold2: {run2.font.bold}")
451
+ debug_logger.debug(f" Run text: '{run1.text}' vs '{run2.text}'")
452
+ return 0
453
+
454
+ # Check font italic
455
+ if run1.font.italic != run2.font.italic:
456
+ if not ((run1.font.italic is None or run1.font.italic is False) and
457
+ (run2.font.italic is None or run2.font.italic is False)):
458
+ if enable_debug:
459
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx} (TABLE) - Cell [{row_idx},{col_idx}], Para {para_idx}, Run {run_idx} - Font italic differs:")
460
+ debug_logger.debug(f" Italic1: {run1.font.italic} vs Italic2: {run2.font.italic}")
461
+ debug_logger.debug(f" Run text: '{run1.text}' vs '{run2.text}'")
462
+ return 0
463
+
464
+ # Check font underline
465
+ if run1.font.underline != run2.font.underline:
466
+ if run1.font.underline is not None and run2.font.underline is not None:
467
+ if enable_debug:
468
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx} (TABLE) - Cell [{row_idx},{col_idx}], Para {para_idx}, Run {run_idx} - Font underline differs:")
469
+ debug_logger.debug(f" Underline1: {run1.font.underline} vs Underline2: {run2.font.underline}")
470
+ debug_logger.debug(f" Run text: '{run1.text}' vs '{run2.text}'")
471
+ return 0
472
+ if (run1.font.underline is None and run2.font.underline is True) or (run1.font.underline is True and run2.font.underline is None):
473
+ if enable_debug:
474
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx} (TABLE) - Cell [{row_idx},{col_idx}], Para {para_idx}, Run {run_idx} - Font underline differs (None vs True):")
475
+ debug_logger.debug(f" Underline1: {run1.font.underline} vs Underline2: {run2.font.underline}")
476
+ debug_logger.debug(f" Run text: '{run1.text}' vs '{run2.text}'")
477
+ return 0
478
+
479
+ if hasattr(shape1, "text") and hasattr(shape2, "text"):
480
+ if shape1.text.strip() != shape2.text.strip() and examine_text:
481
+ return 0
482
+
483
+ # check if the number of paragraphs are the same
484
+ if len(shape1.text_frame.paragraphs) != len(shape2.text_frame.paragraphs):
485
+ if enable_debug:
486
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx} - Different number of paragraphs:")
487
+ debug_logger.debug(f" Shape1 paragraphs: {len(shape1.text_frame.paragraphs)}")
488
+ debug_logger.debug(f" Shape2 paragraphs: {len(shape2.text_frame.paragraphs)}")
489
+ return 0
490
+
491
+ # check if the paragraphs are the same
492
+ para_idx = 0
493
+ for para1, para2 in zip(shape1.text_frame.paragraphs, shape2.text_frame.paragraphs):
494
+ para_idx += 1
495
+ # Handle alignment comparison - treat None and LEFT (1) as equivalent
496
+ if examine_alignment:
497
+ from pptx.enum.text import PP_ALIGN
498
+ align1 = para1.alignment
499
+ align2 = para2.alignment
500
+
501
+ if enable_debug:
502
+ align1_name = "None" if align1 is None else getattr(align1, 'name', str(align1))
503
+ align2_name = "None" if align2 is None else getattr(align2, 'name', str(align2))
504
+ debug_logger.debug(f" Slide {slide_idx}, Shape {shape_idx}, Para {para_idx} - Alignment: '{align1_name}' vs '{align2_name}'")
505
+ debug_logger.debug(f" Slide {slide_idx}, Shape {shape_idx}, Para {para_idx} - Text: '{para1.text}' vs '{para2.text}'")
506
+
507
+ # Convert None to LEFT for comparison since None means default left alignment
508
+ if align1 is None:
509
+ align1 = PP_ALIGN.LEFT # LEFT alignment
510
+ if align2 is None:
511
+ align2 = PP_ALIGN.LEFT # LEFT alignment
512
+
513
+ if align1 != align2:
514
+ if enable_debug:
515
+ align1_final = getattr(align1, 'name', str(align1))
516
+ align2_final = getattr(align2, 'name', str(align2))
517
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx}, Para {para_idx} - Alignment differs: '{align1_final}' vs '{align2_final}'")
518
+ return 0
519
+
520
+ # check if the runs are the same
521
+ if para1.text != para2.text and examine_text:
522
+ return 0
523
+
524
+ if para1.level != para2.level and examine_indent:
525
+ return 0
526
+
527
+ # check if the number of runs are the same
528
+ if len(para1.runs) != len(para2.runs):
529
+ if enable_debug:
530
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx}, Para {para_idx} - Different number of runs:")
531
+ debug_logger.debug(f" Para1 runs: {len(para1.runs)}")
532
+ debug_logger.debug(f" Para2 runs: {len(para2.runs)}")
533
+ return 0
534
+
535
+ for run1, run2 in zip(para1.runs, para2.runs):
536
+
537
+ # check if the font properties are the same
538
+ if run1.font.name != run2.font.name and examine_font_name:
539
+ if enable_debug:
540
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx}, Para {para_idx} - Font name differs:")
541
+ debug_logger.debug(f" Name1: '{run1.font.name}' vs Name2: '{run2.font.name}'")
542
+ debug_logger.debug(f" Text: '{run1.text}' vs '{run2.text}'")
543
+ return 0
544
+
545
+ if run1.font.size != run2.font.size and examine_font_size:
546
+ if enable_debug:
547
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx}, Para {para_idx} - Font size differs:")
548
+ debug_logger.debug(f" Size1: {run1.font.size} vs Size2: {run2.font.size}")
549
+ debug_logger.debug(f" Text: '{run1.text}' vs '{run2.text}'")
550
+ return 0
551
+
552
+ if run1.font.bold != run2.font.bold and examine_font_bold:
553
+ # Special handling for None vs False - both mean "not bold"
554
+ if not ((run1.font.bold is None or run1.font.bold is False) and
555
+ (run2.font.bold is None or run2.font.bold is False)):
556
+ if enable_debug:
557
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx}, Para {para_idx} - Font bold differs:")
558
+ debug_logger.debug(f" Bold1: {run1.font.bold} vs Bold2: {run2.font.bold}")
559
+ debug_logger.debug(f" Text: '{run1.text}' vs '{run2.text}'")
560
+ return 0
561
+
562
+ if run1.font.italic != run2.font.italic and examine_font_italic:
563
+ # Special handling for None vs False - both mean "not italic"
564
+ if not ((run1.font.italic is None or run1.font.italic is False) and
565
+ (run2.font.italic is None or run2.font.italic is False)):
566
+ if enable_debug:
567
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx}, Para {para_idx} - Font italic differs:")
568
+ debug_logger.debug(f" Italic1: {run1.font.italic} vs Italic2: {run2.font.italic}")
569
+ debug_logger.debug(f" Text: '{run1.text}' vs '{run2.text}'")
570
+ return 0
571
+
572
+ if hasattr(run1.font.color, "rgb") and hasattr(run2.font.color, "rgb"):
573
+ if run1.font.color.rgb != run2.font.color.rgb and examine_color_rgb:
574
+ if enable_debug:
575
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx}, Para {para_idx} - Font color differs:")
576
+ debug_logger.debug(f" Color1: {run1.font.color.rgb} vs Color2: {run2.font.color.rgb}")
577
+ debug_logger.debug(f" Text: '{run1.text}' vs '{run2.text}'")
578
+ return 0
579
+
580
+ if run1.font.underline != run2.font.underline and examine_font_underline:
581
+ if run1.font.underline is not None and run2.font.underline is not None:
582
+ if enable_debug:
583
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx}, Para {para_idx} - Font underline differs:")
584
+ debug_logger.debug(f" Underline1: {run1.font.underline} vs Underline2: {run2.font.underline}")
585
+ debug_logger.debug(f" Text: '{run1.text}' vs '{run2.text}'")
586
+ return 0
587
+ if (run1.font.underline is None and run2.font.underline is True) or (run1.font.underline is True and run2.font.underline is None):
588
+ if enable_debug:
589
+ debug_logger.debug(f" MISMATCH: Slide {slide_idx}, Shape {shape_idx}, Para {para_idx} - Font underline differs (None vs True):")
590
+ debug_logger.debug(f" Underline1: {run1.font.underline} vs Underline2: {run2.font.underline}")
591
+ debug_logger.debug(f" Text: '{run1.text}' vs '{run2.text}'")
592
+ return 0
593
+
594
+ if run1.font._element.attrib.get('strike', 'noStrike') != run2.font._element.attrib.get(
595
+ 'strike', 'noStrike') and examine_strike_through:
596
+ return 0
597
+
598
+ def _extract_bullets(xml_data):
599
+ root = ET.fromstring(xml_data)
600
+
601
+ namespaces = {
602
+ 'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
603
+ 'p': 'http://schemas.openxmlformats.org/presentationml/2006/main',
604
+ }
605
+
606
+ bullets = []
607
+
608
+ for paragraph in root.findall('.//a:p', namespaces):
609
+ pPr = paragraph.find('a:pPr', namespaces)
610
+ if pPr is not None:
611
+ lvl = pPr.get('lvl')
612
+ buChar = pPr.find('a:buChar', namespaces)
613
+ char = buChar.get('char') if buChar is not None else "No Bullet"
614
+ buClr = pPr.find('a:buClr/a:srgbClr', namespaces)
615
+ color = buClr.get('val') if buClr is not None else "No Color"
616
+ else:
617
+ lvl = "No Level"
618
+ char = "No Bullet"
619
+ color = "No Color"
620
+
621
+ text = "".join(t.text for t in paragraph.findall('.//a:t', namespaces))
622
+
623
+ # Only add non-empty paragraphs to bullets list
624
+ if text.strip():
625
+ bullets.append((lvl, char, text, color))
626
+
627
+ return bullets
628
+
629
+ def _compare_bullets_with_tolerance(bullets1, bullets2):
630
+ """Compare bullets with tolerance for minor differences"""
631
+ if len(bullets1) != len(bullets2):
632
+ return False
633
+
634
+ for (lvl1, char1, text1, color1), (lvl2, char2, text2, color2) in zip(bullets1, bullets2):
635
+ # Compare text (most important)
636
+ if text1 != text2:
637
+ return False
638
+
639
+ # Compare bullet character
640
+ if char1 != char2:
641
+ return False
642
+
643
+ # Compare level with tolerance (None and '0' are equivalent)
644
+ normalized_lvl1 = '0' if lvl1 is None else lvl1
645
+ normalized_lvl2 = '0' if lvl2 is None else lvl2
646
+ if normalized_lvl1 != normalized_lvl2:
647
+ return False
648
+
649
+ # Color comparison is more lenient - we don't fail on color differences
650
+ # since they might be due to theme or formatting differences
651
+ # if color1 != color2:
652
+ # return False
653
+
654
+ return True
655
+
656
+ if examine_bullets:
657
+ try:
658
+ bullets1 = _extract_bullets(run1.part.blob.decode('utf-8'))
659
+ bullets2 = _extract_bullets(run2.part.blob.decode('utf-8'))
660
+
661
+ # Compare bullets with tolerance for minor differences
662
+ if not _compare_bullets_with_tolerance(bullets1, bullets2):
663
+ return 0
664
+ except:
665
+ # If bullet extraction fails, skip bullet comparison
666
+ pass
667
+
668
+ # fixme: Actually there are more properties to be compared, we can add them later via parsing the xml data
669
+
670
+ # Additional check: compare all text shapes including those in GROUPs
671
+ if examine_alignment and len(text_shapes1) == len(text_shapes2):
672
+ for idx, (tshape1, tshape2) in enumerate(zip(text_shapes1, text_shapes2)):
673
+ if enable_debug:
674
+ debug_logger.debug(f" Additional text shape check {idx+1}: '{tshape1.text.strip()[:30]}' vs '{tshape2.text.strip()[:30]}'")
675
+
676
+ # Compare text content
677
+ if tshape1.text.strip() != tshape2.text.strip() and examine_text:
678
+ if enable_debug:
679
+ debug_logger.debug(f" MISMATCH: Text differs - '{tshape1.text.strip()}' vs '{tshape2.text.strip()}'")
680
+ return 0
681
+
682
+ # Check if text shapes have the same number of paragraphs
683
+ if len(tshape1.text_frame.paragraphs) != len(tshape2.text_frame.paragraphs):
684
+ if enable_debug:
685
+ debug_logger.debug(f" MISMATCH: Different number of paragraphs - {len(tshape1.text_frame.paragraphs)} vs {len(tshape2.text_frame.paragraphs)}")
686
+ return 0
687
+
688
+ # Compare alignment of each paragraph
689
+ for para_idx, (para1, para2) in enumerate(zip(tshape1.text_frame.paragraphs, tshape2.text_frame.paragraphs)):
690
+ from pptx.enum.text import PP_ALIGN
691
+ align1 = para1.alignment
692
+ align2 = para2.alignment
693
+
694
+ if enable_debug:
695
+ align1_name = "None" if align1 is None else getattr(align1, 'name', str(align1))
696
+ align2_name = "None" if align2 is None else getattr(align2, 'name', str(align2))
697
+ debug_logger.debug(f" Para {para_idx+1}: Alignment '{align1_name}' vs '{align2_name}'")
698
+
699
+ # Convert None to LEFT for comparison
700
+ if align1 is None:
701
+ align1 = PP_ALIGN.LEFT
702
+ if align2 is None:
703
+ align2 = PP_ALIGN.LEFT
704
+
705
+ if align1 != align2:
706
+ if enable_debug:
707
+ align1_final = getattr(align1, 'name', str(align1))
708
+ align2_final = getattr(align2, 'name', str(align2))
709
+ debug_logger.debug(f" MISMATCH: Alignment differs - '{align1_final}' vs '{align2_final}'")
710
+ return 0
711
+ elif len(text_shapes1) != len(text_shapes2):
712
+ if enable_debug:
713
+ debug_logger.debug(f"MISMATCH: Different number of text shapes - {len(text_shapes1)} vs {len(text_shapes2)}")
714
+ return 0
715
+
716
+ if enable_debug:
717
+ debug_logger.debug(f"=== COMPARISON SUCCESSFUL - Files match ===")
718
+ return 1
719
+
720
+
721
+ def check_strikethrough(pptx_path, rules):
722
+ # Load the presentation
723
+ presentation = Presentation(pptx_path)
724
+
725
+ slide_index_s = rules["slide_index_s"]
726
+ shape_index_s = rules["shape_index_s"]
727
+ paragraph_index_s = rules["paragraph_index_s"]
728
+
729
+ try:
730
+ for slide_index in slide_index_s:
731
+ # Get the slide
732
+ slide = presentation.slides[slide_index]
733
+
734
+ for shape_index in shape_index_s:
735
+ # Get the text box
736
+ paragraphs = slide.shapes[shape_index].text_frame.paragraphs
737
+
738
+ for paragraph_index in paragraph_index_s:
739
+ paragraph = paragraphs[paragraph_index]
740
+ run = paragraph.runs[0]
741
+ if 'strike' not in run.font._element.attrib:
742
+ return 0
743
+
744
+
745
+ except Exception as e:
746
+ logger.error(f"Error: {e}")
747
+ return 0
748
+
749
+ return 1
750
+
751
+
752
+ def check_slide_orientation_Portrait(pptx_path):
753
+ presentation = Presentation(pptx_path)
754
+
755
+ slide_height = presentation.slide_height
756
+ slide_width = presentation.slide_width
757
+
758
+ if slide_width < slide_height:
759
+ return 1
760
+ return 0
761
+
762
+
763
+ def evaluate_presentation_fill_to_rgb_distance(pptx_file, rules):
764
+ rgb = rules["rgb"]
765
+
766
+ try:
767
+ original_rgb = rules["original_rgb"]
768
+ except:
769
+ original_rgb = None
770
+
771
+ def get_rgb_from_color(color):
772
+ try:
773
+ if hasattr(color, "rgb"):
774
+ return color.rgb
775
+ else:
776
+ return None
777
+ except:
778
+ return None
779
+
780
+ def slide_fill_distance_to_rgb(_slide, _rgb, _original_rgb):
781
+ fill = _slide.background.fill
782
+ if fill.type == 1:
783
+ color_rgb = get_rgb_from_color(fill.fore_color)
784
+ if color_rgb is None:
785
+ return 1
786
+ r1, g1, b1 = color_rgb
787
+ r2, g2, b2 = _rgb
788
+
789
+ if _original_rgb is not None:
790
+ r3, g3, b3 = _original_rgb
791
+ if r1 == r3 and g1 == g3 and b1 == b3:
792
+ return 1
793
+
794
+ return sqrt((r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2) / sqrt(255 ** 2 + 255 ** 2 + 255 ** 2)
795
+ elif fill.type == 5:
796
+ master_fill = _slide.slide_layout.slide_master.background.fill
797
+ if master_fill.type == 1:
798
+ color_rgb = get_rgb_from_color(master_fill.fore_color)
799
+ if color_rgb is None:
800
+ return 1
801
+ r1, g1, b1 = color_rgb
802
+ else:
803
+ return 1
804
+ r2, g2, b2 = _rgb
805
+
806
+ if _original_rgb is not None:
807
+ r3, g3, b3 = _original_rgb
808
+ if r1 == r3 and g1 == g3 and b1 == b3:
809
+ return 1
810
+
811
+ return sqrt((r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2) / sqrt(255 ** 2 + 255 ** 2 + 255 ** 2)
812
+
813
+ return 1
814
+
815
+ prs = Presentation(pptx_file)
816
+ similarity = 1 - sum(slide_fill_distance_to_rgb(slide, rgb, original_rgb) for slide in prs.slides) / len(prs.slides)
817
+ return similarity
818
+
819
+
820
+ def check_left_panel(accessibility_tree):
821
+ namespaces = {
822
+ 'st': 'uri:deskat:state.at-spi.gnome.org',
823
+ 'cp': 'uri:deskat:component.at-spi.gnome.org'
824
+ }
825
+
826
+ root = ET.fromstring(accessibility_tree)
827
+
828
+ # 遍历所有 document-frame 节点
829
+ for doc_frame in root.iter('document-frame'):
830
+ if doc_frame.attrib.get("name") == "Slides View":
831
+ # 说明 Slides View 存在,即左侧面板已打开
832
+ return 1.
833
+
834
+ # 没找到 Slides View,认为左侧面板未打开
835
+ return 0.
836
+
837
+
838
+ def check_transition(pptx_file, rules):
839
+ slide_idx = rules['slide_idx']
840
+ transition_type = rules['transition_type']
841
+
842
+ # Use the zipfile module to open the .pptx file
843
+ with zipfile.ZipFile(pptx_file, 'r') as zip_ref:
844
+ # Get the slide XML file
845
+ slide_name = 'ppt/slides/slide{}.xml'.format(slide_idx + 1)
846
+ try:
847
+ zip_ref.getinfo(slide_name)
848
+ except KeyError:
849
+ # Slide does not exist
850
+ return 0.
851
+
852
+ with zip_ref.open(slide_name) as slide_file:
853
+ # 解析XML
854
+ tree = ET.parse(slide_file)
855
+ root = tree.getroot()
856
+
857
+ # XML namespace
858
+ namespaces = {
859
+ 'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
860
+ 'p': 'http://schemas.openxmlformats.org/presentationml/2006/main',
861
+ }
862
+
863
+ # Search for the transition element
864
+ transition = root.find('.//p:transition', namespaces)
865
+ if transition is not None:
866
+ # Check if the transition is an expected transition
867
+ dissolve = transition.find('.//p:{}'.format(transition_type), namespaces)
868
+ if dissolve is not None:
869
+ return 1.
870
+ else:
871
+ return 0.
872
+ else:
873
+ return 0.
874
+
875
+
876
+ def check_page_number_colors(pptx_file, rules):
877
+ color = rules["color"]
878
+
879
+ def is_red(rgb_str, threshold=50):
880
+ r, g, b = int(rgb_str[1:3], 16), int(rgb_str[3:5], 16), int(rgb_str[5:7], 16)
881
+ return r > g + threshold and r > b + threshold
882
+
883
+ def is_blue(rgb_str, threshold=50):
884
+ r, g, b = int(rgb_str[1:3], 16), int(rgb_str[3:5], 16), int(rgb_str[5:7], 16)
885
+ return b > g + threshold and b > r + threshold
886
+
887
+ def is_green(rgb_str, threshold=50):
888
+ r, g, b = int(rgb_str[1:3], 16), int(rgb_str[3:5], 16), int(rgb_str[5:7], 16)
889
+ return g > r + threshold and g > b + threshold
890
+
891
+ def is_black(rgb_str, threshold=50):
892
+ r, g, b = int(rgb_str[1:3], 16), int(rgb_str[3:5], 16), int(rgb_str[5:7], 16)
893
+ return r < threshold and g < threshold and b < threshold
894
+
895
+ with zipfile.ZipFile(pptx_file, 'r') as zip_ref:
896
+ slide_master_name = 'ppt/slideMasters/slideMaster1.xml'
897
+ with zip_ref.open(slide_master_name) as slide_master_file:
898
+ tree = ET.parse(slide_master_file)
899
+ root = tree.getroot()
900
+
901
+ namespaces = {
902
+ 'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
903
+ 'p': 'http://schemas.openxmlformats.org/presentationml/2006/main',
904
+ }
905
+
906
+ color_elems = root.findall('.//a:solidFill//a:srgbClr', namespaces)
907
+ slides_color_val = color_elems[-2].get('val')
908
+
909
+ if slides_color_val is None:
910
+ return 0
911
+ elif color == "red" and not is_red(slides_color_val):
912
+ return 0
913
+ elif color == "blue" and not is_blue(slides_color_val):
914
+ return 0
915
+ elif color == "green" and not is_green(slides_color_val):
916
+ return 0
917
+ elif color == "black" and not is_black(slides_color_val):
918
+ return 0
919
+
920
+ return 1
921
+
922
+
923
+ def check_auto_saving_time(pptx_file, rules):
924
+ minutes = rules["minutes"]
925
+
926
+ # open and parse xml file
927
+ try:
928
+ tree = ET.parse(pptx_file)
929
+ root = tree.getroot()
930
+
931
+ # Traverse the XML tree to find the autosave time setting
932
+ autosave_time = None
933
+ for item in root.findall(".//item"):
934
+ # Check the path attribute
935
+ path = item.get('{http://openoffice.org/2001/registry}path')
936
+ if path == "/org.openoffice.Office.Common/Save/Document":
937
+ # Once the correct item is found, look for the prop element with the name "AutoSaveTimeIntervall"
938
+ for prop in item.findall(".//prop"):
939
+ name = prop.get('{http://openoffice.org/2001/registry}name')
940
+ if name == "AutoSaveTimeIntervall":
941
+ # Extract the value of the autosave time interval
942
+ autosave_time = prop.find(".//value").text
943
+ break
944
+
945
+ if autosave_time is None:
946
+ return 0
947
+ else:
948
+ autosave_time = int(autosave_time)
949
+ if autosave_time == minutes:
950
+ return 1
951
+ else:
952
+ return 0
953
+
954
+ except ET.ParseError as e:
955
+ logger.error(f"Error parsing XML: {e}")
956
+ except FileNotFoundError:
957
+ logger.error(f"File not found: {pptx_file}")