From 2d3fe34479484b1d31c8dea4c4b7b0d7ffa6afed Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 22 Feb 2026 13:26:44 +0000
Subject: [PATCH 1/5] Initial plan
From 855abdba0e2c7fad88bd1102495d142fbebdfa92 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 22 Feb 2026 13:30:49 +0000
Subject: [PATCH 2/5] Add input validation to public APIs and PDF document
metadata support
- Add argument validation to PdfDocument, PdfPage, and ExcelToPdfConverter
using .NET 9 throw helpers (ArgumentNullException.ThrowIfNull,
ArgumentException.ThrowIfNullOrEmpty, ArgumentOutOfRangeException.ThrowIfLessThanOrEqual)
- Add Title, Author, Subject, Keywords, Creator metadata properties to PdfDocument
- Write PDF Info dictionary in PdfWriter when any metadata property is set
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
src/MiniPdf/ExcelToPdfConverter.cs | 4 ++++
src/MiniPdf/PdfDocument.cs | 19 ++++++++++++++++
src/MiniPdf/PdfPage.cs | 3 +++
src/MiniPdf/PdfWriter.cs | 36 +++++++++++++++++++++++++++++-
4 files changed, 61 insertions(+), 1 deletion(-)
diff --git a/src/MiniPdf/ExcelToPdfConverter.cs b/src/MiniPdf/ExcelToPdfConverter.cs
index 8072fc7..c43784d 100644
--- a/src/MiniPdf/ExcelToPdfConverter.cs
+++ b/src/MiniPdf/ExcelToPdfConverter.cs
@@ -52,6 +52,7 @@ public sealed class ConversionOptions
/// A PdfDocument containing the Excel data.
public static PdfDocument Convert(string excelPath, ConversionOptions? options = null)
{
+ ArgumentException.ThrowIfNullOrEmpty(excelPath);
using var stream = File.OpenRead(excelPath);
return Convert(stream, options);
}
@@ -64,6 +65,7 @@ public static PdfDocument Convert(string excelPath, ConversionOptions? options =
/// A PdfDocument containing the Excel data.
public static PdfDocument Convert(Stream excelStream, ConversionOptions? options = null)
{
+ ArgumentNullException.ThrowIfNull(excelStream);
options ??= new ConversionOptions();
var sheets = ExcelReader.ReadSheets(excelStream);
var doc = new PdfDocument();
@@ -90,6 +92,8 @@ public static PdfDocument Convert(Stream excelStream, ConversionOptions? options
/// Optional conversion settings.
public static void ConvertToFile(string excelPath, string pdfPath, ConversionOptions? options = null)
{
+ ArgumentException.ThrowIfNullOrEmpty(excelPath);
+ ArgumentException.ThrowIfNullOrEmpty(pdfPath);
var doc = Convert(excelPath, options);
doc.Save(pdfPath);
}
diff --git a/src/MiniPdf/PdfDocument.cs b/src/MiniPdf/PdfDocument.cs
index d42d52d..5594c2d 100644
--- a/src/MiniPdf/PdfDocument.cs
+++ b/src/MiniPdf/PdfDocument.cs
@@ -12,6 +12,21 @@ public sealed class PdfDocument
///
public IReadOnlyList Pages => _pages;
+ /// Document title.
+ public string? Title { get; set; }
+
+ /// Document author.
+ public string? Author { get; set; }
+
+ /// Document subject.
+ public string? Subject { get; set; }
+
+ /// Document keywords.
+ public string? Keywords { get; set; }
+
+ /// Creator application name.
+ public string? Creator { get; set; }
+
///
/// Adds a new page to the document.
///
@@ -20,6 +35,8 @@ public sealed class PdfDocument
/// The newly created page.
public PdfPage AddPage(float width = 612, float height = 792)
{
+ ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(width, 0);
+ ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(height, 0);
var page = new PdfPage(width, height);
_pages.Add(page);
return page;
@@ -30,6 +47,7 @@ public PdfPage AddPage(float width = 612, float height = 792)
///
public void Save(string filePath)
{
+ ArgumentException.ThrowIfNullOrEmpty(filePath);
using var stream = File.Create(filePath);
Save(stream);
}
@@ -39,6 +57,7 @@ public void Save(string filePath)
///
public void Save(Stream stream)
{
+ ArgumentNullException.ThrowIfNull(stream);
var writer = new PdfWriter(stream);
writer.Write(this);
}
diff --git a/src/MiniPdf/PdfPage.cs b/src/MiniPdf/PdfPage.cs
index 38449c3..e699ee4 100644
--- a/src/MiniPdf/PdfPage.cs
+++ b/src/MiniPdf/PdfPage.cs
@@ -39,6 +39,7 @@ internal PdfPage(float width, float height)
/// The current page for chaining.
public PdfPage AddText(string text, float x, float y, float fontSize = 12, PdfColor? color = null)
{
+ ArgumentNullException.ThrowIfNull(text);
_textBlocks.Add(new PdfTextBlock(text, x, y, fontSize, color));
return this;
}
@@ -57,6 +58,8 @@ public PdfPage AddText(string text, float x, float y, float fontSize = 12, PdfCo
/// The current page for chaining.
public PdfPage AddTextWrapped(string text, float x, float y, float maxWidth, float fontSize = 12, float lineSpacing = 1.2f, PdfColor? color = null)
{
+ ArgumentNullException.ThrowIfNull(text);
+ ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(maxWidth, 0);
if (string.IsNullOrEmpty(text))
return this;
diff --git a/src/MiniPdf/PdfWriter.cs b/src/MiniPdf/PdfWriter.cs
index 93072f7..2179ee0 100644
--- a/src/MiniPdf/PdfWriter.cs
+++ b/src/MiniPdf/PdfWriter.cs
@@ -63,6 +63,21 @@ internal void Write(PdfDocument document)
_objectOffsets[3] = Position;
WriteRaw("3 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\nendobj\n");
+ // Determine if we need an Info dictionary
+ var hasMetadata = !string.IsNullOrEmpty(document.Title)
+ || !string.IsNullOrEmpty(document.Author)
+ || !string.IsNullOrEmpty(document.Subject)
+ || !string.IsNullOrEmpty(document.Keywords)
+ || !string.IsNullOrEmpty(document.Creator);
+
+ var infoObj = 0;
+ if (hasMetadata)
+ {
+ infoObj = nextObj++;
+ _objectCount = nextObj - 1;
+ _objectOffsets.Add(0);
+ }
+
// Write each page and its content stream
for (var i = 0; i < document.Pages.Count; i++)
{
@@ -89,6 +104,24 @@ internal void Write(PdfDocument document)
WriteRaw("endobj\n");
}
+ // Write Info dictionary if metadata is present
+ if (hasMetadata)
+ {
+ _objectOffsets[infoObj] = Position;
+ WriteRaw($"{infoObj} 0 obj\n<< ");
+ if (!string.IsNullOrEmpty(document.Title))
+ WriteRaw($"/Title ({EscapePdfString(document.Title)}) ");
+ if (!string.IsNullOrEmpty(document.Author))
+ WriteRaw($"/Author ({EscapePdfString(document.Author)}) ");
+ if (!string.IsNullOrEmpty(document.Subject))
+ WriteRaw($"/Subject ({EscapePdfString(document.Subject)}) ");
+ if (!string.IsNullOrEmpty(document.Keywords))
+ WriteRaw($"/Keywords ({EscapePdfString(document.Keywords)}) ");
+ if (!string.IsNullOrEmpty(document.Creator))
+ WriteRaw($"/Creator ({EscapePdfString(document.Creator)}) ");
+ WriteRaw(">>\nendobj\n");
+ }
+
// Write xref table
var xrefOffset = Position;
WriteRaw("xref\n");
@@ -101,7 +134,8 @@ internal void Write(PdfDocument document)
// Write trailer
WriteRaw("trailer\n");
- WriteRaw($"<< /Size {_objectCount + 1} /Root 1 0 R >>\n");
+ var trailerExtra = hasMetadata ? $" /Info {infoObj} 0 R" : "";
+ WriteRaw($"<< /Size {_objectCount + 1} /Root 1 0 R{trailerExtra} >>\n");
WriteRaw("startxref\n");
WriteRaw($"{xrefOffset}\n");
WriteRaw("%%EOF\n");
From cdf2b4e14d523c794b7480b0c038dffe77f0b0d5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 22 Feb 2026 13:34:43 +0000
Subject: [PATCH 3/5] Add validation and metadata tests for PdfDocument and
ExcelToPdfConverter
Add 12 new tests to PdfDocumentTests.cs covering:
- AddPage argument validation (zero width, negative height)
- Save argument validation (null/empty file path, null stream)
- AddText/AddTextWrapped argument validation (null text, zero maxWidth)
- Metadata properties (title, all properties, no metadata, special chars)
Add 5 new tests to ExcelToPdfConverterTests.cs covering:
- Convert argument validation (null/empty path, null stream)
- ConvertToFile argument validation (null excel path, null pdf path)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../MiniPdf.Tests/ExcelToPdfConverterTests.cs | 30 +++++
tests/MiniPdf.Tests/PdfDocumentTests.cs | 122 ++++++++++++++++++
2 files changed, 152 insertions(+)
diff --git a/tests/MiniPdf.Tests/ExcelToPdfConverterTests.cs b/tests/MiniPdf.Tests/ExcelToPdfConverterTests.cs
index f74b68e..30c04fd 100644
--- a/tests/MiniPdf.Tests/ExcelToPdfConverterTests.cs
+++ b/tests/MiniPdf.Tests/ExcelToPdfConverterTests.cs
@@ -405,4 +405,34 @@ int GetStringIndex(string value)
ms.Position = 0;
return ms;
}
+
+ [Fact]
+ public void Convert_NullPath_Throws()
+ {
+ Assert.Throws(() => ExcelToPdfConverter.Convert((string)null!));
+ }
+
+ [Fact]
+ public void Convert_EmptyPath_Throws()
+ {
+ Assert.Throws(() => ExcelToPdfConverter.Convert(""));
+ }
+
+ [Fact]
+ public void Convert_NullStream_Throws()
+ {
+ Assert.Throws(() => ExcelToPdfConverter.Convert((Stream)null!));
+ }
+
+ [Fact]
+ public void ConvertToFile_NullExcelPath_Throws()
+ {
+ Assert.Throws(() => ExcelToPdfConverter.ConvertToFile(null!, "out.pdf"));
+ }
+
+ [Fact]
+ public void ConvertToFile_NullPdfPath_Throws()
+ {
+ Assert.Throws(() => ExcelToPdfConverter.ConvertToFile("in.xlsx", null!));
+ }
}
diff --git a/tests/MiniPdf.Tests/PdfDocumentTests.cs b/tests/MiniPdf.Tests/PdfDocumentTests.cs
index d0b3f47..6e3f7a2 100644
--- a/tests/MiniPdf.Tests/PdfDocumentTests.cs
+++ b/tests/MiniPdf.Tests/PdfDocumentTests.cs
@@ -159,4 +159,126 @@ public void EmptyDocument_ProducesValidPdf()
Assert.Contains("%%EOF", content);
Assert.Contains("/Type /Page", content);
}
+
+ [Fact]
+ public void AddPage_ZeroWidth_Throws()
+ {
+ var doc = new PdfDocument();
+ Assert.Throws(() => doc.AddPage(width: 0, height: 100));
+ }
+
+ [Fact]
+ public void AddPage_NegativeHeight_Throws()
+ {
+ var doc = new PdfDocument();
+ Assert.Throws(() => doc.AddPage(width: 100, height: -1));
+ }
+
+ [Fact]
+ public void Save_NullFilePath_Throws()
+ {
+ var doc = new PdfDocument();
+ doc.AddPage();
+ Assert.Throws(() => doc.Save((string)null!));
+ }
+
+ [Fact]
+ public void Save_EmptyFilePath_Throws()
+ {
+ var doc = new PdfDocument();
+ doc.AddPage();
+ Assert.Throws(() => doc.Save(""));
+ }
+
+ [Fact]
+ public void Save_NullStream_Throws()
+ {
+ var doc = new PdfDocument();
+ doc.AddPage();
+ Assert.Throws(() => doc.Save((Stream)null!));
+ }
+
+ [Fact]
+ public void AddText_NullText_Throws()
+ {
+ var doc = new PdfDocument();
+ var page = doc.AddPage();
+ Assert.Throws(() => page.AddText(null!, 0, 0));
+ }
+
+ [Fact]
+ public void AddTextWrapped_NullText_Throws()
+ {
+ var doc = new PdfDocument();
+ var page = doc.AddPage();
+ Assert.Throws(() => page.AddTextWrapped(null!, 0, 0, 100));
+ }
+
+ [Fact]
+ public void AddTextWrapped_ZeroMaxWidth_Throws()
+ {
+ var doc = new PdfDocument();
+ var page = doc.AddPage();
+ Assert.Throws(() => page.AddTextWrapped("text", 0, 0, 0));
+ }
+
+ [Fact]
+ public void Metadata_Title_IncludedInPdf()
+ {
+ var doc = new PdfDocument();
+ doc.Title = "Test Title";
+ doc.AddPage();
+
+ var bytes = doc.ToArray();
+ var content = System.Text.Encoding.ASCII.GetString(bytes);
+
+ Assert.Contains("/Title (Test Title)", content);
+ }
+
+ [Fact]
+ public void Metadata_AllProperties_IncludedInPdf()
+ {
+ var doc = new PdfDocument();
+ doc.Title = "My Title";
+ doc.Author = "My Author";
+ doc.Subject = "My Subject";
+ doc.Keywords = "My Keywords";
+ doc.Creator = "My Creator";
+ doc.AddPage();
+
+ var bytes = doc.ToArray();
+ var content = System.Text.Encoding.ASCII.GetString(bytes);
+
+ Assert.Contains("/Title (My Title)", content);
+ Assert.Contains("/Author (My Author)", content);
+ Assert.Contains("/Subject (My Subject)", content);
+ Assert.Contains("/Keywords (My Keywords)", content);
+ Assert.Contains("/Creator (My Creator)", content);
+ Assert.Contains("/Info", content);
+ }
+
+ [Fact]
+ public void Metadata_None_NoInfoDictionary()
+ {
+ var doc = new PdfDocument();
+ doc.AddPage();
+
+ var bytes = doc.ToArray();
+ var content = System.Text.Encoding.ASCII.GetString(bytes);
+
+ Assert.DoesNotContain("/Info", content);
+ }
+
+ [Fact]
+ public void Metadata_SpecialChars_Escaped()
+ {
+ var doc = new PdfDocument();
+ doc.Title = "Hello (World)";
+ doc.AddPage();
+
+ var bytes = doc.ToArray();
+ var content = System.Text.Encoding.ASCII.GetString(bytes);
+
+ Assert.Contains("/Title (Hello \\(World\\))", content);
+ }
}
From 0a079e3811e5b585b7a50fc03cd4ac2c578f89f7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 22 Feb 2026 13:37:36 +0000
Subject: [PATCH 4/5] Improve NuGet metadata, update README with feature
matrix, add ROADMAP.md
Co-authored-by: shps951023 <12729184+shps951023@users.noreply.github.com>
---
README.md | 69 +++++++++++++++++++++++++++++++++++++-
ROADMAP.md | 42 +++++++++++++++++++++++
src/MiniPdf/MiniPdf.csproj | 13 +++++--
3 files changed, 121 insertions(+), 3 deletions(-)
create mode 100644 ROADMAP.md
diff --git a/README.md b/README.md
index 99917d0..0ba36d1 100644
--- a/README.md
+++ b/README.md
@@ -6,8 +6,35 @@ A minimal, zero-dependency .NET library for generating PDF documents from text a
- **Text-to-PDF** — Create PDF documents with positioned or auto-wrapped text
- **Excel-to-PDF** — Convert `.xlsx` files to paginated PDF with automatic column layout
+- **Text Color** — Per-cell font color support in both text and Excel-to-PDF conversion
+- **PDF Metadata** — Set document Title, Author, Subject, Keywords, and Creator
- **Zero dependencies** — Uses only built-in .NET APIs (no external packages)
- **Valid PDF 1.4** output with Helvetica font
+- **Input validation** — All public APIs validate arguments with descriptive exceptions
+
+## Supported Feature Matrix
+
+| Feature | Status | Notes |
+|---|---|---|
+| Text positioning | ✅ | Absolute (x,y) placement |
+| Text wrapping | ✅ | Auto-wrap within specified width |
+| Text color | ✅ | RGB color via `PdfColor` class |
+| Excel cell text | ✅ | Shared strings, inline strings, numbers |
+| Excel text color | ✅ | ARGB hex, indexed colors from styles.xml |
+| Multi-sheet support | ✅ | All sheets rendered sequentially |
+| Pagination | ✅ | Automatic page breaks for long content |
+| Column auto-sizing | ✅ | Based on content width |
+| Sheet name headers | ✅ | Optional, configurable |
+| PDF metadata | ✅ | Title, Author, Subject, Keywords, Creator |
+| Page size options | ✅ | US Letter (default), A4, or custom |
+| Bold / italic | ❌ | Planned |
+| Cell background color | ❌ | Planned |
+| Text alignment | ❌ | Planned |
+| Merged cells | ❌ | Planned |
+| Images | ❌ | Planned |
+| Number/date formatting | ❌ | Planned |
+| Column width from Excel | ❌ | Planned |
+| Hyperlinks | ❌ | Planned |
## Getting Started
@@ -15,6 +42,12 @@ A minimal, zero-dependency .NET library for generating PDF documents from text a
- .NET 9.0 or later
+### Install from NuGet
+
+```bash
+dotnet add package MiniPdf
+```
+
### Build
```bash
@@ -57,6 +90,39 @@ page.AddTextWrapped(longText, x: 50, y: 700, maxWidth: 500, fontSize: 12);
doc.Save("wrapped.pdf");
```
+### Text with Color
+
+```csharp
+using MiniPdf;
+
+var doc = new PdfDocument();
+var page = doc.AddPage();
+
+page.AddText("Red text", 50, 700, 12, PdfColor.Red);
+page.AddText("Blue text", 50, 680, 12, PdfColor.Blue);
+page.AddText("Custom color", 50, 660, 12, PdfColor.FromRgb(128, 64, 0));
+page.AddText("Hex color", 50, 640, 12, PdfColor.FromHex("#FF8C00"));
+
+doc.Save("colored.pdf");
+```
+
+### PDF Metadata
+
+```csharp
+using MiniPdf;
+
+var doc = new PdfDocument();
+doc.Title = "My Document";
+doc.Author = "John Doe";
+doc.Subject = "Sample PDF";
+doc.Keywords = "pdf, sample, minipdf";
+doc.Creator = "MiniPdf";
+
+doc.AddPage().AddText("Hello with metadata!", 50, 700);
+
+doc.Save("metadata.pdf");
+```
+
### Excel to PDF
```csharp
@@ -97,9 +163,10 @@ byte[] bytes = doc.ToArray();
```
MiniPdf.sln
├── src/MiniPdf/ # Library
-│ ├── PdfDocument.cs # Document model
+│ ├── PdfDocument.cs # Document model (pages + metadata)
│ ├── PdfPage.cs # Page with text placement
│ ├── PdfTextBlock.cs # Text block data
+│ ├── PdfColor.cs # RGB color for text rendering
│ ├── PdfWriter.cs # PDF 1.4 binary writer
│ ├── ExcelReader.cs # .xlsx parser (ZIP + XML)
│ └── ExcelToPdfConverter.cs# Excel-to-PDF public API
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 0000000..3de5911
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,42 @@
+# MiniPdf Roadmap
+
+This document tracks planned features and improvements for the MiniPdf library.
+
+## v0.1.0 (Current)
+
+- [x] Text-to-PDF with positioned text
+- [x] Auto-wrapped text within width bounds
+- [x] Excel-to-PDF with automatic column layout
+- [x] Multi-sheet support
+- [x] Automatic pagination
+- [x] Text color support (PdfColor)
+- [x] Excel font color reading (ARGB hex, indexed colors)
+- [x] PDF metadata (Title, Author, Subject, Keywords, Creator)
+- [x] Input validation on all public APIs
+- [x] NuGet packaging with README
+
+## v0.2.0 (Planned)
+
+- [ ] Bold / italic / underline text (Helvetica-Bold, Helvetica-Oblique)
+- [ ] Per-cell font size from Excel
+- [ ] Text alignment (left, center, right)
+- [ ] Cell background / fill color
+- [ ] Column width from Excel (`` widths)
+- [ ] Row height from Excel
+
+## v0.3.0 (Planned)
+
+- [ ] Merged cell support
+- [ ] Number and date formatting from Excel format codes
+- [ ] Hyperlink annotations
+- [ ] Headers and footers (page numbers, sheet name, date)
+- [ ] Page setup from Excel (orientation, paper size, margins)
+
+## Future Considerations
+
+- [ ] Image support (JPEG/PNG embedding)
+- [ ] Multi-target frameworks (netstandard2.0, net6.0, net8.0)
+- [ ] Async conversion APIs
+- [ ] Configuration class for advanced options
+- [ ] Streaming reads/writes for large file optimization
+- [ ] PDF security features (passwords, permissions)
diff --git a/src/MiniPdf/MiniPdf.csproj b/src/MiniPdf/MiniPdf.csproj
index 2c98185..d16ec02 100644
--- a/src/MiniPdf/MiniPdf.csproj
+++ b/src/MiniPdf/MiniPdf.csproj
@@ -9,9 +9,18 @@
MiniPdf
0.1.0
- A minimal, zero-dependency .NET library for generating PDF documents from text and Excel files.
- pdf;excel;xlsx;converter;text
+ shps951023
+ A minimal, zero-dependency .NET library for generating PDF documents from text and Excel (.xlsx) files. Supports text-to-PDF with positioning and wrapping, Excel-to-PDF with automatic column layout and pagination, text color, and PDF metadata.
+ pdf;excel;xlsx;converter;text;document;generation
MIT
+ README.md
+ https://github.com/shps951023/MiniPdf
+ git
+ https://github.com/shps951023/MiniPdf
+
+
+
+
From 9fed01b3e54922fa65f3d1b68b7d0dd3891693b1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 22 Feb 2026 13:38:23 +0000
Subject: [PATCH 5/5] Address code review: simplify redundant null check in
AddTextWrapped
Co-authored-by: shps951023 <12729184+shps951023@users.noreply.github.com>
---
src/MiniPdf/PdfPage.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/MiniPdf/PdfPage.cs b/src/MiniPdf/PdfPage.cs
index e699ee4..6a4bc3d 100644
--- a/src/MiniPdf/PdfPage.cs
+++ b/src/MiniPdf/PdfPage.cs
@@ -60,7 +60,7 @@ public PdfPage AddTextWrapped(string text, float x, float y, float maxWidth, flo
{
ArgumentNullException.ThrowIfNull(text);
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(maxWidth, 0);
- if (string.IsNullOrEmpty(text))
+ if (text.Length == 0)
return this;
var lineHeight = fontSize * lineSpacing;