DucHaiten commited on
Commit
47be4ac
·
verified ·
1 Parent(s): b180ab5

Update image_converter.py

Browse files
Files changed (1) hide show
  1. image_converter.py +473 -426
image_converter.py CHANGED
@@ -1,426 +1,473 @@
1
- import tkinter as tk
2
- from tkinter import filedialog, ttk, messagebox
3
- from PIL import Image as PILImage, ImageTk
4
- from wand.image import Image
5
- import os
6
- import queue
7
- import hashlib
8
- import threading
9
- import subprocess
10
-
11
- # Biến toàn cục để điều khiển việc dừng và lưu lỗi
12
- stop_conversion = False
13
- error_messages = []
14
- selected_files = []
15
- converted_hashes = set()
16
- save_directory = ""
17
-
18
- def open_image_converter():
19
- global stop_conversion, error_messages, selected_files, save_dir_var, format_var, status_var, num_files_var, errors_var, thread_count_var, filename_var, filter_var, progress, q, root
20
-
21
- # Tạo cửa sổ Tkinter
22
- root = tk.Tk()
23
- root.title("Image Converter")
24
-
25
- # Khởi tạo các biến Tkinter
26
- save_dir_var = tk.StringVar()
27
- format_var = tk.StringVar(value='png')
28
- status_var = tk.StringVar()
29
- num_files_var = tk.StringVar(value="0 files selected.")
30
- errors_var = tk.StringVar(value="Errors: 0")
31
- thread_count_var = tk.StringVar(value="4")
32
- filename_var = tk.StringVar()
33
- filter_var = tk.BooleanVar(value=False)
34
- progress = tk.IntVar(value=0)
35
- q = queue.Queue()
36
-
37
- # Định nghĩa các định dạng được hỗ trợ
38
- SUPPORTED_TO_PS = ['pdf', 'jpeg', 'jpg', 'tiff', 'pnm']
39
- SUPPORTED_TO_EPS = ['pdf', 'jpeg', 'jpg', 'tiff', 'pnm']
40
- NEEDS_INTERMEDIATE_CONVERSION = {
41
- 'psd': 'pdf', 'svg': 'pdf', 'png': 'pdf',
42
- 'bmp': 'pdf', 'gif': 'pdf', 'ico': 'png'
43
- }
44
-
45
- # Các hàm trợ giúp
46
- def center_window(window):
47
- window.update_idletasks()
48
- width = window.winfo_width()
49
- height = window.winfo_height()
50
- x = (window.winfo_screenwidth() // 2) - (width // 2)
51
- y = (window.winfo_screenheight() // 2) - (height // 2)
52
- window.geometry(f'{width}x{height}+{x}+{y}')
53
-
54
- def select_files():
55
- filepaths = filedialog.askopenfilenames(
56
- title="Select Files",
57
- filetypes=[("All Image files", "*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.tiff;*.tif;*.svg;*.webp;*.pdf;*.psd;*.ico"),
58
- ("JPEG files", "*.jpg;*.jpeg"),
59
- ("PNG files", "*.png"),
60
- ("GIF files", "*.gif"),
61
- ("BMP files", "*.bmp"),
62
- ("TIFF files", "*.tiff;*.tif"),
63
- ("SVG files", "*.svg"),
64
- ("WEBP files", "*.webp"),
65
- ("PDF files", "*.pdf"),
66
- ("PSD files", "*.psd"),
67
- ("ICO files", "*.ico")]
68
- )
69
- if filepaths:
70
- selected_files.clear()
71
- selected_files.extend(filepaths)
72
- num_files_var.set(f"{len(selected_files)} files selected.")
73
- update_image_preview()
74
-
75
- def choose_save_directory():
76
- global save_directory
77
- directory = filedialog.askdirectory()
78
- if directory:
79
- save_directory = directory
80
- save_dir_var.set(directory)
81
- save_dir_entry.config(state='normal')
82
- save_dir_entry.delete(0, tk.END)
83
- save_dir_entry.insert(0, directory)
84
- save_dir_entry.config(state='readonly')
85
-
86
- def hash_image(file_path):
87
- """Tạo hàm băm SHA-256 từ nội dung ảnh."""
88
- hash_sha256 = hashlib.sha256()
89
- try:
90
- file_path = os.path.normpath(file_path)
91
- with open(file_path, "rb") as f:
92
- for chunk in iter(lambda: f.read(4096), b""):
93
- hash_sha256.update(chunk)
94
- except Exception as e:
95
- print(f"Error hashing file {file_path}: {e}")
96
- return None
97
- return hash_sha256.hexdigest()
98
-
99
- def filter_duplicate_images(filepaths):
100
- unique_images = {}
101
- filtered_files = []
102
- for filepath in filepaths:
103
- image_hash = hash_image(filepath)
104
- if image_hash and image_hash not in unique_images:
105
- if image_hash not in converted_hashes:
106
- unique_images[image_hash] = filepath
107
- filtered_files.append(filepath)
108
- return filtered_files
109
-
110
- def can_convert_directly(input_format, output_format):
111
- """Kiểm tra xem thể chuyển đổi trực tiếp từ định dạng nguồn sang định dạng đích không."""
112
- if output_format == 'ps':
113
- return input_format in SUPPORTED_TO_PS
114
- elif output_format == 'eps':
115
- return input_format in SUPPORTED_TO_EPS
116
- elif output_format == 'ico':
117
- return input_format not in ['ps', 'eps', 'pdf']
118
- elif input_format == 'ico':
119
- return output_format not in ['ps', 'eps']
120
- return True
121
-
122
- def notify_conversion_path(input_format, output_format):
123
- """Hiển thị thông báo nhắc nhở người dùng về bước chuyển đổi trung gian cần thiết."""
124
- intermediate_format = NEEDS_INTERMEDIATE_CONVERSION.get(input_format, None)
125
- if intermediate_format:
126
- if output_format in ['ps', 'eps']:
127
- message = (f"Cannot convert directly from {input_format.upper()} to {output_format.upper()}. "
128
- f"Please convert to PDF first, then convert to {output_format.upper()}.")
129
- else:
130
- message = (f"Cannot convert directly from {input_format.upper()} to {output_format.upper()}. "
131
- f"Please convert to {intermediate_format.upper()} first, then convert to {output_format.upper()}.")
132
- else:
133
- message = (f"Conversion from {input_format.upper()} to {output_format.upper()} is not supported. "
134
- "Please use a different format for conversion.")
135
- messagebox.showwarning("Conversion Notice", message)
136
-
137
- def convert_image_with_wand(input_path, output_format, output_path):
138
- """Chuyển đổi ảnh sử dụng ImageMagick thông qua Wand."""
139
- try:
140
- with Image(filename=input_path) as img:
141
- img.format = output_format
142
- img.save(filename=output_path)
143
- except Exception as e:
144
- raise RuntimeError(f"Error converting {input_path} to {output_format}: {e}")
145
-
146
- def convert_image_with_ghostscript(input_path, output_format, output_path):
147
- """Chuyển đổi ảnh sử dụng Ghostscript."""
148
- try:
149
- gs_command = [
150
- "gswin64c",
151
- "-dBATCH",
152
- "-dNOPAUSE",
153
- "-sDEVICE=" + ("ps2write" if output_format == "ps" else "eps2write"),
154
- f"-sOutputFile={output_path}",
155
- input_path
156
- ]
157
- subprocess.run(gs_command, check=True, creationflags=subprocess.CREATE_NO_WINDOW)
158
- except subprocess.CalledProcessError as e:
159
- raise RuntimeError(f"Ghostscript error: {e}")
160
-
161
- def convert_image(input_path, save_directory, output_format, output_filename, q):
162
- if stop_conversion:
163
- return
164
-
165
- filename = os.path.basename(input_path)
166
- name, ext = os.path.splitext(filename)
167
- ext = ext[1:].lower()
168
- original_name = name
169
-
170
- try:
171
- if output_filename:
172
- name = output_filename
173
-
174
- output_path = os.path.join(save_directory, f"{name}.{output_format}")
175
-
176
- counter = 1
177
- while os.path.exists(output_path):
178
- new_name = f"{name} ({counter})"
179
- output_path = os.path.join(save_directory, f"{new_name}.{output_format}")
180
- counter += 1
181
-
182
- if ext in NEEDS_INTERMEDIATE_CONVERSION and output_format in ["ps", "eps"]:
183
- intermediate_format = NEEDS_INTERMEDIATE_CONVERSION[ext]
184
- intermediate_path = os.path.join(save_directory, f"{name}.{intermediate_format}")
185
-
186
- # Chuyển đổi sang định dạng trung gian
187
- convert_image_with_wand(input_path, intermediate_format, intermediate_path)
188
-
189
- # Chuyển đổi từ định dạng trung gian sang định dạng cuối
190
- if output_format in ["ps", "eps"]:
191
- convert_image_with_ghostscript(intermediate_path, output_format, output_path)
192
- else:
193
- convert_image_with_wand(intermediate_path, output_format, output_path)
194
-
195
- os.remove(intermediate_path)
196
- else:
197
- if output_format in ["ps", "eps"]:
198
- convert_image_with_ghostscript(input_path, output_format, output_path)
199
- else:
200
- convert_image_with_wand(input_path, output_format, output_path)
201
-
202
- q.put(input_path)
203
- converted_hashes.add(hash_image(output_path))
204
- except Exception as e:
205
- error_message = f"Error converting {filename}: {str(e)}"
206
- q.put(error_message)
207
- error_messages.append(error_message)
208
-
209
- def worker(save_directory, output_format, num_threads, output_filename, q, filter_duplicates):
210
- try:
211
- total_files = selected_files
212
- if filter_duplicates:
213
- total_files = filter_duplicate_images(selected_files)
214
- progress.set(0)
215
- for i, input_path in enumerate(total_files, 1):
216
- if stop_conversion:
217
- break
218
- input_format = os.path.splitext(input_path)[1][1:].lower()
219
- if not can_convert_directly(input_format, output_format):
220
- notify_conversion_path(input_format, output_format)
221
- q.put(None)
222
- return
223
-
224
- thread = threading.Thread(target=convert_image, args=(input_path, save_directory, output_format, output_filename, q))
225
- thread.start()
226
- thread.join()
227
-
228
- q.put(None)
229
- except Exception as e:
230
- if not stop_conversion:
231
- q.put(e)
232
-
233
- def update_progress():
234
- try:
235
- completed = 0
236
- while True:
237
- item = q.get()
238
- if item is None:
239
- break
240
- if isinstance(item, str):
241
- if "Error" in item:
242
- root.after(0, errors_var.set, f"Errors: {len(error_messages)}")
243
- continue
244
- completed += 1
245
- progress.set(int((completed / len(selected_files)) * 100))
246
- if not stop_conversion:
247
- root.after(0, status_var.set, f"Converted {completed} files")
248
- root.after(0, root.update_idletasks)
249
- if not stop_conversion:
250
- root.after(0, progress.set, 100)
251
- show_completion_message(completed)
252
- except Exception as e:
253
- if not stop_conversion:
254
- root.after(0, status_var.set, f"Error: {e}")
255
-
256
- def show_completion_message(completed):
257
- message = f"Conversion complete. {completed} files converted."
258
- if error_messages:
259
- message += f" {len(error_messages)} errors occurred."
260
- messagebox.showinfo("Conversion Complete", message)
261
-
262
- def convert_files():
263
- global stop_conversion, error_messages
264
- stop_conversion = False
265
- error_messages.clear()
266
- errors_var.set("Errors: 0")
267
- save_directory = save_dir_var.get()
268
- output_format = format_var.get()
269
- try:
270
- num_threads = int(thread_count_var.get() or 4)
271
- except ValueError:
272
- messagebox.showerror("Input Error", "Threads must be a number.")
273
- return
274
- output_filename = filename_var.get()
275
- filter_duplicates = filter_var.get()
276
- if not selected_files or not save_directory or not output_format:
277
- status_var.set("Please select images, output format, and save location.")
278
- return
279
-
280
- threading.Thread(target=worker, args=(save_directory, output_format, num_threads, output_filename, q, filter_duplicates)).start()
281
- threading.Thread(target=update_progress).start()
282
-
283
- def stop_conversion_func():
284
- global stop_conversion
285
- stop_conversion = True
286
- status_var.set("Conversion stopped.")
287
-
288
- def return_to_menu():
289
- stop_conversion_func()
290
- root.destroy()
291
- import main
292
- main.open_main_menu()
293
-
294
- def on_closing():
295
- return_to_menu()
296
-
297
- def show_errors():
298
- global error_window
299
- if error_window is not None:
300
- return
301
-
302
- error_window = tk.Toplevel(root)
303
- error_window.title("Error Details")
304
- error_window.geometry("500x400")
305
-
306
- error_text = tk.Text(error_window, wrap='word')
307
- error_text.pack(expand=True, fill='both')
308
-
309
- if error_messages:
310
- for error in error_messages:
311
- error_text.insert('end', error + '\n')
312
- else:
313
- error_text.insert('end', "No errors recorded.")
314
-
315
- error_text.config(state='disabled')
316
-
317
- def on_close_error_window():
318
- global error_window
319
- error_window.destroy()
320
- error_window = None
321
-
322
- error_window.protocol("WM_DELETE_WINDOW", on_close_error_window)
323
-
324
- def update_image_preview():
325
- for widget in image_preview_frame.winfo_children():
326
- widget.destroy()
327
-
328
- for i, file_path in enumerate(selected_files):
329
- thumbnail_size = (100, 100)
330
- try:
331
- image = PILImage.open(file_path)
332
- image.thumbnail(thumbnail_size)
333
- thumbnail = ImageTk.PhotoImage(image)
334
-
335
- tk.Label(image_preview_frame, image=thumbnail).grid(row=i, column=0, padx=5, pady=5)
336
- tk.Label(image_preview_frame, text=os.path.basename(file_path)).grid(row=i, column=1, padx=5, pady=5)
337
- tk.Label(image_preview_frame, text="Caption placeholder", wraplength=300).grid(row=i, column=2, padx=5, pady=5)
338
- except Exception as e:
339
- tk.Label(image_preview_frame, text="Error loading image").grid(row=i, column=0, columnspan=3, padx=5, pady=5)
340
-
341
- def validate_number(P):
342
- if P.isdigit() or P == "":
343
- return True
344
- else:
345
- messagebox.showerror("Input Error", "Please enter only numbers.")
346
- return False
347
-
348
- validate_command = root.register(validate_number)
349
-
350
- # Tạo các thành phần giao diện
351
- main_frame = tk.Frame(root)
352
- main_frame.pack(fill=tk.BOTH, expand=True)
353
-
354
- control_frame = tk.Frame(main_frame)
355
- control_frame.pack(fill=tk.X, padx=10, pady=10)
356
-
357
- back_button = tk.Button(control_frame, text="<-", font=('Helvetica', 14), command=return_to_menu)
358
- back_button.pack(side=tk.TOP, anchor='w', padx=5, pady=5)
359
-
360
- title_label = tk.Label(control_frame, text="Image Converter", font=('Helvetica', 16))
361
- title_label.pack(side=tk.TOP, padx=5, pady=5)
362
-
363
- select_button = tk.Button(control_frame, text="Select Files", command=select_files)
364
- select_button.pack(side=tk.TOP, padx=5, pady=5)
365
-
366
- num_files_label = tk.Label(control_frame, textvariable=num_files_var)
367
- num_files_label.pack(side=tk.TOP, padx=5, pady=5)
368
-
369
- save_dir_button = tk.Button(control_frame, text="Choose Save Directory", command=choose_save_directory)
370
- save_dir_button.pack(side=tk.TOP, padx=5, pady=5)
371
-
372
- save_dir_entry = tk.Entry(control_frame, textvariable=save_dir_var, state='readonly', justify='center')
373
- save_dir_entry.pack(side=tk.TOP, padx=5, pady=5, fill=tk.X)
374
-
375
- format_frame = tk.Frame(main_frame)
376
- format_frame.pack(fill=tk.X, padx=10, pady=5)
377
-
378
- format_label = tk.Label(format_frame, text="Output Format:")
379
- format_label.pack(side=tk.LEFT, padx=5)
380
-
381
- format_dropdown = ttk.Combobox(format_frame, textvariable=format_var, values=['png', 'jpg', 'gif', 'bmp', 'tiff', 'svg', 'webp', 'pdf', 'psd', 'ps', 'eps', 'ico'])
382
- format_dropdown.pack(side=tk.LEFT, padx=5)
383
-
384
- thread_count_label = tk.Label(format_frame, text="Threads:")
385
- thread_count_label.pack(side=tk.LEFT, padx=5)
386
-
387
- thread_count_entry = tk.Entry(format_frame, textvariable=thread_count_var, width=5, validate="key", validatecommand=(validate_command, '%P'), justify='center')
388
- thread_count_entry.pack(side=tk.LEFT, padx=5)
389
-
390
- filename_label = tk.Label(main_frame, text="Output Filename (optional):")
391
- filename_label.pack(fill=tk.X, padx=10, pady=5)
392
-
393
- filename_entry = tk.Entry(main_frame, textvariable=filename_var, justify='center')
394
- filename_entry.pack(fill=tk.X, padx=10, pady=5)
395
-
396
- filter_frame = tk.Frame(main_frame)
397
- filter_frame.pack(fill=tk.X, padx=10, pady=5)
398
-
399
- filter_checkbox = tk.Checkbutton(filter_frame, text="Filter duplicate images", variable=filter_var)
400
- filter_checkbox.pack(side=tk.LEFT)
401
-
402
- convert_button = tk.Button(main_frame, text="Convert", command=convert_files)
403
- convert_button.pack(pady=10)
404
-
405
- stop_button = tk.Button(main_frame, text="Stop", command=stop_conversion_func)
406
- stop_button.pack(pady=5)
407
-
408
- errors_button = tk.Button(main_frame, textvariable=errors_var, command=show_errors)
409
- errors_button.pack(pady=5)
410
-
411
- progress_bar = ttk.Progressbar(main_frame, variable=progress, maximum=100)
412
- progress_bar.pack(pady=5, fill=tk.X)
413
-
414
- status_label = tk.Label(main_frame, textvariable=status_var, fg="green")
415
- status_label.pack(pady=5)
416
-
417
- # Khung hiển thị ảnh caption
418
- image_preview_frame = tk.Frame(root)
419
- image_preview_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
420
-
421
- center_window(root)
422
- root.protocol("WM_DELETE_WINDOW", on_closing)
423
- root.mainloop()
424
-
425
- if __name__ == "__main__":
426
- open_image_converter()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tkinter as tk
2
+ from tkinter import filedialog, ttk, messagebox
3
+ from PIL import Image as PILImage, ImageTk
4
+ from wand.image import Image
5
+ import os
6
+ import queue
7
+ import hashlib
8
+ import threading
9
+ import subprocess
10
+ import time # Thêm import time
11
+
12
+ # Biến toàn cục để điều khiển việc dừng và lưu lỗi
13
+ stop_conversion = False
14
+ error_messages = []
15
+ selected_files = []
16
+ converted_hashes = set()
17
+ save_directory = ""
18
+ pause_event = threading.Event() # Thêm biến kiểm soát tạm dừng
19
+
20
+ def open_image_converter():
21
+ global stop_conversion, error_messages, selected_files, save_dir_var, format_var, status_var, num_files_var, errors_var, thread_count_var, filename_var, filter_var, progress, q, root
22
+ global total_dimension_var, delete_original_var, stop_button_text # Thêm các biến mới
23
+
24
+ # Tạo cửa sổ Tkinter
25
+ root = tk.Tk()
26
+ root.title("Image Converter")
27
+
28
+ # Khởi tạo các biến Tkinter
29
+ save_dir_var = tk.StringVar()
30
+ format_var = tk.StringVar(value='png')
31
+ status_var = tk.StringVar()
32
+ num_files_var = tk.StringVar(value="0 files selected.")
33
+ errors_var = tk.StringVar(value="Errors: 0")
34
+ thread_count_var = tk.StringVar(value="1")
35
+ filename_var = tk.StringVar()
36
+ filter_var = tk.BooleanVar(value=False)
37
+ progress = tk.IntVar(value=0)
38
+ q = queue.Queue()
39
+ total_dimension_var = tk.StringVar() # Biến cho tổng chiều dài và chiều rộng
40
+ delete_original_var = tk.BooleanVar(value=False) # Biến cho ô tích xóa ảnh gốc
41
+ stop_button_text = tk.StringVar(value="Stop") # Biến để thay đổi text nút Stop/Continue
42
+
43
+ # Định nghĩa các định dạng được hỗ trợ
44
+ SUPPORTED_TO_PS = ['pdf', 'jpeg', 'jpg', 'tiff', 'pnm']
45
+ SUPPORTED_TO_EPS = ['pdf', 'jpeg', 'jpg', 'tiff', 'pnm']
46
+ NEEDS_INTERMEDIATE_CONVERSION = {
47
+ 'psd': 'pdf', 'svg': 'pdf', 'png': 'pdf',
48
+ 'bmp': 'pdf', 'gif': 'pdf', 'ico': 'png'
49
+ }
50
+
51
+ # Các hàm trợ giúp
52
+ def center_window(window):
53
+ window.update_idletasks()
54
+ width = window.winfo_width()
55
+ height = window.winfo_height()
56
+ x = (window.winfo_screenwidth() // 2) - (width // 2)
57
+ y = (window.winfo_screenheight() // 2) - (height // 2)
58
+ window.geometry(f'{width}x{height}+{x}+{y}')
59
+
60
+ def select_files():
61
+ filepaths = filedialog.askopenfilenames(
62
+ title="Select Files",
63
+ filetypes=[("All Image files", "*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.tiff;*.tif;*.svg;*.webp;*.pdf;*.psd;*.ico"),
64
+ ("JPEG files", "*.jpg;*.jpeg"),
65
+ ("PNG files", "*.png"),
66
+ ("GIF files", "*.gif"),
67
+ ("BMP files", "*.bmp"),
68
+ ("TIFF files", "*.tiff;*.tif"),
69
+ ("SVG files", "*.svg"),
70
+ ("WEBP files", "*.webp"),
71
+ ("PDF files", "*.pdf"),
72
+ ("PSD files", "*.psd"),
73
+ ("ICO files", "*.ico")]
74
+ )
75
+ if filepaths:
76
+ selected_files.clear()
77
+ selected_files.extend(filepaths)
78
+ num_files_var.set(f"{len(selected_files)} files selected.")
79
+ update_image_preview()
80
+
81
+ def choose_save_directory():
82
+ global save_directory
83
+ directory = filedialog.askdirectory()
84
+ if directory:
85
+ save_directory = directory
86
+ save_dir_var.set(directory)
87
+ save_dir_entry.config(state='normal')
88
+ save_dir_entry.delete(0, tk.END)
89
+ save_dir_entry.insert(0, directory)
90
+ save_dir_entry.config(state='readonly')
91
+
92
+ def hash_image(file_path):
93
+ """Tạo hàm băm SHA-256 từ nội dung ảnh."""
94
+ hash_sha256 = hashlib.sha256()
95
+ try:
96
+ file_path = os.path.normpath(file_path)
97
+ with open(file_path, "rb") as f:
98
+ for chunk in iter(lambda: f.read(4096), b""):
99
+ hash_sha256.update(chunk)
100
+ except Exception as e:
101
+ print(f"Error hashing file {file_path}: {e}")
102
+ return None
103
+ return hash_sha256.hexdigest()
104
+
105
+ def filter_duplicate_images(filepaths):
106
+ unique_images = {}
107
+ filtered_files = []
108
+ for filepath in filepaths:
109
+ image_hash = hash_image(filepath)
110
+ if image_hash and image_hash not in unique_images:
111
+ if image_hash not in converted_hashes:
112
+ unique_images[image_hash] = filepath
113
+ filtered_files.append(filepath)
114
+ return filtered_files
115
+
116
+ def can_convert_directly(input_format, output_format):
117
+ """Kiểm tra xem thể chuyển đổi trực tiếp từ định dạng nguồn sang định dạng đích không."""
118
+ if output_format == 'ps':
119
+ return input_format in SUPPORTED_TO_PS
120
+ elif output_format == 'eps':
121
+ return input_format in SUPPORTED_TO_EPS
122
+ elif output_format == 'ico':
123
+ return input_format not in ['ps', 'eps', 'pdf']
124
+ elif input_format == 'ico':
125
+ return output_format not in ['ps', 'eps']
126
+ return True
127
+
128
+ def notify_conversion_path(input_format, output_format):
129
+ """Hiển thị thông báo nhắc nhở người dùng về bước chuyển đổi trung gian cần thiết."""
130
+ intermediate_format = NEEDS_INTERMEDIATE_CONVERSION.get(input_format, None)
131
+ if intermediate_format:
132
+ if output_format in ['ps', 'eps']:
133
+ message = (f"Cannot convert directly from {input_format.upper()} to {output_format.upper()}. "
134
+ f"Please convert to PDF first, then convert to {output_format.upper()}.")
135
+ else:
136
+ message = (f"Cannot convert directly from {input_format.upper()} to {output_format.upper()}. "
137
+ f"Please convert to {intermediate_format.upper()} first, then convert to {output_format.upper()}.")
138
+ else:
139
+ message = (f"Conversion from {input_format.upper()} to {output_format.upper()} is not supported. "
140
+ "Please use a different format for conversion.")
141
+ messagebox.showwarning("Conversion Notice", message)
142
+
143
+ def convert_image_with_wand(input_path, output_format, output_path, total_dimension=None):
144
+ """Chuyển đổi ảnh sử dụng ImageMagick thông qua Wand."""
145
+ try:
146
+ with Image(filename=input_path) as img:
147
+ # Thay đổi kích thước nếu total_dimension được cung cấp
148
+ if total_dimension:
149
+ width = img.width
150
+ height = img.height
151
+ aspect_ratio = width / height
152
+ total_dim = float(total_dimension)
153
+ new_height = int(total_dim / (aspect_ratio + 1))
154
+ new_width = int(aspect_ratio * new_height)
155
+ img.resize(new_width, new_height)
156
+ img.format = output_format
157
+ img.save(filename=output_path)
158
+ except Exception as e:
159
+ raise RuntimeError(f"Error converting {input_path} to {output_format}: {e}")
160
+
161
+ def convert_image_with_ghostscript(input_path, output_format, output_path):
162
+ """Chuyển đổi ảnh sử dụng Ghostscript."""
163
+ try:
164
+ gs_command = [
165
+ "gswin64c",
166
+ "-dBATCH",
167
+ "-dNOPAUSE",
168
+ "-sDEVICE=" + ("ps2write" if output_format == "ps" else "eps2write"),
169
+ f"-sOutputFile={output_path}",
170
+ input_path
171
+ ]
172
+ subprocess.run(gs_command, check=True, creationflags=subprocess.CREATE_NO_WINDOW)
173
+ except subprocess.CalledProcessError as e:
174
+ raise RuntimeError(f"Ghostscript error: {e}")
175
+
176
+ def convert_image(input_path, save_directory, output_format, output_filename, q, total_dimension, delete_original):
177
+ if stop_conversion:
178
+ return
179
+
180
+ filename = os.path.basename(input_path)
181
+ name, ext = os.path.splitext(filename)
182
+ ext = ext[1:].lower()
183
+ original_name = name
184
+
185
+ try:
186
+ if output_filename:
187
+ name = output_filename
188
+
189
+ output_path = os.path.join(save_directory, f"{name}.{output_format}")
190
+
191
+ counter = 1
192
+ while os.path.exists(output_path):
193
+ new_name = f"{name} ({counter})"
194
+ output_path = os.path.join(save_directory, f"{new_name}.{output_format}")
195
+ counter += 1
196
+
197
+ if ext in NEEDS_INTERMEDIATE_CONVERSION and output_format in ["ps", "eps"]:
198
+ intermediate_format = NEEDS_INTERMEDIATE_CONVERSION[ext]
199
+ intermediate_path = os.path.join(save_directory, f"{name}.{intermediate_format}")
200
+
201
+ # Chuyển đổi sang định dạng trung gian
202
+ convert_image_with_wand(input_path, intermediate_format, intermediate_path, total_dimension)
203
+
204
+ # Chuyển đổi từ định dạng trung gian sang định dạng cuối
205
+ if output_format in ["ps", "eps"]:
206
+ convert_image_with_ghostscript(intermediate_path, output_format, output_path)
207
+ else:
208
+ convert_image_with_wand(intermediate_path, output_format, output_path, total_dimension)
209
+
210
+ os.remove(intermediate_path)
211
+ else:
212
+ if output_format in ["ps", "eps"]:
213
+ convert_image_with_ghostscript(input_path, output_format, output_path)
214
+ else:
215
+ convert_image_with_wand(input_path, output_format, output_path, total_dimension)
216
+
217
+ q.put(input_path)
218
+ converted_hashes.add(hash_image(output_path))
219
+
220
+ # Xóa ảnh gốc nếu được chọn
221
+ if delete_original:
222
+ os.remove(input_path)
223
+ except Exception as e:
224
+ error_message = f"Error converting {filename}: {str(e)}"
225
+ q.put(error_message)
226
+ error_messages.append(error_message)
227
+
228
+ def worker(save_directory, output_format, num_threads, output_filename, q, filter_duplicates, total_dimension, delete_original):
229
+ try:
230
+ total_files = selected_files
231
+ if filter_duplicates:
232
+ total_files = filter_duplicate_images(selected_files)
233
+ progress.set(0)
234
+ for i, input_path in enumerate(total_files, 1):
235
+ if stop_conversion:
236
+ break
237
+ while pause_event.is_set():
238
+ time.sleep(0.1)
239
+ if stop_conversion:
240
+ break
241
+ input_format = os.path.splitext(input_path)[1][1:].lower()
242
+ if not can_convert_directly(input_format, output_format):
243
+ notify_conversion_path(input_format, output_format)
244
+ q.put(None)
245
+ return
246
+
247
+ thread = threading.Thread(target=convert_image, args=(input_path, save_directory, output_format, output_filename, q, total_dimension, delete_original))
248
+ thread.start()
249
+ thread.join()
250
+
251
+ q.put(None)
252
+ except Exception as e:
253
+ if not stop_conversion:
254
+ q.put(e)
255
+
256
+ def update_progress():
257
+ try:
258
+ completed = 0
259
+ while True:
260
+ item = q.get()
261
+ if item is None:
262
+ break
263
+ if isinstance(item, str):
264
+ if "Error" in item:
265
+ root.after(0, errors_var.set, f"Errors: {len(error_messages)}")
266
+ continue
267
+ completed += 1
268
+ progress.set(int((completed / len(selected_files)) * 100))
269
+ if not stop_conversion:
270
+ root.after(0, status_var.set, f"Converted {completed} files")
271
+ root.after(0, root.update_idletasks)
272
+ if not stop_conversion:
273
+ root.after(0, progress.set, 100)
274
+ show_completion_message(completed)
275
+ except Exception as e:
276
+ if not stop_conversion:
277
+ root.after(0, status_var.set, f"Error: {e}")
278
+
279
+ def show_completion_message(completed):
280
+ message = f"Conversion complete. {completed} files converted."
281
+ if error_messages:
282
+ message += f" {len(error_messages)} errors occurred."
283
+ messagebox.showinfo("Conversion Complete", message)
284
+
285
+ def convert_files():
286
+ global stop_conversion, error_messages
287
+ stop_conversion = False
288
+ pause_event.clear()
289
+ stop_button_text.set("Stop")
290
+ error_messages.clear()
291
+ errors_var.set("Errors: 0")
292
+ save_directory = save_dir_var.get()
293
+ output_format = format_var.get()
294
+ try:
295
+ num_threads = int(thread_count_var.get() or 4)
296
+ except ValueError:
297
+ messagebox.showerror("Input Error", "Threads must be a number.")
298
+ return
299
+ output_filename = filename_var.get()
300
+ filter_duplicates = filter_var.get()
301
+ total_dimension = total_dimension_var.get() or None
302
+ delete_original = delete_original_var.get()
303
+ if not selected_files or not save_directory or not output_format:
304
+ status_var.set("Please select images, output format, and save location.")
305
+ return
306
+
307
+ threading.Thread(target=worker, args=(save_directory, output_format, num_threads, output_filename, q, filter_duplicates, total_dimension, delete_original)).start()
308
+ threading.Thread(target=update_progress).start()
309
+
310
+ def stop_conversion_func():
311
+ global stop_conversion
312
+ if stop_button_text.get() == "Stop":
313
+ pause_event.set()
314
+ stop_button_text.set("Continue")
315
+ status_var.set("Conversion paused.")
316
+ else:
317
+ pause_event.clear()
318
+ stop_button_text.set("Stop")
319
+ status_var.set("Conversion resumed.")
320
+
321
+ def return_to_menu():
322
+ stop_conversion_func()
323
+ root.destroy()
324
+ import main
325
+ main.open_main_menu()
326
+
327
+ def on_closing():
328
+ return_to_menu()
329
+
330
+ error_window = None # Thêm biến để kiểm soát cửa sổ lỗi
331
+
332
+ def show_errors():
333
+ global error_window
334
+ if error_window is not None:
335
+ return
336
+
337
+ error_window = tk.Toplevel(root)
338
+ error_window.title("Error Details")
339
+ error_window.geometry("500x400")
340
+
341
+ error_text = tk.Text(error_window, wrap='word')
342
+ error_text.pack(expand=True, fill='both')
343
+
344
+ if error_messages:
345
+ for error in error_messages:
346
+ error_text.insert('end', error + '\n')
347
+ else:
348
+ error_text.insert('end', "No errors recorded.")
349
+
350
+ error_text.config(state='disabled')
351
+
352
+ def on_close_error_window():
353
+ global error_window
354
+ error_window.destroy()
355
+ error_window = None
356
+
357
+ error_window.protocol("WM_DELETE_WINDOW", on_close_error_window)
358
+
359
+ def update_image_preview():
360
+ for widget in image_preview_frame.winfo_children():
361
+ widget.destroy()
362
+
363
+ for i, file_path in enumerate(selected_files):
364
+ thumbnail_size = (100, 100)
365
+ try:
366
+ image = PILImage.open(file_path)
367
+ image.thumbnail(thumbnail_size)
368
+ thumbnail = ImageTk.PhotoImage(image)
369
+
370
+ tk.Label(image_preview_frame, image=thumbnail).grid(row=i, column=0, padx=5, pady=5)
371
+ tk.Label(image_preview_frame, text=os.path.basename(file_path)).grid(row=i, column=1, padx=5, pady=5)
372
+ tk.Label(image_preview_frame, text="Caption placeholder", wraplength=300).grid(row=i, column=2, padx=5, pady=5)
373
+ image_preview_frame.image = thumbnail # Giữ tham chiếu để tránh bị xóa
374
+ except Exception as e:
375
+ tk.Label(image_preview_frame, text="Error loading image").grid(row=i, column=0, columnspan=3, padx=5, pady=5)
376
+
377
+ def validate_number(P):
378
+ if P.isdigit() or P == "":
379
+ return True
380
+ else:
381
+ messagebox.showerror("Input Error", "Please enter only numbers.")
382
+ return False
383
+
384
+ validate_command = root.register(validate_number)
385
+
386
+ # Tạo các thành phần giao diện
387
+ main_frame = tk.Frame(root)
388
+ main_frame.pack(fill=tk.BOTH, expand=True)
389
+
390
+ control_frame = tk.Frame(main_frame)
391
+ control_frame.pack(fill=tk.X, padx=10, pady=10)
392
+
393
+ back_button = tk.Button(control_frame, text="<-", font=('Helvetica', 14), command=return_to_menu)
394
+ back_button.pack(side=tk.TOP, anchor='w', padx=5, pady=5)
395
+
396
+ title_label = tk.Label(control_frame, text="Image Converter", font=('Helvetica', 16))
397
+ title_label.pack(side=tk.TOP, padx=5, pady=5)
398
+
399
+ select_button = tk.Button(control_frame, text="Select Files", command=select_files)
400
+ select_button.pack(side=tk.TOP, padx=5, pady=5)
401
+
402
+ num_files_label = tk.Label(control_frame, textvariable=num_files_var)
403
+ num_files_label.pack(side=tk.TOP, padx=5, pady=5)
404
+
405
+ save_dir_button = tk.Button(control_frame, text="Choose Save Directory", command=choose_save_directory)
406
+ save_dir_button.pack(side=tk.TOP, padx=5, pady=5)
407
+
408
+ save_dir_entry = tk.Entry(control_frame, textvariable=save_dir_var, state='readonly', justify='center')
409
+ save_dir_entry.pack(side=tk.TOP, padx=5, pady=5, fill=tk.X)
410
+
411
+ format_frame = tk.Frame(main_frame)
412
+ format_frame.pack(fill=tk.X, padx=10, pady=5)
413
+
414
+ format_label = tk.Label(format_frame, text="Output Format:")
415
+ format_label.pack(side=tk.LEFT, padx=5)
416
+
417
+ format_dropdown = ttk.Combobox(format_frame, textvariable=format_var, values=['png', 'jpg', 'gif', 'bmp', 'tiff', 'svg', 'webp', 'pdf', 'psd', 'ps', 'eps', 'ico'])
418
+ format_dropdown.pack(side=tk.LEFT, padx=5)
419
+
420
+ thread_count_label = tk.Label(format_frame, text="Threads:")
421
+ thread_count_label.pack(side=tk.LEFT, padx=5)
422
+
423
+ thread_count_entry = tk.Entry(format_frame, textvariable=thread_count_var, width=5, validate="key", validatecommand=(validate_command, '%P'), justify='center')
424
+ thread_count_entry.pack(side=tk.LEFT, padx=5)
425
+
426
+ filename_label = tk.Label(main_frame, text="Output Filename (optional):")
427
+ filename_label.pack(fill=tk.X, padx=10, pady=5)
428
+
429
+ filename_entry = tk.Entry(main_frame, textvariable=filename_var, justify='center')
430
+ filename_entry.pack(fill=tk.X, padx=10, pady=5)
431
+
432
+ # Thêm ô nhập cho tổng chiều dài và chiều rộng
433
+ total_dimension_label = tk.Label(main_frame, text="Nhập tổng chiều dài và chiều rộng (optional):")
434
+ total_dimension_label.pack(fill=tk.X, padx=10, pady=5)
435
+
436
+ total_dimension_entry = tk.Entry(main_frame, textvariable=total_dimension_var, justify='center', validate="key", validatecommand=(validate_command, '%P'))
437
+ total_dimension_entry.pack(fill=tk.X, padx=10, pady=5)
438
+
439
+ filter_frame = tk.Frame(main_frame)
440
+ filter_frame.pack(fill=tk.X, padx=10, pady=5)
441
+
442
+ filter_checkbox = tk.Checkbutton(filter_frame, text="Filter duplicate images", variable=filter_var)
443
+ filter_checkbox.pack(side=tk.LEFT)
444
+
445
+ # Thêm ô tích để xóa ảnh gốc sau khi chuyển đổi
446
+ delete_original_checkbox = tk.Checkbutton(filter_frame, text="Xóa ảnh gốc sau khi chuyển đổi", variable=delete_original_var)
447
+ delete_original_checkbox.pack(side=tk.LEFT, padx=5)
448
+
449
+ convert_button = tk.Button(main_frame, text="Convert", command=convert_files)
450
+ convert_button.pack(pady=10)
451
+
452
+ stop_button = tk.Button(main_frame, textvariable=stop_button_text, command=stop_conversion_func)
453
+ stop_button.pack(pady=5)
454
+
455
+ errors_button = tk.Button(main_frame, textvariable=errors_var, command=show_errors)
456
+ errors_button.pack(pady=5)
457
+
458
+ progress_bar = ttk.Progressbar(main_frame, variable=progress, maximum=100)
459
+ progress_bar.pack(pady=5, fill=tk.X)
460
+
461
+ status_label = tk.Label(main_frame, textvariable=status_var, fg="green")
462
+ status_label.pack(pady=5)
463
+
464
+ # Khung hiển thị ảnh và caption
465
+ image_preview_frame = tk.Frame(root)
466
+ image_preview_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
467
+
468
+ center_window(root)
469
+ root.protocol("WM_DELETE_WINDOW", on_closing)
470
+ root.mainloop()
471
+
472
+ if __name__ == "__main__":
473
+ open_image_converter()