< Summary - Jellyfin

Information
Class: Jellyfin.Drawing.Skia.SkiaEncoder
Assembly: Jellyfin.Drawing.Skia
File(s): /srv/git/jellyfin/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
Line coverage
10%
Covered lines: 39
Uncovered lines: 330
Coverable lines: 369
Total lines: 820
Line coverage: 10.5%
Branch coverage
1%
Covered branches: 2
Total branches: 168
Branch coverage: 1.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 10/17/2025 - 12:10:14 AM Line coverage: 11% (39/354) Branch coverage: 1.2% (2/162) Total lines: 79012/29/2025 - 12:13:19 AM Line coverage: 10.5% (39/369) Branch coverage: 1.1% (2/168) Total lines: 820 10/17/2025 - 12:10:14 AM Line coverage: 11% (39/354) Branch coverage: 1.2% (2/162) Total lines: 79012/29/2025 - 12:13:19 AM Line coverage: 10.5% (39/369) Branch coverage: 1.1% (2/168) Total lines: 820

Metrics

File(s)

/srv/git/jellyfin/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.IO;
 5using System.Linq;
 6using BlurHashSharp.SkiaSharp;
 7using Jellyfin.Extensions;
 8using MediaBrowser.Common.Configuration;
 9using MediaBrowser.Common.Extensions;
 10using MediaBrowser.Controller.Drawing;
 11using MediaBrowser.Model.Drawing;
 12using Microsoft.Extensions.Logging;
 13using SkiaSharp;
 14using Svg.Skia;
 15
 16namespace Jellyfin.Drawing.Skia;
 17
 18/// <summary>
 19/// Image encoder that uses <see cref="SkiaSharp"/> to manipulate images.
 20/// </summary>
 21public class SkiaEncoder : IImageEncoder
 22{
 23    private const string SvgFormat = "svg";
 124    private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".g
 25    private readonly ILogger<SkiaEncoder> _logger;
 26    private readonly IApplicationPaths _appPaths;
 27    private static readonly SKImageFilter _imageFilter;
 28    private static readonly SKTypeface[] _typefaces;
 29
 30    /// <summary>
 31    /// The default sampling options, equivalent to old high quality filter settings when upscaling.
 32    /// </summary>
 33    public static readonly SKSamplingOptions UpscaleSamplingOptions;
 34
 35    /// <summary>
 36    /// The sampling options, used for downscaling images, equivalent to old high quality filter settings when not upsca
 37    /// </summary>
 38    public static readonly SKSamplingOptions DefaultSamplingOptions;
 39
 40#pragma warning disable CA1810
 41    static SkiaEncoder()
 42#pragma warning restore CA1810
 43    {
 144        var kernel = new[]
 145        {
 146            0,    -.1f,    0,
 147            -.1f, 1.4f, -.1f,
 148            0,    -.1f,    0,
 149        };
 50
 151        var kernelSize = new SKSizeI(3, 3);
 152        var kernelOffset = new SKPointI(1, 1);
 153        _imageFilter = SKImageFilter.CreateMatrixConvolution(
 154            kernelSize,
 155            kernel,
 156            1f,
 157            0f,
 158            kernelOffset,
 159            SKShaderTileMode.Clamp,
 160            true);
 61
 62        // Initialize the list of typefaces
 63        // We have to statically build a list of typefaces because MatchCharacter only accepts a single character or cod
 64        // But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻
 165        _typefaces =
 166        [
 167            SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant
 168            SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant
 169            SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant
 170            SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant
 171            SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant
 172            SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant
 173            SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant
 174            SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Up
 175        ];
 76
 77        // use cubic for upscaling
 178        UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell);
 79        // use bilinear for everything else
 180        DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear);
 181    }
 82
 83    /// <summary>
 84    /// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
 85    /// </summary>
 86    /// <param name="logger">The application logger.</param>
 87    /// <param name="appPaths">The application paths.</param>
 88    public SkiaEncoder(ILogger<SkiaEncoder> logger, IApplicationPaths appPaths)
 89    {
 2190        _logger = logger;
 2191        _appPaths = appPaths;
 2192    }
 93
 94    /// <inheritdoc/>
 095    public string Name => "Skia";
 96
 97    /// <inheritdoc/>
 098    public bool SupportsImageCollageCreation => true;
 99
 100    /// <inheritdoc/>
 0101    public bool SupportsImageEncoding => true;
 102
 103    /// <inheritdoc/>
 104    public IReadOnlyCollection<string> SupportedInputFormats =>
 0105        new HashSet<string>(StringComparer.OrdinalIgnoreCase)
 0106        {
 0107            "jpeg",
 0108            "jpg",
 0109            "png",
 0110            "dng",
 0111            "webp",
 0112            "gif",
 0113            "bmp",
 0114            "ico",
 0115            "astc",
 0116            "ktx",
 0117            "pkm",
 0118            "wbmp",
 0119            // TODO: check if these are supported on multiple platforms
 0120            // https://github.com/google/skia/blob/master/infra/bots/recipes/test.py#L454
 0121            // working on windows at least
 0122            "cr2",
 0123            "nef",
 0124            "arw",
 0125            SvgFormat
 0126        };
 127
 128    /// <inheritdoc/>
 129    public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
 0130        => new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png, ImageFormat.Svg };
 131
 132    /// <summary>
 133    /// Gets the default typeface to use.
 134    /// </summary>
 0135    public static SKTypeface DefaultTypeFace => _typefaces.Last();
 136
 137    /// <summary>
 138    /// Check if the native lib is available.
 139    /// </summary>
 140    /// <returns>True if the native lib is available, otherwise false.</returns>
 141    public static bool IsNativeLibAvailable()
 142    {
 143        try
 144        {
 145            // test an operation that requires the native library
 21146            SKPMColor.PreMultiply(SKColors.Black);
 21147            return true;
 148        }
 0149        catch (Exception)
 150        {
 0151            return false;
 152        }
 21153    }
 154
 155    /// <summary>
 156    /// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
 157    /// </summary>
 158    /// <param name="selectedFormat">The format to convert.</param>
 159    /// <returns>The converted format.</returns>
 160    public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
 161    {
 0162        return selectedFormat switch
 0163        {
 0164            ImageFormat.Bmp => SKEncodedImageFormat.Bmp,
 0165            ImageFormat.Jpg => SKEncodedImageFormat.Jpeg,
 0166            ImageFormat.Gif => SKEncodedImageFormat.Gif,
 0167            ImageFormat.Webp => SKEncodedImageFormat.Webp,
 0168            _ => SKEncodedImageFormat.Png
 0169        };
 170    }
 171
 172    /// <inheritdoc />
 173    /// <exception cref="FileNotFoundException">The path is not valid.</exception>
 174    public ImageDimensions GetImageSize(string path)
 175    {
 0176        if (!File.Exists(path))
 177        {
 0178            throw new FileNotFoundException("File not found", path);
 179        }
 180
 0181        var extension = Path.GetExtension(path.AsSpan());
 0182        if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase))
 183        {
 0184            using var svg = new SKSvg();
 185            try
 186            {
 0187                using var picture = svg.Load(path);
 0188                if (picture is null)
 189                {
 0190                    _logger.LogError("Unable to determine image dimensions for {FilePath}", path);
 0191                    return default;
 192                }
 193
 0194                return new ImageDimensions(Convert.ToInt32(picture.CullRect.Width), Convert.ToInt32(picture.CullRect.Hei
 195            }
 0196            catch (FormatException skiaColorException)
 197            {
 198                // This exception is known to be thrown on vector images that define custom styles
 199                // Skia SVG is not able to handle that and as the repository is quite stale and has not received updates
 0200                _logger.LogDebug(skiaColorException, "There was a issue loading the requested svg file");
 0201                return default;
 202            }
 203        }
 204
 0205        var safePath = NormalizePath(path);
 0206        if (new FileInfo(safePath).Length == 0)
 207        {
 0208            _logger.LogDebug("Skip zero‑byte image {FilePath}", path);
 0209            return default;
 210        }
 211
 0212        SKCodec? codec = null;
 0213        bool isSafePathTemp = !string.Equals(Path.GetFullPath(safePath), Path.GetFullPath(path), StringComparison.Ordina
 214        try
 215        {
 0216            codec = SKCodec.Create(safePath, out var result);
 0217            switch (result)
 218            {
 219                case SKCodecResult.Success:
 220                // Skia/SkiaSharp edge‑case: when the image header is parsed but the actual pixel
 221                // decode fails (truncated JPEG/PNG, exotic ICC/EXIF, CMYK without color‑transform, etc.)
 222                // `SKCodec.Create` returns a *non‑null* codec together with
 223                // SKCodecResult.InternalError.  The header still contains valid dimensions,
 224                // which is all we need here – so we fall back to them instead of aborting.
 225                // See e.g. Skia bugs #4139, #6092.
 0226                case SKCodecResult.InternalError when codec is not null:
 0227                    var info = codec.Info;
 0228                    return new ImageDimensions(info.Width, info.Height);
 229
 230                case SKCodecResult.Unimplemented:
 0231                    _logger.LogDebug("Image format not supported: {FilePath}", path);
 0232                    return default;
 233
 234                default:
 235                {
 0236                    var boundsInfo = SKBitmap.DecodeBounds(safePath);
 0237                    if (boundsInfo.Width > 0 && boundsInfo.Height > 0)
 238                    {
 0239                        return new ImageDimensions(boundsInfo.Width, boundsInfo.Height);
 240                    }
 241
 0242                    _logger.LogWarning(
 0243                        "Unable to determine image dimensions for {FilePath}: {SkCodecResult}",
 0244                        path,
 0245                        result);
 246
 0247                    return default;
 248                }
 249            }
 250        }
 251        finally
 252        {
 253            try
 254            {
 0255                codec?.Dispose();
 0256            }
 0257            catch (Exception ex)
 258            {
 0259                _logger.LogDebug(ex, "Error by closing codec for {FilePath}", safePath);
 0260            }
 261
 0262            if (isSafePathTemp)
 263            {
 264                try
 265                {
 0266                    if (File.Exists(safePath))
 267                    {
 0268                        File.Delete(safePath);
 269                    }
 0270                }
 0271                catch (Exception ex)
 272                {
 0273                    _logger.LogDebug(ex, "Unable to remove temporary file '{TempPath}'", safePath);
 0274                }
 275            }
 0276        }
 0277    }
 278
 279    /// <inheritdoc />
 280    /// <exception cref="ArgumentNullException">The path is null.</exception>
 281    /// <exception cref="FileNotFoundException">The path is not valid.</exception>
 282    public string GetImageBlurHash(int xComp, int yComp, string path)
 283    {
 0284        ArgumentException.ThrowIfNullOrEmpty(path);
 285
 0286        var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
 0287        if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)
 0288            || extension.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase))
 289        {
 0290            _logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path);
 0291            return string.Empty;
 292        }
 293
 294        // Use FileStream with FileShare.Read instead of having Skia open the file to allow concurrent read access
 0295        using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
 296        // Any larger than 128x128 is too slow and there's no visually discernible difference
 0297        return BlurHashEncoder.Encode(xComp, yComp, fileStream, 128, 128);
 0298    }
 299
 300    private bool RequiresSpecialCharacterHack(string path)
 301    {
 0302        for (int i = 0; i < path.Length; i++)
 303        {
 0304            if (char.GetUnicodeCategory(path[i]) == UnicodeCategory.OtherLetter)
 305            {
 0306                return true;
 307            }
 308        }
 309
 0310        return path.HasDiacritics();
 311    }
 312
 313    private string NormalizePath(string path)
 314    {
 0315        if (!RequiresSpecialCharacterHack(path))
 316        {
 0317            return path;
 318        }
 319
 0320        var tempPath = Path.Join(_appPaths.TempDirectory, string.Concat("skia_", Guid.NewGuid().ToString(), Path.GetExte
 0321        var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPat
 0322        Directory.CreateDirectory(directory);
 0323        File.Copy(path, tempPath, true);
 324
 0325        return tempPath;
 326    }
 327
 328    private static SKEncodedOrigin GetSKEncodedOrigin(ImageOrientation? orientation)
 329    {
 0330        if (!orientation.HasValue)
 331        {
 0332            return SKEncodedOrigin.Default;
 333        }
 334
 0335        return (SKEncodedOrigin)orientation.Value;
 336    }
 337
 338    /// <summary>
 339    /// Decode an image.
 340    /// </summary>
 341    /// <param name="path">The filepath of the image to decode.</param>
 342    /// <param name="forceCleanBitmap">Whether to force clean the bitmap.</param>
 343    /// <param name="orientation">The orientation of the image.</param>
 344    /// <param name="origin">The detected origin of the image.</param>
 345    /// <returns>The resulting bitmap of the image.</returns>
 346    internal SKBitmap? Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin ori
 347    {
 0348        if (!File.Exists(path))
 349        {
 0350            throw new FileNotFoundException("File not found", path);
 351        }
 352
 0353        var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path));
 354
 0355        if (requiresTransparencyHack || forceCleanBitmap)
 356        {
 0357            using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res);
 0358            if (res != SKCodecResult.Success)
 359            {
 0360                origin = GetSKEncodedOrigin(orientation);
 0361                return null;
 362            }
 363
 0364            if (codec.FrameCount != 0)
 365            {
 0366                throw new ArgumentException("Cannot decode images with multiple frames");
 367            }
 368
 369            // create the bitmap
 0370            SKBitmap? bitmap = null;
 371            try
 372            {
 0373                bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
 374
 375                // decode
 0376                _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
 377
 0378                origin = codec.EncodedOrigin;
 379
 0380                return bitmap!;
 381            }
 0382            catch (Exception e)
 383            {
 0384                _logger.LogError(e, "Detected intermediary error decoding image {0}", path);
 0385                bitmap?.Dispose();
 0386                throw;
 387            }
 388        }
 389
 0390        var resultBitmap = SKBitmap.Decode(NormalizePath(path));
 391
 0392        if (resultBitmap is null)
 393        {
 0394            return Decode(path, true, orientation, out origin);
 395        }
 396
 397        try
 398        {
 399             // If we have to resize these they often end up distorted
 0400            if (resultBitmap.ColorType == SKColorType.Gray8)
 401            {
 0402                using (resultBitmap)
 403                {
 0404                    return Decode(path, true, orientation, out origin);
 405                }
 406            }
 407
 0408            origin = SKEncodedOrigin.TopLeft;
 0409            return resultBitmap;
 410        }
 0411        catch (Exception e)
 412        {
 0413            _logger.LogError(e, "Detected intermediary error decoding image {0}", path);
 0414            resultBitmap?.Dispose();
 0415            throw;
 416        }
 0417    }
 418
 419    private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation)
 420    {
 0421        if (autoOrient)
 422        {
 0423            var bitmap = Decode(path, true, orientation, out var origin);
 424
 0425            if (bitmap is not null && origin != SKEncodedOrigin.TopLeft)
 426            {
 0427                using (bitmap)
 428                {
 0429                    return OrientImage(bitmap, origin);
 430                }
 431            }
 432
 0433            return bitmap;
 434        }
 435
 0436        return Decode(path, false, orientation, out _);
 0437    }
 438
 439    private SKBitmap? GetBitmapFromSvg(string path)
 440    {
 0441        if (!File.Exists(path))
 442        {
 0443            throw new FileNotFoundException("File not found", path);
 444        }
 445
 0446        using var svg = SKSvg.CreateFromFile(path);
 0447        if (svg.Drawable is null)
 448        {
 0449            return null;
 450        }
 451
 0452        var width = (int)Math.Round(svg.Drawable.Bounds.Width);
 0453        var height = (int)Math.Round(svg.Drawable.Bounds.Height);
 454
 0455        SKBitmap? bitmap = null;
 456        try
 457        {
 0458            bitmap = new SKBitmap(width, height);
 0459            using var canvas = new SKCanvas(bitmap);
 0460            canvas.DrawPicture(svg.Picture);
 0461            canvas.Flush();
 0462            canvas.Save();
 463
 0464            return bitmap!;
 465        }
 0466        catch (Exception e)
 467        {
 0468            _logger.LogError(e, "Detected intermediary error extracting image {0}", path);
 0469            bitmap?.Dispose();
 0470            throw;
 471        }
 0472    }
 473
 474    private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
 475    {
 0476        var needsFlip = origin is SKEncodedOrigin.LeftBottom or SKEncodedOrigin.LeftTop or SKEncodedOrigin.RightBottom o
 0477        SKBitmap? rotated = null;
 478        try
 479        {
 0480            rotated = needsFlip
 0481                ? new SKBitmap(bitmap.Height, bitmap.Width)
 0482                : new SKBitmap(bitmap.Width, bitmap.Height);
 0483            using var surface = new SKCanvas(rotated);
 0484            var midX = (float)rotated.Width / 2;
 0485            var midY = (float)rotated.Height / 2;
 486
 487            switch (origin)
 488            {
 489                case SKEncodedOrigin.TopRight:
 0490                    surface.Scale(-1, 1, midX, midY);
 0491                    break;
 492                case SKEncodedOrigin.BottomRight:
 0493                    surface.RotateDegrees(180, midX, midY);
 0494                    break;
 495                case SKEncodedOrigin.BottomLeft:
 0496                    surface.Scale(1, -1, midX, midY);
 0497                    break;
 498                case SKEncodedOrigin.LeftTop:
 0499                    surface.Translate(0, -rotated.Height);
 0500                    surface.Scale(1, -1, midX, midY);
 0501                    surface.RotateDegrees(-90);
 0502                    break;
 503                case SKEncodedOrigin.RightTop:
 0504                    surface.Translate(rotated.Width, 0);
 0505                    surface.RotateDegrees(90);
 0506                    break;
 507                case SKEncodedOrigin.RightBottom:
 0508                    surface.Translate(rotated.Width, 0);
 0509                    surface.Scale(1, -1, midX, midY);
 0510                    surface.RotateDegrees(90);
 0511                    break;
 512                case SKEncodedOrigin.LeftBottom:
 0513                    surface.Translate(0, rotated.Height);
 0514                    surface.RotateDegrees(-90);
 515                    break;
 516            }
 517
 0518            surface.DrawBitmap(bitmap, 0, 0, DefaultSamplingOptions);
 0519            return rotated;
 520        }
 0521        catch (Exception e)
 522        {
 0523            _logger.LogError(e, "Detected intermediary error rotating image");
 0524            rotated?.Dispose();
 0525            throw;
 526        }
 0527    }
 528
 529    /// <summary>
 530    /// Resizes an image on the CPU, by utilizing a surface and canvas.
 531    ///
 532    /// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect.
 533    /// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP
 534    /// </summary>
 535    /// <param name="source">The source bitmap.</param>
 536    /// <param name="targetInfo">This specifies the target size and other information required to create the surface.</p
 537    /// <param name="isAntialias">This enables anti-aliasing on the SKPaint instance.</param>
 538    /// <param name="isDither">This enables dithering on the SKPaint instance.</param>
 539    /// <returns>The resized image.</returns>
 540    internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither
 541    {
 0542        using var surface = SKSurface.Create(targetInfo);
 0543        using var canvas = surface.Canvas;
 0544        using var paint = new SKPaint();
 0545        paint.IsAntialias = isAntialias;
 0546        paint.IsDither = isDither;
 547
 548        // Historically, kHigh implied cubic filtering, but only when upsampling.
 549        // If specified kHigh, and were down-sampling, Skia used to switch back to kMedium (bilinear filtering plus mipm
 550        // With current skia API, passing Mitchell cubic when down-sampling will cause serious quality degradation.
 0551        var samplingOptions = source.Width > targetInfo.Width || source.Height > targetInfo.Height
 0552            ? DefaultSamplingOptions
 0553            : UpscaleSamplingOptions;
 554
 0555        paint.ImageFilter = _imageFilter;
 0556        canvas.DrawBitmap(
 0557            source,
 0558            SKRect.Create(0, 0, source.Width, source.Height),
 0559            SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
 0560            samplingOptions,
 0561            paint);
 562
 0563        return surface.Snapshot();
 0564    }
 565
 566    /// <inheritdoc/>
 567    public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientat
 568    {
 0569        ArgumentException.ThrowIfNullOrEmpty(inputPath);
 0570        ArgumentException.ThrowIfNullOrEmpty(outputPath);
 571
 0572        var inputFormat = Path.GetExtension(inputPath.AsSpan()).TrimStart('.');
 0573        if (!SupportedInputFormats.Contains(inputFormat, StringComparison.OrdinalIgnoreCase))
 574        {
 0575            _logger.LogDebug("Unable to encode image due to unsupported format: {ImagePath}", inputPath);
 0576            return inputPath;
 577        }
 578
 0579        if (outputFormat == ImageFormat.Svg
 0580            && !inputFormat.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase))
 581        {
 0582            throw new ArgumentException($"Requested svg output from {inputFormat} input");
 583        }
 584
 0585        var skiaOutputFormat = GetImageFormat(outputFormat);
 586
 0587        var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
 0588        var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
 0589        var blur = options.Blur ?? 0;
 0590        var hasIndicator = options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
 591
 0592        using var bitmap = inputFormat.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase)
 0593            ? GetBitmapFromSvg(inputPath)
 0594            : GetBitmap(inputPath, autoOrient, orientation);
 595
 0596        if (bitmap is null)
 597        {
 0598            throw new InvalidDataException($"Skia unable to read image {inputPath}");
 599        }
 600
 0601        var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
 602
 0603        if (options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient)
 604        {
 605            // Just spit out the original file if all the options are default
 0606            return inputPath;
 607        }
 608
 0609        var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
 610
 0611        var width = newImageSize.Width;
 0612        var height = newImageSize.Height;
 613
 614        // scale image (the FromImage creates a copy)
 0615        var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
 0616        using var resizedImage = ResizeImage(bitmap, imageInfo);
 0617        using var resizedBitmap = SKBitmap.FromImage(resizedImage);
 618
 619        // If all we're doing is resizing then we can stop now
 0620        if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
 621        {
 0622            var outputDirectory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({out
 0623            Directory.CreateDirectory(outputDirectory);
 0624            using var outputStream = new SKFileWStream(outputPath);
 0625            using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
 0626            resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
 0627            return outputPath;
 628        }
 629
 630        // create bitmap to use for canvas drawing used to draw into bitmap
 0631        using var saveBitmap = new SKBitmap(width, height);
 0632        using var canvas = new SKCanvas(saveBitmap);
 633        // set background color if present
 0634        if (hasBackgroundColor)
 635        {
 0636            canvas.Clear(SKColor.Parse(options.BackgroundColor));
 637        }
 638
 0639        using var paint = new SKPaint();
 640        // Add blur if option is present
 0641        using var filter = blur > 0 ? SKImageFilter.CreateBlur(blur, blur) : null;
 0642        paint.ImageFilter = filter;
 643
 644        // create image from resized bitmap to apply blur
 0645        canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), DefaultSamplingOptions, paint);
 646
 647        // If foreground layer present then draw
 0648        if (hasForegroundColor)
 649        {
 0650            if (!double.TryParse(options.ForegroundLayer, out double opacity))
 651            {
 0652                opacity = .4;
 653            }
 654
 0655            canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
 656        }
 657
 0658        if (hasIndicator)
 659        {
 0660            DrawIndicator(canvas, width, height, options);
 661        }
 662
 0663        var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) 
 0664        Directory.CreateDirectory(directory);
 0665        using (var outputStream = new SKFileWStream(outputPath))
 666        {
 0667            using var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels());
 0668            pixmap.Encode(outputStream, skiaOutputFormat, quality);
 669        }
 670
 0671        return outputPath;
 0672    }
 673
 674    /// <inheritdoc/>
 675    public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
 676    {
 0677        double ratio = (double)options.Width / options.Height;
 678
 0679        if (ratio >= 1.4)
 680        {
 0681            new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, optio
 682        }
 0683        else if (ratio >= .9)
 684        {
 0685            new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, opti
 686        }
 687        else
 688        {
 689            // TODO: Create Poster collage capability
 0690            new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, opti
 691        }
 0692    }
 693
 694    /// <inheritdoc />
 695    public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
 696    {
 697        // Only generate the splash screen if we have at least one poster and at least one backdrop/thumbnail.
 17698        if (posters.Count > 0 && backdrops.Count > 0)
 699        {
 0700            var splashBuilder = new SplashscreenBuilder(this, _logger);
 0701            var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
 0702            splashBuilder.GenerateSplash(posters, backdrops, outputPath);
 703        }
 17704    }
 705
 706    /// <inheritdoc />
 707    public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
 708    {
 0709        var paths = options.InputPaths;
 0710        var tileWidth = options.Width;
 0711        var tileHeight = options.Height;
 712
 0713        if (paths.Count < 1)
 714        {
 0715            throw new ArgumentException("InputPaths cannot be empty.");
 716        }
 0717        else if (paths.Count > tileWidth * tileHeight)
 718        {
 0719            throw new ArgumentException($"InputPaths contains more images than would fit on {tileWidth}x{tileHeight} gri
 720        }
 721
 722        // If no height provided, use height of first image.
 0723        if (!imgHeight.HasValue)
 724        {
 0725            using var firstImg = Decode(paths[0], false, null, out _);
 726
 0727            if (firstImg is null)
 728            {
 0729                throw new InvalidDataException("Could not decode image data.");
 730            }
 731
 0732            if (firstImg.Width != imgWidth)
 733            {
 0734                throw new InvalidOperationException("Image width does not match provided width.");
 735            }
 736
 0737            imgHeight = firstImg.Height;
 738        }
 739
 740        // Make horizontal strips using every provided image.
 0741        using var tileGrid = new SKBitmap(imgWidth * tileWidth, imgHeight.Value * tileHeight);
 0742        using var canvas = new SKCanvas(tileGrid);
 743
 0744        var imgIndex = 0;
 0745        for (var y = 0; y < tileHeight; y++)
 746        {
 0747            for (var x = 0; x < tileWidth; x++)
 748            {
 0749                if (imgIndex >= paths.Count)
 750                {
 751                    break;
 752                }
 753
 0754                using var img = Decode(paths[imgIndex++], false, null, out _);
 755
 0756                if (img is null)
 757                {
 0758                    throw new InvalidDataException("Could not decode image data.");
 759                }
 760
 0761                if (img.Width != imgWidth)
 762                {
 0763                    throw new InvalidOperationException("Image width does not match provided width.");
 764                }
 765
 0766                if (img.Height != imgHeight)
 767                {
 0768                    throw new InvalidOperationException("Image height does not match first image height.");
 769                }
 770
 0771                canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value, DefaultSamplingOptions);
 772            }
 773        }
 774
 0775        using var outputStream = new SKFileWStream(options.OutputPath);
 0776        tileGrid.Encode(outputStream, SKEncodedImageFormat.Jpeg, quality);
 777
 0778        return imgHeight.Value;
 0779    }
 780
 781    private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
 782    {
 783        try
 784        {
 0785            var currentImageSize = new ImageDimensions(imageWidth, imageHeight);
 786
 0787            if (options.UnplayedCount.HasValue)
 788            {
 0789                UnplayedCountIndicator.DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value)
 790            }
 791
 0792            if (options.PercentPlayed > 0)
 793            {
 0794                PercentPlayedDrawer.Process(canvas, currentImageSize, options.PercentPlayed);
 795            }
 0796        }
 0797        catch (Exception ex)
 798        {
 0799            _logger.LogError(ex, "Error drawing indicator overlay");
 0800        }
 0801    }
 802
 803    /// <summary>
 804    /// Return the typeface that contains the glyph for the given character.
 805    /// </summary>
 806    /// <param name="c">The text character.</param>
 807    /// <returns>The typeface contains the character.</returns>
 808    public static SKTypeface? GetFontForCharacter(string c)
 809    {
 0810        foreach (var typeface in _typefaces)
 811        {
 0812            if (typeface.ContainsGlyphs(c))
 813            {
 0814                return typeface;
 815            }
 816        }
 817
 0818        return null;
 819    }
 820}

