| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using SkiaSharp; |
| | 4 | |
|
| | 5 | | namespace Jellyfin.Drawing.Skia; |
| | 6 | |
|
| | 7 | | /// <summary> |
| | 8 | | /// Used to build the splashscreen. |
| | 9 | | /// </summary> |
| | 10 | | public class SplashscreenBuilder |
| | 11 | | { |
| | 12 | | private const int FinalWidth = 1920; |
| | 13 | | private const int FinalHeight = 1080; |
| | 14 | | // generated collage resolution should be higher than the final resolution |
| | 15 | | private const int WallWidth = FinalWidth * 3; |
| | 16 | | private const int WallHeight = FinalHeight * 2; |
| | 17 | | private const int Rows = 6; |
| | 18 | | private const int Spacing = 20; |
| | 19 | |
|
| | 20 | | private readonly SkiaEncoder _skiaEncoder; |
| | 21 | |
|
| | 22 | | /// <summary> |
| | 23 | | /// Initializes a new instance of the <see cref="SplashscreenBuilder"/> class. |
| | 24 | | /// </summary> |
| | 25 | | /// <param name="skiaEncoder">The SkiaEncoder.</param> |
| | 26 | | public SplashscreenBuilder(SkiaEncoder skiaEncoder) |
| | 27 | | { |
| 0 | 28 | | _skiaEncoder = skiaEncoder; |
| 0 | 29 | | } |
| | 30 | |
|
| | 31 | | /// <summary> |
| | 32 | | /// Generate a splashscreen. |
| | 33 | | /// </summary> |
| | 34 | | /// <param name="posters">The poster paths.</param> |
| | 35 | | /// <param name="backdrops">The landscape paths.</param> |
| | 36 | | /// <param name="outputPath">The output path.</param> |
| | 37 | | public void GenerateSplash(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops, string outputPath) |
| | 38 | | { |
| 0 | 39 | | using var wall = GenerateCollage(posters, backdrops); |
| 0 | 40 | | using var transformed = Transform3D(wall); |
| | 41 | |
|
| 0 | 42 | | using var outputStream = new SKFileWStream(outputPath); |
| 0 | 43 | | using var pixmap = new SKPixmap(new SKImageInfo(FinalWidth, FinalHeight), transformed.GetPixels()); |
| 0 | 44 | | pixmap.Encode(outputStream, StripCollageBuilder.GetEncodedFormat(outputPath), 90); |
| 0 | 45 | | } |
| | 46 | |
|
| | 47 | | /// <summary> |
| | 48 | | /// Generates a collage of posters and landscape pictures. |
| | 49 | | /// </summary> |
| | 50 | | /// <param name="posters">The poster paths.</param> |
| | 51 | | /// <param name="backdrops">The landscape paths.</param> |
| | 52 | | /// <returns>The created collage as a bitmap.</returns> |
| | 53 | | private SKBitmap GenerateCollage(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops) |
| | 54 | | { |
| 0 | 55 | | var posterIndex = 0; |
| 0 | 56 | | var backdropIndex = 0; |
| | 57 | |
|
| 0 | 58 | | var bitmap = new SKBitmap(WallWidth, WallHeight); |
| 0 | 59 | | using var canvas = new SKCanvas(bitmap); |
| 0 | 60 | | canvas.Clear(SKColors.Black); |
| | 61 | |
|
| 0 | 62 | | int posterHeight = WallHeight / 6; |
| | 63 | |
|
| 0 | 64 | | for (int i = 0; i < Rows; i++) |
| | 65 | | { |
| 0 | 66 | | int imageCounter = Random.Shared.Next(0, 5); |
| 0 | 67 | | int currentWidthPos = i * 75; |
| 0 | 68 | | int currentHeight = i * (posterHeight + Spacing); |
| | 69 | |
|
| 0 | 70 | | while (currentWidthPos < WallWidth) |
| | 71 | | { |
| | 72 | | SKBitmap? currentImage; |
| | 73 | |
|
| | 74 | | switch (imageCounter) |
| | 75 | | { |
| | 76 | | case 0: |
| | 77 | | case 2: |
| | 78 | | case 3: |
| 0 | 79 | | currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, posters, posterIndex, out int newPoste |
| 0 | 80 | | posterIndex = newPosterIndex; |
| 0 | 81 | | break; |
| | 82 | | default: |
| 0 | 83 | | currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrops, backdropIndex, out int newB |
| 0 | 84 | | backdropIndex = newBackdropIndex; |
| | 85 | | break; |
| | 86 | | } |
| | 87 | |
|
| 0 | 88 | | if (currentImage is null) |
| | 89 | | { |
| 0 | 90 | | throw new ArgumentException("Not enough valid pictures provided to create a splashscreen!"); |
| | 91 | | } |
| | 92 | |
|
| | 93 | | // resize to the same aspect as the original |
| 0 | 94 | | var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height); |
| 0 | 95 | | using var resizedBitmap = new SKBitmap(imageWidth, posterHeight); |
| 0 | 96 | | currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High); |
| | 97 | |
|
| | 98 | | // draw on canvas |
| 0 | 99 | | canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight); |
| | 100 | |
|
| 0 | 101 | | currentWidthPos += imageWidth + Spacing; |
| | 102 | |
|
| 0 | 103 | | currentImage.Dispose(); |
| | 104 | |
|
| 0 | 105 | | if (imageCounter >= 4) |
| | 106 | | { |
| 0 | 107 | | imageCounter = 0; |
| | 108 | | } |
| | 109 | | else |
| | 110 | | { |
| 0 | 111 | | imageCounter++; |
| | 112 | | } |
| | 113 | | } |
| | 114 | | } |
| | 115 | |
|
| 0 | 116 | | return bitmap; |
| 0 | 117 | | } |
| | 118 | |
|
| | 119 | | /// <summary> |
| | 120 | | /// Transform the collage in 3D space. |
| | 121 | | /// </summary> |
| | 122 | | /// <param name="input">The bitmap to transform.</param> |
| | 123 | | /// <returns>The transformed image.</returns> |
| | 124 | | private SKBitmap Transform3D(SKBitmap input) |
| | 125 | | { |
| 0 | 126 | | var bitmap = new SKBitmap(FinalWidth, FinalHeight); |
| 0 | 127 | | using var canvas = new SKCanvas(bitmap); |
| 0 | 128 | | canvas.Clear(SKColors.Black); |
| 0 | 129 | | var matrix = new SKMatrix |
| 0 | 130 | | { |
| 0 | 131 | | ScaleX = 0.324108899f, |
| 0 | 132 | | ScaleY = 0.563934922f, |
| 0 | 133 | | SkewX = -0.244337708f, |
| 0 | 134 | | SkewY = 0.0377609022f, |
| 0 | 135 | | TransX = 42.0407715f, |
| 0 | 136 | | TransY = -198.104706f, |
| 0 | 137 | | Persp0 = -9.08959337E-05f, |
| 0 | 138 | | Persp1 = 6.85242048E-05f, |
| 0 | 139 | | Persp2 = 0.988209724f |
| 0 | 140 | | }; |
| | 141 | |
|
| 0 | 142 | | canvas.SetMatrix(matrix); |
| 0 | 143 | | canvas.DrawBitmap(input, 0, 0); |
| | 144 | |
|
| 0 | 145 | | return bitmap; |
| 0 | 146 | | } |
| | 147 | | } |