YaeBlog/YaeBlog/Services/ImageCompressService.cs
jackfiled 3126005731
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 49s
feat: 图片压缩命令 (#10)
将图片压缩为webp格式减少流量使用和磁盘占用
Reviewed-on: #10
2025-03-25 15:00:18 +08:00

120 lines
4.4 KiB
C#

using Imageflow.Fluent;
using YaeBlog.Abstraction;
using YaeBlog.Core.Exceptions;
using YaeBlog.Models;
namespace YaeBlog.Services;
public sealed class ImageCompressService(IEssayScanService essayScanService, ILogger<ImageCompressService> logger)
{
private record struct CompressResult(BlogImageInfo ImageInfo, byte[] CompressContent);
public async Task<List<BlogImageInfo>> ScanUsedImages()
{
BlogContents contents = await essayScanService.ScanContents();
List<BlogImageInfo> originalImages = (from content in contents.Posts.Concat(contents.Drafts)
from image in content.Images
where image.IsUsed
select image).ToList();
originalImages.Sort();
return originalImages;
}
public async Task Compress(bool dryRun)
{
BlogContents contents = await essayScanService.ScanContents();
// 筛选需要压缩的图片
// 即图片被博客使用且是jpeg/png格式
List<BlogContent> needCompressContents = (from content in contents
where content.Images.Any(i => i is { IsUsed: true } and { File.Extension: ".jpg" or ".jpeg" or ".png" })
select content).ToList();
if (needCompressContents.Count == 0)
{
return;
}
int uncompressedSize = 0;
int compressedSize = 0;
List<BlogContent> compressedContent = new(needCompressContents.Count);
foreach (BlogContent content in needCompressContents)
{
List<BlogImageInfo> uncompressedImages = (from image in content.Images
where image is { IsUsed: true } and { File.Extension: ".jpg" or ".jpeg" or ".png" }
select image).ToList();
uncompressedSize += uncompressedImages.Select(i => i.Size).Sum();
foreach (BlogImageInfo image in uncompressedImages)
{
logger.LogInformation("Uncompressed image: {} belonging to blog {}.", image.File.Name,
content.BlogName);
}
CompressResult[] compressedImages = (await Task.WhenAll(from image in uncompressedImages
select Task.Run(async () => new CompressResult(image, await ConvertToWebp(image))))).ToArray();
compressedSize += compressedImages.Select(i => i.CompressContent.Length).Sum();
// 直接在原有的图片列表上添加图片
List<BlogImageInfo> images = content.Images.Concat(from r in compressedImages
select r.ImageInfo with
{
File = new FileInfo(r.ImageInfo.File.FullName.Split('.')[0] + ".webp"),
Content = r.CompressContent,
MineType = "image/webp"
}).ToList();
// 修改文本
string blogContent = compressedImages.Aggregate(content.Content, (c, r) =>
{
string originalName = r.ImageInfo.File.Name;
string outputName = originalName.Split('.')[0] + ".webp";
return c.Replace(originalName, outputName);
});
compressedContent.Add(content with { Images = images, Content = blogContent });
}
logger.LogInformation("Compression ratio: {}%.", (double)compressedSize / uncompressedSize * 100.0);
if (dryRun is false)
{
await Task.WhenAll(from content in compressedContent
select essayScanService.SaveBlogContent(content, content.IsDraft));
}
}
private static async Task<byte[]> ConvertToWebp(BlogImageInfo image)
{
using ImageJob job = new();
BuildJobResult result = await job.Decode(MemorySource.Borrow(image.Content))
.Branch(f => f.EncodeToBytes(new WebPLosslessEncoder()))
.EncodeToBytes(new WebPLossyEncoder(75))
.Finish()
.InProcessAsync();
// 超过128KB的图片使用有损压缩
// 反之使用无损压缩
ArraySegment<byte>? losslessImage = result.TryGet(1)?.TryGetBytes();
ArraySegment<byte>? lossyImage = result.TryGet(2)?.TryGetBytes();
if (image.Size <= 128 * 1024 && losslessImage.HasValue)
{
return losslessImage.Value.ToArray();
}
if (lossyImage.HasValue)
{
return lossyImage.Value.ToArray();
}
throw new BlogCommandException($"Failed to convert {image.File.Name} to webp format: return value is null.");
}
}