I was searching for a long time how to write multi-page TIFFs with the JPEG encoding. TIFFs support JPEG encoded frames but the built-in encoder in .NET Framework does not have JPEG as a compression option.
The code is based on the answer to this question: https://stackoverflow.com/questions/14811496/tiff-with-jpeg-compression-much-larger-than-original-jpeg but does not rely on the FreeImage library.
First up, a class to convert BitmapFrame
or Bitmap
to a JPEG image:
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Windows.Media.Imaging;
namespace TIFF
{
public class Jpeg
{
public byte[] Data;
public uint Width;
public uint Height;
public uint HorizontalResolution;
public uint VerticalResolution;
public Jpeg(byte[] data, uint width, uint height, uint horizontalResolution, uint verticalResolution)
{
this.Data = data;
this.Width = width;
this.Height = height;
this.HorizontalResolution = horizontalResolution;
this.VerticalResolution = verticalResolution;
}
public static Jpeg FromBitmapFrame(BitmapFrame bitmap, long quality)
{
Jpeg jpeg;
using (var stream = new MemoryStream())
{
JpegBitmapEncoder encoder = new JpegBitmapEncoder();
encoder.QualityLevel = 90;
encoder.Frames.Add(bitmap);
encoder.Save(stream);
jpeg = new Jpeg(stream.ToArray(), (uint)bitmap.Width, (uint)bitmap.Height, (uint)bitmap.DpiX, (uint)bitmap.DpiY);
}
return jpeg;
}
public static Jpeg FromBitmap(Bitmap bitmap, long quality)
{
Jpeg jpeg;
using (var stream = new MemoryStream())
{
ImageCodecInfo jpgEncoder = GetEncoder(ImageFormat.Jpeg);
Encoder encoder = Encoder.Quality;
EncoderParameters parameters = new EncoderParameters(1);
parameters.Param[0] = new EncoderParameter(encoder, quality);
bitmap.Save(stream, jpgEncoder, parameters);
jpeg = new Jpeg(stream.ToArray(), (uint) bitmap.Width, (uint) bitmap.Height, (uint) bitmap.HorizontalResolution, (uint) bitmap.VerticalResolution);
}
return jpeg;
}
private static ImageCodecInfo GetEncoder(ImageFormat format)
{
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
foreach (ImageCodecInfo codec in codecs)
{
if (codec.FormatID == format.Guid)
{
return codec;
}
}
return null;
}
}
}
Next, a class to create the TIFF image
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Windows.Media.Imaging;
namespace TIFF
{
public static class JpegTiff
{
public static byte[] Create(List<BitmapFrame> frames, long quality)
{
List<Jpeg> jpegs = new List<Jpeg>();
foreach (var frame in frames)
{
jpegs.Add(Jpeg.FromBitmapFrame(frame, quality));
}
return WrapJpegs(jpegs);
}
public static byte[] Create(List<Bitmap> bitmaps, string filename, long quality)
{
List<Jpeg> jpegs = new List<Jpeg>();
foreach (var bitmap in bitmaps)
{
jpegs.Add(Jpeg.FromBitmap(bitmap, quality));
}
return WrapJpegs(jpegs);
}
private static byte[] WrapJpegs(List<Jpeg> jpegs)
{
if (jpegs == null || jpegs.Count == 0 || jpegs.FindIndex(b => b.Data.Length == 0) > -1)
throw new ArgumentNullException("Image Data must not be null or empty");
MemoryStream tiffData = new MemoryStream();
BinaryWriter writer = new BinaryWriter(tiffData);
uint offset = 8; // size of header, offset to IFD
ushort entryCount = 14; // entries per IFD
#region IFH - Image file header
// magic number
if (BitConverter.IsLittleEndian)
writer.Write(0x002A4949);
else
writer.Write(0x4D4D002A);
// offset to (first) IFD
writer.Write(offset);
#endregion IFH
#region IFD Image file directory
// write image file directories for each jpeg
for (int i = 0; offset > 0; i++)
{
var jpeg = jpegs[i];
uint width = jpeg.Width;
uint length = jpeg.Height;
uint xres = jpeg.HorizontalResolution;
uint yres = jpeg.VerticalResolution;
// count of entries:
writer.Write(entryCount);
offset += 6 + 12 * (uint)entryCount; // add lengths of entries, entry-count and next-ifd-offset
// TIFF-fields / IFD-entrys:
// {TAG, TYPE (3 = short, 4 = long, 5 = rational), COUNT, VALUE/OFFSET}
uint[,] fields = new uint[,] {
{254, 4, 1, 0}, // NewSubfileType
{256, 4, 1, width}, // ImageWidth
{257, 4, 1, length}, // ImageLength
{258, 3, 3, offset}, // BitsPerSample
{259, 3, 1, 7}, // Compression (new JPEG)
{262, 3, 1, 6}, //PhotometricInterpretation (YCbCr)
{273, 4, 1, offset + 22}, // StripOffsets (offset IFH + entries + values of BitsPerSample & YResolution & XResolution)
{277, 3, 1, 3}, // SamplesPerPixel
{278, 4, 1, length}, // RowsPerStrip
{279, 4, 1, (uint)jpegs[i].Data.LongLength}, // StripByteCounts
{282, 5, 1, offset + 6}, // XResolution (offset IFH + entries + values of BitsPerSample)
{283, 5, 1, offset + 14}, // YResolution (offset IFH + entries + values of BitsPerSample & YResolution)
{284, 3, 1, 1}, // PlanarConfiguration (chunky)
{296, 3, 1, 2} // ResolutionUnit
};
// write fields
for (int f = 0; f < fields.GetLength(0); f++)
{
writer.Write((ushort)fields[f, 0]);
writer.Write((ushort)fields[f, 1]);
writer.Write(fields[f, 2]);
writer.Write(fields[f, 3]);
}
// offset of next IFD
if (i == jpegs.Count - 1)
offset = 0;
else
offset += 22 + (uint)jpegs[i].Data.LongLength; // add values (of fields) length and jpeg length
writer.Write(offset);
#region values of fields
// BitsPerSample
writer.Write((ushort)8);
writer.Write((ushort)8);
writer.Write((ushort)8);
// XResolution
writer.Write(xres);
writer.Write(1);
// YResolution
writer.Write(yres);
writer.Write(1);
#endregion values of fields
// actual image Data
writer.Write(jpegs[i].Data);
}
#endregion IFD
writer.Close();
return tiffData.ToArray();
}
}
}
It could probably be improved by passing in the stream to write to instead of returning a byte array.