[backend/drive] Fix ImageSharp memory leak, improve image processing memory footprint, don't generate thumbnails for animated images
This commit is contained in:
parent
dd062c6752
commit
d56eda8464
2 changed files with 53 additions and 25 deletions
|
@ -7,6 +7,7 @@ using Iceshrimp.Backend.Core.Services;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using SixLabors.ImageSharp.Memory;
|
||||||
using WebPush;
|
using WebPush;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Extensions;
|
namespace Iceshrimp.Backend.Core.Extensions;
|
||||||
|
@ -193,6 +194,9 @@ public static class WebApplicationExtensions
|
||||||
app.Logger.LogInformation("Warming up meta cache...");
|
app.Logger.LogInformation("Warming up meta cache...");
|
||||||
await meta.WarmupCache();
|
await meta.WarmupCache();
|
||||||
|
|
||||||
|
SixLabors.ImageSharp.Configuration.Default.MemoryAllocator =
|
||||||
|
MemoryAllocator.Create(new MemoryAllocatorOptions { AllocationLimitMegabytes = 20 });
|
||||||
|
|
||||||
app.Logger.LogInformation("Initializing application, please wait...");
|
app.Logger.LogInformation("Initializing application, please wait...");
|
||||||
|
|
||||||
return instanceConfig;
|
return instanceConfig;
|
||||||
|
|
|
@ -11,6 +11,7 @@ using Iceshrimp.Backend.Core.Queues;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Formats;
|
||||||
using SixLabors.ImageSharp.Formats.Webp;
|
using SixLabors.ImageSharp.Formats.Webp;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
@ -111,7 +112,7 @@ public class DriveService(
|
||||||
|
|
||||||
public async Task<DriveFile> StoreFile(Stream data, User user, DriveFileCreationRequest request)
|
public async Task<DriveFile> StoreFile(Stream data, User user, DriveFileCreationRequest request)
|
||||||
{
|
{
|
||||||
var buf = new BufferedStream(data);
|
await using var buf = new BufferedStream(data);
|
||||||
var digest = await DigestHelpers.Sha256DigestAsync(buf);
|
var digest = await DigestHelpers.Sha256DigestAsync(buf);
|
||||||
logger.LogDebug("Storing file {digest} for user {userId}", digest, user.Id);
|
logger.LogDebug("Storing file {digest} for user {userId}", digest, user.Id);
|
||||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Sha256 == digest);
|
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Sha256 == digest);
|
||||||
|
@ -148,50 +149,73 @@ public class DriveService(
|
||||||
|
|
||||||
DriveFile.FileProperties? properties = null;
|
DriveFile.FileProperties? properties = null;
|
||||||
|
|
||||||
if (request.MimeType.StartsWith("image/") || request.MimeType == "image")
|
var isImage = request.MimeType.StartsWith("image/") || request.MimeType == "image";
|
||||||
|
// skip images larger than 10MB
|
||||||
|
var isReasonableSize = buf.Length < 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
if (isImage && isReasonableSize)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var image = await Image.LoadAsync<Rgba32>(buf);
|
var ident = await Image.IdentifyAsync(buf);
|
||||||
|
var isAnimated = ident.FrameMetadataCollection.Count != 0;
|
||||||
|
properties = new DriveFile.FileProperties { Width = ident.Size.Width, Height = ident.Size.Height };
|
||||||
|
|
||||||
|
// Correct mime type
|
||||||
|
if (request.MimeType == "image" && ident.Metadata.DecodedImageFormat?.DefaultMimeType != null)
|
||||||
|
request.MimeType = ident.Metadata.DecodedImageFormat.DefaultMimeType;
|
||||||
|
|
||||||
|
buf.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
var limit = user.Host == null && !isAnimated
|
||||||
|
? 2048
|
||||||
|
: shouldStore
|
||||||
|
? 1024
|
||||||
|
: 200;
|
||||||
|
|
||||||
|
var width = Math.Min(ident.Width, limit);
|
||||||
|
var height = Math.Min(ident.Height, limit);
|
||||||
|
var size = new Size(width, height);
|
||||||
|
|
||||||
|
var options = new DecoderOptions { MaxFrames = 1, TargetSize = size };
|
||||||
|
using var image = await Image.LoadAsync<Rgba32>(options, buf);
|
||||||
|
|
||||||
image.Mutate(x => x.AutoOrient());
|
image.Mutate(x => x.AutoOrient());
|
||||||
|
|
||||||
// Calculate blurhash using a x200px image for improved performance
|
// Calculate blurhash using a x200px image for improved performance
|
||||||
var blurhashImage = image.Clone();
|
using var blurhashImage = image.Clone();
|
||||||
blurhashImage.Mutate(p => p.Resize(image.Width > image.Height ? new Size(200, 0) : new Size(0, 200)));
|
var blurOpts = new ResizeOptions { Size = new Size(200, 200), Mode = ResizeMode.Max };
|
||||||
|
blurhashImage.Mutate(p => p.Resize(blurOpts));
|
||||||
blurhash = Blurhasher.Encode(blurhashImage, 7, 7);
|
blurhash = Blurhasher.Encode(blurhashImage, 7, 7);
|
||||||
|
|
||||||
// Correct mime type
|
|
||||||
if (request.MimeType == "image" && image.Metadata.DecodedImageFormat?.DefaultMimeType != null)
|
|
||||||
request.MimeType = image.Metadata.DecodedImageFormat.DefaultMimeType;
|
|
||||||
|
|
||||||
properties = new DriveFile.FileProperties { Width = image.Size.Width, Height = image.Size.Height };
|
|
||||||
|
|
||||||
if (shouldStore)
|
if (shouldStore)
|
||||||
{
|
{
|
||||||
// Generate thumbnail
|
// Generate thumbnail
|
||||||
var thumbnailImage = image.Clone();
|
using var thumbnailImage = image.Clone();
|
||||||
thumbnailImage.Metadata.ExifProfile = null;
|
thumbnailImage.Metadata.ExifProfile = null;
|
||||||
thumbnailImage.Metadata.XmpProfile = null;
|
thumbnailImage.Metadata.XmpProfile = null;
|
||||||
if (Math.Max(image.Size.Width, image.Size.Height) > 1000)
|
if (Math.Max(image.Size.Width, image.Size.Height) > 1000)
|
||||||
thumbnailImage.Mutate(p => p.Resize(image.Width > image.Height
|
{
|
||||||
? new Size(1000, 0)
|
var thumbOpts = new ResizeOptions { Size = new Size(1000, 1000), Mode = ResizeMode.Max };
|
||||||
: new Size(0, 1000)));
|
thumbnailImage.Mutate(p => p.Resize(thumbOpts));
|
||||||
|
}
|
||||||
|
|
||||||
thumbnail = new MemoryStream();
|
thumbnail = new MemoryStream();
|
||||||
var thumbEncoder = new WebpEncoder { Quality = 75, FileFormat = WebpFileFormatType.Lossy };
|
var thumbEncoder = new WebpEncoder { Quality = 75, FileFormat = WebpFileFormatType.Lossy };
|
||||||
await thumbnailImage.SaveAsWebpAsync(thumbnail, thumbEncoder);
|
await thumbnailImage.SaveAsWebpAsync(thumbnail, thumbEncoder);
|
||||||
thumbnail.Seek(0, SeekOrigin.Begin);
|
thumbnail.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
// Generate webpublic for local users
|
// Generate webpublic for local users, if image is not animated
|
||||||
if (user.Host == null)
|
if (user.Host == null && !isAnimated)
|
||||||
{
|
{
|
||||||
var webpublicImage = image.Clone();
|
using var webpublicImage = image.Clone();
|
||||||
webpublicImage.Metadata.ExifProfile = null;
|
webpublicImage.Metadata.ExifProfile = null;
|
||||||
webpublicImage.Metadata.XmpProfile = null;
|
webpublicImage.Metadata.XmpProfile = null;
|
||||||
if (Math.Max(image.Size.Width, image.Size.Height) > 2048)
|
if (Math.Max(image.Size.Width, image.Size.Height) > 2048)
|
||||||
webpublicImage.Mutate(p => p.Resize(image.Width > image.Height
|
{
|
||||||
? new Size(2048, 0)
|
var webpOpts = new ResizeOptions { Size = new Size(2048, 2048), Mode = ResizeMode.Max };
|
||||||
: new Size(0, 2048)));
|
webpublicImage.Mutate(p => p.Resize(webpOpts));
|
||||||
|
}
|
||||||
|
|
||||||
var encoder = new WebpEncoder
|
var encoder = new WebpEncoder
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Reference in a new issue