Rask
Rask
v0.23.1-alpha.0.458
You're reading the Rask v0.23.1-alpha.0.458 guides. Updated .View source
All guides

Rask.Pdf — a page or component to PDF

Rask.Pdf prints a page of your app — or a component written only for print — to PDF, exactly as the user who asks for it sees it. The page is rendered on the server as its first GET would be, and drawn by a headless Chromium beside the app: real fonts and every script, a table heading repeated on every page, a footer line and page numbers.

dotnet add package Rask.Pdf

A RaskApp turns it on when the app references the package. A hand-wired host calls builder.Services.AddRaskPdf().

using Rask.Printing;

// A button on the report page.
Ui.Button.OnClick(async () =>
    Download.File("visszaterites.pdf", await Pdf.From(Routes.RefundReport(id)), Pdf.ContentType))["Letöltés PDF-ben"]

Pdf.From(route) takes a route of the app (Routes.X(...)), never a URL from user input. It:

  1. Checks the page's [Authorize] for the caller — the session's user in a handler or a render, the request's user in a minimal-API endpoint, the enqueuing user in a job. A page they could not open is never printed: UnauthorizedAccessException.
  2. Renders it as its first GET does — the same layouts, the same data, in a session of its own that is dropped afterwards. No live connection, no runtime script. A page that redirects, or answers 404 or 500, is not printed.
  3. Prints it in a browser context of its own, with the app's stylesheet, the kit's sheet and the fonts served from the app's web root. Nothing else is reachable — another host, an API, a file the app does not serve — and no script on the page runs.

A page's data is read before its first render is printed

