1use 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
18type Result<T> = std::result::Result<T, SyntaxError>;
20
21const MAX_TEXT_RECTANGLES: u32 = 50;
23
24pub struct Page {
26 pub raster: Raster<SRgb8>,
28
29 pub duration_ds: u16,
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35enum PageState {
36 On(bool),
38
39 Off(bool),
41
42 Done,
44}
45
46#[derive(Clone)]
48struct RenderState {
49 color_ctx: ColorCtx,
51
52 page_on_time_ds: u8,
54
55 page_off_time_ds: u8,
57
58 text_rectangle: Rectangle,
60
61 just_page: JustificationPage,
63
64 just_line: JustificationLine,
66
67 font_num: u8,
69
70 font_version_id: Option<u16>,
72
73 line_spacing: Option<u8>,
75
76 char_spacing: Option<u8>,
78
79 line_number: u8,
81
82 span_number: u8,
84}
85
86#[derive(Clone)]
88enum Span<'a> {
89 Text(RenderState, &'a str),
91
92 HexChar(RenderState, FStr<4>),
94}
95
96#[derive(Clone)]
98struct TextLine {
99 height: u16,
101
102 font_spacing: u16,
104
105 line_spacing: Option<u16>,
107}
108
109pub struct Pages<'a, const C: usize, const F: usize, const G: usize> {
126 dms: &'a Dms<C, F, G>,
128
129 default_state: RenderState,
131
132 render_state: RenderState,
134
135 page_state: PageState,
137
138 values: MultiStr<'a>,
140
141 spans: Vec<Span<'a>>,
143}
144
145impl PageState {
146 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 fn done(page_off: bool) -> Self {
157 if page_off {
158 PageState::Off(false)
159 } else {
160 PageState::Done
161 }
162 }
163
164 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 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 fn background_rgb(&self) -> SRgb8 {
204 let (r, g, b) = self.color_ctx.background_rgb();
205 SRgb8::new(r, g, b)
206 }
207
208 fn foreground_rgb(&self) -> SRgb8 {
210 let (r, g, b) = self.color_ctx.foreground_rgb();
211 SRgb8::new(r, g, b)
212 }
213
214 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 fn matches_line(&self, rhs: &Self) -> bool {
223 self.just_page == rhs.just_page
224 }
225
226 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 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 fn state(&self) -> &RenderState {
256 match self {
257 Span::Text(state, _text) => state,
258 Span::HexChar(state, _hc) => state,
259 }
260 }
261
262 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 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 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 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 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 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 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 fn line_spacing(&self) -> Option<u16> {
328 self.state().line_spacing.map(|sp| sp.into())
329 }
330
331 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 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 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 fn line_spacing(&self, rhs: &Self) -> u16 {
364 if let Some(ls) = self.line_spacing {
365 ls
366 } else {
367 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 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 fn fonts(&self) -> &FontTable<C, F> {
398 self.dms.font_definition()
399 }
400
401 fn char_width(&self) -> u16 {
403 self.dms.char_width().max(1).into()
404 }
405
406 fn char_height(&self) -> u16 {
408 self.dms.char_height().max(1).into()
409 }
410
411 fn page_on_time_ds(&self) -> u16 {
413 self.render_state.page_on_time_ds.into()
414 }
415
416 fn page_off_time_ds(&self) -> u16 {
418 self.render_state.page_off_time_ds.into()
419 }
420
421 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 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 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 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 fn update_page_state(&mut self) -> Result<()> {
478 let ds = &self.default_state;
479 let rs = &mut self.render_state;
480 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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; let x = left + offset + before;
773 let cw = self.char_width();
774 Ok((x / cw) * cw)
776 }
777
778 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 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 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 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 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 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; let y = top + offset + above;
853 let ch = self.char_height();
854 Ok((y / ch) * ch)
856 }
857
858 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 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
930fn 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 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}