3

I am trying to create a function that will load a PNG from a URL into memory and then add an iTXt chunk with the keyword "openbadges" and some json data. I have every part working except when I execute

metadata.SetQuery("/iTXt/openbadges", "");

I get an exception:

Value does not fall within the expected range.

Here is the function:

private static byte[] CreateOpenBadge(BadgeAssertionEntity assertion)
{
    using (var image = LoadImage(new Uri(assertion.Badge.ImageUrl)))
    using (var imageStream = new MemoryStream())
    {
        image.Save(imageStream, ImageFormat.Png);

        var pngDecoder = new PngBitmapDecoder(imageStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);

        using (var badgeStream = new MemoryStream())
        {
            var pngEncoder = new PngBitmapEncoder();

            pngEncoder.Frames.Add(BitmapFrame.Create(pngDecoder.Frames[0]));

            var metadata = pngEncoder.Frames[0].Metadata as BitmapMetadata;

            if (metadata == null)
                throw new ApplicationException();

            metadata.SetQuery("/iTXt/openbadges", "");

            pngEncoder.Save(badgeStream);

            return badgeStream.ToArray();
        }
    }
}

Any ideas what I am doing wrong?

jww
  • 97,681
  • 90
  • 411
  • 885
kulin
  • 41
  • 1
  • 4
  • If you are using WPF, you can use the [BitmapMetadata.SetQuery()](http://msdn.microsoft.com/en-us/library/system.windows.media.imaging.bitmapmetadata.setquery(v=vs.110).aspx) function to do this. If its WinForms, you should be able to reference PresentationCore, and WindowsBase and still use it. If its .NET < 3.0, then you're out of luck with this method. – Icemanind Oct 06 '14 at 22:18
  • I am using WindowsBase and PresentationCore in .net 4.5. – kulin Oct 06 '14 at 22:27

2 Answers2

3

The trick is to setquery() "/iTXt/Keyword" and "/iTXt/TextEntry", but you must use a char[] for the value of Keyword.

This is because internally "/iTXt/Keyword" wants a VT_LPSTR as the value type, and only a char[] will correctly convert into that.

Here's a sample that uses WIC to write a checkerboard png, including some iTXt metadata.

using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace PNGEncoder
{
    class Program
    {
        static void Main(string[] args)
        {
            var width = 256;
            var height = 256;

            var pngMetadata = new BitmapMetadata("png");

            pngMetadata.SetQuery("/iTXt/Keyword", "keyword0".ToCharArray());
            pngMetadata.SetQuery("/iTXt/TextEntry", "textentry0");

            pngMetadata.SetQuery("/[1]iTXt/Keyword", "keyword1".ToCharArray());
            pngMetadata.SetQuery("/[1]iTXt/TextEntry", "textentry1");

            var bitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Gray8, null);

            var pixels = new byte[width * height];
            for (var y = 0; y < height; y++)
            {
                for (var x = 0; x < width; x++)
                {
                    pixels[y * width + x] = (byte)(255 * (((x >> 4) ^ (y >> 4)) & 1));
                }
            }

            bitmap.WritePixels(new Int32Rect(0, 0, width, height), pixels, width, 0);

            var encoder = new PngBitmapEncoder();
            encoder.Frames.Add(BitmapFrame.Create(bitmap, null, pngMetadata, null));

            using (var stream = File.Create("checkerBoard.png"))
            {
                encoder.Save(stream);
            }
        }
    }
}
jmik
  • 311
  • 2
  • 10
1

I gave up on using PngBitmapEncoder. Instead I will just modify the png bytes directly. I have attached a class I made for this purpose incase others find it useful.

This class is heavily inspired by AShelly's response at Using Chunks in a PNG

I also use http://damieng.com/blog/2006/08/08/calculating_crc32_in_c_and_net for the crc hash.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;

using Badger.Libraries.Hashing;

