package main // #include import "C" import ( "bytes" "encoding/base64" "encoding/json" "fmt" "github.com/boombuler/barcode" "github.com/boombuler/barcode/code128" "github.com/boombuler/barcode/code39" "github.com/boombuler/barcode/ean" "github.com/disintegration/imaging" "github.com/fogleman/gg" "github.com/golang/freetype" "github.com/golang/freetype/truetype" "github.com/makiuchi-d/gozxing" "github.com/makiuchi-d/gozxing/qrcode" "github.com/nfnt/resize" "golang.org/x/image/draw" "golang.org/x/image/math/fixed" "image" "image/color" "image/jpeg" _ "image/jpeg" "image/png" _ "image/png" "io/ioutil" "math" "os" "path/filepath" "runtime" "strconv" "strings" "time" "unsafe" "github.com/valyala/fasthttp" ) // 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{R: 0, G: 0, B: 0, A: 0} // 白色 // 填充透明背景 draw.Draw(dst, dst.Bounds(), &image.Uniform{C: 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) err = saveImage(destPath, imageNew, format) if err != nil { return "", fmt.Errorf("保存图片失败: %v", err) } 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 } // 保存图片到指定目录下 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 } // 识别二维码 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 simpleAutoWrap(text string, maxCharsPerLine int) ([]string, bool) { var lines []string var currentLine strings.Builder charCount := 0 for _, r := range text { // 换行符处理 if r == '\n' { if currentLine.Len() > 0 { lines = append(lines, currentLine.String()) currentLine.Reset() charCount = 0 } continue } // 计算字符宽度(中文算2个字符,英文算1个) charWidth := 1 if r >= 0x4E00 && r <= 0x9FFF { // 中文范围 charWidth = 2 } else if r >= 0x3000 && r <= 0x303F { // 中文标点 charWidth = 2 } // 检查是否需要换行 if charCount+charWidth > maxCharsPerLine && currentLine.Len() > 0 { lines = append(lines, currentLine.String()) currentLine.Reset() charCount = 0 } currentLine.WriteRune(r) charCount += charWidth } if currentLine.Len() > 0 { lines = append(lines, currentLine.String()) } return lines, false // 返回false表示所有内容都已处理 } // 创建带中文字体的文本图片,支持超出部分显示... // text 文本, width, height 宽度高度, fontSize 文字大小, outputPath 输入路径 func createChineseTextImage(text string, width, height int, fontSize float64, outputPath string) (string, error) { // 获取字体路径 fontPath := getDefaultFontPath() if fontPath == "" { return "", fmt.Errorf("未找到系统字体文件,请手动指定字体路径") } // 读取字体文件 fontBytes, err := ioutil.ReadFile(fontPath) if err != nil { return "", fmt.Errorf("读取字体文件失败: %v", err) } // 解析字体 f, err := truetype.Parse(fontBytes) if err != nil { return "", fmt.Errorf("解析字体失败: %v", err) } // 创建图片 img := image.NewRGBA(image.Rect(0, 0, width, height)) // 白色背景 white := color.RGBA{255, 255, 255, 255} for y := 0; y < height; y++ { for x := 0; x < width; x++ { img.Set(x, y, white) } } // 创建freetype上下文 c := freetype.NewContext() c.SetDPI(72) c.SetFont(f) c.SetFontSize(fontSize) c.SetClip(img.Bounds()) c.SetDst(img) c.SetSrc(image.NewUniform(color.Black)) // 计算可用的文本宽度(左右各留50像素边距) availableWidth := width - 100 // 根据字体大小计算每行大约可以显示多少个字符 charsPerLine := int(float64(availableWidth) / fontSize * 1.7) if charsPerLine < 10 { charsPerLine = 10 } // 计算最大可显示行数 // X坐标:从左侧20像素开始 // Y坐标:从顶部20像素 + 字体高度 lineSpacing := int(c.PointToFixed(fontSize*1.5) >> 6) topMargin := 50 + int(c.PointToFixed(fontSize)>>6) bottomMargin := 50 maxLines := (height - topMargin - bottomMargin) / lineSpacing if maxLines <= 0 { maxLines = 1 } // 使用简单的自动换行,获取所有行 allLines, _ := simpleAutoWrap(text, charsPerLine) // 检查是否有超出图片的内容 hasMore := len(allLines) > maxLines displayLines := allLines if hasMore { // 只显示前 maxLines-1 行,最后一行添加 "..." displayLines = allLines[:maxLines-1] // 获取最后一行文本,并确保能显示 "..." lastLine := allLines[maxLines-1] lastLineChars := 0 var truncatedLine strings.Builder for _, r := range lastLine { // 计算字符宽度 charWidth := 1 if r >= 0x4E00 && r <= 0x9FFF { charWidth = 2 } else if r >= 0x3000 && r <= 0x303F { charWidth = 2 } // 检查是否还能添加字符(留出3个字符给"...") if lastLineChars+charWidth > charsPerLine-3 { break } truncatedLine.WriteRune(r) lastLineChars += charWidth } // 添加 "..." truncatedText := truncatedLine.String() + " ..." displayLines = append(displayLines, truncatedText) } else { // 如果内容不多,直接显示所有行 displayLines = allLines if len(displayLines) > maxLines { displayLines = displayLines[:maxLines] } } // 设置起始位置 startX := 50 startY := topMargin // 绘制每行文字 pt := freetype.Pt(startX, startY) for i, line := range displayLines { // 确保不会超出图片底部 if i*lineSpacing > height-bottomMargin { break } // 绘制文字 _, err = c.DrawString(line, pt) if err != nil { return "", fmt.Errorf("绘制文字失败: %v", err) } // 移动到下一行 pt.Y += c.PointToFixed(fontSize * 1.5) } // 保存图片 file, err := os.Create(outputPath) if err != nil { return "", fmt.Errorf("创建文件失败: %v", err) } defer file.Close() err = png.Encode(file, img) if err != nil { return "", fmt.Errorf("编码并写入失败: %v", err) } return outputPath, nil } // 获取系统字体路径 func getDefaultFontPath() string { switch runtime.GOOS { case "windows": paths := []string{ "C:/Windows/Fonts/simhei.ttf", // 黑体 "C:/Windows/Fonts/simsun.ttc", // 宋体 "C:/Windows/Fonts/msyh.ttc", // 微软雅黑 } for _, path := range paths { if _, err := os.Stat(path); err == nil { return path } } case "darwin": return "/System/Library/Fonts/PingFang.ttc" case "linux": paths := []string{ "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", } for _, path := range paths { if _, err := os.Stat(path); err == nil { return path } } } return "" } // 绘制中文文本(支持自动换行) func drawChineseText(img *image.RGBA, title, author, publisher string) error { // 获取字体路径 fontPath := getDefaultFontPath() if fontPath == "" { return fmt.Errorf("未找到系统字体文件,请手动指定字体路径") } // 读取字体文件 fontBytes, err := ioutil.ReadFile(fontPath) if err != nil { return fmt.Errorf("读取字体文件失败: %v", err) } // 解析字体 fontObj, err := truetype.Parse(fontBytes) if err != nil { return fmt.Errorf("解析字体失败: %v", err) } // 创建freetype上下文 c := freetype.NewContext() c.SetDPI(72) c.SetFont(fontObj) c.SetSrc(image.NewUniform(color.Black)) c.SetClip(img.Bounds()) c.SetDst(img) // 定义绘制区域的宽度 maxWidth := 400 // 可根据需要调整 // 1. 绘制书名(使用较大字体,支持多行) c.SetFontSize(45) titleLines := wrapText(title, fontObj, 45, maxWidth) // 限制书名最多显示3行 if len(titleLines) > 3 { titleLines = titleLines[:3] } titleY := 250 // 起始Y坐标 titleLineHeight := 60 // 行高 for i, line := range titleLines { lineWidth := calculateStringWidth(line, fontObj, 45) pt := freetype.Pt((800-int(lineWidth))/2+30, titleY+i*titleLineHeight) if _, err := c.DrawString(line, pt); err != nil { return fmt.Errorf("绘制书名失败: %v", err) } } // 2. 绘制作者(使用中等字体,支持多行) c.SetFontSize(28) authorLines := wrapText(author, fontObj, 28, maxWidth) // 限制作者最多显示2行 if len(authorLines) > 2 { authorLines = authorLines[:2] } authorY := 420 // 起始Y坐标 authorLineHeight := 40 // 行高 for i, line := range authorLines { lineWidth := calculateStringWidth(line, fontObj, 28) pt := freetype.Pt((800-int(lineWidth))/2+30, authorY+i*authorLineHeight) if _, err := c.DrawString(line, pt); err != nil { return fmt.Errorf("绘制作者失败: %v", err) } } // 3. 绘制出版社(使用中等字体,支持多行) c.SetFontSize(18) publisherLines := wrapText(publisher, fontObj, 18, maxWidth) // 限制出版社最多显示2行 if len(publisherLines) > 1 { publisherLines = publisherLines[:1] } publisherY := 700 // 起始Y坐标 publisherLineHeight := 30 // 行高 for i, line := range publisherLines { lineWidth := calculateStringWidth(line, fontObj, 18) pt := freetype.Pt((800-int(lineWidth))/2+30, publisherY+i*publisherLineHeight) if _, err := c.DrawString(line, pt); err != nil { return fmt.Errorf("绘制出版社失败: %v", err) } } // 4. 绘制右下角文字 err = drawBottomRightText(img, fontObj) if err != nil { return fmt.Errorf("绘制右下角文字失败: %v", err) } return nil } // 绘制右下角文字 func drawBottomRightText(img *image.RGBA, fontObj *truetype.Font) error { // 设置文字内容 line1 := "此为实例图片" line2 := "联系客服获取实图" // 设置字体大小 fontSize := 8.0 // 计算右边界距 rightMargin := 10 // 距离右边界的像素 bottomMargin := 10 // 距离底部的像素 // 计算行高 lineHeight := int(fontSize * 1.5) // 创建freetype上下文 c := freetype.NewContext() c.SetDPI(72) c.SetFont(fontObj) c.SetClip(img.Bounds()) c.SetDst(img) // 计算第二行(最后一行)的位置 line2Width := calculateStringWidth(line2, fontObj, fontSize) x2 := img.Bounds().Dx() - line2Width - rightMargin y2 := img.Bounds().Dy() - bottomMargin // 计算第一行的位置 line1Width := calculateStringWidth(line1, fontObj, fontSize) x1 := img.Bounds().Dx() - line1Width - rightMargin y1 := y2 - lineHeight // 设置字体大小 c.SetFontSize(fontSize) // 方法1:多层绘制实现描边效果 strokeRadius := 1.0 // 描边半径 // 绘制描边(灰色,8个方向) strokeColor := color.RGBA{128, 128, 128, 150} // 灰色 c.SetSrc(image.NewUniform(strokeColor)) // 为第二行绘制描边 offsets := []struct{ dx, dy float64 }{ {-strokeRadius, -strokeRadius}, {0, -strokeRadius}, {strokeRadius, -strokeRadius}, {-strokeRadius, 0}, {strokeRadius, 0}, {-strokeRadius, strokeRadius}, {0, strokeRadius}, {strokeRadius, strokeRadius}, } for _, offset := range offsets { pt2 := freetype.Pt(int(float64(x2)+offset.dx), int(float64(y2)+offset.dy)) if _, err := c.DrawString(line2, pt2); err != nil { return fmt.Errorf("绘制第二行描边失败: %v", err) } pt1 := freetype.Pt(int(float64(x1)+offset.dx), int(float64(y1)+offset.dy)) if _, err := c.DrawString(line1, pt1); err != nil { return fmt.Errorf("绘制第一行描边失败: %v", err) } } // 绘制白色文字(覆盖在中间) textColor := color.RGBA{255, 255, 255, 255} // 白色 c.SetSrc(image.NewUniform(textColor)) // 绘制第二行文字 pt2 := freetype.Pt(x2, y2) if _, err := c.DrawString(line2, pt2); err != nil { return fmt.Errorf("绘制第二行文字失败: %v", err) } // 绘制第一行文字 pt1 := freetype.Pt(x1, y1) if _, err := c.DrawString(line1, pt1); err != nil { return fmt.Errorf("绘制第一行文字失败: %v", err) } return nil } // 文本换行函数 func wrapText(text string, fontObj *truetype.Font, fontSize float64, maxWidth int) []string { var lines []string var currentLine string var currentWidth int for _, ch := range text { char := string(ch) charWidth := calculateStringWidth(char, fontObj, fontSize) // 如果当前字符是换行符,直接换行 if char == "\n" { if currentLine != "" { lines = append(lines, currentLine) } currentLine = "" currentWidth = 0 continue } // 如果加上当前字符会超出宽度,开始新行 if currentWidth+charWidth > maxWidth && currentLine != "" { lines = append(lines, currentLine) currentLine = char currentWidth = charWidth } else { currentLine += char currentWidth += charWidth } } // 添加最后一行 if currentLine != "" { lines = append(lines, currentLine) } return lines } // 计算字符串宽度 func calculateStringWidth(text string, fontObj *truetype.Font, fontSize float64) int { width := 0 for _, ch := range text { idx := fontObj.Index(ch) horizAdvance := fontObj.HMetric(fixed.Int26_6(fontSize), idx).AdvanceWidth width += int(horizAdvance) } return width } // 加载PNG图片 func loadPNG(filename string) (*image.RGBA, error) { file, err := os.Open(filename) if err != nil { return nil, err } defer file.Close() img, err := png.Decode(file) if err != nil { return nil, err } // 转换为RGBA rgba := image.NewRGBA(img.Bounds()) for y := 0; y < img.Bounds().Dy(); y++ { for x := 0; x < img.Bounds().Dx(); x++ { rgba.Set(x, y, img.At(x, y)) } } return rgba, nil } // 保存为PNG文件 func savePNG(img image.Image, filename string) error { file, err := os.Create(filename) if err != nil { return err } defer file.Close() return png.Encode(file, img) } const ( CODE128 = "code128" EAN13 = "ean13" CODE39 = "code39" ) // 根据类型生成条形码 func generateBarcode(barcodeType, content, filename string) (string, error) { switch barcodeType { case CODE128: return generateCode128(content, filename) case EAN13: return generateEAN13(content, filename) case CODE39: return generateCode39(content, filename) } return "", fmt.Errorf("条形码类型不存在: %s", barcodeType) } // 生成Code128条形码 func generateCode128(content, filename string) (string, error) { // 创建条形码 code, err := code128.Encode(content) if err != nil { return "", fmt.Errorf("创建条形码失败: %v", err) } // 缩放条形码尺寸 scaledCode, err := barcode.Scale(code, 250, 85) if err != nil { return "", fmt.Errorf("缩放条形码尺寸失败: %v", err) } // 创建带文字的图像 imgWidth := 250 imgHeight := 110 // 条形码高度 + 文字区域高度 dc := gg.NewContext(imgWidth, imgHeight) // 设置白色背景 dc.SetColor(color.White) dc.Clear() dc.DrawImage(scaledCode, 0, 10) fontPath := getDefaultFontPath() if fontPath == "" { return "", fmt.Errorf("未找到系统字体文件,请手动指定字体路径") } // 设置文字属性 if err := dc.LoadFontFace("/System/Library/Fonts/Helvetica.ttc", 14); err != nil { // 如果字体文件不存在,使用默认字体 dc.LoadFontFace("Arial.ttf", 14) } dc.SetColor(color.Black) // 在条形码下方居中绘制文字0 textY := float64(85 + 20) // 条形码高度100 + 20像素间距 textWidth, _ := dc.MeasureString(content) textX := (float64(imgWidth) - textWidth) / 2 dc.DrawString(content, textX, textY) // 保存图像 err = dc.SavePNG(filename) if err != nil { return "", fmt.Errorf("保存图像失败: %v", err) } return filename, nil } // 生成EAN13条形码 func generateEAN13(content, filename string) (string, error) { eanCode, err := ean.Encode(content) if err != nil { return "", fmt.Errorf("创建条形码失败: %v", err) } scaledEanCode, err := barcode.Scale(eanCode, 250, 85) if err != nil { return "", fmt.Errorf("缩放条形码尺寸失败: %v", err) } // 创建带文字的图像 imgWidth := 250 imgHeight := 110 // 条形码高度 + 文字区域高度 dc := gg.NewContext(imgWidth, imgHeight) // 设置白色背景 dc.SetColor(color.White) dc.Clear() dc.DrawImage(scaledEanCode, 0, 10) fontPath := getDefaultFontPath() if fontPath == "" { return "", fmt.Errorf("未找到系统字体文件,请手动指定字体路径") } // 设置文字属性 if err := dc.LoadFontFace("/System/Library/Fonts/Helvetica.ttc", 14); err != nil { // 如果字体文件不存在,使用默认字体 dc.LoadFontFace("Arial.ttf", 14) } dc.SetColor(color.Black) // 在条形码下方居中绘制文字0 textY := float64(85 + 20) // 条形码高度100 + 20像素间距 textWidth, _ := dc.MeasureString(content) textX := (float64(imgWidth) - textWidth) / 2 dc.DrawString(content, textX, textY) // 保存图像 err = dc.SavePNG(filename) if err != nil { return "", fmt.Errorf("保存图像失败: %v", err) } return filename, nil } // 生成Code39 func generateCode39(content, filename string) (string, error) { code, err := code39.Encode(content, true, true) if err != nil { return "", fmt.Errorf("创建条形码失败: %v", err) } scaled, err := barcode.Scale(code, 250, 85) if err != nil { return "", fmt.Errorf("缩放条形码尺寸失败: %v", err) } // 创建带文字的图像 imgWidth := 250 imgHeight := 110 // 条形码高度 + 文字区域高度 dc := gg.NewContext(imgWidth, imgHeight) // 设置白色背景 dc.SetColor(color.White) dc.Clear() dc.DrawImage(scaled, 0, 10) fontPath := getDefaultFontPath() if fontPath == "" { return "", fmt.Errorf("未找到系统字体文件,请手动指定字体路径") } // 设置文字属性 if err := dc.LoadFontFace("/System/Library/Fonts/Helvetica.ttc", 14); err != nil { // 如果字体文件不存在,使用默认字体 dc.LoadFontFace("Arial.ttf", 14) } dc.SetColor(color.Black) // 在条形码下方居中绘制文字0 textY := float64(85 + 20) // 条形码高度100 + 20像素间距 textWidth, _ := dc.MeasureString(content) textX := (float64(imgWidth) - textWidth) / 2 dc.DrawString(content, textX, textY) // 保存图像 err = dc.SavePNG(filename) if err != nil { return "", fmt.Errorf("保存图像失败: %v", err) } return filename, nil } // =================== 辅助函数 ======================= // 辅助函数 func absDiff(a, b uint8) uint8 { if a > b { return a - b } return b - a } // =========================== 添加水印 =============================== // WatermarkConfig 水印配置结构体 type WatermarkConfig struct { SourceImageURL string // 源图片URL地址 WatermarkURL string // 水印图片URL地址 WatermarkBase64 string // 水印图片base64编码字符串(新增,优先使用) Opacity float64 // 不透明度 (0.0-1.0) Position string // 位置: center, top-left, top-right, bottom-left, bottom-right, tile TileSpacing int // 平铺时的间距 Scale float64 // 水印缩放比例 (0.0-1.0) Rotation float64 // 旋转角度 (度数) XOffset int // X轴偏移量AddWatermarkFromURLEx YOffset int // Y轴偏移量 Timeout int // 下载超时时间(秒),默认30秒 OutputFormat string // 输出格式: "jpeg", "png", "auto"(默认auto,根据源图片格式) JPEGQuality int // JPEG质量 (1-100),默认95 TargetWidth int // 目标宽度(0表示不缩放) TargetHeight int // 目标高度(0表示不缩放) ResizeMode string // 缩放模式: "fit"(适应,保持比例,可能有黑边), "fill"(填充,裁剪), "stretch"(拉伸) } // 将客户端声明为全局变量或缓存 var httpClient = &fasthttp.Client{ MaxConnsPerHost: 100, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, } // loadImageFromURL 直接从URL加载图片 func loadImageFromURL(url string, timeout int) (image.Image, string, error) { // 设置默认超时 if timeout <= 0 { timeout = 120 } // 创建请求和响应对象 req := fasthttp.AcquireRequest() resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseRequest(req) defer fasthttp.ReleaseResponse(resp) // 设置请求URL和方法 req.SetRequestURI(url) req.Header.SetMethod("GET") // 设置请求头 req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6") req.Header.Set("Cache-Control", "max-age=0") req.Header.Set("If-Modified-Since", "Mon, 09 Mar 2026 07:43:59 GMT") req.Header.Set("Priority", "u=0, i") req.Header.Set("Sec-Ch-Ua", `"Not:A-Brand";v="99", "Microsoft Edge";v="145", "Chromium";v="145"`) req.Header.Set("Sec-Ch-Ua-Mobile", "?0") req.Header.Set("Sec-Ch-Ua-Platform", "Windows") req.Header.Set("Sec-Fetch-Dest", "document") req.Header.Set("Sec-Fetch-Mode", "navigate") req.Header.Set("Sec-Fetch-Site", "none") req.Header.Set("Sec-Fetch-User", "?1") req.Header.Set("Upgrade-Insecure-Requests", "1") req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0") // 发送请求 err := httpClient.Do(req, resp) if err != nil { return nil, "", fmt.Errorf("请求失败: %v\n", err) } // 复制body数据,避免引用问题 body := make([]byte, len(resp.Body())) copy(body, resp.Body()) // 直接从响应体解码图片 img, format, err := image.Decode(bytes.NewReader(body)) // 主动释放body内存 body = nil if err != nil { return nil, "", fmt.Errorf("解码图片失败: %v", err) } return img, format, nil } // 添加新的辅助函数:从base64字符串加载图片 func loadImageFromBase64(base64Str string) (image.Image, string, error) { // 移除可能的 data:image/xxx;base64, 前缀 base64Str = strings.TrimPrefix(base64Str, "data:image/jpeg;base64,") base64Str = strings.TrimPrefix(base64Str, "data:image/jpg;base64,") base64Str = strings.TrimPrefix(base64Str, "data:image/png;base64,") base64Str = strings.TrimPrefix(base64Str, "data:image/gif;base64,") base64Str = strings.TrimPrefix(base64Str, "data:image/webp;base64,") // 解码base64 imgData, err := base64.StdEncoding.DecodeString(base64Str) if err != nil { return nil, "", fmt.Errorf("解码base64失败: %v", err) } // 检测图片格式 format := detectImageFormat(imgData) // 解码图片 img, _, err := image.Decode(bytes.NewReader(imgData)) // 主动释放数据内存 imgData = nil if err != nil { return nil, "", fmt.Errorf("解码base64图片失败: %v", err) } return img, format, nil } // 检测图片格式的辅助函数 func detectImageFormat(data []byte) string { if len(data) < 12 { return "unknown" } // PNG: 137 80 78 71 13 10 26 10 if len(data) >= 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 { return "png" } // JPEG: 255 216 255 if len(data) >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF { return "jpg" } // GIF: 71 73 70 if len(data) >= 3 && data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46 { return "gif" } // WEBP: 82 73 70 70 if len(data) >= 12 && data[0] == 0x52 && data[1] == 0x49 && data[2] == 0x46 && data[3] == 0x46 { return "webp" } return "unknown" } // 判断HTTP状态码是否值得重试 func isRetryableStatus(statusCode int) bool { // 5xx服务器错误或429限流可以重试 return statusCode >= 500 || statusCode == 429 } // AddWatermarkFromURL 从URL加载图片添加水印并返回字节集 func AddWatermarkFromURL(config WatermarkConfig) ([]byte, string, error) { // 确保函数退出时释放内存 defer runtime.GC() // 可选,在低内存场景下使用 // 加载源图片 srcImg, srcFormat, err := loadImageFromURL(config.SourceImageURL, config.Timeout) if err != nil { return nil, "", fmt.Errorf("加载源图片失败: %v", err) } // 等比缩放原图到 TargetWidth x TargetHeight 范围内 if config.TargetWidth > 0 || config.TargetHeight > 0 { srcW := srcImg.Bounds().Dx() srcH := srcImg.Bounds().Dy() var newW, newH uint if config.TargetWidth > 0 && config.TargetHeight > 0 { scaleX := float64(config.TargetWidth) / float64(srcW) scaleY := float64(config.TargetHeight) / float64(srcH) scale := math.Min(scaleX, scaleY) newW = uint(float64(srcW) * scale) newH = uint(float64(srcH) * scale) } else if config.TargetWidth > 0 { newW = uint(config.TargetWidth) newH = uint(float64(srcH) * float64(config.TargetWidth) / float64(srcW)) } else { newH = uint(config.TargetHeight) newW = uint(float64(srcW) * float64(config.TargetHeight) / float64(srcH)) } srcImg = resize.Resize(newW, newH, srcImg, resize.Lanczos3) } // 加载水印图片(支持URL或base64) var watermarkImg image.Image //var watermarkFormat string // 没用了 // 优先使用 base64 if config.WatermarkBase64 != "" { watermarkImg, _, err = loadImageFromBase64(config.WatermarkBase64) if err != nil { return nil, "", fmt.Errorf("从base64加载水印图片失败: %v", err) } } else if config.WatermarkURL != "" { // 检查是否为base64格式的URL(以data:image开头) if strings.HasPrefix(config.WatermarkURL, "data:image/") { watermarkImg, _, err = loadImageFromBase64(config.WatermarkURL) if err != nil { return nil, "", fmt.Errorf("从base64 URL加载水印图片失败: %v", err) } } else { // 普通URL watermarkImg, _, err = loadImageFromURL(config.WatermarkURL, config.Timeout) if err != nil { return nil, "", fmt.Errorf("从URL加载水印图片失败: %v", err) } } } else { return nil, "", fmt.Errorf("必须提供水印图片(WatermarkURL或WatermarkBase64)") } // 创建目标图片(RGBA格式以支持透明) // 画布尺寸 = 水印尺寸 watermarkBounds := watermarkImg.Bounds() dst := image.NewRGBA(watermarkBounds) // 先绘制源图片(居中) srcBounds := srcImg.Bounds() x := (watermarkBounds.Dx() - srcBounds.Dx()) / 2 y := (watermarkBounds.Dy() - srcBounds.Dy()) / 2 draw.Draw(dst, image.Rect(x, y, x+srcBounds.Dx(), y+srcBounds.Dy()), srcImg, image.Point{}, draw.Src) // 处理水印 err = applyWatermark(dst, watermarkImg, config) if err != nil { return nil, "", fmt.Errorf("应用水印失败: %v", err) } // 按目标尺寸缩放 finalImg := resizeOutputImage(dst, config) // 确定输出格式 outputFormat := config.OutputFormat if outputFormat == "" || outputFormat == "auto" { outputFormat = srcFormat } // 将图片编码为字节集 imgBytes, err := encodeImageToBytes(finalImg, outputFormat, config.JPEGQuality) if err != nil { return nil, "", fmt.Errorf("编码图片失败: %v", err) } return imgBytes, outputFormat, nil } // encodeImageToBytes 将图片编码为字节集 func encodeImageToBytes(img image.Image, format string, quality int) ([]byte, error) { var buf bytes.Buffer switch strings.ToLower(format) { case "jpeg", "jpg": // 设置默认质量 if quality <= 0 || quality > 100 { quality = 95 } err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality}) if err != nil { return nil, fmt.Errorf("JPEG编码失败: %v", err) } case "png": err := png.Encode(&buf, img) if err != nil { return nil, fmt.Errorf("PNG编码失败: %v", err) } default: // 默认使用PNG err := png.Encode(&buf, img) if err != nil { return nil, fmt.Errorf("PNG编码失败: %v", err) } } return buf.Bytes(), nil } // applyWatermark 应用水印到图片 func applyWatermark(dst *image.RGBA, watermark image.Image, config WatermarkConfig) error { // 缩放水印 watermark = scaleWatermark(watermark, config.Scale) // 根据位置绘制水印 switch config.Position { case "tile": drawTileWatermark(dst, watermark, config) default: drawSingleWatermark(dst, watermark, config) } return nil } // scaleWatermark 缩放水印 func scaleWatermark(watermark image.Image, scale float64) image.Image { if scale <= 0 || scale >= 1 { return watermark } bounds := watermark.Bounds() newWidth := uint(float64(bounds.Dx()) * scale) newHeight := uint(float64(bounds.Dy()) * scale) if newWidth > 0 && newHeight > 0 { return resize.Resize(newWidth, newHeight, watermark, resize.Lanczos3) } return watermark } // drawSingleWatermark 绘制单个水印 func drawSingleWatermark(dst *image.RGBA, watermark image.Image, config WatermarkConfig) { bounds := dst.Bounds() watermarkBounds := watermark.Bounds() watermarkWidth := watermarkBounds.Dx() watermarkHeight := watermarkBounds.Dy() // 计算位置 var x, y int switch config.Position { case "center": x = (bounds.Dx()-watermarkWidth)/2 + config.XOffset y = (bounds.Dy()-watermarkHeight)/2 + config.YOffset case "top-left": x = config.XOffset y = config.YOffset case "top-right": x = bounds.Dx() - watermarkWidth - config.XOffset y = config.YOffset case "bottom-left": x = config.XOffset y = bounds.Dy() - watermarkHeight - config.YOffset case "bottom-right": x = bounds.Dx() - watermarkWidth - config.XOffset y = bounds.Dy() - watermarkHeight - config.YOffset default: // 默认居中 x = (bounds.Dx()-watermarkWidth)/2 + config.XOffset y = (bounds.Dy()-watermarkHeight)/2 + config.YOffset } // 确保不超出边界 x = max(0, min(x, bounds.Dx()-watermarkWidth)) y = max(0, min(y, bounds.Dy()-watermarkHeight)) // 绘制水印 drawWatermarkWithOpacity(dst, watermark, x, y, config.Opacity) } // drawTileWatermark 平铺水印 func drawTileWatermark(dst *image.RGBA, watermark image.Image, config WatermarkConfig) { bounds := dst.Bounds() watermarkBounds := watermark.Bounds() watermarkWidth := watermarkBounds.Dx() watermarkHeight := watermarkBounds.Dy() spacing := config.TileSpacing if spacing < 0 { spacing = 0 } stepX := watermarkWidth + spacing stepY := watermarkHeight + spacing // 计算起始偏移,使水印均匀分布 startX := (bounds.Dx() % stepX) / 2 startY := (bounds.Dy() % stepY) / 2 for y := startY; y < bounds.Dy(); y += stepY { for x := startX; x < bounds.Dx(); x += stepX { drawWatermarkWithOpacity(dst, watermark, x, y, config.Opacity) } } } // drawWatermarkWithOpacity 绘制带透明度的水印 func drawWatermarkWithOpacity(dst *image.RGBA, watermark image.Image, x, y int, opacity float64) { if opacity < 0 { opacity = 0 } if opacity > 1 { opacity = 1 } watermarkBounds := watermark.Bounds() for wy := 0; wy < watermarkBounds.Dy(); wy++ { for wx := 0; wx < watermarkBounds.Dx(); wx++ { // 计算目标位置 dx := x + wx dy := y + wy // 确保在目标图片范围内 if dx < 0 || dx >= dst.Bounds().Dx() || dy < 0 || dy >= dst.Bounds().Dy() { continue } // 获取水印像素 wColor := watermark.At(wx, wy) wr, wg, wb, wa := wColor.RGBA() // 如果有透明度,考虑水印本身的透明度 if wa > 0 { // 转换为8位 wr8 := uint8(wr >> 8) wg8 := uint8(wg >> 8) wb8 := uint8(wb >> 8) wa8 := uint8(wa >> 8) // 获取目标像素 dstColor := dst.At(dx, dy) dr, dg, db, _ := dstColor.RGBA() dr8 := uint8(dr >> 8) dg8 := uint8(dg >> 8) db8 := uint8(db >> 8) // 混合颜色(考虑水印透明度和设置的不透明度) alpha := float64(wa8) / 255.0 * opacity r := uint8(float64(dr8)*(1-alpha) + float64(wr8)*alpha) g := uint8(float64(dg8)*(1-alpha) + float64(wg8)*alpha) b := uint8(float64(db8)*(1-alpha) + float64(wb8)*alpha) dst.Set(dx, dy, color.RGBA{r, g, b, 255}) } } } } // resizeOutputImage 根据配置缩放输出图片 func resizeOutputImage(img image.Image, config WatermarkConfig) image.Image { if config.TargetWidth <= 0 && config.TargetHeight <= 0 { return img } srcBounds := img.Bounds() srcW := srcBounds.Dx() srcH := srcBounds.Dy() // 如果只设了一个维度,另一个按比例算 targetW := config.TargetWidth targetH := config.TargetHeight if targetW <= 0 { targetW = int(float64(targetH) * float64(srcW) / float64(srcH)) } if targetH <= 0 { targetH = int(float64(targetW) * float64(srcH) / float64(srcW)) } switch config.ResizeMode { case "fit": return resizeFit(img, srcW, srcH, targetW, targetH) case "fill": return resizeFill(img, srcW, srcH, targetW, targetH) case "stretch": fallthrough default: return resize.Resize(uint(targetW), uint(targetH), img, resize.Lanczos3) } } // resizeFit 等比缩放适应,多出部分填白 func resizeFit(img image.Image, srcW, srcH, targetW, targetH int) image.Image { scaleX := float64(targetW) / float64(srcW) scaleY := float64(targetH) / float64(srcH) scale := math.Min(scaleX, scaleY) newW := int(float64(srcW) * scale) newH := int(float64(srcH) * scale) scaled := resize.Resize(uint(newW), uint(newH), img, resize.Lanczos3) // 居中放到白色画布上 dst := image.NewRGBA(image.Rect(0, 0, targetW, targetH)) draw.Draw(dst, dst.Bounds(), &image.Uniform{C: color.White}, image.Point{}, draw.Src) x := (targetW - newW) / 2 y := (targetH - newH) / 2 draw.Draw(dst, image.Rect(x, y, x+newW, y+newH), scaled, image.Point{}, draw.Over) return dst } // resizeFill 等比缩放覆盖,溢出部分居中裁剪 func resizeFill(img image.Image, srcW, srcH, targetW, targetH int) image.Image { scaleX := float64(targetW) / float64(srcW) scaleY := float64(targetH) / float64(srcH) scale := math.Max(scaleX, scaleY) newW := int(float64(srcW) * scale) newH := int(float64(srcH) * scale) scaled := resize.Resize(uint(newW), uint(newH), img, resize.Lanczos3) // 居中裁剪 cropX := (newW - targetW) / 2 cropY := (newH - targetH) / 2 dst := image.NewRGBA(image.Rect(0, 0, targetW, targetH)) draw.Draw(dst, dst.Bounds(), scaled, image.Point{X: cropX, Y: cropY}, draw.Src) return dst } // =================== 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) xInt := int(x) yInt := int(y) 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 := cropImage(config, xInt, yInt, widthInt, heightInt) if err != nil { return C.CString(fmt.Sprintf("%v", err)) } return C.CString(fileName) } // CreateChineseTextImage 创建带中文字体的文本图片,支持超出部分显示... // //export CreateChineseTextImage func CreateChineseTextImage(text *C.char, width, height C.int, fontSize *C.char, outputPath *C.char) *C.char { textStr := C.GoString(text) widthInt := int(width) heightInt := int(height) fontSizeStr := C.GoString(fontSize) float, err := strconv.ParseFloat(fontSizeStr, 64) if err != nil { return C.CString(fmt.Sprintf("转换float64类型失败: %v", err)) } outputPathStr := C.GoString(outputPath) textImage, err := createChineseTextImage(textStr, widthInt, heightInt, float, outputPathStr) if err != nil { return C.CString(fmt.Sprintf("%v", err)) } return C.CString(textImage) } // DrawChineseInfo 绘制书名,作者,出版社信息 // //export DrawChineseInfo func DrawChineseInfo(filePath, title, author, publisher, outputPath *C.char) *C.char { filePathStr := C.GoString(filePath) titleStr := C.GoString(title) authorStr := C.GoString(author) publishertr := C.GoString(publisher) outputPathStr := C.GoString(outputPath) img, err := loadPNG(filePathStr) if err != nil { return C.CString(fmt.Sprintf("%v", err)) } err = drawChineseText(img, titleStr, authorStr, publishertr) if err != nil { return C.CString(fmt.Sprintf("%v", err)) } err = savePNG(img, outputPathStr) if err != nil { return C.CString(fmt.Sprintf("%v", err)) } return C.CString(fmt.Sprintf("图片保存成功,路径: %s", outputPath)) } // GenerateBarcode 根据类型生成条形码 // //export GenerateBarcode func GenerateBarcode(barcodeType, content, filename *C.char) *C.char { barcodeTypeStr := C.GoString(barcodeType) contentStr := C.GoString(content) filenameStr := C.GoString(filename) barcodeFilename, err := generateBarcode(barcodeTypeStr, contentStr, filenameStr) if err != nil { return C.CString(fmt.Sprintf("%v", err)) } return C.CString(barcodeFilename) } // AddWatermarkFromURLEx 从URL添加水印并返回字节集(C导出函数) // //export AddWatermarkFromURLEx func AddWatermarkFromURLEx(jsonConfig *C.char) *C.char { configStr := C.GoString(jsonConfig) var config WatermarkConfig if err := json.Unmarshal([]byte(configStr), &config); err != nil { return C.CString(fmt.Sprintf("ERROR:解析水印配置失败: %v", err)) } imgBytes, format, err := AddWatermarkFromURL(config) if err != nil { return C.CString(fmt.Sprintf("ERROR:%v", err)) } // 构建带MIME前缀的Base64数据 var base64Data string switch format { case "jpeg", "jpg": base64Data = "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(imgBytes) case "png": base64Data = "data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes) case "gif": base64Data = "data:image/gif;base64," + base64.StdEncoding.EncodeToString(imgBytes) default: base64Data = "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(imgBytes) } // 构建返回结果:格式 + 字节集大小 + 字节集数据 // 使用Base64编码字节集以便在JSON中传输 result := struct { Success bool `json:"success"` Format string `json:"format"` Data string `json:"data"` // Base64编码的图片数据 Size int `json:"size"` }{ Success: true, Format: format, Data: base64Data, Size: len(imgBytes), } resultBytes, _ := json.Marshal(result) return C.CString(string(resultBytes)) } // FreeCString 导出函数:释放C字符串内存 // //export FreeCString func FreeCString(str *C.char) { C.free(unsafe.Pointer(str)) } // main 函数是必需的,即使为空 func main() { //// 图片URL //surl := "https://img.pddpic.com/open-gw/2026-05-13/2f79386387d1b72f4081a7b661a9849a.png" // //// 1. 下载图片 //resp, err := http.Get(surl) //if err != nil { // panic(err) //} //defer resp.Body.Close() // //// 2. 读取图片二进制数据 //imgData, err := io.ReadAll(resp.Body) //if err != nil { // panic(err) //} // //// 3. 转换为Base64字符串 //base64Str := base64.StdEncoding.EncodeToString(imgData) // //watermarkConfig := WatermarkConfig{ // SourceImageURL: "http://booklibimg.kfzimg.com/data/book_lib_img_v2/isbn/1/b044/b044aae81122166798a30699e0f007d2_0_1_300_300.jpg", // WatermarkBase64: base64Str, // Position: "center", // Opacity: 1.0, // Scale: 1.0, // TileSpacing: 50, // Timeout: 30, // OutputFormat: "jpeg", // JPEGQuality: 95, // TargetWidth: 800, // TargetHeight: 800, // ResizeMode: "fit", //} //url, s, err := AddWatermarkFromURL(watermarkConfig) //if err != nil { // fmt.Println(err) //} else { // fmt.Println("url:", url, "s:", s) //} }