<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Cuong Nguyen Đinh</title><link href="https://world.optimizely.com/blogs/cuong-nguyen-inh/" /><updated>2026-10-10T06:01:41.0000000Z</updated><id>https://world.optimizely.com/blogs/cuong-nguyen-inh/</id><generator uri="https://world.optimizely.com/blogs/cuong-nguyen-inh/" version="2.0">Optimizely World</generator><entry><title>Handling SixLabors.ImageSharp vulnerability warnings in Optimizely CMS 12 (unofficial)</title><link href="https://world.optimizely.com/blogs/cuong-nguyen-inh/dates/2026/10/handling-sixlabors.imagesharp-vulnerability-warnings-in-optimizely-cms-12-unofficial" /><id>https://world.optimizely.com/blogs/cuong-nguyen-inh/dates/2026/10/handling-sixlabors.imagesharp-vulnerability-warnings-in-optimizely-cms-12-unofficial</id><published>2026-10-10T06:01:41.0000000Z</published><updated>2026-10-10T06:01:41.0000000Z</updated><author><name>cuong.nguyendinh@optimizely.com</name></author><category term="Blog post" /><summary type="html">&lt;p&gt;&lt;strong&gt;UPDATED: Optimizely is officially working with SixLabors to find a solution, if possible; the work ID is CMS-57231. An official decision/announcement might come soon.&lt;/strong&gt;&lt;em&gt;&lt;br /&gt;&lt;br /&gt;Written by Claude Opus 5.5 (Anthropic) on behalf of Cuong Nguyen, Optimizely Technical Support.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A note from Cuong:&lt;/strong&gt; Thanks to Optimizely for giving me access to Claude, which made this kind of deep dive possible within a support ticket.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SixLabors.ImageSharp&lt;/strong&gt; is one of the most popular image libraries in the .NET world. Chances are it's in your solution too, even if you never added it yourself. Then one morning you open the project, run a build like any other day, and the output is suddenly full of &lt;strong&gt;NU1902&lt;/strong&gt;/&lt;strong&gt;NU1903&lt;/strong&gt; warnings. Nothing changed in your code. If &lt;strong&gt;TreatWarningsAsErrors&lt;/strong&gt; is on, the build simply fails.&lt;/p&gt;
&lt;p&gt;That's the story behind a lot of tickets we've been getting in Optimizely Support lately. This post covers where the dependency comes from, why "just upgrade ImageSharp" doesn't work, and the four options you have today, including a SkiaSharp-based &lt;strong&gt;IImageService&lt;/strong&gt; sample.&lt;/p&gt;
&lt;h2&gt;1. What you'll see&lt;/h2&gt;
&lt;pre class="language-text"&gt;&lt;code&gt;$ dotnet list package --include-transitive --vulnerable

   Transitive Package          Resolved   Severity   Advisory URL
   &amp;gt; SixLabors.ImageSharp      3.1.11     Moderate   https://github.com/advisories/GHSA-gwg2-r3hj-4w44
                                          High       https://github.com/advisories/GHSA-j3p4-wp97-rph4
                                          High       https://github.com/advisories/GHSA-j9gm-c75j-xc9q
                                          Moderate   https://github.com/advisories/GHSA-wmxv-xphr-5c9g
                                          High       https://github.com/advisories/GHSA-jjfr-hcj7-qf5w
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You'll also get &lt;strong&gt;NU1902&lt;/strong&gt;/&lt;strong&gt;NU1903&lt;/strong&gt; during restore and build. With &lt;strong&gt;TreatWarningsAsErrors&lt;/strong&gt; enabled, the build fails.&lt;/p&gt;
&lt;h2&gt;2. Where the dependency comes from&lt;/h2&gt;
&lt;p&gt;The CMS uses &lt;strong&gt;IImageService&lt;/strong&gt; for thumbnails and the image editor. The default implementation lives in &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt;, which depends on &lt;strong&gt;SixLabors.ImageSharp&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class="language-text"&gt;&lt;code&gt;EPiServer.CMS 12.34.x
└── EPiServer.ImageLibrary.ImageSharp  (&amp;gt;= 1.0.6)   &amp;larr; NuGet picks the lowest matching version
    └── SixLabors.ImageSharp 2.1.11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NuGet resolves dependencies with the &lt;em&gt;lowest applicable version&lt;/em&gt; rule (&lt;a href="https://learn.microsoft.com/nuget/concepts/dependency-resolution#lowest-applicable-version"&gt;docs&lt;/a&gt;). So &lt;strong&gt;EPiServer.CMS&lt;/strong&gt; 12.34.8 resolves &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; &lt;strong&gt;1.0.6&lt;/strong&gt;, which brings in ImageSharp &lt;strong&gt;2.1.11&lt;/strong&gt;. This is standard NuGet behavior, not something specific to Optimizely packages.&lt;/p&gt;
