65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Windows 照片导入 API - Windows.Media.Import

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2015 年 9 月 21 日

CPOL

56分钟阅读

viewsIcon

60983

使用 Windows 照片导入 API,直接从智能手机和数码相机导入照片和视频,适用于 Windows 通用应用和 Windows 10 及更高版本的经典 Win32 应用程序。

引言

Windows 操作系统长期以来通过各种 API 支持从数码相机和存储卡导入照片和视频,例如Windows Image Acquisition (WIA) 和Windows Portable Devices (WPD)。

尽管有用,但这些编程接口要么过时,要么相对底层且使用起来很麻烦,并且通常不适合基于 Windows 运行时构建的现代 Windows 应用程序的异步编程模型。

在 Windows 10 中,Microsoft 推出了 **Windows 照片导入 API**,这是一个新的、完全异步的应用程序编程接口,属于通用 Windows 平台。

该 API 可由使用JavaScriptC#C++/CX 编写的 Windows 应用商店应用以及使用 C++ 编写的经典 Win32/COM 应用程序(借助Windows Runtime C++ 模板库 (WRL),这是许多开发人员熟悉且喜爱的受尊敬的Active Template Library 的现代继任者,或者使用 Kenny Kerr 的 MIT 许可的*Modern CPP* 框架(参考下文)来消费。

本文介绍了 Windows 照片导入 API,并使用 C# 编程语言为假设的 Windows 应用商店应用程序编写的代码片段来说明其用法。在屏幕上显示信息(XAML 标记)和收集用户输入超出了本文的范围,本文主要关注 API 的使用。

本文没有可下载的代码,但您可以下载一个功能齐全的示例,它是 Windows SDK 的一部分,托管在 GitHub 上(请参阅下面的开发人员链接部分)。

文章最后简要讨论了 API 在标准 C++ 和 Win32 中的使用。

简而言之

作为预告,下面的语句通过一行代码枚举所有识别的设备(FindAllSourcesAsync),任意选择第一个([0]),然后连接到它(CreateSessionAsync)。之后,它查找并选择设备上所有可导入的项目(FindItemsAsync),最后将所有内容导入用户的图片库(ImportItemsAsync),全部在一行代码中完成:据说该 API 是流式的,这使得表示法简洁。

请注意,如果未连接任何设备,此语句将崩溃(对空列表的第一个元素进行解引用)。这不是您将在应用程序中编写的实际代码,但它确实完成了上述所有操作。

您必须在应用程序清单中声明 **picturesLibrary** 和 **removableStorage** 功能,API 才能接受为您工作,然后在源文件顶部添加“using Windows.Media.Import;”语句,使 PhotoImport* 类型可用。就是这样。

// Snippet to be pasted in an async C# button handler. Assumes a device is connected and ready!
// Run your applet, click the button once, and go see your Pictures Library.

var result = await (await (await PhotoImportManager
 .FindAllSourcesAsync())[0]
 .CreateImportSession()
 .FindItemsAsync(PhotoImportContentTypeFilter.ImagesAndVideos, PhotoImportItemSelectionMode.SelectAll))
 .ImportItemsAsync(); 

在实际程序中,您可能希望将该语句分解成不同的部分:检查确实有要导入的设备,让用户选择一个,也许指定一些选项,例如文件保存位置。然后,也许可以在链中再添加一个调用,以便在最后删除已导入的文件,同时还可以添加一个进度指示器和一个取消按钮。

尽管如此,其本质就在那里:您现在就可以将照片和视频导入添加到您的应用程序中,并且只需付出最少的努力。

开发人员链接

官方文档:Windows.Media.Import 命名空间 (MSDN)
官方 Windows SDK 示例:MediaImport 示例 (GitHub 上的 Microsoft Universal Samples)
与 Windows SDK 示例相关的 MSDN 教程:从设备导入媒体

背景

Windows 照片导入 API 是为 Windows 10 从头开始设计的,旨在支持现代的、无模态的工作流,其中应用程序启动一个传输操作,该操作异步运行,完全在后台运行,因此在操作运行时不会被阻塞。

后台操作支持进度通知、事件和取消,因此应用程序始终处于完全控制之下。

对于经典(Win32/COM)应用程序,照片导入 API 作为进程内 COM 对象实例运行。通过使用 WRL 模板库或*Modern CPP* 框架可以方便地实例化和使用它。

对于 Windows 应用商店应用程序,照片导入 API 在进程外运行,托管在 Runtime Broker 中,这是一个每个用户的系统进程,为运行在 Windows Runtime 之上的 Windows 应用商店应用程序提供服务。

该 API 已*投影*到 C++/CX,这是 C++ 的一个版本,包含一些 Microsoft 特有的扩展,有助于使用 Windows Runtime 类型,并且还投影到 C# 和 JavaScript 语言,用于 Windows 应用商店应用程序。

它在各种语言中的使用感觉自然而原生,例如,底层的IVectorView<T>在 C# 中*投影*为IReadOnlyList<T>,因此 C# 程序员将像使用任何其他 .NET API 一样使用它,只需通过源文件顶部的 `using` 指令引用它即可。

在 Windows 应用商店应用程序挂起和终止期间,后台运行的导入操作将继续执行,这些挂起和终止由进程生命周期管理器 (PLM) 引起,PLM 可以根据需要挂起、终止并随后重新启动应用程序。例如,当 Windows 应用商店应用程序最小化到任务栏或隐藏时,它会被挂起,如果系统需要回收内存,可能会在没有进一步通知的情况下被终止。

当然,应用程序在 Windows 任务栏上仍然显示为正在运行,并且当用户再次与之交互时,PLM 会恢复或重新启动应用程序,例如通过单击任务栏图标恢复它,或使用 Alt+Tab 切换到它。

大多数时候,用户不会注意到挂起和终止,但应用程序必须处理这些事件并在重新激活时重新连接到资源。照片导入 API 包括一个机制,通过该机制,重新启动的应用程序可以重新连接到正在进行的操作,并重新附加其进度和完成处理程序。请注意,经典 Win32/COM 应用程序不受 PLM 挂起或终止的影响。

API 本身实现为一个基于 WRL 的 C++ 组件,它是 Windows Runtime 表面、ABI 或应用程序二进制接口的一部分,并向所有 Windows 10 变体(从 IoT 到 Phone 到 Desktop 到 Xbox 到 Hololens)上的所有 64 位和 32 位 Windows Runtime 和 Win32/COM 客户端公开,即,它是通用 Windows 平台的一部分,或者更确切地说,它是 OneCore UAP 的一个组件。

但是,有些地方通过 USB 主机连接导入照片是没有意义的,还有一些情况是底层堆栈和驱动程序不存在,例如,某些地区提供的 N 和 KN Windows 版本缺少与数字版权管理 (DRM) 和多媒体播放相关的组件,这不幸地也扩展到了便携式设备。

为了让您的应用程序能够普遍加载,正如通用 Windows 应用应该做的那样,API 在所有这些地方都*存在*,但不一定*有效*。照片导入 API 公开了一个机制,让您的应用程序可以在运行时确定 API 在您的应用程序当前执行的特定平台变体上是否有效。

请注意,对于 N 和 KN SKU,最终用户可以手动下载和安装适用于 Windows 10 的媒体功能包,以恢复平台的完整媒体和 DRM 功能,因此您的应用程序在某个时刻无法使用照片导入 API 并不意味着它在稍后无法在同一台机器上工作:应用程序每次启动时都必须查询以了解是否支持照片导入 API。

Windows 10 照片导入 API 的定义功能

  • Windows Runtime 异步编程模型,具有真正的后台操作。
  • 非常健壮的文件操作:您的应用程序可以崩溃并重新启动,而不会影响传输。
  • 从 PTP/MTP 和 MSC 设备(几乎所有数码相机和智能手机)导入照片和视频。
  • 从 U 盘和读卡器中的存储设备导入照片和视频。
  • 支持相机 RAW 文件和副文件(.WAV 音频注释,.XMP 元数据,...)。
  • 支持分段视频文件(例如 GoPro 视频拆分成多个 2 GB 的片段)。
  • 内置去重机制可自动预选新添加的内容(100 万条历史记录)。
  • 支持来自最多 64 个不同设备的并发导入会话。
  • 经过精心优化,可在 USB 2.0 和 USB 3.x 及更高版本设备上实现最佳传输性能。
  • 内置支持未来的协议(例如PTP/IP),这些协议将在 Windows 平台中添加。

通用工作流

使用 Windows 照片导入 API 的照片导入工作流分为几个简单的*阶段*:源设备选择、内容枚举、内容传输以及可选的源内容删除。

让我们详细看看每一个

1. 选择要导入的源

首先是设备发现,API 枚举可用数据源并将其列表返回给应用程序。USB 连接的智能手机和数码相机在此列表中,以及插入计算机 USB 读卡器的包含照片和视频的存储卡。大多数读卡器(USB 2.0、USB 3.x 和 PCIe 读卡器)都应受支持。API 本身不限于 USB 连接的数据源,并已准备好适应未来的连接机制,例如用于无线空中传输的 PTP/IP。

