Skip to content

Commit

Permalink
Implement ignore aspect ratio for Single Image Resize view
Browse files Browse the repository at this point in the history
  • Loading branch information
Ruben2776 committed Nov 22, 2024
1 parent ff36865 commit 9403baa
Show file tree
Hide file tree
Showing 6 changed files with 797 additions and 690 deletions.
2 changes: 1 addition & 1 deletion src/PicView.Avalonia/Clipboard/ClipboardHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public static async Task CopyBase64ToClipboard(string path, MainViewModel vm)
base64 = Convert.ToBase64String(stream.ToArray());
break;
case ImageType.Svg:
throw new ArgumentOutOfRangeException();
return;
default:
throw new ArgumentOutOfRangeException();
}
Expand Down
1,281 changes: 653 additions & 628 deletions src/PicView.Avalonia/PicViewTheme/Icons.axaml

Large diffs are not rendered by default.

26 changes: 15 additions & 11 deletions src/PicView.Avalonia/Views/SingleImageResizeView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,19 @@
x:Name="PixelWidthTextBox" />
</StackPanel>

<customControls:IconButton
<Button
x:Name="LinkChainButton"
Foreground="{StaticResource SecondaryTextColor}"
Height="21"
Icon="{StaticResource LinkChainImage}"
IconHeight="21"
IconWidth="21"
IsEnabled="False"
Margin="20,20,20,0"
Width="21" />
Background="Transparent"
Classes="altHover"
Height="35"
Margin="5,20,5,0"
Width="38">
<Panel>
<Image x:Name="LinkChainImage" Source="{StaticResource LinkChainImage}" Width="21" Height="21" />
<Image x:Name="UnlinkChainImage" IsVisible="False" Source="{StaticResource UnlinkChainImage}" Width="21" Height="21" />
</Panel>
</Button>


<StackPanel>
Expand Down Expand Up @@ -89,7 +93,7 @@
IsSnapToTickEnabled="True"
Margin="5,0,0,2"
Maximum="100"
Minimum="10"
Minimum="1"
TickFrequency="1"
Value="90"
Width="190"
Expand All @@ -99,7 +103,7 @@
Margin="8,0,0,3"
Text="{Binding Path=Value, ElementName=QualitySlider}" />
</StackPanel>
<StackPanel Margin="60,0,0,0">
<StackPanel Margin="48,0,0,0">
<TextBlock
Classes="txt"
FontFamily="/Assets/Fonts/Roboto-Medium.ttf#Roboto"
Expand All @@ -115,7 +119,7 @@
FontFamily="/Assets/Fonts/Roboto-Medium.ttf#Roboto"
Height="30"
HorizontalAlignment="Right"
Margin="2,0,0,0"
Margin="0"
Padding="5,7,0,7"
SelectedIndex="0"
Width="195"
Expand Down
63 changes: 49 additions & 14 deletions src/PicView.Avalonia/Views/SingleImageResizeView.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ public partial class SingleImageResizeView : UserControl
private double _aspectRatio;

private IDisposable? _imageUpdateSubscription;

// TODO: allow users to be able to disable aspect ratio locking if they want to stretch the image
private readonly bool _isKeepingAspectRatio = true;

private bool _isKeepingAspectRatio = true;

public SingleImageResizeView()
{
Expand Down Expand Up @@ -51,16 +50,50 @@ public SingleImageResizeView()

_imageUpdateSubscription = vm.WhenAnyValue(x => x.FileInfo).Select(x => x is not null).Subscribe(_ =>
{
Dispatcher.UIThread.Post(SetIsQualitySliderEnabled);
Dispatcher.UIThread.Invoke(SetIsQualitySliderEnabled);
});

ResetButton.Click += (_, _) =>
{
PixelWidthTextBox.Text = vm.PixelWidth.ToString();
PixelHeightTextBox.Text = vm.PixelHeight.ToString();
QualitySlider.Value = 90;
if (vm.FileInfo.Extension.Equals(".jpg", StringComparison.OrdinalIgnoreCase) ||
vm.FileInfo.Extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase) ||
vm.FileInfo.Extension.Equals(".png", StringComparison.OrdinalIgnoreCase))
{
QualitySlider.IsEnabled = true;
var quality = ImageFunctionHelper.GetCompressionQuality(vm.FileInfo.FullName);
QualitySlider.Value = quality;
}
else
{
QualitySlider.IsEnabled = false;
}
ConversionComboBox.SelectedItem = NoConversion;
};

LinkChainButton.Click += (_, _) =>
{
if (_isKeepingAspectRatio)
{
_isKeepingAspectRatio = false;
LinkChainImage.IsVisible = false;
UnlinkChainImage.IsVisible = true;
}
else
{
_isKeepingAspectRatio = true;
LinkChainImage.IsVisible = true;
UnlinkChainImage.IsVisible = false;
AdjustAspectRatio(PixelWidthTextBox);
}
};
};

Unloaded += delegate
{
_imageUpdateSubscription?.Dispose();
};
}

Expand All @@ -77,21 +110,22 @@ private void AdjustAspectRatio(TextBox sender)

