It seems that the files you're dealing with are simple 24-bit bitmaps, right?
In that case, you could avoid using GDI+ alltogether (it's a bad idea to use it in ASP.NET anyway), and just parse the bitmap data directly. This will mean you don't even have to read the whole file - just the header, and whatever pixel you need.
If you're indeed working with simple 24-bit bitmaps, the following should work just fine:
Color GetPixel(string fileName, int x, int y)
{
var buffer = new byte[32];
using (var file = File.OpenRead(fileName))
{
if (file.Read(buffer, 0, 32) < 32) return Color.Empty;
// Bitmap type. Pretty much everything you find is BM.
var type = Encoding.ASCII.GetString(buffer, 0, 2);
if (type != "BM") return Color.Empty;
// Data offset
var offset = BitConverter.ToInt32(buffer, 10);
// Windows bitmaps have width and height in a fixed place:
var width = BitConverter.ToInt32(buffer, 18);
var height = BitConverter.ToInt32(buffer, 22);
if (width < x || height < y) return Color.Empty;
// Three bytes per pixel, padded to multiples of four
var rowSize = width * 3 + ((4 - ((width * 3) % 4)) % 4);
// And get our pixel - since we're non-compressed, non-indexed,
// 32-bit pixels, this is easy. Note that bitmaps are usually stored
// top to bottom:
file.Seek(offset + ((rowSize * (height - y - 1)) + x * 3), SeekOrigin.Begin);
if (file.Read(buffer, 0, 3) < 3) return Color.Empty;
// Alpha
buffer[3] = 0xFF;
var color = BitConverter.ToInt32(buffer, 0);
return Color.FromArgb(color);
}
}
Even if your files aren't simple 24-bitmaps, that cron-job of yours shouldn't have a problem converting them - if you want easy indexing into the bitmap data, you don't have much of a choice anyway :)
It needs just a few changes to support 8-bit indexed bitmaps:
Color GetPixel(string fileName, int x, int y)
{
var buffer = new byte[32];
using (var file = File.OpenRead(fileName))
{
if (file.Read(buffer, 0, 32) < 32) return Color.Empty;
// Bitmap type. Pretty much everything you find is BM.
var type = Encoding.ASCII.GetString(buffer, 0, 2);
if (type != "BM") return Color.Empty;
// Data offset
var offset = BitConverter.ToInt32(buffer, 10);
// Windows bitmaps have width and height in a fixed place:
var width = BitConverter.ToInt32(buffer, 18);
var height = BitConverter.ToInt32(buffer, 22);
if (width < x || height < y) return Color.Empty;
// One byte per pixel, padded to multiples of four
var rowSize = width + ((4 - ((width) % 4)) % 4);
// Now we're going to read an index into our palette
file.Seek(offset + ((rowSize * (height - y - 1)) + x), SeekOrigin.Begin);
if (file.Read(buffer, 0, 1) < 1) return Color.Empty;
// Jump to the palette record and get the actual color
file.Seek(54 + buffer[0] * 4, SeekOrigin.Begin);
if (file.Read(buffer, 0, 4) < 4) return Color.Empty;
var color = BitConverter.ToInt32(buffer, 0);
return Color.FromArgb(color);
}
}
If you want to avoid writing your bitmap parsing code, you'll just have to make sure the bitmaps are always loaded and parsed in memory - the bottleneck isn't the GetPixel
, it's loading the Bitmap
from disk.
It might be worth it to cache the headers and the palette of each of the images, to avoid some of the seeking - I'm not sure if it's going to help at all, but the basic idea is like this:
private static readonly ConcurrentDictionary<string, byte[]> _cache;
Color GetPixel(string fileName, int x, int y)
{
var buffer = new byte[3];
using (var file = File.OpenRead(fileName))
{
byte[] headers;
if (_cache.ContainsKey(fileName))
{
headers = _cache[fileName];
}
else
{
headers = new byte[1078];
if (file.Read(headers, 0, headers.Length) < headers.Length) return Color.Empty;
_cache.TryAdd(fileName, headers);
}
// Now read the headers as before, using the headers local instead of buffer
// ...
file.Seek(offset + ((rowSize * (height - y - 1)) + x), SeekOrigin.Begin);
if (file.Read(buffer, 0, 1) < 1) return Color.Empty;
var color = BitConverter.ToInt32(headers, 54 + buffer[0] * 4);
return Color.FromArgb(color);
}
}