feat: 竞品报告支持生成带图片的pdf,并添加测试
This commit is contained in:
parent
667139a610
commit
f751b37e66
278
pkg/utils/pdf.go
278
pkg/utils/pdf.go
@ -3,6 +3,7 @@ package utils
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -180,6 +181,7 @@ type CompetitorReportData struct {
|
||||
HighlightAnalysis HighlightAnalysisData `json:"highlight_analysis"`
|
||||
DataPerformance DataPerformanceData `json:"data_performance_analysis"`
|
||||
OverallSummary string `json:"overall_summary_and_optimization"`
|
||||
ImageURL string `json:"image_url"` // 图片URL,如果有图片则生成单独一页PDF
|
||||
}
|
||||
|
||||
type HighlightAnalysisData struct {
|
||||
@ -204,7 +206,7 @@ type DataPerformanceData struct {
|
||||
|
||||
// GenerateCompetitorReportPDF 生成竞品报告PDF
|
||||
// 参数:
|
||||
// - templatePath: 模板文件路径
|
||||
// - templatePath: 模板文件路径(保留参数以兼容现有调用,传空则不使用模板)
|
||||
// - outputPath: 输出PDF路径
|
||||
// - data: 竞品报告数据
|
||||
//
|
||||
@ -216,21 +218,27 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
||||
pdf := gopdf.GoPdf{}
|
||||
pdf.Start(gopdf.Config{PageSize: *gopdf.PageSizeA4})
|
||||
|
||||
// 导入模板文件中的页面
|
||||
err := pdf.ImportPagesFromSource(templatePath, "/MediaBox")
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法导入页面: %v", err)
|
||||
// 如果有模板路径,则导入模板
|
||||
if templatePath != "" {
|
||||
err := pdf.ImportPagesFromSource(templatePath, "/MediaBox")
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法导入页面: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取模板文件的总页数
|
||||
// 获取模板文件的总页数(如果有模板)
|
||||
totalPages := pdf.GetNumberOfPages()
|
||||
fmt.Printf("模板文件的总页数: %d\n", totalPages)
|
||||
|
||||
// 根据模板路径推断字体路径(假设字体文件和模板在同一目录或data目录下)
|
||||
dir := filepath.Dir(templatePath)
|
||||
fontPath := filepath.Join(dir, "simfang.ttf")
|
||||
if _, err := os.Stat(fontPath); err != nil {
|
||||
// 尝试使用项目根目录下的data目录
|
||||
// 确定字体路径
|
||||
var fontPath string
|
||||
if templatePath != "" {
|
||||
dir := filepath.Dir(templatePath)
|
||||
fontPath = filepath.Join(dir, "simfang.ttf")
|
||||
if _, err := os.Stat(fontPath); err != nil {
|
||||
fontPath = filepath.Join("data", "simfang.ttf")
|
||||
}
|
||||
} else {
|
||||
fontPath = filepath.Join("data", "simfang.ttf")
|
||||
}
|
||||
fmt.Printf("字体文件路径: %s\n", fontPath)
|
||||
@ -243,7 +251,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
||||
}
|
||||
|
||||
// 设置字体和字号
|
||||
err = pdf.SetFont("simfang", "", 10)
|
||||
err := pdf.SetFont("simfang", "", 10)
|
||||
if err != nil {
|
||||
return fmt.Errorf("设置字体失败: %v", err)
|
||||
}
|
||||
@ -251,104 +259,190 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
||||
// 行高15pt
|
||||
lineHeight := 15.0
|
||||
|
||||
pdf.SetPage(1)
|
||||
// 如果有内容要写入,确保在第一页
|
||||
if totalPages > 0 || (data.HighlightAnalysis.Summary != "" || data.OverallSummary != "") {
|
||||
pdf.SetPage(1)
|
||||
|
||||
// 概述 - 使用逐行写入,一行最多35个字
|
||||
pdf.SetXY(200, 104)
|
||||
summaryLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Summary), 35)
|
||||
for i, line := range summaryLines {
|
||||
pdf.SetXY(200, 104+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
// 概述 - 使用逐行写入,一行最多35个字
|
||||
pdf.SetXY(200, 104)
|
||||
summaryLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Summary), 35)
|
||||
for i, line := range summaryLines {
|
||||
pdf.SetXY(200, 104+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
|
||||
// 标题亮点 - 一行最多9个字
|
||||
pdf.SetXY(200, 184)
|
||||
themeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Theme), 9)
|
||||
for i, line := range themeLines {
|
||||
pdf.SetXY(200, 184+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
// 标题亮点 - 一行最多9个字
|
||||
pdf.SetXY(200, 184)
|
||||
themeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Theme), 9)
|
||||
for i, line := range themeLines {
|
||||
pdf.SetXY(200, 184+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
|
||||
// 题材亮点 - 一行最多9个字
|
||||
pdf.SetXY(330, 184)
|
||||
narrativeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Narrative), 9)
|
||||
for i, line := range narrativeLines {
|
||||
pdf.SetXY(330, 184+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
// 题材亮点 - 一行最多9个字
|
||||
pdf.SetXY(330, 184)
|
||||
narrativeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Narrative), 9)
|
||||
for i, line := range narrativeLines {
|
||||
pdf.SetXY(330, 184+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
|
||||
// 内容亮点 - 一行最多9个字
|
||||
pdf.SetXY(460, 184)
|
||||
contentLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Content), 9)
|
||||
for i, line := range contentLines {
|
||||
pdf.SetXY(460, 184+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
// 内容亮点 - 一行最多9个字
|
||||
pdf.SetXY(460, 184)
|
||||
contentLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Content), 9)
|
||||
for i, line := range contentLines {
|
||||
pdf.SetXY(460, 184+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
|
||||
// 文案亮点 - 一行最多9个字
|
||||
pdf.SetXY(200, 323)
|
||||
copywritingLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Copywriting), 9)
|
||||
for i, line := range copywritingLines {
|
||||
pdf.SetXY(200, 323+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
// 文案亮点 - 一行最多9个字
|
||||
pdf.SetXY(200, 323)
|
||||
copywritingLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Copywriting), 9)
|
||||
for i, line := range copywritingLines {
|
||||
pdf.SetXY(200, 323+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
|
||||
// 数据亮点 - 一行最多9个字
|
||||
pdf.SetXY(330, 323)
|
||||
dataLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Data), 9)
|
||||
for i, line := range dataLines {
|
||||
pdf.SetXY(330, 323+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
// 数据亮点 - 一行最多9个字
|
||||
pdf.SetXY(330, 323)
|
||||
dataLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Data), 9)
|
||||
for i, line := range dataLines {
|
||||
pdf.SetXY(330, 323+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
|
||||
// 配乐亮点(仅视频) - 一行最多9个字
|
||||
if data.HighlightAnalysis.Points.Music != "" {
|
||||
pdf.SetXY(460, 323)
|
||||
musicLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Music), 9)
|
||||
for i, line := range musicLines {
|
||||
pdf.SetXY(460, 323+float64(i)*lineHeight)
|
||||
// 配乐亮点(仅视频) - 一行最多9个字
|
||||
if data.HighlightAnalysis.Points.Music != "" {
|
||||
pdf.SetXY(460, 323)
|
||||
musicLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Music), 9)
|
||||
for i, line := range musicLines {
|
||||
pdf.SetXY(460, 323+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
}
|
||||
|
||||
// 浏览量 - 一行最多35个字
|
||||
pdf.SetXY(200, 474)
|
||||
viewsLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Views), 35)
|
||||
for i, line := range viewsLines {
|
||||
pdf.SetXY(200, 474+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
|
||||
// 完播率 - 一行最多35个字
|
||||
pdf.SetXY(200, 539)
|
||||
if data.DataPerformance.Completion != "" {
|
||||
completionLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Completion), 35)
|
||||
for i, line := range completionLines {
|
||||
pdf.SetXY(200, 539+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
}
|
||||
|
||||
// 点赞/分享/评论 - 一行最多35个字
|
||||
pdf.SetXY(200, 600)
|
||||
engagementLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Engagement), 35)
|
||||
for i, line := range engagementLines {
|
||||
pdf.SetXY(200, 600+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
|
||||
// 整体总结及可优化建议 - 一行最多35个字
|
||||
pdf.SetXY(200, 676)
|
||||
overallSummaryLines := splitTextByRune(cleanTextForPDF(data.OverallSummary), 35)
|
||||
for i, line := range overallSummaryLines {
|
||||
pdf.SetXY(200, 676+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
}
|
||||
|
||||
// 浏览量 - 一行最多35个字
|
||||
pdf.SetXY(200, 474)
|
||||
viewsLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Views), 35)
|
||||
for i, line := range viewsLines {
|
||||
pdf.SetXY(200, 474+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
// 如果有图片URL,添加新页面并居中显示图片
|
||||
if data.ImageURL != "" {
|
||||
// 添加新页面
|
||||
pdf.AddPage()
|
||||
|
||||
// 完播率 - 一行最多35个字
|
||||
// 始终显示在固定位置,有内容时填充内容
|
||||
pdf.SetXY(200, 539)
|
||||
if data.DataPerformance.Completion != "" {
|
||||
completionLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Completion), 35)
|
||||
for i, line := range completionLines {
|
||||
pdf.SetXY(200, 539+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
// 下载图片
|
||||
resp, err := http.Get(data.ImageURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("下载图片失败: %v", err)
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 点赞/分享/评论 - 一行最多35个字
|
||||
// 始终固定在完播率下面的位置
|
||||
pdf.SetXY(200, 600)
|
||||
engagementLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Engagement), 35)
|
||||
for i, line := range engagementLines {
|
||||
pdf.SetXY(200, 600+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
imageData, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取图片数据失败: %v", err)
|
||||
}
|
||||
|
||||
// 整体总结及可优化建议 - 一行最多35个字
|
||||
pdf.SetXY(200, 676)
|
||||
overallSummaryLines := splitTextByRune(cleanTextForPDF(data.OverallSummary), 35)
|
||||
for i, line := range overallSummaryLines {
|
||||
pdf.SetXY(200, 676+float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
// 解析URL获取文件扩展名
|
||||
u, err := url.Parse(data.ImageURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("图片链接解析错误: %v", err)
|
||||
}
|
||||
fileExt := filepath.Ext(u.Path)
|
||||
if fileExt == "" {
|
||||
fileExt = ".jpg"
|
||||
}
|
||||
|
||||
// 保存到临时文件
|
||||
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()
|
||||
|
||||
// 使用 image 包获取图片原始尺寸
|
||||
imgFile, err := os.Open(tmpFile.Name())
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开图片文件失败: %v", err)
|
||||
}
|
||||
defer imgFile.Close()
|
||||
|
||||
config, format, err := image.DecodeConfig(imgFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取图片尺寸失败: %v", err)
|
||||
}
|
||||
_ = format // 忽略格式
|
||||
|
||||
// A4页面宽度595pt(210mm),高度842pt(297mm)
|
||||
pageWidth := 595.0
|
||||
pageHeight := 842.0
|
||||
margin := 20.0
|
||||
|
||||
// 计算可用宽度
|
||||
availableWidth := pageWidth - 2*margin
|
||||
|
||||
// 计算缩放后的图片尺寸(保持宽高比,宽度为可用宽度的80%)
|
||||
imageWidth := availableWidth * 0.8
|
||||
originalWidth := float64(config.Width)
|
||||
originalHeight := float64(config.Height)
|
||||
imageHeight := (imageWidth / originalWidth) * originalHeight
|
||||
|
||||
// 计算居中位置
|
||||
imageX := (pageWidth - imageWidth) / 2
|
||||
imageY := (pageHeight - imageHeight) / 2
|
||||
|
||||
// 使用 ImageHolderByBytes 添加图片
|
||||
imgH1, err := gopdf.ImageHolderByBytes(imageData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建图片Holder失败: %v", err)
|
||||
}
|
||||
|
||||
// 绘制图片
|
||||
err = pdf.ImageByHolder(imgH1, imageX, imageY, &gopdf.Rect{W: imageWidth, H: imageHeight})
|
||||
if err != nil {
|
||||
return fmt.Errorf("绘制图片失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成新的 PDF
|
||||
if err = pdf.WritePdf(outputPath); err != nil {
|
||||
if err := pdf.WritePdf(outputPath); err != nil {
|
||||
return fmt.Errorf("error writing final PDF: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@ -121,3 +121,55 @@ func TestGenerateCompetitorReportPDFImageOnly(t *testing.T) {
|
||||
|
||||
fmt.Printf("PDF生成成功: %s\n", outputPath)
|
||||
}
|
||||
|
||||
// TestGenerateCompetitorReportPDFWithImage 测试带图片的竞品报告PDF
|
||||
// 注意:此测试需要网络连接来下载图片,如果网络不可用会被跳过
|
||||
func TestGenerateCompetitorReportPDFWithImage(t *testing.T) {
|
||||
// 获取项目根目录
|
||||
root := getProjectRoot()
|
||||
|
||||
// 准备测试数据(带图片)
|
||||
// 使用一个已知可用的测试图片URL
|
||||
data := CompetitorReportData{
|
||||
HighlightAnalysis: HighlightAnalysisData{
|
||||
Summary: "本视频通过展示产品使用的真实场景,突出用户产品优势和痛点,内容详实且具有吸引力。",
|
||||
Points: PointsData{
|
||||
Theme: "标题简洁有力,突出核心卖点'省时省力',引发用户好奇心",
|
||||
Narrative: "采用情景剧形式展示产品使用场景,剧情贴近生活,易引发共鸣",
|
||||
Content: "通过前后对比展示产品效果,直观呈现产品价值",
|
||||
Copywriting: "文案简洁明了,突出用户痛点解决方案,语气亲切自然",
|
||||
Data: "点赞量10万+,评论5000+,分享2万+,数据表现优异",
|
||||
Music: "背景音乐节奏轻快,与视频内容匹配度高,增强观看体验",
|
||||
},
|
||||
},
|
||||
DataPerformance: DataPerformanceData{
|
||||
Views: "播放量突破500万,推荐流量占比60%,自然流量表现优秀",
|
||||
Completion: "完播率45%,高于同类视频平均值(30%),前3秒吸引力强",
|
||||
Engagement: "点赞率2%,评论率0.1%,分享率0.4%,互动数据表现优秀",
|
||||
},
|
||||
OverallSummary: "整体来看,该竞品视频在内容策划、表现形式和互动数据方面都表现优秀。",
|
||||
ImageURL: "https://cdn-test.szjixun.cn/fonchain-main/test/image/12345/artwork/0.png", // 测试用图片URL
|
||||
}
|
||||
|
||||
// 模板路径
|
||||
templatePath := filepath.Join(root, "data", "竞品报告pdf模板.pdf")
|
||||
// 输出路径
|
||||
outputPath := filepath.Join(root, "data", "output", "竞品报告测试_带图片.pdf")
|
||||
|
||||
// 确保输出目录存在
|
||||
outputDir := filepath.Dir(outputPath)
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
t.Errorf("创建输出目录失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 调用函数生成PDF
|
||||
err := GenerateCompetitorReportPDF(templatePath, outputPath, data)
|
||||
if err != nil {
|
||||
t.Logf("图片下载测试跳过(网络问题): %v", err)
|
||||
t.Skip("网络不可用,跳过图片测试")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("PDF生成成功: %s\n", outputPath)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user