The page prints from that first GET: there is no socket, so nothing arrives after it. Rask renders the page in waves — render, wait for the async work that render started (every OnMount and OnUpdated, a query's first read), render again — for up to Rask:Server:QuiescenceTimeout (5 s) in all, and prints the last wave. So a report that awaits its data in OnMount prints the data, not its "Loading…":

protected override async Task OnMount() => _rows = await Refunds.For(Quarter);

What that wait cannot cover is printed as it stands: work the page detaches (_ = Poll()), work waiting on the browser (a JavaScript call never returns here), and anything still out when the budget is spent. A page that loads its data some other way — a timer, a click — prints its placeholder. The budget is the server's for every first GET; Rask:Pdf:Timeout is a separate limit on the print itself.

The page is rendered in the caller's language when the app ships it (AddRaskCulture), and in the app's default otherwise.

The same call in a minimal-API endpoint:

app.MapGet("/refunds/{id}.pdf", async (Guid id) =>
    Results.File(await Pdf.From(Routes.RefundReport(id)), Pdf.ContentType, "visszaterites.pdf"));

Or a component written for print

var receipt = await Pdf.From(Receipt.Order(order));

The component is rendered as a whole document of the app: its shell, its stylesheet, its fonts. There is no route, so no [Authorize] to run: the caller already holds the data it was given.

Write the report with Flux pieces

A report is an ordinary component, and the kit already knows what paper is:

  • Controls are left off. Every Ui.Button, a badge's ×, Ui.Pagination, Ui.Sidebar and its toggle, Ui.Header, toasts, tooltips, open dropdowns and popovers, and a modal's backdrop print as nothing, and Ui.Main drops its screen padding and panel.
  • Ui.Table prints whole. Cells wrap instead of being cut, the scroll frame lets the table run on, its heading repeats at the top of every page, and a row is never split across two.
  • Ui.Accordion prints open — every item's content, closed ones included, with no chevron.

Hide your own parts with Tailwind's print:hidden; show something the kit hides with an important utility, print:inline-flex!. None of this touches the screen: every rule is inside @media print, so the same defaults apply when a reader prints the page from their browser.

The report is an ordinary component written with Flux pieces. On a server, Pdf.From(Routes.RefundReportPage()) renders it as its first GET would, for the user who clicked, and prints it with headless Chromium: the table heading repeats on every page by itself. rask.sh is static files, so the button downloads the PDF Rask.Pdf printed from this very component.

using System.Globalization;

namespace Rask.Site.Features;

// A report written with Flux pieces, as an app writes one to print: Pdf.From(Routes.RefundReport(id)) on a server
// turns exactly this into the PDF the demo downloads. The table's heading is a <thead>, so Chromium repeats it on
// every printed page by itself; break-inside-avoid keeps a row from being cut across two.
public sealed partial class RefundReport : Component
{
    private static readonly NumberFormatInfo Hungarian = new() { NumberGroupSeparator = " ", NumberDecimalSeparator = "," };

    protected override Component? Render() =>
        Div.Class("refund-report")[
            Ui.Heading.Level(1).Size(Ui.HeadingSize.Xl)["Gázolaj-visszatérítés — 2026. III. negyedév"],
            Ui.Text.Class("mb-4")["Kovács és Társa Fuvarozó Kft. · Győr · jövedéki szám HU00012345"],
            Ui.Table[
                Ui.TableColumns[
                    Ui.TableColumn["Dátum"],
                    Ui.TableColumn["Számlaszám"],
                    Ui.TableColumn["Kút"],
                    Ui.TableColumn.Align(Ui.Align.End)["Liter"],
                    Ui.TableColumn.Align(Ui.Align.End)["Visszatérítés"]
                ],
                RefundData.Vehicles.Select(Group)
            ],
            Ui.Heading.Level(2).Size(Ui.HeadingSize.Lg).Class("mt-6")[
                $"Összesen: {Amount(RefundData.Vehicles.Sum(v => v.Rows.Sum(r => r.Refund)))} Ft"]
        ];

    private static Component Group(RefundData.Vehicle vehicle) =>
        Ui.TableRows.Key(vehicle.Plate)[
            Ui.TableRow.Class("break-inside-avoid")[
                Ui.TableCell.Variant(Ui.TableCellVariant.Strong)[vehicle.Plate],
                Ui.TableCell.Variant(Ui.TableCellVariant.Strong)[vehicle.Model],
                Ui.TableCell[""],
                Ui.TableCell[""],
                Ui.TableCell[""]
            ],
            vehicle.Rows.Select(row => Ui.TableRow.Key(row.Invoice).Class("break-inside-avoid")[
                Ui.TableCell[row.Day.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture)],
                Ui.TableCell[row.Invoice],
                Ui.TableCell[row.Station],
                Ui.TableCell.Align(Ui.Align.End)[row.Litres.ToString("N1", Hungarian)],
                Ui.TableCell.Align(Ui.Align.End)[$"{Amount(row.Refund)} Ft"]
            ]),
            Ui.TableRow.Class("break-inside-avoid")[
                Ui.TableCell[""],
                Ui.TableCell[""],
                Ui.TableCell.Variant(Ui.TableCellVariant.Strong)[$"{vehicle.Plate} összesen"],
                Ui.TableCell.Align(Ui.Align.End).Variant(Ui.TableCellVariant.Strong)[
                    vehicle.Rows.Sum(r => r.Litres).ToString("N1", Hungarian)],
                Ui.TableCell.Align(Ui.Align.End).Variant(Ui.TableCellVariant.Strong)[
                    $"{Amount(vehicle.Rows.Sum(r => r.Refund))} Ft"]
            ]
        ];

    private static string Amount(decimal value) => value.ToString("N0", Hungarian);
}
Live result
Letöltés PDF-ben

Gázolaj-visszatérítés — 2026. III. negyedév

Kovács és Társa Fuvarozó Kft. · Győr · jövedéki szám HU00012345

