Skip to content

Commit 2ab402b

Browse files
committed
Add PreserveMetaData
1 parent 662d87e commit 2ab402b

17 files changed

+424
-11
lines changed

src/Essentials/samples/Samples/View/MediaPickerPage.xaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,17 @@
100100
IsToggled="{Binding PickerRotateImage, Mode=TwoWay}"
101101
OnColor="{AppThemeBinding Light=#007BFF, Dark=#0D6EFD}" />
102102
</Grid>
103+
104+
<!-- Preserve Metadata -->
105+
<Grid ColumnDefinitions="2*,Auto">
106+
<Label Grid.Column="0"
107+
Text="Preserve Metadata"
108+
VerticalOptions="Center"
109+
TextColor="{AppThemeBinding Light=#6C757D, Dark=#CED4DA}" />
110+
<Switch Grid.Column="1"
111+
IsToggled="{Binding PickerPreserveMetaData, Mode=TwoWay}"
112+
OnColor="{AppThemeBinding Light=#007BFF, Dark=#0D6EFD}" />
113+
</Grid>
103114
</VerticalStackLayout>
104115
</Border>
105116

src/Essentials/samples/Samples/ViewModel/MediaPickerViewModel.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class MediaPickerViewModel : BaseViewModel
2929
int pickerMaximumWidth = 0;
3030
int pickerMaximumHeight = 0;
3131
bool pickerRotateImage = false;
32+
bool pickerPreserveMetaData = true;
3233
long imageByteLength = 0;
3334
string imageDimensions = "";
3435
private ObservableCollection<PhotoInfo> photoList = [];
@@ -81,6 +82,12 @@ public bool PickerRotateImage
8182
set => SetProperty(ref pickerRotateImage, value);
8283
}
8384

85+
public bool PickerPreserveMetaData
86+
{
87+
get => pickerPreserveMetaData;
88+
set => SetProperty(ref pickerPreserveMetaData, value);
89+
}
90+
8491
public long ImageByteLength
8592
{
8693
get => imageByteLength;
@@ -128,7 +135,8 @@ async void DoPickPhoto()
128135
CompressionQuality = PickerCompressionQuality,
129136
MaximumWidth = PickerMaximumWidth > 0 ? PickerMaximumWidth : null,
130137
MaximumHeight = PickerMaximumHeight > 0 ? PickerMaximumHeight : null,
131-
RotateImage = PickerRotateImage
138+
RotateImage = PickerRotateImage,
139+
PreserveMetaData = PickerPreserveMetaData
132140
});
133141

134142
await LoadPhotoAsync(photo);
@@ -151,7 +159,8 @@ async void DoCapturePhoto()
151159
CompressionQuality = PickerCompressionQuality,
152160
MaximumWidth = PickerMaximumWidth > 0 ? PickerMaximumWidth : null,
153161
MaximumHeight = PickerMaximumHeight > 0 ? PickerMaximumHeight : null,
154-
RotateImage = PickerRotateImage
162+
RotateImage = PickerRotateImage,
163+
PreserveMetaData = PickerPreserveMetaData
155164
});
156165

157166
await LoadPhotoAsync(photo);
@@ -172,7 +181,8 @@ async void DoPickVideo()
172181
{
173182
Title = "Pick a video",
174183
SelectionLimit = PickerSelectionLimit,
175-
RotateImage = PickerRotateImage
184+
RotateImage = PickerRotateImage,
185+
PreserveMetaData = PickerPreserveMetaData
176186
});
177187

178188
ShowPhoto = false;

