NETReporter 3.3.0

NETReporter

Knihovna pro generování reportů (PDF, SVG, HTML, JSON) z deklarativních JSON šablon a JSON dat.

Šablona popisuje stránku, sekce, rozvržení, styly a bindingy na data; knihovna ji zvaliduje, vyhodnotí nad daty a vykreslí. PDF a SVG kreslí QuestPDF, grafy OxyPlot.

Instalace

Balíček je v privátním NuGet feedu Advisor Software:

dotnet nuget add source https://nuget.advisor-soft.com/v3/index.json --name AdvisorSoft
dotnet add package NETReporter

Požadavky: .NET 10.

Rozhraní (IReportRenderer, ITemplateTools) a sdílené typy (RenderOptions, diagnostiky…) jsou v balíčku NETReporter.Abstractions, který se nainstaluje sám. Aplikace, která report jen zadává a vykreslí ho ReportServer, použije místo této knihovny tenký klient NETReporter.Client (bez QuestPDF a dalších těžkých závislostí; přihlášení API tokenem): viz docs/user/pouziti-knihovny.md a průvodce klientem v repozitáři.

Rychlý start

using NetReporter;

var renderer = new ReportRenderer();

await using var template = File.OpenRead("template.json");
await using var data = File.OpenRead("data.json");
await using var pdf = await renderer.RenderAsync(template, data);   // výchozí formát je PDF

await using var file = File.Create("report.pdf");
await pdf.CopyToAsync(file);

S DI kontejnerem (ReportRenderer se registruje jako singleton):

services.AddNetReporter();                        // ReportRenderer, TemplateSchemaParser, TemplateAnalyzer, VisualTreeBuilder
services.AddNetReporter<MyPipelineObserver>();    // + vlastní IReportPipelineObserver

var renderer = serviceProvider.GetRequiredService<ReportRenderer>();

RenderAsync bere šablonu a data jako Stream a vrací Stream s výstupem (pozice 0). Při neplatné šabloně vyhodí InvalidOperationException s diagnostikami (zpráva + JSON cesta).

Šablona v kostce