Dátum
Számlaszám
Kút
Liter
Visszatérítés
ŐRZ-104Volvo FH 500
2026.07.05SZ-10001-ŐRZTatabánya, Csaba utca228,57 632 Ft
2026.07.08SZ-10002-ŐRZBudaörs, Szabadság út265,58 868 Ft
2026.07.11SZ-10003-ŐRZGyőr, Fehérvári út302,510 104 Ft
2026.07.14SZ-10004-ŐRZMosonmagyaróvár, Lajta út339,511 339 Ft
2026.07.17SZ-10005-ŐRZTatabánya, Csaba utca376,512 575 Ft
2026.07.20SZ-10006-ŐRZBudaörs, Szabadság út193,56 463 Ft
2026.07.23SZ-10007-ŐRZGyőr, Fehérvári út230,57 699 Ft
2026.07.26SZ-10008-ŐRZMosonmagyaróvár, Lajta út267,58 934 Ft
2026.07.29SZ-10009-ŐRZTatabánya, Csaba utca304,510 170 Ft
2026.08.01SZ-10010-ŐRZBudaörs, Szabadság út341,511 406 Ft
2026.08.04SZ-10011-ŐRZGyőr, Fehérvári út378,512 642 Ft
2026.08.07SZ-10012-ŐRZMosonmagyaróvár, Lajta út195,56 530 Ft
2026.08.10SZ-10013-ŐRZTatabánya, Csaba utca232,57 766 Ft
2026.08.13SZ-10014-ŐRZBudaörs, Szabadság út269,59 001 Ft
2026.08.16SZ-10015-ŐRZGyőr, Fehérvári út306,510 237 Ft
ŐRZ-104 összesen4 232,5141 366 Ft
ÜGY-731Scania R 450
2026.07.06SZ-20001-ÜGYBudaörs, Szabadság út239,57 999 Ft
2026.07.09SZ-20002-ÜGYGyőr, Fehérvári út276,59 235 Ft
2026.07.12SZ-20003-ÜGYMosonmagyaróvár, Lajta út313,510 471 Ft
2026.07.15SZ-20004-ÜGYTatabánya, Csaba utca350,511 707 Ft
2026.07.18SZ-20005-ÜGYBudaörs, Szabadság út387,512 942 Ft
2026.07.21SZ-20006-ÜGYGyőr, Fehérvári út204,56 830 Ft
2026.07.24SZ-20007-ÜGYMosonmagyaróvár, Lajta út241,58 066 Ft
2026.07.27SZ-20008-ÜGYTatabánya, Csaba utca278,59 302 Ft
2026.07.30SZ-20009-ÜGYBudaörs, Szabadság út315,510 538 Ft
2026.08.02SZ-20010-ÜGYGyőr, Fehérvári út352,511 774 Ft
2026.08.05SZ-20011-ÜGYMosonmagyaróvár, Lajta út389,513 009 Ft
2026.08.08SZ-20012-ÜGYTatabánya, Csaba utca206,56 897 Ft
2026.08.11SZ-20013-ÜGYBudaörs, Szabadság út243,58 133 Ft
2026.08.14SZ-20014-ÜGYGyőr, Fehérvári út280,59 369 Ft
2026.08.17SZ-20015-ÜGYMosonmagyaróvár, Lajta út317,510 604 Ft
ÜGY-731 összesen4 397,5146 876 Ft
ÁRV-215MAN TGX 18.510
2026.07.07SZ-30001-ÁRVGyőr, Fehérvári út250,58 367 Ft
2026.07.10SZ-30002-ÁRVMosonmagyaróvár, Lajta út287,59 602 Ft
2026.07.13SZ-30003-ÁRVTatabánya, Csaba utca324,510 838 Ft
2026.07.16SZ-30004-ÁRVBudaörs, Szabadság út361,512 074 Ft
2026.07.19SZ-30005-ÁRVGyőr, Fehérvári út398,513 310 Ft
2026.07.22SZ-30006-ÁRVMosonmagyaróvár, Lajta út215,57 198 Ft
2026.07.25SZ-30007-ÁRVTatabánya, Csaba utca252,58 434 Ft
2026.07.28SZ-30008-ÁRVBudaörs, Szabadság út289,59 669 Ft
2026.07.31SZ-30009-ÁRVGyőr, Fehérvári út326,510 905 Ft
2026.08.03SZ-30010-ÁRVMosonmagyaróvár, Lajta út363,512 141 Ft
2026.08.06SZ-30011-ÁRVTatabánya, Csaba utca180,56 029 Ft
2026.08.09SZ-30012-ÁRVBudaörs, Szabadság út217,57 264 Ft
2026.08.12SZ-30013-ÁRVGyőr, Fehérvári út254,58 500 Ft
2026.08.15SZ-30014-ÁRVMosonmagyaróvár, Lajta út291,59 736 Ft
2026.08.18SZ-30015-ÁRVTatabánya, Csaba utca328,510 972 Ft
ÁRV-215 összesen4 342,5145 039 Ft