&lt;p&gt;To get a newer version, reference the CMS components directly instead of the &lt;strong&gt;EPiServer.CMS&lt;/strong&gt; metapackage, and pin &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; to 2.0.6:&lt;/p&gt;
&lt;pre class="language-xml"&gt;&lt;code&gt;&amp;lt;PackageReference Include="EPiServer.CMS.UI" Version="12.34.6" /&amp;gt;
&amp;lt;PackageReference Include="EPiServer.Hosting" Version="12.24.1" /&amp;gt;
&amp;lt;!-- ... --&amp;gt;
&amp;lt;PackageReference Include="EPiServer.ImageLibrary.ImageSharp" Version="2.0.6" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That moves you to ImageSharp 3.1.11, which clears the older advisories but not the five listed above.&lt;/p&gt;
&lt;h2&gt;3. Why not force a newer ImageSharp?&lt;/h2&gt;
&lt;p&gt;The advisories above are fixed in ImageSharp 4.x. Six Labors changed the licensing model in 4.x, and Release builds require a license key (&lt;a href="https://github.com/SixLabors/ImageSharp/issues/3205#issuecomment-6062121101"&gt;discussion&lt;/a&gt;). Forcing 4.x through &lt;strong&gt;Directory.Packages.props&lt;/strong&gt; therefore gives you a build error unless you have a Six Labors license. The 3.1.x line, the last one usable without a license, still carries the advisories.&lt;/p&gt;
&lt;h2&gt;4. Your options&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Remove &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No code, warnings disappear&lt;/td&gt;
&lt;td&gt;No image processing: thumbnails and the image editor stop working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Provide your own &lt;a href="/CsClassLibraries/cms/EPiServer.ImageLibrary.IImageService?version=12"&gt;&lt;strong&gt;IImageService&lt;/strong&gt;&lt;/a&gt;, e.g. with &lt;a href="https://github.com/mono/SkiaSharp"&gt;SkiaSharp&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Clean audit, features kept&lt;/td&gt;
&lt;td&gt;Custom code that your team owns and maintains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Suppress the audit warnings&lt;/td&gt;
&lt;td&gt;Quick&lt;/td&gt;
&lt;td&gt;The vulnerable package stays in your solution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Wait for an updated &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No custom code&lt;/td&gt;
&lt;td&gt;No committed timeline today&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;On risk:&lt;/strong&gt; the image service only processes media that authenticated editors upload through the CMS. It doesn't handle arbitrary anonymous input. That lowers the exposure, and editors validating images on upload lowers it further. Whether that's acceptable is a decision for your own security policy and threat model.&lt;/p&gt;
&lt;h2&gt;5. Option 2: a custom &lt;strong&gt;IImageService&lt;/strong&gt; with SkiaSharp&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;IImageService&lt;/strong&gt; is a public extension point with a single method:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public interface IImageService
{
    void RenderImage(Stream input, Stream output, RenderRequest request);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The default implementation maps each &lt;strong&gt;ImageEditorCommand&lt;/strong&gt; like this, and a replacement should do the same:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;ImageEditorCommand&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;Default (ImageSharp)&lt;/th&gt;
&lt;th&gt;SkiaSharp equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Grayscale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Grayscale()&lt;/strong&gt; (BT.709)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;SKColorFilter.CreateColorMatrix&lt;/strong&gt; with BT.709 coefficients&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Crop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Crop(rect)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DrawBitmap(src, -left, -top)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FlipX&lt;/strong&gt; / &lt;strong&gt;FlipY&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Flip(Horizontal/Vertical)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;canvas.Scale(-1, 1, cx, 0)&lt;/strong&gt; / &lt;strong&gt;Scale(1, -1, 0, cy)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resize&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ResizeMode.Stretch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DrawBitmap(src, destRect, Mitchell)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ResizeKeepScale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ResizeMode.BoxPad&lt;/strong&gt; + &lt;strong&gt;BackgroundColor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;fit the box, &lt;strong&gt;no upscaling&lt;/strong&gt;, center, &lt;strong&gt;Clear(bg)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rotate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Rotate(deg)&lt;/strong&gt;, canvas expands&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Translate&lt;/strong&gt; + &lt;strong&gt;RotateDegrees&lt;/strong&gt; on an expanded canvas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Packages&lt;/h3&gt;
&lt;p&gt;Remove &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; and add SkiaSharp. If you reference the &lt;strong&gt;EPiServer.CMS&lt;/strong&gt; metapackage, switch to individual component references first (see section 2). The metapackage brings &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; in transitively, so you can't remove it otherwise.&lt;/p&gt;
&lt;pre class="language-xml"&gt;&lt;code&gt;&amp;lt;!-- remove: &amp;lt;PackageReference Include="EPiServer.ImageLibrary.ImageSharp" Version="..." /&amp;gt; --&amp;gt;
&amp;lt;PackageReference Include="SkiaSharp" Version="4.153.1" /&amp;gt;
&amp;lt;!-- Required on Linux, containers and DXP: --&amp;gt;
&amp;lt;PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="4.153.1" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The service&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;ResizeKeepScale&lt;/strong&gt; matters most, because thumbnail generation calls it with &lt;strong&gt;BackgroundColor = "#00000000"&lt;/strong&gt;. The full implementation:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;using EPiServer.ImageLibrary;
using SkiaSharp;

namespace YourSite.Business.Imaging;

/// &amp;lt;summary&amp;gt;
/// SkiaSharp-based replacement for EPiServer.ImageLibrary.ImageSharp's DefaultImageService,
/// which pulls in vulnerable SixLabors.ImageSharp versions. Mirrors the default behavior:
/// same commands, ResizeKeepScale = ImageSharp "BoxPad" + background color, ZoomFactor applied last.
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;remarks&amp;gt;
/// Skia only encodes PNG, JPEG and WebP. GIF/BMP/TIFF output is not supported (NotSupportedException),
/// and animated GIF input is flattened to its first frame.
/// &amp;lt;/remarks&amp;gt;
public class SkiaImageService : IImageService
{
    private static readonly SKSamplingOptions ResizeSampling = new(SKCubicResampler.Mitchell);
    private static readonly SKSamplingOptions RotateSampling = new(SKFilterMode.Linear);
    private static readonly SKSamplingOptions CopySampling = new(SKFilterMode.Nearest);

    // ITU-R BT.709 luma, same coefficients as ImageSharp's Grayscale().
    private static readonly float[] GrayscaleMatrix =
    [
        0.2126f, 0.7152f, 0.0722f, 0, 0,
        0.2126f, 0.7152f, 0.0722f, 0, 0,
        0.2126f, 0.7152f, 0.0722f, 0, 0,
        0, 0, 0, 1, 0,
    ];

    public void RenderImage(Stream input, Stream output, RenderRequest request)
    {
        ArgumentNullException.ThrowIfNull(input);
        ArgumentNullException.ThrowIfNull(output);
        ArgumentNullException.ThrowIfNull(request);

        var image = Decode(input);
        try
        {
            foreach (var operation in request.Operations ?? [])
            {
                image = Replace(image, Apply(image, operation));
            }

            if (request.ZoomFactor != 1f)
            {
                var width = (int)Math.Floor(image.Width * request.ZoomFactor);
                var height = (int)Math.Floor(image.Height * request.ZoomFactor);
                image = Replace(image, Resize(image, width, height));
            }

            Encode(image, output, request);
        }
        finally
        {
            image.Dispose();
        }
    }

    private static SKBitmap Decode(Stream input)
    {
        using var codec = SKCodec.Create(input)
            ?? throw new NotSupportedException("The source image format is not supported.");

        var info = codec.Info
            .WithColorType(SKImageInfo.PlatformColorType)
            .WithAlphaType(SKAlphaType.Premul);

        return SKBitmap.Decode(codec, info)
            ?? throw new NotSupportedException("The source image could not be decoded.");
    }

    private static SKBitmap Replace(SKBitmap current, SKBitmap next)
    {
        current.Dispose();
        return next;
    }

    private static SKBitmap Apply(SKBitmap source, ImageOperation operation) =&amp;gt; operation.Command switch
    {
        ImageEditorCommand.Grayscale =&amp;gt; Grayscale(source),
        ImageEditorCommand.Crop =&amp;gt; Crop(source, operation),
        ImageEditorCommand.FlipX =&amp;gt; Draw(source.Width, source.Height, canvas =&amp;gt;
        {
            canvas.Scale(-1, 1, source.Width / 2f, 0);
            canvas.DrawBitmap(source, 0, 0, CopySampling);
        }),
        ImageEditorCommand.FlipY =&amp;gt; Draw(source.Width, source.Height, canvas =&amp;gt;
        {
            canvas.Scale(1, -1, 0, source.Height / 2f);
            canvas.DrawBitmap(source, 0, 0, CopySampling);
        }),
        ImageEditorCommand.Resize =&amp;gt; Resize(source, operation.Width, operation.Height),
        ImageEditorCommand.ResizeKeepScale =&amp;gt; ResizeKeepScale(source, operation),
        ImageEditorCommand.Rotate =&amp;gt; Rotate(source, operation.Angle),
        _ =&amp;gt; throw new InvalidOperationException($"Unknown command {operation.Command}"),
    };

    private static SKBitmap Draw(int width, int height, Action&amp;lt;SKCanvas&amp;gt; draw)
    {
        var bitmap = new SKBitmap(width, height, SKImageInfo.PlatformColorType, SKAlphaType.Premul);
        using var canvas = new SKCanvas(bitmap);
        canvas.Clear(SKColors.Transparent);
        draw(canvas);
        return bitmap;
    }

    private static SKBitmap Grayscale(SKBitmap source)
    {
        using var filter = SKColorFilter.CreateColorMatrix(GrayscaleMatrix);
        using var paint = new SKPaint { ColorFilter = filter };
        return Draw(source.Width, source.Height, canvas =&amp;gt; canvas.DrawBitmap(source, 0, 0, CopySampling, paint));
    }

    private static SKBitmap Crop(SKBitmap source, ImageOperation operation)
    {
        var area = SKRectI.Create(operation.Left, operation.Top, operation.Width, operation.Height);
        if (area.IsEmpty || !SKRectI.Create(source.Width, source.Height).Contains(area))
        {
            throw new ArgumentOutOfRangeException(nameof(operation), $"Crop area {area} is outside the image bounds.");
        }

        return Draw(area.Width, area.Height, canvas =&amp;gt; canvas.DrawBitmap(source, -area.Left, -area.Top, CopySampling));
    }

    private static SKBitmap Resize(SKBitmap source, int width, int height)
    {
        var size = TargetSize(source, width, height);
        return Draw(size.Width, size.Height, canvas =&amp;gt;
            canvas.DrawBitmap(source, SKRect.Create(size.Width, size.Height), ResizeSampling));
    }

    /// &amp;lt;summary&amp;gt;ImageSharp BoxPad: fit inside the box without upscaling, center, fill the rest with the background color.&amp;lt;/summary&amp;gt;
    private static SKBitmap ResizeKeepScale(SKBitmap source, ImageOperation operation)
    {
        var box = TargetSize(source, operation.Width, operation.Height);
        var scale = Math.Min(1f, Math.Min(box.Width / (float)source.Width, box.Height / (float)source.Height));
        var width = Math.Max(1, (int)MathF.Round(source.Width * scale));
        var height = Math.Max(1, (int)MathF.Round(source.Height * scale));
        var destination = SKRect.Create((box.Width - width) / 2, (box.Height - height) / 2, width, height);
        var background = ParseColor(operation.BackgroundColor);

        return Draw(box.Width, box.Height, canvas =&amp;gt;
        {
            canvas.Clear(background);
            canvas.DrawBitmap(source, destination, ResizeSampling);
        });
    }

    private static SKBitmap Rotate(SKBitmap source, double degrees)
    {
        var radians = degrees * Math.PI / 180;
        var cos = Math.Abs(Math.Cos(radians));
        var sin = Math.Abs(Math.Sin(radians));
        // Round before Ceiling so right angles don't grow by a pixel from floating point noise (50.0000001 -&amp;gt; 51).
        var width = (int)Math.Ceiling(Math.Round(source.Width * cos + source.Height * sin, 3));
        var height = (int)Math.Ceiling(Math.Round(source.Width * sin + source.Height * cos, 3));

        return Draw(width, height, canvas =&amp;gt;
        {
            canvas.Translate(width / 2f, height / 2f);
            canvas.RotateDegrees((float)degrees);
            canvas.DrawBitmap(source, -source.Width / 2f, -source.Height / 2f, RotateSampling);
        });
    }

    /// &amp;lt;summary&amp;gt;Same as ImageSharp: a 0 dimension is derived from the other one, keeping the aspect ratio.&amp;lt;/summary&amp;gt;
    private static SKSizeI TargetSize(SKBitmap source, int width, int height)
    {
        if (width == 0 &amp;amp;&amp;amp; height &amp;gt; 0)
        {
            width = Math.Max(1, (int)MathF.Round(source.Width * height / (float)source.Height));
        }

        if (height == 0 &amp;amp;&amp;amp; width &amp;gt; 0)
        {
            height = Math.Max(1, (int)MathF.Round(source.Height * width / (float)source.Width));
        }

        if (width &amp;lt;= 0 || height &amp;lt;= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(width), $"Invalid target size {width}x{height}.");
        }

        return new SKSizeI(width, height);
    }

    /// &amp;lt;summary&amp;gt;Parses ImageSharp-style hex colors (#RGB, #RGBA, #RRGGBB, #RRGGBBAA). Empty means white.&amp;lt;/summary&amp;gt;
    private static SKColor ParseColor(string value)
    {
        if (string.IsNullOrEmpty(value))
        {
            return SKColors.White;
        }

        var hex = value.TrimStart('#');
        // Skia expects the alpha first (#ARGB / #AARRGGBB).
        var argb = hex.Length switch
        {
            4 =&amp;gt; hex[3..] + hex[..3],
            8 =&amp;gt; hex[6..] + hex[..6],
            _ =&amp;gt; hex,
        };

        return SKColor.TryParse(argb, out var color)
            ? color
            : throw new ArgumentException($"Invalid background color '{value}'.", nameof(value));
    }

    private static void Encode(SKBitmap image, Stream output, RenderRequest request)
    {
        using var pixmap = image.PeekPixels();
        var quality = Math.Clamp(request.Quality, 0, 100);

        var encoded = request.MimeType switch
        {
            "image/png" or "image/x-icon" =&amp;gt;
                pixmap.Encode(output, new SKPngEncoderOptions(SKPngEncoderFilterFlags.AllFilters, PngZLibLevel(request.Quality))),
            "image/jpg" or "image/jpe" or "image/jpeg" or "image/pjpeg" =&amp;gt;
                pixmap.Encode(output, new SKJpegEncoderOptions(quality)),
            "image/webp" =&amp;gt;
                pixmap.Encode(output, new SKWebpEncoderOptions(SKWebpEncoderCompression.Lossy, quality)),
            _ =&amp;gt; throw new NotSupportedException($"Not supported image format [{request.MimeType}] when creating images."),
        };

        if (!encoded)
        {
            throw new InvalidOperationException($"Failed to encode image as [{request.MimeType}].");
        }
    }

    /// &amp;lt;summary&amp;gt;Same quality -&amp;gt; compression mapping as the default service: 100 = level 0 ... &amp;amp;lt;20 = level 9.&amp;lt;/summary&amp;gt;
    private static int PngZLibLevel(int quality) =&amp;gt; quality &amp;lt; 0 ? 6 : Math.Clamp(10 - quality / 10, 0, 9);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two details that are easy to miss:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Channel order&lt;/strong&gt; (&lt;strong&gt;ParseColor&lt;/strong&gt;). ImageSharp parses &lt;strong&gt;#RRGGBBAA&lt;/strong&gt;, Skia parses &lt;strong&gt;#AARRGGBB&lt;/strong&gt;. Rotate the hex string before parsing, or &lt;strong&gt;#00FF0080&lt;/strong&gt; comes out as a different color.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;90&amp;deg; rotation&lt;/strong&gt; (&lt;strong&gt;Rotate&lt;/strong&gt;). &lt;strong&gt;cos(90&amp;deg;)&lt;/strong&gt; evaluates to &lt;strong&gt;6e-17&lt;/strong&gt;, not 0, so &lt;strong&gt;Math.Ceiling(50.00000000000001)&lt;/strong&gt; gives 51 and the output gains an extra pixel. Round before taking the ceiling.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Registration&lt;/h3&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;using EPiServer.ImageLibrary;
using YourSite.Business.Imaging;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddCmsAspNetIdentity&amp;lt;ApplicationUser&amp;gt;()
            .AddCms();

        // Replaces EPiServer.ImageLibrary.ImageSharp for thumbnails and the CMS image editor.
        services.AddSingleton&amp;lt;IImageService, SkiaImageService&amp;gt;();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CMS only registers a fallback &lt;strong&gt;IImageService&lt;/strong&gt; when none exists (&lt;strong&gt;TryAddSingleton&lt;/strong&gt;), so your implementation takes precedence. Without any registration, that fallback throws &lt;strong&gt;InvalidOperationException&lt;/strong&gt; the first time it's resolved.&lt;/p&gt;
&lt;pre class="language-text"&gt;&lt;code&gt;$ dotnet list package --include-transitive --vulnerable
The given project `Alloy` has no vulnerable packages given the current sources.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Known limitations&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Skia encodes PNG, JPEG and WebP only. GIF, BMP or TIFF output throws &lt;strong&gt;NotSupportedException&lt;/strong&gt;, so the image editor won't work for those formats. Thumbnails aren't affected, because the CMS always stores them as &lt;strong&gt;.png&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Skia can't decode TIFF, so TIFF uploads get no thumbnail. The upload itself still succeeds, because the CMS handles thumbnail errors.&lt;/li&gt;
&lt;li&gt;Animated GIFs are flattened to the first frame.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; this sample is not an officially supported Optimizely package. It's provided as guidance, and you own it once it's in your solution. Test it against your own media library before you deploy.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;6. Option 3: suppress the warnings&lt;/h2&gt;
&lt;pre class="language-xml"&gt;&lt;code&gt;&amp;lt;ItemGroup&amp;gt;
  &amp;lt;NuGetAuditSuppress Include="https://github.com/advisories/GHSA-gwg2-r3hj-4w44" /&amp;gt;
  &amp;lt;NuGetAuditSuppress Include="https://github.com/advisories/GHSA-j3p4-wp97-rph4" /&amp;gt;
  &amp;lt;NuGetAuditSuppress Include="https://github.com/advisories/GHSA-j9gm-c75j-xc9q" /&amp;gt;
  &amp;lt;NuGetAuditSuppress Include="https://github.com/advisories/GHSA-wmxv-xphr-5c9g" /&amp;gt;
  &amp;lt;NuGetAuditSuppress Include="https://github.com/advisories/GHSA-jjfr-hcj7-qf5w" /&amp;gt;
&amp;lt;/ItemGroup&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Suppressing only hides the warning, the vulnerability is still there. Record the decision (reason, owner, review date) wherever your team tracks accepted risks.&lt;/p&gt;
&lt;h2&gt;7. Option 4: wait for an updated package&lt;/h2&gt;
&lt;p&gt;An updated &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; would remove the problem at the source. We can't share a timeline for that yet. If you go this route:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Watch the Optimizely release notes for a new &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; version, or check from the command line:&lt;/p&gt;
&lt;pre class="language-bash"&gt;&lt;code&gt;dotnet list package --outdated --include-transitive | grep -i imagelibrary
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Combine it with option 3 in the meantime, and add a review date so the suppression gets removed once the update ships:&lt;/p&gt;
&lt;pre class="language-xml"&gt;&lt;code&gt;&amp;lt;!-- TEMP: waiting for an EPiServer.ImageLibrary.ImageSharp update without the vulnerable ImageSharp.
     Review by: 2026-12-31. Owner: @team-cms --&amp;gt;
&amp;lt;NuGetAuditSuppress Include="https://github.com/advisories/GHSA-j3p4-wp97-rph4" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If you're on option 2, moving to the official package later means deleting your &lt;strong&gt;IImageService&lt;/strong&gt; implementation and its registration.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;SixLabors/ImageSharp, 4.x license discussion: &lt;a href="https://github.com/SixLabors/ImageSharp/issues/3205#issuecomment-6062121101"&gt;https://github.com/SixLabors/ImageSharp/issues/3205#issuecomment-6062121101&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Optimizely &lt;strong&gt;IImageService&lt;/strong&gt; API: &lt;a href="/CsClassLibraries/cms/EPiServer.ImageLibrary.IImageService?version=12"&gt;https://world.optimizely.com/CsClassLibraries/cms/EPiServer.ImageLibrary.IImageService?version=12&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;SkiaSharp: &lt;a href="https://github.com/mono/SkiaSharp"&gt;https://github.com/mono/SkiaSharp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;NuGet dependency resolution: &lt;a href="https://learn.microsoft.com/nuget/concepts/dependency-resolution"&gt;https://learn.microsoft.com/nuget/concepts/dependency-resolution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;NuGet Audit: &lt;a href="https://learn.microsoft.com/nuget/concepts/auditing-packages"&gt;https://learn.microsoft.com/nuget/concepts/auditing-packages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Advisories: &lt;a href="https://github.com/advisories/GHSA-gwg2-r3hj-4w44"&gt;GHSA-gwg2-r3hj-4w44&lt;/a&gt;, &lt;a href="https://github.com/advisories/GHSA-j3p4-wp97-rph4"&gt;GHSA-j3p4-wp97-rph4&lt;/a&gt;, &lt;a href="https://github.com/advisories/GHSA-j9gm-c75j-xc9q"&gt;GHSA-j9gm-c75j-xc9q&lt;/a&gt;, &lt;a href="https://github.com/advisories/GHSA-wmxv-xphr-5c9g"&gt;GHSA-wmxv-xphr-5c9g&lt;/a&gt;, &lt;a href="https://github.com/advisories/GHSA-jjfr-hcj7-qf5w"&gt;GHSA-jjfr-hcj7-qf5w&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</summary></entry><entry><title>Optimizely Content Graph on mobile application</title><link href="https://world.optimizely.com/blogs/cuong-nguyen-inh/dates/2025/1/optimizely-content-graph-on-mobile-application/" /><id>https://world.optimizely.com/blogs/cuong-nguyen-inh/dates/2025/1/optimizely-content-graph-on-mobile-application/</id><published>2025-01-20T12:17:42.0000000Z</published><updated>2025-01-20T12:17:42.0000000Z</updated><author><name>cuong.nguyendinh@optimizely.com</name></author><category term="Blog post" /><summary type="html">&lt;p&gt;CG everywhere! I pull schema from our default index https://cg.optimizely.com/app/graphiql?auth=eBrGunULiC5TziTCtiOLEmov2LijBf30obh0KmhcBlyTktGZ in our document to a schema.graphql file via the introspection query, write a query that gets product pages, use a package to generate graph client code and pull data.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;Source code available at &lt;a href="https://github.com/cuong0993/cms_saas_flutter_demo"&gt;https://github.com/cuong0993/cms_saas_flutter_demo&lt;/a&gt;&lt;/p&gt;</summary></entry></feed>