private void SetIsQualitySliderEnabled()
{
if (DataContext is not MainViewModel vm)
{
return;
}
if (DataContext is not MainViewModel vm) return;

try
{
if (JpgItem.IsSelected)
if (JpgItem.IsSelected || PngItem.IsSelected)
{
QualitySlider.IsEnabled = true;
QualitySlider.Value = 75;
}
else if (vm.FileInfo.Extension.Equals(".jpg", StringComparison.OrdinalIgnoreCase) ||
vm.FileInfo.Extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase))
vm.FileInfo.Extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase) ||
vm.FileInfo.Extension.Equals(".png", StringComparison.OrdinalIgnoreCase))
{
QualitySlider.IsEnabled = true;
var quality = ImageFunctionHelper.GetCompressionQuality(vm.FileInfo.FullName);
QualitySlider.Value = quality;
}
else
{
Expand Down Expand Up @@ -187,9 +221,9 @@ private async Task SaveImageAs(MainViewModel vm)
var options = new FilePickerSaveOptions
{
Title = $"{TranslationHelper.Translation.OpenFileDialog} - PicView",
SuggestedFileName = Path.GetFileNameWithoutExtension(vm.FileInfo.Name),
SuggestedFileName = vm.FileInfo.Name,
SuggestedStartLocation =
await desktop.MainWindow.StorageProvider.TryGetFolderFromPathAsync(vm.FileInfo.FullName)
await desktop.MainWindow.StorageProvider.TryGetFolderFromPathAsync(vm.FileInfo.FullName),
};
var file = await provider.SaveFilePickerAsync(options);
if (file is null)
Expand Down Expand Up @@ -276,7 +310,8 @@ private async Task DoSaveImage(MainViewModel vm, string destination)
height,
quality,
ext,
rotationAngle).ConfigureAwait(false);
rotationAngle,
_isKeepingAspectRatio).ConfigureAwait(false);
await Dispatcher.UIThread.InvokeAsync(() =>
{
SpinWaiter.IsVisible = false;
Expand Down
7 changes: 7 additions & 0 deletions src/PicView.Core/ImageDecoding/ImageFunctionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,12 @@ public static int GetImageFrames(string file)
return 0;
}
}

