feat: 优化pdf排版。英文符号和数字只算半个字符,并添加单元测试
This commit is contained in:
parent
08a70e8399
commit
76c5a9f6f3
@ -266,7 +266,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
|||||||
|
|
||||||
// 概述 - 使用逐行写入,一行最多35个字
|
// 概述 - 使用逐行写入,一行最多35个字
|
||||||
pdf.SetXY(200, 104)
|
pdf.SetXY(200, 104)
|
||||||
summaryLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Summary), 35)
|
summaryLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Summary), 35.0)
|
||||||
for i, line := range summaryLines {
|
for i, line := range summaryLines {
|
||||||
pdf.SetXY(200, 104+float64(i)*lineHeight)
|
pdf.SetXY(200, 104+float64(i)*lineHeight)
|
||||||
pdf.Cell(nil, line)
|
pdf.Cell(nil, line)
|
||||||
@ -274,7 +274,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
|||||||
|
|
||||||
// 标题亮点 - 一行最多9个字
|
// 标题亮点 - 一行最多9个字
|
||||||
pdf.SetXY(200, 184)
|
pdf.SetXY(200, 184)
|
||||||
themeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Theme), 9)
|
themeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Theme), 9.0)
|
||||||
for i, line := range themeLines {
|
for i, line := range themeLines {
|
||||||
pdf.SetXY(200, 184+float64(i)*lineHeight)
|
pdf.SetXY(200, 184+float64(i)*lineHeight)
|
||||||
pdf.Cell(nil, line)
|
pdf.Cell(nil, line)
|
||||||
@ -282,7 +282,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
|||||||
|
|
||||||
// 题材亮点 - 一行最多9个字
|
// 题材亮点 - 一行最多9个字
|
||||||
pdf.SetXY(330, 184)
|
pdf.SetXY(330, 184)
|
||||||
narrativeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Narrative), 9)
|
narrativeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Narrative), 9.0)
|
||||||
for i, line := range narrativeLines {
|
for i, line := range narrativeLines {
|
||||||
pdf.SetXY(330, 184+float64(i)*lineHeight)
|
pdf.SetXY(330, 184+float64(i)*lineHeight)
|
||||||
pdf.Cell(nil, line)
|
pdf.Cell(nil, line)
|
||||||
@ -290,7 +290,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
|||||||
|
|
||||||
// 内容亮点 - 一行最多9个字
|
// 内容亮点 - 一行最多9个字
|
||||||
pdf.SetXY(460, 184)
|
pdf.SetXY(460, 184)
|
||||||
contentLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Content), 9)
|
contentLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Content), 9.0)
|
||||||
for i, line := range contentLines {
|
for i, line := range contentLines {
|
||||||
pdf.SetXY(460, 184+float64(i)*lineHeight)
|
pdf.SetXY(460, 184+float64(i)*lineHeight)
|
||||||
pdf.Cell(nil, line)
|
pdf.Cell(nil, line)
|
||||||
@ -298,7 +298,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
|||||||
|
|
||||||
// 文案亮点 - 一行最多9个字
|
// 文案亮点 - 一行最多9个字
|
||||||
pdf.SetXY(200, 323)
|
pdf.SetXY(200, 323)
|
||||||
copywritingLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Copywriting), 9)
|
copywritingLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Copywriting), 9.0)
|
||||||
for i, line := range copywritingLines {
|
for i, line := range copywritingLines {
|
||||||
pdf.SetXY(200, 323+float64(i)*lineHeight)
|
pdf.SetXY(200, 323+float64(i)*lineHeight)
|
||||||
pdf.Cell(nil, line)
|
pdf.Cell(nil, line)
|
||||||
@ -306,7 +306,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
|||||||
|
|
||||||
// 数据亮点 - 一行最多9个字
|
// 数据亮点 - 一行最多9个字
|
||||||
pdf.SetXY(330, 323)
|
pdf.SetXY(330, 323)
|
||||||
dataLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Data), 9)
|
dataLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Data), 9.0)
|
||||||
for i, line := range dataLines {
|
for i, line := range dataLines {
|
||||||
pdf.SetXY(330, 323+float64(i)*lineHeight)
|
pdf.SetXY(330, 323+float64(i)*lineHeight)
|
||||||
pdf.Cell(nil, line)
|
pdf.Cell(nil, line)
|
||||||
@ -315,7 +315,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
|||||||
// 配乐亮点(仅视频) - 一行最多9个字
|
// 配乐亮点(仅视频) - 一行最多9个字
|
||||||
if data.HighlightAnalysis.Points.Music != "" {
|
if data.HighlightAnalysis.Points.Music != "" {
|
||||||
pdf.SetXY(460, 323)
|
pdf.SetXY(460, 323)
|
||||||
musicLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Music), 9)
|
musicLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Music), 9.0)
|
||||||
for i, line := range musicLines {
|
for i, line := range musicLines {
|
||||||
pdf.SetXY(460, 323+float64(i)*lineHeight)
|
pdf.SetXY(460, 323+float64(i)*lineHeight)
|
||||||
pdf.Cell(nil, line)
|
pdf.Cell(nil, line)
|
||||||
@ -324,7 +324,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
|||||||
|
|
||||||
// 浏览量 - 一行最多35个字
|
// 浏览量 - 一行最多35个字
|
||||||
pdf.SetXY(200, 474)
|
pdf.SetXY(200, 474)
|
||||||
viewsLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Views), 35)
|
viewsLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Views), 35.0)
|
||||||
for i, line := range viewsLines {
|
for i, line := range viewsLines {
|
||||||
pdf.SetXY(200, 474+float64(i)*lineHeight)
|
pdf.SetXY(200, 474+float64(i)*lineHeight)
|
||||||
pdf.Cell(nil, line)
|
pdf.Cell(nil, line)
|
||||||
@ -333,7 +333,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
|||||||
// 完播率 - 一行最多35个字
|
// 完播率 - 一行最多35个字
|
||||||
pdf.SetXY(200, 539)
|
pdf.SetXY(200, 539)
|
||||||
if data.DataPerformance.Completion != "" {
|
if data.DataPerformance.Completion != "" {
|
||||||
completionLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Completion), 35)
|
completionLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Completion), 35.0)
|
||||||
for i, line := range completionLines {
|
for i, line := range completionLines {
|
||||||
pdf.SetXY(200, 539+float64(i)*lineHeight)
|
pdf.SetXY(200, 539+float64(i)*lineHeight)
|
||||||
pdf.Cell(nil, line)
|
pdf.Cell(nil, line)
|
||||||
@ -342,7 +342,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
|||||||
|
|
||||||
// 点赞/分享/评论 - 一行最多35个字
|
// 点赞/分享/评论 - 一行最多35个字
|
||||||
pdf.SetXY(200, 600)
|
pdf.SetXY(200, 600)
|
||||||
engagementLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Engagement), 35)
|
engagementLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Engagement), 35.0)
|
||||||
for i, line := range engagementLines {
|
for i, line := range engagementLines {
|
||||||
pdf.SetXY(200, 600+float64(i)*lineHeight)
|
pdf.SetXY(200, 600+float64(i)*lineHeight)
|
||||||
pdf.Cell(nil, line)
|
pdf.Cell(nil, line)
|
||||||
@ -350,7 +350,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
|||||||
|
|
||||||
// 整体总结及可优化建议 - 一行最多35个字
|
// 整体总结及可优化建议 - 一行最多35个字
|
||||||
pdf.SetXY(200, 676)
|
pdf.SetXY(200, 676)
|
||||||
overallSummaryLines := splitTextByRune(cleanTextForPDF(data.OverallSummary), 35)
|
overallSummaryLines := splitTextByRune(cleanTextForPDF(data.OverallSummary), 35.0)
|
||||||
for i, line := range overallSummaryLines {
|
for i, line := range overallSummaryLines {
|
||||||
pdf.SetXY(200, 676+float64(i)*lineHeight)
|
pdf.SetXY(200, 676+float64(i)*lineHeight)
|
||||||
pdf.Cell(nil, line)
|
pdf.Cell(nil, line)
|
||||||
@ -450,49 +450,58 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitTextByRune 将文本按指定字符数拆分成多行
|
// getCharWidth 获取字符的宽度权重
|
||||||
// 按每行最大字符数拆分,中文、英文都按 1 个 rune 计
|
// 英文字母、数字、英文符号返回 0.5,其他字符返回 1.0
|
||||||
func splitTextByRune(text string, maxRunesPerLine int) []string {
|
func getCharWidth(r rune) float64 {
|
||||||
if text == "" {
|
// 英文字母 (A-Z, a-z)
|
||||||
return []string{}
|
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
|
||||||
|
return 0.5
|
||||||
}
|
}
|
||||||
runes := []rune(text)
|
// 数字 (0-9)
|
||||||
if len(runes) <= maxRunesPerLine {
|
if r >= '0' && r <= '9' {
|
||||||
return []string{text}
|
return 0.5
|
||||||
}
|
}
|
||||||
var lines []string
|
// 英文符号
|
||||||
for i := 0; i < len(runes); i += maxRunesPerLine {
|
if (r >= 0x21 && r <= 0x2F) || // ! " # $ % & ' ( ) * + , - . /
|
||||||
end := i + maxRunesPerLine
|
(r >= 0x3A && r <= 0x40) || // : ; < = > ? @
|
||||||
if end > len(runes) {
|
(r >= 0x5B && r <= 0x60) || // [ \ ] ^ _ `
|
||||||
end = len(runes)
|
(r >= 0x7B && r <= 0x7E) { // { | } ~
|
||||||
}
|
return 0.5
|
||||||
lines = append(lines, string(runes[i:end]))
|
|
||||||
}
|
}
|
||||||
return lines
|
return 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrapText 将文本按指定宽度换行(按字符数计算)
|
// splitTextByRune 将文本按指定字符宽度拆分成多行
|
||||||
func wrapText(text string, maxLen int) []string {
|
// 按每行最大宽度拆分,英文字母/数字/英文符号按0.5计算,其他按1计算
|
||||||
|
func splitTextByRune(text string, maxWidth float64) []string {
|
||||||
if text == "" {
|
if text == "" {
|
||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
|
runes := []rune(text)
|
||||||
|
|
||||||
var lines []string
|
var lines []string
|
||||||
runes := []rune(text)
|
|
||||||
currentLine := ""
|
currentLine := ""
|
||||||
|
currentWidth := 0.0
|
||||||
|
|
||||||
for _, r := range runes {
|
for _, r := range runes {
|
||||||
// 如果当前行字符数达到最大限度,换行
|
charWidth := getCharWidth(r)
|
||||||
if len(currentLine) >= maxLen {
|
|
||||||
lines = append(lines, currentLine)
|
// 检查加上这个字符是否会超过最大宽度
|
||||||
|
if currentWidth+charWidth > maxWidth {
|
||||||
|
// 超过最大宽度,保存当前行并开始新行
|
||||||
|
if currentLine != "" {
|
||||||
|
lines = append(lines, currentLine)
|
||||||
|
}
|
||||||
currentLine = string(r)
|
currentLine = string(r)
|
||||||
|
currentWidth = charWidth
|
||||||
} else {
|
} else {
|
||||||
currentLine += string(r)
|
currentLine += string(r)
|
||||||
|
currentWidth += charWidth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加最后一行
|
// 添加最后一行
|
||||||
if len(currentLine) > 0 {
|
if currentLine != "" {
|
||||||
lines = append(lines, currentLine)
|
lines = append(lines, currentLine)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,7 @@ func getProjectRoot() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestGenerateCompetitorReportPDF 测试生成竞品报告PDF
|
// TestGenerateCompetitorReportPDF 测试生成竞品报告PDF
|
||||||
func TestGenerateCompetitorReportPDF(t *testing.T) {
|
func TestGenerateCompetitorReportPDF1(t *testing.T) {
|
||||||
// 获取项目根目录
|
// 获取项目根目录
|
||||||
root := getProjectRoot()
|
root := getProjectRoot()
|
||||||
fmt.Printf("项目根目录: %s\n", root)
|
fmt.Printf("项目根目录: %s\n", root)
|
||||||
@ -124,7 +124,7 @@ func TestGenerateCompetitorReportPDFImageOnly(t *testing.T) {
|
|||||||
|
|
||||||
// TestGenerateCompetitorReportPDFWithImage 测试带图片的竞品报告PDF
|
// TestGenerateCompetitorReportPDFWithImage 测试带图片的竞品报告PDF
|
||||||
// 注意:此测试需要网络连接来下载图片,如果网络不可用会被跳过
|
// 注意:此测试需要网络连接来下载图片,如果网络不可用会被跳过
|
||||||
func TestGenerateCompetitorReportPDFWithImage(t *testing.T) {
|
func TestGenerateCompetitorReportPDFWithImage1(t *testing.T) {
|
||||||
// 获取项目根目录
|
// 获取项目根目录
|
||||||
root := getProjectRoot()
|
root := getProjectRoot()
|
||||||
|
|
||||||
@ -173,3 +173,51 @@ func TestGenerateCompetitorReportPDFWithImage(t *testing.T) {
|
|||||||
|
|
||||||
fmt.Printf("PDF生成成功: %s\n", outputPath)
|
fmt.Printf("PDF生成成功: %s\n", outputPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGenerateCompetitorReportPDFMixedContent 测试中英混合、数字、符号的复杂情况
|
||||||
|
func TestGenerateCompetitorReportPDFMixedContent(t *testing.T) {
|
||||||
|
root := getProjectRoot()
|
||||||
|
|
||||||
|
// 准备包含中英混合、数字、符号的复杂测试数据
|
||||||
|
data := CompetitorReportData{
|
||||||
|
HighlightAnalysis: HighlightAnalysisData{
|
||||||
|
Summary: "这是一个关于2024年短视频创作的爆款分析报告!视频总播放量达到1.2亿次,点赞率8.5%(远超行业平均3.2%)。内容包括:①生活技巧类占比40% ②知识科普类30% ③娱乐搞笑类30%。推荐算法推荐流量占比85%,搜索流量10%,其他渠道5%。",
|
||||||
|
Points: PointsData{
|
||||||
|
Theme: "标题:【逆袭】3个月从0到100万粉丝!我是如何做到的?必看!🔥",
|
||||||
|
Narrative: "采用问题-解决叙事结构:前3秒抛出痛点你还在为XX发愁吗,然后展示解决方案ABC",
|
||||||
|
Content: "内容分为3个板块:①前置干货预告15秒 ②核心内容展示30秒 ③互动引导15秒",
|
||||||
|
Copywriting: "文案使用了大量emoji表情🔥💯👍,增加年轻化气息!结尾一句评论区见强化互动",
|
||||||
|
Data: "点赞率8.5%,高于同类平均3.2%,转发率2.1%,评论数2.3万条,评论区活跃度TOP10%",
|
||||||
|
Music: "使用热门BGM孤勇者前奏3秒,配合画面节奏卡点!音量为Original-3dB",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DataPerformance: DataPerformanceData{
|
||||||
|
Views: "发布24小时内播放量突破500万,48小时达到1200万,72小时稳定在1500万。推荐流量占比85%,搜索流量占比10%,其他渠道5%。数据表现:PV=15000000,UV=8500000。",
|
||||||
|
Completion: "平均完播率45.2%,高于行业均值28%,3秒留存率72%,5秒完播率58%,10秒完播率35%,属于高质量流量。平均观看时长18.5秒,视频总时长42秒。",
|
||||||
|
Engagement: "点赞数128000,点赞率0.85%,评论数23000,其中神评论占比15%,高赞评论TOP3:①学到了 ②太棒了 ③收藏了,分享数56000。评论区@相关账号12个,引发二次传播。",
|
||||||
|
},
|
||||||
|
OverallSummary: "该视频成功因素:①标题使用数字热门词激发点击逆袭必看 ②内容结构清晰,每15秒一个高潮点 ③BGM选择契合内容情绪孤勇者 ④评论区运营到位。建议优化:1)可增加合集功能,将同类内容串联 2)提升粉丝粘性到10%以上 3)适当增加直播预告。整体来看,这是一条典型的爆款体质视频,值得借鉴学习!数据支撑:ROI=3.5,CPM=25元,CPC=0.8元。",
|
||||||
|
ImageURL: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模板路径
|
||||||
|
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.Errorf("生成PDF失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("PDF生成成功(中英混合测试): %s\n", outputPath)
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user