diff --git a/.gitattributes b/.gitattributes index 505c036..68cc179 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,7 +4,7 @@ # Explicitly declare text files we want to always be normalized and converted # to native line endings on checkout. -*.cs text diff=csharp eol=lf +*.cs text diff=csharp eol=crlf # Declare files that will always have CRLF line endings on checkout. *.sln text eol=crlf diff --git a/ClosedXML/ClosedXML.csproj b/ClosedXML/ClosedXML.csproj index 3a8443f..ef9ef98 100644 --- a/ClosedXML/ClosedXML.csproj +++ b/ClosedXML/ClosedXML.csproj @@ -72,11 +72,13 @@ + + diff --git a/ClosedXML/Excel/Drawings/IXLPicture.cs b/ClosedXML/Excel/Drawings/IXLPicture.cs index d059776..2c2f750 100644 --- a/ClosedXML/Excel/Drawings/IXLPicture.cs +++ b/ClosedXML/Excel/Drawings/IXLPicture.cs @@ -15,17 +15,32 @@ XLPictureFormat Format { get; } Int32 Height { get; set; } + MemoryStream ImageStream { get; } + Int32 Left { get; set; } + String Name { get; set; } + Int32 OriginalHeight { get; } + Int32 OriginalWidth { get; } + XLPicturePlacement Placement { get; set; } + Int32 Top { get; set; } + IXLAddress TopLeftCellAddress { get; } + Int32 Width { get; set; } + IXLWorksheet Worksheet { get; } + /// + /// Deletes this picture. + /// + void Delete(); + Point GetOffset(XLMarkerPosition position); IXLPicture MoveTo(Int32 left, Int32 top); diff --git a/ClosedXML/Excel/Drawings/IXLPictures.cs b/ClosedXML/Excel/Drawings/IXLPictures.cs new file mode 100644 index 0000000..ff449cd --- /dev/null +++ b/ClosedXML/Excel/Drawings/IXLPictures.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; + +namespace ClosedXML.Excel.Drawings +{ + public interface IXLPictures : IEnumerable + { + int Count { get; } + + IXLPicture Add(Stream stream); + + IXLPicture Add(Stream stream, String name); + + IXLPicture Add(Stream stream, XLPictureFormat format); + + IXLPicture Add(Stream stream, XLPictureFormat format, String name); + + IXLPicture Add(Bitmap bitmap); + + IXLPicture Add(Bitmap bitmap, String name); + + IXLPicture Add(String imageFile); + + IXLPicture Add(String imageFile, String name); + + void Delete(String pictureName); + + void Delete(IXLPicture picture); + + IXLPicture Picture(String pictureName); + + bool TryGetPicture(string pictureName, out IXLPicture picture); + } +} diff --git a/ClosedXML/Excel/Drawings/XLPicture.cs b/ClosedXML/Excel/Drawings/XLPicture.cs index c87d54c..86d45ae 100644 --- a/ClosedXML/Excel/Drawings/XLPicture.cs +++ b/ClosedXML/Excel/Drawings/XLPicture.cs @@ -12,9 +12,11 @@ [DebuggerDisplay("{Name}")] internal class XLPicture : IXLPicture { + private const String InvalidNameChars = @":\/?*[]"; private static IDictionary FormatMap; private readonly IXLWorksheet _worksheet; private Int32 height; + private String name = string.Empty; private Int32 width; static XLPicture() @@ -147,7 +149,28 @@ } } - public String Name { get; set; } + public String Name + { + get { return name; } + set + { + if (name == value) return; + + if (value.IndexOfAny(InvalidNameChars.ToCharArray()) != -1) + throw new ArgumentException($"Picture names cannot contain any of the following characters: {InvalidNameChars}"); + + if (XLHelper.IsNullOrWhiteSpace(value)) + throw new ArgumentException("Picture names cannot be empty"); + + if (value.Length > 31) + throw new ArgumentException("Picture names cannot be more than 31 characters"); + + if ((_worksheet.Pictures.FirstOrDefault(p => p.Name.Equals(value, StringComparison.OrdinalIgnoreCase)) ?? this) != this) + throw new ArgumentException($"The picture name '{value}' already exists."); + + name = value; + } + } public Int32 OriginalHeight { get; private set; } @@ -203,6 +226,11 @@ internal String RelId { get; set; } + public void Delete() + { + Worksheet.Pictures.Delete(this.Name); + } + public void Dispose() { this.ImageStream.Dispose(); @@ -326,4 +354,4 @@ this.height = bitmap.Height; } } -} \ No newline at end of file +} diff --git a/ClosedXML/Excel/Drawings/XLPictures.cs b/ClosedXML/Excel/Drawings/XLPictures.cs new file mode 100644 index 0000000..3381309 --- /dev/null +++ b/ClosedXML/Excel/Drawings/XLPictures.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; + +namespace ClosedXML.Excel.Drawings +{ + internal class XLPictures : IXLPictures, IEnumerable + { + private readonly List _pictures = new List(); + private readonly XLWorksheet _worksheet; + + public XLPictures(XLWorksheet worksheet) + { + _worksheet = worksheet; + } + + public int Count + { + [DebuggerStepThrough] + get { return _pictures.Count; } + } + + public IXLPicture Add(Stream stream) + { + var picture = new XLPicture(_worksheet, stream); + _pictures.Add(picture); + picture.Name = GetNextPictureName(); + return picture; + } + + public IXLPicture Add(Stream stream, string name) + { + var picture = Add(stream); + picture.Name = name; + return picture; + } + + public Drawings.IXLPicture Add(Stream stream, XLPictureFormat format) + { + var picture = new XLPicture(_worksheet, stream, format); + _pictures.Add(picture); + picture.Name = GetNextPictureName(); + return picture; + } + + public IXLPicture Add(Stream stream, XLPictureFormat format, string name) + { + var picture = Add(stream, format); + picture.Name = name; + return picture; + } + + public IXLPicture Add(Bitmap bitmap) + { + var picture = new XLPicture(_worksheet, bitmap); + _pictures.Add(picture); + picture.Name = GetNextPictureName(); + return picture; + } + + public IXLPicture Add(Bitmap bitmap, string name) + { + var picture = Add(bitmap); + picture.Name = name; + return picture; + } + + public IXLPicture Add(string imageFile) + { + using (var bitmap = Image.FromFile(imageFile) as Bitmap) + { + var picture = new XLPicture(_worksheet, bitmap); + _pictures.Add(picture); + picture.Name = GetNextPictureName(); + return picture; + } + } + + public IXLPicture Add(string imageFile, string name) + { + var picture = Add(imageFile); + picture.Name = name; + return picture; + } + + public void Delete(IXLPicture picture) + { + Delete(picture.Name); + } + + public void Delete(string pictureName) + { + _pictures.RemoveAll(picture => picture.Name.Equals(pictureName, StringComparison.OrdinalIgnoreCase)); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _pictures.Cast().GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + return ((IEnumerable)_pictures).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IXLPicture Picture(string pictureName) + { + IXLPicture p; + + if (TryGetPicture(pictureName, out p)) + return p; + + throw new ArgumentException($"There isn't a picture named '{pictureName}'."); + } + + public bool TryGetPicture(string pictureName, out IXLPicture picture) + { + var matches = _pictures.Where(p => p.Name.Equals(pictureName, StringComparison.OrdinalIgnoreCase)); + if (matches.Any()) + { + picture = matches.Single(); + return true; + } + picture = null; + return false; + } + + private String GetNextPictureName() + { + var pictureNumber = this.Count; + while (_pictures.Any(p => p.Name == $"Picture {pictureNumber}")) + { + pictureNumber++; + } + return $"Picture {pictureNumber}"; + } + } +} diff --git a/ClosedXML/Excel/IXLWorksheet.cs b/ClosedXML/Excel/IXLWorksheet.cs index cee3feb..7117744 100644 --- a/ClosedXML/Excel/IXLWorksheet.cs +++ b/ClosedXML/Excel/IXLWorksheet.cs @@ -1,6 +1,5 @@ using ClosedXML.Excel.Drawings; using System; -using System.Collections.Generic; using System.Drawing; using System.IO; @@ -433,22 +432,22 @@ String Author { get; set; } - IList Pictures { get; } + IXLPictures Pictures { get; } - Drawings.IXLPicture AddPicture(Stream stream); + IXLPicture AddPicture(Stream stream); - Drawings.IXLPicture AddPicture(Stream stream, String name); + IXLPicture AddPicture(Stream stream, String name); - Drawings.IXLPicture AddPicture(Stream stream, XLPictureFormat format); + IXLPicture AddPicture(Stream stream, XLPictureFormat format); - Drawings.IXLPicture AddPicture(Stream stream, XLPictureFormat format, String name); + IXLPicture AddPicture(Stream stream, XLPictureFormat format, String name); - Drawings.IXLPicture AddPicture(Bitmap bitmap); + IXLPicture AddPicture(Bitmap bitmap); - Drawings.IXLPicture AddPicture(Bitmap bitmap, String name); + IXLPicture AddPicture(Bitmap bitmap, String name); - Drawings.IXLPicture AddPicture(String imageFile); + IXLPicture AddPicture(String imageFile); - Drawings.IXLPicture AddPicture(String imageFile, String name); + IXLPicture AddPicture(String imageFile, String name); } -} +} \ No newline at end of file diff --git a/ClosedXML/Excel/XLWorksheet.cs b/ClosedXML/Excel/XLWorksheet.cs index ade653e..ceb8cc7 100644 --- a/ClosedXML/Excel/XLWorksheet.cs +++ b/ClosedXML/Excel/XLWorksheet.cs @@ -50,6 +50,7 @@ RangeAddress.FirstAddress.Worksheet = this; RangeAddress.LastAddress.Worksheet = this; + Pictures = new XLPictures(this); NamedRanges = new XLNamedRanges(workbook); SheetView = new XLSheetView(); Tables = new XLTables(); @@ -588,10 +589,12 @@ 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); @@ -990,8 +993,7 @@ Internals.Dispose(); - foreach (var picture in this.Pictures) - picture.Dispose(); + this.Pictures.ForEach(p => p.Dispose()); base.Dispose(); } @@ -1519,80 +1521,46 @@ public String Author { get; set; } - public IList Pictures { get; private set; } = new List(); - - private String GetNextPictureName() - { - var pictureNumber = this.Pictures.Count; - while (Pictures.Any(p => p.Name == $"Picture {pictureNumber}")) - { - pictureNumber++; - } - return $"Picture {pictureNumber}"; - } + public IXLPictures Pictures { get; private set; } public IXLPicture AddPicture(Stream stream) { - var picture = new XLPicture(this, stream); - Pictures.Add(picture); - picture.Name = GetNextPictureName(); - return picture; + return Pictures.Add(stream); } public IXLPicture AddPicture(Stream stream, string name) { - var picture = AddPicture(stream); - picture.Name = name; - return picture; + return Pictures.Add(stream, name); } public Drawings.IXLPicture AddPicture(Stream stream, XLPictureFormat format) { - var picture = new XLPicture(this, stream, format); - Pictures.Add(picture); - picture.Name = GetNextPictureName(); - return picture; + return Pictures.Add(stream, format); } public IXLPicture AddPicture(Stream stream, XLPictureFormat format, string name) { - var picture = AddPicture(stream, format); - picture.Name = name; - return picture; + return Pictures.Add(stream, format, name); } public IXLPicture AddPicture(Bitmap bitmap) { - var picture = new XLPicture(this, bitmap); - Pictures.Add(picture); - picture.Name = GetNextPictureName(); - return picture; + return Pictures.Add(bitmap); } public IXLPicture AddPicture(Bitmap bitmap, string name) { - var picture = AddPicture(bitmap); - this.Name = name; - return picture; + return Pictures.Add(bitmap, name); } public IXLPicture AddPicture(string imageFile) { - using (var bitmap = Image.FromFile(imageFile) as Bitmap) - { - var picture = new XLPicture(this, bitmap); - Pictures.Add(picture); - picture.Name = GetNextPictureName(); - return picture; - } + return Pictures.Add(imageFile); } public IXLPicture AddPicture(string imageFile, string name) { - var picture = AddPicture(imageFile); - picture.Name = name; - return picture; + return Pictures.Add(imageFile, name); } - } } diff --git a/ClosedXML/Excel/XLWorksheetInternals.cs b/ClosedXML/Excel/XLWorksheetInternals.cs index 4fc467e..04a73f7 100644 --- a/ClosedXML/Excel/XLWorksheetInternals.cs +++ b/ClosedXML/Excel/XLWorksheetInternals.cs @@ -1,10 +1,11 @@ using System; + namespace ClosedXML.Excel { - internal class XLWorksheetInternals: IDisposable + internal class XLWorksheetInternals : IDisposable { public XLWorksheetInternals( - XLCellsCollection cellsCollection, + XLCellsCollection cellsCollection, XLColumnsCollection columnsCollection, XLRowsCollection rowsCollection, XLRanges mergedRanges diff --git a/ClosedXML/Excel/XLWorksheets.cs b/ClosedXML/Excel/XLWorksheets.cs index 057d3b7..ea22509 100644 --- a/ClosedXML/Excel/XLWorksheets.cs +++ b/ClosedXML/Excel/XLWorksheets.cs @@ -14,7 +14,7 @@ private readonly XLWorkbook _workbook; private readonly Dictionary _worksheets = new Dictionary(); - #endregion + #endregion Constructor public HashSet Deleted = new HashSet(); @@ -25,7 +25,7 @@ _workbook = workbook; } - #endregion + #endregion Constructor #region IEnumerable Members @@ -34,7 +34,7 @@ return ((IEnumerable)_worksheets.Values).GetEnumerator(); } - #endregion + #endregion IEnumerable Members #region IXLWorksheets Members @@ -166,7 +166,7 @@ Add(t); } - #endregion + #endregion IXLWorksheets Members public void Rename(String oldSheetName, String newSheetName) { diff --git a/ClosedXML/Extensions.cs b/ClosedXML/Extensions.cs index 7483968..747961f 100644 --- a/ClosedXML/Extensions.cs +++ b/ClosedXML/Extensions.cs @@ -311,6 +311,18 @@ } } + public static class ListExtensions + { + public static void RemoveAll(this IList list, Func predicate) + { + var indices = list.Where(item => predicate(item)).Select((item, i) => i).OrderByDescending(i => i).ToList(); + foreach (var i in indices) + { + list.RemoveAt(i); + } + } + } + public static class DoubleValueExtensions { public static DoubleValue SaveRound(this DoubleValue value) diff --git a/ClosedXML_Examples/ImageHandling/ImageAnchors.cs b/ClosedXML_Examples/ImageHandling/ImageAnchors.cs index b71f37d..b20b42d 100644 --- a/ClosedXML_Examples/ImageHandling/ImageAnchors.cs +++ b/ClosedXML_Examples/ImageHandling/ImageAnchors.cs @@ -70,6 +70,11 @@ ws.AddPicture(fs, XLPictureFormat.Jpeg) .MoveTo(100, 100) .WithPlacement(XLPicturePlacement.FreeFloating); + + // Add and delete picture immediately + ws.AddPicture(fs, XLPictureFormat.Jpeg) + .MoveTo(100, 600) + .Delete(); } wb.SaveAs(filePath); diff --git a/ClosedXML_Tests/Excel/ImageHandling/PictureTests.cs b/ClosedXML_Tests/Excel/ImageHandling/PictureTests.cs index 3750ca3..b0ba0d6 100644 --- a/ClosedXML_Tests/Excel/ImageHandling/PictureTests.cs +++ b/ClosedXML_Tests/Excel/ImageHandling/PictureTests.cs @@ -1,6 +1,7 @@ using ClosedXML.Excel; using ClosedXML.Excel.Drawings; using NUnit.Framework; +using System; using System.Drawing; using System.IO; using System.Linq; @@ -194,5 +195,41 @@ Assert.AreEqual(2, copy.Pictures.Count); } } + + [Test] + public void CanDeletePictures() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\ImageHandling\ImageAnchors.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheets.First(); + ws.Pictures.Delete(ws.Pictures.First()); + + var pictureName = ws.Pictures.First().Name; + ws.Pictures.Delete(pictureName); + } + } + + [Test] + public void PictureRenameTests() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\ImageHandling\ImageAnchors.xlsx"))) + using (var wb = new XLWorkbook(stream)) + { + var ws = wb.Worksheet("Images3"); + var picture = ws.Pictures.First(); + Assert.AreEqual("Picture 1", picture.Name); + + picture.Name = "picture 1"; + picture.Name = "pICture 1"; + picture.Name = "Picture 1"; + + picture = ws.Pictures.Last(); + picture.Name = "new name"; + + Assert.Throws(() => picture.Name = "Picture 1"); + Assert.Throws(() => picture.Name = "picTURE 1"); + } + } } }