Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,48 @@ 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

### Requirements

- .NET 9.0 or later

### Install from NuGet

```bash
dotnet add package MiniPdf
```

### Build

```bash
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -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 (`<col>` 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)
4 changes: 4 additions & 0 deletions src/MiniPdf/ExcelToPdfConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public sealed class ConversionOptions
/// <returns>A PdfDocument containing the Excel data.</returns>
public static PdfDocument Convert(string excelPath, ConversionOptions? options = null)
{
ArgumentException.ThrowIfNullOrEmpty(excelPath);
using var stream = File.OpenRead(excelPath);
return Convert(stream, options);
}
Expand All @@ -64,6 +65,7 @@ public static PdfDocument Convert(string excelPath, ConversionOptions? options =
/// <returns>A PdfDocument containing the Excel data.</returns>
public static PdfDocument Convert(Stream excelStream, ConversionOptions? options = null)
{
ArgumentNullException.ThrowIfNull(excelStream);
options ??= new ConversionOptions();
var sheets = ExcelReader.ReadSheets(excelStream);
var doc = new PdfDocument();
Expand All @@ -90,6 +92,8 @@ public static PdfDocument Convert(Stream excelStream, ConversionOptions? options
/// <param name="options">Optional conversion settings.</param>
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);
}
Expand Down
13 changes: 11 additions & 2 deletions src/MiniPdf/MiniPdf.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,18 @@
<!-- NuGet Package Metadata -->
<PackageId>MiniPdf</PackageId>
<Version>0.1.0</Version>
<Description>A minimal, zero-dependency .NET library for generating PDF documents from text and Excel files.</Description>
<PackageTags>pdf;excel;xlsx;converter;text</PackageTags>
<Authors>shps951023</Authors>
<Description>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.</Description>
<PackageTags>pdf;excel;xlsx;converter;text;document;generation</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/shps951023/MiniPdf</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageProjectUrl>https://github.com/shps951023/MiniPdf</PackageProjectUrl>
</PropertyGroup>

<ItemGroup>
<None Include="../../README.md" Pack="true" PackagePath="/" />
</ItemGroup>

</Project>
19 changes: 19 additions & 0 deletions src/MiniPdf/PdfDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@ public sealed class PdfDocument
/// </summary>
public IReadOnlyList<PdfPage> Pages => _pages;

/// <summary>Document title.</summary>
public string? Title { get; set; }

/// <summary>Document author.</summary>
public string? Author { get; set; }

/// <summary>Document subject.</summary>
public string? Subject { get; set; }

/// <summary>Document keywords.</summary>
public string? Keywords { get; set; }

/// <summary>Creator application name.</summary>
public string? Creator { get; set; }

/// <summary>
/// Adds a new page to the document.
/// </summary>
Expand All @@ -20,6 +35,8 @@ public sealed class PdfDocument
/// <returns>The newly created page.</returns>
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;
Expand All @@ -30,6 +47,7 @@ public PdfPage AddPage(float width = 612, float height = 792)
/// </summary>
public void Save(string filePath)
{
ArgumentException.ThrowIfNullOrEmpty(filePath);
using var stream = File.Create(filePath);
Save(stream);
}
Expand All @@ -39,6 +57,7 @@ public void Save(string filePath)
/// </summary>
public void Save(Stream stream)
{
ArgumentNullException.ThrowIfNull(stream);
var writer = new PdfWriter(stream);
writer.Write(this);
}
Expand Down
5 changes: 4 additions & 1 deletion src/MiniPdf/PdfPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal PdfPage(float width, float height)
/// <returns>The current page for chaining.</returns>
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;
}
Expand All @@ -57,7 +58,9 @@ public PdfPage AddText(string text, float x, float y, float fontSize = 12, PdfCo
/// <returns>The current page for chaining.</returns>
public PdfPage AddTextWrapped(string text, float x, float y, float maxWidth, float fontSize = 12, float lineSpacing = 1.2f, PdfColor? color = null)
{
if (string.IsNullOrEmpty(text))
ArgumentNullException.ThrowIfNull(text);
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(maxWidth, 0);
if (text.Length == 0)
return this;

var lineHeight = fontSize * lineSpacing;
Expand Down
36 changes: 35 additions & 1 deletion src/MiniPdf/PdfWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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++)
{
Expand All @@ -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");
Expand All @@ -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");
Expand Down
30 changes: 30 additions & 0 deletions tests/MiniPdf.Tests/ExcelToPdfConverterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -405,4 +405,34 @@ int GetStringIndex(string value)
ms.Position = 0;
return ms;
}

[Fact]
public void Convert_NullPath_Throws()
{
Assert.Throws<ArgumentNullException>(() => ExcelToPdfConverter.Convert((string)null!));
}

[Fact]
public void Convert_EmptyPath_Throws()
{
Assert.Throws<ArgumentException>(() => ExcelToPdfConverter.Convert(""));
}

[Fact]
public void Convert_NullStream_Throws()
{
Assert.Throws<ArgumentNullException>(() => ExcelToPdfConverter.Convert((Stream)null!));
}

[Fact]
public void ConvertToFile_NullExcelPath_Throws()
{
Assert.Throws<ArgumentNullException>(() => ExcelToPdfConverter.ConvertToFile(null!, "out.pdf"));
}

[Fact]
public void ConvertToFile_NullPdfPath_Throws()
{
Assert.Throws<ArgumentNullException>(() => ExcelToPdfConverter.ConvertToFile("in.xlsx", null!));
}
}
Loading