< 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: 37
Uncovered lines: 303
Coverable lines: 340
Total lines: 742
Line coverage: 10.8%
Branch coverage
1%
Covered branches: 2
Total branches: 148
Branch coverage: 1.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

0255075100

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#pragma warning disable CA1810
 31    static SkiaEncoder()
 32#pragma warning restore CA1810
 33    {
 134        var kernel = new[]
 135        {
 136            0,    -.1f,    0,
 137            -.1f, 1.4f, -.1f,
 138            0,    -.1f,    0,
 139        };
 40
 141        var kernelSize = new SKSizeI(3, 3);
 142        var kernelOffset = new SKPointI(1, 1);
 143        _imageFilter = SKImageFilter.CreateMatrixConvolution(
 144            kernelSize,
 145            kernel,
 146            1f,
 147            0f,
 148            kernelOffset,
 149            SKShaderTileMode.Clamp,
 150            true);
 51
 52        // Initialize the list of typefaces
 53        // We have to statically build a list of typefaces because MatchCharacter only accepts a single character or cod
 54        // But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻
 155        _typefaces =
 156        [
 157            SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant
 158            SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant
 159            SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant
 160            SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant
 161            SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant
 162            SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant
 163            SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant
 164            SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Up
 165        ];
 166    }
 67
 68    /// <summary>
 69    /// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
 70    /// </summary>
 71    /// <param name="logger">The application logger.</param>
 72    /// <param name="appPaths">The application paths.</param>
 73    public SkiaEncoder(ILogger<SkiaEncoder> logger, IApplicationPaths appPaths)
 74    {
 2175        _logger = logger;
 2176        _appPaths = appPaths;
 2177    }
 78
 79    /// <inheritdoc/>
 080    public string Name => "Skia";
 81
 82    /// <inheritdoc/>
 083    public bool SupportsImageCollageCreation => true;
 84
 85    /// <inheritdoc/>
 086    public bool SupportsImageEncoding => true;
 87
 88    /// <inheritdoc/>
 89    public IReadOnlyCollection<string> SupportedInputFormats =>
 090        new HashSet<string>(StringComparer.OrdinalIgnoreCase)
 091        {
 092            "jpeg",
 093            "jpg",
 094            "png",
 095            "dng",
 096            "webp",
 097            "gif",
 098            "bmp",
 099            "ico",
 0100            "astc",
 0101            "ktx",
 0102            "pkm",
 0103            "wbmp",
 0104            // TODO: check if these are supported on multiple platforms
 0105            // https://github.com/google/skia/blob/master/infra/bots/recipes/test.py#L454
 0106            // working on windows at least
 0107            "cr2",
 0108            "nef",
 0109            "arw",
 0110            SvgFormat
 0111        };
 112
 113    /// <inheritdoc/>
 114    public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
 0115        => new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png, ImageFormat.Svg };
 116
 117    /// <summary>
 118    /// Gets the default typeface to use.
 119    /// </summary>
 0120    public static SKTypeface DefaultTypeFace => _typefaces.Last();
 121
 122    /// <summary>
 123    /// Check if the native lib is available.
 124    /// </summary>
 125    /// <returns>True if the native lib is available, otherwise false.</returns>
 126    public static bool IsNativeLibAvailable()
 127    {
 128        try
 129        {
 130            // test an operation that requires the native library
 21131            SKPMColor.PreMultiply(SKColors.Black);
 21132            return true;
 133        }
 0134        catch (Exception)
 135        {
 0136            return false;
 137        }
 21138    }
 139
 140    /// <summary>
 141    /// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
 142    /// </summary>
 143    /// <param name="selectedFormat">The format to convert.</param>
 144    /// <returns>The converted format.</returns>
 145    public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
 146    {
 0147        return selectedFormat switch
 0148        {
 0149            ImageFormat.Bmp => SKEncodedImageFormat.Bmp,
 0150            ImageFormat.Jpg => SKEncodedImageFormat.Jpeg,
 0151            ImageFormat.Gif => SKEncodedImageFormat.Gif,
 0152            ImageFormat.Webp => SKEncodedImageFormat.Webp,
 0153            _ => SKEncodedImageFormat.Png
 0154        };
 155    }
 156
 157    /// <inheritdoc />
 158    /// <exception cref="FileNotFoundException">The path is not valid.</exception>
 159    public ImageDimensions GetImageSize(string path)
 160    {
 0161        if (!File.Exists(path))
 162        {
 0163            throw new FileNotFoundException("File not found", path);
 164        }
 165
 0166        var extension = Path.GetExtension(path.AsSpan());
 0167        if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase))
 168        {
 0169            using var svg = new SKSvg();
 170            try
 171            {
 0172                using var picture = svg.Load(path);
 0173                if (picture is null)
 174                {
 0175                    _logger.LogError("Unable to determine image dimensions for {FilePath}", path);
 0176                    return default;
 177                }
 178
 0179                return new ImageDimensions(Convert.ToInt32(picture.CullRect.Width), Convert.ToInt32(picture.CullRect.Hei
 180            }
 0181            catch (FormatException skiaColorException)
 182            {
 183                // This exception is known to be thrown on vector images that define custom styles
 184                // Skia SVG is not able to handle that and as the repository is quite stale and has not received updates
 0185                _logger.LogDebug(skiaColorException, "There was a issue loading the requested svg file");
 0186                return default;
 187            }
 188        }
 189
 0190        using var codec = SKCodec.Create(path, out SKCodecResult result);
 191        switch (result)
 192        {
 193            case SKCodecResult.Success:
 0194                var info = codec.Info;
 0195                return new ImageDimensions(info.Width, info.Height);
 196            case SKCodecResult.Unimplemented:
 0197                _logger.LogDebug("Image format not supported: {FilePath}", path);
 0198                return default;
 199            default:
 0200                _logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result);
 0201                return default;
 202        }
 0203    }
 204
 205    /// <inheritdoc />
 206    /// <exception cref="ArgumentNullException">The path is null.</exception>
 207    /// <exception cref="FileNotFoundException">The path is not valid.</exception>
 208    public string GetImageBlurHash(int xComp, int yComp, string path)
 209    {
 0210        ArgumentException.ThrowIfNullOrEmpty(path);
 211
 0212        var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
 0213        if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)
 0214            || extension.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase))
 215        {
 0216            _logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path);
 0217            return string.Empty;
 218        }
 219
 220        // Use FileStream with FileShare.Read instead of having Skia open the file to allow concurrent read access
 0221        using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
 222        // Any larger than 128x128 is too slow and there's no visually discernible difference
 0223        return BlurHashEncoder.Encode(xComp, yComp, fileStream, 128, 128);
 0224    }
 225
 226    private bool RequiresSpecialCharacterHack(string path)
 227    {
 0228        for (int i = 0; i < path.Length; i++)
 229        {
 0230            if (char.GetUnicodeCategory(path[i]) == UnicodeCategory.OtherLetter)
 231            {
 0232                return true;
 233            }
 234        }
 235
 0236        return path.HasDiacritics();
 237    }
 238
 239    private string NormalizePath(string path)
 240    {
 0241        if (!RequiresSpecialCharacterHack(path))
 242        {
 0243            return path;
 244        }
 245
 0246        var tempPath = Path.Join(_appPaths.TempDirectory, string.Concat("skia_", Guid.NewGuid().ToString(), Path.GetExte
 0247        var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPat
 0248        Directory.CreateDirectory(directory);
 0249        File.Copy(path, tempPath, true);
 250
 0251        return tempPath;
 252    }
 253
 254    private static SKEncodedOrigin GetSKEncodedOrigin(ImageOrientation? orientation)
 255    {
 0256        if (!orientation.HasValue)
 257        {
 0258            return SKEncodedOrigin.Default;
 259        }
 260
 0261        return (SKEncodedOrigin)orientation.Value;
 262    }
 263
 264    /// <summary>
 265    /// Decode an image.
 266    /// </summary>
 267    /// <param name="path">The filepath of the image to decode.</param>
 268    /// <param name="forceCleanBitmap">Whether to force clean the bitmap.</param>
 269    /// <param name="orientation">The orientation of the image.</param>
 270    /// <param name="origin">The detected origin of the image.</param>
 271    /// <returns>The resulting bitmap of the image.</returns>
 272    internal SKBitmap? Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin ori
 273    {
 0274        if (!File.Exists(path))
 275        {
 0276            throw new FileNotFoundException("File not found", path);
 277        }
 278
 0279        var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path));
 280
 0281        if (requiresTransparencyHack || forceCleanBitmap)
 282        {
 0283            using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res);
 0284            if (res != SKCodecResult.Success)
 285            {
 0286                origin = GetSKEncodedOrigin(orientation);
 0287                return null;
 288            }
 289
 0290            if (codec.FrameCount != 0)
 291            {
 0292                throw new ArgumentException("Cannot decode images with multiple frames");
 293            }
 294
 295            // create the bitmap
 0296            SKBitmap? bitmap = null;
 297            try
 298            {
 0299                bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
 300
 301                // decode
 0302                _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
 303
 0304                origin = codec.EncodedOrigin;
 305
 0306                return bitmap!;
 307            }
 0308            catch (Exception e)
 309            {
 0310                _logger.LogError(e, "Detected intermediary error decoding image {0}", path);
 0311                bitmap?.Dispose();
 0312                throw;
 313            }
 314        }
 315
 0316        var resultBitmap = SKBitmap.Decode(NormalizePath(path));
 317
 0318        if (resultBitmap is null)
 319        {
 0320            return Decode(path, true, orientation, out origin);
 321        }
 322
 323        try
 324        {
 325             // If we have to resize these they often end up distorted
 0326            if (resultBitmap.ColorType == SKColorType.Gray8)
 327            {
 0328                using (resultBitmap)
 329                {
 0330                    return Decode(path, true, orientation, out origin);
 331                }
 332            }
 333
 0334            origin = SKEncodedOrigin.TopLeft;
 0335            return resultBitmap;
 336        }
 0337        catch (Exception e)
 338        {
 0339            _logger.LogError(e, "Detected intermediary error decoding image {0}", path);
 0340            resultBitmap?.Dispose();
 0341            throw;
 342        }
 0343    }
 344
 345    private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation)
 346    {
 0347        if (autoOrient)
 348        {
 0349            var bitmap = Decode(path, true, orientation, out var origin);
 350
 0351            if (bitmap is not null && origin != SKEncodedOrigin.TopLeft)
 352            {
 0353                using (bitmap)
 354                {
 0355                    return OrientImage(bitmap, origin);
 356                }
 357            }
 358
 0359            return bitmap;
 360        }
 361
 0362        return Decode(path, false, orientation, out _);
 0363    }
 364
 365    private SKBitmap? GetBitmapFromSvg(string path)
 366    {
 0367        if (!File.Exists(path))
 368        {
 0369            throw new FileNotFoundException("File not found", path);
 370        }
 371
 0372        using var svg = SKSvg.CreateFromFile(path);
 0373        if (svg.Drawable is null)
 374        {
 0375            return null;
 376        }
 377
 0378        var width = (int)Math.Round(svg.Drawable.Bounds.Width);
 0379        var height = (int)Math.Round(svg.Drawable.Bounds.Height);
 380
 0381        SKBitmap? bitmap = null;
 382        try
 383        {
 0384            bitmap = new SKBitmap(width, height);
 0385            using var canvas = new SKCanvas(bitmap);
 0386            canvas.DrawPicture(svg.Picture);
 0387            canvas.Flush();
 0388            canvas.Save();
 389
 0390            return bitmap!;
 391        }
 0392        catch (Exception e)
 393        {
 0394            _logger.LogError(e, "Detected intermediary error extracting image {0}", path);
 0395            bitmap?.Dispose();
 0396            throw;
 397        }
 0398    }
 399
 400    private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
 401    {
 0402        var needsFlip = origin is SKEncodedOrigin.LeftBottom or SKEncodedOrigin.LeftTop or SKEncodedOrigin.RightBottom o
 0403        SKBitmap? rotated = null;
 404        try
 405        {
 0406            rotated = needsFlip
 0407                ? new SKBitmap(bitmap.Height, bitmap.Width)
 0408                : new SKBitmap(bitmap.Width, bitmap.Height);
 0409            using var surface = new SKCanvas(rotated);
 0410            var midX = (float)rotated.Width / 2;
 0411            var midY = (float)rotated.Height / 2;
 412
 413            switch (origin)
 414            {
 415                case SKEncodedOrigin.TopRight:
 0416                    surface.Scale(-1, 1, midX, midY);
 0417                    break;
 418                case SKEncodedOrigin.BottomRight:
 0419                    surface.RotateDegrees(180, midX, midY);
 0420                    break;
 421                case SKEncodedOrigin.BottomLeft:
 0422                    surface.Scale(1, -1, midX, midY);
 0423                    break;
 424                case SKEncodedOrigin.LeftTop:
 0425                    surface.Translate(0, -rotated.Height);
 0426                    surface.Scale(1, -1, midX, midY);
 0427                    surface.RotateDegrees(-90);
 0428                    break;
 429                case SKEncodedOrigin.RightTop:
 0430                    surface.Translate(rotated.Width, 0);
 0431                    surface.RotateDegrees(90);
 0432                    break;
 433                case SKEncodedOrigin.RightBottom:
 0434                    surface.Translate(rotated.Width, 0);
 0435                    surface.Scale(1, -1, midX, midY);
 0436                    surface.RotateDegrees(90);
 0437                    break;
 438                case SKEncodedOrigin.LeftBottom:
 0439                    surface.Translate(0, rotated.Height);
 0440                    surface.RotateDegrees(-90);
 441                    break;
 442            }
 443
 0444            surface.DrawBitmap(bitmap, 0, 0);
 0445            return rotated;
 446        }
 0447        catch (Exception e)
 448        {
 0449            _logger.LogError(e, "Detected intermediary error rotating image");
 0450            rotated?.Dispose();
 0451            throw;
 452        }
 0453    }
 454
 455    /// <summary>
 456    /// Resizes an image on the CPU, by utilizing a surface and canvas.
 457    ///
 458    /// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect.
 459    /// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP
 460    /// </summary>
 461    /// <param name="source">The source bitmap.</param>
 462    /// <param name="targetInfo">This specifies the target size and other information required to create the surface.</p
 463    /// <param name="isAntialias">This enables anti-aliasing on the SKPaint instance.</param>
 464    /// <param name="isDither">This enables dithering on the SKPaint instance.</param>
 465    /// <returns>The resized image.</returns>
 466    internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither
 467    {
 0468        using var surface = SKSurface.Create(targetInfo);
 0469        using var canvas = surface.Canvas;
 0470        using var paint = new SKPaint
 0471        {
 0472            FilterQuality = SKFilterQuality.High,
 0473            IsAntialias = isAntialias,
 0474            IsDither = isDither
 0475        };
 476
 0477        paint.ImageFilter = _imageFilter;
 0478        canvas.DrawBitmap(
 0479            source,
 0480            SKRect.Create(0, 0, source.Width, source.Height),
 0481            SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
 0482            paint);
 483
 0484        return surface.Snapshot();
 0485    }
 486
 487    /// <inheritdoc/>
 488    public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientat
 489    {
 0490        ArgumentException.ThrowIfNullOrEmpty(inputPath);
 0491        ArgumentException.ThrowIfNullOrEmpty(outputPath);
 492
 0493        var inputFormat = Path.GetExtension(inputPath.AsSpan()).TrimStart('.');
 0494        if (!SupportedInputFormats.Contains(inputFormat, StringComparison.OrdinalIgnoreCase))
 495        {
 0496            _logger.LogDebug("Unable to encode image due to unsupported format: {ImagePath}", inputPath);
 0497            return inputPath;
 498        }
 499
 0500        if (outputFormat == ImageFormat.Svg
 0501            && !inputFormat.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase))
 502        {
 0503            throw new ArgumentException($"Requested svg output from {inputFormat} input");
 504        }
 505
 0506        var skiaOutputFormat = GetImageFormat(outputFormat);
 507
 0508        var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
 0509        var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
 0510        var blur = options.Blur ?? 0;
 0511        var hasIndicator = options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
 512
 0513        using var bitmap = inputFormat.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase)
 0514            ? GetBitmapFromSvg(inputPath)
 0515            : GetBitmap(inputPath, autoOrient, orientation);
 516
 0517        if (bitmap is null)
 518        {
 0519            throw new InvalidDataException($"Skia unable to read image {inputPath}");
 520        }
 521
 0522        var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
 523
 0524        if (options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient)
 525        {
 526            // Just spit out the original file if all the options are default
 0527            return inputPath;
 528        }
 529
 0530        var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
 531
 0532        var width = newImageSize.Width;
 0533        var height = newImageSize.Height;
 534
 535        // scale image (the FromImage creates a copy)
 0536        var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
 0537        using var resizedImage = ResizeImage(bitmap, imageInfo);
 0538        using var resizedBitmap = SKBitmap.FromImage(resizedImage);
 539
 540        // If all we're doing is resizing then we can stop now
 0541        if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
 542        {
 0543            var outputDirectory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({out
 0544            Directory.CreateDirectory(outputDirectory);
 0545            using var outputStream = new SKFileWStream(outputPath);
 0546            using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
 0547            resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
 0548            return outputPath;
 549        }
 550
 551        // create bitmap to use for canvas drawing used to draw into bitmap
 0552        using var saveBitmap = new SKBitmap(width, height);
 0553        using var canvas = new SKCanvas(saveBitmap);
 554        // set background color if present
 0555        if (hasBackgroundColor)
 556        {
 0557            canvas.Clear(SKColor.Parse(options.BackgroundColor));
 558        }
 559
 0560        using var paint = new SKPaint();
 561        // Add blur if option is present
 0562        using var filter = blur > 0 ? SKImageFilter.CreateBlur(blur, blur) : null;
 0563        paint.FilterQuality = SKFilterQuality.High;
 0564        paint.ImageFilter = filter;
 565
 566        // create image from resized bitmap to apply blur
 0567        canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
 568
 569        // If foreground layer present then draw
 0570        if (hasForegroundColor)
 571        {
 0572            if (!double.TryParse(options.ForegroundLayer, out double opacity))
 573            {
 0574                opacity = .4;
 575            }
 576
 0577            canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
 578        }
 579
 0580        if (hasIndicator)
 581        {
 0582            DrawIndicator(canvas, width, height, options);
 583        }
 584
 0585        var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) 
 0586        Directory.CreateDirectory(directory);
 0587        using (var outputStream = new SKFileWStream(outputPath))
 588        {
 0589            using var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels());
 0590            pixmap.Encode(outputStream, skiaOutputFormat, quality);
 591        }
 592
 0593        return outputPath;
 0594    }
 595
 596    /// <inheritdoc/>
 597    public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
 598    {
 0599        double ratio = (double)options.Width / options.Height;
 600
 0601        if (ratio >= 1.4)
 602        {
 0603            new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, optio
 604        }
 0605        else if (ratio >= .9)
 606        {
 0607            new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, opti
 608        }
 609        else
 610        {
 611            // TODO: Create Poster collage capability
 0612            new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, opti
 613        }
 0614    }
 615
 616    /// <inheritdoc />
 617    public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
 618    {
 619        // Only generate the splash screen if we have at least one poster and at least one backdrop/thumbnail.
 12620        if (posters.Count > 0 && backdrops.Count > 0)
 621        {
 0622            var splashBuilder = new SplashscreenBuilder(this, _logger);
 0623            var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
 0624            splashBuilder.GenerateSplash(posters, backdrops, outputPath);
 625        }
 12626    }
 627
 628    /// <inheritdoc />
 629    public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
 630    {
 0631        var paths = options.InputPaths;
 0632        var tileWidth = options.Width;
 0633        var tileHeight = options.Height;
 634
 0635        if (paths.Count < 1)
 636        {
 0637            throw new ArgumentException("InputPaths cannot be empty.");
 638        }
 0639        else if (paths.Count > tileWidth * tileHeight)
 640        {
 0641            throw new ArgumentException($"InputPaths contains more images than would fit on {tileWidth}x{tileHeight} gri
 642        }
 643
 644        // If no height provided, use height of first image.
 0645        if (!imgHeight.HasValue)
 646        {
 0647            using var firstImg = Decode(paths[0], false, null, out _);
 648
 0649            if (firstImg is null)
 650            {
 0651                throw new InvalidDataException("Could not decode image data.");
 652            }
 653
 0654            if (firstImg.Width != imgWidth)
 655            {
 0656                throw new InvalidOperationException("Image width does not match provided width.");
 657            }
 658
 0659            imgHeight = firstImg.Height;
 660        }
 661
 662        // Make horizontal strips using every provided image.
 0663        using var tileGrid = new SKBitmap(imgWidth * tileWidth, imgHeight.Value * tileHeight);
 0664        using var canvas = new SKCanvas(tileGrid);
 665
 0666        var imgIndex = 0;
 0667        for (var y = 0; y < tileHeight; y++)
 668        {
 0669            for (var x = 0; x < tileWidth; x++)
 670            {
 0671                if (imgIndex >= paths.Count)
 672                {
 673                    break;
 674                }
 675
 0676                using var img = Decode(paths[imgIndex++], false, null, out _);
 677
 0678                if (img is null)
 679                {
 0680                    throw new InvalidDataException("Could not decode image data.");
 681                }
 682
 0683                if (img.Width != imgWidth)
 684                {
 0685                    throw new InvalidOperationException("Image width does not match provided width.");
 686                }
 687
 0688                if (img.Height != imgHeight)
 689                {
 0690                    throw new InvalidOperationException("Image height does not match first image height.");
 691                }
 692
 0693                canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value);
 694            }
 695        }
 696
 0697        using var outputStream = new SKFileWStream(options.OutputPath);
 0698        tileGrid.Encode(outputStream, SKEncodedImageFormat.Jpeg, quality);
 699
 0700        return imgHeight.Value;
 0701    }
 702
 703    private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
 704    {
 705        try
 706        {
 0707            var currentImageSize = new ImageDimensions(imageWidth, imageHeight);
 708
 0709            if (options.UnplayedCount.HasValue)
 710            {
 0711                UnplayedCountIndicator.DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value)
 712            }
 713
 0714            if (options.PercentPlayed > 0)
 715            {
 0716                PercentPlayedDrawer.Process(canvas, currentImageSize, options.PercentPlayed);
 717            }
 0718        }
 0719        catch (Exception ex)
 720        {
 0721            _logger.LogError(ex, "Error drawing indicator overlay");
 0722        }
 0723    }
 724
 725    /// <summary>
 726    /// Return the typeface that contains the glyph for the given character.
 727    /// </summary>
 728    /// <param name="c">The text character.</param>
 729    /// <returns>The typeface contains the character.</returns>
 730    public static SKTypeface? GetFontForCharacter(string c)
 731    {
 0732        foreach (var typeface in _typefaces)
 733        {
 0734            if (typeface.ContainsGlyphs(c))
 735            {
 0736                return typeface;
 737            }
 738        }
 739
 0740        return null;
 741    }
 742}

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)