< 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
11%
Covered lines: 39
Uncovered lines: 315
Coverable lines: 354
Total lines: 790
Line coverage: 11%
Branch coverage
1%
Covered branches: 2
Total branches: 162
Branch coverage: 1.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

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        using var codec = SKCodec.Create(safePath, out var result);
 213
 0214        switch (result)
 215        {
 216            case SKCodecResult.Success:
 217            // Skia/SkiaSharp edge‑case: when the image header is parsed but the actual pixel
 218            // decode fails (truncated JPEG/PNG, exotic ICC/EXIF, CMYK without color‑transform, etc.)
 219            // `SKCodec.Create` returns a *non‑null* codec together with
 220            // SKCodecResult.InternalError.  The header still contains valid dimensions,
 221            // which is all we need here – so we fall back to them instead of aborting.
 222            // See e.g. Skia bugs #4139, #6092.
 0223            case SKCodecResult.InternalError when codec is not null:
 0224                var info = codec.Info;
 0225                return new ImageDimensions(info.Width, info.Height);
 226
 227            case SKCodecResult.Unimplemented:
 0228                _logger.LogDebug("Image format not supported: {FilePath}", path);
 0229                return default;
 230
 231            default:
 232            {
 0233                var boundsInfo = SKBitmap.DecodeBounds(safePath);
 234
 0235                if (boundsInfo.Width > 0 && boundsInfo.Height > 0)
 236                {
 0237                    return new ImageDimensions(boundsInfo.Width, boundsInfo.Height);
 238                }
 239
 0240                _logger.LogWarning(
 0241                    "Unable to determine image dimensions for {FilePath}: {SkCodecResult}",
 0242                    path,
 0243                    result);
 0244                return default;
 245            }
 246        }
 0247    }
 248
 249    /// <inheritdoc />
 250    /// <exception cref="ArgumentNullException">The path is null.</exception>
 251    /// <exception cref="FileNotFoundException">The path is not valid.</exception>
 252    public string GetImageBlurHash(int xComp, int yComp, string path)
 253    {
 0254        ArgumentException.ThrowIfNullOrEmpty(path);
 255
 0256        var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
 0257        if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)
 0258            || extension.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase))
 259        {
 0260            _logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path);
 0261            return string.Empty;
 262        }
 263
 264        // Use FileStream with FileShare.Read instead of having Skia open the file to allow concurrent read access
 0265        using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
 266        // Any larger than 128x128 is too slow and there's no visually discernible difference
 0267        return BlurHashEncoder.Encode(xComp, yComp, fileStream, 128, 128);
 0268    }
 269
 270    private bool RequiresSpecialCharacterHack(string path)
 271    {
 0272        for (int i = 0; i < path.Length; i++)
 273        {
 0274            if (char.GetUnicodeCategory(path[i]) == UnicodeCategory.OtherLetter)
 275            {
 0276                return true;
 277            }
 278        }
 279
 0280        return path.HasDiacritics();
 281    }
 282
 283    private string NormalizePath(string path)
 284    {
 0285        if (!RequiresSpecialCharacterHack(path))
 286        {
 0287            return path;
 288        }
 289
 0290        var tempPath = Path.Join(_appPaths.TempDirectory, string.Concat("skia_", Guid.NewGuid().ToString(), Path.GetExte
 0291        var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPat
 0292        Directory.CreateDirectory(directory);
 0293        File.Copy(path, tempPath, true);
 294
 0295        return tempPath;
 296    }
 297
 298    private static SKEncodedOrigin GetSKEncodedOrigin(ImageOrientation? orientation)
 299    {
 0300        if (!orientation.HasValue)
 301        {
 0302            return SKEncodedOrigin.Default;
 303        }
 304
 0305        return (SKEncodedOrigin)orientation.Value;
 306    }
 307
 308    /// <summary>
 309    /// Decode an image.
 310    /// </summary>
 311    /// <param name="path">The filepath of the image to decode.</param>
 312    /// <param name="forceCleanBitmap">Whether to force clean the bitmap.</param>
 313    /// <param name="orientation">The orientation of the image.</param>
 314    /// <param name="origin">The detected origin of the image.</param>
 315    /// <returns>The resulting bitmap of the image.</returns>
 316    internal SKBitmap? Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin ori
 317    {
 0318        if (!File.Exists(path))
 319        {
 0320            throw new FileNotFoundException("File not found", path);
 321        }
 322
 0323        var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path));
 324
 0325        if (requiresTransparencyHack || forceCleanBitmap)
 326        {
 0327            using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res);
 0328            if (res != SKCodecResult.Success)
 329            {
 0330                origin = GetSKEncodedOrigin(orientation);
 0331                return null;
 332            }
 333
 0334            if (codec.FrameCount != 0)
 335            {
 0336                throw new ArgumentException("Cannot decode images with multiple frames");
 337            }
 338
 339            // create the bitmap
 0340            SKBitmap? bitmap = null;
 341            try
 342            {
 0343                bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
 344
 345                // decode
 0346                _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
 347
 0348                origin = codec.EncodedOrigin;
 349
 0350                return bitmap!;
 351            }
 0352            catch (Exception e)
 353            {
 0354                _logger.LogError(e, "Detected intermediary error decoding image {0}", path);
 0355                bitmap?.Dispose();
 0356                throw;
 357            }
 358        }
 359
 0360        var resultBitmap = SKBitmap.Decode(NormalizePath(path));
 361
 0362        if (resultBitmap is null)
 363        {
 0364            return Decode(path, true, orientation, out origin);
 365        }
 366
 367        try
 368        {
 369             // If we have to resize these they often end up distorted
 0370            if (resultBitmap.ColorType == SKColorType.Gray8)
 371            {
 0372                using (resultBitmap)
 373                {
 0374                    return Decode(path, true, orientation, out origin);
 375                }
 376            }
 377
 0378            origin = SKEncodedOrigin.TopLeft;
 0379            return resultBitmap;
 380        }
 0381        catch (Exception e)
 382        {
 0383            _logger.LogError(e, "Detected intermediary error decoding image {0}", path);
 0384            resultBitmap?.Dispose();
 0385            throw;
 386        }
 0387    }
 388
 389    private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation)
 390    {
 0391        if (autoOrient)
 392        {
 0393            var bitmap = Decode(path, true, orientation, out var origin);
 394
 0395            if (bitmap is not null && origin != SKEncodedOrigin.TopLeft)
 396            {
 0397                using (bitmap)
 398                {
 0399                    return OrientImage(bitmap, origin);
 400                }
 401            }
 402
 0403            return bitmap;
 404        }
 405
 0406        return Decode(path, false, orientation, out _);
 0407    }
 408
 409    private SKBitmap? GetBitmapFromSvg(string path)
 410    {
 0411        if (!File.Exists(path))
 412        {
 0413            throw new FileNotFoundException("File not found", path);
 414        }
 415
 0416        using var svg = SKSvg.CreateFromFile(path);
 0417        if (svg.Drawable is null)
 418        {
 0419            return null;
 420        }
 421
 0422        var width = (int)Math.Round(svg.Drawable.Bounds.Width);
 0423        var height = (int)Math.Round(svg.Drawable.Bounds.Height);
 424
 0425        SKBitmap? bitmap = null;
 426        try
 427        {
 0428            bitmap = new SKBitmap(width, height);
 0429            using var canvas = new SKCanvas(bitmap);
 0430            canvas.DrawPicture(svg.Picture);
 0431            canvas.Flush();
 0432            canvas.Save();
 433
 0434            return bitmap!;
 435        }
 0436        catch (Exception e)
 437        {
 0438            _logger.LogError(e, "Detected intermediary error extracting image {0}", path);
 0439            bitmap?.Dispose();
 0440            throw;
 441        }
 0442    }
 443
 444    private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
 445    {
 0446        var needsFlip = origin is SKEncodedOrigin.LeftBottom or SKEncodedOrigin.LeftTop or SKEncodedOrigin.RightBottom o
 0447        SKBitmap? rotated = null;
 448        try
 449        {
 0450            rotated = needsFlip
 0451                ? new SKBitmap(bitmap.Height, bitmap.Width)
 0452                : new SKBitmap(bitmap.Width, bitmap.Height);
 0453            using var surface = new SKCanvas(rotated);
 0454            var midX = (float)rotated.Width / 2;
 0455            var midY = (float)rotated.Height / 2;
 456
 457            switch (origin)
 458            {
 459                case SKEncodedOrigin.TopRight:
 0460                    surface.Scale(-1, 1, midX, midY);
 0461                    break;
 462                case SKEncodedOrigin.BottomRight:
 0463                    surface.RotateDegrees(180, midX, midY);
 0464                    break;
 465                case SKEncodedOrigin.BottomLeft:
 0466                    surface.Scale(1, -1, midX, midY);
 0467                    break;
 468                case SKEncodedOrigin.LeftTop:
 0469                    surface.Translate(0, -rotated.Height);
 0470                    surface.Scale(1, -1, midX, midY);
 0471                    surface.RotateDegrees(-90);
 0472                    break;
 473                case SKEncodedOrigin.RightTop:
 0474                    surface.Translate(rotated.Width, 0);
 0475                    surface.RotateDegrees(90);
 0476                    break;
 477                case SKEncodedOrigin.RightBottom:
 0478                    surface.Translate(rotated.Width, 0);
 0479                    surface.Scale(1, -1, midX, midY);
 0480                    surface.RotateDegrees(90);
 0481                    break;
 482                case SKEncodedOrigin.LeftBottom:
 0483                    surface.Translate(0, rotated.Height);
 0484                    surface.RotateDegrees(-90);
 485                    break;
 486            }
 487
 0488            surface.DrawBitmap(bitmap, 0, 0, DefaultSamplingOptions);
 0489            return rotated;
 490        }
 0491        catch (Exception e)
 492        {
 0493            _logger.LogError(e, "Detected intermediary error rotating image");
 0494            rotated?.Dispose();
 0495            throw;
 496        }
 0497    }
 498
 499    /// <summary>
 500    /// Resizes an image on the CPU, by utilizing a surface and canvas.
 501    ///
 502    /// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect.
 503    /// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP
 504    /// </summary>
 505    /// <param name="source">The source bitmap.</param>
 506    /// <param name="targetInfo">This specifies the target size and other information required to create the surface.</p
 507    /// <param name="isAntialias">This enables anti-aliasing on the SKPaint instance.</param>
 508    /// <param name="isDither">This enables dithering on the SKPaint instance.</param>
 509    /// <returns>The resized image.</returns>
 510    internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither
 511    {
 0512        using var surface = SKSurface.Create(targetInfo);
 0513        using var canvas = surface.Canvas;
 0514        using var paint = new SKPaint();
 0515        paint.IsAntialias = isAntialias;
 0516        paint.IsDither = isDither;
 517
 518        // Historically, kHigh implied cubic filtering, but only when upsampling.
 519        // If specified kHigh, and were down-sampling, Skia used to switch back to kMedium (bilinear filtering plus mipm
 520        // With current skia API, passing Mitchell cubic when down-sampling will cause serious quality degradation.
 0521        var samplingOptions = source.Width > targetInfo.Width || source.Height > targetInfo.Height
 0522            ? DefaultSamplingOptions
 0523            : UpscaleSamplingOptions;
 524
 0525        paint.ImageFilter = _imageFilter;
 0526        canvas.DrawBitmap(
 0527            source,
 0528            SKRect.Create(0, 0, source.Width, source.Height),
 0529            SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
 0530            samplingOptions,
 0531            paint);
 532
 0533        return surface.Snapshot();
 0534    }
 535
 536    /// <inheritdoc/>
 537    public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientat
 538    {
 0539        ArgumentException.ThrowIfNullOrEmpty(inputPath);
 0540        ArgumentException.ThrowIfNullOrEmpty(outputPath);
 541
 0542        var inputFormat = Path.GetExtension(inputPath.AsSpan()).TrimStart('.');
 0543        if (!SupportedInputFormats.Contains(inputFormat, StringComparison.OrdinalIgnoreCase))
 544        {
 0545            _logger.LogDebug("Unable to encode image due to unsupported format: {ImagePath}", inputPath);
 0546            return inputPath;
 547        }
 548
 0549        if (outputFormat == ImageFormat.Svg
 0550            && !inputFormat.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase))
 551        {
 0552            throw new ArgumentException($"Requested svg output from {inputFormat} input");
 553        }
 554
 0555        var skiaOutputFormat = GetImageFormat(outputFormat);
 556
 0557        var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
 0558        var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
 0559        var blur = options.Blur ?? 0;
 0560        var hasIndicator = options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
 561
 0562        using var bitmap = inputFormat.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase)
 0563            ? GetBitmapFromSvg(inputPath)
 0564            : GetBitmap(inputPath, autoOrient, orientation);
 565
 0566        if (bitmap is null)
 567        {
 0568            throw new InvalidDataException($"Skia unable to read image {inputPath}");
 569        }
 570
 0571        var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
 572
 0573        if (options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient)
 574        {
 575            // Just spit out the original file if all the options are default
 0576            return inputPath;
 577        }
 578
 0579        var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
 580
 0581        var width = newImageSize.Width;
 0582        var height = newImageSize.Height;
 583
 584        // scale image (the FromImage creates a copy)
 0585        var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
 0586        using var resizedImage = ResizeImage(bitmap, imageInfo);
 0587        using var resizedBitmap = SKBitmap.FromImage(resizedImage);
 588
 589        // If all we're doing is resizing then we can stop now
 0590        if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
 591        {
 0592            var outputDirectory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({out
 0593            Directory.CreateDirectory(outputDirectory);
 0594            using var outputStream = new SKFileWStream(outputPath);
 0595            using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
 0596            resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
 0597            return outputPath;
 598        }
 599
 600        // create bitmap to use for canvas drawing used to draw into bitmap
 0601        using var saveBitmap = new SKBitmap(width, height);
 0602        using var canvas = new SKCanvas(saveBitmap);
 603        // set background color if present
 0604        if (hasBackgroundColor)
 605        {
 0606            canvas.Clear(SKColor.Parse(options.BackgroundColor));
 607        }
 608
 0609        using var paint = new SKPaint();
 610        // Add blur if option is present
 0611        using var filter = blur > 0 ? SKImageFilter.CreateBlur(blur, blur) : null;
 0612        paint.ImageFilter = filter;
 613
 614        // create image from resized bitmap to apply blur
 0615        canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), DefaultSamplingOptions, paint);
 616
 617        // If foreground layer present then draw
 0618        if (hasForegroundColor)
 619        {
 0620            if (!double.TryParse(options.ForegroundLayer, out double opacity))
 621            {
 0622                opacity = .4;
 623            }
 624
 0625            canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
 626        }
 627
 0628        if (hasIndicator)
 629        {
 0630            DrawIndicator(canvas, width, height, options);
 631        }
 632
 0633        var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) 
 0634        Directory.CreateDirectory(directory);
 0635        using (var outputStream = new SKFileWStream(outputPath))
 636        {
 0637            using var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels());
 0638            pixmap.Encode(outputStream, skiaOutputFormat, quality);
 639        }
 640
 0641        return outputPath;
 0642    }
 643
 644    /// <inheritdoc/>
 645    public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
 646    {
 0647        double ratio = (double)options.Width / options.Height;
 648
 0649        if (ratio >= 1.4)
 650        {
 0651            new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, optio
 652        }
 0653        else if (ratio >= .9)
 654        {
 0655            new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, opti
 656        }
 657        else
 658        {
 659            // TODO: Create Poster collage capability
 0660            new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, opti
 661        }
 0662    }
 663
 664    /// <inheritdoc />
 665    public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
 666    {
 667        // Only generate the splash screen if we have at least one poster and at least one backdrop/thumbnail.
 9668        if (posters.Count > 0 && backdrops.Count > 0)
 669        {
 0670            var splashBuilder = new SplashscreenBuilder(this, _logger);
 0671            var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
 0672            splashBuilder.GenerateSplash(posters, backdrops, outputPath);
 673        }
 9674    }
 675
 676    /// <inheritdoc />
 677    public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
 678    {
 0679        var paths = options.InputPaths;
 0680        var tileWidth = options.Width;
 0681        var tileHeight = options.Height;
 682
 0683        if (paths.Count < 1)
 684        {
 0685            throw new ArgumentException("InputPaths cannot be empty.");
 686        }
 0687        else if (paths.Count > tileWidth * tileHeight)
 688        {
 0689            throw new ArgumentException($"InputPaths contains more images than would fit on {tileWidth}x{tileHeight} gri
 690        }
 691
 692        // If no height provided, use height of first image.
 0693        if (!imgHeight.HasValue)
 694        {
 0695            using var firstImg = Decode(paths[0], false, null, out _);
 696
 0697            if (firstImg is null)
 698            {
 0699                throw new InvalidDataException("Could not decode image data.");
 700            }
 701
 0702            if (firstImg.Width != imgWidth)
 703            {
 0704                throw new InvalidOperationException("Image width does not match provided width.");
 705            }
 706
 0707            imgHeight = firstImg.Height;
 708        }
 709
 710        // Make horizontal strips using every provided image.
 0711        using var tileGrid = new SKBitmap(imgWidth * tileWidth, imgHeight.Value * tileHeight);
 0712        using var canvas = new SKCanvas(tileGrid);
 713
 0714        var imgIndex = 0;
 0715        for (var y = 0; y < tileHeight; y++)
 716        {
 0717            for (var x = 0; x < tileWidth; x++)
 718            {
 0719                if (imgIndex >= paths.Count)
 720                {
 721                    break;
 722                }
 723
 0724                using var img = Decode(paths[imgIndex++], false, null, out _);
 725
 0726                if (img is null)
 727                {
 0728                    throw new InvalidDataException("Could not decode image data.");
 729                }
 730
 0731                if (img.Width != imgWidth)
 732                {
 0733                    throw new InvalidOperationException("Image width does not match provided width.");
 734                }
 735
 0736                if (img.Height != imgHeight)
 737                {
 0738                    throw new InvalidOperationException("Image height does not match first image height.");
 739                }
 740
 0741                canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value, DefaultSamplingOptions);
 742            }
 743        }
 744
 0745        using var outputStream = new SKFileWStream(options.OutputPath);
 0746        tileGrid.Encode(outputStream, SKEncodedImageFormat.Jpeg, quality);
 747
 0748        return imgHeight.Value;
 0749    }
 750
 751    private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
 752    {
 753        try
 754        {
 0755            var currentImageSize = new ImageDimensions(imageWidth, imageHeight);
 756
 0757            if (options.UnplayedCount.HasValue)
 758            {
 0759                UnplayedCountIndicator.DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value)
 760            }
 761
 0762            if (options.PercentPlayed > 0)
 763            {
 0764                PercentPlayedDrawer.Process(canvas, currentImageSize, options.PercentPlayed);
 765            }
 0766        }
 0767        catch (Exception ex)
 768        {
 0769            _logger.LogError(ex, "Error drawing indicator overlay");
 0770        }
 0771    }
 772
 773    /// <summary>
 774    /// Return the typeface that contains the glyph for the given character.
 775    /// </summary>
 776    /// <param name="c">The text character.</param>
 777    /// <returns>The typeface contains the character.</returns>
 778    public static SKTypeface? GetFontForCharacter(string c)
 779    {
 0780        foreach (var typeface in _typefaces)
 781        {
 0782            if (typeface.ContainsGlyphs(c))
 783            {
 0784                return typeface;
 785            }
 786        }
 787
 0788        return null;
 789    }
 790}

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)