注意:Windows 10 的 RTM 版本存在一些设备兼容性问题以及一些错误。特别是,固件问题影响了许多尼康和富士数码相机型号,这些设备的设备始终被视为空的,尽管包含许多照片。此外,升级过程导致许多用户由于迁移错误而无法访问他们的文档文件夹(包括他们的图片库),因此无法使用任何应用程序导入图像。最后,一些旧一代 SATA 连接的读卡器由于驱动程序错误地报告设备性质而被识别为无效导入源,并且导入 API 本身也有一些错误。虽然照片和视频的导入对大多数人来说都能正常工作,但少数用户遇到了上述一个或多个问题,并在 Windows 10 中导入时获得了负面体验。幸运的是,其中大多数问题现在已得到解决。

回到我们的主题:应用程序选择一个*源*设备进行导入,并与该设备建立*导入会话*。在自动播放场景中,设备选择阶段是隐式的,因为系统将要导入的源的即插即用设备 ID 作为激活的一部分直接传递给应用程序:因此,应用程序直接使用系统提供的设备即插即用 ID 建立导入会话。

应用程序可以与DeviceWatcher类组合,以获得设备插入和拔出的通知,使用DeviceClass.PortableStorageDevice枚举,或者与AutoPlay组合,以通过WPD\ImageSource, StorageOnArrival, ShowPicturesOnArrival 和 MixedContentOnArrival自动播放事件获得设备插入的通知(自动播放的细节超出了本文的范围)。

应用程序还可以注册以处理 **ms-photos:import** URI 协议(本文末尾有详细信息)或其自己的类似协议,以允许其他应用程序启动它们并自动触发从给定设备的工作流:例如,Windows 10 中的 Phone Companion 应用通过此确切机制直接启动 Windows 照片应用对特定设备的导入工作流。

一旦创建了导入会话,您就可以开始与设备交互,下一步是查找要导入的内容。

2. 查找并选择要导入的内容

在上一阶段创建的导入会话中,应用程序启动一个*内容枚举操作*,该操作会查找设备上的所有可导入项。应用程序可以指示 API 按内容类型进行过滤:照片、视频或两者。

应用程序还可以指定初始项目选择应如何进行,可以选择不选择任何可导入内容,仅选择新内容(使用内置去重机制),或选择所有内容。

枚举阶段完成后,API 将返回在设备上找到的匹配项列表,并适当地设置其选择标志。

应用程序还可以选择指定导入目标,默认情况下是当前用户图片库的默认保存位置。应用程序还可以设置其他一些选项,其中一些将在本文后面介绍。

最后,应用程序可以通过在某个列表视图中显示带有复选框的缩略图,允许用户手动选择要导入的项目。相关的 XAML 数据绑定和底层视图模型超出了本文的范围:请参阅上面链接的 MediaImport SDK 示例,其中包含一个简单的缩略图视图实现,您可以将其用作起点。

API 公开的方法直接映射到通常的“全选”/“无选择”按钮,这些按钮通常在选择用户界面中找到,以及一个“选择新项”方法,该方法根据去重机制自动选择新内容,因此用户可以返回到默认的初始选择(如果他们愿意)。

需要注意的是,对于 Windows 应用商店应用程序,列表元素存储在不同的进程中,访问元素属性的成本相对较高,因为每次检索属性都会产生一次进程边界的本地 RPC 调用。按需检索项目属性是可行的,例如在列表视图中,当项目逐页显示时检索属性,但这可能不适合一次性遍历整个列表并一次性检索每个项目的属性。如果您想一次性全选或取消选择所有项目,或者返回到默认选择,API 提供了相应的函数,这些函数在服务器端即时运行。对于 Win32 应用程序,照片导入组件在进程内运行,不会产生额外成本。

目标可以是本地文件夹或网络共享。请注意,默认情况下,文件很可能写入 C: 驱动器,用户文档文件夹和图片库通常位于此处。这没有什么问题,但请注意,Windows 中有许多进程“对”新图片和视频的到达感兴趣,特别是系统索引器和属性缓存系统,以及缩略图缓存。这些服务也会写入 C: 驱动器,并与导入操作竞争。如果您的应用程序需要最大性能,最好将导入的文件写入其他驱动器。

3. 将内容从设备传输到主机

一旦选择了要导入的内容,下一个阶段就逻辑上是*内容传输操作*,其中 API 在后台批量导入所有选定的项目。应用程序还可以选择指示 API 忽略副文件或同级文件,只导入主文件。有关照片导入 API 文件处理的更多详细信息,请参阅下文。

传输操作期间会报告进度:到目前为止已导入的字节数、到目前为止已导入的项目数、要导入的总字节数、要导入的总项目数以及进度(一个介于 [0..1] 之间的双精度浮点值)。如果要将进度显示为百分比,可以将此值乘以 100:var percent = (int)(progress * 100 + 0.5);

客户端还可以订阅“ItemImported”事件,以便在项目成功导入后实时获得通知。

重要的是要注意,在进度处理程序或“ItemImported”事件处理程序中不应执行任何重要的处理。但是,设置进度指示器的值或计算/显示导入统计信息当然是可以的。

“ItemImported”事件可用于显示文件在最终用户系统上的名称,或将这些项目排队等待后续处理,也许将其添加到应用程序并行处理的列表中。关键是要尽快从事件处理程序返回。

我建议不要在用户界面中闪烁导入文件的缩略图,因为从目标文件中提取缩略图会干扰从设备到硬盘的数据流。如果您确实想这样做,请尝试限制显示的缩略图数量,在考虑替换之前至少让某个缩略图显示几秒钟。您将获得想要的美观视觉效果,用户将有时间看到图像,并通过限制更改次数来最大程度地减少减速。

请记住,应用程序不应与后台导入操作竞争:尝试在导入文件时处理它们(例如,将结果写回磁盘)会通过创建磁盘争用来破坏从设备到主机的稳定数据流,这可能会导致性能下降,当从慢速设备导入时,这种下降可能不明显,但当从更快的设备导入时,这种下降肯定会受到影响,因为文件比处理速度快得多地到达主机:您将从 USB 2.0 的 Apple iPhone 6S 设备获得每秒 10-15 张照片,有时从 USB 3.0 读卡器中的快速存储卡获得更多照片,因此在导入过程中您做的事情越少越好。

您可以利用 API 的异步特性,在长时间传输过程中保持应用程序的完全响应,但不要认为在进行大量涉及大量磁盘 I/O 的处理时是可以的,因为您的驱动器已经很忙了,或者在从快速源导入时会很忙:当您从快速源导入时,文件会比处理速度快得多地到达主机:您将从 USB 2.0 的 Apple iPhone 6S 设备获得每秒 10-15 张照片,有时从 USB 3.0 读卡器中的快速存储卡获得更多照片,因此在导入过程中您做的事情越少越好。

事件是异步从 Photos Import API 触发回客户端应用程序的,因此如果应用程序花费太长时间来处理进度和“ItemImported”通知,导入后台任务不会减慢速度,但后续事件将被丢弃,因为 API 会确保每个客户端最多只有一个未完成的事件:如果应该发送“ItemImported”事件通知,但前一个事件仍由客户端处理,API 将简单地丢弃事件并继续,而无需等待前一个事件处理程序的返回。

运行在 Windows Runtime 之上的应用程序可能会被挂起,甚至在某些情况下会被终止。最终用户无法分辨,但当应用程序不可见时(例如,它被最小化到任务栏),应用程序会处于“挂起”状态以节省能源。

一段时间后,Windows Runtime 的进程生命周期管理器甚至可以决定完全终止该应用程序并将其从内存中移除以回收资源。

当最终用户恢复或切换回应用程序时,它会被解冻并从挂起处继续,就好像什么都没发生一样,或者会自动启动一个新进程,并且应用程序必须特别注意恢复其状态并返回到被挂起之前的点。

Windows Runtime 应用程序状态和生命周期的详细信息超出了本文的范围,但要点是 API 到应用程序的事件和通知*可能*并且*将会*被错过。

您可以使用事件通知用户,但如果您将文件排队或标记为待处理(这很好,因为它可能为任何处理器提供一个起点),您仍然必须在最后遍历导入文件列表,并确保您捕获了所有内容。

显然,当应用程序挂起时,不会进行任何处理,当应用程序被终止并完全消失时,处理就更少了:必须特别注意在挂起/终止/重启序列后重新连接到挂起的后台操作(稍后会有更多介绍)。

Windows 10 的初始版本(RTM 10240)的照片导入 API 仅在文件边界报告进度和检查取消。这对于典型的实际操作来说不是问题,最常见的负载是从智能手机导入许多照片和一些视频,但在导入单个大文件(例如长视频)时,进度仅在单个文件导入完成后从 0% 跳转到 100%,这可能有点不方便,因为用户在导入该大文件时看不到进度指示器移动。

