A powerful REST API service that generates PDF documents from dynamic HTML templates. PDF Smith supports multiple template engines, offers flexible PDF configuration options, and provides secure API key-based authentication with rate limiting.
- Dynamic PDF Generation: Create PDFs from HTML templates with dynamic data injection
- Multiple Template Engines: Support for Razor, Scriban, and Handlebars template engines
- Flexible PDF Options: Configure page size, orientation, margins, and more
- API Key Authentication: Secure access with subscription-based API keys
- Rate Limiting: Configurable request limits per subscription
- Localization Support: Multi-language template rendering
- Time Zone Support: Correct handling and conversion of dates/times based on the user's specified time zone
- OpenAPI Documentation: Built-in Swagger documentation
- Installation
- Authentication
- API Reference
- Template Engines
- PDF Configuration
- Time Zone Support
- Usage Examples
- Error Handling
- Rate Limiting
- Configuration
- .NET 9.0 SDK or later
- Chromium browser (automatically installed via Playwright)
- Clone the repository:
git clone https://github.com/marcominerva/PdfSmith.git
cd PdfSmith
- Build the solution:
dotnet build
- Configure your database connection in
appsettings.json
:
{
"ConnectionStrings": {
"SqlConnection": "your-connection-string-here"
}
}
- Run the application:
dotnet run --project src/PdfSmith
The API will be available at https://localhost:7226
(or your configured port).
PdfSmith uses API key authentication with subscription-based access control.
- Include your API key in the request header:
x-api-key: your-api-key-here
- Each subscription has configurable rate limits:
- Requests per window: Number of allowed requests
- Window duration: Time window in minutes
- Validity period: API key expiration dates
A default administrator account is created automatically with the following configuration:
- Username: Set via
AppSettings:AdministratorUserName
- API Key: Set via
AppSettings:AdministratorApiKey
- Default limits: 10 requests per minute
Endpoint: POST /api/pdf
Headers:
x-api-key
: Your API key (required)Accept-Language
: Language preference (optional, e.g., "en-US", "it-IT")x-time-zone
: The IANA time zone identifier to handle different time zones (optional, if not present UTC will be used)
Request Body:
{
"template": "HTML template string",
"model": {
"key": "value",
"nested": {
"data": "structure"
}
},
"templateEngine": "razor",
"fileName": "document.pdf",
"options": {
"pageSize": "A4",
"orientation": "Portrait",
"margin": {
"top": "2.5cm",
"bottom": "2cm",
"left": "2cm",
"right": "2cm"
}
}
}
Response:
- Content-Type:
application/pdf
- Content-Disposition:
attachment; filename="document.pdf"
- Body: PDF file binary data
PdfSmith supports three powerful template engines:
Razor provides C#-based templating with full programming capabilities.
Key: razor
Example:
<html>
<body>
<h1>Hello @Model.Name!</h1>
<p>Order Date: @Model.Date.ToString("dd/MM/yyyy")</p>
<ul>
@foreach(var item in Model.Items)
{
<li>@item.Name - @item.Price.ToString("C")</li>
}
</ul>
<p>Total: @Model.Total.ToString("C")</p>
</body>
</html>
Scriban is a fast, powerful, safe, and lightweight text templating language.
Key: scriban
Example:
<html>
<body>
<h1>Hello {{ Model.Name }}!</h1>
<p>Order Date: {{ Model.Date | date.to_string '%d/%m/%Y' }}</p>
<ul>
{{- for item in Model.Items }}
<li>{{ item.Name }} - {{ item.Price | object.format "C" }}</li>
{{- end }}
</ul>
<p>Total: {{ Model.Total | object.format "C" }}</p>
</body>
</html>
Handlebars provides logic-less templates with a designer-friendly syntax, ideal for collaborative development workflows.
Key: handlebars
Example:
<html>
<body>
<h1>Hello {{Model.Name}}!</h1>
<p>Order Date: {{formatDate Model.Date "dd/MM/yyyy"}}</p>
<ul>
{{#each Items}}
<li>{{Name}} - {{formatCurrency Price}}</li>
{{/each}}
</ul>
<p>Total: {{formatCurrency Model.Total}}</p>
</body>
</html>
Built-in Helpers:
formatNumber
- Formats number values as string using the specified format and the current cultureformatCurrency
- Formats decimal values as currency using current cultureformatDate
- Formats dates with optional format stringnow
- Gets the current datetime with optional format stringutcNow
- Gets the current UTC datetime with optional format stringadd
- Adds two numeric values for calculations within templatessubtract
- Subtracts two numeric values for calculations within templatesmultiply
- Multiplies two numeric values for calculations within templatesdivide
- Divides two numeric values for calculations within templatesround
- Rounds a numberic value to the specified number of decimals
- Standard sizes:
"A4"
,"A3"
,"A5"
,"Letter"
,"Legal"
- Custom sizes:
"210mm x 297mm"
or"8.5in x 11in"
"Portrait"
(default)"Landscape"
Configure margins using CSS units:
{
"margin": {
"top": "2.5cm",
"bottom": "2cm",
"left": "2cm",
"right": "2cm"
}
}
Supported units: mm
, cm
, in
, px
, pt
, pc
When generating PDFs that include dates and times, it is important to ensure that these values are represented in the correct time zone for the end user. By default, .NET's DateTime.Now
returns the server's local time, which may not match the user's intended time zone. Similarly, when converting or displaying dates from the input model, the correct time zone context is essential to avoid confusion or errors in generated documents.
PdfSmith allows clients to specify the desired time zone using the x-time-zone
HTTP header (with an IANA time zone identifier). If this header is not provided, UTC is used by default. This ensures that:
- All date and time values (such as those produced by
DateTime.Now
or parsed from the model) are converted and rendered according to the specified time zone. - Templates using date/time expressions (e.g.,
@Model.Date
,date.now
, or similar) will reflect the correct local time for the user, not just the server. - Consistency is maintained across different users and regions, especially for time-sensitive documents like invoices, reports, or logs.
using System.Net.Http.Json;
using PdfSmith.Shared.Models;
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("x-api-key", "your-api-key");
httpClient.DefaultRequestHeaders.Add("x-time-zone", "Europe/Rome")
var request = new PdfGenerationRequest(
template: "<html><body><h1>Hello @Model.Name!</h1></body></html>",
model: new { Name = "John Doe" },
templateEngine: "razor"
);
var response = await httpClient.PostAsJsonAsync("https://localhost:7226/api/pdf", request);
var pdfBytes = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync("output.pdf", pdfBytes);
var request = new PdfGenerationRequest(
template: htmlTemplate,
model: orderData,
options: new PdfOptions
{
PageSize = "A4",
Orientation = PdfOrientation.Portrait,
Margin = new PdfMargin(
Top: "50mm",
Bottom: "30mm",
Left: "25mm",
Right: "25mm"
)
},
templateEngine: "razor",
fileName: "invoice.pdf"
);
var order = new
{
CustomerName = "Acme Corp",
Date = DateTime.Now,
InvoiceNumber = "INV-2024-001",
Items = new[]
{
new { Name = "Product A", Quantity = 2, Price = 29.99m },
new { Name = "Product B", Quantity = 1, Price = 49.99m }
},
Total = 109.97m
};
var razorTemplate = """
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.header { text-align: center; margin-bottom: 30px; }
.invoice-details { margin-bottom: 20px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
.total { font-weight: bold; text-align: right; }
</style>
</head>
<body>
<div class="header">
<h1>Invoice</h1>
<p>Invoice #@Model.InvoiceNumber</p>
</div>
<div class="invoice-details">
<p><strong>Customer:</strong> @Model.CustomerName</p>
<p><strong>Date:</strong> @Model.Date.ToString("dd/MM/yyyy")</p>
</div>
<table>
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th>Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
@foreach(var item in Model.Items)
{
<tr>
<td>@item.Name</td>
<td>@item.Quantity</td>
<td>@item.Price.ToString("C")</td>
<td>@((item.Quantity * item.Price).ToString("C"))</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="3" class="total">Total:</td>
<td class="total">@Model.Total.ToString("C")</td>
</tr>
</tfoot>
</table>
</body>
</html>
""";
var request = new PdfGenerationRequest(razorTemplate, order, templateEngine: "razor");
var order = new
{
CustomerName = "Acme Corp",
Date = DateTime.Now,
InvoiceNumber = "INV-2024-001",
Items = new[]
{
new { Name = "Product A", Quantity = 2, Price = 29.99m },
new { Name = "Product B", Quantity = 1, Price = 49.99m }
},
Total = 109.97m
};
var handlebarsTemplate = """
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.header { text-align: center; margin-bottom: 30px; }
.invoice-details { margin-bottom: 20px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
.total { font-weight: bold; text-align: right; }
</style>
</head>
<body>
<div class="header">
<h1>Invoice</h1>
<p>Invoice #{{Model.InvoiceNumber}}</p>
</div>
<div class="invoice-details">
<p><strong>Customer:</strong> {{Model.CustomerName}}</p>
<p><strong>Date:</strong> {{formatDate Model.Date "dd/MM/yyyy"}}</p>
</div>
<table>
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th>Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{{#each Model.Items}}
<tr>
<td>{{Name}}</td>
<td>{{Quantity}}</td>
<td>{{formatCurrency Price}}</td>
<td>{{formatCurrency (multiply Price Quantity)}}</td>
</tr>
{{/each}}
</tbody>
<tfoot>
<tr>
<td colspan="3" class="total">Total:</td>
<td class="total">{{formatCurrency Model.Total}}</td>
</tr>
</tfoot>
</table>
</body>
</html>
""";
var request = new PdfGenerationRequest(handlebarsTemplate, order, templateEngine: "handlebars");
The API returns appropriate HTTP status codes and error details:
- 200 OK: PDF generated successfully
- 400 Bad Request: Invalid request data or template errors
- 401 Unauthorized: Invalid or missing API key
- 408 Request Timeout: Generation took longer than 30 seconds
- 429 Too Many Requests: Rate limit exceeded
- 500 Internal Server Error: Unexpected server error
{
"type": "https://httpstatuses.com/400",
"title": "Bad Request",
"status": 400,
"detail": "Template engine 'invalid' has not been registered",
"instance": "/api/pdf"
}
Rate limiting is enforced per subscription:
- Window-based limiting: Requests are counted within specified time windows
- Per-user limits: Each API key has its own rate limit configuration
- 429 Response: When limits are exceeded, includes
Retry-After
header - Configurable: Administrators can set custom limits per subscription
Example rate limit headers:
Retry-After: 60
Key configuration options in appsettings.json
:
{
"ConnectionStrings": {
"SqlConnection": "Server=.;Database=PdfSmith;Trusted_Connection=true;"
},
"AppSettings": {
"AdministratorUserName": "admin",
"AdministratorApiKey": "your-admin-api-key"
}
}
PLAYWRIGHT_BROWSERS_PATH
: Custom path for Playwright browsers installation
Chromium is automatically installed via Playwright. If PLAYWRIGHT_BROWSERS_PATH
isn't specified, browsers are installed to the default locations:
Windows:
%USERPROFILE%\AppData\Local\ms-playwright
Linux:
~/.cache/ms-playwright
macOS:
~/Library/Caches/ms-playwright
PdfSmith/
├── src/
│ ├── PdfSmith/ # Main API project
│ ├── PdfSmith.BusinessLayer/ # Business logic and services
│ ├── PdfSmith.DataAccessLayer/ # Data access and entities
│ └── PdfSmith.Shared/ # Shared models and contracts
├── samples/
│ └── PdfSmith.Client/ # Example client application
└── README.md
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.
For issues, questions, or contributions, please visit the GitHub repository.