Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
352 views
in Technique[技术] by (71.8m points)

c# - Saving 8-bit indexed data in a PNG alters original bytes when using BitmapSource, PngBitmapEncoder/Decoder

I have a problem when saving and loading a PNG using BitmapSource and PngBitmapEncoder/Decoder. Basically, I'd like to be able to save an image that originated as an array of bytes, and when the PNG is loaded into my program, reload the exact same bytes. Original data preservation is important.

At the same time, I'd like the PNG to use a custom palette (an indexed array of 256 colors).

I'm trying to save my 8-bit data with a custom indexed palette. The original data can range from 0-255. The palette may be a "Thresholded" palette (e.g. 0-20 is color #1, 21-50 is color #2, etc).

What I'm finding is that when I save the data, reload, and do CopyPixels to retrieve the "raw" data, the data values are set based on the the palette, not the original byte array values.

Is there a way to preserve the original byte array within the PNG, without losing the custom palette? Or is there a different way to retrieve the byte array from the BitmapSource?

The following is my Save routine:

      // This gets me a custom palette that is an array of 256 colors
      List<System.Windows.Media.Color> colors = PaletteToolsWPF.TranslatePalette(this, false, true);
      BitmapPalette myPalette = new BitmapPalette(colors);

      // This retrieves my byte data as an array of dimensions _stride * sizeY
      byte[] ldata = GetData();

      BitmapSource image = BitmapSource.Create(
        sizeX,
        sizeY,
        96,
        96,
        PixelFormats.Indexed8,
        myPalette,
        ldata,
        _stride);

      PngBitmapEncoder enc = new PngBitmapEncoder();
      enc.Interlace = PngInterlaceOption.On;
      enc.Frames.Add(BitmapFrame.Create(image));

      // save the data via FileStream
      enc.Save(fs);

And this is my load routine:

    // Create an array to hold the raw data
    localData = new byte[_stride * sizeY];

    // Load the data via a FileStream
    PngBitmapDecoder pd = new PngBitmapDecoder(Fs, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
    BitmapSource bitmapSource = pd.Frames[0];    

    // When I look at the byte data, it is *not* the same as my original data 
    bitmapSource.CopyPixels(localData, _stride, 0);

Any suggestions would be appreciated. Thanks.

Addendum #1: I'm finding that part of the problem is that the PNG is being saved as 32-bit color, despite the fact that I set it to Indexed8 and use a 256-entry color palette. This also seems to depend on the palette that is set. Any idea why?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I know why your PNG files are being saved in high colour. In fact, they are not "8-bit images saved as high colour"; the problem is that, from the moment they contain transparency, they're being loaded as high-colour by the .Net framework. The images themselves are perfectly fine, it's just the framework that's messing them up.

The workaround for that was posted here:

A: Loading an indexed color image file correctly

As wip said, though, if you want to preserve the original bytes, the actual changing of the palette should be done using chunks, rather than through .Net graphics classes, since .Net re-encoding will inevitably change the bytes. And, funnily enough, the fix for the palette problem I just gave already contains half of the code you need for that, namely, the chunk reading code.

The chunk writing code would be this:

/// <summary>
/// Writes a png data chunk.
/// </summary>
/// <param name="target">Target array to write into.</param>
/// <param name="offset">Offset in the array to write the data to.</param>
/// <param name="chunkName">4-character chunk name.</param>
/// <param name="chunkData">Data to write into the new chunk.</param>
/// <returns>The new offset after writing the new chunk. Always equal to the offset plus the length of chunk data plus 12.</returns>
private static Int32 WritePngChunk(Byte[] target, Int32 offset, String chunkName, Byte[] chunkData)
{
    if (offset + chunkData.Length + 12 > target.Length)
        throw new ArgumentException("Data does not fit in target array!", "chunkData");
    if (chunkName.Length != 4)
        throw new ArgumentException("Chunk must be 4 characters!", "chunkName");
    Byte[] chunkNamebytes = Encoding.ASCII.GetBytes(chunkName);
    if (chunkNamebytes.Length != 4)
        throw new ArgumentException("Chunk must be 4 bytes!", "chunkName");
    Int32 curLength;
    ArrayUtils.WriteIntToByteArray(target, offset, curLength = 4, false, (UInt32)chunkData.Length);
    offset += curLength;
    Int32 nameOffset = offset;
    Array.Copy(chunkNamebytes, 0, target, offset, curLength = 4);
    offset += curLength;
    Array.Copy(chunkData, 0, target, offset, curLength = chunkData.Length);
    offset += curLength;
    UInt32 crcval = Crc32.ComputeChecksum(target, nameOffset, chunkData.Length + 4);
    ArrayUtils.WriteIntToByteArray(target, offset, curLength = 4, false, crcval);
    offset += curLength;
    return offset;
}

The Crc32.ComputeChecksum function I used is an in-array adaption of the Sanity Free Coding CRC implementation. It shouldn't be hard to adapt it to a variable start and length inside the given array.

The byte writing class ArrayUtils is a toolset I made to read and write values to/from arrays with specified endianness. It is posted here on SO at the end of this answer.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...