Bug in Windows /.NET's System.Drawing.Save(Stream, ImageFormat). Corrupt PNG produced

╄→尐↘猪︶ㄣ 提交于 2019-12-12 11:50:16

问题


In some very specific situations, System.Drawing.Save(Stream, Imageformat) creates corrupt PNG images.

Is there a way to avoid it, such that:

  1. I don't need to use third party libraries, and
  2. I don't need to inspect the PNG bytes to know if I need to 'repair' something?

Steps to reproduce

  1. Create a System.Drawing.BitMap
  2. Add content to image such that it results in very specific PNG file size ("When does it occur")
  3. Call Save(Stream, Imageformat) -- choose PNG format

What is the problem?

The problem is an incorrect IDAT chunk after the last image data. It contains NO data, but has length bytes 00 00 ff f4. It can be detected with https://github.com/jsummers/tweakpng. We noticed that image libraries on Linux (not sure which ones) cannot deal with such mistakes. As far as we have seen, in Windows this mistake is ignored, and you will not notice any problem.

When does it occur?

It depends on the PNG-file size. The problem only occurs if the resulting PNG-file size in bytes is 0x1001C + n * 0x10000 with n 0, 1, 2, 3, 4, and likely larger.

It is reproducible

Step two can be tweaked to produce a specific PNG-file size (eg. color a varying amount of pixels in an otherwise empty BitMap). When the size was as described above, the error consistently occurred.

Code to reproduce

Replace the contents of Program.cs in a clean Console Application. When running the program, it will try to create a PNG with the exact specified size and save it as "constructed.png".

As an aside: a second PNG is saved "constructedReExport.png": this is created by loading the first and Saving it again. I think I remember this was a potential workaround, but when I run it now, the second file contains the same error...

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));

        }
    }
}

回答1:


I have seen issues when manipulating some files created by Adobe products, different adobe products have different engines and sometimes this breaks GDI+ with a similar work-around to reserve the file. Using a 3rd party image manipulation tool that does not use GDI+ is probably the best option, eg ImageMagic (or the .net wrapper Magic.Net)

When you ad content, do you mean drawing to the image or adding a file, check that the file added is not the issue.



来源:https://stackoverflow.com/questions/52100703/bug-in-windows-nets-system-drawing-savestream-imageformat-corrupt-png-pro

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!