From d75805ab0b4dcf3dc952e4d9266f29f64883570d Mon Sep 17 00:00:00 2001 From: Rakulan Subramaniam Date: Fri, 2 Oct 2026 14:39:20 +0200 Subject: [PATCH] docs: add user documentation Add a docs/ folder organized as tutorials, how-to guides, reference and explanation: - tutorials/getting-started.md: build an invoice step by step - how-to: generate output, headers/footers/page numbers, fonts, reusable components, debugging layout issues, Linux and Docker - reference: all layout elements, text and text styles - explanation: how the layout engine measures, draws and pages content All C# samples compile against the public API. Closes #9 Co-Authored-By: Claude Opus 5.5 --- docs/README.md | 39 + docs/explanation/layout-engine.md | 204 +++++ docs/how-to/debug-layout-issues.md | 332 ++++++++ docs/how-to/deploy-on-linux.md | 114 +++ docs/how-to/fonts.md | 232 ++++++ docs/how-to/generate-output.md | 271 +++++++ docs/how-to/headers-footers-page-numbers.md | 265 +++++++ docs/how-to/reusable-components.md | 456 +++++++++++ docs/reference/elements.md | 804 ++++++++++++++++++++ docs/reference/text.md | 402 ++++++++++ docs/tutorials/getting-started.md | 394 ++++++++++ 11 files changed, 3513 insertions(+) create mode 100644 docs/README.md create mode 100644 docs/explanation/layout-engine.md create mode 100644 docs/how-to/debug-layout-issues.md create mode 100644 docs/how-to/deploy-on-linux.md create mode 100644 docs/how-to/fonts.md create mode 100644 docs/how-to/generate-output.md create mode 100644 docs/how-to/headers-footers-page-numbers.md create mode 100644 docs/how-to/reusable-components.md create mode 100644 docs/reference/elements.md create mode 100644 docs/reference/text.md create mode 100644 docs/tutorials/getting-started.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..0f34fbe23 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,39 @@ +# ShinyPDF documentation + +ShinyPDF is a .NET library for generating PDF documents with a C# Fluent API. This documentation is organized by what you need right now: learning the basics, solving a specific task, looking up an API, or understanding how the library works. + +New to ShinyPDF? Start with the [getting started tutorial](tutorials/getting-started.md). + +## Tutorials + +Step-by-step lessons that build a working document from scratch. + +- [Getting started](tutorials/getting-started.md): install the package and build your first document + +## How-to guides + +Recipes for specific tasks. + +- [Generate PDF, XPS and images](how-to/generate-output.md): files, streams, images per page, metadata and settings +- [Headers, footers and page numbers](how-to/headers-footers-page-numbers.md): page setup, repeating content and numbering +- [Use custom fonts](how-to/fonts.md): register fonts, fallback for missing glyphs, right-to-left text +- [Build reusable components](how-to/reusable-components.md): components, extension methods and dynamic content +- [Debug layout issues](how-to/debug-layout-issues.md): find out why content does not fit or a layout fails +- [Deploy on Linux and Docker](how-to/deploy-on-linux.md): native assets, system libraries and fonts + +## Reference + +Lookup material for the public API. + +- [Elements](reference/elements.md): layout containers, sizing, spacing, visual elements, paging and navigation +- [Text](reference/text.md): text content, spans, page numbers and text styles + +## Explanation + +Background on how ShinyPDF works. + +- [Layout engine](explanation/layout-engine.md): measuring and drawing, paging, layout exceptions and the rendering pipeline + +## More examples + +The [examples project](../src/ShinyPDF.Examples) contains a rendering test for almost every feature. Each test produces a PDF or images, so it is a good place to see an element in action. diff --git a/docs/explanation/layout-engine.md b/docs/explanation/layout-engine.md new file mode 100644 index 000000000..f35b7bd6f --- /dev/null +++ b/docs/explanation/layout-engine.md @@ -0,0 +1,204 @@ +# How the layout engine works + +This page explains what happens between your `Document.Create(...)` call and the finished PDF: how the Fluent API builds a tree of elements, how that tree is measured and drawn page by page, why content moves to the next page, when and why layout exceptions are thrown, and which libraries do the actual rendering. It is background reading; you do not need it to write your first document, but it makes most layout surprises predictable. For hands-on troubleshooting, see [Debug layout issues](../how-to/debug-layout-issues.md). + +## The element tree + +Every Fluent API call creates an element and attaches it to its parent. A chain such as + +```csharp +container + .Padding(10) + .Background(Colors.Grey.Lighten3) + .Width(200) + .Text("Hello"); +``` + +does not draw anything. It builds a small tree: a `Padding` element whose child is a `Background` element, whose child is a size constraint, whose child is a text block. Most methods (`Padding`, `Background`, `Width`, `AlignCenter`, `ShowEntire`, ...) return the `IContainer` of the new element so that the next call becomes its child. Methods that end a chain (`Text`, `Image`, `Column`, `Row`, `Table`, `Layers`, `PageBreak`, ...) produce leaves or elements with several children. + +Each single-child container accepts exactly one child. Assigning a second one throws a `DocumentComposeException` at compose time; this usually means a container variable was reused outside its closure or inside a loop. + +The whole document is one tree. Each `container.Page(...)` call produces a page component; internally all pages are placed in a column with page breaks between them. A page itself is composed from ordinary elements: + +```mermaid +flowchart TD + Doc["Document root (column of pages, separated by page breaks)"] --> Page + Page["Page: size constraints, margins, default text style"] --> Layers + Layers --> Bg["Background layer"] + Layers --> Primary["Primary layer"] + Layers --> Fg["Foreground layer"] + Primary --> Deco["Decoration"] + Deco --> Header["Header (Before)"] + Deco --> Content["Content"] + Deco --> Footer["Footer (After)"] + Content --> Tree["Your element tree"] +``` + +The tree is built exactly once, when the document is generated. `IComponent.Compose` methods and your `Action` lambdas run at that point, not once per page. The only user code that runs during layout is a dynamic component (see [Reusable components](../how-to/reusable-components.md#dynamic-components)), a `Canvas` handler, and dynamic images. + +## Two operations: Measure and Draw + +Every element implements two internal operations: + +```csharp +// not compiled: simplified internal contract of every element +abstract class Element +{ + // "If I get this much space, how much do I need, and do I finish?" + abstract SpacePlan Measure(Size availableSpace); + + // "Draw yourself into this space and remember how far you got." + abstract void Draw(Size availableSpace); +} +``` + +**Measure** receives the space the parent offers and returns a `SpacePlan`: a width, a height and one of three types (`ShinyPDF.Drawing.SpacePlanType`): + +| SpacePlanType | Meaning | +|---|---| +| `FullRender` | The element fits completely into the offered space, using the reported width and height. | +| `PartialRender` | The element can draw a part of itself here (using the reported size) and has more content left for the next page. | +| `Wrap` | The element cannot draw anything useful in this space. It has to move to the next page (or the layout fails). | + +**Draw** paints the element into the space it was given and advances its internal progress. A column remembers which items are already finished, a text block remembers which lines it already printed, a table remembers its current row. The next page then continues from that point. + +Measure has no side effects and is called often: parents measure children to plan their own layout, and most elements measure their children again inside Draw. This is also why caching matters (see below). + +### Constraints go down, sizes come up + +Layout is a negotiation between parent and child: + +1. The parent passes an available `Size` down. A page offers the page size minus margins; `Padding(10)` offers its child 20 points less in each direction; `Width(100)` offers exactly 100 points of width; a column offers each item the full width and whatever height is still left on the page. +2. The child reports back the size it needs, never more than it was offered. If it would need more, it reports `Wrap`. +3. The parent combines the children's results into its own `SpacePlan` and reports that further up. + +Some consequences that explain many layouts: + +- An element can never be larger than its parent's offer. `Width(150)` inside `Width(100)` does not overflow; it reports `Wrap`, because a minimum width of 150 cannot be satisfied in 100 points. +- Most elements report only the space they need, but some take everything offered: an image (within its aspect ratio) and a `Canvas` fill the available space, and `ExtendHorizontal()` / `ExtendVertical()` stretch their child in that direction. Wrap such elements in a size constraint to give them a specific size. +- `Unconstrained` measures its child with unlimited space and reports a size of zero to its parent, so the child can draw outside the parent's bounds. +- `ScaleToFit` measures its child at smaller and smaller scales until it fits; it never splits content. + +## How content flows across pages + +The generator lays out the document one page at a time: + +```mermaid +flowchart TD + Start([Start page N]) --> M["Measure the root with unlimited space"] + M --> W{Result} + W -- Wrap --> Ex["Throw DocumentLayoutException"] + W -- PartialRender or FullRender --> D["Draw page N"] + D --> T{"N >= DocumentLayoutExceptionThreshold?"} + T -- yes --> Ex + T -- no --> F{"Was it FullRender?"} + F -- no --> Next["N = N + 1"] --> Start + F -- yes --> Done([Document finished]) +``` + +On every page the root is measured, then drawn into the measured size. As long as the root reports `PartialRender`, another page follows. Because elements remember their progress during Draw, each page continues where the previous one stopped. + +### Page header, content and footer + +Inside a page, the header, content and footer are arranged by a `Decoration` element (the same element that `container.Decoration(...)` creates): + +- The header and the footer are measured first, with the full page area. They are drawn on **every** page and must fit **completely** on every page. If a header or footer reports `PartialRender` or `Wrap`, the page cannot be laid out. +- The content receives the remaining height (page height minus margins, header and footer) and is the only part that is split across pages. + +Table headers and footers (`table.Header(...)`, `table.Footer(...)`) use the same mechanism: they repeat on every page the table spans, and each must fit entirely. + +Use `ShowOnce()` and `SkipOnce()` inside a header or footer to vary them between the first and the following pages; see [Headers, footers and page numbers](../how-to/headers-footers-page-numbers.md). + +### Which elements can split + +| Splits across pages | How it splits | +|---|---| +| `Text` | Between lines. A single line is never split. | +| `Column` | Between items, and inside an item if that item can split. | +| `Table` | Between rows; a row splits if its cells can split. Header and footer repeat. | +| `Row` | All items continue on the next page together; the row is as tall as its tallest unfinished item. | +| `Inlined` | Between lines of items. | +| Single-child wrappers (`Padding`, `Background`, `Border`, `Width`/`Height` constraints, alignment, `Scale`, ...) | They pass the split through from their child. Padding and borders are repeated on each page part. | + +| Does not split | Behavior | +|---|---| +| `Image`, `Canvas`, `Line` | Drawn as a whole into the offered space, or `Wrap` if they cannot get it. | +| `AspectRatio` (used by `Image`) | Needs its full target size; if that is taller than the remaining space, it wraps. | +| `ScaleToFit` | Shrinks the child until it fits in one piece. | +| Page header and footer, table header and footer | Must fit entirely on every page. | +| `ShowEntire()` | Turns a `PartialRender` of its child into `Wrap`: the child is moved to the next page instead of being split. | +| Content of a dynamic component | Must fit completely on the current page; the component itself decides what to show on the next page. | + +A few elements control paging explicitly: + +- `PageBreak()` reports `PartialRender` with zero size the first time, which ends the current page. +- `EnsureSpace(minHeight)` moves its child to the next page if the child would be split and less than `minHeight` points (default 150) are left. +- `StopPaging()` draws only what fits on the current page and then reports the element as finished, so the rest is dropped. +- `ShowOnce()` shows its child until it has been drawn completely once and hides it afterwards; `SkipOnce()` hides its child on the first page it appears on and shows it on the following ones. + +### Why Wrap happens + +`Wrap` is not an error by itself; it is how an element says "not here". A column that receives `Wrap` from its next item stops and reports `PartialRender` for the items it could place, so the wrapped item starts at the top of the next page. That is normal page flow. + +`Wrap` becomes a problem when it reaches the root of the page. That happens when the content cannot be placed even on an empty page, for example: + +- a fixed `Height(...)` or `Width(...)` (or `MinHeight`, `MinWidth`) larger than the space the parent offers, +- an image whose height at the current width is larger than the page content area, +- `ShowEntire()` around content that is longer than one page, +- a header or footer that does not fit, or leaves no room for the content that still has to be placed, +- text in a space narrower than a single character or lower than a single line. + +## Why DocumentLayoutException happens + +The generator throws `ShinyPDF.Drawing.Exceptions.DocumentLayoutException` in two situations, both with the same message ("Composed layout generates infinite document"): + +1. **The root reports `Wrap`.** Some element needs more space than the page can ever offer, so moving it to another page would not help. Without this check the engine would produce empty pages forever. +2. **The page count reaches `Settings.DocumentLayoutExceptionThreshold`** (default: 250 pages). Either the document really is that long, or something keeps reporting `PartialRender` without ever finishing (for example a dynamic component that always returns `HasMoreContent = true`). The check runs after each drawn page, before the engine looks at whether the document is finished, so a document with exactly as many pages as the threshold already fails; the limit has to be higher than your longest document. + +If your documents legitimately exceed 250 pages, raise the limit once at application startup: + +```csharp +using ShinyPDF; + +Settings.DocumentLayoutExceptionThreshold = 2000; +``` + +A dynamic component whose generated content does not fit on the current page also throws `DocumentLayoutException` (with the message "Dynamic component generated content that does not fit on a single page."). + +The exception has an `ElementTrace` property. When `Settings.EnableDebugging` is on, it contains the measured element tree of the failing page, with the available and requested space of every element; otherwise it only contains a hint that the trace requires debugging mode. How to read that trace is described in [Debug layout issues](../how-to/debug-layout-issues.md#reading-a-documentlayoutexception). + +## Caching + +Since Measure is called many times for the same element on the same page, the engine can memoize results. With `Settings.EnableCaching` on, cacheable elements (most built-in layout elements) get a cache in front of their children: a repeated Measure with the same available size returns the stored result, and the cache is cleared when the element is drawn, because drawing changes its progress. + +The default is `true` when no debugger is attached and `false` when one is. Caching only affects speed and memory, never the result, so you rarely need to touch it. Debugging works the other way around: `Settings.EnableDebugging` defaults to `true` only when a debugger is attached, because tracking every measurement costs time. + +## The rendering pipeline + +```mermaid +flowchart LR + A["Document.Create / IDocument"] --> B["Compose: build element tree"] + B --> C["Apply text styles, content direction, debugging and caching"] + C --> D["Pass 1: layout on a measuring canvas (no output)"] + D --> E["Pass 2: layout and draw on the target canvas"] + E --> F["PdfCanvas: PDF"] + E --> G["XpsCanvas: XPS"] + E --> H["ImageCanvas: one PNG per page"] +``` + +When you call `GeneratePdf`, `GenerateXps` or `GenerateImages` (see [Generate output](../how-to/generate-output.md)), the internal `DocumentGenerator`: + +1. Calls `Compose` on the document and builds the element tree. +2. Propagates default text styles and content direction (left-to-right or right-to-left) down the tree, and adds the debugging and caching helpers if enabled. +3. Runs the complete page-by-page layout **twice** with the same page context. The first pass draws on a canvas that discards everything; its purpose is to learn where every page and every section ends. This is how `TotalPages()`, section page numbers and section links know values that are only known after the whole document has been laid out. Before each pass all elements reset their progress. +4. Runs the second pass on the target canvas: `PdfCanvas` (PDF), `XpsCanvas` (XPS) or `ImageCanvas` (one PNG per page, rendered at `DocumentMetadata.RasterDpi`). + +All three canvases are thin wrappers around [SkiaSharp](https://github.com/mono/SkiaSharp), which writes the PDF and XPS files and rasterizes images. Text is shaped with [HarfBuzz](https://harfbuzz.github.io/) (through HarfBuzzSharp), which turns characters into positioned glyphs: kerning, ligatures, combining marks and right-to-left scripts. Missing glyphs are resolved through font fallback; see [Use custom fonts](../how-to/fonts.md). Both libraries ship native binaries, which is why Linux needs extra packages ([Deploy on Linux](../how-to/deploy-on-linux.md)). + +Because layout runs twice, any code that runs during layout (dynamic components, `Canvas` handlers) also runs at least twice, and dynamic components are additionally called several times per page. Keep such code free of side effects. + +## Relationship to QuestPDF + +ShinyPDF is based on the latest fully open-source version of [QuestPDF](https://github.com/QuestPDF/QuestPDF), released under the MIT license. The layout engine described on this page, the element set and the Fluent API come from that code base; ShinyPDF uses its own `ShinyPDF.*` namespaces and is maintained independently. The repository's [LICENSE](../../LICENSE) file contains both the ShinyPDF MIT license (LM IT Services AG) and the original QuestPDF MIT license notice. + +Because of this shared history, articles and answers written for that QuestPDF version often apply to ShinyPDF as well, but newer QuestPDF features and behavior are not part of ShinyPDF. diff --git a/docs/how-to/debug-layout-issues.md b/docs/how-to/debug-layout-issues.md new file mode 100644 index 000000000..a81256d8f --- /dev/null +++ b/docs/how-to/debug-layout-issues.md @@ -0,0 +1,332 @@ +# Debug layout issues + +This guide shows how to find out why a document does not look the way you expect or why generation fails: how to visualize element boundaries with `DebugArea`, how to get and read the element trace of a `DocumentLayoutException`, how to mark parts of your layout with `DebugPointer`, and how to fix the most common causes. It assumes you know the basics of the Fluent API; the background on measuring, wrapping and paging is in [How the layout engine works](../explanation/layout-engine.md). + +## Know which exception you have + +ShinyPDF throws four exception types, all in the `ShinyPDF.Drawing.Exceptions` namespace. They tell you in which phase the problem occurred: + +| Exception | When | Typical cause | +|---|---|---| +| `DocumentComposeException` | While the element tree is built, before any layout | Invalid structure: two children in one container, a `Layers` element without exactly one primary layer, a table without columns or with impossible cell positions, image data that cannot be decoded | +| `DocumentLayoutException` | During layout | Content that can never fit on a page, or a document longer than `Settings.DocumentLayoutExceptionThreshold` pages | +| `DocumentDrawingException` | During drawing | An exception thrown while a page was drawn (the original one is in `InnerException`), or a character that no configured font contains | +| `InitializationException` | When the PDF, XPS or image backend is created | Missing native SkiaSharp or HarfBuzzSharp libraries, see [Deploy on Linux](deploy-on-linux.md) | + +## See element boundaries with DebugArea + +Many layout problems are not exceptions: an element is smaller, larger or placed differently than you thought. `DebugArea` draws a colored border and a translucent background around its child, with an optional label, so you can see the exact space an element occupies: + +```csharp +container + .Padding(20) + .DebugArea("Customer address", Colors.Blue.Medium) + .Column(column => + { + column.Item().Text("Jane Doe"); + column.Item().Text("Main Street 1"); + }); +``` + +The overloads are `DebugArea()`, `DebugArea(string text)` (red) and `DebugArea(string text, string color)`. `DebugArea` returns the container for the child, so you can insert it anywhere in a chain and remove it again later without restructuring your code. Nest several areas with different colors to compare a parent and its child. + +## Get the element trace of a DocumentLayoutException + +A `DocumentLayoutException` always has the same message ("Composed layout generates infinite document ..."), because the engine only knows that the page could not be completed. The useful information is in the `ElementTrace` property, which is only filled when `Settings.EnableDebugging` is `true`. + +`EnableDebugging` defaults to `true` only when a debugger is attached. In tests, CI or production it is off, and `ElementTrace` only contains "Debug trace is available only in the DEBUG mode." Turn it on explicitly while you investigate: + +```csharp +using ShinyPDF; +using ShinyPDF.Drawing.Exceptions; + +Settings.EnableDebugging = true; + +var document = Document.Create(container => +{ + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(2, Unit.Centimetre); + + page.Content() + .Width(100) + .DebugPointer("Customer card") + .Width(150) + .Text("Example"); + }); +}); + +try +{ + document.GeneratePdf("report.pdf"); +} +catch (DocumentLayoutException exception) +{ + Console.WriteLine(exception.Message); + Console.WriteLine(exception.ElementTrace); + throw; +} +``` + +Debugging adds a tracking proxy around every element and makes generation slower, so do not leave it enabled in production code. + +## Reading a DocumentLayoutException + +The trace is the tree of elements that were measured on the page where layout failed, starting at the document root. Shortened, the trace for the example above looks like this: + +```text +🔥 Column +--------- +Available space: (Width: 14,400.000, Height: 14,400.000) +Requested space: Wrap + + ... + + 🔥 Constrained + -------------- + Available space: (Width: 14,400.000, Height: 14,400.000) + Requested space: Wrap + Content Direction: LeftToRight + Min Width: 595.4 + Max Width: 595.4 + Min Height: 842 + Max Height: 842 + + 🔥 Padding + ---------- + Available space: (Width: 595.400, Height: 842.000) + Requested space: Wrap + Top: 56.692913 + Right: 56.692913 + Bottom: 56.692913 + Left: 56.692913 + + ... + + 🔥 Decoration + ------------- + Available space: (Width: 482.014, Height: 728.614) + Requested space: Wrap + Content Direction: LeftToRight + + Page header 🌟 + -------------- + Available space: (Width: 482.014, Height: 728.614) + Requested space: FullRender (Width: 0.000, Height: 0.000) + + ... + + 🔥 Page content 🌟 + ------------------ + Available space: (Width: 482.014, Height: 728.614) + Requested space: Wrap + + 🔥 Constrained + -------------- + Available space: (Width: 482.014, Height: 728.614) + Requested space: Wrap + Content Direction: LeftToRight + Min Width: 100 + Max Width: 100 + Min Height: - + Max Height: - + + 🔥 Customer card 🌟 + ------------------- + Available space: (Width: 100.000, Height: 728.614) + Requested space: Wrap + + 🔥 Constrained + -------------- + Available space: (Width: 100.000, Height: 728.614) + Requested space: Wrap + Content Direction: LeftToRight + Min Width: 150 + Max Width: 150 + Min Height: - + Max Height: - +``` + +How to read it: + +- **Each block is one Measure call** on one element, indented under its parent. The title is the internal element type (`Column`, `Padding`, `Constrained`, `Decoration`, `TextBlock`, ...); a block can appear more than once when a parent measures a child several times. +- **Available space** is what the parent offered, **Requested space** is what the element answered: `FullRender` or `PartialRender` with a size, or `Wrap`. +- **The flame marker** (🔥) marks every element that did not answer `FullRender`. Follow the chain of flames from the root downwards. The culprit is usually the deepest flame whose own children fit, or whose configuration cannot be met by its available space. Here, the innermost `Constrained` needs a width of 150 but only gets 100. +- **The star marker** (🌟) marks a `DebugPointer`, shown with its name instead of a type name. The page itself adds pointers named "Page header", "Page content", "Page footer", "Page background layer" and "Page foreground layer", so you can always tell which page area failed. +- **Configuration lines** below the sizes are the element's settings, for example `Min Width` and `Max Width` for size constraints (`Width`, `MinWidth`, `MaxWidth`, `Height`, ...), the four sides of a `Padding`, or `Extend Vertical` for the page content area. +- Numbers are formatted with the current culture, so the separators may look different on your machine. +- The trace only covers the last page that was attempted. If the exception is caused by the page limit, the trace shows the measurement of that last page. + +Some internal elements appear in every trace and can usually be skipped: the root `Column` (measured with unlimited space, shown as 14,400 points), `DebuggingProxy`, `ContentDirectionSetter`, `Background`, `Layers`, `DefaultTextStyle`, `Decoration` and the two `Extend` elements of the page content. The outermost `Constrained` and `Padding` are the page size and the page margins: in the example, an A4 page of 595.4 x 842 points minus 2 cm margins leaves 482 x 728.6 points for the page content. + +## Mark your own elements with DebugPointer + +In a large document, a trace full of `Column`, `Padding` and `TextBlock` entries is hard to map back to your code. `DebugPointer(string)` inserts an invisible marker that appears in the trace under the name you give it, highlighted with the star marker. It does not change the layout or the output: + +```csharp +container.Column(column => +{ + column.Item() + .DebugPointer("Invoice header") + .Text("Invoice #1234"); + + column.Item() + .DebugPointer("Line items table") + .Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(); + columns.ConstantColumn(80); + }); + + table.Cell().Text("Item"); + table.Cell().AlignRight().Text("Price"); + }); +}); +``` + +When a debugger is attached, each `IComponent` used with `container.Component(...)` is also added to the trace under its class name (without the star marker), so well-named components make traces easier to read. See [Build reusable components](reusable-components.md). + +## Common causes and how to fix them + +### A fixed size larger than the available space + +`Width`, `Height`, `MinWidth` and `MinHeight` are hard requirements. If the parent offers less, the element reports `Wrap`, and if even a new page does not offer enough, layout fails. The trace shows a `Constrained` element whose `Min Width` or `Min Height` is larger than its available space. + +Remember that margins, padding, borders, the header and the footer all reduce the space. A `Height(800)` does not fit on an A4 page (842 points tall) with 2 cm margins. + +Fixes: use `MaxHeight` or `MaxWidth` when you mean "at most", let the content decide its size, or scale the content down: + +```csharp +container + .MaxHeight(300) + .ScaleToFit() + .Text(Placeholders.Paragraphs()); +``` + +### Images that are too tall + +An image keeps its aspect ratio. With the default `ImageScaling.FitWidth`, its height follows from the available width, and a tall image on a wide page can be higher than the whole content area. Limit the height and fit the image into the resulting area: + +```csharp +container + .Height(250) + .Image(Placeholders.Image(400, 1200), ImageScaling.FitArea); +``` + +### ShowEntire on content that is longer than a page + +`ShowEntire()` prevents splitting: if its content does not fit completely, it moves to the next page. If the content is longer than a whole page, it cannot fit anywhere and layout fails. Use `EnsureSpace` instead, which only moves the content when little space is left and otherwise lets it split: + +```csharp +container.Column(column => +{ + foreach (var index in Enumerable.Range(1, 5)) + { + column.Item() + .EnsureSpace(120) + .Column(section => + { + section.Item().Text($"Section {index}").Bold(); + section.Item().Text(Placeholders.Paragraphs()); + }); + } +}); +``` + +Keep `ShowEntire()` for small blocks that must stay together, such as a signature block or a heading with its first paragraph. + +### Elements that cannot be split + +Images, canvases, lines, `ScaleToFit` and the content of a dynamic component are always drawn in one piece. The same applies to the page header and footer and to table headers and footers, which must fit completely on every page. If they get too large, every page fails: + +- Keep headers and footers small. Content that belongs only to the first page goes into the page content, or into the header with `ShowOnce()`. +- A long table header (for example, several rows of column titles) reduces the space for rows on every page. +- A dynamic component must return content that fits into `context.AvailableSize`; otherwise it throws `DocumentLayoutException` with "Dynamic component generated content that does not fit on a single page." + +### Rows wider than the page + +The width of a `Row` is the sum of its `ConstantItem` widths, its `AutoItem` widths and the spacing. If that is wider than the available width, the row reports `Wrap`. Prefer `RelativeItem` for at least one column so the row can adapt: + +```csharp +container.Row(row => +{ + row.Spacing(10); + row.ConstantItem(120).Text("Label"); + row.RelativeItem().Text(Placeholders.Sentence()); +}); +``` + +### Very long documents + +If the trace shows nothing unusual, the document may simply be longer than `Settings.DocumentLayoutExceptionThreshold` (250 pages by default). The limit has to be higher than your longest document, because reaching it already fails: + +```csharp +using ShinyPDF; + +Settings.DocumentLayoutExceptionThreshold = 5000; +``` + +A dynamic component that always returns `HasMoreContent = true` also produces pages until this limit is reached. + +### Missing glyphs and the font fallback exception + +When `Settings.CheckIfAllTextGlyphsAreAvailable` is `true` (the default when a debugger is attached), text containing a character that neither the font nor any fallback font provides throws a `DocumentDrawingException`: + +```text +Could not find an appropriate font fallback for glyph: U-4E16 '世'. Font families available on current environment that contain this glyph: ... +``` + +The message lists the registered font families that contain the character. Fix it by using one of them as the main font or by adding it as a fallback: + +```csharp +container + .Text("Hello 世界") + .FontFamily(Fonts.Lato) + .Fallback(x => x.FontFamily("Noto Sans JP")); +``` + +The font has to be registered first; see [Use custom fonts](fonts.md). When the check is off, missing characters are silently rendered as placeholder glyphs, so it is worth enabling it in your tests: + +```csharp +using ShinyPDF; + +Settings.CheckIfAllTextGlyphsAreAvailable = true; +``` + +### DocumentComposeException: multiple children in one container + +Most containers hold exactly one child. Calling two content methods on the same container throws a `DocumentComposeException` ("You should not assign multiple child elements to a single-child container"): + +```csharp +// not compiled: this throws DocumentComposeException +container.Text("First"); +container.Text("Second"); +``` + +Use a `Column` (or `Row`, `Layers`, `Table`) when you need several children: + +```csharp +container.Column(column => +{ + column.Item().Text("First"); + column.Item().Text("Second"); +}); +``` + +The same error appears when a container captured in a lambda is used after its scope, or when one container is filled inside a loop. + +## Debugging settings at a glance + +All settings are static properties of `ShinyPDF.Settings` and apply to every document generated afterwards: + +| Setting | Default | Effect | +|---|---|---| +| `EnableDebugging` | `true` only with a debugger attached | Fills `DocumentLayoutException.ElementTrace`; slower | +| `EnableCaching` | `false` with a debugger attached, otherwise `true` | Caches measurements; faster, does not change the output | +| `CheckIfAllTextGlyphsAreAvailable` | `true` only with a debugger attached | Throws on characters that no configured font contains | +| `DocumentLayoutExceptionThreshold` | `250` | Maximum page count before a `DocumentLayoutException` | + +Because three of these defaults depend on whether a debugger is attached, a document can fail in a test run but show a placeholder glyph in production (or the other way round). Set the values explicitly where consistent behavior matters. diff --git a/docs/how-to/deploy-on-linux.md b/docs/how-to/deploy-on-linux.md new file mode 100644 index 000000000..9ba8ce355 --- /dev/null +++ b/docs/how-to/deploy-on-linux.md @@ -0,0 +1,114 @@ +# Deploy on Linux and Docker + +ShinyPDF renders with SkiaSharp and HarfBuzzSharp, which call into native libraries. On Windows and macOS these native libraries are part of the package dependencies, but on Linux you have to add them yourself, and the machine or container needs a few system libraries. This guide lists what you need and gives an example Dockerfile. + +## Add the Linux native assets + +Since version 2.0.0, the ShinyPDF package does not bring the Linux native libraries for SkiaSharp and HarfBuzzSharp. Add both native asset packages to the project that runs on Linux (your application, not a class library in between): + +```bash +dotnet add package SkiaSharp.NativeAssets.Linux +dotnet add package HarfBuzzSharp.NativeAssets.Linux +``` + +Without them, the application compiles and starts, but fails as soon as it renders the first document: + +```text +System.DllNotFoundException: Unable to load shared library 'libSkiaSharp' or one of its dependencies. +``` + +Depending on where the error occurs, it can also arrive wrapped in a ShinyPDF `InitializationException` ("Cannot create the PDF document using the SkiaSharp library") with the `DllNotFoundException` as the inner exception. The same applies to `libHarfBuzzSharp`. + +### Use matching versions + +The native assets must match the managed SkiaSharp and HarfBuzzSharp versions that ShinyPDF uses. Check which versions your project resolved: + +```bash +dotnet list package --include-transitive +``` + +Look for `SkiaSharp` and `HarfBuzzSharp` in the transitive packages and add the native assets with the same versions. For the current ShinyPDF source, these are: + +```xml + + + + + +``` + +Replace `*` with the ShinyPDF version you use, and update the native asset versions whenever a ShinyPDF update changes the SkiaSharp or HarfBuzzSharp version. A mismatch can lead to errors at runtime that are hard to diagnose. + +The packages only add Linux binaries, so you can keep them in the project unconditionally even if you also develop on Windows or macOS. + +## Install the native system libraries + +SkiaSharp's Linux binary depends on system libraries for fonts and image formats. The ShinyPDF repository develops and tests in a container based on `mcr.microsoft.com/dotnet/sdk:10.0` (Ubuntu 24.04) with these packages (see `.devcontainer/devcontainer.json`): + +```bash +apt-get update +apt-get install -y libfontconfig1 libfreetype6 libpng16-16t64 libharfbuzz0b libjpeg-turbo8 libgif7 libwebp7 +``` + +| Package | Purpose | +| --- | --- | +| `libfontconfig1` | Font configuration and lookup of system fonts. `libSkiaSharp` links against it. | +| `libfreetype6` | Font rasterization. | +| `libharfbuzz0b` | Text shaping. | +| `libpng16-16t64`, `libjpeg-turbo8`, `libgif7`, `libwebp7` | PNG, JPEG, GIF and WebP image codecs. | + +> **Note:** This is the set the repository uses; it has not been reduced to a verified minimum. The package names are the Ubuntu 24.04 names. On other distributions or versions, the names can differ (for example `libpng16-16` instead of `libpng16-16t64` on older releases); install the equivalent packages. + +## Fonts on Linux + +ShinyPDF embeds its default fonts in the library and registers them automatically the first time it renders text, so they work in any container without installed system fonts: + +- `Fonts.Lato` (`"Lato"`), the default font of every text, +- `Fonts.NotoSans` (`"Noto Sans"`), including condensed variants and Noto Color Emoji, +- `Fonts.Courier` (`"Courier"`). + +If your document uses any other font family, ShinyPDF first looks at the fonts you registered with `FontManager` and then asks the operating system through fontconfig. Minimal Linux images contain few or no fonts, so a family that works on your Windows machine (for example `"Arial"`) may be missing in the container. Generation then fails with an `ArgumentException` ("The typeface '...' could not be found") or, if the system substitutes a font, renders with different metrics than on your development machine. + +To get identical output everywhere, ship the font files with your application and register them at startup instead of relying on system fonts. [Use custom fonts](fonts.md) shows how. + +Characters that the chosen font does not contain are another common source of differences between machines. Enable `Settings.CheckIfAllTextGlyphsAreAvailable` in your tests to catch them (see [Generate PDF, XPS and images](generate-output.md#global-settings)). + +## Example Dockerfile + +The following Dockerfile is an **example to adapt**, not a tested production setup. It builds a console or web application called `MyApp` in a multi-stage build and installs the system libraries listed above in the runtime image. + +```dockerfile +# Example only: adapt project names, base image and packages to your application. +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY . . +RUN dotnet publish MyApp/MyApp.csproj -c Release -o /app + +FROM mcr.microsoft.com/dotnet/runtime:10.0 +# Use mcr.microsoft.com/dotnet/aspnet:10.0 instead for an ASP.NET Core application. + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + libfontconfig1 libfreetype6 libpng16-16t64 libharfbuzz0b libjpeg-turbo8 libgif7 libwebp7 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY --from=build /app . +ENTRYPOINT ["dotnet", "MyApp.dll"] +``` + +Points to check when you adapt it: + +- `MyApp.csproj` must reference `SkiaSharp.NativeAssets.Linux` and `HarfBuzzSharp.NativeAssets.Linux`. After `dotnet publish`, the output contains `runtimes/linux-x64/native/libSkiaSharp.so` (or the folder for your architecture). +- The package names match the Ubuntu 24.04 based .NET 10 images. If you switch to a different base image (for example a Debian, Alpine or "chiseled" variant), check the package names and whether a package manager is available at all. +- If your documents use fonts other than the embedded ones, copy the font files into the image (or embed them in your assembly) and register them as described in [Use custom fonts](fonts.md). + +## Troubleshooting + +| Symptom | Likely cause | +| --- | --- | +| `DllNotFoundException: Unable to load shared library 'libSkiaSharp'` | `SkiaSharp.NativeAssets.Linux` is not referenced by the application project, or a system library that `libSkiaSharp` needs (such as `libfontconfig1`) is missing. | +| `DllNotFoundException` for `libHarfBuzzSharp` | `HarfBuzzSharp.NativeAssets.Linux` is not referenced. | +| `InitializationException` when generating a PDF | One of the two causes above; read the inner exception for the library name. | +| `ArgumentException: The typeface '...' could not be found` | The font is neither registered with `FontManager` nor installed in the container. | +| Text looks different than on Windows | A system font was substituted; register the font files yourself. | diff --git a/docs/how-to/fonts.md b/docs/how-to/fonts.md new file mode 100644 index 000000000..9636f2f05 --- /dev/null +++ b/docs/how-to/fonts.md @@ -0,0 +1,232 @@ +# Use custom fonts + +This guide shows how to register your own font files with ShinyPDF, use them in text styles, configure font fallback for scripts and symbols your main font does not cover (CJK, Arabic, emoji), and lay out right-to-left text. It also lists the fonts that ship inside the library. For the full list of text style options, see the [Text reference](../reference/text.md). + +## Built-in fonts + +The ShinyPDF assembly embeds these fonts and registers them automatically on first use, so they work on every platform without installation (all under the SIL Open Font License): + +| Family name | Constant | Styles | +|---|---|---| +| `Lato` | `Fonts.Lato` | Light, Regular, Bold, Black, each with italic. The library default font. | +| `Lato Hairline` | n/a | Lato Thin and Thin Italic register under this separate family name. | +| `Courier` | `Fonts.Courier` | Regular, Bold, Italic. | +| `Courier BoldItalic` | n/a | Courier Bold Italic registers under this separate family name. | +| `Noto Sans` | `Fonts.NotoSans` | Thin to Black (100 to 900), each with italic. | +| `Noto Sans SemiCondensed`, `Noto Sans Condensed`, `Noto Sans ExtraCondensed` | n/a | Narrower widths of Noto Sans, Thin to Black with italic. | +| `Noto Color Emoji` | n/a | Color emoji. The library default fallback font. | + +The constants live in `ShinyPDF.Helpers.Fonts`. Fonts installed on the operating system can also be used by their family name, but they are not available on every machine (see [Deploy on Linux](deploy-on-linux.md)). Prefer registering the font files you need, as shown below, so output does not depend on the environment. + +```csharp +container.Column(column => +{ + column.Item().Text("Lato (default)"); + column.Item().Text("Noto Sans").FontFamily(Fonts.NotoSans); + column.Item().Text("Noto Sans Condensed").FontFamily("Noto Sans Condensed"); + column.Item().Text("Courier").FontFamily(Fonts.Courier); +}); +``` + +## Registering fonts + +`FontManager` (namespace `ShinyPDF.Drawing`) is a static class. Register fonts once at application startup, before the first document is generated. Registration is global for the process. + +| Method | Description | +|---|---| +| `RegisterFont(Stream stream)` | Registers the font under the family name stored in the font file. | +| `RegisterFontWithCustomName(string fontName, Stream stream)` | Registers the font under its own family name and additionally under `fontName`. | +| `RegisterFontFromEmbeddedResource(string pathName)` | Loads an embedded resource from the calling assembly and registers it like `RegisterFont`. Throws `ArgumentException` if the resource does not exist. | +| `GetRegisteredFontFamilies()` | Returns the family names of installed system fonts plus all registered fonts. | + +TrueType (`.ttf`) and OpenType (`.otf`) files work, and font collections (`.ttc`) register every face they contain. The font data is copied during registration, so you can dispose the stream afterwards. Register every style file of a family (regular, bold, italic and so on) with the same family name; ShinyPDF picks the right file per span (see [Font weight and italic matching](#font-weight-and-italic-matching)). + +### From a file + +There is no overload that takes a path: open a stream. + +```csharp +using ShinyPDF.Drawing; + +using (var regular = File.OpenRead("fonts/NotoSansJP-Regular.ttf")) + FontManager.RegisterFont(regular); + +using (var arabic = File.OpenRead("fonts/NotoSansArabic-Regular.ttf")) + FontManager.RegisterFont(arabic); +``` + +Make sure the font files are copied to the output directory, for example with `` in your project file. + +### From an embedded resource + +Mark the files as embedded resources in your project file: + +```xml + + + +``` + +The resource name is the assembly's default namespace followed by the folder path and file name, separated by dots (for a project `MyApp` with `Fonts\Barlow-Regular.ttf`, it is `MyApp.Fonts.Barlow-Regular.ttf`). The method searches the assembly that calls it, so call it from the assembly that contains the resource, not from a helper in another assembly. + +```csharp +using ShinyPDF.Drawing; + +FontManager.RegisterFontFromEmbeddedResource("MyApp.Fonts.Barlow-Regular.ttf"); +FontManager.RegisterFontFromEmbeddedResource("MyApp.Fonts.Barlow-Bold.ttf"); +``` + +### From any stream with a custom name + +Use `RegisterFontWithCustomName` when the family name inside the file is inconvenient, or to load a font from a database or HTTP response. The font stays available under its original family name as well. + +```csharp +using ShinyPDF.Drawing; + +var fontBytes = File.ReadAllBytes("fonts/LibreBarcode39-Regular.ttf"); + +using (var stream = new MemoryStream(fontBytes)) + FontManager.RegisterFontWithCustomName("Barcode", stream); +``` + +## Using a registered font + +Refer to the font by its family name with `FontFamily`, either on a span or in a default text style. To find the exact family name of a file, register it and inspect `FontManager.GetRegisteredFontFamilies()`. + +```csharp +using ShinyPDF.Drawing; + +FontManager.RegisterFont(File.OpenRead("fonts/LibreBarcode39-Regular.ttf")); + +Document.Create(document => +{ + document.Page(page => + { + page.DefaultTextStyle(x => x.FontFamily(Fonts.NotoSans).FontSize(11)); + + page.Content().Column(column => + { + column.Item().Text("Body text in Noto Sans"); + column.Item().Text("*SHINYPDF*").FontFamily("Libre Barcode 39").FontSize(48); + }); + }); +}).GeneratePdf("fonts.pdf"); +``` + +If a family name is neither registered nor installed, document generation throws an `ArgumentException` ("The typeface '...' could not be found") that lists all available family names. + +## Font weight and italic matching + +Each span requests a weight (`Thin()` to `ExtraBlack()`, see the [Text reference](../reference/text.md#font-weight)) and a slant (`Italic()`). ShinyPDF picks the best file of the family with the CSS font matching rules: + +1. Slant first: italic falls back to oblique, then upright; upright falls back to oblique, then italic. +2. Weight second: for a requested 400 or 500, weights in 400 to 500 are preferred. Below that, the nearest lighter weight is preferred; above, the nearest heavier one. If none exists in that direction, the closest weight in the other direction is used. + +There is no synthetic bold or italic: if a family has only a regular file, bold text renders in regular. Register a file for every weight and style you use. Two consequences for the built-in fonts: + +- Lato has no Medium or SemiBold file: `Medium()` renders as Regular and `SemiBold()` as Bold. `Thin()` and `ExtraLight()` render as Light, unless you use the `Lato Hairline` family. +- `Courier` has no bold italic file in its family, so `Bold().Italic()` renders as regular italic (slant wins over weight). Use the `Courier BoldItalic` family instead. + +Subscript and superscript spans request one weight step heavier (+100) to keep strokes as thick as the surrounding text. + +## Font fallback + +A single font rarely covers every script. With `Fallback` you define a chain of styles; for each character ShinyPDF uses the first style in the chain whose font contains the glyph: + +1. The span's font. +2. Its `Fallback` style, then that style's own `Fallback`, and so on. +3. The library default style: Lato, then Noto Color Emoji. + +Because of step 3, emoji work out of the box. For other scripts, register a suitable font and add it as a fallback, usually in the page's default text style: + +```csharp +using ShinyPDF.Drawing; + +FontManager.RegisterFont(File.OpenRead("fonts/NotoSansJP-Regular.ttf")); +FontManager.RegisterFont(File.OpenRead("fonts/NotoSansArabic-Regular.ttf")); + +Document.Create(document => +{ + document.Page(page => + { + page.DefaultTextStyle(x => x + .FontFamily(Fonts.Lato) + .Fallback(y => y + .FontFamily("Noto Sans JP") + .Fallback(z => z.FontFamily("Noto Sans Arabic")))); + + page.Content().Text(text => + { + text.Line("Latin text in Lato."); + text.Line("Mixed line: This 中文 is 文本 mixed."); + text.Line("Arabic: خوارزمية ترتيب"); + text.Line("Emoji need no setup: 😊👍"); + }); + }); +}).GeneratePdf("fallback.pdf"); +``` + +A fallback style can set other properties too, but properties set on the main style take precedence: the fallback keeps its own font family, and its other properties only apply when nothing in the main style chain sets them. For example, if the fallback sets `Underline()` and a span sets `Underline(false)`, the fallback characters in that span are not underlined. Details are in [Style inheritance](../reference/text.md#style-inheritance). + +> **Note:** Configure fallbacks on a `TextStyle` (in `DefaultTextStyle` or with `Style(...)`), not with the span method `Fallback(...)`, which currently modifies a shared style instance. See the note in the [Text reference](../reference/text.md#font-fallback). + +### Detecting missing glyphs + +`Settings.CheckIfAllTextGlyphsAreAvailable` (namespace `ShinyPDF`) controls what happens when no font in the chain contains a character: + +- `true`: generation throws a `DocumentDrawingException` (namespace `ShinyPDF.Drawing.Exceptions`). The message names the character and its code point, for example `U-4E2D '中'`, and lists the installed or registered font families that contain it. +- `false`: the first font of the chain is used and the character renders as that font's placeholder glyph (often an empty box), without any warning. + +The default is `true` only when a debugger is attached, so you see the problem during development and production runs are not interrupted. Set it explicitly if you want the same behavior everywhere, for example in tests: + +```csharp +ShinyPDF.Settings.CheckIfAllTextGlyphsAreAvailable = true; +``` + +Page numbers (`CurrentPageNumber()` and the other page number members) are not checked and do not use fallback. + +## Right-to-left text + +Arabic, Hebrew and other right-to-left scripts need two things: a font that contains the script, and the right direction. + +- **Font:** register a font such as Noto Sans Arabic and use it as the family or as a fallback. None of the built-in fonts contain Arabic or Hebrew glyphs. +- **Shaping direction:** by default (`DirectionAuto()`) the direction of each span is detected from its content, and HarfBuzz shapes the glyphs (ligatures, contextual forms). Force it with `DirectionFromRightToLeft()` or `DirectionFromLeftToRight()`. +- **Layout direction:** `ContentFromRightToLeft()` on a page or container makes text align right by default and mirrors layouts such as rows. The text direction alone does not change alignment. + +To mix directions in one paragraph, put each part in its own span; each span is then shaped with its own direction. + +```csharp +using ShinyPDF.Drawing; + +FontManager.RegisterFont(File.OpenRead("fonts/NotoSansArabic-Regular.ttf")); + +Document.Create(document => +{ + document.Page(page => + { + page.ContentFromRightToLeft(); + page.DefaultTextStyle(x => x + .FontSize(14) + .FontFamily(Fonts.Lato) + .Fallback(y => y.FontFamily("Noto Sans Arabic"))); + + page.Content().Column(column => + { + column.Item().Text("في المعلوماتية أو الرياضيات، خوارزمية الترتيب هي خوارزمية تمكن من تنظيم مجموعة عناصر حسب ترتيب محدد."); + + column.Item().Text(text => + { + text.Span("الجوريتم"); + text.Span(" - "); + text.Span("algorithm in Arabic").DirectionFromLeftToRight(); + }); + }); + }); +}).GeneratePdf("rtl.pdf"); +``` + +## See also + +- [Text reference](../reference/text.md): every text and style option. +- [Deploy on Linux](deploy-on-linux.md): native dependencies and system fonts in containers. +- [Debug layout issues](debug-layout-issues.md) diff --git a/docs/how-to/generate-output.md b/docs/how-to/generate-output.md new file mode 100644 index 000000000..1ab826825 --- /dev/null +++ b/docs/how-to/generate-output.md @@ -0,0 +1,271 @@ +# Generate PDF, XPS and images + +This guide shows how to turn a ShinyPDF document into output: a PDF file, a byte array or a stream, an XPS document, or one PNG image per page. It also covers document metadata, the global `Settings` that influence generation, and how to move a document into its own class by implementing `IDocument`. + +All generation methods are extension methods on `IDocument` in the `ShinyPDF.Fluent` namespace. `Document.Create(...)` returns a `Document`, which implements `IDocument`, so you can call them directly on it. + +## Generate a PDF file + +Pass a file path to `GeneratePdf`. An existing file is overwritten. + +```csharp +using ShinyPDF.Fluent; +using ShinyPDF.Helpers; +using ShinyPDF.Infrastructure; + +var document = Document.Create(container => +{ + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(2, Unit.Centimetre); + page.Content().Text("Hello PDF!"); + }); +}); + +document.GeneratePdf("hello.pdf"); +``` + +A `Document` only stores the description of your content. Every call to a `Generate*` method runs the layout and rendering again, so you can generate the same document several times, for example as PDF and as images. + +## Generate a PDF in memory or into a stream + +Without arguments, `GeneratePdf` returns the PDF as a `byte[]`: + +```csharp +var document = Document.Create(container => +{ + container.Page(page => page.Content().Text("Hello PDF!")); +}); + +byte[] pdf = document.GeneratePdf(); +``` + +To write into a stream you own, pass it to `GeneratePdf(Stream)`: + +```csharp +var document = Document.Create(container => +{ + container.Page(page => page.Content().Text("Hello PDF!")); +}); + +using var stream = new MemoryStream(); +document.GeneratePdf(stream); +stream.Position = 0; +``` + +The stream must support writing and seeking (`CanWrite` and `CanSeek`); otherwise `GeneratePdf` throws an `ArgumentException`. `MemoryStream` and `FileStream` work. Network streams such as an ASP.NET Core response body do not support seeking, so generate into a `byte[]` or a `MemoryStream` first and then write that to the response. ShinyPDF does not dispose or rewind the stream; set `Position = 0` before you read it back. + +For example, in an ASP.NET Core minimal API you can return the byte array as a file result (this snippet needs the ASP.NET Core shared framework, so it is not compiled here): + +```csharp +// not compiled: requires an ASP.NET Core project +app.MapGet("/invoice.pdf", () => +{ + var pdf = Document.Create(container => + { + container.Page(page => page.Content().Text("Hello PDF!")); + }) + .GeneratePdf(); + + return Results.File(pdf, "application/pdf", "invoice.pdf"); +}); +``` + +## Generate XPS + +XPS output uses the same pattern with `GenerateXps`: a file path, a `Stream` (writable and seekable), or no argument to get a `byte[]`. + +```csharp +var document = Document.Create(container => +{ + container.Page(page => page.Content().Text("Hello XPS!")); +}); + +document.GenerateXps("hello.xps"); +``` + +> **Note:** ShinyPDF creates XPS documents through SkiaSharp's XPS backend, which is only available on Windows. On Linux, `GenerateXps` currently fails with a `NullReferenceException` instead of a descriptive error. Generate XPS on Windows; on Linux and macOS use PDF. + +## Generate images + +`GenerateImages` renders every page as a PNG image. Without arguments it returns one `byte[]` per page: + +```csharp +var document = Document.Create(container => +{ + container.Page(page => page.Content().Text("Hello images!")); +}); + +IEnumerable pages = document.GenerateImages(); +``` + +To write the images to files, pass a function that maps the page index to a file name. The index starts at 0 for the first page. Existing files are overwritten. + +```csharp +var document = Document.Create(container => +{ + container.Page(page => page.Content().Text("Hello images!")); +}); + +document.GenerateImages(index => $"page-{index + 1}.png"); +``` + +This writes `page-1.png`, `page-2.png` and so on. The image resolution comes from `DocumentMetadata.RasterDpi` (see the next section). The default of 72 DPI produces one pixel per point, so an A4 page becomes an image of about 595 by 842 pixels. For sharper images, raise `RasterDpi`, for example to 150 or 300. + +## Set document metadata + +`DocumentMetadata` (namespace `ShinyPDF.Drawing`) holds the PDF metadata and a few rendering options. Attach it with `WithMetadata`: + +```csharp +using ShinyPDF.Drawing; + +var metadata = new DocumentMetadata +{ + Title = "Invoice 2026-0042", + Author = "Shiny Consulting GmbH", + Subject = "Invoice", + Keywords = "invoice, consulting", + Creator = "InvoiceDemo", + Producer = "ShinyPDF", + CreationDate = DateTime.Now, + ModifiedDate = DateTime.Now, + RasterDpi = 150, +}; + +Document + .Create(container => + { + container.Page(page => page.Content().Text("Hello PDF!")); + }) + .WithMetadata(metadata) + .GeneratePdf("invoice.pdf"); +``` + +The available properties: + +| Property | Default | Meaning | +| --- | --- | --- | +| `Title`, `Author`, `Subject`, `Keywords`, `Creator`, `Producer` | `null` | Text fields written to the PDF document information. | +| `CreationDate`, `ModifiedDate` | `DateTime.Now` when the metadata object is created | Dates written to the PDF. | +| `RasterDpi` | `72` | Resolution for content that has to be rasterized in PDF and XPS output, and the resolution of images from `GenerateImages`. | +| `ImageQuality` | `101` | Encoding quality for images in the PDF, passed to SkiaSharp. 101 means lossless. With a value from 0 to 100, SkiaSharp may encode opaque images as JPEG with that quality, which makes the file smaller. | +| `PdfA` | `false` | Asks SkiaSharp to add the metadata that PDF/A requires (such as XMP metadata and an output intent). | + +> **Note:** `PdfA = true` only adds metadata. It does not check that your content (for example fonts or transparency) conforms to PDF/A. Validate the output with a PDF/A validator if you need certified conformance. + +`DocumentMetadata.Default` returns a new instance with the default values. Passing `null` to `WithMetadata` keeps the current metadata. + +## Implement IDocument as a class + +For anything beyond a small script, put your document into its own class. Implement `IDocument` with two methods: `GetMetadata()` returns the metadata, and `Compose(IDocumentContainer)` adds the pages. The data the document needs comes in through the constructor. + +```csharp +using ShinyPDF.Drawing; +using ShinyPDF.Fluent; +using ShinyPDF.Helpers; +using ShinyPDF.Infrastructure; + +public record InvoiceLine(string Description, decimal Amount); + +public class InvoiceDocument : IDocument +{ + private readonly string _number; + private readonly IReadOnlyList _lines; + + public InvoiceDocument(string number, IReadOnlyList lines) + { + _number = number; + _lines = lines; + } + + public DocumentMetadata GetMetadata() => new DocumentMetadata + { + Title = $"Invoice {_number}", + Author = "Shiny Consulting GmbH", + }; + + public void Compose(IDocumentContainer container) + { + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(2, Unit.Centimetre); + page.DefaultTextStyle(x => x.FontSize(11)); + + page.Header().Text($"Invoice {_number}").FontSize(20).SemiBold(); + page.Content().PaddingVertical(10).Element(ComposeLines); + page.Footer().AlignCenter().Text(text => + { + text.CurrentPageNumber(); + text.Span(" / "); + text.TotalPages(); + }); + }); + } + + private void ComposeLines(IContainer container) + { + container.Column(column => + { + column.Spacing(5); + + foreach (var line in _lines) + { + column.Item().Row(row => + { + row.RelativeItem().Text(line.Description); + row.AutoItem().Text($"{line.Amount:N2} EUR"); + }); + } + }); + } +} +``` + +Generate it like any other document: + +```csharp +// not compiled: uses the InvoiceDocument class from the previous block +var lines = new List +{ + new("Workshop", 1200m), + new("Template development", 1330m), +}; + +var document = new InvoiceDocument("2026-0042", lines); +document.GeneratePdf("invoice.pdf"); +``` + +`Document.Create` is a convenience implementation of the same interface: it stores your lambda and calls it from `Compose`. You can also split a class-based document into smaller parts with `IComponent`; see [Build reusable components](reusable-components.md). + +## Global settings + +The static class `ShinyPDF.Settings` contains process-wide switches. Set them once at application startup, before you generate documents. They apply to every document generated afterwards. + +```csharp +using ShinyPDF; + +Settings.DocumentLayoutExceptionThreshold = 1000; +Settings.EnableCaching = true; +Settings.EnableDebugging = false; +Settings.CheckIfAllTextGlyphsAreAvailable = true; +``` + +| Setting | Default | What it does | +| --- | --- | --- | +| `DocumentLayoutExceptionThreshold` | `250` | Maximum number of pages ShinyPDF produces. When the document reaches this many pages, generation stops with a `DocumentLayoutException`. This protects you from layouts that would never finish, for example an element that never fits on a page. Raise it if your documents are legitimately longer. | +| `EnableCaching` | `true` when no debugger is attached | Caches layout measurements. This usually speeds up generation noticeably at the cost of slightly more memory. | +| `EnableDebugging` | `true` when a debugger is attached | Records extra layout information, so that a `DocumentLayoutException` carries an element trace (`ElementTrace`) that points to the problematic area. Costs performance. | +| `CheckIfAllTextGlyphsAreAvailable` | `true` when a debugger is attached | Checks that the selected font contains every character in your text. If not, generation fails with a `DocumentDrawingException` instead of silently drawing placeholder characters. Costs a little performance. | + +Because the debugging-related defaults depend on whether a debugger is attached, a document can behave differently under the debugger than in production: for example, a missing glyph throws while debugging but renders as a placeholder character in production. Set the flags explicitly if you want the same behavior everywhere. [Debug layout issues](debug-layout-issues.md) explains how to read layout exceptions and element traces, and [Use custom fonts](fonts.md) shows how to provide fonts for missing glyphs. + +## Errors during generation + +The exceptions ShinyPDF throws during generation live in the `ShinyPDF.Drawing.Exceptions` namespace: + +- `DocumentComposeException`: the document description is invalid, for example a table without column definitions. +- `DocumentLayoutException`: the content cannot be laid out, or it exceeds `DocumentLayoutExceptionThreshold` pages. With `EnableDebugging`, its `ElementTrace` property describes where the problem is. +- `DocumentDrawingException`: an error occurred while drawing a page, for example a missing glyph when `CheckIfAllTextGlyphsAreAvailable` is enabled. The original exception is the `InnerException`. +- `InitializationException`: SkiaSharp could not create the PDF or XPS document, usually because native libraries are missing. See [Deploy on Linux and Docker](deploy-on-linux.md). diff --git a/docs/how-to/headers-footers-page-numbers.md b/docs/how-to/headers-footers-page-numbers.md new file mode 100644 index 000000000..960f237f2 --- /dev/null +++ b/docs/how-to/headers-footers-page-numbers.md @@ -0,0 +1,265 @@ +# Headers, footers and page numbers + +This guide shows how to set up pages in ShinyPDF: page size and orientation, margins, the header, content and footer slots, background and foreground layers, and page numbers. It also shows how to give the first page a different header with `ShowOnce` and `SkipOnce`. + +## The page slots + +You configure a page inside `container.Page(page => ...)`. The `PageDescriptor` you receive has five slots, each returning an `IContainer` that takes one child element: + +- `page.Header()`: drawn at the top of every page. +- `page.Content()`: the main content. It flows from page to page; ShinyPDF adds pages until all content is placed. +- `page.Footer()`: drawn at the bottom of every page. +- `page.Background()`: a layer behind the page that covers the whole page, including the margins. +- `page.Foreground()`: a layer in front of the page, also covering the whole page. + +```csharp +using ShinyPDF.Fluent; +using ShinyPDF.Helpers; +using ShinyPDF.Infrastructure; + +Document.Create(container => +{ + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(2, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontSize(11)); + + page.Header() + .PaddingBottom(10) + .BorderBottom(1) + .BorderColor(Colors.Grey.Lighten1) + .Text("Quarterly report").FontSize(18).SemiBold(); + + page.Content() + .PaddingVertical(10) + .Text(Placeholders.Paragraphs()); + + page.Footer() + .AlignCenter() + .Text(text => + { + text.Span("Page "); + text.CurrentPageNumber(); + text.Span(" of "); + text.TotalPages(); + }); + }); +}) +.GeneratePdf("report.pdf"); +``` + +The header and footer take the height they need on each page; the content gets the space in between. Keep the header and footer small: if they leave no room for the content, ShinyPDF cannot place it and fails with a `DocumentLayoutException` (see [Debug layout issues](debug-layout-issues.md)). + +Every slot is optional. A slot you do not use stays empty. + +## Page size and orientation + +`page.Size(...)` sets a fixed page size. If you do not call it, pages are A4 portrait. + +```csharp +Document.Create(container => +{ + container.Page(page => + { + page.Size(PageSizes.Letter); + page.Content().Text("US Letter"); + }); +}); +``` + +- `PageSizes` (namespace `ShinyPDF.Helpers`) has predefined sizes: the ISO A and B series (`A0` to `A10`, `B0` to `B10`), the envelopes `Env10`, `EnvC4` and `EnvDL`, the North American sizes `Letter`, `Legal` and `Executive`, and the architectural sizes `ARCH_A` to `ARCH_E3`. All predefined sizes are portrait. +- `.Landscape()` and `.Portrait()` are extension methods on `PageSize` that swap width and height when needed: `PageSizes.A4.Landscape()`. +- `page.Size(width, height, unit)` sets a custom size, for example `page.Size(10, 15, Unit.Centimetre)`. You can also create a reusable `new PageSize(10, 15, Unit.Centimetre)`. + +```csharp +Document.Create(container => +{ + container.Page(page => + { + page.Size(PageSizes.A4.Landscape()); + page.Content().Text("A4 landscape"); + }); + + container.Page(page => + { + page.Size(105, 148, Unit.Millimetre); + page.Content().Text("Custom size"); + }); +}); +``` + +As the example shows, you can call `container.Page(...)` several times. Each call starts a new page set with its own size, margins and slots, and always begins on a new page. Page numbers continue across page sets, and `TotalPages()` counts all pages of the document. + +For special cases, `page.ContinuousSize(width, unit)` creates a single page with a fixed width whose height grows with the content (useful for receipts or long images), and `page.MinSize(...)` and `page.MaxSize(...)` let the page size vary between two limits. + +## Margins + +Margins are the space between the page edge and the header, content and footer. They default to 0. + +```csharp +Document.Create(container => +{ + container.Page(page => + { + page.MarginVertical(2, Unit.Centimetre); + page.MarginHorizontal(25, Unit.Millimetre); + + page.Content().Text(Placeholders.Paragraph()); + }); +}); +``` + +The available methods are `Margin` (all sides), `MarginVertical`, `MarginHorizontal`, `MarginTop`, `MarginBottom`, `MarginLeft` and `MarginRight`. All take a value and an optional `Unit` (`Unit.Point` by default; also `Millimetre`, `Centimetre`, `Meter`, `Inch`, `Feet` and `Mill`, a thousandth of an inch). Later calls override earlier ones for the same side, so `page.Margin(20); page.MarginTop(40);` gives a larger top margin. + +To add space only between the header and the content, use padding inside the slot, for example `page.Content().PaddingVertical(10)`. + +## Background and foreground + +`Background()` and `Foreground()` are drawn on every page and ignore the margins. Use them for watermarks, colored bands or decorative frames. + +```csharp +Document.Create(container => +{ + container.Page(page => + { + page.Margin(2, Unit.Centimetre); + + page.Background() + .AlignTop() + .ExtendHorizontal() + .Height(80) + .Background(Colors.Blue.Lighten4); + + page.Foreground() + .AlignMiddle() + .AlignCenter() + .Text("DRAFT").FontSize(72).Bold().FontColor("#33ff0000"); + + page.Content().Text(Placeholders.Paragraphs()); + }); +}); +``` + +Colors are hexadecimal strings; an 8-digit value such as `#33ff0000` includes an alpha channel, which makes the foreground text semi-transparent. For a plain page color, use `page.PageColor(...)` instead of the background slot. + +## Page numbers + +Page numbers are special text spans. Add them inside a `Text(text => ...)` block, usually in the footer or header: + +| Method | Value | +| --- | --- | +| `CurrentPageNumber()` | The number of the current page, starting at 1. | +| `TotalPages()` | The number of pages in the whole document. | + +```csharp +container.AlignRight().Text(text => +{ + text.DefaultTextStyle(x => x.FontSize(9).FontColor(Colors.Grey.Darken1)); + + text.Span("Page "); + text.CurrentPageNumber().SemiBold(); + text.Span(" / "); + text.TotalPages(); +}); +``` + +The page number methods return a `TextPageNumberDescriptor`, so you can style them like any other span. With `Format(...)` you control how the number is printed. The formatter receives the number as `int?` and returns a string: + +```csharp +container.Text(text => +{ + text.Span("Page "); + text.CurrentPageNumber().Format(number => number?.ToString("00") ?? "-"); +}); +``` + +### Page numbers per section + +For page numbers relative to a part of the document (for example "Chapter page 2 of 5", or a table of contents), mark that part with `Section(name)` and use the section variants: + +| Method | Value | +| --- | --- | +| `BeginPageNumberOfSection(name)` | The page on which the section starts. | +| `EndPageNumberOfSection(name)` | The page on which the section ends. | +| `PageNumberWithinSection(name)` | The current page number, counted from the start of the section. | +| `TotalPagesWithinSection(name)` | The number of pages the section spans. | + +```csharp +container.Column(column => +{ + column.Item().Text(text => + { + text.Span("Appendix starts on page "); + text.BeginPageNumberOfSection("appendix"); + }); + + column.Item().PageBreak(); + + column.Item().Section("appendix").Column(appendix => + { + appendix.Item().Text("Appendix").FontSize(18).SemiBold(); + appendix.Item().Text(Placeholders.Paragraphs()); + }); +}); +``` + +`Section` also makes the area a link target: `SectionLink(name)` on a container, or `text.SectionLink(text, name)` inside a text block, creates a link that jumps to it. Give each section its own name. Avoid the name `document`, which ShinyPDF uses internally for `TotalPages()`. + +## A different header on the first page + +A common requirement is a large header on the first page and a compact one on all following pages. Because the header is drawn on every page, you put both variants into it and use `ShowOnce` and `SkipOnce`: + +- `ShowOnce()` draws its content only the first time, on the first page where it appears. +- `SkipOnce()` hides its content the first time and draws it from the second time on. + +```csharp +Document.Create(container => +{ + container.Page(page => + { + page.Size(PageSizes.A5); + page.Margin(1, Unit.Centimetre); + page.PageColor(Colors.White); + + page.Header().Column(column => + { + column.Item().ShowOnce().Column(first => + { + first.Item().Text("Shiny Consulting GmbH").FontSize(24).SemiBold().FontColor(Colors.Blue.Darken2); + first.Item().Text("Project proposal, October 2026").FontColor(Colors.Grey.Darken1); + }); + + column.Item().SkipOnce().Text("Project proposal (continued)").FontSize(10).FontColor(Colors.Grey.Darken1); + }); + + page.Content().PaddingVertical(10).Column(column => + { + column.Spacing(10); + + foreach (var _ in Enumerable.Range(0, 10)) + column.Item().Text(Placeholders.Paragraph()); + }); + + page.Footer().AlignCenter().Text(text => + { + text.CurrentPageNumber(); + text.Span(" / "); + text.TotalPages(); + }); + }); +}) +.GeneratePdf("proposal.pdf"); +``` + +On page 1 the header shows the company name and subtitle; from page 2 on it shows the single "continued" line. The same technique works in the footer, for example to show terms and conditions only on the first page. + +`ShowOnce` and `SkipOnce` are not limited to headers. Inside the content, `ShowOnce` prevents an element from being repeated when its parent continues on the next page, for example a label in a row next to a long paragraph. + +## Related + +- [Getting started](../tutorials/getting-started.md) builds a complete invoice with header, table and footer. +- [Elements reference](../reference/elements.md) lists all layout elements, including `PageBreak`, `EnsureSpace` and `ShowEntire` for controlling page breaks. +- [Text reference](../reference/text.md) covers text styles and spans. +- [Layout engine](../explanation/layout-engine.md) explains how content is split across pages. diff --git a/docs/how-to/reusable-components.md b/docs/how-to/reusable-components.md new file mode 100644 index 000000000..6ff0ccba5 --- /dev/null +++ b/docs/how-to/reusable-components.md @@ -0,0 +1,456 @@ +# Build reusable components + +As documents grow, the same building blocks appear again and again: an address block, a styled table cell, a card with a border, a footer with page numbers. This guide shows the ways ShinyPDF lets you reuse layout code: extension methods and style functions for small pieces, `IComponent` classes for self-contained parts with their own data, `IDocument` classes for whole documents, and dynamic components (`IDynamicComponent`) for content that depends on the page number or on the space left on the page. All examples use only the public API. + +## Choose the right tool + +| You want to reuse | Use | +|---|---| +| A style or a chain of decorations (padding, border, background) | A function `IContainer -> IContainer` or an extension method on `IContainer` | +| A self-contained block with its own data (address, invoice header) | A class implementing `IComponent` | +| A whole document, for example one per report type | A class implementing `IDocument` | +| Content that changes per page or depends on the remaining space | A class implementing `IDynamicComponent` | + +The first three only run while the element tree is built, once per document generation. Dynamic components run during layout, see [How the layout engine works](../explanation/layout-engine.md). + +## Style functions with Element + +The lightest form of reuse is a plain function that takes a container, adds some elements, and returns the innermost container. Apply it with `Element(...)`, which accepts a `Func`: + +```csharp +container.Table(table => +{ + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(); + columns.ConstantColumn(80); + }); + + table.Cell().Element(HeaderCell).Text("Item"); + table.Cell().Element(HeaderCell).AlignRight().Text("Price"); + + table.Cell().Element(BodyCell).Text("Coffee"); + table.Cell().Element(BodyCell).AlignRight().Text("3.50"); + + static IContainer HeaderCell(IContainer cell) => cell + .DefaultTextStyle(x => x.SemiBold()) + .BorderBottom(1) + .BorderColor(Colors.Grey.Darken2) + .PaddingVertical(5); + + static IContainer BodyCell(IContainer cell) => cell + .BorderBottom(1) + .BorderColor(Colors.Grey.Lighten2) + .PaddingVertical(5); +}); +``` + +`Element` also has an overload taking an `Action`, for functions that fill the container completely instead of returning it. + +## Extension methods on IContainer + +When the same style is used across files, turn it into an extension method. It then reads like a built-in Fluent API method: + +```csharp +public static class LayoutExtensions +{ + // Returns the inner container, so the caller decides what goes inside. + public static IContainer Card(this IContainer container) + { + return container + .Border(1) + .BorderColor(Colors.Grey.Lighten1) + .Background(Colors.Grey.Lighten4) + .Padding(10); + } + + // Fills the container completely, so it returns nothing. + public static void SectionTitle(this IContainer container, string text) + { + container + .PaddingBottom(5) + .Text(text) + .FontSize(16) + .SemiBold(); + } +} + +public static class LayoutExtensionsUsage +{ + public static void Compose(IContainer container) + { + container.Column(column => + { + column.Spacing(10); + column.Item().SectionTitle("Summary"); + column.Item().Card().Text(Placeholders.Paragraph()); + }); + } +} +``` + +The convention from the built-in API applies here as well: return `IContainer` when the method only wraps content, return `void` when it produces the final content. + +## Components with IComponent + +`IComponent` has a single method, `void Compose(IContainer container)`. A component is an ordinary class, so it can take its data through the constructor or properties, hold helper methods, and be unit-tested in isolation. Insert it with `container.Component(...)`: + +```csharp +public record Address(string Name, string Street, string City, string Email); + +public class AddressComponent : IComponent +{ + private string Title { get; } + private Address Address { get; } + + public AddressComponent(string title, Address address) + { + Title = title; + Address = address; + } + + public void Compose(IContainer container) + { + container.Column(column => + { + column.Spacing(2); + + column.Item() + .BorderBottom(1) + .PaddingBottom(5) + .Text(Title) + .SemiBold(); + + column.Item().Text(Address.Name); + column.Item().Text(Address.Street); + column.Item().Text(Address.City); + column.Item().Text(Address.Email).FontColor(Colors.Blue.Medium); + }); + } +} + +public static class AddressComponentUsage +{ + public static void Compose(IContainer container, Address seller, Address customer) + { + container.Row(row => + { + row.RelativeItem().Component(new AddressComponent("From", seller)); + row.ConstantItem(50); + row.RelativeItem().Component(new AddressComponent("For", customer)); + }); + } +} +``` + +There are two public overloads: + +- `container.Component(T component)` takes an instance; use it when the component needs data. +- `container.Component()` creates the instance itself and requires a public parameterless constructor (`where T : IComponent, new()`). + +`Compose` is called once, immediately, when `Component(...)` runs, with a fresh container. Any loops and conditions inside it are evaluated at that moment, not per page. Like any container, the one passed to `Compose` accepts exactly one child, so use a `Column`, `Row` or similar when the component has several parts. + +When a debugger is attached, `Component(...)` also records the component's class name in the element trace of a `DocumentLayoutException`, which makes layout errors easier to locate. See [Debug layout issues](debug-layout-issues.md#mark-your-own-elements-with-debugpointer). + +### Passing content into a component + +To let the caller provide part of a component's content (for example the body of a card with a fixed frame and title), accept an `Action` and invoke it with the container where that content belongs: + +```csharp +public class TitledBox : IComponent +{ + private string Title { get; } + private Action Content { get; } + + public TitledBox(string title, Action content) + { + Title = title; + Content = content; + } + + public void Compose(IContainer container) + { + container + .Border(1) + .BorderColor(Colors.Grey.Medium) + .Column(column => + { + column.Item() + .Background(Colors.Grey.Lighten3) + .Padding(5) + .Text(Title) + .SemiBold(); + + column.Item().Padding(5).Element(Content); + }); + } +} + +public static class TitledBoxUsage +{ + public static void Compose(IContainer container) + { + container.Component(new TitledBox("Notes", content => + { + content.Text(Placeholders.Paragraph()); + })); + } +} +``` + +> **Note:** The ShinyPDF source contains an internal "slot" mechanism for components, inherited from QuestPDF. It is not part of the public API, so use delegates as shown above to pass content into a component. + +## Whole documents with IDocument + +`Document.Create(...)` is convenient for small documents. For documents with their own data model and metadata, implement `IDocument` instead. The generation methods (`GeneratePdf`, `GenerateXps`, `GenerateImages`) are extension methods on `IDocument`, so they work the same way: + +```csharp +using ShinyPDF.Drawing; + +public record InvoiceModel(string Number, IReadOnlyList Lines); + +public class InvoiceDocument : IDocument +{ + private InvoiceModel Model { get; } + + public InvoiceDocument(InvoiceModel model) + { + Model = model; + } + + public DocumentMetadata GetMetadata() + { + var metadata = DocumentMetadata.Default; + metadata.Title = $"Invoice {Model.Number}"; + return metadata; + } + + public void Compose(IDocumentContainer container) + { + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(2, Unit.Centimetre); + + page.Header().Text($"Invoice {Model.Number}").FontSize(24).SemiBold(); + + page.Content().PaddingVertical(10).Column(column => + { + foreach (var line in Model.Lines) + column.Item().Text(line); + }); + + page.Footer().AlignCenter().Text(text => + { + text.CurrentPageNumber(); + text.Span(" / "); + text.TotalPages(); + }); + }); + } +} + +public static class InvoiceDocumentUsage +{ + public static void Generate() + { + var model = new InvoiceModel("2026-001", new[] { "Consulting", "Support" }); + new InvoiceDocument(model).GeneratePdf("invoice.pdf"); + } +} +``` + +Inside `Compose`, split the page into components (`page.Header().Component(new InvoiceHeader(Model))`) to keep each part small. More on generation options is in [Generate PDF, XPS and images](generate-output.md). + +## Dynamic components + +A regular component is composed once, before layout starts, so it cannot know on which page it ends up or how much space is left. A dynamic component is asked for its content **during layout, on every page it appears on**. Use it for: + +- content that depends on the page number, for example a footer aligned left on even and right on odd pages, +- content that depends on the available space, for example a list that shows as many entries as fit and adds a per-page subtotal. + +For page numbers in text, the built-in `CurrentPageNumber()` and `TotalPages()` text elements are simpler; see [Headers, footers and page numbers](headers-footers-page-numbers.md). Reach for a dynamic component when the layout itself, not just a number, changes. + +### The contract + +A dynamic component implements `IDynamicComponent`: + +- `TState State { get; set; }` holds the component's progress between pages. `TState` must be a struct (`where TState : struct`). +- `DynamicComponentComposeResult Compose(DynamicContext context)` returns the content for the current page in `Content` and sets `HasMoreContent` to `true` if the component needs another page. + +`IDynamicComponent` and `DynamicComponentComposeResult` are in `ShinyPDF.Infrastructure`; `DynamicContext` and `IDynamicElement` are in `ShinyPDF.Elements`, so add `using ShinyPDF.Elements;`. + +`DynamicContext` provides: + +| Member | Description | +|---|---| +| `PageNumber` | The number of the current page. | +| `TotalPages` | The total number of pages. See the note below. | +| `AvailableSize` | The space offered to the component on this page. | +| `CreateElement(Action)` | Builds content with the Fluent API and returns an `IDynamicElement`, whose `Size` is its measured size. | + +Rules that follow from how the engine calls it: + +- **Keep all progress in `State`.** `Compose` is called several times per page: when the engine measures the page, its state changes are discarded and `State` is restored; only the call made while drawing keeps the new state. Fields other than `State` must not change in `Compose`. +- **The content must fit.** The returned content has to fit completely into `AvailableSize`. Otherwise the component throws a `DocumentLayoutException` ("Dynamic component generated content that does not fit on a single page."). Use `IDynamicElement.Size` to check before returning. +- **`CreateElement` measures with unlimited space.** Text inside it does not wrap at the page width unless you constrain the width, for example with `.Width(context.AvailableSize.Width)`; otherwise the reported `Size` is not what the element needs on the page. +- **State is reset per generation pass.** The engine lays out the document twice (see [The rendering pipeline](../explanation/layout-engine.md#the-rendering-pipeline)) and restores the state the component had when it was added before each pass. Set the initial state in the constructor. +- `HasMoreContent = true` forever produces pages until `Settings.DocumentLayoutExceptionThreshold` is reached. + +> **Note:** During the first layout pass, `TotalPages` is the number of pages laid out so far, not the final count. The final value is available in the second pass, which produces the output. Avoid making the size of the content depend on `TotalPages`, or the two passes may lay out pages differently. + +Content created with `CreateElement` inherits the text style and content direction from the place where the component is used. + +### Example: content that depends on the page number + +This footer alternates its alignment between pages. It keeps no progress, so it uses `int` as a dummy state: + +```csharp +using ShinyPDF.Elements; + +public class AlternatingFooter : IDynamicComponent +{ + public int State { get; set; } + + public DynamicComponentComposeResult Compose(DynamicContext context) + { + var content = context.CreateElement(container => + { + container + .Element(x => context.PageNumber % 2 == 0 ? x.AlignLeft() : x.AlignRight()) + .Text(text => + { + text.Span("Page "); + text.CurrentPageNumber(); + }); + }); + + return new DynamicComponentComposeResult + { + Content = content, + HasMoreContent = false + }; + } +} + +public static class AlternatingFooterUsage +{ + public static void Generate() + { + Document.Create(document => + { + document.Page(page => + { + page.Size(PageSizes.A5); + page.Margin(1, Unit.Centimetre); + + page.Content().Column(column => + { + foreach (var _ in Enumerable.Range(0, 20)) + column.Item().PaddingBottom(10).Text(Placeholders.Paragraph()); + }); + + page.Footer().Dynamic(new AlternatingFooter()); + }); + }) + .GeneratePdf("alternating-footer.pdf"); + } +} +``` + +`Dynamic(...)` is an extension method on `IContainer`, so a dynamic component can be placed in the header, footer, content or any nested container. + +### Example: content that depends on the available space + +This list shows as many entries as fit on each page and adds a line with the number of entries shown on that page. It stores the number of entries already shown in its state and reports `HasMoreContent` until all entries are done: + +```csharp +using ShinyPDF.Elements; + +public struct EntryListState +{ + public int ShownCount { get; set; } +} + +public class PagedEntryList : IDynamicComponent +{ + private IReadOnlyList Entries { get; } + public EntryListState State { get; set; } + + public PagedEntryList(IReadOnlyList entries) + { + Entries = entries; + State = new EntryListState { ShownCount = 0 }; + } + + public DynamicComponentComposeResult Compose(DynamicContext context) + { + var start = State.ShownCount; + var fittingCount = 0; + IDynamicElement content = context.CreateElement(_ => { }); + + // Grow the page content one entry at a time until it no longer fits. + for (var count = 1; start + count <= Entries.Count; count++) + { + var candidate = Build(context, start, count); + + if (candidate.Size.Height > context.AvailableSize.Height) + break; + + content = candidate; + fittingCount = count; + } + + State = new EntryListState { ShownCount = start + fittingCount }; + + return new DynamicComponentComposeResult + { + Content = content, + HasMoreContent = State.ShownCount < Entries.Count + }; + } + + private IDynamicElement Build(DynamicContext context, int start, int count) + { + return context.CreateElement(container => + { + container + .Width(context.AvailableSize.Width) + .Column(column => + { + foreach (var entry in Entries.Skip(start).Take(count)) + column.Item().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).Padding(4).Text(entry); + + column.Item().AlignRight().PaddingTop(4).Text($"{count} entries on this page").Italic(); + }); + }); + } +} + +public static class PagedEntryListUsage +{ + public static void Generate() + { + var entries = Enumerable.Range(1, 80).Select(i => $"{i}. {Placeholders.Sentence()}").ToList(); + + Document.Create(document => + { + document.Page(page => + { + page.Size(PageSizes.A5); + page.Margin(1, Unit.Centimetre); + page.Content().Dynamic(new PagedEntryList(entries)); + }); + }) + .GeneratePdf("entries.pdf"); + } +} +``` + +Points worth noting in this example: + +- The width constraint in `Build` makes text wrap at the real page width, so `candidate.Size.Height` is accurate. +- If not even one entry fits, the component returns empty content and `HasMoreContent = true`, so the entries continue on the next page. An entry that is taller than a whole page would repeat this forever; in real code, guard against that case. +- Building the content once per candidate count is simple but grows quadratically with the number of entries per page. For long lists, measure each entry once with `CreateElement` and add up the heights, then combine the measured elements with `.Element(...)`; `IContainer.Element(IDynamicElement)` accepts elements created by `CreateElement`. + +If you only need a table whose header and footer repeat unchanged on every page, a regular `Table` with `table.Header(...)` and `table.Footer(...)` does that without a dynamic component. A dynamic component is needed when the repeated content differs per page, like the per-page count above. diff --git a/docs/reference/elements.md b/docs/reference/elements.md new file mode 100644 index 000000000..9b865ee95 --- /dev/null +++ b/docs/reference/elements.md @@ -0,0 +1,804 @@ +# Layout elements reference + +This page lists every layout element of the ShinyPDF Fluent API: what it does, how it behaves when content flows over several pages, its method signatures and a short example. All methods are extension methods in the `ShinyPDF.Fluent` namespace and operate on `IContainer` (namespace `ShinyPDF.Infrastructure`). Text and text styling are covered in [Text reference](text.md); page size, margins and the page slots (header, content, footer) are covered in [Getting started](../tutorials/getting-started.md) and [Headers, footers and page numbers](../how-to/headers-footers-page-numbers.md). + +## Contents + +- [Conventions](#conventions) +- [Layout containers](#layout-containers): [Column](#column), [Row](#row), [Table](#table), [Layers](#layers), [Inlined](#inlined), [Decoration](#decoration), [Grid](#grid) +- [Sizing](#sizing): [Width, Height, Min and Max](#width-height-min-and-max), [Extend](#extend), [AspectRatio](#aspectratio), [ScaleToFit](#scaletofit), [MinimalBox](#minimalbox), [Unconstrained](#unconstrained) +- [Spacing and positioning](#spacing-and-positioning): [Padding](#padding), [Alignment](#alignment), [Translate](#translate), [Rotate](#rotate), [Scale and Flip](#scale-and-flip) +- [Visual](#visual): [Background](#background), [Border](#border), [Line](#line), [Image](#image), [Canvas](#canvas), [Placeholder](#placeholder) +- [Paging and flow control](#paging-and-flow-control): [PageBreak](#pagebreak), [EnsureSpace](#ensurespace), [ShowEntire](#showentire), [ShowOnce](#showonce), [SkipOnce](#skiponce), [ShowIf](#showif), [StopPaging](#stoppaging) +- [Navigation](#navigation): [Hyperlink](#hyperlink), [Section](#section), [SectionLink](#sectionlink) +- [Content direction](#content-direction) +- [Composition helpers](#composition-helpers): [Container and Element](#container-and-element), [DefaultTextStyle](#defaulttextstyle), [Components](#components), [Dynamic components](#dynamic-components) +- [Debugging](#debugging): [DebugArea](#debugarea), [DebugPointer](#debugpointer) + +## Conventions + +**Containers and chaining.** Most methods wrap the following content and return a new `IContainer`, so they chain: `container.Padding(10).Background(Colors.Grey.Lighten3).Text("Hello")`. Every container accepts exactly one child; assigning a second child to the same container throws a `DocumentComposeException`. Methods that return `void` (for example `Column`, `Row`, `Table`, `Image`, `PageBreak`) end a chain. Methods that take a handler (`Column`, `Row`, `Table`, ...) hold several children through their descriptor. + +**Order matters.** Elements are applied from the outside in. `Padding(10).Background(...)` paints the background inside the padding, `Background(...).Padding(10)` paints it under the padding as well. + +**Units.** Every size parameter is a `float` in points (1/72 inch) unless an optional `Unit unit = Unit.Point` parameter is offered. The `Unit` enum (namespace `ShinyPDF.Infrastructure`) has the values `Point`, `Meter`, `Centimetre`, `Millimetre`, `Feet`, `Inch` and `Mill` (1/1000 inch). Signatures below show the parameter as `Unit unit = Unit.Point` where it exists; where it is missing, the value is in points. + +**Paging.** The layout engine measures each element against the space left on the current page. An element either fits fully, fits partially (the rest continues on the next page) or does not fit at all (it moves to the next page). If an element never fits, even on an empty page, generation fails with a `DocumentLayoutException`. See [How the layout engine works](../explanation/layout-engine.md) and [Debug layout issues](../how-to/debug-layout-issues.md). + +**Examples.** Fragments on this page use a variable `container` of type `IContainer`, which is what `page.Content()`, `page.Header()`, `column.Item()` and similar methods return: + +```csharp +Document + .Create(document => + { + document.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(2, Unit.Centimetre); + + page.Content().Padding(10).Background(Colors.Grey.Lighten3).Text("Hello"); + }); + }) + .GeneratePdf("hello.pdf"); +``` + +Colors are hex strings; the `Colors` helper (namespace `ShinyPDF.Helpers`) provides the Material Design palette, for example `Colors.Blue.Medium` or `Colors.Grey.Lighten3`. Methods that take a color validate it and throw on an invalid value. + +## Layout containers + +### Column + +Stacks items vertically. Every item gets the full available width. When the page is full, the remaining items continue on the next page; an item that supports paging (text, nested columns, tables) is split, other items move to the next page as a whole. + +| Member | Description | +| --- | --- | +| `void Column(this IContainer element, Action handler)` | Creates the column. | +| `ColumnDescriptor.Spacing(float value, Unit unit = Unit.Point)` | Vertical space between items. Default 0. | +| `IContainer ColumnDescriptor.Item()` | Adds an item and returns its container. | + +```csharp +container.Column(column => +{ + column.Spacing(5); + + column.Item().Background(Colors.Grey.Lighten3).Padding(5).Text("First"); + column.Item().Background(Colors.Grey.Lighten2).Padding(5).Text("Second"); + + foreach (var i in Enumerable.Range(1, 3)) + column.Item().Text($"Item {i}"); +}); +``` + +### Row + +Places items horizontally. The width of each item depends on its type: constant (fixed width), relative (share of the remaining width) or auto (the width its content needs). Every item is drawn with the height of the tallest item. If any item does not fit on the page, the whole row moves to the next page; items that are split continue on the next page side by side. + +| Member | Description | +| --- | --- | +| `void Row(this IContainer element, Action handler)` | Creates the row. | +| `RowDescriptor.Spacing(float value)` | Horizontal space between items, in points (no `Unit` parameter). Default 0. | +| `IContainer RowDescriptor.ConstantItem(float size, Unit unit = Unit.Point)` | Item with a fixed width. | +| `IContainer RowDescriptor.RelativeItem(float size = 1)` | Item that takes `size` shares of the width left after constant items, auto items and spacing. | +| `IContainer RowDescriptor.AutoItem()` | Item as wide as its content, measured without width limit. Use it for short content such as labels or icons. | + +```csharp +container.Row(row => +{ + row.Spacing(10); + + row.ConstantItem(3, Unit.Centimetre).Background(Colors.Grey.Lighten2).Text("Fixed"); + row.RelativeItem().Background(Colors.Grey.Lighten3).Text("1 share"); + row.RelativeItem(2).Background(Colors.Grey.Lighten4).Text("2 shares"); + row.AutoItem().Text("Auto"); +}); +``` + +### Table + +A grid of cells with column definitions, optional header and footer, cell spanning and explicit or automatic cell placement. Tables page row by row: a row that does not fit moves to the next page, and if a cell is split, the rest of its row continues on the next page. The header and footer are repeated on every page the table occupies and must fit on a page without splitting. + +**Descriptor members (`TableDescriptor`):** + +| Member | Description | +| --- | --- | +| `void Table(this IContainer element, Action handler)` | Creates the table. | +| `ColumnsDefinition(Action handler)` | Defines the columns. Required: a table without columns throws a `DocumentComposeException`. | +| `Header(Action handler)` | Cells repeated at the top of every page. | +| `Footer(Action handler)` | Cells repeated at the bottom of every page. | +| `ITableCellContainer Cell()` | Adds a content cell. `TableCellDescriptor.Cell()` does the same inside `Header` and `Footer`. | +| `ExtendLastCellsToTableBottom()` | Stretches the last cells of each column to the bottom of the table on every page, so cell borders and backgrounds reach the table bottom. | + +**Column definitions (`TableColumnsDefinitionDescriptor`):** + +| Member | Description | +| --- | --- | +| `ConstantColumn(float width, Unit unit = Unit.Point)` | Column with a fixed width. | +| `RelativeColumn(float width = 1)` | Column that takes `width` shares of the width left after constant columns. | + +**Cell positioning (`TableCellExtensions`, on `ITableCellContainer`):** + +| Member | Description | +| --- | --- | +| `Row(uint value)` | Row position, starting at 1. | +| `Column(uint value)` | Column position, starting at 1. | +| `RowSpan(uint value)` | Number of rows the cell covers. Default 1. | +| `ColumnSpan(uint value)` | Number of columns the cell covers. Default 1. | + +Cells without a position are placed automatically, left to right and top to bottom, into the next free slot after the previous cell; slots covered by spanning cells are skipped. If only one of `Row` and `Column` is set, the other defaults to 1. A cell that starts at, or spans into, a column that does not exist throws a `DocumentComposeException`. Call the positioning methods directly after `Cell()`, before any other element method, because the other methods return a plain `IContainer`. + +Cells have no border or padding of their own. A common pattern is a local style function applied with `Element`: + +```csharp +static IContainer CellStyle(IContainer cell) => + cell.Border(1).BorderColor(Colors.Grey.Lighten1).Padding(5); + +container.Table(table => +{ + table.ColumnsDefinition(columns => + { + columns.ConstantColumn(40); + columns.RelativeColumn(3); + columns.RelativeColumn(); + }); + + table.Header(header => + { + header.Cell().Element(CellStyle).Text("#"); + header.Cell().Element(CellStyle).Text("Product"); + header.Cell().Element(CellStyle).AlignRight().Text("Price"); + }); + + foreach (var i in Enumerable.Range(1, 50)) + { + table.Cell().Element(CellStyle).Text($"{i}"); + table.Cell().Element(CellStyle).Text(Placeholders.Label()); + table.Cell().Element(CellStyle).AlignRight().Text(Placeholders.Decimal()); + } + + table.Footer(footer => + { + footer.Cell().ColumnSpan(3).Element(CellStyle).Text("Prices in EUR"); + }); +}); +``` + +Explicit positions and spans: + +```csharp +container.Table(table => +{ + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(); + columns.RelativeColumn(); + columns.RelativeColumn(); + }); + + table.Cell().Row(1).Column(1).RowSpan(2).Background(Colors.Blue.Lighten3).Text("A (2 rows)"); + table.Cell().Row(1).Column(2).ColumnSpan(2).Background(Colors.Green.Lighten3).Text("B (2 columns)"); + table.Cell().Row(2).Column(3).Background(Colors.Orange.Lighten3).Text("C"); + table.Cell().Text("D"); // placed automatically in the next free slot +}); +``` + +### Layers + +Draws several layers on top of each other in the same space. Exactly one layer must be the primary layer: it determines the size and the paging of the whole element. The other layers get the same space on every page the primary layer occupies, and are drawn in declaration order (later layers on top). Zero or more than one primary layer throws a `DocumentComposeException`. + +| Member | Description | +| --- | --- | +| `void Layers(this IContainer element, Action handler)` | Creates the layers element. | +| `IContainer LayersDescriptor.Layer()` | Adds a secondary layer. | +| `IContainer LayersDescriptor.PrimaryLayer()` | Adds the primary layer. | + +```csharp +container.Layers(layers => +{ + layers.Layer().Background(Colors.Grey.Lighten3); + layers.PrimaryLayer().Padding(10).Text(Placeholders.Paragraph()); + layers.Layer().AlignBottom().AlignRight().Text("DRAFT").FontColor(Colors.Red.Medium); +}); +``` + +### Inlined + +Places items next to each other like words in a paragraph and wraps them into new lines when the width is used up. Items are never split; whole lines continue on the next page. + +| Member | Description | +| --- | --- | +| `void Inlined(this IContainer element, Action handler)` | Creates the element. | +| `Spacing(float value, Unit unit = Unit.Point)` | Sets vertical and horizontal spacing. | +| `VerticalSpacing(float value, Unit unit = Unit.Point)` | Space between lines. | +| `HorizontalSpacing(float value, Unit unit = Unit.Point)` | Space between items in a line. | +| `BaselineTop()`, `BaselineMiddle()`, `BaselineBottom()` | Vertical alignment of items within a line. | +| `AlignLeft()`, `AlignCenter()`, `AlignRight()`, `AlignJustify()`, `AlignSpaceAround()` | Horizontal distribution of items within a line. Default: left (right for right-to-left content). | +| `IContainer Item()` | Adds an item. | + +```csharp +container.Inlined(inlined => +{ + inlined.Spacing(5); + inlined.BaselineMiddle(); + inlined.AlignCenter(); + + foreach (var tag in new[] { "pdf", "dotnet", "fluent", "layout", "csharp" }) + inlined.Item().Background(Colors.Blue.Lighten4).PaddingHorizontal(6).PaddingVertical(2).Text(tag); +}); +``` + +### Decoration + +Splits a space into three parts: `Before` (top), `Content` and `After` (bottom). When the content spans several pages, `Before` and `After` are repeated on every page. `Before` and `After` must fit on a page without splitting. This is the element behind table headers and footers and is useful for section titles that repeat, for example "Orders (continued)". + +| Member | Description | +| --- | --- | +| `void Decoration(this IContainer element, Action handler)` | Creates the element. | +| `IContainer Before()` / `void Before(Action handler)` | Repeated top part. | +| `IContainer Content()` / `void Content(Action handler)` | Main content, pages normally. | +| `IContainer After()` / `void After(Action handler)` | Repeated bottom part. | + +```csharp +container.Decoration(decoration => +{ + decoration.Before().Background(Colors.Grey.Lighten3).Padding(5).Text("Orders"); + + decoration.Content().Column(column => + { + foreach (var i in Enumerable.Range(1, 100)) + column.Item().Text($"Order {i}"); + }); + + decoration.After(after => after.AlignRight().Text("End of section")); +}); +``` + +### Grid + +Arranges items in rows of a fixed number of columns (12 by default); each item takes a number of those columns, and a new row starts when an item does not fit into the current one. Internally, a grid is a [Column](#column) of [Rows](#row) with relative items, so it pages like a column of rows. + +> **Note:** `Grid` is currently marked `[Obsolete]`. The marker is inherited from QuestPDF, and calling `Grid` produces compiler warning CS0618. + +| Member | Description | +| --- | --- | +| `void Grid(this IContainer element, Action handler)` | Creates the grid. If the container was created by `AlignLeft()`, `AlignCenter()` or `AlignRight()`, that horizontal alignment is used as the grid alignment. | +| `Columns(int value = 12)` | Number of columns per row. | +| `Spacing(float value, Unit unit = Unit.Point)` | Sets vertical and horizontal spacing. | +| `VerticalSpacing(float value, Unit unit = Unit.Point)` | Space between rows. | +| `HorizontalSpacing(float value, Unit unit = Unit.Point)` | Space between items in a row. | +| `Alignment(HorizontalAlignment alignment)`, `AlignLeft()`, `AlignCenter()`, `AlignRight()` | Position of incomplete rows. Default: left. | +| `IContainer Item(int columns = 1)` | Adds an item spanning `columns` columns. | + +```csharp +container.Grid(grid => +{ + grid.Columns(12); + grid.Spacing(5); + grid.AlignCenter(); + + grid.Item(8).Background(Colors.Blue.Lighten3).Height(40); + grid.Item(4).Background(Colors.Blue.Lighten2).Height(40); + grid.Item(6).Background(Colors.Green.Lighten3).Height(40); +}); +``` + +## Sizing + +### Width, Height, Min and Max + +Constrain the size of the content. `Min*` makes the content at least that large; `Max*` limits the space the content may use. `Width` and `Height` set both minimum and maximum to the same value. If a minimum is larger than the space left on the page, the element moves to the next page. Content larger than a maximum must fit into it or page within it. + +| Method | Description | +| --- | --- | +| `IContainer Width(float value, Unit unit = Unit.Point)` | Exact width. | +| `IContainer MinWidth(float value, Unit unit = Unit.Point)` | Minimum width. | +| `IContainer MaxWidth(float value, Unit unit = Unit.Point)` | Maximum width. | +| `IContainer Height(float value, Unit unit = Unit.Point)` | Exact height. | +| `IContainer MinHeight(float value, Unit unit = Unit.Point)` | Minimum height. | +| `IContainer MaxHeight(float value, Unit unit = Unit.Point)` | Maximum height. | + +Calls chained directly after each other configure the same element. + +```csharp +container.Column(column => +{ + column.Item().Width(5, Unit.Centimetre).Height(2, Unit.Centimetre).Background(Colors.Blue.Lighten3); + column.Item().MinHeight(50).MaxWidth(200).Background(Colors.Green.Lighten3).Text(Placeholders.Sentence()); +}); +``` + +### Extend + +Makes the element take all available space in one or both directions instead of only the space its content needs. On a page, `ExtendVertical` fills the remaining height of the current page. + +| Method | Description | +| --- | --- | +| `IContainer Extend()` | Extends in both directions. | +| `IContainer ExtendVertical()` | Takes the full available height. | +| `IContainer ExtendHorizontal()` | Takes the full available width. | + +```csharp +container.ExtendVertical().Background(Colors.Grey.Lighten4).AlignBottom().Text("Bottom of the page"); +``` + +### AspectRatio + +Gives the content a size with a fixed width-to-height ratio. The content never pages: if the computed size does not fit into the remaining space, the element moves to the next page. + +`IContainer AspectRatio(float ratio, AspectRatioOption option = AspectRatioOption.FitWidth)` + +- `ratio`: width divided by height, for example `16f / 9`. +- `option` (enum `AspectRatioOption`): `FitWidth` uses the full available width and computes the height; `FitHeight` uses the full available height and computes the width; `FitArea` uses the largest size that fits both. + +```csharp +container.AspectRatio(16f / 9).Background(Colors.Blue.Lighten3).AlignCenter().AlignMiddle().Text("16:9"); +``` + +### ScaleToFit + +Scales the content down until it fits completely into the available space, for example a long text in a fixed-size box. Content that already fits is not scaled; content is never scaled up. The scale is found by a binary search, and if the content does not fit even at a very small scale, the element moves to the next page. + +`IContainer ScaleToFit()` + +```csharp +container.Width(150).Height(40).Border(1).ScaleToFit().Text(Placeholders.Paragraph()); +``` + +### MinimalBox + +Draws the content with the size it actually needs instead of the full space offered by the parent. Useful in front of `Background` or `Border` so they wrap the content tightly instead of stretching to the full width. + +`IContainer MinimalBox()` + +```csharp +container.MinimalBox().Border(1).Padding(5).Text("Border fits the text"); +``` + +### Unconstrained + +Removes all size limits: the content is measured with unlimited space and takes no space in the parent layout. It is drawn from its position and can overflow the parent and the page. Useful for overlays such as stamps or markers. In right-to-left content it extends to the left. + +`IContainer Unconstrained()` + +```csharp +container.Column(column => +{ + column.Item().Unconstrained().TranslateX(-20).Text("Margin note"); + column.Item().Text(Placeholders.Paragraph()); +}); +``` + +## Spacing and positioning + +### Padding + +Adds empty space around the content. Padding reduces the space available to the content; if no space is left, the element moves to the next page. Directly chained padding calls add up: `Padding(10).PaddingLeft(5)` results in 15 points on the left. + +| Method | Description | +| --- | --- | +| `IContainer Padding(float value, Unit unit = Unit.Point)` | All four sides. | +| `IContainer PaddingHorizontal(float value, Unit unit = Unit.Point)` | Left and right. | +| `IContainer PaddingVertical(float value, Unit unit = Unit.Point)` | Top and bottom. | +| `IContainer PaddingTop(float value, Unit unit = Unit.Point)` | Top. | +| `IContainer PaddingBottom(float value, Unit unit = Unit.Point)` | Bottom. | +| `IContainer PaddingLeft(float value, Unit unit = Unit.Point)` | Left. | +| `IContainer PaddingRight(float value, Unit unit = Unit.Point)` | Right. | + +```csharp +container.PaddingVertical(1, Unit.Centimetre).PaddingHorizontal(20).Background(Colors.Grey.Lighten3).Text("Padded"); +``` + +### Alignment + +Positions the content inside the available space. The content gets its own size in the aligned direction and the full space in the other direction. A horizontal and a vertical alignment chained directly configure the same element. + +| Method | Description | +| --- | --- | +| `IContainer AlignLeft()`, `AlignCenter()`, `AlignRight()` | Horizontal alignment. | +| `IContainer AlignTop()`, `AlignMiddle()`, `AlignBottom()` | Vertical alignment. | + +```csharp +container.Height(100).Background(Colors.Grey.Lighten3).AlignCenter().AlignMiddle().Text("Centered"); +``` + +### Translate + +Moves the content when it is drawn, without changing the layout: the space reserved for the content stays where it was, and the moved content may overlap neighbors. Positive values move right and down. Directly chained calls add up. + +| Method | Description | +| --- | --- | +| `IContainer TranslateX(float value, Unit unit = Unit.Point)` | Horizontal offset. | +| `IContainer TranslateY(float value, Unit unit = Unit.Point)` | Vertical offset. | + +```csharp +container.TranslateX(10).TranslateY(-5).Text("Shifted"); +``` + +### Rotate + +`RotateLeft` and `RotateRight` turn the content by 90 degrees and swap its width and height in the layout, so the parent reserves the rotated size. Calls add up: two `RotateRight` calls turn it upside down. `Rotate(angle)` turns the content by any angle, clockwise, around its top-left corner; it only affects drawing, the layout keeps the unrotated size. + +| Method | Description | +| --- | --- | +| `IContainer RotateLeft()` | 90 degrees counterclockwise, affects layout. | +| `IContainer RotateRight()` | 90 degrees clockwise, affects layout. | +| `IContainer Rotate(float angle)` | Any angle in degrees, drawing only. | + +```csharp +container.Row(row => +{ + row.AutoItem().RotateLeft().Text("Vertical label"); + row.RelativeItem().Padding(10).Rotate(15).Text("Tilted text"); +}); +``` + +### Scale and Flip + +Scales the content. Unlike `Rotate(angle)`, scaling affects the layout: the content is measured with the available space divided by the factor and reserves the scaled size. Negative factors mirror the content. Directly chained calls multiply. + +| Method | Description | +| --- | --- | +| `IContainer Scale(float value)` | Scales both directions by `value` (1 = original size). | +| `IContainer ScaleHorizontal(float value)` | Scales horizontally. | +| `IContainer ScaleVertical(float value)` | Scales vertically. | +| `IContainer FlipHorizontal()` | Mirrors horizontally (`ScaleHorizontal(-1)`). | +| `IContainer FlipVertical()` | Mirrors vertically (`ScaleVertical(-1)`). | +| `IContainer FlipOver()` | Mirrors in both directions. | + +```csharp +container.Column(column => +{ + column.Item().Scale(0.5f).Text("Half size"); + column.Item().ScaleHorizontal(2).Text("Stretched"); + column.Item().FlipHorizontal().Text("Mirrored"); +}); +``` + +## Visual + +### Background + +Fills the space given to the element with a color, then draws the content on top. The background covers the full space offered by the parent (often the full width); combine it with [MinimalBox](#minimalbox) or an alignment to fit it to the content. + +`IContainer Background(string color)` + +```csharp +container.Background(Colors.Amber.Lighten4).Padding(10).Text("Highlighted"); +``` + +### Border + +Draws lines along the edges of the element. Borders take no layout space: each line is centered on the edge (half inside, half outside) and drawn over the content, so add [Padding](#padding) inside the border to keep content away from it. Default color: black. + +| Method | Description | +| --- | --- | +| `IContainer Border(float value, Unit unit = Unit.Point)` | All four sides. | +| `IContainer BorderVertical(float value, Unit unit = Unit.Point)` | Left and right. | +| `IContainer BorderHorizontal(float value, Unit unit = Unit.Point)` | Top and bottom. | +| `IContainer BorderTop(float value, Unit unit = Unit.Point)` | Top. | +| `IContainer BorderBottom(float value, Unit unit = Unit.Point)` | Bottom. | +| `IContainer BorderLeft(float value, Unit unit = Unit.Point)` | Left. | +| `IContainer BorderRight(float value, Unit unit = Unit.Point)` | Right. | +| `IContainer BorderColor(string color)` | Line color. | + +Chain the thickness and color calls directly so they configure the same border. + +```csharp +container.BorderBottom(2).BorderColor(Colors.Blue.Medium).PaddingBottom(5).Text("Underlined block"); +``` + +### Line + +Draws a horizontal or vertical line. A horizontal line takes the full available width and `size` points of height; a vertical line takes the full available height and `size` points of width. Default color: black. The methods return `ILine` (namespace `ShinyPDF.Elements`), on which `LineColor` sets the color and ends the chain. + +| Method | Description | +| --- | --- | +| `ILine LineHorizontal(float size, Unit unit = Unit.Point)` | Horizontal line of thickness `size`. | +| `ILine LineVertical(float size, Unit unit = Unit.Point)` | Vertical line of thickness `size`. | +| `void LineColor(this ILine descriptor, string value)` | Line color. | + +```csharp +container.Column(column => +{ + column.Item().Text("Above"); + column.Item().PaddingVertical(5).LineHorizontal(1).LineColor(Colors.Grey.Medium); + column.Item().Text("Below"); +}); +``` + +### Image + +Draws a raster image (any format SkiaSharp can decode, for example PNG or JPEG). The image is never split across pages: if it does not fit into the remaining space, it moves to the next page. An image that cannot be loaded or decoded throws a `DocumentComposeException`. + +| Method | Description | +| --- | --- | +| `void Image(byte[] imageData, ImageScaling scaling = ImageScaling.FitWidth)` | Image from bytes. | +| `void Image(string filePath, ImageScaling scaling = ImageScaling.FitWidth)` | Image from a file. | +| `void Image(Stream fileStream, ImageScaling scaling = ImageScaling.FitWidth)` | Image from a stream. | +| `void Image(Func imageSource)` | Dynamic image, see below. | + +`ImageScaling` (namespace `ShinyPDF.Infrastructure`): `FitWidth` uses the full available width and keeps the aspect ratio; `FitHeight` uses the full available height; `FitArea` uses the largest size that fits; `Resize` stretches the image to the available space and ignores the aspect ratio. Limit the size with [Width, Height, Min and Max](#width-height-min-and-max). + +```csharp +var logo = File.ReadAllBytes("logo.png"); + +container.Row(row => +{ + row.ConstantItem(80).Image(logo); + row.RelativeItem().Height(60).Image("photo.jpg", ImageScaling.FitArea); +}); +``` + +The dynamic overload calls `imageSource` while drawing, with the size (in points) the image gets, and expects encoded image bytes. Use it to render charts or other graphics in the exact target size. The image takes all available space, so constrain it. + +```csharp +container.Height(150).Image(size => Placeholders.Image((int)size.Width, (int)size.Height)); +``` + +### Canvas + +Gives direct access to the SkiaSharp canvas for custom drawing. The handler has the delegate type `DrawOnCanvas(SKCanvas canvas, Size availableSpace)` (namespace `ShinyPDF.Elements`); coordinates start at the top-left corner of the element, in points. The element takes all available space; the canvas transformation is restored after the handler returns. + +`void Canvas(DrawOnCanvas handler)` + +```csharp +using SkiaSharp; + +container.Height(100).Canvas((canvas, size) => +{ + using var paint = new SKPaint + { + Color = SKColor.Parse(Colors.Blue.Medium), + IsStroke = true, + StrokeWidth = 2 + }; + + canvas.DrawRoundRect(0, 0, size.Width, size.Height, 10, 10, paint); +}); +``` + +### Placeholder + +Draws a grey box with an image icon, or with the given text. Use it while designing a layout to mark areas whose content is not ready yet. It fills the space it is given, so set a size. + +`void Placeholder(string? text = null)` + +```csharp +container.Row(row => +{ + row.RelativeItem().Height(100).Placeholder(); + row.RelativeItem().Height(100).Placeholder("Chart"); +}); +``` + +The `Placeholders` helper (namespace `ShinyPDF.Helpers`) generates random sample content: text (`LoremIpsum()`, `Label()`, `Sentence()`, `Paragraph()`, `Paragraphs()`), values (`Name()`, `Email()`, `PhoneNumber()`, `Integer()`, `Decimal()`, `Percent()`, dates and times), colors (`Color()`, `BackgroundColor()`) and images (`Image(int width, int height)`, `Image(Size size)`). + +## Paging and flow control + +### PageBreak + +Ends the current page; the following content starts on the next page. Typically used as a column item. + +`void PageBreak()` + +```csharp +container.Column(column => +{ + column.Item().Text("Chapter 1"); + column.Item().PageBreak(); + column.Item().Text("Chapter 2"); +}); +``` + +### EnsureSpace + +Avoids starting content at the very bottom of a page. If the content would be split and less than `minHeight` points are left on the current page, the whole element moves to the next page. Content that fits completely stays where it is. + +`IContainer EnsureSpace(float minHeight = 150)` + +```csharp +container.Column(column => +{ + column.Item().Text(Placeholders.Paragraphs()); + column.Item().EnsureSpace(100).Text(Placeholders.Paragraphs()); +}); +``` + +### ShowEntire + +Prevents splitting: if the content does not fit completely into the remaining space, it moves to the next page. Content that is taller than a whole page then fails with a `DocumentLayoutException`. + +`IContainer ShowEntire()` + +```csharp +container.ShowEntire().Column(column => +{ + column.Item().Text("Signature"); + column.Item().Height(60).Border(1); +}); +``` + +### ShowOnce + +Shows the content only the first time it is drawn completely. When the element is repeated on later pages (for example in a page header, a `Decoration` or a table header), it takes no space there. + +`IContainer ShowOnce()` + +### SkipOnce + +The opposite of `ShowOnce`: hides the content the first time it is drawn and shows it on all later repetitions. Combined with `ShowOnce`, it creates "continued" labels: + +`IContainer SkipOnce()` + +```csharp +container.Decoration(decoration => +{ + decoration.Before().Column(column => + { + column.Item().ShowOnce().Text("Invoice items"); + column.Item().SkipOnce().Text("Invoice items (continued)"); + }); + + decoration.Content().Column(column => + { + foreach (var i in Enumerable.Range(1, 80)) + column.Item().Text($"Item {i}"); + }); +}); +``` + +### ShowIf + +Includes the content only if `condition` is `true`. If it is `false`, the returned container is detached from the document and everything added to it is ignored. + +`IContainer ShowIf(bool condition)` + +```csharp +var isDraft = true; + +container.Column(column => +{ + column.Item().ShowIf(isDraft).Text("DRAFT").FontColor(Colors.Red.Medium); + column.Item().Text("Document body"); +}); +``` + +### StopPaging + +Limits the content to one page: only the part that fits into the current space is drawn, the rest is discarded. If nothing fits, the content is skipped and takes no space. Useful for previews or content that must not create extra pages. + +`IContainer StopPaging()` + +```csharp +container.Height(200).StopPaging().Text(Placeholders.Paragraphs()); +``` + +## Navigation + +### Hyperlink + +Makes the area of the content a clickable link to an external URL. For a link on part of a text, use `Hyperlink` inside a text block, see [Text reference](text.md). + +`IContainer Hyperlink(string url)` + +```csharp +container.Hyperlink("https://github.com/LM-Development/ShinyPDF").Text("ShinyPDF on GitHub").FontColor(Colors.Blue.Medium); +``` + +### Section + +Marks the content as a named location (a PDF destination) that `SectionLink` can jump to. The section name also feeds the section page number methods of text blocks (`BeginPageNumberOfSection`, `EndPageNumberOfSection`, `PageNumberWithinSection`, `TotalPagesWithinSection`, see [Text reference](text.md)). + +`IContainer Section(string sectionName)` + +### SectionLink + +Makes the area of the content a clickable link that jumps to the section with the given name. + +`IContainer SectionLink(string sectionName)` + +```csharp +container.Column(column => +{ + column.Item().SectionLink("details").Text("Go to details").FontColor(Colors.Blue.Medium); + column.Item().PageBreak(); + column.Item().Section("details").Text("Details"); +}); +``` + +## Content direction + +Sets the direction for the content and everything inside it. Right-to-left mirrors the order of row items and table columns, the default alignment of inlined items, and the placement of elements that do not fill the full width. The default is left-to-right; it can also be set for a whole page with `page.ContentFromRightToLeft()`. + +| Method | Description | +| --- | --- | +| `IContainer ContentFromLeftToRight()` | Left-to-right layout (default). | +| `IContainer ContentFromRightToLeft()` | Right-to-left layout, for example for Arabic or Hebrew documents. | + +```csharp +container.ContentFromRightToLeft().Row(row => +{ + row.ConstantItem(50).Background(Colors.Red.Lighten3).Text("1"); + row.RelativeItem().Background(Colors.Green.Lighten3).Text("2"); +}); +``` + +## Composition helpers + +### Container and Element + +`Container()` inserts an empty wrapper and returns its container. `Element` applies a function to the container, which lets you extract and reuse a chain of styling calls. + +| Method | Description | +| --- | --- | +| `IContainer Container()` | Adds a neutral container. | +| `void Element(Action handler)` | Passes the container to `handler`. | +| `IContainer Element(Func handler)` | Passes the container to `handler` and continues the chain with the returned container. | + +```csharp +static IContainer Card(IContainer card) => + card.Border(1).BorderColor(Colors.Grey.Lighten1).Background(Colors.Grey.Lighten5).Padding(10); + +container.Column(column => +{ + column.Spacing(10); + column.Item().Element(Card).Text("First card"); + column.Item().Element(Card).Text("Second card"); + column.Item().Element(item => item.Text("Built inline")); +}); +``` + +### DefaultTextStyle + +Sets the default text style for all text inside the container. The available style options are described in [Text reference](text.md). + +| Method | Description | +| --- | --- | +| `IContainer DefaultTextStyle(TextStyle textStyle)` | Uses the given style. | +| `IContainer DefaultTextStyle(Func handler)` | Builds a style from `TextStyle.Default`. | + +```csharp +container.DefaultTextStyle(style => style.FontSize(14).FontColor(Colors.Grey.Darken3)).Column(column => +{ + column.Item().Text("Uses the default style"); + column.Item().Text("Overrides the size").FontSize(20); +}); +``` + +### Components + +`Component(T component)` and `Component()` insert a reusable piece of layout that implements `IComponent` (a single `Compose(IContainer container)` method). See [Reusable components](../how-to/reusable-components.md). + +### Dynamic components + +`Dynamic(IDynamicComponent dynamicElement)` inserts a component that is composed again for every page, with access to the page number, the total page count and the available size (`DynamicContext`). Use it for content that depends on its position, such as running totals. The related overload `Element(IDynamicElement child)` places an element created with `DynamicContext.CreateElement`. See [Reusable components](../how-to/reusable-components.md) for details and examples. + +## Debugging + +These elements help to find layout problems; see [Debug layout issues](../how-to/debug-layout-issues.md). + +### DebugArea + +Draws a 1-point border and a semi-transparent fill over the space the content gets, with an optional label centered at the top, so you can see the space an element actually occupies. + +| Method | Description | +| --- | --- | +| `IContainer DebugArea()` | Red, without label. | +| `IContainer DebugArea(string text)` | Red, with label. | +| `IContainer DebugArea(string text, string color)` | Label, border and fill in the given color. | + +```csharp +container.Padding(10).DebugArea("Content", Colors.Blue.Medium).Text(Placeholders.Paragraph()); +``` + +### DebugPointer + +Adds a named marker that shows up in the element trace of a `DocumentLayoutException`, so you can locate the failing element. It does not change the output. + +`IContainer DebugPointer(string elementTraceText)` + +```csharp +container.DebugPointer("Invoice items").Column(column => +{ + column.Item().Text("Item 1"); +}); +``` diff --git a/docs/reference/text.md b/docs/reference/text.md new file mode 100644 index 000000000..14046e964 --- /dev/null +++ b/docs/reference/text.md @@ -0,0 +1,402 @@ +# Text reference + +This page lists everything ShinyPDF offers for text: the `Text` entry points, the text descriptor with spans, links, page numbers and inline elements, every text style option, and the rules for how styles are inherited. All members live in the `ShinyPDF.Fluent` and `ShinyPDF.Infrastructure` namespaces. For registering your own fonts and handling missing glyphs, see [Fonts](../how-to/fonts.md). + +## Contents + +- [Adding text](#adding-text) +- [Spans, lines and paragraphs](#spans-lines-and-paragraphs) +- [Alignment](#alignment) +- [Links](#links) +- [Page numbers](#page-numbers) +- [Inline elements](#inline-elements) +- [Text styles](#text-styles) + - [Font family, size and color](#font-family-size-and-color) + - [Font weight](#font-weight) + - [Italic, underline and strikethrough](#italic-underline-and-strikethrough) + - [Subscript and superscript](#subscript-and-superscript) + - [Line height and letter spacing](#line-height-and-letter-spacing) + - [Word wrapping](#word-wrapping) + - [Text direction](#text-direction) + - [Font fallback](#font-fallback) +- [Style inheritance](#style-inheritance) +- [Library defaults](#library-defaults) + +## Adding text + +`TextExtensions` provides two overloads on `IContainer`: + +| Overload | Returns | Use it for | +|---|---|---| +| `Text(string? text)` | `TextSpanDescriptor` | A single run of text with one style. Style methods can be chained directly. | +| `Text(Action content)` | `void` | Rich text: several spans with different styles, links, page numbers, inline elements. | + +Both overloads end the container chain: a text element has no child. + +```csharp +container.Column(column => +{ + column.Item().Text("A single styled line").FontSize(16).Bold(); + + column.Item().Text(text => + { + text.Span("Rich text with a "); + text.Span("bold").Bold(); + text.Span(" word."); + }); +}); +``` + +## Spans, lines and paragraphs + +Members of `TextDescriptor` that add text. Each returns a `TextSpanDescriptor`, so you can style the added text. + +| Member | Description | +|---|---| +| `Span(string? text)` | Appends text to the current paragraph. A `null` value adds nothing. | +| `Line(string? text)` | Appends text followed by a line break, so the next content starts a new paragraph. | +| `EmptyLine()` | Adds an empty paragraph (a blank line in the current style). | +| `ParagraphSpacing(float value, Unit unit = Unit.Point)` | Vertical space between paragraphs. Default is `0`. | +| `DefaultTextStyle(TextStyle style)` / `DefaultTextStyle(Func style)` | Default style for all spans inside this text element (see [Style inheritance](#style-inheritance)). | + +Every line break (`\n`, also inside `Span`) starts a new paragraph; `\r` characters are removed. `ParagraphSpacing` applies between these paragraphs, not between wrapped lines of one paragraph. Lines wrap at spaces; a word longer than the available width is broken only when it is the first item in the line. + +```csharp +container.Text(text => +{ + text.ParagraphSpacing(8); + text.DefaultTextStyle(x => x.FontSize(11)); + + text.Line("First paragraph."); + text.Line("Second paragraph, separated by 8 points."); + text.EmptyLine(); + text.Span("Text after a blank line.\nThis starts another paragraph."); +}); +``` + +## Alignment + +| Member | Description | +|---|---| +| `AlignLeft()` | Aligns every line to the left. | +| `AlignCenter()` | Centers every line. | +| `AlignRight()` | Aligns every line to the right. | + +If you do not set an alignment on the descriptor, ShinyPDF uses the horizontal alignment of an `AlignLeft()`, `AlignCenter()` or `AlignRight()` container directly in front of `Text`. Without either, text is aligned left in left-to-right content and right in right-to-left content (`ContentFromRightToLeft()`). There is no justified alignment. + +```csharp +container.Column(column => +{ + column.Item().Text(text => + { + text.AlignCenter(); + text.Span("Centered with the descriptor"); + }); + + // the container alignment is picked up by the text element + column.Item().AlignRight().Text("Right-aligned with the container"); +}); +``` + +## Links + +| Member | Description | +|---|---| +| `Hyperlink(string? text, string url)` | Text that opens an external URL. Throws `ArgumentException` if `url` is null or empty. Empty `text` adds nothing. | +| `SectionLink(string? text, string sectionName)` | Text that jumps to a location inside the document marked with `container.Section(sectionName)`. Throws `ArgumentException` if `sectionName` is null or empty. Empty `text` adds nothing. | + +Links get no default styling: add color or underline yourself. To make a whole area clickable instead of a text run, use the `Hyperlink(url)` and `SectionLink(sectionName)` container extensions (see [Elements](elements.md)). + +```csharp +container.Column(column => +{ + column.Item().Text(text => + { + text.Span("Visit "); + text.Hyperlink("the repository", "https://github.com/LM-Development/ShinyPDF") + .FontColor(Colors.Blue.Medium) + .Underline(); + text.Span(" or jump to the "); + text.SectionLink("summary", "summary").Underline(); + text.Span("."); + }); + + column.Item().PageBreak(); + + column.Item().Section("summary").Text("Summary").FontSize(20).Bold(); +}); +``` + +## Page numbers + +These `TextDescriptor` members insert a number that is resolved while the page is drawn. Each returns a `TextPageNumberDescriptor`, which supports every span style method plus `Format`. + +| Member | Value | +|---|---| +| `CurrentPageNumber()` | Number of the current page (1-based). | +| `TotalPages()` | Total number of pages in the document. | +| `BeginPageNumberOfSection(string locationName)` | First page of the section marked with `Section(locationName)`. | +| `EndPageNumberOfSection(string locationName)` | Last page of that section. | +| `PageNumberWithinSection(string locationName)` | Current page number counted from the start of that section (1 on its first page). | +| `TotalPagesWithinSection(string locationName)` | Number of pages the section spans. | + +`Format(PageNumberFormatter formatter)` controls how the number is printed. The delegate is `string PageNumberFormatter(int? pageNumber)`. The value is `null` when it cannot be resolved, for example when no section with that name exists; the default formatter prints an empty string in that case. + +Page numbers are typical in headers and footers; see [Headers, footers and page numbers](../how-to/headers-footers-page-numbers.md) for complete layouts. + +```csharp +container.AlignCenter().Text(text => +{ + text.Span("Page "); + text.CurrentPageNumber().Bold(); + text.Span(" of "); + text.TotalPages(); + + text.Span(" | "); + text.CurrentPageNumber().Format(number => number is null ? "-" : $"#{number:D3}"); +}); +``` + +## Inline elements + +`Element()` returns an `IContainer` placed inside the text flow, like a large glyph. Use it for icons, small images or spacers. The content is bottom-aligned on the line and takes its minimal size, so give it an explicit size. Inline elements inherit the surrounding default text style. + +```csharp +container.Text(text => +{ + text.Span("Status: "); + text.Element().Width(10).Height(10).Background(Colors.Green.Medium); + text.Span(" online"); +}); +``` + +## Text styles + +A `TextStyle` (namespace `ShinyPDF.Infrastructure`) is an immutable record. Start from `TextStyle.Default` (a style with nothing set) and chain methods from `TextStyleExtensions`; each call returns a new style. Reuse a style on a span with `Style(TextStyle style)` or set it as a default with `DefaultTextStyle`. + +The same options exist as span methods (`TextSpanDescriptorExtensions`), which work on `TextSpanDescriptor` and `TextPageNumberDescriptor` and return the same descriptor type for chaining. + +```csharp +var heading = TextStyle.Default.FontSize(18).SemiBold().FontColor(Colors.Blue.Darken2); + +container.Column(column => +{ + column.Item().Text("Styled with a reusable TextStyle").Style(heading); + column.Item().Text("Styled with span methods").FontSize(18).SemiBold(); +}); +``` + +| Option | `TextStyle` method | Span method | +|---|---|---| +| Font family | `FontFamily(string)` | same | +| Font size (points) | `FontSize(float)` | same | +| Text color | `FontColor(string)` | same | +| Background color | `BackgroundColor(string)` | same | +| Line height | `LineHeight(float)` | same | +| Letter spacing | `LetterSpacing(float)` | same | +| Weight | `Weight(FontWeight)`, `Thin()` ... `ExtraBlack()` | `Thin()` ... `ExtraBlack()` (no `Weight`) | +| Italic | `Italic(bool value = true)` | same | +| Underline | `Underline(bool value = true)` | same | +| Strikethrough | `Strikethrough(bool value = true)` | same | +| Position | `NormalPosition()`, `Subscript()`, `Superscript()` | same | +| Wrapping | `WrapAnywhere(bool value = true)` | same | +| Direction | `DirectionAuto()`, `DirectionFromLeftToRight()`, `DirectionFromRightToLeft()` | same | +| Fallback | `Fallback(TextStyle? value = null)`, `Fallback(Func)` | same, see the note in [Font fallback](#font-fallback) | +| Apply a whole style | n/a | `Style(TextStyle)` | + +### Font family, size and color + +- `FontFamily(string value)`: the family name of a built-in, registered or installed font, for example `Fonts.Lato`, `Fonts.NotoSans`, `Fonts.Courier` (constants in `ShinyPDF.Helpers.Fonts`). An unknown family causes an `ArgumentException` during generation. See [Fonts](../how-to/fonts.md). +- `FontSize(float value)`: size in points. Values `<= 0` throw `ArgumentException`. +- `FontColor(string value)` and `BackgroundColor(string value)`: hex colors in the formats `#RGB`, `#ARGB`, `#RRGGBB` or `#AARRGGBB` (the `#` is optional). Invalid values throw `ArgumentException` immediately. Use the `Colors` palette in `ShinyPDF.Helpers` (for example `Colors.Red.Medium`, `Colors.Grey.Lighten3`). The background covers the span's full line height. + +```csharp +container.Text(text => +{ + text.Span("Courier 14, red ").FontFamily(Fonts.Courier).FontSize(14).FontColor(Colors.Red.Medium); + text.Span("highlighted").BackgroundColor(Colors.Yellow.Lighten3); + text.Span(" semi-transparent").FontColor("#80000000"); +}); +``` + +### Font weight + +`FontWeight` enum values and their shortcut methods: + +| Method | `FontWeight` | Value | +|---|---|---| +| `Thin()` | `Thin` | 100 | +| `ExtraLight()` | `ExtraLight` | 200 | +| `Light()` | `Light` | 300 | +| `NormalWeight()` | `Normal` | 400 | +| `Medium()` | `Medium` | 500 | +| `SemiBold()` | `SemiBold` | 600 | +| `Bold()` | `Bold` | 700 | +| `ExtraBold()` | `ExtraBold` | 800 | +| `Black()` | `Black` | 900 | +| `ExtraBlack()` | `ExtraBlack` | 1000 | + +If the font has no file for the requested weight, the closest available one is used (see [Fonts](../how-to/fonts.md#font-weight-and-italic-matching)). For example, the built-in Lato family has Light, Regular, Bold and Black, so `Medium()` renders as Regular and `SemiBold()` as Bold. + +```csharp +var semiBoldStyle = TextStyle.Default.Weight(FontWeight.SemiBold); + +container.Text(text => +{ + text.DefaultTextStyle(x => x.FontFamily(Fonts.NotoSans)); + text.Line("Thin").Thin(); + text.Line("Normal").NormalWeight(); + text.Line("SemiBold via Weight").Style(semiBoldStyle); + text.Line("Black").Black(); +}); +``` + +### Italic, underline and strikethrough + +`Italic()`, `Underline()` and `Strikethrough()` accept an optional `bool`, so you can switch an inherited decoration off, for example `Underline(false)`. + +Decoration lines use the text color and the position and thickness defined by the font. There are no options for line color, thickness or style. Current behavior with sub- and superscript: + +- Superscript: the underline is drawn at the position of normal-sized text, so it lines up with the surrounding text. +- Subscript: the underline follows the lowered, smaller glyphs. +- Strikethrough follows the glyphs; its thickness is scaled to 62.5% for sub- and superscript. + +```csharp +container.Text(text => +{ + text.DefaultTextStyle(x => x.Underline()); + text.Span("Underlined, "); + text.Span("not underlined, ").Underline(false); + text.Span("italic, ").Italic(); + text.Span("struck through").Strikethrough(); +}); +``` + +### Subscript and superscript + +`Subscript()` and `Superscript()` render the span at 62.5% of the font size, shifted down by 10% or up by 35% of the font size. To keep strokes visually as thick as the surrounding text, the font is matched one weight step heavier (+100). `NormalPosition()` resets an inherited position. + +```csharp +container.Text(text => +{ + text.Span("H"); + text.Span("2").Subscript(); + text.Span("O and E = mc"); + text.Span("2").Superscript(); +}); +``` + +### Line height and letter spacing + +- `LineHeight(float value)`: a multiplier of the font's natural line height (default `1.2`). The height of a wrapped line is the largest height of the items on it, so a big span or inline element makes its whole line taller. +- `LetterSpacing(float value)`: extra space between characters, relative to the font size (em). `0` is the font's normal spacing, positive values add space, negative values reduce it. For example `0.1f` at 20 pt adds 2 pt per character. + +```csharp +container.Column(column => +{ + column.Item().Text(Placeholders.Paragraph()).LineHeight(1.5f); + column.Item().Text("W I D E").LetterSpacing(0.2f); + column.Item().Text("Condensed").LetterSpacing(-0.05f); +}); +``` + +### Word wrapping + +By default lines break at spaces. `WrapAnywhere()` lets a line break between any two characters, which is useful for long identifiers, URLs or hashes without spaces. + +```csharp +container.Width(120).Text("https://example.com/a/very/long/path/without/spaces").WrapAnywhere(); +``` + +### Text direction + +Direction methods control how a span is shaped (the order of glyphs): + +| Method | Behavior | +|---|---| +| `DirectionAuto()` | Default. The direction is detected from the text content. | +| `DirectionFromLeftToRight()` | Forces left-to-right. | +| `DirectionFromRightToLeft()` | Forces right-to-left. | + +Text direction does not change the layout direction or default alignment of the surrounding content; use `ContentFromRightToLeft()` on a container or page for that. To mix directions in one paragraph, put each part in its own span. More on right-to-left documents in [Fonts](../how-to/fonts.md#right-to-left-text). + +```csharp +container.Column(column => +{ + var text = "الجوريتم - algorithm in Arabic"; + + column.Item().Text(text).FontFamily("Noto Sans Arabic"); + column.Item().Text(text).FontFamily("Noto Sans Arabic").DirectionFromLeftToRight(); + column.Item().Text(text).FontFamily("Noto Sans Arabic").DirectionFromRightToLeft(); +}); +``` + +The example assumes the Noto Sans Arabic font is registered, see [Fonts](../how-to/fonts.md#registering-fonts). + +### Font fallback + +`Fallback` sets another style that is used for characters the primary font does not contain, for example CJK characters or emoji in a Latin font. Fallbacks can be nested. How fallback styles combine with the main style and what happens with missing glyphs is described in [Fonts](../how-to/fonts.md#font-fallback). + +```csharp +container + .DefaultTextStyle(x => x + .FontFamily(Fonts.Lato) + .Fallback(y => y.FontFamily("Noto Sans JP"))) + .Text("Latin text with 日本語 inside"); +``` + +> **Note:** Set fallbacks on a `TextStyle` (with `DefaultTextStyle` or `Style(...)`), as shown above. The span method `Fallback(...)` on `TextSpanDescriptor` currently writes into a shared style instance instead of creating a new one, which can leak the fallback into `TextStyle.Default` and other spans. Use `.Style(TextStyle.Default.Fallback(y => y.FontFamily("...")))` on a single span instead. + +## Style inheritance + +Text styles are resolved per span, from the most specific to the least specific source. A property that is set wins; only unset properties are taken from the next level: + +1. Styles set on the span itself (span methods, `Style(...)`). +2. `DefaultTextStyle` of the `Text` descriptor. +3. `DefaultTextStyle(...)` on containers wrapping the text, innermost first. +4. `page.DefaultTextStyle(...)` of the page (applies to header, content and footer, not to the page `Background()` and `Foreground()` layers). +5. `TextStyle.LibraryDefault` (see [Library defaults](#library-defaults)). + +`Style(TextStyle)` on a span overrides every property that the given style sets, including the font family; properties the style leaves unset keep their earlier value. `TextStyle.Default` is an empty style, so `x => x.FontSize(14)` in a `DefaultTextStyle` call changes only the size and inherits everything else. + +```csharp +Document.Create(document => +{ + document.Page(page => + { + page.DefaultTextStyle(x => x.FontSize(10).FontColor(Colors.Grey.Darken3)); + + page.Content() + .DefaultTextStyle(x => x.FontFamily(Fonts.NotoSans)) + .Text(text => + { + text.DefaultTextStyle(x => x.LineHeight(1.4f)); + + // Noto Sans, 10 pt, dark grey, line height 1.4, bold + text.Span("Inherited style plus bold").Bold(); + }); + }); +}).GeneratePdf("inheritance.pdf"); +``` + +Fallback styles are resolved against the main style: the fallback keeps its own font family, but every property set on the main style (after steps 1 to 4) overrides the fallback's value. A fallback property only takes effect when nothing in the main chain sets it. Library defaults never override fallback properties. For example, if the page default sets `Fallback(y => y.FontFamily("Noto Sans JP").Underline())` and a span sets `Underline(false)`, the Japanese characters in that span are not underlined either. + +## Library defaults + +Values used when nothing else sets a property (`TextStyle.LibraryDefault`, internal): + +| Property | Default | +|---|---| +| Font family | `Lato` (embedded in the library) | +| Font size | 12 pt | +| Line height | 1.2 | +| Letter spacing | 0 | +| Color | Black (`#000000`) | +| Background color | Transparent | +| Weight | Normal (400) | +| Position | Normal | +| Italic, underline, strikethrough, wrap anywhere | off | +| Direction | Auto | +| Fallback | `Noto Color Emoji` (embedded), black | + +Characters that neither your style nor its fallbacks cover are finally tried with Lato and Noto Color Emoji, which is why emoji work without configuration. See [Fonts](../how-to/fonts.md#built-in-fonts) for the full list of embedded fonts. diff --git a/docs/tutorials/getting-started.md b/docs/tutorials/getting-started.md new file mode 100644 index 000000000..296eabed8 --- /dev/null +++ b/docs/tutorials/getting-started.md @@ -0,0 +1,394 @@ +# Getting started + +In this tutorial you build a small but realistic one-page invoice with ShinyPDF: a header with a title and a date, a table of line items, a total, and a footer with page numbers. You start from an empty console application and add one piece at a time, so you see what each part of the Fluent API does. You need the .NET 10 SDK and about fifteen minutes. + +## Step 1: Create a project and install ShinyPDF + +Create a new console application and add the [ShinyPDF NuGet package](https://www.nuget.org/packages/ShinyPDF): + +```bash +dotnet new console -n InvoiceDemo +cd InvoiceDemo +dotnet add package ShinyPDF +``` + +If you develop or run on Linux (including Docker and WSL), also add the native assets for SkiaSharp and HarfBuzzSharp. Without them the application fails at the first render with `DllNotFoundException: Unable to load shared library 'libSkiaSharp'`: + +```bash +dotnet add package SkiaSharp.NativeAssets.Linux +dotnet add package HarfBuzzSharp.NativeAssets.Linux +``` + +See [Deploy on Linux and Docker](../how-to/deploy-on-linux.md) for the details. On Windows and macOS the native libraries come with ShinyPDF's dependencies. + +## Step 2: Generate your first PDF + +Replace the contents of `Program.cs` with the following code and run it with `dotnet run`: + +```csharp +using ShinyPDF.Fluent; +using ShinyPDF.Helpers; +using ShinyPDF.Infrastructure; + +Document.Create(container => +{ + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(2, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontSize(11)); + + page.Content().Text("Hello from ShinyPDF!"); + }); +}) +.GeneratePdf("invoice.pdf"); +``` + +The program writes `invoice.pdf` to the current working directory (the project folder when you use `dotnet run`). Open it in any PDF viewer. + +What the code does: + +- `Document.Create` describes a document. The lambda receives a document container, and you add pages to it. +- `container.Page(...)` defines a page set. The settings apply to every page the content needs: if your content is longer than one page, ShinyPDF adds pages automatically. +- `page.Size`, `page.Margin`, `page.PageColor` and `page.DefaultTextStyle` configure the page. Sizes and margins use points by default (1 point is 1/72 inch); pass a `Unit` such as `Unit.Centimetre` or `Unit.Millimetre` to use other units. +- `page.Content()` returns the main content slot. Every slot accepts exactly one child element, here a piece of text. +- `GeneratePdf("invoice.pdf")` lays out and renders the document. [Generate PDF, XPS and images](../how-to/generate-output.md) shows the other output options. + +The three `using` lines are the namespaces you need in almost every ShinyPDF file: `ShinyPDF.Fluent` (the API), `ShinyPDF.Helpers` (`Colors`, `PageSizes`, `Placeholders`, `Fonts`) and `ShinyPDF.Infrastructure` (`IContainer`, `Unit`, `TextStyle`). + +## Step 3: Split the page into header, content and footer + +A page has three main slots: `Header()`, `Content()` and `Footer()`. The header and footer repeat on every page; the content flows across pages. To keep the code readable, give each slot its own local function. Change the page definition like this: + +```csharp +Document.Create(container => +{ + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(2, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontSize(11)); + + page.Header().Element(ComposeHeader); + page.Content().Element(ComposeContent); + page.Footer().Element(ComposeFooter); + }); +}) +.GeneratePdf("invoice.pdf"); + +void ComposeHeader(IContainer container) +{ + container.Text("Header"); +} + +void ComposeContent(IContainer container) +{ + container.Text("Content"); +} + +void ComposeFooter(IContainer container) +{ + container.Text("Footer"); +} +``` + +`Element(...)` hands the slot's container to your function. Inside the function you build the slot's content on `container` exactly as you would inline. You fill in the three functions in the next steps. + +## Step 4: Build the header + +The header shows the invoice title and number on the left and the date on the right. A `Row` places items side by side: + +```csharp +void ComposeHeader(IContainer container) +{ + container.Row(row => + { + row.RelativeItem().Column(column => + { + column.Item().Text("Invoice #2026-0042").FontSize(20).SemiBold().FontColor(Colors.Blue.Darken2); + column.Item().Text("Shiny Consulting GmbH"); + }); + + row.ConstantItem(150).AlignRight().Text(text => + { + text.Span("Date: ").SemiBold(); + text.Span(DateTime.Today.ToString("yyyy-MM-dd")); + }); + }); +} +``` + +- `row.RelativeItem()` takes the remaining width; `row.ConstantItem(150)` is always 150 points wide. There is also `row.AutoItem()`, which is as wide as its content. +- `Column` stacks items vertically. Each `column.Item()` is a container for one child. +- `Text("...")` returns a span descriptor, so you can chain styles such as `FontSize`, `SemiBold` and `FontColor`. With `Text(text => ...)` you combine several differently styled spans in one paragraph. +- `AlignRight()` aligns the text inside the constant item. + +## Step 5: Add the line items table + +Now add the invoice data and a table. Put the data at the top of `Program.cs` (after the `using` lines) and replace `ComposeContent`: + +```csharp +var items = new[] +{ + (Name: "Workshop: document design", Quantity: 1, UnitPrice: 1200m), + (Name: "Template development (hours)", Quantity: 14, UnitPrice: 95m), + (Name: "Font licensing advice", Quantity: 2, UnitPrice: 60m), +}; + +void ComposeContent(IContainer container) +{ + container.PaddingVertical(1, Unit.Centimetre).Table(table => + { + table.ColumnsDefinition(columns => + { + columns.ConstantColumn(25); + columns.RelativeColumn(3); + columns.RelativeColumn(); + columns.RelativeColumn(); + columns.RelativeColumn(); + }); + + table.Header(header => + { + header.Cell().Element(HeaderCellStyle).Text("#"); + header.Cell().Element(HeaderCellStyle).Text("Description"); + header.Cell().Element(HeaderCellStyle).AlignRight().Text("Quantity"); + header.Cell().Element(HeaderCellStyle).AlignRight().Text("Unit price"); + header.Cell().Element(HeaderCellStyle).AlignRight().Text("Amount"); + }); + + var position = 1; + + foreach (var item in items) + { + table.Cell().Element(CellStyle).Text(position++.ToString()); + table.Cell().Element(CellStyle).Text(item.Name); + table.Cell().Element(CellStyle).AlignRight().Text(item.Quantity.ToString()); + table.Cell().Element(CellStyle).AlignRight().Text($"{item.UnitPrice:N2} EUR"); + table.Cell().Element(CellStyle).AlignRight().Text($"{item.Quantity * item.UnitPrice:N2} EUR"); + } + }); +} + +IContainer HeaderCellStyle(IContainer container) +{ + return container + .DefaultTextStyle(x => x.SemiBold()) + .BorderBottom(1) + .BorderColor(Colors.Black) + .PaddingVertical(5); +} + +IContainer CellStyle(IContainer container) +{ + return container + .BorderBottom(1) + .BorderColor(Colors.Grey.Lighten2) + .PaddingVertical(5); +} +``` + +- `ColumnsDefinition` defines the columns: `ConstantColumn(25)` is 25 points wide, and the `RelativeColumn`s share the remaining width in the ratio 3:1:1:1. +- `table.Header(...)` defines header cells. If the table ever spans several pages, the header row repeats on each page. +- `table.Cell()` adds the next cell. Cells fill the table left to right, row by row, so five cells make one row. +- `HeaderCellStyle` and `CellStyle` are plain functions that take a container, add styling (border, padding, default text style) and return the inner container. `Element(CellStyle)` applies such a function in the middle of a chain. This is the simplest way to reuse styling; [Build reusable components](../how-to/reusable-components.md) shows more options. +- `PaddingVertical(1, Unit.Centimetre)` adds space between the header, the table and the footer. + +## Step 6: Show the total + +Below the table, show the sum of all items. Wrap the table in a `Column` so you can add another item after it. Replace `ComposeContent` again: + +```csharp +// not compiled: excerpt that uses `items` and the cell style functions from step 5 +void ComposeContent(IContainer container) +{ + var total = items.Sum(x => x.Quantity * x.UnitPrice); + + container.PaddingVertical(1, Unit.Centimetre).Column(column => + { + column.Spacing(10); + + column.Item().Element(ComposeTable); + + column.Item().AlignRight().Text(text => + { + text.Span("Total: ").SemiBold(); + text.Span($"{total:N2} EUR").SemiBold().FontSize(14); + }); + }); +} +``` + +Move the `container.Table(...)` call from step 5 into a new function `void ComposeTable(IContainer container)` (without the `PaddingVertical`, which now sits on the column). `column.Spacing(10)` puts 10 points between the table and the total. The complete program at the end of this tutorial shows the result. + +## Step 7: Add page numbers to the footer + +Finally, fill the footer with a centered page number. `CurrentPageNumber()` and `TotalPages()` are special spans that ShinyPDF fills in while it renders each page: + +```csharp +void ComposeFooter(IContainer container) +{ + container.AlignCenter().Text(text => + { + text.DefaultTextStyle(x => x.FontSize(9).FontColor(Colors.Grey.Darken1)); + + text.Span("Page "); + text.CurrentPageNumber(); + text.Span(" of "); + text.TotalPages(); + }); +} +``` + +`text.DefaultTextStyle(...)` sets the style for all spans in this text block. The invoice fits on one page, so the footer reads "Page 1 of 1", but the same code works for documents of any length. [Headers, footers and page numbers](../how-to/headers-footers-page-numbers.md) covers formatting page numbers and showing a different header on the first page. + +## The complete program + +Here is the complete `Program.cs`: + +```csharp +using ShinyPDF.Fluent; +using ShinyPDF.Helpers; +using ShinyPDF.Infrastructure; + +var items = new[] +{ + (Name: "Workshop: document design", Quantity: 1, UnitPrice: 1200m), + (Name: "Template development (hours)", Quantity: 14, UnitPrice: 95m), + (Name: "Font licensing advice", Quantity: 2, UnitPrice: 60m), +}; + +Document.Create(container => +{ + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(2, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontSize(11)); + + page.Header().Element(ComposeHeader); + page.Content().Element(ComposeContent); + page.Footer().Element(ComposeFooter); + }); +}) +.GeneratePdf("invoice.pdf"); + +void ComposeHeader(IContainer container) +{ + container.Row(row => + { + row.RelativeItem().Column(column => + { + column.Item().Text("Invoice #2026-0042").FontSize(20).SemiBold().FontColor(Colors.Blue.Darken2); + column.Item().Text("Shiny Consulting GmbH"); + }); + + row.ConstantItem(150).AlignRight().Text(text => + { + text.Span("Date: ").SemiBold(); + text.Span(DateTime.Today.ToString("yyyy-MM-dd")); + }); + }); +} + +void ComposeContent(IContainer container) +{ + var total = items.Sum(x => x.Quantity * x.UnitPrice); + + container.PaddingVertical(1, Unit.Centimetre).Column(column => + { + column.Spacing(10); + + column.Item().Element(ComposeTable); + + column.Item().AlignRight().Text(text => + { + text.Span("Total: ").SemiBold(); + text.Span($"{total:N2} EUR").SemiBold().FontSize(14); + }); + }); +} + +void ComposeTable(IContainer container) +{ + container.Table(table => + { + table.ColumnsDefinition(columns => + { + columns.ConstantColumn(25); + columns.RelativeColumn(3); + columns.RelativeColumn(); + columns.RelativeColumn(); + columns.RelativeColumn(); + }); + + table.Header(header => + { + header.Cell().Element(HeaderCellStyle).Text("#"); + header.Cell().Element(HeaderCellStyle).Text("Description"); + header.Cell().Element(HeaderCellStyle).AlignRight().Text("Quantity"); + header.Cell().Element(HeaderCellStyle).AlignRight().Text("Unit price"); + header.Cell().Element(HeaderCellStyle).AlignRight().Text("Amount"); + }); + + var position = 1; + + foreach (var item in items) + { + table.Cell().Element(CellStyle).Text(position++.ToString()); + table.Cell().Element(CellStyle).Text(item.Name); + table.Cell().Element(CellStyle).AlignRight().Text(item.Quantity.ToString()); + table.Cell().Element(CellStyle).AlignRight().Text($"{item.UnitPrice:N2} EUR"); + table.Cell().Element(CellStyle).AlignRight().Text($"{item.Quantity * item.UnitPrice:N2} EUR"); + } + }); +} + +IContainer HeaderCellStyle(IContainer container) +{ + return container + .DefaultTextStyle(x => x.SemiBold()) + .BorderBottom(1) + .BorderColor(Colors.Black) + .PaddingVertical(5); +} + +IContainer CellStyle(IContainer container) +{ + return container + .BorderBottom(1) + .BorderColor(Colors.Grey.Lighten2) + .PaddingVertical(5); +} + +void ComposeFooter(IContainer container) +{ + container.AlignCenter().Text(text => + { + text.DefaultTextStyle(x => x.FontSize(9).FontColor(Colors.Grey.Darken1)); + + text.Span("Page "); + text.CurrentPageNumber(); + text.Span(" of "); + text.TotalPages(); + }); +} +``` + +Run `dotnet run` and open `invoice.pdf`. You see the blue invoice title with the date on the right, the table with a bold header row and thin grey row separators, the total below the table, and "Page 1 of 1" at the bottom of the page. + +While you design a layout, `Placeholders` gives you sample content, for example `Placeholders.Sentence()`, `Placeholders.Paragraph()` or `Placeholders.Image(200, 100)`. Placeholder text is random, so do not use it in tests that compare output. + +## Next steps + +- [Generate PDF, XPS and images](../how-to/generate-output.md): write to a stream (for example in a web API), render pages as PNG images, set document metadata, and move your document into an `IDocument` class. +- [Headers, footers and page numbers](../how-to/headers-footers-page-numbers.md): page sizes, orientation, margins and numbering. +- [Use custom fonts](../how-to/fonts.md): register your own fonts. +- [Build reusable components](../how-to/reusable-components.md): turn parts of a document into components. +- [Debug layout issues](../how-to/debug-layout-issues.md): what to do when content does not fit. +- [Deploy on Linux and Docker](../how-to/deploy-on-linux.md): run ShinyPDF in containers. +- [Elements reference](../reference/elements.md) and [Text reference](../reference/text.md): all available elements and text options. +- [Layout engine](../explanation/layout-engine.md): how ShinyPDF measures, pages and draws content.