Skip to main content

ntcip/dms/
render.rs

1// render.rs
2//
3// Copyright (C) 2018-2025  Minnesota Department of Transportation
4//
5//! This module is for NTCIP 1203 DMS rendering.
6use crate::dms::font::{Font, FontTable};
7use crate::dms::graphic::Graphic;
8use crate::dms::multi::{
9    ColorCtx, JustificationLine, JustificationPage, MultiStr, Rectangle,
10    SyntaxError, Tag, Value,
11};
12use crate::dms::sign::Dms;
13use fstr::FStr;
14use log::debug;
15use pix::{Raster, Region, rgb::SRgb8};
16use std::fmt::Write;
17
18/// Result type
19type Result<T> = std::result::Result<T, SyntaxError>;
20
21/// Maximum number of text rectangles per page
22const MAX_TEXT_RECTANGLES: u32 = 50;
23
24/// Rendered DMS page
25pub struct Page {
26    /// Page raster
27    pub raster: Raster<SRgb8>,
28
29    /// Page duration (1/10 s)
30    pub duration_ds: u16,
31}
32
33/// Page state
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35enum PageState {
36    /// Page ON with flag for more pages
37    On(bool),
38
39    /// Page OFF with flag for more pages
40    Off(bool),
41
42    /// All pages done
43    Done,
44}
45
46/// Rendering state
47#[derive(Clone)]
48struct RenderState {
49    /// Color context
50    color_ctx: ColorCtx,
51
52    /// Current page-on time in deciseconds
53    page_on_time_ds: u8,
54
55    /// Current page-off time in deciseconds
56    page_off_time_ds: u8,
57
58    /// Current text rectangle
59    text_rectangle: Rectangle,
60
61    /// Current page justification
62    just_page: JustificationPage,
63
64    /// Current line justification
65    just_line: JustificationLine,
66
67    /// Font number
68    font_num: u8,
69
70    /// Font version_id
71    font_version_id: Option<u16>,
72
73    /// Current specified line spacing
74    line_spacing: Option<u8>,
75
76    /// Current specified char spacing
77    char_spacing: Option<u8>,
78
79    /// Current line number
80    line_number: u8,
81
82    /// Current text span number
83    span_number: u8,
84}
85
86/// Span of text
87#[derive(Clone)]
88enum Span<'a> {
89    /// Slice of text
90    Text(RenderState, &'a str),
91
92    /// Hexadecimal character
93    HexChar(RenderState, FStr<4>),
94}
95
96/// Text line
97#[derive(Clone)]
98struct TextLine {
99    /// Height in pixels
100    height: u16,
101
102    /// Font spacing
103    font_spacing: u16,
104
105    /// Specified line spacing
106    line_spacing: Option<u16>,
107}
108
109/// Page renderer for dynamic message signs
110///
111/// This renders a MULTI string on a DMS, as a [Page] iterator.
112///
113/// These tags are not currently supported:
114///
115/// * `[f…]`: [Field]
116/// * `[fl…]`: [Flash]
117/// * `[ms…]`: [Manufacturer Specific]
118/// * `[mv…]`: [Moving Text]
119///
120/// [field]: multi/enum.Tag.html#variant.F1
121/// [flash]: multi/enum.Tag.html#variant.Fl
122/// [manufacturer specific]: multi/enum.Tag.html#variant.Ms
123/// [moving text]: multi/enum.Tag.html#variant.Mv
124/// [page]: struct.Page.html
125pub struct Pages<'a, const C: usize, const F: usize, const G: usize> {
126    /// Sign to render
127    dms: &'a Dms<C, F, G>,
128
129    /// Default rendering state
130    default_state: RenderState,
131
132    /// Current render state
133    render_state: RenderState,
134
135    /// Page state
136    page_state: PageState,
137
138    /// MULTI string iterator
139    values: MultiStr<'a>,
140
141    /// Spans for current text rectangle
142    spans: Vec<Span<'a>>,
143}
144
145impl PageState {
146    /// Get state for new page
147    fn new_page(page_off: bool) -> Self {
148        if page_off {
149            PageState::Off(true)
150        } else {
151            PageState::On(true)
152        }
153    }
154
155    /// Get state for end of message
156    fn done(page_off: bool) -> Self {
157        if page_off {
158            PageState::Off(false)
159        } else {
160            PageState::Done
161        }
162    }
163
164    /// Get next page state
165    fn next_state(self) -> Self {
166        match self {
167            PageState::On(true) => PageState::Off(true),
168            PageState::On(false) => PageState::Done,
169            PageState::Off(true) => PageState::On(false),
170            PageState::Off(false) => PageState::Done,
171            PageState::Done => PageState::Done,
172        }
173    }
174}
175
176impl RenderState {
177    /// Create a new render state
178    fn new<const C: usize, const F: usize, const G: usize>(
179        dms: &Dms<C, F, G>,
180    ) -> Self {
181        RenderState {
182            color_ctx: dms.color_ctx(),
183            page_on_time_ds: dms.multi_cfg.default_page_on_time,
184            page_off_time_ds: dms.multi_cfg.default_page_off_time,
185            text_rectangle: Rectangle::new(
186                1,
187                1,
188                dms.vms_cfg.sign_width_pixels,
189                dms.vms_cfg.sign_height_pixels,
190            ),
191            just_page: dms.multi_cfg.default_justification_page,
192            just_line: dms.multi_cfg.default_justification_line,
193            font_num: dms.multi_cfg.default_font,
194            font_version_id: None,
195            line_spacing: None,
196            char_spacing: None,
197            line_number: 0,
198            span_number: 0,
199        }
200    }
201
202    /// Get the background RGB color
203    fn background_rgb(&self) -> SRgb8 {
204        let (r, g, b) = self.color_ctx.background_rgb();
205        SRgb8::new(r, g, b)
206    }
207
208    /// Get the foreground RGB color
209    fn foreground_rgb(&self) -> SRgb8 {
210        let (r, g, b) = self.color_ctx.foreground_rgb();
211        SRgb8::new(r, g, b)
212    }
213
214    /// Check if states match for text spans
215    fn matches_span(&self, rhs: &Self) -> bool {
216        self.just_page == rhs.just_page
217            && self.line_number == rhs.line_number
218            && self.just_line == rhs.just_line
219    }
220
221    /// Check if states match for lines
222    fn matches_line(&self, rhs: &Self) -> bool {
223        self.just_page == rhs.just_page
224    }
225
226    /// Lookup current font in cache
227    fn font<'a, const C: usize, const F: usize>(
228        &self,
229        fonts: &'a FontTable<C, F>,
230    ) -> Result<&'a Font<C>> {
231        match (fonts.font(self.font_num), self.font_version_id) {
232            (Some(f), Some(vid)) => {
233                if vid == f.version_id() {
234                    Ok(f)
235                } else {
236                    Err(SyntaxError::FontVersionID)
237                }
238            }
239            (Some(f), None) => Ok(f),
240            (None, _) => Err(SyntaxError::FontNotDefined(self.font_num)),
241        }
242    }
243}
244
245impl Span<'_> {
246    /// Get span as a str slice
247    fn as_str(&self) -> &str {
248        match self {
249            Span::Text(_state, text) => text,
250            Span::HexChar(_state, hc) => hc.slice_to_terminator('\0'),
251        }
252    }
253
254    /// Get the render state
255    fn state(&self) -> &RenderState {
256        match self {
257            Span::Text(state, _text) => state,
258            Span::HexChar(state, _hc) => state,
259        }
260    }
261
262    /// Get the width of a text span
263    fn width<const C: usize, const F: usize>(
264        &self,
265        fonts: &FontTable<C, F>,
266    ) -> Result<u16> {
267        let font = self.state().font(fonts)?;
268        let cs = self.char_spacing_fonts(fonts)?;
269        Ok(font.text_width(self.as_str(), Some(cs))?)
270    }
271
272    /// Get the char spacing
273    fn char_spacing_fonts<const C: usize, const F: usize>(
274        &self,
275        fonts: &FontTable<C, F>,
276    ) -> Result<u16> {
277        let state = self.state();
278        match state.char_spacing {
279            Some(sp) => Ok(sp.into()),
280            None => Ok(state.font(fonts)?.char_spacing.into()),
281        }
282    }
283
284    /// Get the char spacing
285    fn char_spacing_font<const C: usize>(&self, font: &Font<C>) -> u8 {
286        match self.state().char_spacing {
287            Some(sp) => sp,
288            None => font.char_spacing,
289        }
290    }
291
292    /// Get the char spacing from a previous span
293    fn char_spacing_between<const C: usize, const F: usize>(
294        &self,
295        prev: &Span,
296        fonts: &FontTable<C, F>,
297    ) -> Result<u16> {
298        if let Some(c) = self.state().char_spacing {
299            Ok(c.into())
300        } else {
301            // NTCIP 1203 fontCharSpacing:
302            // "... the average character spacing of the two fonts,
303            // rounded up to the nearest whole pixel ..." ???
304            let psc = prev.char_spacing_fonts(fonts)?;
305            let sc = self.char_spacing_fonts(fonts)?;
306            Ok(((psc + sc) >> 1) + ((psc + sc) & 1))
307        }
308    }
309
310    /// Get the height of a text span
311    fn height<const C: usize, const F: usize>(
312        &self,
313        fonts: &FontTable<C, F>,
314    ) -> Result<u16> {
315        Ok(self.state().font(fonts)?.height.into())
316    }
317
318    /// Get the font line spacing
319    fn font_spacing<const C: usize, const F: usize>(
320        &self,
321        fonts: &FontTable<C, F>,
322    ) -> Result<u16> {
323        Ok(self.state().font(fonts)?.line_spacing.into())
324    }
325
326    /// Get the line spacing
327    fn line_spacing(&self) -> Option<u16> {
328        self.state().line_spacing.map(|sp| sp.into())
329    }
330
331    /// Render the text span
332    fn render_text<const C: usize>(
333        &self,
334        raster: &mut Raster<SRgb8>,
335        font: &Font<C>,
336        x: i32,
337        y: i32,
338    ) -> Result<()> {
339        let cs = self.char_spacing_font(font).into();
340        let cf = self.state().foreground_rgb();
341        Ok(font.render_text(raster, self.as_str(), x, y, cs, cf)?)
342    }
343}
344
345impl TextLine {
346    /// Create a new text line.
347    fn new(height: u16, font_spacing: u16, line_spacing: Option<u16>) -> Self {
348        TextLine {
349            height,
350            font_spacing,
351            line_spacing,
352        }
353    }
354
355    /// Combine a text line with another.
356    fn combine(&mut self, rhs: &Self) {
357        self.height = self.height.max(rhs.height);
358        self.font_spacing = self.font_spacing.max(rhs.font_spacing);
359        self.line_spacing = self.line_spacing.or(rhs.line_spacing);
360    }
361
362    /// Get the spacing between two text lines.
363    fn line_spacing(&self, rhs: &Self) -> u16 {
364        if let Some(ls) = self.line_spacing {
365            ls
366        } else {
367            // NTCIP 1203 fontLineSpacing:
368            // "The number of pixels between adjacent lines
369            // is the average of the 2 line spacings of each
370            // line, rounded up to the nearest whole pixel."
371            let ps = rhs.font_spacing;
372            let fs = self.font_spacing;
373            ((ps + fs) >> 1) + ((ps + fs) & 1)
374        }
375    }
376}
377
378impl<'a, const C: usize, const F: usize, const G: usize> Pages<'a, C, F, G> {
379    /// Create a new DMS page renderer.
380    ///
381    /// * `dms` Sign to render.
382    /// * `ms` MULTI string to render.
383    pub fn new(dms: &'a Dms<C, F, G>, ms: &'a str) -> Self {
384        let default_state = RenderState::new(dms);
385        let render_state = default_state.clone();
386        Pages {
387            dms,
388            default_state,
389            render_state,
390            page_state: PageState::On(true),
391            values: MultiStr::new(ms),
392            spans: Vec::new(),
393        }
394    }
395
396    /// Get the font definition
397    fn fonts(&self) -> &FontTable<C, F> {
398        self.dms.font_definition()
399    }
400
401    /// Get the character width (1 for variable width)
402    fn char_width(&self) -> u16 {
403        self.dms.char_width().max(1).into()
404    }
405
406    /// Get the character height (1 for variable height)
407    fn char_height(&self) -> u16 {
408        self.dms.char_height().max(1).into()
409    }
410
411    /// Get the page-on time (deciseconds)
412    fn page_on_time_ds(&self) -> u16 {
413        self.render_state.page_on_time_ds.into()
414    }
415
416    /// Get the page-off time (deciseconds)
417    fn page_off_time_ds(&self) -> u16 {
418        self.render_state.page_off_time_ds.into()
419    }
420
421    /// Render an OFF page
422    fn render_off_page(&mut self) -> Page {
423        self.page_state = self.page_state.next_state();
424        Page {
425            raster: self.build_raster(),
426            duration_ds: self.page_off_time_ds(),
427        }
428    }
429
430    /// Build a raster
431    fn build_raster(&self) -> Raster<SRgb8> {
432        let width = self.render_state.text_rectangle.width.into();
433        let height = self.render_state.text_rectangle.height.into();
434        let clr = self.render_state.background_rgb();
435        Raster::with_color(width, height, clr)
436    }
437
438    /// Render an ON page
439    fn render_on_page(&mut self) -> Result<Page> {
440        self.check_unsupported()?;
441        self.update_page_state()?;
442        let mut raster = self.build_raster();
443        debug!("render_on_page {}x{}", raster.width(), raster.height());
444        let mut n_text_rectangles = 0;
445        self.page_state = PageState::On(false);
446        while self.page_state == PageState::On(false) {
447            self.render_graphics(&mut raster)?;
448            self.render_text_rectangle(&mut raster)?;
449            n_text_rectangles += 1;
450            if n_text_rectangles > MAX_TEXT_RECTANGLES {
451                return Err(SyntaxError::Other("Too many text rectangles"));
452            }
453        }
454        Ok(Page {
455            raster,
456            duration_ds: self.page_on_time_ds(),
457        })
458    }
459
460    /// Check for unsupported MULTI tags in a page
461    fn check_unsupported(&self) -> Result<()> {
462        for value in self.values.clone() {
463            let val = value?;
464            if let Some(tag) = val.tag() {
465                if !self.dms.multi_cfg.supported_multi_tags.contains(tag) {
466                    return Err(SyntaxError::UnsupportedTag(val.into()));
467                }
468                if tag == Tag::Np {
469                    break;
470                }
471            }
472        }
473        Ok(())
474    }
475
476    /// Iterate through page values to update its state
477    fn update_page_state(&mut self) -> Result<()> {
478        let ds = &self.default_state;
479        let rs = &mut self.render_state;
480        // Set these back to default values
481        rs.text_rectangle = ds.text_rectangle;
482        rs.line_spacing = ds.line_spacing;
483        rs.line_number = 0;
484        rs.span_number = 0;
485        for value in self.values.clone() {
486            let val = value?;
487            match val {
488                Value::ColorBackground(clr) | Value::PageBackground(clr) => {
489                    rs.color_ctx.set_background(clr, &val)?;
490                }
491                Value::NewPage() => break,
492                Value::PageTime(on, off) => {
493                    rs.page_on_time_ds = on.unwrap_or(ds.page_on_time_ds);
494                    rs.page_off_time_ds = off.unwrap_or(ds.page_off_time_ds);
495                }
496                _ => (),
497            }
498        }
499        Ok(())
500    }
501
502    /// Render graphics and color rectangles
503    fn render_graphics(&mut self, raster: &mut Raster<SRgb8>) -> Result<()> {
504        let mut rs = self.render_state.clone();
505        for value in self.values.clone() {
506            let val = value?;
507            match val {
508                Value::ColorBackground(clr) => {
509                    rs.color_ctx.set_background(clr, &val)?;
510                }
511                Value::ColorForeground(clr) => {
512                    rs.color_ctx.set_foreground(clr, &val)?;
513                }
514                Value::ColorRectangle(rect, clr) => {
515                    let mut ctx = rs.color_ctx.clone();
516                    // only set foreground color in cloned context
517                    ctx.set_foreground(Some(clr), &val)?;
518                    let (r, g, b) = ctx.foreground_rgb();
519                    let rgb = SRgb8::new(r, g, b);
520                    render_rect(raster, rect, rgb, &val)?;
521                }
522                Value::Field(_, _) => unimplemented!(),
523                Value::Flash(_, _, _) => unimplemented!(),
524                Value::FlashEnd() => unimplemented!(),
525                Value::Graphic(gn, None) => {
526                    let g = self.graphic(gn, None)?;
527                    g.render_graphic(raster, 1, 1, &rs.color_ctx)?;
528                }
529                Value::Graphic(gn, Some((x, y, gid))) => {
530                    let g = self.graphic(gn, gid)?;
531                    let x = x.into();
532                    let y = y.into();
533                    g.render_graphic(raster, x, y, &rs.color_ctx)?;
534                }
535                Value::ManufacturerSpecific(_, _) => unimplemented!(),
536                Value::ManufacturerSpecificEnd(_, _) => unimplemented!(),
537                Value::MovingText(_, _, _, _, _, _) => unimplemented!(),
538                Value::NewPage() | Value::TextRectangle(_) => break,
539                _ => (),
540            }
541        }
542        Ok(())
543    }
544
545    /// Lookup a graphic from the table
546    fn graphic(&self, gn: u8, gid: Option<u16>) -> Result<&'a Graphic> {
547        let graphics = self.dms.graphic_definition();
548        match (graphics.graphic(gn), gid) {
549            (Some(g), None) => Ok(g),
550            (Some(g), Some(gid)) => {
551                if gid == g.version_id() {
552                    Ok(g)
553                } else {
554                    Err(SyntaxError::GraphicID)
555                }
556            }
557            (None, _) => Err(SyntaxError::GraphicNotDefined(gn)),
558        }
559    }
560
561    /// Render one text rectangle
562    fn render_text_rectangle(
563        &mut self,
564        raster: &mut Raster<SRgb8>,
565    ) -> Result<()> {
566        let is_char_matrix = self.dms.char_width() > 0;
567        let is_char_or_line_matrix = self.dms.char_height() > 0;
568        let page_off = self.page_off_time_ds() > 0;
569        let ds = &self.default_state;
570        let mut line_blank = true;
571        self.page_state = PageState::done(page_off);
572        self.spans.clear();
573        for value in self.values.by_ref() {
574            let val = value?;
575            match val {
576                Value::ColorForeground(clr) => {
577                    self.render_state.color_ctx.set_foreground(clr, &val)?;
578                }
579                Value::Font(f) => {
580                    let rs = &mut self.render_state;
581                    rs.font_num = f.map_or(ds.font_num, |t| t.0);
582                    rs.font_version_id = f.map_or(ds.font_version_id, |t| t.1);
583                }
584                #[allow(deprecated)]
585                Value::JustificationLine(Some(JustificationLine::Other)) => {
586                    return Err(SyntaxError::UnsupportedTagValue(val.into()));
587                }
588                Value::JustificationLine(Some(JustificationLine::Full)) => {
589                    return Err(SyntaxError::UnsupportedTagValue(val.into()));
590                }
591                Value::JustificationLine(jl) => {
592                    let rs = &mut self.render_state;
593                    rs.just_line = jl.unwrap_or(ds.just_line);
594                    rs.span_number = 0;
595                }
596                #[allow(deprecated)]
597                Value::JustificationPage(Some(JustificationPage::Other)) => {
598                    return Err(SyntaxError::UnsupportedTagValue(val.into()));
599                }
600                Value::JustificationPage(jp) => {
601                    let rs = &mut self.render_state;
602                    let jp = jp.unwrap_or(ds.just_page);
603                    if jp != rs.just_page {
604                        rs.just_page = jp;
605                        rs.line_number = 0;
606                        rs.span_number = 0;
607                    }
608                }
609                Value::NewLine(ls) => {
610                    if let Some(ls) = ls
611                        && is_char_or_line_matrix
612                        && ls > 0
613                    {
614                        return Err(SyntaxError::UnsupportedTagValue(
615                            val.into(),
616                        ));
617                    }
618                    let rs = &mut self.render_state;
619                    // Insert an empty text span for blank lines.
620                    if line_blank {
621                        self.spans.push(Span::Text(rs.clone(), ""));
622                    }
623                    line_blank = true;
624                    rs.line_spacing = ls;
625                    rs.line_number += 1;
626                    rs.span_number = 0;
627                }
628                Value::NewPage() => {
629                    self.page_state = PageState::new_page(page_off);
630                    break;
631                }
632                Value::SpacingCharacter(sc) => {
633                    if is_char_matrix && sc > 0 {
634                        return Err(SyntaxError::UnsupportedTagValue(
635                            val.into(),
636                        ));
637                    }
638                    self.render_state.char_spacing = Some(sc);
639                }
640                Value::SpacingCharacterEnd() => {
641                    self.render_state.char_spacing = None;
642                }
643                Value::TextRectangle(rect) => {
644                    self.page_state = PageState::On(false);
645                    match self.update_text_rectangle(rect) {
646                        Some(rect) => {
647                            let rs = &mut self.render_state;
648                            rs.text_rectangle = rect;
649                            rs.line_number = 0;
650                            rs.span_number = 0;
651                        }
652                        None => {
653                            return Err(SyntaxError::UnsupportedTagValue(
654                                val.into(),
655                            ));
656                        }
657                    }
658                    break;
659                }
660                Value::Text(t) => {
661                    let rs = &mut self.render_state;
662                    self.spans.push(Span::Text(rs.clone(), t));
663                    rs.span_number += 1;
664                    line_blank = false;
665                }
666                Value::HexadecimalCharacter(hc) => {
667                    match std::char::from_u32(hc.into()) {
668                        Some(c) => {
669                            let rs = &mut self.render_state;
670                            let mut fs = FStr::from_ascii_filler(0);
671                            write!(fs.writer_at(0), "{c}").unwrap();
672                            self.spans.push(Span::HexChar(rs.clone(), fs));
673                            rs.span_number += 1;
674                            line_blank = false;
675                        }
676                        None => {
677                            // Invalid code point (surrogate in D800-DFFF range)
678                            return Err(SyntaxError::UnsupportedTagValue(
679                                val.into(),
680                            ));
681                        }
682                    }
683                }
684                _ => (),
685            }
686        }
687        self.render_text_spans(raster)?;
688        Ok(())
689    }
690
691    /// Update the text rectangle
692    fn update_text_rectangle(&self, rect: Rectangle) -> Option<Rectangle> {
693        let rect = rect.extend_width_height(self.default_state.text_rectangle);
694        if rect.intersection(self.default_state.text_rectangle) != rect {
695            return None;
696        }
697        let cw = self.char_width();
698        debug_assert!(cw > 0);
699        // Check text rectangle matches character boundaries
700        let x = rect.x - 1;
701        if !x.is_multiple_of(cw) || !rect.width.is_multiple_of(cw) {
702            return None;
703        }
704        let lh = self.char_height();
705        debug_assert!(lh > 0);
706        // Check text rectangle matches line boundaries
707        let y = rect.y - 1;
708        if !y.is_multiple_of(lh) || !rect.height.is_multiple_of(lh) {
709            return None;
710        }
711        Some(rect)
712    }
713
714    /// Render spans for the current text rectangle
715    fn render_text_spans(&self, raster: &mut Raster<SRgb8>) -> Result<()> {
716        self.check_justification()?;
717        for span in &self.spans {
718            let x = self.span_x(span)?.into();
719            let y = self.span_y(span)?.into();
720            let font = span.state().font(self.fonts())?;
721            span.render_text(raster, font, x, y)?;
722        }
723        Ok(())
724    }
725
726    /// Check page and line justification ordering
727    fn check_justification(&self) -> Result<()> {
728        #[allow(deprecated)]
729        let mut jp = JustificationPage::Other;
730        #[allow(deprecated)]
731        let mut jl = JustificationLine::Other;
732        let mut ln = 0;
733        for span in &self.spans {
734            let just_page = span.state().just_page;
735            let just_line = span.state().just_line;
736            let line_number = span.state().line_number;
737            if just_page < jp
738                || (just_page == jp && line_number == ln && just_line < jl)
739            {
740                return Err(SyntaxError::TagConflict);
741            }
742            jp = just_page;
743            jl = just_line;
744            ln = line_number;
745        }
746        Ok(())
747    }
748
749    /// Get the X position of a text span
750    fn span_x(&self, span: &Span) -> Result<u16> {
751        match span.state().just_line {
752            JustificationLine::Left => self.span_x_left(span),
753            JustificationLine::Center => self.span_x_center(span),
754            JustificationLine::Right => self.span_x_right(span),
755            _ => unreachable!(),
756        }
757    }
758
759    /// Get the X position of a left-justified text span
760    fn span_x_left(&self, span: &Span) -> Result<u16> {
761        let left = span.state().text_rectangle.x - 1;
762        let (before, _) = self.offset_horiz(span)?;
763        Ok(left + before)
764    }
765
766    /// Get the X position of a center-justified text span
767    fn span_x_center(&self, span: &Span) -> Result<u16> {
768        let left = span.state().text_rectangle.x - 1;
769        let w = span.state().text_rectangle.width;
770        let (before, after) = self.offset_horiz(span)?;
771        let offset = (w - before - after) / 2; // offset for centering
772        let x = left + offset + before;
773        let cw = self.char_width();
774        // Truncate to character-width boundary
775        Ok((x / cw) * cw)
776    }
777
778    /// Get the X position of a right-justified span
779    fn span_x_right(&self, span: &Span) -> Result<u16> {
780        let left = span.state().text_rectangle.x - 1;
781        let w = span.state().text_rectangle.width;
782        let (_, after) = self.offset_horiz(span)?;
783        Ok(left + w - after)
784    }
785
786    /// Calculate horizontal offsets of a span.
787    ///
788    /// Returns a tuple of (before, after) widths of matching spans.
789    fn offset_horiz(&self, text_span: &Span) -> Result<(u16, u16)> {
790        debug!("offset_horiz '{}'", text_span.as_str());
791        let rs = &text_span.state();
792        let mut before = 0;
793        let mut after = 0;
794        let mut pspan = None;
795        for span in self.spans.iter().filter(|s| rs.matches_span(s.state())) {
796            if let Some(ps) = pspan {
797                let w = span.char_spacing_between(ps, self.fonts())?;
798                if span.state().span_number <= rs.span_number {
799                    before += w
800                } else {
801                    after += w
802                }
803                debug!("  spacing {w} before {before} after {after}");
804            }
805            let w = span.width(self.fonts())?;
806            if span.state().span_number < rs.span_number {
807                before += w
808            } else {
809                after += w
810            }
811            debug!("  span '{}'  before {before} after {after}", span.as_str());
812            pspan = Some(span);
813        }
814        if before + after <= rs.text_rectangle.width {
815            Ok((before, after))
816        } else {
817            Err(SyntaxError::TextTooBig)
818        }
819    }
820
821    /// Get the Y position of a text span
822    fn span_y(&self, span: &Span) -> Result<u16> {
823        let b = self.baseline(span)?;
824        let h = span.height(self.fonts())?;
825        debug_assert!(b >= h);
826        Ok(b - h)
827    }
828
829    /// Get the baseline of a text span
830    fn baseline(&self, span: &Span) -> Result<u16> {
831        match span.state().just_page {
832            JustificationPage::Top => self.baseline_top(span),
833            JustificationPage::Middle => self.baseline_middle(span),
834            JustificationPage::Bottom => self.baseline_bottom(span),
835            _ => unreachable!(),
836        }
837    }
838
839    /// Get the baseline of a top-justified span
840    fn baseline_top(&self, span: &Span) -> Result<u16> {
841        let top = span.state().text_rectangle.y - 1;
842        let (above, _) = self.offset_vert(span)?;
843        Ok(top + above)
844    }
845
846    /// Get the baseline of a middle-justified span
847    fn baseline_middle(&self, span: &Span) -> Result<u16> {
848        let top = span.state().text_rectangle.y - 1;
849        let h = span.state().text_rectangle.height;
850        let (above, below) = self.offset_vert(span)?;
851        let offset = (h - above - below) / 2; // offset for centering
852        let y = top + offset + above;
853        let ch = self.char_height();
854        // Truncate to line-height boundary
855        Ok((y / ch) * ch)
856    }
857
858    /// Get the baseline of a bottom-justified span
859    fn baseline_bottom(&self, span: &Span) -> Result<u16> {
860        let top = span.state().text_rectangle.y - 1;
861        let h = span.state().text_rectangle.height;
862        let (_, below) = self.offset_vert(span)?;
863        Ok(top + h - below)
864    }
865
866    /// Calculate vertical offset of a span.
867    ///
868    /// Returns a tuple of (above, below) heights of matching lines.
869    fn offset_vert(&self, text_span: &Span) -> Result<(u16, u16)> {
870        debug!("offset_vert '{}'", text_span.as_str());
871        let is_full_matrix = self.dms.char_height() == 0;
872        let rs = &text_span.state();
873        let mut lines = Vec::new();
874        for span in self.spans.iter().filter(|s| rs.matches_line(s.state())) {
875            let ln = usize::from(span.state().line_number);
876            let h = span.height(self.fonts())?;
877            let fs = span.font_spacing(self.fonts())?;
878            let ls = span.line_spacing();
879            let line = TextLine::new(h, fs, ls);
880            if ln >= lines.len() {
881                lines.push(line);
882            } else {
883                lines[ln].combine(&line);
884            }
885        }
886        let sln = usize::from(rs.line_number);
887        let mut above = 0;
888        let mut below = 0;
889        for ln in 0..lines.len() {
890            let line = &lines[ln];
891            if ln > 0 && is_full_matrix {
892                let h = line.line_spacing(&lines[ln - 1]);
893                if ln <= sln {
894                    above += h
895                } else {
896                    below += h
897                }
898                debug!("  spacing {}  above {} below {}", h, above, below);
899            }
900            let h = line.height;
901            if ln <= sln {
902                above += h
903            } else {
904                below += h
905            }
906            debug!("  line {}  above {} below {}", ln, above, below);
907        }
908        if above + below <= rs.text_rectangle.height {
909            Ok((above, below))
910        } else {
911            Err(SyntaxError::TextTooBig)
912        }
913    }
914}
915
916impl<const C: usize, const F: usize, const G: usize> Iterator
917    for Pages<'_, C, F, G>
918{
919    type Item = Result<Page>;
920
921    fn next(&mut self) -> Option<Self::Item> {
922        match self.page_state {
923            PageState::On(_) => Some(self.render_on_page()),
924            PageState::Off(_) => Some(Ok(self.render_off_page())),
925            _ => None,
926        }
927    }
928}
929
930/// Render a color rectangle.
931fn render_rect(
932    raster: &mut Raster<SRgb8>,
933    rect: Rectangle,
934    clr: SRgb8,
935    value: &Value,
936) -> Result<()> {
937    debug_assert!(rect.x > 0);
938    debug_assert!(rect.y > 0);
939    let width = raster.width().try_into().unwrap();
940    let height = raster.height().try_into().unwrap();
941    let full_rect = Rectangle::new(1, 1, width, height);
942    let rect = rect.extend_width_height(full_rect);
943    if rect.intersection(full_rect) == rect {
944        let rx = i32::from(rect.x) - 1;
945        let ry = i32::from(rect.y) - 1;
946        let rw = u32::from(rect.width);
947        let rh = u32::from(rect.height);
948        let region = Region::new(rx, ry, rw, rh);
949        raster.copy_color(region, clr);
950        Ok(())
951    } else {
952        Err(SyntaxError::UnsupportedTagValue(value.into()))
953    }
954}
955
956#[cfg(test)]
957mod test {
958    use super::*;
959    use crate::dms::config::{MultiCfg, SignCfg, VmsCfg};
960    use crate::dms::font::tfon;
961    use crate::dms::multi::{ColorClassic, ColorScheme};
962
963    fn font_table() -> FontTable<128, 4> {
964        let mut fonts = FontTable::default();
965        let buf = include_str!("../../test/F07-C.tfon");
966        let f = fonts.font_mut(0).unwrap();
967        *f = tfon::parse(&buf[..]).unwrap();
968        let buf = include_str!("../../test/F08.tfon");
969        let f = fonts.font_mut(0).unwrap();
970        *f = tfon::parse(&buf[..]).unwrap();
971        fonts
972    }
973
974    fn render_full(ms: &str) -> Result<Vec<Page>> {
975        let dms = Dms::<128, 4, 0>::builder()
976            .with_sign_cfg(SignCfg {
977                sign_width: 2100,
978                sign_height: 1110,
979                ..Default::default()
980            })
981            .with_vms_cfg(VmsCfg {
982                char_height_pixels: 0,
983                char_width_pixels: 0,
984                sign_height_pixels: 30,
985                sign_width_pixels: 60,
986                horizontal_pitch: 33,
987                vertical_pitch: 33,
988                ..Default::default()
989            })
990            .with_font_definition(font_table())
991            .with_multi_cfg(MultiCfg {
992                default_justification_line: JustificationLine::Left,
993                default_justification_page: JustificationPage::Top,
994                default_font: 8,
995                color_scheme: ColorScheme::Color24Bit,
996                default_foreground_rgb: ColorClassic::White.rgb().into(),
997                ..Default::default()
998            })
999            .build()
1000            .unwrap();
1001        Pages::new(&dms, ms).collect()
1002    }
1003
1004    #[test]
1005    fn page_count() {
1006        assert_eq!(render_full("").unwrap().len(), 1);
1007        assert_eq!(render_full("1").unwrap().len(), 1);
1008        assert_eq!(render_full("[np]").unwrap().len(), 2);
1009        assert_eq!(render_full("1[NP]").unwrap().len(), 2);
1010        assert_eq!(render_full("1[Np]2").unwrap().len(), 2);
1011        assert_eq!(render_full("1[np]2[nP]").unwrap().len(), 3);
1012        assert_eq!(render_full("[pto1]1[np]2").unwrap().len(), 4);
1013        assert_eq!(render_full("[pto1][np]").unwrap().len(), 4);
1014        let pages = render_full(
1015            "[fo8][jl2][cf255,255,255]RAMP A[jl4][cf255,255,0]FULL[nl]\
1016             [jl2][cf255,255,255]RAMP B[jl4][cf255,255,0]FULL[nl]\
1017             [jl2][cf255,255,255]RAMP C[jl4][cf255,255,0]FULL",
1018        )
1019        .unwrap();
1020        assert_eq!(pages.len(), 1);
1021    }
1022
1023    #[test]
1024    fn page_times() {
1025        assert_eq!(render_full("").unwrap()[0].duration_ds, 30);
1026        assert_eq!(render_full("[pt25o10]").unwrap()[0].duration_ds, 25);
1027        assert_eq!(render_full("[pt20o10]").unwrap()[1].duration_ds, 10);
1028        assert_eq!(render_full("[pt30o5][np]").unwrap()[2].duration_ds, 30);
1029        assert_eq!(render_full("[pto15][np]").unwrap()[3].duration_ds, 15);
1030    }
1031
1032    fn justify_dot(ms: &str, i: usize) {
1033        let mut raster = Raster::<SRgb8>::with_clear(60, 30);
1034        raster.pixels_mut()[i] = SRgb8::new(255, 255, 255);
1035        let pages = render_full(ms).unwrap();
1036        assert_eq!(pages.len(), 1);
1037        let page = &pages[0].raster;
1038        for (i, (p0, p1)) in
1039            page.pixels().iter().zip(raster.pixels()).enumerate()
1040        {
1041            dbg!(i);
1042            assert_eq!(p0, p1);
1043        }
1044        assert_eq!(page.pixels(), raster.pixels());
1045    }
1046
1047    #[test]
1048    fn left_justify() {
1049        // 60 pixels wide * 7 = 420
1050        justify_dot(".", 420);
1051    }
1052
1053    #[test]
1054    fn center_justify() {
1055        justify_dot("[jl3].", 449);
1056    }
1057
1058    #[test]
1059    fn right_justify() {
1060        justify_dot("[jl4].", 478);
1061    }
1062
1063    #[test]
1064    fn middle_justify() {
1065        justify_dot("[jp3].", 1080);
1066    }
1067
1068    #[test]
1069    fn bottom_justify() {
1070        justify_dot("[jp4].", 1740);
1071    }
1072
1073    #[test]
1074    fn char_spacing() {
1075        justify_dot(" .", 423);
1076        justify_dot("[sc4] .[/sc]", 425);
1077        justify_dot("[sc5] .[/sc]", 426);
1078    }
1079
1080    #[test]
1081    fn line_spacing() {
1082        justify_dot("[nl].", 1020);
1083        justify_dot("[nl1].", 960);
1084        justify_dot("[nl3].", 1080);
1085    }
1086
1087    #[test]
1088    fn text_rectangles() {
1089        justify_dot("[tr2,1,10,10].", 421);
1090        justify_dot("[tr1,2,10,10].", 480);
1091        justify_dot("[tr2,2,10,10].", 481);
1092    }
1093
1094    fn render_char(ms: &str) -> Result<Vec<Page>> {
1095        let dms = Dms::<128, 4, 0>::builder()
1096            .with_sign_cfg(SignCfg {
1097                sign_width: 7120,
1098                sign_height: 1590,
1099                ..Default::default()
1100            })
1101            .with_vms_cfg(VmsCfg {
1102                char_height_pixels: 7,
1103                char_width_pixels: 5,
1104                sign_height_pixels: 21,
1105                sign_width_pixels: 100,
1106                ..Default::default()
1107            })
1108            .with_font_definition(font_table())
1109            .with_multi_cfg(MultiCfg {
1110                default_justification_line: JustificationLine::Left,
1111                default_justification_page: JustificationPage::Top,
1112                default_font: 5,
1113                ..Default::default()
1114            })
1115            .build()
1116            .unwrap();
1117        Pages::new(&dms, ms).collect()
1118    }
1119
1120    #[test]
1121    fn page_char_matrix() {
1122        match render_char("[tr1,1,12,12]") {
1123            Err(SyntaxError::UnsupportedTagValue(_)) => assert!(true),
1124            _ => assert!(false),
1125        }
1126        match render_char("[tr1,1,50,12]") {
1127            Err(SyntaxError::UnsupportedTagValue(_)) => assert!(true),
1128            _ => assert!(false),
1129        }
1130        match render_char("[tr1,1,12,14]") {
1131            Err(SyntaxError::UnsupportedTagValue(_)) => assert!(true),
1132            _ => assert!(false),
1133        }
1134        match render_char("[tr1,1,50,14]") {
1135            Ok(_) => assert!(true),
1136            _ => assert!(false),
1137        }
1138        match render_char("[pb9]") {
1139            Err(SyntaxError::UnsupportedTagValue(_)) => assert!(true),
1140            _ => assert!(false),
1141        }
1142    }
1143
1144    #[test]
1145    fn char_matrix_spacing() {
1146        match render_char("[sc1][/sc]") {
1147            Err(SyntaxError::UnsupportedTagValue(_)) => assert!(true),
1148            _ => assert!(false),
1149        }
1150        match render_char("[sc0][/sc]") {
1151            Ok(_) => assert!(true),
1152            _ => assert!(false),
1153        }
1154    }
1155
1156    fn render_line(ms: &str) -> Result<Vec<Page>> {
1157        let dms = Dms::<128, 4, 0>::builder()
1158            .with_sign_cfg(SignCfg {
1159                sign_width: 7120,
1160                sign_height: 1800,
1161                ..Default::default()
1162            })
1163            .with_vms_cfg(VmsCfg {
1164                char_height_pixels: 8,
1165                char_width_pixels: 0,
1166                sign_height_pixels: 24,
1167                sign_width_pixels: 100,
1168                ..Default::default()
1169            })
1170            .with_font_definition(font_table())
1171            .with_multi_cfg(MultiCfg {
1172                default_justification_line: JustificationLine::Left,
1173                default_justification_page: JustificationPage::Top,
1174                default_font: 8,
1175                ..Default::default()
1176            })
1177            .build()
1178            .unwrap();
1179        Pages::new(&dms, ms).collect()
1180    }
1181
1182    #[test]
1183    fn line_matrix_lines() {
1184        match render_line(".[nl].[nl].") {
1185            Ok(_) => assert!(true),
1186            Err(e) => panic!("{e}"),
1187        }
1188    }
1189
1190    #[test]
1191    fn line_matrix_spacing() {
1192        match render_line("[nl]") {
1193            Ok(_) => assert!(true),
1194            _ => assert!(false),
1195        }
1196        match render_line("[nl1]") {
1197            Err(SyntaxError::UnsupportedTagValue(_)) => assert!(true),
1198            _ => assert!(false),
1199        }
1200    }
1201
1202    #[test]
1203    fn page_just_quirk() {
1204        match render_line("[jl3]LINE 1[nl][jl2][jp2]LINE 2") {
1205            Ok(_) => assert!(true),
1206            Err(_) => assert!(false),
1207        }
1208    }
1209}