- 新增 imglib 包,支持 FilePath/URL/Base64 三种图片输入方式 - 纯白占比检测、白底居中合成、等比缩放、去白边、裁切 - 二维码生成与识别、条形码生成(Code128/EAN13/Code39) - 中文文字图片、书籍信息水印、通用水印叠加 - 输出辅助:EncodeToBytes/EncodeToBase64/SaveToFile/SaveJPEG/SavePNG - 字体缓存,避免重复加载 - 完整测试覆盖(23个测试用例)
1303 lines
35 KiB
Go
1303 lines
35 KiB
Go
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
|
||
}
|