{
  "version": "1.0",
  "meta": { "name": "Faktura", "culture": "cs-CZ", "palette": { "primary": "#1A73E8" } },
  "page": { "size": "A4", "orientation": "Portrait", "marginsMm": { "top": 15, "right": 12, "bottom": 15, "left": 12 } },
  "styles": {
    "base": { "font": "Arial", "fontSize": 9 },
    "h1": { "fontSize": 16, "bold": true, "color": "primary" }
  },
  "content": {
    "root": {
      "type": "Stack",
      "gapMm": 3,
      "items": [
        { "type": "Text", "text": "Faktura {{$.Number}}", "styleRef": "h1" },
        { "type": "Text", "text": "Vystaveno: {{$.Issued:dd.MM.yyyy}}, celkem {{$.Total:C2}}" },
        {
          "type": "Table",
          "columns": [
            { "key": "name", "header": "Položka", "width": "3*" },
            { "key": "price", "header": "Cena", "width": "1*", "align": "Right" }
          ],
          "rows": {
            "source": "$.Items",
            "as": "item",
            "cells": {
              "name": { "text": "{{item.Name}}" },
              "price": { "text": "{{item.Price:N2}}" }
            }
          }
        }
      ]
    }
  },
  "pageFooter": {
    "root": { "type": "PageNumbers", "mode": "Current", "showTotal": true, "separator": " / " }
  }
}
  • Sekce: pageBackground, pageHeader, reportHeader, content, reportFooter, firstPageFooter, pageFooter, pageOverlay.
  • Vodoznak a razítko: kořenové watermark (text nebo obrázek, podmínka visibleIf, průhlednost v barvě) na každé stránce PDF, SVG, HTML i JSON; z kódu RenderOptions.Watermark.
  • Rozvržení: Stack, Row, Box, Absolute, Spacer, PageBreak; libovolný uzel lze opakovat nad kolekcí přes "repeat": { "source": "$.Items", "as": "item" }; "keepTogether": true (u Stack, Row, Box, Text, Paragraph, Table) zabrání zalomení uzlu přes stránku – nevejde-li se na zbytek stránky, přesune se celý na další.
  • Obsah: Text, Paragraph, HttpLink, Image, Line, PageNumbers, Barcode, Code2D (QR, DataMatrix…), PaymentQr (QR platba SPAYD a EPC s kontrolou IBAN), Checkbox, Emoji (barevné emoji Twemoji jako vektor), Icon (ikony FontAwesome Free, s licencí i Pro, jako vektor), Table (skupiny, souhrny, stránkování), Chart.
  • Binding: {{$.Cesta.K.Hodnote}} (kořen dat), {{alias.Pole}} (položka repeat/tabulky/grafu), formát za dvojtečkou ({{$.Total:N2}}, {{$.Date:dd.MM.yyyy}}), C# výrazy s prefixem # ({{#item.Qty * item.Price}}), systémové proměnné now, page.number, page.total. Formátuje se podle meta.culture.
  • Styly: pojmenované styly v styles (base platí pro vše), odkaz styleRef a inline style; písmo, barvy (#RRGGBB nebo název z meta.palette), okraje a výplně v mm, rámečky, zarovnání. Styl lze vybrat podle dat – styleRef může obsahovat binding, jehož výsledkem je jméno stylu ("styleRef": "{{# item.Overdue ? \"overdue\" : \"\" }}"); tabulky mají styl řádku rows.styleRef a buňky rows.cells.<key>.styleRef / style.

Kompletní popis formátu je v přibalené dokumentaci (viz níže) – kapitola 02_template_format.md.

Grafy

{
  "type": "Chart",
  "chartType": "Column",
  "source": "$.Months",
  "as": "m",
  "category": "{{m.Month}}",
  "series": [
    { "name": "2025", "value": "m.Revenue", "color": "primary" },
    { "name": "Plán", "value": "#m.Revenue * 1.1", "dashed": true }
  ],
  "valueAxis": { "format": "N0" },
  "legend": { "position": "Top" }
}

Typy Column, Bar, Line, Area, Pie, Donut; více sérií, paleta, legenda, osy, popisky hodnot, skládání. Kombinovaný graf: série může přepsat typ ("type": "Line") a čáry mohou mít druhou osu vpravo ("axis": "Secondary" + secondaryValueAxis). Grafy jsou vektorové ve všech výstupech. Podrobnosti: 02_template_format.md §7.11 a 17-charts.md.

Výstupní formáty

await using var output = await renderer.RenderAsync(template, data, new RenderOptions { Format = RenderFormat.Svg });
Formát Popis
RenderFormat.Pdf výchozí; plnohodnotné PDF se stránkováním
RenderFormat.Svg stejný layout a stránkování jako PDF; jeden SVG se stránkami pod sebou (data-pages, <svg data-page="N">)
RenderFormat.Html HTML náhled bez stránkování (vhodný pro editor/prohlížeč)
RenderFormat.Json strukturovaný výsledek renderu – vyhodnocené texty, data grafů, metadata obrázků a kódů; pro automatické a AI kontroly
RenderFormat.Webp obrázek stránky se stejným layoutem jako PDF (ztrátový, bílé pozadí); RenderAsync vrací první stránku, všechny RenderWebpPagesAsync
RenderFormat.Png obrázek stránky bezeztrátově (bílé neprůhledné pozadí); RenderAsync vrací první stránku, všechny RenderPngPagesAsync
RenderFormat.Jpeg obrázek stránky ztrátově, bez průhlednosti; RenderAsync vrací první stránku, všechny RenderJpegPagesAsync

Každou SVG stránku zvlášť vrací RenderSvgPagesAsync:

IReadOnlyList<string> pages = await renderer.RenderSvgPagesAsync(template, data);

Do SVG se nevkládají fonty – prohlížeč použije písmo nainstalované v systému (pozice znaků zůstanou zachované).

Každou stránku jako obrázek WebP vrací RenderWebpPagesAsync (rozlišení RenderOptions.ImageDpi 1–600, výchozí 288, kvalita ImageQuality; všechny stránky jsou najednou v paměti):

IReadOnlyList<byte[]> pages = await renderer.RenderWebpPagesAsync(template, data, new RenderOptions { ImageDpi = 150 });

Stejně funguje RenderPngPagesAsync (bezeztrátově, ImageQuality se ignoruje) a RenderJpegPagesAsync (ztrátově, kvalita ImageQuality).

Ladění šablon: new RenderOptions { Format = RenderFormat.Html, RenderBindingsAsExpressions = true } zobrazí místo hodnot samotné binding výrazy. RenderOptions.DebugMode přidá k HTML výstupu ladicí atributy (data-nr-type s typem uzlu u prvků s id), ladicí CSS a skript pro vizuální editor šablon; výstup bez něj se nemění.

Limit stránek: PDF/SVG render skončí RenderPageLimitExceededException po překročení RenderOptions.MaxPages (výchozí 10 000, 0 = bez limitu) – ochrana před šablonou se zacykleným layoutem. CancellationToken se kontroluje i během generování stránek. Všechny formáty se renderují souběžně: od QuestPDF 2026.9 je souběžné generování PDF/SVG v jednom procesu bezpečné a ReportRenderer ho neserializuje, takže propustnost roste s počtem vláken.

Metadata PDF

Vlastnosti dokumentu (Název, Autor, Předmět, Klíčová slova, Aplikace, Jazyk) se nastavují v meta.document. Hodnoty jsou texty s bindingy vyhodnocené nad daty; prázdný výsledek se nezapíše, klíčová slova mohou být pole a bez language se použije meta.culture. Metadata se zapisují jen do PDF.

"meta": {
  "name": "Faktura",
  "culture": "cs-CZ",
  "document": { "title": "Faktura {{$.Number}}", "author": "{{$.Supplier}}", "keywords": [ "faktura", "{{$.Number}}" ] }
}

Podrobnosti: 02_template_format.md §1.4.

Profesionální PDF

Hlavičkový papír, připojená PDF, PDF/A, přílohy a šifrování se doplňují do PDF po vygenerování (post-processing přes qpdf z balíčku QuestPDF; šablony bez těchto funkcí dávají stejné PDF jako dřív).

Hlavičkový papír a připojená PDF (šablona; fragment): page.letterhead vloží stránku PDF pod obsah stránek, meta.document.append připojí PDF za dokument. Zdroj je cesta k souboru, http(s) URL nebo data URI, nejčastěji {{$.Assets.Název}}:

"meta": { "assets": { "Hlavicka": "data:application/pdf;base64,JVBERi0…" },
          "document": { "append": [ "https://firma.cz/vop.pdf" ] } },
"page": { "size": "A4", "orientation": "Portrait", "marginsMm": { "top": 40, "right": 15, "bottom": 25, "left": 15 },
          "letterhead": { "src": "{{$.Assets.Hlavicka}}", "pages": "FirstAndRest" } }

pages: All (výchozí), First, FirstAndRest (první stránka 1. stránkou podkladu, ostatní 2. stránkou). Nedostupný zdroj render nezastaví: PDF vznikne bez něj a RenderOptions.OnBindingDiagnostics (nebo pozorovatel) dostane varování NRP036 s JSON cestou zdroje; kontrolujte je, než fakturu odešlete. Server, který renderuje cizí šablony, má nastavit ResourcePolicy = ResourcePolicy.Server (zdroje ze souborů zakázané, stahování jen z veřejných adres, limity velikosti a času).

PDF/A-3b s přílohou (ISDOC), vstupní data v PDF a heslo:

var options = new RenderOptions
{
    PdfConformance = PdfConformance.PdfA3B,
    Attachments = [ IsdocWriter.CreateAttachment(isdocInvoice) ],   // ISDOC z dat faktury (NetReporter.Isdoc), nebo vlastní new PdfAttachment { ... }
    EmbedData = true,                                   // vstupní JSON jako data.json (null = podle meta.document.embedData)
};

// šifrování (AES-256) nejde spojit s PDF/A
var secured = new RenderOptions { Encryption = new PdfEncryption { UserPassword = "ctenar", OwnerPassword = "vlastnik", AllowModification = false } };

PDF/A-2b přílohy nepovoluje. Volby PDF u jiného formátu než PDF a neslučitelné volby jsou ArgumentException. IsdocWriter složí ISDOC 6.0.2 z IsdocInvoice (součty a rekapitulaci DPH dopočítá, UUID je stabilní, výstup je ověřený proti oficiálnímu XSD); PDF/A ověřte nástrojem veraPDF. Podrobnosti: user/pouziti-knihovny.md a dev/07_renderer_questpdf.md.

Validace šablony

Šablonu lze zkontrolovat bez renderu. Parser ji ověří proti vestavěnému schématu a vrátí diagnostiky s JSON cestou k chybě:

using NetReporter.Common;
using NetReporter.Template.Parsing;

var parser = new TemplateSchemaParser();                  // výchozí ParserValidationMode.Strict
var result = parser.Parse(templateJson);

if (!result.Success)
{
    foreach (var d in result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error))
        Console.WriteLine($"{d.JsonPath}: {d.Message} ({d.Code})");
}
  • Strict hlásí neznámé vlastnosti a chybný obsah stylů (neznámá vlastnost, špatný typ, neplatný enum) jako chybu, Compat jako varování (new TemplateSchemaParser(validationMode: ParserValidationMode.Compat); při renderu new RenderOptions { ValidationMode = ParserValidationMode.Compat }, varování pak dostane pozorovatel v ParseCompletedContext.Diagnostics).
  • Diagnostiky mají stabilní Code a JsonPath (např. $.content.root.items[2].rows.source).
  • Schéma šablon (typy uzlů, vlastnosti, enumy) vrací TemplateSchemaProvider.Create().

