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,637 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from typing import List, Union
|
|
4
|
+
from skimage.metrics import structural_similarity as ssim
|
|
5
|
+
from PIL import Image, ImageChops, ImageStat
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compare_image_list(pred_img_path_list: Union[str, List[str]],
|
|
9
|
+
gold_img_path_list: Union[str, List[str]]) -> float:
|
|
10
|
+
""" Compare two image lists, only if all images are the same, return 1.0, otherwise return 0.0
|
|
11
|
+
"""
|
|
12
|
+
if type(pred_img_path_list) != list:
|
|
13
|
+
pred_img_path_list = [pred_img_path_list]
|
|
14
|
+
gold_img_path_list = [gold_img_path_list]
|
|
15
|
+
for pred_img_path, gold_img_path in zip(pred_img_path_list, gold_img_path_list):
|
|
16
|
+
if not pred_img_path or not gold_img_path:
|
|
17
|
+
return 0.0
|
|
18
|
+
pred_img = Image.open(pred_img_path)
|
|
19
|
+
gold_img = Image.open(gold_img_path)
|
|
20
|
+
diff = ImageChops.difference(pred_img, gold_img)
|
|
21
|
+
if diff.getbbox():
|
|
22
|
+
return 0.0
|
|
23
|
+
return 1.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_gimp_export_path():
|
|
27
|
+
# Path to GIMP's configuration file. This example assumes GIMP version 2.10.
|
|
28
|
+
# You need to adjust the path according to the GIMP version and user's file system.
|
|
29
|
+
gimp_config_file = os.path.expanduser("~/.config/GIMP/2.10/gimprc")
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
# Open and read the configuration file
|
|
33
|
+
with open(gimp_config_file, 'r') as file:
|
|
34
|
+
for line in file:
|
|
35
|
+
# Search for the default export path setting
|
|
36
|
+
if "default-export-path" in line:
|
|
37
|
+
# Extract the current path from the line (assuming it's enclosed in quotes)
|
|
38
|
+
current_path = line.split('"')[1]
|
|
39
|
+
# Compare the current path with the expected path
|
|
40
|
+
return current_path
|
|
41
|
+
except FileNotFoundError:
|
|
42
|
+
# Handle the case where the configuration file is not found
|
|
43
|
+
logging.debug("GIMP configuration file not found")
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def check_file_exists(directory, filename):
|
|
48
|
+
file_path = os.path.join(directory, filename)
|
|
49
|
+
return 1 if os.path.isfile(file_path) else 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def increase_saturation(image1_path: str, image2_path: str) -> float:
|
|
53
|
+
def calculate_saturation(image):
|
|
54
|
+
# convert the image to HSV mode
|
|
55
|
+
hsv_image = image.convert("HSV")
|
|
56
|
+
|
|
57
|
+
saturation_channel = hsv_image.split()[1]
|
|
58
|
+
|
|
59
|
+
# calculate the mean saturation level
|
|
60
|
+
stat = ImageStat.Stat(saturation_channel)
|
|
61
|
+
mean_saturation = stat.mean[0]
|
|
62
|
+
|
|
63
|
+
return mean_saturation
|
|
64
|
+
|
|
65
|
+
image1 = Image.open(image1_path)
|
|
66
|
+
image2 = Image.open(image2_path)
|
|
67
|
+
|
|
68
|
+
# calculate the saturation level of each image
|
|
69
|
+
saturation1 = calculate_saturation(image1)
|
|
70
|
+
saturation2 = calculate_saturation(image2)
|
|
71
|
+
|
|
72
|
+
return 1 if saturation1 < saturation2 else 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def decrease_brightness(image1_path: str, image2_path: str) -> float:
|
|
76
|
+
def calculate_brightness(image):
|
|
77
|
+
# Convert the image to grayscale mode
|
|
78
|
+
grayscale_image = image.convert("L")
|
|
79
|
+
|
|
80
|
+
# Get the image data
|
|
81
|
+
pixels = list(grayscale_image.getdata())
|
|
82
|
+
|
|
83
|
+
brightness = sum(pixels) / len(pixels)
|
|
84
|
+
return brightness
|
|
85
|
+
|
|
86
|
+
image1 = Image.open(image1_path)
|
|
87
|
+
image2 = Image.open(image2_path)
|
|
88
|
+
|
|
89
|
+
brightness1 = calculate_brightness(image1)
|
|
90
|
+
brightness2 = calculate_brightness(image2)
|
|
91
|
+
|
|
92
|
+
return 1 if brightness1 > brightness2 else 0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
import cv2
|
|
96
|
+
import numpy as np
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def find_yellow_triangle(image):
|
|
100
|
+
# Convert the image to RGBA
|
|
101
|
+
rgba = cv2.cvtColor(image, cv2.COLOR_BGR2RGBA)
|
|
102
|
+
|
|
103
|
+
# define range of yellow color in HSV
|
|
104
|
+
lower_yellow = np.array([0, 0, 0], dtype=np.uint8)
|
|
105
|
+
upper_yellow = np.array([255, 255, 255], dtype=np.uint8)
|
|
106
|
+
|
|
107
|
+
# expand the dimensions of lower and upper yellow to match the image dimensions
|
|
108
|
+
lower_yellow = np.reshape(lower_yellow, (1, 1, 3))
|
|
109
|
+
upper_yellow = np.reshape(upper_yellow, (1, 1, 3))
|
|
110
|
+
# build a mask for the yellow color
|
|
111
|
+
mask = cv2.inRange(rgba, lower_yellow, upper_yellow)
|
|
112
|
+
|
|
113
|
+
# search for contours in the mask
|
|
114
|
+
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
115
|
+
|
|
116
|
+
# choose the largest contour
|
|
117
|
+
max_contour = max(contours, key=cv2.contourArea)
|
|
118
|
+
|
|
119
|
+
# calculate the center of the contour
|
|
120
|
+
M = cv2.moments(max_contour)
|
|
121
|
+
cx = int(M['m10'] / M['m00'])
|
|
122
|
+
cy = int(M['m01'] / M['m00'])
|
|
123
|
+
|
|
124
|
+
return cx, cy
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def compare_triangle_positions(image1, image2):
|
|
128
|
+
image1 = cv2.imread(image1, cv2.IMREAD_COLOR)
|
|
129
|
+
image2 = cv2.imread(image2, cv2.IMREAD_COLOR)
|
|
130
|
+
# find the center of the yellow triangle in each image
|
|
131
|
+
cx1, cy1 = find_yellow_triangle(image1)
|
|
132
|
+
cx2, cy2 = find_yellow_triangle(image2)
|
|
133
|
+
|
|
134
|
+
# calculate the distance between the center of the triangle and the center of the image
|
|
135
|
+
center_distance1 = np.sqrt(
|
|
136
|
+
(cx1 - image1.shape[1] // 2) ** 2 + (cy1 - image1.shape[0] // 2) ** 2)
|
|
137
|
+
center_distance2 = np.sqrt(
|
|
138
|
+
(cx2 - image2.shape[1] // 2) ** 2 + (cy2 - image2.shape[0] // 2) ** 2)
|
|
139
|
+
|
|
140
|
+
return 1 if center_distance1 > center_distance2 else 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Functions for the GIMP evaluator
|
|
144
|
+
def calculate_brightness(image):
|
|
145
|
+
"""Calculate the average brightness of an image"""
|
|
146
|
+
grayscale = image.convert('L')
|
|
147
|
+
stat = ImageStat.Stat(grayscale)
|
|
148
|
+
return stat.mean[0]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def normalize_brightness(image, target_brightness):
|
|
152
|
+
"""Normalize the brightness of an image to a target brightness in [0, 1]"""
|
|
153
|
+
current_brightness = calculate_brightness(image)
|
|
154
|
+
factor = target_brightness / current_brightness
|
|
155
|
+
|
|
156
|
+
# Apply a point transform to each pixel
|
|
157
|
+
def point_transform(x):
|
|
158
|
+
return min(255, max(0, int(x * factor)))
|
|
159
|
+
|
|
160
|
+
return image.point(point_transform)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def measure_saturation(hsv_image):
|
|
164
|
+
"""Measure the average saturation of an image"""
|
|
165
|
+
# Split into H, S, V channels
|
|
166
|
+
_, s, _ = hsv_image.split()
|
|
167
|
+
# Convert the saturation channel to a numpy array
|
|
168
|
+
s_array = np.array(s)
|
|
169
|
+
# Calculate the average saturation
|
|
170
|
+
avg_saturation = np.mean(s_array)
|
|
171
|
+
return avg_saturation
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def calculate_contrast(image):
|
|
175
|
+
"""Calculate the contrast of an image as the standard deviation of the pixel
|
|
176
|
+
values."""
|
|
177
|
+
pixels = np.asarray(image, dtype=np.float32)
|
|
178
|
+
return np.std(pixels)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def calculate_image_sharpness(image_path):
|
|
182
|
+
# Load the image in grayscale
|
|
183
|
+
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
|
|
184
|
+
# Apply the Laplacian operator
|
|
185
|
+
laplacian = cv2.Laplacian(image, cv2.CV_64F)
|
|
186
|
+
# Calculate the variance
|
|
187
|
+
variance = np.var(laplacian)
|
|
188
|
+
return variance
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def structure_check_by_mse(img1, img2, threshold=0.03):
|
|
192
|
+
"""Check if two images are approximately the same by MSE"""
|
|
193
|
+
mse = np.mean(
|
|
194
|
+
(np.array(img1, dtype=np.float32) / 255
|
|
195
|
+
- np.array(img2, dtype=np.float32) / 255) ** 2)
|
|
196
|
+
structure_same = True if mse < threshold else False
|
|
197
|
+
logging.debug(f"MSE: {mse}, threshold: {threshold}")
|
|
198
|
+
return structure_same
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def structure_check_by_ssim(img1, img2, threshold=0.9):
|
|
202
|
+
"""Check if two images are approximately the same by SSIM"""
|
|
203
|
+
similarity = ssim(np.array(img1), np.array(img2), multichannel=True, channel_axis=-1)
|
|
204
|
+
logging.debug("SSIM: %s", similarity)
|
|
205
|
+
return similarity >= threshold
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def check_brightness_decrease_and_structure_sim(src_path, tgt_path, threshold=0.03):
|
|
209
|
+
"""
|
|
210
|
+
Check the brightness of src is lower than tgt and the structures are similar
|
|
211
|
+
gimp:7a4deb26-d57d-4ea9-9a73-630f66a7b568
|
|
212
|
+
"""
|
|
213
|
+
if src_path is None or tgt_path is None:
|
|
214
|
+
return 0.
|
|
215
|
+
|
|
216
|
+
img_src = Image.open(src_path)
|
|
217
|
+
img_tgt = Image.open(tgt_path)
|
|
218
|
+
|
|
219
|
+
# Brightness comparison
|
|
220
|
+
brightness_src = calculate_brightness(img_src)
|
|
221
|
+
brightness_tgt = calculate_brightness(img_tgt)
|
|
222
|
+
brightness_reduced = brightness_tgt > brightness_src
|
|
223
|
+
|
|
224
|
+
# print(f"Brightness src: {brightness_src}, tgt: {brightness_tgt}, reduced: {brightness_reduced}")
|
|
225
|
+
|
|
226
|
+
# Normalize and compare images
|
|
227
|
+
target_brightness = 128
|
|
228
|
+
img_src_normalized = normalize_brightness(img_src, target_brightness)
|
|
229
|
+
img_tgt_normalized = normalize_brightness(img_tgt, target_brightness)
|
|
230
|
+
|
|
231
|
+
structure_same = structure_check_by_mse(img_src_normalized, img_tgt_normalized, threshold=threshold)
|
|
232
|
+
if brightness_reduced and structure_same:
|
|
233
|
+
return 1.
|
|
234
|
+
else:
|
|
235
|
+
return 0.
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def check_saturation_increase_and_structure_sim(src_path, tgt_path):
|
|
239
|
+
"""
|
|
240
|
+
Check the saturation of src is higher than tgt and the structures are similar
|
|
241
|
+
gimp:554785e9-4523-4e7a-b8e1-8016f565f56a
|
|
242
|
+
"""
|
|
243
|
+
if src_path is None or tgt_path is None:
|
|
244
|
+
return 0.
|
|
245
|
+
|
|
246
|
+
img_src = Image.open(src_path)
|
|
247
|
+
hsv_img_src = img_src.convert('HSV')
|
|
248
|
+
img_tgt = Image.open(tgt_path)
|
|
249
|
+
hsv_img_tgt = img_tgt.convert('HSV')
|
|
250
|
+
|
|
251
|
+
# Saturation comparison
|
|
252
|
+
src_saturation = measure_saturation(hsv_img_src)
|
|
253
|
+
tgt_saturation = measure_saturation(hsv_img_tgt)
|
|
254
|
+
|
|
255
|
+
saturation_increased = tgt_saturation < src_saturation
|
|
256
|
+
|
|
257
|
+
# Structure comparison
|
|
258
|
+
h1, s1, v1 = hsv_img_src.split()
|
|
259
|
+
h2, s2, v2 = hsv_img_tgt.split()
|
|
260
|
+
h_same = structure_check_by_ssim(h1, h2)
|
|
261
|
+
v_same = structure_check_by_ssim(v1, v2)
|
|
262
|
+
if h_same and v_same:
|
|
263
|
+
structure_same = True
|
|
264
|
+
else:
|
|
265
|
+
structure_same = False
|
|
266
|
+
|
|
267
|
+
if saturation_increased and structure_same:
|
|
268
|
+
return 1.
|
|
269
|
+
else:
|
|
270
|
+
return 0.
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def check_file_exists_and_structure_sim(src_path, tgt_path):
|
|
274
|
+
"""
|
|
275
|
+
Check if the image has been exported to the desktop
|
|
276
|
+
gimp:77b8ab4d-994f-43ac-8930-8ca087d7c4b4
|
|
277
|
+
"""
|
|
278
|
+
if src_path is None or tgt_path is None:
|
|
279
|
+
return 0.
|
|
280
|
+
|
|
281
|
+
# Check if the file exists
|
|
282
|
+
export_file_exists = os.path.isfile(src_path)
|
|
283
|
+
if not export_file_exists:
|
|
284
|
+
return 0.
|
|
285
|
+
|
|
286
|
+
# Check whether the target image is the same as the source image
|
|
287
|
+
img_src = Image.open(src_path)
|
|
288
|
+
img_tgt = Image.open(tgt_path)
|
|
289
|
+
structure_same = structure_check_by_ssim(img_src, img_tgt)
|
|
290
|
+
|
|
291
|
+
if structure_same:
|
|
292
|
+
return 1.
|
|
293
|
+
else:
|
|
294
|
+
return 0.
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def check_triangle_position(tgt_path):
|
|
298
|
+
"""
|
|
299
|
+
Check if the triangle is in the middle of the image.
|
|
300
|
+
gimp:f4aec372-4fb0-4df5-a52b-79e0e2a5d6ce
|
|
301
|
+
"""
|
|
302
|
+
if tgt_path is None:
|
|
303
|
+
return 0.
|
|
304
|
+
|
|
305
|
+
# Load the image
|
|
306
|
+
img = Image.open(tgt_path)
|
|
307
|
+
img_array = np.array(img)
|
|
308
|
+
|
|
309
|
+
# We assume the triangle is a different color from the background
|
|
310
|
+
# Find the unique colors
|
|
311
|
+
unique_colors, counts = np.unique(img_array.reshape(-1, img_array.shape[2]), axis=0,
|
|
312
|
+
return_counts=True)
|
|
313
|
+
unique_colors_sorted = unique_colors[np.argsort(counts)]
|
|
314
|
+
|
|
315
|
+
# Assuming the background is the most common color and the triangle is a different color
|
|
316
|
+
triangle_color = unique_colors_sorted[1]
|
|
317
|
+
|
|
318
|
+
# Create a mask where the triangle pixels are True
|
|
319
|
+
triangle_mask = np.all(img_array == triangle_color, axis=2)
|
|
320
|
+
|
|
321
|
+
# Get the coordinates of the triangle pixels
|
|
322
|
+
triangle_coords = np.argwhere(triangle_mask)
|
|
323
|
+
|
|
324
|
+
# Calculate the centroid of the triangle
|
|
325
|
+
centroid = triangle_coords.mean(axis=0)
|
|
326
|
+
|
|
327
|
+
# Check if the centroid is approximately in the middle of the image
|
|
328
|
+
image_center = np.array(img_array.shape[:2]) / 2
|
|
329
|
+
|
|
330
|
+
# We will consider the triangle to be in the middle if the centroid is within 5% of the image's center
|
|
331
|
+
tolerance = 0.05 * np.array(img_array.shape[:2])
|
|
332
|
+
middle = np.all(np.abs(centroid - image_center) < tolerance)
|
|
333
|
+
|
|
334
|
+
if bool(middle):
|
|
335
|
+
return 1.
|
|
336
|
+
else:
|
|
337
|
+
return 0.
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def check_structure_sim(src_path, tgt_path):
|
|
341
|
+
"""
|
|
342
|
+
Check if the structure of the two images are similar
|
|
343
|
+
gimp:2a729ded-3296-423d-aec4-7dd55ed5fbb3
|
|
344
|
+
"""
|
|
345
|
+
if src_path is None or tgt_path is None:
|
|
346
|
+
return 0.
|
|
347
|
+
|
|
348
|
+
img_src = Image.open(src_path)
|
|
349
|
+
img_tgt = Image.open(tgt_path)
|
|
350
|
+
structure_same = structure_check_by_ssim(img_src, img_tgt)
|
|
351
|
+
if structure_same:
|
|
352
|
+
return 1.
|
|
353
|
+
else:
|
|
354
|
+
return 0.
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def check_structure_sim_resized(src_path, tgt_path):
|
|
358
|
+
"""
|
|
359
|
+
Check if the structure of the two images are similar after resizing.
|
|
360
|
+
gimp:d16c99dc-2a1e-46f2-b350-d97c86c85c15
|
|
361
|
+
"""
|
|
362
|
+
if src_path is None or tgt_path is None:
|
|
363
|
+
return 0.
|
|
364
|
+
|
|
365
|
+
img_src = Image.open(src_path)
|
|
366
|
+
img_tgt = Image.open(tgt_path)
|
|
367
|
+
|
|
368
|
+
# Check if source image has transparency and extract content area
|
|
369
|
+
if img_src.mode in ('RGBA', 'LA') or 'transparency' in img_src.info:
|
|
370
|
+
if img_src.mode != 'RGBA':
|
|
371
|
+
img_src = img_src.convert('RGBA')
|
|
372
|
+
|
|
373
|
+
# Get alpha channel and find bounding box of non-transparent pixels
|
|
374
|
+
alpha = img_src.split()[-1]
|
|
375
|
+
bbox = alpha.getbbox()
|
|
376
|
+
|
|
377
|
+
if bbox is None:
|
|
378
|
+
# Image is completely transparent
|
|
379
|
+
logging.debug("Source image is completely transparent")
|
|
380
|
+
return 0.
|
|
381
|
+
|
|
382
|
+
# Crop to content area only
|
|
383
|
+
img_src_content = img_src.crop(bbox)
|
|
384
|
+
logging.debug(f"Source image cropped from {img_src.size} to {img_src_content.size}")
|
|
385
|
+
|
|
386
|
+
# Convert to RGB for comparison
|
|
387
|
+
img_src_content = img_src_content.convert('RGB')
|
|
388
|
+
img_src_resized = img_src_content.resize(img_tgt.size)
|
|
389
|
+
else:
|
|
390
|
+
# No transparency, resize normally
|
|
391
|
+
img_src_resized = img_src.resize(img_tgt.size)
|
|
392
|
+
|
|
393
|
+
# Ensure target image is RGB for comparison
|
|
394
|
+
if img_tgt.mode != 'RGB':
|
|
395
|
+
img_tgt = img_tgt.convert('RGB')
|
|
396
|
+
|
|
397
|
+
# Check if the structure is similar
|
|
398
|
+
structure_same = structure_check_by_ssim(img_src_resized, img_tgt)
|
|
399
|
+
if structure_same:
|
|
400
|
+
return 1.
|
|
401
|
+
else:
|
|
402
|
+
return 0.
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def check_contrast_increase_and_structure_sim(src_path, tgt_path):
|
|
406
|
+
"""
|
|
407
|
+
Check if the src image has higher contrast than the tgt image and the structures are similar
|
|
408
|
+
gimp:f723c744-e62c-4ae6-98d1-750d3cd7d79d
|
|
409
|
+
"""
|
|
410
|
+
if src_path is None or tgt_path is None:
|
|
411
|
+
return 0.
|
|
412
|
+
|
|
413
|
+
# Load images
|
|
414
|
+
source_image = Image.open(src_path)
|
|
415
|
+
target_image = Image.open(tgt_path)
|
|
416
|
+
|
|
417
|
+
# Calculate contrast
|
|
418
|
+
source_contrast = calculate_contrast(source_image)
|
|
419
|
+
target_contrast = calculate_contrast(target_image)
|
|
420
|
+
higher_contrast = target_contrast < source_contrast
|
|
421
|
+
|
|
422
|
+
# Check structure
|
|
423
|
+
structure_same = structure_check_by_ssim(source_image, target_image, threshold=0.65)
|
|
424
|
+
|
|
425
|
+
if higher_contrast and structure_same:
|
|
426
|
+
return 1.
|
|
427
|
+
else:
|
|
428
|
+
return 0.
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def check_config_status(actual_config_path, rule):
|
|
432
|
+
"""
|
|
433
|
+
Check if the GIMP status is as expected
|
|
434
|
+
"""
|
|
435
|
+
if actual_config_path is None:
|
|
436
|
+
return 0.
|
|
437
|
+
|
|
438
|
+
with open(actual_config_path, 'r') as f:
|
|
439
|
+
content = f.readlines()
|
|
440
|
+
|
|
441
|
+
for line in content:
|
|
442
|
+
if line.startswith('#') or line == '\n':
|
|
443
|
+
continue
|
|
444
|
+
items = line.strip().lstrip('(').rstrip(')\n').split()
|
|
445
|
+
if isinstance(rule["key"], str):
|
|
446
|
+
if items[0] == rule["key"] and items[-1] == rule["value"]:
|
|
447
|
+
return 1.
|
|
448
|
+
elif isinstance(rule["key"], list) and len(rule["key"]) == 2:
|
|
449
|
+
if items[0] == rule["key"][0] \
|
|
450
|
+
and items[1] == rule["key"][1] \
|
|
451
|
+
and items[-1] == rule["value"]:
|
|
452
|
+
return 1.
|
|
453
|
+
return 0.
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def check_image_size(src_path, rule):
|
|
457
|
+
"""
|
|
458
|
+
Check if the size of the src image is correct
|
|
459
|
+
multi-apps:42f4d1c7-4521-4161-b646-0a8934e36081
|
|
460
|
+
"""
|
|
461
|
+
if src_path is None:
|
|
462
|
+
return 0.
|
|
463
|
+
|
|
464
|
+
# Load the image
|
|
465
|
+
img = Image.open(src_path)
|
|
466
|
+
|
|
467
|
+
# Check if we should ignore transparent parts
|
|
468
|
+
ignore_transparent = rule.get("ignore_transparent", False)
|
|
469
|
+
|
|
470
|
+
if ignore_transparent and img.mode in ('RGBA', 'LA') or 'transparency' in img.info:
|
|
471
|
+
# Calculate bounding box of non-transparent pixels
|
|
472
|
+
if img.mode != 'RGBA':
|
|
473
|
+
img = img.convert('RGBA')
|
|
474
|
+
|
|
475
|
+
# Get alpha channel
|
|
476
|
+
alpha = img.split()[-1]
|
|
477
|
+
|
|
478
|
+
# Find bounding box of non-transparent pixels
|
|
479
|
+
bbox = alpha.getbbox()
|
|
480
|
+
|
|
481
|
+
if bbox is None:
|
|
482
|
+
# Image is completely transparent
|
|
483
|
+
actual_width = 0
|
|
484
|
+
actual_height = 0
|
|
485
|
+
else:
|
|
486
|
+
# Calculate actual content size
|
|
487
|
+
actual_width = bbox[2] - bbox[0]
|
|
488
|
+
actual_height = bbox[3] - bbox[1]
|
|
489
|
+
|
|
490
|
+
logging.debug(f"Original size: {img.size}, Content size: {actual_width}x{actual_height}")
|
|
491
|
+
else:
|
|
492
|
+
# Use original image size
|
|
493
|
+
actual_width = img.size[0]
|
|
494
|
+
actual_height = img.size[1]
|
|
495
|
+
logging.debug(f"Image size: {img.size}")
|
|
496
|
+
|
|
497
|
+
# Check the size
|
|
498
|
+
if rule.get("height", None) is not None:
|
|
499
|
+
height_same = actual_height == rule["height"]
|
|
500
|
+
else:
|
|
501
|
+
height_same = True
|
|
502
|
+
if rule.get("width", None) is not None:
|
|
503
|
+
width_same = actual_width == rule["width"]
|
|
504
|
+
else:
|
|
505
|
+
width_same = True
|
|
506
|
+
|
|
507
|
+
if height_same and width_same:
|
|
508
|
+
logging.debug(f"height_same: {height_same}, width_same: {width_same}")
|
|
509
|
+
return 1.
|
|
510
|
+
else:
|
|
511
|
+
logging.debug(f"height_same: {height_same}, width_same: {width_same}")
|
|
512
|
+
return 0.
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def check_palette_and_structure_sim(src_path, tgt_path):
|
|
516
|
+
"""
|
|
517
|
+
Check if the src image is palette-based and the structure of the two images are similar
|
|
518
|
+
gimp:06ca5602-62ca-47f6-ad4f-da151cde54cc
|
|
519
|
+
"""
|
|
520
|
+
if src_path is None or tgt_path is None:
|
|
521
|
+
return 0.
|
|
522
|
+
|
|
523
|
+
# Check if the source image is palette-based
|
|
524
|
+
source_image = Image.open(src_path)
|
|
525
|
+
palette_based = source_image.mode == 'P'
|
|
526
|
+
|
|
527
|
+
# Check structure
|
|
528
|
+
target_image = Image.open(tgt_path)
|
|
529
|
+
source_image = source_image.convert('RGB')
|
|
530
|
+
structure_same = structure_check_by_ssim(source_image, target_image)
|
|
531
|
+
if palette_based and structure_same:
|
|
532
|
+
return 1.
|
|
533
|
+
else:
|
|
534
|
+
return 0.
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def check_textbox_on_leftside(src_path):
|
|
538
|
+
"""
|
|
539
|
+
Check if the textbox is on the left side of the image.
|
|
540
|
+
gimp:e2dd0213-26db-4349-abe5-d5667bfd725c
|
|
541
|
+
"""
|
|
542
|
+
if src_path is None:
|
|
543
|
+
return 0.
|
|
544
|
+
|
|
545
|
+
source_image = Image.open(src_path)
|
|
546
|
+
gray_image = source_image.convert("L")
|
|
547
|
+
width, height = source_image.size
|
|
548
|
+
|
|
549
|
+
# Find the bounds of the black text
|
|
550
|
+
left_most_dark_pixel = width # Start with the farthest possible left position
|
|
551
|
+
for y in range(height):
|
|
552
|
+
for x in range(width):
|
|
553
|
+
# If the pixel is dark, consider it as part of the text
|
|
554
|
+
if gray_image.getpixel((x, y)) < 128: # Arbitrary threshold for "dark"
|
|
555
|
+
left_most_dark_pixel = min(left_most_dark_pixel, x)
|
|
556
|
+
break # Stop after finding the first dark pixel in this row
|
|
557
|
+
|
|
558
|
+
# Here we define "almost" on the left side as being within the left 5% of the image
|
|
559
|
+
if left_most_dark_pixel < width * 0.05:
|
|
560
|
+
return 1.
|
|
561
|
+
else:
|
|
562
|
+
return 0.
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def check_image_mirror(src_path, tgt_path):
|
|
566
|
+
"""
|
|
567
|
+
Check if the image is mirrored
|
|
568
|
+
gimp:72f83cdc-bf76-4531-9a1b-eb893a13f8aa
|
|
569
|
+
"""
|
|
570
|
+
if src_path is None or tgt_path is None:
|
|
571
|
+
return 0.
|
|
572
|
+
|
|
573
|
+
# Load images
|
|
574
|
+
source_image = Image.open(src_path)
|
|
575
|
+
target_image = Image.open(tgt_path)
|
|
576
|
+
|
|
577
|
+
# Check if the image is mirrored
|
|
578
|
+
transposed_image = source_image.transpose(Image.FLIP_LEFT_RIGHT)
|
|
579
|
+
# Use 0.99 because the image may not be exactly mirrored by gimp
|
|
580
|
+
mirrored = structure_check_by_ssim(transposed_image, target_image, 0.99)
|
|
581
|
+
if mirrored:
|
|
582
|
+
return 1.
|
|
583
|
+
else:
|
|
584
|
+
return 0.
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def check_green_background(src_path, tgt_path):
|
|
588
|
+
"""
|
|
589
|
+
Check if the background of the source image is green.
|
|
590
|
+
gimp:734d6579-c07d-47a8-9ae2-13339795476b
|
|
591
|
+
"""
|
|
592
|
+
if src_path is None or tgt_path is None:
|
|
593
|
+
return 0.
|
|
594
|
+
|
|
595
|
+
# Load images
|
|
596
|
+
source_image = Image.open(src_path)
|
|
597
|
+
target_image = Image.open(tgt_path)
|
|
598
|
+
|
|
599
|
+
source_pixels = np.array(source_image)
|
|
600
|
+
target_pixels = np.array(target_image)
|
|
601
|
+
|
|
602
|
+
for x in range(target_image.width):
|
|
603
|
+
for y in range(target_image.height):
|
|
604
|
+
# Identify background pixel in target image (not black)
|
|
605
|
+
if tuple(target_pixels[x, y][:3]) != (0, 0, 0):
|
|
606
|
+
# Check if corresponding pixel in source image is green
|
|
607
|
+
# Here, "green" means more green than red or blue
|
|
608
|
+
r, g, b = source_pixels[x, y][:3]
|
|
609
|
+
if not (g > r and g > b):
|
|
610
|
+
return 0.
|
|
611
|
+
|
|
612
|
+
return 1.
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def check_sharper(src_path, tgt_path):
|
|
616
|
+
"""
|
|
617
|
+
Check if the source image is sharper than the target image.
|
|
618
|
+
multi-app:bb7db4c2-30b5-4be7-8dd7-b8c4ec7d3108
|
|
619
|
+
"""
|
|
620
|
+
sharpness_src = calculate_image_sharpness(src_path)
|
|
621
|
+
sharpness_tgt = calculate_image_sharpness(tgt_path)
|
|
622
|
+
return 1.0 if sharpness_src > sharpness_tgt else 0.0
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def check_image_file_size(src_path, rule):
|
|
626
|
+
"""
|
|
627
|
+
Check if the size of the src image within 500KB
|
|
628
|
+
"""
|
|
629
|
+
if src_path is None:
|
|
630
|
+
return 0.0
|
|
631
|
+
|
|
632
|
+
# Check the size
|
|
633
|
+
file_size = os.path.getsize(src_path)
|
|
634
|
+
if file_size < rule["max_size"]:
|
|
635
|
+
return 1.0
|
|
636
|
+
else:
|
|
637
|
+
return 0.0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fnmatch
|
|
2
|
+
from typing import Dict, List
|
|
3
|
+
|
|
4
|
+
import lxml.cssselect
|
|
5
|
+
import lxml.etree
|
|
6
|
+
from lxml.etree import _Element as Element
|
|
7
|
+
|
|
8
|
+
_libconf_namespaces = [("oor", "http://openoffice.org/2001/registry")]
|
|
9
|
+
_libconf_ns_mapping = dict(_libconf_namespaces)
|
|
10
|
+
_setup_locale_selector = lxml.cssselect.CSSSelector('item[oor|path$=L10N]>prop[oor|name=ooSetupSystemLocale]>value',
|
|
11
|
+
namespaces=_libconf_ns_mapping)
|
|
12
|
+
_locale_selector = lxml.cssselect.CSSSelector('item[oor|path$=L10N]>prop[oor|name=ooLocale]>value',
|
|
13
|
+
namespaces=_libconf_ns_mapping)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_libre_locale(config_file: str, rules: Dict[str, List[str]]) -> float:
|
|
17
|
+
config: Element = lxml.etree.parse(config_file).getroot()
|
|
18
|
+
setup_locale_setting: List[Element] = _setup_locale_selector(config)
|
|
19
|
+
locale_setting: List[Element] = _locale_selector(config)
|
|
20
|
+
|
|
21
|
+
setup_locale_setting: str = setup_locale_setting[0].text \
|
|
22
|
+
if len(setup_locale_setting) > 0 \
|
|
23
|
+
else locale_setting[0].text
|
|
24
|
+
|
|
25
|
+
return float(any(fnmatch.fnmatchcase(setup_locale_setting, ptn) \
|
|
26
|
+
for ptn in rules["locale_set"]
|
|
27
|
+
)
|
|
28
|
+
)
|