186 lines
4.6 KiB
Go
186 lines
4.6 KiB
Go
package service
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/base64"
|
||
"fmt"
|
||
"image"
|
||
"image/color"
|
||
"image/draw"
|
||
"image/png"
|
||
"os"
|
||
systemRes "psi/models/response"
|
||
"strings"
|
||
|
||
"github.com/boombuler/barcode"
|
||
"github.com/boombuler/barcode/code128"
|
||
"golang.org/x/image/font"
|
||
"golang.org/x/image/font/opentype"
|
||
"golang.org/x/image/math/fixed"
|
||
)
|
||
|
||
type BarcodeService struct{}
|
||
|
||
// Config 条形码配置
|
||
type Config struct {
|
||
BarcodeHeight int
|
||
ModuleWidth int
|
||
Padding int
|
||
FontPath string
|
||
FontSize float64
|
||
BgColor color.Color
|
||
BarColor color.Color
|
||
}
|
||
|
||
// GenerateBarcode 根据货号生成条形码图片(Base64)
|
||
func (s *BarcodeService) GenerateBarcode(content string) (systemRes.BarcodeResponse, error) {
|
||
// 配置参数
|
||
config := &Config{
|
||
BarcodeHeight: 120,
|
||
ModuleWidth: 2,
|
||
Padding: 20,
|
||
FontPath: "fonts/youaimoshouheiti-regular.ttf",
|
||
FontSize: 50,
|
||
BgColor: color.White,
|
||
BarColor: color.Black,
|
||
}
|
||
|
||
// 若内容包含isbn,则使用较小字号且不加粗
|
||
if strings.Contains(strings.ToLower(content), "9787") {
|
||
config.FontSize = 35
|
||
}
|
||
|
||
// 生成条形码图片的Base64
|
||
base64Image, err := generateBase64Barcode(content, config)
|
||
if err != nil {
|
||
return systemRes.BarcodeResponse{}, fmt.Errorf("生成条形码失败: %w", err)
|
||
}
|
||
|
||
return systemRes.BarcodeResponse{
|
||
ImageBase64: base64Image,
|
||
Content: content,
|
||
}, nil
|
||
}
|
||
|
||
func generateBase64Barcode(content string, config *Config) (string, error) {
|
||
// 1. 生成条形码图像
|
||
barcodeImg, err := generateBarcodeImage(content, config)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
barcodeBounds := barcodeImg.Bounds()
|
||
barcodeWidth := barcodeBounds.Dx()
|
||
barcodeHeight := barcodeBounds.Dy()
|
||
|
||
// 2. 加载字体
|
||
face, err := loadFont(config)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer face.Close()
|
||
|
||
// 3. 获取文字度量信息
|
||
metrics := face.Metrics()
|
||
ascent := metrics.Ascent.Ceil()
|
||
descent := metrics.Descent.Ceil()
|
||
textHeight := ascent + descent
|
||
|
||
// 计算文字宽度
|
||
drawer := &font.Drawer{Face: face}
|
||
advance := drawer.MeasureString(content)
|
||
textWidth := advance.Ceil()
|
||
|
||
// 4. 计算最终图片尺寸
|
||
gapBetween := 12
|
||
textBottomPadding := 12
|
||
|
||
maxContentWidth := barcodeWidth
|
||
if textWidth > maxContentWidth {
|
||
maxContentWidth = textWidth
|
||
}
|
||
finalWidth := maxContentWidth + config.Padding*2
|
||
finalHeight := barcodeHeight + gapBetween + textHeight + config.Padding*2 + textBottomPadding
|
||
|
||
// 5. 创建最终图片
|
||
finalImg := image.NewRGBA(image.Rect(0, 0, finalWidth, finalHeight))
|
||
draw.Draw(finalImg, finalImg.Bounds(), &image.Uniform{config.BgColor}, image.Point{}, draw.Src)
|
||
|
||
// 6. 绘制条形码(居中)
|
||
barcodeStartX := (finalWidth - barcodeWidth) / 2
|
||
barcodeStartY := config.Padding
|
||
draw.Draw(finalImg,
|
||
image.Rect(barcodeStartX, barcodeStartY, barcodeStartX+barcodeWidth, barcodeStartY+barcodeHeight),
|
||
barcodeImg,
|
||
image.Point{},
|
||
draw.Over)
|
||
|
||
// 7. 绘制货号文字(居中,加粗字体)
|
||
textAreaTop := barcodeStartY + barcodeHeight + gapBetween
|
||
textAreaHeight := textHeight + textBottomPadding
|
||
baselineY := textAreaTop + (textAreaHeight / 2) + (ascent / 2) - 2
|
||
|
||
textX := (finalWidth - textWidth) / 2
|
||
if textX < 0 {
|
||
textX = config.Padding
|
||
}
|
||
|
||
textDrawer := &font.Drawer{
|
||
Dst: finalImg,
|
||
Src: image.NewUniform(config.BarColor),
|
||
Face: face,
|
||
Dot: fixed.Point26_6{
|
||
X: fixed.I(textX),
|
||
Y: fixed.I(baselineY),
|
||
},
|
||
}
|
||
textDrawer.DrawString(content)
|
||
|
||
// 8. 转换为Base64
|
||
buf := new(bytes.Buffer)
|
||
if err := png.Encode(buf, finalImg); err != nil {
|
||
return "", err
|
||
}
|
||
|
||
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
||
}
|
||
|
||
// generateBarcodeImage 生成条形码图像
|
||
func generateBarcodeImage(content string, config *Config) (image.Image, error) {
|
||
// 使用Code128编码
|
||
code128Barcode, err := code128.Encode(content)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("编码失败: %w", err)
|
||
}
|
||
|
||
rawBounds := code128Barcode.Bounds()
|
||
rawWidth := rawBounds.Dx()
|
||
targetWidth := rawWidth * config.ModuleWidth
|
||
targetHeight := config.BarcodeHeight
|
||
|
||
return barcode.Scale(code128Barcode, targetWidth, targetHeight)
|
||
}
|
||
|
||
// loadFont 加载加粗TrueType字体
|
||
func loadFont(config *Config) (font.Face, error) {
|
||
fontBytes, err := os.ReadFile(config.FontPath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取字体文件失败: %w", err)
|
||
}
|
||
|
||
parsedFont, err := opentype.Parse(fontBytes)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("解析字体失败: %w", err)
|
||
}
|
||
|
||
face, err := opentype.NewFace(parsedFont, &opentype.FaceOptions{
|
||
Size: config.FontSize,
|
||
DPI: 96,
|
||
Hinting: font.HintingFull,
|
||
})
|
||
if err != nil {
|
||
return nil, fmt.Errorf("创建字体失败: %w", err)
|
||
}
|
||
return face, nil
|
||
}
|