Xamarin.Forms Android 复制图片到公共目录时添加EXIF元数据
在基于Xamarin.Forms开发跨平台应用时,需要实现三项核心功能:
- 拍摄照片并保存到本地存储
- 为拍摄的图片添加各类元数据
- 将携带完整元数据的图片副本存储到全局可访问的公共目录,方便用户将图片添加到邮件或通过其他方式分享
其中第一项功能可通过Xamarin.Forms原生能力在Android、iOS、UWP三端轻松实现;第二项功能可通过DependencyService在各平台分别实现,Android平台原本通过如下代码访问图片的EXIF属性值:
ExifInterface newExif = new ExifInterface(filePath);
设置完所需的全部EXIF属性值后,将修改保存回原图片即可。第三项功能同样通过DependencyService实现,将图片副本写入系统公共目录(例如Pictures相册目录)。
目前遇到的问题是:无法确定如何访问写入公共目录后的目标图片,为其添加EXIF数据。
当前已实现的公共目录文件保存代码如下:
public void SaveFileToPublicDirectory(string internalPath, string name) { string path = ""; try { // 从本地文件路径解码位图 Bitmap bitmap = BitmapFactory.DecodeFile(internalPath); System.IO.Stream fos; // 适配Android Q及以上版本 if (Build.VERSION.SdkInt >= BuildVersionCodes.Q) { ContentValues contentValues = new ContentValues(); contentValues.Put(MediaStore.IMediaColumns.DisplayName, name); contentValues.Put(MediaStore.IMediaColumns.MimeType, "image/jpg"); contentValues.Put(MediaStore.IMediaColumns.RelativePath, Android.OS.Environment.DirectoryPictures); var imageUri = Android.App.Application.Context.ContentResolver. Insert(MediaStore.Images.Media.ExternalContentUri, contentValues); fos = Android.App.Application.Context.ContentResolver.OpenOutputStream(imageUri); bitmap.Compress(Bitmap.CompressFormat.Jpeg, 100, fos); path = imageUri.ToString(); Log.Debug("i-Trax Droid", "Path for saving file in Pictures = " + path); // 得到的路径格式类似:content:/media/external/images/media/594 } else { String imagesDir = Android.OS.Environment.GetExternalStoragePublicDirectory( Android.OS.Environment.DirectoryPictures).ToString(); path = System.IO.Path.Combine(imagesDir, name); fos = new FileStream(path, FileMode.Create); bitmap.Compress(Bitmap.CompressFormat.Jpeg, 100, fos); Util.PathFilenameForPicturesDir_Android = path; } } catch (Exception ex) { string msg = string.Format("** Exception in SaveFileToPublicDirectory: {0} ", ex.ToString()); System.Diagnostics.Debug.WriteLine(msg); Util.WriteExceptionToSQL(msg); } return; }
在Android Q及以上版本中,上述代码执行后得到的图片路径为类似content:/media/external/images/media/594的Content Uri格式,需要获取刚写入文件的ExifInterface实例,读取、修改EXIF数据后保存回文件。
AndroidX的ExifInterface从1.3.0版本开始已经支持直接通过FileDescriptor从Content Uri初始化,不需要转换真实文件路径,适配分区存储规则,具体实现如下:
核心实现逻辑
- 首先确认Xamarin.Android项目已引用
Xamarin.AndroidX.ExifInterface包,版本不低于1.3.0,低版本不支持FileDescriptor初始化方式。 - 插入MediaStore拿到
imageUri后,不要只存Uri的字符串形式,保留Uri对象,通过ContentResolver打开可读写模式的FileDescriptor,直接传入ExifInterface构造函数。 - 完成EXIF属性修改后调用
SaveAttributes()即可自动将修改写回Uri对应的文件,无需额外做路径转换。
Android Q+版本EXIF写入代码示例:
// 传入Insert操作返回的imageUri对象,不要用ToString()转换后的字符串 using (var parcelFileDescriptor = Android.App.Application.Context.ContentResolver.OpenFileDescriptor(imageUri, "rw")) { if (parcelFileDescriptor != null) { // 直接通过文件描述符初始化ExifInterface ExifInterface exif = new ExifInterface(parcelFileDescriptor.FileDescriptor); // 按需设置EXIF属性,以下为常用示例 exif.SetAttribute(ExifInterface.TagDatetime, DateTime.Now.ToString("yyyy:MM:dd HH:mm:ss")); exif.SetAttribute(ExifInterface.TagMake, "Device Brand"); exif.SetAttribute(ExifInterface.TagModel, "Device Model"); // 保存修改,直接回写到公共目录对应的文件 exif.SaveAttributes(); } }
注意:打开FileDescriptor时必须指定模式为
"rw"(读写模式),传只读模式会导致SaveAttributes抛出权限异常。
低版本兼容
Android Q以下版本的逻辑不需要调整,原有代码已经能拿到公共目录文件的绝对路径,直接用new ExifInterface(filePath)的方式初始化、修改、保存EXIF即可。
优化建议
当前实现先将原图解码为Bitmap再重新压缩为JPG写入公共目录,会丢失原文件所有EXIF信息,同时二次压缩会造成画质损失,更合理的处理方式是:
- 跳过Bitmap解码、压缩步骤,直接将内部存储的原文件流复制到公共目录对应的输出流,保留原始文件数据
- 流操作统一加
using块或手动调用Close()、Dispose()释放句柄,避免文件占用导致EXIF写入失败 - 文件复制完成后再通过上述ExifInterface方法补充、修改需要的元数据。
内容的提问来源于stack exchange,提问作者Will Autio

