diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3691a4d17..7189aaca5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,7 @@ on: jobs: build: - # Windows: the suite exercises System.Drawing, which only works there. On Linux - # DTS_FailingTests.DeleteWorksheetWithReferencedImage hangs the test host. - runs-on: windows-latest + runs-on: ubuntu-latest env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} steps: @@ -31,22 +29,13 @@ jobs: - name: Build run: dotnet build --configuration Release --no-restore - # Unit tests only (no live services). Excluded by name: - # - DateFunctionsWorkWithDifferentCultureDateFormats_*: throws an unhandled - # InvalidCastException on a worker thread, which crashes the test host and - # aborts the whole run (pre-existing). - # - Eleven tests that fail on unmodified main: RunDrawingTests, Issuer246, - # RunWorksheetTests, SetBackground, RCRelativeToABToR1C1_2, SimpleRelativeR1C1, - # FullTwoColumn, FullColumn, FullTwoRow, FullRow, OperatorsActingOnNumericStrings. - # - Issue333: asserts the ISO text "2019-03-07" for the built-in mm-dd-yy format, which - # is rendered in the current culture; the en-US runner gives "3/7/2019". + # Unit tests only (no live services). - name: Test with coverage run: > dotnet test PanoramicData.EPPlus.Test/PanoramicData.EPPlus.Test.csproj --configuration Release --no-build --collect "Code Coverage;Format=Cobertura" --results-directory ./coverage --blame-hang-timeout 5m - --filter "FullyQualifiedName!~DateFunctionsWorkWithDifferentCultureDateFormats&Name!=RunDrawingTests&Name!=Issuer246&Name!=RunWorksheetTests&Name!=SetBackground&Name!=RCRelativeToABToR1C1_2&Name!=SimpleRelativeR1C1&Name!=FullTwoColumn&Name!=FullColumn&Name!=FullTwoRow&Name!=FullRow&Name!=OperatorsActingOnNumericStrings&Name!=Issue333" - name: Upload coverage to Codacy if: ${{ env.CODACY_PROJECT_TOKEN != '' }} diff --git a/Directory.Packages.props b/Directory.Packages.props index 3c1698e32..0e1ae2fa8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,8 @@ - + + diff --git a/PanoramicData.EPPlus.Test/AddPictureFromBytesTests.cs b/PanoramicData.EPPlus.Test/AddPictureFromBytesTests.cs index 97ffb4c33..9176a84f8 100644 --- a/PanoramicData.EPPlus.Test/AddPictureFromBytesTests.cs +++ b/PanoramicData.EPPlus.Test/AddPictureFromBytesTests.cs @@ -7,9 +7,8 @@ namespace PanoramicData.EPPlus.Test; /// -/// MagicSuite MS-25871: the byte-based AddPicture overload, which must work without System.Drawing -/// so pictures can be added on platforms where GDI+ is unavailable (System.Drawing.Common 7 and -/// later is Windows-only). Nothing in this class may reference System.Drawing. +/// MagicSuite MS-25871: the byte-based AddPicture overloads, which work without any graphics library +/// on every platform. Nothing in this class may reference System.Drawing image types. /// [TestClass] public class AddPictureFromBytesTests @@ -33,21 +32,13 @@ public void AddPictureFromBytes_RoundTripsThroughASavedPackage() saved = package.GetAsByteArray(); } - if (!OperatingSystem.IsWindowsVersionAtLeast(6, 1)) - { - // Reading a picture back goes through ExcelPicture's loading constructor, which uses - // System.Drawing and deliberately throws where that is unsupported. Adding and saving - // (asserted above) work everywhere; assert the documented limit rather than skip. - using var unsupported = new ExcelPackage(new MemoryStream(saved)); - Assert.ThrowsException(() => unsupported.Workbook.Worksheets[0].Drawings); - return; - } using var reopened = new ExcelPackage(new MemoryStream(saved)); var drawing = reopened.Workbook.Worksheets[0].Drawings["CatBytes"]; Assert.IsInstanceOfType(drawing, typeof(ExcelPicture)); var reopenedPicture = (ExcelPicture)drawing; + CollectionAssert.AreEqual(OnePixelPng, reopenedPicture.ImageBytes, "the picture reads back through the public API"); using var partStream = reopenedPicture.Part.GetStream(); using var ms = new MemoryStream(); partStream.CopyTo(ms); @@ -87,4 +78,84 @@ public void AddPictureFromBytes_DuplicateBytes_ShareOnePackagePart() Assert.AreEqual(first.UriPic.OriginalString, second.UriPic.OriginalString, "identical bytes deduplicate to one image part"); } + + [TestMethod] + public void AddPictureFromBytes_ReadsSizeFromTheHeader() + { + using var package = new ExcelPackage(); + var worksheet = package.Workbook.Worksheets.Add("Pictures"); + + var picture = worksheet.Drawings.AddPicture("Auto", OnePixelPng); + + Assert.AreEqual("image/png", picture.ContentType); + Assert.AreEqual(1, picture.GetPixelWidth()); + Assert.AreEqual(1, picture.GetPixelHeight()); + } + + [TestMethod] + public void AddPictureFromStream_StoresTheBytesVerbatim() + { + using var package = new ExcelPackage(); + var worksheet = package.Workbook.Worksheets.Add("Pictures"); + + var picture = worksheet.Drawings.AddPicture("Streamed", new MemoryStream(OnePixelPng)); + + CollectionAssert.AreEqual(OnePixelPng, picture.ImageBytes); + } + + [TestMethod] + public void AddPictureFromFile_ReadsBytesAndSize() + { + var file = new FileInfo(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".png")); + File.WriteAllBytes(file.FullName, OnePixelPng); + try + { + using var package = new ExcelPackage(); + var worksheet = package.Workbook.Worksheets.Add("Pictures"); + + var picture = worksheet.Drawings.AddPicture("File", file); + + CollectionAssert.AreEqual(OnePixelPng, picture.ImageBytes); + Assert.AreEqual(1, picture.GetPixelWidth()); + } + finally + { + file.Delete(); + } + } + + [TestMethod] + public void ImageHeaderReader_ReadsPngJpegGifAndBmp() + { + var png = OfficeOpenXml.Compatibility.ImageHeaderReader.Read(OnePixelPng); + Assert.AreEqual((1, 1), (png.Width, png.Height)); + + var jpeg = OfficeOpenXml.Compatibility.ImageHeaderReader.Read(EPPlusTest.Properties.Resources.Test1); + Assert.AreEqual("image/jpeg", jpeg.ContentType); + Assert.IsTrue(jpeg.Width > 1 && jpeg.Height > 1); + + // GIF89a, 3x2 + byte[] gif = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 3, 0, 2, 0, 0, 0, 0]; + var g = OfficeOpenXml.Compatibility.ImageHeaderReader.Read(gif); + Assert.AreEqual((3, 2), (g.Width, g.Height)); + + // BITMAPINFOHEADER, 5x4, 2835 pixels per metre (72 DPI) + var bmp = new byte[54]; + bmp[0] = 0x42; + bmp[1] = 0x4D; + bmp[14] = 40; + bmp[18] = 5; + bmp[22] = 4; + bmp[38] = 0x13; + bmp[39] = 0x0B; + var b = OfficeOpenXml.Compatibility.ImageHeaderReader.Read(bmp); + Assert.AreEqual((5, 4), (b.Width, b.Height)); + Assert.AreEqual(72F, b.HorizontalResolution, 0.1F); + } + + [TestMethod] + public void ImageHeaderReader_RejectsTruncatedData() + { + Assert.ThrowsException(() => OfficeOpenXml.Compatibility.ImageHeaderReader.Read([0x89, 0x50, 0x4E, 0x47])); + } } diff --git a/PanoramicData.EPPlus.Test/CompoundDoc.cs b/PanoramicData.EPPlus.Test/CompoundDoc.cs index a69255bfa..f125bcbba 100644 --- a/PanoramicData.EPPlus.Test/CompoundDoc.cs +++ b/PanoramicData.EPPlus.Test/CompoundDoc.cs @@ -1,5 +1,4 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Runtime.Versioning; using OfficeOpenXml; using OfficeOpenXml.Style; using OfficeOpenXml.Utils.CompundDocument; @@ -164,7 +163,6 @@ public void Issue131() package.Save(); } - [SupportedOSPlatform("windows")] [TestMethod, Ignore] public void Sample7EncrLargeTest() { diff --git a/PanoramicData.EPPlus.Test/ConditionalFormatting/ConditionalFormatting.cs b/PanoramicData.EPPlus.Test/ConditionalFormatting/ConditionalFormatting.cs index 36fc3255d..5bdd33f17 100644 --- a/PanoramicData.EPPlus.Test/ConditionalFormatting/ConditionalFormatting.cs +++ b/PanoramicData.EPPlus.Test/ConditionalFormatting/ConditionalFormatting.cs @@ -42,7 +42,7 @@ public static void MyClassInitialize(TestContext testContext) Directory.CreateDirectory(string.Format("Test")); } - _excelPackage = new ExcelPackage(new FileInfo(@"Test\ConditionalFormatting.xlsx")); + _excelPackage = new ExcelPackage(new FileInfo(Path.Combine("Test", "ConditionalFormatting.xlsx"))); } // Use ClassCleanup to run code after all tests in a class have run diff --git a/PanoramicData.EPPlus.Test/DTS_FailingTests.cs b/PanoramicData.EPPlus.Test/DTS_FailingTests.cs index 533e01ae4..81eeda59c 100644 --- a/PanoramicData.EPPlus.Test/DTS_FailingTests.cs +++ b/PanoramicData.EPPlus.Test/DTS_FailingTests.cs @@ -1,5 +1,4 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Runtime.Versioning; using OfficeOpenXml; using System.IO; @@ -9,7 +8,6 @@ namespace PanoramicData.EPPlus.Test; public class DTS_FailingTests { - [SupportedOSPlatform("windows")] [TestMethod] public void DeleteWorksheetWithReferencedImage() { @@ -32,7 +30,6 @@ public void DeleteWorksheetWithReferencedImage() } } - [SupportedOSPlatform("windows")] [TestMethod] public void CopyAndDeleteWorksheetWithImage() { diff --git a/PanoramicData.EPPlus.Test/DrawingTest.cs b/PanoramicData.EPPlus.Test/DrawingTest.cs index a2846004f..f0a154c3d 100644 --- a/PanoramicData.EPPlus.Test/DrawingTest.cs +++ b/PanoramicData.EPPlus.Test/DrawingTest.cs @@ -1,5 +1,4 @@ using EPPlusTest.Properties; -using System.Runtime.Versioning; using Microsoft.VisualStudio.TestTools.UnitTesting; using OfficeOpenXml; using OfficeOpenXml.Drawing; @@ -16,7 +15,6 @@ namespace PanoramicData.EPPlus.Test; [TestClass] public class DrawingTest : TestBase { - [SupportedOSPlatform("windows")] [TestMethod] public void RunDrawingTests() { @@ -61,7 +59,6 @@ public void ReadDrawing() Assert.AreEqual(cht.Title.Text, "Test"); } - [SupportedOSPlatform("windows")] public void Picture() { var ws = _pck.Workbook.Worksheets.Add("Picture"); @@ -112,7 +109,6 @@ public void Picture() } //[TestMethod] //[Ignore] - [SupportedOSPlatform("windows")] public void DrawingSizingAndPositioning() { var ws = _pck.Workbook.Worksheets.Add("DrawingPosSize"); @@ -255,7 +251,6 @@ public void PieChart3D() } //[TestMethod] //[Ignore] - [SupportedOSPlatform("windows")] public void Scatter() { var ws = _pck.Workbook.Worksheets.Add("Scatter"); @@ -281,7 +276,9 @@ public void Scatter() ser.DataLabel.Fill.Color = Color.BlueViolet; ser.DataLabel.Font.Color = Color.White; ser.DataLabel.Font.Italic = true; - ser.DataLabel.Font.SetFromFont(new Font("bookman old style", 8)); + ser.DataLabel.Font.LatinFont = "bookman old style"; + ser.DataLabel.Font.ComplexFont = "bookman old style"; + ser.DataLabel.Font.Size = 8; Assert.IsTrue(chrt.ChartType == eChartType.XYScatterSmoothNoMarkers, "Invalid Charttype"); chrt.Series[0].Header = "Test serie"; chrt = ws.Drawings.AddChart("ScatterChart2", eChartType.XYScatterSmooth) as ExcelScatterChart; @@ -575,7 +572,6 @@ public void LineMarker() } //[TestMethod] //[Ignore] - [SupportedOSPlatform("windows")] public void Drawings() { var ws = _pck.Workbook.Worksheets.Add("Shapes"); @@ -601,7 +597,10 @@ public void Drawings() rt = (ws.Drawings["shape2"] as ExcelShape).RichText.Add("\r\nAdded formated richtext"); rt.Bold = true; rt.Color = Color.DarkGoldenrod; - rt.SetFromFont(new Font("Times new roman", 18, FontStyle.Underline)); + rt.LatinFont = "Times new roman"; + rt.ComplexFont = "Times new roman"; + rt.Size = 18; + rt.UnderLine = eUnderLineType.Single; rt.UnderLineColor = Color.Green; @@ -761,7 +760,6 @@ public void MultiChartSeries() } //[TestMethod] //[Ignore] - [SupportedOSPlatform("windows")] public void DeleteDrawing() { var ws = _pck.Workbook.Worksheets.Add("DeleteDrawing1"); @@ -901,7 +899,7 @@ public void TestHeaderaddress() [TestMethod] public void AllDrawingsInsideMarkupCompatibility() { - var workbooksDir = Path.Combine(AppContext.BaseDirectory, @"..\..\workbooks"); + var workbooksDir = Path.Combine(AppContext.BaseDirectory, Path.Combine("..", "..", "workbooks")); // This is codeplex issue 15028: Making an unrelated change to an Excel file that contains drawings that ALL exist // inside MarkupCompatibility/Choice nodes causes the drawings.xml file to be incorrectly garbage collected @@ -959,7 +957,6 @@ public void AllDrawingsInsideMarkupCompatibility() // Cleanup: File.Delete(savedPath); } - [SupportedOSPlatform("windows")] public void DrawingRowheightDynamic() { var ws = _pck.Workbook.Worksheets.Add("PicResize"); diff --git a/PanoramicData.EPPlus.Test/Encrypt.cs b/PanoramicData.EPPlus.Test/Encrypt.cs index 852b75b3c..9f866959a 100644 --- a/PanoramicData.EPPlus.Test/Encrypt.cs +++ b/PanoramicData.EPPlus.Test/Encrypt.cs @@ -11,7 +11,7 @@ public class Encrypt : TestBase [Ignore] public void ReadWriteEncrypt() { - using (ExcelPackage pck = new(new FileInfo(@"Test\Drawing.xlsx"), true)) + using (ExcelPackage pck = new(new FileInfo(Path.Combine("Test", "Drawing.xlsx")), true)) { pck.Encryption.Password = "EPPlus"; pck.Encryption.Algorithm = EncryptionAlgorithm.AES192; @@ -19,16 +19,16 @@ public void ReadWriteEncrypt() pck.Workbook.Protection.LockStructure = true; pck.Workbook.Protection.LockWindows = true; - pck.SaveAs(new FileInfo(@"Test\DrawingEncr.xlsx")); + pck.SaveAs(new FileInfo(Path.Combine("Test", "DrawingEncr.xlsx"))); } - using (ExcelPackage pck = new(new FileInfo(_worksheetPath + @"\DrawingEncr.xlsx"), true, "EPPlus")) + using (ExcelPackage pck = new(new FileInfo(_worksheetPath + "DrawingEncr.xlsx"), true, "EPPlus")) { pck.Encryption.IsEncrypted = false; - pck.SaveAs(new FileInfo(_worksheetPath + @"\DrawingNotEncr.xlsx")); + pck.SaveAs(new FileInfo(_worksheetPath + "DrawingNotEncr.xlsx")); } - FileStream fs = new(_worksheetPath + @"\DrawingEncr.xlsx", FileMode.Open, FileAccess.ReadWrite); + FileStream fs = new(_worksheetPath + "DrawingEncr.xlsx", FileMode.Open, FileAccess.ReadWrite); using (ExcelPackage pck = new(fs, "EPPlus")) { pck.Encryption.IsEncrypted = false; diff --git a/PanoramicData.EPPlus.Test/Issues.cs b/PanoramicData.EPPlus.Test/Issues.cs index 9d1ab1f74..fb89631cc 100644 --- a/PanoramicData.EPPlus.Test/Issues.cs +++ b/PanoramicData.EPPlus.Test/Issues.cs @@ -1,5 +1,4 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Runtime.Versioning; using OfficeOpenXml; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; @@ -73,7 +72,6 @@ public void Issue15031() ws.Cells["A1"].Style.Numberformat.Format = "[t]:mm:ss"; ws.Dispose(); } - [SupportedOSPlatform("windows")] [TestMethod] public void Issue15022() { @@ -435,7 +433,6 @@ public void Issue15174() package.SaveAs(new FileInfo(@"C:\temp\bug\MyTemplate2.xlsx")); } - [SupportedOSPlatform("windows")] [Ignore] [TestMethod] public void PictureIssue() @@ -693,7 +690,6 @@ public void Issue15234() } [TestMethod] /**** Pivottable issue ****/ - [SupportedOSPlatform("windows")] public void Issue() { DirectoryInfo outputDir = new(@"c:\ExcelPivotTest"); @@ -703,7 +699,6 @@ public void Issue() BuildPivotTable2(MyFile); } - [SupportedOSPlatform("windows")] private static void LoadData(FileInfo MyFile) { if (MyFile.Exists) @@ -1479,7 +1474,6 @@ public void Issue_8() } - [SupportedOSPlatform("windows")] [TestMethod, Ignore] public void Issuer27() { @@ -1515,8 +1509,8 @@ public void Issuer26() public void Issue32() { var outputDir = new DirectoryInfo(@"c:\temp\sampleapp"); - var existFile = new FileInfo(outputDir.FullName + @"\sample1.xlsx"); - var newFileName = outputDir.FullName + @"\sample1_copied.xlsx"; + var existFile = new FileInfo(Path.Combine(outputDir.FullName, "sample1.xlsx")); + var newFileName = Path.Combine(outputDir.FullName, "sample1_copied.xlsx"); File.Copy(existFile.FullName, newFileName, true); var newFile = new FileInfo(newFileName); @@ -1745,7 +1739,6 @@ public void Issue70() workbook.SaveAs(new FileInfo(outputPath)); } - [SupportedOSPlatform("windows")] [TestMethod, Ignore] public void Issue100() { @@ -1754,7 +1747,7 @@ public void Issue100() using ExcelPackage package = new(templateFile); var wb = package.Workbook; var sh = wb.Worksheets[1]; - var img_ = Image.FromFile(@"C:\temp\img\background.gif"); + var img_ = File.ReadAllBytes(@"C:\temp\img\background.gif"); var pic = sh.Drawings.AddPicture("logo", img_); pic.SetPosition(1, 1); @@ -1859,7 +1852,6 @@ public void Issue181() var a = pck.Workbook.Properties.Author; pck.SaveAs(new FileInfo($@"C:\temp\bug\issue181-saved.xlsx")); } - [SupportedOSPlatform("windows")] [TestMethod, Ignore] public void Issue10() { @@ -1874,7 +1866,7 @@ public void Issue10() var row = 1; foreach (var f in Directory.EnumerateFiles(@"c:\temp\addin_temp\Addin\img\open_icon_library-full\icons\ico\16x16\actions\")) { - var b = new Bitmap(f); + var b = File.ReadAllBytes(f); var pic = ws.Drawings.AddPicture($"Image{(row + 1) / 2}", b); pic.SetPosition(row, 0, 0, 0); row += 2; @@ -2050,7 +2042,6 @@ public void Issue236() _pck.Workbook.Worksheets["Sheet1"].Cells[7, 10].AddComment("test", "Author"); SaveWorksheet("Issue236-Saved.xlsx"); } - [SupportedOSPlatform("windows")] [TestMethod] public void Issue228() { @@ -2121,8 +2112,9 @@ public void Issuer246() pkg = OpenPackage("issue246.xlsx"); ws = _pck.Workbook.Worksheets["DateFormat"]; var pCulture = System.Threading.Thread.CurrentThread.CurrentCulture; - System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-Se"); - Assert.AreEqual(ws.Cells["A1"].Text, "den 31 december 2018"); + System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-SE", false); + var sv = new CultureInfo("sv-SE", false); + Assert.AreEqual(new DateTime(2018, 12, 31).ToString("D", sv), ws.Cells["A1"].Text); Assert.AreEqual(ws.GetValue(1, 1), new DateTime(2018, 12, 31)); System.Threading.Thread.CurrentThread.CurrentCulture = pCulture; } @@ -2201,14 +2193,22 @@ public void Issue387() [TestMethod] public void Issue333() { + var previousCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = new CultureInfo("sv-SE", false); // the built-in mm-dd-yy format is the culture's short date, which is ISO in sv-SE using var package = new ExcelPackage(); var ws = package.Workbook.Worksheets.Add("TextBug"); ws.Cells["A1"].Value = new DateTime(2019, 3, 7); ws.Cells["A1"].Style.Numberformat.Format = "mm-dd-yy"; - Assert.AreEqual("2019-03-07", ws.Cells["A1"].Text); + try + { + Assert.AreEqual("2019-03-07", ws.Cells["A1"].Text); + } + finally + { + CultureInfo.CurrentCulture = previousCulture; + } } - [SupportedOSPlatform("windows")] [TestMethod] public void Issue445() { diff --git a/PanoramicData.EPPlus.Test/Properties/Resources.Designer.cs b/PanoramicData.EPPlus.Test/Properties/Resources.Designer.cs index 77038af28..278de20cc 100644 --- a/PanoramicData.EPPlus.Test/Properties/Resources.Designer.cs +++ b/PanoramicData.EPPlus.Test/Properties/Resources.Designer.cs @@ -75,19 +75,18 @@ internal Resources() } /// - /// Looks up a localized resource of type System.Drawing.Bitmap. + /// Looks up the Test1.jpg test image (file bytes). /// - internal static System.Drawing.Bitmap Test1 + internal static byte[] Test1 { get { string path = AppContext.BaseDirectory; - while (!Directory.Exists(path + "\\Resources")) + while (!Directory.Exists(Path.Combine(path, "Resources"))) { - path = new DirectoryInfo(path + "\\..").FullName; + path = new DirectoryInfo(Path.Combine(path, "..")).FullName; } - object obj = Image.FromFile(path + "\\Resources\\Test1.jpg"); - return ((System.Drawing.Bitmap)(obj)); + return File.ReadAllBytes(Path.Combine(path, "Resources", "Test1.jpg")); } } } diff --git a/PanoramicData.EPPlus.Test/TestAssemblySetup.cs b/PanoramicData.EPPlus.Test/TestAssemblySetup.cs new file mode 100644 index 000000000..1dea37b55 --- /dev/null +++ b/PanoramicData.EPPlus.Test/TestAssemblySetup.cs @@ -0,0 +1,24 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Globalization; + +namespace PanoramicData.EPPlus.Test; + +/// +/// The suite asserts on culture-dependent text (dates, built-in number formats), so it runs under +/// one fixed culture rather than whatever the machine is set to. Without this, a Linux runner +/// (usually the invariant culture) and a developer's machine (any locale) would disagree. +/// The culture is created without user overrides so Windows regional settings do not leak in. +/// +[TestClass] +public static class TestAssemblySetup +{ + [AssemblyInitialize] + public static void SetCulture(TestContext _) + { + var culture = new CultureInfo("en-US", false); + CultureInfo.DefaultThreadCurrentCulture = culture; + CultureInfo.DefaultThreadCurrentUICulture = culture; + CultureInfo.CurrentCulture = culture; + CultureInfo.CurrentUICulture = culture; + } +} diff --git a/PanoramicData.EPPlus.Test/TestBase.cs b/PanoramicData.EPPlus.Test/TestBase.cs index 5f8893bd7..b283debb5 100644 --- a/PanoramicData.EPPlus.Test/TestBase.cs +++ b/PanoramicData.EPPlus.Test/TestBase.cs @@ -11,8 +11,8 @@ public abstract class TestBase { protected ExcelPackage _pck; protected string _clipartPath = ""; - protected string _worksheetPath = @"..\..\..\Test Output\"; - protected string _testInputPath = @"..\..\..\Workbooks\"; + protected string _worksheetPath = Path.Combine("..", "..", "..", "Test Output") + Path.DirectorySeparatorChar; + protected string _testInputPath = Path.Combine("..", "..", "..", "Workbooks") + Path.DirectorySeparatorChar; public TestContext TestContext { get; set; } [TestInitialize] @@ -51,7 +51,7 @@ public void InitBase() { if (name.EndsWith(ext, StringComparison.OrdinalIgnoreCase)) { - var fileName = name.Replace("EPPlusTest.Resources.", ""); + var fileName = name[(name.IndexOf(".Resources.", StringComparison.Ordinal) + ".Resources.".Length)..]; using var stream = asm.GetManifestResourceStream(name); using var file = File.Create(Path.Combine(_clipartPath, fileName)); stream.CopyTo(file); @@ -67,7 +67,7 @@ public void InitBase() // Directory.CreateDirectory(_worksheetPath); //} var di = new DirectoryInfo(_worksheetPath); - _worksheetPath = di.FullName + "\\"; + _worksheetPath = di.FullName + Path.DirectorySeparatorChar; _pck = new ExcelPackage(); } diff --git a/PanoramicData.EPPlus.Test/VBATests.cs b/PanoramicData.EPPlus.Test/VBATests.cs index 4e86cfaad..79b2db088 100644 --- a/PanoramicData.EPPlus.Test/VBATests.cs +++ b/PanoramicData.EPPlus.Test/VBATests.cs @@ -204,7 +204,7 @@ public void DecompressionChunkGreaterThan4k() #else AppDomain.CurrentDomain.BaseDirectory #endif - , @"..\..\workbooks"); + , Path.Combine("..", "..", "workbooks")); var path = Path.Combine(workbookDir, "VBADecompressBug.xlsm"); var f = new FileInfo(path); if (f.Exists) diff --git a/PanoramicData.EPPlus.Test/WorkSheetTests.cs b/PanoramicData.EPPlus.Test/WorkSheetTests.cs index 9b32fda69..3d78b6a62 100644 --- a/PanoramicData.EPPlus.Test/WorkSheetTests.cs +++ b/PanoramicData.EPPlus.Test/WorkSheetTests.cs @@ -1,5 +1,4 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Runtime.Versioning; using OfficeOpenXml; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; @@ -35,7 +34,6 @@ internal class InhItem : BaseItem [TestClass] public class WorkSheetTest : TestBase { - [SupportedOSPlatform("windows")] [TestMethod] public void RunWorksheetTests() { @@ -183,11 +181,11 @@ public void ReadWorkSheet() Assert.AreEqual(ws.Cells["F5"].Style.Font.UnderLineType, ExcelUnderLineType.None); Assert.AreEqual(ws.Cells["F5"].Style.Font.UnderLine, false); - Assert.AreEqual(ws.Cells["T20"].GetValue(), 0.396180555555556d.ToString(CultureInfo.CurrentCulture)); + Assert.AreEqual(ws.Cells["T20"].GetValue(), 0.39618055555555554d.ToString(CultureInfo.CurrentCulture)); Assert.AreEqual(ws.Cells["T20"].GetValue(), 0); Assert.AreEqual(ws.Cells["T20"].GetValue(), 0); - Assert.AreEqual(ws.Cells["T20"].GetValue(), 0.396180555555556d); - Assert.AreEqual(ws.Cells["T20"].GetValue(), 0.396180555555556d); + Assert.AreEqual(ws.Cells["T20"].GetValue(), 0.39618055555555554d); + Assert.AreEqual(ws.Cells["T20"].GetValue(), 0.39618055555555554d); Assert.AreEqual(ws.Cells["T20"].GetValue(), 0.396180555555556m); Assert.AreEqual(ws.Cells["T20"].GetValue(), 0.396180555555556m); Assert.AreEqual(ws.Cells["T20"].GetValue(), true); @@ -198,11 +196,11 @@ public void ReadWorkSheet() Assert.AreEqual(ws.Cells["T20"].GetValue(), new TimeSpan(693593, 9, 30, 30)); Assert.AreEqual(ws.Cells["T20"].Text, "09:30:30"); - Assert.AreEqual(ws.Cells["T24"].GetValue(), 1.39618055555556d.ToString(CultureInfo.CurrentCulture)); + Assert.AreEqual(ws.Cells["T24"].GetValue(), 1.3961805555555555d.ToString(CultureInfo.CurrentCulture)); Assert.AreEqual(ws.Cells["T24"].GetValue(), 1); Assert.AreEqual(ws.Cells["T24"].GetValue(), 1); - Assert.AreEqual(ws.Cells["T24"].GetValue(), 1.39618055555556d); - Assert.AreEqual(ws.Cells["T24"].GetValue(), 1.39618055555556d); + Assert.AreEqual(ws.Cells["T24"].GetValue(), 1.3961805555555555d); + Assert.AreEqual(ws.Cells["T24"].GetValue(), 1.3961805555555555d); Assert.AreEqual(ws.Cells["T24"].GetValue(), 1.39618055555556m); Assert.AreEqual(ws.Cells["T24"].GetValue(), 1.39618055555556m); Assert.AreEqual(ws.Cells["T24"].GetValue(), true); @@ -371,7 +369,7 @@ public void ReadStreamWithTemplateWorkSheet() Assert.AreEqual(pck.Workbook.Worksheets["Address"].GetValue(40, 1), "\b\t"); - pck.SaveAs(new FileInfo(@"Test\Worksheet2.xlsx")); + pck.SaveAs(new FileInfo(Path.Combine("Test", "Worksheet2.xlsx"))); } instream.Close(); @@ -404,7 +402,6 @@ public void ReadStreamSaveAsStream() // Use ClassCleanup to run code after all tests in a class have run //[Ignore] //[TestMethod] - [SupportedOSPlatform("windows")] public void LoadData() { var ws = _pck.Workbook.Worksheets.Add("newsheet"); @@ -1140,7 +1137,6 @@ public void FormulaArray() } //[Ignore] //[TestMethod] - [SupportedOSPlatform("windows")] public void PictureURL() { var ws = _pck.Workbook.Worksheets.Add("Pic URL"); @@ -1879,7 +1875,6 @@ public void DefColWidthBug() // return pkg } - [SupportedOSPlatform("windows")] [Ignore] [TestMethod] public void CloseProblem() @@ -1892,7 +1887,9 @@ public void CloseProblem() using (var r = ws.Cells["A1:F1"]) { r.Merge = true; - r.Style.Font.SetFromFont(new Font("Arial", 18, FontStyle.Italic)); + r.Style.Font.Name = "Arial"; + r.Style.Font.Size = 18; + r.Style.Font.Italic = true; r.Style.Font.Color.SetColor(Color.DarkRed); r.Style.HorizontalAlignment = ExcelHorizontalAlignment.CenterContinuous; //r.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid; @@ -2255,18 +2252,16 @@ public void CreatePivotMultData() pck.Save(); } //[Ignore] - [SupportedOSPlatform("windows")] [TestMethod] public void SetBackground() { var ws = _pck.Workbook.Worksheets.Add("backimg"); - ws.BackgroundImage.Image = EPPlusTest.Properties.Resources.Test1; + ws.BackgroundImage.ImageBytes = EPPlusTest.Properties.Resources.Test1; ws = _pck.Workbook.Worksheets.Add("backimg2"); ws.BackgroundImage.SetFromFile(new FileInfo(Path.Combine(_clipartPath, "Vector Drawing.wmf"))); } //[Ignore] - [SupportedOSPlatform("windows")] [TestMethod] public void SetHeaderFooterImage() { @@ -2300,7 +2295,6 @@ public void SetHeaderFooterImage() } //[Ignore] //[TestMethod] - [SupportedOSPlatform("windows")] public void NamedStyles() { var wsSheet = _pck.Workbook.Worksheets.Add("NamedStyles"); @@ -2317,7 +2311,8 @@ public void NamedStyles() var secondNamedStyle = _pck.Workbook.Styles.CreateNamedStyle("first", firstNamedStyle.Style).Style; secondNamedStyle.Font.Bold = true; - secondNamedStyle.Font.SetFromFont(new Font("Arial Black", 8)); + secondNamedStyle.Font.Name = "Arial Black"; + secondNamedStyle.Font.Size = 8; secondNamedStyle.Border.Bottom.Style = ExcelBorderStyle.Medium; secondNamedStyle.Border.Left.Style = ExcelBorderStyle.Medium; @@ -2421,7 +2416,6 @@ public void BuildInStyles() } //[Ignore] //[TestMethod] - [SupportedOSPlatform("windows")] public void AutoFitColumns() { var ws = _pck.Workbook.Worksheets.Add("Autofit"); @@ -2877,7 +2871,6 @@ public void SaveToStream() excelPackage.Save(); var s = stream.ToArray(); } - [SupportedOSPlatform("windows")] [TestMethod, Ignore] public void ColumnsTest() { @@ -2992,70 +2985,56 @@ public void CommentShiftsWithColumnInserts() #endregion [TestMethod] - public void DateFunctionsWorkWithDifferentCultureDateFormats_US() + public void DateFunctionsWorkWithDifferentCultureDateFormats_US() => AssertDateFunctionsUnderCulture("en-US", "1/15/2014"); + + [TestMethod] + public void DateFunctionsWorkWithDifferentCultureDateFormats_GB() => AssertDateFunctionsUnderCulture("en-GB", "15/1/2014"); + + /// + /// Runs EDATE on a text date in the given culture on its own thread. Any failure on that thread + /// is captured and asserted on the test thread: an unhandled exception on a worker thread would + /// otherwise take the whole test host down. The culture is created without user overrides so + /// the result does not depend on the machine's regional settings. + /// + private static void AssertDateFunctionsUnderCulture(string cultureName, string textDate) { - var currentCulture = CultureInfo.CurrentCulture; -#if Core - var us = CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US"); -#else - var us = CultureInfo.CreateSpecificCulture("en-US"); - Thread.CurrentThread.CurrentCulture = us; -#endif - double usEoMonth = 0d, usEdate = 0d; - var thread = new Thread(delegate () + var culture = new CultureInfo(cultureName, false); + var previous = CultureInfo.DefaultThreadCurrentCulture; + CultureInfo.DefaultThreadCurrentCulture = culture; + double edate = 0d, eoMonth = 0d; + Exception failure = null; + try { - using var package = new ExcelPackage(); - var ws = package.Workbook.Worksheets.Add("Sheet1"); - ws.Cells[2, 2].Value = "1/15/2014"; - ws.Cells[3, 3].Formula = "EOMONTH(C2, 0)"; - ws.Cells[2, 3].Formula = "EDATE(B2, 0)"; - ws.Calculate(); - usEoMonth = Convert.ToDouble(ws.Cells[2, 3].Value); - usEdate = Convert.ToDouble(ws.Cells[3, 3].Value); - }); - thread.Start(); - thread.Join(); - Assert.AreEqual(41654.0, usEoMonth); - Assert.AreEqual(41670.0, usEdate); -#if Core - CultureInfo.DefaultThreadCurrentCulture = currentCulture; -#else - Thread.CurrentThread.CurrentCulture = currentCulture; -#endif - } - - [TestMethod] - public void DateFunctionsWorkWithDifferentCultureDateFormats_GB() - { - var currentCulture = CultureInfo.CurrentCulture; - -#if Core - var gb = CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-GB"); -#else - var gb = CultureInfo.CreateSpecificCulture("en-GB"); - Thread.CurrentThread.CurrentCulture = gb; -#endif - double gbEoMonth = 0d, gbEdate = 0d; - var thread = new Thread(delegate () + var thread = new Thread(delegate () + { + try + { + Thread.CurrentThread.CurrentCulture = culture; + using var package = new ExcelPackage(); + var ws = package.Workbook.Worksheets.Add("Sheet1"); + ws.Cells[2, 2].Value = textDate; + ws.Cells[3, 3].Formula = "EOMONTH(C2, 0)"; + ws.Cells[2, 3].Formula = "EDATE(B2, 0)"; + ws.Calculate(); + edate = Convert.ToDouble(ws.Cells[2, 3].Value); + eoMonth = Convert.ToDouble(ws.Cells[3, 3].Value); + } + catch (Exception ex) + { + failure = ex; + } + }); + thread.Start(); + thread.Join(); + } + finally { - using var package = new ExcelPackage(); - var ws = package.Workbook.Worksheets.Add("Sheet1"); - ws.Cells[2, 2].Value = "15/1/2014"; - ws.Cells[3, 3].Formula = "EOMONTH(C2, 0)"; - ws.Cells[2, 3].Formula = "EDATE(B2, 0)"; - ws.Calculate(); - gbEoMonth = Convert.ToDouble(ws.Cells[2, 3].Value); - gbEdate = Convert.ToDouble(ws.Cells[3, 3].Value); - }); - thread.Start(); - thread.Join(); - Assert.AreEqual(41654.0, gbEoMonth); - Assert.AreEqual(41670.0, gbEdate); -#if Core - CultureInfo.DefaultThreadCurrentCulture = currentCulture; -#else - Thread.CurrentThread.CurrentCulture = currentCulture; -#endif + CultureInfo.DefaultThreadCurrentCulture = previous; + } + + Assert.IsNull(failure, failure?.ToString()); + Assert.AreEqual(41654.0, edate); + Assert.AreEqual(41670.0, eoMonth); } [TestMethod] public void Text() diff --git a/PanoramicData.EPPlus/Compatibility/ImageCompat.cs b/PanoramicData.EPPlus/Compatibility/ImageCompat.cs deleted file mode 100644 index 348e1972f..000000000 --- a/PanoramicData.EPPlus/Compatibility/ImageCompat.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Drawing; -using System.Drawing.Imaging; -using System.IO; -using System.Runtime.Versioning; - -namespace OfficeOpenXml.Compatibility; - -[SupportedOSPlatform("windows")] -internal class ImageCompat -{ - internal static byte[] GetImageAsByteArray(Image image) - { - var ms = new MemoryStream(); - if (image.RawFormat.Guid == ImageFormat.Gif.Guid) - { - image.Save(ms, ImageFormat.Gif); - } - else if (image.RawFormat.Guid == ImageFormat.Bmp.Guid) - { - image.Save(ms, ImageFormat.Bmp); - } - else if (image.RawFormat.Guid == ImageFormat.Png.Guid) - { - image.Save(ms, ImageFormat.Png); - } - else if (image.RawFormat.Guid == ImageFormat.Tiff.Guid) - { - image.Save(ms, ImageFormat.Tiff); - } - else - { - image.Save(ms, ImageFormat.Jpeg); - } - - return ms.ToArray(); - } -} diff --git a/PanoramicData.EPPlus/Compatibility/ImageHeaderReader.cs b/PanoramicData.EPPlus/Compatibility/ImageHeaderReader.cs new file mode 100644 index 000000000..5d7325ab8 --- /dev/null +++ b/PanoramicData.EPPlus/Compatibility/ImageHeaderReader.cs @@ -0,0 +1,288 @@ +using System.Buffers.Binary; + +namespace OfficeOpenXml.Compatibility; + +/// +/// The pixel size, resolution and content type of an image, read from its file header. +/// +/// +/// Pure managed code: no image is decoded and no graphics library is needed, so it behaves +/// identically on every platform. Handles PNG, JPEG, GIF, BMP, TIFF, EMF and WMF (placeable). +/// +internal readonly record struct ImageHeaderInfo(string ContentType, int Width, int Height, float HorizontalResolution, float VerticalResolution) +{ + internal const float DefaultDpi = 96F; +} + +internal static class ImageHeaderReader +{ + /// + /// Sniffs the content type from the signature. Unrecognised data is reported as image/jpeg, + /// matching the long-standing fallback. + /// + internal static string GetContentType(ReadOnlySpan b) + { + if (b.Length >= 4 && b[0] == 0x89 && b[1] == 0x50 && b[2] == 0x4E && b[3] == 0x47) + { + return "image/png"; + } + + if (b.Length >= 3 && b[0] == 0xFF && b[1] == 0xD8 && b[2] == 0xFF) + { + return "image/jpeg"; + } + + if (b.Length >= 3 && b[0] == 0x47 && b[1] == 0x49 && b[2] == 0x46) + { + return "image/gif"; + } + + if (b.Length >= 2 && b[0] == 0x42 && b[1] == 0x4D) + { + return "image/bmp"; + } + + if (b.Length >= 4 && ((b[0] == 0x49 && b[1] == 0x49 && b[2] == 0x2A && b[3] == 0) || (b[0] == 0x4D && b[1] == 0x4D && b[2] == 0 && b[3] == 0x2A))) + { + return "image/x-tiff"; + } + + if (b.Length >= 44 && BinaryPrimitives.ReadInt32LittleEndian(b) == 1 && BinaryPrimitives.ReadInt32LittleEndian(b[40..]) == 0x464D4520) + { + return "image/x-emf"; + } + + if (b.Length >= 4 && b[0] == 0xD7 && b[1] == 0xCD && b[2] == 0xC6 && b[3] == 0x9A) + { + return "image/x-wmf"; + } + + return "image/jpeg"; + } + + /// + /// Reads the header. Throws when the data is not a + /// recognised image or is truncated. + /// + internal static ImageHeaderInfo Read(byte[] bytes) + { + ArgumentNullException.ThrowIfNull(bytes); + try + { + var contentType = GetContentType(bytes); + var data = new ReadOnlySpan(bytes); + var (w, h, hx, hy) = contentType switch + { + "image/png" => ReadPng(data), + "image/gif" => ReadGif(data), + "image/bmp" => ReadBmp(data), + "image/x-tiff" => ReadTiff(data), + "image/x-emf" => ReadEmf(data), + "image/x-wmf" => ReadWmf(data), + _ => ReadJpeg(data), + }; + return new ImageHeaderInfo( + contentType, + w, + h, + hx > 0 ? hx : ImageHeaderInfo.DefaultDpi, + hy > 0 ? hy : ImageHeaderInfo.DefaultDpi); + } + catch (Exception ex) when (ex is ArgumentException or IndexOutOfRangeException or OverflowException) + { + throw new InvalidDataException("Data is not a supported image or is corrupt", ex); + } + } + + private static (int, int, float, float) ReadPng(ReadOnlySpan b) + { + // 8 byte signature, then chunks of length(4) type(4) data crc(4). IHDR is first. + var width = BinaryPrimitives.ReadInt32BigEndian(b[16..]); + var height = BinaryPrimitives.ReadInt32BigEndian(b[20..]); + float hx = 0, hy = 0; + var pos = 8; + while (pos + 12 <= b.Length) + { + var len = BinaryPrimitives.ReadInt32BigEndian(b[pos..]); + var type = b.Slice(pos + 4, 4); + if (type.SequenceEqual("pHYs"u8) && len >= 9 && b[pos + 16] == 1) + { + hx = (float)(BinaryPrimitives.ReadUInt32BigEndian(b[(pos + 8)..]) * 0.0254); + hy = (float)(BinaryPrimitives.ReadUInt32BigEndian(b[(pos + 12)..]) * 0.0254); + break; + } + + if (type.SequenceEqual("IDAT"u8) || type.SequenceEqual("IEND"u8) || len < 0) + { + break; // pHYs must precede IDAT + } + + pos += 12 + len; + } + + return (width, height, hx, hy); + } + + private static (int, int, float, float) ReadGif(ReadOnlySpan b) + => (BinaryPrimitives.ReadUInt16LittleEndian(b[6..]), BinaryPrimitives.ReadUInt16LittleEndian(b[8..]), 0, 0); + + private static (int, int, float, float) ReadBmp(ReadOnlySpan b) + { + var headerSize = BinaryPrimitives.ReadInt32LittleEndian(b[14..]); + if (headerSize == 12) + { + // BITMAPCOREHEADER + return (BinaryPrimitives.ReadUInt16LittleEndian(b[18..]), BinaryPrimitives.ReadUInt16LittleEndian(b[20..]), 0, 0); + } + + var width = BinaryPrimitives.ReadInt32LittleEndian(b[18..]); + var height = Math.Abs(BinaryPrimitives.ReadInt32LittleEndian(b[22..])); // negative = top-down + var xppm = BinaryPrimitives.ReadInt32LittleEndian(b[38..]); + var yppm = BinaryPrimitives.ReadInt32LittleEndian(b[42..]); + return (width, height, (float)(xppm * 0.0254), (float)(yppm * 0.0254)); + } + + private static (int, int, float, float) ReadJpeg(ReadOnlySpan b) + { + float hx = 0, hy = 0; + var pos = 2; + while (pos + 4 <= b.Length) + { + if (b[pos] != 0xFF) + { + pos++; + continue; + } + + var marker = b[pos + 1]; + if (marker == 0xFF) + { + pos++; // fill byte + continue; + } + + if (marker is 0xD8 or 0x01 or (>= 0xD0 and <= 0xD7)) + { + pos += 2; // standalone markers have no length + continue; + } + + var len = BinaryPrimitives.ReadUInt16BigEndian(b[(pos + 2)..]); + if (marker == 0xE0 && len >= 16 && b.Slice(pos + 4, 5).SequenceEqual("JFIF\0"u8)) + { + var units = b[pos + 11]; + var xd = BinaryPrimitives.ReadUInt16BigEndian(b[(pos + 12)..]); + var yd = BinaryPrimitives.ReadUInt16BigEndian(b[(pos + 14)..]); + if (units == 1) + { + hx = xd; + hy = yd; + } + else if (units == 2) + { + hx = (float)(xd * 2.54); + hy = (float)(yd * 2.54); + } + } + + // Start-of-frame markers (not DHT 0xC4, JPG 0xC8, DAC 0xCC) + if (marker is >= 0xC0 and <= 0xCF and not 0xC4 and not 0xC8 and not 0xCC) + { + var height = BinaryPrimitives.ReadUInt16BigEndian(b[(pos + 5)..]); + var width = BinaryPrimitives.ReadUInt16BigEndian(b[(pos + 7)..]); + return (width, height, hx, hy); + } + + pos += 2 + len; + } + + throw new InvalidDataException("No JPEG frame header found"); + } + + private static (int, int, float, float) ReadTiff(ReadOnlySpan b) + { + var little = b[0] == 0x49; + ushort U16(ReadOnlySpan s, int o) => little ? BinaryPrimitives.ReadUInt16LittleEndian(s[o..]) : BinaryPrimitives.ReadUInt16BigEndian(s[o..]); + uint U32(ReadOnlySpan s, int o) => little ? BinaryPrimitives.ReadUInt32LittleEndian(s[o..]) : BinaryPrimitives.ReadUInt32BigEndian(s[o..]); + double Value(ReadOnlySpan s, int entry) + { + var type = U16(s, entry + 2); + switch (type) + { + case 3: + return U16(s, entry + 8); // SHORT + case 4: + return U32(s, entry + 8); // LONG + case 5: // RATIONAL + var at = (int)U32(s, entry + 8); + return (double)U32(s, at) / Math.Max(1U, U32(s, at + 4)); + default: + return 0; + } + } + + var ifd = (int)U32(b, 4); + var count = U16(b, ifd); + double width = 0, height = 0, xres = 0, yres = 0; + var unit = 2; + for (var i = 0; i < count; i++) + { + var entry = ifd + 2 + (i * 12); + switch (U16(b, entry)) + { + case 256: + width = Value(b, entry); + break; + case 257: + height = Value(b, entry); + break; + case 282: + xres = Value(b, entry); + break; + case 283: + yres = Value(b, entry); + break; + case 296: + unit = (int)Value(b, entry); + break; + } + } + + var scale = unit == 3 ? 2.54 : 1; + return ((int)width, (int)height, (float)(xres * scale), (float)(yres * scale)); + } + + private static (int, int, float, float) ReadEmf(ReadOnlySpan b) + { + // ENHMETAHEADER: rclFrame (0.01mm units) at offset 24. + var frameWidth = BinaryPrimitives.ReadInt32LittleEndian(b[32..]) - BinaryPrimitives.ReadInt32LittleEndian(b[24..]); + var frameHeight = BinaryPrimitives.ReadInt32LittleEndian(b[36..]) - BinaryPrimitives.ReadInt32LittleEndian(b[28..]); + var dpi = ImageHeaderInfo.DefaultDpi; + return ( + (int)Math.Round(frameWidth / 2540.0 * dpi, MidpointRounding.AwayFromZero), + (int)Math.Round(frameHeight / 2540.0 * dpi, MidpointRounding.AwayFromZero), + dpi, + dpi); + } + + private static (int, int, float, float) ReadWmf(ReadOnlySpan b) + { + // Placeable header: bounding box in logical units, then units-per-inch. + var left = BinaryPrimitives.ReadInt16LittleEndian(b[6..]); + var top = BinaryPrimitives.ReadInt16LittleEndian(b[8..]); + var right = BinaryPrimitives.ReadInt16LittleEndian(b[10..]); + var bottom = BinaryPrimitives.ReadInt16LittleEndian(b[12..]); + var inch = (int)BinaryPrimitives.ReadUInt16LittleEndian(b[14..]); + if (inch == 0) + { + inch = 1440; + } + + var dpi = ImageHeaderInfo.DefaultDpi; + return ( + (int)Math.Round((right - left) / (double)inch * dpi, MidpointRounding.AwayFromZero), + (int)Math.Round((bottom - top) / (double)inch * dpi, MidpointRounding.AwayFromZero), + dpi, + dpi); + } +} diff --git a/PanoramicData.EPPlus/Compatibility/TextMeasurer.cs b/PanoramicData.EPPlus/Compatibility/TextMeasurer.cs new file mode 100644 index 000000000..f36007b98 --- /dev/null +++ b/PanoramicData.EPPlus/Compatibility/TextMeasurer.cs @@ -0,0 +1,87 @@ +using SkiaSharp; +using System.Drawing; + +namespace OfficeOpenXml.Compatibility; + +/// +/// Measures text for column auto-fit, using SkiaSharp so that it works on Windows, Linux and macOS. +/// +internal sealed class TextMeasurer : IDisposable +{ + private const float PixelsPerPoint = 96F / 72F; + + private static readonly Lazy _isAvailable = new(Probe); + + private readonly Dictionary<(string Name, bool Bold, bool Italic), SKTypeface> _typefaces = []; + private readonly Dictionary<(string Name, float Size, bool Bold, bool Italic), SKFont> _fonts = []; + + /// + /// False when the native SkiaSharp library cannot be loaded on this machine. + /// + internal static bool IsAvailable => _isAvailable.Value; + + private static bool Probe() + { + try + { + using var typeface = SKTypeface.Default; + using var font = new SKFont(typeface, 12); + font.MeasureText("0"); + return true; + } + catch (Exception ex) when (ex is DllNotFoundException or TypeInitializationException or EntryPointNotFoundException or BadImageFormatException) + { + return false; + } + } + + /// + /// The size in pixels (at 96 DPI) of the text in the given font. Size is in points. + /// A font that is not installed falls back to the platform's default typeface. + /// + internal SizeF Measure(string text, string fontName, float sizeInPoints, bool bold, bool italic) + { + if (string.IsNullOrEmpty(text)) + { + return SizeF.Empty; + } + + var key = (fontName, sizeInPoints, bold, italic); + if (!_fonts.TryGetValue(key, out var font)) + { + if (!_typefaces.TryGetValue((fontName, bold, italic), out var typeface)) + { + typeface = SKTypeface.FromFamilyName( + fontName, + bold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal, + SKFontStyleWidth.Normal, + italic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright) + ?? SKTypeface.Default; + _typefaces.Add((fontName, bold, italic), typeface); + } + + font = new SKFont(typeface, sizeInPoints * PixelsPerPoint); + _fonts.Add(key, font); + } + + var width = font.MeasureText(text); + var height = font.Spacing; + return new SizeF(width, height); + } + + public void Dispose() + { + foreach (var font in _fonts.Values) + { + font.Dispose(); + } + + foreach (var typeface in _typefaces.Values) + { + typeface.Dispose(); + } + + _fonts.Clear(); + _typefaces.Clear(); + } +} diff --git a/PanoramicData.EPPlus/Drawing/ExcelDrawings.cs b/PanoramicData.EPPlus/Drawing/ExcelDrawings.cs index 04e0a81ee..73807c4fe 100644 --- a/PanoramicData.EPPlus/Drawing/ExcelDrawings.cs +++ b/PanoramicData.EPPlus/Drawing/ExcelDrawings.cs @@ -29,6 +29,7 @@ * Jan Källman Initial Release 2009-12-22 * Jan Källman License changed GPL-->LGPL 2011-12-16 *******************************************************************************/ +using OfficeOpenXml.Compatibility; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Table.PivotTable; using OfficeOpenXml.Utils; @@ -37,7 +38,6 @@ using System.Collections.Generic; using System.Drawing; using System.IO; -using System.Runtime.Versioning; using System.Xml; namespace OfficeOpenXml.Drawing; @@ -243,42 +243,55 @@ eChartType.StockOHLC or /// The chart public ExcelChart AddChart(string Name, eChartType ChartType) => AddChart(Name, ChartType, null); /// - /// Add a picture to the worksheet + /// Add a picture to the worksheet from an image file's bytes. The size and resolution are read + /// from the image header (PNG, JPEG, GIF, BMP, TIFF, EMF or WMF); no graphics library is used, so + /// this works on every platform. The bytes are stored in the package unchanged. /// /// - /// An image. Always saved in then JPeg format + /// The image file's bytes /// - [SupportedOSPlatform("windows")] - public ExcelPicture AddPicture(string Name, Image image) => AddPicture(Name, image, null); + public ExcelPicture AddPicture(string Name, byte[] ImageBytes) => AddPicture(Name, ImageBytes, (Uri)null); /// - /// Add a picture to the worksheet + /// Add a picture to the worksheet from an image file's bytes. The size and resolution are read + /// from the image header (PNG, JPEG, GIF, BMP, TIFF, EMF or WMF). /// /// - /// An image. Always saved in then JPeg format + /// The image file's bytes /// Picture hyperlink /// - [SupportedOSPlatform("windows")] - public ExcelPicture AddPicture(string Name, Image image, Uri Hyperlink) + public ExcelPicture AddPicture(string Name, byte[] ImageBytes, Uri Hyperlink) { - if (image != null) + if (ImageBytes == null || ImageBytes.Length == 0) { - if (_drawingNames.ContainsKey(Name)) - { - throw new Exception("Name already exists in the drawings collection"); - } + throw new ArgumentException("AddPicture: ImageBytes can't be null or empty", nameof(ImageBytes)); + } - var drawNode = CreateDrawingXml(); - drawNode.SetAttribute("editAs", "oneCell"); - ExcelPicture pic = new(this, drawNode, image, Hyperlink) - { - Name = Name - }; - _drawings.Add(pic); - _drawingNames.Add(Name, _drawings.Count - 1); - return pic; + return AddPictureCore(Name, drawNode => new ExcelPicture(this, drawNode, ImageBytes, "/xl/media/image{0}" + ExcelPicture.GetExtension(ExcelPicture.GetContentTypeFromBytes(ImageBytes)), Hyperlink)); + } + /// + /// Add a picture to the worksheet from a stream containing an image file. The stream is read to the end. + /// + /// + /// A stream containing the image file + /// + public ExcelPicture AddPicture(string Name, Stream ImageStream) => AddPicture(Name, ImageStream, null); + /// + /// Add a picture to the worksheet from a stream containing an image file. The stream is read to the end. + /// + /// + /// A stream containing the image file + /// Picture hyperlink + /// + public ExcelPicture AddPicture(string Name, Stream ImageStream, Uri Hyperlink) + { + if (ImageStream == null) + { + throw new ArgumentNullException(nameof(ImageStream), "AddPicture: ImageStream can't be null"); } - throw (new Exception("AddPicture: Image can't be null")); + using var ms = new MemoryStream(); + ImageStream.CopyTo(ms); + return AddPicture(Name, ms.ToArray(), Hyperlink); } /// /// Add a picure to the worksheet @@ -286,7 +299,6 @@ public ExcelPicture AddPicture(string Name, Image image, Uri Hyperlink) /// /// The image file /// - [SupportedOSPlatform("windows")] public ExcelPicture AddPicture(string Name, FileInfo ImageFile) => AddPicture(Name, ImageFile, null); /// /// Add a picure to the worksheet @@ -295,7 +307,6 @@ public ExcelPicture AddPicture(string Name, Image image, Uri Hyperlink) /// The image file /// Picture Hyperlink /// - [SupportedOSPlatform("windows")] public ExcelPicture AddPicture(string Name, FileInfo ImageFile, Uri Hyperlink) { if (Worksheet is ExcelChartsheet && _drawings.Count > 0) @@ -303,30 +314,49 @@ public ExcelPicture AddPicture(string Name, FileInfo ImageFile, Uri Hyperlink) throw new InvalidOperationException("Chart worksheets can't have more than one drawing"); } - if (ImageFile != null) + if (ImageFile == null) { - if (_drawingNames.ContainsKey(Name)) - { - throw new Exception("Name already exists in the drawings collection"); - } + throw new Exception("AddPicture: ImageFile can't be null"); + } - var drawNode = CreateDrawingXml(); - drawNode.SetAttribute("editAs", "oneCell"); - ExcelPicture pic = new(this, drawNode, ImageFile, Hyperlink) - { - Name = Name - }; - _drawings.Add(pic); - _drawingNames.Add(Name, _drawings.Count - 1); - return pic; + byte[] bytes; + try + { + bytes = File.ReadAllBytes(ImageFile.FullName); + ImageHeaderReader.Read(bytes); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException) + { + throw new InvalidDataException("File is not a supported image-file or is corrupt", ex); + } + + return AddPictureCore(Name, drawNode => new ExcelPicture(this, drawNode, bytes, "/xl/media/{0}" + ImageFile.Name, Hyperlink)); + } + + private ExcelPicture AddPictureCore(string Name, Func create) + { + if (Worksheet is ExcelChartsheet && _drawings.Count > 0) + { + throw new InvalidOperationException("Chart worksheets can't have more than one drawing"); + } + + if (_drawingNames.ContainsKey(Name)) + { + throw new Exception("Name already exists in the drawings collection"); } - throw (new Exception("AddPicture: ImageFile can't be null")); + var drawNode = CreateDrawingXml(); + drawNode.SetAttribute("editAs", "oneCell"); + var pic = create(drawNode); + pic.Name = Name; + _drawings.Add(pic); + _drawingNames.Add(Name, _drawings.Count - 1); + return pic; } /// - /// Add a picture to the worksheet from raw image bytes, without using System.Drawing - works on - /// platforms where GDI+ is unavailable (System.Drawing.Common 7 and later is Windows-only). + /// Add a picture to the worksheet from raw image bytes, without a graphics library - works on + /// every platform. /// MagicSuite MS-25871. /// /// @@ -336,8 +366,8 @@ public ExcelPicture AddPicture(string Name, FileInfo ImageFile, Uri Hyperlink) /// public ExcelPicture AddPicture(string Name, byte[] ImageBytes, int WidthPixels, int HeightPixels) => AddPicture(Name, ImageBytes, WidthPixels, HeightPixels, null); /// - /// Add a picture to the worksheet from raw image bytes, without using System.Drawing - works on - /// platforms where GDI+ is unavailable (System.Drawing.Common 7 and later is Windows-only). + /// Add a picture to the worksheet from raw image bytes, without a graphics library - works on + /// every platform. /// MagicSuite MS-25871. /// /// diff --git a/PanoramicData.EPPlus/Drawing/ExcelPicture.cs b/PanoramicData.EPPlus/Drawing/ExcelPicture.cs index fdcdfb547..4123b3ed0 100644 --- a/PanoramicData.EPPlus/Drawing/ExcelPicture.cs +++ b/PanoramicData.EPPlus/Drawing/ExcelPicture.cs @@ -32,11 +32,8 @@ using OfficeOpenXml.Compatibility; using OfficeOpenXml.Utils; using System; -using System.Drawing; -using System.Drawing.Imaging; using System.Globalization; using System.IO; -using System.Runtime.Versioning; using System.Text; using System.Xml; @@ -61,19 +58,12 @@ internal ExcelPicture(ExcelDrawings drawings, XmlNode node) : Part = drawings.Part.Package.GetPart(UriPic); FileInfo f = new(UriPic.OriginalString); ContentType = GetContentType(f.Extension); - if (!OperatingSystem.IsWindowsVersionAtLeast(6, 1)) - { - throw new PlatformNotSupportedException("Loading a picture uses System.Drawing, which is only supported on Windows."); - } - - _image = Image.FromStream(Part.GetStream()); - var iby = ImageCompat.GetImageAsByteArray(_image); + // The bytes are hashed as stored. They are not retained: ImageBytes reads them from the package on demand. + var iby = Part.GetStream().ToArray(); var ii = _drawings._package.LoadImage(iby, UriPic, Part); ImageHash = ii.Hash; - //_height = _image.Height; - //_width = _image.Width; var relID = GetXmlNodeString("xdr:pic/xdr:nvPicPr/xdr:cNvPr/a:hlinkClick/@r:id"); if (!string.IsNullOrEmpty(relID)) { @@ -91,78 +81,31 @@ internal ExcelPicture(ExcelDrawings drawings, XmlNode node) : } } } - [SupportedOSPlatform("windows")] - internal ExcelPicture(ExcelDrawings drawings, XmlNode node, Image image, Uri hyperlink) : - base(drawings, node, "xdr:pic/xdr:nvPicPr/xdr:cNvPr/@name") - { - var picNode = node.OwnerDocument.CreateElement("xdr", "pic", ExcelPackage.schemaSheetDrawings); - node.InsertAfter(picNode, node.SelectSingleNode("xdr:to", NameSpaceManager)); - Hyperlink = hyperlink; - picNode.InnerXml = PicStartXml(); - - node.InsertAfter(node.OwnerDocument.CreateElement("xdr", "clientData", ExcelPackage.schemaSheetDrawings), picNode); - var package = drawings.Worksheet._package.Package; - //Get the picture if it exists or save it if not. - _image = image; - var relID = SavePicture(image); - - //Create relationship - node.SelectSingleNode("xdr:pic/xdr:blipFill/a:blip/@r:embed", NameSpaceManager).Value = relID; - _height = image.Height; - _width = image.Width; - SetPosDefaults(image); - Packaging.ZipPackage.Flush(); - } - [SupportedOSPlatform("windows")] - internal ExcelPicture(ExcelDrawings drawings, XmlNode node, FileInfo imageFile, Uri hyperlink) : + /// + /// Creates a picture from an image file's bytes. The size and resolution are read from the image header. + /// The bytes are written into the package verbatim. + /// + internal ExcelPicture(ExcelDrawings drawings, XmlNode node, byte[] imageBytes, string uriTemplate, Uri hyperlink) : base(drawings, node, "xdr:pic/xdr:nvPicPr/xdr:cNvPr/@name") { - var picNode = node.OwnerDocument.CreateElement("xdr", "pic", ExcelPackage.schemaSheetDrawings); - node.InsertAfter(picNode, node.SelectSingleNode("xdr:to", NameSpaceManager)); - Hyperlink = hyperlink; - picNode.InnerXml = PicStartXml(); - - node.InsertAfter(node.OwnerDocument.CreateElement("xdr", "clientData", ExcelPackage.schemaSheetDrawings), picNode); - - //Changed to stream 2/4-13 (issue 14834). Thnx SClause - var package = drawings.Worksheet._package.Package; - ContentType = GetContentType(imageFile.Extension); - var imagestream = new FileStream(imageFile.FullName, FileMode.Open, FileAccess.Read); - _image = Image.FromStream(imagestream); - - var img = ImageCompat.GetImageAsByteArray(_image); - - imagestream.Close(); - UriPic = GetNewUri(package, "/xl/media/{0}" + imageFile.Name); - var ii = _drawings._package.AddImage(img, UriPic, ContentType); - string relID; - if (!drawings._hashes.TryGetValue(ii.Hash, out var value)) - { - Part = ii.Part; - RelPic = drawings.Part.CreateRelationship(UriHelper.GetRelativeUri(drawings.UriDrawing, ii.Uri), Packaging.TargetMode.Internal, ExcelPackage.schemaRelationships + "/image"); - relID = RelPic.Id; - _drawings._hashes.Add(ii.Hash, relID); - AddNewPicture(img, relID); - } - else - { - relID = value; - var rel = _drawings.Part.GetRelationship(relID); - UriPic = UriHelper.ResolvePartUri(rel.SourceUri, rel.TargetUri); - } - - ImageHash = ii.Hash; - _height = Image.Height; - _width = Image.Width; - SetPosDefaults(Image); - //Create relationship - node.SelectSingleNode("xdr:pic/xdr:blipFill/a:blip/@r:embed", NameSpaceManager).Value = relID; - Packaging.ZipPackage.Flush(); + var info = ImageHeaderReader.Read(imageBytes); + Create(node, imageBytes, info.ContentType, uriTemplate, info.Width, info.Height, info.HorizontalResolution, info.VerticalResolution, hyperlink); } + /// + /// Creates a picture from an image file's bytes, with the display size supplied by the caller. + /// No header is read. The bytes are written into the package verbatim, with the content type + /// sniffed from the byte signature. + /// internal ExcelPicture(ExcelDrawings drawings, XmlNode node, byte[] imageBytes, int widthPixels, int heightPixels, Uri hyperlink) : base(drawings, node, "xdr:pic/xdr:nvPicPr/xdr:cNvPr/@name") + { + var contentType = GetContentTypeFromBytes(imageBytes); + Create(node, imageBytes, contentType, "/xl/media/image{0}" + GetExtension(contentType), widthPixels, heightPixels, ImageHeaderInfo.DefaultDpi, ImageHeaderInfo.DefaultDpi, hyperlink); + } + + private void Create(XmlNode node, byte[] imageBytes, string contentType, string uriTemplate, int widthPixels, int heightPixels, float horizontalDpi, float verticalDpi, Uri hyperlink) { var picNode = node.OwnerDocument.CreateElement("xdr", "pic", ExcelPackage.schemaSheetDrawings); node.InsertAfter(picNode, node.SelectSingleNode("xdr:to", NameSpaceManager)); @@ -171,26 +114,24 @@ internal ExcelPicture(ExcelDrawings drawings, XmlNode node, byte[] imageBytes, i node.InsertAfter(node.OwnerDocument.CreateElement("xdr", "clientData", ExcelPackage.schemaSheetDrawings), picNode); - // MagicSuite MS-25871: the byte-based path touches System.Drawing nowhere, so it works on - // platforms where GDI+ is unavailable (System.Drawing.Common 7+ is Windows-only). The - // caller supplies the pixel size, so no decode is needed; the bytes are written into the - // package verbatim, with the content type sniffed from the byte signature. + var drawings = _drawings; var package = drawings.Worksheet._package.Package; - ContentType = GetContentTypeFromBytes(imageBytes); - UriPic = GetNewUri(package, "/xl/media/image{0}" + GetExtension(ContentType)); - var ii = _drawings._package.AddImage(imageBytes, UriPic, ContentType); + _imageBytes = imageBytes; + ContentType = contentType; + UriPic = GetNewUri(package, uriTemplate); + var ii = drawings._package.AddImage(imageBytes, UriPic, ContentType); string relID; if (!drawings._hashes.TryGetValue(ii.Hash, out var value)) { Part = ii.Part; RelPic = drawings.Part.CreateRelationship(UriHelper.GetRelativeUri(drawings.UriDrawing, ii.Uri), Packaging.TargetMode.Internal, ExcelPackage.schemaRelationships + "/image"); relID = RelPic.Id; - _drawings._hashes.Add(ii.Hash, relID); + drawings._hashes.Add(ii.Hash, relID); } else { relID = value; - var rel = _drawings.Part.GetRelationship(relID); + var rel = drawings.Part.GetRelationship(relID); UriPic = UriHelper.ResolvePartUri(rel.SourceUri, rel.TargetUri); } @@ -198,47 +139,26 @@ internal ExcelPicture(ExcelDrawings drawings, XmlNode node, byte[] imageBytes, i _height = heightPixels; _width = widthPixels; EditAs = eEditAs.OneCell; - SetPixelWidth(widthPixels); - SetPixelHeight(heightPixels); + SetPixelWidth(widthPixels, horizontalDpi); + SetPixelHeight(heightPixels, verticalDpi); //Create relationship node.SelectSingleNode("xdr:pic/xdr:blipFill/a:blip/@r:embed", NameSpaceManager).Value = relID; Packaging.ZipPackage.Flush(); } /// - /// MagicSuite MS-25871: content type from the byte signature, so the byte-based AddPicture - /// needs no decode and no file extension. + /// Content type from the byte signature, so no decode and no file extension is needed. /// - internal static string GetContentTypeFromBytes(byte[] imageBytes) - { - if (imageBytes.Length >= 4 && imageBytes[0] == 0x89 && imageBytes[1] == 0x50 && imageBytes[2] == 0x4E && imageBytes[3] == 0x47) - { - return "image/png"; - } - - if (imageBytes.Length >= 3 && imageBytes[0] == 0xFF && imageBytes[1] == 0xD8 && imageBytes[2] == 0xFF) - { - return "image/jpeg"; - } - - if (imageBytes.Length >= 3 && imageBytes[0] == 0x47 && imageBytes[1] == 0x49 && imageBytes[2] == 0x46) - { - return "image/gif"; - } - - if (imageBytes.Length >= 2 && imageBytes[0] == 0x42 && imageBytes[1] == 0x4D) - { - return "image/bmp"; - } - - return "image/jpeg"; - } + internal static string GetContentTypeFromBytes(byte[] imageBytes) => ImageHeaderReader.GetContentType(imageBytes); internal static string GetExtension(string contentType) => contentType switch { "image/png" => ".png", "image/gif" => ".gif", "image/bmp" => ".bmp", + "image/x-tiff" => ".tif", + "image/x-emf" => ".emf", + "image/x-wmf" => ".wmf", _ => ".jpg", }; @@ -257,34 +177,11 @@ internal static string GetContentTypeFromBytes(byte[] imageBytes) ".wmf" => "image/x-wmf", _ => "image/jpeg", }; - [SupportedOSPlatform("windows")] - internal static ImageFormat GetImageFormat(string contentType) => contentType.ToLower(CultureInfo.InvariantCulture) switch - { - "image/bmp" => ImageFormat.Bmp, - "image/jpeg" => ImageFormat.Jpeg, - "image/gif" => ImageFormat.Gif, - "image/png" => ImageFormat.Png, - "image/x-emf" => ImageFormat.Emf, - "image/x-tiff" => ImageFormat.Tiff, - "image/x-wmf" => ImageFormat.Wmf, - _ => ImageFormat.Jpeg, - }; //Add a new image to the compare collection - private static void AddNewPicture(byte[] img, string relID) - { - var newPic = new ExcelDrawings.ImageCompare - { - image = img, - relID = relID - }; - //_drawings._pics.Add(newPic); - } #endregion - [SupportedOSPlatform("windows")] - private string SavePicture(Image image) + private string SavePicture(byte[] imageBytes) { - var img = ImageCompat.GetImageAsByteArray(image); - var ii = _drawings._package.AddImage(img); - + var contentType = GetContentTypeFromBytes(imageBytes); + var ii = _drawings._package.AddImage(imageBytes, GetNewUri(_drawings.Worksheet._package.Package, "/xl/media/image{0}" + GetExtension(contentType)), contentType); ImageHash = ii.Hash; if (_drawings._hashes.TryGetValue(ii.Hash, out var relID)) @@ -299,21 +196,13 @@ private string SavePicture(Image image) ImageHash = ii.Hash; } - //Set the Image and save it to the package. + ContentType = contentType; RelPic = _drawings.Part.CreateRelationship(UriHelper.GetRelativeUri(_drawings.UriDrawing, UriPic), Packaging.TargetMode.Internal, ExcelPackage.schemaRelationships + "/image"); - //AddNewPicture(img, picRelation.Id); _drawings._hashes.Add(ii.Hash, RelPic.Id); return RelPic.Id; } - [SupportedOSPlatform("windows")] - private void SetPosDefaults(Image image) - { - EditAs = eEditAs.OneCell; - SetPixelWidth(image.Width, image.HorizontalResolution); - SetPixelHeight(image.Height, image.VerticalResolution); - } private string PicStartXml() { @@ -352,29 +241,33 @@ private string PicStartXml() } internal string ImageHash { get; set; } - Image _image = null; + byte[] _imageBytes = null; /// - /// The Image + /// The image file's bytes, exactly as stored in the package (PNG, JPEG, GIF, BMP, TIFF, EMF or WMF). + /// Setting it replaces the picture; the bytes are written to the package verbatim. /// - [SupportedOSPlatform("windows")] - public Image Image + public byte[] ImageBytes { get { - return _image; + if (_imageBytes != null) + { + return _imageBytes; + } + + return Part?.GetStream().ToArray(); } set { if (value != null) { - _image = value; + _imageBytes = value; try { var relID = SavePicture(value); //Create relationship TopNode.SelectSingleNode("xdr:pic/xdr:blipFill/a:blip/@r:embed", NameSpaceManager).Value = relID; - //_image.Save(Part.GetStream(FileMode.Create, FileAccess.Write), _imageFormat); //Always JPEG here at this point. } catch (Exception ex) { @@ -383,27 +276,13 @@ public Image Image } } } - ImageFormat _imageFormat; /// - /// Image format - /// If the picture is created from an Image this type is always Jpeg + /// The image's MIME content type, for example image/png or image/jpeg. /// - [SupportedOSPlatform("windows")] - public ImageFormat ImageFormat - { - get - { - return _imageFormat ?? ImageFormat.Jpeg; - } - internal set - { - _imageFormat = value; - } - } - internal string ContentType + public string ContentType { get; - set; + internal set; } /// /// Set the size of the image in percent from the orginal size @@ -412,21 +291,22 @@ internal string ContentType /// Percent public override void SetSize(int Percent) { - // Image is only ever set on Windows. - if (!OperatingSystem.IsWindowsVersionAtLeast(6, 1) || Image == null) + var bytes = ImageBytes; + if (bytes == null) { base.SetSize(Percent); } else { - _width = Image.Width; - _height = Image.Height; + var info = ImageHeaderReader.Read(bytes); + _width = info.Width; + _height = info.Height; _width = (int)(_width * ((decimal)Percent / 100)); _height = (int)(_height * ((decimal)Percent / 100)); - SetPixelWidth(_width, Image.HorizontalResolution); - SetPixelHeight(_height, Image.VerticalResolution); + SetPixelWidth(_width, info.HorizontalResolution); + SetPixelHeight(_height, info.VerticalResolution); } } internal Uri UriPic { get; set; } @@ -476,11 +356,6 @@ public override void Dispose() { base.Dispose(); Hyperlink = null; - // MagicSuite MS-25871: a picture created from raw bytes has no Image - if (OperatingSystem.IsWindowsVersionAtLeast(6, 1)) - { - _image?.Dispose(); - } - _image = null; + _imageBytes = null; } } \ No newline at end of file diff --git a/PanoramicData.EPPlus/Drawing/Vml/ExcelVmlDrawingPicture.cs b/PanoramicData.EPPlus/Drawing/Vml/ExcelVmlDrawingPicture.cs index 1f3ee93af..befbff9af 100644 --- a/PanoramicData.EPPlus/Drawing/Vml/ExcelVmlDrawingPicture.cs +++ b/PanoramicData.EPPlus/Drawing/Vml/ExcelVmlDrawingPicture.cs @@ -30,9 +30,7 @@ * Jan Källman License changed GPL-->LGPL 2011-12-16 *******************************************************************************/ using System; -using System.Drawing; using System.Globalization; -using System.Runtime.Versioning; using System.Xml; @@ -124,10 +122,9 @@ public string Title } } /// - /// The image + /// The image file's bytes /// - [SupportedOSPlatform("windows")] - public Image Image + public byte[] ImageBytes { get { @@ -135,7 +132,7 @@ public Image Image if (pck.PartExists(ImageUri)) { var part = pck.GetPart(ImageUri); - return Image.FromStream(part.GetStream()); + return part.GetStream().ToArray(); } else { diff --git a/PanoramicData.EPPlus/ExcelBackgroundImage.cs b/PanoramicData.EPPlus/ExcelBackgroundImage.cs index b243df4b9..4b8dddcbd 100644 --- a/PanoramicData.EPPlus/ExcelBackgroundImage.cs +++ b/PanoramicData.EPPlus/ExcelBackgroundImage.cs @@ -33,9 +33,7 @@ using OfficeOpenXml.Drawing; using OfficeOpenXml.Utils; using System; -using System.Drawing; using System.IO; -using System.Runtime.Versioning; using System.Xml; namespace OfficeOpenXml; @@ -60,11 +58,10 @@ internal ExcelBackgroundImage(XmlNamespaceManager nsm, XmlNode topNode, ExcelWor const string BACKGROUNDPIC_PATH = "d:picture/@r:id"; /// - /// The background image of the worksheet. - /// The image will be saved internally as a jpg. + /// The background image file's bytes, exactly as stored in the package. Null when there is no background image. + /// Setting the property stores the bytes unchanged; set it to null to remove the background image. /// - [SupportedOSPlatform("windows")] - public Image Image + public byte[] ImageBytes { get { @@ -73,7 +70,7 @@ public Image Image { var rel = _workSheet.Part.GetRelationship(relID); var imagePart = _workSheet.Part.Package.GetPart(UriHelper.ResolvePartUri(rel.SourceUri, rel.TargetUri)); - return Image.FromStream(imagePart.GetStream()); + return imagePart.GetStream().ToArray(); } return null; @@ -87,31 +84,41 @@ public Image Image } else { - var img = ImageCompat.GetImageAsByteArray(value); - var ii = _workSheet.Workbook._package.AddImage(img); + var contentType = ExcelPicture.GetContentTypeFromBytes(value); + var uri = GetNewUri(_workSheet._package.Package, "/xl/media/image{0}" + ExcelPicture.GetExtension(contentType)); + var ii = _workSheet.Workbook._package.AddImage(value, uri, contentType); var rel = _workSheet.Part.CreateRelationship(ii.Uri, Packaging.TargetMode.Internal, ExcelPackage.schemaRelationships + "/image"); SetXmlNodeString(BACKGROUNDPIC_PATH, rel.Id); } } } /// + /// Set the picture from a stream containing an image file. The stream is read to the end. + /// + /// The image file's stream. + public void SetFromStream(Stream PictureStream) + { + ArgumentNullException.ThrowIfNull(PictureStream); + using var ms = new MemoryStream(); + PictureStream.CopyTo(ms); + ImageBytes = ms.ToArray(); + } + /// /// Set the picture from an image file. /// The image file will be saved as a blob, so make sure Excel supports the image format. /// /// The image file. - [SupportedOSPlatform("windows")] public void SetFromFile(FileInfo PictureFile) { DeletePrevImage(); - Image img; byte[] fileBytes; try { fileBytes = File.ReadAllBytes(PictureFile.FullName); - img = Image.FromFile(PictureFile.FullName); + ImageHeaderReader.Read(fileBytes); } - catch (Exception ex) + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException) { throw (new InvalidDataException("File is not a supported image-file or is corrupt", ex)); } @@ -137,14 +144,13 @@ public void SetFromFile(FileInfo PictureFile) var rel = _workSheet.Part.CreateRelationship(imageURI, Packaging.TargetMode.Internal, ExcelPackage.schemaRelationships + "/image"); SetXmlNodeString(BACKGROUNDPIC_PATH, rel.Id); } - [SupportedOSPlatform("windows")] private void DeletePrevImage() { var relID = GetXmlNodeString(BACKGROUNDPIC_PATH); if (relID != "") { - var img = ImageCompat.GetImageAsByteArray(Image); - var ii = _workSheet.Workbook._package.GetImageInfo(img); + var img = ImageBytes; + var ii = img == null ? null : _workSheet.Workbook._package.GetImageInfo(img); //Delete the relation _workSheet.Part.DeleteRelationship(relID); diff --git a/PanoramicData.EPPlus/ExcelColumn.cs b/PanoramicData.EPPlus/ExcelColumn.cs index 439e19468..8450d950a 100644 --- a/PanoramicData.EPPlus/ExcelColumn.cs +++ b/PanoramicData.EPPlus/ExcelColumn.cs @@ -31,7 +31,6 @@ *******************************************************************************/ using OfficeOpenXml.Style; using System; -using System.Runtime.Versioning; using System.Xml; namespace OfficeOpenXml; @@ -250,7 +249,6 @@ public bool Merged /// Note: Cells containing formulas are ignored since EPPlus don't have a calculation engine. /// Wrapped and merged cells are also ignored. /// - [SupportedOSPlatform("windows")] public void AutoFit() => _worksheet.Cells[1, _columnMin, ExcelPackage.MaxRows, _columnMax].AutoFitColumns(); /// @@ -259,7 +257,6 @@ public bool Merged /// Wrapped and merged cells are also ignored. /// /// Minimum column width - [SupportedOSPlatform("windows")] public void AutoFit(double MinimumWidth) => _worksheet.Cells[1, _columnMin, ExcelPackage.MaxRows, _columnMax].AutoFitColumns(MinimumWidth); /// @@ -269,7 +266,6 @@ public bool Merged /// /// Minimum column width /// Maximum column width - [SupportedOSPlatform("windows")] public void AutoFit(double MinimumWidth, double MaximumWidth) => _worksheet.Cells[1, _columnMin, ExcelPackage.MaxRows, _columnMax].AutoFitColumns(MinimumWidth, MaximumWidth); /// diff --git a/PanoramicData.EPPlus/ExcelHeaderFooter.cs b/PanoramicData.EPPlus/ExcelHeaderFooter.cs index 2eea33e7a..e04d333bd 100644 --- a/PanoramicData.EPPlus/ExcelHeaderFooter.cs +++ b/PanoramicData.EPPlus/ExcelHeaderFooter.cs @@ -35,9 +35,7 @@ using OfficeOpenXml.Drawing.Vml; using OfficeOpenXml.Utils; using System; -using System.Drawing; using System.IO; -using System.Runtime.Versioning; using System.Xml; namespace OfficeOpenXml; @@ -124,31 +122,42 @@ private void SetText(string code, string text) /// /// Inserts a picture at the end of the text in the header or footer /// - /// The image object containing the Picture + /// The image file's bytes (PNG, JPEG, GIF, BMP, TIFF, EMF or WMF) /// Alignment. The image object will be inserted at the end of the Text. - [SupportedOSPlatform("windows")] - public ExcelVmlDrawingPicture InsertPicture(Image Picture, PictureAlignment Alignment) + public ExcelVmlDrawingPicture InsertPicture(byte[] Picture, PictureAlignment Alignment) { + ArgumentNullException.ThrowIfNull(Picture); var id = ValidateImage(Alignment); + var header = ReadHeader(Picture); + var contentType = header.ContentType; + var uriPic = XmlHelper.GetNewUri(_ws._package.Package, "/xl/media/image{0}" + ExcelPicture.GetExtension(contentType)); + var ii = _ws.Workbook._package.AddImage(Picture, uriPic, contentType); - //Add the image - var img = ImageCompat.GetImageAsByteArray(Picture); - - var ii = _ws.Workbook._package.AddImage(img); - - return AddImage(Picture, id, ii); + return AddImage(header, id, ii); } /// /// Inserts a picture at the end of the text in the header or footer /// - /// The image object containing the Picture + /// A stream containing the image file. It is read to the end. + /// Alignment. The image object will be inserted at the end of the Text. + public ExcelVmlDrawingPicture InsertPicture(Stream Picture, PictureAlignment Alignment) + { + ArgumentNullException.ThrowIfNull(Picture); + using var ms = new MemoryStream(); + Picture.CopyTo(ms); + return InsertPicture(ms.ToArray(), Alignment); + } + /// + /// Inserts a picture at the end of the text in the header or footer + /// + /// The image file /// Alignment. The image object will be inserted at the end of the Text. - [SupportedOSPlatform("windows")] public ExcelVmlDrawingPicture InsertPicture(FileInfo PictureFile, PictureAlignment Alignment) { var id = ValidateImage(Alignment); - Image Picture; + byte[] imgBytes; + ImageHeaderInfo header; try { if (!PictureFile.Exists) @@ -156,7 +165,8 @@ public ExcelVmlDrawingPicture InsertPicture(FileInfo PictureFile, PictureAlignme throw (new FileNotFoundException(string.Format("{0} is missing", PictureFile.FullName))); } - Picture = Image.FromFile(PictureFile.FullName); + imgBytes = File.ReadAllBytes(PictureFile.FullName); + header = ReadHeader(imgBytes); } catch (Exception ex) { @@ -165,15 +175,15 @@ public ExcelVmlDrawingPicture InsertPicture(FileInfo PictureFile, PictureAlignme var contentType = ExcelPicture.GetContentType(PictureFile.Extension); var uriPic = XmlHelper.GetNewUri(_ws._package.Package, "/xl/media/" + PictureFile.Name[..^PictureFile.Extension.Length] + "{0}" + PictureFile.Extension); - var imgBytes = ImageCompat.GetImageAsByteArray(Picture); var ii = _ws.Workbook._package.AddImage(imgBytes, uriPic, contentType); - return AddImage(Picture, id, ii); + return AddImage(header, id, ii); } - [SupportedOSPlatform("windows")] - private ExcelVmlDrawingPicture AddImage(Image Picture, string id, ExcelPackage.ImageInfo ii) + private static ImageHeaderInfo ReadHeader(byte[] bytes) => ImageHeaderReader.Read(bytes); + + private ExcelVmlDrawingPicture AddImage(ImageHeaderInfo Picture, string id, ExcelPackage.ImageInfo ii) { double width = Picture.Width * 72 / Picture.HorizontalResolution, //Pixel --> Points height = Picture.Height * 72 / Picture.VerticalResolution; //Pixel --> Points diff --git a/PanoramicData.EPPlus/ExcelRangeBase.cs b/PanoramicData.EPPlus/ExcelRangeBase.cs index 7970170ca..7a96ac978 100644 --- a/PanoramicData.EPPlus/ExcelRangeBase.cs +++ b/PanoramicData.EPPlus/ExcelRangeBase.cs @@ -49,7 +49,6 @@ using System.IO; using System.Linq; using System.Reflection; -using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using System.Xml; @@ -746,7 +745,6 @@ private ExcelAddressBase GetAddressDim(ExcelRangeBase addr) /// Note: Cells containing formulas must be calculated before autofit is called. /// Wrapped and merged cells are also ignored. /// - [SupportedOSPlatform("windows")] public void AutoFitColumns() => AutoFitColumns(_worksheet.DefaultColWidth); /// @@ -754,9 +752,7 @@ private ExcelAddressBase GetAddressDim(ExcelRangeBase addr) /// Note: Cells containing formulas are ignored if no calculation is made. /// Wrapped and merged cells are also ignored. /// - /// This method will not work if you run in an environment that does not support GDI /// Minimum column width - [SupportedOSPlatform("windows")] public void AutoFitColumns(double MinimumWidth) => AutoFitColumns(MinimumWidth, double.MaxValue); /// @@ -767,7 +763,6 @@ private ExcelAddressBase GetAddressDim(ExcelRangeBase addr) /// /// Minimum column width /// Maximum column width - [SupportedOSPlatform("windows")] public void AutoFitColumns(double MinimumWidth, double MaximumWidth) { if (_worksheet.Dimension == null) @@ -780,7 +775,6 @@ public void AutoFitColumns(double MinimumWidth, double MaximumWidth) SetToSelectedRange(); } - var fontCache = new Dictionary(); var doAdjust = _worksheet._package.DoAdjustDrawings; _worksheet._package.DoAdjustDrawings = false; @@ -833,45 +827,17 @@ public void AutoFitColumns(double MinimumWidth, double MaximumWidth) var styles = _worksheet.Workbook.Styles; var nf = styles.Fonts[styles.CellXfs[0].FontId]; - var fs = FontStyle.Regular; - if (nf.Bold) - { - fs |= FontStyle.Bold; - } - - if (nf.UnderLine) - { - fs |= FontStyle.Underline; - } - - if (nf.Italic) - { - fs |= FontStyle.Italic; - } - - if (nf.Strike) - { - fs |= FontStyle.Strikeout; - } - - var nfont = new Font(nf.Name, nf.Size, fs); - var normalSize = Convert.ToSingle(ExcelWorkbook.GetWidthPixels(nf.Name, nf.Size)); - Bitmap b; - Graphics g = null; - try - { - //Check for missing GDI+, then use WPF instead. - b = new Bitmap(1, 1); - g = Graphics.FromImage(b); - g.PageUnit = GraphicsUnit.Pixel; - } - catch + if (!TextMeasurer.IsAvailable) { + //No usable font engine (native SkiaSharp library missing): leave the widths as they are. + _worksheet.Drawings.AdjustWidth(drawWidths); + _worksheet._package.DoAdjustDrawings = doAdjust; return; } + using var measurer = new TextMeasurer(); foreach (var cell in this) { if (_worksheet.Column(cell.Start.Column).Hidden) //Issue 15338 @@ -885,40 +851,7 @@ public void AutoFitColumns(double MinimumWidth, double MaximumWidth) } var fntID = styles.CellXfs[cell.StyleID].FontId; - Font f; - if (fontCache.TryGetValue(fntID, out var value)) - { - f = value; - } - else - { - var fnt = styles.Fonts[fntID]; - fs = FontStyle.Regular; - if (fnt.Bold) - { - fs |= FontStyle.Bold; - } - - if (fnt.UnderLine) - { - fs |= FontStyle.Underline; - } - - if (fnt.Italic) - { - fs |= FontStyle.Italic; - } - - if (fnt.Strike) - { - fs |= FontStyle.Strikeout; - } - - f = new Font(fnt.Name, fnt.Size, fs); - - fontCache.Add(fntID, f); - } - + var fnt = styles.Fonts[fntID]; var ind = styles.CellXfs[cell.StyleID].Indent; var textForWidth = cell.TextForWidth; var t = textForWidth + (ind > 0 && !string.IsNullOrEmpty(textForWidth) ? new string('_', ind) : ""); @@ -927,7 +860,7 @@ public void AutoFitColumns(double MinimumWidth, double MaximumWidth) t = t[..32000]; //Issue } - var size = g.MeasureString(t, f, 10000, StringFormat.GenericDefault); + var size = measurer.Measure(t, fnt.Name, fnt.Size, fnt.Bold, fnt.Italic); double width; double r = styles.CellXfs[cell.StyleID].TextRotation; diff --git a/PanoramicData.EPPlus/ExcelWorkbook.cs b/PanoramicData.EPPlus/ExcelWorkbook.cs index 750a5fe28..4598d3542 100644 --- a/PanoramicData.EPPlus/ExcelWorkbook.cs +++ b/PanoramicData.EPPlus/ExcelWorkbook.cs @@ -325,7 +325,7 @@ public FormulaParserManager FormulaParserManager } /// /// Max font width for the workbook - /// This method uses GDI. If you use Asure or another environment that does not support GDI, you have to set this value manually if you don't use the standard Calibri font + /// Taken from a built-in table of font metrics. For a font that is not in the table, set this value manually. /// public decimal MaxFontWidth { diff --git a/PanoramicData.EPPlus/ExcelWorksheets.cs b/PanoramicData.EPPlus/ExcelWorksheets.cs index 3b2a87dcb..caa6db6a6 100644 --- a/PanoramicData.EPPlus/ExcelWorksheets.cs +++ b/PanoramicData.EPPlus/ExcelWorksheets.cs @@ -728,13 +728,9 @@ private void CopyDrawing(ExcelWorksheet Copy, ExcelWorksheet workSheet/*, Packag var uri = pic.UriPic; if (!workSheet.Workbook._package.Package.PartExists(uri)) { - if (!OperatingSystem.IsWindowsVersionAtLeast(6, 1)) - { - throw new PlatformNotSupportedException("Copying a picture to another package uses System.Drawing, which is only supported on Windows."); - } - var picPart = workSheet.Workbook._package.Package.CreatePart(uri, pic.ContentType, CompressionLevel.None); - pic.Image.Save(picPart.GetStream(FileMode.Create, FileAccess.Write), ExcelPicture.GetImageFormat(pic.ContentType)); + var imageBytes = pic.ImageBytes; + picPart.GetStream(FileMode.Create, FileAccess.Write).Write(imageBytes, 0, imageBytes.Length); } var rel = part.CreateRelationship(UriHelper.GetRelativeUri(workSheet.WorksheetUri, uri), Packaging.TargetMode.Internal, ExcelPackage.schemaRelationships + "/image"); diff --git a/PanoramicData.EPPlus/PanoramicData.EPPlus.csproj b/PanoramicData.EPPlus/PanoramicData.EPPlus.csproj index cce209abe..fa65556ee 100644 --- a/PanoramicData.EPPlus/PanoramicData.EPPlus.csproj +++ b/PanoramicData.EPPlus/PanoramicData.EPPlus.csproj @@ -21,6 +21,11 @@ Large workbooks can now be saved. A workbook that exceeded the classic ZIP limits, being 4 GB of size or offset or 65,534 internal entries, previously threw while saving, after all of its content had been built. Zip64 is now used automatically when, and only when, a workbook needs it, so files that fit the classic format are written exactly as before and remain readable by older tools. This release targets .NET 10. The previous release, 10.1.1, targeted .NET 9. + + 11.0 - BREAKING: fully cross-platform. System.Drawing.Common (GDI+) is no longer used, so the library runs the same on Windows, Linux and macOS. + Removed: ExcelDrawings.AddPicture(string, Image[, Uri]); ExcelPicture.Image and ExcelPicture.ImageFormat; ExcelBackgroundImage.Image; ExcelVmlDrawingPicture.Image; ExcelHeaderFooterText.InsertPicture(Image, PictureAlignment); ExcelFont.SetFromFont(Font), ExcelTextFont.SetFromFont(Font) and ExcelFontXml.SetFromFont(Font); the [SupportedOSPlatform("windows")] attributes. + Added: ExcelDrawings.AddPicture(string, byte[]) and AddPicture(string, Stream) (with optional hyperlink); ExcelPicture.ImageBytes and ExcelPicture.ContentType; ExcelBackgroundImage.ImageBytes and SetFromStream; ExcelVmlDrawingPicture.ImageBytes; ExcelHeaderFooterText.InsertPicture(byte[], ...) and InsertPicture(Stream, ...). Image size and resolution are read from the PNG, JPEG, GIF, BMP, TIFF, EMF or WMF header and the file bytes are stored unchanged (pictures are no longer re-encoded as JPEG). + AutoFitColumns now measures text with SkiaSharp (MIT) instead of GDI+. Results can differ slightly from earlier versions, and on a machine without the named font the platform default font is measured. False OpenOfficeXml.Rimland.snk @@ -51,7 +56,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/PanoramicData.EPPlus/Style/ExcelFont.cs b/PanoramicData.EPPlus/Style/ExcelFont.cs index 9f3f8f665..322a28b03 100644 --- a/PanoramicData.EPPlus/Style/ExcelFont.cs +++ b/PanoramicData.EPPlus/Style/ExcelFont.cs @@ -31,7 +31,6 @@ *******************************************************************************/ using System; using System.Drawing; -using System.Runtime.Versioning; namespace OfficeOpenXml.Style; @@ -197,21 +196,6 @@ public ExcelVerticalAlignmentFont VerticalAlign _ChangedEvent(this, new StyleChangeEventArgs(eStyleClass.Font, eStyleProperty.VerticalAlign, value, _positionID, _address)); } } - /// - /// Set the font from a Font object - /// - /// - [SupportedOSPlatform("windows")] - public void SetFromFont(Font Font) - { - Name = Font.Name; - //Family=fnt.FontFamily.; - Size = (int)Font.Size; - Strike = Font.Strikeout; - Bold = Font.Bold; - UnderLine = Font.Underline; - Italic = Font.Italic; - } internal override string Id => Name + Size.ToString() + Family.ToString() + Scheme.ToString() + Bold.ToString()[0] + Italic.ToString()[0] + Strike.ToString()[0] + UnderLine.ToString()[0] + VerticalAlign; } diff --git a/PanoramicData.EPPlus/Style/ExcelTextFont.cs b/PanoramicData.EPPlus/Style/ExcelTextFont.cs index 114e03389..9613a8824 100644 --- a/PanoramicData.EPPlus/Style/ExcelTextFont.cs +++ b/PanoramicData.EPPlus/Style/ExcelTextFont.cs @@ -32,7 +32,6 @@ using System; using System.Drawing; using System.Globalization; -using System.Runtime.Versioning; using System.Xml; namespace OfficeOpenXml.Style; @@ -255,34 +254,4 @@ private static string TranslateUnderlineText(eUnderLineType value) _ => "noStrike", }; #endregion - /// - /// Set the font style from a font object - /// - /// - [SupportedOSPlatform("windows")] - public void SetFromFont(Font Font) - { - LatinFont = Font.Name; - ComplexFont = Font.Name; - Size = Font.Size; - if (Font.Bold) - { - Bold = Font.Bold; - } - - if (Font.Italic) - { - Italic = Font.Italic; - } - - if (Font.Underline) - { - UnderLine = eUnderLineType.Single; - } - - if (Font.Strikeout) - { - Strike = eStrikeType.Single; - } - } } diff --git a/PanoramicData.EPPlus/Style/XmlAccess/ExcelFontXml.cs b/PanoramicData.EPPlus/Style/XmlAccess/ExcelFontXml.cs index 0ccc4aedf..e05ba66de 100644 --- a/PanoramicData.EPPlus/Style/XmlAccess/ExcelFontXml.cs +++ b/PanoramicData.EPPlus/Style/XmlAccess/ExcelFontXml.cs @@ -32,7 +32,6 @@ using System; using System.Drawing; using System.Globalization; -using System.Runtime.Versioning; using System.Xml; namespace OfficeOpenXml.Style.XmlAccess; @@ -262,17 +261,6 @@ public string VerticalAlign _verticalAlign = value; } } - [SupportedOSPlatform("windows")] - public void SetFromFont(System.Drawing.Font Font) - { - Name = Font.Name; - //Family=fnt.FontFamily.; - Size = (int)Font.Size; - Strike = Font.Strikeout; - Bold = Font.Bold; - UnderLine = Font.Underline; - Italic = Font.Italic; - } public static float GetFontHeight(string name, float size) { name = name.StartsWith("@") ? name[1..] : name; diff --git a/SampleApp/Sample1.cs b/SampleApp/Sample1.cs index 119129ce3..c70fe646d 100644 --- a/SampleApp/Sample1.cs +++ b/SampleApp/Sample1.cs @@ -29,7 +29,6 @@ ******************************************************************************* * Jan Källman Added 21 Mar 2010 *******************************************************************************/ -using System.Runtime.Versioning; using OfficeOpenXml; using System.Drawing; using OfficeOpenXml.Style; @@ -42,7 +41,6 @@ class Sample1 /// Sample 1 - simply creates a new workbook from scratch. /// The workbook contains one worksheet with a simple invertory list /// - [SupportedOSPlatform("windows")] public static string RunSample1() { using var package = new ExcelPackage(); diff --git a/SampleApp/Sample12.cs b/SampleApp/Sample12.cs index db4dfb409..f6c1bfad2 100644 --- a/SampleApp/Sample12.cs +++ b/SampleApp/Sample12.cs @@ -26,7 +26,6 @@ * ****************************************************************************** * Jan Källman Added 2011-04-18 *******************************************************************************/ -using System.Runtime.Versioning; using System; using System.Collections.Generic; using System.Linq; @@ -54,7 +53,6 @@ public class SalesDTO public decimal Freight { get; set; } public decimal Total => SubTotal + Tax + Freight; } - [SupportedOSPlatform("windows")] public static string RunSample12(string connectionStr) { var list = new List(); diff --git a/SampleApp/Sample13.cs b/SampleApp/Sample13.cs index 3736e720d..b3a529601 100644 --- a/SampleApp/Sample13.cs +++ b/SampleApp/Sample13.cs @@ -26,7 +26,6 @@ * ****************************************************************************** * Jan Källman Added 2011-05-03 *******************************************************************************/ -using System.Runtime.Versioning; using System; using System.Linq; using System.IO; @@ -52,7 +51,6 @@ public class FileDTO public override string ToString() => IsDirectory ? Name + "\t" : Name + "\t" + Size.ToString("#,##0"); } - [SupportedOSPlatform("windows")] public static void RunSample13() { var pck = new ExcelPackage(); diff --git a/SampleApp/Sample16.cs b/SampleApp/Sample16.cs index d43da6906..92a33918e 100644 --- a/SampleApp/Sample16.cs +++ b/SampleApp/Sample16.cs @@ -26,7 +26,6 @@ * ****************************************************************************** * Jan Källman Added 2018-03-20 *******************************************************************************/ -using System.Runtime.Versioning; using OfficeOpenXml; using OfficeOpenXml.Sparkline; using OfficeOpenXml.Table; @@ -38,7 +37,6 @@ namespace SampleApp; public static class Sample16 { - [SupportedOSPlatform("windows")] public static void RunSample16() { using var package = new ExcelPackage(); diff --git a/SampleApp/Sample3.cs b/SampleApp/Sample3.cs index c7640754d..9dff81494 100644 --- a/SampleApp/Sample3.cs +++ b/SampleApp/Sample3.cs @@ -46,7 +46,6 @@ * All code and executables are provided "as is" with no warranty either express or implied. * The author accepts no liability for any damage or loss of business that this product may cause. */ -using System.Runtime.Versioning; using System; using OfficeOpenXml; using Microsoft.Data.SqlClient; @@ -65,7 +64,6 @@ class Sample3 /// The output directory /// The location of the sample template /// The connection string to your copy of the AdventureWorks database - [SupportedOSPlatform("windows")] public static string RunSample3(string connectionString) { var file = Utils.GetFileInfo("Sample3.xlsx"); @@ -86,7 +84,9 @@ public static string RunSample3(string connectionString) using (var r = worksheet.Cells["A1:G1"]) { r.Merge = true; - r.Style.Font.SetFromFont(new Font("Britannic Bold", 22, FontStyle.Italic)); + r.Style.Font.Name = "Britannic Bold"; + r.Style.Font.Size = 22; + r.Style.Font.Italic = true; r.Style.Font.Color.SetColor(Color.White); r.Style.HorizontalAlignment = ExcelHorizontalAlignment.CenterContinuous; r.Style.Fill.PatternType = ExcelFillStyle.Solid; @@ -97,7 +97,9 @@ public static string RunSample3(string connectionString) using (var r = worksheet.Cells["A2:G2"]) { r.Merge = true; - r.Style.Font.SetFromFont(new Font("Britannic Bold", 18, FontStyle.Italic)); + r.Style.Font.Name = "Britannic Bold"; + r.Style.Font.Size = 18; + r.Style.Font.Italic = true; r.Style.Font.Color.SetColor(Color.Black); r.Style.HorizontalAlignment = ExcelHorizontalAlignment.CenterContinuous; r.Style.Fill.PatternType = ExcelFillStyle.Solid; diff --git a/SampleApp/Sample6.cs b/SampleApp/Sample6.cs index ae3e0858f..bd9d9c5c7 100644 --- a/SampleApp/Sample6.cs +++ b/SampleApp/Sample6.cs @@ -29,7 +29,6 @@ ******************************************************************************* * Jan Källman Added 25-JAN-2010 *******************************************************************************/ -using System.Runtime.Versioning; using OfficeOpenXml; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; @@ -72,7 +71,6 @@ public int CompareTo(StatItem other) => Size < other.Size ? -1 : /// Directory to scan /// How many levels? /// Skip the icons in column A. A lot faster - [SupportedOSPlatform("windows")] public static string RunSample6(DirectoryInfo dir, int depth, bool skipIcons) { _maxLevels = depth; @@ -212,7 +210,6 @@ private static void AddComments(ExcelWorksheet ws) /// Package /// /// - [SupportedOSPlatform("windows")] private static void AddGraphs(ExcelPackage pck, int rows, string dir) { var ws = pck.Workbook.Worksheets.Add("Statistics"); @@ -223,7 +220,8 @@ private static void AddGraphs(ExcelPackage pck, int rows, string dir) using (var r = ws.Cells["A1:O1"]) { r.Merge = true; - r.Style.Font.SetFromFont(new Font("Arial", 22, FontStyle.Regular)); + r.Style.Font.Name = "Arial"; + r.Style.Font.Size = 22; r.Style.Font.Color.SetColor(Color.White); r.Style.HorizontalAlignment = ExcelHorizontalAlignment.CenterContinuous; r.Style.Fill.PatternType = ExcelFillStyle.Solid; @@ -311,7 +309,6 @@ private static void AddGraphs(ExcelPackage pck, int rows, string dir) /// Header text /// Size or Count /// - [SupportedOSPlatform("windows")] private static int AddStatRows(ExcelWorksheet ws, List lst, int startRow, string header, string propertyName) { //Add Headers @@ -322,7 +319,9 @@ private static int AddStatRows(ExcelWorksheet ws, List lst, int startR using (var r = ws.Cells[row, 1, row, 2]) { r.Merge = true; - r.Style.Font.SetFromFont(new Font("Arial", 16, FontStyle.Italic)); + r.Style.Font.Name = "Arial"; + r.Style.Font.Size = 16; + r.Style.Font.Italic = true; r.Style.Font.Color.SetColor(Color.White); r.Style.HorizontalAlignment = ExcelHorizontalAlignment.CenterContinuous; r.Style.Fill.PatternType = ExcelFillStyle.Solid; @@ -338,7 +337,9 @@ private static int AddStatRows(ExcelWorksheet ws, List lst, int startR ws.Cells[row, 2].Value = propertyName; using (var r = ws.Cells[row, 1, row, 2]) { - r.Style.Font.SetFromFont(new Font("Arial", 12, FontStyle.Bold)); + r.Style.Font.Name = "Arial"; + r.Style.Font.Size = 12; + r.Style.Font.Bold = true; } row++; @@ -409,22 +410,12 @@ private static void AlterColor(ExcelWorksheet ws, int row) } } - [SupportedOSPlatform("windows")] private static int AddDirectory(ExcelWorksheet ws, DirectoryInfo dir, int row, double height, int level, bool skipIcons) { - //Get the icon as a bitmap Console.WriteLine("Directory " + dir.Name); if (!skipIcons) { - var icon = GetIcon(dir.FullName); - ws.Row(row).Height = height; - //Add the icon as a picture - if (icon != null) - { - var pic = ws.Drawings.AddPicture("pic" + row.ToString(), icon); - pic.SetPosition(20 * (row - 1) + 2, 0); - } } ws.Cells[row, 2].Value = dir.Name; @@ -451,14 +442,7 @@ private static int AddDirectory(ExcelWorksheet ws, DirectoryInfo dir, int row, d { if (!skipIcons) { - var fileIcon = GetIcon(file.FullName); - ws.Row(row).Height = height; - if (fileIcon != null) - { - var pic = ws.Drawings.AddPicture("pic" + row.ToString(), fileIcon); - pic.SetPosition(20 * (row - 1) + 2, 0); - } } ws.Cells[row, 2].Value = file.Name; @@ -519,22 +503,4 @@ private static void AddStatistics(FileInfo file) _fileSize.Sort(); } } - /// - /// Gets the icon for a file or directory - /// - /// - /// - [SupportedOSPlatform("windows")] - private static Bitmap GetIcon(string FileName) - { - if (File.Exists(FileName)) - { - var bmp = Icon.ExtractAssociatedIcon(FileName).ToBitmap(); - return new Bitmap(bmp, new Size(16, 16)); - } - else - { - return null; - } - } } diff --git a/SampleApp/Sample7.cs b/SampleApp/Sample7.cs index 56adeff67..0e1752cd8 100644 --- a/SampleApp/Sample7.cs +++ b/SampleApp/Sample7.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.Versioning; using OfficeOpenXml; using OfficeOpenXml.Style; using System.Drawing; @@ -13,7 +12,6 @@ class Sample7 /// A password is set to protect locked cells. Column 3 and 4 will be editable, the rest will be locked. /// /// - [SupportedOSPlatform("windows")] public static string RunSample7(int rows) { var newFile = Utils.GetFileInfo("sample7.xlsx"); diff --git a/SampleApp/Sample9.cs b/SampleApp/Sample9.cs index 67bef558f..5be8dcc97 100644 --- a/SampleApp/Sample9.cs +++ b/SampleApp/Sample9.cs @@ -29,7 +29,6 @@ ******************************************************************************* * Jan Källman Added 28 Oct 2010 *******************************************************************************/ -using System.Runtime.Versioning; using System; using OfficeOpenXml; using System.IO; @@ -50,7 +49,6 @@ public static class Sample9 /// /// /// - [SupportedOSPlatform("windows")] public static string RunSample9() { var newFile = Utils.GetFileInfo(@"sample9.xlsx"); @@ -65,7 +63,6 @@ public static string RunSample9() return newFile.FullName; } - [SupportedOSPlatform("windows")] private static void LoadFile1(ExcelPackage package) { //Create the Worksheet @@ -132,7 +129,6 @@ private static void LoadFile1(ExcelPackage package) sheet.Cells[sheet.Dimension.Address].AutoFitColumns(); } - [SupportedOSPlatform("windows")] private static void LoadFile2(ExcelPackage package) { //Create the Worksheet diff --git a/SampleApp/Sample_Main.cs b/SampleApp/Sample_Main.cs index 253c801ec..7fd5f4533 100644 --- a/SampleApp/Sample_Main.cs +++ b/SampleApp/Sample_Main.cs @@ -29,7 +29,6 @@ ******************************************************************************* * Jan Källman Added 10-SEP-2009 *******************************************************************************/ -using System.Runtime.Versioning; using System; using System.IO; @@ -37,7 +36,6 @@ namespace SampleApp; class Sample_Main { - [SupportedOSPlatform("windows")] static void Main(string[] args) { try diff --git a/version.json b/version.json index 64643762b..cc2f86cb5 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "10.1", + "version": "11.0", "publicReleaseRefSpec": [ "^refs/heads/main$", "^refs/heads/master$",