Összesen: 433 281 Ft

Options

var pdf = await Pdf.From(Routes.RefundReport(id), new PdfOptions
{
    Page = PdfPage.A4.Landscape,                 // PdfPage.A4 (upright) unless set
    Margins = PdfMargins.Millimetres(15),        // or Millimetres(vertical, horizontal); 10 mm unless set
    Footer = "Visszatérítés — 2026. III. negyedév",
    PageNumbers = true,                          // "1 / 5" at the foot, right; on unless switched off
    Tagged = false,                              // an accessible, tagged PDF; off unless asked for
});
  • Footer is printed as text — markup in it is shown, never run — at the foot of every page, left.
  • PageNumbers are written from the PdfPageNumber catalog text ({0} the page, {1} the count) in the caller's language: 1 / 5 in English, 1 / 5 oldal in Hungarian. Translate or reword it in your own Resources/RaskStrings.{culture}.json like any framework text.
  • Tagged writes the document's structure (headings, tables, reading order) for screen readers. It is most of the file: a 209-page table comes out 12 MB tagged and 0.45 MB untagged, and prints in twice the time.

The browser

Rask.Pdf prints with the Chrome for Testing headless shell, at one pinned version with a SHA-256 per platform (build/Rask.Pdf.ChromePin.props in the package). It is never downloaded by the running app:

Where How the browser gets there
Your machine, rask dev, dotnet test, CI Building an app that references Rask.Pdf fetches the pinned shell into the user cache — ~/Library/Caches/Rask, ~/.cache/rask (or $XDG_CACHE_HOME/rask), %LOCALAPPDATA%\Rask\Cache — once per version, after checking its SHA-256.
The container image The scaffolded Dockerfile has dotnet publish fetch it into the build stage, copies it to /opt/rask/chrome, and installs the libraries it links against plus fonts — only when the app references Rask.Pdf. About 510 MB more on aspnet:10.0.
Anywhere else Install Chromium or Google Chrome and set Rask:Pdf:ChromiumPath; the build's fetch is off with <RaskPdfDownloadChromium>false</RaskPdfDownloadChromium>.

An app scaffolded before Rask.Pdf existed adds the same lines to its Dockerfile: copy them from any template's (src/Rask.Templates/server/Dockerfile). The pin moves with Rask's daily dependency run, only ever to a Stable release at least a week old.

Without a browser, the first print fails with what to do — never a download.

Settings

Rask:Pdf in appsettings.json (or Rask__Pdf__… in the environment):

Setting Default
ChromiumPath the pinned shell, then the usual install folders and PATH A browser of your own.
NoSandbox false Starts Chromium with --no-sandbox. Only for a host whose kernel forbids the sandbox's user namespaces; the documents printed are always the app's own.
MaxConcurrentPrints 2 How many documents print at once; the rest wait. Memory is the bound: a 5,000-row table peaks its renderer at about 1.2 GB.
Timeout 00:00:30 How long one document may take; longer is a TimeoutException.
MaxPages 1000 The longest document that prints; a longer one fails rather than come out cut.

How it runs

One headless Chromium per app, started at the first print and kept: a browser that crashed is replaced at the next print, and stopping the host closes it. Rask drives it over its own DevTools pipe (--remote-debugging-pipe) — no Puppeteer, no Playwright, no NuGet dependency. A 5,000-row table (139 A4 landscape pages, heading on every page) prints in under a second on Linux.

Linux and macOS for now.

Confirm