1use 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#[derive(Clone, Debug, Default)]
26pub struct RenderOptions {
27 pub parameters: HashMap<String, serde_json::Value>,
29 pub now: Option<DateTime>,
31}
32
33pub 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)>, height: f32,
66}
67
68struct Block {
70 height: f32,
71 items: Vec<Item>,
72 keep_with_next: bool,
73 break_before: bool,
74 break_after: bool,
75 row: Option<usize>,
77 header_level: Option<usize>,
79 report_header: bool,
81}
82
83const DEFAULT_SIZE: f32 = 10.0;
84const CHART_MUTED: Color = Color::rgba(0.353, 0.388, 0.459, 1.0);
86const CHART_GRID: Color = Color::rgba(0.855, 0.867, 0.898, 1.0);
88const MAX_IMAGE_SIDE: u32 = 12_000;
90const MAX_IMAGE_BYTES: u64 = 512 * 1024 * 1024;
92const MAX_CACHED_IMAGES: usize = 256;
94
95impl ReportEngine {
96 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 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 pub fn render(&mut self, report: &Report, data: &Json) -> Document {
117 self.render_with(report, data, &RenderOptions::default())
118 }
119
120 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 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 if self.measured.len() > 50_000 {
150 self.measured.clear();
151 }
152 self.measured.insert(key, m.clone());
153 m
154 }
155
156 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 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
186struct 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
194struct 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
1194pub 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
1210fn 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
1218pub 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
1246fn 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
1260fn 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
1292fn 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
1373pub(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}