#region PDFsharp - A .NET library for processing PDF
//
// Authors:
//   Stefan Lange (mailto:Stefan.Lange@pdfsharp.com)
//   Thomas Hövel (mailto:Thomas.Hoevel@pdfsharp.com)
//
// Copyright (c) 2005-2009 empira Software GmbH, Cologne (Germany)
//
// http://www.pdfsharp.com
// http://sourceforge.net/projects/pdfsharp
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
// DEALINGS IN THE SOFTWARE.
#endregion

using System;
using System.Diagnostics;
using System.Collections;
using System.Text;
using System.IO;
#if GDI
using System.Drawing;
using System.Drawing.Imaging;
#endif
#if WPF
using System.Windows.Media;
using System.Windows.Media.Imaging;
#endif
using PdfSharp.Drawing;
using PdfSharp.Fonts.OpenType;
using PdfSharp.Internal;
using PdfSharp.Pdf.Filters;

namespace PdfSharp.Pdf.Advanced
{
  /// <summary>
  /// Represents an image.
  /// </summary>
  public sealed partial class PdfImage : PdfXObject
  {
    /// <summary>
    /// Initializes a new instance of PdfImage from an XImage.
    /// </summary>
    public PdfImage(PdfDocument document, XImage image)
      : base(document)
    {
      Elements.SetName(Keys.Type, "/XObject");
      Elements.SetName(Keys.Subtype, "/Image");

      this.image = image;

      ////// TODO: identify multiple used images. If the image already exists use the same XRef.
      ////this.defaultName = PdfImageTable.NextImageName;

      switch (this.image.Format.Guid.ToString("B").ToUpper())
      {
        // Pdf supports Jpeg, therefore we can write what we've read:
        case "{B96B3CAE-0728-11D3-9D7B-0000F81EF32E}":  //XImageFormat.Jpeg
          InitializeJpeg();
          break;

        // All other image formats are converted to PDF bitmaps:
        case "{B96B3CAF-0728-11D3-9D7B-0000F81EF32E}":  //XImageFormat.Png
        case "{B96B3CB0-0728-11D3-9D7B-0000F81EF32E}":  //XImageFormat.Gif
        case "{B96B3CB1-0728-11D3-9D7B-0000F81EF32E}":  //XImageFormat.Tiff
        case "{B96B3CB5-0728-11D3-9D7B-0000F81EF32E}":  //XImageFormat.Icon
          // TODO: possible optimization for PNG (do not decompress/recompress)???
          // TODO: try Jpeg for size optimization???
          InitializeNonJpeg();
          break;

        case "{84570158-DBF0-4C6B-8368-62D6A3CA76E0}":  //XImageFormat.Pdf:
          Debug.Assert(false, "XPdfForm not expected here.");
          break;

        default:
          Debug.Assert(false, "Unexpected image type.");
          break;
      }
    }

    /// <summary>
    /// Gets the underlying XImage object.
    /// </summary>
    public XImage Image
    {
      get { return image; }
    }

    readonly XImage image;

    /// <summary>
    /// Returns 'Image'.
    /// </summary>
    public override string ToString()
    {
      return "Image";
    }

    /// <summary>
    /// Creates the keys for a JPEG image.
    /// </summary>
    void InitializeJpeg()
    {
      // PDF support JPEG, so there's not much to be done
      MemoryStream memory;
      byte[] imageBits = null;
      int streamLength = 0;
#if GDI
      memory = new MemoryStream();
      image.gdiImage.Save(memory, ImageFormat.Jpeg);
      if ((int)memory.Length == 0)
      {
        Debug.Assert(false, "Internal error? JPEG image, but file not found!");
      }
#endif
#if WPF && !SILVERLIGHT
      // AGHACK
      //string filename = XImage.GetImageFilename(image.wpfImage);
      //if (XImage.ReadJpegFile(filename, -1, ref imageBits))
      //{
      //  streamLength = imageBits.Length;
      //}
      //else
      //  imageBits = null;
      memory = image.Memory;
#endif
      if (imageBits == null)
      {
        streamLength = (int)memory.Length;
        imageBits = new byte[streamLength];
        memory.Seek(0, SeekOrigin.Begin);
        memory.Read(imageBits, 0, streamLength);
        memory.Close();
      }

      FlateDecode fd = new FlateDecode();
      byte[] imageDataCompressed = fd.Encode(imageBits);
      if (imageDataCompressed.Length < imageBits.Length)
      {
        Stream = new PdfStream(imageDataCompressed, this);
        Elements[Keys.Length] = new PdfInteger(imageDataCompressed.Length);
        PdfArray arrayFilters = new PdfArray(document);
        arrayFilters.Elements.Add(new PdfName("/FlateDecode"));
        arrayFilters.Elements.Add(new PdfName("/DCTDecode"));
        Elements[Keys.Filter] = arrayFilters;
      }
      else
      {
        Stream = new PdfStream(imageBits, this);
        Elements[Keys.Length] = new PdfInteger(streamLength);
        Elements[Keys.Filter] = new PdfName("/DCTDecode");
      }
      Elements[Keys.Width] = new PdfInteger(image.PixelWidth);
      Elements[Keys.Height] = new PdfInteger(image.PixelHeight);
      Elements[Keys.BitsPerComponent] = new PdfInteger(8);
#if GDI
      if ((image.gdiImage.Flags & ((int)ImageFlags.ColorSpaceCmyk | (int)ImageFlags.ColorSpaceYcck)) != 0)
      {
        // TODO: Test with CMYK JPEG files
        // THHO: I only found ImageFlags.ColorSpaceYcck JPEG files ...
        Elements[Keys.ColorSpace] = new PdfName("/DeviceCMYK");
        if ((image.gdiImage.Flags & (int)ImageFlags.ColorSpaceYcck) != 0)
          Elements["/Decode"] = new PdfLiteral("[1 0 1 0 1 0 1 0]");  // Invert colors? Why??
      }
      else if ((image.gdiImage.Flags & (int)ImageFlags.ColorSpaceGray) != 0)
      {
        Elements[Keys.ColorSpace] = new PdfName("/DeviceGray");
      }
      else
      {
        Elements[Keys.ColorSpace] = new PdfName("/DeviceRGB");
      }
#endif
#if WPF
      // TODOWPF
      // WPFTHHO
#if !SILVERLIGHT
      string pixelFormat = image.wpfImage.Format.ToString();
#else
      string pixelFormat = "xxx";
#endif
      bool isCmyk = image.IsCmyk;
      bool isGrey = pixelFormat == "Gray8";
      if (isCmyk)
      {
        // TODO: Test with CMYK JPEG files
        // THHO: I only found ImageFlags.ColorSpaceYcck JPEG files ...
        Elements[Keys.ColorSpace] = new PdfName("/DeviceCMYK");
        Elements["/Decode"] = new PdfLiteral("[1 0 1 0 1 0 1 0]");  // Invert colors? Why??
      }
      else if (isGrey)
      {
        Elements[Keys.ColorSpace] = new PdfName("/DeviceGray");
      }
      else
      {
        Elements[Keys.ColorSpace] = new PdfName("/DeviceRGB");
      }
#endif
    }