src/Essentials/src/MediaPicker/ImageProcessor.android.cs

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#nullable enable
22
using System;
3+
using System.Collections.Generic;
34
using System.IO;
45
using System.Threading.Tasks;
56
using Android.Graphics;
@@ -150,4 +151,169 @@ private static int GetExifOrientation(byte[] imageBytes)
150151
return bitmap;
151152
}
152153
}
154+
155+
public static partial async Task<byte[]?> ExtractMetadataAsync(Stream inputStream, string? originalFileName)
156+
{
157+
if (inputStream == null)
158+
return null;
159+
160+
try
161+
{
162+
// Reset stream position
163+
if (inputStream.CanSeek)
164+
inputStream.Position = 0;
165+
166+
// Read stream into byte array
167+
byte[] bytes;
168+
using (var memoryStream = new MemoryStream())
169+
{
170+
await inputStream.CopyToAsync(memoryStream);
171+
bytes = memoryStream.ToArray();
172+
}
173+
174+
// Create temporary file to extract EXIF data
175+
var tempFileName = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.jpg");
176+
using (var fileStream = File.Create(tempFileName))
177+
{
178+
fileStream.Write(bytes, 0, bytes.Length);
179+
}
180+
181+
// Extract all EXIF attributes
182+
var exif = new ExifInterface(tempFileName);
183+
var metadataList = new List<string>();
184+
185+
// Extract common EXIF tags
186+
var tags = new string[]
187+
{
188+
ExifInterface.TagArtist,
189+
ExifInterface.TagCopyright,
190+
ExifInterface.TagDatetime,
191+
ExifInterface.TagImageDescription,
192+
ExifInterface.TagMake,
193+
ExifInterface.TagModel,
194+
ExifInterface.TagOrientation,
195+
ExifInterface.TagSoftware,
196+
ExifInterface.TagGpsLatitude,
197+
ExifInterface.TagGpsLongitude,
198+
ExifInterface.TagGpsAltitude,
199+
ExifInterface.TagExposureTime,
200+
ExifInterface.TagFNumber,
201+
ExifInterface.TagIso,
202+
ExifInterface.TagWhiteBalance,
203+
ExifInterface.TagFlash,
204+
ExifInterface.TagFocalLength
205+
};
206+
207+
foreach (var tag in tags)
208+
{
209+
var value = exif.GetAttribute(tag);
210+
if (!string.IsNullOrEmpty(value))
211+
{
212+
metadataList.Add($"{tag}={value}");
213+
}
214+
}
215+
216+
// Serialize metadata to simple string format
217+
var metadataString = string.Join("\n", metadataList);
218+
var metadataBytes = System.Text.Encoding.UTF8.GetBytes(metadataString);
219+
220+
// Clean up temp file
221+
try
222+
{
223+
File.Delete(tempFileName);
224+
}
225+
catch
226+
{
227+
// Ignore cleanup failures
228+
}
229+
230+
return metadataBytes;
231+
}
232+
catch
233+
{
234+
return null;
235+
}
236+
}
237+
238+
public static partial async Task<Stream> ApplyMetadataAsync(Stream processedStream, byte[] metadata, string? originalFileName)
239+
{
240+
if (processedStream == null || metadata == null || metadata.Length == 0)
241+
return processedStream ?? new MemoryStream();
242+
243+
try
244+
{
245+
// Reset stream position
246+
if (processedStream.CanSeek)
247+
processedStream.Position = 0;
248+
249+
// Read processed stream into byte array
250+
byte[] bytes;
251+
using (var memoryStream = new MemoryStream())
252+
{
253+
await processedStream.CopyToAsync(memoryStream);
254+
bytes = memoryStream.ToArray();
255+
}
256+
257+
// Deserialize metadata
258+
var metadataString = System.Text.Encoding.UTF8.GetString(metadata);
259+
var metadataLines = metadataString.Split('\n', StringSplitOptions.RemoveEmptyEntries);
260+
261+
var metadataDict = new Dictionary<string, string>();
262+
foreach (var line in metadataLines)
263+
{
264+
var parts = line.Split('=', 2);
265+
if (parts.Length == 2)
266+
{
267+
metadataDict[parts[0]] = parts[1];
268+
}
269+
}
270+
271+
if (metadataDict.Count == 0)
272+
return new MemoryStream(bytes);
273+
274+
// Create temporary file to apply EXIF data
275+
var tempFileName = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.jpg");
276+
using (var fileStream = File.Create(tempFileName))
277+
{
278+
fileStream.Write(bytes, 0, bytes.Length);
279+
}
280+
281+
// Apply EXIF data
282+
var exif = new ExifInterface(tempFileName);
283+
foreach (var kvp in metadataDict)
284+
{
285+
try
286+
{
287+
exif.SetAttribute(kvp.Key, kvp.Value);
288+
}
289+
catch
290+
{
291+
// Skip attributes that can't be set
292+
}
293+
}
294+
exif.SaveAttributes();
295+
296+
// Read back the file with applied metadata
297+
var resultBytes = File.ReadAllBytes(tempFileName);
298+
299+
// Clean up temp file
300+
try
301+
{
302+
File.Delete(tempFileName);
303+
}
304+
catch
305+
{
306+
// Ignore cleanup failures
307+
}
308+
309+
return new MemoryStream(resultBytes);
310+
}
311+
catch
312+
{
313+
// If metadata application fails, return original processed stream
314+
if (processedStream.CanSeek)
315+
processedStream.Position = 0;
316+
return processedStream;
317+
}
318+
}
153319
}

src/Essentials/src/MediaPicker/ImageProcessor.ios.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#nullable enable
22
using System.IO;
33
using System.Threading.Tasks;
4+
using CoreGraphics;
45
using Foundation;
6+
using ImageIO;
57
using UIKit;
68

