ASP.NET Core Library – ImageSharp


前言

2021 年就寫過一篇了, Asp.net core 學習筆記 Image processing (ImageSharp), 只是那時還是舊的寫法, 這篇作為翻新和以后繼續增加新功能的介紹.

ImageSharp 是 .NET 平台開源的圖片處理 Library. 完全用 C# 來寫, 從 0 開始寫. 寫了很多年, 目前算是比較 ok 用了. 2022 年開始也支持 webp 了哦.

 

參考

官網 Docs

 

安裝

dotnet add package SixLabors.ImageSharp

 

查看圖片信息

var fileFullPath = @"C:\keatkeat\projects\stooges-lib\stooges-aspnetcore\Project\wwwroot\uploaded-files\vertical-huawei.jpg";
using var image = Image.Load(fileFullPath);
var w = image.Width;
var h = image.Height;
var exifOrientation = image.Metadata.ExifProfile.GetValue(ExifTag.Orientation);

手機圖片經常會出現反轉問題, 可以查看 Exif Orientation 信息. 關於這個問題可以看我之前寫的: Image Exif Orientation 圖片方向信息

 

修改圖片

using var newImage = image.Clone(imageProcessing =>
{
    imageProcessing.AutoOrient();
    imageProcessing.Resize((int)Decimal.Divide(image.Width, 2), (int)Decimal.Divide(image.Width, 2));
    imageProcessing.Crop(new Rectangle(x: 150, y: 150, width: 150, height: 150));
    imageProcessing.Rotate(RotateMode.Rotate90);
    imageProcessing.Flip(FlipMode.Vertical);
});
newImage.SaveAsJpeg(
    @"C:\keatkeat\projects\stooges-lib\stooges-aspnetcore\Project\wwwroot\uploaded-files\vertical-huawei-croped.jpg", new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder
    {
        Quality = 85
    }
);

Clone() 是復制一張來做修改, 如果想改原圖拿就用 .Mutate(). 注: clone 記得使用 using 哦

AutoOrient 就是依據 Exif Orientation 來旋轉和 flip 圖片, 超方便的. 不需要自己搞, 轉換之后 Exif Orientation 會變成 1 (哪怕之前是 0, 比如華為手機是 0)

resize, crop, rotate, flip 也是常見的圖片操作, 還有一個 crop avatar 的 Github Sample.

修改 background color

png to jpg 默認 background color 是黑色的. 參考: Converting Png to Jpg give a color background and not white (Asp.Net Core)

var clonedImage = image.Clone(imageProcessing =>
{
    imageProcessing.BackgroundColor(Color.White);
});

想保存為 webp, 調用 SaveAsWebp 就可以了哦.

await image.SaveAsWebpAsync(Path.Combine(rootPath, "tifa2.webp"));

注: SaveAs... 是可以多次調用的. 它內部有處理好 stream reading 了.

 

水印 Watermark

水印的做法是在一張圖上面, 添加上另一張圖, 同時第二張圖需要帶有點 opacity 的效果.

using var tifa = await Image.LoadAsync(@"C:\Users\keatk\Desktop\temp\review-avatar\tifa.jpg"); // 原圖
using var logo = await Image.LoadAsync(@"C:\Users\keatk\Desktop\temp\review-avatar\Stooges Logo.png"); // 水印
// Clone 做出 new image
using var newImage = tifa.Clone(imageProcessing =>
{
    logo.Mutate(x => x.StgResizeWidth(200)); // 縮小 logo (這個不是必須的, 剛巧我的圖片比較大而已)
    imageProcessing.DrawImage(
        logo,
        opacity: 0.5f,
        location: new Point(100, 100)
    );
    // imageProcessing 就是原圖
    // DrawImage 就是在圖上畫畫
    // logo 就是把水印畫上去的意思
    // opacity 給 logo 加上透明度
    // location 是 x,y 坐標, 看你想把水印打到哪里
});
await newImage.SaveAsJpegAsync(@"C:\Users\keatk\Desktop\temp\review-avatar\new-image.jpg"); // 保存

效果

 

New Image for object-fit: content 效果

CSS object-fit 效果, 把一張大圖按比例縮小到框框里, 並且保留所有圖片信息 (留白)

