名称: 图像优化 描述: 用于实现响应式图像、格式转换、焦点裁剪或图像处理管道时使用。涵盖srcset生成、WebP/AVIF转换、懒加载和用于无头CMS的图像转换API。 允许工具: 读取, 全局, 搜索, 任务, 技能
图像优化
关于为无头CMS实现响应式图像、格式优化和图像处理管道的指南。
何时使用此技能
- 实现响应式图像交付
- 将图像转换为现代格式(WebP、AVIF)
- 构建图像处理管道
- 实现焦点裁剪
- 优化图像加载性能
响应式图像模式
Srcset用于分辨率切换
<!-- 基本srcset带有宽度描述符 -->
<img
src="/images/hero.jpg"
srcset="
/images/hero-400w.jpg 400w,
/images/hero-800w.jpg 800w,
/images/hero-1200w.jpg 1200w,
/images/hero-1600w.jpg 1600w
"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 800px"
alt="英雄图像"
>
<!-- Srcset带有像素密度 -->
<img
src="/images/logo.png"
srcset="
/images/logo.png 1x,
/images/logo@2x.png 2x,
/images/logo@3x.png 3x
"
alt="徽标"
>
Picture元素用于艺术指导
<picture>
<!-- 移动端:方形裁剪 -->
<source
media="(max-width: 600px)"
srcset="/images/hero-mobile.webp"
type="image/webp"
>
<source
media="(max-width: 600px)"
srcset="/images/hero-mobile.jpg"
>
<!-- 桌面端:宽幅裁剪 -->
<source
srcset="/images/hero-desktop.webp"
type="image/webp"
>
<!-- 回退 -->
<img src="/images/hero-desktop.jpg" alt="英雄">
</picture>
图像处理服务
核心处理
public class ImageProcessingService
{
public async Task<Stream> ProcessAsync(
Stream source,
ImageProcessingOptions options)
{
using var image = await Image.LoadAsync(source);
// 调整大小
if (options.Width.HasValue || options.Height.HasValue)
{
var resizeOptions = new ResizeOptions
{
Size = new Size(
options.Width ?? 0,
options.Height ?? 0),
Mode = options.ResizeMode,
Position = options.FocalPoint != null
? CalculateAnchor(options.FocalPoint, image.Size)
: AnchorPositionMode.Center
};
image.Mutate(x => x.Resize(resizeOptions));
}
// 应用效果
if (options.Blur > 0)
{
image.Mutate(x => x.GaussianBlur(options.Blur));
}
if (options.Grayscale)
{
image.Mutate(x => x.Grayscale());
}
// 编码为目标格式
var outputStream = new MemoryStream();
await EncodeAsync(image, outputStream, options);
outputStream.Position = 0;
return outputStream;
}
private async Task EncodeAsync(
Image image,
Stream output,
ImageProcessingOptions options)
{
switch (options.Format)
{
case ImageFormat.WebP:
await image.SaveAsWebpAsync(output, new WebpEncoder
{
Quality = options.Quality,
Method = WebpEncodingMethod.BestQuality
});
break;
case ImageFormat.Avif:
await image.SaveAsAvifAsync(output, new AvifEncoder
{
Quality = options.Quality
});
break;
case ImageFormat.Jpeg:
await image.SaveAsJpegAsync(output, new JpegEncoder
{
Quality = options.Quality,
ColorType = JpegEncodingColor.YCbCrRatio420
});
break;
case ImageFormat.Png:
await image.SaveAsPngAsync(output, new PngEncoder
{
CompressionLevel = PngCompressionLevel.BestCompression
});
break;
}
}
}
public class ImageProcessingOptions
{
public int? Width { get; set; }
public int? Height { get; set; }
public ResizeMode ResizeMode { get; set; } = ResizeMode.Max;
public FocalPoint? FocalPoint { get; set; }
public ImageFormat Format { get; set; } = ImageFormat.WebP;
public int Quality { get; set; } = 80;
public float Blur { get; set; }
public bool Grayscale { get; set; }
}
public class FocalPoint
{
public float X { get; set; } // 0-1, 从左到右
public float Y { get; set; } // 0-1, 从上到下
}
public enum ImageFormat
{
Jpeg,
Png,
WebP,
Avif,
Gif
}
基于预设的处理
public class ImagePresets
{
public static readonly Dictionary<string, ImageProcessingOptions> Presets = new()
{
["缩略图"] = new()
{
Width = 150,
Height = 150,
ResizeMode = ResizeMode.Crop,
Format = ImageFormat.WebP,
Quality = 75
},
["卡片"] = new()
{
Width = 400,
Height = 300,
ResizeMode = ResizeMode.Crop,
Format = ImageFormat.WebP,
Quality = 80
},
["英雄"] = new()
{
Width = 1920,
Height = 800,
ResizeMode = ResizeMode.Crop,
Format = ImageFormat.WebP,
Quality = 85
},
["开放图谱图像"] = new()
{
Width = 1200,
Height = 630,
ResizeMode = ResizeMode.Crop,
Format = ImageFormat.Jpeg,
Quality = 90
},
["模糊占位符"] = new()
{
Width = 20,
Height = 20,
Format = ImageFormat.Jpeg,
Quality = 30,
Blur = 5
}
};
}
焦点裁剪
焦点模型
public class FocalPointService
{
public AnchorPositionMode CalculateAnchor(
FocalPoint focal,
Size imageSize,
Size targetSize)
{
// 计算图像裁剪部分
var imageAspect = (float)imageSize.Width / imageSize.Height;
var targetAspect = (float)targetSize.Width / targetSize.Height;
if (Math.Abs(imageAspect - targetAspect) < 0.01f)
{
return AnchorPositionMode.Center;
}
// 确定裁剪方向
var cropHorizontal = imageAspect > targetAspect;
if (cropHorizontal)
{
// 裁剪侧面,使用X焦点
return focal.X switch
{
< 0.33f => AnchorPositionMode.Left,
> 0.66f => AnchorPositionMode.Right,
_ => AnchorPositionMode.Center
};
}
else
{
// 裁剪顶部/底部,使用Y焦点
return focal.Y switch
{
< 0.33f => AnchorPositionMode.Top,
> 0.66f => AnchorPositionMode.Bottom,
_ => AnchorPositionMode.Center
};
}
}
}
存储焦点
public class MediaItem
{
public Guid Id { get; set; }
public string FileName { get; set; } = string.Empty;
// 用于智能裁剪的焦点
public FocalPoint? FocalPoint { get; set; }
}
// 通过API设置
PATCH /api/media/{id}
{
"focalPoint": { "x": 0.3, "y": 0.2 }
}
即时图像转换
基于URL的转换
# 基础URL
https://cdn.example.com/media/hero.jpg
# 带转换
https://cdn.example.com/media/hero.jpg?w=800&h=600&fit=crop
https://cdn.example.com/media/hero.jpg?w=400&format=webp&q=80
https://cdn.example.com/media/hero.jpg?preset=thumbnail
转换端点
[Route("media/{*path}")]
public class ImageTransformController : ControllerBase
{
[HttpGet]
[ResponseCache(Duration = 31536000, VaryByQueryKeys = new[] { "*" })]
public async Task<IActionResult> GetTransformed(
string path,
[FromQuery] int? w,
[FromQuery] int? h,
[FromQuery] string? format,
[FromQuery] int? q,
[FromQuery] string? fit,
[FromQuery] string? preset)
{
// 获取原始图像
var original = await _mediaService.GetStreamAsync(path);
if (original == null) return NotFound();
// 从查询或预设构建选项
var options = preset != null
? ImagePresets.Presets.GetValueOrDefault(preset) ?? new()
: new ImageProcessingOptions
{
Width = w,
Height = h,
Format = ParseFormat(format),
Quality = q ?? 80,
ResizeMode = ParseFit(fit)
};
// 处理图像
var processed = await _imageProcessor.ProcessAsync(original, options);
return File(processed, GetMimeType(options.Format));
}
}
Srcset生成
响应式图像服务
public class ResponsiveImageService
{
private readonly int[] _defaultWidths = { 320, 640, 960, 1280, 1920 };
public ResponsiveImageData GenerateSrcset(
MediaItem media,
ResponsiveImageOptions? options = null)
{
options ??= new ResponsiveImageOptions();
var widths = options.Widths ?? _defaultWidths;
var srcset = widths
.Where(w => w <= (media.Metadata.Width ?? int.MaxValue))
.Select(w => new SrcsetEntry
{
Url = BuildTransformUrl(media, w, options.Format),
Width = w
})
.ToList();
return new ResponsiveImageData
{
Src = BuildTransformUrl(media, options.DefaultWidth, options.Format),
Srcset = srcset,
Sizes = options.Sizes ?? "(max-width: 1200px) 100vw, 1200px",
Alt = media.Metadata.Alt ?? "",
Width = media.Metadata.Width,
Height = media.Metadata.Height,
BlurDataUrl = options.IncludeBlurPlaceholder
? BuildTransformUrl(media, 20, ImageFormat.Jpeg, blur: 5)
: null
};
}
private string BuildTransformUrl(
MediaItem media,
int width,
ImageFormat format,
int? blur = null)
{
var query = $"?w={width}&format={format.ToString().ToLower()}";
if (blur.HasValue) query += $"&blur={blur}";
return $"{_cdnBaseUrl}/media/{media.StoragePath}{query}";
}
}
public class ResponsiveImageData
{
public string Src { get; set; } = string.Empty;
public List<SrcsetEntry> Srcset { get; set; } = new();
public string Sizes { get; set; } = string.Empty;
public string Alt { get; set; } = string.Empty;
public int? Width { get; set; }
public int? Height { get; set; }
public string? BlurDataUrl { get; set; }
}
public class SrcsetEntry
{
public string Url { get; set; } = string.Empty;
public int Width { get; set; }
public override string ToString() => $"{Url} {Width}w";
}
低质量图像占位符(LQIP)
模糊哈希生成
public class BlurHashService
{
public string GenerateBlurHash(Image image, int componentsX = 4, int componentsY = 3)
{
// 调整到小尺寸用于哈希计算
using var small = image.Clone(x => x.Resize(32, 32));
// 计算模糊哈希
var pixels = new Pixel[32, 32];
for (var y = 0; y < 32; y++)
{
for (var x = 0; x < 32; x++)
{
var pixel = small[x, y];
pixels[x, y] = new Pixel(pixel.R, pixel.G, pixel.B);
}
}
return BlurHash.Encode(pixels, componentsX, componentsY);
}
}
占位符策略
| 策略 | 大小 | 质量 | 使用场景 |
|---|---|---|---|
| 模糊哈希 | ~30字符 | 好 | HTML内联 |
| 微小JPEG | ~500字节 | 中 | 数据URL |
| 主导颜色 | 7字符 | 简单 | CSS背景 |
| SVG追踪 | ~2KB | 好 | 艺术网站 |
性能最佳实践
懒加载
<!-- 原生懒加载 -->
<img
src="/images/photo.jpg"
loading="lazy"
decoding="async"
alt="照片"
>
<!-- 带模糊占位符 -->
<div class="image-container" style="background: url(data:image/jpeg;base64,...)">
<img
src="/images/photo.jpg"
loading="lazy"
onload="this.parentElement.style.background = 'none'"
alt="照片"
>
</div>
格式选择
public ImageFormat SelectOptimalFormat(string acceptHeader, ImageFormat preferred)
{
if (acceptHeader.Contains("image/avif"))
return ImageFormat.Avif;
if (acceptHeader.Contains("image/webp"))
return ImageFormat.WebP;
return preferred;
}
图像API响应
{
"data": {
"id": "media-123",
"original": {
"url": "https://cdn.example.com/media/original/hero.jpg",
"width": 3840,
"height": 2160,
"mimeType": "image/jpeg",
"sizeBytes": 2456789
},
"responsive": {
"src": "https://cdn.example.com/media/hero.jpg?w=1280&format=webp",
"srcset": "https://cdn.example.com/media/hero.jpg?w=320&format=webp 320w, ...",
"sizes": "(max-width: 1200px) 100vw, 1200px"
},
"placeholder": {
"blurHash": "LEHV6nWB2yk8pyo0adR*.7kCMdnj",
"dataUrl": "data:image/jpeg;base64,/9j/4AAQ..."
},
"focalPoint": { "x": 0.5, "y": 0.3 }
}
}
相关技能
媒体资产管理- 媒体库和上传CDN媒体交付- CDN集成和缓存无头API设计- 图像API端点