Skip to main content

jet_print 1.1.0 copy "jet_print: ^1.1.0" to clipboard
jet_print: ^1.1.0 copied to clipboard

A Flutter library for building WYSIWYG report designers: a report model, a paginating render engine, PDF/PNG export, printing, and a themed designer.

Changelog #

All notable changes to the jet_print library are documented here. The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

1.1.0 #

Roll paper for receipt and kitchen-ticket printers, the Turkish lira sign in the bundled font, a currency symbol for ¤, and a PDF export fix for characters a font lacks. A minor release under the 1.0.0 freeze: everything is additive except the one output change under Fixed, accepted by decision 0006. One optional page key is new, and only a report that sets it is written at schema version 3; every other report is written at version 2, as before (see Compatibility).

Added #

  • The default font draws the Turkish lira sign. ₺ (U+20BA) is now in all four faces of the bundled font, so a price such as 125,50 ₺ prints on the canvas, in the preview and in PDF and PNG exports, rather than as a tofu box. Before, a text element ending in ₺ made PDF export throw. The glyph was appended to the existing Noto Sans 2.015 subsets, and every glyph that was already there is byte-for-byte unchanged, so existing reports render exactly as before. tool/fonts/add_glyphs.py adds a codepoint the same way. The package grows by about 0.5 KB.

  • RenderOptions.currencySymbol sets what ¤ prints. By default ¤ in a number pattern prints the ISO code of the render locale's currency, so the designer's Currency preset ¤#,##0.00 shows TRY1.234,50 under Locale('tr'). That stays the default, because changing it would change what existing reports print. Pass currencySymbol: '₺' to print ₺1.234,50 instead, in the format property and in FORMAT(...) alike, page headers and footers included. Only the sign changes: the pattern still sets the decimal digits (#,##0 ¤ is 1.235 ₺), and the locale still sets the separators.

  • Roll paper: pages that fit their content. PageFormat.continuous lays a report out for a receipt or kitchen-ticket printer. A continuous page never breaks because it is full: each rendered page is as tall as its header, body, footer and margins, and height becomes the shortest a page may be. The page footer follows the content instead of sitting at a fixed bottom. A group that starts a new page still does, so one report grouped by station prints one ticket per station, each cut to its own length. keepTogether has nothing to do on a roll, and a label grid lays every label on one page. The preview, PDF, PNG and print all take each page's height from the same frame, so a 203 dpi PNG for an ESC/POS printer is pageToPng(report, i, scale: 203 / 72). A page taller than 14,400 pt, the size many PDF readers refuse, still renders, with a warning.

  • The designer offers roll paper. The page section has a Continuous (roll) page switch, and the paper picker lists Roll 58 mm and Roll 80 mm after the sheet sizes, with a 50 mm minimum height. Picking a roll turns the switch on, and picking a sheet size turns it off. A roll has no landscape, so the orientation toggle is hidden on one. English, German and Turkish strings are included.

Changed #

  • The designer is no longer claimed on Android and iOS. The README now says that, on a phone or tablet, rendering, export, printing and JetReportPreview are supported, while JetReportDesigner and JetReportWorkspace run and are tested at phone width but are not yet verified on a real device. Nothing in the code changes. The 1.0.0 entry below says that version would be published only once that device pass was recorded; it was published without it, and decision 0005 narrows the claim accordingly.

  • The preview's thumbnail rail fits a taller page into its tile. Tiles still take their height from page 0. A later page that is taller, as a continuous report's longer ticket can be, is scaled down to fit its tile, keeping its proportions, instead of overflowing it. Reports whose pages are all one size look exactly as before.

Fixed #

  • PDF export no longer fails on a character the font cannot draw. A text whose last new character had no glyph in its font, such as ∑ or ✓ in the bundled default font, made JetReportExporter.toPdf throw Bad state: No element. When the character came earlier in the text the export succeeded but was garbled, with later characters drawn with another character's shape. Such a character now exports as the font's missing-glyph box (.notdef), at the width the layout gave it, and the rest of the text is drawn correctly. Copying the box's text out of the PDF yields U+0000. A tab is drawn as the space the layout measured it as. In a font that draws several characters with one shape, such as a space and a no-break space, copied text gives the lowest of them for each, whichever came first. A PDF with none of these characters draws exactly as before; its bytes change only in the width its fonts list for .notdef, which such a PDF never draws. A PDF that drew a borrowed glyph now draws .notdef in its place. That is a change in output under decision 0004, accepted as an exception by decision 0006: the old output was never the character asked for, and was not even stable.

Compatibility #

  • A roll-paper report is written at schema version 3; every other report is still written at version 2. A continuous page writes "continuous": true in the page JSON and stamps the document "schemaVersion": 3. A sheet report writes neither, so its JSON is byte-for-byte unchanged and 1.0.0 keeps opening it. 1.0.0 refuses a roll report with a ReportFormatException (its version is newer), rather than opening it as a sheet and dropping the roll when it saves. JetReportFormat.schemaVersion is now 3: the newest version this build reads and writes, not one every document carries. A host that stores the stamped version should read it from each document.

1.0.0 #

The first stable release. From here a 1.x upgrade changes no public API, no default, and nothing a report prints for a definition validate accepts; anything that would break these promises waits for 2.0, as decision 0004 defines. The pubspec declares all six Flutter platforms, each built in CI. Touch authoring in the designer is covered by widget tests at phone width (docs/touch-authoring.md); 1.0.0 is published only once the real-device pass on iOS and Android that decision 0003 requires is recorded there. The report schema is unchanged since 0.2.0, so saved reports need no migration.

Added #

  • The README states what a large report costs. Platform notes now gives measured memory figures: about 1.5 MB per 1,000 rows to lay a report out, and about 0.3 MB more per page to export it to PDF. It also advises keeping one report on a phone to tens of thousands of rows.

  • The pubspec declares all six platforms. A platforms: block lists Android, iOS, Linux, macOS, web and Windows, so pub.dev shows what this package claims instead of inferring it from the plugin dependencies. Each platform is built by a CI leg, and a test keeps the declaration and CI in step.

  • Multi-select by touch. Once a finger has pressed the designer canvas, a toggle appears in its bottom-left corner. While it is on, a tap adds an element to the selection or removes it, as shift-click does, so align and distribute can be reached without a keyboard. A mouse never sees it. The phone-width criteria it completes are in docs/touch-authoring.md.

  • Render warns when the rows are not sorted by a group's key. A group breaks whenever its key changes, so a key value that comes back after a different one prints its group a second time. Render now reports that as a warning, once per group level, naming the group and the key. The report itself is unchanged. GroupLevel.key's dartdoc states the rule, and the README's Bind your data now says which bands compute an aggregate. TextElement.format's dartdoc now says that the pattern's separators come from RenderOptions.locale.

Fixed #

  • A finger drag on a small element moved it. The designer canvas reported a drag from where it cleared the touch slop, about 18 pixels, rather than where the finger went down. At phone zoom, a drag that started on an element 15 pixels tall was hit-tested beside it and scrolled the page.
  • A finger resize keeps up with the finger. For the same reason, an element's or a band's resize handle dropped the first ~36 pixels of a touch drag.
  • A selected element can be moved by touch. Its 44-pixel touch handles, centred on the edges, covered the whole body of a small element, so a drag on it grabbed a handle. Under touch an element's body now takes precedence over every handle target, this element's or a neighbour's, and the targets lean out of the element into the gaps. They also reach past the paper's edge, where they used to be cut off, so an element flush with the page margin keeps a full-sized corner target. The drawn squares stay where they were.

0.2.0 #

Settles the API questions that decision 0004 says 1.0 cannot leave open: each one is fixed or recorded as the 1.0 behaviour. Several are breaking, as a pre-1.0 minor release allows; the Breaking entries under Changed describe what a host sees differently. The report schema is unchanged, so saved reports need no migration.

Added #

  • hasErrors on any collection of diagnostics. The new exported DiagnosticQueries extension gives the List<Diagnostic> that validate returns the same hasErrors the render-time ReportDiagnostics already had, so a host's check is validate(definition).hasErrors. Render itself still does not validate; that is now the documented 1.0 behaviour on JetReportEngine. (#98)

