Windows /.NET 的 System.Drawing.Save(Stream, ImageFormat) 中的错误。损坏的 PNG 生成

Posted

技术标签:

【中文标题】Windows /.NET 的 System.Drawing.Save(Stream, ImageFormat) 中的错误。损坏的 PNG 生成【英文标题】:Bug in Windows /.NET's System.Drawing.Save(Stream, ImageFormat). Corrupt PNG produced 【发布时间】:2019-02-05 14:46:33 【问题描述】:

在某些非常特殊的情况下,System.Drawing.Save(Stream, Imageformat) 会创建损坏的 PNG 图像。

有没有办法避免它,例如:

    我不需要使用第三方库,并且 我不需要检查 PNG 字节就知道我是否需要“修复”某些东西?

复制步骤

    创建 System.Drawing.BitMap 向图像添加内容,使其生成非常特定的 PNG 文件大小(“何时发生”) 调用 Save(Stream, Imageformat) -- 选择 PNG 格式

有什么问题?

问题是最后一个图像数据后的 IDAT 块不正确。它不包含数据,但长度字节为 00 00 ff f4。 可以通过https://github.com/jsummers/tweakpng 检测到。我们注意到 Linux 上的图像库(不确定哪些)无法处理此类错误。据我们所知,在 Windows 中这个错误被忽略了,你不会注意到任何问题。

什么时候发生?

这取决于 PNG 文件的大小。仅当生成的 PNG 文件大小(以字节为单位)为 0x1001C + n * 0x10000,n 为 0、1、2、3、4,可能更大。

可重现

可以调整第二步以生成特定的 PNG 文件大小(例如,在原本为空的 BitMap 中为不同数量的像素着色)。当大小如上述时,错误始终发生。

重现代码

在干净的控制台应用程序中替换 Program.cs 的内容。运行程序时,它会尝试创建一个具有确切指定大小的 PNG 并将其保存为“constructed.png”。

顺便说一句:第二个PNG保存“constructedReExport.png”:这是通过加载第一个并再次保存来创建的。我想我记得这是一个潜在的解决方法,但是当我现在运行它时,第二个文件包含相同的错误......

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;

namespace CreateCorruptPng

    class Program
    
        static void Main(string[] args)
        
            // Choose 0x1001C + 0x10000 * n; with n = 0, 1, 2, 3, 4 to get corrupt PNG
            int targetOutputSizeBytes = 0x5001C;

            // You may need to fiddle with these parameters to 
            // successfully create an image of the exact size.
            int widthPixels = 2000;
            int height = 1200;

            var creator = new PngCreator(widthPixels, height);

            string outputPath = ".";
            creator.TryCreateWithSize(targetOutputSizeBytes);
            creator.SaveCurrentImage(Path.Combine(outputPath, "constructed.png"));
            creator.SaveAfterSecondExport(Path.Combine(outputPath, "constructedReExport.png"));
        
    

    public class PngCreator
    
        Bitmap _img;
        int _width;
        int _height;
        int _maxPixcount;

        public PngCreator(int w, int h)
        
            _width = w;
            _height = h;
            _maxPixcount = w * h;
        

        public void TryCreateWithSize(int requiredByteCount)
        
            Console.WriteLine($"Attempting to create png file of exactly requiredByteCount bytes.");
            Console.WriteLine($"Image size (w x h) = _width x _height.");

            int lowerBound = 0;
            int upperBound = _maxPixcount;

            bool success = false;
            while (upperBound > lowerBound + 1)
            
                InitImage();
                int pixelCount = (upperBound + lowerBound) / 2;
                AddPixels(pixelCount);

                int currentSize = GetPngByteCount();
                if (currentSize == requiredByteCount)
                
                    success = true;
                    break;
                

                if (currentSize < requiredByteCount)
                    lowerBound = pixelCount;
                else
                    upperBound = pixelCount;
            
            Console.WriteLine("Search stopped.");

            if (success)
                Console.WriteLine($"SUCCESS.\n   Created PNG with exact file size requiredByteCount bytes.");
            else
                Console.WriteLine($"WARNING.\n" +
                    $"   Could not produce PNG with file size requiredByteCount bytes.\n" +
                    "   Try to run again with different resolution.\n" +
                    "   If the file size in the last iteration is too small, try larger resolution.");
        

        private void InitImage()
        
            _img?.Dispose();
            _img = new Bitmap(_width, _height, PixelFormat.Format16bppArgb1555);
        

        private void AddPixels(int n)
        
            Console.WriteLine($"Coloring n pixels...");
            for (int i = 0; i < n; i++)
            
                int x = i % _width;
                int y = i / _width;
                _img.SetPixel(x, y, Color.FromArgb((i / 2) % 255, 0, 0));
            
        

        private int GetPngByteCount()
        
            using (MemoryStream s = new MemoryStream())
            
                _img.Save(s, ImageFormat.Png);

                byte[] imgBytes = s.ToArray();
                Console.WriteLine($"Png file size imgBytes.Length");
                return imgBytes.Length;
            
        

        public void SaveCurrentImage(string path)
        
            SaveImage(path, _img);
        

        public void SaveAfterSecondExport(string path)
        
            using (Bitmap imgReimported = ToPngBytesAndLoadAgain(_img))
            
                SaveImage(path, imgReimported);
            
        

        private Bitmap ToPngBytesAndLoadAgain(Bitmap img)
        
            return new Bitmap(new MemoryStream(ToPngBytes(img)));
        

        private static byte[] ToPngBytes(Bitmap img)
        
            using (MemoryStream s = new MemoryStream())
            
                img.Save(s, ImageFormat.Png);
                return s.ToArray();
            
        

        private static void SaveImage(string path, Bitmap img)
        
            Console.WriteLine($"Saving file to path");
            File.WriteAllBytes(path, ToPngBytes(img));

        
    

【问题讨论】:

欢迎来到Stack Overflow。我们不是微软。如果你发现了一个错误,你应该向他们报告。 这是一个有趣的发现。您是否受限于使用System.Drawing?如果没有,使用 3rd 方库(例如 Magick.NET)可能是您的解决方法。 @barrypicker Duurt 似乎已经有了解决方法,即加载生成的文件并再次保存。我同意将此报告给 Microsoft。 这里的实际问题是什么?看起来你已经回答了你自己的问题。我不太清楚为什么你把它放在 SO 上。你希望这里的人做什么? @Liam:感谢您的反馈;我更新了问题,以更清楚地说明我在寻找什么。另外,我自己的“答案”不是我想要的,也不是我想要的:-) 【参考方案1】:

我在处理一些由 Adob​​e 产品创建的文件时遇到了问题,不同的 adobe 产品有不同的引擎,有时这会破坏 GDI+,并使用类似的解决方法来保留文件。使用不使用 GDI+ 的第 3 方图像处理工具可能是最好的选择,例如 ImageMagic(或 .net 包装器 Magic.Net)

当您广告内容时,您是指绘制到图像还是添加文件,请检查添加的文件是否不是问题。

【讨论】:

以上是关于Windows /.NET 的 System.Drawing.Save(Stream, ImageFormat) 中的错误。损坏的 PNG 生成的主要内容,如果未能解决你的问题,请参考以下文章

如何将BitmapSource转换为位图

仅在 Windows 上:net5.0-windows(或 net6.0-windows)是不是允许我在 .Net 5(或 .Net 6)中重新编译 .Net 框架?

windows net 命令的用法大全

windows 10如何安装.net4

.NET对象与Windows句柄:句柄分类和.NET句柄泄露的例子

在 .NET 中检测 Windows 版本