diff --git a/ClosedXML/ClosedXML.csproj b/ClosedXML/ClosedXML.csproj index d73fbfd..3a8443f 100644 --- a/ClosedXML/ClosedXML.csproj +++ b/ClosedXML/ClosedXML.csproj @@ -81,6 +81,8 @@ + + diff --git a/ClosedXML/Excel/Drawings/XLPicture.cs b/ClosedXML/Excel/Drawings/XLPicture.cs index d591f8f..c87d54c 100644 --- a/ClosedXML/Excel/Drawings/XLPicture.cs +++ b/ClosedXML/Excel/Drawings/XLPicture.cs @@ -12,11 +12,45 @@ [DebuggerDisplay("{Name}")] internal class XLPicture : IXLPicture { + private static IDictionary FormatMap; private readonly IXLWorksheet _worksheet; - private Int32 height; private Int32 width; + static XLPicture() + { + var properties = typeof(ImageFormat).GetProperties(BindingFlags.Static | BindingFlags.Public); + FormatMap = Enum.GetValues(typeof(XLPictureFormat)) + .Cast() + .Where(pf => properties.Any(pi => pi.Name.Equals(pf.ToString(), StringComparison.OrdinalIgnoreCase))) + .ToDictionary( + pf => pf, + pf => properties.Single(pi => pi.Name.Equals(pf.ToString(), StringComparison.OrdinalIgnoreCase)).GetValue(null, null) as ImageFormat + ); + } + + internal XLPicture(IXLWorksheet worksheet, Stream stream) + : this(worksheet) + { + if (stream == null) throw new ArgumentNullException(nameof(stream)); + + this.ImageStream = new MemoryStream(); + { + stream.Position = 0; + stream.CopyTo(ImageStream); + ImageStream.Seek(0, SeekOrigin.Begin); + + using (var bitmap = new Bitmap(ImageStream)) + { + if (FormatMap.Values.Select(f => f.Guid).Contains(bitmap.RawFormat.Guid)) + this.Format = FormatMap.Single(f => f.Value.Guid.Equals(bitmap.RawFormat.Guid)).Key; + + DeduceDimensionsFromBitmap(bitmap); + } + ImageStream.Seek(0, SeekOrigin.Begin); + } + } + internal XLPicture(IXLWorksheet worksheet, Stream stream, XLPictureFormat format) : this(worksheet) { @@ -31,9 +65,11 @@ using (var bitmap = new Bitmap(ImageStream)) { - var expectedFormat = typeof(System.Drawing.Imaging.ImageFormat).GetProperty(this.Format.ToString()).GetValue(null, null) as System.Drawing.Imaging.ImageFormat; - if (expectedFormat.Guid != bitmap.RawFormat.Guid) - throw new ArgumentException("The picture format in the stream and the parameter don't match"); + if (FormatMap.ContainsKey(this.Format)) + { + if (FormatMap[this.Format].Guid != bitmap.RawFormat.Guid) + throw new ArgumentException("The picture format in the stream and the parameter don't match"); + } DeduceDimensionsFromBitmap(bitmap); } @@ -50,13 +86,11 @@ ImageStream.Seek(0, SeekOrigin.Begin); DeduceDimensionsFromBitmap(bitmap); - var formats = typeof(ImageFormat).GetProperties(BindingFlags.Static | BindingFlags.Public) - .Where(pi => (pi.GetValue(null, null) as ImageFormat).Guid.Equals(bitmap.RawFormat.Guid)); - + var formats = FormatMap.Where(f => f.Value.Guid.Equals(bitmap.RawFormat.Guid)); if (!formats.Any() || formats.Count() > 1) throw new ArgumentException("Unsupported or unknown image format in bitmap"); - this.Format = Enum.Parse(typeof(XLPictureFormat), formats.Single().Name, true).CastTo(); + this.Format = formats.Single().Key; } private XLPicture(IXLWorksheet worksheet) @@ -114,8 +148,11 @@ } public String Name { get; set; } + public Int32 OriginalHeight { get; private set; } + public Int32 OriginalWidth { get; private set; } + public XLPicturePlacement Placement { get; set; } public Int32 Top @@ -164,6 +201,8 @@ internal IDictionary Markers { get; private set; } + internal String RelId { get; set; } + public void Dispose() { this.ImageStream.Dispose(); @@ -256,6 +295,28 @@ return this; } + private static ImageFormat FromMimeType(string mimeType) + { + var guid = ImageCodecInfo.GetImageDecoders().FirstOrDefault(c => c.MimeType.Equals(mimeType, StringComparison.OrdinalIgnoreCase))?.FormatID; + if (!guid.HasValue) return null; + var property = typeof(System.Drawing.Imaging.ImageFormat).GetProperties(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(pi => (pi.GetValue(null, null) as ImageFormat).Guid.Equals(guid.Value)); + + if (property == null) return null; + return (property.GetValue(null, null) as ImageFormat); + } + + private static string GetMimeType(Image i) + { + var imgguid = i.RawFormat.Guid; + foreach (ImageCodecInfo codec in ImageCodecInfo.GetImageDecoders()) + { + if (codec.FormatID == imgguid) + return codec.MimeType; + } + return "image/unknown"; + } + private void DeduceDimensionsFromBitmap(Bitmap bitmap) { this.OriginalWidth = bitmap.Width; @@ -265,4 +326,4 @@ this.height = bitmap.Height; } } -} +} \ No newline at end of file diff --git a/ClosedXML/Excel/IXLWorksheet.cs b/ClosedXML/Excel/IXLWorksheet.cs index a4ce4e0..cee3feb 100644 --- a/ClosedXML/Excel/IXLWorksheet.cs +++ b/ClosedXML/Excel/IXLWorksheet.cs @@ -435,6 +435,10 @@ IList Pictures { get; } + Drawings.IXLPicture AddPicture(Stream stream); + + Drawings.IXLPicture AddPicture(Stream stream, String name); + Drawings.IXLPicture AddPicture(Stream stream, XLPictureFormat format); Drawings.IXLPicture AddPicture(Stream stream, XLPictureFormat format, String name); diff --git a/ClosedXML/Excel/XLWorkbook_ImageHandling.cs b/ClosedXML/Excel/XLWorkbook_ImageHandling.cs new file mode 100644 index 0000000..0533e1e --- /dev/null +++ b/ClosedXML/Excel/XLWorkbook_ImageHandling.cs @@ -0,0 +1,49 @@ +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Drawing.Spreadsheet; +using DocumentFormat.OpenXml.Packaging; +using System; +using System.Linq; + +using Xdr = DocumentFormat.OpenXml.Drawing.Spreadsheet; + +namespace ClosedXML.Excel +{ + public partial class XLWorkbook + { + public static OpenXmlElement GetAnchorFromImageId(WorksheetPart worksheetPart, string relId) + { + var drawingsPart = worksheetPart.DrawingsPart; + var matchingAnchor = drawingsPart.WorksheetDrawing + .Where(wsdr => wsdr.Descendants() + .Any(x => x?.Blip?.Embed?.Value.Equals(relId) ?? false) + ); + + if (!matchingAnchor.Any()) + return null; + else + return matchingAnchor.First(); + } + + public static OpenXmlElement GetAnchorFromImageIndex(WorksheetPart worksheetPart, Int32 index) + { + var drawingsPart = worksheetPart.DrawingsPart; + var matchingAnchor = drawingsPart.WorksheetDrawing + .Where(wsdr => wsdr.Descendants() + .Any(x => x.Id.Value.Equals(Convert.ToUInt32(index + 1))) + ); + + if (!matchingAnchor.Any()) + return null; + else + return matchingAnchor.First(); + } + + public static NonVisualDrawingProperties GetPropertiesFromImageIndex(WorksheetPart worksheetPart, Int32 index) + { + var drawingsPart = worksheetPart.DrawingsPart; + return drawingsPart.WorksheetDrawing + .Descendants() + .FirstOrDefault(x => x.Id.Value.Equals(Convert.ToUInt32(index + 1))); + } + } +} diff --git a/ClosedXML/Excel/XLWorkbook_Load.cs b/ClosedXML/Excel/XLWorkbook_Load.cs index 76d38b6..e74d134 100644 --- a/ClosedXML/Excel/XLWorkbook_Load.cs +++ b/ClosedXML/Excel/XLWorkbook_Load.cs @@ -87,7 +87,6 @@ } } - var wbProps = dSpreadsheet.WorkbookPart.Workbook.WorkbookProperties; Use1904DateSystem = wbProps != null && wbProps.Date1904 != null && wbProps.Date1904.Value; @@ -340,7 +339,6 @@ #endregion - LoadDrawings(wsPart, ws); #region LoadComments @@ -418,8 +416,6 @@ } LoadDefinedNames(workbook); - - #region Pivot tables // Delay loading of pivot tables until all sheets have been loaded @@ -624,72 +620,70 @@ #endregion } - private void LoadDrawings(WorksheetPart wsPart, XLWorksheet ws) + private void LoadDrawings(WorksheetPart wsPart, IXLWorksheet ws) { if (wsPart.DrawingsPart != null) { var drawingsPart = wsPart.DrawingsPart; - var imageParts = drawingsPart.ImageParts; - var wsdrawing = drawingsPart.WorksheetDrawing; - foreach (var rel in drawingsPart.Parts.Where(x => x.OpenXmlPart is ImagePart)) + var imageParts = drawingsPart.GetPartsOfType(); + for (int i = 0; i < imageParts.Count(); i++) { - var imgId = rel.RelationshipId; - var image = rel.OpenXmlPart as ImagePart; - var drawing = new XLPicture(); - drawing.Type = image.ContentType.Replace("image/", ""); - drawing.ImageStream = image.GetStream(); - var nonVis = wsdrawing - .Descendants() - .FirstOrDefault(x => x.Name.Value == imgId); - - drawing.Name = nonVis.Name.Value; - - var anchor = nonVis.Parent.Parent.Parent; - if (anchor is Xdr.OneCellAnchor) + var imagePart = imageParts.ElementAt(i); + var imgId = drawingsPart.GetIdOfPart(imagePart); + using (var stream = imagePart.GetStream()) { - var oneCellAnchor = anchor as Xdr.OneCellAnchor; - drawing.IsAbsolute = false; - var from = LoadMarker(oneCellAnchor.FromMarker); - drawing.OffsetX = (int)from.ColumnOffset; - drawing.OffsetY = (int)from.RowOffset; - drawing.AddMarker(from); + var anchor = GetAnchorFromImageId(wsPart, imgId); + var vsdp = GetPropertiesFromImageIndex(wsPart, i); + + var picture = ws.AddPicture(stream, vsdp.Name) as XLPicture; + picture.RelId = imgId; + + Xdr.ShapeProperties spPr = anchor.Descendants().First(); + picture.Placement = XLPicturePlacement.FreeFloating; + picture.Width = ConvertFromEnglishMetricUnits(spPr.Transform2D.Extents.Cx, GraphicsUtils.Graphics.DpiX); + picture.Height = ConvertFromEnglishMetricUnits(spPr.Transform2D.Extents.Cy, GraphicsUtils.Graphics.DpiY); + + if (anchor is Xdr.OneCellAnchor) + { + var oneCellAnchor = anchor as Xdr.OneCellAnchor; + var from = LoadMarker(ws, oneCellAnchor.FromMarker); + picture.MoveTo(from.Address, from.Offset); + } + else if (anchor is Xdr.TwoCellAnchor) + { + var twoCellAnchor = anchor as Xdr.TwoCellAnchor; + var from = LoadMarker(ws, twoCellAnchor.FromMarker); + var to = LoadMarker(ws, twoCellAnchor.ToMarker); + picture.MoveTo(from.Address, from.Offset, to.Address, to.Offset); + } + else if (anchor is Xdr.AbsoluteAnchor) + { + var absoluteAnchor = anchor as Xdr.AbsoluteAnchor; + picture.MoveTo( + ConvertFromEnglishMetricUnits(absoluteAnchor.Position.X.Value, GraphicsUtils.Graphics.DpiX), + ConvertFromEnglishMetricUnits(absoluteAnchor.Position.Y.Value, GraphicsUtils.Graphics.DpiY) + ); + } } - else if (anchor is Xdr.TwoCellAnchor) - { - var twoCellAnchor = anchor as Xdr.TwoCellAnchor; - drawing.IsAbsolute = false; - - var from = LoadMarker(twoCellAnchor.FromMarker); - var to = LoadMarker(twoCellAnchor.ToMarker); - - drawing.OffsetX = (int)from.ColumnOffset; - drawing.OffsetY = (int)from.RowOffset; - drawing.AddMarker(from); - drawing.AddMarker(to); - - } - else if (anchor is Xdr.AbsoluteAnchor) - { - var absAnchor = anchor as Xdr.AbsoluteAnchor; - drawing.IsAbsolute = true; - drawing.RawOffsetX = absAnchor.Position.X; - drawing.RawOffsetY = absAnchor.Position.Y; - } - - ws.Pictures.Add(drawing); } } } - private XLMarker LoadMarker(Xdr.MarkerType marker) + private static Int32 ConvertFromEnglishMetricUnits(long emu, float resolution) { - var result = new XLMarker(); - result.ColumnId = Convert.ToInt32(marker.ColumnId.InnerText) + 1; - result.ColumnOffset = Convert.ToInt32(marker.ColumnOffset.InnerText); - result.RowId = Convert.ToInt32(marker.RowId.InnerText) + 1; - result.RowOffset = Convert.ToInt32(marker.RowOffset.InnerText); - return result; + return Convert.ToInt32(emu * resolution / 914400); + } + + private static IXLMarker LoadMarker(IXLWorksheet ws, Xdr.MarkerType marker) + { + return new XLMarker( + ws.Cell(Convert.ToInt32(marker.RowId.InnerText) + 1, Convert.ToInt32(marker.ColumnId.InnerText) + 1).Address, + new Point( + ConvertFromEnglishMetricUnits(Convert.ToInt32(marker.ColumnOffset.InnerText), GraphicsUtils.Graphics.DpiX), + ConvertFromEnglishMetricUnits(Convert.ToInt32(marker.RowOffset.InnerText), GraphicsUtils.Graphics.DpiY) + ) + ); } #region Comment Helpers diff --git a/ClosedXML/Excel/XLWorkbook_Save.cs b/ClosedXML/Excel/XLWorkbook_Save.cs index 457ecbc..886655c 100644 --- a/ClosedXML/Excel/XLWorkbook_Save.cs +++ b/ClosedXML/Excel/XLWorkbook_Save.cs @@ -1,3 +1,4 @@ +using ClosedXML.Extensions; using ClosedXML.Utils; using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.CustomProperties; @@ -2509,9 +2510,9 @@ // http://polymathprogrammer.com/2009/10/22/english-metric-units-and-open-xml/ // http://archive.oreilly.com/pub/post/what_is_an_emu.html // https://en.wikipedia.org/wiki/Office_Open_XML_file_formats#DrawingML - private static long ConvertToEnglishMetricUnits(long pixels, float resolution) + private static Int64 ConvertToEnglishMetricUnits(Int32 pixels, Double resolution) { - return (long)(914400 * pixels / resolution); + return Convert.ToInt64(914400 * pixels / resolution); } private static void AddPictureAnchor(WorksheetPart worksheetPart, Drawings.IXLPicture picture, SaveContext context) @@ -2533,7 +2534,12 @@ worksheetDrawing.AddNamespaceDeclaration("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"); ///////// - var imagePart = drawingsPart.AddImagePart(pic.Format.ToOpenXml(), context.RelIdGenerator.GetNext(RelType.Workbook)); + // Overwrite actual image binary data + ImagePart imagePart; + if (drawingsPart.HasPartWithId(pic.RelId)) + imagePart = drawingsPart.GetPartById(pic.RelId) as ImagePart; + else + imagePart = drawingsPart.AddImagePart(pic.Format.ToOpenXml(), context.RelIdGenerator.GetNext(RelType.Workbook)); using (var stream = new MemoryStream()) { @@ -2541,14 +2547,17 @@ stream.Seek(0, SeekOrigin.Begin); imagePart.FeedData(stream); } + ///////// + + // Clear current anchors + var existingAnchor = GetAnchorFromImageId(worksheetPart, pic.RelId); + if (existingAnchor != null) + worksheetDrawing.RemoveChild(existingAnchor); var extentsCx = ConvertToEnglishMetricUnits(pic.Width, GraphicsUtils.Graphics.DpiX); var extentsCy = ConvertToEnglishMetricUnits(pic.Height, GraphicsUtils.Graphics.DpiY); - var nvps = worksheetDrawing.Descendants(); - var nvpId = nvps.Any() ? - (UInt32Value)worksheetDrawing.Descendants().Max(p => p.Id.Value) + 1 : - 1U; + var nvpId = Convert.ToUInt32(worksheetDrawing.DrawingsPart.ImageParts.ToList().IndexOf(imagePart) + 1); Xdr.FromMarker fMark; Xdr.ToMarker tMark; @@ -4751,7 +4760,7 @@ AddPictureAnchor(worksheetPart, pic, context); } - if (xlWorksheet.Pictures.Any()) + if (xlWorksheet.Pictures.Any() && !worksheetPart.Worksheet.OfType().Any()) { var worksheetDrawing = new Drawing { Id = worksheetPart.GetIdOfPart(worksheetPart.DrawingsPart) }; worksheetDrawing.AddNamespaceDeclaration("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"); @@ -4986,4 +4995,4 @@ #endregion GenerateWorksheetPartContent } -} +} \ No newline at end of file diff --git a/ClosedXML/Excel/XLWorksheet.cs b/ClosedXML/Excel/XLWorksheet.cs index 9263993..ade653e 100644 --- a/ClosedXML/Excel/XLWorksheet.cs +++ b/ClosedXML/Excel/XLWorksheet.cs @@ -577,6 +577,30 @@ Internals.MergedRanges.ForEach( kp => targetSheet.Internals.MergedRanges.Add(targetSheet.Range(kp.RangeAddress.ToString()))); + foreach (var picture in Pictures) + { + var newPic = targetSheet.AddPicture(picture.ImageStream, picture.Format, picture.Name) + .WithPlacement(picture.Placement) + .WithSize(picture.Width, picture.Height); + + switch (picture.Placement) + { + case XLPicturePlacement.FreeFloating: + newPic.MoveTo(picture.Left, picture.Top); + break; + case XLPicturePlacement.Move: + var newAddress = new XLAddress(targetSheet, picture.TopLeftCellAddress.RowNumber, picture.TopLeftCellAddress.ColumnNumber, false, false); + newPic.MoveTo(newAddress, picture.GetOffset(XLMarkerPosition.TopLeft)); + break; + case XLPicturePlacement.MoveAndSize: + var newFromAddress = new XLAddress(targetSheet, picture.TopLeftCellAddress.RowNumber, picture.TopLeftCellAddress.ColumnNumber, false, false); + var newToAddress = new XLAddress(targetSheet, picture.BottomRightCellAddress.RowNumber, picture.BottomRightCellAddress.ColumnNumber, false, false); + + newPic.MoveTo(newFromAddress, picture.GetOffset(XLMarkerPosition.TopLeft), newToAddress, picture.GetOffset(XLMarkerPosition.BottomRight)); + break; + } + } + foreach (IXLNamedRange r in NamedRanges) { var ranges = new XLRanges(); @@ -1507,6 +1531,21 @@ return $"Picture {pictureNumber}"; } + public IXLPicture AddPicture(Stream stream) + { + var picture = new XLPicture(this, stream); + Pictures.Add(picture); + picture.Name = GetNextPictureName(); + return picture; + } + + public IXLPicture AddPicture(Stream stream, string name) + { + var picture = AddPicture(stream); + picture.Name = name; + return picture; + } + public Drawings.IXLPicture AddPicture(Stream stream, XLPictureFormat format) { var picture = new XLPicture(this, stream, format); @@ -1554,5 +1593,6 @@ picture.Name = name; return picture; } + } } diff --git a/ClosedXML/Extensions/OpenXmlPartContainerExtensions.cs b/ClosedXML/Extensions/OpenXmlPartContainerExtensions.cs new file mode 100644 index 0000000..db2424f --- /dev/null +++ b/ClosedXML/Extensions/OpenXmlPartContainerExtensions.cs @@ -0,0 +1,14 @@ +using DocumentFormat.OpenXml.Packaging; +using System; +using System.Linq; + +namespace ClosedXML.Extensions +{ + internal static class OpenXmlPartContainerExtensions + { + public static Boolean HasPartWithId(this OpenXmlPartContainer container, String relId) + { + return container.Parts.Any(p => p.RelationshipId.Equals(relId)); + } + } +} diff --git a/ClosedXML_Tests/Excel/Loading/LoadingTests.cs b/ClosedXML_Tests/Excel/Loading/LoadingTests.cs index f02a83e..754295a 100644 --- a/ClosedXML_Tests/Excel/Loading/LoadingTests.cs +++ b/ClosedXML_Tests/Excel/Loading/LoadingTests.cs @@ -1,5 +1,5 @@ using ClosedXML.Excel; -using DocumentFormat.OpenXml.Drawing.Spreadsheet; +using ClosedXML.Excel.Drawings; using NUnit.Framework; using System.Collections.Generic; using System.IO; @@ -128,12 +128,12 @@ { var ws = wb.Worksheets.First(); Assert.AreEqual(2, ws.Pictures.Count); - Assert.IsTrue(ws.Pictures[0].IsAbsolute); - Assert.AreEqual(1, ws.Pictures[1].GetMarkers().Count, "Expected image to use a OneCellAnchor."); + Assert.AreEqual(XLPicturePlacement.FreeFloating, ws.Pictures.First().Placement); + Assert.AreEqual(XLPicturePlacement.Move, ws.Pictures.Skip(1).First().Placement); var ws2 = wb.Worksheets.Skip(1).First(); Assert.AreEqual(1, ws2.Pictures.Count); - Assert.AreEqual(2, ws2.Pictures[0].GetMarkers().Count, "Expected image to use a TwoCellAnchor."); + Assert.AreEqual(XLPicturePlacement.MoveAndSize, ws2.Pictures.First().Placement); } } @@ -145,11 +145,11 @@ { var ws = wb.Worksheets.First(); Assert.AreEqual(1, ws.Pictures.Count); - Assert.AreEqual("jpeg", ws.Pictures[0].Type); + Assert.AreEqual(XLPictureFormat.Jpeg, ws.Pictures.First().Format); var ws2 = wb.Worksheets.Skip(1).First(); Assert.AreEqual(1, ws2.Pictures.Count); - Assert.AreEqual("png", ws2.Pictures[0].Type); + Assert.AreEqual(XLPictureFormat.Png, ws2.Pictures.First().Format); } }