痛點(diǎn)催生的靈感
話說(shuō)某天,我正在幫家里的大朋友處理"學(xué)生綜合素質(zhì)評(píng)價(jià)信息管理系統(tǒng)"的資料上傳。系統(tǒng)有個(gè)令人無(wú)語(yǔ)的限制:圖片大小不能超過(guò)200KB。
可是現(xiàn)在的手機(jī)拍照功能太強(qiáng)大了?。‰S隨便便一張照片都是幾MB大小。于是我就陷入了一個(gè)循環(huán):
- 拍照片 → 太大上傳失敗
- 找在線壓縮網(wǎng)站 → 廣告彈窗滿天飛
- 打開(kāi)Photoshop → 等半天,然后點(diǎn)半天,最后忘了保存格式
- 最終:心態(tài)爆炸
于是我想:"我可是個(gè)程序員?。槭裁床蛔约簩?xiě)一個(gè)工具呢?" 于是這個(gè)20分鐘速成的圖片壓縮小工具就誕生了。
Go語(yǔ)言:程序員的瑞士軍刀
為什么用Go語(yǔ)言?因?yàn)樗娴奶?!好!用!了?/p>
- 開(kāi)箱即用:標(biāo)準(zhǔn)庫(kù)自帶圖像處理功能,完全不用找第三方包
- 編譯超快:寫(xiě)完代碼,"go build"一下,瞬間得到一個(gè)可執(zhí)行文件
- 跨平臺(tái):一次編寫(xiě),到處運(yùn)行,Windows、Mac、Linux通吃
- 代碼簡(jiǎn)潔:同樣的功能,Go代碼比其他語(yǔ)言至少短一半
工作原理:雙管齊下的壓縮策略
這個(gè)工具的核心思想很簡(jiǎn)單:先調(diào)質(zhì)量,再縮尺寸。
第一階段:質(zhì)量壓縮
JPEG圖片有個(gè)質(zhì)量參數(shù)(1-100),我們從高質(zhì)量開(kāi)始,逐步降低:
- 從95%質(zhì)量開(kāi)始嘗試
- 如果文件大小還是超過(guò)目標(biāo),降到90%
- 繼續(xù)降到85%...直到10%
- 如果某個(gè)質(zhì)量下的文件大小滿足要求,直接返回
這種方法的優(yōu)點(diǎn)是不會(huì)改變圖片尺寸,只是犧牲一點(diǎn)點(diǎn)視覺(jué)質(zhì)量。
第二階段:尺寸壓縮
如果單純降低質(zhì)量還不夠,我們就開(kāi)始縮小圖片尺寸:
- 使用二分法智能尋找最佳縮放比例
- 從25%到100%之間搜索
- 每次縮放后都檢查文件大小
- 確保圖像不會(huì)被縮得太?。ㄔO(shè)置最小尺寸保護(hù))
技術(shù)亮點(diǎn)
- 二分查找算法:比線性搜索更高效,幾輪迭代就能找到合適的尺寸
- 邊界保護(hù):防止圖像被壓縮得太小導(dǎo)致無(wú)法識(shí)別
- 錯(cuò)誤處理:友好的錯(cuò)誤提示,不會(huì)讓用戶一臉懵逼
- 多種格式支持:輸入支持JPEG、PNG、GIF等多種格式
編譯方法:三步搞定
Go語(yǔ)言的編譯超級(jí)簡(jiǎn)單,只需要幾個(gè)簡(jiǎn)單的步驟:
1. 確保已安裝Go環(huán)境
首先,確保你的電腦上已經(jīng)安裝了Go。打開(kāi)命令行,輸入:
go version
如果顯示了Go的版本信息,說(shuō)明已經(jīng)安裝好了。如果沒(méi)有,請(qǐng)先去Go官網(wǎng)下載安裝。
2. 編譯程序
進(jìn)入到包含main.go的目錄,執(zhí)行以下命令:
# Windows系統(tǒng)
go build -o 圖片壓縮.exe main.go
# Mac/Linux系統(tǒng)
go build -o 圖片壓縮 main.go
編譯完成后,當(dāng)前目錄下會(huì)生成一個(gè)可執(zhí)行文件。
3. 運(yùn)行程序
編譯完成后,使用方法超級(jí)簡(jiǎn)單:
# Windows
圖片壓縮.exe <輸入文件路徑> <輸出文件路徑> <目標(biāo)大?。↘B)>
# Mac/Linux
./圖片壓縮 <輸入文件路徑> <輸出文件路徑> <目標(biāo)大小(KB)>
例如,要把一張照片壓縮到200KB以內(nèi):
# Windows
圖片壓縮.exe 學(xué)生照片.jpg 壓縮后的照片.jpg 200
# Mac/Linux
./圖片壓縮 學(xué)生照片.jpg 壓縮后的照片.jpg 200
運(yùn)行后,工具會(huì)告訴你壓縮結(jié)果,包括最終大小和壓縮率。
總結(jié):編程解決實(shí)際問(wèn)題的快樂(lè)
這個(gè)小工具雖然簡(jiǎn)單,但它真正解決了實(shí)際問(wèn)題。現(xiàn)在,我再也不用為了上傳一張照片而煩惱了。
這也正是編程的魅力所在:用幾行代碼,解決生活中的一個(gè)小痛點(diǎn)。而且Go語(yǔ)言讓這個(gè)過(guò)程變得如此簡(jiǎn)單和高效。
如果你也有類似的需求,不妨試試這個(gè)工具,或者根據(jù)源碼自己定制一個(gè)適合你的版本!
完整源碼
package main
import (
"bytes"
"flag"
"fmt"
"image"
"image/jpeg"
"os"
"path/filepath"
"strconv"
)
// resizeNearest 使用最近鄰插值算法縮放圖像
// 最近鄰插值是最簡(jiǎn)單的圖像縮放算法,通過(guò)直接映射源圖像像素來(lái)實(shí)現(xiàn)縮放
// 參數(shù):
// - src: 源圖像
// - newWidth: 目標(biāo)寬度
// - newHeight: 目標(biāo)高度
// 返回值:
// - *image.NRGBA: 縮放后的圖像
func resizeNearest(src image.Image, newWidth, newHeight int) *image.NRGBA {
// 獲取源圖像的邊界和尺寸
srcBounds := src.Bounds()
srcW, srcH := srcBounds.Dx(), srcBounds.Dy()
// 創(chuàng)建目標(biāo)圖像,使用NRGBA格式(無(wú)預(yù)乘alpha的RGBA)
dst := image.NewNRGBA(image.Rect(0, 0, newWidth, newHeight))
// 遍歷目標(biāo)圖像的每個(gè)像素
for y := 0; y < newHeight; y++ {
for x := 0; x < newWidth; x++ {
// 計(jì)算源圖像中對(duì)應(yīng)的坐標(biāo)
srcX := int(float64(x) * float64(srcW) / float64(newWidth))
srcY := int(float64(y) * float64(srcH) / float64(newHeight))
// 邊界檢查,確保不會(huì)越界
if srcX >= srcW {
srcX = srcW - 1
}
if srcY >= srcH {
srcY = srcH - 1
}
// 將源圖像像素顏色復(fù)制到目標(biāo)圖像
dst.Set(x, y, src.At(srcX, srcY))
}
}
return dst
}
// encodeJPEG 將圖像編碼為JPEG格式的字節(jié)數(shù)據(jù)
// 參數(shù):
// - img: 要編碼的圖像
// - quality: JPEG壓縮質(zhì)量(1-100),值越高質(zhì)量越好
// 返回值:
// - []byte: 編碼后的JPEG字節(jié)數(shù)據(jù)
func encodeJPEG(img image.Image, quality int) []byte {
var buf bytes.Buffer
// 使用Go標(biāo)準(zhǔn)庫(kù)的jpeg.Encode函數(shù)進(jìn)行編碼
jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality})
return buf.Bytes()
}
// forceCompressToSize 強(qiáng)制將圖像壓縮到指定大小
// 使用兩階段壓縮策略:先嘗試調(diào)整質(zhì)量,若不行再進(jìn)行尺寸縮放
// 參數(shù):
// - img: 原始圖像
// - targetSize: 目標(biāo)文件大小(字節(jié))
// 返回值:
// - []byte: 壓縮后的圖像數(shù)據(jù)
// - error: 可能的錯(cuò)誤信息
func forceCompressToSize(img image.Image, targetSize int64) ([]byte, error) {
// 獲取原始圖像的尺寸
originalBounds := img.Bounds()
origW, origH := originalBounds.Dx(), originalBounds.Dy()
// 階段1:僅調(diào)整JPEG質(zhì)量參數(shù)(不改變圖像尺寸)
// 從高質(zhì)量開(kāi)始嘗試,逐步降低質(zhì)量
for q := 95; q >= 10; q -= 5 {
data := encodeJPEG(img, q)
// 如果當(dāng)前質(zhì)量下的文件大小已滿足要求,直接返回
if int64(len(data)) <= targetSize {
return data, nil
}
}
// 階段2:如果僅調(diào)整質(zhì)量無(wú)法達(dá)到目標(biāo)大小,則使用二分法縮放圖像尺寸
lowScale, highScale := 0.25, 1.0 // 縮放比例范圍 (25% - 100%)
bestData := make([]byte, 0) // 存儲(chǔ)最佳壓縮結(jié)果
minDimension := 100 // 最小維度,防止圖像被過(guò)度縮小
const maxIterations = 100 // 最大迭代次數(shù),避免死循環(huán)
for i := 0; i < maxIterations; i++ {
// 計(jì)算當(dāng)前的縮放比例(二分法)
scale := (lowScale + highScale) / 2
// 計(jì)算新的圖像尺寸
newW := int(float64(origW) * scale)
newH := int(float64(origH) * scale)
// 確保圖像不會(huì)小于最小尺寸
if newW < minDimension || newH < minDimension {
newW, newH = minDimension, minDimension
}
// 縮放圖像并以固定質(zhì)量(75)編碼
smallImg := resizeNearest(img, newW, newH)
data := encodeJPEG(smallImg, 75)
fileSize := int64(len(data))
// 根據(jù)當(dāng)前文件大小調(diào)整搜索范圍
if fileSize <= targetSize {
// 如果符合要求,記錄最佳結(jié)果并嘗試更大的尺寸
if len(bestData) == 0 || fileSize < int64(len(bestData)) {
bestData = data
}
lowScale = scale // 嘗試更大的尺寸
} else {
highScale = scale // 嘗試更小的尺寸
}
// 當(dāng)縮放比例的精度足夠時(shí)退出循環(huán)
if highScale-lowScale < 0.01 { // 1%的精度
break
}
}
// 如果找到合適的壓縮結(jié)果,返回最佳數(shù)據(jù)
if len(bestData) > 0 {
return bestData, nil
}
// 最后兜底策略:如果沒(méi)有找到合適的尺寸,則返回最小尺寸的圖片
finalImg := resizeNearest(img, minDimension, minDimension)
return encodeJPEG(finalImg, 75), nil
}
func main() {
// 解析命令行參數(shù)
flag.Parse()
args := flag.Args()
// 檢查參數(shù)數(shù)量是否正確
if len(args) != 3 {
fmt.Println("用法: go run main.go <輸入文件路徑> <輸出文件路徑> <目標(biāo)大?。↘B)>")
fmt.Println("功能: 將輸入圖像壓縮到指定大小,輸出始終為JPEG格式")
os.Exit(1)
}
// 提取參數(shù)值
inFile := args[0]
outFile := args[1]
// 解析目標(biāo)大小
targetKB, err := strconv.ParseInt(args[2], 10, 64)
if err != nil {
fmt.Println("請(qǐng)?zhí)峁┯行У哪繕?biāo)大?。↘B)")
os.Exit(1)
}
// 將KB轉(zhuǎn)換為字節(jié)
target := targetKB * 1024
// 檢查輸入文件是否存在
if _, err := os.Stat(inFile); os.IsNotExist(err) {
fmt.Printf("錯(cuò)誤: 輸入文件 '%s' 不存在\n", inFile)
os.Exit(1)
}
// 打開(kāi)輸入文件
f, err := os.Open(inFile)
if err != nil {
fmt.Printf("錯(cuò)誤: 無(wú)法打開(kāi)輸入文件: %v\n", err)
os.Exit(1)
}
defer f.Close() // 確保在函數(shù)結(jié)束時(shí)關(guān)閉文件
// 解碼圖像(支持多種格式)
img, format, err := image.Decode(f)
if err != nil {
fmt.Printf("錯(cuò)誤: 無(wú)法解碼圖像: %v\n", err)
os.Exit(1)
}
fmt.Printf("成功讀取 %s 格式圖像\n", format)
fmt.Printf("開(kāi)始?jí)嚎s,目標(biāo)大小: %d 字節(jié)\n", target)
// 執(zhí)行壓縮
data, err := forceCompressToSize(img, target)
if err != nil {
fmt.Printf("錯(cuò)誤: 壓縮過(guò)程中出錯(cuò): %v\n", err)
os.Exit(1)
}
// 確保輸出目錄存在
outDir := filepath.Dir(outFile)
if outDir != "." {
if err := os.MkdirAll(outDir, 0755); err != nil {
fmt.Printf("錯(cuò)誤: 無(wú)法創(chuàng)建輸出目錄: %v\n", err)
os.Exit(1)
}
}
// 寫(xiě)入壓縮后的圖像數(shù)據(jù)
err = os.WriteFile(outFile, data, 0644)
if err != nil {
fmt.Printf("錯(cuò)誤: 無(wú)法寫(xiě)入輸出文件: %v\n", err)
os.Exit(1)
}
// 輸出壓縮結(jié)果信息
fmt.Printf("壓縮完成!\n")
fmt.Printf("最終大小: %d 字節(jié) (目標(biāo): %d 字節(jié))\n", len(data), target)
fmt.Printf("壓縮率: %.2f%%\n", float64(len(data))/float64(target)*100)
}
往期部分文章列表
- 從“CPU 燒開(kāi)水“到優(yōu)雅暫停:Go 里 sync.Cond 的正確打開(kāi)方式
- 時(shí)移世易,篡改天機(jī):吾以 Go 語(yǔ)令 Windows 文件“返老還童“記
- golang圓陣列圖記:天靈靈地靈靈圖標(biāo)排圓形
- golang解圖記
- 從 4.8 秒到 0.25 秒:我是如何把 Go 正則匹配提速 19 倍的?
- 用 Go 手搓一個(gè)內(nèi)網(wǎng) DNS 服務(wù)器:從此告別 IP 地址,用域名暢游家庭網(wǎng)絡(luò)!
- 我用Go寫(xiě)了個(gè)華容道游戲,曹操終于不用再求關(guān)羽了!
- 用 Go 接口把 Excel 變成數(shù)據(jù)庫(kù):一個(gè)瘋狂但可行的想法
- 穿墻術(shù)大揭秘:用 Go 手搓一個(gè)"內(nèi)網(wǎng)穿透"神器!
- 布隆過(guò)濾器(go):一個(gè)可能犯錯(cuò)但從不撒謊的內(nèi)存大師
- 自由通訊的魔法:Go從零實(shí)現(xiàn)UDP/P2P 聊天工具
- Go語(yǔ)言實(shí)現(xiàn)的簡(jiǎn)易遠(yuǎn)程傳屏工具:讓你的屏幕「飛」起來(lái)
- 當(dāng)你的程序?qū)W會(huì)了"詐尸":Go 實(shí)現(xiàn) Windows 進(jìn)程守護(hù)術(shù)
- 驗(yàn)證碼識(shí)別API:告別收費(fèi)接口,迎接免費(fèi)午餐
- 用 Go 給 Windows 裝個(gè)"順風(fēng)耳":兩分鐘寫(xiě)個(gè)錄音小工具
- 無(wú)奈!我用go寫(xiě)了個(gè)MySQL服務(wù)
- 使用 Go + govcl 實(shí)現(xiàn) Windows 資源管理器快捷方式管理器
- 用 Go 手搓一個(gè) NTP 服務(wù):從"時(shí)間混亂"到"精準(zhǔn)同步"的奇幻之旅
- 用 Go 手搓一個(gè) Java 構(gòu)建工具:當(dāng) IDE 不在身邊時(shí)的自救指南
- 深入理解 Windows 全局鍵盤(pán)鉤子(Hook):攔截 Win 鍵的 Go 實(shí)現(xiàn)
- 用 Go 語(yǔ)言實(shí)現(xiàn)《周易》大衍筮法起卦程序
- Go 語(yǔ)言400行代碼實(shí)現(xiàn) INI 配置文件解析器:支持注釋、轉(zhuǎn)義與類型推斷
- 高性能 Go 語(yǔ)言帶 TTL 的內(nèi)存緩存實(shí)現(xiàn):精確過(guò)期、自動(dòng)刷新、并發(fā)安全
- Golang + OpenSSL 實(shí)現(xiàn) TLS 安全通信:從私有 CA 到動(dòng)態(tài)證書(shū)加載