[Python] 纯文本查看 复制代码
import tkinter as tk
from tkinter import filedialog, messagebox
import os
from PIL import Image
import numpy as np
from skimage.filters import threshold_sauvola
from io import BytesIO
import threading
from sklearn.cluster import KMeans
import io
import fitz
import queue
import subprocess
import time
#等待文件解除占用的函数
def wait_file(file_name, check_interval=0.2):
"""
持续等待直到指定的文件可访问。
:param file_name: 文件的路径
:param check_interval: 检查文件状态的时间间隔(秒)
"""
while True:
# 检查文件是否存在
if os.path.exists(file_name):
# 检查文件是否可读(也可以检查可写权限,根据需要)
if os.access(file_name, os.R_OK):
print(f"文件 '{file_name}' 现在可以访问了。")
return
else:
print(f"文件 '{file_name}' 存在,但不可读。")
else:
print(f"文件 '{file_name}' 尚不存在。")
return
# 等待一段时间后再次检查
time.sleep(check_interval)
#输出图像到输出目录的函数
def save_image_to_pdf_folder(image, output_folder, pdf_file_name, page_num):
# 创建一个以PDF文件名命名的文件夹
pdf_folder_name = os.path.splitext(pdf_file_name)[0]
pdf_folder_path = os.path.join(output_folder, pdf_folder_name)
# 如果文件夹不存在,则创建它
if not os.path.exists(pdf_folder_path):
os.makedirs(pdf_folder_path)
# 定义图像保存的文件名基础
image_file_base_name = f'{page_num}'
image_file_name = f'{image_file_base_name}.png'
image_file_path = os.path.join(pdf_folder_path, image_file_name)
# 检查文件是否存在,如果存在,则添加数字后缀
counter = 1
while os.path.exists(image_file_path):
image_file_name = f'{image_file_base_name}_{counter}.png'
image_file_path = os.path.join(pdf_folder_path, image_file_name)
counter += 1
# 保存图像
if isinstance(image, Image.Image):
# 如果image是PIL.Image.Image对象,则使用save方法保存
image.save(image_file_path, 'PNG')
print(1111)
elif isinstance(image, io.BytesIO):
# 如果image是BytesIO对象,则写入文件
with open(image_file_path, 'wb') as f:
image.seek(0)
f.write(image.read())
print(11221)
elif isinstance(image, str) and os.path.isfile(image):
# 如果image是文件名字符串,并且文件存在,则复制文件
import shutil
shutil.copy(image, image_file_path)
print(11331)
else:
raise TypeError("Unsupported image type. Must be PIL.Image.Image, io.BytesIO, or a valid file path string.")
print(f"Image saved to {image_file_path}")
# 在app中创建一个队列
page_queue = queue.Queue()
#更新状态的函数
def update_current_page_label():
try:
current_page_info = page_queue.get_nowait()
current_page_label.config(text=current_page_info)
except queue.Empty:
pass
app.after(100, update_current_page_label) # 每100毫秒更新一次
#减少颜色的函数
def kmeans(im, km, kuandu, quant):
if km > 256:
km = 256
# 确保 im 是一个 PIL Image 对象
if not isinstance(im, Image.Image):
raise ValueError("im 必须是 PIL Image 对象")
im = resize_im(im, kuandu)
# 将图像转换为 RGB 模式
im = im.convert('RGB')
# 统计图像中所有唯一的颜色数量
im2 = resize_im(im, im.width // 3)
unique_colors = len(np.unique(im2, axis=0))
#避免只有一种颜色
if unique_colors < 2:
img_buffer = BytesIO()
im.save(img_buffer, format='TIFF')
img_buffer.seek(0)
return img_buffer
else:
# 将图像数据转换为二维数组,每个像素一个条目,每个条目包含RGB三个通道的值
image_data = np.array(im).reshape(-1, 3)
q = km
# 如果唯一颜色数量小于k,则将k设置为颜色数量
km = min(km, unique_colors)
# 使用KMeans算法对颜色进行聚类,k为聚类的数量
kmeans = KMeans(n_clusters=km, random_state=0).fit(image_data)
# 获取聚类中心,即新的颜色
centers = kmeans.cluster_centers_
# 将图像数据量化为聚类中心的颜色
quantized_data = centers[kmeans.labels_]
# 将量化后的数据重塑为原始图像的形状
quantized_image = quantized_data.reshape(im.height, im.width, 3)
# 将量化后的数组转换回图像
quantized_image = Image.fromarray(quantized_image.astype('uint8'))
# 保存量化后的图像到临时文件
imgbuffer = 'temp_image.png'
quantized_image.save(imgbuffer, format="PNG", optimize=True)
# 调用外部程序pngquant来进一步压缩图像
path = 'pngquant.exe'
cmd = f'"{path}" -o temp_image.png --force --quality=0-1 --verbose {km} temp_image.png'
try:
subprocess.run(cmd, shell=True, check=True)
except subprocess.CalledProcessError as e:
print(f"Error executing pngquant: {e}")
return None
# 重新打开压缩后的图像
image = Image.open(imgbuffer)
img_buffer = BytesIO()
image.save(img_buffer, format="TIFF")
img_buffer.seek(0) # 重置 BytesIO 对象到开始位置
return img_buffer
#创建线程处理pdf的函数
def start_process_pdf(files_list, output_folder, choice, quality, kuandu, sau, k, weishendu, quant, daochutupian, pinghua):
thread = threading.Thread(target=process_pdf_thread, args=(files_list, output_folder, choice, quality, kuandu, sau, k, weishendu, quant, daochutupian, pinghua))
thread.start()
# 计算新的图像宽度和高度,保持宽高比
def resize_im(im, kuandu):
if kuandu < im.width and kuandu > 0:
new_width = kuandu
original_width, original_height = im.size
aspect_ratio = original_height / original_width
new_height = round(new_width * aspect_ratio)
else:
new_width = im.width
new_height = im.height
# 改变图像分辨率
im = im.resize((new_width, new_height), Image.Resampling.LANCZOS)
return im
# 计算新的图像宽度和高度,保持宽高比,改变质量
def resize_quality_image(im, kuandu, quality):
# 计算新的图像宽度和高度,保持宽高比
im = resize_im(im, kuandu)
# 将图像保存到内存中的BytesIO对象
imgbuffer = BytesIO()
im.save(imgbuffer, format="JPEG", quality=quality)
imgbuffer.seek(0) # 重置文件指针到开始位置
return imgbuffer
#pix = fitz.Pixmap(imgbuffer)
#return pix
#黑白二值化的函数
def blacky(im, kuandu, sau, pinghua, weishendu, daochutupian, pdf_file, page_num, xref):
try:
print(kuandu)
# 1. 改变图像分辨率
if pinghua_var.get() == "0":
im = resize_im(im, kuandu)
# 2. 转换图片为灰度模式
im = im.convert('L')
# 3. 将图像转换为numpy数组
image_array = np.array(im)
# 4. 应用Sauvola算法
threshold = threshold_sauvola(image_array, window_size=sau) # 窗口大小可以根据需要调整
binary_image = image_array > threshold
# 5. 将布尔数组转换为0和1的数组
binary_image_array = (binary_image * 255).astype(np.uint8) # True变为255,False变为0
# 6. 将 NumPy 数组转换为 PIL 图像
new_image = Image.fromarray(binary_image_array).convert('1')
# 7. 将二值图像保存为临时BMP文件
temp_bmp_file = 'temp_image.bmp'
wait_file('temp_image.bmp')
new_image.save(temp_bmp_file)
# 将这个数字转换成字符串,以便可以进行切片操作,确保有两位
number_str = str(pinghua).zfill(2)
# 两两拆分
aa = float(f"{number_str[0]}.{number_str[1]}") # 取前两位
#oo = float(f"{number_str[2]}.{number_str[3]}") # 取后两位
# 如果aa或oo大于1,只取1
#aa = int(min(int(aa), 1))
#oo = int(min(int(oo), 1))
if pinghua_var.get() != "0" and weishendu > 0:
# 8. 使用potrace.exe将BMP文件转换为SVG
svg_file = 'temp_image.svg'
wait_file('temp_image.bmp')
wait_file('temp_image.svg')
cmd = f'potrace -s -a {aa} -O 10 -o "{svg_file}" "{temp_bmp_file}"'
subprocess.run(cmd, shell=True)
#使用imagemagick的convent转换svg为png并改分辨率
if kuandu == 0:
kuandu = im.width
wait_file('temp_image.svg')
wait_file('temp_image.png')
temp_png_file = 'temp_image.png'
cmd2 = f'convert "{svg_file}" -resize {kuandu} "{temp_png_file}"'
subprocess.run(cmd2, shell=True)
wait_file('temp_image.png')
img_buffer = 'temp_image.png'
#img_buffer = BytesIO()
#image.save(img_buffer, format="TIFF")
#img_buffer.seek(0) # 重置 BytesIO 对象到开始位置
elif pinghua_var.get() != "0" and weishendu == 0:
# 8. 使用potrace.exe将bmp文件转换为矢量pdf
svg_file = 'temp_image.pdf'
wait_file('temp_image.bmp')
wait_file('temp_image.pdf')
cmd = f'potrace -b pdf -a {aa} -O 10 -o "{svg_file}" "{temp_bmp_file}"'
subprocess.run(cmd, shell=True)
#使用imagemagick的convent转换svg为png
wait_file('temp_image.pdf')
img_buffer = fitz.open(svg_file)
if daochutupian_var.get() == 2 or daochutupian_var.get() == 1:
svg_file2 = 'temp_image.svg'
wait_file('temp_image.bmp')
wait_file('temp_image.svg')
cmd = f'potrace -s -a {aa} -O 10 -o "{svg_file2}" "{temp_bmp_file}"'
subprocess.run(cmd, shell=True)
# 构建PDF文件名的文件夹路径
pdf_folder_path = os.path.join(output_folder_var.get(), os.path.splitext(pdf_file)[0])
# 如果文件夹不存在,则创建它
if not os.path.exists(pdf_folder_path):
os.makedirs(pdf_folder_path)
# 构建目标SVG文件的路径
target_svg_path = os.path.join(pdf_folder_path, os.path.basename(pdf_file) + f"_{page_num+1}_{xref}.svg")
# 检查目标路径是否已经存在文件
if os.path.exists(target_svg_path):
# 如果存在,添加后缀并重新检查
base, extension = os.path.splitext(target_svg_path)
counter = 1
while True:
new_target_svg_path = f"{base}_{counter}{extension}"
if not os.path.exists(new_target_svg_path):
target_svg_path = new_target_svg_path
break
counter += 1
# 如果不存在,执行重命名操作
os.rename(svg_file2, target_svg_path)
else:
# 直接将BMP文件转换为TIFF
wait_file('temp_image.bmp')
img_buffer = 'temp_image.bmp'
#new_image.save(img_buffer, format='TIFF')
#img_buffer.seek(0) # 重置文件指针到开始位置
return img_buffer
except Exception as e:
print(f"Error processing image: {e}")
return None
#处理pdf的主函数
def process_pdf_thread(pdf_files, output_folder, choice, quality, kuandu, sau, k, weishendu, quant, daochutupian, pinghua):
total_files = len(pdf_files) # 获取总文件数
for index, pdf_file in enumerate(pdf_files):
doc = fitz.open(pdf_file)
total_pages = doc.page_count
current_file_info = f"文件 {index + 1}/{total_files}: {os.path.basename(pdf_file)}"
wrapped_current_file_info = '\n'.join([current_file_info[i:i+25] for i in range(0, len(current_file_info), 25)])
for page_num in range(total_pages):
page = doc[page_num]
page_info = f"处理文件: {wrapped_current_file_info}\n当前页面: {page_num + 1}/{total_pages}"
page_queue.put(page_info) # 将页面信息放入队列
# 提取图片
for img in page.get_images(full=True):
xref = img[0]
base_image = doc.extract_image(xref)
image_bytes = base_image["image"]
image = Image.open(io.BytesIO(image_bytes))
if choice == '1':
image = resize_quality_image(image, kuandu, quality)
page.delete_image(xref)
page.insert_image(page.rect, stream=image)
elif choice == '2':
page.delete_image(xref)
elif choice == '6':
if is_color_image(image):
image = kmeans(image, k, kuandu, quant)
page.insert_image(page.rect, stream=image)
else:
image = blacky(image, kuandu, sau, pinghua, weishendu, daochutupian, pdf_file, page_num, xref)
page.delete_image(xref)
if weishendu == 0 and pinghua_var.get() != "0":
page.show_pdf_page(page.rect, image, 0)
else:
page.insert_image(page.rect, filename=image)
elif choice == '5':
None
elif choice == '3':
image = blacky(image, kuandu, sau, pinghua, weishendu, daochutupian, pdf_file, page_num, xref)
page.delete_image(xref)
if weishendu == 0 and pinghua_var.get() != "0":
page.show_pdf_page(page.rect, image, 0)
else:
page.insert_image(page.rect, filename=image)
elif choice == '7':
if is_color_image(image):
image = resize_quality_image(image, kuandu, quality)
page.insert_image(page.rect, stream=image)
else:
image = blacky(image, kuandu, sau, pinghua, weishendu, daochutupian, pdf_file, page_num, xref)
page.delete_image(xref)
if weishendu == 0 and pinghua_var.get() != "0":
page.show_pdf_page(page.rect, image, 0)
else:
page.insert_image(page.rect, filename=image)
elif choice == '4':
image = kmeans(image, k, kuandu, quant)
page.delete_image(xref)
page.insert_image(page.rect, stream=image)
if daochutupian_var.get() == 2 or daochutupian_var.get() == 1:
xxxx = f"{os.path.basename(pdf_file)}_{page_num + 1}_{xref}"
try:
save_image_to_pdf_folder(image, output_folder, os.path.basename(pdf_file), xxxx)
except Exception:
# 如果函数执行中出现任何异常,这里将被执行,但不做任何操作
pass
if daochutupian_var.get() == 2 or daochutupian_var.get() == 0:
# 保存处理后的PDF文件
output_file = os.path.join(output_folder, "reduce-" + os.path.basename(pdf_file))
# 对每个页面调用 clean_contents 方法
for page in doc:
page.clean_contents()
doc.save(output_file, deflate=True, garbage=4, clean=True, deflate_images=True)
print(f"Processed file saved as {output_file}")
page_info = "全部完成"
page_queue.put(page_info) # 将页面信息放入队列
#判断颜色是否是彩色
def is_color_image(img, threshold=30, percentage=0.001, step=10):
# 复制图像并转换为RGB模式
img_copy = img.copy().convert('RGB')
width, height = img_copy.size
count = 0
total_pixels = 0 # 记录实际检查的像素数
for y in range(0, height, step): # 每隔step行取一个像素
for x in range(0, width, step): # 每隔step列取一个像素
r, g, b = img_copy.getpixel((x, y))
diff = max(abs(r - g), abs(r - b), abs(g - b))
if diff > threshold:
count += 1
total_pixels += 1
# 计算色偏值大于阈值的像素所占的比例
if total_pixels == 0:
return False # 避免除以零
proportion = count / total_pixels
# 如果比例超过0.1%,则认为是彩色图像
return proportion > percentage
#选择文件
def select_files():
file_paths = filedialog.askopenfilenames(filetypes=[("PDF files", "*.pdf")])
if file_paths:
files_list[:] = file_paths
files_entry.delete(0, tk.END)
files_entry.insert(0, ' '.join(file_paths)) # 显示选择的文件路径
def select_output_folder():
folder_path = filedialog.askdirectory()
if folder_path:
output_folder_var.set(folder_path)
app = tk.Tk()
app.title("PDF图片压缩处理器")
# 在app界面中添加一个用于显示当前处理页面的标签
current_page_label = tk.Label(app, text="当前处理页面: 0", font=('Helvetica', 12))
current_page_label.pack()
# 创建两个框架,一个用于左边栏,一个用于右边栏
left_frame = tk.Frame(app)
left_frame.pack(side=tk.LEFT, fill=tk.Y)
right_frame = tk.Frame(app)
right_frame.pack(side=tk.RIGHT, fill=tk.Y)
# 默认值
choice_var = tk.StringVar(value=3)
quality_var = tk.IntVar(value=35)
kuandu_var = tk.IntVar(value=0)
k_var = tk.IntVar(value=4)
sau_var = tk.IntVar(value=19)
weishendu_var = tk.IntVar(value=0)
quant_var = tk.IntVar(value=1)
daochutupian_var = tk.IntVar(value=0)
pinghua_var = tk.StringVar(value=10)
files_list = [] # 使用列表存储文件路径
output_folder_var = tk.StringVar(value='')
# 定义一个函数来更新界面元素的可见性
def update_ui():
app.after(100, update_current_page_label)
choice = choice_var.get()
if choice == '1':
kuandu_label.pack()
kuandu_entry.pack()
pinghua_label.pack_forget()
pinghua_entry.pack_forget()
weishendu_label.pack_forget()
weishendu_entry.pack_forget()
jpeg_quality_label.pack()
jpeg_quality_entry.pack()
sau_label.pack_forget()
sau_entry.pack_forget()
k_label.pack_forget()
k_entry.pack_forget()
quant_label.pack_forget()
quant_entry.pack_forget()
elif choice == '2':
kuandu_label.pack_forget()
kuandu_entry.pack_forget()
pinghua_label.pack_forget()
pinghua_entry.pack_forget()
weishendu_label.pack_forget()
weishendu_entry.pack_forget()
jpeg_quality_label.pack_forget()
jpeg_quality_entry.pack_forget()
sau_label.pack_forget()
sau_entry.pack_forget()
k_label.pack_forget()
k_entry.pack_forget()
quant_label.pack_forget()
quant_entry.pack_forget()
elif choice == '3':
kuandu_label.pack()
kuandu_entry.pack()
pinghua_label.pack()
pinghua_entry.pack()
weishendu_label.pack()
weishendu_entry.pack()
jpeg_quality_label.pack_forget()
jpeg_quality_entry.pack_forget()
sau_label.pack()
sau_entry.pack()
k_label.pack_forget()
k_entry.pack_forget()
quant_label.pack_forget()
quant_entry.pack_forget()
elif choice == '4':
kuandu_label.pack()
kuandu_entry.pack()
pinghua_label.pack_forget()
pinghua_entry.pack_forget()
weishendu_label.pack_forget()
weishendu_entry.pack_forget()
jpeg_quality_label.pack_forget()
jpeg_quality_entry.pack_forget()
sau_label.pack_forget()
sau_entry.pack_forget()
k_label.pack()
k_entry.pack()
quant_label.pack()
quant_entry.pack()
elif choice == '5':
kuandu_label.pack_forget()
kuandu_entry.pack_forget()
pinghua_label.pack_forget()
pinghua_entry.pack_forget()
weishendu_label.pack_forget()
weishendu_entry.pack_forget()
jpeg_quality_label.pack_forget()
jpeg_quality_entry.pack_forget()
sau_label.pack_forget()
sau_entry.pack_forget()
k_label.pack_forget()
k_entry.pack_forget()
quant_label.pack_forget()
quant_entry.pack_forget()
elif choice == '6':
kuandu_label.pack()
kuandu_entry.pack()
pinghua_label.pack()
pinghua_entry.pack()
weishendu_label.pack()
weishendu_entry.pack()
jpeg_quality_label.pack_forget()
jpeg_quality_entry.pack_forget()
sau_label.pack()
sau_entry.pack()
k_label.pack()
k_entry.pack()
quant_label.pack()
quant_entry.pack()
elif choice == '7':
kuandu_label.pack()
kuandu_entry.pack()
pinghua_label.pack()
pinghua_entry.pack()
weishendu_label.pack()
weishendu_entry.pack()
jpeg_quality_label.pack()
jpeg_quality_entry.pack()
sau_label.pack()
sau_entry.pack()
k_label.pack_forget()
k_entry.pack_forget()
quant_label.pack_forget()
quant_entry.pack_forget()
# 在left_frame中添加左边栏的组件
#current_page_label = tk.Label(left_frame, text="当前处理页面: 0", font=('Helvetica', 12))
#current_page_label.pack()
tk.Label(left_frame, text="是否导出图片:0单pdf,1是单图片,2是pdf和图片").pack()
tk.Entry(left_frame, textvariable=daochutupian_var).pack()
tk.Label(left_frame, text="**********选择PDF处理选项**********").pack()
tk.Radiobutton(left_frame, text="1.改像素和质量", variable=choice_var, value="1", command=update_ui).pack()
tk.Radiobutton(left_frame, text="2.删除图像", variable=choice_var, value="2", command=update_ui).pack()
tk.Radiobutton(left_frame, text="3.黑白二值化图像", variable=choice_var, value="3", command=update_ui).pack()
tk.Radiobutton(left_frame, text="4.降低颜色数,超级慢,最多256", variable=choice_var, value="4", command=update_ui).pack()
tk.Radiobutton(left_frame, text="5.啥也不干", variable=choice_var, value="5", command=update_ui).pack()
tk.Radiobutton(left_frame, text="6.彩图降低颜色数,黑白二值化", variable=choice_var, value="6", command=update_ui).pack()
tk.Radiobutton(left_frame, text="7.彩图改质量,黑白二值化", variable=choice_var, value="7", command=update_ui).pack()
tk.Label(left_frame, text="选择PDF文件:").pack()
files_entry = tk.Entry(left_frame, width=50)
files_entry.pack()
tk.Button(left_frame, text="选择文件", command=select_files).pack()
tk.Label(left_frame, text="选择输出文件夹:").pack()
output_folder_entry = tk.Entry(left_frame, textvariable=output_folder_var, width=50)
output_folder_entry.pack()
tk.Button(left_frame, text="选择文件夹", command=select_output_folder).pack()
tk.Button(left_frame, text="开始处理", command=lambda: start_process_pdf(files_list, output_folder_var.get(), choice_var.get(), quality_var.get(), kuandu_var.get(), sau_var.get(), k_var.get(), weishendu_var.get(), quant_var.get(), daochutupian_var.get(),pinghua_var.get())).pack()
# 在right_frame中添加右边栏的组件
kuandu_label = tk.Label(right_frame, text="缩小像素输入宽度\n 0 表示不改变,只有平滑输出位图可以放大:")
kuandu_label.pack()
kuandu_entry = tk.Entry(right_frame, textvariable=kuandu_var)
kuandu_entry.pack()
pinghua_label = tk.Label(right_frame, text="平滑开关及程度\n0为不开启,最多两位数字,小于10够了")
pinghua_label.pack()
pinghua_entry = tk.Entry(right_frame, textvariable=pinghua_var)
pinghua_entry.pack()
weishendu_label = tk.Label(right_frame, text="平滑后图片输出格式\n0为输出矢量图,1248为位图:")
weishendu_label.pack()
weishendu_entry = tk.Entry(right_frame, textvariable=weishendu_var)
weishendu_entry.pack()
jpeg_quality_label = tk.Label(right_frame, text="jpeg质量 (1-100):")
jpeg_quality_label.pack()
jpeg_quality_entry = tk.Entry(right_frame, textvariable=quality_var)
jpeg_quality_entry.pack()
sau_label = tk.Label(right_frame, text="二值化检测块大小 (奇数):")
sau_label.pack()
sau_entry = tk.Entry(right_frame, textvariable=sau_var)
sau_entry.pack()
k_label = tk.Label(right_frame, text="降到的颜色数量,小于256:")
k_label.pack()
k_entry = tk.Entry(right_frame, textvariable=k_var)
k_entry.pack()
quant_label = tk.Label(right_frame, text="降颜色后压缩量:")
quant_label.pack()
quant_entry = tk.Entry(right_frame, textvariable=quant_var)
quant_entry.pack()
# 在app启动时更新界面元素的可见性
update_ui()
app.mainloop()