jet_print 1.1.0
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 as125,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.pyadds a codepoint the same way. The package grows by about 0.5 KB. -
RenderOptions.currencySymbolsets 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.00showsTRY1.234,50underLocale('tr'). That stays the default, because changing it would change what existing reports print. PasscurrencySymbol: '₺'to print₺1.234,50instead, in the format property and inFORMAT(...)alike, page headers and footers included. Only the sign changes: the pattern still sets the decimal digits (#,##0 ¤is1.235 ₺), and the locale still sets the separators. -
Roll paper: pages that fit their content.
PageFormat.continuouslays 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, andheightbecomes 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.keepTogetherhas 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 ispageToPng(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
JetReportPrevieware supported, whileJetReportDesignerandJetReportWorkspacerun and are tested at phone width but are not yet verified on a real device. Nothing in the code changes. The1.0.0entry 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, madeJetReportExporter.toPdfthrowBad 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.notdefin 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": truein the page JSON and stamps the document"schemaVersion": 3. A sheet report writes neither, so its JSON is byte-for-byte unchanged and1.0.0keeps opening it.1.0.0refuses a roll report with aReportFormatException(its version is newer), rather than opening it as a sheet and dropping the roll when it saves.JetReportFormat.schemaVersionis now3: 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 fromRenderOptions.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 #
hasErrorson any collection of diagnostics. The new exportedDiagnosticQueriesextension gives theList<Diagnostic>thatvalidatereturns the samehasErrorsthe render-timeReportDiagnosticsalready had, so a host's check isvalidate(definition).hasErrors. Render itself still does not validate; that is now the documented 1.0 behaviour onJetReportEngine. (#98)
Changed #
-
Breaking (behaviour): a group is keyed by its id; its name is a label.
ReportVariable.resetGroupmust name aGroupLevelbyid. A reset that names a group by itsname, which resolved before when the two differed, is now a validation error and resets nothing. Two groups in a scope may share a name, whichvalidateno longer reports. Renaming a group changes nothing the report prints, except through a print callback that readsElementPrintContext.bandName, which still carries the name. Saved reports are unaffected: the 1→2 migration already wroteresetGroupas an id. A crosstab's synthetic group now collides with a report group only by id. (#100) -
Breaking (API):
RenderedPage.frameandJetReportDesignerController.activeGuidesare removed. Both had types the barrel never exported:framethe engine's internal display list,activeGuidesthe canvas's snap guides. A consumer could reach members of types it could not name, and they would have frozen at 1.0 unpublished.RenderedPagekeepsindex, and its constructor is no longer public; a report's pages still come fromRenderedReport.pageAt. Inspect rendered output through the exporters (JetReportExporter.toPdf,pageToPng). (#105) -
Breaking (default): an unknown field is
#ERRORwhen the data source declares its schema. WithRenderOptions.knownFieldsleft null, the engine now derives it from a source with an explicit schema:JetObjectDataSourceandJetPagedDataSourcealways, andJetInMemoryDataSourceandJetJsonDataSourcewhen givenfields:. Nested collection fields are included. A binding to a field outside that schema rendersRenderOptions.unresolvedFieldToken(#ERRORby default) and records a warning, instead of an empty cell. Sources with an inferred schema, and customJetDataSources, render as before. A host's ownknownFieldsstill replaces the derived set. (#99) -
Breaking (API): the preview's export and print callbacks receive the report and may be async.
JetReportPreview.onExportPdfand.onPrintwereVoidCallbacks, andJetReportWorkspace's wereValueChanged<RenderedReport>; both are now aRenderedReportCallback,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 newJetReportPreview.onError.JetReportWorkspaceforwards itsonErrorthere, as it already did to the designer. Without anonError, 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'sValueChanged<RenderedReport>still compiles.ReportErrorCallbackis unchanged but now lives in its own library; it is still exported fromjet_print.dart. (#101) -
Breaking (API):
RenderedReport.fontsand the constructor'sfontsparameter 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. ARenderedReportbuilt with its public constructor paints with the bundled default font, as it did whenfontswas omitted. (#102) -
Breaking (behaviour):
validaterejects a band in a reserved furniture slot.PageFurniture.columnHeader,columnFooterandbackgroundwere never laid out; a band there drew nothing but an info diagnostic at render, yetvalidateaccepted it. Now each is an error, so the slots can be laid out in a later release without changing the output of a definitionvalidateaccepts. 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 as0.3 - 0.1 - 0.2sums 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, throughFORMATand theformatproperty alike. Numbers stay doubles for 1.0; theJetNumberdartdoc says so. (#95) -
Breaking (output):
UPPERandLOWERfollow the render locale. Under a Turkish or AzerbaijaniRenderOptions.locale,UPPERmapsitoİandLOWERmapsItoıandİtoi, soUPPER("Kısa Vadeli")printsKISA VADELİinstead ofKISA VADELI. Every other locale, including the defaulten, cases exactly as before. Atrorazreport that changes the case of anirenders differently. Outside a render, the functions followIntl.getCurrentLocale(), asFORMATdoes. (#97)
Fixed #
validateno longer warns about an aggregate nested in a summary expression.SUM($F{debit}) - SUM($F{credit})orROUND(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, soSUM($F{typo}) + 1orSUM($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 passedvalidate, but the aggregate synthesizer only liftedSUM(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.yamllists GitHub Sponsors and Buy Me a Coffee underfunding:, 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-timevalidate()diagnostics, versioned serialization, and a forward migration framework.- Render & export —
JetReportEnginefills and paginates lazily;JetReportExporterwrites deterministic PDFs (selectable text, embedded fonts) and PNGs;JetReportPrinterdrives system printing behind an injectable seam.- Interactive designer —
JetReportDesignerwith 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 #
-
JetReportPreviewsays that it needs a shadcn_ui theme. The preview reads the ambientShadTheme, so under aMaterialAppalone it throws on its first build, and neither its dartdoc nor the README Quickstart said so. Both now do: wrap it in aShadApp, or in aShadThemeinside aMaterialApp, withJetPrintLocalizations.delegatewired, and addshadcn_uito the app's own dependencies to import them. The Quickstart shows theShadAppfromexample/jet_print_example.dart. The dartdoc's code sample, which still called the removedJetReportEngine.render, now callsrenderDefinition. 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_uiallowslucide_icons_flutter3.0.0, but some icons the designer uses are missing from versions before 3.1.14 (textAlignStart/Center/Endfirst ship in 3.1.1, andcircleHelpis absent from most 3.1.x releases before 3.1.14), so a downgraded resolution failed to compile.lucide_icons_flutter: ^3.1.14is now declared to raise that floor.intlis constrained to^0.20.2(it wasany). -
The README quickstart compiles. Its
TextElementwas missing the requiredtext: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
renderReportordataSchemachanged 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 programmaticSetBandHeightCommandis 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
propertiesGroupOnFooterHintin 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 namedDraft, andC:\templost 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.
JetReportWorkspacecached the rendered report by definition alone and had nodidUpdateWidget, so a host that changed itsrenderReportcallback ordataSchema— 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
startNewPagebreak, joined the first'skeepTogetherspan, 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.decodeDefinitionanddecodeDefinitionJsonpromisedReportFormatException, but a truncated or hand-edited document could escape as aTypeError(a!orascast meeting the wrong type), anArgumentError(an unknown enum name such as a variablecalculationof"median") or aFormatException(text that is not JSON), so a host catchingon ReportFormatExceptioncrashed. Both entry points now convert those into aReportFormatExceptionthat 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 ofJetReportEngine.renderDefinition— while a malformed element expression has always rendered!ERRwith 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 throwExpressionException; 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 whereqtyis 0 — was skipped exactly like a blank, so the total came out short with no!ERRand 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 like1e400or 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/finallythat closes it at the row loop, so anything that threw in between — a malformed variable or group key fails fast there — left a customDataSet(a database cursor, a file handle) open. The whole fill now runs under onefinallythat 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
deandtr. They are now thefieldPickerSearchHintandfieldPickerNoMatchesstrings 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 isSUM($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 itsname; it now usescopyWith. -
Page headers and footers now honor
visibleandformat. 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'sformatwas 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 withcopyWith, so it cannot drop a field either. -
Band.copyWithcan now clearnameandcolumnLayout. Both took plain nullable parameters, sonullmeant "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 dayBandgains one. Both parameters are now thunks (name: () => nullclears) — breaking for callers passing a value, which becomesname: () => 'x'— and both commands go throughcopyWith. -
A chart type this build does not know is no longer rewritten to
bar. A report from a newer build naming, say, anareachart 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 newChartElement.unknownChartTypeand written back on save, the same contractShapeElement.unknownFormalready 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.copyWithtook plain nullabletitle/categoryExpressionparameters, so anullmeant "keep" and nothing could clear them. Both parameters are now thunks, likename(title: () => nullclears) — breaking for callers passingtitle: 'x', which becomestitle: () => 'x'.setChartOptionskeeps its signature and, likesetFormat, now reads an emptytitleorcategoryExpressionas "clear". -
The declared Flutter floor now matches what is actually tested.
packages/jet_print/pubspec.yamldeclaredflutter: ">=3.6.0"— 38 minor versions below anything the package has ever been built against. CI pins 3.44.0 in all four jobs andAGENTS.mddocuments 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 wholeImagePrimitive. BecauseImagePrimitive's value equality includesboundsandelementId— 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_embeddedImagescache in the same class and the equivalent fix already made toCanvasPainter. Output is unchanged: the byte-pinnedinvoice.pdfgolden is identical, since embedding was already deduplicated. -
Documentation: the element and expression registries were described as host extension points they are not.
JetFunctionRegistry,ElementCodecRegistry,ElementRendererRegistryandElementTypeRegistryeach advertised themselves as a public/open-closed seam — "consumersregistercustom functions with zero core edits", "consumers add their own types", "a consumer can override a built-in". None of them is exported frompackage:jet_print/jet_print.dart, and no public entry point accepts one: the instances they feed are private and pre-wired (JetReportFormat's never-mutatedstatic 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.fontsandPrintDialogPresenter— and what is internal;docs/06-round-tripping.mdcovers 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 thejet_printrepository, not by a host. A public registration API would stretch every saved report's compatibility promise over types this library did not define, sojet_printdoes 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.Paragraphper line on every record.CanvasPainter.drawTextRunbuilds 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.drawParagraphhas 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 andPageRasterizerhold theReportPainterabstraction (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 theReportPaintercontract, and the recorder → painter →paintFrame→endRecording→ release sequence is single-sourced inrecordPageFrame, so no consumer can drop the last step; a new architecture test keepsCanvasPainterconstruction to that one seam.PageRasterizeralso now disposes the intermediateui.Pictureit rasterizes from, which was leaked on the same path. -
Charts can be saved, duplicated and pasted again.
ChartElementwas registered only in the render-time codec+renderer pairing (registerBuiltInElementTypes) and never in the codec-onlyregisterBuiltInElementCodecs, which is what the persistence paths use. A report containing a chart therefore rendered correctly but threwBad 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 bytest/architecture/built_in_element_registration_test.dart, so a future element type cannot reach one list without the other.
Changed #
-
Requires
shadcn_ui0.57.1 (was^0.54.0), the latest release. A host app that pinsshadcn_uibelow 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
CrosstabStyleplusCrosstabMeasure'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.Selectiongains a sixth target,crosstabId, and itsSelection.crosstabfactory. 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
Crosstabblock — configurable row/column axisCrosstabGroups (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 viaCrosstabStyle— prints once within aDetailScopevia a newCrosstabNode. It folds the scope's own rows (or a nested collection, viacollectionField) 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 theCrosstabSortenum);CrosstabMatrix/CrosstabPlan/the aggregator stay internal. Breaking: the sealedScopeNodegains two new variants —CrosstabNodeandUnknownScopeNode(losslessly round-trips a scope node whosekindthis build doesn't recognize, the crosstab's own forward-migration seam) — so any downstream exhaustiveswitchoverScopeNodemust add cases for both. -
BREAKING — the report model is reified. The flat
ReportTemplateband list — where a band's role was inferred fromtype+ group-name +collectionField+ sibling position — is replaced by an explicit, id'd section tree,ReportDefinition:ReportDefinition { name, page, parameters, variables, furniture: PageFurniture, body: ReportBody }.PageFurnitureholds the record-blind page chrome (page/column header & footer, background — the last three reserved, not yet laid out);ReportBodyholds the once-bands (title/summary/no-data) and the masterDetailScope.DetailScope { id, collectionField?, groups: GroupLevel[], children: ScopeNode[] }with the sealedScopeNode = BandNode(Band) | NestedScope(DetailScope).childrenis 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.startNewPageis nowGroupLevel.startNewPage. - Every
Bandcarries a stableid; its rendering role is stated by its position in the tree, withBandTyperetained for labels, glyphs, identity, and faithful migration (and validated consistent with its slot). - The engine, serialization, and designer consume
ReportDefinitiondirectly. Rendering is byte-identical to the previous engine for every existing report (golden-locked). - New author-time
validate(ReportDefinition)returnsDiagnostics 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, theReportBandclass (theBandTypeenum stays), and the publicReportGrouptype are removed, along withJetReportEngine.render(ReportTemplate),JetReportFormat.encode/decode/encodeJson/decodeJson, and the v1 codec. UseJetReportEngine.renderDefinition,JetReportFormat.encodeDefinition/decodeDefinition, andencodeDefinitionJson/decodeDefinitionJson.
Migration #
- v1 → v2 schema migration (lossless). A document saved in the v1 (flat)
format loads forward automatically:
JetReportFormat.decodeDefinitionJsonmigrates a v1 JSON map to aReportDefinition— every v1 construct maps to a v2 home and master-level band/sub-scope order is preserved. The schema version is now2, public asJetReportFormat.schemaVersion.
Added #
-
JetReportFormat.schemaVersion, the report schema version this build writes (2): theschemaVersionkey 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 mirrorsJetDataSourceFile.versionfor the data-source format. It is aconst, 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.onElementPrintandPrintDialogPresenter) 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 fromexample/designer_example.dartandexample/data_example.dart(new) andexample/jet_print_example.dart, which tests run, andtest/readme_snippets_test.dartfails 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 oneGroupLevel(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, pluscreateGroup/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 whentrue, 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) andJetFontFamily({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 widgetbuild()or arender(). FontFormatExceptionis now exported — the single detectable rejection type.- Two host touch-points, one shared list:
RenderOptions.fonts(threaded through the render chain) andfontsonJetReportDesigner/JetReportWorkspace(the designer picker + canvas). Pass the sameList<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.renderbuilds oneFontRegistry(defaults + host families), measures with it, and carries it on the returnedRenderedReport; 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.FontRegistrystays internal. - No schema change: reports still store only the font-family name
(
schemaVersionunchanged). 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.
- Two bytes-in value types:
-
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/semiBoldare preserved until the toggle is operated), a swatch + hex color editor, and left/center/right alignment segments (a storedjustifyshows no active segment and survives unrelated edits). - Underline, end-to-end (the one net-new attribute):
JetTextStylegainsunderline(defaultfalse, serialized only whentrue). Both painters draw it as an explicit stroked segment from one shared geometry helper — neverTextDecoration— 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#AARRGGBBwhen 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(sentinelfontFamily),JetBoxStyle.copyWith(sentinelfill/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, andFontRegistry(including its new internalfamiliesgetter) 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.Defaultis 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:
kReportSchemaVersionstays 1, no migration.underlineis 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.
- 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
-
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
ShapeKindvalues (ellipse,triangle,diamond,pentagon,hexagon,star),JetReportDesignerController.setShapeKind(String, ShapeKind)(one undoable, notifying step), andShapeElement.copyWith(...)plus an optionalunknownFormfield on the already-public immutable type. The geometry (shapePath), theSetShapeKindCommand, and the gallery widget stay private. - Rendering fidelity (WYSIWYG): every closed form emits a single
PathPrimitivefrom one sharedshapePath, 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:
kReportSchemaVersionstays 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 inunknownForm, 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.
- New public surface is additive and minimal: six new
-
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), plusPageFormat.copyWith(...)andJetEdgeInsets.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 —kReportSchemaVersionstays 1, no migration, old templates load unchanged. New labels are localized in en/de/tr.
- New public surface is additive and minimal:
-
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 vialoadingBuilder) so the first large-data render gives visible feedback instead of a frozen frame. A failed render is reported viaFlutterError.reportErrorand clears the indicator. Export/print receive the currentRenderedReport. -
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 firesonBack— 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 theJetReportPreview.onRenamehook, 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:kReportSchemaVersionstays1and report goldens are byte-identical.
- The mode switch reuses the existing switch events — selecting Preview
from the designer fires
-
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
JetReportDesignerwith 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,
canCopyandcanPaste, mirror the existingcanUndo/canRedoidiom;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
schemaVersionchange).
-
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
schemaVersionchange).
-
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 inTextElement.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 sharedapplyJetFormathelper (the same logic behind theFORMATfunction, 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
#ERRORtoken in schema-aware contexts (designer/preview).RenderOptionsgains two additive fields —knownFields(Set<String>?, the data source's declared field names) andunresolvedFieldToken(default#ERROR); whenknownFieldsis 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 aBuildContextpasses a localized value. - New Properties-panel strings localized en/de/tr with English fallback.
- The Value field recognizes three forms — a whole-value
-
Export & print — PDF, PNG, and the system print dialog (spec 012-export-support). The same
RenderedReportthe preview displays now turns into shareable artifacts; one render feeds every target. New public surface from the single entry point:JetReportExporter— statelessconstfacade.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'scomputeImageFitgeometry. 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 exactlyround(page × scale)pixels; out-of-range pages throwRangeError, non-positive scales throwArgumentError.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). Returnsfalseon user cancellation (not an error); throwsPrintUnavailableExceptionwhere the platform cannot print. The dialog sits behind the injectablePrintDialogPresenterseam, so hosts and tests can substitute it without platform channels.JetReportPreviewgains optionalonExportPdf/onPrintcallbacks: 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.diagnosticsuntouched. - 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 thecom.apple.security.printentitlement 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 ambientIntl.defaultLocale).RenderedReport/RenderedPage— the render output IR: an exactpageCountresolved by a cheap boundary-only pass, lazy per-page frame construction with caching (the first page renders without materializing the rest), and merged ordereddiagnostics. 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 samepaintFrame/CanvasPainterpipeline as the designer surface.RenderedReportcarries the source template's name astitlefor the toolbar.JetReportDesigner(onPreviewRequested:)— the top bar's Preview action is now wired to this host callback (receiving the live template), mirroringonSaveRequested/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 gainsaddfor 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
collectionFieldrepeats 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.layoutLazyproduces pages on demand; the eagerlayout()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 ofFieldDefs) attached to the designer via the newJetReportDesignerdataSchema:parameter. Not embedded in the saved template; bindings are self-describing, so a report reopened without a source still shows its tokens.FieldDefis now public and recursive — aJetFieldType.collectionfield carries its own childfields, modelling master/detail (e.g. an invoice with a nestedlinescollection) to arbitrary depth.JetFieldType.collection— the nested-collection field type.JetReportDesignerControllergainssetBinding/clearBinding/setImageField/createBoundElement(element data binding) andsetBandCollection(bandPath, field)(designate a band's master/detail collection); each is one undoable step.ReportBandgains additive-optionalcollectionField+ nestedchildren(master/detail); they round-trip losslessly at the existingschemaVersion(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-memoryReportTemplate, theSelection, and unlimited session undo/redo over immutable(template, selection)snapshots; exposesopen,createElement,moveBy, livebegin/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.UnknownElementand the full parameter/variable/group payload.- The
ReportTemplate-reachable model graph + geometry/style types, plus the additiveReportElement.withBounds/copyWithvalue-copy helpers. JetReportDesignergains optionalcontroller/initialReport/onSaveRequested/onOpenRequested(stillconst-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 aui.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-resizeNSCursordirectly, 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.
SelectiongainsSelection.band(i)/Selection.report()withbandIndex/isReport; the controller gainsselectBand/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);
setBandHeightexposes 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/onSaveRequestedcallbacks and render disabled when the host wired none. The library still performs no file I/O itself — the playground app implements the host side withfile_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, anElementCodecRegistryextension 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 itsdelegateandsupportedLocalesso 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 (
JetColorwith hex serialization,JetTextStyle,JetBoxStyle); text styling onTextElement(sparse-serialized); new element typesShapeElement(line/rectangle),ImageElement(url/field/base64-bytes sources,JetBoxFit), andBarcodeElement(QR / Code128 / EAN-13 / Data Matrix); andregisterBuiltInElementCodecsto wire all four built-in element codecs. -
Data layer: the headless data-access seam —
JetDataSource(factory) →DataSet(forward-only cursor) → immutableDataRowsnapshots, with typedFieldDef/JetFieldTypemetadata (best-effort column-type inference). Three in-memory implementations:JetInMemoryDataSource(List<Map>),JetJsonDataSource(JSON array string), andJetObjectDataSource<T>(typed object list). The architecture test now also enforces thedata → domainboundary. -
Expression engine core: the headless expression language — a sealed
JetValuemodel (null/bool/number/string/date/error; numbers aredouble), a lexer/parser/AST/evaluator pipeline compiling expressions like$F{qty} * $F{price}andFORMAT(ROUND($F{total}, 2), '#,##0.00'), and a pluggableJetFunctionRegistry(engine extension point) with built-in math, string, logic, and format function families. Evaluation never throws — a bad operation yields aJetErrorvalue (rendered!ERR); only malformed syntax throwsExpressionException.RowEvalContextresolves$F{}from aDataRowand$P{}from a parameter map. The architecture test now enforces theexpression -> domain/databoundary. (Aggregates, variables, groups, and$V{}references follow in 005b.) -
Aggregates & variables:
ReportVariable(withJetCalculationSUM/COUNT/AVG/MIN/MAX/FIRST/LAST or a plain expression, and report/group reset scopes),ReportGroup, and typedReportParameterdeclarations joinReportTemplateand serialize sparsely (still schema v1 — additive). The expression engine gains$V{}variable references, and a one-passVariableCalculatorfolds per-row values into running/group-scoped accumulators with group-break detection (outermost-changed group cascades to inner groups).JetFieldTypemoved to thedomainseam (re-exported fromdata) 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+FrameBuilderbuild a flat list of positionedFramePrimitives (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-keyedFontRegistrywith a bundled Latin default font (Noto Sans, OFL), and a line-levelTextMeasurer(MetricsTextMeasurer) that owns word-wrapping — guarantees deterministic line breaks across backends. AReportPainterabstraction (with asyncprepare) and the backend-agnosticpaintFramewalk driveCanvasPainter(dart:ui). The architecture test now enforces that onlyCanvasPaintermay importdart:ui;frame/andtext/stay headless. Cross-backend pixel parity arrives with the PDF/PNG backends in 009. Replaces theReportDocument/ReportLayoutscaffold placeholders. -
Element renderers.
ElementRenderer<E>(measure + emit) paired withElementCodec<E>viaElementTypeRegistry.register; built-in renderers for text, shape, image, and barcode/unknown placeholders;RenderContext;JetConstraints.MeasuredTextgains a resolvedfontFamily(006 amendment). Custom element types round-trip through JSON and render with zero core edits. -
Fill data pass.
ReportFillerturns aReportTemplate+JetDataSourceinto a resolved band-instance stream (FilledReport) — title/detail/summary/noData — with per-row textexpressionand imageFieldImageSourceresolution, report-scoped running/grand totals, frozen variable snapshots, and aReportDiagnostics(missing-field warnings;!ERRon bad expressions; rejection of illegal page-scoped variable use). AddsTextElement.expression. -
Grouping in Fill.
ReportFillernow emitsgroupHeader/groupFooterband 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 optionalReportBand.grouplink and aGroupBandIndex(fail-fast on duplicate group names; error diagnostics for null/unknown group references). Nesting order is derived from the authored group list. -
Layout engine.
ReportLayouterlays aFilledReportband stream onto pages: it measures body bands (grow-only, via the element renderers), stacks and paginates them in the per-page body region, and repeatspageHeader/pageFooterchrome on every page, emitting onePageFrameper page plus diagnostics. A pureBandMeasurercomputes 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
ReportGroupflags:reprintHeaderOnEachPagerepeats a group's header band(s) at the top of each continuation page it spans, andkeepTogethermoves 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 viaFilledBand.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/pageFootertext 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 normalizedFilledReport.params). A new read-onlyExpression.referencesgives 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.paramsis 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/setGridEnabledkeep 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. (
snapEnableddefaults tofalse.) -
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
JetReportDesignerControllermembers back this:requestPropertiesFocus()raises a one-shot UI intent,pendingPropertiesFocuspeeks at it, andtakePropertiesFocus()consumes it (the designer chrome wires these automatically; hosts may callrequestPropertiesFocus()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.
