< 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
8%
Covered lines: 26
Uncovered lines: 278
Coverable lines: 304
Total lines: 667
Line coverage: 8.5%
Branch coverage
1%
Covered branches: 2
Total branches: 136
Branch coverage: 1.4%
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 BlurHashSharp.SkiaSharp;
 6using Jellyfin.Extensions;
 7using MediaBrowser.Common.Configuration;
 8using MediaBrowser.Common.Extensions;
 9using MediaBrowser.Controller.Drawing;
 10using MediaBrowser.Model.Drawing;
 11using Microsoft.Extensions.Logging;
 12using SkiaSharp;
 13using Svg.Skia;
 14
 15namespace Jellyfin.Drawing.Skia;
 16
 17/// <summary>
 18/// Image encoder that uses <see cref="SkiaSharp"/> to manipulate images.
 19/// </summary>
 20public class SkiaEncoder : IImageEncoder
 21{
 22    private const string SvgFormat = "svg";
 123    private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".g
 24    private readonly ILogger<SkiaEncoder> _logger;
 25    private readonly IApplicationPaths _appPaths;
 26    private static readonly SKImageFilter _imageFilter;
 27
 28#pragma warning disable CA1810
 29    static SkiaEncoder()
 30#pragma warning restore CA1810
 31    {
 132        var kernel = new[]
 133        {
 134            0,    -.1f,    0,
 135            -.1f, 1.4f, -.1f,
 136            0,    -.1f,    0,
 137        };
 38
 139        var kernelSize = new SKSizeI(3, 3);
 140        var kernelOffset = new SKPointI(1, 1);
 141        _imageFilter = SKImageFilter.CreateMatrixConvolution(
 142            kernelSize,
 143            kernel,
 144            1f,
 145            0f,
 146            kernelOffset,
 147            SKShaderTileMode.Clamp,
 148            true);
 149    }
 50
 51    /// <summary>
 52    /// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
 53    /// </summary>
 54    /// <param name="logger">The application logger.</param>
 55    /// <param name="appPaths">The application paths.</param>
 56    public SkiaEncoder(ILogger<SkiaEncoder> logger, IApplicationPaths appPaths)
 57    {
 2258        _logger = logger;
 2259        _appPaths = appPaths;
 2260    }
 61
 62    /// <inheritdoc/>
 063    public string Name => "Skia";
 64
 65    /// <inheritdoc/>
 066    public bool SupportsImageCollageCreation => true;
 67
 68    /// <inheritdoc/>
 069    public bool SupportsImageEncoding => true;
 70
 71    /// <inheritdoc/>
 72    public IReadOnlyCollection<string> SupportedInputFormats =>
 073        new HashSet<string>(StringComparer.OrdinalIgnoreCase)
 074        {
 075            "jpeg",
 076            "jpg",
 077            "png",
 078            "dng",
 079            "webp",
 080            "gif",
 081            "bmp",
 082            "ico",
 083            "astc",
 084            "ktx",
 085            "pkm",
 086            "wbmp",
 087            // TODO: check if these are supported on multiple platforms
 088            // https://github.com/google/skia/blob/master/infra/bots/recipes/test.py#L454
 089            // working on windows at least
 090            "cr2",
 091            "nef",
 092            "arw",
 093            SvgFormat
 094        };
 95
 96    /// <inheritdoc/>
 97    public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
 098        => new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png, ImageFormat.Svg };
 99
 100    /// <summary>
 101    /// Check if the native lib is available.
 102    /// </summary>
 103    /// <returns>True if the native lib is available, otherwise false.</returns>
 104    public static bool IsNativeLibAvailable()
 105    {
 106        try
 107        {
 108            // test an operation that requires the native library
 22109            SKPMColor.PreMultiply(SKColors.Black);
 22110            return true;
 111        }
 0112        catch (Exception)
 113        {
 0114            return false;
 115        }
 22116    }
 117
 118    /// <summary>
 119    /// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
 120    /// </summary>
 121    /// <param name="selectedFormat">The format to convert.</param>
 122    /// <returns>The converted format.</returns>
 123    public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
 124    {
 0125        return selectedFormat switch
 0126        {
 0127            ImageFormat.Bmp => SKEncodedImageFormat.Bmp,
 0128            ImageFormat.Jpg => SKEncodedImageFormat.Jpeg,
 0129            ImageFormat.Gif => SKEncodedImageFormat.Gif,
 0130            ImageFormat.Webp => SKEncodedImageFormat.Webp,
 0131            _ => SKEncodedImageFormat.Png
 0132        };
 133    }
 134
 135    /// <inheritdoc />
 136    /// <exception cref="FileNotFoundException">The path is not valid.</exception>
 137    public ImageDimensions GetImageSize(string path)
 138    {
 0139        if (!File.Exists(path))
 140        {
 0141            throw new FileNotFoundException("File not found", path);
 142        }
 143
 0144        var extension = Path.GetExtension(path.AsSpan());
 0145        if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase))
 146        {
 0147            using var svg = new SKSvg();
 148            try
 149            {
 0150                using var picture = svg.Load(path);
 0151                if (picture is null)
 152                {
 0153                    _logger.LogError("Unable to determine image dimensions for {FilePath}", path);
 0154                    return default;
 155                }
 156
 0157                return new ImageDimensions(Convert.ToInt32(picture.CullRect.Width), Convert.ToInt32(picture.CullRect.Hei
 158            }
 0159            catch (FormatException skiaColorException)
 160            {
 161                // This exception is known to be thrown on vector images that define custom styles
 162                // Skia SVG is not able to handle that and as the repository is quite stale and has not received updates
 0163                _logger.LogDebug(skiaColorException, "There was a issue loading the requested svg file");
 0164                return default;
 165            }
 166        }
 167
 0168        using var codec = SKCodec.Create(path, out SKCodecResult result);
 169        switch (result)
 170        {
 171            case SKCodecResult.Success:
 0172                var info = codec.Info;
 0173                return new ImageDimensions(info.Width, info.Height);
 174            case SKCodecResult.Unimplemented:
 0175                _logger.LogDebug("Image format not supported: {FilePath}", path);
 0176                return default;
 177            default:
 0178                _logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result);
 0179                return default;
 180        }
 0181    }
 182
 183    /// <inheritdoc />
 184    /// <exception cref="ArgumentNullException">The path is null.</exception>
 185    /// <exception cref="FileNotFoundException">The path is not valid.</exception>
 186    public string GetImageBlurHash(int xComp, int yComp, string path)
 187    {
 0188        ArgumentException.ThrowIfNullOrEmpty(path);
 189
 0190        var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
 0191        if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)
 0192            || extension.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase))
 193        {
 0194            _logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path);
 0195            return string.Empty;
 196        }
 197
 198        // Any larger than 128x128 is too slow and there's no visually discernible difference
 0199        return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
 200    }
 201
 202    private bool RequiresSpecialCharacterHack(string path)
 203    {
 0204        for (int i = 0; i < path.Length; i++)
 205        {
 0206            if (char.GetUnicodeCategory(path[i]) == UnicodeCategory.OtherLetter)
 207            {
 0208                return true;
 209            }
 210        }
 211
 0212        return path.HasDiacritics();
 213    }
 214
 215    private string NormalizePath(string path)
 216    {
 0217        if (!RequiresSpecialCharacterHack(path))
 218        {
 0219            return path;
 220        }
 221
 0222        var tempPath = Path.Join(_appPaths.TempDirectory, string.Concat("skia_", Guid.NewGuid().ToString(), Path.GetExte
 0223        var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPat
 0224        Directory.CreateDirectory(directory);
 0225        File.Copy(path, tempPath, true);
 226
 0227        return tempPath;
 228    }
 229
 230    private static SKEncodedOrigin GetSKEncodedOrigin(ImageOrientation? orientation)
 231    {
 0232        if (!orientation.HasValue)
 233        {
 0234            return SKEncodedOrigin.Default;
 235        }
 236
 0237        return (SKEncodedOrigin)orientation.Value;
 238    }
 239
 240    /// <summary>
 241    /// Decode an image.
 242    /// </summary>
 243    /// <param name="path">The filepath of the image to decode.</param>
 244    /// <param name="forceCleanBitmap">Whether to force clean the bitmap.</param>
 245    /// <param name="orientation">The orientation of the image.</param>
 246    /// <param name="origin">The detected origin of the image.</param>
 247    /// <returns>The resulting bitmap of the image.</returns>
 248    internal SKBitmap? Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin ori
 249    {
 0250        if (!File.Exists(path))
 251        {
 0252            throw new FileNotFoundException("File not found", path);
 253        }
 254
 0255        var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path));
 256
 0257        if (requiresTransparencyHack || forceCleanBitmap)
 258        {
 0259            using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res);
 0260            if (res != SKCodecResult.Success)
 261            {
 0262                origin = GetSKEncodedOrigin(orientation);
 0263                return null;
 264            }
 265
 0266            if (codec.FrameCount != 0)
 267            {
 0268                throw new ArgumentException("Cannot decode images with multiple frames");
 269            }
 270
 271            // create the bitmap
 0272            var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
 273
 274            // decode
 0275            _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
 276
 0277            origin = codec.EncodedOrigin;
 278
 0279            return bitmap;
 280        }
 281
 0282        var resultBitmap = SKBitmap.Decode(NormalizePath(path));
 283
 0284        if (resultBitmap is null)
 285        {
 0286            return Decode(path, true, orientation, out origin);
 287        }
 288
 289        // If we have to resize these they often end up distorted
 0290        if (resultBitmap.ColorType == SKColorType.Gray8)
 291        {
 0292            using (resultBitmap)
 293            {
 0294                return Decode(path, true, orientation, out origin);
 295            }
 296        }
 297
 0298        origin = SKEncodedOrigin.TopLeft;
 0299        return resultBitmap;
 0300    }
 301
 302    private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation)
 303    {
 0304        if (autoOrient)
 305        {
 0306            var bitmap = Decode(path, true, orientation, out var origin);
 307
 0308            if (bitmap is not null && origin != SKEncodedOrigin.TopLeft)
 309            {
 0310                using (bitmap)
 311                {
 0312                    return OrientImage(bitmap, origin);
 313                }
 314            }
 315
 0316            return bitmap;
 317        }
 318
 0319        return Decode(path, false, orientation, out _);
 0320    }
 321
 322    private SKBitmap? GetBitmapFromSvg(string path)
 323    {
 0324        if (!File.Exists(path))
 325        {
 0326            throw new FileNotFoundException("File not found", path);
 327        }
 328
 0329        using var svg = SKSvg.CreateFromFile(path);
 0330        if (svg.Drawable is null)
 331        {
 0332            return null;
 333        }
 334
 0335        var width = (int)Math.Round(svg.Drawable.Bounds.Width);
 0336        var height = (int)Math.Round(svg.Drawable.Bounds.Height);
 337
 0338        var bitmap = new SKBitmap(width, height);
 0339        using var canvas = new SKCanvas(bitmap);
 0340        canvas.DrawPicture(svg.Picture);
 0341        canvas.Flush();
 0342        canvas.Save();
 343
 0344        return bitmap;
 0345    }
 346
 347    private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
 348    {
 0349        var needsFlip = origin is SKEncodedOrigin.LeftBottom or SKEncodedOrigin.LeftTop or SKEncodedOrigin.RightBottom o
 0350        var rotated = needsFlip
 0351            ? new SKBitmap(bitmap.Height, bitmap.Width)
 0352            : new SKBitmap(bitmap.Width, bitmap.Height);
 0353        using var surface = new SKCanvas(rotated);
 0354        var midX = (float)rotated.Width / 2;
 0355        var midY = (float)rotated.Height / 2;
 356
 357        switch (origin)
 358        {
 359            case SKEncodedOrigin.TopRight:
 0360                surface.Scale(-1, 1, midX, midY);
 0361                break;
 362            case SKEncodedOrigin.BottomRight:
 0363                surface.RotateDegrees(180, midX, midY);
 0364                break;
 365            case SKEncodedOrigin.BottomLeft:
 0366                surface.Scale(1, -1, midX, midY);
 0367                break;
 368            case SKEncodedOrigin.LeftTop:
 0369                surface.Translate(0, -rotated.Height);
 0370                surface.Scale(1, -1, midX, midY);
 0371                surface.RotateDegrees(-90);
 0372                break;
 373            case SKEncodedOrigin.RightTop:
 0374                surface.Translate(rotated.Width, 0);
 0375                surface.RotateDegrees(90);
 0376                break;
 377            case SKEncodedOrigin.RightBottom:
 0378                surface.Translate(rotated.Width, 0);
 0379                surface.Scale(1, -1, midX, midY);
 0380                surface.RotateDegrees(90);
 0381                break;
 382            case SKEncodedOrigin.LeftBottom:
 0383                surface.Translate(0, rotated.Height);
 0384                surface.RotateDegrees(-90);
 385                break;
 386        }
 387
 0388        surface.DrawBitmap(bitmap, 0, 0);
 0389        return rotated;
 0390    }
 391
 392    /// <summary>
 393    /// Resizes an image on the CPU, by utilizing a surface and canvas.
 394    ///
 395    /// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect.
 396    /// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP
 397    /// </summary>
 398    /// <param name="source">The source bitmap.</param>
 399    /// <param name="targetInfo">This specifies the target size and other information required to create the surface.</p
 400    /// <param name="isAntialias">This enables anti-aliasing on the SKPaint instance.</param>
 401    /// <param name="isDither">This enables dithering on the SKPaint instance.</param>
 402    /// <returns>The resized image.</returns>
 403    internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither
 404    {
 0405        using var surface = SKSurface.Create(targetInfo);
 0406        using var canvas = surface.Canvas;
 0407        using var paint = new SKPaint
 0408        {
 0409            FilterQuality = SKFilterQuality.High,
 0410            IsAntialias = isAntialias,
 0411            IsDither = isDither
 0412        };
 413
 0414        paint.ImageFilter = _imageFilter;
 0415        canvas.DrawBitmap(
 0416            source,
 0417            SKRect.Create(0, 0, source.Width, source.Height),
 0418            SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
 0419            paint);
 420
 0421        return surface.Snapshot();
 0422    }
 423
 424    /// <inheritdoc/>
 425    public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientat
 426    {
 0427        ArgumentException.ThrowIfNullOrEmpty(inputPath);
 0428        ArgumentException.ThrowIfNullOrEmpty(outputPath);
 429
 0430        var inputFormat = Path.GetExtension(inputPath.AsSpan()).TrimStart('.');
 0431        if (!SupportedInputFormats.Contains(inputFormat, StringComparison.OrdinalIgnoreCase))
 432        {
 0433            _logger.LogDebug("Unable to encode image due to unsupported format: {ImagePath}", inputPath);
 0434            return inputPath;
 435        }
 436
 0437        if (outputFormat == ImageFormat.Svg
 0438            && !inputFormat.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase))
 439        {
 0440            throw new ArgumentException($"Requested svg output from {inputFormat} input");
 441        }
 442
 0443        var skiaOutputFormat = GetImageFormat(outputFormat);
 444
 0445        var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
 0446        var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
 0447        var blur = options.Blur ?? 0;
 0448        var hasIndicator = options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
 449
 0450        using var bitmap = inputFormat.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase)
 0451            ? GetBitmapFromSvg(inputPath)
 0452            : GetBitmap(inputPath, autoOrient, orientation);
 453
 0454        if (bitmap is null)
 455        {
 0456            throw new InvalidDataException($"Skia unable to read image {inputPath}");
 457        }
 458
 0459        var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
 460
 0461        if (options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient)
 462        {
 463            // Just spit out the original file if all the options are default
 0464            return inputPath;
 465        }
 466
 0467        var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
 468
 0469        var width = newImageSize.Width;
 0470        var height = newImageSize.Height;
 471
 472        // scale image (the FromImage creates a copy)
 0473        var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
 0474        using var resizedImage = ResizeImage(bitmap, imageInfo);
 0475        using var resizedBitmap = SKBitmap.FromImage(resizedImage);
 476
 477        // If all we're doing is resizing then we can stop now
 0478        if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
 479        {
 0480            var outputDirectory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({out
 0481            Directory.CreateDirectory(outputDirectory);
 0482            using var outputStream = new SKFileWStream(outputPath);
 0483            using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
 0484            resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
 0485            return outputPath;
 486        }
 487
 488        // create bitmap to use for canvas drawing used to draw into bitmap
 0489        using var saveBitmap = new SKBitmap(width, height);
 0490        using var canvas = new SKCanvas(saveBitmap);
 491        // set background color if present
 0492        if (hasBackgroundColor)
 493        {
 0494            canvas.Clear(SKColor.Parse(options.BackgroundColor));
 495        }
 496
 497        // Add blur if option is present
 0498        if (blur > 0)
 499        {
 500            // create image from resized bitmap to apply blur
 0501            using var paint = new SKPaint();
 0502            using var filter = SKImageFilter.CreateBlur(blur, blur);
 0503            paint.ImageFilter = filter;
 0504            canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
 505        }
 506        else
 507        {
 508            // draw resized bitmap onto canvas
 0509            canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
 510        }
 511
 512        // If foreground layer present then draw
 0513        if (hasForegroundColor)
 514        {
 0515            if (!double.TryParse(options.ForegroundLayer, out double opacity))
 516            {
 0517                opacity = .4;
 518            }
 519
 0520            canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
 521        }
 522
 0523        if (hasIndicator)
 524        {
 0525            DrawIndicator(canvas, width, height, options);
 526        }
 527
 0528        var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) 
 0529        Directory.CreateDirectory(directory);
 0530        using (var outputStream = new SKFileWStream(outputPath))
 531        {
 0532            using var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels());
 0533            pixmap.Encode(outputStream, skiaOutputFormat, quality);
 534        }
 535
 0536        return outputPath;
 0537    }
 538
 539    /// <inheritdoc/>
 540    public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
 541    {
 0542        double ratio = (double)options.Width / options.Height;
 543
 0544        if (ratio >= 1.4)
 545        {
 0546            new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, optio
 547        }
 0548        else if (ratio >= .9)
 549        {
 0550            new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, opti
 551        }
 552        else
 553        {
 554            // TODO: Create Poster collage capability
 0555            new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, opti
 556        }
 0557    }
 558
 559    /// <inheritdoc />
 560    public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
 561    {
 562        // Only generate the splash screen if we have at least one poster and at least one backdrop/thumbnail.
 19563        if (posters.Count > 0 && backdrops.Count > 0)
 564        {
 0565            var splashBuilder = new SplashscreenBuilder(this);
 0566            var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
 0567            splashBuilder.GenerateSplash(posters, backdrops, outputPath);
 568        }
 19569    }
 570
 571    /// <inheritdoc />
 572    public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
 573    {
 0574        var paths = options.InputPaths;
 0575        var tileWidth = options.Width;
 0576        var tileHeight = options.Height;
 577
 0578        if (paths.Count < 1)
 579        {
 0580            throw new ArgumentException("InputPaths cannot be empty.");
 581        }
 0582        else if (paths.Count > tileWidth * tileHeight)
 583        {
 0584            throw new ArgumentException($"InputPaths contains more images than would fit on {tileWidth}x{tileHeight} gri
 585        }
 586
 587        // If no height provided, use height of first image.
 0588        if (!imgHeight.HasValue)
 589        {
 0590            using var firstImg = Decode(paths[0], false, null, out _);
 591
 0592            if (firstImg is null)
 593            {
 0594                throw new InvalidDataException("Could not decode image data.");
 595            }
 596
 0597            if (firstImg.Width != imgWidth)
 598            {
 0599                throw new InvalidOperationException("Image width does not match provided width.");
 600            }
 601
 0602            imgHeight = firstImg.Height;
 603        }
 604
 605        // Make horizontal strips using every provided image.
 0606        using var tileGrid = new SKBitmap(imgWidth * tileWidth, imgHeight.Value * tileHeight);
 0607        using var canvas = new SKCanvas(tileGrid);
 608
 0609        var imgIndex = 0;
 0610        for (var y = 0; y < tileHeight; y++)
 611        {
 0612            for (var x = 0; x < tileWidth; x++)
 613            {
 0614                if (imgIndex >= paths.Count)
 615                {
 616                    break;
 617                }
 618
 0619                using var img = Decode(paths[imgIndex++], false, null, out _);
 620
 0621                if (img is null)
 622                {
 0623                    throw new InvalidDataException("Could not decode image data.");
 624                }
 625
 0626                if (img.Width != imgWidth)
 627                {
 0628                    throw new InvalidOperationException("Image width does not match provided width.");
 629                }
 630
 0631                if (img.Height != imgHeight)
 632                {
 0633                    throw new InvalidOperationException("Image height does not match first image height.");
 634                }
 635
 0636                canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value);
 637            }
 638        }
 639
 0640        using var outputStream = new SKFileWStream(options.OutputPath);
 0641        tileGrid.Encode(outputStream, SKEncodedImageFormat.Jpeg, quality);
 642
 0643        return imgHeight.Value;
 0644    }
 645
 646    private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
 647    {
 648        try
 649        {
 0650            var currentImageSize = new ImageDimensions(imageWidth, imageHeight);
 651
 0652            if (options.UnplayedCount.HasValue)
 653            {
 0654                UnplayedCountIndicator.DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value)
 655            }
 656
 0657            if (options.PercentPlayed > 0)
 658            {
 0659                PercentPlayedDrawer.Process(canvas, currentImageSize, options.PercentPlayed);
 660            }
 0661        }
 0662        catch (Exception ex)
 663        {
 0664            _logger.LogError(ex, "Error drawing indicator overlay");
 0665        }
 0666    }
 667}

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()
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)