daShangDao_kfzgw-info/image/image.go
97694732@qq.com ac2a39742d 各种修改
2026-06-11 13:21:55 +08:00

2458 lines
65 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
// #include <stdlib.h>
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)
//}
}