    /// <summary>
    /// Creates the keys for a FLATE image.
    /// </summary>
    void InitializeNonJpeg()
    {
#if GDI && !WPF
      //bool hasMask = false;
      switch (image.gdiImage.PixelFormat)
      {
        case PixelFormat.Format24bppRgb:
          ReadTrueColorMemoryBitmap(3, 8, false);
          break;

        case PixelFormat.Format32bppRgb:
          ReadTrueColorMemoryBitmap(4, 8, false);
          break;

        case PixelFormat.Format32bppArgb:
        case PixelFormat.Format32bppPArgb:
          //hasMask = true;
          ReadTrueColorMemoryBitmap(3, 8, true);
          break;

        case PixelFormat.Format8bppIndexed:
          ReadIndexedMemoryBitmap(8/*, ref hasMask*/);
          break;

        case PixelFormat.Format4bppIndexed:
          ReadIndexedMemoryBitmap(4/*, ref hasMask*/);
          break;

        case PixelFormat.Format1bppIndexed:
          ReadIndexedMemoryBitmap(1/*, ref hasMask*/);
          break;

        default:
#if DEBUGxxx
          image.image.Save("$$$.bmp", ImageFormat.Bmp);
#endif
          throw new NotImplementedException("Image format not supported.");
      }
#endif
#if WPF // && !GDI
      // WPFTHHO: Bitte prüfen, siehe System.Windows.Media.PixelFormats
      //bool hasMask = false;
#if !SILVERLIGHT
      string format = image.wpfImage.Format.ToString();
#else
      string format = "Bgr24";
#endif
      switch (format)
      {
        case "Bgr24": //Format24bppRgb:
          ReadTrueColorMemoryBitmap(3, 8, false);
          break;

        //case .PixelFormat.Format32bppRgb:
        //  ReadTrueColorMemoryBitmap(4, 8, false);
        //  break;

        case "Bgra32":  //PixelFormat.Format32bppArgb:
          //case PixelFormat.Format32bppPArgb:
          //hasMask = true;
          ReadTrueColorMemoryBitmap(3, 8, true);
          break;

        case "Bgr32": // StL: neu
          //hasMask = false;
          ReadTrueColorMemoryBitmap(3, 8, true);
          break;

        case "Pbgra32": // StL: neu
          //hasMask = false;
          ReadTrueColorMemoryBitmap(3, 8, true);
          break;

        case "Indexed8":  //Format8bppIndexed:
        case "Gray8":
          ReadIndexedMemoryBitmap(8/*, ref hasMask*/);
          break;

        case "Indexed4":  //Format4bppIndexed:
        case "Gray4":
          ReadIndexedMemoryBitmap(4/*, ref hasMask*/);
          break;

        case "Indexed2":
          ReadIndexedMemoryBitmap(2/*, ref hasMask*/);
          break;

        case "Indexed1":  //Format1bppIndexed:
        case "BlackWhite":  //Format1bppIndexed:
          ReadIndexedMemoryBitmap(1/*, ref hasMask*/);
          break;

        default:
#if DEBUGxxx
          image.image.Save("$$$.bmp", ImageFormat.Bmp);
#endif
          throw new NotImplementedException("Image format \"" + format + "\" not supported.");
      }
#endif
    }

    private static int ReadWord(byte[] ab, int offset)
    {
      return ab[offset] + 256 * ab[offset + 1];
    }

    private static int ReadDWord(byte[] ab, int offset)
    {
      return ReadWord(ab, offset) + 0x10000 * ReadWord(ab, offset + 2);
    }

