feat: add text-to-image conversion utility with Markdown and HTML support
This commit is contained in:
parent
d464f18fbd
commit
da77357401
2
go.mod
2
go.mod
@ -45,6 +45,7 @@ require (
|
|||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
|
github.com/gomarkdown/markdown v0.0.0-20250207164621-7a1f277a159e // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
@ -62,6 +63,7 @@ require (
|
|||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
github.com/yuin/goldmark v1.7.8 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.31.0 // indirect
|
go.opentelemetry.io/otel v1.31.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -73,6 +73,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
|||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
|
github.com/gomarkdown/markdown v0.0.0-20250207164621-7a1f277a159e h1:ESHlT0RVZphh4JGBz49I5R6nTdC8Qyc08vU25GQHzzQ=
|
||||||
|
github.com/gomarkdown/markdown v0.0.0-20250207164621-7a1f277a159e/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
@ -163,6 +165,8 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
|
|||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||||
|
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
|
||||||
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
|
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
|
||||||
|
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)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user