feat(api,image): 智能缩略图与大图瓦片支持

- ImageAPI 增加智能缩略图尺寸选择(按文件大小分级)\n- 检测超大图片时触发 dzsave 瓦片生成,常规缩略图兜底\n- 统一缩略图与转换路径构建;Vips/Imagemagick 互为兜底\n- 便于前端大图渐进加载,降低内存占用
This commit is contained in:
jimmy committed 2025-10-07 14:31:34 +08:00
1 parent b8693dc547
commit 9e237cb4fe
2 files changed
+283 -4

No files matched your search

+167 -4
View File
@@ -479,8 +479,36 @@ func (api *ImageAPI) ProcessImage() error {
} }
logger.Info("格式转换完成", zap.String("path", supportedPath)) logger.Info("格式转换完成", zap.String("path", supportedPath))
// 3. 生成缩略图 // 3. 获取图像信息以判断处理策略
if err := api.generateAllThumbnails(ctx); err != nil { imageInfo, err := tools.GetVipsImageInfo(ctx, supportedPath)
if err != nil {
return fmt.Errorf("获取图像信息失败: %w", err)
}
// 获取文件大小
fileInfo, err := os.Stat(api.path)
if err != nil {
return fmt.Errorf("获取文件信息失败: %w", err)
}
fileSize := fileInfo.Size()
// 4. 根据文件大小和尺寸决定处理策略
if api.ShouldGenerateTiles(fileSize, imageInfo.Width, imageInfo.Height) {
// 超大图:生成瓦片
logger.Info("检测到超大图片,将生成瓦片",
zap.String("path", api.path),
zap.Int64("file_size", fileSize),
zap.Int("width", imageInfo.Width),
zap.Int("height", imageInfo.Height))
if err := api.GenerateTiles(ctx); err != nil {
logger.Error("瓦片生成失败", zap.Error(err))
// 瓦片生成失败,继续生成常规缩略图
}
}
// 5. 生成缩略图(使用智能策略)
if err := api.generateSmartThumbnails(ctx, fileSize); err != nil {
return fmt.Errorf("生成缩略图失败: %w", err) return fmt.Errorf("生成缩略图失败: %w", err)
} }
@@ -511,6 +539,35 @@ func (api *ImageAPI) generateAllThumbnails(ctx context.Context) error {
return nil return nil
} }
// generateSmartThumbnails 根据文件大小智能生成缩略图
func (api *ImageAPI) generateSmartThumbnails(ctx context.Context, fileSize int64) error {
// 使用智能策略获取需要生成的缩略图尺寸
thumbnailSizes := api.GetSmartThumbnailSizes(fileSize)
logger.Info("开始生成智能缩略图",
zap.String("path", api.path),
zap.Int64("file_size", fileSize),
zap.Ints("sizes", thumbnailSizes))
for _, size := range thumbnailSizes {
thumbnailPath, err := api.GetImagePath(size)
if err != nil {
logger.Warn("生成缩略图失败",
zap.String("path", api.path),
zap.Int("size", size),
zap.Error(err))
continue // 某个尺寸失败不影响其他尺寸
}
logger.Debug("生成缩略图完成",
zap.String("original", api.path),
zap.String("thumbnail", thumbnailPath),
zap.Int("size", size))
}
return nil
}
// getThumbnailSizes 获取需要预生成的缩略图尺寸 // getThumbnailSizes 获取需要预生成的缩略图尺寸
func (api *ImageAPI) getThumbnailSizes() []int { func (api *ImageAPI) getThumbnailSizes() []int {
// 从配置中获取,如果没有配置则使用默认值 // 从配置中获取,如果没有配置则使用默认值
@@ -518,11 +575,117 @@ func (api *ImageAPI) getThumbnailSizes() []int {
return config.CONFIG.ImageCompressionOption.ThumbnailSize return config.CONFIG.ImageCompressionOption.ThumbnailSize
} }
// 默认缩略图尺寸 // 默认缩略图尺寸 - 多级渐进式加载
return []int{800} // 512: 预览缩略图, 1080: 中等画质, 2048: 高清, 4096: 超高清
return []int{512, 1080, 2048, 4096}
}
// GetSmartThumbnailSizes 根据文件大小智能选择缩略图尺寸
// fileSize: 文件大小(字节)
// 返回: 需要生成的缩略图尺寸列表
func (api *ImageAPI) GetSmartThumbnailSizes(fileSize int64) []int {
const MB = 1024 * 1024
// <1MB: 只生成一个小缩略图用于列表显示
if fileSize < MB {
return []int{512}
}
// 1-5MB: 生成两级缩略图
if fileSize < 5*MB {
return []int{512, 1080}
}
// 5-20MB: 生成三级缩略图(渐进式加载)
if fileSize < 20*MB {
return []int{512, 1080, 2048}
}
// 20MB以上: 生成完整的四级缩略图
return []int{512, 1080, 2048, 4096}
}
// ShouldGenerateTiles 判断是否应该生成瓦片
// 对于超大图片(>50MB或尺寸>10000px),使用瓦片方式加载性能更好
func (api *ImageAPI) ShouldGenerateTiles(fileSize int64, width, height int) bool {
const (
SIZE_THRESHOLD = 50 * 1024 * 1024 // 50MB
DIMENSION_THRESHOLD = 10000 // 10000像素
)
return fileSize > SIZE_THRESHOLD ||
width > DIMENSION_THRESHOLD ||
height > DIMENSION_THRESHOLD
}
// GenerateTiles 为超大图生成瓦片
func (api *ImageAPI) GenerateTiles(ctx context.Context) error {
// 获取原图路径
supportedPath, err := api.GetSupportOriginalImagePath(ctx)
if err != nil {
return fmt.Errorf("获取原图路径失败: %w", err)
}
// 计算瓦片输出目录
tilesDir := api.GetTilesDirectory()
if err := os.MkdirAll(tilesDir, 0755); err != nil {
return fmt.Errorf("创建瓦片目录失败: %w", err)
}
// 生成瓦片
options := &tools.TileGenerationOptions{
TileSize: 256,
Overlap: 1,
Quality: config.CONFIG.ImageCompressionOption.ThumbnailQuality,
OutputDir: filepath.Join(tilesDir, "tiles"),
Layout: "dz",
}
err = tools.GenerateTiles(ctx, supportedPath, options)
if err != nil {
return fmt.Errorf("生成瓦片失败: %w", err)
}
logger.Info("瓦片生成成功",
zap.String("hash", api.hash),
zap.String("tiles_dir", tilesDir))
return nil
}
// GetTilesDirectory 获取瓦片存储目录
func (api *ImageAPI) GetTilesDirectory() string {
thumbnailBasePath := filepath.Join(config.CONFIG.AppDir, config.CONFIG.PathConfig.CachePath, config.CONFIG.PathConfig.ThumbnailPath)
return utils.HashUtils.HashThumbPath(thumbnailBasePath, api.hash, "tiles", "")
}
// HasTiles 检查是否已生成瓦片
func (api *ImageAPI) HasTiles() bool {
tilesDir := api.GetTilesDirectory()
dziFile := filepath.Join(tilesDir, "tiles.dzi")
return utils.FileUtils.Exists(dziFile)
} }
// CompressedImg 压缩图片获取路径 // CompressedImg 压缩图片获取路径
func (api *ImageAPI) CompressedImg(longSide int) (string, error) { func (api *ImageAPI) CompressedImg(longSide int) (string, error) {
return api.GetImagePath(longSide) return api.GetImagePath(longSide)
} }
// ReadFileWithLimit 读取文件内容(限制大小)
func ReadFileWithLimit(filePath string, maxSize int64) ([]byte, error) {
fileInfo, err := os.Stat(filePath)
if err != nil {
return nil, fmt.Errorf("获取文件信息失败: %w", err)
}
if fileInfo.Size() > maxSize {
return nil, fmt.Errorf("文件过大: %d bytes (max: %d)", fileInfo.Size(), maxSize)
}
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("读取文件失败: %w", err)
}
return data, nil
}
@@ -459,3 +459,119 @@ func QuickSmartCrop(ctx context.Context, inputPath, outputPath string, width, he
} }
return CropVipsImage(ctx, inputPath, outputPath, width, height, options) return CropVipsImage(ctx, inputPath, outputPath, width, height, options)
} }
// TileGenerationOptions 瓦片生成选项
type TileGenerationOptions struct {
TileSize int // 瓦片大小(正方形边长,默认256)
Overlap int // 瓦片重叠像素(默认0)
Layout string // 布局方式: "dz" (DeepZoom), "google", "zoomify"
Quality int // JPEG质量(1-100)
OutputDir string // 输出目录
Background string // 背景颜色(用于填充不完整的瓦片)
}
// GenerateTiles 生成图片瓦片(用于超大图的渐进式加载)
// 使用 Deep Zoom Image (DZI) 格式
func GenerateTiles(ctx context.Context, inputPath string, options *TileGenerationOptions) error {
if err := utils.EnsureInitialized(nil, nil); err != nil {
return err
}
if options == nil {
options = &TileGenerationOptions{
TileSize: 256,
Quality: 85,
Layout: "dz",
}
}
// 设置默认值
if options.TileSize <= 0 {
options.TileSize = 256
}
if options.Quality <= 0 {
options.Quality = 85
}
if options.Layout == "" {
options.Layout = "dz"
}
// 构建vips dzsave命令参数
args := []string{"dzsave", inputPath, options.OutputDir}
// 添加瓦片大小参数
args = append(args, "--tile-size", strconv.Itoa(options.TileSize))
// 添加重叠参数
if options.Overlap > 0 {
args = append(args, "--overlap", strconv.Itoa(options.Overlap))
}
// 添加质量参数
if options.Quality > 0 {
args = append(args, "--Q", strconv.Itoa(options.Quality))
}
// 添加布局参数
switch options.Layout {
case "google":
args = append(args, "--layout", "google")
case "zoomify":
args = append(args, "--layout", "zoomify")
default:
args = append(args, "--layout", "dz")
}
// 添加背景颜色(如果指定)
if options.Background != "" {
args = append(args, "--background", options.Background)
}
result, err := utils.ExecuteCommand(ctx, utils.VipsPath, args...)
if err != nil {
return fmt.Errorf("vips dzsave failed: %w, stderr: %s", err, string(result.Stderr))
}
return nil
}
// GenerateProgressive 生成渐进式JPEG(用于网络传输优化)
func GenerateProgressive(ctx context.Context, inputPath, outputPath string, quality int) error {
if err := utils.EnsureInitialized(nil, nil); err != nil {
return err
}
if quality <= 0 {
quality = 85
}
args := []string{"jpegsave", inputPath, outputPath,
"--Q", strconv.Itoa(quality),
"--interlace", // 渐进式JPEG
"--optimize-coding", // 优化编码
"--strip", // 移除元数据
}
result, err := utils.ExecuteCommand(ctx, utils.VipsPath, args...)
if err != nil {
return fmt.Errorf("vips progressive jpeg failed: %w, stderr: %s", err, string(result.Stderr))
}
return nil
}
// GetImageLevels 计算图片金字塔层级数(用于瓦片生成)
func GetImageLevels(width, height, tileSize int) int {
maxDimension := width
if height > maxDimension {
maxDimension = height
}
levels := 0
for maxDimension > tileSize {
maxDimension = maxDimension / 2
levels++
}
return levels + 1
}