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

WPF:在纵向和横向方向正确显示照片和视频

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2023年9月23日

公共领域

6分钟阅读

viewsIcon

7054

详细文章,解释如何使 WPF 正确显示手机创建的媒体文件

引言

我编写了一个WPF应用程序,用于显示从手机下载到Windows的媒体文件(即照片和视频)。XAML代码和后台代码都很简单。

<Window x:Class="Show.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        WindowState="Maximized" WindowStyle="None">
  <Grid>
    <Image x:Name="PictureViewer" Visibility="Collapsed"/>
    <MediaElement x:Name="VideoPlayer" Visibility="Collapsed"/>
  </Grid>
</Window>

这里的想法是,应用程序使用<Image>来显示照片,并使用<MediaElement>以全屏模式(WindowState="Maximized" WindowStyle="None")显示视频。文件将在后台代码中读取,并根据文件扩展名决定是显示<Image>还是<MediaElement>

public MainWindow() {
  InitializeComponent();

  var commandLineArgs = Environment.GetCommandLineArgs();
  DirectoryInfo showDirectory;
  FileInfo? startFile;
  if (commandLineArgs.Length>1) {
    startFile = new FileInfo(commandLineArgs[1]);
  } else {
    throw new NotSuportedException();
  }

  switch (startFile.Extension.ToLowerInvariant()[1..]) {
  case "mp4":
    VideoPlayer.Visibility = Visibility.Visible;
    VideoPlayer.Source = new Uri(startFile.FullName);
    VideoPlayer.Play();
    break;

  case "jpg":
  case "png":
  case "bmp":
  case "gif":
  case "tiff":
  case "ico":
  case "wdp":
    PictureViewer.Visibility = Visibility.Visible;
    var bitmap = new BitmapImage();
    bitmap.BeginInit();
    bitmap.UriSource = new Uri(startFile.FullName);
    bitmap.EndInit();
    PictureViewer.Source = bitmap;
    break;

  default:
    throw new NotSupportedException();
  }
}

看起来简洁明了。但是,当我尝试显示手机以纵向模式拍摄的照片或视频时,方向(即横向或纵向)是错误的。

我用手机竖着(纵向)拍了这篇文章,然后在我的应用程序中显示它。正如你所看到的,照片以横向模式显示,即使它是以纵向模式拍摄的。

当我在Windows资源管理器中双击文件时,它会正确地以纵向模式显示。

问题排查

以下文件资源管理器截图显示了以前以横向和纵向模式拍摄的照片的这些尺寸。

Windows资源管理器会根据方向假定宽度和高度是交换的。

但是Bitmap则不然。它显示了这些尺寸,而不管媒体方向如何。

bitmap.Width: 4000
bitmap.Height: 1848

后来我发现,手机相机总是将传感器尺寸作为媒体的宽度和高度。所以我猜测手机是水平还是垂直握持的信息是额外存储在媒体文件中的。但存储在哪里呢?

于是我的麻烦就开始了。出于某种原因,我首先调查了视频(即mp4)的问题。有一个规范ISO_IEC_14496-14_2003-11-15。与所有ISO规范一样,需要付费购买。我觉得如今这样的规范应该在线免费提供。我确实找到了一份文件,但阅读它毫无帮助。这些规范很难读懂,会链接到其他规范,而且经常不包含所需信息,就像本例中媒体方向是如何写入文件中的一样。

有一些库可以读取元数据。我使用ffProbe查看了mp4文件,并得到了视频流的以下信息(还有音频、字幕和其他流)。

  Stream #0:0[0x1](eng): Video: h264 (High) (avc1 / 0x31637661), 
  yuv420p(tv, bt709, progressive), 3840x2160, 71957 kb/s, 32.24 fps, 
          29.83 tbr, 90k tbn (default)
    Metadata:
      creation_time   : 2023-08-05T09:04:15.000000Z
      handler_name    : VideoHandle
      vendor_id       : [0][0][0][0]
    Side data:
      displaymatrix: rotation of -90.00 degrees

方向在最后一行:rotation of -90.00 degrees。要正确显示纵向媒体,需要将媒体顺时针旋转90度,这在WPF中很容易实现。

有NuGet库FFMPegCore可以运行ffProbe,但由于ffProbe是一个.exe文件,它的运行速度有点慢。它需要0.5-0.1秒才能返回媒体文件的方向。对我来说太慢了,因为视频仍然需要由MediaElement加载。最好是使用一个作为我的程序一部分运行的库,只读取媒体文件的元数据,而C#有很多这样的库。

不幸的是,我之前已经安装了TagLibSharp,几天后我才意识到它无法读取mp4文件的任何媒体方向信息。我试图找到另一个支持读取Side data的库,但我没有找到任何。我甚至找不到Side data是什么的规范。似乎只有ffProbe使用这个表达。

解决方案

所以我尝试了一系列元数据读取库,最终发现它们将此信息称为Rotation。我选择了Nuget库MetaDataExtractor,它的速度至少是ffProbe的10倍。视频代码如下:

var isLandscape = true;
var directories = ImageMetadataReader.ReadMetadata(startFile.FullName);
var isRotationFound = false;
foreach (var directory in directories) {
  if (directory.Name=="QuickTime Track Header") {
    foreach (var imageMetadataTag in directory.Tags) {
      if (imageMetadataTag.Name=="Rotation") {
        isRotationFound = true;
        if (imageMetadataTag.Description=="-90") {
          isLandscape = false;
        }
        break;
      }
    }
  }
  if (isRotationFound) break;
}

VideoPlayer.Source = new Uri(startFile.FullName);
VideoPlayer.LayoutTransform =
  isLandscape ? new RotateTransform { Angle = 0 } : new RotateTransform { Angle = 90 };
VideoPlayer.Play();

MetaDataExtractor将一个stream称为directory。mp4实际上是基于QuickTime格式的。在我的视频文件中,有三个QuickTime Track Header类型的目录,其中一个包含方向信息。一个streamdictionary)包含实际数据,如视频帧,以及描述stream内容的元数据。这些元数据存储在Tag集合中。一个Tag有一个名称和一个值(description)。所以我们必须搜索一个名称为RotationTag。如果Tag缺失,则方向为横向,或者MetaDataExtractor根本不理解文件格式(还有很多其他格式,不仅仅是mp4或jpg)。

如上所述,我已经使用了TagLib,它对jpg文件有效。

var fileProperties = TagLib.File.Create(startFile.FullName);
var tagLibTag = (TagLib.Image.CombinedImageTag)fileProperties.Tag;
var rotation = tagLibTag.Orientation switch {
  TagLib.Image.ImageOrientation.None => Rotation.Rotate0,
  TagLib.Image.ImageOrientation.TopLeft => Rotation.Rotate0,
  //TagLib.Image.ImageOrientation.TopRight => Rotation.Rotate0, 
  //Mirror image vertically
  TagLib.Image.ImageOrientation.BottomRight => Rotation.Rotate180,
  //TagLib.Image.ImageOrientation.BottomLeft => Rotation.Rotate0, 
  //Mirror image horizontally
  //TagLib.Image.ImageOrientation.LeftTop => Rotation.Rotate90, 
  //Mirror image horizontally and rotate 90 degrees clockwise.
  TagLib.Image.ImageOrientation.RightTop => Rotation.Rotate90,
  //TagLib.Image.ImageOrientation.RightBottom => Rotation.Rotate90, 
  // Mirror image vertically and rotate 90 degrees clockwise.
  TagLib.Image.ImageOrientation.LeftBottom => Rotation.Rotate270, //
  _ => throw new NotSupportedException(),
};
var bitmap = new BitmapImage();
var stopwatch = new Stopwatch();
stopwatch.Start();
bitmap.BeginInit();
bitmap.UriSource = new Uri(startFile.FullName);
bitmap.Rotation = rotation;
bitmap.EndInit();
PictureViewer.Source = bitmap;

TagLibMetaDataExtractor更方便一些。TagLib为每个文件扩展名指定了所有标签名称,并为其创建了属性。有一个属性tagLibTag.Orientation,甚至还有一个枚举TagLib.Image.ImageOrientation列出了所有可能的合法值。MetaDataExtractor不知道有哪些可能的标签名称,开发者需要知道要搜索哪个标签名称。由于没有易于阅读的mp4标签规范,这可能很难弄清楚。另一方面,TagLib并不支持所有现有的标签,例如mp4文件中的Rotation标签。我想只使用MetaDataExtractor来处理照片和视频就足够了,但我认为展示使用两个库的代码可能对读者更有趣。

推荐阅读

MP4文件格式简介

如果您对WPF感兴趣,我强烈建议您查看我在CodeProject上的其他WPF文章。前两篇像本文一样深入技术细节,其余的则更容易阅读。

其他照片相关文章

最有用 的 WPF 文章

最引以为豪 的 WPF 文章

WPF 控件的 不可或缺的测试工具

MS 文档中 严重缺失 的 WPF 信息

我还写了一些非 WPF 的文章。

实现了不可能:

最受欢迎(300 万次浏览,37,000 次下载)

最有趣的部分:

我写MasterGrab已有6年了,从那时起,我几乎每天在开始编程之前都会玩它。打败3个试图争夺随机地图上所有200个国家的机器人大约需要10分钟。一旦一个玩家拥有所有国家,游戏就结束了。由于地图每次看起来都完全不同,所以游戏每天都充满乐趣和新鲜感。机器人为游戏带来了一些动态,它们之间的竞争程度与它们与人类玩家的竞争程度一样。如果你愿意,你甚至可以写自己的机器人,这个游戏是开源的。我大约花了两个星期写了我的机器人(整个游戏花了一年时间),但我很惊讶要打败机器人有多难。与它们对战时,必须制定策略,让机器人互相攻击,而不是攻击你。我迟早会写一篇关于它的CodeProject文章,但你现在就可以下载并玩它。应用程序中有很好的帮助说明如何玩。

© . All rights reserved.