Skip to content

Commit

Permalink
Support shifting dates of multiple photos.
Browse files Browse the repository at this point in the history
Also fix a crash when saving empty Author names.
  • Loading branch information
adam-azarchs committed Jun 24, 2018
1 parent 0076c78 commit 212d9a6
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 67 deletions.
25 changes: 20 additions & 5 deletions PhotoTagger.Imaging/Exif.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace PhotoTagger.Imaging {
Expand Down Expand Up @@ -74,6 +73,18 @@ private static string fromUniversalNewline(string from) {
XmpDescriptionQuery,
};

// From the System.Author Photo Metadata Policy
readonly static string[] AuthorRemoveQueries = {
"/xmp/dc:creator",
"/xmp/tiff:artist",
"/app13/irb/8bimiptc/iptc/by-line",
"/app1/ifd/{ushort=315}",
"/app1/ifd/{ushort=40093}",
};

private static readonly ReadOnlyCollection<string> EmptyStringCollection =
new ReadOnlyCollection<string>(new string[] { });

#endregion

#region field readers
Expand Down Expand Up @@ -144,11 +155,12 @@ private static string readAuthor(BitmapMetadata metadata) {
}

private static GpsLocation readLocation(BitmapMetadata metadata) {
var latSignProp = metadata.GetQuery(LatitudeRefQuery) as string;
var latProp = metadata.GetQuery(LatitudeQuery) as ulong[];
var lonSignProp = metadata.GetQuery(LongitudeRefQuery) as string;
var lonProp = metadata.GetQuery(LongitudeQuery) as ulong[];
if (latSignProp == null || latProp == null || lonSignProp == null || lonProp == null) {
if (!(metadata.GetQuery(LatitudeRefQuery) is string latSignProp) ||
latProp == null ||
!(metadata.GetQuery(LongitudeRefQuery) is string lonSignProp) ||
lonProp == null) {
return null;
}
if (latSignProp.Length != 1 || lonSignProp.Length != 1 ||
Expand Down Expand Up @@ -228,7 +240,10 @@ await photo.Dispatcher.InvokeAsync(() => {
Encoding.Default.GetString(bytes)
});
} else {
dest.Author = null;
dest.Author = EmptyStringCollection;
foreach (var query in AuthorRemoveQueries) {
dest.RemoveQuery(query);
}
}
if (source.DateTaken.HasValue) {
var bytes = Encoding.ASCII.GetBytes(
Expand Down
4 changes: 1 addition & 3 deletions PhotoTagger.Imaging/ImageLoadManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;

Expand Down Expand Up @@ -113,8 +112,7 @@ private async Task loadMeta(Photo photo,
if (frames.Count < 1) {
throw new ArgumentException("Image contained no frame data.", nameof(photo));
}
var imgMeta = frames[0].Metadata as BitmapMetadata;
if (imgMeta == null) {
if (!(frames[0].Metadata is BitmapMetadata imgMeta)) {
throw new NullReferenceException("Image contained no metadata");
}
metadata = Exif.GetMetadata(imgMeta);
Expand Down
37 changes: 35 additions & 2 deletions PhotoTagger.Wpf/DateTimeRange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,11 @@ public string ToString(string format, IFormatProvider formatProvider) {
public class DateTimeRangeIsRangeToVisibilityConverter : IValueConverter {
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture) {
var invert = (parameter as bool?) ?? false;
if (value is DateTimeRange dtr) {
return dtr.IsRange ? Visibility.Visible : Visibility.Hidden;
return (dtr.IsRange ^ invert) ? Visibility.Visible : Visibility.Hidden;
} else {
return Visibility.Hidden;
return invert ? Visibility.Visible : Visibility.Hidden;
}
}

Expand All @@ -119,4 +120,36 @@ public object ConvertBack(object value, Type targetType,
throw new NotSupportedException();
}
}

[ValueConversion(typeof(DateTimeRange?), typeof(DateTime))]
[ValueConversion(typeof(DateTime?), typeof(DateTimeRange))]
[ValueConversion(typeof(DateTimeRange?), typeof(string))]
public class DateTimeRangeToSingleDateConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value is DateTimeRange range) {
if (parameter is bool useMax && useMax ||
parameter is string pstring && pstring == "true") {
if (targetType == typeof(string)) {
return range.Max.ToString("G", culture);
} else {
return range.Max;
}
} else {
if (targetType == typeof(string)) {
return range.Min.ToString("G", culture);
} else {
return range.Min;
}
}
} else if (value is DateTime t) {
return new DateTimeRange(t, t);
} else {
return null;
}
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return Convert(value, targetType, parameter, culture);
}
}
}
81 changes: 81 additions & 0 deletions PhotoTagger/DateTimeRangeEdit.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<UserControl x:Class="PhotoTagger.DateTimeRangeEdit"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
xmlns:ptwpf="clr-namespace:PhotoTagger.Wpf;assembly=PhotoTagger.Wpf"
Name="Editor"
mc:Ignorable="d" >
<UserControl.Resources>
<ptwpf:DateTimeRangeIsRangeToVisibilityConverter
x:Key="DateTimeRangeIsRangeToVisibilityConverter"/>
<ptwpf:DateTimeRangeToSingleDateConverter
x:Key="DateTimeRangeToSingleDateConverter"/>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<xctk:DateTimePicker Grid.Column="0"
Name="minDatePicker" >
<xctk:DateTimePicker.UpdateValueOnEnterKey>
False
</xctk:DateTimePicker.UpdateValueOnEnterKey>
<xctk:DateTimePicker.Format>
Custom
</xctk:DateTimePicker.Format>
<xctk:DateTimePicker.FormatString>
G
</xctk:DateTimePicker.FormatString>
<xctk:DateTimePicker.Value>
<Binding ElementName="Editor"
Path="DateRange"
Converter="{StaticResource
DateTimeRangeToSingleDateConverter}"
Mode="TwoWay" />
</xctk:DateTimePicker.Value>
<xctk:DateTimePicker.HorizontalContentAlignment>
Left
</xctk:DateTimePicker.HorizontalContentAlignment>
<xctk:DateTimePicker.HorizontalAlignment>
Left
</xctk:DateTimePicker.HorizontalAlignment>
</xctk:DateTimePicker>
<Grid Grid.Column="1">
<Grid.Visibility>
<Binding ElementName="Editor"
Path="DateRange"
Converter="{StaticResource
DateTimeRangeIsRangeToVisibilityConverter}"
Mode="OneWay" />
</Grid.Visibility>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="to" Grid.Column="0" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center" >
<TextBlock.Text>
<Binding ElementName="Editor"
Path="DateRange"
Converter="{StaticResource
DateTimeRangeToSingleDateConverter}"
ConverterParameter="true" />
</TextBlock.Text>
</TextBlock>
<Button Grid.Column="2" Click="setAllEqual">
<Button.Content>
Set All Equal
</Button.Content>
<Button.Margin>
2
</Button.Margin>
</Button>
</Grid>
</Grid>
</UserControl>
140 changes: 140 additions & 0 deletions PhotoTagger/DateTimeRangeEdit.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using PhotoTagger.Imaging;
using PhotoTagger.Wpf;
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Windows;
using System.Windows.Controls;

