/*------------------------------------------------------------------------------
* 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);
}
}
}