using var tifa = await Image.LoadAsync(@"C:\Users\keatk\Desktop\temp\review-avatar\tifa.jpg");
using var newImage = new Image<Rgba32>(500, 500, backgroundColor: Rgba32.ParseHex("#fff")); // hex 或者 Color.White 都可以
tifa.Mutate(imageProcessing =>
{
    imageProcessing.Resize(width: 500, height: 500 * tifa.Height / tifa.Width); // 按比例縮小
});
newImage.Mutate(imageProcessing =>
{
    // 畫到中間去
    imageProcessing.DrawImage(tifa, location: new Point(0, (500 / 2) - (tifa.Height / 2)), opacity: 1);
});
await newImage.SaveAsJpegAsync(@"C:\Users\keatk\Desktop\temp\review-avatar\tifa-contain.jpg");

效果

 

 

擴展 ImageProcessing

雖然 imageProcessing 已經有很多好用的接口了, 但是不夠上層.

resize width keep aspect ratio

比如我有一張圖, dimension 是 1349 x 761

我想把它 resize to width 500, aspect ratio 保持

那可以這樣寫

var clonedImage = image.Clone(imageProcessing =>
{
    imageProcessing.Resize(width: 500, height: 500 * image.Height / image.Width);
});

它的缺點就是要寫計算. 這就是所謂的不夠上層. 那我們自己封裝一下

封裝 extensions

調用

var clonedImage = image.Clone(imageProcessing =>
{
    imageProcessing.MyResizeWidth(500);
});
extensions
public static class ImageProcessingExtensions
{
    public static IImageProcessingContext MyResizeWidth(this IImageProcessingContext imageProcessing, int width)
    {
        imageProcessing.Resize(width, height: width * originalImage.Height / originalImage.Width);
        return imageProcessing;
    }
}

這里遇到了一個問題, 算法需要 original image 的 dimension. 上面我們用了閉包才拿到的. 封裝以后就拿不到了.

難不成要通過 parameter 傳進來 (這樣接口調用就扣分了丫). 還是可以從 IImageProcessingContext 里獲取呢? 

在 IImageProcessingContext 如何獲取 Image

源碼追蹤

首先去看看文檔, 沒發現類似的案例. 也不奇怪啦. 大部分文檔都對擴展不友好的. 還是翻源碼看看唄.

GetCurrentSize 最接近, 嗯... 通常 current size 已經足夠我們用了, 但既然來了, 就再看看能不能拿到 original image 唄.

我們繼續翻 Clone 的源碼

AcceptVisitor 調用了 Accept

Accept 又調用了 visitor 的 Visit 並把 Image 自己傳進去. 

 繼續看 Visitor 的 visit 

到這里還算順利, 我們要的 Image 最終有傳入到 ImageProcessing 里頭. 那樣我們就可以通過 ImageProcessing 找回 Image 了.

這個 DefaultImageProcessorContext 就是我們最終使用的 ProcessingImage 了, 反射來確認一下

最后就是看這個 ProcessingImage 初始化, 看它把 Image 藏去哪里了.

很遺憾, 我們要的 Image 被放到了 private field. 沒有任何接口可以拿到.

黑科技 – 反射獲取 private field

雖然沒有接口沒有公開, 但可以通過反射, 強制去獲取 private field

public static class ImageProcessingExtensions
{
    public static Image MyGetOriginalImage(this IImageProcessingContext imageProcessing)
    {var sourceField = imageProcessing.GetType().GetField("source", BindingFlags.NonPublic | BindingFlags.Instance)!;
        return (sourceField.GetValue(imageProcessing) as Image)!;
    }
    public static IImageProcessingContext ResizeWidth(this IImageProcessingContext imageProcessing, int width)
    {
        var originalImage = imageProcessing.MyGetOriginalImage();
        imageProcessing.Resize(width, height: width * originalImage.Height / originalImage.Width);
        return imageProcessing;
    }
}

這樣就可以了. 但是要記得哦, 這個是 hacking way, 每次升級都有可能引發 unknown breaking change. 一定要特別注意哦.

p.s. rezie 用 current size 通常才是正確的需求, 上面拿 original 只是隨便舉個例子而已.

 

寫字 DrawText

參考:

Docs – Fonts

Github – Sample

