【Python】PIL 压缩图片刚好 200KB

问题来源

今天遇到一个特别奇葩的需求,甲方要求我把图片制作成刚好 200KB,天啦撸。这这这怎么可能,理论上只能压缩到接近于目标大小。但,干了兄弟萌。

解决思路

压缩图片至低于目标大小,再把差的部分全部填充 “0”。

核心内容

核心内容是如何补齐,以下提供两种思路:

  1. 第一种(save):
    ① 打开图片文件并转换为 BytesIO
    ② 计算 (目标大小 - 压缩后大小) 的差值, 并用 “\x00” 补足
    ③ 保存
  2. 第二种(save2):
    ① cmd 生成一个指定大小的文件
    ② 将压缩后的二进制流写入生成的文件

代码

文件结构
main.py
| - - new
| - - old
| - - | - - test.jpg

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# main.py

from PIL import Image
from io import BytesIO
from os import system
from os import listdir
from os import remove
from os.path import getsize
from os.path import exists


__author__ = 'one-ccs'

"""
功能: 把 ".\old" 目录下的所有图片压缩并填充至 200KB 后存放在 ".\new" 目录下.
"""


settings = {
    'loadPath': r'.\old',  # 待压缩图片路径
    'savePath': r'.\new',  # 保存路径
    'size': 200,
    'quality': 90
}


class ImageFixCompressor():

    def __init__(self) -> None:
        self.imgBytes = None
        self.imgPath = None
        self.name = None
        self.oldSize = 0
        self.newSize = 0
        self.saveSize = 0

        self.format = 'JPEG'
        self.targetSize = 200 # 目标大小 (KB)
        self.quality = 90     # 压缩比率 若压缩后图片大于目标大小应减小该值

    def split_path(self, path:str='') -> tuple:
        """
        提取 path 中的路径与文件名.
        """
        if not isinstance(path, str):
            raise ValueError(f'参数 "path" 的数据类型应该为 "str", 但是传入了 "{type(path)}" 类型.')

        # 判断是否是以 '/' '\' 作为目录分隔符
        flag = path[::-1].find('/')
        if flag == -1:
            flag = path[::-1].find('\\')
            if flag == -1:
                raise ValueError(f'参数 "path" 的数据类型应该为 "str", 但是传入了 "{type(path)}" 类型.')

        name = path[-flag:]
        path = path[:-flag]

        return (path, name)

    def full_path(self, path) -> str:
        return fr'{path}\{self.name}'

    def open(self, imgPath:str) -> None:
        """
        打开图像文件

        :参数 imgPath: 图片文件路径.
        """
        try:
            _, self.name = self.split_path(imgPath)
            self.oldSize = getsize(imgPath)
            self.image = Image.open(imgPath)

            print(f'打开: "{imgPath}" 成功; 大小: {self.oldSize / 1024:.2f} KB.')
        except Exception as e:
            print(f'错误: 文件 "{imgPath}" 打开失败; 原因: "{e}".')

    def show(self) -> None:
        self.image.show()

    def compress(self, format='JPEG', quality=None) -> None:
        if format == 'PNG' or format == 'png':
            self.format = 'PNG'
        if quality:
            self.quality = quality

        self.image.tobytes()
        self.imageBuffer = BytesIO()
        self.image.save(self.imageBuffer, format=self.format, quality=self.quality)

    def save(self, savePath:str, cover:bool=False, name=None) -> None:
        if cover:
            mode = 'wb+'
        else:
            mode = 'rb+'

        try:
            self.newSize = self.imageBuffer.tell() / 1024
            # ~ 补齐大小
            for i in range(0, self.targetSize * 1024 - self.imageBuffer.tell()):
                self.imageBuffer.write(b'\x00')

            with open(self.full_path(savePath), mode) as fb:
                fb.write(self.imageBuffer.getvalue())

            self.saveSize = getsize(self.full_path(savePath)) / 1024
            print(fr'保存: "{self.full_path(savePath)}" 成功; 压缩大小: {self.newSize:.2f} KB; 保存大小: {self.saveSize:.2f} KB.')
        except Exception as e:
            print(f'错误: "{self.name}" 保存失败; 原因: "{e}".')

    def save2(self, savePath:str, cover:bool=False, name=None) -> None:
        if cover:
            if exists(self.full_path(savePath)):
                remove(self.full_path(savePath))
        else:
            print(f'异常: 文件 "{savePath}" 已存在, 已放弃保存.')
            return

        system('@echo off')
        system(f'fsutil file createnew {self.full_path(savePath)} {self.targetSize * 1024}')

        try:
            with open(self.full_path(savePath), 'rb+') as fb:
                fb.write(self.imageBuffer.getvalue())
                self.newSize = self.imageBuffer.tell() / 1024
            self.saveSize = getsize(self.full_path(savePath)) / 1024
            print(fr'保存: "{self.full_path(savePath)}" 成功; 压缩大小: {self.newSize:.2f} KB; 保存大小: {self.saveSize:.2f} KB.')
        except Exception as e:
            print(f'错误: "{self.name}" 保存失败; 原因: "{e}".')


def main(args):
    compressor = ImageFixCompressor()

    oldImgPaths = listdir(settings['loadPath'])

    for oldImgPath in oldImgPaths:
        fullPath = f"{settings['loadPath']}\{oldImgPath}"

        compressor.open(fullPath)
        compressor.compress()
        compressor.save(settings['savePath'], cover=True)
        # ~ return

    return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))

【如果觉得本文不错 “点赞、收藏、关注”,是对作者最大的鼓励。】

  • 12
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值