Skip to content

Commit

Permalink
Initial multi-page TIFF support #190
Browse files Browse the repository at this point in the history
  • Loading branch information
Ruben2776 committed Feb 6, 2025
1 parent 170470f commit 690c983
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 56 deletions.
29 changes: 9 additions & 20 deletions src/PicView.Avalonia/Navigation/ImageIterator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -486,13 +486,18 @@ public async Task NextIteration(NavigateTo navigateTo, CancellationTokenSource c
return;
}

await NextIteration(index, cts).ConfigureAwait(false);
}

public async Task NextIteration(int iteration, CancellationTokenSource cts)
{
if (!MainKeyboardShortcuts.IsKeyHeldDown)
{
await IterateToIndex(index, cts).ConfigureAwait(false);
await IterateToIndex(iteration, cts).ConfigureAwait(false);
}
else
{
await TimerIteration(index, cts).ConfigureAwait(false);
await TimerIteration(iteration, cts).ConfigureAwait(false);
}
}

Expand Down Expand Up @@ -521,7 +526,7 @@ public async Task IterateToIndex(int index, CancellationTokenSource cts)
// Wait for image to load
if (preloadValue is { IsLoading: true, ImageModel.Image: not null })
{
TryShowPreview();
UpdateImage.LoadingPreview(_vm, CurrentIndex);

do
{
Expand All @@ -537,7 +542,7 @@ public async Task IterateToIndex(int index, CancellationTokenSource cts)
}
else
{
TryShowPreview();
UpdateImage.LoadingPreview(_vm, CurrentIndex);
preloadValue = await PreLoader.GetAsync(CurrentIndex, ImagePaths).ConfigureAwait(false);
}

Expand Down Expand Up @@ -622,22 +627,6 @@ await PreLoader.PreLoadAsync(CurrentIndex, IsReversed, ImagePaths)
}

return;

void TryShowPreview()
{
SetTitleHelper.SetLoadingTitle(_vm);
_vm.IsLoading = true;
_vm.ImageSource = GetThumbnails.GetExifThumb(ImagePaths[index]);
if (Settings.ImageScaling.ShowImageSideBySide)
{
_vm.SecondaryImageSource = GetThumbnails.GetExifThumb(ImagePaths[NextIndex]);
}
else
{
_vm.SecondaryImageSource = null;
}

}
}

private static Timer? _timer;
Expand Down
70 changes: 42 additions & 28 deletions src/PicView.Avalonia/Navigation/NavigationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ namespace PicView.Avalonia.Navigation;
public static class NavigationHelper
{
private static CancellationTokenSource? _cancellationTokenSource;

public static TiffManager.TiffNavigationInfo? TiffNavigationInfo { get; private set; }

#region Navigation

Expand Down Expand Up @@ -56,11 +58,47 @@ public static async Task Navigate(bool next, MainViewModel vm)
if (GalleryFunctions.IsFullGalleryOpen)
{
await ScrollGallery(next);
return;
}

var navigateTo = next ? NavigateTo.Next : NavigateTo.Previous;
var nextIteration = vm.ImageIterator.GetIteration(vm.ImageIterator.CurrentIndex, navigateTo);
var currentFileName = vm.ImageIterator.ImagePaths[vm.ImageIterator.CurrentIndex];
if (!TiffManager.IsTiff(currentFileName) || vm.ImageIterator.IsReversed)
{
await CheckCancellationAndStartIterateToIndex(nextIteration, vm).ConfigureAwait(false);
return;
}

if (TiffNavigationInfo is null)
{
var tiffPages = await Task.FromResult(TiffManager.LoadTiffPages(currentFileName)).ConfigureAwait(false);
if (tiffPages.Count < 1)
{
await CheckCancellationAndStartIterateToIndex(nextIteration, vm).ConfigureAwait(false);
return;
}
TiffNavigationInfo = new TiffManager.TiffNavigationInfo
{
CurrentPage = 1, // Skip first page since it has already been shown
PageCount = tiffPages.Count,
Pages = tiffPages
};
}
else
{
TiffNavigationInfo.CurrentPage += 1;
}

if (TiffNavigationInfo.CurrentPage >= TiffNavigationInfo.PageCount)
{
TiffNavigationInfo.Dispose();
TiffNavigationInfo = null;
await CheckCancellationAndStartIterateToIndex(nextIteration, vm).ConfigureAwait(false);
}
else
{
var navigateTo = next ? NavigateTo.Next : NavigateTo.Previous;
await CheckCancellationAndStartNextIteration(navigateTo, vm).ConfigureAwait(false);
UpdateImage.SetTiffImage(TiffNavigationInfo, Path.GetFileName(currentFileName), vm);
}
}