同样,如果导入批次被用户取消,当前正在导入的文件在实际取消发生之前仍然会完成。

照片导入 API(随 2015 年 11 月更新发布,版本 1511)解决了这些问题,并提供了精细的进度和取消,而无需对现有客户端进行任何更改:无需重新编译或重新部署,应用程序只需获得*progress.ImportProgress* 浮点值的更精细更新,该值是进度报告的一部分,包括导入大文件期间的中间更新。

照片导入 API 的传输性能经过精心调整,可充分利用设备的带宽,并且可以很好地与您的硬件和总线速度保持一致。

“谎言,该死的谎言和基准测试”一如既往地真实,但我仍然为您提供一些数据点。

在配置良好的 PC 上,此 API 已在 USB 3.0 密钥(ExtremePro CZ88)上以 230 MB/s 的持续实际速率将多 GB 视频文件导入到 SSD 硬盘(840PRO)。

从代表未来设备的极速 USB 3.0 源导入典型的智能手机快照时,连接到非常现代的配备 SSD 的 Z440 开发工作站,该 API 在 12,600 个文件的负载上每秒导入超过 130 个文件,在大约 95 秒内完成。

在另一项更综合的测试中,旨在测量 API 和底层堆栈(包括文件系统、I/O 子系统和驱动程序开销)的开销,但不受 SSD 闪存速度的限制,将一百个 Nikon D810 文件(每个约 40 MB)从内存盘导入到另一个内存盘,速度为 2.4-2.5 GB/s - 即,在大约 1.65 秒内导入了 4 GB 的文件。

请记住,我们讨论的是端到端场景,其中*磁盘文件*是从一个*驱动器*复制到另一个*驱动器*,而不是硬件制造商的低级“高达”数字。在内存盘测试中,照片导入 API 的性能比 XCOPY 快约 10%,在 SSD 测试中性能也差不多。

照片导入 API 针对传输大型多媒体文件进行了优化,并尝试流畅地流式传输相对较大的数据块。当从海量存储设备(USB 3.x 或 PCIe 读卡器)导入并写入 SSD(最好不是 C: 以减少争用)时,可以实现最大速度。Windows 10 “Redstone” 2016 年中期版本中包含的 API 版本将稍快一些,并且一个(在平台更深处修复的)错误将激活一些旧的(SATA)读卡器,据报道,这些读卡器在 Windows 10 的原始版本和 2015 年 11 月版本中与此 API 无法正常工作。

一如既往,您的体验可能会因您的条件、测试平台、文件大小以及许多其他因素而大不相同,但可以肯定的是,您的照片和视频传输的速度限制可能不在 Windows 10 照片导入 API 中:如果您正在寻找一种能够很好地适应未来硬件发展的快速导入解决方案,那就是它了!

Windows 10 版本 1511 中的照片导入 API,在从 iPhone 6S 导入时,速度比 Windows Live Photo Gallery 2012 或旧的内置“导入图片和视频”向导快*高达 3 倍*!当处理 USB 3.0 设备或最新的 USB 3.1 Type-C 外设时,甚至更快 :-)

♦ Windows 10 “RS1”(2016 年夏季)

Windows 10 “RS1”上的照片导入 API 速度更快。使用Windows Performance Analyzer,它已显示出高达 92-93% 的稳定磁盘写入效率平均值,考虑到测量是在实时零售系统的 C:\ 驱动器上进行的,未禁用任何服务或进程,并且总磁盘时间在测试期间几乎为 100%,即,计算机的 SSD 不会写得更快。作为旁注,如果您倾向于进行超详细的低级和非常低级的性能分析,Windows Performance Analyzer 是一个很棒的工具。

4. 从设备删除已导入的内容(可选)

在可选的*内容删除操作*中,应用程序可以指示 API 从设备中删除已导入的内容。API 将*仅*删除已成功导入的文件,这是由于其流畅界面的“链式”性质,因此除非文件在当前导入会话中首次成功导入到主机,否则不会意外地从用户设备中删除文件的风险。删除操作也报告进度并且可以取消。

从 Windows 应用商店应用使用照片导入 API

由于照片导入 API 访问用户的照片库和连接到其计算机的可移动设备,因此要启用对照片导入 API 的访问,第一项任务是在应用程序清单中声明“removableStorage”和“picturesLibrary”功能。

<Capabilities>
    <uap:Capability Name="removableStorage" />
    <uap:Capability Name="picturesLibrary" />
    ...
</Capabilities>

否则,在尝试访问 API 时将引发“访问被拒绝”异常,并且这些声明的功能还将指示 Windows 在应用程序首次安装时请求用户特定的同意。

由于照片导入 API 是通用 Windows 平台的一部分,因此它在所有平台上都可用,事实上,在所有平台上都可用,但并非所有平台都包含底层设备驱动程序和 MTP 堆栈,因此通用应用程序应做的第一件事是询问 API 在应用程序当前运行的平台上是否受支持。

using Windows.Media.Import;

...

if (await PhotoImportManager.IsSupportedAsync())
{
    // Light up the import UX in your application: show Import button etc...
}

如果不支持,应用程序通常应避免显示与导入体验相关的用户界面,例如应用栏上的“导入”按钮或某些菜单项可以隐藏或禁用。

场景 1:用户触发的导入工作流

当导入工作流被显式启动时,例如通过按下或点击应用程序用户界面中的“导入”按钮,则称为用户触发的导入工作流。

应用程序应枚举连接的设备并将其显示在列表中,以便用户可以选择要从中导入的设备。当只找到一个设备时,应用程序仍应显示设备选择列表,以提供一致的用户体验并避免跳到可能不正确的设备,例如当用户连接了外部 USB 硬盘并且相机已关闭时。

枚举可用设备非常直接:假设 sourceListBox 是一个列表控件,您可以直接绑定静态方法 FindAllSourcesAsync 的结果。

// Enumerate all available sources from which we can import

sourceListBox.ItemsSource = await PhotoImportManager.FindAllSourcesAsync(); // See ma' - no glue!

您通常会使用 XAML 将 PhotoImportSource 的属性绑定在一起,以在列表中显示设备名称并让用户选择一个。有关更多详细信息,请参阅 MSDN 示例。

导入源有许多属性,例如显示名称、制造商、型号、序列号、类型、电池电量、媒体列表(包含每个媒体的可用/已用空间)等。

您可以显示应用程序所需的任意数量的详细信息,例如,以下项目模板标记仅显示显示名称和描述。

<ListBox.ItemTemplate>
    <DataTemplate>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Column="0" Grid.Row="0" Text="{Binding DisplayName}" />
            <TextBlock Grid.Column="1" Grid.Row="0" Margin="10,0,0,0" Text="{Binding Description}" />
        </Grid>
    </DataTemplate>
</ListBox.ItemTemplate>

在撰写本文时,由于 XAML 平台中的一个差距,诸如 PhotoImportSource.Thumbnail 之类的缩略图属性是不可绑定的。SDK 示例演示了一种解决此限制的方法(请参阅项目列表视图)。

场景 2:自动播放

自动播放(和 URI 协议激活)需要较少的用户交互。外壳启动您的应用程序(当与设备关联时),并将设备 ID 直接作为您在激活时接收的 DeviceActivatedEventArgs 的属性传递。所有需要做的就是使用设备 ID 来创建会话。

假设 args 变量的类型为 DeviceActivatedEventArgs,则通过调用 PhotoImportSource 运行时类本身的静态 FromIdAsync() 方法来创建 PhotoImportSource,如下所示:

var source = await PhotoImportSource.FromIdAsync(args.DeviceInformationId);

您还可以从 StorageFolder 创建导入源,例如,某些设备在 Explorer 中挂载为外部驱动器,您可以使用 `PhotoImportSource.FromFolderAsync()` 从这些驱动器的根文件夹轻松创建导入源。

检查设备锁定状态

与数码相机和裸存储卡不同,智能手机可以被锁定,并且大多数智能手机在用户解锁手机之前不允许访问其内容,有时还需要用户通过点击设备上的额外提示中的“允许”来明确允许主机访问照片和视频。

PhotoImportSource 类有一个 IsLocked() 方法,您应该调用它来确定设备是否被锁定。

不幸的是,在至少撰写本文时,该方法仅适用于 Windows Phone 设备。幸运的是,它以布尔值引用的形式返回三态值,因此可以是 null、true 或 false,其中 null 表示“不知道”。

当值为 false(设备未锁定)时,只需继续工作流。当值为 true 时,您可能希望提示用户解锁其设备,然后单击您提供的“重试”按钮。

您可以考虑以合理的速率轮询锁定状态 - 例如每 2 秒一次,这为您提供了平均一秒的延迟 - 并且一旦发现设备未锁定就自动继续。

