公司同事做标书做合同需要用到很多图片,然后做出来的成品有时又会有文件大小的限制,所以需要压缩图片,又不希望改变图片分辨率,因为会影响清晰度,如果原本是png, bmp图片,另存为jpg就会小很多了,但有时候,jpg也还是嫌大,所以需要在jpg的基础上再压缩,给同事简单地解释了一下,结论是jpg不能再压缩了,除非改变分辨率或者改变清晰度,妥协后的结果是,分辨率不能小,清晰度可以根据情况降低。
然后同事拿出了2个网上找的图片压缩工具,效果可以接受,但最大的压缩比例,在有些情况下文件大小还是太大,而且在使用上希望可以再方便一点,于是收集了需求如下:
- 可以选择单个图片、多个图片、多个文件夹
- 可以单独选择某个文件类型,也可以选择所有文件类型
- 可以在压缩前预览压缩后的大小
- 最好可以预览压缩后的图片质量(画面质量),并和压缩前的图片放在一起对比
- 可以选择压缩后图片另存为路径,也可以直接就保存在原图片一起
1,2其实是最初的需求,在拿出成品后给多名同事使用后,又出现了3,4,5需求,所以甲方才会被人砍(狗头,你看第3点,因为jpg压缩有不可预测性,你看连PS在保存jpg的时候,也只是给了个预估的大小,你再看第4点,心里的话:这个需求做不了!我跟同事说,这个功能连photoshop 2020都没有,表示做不了。
但是后来下班骑车的时候一想,3和4也不是不能实现啊,1.0版的时候,是压缩完了,读取新旧文件,然后显示压缩前后的文件大小对比,要达到“预览”的效果,我tm直接把这个压缩完了的结果说是“预览”不就好了嘛,反正使用上也体会不到区别,至于对比压缩前后的图片质量,我也tm直接把图片先给压缩完了,再把新旧图片同时显示出来对比不就好了嘛。一开始不能做的想法,是太从程序本身出发了,预测jpg的压缩比例和预测压缩后的图片质量,这种不可能的事如果从用户角度出发,只要呈现出需要的结果就行了,况且都0202年了,机器性能对于这种小工具来说,近乎是取之不尽的,怎么方便怎么来就是了。
先放张成品图
UI部分的代码:
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Help(object):
def setupUi(self, Help):
Help.setObjectName("Help")
Help.resize(700, 450)
Help.setMinimumSize(QtCore.QSize(700, 450))
Help.setMaximumSize(QtCore.QSize(700, 450))
self.buttonBox = QtWidgets.QDialogButtonBox(Help)
self.buttonBox.setGeometry(QtCore.QRect(240, 400, 191, 32))
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok)
self.buttonBox.setObjectName("buttonBox")
self.lb_help = QtWidgets.QLabel(Help)
self.lb_help.setGeometry(QtCore.QRect(10, 10, 251, 16))
self.lb_help.setObjectName("lb_help")
self.lb_img = QtWidgets.QLabel(Help)
self.lb_img.setGeometry(QtCore.QRect(20, 30, 661, 371))
self.lb_img.setText("")
self.lb_img.setPixmap(QtGui.QPixmap("help.png"))
self.lb_img.setObjectName("lb_img")
self.retranslateUi(Help)
self.buttonBox.accepted.connect(Help.accept)
self.buttonBox.rejected.connect(Help.reject)
QtCore.QMetaObject.connectSlotsByName(Help)
def retranslateUi(self, Help):
_translate = QtCore.QCoreApplication.translate
Help.setWindowTitle(_translate("Help", "Help"))
self.lb_help.setText(_translate("Help", "关于“选择多个文件夹”按钮的用法"))
因为还有一个帮助的弹窗,分另一个文件写了UI
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_JPG_Compressor(object):
def setupUi(self, JPG_Compressor):
JPG_Compressor.setObjectName("JPG_Compressor")
JPG_Compressor.resize(900, 700)
JPG_Compressor.setMinimumSize(QtCore.QSize(900, 700))
JPG_Compressor.setMaximumSize(QtCore.QSize(900, 700))
self.te_showDetails = QtWidgets.QTextEdit(JPG_Compressor)
self.te_showDetails.setGeometry(QtCore.QRect(270, 10, 611, 271))
self.te_showDetails.setFocusPolicy(QtCore.Qt.NoFocus)
self.te_showDetails.setObjectName("te_showDetails")
self.layoutWidget = QtWidgets.QWidget(JPG_Compressor)
self.layoutWidget.setGeometry(QtCore.QRect(20, 10, 241, 201))
self.layoutWidget.setObjectName("layoutWidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.verticalLayout.setObjectName("verticalLayout")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.bt_selectFiles = QtWidgets.QPushButton(self.layoutWidget)
self.bt_selectFiles.setObjectName("bt_selectFiles")
self.horizontalLayout.addWidget(self.bt_selectFiles)
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem1)
self.verticalLayout.addLayout(self.horizontalLayout)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setSpacing(7)
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_2.addItem(spacerItem2)
self.bt_selectDirs = QtWidgets.QPushButton(self.layoutWidget)
self.bt_selectDirs.setObjectName("bt_selectDirs")
self.horizontalLayout_2.addWidget(self.bt_selectDirs)
spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_2.addItem(spacerItem3)
self.verticalLayout.addLayout(self.horizontalLayout_2)
self.qualitySlider = QtWidgets.QSlider(self.layoutWidget)
self.qualitySlider.setMinimum(10)
self.qualitySlider.setMaximum(90)
self.qualitySlider.setProperty("value", 50)
self.qualitySlider.setOrientation(QtCore.Qt.Horizontal)
self.qualitySlider.setObjectName("qualitySlider")
self.verticalLayout.addWidget(self.qualitySlider)
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_3.addItem(spacerItem4)
self.lb_qualityText = QtWidgets.QLabel(self.layoutWidget)
self.lb_qualityText.setObjectName("lb_qualityText")
self.horizontalLayout_3.addWidget(self.lb_qualityText)
self.lb_quality = QtWidgets.QLabel(self.layoutWidget)
self.lb_quality.setObjectName("lb_quality")
self.horizontalLayout_3.addWidget(self.lb_quality)
spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_3.addItem(spacerItem5)
self.verticalLayout.addLayout(self.horizontalLayout_3)
self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
self.horizontalLayout_6.setObjectName("horizontalLayout_6")
spacerItem6 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_6.addItem(spacerItem6)
self.bt_preview = QtWidgets.QPushButton(self.layoutWidget)
self.bt_preview.setObjectName("bt_preview")
self.horizontalLayout_6.addWidget(self.bt_preview)
spacerItem7 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_6.addItem(spacerItem7)
self.verticalLayout.addLayout(self.horizontalLayout_6)
self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
self.horizontalLayout_4.setObjectName("horizontalLayout_4")
spacerItem8 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_4.addItem(spacerItem8)
self.bt_compress = QtWidgets.QPushButton(self.layoutWidget)
self.bt_compress.setObjectName("bt_compress")
self.horizontalLayout_4.addWidget(self.bt_compress)
self.bt_save = QtWidgets.QPushButton(self.layoutWidget)
self.bt_save.setObjectName("bt_save")
self.horizontalLayout_4.addWidget(self.bt_save)
spacerItem9 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_4.addItem(spacerItem9)
self.verticalLayout.addLayout(self.horizontalLayout_4)
self.layoutWidget1 = QtWidgets.QWidget(JPG_Compressor)
self.layoutWidget1.setGeometry(QtCore.QRect(20, 290, 861, 391))
self.layoutWidget1.setObjectName("layoutWidget1")
self.horizontalLayout_8 = QtWidgets.QHBoxLayout(self.layoutWidget1)
self.horizontalLayout_8.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout_8.setObjectName("horizontalLayout_8")
self.verticalLayout_2 = QtWidgets.QVBoxLayout()
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
self.lb_before = QtWidgets.QLabel(self.layoutWidget1)
self.lb_before.setObjectName("lb_before")
self.horizontalLayout_5.addWidget(self.lb_before)
spacerItem10 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_5.addItem(spacerItem10)
self.verticalLayout_2.addLayout(self.horizontalLayout_5)
self.gv_before = QtWidgets.QGraphicsView(self.layoutWidget1)
self.gv_before.setFocusPolicy(QtCore.Qt.NoFocus)
self.gv_before.setObjectName("gv_before")
self.verticalLayout_2.addWidget(self.gv_before)
self.horizontalLayout_8.addLayout(self.verticalLayout_2)
self.verticalLayout_3 = QtWidgets.QVBoxLayout()
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
self.lb_after = QtWidgets.QLabel(self.layoutWidget1)
self.lb_after.setObjectName("lb_after")
self.horizontalLayout_7.addWidget(self.lb_after)
spacerItem11 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_7.addItem(spacerItem11)
self.verticalLayout_3.addLayout(self.horizontalLayout_7)
self.gv_after = QtWidgets.QGraphicsView(self.layoutWidget1)
self.gv_after.setFocusPolicy(QtCore.Qt.NoFocus)
self.gv_after.setObjectName("gv_after")
self.verticalLayout_3.addWidget(self.gv_after)
self.horizontalLayout_8.addLayout(self.verticalLayout_3)
self.bt_previewimg = QtWidgets.QPushButton(JPG_Compressor)
self.bt_previewimg.setGeometry(QtCore.QRect(80, 250, 131, 28))
self.bt_previewimg.setObjectName("bt_previewimg")
self.bt_help = QtWidgets.QPushButton(JPG_Compressor)
self.bt_help.setGeometry(QtCore.QRect(20, 220, 31, 28))
self.bt_help.setObjectName("bt_help")
self.retranslateUi(JPG_Compressor)
QtCore.QMetaObject.connectSlotsByName(JPG_Compressor)
def retranslateUi(self, JPG_Compressor):
_translate = QtCore.QCoreApplication.translate
JPG_Compressor.setWindowTitle(_translate("JPG_Compressor", "图片压缩工具 V1.3"))
self.bt_selectFiles.setText(_translate("JPG_Compressor", "选择文件"))
self.bt_selectDirs.setText(_translate("JPG_Compressor", "选择多个文件夹"))
self.lb_qualityText.setText(_translate("JPG_Compressor", "文件质量:"))
self.lb_quality.setText(_translate("JPG_Compressor", "50"))
self.bt_preview.setText(_translate("JPG_Compressor", "压缩并预览文件大小"))
self.bt_compress.setText(_translate("JPG_Compressor", "就地保存"))
self.bt_save.setText(_translate("JPG_Compressor", "另存为图片"))
self.lb_before.setText(_translate("JPG_Compressor", "压缩前"))
self.lb_after.setText(_translate("JPG_Compressor", "压缩后"))
self.bt_previewimg.setText(_translate("JPG_Compressor", "预览图片质量"))
self.bt_help.setText(_translate("JPG_Compressor", "?"))
程序代码:
import re, os, sys, shutil
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QFileDialog, QGraphicsPixmapItem, QGraphicsScene, QDialog
from PyQt5.QtGui import QPixmap, QTextCursor
from PIL import Image
from JPG_Compressor_UI import Ui_JPG_Compressor
from JPG_Compressor_Help_UI import Ui_Help
class JpgCompressor(QtWidgets.QWidget, Ui_JPG_Compressor):
def __init__(self):
super(JpgCompressor, self).__init__()
self.setupUi(self)
#初始化UI及信号连接
self.qualitySlider.setMinimum(10)
self.qualitySlider.setMaximum(90)
self.qualitySlider.setValue(50)
self.lb_quality.setText('50')
self.qualitySlider.valueChanged.connect(self.QualityChanged)
self.bt_selectFiles.clicked.connect(self.SelectFiles)
self.bt_selectDirs.clicked.connect(self.SelectDirs)
self.bt_preview.clicked.connect(self.Preview)
self.bt_compress.clicked.connect(lambda:self.Save(0))
self.bt_save.clicked.connect(lambda:self.Save(1))
self.bt_help.clicked.connect(self.DisplayHelp)
self.bt_previewimg.clicked.connect(self.PreviewImg)
#初始化默认路径
with open('config.ini', 'r') as file:
self.inidir =file.read()
self.filepaths = [] #列表存放文件完整路径
self.dirs = [] #列表存放文件夹的路径
self.selectedpath = '' #选中的文件夹
self.filecount = 0 #选中的文件数
self.compressmode = 0 #选择文件的模式
self.comprssed = 0 #标记是否完成压缩
#若上次意外退出,初始化时清理文件
if os.path.exists('temp'):
shutil.rmtree('temp')
def QualityChanged(self):
self.lb_quality.setText(str(self.qualitySlider.value()))
def SelectFiles(self):
selectedfiles, filetype = QFileDialog.getOpenFileNames(self, '选择文件', self.inidir, 'Image Files(*.jpg;*.jpeg;*.png;*.bmp)')
#仅当选择了文件之后执行
if(selectedfiles != []):
self.compressmode = 0
self.te_showDetails.clear() #清空显示框
self.filecount = len(selectedfiles)
self.filepaths = selectedfiles
#记录当前选中文件的路径,作为下次开启对话框时的初始路径
self.inidir = os.path.dirname(self.filepaths[0])
self.te_showDetails.setText('已选择' + str(len(self.filepaths)) + '个文件')
def SelectDirs(self):
self.selectedpath = QFileDialog.getExistingDirectory(self, '选择文件夹', self.inidir + '/..')
#仅当选择了文件夹之后执行
if(self.selectedpath != ''):
self.compressmode = 1
self.inidir = self.selectedpath
self.te_showDetails.clear()
self.Clear()
#列出文件夹中所有文件
buf = os.listdir(self.selectedpath)
for i in buf:
#只保留文件夹
if os.path.isdir(self.selectedpath + '/' + i):
self.dirs.append(self.selectedpath + '/' + i + '/')
self.te_showDetails.setText('已选择:')
self.te_showDetails.append(str(len(self.dirs)) + '个文件夹')
#读取各个文件夹中的文件路径
self.ReadFilesFromDirs()
def ReadFilesFromDirs(self):
self.filecount = 0
for path in self.dirs:
filename = os.listdir(path)
#只保留图像文件
for i in filename:
if re.search('\.jpg|\.JPG|\.png|\.PNG|\.jpeg|\.JPEG|\.bmp|\.BMP', i) != None:
self.filepaths.append(path + i)
self.filecount += 1
self.te_showDetails.append(str(self.filecount) + '个文件')
def Preview(self):
if len(self.filepaths) == 0:
self.te_showDetails.append('没有选择文件!')
else:
for i in self.filepaths:
#压缩文件
self.Compress(i)
#标记已经完成压缩
self.comprssed = 1
self.te_showDetails.append('所有图片已压缩完成')
def Compress(self, filepath):
dirname = os.path.dirname(filepath)
#读取文件名
filename = os.path.basename(filepath)
#过滤掉后缀名
filename_ex = re.sub('\.jpg|\.JPG|\.png|\.PNG|\.jpeg|\.JPEG|\.bmp|\.BMP', '', filename)
#创建临时文件夹
if not os.path.exists('temp'):
os.mkdir('temp')
os.chdir('temp')
#打开图片并保存
with Image.open(filepath).convert('RGB') as img:
#一律保存为jpg格式
#如果选择的只有文件
if self.compressmode == 0:
newpath = os.path.abspath('.') + '/' + filename_ex + '.jpg'
#如果选择的是多个文件夹
elif self.compressmode == 1:
buf = os.path.basename(dirname)
#在temp文件夹里创建相应的子文件夹
if not os.path.exists(buf):
os.mkdir(buf)
newpath = os.path.abspath('.') + '/' + buf + '/' + filename_ex + '.jpg'
img.save(newpath, quality = self.qualitySlider.value())
#退出文件夹
os.chdir('..')
#显示压缩结果
self.ShowDetails(filename, filepath, newpath)
def ShowDetails(self, filename, oldpath, newpath):
old = os.path.getsize(oldpath) / 1024.0
new = os.path.getsize(newpath) / 1024.0
old = round(old, 1)
new = round(new, 1)
self.te_showDetails.append(filename + '\t\t' + str(old) + 'KB --> ' + str(new) + 'KB')
#把光标移到最下面
self.te_showDetails.moveCursor(QTextCursor.End)
def Save(self, savemode):
#选择了文件,并且完成了压缩
if self.filecount != 0 and self.comprssed == 1:
#选择就地保存
if savemode == 0:
#如果是选择了文件压缩的
if self.compressmode == 0:
#扫描temp文件夹内所有文件
files = os.listdir('./temp')
#设置目的地
dest = os.path.dirname(self.filepaths[0]) + '/已压缩'
#在原图片位置创建文件夹:已压缩
if not os.path.exists(dest):
os.mkdir(dest)
#逐个移动文件
for f in files:
source = './temp/' + f
try:
shutil.move(source, dest)
except shutil.Error:
self.te_showDetails.append('文件已存在!')
#如果是选择了文件夹压缩的
elif self.compressmode == 1:
for i in range(len(self.dirs)):
#在每一个原文件夹位置创建新文件夹:已压缩
if not os.path.exists(self.dirs[i] + '/已压缩'):
os.mkdir(self.dirs[i] + '/已压缩')
#设置起始地文件夹位置
buf = os.path.abspath(self.dirs[i])
sourcedir = './temp/' + os.path.basename(buf)
#设置目的地文件夹位置
dest = self.dirs[i] + '已压缩/'
files = os.listdir(sourcedir)
#逐个移动文件
for f in files:
source = sourcedir + '/' + f
try:
shutil.move(source, dest)
except shutil.Error:
self.te_showDetails.append('文件已存在!')
#选择另存为
elif savemode == 1:
#选择保存的位置
filepath = QFileDialog.getExistingDirectory(self, '选择保存位置', self.inidir + '/..')
#把临时文件夹中的图片移动到需要保存的位置
files = os.listdir('./temp/')
for i in files:
shutil.move('./temp/' + i, filepath)
self.te_showDetails.append('文件另存为成功')
#重设压缩标记
self.comprssed = 0
self.Clear()
#没有选择文件
elif self.filecount == 0:
self.te_showDetails.append('没有选择文件!')
#选择了文件,但没有完成压缩
elif self.filecount != 0 and self.comprssed == 0:
self.te_showDetails.append('没有完成压缩!')
def PreviewImg(self):
if self.filecount == 0:
self.te_showDetails.append('没有选择文件!')
elif self.filecount > 1:
self.te_showDetails.append('请选择单个文件!')
else:
#先压缩图片
self.Compress(self.filepaths[0])
#创建pixmap对象
img_1 = QPixmap()
img_2 = QPixmap()
#pixmap读取图片
QPixmap.load(img_1, self.filepaths[0])
buf = re.sub('\.png|\.PNG|\.jpeg|\.JPEG|\.bmp|\.BMP', '.jpg', os.path.basename(self.filepaths[0]))
QPixmap.load(img_2, './temp/' + buf)
#创建graphics pixmap item
item_1 = QGraphicsPixmapItem(img_1)
item_2 = QGraphicsPixmapItem(img_2)
#创建graphics scene
scene_1 = QGraphicsScene()
scene_2 = QGraphicsScene()
#往scene里添加item
scene_1.addItem(item_1)
scene_2.addItem(item_2)
#往graphics view里设置scene
self.gv_before.setScene(scene_1)
self.gv_after.setScene(scene_2)
#设置graphics view为自适应item尺寸
self.gv_before.fitInView(item_1)
self.gv_after.fitInView(item_2)
#清场用
def Clear(self):
self.filecount = 0
self.filepaths.clear()
self.dirs.clear()
#显示帮助
def DisplayHelp(self):
self.helpwindow = Help()
self.helpwindow.show()
#重写退出事件,退出前清场并更新ini文件
def closeEvent(self, event):
if os.path.exists('temp'):
shutil.rmtree('temp')
with open('config.ini', 'w') as file:
file.write(self.inidir)
#弹出帮助窗口类
class Help(QDialog, Ui_Help):
def __init__(self):
super(Help, self).__init__()
self.setupUi(self)
if __name__=="__main__":
app = QtWidgets.QApplication(sys.argv)
mainwindow = JpgCompressor()
mainwindow.show()
sys.exit(app.exec_())