    /// <summary>
    /// Reads images that are returned from GDI+ without color palette.
    /// </summary>
    /// <param name="components">4 (32bpp RGB), 3 (24bpp RGB, 32bpp ARGB)</param>
    /// <param name="bits">8</param>
    /// <param name="hasAlpha">true (ARGB), false (RGB)</param>
    private void ReadTrueColorMemoryBitmap(int components, int bits, bool hasAlpha)
    {
#if DEBUG_
      image.image.Save("$$$.bmp", ImageFormat.Bmp);
#endif
      int pdfVersion = Owner.Version;
      MemoryStream memory = new MemoryStream();
#if GDI
      image.gdiImage.Save(memory, ImageFormat.Bmp);
#endif
#if WPF
#if !SILVERLIGHT
      // WPFTHHO: Bitte prüfen
      BmpBitmapEncoder encoder = new BmpBitmapEncoder();
      encoder.Frames.Add(BitmapFrame.Create(image.wpfImage));
      encoder.Save(memory);
#else
      // AGHACK
#endif
#endif
      int streamLength = (int)memory.Length;
      Debug.Assert(streamLength > 0, "Bitmap image encoding failed.");
      if (streamLength > 0)
      {
        byte[] imageBits = new byte[streamLength];
        memory.Seek(0, SeekOrigin.Begin);
        memory.Read(imageBits, 0, streamLength);
        memory.Close();

        int height = image.PixelHeight;
        int width = image.PixelWidth;

        // TODO: we could define structures for
        //   BITMAPFILEHEADER
        //   { BITMAPINFO }
        //   BITMAPINFOHEADER
        // to avoid ReadWord and ReadDWord ... (but w/o pointers this doesn't help much)

        if (ReadWord(imageBits, 0) != 0x4d42 || // "BM"
            ReadDWord(imageBits, 2) != streamLength ||
            ReadDWord(imageBits, 14) != 40 || // sizeof BITMAPINFOHEADER
            ReadDWord(imageBits, 18) != width ||
            ReadDWord(imageBits, 22) != height)
        {
          throw new NotImplementedException("ReadTrueColorMemoryBitmap: unsupported format");
        }
        if (ReadWord(imageBits, 26) != 1 ||
          (!hasAlpha && ReadWord(imageBits, 28) != components * bits ||
           hasAlpha && ReadWord(imageBits, 28) != (components + 1) * bits) ||
          ReadDWord(imageBits, 30) != 0)
        {
          throw new NotImplementedException("ReadTrueColorMemoryBitmap: unsupported format #2");
        }

        int nFileOffset = ReadDWord(imageBits, 10);
        int logicalComponents = components;
        if (components == 4)
          logicalComponents = 3;

        byte[] imageData = new byte[components * width * height];

        bool hasMask = false;
        bool hasAlphaMask = false;
        byte[] alphaMask = hasAlpha ? new byte[width * height] : null;
        MonochromeMask mask = hasAlpha ?
          new MonochromeMask(width, height) : null;

        int nOffsetRead = 0;
        if (logicalComponents == 3)
        {
          for (int y = 0; y < height; ++y)
          {
            int nOffsetWrite = 3 * (height - 1 - y) * width;
            int nOffsetWriteAlpha = 0;
            if (hasAlpha)
            {
              mask.StartLine(y);
              nOffsetWriteAlpha = (height - 1 - y) * width;
            }

            for (int x = 0; x < width; ++x)
            {
              imageData[nOffsetWrite] = imageBits[nFileOffset + nOffsetRead + 2];
              imageData[nOffsetWrite + 1] = imageBits[nFileOffset + nOffsetRead + 1];
              imageData[nOffsetWrite + 2] = imageBits[nFileOffset + nOffsetRead];
              if (hasAlpha)
              {
                mask.AddPel(imageBits[nFileOffset + nOffsetRead + 3]);
                alphaMask[nOffsetWriteAlpha] = imageBits[nFileOffset + nOffsetRead + 3];
                if (!hasMask || !hasAlphaMask)
                {
                  if (imageBits[nFileOffset + nOffsetRead + 3] != 255)
                  {
                    hasMask = true;
                    if (imageBits[nFileOffset + nOffsetRead + 3] != 0)
                      hasAlphaMask = true;
                  }
                }
                ++nOffsetWriteAlpha;
              }
              nOffsetRead += hasAlpha ? 4 : components;
              nOffsetWrite += 3;
            }
            nOffsetRead = 4 * ((nOffsetRead + 3) / 4); // Align to 32 bit boundary
          }
        }
        else if (components == 1)
        {
          // Grayscale
          throw new NotImplementedException("Image format not supported (grayscales).");
        }

        FlateDecode fd = new FlateDecode();
        if (hasMask)
        {
          // monochrome mask is either sufficient or
          // provided for compatibility with older reader versions
          byte[] maskDataCompressed = fd.Encode(mask.MaskData);
          PdfDictionary pdfMask = new PdfDictionary(document);
          pdfMask.Elements.SetName(Keys.Type, "/XObject");
          pdfMask.Elements.SetName(Keys.Subtype, "/Image");

          Owner.irefTable.Add(pdfMask);
          pdfMask.Stream = new PdfStream(maskDataCompressed, pdfMask);
          pdfMask.Elements[Keys.Length] = new PdfInteger(maskDataCompressed.Length);
          pdfMask.Elements[Keys.Filter] = new PdfName("/FlateDecode");
          pdfMask.Elements[Keys.Width] = new PdfInteger(width);
          pdfMask.Elements[Keys.Height] = new PdfInteger(height);
          pdfMask.Elements[Keys.BitsPerComponent] = new PdfInteger(1);
          pdfMask.Elements[Keys.ImageMask] = new PdfBoolean(true);
          Elements[Keys.Mask] = pdfMask.Reference;
        }
        if (hasMask && hasAlphaMask && pdfVersion >= 14)
        {
          // The image provides an alpha mask (requires Arcrobat 5.0 or higher)
          byte[] alphaMaskCompressed = fd.Encode(alphaMask);
          PdfDictionary smask = new PdfDictionary(document);
          smask.Elements.SetName(Keys.Type, "/XObject");
          smask.Elements.SetName(Keys.Subtype, "/Image");

          Owner.irefTable.Add(smask);
          smask.Stream = new PdfStream(alphaMaskCompressed, smask);
          smask.Elements[Keys.Length] = new PdfInteger(alphaMaskCompressed.Length);
          smask.Elements[Keys.Filter] = new PdfName("/FlateDecode");
          smask.Elements[Keys.Width] = new PdfInteger(width);
          smask.Elements[Keys.Height] = new PdfInteger(height);
          smask.Elements[Keys.BitsPerComponent] = new PdfInteger(8);
          smask.Elements[Keys.ColorSpace] = new PdfName("/DeviceGray");
          Elements[Keys.SMask] = smask.Reference;
        }

        byte[] imageDataCompressed = fd.Encode(imageData);

        Stream = new PdfStream(imageDataCompressed, this);
        Elements[Keys.Length] = new PdfInteger(imageDataCompressed.Length);
        Elements[Keys.Filter] = new PdfName("/FlateDecode");
        Elements[Keys.Width] = new PdfInteger(width);
        Elements[Keys.Height] = new PdfInteger(height);
        Elements[Keys.BitsPerComponent] = new PdfInteger(8);
        // TODO: CMYK
        Elements[Keys.ColorSpace] = new PdfName("/DeviceRGB");
        if (image.Interpolate)
          Elements[Keys.Interpolate] = PdfBoolean.True;
      }
    }