最后一种情况 - 结果为 null - 可能是最常见的情况,如果相应的智能手机市场份额能说明问题的话。当您从 IsLocked() 获得 null 引用时,您应该尝试探测存储介质并获取其容量。锁定的设备要么隐藏其存储,要么报告其容量为零,因此如果您在检索介质特性时遇到任何错误,或者报告的总容量为零,您可以安全地假定设备已锁定并相应地提示用户。

PhotoImportSource::StorageMedia 属性返回一个 PhotoImportStorageMedium 对象列表,这些对象详细描述了设备的存储。同样,获取一个空集合,或获取一个总容量(PhotoImportStorageMedium::CapacityInBytes)为零的介质集合,或在探测存储介质时捕获任何错误,都应解释为“设备已锁定”的情况。

创建导入会话

一旦选择了设备(手动或隐式),并且您对设备可访问感到满意(见上文),您的应用程序就可以通过创建 PhotoImportSession 类型的*导入会话*来建立与设备的连接。

var session = source.CreateImportSession();

会话可以通过多种方式进行配置,例如,您可以设置目标文件夹、目标文件的前缀以及是否创建子文件夹。

在下面的代码片段中,folder 是一个 StorageFolder,您可以通过文件夹选择器等方式获得。

session.DestinationFolder = folder;
session.AppendSessionDateToDestinationFolder = true;
session.DestinationFileNamePrefix = "Holidays in Switzerland - ";
session.SubfolderCreationMode = PhotoImportSubfolderCreationMode.DoNotCreateSubfolders;

API 可以为您在目标位置创建不同类型的子文件夹。

假设导入根文件夹是“C:\Users\<alias>\Pictures”。

将 session.AppendSessionDateToDestinationFolder 设置为 true 会自动创建类似“C:\Users\<your_alias>\Pictures**\YYYY-MM-DD nnn**”的内容,其中使用当前日期,nnn 是从 001 开始的顺序号。

API 可以将所有文件展平(无论是到导入根目录,还是到根目录中创建的会话日期文件夹),或者当源是海量存储设备时,可以通过将 session.SubfolderCreationMode 设置为 PhotoImportSubfolderCreationMode.KeepOriginalFolderStructure 来重新创建源的文件夹层次结构。

最后,API 还可以创建基于日期的子文件夹,形式为 YYYY-MM,使用项目的创建/修改日期(以较早者为准)或 EXIF 的拍摄日期作为日期源。

这两种文件夹创建模式是正交的,可以合并:将 AppendSessionDateToDestinationFolder 属性设置为 true 并将 SubfolderCreationMode 属性设置为 PhotoImportSubfolderCreationMode.CreateSubfoldersFromFileDate 将导致文件夹的形式为“C:\Users\<alias>\Pictures\YYYY-MM-DD nnn**\YYYY-MM**”,其中最后一个级别基于文件日期。

♦ Windows 10 “RS1”(2016 年夏季)

已添加了一个新的会话属性来控制文件夹名称格式。例如,要将格式设置为 **YYYY-MM-DD**,请将 session.SubfolderDateFormat 设置为 PhotoImportSubfolderDateFormat.YearMonthDay。

session.SubfolderDateFormat = PhotoImportSubfolderDateFormat.YearMonthDay; // RS1

会话中添加了另一个属性来控制用户取消选择的项目(如果您提供了用户界面)的未来行为。默认情况下,未选择的项目在下次运行时仍被视为新项目,因为它们实际上从未被导入。

这迫使用户一遍又一遍地取消选择他们从未想导入的某些项目。API 的“RS1”版本添加了一个新的会话属性来控制此行为:将 session.RememberDeselectedItems 设置为 true 将使 API 记住那些未选择的项目,因此它们在将来的会话中保持未选择状态。最终用户始终可以手动重新选择任何项目(您只需将项目的 IsSelected 属性设置回 true)。

session.RememberDeselectedItems = true; // RS1

此新属性仅在 Windows 10 “RS1”上可用,并且仅当您使用 RS1 SDK 编译应用程序时才可用。在 TH2 或 TH1 上,该属性不存在,如果您在 RS1 上运行 TH1/TH2 应用程序,它仍然不知道新的 RS1 及更高版本属性的存在。

请注意,EXIF 模式*明显较慢*,因为必须即时打开每个文件并解析其 EXIF 元数据以确定它们应放入哪个文件夹,因此我*不*建议使用 EXIF 日期作为默认设置。

如果您选择向用户公开这些选项,让他们先在“按会话”或“按日期”之间选择,然后在他们选择“按日期”时,让他们额外选择“使用文件日期(快)”或“提取 EXIF 日期(慢)”,以便他们做出明智的决定。

EXIF 日期提取对视频无效,并且对照片造成了很大的额外负担,特别是对于 RAW 文件,其中必须为每个文件加载和初始化 RAW 编解码器,因此它*确实不*应该是默认设置:将其作为高级用户选项保留!

您可以使用上面链接的 Windows SDK 的 MediaImport 示例来玩弄大多数选项,以了解事物的工作方式。

枚举导入源的内容

下一步是枚举设备的内容。为此,您的应用程序调用 FindItemsAsync(),传递几个参数来控制返回的内容和默认选择的内容。

var foundItems = await session.FindItemsAsync(PhotoImportContentTypeFilter.ImagesAndVideos,
                                              PhotoImportItemSelectionMode.SelectAll);

您可以轻松地仅枚举照片(或仅视频)并预选所有项目、仅新项目或不选择任何项目。

如果您想显示进度并允许取消,您必须创建一个取消令牌和一个进度处理程序,并将结果映射到一个 Task。

请注意,我们事先不知道会找到多少项目,因此进度应该是*不确定的*,进度回调仅提供到目前为止找到的项目数量,作为一个非负整数。

var cts = new CancellationTokenSource();

...

// Progress handler for FindItemsAsync

var progress = new Progress<uint>((result) =>
{
    page.NotifyUser(String.Format("Found {0} Files", result.ToString()), NotifyType.StatusMessage);
});

// Find all items importable from our source

var foundItems = await session.FindItemsAsync(typeFilter, selectionMode).AsTask(cts.Token, progress);

选择要导入的内容

FindItemsAsync() 完成后,您会得到一个 PhotoImportFindItemsResult 实例,它公开了几个方法和属性,并包含一个找到的项目列表。请注意,默认项目选择已在上一步中指定。首选的默认选择模式是“SelectNew”,它将新内容标记为待传输。

您可以根据您想提供的交互和复杂性级别,将 PhotoImportFindItemsResult 的方法和属性公开给用户界面或首选项选项。

// Deselecting all items

foundItems.SelectNone();

// Selecting all items

foundItems.SelectAll();

// Selecting new items

await foundItems.SelectNewAsync(); // Runs the de-duplication algorithm

// Selecting what to import

foundItems.SetImportMode = PhotoImportImportMode.ImportEverything; // Items, sidecars & siblings

♦ Windows 10 “RS1”(2016 年夏季周年更新)

照片导入 API 的“RS1”版本增加了一个额外的方法,让您可以有效地选择一个日期范围,而无需自己遍历项目列表。

// Select a date range

foundItems.AddItemsInDateRangeToSelection(start, duration); // RS1

新方法选择一个称为“右半开区间”的区间,即 [start, start+duration),也就是说,开始包含,但结束不包含,避免意外重叠。例如,为了讨论起见,假设您想添加一个涵盖 1 月 1 日全天的范围。您可以将开始设置为 1 月 1 日 00:00,持续时间设置为 24:00:00.000,您将选择整天,但不会选择恰好在 1 月 2 日 00:00 拍摄的图像,因为如果您包含了区间结束,它将涵盖 1 月 1 日 00:00 + 24 小时正好是 1 月 2 日 00:00。

请注意,此调用是累积的:它不会清除已选中的内容,而是添加到当前选择中,因此得名。如果您想精确选择一个范围,请先调用 SelectNone()。

您可以轻松地提供预设,例如“上周末”、“上周”、“上个月”等,或者实现一个滑块,用户可以向后拖动,例如用滑块向前移动 30 天。您可以将“start”设置为当前日期/时间,并使用负的 TimeSpan 作为持续时间:要选择今天,您可以使用明天的 00:00 和 -24:00 小时的持续时间。

此新方法仅在 Windows 10 “RS1”上可用,并且仅当您使用 RS1 SDK 编译应用程序时才可用。在 TH2 或 TH1 上,该函数不存在,如果您在 RS1 上运行 TH1/TH2 应用程序,它仍然不知道新的 RS1 及更高版本方法的存在。

找到的项目列表中的每个 PhotoImportItem 都有自己的 IsSelected 布尔属性,可以单独设置。通常,这会通过列表视图来实现,最终用户可以勾选/取消勾选项目以标记是否传输它们。

假设在设备上找到了至少一个项目,以下代码片段将标记列表中的第一个项目以进行导入。

