diff options
| -rw-r--r-- | Emby.Drawing/NullImageEncoder.cs | 6 | ||||
| -rw-r--r-- | Jellyfin.Drawing.Skia/SkiaEncoder.cs | 7 | ||||
| -rw-r--r-- | Jellyfin.Drawing.Skia/SkiaHelper.cs | 37 | ||||
| -rw-r--r-- | Jellyfin.Drawing.Skia/SplashscreenBuilder.cs | 162 | ||||
| -rw-r--r-- | Jellyfin.Drawing.Skia/StripCollageBuilder.cs | 32 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Drawing/IImageEncoder.cs | 6 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Drawing/SplashscreenOptions.cs | 59 |
7 files changed, 279 insertions, 30 deletions
diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs index 1c05aa916..ed12f6acb 100644 --- a/Emby.Drawing/NullImageEncoder.cs +++ b/Emby.Drawing/NullImageEncoder.cs @@ -44,6 +44,12 @@ namespace Emby.Drawing } /// <inheritdoc /> + public void CreateSplashscreen(SplashscreenOptions options) + { + throw new NotImplementedException(); + } + + /// <inheritdoc /> public string GetImageBlurHash(int xComp, int yComp, string path) { throw new NotImplementedException(); diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 6d0a5ac2b..16de5d7fd 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -492,6 +492,13 @@ namespace Jellyfin.Drawing.Skia } } + /// <inheritdoc/> + public void CreateSplashscreen(SplashscreenOptions options) + { + var splashBuilder = new SplashscreenBuilder(this); + splashBuilder.GenerateSplash(options); + } + private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options) { try diff --git a/Jellyfin.Drawing.Skia/SkiaHelper.cs b/Jellyfin.Drawing.Skia/SkiaHelper.cs index f9c79c855..35dcebdab 100644 --- a/Jellyfin.Drawing.Skia/SkiaHelper.cs +++ b/Jellyfin.Drawing.Skia/SkiaHelper.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using SkiaSharp; namespace Jellyfin.Drawing.Skia @@ -19,5 +20,41 @@ namespace Jellyfin.Drawing.Skia throw new SkiaCodecException(result); } } + + /// <summary> + /// Gets the next valid image as a bitmap. + /// </summary> + /// <param name="skiaEncoder">The current skia encoder.</param> + /// <param name="paths">The list of image paths.</param> + /// <param name="currentIndex">The current checked indes.</param> + /// <param name="newIndex">The new index.</param> + /// <returns>A valid bitmap, or null if no bitmap exists after <c>currentIndex</c>.</returns> + public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList<string> paths, int currentIndex, out int newIndex) + { + var imagesTested = new Dictionary<int, int>(); + SKBitmap? bitmap = null; + + while (imagesTested.Count < paths.Count) + { + if (currentIndex >= paths.Count) + { + currentIndex = 0; + } + + bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _); + + imagesTested[currentIndex] = 0; + + currentIndex++; + + if (bitmap != null) + { + break; + } + } + + newIndex = currentIndex; + return bitmap; + } } } diff --git a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs new file mode 100644 index 000000000..8b6942be0 --- /dev/null +++ b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Drawing; +using SkiaSharp; + +namespace Jellyfin.Drawing.Skia +{ + /// <summary> + /// Used to build the splashscreen. + /// </summary> + public class SplashscreenBuilder + { + private const int Rows = 6; + private const int Spacing = 20; + + private readonly SkiaEncoder _skiaEncoder; + + private Random? _random; + private int _finalWidth; + private int _finalHeight; + + /// <summary> + /// Initializes a new instance of the <see cref="SplashscreenBuilder"/> class. + /// </summary> + /// <param name="skiaEncoder">The SkiaEncoder.</param> + public SplashscreenBuilder(SkiaEncoder skiaEncoder) + { + _skiaEncoder = skiaEncoder; + } + + /// <summary> + /// Generate a splashscreen. + /// </summary> + /// <param name="options">The options to generate the splashscreen.</param> + public void GenerateSplash(SplashscreenOptions options) + { + _finalWidth = options.Width; + _finalHeight = options.Height; + var wall = GenerateCollage(options.PortraitInputPaths, options.LandscapeInputPaths, options.ApplyFilter); + var transformed = Transform3D(wall); + + using var outputStream = new SKFileWStream(options.OutputPath); + using var pixmap = new SKPixmap(new SKImageInfo(_finalWidth, _finalHeight), transformed.GetPixels()); + pixmap.Encode(outputStream, StripCollageBuilder.GetEncodedFormat(options.OutputPath), 90); + } + + /// <summary> + /// Generates a collage of posters and landscape pictures. + /// </summary> + /// <param name="poster">The poster paths.</param> + /// <param name="backdrop">The landscape paths.</param> + /// <param name="applyFilter">Whether to apply the darkening filter.</param> + /// <returns>The created collage as a bitmap.</returns> + private SKBitmap GenerateCollage(IReadOnlyList<string> poster, IReadOnlyList<string> backdrop, bool applyFilter) + { + _random = new Random(); + + var posterIndex = 0; + var backdropIndex = 0; + + // use higher resolution than final image + var bitmap = new SKBitmap(_finalWidth * 3, _finalHeight * 2); + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.Black); + + int posterHeight = _finalHeight * 2 / 6; + + for (int i = 0; i < Rows; i++) + { + int imageCounter = _random.Next(0, 5); + int currentWidthPos = i * 75; + int currentHeight = i * (posterHeight + Spacing); + + while (currentWidthPos < _finalWidth * 3) + { + SKBitmap? currentImage; + + switch (imageCounter) + { + case 0: + case 2: + case 3: + currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, poster, posterIndex, out int newPosterIndex); + posterIndex = newPosterIndex; + break; + default: + currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrop, backdropIndex, out int newBackdropIndex); + backdropIndex = newBackdropIndex; + break; + } + + if (currentImage == null) + { + throw new ArgumentException("Not enough valid pictures provided to create a splashscreen!"); + } + + // resize to the same aspect as the original + var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height); + using var resizedBitmap = new SKBitmap(imageWidth, posterHeight); + currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High); + + // draw on canvas + canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight); + + currentWidthPos += imageWidth + Spacing; + + currentImage.Dispose(); + + if (imageCounter >= 4) + { + imageCounter = 0; + } + else + { + imageCounter++; + } + } + } + + if (applyFilter) + { + var paintColor = new SKPaint + { + Color = SKColors.Black.WithAlpha(0x50), + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(0, 0, _finalWidth * 3, _finalHeight * 2, paintColor); + } + + return bitmap; + } + + /// <summary> + /// Transform the collage in 3D space. + /// </summary> + /// <param name="input">The bitmap to transform.</param> + /// <returns>The transformed image.</returns> + private SKBitmap Transform3D(SKBitmap input) + { + var bitmap = new SKBitmap(_finalWidth, _finalHeight); + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.Black); + var matrix = new SKMatrix + { + ScaleX = 0.324108899f, + ScaleY = 0.563934922f, + SkewX = -0.244337708f, + SkewY = 0.0377609022f, + TransX = 42.0407715f, + TransY = -198.104706f, + Persp0 = -9.08959337E-05f, + Persp1 = 6.85242048E-05f, + Persp2 = 0.988209724f + }; + + canvas.SetMatrix(matrix); + canvas.DrawBitmap(input, 0, 0); + + return bitmap; + } + } +} diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index d1cc2255d..6bece9db6 100644 --- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -99,7 +99,7 @@ namespace Jellyfin.Drawing.Skia using var canvas = new SKCanvas(bitmap); canvas.Clear(SKColors.Black); - using var backdrop = GetNextValidImage(paths, 0, out _); + using var backdrop = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, 0, out _); if (backdrop == null) { return bitmap; @@ -152,34 +152,6 @@ namespace Jellyfin.Drawing.Skia return bitmap; } - private SKBitmap? GetNextValidImage(IReadOnlyList<string> paths, int currentIndex, out int newIndex) - { - var imagesTested = new Dictionary<int, int>(); - SKBitmap? bitmap = null; - - while (imagesTested.Count < paths.Count) - { - if (currentIndex >= paths.Count) - { - currentIndex = 0; - } - - bitmap = _skiaEncoder.Decode(paths[currentIndex], false, null, out _); - - imagesTested[currentIndex] = 0; - - currentIndex++; - - if (bitmap != null) - { - break; - } - } - - newIndex = currentIndex; - return bitmap; - } - private SKBitmap BuildSquareCollageBitmap(IReadOnlyList<string> paths, int width, int height) { var bitmap = new SKBitmap(width, height); @@ -192,7 +164,7 @@ namespace Jellyfin.Drawing.Skia { for (var y = 0; y < 2; y++) { - using var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex); + using var currentBitmap = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, imageIndex, out int newIndex); imageIndex = newIndex; if (currentBitmap == null) diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index 4e67cfee4..57d73699f 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -74,5 +74,11 @@ namespace MediaBrowser.Controller.Drawing /// <param name="options">The options to use when creating the collage.</param> /// <param name="libraryName">Optional. </param> void CreateImageCollage(ImageCollageOptions options, string? libraryName); + + /// <summary> + /// Creates a splashscreen image. + /// </summary> + /// <param name="options">The options to use when creating the splashscreen.</param> + void CreateSplashscreen(SplashscreenOptions options); } } diff --git a/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs b/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs new file mode 100644 index 000000000..d70773d8f --- /dev/null +++ b/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Drawing +{ + /// <summary> + /// Options used to generate the splashscreen. + /// </summary> + public class SplashscreenOptions + { + /// <summary> + /// Initializes a new instance of the <see cref="SplashscreenOptions"/> class. + /// </summary> + /// <param name="portraitInputPaths">The portrait input paths.</param> + /// <param name="landscapeInputPaths">The landscape input paths.</param> + /// <param name="outputPath">The output path.</param> + /// <param name="width">Optional. The image width.</param> + /// <param name="height">Optional. The image height.</param> + /// <param name="applyFilter">Optional. Apply a darkening filter.</param> + public SplashscreenOptions(IReadOnlyList<string> portraitInputPaths, IReadOnlyList<string> landscapeInputPaths, string outputPath, int width = 1920, int height = 1080, bool applyFilter = false) + { + PortraitInputPaths = portraitInputPaths; + LandscapeInputPaths = landscapeInputPaths; + OutputPath = outputPath; + Width = width; + Height = height; + ApplyFilter = applyFilter; + } + + /// <summary> + /// Gets or sets the poster input paths. + /// </summary> + public IReadOnlyList<string> PortraitInputPaths { get; set; } + + /// <summary> + /// Gets or sets the landscape input paths. + /// </summary> + public IReadOnlyList<string> LandscapeInputPaths { get; set; } + + /// <summary> + /// Gets or sets the output path. + /// </summary> + public string OutputPath { get; set; } + + /// <summary> + /// Gets or sets the width. + /// </summary> + public int Width { get; set; } + + /// <summary> + /// Gets or sets the height. + /// </summary> + public int Height { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to apply a darkening filter at the end. + /// </summary> + public bool ApplyFilter { get; set; } + } +} |