    /* BITMAPINFOHEADER struct and byte offsets:
        typedef struct tagBITMAPINFOHEADER{
          DWORD  biSize;           // 14
          LONG   biWidth;          // 18
          LONG   biHeight;         // 22
          WORD   biPlanes;         // 26
          WORD   biBitCount;       // 28
          DWORD  biCompression;    // 30
          DWORD  biSizeImage;      // 34
          LONG   biXPelsPerMeter;  // 38
          LONG   biYPelsPerMeter;  // 42
          DWORD  biClrUsed;        // 46
          DWORD  biClrImportant;   // 50
        } BITMAPINFOHEADER, *PBITMAPINFOHEADER; 
    */

    private void ReadIndexedMemoryBitmap(int bits/*, ref bool hasAlpha*/)
    {
#if DEBUG_
      image.image.Save("$$$.bmp", ImageFormat.Bmp);
#endif
      int pdfVersion = Owner.Version;
      int firstMaskColor = -1, lastMaskColor = -1;
      bool segmentedColorMask = false;

      MemoryStream memory = new MemoryStream();
#if GDI
      image.gdiImage.Save(memory, ImageFormat.Bmp);
#endif
#if WPF
#if !SILVERLIGHT
      // WPFTHHO: StL: keine Ahnung ob das so stimmt.
      BmpBitmapEncoder encoder = new BmpBitmapEncoder();
      encoder.Frames.Add(BitmapFrame.Create(image.wpfImage));
      encoder.Save(memory);
#else
      // AGHACK
#endif
#endif
      int streamLength = (int)memory.Length;
      Debug.Assert(streamLength > 0, "Bitmap image encoding failed.");
      if (streamLength > 0)
      {
        byte[] imageBits = new byte[streamLength];
        memory.Seek(0, SeekOrigin.Begin);
        memory.Read(imageBits, 0, streamLength);
        memory.Close();

        int height = image.PixelHeight;
        int width = image.PixelWidth;

        if (ReadWord(imageBits, 0) != 0x4d42 || // "BM"
          ReadDWord(imageBits, 2) != streamLength ||
          ReadDWord(imageBits, 14) != 40 || // sizeof BITMAPINFOHEADER
#if WPF
          // TODOWPF: bug with height and width??? With which files???
          ReadDWord(imageBits, 18) != width ||
          ReadDWord(imageBits, 22) != height)
#else
          ReadDWord(imageBits, 18) != width ||
          ReadDWord(imageBits, 22) != height)
#endif
        {
          throw new NotImplementedException("ReadIndexedMemoryBitmap: unsupported format");
        }
#if WPF
        // TODOWPF: bug with height and width
        width = ReadDWord(imageBits, 18);
        height = ReadDWord(imageBits, 22);
#endif
        int fileBits = ReadWord(imageBits, 28);
        if (fileBits != bits)
        {
          if (fileBits == 1 || fileBits == 4 || fileBits == 8)
            bits = fileBits;
        }

        if (ReadWord(imageBits, 26) != 1 ||
            ReadWord(imageBits, 28) != bits ||
            ReadDWord(imageBits, 30) != 0)
        {
          throw new NotImplementedException("ReadIndexedMemoryBitmap: unsupported format #2");
        }

        int bytesFileOffset = ReadDWord(imageBits, 10);
        int bytesColorPaletteOffset = 0x36; // GDI+ always returns Windows bitmaps: sizeof BITMAPFILEHEADER + sizeof BITMAPINFOHEADER
        int paletteColors = ReadDWord(imageBits, 46);
        if ((bytesFileOffset - bytesColorPaletteOffset) / 4 != paletteColors)
        {
          throw new NotImplementedException("ReadIndexedMemoryBitmap: unsupported format #3");
        }

        MonochromeMask mask = new MonochromeMask(width, height);

        bool isGray = bits == 8 && (paletteColors == 256 || paletteColors == 0);
        int isBitonal = 0; // 0: false; >0: true; <0: true (inverted)
        byte[] paletteData = new byte[3 * paletteColors];
        for (int color = 0; color < paletteColors; ++color)
        {
          paletteData[3 * color] = imageBits[bytesColorPaletteOffset + 4 * color + 2];
          paletteData[3 * color + 1] = imageBits[bytesColorPaletteOffset + 4 * color + 1];
          paletteData[3 * color + 2] = imageBits[bytesColorPaletteOffset + 4 * color + 0];
          if (isGray)
            isGray = paletteData[3 * color] == paletteData[3 * color + 1] &&
              paletteData[3 * color] == paletteData[3 * color + 2];

          if (imageBits[bytesColorPaletteOffset + 4 * color + 3] < 128)
          {
            // We treat this as transparency:
            if (firstMaskColor == -1)
              firstMaskColor = color;
            if (lastMaskColor == -1 || lastMaskColor == color - 1)
              lastMaskColor = color;
            if (lastMaskColor != color)
              segmentedColorMask = true;
          }
          //else
          //{
          //  // We treat this as opacity:
          //}
        }

        if (bits == 1)
        {
          if (paletteColors == 0)
            isBitonal = 1;
          if (paletteColors == 2)
          {
            if (paletteData[0] == 0 &&
              paletteData[1] == 0 &&
              paletteData[2] == 0 &&
              paletteData[3] == 255 &&
              paletteData[4] == 255 &&
              paletteData[5] == 255)
              isBitonal = 1; // Black on white
            if (paletteData[5] == 0 &&
              paletteData[4] == 0 &&
              paletteData[3] == 0 &&
              paletteData[2] == 255 &&
              paletteData[1] == 255 &&
              paletteData[0] == 255)
              isBitonal = -1; // White on black
          }
        }

        // NYI: (no sample found where this was required) 
        // if (segmentedColorMask = true)
        // { ... }

        FlateDecode fd = new FlateDecode();
        PdfDictionary colorPalette = null;
        if (isBitonal == 0 && !isGray)
        {
          colorPalette = new PdfDictionary(this.document);
          byte[] packedPaletteData = paletteData.Length >= 48 ? fd.Encode(paletteData) : null; // don't compress small palettes
          if (packedPaletteData != null && packedPaletteData.Length + 20 < paletteData.Length) // +20: compensate for the overhead (estimated value)
          {
            // Create compressed color palette:
            colorPalette.CreateStream(packedPaletteData);
            colorPalette.Elements[Keys.Length] = new PdfInteger(packedPaletteData.Length);
            colorPalette.Elements[Keys.Filter] = new PdfName("/FlateDecode");
          }
          else
          {
            // Create uncompressed color palette:
            colorPalette.CreateStream(paletteData);
            colorPalette.Elements[Keys.Length] = new PdfInteger(paletteData.Length);
          }
          Owner.irefTable.Add(colorPalette);
        }

        bool isFaxEncoding = false;
        byte[] imageData = new byte[((width * bits + 7) / 8) * height];
        byte[] imageDataFax = null;
        int k = 0;


        if (bits == 1)
        {
          // TODO: flag/option?
          // We try Group 3 1D and Group 4 (2D) encoding here and keep the smaller byte array.
          //byte[] temp = new byte[imageData.Length];
          //int ccittSize = DoFaxEncoding(ref temp, imageBits, (uint)bytesFileOffset, (uint)width, (uint)height);

          // It seems that Group 3 2D encoding never beats both other encodings, therefore we don't call it here.
          //byte[] temp2D = new byte[imageData.Length];
          //uint dpiY = (uint)image.VerticalResolution;
          //uint kTmp = 0;
          //int ccittSize2D = DoFaxEncoding2D((uint)bytesFileOffset, ref temp2D, imageBits, (uint)width, (uint)height, dpiY, out kTmp);
          //k = (int) kTmp;

          byte[] tempG4 = new byte[imageData.Length];
          int ccittSizeG4 = DoFaxEncodingGroup4(ref tempG4, imageBits, (uint)bytesFileOffset, (uint)width, (uint)height);

          isFaxEncoding = /*ccittSize > 0 ||*/ ccittSizeG4 > 0;
          if (isFaxEncoding)
          {
            //if (ccittSize == 0)
            //  ccittSize = 0x7fffffff;
            if (ccittSizeG4 == 0)
              ccittSizeG4 = 0x7fffffff;
            //if (ccittSize <= ccittSizeG4)
            //{
            //  Array.Resize(ref temp, ccittSize);
            //  imageDataFax = temp;
            //  k = 0;
            //}
            //else
            {
              Array.Resize(ref tempG4, ccittSizeG4);
              imageDataFax = tempG4;
              k = -1;
            }
          }
        }

        //if (!isFaxEncoding)
        {
          int bytesOffsetRead = 0;
          if (bits == 8 || bits == 4 || bits == 1)
          {
            int bytesPerLine = (width * bits + 7) / 8;
            for (int y = 0; y < height; ++y)
            {
              mask.StartLine(y);
              int bytesOffsetWrite = (height - 1 - y) * ((width * bits + 7) / 8);
              for (int x = 0; x < bytesPerLine; ++x)
              {
                if (isGray)
                {
                  // Lookup the gray value from the palette:
                  imageData[bytesOffsetWrite] = paletteData[3 * imageBits[bytesFileOffset + bytesOffsetRead]];
                }
                else
                {
                  // Store the palette index:
                  imageData[bytesOffsetWrite] = imageBits[bytesFileOffset + bytesOffsetRead];
                }
                if (firstMaskColor != -1)
                {
                  int n = imageBits[bytesFileOffset + bytesOffsetRead];
                  if (bits == 8)
                  {
                    // TODO???: segmentedColorMask == true => bad mask NYI
                    mask.AddPel((n >= firstMaskColor) && (n <= lastMaskColor));
                  }
                  else if (bits == 4)
                  {
                    // TODO???: segmentedColorMask == true => bad mask NYI
                    int n1 = (n & 0xf0) / 16;
                    int n2 = (n & 0x0f);
                    mask.AddPel((n1 >= firstMaskColor) && (n1 <= lastMaskColor));
                    mask.AddPel((n2 >= firstMaskColor) && (n2 <= lastMaskColor));
                  }
                  else if (bits == 1)
                  {
                    // TODO???: segmentedColorMask == true => bad mask NYI
                    for (int bit = 1; bit <= 8; ++bit)
                    {
                      int n1 = (n & 0x80) / 128;
                      mask.AddPel((n1 >= firstMaskColor) && (n1 <= lastMaskColor));
                      n *= 2;
                    }
                  }
                }
                bytesOffsetRead += 1;
                bytesOffsetWrite += 1;
              }
              bytesOffsetRead = 4 * ((bytesOffsetRead + 3) / 4); // Align to 32 bit boundary
            }
          }
          else
          {
            throw new NotImplementedException("ReadIndexedMemoryBitmap: unsupported format #3");
          }
        }

        if (firstMaskColor != -1 &&
          lastMaskColor != -1)
        {
          // Color mask requires Reader 4.0 or higher:
          if (!segmentedColorMask && pdfVersion >= 13)
          {
            PdfArray array = new PdfArray(document);
            array.Elements.Add(new PdfInteger(firstMaskColor));
            array.Elements.Add(new PdfInteger(lastMaskColor));
            Elements[Keys.Mask] = array;
          }
          else
          {
            // Monochrome mask
            byte[] maskDataCompressed = fd.Encode(mask.MaskData);
            PdfDictionary pdfMask = new PdfDictionary(document);
            pdfMask.Elements.SetName(Keys.Type, "/XObject");
            pdfMask.Elements.SetName(Keys.Subtype, "/Image");

            this.Owner.irefTable.Add(pdfMask);
            pdfMask.Stream = new PdfStream(maskDataCompressed, pdfMask);
            pdfMask.Elements[Keys.Length] = new PdfInteger(maskDataCompressed.Length);
            pdfMask.Elements[Keys.Filter] = new PdfName("/FlateDecode");
            pdfMask.Elements[Keys.Width] = new PdfInteger(width);
            pdfMask.Elements[Keys.Height] = new PdfInteger(height);
            pdfMask.Elements[Keys.BitsPerComponent] = new PdfInteger(1);
            pdfMask.Elements[Keys.ImageMask] = new PdfBoolean(true);
            Elements[Keys.Mask] = pdfMask.Reference;
          }
        }

        byte[] imageDataCompressed = fd.Encode(imageData);
        byte[] imageDataFaxCompressed = isFaxEncoding ? fd.Encode(imageDataFax) : null;

        bool usesCcittEncoding = false;
        if (isFaxEncoding &&
          (imageDataFax.Length < imageDataCompressed.Length ||
          imageDataFaxCompressed.Length < imageDataCompressed.Length))
        {
          // /CCITTFaxDecode creates the smaller file (with or without /FlateDecode):
          usesCcittEncoding = true;

          if (imageDataFax.Length < imageDataCompressed.Length)
          {
            Stream = new PdfStream(imageDataFax, this);
            Elements[Keys.Length] = new PdfInteger(imageDataFax.Length);
            Elements[Keys.Filter] = new PdfName("/CCITTFaxDecode");
            //PdfArray array2 = new PdfArray(this.document);
            PdfDictionary dictionary = new PdfDictionary();
            if (k != 0)
              dictionary.Elements.Add("/K", new PdfInteger(k));
            if (isBitonal < 0)
              dictionary.Elements.Add("/BlackIs1", new PdfBoolean(true));
            dictionary.Elements.Add("/EndOfBlock", new PdfBoolean(false));
            dictionary.Elements.Add("/Columns", new PdfInteger(width));
            dictionary.Elements.Add("/Rows", new PdfInteger(height));
            //array2.Elements.Add(dictionary);
            Elements[Keys.DecodeParms] = dictionary; // array2;
          }
          else
          {
            Stream = new PdfStream(imageDataFaxCompressed, this);
            Elements[Keys.Length] = new PdfInteger(imageDataFaxCompressed.Length);
            PdfArray arrayFilters = new PdfArray(document);
            arrayFilters.Elements.Add(new PdfName("/FlateDecode"));
            arrayFilters.Elements.Add(new PdfName("/CCITTFaxDecode"));
            Elements[Keys.Filter] = arrayFilters;
            PdfArray arrayDecodeParms = new PdfArray(document);

            PdfDictionary dictFlateDecodeParms = new PdfDictionary();
            //dictFlateDecodeParms.Elements.Add("/Columns", new PdfInteger(1));

            PdfDictionary dictCcittFaxDecodeParms = new PdfDictionary();
            if (k != 0)
              dictCcittFaxDecodeParms.Elements.Add("/K", new PdfInteger(k));
            if (isBitonal < 0)
              dictCcittFaxDecodeParms.Elements.Add("/BlackIs1", new PdfBoolean(true));
            dictCcittFaxDecodeParms.Elements.Add("/EndOfBlock", new PdfBoolean(false));
            dictCcittFaxDecodeParms.Elements.Add("/Columns", new PdfInteger(width));
            dictCcittFaxDecodeParms.Elements.Add("/Rows", new PdfInteger(height));

            arrayDecodeParms.Elements.Add(dictFlateDecodeParms); // How to add the "null object"?
            arrayDecodeParms.Elements.Add(dictCcittFaxDecodeParms);
            Elements[Keys.DecodeParms] = arrayDecodeParms;
          }
        }
        else
        {
          // /FlateDecode creates the smaller file (or no monochrome bitmap):
          Stream = new PdfStream(imageDataCompressed, this);
          Elements[Keys.Length] = new PdfInteger(imageDataCompressed.Length);
          Elements[Keys.Filter] = new PdfName("/FlateDecode");
        }

        Elements[Keys.Width] = new PdfInteger(width);
        Elements[Keys.Height] = new PdfInteger(height);
        Elements[Keys.BitsPerComponent] = new PdfInteger(bits);
        // TODO: CMYK

        // CCITT encoding: we need color palette for isBitonal == 0
        // FlateDecode: we need color palette for isBitonal <= 0 unless we have grayscales
        if ((usesCcittEncoding && isBitonal == 0) ||
          (!usesCcittEncoding && isBitonal <= 0  && !isGray))
        {
          PdfArray arrayColorSpace = new PdfArray(document);
          arrayColorSpace.Elements.Add(new PdfName("/Indexed"));
          arrayColorSpace.Elements.Add(new PdfName("/DeviceRGB"));
          arrayColorSpace.Elements.Add(new PdfInteger(paletteColors - 1));
          // ReSharper disable PossibleNullReferenceException
          arrayColorSpace.Elements.Add(colorPalette.Reference);
          // ReSharper restore PossibleNullReferenceException
          Elements[Keys.ColorSpace] = arrayColorSpace;
        }
        else
        {
          Elements[Keys.ColorSpace] = new PdfName("/DeviceGray");
        }
        if (image.Interpolate)
          Elements[Keys.Interpolate] = PdfBoolean.True;
      }
    }