Methods/Properties

.cctor()
.ctor(Microsoft.Extensions.Logging.ILogger`1<Jellyfin.Drawing.Skia.SkiaEncoder>,MediaBrowser.Common.Configuration.IApplicationPaths)
get_Name()
get_SupportsImageCollageCreation()
get_SupportsImageEncoding()
get_SupportedInputFormats()
get_SupportedOutputFormats()
get_DefaultTypeFace()
IsNativeLibAvailable()
GetImageFormat(MediaBrowser.Model.Drawing.ImageFormat)
GetImageSize(System.String)
GetImageBlurHash(System.Int32,System.Int32,System.String)
RequiresSpecialCharacterHack(System.String)
NormalizePath(System.String)
GetSKEncodedOrigin(System.Nullable`1<MediaBrowser.Model.Drawing.ImageOrientation>)
Decode(System.String,System.Boolean,System.Nullable`1<MediaBrowser.Model.Drawing.ImageOrientation>,SkiaSharp.SKEncodedOrigin&)
GetBitmap(System.String,System.Boolean,System.Nullable`1<MediaBrowser.Model.Drawing.ImageOrientation>)
GetBitmapFromSvg(System.String)
OrientImage(SkiaSharp.SKBitmap,SkiaSharp.SKEncodedOrigin)
ResizeImage(SkiaSharp.SKBitmap,SkiaSharp.SKImageInfo,System.Boolean,System.Boolean)
EncodeImage(System.String,System.DateTime,System.String,System.Boolean,System.Nullable`1<MediaBrowser.Model.Drawing.ImageOrientation>,System.Int32,MediaBrowser.Controller.Drawing.ImageProcessingOptions,MediaBrowser.Model.Drawing.ImageFormat)
CreateImageCollage(MediaBrowser.Controller.Drawing.ImageCollageOptions,System.String)
CreateSplashscreen(System.Collections.Generic.IReadOnlyList`1<System.String>,System.Collections.Generic.IReadOnlyList`1<System.String>)
CreateTrickplayTile(MediaBrowser.Controller.Drawing.ImageCollageOptions,System.Int32,System.Int32,System.Nullable`1<System.Int32>)
DrawIndicator(SkiaSharp.SKCanvas,System.Int32,System.Int32,MediaBrowser.Controller.Drawing.ImageProcessingOptions)
GetFontForCharacter(System.String)