Skip to content

Commit

Permalink
Mac: Fix thumbnail rendering memory leak
Browse files Browse the repository at this point in the history
  • Loading branch information
cyanfish committed Feb 14, 2024
1 parent 74e9e59 commit 5001d42
Show file tree
Hide file tree
Showing 6 changed files with 34 additions and 23 deletions.
3 changes: 2 additions & 1 deletion NAPS2.Images.Mac/MacBitmapHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public static NSBitmapImageRep CopyRep(NSBitmapImageRep original)
var copy = CreateRepForDrawing(w, h);
using var c = CreateContext(copy, false, true);
CGRect rect = new CGRect(0, 0, w, h);
c.DrawImage(rect, original.AsCGImage(ref rect, null, null));
using var cgImage = original.AsCGImage(ref rect, null, null);
c.DrawImage(rect, cgImage);
return copy;
}

Expand Down
2 changes: 2 additions & 0 deletions NAPS2.Images.Mac/MacImageContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ protected override IMemoryImage LoadCore(Stream stream, ImageFileFormat format)
{
if (reps.Length > 1)
{
image.Dispose();
return CreateImage(reps[0]);
}
return new MacImage(this, image);
Expand Down Expand Up @@ -69,6 +70,7 @@ protected override void LoadFramesCore(Action<IMemoryImage> produceImage, Stream
}
finally
{
image.Dispose();
foreach (var rep in reps)
{
rep.Dispose();
Expand Down
6 changes: 4 additions & 2 deletions NAPS2.Images.Mac/MacImageTransformer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ protected override MacImage PerformTransform(MacImage image, RotationTransform t
c.ConcatCTM(CGAffineTransform.Multiply(CGAffineTransform.Multiply(t1, t2), t3));

CGRect rect = new CGRect(0, 0, image.Width, image.Height);
c.DrawImage(rect, image.Rep.AsCGImage(ref rect, null, null));
using var cgImage = image.Rep.AsCGImage(ref rect, null, null);
c.DrawImage(rect, cgImage);
image.Dispose();
return newImage;
}
Expand All @@ -46,7 +47,8 @@ protected override MacImage PerformTransform(MacImage image, ResizeTransform tra
using CGBitmapContext c = MacBitmapHelper.CreateContext(newImage);
CGRect rect = new CGRect(0, 0, transform.Width, transform.Height);
// TODO: This changes the image size to match the original which we probably don't want.
c.DrawImage(rect, image.Rep.AsCGImage(ref rect, null, null));
using var cgImage = image.Rep.AsCGImage(ref rect, null, null);
c.DrawImage(rect, cgImage);
image.Dispose();
return newImage;
}
Expand Down
2 changes: 1 addition & 1 deletion NAPS2.Lib/Images/ThumbnailRenderQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ private void RenderThumbnails()
{
var thumb = worker != null
? RenderThumbnailWithWorker(worker, imageToRender, thumbnailSize)
: _thumbnailRenderer.Render(imageToRender, thumbnailSize);
: _thumbnailRenderer.Render(imageToRender, thumbnailSize).Result;

if (!ThumbnailStillNeedsRendering(next, thumbnailSize))
{
Expand Down
34 changes: 20 additions & 14 deletions NAPS2.Sdk/Images/ThumbnailRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,27 @@ public ThumbnailRenderer(ImageContext imageContext)
_imageContext = imageContext;
}

public IMemoryImage Render(ProcessedImage processedImage, int outputSize)
public Task<IMemoryImage> Render(ProcessedImage processedImage, int outputSize)
{
var image = _imageContext.RenderWithoutTransforms(processedImage);
var transformList = processedImage.TransformState.Transforms;
if (!processedImage.TransformState.IsEmpty)
// On Mac it's really important this runs as a task, as Mac memory management depends on autorelease pools which
// in Xamarin operate at the task level. Otherwise a long-running thumbnail render thread will leak memory like
// a sieve.
return Task.Run(() =>
{
// When we have additional transformations, performing them on a large original image may be quite slow.
// On the other hand, scaling the image to the thumbnail size first can result in transforms losing detail.
// As a middle ground we scale to an "oversampled" size first.
double oversampledSize = outputSize * OVERSAMPLE;
double scaleFactor = Math.Min(oversampledSize / image.Height, oversampledSize / image.Width);
scaleFactor = Math.Min(scaleFactor, 1);
transformList = transformList.Insert(0, new ScaleTransform(scaleFactor));
}
transformList = transformList.Add(new ThumbnailTransform(outputSize));
return _imageContext.PerformAllTransforms(image, transformList);
var image = _imageContext.RenderWithoutTransforms(processedImage);
var transformList = processedImage.TransformState.Transforms;
if (!processedImage.TransformState.IsEmpty)
{
// When we have additional transformations, performing them on a large original image may be quite slow.
// On the other hand, scaling the image to the thumbnail size first can result in transforms losing detail.
// As a middle ground we scale to an "oversampled" size first.
double oversampledSize = outputSize * OVERSAMPLE;
double scaleFactor = Math.Min(oversampledSize / image.Height, oversampledSize / image.Width);
scaleFactor = Math.Min(scaleFactor, 1);
transformList = transformList.Insert(0, new ScaleTransform(scaleFactor));
}
transformList = transformList.Add(new ThumbnailTransform(outputSize));
return _imageContext.PerformAllTransforms(image, transformList);
});
}
}
10 changes: 5 additions & 5 deletions NAPS2.Sdk/Remoting/Worker/WorkerServiceImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ public override async Task<SendMapiEmailResponse> SendMapiEmail(SendMapiEmailReq
}
}

public override Task<RenderThumbnailResponse> RenderThumbnail(RenderThumbnailRequest request,
public override async Task<RenderThumbnailResponse> RenderThumbnail(RenderThumbnailRequest request,
ServerCallContext context)
{
using var callRef = StartCall();
Expand All @@ -221,16 +221,16 @@ public override Task<RenderThumbnailResponse> RenderThumbnail(RenderThumbnailReq
};
using var image =
ImageSerializer.Deserialize(_scanningContext, request.Image, deserializeOptions);
var thumbnail = _thumbnailRenderer.Render(image, request.Size);
var thumbnail = await _thumbnailRenderer.Render(image, request.Size);
var stream = thumbnail.SaveToMemoryStream(ImageFileFormat.Png);
return Task.FromResult(new RenderThumbnailResponse
return new RenderThumbnailResponse
{
Thumbnail = ByteString.FromStream(stream)
});
};
}
catch (Exception e)
{
return Task.FromResult(new RenderThumbnailResponse { Error = RemotingHelper.ToError(e) });
return new RenderThumbnailResponse { Error = RemotingHelper.ToError(e) };
}
}

Expand Down

0 comments on commit 5001d42

Please sign in to comment.