Skip to content

Commit 3528431

Browse files
committed
Some improvements to iOS thumbnail loading
1 parent b07c862 commit 3528431

5 files changed

Lines changed: 39 additions & 37 deletions

File tree

src/Platforms/SecureFolderFS.Maui/AppModels/ImageStream.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using SecureFolderFS.Shared.ComponentModel;
2-
using SecureFolderFS.Shared.Models;
32
using SecureFolderFS.Storage.Streams;
43
using IImage = SecureFolderFS.Shared.ComponentModel.IImage;
54

src/Platforms/SecureFolderFS.Maui/Platforms/iOS/ServiceImplementation/IOSMediaService.cs

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using CoreGraphics;
33
using CoreMedia;
44
using Foundation;
5+
using ImageIO;
56
using OwlCore.Storage;
67
using SecureFolderFS.Maui.AppModels;
78
using SecureFolderFS.Maui.ServiceImplementation;
@@ -51,33 +52,28 @@ private static async Task<IImageStream> GenerateImageThumbnailAsync(Stream strea
5152
if (data is null)
5253
throw new Exception("Failed to load image data.");
5354

54-
using var image = UIImage.LoadFromData(data);
55-
if (image?.CGImage is null)
56-
throw new Exception("Failed to load image.");
55+
using var source = CGImageSource.FromData(data);
56+
if (source is null)
57+
throw new Exception("Failed to create image source.");
5758

58-
// Apply EXIF orientation
59-
var orientedImage = image.Orientation == UIImageOrientation.Up
60-
? image
61-
: UIImage.FromImage(image.CGImage, 1.0f, image.Orientation);
62-
63-
// Resize
64-
var scale = Math.Min(maxSize / orientedImage.Size.Width, maxSize / orientedImage.Size.Height);
65-
var newSize = new CGSize(orientedImage.Size.Width * scale, orientedImage.Size.Height * scale);
66-
67-
UIGraphics.BeginImageContextWithOptions(newSize, false, 1.0f);
68-
orientedImage.Draw(new CGRect(CGPoint.Empty, newSize));
69-
using var resizedImage = UIGraphics.GetImageFromCurrentImageContext();
70-
UIGraphics.EndImageContext();
71-
72-
if (resizedImage is null)
73-
throw new Exception("Failed to resize image.");
74-
75-
// Compress to JPEG
76-
using var jpegData = resizedImage.AsJPEG(Constants.Browser.IMAGE_THUMBNAIL_QUALITY);
59+
var options = new CGImageThumbnailOptions
60+
{
61+
MaxPixelSize = (int)maxSize,
62+
ShouldAllowFloat = false,
63+
CreateThumbnailWithTransform = true, // handles EXIF orientation
64+
CreateThumbnailFromImageAlways = true
65+
};
66+
67+
using var cgImage = source.CreateThumbnail(0, options);
68+
if (cgImage is null)
69+
throw new Exception("Failed to create thumbnail.");
70+
71+
using var image = UIImage.FromImage(cgImage);
72+
using var jpegData = image.AsJPEG(Constants.Browser.IMAGE_THUMBNAIL_QUALITY);
7773
if (jpegData is null)
7874
throw new FormatException("Failed to convert image to JPEG.");
7975

80-
var memoryStream = new MemoryStream();
76+
var memoryStream = new MemoryStream((int)jpegData.Length);
8177
await jpegData.AsStream().CopyToAsync(memoryStream).ConfigureAwait(false);
8278
memoryStream.Position = 0L;
8379

@@ -115,10 +111,18 @@ private static async Task<IImageStream> GenerateVideoThumbnailAsync(Stream strea
115111
};
116112

117113
var actualTime = new CMTime((long)captureTime.TotalSeconds, 1);
118-
var imageRef = generator.CopyCGImageAtTime(actualTime, out _, out var error);
119-
if (imageRef is null || error != null)
120-
throw new FormatException($"Failed to generate thumbnail: {error?.LocalizedDescription}");
114+
var tcs = new TaskCompletionSource<CGImage>(TaskCreationOptions.RunContinuationsAsynchronously);
115+
var times = new[] { NSValue.FromCMTime(actualTime) };
116+
117+
generator.GenerateCGImagesAsynchronously(times, (_, image, _, _, error) =>
118+
{
119+
if (error != null || image is null)
120+
tcs.TrySetException(new FormatException($"Failed to generate thumbnail: {error?.LocalizedDescription}"));
121+
else
122+
tcs.TrySetResult(image);
123+
});
121124

125+
using var imageRef = await tcs.Task.ConfigureAwait(false);
122126
using var image = UIImage.FromImage(imageRef);
123127
using var jpegData = image.AsJPEG(Constants.Browser.IMAGE_THUMBNAIL_QUALITY);
124128
if (jpegData is null)

src/Platforms/SecureFolderFS.Maui/UserControls/Browser/BrowserControl.Rendering.xaml.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ private void EnqueueVisibleItemsForThumbnails()
104104
if (!_settingsService.UserSettings.AreThumbnailsEnabled || ItemsSource is null)
105105
return;
106106

107-
var items = ItemsSource.OfType<FileViewModel>().Where(f => f.CanLoadThumbnail()).ToList();
108-
if (items.Count == 0)
107+
var items = ItemsSource.OfType<FileViewModel>().Where(f => f.CanLoadThumbnail()).ToArray();
108+
if (items.Length == 0)
109109
return;
110110

111111
// Cancel any in-flight thumbnail work from the previous folder

src/Platforms/SecureFolderFS.UI/Constants.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,8 @@ public static class Application
4343
public static class Browser
4444
{
4545
public const int THUMBNAIL_MAX_PARALLELISATION = 4;
46-
public const int IMAGE_THUMBNAIL_MAX_SIZE = 300;
47-
public const int IMAGE_THUMBNAIL_QUALITY = 80;
48-
public const int VIDEO_THUMBNAIL_QUALITY = 80;
46+
public const int IMAGE_THUMBNAIL_MAX_SIZE = 296;
47+
public const int IMAGE_THUMBNAIL_QUALITY = 75;
4948
}
5049

5150
public static class FileData

src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Vault/BrowserViewModel.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -274,14 +274,14 @@ protected virtual async Task NewItemAsync(string? itemType, CancellationToken ca
274274
case "File":
275275
{
276276
var file = await modifiableFolder.CreateFileAsync(formattedName, false, cancellationToken);
277-
CurrentFolder.Items.Insert(new FileViewModel(file, this, CurrentFolder), Layouts.GetSorter());
277+
CurrentFolder.Items.Insert(new FileViewModel(file, this, CurrentFolder).WithInitAsync(), Layouts.GetSorter());
278278
break;
279279
}
280280

281281
case "Folder":
282282
{
283283
var folder = await modifiableFolder.CreateFolderAsync(formattedName, false, cancellationToken);
284-
CurrentFolder.Items.Insert(new FolderViewModel(folder, this, CurrentFolder), Layouts.GetSorter());
284+
CurrentFolder.Items.Insert(new FolderViewModel(folder, this, CurrentFolder).WithInitAsync(), Layouts.GetSorter());
285285
break;
286286
}
287287
}
@@ -328,7 +328,7 @@ await TransferViewModel.TransferAsync([ file ], async (item, token) =>
328328
var copiedFile = await modifiableFolder.CreateCopyOfAsync(item, false, availableName, token);
329329

330330
// Add to destination
331-
CurrentFolder.Items.Insert(new FileViewModel(copiedFile, this, CurrentFolder), Layouts.GetSorter());
331+
CurrentFolder.Items.Insert(new FileViewModel(copiedFile, this, CurrentFolder).WithInitAsync(), Layouts.GetSorter());
332332
}, cts.Token);
333333

334334
break;
@@ -351,7 +351,7 @@ await TransferViewModel.TransferAsync([ folder ], async (item, reporter, token)
351351
var copiedFolder = await modifiableFolder.CreateCopyOfAsync(item, false, availableName, reporter, token);
352352

353353
// Add to destination
354-
CurrentFolder.Items.Insert(new FolderViewModel(copiedFolder, this, CurrentFolder), Layouts.GetSorter());
354+
CurrentFolder.Items.Insert(new FolderViewModel(copiedFolder, this, CurrentFolder).WithInitAsync(), Layouts.GetSorter());
355355
}, cts.Token);
356356

357357
break;
@@ -374,7 +374,7 @@ await TransferViewModel.TransferAsync(galleryItems, async (item, token) =>
374374
var copiedFile = await modifiableFolder.CreateCopyOfAsync(item, false, availableName, token);
375375

376376
// Add to destination
377-
CurrentFolder.Items.Insert(new FileViewModel(copiedFile, this, CurrentFolder), Layouts.GetSorter());
377+
CurrentFolder.Items.Insert(new FileViewModel(copiedFile, this, CurrentFolder).WithInitAsync(), Layouts.GetSorter());
378378
}, cts.Token);
379379

380380
break;

0 commit comments

Comments
 (0)