591 lines
15 KiB
Go
591 lines
15 KiB
Go
package main
|
||
|
||
// #include <stdlib.h>
|
||
import "C"
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"github.com/disintegration/imaging"
|
||
"github.com/nfnt/resize"
|
||
"golang.org/x/image/draw"
|
||
"image"
|
||
"image/color"
|
||
"image/jpeg"
|
||
_ "image/jpeg"
|
||
"image/png"
|
||
_ "image/png"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"unsafe"
|
||
)
|
||
|
||
// Config 配置结构体
|
||
type Config struct {
|
||
OutputDir string // 输出目录路径
|
||
FileName string // 文件名
|
||
MatchDir string // 满足条件的图片目录名
|
||
UnmatchDir string // 不满足条件的图片目录名
|
||
EqualHeightDir string // 等高的图片目录名
|
||
WhiteDir string // 白色底图的图片目录名
|
||
WhiteBorderPngDir string // 去白边转PNG的图片目录名
|
||
MinWhitePct float64 // 纯白占比下限(0-1)
|
||
MaxWhitePct float64 // 纯白占比上限(0-1)
|
||
Extensions []string // 支持的图片扩展名
|
||
}
|
||
|
||
// 检查图片
|
||
func validateConfig(config *Config) error {
|
||
// 检查百分比范围
|
||
if config.MinWhitePct < 0 || config.MinWhitePct > 1 {
|
||
return fmt.Errorf("纯白占比下限必须在0-1之间")
|
||
}
|
||
|
||
if config.MaxWhitePct < 0 || config.MaxWhitePct > 1 {
|
||
return fmt.Errorf("纯白占比上限必须在0-1之间")
|
||
}
|
||
|
||
if config.MinWhitePct > config.MaxWhitePct {
|
||
return fmt.Errorf("下限不能大于上限")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func createDirs(config *Config) error {
|
||
// 创建输出根目录
|
||
if err := os.MkdirAll(config.OutputDir, 0755); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 创建匹配目录
|
||
matchPath := filepath.Join(config.OutputDir, config.MatchDir)
|
||
if err := os.MkdirAll(matchPath, 0755); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 创建不匹配目录
|
||
unmatchPath := filepath.Join(config.OutputDir, config.UnmatchDir)
|
||
if err := os.MkdirAll(unmatchPath, 0755); err != nil {
|
||
return err
|
||
}
|
||
|
||
equalHeightPath := filepath.Join(config.OutputDir, config.EqualHeightDir)
|
||
if err := os.MkdirAll(equalHeightPath, 0755); err != nil {
|
||
return err
|
||
}
|
||
|
||
whitePath := filepath.Join(config.OutputDir, config.WhiteDir)
|
||
if err := os.MkdirAll(whitePath, 0755); err != nil {
|
||
return err
|
||
}
|
||
|
||
whiteBorderPngPath := filepath.Join(config.OutputDir, config.WhiteBorderPngDir)
|
||
if err := os.MkdirAll(whiteBorderPngPath, 0755); err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// 计算纯白占比
|
||
func calculateWhitePercentage(imagePath string) (float64, error) {
|
||
// 打开图片文件
|
||
file, err := os.Open(imagePath)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
defer file.Close()
|
||
|
||
// 解码图片
|
||
img, _, err := image.Decode(file)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
|
||
bounds := img.Bounds()
|
||
totalPixels := bounds.Dx() * bounds.Dy()
|
||
whitePixels := 0
|
||
|
||
// 遍历每个像素,判断是否为纯白色
|
||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||
pixel := color.RGBAModel.Convert(img.At(x, y)).(color.RGBA)
|
||
|
||
// 判断是否为纯白色 (R=255, G=255, B=255)
|
||
if pixel.R == 255 && pixel.G == 255 && pixel.B == 255 {
|
||
whitePixels++
|
||
}
|
||
}
|
||
}
|
||
|
||
return float64(whitePixels) / float64(totalPixels), nil
|
||
}
|
||
|
||
// 复制文件到相应目录
|
||
func copyToDestination(srcPath string, config *Config, isMatch bool) error {
|
||
filename := filepath.Base(srcPath)
|
||
|
||
// 确定目标目录
|
||
var destDir string
|
||
if isMatch {
|
||
destDir = filepath.Join(config.OutputDir, config.MatchDir)
|
||
} else {
|
||
destDir = filepath.Join(config.OutputDir, config.UnmatchDir)
|
||
}
|
||
|
||
destPath := filepath.Join(destDir, filename)
|
||
|
||
// 读取源文件
|
||
data, err := os.ReadFile(srcPath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 写入目标文件
|
||
return os.WriteFile(destPath, data, 0644)
|
||
}
|
||
|
||
// 保存文件
|
||
func saveImage(outputPath string, img image.Image, format string) error {
|
||
// 创建输出文件
|
||
outFile, err := os.Create(outputPath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer outFile.Close()
|
||
|
||
// 根据原始格式保存图片
|
||
switch format {
|
||
case "jpeg", "jpg":
|
||
// JPEG 格式可以设置质量参数
|
||
return jpeg.Encode(outFile, img, &jpeg.Options{Quality: 95})
|
||
case "png":
|
||
// PNG 格式通常不需要质量参数
|
||
return png.Encode(outFile, img)
|
||
default:
|
||
// 默认使用 JPEG 格式
|
||
return jpeg.Encode(outFile, img, &jpeg.Options{Quality: 95})
|
||
}
|
||
}
|
||
|
||
// 检测图片纯白占比
|
||
func processImage(config *Config) error {
|
||
// 创建输出目录
|
||
if err := createDirs(config); err != nil {
|
||
return fmt.Errorf("创建目录失败: %v\n", err)
|
||
}
|
||
|
||
if err := validateConfig(config); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 计算纯白占比
|
||
whitePct, err := calculateWhitePercentage(config.FileName)
|
||
if err != nil {
|
||
return fmt.Errorf("错误: %v\n", err)
|
||
}
|
||
// 判断是否在范围内
|
||
isMatch := whitePct >= config.MinWhitePct && whitePct <= config.MaxWhitePct
|
||
status := "❌ 不满足"
|
||
if isMatch {
|
||
status = "✅ 满足"
|
||
}
|
||
fmt.Printf("纯白占比: %.2f%% %s\n", whitePct*100, status)
|
||
|
||
// 复制文件到相应目录
|
||
if err = copyToDestination(config.FileName, config, isMatch); err != nil {
|
||
return fmt.Errorf("复制失败: %v\n", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// 根据原始图片生成新的白底图片
|
||
func createWhiteBottomCenteredImage(config *Config, width, height int) (string, error) {
|
||
// 创建输出目录
|
||
if err := createDirs(config); err != nil {
|
||
fmt.Printf("创建目录失败: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
// 打开图片
|
||
file, err := os.Open(config.FileName)
|
||
if err != nil {
|
||
return "", fmt.Errorf("打开图片失败: %v", err)
|
||
}
|
||
defer file.Close()
|
||
// 解码原始图片
|
||
img, format, err := image.Decode(file)
|
||
if err != nil {
|
||
return "", fmt.Errorf("图片解码失败: %v", err)
|
||
}
|
||
|
||
// 创建透明背景
|
||
dst := image.NewRGBA(image.Rect(0, 0, width, height))
|
||
|
||
// 设置背景颜色
|
||
var bgColor color.Color
|
||
|
||
bgColor = color.RGBA{255, 255, 255, 255} // 白色
|
||
|
||
// 填充透明背景
|
||
draw.Draw(dst, dst.Bounds(), &image.Uniform{bgColor}, image.Point{}, draw.Src)
|
||
|
||
// 计算居中位置
|
||
srcBounds := img.Bounds()
|
||
srcWidth := srcBounds.Dx()
|
||
srcHeight := srcBounds.Dy()
|
||
|
||
x := (width - srcWidth) / 2
|
||
y := (height - srcHeight) / 2
|
||
|
||
// 将原图绘制到中央
|
||
draw.Draw(dst, image.Rect(x, y, x+srcWidth, y+srcHeight), img, image.Point{}, draw.Over)
|
||
|
||
filename := filepath.Base(config.FileName)
|
||
destPath := filepath.Join(config.OutputDir, config.WhiteDir, filename)
|
||
saveImage(destPath, dst, format)
|
||
|
||
return destPath, nil
|
||
}
|
||
|
||
// 根据高度生成等比例图片
|
||
func resizeToHeightQuality(config *Config, targetHeight int) (string, error) {
|
||
// 创建输出目录
|
||
if err := createDirs(config); err != nil {
|
||
fmt.Printf("创建目录失败: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
// 打开图片
|
||
file, err := os.Open(config.FileName)
|
||
if err != nil {
|
||
return "", fmt.Errorf("打开图片失败: %v", err)
|
||
}
|
||
defer file.Close()
|
||
// 解码原始图片
|
||
img, format, err := image.Decode(file)
|
||
if err != nil {
|
||
return "", fmt.Errorf("图片解码失败: %v", err)
|
||
}
|
||
|
||
bounds := img.Bounds()
|
||
srcWidth := bounds.Dx()
|
||
srcHeight := bounds.Dy()
|
||
|
||
// 计算等比例缩放后的宽度
|
||
targetWidth := uint(float64(srcWidth) * float64(targetHeight) / float64(srcHeight))
|
||
// 使用 Lanczos3 插值算法进行高质量缩放
|
||
imageNew := resize.Resize(targetWidth, uint(targetHeight), img, resize.Lanczos3)
|
||
|
||
// 保存图片到指定目录下
|
||
filename := filepath.Base(config.FileName)
|
||
destPath := filepath.Join(config.OutputDir, config.EqualHeightDir, filename)
|
||
saveImage(destPath, imageNew, format)
|
||
|
||
return destPath, nil
|
||
}
|
||
|
||
// ImageToPNGConverter 图片去白边并转为PNG
|
||
type ImageToPNGConverter struct {
|
||
Threshold int
|
||
Margin int
|
||
BgColor color.RGBA
|
||
DetectColor *color.RGBA
|
||
KeepTransparent bool
|
||
PNGCompressLevel png.CompressionLevel
|
||
Quality int
|
||
}
|
||
|
||
// 去掉白边并转PNG图片工具
|
||
func removeWhiteBorderAndPNG(config *Config) (string, error) {
|
||
// 创建输出目录
|
||
if err := createDirs(config); err != nil {
|
||
fmt.Printf("创建目录失败: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
// 打开图片
|
||
file, err := os.Open(config.FileName)
|
||
if err != nil {
|
||
return "", fmt.Errorf("打开图片失败: %v", err)
|
||
}
|
||
defer file.Close()
|
||
// 解码原始图片
|
||
img, _, err := image.Decode(file)
|
||
if err != nil {
|
||
return "", fmt.Errorf("图片解码失败: %v", err)
|
||
}
|
||
|
||
compressLevel := 6
|
||
|
||
// 创建转换器
|
||
compressionLevel := png.DefaultCompression
|
||
switch {
|
||
case compressLevel <= 0:
|
||
compressionLevel = png.NoCompression
|
||
case compressLevel >= 9:
|
||
compressionLevel = png.BestCompression
|
||
default:
|
||
// 使用默认压缩级别
|
||
}
|
||
|
||
converter := newImageToPNGConverter(
|
||
240,
|
||
0,
|
||
&color.RGBA{R: 255, G: 255, B: 255, A: 255},
|
||
nil,
|
||
false,
|
||
compressionLevel,
|
||
95,
|
||
)
|
||
|
||
toPNG := converter.convertToPNG(img, true)
|
||
|
||
// 保存图片到指定目录下
|
||
filename := filepath.Base(config.FileName)
|
||
// 去除扩展名
|
||
nameWithoutExt := strings.TrimSuffix(filename, filepath.Ext(filename))
|
||
destPath := filepath.Join(config.OutputDir, config.WhiteBorderPngDir, nameWithoutExt+".png")
|
||
saveImage(destPath, toPNG, "png")
|
||
return destPath, nil
|
||
}
|
||
|
||
// newImageToPNGConverter 创建新的转换器
|
||
func newImageToPNGConverter(threshold, margin int, bgColor, detectColor *color.RGBA,
|
||
keepTransparent bool, compressLevel png.CompressionLevel, quality int) *ImageToPNGConverter {
|
||
|
||
// 默认背景色为白色
|
||
bg := color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
||
if bgColor != nil {
|
||
bg = *bgColor
|
||
}
|
||
|
||
return &ImageToPNGConverter{
|
||
Threshold: threshold,
|
||
Margin: margin,
|
||
BgColor: bg,
|
||
DetectColor: detectColor,
|
||
KeepTransparent: keepTransparent,
|
||
PNGCompressLevel: compressLevel,
|
||
Quality: quality,
|
||
}
|
||
}
|
||
|
||
// ConvertToPNG 转换图片为PNG格式
|
||
func (c *ImageToPNGConverter) convertToPNG(img image.Image, addBackground bool) image.Image {
|
||
// 先裁剪白边
|
||
trimmed := c.trimImage(img)
|
||
|
||
// 检查是否有alpha通道
|
||
_, hasAlpha := trimmed.(*image.NRGBA)
|
||
if !hasAlpha {
|
||
_, hasAlpha = trimmed.(*image.RGBA)
|
||
}
|
||
|
||
if hasAlpha {
|
||
if c.KeepTransparent {
|
||
// 保持透明
|
||
return trimmed
|
||
} else if addBackground {
|
||
// 添加背景色
|
||
bg := image.NewRGBA(trimmed.Bounds())
|
||
draw.Draw(bg, bg.Bounds(), &image.Uniform{C: c.BgColor}, image.Point{}, draw.Src)
|
||
draw.Draw(bg, bg.Bounds(), trimmed, trimmed.Bounds().Min, draw.Over)
|
||
return bg
|
||
}
|
||
} else {
|
||
// 非透明图像
|
||
if c.KeepTransparent {
|
||
// 转换为RGBA
|
||
rgba := image.NewRGBA(trimmed.Bounds())
|
||
draw.Draw(rgba, rgba.Bounds(), trimmed, trimmed.Bounds().Min, draw.Src)
|
||
return rgba
|
||
}
|
||
return trimmed
|
||
}
|
||
|
||
return trimmed
|
||
}
|
||
|
||
// TrimImage 裁剪图片白边
|
||
func (c *ImageToPNGConverter) trimImage(img image.Image) image.Image {
|
||
borders := c.findBorders(img)
|
||
|
||
// 创建一个新的图像并裁剪
|
||
trimmed := imaging.Crop(img, borders)
|
||
return trimmed
|
||
}
|
||
|
||
// FindBorders 查找图片的有效边界
|
||
func (c *ImageToPNGConverter) findBorders(img image.Image) image.Rectangle {
|
||
bounds := img.Bounds()
|
||
width := bounds.Dx()
|
||
height := bounds.Dy()
|
||
|
||
// 检查图像是否有alpha通道
|
||
_, hasAlpha := img.(*image.NRGBA)
|
||
if !hasAlpha {
|
||
_, hasAlpha = img.(*image.RGBA)
|
||
}
|
||
|
||
// 初始化边界
|
||
left := width
|
||
top := height
|
||
right := 0
|
||
bottom := 0
|
||
|
||
// 查找非背景区域
|
||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||
pixel := img.At(x, y)
|
||
if !c.isBackgroundColor(pixel, hasAlpha) {
|
||
if x < left {
|
||
left = x
|
||
}
|
||
if x > right {
|
||
right = x
|
||
}
|
||
if y < top {
|
||
top = y
|
||
}
|
||
if y > bottom {
|
||
bottom = y
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果没有找到非背景区域,返回整个图像
|
||
if left > right || top > bottom {
|
||
return bounds
|
||
}
|
||
|
||
// 添加边距
|
||
left = max(bounds.Min.X, left-c.Margin)
|
||
top = max(bounds.Min.Y, top-c.Margin)
|
||
right = min(bounds.Max.X, right+c.Margin+1)
|
||
bottom = min(bounds.Max.Y, bottom+c.Margin+1)
|
||
|
||
return image.Rect(left, top, right, bottom)
|
||
}
|
||
|
||
// IsBackgroundColor 判断像素是否为背景色
|
||
func (c *ImageToPNGConverter) isBackgroundColor(pixel color.Color, hasAlpha bool) bool {
|
||
r, g, b, a := pixel.RGBA()
|
||
|
||
// 转换为8位值
|
||
r8 := uint8(r >> 8)
|
||
g8 := uint8(g >> 8)
|
||
b8 := uint8(b >> 8)
|
||
a8 := uint8(a >> 8)
|
||
|
||
// 检查透明度
|
||
if hasAlpha && a8 < 25 { // 透明度 > 90%
|
||
return true
|
||
}
|
||
|
||
// 如果指定了检测颜色
|
||
if c.DetectColor != nil {
|
||
dr, dg, db, _ := c.DetectColor.RGBA()
|
||
dr8 := uint8(dr >> 8)
|
||
dg8 := uint8(dg >> 8)
|
||
db8 := uint8(db >> 8)
|
||
|
||
// threshold 是 int 类型,需要转换为 uint8 比较
|
||
threshold8 := uint8(255 - c.Threshold)
|
||
return absDiff(r8, dr8) <= threshold8 &&
|
||
absDiff(g8, dg8) <= threshold8 &&
|
||
absDiff(b8, db8) <= threshold8
|
||
}
|
||
|
||
// 自动检测白色/浅色背景
|
||
// 注意:这里的 c.Threshold 是 int,需要转换为 uint8
|
||
threshold8 := uint8(c.Threshold)
|
||
return r8 >= threshold8 &&
|
||
g8 >= threshold8 &&
|
||
b8 >= threshold8
|
||
}
|
||
|
||
// 辅助函数
|
||
func absDiff(a, b uint8) uint8 {
|
||
if a > b {
|
||
return a - b
|
||
}
|
||
return b - a
|
||
}
|
||
|
||
// =================== C 导出函数 =======================
|
||
// 检测图片纯白占比
|
||
//
|
||
//export ProcessImage
|
||
func ProcessImage(jsonConfig *C.char) *C.char {
|
||
configStr := C.GoString(jsonConfig)
|
||
var config *Config
|
||
if err := json.Unmarshal([]byte(configStr), &config); err != nil {
|
||
return C.CString(fmt.Sprintf("解析 config 失败: %v", err))
|
||
}
|
||
if err := processImage(config); err != nil {
|
||
return C.CString(fmt.Sprintf("%v", err))
|
||
}
|
||
return C.CString("成功")
|
||
}
|
||
|
||
// 根据原始图片生成新的白底图片
|
||
//
|
||
//export CreateWhiteBottomCenteredImage
|
||
func CreateWhiteBottomCenteredImage(jsonConfig *C.char, width, height C.int) *C.char {
|
||
configStr := C.GoString(jsonConfig)
|
||
widthInt := int(width)
|
||
heightInt := int(height)
|
||
var config *Config
|
||
if err := json.Unmarshal([]byte(configStr), &config); err != nil {
|
||
return C.CString(fmt.Sprintf("解析 config 失败: %v", err))
|
||
}
|
||
fileName, err := createWhiteBottomCenteredImage(config, widthInt, heightInt)
|
||
if err != nil {
|
||
return C.CString(fmt.Sprintf("%v", err))
|
||
}
|
||
return C.CString(fileName)
|
||
}
|
||
|
||
// 根据高度生成等比例图片
|
||
//
|
||
//export ResizeToHeightQuality
|
||
func ResizeToHeightQuality(jsonConfig *C.char, targetHeight C.int) *C.char {
|
||
configStr := C.GoString(jsonConfig)
|
||
targetHeightInt := int(targetHeight)
|
||
var config *Config
|
||
if err := json.Unmarshal([]byte(configStr), &config); err != nil {
|
||
return C.CString(fmt.Sprintf("解析 config 失败: %v", err))
|
||
}
|
||
fileName, err := resizeToHeightQuality(config, targetHeightInt)
|
||
if err != nil {
|
||
return C.CString(fmt.Sprintf("%v", err))
|
||
}
|
||
return C.CString(fileName)
|
||
}
|
||
|
||
// 去掉白边并转PNG图片工具
|
||
//
|
||
//export RemoveWhiteBorderAndPNG
|
||
func RemoveWhiteBorderAndPNG(jsonConfig *C.char) *C.char {
|
||
configStr := C.GoString(jsonConfig)
|
||
var config *Config
|
||
if err := json.Unmarshal([]byte(configStr), &config); err != nil {
|
||
return C.CString(fmt.Sprintf("解析 config 失败: %v", err))
|
||
}
|
||
fileName, err := removeWhiteBorderAndPNG(config)
|
||
if err != nil {
|
||
return C.CString(fmt.Sprintf("%v", err))
|
||
}
|
||
return C.CString(fileName)
|
||
}
|
||
|
||
// 导出函数:释放C字符串内存
|
||
//
|
||
//export FreeCString
|
||
func FreeCString(str *C.char) {
|
||
C.free(unsafe.Pointer(str))
|
||
}
|
||
|
||
//func main() {
|
||
//
|
||
//}
|