Skip to content

Commit a7f43d2

Browse files
committed
feat: 新增移除 Alpha 通道功能
1 parent db1de5e commit a7f43d2

File tree

4 files changed

+108
-47
lines changed

4 files changed

+108
-47
lines changed

.gitignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# PyInstaller
2+
/dist/
3+
/build/
4+
*.spec
5+
6+
# Python
7+
__pycache__/
8+
*.pyc
9+
*.pyo
10+
*.pyd
11+
.env
12+
venv/
13+
env/
14+
15+
# User specified
16+
/测试/
17+
list_chunks.py
Lines changed: 85 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import tempfile
44
import shutil
55
import piexif
6-
import requests
6+
from urllib import request
77
from dataclasses import dataclass
88
from typing import Optional, List, Tuple
99
from PyQt5.QtWidgets import (
@@ -21,12 +21,13 @@
2121
@dataclass
2222
class 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'\x89PNG\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'\x89PNG\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))

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@
44
<img src="使用示例.png" alt="使用示例" />
55
</div>
66

7-
图片元数据消除器是一个基于 PyQt5 开发的图形用户界面工具,旨在帮助用户轻松地从图片中移除所有元数据(如 EXIF、IPTC、XMP 信息),以保护隐私或减小文件大小。
7+
图片元数据消除器是一个基于 PyQt5 开发的图形用户界面工具,旨在帮助用户轻松地从图片中移除所有元数据,以保护隐私或减小文件大小。
88

9-
该项目的代码主要由 `OpenAI o1-preview` `OpenAI o1-mini` `claude-3-5-sonnet` 编写,我提供了非常多的功能设计提议和反馈。
9+
该项目的代码主要由 `OpenAI o1-preview` `OpenAI o1-mini` `Claude 3.5 Sonnet` `Gemini 2.5 Pro Preview` 编写,我提供了非常多的功能设计提议和反馈。
1010

1111
## 功能特性
1212

1313
- **多种文件来源支持:** 支持拖拽本地、网络图片文件
1414
- **高效并发处理:** 最多同时处理3张图片
1515
- **智能格式处理:**
1616
- JPEG/WEBP:使用piexif库处理
17-
- PNG:采用专门的数据块处理方法,精确移除元数据
18-
- 其他格式:通过图像数据重构方式移除元数据
17+
- **PNG 白名单过滤**:对于PNG图片,采用白名单策略,仅保留必要的图像块(如`IHDR`, `IDAT`, `PLTE`等),彻底移除所有元数据块(如`tEXt`, `iTXt`, `zTXt`等),同时避免重新编码,保证处理速度和图片质量。
18+
- **删除Alpha通道(可选)**:勾选后,程序会将图像从 `RGBA` 转换为 `RGB` 模式,因此能有效删除隐藏在Alpha通道中的隐写信息。
1919
- **友好的错误处理:**
2020
- 详细的错误提示:通过弹窗显示具体的错误信息
2121
- 批量处理状态:显示成功/失败数量统计
@@ -51,7 +51,8 @@
5151
3. 关闭程序后临时文件会自动清理
5252

5353
4. **其他选项:**
54-
- **窗口置顶:** 勾选"窗口置顶"使窗口保持在最前
54+
- **窗口置顶:** 勾选“窗口置顶”使窗口保持在最前
55+
- **删除Alpha通道:** 勾选“删除Alpha通道”将图像从 `RGBA` 转换为 `RGB` 模式,删除隐藏在Alpha通道中的隐写信息
5556
- **状态查看:** 通过状态栏颜色直观了解处理进度
5657
- 黑色:开始处理
5758
- 蓝色:处理中

使用示例.png

-8.74 KB
Loading

0 commit comments

Comments
 (0)