using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Drawing; using FastReport.Utils; using System.Drawing.Imaging; using System.Security.Cryptography; #if PRINT_HOUSE using System.IO.Compression; #endif using System.Windows.Forms; namespace FastReport.Export.Pdf { public partial class PDFExport : ExportBase { #region AddPictureObject, AddPictureObjectRAW private void AddPictureObject(ReportComponentBase obj, bool drawBorder, int quality, StringBuilder sb_in) { if (obj is PictureObject) { PictureObject pict = (obj as PictureObject); if (pict.Transparency != 0 && pict.Transparency != 1) { // Transparency scale for PDF pict.Transparency /= 1.5f; } if (PdfCompliance == PdfStandard.PdfA_1a) { pict.Transparency = 0; } if ((obj as PictureObject).Fill is TextureFill) { TextureFill fill = (obj as PictureObject).Fill as TextureFill; Brush textureBrush = fill.CreateBrush(new RectangleF(0, 0, obj.Width, obj.Height)); using (Bitmap image = new Bitmap((int)obj.Width, (int)obj.Height, PixelFormat.Format32bppArgb)) using (Graphics g = Graphics.FromImage(image)) { g.FillRectangle(textureBrush, new RectangleF(0, 0, obj.Width, obj.Height)); PictureObject fillPic = new PictureObject(); fillPic.Image = image; fillPic.Left = obj.AbsLeft; fillPic.Top = obj.AbsTop; fillPic.Width = obj.Width; fillPic.Height = obj.Height; AddPictureObject(fillPic, false, quality, sb_in, false); } } else { //ToDo: add other fills } } if (!isPdfX()) AddAnnot(obj); AddPictureObject(obj, drawBorder, quality, sb_in, false); } private long AddPictureObject(ReportComponentBase obj, bool drawBorder, int quality, StringBuilder sb_in, bool keepZeroPosition) { long imageIndex = -1; if (ImagesOriginalResolution) if ((imageIndex = AddPictureObjectRAW(obj as PictureObject, drawBorder, sb_in)) >= 0) return imageIndex; float width = obj.Width;// > paperWidth + obj.AbsLeft ? paperWidth + obj.AbsLeft : obj.Width; float height = obj.Height;// > paperHeight + obj.AbsTop ? paperHeight + obj.AbsTop : obj.Height; if (width < 0.5f || height < 0.5f) return -1; Border oldBorder = obj.Border.Clone(); obj.Border.Lines = BorderLines.None; float printZoom = printOptimized ? 4 : 1; int bitmapWidth = (int)Math.Round(width * printZoom); int bitmapHeight = (int)Math.Round(height * printZoom); // check for max bitmap object size { // 2GB (max .net object size) / 4 (Format32bppArgb is 4 bytes) // see http://stackoverflow.com/a/29175905/4667434 const ulong maxPixels = 536870912; if ((ulong)bitmapWidth * (ulong)bitmapHeight >= maxPixels) { bitmapWidth = (int)width; bitmapHeight = (int)height; } if ((ulong)bitmapWidth * (ulong)bitmapHeight >= maxPixels) { return -1; } } using (Bitmap image = new Bitmap(bitmapWidth, bitmapHeight, PixelFormat.Format32bppArgb)) using (Graphics g = Graphics.FromImage(image)) { g.TranslateTransform(-obj.AbsLeft * printZoom, -obj.AbsTop * printZoom); g.Clear(transparentImages #if !SKIA && (PdfCompliance != PdfStandard.PdfA_1a) #endif ? Color.Transparent : Color.White); // if PDF/X-3 or PDF/A-1a then render image with page // because they don't support transparency if (PdfCompliance == PdfStandard.PdfX_3 && obj.Page != null && obj.Page is ReportPage) { ReportPage page = obj.Page as ReportPage; float leftMargin = (int)Math.Round(page.LeftMargin * Units.Millimeters * printZoom); float topMargin = (int)Math.Round(page.TopMargin * Units.Millimeters * printZoom); g.TranslateTransform(-leftMargin, -topMargin); obj.Page.Draw(new FRPaintEventArgs(g, printZoom, printZoom, Report.GraphicCache)); } else { obj.Draw(new FRPaintEventArgs(g, printZoom, printZoom, Report.GraphicCache)); } imageIndex = AppendPDFImage(image, quality); } if (imageIndex == -1) return imageIndex; AddImageToList(imageIndex); StringBuilder sb = new StringBuilder(256); sb.AppendLine("q"); if (obj is PictureObject) GetPDFFillTransparent(Color.FromArgb((byte)((1 - (obj as PictureObject).Transparency) * 255f), Color.Black), sb); float bWidth = width == 0 ? 1 : width * PDF_DIVIDER; float bHeight = height == 0 ? 1 : height * PDF_DIVIDER; sb.Append(FloatToString(bWidth)).Append(" 0 0 "); sb.Append(FloatToString(bHeight)).Append(" "); if (keepZeroPosition) { sb.Append("0 0"); } else { sb.Append(FloatToString(GetLeft(obj.AbsLeft))).Append(" "); sb.Append(FloatToString(GetTop(obj.AbsTop + height))); } sb.AppendLine(" cm"); sb.Append("/Im").Append(imageIndex).AppendLine(" Do"); sb.AppendLine("Q"); sb_in.Append(sb); obj.Border = oldBorder; if (drawBorder) DrawPDFBorder(obj.Border, obj.AbsLeft, obj.AbsTop, width, height, sb_in); return imageIndex; } private long AppendPDFImage(Bitmap image, int quality) { int[] rawBitmap = GetRawBitmap(image); string hash = CalculateHash(rawBitmap); long imageIndex = GetImageIndexByHash(hash); if (imageIndex == -1) { using (MemoryStream imageStream = new MemoryStream()) using (MemoryStream maskStream = GetMask(rawBitmap)) { if (JpegCompression) { ExportUtils.SaveJpeg(image, imageStream, quality); imageStream.Position = 0; imageIndex = WriteImage_Jpeg(imageStream, maskStream, image.Width, image.Height); } else { // grayscale is set to false // because it's already grayscaled // if "Grayscale" option is set to true WritePixelColors(rawBitmap, imageStream, false); imageStream.Position = 0; using (MemoryStream imageDeflateStream = new MemoryStream()) { ExportUtils.ZLibDeflate(imageStream, imageDeflateStream); imageDeflateStream.Position = 0; imageIndex = WriteImage_Deflate(imageDeflateStream, maskStream, image.Width, image.Height); } } } SetImageIndexByHash(hash, imageIndex); } return imageIndex; } private long AddPictureObjectRAW(PictureObject obj, bool drawBorder, StringBuilder sb_in) { if (obj == null) return -1; if (obj.Width < 0.5f || obj.Height < 0.5f) return -1; obj.ForceLoadImage(); if (obj.Image == null) return -1; int rawWidth = obj.Image.Width; int rawHeight = obj.Image.Height; long imageIndex = -1; // convert to PixelFormat.Format32bppArgb using (Bitmap image = new Bitmap(obj.Image.Width, obj.Image.Height, PixelFormat.Format32bppArgb)) using (Graphics g = Graphics.FromImage(image)) { g.DrawImage(obj.Image, new Rectangle(0, 0, obj.Image.Width, obj.Image.Height)); int[] rawBitmap = GetRawBitmap(image); // for hash we also use Width, Height and SizeMode // because there are different masks for them string hash = CalculateHash(rawBitmap) + "|" + obj.SizeMode.ToString() + "|" + obj.Width.ToString() + "|" + obj.Height.ToString(); imageIndex = GetImageIndexByHash(hash); if (imageIndex == -1) { using (MemoryStream colorsRawStream = new MemoryStream()) { bool hasMask = false; byte[] mask = null; #if PRINT_HOUSE if (ColorSpace == PdfColorSpace.CMYK) using (Stream rawCMYKA = FindRawCMYKA(obj)) if (rawCMYKA != null && rawCMYKA.Length > 0) { if (rawCMYKA.Length != rawWidth * rawHeight * 5) throw new Exception(ExportUtils.StringFormat("Incorrect CMYKA data length (length={0}, width={1}, height={2})", rawCMYKA.Length, rawWidth, rawHeight)); mask = new byte[rawWidth * rawHeight]; const int cmykaSamples = 5; for (int i = 0, mask_i = 0; i < rawCMYKA.Length; i += cmykaSamples, mask_i++) { colorsRawStream.WriteByte((byte)rawCMYKA.ReadByte()); // C colorsRawStream.WriteByte((byte)rawCMYKA.ReadByte()); // M colorsRawStream.WriteByte((byte)rawCMYKA.ReadByte()); // Y colorsRawStream.WriteByte((byte)rawCMYKA.ReadByte()); // K mask[mask_i] = (byte)rawCMYKA.ReadByte(); // A if (!hasMask && mask[mask_i] != 0xff) hasMask = true; } } #endif if (mask == null) { mask = new byte[rawWidth * rawHeight]; for (int i = 0, mask_i = 0; i < rawBitmap.Length; i++, mask_i++) { mask[mask_i] = (byte)(((UInt32)rawBitmap[i]) >> 24); if (!hasMask && mask[mask_i] != 0xff) hasMask = true; } WritePixelColors(rawBitmap, colorsRawStream, obj.Grayscale); } // emulate PictureBoxSizeMode.Normal behavior if (obj.SizeMode == PictureBoxSizeMode.Normal) { for (int x = 0; x < rawWidth; x++) for (int y = 0; y < rawHeight; y++) if (x >= obj.Width || y >= obj.Height) { mask[y * rawWidth + x] = 0; if (!hasMask) hasMask = true; } } // emulate PictureBoxSizeMode.CenterImage behavior else if (obj.SizeMode == PictureBoxSizeMode.CenterImage) { if (obj.Width < rawWidth || obj.Height < rawHeight) { float imgWidth = rawWidth; float imgHeight = rawHeight; float imgLeft = obj.AbsLeft + obj.Width / 2 - imgWidth / 2; float imgTop = obj.AbsTop + obj.Height / 2 - imgHeight / 2; float borderWidth = obj.Width; float borderHeight = obj.Height; float borderLeft = obj.AbsLeft; float borderTop = obj.AbsTop; RectangleF imgRect = new RectangleF ( imgLeft, imgTop, imgWidth, imgHeight ); RectangleF borderRect = new RectangleF ( borderLeft, borderTop, borderWidth, borderHeight ); if (imgRect.IntersectsWith(borderRect)) { // todo: need better check // on high zoom borders don't match image area RectangleF intersect = RectangleF.Intersect(imgRect, borderRect); intersect.Width = (int)intersect.Width; intersect.Height = (int)intersect.Height; for (int x = 0; x < rawWidth; x++) for (int y = 0; y < rawHeight; y++) { int _x = (x + (int)imgRect.X); int _y = (y + (int)imgRect.Y); if (!intersect.Contains(_x, _y)) { mask[y * rawWidth + x] = 0; if (!hasMask) hasMask = true; } } } } } using (MemoryStream colorsDeflateStream = new MemoryStream()) { colorsRawStream.Position = 0; ExportUtils.ZLibDeflate(colorsRawStream, colorsDeflateStream); colorsDeflateStream.Position = 0; using (MemoryStream maskStream = hasMask ? new MemoryStream(mask) : null) { imageIndex = WriteImage_Deflate(colorsDeflateStream, maskStream, rawWidth, rawHeight); } } } SetImageIndexByHash(hash, imageIndex); } } if (imageIndex == -1) return -1; AddImageToList(imageIndex); StringBuilder sb = new StringBuilder(256); sb.AppendLine("q"); GetPDFFillTransparent(Color.FromArgb((byte)((1 - obj.Transparency) * 255f), Color.Black), sb); RectangleF area = CalculateArea(obj, rawWidth, rawHeight); //double angle = (Math.PI / 180) * -obj.Angle; // get angle in radians // PLEASE NOTE // to avoid distortion, matrices should be in this order: // 1. translate // 2. rotate // 3. scale. // translate matrix sb.AppendLine(ExportUtils.StringFormat("1 0 0 1 {0} {1} cm", FloatToString(area.Left), FloatToString(area.Top) )); // we don't yet support rotation // rotate matrix /*sb.AppendLine(ExportUtils.StringFormat("{0} {1} {2} {3} 0 0 cm", FloatToString(Math.Cos(angle)), FloatToString(Math.Sin(angle)), FloatToString(-Math.Sin(angle)), FloatToString(Math.Cos(angle)) ));*/ // scale matrix sb.AppendLine(ExportUtils.StringFormat("{0} 0 0 {1} 0 0 cm", FloatToString(area.Width), FloatToString(area.Height) )); // draw image sb.AppendLine(ExportUtils.StringFormat("/Im{0} Do", imageIndex.ToString() )); sb.AppendLine("Q"); sb_in.Append(sb); if (drawBorder) DrawPDFBorder(obj.Border, obj.AbsLeft, obj.AbsTop, /*width*/obj.Width, /*height*/obj.Height, sb_in); return imageIndex; } #endregion #region WriteMask, WriteImage_Deflate, WriteImage_Jpeg private long WriteMask(MemoryStream mask, int width, int height) { long maskIndex = -1; if (mask == null || mask.Length == 0) return maskIndex; maskIndex = UpdateXRef(); WriteLn(pdf, ObjNumber(maskIndex)); WriteLn(pdf, "<<"); WriteLn(pdf, "/Type /XObject"); WriteLn(pdf, "/Subtype /Image"); WriteLn(pdf, "/Width " + width.ToString()); WriteLn(pdf, "/Height " + height.ToString()); WriteLn(pdf, "/ColorSpace /DeviceGray/Matte[ 0 0 0]"); WriteLn(pdf, "/BitsPerComponent 8"); WriteLn(pdf, "/Interpolate false"); WritePDFStream(pdf, mask, maskIndex, compressed, encrypted, false, true); return maskIndex; } private long WriteImage_Deflate(MemoryStream image, MemoryStream mask, int width, int height) { long imageIndex = -1; if (image == null || image.Length == 0) return imageIndex; long maskIndex = WriteMask(mask, width, height); imageIndex = UpdateXRef(); WriteLn(pdf, ObjNumber(imageIndex)); WriteLn(pdf, "<<"); WriteLn(pdf, "/Type /XObject"); WriteLn(pdf, "/Subtype /Image"); WriteLn(pdf, "/Width " + width.ToString()); WriteLn(pdf, "/Height " + height.ToString()); WriteLn(pdf, "/Predictor 10"); if (ColorSpace == PdfColorSpace.RGB) WriteLn(pdf, "/ColorSpace /DeviceRGB"); else if (ColorSpace == PdfColorSpace.CMYK) WriteLn(pdf, "/ColorSpace /DeviceCMYK"); WriteLn(pdf, "/BitsPerComponent 8"); WriteLn(pdf, "/Filter /FlateDecode"); WriteLn(pdf, "/Interpolate false"); if (maskIndex != -1) WriteLn(pdf, "/SMask " + ObjNumberRef(maskIndex)); WritePDFStream(pdf, image, imageIndex, false, encrypted, false, true); return imageIndex; } private long WriteImage_Jpeg(MemoryStream image, MemoryStream mask, int width, int height) { long imageIndex = -1; if (image == null || image.Length == 0) return imageIndex; long maskIndex = WriteMask(mask, width, height); imageIndex = UpdateXRef(); WriteLn(pdf, ObjNumber(imageIndex)); WriteLn(pdf, "<<"); WriteLn(pdf, "/Type /XObject"); WriteLn(pdf, "/Subtype /Image"); WriteLn(pdf, "/Width " + width.ToString()); WriteLn(pdf, "/Height " + height.ToString()); WriteLn(pdf, "/ColorSpace /DeviceRGB"); WriteLn(pdf, "/BitsPerComponent 8"); WriteLn(pdf, "/Filter /DCTDecode"); WriteLn(pdf, "/Interpolate false"); if (maskIndex != -1) WriteLn(pdf, "/SMask " + ObjNumberRef(maskIndex)); WritePDFStream(pdf, image, imageIndex, false, encrypted, false, true); return imageIndex; } #endregion #region Other methods private int[] GetRawBitmap(Bitmap image) { int raw_size = image.Width * image.Height; int[] raw_picture = new int[raw_size]; BitmapData bmpdata = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadOnly, image.PixelFormat); IntPtr ptr = bmpdata.Scan0; System.Runtime.InteropServices.Marshal.Copy(ptr, raw_picture, 0, raw_size); image.UnlockBits(bmpdata); return raw_picture; } private string CalculateHash(int[] raw_picture) { byte[] raw_picture_byte = new byte[raw_picture.Length * sizeof(int)]; Buffer.BlockCopy(raw_picture, 0, raw_picture_byte, 0, raw_picture_byte.Length); byte[] hash = new Murmur3().ComputeHash(raw_picture_byte); return Convert.ToBase64String(hash); } /// /// Calculates mask for image. /// private MemoryStream GetMask(int[] raw_pixels) { if (!transparentImages) return null; if (PdfCompliance == PdfStandard.PdfX_3) return null; MemoryStream mask_stream = new MemoryStream(raw_pixels.Length); bool alpha = false; byte pixel; for (int i = 0; i < raw_pixels.Length; i++) unchecked { pixel = (byte)(((UInt32)raw_pixels[i]) >> 24); if (!alpha && pixel != 0xff) alpha = true; mask_stream.WriteByte(pixel); } if (alpha) { mask_stream.Position = 0; return mask_stream; } return null; } /// /// Calculates image bounds according to . /// RectangleF CalculateArea(PictureObject obj, int raw_width, int raw_height) { float width = obj.Width; float height = obj.Height; float pdfLeft = GetLeft(obj.AbsLeft); float pdfTop = GetTop(obj.AbsTop + height); switch (obj.SizeMode) { case PictureBoxSizeMode.Normal: { width = raw_width; height = raw_height; pdfTop = GetTop(obj.AbsTop + height); } break; case PictureBoxSizeMode.Zoom: { float w = obj.Width / raw_width; float h = obj.Height / raw_height; if (w > h) { width = raw_width * h; height = raw_height * h; pdfLeft = GetLeft(obj.AbsLeft + obj.Width / 2 - width / 2); } else { width = raw_width * w; height = raw_height * w; pdfTop = GetTop(obj.AbsTop + height + obj.Height / 2 - height / 2); } } break; case PictureBoxSizeMode.CenterImage: { width = raw_width; height = raw_height; pdfLeft = GetLeft(obj.AbsLeft + obj.Width / 2 - width / 2); pdfTop = GetTop(obj.AbsTop + height + obj.Height / 2 - height / 2); } break; } float pdfWidth = width * PDF_DIVIDER; float pdfHeight = height * PDF_DIVIDER; return new RectangleF(pdfLeft, pdfTop, pdfWidth, pdfHeight); } /// /// Writes pixels' colors without alpha to stream according to CMYK or RGB color space. /// Pixels should be in the format. /// void WritePixelColors(int[] pixels, Stream stream, bool grayscale) { for (int i = 0; i < pixels.Length; i++) { byte blue = (byte)(pixels[i] & 0xFF); byte green = (byte)((pixels[i] >> 8) & 0xFF); byte red = (byte)((pixels[i] >> 16) & 0xFF); if (ColorSpace == PdfColorSpace.RGB) { if (grayscale) { byte gray = (byte)(red * 0.299 + green * 0.587 + blue * 0.114); stream.WriteByte(gray); stream.WriteByte(gray); stream.WriteByte(gray); } else { stream.WriteByte(red); stream.WriteByte(green); stream.WriteByte(blue); } } else if (ColorSpace == PdfColorSpace.CMYK) { if (grayscale) { byte gray = (byte)(255 - (red * 0.299 + green * 0.587 + blue * 0.114)); stream.WriteByte(0); stream.WriteByte(0); stream.WriteByte(0); stream.WriteByte(gray); } else { float fred = ((float)red) / 255f; float fgreen = ((float)green) / 255f; float fblue = ((float)blue) / 255f; float fblack = 1 - Math.Max(fred, Math.Max(fgreen, fblue)); float fcyan = (1 - fred - fblack) / (1 - fblack); float fmagenta = (1 - fgreen - fblack) / (1 - fblack); float fyellow = (1 - fblue - fblack) / (1 - fblack); byte black = (byte)(fblack * 255); byte cyan = (byte)(fcyan * 255); byte magenta = (byte)(fmagenta * 255); byte yellow = (byte)(fyellow * 255); stream.WriteByte(cyan); stream.WriteByte(magenta); stream.WriteByte(yellow); stream.WriteByte(black); } } } } #if PRINT_HOUSE /// /// This method attempts to find a raw CMYKA data /// which should be stored in a separate file /// alongside of the image location. /// Stream FindRawCMYKA(PictureObject picObj) { Stream stream = null; if (picObj != null && picObj.ImageLocation != null && picObj.ImageLocation.Trim() != "") { string file = picObj.ImageLocation + ".cmyka"; string file_gz = picObj.ImageLocation + ".cmyka.gz"; if (File.Exists(file)) { stream = File.OpenRead(file); } else if (File.Exists(file_gz)) { //stream = new GZipStream(File.OpenRead(file_gz), CompressionMode.Decompress); using (FileStream reader = File.OpenRead(file_gz)) using (GZipStream zip = new GZipStream(reader, CompressionMode.Decompress, true)) { stream = new MemoryStream(); zip.CopyTo(stream); stream.Position = 0; } } } return stream; } #endif #endregion #region Picture list and hashes private List picResList; private Dictionary hashList; private long GetImageIndexByHash(string hash) { long result = -1; if (hashList.TryGetValue(hash, out result)) return result; else return -1; } private void SetImageIndexByHash(string hash, long index) { hashList.Add(hash, index); } private void AddImageToList(long imageIndex) { if (picResList.IndexOf(imageIndex) == -1) picResList.Add(imageIndex); } #endregion } }