Expand Down Expand Up @@ -157,11 +195,6 @@ await vm.ImageIterator.NextIteration(last ? NavigateTo.Last : NavigateTo.First,
/// <returns>A task representing the asynchronous operation.</returns>
public static async Task Iterate(bool next, MainViewModel vm)
{
if (!CanNavigate(vm))
{
return;
}

if (GalleryFunctions.IsFullGalleryOpen)
{
GalleryNavigation.NavigateGallery(next ? Direction.Right : Direction.Left, vm);
Expand Down Expand Up @@ -621,27 +654,6 @@ public static async Task LoadPicFromDirectoryAsync(string file, MainViewModel vm

#region Private helpers

/// <summary>
/// Checks if the previous iteration has been cancelled and starts the next iteration in a new task.
/// </summary>
/// <param name="navigateTo">The direction to navigate.</param>
/// <param name="vm">The main view model instance.</param>
/// <returns>A task representing the asynchronous operation.</returns>
private static async Task CheckCancellationAndStartNextIteration(NavigateTo navigateTo, MainViewModel vm)
{
await Task.Run(() =>
{
if (_cancellationTokenSource is not null)
{
_ = _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
}

_cancellationTokenSource = new CancellationTokenSource();
_ = vm.ImageIterator.NextIteration(navigateTo, _cancellationTokenSource).ConfigureAwait(false);
_cancellationTokenSource.CancelAfter(TimeSpan.FromMinutes(5));
}).ConfigureAwait(false);
}

/// <summary>
/// Checks if the previous iteration has been cancelled and starts the iteration at the given index in a new task.
/// </summary>
Expand Down Expand Up @@ -736,6 +748,8 @@ await Dispatcher.UIThread.InvokeAsync(() =>
WindowResizing.SetSize(imageModel.PixelWidth, imageModel.PixelHeight, 0, 0, imageModel.Rotation, vm);
});
}

vm.IsLoading = false; //Don't show loading indicator

await vm.ImageIterator.DisposeAsync();

Expand Down
53 changes: 45 additions & 8 deletions src/PicView.Avalonia/Navigation/UpdateImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using PicView.Avalonia.ViewModels;
using PicView.Avalonia.WindowBehavior;
using PicView.Core.Gallery;
using PicView.Core.ImageDecoding;
using PicView.Core.Navigation;

namespace PicView.Avalonia.Navigation;
Expand Down Expand Up @@ -189,9 +190,45 @@ public static void SetSingleImage(object source, ImageType imageType, string nam
vm.PixelWidth = width;
vm.PixelHeight = height;
}

public static void SetTiffImage(TiffManager.TiffNavigationInfo tiffNavigationInfo, string name, MainViewModel vm)
{
var source = tiffNavigationInfo.Pages[tiffNavigationInfo.CurrentPage].ToWriteableBitmap();
vm.ImageSource = source;
vm.SecondaryImageSource = null;
vm.ImageType = ImageType.Bitmap;
var width = source?.PixelSize.Width ?? 0;
var height = source?.PixelSize.Height ?? 0;

name = name.Insert(name.LastIndexOf('.'), $" [{tiffNavigationInfo.CurrentPage}/{tiffNavigationInfo.PageCount - 1}]");

Dispatcher.UIThread.Invoke(() =>
{
WindowResizing.SetSize(width, height, 0, 0, 0, vm);
}, DispatcherPriority.Send);

if (vm.RotationAngle != 0)
{
vm.ImageViewer.Rotate(vm.RotationAngle);
}

var singeImageWindowTitles = ImageTitleFormatter.GenerateTitleForSingleImage(width, height, name, 1);
vm.WindowTitle = singeImageWindowTitles.TitleWithAppName;
vm.Title = singeImageWindowTitles.BaseTitle;
vm.TitleTooltip = singeImageWindowTitles.BaseTitle;
vm.GalleryMargin = new Thickness(0, 0, 0, 0);

vm.PlatformService.StopTaskbarProgress();

vm.PixelWidth = width;
vm.PixelHeight = height;
}

public static void LoadingPreview(MainViewModel vm, int index)
{
SetTitleHelper.SetLoadingTitle(vm);
vm.IsLoading = true;

vm.SelectedGalleryItemIndex = index;
if (Settings.Gallery.IsBottomGalleryShown)
{
Expand All @@ -200,17 +237,17 @@ public static void LoadingPreview(MainViewModel vm, int index)

using var image = new MagickImage();
image.Ping(vm.ImageIterator.ImagePaths[index]);
var thumb = image.GetExifProfile()?.CreateThumbnail();
var thumb = image.GetExifProfile()?.CreateThumbnail().ToWriteableBitmap();

var byteArray = thumb?.ToByteArray();
if (byteArray is null)
vm.ImageSource = thumb;
if (Settings.ImageScaling.ShowImageSideBySide)
{
return;
vm.SecondaryImageSource = GetThumbnails.GetExifThumb(vm.ImageIterator.ImagePaths[vm.ImageIterator.NextIndex]);
}
else
{
vm.SecondaryImageSource = null;
}

var stream = new MemoryStream(byteArray);
vm.ImageSource = new Bitmap(stream);
vm.ImageType = ImageType.Bitmap;
}

#endregion
Expand Down
64 changes: 64 additions & 0 deletions src/PicView.Core/ImageDecoding/TiffManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using ImageMagick;

namespace PicView.Core.ImageDecoding;

public static class TiffManager
{
public static bool IsTiff(string path)
{
return path.EndsWith(".tif", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".tiff", StringComparison.OrdinalIgnoreCase);
}

public static MagickImageCollection? LoadTiffPages(string path)
{
using var image = new MagickImage(path);
var settings = new MagickReadSettings
{
// Specify that we want to read a TIFF format image
Format = MagickFormat.Tiff
};

var pageCollection = new MagickImageCollection(path, settings);
foreach (var page in pageCollection)
{
page.Quality = 100;
}

return pageCollection;
}

public class TiffNavigationInfo : IDisposable
{
public int PageCount { get; set; }
public int CurrentPage { get; set; }

public MagickImageCollection? Pages { get; set; }

#region IDisposable

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

private void Dispose(bool disposing)
{
if (!disposing)
{
return;
}

if (Pages == null)
{
return;
}

Pages.Dispose();
Pages = null;
}

#endregion
}
}

0 comments on commit 690c983

Please sign in to comment.