namespace Badger.Libraries.Images
{
    public class Png
    {
        private readonly byte[] _header;
        private readonly IList<Chunk> _chunks;

        public Png(Uri imageUri)
        {
            _header = new byte[8];
            _chunks = new List<Chunk>();

            var webResponse = WebRequest.Create(imageUri).GetResponse();

            using (var webResponseStream = webResponse.GetResponseStream())
            using (var memoryStream = new MemoryStream())
            {
                if (webResponseStream == null)
                    throw new ArgumentException("invalid uri");

                webResponseStream.CopyTo(memoryStream);

                memoryStream.Seek(0, SeekOrigin.Begin);

                memoryStream.Read(_header, 0, _header.Length);

                while (memoryStream.Position < memoryStream.Length)
                    _chunks.Add(ChunkFromStream(memoryStream));

                memoryStream.Close();
            }
        }

        public void AddInternationalText(string keyword, string text)
        {
            // 1-79     (keyword)
            // 1        (null character)
            // 1        (compression flag)
            // 1        (compression method)
            // 0+       (language)
            // 1        (null character)
            // 0+       (translated keyword)
            // 1        (null character)
            // 0+       (text)

            var typeBytes = Encoding.UTF8.GetBytes("iTXt");
            var keywordBytes = Encoding.UTF8.GetBytes(keyword);
            var textBytes = Encoding.UTF8.GetBytes(text);
            var nullByte = BitConverter.GetBytes('\0')[0];
            var zeroByte = BitConverter.GetBytes(0)[0];

            var data = new List<byte>();

            data.AddRange(keywordBytes);
            data.Add(nullByte);
            data.Add(zeroByte);
            data.Add(zeroByte);
            data.Add(nullByte);
            data.Add(nullByte);
            data.AddRange(textBytes);

            var chunk = new Chunk(typeBytes, data.ToArray());

            _chunks.Insert(1, chunk);
        }

        public byte[] ToBytes()
        {
            using (var stream = new MemoryStream())
            {
                stream.Write(_header, 0, _header.Length);

                foreach (var chunk in _chunks)
                    chunk.WriteToStream(stream);

                var bytes = stream.ToArray();

                stream.Close();

                return bytes;
            }
        }

        private static Chunk ChunkFromStream(Stream stream)
        {
            var length = ReadBytes(stream, 4);
            var type = ReadBytes(stream, 4);
            var data = ReadBytes(stream, Convert.ToInt32(BitConverter.ToUInt32(length.Reverse().ToArray(), 0)));

            stream.Seek(4, SeekOrigin.Current);

            return new Chunk(type, data);
        }

        private static byte[] ReadBytes(Stream stream, int n)
        {
            var buffer = new byte[n];
            stream.Read(buffer, 0, n);
            return buffer;
        }

        private static void WriteBytes(Stream stream, byte[] bytes)
        {
            stream.Write(bytes, 0, bytes.Length);
        }

        private class Chunk
        {
            public Chunk(byte[] type, byte[] data)
            {
                _type = type;
                _data = data;
            }

            public void WriteToStream(Stream stream)
            {
                WriteBytes(stream, BitConverter.GetBytes(Convert.ToUInt32(_data.Length)).Reverse().ToArray());
                WriteBytes(stream, _type);
                WriteBytes(stream, _data);
                WriteBytes(stream, CalculateCrc(_type, _data));
            }

            private static byte[] CalculateCrc(IEnumerable<byte> type, IEnumerable<byte> data)
            {
                var bytes = new List<byte>();

                bytes.AddRange(type);
                bytes.AddRange(data);

                var hasher = new Crc32();

                using (var stream = new MemoryStream(bytes.ToArray()))
                    return hasher.ComputeHash(stream);
            }

            private readonly byte[] _type;
            private readonly byte[] _data;
        }
    }
}
Community
  • 1
  • 1
kulin
  • 41
  • 1
  • 4