310 lines
13 KiB
Go
310 lines
13 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"golang.org/x/image/draw"
|
||
"image"
|
||
"image/jpeg"
|
||
"image/png"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
)
|
||
|
||
//func main() {
|
||
// //// 测试长文本
|
||
// //longText := "这是一段非常长的中文文本,需要测试自动换行功能。我们希望当文本超过图片宽度时,能够自动换行到下一行显示。这样就不需要手动添加换行符了。这段文本包含了中英文混合的内容,比如这里有一些English words mixed with中文。同时,我们也要测试标点符号的处理,例如逗号、句号、感叹号!问号?以及各种括号(包括圆括号、方括号[]、花括号{})等等。最后,我们还要测试一下当文本非常长,超过图片高度时的处理情况。" +
|
||
// // "这是一段非常长的中文文本,需要测试自动换行功能。我们希望当文本超过图片宽度时,能够自动换行到下一行显示。这样就不需要手动添加换行符了。这段文本包含了中英文混合的内容,比如这里有一些English words mixed with中文。同时,我们也要测试标点符号的处理,例如逗号、句号、感叹号!问号?以及各种括号(包括圆括号、方括号[]、花括号{})等等。最后,我们还要测试一下当文本非常长,超过图片高度时的处理情况。" +
|
||
// // "这是一段非常长的中文文本,需要测试自动换行功能。我们希望当文本超过图片宽度时,能够自动换行到下一行显示。这样就不需要手动添加换行符了。这段文本包含了中英文混合的内容,比如这里有一些English words mixed with中文。同时,我们也要测试标点符号的处理,例如逗号、句号、感叹号!问号?以及各种括号(包括圆括号、方括号[]、花括号{})等等。最后,我们还要测试一下当文本非常长,超过图片高度时的处理情况。" +
|
||
// // "这是一段非常长的中文文本,需要测试自动换行功能。我们希望当文本超过图片宽度时,能够自动换行到下一行显示。这样就不需要手动添加换行符了。这段文本包含了中英文混合的内容,比如这里有一些English words mixed with中文。同时,我们也要测试标点符号的处理,例如逗号、句号、感叹号!问号?以及各种括号(包括圆括号、方括号[]、花括号{})等等。最后,我们还要测试一下当文本非常长,超过图片高度时的处理情况。" +
|
||
// // "这是一段非常长的中文文本,需要测试自动换行功能。我们希望当文本超过图片宽度时,能够自动换行到下一行显示。这样就不需要手动添加换行符了。这段文本包含了中英文混合的内容,比如这里有一些English words mixed with中文。同时,我们也要测试标点符号的处理,例如逗号、句号、感叹号!问号?以及各种括号(包括圆括号、方括号[]、花括号{})等等。最后,我们还要测试一下当文本非常长,超过图片高度时的处理情况。" +
|
||
// // "这是一段非常长的中文文本,需要测试自动换行功能。我们希望当文本超过图片宽度时,能够自动换行到下一行显示。这样就不需要手动添加换行符了。这段文本包含了中英文混合的内容,比如这里有一些English words mixed with中文。同时,我们也要测试标点符号的处理,例如逗号、句号、感叹号!问号?以及各种括号(包括圆括号、方括号[]、花括号{})等等。最后,我们还要测试一下当文本非常长,超过图片高度时的处理情况。"
|
||
// //
|
||
// //text := longText
|
||
// //
|
||
// //err := createChineseTextImage(text, 800, 600, 20, "chinese_output.png")
|
||
// //if err != nil {
|
||
// // fmt.Printf("生成图片失败: %v\n", err)
|
||
// // fmt.Println("\n解决方案:")
|
||
// // fmt.Println("1. 下载中文字体(如思源黑体)")
|
||
// // fmt.Println("2. 修改代码中的字体路径")
|
||
// // fmt.Println("3. 将字体文件放在项目目录中")
|
||
// // return
|
||
// //}
|
||
// //
|
||
// //fmt.Println("图片已生成: chinese_output.png")
|
||
//
|
||
// img, err := loadPNG("image/123.png")
|
||
// if err != nil {
|
||
// fmt.Println(err)
|
||
// }
|
||
// // 绘制文本信息
|
||
// err = drawChineseText(img, "Go语言高级编程Go语言高级编程Go语言高级编程Go语言高级编程Go语言高级编程Go语言高级编程", "作者:王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明王小明", "出版社:人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社人民邮电出版社")
|
||
// if err != nil {
|
||
// fmt.Println(err)
|
||
// }
|
||
// err = savePNG(img, "image/book_cover.png")
|
||
// if err != nil {
|
||
// fmt.Println(err)
|
||
// }
|
||
//}
|
||
|
||
// ScaleImage 缩放图片的主函数
|
||
// imgPath: 原始图片路径
|
||
// scaleWidth: 目标宽度(像素),如果为0则按比例自动计算
|
||
// scaleHeight: 目标高度(像素),如果为0则按比例自动计算
|
||
// quality: 图片质量(1-100),仅对JPEG有效
|
||
// 返回:缩放后的图片保存路径和错误信息
|
||
func ScaleImage(imgPath string, scaleWidth, scaleHeight int, quality int) (string, error) {
|
||
// 检查文件是否存在
|
||
if _, err := os.Stat(imgPath); os.IsNotExist(err) {
|
||
return "", fmt.Errorf("图片文件不存在: %s", imgPath)
|
||
}
|
||
|
||
// 打开原始图片
|
||
file, err := os.Open(imgPath)
|
||
if err != nil {
|
||
return "", fmt.Errorf("打开图片失败: %v", err)
|
||
}
|
||
defer file.Close()
|
||
|
||
// 解码图片
|
||
srcImg, format, err := image.Decode(file)
|
||
if err != nil {
|
||
return "", fmt.Errorf("解码图片失败: %v", err)
|
||
}
|
||
|
||
// 获取原始尺寸
|
||
srcBounds := srcImg.Bounds()
|
||
srcWidth := srcBounds.Dx()
|
||
srcHeight := srcBounds.Dy()
|
||
|
||
// 计算目标尺寸(保持宽高比)
|
||
dstWidth, dstHeight := calculateTargetSize(srcWidth, srcHeight, scaleWidth, scaleHeight)
|
||
|
||
// 创建目标图片
|
||
dstImg := image.NewRGBA(image.Rect(0, 0, dstWidth, dstHeight))
|
||
|
||
// 使用高质量的缩放算法
|
||
draw.ApproxBiLinear.Scale(dstImg, dstImg.Bounds(), srcImg, srcBounds, draw.Over, nil)
|
||
|
||
// 生成保存路径
|
||
outputPath := generateOutputPath(imgPath, dstWidth, dstHeight)
|
||
|
||
// 保存图片
|
||
if err := SaveImage(outputPath, dstImg, format, quality); err != nil {
|
||
return "", fmt.Errorf("保存图片失败: %v", err)
|
||
}
|
||
|
||
return outputPath, nil
|
||
}
|
||
|
||
// ScaleImageByWidth 按宽度缩放图片(高度自动适配)
|
||
func ScaleImageByWidth(imgPath string, targetWidth int, quality int) (string, error) {
|
||
return ScaleImage(imgPath, targetWidth, 0, quality)
|
||
}
|
||
|
||
// ScaleImageByHeight 按高度缩放图片(宽度自动适配)
|
||
func ScaleImageByHeight(imgPath string, targetHeight int, quality int) (string, error) {
|
||
return ScaleImage(imgPath, 0, targetHeight, quality)
|
||
}
|
||
|
||
// ScaleImageByPercent 按百分比缩放图片
|
||
func ScaleImageByPercent(imgPath string, percent float64, quality int) (string, error) {
|
||
if percent <= 0 {
|
||
return "", fmt.Errorf("缩放百分比必须大于0")
|
||
}
|
||
|
||
// 获取原始尺寸
|
||
file, err := os.Open(imgPath)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer file.Close()
|
||
|
||
srcImg, _, err := image.Decode(file)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
srcBounds := srcImg.Bounds()
|
||
targetWidth := int(float64(srcBounds.Dx()) * percent)
|
||
targetHeight := int(float64(srcBounds.Dy()) * percent)
|
||
|
||
return ScaleImage(imgPath, targetWidth, targetHeight, quality)
|
||
}
|
||
|
||
// calculateTargetSize 计算目标尺寸,保持宽高比
|
||
func calculateTargetSize(srcWidth, srcHeight, targetWidth, targetHeight int) (int, int) {
|
||
// 如果目标尺寸都未指定,返回原尺寸
|
||
if targetWidth == 0 && targetHeight == 0 {
|
||
return srcWidth, srcHeight
|
||
}
|
||
|
||
// 按宽度缩放
|
||
if targetWidth != 0 && targetHeight == 0 {
|
||
ratio := float64(targetWidth) / float64(srcWidth)
|
||
return targetWidth, int(float64(srcHeight) * ratio)
|
||
}
|
||
|
||
// 按高度缩放
|
||
if targetHeight != 0 && targetWidth == 0 {
|
||
ratio := float64(targetHeight) / float64(srcHeight)
|
||
return int(float64(srcWidth) * ratio), targetHeight
|
||
}
|
||
|
||
// 同时指定宽高,但保持原图宽高比,居中裁剪或留白
|
||
// 这里采用完全缩放到指定尺寸(可能会变形)
|
||
// 如需保持比例,可以取消下面的注释
|
||
/*
|
||
srcRatio := float64(srcWidth) / float64(srcHeight)
|
||
dstRatio := float64(targetWidth) / float64(targetHeight)
|
||
|
||
if srcRatio > dstRatio {
|
||
// 原图更宽,按高度缩放
|
||
newHeight := targetHeight
|
||
newWidth := int(float64(targetHeight) * srcRatio)
|
||
return newWidth, newHeight
|
||
} else {
|
||
// 原图更高,按宽度缩放
|
||
newWidth := targetWidth
|
||
newHeight := int(float64(targetWidth) / srcRatio)
|
||
return newWidth, newHeight
|
||
}
|
||
*/
|
||
|
||
return targetWidth, targetHeight
|
||
}
|
||
|
||
// generateOutputPath 生成输出文件路径
|
||
func generateOutputPath(srcPath string, width, height int) string {
|
||
dir := filepath.Dir(srcPath)
|
||
ext := filepath.Ext(srcPath)
|
||
name := strings.TrimSuffix(filepath.Base(srcPath), ext)
|
||
|
||
outputName := fmt.Sprintf("%s_scaled_%dx%d%s", name, width, height, ext)
|
||
return filepath.Join(dir, outputName)
|
||
}
|
||
|
||
// saveImage 保存图片到文件
|
||
func SaveImage(path string, img image.Image, format string, quality int) error {
|
||
// 创建输出文件
|
||
outFile, err := os.Create(path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer outFile.Close()
|
||
|
||
// 根据原始格式保存
|
||
switch strings.ToLower(format) {
|
||
case "jpeg", "jpg":
|
||
opts := &jpeg.Options{Quality: quality}
|
||
return jpeg.Encode(outFile, img, opts)
|
||
case "png":
|
||
return png.Encode(outFile, img)
|
||
default:
|
||
// 默认保存为PNG格式
|
||
return png.Encode(outFile, img)
|
||
}
|
||
}
|
||
|
||
// ScaleImageWithOptions 带更多选项的缩放函数
|
||
type ScaleOptions struct {
|
||
Width int // 目标宽度
|
||
Height int // 目标高度
|
||
Quality int // 图片质量 (1-100)
|
||
OutputPath string // 自定义输出路径,为空则自动生成
|
||
KeepRatio bool // 是否保持宽高比
|
||
CropCenter bool // 是否居中裁剪(仅在KeepRatio为true且指定了宽高时有效)
|
||
}
|
||
|
||
func ScaleImageWithOptions(imgPath string, opts ScaleOptions) (string, error) {
|
||
// 参数验证
|
||
if opts.Quality < 1 || opts.Quality > 100 {
|
||
opts.Quality = 90 // 默认质量
|
||
}
|
||
|
||
// 检查文件是否存在
|
||
if _, err := os.Stat(imgPath); os.IsNotExist(err) {
|
||
return "", fmt.Errorf("图片文件不存在: %s", imgPath)
|
||
}
|
||
|
||
// 打开并解码图片
|
||
file, err := os.Open(imgPath)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer file.Close()
|
||
|
||
srcImg, format, err := image.Decode(file)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
srcBounds := srcImg.Bounds()
|
||
srcWidth := srcBounds.Dx()
|
||
srcHeight := srcBounds.Dy()
|
||
|
||
var dstWidth, dstHeight int
|
||
|
||
if opts.KeepRatio {
|
||
// 保持宽高比
|
||
dstWidth, dstHeight = calculateTargetSize(srcWidth, srcHeight, opts.Width, opts.Height)
|
||
} else {
|
||
// 不保持宽高比,直接使用指定尺寸
|
||
dstWidth, dstHeight = opts.Width, opts.Height
|
||
if dstWidth == 0 {
|
||
dstWidth = srcWidth
|
||
}
|
||
if dstHeight == 0 {
|
||
dstHeight = srcHeight
|
||
}
|
||
}
|
||
|
||
// 创建目标图片
|
||
dstImg := image.NewRGBA(image.Rect(0, 0, dstWidth, dstHeight))
|
||
|
||
// 如果需要居中裁剪
|
||
if opts.CropCenter && opts.KeepRatio && opts.Width > 0 && opts.Height > 0 {
|
||
// 计算裁剪区域
|
||
srcRatio := float64(srcWidth) / float64(srcHeight)
|
||
dstRatio := float64(opts.Width) / float64(opts.Height)
|
||
|
||
var cropRect image.Rectangle
|
||
if srcRatio > dstRatio {
|
||
// 原图更宽,裁剪宽度
|
||
cropWidth := int(float64(srcHeight) * dstRatio)
|
||
cropX := (srcWidth - cropWidth) / 2
|
||
cropRect = image.Rect(cropX, 0, cropX+cropWidth, srcHeight)
|
||
} else {
|
||
// 原图更高,裁剪高度
|
||
cropHeight := int(float64(srcWidth) / dstRatio)
|
||
cropY := (srcHeight - cropHeight) / 2
|
||
cropRect = image.Rect(0, cropY, srcWidth, cropY+cropHeight)
|
||
}
|
||
|
||
// 先裁剪,再缩放
|
||
croppedImg := srcImg.(interface {
|
||
SubImage(r image.Rectangle) image.Image
|
||
}).SubImage(cropRect)
|
||
|
||
draw.ApproxBiLinear.Scale(dstImg, dstImg.Bounds(), croppedImg, croppedImg.Bounds(), draw.Over, nil)
|
||
} else {
|
||
// 直接缩放
|
||
draw.ApproxBiLinear.Scale(dstImg, dstImg.Bounds(), srcImg, srcBounds, draw.Over, nil)
|
||
}
|
||
|
||
// 确定输出路径
|
||
outputPath := opts.OutputPath
|
||
if outputPath == "" {
|
||
outputPath = generateOutputPath(imgPath, dstWidth, dstHeight)
|
||
}
|
||
|
||
// 保存图片
|
||
if err := SaveImage(outputPath, dstImg, format, opts.Quality); err != nil {
|
||
return "", err
|
||
}
|
||
|
||
return outputPath, nil
|
||
}
|