    /// <summary>
    /// Common keys for all streams.
    /// </summary>
    public sealed new class Keys : PdfXObject.Keys
    {
      /// <summary>
      /// (Optional) The type of PDF object that this dictionary describes;
      /// if present, must be XObject for an image XObject.
      /// </summary>
      [KeyInfo(KeyType.Name | KeyType.Optional)]
      public const string Type = "/Type";

      /// <summary>
      /// (Required) The type of XObject that this dictionary describes;
      /// must be Image for an image XObject.
      /// </summary>
      [KeyInfo(KeyType.Name | KeyType.Required)]
      public const string Subtype = "/Subtype";

      /// <summary>
      /// (Required) The width of the image, in samples.
      /// </summary>
      [KeyInfo(KeyType.Integer | KeyType.Required)]
      public const string Width = "/Width";

      /// <summary>
      /// (Required) The height of the image, in samples.
      /// </summary>
      [KeyInfo(KeyType.Integer | KeyType.Required)]
      public const string Height = "/Height";

      /// <summary>
      /// (Required for images, except those that use the JPXDecode filter; not allowed for image masks)
      /// The color space in which image samples are specified; it can be any type of color space except
      /// Pattern. If the image uses the JPXDecode filter, this entry is optional:
      /// • If ColorSpace is present, any color space specifications in the JPEG2000 data are ignored.
      /// • If ColorSpace is absent, the color space specifications in the JPEG2000 data are used.
      ///   The Decode array is also ignored unless ImageMask is true.
      /// </summary>
      [KeyInfo(KeyType.NameOrArray | KeyType.Required)]
      public const string ColorSpace = "/ColorSpace";

      /// <summary>
      /// (Required except for image masks and images that use the JPXDecode filter)
      /// The number of bits used to represent each color component. Only a single value may be specified;
      /// the number of bits is the same for all color components. Valid values are 1, 2, 4, 8, and 
      /// (in PDF 1.5) 16. If ImageMask is true, this entry is optional, and if specified, its value 
      /// must be 1.
      /// If the image stream uses a filter, the value of BitsPerComponent must be consistent with the 
      /// size of the data samples that the filter delivers. In particular, a CCITTFaxDecode or JBIG2Decode 
      /// filter always delivers 1-bit samples, a RunLengthDecode or DCTDecode filter delivers 8-bit samples,
      /// and an LZWDecode or FlateDecode filter delivers samples of a specified size if a predictor function
      /// is used.
      /// If the image stream uses the JPXDecode filter, this entry is optional and ignored if present.
      /// The bit depth is determined in the process of decoding the JPEG2000 image.
      /// </summary>
      [KeyInfo(KeyType.Integer | KeyType.Required)]
      public const string BitsPerComponent = "/BitsPerComponent";

      /// <summary>
      /// (Optional; PDF 1.1) The name of a color rendering intent to be used in rendering the image.
      /// Default value: the current rendering intent in the graphics state.
      /// </summary>
      [KeyInfo(KeyType.Name | KeyType.Optional)]
      public const string Intent = "/Intent";

      /// <summary>
      /// (Optional) A flag indicating whether the image is to be treated as an image mask.
      /// If this flag is true, the value of BitsPerComponent must be 1 and Mask and ColorSpace should
      /// not be specified; unmasked areas are painted using the current nonstroking color.
      /// Default value: false.
      /// </summary>
      [KeyInfo(KeyType.Boolean | KeyType.Optional)]
      public const string ImageMask = "/ImageMask";

      /// <summary>
      /// (Optional except for image masks; not allowed for image masks; PDF 1.3)
      /// An image XObject defining an image mask to be applied to this image, or an array specifying 
      /// a range of colors to be applied to it as a color key mask. If ImageMask is true, this entry
      /// must not be present.
      /// </summary>
      [KeyInfo(KeyType.StreamOrArray | KeyType.Optional)]
      public const string Mask = "/Mask";

      /// <summary>
      /// (Optional) An array of numbers describing how to map image samples into the range of values
      /// appropriate for the image’s color space. If ImageMask is true, the array must be either
      /// [0 1] or [1 0]; otherwise, its length must be twice the number of color components required 
      /// by ColorSpace. If the image uses the JPXDecode filter and ImageMask is false, Decode is ignored.
      /// Default value: see “Decode Arrays”.
      /// </summary>
      [KeyInfo(KeyType.Array | KeyType.Optional)]
      public const string Decode = "/Decode";

      /// <summary>
      /// (Optional) A flag indicating whether image interpolation is to be performed. 
      /// Default value: false.
      /// </summary>
      [KeyInfo(KeyType.Boolean | KeyType.Optional)]
      public const string Interpolate = "/Interpolate";

      /// <summary>
      /// (Optional; PDF 1.3) An array of alternate image dictionaries for this image. The order of 
      /// elements within the array has no significance. This entry may not be present in an image 
      /// XObject that is itself an alternate image.
      /// </summary>
      [KeyInfo(KeyType.Array | KeyType.Optional)]
      public const string Alternates = "/Alternates";

      /// <summary>
      /// (Optional; PDF 1.4) A subsidiary image XObject defining a soft-mask image to be used as a 
      /// source of mask shape or mask opacity values in the transparent imaging model. The alpha 
      /// source parameter in the graphics state determines whether the mask values are interpreted as
      /// shape or opacity. If present, this entry overrides the current soft mask in the graphics state,
      /// as well as the image’s Mask entry, if any. (However, the other transparency related graphics 
      /// state parameters — blend mode and alpha constant — remain in effect.) If SMask is absent, the 
      /// image has no associated soft mask (although the current soft mask in the graphics state may
      /// still apply).
      /// </summary>
      [KeyInfo(KeyType.Integer | KeyType.Required)]
      public const string SMask = "/SMask";

      /// <summary>
      /// (Optional for images that use the JPXDecode filter, meaningless otherwise; PDF 1.5)
      /// A code specifying how soft-mask information encoded with image samples should be used:
      /// 0 If present, encoded soft-mask image information should be ignored.
      /// 1 The image’s data stream includes encoded soft-mask values. An application can create
      ///   a soft-mask image from the information to be used as a source of mask shape or mask 
      ///   opacity in the transparency imaging model.
      /// 2 The image’s data stream includes color channels that have been preblended with a 
      ///   background; the image data also includes an opacity channel. An application can create
      ///   a soft-mask image with a Matte entry from the opacity channel information to be used as
      ///   a source of mask shape or mask opacity in the transparency model. If this entry has a 
      ///   nonzero value, SMask should not be specified.
      /// Default value: 0.
      /// </summary>
      [KeyInfo(KeyType.Integer | KeyType.Optional)]
      public const string SMaskInData = "/SMaskInData";

      /// <summary>
      /// (Required in PDF 1.0; optional otherwise) The name by which this image XObject is 
      /// referenced in the XObject subdictionary of the current resource dictionary.
      /// </summary>
      [KeyInfo(KeyType.Name | KeyType.Optional)]
      public const string Name = "/Name";

      /// <summary>
      /// (Required if the image is a structural content item; PDF 1.3) The integer key of the 
      /// image’s entry in the structural parent tree.
      /// </summary>
      [KeyInfo(KeyType.Integer | KeyType.Required)]
      public const string StructParent = "/StructParent";

      /// <summary>
      /// (Optional; PDF 1.3; indirect reference preferred) The digital identifier of the image’s
      /// parent Web Capture content set.
      /// </summary>
      [KeyInfo(KeyType.String | KeyType.Optional)]
      public const string ID = "/ID";

      /// <summary>
      /// (Optional; PDF 1.2) An OPI version dictionary for the image. If ImageMask is true, 
      /// this entry is ignored.
      /// </summary>
      [KeyInfo(KeyType.Dictionary | KeyType.Optional)]
      public const string OPI = "/OPI";

      /// <summary>
      /// (Optional; PDF 1.4) A metadata stream containing metadata for the image.
      /// </summary>
      [KeyInfo(KeyType.Stream | KeyType.Optional)]
      public const string Metadata = "/Metadata";

      /// <summary>
      /// (Optional; PDF 1.5) An optional content group or optional content membership dictionary,
      /// specifying the optional content properties for this image XObject. Before the image is
      /// processed, its visibility is determined based on this entry. If it is determined to be 
      /// invisible, the entire image is skipped, as if there were no Do operator to invoke it.
      /// </summary>
      [KeyInfo(KeyType.Dictionary | KeyType.Optional)]
      public const string OC = "/OC";
    }
  }

  /// <summary>
  /// Helper class for creating bitmap masks (8 pels per byte).
  /// </summary>
  class MonochromeMask
  {
    private readonly int sizeX;
    private readonly int sizeY;
    private int writeOffset;
    private int byteBuffer;
    private int bitsWritten;

    /// <summary>
    /// Returns the bitmap mask that will be written to PDF.
    /// </summary>
    public byte[] MaskData
    {
      get { return maskData; }
    }
    private readonly byte[] maskData;

    /// <summary>
    /// Creates a bitmap mask.
    /// </summary>
    public MonochromeMask(int sizeX, int sizeY)
    {
      this.sizeX = sizeX;
      this.sizeY = sizeY;
      int byteSize = ((sizeX + 7) / 8) * sizeY;
      maskData = new byte[byteSize];
      StartLine(0);
    }

    /// <summary>
    /// Starts a new line.
    /// </summary>
    public void StartLine(int newCurrentLine)
    {
      bitsWritten = 0;
      byteBuffer = 0;
      writeOffset = ((sizeX + 7) / 8) * (sizeY - 1 - newCurrentLine);
    }

    /// <summary>
    /// Adds a pel to the current line.
    /// </summary>
    /// <param name="isTransparent"></param>
    public void AddPel(bool isTransparent)
    {
      if (bitsWritten < sizeX)
      {
        // Mask: 0: opaque, 1: transparent (default mapping)
        if (isTransparent)
          byteBuffer = (byteBuffer << 1) + 1;
        else
          byteBuffer = byteBuffer << 1;
        ++bitsWritten;
        if ((bitsWritten & 7) == 0)
        {
          maskData[writeOffset] = (byte)byteBuffer;
          ++writeOffset;
          byteBuffer = 0;
        }
        else if (bitsWritten == sizeX)
        {
          int n = 8 - (bitsWritten & 7);
          byteBuffer = byteBuffer << n;
          maskData[writeOffset] = (byte)byteBuffer;
        }
      }
    }

    /// <summary>
    /// Adds a pel from an alpha mask value.
    /// </summary>
    public void AddPel(int shade)
    {
      // NYI: dithering!!!
      AddPel(shade < 128);
    }
  }
}