#region PDFsharp - A .NET library for processing PDF // // Authors: // Stefan Lange // Thomas Hövel // // Copyright (c) 2005-2016 empira Software GmbH, Cologne Area (Germany) // // http://www.PdfSharpCore.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.IO; using PdfSharpCore.Drawing; using PdfSharpCore.Drawing.Internal; using PdfSharpCore.Pdf.Filters; namespace PdfSharpCore.Pdf.Advanced { /// /// Represents an image. /// public sealed partial class PdfImage : PdfXObject { /// /// Initializes a new instance of PdfImage from an XImage. /// public PdfImage(PdfDocument document, XImage image) : base(document) { Elements.SetName(Keys.Type, "/XObject"); Elements.SetName(Keys.Subtype, "/Image"); _image = image; ////// TODO: identify multiple used images. If the image already exists use the same XRef. ////_defaultName = PdfImageTable.NextImageName; switch (_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; } } /// /// Gets the underlying XImage object. /// public XImage Image { get { return _image; } } readonly XImage _image; /// /// Returns 'Image'. /// public override string ToString() { return "Image"; } /// /// Creates the keys for a JPEG image. /// void InitializeJpeg() { byte[] imageBits = null; using (MemoryStream memory = _image.AsJpeg()) { imageBits = memory.ToArray(); } bool tryFlateDecode = _document.Options.UseFlateDecoderForJpegImages == PdfUseFlateDecoderForJpegImages.Automatic; bool useFlateDecode = _document.Options.UseFlateDecoderForJpegImages == PdfUseFlateDecoderForJpegImages.Always; FlateDecode fd = new FlateDecode(); byte[] imageDataCompressed = (useFlateDecode || tryFlateDecode) ? fd.Encode(imageBits, _document.Options.FlateEncodeMode) : null; if (useFlateDecode || tryFlateDecode && imageDataCompressed.Length < imageBits.Length) { Stream = new PdfStream(imageDataCompressed, this); Elements[PdfStream.Keys.Length] = new PdfInteger(imageDataCompressed.Length); PdfArray arrayFilters = new PdfArray(_document); arrayFilters.Elements.Add(new PdfName("/FlateDecode")); arrayFilters.Elements.Add(new PdfName("/DCTDecode")); Elements[PdfStream.Keys.Filter] = arrayFilters; } else { Stream = new PdfStream(imageBits, this); Elements[PdfStream.Keys.Length] = new PdfInteger(imageBits.Length); Elements[PdfStream.Keys.Filter] = new PdfName("/DCTDecode"); } if (_image.Interpolate) Elements[Keys.Interpolate] = PdfBoolean.True; Elements[Keys.Width] = new PdfInteger(_image.PixelWidth); Elements[Keys.Height] = new PdfInteger(_image.PixelHeight); Elements[Keys.BitsPerComponent] = new PdfInteger(8); Elements[Keys.ColorSpace] = new PdfName("/DeviceRGB"); } /// /// Creates the keys for a FLATE image. /// void InitializeNonJpeg() { ReadTrueColorMemoryBitmap(3, 8, true); } 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); } /// /// Reads images that are returned from GDI+ without color palette. /// /// 4 (32bpp RGB), 3 (24bpp RGB, 32bpp ARGB) /// 8 /// true (ARGB), false (RGB) private void ReadTrueColorMemoryBitmap(int components, int bits, bool hasAlpha) { int pdfVersion = Owner.Version; MemoryStream memory = new MemoryStream(); memory = _image.AsBitmap(); // THHO4THHO Use ImageImporterBMP here to avoid redundant code. 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.Dispose(); 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) bool bigHeader = false; if (ReadWord(imageBits, 0) != 0x4d42 || // "BM" ReadDWord(imageBits, 2) != streamLength || ReadDWord(imageBits, 18) != width || ReadDWord(imageBits, 22) != height) { throw new NotImplementedException("ReadTrueColorMemoryBitmap: unsupported format"); } int infoHeaderSize = ReadDWord(imageBits, 14); // sizeof BITMAPINFOHEADER if (infoHeaderSize != 40 && infoHeaderSize != 108) { throw new NotImplementedException("ReadTrueColorMemoryBitmap: unsupported format #2"); } bigHeader = infoHeaderSize == 108; if (ReadWord(imageBits, 26) != 1 || (!hasAlpha && ReadWord(imageBits, bigHeader?30:28) != components * bits || hasAlpha && ReadWord(imageBits, bigHeader?30:28) != (components + 1) * bits) || bigHeader ? ReadWord(imageBits, 32) != 0 : ReadDWord(imageBits, 30) != 0) { throw new NotImplementedException("ReadTrueColorMemoryBitmap: unsupported format #3"); } 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, _document.Options.FlateEncodeMode); 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[PdfStream.Keys.Length] = new PdfInteger(maskDataCompressed.Length); pdfMask.Elements[PdfStream.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, _document.Options.FlateEncodeMode); 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[PdfStream.Keys.Length] = new PdfInteger(alphaMaskCompressed.Length); smask.Elements[PdfStream.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, _document.Options.FlateEncodeMode); Stream = new PdfStream(imageDataCompressed, this); Elements[PdfStream.Keys.Length] = new PdfInteger(imageDataCompressed.Length); Elements[PdfStream.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*/) { int pdfVersion = Owner.Version; int firstMaskColor = -1, lastMaskColor = -1; bool segmentedColorMask = false; MemoryStream memory = new MemoryStream(); // THHO4THHO Use ImageImporterBMP here to avoid redundant code. 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.Dispose(); int height = _image.PixelHeight; int width = _image.PixelWidth; 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"); } 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); const 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) // { ... } 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"); } } FlateDecode fd = new FlateDecode(); if (firstMaskColor != -1 && lastMaskColor != -1) { // Color mask requires Reader 4.0 or higher: //if (!segmentedColorMask && pdfVersion >= 13) if (!segmentedColorMask && pdfVersion >= 13 && !isGray) { 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, _document.Options.FlateEncodeMode); 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[PdfStream.Keys.Length] = new PdfInteger(maskDataCompressed.Length); pdfMask.Elements[PdfStream.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, _document.Options.FlateEncodeMode); byte[] imageDataFaxCompressed = isFaxEncoding ? fd.Encode(imageDataFax, _document.Options.FlateEncodeMode) : 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[PdfStream.Keys.Length] = new PdfInteger(imageDataFax.Length); Elements[PdfStream.Keys.Filter] = new PdfName("/CCITTFaxDecode"); //PdfArray array2 = new PdfArray(_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[PdfStream.Keys.DecodeParms] = dictionary; // array2; } else { Stream = new PdfStream(imageDataFaxCompressed, this); Elements[PdfStream.Keys.Length] = new PdfInteger(imageDataFaxCompressed.Length); PdfArray arrayFilters = new PdfArray(_document); arrayFilters.Elements.Add(new PdfName("/FlateDecode")); arrayFilters.Elements.Add(new PdfName("/CCITTFaxDecode")); Elements[PdfStream.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[PdfStream.Keys.DecodeParms] = arrayDecodeParms; } } else { // /FlateDecode creates the smaller file (or no monochrome bitmap): Stream = new PdfStream(imageDataCompressed, this); Elements[PdfStream.Keys.Length] = new PdfInteger(imageDataCompressed.Length); Elements[PdfStream.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)) { PdfDictionary colorPalette = null; colorPalette = new PdfDictionary(_document); byte[] packedPaletteData = paletteData.Length >= 48 ? fd.Encode(paletteData, _document.Options.FlateEncodeMode) : 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[PdfStream.Keys.Length] = new PdfInteger(packedPaletteData.Length); colorPalette.Elements[PdfStream.Keys.Filter] = new PdfName("/FlateDecode"); } else { // Create uncompressed color palette: colorPalette.CreateStream(paletteData); colorPalette.Elements[PdfStream.Keys.Length] = new PdfInteger(paletteData.Length); } Owner._irefTable.Add(colorPalette); PdfArray arrayColorSpace = new PdfArray(_document); arrayColorSpace.Elements.Add(new PdfName("/Indexed")); arrayColorSpace.Elements.Add(new PdfName("/DeviceRGB")); arrayColorSpace.Elements.Add(new PdfInteger(paletteColors - 1)); arrayColorSpace.Elements.Add(colorPalette.Reference); Elements[Keys.ColorSpace] = arrayColorSpace; } else { Elements[Keys.ColorSpace] = new PdfName("/DeviceGray"); } if (_image.Interpolate) Elements[Keys.Interpolate] = PdfBoolean.True; } } /// /// Common keys for all streams. /// public sealed new class Keys : PdfXObject.Keys { // ReSharper disable InconsistentNaming /// /// (Optional) The type of PDF object that this dictionary describes; /// if present, must be XObject for an image XObject. /// [KeyInfo(KeyType.Name | KeyType.Optional)] public const string Type = "/Type"; /// /// (Required) The type of XObject that this dictionary describes; /// must be Image for an image XObject. /// [KeyInfo(KeyType.Name | KeyType.Required)] public const string Subtype = "/Subtype"; /// /// (Required) The width of the image, in samples. /// [KeyInfo(KeyType.Integer | KeyType.Required)] public const string Width = "/Width"; /// /// (Required) The height of the image, in samples. /// [KeyInfo(KeyType.Integer | KeyType.Required)] public const string Height = "/Height"; /// /// (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. /// [KeyInfo(KeyType.NameOrArray | KeyType.Required)] public const string ColorSpace = "/ColorSpace"; /// /// (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. /// [KeyInfo(KeyType.Integer | KeyType.Required)] public const string BitsPerComponent = "/BitsPerComponent"; /// /// (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. /// [KeyInfo(KeyType.Name | KeyType.Optional)] public const string Intent = "/Intent"; /// /// (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. /// [KeyInfo(KeyType.Boolean | KeyType.Optional)] public const string ImageMask = "/ImageMask"; /// /// (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. /// [KeyInfo(KeyType.StreamOrArray | KeyType.Optional)] public const string Mask = "/Mask"; /// /// (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”. /// [KeyInfo(KeyType.Array | KeyType.Optional)] public const string Decode = "/Decode"; /// /// (Optional) A flag indicating whether image interpolation is to be performed. /// Default value: false. /// [KeyInfo(KeyType.Boolean | KeyType.Optional)] public const string Interpolate = "/Interpolate"; /// /// (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. /// [KeyInfo(KeyType.Array | KeyType.Optional)] public const string Alternates = "/Alternates"; /// /// (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). /// [KeyInfo(KeyType.Integer | KeyType.Required)] public const string SMask = "/SMask"; /// /// (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. /// [KeyInfo(KeyType.Integer | KeyType.Optional)] public const string SMaskInData = "/SMaskInData"; /// /// (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. /// [KeyInfo(KeyType.Name | KeyType.Optional)] public const string Name = "/Name"; /// /// (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. /// [KeyInfo(KeyType.Integer | KeyType.Required)] public const string StructParent = "/StructParent"; /// /// (Optional; PDF 1.3; indirect reference preferred) The digital identifier of the image’s /// parent Web Capture content set. /// [KeyInfo(KeyType.String | KeyType.Optional)] public const string ID = "/ID"; /// /// (Optional; PDF 1.2) An OPI version dictionary for the image. If ImageMask is true, /// this entry is ignored. /// [KeyInfo(KeyType.Dictionary | KeyType.Optional)] public const string OPI = "/OPI"; /// /// (Optional; PDF 1.4) A metadata stream containing metadata for the image. /// [KeyInfo(KeyType.Stream | KeyType.Optional)] public const string Metadata = "/Metadata"; /// /// (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. /// [KeyInfo(KeyType.Dictionary | KeyType.Optional)] public const string OC = "/OC"; // ReSharper restore InconsistentNaming } } /// /// Helper class for creating bitmap masks (8 pels per byte). /// class MonochromeMask { /// /// Returns the bitmap mask that will be written to PDF. /// public byte[] MaskData { get { return _maskData; } } private readonly byte[] _maskData; /// /// Creates a bitmap mask. /// public MonochromeMask(int sizeX, int sizeY) { _sizeX = sizeX; _sizeY = sizeY; int byteSize = ((sizeX + 7) / 8) * sizeY; _maskData = new byte[byteSize]; StartLine(0); } /// /// Starts a new line. /// public void StartLine(int newCurrentLine) { _bitsWritten = 0; _byteBuffer = 0; _writeOffset = ((_sizeX + 7) / 8) * (_sizeY - 1 - newCurrentLine); } /// /// Adds a pel to the current line. /// /// 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; } } } /// /// Adds a pel from an alpha mask value. /// public void AddPel(int shade) { // NYI: dithering!!! AddPel(shade < 128); } private readonly int _sizeX; private readonly int _sizeY; private int _writeOffset; private int _byteBuffer; private int _bitsWritten; } }