Skip to main content

ravenrs_report/
engine.rs

1//! Rendering: binds data to bands, groups and sorts rows, lays out each band
2//! instance (controls grow to fit their text), and paginates.
3
4use std::cell::RefCell;
5use std::collections::HashMap;
6use std::rc::Rc;
7
8use ravenrs_core::{Color, ImageData, Point, Rect};
9use ravenrs_text::{TextAttrs, TextSystem};
10use serde_json::Value as Json;
11
12use crate::document::{Document, DocumentImage, ImageFormat, Item, Page, PlacedLine, TextBlock, parse_color};
13use ravenrs_criteria::{
14    Criteria, Culture, CustomFunction, DateTime, Environment, ParseError, Scope, Value, format_composite,
15    format_value,
16};
17
18use crate::chart::LabelAlign;
19use crate::model::{
20    Band, Control, ControlKind, LineDirection, ParameterKind, PictureMode, Report, ShapeKind, SortField,
21    Style, TableCell, TextAlign, VerticalAlign,
22};
23
24/// Settings for one run of a report.
25#[derive(Clone, Debug, Default)]
26pub struct RenderOptions {
27    /// Parameter values by name; missing ones take their defaults.
28    pub parameters: HashMap<String, serde_json::Value>,
29    /// The time `Today()` and `Now()` return; the system clock when `None`.
30    pub now: Option<DateTime>,
31}
32
33/// Renders reports. Keep one around: it caches fonts and text measurements.
34///
35/// ```
36/// use ravenrs_report::{Band, Control, Report, ReportEngine};
37/// use serde_json::json;
38///
39/// let report = Report::new("Hello").detail(Band::new(20.0).control(Control::field("[Name]").at(0.0, 0.0, 200.0, 20.0)));
40/// let doc = ReportEngine::new().render(&report, &json!([{ "Name": "Ana" }, { "Name": "Bruno" }]));
41/// assert_eq!(doc.pages.len(), 1);
42/// assert!(doc.page_contains(0, "Bruno"));
43/// ```
44pub struct ReportEngine {
45    text: TextSystem,
46    measured: HashMap<(String, u32, bool, u32), Rc<Measured>>,
47    functions: Vec<(String, CustomFunction)>,
48    images: HashMap<String, Option<DocumentImage>>,
49}
50
51impl std::fmt::Debug for ReportEngine {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.debug_struct("ReportEngine").finish_non_exhaustive()
54    }
55}
56
57impl Default for ReportEngine {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63struct Measured {
64    lines: Vec<(std::ops::Range<usize>, f32, f32, f32)>, // range, width, top, baseline
65    height: f32,
66}
67
68/// A laid-out band instance: its height and items relative to its top-left.
69struct Block {
70    height: f32,
71    items: Vec<Item>,
72    keep_with_next: bool,
73    break_before: bool,
74    break_after: bool,
75    /// Top-level row printed by this block (for page aggregates).
76    row: Option<usize>,
77    /// Group level this block opens a header for (repeated headers).
78    header_level: Option<usize>,
79    /// The report header: printed above the page header on the first page.
80    report_header: bool,
81}
82
83const DEFAULT_SIZE: f32 = 10.0;
84/// Text on a chart's axes and legend.
85const CHART_MUTED: Color = Color::rgba(0.353, 0.388, 0.459, 1.0);
86/// A chart's gridlines.
87const CHART_GRID: Color = Color::rgba(0.855, 0.867, 0.898, 1.0);
88/// The largest side of an image, in pixels.
89const MAX_IMAGE_SIDE: u32 = 12_000;
90/// The most memory decoding one image may use.
91const MAX_IMAGE_BYTES: u64 = 512 * 1024 * 1024;
92/// How many decoded images an engine remembers.
93const MAX_CACHED_IMAGES: usize = 256;
94
95impl ReportEngine {
96    /// An engine with the embedded Inter font.
97    pub fn new() -> Self {
98        Self {
99            text: TextSystem::new(),
100            measured: HashMap::new(),
101            functions: Vec::new(),
102            images: HashMap::new(),
103        }
104    }
105
106    /// Adds a function to the expressions of every report this engine renders.
107    pub fn register_function(
108        &mut self,
109        name: &str,
110        function: impl Fn(&[Value]) -> Result<Value, String> + 'static,
111    ) {
112        self.functions.push((name.to_owned(), Rc::new(function)));
113    }
114
115    /// Renders `report` over `data` with default parameters.
116    pub fn render(&mut self, report: &Report, data: &Json) -> Document {
117        self.render_with(report, data, &RenderOptions::default())
118    }
119
120    /// How many rows of `data` the report would print: after its data member and
121    /// filter, before grouping. Lets a scheduled job skip a report with nothing in it.
122    pub fn count_rows(&self, report: &Report, data: &Json, options: &RenderOptions) -> usize {
123        Run::new(report, options, &self.functions).rows(data).len()
124    }
125
126    /// Renders `report` over `data`. Expression errors do not stop the render:
127    /// they are listed in [`Document::errors`] and shown in place.
128    pub fn render_with(&mut self, report: &Report, data: &Json, options: &RenderOptions) -> Document {
129        let run = Run::new(report, options, &self.functions);
130        let rows_owned = run.rows(data);
131        let rows: Vec<&Json> = rows_owned.iter().collect();
132        let blocks = run.blocks(self, &rows);
133        let pages = run.paginate(self, blocks, &rows);
134        let errors = run.errors.into_inner();
135        Document { title: report.name.clone(), culture: report.culture.clone(), pages, errors }
136    }
137
138    fn measure(&mut self, text: &str, size: f32, bold: bool, width: Option<f32>) -> Rc<Measured> {
139        let key = (text.to_owned(), size.to_bits(), bold, width.map_or(u32::MAX, f32::to_bits));
140        if let Some(m) = self.measured.get(&key) {
141            return m.clone();
142        }
143        let attrs = text_attrs(size, bold);
144        let mut layout = self.text.layout(text, &attrs);
145        let size = layout.measure(width);
146        let lines = layout.lines().into_iter().map(|l| (l.range, l.width, l.top, l.baseline)).collect();
147        let m = Rc::new(Measured { lines, height: if text.is_empty() { 0.0 } else { size.height } });
148        // Bounded, so a long report cannot grow the cache without limit.
149        if self.measured.len() > 50_000 {
150            self.measured.clear();
151        }
152        self.measured.insert(key, m.clone());
153        m
154    }
155
156    /// An image from its source text, decoded once: a logo on every page, or the
157    /// same photo on many rows, is one image.
158    fn image(&mut self, source: &str) -> Option<DocumentImage> {
159        if let Some(image) = self.images.get(source) {
160            return image.clone();
161        }
162        let image = decode_picture(source);
163        if self.images.len() >= MAX_CACHED_IMAGES {
164            self.images.clear();
165        }
166        self.images.insert(source.to_owned(), image.clone());
167        image
168    }
169
170    /// The text system used for measuring, to paint documents with the same metrics.
171    pub fn text_system(&mut self) -> &mut TextSystem {
172        &mut self.text
173    }
174}
175
176pub(crate) fn text_attrs(size: f32, bold: bool) -> TextAttrs {
177    TextAttrs {
178        size,
179        line_height: (size * 1.25).ceil(),
180        weight: if bold { 600.0 } else { 400.0 },
181        letter_spacing: 0.0,
182        family: None,
183    }
184}
185
186/// Per-render state.
187struct Run<'r> {
188    report: &'r Report,
189    env: Environment,
190    exprs: RefCell<HashMap<String, Rc<Result<Criteria, ParseError>>>>,
191    errors: RefCell<Vec<String>>,
192}
193
194/// A resolved style with defaults filled in.
195struct Look {
196    size: f32,
197    bold: bool,
198    color: Color,
199    background: Option<Color>,
200    border: Option<(f32, Color, [bool; 4])>,
201    padding: f32,
202    align: TextAlign,
203    valign: VerticalAlign,
204    wrap: bool,
205}
206
207impl<'r> Run<'r> {
208    fn new(report: &'r Report, options: &RenderOptions, functions: &[(String, CustomFunction)]) -> Self {
209        let mut env = Environment::new()
210            .culture(Culture::named(&report.culture))
211            .at(options.now.unwrap_or_else(DateTime::now));
212        for p in &report.parameters {
213            let json = options.parameters.get(&p.name).or(p.default.as_ref());
214            let v = json.map_or(Value::Null, Value::from_json);
215            let v = match (p.kind, &v) {
216                (ParameterKind::Date, _) => v.as_date().map_or(v, Value::Date),
217                (ParameterKind::Number, Value::Text(_)) => v.as_number().map_or(v, Value::Number),
218                _ => v,
219            };
220            env = env.parameter(p.name.clone(), v);
221        }
222        for (k, v) in &options.parameters {
223            if !report.parameters.iter().any(|p| &p.name == k) {
224                env = env.parameter(k.clone(), Value::from_json(v));
225            }
226        }
227        for (name, function) in functions {
228            let function = function.clone();
229            env = env.register(name, move |args| function(args));
230        }
231        let errors = RefCell::new(Vec::new());
232        for c in &report.calculated_fields {
233            match Criteria::parse(&c.expression) {
234                Ok(e) => env = env.calculated(c.name.clone(), e),
235                Err(e) => errors.borrow_mut().push(format!("calculated field '{}': {e}", c.name)),
236            }
237        }
238        Self { report, env, exprs: RefCell::new(HashMap::new()), errors }
239    }
240
241    /// The parsed expression for `src`, cached.
242    fn expr(&self, src: &str) -> Rc<Result<Criteria, ParseError>> {
243        if let Some(e) = self.exprs.borrow().get(src) {
244            return e.clone();
245        }
246        let parsed = Rc::new(Criteria::parse(src));
247        self.exprs.borrow_mut().insert(src.to_owned(), parsed.clone());
248        parsed
249    }
250
251    fn eval(&self, ctx: &Scope<'_, Json>, src: &str, what: &str) -> Result<Value, String> {
252        let problem = match &*self.expr(src) {
253            Ok(e) => match ctx.evaluate(e) {
254                Ok(v) => return Ok(v),
255                Err(e) => e.to_string(),
256            },
257            Err(e) => e.to_string(),
258        };
259        let msg = format!("{what}: {problem} in `{src}`");
260        let mut errors = self.errors.borrow_mut();
261        if !errors.contains(&msg) {
262            errors.push(msg);
263        }
264        Err(problem)
265    }
266
267    fn ctx<'a>(
268        &'a self,
269        row: Option<&'a Json>,
270        scope: &'a [&'a Json],
271        position: Option<usize>,
272    ) -> Scope<'a, Json> {
273        Scope::new(&self.env).maybe_row(row).rows(scope).position(position)
274    }
275
276    /// The report's rows: located, filtered and sorted (group keys first).
277    fn rows(&self, data: &Json) -> Vec<Json> {
278        let base = match &self.report.data_member {
279            Some(path) if !path.trim().is_empty() => {
280                let segs: Vec<String> = path.split('.').map(|s| s.trim().to_owned()).collect();
281                match lookup(Some(data), &segs) {
282                    Some(v) => v,
283                    None => {
284                        self.errors
285                            .borrow_mut()
286                            .push(format!("data member '{path}' was not found in the data"));
287                        return Vec::new();
288                    }
289                }
290            }
291            _ => data,
292        };
293        let rows: Vec<&Json> = match base {
294            Json::Array(items) => items.iter().collect(),
295            Json::Null => Vec::new(),
296            other => vec![other],
297        };
298        let mut keys: Vec<SortField> = self
299            .report
300            .groups
301            .iter()
302            .map(|g| SortField { expression: g.expression.clone(), descending: g.descending })
303            .collect();
304        keys.extend(self.report.sort.iter().cloned());
305        filter_and_sort(self, &rows, self.report.filter.as_deref(), &keys, None)
306    }
307
308    /// Lays out every band instance in print order (page bands excluded).
309    fn blocks(&self, engine: &mut ReportEngine, rows: &[&Json]) -> Vec<Block> {
310        let report = self.report;
311        let width = report.page.content_width();
312        let mut out = Vec::new();
313        let first = rows.first().copied();
314        let last = rows.last().copied();
315        if let Some(band) = &report.report_header
316            && let Some(mut b) = self.band(engine, band, &self.ctx(first, rows, None), width, None)
317        {
318            b.report_header = true;
319            out.push(b);
320        }
321        // Group boundaries: the key of each level for each row.
322        let levels = report.groups.len();
323        let keys: Vec<Vec<Value>> = rows
324            .iter()
325            .map(|r| {
326                report
327                    .groups
328                    .iter()
329                    .map(|g| {
330                        self.eval(&self.ctx(Some(r), rows, None), &g.expression, "group").unwrap_or_default()
331                    })
332                    .collect()
333            })
334            .collect();
335        // For each level, the row range of the group containing each row.
336        let mut starts = vec![0usize; levels];
337        let group_end = |level: usize, start: usize| -> usize {
338            let mut end = start + 1;
339            while end < rows.len()
340                && (0..=level).all(|l| {
341                    keys[end][l].loose_eq(&keys[start][l])
342                        || (keys[end][l].is_null() && keys[start][l].is_null())
343                })
344            {
345                end += 1;
346            }
347            end
348        };
349        let mut ends = vec![0usize; levels];
350        let mut first_group = vec![true; levels];
351        for (i, row) in rows.iter().enumerate() {
352            // Open groups that start at this row, outermost first.
353            for level in 0..levels {
354                let opens = i == 0 || i >= ends[level];
355                if !opens {
356                    continue;
357                }
358                starts[level] = i;
359                ends[level] = group_end(level, i);
360                let g = &report.groups[level];
361                if let Some(header) = &g.header {
362                    let scope = &rows[i..ends[level]];
363                    let ctx = self.ctx(Some(row), scope, Some(0));
364                    if let Some(mut b) = self.band(engine, header, &ctx, width, None) {
365                        b.header_level = Some(level);
366                        b.break_before |= g.page_break_before && !first_group[level];
367                        b.keep_with_next = true;
368                        out.push(b);
369                    }
370                } else if g.page_break_before && !first_group[level] {
371                    out.push(Block {
372                        height: 0.0,
373                        items: Vec::new(),
374                        keep_with_next: false,
375                        break_before: true,
376                        break_after: false,
377                        row: None,
378                        header_level: None,
379                        report_header: false,
380                    });
381                }
382                first_group[level] = false;
383            }
384            // The detail band, its aggregates scoped to the innermost group.
385            let (scope, position) = match levels {
386                0 => (rows, i),
387                n => (&rows[starts[n - 1]..ends[n - 1]], i - starts[n - 1]),
388            };
389            let ctx = self.ctx(Some(row), scope, Some(position));
390            if let Some(mut b) = self.band(engine, &report.detail, &ctx, width, Some(i % 2 == 0)) {
391                b.row = Some(i);
392                out.push(b);
393            }
394            for dr in &report.detail_reports {
395                self.detail_report(engine, dr, row, width, &mut out);
396            }
397            // Close groups that end after this row, innermost first.
398            for level in (0..levels).rev() {
399                if i + 1 != ends[level] {
400                    continue;
401                }
402                if let Some(footer) = &report.groups[level].footer {
403                    let scope = &rows[starts[level]..ends[level]];
404                    let ctx = self.ctx(Some(row), scope, None);
405                    if let Some(b) = self.band(engine, footer, &ctx, width, None) {
406                        out.push(b);
407                    }
408                }
409            }
410        }
411        if let Some(band) = &report.report_footer
412            && let Some(b) = self.band(engine, band, &self.ctx(last, rows, None), width, None)
413        {
414            out.push(b);
415        }
416        out
417    }
418
419    fn detail_report(
420        &self,
421        engine: &mut ReportEngine,
422        dr: &crate::model::DetailReport,
423        master: &Json,
424        width: f32,
425        out: &mut Vec<Block>,
426    ) {
427        let segs: Vec<String> = dr.data_member.split('.').map(|s| s.trim().to_owned()).collect();
428        let items: Vec<&Json> = match lookup(Some(master), &segs) {
429            Some(Json::Array(a)) => a.iter().collect(),
430            _ => Vec::new(),
431        };
432        let owned = filter_and_sort(self, &items, dr.filter.as_deref(), &dr.sort, Some(master));
433        let rows: Vec<&Json> = owned.iter().collect();
434        if rows.is_empty() {
435            return;
436        }
437        if let Some(h) = &dr.header {
438            let ctx = self.ctx(Some(master), &rows, None).parent(Some(master));
439            if let Some(mut b) = self.band(engine, h, &ctx, width, None) {
440                b.keep_with_next = true;
441                out.push(b);
442            }
443        }
444        for (j, r) in rows.iter().enumerate() {
445            let ctx = self.ctx(Some(r), &rows, Some(j)).parent(Some(master));
446            if let Some(b) = self.band(engine, &dr.detail, &ctx, width, Some(j % 2 == 0)) {
447                out.push(b);
448            }
449        }
450        if let Some(f) = &dr.footer {
451            let ctx = self.ctx(Some(master), &rows, None).parent(Some(master));
452            if let Some(b) = self.band(engine, f, &ctx, width, None) {
453                out.push(b);
454            }
455        }
456    }
457
458    /// Lays out one instance of `band`; `None` when it is hidden.
459    fn band(
460        &self,
461        engine: &mut ReportEngine,
462        band: &Band,
463        ctx: &Scope<'_, Json>,
464        width: f32,
465        odd: Option<bool>,
466    ) -> Option<Block> {
467        if let Some(v) = &band.visible
468            && !self.eval(ctx, v, "band visibility").map(|v| v.is_true()).unwrap_or(true)
469        {
470            return None;
471        }
472        let zebra = match odd {
473            Some(true) => band.odd_style.as_deref(),
474            Some(false) => band.even_style.as_deref(),
475            None => None,
476        };
477        let mut items = Vec::new();
478        if let Some(name) = zebra
479            && let Some(bg) =
480                self.report.styles.get(name).and_then(|s| s.background.as_deref()).and_then(parse_color)
481        {
482            // Row striping fills the whole band; it is placed first so content draws on top.
483            items.push(Item::Rect {
484                rect: Rect::new(0.0, 0.0, width, band.height),
485                fill: Some(bg),
486                stroke: None,
487                radius: 0.0,
488            });
489        }
490        let height = self.controls(engine, &band.controls, ctx, Point::ZERO, zebra, &mut items);
491        let height = height.max(band.height);
492        // The stripe grows with the band.
493        if zebra.is_some()
494            && let Some(Item::Rect { rect, .. }) = items.first_mut()
495        {
496            *rect = Rect::new(0.0, 0.0, width, height);
497        }
498        Some(Block {
499            height,
500            items,
501            keep_with_next: band.keep_with_next,
502            break_before: band.page_break_before,
503            break_after: band.page_break_after,
504            row: None,
505            header_level: None,
506            report_header: false,
507        })
508    }
509
510    /// Lays out controls at `origin`, moving controls below a grown one down by
511    /// as much as it grew. Returns the bottom of the content.
512    fn controls(
513        &self,
514        engine: &mut ReportEngine,
515        controls: &[Control],
516        ctx: &Scope<'_, Json>,
517        origin: Point,
518        zebra: Option<&str>,
519        items: &mut Vec<Item>,
520    ) -> f32 {
521        let mut order: Vec<&Control> = controls.iter().collect();
522        order.sort_by(|a, b| a.y.total_cmp(&b.y).then(a.x.total_cmp(&b.x)));
523        // (original bottom, how much it grew)
524        let mut grown: Vec<(f32, f32)> = Vec::new();
525        let mut bottom: f32 = 0.0;
526        for c in order {
527            if let Some(v) = &c.visible
528                && !self.eval(ctx, v, &control_name(c)).map(|v| v.is_true()).unwrap_or(true)
529            {
530                continue;
531            }
532            let shift = grown.iter().filter(|(b, _)| *b <= c.y + 0.5).map(|(_, d)| *d).fold(0.0, f32::max);
533            let at = Point::new(origin.x + c.x, origin.y + c.y + shift);
534            let h = self.control(engine, c, ctx, at, zebra, items);
535            if h > c.height + 0.01 {
536                grown.push((c.y + c.height, h - c.height + shift));
537            }
538            bottom = bottom.max(c.y + shift + h);
539        }
540        bottom
541    }
542
543    /// Resolves a style from layers (each a named style and own settings,
544    /// later layers winning), then conditional rules on top.
545    fn look(
546        &self,
547        layers: &[(Option<&str>, &Style)],
548        rules: &[crate::model::FormattingRule],
549        ctx: &Scope<'_, Json>,
550        zebra: Option<&str>,
551    ) -> Look {
552        let mut s = Style::default();
553        if let Some(z) = zebra.and_then(|z| self.report.styles.get(z)) {
554            // Striping contributes text settings, not a second fill.
555            s = s.merged(&Style { background: None, ..z.clone() });
556        }
557        for (name, own) in layers {
558            if let Some(named) = name.and_then(|n| self.report.styles.get(n)) {
559                s = s.merged(named);
560            }
561            s = s.merged(own);
562        }
563        for r in rules {
564            if self.eval(ctx, &r.condition, "formatting rule").map(|v| v.is_true()).unwrap_or(false) {
565                s = s.merged(&r.style);
566            }
567        }
568        let border_w = s.border_width.unwrap_or(0.0);
569        let border = (border_w > 0.0).then(|| {
570            let sides = s.borders.as_deref().unwrap_or("all").to_lowercase();
571            let all = sides.contains("all");
572            let has = |side: &str| all || sides.contains(side);
573            let color =
574                s.border_color.as_deref().and_then(parse_color).unwrap_or(Color::rgba(0.0, 0.0, 0.0, 1.0));
575            (border_w, color, [has("top"), has("right"), has("bottom"), has("left")])
576        });
577        Look {
578            size: s.font_size.unwrap_or(DEFAULT_SIZE).clamp(1.0, 400.0),
579            bold: s.bold.unwrap_or(false),
580            color: s.color.as_deref().and_then(parse_color).unwrap_or(Color::rgba(0.0, 0.0, 0.0, 1.0)),
581            background: s.background.as_deref().and_then(parse_color),
582            border,
583            padding: s.padding.unwrap_or(2.0).max(0.0),
584            align: s.text_align.unwrap_or_default(),
585            valign: s.vertical_align.unwrap_or_default(),
586            wrap: s.word_wrap.unwrap_or(true),
587        }
588    }
589
590    /// Lays out a control at `at`; returns its final height.
591    fn control(
592        &self,
593        engine: &mut ReportEngine,
594        c: &Control,
595        ctx: &Scope<'_, Json>,
596        at: Point,
597        zebra: Option<&str>,
598        items: &mut Vec<Item>,
599    ) -> f32 {
600        let look = self.look(&[(c.style_name.as_deref(), &c.style)], &c.rules, ctx, zebra);
601        match &c.kind {
602            ControlKind::Label(label) => {
603                let (text, value) = self.label_text(ctx, label, &control_name(c));
604                let height = self.text_box(
605                    engine,
606                    &text,
607                    &look,
608                    Rect::new(at.x, at.y, c.width, c.height),
609                    label.grows(),
610                    items,
611                );
612                keep_value(items, &text, value, label);
613                height
614            }
615            ControlKind::CheckBox(cb) => {
616                let rect = Rect::new(at.x, at.y, c.width, c.height);
617                backdrop(&look, rect, items);
618                let checked =
619                    self.eval(ctx, &cb.expression, &control_name(c)).map(|v| v.is_true()).unwrap_or(false);
620                let side = (look.size * 1.1).min(c.height);
621                let bx = Rect::new(at.x + look.padding, at.y + (c.height - side) / 2.0, side, side);
622                items.push(Item::Rect {
623                    rect: bx,
624                    fill: None,
625                    stroke: Some((0.75, look.color)),
626                    radius: 1.5,
627                });
628                if checked {
629                    let p =
630                        |fx: f32, fy: f32| Point::new(bx.x() + bx.width() * fx, bx.y() + bx.height() * fy);
631                    items.push(Item::Line {
632                        from: p(0.22, 0.52),
633                        to: p(0.42, 0.72),
634                        width: 1.2,
635                        color: look.color,
636                    });
637                    items.push(Item::Line {
638                        from: p(0.42, 0.72),
639                        to: p(0.8, 0.28),
640                        width: 1.2,
641                        color: look.color,
642                    });
643                }
644                if !cb.text.is_empty() {
645                    let tx = bx.max_x() + look.padding * 2.0;
646                    let rest = Rect::new(tx, at.y, (rect.max_x() - tx).max(0.0), c.height);
647                    let mut plain = Vec::new();
648                    self.text_box(
649                        engine,
650                        &cb.text,
651                        &Look { background: None, border: None, ..look },
652                        rest,
653                        false,
654                        &mut plain,
655                    );
656                    items.extend(plain);
657                }
658                c.height
659            }
660            ControlKind::Line(line) => {
661                let w = line.thickness.unwrap_or(1.0);
662                let r = Rect::new(at.x, at.y, c.width, c.height);
663                let (from, to) = match line.direction {
664                    LineDirection::Vertical => {
665                        (Point::new(r.center().x, r.y()), Point::new(r.center().x, r.max_y()))
666                    }
667                    LineDirection::Slant => (r.origin, Point::new(r.max_x(), r.max_y())),
668                    _ => (Point::new(r.x(), r.center().y), Point::new(r.max_x(), r.center().y)),
669                };
670                items.push(Item::Line { from, to, width: w, color: look.color });
671                c.height
672            }
673            ControlKind::Shape(shape) => {
674                let rect = Rect::new(at.x, at.y, c.width, c.height);
675                let stroke = look.border.map(|(w, color, _)| (w, color));
676                items.push(match shape.shape {
677                    ShapeKind::Ellipse => Item::Ellipse { rect, fill: look.background, stroke },
678                    _ => Item::Rect { rect, fill: look.background, stroke, radius: shape.radius },
679                });
680                c.height
681            }
682            ControlKind::Table(table) => {
683                let mut y = at.y;
684                for row in &table.rows {
685                    // Measure every cell, then draw them at the row's final height.
686                    let mut cells: Vec<(&TableCell, Look, String, Option<Value>)> = Vec::new();
687                    let mut row_h = row.height;
688                    for cell in &row.cells {
689                        // Cells inherit the table's style; the table's rules apply on top.
690                        let look = self.look(
691                            &[(c.style_name.as_deref(), &c.style), (cell.style_name.as_deref(), &cell.style)],
692                            &c.rules,
693                            ctx,
694                            zebra,
695                        );
696                        let (text, value) = self.label_text(ctx, &cell.label, &control_name(c));
697                        let needed = self.text_height(engine, &text, &look, cell.width);
698                        if cell.label.grows() {
699                            row_h = row_h.max(needed);
700                        }
701                        cells.push((cell, look, text, value));
702                    }
703                    let mut x = at.x;
704                    for (cell, look, text, value) in cells {
705                        self.text_box(engine, &text, &look, Rect::new(x, y, cell.width, row_h), false, items);
706                        keep_value(items, &text, value, &cell.label);
707                        x += cell.width;
708                    }
709                    y += row_h;
710                }
711                (y - at.y).max(c.height)
712            }
713            ControlKind::Picture(picture) => {
714                let rect = Rect::new(at.x, at.y, c.width, c.height);
715                backdrop(&look, rect, items);
716                let source = match picture.expression.as_deref().filter(|s| !s.trim().is_empty()) {
717                    Some(expression) => match self.eval(ctx, expression, &control_name(c)) {
718                        Ok(value) => value.to_text(),
719                        Err(_) => String::new(),
720                    },
721                    None => picture.source.clone(),
722                };
723                if !source.trim().is_empty() {
724                    match engine.image(&source) {
725                        Some(image) => {
726                            let drawn = picture_rect(picture.mode, rect, &image);
727                            items.push(Item::Image { rect: drawn, clip: rect, image });
728                        }
729                        None => self
730                            .errors
731                            .borrow_mut()
732                            .push(format!("{}: not a PNG or JPEG image, or too large", control_name(c))),
733                    }
734                }
735                c.height
736            }
737            ControlKind::Barcode(barcode) => {
738                let rect = Rect::new(at.x, at.y, c.width, c.height);
739                backdrop(&look, rect, items);
740                let content = match barcode.expression.as_deref().filter(|s| !s.trim().is_empty()) {
741                    Some(expression) => self
742                        .eval(ctx, expression, &control_name(c))
743                        .map(|value| format_value(&value, "", self.env.formats()))
744                        .unwrap_or_default(),
745                    None => barcode.text.clone(),
746                };
747                match crate::barcode::encode(barcode.symbology, content.trim()) {
748                    Ok(pattern) => {
749                        let printed = match &pattern {
750                            crate::barcode::Pattern::Linear { text, .. } if barcode.show_text => {
751                                Some(text.clone())
752                            }
753                            _ => None,
754                        };
755                        let text_height = printed.as_ref().map_or(0.0, |_| look.size * 1.4);
756                        let bars = Rect::new(
757                            rect.x(),
758                            rect.y(),
759                            rect.width(),
760                            (rect.height() - text_height).max(1.0),
761                        );
762                        items.push(Item::Vector {
763                            rect: bars,
764                            shapes: vec![crate::barcode::shape(&pattern, bars, look.color)],
765                        });
766                        if let Some(printed) = printed {
767                            let caption =
768                                Look { align: TextAlign::Center, background: None, border: None, ..look };
769                            self.text_box(
770                                engine,
771                                &printed,
772                                &caption,
773                                Rect::new(rect.x(), bars.max_y(), rect.width(), text_height),
774                                false,
775                                items,
776                            );
777                        }
778                    }
779                    // A row without a code is not a mistake; a code that does not fit its format is.
780                    Err(_) if content.trim().is_empty() => {}
781                    Err(reason) => self.errors.borrow_mut().push(format!("{}: {reason}", control_name(c))),
782                }
783                c.height
784            }
785            ControlKind::Chart(chart) => {
786                let rect = Rect::new(at.x, at.y, c.width, c.height);
787                backdrop(&look, rect, items);
788                let data = self.chart_data(ctx, chart, &control_name(c));
789                let format = chart.format.as_deref().map(str::trim).filter(|f| !f.is_empty()).unwrap_or("N0");
790                let number = |value: f64| format_value(&Value::Number(value), format, self.env.formats());
791                let drawing = crate::chart::draw(
792                    &data,
793                    rect,
794                    &crate::chart::ChartOptions {
795                        kind: chart.kind,
796                        title: &chart.title,
797                        legend: !chart.hide_legend,
798                        size: look.size,
799                        ink: look.color,
800                        muted: CHART_MUTED,
801                        grid: CHART_GRID,
802                        format: &number,
803                    },
804                );
805                items.push(Item::Vector { rect, shapes: drawing.shapes });
806                for label in drawing.labels {
807                    let label_look = Look {
808                        size: label.size,
809                        bold: label.bold,
810                        color: label.color,
811                        align: match label.align {
812                            LabelAlign::Center => TextAlign::Center,
813                            LabelAlign::Right => TextAlign::Right,
814                            _ => TextAlign::Left,
815                        },
816                        valign: VerticalAlign::Middle,
817                        padding: 0.0,
818                        wrap: false,
819                        background: None,
820                        border: None,
821                    };
822                    self.text_box(engine, &label.text, &label_look, label.rect, false, items);
823                }
824                c.height
825            }
826            ControlKind::Link(link) => {
827                let (text, _) = self.label_text(ctx, &link.label, &control_name(c));
828                let rect = Rect::new(at.x, at.y, c.width, c.height);
829                let height = self.text_box(engine, &text, &look, rect, link.label.grows(), items);
830                let url = match link.url_expression.as_deref().filter(|s| !s.trim().is_empty()) {
831                    Some(expression) => match self.eval(ctx, expression, &control_name(c)) {
832                        Ok(value) => value.to_text(),
833                        Err(_) => String::new(),
834                    },
835                    None => link.url.clone(),
836                };
837                let url = url.trim();
838                if safe_url(url) {
839                    items.push(Item::Link {
840                        rect: Rect::new(at.x, at.y, c.width, height),
841                        url: url.to_owned(),
842                    });
843                } else if !url.is_empty() {
844                    self.errors
845                        .borrow_mut()
846                        .push(format!("{}: links must use https:, http: or mailto:", control_name(c)));
847                }
848                height
849            }
850            ControlKind::Bookmark(bookmark) => {
851                let title = match bookmark.expression.as_deref().filter(|s| !s.trim().is_empty()) {
852                    Some(expression) => match self.eval(ctx, expression, &control_name(c)) {
853                        Ok(value) => format_value(&value, "", self.env.formats()),
854                        Err(_) => String::new(),
855                    },
856                    None => bookmark.text.clone(),
857                };
858                if !title.trim().is_empty() {
859                    items.push(Item::Bookmark { title, level: bookmark.level, y: at.y });
860                }
861                0.0
862            }
863            ControlKind::Panel(panel) => {
864                let mut content = Vec::new();
865                let inner = self.controls(engine, &panel.controls, ctx, at, zebra, &mut content);
866                let h = inner.max(c.height);
867                backdrop(&look, Rect::new(at.x, at.y, c.width, h), items);
868                items.extend(content);
869                h
870            }
871        }
872    }
873
874    /// A label's text and the value behind it, when it has one.
875    /// Evaluates a chart's points from the rows in scope or a nested list.
876    fn chart_data(
877        &self,
878        ctx: &Scope<'_, Json>,
879        chart: &crate::model::Chart,
880        what: &str,
881    ) -> crate::chart::ChartData {
882        let nested = chart
883            .data_member
884            .as_deref()
885            .filter(|path| !path.trim().is_empty())
886            .and_then(|path| {
887                let segments: Vec<String> = path.split('.').map(|s| s.trim().to_owned()).collect();
888                lookup(ctx.current_row(), &segments)
889            })
890            .and_then(Json::as_array);
891        let rows: Vec<&Json> = match nested {
892            Some(items) => items.iter().collect(),
893            None if chart.data_member.as_deref().is_some_and(|p| !p.trim().is_empty()) => Vec::new(),
894            None => ctx.current_rows().to_vec(),
895        };
896        let argument = chart.argument.as_deref().filter(|s| !s.trim().is_empty());
897        let mut categories: Vec<String> = Vec::new();
898        let mut positions: Vec<f64> = Vec::new();
899        let mut index: HashMap<String, usize> = HashMap::new();
900        let mut totals: Vec<Vec<f64>> = vec![Vec::new(); chart.series.len()];
901        for (row_index, row) in rows.iter().enumerate() {
902            let scope = Scope::new(&self.env).row(*row).rows(&rows).position(Some(row_index));
903            let (name, position) = match argument {
904                Some(expression) => match self.eval(&scope, expression, what) {
905                    Ok(value) => (
906                        format_value(&value, "", self.env.formats()),
907                        value.as_number().unwrap_or(row_index as f64),
908                    ),
909                    Err(_) => continue,
910                },
911                None => ((row_index + 1).to_string(), row_index as f64),
912            };
913            // A scatter chart has one dot per row, whatever its argument.
914            let scatter = chart.kind == crate::chart::ChartKind::Scatter || argument.is_none();
915            let slot = if scatter {
916                categories.push(name);
917                positions.push(position);
918                categories.len() - 1
919            } else {
920                *index.entry(name.clone()).or_insert_with(|| {
921                    categories.push(name);
922                    positions.push(position);
923                    categories.len() - 1
924                })
925            };
926            for (series, total) in chart.series.iter().zip(&mut totals) {
927                total.resize(categories.len(), 0.0);
928                if let Ok(value) = self.eval(&scope, &series.value, what)
929                    && let Some(number) = value.as_number()
930                {
931                    total[slot] += number;
932                }
933            }
934        }
935        if chart.sorted {
936            let numeric = positions
937                .iter()
938                .zip(&categories)
939                .all(|(_, name)| name.trim().replace(self.env.formats().decimal, ".").parse::<f64>().is_ok());
940            let mut order: Vec<usize> = (0..categories.len()).collect();
941            if numeric {
942                order.sort_by(|a, b| positions[*a].total_cmp(&positions[*b]));
943            } else {
944                order.sort_by(|a, b| categories[*a].cmp(&categories[*b]));
945            }
946            categories = order.iter().map(|i| categories[*i].clone()).collect();
947            positions = order.iter().map(|i| positions[*i]).collect();
948            for total in &mut totals {
949                *total = order.iter().map(|i| total.get(*i).copied().unwrap_or(0.0)).collect();
950            }
951        }
952        let series = chart
953            .series
954            .iter()
955            .zip(totals)
956            .enumerate()
957            .map(|(i, (series, mut values))| {
958                values.resize(categories.len(), 0.0);
959                crate::chart::SeriesData {
960                    name: series.name.clone(),
961                    color: series
962                        .color
963                        .as_deref()
964                        .and_then(parse_color)
965                        .unwrap_or(crate::chart::PALETTE[i % crate::chart::PALETTE.len()]),
966                    values,
967                }
968            })
969            .collect();
970        crate::chart::ChartData { categories, positions, series }
971    }
972
973    fn label_text(
974        &self,
975        ctx: &Scope<'_, Json>,
976        label: &crate::model::Label,
977        what: &str,
978    ) -> (String, Option<Value>) {
979        let Some(src) = label.expression.as_deref().filter(|s| !s.trim().is_empty()) else {
980            return (label.text.clone(), None);
981        };
982        match self.eval(ctx, src, what) {
983            Ok(v) => {
984                let text = match label.format.as_deref().map(str::trim).filter(|f| !f.is_empty()) {
985                    Some(f) if f.contains('{') => {
986                        format_composite(f, std::slice::from_ref(&v), self.env.formats())
987                    }
988                    Some(f) => format_value(&v, f, self.env.formats()),
989                    None => format_value(&v, "", self.env.formats()),
990                };
991                (text, Some(v))
992            }
993            Err(_) => ("#ERRO".to_owned(), None),
994        }
995    }
996
997    fn text_height(&self, engine: &mut ReportEngine, text: &str, look: &Look, width: f32) -> f32 {
998        let inner = (width - look.padding * 2.0).max(1.0);
999        let m = engine.measure(text, look.size, look.bold, look.wrap.then_some(inner));
1000        m.height + look.padding * 2.0
1001    }
1002
1003    /// Draws a text box (fill, borders, text); returns its height, grown to
1004    /// fit the text when `grows`.
1005    fn text_box(
1006        &self,
1007        engine: &mut ReportEngine,
1008        text: &str,
1009        look: &Look,
1010        rect: Rect,
1011        grows: bool,
1012        items: &mut Vec<Item>,
1013    ) -> f32 {
1014        let inner_w = (rect.width() - look.padding * 2.0).max(1.0);
1015        let m = engine.measure(text, look.size, look.bold, look.wrap.then_some(inner_w));
1016        let needed = m.height + look.padding * 2.0;
1017        let height = if grows { rect.height().max(needed) } else { rect.height() };
1018        let rect = Rect::new(rect.x(), rect.y(), rect.width(), height);
1019        backdrop(look, rect, items);
1020        if m.lines.is_empty() || text.is_empty() {
1021            return height;
1022        }
1023        let inner = Rect::new(
1024            rect.x() + look.padding,
1025            rect.y() + look.padding,
1026            inner_w,
1027            (height - look.padding * 2.0).max(0.0),
1028        );
1029        let dy = match look.valign {
1030            VerticalAlign::Top => 0.0,
1031            VerticalAlign::Bottom => inner.height() - m.height,
1032            _ => (inner.height() - m.height) / 2.0,
1033        }
1034        .max(0.0);
1035        let lines = m
1036            .lines
1037            .iter()
1038            .map(|(range, width, top, baseline)| {
1039                let dx = match look.align {
1040                    TextAlign::Center => (inner.width() - width) / 2.0,
1041                    TextAlign::Right => inner.width() - width,
1042                    _ => 0.0,
1043                };
1044                let s = text.get(range.clone()).unwrap_or_default().trim_end_matches(['\n', '\r']).to_owned();
1045                PlacedLine {
1046                    text: s,
1047                    x: inner.x() + dx.max(0.0),
1048                    top: inner.y() + dy + top,
1049                    baseline: inner.y() + dy + baseline,
1050                    width: *width,
1051                }
1052            })
1053            .collect();
1054        items.push(Item::Text(TextBlock {
1055            clip: rect,
1056            lines,
1057            size: look.size,
1058            line_height: (look.size * 1.25).ceil(),
1059            bold: look.bold,
1060            color: look.color,
1061            align: look.align,
1062            value: None,
1063            format: None,
1064        }));
1065        height
1066    }
1067
1068    /// Places blocks on pages, then adds page headers and footers.
1069    fn paginate(&self, engine: &mut ReportEngine, blocks: Vec<Block>, rows: &[&Json]) -> Vec<Page> {
1070        let report = self.report;
1071        let page = report.page;
1072        let header_h = report.page_header.as_ref().map_or(0.0, |b| b.height);
1073        let footer_h = report.page_footer.as_ref().map_or(0.0, |b| b.height);
1074        let top = page.margin_top + header_h;
1075        let bottom = (page.height - page.margin_bottom - footer_h).max(top + 1.0);
1076        let x0 = page.margin_left;
1077        let mut pages: Vec<(Vec<Item>, Vec<usize>)> = vec![(Vec::new(), Vec::new())];
1078        let mut y = top;
1079        // The report header sits above the page header on the first page.
1080        let mut blocks = blocks;
1081        let mut report_header_h = 0.0;
1082        if blocks.first().is_some_and(|b| b.report_header) {
1083            let b = blocks.remove(0);
1084            report_header_h = b.height;
1085            pages[0].0.extend(b.items.iter().map(|it| translate(it, Point::new(x0, page.margin_top))));
1086            y += b.height;
1087        }
1088        // The latest header of each group level that repeats.
1089        let mut repeat: Vec<Option<(f32, Vec<Item>)>> = vec![None; report.groups.len()];
1090        let new_page =
1091            |pages: &mut Vec<(Vec<Item>, Vec<usize>)>, y: &mut f32, repeat: &[Option<(f32, Vec<Item>)>]| {
1092                pages.push((Vec::new(), Vec::new()));
1093                *y = top;
1094                for (h, items) in repeat.iter().flatten() {
1095                    let last = pages.len() - 1;
1096                    pages[last].0.extend(items.iter().map(|i| translate(i, Point::new(x0, *y))));
1097                    *y += h;
1098                }
1099            };
1100        let mut i = 0;
1101        while i < blocks.len() {
1102            // A run of keep-with-next blocks travels with the block after it.
1103            let mut j = i;
1104            while j + 1 < blocks.len() && blocks[j].keep_with_next {
1105                j += 1;
1106            }
1107            let run = &blocks[i..=j];
1108            let run_h: f32 = run.iter().map(|b| b.height).sum();
1109            let at_top = y <= top + repeat.iter().flatten().map(|(h, _)| *h).sum::<f32>() + 0.01;
1110            if run[0].break_before && !at_top {
1111                // A new group on a new page: its own header replaces the repeated one.
1112                if let Some(level) = run[0].header_level {
1113                    for r in repeat.iter_mut().skip(level) {
1114                        *r = None;
1115                    }
1116                }
1117                new_page(&mut pages, &mut y, &repeat);
1118            } else if y + run_h > bottom + 0.01 && !at_top {
1119                if let Some(level) = run[0].header_level {
1120                    for r in repeat.iter_mut().skip(level) {
1121                        *r = None;
1122                    }
1123                }
1124                new_page(&mut pages, &mut y, &repeat);
1125            }
1126            for b in run {
1127                // A single block taller than the space left starts a new page.
1128                let at_top = y <= top + 0.01;
1129                if y + b.height > bottom + 0.01 && !at_top {
1130                    new_page(&mut pages, &mut y, &repeat);
1131                }
1132                if let Some(level) = b.header_level {
1133                    let repeats = report.groups.get(level).is_some_and(|g| g.repeat_header);
1134                    for r in repeat.iter_mut().skip(level) {
1135                        *r = None;
1136                    }
1137                    if repeats {
1138                        repeat[level] = Some((b.height, b.items.clone()));
1139                    }
1140                }
1141                let last = pages.len() - 1;
1142                pages[last].0.extend(b.items.iter().map(|it| translate(it, Point::new(x0, y))));
1143                if let Some(r) = b.row {
1144                    pages[last].1.push(r);
1145                }
1146                y += b.height;
1147                if b.break_after {
1148                    new_page(&mut pages, &mut y, &repeat);
1149                }
1150            }
1151            i = j + 1;
1152        }
1153        // Drop a trailing empty page left by a final page break.
1154        if pages.len() > 1 && pages.last().is_some_and(|p| p.0.is_empty()) {
1155            pages.pop();
1156        }
1157        let count = pages.len();
1158        let width = page.content_width();
1159        pages
1160            .into_iter()
1161            .enumerate()
1162            .map(|(n, (mut items, page_rows))| {
1163                let scope: Vec<&Json> = page_rows.iter().map(|&r| rows[r]).collect();
1164                let first = scope.first().copied();
1165                let ctx = self.ctx(first, &scope, None).page(n + 1, count);
1166                let mut chrome = Vec::new();
1167                if let Some(band) = &report.page_header
1168                    && let Some(b) = self.band(engine, band, &ctx, width, None)
1169                {
1170                    let py = page.margin_top + if n == 0 { report_header_h } else { 0.0 };
1171                    chrome.extend(b.items.iter().map(|it| translate(it, Point::new(x0, py))));
1172                }
1173                let header_end = chrome.len();
1174                if let Some(band) = &report.page_footer
1175                    && let Some(b) = self.band(engine, band, &ctx, width, None)
1176                {
1177                    let fy = page.height - page.margin_bottom - band.height;
1178                    chrome.extend(b.items.iter().map(|it| translate(it, Point::new(x0, fy))));
1179                }
1180                let footer_end = chrome.len();
1181                chrome.append(&mut items);
1182                Page {
1183                    width: page.width,
1184                    height: page.height,
1185                    items: chrome,
1186                    page_header: 0..header_end,
1187                    page_footer: header_end..footer_end,
1188                }
1189            })
1190            .collect()
1191    }
1192}
1193
1194/// Where an image goes in its control's box.
1195pub fn picture_rect(mode: PictureMode, rect: Rect, image: &DocumentImage) -> Rect {
1196    let (w, h) = (image.pixels().width() as f32, image.pixels().height() as f32);
1197    let centered = |scale: f32| {
1198        let (dw, dh) = (w * scale, h * scale);
1199        Rect::new(rect.x() + (rect.width() - dw) / 2.0, rect.y() + (rect.height() - dh) / 2.0, dw, dh)
1200    };
1201    let fit = (rect.width() / w).min(rect.height() / h);
1202    match mode {
1203        PictureMode::Stretch => rect,
1204        PictureMode::Normal => Rect::new(rect.x(), rect.y(), w, h),
1205        PictureMode::Squeeze => centered(fit.min(1.0)),
1206        _ => centered(fit),
1207    }
1208}
1209
1210/// Whether a link's address is one a reader can open safely: data can fill
1211/// links in, and a `javascript:` or `file:` address must not reach a document.
1212fn safe_url(url: &str) -> bool {
1213    let lower = url.to_ascii_lowercase();
1214    (lower.starts_with("https://") || lower.starts_with("http://") || lower.starts_with("mailto:"))
1215        && !url.chars().any(char::is_control)
1216}
1217
1218/// Decodes a PNG or JPEG from a `data:` address or base64 text. Limits protect
1219/// a server from a small file that expands into huge pixels.
1220pub fn decode_picture(source: &str) -> Option<DocumentImage> {
1221    let text = source.trim();
1222    let text = match text.strip_prefix("data:") {
1223        Some(rest) => rest.split_once("base64,")?.1,
1224        None => text,
1225    };
1226    let bytes = crate::base64::decode(text)?;
1227    let format = match image::guess_format(&bytes).ok()? {
1228        image::ImageFormat::Png => ImageFormat::Png,
1229        image::ImageFormat::Jpeg => ImageFormat::Jpeg,
1230        _ => return None,
1231    };
1232    let mut reader = image::ImageReader::new(std::io::Cursor::new(&bytes)).with_guessed_format().ok()?;
1233    let mut limits = image::Limits::default();
1234    limits.max_image_width = Some(MAX_IMAGE_SIDE);
1235    limits.max_image_height = Some(MAX_IMAGE_SIDE);
1236    limits.max_alloc = Some(MAX_IMAGE_BYTES);
1237    reader.limits(limits);
1238    let decoded = reader.decode().ok()?;
1239    let channels = decoded.color().channel_count();
1240    let rgba = decoded.to_rgba8();
1241    let (width, height) = rgba.dimensions();
1242    let pixels = ImageData::from_rgba8(width, height, rgba.into_raw())?;
1243    Some(DocumentImage::new(pixels, bytes.into(), format, channels))
1244}
1245
1246/// Records on the text box just drawn the value behind its text. A format with
1247/// literal text around the value (`Total: {0:C2}`) keeps it text.
1248fn keep_value(items: &mut [Item], text: &str, value: Option<Value>, label: &crate::model::Label) {
1249    let format = label.format.as_deref().map(str::trim).filter(|f| !f.is_empty());
1250    let (Some(value), false, false) = (value, text.is_empty(), format.is_some_and(|f| f.contains('{')))
1251    else {
1252        return;
1253    };
1254    if let Some(Item::Text(block)) = items.last_mut() {
1255        block.value = Some(value);
1256        block.format = format.map(str::to_owned);
1257    }
1258}
1259
1260/// Fill and borders of a box.
1261fn backdrop(look: &Look, rect: Rect, items: &mut Vec<Item>) {
1262    if let Some(fill) = look.background {
1263        items.push(Item::Rect { rect, fill: Some(fill), stroke: None, radius: 0.0 });
1264    }
1265    if let Some((w, color, [top, right, bottom, left])) = look.border {
1266        let (x0, y0, x1, y1) = (rect.x(), rect.y(), rect.max_x(), rect.max_y());
1267        let mut line = |a: Point, b: Point| items.push(Item::Line { from: a, to: b, width: w, color });
1268        if top {
1269            line(Point::new(x0, y0), Point::new(x1, y0));
1270        }
1271        if right {
1272            line(Point::new(x1, y0), Point::new(x1, y1));
1273        }
1274        if bottom {
1275            line(Point::new(x0, y1), Point::new(x1, y1));
1276        }
1277        if left {
1278            line(Point::new(x0, y0), Point::new(x0, y1));
1279        }
1280    }
1281}
1282
1283fn control_name(c: &Control) -> String {
1284    c.name.clone().unwrap_or_else(|| match &c.kind {
1285        ControlKind::Label(l) => {
1286            l.expression.clone().map_or_else(|| format!("label '{}'", l.text), |e| format!("field {e}"))
1287        }
1288        _ => "control".to_owned(),
1289    })
1290}
1291
1292/// Filters and sorts rows; returns them as owned JSON (cheap enough at report
1293/// sizes, and it frees the result from the input's lifetime).
1294fn filter_and_sort(
1295    run: &Run<'_>,
1296    rows: &[&Json],
1297    filter: Option<&str>,
1298    keys: &[SortField],
1299    parent: Option<&Json>,
1300) -> Vec<Json> {
1301    let mut kept: Vec<&Json> = match filter.filter(|f| !f.trim().is_empty()) {
1302        Some(f) => rows
1303            .iter()
1304            .copied()
1305            .filter(|r| {
1306                let ctx = run.ctx(Some(r), rows, None).parent(parent);
1307                run.eval(&ctx, f, "filter").map(|v| v.is_true()).unwrap_or(true)
1308            })
1309            .collect(),
1310        None => rows.to_vec(),
1311    };
1312    if !keys.is_empty() {
1313        let mut keyed: Vec<(Vec<Value>, &Json)> = kept
1314            .iter()
1315            .map(|r| {
1316                let ctx = run.ctx(Some(r), rows, None).parent(parent);
1317                (keys.iter().map(|k| run.eval(&ctx, &k.expression, "sort").unwrap_or_default()).collect(), *r)
1318            })
1319            .collect();
1320        keyed.sort_by(|a, b| {
1321            for (i, k) in keys.iter().enumerate() {
1322                let o = a.0[i].compare(&b.0[i]);
1323                let o = if k.descending { o.reverse() } else { o };
1324                if o.is_ne() {
1325                    return o;
1326                }
1327            }
1328            std::cmp::Ordering::Equal
1329        });
1330        kept = keyed.into_iter().map(|(_, r)| r).collect();
1331    }
1332    kept.into_iter().cloned().collect()
1333}
1334
1335fn translate(item: &Item, by: Point) -> Item {
1336    let r = |rect: &Rect| Rect::from_origin_size(Point::new(rect.x() + by.x, rect.y() + by.y), rect.size);
1337    let p = |pt: &Point| Point::new(pt.x + by.x, pt.y + by.y);
1338    match item {
1339        Item::Rect { rect, fill, stroke, radius } => {
1340            Item::Rect { rect: r(rect), fill: *fill, stroke: *stroke, radius: *radius }
1341        }
1342        Item::Ellipse { rect, fill, stroke } => Item::Ellipse { rect: r(rect), fill: *fill, stroke: *stroke },
1343        Item::Line { from, to, width, color } => {
1344            Item::Line { from: p(from), to: p(to), width: *width, color: *color }
1345        }
1346        Item::Image { rect, clip, image } => {
1347            Item::Image { rect: r(rect), clip: r(clip), image: image.clone() }
1348        }
1349        Item::Link { rect, url } => Item::Link { rect: r(rect), url: url.clone() },
1350        Item::Vector { rect, shapes } => {
1351            Item::Vector { rect: r(rect), shapes: shapes.iter().map(|shape| shape.translated(by)).collect() }
1352        }
1353        Item::Bookmark { title, level, y } => {
1354            Item::Bookmark { title: title.clone(), level: *level, y: y + by.y }
1355        }
1356        Item::Text(t) => Item::Text(TextBlock {
1357            clip: r(&t.clip),
1358            lines: t
1359                .lines
1360                .iter()
1361                .map(|l| PlacedLine {
1362                    x: l.x + by.x,
1363                    top: l.top + by.y,
1364                    baseline: l.baseline + by.y,
1365                    ..l.clone()
1366                })
1367                .collect(),
1368            ..t.clone()
1369        }),
1370    }
1371}
1372
1373/// The JSON value at `path` under `base`.
1374pub(crate) fn lookup<'j>(base: Option<&'j Json>, path: &[String]) -> Option<&'j Json> {
1375    let mut cur = base?;
1376    for seg in path {
1377        cur = match cur {
1378            Json::Object(map) => map.get(seg)?,
1379            Json::Array(items) => items.get(seg.parse::<usize>().ok()?)?,
1380            _ => return None,
1381        };
1382    }
1383    Some(cur)
1384}