169 lines
4.7 KiB
Go
169 lines
4.7 KiB
Go
package utils
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/url"
|
||
"os"
|
||
"path/filepath"
|
||
"unicode"
|
||
|
||
"github.com/phpdave11/gofpdf"
|
||
)
|
||
|
||
// cleanTextForPDF 清理文本,移除PDF不支持的字符(如emoji)
|
||
// gofpdf库不支持某些特殊字符
|
||
func cleanTextForPDF(text string) string {
|
||
var result []rune
|
||
for _, r := range text {
|
||
// 保留基本多文种平面(BMP)内的字符(码点 <= 0xFFFF)
|
||
// 这样可以保留中文、英文、数字等常用字符,但过滤掉emoji等特殊字符
|
||
if r <= 0xFFFF && (unicode.IsPrint(r) || unicode.IsSpace(r)) {
|
||
result = append(result, r)
|
||
}
|
||
}
|
||
return string(result)
|
||
}
|
||
|
||
// loadChineseFont 加载中文字体
|
||
func loadChineseFont(pdf *gofpdf.Fpdf, fontPath string) error {
|
||
var fontData []byte
|
||
var err error
|
||
|
||
// 如果提供了本地字体路径,优先使用本地字体
|
||
if fontPath == "" {
|
||
return errors.New("字体文件路径不能为空")
|
||
}
|
||
|
||
fontData, err = os.ReadFile(fontPath)
|
||
if err != nil {
|
||
return fmt.Errorf("读取字体文件失败: %v", err)
|
||
}
|
||
// 使用本地字体文件
|
||
pdf.AddUTF8FontFromBytes("Chinese", "", fontData)
|
||
return nil
|
||
}
|
||
|
||
// GeneratePDF 生成PDF文件
|
||
func GeneratePDF(text, imageURL, outputPath, fontPath string) error {
|
||
// 创建PDF实例,P=纵向,mm=毫米单位,A4=页面大小
|
||
pdf := gofpdf.New("P", "mm", "A4", "")
|
||
|
||
// 加载中文字体
|
||
err := loadChineseFont(pdf, fontPath)
|
||
if err != nil {
|
||
return fmt.Errorf("加载中文字体失败: %v", err)
|
||
}
|
||
|
||
// 添加新页面
|
||
pdf.AddPage()
|
||
|
||
// 设置字体,使用中文字体,12号字体
|
||
pdf.SetFont("Chinese", "", 12)
|
||
|
||
// 设置页面边距(左、上、右)
|
||
pdf.SetMargins(20, 10, 20)
|
||
|
||
// 设置当前位置(x, y),从左上角开始
|
||
pdf.SetXY(20, 10)
|
||
|
||
// 清理文本,移除PDF不支持的字符(如emoji)
|
||
cleanedText := cleanTextForPDF(text)
|
||
|
||
// 添加文本内容
|
||
// 使用MultiCell方法处理多行文本,支持自动换行
|
||
// 参数:宽度、行高、文本内容、边框、对齐方式、是否填充
|
||
// A4页面宽度210mm,减去左右边距40mm,可用宽度170mm
|
||
textWidth := 170.0
|
||
lineHeight := 7.0
|
||
pdf.MultiCell(textWidth, lineHeight, cleanedText, "", "L", false)
|
||
|
||
// 添加一些间距
|
||
pdf.Ln(5)
|
||
|
||
// 解析URL获取文件扩展名
|
||
u, err := url.Parse(imageURL)
|
||
if err != nil {
|
||
return fmt.Errorf("图片链接解析错误: %v", err)
|
||
}
|
||
fileExt := filepath.Ext(u.Path)
|
||
// 如果没有扩展名,默认使用.jpg
|
||
if fileExt == "" {
|
||
fileExt = ".jpg"
|
||
}
|
||
|
||
// 下载图片
|
||
resp, err := http.Get(imageURL)
|
||
if err != nil {
|
||
return fmt.Errorf("下载图片失败: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 读取图片数据
|
||
imageData, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return fmt.Errorf("读取图片数据失败: %v", err)
|
||
}
|
||
|
||
// 将图片数据保存到临时文件(gofpdf需要文件路径)
|
||
tmpFile, err := os.CreateTemp("", "pdf_image_*"+fileExt)
|
||
if err != nil {
|
||
return fmt.Errorf("创建临时文件失败: %v", err)
|
||
}
|
||
defer os.Remove(tmpFile.Name()) // 使用完后删除临时文件
|
||
defer tmpFile.Close()
|
||
|
||
// 写入图片数据到临时文件
|
||
_, err = tmpFile.Write(imageData)
|
||
if err != nil {
|
||
return fmt.Errorf("写入临时文件失败: %v", err)
|
||
}
|
||
tmpFile.Close()
|
||
|
||
// A4纵向页面宽度210mm,减去左右边距40mm,可用宽度170mm
|
||
// 图片宽度设为可用宽度的70%
|
||
imageWidth := textWidth * 0.7
|
||
// 计算居中位置:页面宽度210mm,图片居中
|
||
imageX := (210.0 - imageWidth) / 2
|
||
currentY := pdf.GetY()
|
||
|
||
// 注册图片并获取原始尺寸,用于计算缩放后的高度
|
||
imgInfo := pdf.RegisterImageOptions(tmpFile.Name(), gofpdf.ImageOptions{})
|
||
if imgInfo == nil {
|
||
return fmt.Errorf("注册图片失败")
|
||
}
|
||
|
||
// 计算缩放后的图片高度(按比例缩放)
|
||
// 原始宽度:原始高度 = 缩放后宽度:缩放后高度
|
||
originalWidth, originalHeight := imgInfo.Extent()
|
||
imageHeight := (imageWidth / originalWidth) * originalHeight
|
||
|
||
// A4页面高度297mm,底部边距10mm,计算可用的最大Y坐标
|
||
pageHeight := 297.0
|
||
bottomMargin := 10.0
|
||
maxY := pageHeight - bottomMargin
|
||
|
||
// 检查当前页面剩余空间是否足够放下图片
|
||
// 如果图片底部会超出页面可用区域,则添加新页面
|
||
if currentY+imageHeight > maxY {
|
||
pdf.AddPage()
|
||
// 新页面从顶部边距开始
|
||
currentY = 10.0
|
||
}
|
||
|
||
// 添加图片
|
||
// ImageOptions参数:图片路径、x坐标、y坐标、宽度、高度、是否流式布局、选项、链接
|
||
// 高度设为0表示按比例自动计算
|
||
pdf.ImageOptions(tmpFile.Name(), imageX, currentY, imageWidth, 0, false, gofpdf.ImageOptions{}, 0, "")
|
||
|
||
// 生成并保存PDF文件
|
||
err = pdf.OutputFileAndClose(outputPath)
|
||
if err != nil {
|
||
return fmt.Errorf("生成PDF失败: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|