foundItems.FoundItems[0].IsSelected = true; // Just for illustration!

PhotoImportFindItemsResult 类公开了几个计数器,例如找到的项目数量(细分为照片、视频、副文件和同级文件),以及选定项目的数量,这是一个动态的计数器,随着选择方法的调用或每个项目的 IsSelected 属性的设置而变化。

var count = foundItems.TotalCount;
var selected = foundItems.SelectedTotalCount;
var selectedPhotos = foundItems.SelectedPhotosCount;
var selectedPhotosSize = foundItems.SelectedPhotosSizeInBytes;

// etc

PhotoImportFindItemsResult 还有一个 SelectionChanged 事件,您的应用程序可以订阅该事件,以在选择发生变化时获得通知。有关详细信息,请参阅 SDK 示例。

导入选定的内容

导入内容同样简单:对找到的项目调用 ImportItemsAsync()。

var importedItems = await foundItems.ImportItemsAsync();

更高级的版本,带取消令牌、逐项事件和进度报告

var cts = new CancellationTokenSource();

...

var progress = new Progress<PhotoImportProgress>((result) =>
{
    progressBar.Value = result.ImportProgress; // The progress control's range should be [0..1]
});

foundItems.ItemImported += async (s, a) =>
{
    await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
        page.NotifyUser(String.Format("Imported: {0}", a.ImportedItem.Name),
                        NotifyType.StatusMessage);
    });
};

// Import items from the our list of selected items

var importedItems = await foundItems.ImportItemsAsync().AsTask(cts.Token, progress);

从源设备删除已导入的内容

删除自然地从上一步开始:对 ImportItemsAsync() 任务返回的 PhotoImportImportedItems 调用 DeleteImportedItemsFromSourceAsync()。

var deleteResult = await importedItems.DeleteImportedItemsFromSourceAsync();

…或者,带进度和取消

var cts = new CancellationTokenSource();

...

var progress = new Progress<double>((result) =>
{
    progressBar.Value = result; // The progress range should be [0..1]
});

var deleteResult = await importedItems.DeleteImportedItemsFromSourceAsync()
                         .AsTask(cts.Token, progress);

关闭会话(IClosable/IDisposable)

PhotoImportSession 类型是可关闭/可处置的,当您的应用程序不再需要它时,必须调用 Close()(或 Dispose(),取决于您使用的语言)。

如果这样做,API 将在您的应用程序退出后很长一段时间内占用资源。

处理会话的一个好时机是结束导入工作流时,在所有异步操作完成后,例如在末尾显示摘要和 [关闭] 按钮。

将负责处置会话的调用放在该摘要的“关闭”按钮的处理程序中是一个不错的选择。多次调用 Close/Dispose 不会有负面影响(该函数被称为幂等)。

session.Dispose(); // Very important!

您无需保留对创建的会话的引用,因为所有结果类都有一个 Session 属性,让您随时可以访问它。

如果您在任何异步操作仍在运行时调用 Close()/Dispose(),该调用将因 ERROR_BUSY 而失败。此行为在MSDN上得到解释:“调用者必须等待所有未完成的异步操作完成后才能调用 Close”

目前,Windows 应用商店应用无法区分用户单击应用程序窗口右上角的红色 [X] 按钮引起的挂起和终止,因此没有实际方法可以决定是否终止后台运行的任务。

正确做法是让任务运行并正确实现终止/重启场景,以便在应用程序重新启动时重新连接到任何(仍在)运行的操作,有关详细信息,请参阅下一节。

支持终止/重启(PLM 终止)

照片导入 API 实现了一个特殊的机制,允许重新启动的应用程序重新连接到正在进行(并可能已完成)的异步任务,而应用程序则不在。

这个概念很简单:当应用程序启动时,它通过从静态方法检索操作列表来检查是否正在进行后台导入操作。

IReadOnlyList<PhotoImportOperation> ops = PhotoImportManager.GetPendingOperations();

列表中*最多*应有一个条目。API 支持高级用例的多个并发会话,因此如果您的应用程序一次处理多个导入会话,您可能会在列表中找到多个操作,但处理多个会话超出了本文的范围。

如果列表不为空,则检索最后一个条目并切换到导入阶段(`op` 是上述列表的一个元素,类型为 PhotoImportOperation)。

switch (op.Stage)
{
    case PhotoImportStage.FindingItems:

        foundItems = await op.ContinueFindingItemsAsync.AsTask(cts.Token,
            findAllItemsProgress);
            
        // Navigate to the "finding items" user experience

        break;

    case PhotoImportStage.ImportingItems:

        importedItems = await op.ContinueImportingItemsAsync.AsTask(cts.Token,
            importSelectedItemsProgress);

        // Navigate to the "importing items" user experience

        break;

    case PhotoImportStage.DeletingImportedItemsFromSource:

        deletedItems = await op.ContinueDeletingImportedItemsFromSourceAsync.AsTask(
            cts.Token, progress);

        // Navigate to the "deleting items" user experience

        break;

    case PhotoImportStage.NotStarted:

        // Navigate to the top of the import user experience
        
        session = op.Session;
        break;
}    

对于每种情况,应用程序都应导航到相应阶段的页面或对话框,并像什么都没发生一样恢复。

API 跟踪多个进程,并将正确的挂起操作返回给正确的进程,假设有多个 Windows 应用商店应用程序并发使用该 API。

MediaImport Windows SDK 示例包含终止/重启机制的完整工作实现:您可能需要参考它以获取详细信息。

从 Win32 中的 C++ 使用照片导入 API

本节旨在*作为介绍*,帮助您开始从纯 Win32 C++ 应用程序中使用 Windows 照片导入 API。

如果您已经熟悉使用 COM 对象,那么从纯 C++ / Win32 使用照片导入 API 相对容易,主要区别在于组件的激活,请继续阅读...

激活 Windows Runtime 组件与激活 COM 组件略有不同:您不是使用 CoCreateInstance() 和 CLSID,而是使用 GetActivationFactory() 和代表组件规范名称的字符串,然后调用 ActivateInstance(),最后 QI 以获取您想要的接口。

除了激活之外,*一切都相同*,因为 Windows Runtime 组件*确实*是 COM 组件。事实上,Windows Runtime 的原始名称是“Modern COM”,并且您仍然可以在新的 WRL 模板库中找到 ATL 的许多痕迹。

以下是如何进行(我可能省略了一两个细节,但重要的点都涵盖了):

#include <Windows.h>

#include <WRL.h>
#include <wrl\wrappers\corewrappers.h>

#include <Windows.Media.Import.h> // This header is part of the Windows SDK (Win10 10240 Universal)

// ...

using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;

using namespace Windows::Foundation;
using namespace ABI::Windows::Foundation::Collections;

using namespace ABI::Windows::Media::Import;

// ...

ComPtr<IInspectable> inspectable;
ComPtr<IActivationFactory> factory;
ComPtr<IPhotoImportSourceStatics> photoImportSourceStatics;
ComPtr<IPhotoImportManagerStatics> photoImportManagerStatics;

// Initialize the Windows Runtime.

RoInitializeWrapper initialize(RO_INIT_MULTITHREADED); // The new CoInitializeEx()

// ...

// Error checking omitted for brevity... the following three
//  statements essentially emulate CoCreateInstance()

// IPhotoImportManagerStatics

HRESULT hr = GetActivationFactory(
    HStringReference(RuntimeClass_Windows_Media_Import_PhotoImportManager).Get(),
    factory.GetAddressOf());
    
hr = factory->ActivateInstance(inspectable.GetAddressOf());
hr = inspectable.As<IPhotoImportManagerStatics>(&photoImportManagerStatics); // QI

您可以通过类似的方式创建 PhotoImportSource 运行时类的实例。

// IPhotoImportSourceStatics

hr = GetActivationFactory(
    HStringReference(RuntimeClass_Windows_Media_Import_PhotoImportSource).Get(),
    factory.ReleaseAndGetAddressOf());

hr = factory->ActivateInstance(inspectable.ReleaseAndGetAddressOf());
hr = inspectable.As<IPhotoImportSourceStatics>(&photoImportSourceStatics);

现在我们可以摆脱工厂和临时 inspectable 了。

factory.Reset();
inspectable.Reset();

顺便说一句,您可能会发现新方法比旧的 CoCreateInstance() 更冗长,但实际上将该调用拆分为三个调用通常效率更高,因为 GetActivationFactory() 对给定类只命中一次注册表,然后工厂会被缓存。这使得 ActivateInstance() 非常快,因此在创建大量相同类的对象实例时,新方法比旧方法都要好。

不幸的是,如果您是 COM 开发人员,目前没有办法注册 Windows Runtime 组件并使其可供您自己或通用使用。

♦ Windows 10 “RS1”(2016 年夏季周年更新)

API 的“RS1”版本引入了两个额外的接口来支持新方法和添加的两个属性。您需要显式查询它们,因为接口在 WinRT 中不继承,也就是说,您无法强制转换接口指针。