79
namespace Microsoft.Maui.Essentials;
@@ -68,4 +70,98 @@ public static partial async Task<Stream> RotateImageAsync(Stream inputStream, st
6870

6971
return inputStream;
7072
}
73+
74+
public static partial Task<byte[]?> ExtractMetadataAsync(Stream inputStream, string? originalFileName)
75+
{
76+
if (inputStream == null)
77+
return Task.FromResult<byte[]?>(null);
78+
79+
try
80+
{
81+
using var data = NSData.FromStream(inputStream);
82+
if (data == null)
83+
return Task.FromResult<byte[]?>(null);
84+
85+
using var source = CGImageSource.FromData(data);
86+
if (source == null)
87+
return Task.FromResult<byte[]?>(null);
88+
89+
// Get metadata from the first image
90+
var metadata = source.CopyProperties((NSDictionary?)null, 0);
91+
if (metadata == null)
92+
return Task.FromResult<byte[]?>(null);
93+
94+
// Convert metadata to binary plist data
95+
NSError? error;
96+
var plistData = NSPropertyListSerialization.DataWithPropertyList(metadata, NSPropertyListFormat.Binary, 0, out error);
97+
if (plistData == null || error != null)
98+
return Task.FromResult<byte[]?>(null);
99+
100+
return Task.FromResult<byte[]?>(plistData.ToArray());
101+
}
102+
catch
103+
{
104+
return Task.FromResult<byte[]?>(null);
105+
}
106+
}
107+
108+
public static partial Task<Stream> ApplyMetadataAsync(Stream processedStream, byte[] metadata, string? originalFileName)
109+
{
110+
if (processedStream == null || metadata == null || metadata.Length == 0)
111+
return Task.FromResult(processedStream ?? new MemoryStream());
112+
113+
try
114+
{
115+
using var processedData = NSData.FromStream(processedStream);
116+
if (processedData == null)
117+
return Task.FromResult(processedStream);
118+
119+
using var source = CGImageSource.FromData(processedData);
120+
if (source == null)
121+
return Task.FromResult(processedStream);
122+
123+
// Restore metadata from NSData
124+
using var metadataNSData = NSData.FromArray(metadata);
125+
NSPropertyListFormat format = NSPropertyListFormat.Binary;
126+
NSError? error;
127+
var restoredMetadata = NSPropertyListSerialization.PropertyListWithData(metadataNSData, NSPropertyListReadOptions.Immutable, ref format, out error) as NSDictionary;
128+
if (restoredMetadata == null || error != null)
129+
return Task.FromResult(processedStream);
130+
131+
// Create mutable data for output
132+
var outputData = NSMutableData.FromCapacity(0);
133+
if (outputData == null)
134+
return Task.FromResult(processedStream);
135+
136+
// Determine UTI based on original filename
137+
string uti = "public.jpeg"; // Default to JPEG
138+
if (!string.IsNullOrEmpty(originalFileName))
139+
{
140+
var ext = Path.GetExtension(originalFileName).ToLowerInvariant();
141+
if (ext == ".png")
142+
uti = "public.png";
143+
}
144+
145+
// Create destination with metadata
146+
using var destination = CGImageDestination.Create(outputData, uti, 1);
147+
if (destination == null)
148+
return Task.FromResult(processedStream);
149+
150+
// Add image with preserved metadata
151+
using var image = source.CreateImage(0, new CGImageOptions());
152+
if (image != null)
153+
{
154+
destination.AddImage(image, restoredMetadata);
155+
destination.Close();
156+
157+
return Task.FromResult<Stream>(outputData.AsStream());
158+
}
159+
160+
return Task.FromResult(processedStream);
161+
}
162+
catch
163+
{
164+
return Task.FromResult(processedStream);
165+
}
166+
}
71167
}

src/Essentials/src/MediaPicker/ImageProcessor.netstandard.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,16 @@ public static partial Task<Stream> RotateImageAsync(Stream inputStream, string?
1111
// No EXIF rotation support on these platforms
1212
return Task.FromResult(inputStream);
1313
}
14+
15+
public static partial Task<byte[]?> ExtractMetadataAsync(Stream inputStream, string? originalFileName)
16+
{
17+
// No metadata extraction support on netstandard platforms
18+
return Task.FromResult<byte[]?>(null);
19+
}
20+
21+
public static partial Task<Stream> ApplyMetadataAsync(Stream processedStream, byte[] metadata, string? originalFileName)
22+
{
23+
// No metadata application support on netstandard platforms
24+
return Task.FromResult(processedStream ?? new MemoryStream());
25+
}
1426
}

0 commit comments

Comments
 (0)