From bd02f88cf32e8aa4a9ea533efd228bb9324be94b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 03:18:26 +0000 Subject: [PATCH 1/2] Initial plan From e6147e44650c8848cfeef70d4d34c801103d7753 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 03:23:17 +0000 Subject: [PATCH 2/2] Add SheetIndices and SheetNames options for multi-sheet filtering (#11) Co-authored-by: shps951023 <12729184+shps951023@users.noreply.github.com> --- README.md | 21 ++ src/MiniPdf/ExcelToPdfConverter.cs | 26 ++- .../MiniPdf.Tests/ExcelToPdfConverterTests.cs | 179 ++++++++++++++++++ 3 files changed, 225 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e281c99..427ac54 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,27 @@ var doc = ExcelToPdfConverter.Convert("data.xlsx", options); doc.Save("data.pdf"); ``` +### Multi-Sheet Handling + +By default all sheets are converted. Use `SheetIndices` (zero-based) or `SheetNames` (case-insensitive) to select a subset: + +```csharp +// Convert only the first and third sheet (by index) +var options = new ExcelToPdfConverter.ConversionOptions +{ + SheetIndices = new[] { 0, 2 }, +}; + +// Convert only sheets named "Sales" and "Summary" (case-insensitive) +var options2 = new ExcelToPdfConverter.ConversionOptions +{ + SheetNames = new[] { "Sales", "Summary" }, +}; +``` + +When both `SheetIndices` and `SheetNames` are specified, `SheetIndices` takes precedence. +``` + ### Save to Stream or Byte Array ```csharp diff --git a/src/MiniPdf/ExcelToPdfConverter.cs b/src/MiniPdf/ExcelToPdfConverter.cs index 8072fc7..c2d3a52 100644 --- a/src/MiniPdf/ExcelToPdfConverter.cs +++ b/src/MiniPdf/ExcelToPdfConverter.cs @@ -42,6 +42,17 @@ public sealed class ConversionOptions /// Whether to include sheet name as a header (default: true). public bool IncludeSheetName { get; set; } = true; + + /// + /// Zero-based indices of the sheets to include. When null or empty, all sheets are included. + /// + public IReadOnlyList? SheetIndices { get; set; } + + /// + /// Names of the sheets to include (case-insensitive). When null or empty, all sheets are included. + /// Ignored if is also specified. + /// + public IReadOnlyList? SheetNames { get; set; } } /// @@ -68,7 +79,20 @@ public static PdfDocument Convert(Stream excelStream, ConversionOptions? options var sheets = ExcelReader.ReadSheets(excelStream); var doc = new PdfDocument(); - foreach (var sheet in sheets) + // Filter sheets by index or name if specified + IEnumerable sheetsToRender = sheets; + if (options.SheetIndices is { Count: > 0 }) + { + var indexSet = new HashSet(options.SheetIndices); + sheetsToRender = sheets.Where((_, i) => indexSet.Contains(i)); + } + else if (options.SheetNames is { Count: > 0 }) + { + var nameSet = new HashSet(options.SheetNames, StringComparer.OrdinalIgnoreCase); + sheetsToRender = sheets.Where(s => nameSet.Contains(s.Name)); + } + + foreach (var sheet in sheetsToRender) { RenderSheet(doc, sheet, options); } diff --git a/tests/MiniPdf.Tests/ExcelToPdfConverterTests.cs b/tests/MiniPdf.Tests/ExcelToPdfConverterTests.cs index f74b68e..79dc3c4 100644 --- a/tests/MiniPdf.Tests/ExcelToPdfConverterTests.cs +++ b/tests/MiniPdf.Tests/ExcelToPdfConverterTests.cs @@ -127,6 +127,83 @@ public void Convert_WithTextColor_PreservesColorInPdf() Assert.Contains("0.000 0.000 1.000 rg", content); } + [Fact] + public void Convert_WithSheetIndices_OnlyRendersSpecifiedSheets() + { + using var excelStream = CreateMultiSheetExcel( + ("Alpha", new[] { new[] { "AlphaCell" } }), + ("Beta", new[] { new[] { "BetaCell" } }), + ("Gamma", new[] { new[] { "GammaCell" } })); + + var options = new ExcelToPdfConverter.ConversionOptions + { + SheetIndices = new[] { 0, 2 }, // Alpha and Gamma only + }; + + var doc = ExcelToPdfConverter.Convert(excelStream, options); + var content = Encoding.ASCII.GetString(doc.ToArray()); + + Assert.Contains("AlphaCell", content); + Assert.DoesNotContain("BetaCell", content); + Assert.Contains("GammaCell", content); + } + + [Fact] + public void Convert_WithSheetNames_OnlyRendersSpecifiedSheets() + { + using var excelStream = CreateMultiSheetExcel( + ("Sales", new[] { new[] { "SalesData" } }), + ("Costs", new[] { new[] { "CostsData" } }), + ("Summary", new[] { new[] { "SummaryData" } })); + + var options = new ExcelToPdfConverter.ConversionOptions + { + SheetNames = new[] { "costs", "Summary" }, // case-insensitive + }; + + var doc = ExcelToPdfConverter.Convert(excelStream, options); + var content = Encoding.ASCII.GetString(doc.ToArray()); + + Assert.DoesNotContain("SalesData", content); + Assert.Contains("CostsData", content); + Assert.Contains("SummaryData", content); + } + + [Fact] + public void Convert_SheetIndices_TakesPrecedenceOverSheetNames() + { + using var excelStream = CreateMultiSheetExcel( + ("First", new[] { new[] { "FirstCell" } }), + ("Second", new[] { new[] { "SecondCell" } })); + + var options = new ExcelToPdfConverter.ConversionOptions + { + SheetIndices = new[] { 0 }, // First only + SheetNames = new[] { "Second" }, // would select Second, but ignored + }; + + var doc = ExcelToPdfConverter.Convert(excelStream, options); + var content = Encoding.ASCII.GetString(doc.ToArray()); + + Assert.Contains("FirstCell", content); + Assert.DoesNotContain("SecondCell", content); + } + + [Fact] + public void Convert_NoMatchingSheets_ProducesAtLeastOnePage() + { + using var excelStream = CreateMultiSheetExcel( + ("Sheet1", new[] { new[] { "Data" } })); + + var options = new ExcelToPdfConverter.ConversionOptions + { + SheetNames = new[] { "DoesNotExist" }, + }; + + var doc = ExcelToPdfConverter.Convert(excelStream, options); + Assert.True(doc.Pages.Count >= 1); + } + /// /// Creates a minimal valid .xlsx file in memory with the given data. /// @@ -405,4 +482,106 @@ int GetStringIndex(string value) ms.Position = 0; return ms; } + + /// + /// Creates a minimal .xlsx with multiple named sheets, each containing string rows. + /// + private static MemoryStream CreateMultiSheetExcel(params (string name, string[][] rows)[] sheets) + { + var ms = new MemoryStream(); + + using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) + { + // [Content_Types].xml + var ctSb = new StringBuilder(); + ctSb.AppendLine(""); + ctSb.AppendLine(""); + ctSb.AppendLine(" "); + ctSb.AppendLine(" "); + ctSb.AppendLine(" "); + for (var i = 0; i < sheets.Length; i++) + ctSb.AppendLine($" "); + ctSb.AppendLine(" "); + ctSb.AppendLine(""); + AddEntry(archive, "[Content_Types].xml", ctSb.ToString()); + + AddEntry(archive, "_rels/.rels", + """ + + + + + """); + + // xl/_rels/workbook.xml.rels + var relsSb = new StringBuilder(); + relsSb.AppendLine(""); + relsSb.AppendLine(""); + for (var i = 0; i < sheets.Length; i++) + relsSb.AppendLine($" "); + relsSb.AppendLine($" "); + relsSb.AppendLine(""); + AddEntry(archive, "xl/_rels/workbook.xml.rels", relsSb.ToString()); + + // xl/workbook.xml + var wbSb = new StringBuilder(); + wbSb.AppendLine(""); + wbSb.AppendLine(""); + wbSb.AppendLine(" "); + for (var i = 0; i < sheets.Length; i++) + wbSb.AppendLine($" "); + wbSb.AppendLine(" "); + wbSb.AppendLine(""); + AddEntry(archive, "xl/workbook.xml", wbSb.ToString()); + + // Shared strings (global across all sheets) + var sharedStrings = new List(); + var sharedStringIndex = new Dictionary(); + + int GetStringIndex(string value) + { + if (!sharedStringIndex.TryGetValue(value, out var idx)) + { + idx = sharedStrings.Count; + sharedStrings.Add(value); + sharedStringIndex[value] = idx; + } + return idx; + } + + for (var i = 0; i < sheets.Length; i++) + { + var (_, rows) = sheets[i]; + var sheetSb = new StringBuilder(); + sheetSb.AppendLine(""); + sheetSb.AppendLine(""); + sheetSb.AppendLine(""); + for (var r = 0; r < rows.Length; r++) + { + sheetSb.AppendLine($" "); + for (var c = 0; c < rows[r].Length; c++) + { + var colLetter = (char)('A' + c); + var idx = GetStringIndex(rows[r][c]); + sheetSb.AppendLine($" {idx}"); + } + sheetSb.AppendLine(" "); + } + sheetSb.AppendLine(""); + sheetSb.AppendLine(""); + AddEntry(archive, $"xl/worksheets/sheet{i + 1}.xml", sheetSb.ToString()); + } + + var ssSb = new StringBuilder(); + ssSb.AppendLine(""); + ssSb.AppendLine($""); + foreach (var s in sharedStrings) + ssSb.AppendLine($" {EscapeXml(s)}"); + ssSb.AppendLine(""); + AddEntry(archive, "xl/sharedStrings.xml", ssSb.ToString()); + } + + ms.Position = 0; + return ms; + } }