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(不绘制文字) }