Skip to content

Commit 212d9a6

Browse files
committed
Support shifting dates of multiple photos.
Also fix a crash when saving empty Author names.
1 parent 0076c78 commit 212d9a6

File tree

10 files changed

+304
-67
lines changed

10 files changed

+304
-67
lines changed

PhotoTagger.Imaging/Exif.cs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Linq;
66
using System.Text;
77
using System.Threading.Tasks;
8-
using System.Windows.Media;
98
using System.Windows.Media.Imaging;
109

1110
namespace PhotoTagger.Imaging {
@@ -74,6 +73,18 @@ private static string fromUniversalNewline(string from) {
7473
XmpDescriptionQuery,
7574
};
7675

76+
// From the System.Author Photo Metadata Policy
77+
readonly static string[] AuthorRemoveQueries = {
78+
"/xmp/dc:creator",
79+
"/xmp/tiff:artist",
80+
"/app13/irb/8bimiptc/iptc/by-line",
81+
"/app1/ifd/{ushort=315}",
82+
"/app1/ifd/{ushort=40093}",
83+
};
84+
85+
private static readonly ReadOnlyCollection<string> EmptyStringCollection =
86+
new ReadOnlyCollection<string>(new string[] { });
87+
7788
#endregion
7889

7990
#region field readers
@@ -144,11 +155,12 @@ private static string readAuthor(BitmapMetadata metadata) {
144155
}
145156

146157
private static GpsLocation readLocation(BitmapMetadata metadata) {
147-
var latSignProp = metadata.GetQuery(LatitudeRefQuery) as string;
148158
var latProp = metadata.GetQuery(LatitudeQuery) as ulong[];
149-
var lonSignProp = metadata.GetQuery(LongitudeRefQuery) as string;
150159
var lonProp = metadata.GetQuery(LongitudeQuery) as ulong[];
151-
if (latSignProp == null || latProp == null || lonSignProp == null || lonProp == null) {
160+
if (!(metadata.GetQuery(LatitudeRefQuery) is string latSignProp) ||
161+
latProp == null ||
162+
!(metadata.GetQuery(LongitudeRefQuery) is string lonSignProp) ||
163+
lonProp == null) {
152164
return null;
153165
}
154166
if (latSignProp.Length != 1 || lonSignProp.Length != 1 ||
@@ -228,7 +240,10 @@ await photo.Dispatcher.InvokeAsync(() => {
228240
Encoding.Default.GetString(bytes)
229241
});
230242
} else {
231-
dest.Author = null;
243+
dest.Author = EmptyStringCollection;
244+
foreach (var query in AuthorRemoveQueries) {
245+
dest.RemoveQuery(query);
246+
}
232247
}
233248
if (source.DateTaken.HasValue) {
234249
var bytes = Encoding.ASCII.GetBytes(

PhotoTagger.Imaging/ImageLoadManager.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Threading;
77
using System.Threading.Tasks;
88
using System.Windows;
9-
using System.Windows.Media;
109
using System.Windows.Media.Imaging;
1110
using System.Windows.Threading;
1211

@@ -113,8 +112,7 @@ private async Task loadMeta(Photo photo,
113112
if (frames.Count < 1) {
114113
throw new ArgumentException("Image contained no frame data.", nameof(photo));
115114
}
116-
var imgMeta = frames[0].Metadata as BitmapMetadata;
117-
if (imgMeta == null) {
115+
if (!(frames[0].Metadata is BitmapMetadata imgMeta)) {
118116
throw new NullReferenceException("Image contained no metadata");
119117
}
120118
metadata = Exif.GetMetadata(imgMeta);

PhotoTagger.Wpf/DateTimeRange.cs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,11 @@ public string ToString(string format, IFormatProvider formatProvider) {
107107
public class DateTimeRangeIsRangeToVisibilityConverter : IValueConverter {
108108
public object Convert(object value, Type targetType,
109109
object parameter, CultureInfo culture) {
110+
var invert = (parameter as bool?) ?? false;
110111
if (value is DateTimeRange dtr) {
111-
return dtr.IsRange ? Visibility.Visible : Visibility.Hidden;
112+
return (dtr.IsRange ^ invert) ? Visibility.Visible : Visibility.Hidden;
112113
} else {
113-
return Visibility.Hidden;
114+
return invert ? Visibility.Visible : Visibility.Hidden;
114115
}
115116
}
116117

@@ -119,4 +120,36 @@ public object ConvertBack(object value, Type targetType,
119120
throw new NotSupportedException();
120121
}
121122
}
123+
124+
[ValueConversion(typeof(DateTimeRange?), typeof(DateTime))]
125+
[ValueConversion(typeof(DateTime?), typeof(DateTimeRange))]
126+
[ValueConversion(typeof(DateTimeRange?), typeof(string))]
127+
public class DateTimeRangeToSingleDateConverter : IValueConverter {
128+
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
129+
if (value is DateTimeRange range) {
130+
if (parameter is bool useMax && useMax ||
131+
parameter is string pstring && pstring == "true") {
132+
if (targetType == typeof(string)) {
133+
return range.Max.ToString("G", culture);
134+
} else {
135+
return range.Max;
136+
}
137+
} else {
138+
if (targetType == typeof(string)) {
139+
return range.Min.ToString("G", culture);
140+
} else {
141+
return range.Min;
142+
}
143+
}
144+
} else if (value is DateTime t) {
145+
return new DateTimeRange(t, t);
146+
} else {
147+
return null;
148+
}
149+
}
150+
151+
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
152+
return Convert(value, targetType, parameter, culture);
153+
}
154+
}
122155
}

PhotoTagger/DateTimeRangeEdit.xaml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<UserControl x:Class="PhotoTagger.DateTimeRangeEdit"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
6+
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
7+
xmlns:ptwpf="clr-namespace:PhotoTagger.Wpf;assembly=PhotoTagger.Wpf"
8+
Name="Editor"
9+
mc:Ignorable="d" >
10+
<UserControl.Resources>
11+
<ptwpf:DateTimeRangeIsRangeToVisibilityConverter
12+
x:Key="DateTimeRangeIsRangeToVisibilityConverter"/>
13+
<ptwpf:DateTimeRangeToSingleDateConverter
14+
x:Key="DateTimeRangeToSingleDateConverter"/>
15+
</UserControl.Resources>
16+
<Grid>
17+
<Grid.ColumnDefinitions>
18+
<ColumnDefinition Width="Auto"/>
19+
<ColumnDefinition Width="Auto"/>
20+
</Grid.ColumnDefinitions>
21+
<xctk:DateTimePicker Grid.Column="0"
22+
Name="minDatePicker" >
23+
<xctk:DateTimePicker.UpdateValueOnEnterKey>
24+
False
25+
</xctk:DateTimePicker.UpdateValueOnEnterKey>
26+
<xctk:DateTimePicker.Format>
27+
Custom
28+
</xctk:DateTimePicker.Format>
29+
<xctk:DateTimePicker.FormatString>
30+
G
31+
</xctk:DateTimePicker.FormatString>
32+
<xctk:DateTimePicker.Value>
33+
<Binding ElementName="Editor"
34+
Path="DateRange"
35+
Converter="{StaticResource
36+
DateTimeRangeToSingleDateConverter}"
37+
Mode="TwoWay" />
38+
</xctk:DateTimePicker.Value>
39+
<xctk:DateTimePicker.HorizontalContentAlignment>
40+
Left
41+
</xctk:DateTimePicker.HorizontalContentAlignment>
42+
<xctk:DateTimePicker.HorizontalAlignment>
43+
Left
44+
</xctk:DateTimePicker.HorizontalAlignment>
45+
</xctk:DateTimePicker>
46+
<Grid Grid.Column="1">
47+
<Grid.Visibility>
48+
<Binding ElementName="Editor"
49+
Path="DateRange"
50+
Converter="{StaticResource
51+
DateTimeRangeIsRangeToVisibilityConverter}"
52+
Mode="OneWay" />
53+
</Grid.Visibility>
54+
<Grid.ColumnDefinitions>
55+
<ColumnDefinition Width="Auto"/>
56+
<ColumnDefinition Width="*"/>
57+
<ColumnDefinition Width="Auto"/>
58+
</Grid.ColumnDefinitions>
59+
<TextBlock Text="to" Grid.Column="0" />
60+
<TextBlock Grid.Column="1"
61+
HorizontalAlignment="Center"
62+
VerticalAlignment="Center" >
63+
<TextBlock.Text>
64+
<Binding ElementName="Editor"
65+
Path="DateRange"
66+
Converter="{StaticResource
67+
DateTimeRangeToSingleDateConverter}"
68+
ConverterParameter="true" />
69+
</TextBlock.Text>
70+
</TextBlock>
71+
<Button Grid.Column="2" Click="setAllEqual">
72+
<Button.Content>
73+
Set All Equal
74+
</Button.Content>
75+
<Button.Margin>
76+
2
77+
</Button.Margin>
78+
</Button>
79+
</Grid>
80+
</Grid>
81+
</UserControl>

PhotoTagger/DateTimeRangeEdit.xaml.cs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using PhotoTagger.Imaging;
2+
using PhotoTagger.Wpf;
3+
using System;
4+
using System.Collections.ObjectModel;
5+
using System.Collections.Specialized;
6+
using System.Linq;
7+
using System.Windows;
8+
using System.Windows.Controls;
9+
10+
namespace PhotoTagger {
11+
/// <summary>
12+
/// Interaction logic for DateTimeRangeEdit.xaml
13+
/// </summary>
14+
public partial class DateTimeRangeEdit : UserControl {
15+
public DateTimeRangeEdit() {
16+
if (PhotoSet is INotifyCollectionChanged oc) {
17+
oc.CollectionChanged += setChanged;
18+
}
19+
InitializeComponent();
20+
}
21+
22+
public ReadOnlyObservableCollection<Photo> PhotoSet {
23+
get {
24+
return (ReadOnlyObservableCollection<Photo>)GetValue(
25+
PhotoSetProperty);
26+
}
27+
set {
28+
SetValue(PhotoSetProperty, value);
29+
}
30+
}
31+
32+
public static readonly DependencyProperty PhotoSetProperty =
33+
DependencyProperty.Register(nameof(PhotoSet),
34+
typeof(ReadOnlyObservableCollection<Photo>),
35+
typeof(DateTimeRangeEdit),
36+
new PropertyMetadata(setChanged));
37+
38+
private static void setChanged(DependencyObject d,
39+
DependencyPropertyChangedEventArgs e) {
40+
var photos = e.NewValue as ReadOnlyObservableCollection<Photo>;
41+
DateTimeRangeEdit self = (d as DateTimeRangeEdit);
42+
self.DateRange = DateTimeRange.FromList(
43+
photos.Select(p => p.DateTaken));
44+
if (e.OldValue is INotifyCollectionChanged oc) {
45+
oc.CollectionChanged -= self.setChanged;
46+
}
47+
if (photos is INotifyCollectionChanged nc) {
48+
nc.CollectionChanged += self.setChanged;
49+
}
50+
}
51+
52+
private void setChanged(object sender, NotifyCollectionChangedEventArgs e) {
53+
DateRange = DateTimeRange.FromList(
54+
PhotoSet.Select(p => p.DateTaken));
55+
}
56+
57+
58+
public DateTimeRange? DateRange {
59+
get {
60+
return (DateTimeRange?)GetValue(DateRangeProperty);
61+
}
62+
set {
63+
SetValue(DateRangeProperty, value);
64+
}
65+
}
66+
67+
public static readonly DependencyProperty DateRangeProperty =
68+
DependencyProperty.Register(nameof(DateRange), typeof(DateTimeRange?),
69+
typeof(DateTimeRangeEdit),
70+
new PropertyMetadata(dateChanged));
71+
72+
private static void dateChanged(DependencyObject d,
73+
DependencyPropertyChangedEventArgs e) {
74+
var newTime = e.NewValue as DateTimeRange?;
75+
if (!newTime.HasValue) {
76+
return;
77+
}
78+
if (d is DateTimeRangeEdit self) {
79+
if (self.PhotoSet.Count == 0) {
80+
return;
81+
}
82+
DateTimeRange? oldRange = DateTimeRange.FromList(
83+
self.PhotoSet.Select(p => p.DateTaken));
84+
if (!oldRange.HasValue ||
85+
!oldRange.Value.IsRange) {
86+
self.setAll(newTime.Value.Min);
87+
} else {
88+
var shiftAmount = newTime.Value.Min - oldRange.Value.Min;
89+
self.shiftDates(shiftAmount);
90+
}
91+
}
92+
}
93+
94+
private async void shiftDates(TimeSpan shiftAmount) {
95+
if (shiftAmount == TimeSpan.Zero) {
96+
return;
97+
}
98+
var part = this.minDatePicker.CurrentDateTimePart;
99+
foreach (Photo p in this.PhotoSet) {
100+
if (p.DateTaken.HasValue) {
101+
p.DateTaken = p.DateTaken.Value + shiftAmount;
102+
}
103+
}
104+
this.DateRange = DateTimeRange.FromList(
105+
this.PhotoSet.Select(p => p.DateTaken));
106+
// restore the CurrentDateTimePart, but only after all of the data
107+
// binding flow-through has had a chance to propagate.
108+
await this.Dispatcher.InvokeAsync(() =>
109+
this.minDatePicker.CurrentDateTimePart = part);
110+
}
111+
112+
private void setAllEqual(object sender, RoutedEventArgs e) {
113+
if (!this.DateRange.HasValue) {
114+
return;
115+
}
116+
var newTime = this.DateRange.Value.Min;
117+
setAll(newTime);
118+
}
119+
120+
private async void setAll(DateTime newTime) {
121+
bool anyChanged = false;
122+
var part = this.minDatePicker.CurrentDateTimePart;
123+
foreach (Photo p in this.PhotoSet) {
124+
if (p.DateTaken.HasValue &&
125+
p.DateTaken.Value != newTime) {
126+
p.DateTaken = newTime;
127+
anyChanged = true;
128+
}
129+
}
130+
if (anyChanged) {
131+
this.DateRange = DateTimeRange.FromList(
132+
this.PhotoSet.Select(p => p.DateTaken));
133+
// restore the CurrentDateTimePart, but only after all of the data
134+
// binding flow-through has had a chance to propagate.
135+
await this.Dispatcher.InvokeAsync(() =>
136+
this.minDatePicker.CurrentDateTimePart = part);
137+
}
138+
}
139+
}
140+
}

0 commit comments

Comments
 (0)