3
\$\begingroup\$

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.

\$\endgroup\$
5
\$\begingroup\$
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;
}

Here it should be safe to return from inside the using statement:

public static Jpeg FromBitmapFrame(BitmapFrame bitmap, long quality)
{
  using (var stream = new MemoryStream())
  {
    JpegBitmapEncoder encoder = new JpegBitmapEncoder();
    encoder.QualityLevel = 90;
    encoder.Frames.Add(bitmap);
    encoder.Save(stream);
    return new Jpeg(stream.ToArray(), (uint)bitmap.Width, (uint)bitmap.Height, (uint)bitmap.DpiX, (uint)bitmap.DpiY);
  }
}

    private static ImageCodecInfo GetEncoder(ImageFormat format)
    {
        ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
        foreach (ImageCodecInfo codec in codecs)
        {
            if (codec.FormatID == format.Guid)
            {
                return codec;
            }
        }
        return null;
    }

Using LINQ this can be reduced to a oneliner:

private static ImageCodecInfo GetEncoder(ImageFormat format)
{
  return ImageCodecInfo.GetImageDecoders().FirstOrDefault(codec => codec.FormatID == format.Guid);
}

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

Again LINQ can "modernize" this a little:

public static byte[] Create(IEnumerable<BitmapFrame> frames, long quality)
{
  return WrapJpegs(frames.Select(frame => Jpeg.FromBitmapFrame(frame, quality)).ToList());
}

jpegs.FindIndex(b => b.Data.Length == 0) > -1

LINQ:

jpegs.Any(j => j.Data.Length == 0)

IMO easier to read.


        MemoryStream tiffData = new MemoryStream();
        BinaryWriter writer = new BinaryWriter(tiffData);

You need to wrap these in using statements:

  using (MemoryStream tiffData = new MemoryStream()) // HDH Use using in order to clean up 
  using (BinaryWriter writer = new BinaryWriter(tiffData))
  {
     ...
    writer.Flush();
    return tiffData.ToArray();
  }

    for (int i = 0; offset > 0; i++)
      {

The stop condition is confusing. Why not just use jpegs.Count because you actually iterate through all items anyway.


In the main loop you declare this:

var jpeg = jpegs[i];

But you use jpegs[i] several times in the loop. Be consistent.


      uint[,] fields = new uint[,] {
                {254, 4, 1, 0}, // NewSubfileType
                {256, 4, 1, width}, // ImageWidth

I think I would make a struct or class for these fields in order to be strict with types - hence avoiding the casting.


      writer.Write((ushort)8);
      writer.Write((ushort)8);
      writer.Write((ushort)8);

make a const ushort bitsPerSample = 8; for this - before the loop.


The idea of having a stream as argument to the methods is good, but be aware that BinaryWriter disposes the stream, when it is disposed unless you use the constructor with the leaveOpen flag.

\$\endgroup\$
  • \$\begingroup\$ Nice stuff with the LINQ, looks great. BTW is it better to edit the original question with these changes or post another answer? \$\endgroup\$ – geometrikal Mar 6 at 11:00
  • \$\begingroup\$ @geometrikal: You're not allowed to change the question once an answer has been posted. Feel free to make an answer with the changes or post a new question with an updated version :-) \$\endgroup\$ – Henrik Hansen Mar 6 at 11:01
4
\$\begingroup\$

Henrik covered a number of points which I would have raised, so I won't repeat those.


namespace TIFF
{
    public class Jpeg

seems inconsistent to me. The convention in .Net is to camel-case acronyms and initialisms, so the class name is as expected and the namespace is not.


    public static class JpegTiff
    {
        public static byte[] Create(List<BitmapFrame> frames, long quality)
        ...
        public static byte[] Create(List<Bitmap> bitmaps, string filename, long quality)
        ...
        private static byte[] WrapJpegs(List<Jpeg> jpegs)
        ...

Since Jpeg is a public class it seems to me that you could rename WrapJpegs to Create and make it public. That opens up the option, for example, of encoding different frames at different qualities. (I'd also change List to IEnumerable, as Henrik proposes for the existing Create methods).


There are some worrying magic numbers. Some of these concerns might be alleviated by a comment with a URL for the file format specification.

            uint offset = 8; // size of header, offset to IFD

Is this 4 for the endianness magic number and 4 for offset itself?

            ushort entryCount = 14; // entries per IFD

This is fields.GetLength(0), isn't it? Is there any reason that you can't explicitly use fields.GetLength(0) for robustness if you later add or remove a field?

                // TIFF-fields / IFD-entrys:
                // {TAG, TYPE (3 = short, 4 = long, 5 = rational), COUNT, VALUE/OFFSET}
                uint[,] fields = ...

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

I find it very confusing that a short and a long should take the same amount of space.

                if (i == jpegs.Count - 1)
                    offset = 0;
                else
                    offset += 22 + (uint)jpegs[i].Data.LongLength; // add values (of fields) length and jpeg length

22? Three shorts and four ints?

One more explicit (although less portable, I admit) approach to these lengths would be to use structs for the chunks of fields and Marshal.SizeOf. But I understand if you think that's overkill.

\$\endgroup\$
  • \$\begingroup\$ Some good points. Most of the WrapJpegs code comes from the linked SO question, so I'm still trying to work it out. I would be interested in what using a struct for the chunks of fields looks like \$\endgroup\$ – geometrikal Mar 6 at 23:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.