В некоторых специфических ситуациях System.Drawing.Save (Stream, Imageformat) создает поврежденные изображения PNG.
Есть ли способ избежать этого, такой что:
- Мне не нужно использовать сторонние библиотеки, а
- Мне не нужно проверять байты PNG, чтобы узнать, нужно ли мне что-то «ремонтировать»?
Шаги для воспроизведения
- Создание System.Drawing.BitMap
- Добавление содержимого к изображению таким образом, чтобы оно приводило к очень конкретному размеру файла PNG («Когда это происходит»)
- Call 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 с точным указанным размером и сохранить его как «built.png».
В качестве отступления: второй PNG-файл сохраняется "construReExport.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));
}
}
}