Changed #

  • Breaking (behaviour): a group is keyed by its id; its name is a label. ReportVariable.resetGroup must name a GroupLevel by id. A reset that names a group by its name, which resolved before when the two differed, is now a validation error and resets nothing. Two groups in a scope may share a name, which validate no longer reports. Renaming a group changes nothing the report prints, except through a print callback that reads ElementPrintContext.bandName, which still carries the name. Saved reports are unaffected: the 1→2 migration already wrote resetGroup as an id. A crosstab's synthetic group now collides with a report group only by id. (#100)

  • Breaking (API): RenderedPage.frame and JetReportDesignerController.activeGuides are removed. Both had types the barrel never exported: frame the engine's internal display list, activeGuides the canvas's snap guides. A consumer could reach members of types it could not name, and they would have frozen at 1.0 unpublished. RenderedPage keeps index, and its constructor is no longer public; a report's pages still come from RenderedReport.pageAt. Inspect rendered output through the exporters (JetReportExporter.toPdf, pageToPng). (#105)

  • Breaking (default): an unknown field is #ERROR when the data source declares its schema. With RenderOptions.knownFields left null, the engine now derives it from a source with an explicit schema: JetObjectDataSource and JetPagedDataSource always, and JetInMemoryDataSource and JetJsonDataSource when given fields:. Nested collection fields are included. A binding to a field outside that schema renders RenderOptions.unresolvedFieldToken (#ERROR by default) and records a warning, instead of an empty cell. Sources with an inferred schema, and custom JetDataSources, render as before. A host's own knownFields still replaces the derived set. (#99)

  • Breaking (API): the preview's export and print callbacks receive the report and may be async. JetReportPreview.onExportPdf and .onPrint were VoidCallbacks, and JetReportWorkspace's were ValueChanged<RenderedReport>; both are now a RenderedReportCallback, FutureOr<void> Function(RenderedReport report). While a returned Future runs, that action's button is disabled, and a thrown error or rejected Future goes to the new JetReportPreview.onError. JetReportWorkspace forwards its onError there, as it already did to the designer. Without an onError, errors propagate as before. A preview host that closed over the report now takes it as the parameter: onExportPdf: (RenderedReport r) => save(r). A workspace host's ValueChanged<RenderedReport> still compiles. ReportErrorCallback is unchanged but now lives in its own library; it is still exported from jet_print.dart. (#101)

  • Breaking (API): RenderedReport.fonts and the constructor's fonts parameter are removed. The field's type, FontRegistry, was never exported, so a consumer could call its members without being able to name it. The registry a render measured with still travels to the preview, exporter and printer, internally. A RenderedReport built with its public constructor paints with the bundled default font, as it did when fonts was omitted. (#102)

  • Breaking (behaviour): validate rejects a band in a reserved furniture slot. PageFurniture.columnHeader, columnFooter and background were never laid out; a band there drew nothing but an info diagnostic at render, yet validate accepted it. Now each is an error, so the slots can be laid out in a later release without changing the output of a definition validate accepts. The API, the schema and render are unchanged. (#96)

  • Breaking (output): a total that should foot to zero prints 0,00, not -0,00. Numbers are doubles, so a column such as 0.3 - 0.1 - 0.2 sums to about -2.8e-17, and a number pattern printed any value below zero with its sign, or in a negative subpattern's parentheses. A value that rounds to zero at the pattern's precision now prints as zero, through FORMAT and the format property alike. Numbers stay doubles for 1.0; the JetNumber dartdoc says so. (#95)

  • Breaking (output): UPPER and LOWER follow the render locale. Under a Turkish or Azerbaijani RenderOptions.locale, UPPER maps i to İ and LOWER maps I to ı and İ to i, so UPPER("Kısa Vadeli") prints KISA VADELİ instead of KISA VADELI. Every other locale, including the default en, cases exactly as before. A tr or az report that changes the case of an i renders differently. Outside a render, the functions follow Intl.getCurrentLocale(), as FORMAT does. (#97)

Fixed #

  • validate no longer warns about an aggregate nested in a summary expression. SUM($F{debit}) - SUM($F{credit}) or ROUND(AVG($F{rate}), 2) in the summary band drew a record-blind-field warning for each operand, though the synthesizer folds them over the data rows. A summary element now counts only the fields it reads outside every aggregate call. In other record-blind bands, where aggregates are not computed, the warning stands. With a schema, every field an aggregate's operand reads is now resolved, whether the aggregate is nested or the operand is compound, so SUM($F{typo}) + 1 or SUM($F{typo} * 2) in a summary or footer reports the unknown field. (api-friction entry 6)
  • An inline aggregate written with a space before its parenthesis is computed. SUM ($F{amount}) parsed, and passed validate, but the aggregate synthesizer only lifted SUM( with no space, so the summary or footer printed an "Unknown function" error. Whitespace there is now accepted, as the parser accepts it.

0.1.1 #

Documentation and package metadata only. Apart from jetPrintVersion, which now reports 0.1.1, the library's code is the same as in 0.1.0, so upgrading changes nothing a host calls or renders.

Changed #

  • Sponsor links. pubspec.yaml lists GitHub Sponsors and Buy Me a Coffee under funding:, which pub.dev shows in the package sidebar, and the README shows both as badges and in a Support section.
  • The 0.1.0 highlights give the right barcode count. They said 10 barcode/QR symbologies; 0.1.0 shipped 22, plus auto, which infers one of six. The 0.1.0 entry below is corrected in place.

0.1.0 #

Highlights — the first public shape of jet_print. In one sentence: design a report as a reified, id'd section tree, fill it with your data, and preview, paginate, export to PDF/PNG, or print it — from a single entry point.

  • Reified model — ReportDefinition (page furniture + body, bands, groups, nested/recursive detail scopes) with author-time validate() diagnostics, versioned serialization, and a forward migration framework.
  • Render & export — JetReportEngine fills and paginates lazily; JetReportExporter writes deterministic PDFs (selectable text, embedded fonts) and PNGs; JetReportPrinter drives system printing behind an injectable seam.
  • Interactive designer — JetReportDesigner with select/move/resize/align, undo/redo, zoom, rulers, grid-snap, and clipboard, themed via shadcn_ui.
  • Rich elements — fx-expression text, shapes, images, charts, 22 barcode/QR symbologies (plus auto, which infers QR, Code 128, EAN-13, UPC-A, EAN-8 or ITF-14 from the data), and multi-column label layouts; host & system fonts; en/de/tr chrome.

The detailed, spec-by-spec log below records every change since the legacy flat model and is aimed at upgraders, not first-time readers.

Fixed #

  • JetReportPreview says that it needs a shadcn_ui theme. The preview reads the ambient ShadTheme, so under a MaterialApp alone it throws on its first build, and neither its dartdoc nor the README Quickstart said so. Both now do: wrap it in a ShadApp, or in a ShadTheme inside a MaterialApp, with JetPrintLocalizations.delegate wired, and add shadcn_ui to the app's own dependencies to import them. The Quickstart shows the ShadApp from example/jet_print_example.dart. The dartdoc's code sample, which still called the removed JetReportEngine.render, now calls renderDefinition. No behaviour changed.

  • Band captions no longer cover the first element of a band. The designer's band captions ("Group Header", "Detail", …) were drawn in the page's left margin, but a caption is wider than many margins (~70px for "Group Header", ~100px for the Turkish "Sayfa Alt Bilgisi"), so it ran onto the page and over whatever started there. They now sit in a column beside the page, right-aligned against its left edge at each band's top. Fit-to-width and fit-to-page leave room for the column, and clicking a caption still selects its band.

  • The package compiles at the lowest dependency versions it allows. shadcn_ui allows lucide_icons_flutter 3.0.0, but some icons the designer uses are missing from versions before 3.1.14 (textAlignStart/Center/End first ship in 3.1.1, and circleHelp is absent from most 3.1.x releases before 3.1.14), so a downgraded resolution failed to compile. lucide_icons_flutter: ^3.1.14 is now declared to raise that floor. intl is constrained to ^0.20.2 (it was any).

  • The README quickstart compiles. Its TextElement was missing the required text: argument.

  • A preview render superseded before it starts no longer calls the host. The workspace yields one frame before rendering so the loading bar can paint. If renderReport or dataSchema changed during that frame, the superseded render still went on to call the host's current callback, then discarded the result, so one input change rendered twice. It now stops as soon as it is superseded, and only the newer render calls the host.

  • Resizing a band no longer leaves elements outside it. Shrinking a band — by dragging its divider or typing its height in the inspector (setBandHeight) — had only the minimum-height floor, so elements ended up hanging past the new bottom edge. Both now stop at the lowest element's bottom edge — the same edge-pinning a dragged element gets at its band — so nothing is moved or resized to make room; content that already overflows blocks shrinking without growing the band. A numeric inspector field now shows the value the model took after a commit, so a clamped height reads back as the height the band kept rather than the number typed. A programmatic SetBandHeightCommand is unchanged.

  • The group-row hint names the band that actually carries the settings. Selecting a group row always said to edit its settings "on the group header band" — even for a footer-only group, whose settings live on its footer, so the hint pointed at a band that did not exist. It now names the header when there is one and the footer otherwise (new key propertiesGroupOnFooterHint in en/de/tr), and shows no hint for a group with neither.

  • Committing a plain-text value unchanged no longer changes it. The value field showed a literal label raw but committed it through the template parser, which reads [name] as a field binding and \ as an escape. So re-committing what the field showed — which the fx editor's Apply does even without an edit — turned a label reading [Draft] into a binding to a field named Draft, and C:\temp lost its backslash. A literal is now displayed with its \ [ ] { } escaped (\[Draft\]), the template's own literal syntax, so it parses back to exactly what was stored.

  • Undo, redo and opening a report now cancel a drag in progress. Replacing the document left the live move, resize or band-resize state behind, and Ctrl+Z works mid-drag. So dragging element B and pressing Ctrl+Z restored the previous selection (say A) with B's drag still pending; releasing the pointer then moved A, and that commit discarded the redo entry. The drag is now abandoned whenever the document is replaced, and the release commits nothing.

  • The workspace preview no longer shows stale data after the host switches data. JetReportWorkspace cached the rendered report by definition alone and had no didUpdateWidget, so a host that changed its renderReport callback or dataSchema — the playground's "Select data source" — kept seeing the old rows in preview until the report itself was edited. Either change now invalidates the cache: the next preview entry re-renders, and a preview already on screen re-renders in place. A render still in flight when the inputs change is superseded, and export and print are offered only while the report on screen is current — not during a re-render, and not after a failed one.

  • Back-to-back instances of a group now lay out as separate instances. The layouter folded consecutive headers of one group into a single header run — a rule that exists for crosstab groups, which emit several header bands per instance. An ordinary group owns one header band, so two in a row are two instances (a group with no footer whose rows printed nothing, for example because the detail band is hidden). The second instance lost its startNewPage break, joined the first's keepTogether span, and a page break reprinted both headers. The merge now applies to crosstab groups only. Relatedly, a page break caused by a new instance's own header no longer reprints the header of the instance it replaces: the previous instance is now closed before the break checks, not after.

  • Malformed report JSON now fails with ReportFormatException, and only that. JetReportFormat.decodeDefinition and decodeDefinitionJson promised ReportFormatException, but a truncated or hand-edited document could escape as a TypeError (a ! or as cast meeting the wrong type), an ArgumentError (an unknown enum name such as a variable calculation of "median") or a FormatException (text that is not JSON), so a host catching on ReportFormatException crashed. Both entry points now convert those into a ReportFormatException that keeps the underlying message. A test swaps every value of a report touching every decoder for each wrong-typed stand-in, and removes every key, and requires nothing else to escape.

  • A malformed variable or group key no longer aborts the render, and validate() now checks variables. A report variable whose expression did not parse (say $F{amount), or a group key that did not parse, threw out of JetReportEngine.renderDefinition — while a malformed element expression has always rendered !ERR with a diagnostic. Now a broken variable evaluates to an error on every row whatever its calculation (it prints !ERR, never a plausible-looking total), a broken group key never breaks, and each records one error diagnostic. validate() gains rule I9: a variable's expression must parse, its name must be unique and outside the __ prefix the aggregate synthesizer reserves, and a group-scoped variable must reset on exactly one group (a reference that is one group's id and another's name is ambiguous). An element whose expression errors is now diagnosed once per element and message rather than on every row. Behaviour change: a fill with a malformed variable used to throw ExpressionException; it now returns normally.

  • An aggregate that skips a failed value now says so. An aggregate input that evaluated to an error — SUM($F{amount} / $F{qty}) over a row where qty is 0 — was skipped exactly like a blank, so the total came out short with no !ERR and no diagnostic. It is still skipped (the total remains the fold of the rows that evaluated), but each such row now records a warning, "N value(s) failed to evaluate and were skipped from …", next to the existing one for non-numeric values.

  • A chart over an infinite value no longer throws from pageAt. An infinite chart value — reachable from a JSON number like 1e400 or an overflowing multiplication — went straight into the series, and scaling the value axis threw (Infinity.floor()) out of page building, breaking the engine's never-throw promise. A non-finite value is now plotted as 0 with a warning, like a non-number, and the value axis is now total: a non-finite maximum gets a unit scale, and an extreme finite one (double.maxFinite, whose rounded maximum overflowed and hung the tick loop, or the smallest positive double, whose step underflowed to 0) gets an exact linear scale. Ticks are generated by count, so no input can loop forever.

  • A fill that throws now closes the host's data set. The filler opened the data set early (to read its schema) but only started the try/finally that closes it at the row loop, so anything that threw in between — a malformed variable or group key fails fast there — left a custom DataSet (a database cursor, a file handle) open. The whole fill now runs under one finally that closes it exactly once.

  • The field picker's search box and empty result are localized. Its "Search fields" placeholder and "No matching fields" line were hardcoded English and showed untranslated under de and tr. They are now the fieldPickerSearchHint and fieldPickerNoMatches strings in all three locales.

  • The design canvas no longer freezes after a frame fails to record. A record that threw — a corrupt embedded image fails to decode — left the canvas's in-flight flag set forever, so every later edit was skipped, even deleting the image that caused it, and the error escaped unhandled. The failure is now caught, as the page-thumbnail rail already did: the canvas keeps its last good picture and records again on the next edit.

  • A total wrapped in parentheses no longer prints 0. Nested-footer and scope totals recovered an aggregate's argument by slicing the source text between its first ( and last ). For (SUM($F{qty})) that slice is SUM($F{qty}) itself, which failed as an unknown function on every row, and the failures were folded away, leaving 0. The argument is now taken from the parsed expression. The same rewrite also rebuilt the footer text field by field and dropped its name; it now uses copyWith.

  • Page headers and footers now honor visible and format. The layouter placed every page-furniture element as authored: a header logo set hidden, or given a visibility expression, printed on every page, and a footer text's format was dropped when its expression was substituted, so a formatted date or total printed raw. Furniture visibility is evaluated per page, so $V{PAGE_NUMBER} == "1" works as a "first page only" condition; a hidden furniture band keeps its height, since the body's capacity is fixed before pages are built. A visibility expression that references a field or a non-page variable, which a page cannot resolve, keeps the object visible with a warning rather than silently hiding it. The per-page text is now resolved with copyWith, so it cannot drop a field either.

  • Band.copyWith can now clear name and columnLayout. Both took plain nullable parameters, so null meant "keep" and nothing could clear them; renaming a band back to its default label and removing a label layout each rebuilt the band field by field instead — the pattern that silently drops a field the day Band gains one. Both parameters are now thunks (name: () => null clears) — breaking for callers passing a value, which becomes name: () => 'x' — and both commands go through copyWith.

  • A chart type this build does not know is no longer rewritten to bar. A report from a newer build naming, say, an area chart loaded as a bar chart — fine for rendering — but re-saved as "chartType":"bar", silently replacing the author's choice and breaking the lossless round-trip. The original name is now kept in the new ChartElement.unknownChartType and written back on save, the same contract ShapeElement.unknownForm already gave shapes; a deliberate type pick clears it.

  • Emptying a chart's title or category field now clears it. Two layers swallowed the edit: the title field reverted a blank entry before committing, and ChartElement.copyWith took plain nullable title / categoryExpression parameters, so a null meant "keep" and nothing could clear them. Both parameters are now thunks, like name (title: () => null clears) — breaking for callers passing title: 'x', which becomes title: () => 'x'. setChartOptions keeps its signature and, like setFormat, now reads an empty title or categoryExpression as "clear".

  • The declared Flutter floor now matches what is actually tested. packages/jet_print/pubspec.yaml declared flutter: ">=3.6.0" — 38 minor versions below anything the package has ever been built against. CI pins 3.44.0 in all four jobs and AGENTS.md documents 3.44.0+, so the constraint claimed support nobody had verified, and a consumer on an older SDK would have resolved this package and then failed to build it. It is now >=3.44.0. The Dart constraint stays ^3.6.0: pub workspaces need 3.6, and each Flutter release pins its own Dart, so that pairing is unreachable in practice.

  • PDF export decodes each image once, not once per row. PdfPainter's decoded-pixel cache was keyed on the whole ImagePrimitive. Because ImagePrimitive's value equality includes bounds and elementId — and walks the byte list element-wise — every cache probe hashed the entire image and missed for a repeated one, so a logo down a 50-row band was decoded 50 times on export. It now keys on the byte buffer, matching the sibling _embeddedImages cache in the same class and the equivalent fix already made to CanvasPainter. Output is unchanged: the byte-pinned invoice.pdf golden is identical, since embedding was already deduplicated.

  • Documentation: the element and expression registries were described as host extension points they are not. JetFunctionRegistry, ElementCodecRegistry, ElementRendererRegistry and ElementTypeRegistry each advertised themselves as a public/open-closed seam — "consumers register custom functions with zero core edits", "consumers add their own types", "a consumer can override a built-in". None of them is exported from package:jet_print/jet_print.dart, and no public entry point accepts one: the instances they feed are private and pre-wired (JetReportFormat's never-mutated static final, the render chain's own), so exporting the types alone would not help. Their dartdoc now says what is actually reachable from a host — JetDataSource, RenderOptions.onElementPrint, RenderOptions.fonts and PrintDialogPresenter — and what is internal; docs/06-round-tripping.md covers the element-codec case in full. No behaviour changed. Element types and expression functions are a first-party set: the registries hold the library's own, and a new one is added by a change to the jet_print repository, not by a host. A public registration API would stretch every saved report's compatibility promise over types this library did not define, so jet_print does not offer one. That bounds the current scope rather than ruling one out: it could be added later without changing any existing call.

  • Text no longer leaks a ui.Paragraph per line on every record. CanvasPainter.drawTextRun builds one paragraph per laid-out line and drew it without ever releasing it, so each record leaked one handle per line — denser than the decoded-image leak fixed alongside it, since every report has text and most have no images, and the preview re-records on every edit. Each paragraph is now released as soon as its draw returns: Canvas.drawParagraph has the paragraph paint itself into the canvas, so the recording holds its own reference by the time the draw returns and the handle is redundant from that point. That keeps the peak at one live paragraph rather than one per line of the page. Guarded at the raster level on the web leg, because the golden suite is macOS-only and a CanvasKit-only disposal bug cannot surface there.

  • One image repeated down a band decoded once per row. The decode cache in the on-screen paint backend was keyed by the whole drawing primitive, whose identity includes position, so a single logo on 40 detail rows produced 40 separate decodes and 40 GPU textures instead of one. It is now keyed by the image bytes. Each cache probe was also hashing the entire encoded image to compare keys; that cost goes with it.

  • Decoded image textures are released on every paint path, not just the design canvas. CanvasPainter.dispose() — which frees the GPU texture behind each decoded image — was declared on the concrete painter only. The preview, the page-thumbnail rail and PageRasterizer hold the ReportPainter abstraction (or, in the rasterizer's case, kept no reference at all), so the method was invisible to them and three of the four consumers silently skipped it, leaking a texture per recorded frame on CanvasKit. dispose() is now part of the ReportPainter contract, and the recorder → painter → paintFrame → endRecording → release sequence is single-sourced in recordPageFrame, so no consumer can drop the last step; a new architecture test keeps CanvasPainter construction to that one seam. PageRasterizer also now disposes the intermediate ui.Picture it rasterizes from, which was leaked on the same path.

  • Charts can be saved, duplicated and pasted again. ChartElement was registered only in the render-time codec+renderer pairing (registerBuiltInElementTypes) and never in the codec-only registerBuiltInElementCodecs, which is what the persistence paths use. A report containing a chart therefore rendered correctly but threw Bad state: No ElementCodec registered for type "chart" on save (JetReportFormat.encodeDefinition) and on designer duplicate / paste. The two built-in registration lists are now pinned against each other by test/architecture/built_in_element_registration_test.dart, so a future element type cannot reach one list without the other.

Changed #

  • Requires shadcn_ui 0.57.1 (was ^0.54.0), the latest release. A host app that pins shadcn_ui below 0.57 must raise it. Nothing the designer renders changes. 0.56 renamed shadcn_ui's slang-generated localization classes, which jet_print does not use. 0.57 moved the dialog and sheet safe-area padding to the route, which affects the expression editor sheet only on a device with system insets.

  • Crosstab style editors: the eight crosstab appearance slots — the six on CrosstabStyle plus CrosstabMeasure's two per-measure overrides — are now authorable in the Properties panel. They already rendered; only the UI was missing. Unset slots display the value they inherit rather than a blank, the first edit materializes only that slot, and a reset clears it back, so a crosstab whose appearance is untouched still serializes without any style keys.

  • Crosstab designer authoring. A crosstab is now a first-class authorable object, not just a rendered one. The root scope's Outline "+" menu adds one — born valid and bound, seeded from the attached schema — and its row is selectable, inline-renameable, reorderable (its position decides whether it prints before or after the row loop) and removable. Selecting it opens a Properties inspector for its name, data source, row and column axis levels, measures, size metrics and visibility, with an inline warning when the crosstab cannot fit the printable body width. On the canvas its block is selectable and draws a schematic of the model — real column widths and axis names, with data-driven row and column counts stood in for. New public controller extension: CtrlCrosstab. Selection gains a sixth target, crosstabId, and its Selection.crosstab factory. The crosstab's eight text/box style slots are now authorable in the Properties panel — see Crosstab style editors, above.

  • BREAKING — crosstab (pivot grid) support. A new Crosstab block — configurable row/column axis CrosstabGroups (each with a sort order, an optional subtotal, and an overridable total label), CrosstabMeasures (a per-row expression folded by an aggregate, with its own format/text/box overrides), and appearance via CrosstabStyle — prints once within a DetailScope via a new CrosstabNode. It folds the scope's own rows (or a nested collection, via collectionField) into a sparse matrix, synthesises subtotal/grand-total rows and columns addressing exactly the prefixes the aggregator folded, and paginates horizontally into slices when the column axis is wider than the page body — all through the existing band/group machinery (no new render-engine seam). New public types: Crosstab, CrosstabGroup, CrosstabMeasure, CrosstabStyle (plus the CrosstabSort enum); CrosstabMatrix/CrosstabPlan/the aggregator stay internal. Breaking: the sealed ScopeNode gains two new variants — CrosstabNode and UnknownScopeNode (losslessly round-trips a scope node whose kind this build doesn't recognize, the crosstab's own forward-migration seam) — so any downstream exhaustive switch over ScopeNode must add cases for both.

  • BREAKING — the report model is reified. The flat ReportTemplate band list — where a band's role was inferred from type + group-name + collectionField + sibling position — is replaced by an explicit, id'd section tree, ReportDefinition:

    • ReportDefinition { name, page, parameters, variables, furniture: PageFurniture, body: ReportBody }. PageFurniture holds the record-blind page chrome (page/column header & footer, background — the last three reserved, not yet laid out); ReportBody holds the once-bands (title/summary/no-data) and the master DetailScope.
    • DetailScope { id, collectionField?, groups: GroupLevel[], children: ScopeNode[] } with the sealed ScopeNode = BandNode(Band) | NestedScope(DetailScope). children is ordered and heterogeneous, so the authored interleaving of per-row bands and nested scopes (e.g. "meta band → lines scope → total band") is preserved.
    • Groups are first-class GroupLevels that own their key expression, header/footer bands, and pagination flags (keep-together, reprint-header, start-new-page) — one editing home, fixing the "same flag on both the group header and footer band" smell. ReportGroup.startNewPage is now GroupLevel.startNewPage.
    • Every Band carries a stable id; its rendering role is stated by its position in the tree, with BandType retained for labels, glyphs, identity, and faithful migration (and validated consistent with its slot).
    • The engine, serialization, and designer consume ReportDefinition directly. Rendering is byte-identical to the previous engine for every existing report (golden-locked).
    • New author-time validate(ReportDefinition) returns Diagnostics for the semantic invariants — unique ids/group-names, record-blind furniture rejecting $F{} field bindings, parseable group keys — surfaced in the designer (controller.diagnostics) before render.

Removed #

  • BREAKING — the legacy flat model is gone. ReportTemplate, the ReportBand class (the BandType enum stays), and the public ReportGroup type are removed, along with JetReportEngine.render(ReportTemplate), JetReportFormat.encode/decode/encodeJson/decodeJson, and the v1 codec. Use JetReportEngine.renderDefinition, JetReportFormat.encodeDefinition/ decodeDefinition, and encodeDefinitionJson/decodeDefinitionJson.

Migration #

  • v1 → v2 schema migration (lossless). A document saved in the v1 (flat) format loads forward automatically: JetReportFormat.decodeDefinitionJson migrates a v1 JSON map to a ReportDefinition — every v1 construct maps to a v2 home and master-level band/sub-scope order is preserved. The schema version is now 2, public as JetReportFormat.schemaVersion.

Added #

  • JetReportFormat.schemaVersion, the report schema version this build writes (2): the schemaVersion key at the head of every encoded document. A host that stores reports can keep it beside each one, to find the documents an upgrade will migrate, without hard-coding the number or encoding a throwaway definition to read it back. It mirrors JetDataSourceFile.version for the data-source format. It is a const, so it can be used where a constant is required.

  • The README says what a host can extend. A new What you can extend section lists the four host seams (JetDataSource, RenderOptions.fonts, RenderOptions.onElementPrint and PrintDialogPresenter) and states that element types and expression functions are a fixed, first-party set, with no registration API.

  • README rewrite with screenshots and tested examples. The README now shows the designer, previews, charts and barcodes (also listed under screenshots: for the pub.dev gallery), and covers installing, hosting the designer, saving and reopening designs as JSON, and binding JSON or object data with groups and totals. Its code is copied from example/designer_example.dart and example/data_example.dart (new) and example/jet_print_example.dart, which tests run, and test/readme_snippets_test.dart fails if a README snippet stops matching them.

  • A package example, example/jet_print_example.dart: describe a report, fill it, export it to PDF, and preview it. A test runs it, so it keeps compiling against the public API.

  • The designer authors the reified tree natively. The controller, Outline and Properties panels, and canvas all edit a ReportDefinition; bands, groups, and scopes are addressed by stable id (selection survives add/remove/reorder). Groups and detail scopes are first-class entities; in the Outline a group surfaces through its header/footer bands (no separate node, Jasper-style), and its key + start on new page are edited on its header band, writing through to the one GroupLevel (keep-together and reprint-header are implemented but not yet surfaced in the UI). Full band/group/scope lifecycle — add, remove, reorder, and retype, each one undoable step — is available from the Outline panel (controller.addBand/addDetailBand/addGroupBand/removeBand/moveBand/ retypeBand, plus createGroup/deleteGroup/createScope/deleteScope); "retype" relocates a band to the matching slot, keeping its id.

  • Per-group page breaks (GroupLevel.startNewPage). A group can begin each instance after its first on a fresh page; the first instance never forces a leading blank page. Combined with grouping per record (e.g. one group per invoice) this yields one record per page. Additive and optional — serialized only when true, so existing reports round-trip byte-identically. Edited by selecting the group's header band, which surfaces the group's key + a start on new page toggle in the Properties panel (controller.setGroupStartNewPage(groupId, value)), as one undoable step.

  • Host & system fonts in font pickers. A host can now contribute its own fonts, selectable in every designer picker and rendered byte-identically across canvas, preview, PDF, and PNG:

    • Two bytes-in value types: JetFontFace ({bytes, weight, italic}, value-equal by bytes identity) and JetFontFamily ({name, faces}). A family validates its faces eagerly and synchronously at construction — it requires at least a regular face, parses every face, and rejects an empty/malformed/regular-less/duplicate-slot family — so a bad font is caught where it is assembled, never later inside a widget build() or a render().
    • FontFormatException is now exported — the single detectable rejection type.
    • Two host touch-points, one shared list: RenderOptions.fonts (threaded through the render chain) and fonts on JetReportDesigner / JetReportWorkspace (the designer picker + canvas). Pass the same List<JetFontFamily> to both. Duplicate family names resolve last-registration-wins; missing bold/italic fall back to regular; built-ins are listed first, then host families in the order supplied.
    • WYSIWYG by construction: JetReportEngine.render builds one FontRegistry (defaults + host families), measures with it, and carries it on the returned RenderedReport; the preview, the PDF/PNG exporter, and the printer read it off the report instead of building a parallel default-only registry — they gain no new parameter. FontRegistry stays internal.
    • No schema change: reports still store only the font-family name (schemaVersion unchanged). A report naming a font absent in the session opens without error, renders in a fallback, shows the name marked unavailable in the picker, preserves it on save, and still exports.
  • Format properties — font & color editors. The Properties panel now edits the style the model already carries, per element type:

    • Text — a new Font section: family picker (enumerates the designer's registered families, each previewed in its own typeface; an unregistered stored family is shown as unavailable and preserved verbatim until repicked), size field clamped to 4–144 pt, B/I/U toggles (Bold is active iff the weight is exactly bold; medium/semiBold are preserved until the toggle is operated), a swatch + hex color editor, and left/center/right alignment segments (a stored justify shows no active segment and survives unrelated edits).
    • Underline, end-to-end (the one net-new attribute): JetTextStyle gains underline (default false, serialized only when true). Both painters draw it as an explicit stroked segment from one shared geometry helper — never TextDecoration — so canvas, PNG preview, and PDF export are identical by construction.
    • Shape — a new Appearance section: fill color with None (hidden for line shapes), outline color with None, and outline width clamped to 0–20 pt. Width 0 hides the outline at a single renderer seam while the stored color is remembered, so stepping back above 0 restores it.
    • Barcode — a color row bound to BarcodeElement.color; the placeholder rendering is tinted with it (real symbology rendering will inherit it).
    • Color editing rules: display is #RRGGBB (or #AARRGGBB when translucent); a palette pick or 6-digit hex preserves the stored alpha, an 8-digit hex sets it, malformed input is rejected with the last valid value restored and no history entry.
    • New public surface is additive and minimal: JetTextStyle.underline + JetTextStyle.copyWith (sentinel fontFamily), JetBoxStyle.copyWith (sentinel fill/stroke — explicit null = the None states), BarcodeElement.copyWith, and three controller mutators — setTextStyle, setShapeStyle, setBarcodeColor — each one undoable, notifying step that no-ops (no history, no notification) on an equal value, a missing id, or a wrong element type. The editors, commands, underline helper, and FontRegistry (including its new internal families getter) stay private.
    • One bundled font, all four faces. The library now ships a single built-in family — Default (a Latin subset of Noto Sans, OFL 1.1) — in Regular, Bold, Italic, and Bold Italic subsets covering one codepoint set, so B/I/U edits are visible on canvas, preview, and export alike. Intermediate weights (medium/semiBold) resolve to Regular via the fallback chain. Default is the always-resolvable render fallback: text with no/unknown family always renders. The designer preloads its Regular face into the engine at mount so picker options preview in their own typeface before the canvas has painted them. Every other family is contributed by the host (see above) — the library bundles no serif or monospaced font of its own. (~112 KB embedded font data; the PDF exporter embeds each face once per document, only when used.)
    • Canvas right-click menu now dismisses on a primary click anywhere on the canvas. It previously closed only when clicking the surrounding chrome, because the canvas gesture layer consumed the tap before the menu's own dismiss handler saw it. The dismissing click still acts on the canvas — selecting or deselecting as usual — and changes no document state.
    • Serialization: kReportSchemaVersion stays 1, no migration. underline is additive-optional; all existing omission rules are unchanged, and pre-feature reports load and re-save byte-identically (pinned by a frozen fixture test). ~35 new localized strings ship in en/de/tr.
  • Visual shape gallery in the Properties pane. When a single shape element is selected, the Properties panel now shows a Shape section with eight thumbnails — line, rectangle, ellipse, triangle, diamond, pentagon, hexagon, star — drawn through the same geometry the renderer uses, so the picker icon is exactly what the canvas, preview, and export produce. The active form is highlighted; a single click switches the shape's form in one undoable step, preserving its bounds and fill/stroke. Re-picking the active form is a no-op (no history, no notification).

    • New public surface is additive and minimal: six new ShapeKind values (ellipse, triangle, diamond, pentagon, hexagon, star), JetReportDesignerController.setShapeKind(String, ShapeKind) (one undoable, notifying step), and ShapeElement.copyWith(...) plus an optional unknownForm field on the already-public immutable type. The geometry (shapePath), the SetShapeKindCommand, and the gallery widget stay private.
    • Rendering fidelity (WYSIWYG): every closed form emits a single PathPrimitive from one shared shapePath, so canvas, preview, and PDF/PNG export agree with no forked render path and no new painter code. The ellipse is a smooth 64-segment inscribed polygon (no curve primitive needed).
    • Lossless forward-compatible serialization: kReportSchemaVersion stays 1 with no migration. Known forms are wire-identical to before, so pre-feature reports load byte-for-byte unchanged. A form a newer version added loads as a rectangle while preserving the original name in unknownForm, and re-saves it — a lossless round-trip. A deliberate pick clears the preserved name.
    • The section label and all eight form names are localized in en/de/tr; each thumbnail is a keyboard-reachable button exposing its localized accessible name and selected state.
  • Editable report Name in Report properties. When the report is selected, the Properties panel now leads with a primary Name field that edits the report (template) name through the existing JetReportDesignerController.rename — one undoable step, kept in sync with the toolbar's inline rename. A blank entry reverts, so the report always keeps a name. No new public surface or schema change; the label is localized in en/de/tr.

  • Editable paper type & margins in Report properties. The Properties panel's PAGE section is now an editable, Microsoft Office–style page setup: a live page-sample thumbnail (a proportional sheet with margin guides), a paper-type picker (A4 / A3 / A5 / Letter / Legal / Custom, recognized by name in either orientation), a Portrait | Landscape toggle, Custom width/height fields (shown for a custom size), a margin preset picker (Normal / Narrow / Wide / None) and editable per-side left/top/right/bottom fields.

    • New public surface is additive and minimal: JetReportDesignerController.setPageFormat(PageFormat) (one undoable, notifying step that clamps to a usable page so the content area stays positive), plus PageFormat.copyWith(...) and JetEdgeInsets.copyWith(...) on the existing immutable value types. Paper/margin presets, recognition, the clamp, and the page-preview are private.
    • Canvas, preview, and export all read the one template.page, so a size / orientation / margin change shows identically in all three (WYSIWYG). Preset identity and orientation are derived for display, never serialized — kReportSchemaVersion stays 1, no migration, old templates load unchanged. New labels are localized in en/de/tr.
  • Added JetReportWorkspace: a keep-alive designer↔preview workspace. Both views stay mounted so switching modes is instant (no canvas re-record), and the preview renders behind a loading indicator (ShadProgress, overridable via loadingBuilder) so the first large-data render gives visible feedback instead of a frozen frame. A failed render is reported via FlutterError.reportError and clears the indicator. Export/print receive the current RenderedReport.

  • Unified context-switching toolbar. The designer and the report preview now read as one toolbar that changes by context: a single shared shell renders the report name (left) and a two-segment Designer | Preview mode switch (center), identical in both modes, while each mode fills the right slot with its own actions.

    • The mode switch reuses the existing switch events — selecting Preview from the designer fires onPreviewRequested, selecting Designer from the preview fires onBack — so existing hosts get the switch for free and the host keeps ownership of the actual swap. The inactive segment is enabled only when its callback is wired. Each segment carries an expressive glyph (a drafting pencil-ruler for Designer, a page-under-magnifier for Preview), and the leading file icon is the same in both modes so the shared region reads as one bar.
    • The report name is shown read-only in the toolbar (the inline-rename affordance is no longer surfaced there). Renaming stays available to hosts through JetReportDesignerController.rename(String) (a single undoable step) and the JetReportPreview.onRename hook, to be surfaced from the host's own UI.
    • The designer's right slot leads with the Open / Save file actions (ahead of the editing commands); Export is offered only in the preview, where the rendered artifact exists.
    • New localized chrome (modeDesigner, modePreview) in en/de/tr. No render-path or serialization change: kReportSchemaVersion stays 1 and report goldens are byte-identical.
  • Clipboard operations in the designer UI. The designer's cut/copy/paste — until now keyboard-only — become mouse-reachable through two new surfaces, both built into JetReportDesigner with zero host wiring:

    • A fenced Cut / Copy / Paste icon-button group joins the top bar, right after Undo/Redo. Cut/Copy enable when elements are selected; Paste enables the moment the clipboard has content. Tooltips are localized (en/de/tr) and carry the platform keyboard shortcut (⌘ on Apple, Ctrl+ elsewhere).
    • A net-new canvas context menu (Cut, Copy, Paste, Duplicate, Delete) opens on right-click. It resolves selection before opening — right-clicking an unselected element selects it, a multi-selection is preserved, and an empty right-click never deselects — and shows each item's shortcut hint.
    • Both surfaces invoke the existing controller clipboard ops (no second clipboard) and read the same two predicates, so they can never diverge. Two additive controller getters, canCopy and canPaste, mirror the existing canUndo/canRedo idiom; copy() now notifies listeners (without creating an undo entry) so Paste re-enables immediately after a mouse Copy.
    • Invocation surfaces only — no model, codec, schemaVersion, or render-path change, so saved files and preview/export/print output stay byte-identical and the keyboard shortcuts keep working unchanged.
  • Grid & snap helper tools. The two remaining design-canvas helper tools now behave as their top-bar icons promise:

    • A light 5 mm alignment grid is drawn as backmost design-time chrome on each band, registered to the band's content origin so every drawn line lands exactly on a snap target (true WYSIWYG). It is on by default, toggled by the existing grid button, stays page-registered through zoom/pan, and adaptively coarsens then hides when zoomed far out so it never smears into a solid fill.
    • The grid is design-time chrome only — like the rulers it never flows through the render pipeline, so preview, export, print, and saved templates are completely unaffected (no model, codec, or schemaVersion change).
  • Canvas rulers. The design canvas now shows a horizontal ruler along the top and a vertical ruler down the left, calibrated in millimetres from the page's physical top-left corner (0,0):

    • Marks stay locked to true page positions across the full zoom range and while panning, with an adaptive "nice-step" labelled interval (1/2/5 ladder) so labels never crowd or vanish, plus finer unlabelled subdivisions. Labels are localized to the active locale's number grouping (en/de/tr).
    • A thin marker tracks the pointer on both rulers, and the current selection's union bounding box is highlighted as one combined span per ruler (single element, multi-selection, or band), updating on move/resize and clearing on deselect.
    • Rulers are shown/hidden by the existing top-bar ruler toggle and are on by default. Two methods are added to JetReportDesignerController, rulersEnabled / setRulersEnabled, mirroring the grid/snap pair; ruler visibility is a per-session view preference and is never serialized.
    • Rulers are design-time chrome only — they never flow through the render pipeline, so preview, export, and saved templates are completely unaffected (no model, codec, or schemaVersion change).
  • Simplified label value & format properties (spec 013-label-value-format). The label (text element) Properties panel now has a single Value field in place of the separate Text and Binding inputs, plus a new Format field:

    • The Value field recognizes three forms — a whole-value [fieldName] simple binding, a { … } advanced template mixing [field] tokens, literal text, and functions (e.g. {upper[name]}, {[firstName] [lastName]}), and otherwise literal text (with a \ escape for literal brackets/braces). It shows a stored binding exactly as the canvas token, and presents a legacy/out-of-grammar expression read-only so it is never lost. Bindings stay single-sourced in TextElement.expression; the template is a pure string↔string projection over the existing expression parser (no parallel render path).
    • A new optional TextElement.format (additive, no schema-version bump) — an ICU number/date pattern applied to the resolved value at render time through a shared applyJetFormat helper (the same logic behind the FORMAT function, so they cannot drift). The Format field offers seven quick-pick presets (None, Integer, Decimal, Currency, Percent, Date, Date & time); a pattern that does not fit the value or is malformed falls back to the unformatted value.
    • A binding to a field absent from the active data source renders a localized #ERROR token in schema-aware contexts (designer/preview). RenderOptions gains two additive fields — knownFields (Set<String>?, the data source's declared field names) and unresolvedFieldToken (default #ERROR); when knownFields is supplied, a binding outside it renders the token, otherwise behavior is unchanged. The token is a plain string so the headless render layer stays Flutter-free; a host with a BuildContext passes a localized value.
    • New Properties-panel strings localized en/de/tr with English fallback.
  • Export & print — PDF, PNG, and the system print dialog (spec 012-export-support). The same RenderedReport the preview displays now turns into shareable artifacts; one render feeds every target. New public surface from the single entry point:

    • JetReportExporter — stateless const facade. toPdf(report) returns in-memory PDF bytes: every page in order at the template's true physical size (PostScript points), real selectable/searchable text placed at the pre-measured baselines with the measuring fonts embedded, and images at the preview's computeImageFit geometry. Byte-deterministic: identical rendered inputs produce byte-identical files (fixed document ID, no timestamp metadata). pageToPng(report, i, {scale}) rasterizes one page through the preview's own paint path at exactly round(page × scale) pixels; out-of-range pages throw RangeError, non-positive scales throw ArgumentError.
    • JetReportPrinter — printReport(report, {jobName}) exports the PDF and presents the operating system's print dialog at the template's true page size (the one sanctioned exception to the library's headlessness). Returns false on user cancellation (not an error); throws PrintUnavailableException where the platform cannot print. The dialog sits behind the injectable PrintDialogPresenter seam, so hosts and tests can substitute it without platform channels.
    • JetReportPreview gains optional onExportPdf / onPrint callbacks: each non-null callback adds a localized (en/de/tr), keyboard-operable toolbar action; with both null the widget is unchanged from 011. The library only invokes the callback — saving/sharing stays host code.
    • Content problems (empty dataset, unresolved image, failed expression) never fail an export: artifacts show the same fallbacks as the preview, with report.diagnostics untouched.
    • New dependencies: pdf (pure-Dart PDF generation), printing (system print dialog; confined to the print seam), image (raster decoding for PDF embedding). The playground demonstrates save-as-PDF (file_selector) and print end-to-end from the preview toolbar; sandboxed macOS hosts need the com.apple.security.print entitlement to print.
  • Render engine — data-filled paginated preview. A host hands a designed template plus actual data to a public engine facade and gets a lazily-paginated, WYSIWYG, on-screen preview. New public surface from the single entry point:

    • JetReportEngine — render(template, source, {options}) composes the fill pass (expression evaluation, master/detail iteration, variables/aggregates) with the layout pass (pagination, repeated page chrome, PAGE_NUMBER/PAGE_COUNT). Never throws on malformed data; deterministic over (template, data, parameters, locale).
    • RenderOptions — per-render parameter values plus an explicit formatting locale (number/date/currency formatting follows it — never the app UI locale or the ambient Intl.defaultLocale).
    • RenderedReport / RenderedPage — the render output IR: an exact pageCount resolved by a cheap boundary-only pass, lazy per-page frame construction with caching (the first page renders without materializing the rest), and merged ordered diagnostics. Structured so a future export slice consumes it without rework.
    • JetReportPreview — a read-only paginated viewer with a top toolbar styled to match the designer: the report name as the title, bounded prev/next + arrow-key navigation with a localized "page X of N" indicator (en/de/tr with English fallback), a zoom group (out / tap-% to fit / in; the page scrolls when zoomed past fit-to-width), and an optional back button (onBack). Pages paint through the same paintFrame/CanvasPainter pipeline as the designer surface. RenderedReport carries the source template's name as title for the toolbar.
    • JetReportDesigner(onPreviewRequested:) — the top bar's Preview action is now wired to this host callback (receiving the live template), mirroring onSaveRequested/onOpenRequested; it renders disabled when unwired. The playground opens its rendered-invoice preview from it.
    • The full data-source API is now public: JetDataSource, JetInMemoryDataSource, JetJsonDataSource, JetObjectDataSource<T>, DataSet, DataRow — the same logical dataset renders byte-identically through all three implementations, including nested collections.
    • Diagnostics are now public: Diagnostic, DiagnosticSeverity, ReportDiagnostics (which gains add for merging). Unknown fields, missing parameters, expression errors, empty datasets, and URL-only images each yield a specific diagnostic plus a best-effort render.
    • The fill pass now iterates nested collections: a detail band bound to a collectionField repeats once per child row (children bands nest to arbitrary depth), completing the 009 master/detail authoring seam at render time. Declared template parameter defaults are applied when the host supplies no value.
    • Internal additive seam: ReportLayouter.layoutLazy produces pages on demand; the eager layout() is preserved as a thin wrapper over it, so existing rendering output is byte-stable.
    • The playground gains a runnable rendered-invoice example (in-memory, JSON, and object-backed variants) and a Preview path that opens it.
  • Data-aware designer — Invoice MVP. The designer can now describe, display, and bind to a data source's structure (tokens only this iteration — values are not yet filled/rendered). New/changed public surface from the single entry point:

    • JetDataSchema — a host-supplied data-source structure (a named dataset of FieldDefs) attached to the designer via the new JetReportDesigner dataSchema: parameter. Not embedded in the saved template; bindings are self-describing, so a report reopened without a source still shows its tokens.
    • FieldDef is now public and recursive — a JetFieldType.collection field carries its own child fields, modelling master/detail (e.g. an invoice with a nested lines collection) to arbitrary depth.
    • JetFieldType.collection — the nested-collection field type.
    • JetReportDesignerController gains setBinding / clearBinding / setImageField / createBoundElement (element data binding) and setBandCollection(bandPath, field) (designate a band's master/detail collection); each is one undoable step.
    • ReportBand gains additive-optional collectionField + nested children (master/detail); they round-trip losslessly at the existing schemaVersion (no migration — pre-1.0 additive carve-out).
    • Designer UX: the Data Source panel renders the attached schema as an expandable tree (nested collections included) with a clear empty state, replacing the hardcoded placeholder; leaf fields drag onto the canvas to create a bound element; the Properties inspector binds an element (field or expression) or designates a band's collection, clears a binding, and flags an unresolved binding; bound elements show a design-time token through the shared render pipeline. All new chrome localized (en/de/tr).
    • The playground ships an invoice sample (invoiceSchema + invoiceSampleTemplate) demonstrating master/detail through the public API.
    • Deferred: filling/rendering real data values, exposing the fill/expression engine, barcode binding, and design-canvas rendering of nested child bands (the model/codec/scope support arbitrary nesting today).
  • Designer edit surface. The center surface is now a fully interactive WYSIWYG canvas — create, select, move, resize, multi-select, snap, align/distribute/z-order, undo/redo, zoom/pan, inline text edit, model-driven panels, accessibility, localization, and a host save/open seam. New public surface from the single entry point:

    • JetReportDesignerController (ChangeNotifier) — holds the in-memory ReportTemplate, the Selection, and unlimited session undo/redo over immutable (template, selection) snapshots; exposes open, createElement, moveBy, live begin/update/commit/cancelMove, select/clearSelection, undo/redo. Headless (no file I/O).
    • JetReportFormat — a static facade over the versioned codec (encode/decode/encodeJson/decodeJson) with built-in element codecs + migrations pre-wired; lossless round-trip incl. UnknownElement and the full parameter/variable/group payload.
    • The ReportTemplate-reachable model graph + geometry/style types, plus the additive ReportElement.withBounds / copyWith value-copy helpers.
    • JetReportDesigner gains optional controller / initialReport / onSaveRequested / onOpenRequested (still const-constructible — the 002 contract holds).
    • Canvas: drag-from-toolbox or click-to-place create; click-select with eight resize handles; drag-to-move; fit-to-width zoom transform; per-element accessibility regions. Element appearance is painted through the unchanged shared render pipeline (ElementRenderer.emit + CanvasPainter), cached as a ui.Picture (no parallel draw code).
    • Undo/redo wired to the top bar (disabled at the history ends) and to canvas-focus-scoped ⌘Z / ⇧⌘Z (Ctrl on non-macOS).
    • Per-handle resize with a 4×4 pt minimum floor, plus snapping to the grid, sibling edges/centers, and band/page bounds with live guide lines; Alt/Option bypasses snapping; grid/snap toggles in the top bar.
    • Multi-select (shift-click; marquee rubber-band) and bulk operations — delete, z-order (forward/back/to-front/to-back), cut/copy/paste/duplicate (codec-cloned with fresh ids + offset), align (6 ways), distribute, and arrow-key nudge (Shift = 10 pt) — each one undoable.
    • Numeric geometry + text editing on the controller (setGeometry / setText) and inline text editing: double-click a text element to edit in place (Enter commits, Escape cancels), undoable.
    • Zoom / pan / fit: top-bar zoom in/out, click the zoom % to fit-to-width, ⌘±/⌘0 shortcuts, and trackpad/wheel pan (Ctrl/⌘+scroll zooms), clamped 25 %–400 %; placement stays pointer-accurate at every zoom.
    • Band-type badges: each band on the canvas carries a small localized caption (Page Header / Detail / Page Footer / …, all eleven BandTypes) at its top-left corner, so authors always know which band they are editing. The badge is constant-size UI chrome (legible at any zoom) and never captures pointers.
    • The page viewport scrolls when the (paper-sized) page doesn't fit: horizontal + vertical scrollbars appear and the wheel/trackpad scroll the sheet, so the bottom of a full A4 page is always reachable. Drag-to-scroll is intentionally disabled so a pointer drag still moves elements / rubber-band selects; Ctrl/⌘+wheel still zooms. A page smaller than the viewport is centered.
    • The design surface is a real, paper-sized sheet: it spans the page format's full dimensions (A4 portrait by default), flow bands (title / page header / detail / groups / …) stack from the top margin, and the page-/ column-footer bands are anchored to the bottom of the sheet with an empty flow gap between — true WYSIWYG, the way a rendered page looks. The surface grows if the authored bands exceed the sheet, and a drop in the empty gap snaps to the vertically nearest band.
    • The paper page surface is a constant white in every theme (WYSIWYG — it represents printed paper, and report content is emitted with print colors such as dark text that only read on white). Only the surrounding canvas and the app chrome follow the light/dark theme; the design-time chrome drawn on the page (band separators, band badges, the empty hint) uses a fixed paper-relative palette so it stays legible on white in dark mode too.
    • Resize handles show directional cursors on hover: each of the eight handles exposes the matching resize cursor (diagonal ↖↘/↗↙ for corners, ↕/↔ for edges) so the pointer signals which edges a drag moves. Corners sit above edges in the overlay so an overlapping edge hit-area can't mask the diagonal cursor when zoomed out. On macOS — whose public cursor set has no diagonal — the corners drive the native window-resize NSCursor directly, so they look the same as everywhere else.
    • The report (page) and individual bands are now selectable, not just elements. Clicking a band's empty area selects that band; clicking the paper off any band selects the report; clicking off the paper clears. The selection targets are mutually exclusive. Selection gains Selection.band(i) / Selection.report() with bandIndex / isReport; the controller gains selectBand / selectReport.
    • Bands resize vertically. A selected band shows a single divider handle on its growth-facing edge (the bottom for a flow band, the top for a bottom-anchored footer) — no element-style corner/side handles, since a band only has a height. Dragging it changes the band height (floor-clamped, one undoable step); setBandHeight exposes the same change numerically. A selected report shows an outline only (the sheet is a fixed format).
    • The Outline panel is now model-driven (was static sample content): it renders the live template as a tree (Report root → bands → elements), tapping a row selects that object through the controller (report / band / element), and the row matching the current selection is highlighted (and marked selected for accessibility). The disclosure chevron collapses/expands a branch independently of selection.
    • The Properties panel is now a model-driven, context-aware inspector (was static sample content): a selected element exposes live X / Y / W / H fields (committed through setGeometry) and, for a text element, its text (setText) — each edit one undoable step, with steppers for ±1 nudges and commit on Enter or blur; a selected band exposes its height (setBandHeight); the report shows read-only page info; and nothing/a multi-selection shows a friendly empty state. Fields reflect the live model, so a canvas move/resize updates the numbers and vice-versa.
    • Selecting an element scrolls it into view. When a selection comes from the Outline panel (or any non-canvas source), the canvas scrolls the element into the viewport so the user sees what they selected — a no-op when it is already visible. This completes the canvas ↔ Outline ↔ Properties two-way sync.
    • An "Arrange" menu in the top bar gathers the selection-wide layout actions: align (left / center / right / top / middle / bottom), distribute (horizontally / vertically), and z-order (bring to front / forward, send backward / to back) — each one undoable. The trigger enables once an element is selected; the align/distribute items further require two or more (a lone element has nothing to align against), while the z-order items act on a single element too.
    • Open / Save are wired to the host. The top bar gains an Open action beside Save; both call the new JetReportDesigner.onOpenRequested / onSaveRequested callbacks and render disabled when the host wired none. The library still performs no file I/O itself — the playground app implements the host side with file_selector + JetReportFormat.
    • Accessibility. Every interactive affordance now exposes a localized accessible name and a button role: each canvas element ("Text element …"), all eight resize handles (directional names) and the band-height handle, and the top-bar actions (Arrange, zoom, Open/Save). Element regions are discrete semantics nodes so a screen reader announces one element per stop.
    • Localization (en / de / tr). The Arrange menu, the Properties inspector labels, the Outline root, the Open action, and all the new accessible names are fully localized with English fallback — no raw keys or blank labels.
    • Fidelity + performance coverage. Added design-surface goldens (representative elements with a selection shown, light + dark, via the shared render pipeline) and a 200-element multi-select drag performance smoke.
    • This completes the designer edit surface; only the merge-gate house-keeping remains.
  • Report model foundation (Part 1): pure-Dart geometry value types (JetSize/JetOffset/JetEdgeInsets/JetRect), PageFormat, the element model (ReportElement, TextElement, UnknownElement), ReportBand/ BandType/ReportTemplate, an ElementCodecRegistry extension point, and versioned JSON serialization with a forward-migration framework (encodeTemplate/decodeTemplate, schemaVersion, SchemaMigration).

  • JetReportDesigner — the report designer shell widget: a top command bar, a left element toolbox (a compact icon toolbar with tooltips), a center design surface (a bounded paper page), and a right three-tab context panel (Data Source / Outline / Properties) in a theme-driven frame. Layout-only this iteration — every control is a non-functional placeholder; the live interactions are tab switching, splitter resize of the right panel (down to its minimum width), and collapse/expand of the right panel to an icon rail below the 1024px width breakpoint. The icon toolbox stays visible at every width.

  • JetPrintLocalizations — the library's own gen-l10n localization delegate covering the designer chrome in English (default/fallback), German, and Turkish, exported with its delegate and supportedLocales so consumers can wire it into their app shell. Unsupported locales and missing keys fall back to English.

  • Visual model completion (Part 2): style value types (JetColor with hex serialization, JetTextStyle, JetBoxStyle); text styling on TextElement (sparse-serialized); new element types ShapeElement (line/rectangle), ImageElement (url/field/base64-bytes sources, JetBoxFit), and BarcodeElement (QR / Code128 / EAN-13 / Data Matrix); and registerBuiltInElementCodecs to wire all four built-in element codecs.

  • Data layer: the headless data-access seam — JetDataSource (factory) → DataSet (forward-only cursor) → immutable DataRow snapshots, with typed FieldDef/JetFieldType metadata (best-effort column-type inference). Three in-memory implementations: JetInMemoryDataSource (List<Map>), JetJsonDataSource (JSON array string), and JetObjectDataSource<T> (typed object list). The architecture test now also enforces the data → domain boundary.

  • Expression engine core: the headless expression language — a sealed JetValue model (null/bool/number/string/date/error; numbers are double), a lexer/parser/AST/evaluator pipeline compiling expressions like $F{qty} * $F{price} and FORMAT(ROUND($F{total}, 2), '#,##0.00'), and a pluggable JetFunctionRegistry (engine extension point) with built-in math, string, logic, and format function families. Evaluation never throws — a bad operation yields a JetError value (rendered !ERR); only malformed syntax throws ExpressionException. RowEvalContext resolves $F{} from a DataRow and $P{} from a parameter map. The architecture test now enforces the expression -> domain/data boundary. (Aggregates, variables, groups, and $V{} references follow in 005b.)

  • Aggregates & variables: ReportVariable (with JetCalculation SUM/COUNT/AVG/MIN/MAX/FIRST/LAST or a plain expression, and report/group reset scopes), ReportGroup, and typed ReportParameter declarations join ReportTemplate and serialize sparsely (still schema v1 — additive). The expression engine gains $V{} variable references, and a one-pass VariableCalculator folds per-row values into running/group-scoped accumulators with group-break detection (outermost-changed group cascades to inner groups). JetFieldType moved to the domain seam (re-exported from data) so parameters and fields share one value-type taxonomy. Page/column reset scopes are deferred to 008 (pagination).

  • Frame, text-metrics & paint backends: the rendering display-list and first paint backend. PageFrame + FrameBuilder build a flat list of positioned FramePrimitives (text run / image / line / rect / path, each tagged with its originating element id). A headless text seam — an in-house TTF/OTF metrics parser (parseTtfMetrics), a byte-keyed FontRegistry with a bundled Latin default font (Noto Sans, OFL), and a line-level TextMeasurer (MetricsTextMeasurer) that owns word-wrapping — guarantees deterministic line breaks across backends. A ReportPainter abstraction (with async prepare) and the backend-agnostic paintFrame walk drive CanvasPainter (dart:ui). The architecture test now enforces that only CanvasPainter may import dart:ui; frame/ and text/ stay headless. Cross-backend pixel parity arrives with the PDF/PNG backends in 009. Replaces the ReportDocument/ReportLayout scaffold placeholders.

  • Element renderers. ElementRenderer<E> (measure + emit) paired with ElementCodec<E> via ElementTypeRegistry.register; built-in renderers for text, shape, image, and barcode/unknown placeholders; RenderContext; JetConstraints. MeasuredText gains a resolved fontFamily (006 amendment). Custom element types round-trip through JSON and render with zero core edits.

  • Fill data pass. ReportFiller turns a ReportTemplate + JetDataSource into a resolved band-instance stream (FilledReport) — title/detail/summary/noData — with per-row text expression and image FieldImageSource resolution, report-scoped running/grand totals, frozen variable snapshots, and a ReportDiagnostics (missing-field warnings; !ERR on bad expressions; rejection of illegal page-scoped variable use). Adds TextElement.expression.

  • Grouping in Fill. ReportFiller now emits groupHeader/groupFooter band instances with group-scoped subtotals at each group break — headers resolve the group's first row, footers the last row with the pre-reset subtotal. Adds an optional ReportBand.group link and a GroupBandIndex (fail-fast on duplicate group names; error diagnostics for null/unknown group references). Nesting order is derived from the authored group list.

  • Layout engine. ReportLayouter lays a FilledReport band stream onto pages: it measures body bands (grow-only, via the element renderers), stacks and paginates them in the per-page body region, and repeats pageHeader/pageFooter chrome on every page, emitting one PageFrame per page plus diagnostics. A pure BandMeasurer computes grown band heights. Chrome is emitted as authored (no expression evaluation yet — page-scoped substitution arrives in 008c); unresolved chrome bindings, chrome that overcommits the page, and not-yet-supported column/background bands are reported as diagnostics.

  • Group-aware pagination. Two opt-in ReportGroup flags: reprintHeaderOnEachPage repeats a group's header band(s) at the top of each continuation page it spans, and keepTogether moves a whole group instance to a fresh page rather than splitting it (when it fits a fresh page, accounting for any repeated outer headers). Group identity is carried into the internal Fill→Layout IR via FilledBand.group; the schema is unchanged (the codec contract comment now codifies the pre-1.0 additive-optional-fields carve-out). A flag on a header-less group is a no-op + info.

  • Page-scoped substitution. pageHeader/pageFooter text expressions are evaluated at layout time and substituted at their authored bounds: $V{PAGE_NUMBER}/$V{PAGE_COUNT} (as integer strings, e.g. Page 1 of 3) and report $P{params} (threaded through the IR as the normalized FilledReport.params). A new read-only Expression.references gives complete, branch-independent reference analysis, so unavailable chrome references ($F{}, non-page $V{}) are diagnosed once per element regardless of short-circuiting. Substitution is fixed-bounds (no repagination, no chrome box growth); parse/evaluation failures render !ERR. The schema is unchanged (FilledReport.params is internal IR).

Changed #

  • Grid snap step is now 5 mm (was 8 pt) so the snap grid coincides with the new visible grid and the millimetre rulers. This affects new interactive placements only; stored report coordinates are untouched.

  • Grid visibility is decoupled from grid snapping. The grid button now controls only whether the grid is drawn; snapping (grid + sibling + band) is governed solely by the magnet/snap toggle. Elements snap to the 5 mm grid even when the grid is hidden, and all four grid/snap on-off combinations are valid. JetReportDesignerController.gridEnabled / setGridEnabled keep their signatures but now mean visibility only.

  • Snapping now defaults to off. The grid is still shown by default, but the magnet/snap toggle starts off — switch it on to snap. (snapEnabled defaults to false.)

  • Top bar: the ruler toggle moved to the left of the view-toggle group, so the order is now ruler, grid, snap.

  • Designer: double-tap now opens the Properties inspector (in-place editing removed). Double-tapping anything on the canvas — an element, a band's empty area, or the report (paper off any band) — selects it, brings the right panel to the Properties tab (expanding the collapsed narrow-layout overlay first when needed), and moves keyboard focus into the most relevant field: Text for a text element, X for any other element, height for a band. The report inspector is read-only, so its double-tap simply brings the pane forward. The inline double-click text editor is gone; the Properties panel is the single text-editing surface. New JetReportDesignerController members back this: requestPropertiesFocus() raises a one-shot UI intent, pendingPropertiesFocus peeks at it, and takePropertiesFocus() consumes it (the designer chrome wires these automatically; hosts may call requestPropertiesFocus() to deep-link their own UI into the inspector).

Initial scaffold #

  • Single public entry point package:jet_print/jet_print.dart.
  • jetPrintVersion — the library's declared version string, establishing the SemVer baseline.
  • Internal layer seams under lib/src/ with an inward-only dependency rule enforced by an architecture test.
2
likes
160
points
242
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter library for building WYSIWYG report designers: a report model, a paginating render engine, PDF/PNG export, printing, and a themed designer.

Repository (GitHub)
View/report issues

Topics

#report #pdf #wysiwyg #designer #printing

Funding

Consider supporting this project:

github.com
buymeacoffee.com

License

Apache-2.0 (license)

Dependencies

barcode, flutter, flutter_localizations, image, intl, lucide_icons_flutter, pdf, printing, shadcn_ui, vector_math

More

Packages that depend on jet_print