Podrobnosti: kapitola 03_parser_and_validation.md.

Data, která šablona čte

Chybějící datové pole se vykreslí prázdné a nikdo o tom nezjistí. Analyzátor zjistí ze šablony (bez renderu), která pole dat čte, a složí z nich JSON Schema nebo ukázková data:

using NetReporter.Template.DataContract;

var contract = new TemplateDataContractAnalyzer().Analyze(templateJson);
foreach (var field in contract.EnumerateFields())
    Console.WriteLine($"{field.Path}  {field.Kind}");          // $.Invoice.Items[*].Total  Number

string schema = contract.ToJsonSchema();                        // JSON Schema 2020-12, bez povinných polí
string sample = new TemplateSampleDataGenerator().GenerateJson(contract);   // deterministická ukázková data

Druh pole se odvodí z použití, formátu a názvu (názvový odhad kontrakt označí); C# výrazy {{# …}} se čtou jen podle zápisu. Podrobnosti: user/pouziti-knihovny.md a kapitola 05_binding_system.md.

Sledování pipeline

IReportPipelineObserver dostává události OnPipelineStarted, OnParseStarted/Completed, OnRenderStarted/Completed (doba, velikost výstupu) a OnError (fáze, kategorie, stabilní kód chyby) – vhodné pro metriky a logování. Registruje se přes AddNetReporter<TObserver>() nebo konstruktor ReportRenderer. Knihovna loguje přes ILogger, pokud předáte ILoggerFactory.

