问题
In some very specific situations, System.Drawing.Save(Stream, Imageformat) creates corrupt PNG images.
Is there a way to avoid it, such that:
- I don't need to use third party libraries, and
- I don't need to inspect the PNG bytes to know if I need to 'repair' something?
Steps to reproduce
- Create a System.Drawing.BitMap
- Add content to image such that it results in very specific PNG file size ("When does it occur")
- 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