212 lines
5.7 KiB
Go
212 lines
5.7 KiB
Go
package service
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/base64"
|
||
"fmt"
|
||
"image"
|
||
"image/color"
|
||
"image/draw"
|
||
"image/png"
|
||
"os"
|
||
"path/filepath"
|
||
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) {
|
||
// 配置参数
|
||
cfg := &Config{
|
||
BarcodeHeight: 120,
|
||
ModuleWidth: 2,
|
||
Padding: 20,
|
||
FontSize: 50,
|
||
BgColor: color.White,
|
||
BarColor: color.Black,
|
||
}
|
||
|
||
// 若内容包含isbn,则使用较小字号且不加粗
|
||
if strings.Contains(strings.ToLower(content), "9787") {
|
||
cfg.FontSize = 35
|
||
}
|
||
|
||
// 生成条形码图片的Base64
|
||
base64Image, err := generateBase64Barcode(content, cfg)
|
||
if err != nil {
|
||
return systemRes.BarcodeResponse{}, fmt.Errorf("生成条形码失败: %w", err)
|
||
}
|
||
|
||
return systemRes.BarcodeResponse{
|
||
ImageBase64: base64Image,
|
||
Content: content,
|
||
}, nil
|
||
}
|
||
|
||
func generateBase64Barcode(content string, cfg *Config) (string, error) {
|
||
// 1. 生成条形码图像
|
||
barcodeImg, err := generateBarcodeImage(content, cfg)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
barcodeBounds := barcodeImg.Bounds()
|
||
barcodeWidth := barcodeBounds.Dx()
|
||
barcodeHeight := barcodeBounds.Dy()
|
||
|
||
// 2. 尝试加载字体(自定义字体 -> 系统字体 -> 跳过文字)
|
||
face, fontErr := loadFont(cfg.FontSize)
|
||
var textHeight, ascent, descent int
|
||
var textWidth int
|
||
if fontErr == nil && face != nil {
|
||
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
|
||
|
||
finalWidth := barcodeWidth + cfg.Padding*2
|
||
finalHeight := barcodeHeight + cfg.Padding*2
|
||
if face != nil {
|
||
maxContentWidth := barcodeWidth
|
||
if textWidth > maxContentWidth {
|
||
maxContentWidth = textWidth
|
||
}
|
||
finalWidth = maxContentWidth + cfg.Padding*2
|
||
finalHeight = barcodeHeight + gapBetween + textHeight + cfg.Padding*2 + textBottomPadding
|
||
}
|
||
|
||
// 5. 创建最终图片
|
||
finalImg := image.NewRGBA(image.Rect(0, 0, finalWidth, finalHeight))
|
||
draw.Draw(finalImg, finalImg.Bounds(), &image.Uniform{cfg.BgColor}, image.Point{}, draw.Src)
|
||
|
||
// 6. 绘制条形码(居中)
|
||
barcodeStartX := (finalWidth - barcodeWidth) / 2
|
||
barcodeStartY := cfg.Padding
|
||
draw.Draw(finalImg,
|
||
image.Rect(barcodeStartX, barcodeStartY, barcodeStartX+barcodeWidth, barcodeStartY+barcodeHeight),
|
||
barcodeImg,
|
||
image.Point{},
|
||
draw.Over)
|
||
|
||
// 7. 绘制货号文字(居中,加粗字体)
|
||
if face != nil {
|
||
textAreaTop := barcodeStartY + barcodeHeight + gapBetween
|
||
textAreaHeight := textHeight + textBottomPadding
|
||
baselineY := textAreaTop + (textAreaHeight / 2) + (ascent / 2) - 2
|
||
|
||
textX := (finalWidth - textWidth) / 2
|
||
if textX < 0 {
|
||
textX = cfg.Padding
|
||
}
|
||
|
||
textDrawer := &font.Drawer{
|
||
Dst: finalImg,
|
||
Src: image.NewUniform(cfg.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 尝试加载字体,按优先级:项目字体目录 -> Windows系统字体 -> Linux系统字体
|
||
// 如果都失败,返回 nil(调用方会跳过文字绘制)
|
||
func loadFont(fontSize float64) (font.Face, error) {
|
||
// 按优先级尝试的字体路径列表
|
||
fontPaths := []string{
|
||
"fonts/youaimoshouheiti-regular.ttf", // 项目自定义字体
|
||
"C:\\Windows\\Fonts\\arial.ttf", // Windows Arial
|
||
"C:\\Windows\\Fonts\\simhei.ttf", // Windows 黑体(支持中文)
|
||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", // Linux DejaVu
|
||
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", // Linux Liberation
|
||
}
|
||
|
||
// 如果是相对路径,尝试基于可执行文件目录解析
|
||
exePath, exeErr := os.Executable()
|
||
for i, p := range fontPaths {
|
||
if !filepath.IsAbs(p) && exeErr == nil {
|
||
fontPaths[i] = filepath.Join(filepath.Dir(exePath), p)
|
||
}
|
||
}
|
||
|
||
for _, fontPath := range fontPaths {
|
||
fontBytes, err := os.ReadFile(fontPath)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
parsedFont, err := opentype.Parse(fontBytes)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
face, err := opentype.NewFace(parsedFont, &opentype.FaceOptions{
|
||
Size: fontSize,
|
||
DPI: 96,
|
||
Hinting: font.HintingFull,
|
||
})
|
||
if err != nil {
|
||
continue
|
||
}
|
||
return face, nil
|
||
}
|
||
|
||
return nil, nil // 所有字体都加载失败,返回 nil(不绘制文字)
|
||
}
|