Ensure Skia images are always disposed (#12786)

This commit is contained in:
JPVenson 2024-10-17 15:35:03 +02:00 committed by GitHub
parent 4251cbc277
commit 8b4fa42e49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 184 additions and 120 deletions

View File

@ -269,14 +269,24 @@ public class SkiaEncoder : IImageEncoder
} }
// create the bitmap // create the bitmap
var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack); SKBitmap? bitmap = null;
try
{
bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
// decode // decode
_ = codec.GetPixels(bitmap.Info, bitmap.GetPixels()); _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
origin = codec.EncodedOrigin; origin = codec.EncodedOrigin;
return bitmap; return bitmap!;
}
catch (Exception e)
{
_logger.LogError(e, "Detected intermediary error decoding image {0}", path);
bitmap?.Dispose();
throw;
}
} }
var resultBitmap = SKBitmap.Decode(NormalizePath(path)); var resultBitmap = SKBitmap.Decode(NormalizePath(path));
@ -286,17 +296,26 @@ public class SkiaEncoder : IImageEncoder
return Decode(path, true, orientation, out origin); return Decode(path, true, orientation, out origin);
} }
// If we have to resize these they often end up distorted try
if (resultBitmap.ColorType == SKColorType.Gray8)
{ {
using (resultBitmap) // If we have to resize these they often end up distorted
if (resultBitmap.ColorType == SKColorType.Gray8)
{ {
return Decode(path, true, orientation, out origin); using (resultBitmap)
{
return Decode(path, true, orientation, out origin);
}
} }
}
origin = SKEncodedOrigin.TopLeft; origin = SKEncodedOrigin.TopLeft;
return resultBitmap; return resultBitmap;
}
catch (Exception e)
{
_logger.LogError(e, "Detected intermediary error decoding image {0}", path);
resultBitmap?.Dispose();
throw;
}
} }
private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation) private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation)
@ -335,58 +354,78 @@ public class SkiaEncoder : IImageEncoder
var width = (int)Math.Round(svg.Drawable.Bounds.Width); var width = (int)Math.Round(svg.Drawable.Bounds.Width);
var height = (int)Math.Round(svg.Drawable.Bounds.Height); var height = (int)Math.Round(svg.Drawable.Bounds.Height);
var bitmap = new SKBitmap(width, height); SKBitmap? bitmap = null;
using var canvas = new SKCanvas(bitmap); try
canvas.DrawPicture(svg.Picture); {
canvas.Flush(); bitmap = new SKBitmap(width, height);
canvas.Save(); using var canvas = new SKCanvas(bitmap);
canvas.DrawPicture(svg.Picture);
canvas.Flush();
canvas.Save();
return bitmap; return bitmap!;
}
catch (Exception e)
{
_logger.LogError(e, "Detected intermediary error extracting image {0}", path);
bitmap?.Dispose();
throw;
}
} }
private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin) private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
{ {
var needsFlip = origin is SKEncodedOrigin.LeftBottom or SKEncodedOrigin.LeftTop or SKEncodedOrigin.RightBottom or SKEncodedOrigin.RightTop; var needsFlip = origin is SKEncodedOrigin.LeftBottom or SKEncodedOrigin.LeftTop or SKEncodedOrigin.RightBottom or SKEncodedOrigin.RightTop;
var rotated = needsFlip SKBitmap? rotated = null;
? new SKBitmap(bitmap.Height, bitmap.Width) try
: new SKBitmap(bitmap.Width, bitmap.Height);
using var surface = new SKCanvas(rotated);
var midX = (float)rotated.Width / 2;
var midY = (float)rotated.Height / 2;
switch (origin)
{ {
case SKEncodedOrigin.TopRight: rotated = needsFlip
surface.Scale(-1, 1, midX, midY); ? new SKBitmap(bitmap.Height, bitmap.Width)
break; : new SKBitmap(bitmap.Width, bitmap.Height);
case SKEncodedOrigin.BottomRight: using var surface = new SKCanvas(rotated);
surface.RotateDegrees(180, midX, midY); var midX = (float)rotated.Width / 2;
break; var midY = (float)rotated.Height / 2;
case SKEncodedOrigin.BottomLeft:
surface.Scale(1, -1, midX, midY);
break;
case SKEncodedOrigin.LeftTop:
surface.Translate(0, -rotated.Height);
surface.Scale(1, -1, midX, midY);
surface.RotateDegrees(-90);
break;
case SKEncodedOrigin.RightTop:
surface.Translate(rotated.Width, 0);
surface.RotateDegrees(90);
break;
case SKEncodedOrigin.RightBottom:
surface.Translate(rotated.Width, 0);
surface.Scale(1, -1, midX, midY);
surface.RotateDegrees(90);
break;
case SKEncodedOrigin.LeftBottom:
surface.Translate(0, rotated.Height);
surface.RotateDegrees(-90);
break;
}
surface.DrawBitmap(bitmap, 0, 0); switch (origin)
return rotated; {
case SKEncodedOrigin.TopRight:
surface.Scale(-1, 1, midX, midY);
break;
case SKEncodedOrigin.BottomRight:
surface.RotateDegrees(180, midX, midY);
break;
case SKEncodedOrigin.BottomLeft:
surface.Scale(1, -1, midX, midY);
break;
case SKEncodedOrigin.LeftTop:
surface.Translate(0, -rotated.Height);
surface.Scale(1, -1, midX, midY);
surface.RotateDegrees(-90);
break;
case SKEncodedOrigin.RightTop:
surface.Translate(rotated.Width, 0);
surface.RotateDegrees(90);
break;
case SKEncodedOrigin.RightBottom:
surface.Translate(rotated.Width, 0);
surface.Scale(1, -1, midX, midY);
surface.RotateDegrees(90);
break;
case SKEncodedOrigin.LeftBottom:
surface.Translate(0, rotated.Height);
surface.RotateDegrees(-90);
break;
}
surface.DrawBitmap(bitmap, 0, 0);
return rotated;
}
catch (Exception e)
{
_logger.LogError(e, "Detected intermediary error rotating image");
rotated?.Dispose();
throw;
}
} }
/// <summary> /// <summary>
@ -562,7 +601,7 @@ public class SkiaEncoder : IImageEncoder
// Only generate the splash screen if we have at least one poster and at least one backdrop/thumbnail. // Only generate the splash screen if we have at least one poster and at least one backdrop/thumbnail.
if (posters.Count > 0 && backdrops.Count > 0) if (posters.Count > 0 && backdrops.Count > 0)
{ {
var splashBuilder = new SplashscreenBuilder(this); var splashBuilder = new SplashscreenBuilder(this, _logger);
var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
splashBuilder.GenerateSplash(posters, backdrops, outputPath); splashBuilder.GenerateSplash(posters, backdrops, outputPath);
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using SkiaSharp; using SkiaSharp;
namespace Jellyfin.Drawing.Skia; namespace Jellyfin.Drawing.Skia;
@ -18,14 +19,17 @@ public class SplashscreenBuilder
private const int Spacing = 20; private const int Spacing = 20;
private readonly SkiaEncoder _skiaEncoder; private readonly SkiaEncoder _skiaEncoder;
private readonly ILogger _logger;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SplashscreenBuilder"/> class. /// Initializes a new instance of the <see cref="SplashscreenBuilder"/> class.
/// </summary> /// </summary>
/// <param name="skiaEncoder">The SkiaEncoder.</param> /// <param name="skiaEncoder">The SkiaEncoder.</param>
public SplashscreenBuilder(SkiaEncoder skiaEncoder) /// <param name="logger">The logger.</param>
public SplashscreenBuilder(SkiaEncoder skiaEncoder, ILogger logger)
{ {
_skiaEncoder = skiaEncoder; _skiaEncoder = skiaEncoder;
_logger = logger;
} }
/// <summary> /// <summary>
@ -55,65 +59,76 @@ public class SplashscreenBuilder
var posterIndex = 0; var posterIndex = 0;
var backdropIndex = 0; var backdropIndex = 0;
var bitmap = new SKBitmap(WallWidth, WallHeight); SKBitmap? bitmap = null;
using var canvas = new SKCanvas(bitmap); try
canvas.Clear(SKColors.Black);
int posterHeight = WallHeight / 6;
for (int i = 0; i < Rows; i++)
{ {
int imageCounter = Random.Shared.Next(0, 5); bitmap = new SKBitmap(WallWidth, WallHeight);
int currentWidthPos = i * 75; using var canvas = new SKCanvas(bitmap);
int currentHeight = i * (posterHeight + Spacing); canvas.Clear(SKColors.Black);
while (currentWidthPos < WallWidth) int posterHeight = WallHeight / 6;
for (int i = 0; i < Rows; i++)
{ {
SKBitmap? currentImage; int imageCounter = Random.Shared.Next(0, 5);
int currentWidthPos = i * 75;
int currentHeight = i * (posterHeight + Spacing);
switch (imageCounter) while (currentWidthPos < WallWidth)
{ {
case 0: SKBitmap? currentImage;
case 2:
case 3:
currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, posters, posterIndex, out int newPosterIndex);
posterIndex = newPosterIndex;
break;
default:
currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrops, backdropIndex, out int newBackdropIndex);
backdropIndex = newBackdropIndex;
break;
}
if (currentImage is null) switch (imageCounter)
{ {
throw new ArgumentException("Not enough valid pictures provided to create a splashscreen!"); case 0:
} case 2:
case 3:
currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, posters, posterIndex, out int newPosterIndex);
posterIndex = newPosterIndex;
break;
default:
currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrops, backdropIndex, out int newBackdropIndex);
backdropIndex = newBackdropIndex;
break;
}
// resize to the same aspect as the original if (currentImage is null)
var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height); {
using var resizedBitmap = new SKBitmap(imageWidth, posterHeight); throw new ArgumentException("Not enough valid pictures provided to create a splashscreen!");
currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High); }
// draw on canvas using (currentImage)
canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight); {
var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height);
using var resizedBitmap = new SKBitmap(imageWidth, posterHeight);
currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High);
currentWidthPos += imageWidth + Spacing; // draw on canvas
canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight);
currentImage.Dispose(); // resize to the same aspect as the original
currentWidthPos += imageWidth + Spacing;
}
if (imageCounter >= 4) if (imageCounter >= 4)
{ {
imageCounter = 0; imageCounter = 0;
} }
else else
{ {
imageCounter++; imageCounter++;
}
} }
} }
}
return bitmap; return bitmap;
}
catch (Exception e)
{
_logger.LogError(e, "Detected intermediary error creating splashscreen image");
bitmap?.Dispose();
throw;
}
} }
/// <summary> /// <summary>
@ -123,25 +138,35 @@ public class SplashscreenBuilder
/// <returns>The transformed image.</returns> /// <returns>The transformed image.</returns>
private SKBitmap Transform3D(SKBitmap input) private SKBitmap Transform3D(SKBitmap input)
{ {
var bitmap = new SKBitmap(FinalWidth, FinalHeight); SKBitmap? bitmap = null;
using var canvas = new SKCanvas(bitmap); try
canvas.Clear(SKColors.Black);
var matrix = new SKMatrix
{ {
ScaleX = 0.324108899f, bitmap = new SKBitmap(FinalWidth, FinalHeight);
ScaleY = 0.563934922f, using var canvas = new SKCanvas(bitmap);
SkewX = -0.244337708f, canvas.Clear(SKColors.Black);
SkewY = 0.0377609022f, var matrix = new SKMatrix
TransX = 42.0407715f, {
TransY = -198.104706f, ScaleX = 0.324108899f,
Persp0 = -9.08959337E-05f, ScaleY = 0.563934922f,
Persp1 = 6.85242048E-05f, SkewX = -0.244337708f,
Persp2 = 0.988209724f SkewY = 0.0377609022f,
}; TransX = 42.0407715f,
TransY = -198.104706f,
Persp0 = -9.08959337E-05f,
Persp1 = 6.85242048E-05f,
Persp2 = 0.988209724f
};
canvas.SetMatrix(matrix); canvas.SetMatrix(matrix);
canvas.DrawBitmap(input, 0, 0); canvas.DrawBitmap(input, 0, 0);
return bitmap; return bitmap;
}
catch (Exception e)
{
_logger.LogError(e, "Detected intermediary error creating splashscreen image transforming the image");
bitmap?.Dispose();
throw;
}
} }
} }