public static uint GetCompressionQuality(string file)
{
using var magickImage = new MagickImage();
magickImage.Ping(file);
return magickImage.Quality;
}
}
}
108 changes: 72 additions & 36 deletions src/PicView.Core/ImageDecoding/SaveImageFileHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@ namespace PicView.Core.ImageDecoding;
public static class SaveImageFileHelper
{
/// <summary>
/// Saves an image from a stream or file path asynchronously with optional transformations.
/// Saves an image asynchronously from a stream or file path with optional resizing, rotation, and format conversion.
/// </summary>
/// <param name="stream">The stream containing the image data.</param>
/// <param name="path">The path of the image file to read.</param>
/// <param name="destination">The path of the destination file to save the image.</param>
/// <param name="width">The target width of the image.</param>
/// <param name="height">The target height of the image.</param>
/// <param name="quality">The quality level of the image.</param>
/// <param name="ext">The file extension of the output image.</param>
/// <param name="rotationAngle">The angle to rotate the image, in degrees.</param>
/// <param name="stream">The stream containing the image data. If null, the image will be loaded from the specified file path.</param>
/// <param name="path">The path of the image file to load. If null, the image will be loaded from the stream.</param>
/// <param name="destination">The path to save the processed image. If null, the image will be saved to the original path.</param>
/// <param name="width">The target width of the image. If null, the image will not be resized based on width.</param>
/// <param name="height">The target height of the image. If null, the image will not be resized based on height.</param>
/// <param name="quality">The quality level of the saved image, as a percentage (0-100). If null, the default quality is used.</param>
/// <param name="ext">The file extension of the output image (e.g., ".jpg", ".png"). If null, the original extension is kept.</param>
/// <param name="rotationAngle">The angle to rotate the image, in degrees. If null, no rotation is applied.</param>
/// <param name="respectAspectRatio">Indicates whether to maintain the aspect ratio when resizing.</param>
/// <returns>True if the image is saved successfully; otherwise, false.</returns>
public static async Task<bool> SaveImageAsync(Stream? stream, string? path, string? destination = null, uint? width = null,
uint? height = null, uint? quality = null, string? ext = null, double? rotationAngle = null)
public static async Task<bool> SaveImageAsync(Stream? stream, string? path, string? destination = null,
uint? width = null,
uint? height = null, uint? quality = null, string? ext = null, double? rotationAngle = null,
bool respectAspectRatio = true)
{
try
{
Expand Down Expand Up @@ -48,7 +51,20 @@ public static async Task<bool> SaveImageAsync(Stream? stream, string? path, stri
{
if (height > 0)
{
magickImage.Resize(width > 0 ? width.Value : 0, height.Value);
if (!respectAspectRatio)
{
var geometry = new MagickGeometry(width > 0 ? width.Value : 0, height.Value)
{ IgnoreAspectRatio = true };
magickImage.Resize(geometry);
}
else
{
magickImage.Resize(width > 0 ? width.Value : 0, height.Value);
}
}
else
{
magickImage.Resize(width.Value, 0);
}
}
else
Expand All @@ -62,7 +78,20 @@ public static async Task<bool> SaveImageAsync(Stream? stream, string? path, stri
{
if (width > 0)
{
magickImage.Resize(width.Value, height > 0 ? height.Value : 0);
if (!respectAspectRatio)
{
var geometry = new MagickGeometry(width > 0 ? width.Value : 0, height.Value)
{ IgnoreAspectRatio = true };
magickImage.Resize(geometry);
}
else
{
magickImage.Resize(width.Value, height > 0 ? height.Value : 0);
}
}
else
{
magickImage.Resize(0, height.Value);
}
}
else
Expand All @@ -76,7 +105,7 @@ public static async Task<bool> SaveImageAsync(Stream? stream, string? path, stri
magickImage.Rotate(rotationAngle.Value);
}

var keepExt = string.IsNullOrEmpty(ext);
var keepExt = string.IsNullOrEmpty(ext);
if (!keepExt)
{
magickImage.Format = ext.ToLowerInvariant() switch
Expand All @@ -92,7 +121,7 @@ public static async Task<bool> SaveImageAsync(Stream? stream, string? path, stri
};
}


if (destination is not null)
{
await magickImage.WriteAsync(!keepExt ? Path.ChangeExtension(destination, ext) : destination)
Expand All @@ -103,13 +132,16 @@ await magickImage.WriteAsync(!keepExt ? Path.ChangeExtension(destination, ext) :
await magickImage.WriteAsync(!keepExt ? Path.ChangeExtension(path, ext) : path)
.ConfigureAwait(false);
}
else return false;
else
{
return false;
}
}
catch (Exception exception)
{
#if DEBUG
#if DEBUG
Trace.WriteLine(exception);
#endif
#endif
return false;
}

Expand All @@ -118,16 +150,16 @@ await magickImage.WriteAsync(!keepExt ? Path.ChangeExtension(path, ext) : path)


/// <summary>
/// Resizes an image asynchronously with optional compression and format conversion.
/// Resizes and optionally compresses an image asynchronously, with optional format conversion.
/// </summary>
/// <param name="fileInfo">The FileInfo of the image file to resize.</param>
/// <param name="width">The target width of the image.</param>
/// <param name="height">The target height of the image.</param>
/// <param name="quality">The quality level of the image.</param>
/// <param name="percentage">The percentage value to resize the image.</param>
/// <param name="destination">The path of the destination file to save the resized image.</param>
/// <param name="compress">Indicates whether to compress the image.</param>
/// <param name="ext">The file extension of the output image.</param>
/// <param name="fileInfo">The FileInfo object representing the image file to resize.</param>
/// <param name="width">The target width of the resized image. Ignored if percentage is specified.</param>
/// <param name="height">The target height of the resized image. Ignored if percentage is specified.</param>
/// <param name="quality">The quality level of the resized image, as a percentage (0-100). Defaults to 100.</param>
/// <param name="percentage">An optional percentage to resize the image by. If specified, width and height are ignored.</param>
/// <param name="destination">The path to save the resized image. If null, the original file will be overwritten.</param>
/// <param name="compress">Indicates whether to apply optimal compression to the image after resizing. If null, no compression is applied.</param>
/// <param name="ext">The file extension of the output image (e.g., ".jpg", ".png"). If null, the original extension is kept.</param>
/// <returns>True if the image is resized and saved successfully; otherwise, false.</returns>
public static async Task<bool> ResizeImageAsync(FileInfo fileInfo, uint width, uint height, uint quality = 100,
Percentage? percentage = null, string? destination = null, bool? compress = null, string? ext = null)
Expand All @@ -137,11 +169,6 @@ public static async Task<bool> ResizeImageAsync(FileInfo fileInfo, uint width, u
return false;
}

if (width < 0 && percentage is not null || height < 0 && percentage is not null)
{
return false;
}

var magick = new MagickImage
{
ColorSpace = ColorSpace.Transparent
Expand All @@ -155,9 +182,14 @@ public static async Task<bool> ResizeImageAsync(FileInfo fileInfo, uint width, u
try
{
if (fileInfo.Length < 2147483648)
{
await magick.ReadAsync(fileInfo).ConfigureAwait(false);
// ReSharper disable once MethodHasAsyncOverload
else magick.Read(fileInfo);
}
else
{
// ReSharper disable once MethodHasAsyncOverload
magick.Read(fileInfo);
}
}
catch (MagickException e)
{
Expand Down Expand Up @@ -229,10 +261,14 @@ public static async Task<bool> ResizeImageAsync(FileInfo fileInfo, uint width, u

magick.Dispose();

if (!compress.HasValue) return true;
if (!compress.HasValue)
{
return true;
}

ImageOptimizer imageOptimizer = new()
{
OptimalCompression = compress.Value,
OptimalCompression = compress.Value
};

var x = destination ?? fileInfo.FullName;
Expand Down

0 comments on commit 9403baa

Please sign in to comment.