// Interfaces introduced in Windows 10 "RS1" (Summer 2016 Anniversary Update)
//
// IPhotoImportSession2
// IPhotoImportFindItemsResult2

ComPtr<IPhotoImportSession2> session2;
session.As<IPhotoImportSession2>(&session2); // Query for IPhotoImportSession2

ComPtr<IPhotoImportFindItemsResult2> foundItems2;
foundItems.As<IPhotoImportFindItemsResult2(&foundItems2);

为了使上述代码正常工作,您需要确保以下 Windows SDK 文件夹在您的 Include 路径中。如果您在 Visual Studio 中选择了 Windows 10 目标平台版本,这应该是自动的。

以下是 RS1 14393 版本(即“1607”,周年更新)所需的 SDK 路径。

C:\Program Files (x86)\Windows Kits\10\Include\10.0.14393.0\winrt
C:\Program Files (x86)\Windows Kits\10\Include\10.0.14393.0\shared
C:\Program Files (x86)\Windows Kits\10\Include\10.0.14393.0\um

您还需要链接一个静态库(根据您的构建类型选择 x86 或 x64)。

C:\Program Files (x86)\Windows Kits\10\Lib\10.0.14393.0\um\x64\runtimeobject.lib // 64-bit project
C:\Program Files (x86)\Windows Kits\10\Lib\10.0.14393.0\um\x86\runtimeobject.lib // 32-bit project

其他所有内容都是纯粹的、好的、熟悉的 COM,您会在 WRL 中找到很多帮助。例如,您现在可以调用 photoImportManagerStatics->FindAllSourcesAsync(...); 等,并开始从经典的 Win32 使用这个全新的 Windows 10 WinRT API!

最后但同样重要的是,您将不得不自己处理 WinRT 异步,这并不难,但您会注意到异步方法返回 IAsyncOperationIAsyncAction 的派生类,它们又派生自 IAsyncInfo:有很多乐趣在前面,下面的链接很快就会证明非常有用了。

另请参阅:

如何:使用 WRL 激活和使用 Windows Runtime 组件
如何:使用 WRL 完成异步操作
如何:使用 WRL 处理事件

这应该足以让您入门了!如果您在评论中表现出浓厚的兴趣(并且我找到了时间),我可能会扩展 Win32 代码片段以帮助启动您的 Win32 项目。请记住,从经典的 COM/Win32 使用异步 WinRT API 已经是一项中级到高级的任务,其难度和冗长程度明显高于从 C#(例如)在 Windows 应用商店应用程序中使用相同的 API!

用于 Windows Runtime ABI 上原生 C++11 开发的 WRL 替代方案

您还可以考虑 Kenny Kerr 的 **Modern CPP** 框架(MIT 许可),它以标准的 C++ 方式公开所有 Windows Runtime 类型,包括 Windows.Media.Import 命名空间和本文中描述的所有类型:请访问 http://moderncpp.com 获取更多信息。

您可以在 GitHub 上找到 Kenny 框架的最新版本:https://github.com/kennykerr/modern

更新:Kenny 现在为 Microsoft 工作!他的 Modern CPP 框架可能不会继续维护,但他可能是因为某个原因被聘用的 ;-)

ObjectiveC?

据报道,甚至还有一个用于照片导入 API(以及大部分 Windows Runtime)的 ObjectiveC 包装器。我不确定如何使用它们,但如果您有以该语言编写并正在移植到 Windows 的应用程序,这可能会有帮助。

关注点

副文件、同级文件和视频片段处理

本节描述了数字相机或摄像机上可能出现的文件(JPEG 文件、相机 RAW 文件、副文件…)的各种实际情况,以及照片导入 API 为处理它们而做出的设计选择。

术语

定义

JPEG

表示由大多数数码相机和智能手机等拍照设备生成的普遍存在的 .JPG 文件。

RAW

表示由某些数码相机和一些智能手机生成的制造商特定的相机 RAW 格式。RAW 格式包括尼康 .NEF、佳能 .CR2 和 .CRW、徕卡、松下和诺基亚 .DNG、宾得 .PEF、奥林巴斯 .ORF 以及其他几种。Threshold 自带内置 RAW 编解码器(以前称为 Microsoft Camera Codec Pack),并原生支持许多 RAW 格式。

同级

对于照片,*同级*是与数码相机拍摄 RAW+JPEG 模式匹配的 RAW 文件,相机为每张拍摄的照片创建两个单独的文件。照片导入 API 任意将 JPEG 文件视为该对的“主”项目,将 RAW 视为同级。

对于视频,*同级*是主视频的低分辨率版本,例如 GoPro .mp4 + .lrv 对。照片导入 API 任意将 .MP4 视为该对的“主”项目,将 LRV 视为同级。

每个项目最多可以附加一个同级。

视频片段

视频片段是某些视频录制设备的产物,它会将长的视频文件分割成多个片段。原因可能是文件格式限制、存储介质文件系统的大小限制(例如 2GB)或某些继承的任意约定,例如 35mm 电影胶片的 20 分钟标准时长。主(“第一个”)片段和后续(“延续”)片段的命名取决于视频录制设备。对于流行的 GoPro 运动相机,命名如下:如果 GOPR0052.MP4 是主文件,那么 GP010052.MP4、GP020052.MP4 等是延续文件,这种方案最多允许 99 个延续文件。相关性可以根据完整路径 + 视频类型 + 最后四位数字进行,然后查找前四位数字以决定哪个是主文件。最后一个延续文件(通常)也比主文件小,并且时间戳也可能反映时间顺序。视频片段可以有同级文件和副文件。

副文件

副文件不是 JPEG 或 RAW,也不是视频情况下的 LRV 的文件,它具有与它所引用的主文件相同的基本名称,但具有不同的文件扩展名。副文件可以包含其他元数据 (.XMP)、缩略图 (.THM)、音频注释 (.WAV) 或可能包含任何其他附加数据,例如与视频文件一起的 GPS 轨迹,其形式为包含 NMEA 语句的文本文件。例如,名为 DSC_0001.JPG 的 JPEG 文件可能有一个 DSC_0001.WAV 音频注释副文件。相关性是通过路径+基本名称进行的。副文件的时间戳可能与主文件的时间戳相似或相同,但是,在 .XMP 副文件(元数据)和其他文件的情况下,可以设想副文件已被编辑,例如在稍后的时间对图像进行评分。任何给定项目可能有一个以上的副文件,例如,一个缩略图、一个音频注释和一个 XMP 元数据副文件,它们都附加到同一个主文件。照片导入 API 还支持“双扩展名”,例如“Video.mp4”和“Video.mp4.thm”副文件,以及所有 Windows Phone“丰富捕获”副文件。

可能遇到几种情况

  • 数码相机只能创建 JPEG 文件,带或不带副文件。

  • 数码相机可以创建 JPEG 或 RAW 文件,带或不带副文件。

  • 数码相机只能创建 RAW 文件,带或不带副文件。

  • 数码相机可以创建 RAW 和 JPEG 两种格式的图像,并可附带或不附带 sidecar 文件。由于拍摄模式可以由用户即时更改,因此在同一个文件夹、同一张存储卡中同时看到独立的 JPEG、独立的 RAW 文件以及 RAW+JPEG 对是完全可能且甚至很常见的。

  • 视频录像机可以创建带或不带 sidecar 的视频文件,视频文件可能被分割成多个片段。片段本身可以有自己的同级文件,例如 GoPro 运动相机关联的 .LRV 文件,以及其自身的 sidecar 文件(例如 .THM 文件、GPS 轨迹…)

Photo Import API 支持上述所有场景

主要场景的处理方式符合预期:可以导入独立的 JPEG 文件,也可以导入独立的 RAW 文件。RAW+JPEG 文件会被检测并自动聚合,然后(可选是否删除)作为单个项目导入。该 API 区分 RAW+JPEG 与常规的 RAW + sidecar 文件或 JPEG + sidecar 文件场景:JPEG 被任意地视为主项目,而关联的 RAW 则被视为其同级文件。在 API 层面,同级文件与 sidecar 文件是分开识别的,以便简化应用程序的任务(无需匹配文件扩展名来推断是什么)。

诸如 .TMH、.WAV 或 .XMP 等 sidecar 文件会被识别并附加到主项目,或者在分段视频的情况下,附加到相应的视频片段。主项目及其同级文件和 sidecar 文件被视为一个项目,并被一次性选中、导入和可选删除。这符合用户关于拍摄一张照片或一个视频的感知,尽管该照片或视频可能以多种格式(JPEG + RAW)或多个 .MP4 视频文件片段存储。

API 用户可以选择导入所有内容,忽略任何 sidecar 文件,忽略同级文件,或同时忽略 sidecar 文件和同级文件。

