#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-2007 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 Gdip
using System.Drawing;
using System.Drawing.Imaging;
#endif
#if Wpf
using System.Windows.Media;
#endif
using PdfSharp.Drawing;
using PdfSharp.Fonts.TrueType;
using PdfSharp.Internal;
using PdfSharp.Pdf.Filters;

namespace PdfSharp.Pdf.Advanced
{
  /// <summary>
  /// Represents an image.
  /// </summary>
  public sealed 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 this.image;}
    }
    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 = new MemoryStream();
      image.image.Save(memory, ImageFormat.Jpeg);
      int streamLength = (int)memory.Length;
      byte[] imageBits = new byte[streamLength];
      memory.Seek(0, SeekOrigin.Begin);
      memory.Read(imageBits, 0, streamLength);
      memory.Close();

      Stream = new PdfStream(imageBits, this);

      Elements[Keys.Length]           = new PdfInteger(streamLength);
      Elements[Keys.Filter]           = new PdfName("/DCTDecode");
      Elements[Keys.Width]            = new PdfInteger(image.Width);
      Elements[Keys.Height]           = new PdfInteger(image.Height);
      Elements[Keys.BitsPerComponent] = new PdfInteger(8);
      Elements[Keys.ColorSpace]       = new PdfName("/DeviceRGB");
    }

    /// <summary>
    /// Creates the keys for a FLATE image.
    /// </summary>
    void InitializeNonJpeg()
    {
      bool hasMask = false;
      int pPdfVersion = this.Owner.Version;
      switch (image.image.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.");
      }
    }

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

    private 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 = this.Owner.Version;
      MemoryStream memory = new MemoryStream();
      //image.image.Save(memory, ImageFormat.MemoryBmp);
      image.image.Save(memory, ImageFormat.Bmp);
      if (memory.Length > 0)
      {
        int streamLength = (int)memory.Length;
        byte[] imageBits = new byte[streamLength];
        memory.Seek(0, SeekOrigin.Begin);
        memory.Read(imageBits, 0, streamLength);
        memory.Close();
        //memory = null;
        //GC.Collect();

        int height = this.image.Height;
        int width = this.image.Width;

        // 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;
            if (hasAlpha)
              mask.StartLine(y);

            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[nOffsetWrite / 3] = imageBits[nFileOffset + nOffsetRead + 3];
                if (!hasMask || !hasAlphaMask)
                {
                  if (imageBits[nFileOffset + nOffsetRead + 3] != 255)
                  {
                    hasMask = true;
                    if (imageBits[nFileOffset + nOffsetRead + 3] != 0)
                      hasAlphaMask = true;
                  }
                }
              }
              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(this.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;
        }
        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(this.document);
          smask.Elements.SetName(Keys.Type, "/XObject");
          smask.Elements.SetName(Keys.Subtype, "/Image");

          this.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);

        //imageData = null;
        //GC.Collect();

        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);
        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 = this.Owner.Version;
      int firstMaskColor = -1, lastMaskColor = -1;
      bool segmentedColorMask = false;

      MemoryStream memory = new MemoryStream();
      //image.image.Save(memory, ImageFormat.MemoryBmp);
      image.image.Save(memory, ImageFormat.Bmp);
      if (memory.Length > 0)
      {
        int streamLength = (int)memory.Length;
        //if (streamLength > 0)
        byte[] imageBits = new byte[streamLength];
        memory.Seek(0, SeekOrigin.Begin);
        memory.Read(imageBits, 0, streamLength);
        memory.Close();

        int height = this.image.Height;
        int width = this.image.Width;

        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("ReadIndexedMemoryBitmap: unsupported format");
        }
        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);

        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 (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:
          }
        }

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

        FlateDecode fd = new FlateDecode();
        PdfDictionary colorPalette = new PdfDictionary(this.document);
        // TODO: decide at run-time if compression makes sense
#if false
        // Create uncompressed color palette:
        colorPalette.CreateStream(paletteData);
        colorPalette.Elements[Keys.Length] = new PdfInteger(paletteData.Length);
#else
        // Create compressed color palette:
        byte[] packedPaletteData = fd.Encode(paletteData);
        colorPalette.CreateStream(packedPaletteData);
        colorPalette.Elements[Keys.Length] = new PdfInteger(packedPaletteData.Length);
        colorPalette.Elements[Keys.Filter] = new PdfName("/FlateDecode");
#endif
        this.Owner.irefTable.Add(colorPalette);

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

        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)
            {
              imageData[bytesOffsetWrite] = imageBits[bytesFileOffset + bytesOffsetRead];
              if (firstMaskColor != -1)
              {
                int n = imageBits[bytesFileOffset + bytesOffsetRead];
                if (bits == 8)
                {
                  // TODO???: segmentedColorMask == true => falsche Maske NYI
                  mask.AddPel((n >= firstMaskColor) && (n <= lastMaskColor));
                }
                else if (bits == 4)
                {
                  // TODO???: segmentedColorMask == true => falsche Maske 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(this.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(this.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);

        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);
        PdfArray array2 = new PdfArray(this.document);
        array2.Elements.Add(new PdfName("/Indexed"));
        array2.Elements.Add(new PdfName("/DeviceRGB"));
        array2.Elements.Add(new PdfInteger(paletteColors - 1));
        array2.Elements.Add(colorPalette.Reference);
        Elements[Keys.ColorSpace]       = array2; 
      }
    }

    /// <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 transparencyrelated 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 int sizeX;
    private 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 byte[] maskData = null;

    /// <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 * 2 + 1;
        else
          byteBuffer = byteBuffer * 2;
        ++bitsWritten;
        if ((bitsWritten & 7) == 0)
        {
          maskData[writeOffset] = (byte)byteBuffer;
          ++writeOffset;
          byteBuffer = 0;
        }
        else if (bitsWritten == sizeX)
        {
          int n = 8 - (bitsWritten & 7);
          for (int i = 1; i <= n; ++i)
            byteBuffer *= 2;
          maskData[writeOffset] = (byte)byteBuffer;
        }
      }
    }

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