< Summary - Jellyfin

Information
Class: Jellyfin.Drawing.Skia.StripCollageBuilder
Assembly: Jellyfin.Drawing.Skia
File(s): /srv/git/jellyfin/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 80
Coverable lines: 80
Total lines: 203
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 28
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
GetEncodedFormat(...)0%110100%
BuildSquareCollage(...)100%210%
BuildThumbCollage(...)100%210%
BuildThumbCollageBitmap(...)0%156120%
BuildSquareCollageBitmap(...)0%4260%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.IO;
 4using System.Text.RegularExpressions;
 5using SkiaSharp;
 6using SkiaSharp.HarfBuzz;
 7
 8namespace Jellyfin.Drawing.Skia;
 9
 10/// <summary>
 11/// Used to build collages of multiple images arranged in vertical strips.
 12/// </summary>
 13public partial class StripCollageBuilder
 14{
 15    private readonly SkiaEncoder _skiaEncoder;
 16
 17    /// <summary>
 18    /// Initializes a new instance of the <see cref="StripCollageBuilder"/> class.
 19    /// </summary>
 20    /// <param name="skiaEncoder">The encoder to use for building collages.</param>
 21    public StripCollageBuilder(SkiaEncoder skiaEncoder)
 22    {
 023        _skiaEncoder = skiaEncoder;
 024    }
 25
 26    [GeneratedRegex(@"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsH
 27    private static partial Regex NonCjkPatternRegex();
 28
 29    [GeneratedRegex(@"\p{IsArabic}|\p{IsArmenian}|\p{IsHebrew}|\p{IsSyriac}|\p{IsThaana}")]
 30    private static partial Regex IsRtlTextRegex();
 31
 32    /// <summary>
 33    /// Check which format an image has been encoded with using its filename extension.
 34    /// </summary>
 35    /// <param name="outputPath">The path to the image to get the format for.</param>
 36    /// <returns>The image format.</returns>
 37    public static SKEncodedImageFormat GetEncodedFormat(string outputPath)
 38    {
 039        ArgumentNullException.ThrowIfNull(outputPath);
 40
 041        var ext = Path.GetExtension(outputPath.AsSpan());
 42
 043        if (ext.Equals(".jpg", StringComparison.OrdinalIgnoreCase)
 044            || ext.Equals(".jpeg", StringComparison.OrdinalIgnoreCase))
 45        {
 046            return SKEncodedImageFormat.Jpeg;
 47        }
 48
 049        if (ext.Equals(".webp", StringComparison.OrdinalIgnoreCase))
 50        {
 051            return SKEncodedImageFormat.Webp;
 52        }
 53
 054        if (ext.Equals(".gif", StringComparison.OrdinalIgnoreCase))
 55        {
 056            return SKEncodedImageFormat.Gif;
 57        }
 58
 059        if (ext.Equals(".bmp", StringComparison.OrdinalIgnoreCase))
 60        {
 061            return SKEncodedImageFormat.Bmp;
 62        }
 63
 64        // default to png
 065        return SKEncodedImageFormat.Png;
 66    }
 67
 68    /// <summary>
 69    /// Create a square collage.
 70    /// </summary>
 71    /// <param name="paths">The paths of the images to use in the collage.</param>
 72    /// <param name="outputPath">The path at which to place the resulting collage image.</param>
 73    /// <param name="width">The desired width of the collage.</param>
 74    /// <param name="height">The desired height of the collage.</param>
 75    public void BuildSquareCollage(IReadOnlyList<string> paths, string outputPath, int width, int height)
 76    {
 077        using var bitmap = BuildSquareCollageBitmap(paths, width, height);
 078        using var outputStream = new SKFileWStream(outputPath);
 079        using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
 080        pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
 081    }
 82
 83    /// <summary>
 84    /// Create a thumb collage.
 85    /// </summary>
 86    /// <param name="paths">The paths of the images to use in the collage.</param>
 87    /// <param name="outputPath">The path at which to place the resulting image.</param>
 88    /// <param name="width">The desired width of the collage.</param>
 89    /// <param name="height">The desired height of the collage.</param>
 90    /// <param name="libraryName">The name of the library to draw on the collage.</param>
 91    public void BuildThumbCollage(IReadOnlyList<string> paths, string outputPath, int width, int height, string? library
 92    {
 093        using var bitmap = BuildThumbCollageBitmap(paths, width, height, libraryName);
 094        using var outputStream = new SKFileWStream(outputPath);
 095        using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
 096        pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
 097    }
 98
 99    private SKBitmap BuildThumbCollageBitmap(IReadOnlyList<string> paths, int width, int height, string? libraryName)
 100    {
 0101        var bitmap = new SKBitmap(width, height);
 102
 0103        using var canvas = new SKCanvas(bitmap);
 0104        canvas.Clear(SKColors.Black);
 105
 0106        using var backdrop = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, 0, out _);
 0107        if (backdrop is null)
 108        {
 0109            return bitmap;
 110        }
 111
 112        // resize to the same aspect as the original
 0113        var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width);
 0114        using var residedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.Co
 115        // draw the backdrop
 0116        canvas.DrawImage(residedBackdrop, 0, 0);
 117
 118        // draw shadow rectangle
 0119        using var paintColor = new SKPaint
 0120        {
 0121            Color = SKColors.Black.WithAlpha(0x78),
 0122            Style = SKPaintStyle.Fill
 0123        };
 0124        canvas.DrawRect(0, 0, width, height, paintColor);
 125
 0126        var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontSt
 127
 128        // use the system fallback to find a typeface for the given CJK character
 0129        var filteredName = NonCjkPatternRegex().Replace(libraryName ?? string.Empty, string.Empty);
 0130        if (!string.IsNullOrEmpty(filteredName))
 131        {
 0132            typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFon
 133        }
 134
 135        // draw library name
 0136        using var textPaint = new SKPaint
 0137        {
 0138            Color = SKColors.White,
 0139            Style = SKPaintStyle.Fill,
 0140            TextSize = 112,
 0141            TextAlign = SKTextAlign.Center,
 0142            Typeface = typeFace,
 0143            IsAntialias = true
 0144        };
 145
 146        // scale down text to 90% of the width if text is larger than 95% of the width
 0147        var textWidth = textPaint.MeasureText(libraryName);
 0148        if (textWidth > width * 0.95)
 149        {
 0150            textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth;
 151        }
 152
 0153        if (string.IsNullOrWhiteSpace(libraryName))
 154        {
 0155            return bitmap;
 156        }
 157
 0158        if (IsRtlTextRegex().IsMatch(libraryName))
 159        {
 0160            canvas.DrawShapedText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPain
 161        }
 162        else
 163        {
 0164            canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint);
 165        }
 166
 0167        return bitmap;
 0168    }
 169
 170    private SKBitmap BuildSquareCollageBitmap(IReadOnlyList<string> paths, int width, int height)
 171    {
 0172        var bitmap = new SKBitmap(width, height);
 0173        var imageIndex = 0;
 0174        var cellWidth = width / 2;
 0175        var cellHeight = height / 2;
 176
 0177        using var canvas = new SKCanvas(bitmap);
 0178        for (var x = 0; x < 2; x++)
 179        {
 0180            for (var y = 0; y < 2; y++)
 181            {
 0182                using var currentBitmap = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, imageIndex, out int newIndex
 0183                imageIndex = newIndex;
 184
 0185                if (currentBitmap is null)
 186                {
 0187                    continue;
 188                }
 189
 190                // Scale image. The FromBitmap creates a copy
 0191                var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType,
 0192                using var resizeImage = SkiaEncoder.ResizeImage(currentBitmap, imageInfo);
 193
 194                // draw this image into the strip at the next position
 0195                var xPos = x * cellWidth;
 0196                var yPos = y * cellHeight;
 0197                canvas.DrawImage(resizeImage, xPos, yPos);
 198            }
 199        }
 200
 0201        return bitmap;
 0202    }
 203}