Photo Import API 中实现的机制反映了设备的自然行为和用户的感知:当设置为 RAW+JPEG 模式时,设备上拍摄的每张照片都算作一张,尽管它会生成两个单独的文件。对于现代智能手机上的“实况照片”或“动态照片”也是如此,它们会在拍摄静态照片的同时录制一到两秒的视频,并与 JPEG 静态照片一起存储。

在所有逻辑中,Photo Import API 也将此类配对视为 1 张图像,并以此报告、导入和删除。同样,一个被分割成 20 个片段的长视频仍将在设备上以及 Photo Import API 中报告为单个视频。一种基于文件而非项目的更简单的方法将为每个 JPEG + RAW 配对计算两张图像,并报告拍摄次数的两倍,并将每个找到的视频片段报告为一个独立的视频,即报告 20 个视频,而实际上只拍摄了一个。

♦ Windows 10 “RS1”(2016 年夏季)

Apple Live Images 于 2015 年底在 iOS 中推出,在此之后 Photo Import API 才发布。使用 iPhone 设备拍摄 Live Images 时,API 将它们归类为“视频”,因为动态部分是 .MOV,而 JPEG 被视为 sidecar。这并不是一个大问题,因为默认情况下两个文件都会被导入,但这种行为已在 Windows 10 “RS1”(2016 年夏季)版本的 Photo Import API 中得到纠正,所有 iPhone 照片(无论是 Live 还是非 Live)都会被正确地报告为“照片”。

部分结果的成功-失败处理

Windows Runtime 的一个不为人知的特点是它试图通过不要求应用开发者处理他们无能为力的错误来简化他们的工作。

考虑以下情况:您的后台导入任务正在顺利进行,在传输过程中,用户拔出了设备(在驱动程序术语中称为惊喜移除)。连接已断开,当前正在传输的文件当然会被中止。应用程序应该怎么做?

过去,典型的应用程序会显示一个著名的“中止、重试、忽略”消息框,很可能针对批处理中一个剩余的项目。

最近,API 会失败。语言投影(或包装器)会将该失败转换为异常。在抢占市场的竞赛中,再加上墨菲定律的帮助,应用开发者在开发过程中可能没有测试过这个特定的异常,结果应用程序就会崩溃。

不要假装你的软件不会发生这种情况,未处理的软件异常让许多人措手不及,有时会带来灾难性和惊人的后果,而且将来还会继续发生。

如今,面向应用开发者的非常现代的 API,比如这里描述的 API,在这种情况下只会返回“成功”。应用程序只会显示典型的导入摘要,显示到目前为止所完成的工作,仿佛什么都没发生一样。相信我:这要好得多。

如果应用程序感兴趣(大多数可能不需要),您可以检查这三个长时间运行的异步操作(查找、传输和删除)结果类中存在的 HasSucceeded 布尔属性。

如果 HasSucceeded 被设置为 false,例如在导入阶段,那么就发生了问题,您可以根据需要在结果中比较选定项目的数量与已导入项目的数量。

如果您看到某些项目未被导入,您可以在摘要中显示一条额外的消息,例如“发生了一些问题,并非所有项目都已导入”,可能还会附带丢失文件的数量。

实际上您能做的不多了,所以就别做了。

如果用户拔出了设备,他们已经知道您的应用程序将无法完成任务。如果磁盘已满,他们也知道,因为系统已经显示了一个烦人的提示消息,并建议使用磁盘清理来腾出空间。

再次考虑:磁盘已满,传输中止,假设为 80% 来讨论。为什么您要取消整个导入工作流程,而不允许用户(或应用程序)删除设备上已导入的任何内容?就这样继续,仿佛什么都没发生一样。如果设备被拔出,删除任务也不会失败:它只会什么都不做,返回 S_OKHasSucceeded 设置为 false,而您也不应该在意。

底线:不再有含糊不清或未记录的 HRESULT,不再有未处理的软件异常。

一旦您克服了最初的震惊并仔细思考,这种新方法更好:不要用用户已知或无法控制的事情来打扰用户。 有道理吗?

程序员错误,例如忘记声明正确的应用程序功能,或者设法传递了错误的枚举值或其他失误,当然会像往常一样被报告,并会导致您的应用程序崩溃,希望只在开发期间崩溃,如果您不捕获语言投影为您抛出的异常的话。

顺便说一句,如果您有兴趣了解您的应用程序在实际使用中的表现(您绝对应该!),请为其配备一些“运行时智能”框架,例如 Microsoft Application Insights 或类似的工具,并向自己发送遥测摘要,报告失败和成功的情况,以及用户如何使用您的应用程序,他们选择了哪些选项,哪些功能被实际使用,哪些没有。然后,您可以使用这些数据来改善用户体验,例如通过选择更好的默认设置,或添加“软着陆”助手(例如通知),引导用户发现您应用程序中尚未被发现的隐藏功能。

内置去重机制

Photo Import API 实现了一个简单的去重机制,基于从原始的区分大小写的文件名、文件大小和文件日期(最早的创建日期和修改日期)计算出的键。

任何同级文件或视频片段都包含在该键的计算中,该键使用 CRC-64 算法创建。

在枚举设备内容时,API 提供了一种模式,该模式会自动预先选择自上次从该设备或介质进行导入以来在设备或介质上找到的新项目。每个项目的键都会被即时计算,并与存储在最终用户计算机上的每个用户位置的已知键表进行比较。

如果键存在于表中,则该确切的项目之前在该计算机上已被该用户导入,并且不再被标记为导入(项目的 IsSelected 属性会自动设置为 false)。如果键在已知键表中未找到,则该项目被视为新项目,并自动预选导入(其 IsSelected 属性设置为 true)。

成功导入项目后,其键将被添加到已知键表中,因此下次出现同一设备或介质时,该项目默认不会再次被选中。

该表最多存储约一百万个键,因此在大多数消费者场景中,去重功能几乎可以永久使用,并且应该相当可靠:拥有如此多的键,碰撞概率约为二千万分之一,因此去重机制“遗漏”(即将新文件视为已导入)的可能性在 Windows 消费者看来非常非常低,即使是对于每周拍摄数千张照片的专业摄影师来说也是如此。

有关哈希碰撞概率的更多信息,请参阅 Jeff Preshing 的这篇非常有趣的文章http://preshing.com/20110504/hash-collision-probabilities/

请注意,去重机制不尝试比较文件内容,因此在文件在设备上被更改但其大小和日期保持完全相同的情况下,它并非完全万无一失。不幸的是,对此问题没有真正的解决方案:通过 USB 上的 PTP 协议与设备通信就像通过吸管说话一样,对整个文件甚至部分文件进行哈希处理,然后在下载之前是不可能的。

去重机制内部使用的键作为 item.ItemKey 提供给您,这是一个 64 位无符号整数,您可以将其用作您自己(更好?)的去重机制的一部分。

数据库位于C:\Users\<alias>\AppData\Local\Microsoft\Windows\PhotoImport\History.dat,但没有人应该直接访问此文件,因为 API 提供了 SelectAll 选择模式、SelectAll() 方法以及每个项目的 IsSelected 属性,允许在没有任何麻烦的情况下重新导入任何或所有项目任意次数,因此永远不需要删除 PhotoImport 文件夹及其包含的历史文件:只需在用户界面中添加一个“全部导入”或“全选”按钮,您的用户会感谢您让他们在需要时重新开始。

无论如何,切勿直接删除该文件夹中的单个文件,因为这很可能会使数据库处于损坏状态。如果必须这样做,请在下次重新启动时删除或重命名该文件夹(例如使用 SysInternals 工具),或注销/登录并立即删除该文件夹。只要 Photo Import API 组件保持加载在 Runtime Broker 中,该去重数据库就可能一直被占用,它是在当前用户的名义下运行的!

不要杀死 RuntimeBroker.exe,并且在删除文件夹内容后始终注销或重启。

在 Windows 10 “RS3”(2017 年秋季)中,Runtime Broker 将是每个应用程序独立的,但 Import API 将保留在(已弃用) 共享的 Runtime Broker 中。

对于 Windows 10 “RS4”(2018 年),API 可能会被重新托管到其自己的共享进程主机中,因为共享的 Runtime Broker 将被淘汰。

URI 激活协议

如果希望允许其他应用程序以编程方式启动您的导入体验,您的应用程序可以注册并处理自定义 URI 协议。例如,Windows 10 随附的 Microsoft Photos 应用实现了 ms-photos:import 协议(完整语法:ms-photos:import?device=<percentEncodedDeviceId>),当以此方式启动时,它将自动开始其导入工作流。

await Launcher::LaunchUriAsync(ref new Uri("ms-photos:import?device=" +
    Uri::EscapeComponent(deviceId))); // ms-photos:import is handled by the Photos app

上述语句是用 C++/CX 编写的,适用于 Visual C++ 2015 Update 1 或更高版本。请注意‘await’关键字。下面是 C# 中的等效版本:

