From 9533f80e89e5875c60e78fc3f9e042b8a1033d4d Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Mon, 3 Nov 2025 12:58:44 -0700 Subject: [PATCH] Replace NPOI excel workbook library with ClosedXML - Reduce build bundles by 30-40 MB --- .../ApplicationServices.csproj | 2 +- Source/ApplicationServices/LibraryExporter.cs | 137 ++++++++---------- Source/ApplicationServices/RecordExporter.cs | 64 ++++---- Source/LibationUiBase/IcoEncoder.cs | 52 ------- Source/LibationUiBase/MessageBoxBase.cs | 4 +- .../LoadByOS/WindowsConfigApp/IcoEncoder.cs | 51 +++++++ .../WindowsConfigApp/WindowsConfigApp.csproj | 4 + 7 files changed, 143 insertions(+), 171 deletions(-) delete mode 100644 Source/LibationUiBase/IcoEncoder.cs create mode 100644 Source/LoadByOS/WindowsConfigApp/IcoEncoder.cs diff --git a/Source/ApplicationServices/ApplicationServices.csproj b/Source/ApplicationServices/ApplicationServices.csproj index c1453c2c..2105a0b0 100644 --- a/Source/ApplicationServices/ApplicationServices.csproj +++ b/Source/ApplicationServices/ApplicationServices.csproj @@ -6,7 +6,7 @@ - + diff --git a/Source/ApplicationServices/LibraryExporter.cs b/Source/ApplicationServices/LibraryExporter.cs index 1f16052a..7ec92f0b 100644 --- a/Source/ApplicationServices/LibraryExporter.cs +++ b/Source/ApplicationServices/LibraryExporter.cs @@ -1,11 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using ClosedXML.Excel; using CsvHelper; using CsvHelper.Configuration.Attributes; using DataLayer; using Newtonsoft.Json; -using NPOI.XSSF.UserModel; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; namespace ApplicationServices { @@ -208,19 +209,11 @@ namespace ApplicationServices { var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos(); - var workbook = new XSSFWorkbook(); - var sheet = workbook.CreateSheet("Library"); + using var workbook = new XLWorkbook(); + var sheet = workbook.AddWorksheet("Library"); - var detailSubtotalFont = workbook.CreateFont(); - detailSubtotalFont.IsBold = true; - - var detailSubtotalCellStyle = workbook.CreateCellStyle(); - detailSubtotalCellStyle.SetFont(detailSubtotalFont); // headers - var rowIndex = 0; - var row = sheet.CreateRow(rowIndex); - var columns = new[] { nameof(ExportDto.Account), nameof(ExportDto.DateAdded), @@ -261,81 +254,71 @@ namespace ApplicationServices nameof(ExportDto.ChannelCount), nameof(ExportDto.BitRate) }; - var col = 0; + + int rowIndex = 1, col = 1; + var headerRow = sheet.Row(rowIndex++); foreach (var c in columns) { - var cell = row.CreateCell(col++); - var name = ExportDto.GetName(c); - cell.SetCellValue(name); - cell.CellStyle = detailSubtotalCellStyle; + var headerCell = headerRow.Cell(col++); + headerCell.Value = ExportDto.GetName(c); + headerCell.Style.Font.Bold = true; } - var dateFormat = workbook.CreateDataFormat(); - var dateStyle = workbook.CreateCellStyle(); - dateStyle.DataFormat = dateFormat.GetFormat("MM/dd/yyyy HH:mm:ss"); - - rowIndex++; + var dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern + " HH:mm:ss"; // Add data rows foreach (var dto in dtos) { - col = 0; - row = sheet.CreateRow(rowIndex++); + col = 1; + var row = sheet.Row(rowIndex++); - row.CreateCell(col++).SetCellValue(dto.Account); - row.CreateCell(col++).SetCellValue(dto.DateAdded).CellStyle = dateStyle; - row.CreateCell(col++).SetCellValue(dto.AudibleProductId); - row.CreateCell(col++).SetCellValue(dto.Locale); - row.CreateCell(col++).SetCellValue(dto.Title); - row.CreateCell(col++).SetCellValue(dto.Subtitle); - row.CreateCell(col++).SetCellValue(dto.AuthorNames); - row.CreateCell(col++).SetCellValue(dto.NarratorNames); - row.CreateCell(col++).SetCellValue(dto.LengthInMinutes); - row.CreateCell(col++).SetCellValue(dto.Description); - row.CreateCell(col++).SetCellValue(dto.Publisher); - row.CreateCell(col++).SetCellValue(dto.HasPdf); - row.CreateCell(col++).SetCellValue(dto.SeriesNames); - row.CreateCell(col++).SetCellValue(dto.SeriesOrder); - row.CreateCell(col++).SetCellValue(dto.CommunityRatingOverall); - row.CreateCell(col++).SetCellValue(dto.CommunityRatingPerformance); - row.CreateCell(col++).SetCellValue(dto.CommunityRatingStory); - row.CreateCell(col++).SetCellValue(dto.PictureId); - row.CreateCell(col++).SetCellValue(dto.IsAbridged); - row.CreateCell(col++).SetCellValue(dto.DatePublished).CellStyle = dateStyle; - row.CreateCell(col++).SetCellValue(dto.CategoriesNames); - row.CreateCell(col++).SetCellValue(dto.MyRatingOverall); - row.CreateCell(col++).SetCellValue(dto.MyRatingPerformance); - row.CreateCell(col++).SetCellValue(dto.MyRatingStory); - row.CreateCell(col++).SetCellValue(dto.MyLibationTags); - row.CreateCell(col++).SetCellValue(dto.BookStatus); - row.CreateCell(col++).SetCellValue(dto.PdfStatus); - row.CreateCell(col++).SetCellValue(dto.ContentType); - row.CreateCell(col++).SetCellValue(dto.Language); - row.CreateCell(col++).SetCellValue(dto.LastDownloaded).CellStyle = dateStyle; - row.CreateCell(col++).SetCellValue(dto.LastDownloadedVersion); - row.CreateCell(col++).SetCellValue(dto.IsFinished); - row.CreateCell(col++).SetCellValue(dto.IsSpatial); - row.CreateCell(col++).SetCellValue(dto.LastDownloadedFileVersion); - row.CreateCell(col++).SetCellValue(dto.CodecString); - row.CreateCell(col++).SetCellValue(dto.SampleRate); - row.CreateCell(col++).SetCellValue(dto.ChannelCount); - row.CreateCell(col++).SetCellValue(dto.BitRate); + row.Cell(col++).Value = dto.Account; + row.Cell(col++).SetDate(dto.DateAdded, dateFormat); + row.Cell(col++).Value = dto.AudibleProductId; + row.Cell(col++).Value = dto.Locale; + row.Cell(col++).Value = dto.Title; + row.Cell(col++).Value = dto.Subtitle; + row.Cell(col++).Value = dto.AuthorNames; + row.Cell(col++).Value = dto.NarratorNames; + row.Cell(col++).Value = dto.LengthInMinutes; + row.Cell(col++).Value = dto.Description; + row.Cell(col++).Value = dto.Publisher; + row.Cell(col++).Value = dto.HasPdf; + row.Cell(col++).Value = dto.SeriesNames; + row.Cell(col++).Value = dto.SeriesOrder; + row.Cell(col++).Value = dto.CommunityRatingOverall; + row.Cell(col++).Value = dto.CommunityRatingPerformance; + row.Cell(col++).Value = dto.CommunityRatingStory; + row.Cell(col++).Value = dto.PictureId; + row.Cell(col++).Value = dto.IsAbridged; + row.Cell(col++).SetDate(dto.DatePublished, dateFormat); + row.Cell(col++).Value = dto.CategoriesNames; + row.Cell(col++).Value = dto.MyRatingOverall; + row.Cell(col++).Value = dto.MyRatingPerformance; + row.Cell(col++).Value = dto.MyRatingStory; + row.Cell(col++).Value = dto.MyLibationTags; + row.Cell(col++).Value = dto.BookStatus; + row.Cell(col++).Value = dto.PdfStatus; + row.Cell(col++).Value = dto.ContentType; + row.Cell(col++).Value = dto.Language; + row.Cell(col++).SetDate(dto.LastDownloaded, dateFormat); + row.Cell(col++).Value = dto.LastDownloadedVersion; + row.Cell(col++).Value = dto.IsFinished; + row.Cell(col++).Value = dto.IsSpatial; + row.Cell(col++).Value = dto.LastDownloadedFileVersion; + row.Cell(col++).Value = dto.CodecString; + row.Cell(col++).Value = dto.SampleRate; + row.Cell(col++).Value = dto.ChannelCount; + row.Cell(col++).Value = dto.BitRate; } - using var fileData = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create); - workbook.Write(fileData); + workbook.SaveAs(saveFilePath); } - private static NPOI.SS.UserModel.ICell SetCellValue(this NPOI.SS.UserModel.ICell cell, DateTime? nullableDate) - => nullableDate.HasValue ? cell.SetCellValue(nullableDate.Value) - : cell.SetCellType(NPOI.SS.UserModel.CellType.Numeric); - - private static NPOI.SS.UserModel.ICell SetCellValue(this NPOI.SS.UserModel.ICell cell, int? nullableInt) - => nullableInt.HasValue ? cell.SetCellValue(nullableInt.Value) - : cell.SetCellType(NPOI.SS.UserModel.CellType.Numeric); - - private static NPOI.SS.UserModel.ICell SetCellValue(this NPOI.SS.UserModel.ICell cell, float? nullableFloat) - => nullableFloat.HasValue ? cell.SetCellValue(nullableFloat.Value) - : cell.SetCellType(NPOI.SS.UserModel.CellType.Numeric); + private static void SetDate(this IXLCell cell, DateTime? value, string dateFormat) + { + cell.Value = value; + cell.Style.DateFormat.Format = dateFormat; + } } } diff --git a/Source/ApplicationServices/RecordExporter.cs b/Source/ApplicationServices/RecordExporter.cs index 52506383..b888222f 100644 --- a/Source/ApplicationServices/RecordExporter.cs +++ b/Source/ApplicationServices/RecordExporter.cs @@ -1,10 +1,11 @@ using AudibleApi.Common; +using ClosedXML.Excel; using CsvHelper; using DataLayer; using Newtonsoft.Json.Linq; -using NPOI.XSSF.UserModel; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace ApplicationServices @@ -16,19 +17,10 @@ namespace ApplicationServices if (!records.Any()) return; - using var workbook = new XSSFWorkbook(); - var sheet = workbook.CreateSheet("Records"); - - var detailSubtotalFont = workbook.CreateFont(); - detailSubtotalFont.IsBold = true; - - var detailSubtotalCellStyle = workbook.CreateCellStyle(); - detailSubtotalCellStyle.SetFont(detailSubtotalFont); + using var workbook = new XLWorkbook(); + var worksheet = workbook.AddWorksheet("Records"); // headers - var rowIndex = 0; - var row = sheet.CreateRow(rowIndex); - var columns = new List { nameof(Type.Name), @@ -49,56 +41,52 @@ namespace ApplicationServices if (records.OfType().Any()) columns.Add(nameof(Clip.Title)); - var col = 0; + int rowIndex = 1, col = 1; + var headerRow = worksheet.Row(rowIndex++); foreach (var c in columns) { - var cell = row.CreateCell(col++); - cell.SetCellValue(c); - cell.CellStyle = detailSubtotalCellStyle; + var headerCell = headerRow.Cell(col++); + headerCell.Value = c; + headerCell.Style.Font.Bold = true; } - var dateFormat = workbook.CreateDataFormat(); - var dateStyle = workbook.CreateCellStyle(); - dateStyle.DataFormat = dateFormat.GetFormat("MM/dd/yyyy HH:mm:ss"); + var dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern + " HH:mm:ss"; // Add data rows foreach (var record in records) { - col = 0; + col = 1; + var row = worksheet.Row(rowIndex++); - row = sheet.CreateRow(++rowIndex); - - row.CreateCell(col++).SetCellValue(record.GetType().Name); - - var dateCreatedCell = row.CreateCell(col++); - dateCreatedCell.CellStyle = dateStyle; - dateCreatedCell.SetCellValue(record.Created.DateTime); - - row.CreateCell(col++).SetCellValue(record.Start.TotalMilliseconds); + row.Cell(col++).Value = record.GetType().Name; + row.Cell(col++).SetDate(record.Created.DateTime, dateFormat); + row.Cell(col++).Value = record.Start.TotalMilliseconds; if (record is IAnnotation annotation) { - row.CreateCell(col++).SetCellValue(annotation.AnnotationId); - var lastModifiedCell = row.CreateCell(col++); - lastModifiedCell.CellStyle = dateStyle; - lastModifiedCell.SetCellValue(annotation.LastModified.DateTime); + row.Cell(col++).Value = annotation.AnnotationId; + row.Cell(col++).SetDate(annotation.LastModified.DateTime, dateFormat); if (annotation is IRangeAnnotation rangeAnnotation) { - row.CreateCell(col++).SetCellValue(rangeAnnotation.End.TotalMilliseconds); - row.CreateCell(col++).SetCellValue(rangeAnnotation.Text); + row.Cell(col++).Value = rangeAnnotation.End.TotalMilliseconds; + row.Cell(col++).Value = rangeAnnotation.Text; if (rangeAnnotation is Clip clip) - row.CreateCell(col++).SetCellValue(clip.Title); + row.Cell(col++).Value = clip.Title; } } } - using var fileData = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create); - workbook.Write(fileData); + workbook.SaveAs(saveFilePath); } + private static void SetDate(this IXLCell cell, DateTime? value, string dateFormat) + { + cell.Value = value; + cell.Style.DateFormat.Format = dateFormat; + } public static void ToJson(string saveFilePath, LibraryBook libraryBook, IEnumerable records) { if (!records.Any()) diff --git a/Source/LibationUiBase/IcoEncoder.cs b/Source/LibationUiBase/IcoEncoder.cs deleted file mode 100644 index 92b445f9..00000000 --- a/Source/LibationUiBase/IcoEncoder.cs +++ /dev/null @@ -1,52 +0,0 @@ -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.PixelFormats; -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace LibationUiBase -{ - public class IcoEncoder : IImageEncoder - { - public bool SkipMetadata { get; init; } = true; - - public void Encode(Image image, Stream stream) where TPixel : unmanaged, IPixel - { - // https://stackoverflow.com/a/21389253 - - using var ms = new MemoryStream(); - //Knowing the image size ahead of time removes the - //requirement of the output stream to support seeking. - image.SaveAsPng(ms); - - //Disposing of the BinaryWriter disposes the soutput stream. Let the caller clean up. - var bw = new BinaryWriter(stream); - - // Header - bw.Write((short)0); // 0-1 : reserved - bw.Write((short)1); // 2-3 : 1=ico, 2=cur - bw.Write((short)1); // 4-5 : number of images - - // Image directory - var w = image.Width; - if (w >= 256) w = 0; - bw.Write((byte)w); // 0 : width of image - var h = image.Height; - if (h >= 256) h = 0; - bw.Write((byte)h); // 1 : height of image - bw.Write((byte)0); // 2 : number of colors in palette - bw.Write((byte)0); // 3 : reserved - bw.Write((short)0); // 4 : number of color planes - bw.Write((short)0); // 6 : bits per pixel - bw.Write((int)ms.Position); // 8 : image size - bw.Write((int)stream.Position + 4); // 12: offset of image data - ms.Position = 0; - ms.CopyTo(stream); // Image data - } - - public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel - => throw new NotImplementedException(); - } -} diff --git a/Source/LibationUiBase/MessageBoxBase.cs b/Source/LibationUiBase/MessageBoxBase.cs index 4ff8c24d..02aed519 100644 --- a/Source/LibationUiBase/MessageBoxBase.cs +++ b/Source/LibationUiBase/MessageBoxBase.cs @@ -1,6 +1,4 @@ -using MathNet.Numerics; -using System.IO; -using System.Threading.Tasks; +using System.Threading.Tasks; #nullable enable namespace LibationUiBase.Forms; diff --git a/Source/LoadByOS/WindowsConfigApp/IcoEncoder.cs b/Source/LoadByOS/WindowsConfigApp/IcoEncoder.cs new file mode 100644 index 00000000..4e9899ec --- /dev/null +++ b/Source/LoadByOS/WindowsConfigApp/IcoEncoder.cs @@ -0,0 +1,51 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace WindowsConfigApp; + +public class IcoEncoder : IImageEncoder +{ + public bool SkipMetadata { get; init; } = true; + + public void Encode(Image image, Stream stream) where TPixel : unmanaged, IPixel + { + // https://stackoverflow.com/a/21389253 + + using var ms = new MemoryStream(); + //Knowing the image size ahead of time removes the + //requirement of the output stream to support seeking. + image.SaveAsPng(ms); + + //Disposing of the BinaryWriter disposes the soutput stream. Let the caller clean up. + var bw = new BinaryWriter(stream); + + // Header + bw.Write((short)0); // 0-1 : reserved + bw.Write((short)1); // 2-3 : 1=ico, 2=cur + bw.Write((short)1); // 4-5 : number of images + + // Image directory + var w = image.Width; + if (w >= 256) w = 0; + bw.Write((byte)w); // 0 : width of image + var h = image.Height; + if (h >= 256) h = 0; + bw.Write((byte)h); // 1 : height of image + bw.Write((byte)0); // 2 : number of colors in palette + bw.Write((byte)0); // 3 : reserved + bw.Write((short)0); // 4 : number of color planes + bw.Write((short)0); // 6 : bits per pixel + bw.Write((int)ms.Position); // 8 : image size + bw.Write((int)stream.Position + 4); // 12: offset of image data + ms.Position = 0; + ms.CopyTo(stream); // Image data + } + + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel + => throw new NotImplementedException(); +} diff --git a/Source/LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj b/Source/LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj index 9081cc90..9ed2f956 100644 --- a/Source/LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj +++ b/Source/LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj @@ -24,6 +24,10 @@ embedded + + + +