feat: add text-to-image conversion utility with Markdown and HTML support
This commit is contained in:
199
util/text_to_pic.go
Normal file
199
util/text_to_pic.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fogleman/gg"
|
||||
"github.com/yuin/goldmark"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultWidth = 800
|
||||
defaultMargin = 20
|
||||
lineHeight = 1.5
|
||||
fontSize = 14
|
||||
maxImageWidth = 600
|
||||
maxImageHeight = 400
|
||||
)
|
||||
|
||||
type ContentBlock struct {
|
||||
Type string // "text" or "image"
|
||||
Content string // text content or image path
|
||||
ImgWidth float64 // image width after resize
|
||||
ImgHeight float64 // image height after resize
|
||||
Image image.Image // loaded image
|
||||
}
|
||||
|
||||
// TextToPicture 将文本转换为图片
|
||||
func TextToPicture(text string, isMarkdown bool) (image.Image, error) {
|
||||
var htmlContent string
|
||||
if isMarkdown {
|
||||
// 将Markdown转换为HTML
|
||||
md := goldmark.New()
|
||||
var buf bytes.Buffer
|
||||
if err := md.Convert([]byte(text), &buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
htmlContent = buf.String()
|
||||
} else {
|
||||
htmlContent = text
|
||||
}
|
||||
|
||||
// 解析HTML并提取内容块
|
||||
doc, err := html.Parse(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var blocks []ContentBlock
|
||||
var extractContent func(*html.Node)
|
||||
extractContent = func(n *html.Node) {
|
||||
if n.Type == html.TextNode {
|
||||
text := strings.TrimSpace(n.Data)
|
||||
if text != "" {
|
||||
blocks = append(blocks, ContentBlock{
|
||||
Type: "text",
|
||||
Content: text,
|
||||
})
|
||||
}
|
||||
} else if n.Type == html.ElementNode && n.Data == "img" {
|
||||
// 处理图片节点
|
||||
var src string
|
||||
for _, attr := range n.Attr {
|
||||
if attr.Key == "src" {
|
||||
src = attr.Val
|
||||
break
|
||||
}
|
||||
}
|
||||
if src != "" && !strings.HasPrefix(src, "http") {
|
||||
// 处理本地图片
|
||||
img, w, h, err := loadAndResizeImage(src)
|
||||
if err == nil {
|
||||
blocks = append(blocks, ContentBlock{
|
||||
Type: "image",
|
||||
Content: src,
|
||||
Image: img,
|
||||
ImgWidth: w,
|
||||
ImgHeight: h,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
extractContent(c)
|
||||
}
|
||||
}
|
||||
extractContent(doc)
|
||||
|
||||
// 计算总高度
|
||||
totalHeight := calculateTotalHeight(blocks)
|
||||
|
||||
// 创建图片上下文
|
||||
dc := gg.NewContext(defaultWidth, totalHeight)
|
||||
dc.SetRGB(1, 1, 1) // 设置白色背景
|
||||
dc.Clear()
|
||||
|
||||
// 设置字体和颜色
|
||||
if err := dc.LoadFontFace("./resource/fonts/SourceHanSansCN-Regular.otf", fontSize); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dc.SetColor(color.Black)
|
||||
|
||||
// 绘制内容
|
||||
y := float64(defaultMargin)
|
||||
for _, block := range blocks {
|
||||
if block.Type == "text" {
|
||||
wrapped := dc.WordWrap(block.Content, float64(defaultWidth-2*defaultMargin))
|
||||
for _, wrappedLine := range wrapped {
|
||||
dc.DrawString(wrappedLine, float64(defaultMargin), y+fontSize)
|
||||
y += fontSize * lineHeight
|
||||
}
|
||||
y += fontSize // 段落间额外间距
|
||||
} else if block.Type == "image" {
|
||||
// 居中绘制图片
|
||||
x := (float64(defaultWidth) - block.ImgWidth) / 2
|
||||
dc.DrawImage(block.Image, int(x), int(y))
|
||||
y += block.ImgHeight + float64(defaultMargin) // 图片后添加间距
|
||||
}
|
||||
}
|
||||
|
||||
return dc.Image(), nil
|
||||
}
|
||||
|
||||
// loadAndResizeImage 加载并调整图片大小
|
||||
func loadAndResizeImage(path string) (image.Image, float64, float64, error) {
|
||||
// 确保路径是绝对路径
|
||||
if !filepath.IsAbs(path) {
|
||||
path = filepath.Join(".", path)
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 解码图片
|
||||
var img image.Image
|
||||
if strings.HasSuffix(strings.ToLower(path), ".png") {
|
||||
img, err = png.Decode(file)
|
||||
} else {
|
||||
img, err = jpeg.Decode(file)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
// 计算调整后的尺寸
|
||||
bounds := img.Bounds()
|
||||
origWidth := float64(bounds.Dx())
|
||||
origHeight := float64(bounds.Dy())
|
||||
|
||||
var newWidth, newHeight float64
|
||||
if origWidth > maxImageWidth {
|
||||
ratio := maxImageWidth / origWidth
|
||||
newWidth = maxImageWidth
|
||||
newHeight = origHeight * ratio
|
||||
} else {
|
||||
newWidth = origWidth
|
||||
newHeight = origHeight
|
||||
}
|
||||
|
||||
if newHeight > maxImageHeight {
|
||||
ratio := maxImageHeight / newHeight
|
||||
newHeight = maxImageHeight
|
||||
newWidth = newWidth * ratio
|
||||
}
|
||||
|
||||
// 创建新的图片上下文并调整大小
|
||||
dc := gg.NewContext(int(newWidth), int(newHeight))
|
||||
dc.DrawImage(img, 0, 0)
|
||||
|
||||
return dc.Image(), newWidth, newHeight, nil
|
||||
}
|
||||
|
||||
// calculateTotalHeight 计算总高度
|
||||
func calculateTotalHeight(blocks []ContentBlock) int {
|
||||
var totalHeight float64 = float64(2 * defaultMargin) // 上下边距
|
||||
|
||||
for _, block := range blocks {
|
||||
if block.Type == "text" {
|
||||
// 估算文本高度
|
||||
estimatedLines := (len(block.Content)*fontSize)/(defaultWidth-2*defaultMargin) + 1
|
||||
totalHeight += float64(estimatedLines) * fontSize * lineHeight
|
||||
totalHeight += fontSize // 段落间距
|
||||
} else if block.Type == "image" {
|
||||
totalHeight += block.ImgHeight + float64(defaultMargin) // 图片高度加间距
|
||||
}
|
||||
}
|
||||
|
||||
return int(totalHeight)
|
||||
}
|
||||
Reference in New Issue
Block a user