/*------------------------------------------------------------------------------ * Author - Rob (http://stackoverflow.com/users/1185/rob) * ----------------------------------------------------------------------------- * Notes * - This program provides one example of fitting an image into 140 characters, * as per the challenge on StackOverflow at the following URL: * http://stackoverflow.com/questions/891643/twitter-image-encoding-challenge * ----------------------------------------------------------------------------- * Sources / References * - http://www.bobpowell.net/grayscale.htm * - http://porg.es/blog/what-can-we-fit-in-140-characters * ---------------------------------------------------------------------------*/ using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Text; namespace Twitter_Image { sealed class TwitterImage { /// /// CJK characters are used for encoded data. /// private const int CJK = 0x4E00; /// /// Katakana are used for offset encoding. /// private const int KATAKANA = 0x30A0; /// /// This number to use when down-sampling the image, note that this must /// ensure that the is less than or equal to 0xF for this to work. /// private const int LUMA = 0xF; /// /// Actual tweet size = 140 characters / 280 bytes /// Header = 4 characters / 8 bytes /// Data = 136 characters / 272 bytes /// private const int TWEET = 140; /// /// Take the data provided and run through it to remove the duplicate /// characters, when they are encountered they are converted to a /// number followed by the character. This is only done to the color /// run count part of the data. /// private static char[] CompressData(char[] data) { int count = 0; List compressed = new List(); char working = data[0]; for (int ndx = 0; ndx < data.Length; ndx++) { if (data[ndx] == working) { count++; } else { if (count != 1) { // Check to see if we have seen this pair before string str = new string(compressed.ToArray()); int index = str.IndexOf( Convert.ToString((char)((char)count + 0x30)) + Convert.ToString(working)); if (index != -1) { // Calculate the offset from the current location // and add it as a capital letter int offset = str.Length - index; compressed.Add((char)(offset + KATAKANA)); } else { compressed.Add((char)((char)count + 0x30)); compressed.Add(working); } } else { compressed.Add(working); } count = 1; working = data[ndx]; } } // Add whatever might be left if (count > 1) { compressed.Add((char)((char)count + 0x30)); } compressed.Add(working); return compressed.ToArray(); } /// /// Decode the image information provided and return an image based upon /// the contents provided. /// private static Bitmap DecodeRle(List encoded, int height, int width, int sampling) { // Create the new image Bitmap working = new Bitmap(width, height); // Set the current offset int center = (int)Math.Round(sampling / 2.0, MidpointRounding.AwayFromZero); // Move to the first encoded offset int count = 0, offset = 0; Color color = Color.FromArgb(encoded[offset].Luma, encoded[offset].Luma, encoded[offset].Luma); for (int ndy = center - 1; ndy < working.Height - center; ndy += sampling) { for (int ndx = center - 1; ndx < working.Width - center; ndx += sampling) { // Get the next color if (count == encoded[offset].Count) { offset++; color = Color.FromArgb(encoded[offset].Luma, encoded[offset].Luma, encoded[offset].Luma); count = 0; } // Create the block on the image for (int cx = -(center - 1); cx < center; cx++) { for (int cy = -(center - 1); cy < center; cy++) { working.SetPixel(ndx + cx, ndy + cy, color); } } count++; } } return working; } /// /// Reverse the operations performed by CompressData. /// private static char[] DecompressData(char[] data) { List decompressed = new List(); // Run though the data and expand the relevent characters for (int ndx = 0; ndx < data.Length; ndx++) { if (data[ndx] >= CJK) { decompressed.Add(data[ndx]); } else if (data[ndx] > KATAKANA) { // Get the information that is at the location behind the // current offset and expand it int offset = (data[ndx] - KATAKANA); for (int ndy = 0; ndy < (int)(data[ndx - offset] - 0x30); ndy++) { decompressed.Add(data[(ndx - offset) + 1]); } } else { for (int ndy = 0; ndy < (int)(data[ndx] - 0x30); ndy++) { decompressed.Add(data[ndx + 1]); } ndx++; } } return decompressed.ToArray(); } /// /// Down sample the image provided into the grayscale brackets /// provided and return the results. Note that the results are /// not converted back to a visible image. /// private static Bitmap DownSampleLuma(Bitmap image, int brackets) { for (int ndy = 0; ndy < image.Height; ndy++) { for (int ndx = 0; ndx < image.Width; ndx++) { // Get the color and down-sample based on the bracket Color original = image.GetPixel(ndx, ndy); int luma = (int)(original.R * 0.3 + original.G * 0.59 + original.B * 0.11); luma = luma / brackets; // Make sure that the luma doesn't go above 0xF luma = (luma > LUMA) ? LUMA : luma; image.SetPixel(ndx, ndy, Color.FromArgb(luma, luma, luma)); } } return image; } /// /// Returns a list RLE encoding information for the image provided. /// public static char[] EncodeImage(Bitmap image) { char[] packed; // Convert the image to grayscale Bitmap grayScale = DownSampleLuma(image, LUMA); // Update block sizes until we find a good sample size int sampling = 1; do { sampling += 2; Bitmap working = new Bitmap(grayScale); // Determine the center-point to use for sampling int center = (int)Math.Round(sampling / 2.0, MidpointRounding.AwayFromZero); // Sample the values List encoded = new List(); for (int ndy = center - 1; ndy < image.Height - center; ndy += sampling) { for (int ndx = center - 1; ndx < image.Width - center; ndx += sampling) { // Get the grayscale value for the pixel Color pixel = image.GetPixel(ndx, ndy); byte luma = (byte)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); // Is this the same as the last color? if (encoded.Count == 0 || encoded[encoded.Count - 1].Luma != luma) { encoded.Add(new DataPoint() { Luma = luma, Count = 1 }); } else { encoded[encoded.Count - 1].Count++; } } } packed = CompressData(PackRle(encoded, image.Height, image.Width, sampling)); } while (packed.Length > TWEET); return packed; } /// /// Pack the data provided in the encoded list such that there are /// two run count bytes to every Unicode character and four gray-scale /// color shades to ever character. /// private static char[] PackRle(List encoded, int height, int width, int sampling) { char working = default(char); // Create the array for the data char[] chars = new char[encoded.Count + 3]; // Add the header information chars[0] = (char)(CJK + height); chars[1] = (char)(CJK + width); chars[2] = (char)(CJK + sampling); // Leave four characters for the header int offset = 4; // Process the color count information for (int ndx = 0; ndx < encoded.Count; ndx += 2) { if (ndx + 1 == encoded.Count) { // Move the character to the top byte, the bottom byte will be zero working = (char)(encoded[ndx].Count << 8); } else { working = (char)(encoded[ndx + 1].Count << 8); working += (char)(encoded[ndx].Count); } chars[offset] = (char)(CJK + working); offset++; } // Store the offset of the start of the color information chars[3] = (char)(CJK + offset); // Process the color information so it is packed in groups of three for (int ndx = 0; ndx < encoded.Count; ndx += 3) { working = (char)0x0; for (int ndy = 2; ndy >= 0; ndy--) { if (ndx + ndy < encoded.Count) { working += (char)(encoded[ndx + ndy].Luma << (4 * ndy)); } } chars[offset] = (char)(working + CJK); offset++; } // Copy things over to a new array to remove any redundent data char[] results = new char[offset]; Array.Copy(chars, results, offset); return results; } /// /// Read the file provided from the disk, unpack the data, and process /// it so that the image can be returned. /// public static Bitmap ReadEncodedImage(string fileName) { char working; int offset = 0; // Read the contents of the file to the character array FileStream file = new FileStream(fileName, FileMode.Open); StreamReader stream = new StreamReader(file, Encoding.Unicode); char[] bytes = new char[file.Length / sizeof(char)]; stream.ReadBlock(bytes, 0, (int)(file.Length / sizeof(char))); stream.Close(); file.Close(); // Unpack the data bytes = DecompressData(bytes); // Read the header information int height = bytes[0] - CJK; int width = bytes[1] - CJK; int sampling = bytes[2] - CJK; int elementCount = bytes[3] - CJK; // Read the packed run count information List encoded = new List(); for (int ndx = 4; ndx < elementCount; ndx++) { working = (char)(bytes[ndx] - CJK); byte count = (byte)(working >> 8); encoded.Add(new DataPoint() { Count = (byte)(working - (count << 8)) }); if (count != 0) { encoded.Add(new DataPoint() { Count = count }); } } // Read the packed color information for (int ndx = elementCount; ndx < bytes.Length; ndx++) { if (bytes[ndx] == 0) { break; } working = (char)(bytes[ndx] - CJK); // Unpack the colors byte[] luma = new byte[3]; for (int ndy = 2; ndy >= 0; ndy--) { int value = (working >> (4 * ndy)); working = (char)(working - (value << (4 * ndy))); luma[ndy] = (byte)(value * LUMA); } // Double check to make sure the location is valid before storing for (int ndy = 0; ndy < 3; ndy++) { if (offset + ndy < encoded.Count) { encoded[offset + ndy].Luma = luma[ndy]; } } offset += 3; } // Decode the image and return it return DecodeRle(encoded, height, width, sampling); } } }