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

标签 c# .net windows gdi+

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

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

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

重现步骤

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

问题是什么?

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

什么时候发生?

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

可重现

可以调整第二步以生成特定的 PNG 文件大小(例如,为空位图中的不同数量的像素着色)。当尺寸为上述尺寸时,一直出现错误。

重现代码

在干净的控制台应用程序中替换 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));

        }
    }
}

最佳答案

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

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

关于c# - Windows/.NET 的 System.Drawing.Save(Stream, ImageFormat) 中的错误。生成损坏的 PNG,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52100703/

相关文章:

c# - 从显示的 dataGridView 中删除一行?

c# - Windows 窗体中的 3 层架构

c++ - 如何使用 CPP 程序将文本插入窗口?

c++ - Winapi 对话框在 Windows XP 上损坏

python - 运行theano时的编译问题

c# - 如何保护图书馆的用户免受错误的初始化?

c# - Convert.ToDecimal(string) 和 Decimal.Parse(string) 之间的区别

C#同步进程启动

.net - 如何知道 IDisposable 是否已处理?

c# - 如何以编程方式隐藏桌面图标?