package imglib import ( "bytes" "encoding/base64" "encoding/json" "fmt" "image" "image/color" "image/jpeg" "image/png" "math" "os" "path/filepath" "strings" "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" ) // ======================= 配置结构体 ======================= // Config 白色占比检测配置 type Config struct { OutputDir string FileName string MatchDir string UnmatchDir string EqualHeightDir string WhiteDir string WhiteBorderPngDir string WhiteHeightZoomDir string CropDir string MinWhitePct float64 MaxWhitePct float64 Extensions []string } // ======================= 纯白占比检测 ======================= // CalculateWhitePercentage 计算图片中纯白色像素占比 func CalculateWhitePercentage(input ImageInput) (float64, error) { img, _, err := input.Load() if err != nil { return 0, err } return calculateWhitePercentageFromImage(img) } func calculateWhitePercentageFromImage(img image.Image) (float64, error) { bounds := img.Bounds() totalPixels := bounds.Dx() * bounds.Dy() if totalPixels == 0 { return 0, nil } whitePixels := 0 for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for x := bounds.Min.X; x < bounds.Max.X; x++ { r, g, b, _ := img.At(x, y).RGBA() if r == 0xFFFF && g == 0xFFFF && b == 0xFFFF { whitePixels++ } } } return float64(whitePixels) / float64(totalPixels), nil } // ======================= 白底居中合成 ======================= // CreateWhiteBottomCenteredImage 将图片居中放置到指定尺寸的白色背景上 func CreateWhiteBottomCenteredImage(input ImageInput, width, height int) (*image.RGBA, error) { img, _, err := input.Load() if err != nil { return nil, err } dst := image.NewRGBA(image.Rect(0, 0, width, height)) bgColor := color.RGBA{R: 255, G: 255, B: 255, A: 255} draw.Draw(dst, dst.Bounds(), &image.Uniform{C: bgColor}, image.Point{}, draw.Src) srcBounds := img.Bounds() x := (width - srcBounds.Dx()) / 2 y := (height - srcBounds.Dy()) / 2 draw.Draw(dst, image.Rect(x, y, x+srcBounds.Dx(), y+srcBounds.Dy()), img, image.Point{}, draw.Over) return dst, nil } // ======================= 等比例高度缩放 ======================= // ResizeToHeight 按目标高度等比例缩放图片(Lanczos3) func ResizeToHeight(input ImageInput, targetHeight int) (image.Image, error) { img, _, err := input.Load() if err != nil { return nil, err } bounds := img.Bounds() ratio := float64(targetHeight) / float64(bounds.Dy()) targetWidth := uint(float64(bounds.Dx()) * ratio) return resize.Resize(targetWidth, uint(targetHeight), img, resize.Lanczos3), nil } // ======================= 去白边 ======================= // RemoveWhiteBorder 去掉图片白色边框并返回 PNG 格式图片 func RemoveWhiteBorder(input ImageInput) (image.Image, error) { img, _, err := input.Load() if err != nil { return nil, err } converter := newImageToPNGConverter(240, 0, &color.RGBA{R: 255, G: 255, B: 255, A: 255}, nil, false, png.DefaultCompression, 95) return converter.convertToPNG(img, true), nil } // RemoveWhiteBorderWithConfig 带自定义配置的去白边 func RemoveWhiteBorderWithConfig(input ImageInput, threshold, margin int) (image.Image, error) { img, _, err := input.Load() if err != nil { return nil, err } converter := newImageToPNGConverter(threshold, margin, &color.RGBA{R: 255, G: 255, B: 255, A: 255}, nil, false, png.DefaultCompression, 95) return converter.convertToPNG(img, true), nil } // ======================= 自适应尺寸缩放 ======================= // ResizeToDimensions 按目标宽高等比例缩放图片(自实现 Lanczos3) func ResizeToDimensions(input ImageInput, dstWidth, dstHeight int) (*image.RGBA, error) { img, _, err := input.Load() if err != nil { return nil, err } return resizeImageOptimized(img, dstWidth, dstHeight), nil } // ======================= 图片裁切 ======================= // Crop 按指定区域裁切图片 func Crop(input ImageInput, x, y, width, height int) (*image.RGBA, error) { img, _, err := input.Load() if err != nil { return nil, err } return basicCrop(img, x, y, width, height) } func basicCrop(src image.Image, x, y, width, height int) (*image.RGBA, 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 } // ======================= Lanczos3 缩放算法 ======================= func resizeImageOptimized(src image.Image, dstWidth, dstHeight int) *image.RGBA { 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) radius := 3.0 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))) 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 } 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) } func clamp(v float64) uint8 { if v < 0 { return 0 } if v > 255 { return 255 } return uint8(v + 0.5) } // ======================= 去白边辅助 ======================= type ImageToPNGConverter struct { Threshold int Margin int BgColor color.RGBA DetectColor *color.RGBA KeepTransparent bool PNGCompressLevel png.CompressionLevel Quality int } 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, } } func (c *ImageToPNGConverter) convertToPNG(img image.Image, addBackground bool) image.Image { trimmed := c.trimImage(img) _, 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 := image.NewRGBA(trimmed.Bounds()) draw.Draw(rgba, rgba.Bounds(), trimmed, trimmed.Bounds().Min, draw.Src) return rgba } return trimmed } return trimmed } func (c *ImageToPNGConverter) trimImage(img image.Image) image.Image { return imaging.Crop(img, c.findBorders(img)) } func (c *ImageToPNGConverter) findBorders(img image.Image) image.Rectangle { bounds := img.Bounds() width := bounds.Dx() height := bounds.Dy() _, 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) } func (c *ImageToPNGConverter) isBackgroundColor(pixel color.Color, hasAlpha bool) bool { r, g, b, a := pixel.RGBA() r8 := uint8(r >> 8) g8 := uint8(g >> 8) b8 := uint8(b >> 8) a8 := uint8(a >> 8) if hasAlpha && a8 < 25 { return true } if c.DetectColor != nil { dr, dg, db, _ := c.DetectColor.RGBA() dr8 := uint8(dr >> 8) dg8 := uint8(dg >> 8) db8 := uint8(db >> 8) threshold8 := uint8(255 - c.Threshold) return absDiff(r8, dr8) <= threshold8 && absDiff(g8, dg8) <= threshold8 && absDiff(b8, db8) <= threshold8 } threshold8 := uint8(c.Threshold) return r8 >= threshold8 && g8 >= threshold8 && b8 >= threshold8 } func absDiff(a, b uint8) uint8 { if a > b { return a - b } return b - a } // ======================= 二维码识别 ======================= // ScanQRCode 识别图片中的二维码 func ScanQRCode(input ImageInput) (string, error) { img, _, err := input.Load() if err != nil { return "", err } return scanQRCodeFromImage(img) } func scanQRCodeFromImage(img image.Image) (string, error) { bmp, err := gozxing.NewBinaryBitmapFromImage(img) if err != nil { return "", fmt.Errorf("创建位图失败: %v", err) } reader := qrcode.NewQRCodeReader() result, err := reader.Decode(bmp, nil) if err != nil { return "", analyzeQRCodeError(err) } return result.GetText(), nil } 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) } } // ScanQRCodeWithBounds 识别二维码并返回裁剪后的二维码区域图片 func ScanQRCodeWithBounds(input ImageInput) (string, *image.RGBA, error) { img, _, err := input.Load() if err != nil { return "", nil, err } bmp, err := gozxing.NewBinaryBitmapFromImage(img) if err != nil { return "", nil, fmt.Errorf("创建位图失败: %v", err) } reader := qrcode.NewQRCodeReader() result, err := reader.Decode(bmp, nil) if err != nil { return "", nil, analyzeQRCodeError(err) } text := result.GetText() points := result.GetResultPoints() if len(points) < 3 { return text, nil, nil } 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 } } 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 } qrRect := image.Rect(minX, minY, maxX, maxY) qrImg := image.NewRGBA(qrRect) draw.Draw(qrImg, qrImg.Bounds(), img, qrRect.Min, draw.Src) return text, qrImg, nil } // ======================= 二维码生成 ======================= // GenerateQRCode 生成二维码图片 func GenerateQRCode(content string, width, height int) (*image.RGBA, error) { 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 nil, 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++ { if bitMatrix.Get(x, y) { img.Set(x, y, color.Black) } else { img.Set(x, y, color.White) } } } return img, nil } // ======================= 条形码生成 ======================= const ( Code128 = "code128" EAN13 = "ean13" Code39 = "code39" ) // GenerateBarcode 生成条形码图片(底部含文字标签) func GenerateBarcode(barcodeType, content string) (image.Image, error) { switch barcodeType { case Code128: return generateCode128(content) case EAN13: return generateEAN13(content) case Code39: return generateCode39(content) } return nil, fmt.Errorf("不支持的条形码类型: %s", barcodeType) } func generateCode128(content string) (image.Image, error) { code, err := code128.Encode(content) if err != nil { return nil, fmt.Errorf("创建条形码失败: %v", err) } scaledCode, err := barcode.Scale(code, 250, 85) if err != nil { return nil, fmt.Errorf("缩放条形码失败: %v", err) } return addBarcodeLabel(scaledCode, content), nil } func generateEAN13(content string) (image.Image, error) { code, err := ean.Encode(content) if err != nil { return nil, fmt.Errorf("创建条形码失败: %v", err) } scaledCode, err := barcode.Scale(code, 250, 85) if err != nil { return nil, fmt.Errorf("缩放条形码失败: %v", err) } return addBarcodeLabel(scaledCode, content), nil } func generateCode39(content string) (image.Image, error) { code, err := code39.Encode(content, true, true) if err != nil { return nil, fmt.Errorf("创建条形码失败: %v", err) } scaledCode, err := barcode.Scale(code, 250, 85) if err != nil { return nil, fmt.Errorf("缩放条形码失败: %v", err) } return addBarcodeLabel(scaledCode, content), nil } func addBarcodeLabel(barcodeImg image.Image, label string) image.Image { imgWidth := 250 imgHeight := 110 dc := gg.NewContext(imgWidth, imgHeight) dc.SetColor(color.White) dc.Clear() dc.DrawImage(barcodeImg, 0, 10) if err := dc.LoadFontFace("Arial.ttf", 14); err != nil { _ = dc.LoadFontFace("/System/Library/Fonts/Helvetica.ttc", 14) } dc.SetColor(color.Black) textY := 105.0 textWidth, _ := dc.MeasureString(label) textX := (float64(imgWidth) - textWidth) / 2 dc.DrawString(label, textX, textY) return dc.Image() } // ======================= 中文文字图片 ======================= // CreateChineseTextImage 将中文文本渲染为图片,超出部分显示省略号 func CreateChineseTextImage(text string, width, height int, fontSize float64) (*image.RGBA, error) { f, err := GetFont() if err != nil { return nil, 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) } } 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)) availableWidth := width - 100 charsPerLine := int(float64(availableWidth) / fontSize * 1.7) if charsPerLine < 10 { charsPerLine = 10 } 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 { 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 } if lastLineChars+charWidth > charsPerLine-3 { break } truncatedLine.WriteRune(r) lastLineChars += charWidth } displayLines = append(displayLines, truncatedLine.String()+" ...") } 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 } if _, err := c.DrawString(line, pt); err != nil { return nil, fmt.Errorf("绘制文字失败: %v", err) } pt.Y += c.PointToFixed(fontSize * 1.5) } return img, nil } // DrawChineseInfo 在现有 RGBA 图片上绘制书名、作者、出版社信息 func DrawChineseInfo(img *image.RGBA, title, author, publisher string) error { fontObj, err := GetFont() if err != nil { return err } c := freetype.NewContext() c.SetDPI(72) c.SetFont(fontObj) c.SetSrc(image.NewUniform(color.Black)) c.SetClip(img.Bounds()) c.SetDst(img) maxWidth := 400 c.SetFontSize(45) titleLines := wrapText(title, fontObj, 45, maxWidth) if len(titleLines) > 3 { titleLines = titleLines[:3] } titleY := 250 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) } } c.SetFontSize(28) authorLines := wrapText(author, fontObj, 28, maxWidth) if len(authorLines) > 2 { authorLines = authorLines[:2] } authorY := 420 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) } } c.SetFontSize(18) publisherLines := wrapText(publisher, fontObj, 18, maxWidth) if len(publisherLines) > 1 { publisherLines = publisherLines[:1] } publisherY := 700 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) } } return drawBottomRightText(img, fontObj) } func drawBottomRightText(img *image.RGBA, fontObj *truetype.Font) error { line1 := "此为实例图片" line2 := "联系客服获取实图" fontSize := 8.0 rightMargin := 10 bottomMargin := 10 lineHeight := int(fontSize * 1.5) c := freetype.NewContext() c.SetDPI(72) c.SetFont(fontObj) c.SetClip(img.Bounds()) c.SetDst(img) c.SetFontSize(fontSize) 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 strokeRadius := 1.0 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 err } pt1 := freetype.Pt(int(float64(x1)+offset.dx), int(float64(y1)+offset.dy)) if _, err := c.DrawString(line1, pt1); err != nil { return 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 err } pt1 := freetype.Pt(x1, y1) if _, err := c.DrawString(line1, pt1); err != nil { return err } return 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 } 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 } func wrapText(text string, fontObj *truetype.Font, fontSize float64, maxWidth int) []string { var lines []string var currentLine strings.Builder currentLine.Grow(len(text)) var currentWidth int for _, ch := range text { charWidth := calculateStringWidth(string(ch), fontObj, fontSize) if ch == '\n' { if currentLine.Len() > 0 { lines = append(lines, currentLine.String()) currentLine.Reset() } currentWidth = 0 continue } if currentWidth+charWidth > maxWidth && currentLine.Len() > 0 { lines = append(lines, currentLine.String()) currentLine.Reset() currentLine.WriteRune(ch) currentWidth = charWidth } else { currentLine.WriteRune(ch) currentWidth += charWidth } } if currentLine.Len() > 0 { lines = append(lines, currentLine.String()) } return lines } func calculateStringWidth(text string, fontObj *truetype.Font, fontSize float64) int { width := 0 for _, ch := range text { idx := fontObj.Index(ch) advance := fontObj.HMetric(fixed.Int26_6(fontSize), idx).AdvanceWidth width += int(advance) } return width } // ======================= 水印处理 ======================= // WatermarkConfig 水印配置 type WatermarkConfig struct { SourceImage ImageInput // 源图片输入 WatermarkImg ImageInput // 水印图片输入 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轴偏移 YOffset int // Y轴偏移 Timeout int // 下载超时(秒) OutputFormat string // 输出格式: "jpeg", "png", "auto" JPEGQuality int // JPEG质量 (1-100) TargetWidth int // 目标宽度 TargetHeight int // 目标高度 ResizeMode string // 缩放模式: "fit", "fill", "stretch" } // ApplyWatermark 对图片应用水印 func ApplyWatermark(config WatermarkConfig) (*image.RGBA, error) { srcImg, _, err := config.SourceImage.Load() if err != nil { return nil, fmt.Errorf("加载源图片失败: %v", err) } watermarkImg, _, err := config.WatermarkImg.Load() if err != nil { return nil, fmt.Errorf("加载水印图片失败: %v", err) } 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) if err := applyWatermarkToImage(dst, watermarkImg, config); err != nil { return nil, err } return dst, nil } func applyWatermarkToImage(dst *image.RGBA, watermark image.Image, config WatermarkConfig) error { watermark = scaleWatermarkImage(watermark, config.Scale) switch config.Position { case "tile": drawTileWatermark(dst, watermark, config) default: drawSingleWatermark(dst, watermark, config) } return nil } func scaleWatermarkImage(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 } func drawSingleWatermark(dst *image.RGBA, watermark image.Image, config WatermarkConfig) { bounds := dst.Bounds() wb := watermark.Bounds() ww := wb.Dx() wh := wb.Dy() var x, y int switch config.Position { case "center": x = (bounds.Dx()-ww)/2 + config.XOffset y = (bounds.Dy()-wh)/2 + config.YOffset case "top-left": x = config.XOffset y = config.YOffset case "top-right": x = bounds.Dx() - ww - config.XOffset y = config.YOffset case "bottom-left": x = config.XOffset y = bounds.Dy() - wh - config.YOffset case "bottom-right": x = bounds.Dx() - ww - config.XOffset y = bounds.Dy() - wh - config.YOffset default: x = (bounds.Dx()-ww)/2 + config.XOffset y = (bounds.Dy()-wh)/2 + config.YOffset } x = max(0, min(x, bounds.Dx()-ww)) y = max(0, min(y, bounds.Dy()-wh)) drawWatermarkWithOpacity(dst, watermark, x, y, config.Opacity) } func drawTileWatermark(dst *image.RGBA, watermark image.Image, config WatermarkConfig) { bounds := dst.Bounds() wb := watermark.Bounds() ww := wb.Dx() wh := wb.Dy() spacing := config.TileSpacing if spacing < 0 { spacing = 0 } stepX := ww + spacing stepY := wh + 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) } } } func drawWatermarkWithOpacity(dst *image.RGBA, watermark image.Image, x, y int, opacity float64) { if opacity < 0 { opacity = 0 } if opacity > 1 { opacity = 1 } wb := watermark.Bounds() for wy := 0; wy < wb.Dy(); wy++ { for wx := 0; wx < wb.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 { 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}) } } } } // ======================= 输出辅助 ======================= // EncodeToBytes 将图片编码为字节数组 func EncodeToBytes(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 } if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality}); err != nil { return nil, fmt.Errorf("JPEG编码失败: %v", err) } case "png": if err := png.Encode(&buf, img); err != nil { return nil, fmt.Errorf("PNG编码失败: %v", err) } default: if err := png.Encode(&buf, img); err != nil { return nil, fmt.Errorf("PNG编码失败: %v", err) } } return buf.Bytes(), nil } // EncodeToBase64 将图片编码为带前缀的 Base64 字符串 func EncodeToBase64(img image.Image, format string, quality int) (string, error) { data, err := EncodeToBytes(img, format, quality) if err != nil { return "", err } var prefix string switch strings.ToLower(format) { case "jpeg", "jpg": prefix = "data:image/jpeg;base64," case "png": prefix = "data:image/png;base64," default: prefix = "data:image/jpeg;base64," } return prefix + base64.StdEncoding.EncodeToString(data), nil } // SaveToFile 将图片保存到文件 func SaveToFile(img image.Image, path string) error { outFile, err := os.Create(path) if err != nil { return err } defer outFile.Close() ext := strings.ToLower(filepath.Ext(path)) switch ext { case ".jpg", ".jpeg": return jpeg.Encode(outFile, img, &jpeg.Options{Quality: 95}) case ".png": return png.Encode(outFile, img) default: return png.Encode(outFile, img) } } // SaveJPEG 将图片保存为 JPEG 文件 func SaveJPEG(img image.Image, path string, quality int) error { outFile, err := os.Create(path) if err != nil { return err } defer outFile.Close() if quality <= 0 || quality > 100 { quality = 95 } return jpeg.Encode(outFile, img, &jpeg.Options{Quality: quality}) } // SavePNG 将图片保存为 PNG 文件 func SavePNG(img image.Image, path string) error { outFile, err := os.Create(path) if err != nil { return err } defer outFile.Close() return png.Encode(outFile, img) } // ======================= JSON 辅助(兼容原有 C 接口) ======================= // WatermarkConfigJSON 兼容原有 WatermarkConfig JSON 配置 type WatermarkConfigJSON struct { SourceImageURL string `json:"source_image_url"` WatermarkURL string `json:"watermark_url"` WatermarkBase64 string `json:"watermark_base64"` Opacity float64 `json:"opacity"` Position string `json:"position"` TileSpacing int `json:"tile_spacing"` Scale float64 `json:"scale"` Rotation float64 `json:"rotation"` XOffset int `json:"x_offset"` YOffset int `json:"y_offset"` Timeout int `json:"timeout"` OutputFormat string `json:"output_format"` JPEGQuality int `json:"jpeg_quality"` TargetWidth int `json:"target_width"` TargetHeight int `json:"target_height"` ResizeMode string `json:"resize_mode"` } // ApplyWatermarkFromJSON 从 JSON 配置应用水印(兼容原有接口) func ApplyWatermarkFromJSON(jsonStr string) ([]byte, string, error) { var cfg WatermarkConfigJSON if err := json.Unmarshal([]byte(jsonStr), &cfg); err != nil { return nil, "", fmt.Errorf("解析配置失败: %v", err) } wmCfg := WatermarkConfig{ SourceImage: ImageInput{URL: cfg.SourceImageURL}, WatermarkImg: ImageInput{URL: cfg.WatermarkURL, Base64: cfg.WatermarkBase64}, Opacity: cfg.Opacity, Position: cfg.Position, TileSpacing: cfg.TileSpacing, Scale: cfg.Scale, Rotation: cfg.Rotation, XOffset: cfg.XOffset, YOffset: cfg.YOffset, Timeout: cfg.Timeout, OutputFormat: cfg.OutputFormat, JPEGQuality: cfg.JPEGQuality, TargetWidth: cfg.TargetWidth, TargetHeight: cfg.TargetHeight, ResizeMode: cfg.ResizeMode, } dst, err := ApplyWatermark(wmCfg) if err != nil { return nil, "", err } // 按目标尺寸缩放 finalImg := resizeOutputImage(dst, wmCfg) outputFormat := cfg.OutputFormat if outputFormat == "" || outputFormat == "auto" { outputFormat = "jpeg" } imgBytes, err := EncodeToBytes(finalImg, outputFormat, cfg.JPEGQuality) if err != nil { return nil, "", err } return imgBytes, outputFormat, nil } 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": return resize.Resize(uint(targetW), uint(targetH), img, resize.Lanczos3) default: return resize.Resize(uint(targetW), uint(targetH), img, resize.Lanczos3) } } 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 } 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 }