How to add text to an image with C# in dotnet

寫字功能目前還處於 beta version, 需要用到 2 個的插件, SixLabors.Fonts 和 SixLabors.ImageSharp.Drawing

dotnet add package SixLabors.Fonts --version 1.0.0-beta19
dotnet add package SixLabors.ImageSharp.Drawing --version 1.0.0-beta15

下載字體

.ttf 肯定可以, .otf 我沒有測試. 我是用 Google Font 做測試.

Load Image

public class Program
{
    public static void Main()
    {
        var rootPath = Path.Combine(AppContext.BaseDirectory, @"..\..\..\");
        var yangmi = new FileInfo(Path.Combine(rootPath, "yangmi.jpg"));
        using var image = Image.Load(yangmi.FullName);
    }
}

Setup Font

var collection = new FontCollection();
var family = collection.Add(Path.Combine(rootPath, "Caveat-Bold.ttf"));
var font = family.CreateFont(128, FontStyle.Bold);

導入字體, 並且設置 font style

{
  font-family: 'Caveat';
  font-size: 128px;
  font-weight: 700;
}

不想導入, 也可以用 system font

var font = SystemFonts.CreateFont("Times New Roman", 128, FontStyle.Bold);

Setup Font Drawing Options

var textOptions = new TextOptions(font)
{
    Origin = new PointF(64, 64),
    WrappingLength = 3840f * 0.25f,
    HorizontalAlignment = HorizontalAlignment.Left,
};

Origin 是要 draw 的 coordinate (x,y)

WrappingLength 是 frame 的 width. 多少之后要 wrap (字掉去下一行)

HorizontalAlignment 是從左寫到右

Setup Brush

var brush = Brushes.Solid(Color.White);

Brush 負責調顏色

DrawText

string text = "Yang Mi is a Chinese actress, singer, and model known for her roles in popular Chinese TV dramas and films.";
using var newImage = image.Clone(ctx =>
{
    ctx.DrawText(options, text, brush);
});
await newImage.SaveAsJpegAsync(Path.Combine(rootPath, "yangmi-name.jpg"));

把字和 setting 丟進去就可以了.

效果

漂亮吧

 

Text Background Color

繼續上一 part 的例子. 我們要添加 overlay background 到 text 上.

首先得畫出 overlay. 

overlay 的 width, height depend on text 內容. 所以需要計算.

Calc Text Szie

var font = SystemFonts.CreateFont("Times New Roman", 128, FontStyle.Bold);
var text = "Yang Mi is a Chinese actress, singer, and model known for her roles in popular Chinese TV dramas and films.";
var textOptions = new TextOptions(font)
{
    Origin = new PointF(16, 16),
    WrappingLength = 3840f * 0.25f,
    HorizontalAlignment = HorizontalAlignment.Left,
};
var textSize = TextMeasurer.MeasureSize(text, textOptions);
Console.Write(textSize.Width + "x" + textSize.Height);

Draw Text Overlay

using var overlay = new Image<Rgba32>((int)textSize.Width + 64, (int)textSize.Height + 64);
overlay.Mutate(ctx =>
{
    ctx.Fill(Color.Red.WithAlpha(0.5f));
    var textBrush = Brushes.Solid(Color.White);
    ctx.DrawText(textOptions, text, textBrush);
});

創建一張圖, 通過 fill 設置 background color, 再把字上進去.

注: background 是有 opacity 的哦, 但字沒有.

Draw into Image

最后把 overlay 圖片換進原圖. 也就是上面做 watermark 的方法.

using var newImage = image.Clone(ctx =>
{
    ctx.DrawImage(overlay, location: new Point(64, 64), opacity: 1);
});
await newImage.SaveAsJpegAsync(Path.Combine(rootPath, "yangmi-name.jpg"));

效果

我們不可以直接在原圖上 Fill. 它會覆蓋掉原圖, 而且沒有 opacity 效果.

using var newImage = image.Clone(ctx =>
{
    ctx.Fill(Color.Red.WithAlpha(0.5f));
    var textBrush = Brushes.Solid(Color.White);
    ctx.DrawText(textOptions, text, textBrush);
});

上面這樣是不 ok 的. 必須用 DrawImage 的方式來做. (我不清楚原因...)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM