【问题标题】:Validate image from file in C#在 C# 中验证文件中的图像
【发布时间】:2010-09-17 15:52:03
【问题描述】:

我正在从文件中加载图像,我想知道如何在从文件中完全读取图像之前对其进行验证。

string filePath = "image.jpg";
Image newImage = Image.FromFile(filePath);

当 image.jpg 不是真正的 jpg 时,就会出现问题。例如,如果我创建一个空文本文件并将其重命名为 image.jpg,则在加载 image.jpg 时会抛出 OutOfMemory Exception。

我正在寻找一个函数来验证给定图像的流或文件路径的图像。

示例函数原型

bool IsValidImage(string fileName);
bool IsValidImage(Stream imageStream);

【问题讨论】:

  • 为什么不将该代码包装在 try...catch 块中,如果它抛出此异常,您可以认为它“无效”?诚然,这是一种幼稚的启发式方法,但它确实有效。其他任何东西都必须打开文件,因此无论如何,IMO 都不会节省大量性能。
  • 另一种方法请参见:stackoverflow.com/q/2053662/2181514

标签: c# .net image file-io


【解决方案1】:

这里是 2019 年,dotnet core 3.1。 我接受Alex 的回答,稍微实现一下

public static bool IsImage(this byte[] fileBytes)
{
    var headers = new List<byte[]>
    {
        Encoding.ASCII.GetBytes("BM"),      // BMP
        Encoding.ASCII.GetBytes("GIF"),     // GIF
        new byte[] { 137, 80, 78, 71 },     // PNG
        new byte[] { 73, 73, 42 },          // TIFF
        new byte[] { 77, 77, 42 },          // TIFF
        new byte[] { 255, 216, 255, 224 },  // JPEG
        new byte[] { 255, 216, 255, 225 }   // JPEG CANON
    };

    return headers.Any(x => x.SequenceEqual(fileBytes.Take(x.Length)));
}

用法:

public async Task UploadImage(Stream file)
{
    using (MemoryStream ms = new MemoryStream())
    {
        await file.CopyToAsync(ms);

        byte[] bytes = ms.ToArray();

        if (!bytes.IsImage())
            throw new ArgumentException("Not an image", nameof(file));

        // Upload your file
    }
}

【讨论】:

    【解决方案2】:

    这是我使用多个验证的方法。

    public class ImageValidator
    {
        private readonly Dictionary<string,byte[]> _validBytes = new Dictionary<string, byte[]>() {
            { ".bmp", new byte[] { 66, 77 } },
            { ".gif", new byte[] { 71, 73, 70, 56 } },
            { ".ico", new byte[] { 0, 0, 1, 0 } },
            { ".jpg", new byte[] { 255, 216, 255 } },
            { ".png", new byte[] { 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82 } },
            { ".tiff", new byte[] { 73, 73, 42, 0 } },
        };
    
        /// <summary>
        /// image formats to validate using Guids from ImageFormat.
        /// </summary>
        private readonly Dictionary<Guid, string> _validGuids = new Dictionary<Guid, string>() {
            {ImageFormat.Jpeg.Guid, ".jpg" },
            {ImageFormat.Png.Guid, ".png"},
            {ImageFormat.Bmp.Guid, ".bmp"},
            {ImageFormat.Gif.Guid, ".gif"},
            {ImageFormat.Tiff.Guid, ".tiff"},
            {ImageFormat.Icon.Guid, ".ico" }
        };
    
        /// <summary>
        /// Supported extensions: .jpg,.png,.bmp,.gif,.tiff,.ico
        /// </summary>
        /// <param name="allowedExtensions"></param>
        public ImageValidator(string allowedExtensions = ".jpg;.png")
        {
            var exts = allowedExtensions.Split(';');
            foreach (var pair in _validGuids.ToArray())
            {
                if (!exts.Contains(pair.Value))
                {
                    _validGuids.Remove(pair.Key);
                }
            }
    
            foreach (var pair in _validBytes.ToArray())
            {
                if (!exts.Contains(pair.Key))
                {
                    _validBytes.Remove(pair.Key);
                }
            }
        }
    
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0063:Use simple 'using' statement", Justification = "<Pending>")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "<Pending>")]
        public async Task<bool> IsValidAsync(Stream imageStream, string filePath)
        {
            if(imageStream == null || imageStream.Length == 0)
            {
                return false;
            }
    
            //First validate using file extension
            string ext = Path.GetExtension(filePath).ToLower();
            if(!_validGuids.ContainsValue(ext))
            {
                return false;
            }
    
            //Check mimetype by content
            if(!await IsImageBySigAsync(imageStream, ext))
            {
                return false;
            }
    
            try
            {
                //Validate file using Guid.
                using (var image = Image.FromStream(imageStream))
                {
                    imageStream.Position = 0;
                    var imgGuid = image.RawFormat.Guid;
                    if (!_validGuids.ContainsKey(imgGuid))
                    {
                        return false;
                    }
    
                    var validExtension = _validGuids[imgGuid];
                    if (validExtension != ext)
                    {
                        return false;
                    }
                }
            }
            catch (OutOfMemoryException)
            {
                return false;
            }
    
            return true;
        }
    
        /// <summary>
        /// Validate the mimetype using byte and file extension.
        /// </summary>
        /// <param name="imageStream"></param>
        /// <param name="extension"></param>
        /// <returns></returns>
        private async Task<bool> IsImageBySigAsync(Stream imageStream, string extension)
        {
            var length = _validBytes.Max(x => x.Value.Length);
            byte[] imgByte = new byte[length];
            await imageStream.ReadAsync(imgByte, 0, length);
            imageStream.Position = 0;
    
            if (_validBytes.ContainsKey(extension))
            {
                var validImgByte = _validBytes[extension];
                if (imgByte.Take(validImgByte.Length).SequenceEqual(validImgByte))
                {
                    return true;
                }
            }
    
            return false;
        }
    }
    

    【讨论】:

      【解决方案3】:

      注意到上述所有功能的几个问题。 首先 - Image.FromFile 打开给定的图像,然后会导致打开文件错误,无论出于何种原因想要打开给定的图像文件。甚至应用程序本身 - 所以我已经切换使用 Image.FromStream。

      切换 api 后,异常类型从 OutOfMemoryException 更改为 ArgumentException,原因我不清楚。 (可能是 .net 框架错误?)

      此外,如果 .net 将添加比当前更多的图像文件格式支持,我们将通过函数进行检查 - 只有在失败时才尝试加载图像是有意义的 - 只有在此之后才会报告错误。

      所以我的代码现在看起来像这样:

      try {
          using (FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read))
          {
              Image im = Image.FromStream(stream);
              // Do something with image if needed.
          }
      }
      catch (ArgumentException)
      {
          if( !IsValidImageFormat(path) )
              return SetLastError("File '" + fileName + "' is not a valid image");
      
          throw;
      }
      

      地点:

      /// <summary>
      /// Check if we have valid Image file format.
      /// </summary>
      /// <param name="path"></param>
      /// <returns>true if it's image file</returns>
      public static bool IsValidImageFormat( String path )
      {
          using ( FileStream fs = File.OpenRead(path) )
          {
              byte[] header = new byte[10];
              fs.Read(header, 0, 10);
      
              foreach ( var pattern in new byte[][] {
                          Encoding.ASCII.GetBytes("BM"),
                          Encoding.ASCII.GetBytes("GIF"),
                          new byte[] { 137, 80, 78, 71 },     // PNG
                          new byte[] { 73, 73, 42 },          // TIFF
                          new byte[] { 77, 77, 42 },          // TIFF
                          new byte[] { 255, 216, 255, 224 },  // jpeg
                          new byte[] { 255, 216, 255, 225 }   // jpeg canon
                  } )
              {
                  if (pattern.SequenceEqual(header.Take(pattern.Length)))
                      return true;
              }
          }
      
          return false;
      } //IsValidImageFormat
      

      【讨论】:

        【解决方案4】:

        使用 Windows 窗体:

        bool IsValidImage(string filename)
        {
            try
            {
                using(Image newImage = Image.FromFile(filename))
                {}
            }
            catch (OutOfMemoryException ex)
            {
                //The file does not have a valid image format.
                //-or- GDI+ does not support the pixel format of the file
        
                return false;
            }
            return true;
        }
        

        否则,如果您使用 WPF,则可以执行以下操作:

        bool IsValidImage(string filename)
        {
            try
            {
                using(BitmapImage newImage = new BitmapImage(filename))
                {}
            }
            catch(NotSupportedException)
            {
                // System.NotSupportedException:
                // No imaging component suitable to complete this operation was found.
                return false;
            }
            return true;
        }
        

        您必须释放创建的图像。否则,当您多次调用此函数时,这将抛出 OutOfMemoryException,因为系统资源不足,而不是因为图像损坏产生不正确的结果,并且如果您在此步骤后删除图像,你可能会删除好的。

        【讨论】:

        • 谢谢 :) 。我正在考虑这样做,但我想知道是否有一种方法已经内置到 .NET 框架中。由于没有其他人提到 .NET 框架中的任何内置函数来执行此操作,我相信这将是一个很好的解决方案。
        • 您可能应该捕获 OutOfMemoryException,这是文件格式无效时抛出的记录异常。这意味着你会让 FileNotFoundException 传播给调用者。
        • @dbkk:VB 参考真的很受伤。 :)
        • @Ervin:提问者不这么认为,但显然 这么认为。在编程的上下文中,您不是试图确定一个文件是否是某种柏拉图式的 JPEG 理想;您正在尝试确定您的程序是否可以打开并显示它。我认为最好的方法是让 .Net 尝试打开它并告诉您它是否可以这样做。
        • OutOfMemoryException 确实是根据 MSDN 捕获的正确异常!!! msdn.microsoft.com/en-us/library/stf701f5.aspx微软,你永远不会停止惊奇和困惑。
        【解决方案5】:

        这是我的图像检查。我不能依赖文件扩展名,必须自己检查格式。 我正在从字节数组中加载 WPF 中的 BitmapImages 并且不知道预先的格式。 WPF 可以很好地检测格式,但不会告诉您 BitmapImage 对象的图像格式(至少我不知道这个属性)。而且我不想再次使用 System.Drawing 加载图像来检测格式。这个解决方案速度很快,对我来说效果很好。

        public enum ImageFormat
        {
            bmp,
            jpeg,
            gif,
            tiff,
            png,
            unknown
        }
        
        public static ImageFormat GetImageFormat(byte[] bytes)
        {
            // see http://www.mikekunz.com/image_file_header.html  
            var bmp    = Encoding.ASCII.GetBytes("BM");     // BMP
            var gif    = Encoding.ASCII.GetBytes("GIF");    // GIF
            var png    = new byte[] { 137, 80, 78, 71 };    // PNG
            var tiff   = new byte[] { 73, 73, 42 };         // TIFF
            var tiff2  = new byte[] { 77, 77, 42 };         // TIFF
            var jpeg   = new byte[] { 255, 216, 255, 224 }; // jpeg
            var jpeg2  = new byte[] { 255, 216, 255, 225 }; // jpeg canon
        
            if (bmp.SequenceEqual(bytes.Take(bmp.Length)))
                return ImageFormat.bmp;
        
            if (gif.SequenceEqual(bytes.Take(gif.Length)))
                return ImageFormat.gif;
        
            if (png.SequenceEqual(bytes.Take(png.Length)))
                return ImageFormat.png;
        
            if (tiff.SequenceEqual(bytes.Take(tiff.Length)))
                return ImageFormat.tiff;
        
            if (tiff2.SequenceEqual(bytes.Take(tiff2.Length)))
                return ImageFormat.tiff;
        
            if (jpeg.SequenceEqual(bytes.Take(jpeg.Length)))
                return ImageFormat.jpeg;
        
            if (jpeg2.SequenceEqual(bytes.Take(jpeg2.Length)))
                return ImageFormat.jpeg;
        
            return ImageFormat.unknown;
        }
        

        【讨论】:

        • 上述代码对于特定的 PNG 文件失败。当我检查时,前 4 个字节包含 {80, 75, 3, 4} 而不是您提到的序列。普通查看者/编辑者可以打开图像。怎么回事?
        • 我有一个 255,216,255,237 的 JPEG,所以这不起作用。
        • 当这对 jpeg 有效时,只需将此字节序列添加到代码中,代码就可以正常工作
        • 旧但黄金 :) 我实现了一点,请参阅下面的答案
        【解决方案6】:

        我把分号的答案转换成VB:

        Private Function IsValidImage(imageStream As System.IO.Stream) As Boolean
        
                    If (imageStream.Length = 0) Then
                        isvalidimage = False
                        Exit Function
                    End If
        
                    Dim pngByte() As Byte = New Byte() {137, 80, 78, 71}
                    Dim pngHeader As String = System.Text.Encoding.ASCII.GetString(pngByte)
        
                    Dim jpgByte() As Byte = New Byte() {255, 216}
                    Dim jpgHeader As String = System.Text.Encoding.ASCII.GetString(jpgByte)
        
                    Dim bmpHeader As String = "BM"
                    Dim gifHeader As String = "GIF"
        
                    Dim header(3) As Byte
        
                    Dim imageHeaders As String() = New String() {jpgHeader, bmpHeader, gifHeader, pngHeader}
                    imageStream.Read(header, 0, header.Length)
        
                    Dim isImageHeader As Boolean = imageHeaders.Count(Function(str) System.Text.Encoding.ASCII.GetString(header).StartsWith(str)) > 0
        
                    If (isImageHeader) Then
                        Try
                            System.Drawing.Image.FromStream(imageStream).Dispose()
                            imageStream.Close()
                            IsValidImage = True
                            Exit Function
                        Catch ex As Exception
                            System.Diagnostics.Debug.WriteLine("Not an image")
                        End Try
                    Else
                        System.Diagnostics.Debug.WriteLine("Not an image")
                    End If
        
                    imageStream.Close()
                    IsValidImage = False
                End Function
        

        【讨论】:

          【解决方案7】:

          这应该可以解决问题 - 您不必从标头中读取原始字节:

          using(Image test = Image.FromFile(filePath))
          {
              bool isJpeg = (test.RawFormat.Equals(ImageFormat.Jpeg));
          }
          

          当然,您也应该捕获 OutOfMemoryException,如果文件根本不是图像,这将节省您的时间。

          而且,ImageFormat 具有 GDI+ 支持的所有其他主要图像类型的预设项。

          注意,您必须在 ImageFormat 对象上使用 .Equals() 而不是 ==(它不是枚举),因为运算符 == 不会被重载以调用 Equals 方法。

          【讨论】:

            【解决方案8】:

            一个同时支持 Tiff 和 Jpeg 的方法

            private bool IsValidImage(string filename)
            {
                Stream imageStream = null;
                try
                {
                    imageStream = new FileStream(filename, FileMode.Open);
            
                    if (imageStream.Length > 0)
                    {
                        byte[] header = new byte[30]; // Change size if needed.
                        string[] imageHeaders = new[]
                        {
                            "BM",       // BMP
                            "GIF",      // GIF
                            Encoding.ASCII.GetString(new byte[]{137, 80, 78, 71}),// PNG
                            "MM\x00\x2a", // TIFF
                            "II\x2a\x00" // TIFF
                        };
            
                        imageStream.Read(header, 0, header.Length);
            
                        bool isImageHeader = imageHeaders.Count(str => Encoding.ASCII.GetString(header).StartsWith(str)) > 0;
                        if (imageStream != null)
                        {
                            imageStream.Close();
                            imageStream.Dispose();
                            imageStream = null;
                        }
            
                        if (isImageHeader == false)
                        {
                            //Verify if is jpeg
                            using (BinaryReader br = new BinaryReader(File.Open(filename, FileMode.Open)))
                            {
                                UInt16 soi = br.ReadUInt16();  // Start of Image (SOI) marker (FFD8)
                                UInt16 jfif = br.ReadUInt16(); // JFIF marker
            
                                return soi == 0xd8ff && (jfif == 0xe0ff || jfif == 57855);
                            }
                        }
            
                        return isImageHeader;
                    }
            
                    return false;
                }
                catch { return false; }
                finally
                {
                    if (imageStream != null)
                    {
                        imageStream.Close();
                        imageStream.Dispose();
                    }
                }
            }
            

            【讨论】:

            • 我试过这个。它适用于大多数测试用例,但不适用于特定的有效 jpg。 soi 值匹配,但 jpg 的 jfif 是 58111。我查看了标题,它在标题中包含 ICC_PROFILE 和其他一些东西,其中 JFIF 是预期的。 JFIF 在那之后,走得更远。
            【解决方案9】:

            如果您稍后需要为其他操作和/或其他文件类型(例如 PSD)读取数据,那么使用Image.FromStream 函数不一定是个好主意。

            【讨论】:

              【解决方案10】:

              好吧,我继续编写了一组函数来解决问题。它首先检查标题,然后尝试在 try/catch 块中加载图像。它只检查 GIF、BMP、JPG 和 PNG 文件。您可以通过向 imageHeaders 添加标头来轻松添加更多类型。

              static bool IsValidImage(string filePath)
              {
                  return File.Exists(filePath) && IsValidImage(new FileStream(filePath, FileMode.Open, FileAccess.Read));
              }
              
              static bool IsValidImage(Stream imageStream)
              {
                  if(imageStream.Length > 0)
                  {
                      byte[] header = new byte[4]; // Change size if needed.
                      string[] imageHeaders = new[]{
                              "\xFF\xD8", // JPEG
                              "BM",       // BMP
                              "GIF",      // GIF
                              Encoding.ASCII.GetString(new byte[]{137, 80, 78, 71})}; // PNG
              
                      imageStream.Read(header, 0, header.Length);
              
                      bool isImageHeader = imageHeaders.Count(str => Encoding.ASCII.GetString(header).StartsWith(str)) > 0;
                      if (isImageHeader == true)
                      {
                          try
                          {
                              Image.FromStream(imageStream).Dispose();
                              imageStream.Close();
                              return true;
                          }
              
                          catch
                          {
              
                          }
                      }
                  }
              
                  imageStream.Close();
                  return false;
              }
              

              【讨论】:

              • 不完全。如果 imageStream.Read 抛出异常,你还是不要关闭它。最好在流实例化周围加上 using 语句。
              • @Joe 我一定不同意。他不应该在这个函数中关闭或处理流。此函数未创建流,因此不应执行意外行为。另外.. 如果成功, Image.FromStream 将使用流(可能是只读的,并且无法重置),这意味着稍后对流的后续读取将失败,因为流已经被使用。此外,一旦成功加载图像(非常昂贵),然后立即处理。如果此方法返回 true,则调用者很可能会在下一行加载图像。所以这是双重工作。
              • @Troy,我同意。这种方法最好采用不受该方法影响的字节数组或一些类似的对象,特别是因为它是静态的。
              【解决方案11】:

              您可以通过嗅探标题进行粗略输入。

              这意味着您实现的每种文件格式都需要有一个可识别的标题...

              JPEG:前 4 个字节是 FF D8 FF E0(实际上,对于非 jfif jpeg,只有前两个字节可以做到这一点,更多信息 here)。

              GIF:前 6 个字节是“GIF87a”或“GIF89a”(更多信息here

              PNG:前 8 个字节是:89 50 4E 47 0D 0A 1A 0A(更多信息here

              TIFF:前 4 个字节是:II42 或 MM42(更多信息 here

              等...您可以找到您关心的几乎任何图形格式的标题/格式信息,并根据需要添加到它处理的内容中。这不会告诉你文件是否是该类型的有效版本,但它会给你一个关于“图像不是图像?”的提示。它仍然可能是损坏或不完整的图像,因此在打开时会崩溃,因此仍然需要尝试捕获 .FromFile 调用。

              【讨论】:

              • hmm.. 四个人在我打字和收集链接时回答了。繁忙的地方。
              • 请更正 TIFF 的前 4 个字节是 II* (49 49 42 00) 或 MM* (4D 4D 00 42)
              • 对于 JPEG,前 3 个字节可以,FFD8 是 SOI 标记,FF?? APP标记在哪里??通常为 E0。所以对于非 jfif jpeg 3 字节 FFD8FF 就可以了。
              【解决方案12】:

              您可以读取 Stream 的前几个字节,并将它们与 JPEG 的魔术头字节进行比较。

              【讨论】:

                【解决方案13】:

                JPEG 没有正式的标头定义,但它们确实有少量元数据可供您使用。

                • 偏移量 0(两个字节):JPEG SOI 标记(FFD8 十六进制)
                • 偏移量 2(两个字节):图像宽度(以像素为单位)
                • 偏移量 4(两个字节):图像高度(以像素为单位)
                • 偏移量 6(字节):分量数(1 = 灰度,3 = RGB)

                在那之后还有其他一些事情,但这些并不重要。

                您可以使用二进制流打开文件,并读取此初始数据,并确保 OffSet 0 为 0,OffSet 6 为 1,2 或 3。

                这至少会给您带来更高的精确度。

                或者您可以捕获异常并继续前进,但我认为您想要挑战 :)

                【讨论】:

                • 我会继续阅读文件的标题并将其与 .NET 支持的图像文件标题数组进行比较。最终,我会编写代码并将其作为解决方案发布给将来需要它的任何人。
                • 仅仅读取header并不能保证文件是有效的,并且在Image.FromFile()中打开时不会抛出异常。
                • 不,但我没有声称会。
                • 请更新 JPEG 格式en.wikipedia.org/wiki/JPEG_File_Interchange_Format 我将查找前 2 个字节 FFD8 和后 2 个字节 FFD9。你说的是offset2和offset 4无效或者可能不适用于所有JPEG格式
                【解决方案14】:

                我会创建一个类似的方法:

                Image openImage(string filename);
                

                我在其中处理异常。如果返回值为 Null,则说明文件名/类型无效。

                【讨论】:

                • 大声笑,当您发布此内容时,我一定是在写评论。我同意这个答案,完成工作很简单。
                • 这种方法有点错误。您不应该使用异常来控制程序流。另外.. 从该特定调用返回的异常可能非常具有误导性和模棱两可。
                • 我看不出这有什么问题。编写 openImage 的人选择在图像无效时抛出异常,而不是提供返回值。所以在我看来,捕捉和处理异常是他们打算让你处理这种情况的方式。
                • 异常占用资源。我们都知道。有点不对劲!不要偷懒!
                猜你喜欢
                • 2011-01-04
                • 2011-04-24
                • 1970-01-01
                • 2010-10-03
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2012-03-21
                相关资源
                最近更新 更多