< 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: 303
Coverable lines: 342
Total lines: 761
Line coverage: 11.4%
Branch coverage
1%
Covered branches: 2
Total branches: 152
Branch coverage: 1.3%
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        using var codec = SKCodec.Create(path, out SKCodecResult result);
 206        switch (result)
 207        {
 208            case SKCodecResult.Success:
 0209                var info = codec.Info;
 0210                return new ImageDimensions(info.Width, info.Height);
 211            case SKCodecResult.Unimplemented:
 0212                _logger.LogDebug("Image format not supported: {FilePath}", path);
 0213                return default;
 214            default:
 0215                _logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result);
 0216                return default;
 217        }
 0218    }
 219
 220    /// <inheritdoc />
 221    /// <exception cref="ArgumentNullException">The path is null.</exception>
 222    /// <exception cref="FileNotFoundException">The path is not valid.</exception>
 223    public string GetImageBlurHash(int xComp, int yComp, string path)
 224    {
 0225        ArgumentException.ThrowIfNullOrEmpty(path);
 226
 0227        var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
 0228        if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)
 0229            || extension.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase))
 230        {
 0231            _logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path);
 0232            return string.Empty;
 233        }
 234
 235        // Use FileStream with FileShare.Read instead of having Skia open the file to allow concurrent read access
 0236        using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
 237        // Any larger than 128x128 is too slow and there's no visually discernible difference
 0238        return BlurHashEncoder.Encode(xComp, yComp, fileStream, 128, 128);
 0239    }
 240
 241    private bool RequiresSpecialCharacterHack(string path)
 242    {
 0243        for (int i = 0; i < path.Length; i++)
 244        {
 0245            if (char.GetUnicodeCategory(path[i]) == UnicodeCategory.OtherLetter)
 246            {
 0247                return true;
 248            }
 249        }
 250
 0251        return path.HasDiacritics();
 252    }
 253
 254    private string NormalizePath(string path)
 255    {
 0256        if (!RequiresSpecialCharacterHack(path))
 257        {
 0258            return path;
 259        }
 260
 0261        var tempPath = Path.Join(_appPaths.TempDirectory, string.Concat("skia_", Guid.NewGuid().ToString(), Path.GetExte
 0262        var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPat
 0263        Directory.CreateDirectory(directory);
 0264        File.Copy(path, tempPath, true);
 265
 0266        return tempPath;
 267    }
 268
 269    private static SKEncodedOrigin GetSKEncodedOrigin(ImageOrientation? orientation)
 270    {
 0271        if (!orientation.HasValue)
 272        {
 0273            return SKEncodedOrigin.Default;
 274        }
 275
 0276        return (SKEncodedOrigin)orientation.Value;
 277    }
 278
 279    /// <summary>
 280    /// Decode an image.
 281    /// </summary>
 282    /// <param name="path">The filepath of the image to decode.</param>
 283    /// <param name="forceCleanBitmap">Whether to force clean the bitmap.</param>
 284    /// <param name="orientation">The orientation of the image.</param>
 285    /// <param name="origin">The detected origin of the image.</param>
 286    /// <returns>The resulting bitmap of the image.</returns>
 287    internal SKBitmap? Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin ori
 288    {
 0289        if (!File.Exists(path))
 290        {
 0291            throw new FileNotFoundException("File not found", path);
 292        }
 293
 0294        var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path));
 295
 0296        if (requiresTransparencyHack || forceCleanBitmap)
 297        {
 0298            using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res);
 0299            if (res != SKCodecResult.Success)
 300            {
 0301                origin = GetSKEncodedOrigin(orientation);
 0302                return null;
 303            }
 304
 0305            if (codec.FrameCount != 0)
 306            {
 0307                throw new ArgumentException("Cannot decode images with multiple frames");
 308            }
 309
 310            // create the bitmap
 0311            SKBitmap? bitmap = null;
 312            try
 313            {
 0314                bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
 315
 316                // decode
 0317                _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
 318
 0319                origin = codec.EncodedOrigin;
 320
 0321                return bitmap!;
 322            }
 0323            catch (Exception e)
 324            {
 0325                _logger.LogError(e, "Detected intermediary error decoding image {0}", path);
 0326                bitmap?.Dispose();
 0327                throw;
 328            }
 329        }
 330
 0331        var resultBitmap = SKBitmap.Decode(NormalizePath(path));
 332
 0333        if (resultBitmap is null)
 334        {
 0335            return Decode(path, true, orientation, out origin);
 336        }
 337
 338        try
 339        {
 340             // If we have to resize these they often end up distorted
 0341            if (resultBitmap.ColorType == SKColorType.Gray8)
 342            {
 0343                using (resultBitmap)
 344                {
 0345                    return Decode(path, true, orientation, out origin);
 346                }
 347            }
 348
 0349            origin = SKEncodedOrigin.TopLeft;
 0350            return resultBitmap;
 351        }
 0352        catch (Exception e)
 353        {
 0354            _logger.LogError(e, "Detected intermediary error decoding image {0}", path);
 0355            resultBitmap?.Dispose();
 0356            throw;
 357        }
 0358    }
 359
 360    private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation)
 361    {
 0362        if (autoOrient)
 363        {
 0364            var bitmap = Decode(path, true, orientation, out var origin);
 365
 0366            if (bitmap is not null && origin != SKEncodedOrigin.TopLeft)
 367            {
 0368                using (bitmap)
 369                {
 0370                    return OrientImage(bitmap, origin);
 371                }
 372            }
 373
 0374            return bitmap;
 375        }
 376
 0377        return Decode(path, false, orientation, out _);
 0378    }
 379
 380    private SKBitmap? GetBitmapFromSvg(string path)
 381    {
 0382        if (!File.Exists(path))
 383        {
 0384            throw new FileNotFoundException("File not found", path);
 385        }
 386
 0387        using var svg = SKSvg.CreateFromFile(path);
 0388        if (svg.Drawable is null)
 389        {
 0390            return null;
 391        }
 392
 0393        var width = (int)Math.Round(svg.Drawable.Bounds.Width);
 0394        var height = (int)Math.Round(svg.Drawable.Bounds.Height);
 395
 0396        SKBitmap? bitmap = null;
 397        try
 398        {
 0399            bitmap = new SKBitmap(width, height);
 0400            using var canvas = new SKCanvas(bitmap);
 0401            canvas.DrawPicture(svg.Picture);
 0402            canvas.Flush();
 0403            canvas.Save();
 404
 0405            return bitmap!;
 406        }
 0407        catch (Exception e)
 408        {
 0409            _logger.LogError(e, "Detected intermediary error extracting image {0}", path);
 0410            bitmap?.Dispose();
 0411            throw;
 412        }
 0413    }
 414
 415    private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
 416    {
 0417        var needsFlip = origin is SKEncodedOrigin.LeftBottom or SKEncodedOrigin.LeftTop or SKEncodedOrigin.RightBottom o
 0418        SKBitmap? rotated = null;
 419        try
 420        {
 0421            rotated = needsFlip
 0422                ? new SKBitmap(bitmap.Height, bitmap.Width)
 0423                : new SKBitmap(bitmap.Width, bitmap.Height);
 0424            using var surface = new SKCanvas(rotated);
 0425            var midX = (float)rotated.Width / 2;
 0426            var midY = (float)rotated.Height / 2;
 427
 428            switch (origin)
 429            {
 430                case SKEncodedOrigin.TopRight:
 0431                    surface.Scale(-1, 1, midX, midY);
 0432                    break;
 433                case SKEncodedOrigin.BottomRight:
 0434                    surface.RotateDegrees(180, midX, midY);
 0435                    break;
 436                case SKEncodedOrigin.BottomLeft:
 0437                    surface.Scale(1, -1, midX, midY);
 0438                    break;
 439                case SKEncodedOrigin.LeftTop:
 0440                    surface.Translate(0, -rotated.Height);
 0441                    surface.Scale(1, -1, midX, midY);
 0442                    surface.RotateDegrees(-90);
 0443                    break;
 444                case SKEncodedOrigin.RightTop:
 0445                    surface.Translate(rotated.Width, 0);
 0446                    surface.RotateDegrees(90);
 0447                    break;
 448                case SKEncodedOrigin.RightBottom:
 0449                    surface.Translate(rotated.Width, 0);
 0450                    surface.Scale(1, -1, midX, midY);
 0451                    surface.RotateDegrees(90);
 0452                    break;
 453                case SKEncodedOrigin.LeftBottom:
 0454                    surface.Translate(0, rotated.Height);
 0455                    surface.RotateDegrees(-90);
 456                    break;
 457            }
 458
 0459            surface.DrawBitmap(bitmap, 0, 0, DefaultSamplingOptions);
 0460            return rotated;
 461        }
 0462        catch (Exception e)
 463        {
 0464            _logger.LogError(e, "Detected intermediary error rotating image");
 0465            rotated?.Dispose();
 0466            throw;
 467        }
 0468    }
 469
 470    /// <summary>
 471    /// Resizes an image on the CPU, by utilizing a surface and canvas.
 472    ///
 473    /// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect.
 474    /// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP
 475    /// </summary>
 476    /// <param name="source">The source bitmap.</param>
 477    /// <param name="targetInfo">This specifies the target size and other information required to create the surface.</p
 478    /// <param name="isAntialias">This enables anti-aliasing on the SKPaint instance.</param>
 479    /// <param name="isDither">This enables dithering on the SKPaint instance.</param>
 480    /// <returns>The resized image.</returns>
 481    internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither
 482    {
 0483        using var surface = SKSurface.Create(targetInfo);
 0484        using var canvas = surface.Canvas;
 0485        using var paint = new SKPaint();
 0486        paint.IsAntialias = isAntialias;
 0487        paint.IsDither = isDither;
 488
 489        // Historically, kHigh implied cubic filtering, but only when upsampling.
 490        // If specified kHigh, and were down-sampling, Skia used to switch back to kMedium (bilinear filtering plus mipm
 491        // With current skia API, passing Mitchell cubic when down-sampling will cause serious quality degradation.
 0492        var samplingOptions = source.Width > targetInfo.Width || source.Height > targetInfo.Height
 0493            ? DefaultSamplingOptions
 0494            : UpscaleSamplingOptions;
 495
 0496        paint.ImageFilter = _imageFilter;
 0497        canvas.DrawBitmap(
 0498            source,
 0499            SKRect.Create(0, 0, source.Width, source.Height),
 0500            SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
 0501            samplingOptions,
 0502            paint);
 503
 0504        return surface.Snapshot();
 0505    }
 506
 507    /// <inheritdoc/>
 508    public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientat
 509    {
 0510        ArgumentException.ThrowIfNullOrEmpty(inputPath);
 0511        ArgumentException.ThrowIfNullOrEmpty(outputPath);
 512
 0513        var inputFormat = Path.GetExtension(inputPath.AsSpan()).TrimStart('.');
 0514        if (!SupportedInputFormats.Contains(inputFormat, StringComparison.OrdinalIgnoreCase))
 515        {
 0516            _logger.LogDebug("Unable to encode image due to unsupported format: {ImagePath}", inputPath);
 0517            return inputPath;
 518        }
 519
 0520        if (outputFormat == ImageFormat.Svg
 0521            && !inputFormat.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase))
 522        {
 0523            throw new ArgumentException($"Requested svg output from {inputFormat} input");
 524        }
 525
 0526        var skiaOutputFormat = GetImageFormat(outputFormat);
 527
 0528        var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
 0529        var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
 0530        var blur = options.Blur ?? 0;
 0531        var hasIndicator = options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
 532
 0533        using var bitmap = inputFormat.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase)
 0534            ? GetBitmapFromSvg(inputPath)
 0535            : GetBitmap(inputPath, autoOrient, orientation);
 536
 0537        if (bitmap is null)
 538        {
 0539            throw new InvalidDataException($"Skia unable to read image {inputPath}");
 540        }
 541
 0542        var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
 543
 0544        if (options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient)
 545        {
 546            // Just spit out the original file if all the options are default
 0547            return inputPath;
 548        }
 549
 0550        var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
 551
 0552        var width = newImageSize.Width;
 0553        var height = newImageSize.Height;
 554
 555        // scale image (the FromImage creates a copy)
 0556        var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
 0557        using var resizedImage = ResizeImage(bitmap, imageInfo);
 0558        using var resizedBitmap = SKBitmap.FromImage(resizedImage);
 559
 560        // If all we're doing is resizing then we can stop now
 0561        if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
 562        {
 0563            var outputDirectory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({out
 0564            Directory.CreateDirectory(outputDirectory);
 0565            using var outputStream = new SKFileWStream(outputPath);
 0566            using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
 0567            resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
 0568            return outputPath;
 569        }
 570
 571        // create bitmap to use for canvas drawing used to draw into bitmap
 0572        using var saveBitmap = new SKBitmap(width, height);
 0573        using var canvas = new SKCanvas(saveBitmap);
 574        // set background color if present
 0575        if (hasBackgroundColor)
 576        {
 0577            canvas.Clear(SKColor.Parse(options.BackgroundColor));
 578        }
 579
 0580        using var paint = new SKPaint();
 581        // Add blur if option is present
 0582        using var filter = blur > 0 ? SKImageFilter.CreateBlur(blur, blur) : null;
 0583        paint.ImageFilter = filter;
 584
 585        // create image from resized bitmap to apply blur
 0586        canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), DefaultSamplingOptions, paint);
 587
 588        // If foreground layer present then draw
 0589        if (hasForegroundColor)
 590        {
 0591            if (!double.TryParse(options.ForegroundLayer, out double opacity))
 592            {
 0593                opacity = .4;
 594            }
 595
 0596            canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
 597        }
 598
 0599        if (hasIndicator)
 600        {
 0601            DrawIndicator(canvas, width, height, options);
 602        }
 603
 0604        var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) 
 0605        Directory.CreateDirectory(directory);
 0606        using (var outputStream = new SKFileWStream(outputPath))
 607        {
 0608            using var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels());
 0609            pixmap.Encode(outputStream, skiaOutputFormat, quality);
 610        }
 611
 0612        return outputPath;
 0613    }
 614
 615    /// <inheritdoc/>
 616    public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
 617    {
 0618        double ratio = (double)options.Width / options.Height;
 619
 0620        if (ratio >= 1.4)
 621        {
 0622            new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, optio
 623        }
 0624        else if (ratio >= .9)
 625        {
 0626            new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, opti
 627        }
 628        else
 629        {
 630            // TODO: Create Poster collage capability
 0631            new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, opti
 632        }
 0633    }
 634
 635    /// <inheritdoc />
 636    public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
 637    {
 638        // Only generate the splash screen if we have at least one poster and at least one backdrop/thumbnail.
 9639        if (posters.Count > 0 && backdrops.Count > 0)
 640        {
 0641            var splashBuilder = new SplashscreenBuilder(this, _logger);
 0642            var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
 0643            splashBuilder.GenerateSplash(posters, backdrops, outputPath);
 644        }
 9645    }
 646
 647    /// <inheritdoc />
 648    public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
 649    {
 0650        var paths = options.InputPaths;
 0651        var tileWidth = options.Width;
 0652        var tileHeight = options.Height;
 653
 0654        if (paths.Count < 1)
 655        {
 0656            throw new ArgumentException("InputPaths cannot be empty.");
 657        }
 0658        else if (paths.Count > tileWidth * tileHeight)
 659        {
 0660            throw new ArgumentException($"InputPaths contains more images than would fit on {tileWidth}x{tileHeight} gri
 661        }
 662
 663        // If no height provided, use height of first image.
 0664        if (!imgHeight.HasValue)
 665        {
 0666            using var firstImg = Decode(paths[0], false, null, out _);
 667
 0668            if (firstImg is null)
 669            {
 0670                throw new InvalidDataException("Could not decode image data.");
 671            }
 672
 0673            if (firstImg.Width != imgWidth)
 674            {
 0675                throw new InvalidOperationException("Image width does not match provided width.");
 676            }
 677
 0678            imgHeight = firstImg.Height;
 679        }
 680
 681        // Make horizontal strips using every provided image.
 0682        using var tileGrid = new SKBitmap(imgWidth * tileWidth, imgHeight.Value * tileHeight);
 0683        using var canvas = new SKCanvas(tileGrid);
 684
 0685        var imgIndex = 0;
 0686        for (var y = 0; y < tileHeight; y++)
 687        {
 0688            for (var x = 0; x < tileWidth; x++)
 689            {
 0690                if (imgIndex >= paths.Count)
 691                {
 692                    break;
 693                }
 694
 0695                using var img = Decode(paths[imgIndex++], false, null, out _);
 696
 0697                if (img is null)
 698                {
 0699                    throw new InvalidDataException("Could not decode image data.");
 700                }
 701
 0702                if (img.Width != imgWidth)
 703                {
 0704                    throw new InvalidOperationException("Image width does not match provided width.");
 705                }
 706
 0707                if (img.Height != imgHeight)
 708                {
 0709                    throw new InvalidOperationException("Image height does not match first image height.");
 710                }
 711
 0712                canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value, DefaultSamplingOptions);
 713            }
 714        }
 715
 0716        using var outputStream = new SKFileWStream(options.OutputPath);
 0717        tileGrid.Encode(outputStream, SKEncodedImageFormat.Jpeg, quality);
 718
 0719        return imgHeight.Value;
 0720    }
 721
 722    private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
 723    {
 724        try
 725        {
 0726            var currentImageSize = new ImageDimensions(imageWidth, imageHeight);
 727
 0728            if (options.UnplayedCount.HasValue)
 729            {
 0730                UnplayedCountIndicator.DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value)
 731            }
 732
 0733            if (options.PercentPlayed > 0)
 734            {
 0735                PercentPlayedDrawer.Process(canvas, currentImageSize, options.PercentPlayed);
 736            }
 0737        }
 0738        catch (Exception ex)
 739        {
 0740            _logger.LogError(ex, "Error drawing indicator overlay");
 0741        }
 0742    }
 743
 744    /// <summary>
 745    /// Return the typeface that contains the glyph for the given character.
 746    /// </summary>
 747    /// <param name="c">The text character.</param>
 748    /// <returns>The typeface contains the character.</returns>
 749    public static SKTypeface? GetFontForCharacter(string c)
 750    {
 0751        foreach (var typeface in _typefaces)
 752        {
 0753            if (typeface.ContainsGlyphs(c))
 754            {
 0755                return typeface;
 756            }
 757        }
 758
 0759        return null;
 760    }
 761}

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)