痛点催生的灵感
话说某天,我正在帮家里的大朋友处理"学生综合素质评价信息管理系统"的资料上传。系统有个令人无语的限制:图片大小不能超过200KB。
可是现在的手机拍照功能太强大了啊!随随便便一张照片都是几MB大小。于是我就陷入了一个循环:
- 拍照片 → 太大上传失败
- 找在线压缩网站 → 广告弹窗满天飞
- 打开Photoshop → 等半天,然后点半天,最后忘了保存格式
- 最终:心态爆炸
于是我想:"我可是个程序员啊!为什么不自己写一个工具呢?" 于是这个20分钟速成的图片压缩小工具就诞生了。
Go语言:程序员的瑞士军刀
为什么用Go语言?因为它真的太!好!用!了!
- 开箱即用:标准库自带图像处理功能,完全不用找第三方包
- 编译超快:写完代码,"go build"一下,瞬间得到一个可执行文件
- 跨平台:一次编写,到处运行,Windows、Mac、Linux通吃
- 代码简洁:同样的功能,Go代码比其他语言至少短一半
工作原理:双管齐下的压缩策略
这个工具的核心思想很简单:先调质量,再缩尺寸。
第一阶段:质量压缩
JPEG图片有个质量参数(1-100),我们从高质量开始,逐步降低:
- 从95%质量开始尝试
- 如果文件大小还是超过目标,降到90%
- 继续降到85%...直到10%
- 如果某个质量下的文件大小满足要求,直接返回
这种方法的优点是不会改变图片尺寸,只是牺牲一点点视觉质量。
第二阶段:尺寸压缩
如果单纯降低质量还不够,我们就开始缩小图片尺寸:
- 使用二分法智能寻找最佳缩放比例
- 从25%到100%之间搜索
- 每次缩放后都检查文件大小
- 确保图像不会被缩得太小(设置最小尺寸保护)
技术亮点
- 二分查找算法:比线性搜索更高效,几轮迭代就能找到合适的尺寸
- 边界保护:防止图像被压缩得太小导致无法识别
- 错误处理:友好的错误提示,不会让用户一脸懵逼
- 多种格式支持:输入支持JPEG、PNG、GIF等多种格式
编译方法:三步搞定
Go语言的编译超级简单,只需要几个简单的步骤:
1. 确保已安装Go环境
首先,确保你的电脑上已经安装了Go。打开命令行,输入:
go version
如果显示了Go的版本信息,说明已经安装好了。如果没有,请先去Go官网下载安装。
2. 编译程序
进入到包含main.go的目录,执行以下命令:
# Windows系统
go build -o 图片压缩.exe main.go
# Mac/Linux系统
go build -o 图片压缩 main.go
编译完成后,当前目录下会生成一个可执行文件。
3. 运行程序
编译完成后,使用方法超级简单:
# Windows
图片压缩.exe <输入文件路径> <输出文件路径> <目标大小(KB)>
# Mac/Linux
./图片压缩 <输入文件路径> <输出文件路径> <目标大小(KB)>
例如,要把一张照片压缩到200KB以内:
# Windows
图片压缩.exe 学生照片.jpg 压缩后的照片.jpg 200
# Mac/Linux
./图片压缩 学生照片.jpg 压缩后的照片.jpg 200
运行后,工具会告诉你压缩结果,包括最终大小和压缩率。
总结:编程解决实际问题的快乐
这个小工具虽然简单,但它真正解决了实际问题。现在,我再也不用为了上传一张照片而烦恼了。
这也正是编程的魅力所在:用几行代码,解决生活中的一个小痛点。而且Go语言让这个过程变得如此简单和高效。
如果你也有类似的需求,不妨试试这个工具,或者根据源码自己定制一个适合你的版本!
完整源码
package main
import (
"bytes"
"flag"
"fmt"
"image"
"image/jpeg"
"os"
"path/filepath"
"strconv"
)
// resizeNearest 使用最近邻插值算法缩放图像
// 最近邻插值是最简单的图像缩放算法,通过直接映射源图像像素来实现缩放
// 参数:
// - src: 源图像
// - newWidth: 目标宽度
// - newHeight: 目标高度
// 返回值:
// - *image.NRGBA: 缩放后的图像
func resizeNearest(src image.Image, newWidth, newHeight int) *image.NRGBA {
// 获取源图像的边界和尺寸
srcBounds := src.Bounds()
srcW, srcH := srcBounds.Dx(), srcBounds.Dy()
// 创建目标图像,使用NRGBA格式(无预乘alpha的RGBA)
dst := image.NewNRGBA(image.Rect(0, 0, newWidth, newHeight))
// 遍历目标图像的每个像素
for y := 0; y < newHeight; y++ {
for x := 0; x < newWidth; x++ {
// 计算源图像中对应的坐标
srcX := int(float64(x) * float64(srcW) / float64(newWidth))
srcY := int(float64(y) * float64(srcH) / float64(newHeight))
// 边界检查,确保不会越界
if srcX >= srcW {
srcX = srcW - 1
}
if srcY >= srcH {
srcY = srcH - 1
}
// 将源图像像素颜色复制到目标图像
dst.Set(x, y, src.At(srcX, srcY))
}
}
return dst
}
// encodeJPEG 将图像编码为JPEG格式的字节数据
// 参数:
// - img: 要编码的图像
// - quality: JPEG压缩质量(1-100),值越高质量越好
// 返回值:
// - []byte: 编码后的JPEG字节数据
func encodeJPEG(img image.Image, quality int) []byte {
var buf bytes.Buffer
// 使用Go标准库的jpeg.Encode函数进行编码
jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality})
return buf.Bytes()
}
// forceCompressToSize 强制将图像压缩到指定大小
// 使用两阶段压缩策略:先尝试调整质量,若不行再进行尺寸缩放
// 参数:
// - img: 原始图像
// - targetSize: 目标文件大小(字节)
// 返回值:
// - []byte: 压缩后的图像数据
// - error: 可能的错误信息
func forceCompressToSize(img image.Image, targetSize int64) ([]byte, error) {
// 获取原始图像的尺寸
originalBounds := img.Bounds()
origW, origH := originalBounds.Dx(), originalBounds.Dy()
// 阶段1:仅调整JPEG质量参数(不改变图像尺寸)
// 从高质量开始尝试,逐步降低质量
for q := 95; q >= 10; q -= 5 {
data := encodeJPEG(img, q)
// 如果当前质量下的文件大小已满足要求,直接返回
if int64(len(data)) <= targetSize {
return data, nil
}
}
// 阶段2:如果仅调整质量无法达到目标大小,则使用二分法缩放图像尺寸
lowScale, highScale := 0.25, 1.0 // 缩放比例范围 (25% - 100%)
bestData := make([]byte, 0) // 存储最佳压缩结果
minDimension := 100 // 最小维度,防止图像被过度缩小
const maxIterations = 100 // 最大迭代次数,避免死循环
for i := 0; i < maxIterations; i++ {
// 计算当前的缩放比例(二分法)
scale := (lowScale + highScale) / 2
// 计算新的图像尺寸
newW := int(float64(origW) * scale)
newH := int(float64(origH) * scale)
// 确保图像不会小于最小尺寸
if newW < minDimension || newH < minDimension {
newW, newH = minDimension, minDimension
}
// 缩放图像并以固定质量(75)编码
smallImg := resizeNearest(img, newW, newH)
data := encodeJPEG(smallImg, 75)
fileSize := int64(len(data))
// 根据当前文件大小调整搜索范围
if fileSize <= targetSize {
// 如果符合要求,记录最佳结果并尝试更大的尺寸
if len(bestData) == 0 || fileSize < int64(len(bestData)) {
bestData = data
}
lowScale = scale // 尝试更大的尺寸
} else {
highScale = scale // 尝试更小的尺寸
}
// 当缩放比例的精度足够时退出循环
if highScale-lowScale < 0.01 { // 1%的精度
break
}
}
// 如果找到合适的压缩结果,返回最佳数据
if len(bestData) > 0 {
return bestData, nil
}
// 最后兜底策略:如果没有找到合适的尺寸,则返回最小尺寸的图片
finalImg := resizeNearest(img, minDimension, minDimension)
return encodeJPEG(finalImg, 75), nil
}
func main() {
// 解析命令行参数
flag.Parse()
args := flag.Args()
// 检查参数数量是否正确
if len(args) != 3 {
fmt.Println("用法: go run main.go <输入文件路径> <输出文件路径> <目标大小(KB)>")
fmt.Println("功能: 将输入图像压缩到指定大小,输出始终为JPEG格式")
os.Exit(1)
}
// 提取参数值
inFile := args[0]
outFile := args[1]
// 解析目标大小
targetKB, err := strconv.ParseInt(args[2], 10, 64)
if err != nil {
fmt.Println("请提供有效的目标大小(KB)")
os.Exit(1)
}
// 将KB转换为字节
target := targetKB * 1024
// 检查输入文件是否存在
if _, err := os.Stat(inFile); os.IsNotExist(err) {
fmt.Printf("错误: 输入文件 '%s' 不存在\n", inFile)
os.Exit(1)
}
// 打开输入文件
f, err := os.Open(inFile)
if err != nil {
fmt.Printf("错误: 无法打开输入文件: %v\n", err)
os.Exit(1)
}
defer f.Close() // 确保在函数结束时关闭文件
// 解码图像(支持多种格式)
img, format, err := image.Decode(f)
if err != nil {
fmt.Printf("错误: 无法解码图像: %v\n", err)
os.Exit(1)
}
fmt.Printf("成功读取 %s 格式图像\n", format)
fmt.Printf("开始压缩,目标大小: %d 字节\n", target)
// 执行压缩
data, err := forceCompressToSize(img, target)
if err != nil {
fmt.Printf("错误: 压缩过程中出错: %v\n", err)
os.Exit(1)
}
// 确保输出目录存在
outDir := filepath.Dir(outFile)
if outDir != "." {
if err := os.MkdirAll(outDir, 0755); err != nil {
fmt.Printf("错误: 无法创建输出目录: %v\n", err)
os.Exit(1)
}
}
// 写入压缩后的图像数据
err = os.WriteFile(outFile, data, 0644)
if err != nil {
fmt.Printf("错误: 无法写入输出文件: %v\n", err)
os.Exit(1)
}
// 输出压缩结果信息
fmt.Printf("压缩完成!\n")
fmt.Printf("最终大小: %d 字节 (目标: %d 字节)\n", len(data), target)
fmt.Printf("压缩率: %.2f%%\n", float64(len(data))/float64(target)*100)
}
往期部分文章列表
- 从“CPU 烧开水“到优雅暂停:Go 里 sync.Cond 的正确打开方式
- 时移世易,篡改天机:吾以 Go 语令 Windows 文件“返老还童“记
- golang圆阵列图记:天灵灵地灵灵图标排圆形
- golang解图记
- 从 4.8 秒到 0.25 秒:我是如何把 Go 正则匹配提速 19 倍的?
- 用 Go 手搓一个内网 DNS 服务器:从此告别 IP 地址,用域名畅游家庭网络!
- 我用Go写了个华容道游戏,曹操终于不用再求关羽了!
- 用 Go 接口把 Excel 变成数据库:一个疯狂但可行的想法
- 穿墙术大揭秘:用 Go 手搓一个"内网穿透"神器!
- 布隆过滤器(go):一个可能犯错但从不撒谎的内存大师
- 自由通讯的魔法:Go从零实现UDP/P2P 聊天工具
- Go语言实现的简易远程传屏工具:让你的屏幕「飞」起来
- 当你的程序学会了"诈尸":Go 实现 Windows 进程守护术
- 验证码识别API:告别收费接口,迎接免费午餐
- 用 Go 给 Windows 装个"顺风耳":两分钟写个录音小工具
- 无奈!我用go写了个MySQL服务
- 使用 Go + govcl 实现 Windows 资源管理器快捷方式管理器
- 用 Go 手搓一个 NTP 服务:从"时间混乱"到"精准同步"的奇幻之旅
- 用 Go 手搓一个 Java 构建工具:当 IDE 不在身边时的自救指南
- 深入理解 Windows 全局键盘钩子(Hook):拦截 Win 键的 Go 实现
- 用 Go 语言实现《周易》大衍筮法起卦程序
- Go 语言400行代码实现 INI 配置文件解析器:支持注释、转义与类型推断
- 高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全
- Golang + OpenSSL 实现 TLS 安全通信:从私有 CA 到动态证书加载