Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
443ce3d
Python: Add structured invoice complex flow
datalogics-erics Aug 13, 2026
998e220
Python: Refine invoice header layout
datalogics-erics Aug 13, 2026
5b5c9d7
Python: Fix invoice profile indentation
datalogics-erics Aug 13, 2026
a20108f
Python: Improve invoice realism and spacing
datalogics-erics Aug 13, 2026
f44cce3
Python: Balance invoice section spacing
datalogics-erics Aug 13, 2026
ca45b8e
Python: Refine invoice footer placement
datalogics-erics Aug 13, 2026
3023bf9
Python: Align invoice footers
datalogics-erics Aug 13, 2026
dd224c7
Python: Center invoice panel content
datalogics-erics Aug 13, 2026
f94e07a
Python invoice sample: Explain setup and input files
datalogics-erics Aug 13, 2026
609df99
CreateInvoiceFromStructuredData.java: Add Java invoice workflow sample
datalogics-erics Aug 13, 2026
7cbdaab
create-invoice-from-structured-data.js: Add JavaScript invoice workflow
datalogics-erics Aug 13, 2026
f90caee
create-invoice-from-structured-data.php: Add PHP invoice workflow
datalogics-erics Aug 13, 2026
f72c460
create-invoice-from-structured-data.cs: Add .NET invoice workflow
datalogics-erics Aug 13, 2026
97b733c
create-invoice-from-structured-data.sh: Add cURL invoice workflow
datalogics-erics Aug 13, 2026
6e0d5f4
create-invoice-from-structured-data.cs: Avoid unnecessary cookie hand…
datalogics-erics Aug 13, 2026
3bc0d98
create-invoice-from-structured-data.cs: Serialize single image settin…
datalogics-erics Aug 13, 2026
e5fca0f
Invoice samples: Upload logo and pass image resource ID
datalogics-erics Aug 13, 2026
dbd940e
CreateInvoiceFromStructuredData.java: Apply Java sample formatting
datalogics-erics Aug 14, 2026
aff55e3
cURL: Fix invoice profile generation
datalogics-erics Aug 14, 2026
9ffa088
PHP: Make CSV parsing explicit
datalogics-erics Aug 14, 2026
26dd203
cURL: Format invoice currency values
datalogics-erics Aug 14, 2026
1701e79
Invoice samples: Clarify setup and failure behavior
datalogics-erics Aug 14, 2026
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
139 changes: 139 additions & 0 deletions DotNET/Complex Flow Examples/create-invoice-from-structured-data.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Generate a tagged invoice from JSON and CSV input using pdfRest.
*
* Set PDFREST_API_KEY before running this sample. It reads metadata.json,
* style.json, and line-items.csv from invoice-data, then downloads the PDF
* beside this source file. From the DotNET directory, run:
* dotnet run -- create-invoice-from-structured-data
*/

using Newtonsoft.Json.Linq;
using System.Globalization;
using System.Net.Http.Headers;
using System.Text;

namespace Samples.ComplexFlowExamples;

