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.
- desktop_env/__init__.py +1 -0
- desktop_env/actions.py +203 -0
- desktop_env/controllers/__init__.py +0 -0
- desktop_env/controllers/python.py +471 -0
- desktop_env/controllers/setup.py +882 -0
- desktop_env/desktop_env.py +509 -0
- desktop_env/evaluators/__init__.py +5 -0
- desktop_env/evaluators/getters/__init__.py +41 -0
- desktop_env/evaluators/getters/calc.py +15 -0
- desktop_env/evaluators/getters/chrome.py +1774 -0
- desktop_env/evaluators/getters/file.py +154 -0
- desktop_env/evaluators/getters/general.py +42 -0
- desktop_env/evaluators/getters/gimp.py +38 -0
- desktop_env/evaluators/getters/impress.py +126 -0
- desktop_env/evaluators/getters/info.py +24 -0
- desktop_env/evaluators/getters/misc.py +406 -0
- desktop_env/evaluators/getters/replay.py +20 -0
- desktop_env/evaluators/getters/vlc.py +86 -0
- desktop_env/evaluators/getters/vscode.py +35 -0
- desktop_env/evaluators/metrics/__init__.py +160 -0
- desktop_env/evaluators/metrics/basic_os.py +68 -0
- desktop_env/evaluators/metrics/chrome.py +493 -0
- desktop_env/evaluators/metrics/docs.py +1011 -0
- desktop_env/evaluators/metrics/general.py +665 -0
- desktop_env/evaluators/metrics/gimp.py +637 -0
- desktop_env/evaluators/metrics/libreoffice.py +28 -0
- desktop_env/evaluators/metrics/others.py +92 -0
- desktop_env/evaluators/metrics/pdf.py +31 -0
- desktop_env/evaluators/metrics/slides.py +957 -0
- desktop_env/evaluators/metrics/table.py +585 -0
- desktop_env/evaluators/metrics/thunderbird.py +176 -0
- desktop_env/evaluators/metrics/utils.py +719 -0
- desktop_env/evaluators/metrics/vlc.py +524 -0
- desktop_env/evaluators/metrics/vscode.py +283 -0
- desktop_env/providers/__init__.py +35 -0
- desktop_env/providers/aws/__init__.py +0 -0
- desktop_env/providers/aws/manager.py +278 -0
- desktop_env/providers/aws/provider.py +186 -0
- desktop_env/providers/aws/provider_with_proxy.py +315 -0
- desktop_env/providers/aws/proxy_pool.py +193 -0
- desktop_env/providers/azure/__init__.py +0 -0
- desktop_env/providers/azure/manager.py +87 -0
- desktop_env/providers/azure/provider.py +207 -0
- desktop_env/providers/base.py +97 -0
- desktop_env/providers/gcp/__init__.py +0 -0
- desktop_env/providers/gcp/manager.py +0 -0
- desktop_env/providers/gcp/provider.py +0 -0
- desktop_env/providers/virtualbox/__init__.py +0 -0
- desktop_env/providers/virtualbox/manager.py +463 -0
- desktop_env/providers/virtualbox/provider.py +124 -0
- desktop_env/providers/vmware/__init__.py +0 -0
- desktop_env/providers/vmware/manager.py +455 -0
- desktop_env/providers/vmware/provider.py +105 -0
- gui_agents/__init__.py +0 -0
- gui_agents/agents/Action.py +209 -0
- gui_agents/agents/__init__.py +0 -0
- gui_agents/agents/agent_s.py +832 -0
- gui_agents/agents/global_state.py +610 -0
- gui_agents/agents/grounding.py +651 -0
- gui_agents/agents/hardware_interface.py +129 -0
- gui_agents/agents/manager.py +568 -0
- gui_agents/agents/translator.py +132 -0
- gui_agents/agents/worker.py +355 -0
- gui_agents/cli_app.py +560 -0
- gui_agents/core/__init__.py +0 -0
- gui_agents/core/engine.py +1496 -0
- gui_agents/core/knowledge.py +449 -0
- gui_agents/core/mllm.py +555 -0
- gui_agents/tools/__init__.py +0 -0
- gui_agents/tools/tools.py +727 -0
- gui_agents/unit_test/__init__.py +0 -0
- gui_agents/unit_test/run_tests.py +65 -0
- gui_agents/unit_test/test_manager.py +330 -0
- gui_agents/unit_test/test_worker.py +269 -0
- gui_agents/utils/__init__.py +0 -0
- gui_agents/utils/analyze_display.py +301 -0
- gui_agents/utils/common_utils.py +263 -0
- gui_agents/utils/display_viewer.py +281 -0
- gui_agents/utils/embedding_manager.py +53 -0
- gui_agents/utils/image_axis_utils.py +27 -0
- lybic_guiagents-0.1.0.dist-info/METADATA +416 -0
- lybic_guiagents-0.1.0.dist-info/RECORD +85 -0
- lybic_guiagents-0.1.0.dist-info/WHEEL +5 -0
- lybic_guiagents-0.1.0.dist-info/licenses/LICENSE +201 -0
- 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}")
|