await Windows.System.Launcher.LaunchUriAsync(new Uri("import:from?device=" +
    Uri.EscapeDataString(deviceId))); // import:from is handled by the Photo & Video Import app

设备 ID 是要从中导入的设备的即插即用 ID,通常由自动播放(AutoPlay)传递给您的应用程序。

您可以通过添加几行代码和一些标记到您的应用程序清单中,在您的通用应用程序中实现类似的机制:有关更多信息,请参阅 MSDN 上的如何处理 Uri 激活

陷阱与注意事项

为了试水 Windows 应用商店应用开发,并“吃自己的狗粮”(eat my own dogfood),我基于本文创建了一个小型免费应用(可在 Windows 应用商店获取)。以下是我学到的东西。

session.DestinationFolder 属性

session.DestinationFolder 属性有点棘手。首先,您设置的任何文件夹都必须是用户图片库的一部分,因为应用商店应用无法访问它们运行的计算机的整个文件系统。如果您希望用户能够选择自己的任意目标文件夹,您必须以一种也将该文件夹添加到图片库的方式来实现。

将文件夹添加到图片库是导入 API 能够写入它的唯一方式,也是应用商店应用(包括内置照片应用)之后能够读取您图片的唯一方式。幸运的是,Windows Runtime 包含了所需的功能,而且并不难使用。

var pictureLibrary = await StorageLibrary.GetLibraryAsync(KnownLibraryId.Pictures);

if (pictureLibrary != null)
{
    var folder = await pictureLibrary.RequestAddFolderAsync();

    if (folder != null)
    {
        session.DestinationFolder = folder;
    }
}

请注意,您从同一属性读回的是实际的目标文件夹,包括 API 可能根据您设置的其他选项而附加的任何会话文件夹。因此,您不能依赖会话来为您存储文件夹:如果您想记住用户的选择(很可能如此),请存储用户选择的文件夹的 .Path 属性,而不是从 session.DestinationFolder 属性读取的值。

如果您将自定义文件夹的路径存储到您的应用程序设置中,请在读取它时小心:文件夹可能不再存在,例如,用户可能已删除或重命名了它。

您需要检查创建 StorageFolder(从保存的路径字符串)时的错误,并在必要时回退到图片库的默认设置:使用 StorageFolder.GetFolderFromPathAsync() 并捕获任何异常,然后使用从 StorageLibrary.GetLibraryAsync(KnownLibraryId.Pictures) 获取的 StorageLibrary 对象的 .SaveFolder 属性来获取一个安全写入内容的位置。如果出于任何原因不得不重置目标文件夹,请适当地警告用户,并擦除或更新您的设置中停滞的路径。

使用 DeviceWatcher

如果您的应用程序向用户显示可用设备列表,您可以使用 DeviceWatcher。DeviceWatcher 让您可以监视设备的添加和删除,并且您的列表可以动态更新以反映可用设备,而无需用户总是按下刷新按钮或按键。

创建和设置 DeviceWatcher 并不难:将其声明为,例如,您页面(Page)的一个成员变量

DeviceWatcher watcher;

然后在页面的构造函数中创建/初始化它

watcher = DeviceInformation.CreateWatcher(DeviceClass.PortableStorageDevice);

watcher.Added += Watcher_Added;
watcher.Removed += Watcher_Removed;
watcher.EnumerationCompleted += Watcher_EnumerationCompleted;

watcher.Start();

现在事件处理程序将自动调用:首先,对于每个已存在的设备,Added 处理程序将调用一次;然后,EnumerationCompleted 处理程序将调用一次;之后,随着设备的到来和离开,将调用适当的处理程序,AddedRemoved

我使用了一个成员变量 int deviceCount = 0; 来跟踪连接的设备数量。这样我就可以轻松地向用户显示一个“空”指示器,邀请他们插入兼容的设备,并在适当的时候隐藏该消息。

private async void Watcher_Added(DeviceWatcher sender, DeviceInformation args)
{
    ++devicesCount;

    if (devicesCount > 0)
    {
        await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
            this.Empty.Visibility = Visibility.Collapsed; // Hide "the list is empty' message
        });
    }

    if (watching)
    {
        await RefreshDeviceListAsync(deviceAdded: true);
    }
}

同样,您跟踪设备移除并维护设备计数、消息,并酌情刷新列表。

private async void Watcher_Removed(DeviceWatcher sender, DeviceInformationUpdate args)
{
    if (devicesCount > 0)
    {
        --devicesCount;
    }

    if (devicesCount == 0)
    {
        await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
            this.Empty.Opacity = 0;
            this.Empty.Visibility = Visibility.Visible;
            this.EmptyFadeInStoryboard.Begin();
        });
    }

    if (watching)
    {
        await RefreshDeviceListAsync(deviceAdded: false);
    }
}

由于您不希望每次连接设备时都更新列表,因此可以维护一些状态来跟踪正在发生的事情。例如,可以使用一个 bool watching = false; 成员变量,在 EnumerationCompleted 事件处理程序中将其设置为 true,这样您就知道,从现在开始,事件与用户操作相关。

private async void Watcher_EnumerationCompleted(DeviceWatcher sender, object args)
{
    watching = true;

    if (devicesCount == 0)
    {
        await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
            this.Empty.Visibility = Visibility.Visible; // Show "the list is empty' message
        });
    }
}

当设备被添加或移除时,您可以调用您自己的 RefreshDeviceListAsync(bool deviceAdded) 方法,该方法可以执行类似以下操作:

if (watching)
{
    IReadOnlyList<PhotoImportSource> sources = await PhotoImportManager.FindAllSourcesAsync();

    // NOTE: note entirely reliable, sometimes the DeviceWatcher fires the Added event
    // before the device is fully registered and ready to use. On other occasions, the
    // event fires for devices that have empty slots, and that will not be reported by
    // the Import API as valid import sources. Read below for possible mitigations.

    if (sources != null)
    {
        await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
            SourceListBox.ItemsSource = sources;
        });
    }
}

您可能会及早注意到的一件事是,DeviceWatcher 有时会在底层 WPD API 完全注册新设备之前触发设备到达通知,因此,上面的代码片段并不能非常可靠地工作。

为了使其更健壮,我将 DeviceWatcher 跟踪的设备计数与源列表中看到的设备计数进行了比较,并在循环中使用了一个等待-重试方案,使用 await Task.Delay(100); 在重试之间等待 0.1 秒,让系统有时间稳定下来。

请注意,某些设备,例如 Lexar USB 3.0 双槽读卡器,会触发两次设备到达事件,一次是为每个插槽触发一次,但 Import API 只会报告已填充的插槽——即实际包含介质的插槽。因此,您会观察到两个计数器之间的差异。如果您插入一个 Lexar 双槽读卡器,例如,两个插槽都为空,则由您的代码跟踪的 deviceCount 将是 2,但 sources.Count 将是 0,因为插槽为空,或者如果读卡器只插入了一张卡,则为 1。

您需要处理这些情况,而不是永远循环等待永远不会到达的设备(因此请使用最大重试次数,并在总共 1 秒后放弃)。另外,像 Lexar 这样的设备在插入和拔出时会触发自动播放事件,但在从读卡器自身的插槽插入或拔出内存卡时不会触发。因此,您仍然必须提供一个刷新按钮和/或某种热键(F5?)以便用户可以手动重新加载设备列表。

最后,如果您的应用程序被注册为某种设备类型的设备到达事件的处理程序,您的设备监视业务将与自动播放竞争:您将同时收到来自 DeviceWatcher 的 Added 事件,以及您的应用程序上的 OnActivated 事件,其中包含 ActivationKind.DeviceDeviceActivatedEventArgs。您必须仔细管理应用程序中的状态来处理这种情况,并确保应用程序行为得当!

另外,请考虑您的应用程序已经在执行某些操作(例如导入文件)时,另一个设备被插入并触发了 OnActivated 事件的情况:如果您的应用程序已经很忙,您可能希望在它完成当前工作之前忽略后续事件,除非您想付出额外的努力来支持多个并发导入会话,Import API 会愉快地允许您这样做,而您可能希望在一个专业的媒体导入解决方案中实现这一点。

状态管理带来了很多乐趣,而对这一切进行测试更是乐趣无穷 :-)

历史

2015-09-21 - 发布。
2015-10-07 - 修正了一些拼写错误,添加了一些段落。进行了少量可读性编辑和一些格式调整。
2016-01-24 - 添加了“检查设备锁定状态”部分,以及少量更新和编辑。
2016-02-09 - 扩展了“从 Win32 在 C++ 中使用 Photo Import API”部分。
2016-04-10 - 添加了关于 Windows 10“RS1”版本 API(2016 年夏季)的信息。
2016-12-28 - 添加了指向 MSDN 教程的链接,该教程与 Photo Import API 的 Windows SDK 示例相关。
2017-06-04 - 添加了“陷阱与注意事项”部分,以及少量更新和编辑。

© . All rights reserved.