public static class CreateInvoiceFromStructuredData
{
// Keep the JSON, CSV, and logo fixtures beside this sample so it can run independently.
private static readonly string DataDirectory = Path.Combine("Complex Flow Examples", "invoice-data");
private static readonly string OutputPath = Path.Combine("Complex Flow Examples", "invoice-from-structured-data.pdf");

public static async Task Execute(string[] args)
{
var apiKey = Environment.GetEnvironmentVariable("PDFREST_API_KEY");
if (string.IsNullOrWhiteSpace(apiKey)) { Console.Error.WriteLine("Missing required environment variable: PDFREST_API_KEY"); Environment.Exit(1); return; }
var baseUrl = (Environment.GetEnvironmentVariable("PDFREST_URL") ?? "https://api.pdfrest.com").TrimEnd('/');
using var client = new HttpClient(new HttpClientHandler { UseCookies = false }) { BaseAddress = new Uri(baseUrl) };

// Load the bundled invoice metadata, style settings, and variable line items.
var metadata = JObject.Parse(await File.ReadAllTextAsync(Path.Combine(DataDirectory, "metadata.json")));
var style = JObject.Parse(await File.ReadAllTextAsync(Path.Combine(DataDirectory, "style.json")));
var items = ReadCsv(Path.Combine(DataDirectory, "line-items.csv"));

// Start with a blank page, then add the header panels and tagged header text.
var id = (string)(await PostJson(client, apiKey, "blank-pdf", new JObject { ["page_size"] = "letter", ["page_count"] = 1, ["page_orientation"] = "portrait" }))!["outputId"]!;
id = (string)(await PostMultipart(client, apiKey, "pdf-with-added-shapes", new Dictionary<string, object?> { ["id"] = id, ["shape_objects"] = Shapes(style, 1), ["tag_enabled"] = "true" }))!["outputId"]!;
id = (string)(await PostMultipart(client, apiKey, "pdf-with-added-text", new Dictionary<string, object?> { ["id"] = id, ["text_objects"] = TextObjects(metadata, style), ["tag_enabled"] = "true", ["tag_language"] = "en-US" }))!["outputId"]!;

// Insert the data-driven table; the table endpoint handles row layout and overflow.
id = (string)(await PostMultipart(client, apiKey, "pdf-with-added-tables", new Dictionary<string, object?> { ["id"] = id, ["table_objects"] = Tables(metadata, style, items), ["tag_enabled"] = "true", ["tag_language"] = "en-US" }))!["outputId"]!;

// Upload the logo once, then reuse its resource ID when placing the image.
var imagePath = Path.Combine(DataDirectory, "northstar-logo.png");
var logoId = await UploadImage(client, apiKey, imagePath);
id = (string)(await PostMultipart(client, apiKey, "pdf-with-added-image", new Dictionary<string, object?> { ["id"] = id, ["image_id"] = logoId, ["page"] = 1, ["x"] = 54, ["y"] = 716, ["width"] = 200, ["tag_alt_text"] = "Northstar Sample Supply logo", ["tag_structure_type"] = "Figure", ["tag_enabled"] = "true", ["tag_language"] = "en-US" }))!["outputId"]!;

// Query the final page count before adding the last-page panel and running footers.
var info = await PostMultipart(client, apiKey, "pdf-info", new Dictionary<string, object?> { ["id"] = id, ["queries"] = "page_count" });
var pageCount = info!["page_count"]!.Value<int>();
id = (string)(await PostMultipart(client, apiKey, "pdf-with-added-shapes", new Dictionary<string, object?> { ["id"] = id, ["shape_objects"] = Shapes(style, pageCount, true), ["tag_enabled"] = "true" }))!["outputId"]!;
id = (string)(await PostMultipart(client, apiKey, "pdf-with-added-text", new Dictionary<string, object?> { ["id"] = id, ["text_objects"] = Footer(metadata, style, pageCount), ["tag_enabled"] = "true", ["tag_language"] = "en-US", ["output"] = "invoice_from_structured_data" }))!["outputId"]!;
var output = await client.GetByteArrayAsync($"resource/{id}?format=file");
await File.WriteAllBytesAsync(OutputPath, output);
Console.WriteLine($"Created {Path.GetFullPath(OutputPath)}");
}

private static async Task<string> UploadImage(HttpClient client, string apiKey, string path)
{
using var request = new HttpRequestMessage(HttpMethod.Post, "upload");
request.Headers.Add("Api-Key", apiKey);
request.Headers.Add("Content-Filename", Path.GetFileName(path));
request.Content = new ByteArrayContent(await File.ReadAllBytesAsync(path));
request.Content.Headers.ContentType = new("application/octet-stream");
var result = await Send(client, request, "upload");
return (string)result["files"]![0]!["id"]!;
}

private static async Task<JObject> PostJson(HttpClient client, string key, string endpoint, JObject payload)
{
using var request = new HttpRequestMessage(HttpMethod.Post, endpoint); request.Headers.Add("Api-Key", key); request.Content = new StringContent(payload.ToString(Newtonsoft.Json.Formatting.None), Encoding.UTF8, "application/json");
return await Send(client, request, endpoint);
}

private static async Task<JObject> PostMultipart(HttpClient client, string key, string endpoint, Dictionary<string, object?> fields)
{
using var request = new HttpRequestMessage(HttpMethod.Post, endpoint); request.Headers.Add("Api-Key", key); using var form = new MultipartFormDataContent();
foreach (var field in fields) {
if (field.Value is string path && field.Key == "image_files") form.Add(new StreamContent(File.OpenRead(path)), field.Key, Path.GetFileName(path));
else form.Add(new StringContent(field.Value is JToken token ? token.ToString(Newtonsoft.Json.Formatting.None) : field.Value?.ToString() ?? ""), field.Key);
}
request.Content = form; return await Send(client, request, endpoint);
}

private static async Task<JObject> Send(HttpClient client, HttpRequestMessage request, string endpoint)
{
using var response = await client.SendAsync(request); var text = await response.Content.ReadAsStringAsync(); Console.WriteLine($"{endpoint}: {(int)response.StatusCode}");
if (!response.IsSuccessStatusCode) throw new InvalidOperationException($"{endpoint} failed: {text}"); return JObject.Parse(text);
}

private static List<Dictionary<string, string>> ReadCsv(string path)
{
var lines = File.ReadAllLines(path); var headers = ParseCsvLine(lines[0]); var rows = new List<Dictionary<string, string>>();
foreach (var line in lines.Skip(1).Where(line => !string.IsNullOrWhiteSpace(line))) { var values = ParseCsvLine(line); rows.Add(headers.Select((header, index) => new { header, value = values[index] }).ToDictionary(x => x.header, x => x.value)); }
return rows;
}

private static List<string> ParseCsvLine(string line)
{ var fields = new List<string>(); var field = new StringBuilder(); var quoted = false; foreach (var character in line) { if (character == '"') quoted = !quoted; else if (character == ',' && !quoted) { fields.Add(field.ToString()); field.Clear(); } else field.Append(character); } fields.Add(field.ToString()); return fields; }
private static string Rgb(JToken token) => string.Join(',', token.Values<int>());
private static string Money(double value) => value.ToString("C2", CultureInfo.GetCultureInfo("en-US"));

private static JArray TextObjects(JObject metadata, JObject style)
{
var objects = new JArray(); var seller = metadata["seller"]!; var customer = metadata["customer"]!; var primary = Rgb(style["primaryColorRgb"]!); var muted = Rgb(style["mutedTextColorRgb"]!);
void Add(int x, int y, string text, double size, string color, int width, string structure = "P", bool bold = false) => objects.Add(new JObject { ["font"] = bold ? style["boldFont"] : style["bodyFont"], ["max_width"] = width, ["opacity"] = "1", ["page"] = "1", ["rotation"] = "0", ["text"] = text, ["text_color_rgb"] = color, ["text_size"] = size, ["x"] = x, ["y"] = y, ["tag_structure_type"] = structure });
Add(405, 724, "INVOICE", 22, primary, 153, "H1", true); Add(405, 696, $"Invoice {metadata["invoiceNumber"]}", 9, muted, 153, "P", true); Add(405, 682, $"Issued {metadata["issueDate"]}", 9, muted, 153); Add(405, 668, $"Due {metadata["dueDate"]}", 9, muted, 153);
Add(66, 622, "FROM", 8, primary, 216, "H2", true); Add(66, 606, (string)seller["name"]!, 9, "34,34,34", 216, "P", true); Add(66, 592, (string)seller["taxId"]!, 8, muted, 216); Add(66, 578, (string)seller["addressLine1"]!, 8, muted, 216); Add(66, 566, $"{seller["city"]}, {seller["region"]} {seller["postalCode"]}", 8, muted, 216);
Add(330, 622, "BILL TO", 8, primary, 216, "H2", true); Add(330, 606, (string)customer["name"]!, 9, "34,34,34", 216, "P", true); Add(330, 592, (string)customer["addressLine1"]!, 8, muted, 216); Add(330, 578, $"{customer["city"]}, {customer["region"]} {customer["postalCode"]}", 8, muted, 216); return objects;
}

private static JArray Tables(JObject metadata, JObject style, List<Dictionary<string, string>> items)
{
var subtotal = items.Sum(item => double.Parse(item["quantity"]) * double.Parse(item["unitPrice"])); var tax = Math.Round(subtotal * (double)metadata["taxRate"]!, 2); var total = subtotal + tax; var primary = style["primaryColorRgb"]!;
var headers = new JArray("Description", "Qty", "Unit Price", "Amount"); var header = new JArray(headers.Select((text, index) => new JObject { ["text"] = text, ["tag_structure_type"] = "TH", ["style"] = new JObject { ["background_color_rgb"] = primary, ["text_color_rgb"] = new JArray(255, 255, 255), ["text_align"] = index > 0 ? "right" : "left" } }));
var rows = new JArray();
foreach (var item in items)
{
rows.Add(new JObject
{
["cells"] = new JArray(
new JObject { ["text"] = item["description"] },
new JObject { ["text"] = item["quantity"], ["style"] = new JObject { ["text_align"] = "right" } },
new JObject { ["text"] = Money(double.Parse(item["unitPrice"])), ["style"] = new JObject { ["text_align"] = "right" } },
new JObject { ["text"] = Money(double.Parse(item["quantity"]) * double.Parse(item["unitPrice"])), ["style"] = new JObject { ["text_align"] = "right" } })
});
}
JObject Summary(string label, double value, bool highlight = false) { var cellStyle = new JObject { ["text_align"] = "right" }; if (highlight) { cellStyle["background_color_rgb"] = style["accentColorRgb"]; cellStyle["text_size"] = 10; } return new JObject { ["cells"] = new JArray(new JObject { ["text"] = label, ["col_span"] = 3, ["style"] = cellStyle }, new JObject { ["text"] = Money(value), ["style"] = cellStyle }) }; }
var tableStyle = new JObject { ["padding"] = new JObject { ["top"] = 5, ["right"] = 6, ["bottom"] = 5, ["left"] = 6 }, ["text_size"] = style["tableHeaderFontSize"], ["text_color_rgb"] = style["textColorRgb"] };
var columns = new JArray(new JObject { ["width"] = 276 }, new JObject { ["width"] = 54 }, new JObject { ["width"] = 84 }, new JObject { ["width"] = 90 });
var footer = new JArray(Summary("Subtotal", subtotal), Summary($"Tax {(double)metadata["taxRate"]! * 100:0.00}%", tax), Summary("Total", total, true));
var table = new JObject { ["page"] = 1, ["x"] = 54, ["y"] = 510, ["width"] = 504, ["columns"] = columns, ["continuation_page_top_margin"] = 85, ["page_bottom_margin"] = 96, ["final_page_bottom_margin"] = 164, ["overflow_behavior"] = "split-row", ["row_split_behavior"] = "prefer-next-page", ["repeat_header_on_overflow"] = true, ["show_footer_on_last_page"] = true, ["tag_structure_type"] = "Table", ["style"] = tableStyle, ["header_rows"] = new JArray(new JObject { ["cells"] = header }), ["rows"] = rows, ["footer_rows"] = footer };
return new JArray(table);
}

private static JArray Shapes(JObject style, int page, bool footer = false) { var border = Rgb(style["borderColorRgb"]!); if (footer) return new JArray(new JObject { ["type"] = "rectangle", ["page"] = page, ["x"] = 54, ["y"] = 70, ["width"] = 504, ["height"] = 104, ["fill_color_rgb"] = "248,250,251", ["stroke_color_rgb"] = border, ["stroke_width"] = 0.5, ["tag_is_artifact"] = true }); return new JArray(new JObject { ["type"] = "rectangle", ["page"] = 1, ["x"] = 54, ["y"] = 540, ["width"] = 240, ["height"] = 96, ["fill_color_rgb"] = Rgb(style["accentColorRgb"]!), ["stroke_color_rgb"] = border, ["stroke_width"] = 0.5, ["tag_is_artifact"] = true }, new JObject { ["type"] = "rectangle", ["page"] = 1, ["x"] = 318, ["y"] = 540, ["width"] = 240, ["height"] = 96, ["fill_color_rgb"] = Rgb(style["accentColorRgb"]!), ["stroke_color_rgb"] = border, ["stroke_width"] = 0.5, ["tag_is_artifact"] = true }); }

private static JArray Footer(JObject metadata, JObject style, int pageCount) { var footer = new JArray(); var muted = Rgb(style["mutedTextColorRgb"]!); void Add(string text, int page, int x, int y, double size, int width, string structure = "P", bool bold = false) => footer.Add(new JObject { ["font"] = bold ? style["boldFont"] : style["bodyFont"], ["max_width"] = width, ["opacity"] = "1", ["page"] = page.ToString(), ["rotation"] = "0", ["text"] = text, ["text_color_rgb"] = muted, ["text_size"] = size, ["x"] = x, ["y"] = y, ["tag_structure_type"] = structure }); Add("Payment terms", pageCount, 66, 156, 8, 480, "H2", true); Add((string)metadata["paymentTerms"]!, pageCount, 66, 142, 7.5, 480); Add("Notes", pageCount, 66, 112, 8, 480, "H2", true); Add((string)metadata["notes"]!, pageCount, 66, 98, 7.5, 480); for (int page = 1; page <= pageCount; page++) { Add("Generated from structured JSON and CSV input with pdfRest.", page, 54, 54, 7.5, 400); Add($"Page {page} of {pageCount}", page, 490, 54, 7.5, 68); } return footer; }
}
27 changes: 27 additions & 0 deletions DotNET/Complex Flow Examples/invoice-data/line-items.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
itemCode,description,quantity,unitPrice
PLAN-100,"Seasonal site planning workshop for community maintenance team",1,850.00
FIELD-210,"Field guide drafting for entrance signage, path notes, and accessibility reminders",4,175.00
MAP-115,"Illustrated reference map preparation using supplied fictional location data",2,240.00
OPS-305,"Operations checklist review and formatting for monthly volunteer handoff",3,135.00
TRAIN-250,"Remote training session for invoice recipient staff",2,225.00
DOC-410,"Structured source content review and cleanup",5,96.00
PDF-120,"Tagged PDF generation setup and verification",2,310.00
TABLE-215,"Long table layout review for multipage invoice output",6,88.00
QA-330,"Accessibility-oriented review of headings, tables, and reading order",3,155.00
SUP-101,"Sample support block for customer billing questions",1,75.00
DATA-222,"CSV line item normalization and validation",4,82.50
STYLE-140,"Style configuration tuning for colors, fonts, and spacing",3,115.00
IMG-118,"Local logo placement and scaling check",1,145.00
COPY-201,"Boilerplate payment and note copy preparation",2,90.00
ARCH-302,"Document structure planning for tagged invoice output",2,275.00
PAG-450,"Pagination stress review with repeated line item sections",5,122.00
TOTAL-190,"Subtotal, tax, and grand total calculation review",1,160.00
EXPORT-275,"Final PDF save and font embedding verification",2,205.00
HAND-410,"Reviewer handoff notes and sample usage instructions",1,130.00
MISC-999,"Fictional miscellaneous line item included to extend the sample invoice across pages",7,64.00
MEET-275,"Quarterly planning meeting preparation and follow-up summary",2,185.00
TRAIN-315,"Team workflow training and recorded process walkthrough",3,145.00
DATA-360,"Operational data cleanup and reporting field review",4,92.50
BRAND-125,"Customer-facing document template and brand review",1,225.00
ACCESS-205,"Accessibility checks for headings, contrast, and table structure",2,195.00
ARCH-515,"Archive-ready document packaging and delivery checklist",1,180.00
10 changes: 10 additions & 0 deletions DotNET/Complex Flow Examples/invoice-data/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"invoiceNumber": "INV-2026-0417",
"issueDate": "2026-04-17",
"dueDate": "2026-05-17",
"taxRate": 0.0825,
"paymentTerms": "Payment is due within 30 days. ACH payments may be submitted through the fictional payment portal. Please include the invoice number with all payments.",
"notes": "Thank you for choosing Northstar Sample Supply. This invoice uses fictional data and demonstrates PDF generation from structured input files.",
"seller": {"name": "Northstar Sample Supply", "taxId": "EIN 84-5162047", "addressLine1": "410 Fictional Market Street", "city": "Port Azure", "region": "WA", "postalCode": "98104"},
"customer": {"name": "Harbor & Pine Community Works", "addressLine1": "88 Cedar Walk", "city": "Lakefield", "region": "OR", "postalCode": "97035"}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions DotNET/Complex Flow Examples/invoice-data/style.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"bodyFont": "helvetica",
"boldFont": "helvetica bold",
"tableHeaderFontSize": 8.5,
"primaryColorRgb": [36, 83, 106],
"accentColorRgb": [232, 241, 244],
"textColorRgb": [34, 34, 34],
"mutedTextColorRgb": [102, 102, 102],
"borderColorRgb": [184, 199, 206]
}
4 changes: 4 additions & 0 deletions DotNET/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ static void PrintUsage()
Console.Error.WriteLine(" preserve-word-document <office> Office → PDF → PDF/A-3b");
Console.Error.WriteLine(" protected-watermark <pdf> Watermark then restrict");
Console.Error.WriteLine(" redact-preview-and-finalize <pdf> Preview then apply redactions\n");
Console.Error.WriteLine(" create-invoice-from-structured-data Generate invoice from JSON and CSV\n");

Console.Error.WriteLine("Environment (.env supported):");
Console.Error.WriteLine(" PDFREST_API_KEY=... Required API key");
Expand Down Expand Up @@ -218,6 +219,9 @@ static void PrintUsage()
case "redact-preview-and-finalize":
await Samples.ComplexFlowExamples.RedactPreviewAndFinalize.Execute(rest);
break;
case "create-invoice-from-structured-data":
await Samples.ComplexFlowExamples.CreateInvoiceFromStructuredData.Execute(rest);
break;
case "extracted-text":
await Samples.EndpointExamples.JsonPayload.ExtractedText.Execute(rest);
break;
Expand Down
Loading
Loading