33import tempfile
44import shutil
55import piexif
6- import requests
6+ from urllib import request
77from dataclasses import dataclass
88from typing import Optional , List , Tuple
99from PyQt5 .QtWidgets import (
2121@dataclass
2222class ImageTask :
2323 """图片处理任务"""
24- def __init__ (self , index : int , data_type : str , data : str , save_path : str = None , original_name : str = None ):
24+ def __init__ (self , index : int , data_type : str , data : str , save_path : str = None , original_name : str = None , remove_alpha : bool = False ):
2525 self .index = index
2626 self .data_type = data_type
2727 self .data = data
2828 self .save_path = save_path
2929 self .original_name = original_name
30+ self .remove_alpha = remove_alpha
3031 self .result = None
3132 self .error = None
3233
@@ -46,7 +47,7 @@ def __init__(self, task: ImageTask):
4647 def run (self ):
4748 try :
4849 if self .task .data_type == 'file' :
49- success = remove_exif (self .task .data , self .task .save_path )
50+ success = remove_exif (self .task .data , self .task .save_path , self . task . remove_alpha )
5051 else :
5152 # 对于非文件类型,先保存为临时文件
5253 temp_buffer = tempfile .NamedTemporaryFile (delete = False )
@@ -56,11 +57,11 @@ def run(self):
5657 self .task .data .save (buffer , "PNG" )
5758 temp_buffer .write (buffer .data ())
5859 else : # URL
59- response = requests . get (self .task .data )
60- temp_buffer .write (response .content )
60+ with request . urlopen (self .task .data ) as response :
61+ temp_buffer .write (response .read () )
6162 temp_buffer .close ()
6263
63- success = remove_exif (temp_buffer .name , self .task .save_path )
64+ success = remove_exif (temp_buffer .name , self .task .save_path , self . task . remove_alpha )
6465 os .unlink (temp_buffer .name )
6566
6667 if not success :
@@ -85,6 +86,8 @@ def __init__(self):
8586 self .save_checkbox_state = self .settings .value ('save_checkbox_state' , False , type = bool )
8687 self .always_on_top_state = self .settings .value ('always_on_top_state' , False , type = bool )
8788
89+ self .remove_alpha_state = self .settings .value ('remove_alpha_state' , False , type = bool )
90+
8891 # 初始化线程池
8992 self .threadpool = QThreadPool ()
9093 self .threadpool .setMaxThreadCount (3 ) # 最多3个线程
@@ -114,6 +117,9 @@ def __init__(self):
114117 self .always_on_top_checkbox .setChecked (self .always_on_top_state )
115118 self .toggle_always_on_top (Qt .Checked if self .always_on_top_state else Qt .Unchecked )
116119
120+ # 设置"删除Alpha通道"复选框状态
121+ self .remove_alpha_checkbox .setChecked (self .remove_alpha_state )
122+
117123 # 检查保存目录状态并设置提示信息
118124 self .update_directory_status ()
119125
@@ -145,6 +151,11 @@ def initUI(self):
145151 self .copy_button .clicked .connect (self .copy_results )
146152 self .layout .addWidget (self .copy_button )
147153
154+ # 新增的删除Alpha通道功能
155+ self .remove_alpha_checkbox = QCheckBox ('删除Alpha通道' )
156+ self .remove_alpha_checkbox .stateChanged .connect (self .toggle_remove_alpha )
157+ self .layout .addWidget (self .remove_alpha_checkbox )
158+
148159 # 新增的窗口置顶功能
149160 self .always_on_top_checkbox = QCheckBox ('窗口置顶' )
150161 self .always_on_top_checkbox .stateChanged .connect (self .toggle_always_on_top )
@@ -213,6 +224,9 @@ def show_error_dialog(self):
213224 # 清空错误列表
214225 self .errors .clear ()
215226
227+ def toggle_remove_alpha (self , state ):
228+ self .settings .setValue ('remove_alpha_state' , state == Qt .Checked )
229+
216230 def toggle_always_on_top (self , state ):
217231 self .settings .setValue ('always_on_top_state' , state == Qt .Checked ) # 保存“窗口置顶”复选框状态
218232 if state == Qt .Checked :
@@ -393,6 +407,7 @@ def closeEvent(self, event):
393407 self .settings .setValue ('save_directory' , self .save_directory )
394408 self .settings .setValue ('save_checkbox_state' , self .save_checkbox .isChecked ())
395409 self .settings .setValue ('always_on_top_state' , self .always_on_top_checkbox .isChecked ())
410+ self .settings .setValue ('remove_alpha_state' , self .remove_alpha_checkbox .isChecked ())
396411
397412 # 清理临时文件
398413 for temp_file in self .temp_files :
@@ -441,60 +456,88 @@ def process_images(self):
441456 self .temp_files .append (tmp .name )
442457
443458 # 创建任务
444- task = ImageTask (i , data_type , data , save_path , original_name )
459+ remove_alpha = self .remove_alpha_checkbox .isChecked ()
460+ task = ImageTask (i , data_type , data , save_path , original_name , remove_alpha )
445461 worker = ImageWorker (task )
446462 worker .signals .finished .connect (self .handle_worker_finished )
447463 worker .signals .error .connect (self .handle_worker_error )
448464 self .threadpool .start (worker )
449465
450466 self .update_progress (0 , self .total_tasks )
451467
452- def remove_exif (src_path , dst_path ):
453- """直接移除图片的元数据,不重新编码图片内容 """
468+ def remove_exif (src_path , dst_path , remove_alpha = False ):
469+ """移除图片的元数据,并可选择移除Alpha通道。 """
454470 try :
455- # 先尝试用 PIL 检查图片格式
456471 with Image .open (src_path ) as img :
457- format = img .format .upper ()
472+ # 如果请求移除Alpha通道,这将是主要路径,因为必须重新编码图像。
473+ if remove_alpha :
474+ img_to_save = None
475+ if img .mode in ('RGBA' , 'LA' ):
476+ img_to_save = img .convert ('RGB' if img .mode == 'RGBA' else 'L' )
477+ elif img .mode == 'P' and 'transparency' in img .info :
478+ img_to_save = img .convert ('RGBA' ).convert ('RGB' )
479+
480+ if img_to_save :
481+ # 保存转换后的图像。Pillow在保存时会去除元数据。
482+ img_to_save .save (dst_path , format = img .format )
483+ return True
484+ # 如果没有找到Alpha通道,则继续执行下面的标准元数据移除流程。
485+
486+ # --- 标准元数据移除流程 ---
487+ img_format = img .format .upper ()
458488
459- # 对于 JPEG/WEBP 使用 piexif
460- if format in ['JPEG' , 'WEBP' ]:
489+ if img_format in ['JPEG' , 'WEBP' ]:
461490 try :
462491 piexif .remove (src_path , dst_path )
463492 return True
464- except Exception as e :
465- raise Exception (f"处理 { format } 格式时出错: { str (e )} " )
493+ except Exception :
494+ # 如果piexif失败,回退到重新保存
495+ img .save (dst_path , format = img .format )
496+ return True
466497
467- # 对于 PNG 格式
468- elif format == 'PNG' :
498+ elif img_format == 'PNG' :
469499 try :
470- with open (src_path , 'rb' ) as src :
471- data = src .read ()
472- if data .startswith (b'\x89 PNG\r \n \x1a \n ' ):
473- pos = 8
474- chunks = []
475- while pos < len (data ):
476- length = int .from_bytes (data [pos :pos + 4 ], 'big' )
477- chunk_type = data [pos + 4 :pos + 8 ]
478- if chunk_type not in [b'tEXt' , b'iTXt' , b'zTXt' ]:
479- chunks .append (data [pos :pos + length + 12 ])
480- pos += length + 12
481-
482- with open (dst_path , 'wb' ) as dst :
483- dst .write (data [:8 ])
484- for chunk in chunks :
485- dst .write (chunk )
486- return True
500+ chunks_to_keep = [
501+ b'IHDR' , b'PLTE' , b'IDAT' , b'IEND' ,
502+ b'sRGB' , b'gAMA' , b'pHYs' , b'cHRM' ,
503+ b'tRNS' , b'bKGD' , b'iCCP'
504+ ]
505+
506+ with open (src_path , 'rb' ) as src_file :
507+ data = src_file .read ()
508+
509+ if not data .startswith (b'\x89 PNG\r \n \x1a \n ' ):
510+ raise Exception ("无效的PNG文件(缺少签名)。" )
511+
512+ new_png_data = bytearray ()
513+ new_png_data .extend (data [:8 ])
514+
515+ pos = 8
516+ while pos < len (data ):
517+ length = int .from_bytes (data [pos :pos + 4 ], 'big' )
518+ chunk_type = data [pos + 4 :pos + 8 ]
519+
520+ if chunk_type in chunks_to_keep :
521+ new_png_data .extend (data [pos : pos + 12 + length ])
522+
523+ pos += (12 + length )
524+
525+ if chunk_type == b'IEND' :
526+ break
527+
528+ with open (dst_path , 'wb' ) as dst_file :
529+ dst_file .write (new_png_data )
530+ return True
487531 except Exception as e :
488532 raise Exception (f"处理 PNG 格式时出错: { str (e )} " )
489533
490- # 对于其他格式,复制图像数据到新图片
491- try :
492- new_img = Image .new (img .mode , img .size )
493- new_img .putdata (list (img .getdata ()))
494- new_img .save (dst_path , format = format )
495- return True
496- except Exception as e :
497- raise Exception (f"处理 { format } 格式图像时出错: { str (e )} " )
534+ # 对于其他格式
535+ else :
536+ try :
537+ img .save (dst_path , format = img .format )
538+ return True
539+ except Exception as e :
540+ raise Exception (f"处理 { img_format } 格式图像时出错: { str (e )} " )
498541
499542 except Exception as e :
500543 raise Exception (str (e ))
0 commit comments