1235 lines
30 KiB
Go
1235 lines
30 KiB
Go
package main
|
||
|
||
// #include <stdlib.h>
|
||
import "C"
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"github.com/disintegration/imaging"
|
||
"github.com/makiuchi-d/gozxing"
|
||
"github.com/makiuchi-d/gozxing/qrcode"
|
||
"github.com/nfnt/resize"
|
||
"golang.org/x/image/draw"
|
||
"image"
|
||
"image/color"
|
||
"image/jpeg"
|
||
_ "image/jpeg"
|
||
"image/png"
|
||
_ "image/png"
|
||
"math"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"unsafe"
|
||
)
|
||
|
||
// Config 配置结构体
|
||
type Config struct {
|
||
OutputDir string // 输出目录路径
|
||
FileName string // 文件名
|
||
MatchDir string // 满足条件的图片目录名
|
||
UnmatchDir string // 不满足条件的图片目录名
|
||
EqualHeightDir string // 等高的图片目录名
|
||
WhiteDir string // 白色底图的图片目录名
|
||
WhiteBorderPngDir string // 去白边转PNG的图片目录名
|
||
WhiteHeightZoomDir string // 缩放的图片目录
|
||
CropDir string // 裁切的图片目录
|
||
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
|
||
}
|
||
|
||
whiteHeightZoomPath := filepath.Join(config.OutputDir, config.WhiteHeightZoomDir)
|
||
if err := os.MkdirAll(whiteHeightZoomPath, 0755); err != nil {
|
||
return err
|
||
}
|
||
|
||
cropPath := filepath.Join(config.OutputDir, config.CropDir)
|
||
if err := os.MkdirAll(cropPath, 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 resizeWTToHeightQuality(config *Config, dsWidth, dsHeight 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)
|
||
}
|
||
|
||
// 使用Lanczos3算法缩放图片
|
||
optimized := resizeImageOptimized(img, dsWidth, dsHeight)
|
||
|
||
// 保存图片到指定目录下
|
||
filename := filepath.Base(config.FileName)
|
||
destPath := filepath.Join(config.OutputDir, config.WhiteHeightZoomDir, filename)
|
||
err = saveImage(destPath, optimized, format)
|
||
if err != nil {
|
||
return "", fmt.Errorf("保存图片失败: %v", err)
|
||
}
|
||
|
||
return destPath, nil
|
||
}
|
||
|
||
// 使用Lanczos3算法缩放图片
|
||
func resizeImageOptimized(src image.Image, dstWidth, dstHeight int) image.Image {
|
||
srcBounds := src.Bounds()
|
||
srcWidth := srcBounds.Dx()
|
||
srcHeight := srcBounds.Dy()
|
||
|
||
// 创建目标图片
|
||
dst := image.NewRGBA(image.Rect(0, 0, dstWidth, dstHeight))
|
||
|
||
// 计算缩放比例
|
||
xScale := float64(srcWidth) / float64(dstWidth)
|
||
yScale := float64(srcHeight) / float64(dstHeight)
|
||
|
||
// Lanczos3算法半径
|
||
radius := 3.0
|
||
|
||
// 预计算x方向的权重
|
||
xWeights := make([][]float64, dstWidth)
|
||
xIndices := make([][]int, dstWidth)
|
||
|
||
for x := 0; x < dstWidth; x++ {
|
||
srcX := float64(x) * xScale
|
||
startX := int(math.Max(0, math.Floor(srcX-radius+0.5)))
|
||
endX := int(math.Min(float64(srcWidth-1), math.Floor(srcX+radius)))
|
||
|
||
weights := make([]float64, 0, endX-startX+1)
|
||
indices := make([]int, 0, endX-startX+1)
|
||
|
||
for sx := startX; sx <= endX; sx++ {
|
||
xDist := float64(sx) + 0.5 - srcX
|
||
weight := lanczos3(xDist)
|
||
if weight != 0 {
|
||
weights = append(weights, weight)
|
||
indices = append(indices, sx)
|
||
}
|
||
}
|
||
|
||
xWeights[x] = weights
|
||
xIndices[x] = indices
|
||
}
|
||
|
||
// 处理每一行
|
||
for y := 0; y < dstHeight; y++ {
|
||
srcY := float64(y) * yScale
|
||
startY := int(math.Max(0, math.Floor(srcY-radius+0.5)))
|
||
endY := int(math.Min(float64(srcHeight-1), math.Floor(srcY+radius)))
|
||
|
||
// 预计算y方向的权重
|
||
yWeights := make([]float64, 0, endY-startY+1)
|
||
yIndices := make([]int, 0, endY-startY+1)
|
||
|
||
for sy := startY; sy <= endY; sy++ {
|
||
yDist := float64(sy) + 0.5 - srcY
|
||
weight := lanczos3(yDist)
|
||
if weight != 0 {
|
||
yWeights = append(yWeights, weight)
|
||
yIndices = append(yIndices, sy)
|
||
}
|
||
}
|
||
|
||
// 处理每一列
|
||
for x := 0; x < dstWidth; x++ {
|
||
var rSum, gSum, bSum, aSum, weightSum float64
|
||
|
||
// 应用预计算的权重
|
||
for i, sy := range yIndices {
|
||
yWeight := yWeights[i]
|
||
|
||
for j, sx := range xIndices[x] {
|
||
xWeight := xWeights[x][j]
|
||
weight := xWeight * yWeight
|
||
|
||
// 获取源像素颜色
|
||
srcColor := src.At(sx+srcBounds.Min.X, sy+srcBounds.Min.Y)
|
||
r, g, b, a := srcColor.RGBA()
|
||
|
||
// 累加加权颜色值
|
||
rSum += float64(r>>8) * weight
|
||
gSum += float64(g>>8) * weight
|
||
bSum += float64(b>>8) * weight
|
||
aSum += float64(a>>8) * weight
|
||
weightSum += weight
|
||
}
|
||
}
|
||
|
||
// 防止除以零
|
||
if weightSum == 0 {
|
||
weightSum = 1
|
||
}
|
||
|
||
// 计算最终颜色值
|
||
r := clamp(rSum / weightSum)
|
||
g := clamp(gSum / weightSum)
|
||
b := clamp(bSum / weightSum)
|
||
a := clamp(aSum / weightSum)
|
||
|
||
// 设置目标像素
|
||
dst.SetRGBA(x, y, color.RGBA{r, g, b, a})
|
||
}
|
||
}
|
||
|
||
return dst
|
||
}
|
||
|
||
// 函数计算Lanczos3核函数值
|
||
func lanczos3(x float64) float64 {
|
||
if x == 0 {
|
||
return 1.0
|
||
}
|
||
if x < -3 || x > 3 {
|
||
return 0.0
|
||
}
|
||
return (3 * math.Sin(math.Pi*x) * math.Sin(math.Pi*x/3)) / (math.Pi * math.Pi * x * x)
|
||
}
|
||
|
||
// 将值限制在0-255范围内
|
||
func clamp(v float64) uint8 {
|
||
if v < 0 {
|
||
return 0
|
||
}
|
||
if v > 255 {
|
||
return 255
|
||
}
|
||
return uint8(v + 0.5)
|
||
}
|
||
|
||
// 图片裁切
|
||
func cropImage(config *Config, x, y, 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)
|
||
}
|
||
|
||
fmt.Printf("输入图片: %s (%dx%d, 格式: %s)\n",
|
||
config.FileName, img.Bounds().Dx(), img.Bounds().Dy(), format)
|
||
|
||
imgFile, err := basicCrop(img, x, y, width, height)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
//// 执行裁切
|
||
//var cropped image.Image
|
||
//if width > 0 && height > 0 {
|
||
// cropped, err = SmartCrop(img, x, y, width, height int)
|
||
//} else {
|
||
// // 如果未指定尺寸,使用中心裁切
|
||
// cropped, err = CropCenter(img, config.Width, config.Height)
|
||
//}
|
||
//
|
||
//if err != nil {
|
||
// return "", err
|
||
//}
|
||
//
|
||
|
||
// 保存图片到指定目录下
|
||
filename := filepath.Base(config.FileName)
|
||
destPath := filepath.Join(config.OutputDir, config.CropDir, filename)
|
||
err = saveImage(destPath, imgFile, format)
|
||
if err != nil {
|
||
return "", fmt.Errorf("保存图片失败: %v", err)
|
||
}
|
||
return destPath, nil
|
||
}
|
||
|
||
// basicCrop 基础裁切功能
|
||
func basicCrop(src image.Image, x, y, width, height int) (image.Image, error) {
|
||
srcBounds := src.Bounds()
|
||
srcWidth := srcBounds.Dx()
|
||
srcHeight := srcBounds.Dy()
|
||
|
||
// 验证裁切参数
|
||
if x < 0 || y < 0 || width <= 0 || height <= 0 {
|
||
return nil, fmt.Errorf("invalid crop parameters: x=%d, y=%d, width=%d, height=%d",
|
||
x, y, width, height)
|
||
}
|
||
|
||
if x >= srcWidth || y >= srcHeight {
|
||
return nil, fmt.Errorf("crop start point (%d, %d) is outside image bounds (%dx%d)",
|
||
x, y, srcWidth, srcHeight)
|
||
}
|
||
|
||
// 调整裁切尺寸以避免超出边界
|
||
if x+width > srcWidth {
|
||
width = srcWidth - x
|
||
}
|
||
if y+height > srcHeight {
|
||
height = srcHeight - y
|
||
}
|
||
|
||
// 从源图像中裁切
|
||
cropped := image.NewRGBA(image.Rect(0, 0, width, height))
|
||
for cy := 0; cy < height; cy++ {
|
||
for cx := 0; cx < width; cx++ {
|
||
cropped.Set(cx, cy, src.At(x+cx, y+cy))
|
||
}
|
||
}
|
||
|
||
return cropped, nil
|
||
}
|
||
|
||
//// smartCrop 智能裁切,处理边界和自动调整 -- 暂时没用
|
||
//func smartCrop(src image.Image, x, y, width, height int) (image.Image, error) {
|
||
// srcBounds := src.Bounds()
|
||
// srcWidth := srcBounds.Dx()
|
||
// srcHeight := srcBounds.Dy()
|
||
//
|
||
// // 如果宽度或高度为0,使用图像的最大可能尺寸
|
||
// if width == 0 {
|
||
// width = srcWidth - x
|
||
// }
|
||
// if height == 0 {
|
||
// height = srcHeight - y
|
||
// }
|
||
//
|
||
// // 调整裁切区域以确保在边界内
|
||
// newX := max(0, min(x, srcWidth-1))
|
||
// newY := max(0, min(x, srcHeight-1))
|
||
// width = max(1, min(width, srcWidth-x))
|
||
// height = max(1, min(height, srcHeight-y))
|
||
//
|
||
// // 如果需要保持宽高比,调整裁切区域
|
||
// if width > 0 && height > 0 {
|
||
// currentRatio := float64(width) / float64(height)
|
||
// originalRatio := float64(srcWidth) / float64(srcHeight)
|
||
//
|
||
// if math.Abs(currentRatio-originalRatio) > 0.01 {
|
||
// // 调整宽度以匹配原始宽高比
|
||
// newWidth := int(float64(height) * originalRatio)
|
||
// if newWidth <= (srcWidth - newX) {
|
||
// width = newWidth
|
||
// } else {
|
||
// // 调整高度以匹配原始宽高比
|
||
// newHeight := int(float64(width) / originalRatio)
|
||
// if newHeight <= (srcHeight - newY) {
|
||
// height = newHeight
|
||
// }
|
||
// }
|
||
// }
|
||
// }
|
||
//
|
||
// // 执行裁切
|
||
// cropped, err := basicCrop(src, newX, newY, width, height)
|
||
// if err != nil {
|
||
// return nil, err
|
||
// }
|
||
//
|
||
// return cropped, nil
|
||
//}
|
||
|
||
//// CropCenter 中心裁切 -- 暂时没用
|
||
//func CropCenter(src image.Image, width, height int) (image.Image, error) {
|
||
// srcBounds := src.Bounds()
|
||
// srcWidth := srcBounds.Dx()
|
||
// srcHeight := srcBounds.Dy()
|
||
//
|
||
// // 如果裁切尺寸大于原图,返回原图或调整裁切尺寸
|
||
// if width >= srcWidth && height >= srcHeight {
|
||
// return src, nil
|
||
// }
|
||
//
|
||
// // 计算中心裁切的起始点
|
||
// x := (srcWidth - width) / 2
|
||
// y := (srcHeight - height) / 2
|
||
//
|
||
// // 确保不超出边界
|
||
// x = max(0, min(x, srcWidth-width))
|
||
// y = max(0, min(y, srcHeight-height))
|
||
//
|
||
// return BasicCrop(src, x, y, width, height)
|
||
//}
|
||
|
||
// 识别二维码
|
||
func scanQRCode(fileName string) (bool, string, error) {
|
||
file, err := os.Open(fileName)
|
||
if err != nil {
|
||
return false, "", fmt.Errorf("打开图片失败: %v", err)
|
||
}
|
||
defer file.Close()
|
||
|
||
// 解码图像
|
||
img, _, err := image.Decode(file)
|
||
if err != nil {
|
||
return false, "", fmt.Errorf("解码图片失败: %v", err)
|
||
}
|
||
|
||
// 创建二维码读取器
|
||
bmp, err := gozxing.NewBinaryBitmapFromImage(img)
|
||
if err != nil {
|
||
return false, "", fmt.Errorf("创建位图失败: %v", err)
|
||
}
|
||
|
||
// 解码二维码
|
||
reader := qrcode.NewQRCodeReader()
|
||
|
||
result, err := reader.Decode(bmp, nil)
|
||
if err != nil {
|
||
return false, "", analyzeQRCodeError(err)
|
||
}
|
||
|
||
// 6. 打印二维码内容
|
||
fmt.Printf("二维码内容: %s\n", result.GetText())
|
||
|
||
// 7. 获取二维码位置点
|
||
points := result.GetResultPoints()
|
||
if len(points) < 3 {
|
||
fmt.Println("未找到足够的定位点")
|
||
}
|
||
|
||
return true, result.GetText(), nil
|
||
}
|
||
|
||
// 识别二维码
|
||
func scanQRCodeNew(fileName string) (bool, string, error) {
|
||
file, err := os.Open(fileName)
|
||
if err != nil {
|
||
return false, "", fmt.Errorf("打开图片失败: %v", err)
|
||
}
|
||
defer file.Close()
|
||
|
||
// 解码图像
|
||
img, _, err := image.Decode(file)
|
||
if err != nil {
|
||
return false, "", fmt.Errorf("解码图片失败: %v", err)
|
||
}
|
||
|
||
// 创建二维码读取器
|
||
bmp, err := gozxing.NewBinaryBitmapFromImage(img)
|
||
if err != nil {
|
||
return false, "", fmt.Errorf("创建位图失败: %v", err)
|
||
}
|
||
|
||
// 解码二维码
|
||
reader := qrcode.NewQRCodeReader()
|
||
|
||
result, err := reader.Decode(bmp, nil)
|
||
if err != nil {
|
||
return false, "", analyzeQRCodeError(err)
|
||
}
|
||
|
||
// 6. 打印二维码内容
|
||
fmt.Printf("二维码内容: %s\n", result.GetText())
|
||
|
||
// 7. 获取二维码位置点
|
||
points := result.GetResultPoints()
|
||
if len(points) < 3 {
|
||
fmt.Println("未找到足够的定位点")
|
||
}
|
||
|
||
// 8. 计算二维码边界框
|
||
minX, minY := int(points[0].GetX()), int(points[0].GetY())
|
||
maxX, maxY := minX, minY
|
||
|
||
for _, point := range points {
|
||
x, y := int(point.GetX()), int(point.GetY())
|
||
if x < minX {
|
||
minX = x
|
||
}
|
||
if x > maxX {
|
||
maxX = x
|
||
}
|
||
if y < minY {
|
||
minY = y
|
||
}
|
||
if y > maxY {
|
||
maxY = y
|
||
}
|
||
}
|
||
|
||
// 9. 添加一些边距(可选)
|
||
margin := 10
|
||
minX -= margin
|
||
minY -= margin
|
||
maxX += margin
|
||
maxY += margin
|
||
|
||
// 确保坐标不超出图片范围
|
||
bounds := img.Bounds()
|
||
if minX < bounds.Min.X {
|
||
minX = bounds.Min.X
|
||
}
|
||
if minY < bounds.Min.Y {
|
||
minY = bounds.Min.Y
|
||
}
|
||
if maxX > bounds.Max.X {
|
||
maxX = bounds.Max.X
|
||
}
|
||
if maxY > bounds.Max.Y {
|
||
maxY = bounds.Max.Y
|
||
}
|
||
|
||
// 10. 创建裁剪区域
|
||
qrRect := image.Rect(minX, minY, maxX, maxY)
|
||
fmt.Printf("二维码位置: %v\n", qrRect)
|
||
|
||
// 11. 创建新图片并复制二维码区域
|
||
qrImg := image.NewRGBA(qrRect)
|
||
draw.Draw(qrImg, qrImg.Bounds(), img, qrRect.Min, draw.Src)
|
||
|
||
// 12. 保存二维码图片
|
||
outputFile, err := os.Create("image/qrcode.png")
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
defer outputFile.Close()
|
||
|
||
err = png.Encode(outputFile, qrImg)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
return true, result.GetText(), nil
|
||
}
|
||
|
||
// 图像预处理
|
||
func preprocessImage(img image.Image) image.Image {
|
||
// 转为灰度图
|
||
gray := imaging.Grayscale(img)
|
||
|
||
// 增强对比度
|
||
enhanced := imaging.AdjustContrast(gray, 20)
|
||
|
||
// 高斯模糊去噪
|
||
blurred := imaging.Blur(enhanced, 1.0)
|
||
|
||
// 自适应二值化
|
||
binary := adaptiveThreshold(blurred)
|
||
|
||
return binary
|
||
}
|
||
|
||
// 二值化处理
|
||
func adaptiveThreshold(img image.Image) image.Image {
|
||
bounds := img.Bounds()
|
||
dst := image.NewGray(bounds)
|
||
|
||
// 局部自适应阈值
|
||
blockSize := 15
|
||
c := 2.0
|
||
|
||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||
// 计算局部均值
|
||
sum := 0.0
|
||
count := 0
|
||
|
||
for dy := -blockSize / 2; dy <= blockSize/2; dy++ {
|
||
for dx := -blockSize / 2; dx <= blockSize/2; dx++ {
|
||
nx, ny := x+dx, y+dy
|
||
if nx >= bounds.Min.X && nx < bounds.Max.X &&
|
||
ny >= bounds.Min.Y && ny < bounds.Max.Y {
|
||
r, _, _, _ := img.At(nx, ny).RGBA()
|
||
sum += float64(r >> 8)
|
||
count++
|
||
}
|
||
}
|
||
}
|
||
|
||
localMean := sum / float64(count)
|
||
r, _, _, _ := img.At(x, y).RGBA()
|
||
gray := float64(r >> 8)
|
||
|
||
if gray > localMean-c {
|
||
dst.SetGray(x, y, color.Gray{Y: 255})
|
||
} else {
|
||
dst.SetGray(x, y, color.Gray{Y: 0})
|
||
}
|
||
}
|
||
}
|
||
return dst
|
||
}
|
||
|
||
// 分析二维码错误类型
|
||
func analyzeQRCodeError(err error) error {
|
||
if err == nil {
|
||
return nil
|
||
}
|
||
// 检查不同类型的错误
|
||
switch e := err.(type) {
|
||
case gozxing.ChecksumException:
|
||
return fmt.Errorf("二维码校验失败(可能部分损坏或被遮挡): %v", e)
|
||
case gozxing.FormatException:
|
||
return fmt.Errorf("二维码格式错误(可能不是有效的二维码): %v", e)
|
||
case gozxing.NotFoundException:
|
||
return fmt.Errorf("二维码格式错误: %v", e)
|
||
default:
|
||
return fmt.Errorf("解码失败: %v", err)
|
||
}
|
||
}
|
||
|
||
// 生成二维码
|
||
func generateQRCode(content string, width int, height int, fileName string) (string, error) {
|
||
// 创建 QR 码写入器
|
||
writer := qrcode.NewQRCodeWriter()
|
||
|
||
// 设置编码参数
|
||
hints := make(map[gozxing.EncodeHintType]interface{})
|
||
|
||
// 纠错级别设置
|
||
hints[gozxing.EncodeHintType_ERROR_CORRECTION] = "H"
|
||
|
||
// 字符集
|
||
hints[gozxing.EncodeHintType_CHARACTER_SET] = "UTF-8"
|
||
|
||
// 边距
|
||
hints[gozxing.EncodeHintType_MARGIN] = 4
|
||
|
||
// 生成二维码
|
||
bitMatrix, err := writer.Encode(content, gozxing.BarcodeFormat_QR_CODE, width, height, hints)
|
||
if err != nil {
|
||
return "", fmt.Errorf("生成二维码失败: %v", err)
|
||
}
|
||
|
||
// 创建图像
|
||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||
|
||
// 白色背景
|
||
for y := 0; y < height; y++ {
|
||
for x := 0; x < width; x++ {
|
||
img.Set(x, y, color.White)
|
||
}
|
||
}
|
||
|
||
// 黑色二维码点
|
||
for y := 0; y < height; y++ {
|
||
for x := 0; x < width; x++ {
|
||
if bitMatrix.Get(x, y) {
|
||
img.Set(x, y, color.Black)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 保存文件
|
||
file, err := os.Create(fileName)
|
||
if err != nil {
|
||
return "", fmt.Errorf("保存二维码失败: %v", err)
|
||
}
|
||
defer file.Close()
|
||
|
||
png.Encode(file, img)
|
||
return fmt.Sprintf("生成二维码成功: %v", fileName), nil
|
||
}
|
||
|
||
// =================== 辅助函数 =======================
|
||
|
||
// 辅助函数
|
||
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)
|
||
}
|
||
|
||
// ResizeWTToHeightQuality 图片缩放
|
||
//
|
||
//export ResizeWTToHeightQuality
|
||
func ResizeWTToHeightQuality(jsonConfig *C.char, dsWidth, dsHeight C.int) *C.char {
|
||
configStr := C.GoString(jsonConfig)
|
||
dsWidthStr := int(dsWidth)
|
||
dsHeightStr := int(dsHeight)
|
||
var config *Config
|
||
if err := json.Unmarshal([]byte(configStr), &config); err != nil {
|
||
return C.CString(fmt.Sprintf("解析 config 失败: %v", err))
|
||
}
|
||
fileName, err := resizeWTToHeightQuality(config, dsWidthStr, dsHeightStr)
|
||
if err != nil {
|
||
return C.CString(fmt.Sprintf("%v", err))
|
||
}
|
||
return C.CString(fileName)
|
||
}
|
||
|
||
// CropImage 图片裁切
|
||
//
|
||
//export CropImage
|
||
func CropImage(jsonConfig *C.char, x, y, width, height C.int) *C.char {
|
||
configStr := C.GoString(jsonConfig)
|
||
xStr := int(x)
|
||
yStr := int(y)
|
||
widthStr := int(width)
|
||
heightStr := int(height)
|
||
var config *Config
|
||
if err := json.Unmarshal([]byte(configStr), &config); err != nil {
|
||
return C.CString(fmt.Sprintf("解析 config 失败: %v", err))
|
||
}
|
||
fileName, err := cropImage(config, xStr, yStr, widthStr, heightStr)
|
||
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))
|
||
}
|
||
|
||
// main 函数是必需的,即使为空
|
||
//func main() {
|
||
//}
|