Files
PowerToys/src/modules/imageresizer/ui/Properties/Settings.cs
CleanCodeDeveloper 9ca32aa3ea [Image Resizer] Add option to remove metadata (#14176)
* Implements option to remove metadata (see #1928)

* Add unit test

* renamed settings switch, update ui text

* Fix exception type, add justification for swallowing exception

* Add unit test to check handling if no metadata is there in targetfile

* Reordered the checkboxes as suggested by @htcfreek

* Reduced size of test image
2021-11-03 18:05:35 +00:00

470 lines
16 KiB
C#

// Copyright (c) Brice Lambson
// The Brice Lambson licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.IO.Abstractions;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Windows.Media.Imaging;
using ImageResizer.Models;
namespace ImageResizer.Properties
{
public sealed partial class Settings : IDataErrorInfo, INotifyPropertyChanged
{
private static readonly IFileSystem _fileSystem = new FileSystem();
// Used to synchronize access to the settings.json file
private static Mutex _jsonMutex = new Mutex();
private static string _settingsPath = _fileSystem.Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData), "Microsoft", "PowerToys", "ImageResizer", "settings.json");
private string _fileNameFormat;
private bool _shrinkOnly;
private int _selectedSizeIndex;
private bool _replace;
private bool _ignoreOrientation;
private bool _removeMetadata;
private int _jpegQualityLevel;
private PngInterlaceOption _pngInterlaceOption;
private TiffCompressOption _tiffCompressOption;
private string _fileName;
private bool _keepDateModified;
private System.Guid _fallbackEncoder;
private CustomSize _customSize;
public Settings()
{
SelectedSizeIndex = 0;
ShrinkOnly = false;
Replace = false;
IgnoreOrientation = true;
RemoveMetadata = false;
JpegQualityLevel = 90;
PngInterlaceOption = System.Windows.Media.Imaging.PngInterlaceOption.Default;
TiffCompressOption = System.Windows.Media.Imaging.TiffCompressOption.Default;
FileName = "%1 (%2)";
Sizes = new ObservableCollection<ResizeSize>
{
new ResizeSize("$small$", ResizeFit.Fit, 854, 480, ResizeUnit.Pixel),
new ResizeSize("$medium$", ResizeFit.Fit, 1366, 768, ResizeUnit.Pixel),
new ResizeSize("$large$", ResizeFit.Fit, 1920, 1080, ResizeUnit.Pixel),
new ResizeSize("$phone$", ResizeFit.Fit, 320, 568, ResizeUnit.Pixel),
};
KeepDateModified = false;
FallbackEncoder = new System.Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057");
CustomSize = new CustomSize(ResizeFit.Fit, 1024, 640, ResizeUnit.Pixel);
AllSizes = new AllSizesCollection(this);
}
[JsonIgnore]
public IEnumerable<ResizeSize> AllSizes { get; set; }
// Using OrdinalIgnoreCase since this is internal and used for comparison with symbols
public string FileNameFormat
=> _fileNameFormat
?? (_fileNameFormat = FileName
.Replace("{", "{{", StringComparison.OrdinalIgnoreCase)
.Replace("}", "}}", StringComparison.OrdinalIgnoreCase)
.Replace("%1", "{0}", StringComparison.OrdinalIgnoreCase)
.Replace("%2", "{1}", StringComparison.OrdinalIgnoreCase)
.Replace("%3", "{2}", StringComparison.OrdinalIgnoreCase)
.Replace("%4", "{3}", StringComparison.OrdinalIgnoreCase)
.Replace("%5", "{4}", StringComparison.OrdinalIgnoreCase)
.Replace("%6", "{5}", StringComparison.OrdinalIgnoreCase));
[JsonIgnore]
public ResizeSize SelectedSize
{
get => SelectedSizeIndex >= 0 && SelectedSizeIndex < Sizes.Count
? Sizes[SelectedSizeIndex]
: CustomSize;
set
{
var index = Sizes.IndexOf(value);
if (index == -1)
{
index = Sizes.Count;
}
SelectedSizeIndex = index;
}
}
string IDataErrorInfo.Error
{
get
{
return string.Empty;
}
}
string IDataErrorInfo.this[string columnName]
{
get
{
if (columnName != nameof(JpegQualityLevel))
{
return string.Empty;
}
if (JpegQualityLevel < 1 || JpegQualityLevel > 100)
{
// Using CurrentCulture since this is user facing
return string.Format(CultureInfo.CurrentCulture, Resources.ValueMustBeBetween, 1, 100);
}
return string.Empty;
}
}
private class AllSizesCollection : IEnumerable<ResizeSize>, INotifyCollectionChanged, INotifyPropertyChanged
{
private ObservableCollection<ResizeSize> _sizes;
private CustomSize _customSize;
public AllSizesCollection(Settings settings)
{
_sizes = settings.Sizes;
_customSize = settings.CustomSize;
_sizes.CollectionChanged += HandleCollectionChanged;
((INotifyPropertyChanged)_sizes).PropertyChanged += HandlePropertyChanged;
settings.PropertyChanged += (sender, e) =>
{
if (e.PropertyName == nameof(Models.CustomSize))
{
var oldCustomSize = _customSize;
_customSize = settings.CustomSize;
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Replace,
_customSize,
oldCustomSize,
_sizes.Count));
}
else if (e.PropertyName == nameof(Sizes))
{
var oldSizes = _sizes;
oldSizes.CollectionChanged -= HandleCollectionChanged;
((INotifyPropertyChanged)oldSizes).PropertyChanged -= HandlePropertyChanged;
_sizes = settings.Sizes;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
_sizes.CollectionChanged += HandleCollectionChanged;
((INotifyPropertyChanged)_sizes).PropertyChanged += HandlePropertyChanged;
}
};
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event PropertyChangedEventHandler PropertyChanged;
public int Count
=> _sizes.Count + 1;
public ResizeSize this[int index]
=> index == _sizes.Count
? _customSize
: _sizes[index];
public IEnumerator<ResizeSize> GetEnumerator()
=> new AllSizesEnumerator(this);
private void HandleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
=> OnCollectionChanged(e);
private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
=> PropertyChanged?.Invoke(this, e);
private void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
=> CollectionChanged?.Invoke(this, e);
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
private class AllSizesEnumerator : IEnumerator<ResizeSize>
{
private readonly AllSizesCollection _list;
private int _index = -1;
public AllSizesEnumerator(AllSizesCollection list)
=> _list = list;
public ResizeSize Current
=> _list[_index];
object IEnumerator.Current
=> Current;
public void Dispose()
{
}
public bool MoveNext()
=> ++_index < _list.Count;
public void Reset()
=> _index = -1;
}
}
private static Settings defaultInstance = new Settings();
[JsonIgnore]
public static Settings Default
{
get
{
defaultInstance.Reload();
return defaultInstance;
}
}
[JsonConverter(typeof(WrappedJsonValueConverter))]
[JsonPropertyName("imageresizer_selectedSizeIndex")]
public int SelectedSizeIndex
{
get => _selectedSizeIndex;
set
{
_selectedSizeIndex = value;
NotifyPropertyChanged();
}
}
[JsonConverter(typeof(WrappedJsonValueConverter))]
[JsonPropertyName("imageresizer_shrinkOnly")]
public bool ShrinkOnly
{
get => _shrinkOnly;
set
{
_shrinkOnly = value;
NotifyPropertyChanged();
}
}
[JsonConverter(typeof(WrappedJsonValueConverter))]
[JsonPropertyName("imageresizer_replace")]
public bool Replace
{
get => _replace;
set
{
_replace = value;
NotifyPropertyChanged();
}
}
[JsonConverter(typeof(WrappedJsonValueConverter))]
[JsonPropertyName("imageresizer_ignoreOrientation")]
public bool IgnoreOrientation
{
get => _ignoreOrientation;
set
{
_ignoreOrientation = value;
NotifyPropertyChanged();
}
}
/// <summary>
/// Gets or sets a value indicating whether resizing images removes any metadata that doesn't affect rendering.
/// Default is false.
/// </summary>
/// <remarks>
/// Preserved Metadata:
/// System.Photo.Orientation,
/// System.Image.ColorSpace
/// </remarks>
[JsonConverter(typeof(WrappedJsonValueConverter))]
[JsonPropertyName("imageresizer_removeMetadata")]
public bool RemoveMetadata
{
get => _removeMetadata;
set
{
_removeMetadata = value;
NotifyPropertyChanged();
}
}
[JsonConverter(typeof(WrappedJsonValueConverter))]
[JsonPropertyName("imageresizer_jpegQualityLevel")]
public int JpegQualityLevel
{
get => _jpegQualityLevel;
set
{
_jpegQualityLevel = value;
NotifyPropertyChanged();
}
}
[JsonConverter(typeof(WrappedJsonValueConverter))]
[JsonPropertyName("imageresizer_pngInterlaceOption")]
public PngInterlaceOption PngInterlaceOption
{
get => _pngInterlaceOption;
set
{
_pngInterlaceOption = value;
NotifyPropertyChanged();
}
}
[JsonConverter(typeof(WrappedJsonValueConverter))]
[JsonPropertyName("imageresizer_tiffCompressOption")]
public TiffCompressOption TiffCompressOption
{
get => _tiffCompressOption;
set
{
_tiffCompressOption = value;
NotifyPropertyChanged();
}
}
[JsonConverter(typeof(WrappedJsonValueConverter))]
[JsonPropertyName("imageresizer_fileName")]
public string FileName
{
get => _fileName;
set
{
if (string.IsNullOrWhiteSpace(value))
{
throw new System.ArgumentNullException(nameof(FileName));
}
_fileName = value;
NotifyPropertyChanged();
}
}
[JsonInclude]
[JsonConverter(typeof(WrappedJsonValueConverter))]
[JsonPropertyName("imageresizer_sizes")]
public ObservableCollection<ResizeSize> Sizes { get; private set; }
[JsonConverter(typeof(WrappedJsonValueConverter))]
[JsonPropertyName("imageresizer_keepDateModified")]
public bool KeepDateModified
{
get => _keepDateModified;
set
{
_keepDateModified = value;
NotifyPropertyChanged();
}
}
[JsonConverter(typeof(WrappedJsonValueConverter))]
[JsonPropertyName("imageresizer_fallbackEncoder")]
public Guid FallbackEncoder
{
get => _fallbackEncoder;
set
{
_fallbackEncoder = value;
NotifyPropertyChanged();
}
}
[JsonConverter(typeof(WrappedJsonValueConverter))]
[JsonPropertyName("imageresizer_customSize")]
public CustomSize CustomSize
{
get => _customSize;
set
{
_customSize = value;
NotifyPropertyChanged();
}
}
public static string SettingsPath { get => _settingsPath; set => _settingsPath = value; }
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void Save()
{
_jsonMutex.WaitOne();
string jsonData = JsonSerializer.Serialize(new SettingsWrapper() { Properties = this });
// Create directory if it doesn't exist
IFileInfo file = _fileSystem.FileInfo.FromFileName(SettingsPath);
file.Directory.Create();
// write string to file
_fileSystem.File.WriteAllText(SettingsPath, jsonData);
_jsonMutex.ReleaseMutex();
}
public void Reload()
{
_jsonMutex.WaitOne();
if (!_fileSystem.File.Exists(SettingsPath))
{
_jsonMutex.ReleaseMutex();
Save();
return;
}
string jsonData = _fileSystem.File.ReadAllText(SettingsPath);
var jsonSettings = new Settings();
try
{
jsonSettings = JsonSerializer.Deserialize<SettingsWrapper>(jsonData)?.Properties;
}
catch (JsonException)
{
}
// Needs to be called on the App UI thread as the properties are bound to the UI.
App.Current.Dispatcher.Invoke(() =>
{
ShrinkOnly = jsonSettings.ShrinkOnly;
Replace = jsonSettings.Replace;
IgnoreOrientation = jsonSettings.IgnoreOrientation;
RemoveMetadata = jsonSettings.RemoveMetadata;
JpegQualityLevel = jsonSettings.JpegQualityLevel;
PngInterlaceOption = jsonSettings.PngInterlaceOption;
TiffCompressOption = jsonSettings.TiffCompressOption;
FileName = jsonSettings.FileName;
KeepDateModified = jsonSettings.KeepDateModified;
FallbackEncoder = jsonSettings.FallbackEncoder;
CustomSize = jsonSettings.CustomSize;
SelectedSizeIndex = jsonSettings.SelectedSizeIndex;
if (jsonSettings.Sizes.Count > 0)
{
Sizes.Clear();
Sizes.AddRange(jsonSettings.Sizes);
}
});
_jsonMutex.ReleaseMutex();
}
}
}