RenderCompletedContext v OnRenderCompleted nese také BindingDiagnostics a OmittedBindingDiagnostics: chyby v bindingech (nedokončené {{, nečíselná hodnota grafu…) render nezastaví, takže je jinak neuvidíte. Shodné diagnostiky se slučují a počet omezuje RenderOptions.MaxBindingDiagnostics (výchozí 200, 0 sběr vypne). Při selhání renderu je (do chyby nasbírané) nese i PipelineError v OnError.

Dokumentace v balíčku

Balíček obsahuje dokumentaci knihovny a šablon ve složce docs/ (po instalaci v %USERPROFILE%\.nuget\packages\netreporter\<verze>\docs\, na Linuxu ~/.nuget/packages/netreporter/<verze>/docs/). Rozcestník je docs/README.md:

Soubor Obsah
docs/user/pouziti-knihovny.md průvodce použitím knihovny krok za krokem
docs/dev/02_template_format.md formát šablony – sekce, uzly, vlastnosti, repeat, tabulky, grafy
docs/dev/03_parser_and_validation.md parser a validace šablon, diagnostiky
docs/dev/04_style_system.md stylovací systém
docs/dev/05_binding_system.md binding a výrazy
docs/dev/17-charts.md grafy – chování a omezení
docs/dev/18-emoji-a-ikony.md emoji a ikony FontAwesome – vestavěné sady, Pro a stahování verzí, licence
docs/dev/01_introduction.md, 06_runtime_pipeline.md, 07_renderer_questpdf.md, 08_program_flow_example.md, 09_extensibility.md architektura knihovny, pipeline, renderer, rozšiřitelnost

Seznam změn všech verzí je v souboru CHANGELOG.md v kořeni balíčku; změny konkrétní verze ukazuje NuGet v release notes balíčku.

Licence

MIT, © Advisor Software s.r.o.

Zdrojový kód, ukázkové šablony a změny verzí: repozitář projektu.

No packages depend on NETReporter.

- Template editor (ToDo #185): *Uložit jako…* (the *Soubor* menu, the command palette, a click on the template name in the top bar) saves the template under another name as a copy; the original stays unchanged and the new one stays in the editor. A template that is not in the store yet (a gallery sample, the empty template, an uploaded file, after *Reset*) is named at the first *Uložit* or *Publikovat*, so two new templates no longer overwrite each other silently under `nova_sablona`; an existing template of that name is overwritten only after a confirmation. - ReportServer UI (ToDo #186) fix: a template without a published version (stored only by the editor, version `#0`) now shows the badge *nepublikováno* in the list and a warning in its detail (the JSON is empty because the API returns the published version), and an error of the API (for example saving an empty JSON) is shown in the detail panel instead of silently closing the form. - Styles (ToDo #182): the new style properties `selfAlign` (`Left`, `Center`, `Right`) and `selfAlignVertical` (`Top`, `Center`, `Bottom`) align the whole node in its parent, while `align` and `alignVertical` align the content inside it. In the PDF the alignment is the outermost layer (before margin, background and border), so a box with a background and a border shrinks to its content and moves as a whole, margin included; the HTML output uses `width:fit-content` and automatic margins (a link and an image become `display:block`). The vertical variant shows only where the parent gives the node more room than it needs (a `Row` with a taller neighbour, a fixed height). `Table` and its cells ignore both properties, so a value in `styles.base` does not break the columns. `Middle` and `End` are accepted as synonyms like for `align`. New members `StyleNode.SelfAlign`, `StyleNode.SelfAlignVertical`, `ResolvedStyle.SelfAlign` and `ResolvedStyle.SelfAlignVertical`; the template schema has the enum `SelfAlign`. - Template editor (ToDo #182): the new ribbon tab *Formát* aligns the selected node with one click: horizontally (left, center, right, justify) and vertically (top, center, bottom). The buttons are icon-only with a tooltip, the button of the value that applies to the node is on (the value comes from the node's style, its `styleRef` and `styles`, and `styles.base`). The buttons *Obsah* and *Celý uzel* choose what is aligned: the content inside the node (`align`, `alignVertical`: the text of a text node, the content of a box that keeps its full width) or the whole node in its parent (`selfAlign`, `selfAlignVertical`). Nodes without content (image, barcode, 2D code, payment QR, checkbox, emoji, icon, chart) always align the whole node; the tab is off for a table, a spacer, a page break, a line, an absolute node, a section and the template root. The value is written into the style of the node as one undo step (quick repeated clicks on the same node merge), a click on the button that is on removes it, *Zrušit* removes both axes of the chosen target (a named style applies again). Like the insertion of nodes it works for a node selected in the tree, by the cursor in the JSON or in the preview. - Styles (ToDo #182) fix: `align: Justify` justified nothing, the PDF and the HTML output treated it as `Left`. Text (`Text`, `Paragraph`, a link, a table cell) is now justified in both outputs (`text-align: justify` in HTML); a container or an image still takes `Justify` as `Left`. - Template editor (ToDo #181): dockable panels like in Visual Studio. The properties of the selected node (JSON of the node, property grid, style) are a panel on the left that is reachable from every tab (they were only next to the preview); *Struktura* (the tree), *Data* (the field tree of the preview data and the data contract), *Chyby* (the diagnostics with quick fixes) and *Asistent* are separate panels, by default on the right. Every panel can be moved to the other side (the arrows in its header), a side is either pinned or auto-hidden (the pin in the header: the panel shrinks to a vertical strip of tabs at the edge, a click slides it over the center, a click outside, the Esc key or a click into the preview hides it), the panels of one side are tabs in the strip, and the width is dragged by the inner edge. The tab of *Chyby* takes the color of the most severe finding (red error, orange warning, blue information) with the number of findings. The layout is remembered per user in the browser (`localStorage`). The content of a panel is in the DOM once and only CSS classes change, so the tree, Monaco and the conversation of the assistant are never re-created when a panel is pinned, unpinned or moved. Ctrl+I and the *Asistent* button open the assistant as a panel. The preview tab no longer has the splitter with the properties; selecting a node no longer switches the tab (only the tabs *Tabulka* and *Graf* follow the node type). Because the properties now sit next to the JSON tab, *Uložit uzel* first writes the unparsed text of the JSON tab (ToDo #58). - Template editor (ToDo #181): the ribbon (Syncfusion `SfRibbon`, layout Simplified) replaces the buttons of the app bar and the toolbar above the tree: *Soubor* (load, save, publish, versions, assets, bundle, clear, reset), *Domů* (save, publish, undo, redo, copy, paste, duplicate, delete, assistant), *Vložit* (all node types and the blocks) and *Zobrazení* (the panels, the default layout, the command palette). Inserting nodes is global, not tied to the tree: it is active whenever a node is selected (in the tree, by the cursor in the JSON, in the preview). A status bar at the bottom shows the errors and warnings (a click opens *Chyby*), the saved and published state and the path to the selected node (a click on an ancestor selects it). A right click on a tree node opens a context menu (add a child, clipboard, duplicate, move, delete, save as a block, edit with the assistant). The notices (error, assistant is running, degraded template, information, the offered draft) float over the tabs and no longer push the content down. - Template editor: the new function *Vyčistit šablonu* (the button on the *Domů* tab, the *Soubor* menu, the command palette) asks for a confirmation and then reduces the template to the base of the page: the version, `meta.name`, `meta.culture`, `page.size`, `orientation`, the custom size and the margins stay; the content, the styles, the palette, the assets, the watermark, the letterhead and the document sections go, and the template gets a base style and an empty root `Stack`. One step of Undo brings the template back. - ReportServer UI (ToDo #184): the pages *Render* and *Templates* use the whole screen like the editor. *Render* is a workspace: on the left the template (stored or ad-hoc) and the data in the JSON editor (Monaco) with the output options folded below, on the right the result (the PDF is shown in the panel, an image fits the panel; tabs for the binding diagnostics and the ISDOC), a draggable divider (remembered) and a status bar with the state, format, pages, cache hit, size and time. *Templates* is master-detail: a dense filterable table on the left, the metadata, the JSON editor and the preview of the selected template on the right (a second click confirms a delete). The JSON editors are Monaco with highlighting and syntax checks (the same fallback to a `textarea` without internet). The result is handed to the browser as a stream (`DotNetStreamReference`, no base64 in the DOM). - New project `NetReporter.UiShared` (a Razor class library for both web apps): the JSON editor component `JsonEditor` with its script, its types (`JsonEditorSchema`, `JsonEditorMarker`, `JsonTextRange`...) and the divider script `nrSplit.js`. The editor moved there from `NetReporter.TemplateEditor`; the design files `nr-ui.css` and `nr-theme.js` stay in two copies (the account pages of `NetReporter.Identity` link them from the host's `wwwroot`) and `DesignAssetsSyncTests` still guards them. The Monaco surface fills its root element absolutely instead of by the percentage of height (the percentage did not resolve in a chain of flex and tab elements and Monaco stayed a few pixels high). - **Behavior change** ReportServer (ToDo #171): a bitmap (PNG, JPEG, WebP) whose longer side exceeds `ReportServer:Content:Assets:MaxImageEdgePixels` (`ReportServer__Content__Assets__MaxImageEdgePixels`, default 3508 = the longer side of A4 at 300 DPI, `0` turns it off) is stored shrunk when it is uploaded or its content is replaced (`POST /content/assets`, `PUT /content/assets/{id}/content`): the aspect ratio and the format stay, JPEG and WebP use quality 90, PNG stays lossless, transparency stays. The PDF and image renders resample an image to the page resolution anyway, but they decode the whole bitmap on every render: a 6000 × 4000 JPEG took about 265 ms and 780 MB of peak memory (PNG about 600 ms and 1.5 GB) against 20 ms and 210 MB for the shrunk 1200 × 800. The shrinking drops the metadata (EXIF including GPS) and writes the EXIF rotation into the pixels (the render honours the orientation, so the image would lie on its side without it). The asset `sha256`, `length` and `ETag` belong to the stored (shrunk) content, the original is not kept. SVG, GIF and BMP stay byte for byte. Bundle import and the moving of data URIs into assets shrink the image too, before the hash of the content is computed, so the same picture is still reused as one asset and `ExtractedAssetResponse.Length` is the length of the stored bytes. At most two images are processed at a time. New dependency `SkiaSharp` in the API (the editor already has it). - **Behavior change** ReportServer (ToDo #179): metadata are removed from every PNG, JPEG and WebP that is stored as an asset (upload, replacing the content, bundle import, data URIs moved into assets), because anybody can download a public asset and a photo carries the place where it was taken. EXIF including GPS, XMP, IPTC, comments, the text chunks of PNG and `tIME` are cut out losslessly (`ImageMetadataStripper`, the pixels stay byte for byte; the ICC profile, the Adobe marker, transparency, gamma and APNG animation stay). An image whose EXIF orientation is not "as is" is re-encoded with the rotation written into the pixels (the render honours the orientation, so the image would lie on its side without the tag). `ReportServer:Content:Assets:StripImageMetadata` (`ReportServer__Content__Assets__StripImageMetadata`, default `true`) turns it off. The dialog *Assety* of the template editor tells the user when the server stored something other than what was uploaded and shows the size before and after. - Template editor (ToDo #168): fewer and overlapping calls to the ReportServer API. At start the blocks, the themes and the sample data list load while the draft, the analysis and the preview data are prepared (they were loaded one after another). The gallery lists the sample templates, the sample data names and the user templates at the same time. Saving a template whose identifier the store already knows (it was loaded, listed or saved before) is one call instead of two, publishing and the version list too (a stale identifier falls back to the lookup by name). Saving a template under a name that is not loaded asks `GET content/templates/lookup` instead of listing every template. A gallery thumbnail resolves its card with one small call (`lookup` or `GET {id}`) instead of the full list of templates once per card. The content of the shipped sample data is kept in the editor process (keyed by the revision, 10 minutes), so thumbnails and opening a sample no longer fetch it again. `ITemplateStore` has the new members `GetInfoAsync`, `GetSharedInfoAsync` and `ExistsAsync` (default implementations over `ListAsync`). - Kubernetes manifests (ToDo #169): `deploy/k8s/03-configmap.yaml` sets `ASPNETCORE_ENVIRONMENT: Production` (it was `Development`; the editor and the MCP server already set `Production` in their Deployments, so the value matters for the API and the UI): no developer exception page that returns the stack trace to the client, the UI gets the error handler and HSTS, `/openapi/v1.json` of the API is gone. The new `Serilog__MinimumLevel__Override__Microsoft.EntityFrameworkCore.Database.Command: Warning` stops the API and the UI from logging every SQL command (the environment never controlled that: Serilog has no minimum level configured, so it was `Information` everywhere). An existing deployment keeps its own copy of the configmap: add both keys there and restart the API and the UI. - Editor metrics: the histogram `netreporter_editor_api_call_duration_seconds{operation,status}` times every call of the editor to the ReportServer API (the operation is the method and the path without identifiers, the status is `2xx` to `5xx` or `error`), the base for measuring what an action in the editor costs; each call is also logged at Debug. The Grafana dashboard `netreporter-business` has two new panels (calls per minute and p95 by operation). - Template editor (ToDo #180) fix: a color added to the palette on the tab *Stránka* did not get into the template. The cards *Color paleta* and *Assety* changed only the form model; `meta.palette` and `meta.assets` were written by the button *Uložit parametry* in another part of the tab, so until then the color was unknown to the validation, the color suggestions and the draft, and the next analysis (for example saving a style) reloaded the palette from the JSON and the color vanished. Now every change is written to the template at once as one undo step: adding a color (with a proposed name `barva1`, `barva2`…), changing it, renaming it (on leaving the field) and removing it; an asset is written once its row has a name. Only `meta.palette` or `meta.assets` is written, so the half-typed page parameters of the other cards are neither saved nor lost. Renaming a color rewrites the references to it in styles, nodes and charts; removing a color the template uses asks first; an empty or an already used name is refused and the field returns to the old name. - Kubernetes manifests (ToDo #170): the API and the editor containers in `deploy/k8s/05-workloads.yaml` get the memory limit `768Mi` and the request `256Mi` (were `512Mi` and `128Mi`; the MCP server already had these values), because a loaded FontAwesome Pro 7 pack takes about 53 MB in every process that loads it (about 106 MB with Pro 6 and 7). CPU stays as it was (installing a pack costs about 3 s of CPU). An existing deployment keeps its own copy of the manifest: raise the limits there too. - New: lightweight clients of ReportServer for **Node.js/TypeScript** (`clients/node`, `@netreporter/client`, no runtime dependencies, built-in `fetch`) and **Python** (`clients/python`, `netreporter-client`, one dependency `httpx`, sync and async API). They match `NETReporter.Client`: ad-hoc render in every format with all options (PDF/A, attachments, encryption, watermark, resolution; the options are checked locally before anything is sent), render by pages, binding diagnostics, the four template tools (validate, validate data, data contract with a local JSON Schema, sample data) and the error mapping including `Retry-After`; they authenticate with an API token (`nrp_…`). Source only for now, not published to npm or PyPI. The shared contract of the three clients lives in `clients/contract` (golden files) and the new test `ClientContractFixturesTests` checks it against `NETReporter.Client` and `NetReporter.ReportServer.Contracts`. Docs: `doc/netreporter-knihovna/user/pouziti-klienta-node.md` and `pouziti-klienta-python.md`. - ReportServer (ToDo #189, phase F1 of `.work/plan-billing.md`): a usage ledger, the new table `billing.UsageEvents` (migration `AddUsageEvents`), the base for usage overviews and billing. Every render through the API (success, failure and cache hit, written in the same save as the job's metric, with the job's id), every operation of the AI assistant (written next to `AssistantUsage` with the same id) and every render the MCP server draws itself (`render_report`, see below) is one row that is only ever added: it survives the deletion of the job, the API token and the user. It stores facts (channel API token / signed-in user / MCP / editor, the token, format, pages, bytes, cache hit, outcome, tokens and cost of the AI) and the number of documents (a successful render outside the cache is one document per started 20 pages, a failure and a cache hit are 0); no price is computed yet. The migration copies the history (render jobs with a metric and the `AssistantUsage` records) into the ledger. - **Behavior change** ReportServer: `RenderCount` and `LastRenderUtc` of the API tokens (`GET /auth/tokens`, `GET /admin/api-tokens`) and the render values of `/metrics/me`, `/metrics/users/{id}` and `/metrics/summary` are computed from the usage ledger instead of the render jobs, so they survive the deletion of jobs and users and include the renders the MCP server drew itself with the caller's token. The sums and the months are computed by the database; before, every render metric since the start was loaded into memory on each call. - ReportServer: `POST /me/usage/renders` (policy User, JWT and API token) records a render the MCP server drew itself, idempotent by `id` (`{"created": false}` on a repeat); an id of another user is `409 USAGE_ID_CONFLICT`, an invalid record `400 USAGE_INVALID_REQUEST`. New contract types `RenderUsageRecordRequest` and `UsageRecordResponse`. - MCP server: `render_report` (rendered by the MCP server itself) is recorded in the usage of the caller through `POST /me/usage/renders` when the caller is authenticated with a user API token (`nrp_…`), so the render counts for that user and token like a render through ReportServer. Static tokens, the service account and stdio record nothing. A failed record is only logged and never fails the render. - Template editor: the purchase price of the shared model, `EDITOR_AI_LOCAL_INPUT_PRICE_PER_MTOK`, `EDITOR_AI_LOCAL_OUTPUT_PRICE_PER_MTOK` and `EDITOR_AI_LOCAL_CACHE_READ_PRICE_PER_MTOK` (USD per million tokens; `Assistant:Local:InputPricePerMTok`…), gives the runs of the shared model an estimated cost, which goes into the usage (`EstimatedCostUsd`) and the run summary of the panel; without it only the tokens are recorded, as before. The cost warning (`EDITOR_AI_COST_WARNING_USD`) applies only to models paid with the user's own key. - New role **Owner** (ToDo #189, Identity migration `AddOwnerRole`): the business owner of the deployment, separate from the technical `Admin`. An admin does not get it, cannot grant it and cannot change an owner's account (roles, password, deactivation, external logins, deletion): `NetReporterUserAdministration` returns the new error `OwnerOnly`, the API `403`, and the user management pages show the role disabled for non-owners. Only an owner grants the role; the first owner comes from the new setting `REPORTSERVER_OWNER_EMAILS` (`ReportServer:OwnerEmails`, comma-separated e-mails of existing accounts that get the role at startup; the role is only added, an invalid e-mail stops the start). An owner cannot remove the role from their own account. - ReportServer: the owner section of the API (`/owner/*`, policy `InteractiveOwner`: role Owner signed in with JWT, never an API token, `Admin` alone is `403`): `GET /owner/usage/overview` (totals and a gap-free series by day or month with documents per channel: API token, web UI, MCP), `GET /owner/usage/consumers` (users by documents with the breakdown per API token, including revoked and deleted tokens and the work without a token), `GET /owner/usage/ai` (AI by model and user, split into the cost the operator pays for the shared model and the cost users pay with their own key) and `GET /owner/revenue-estimate` (an indicative monthly revenue under the draft price list of the billing plan: every user with usage gets the cheapest tariff; not an invoice). New contract types `OwnerUsageOverviewResponse`, `OwnerConsumersResponse`, `OwnerAiUsageResponse`, `OwnerRevenueEstimateResponse` and their rows; the draft price list is `DraftPriceList` in `NetReporter.ReportServer.Core`. - ReportServer UI: a separate owner section at `/owner` with its own layout and menu (Usage overview, Consumers, AI costs, Revenue estimate), reached by a briefcase icon next to the user name, visible only to owners. The overview has stat tiles, a stacked column chart of documents per channel, a column chart of the operator's AI cost and a table with the same numbers; the period is 7, 30 or 90 days by day or 12 months by month. The chart is plain HTML and CSS (`ColumnChart`) with series colors for light and dark mode and a tooltip on hover and keyboard focus. Celý seznam změn: CHANGELOG.md v balíčku a https://dev.azure.com/advsoft/Knihovny/_git/NetReporter?path=/src/NetReporter/CHANGELOG.md

Version Downloads Last updated
3.9.2 3 10/28/2026
3.9.1 1 10/27/2026
3.9.0 6 10/20/2026
3.8.1 6 10/19/2026
3.8.0 4 10/19/2026
3.7.0 4 10/19/2026
3.6.0 6 10/17/2026
3.5.1 62 10/15/2026
3.5.0 4 10/15/2026
3.4.2 5 10/15/2026
3.4.0 4 10/12/2026
3.3.0 5 10/11/2026
3.2.2 5 10/10/2026
3.2.1 4 10/10/2026
3.2.0 4 10/10/2026
3.1.1 4 10/09/2026
3.1.0 5 10/09/2026
3.0.1 8 10/08/2026
3.0.0 5 10/08/2026
2.2.1 5 10/05/2026
2.2.0 4 10/05/2026
2.1.0 9 10/04/2026
2.0.1 8 10/04/2026
2.0.0 5 10/03/2026
1.7.0 6 10/03/2026
1.6.0 5 10/02/2026
1.5.0 12 10/02/2026
1.4.0 6 10/01/2026
1.3.0 4 10/01/2026
1.2.0 5 10/01/2026
1.1.1 6 09/30/2026
1.1.0 8 09/30/2026
1.0.10 4 09/29/2026
1.0.9 4 09/29/2026
1.0.8 16 09/29/2026
1.0.7 115 09/24/2026
1.0.6 9 09/24/2026
1.0.5 7 09/24/2026
1.0.4 11 09/24/2026
1.0.3 8 09/24/2026
1.0.2 13 09/23/2026
1.0.0 7 09/23/2026
0.9.1 13 09/23/2026
0.9.0 87 09/14/2026
0.8.1 25 02/20/2026
0.8.1-preview.1 34 02/13/2026
0.7.1-preview.2 32 02/10/2026
0.7.1-preview.1 33 02/10/2026
0.7.0 30 02/10/2026
0.0.6 28 02/01/2026
0.0.5 19 02/01/2026
0.0.0-alpha.0.17 35 02/01/2026