namespace PhotoTagger {
/// <summary>
/// Interaction logic for DateTimeRangeEdit.xaml
/// </summary>
public partial class DateTimeRangeEdit : UserControl {
public DateTimeRangeEdit() {
if (PhotoSet is INotifyCollectionChanged oc) {
oc.CollectionChanged += setChanged;
}
InitializeComponent();
}

public ReadOnlyObservableCollection<Photo> PhotoSet {
get {
return (ReadOnlyObservableCollection<Photo>)GetValue(
PhotoSetProperty);
}
set {
SetValue(PhotoSetProperty, value);
}
}

public static readonly DependencyProperty PhotoSetProperty =
DependencyProperty.Register(nameof(PhotoSet),
typeof(ReadOnlyObservableCollection<Photo>),
typeof(DateTimeRangeEdit),
new PropertyMetadata(setChanged));

private static void setChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
var photos = e.NewValue as ReadOnlyObservableCollection<Photo>;
DateTimeRangeEdit self = (d as DateTimeRangeEdit);
self.DateRange = DateTimeRange.FromList(
photos.Select(p => p.DateTaken));
if (e.OldValue is INotifyCollectionChanged oc) {
oc.CollectionChanged -= self.setChanged;
}
if (photos is INotifyCollectionChanged nc) {
nc.CollectionChanged += self.setChanged;
}
}

private void setChanged(object sender, NotifyCollectionChangedEventArgs e) {
DateRange = DateTimeRange.FromList(
PhotoSet.Select(p => p.DateTaken));
}


public DateTimeRange? DateRange {
get {
return (DateTimeRange?)GetValue(DateRangeProperty);
}
set {
SetValue(DateRangeProperty, value);
}
}

public static readonly DependencyProperty DateRangeProperty =
DependencyProperty.Register(nameof(DateRange), typeof(DateTimeRange?),
typeof(DateTimeRangeEdit),
new PropertyMetadata(dateChanged));

private static void dateChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
var newTime = e.NewValue as DateTimeRange?;
if (!newTime.HasValue) {
return;
}
if (d is DateTimeRangeEdit self) {
if (self.PhotoSet.Count == 0) {
return;
}
DateTimeRange? oldRange = DateTimeRange.FromList(
self.PhotoSet.Select(p => p.DateTaken));
if (!oldRange.HasValue ||
!oldRange.Value.IsRange) {
self.setAll(newTime.Value.Min);
} else {
var shiftAmount = newTime.Value.Min - oldRange.Value.Min;
self.shiftDates(shiftAmount);
}
}
}

private async void shiftDates(TimeSpan shiftAmount) {
if (shiftAmount == TimeSpan.Zero) {
return;
}
var part = this.minDatePicker.CurrentDateTimePart;
foreach (Photo p in this.PhotoSet) {
if (p.DateTaken.HasValue) {
p.DateTaken = p.DateTaken.Value + shiftAmount;
}
}
this.DateRange = DateTimeRange.FromList(
this.PhotoSet.Select(p => p.DateTaken));
// restore the CurrentDateTimePart, but only after all of the data
// binding flow-through has had a chance to propagate.
await this.Dispatcher.InvokeAsync(() =>
this.minDatePicker.CurrentDateTimePart = part);
}

private void setAllEqual(object sender, RoutedEventArgs e) {
if (!this.DateRange.HasValue) {
return;
}
var newTime = this.DateRange.Value.Min;
setAll(newTime);
}

private async void setAll(DateTime newTime) {
bool anyChanged = false;
var part = this.minDatePicker.CurrentDateTimePart;
foreach (Photo p in this.PhotoSet) {
if (p.DateTaken.HasValue &&
p.DateTaken.Value != newTime) {
p.DateTaken = newTime;
anyChanged = true;
}
}
if (anyChanged) {
this.DateRange = DateTimeRange.FromList(
this.PhotoSet.Select(p => p.DateTaken));
// restore the CurrentDateTimePart, but only after all of the data
// binding flow-through has had a chance to propagate.
await this.Dispatcher.InvokeAsync(() =>
this.minDatePicker.CurrentDateTimePart = part);
}
}
}
}
Loading

0 comments on commit 212d9a6

Please sign in to comment.