// Copyright (C) 2023 Matthew Waters // // This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at // . // // SPDX-License-Identifier: MPL-2.0 use cea708_types::{tables::*, Service}; use std::collections::VecDeque; use gst::glib; use gst::prelude::MulDiv; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use pango::prelude::*; use crate::ccutils::recalculate_pango_layout; use crate::cea608utils::{Cea608Renderer, TextStyle}; static CAT: Lazy = Lazy::new(|| { gst::DebugCategory::new( "cea708utils", gst::DebugColorFlags::empty(), Some("CEA-708 Utilities"), ) }); #[derive( Serialize, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum, )] #[repr(u32)] #[enum_type(name = "GstTtToCea708Mode")] pub enum Cea708Mode { PopOn, PaintOn, RollUp, } pub fn textstyle_foreground_color(style: TextStyle) -> cea708_types::tables::Color { let color = match style { TextStyle::Red => cea608_types::tables::Color::Red, TextStyle::Blue => cea608_types::tables::Color::Blue, TextStyle::Cyan => cea608_types::tables::Color::Cyan, TextStyle::White => cea608_types::tables::Color::White, TextStyle::Green => cea608_types::tables::Color::Green, TextStyle::Yellow => cea608_types::tables::Color::Yellow, TextStyle::Magenta => cea608_types::tables::Color::Magenta, TextStyle::ItalicWhite => cea608_types::tables::Color::White, }; cea608_color_to_foreground_color(color) } pub fn cea608_color_to_foreground_color( color: cea608_types::tables::Color, ) -> cea708_types::tables::Color { match color { cea608_types::tables::Color::Red => cea708_types::tables::Color { r: ColorValue::Full, g: ColorValue::None, b: ColorValue::None, }, cea608_types::tables::Color::Green => cea708_types::tables::Color { r: ColorValue::None, g: ColorValue::Full, b: ColorValue::None, }, cea608_types::tables::Color::Blue => cea708_types::tables::Color { r: ColorValue::None, g: ColorValue::None, b: ColorValue::Full, }, cea608_types::tables::Color::Cyan => cea708_types::tables::Color { r: ColorValue::None, g: ColorValue::Full, b: ColorValue::Full, }, cea608_types::tables::Color::Yellow => cea708_types::tables::Color { r: ColorValue::Full, g: ColorValue::Full, b: ColorValue::None, }, cea608_types::tables::Color::Magenta => cea708_types::tables::Color { r: ColorValue::Full, g: ColorValue::None, b: ColorValue::Full, }, cea608_types::tables::Color::White => cea708_types::tables::Color { r: ColorValue::Full, g: ColorValue::Full, b: ColorValue::Full, }, } } pub fn textstyle_to_pen_color(style: TextStyle) -> SetPenColorArgs { let black = Color { r: ColorValue::None, g: ColorValue::None, b: ColorValue::None, }; SetPenColorArgs { foreground_color: textstyle_foreground_color(style), foreground_opacity: Opacity::Solid, background_color: black, background_opacity: Opacity::Solid, edge_color: black, } } #[derive(Debug)] pub(crate) struct Cea708ServiceWriter { codes: Vec, service_no: u8, active_window: WindowBits, hidden_window: WindowBits, } impl Cea708ServiceWriter { pub fn new(service_no: u8) -> Self { Self { codes: vec![], service_no, active_window: WindowBits::ZERO, hidden_window: WindowBits::ONE, } } pub fn take_service(&mut self, available_bytes: usize) -> Option { if self.codes.is_empty() { return None; } gst::trace!(CAT, "New service block {}", self.service_no); let mut service = Service::new(self.service_no); let mut i = 0; for code in self.codes.iter() { if code.byte_len() > service.free_space() { gst::trace!(CAT, "service is full"); break; } if service.len() + code.byte_len() > available_bytes { gst::trace!(CAT, "packet is full"); break; } gst::trace!(CAT, "Adding code {code:?} to service"); match service.push_code(code) { Ok(_) => i += 1, Err(_) => break, } } if i == 0 { return None; } self.codes = self.codes.split_off(i); Some(service) } pub fn popon_preamble(&mut self) { gst::trace!(CAT, "popon_preamble"); let window = match self.hidden_window { // switch up the newly defined window WindowBits::ZERO => 0, WindowBits::ONE => 1, _ => unreachable!(), }; let args = DefineWindowArgs::new( window, 0, Anchor::BottomMiddle, true, 100, 50, 14, 31, true, true, false, 1, 1, ); gst::trace!(CAT, "active window {:?}", self.active_window); let codes = [ Code::DeleteWindows(!self.active_window), Code::DefineWindow(args), ]; self.push_codes(&codes) } pub fn clear_current_window(&mut self) { gst::trace!(CAT, "clear_current_window {:?}", self.active_window); self.push_codes(&[Code::ClearWindows(self.active_window)]) } pub fn clear_hidden_window(&mut self) { gst::trace!(CAT, "clear_hidden_window"); self.push_codes(&[Code::ClearWindows(self.hidden_window)]) } pub fn end_of_caption(&mut self) { gst::trace!(CAT, "end_of_caption"); self.push_codes(&[Code::ToggleWindows(self.active_window | self.hidden_window)]); std::mem::swap(&mut self.active_window, &mut self.hidden_window); gst::trace!(CAT, "active window {:?}", self.active_window); } pub fn paint_on_preamble(&mut self) { gst::trace!(CAT, "paint_on_preamble"); let window = match self.active_window { WindowBits::ZERO => 0, WindowBits::ONE => 1, _ => unreachable!(), }; self.push_codes(&[ // FIXME: assumes positioning in a 16:9 ratio Code::DefineWindow(DefineWindowArgs::new( window, 0, Anchor::BottomMiddle, true, 100, 50, 14, 31, true, true, true, 1, 1, )), ]) } pub fn rollup_preamble(&mut self, rollup_count: u8, base_row: u8) { let base_row = std::cmp::max(rollup_count, base_row); let anchor_vertical = (base_row as u32 * 100 / 14) as u8; gst::trace!( CAT, "rollup_preamble base {base_row} count {rollup_count}, anchor-v {anchor_vertical}" ); let codes = [ Code::DeleteWindows(!WindowBits::ZERO), Code::DefineWindow(DefineWindowArgs::new( 0, 0, Anchor::BottomMiddle, true, anchor_vertical, 50, rollup_count - 1, 31, true, true, true, 1, 1, )), Code::SetPenLocation(SetPenLocationArgs::new(rollup_count - 1, 0)), ]; self.active_window = WindowBits::ZERO; self.hidden_window = WindowBits::ONE; self.push_codes(&codes) } pub fn write_char(&mut self, c: char) { if let Some(code) = Code::from_char(c) { self.push_codes(&[code]) } } pub fn push_codes(&mut self, codes: &[Code]) { gst::log!(CAT, "pushing codes: {codes:?}"); self.codes.extend(codes.iter().cloned()); } pub fn etx(&mut self) { self.push_codes(&[Code::ETX]) } pub fn carriage_return(&mut self) { self.push_codes(&[Code::CR]) } pub fn set_pen_attributes(&mut self, args: SetPenAttributesArgs) { self.push_codes(&[Code::SetPenAttributes(args)]) } pub fn set_pen_location(&mut self, args: SetPenLocationArgs) { self.push_codes(&[Code::SetPenLocation(args)]) } pub fn set_pen_color(&mut self, args: SetPenColorArgs) { self.push_codes(&[Code::SetPenColor(args)]) } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum ServiceOrChannel { Service(u8), Cea608Channel(cea608_types::Id), } pub struct Cea708Renderer { selected: Option, cea608: Cea608Renderer, service: Option, video_width: u32, video_height: u32, composition: Option, } impl Cea708Renderer { pub fn new() -> Self { Self { selected: None, cea608: Cea608Renderer::new(), service: None, video_width: 0, video_height: 0, composition: None, } } pub fn set_video_size(&mut self, width: u32, height: u32) { if width != self.video_width || height != self.video_height { self.video_width = width; self.video_height = height; self.cea608.set_video_size(width, height); if let Some(service) = self.service.as_mut() { service.set_video_size(width, height); } self.composition.take(); } } pub fn set_service_channel(&mut self, service_channel: Option) { if self.selected != service_channel { self.selected = service_channel; match service_channel { Some(ServiceOrChannel::Cea608Channel(id)) => self.cea608.set_channel(id.channel()), None => self.cea608.set_channel(cea608_types::tables::Channel::ONE), _ => (), } } } pub fn push_service(&mut self, service: &Service) { for code in service.codes() { let overlay_service = self.service.get_or_insert_with(|| { let mut service = ServiceState::new(); service.set_video_size(self.video_width, self.video_height); service }); overlay_service.handle_code(code); } } pub fn push_cea608( &mut self, field: cea608_types::tables::Field, pair: [u8; 2], ) -> Result { match self.selected { Some(ServiceOrChannel::Cea608Channel(channel)) => { if channel.field() != field { // data is not for the configured field, ignore return Ok(false); } else { channel } } None => cea608_types::Id::from_caption_field_channel( field, cea608_types::tables::Channel::ONE, ), // data is not for the configured service, ignore _ => return Ok(false), }; let ret = self.cea608.push_pair(pair); if let Ok(changed) = ret { if self.selected.is_none() { if let Some(chan) = self.cea608.channel() { self.selected = Some(ServiceOrChannel::Cea608Channel( cea608_types::Id::from_caption_field_channel(field, chan), )); } } if changed { self.composition.take(); } } ret } pub fn clear_composition(&mut self) { self.composition.take(); } pub fn generate_composition(&mut self) -> Option { let Some(selected) = self.selected else { self.composition.take(); return None; }; if matches!(selected, ServiceOrChannel::Service(_)) { let service = self.service.as_mut()?; let mut composition: Option = None; for window in service.windows.iter_mut() { if let Some(rectangle) = window.generate_rectangle() { if let Some(composition) = composition.as_mut() { composition.get_mut().unwrap().add_rectangle(&rectangle); } else { composition = gst_video::VideoOverlayComposition::new(Some(&rectangle)).ok(); } } } self.composition = composition; } else if let Some(rectangle) = self.cea608.generate_rectangle() { self.composition = gst_video::VideoOverlayComposition::new(Some(&rectangle)).ok(); } self.composition.clone() } } // SAFETY: Required because `pango::Layout` / `pango::Context` are not `Send` but the whole // `ServiceState` needs to be. // We ensure that no additional references to the layout are ever created, which makes it safe // to send it to other threads as long as only a single thread uses it concurrently. unsafe impl Send for ServiceState {} struct ServiceState { windows: VecDeque, current_window: usize, pango_context: pango::Context, video_width: u32, video_height: u32, } impl ServiceState { fn new() -> Self { let fontmap = pangocairo::FontMap::new(); let context = fontmap.create_context(); // XXX: may need a different language sometimes context.set_language(Some(&pango::Language::from_string("en_US"))); // XXX: May need a different direction context.set_base_dir(pango::Direction::Ltr); Self { windows: VecDeque::new(), current_window: usize::MAX, pango_context: context, video_width: 0, video_height: 0, } } fn window_mut(&mut self, id: usize) -> Option<&mut Window> { self.windows .iter_mut() .find(|window| window.define.window_id as usize == id) } fn define_window(&mut self, args: &DefineWindowArgs) { if let Some(window) = self.window_mut(args.window_id as usize) { if &window.define != args { // we only change these if they are different from the previous define_window // command window.attrs = args.window_attributes(); window.pen_attrs = args.pen_attributes(); window.pen_color = args.pen_color(); } window.define = *args; window.recalculate_window_position(); } else { let layout = pango::Layout::new(&self.pango_context); // XXX: May need a different alignment layout.set_alignment(pango::Alignment::Left); let mut window = Window { visible: args.visible, attrs: args.window_attributes(), pen_attrs: args.pen_attributes(), pen_color: args.pen_color(), define: *args, pen_location: SetPenLocationArgs::default(), lines: VecDeque::new(), rectangle: None, layout, video_dims: Dimensions::default(), window_position: Dimensions::default(), window_dims: Dimensions::default(), max_layout_dims: Dimensions::default(), }; window.set_video_size(self.video_width, self.video_height); self.windows.push_back(window); }; self.current_window = args.window_id as usize; } fn set_current_window(&mut self, window_id: u8) { self.current_window = window_id as usize; } fn clear_windows(&mut self, args: &WindowBits) { for window in self.windows.iter_mut() { if (WindowBits::from_window_id(window.define.window_id) & *args) != WindowBits::NONE { window.pen_location = SetPenLocationArgs::default(); window.lines.clear(); window.rectangle = None; } } } fn delete_windows(&mut self, args: &WindowBits) { self.windows.retain(|window| { (WindowBits::from_window_id(window.define.window_id) & *args) == WindowBits::NONE }); } fn display_windows(&mut self, args: &WindowBits) { for window in self.windows.iter_mut() { if (WindowBits::from_window_id(window.define.window_id) & *args) != WindowBits::NONE { window.visible = true; } } } fn hide_windows(&mut self, args: &WindowBits) { for window in self.windows.iter_mut() { if (WindowBits::from_window_id(window.define.window_id) & *args) != WindowBits::NONE { window.visible = false; } } } fn toggle_windows(&mut self, args: &WindowBits) { for window in self.windows.iter_mut() { if (WindowBits::from_window_id(window.define.window_id) & *args) != WindowBits::NONE { window.visible = !window.visible; } } } fn set_window_attributes(&mut self, attrs: &SetWindowAttributesArgs) { let Some(window) = self.window_mut(self.current_window) else { return; }; if &window.attrs != attrs { window.lines.clear(); window.attrs = *attrs; window.rectangle = None; } } fn set_pen_attributes(&mut self, attrs: &SetPenAttributesArgs) { let Some(window) = self.window_mut(self.current_window) else { return; }; window.pen_attrs = *attrs; } fn set_pen_color(&mut self, color: &SetPenColorArgs) { let Some(window) = self.window_mut(self.current_window) else { return; }; window.pen_color = *color; } fn set_pen_location(&mut self, location: &SetPenLocationArgs) { let Some(window) = self.window_mut(self.current_window) else { return; }; window.pen_location = *location; } fn reset(&mut self) { *self = Self::new(); } fn handle_code(&mut self, code: &Code) { match code { Code::DefineWindow(args) => self.define_window(args), Code::SetCurrentWindow0 => self.set_current_window(0), Code::SetCurrentWindow1 => self.set_current_window(1), Code::SetCurrentWindow2 => self.set_current_window(2), Code::SetCurrentWindow3 => self.set_current_window(3), Code::SetCurrentWindow4 => self.set_current_window(4), Code::SetCurrentWindow5 => self.set_current_window(5), Code::SetCurrentWindow6 => self.set_current_window(6), Code::SetCurrentWindow7 => self.set_current_window(7), Code::ClearWindows(args) => self.clear_windows(args), Code::DeleteWindows(args) => self.delete_windows(args), Code::DisplayWindows(args) => self.display_windows(args), Code::HideWindows(args) => self.hide_windows(args), Code::ToggleWindows(args) => self.toggle_windows(args), Code::SetWindowAttributes(args) => self.set_window_attributes(args), Code::SetPenAttributes(args) => self.set_pen_attributes(args), Code::SetPenColor(args) => self.set_pen_color(args), Code::SetPenLocation(args) => self.set_pen_location(args), Code::BS => self.backspace(), Code::CR => self.carriage_return(), Code::FF => { self.clear_windows(&WindowBits::from_window_id(self.current_window as u8)); self.set_pen_location(&SetPenLocationArgs { row: 0, column: 0 }); } Code::ETX => (), Code::HCR => self.horizontal_carriage_return(), Code::Reset => self.reset(), _ => { if let Some(ch) = code.char() { self.push_char(ch); } } } } fn push_char(&mut self, ch: char) { let Some(window) = self.window_mut(self.current_window) else { return; }; window.push_char(ch); } fn backspace(&mut self) { let Some(window) = self.window_mut(self.current_window) else { return; }; window.backspace(); } fn carriage_return(&mut self) { let Some(window) = self.window_mut(self.current_window) else { return; }; window.carriage_return(); } fn horizontal_carriage_return(&mut self) { let Some(window) = self.window_mut(self.current_window) else { return; }; window.horizontal_carriage_return(); } fn set_video_size(&mut self, video_width: u32, video_height: u32) { for window in self.windows.iter_mut() { window.set_video_size(video_width, video_height); } self.video_width = video_width; self.video_height = video_height; } } fn color_value_as_u16(val: ColorValue) -> u16 { match val { ColorValue::None => 0, ColorValue::OneThird => u16::MAX / 3, ColorValue::TwoThirds => u16::MAX / 3 * 2, ColorValue::Full => u16::MAX, } } fn opacity_as_u16(val: Opacity) -> u16 { match val { Opacity::Transparent => 0, Opacity::Translucent => u16::MAX / 3, // FIXME Opacity::Flash => u16::MAX / 3 * 2, Opacity::Solid => u16::MAX, } } fn pango_foreground_color_from_708(args: &SetPenColorArgs) -> pango::AttrColor { pango::AttrColor::new_foreground( color_value_as_u16(args.foreground_color.r), color_value_as_u16(args.foreground_color.g), color_value_as_u16(args.foreground_color.b), ) } fn pango_foreground_opacity_from_708(args: &SetPenColorArgs) -> pango::AttrInt { pango::AttrInt::new_foreground_alpha(opacity_as_u16(args.foreground_opacity)) } fn pango_background_color_from_708(args: &SetPenColorArgs) -> pango::AttrColor { pango::AttrColor::new_foreground( color_value_as_u16(args.background_color.r), color_value_as_u16(args.background_color.g), color_value_as_u16(args.background_color.b), ) } fn pango_background_opacity_from_708(args: &SetPenColorArgs) -> pango::AttrInt { pango::AttrInt::new_background_alpha(opacity_as_u16(args.background_opacity)) } #[derive(Debug, Default, PartialEq, Eq)] struct Dimensions { w: u32, h: u32, } pub enum Align { First, Center, Last, } impl Align { fn horizontal_from_anchor(anchor: Anchor) -> Self { match anchor { Anchor::TopLeft | Anchor::CenterLeft | Anchor::BottomLeft => Self::First, Anchor::TopMiddle | Anchor::CenterMiddle | Anchor::BottomMiddle => Self::Center, Anchor::TopRight | Anchor::CenterRight | Anchor::BottomRight => Self::Last, _ => Self::Center, } } fn vertical_from_anchor(anchor: Anchor) -> Self { match anchor { Anchor::TopLeft | Anchor::TopMiddle | Anchor::TopRight => Self::First, Anchor::CenterLeft | Anchor::CenterMiddle | Anchor::CenterRight => Self::Center, Anchor::BottomLeft | Anchor::BottomMiddle | Anchor::BottomRight => Self::Last, _ => Self::Last, } } } struct Window { visible: bool, define: DefineWindowArgs, attrs: SetWindowAttributesArgs, pen_attrs: SetPenAttributesArgs, pen_color: SetPenColorArgs, pen_location: SetPenLocationArgs, lines: VecDeque, window_position: Dimensions, video_dims: Dimensions, window_dims: Dimensions, max_layout_dims: Dimensions, rectangle: Option, layout: pango::Layout, } impl Window { fn dump(&self) { for line in self.lines.iter() { let mut string = line.no.to_string(); string.push(' '); for cell in line.line.iter() { string.push(cell.character.unwrap_or(' ')); } string.push('|'); gst::trace!(CAT, "dump: {string}"); } } fn ensure_cell(&mut self, row: usize, column: usize) { let line = if let Some(line) = self.lines.iter_mut().find(|line| line.no == row) { line } else { self.lines.push_back(WindowLine { no: row, line: VecDeque::new(), }); self.lines.back_mut().unwrap() }; while line.line.len() <= column { line.line .push_back(Cell::new_empty(self.pen_attrs, self.pen_color)); } } fn cell_mut(&mut self, row: usize, column: usize) -> Option<&mut Cell> { self.lines .iter_mut() .find(|line| line.no == row) .and_then(|line| line.line.get_mut(column)) } fn backspace(&mut self) { match self.attrs.print_direction { Direction::LeftToRight => { self.pen_location.column = self.pen_location.column.max(1) - 1; } Direction::RightToLeft => { self.pen_location.column = (self.pen_location.column + 1).min(self.column_count()); } Direction::TopToBottom => { self.pen_location.row = self.pen_location.row.max(1) - 1; } Direction::BottomToTop => { self.pen_location.row = (self.pen_location.row + 1).min(self.row_count()); } } self.ensure_cell( self.pen_location.row as usize, self.pen_location.column as usize, ); let cell = self .cell_mut( self.pen_location.row as usize, self.pen_location.column as usize, ) .unwrap(); if cell.character.take().is_some() { self.rectangle.take(); } } fn row_count(&self) -> u8 { self.define.row_count + 1 } fn column_count(&self) -> u8 { self.define.column_count + 1 } fn move_to_line_beginning(&mut self) { match self.attrs.print_direction { Direction::LeftToRight => { self.pen_location.column = 0; } Direction::RightToLeft => { self.pen_location.column = self.define.column_count; } Direction::TopToBottom => { self.pen_location.row = 0; } Direction::BottomToTop => { self.pen_location.row = self.row_count(); } } } fn scroll_top_to_bottom(&mut self) { if self.pen_location.row == 0 { let row_count = self.row_count() as usize; self.lines .retain(|line| (0..=row_count - 1).contains(&line.no)); for line in self.lines.iter_mut() { line.no += 1; } } else { self.pen_location.row -= 1; } } fn scroll_bottom_to_top(&mut self) { if self.pen_location.row >= self.define.row_count { let row_count = self.row_count() as usize; self.lines.retain(|line| (1..=row_count).contains(&line.no)); for line in self.lines.iter_mut() { line.no -= 1 } } else { self.pen_location.row += 1; } } fn scroll_left_to_right(&mut self) { if self.pen_location.column == 0 { gst::warning!(CAT, "Unsupported scroll direction left-to-right"); } else { self.pen_location.column -= 1; } } fn scroll_right_to_left(&mut self) { if self.pen_location.column >= self.column_count() { gst::warning!(CAT, "Unsupported scroll direction right-to-left"); } else { self.pen_location.column += 1; } } fn carriage_return(&mut self) { match (self.attrs.print_direction, self.attrs.scroll_direction) { (Direction::LeftToRight, Direction::TopToBottom) => { self.scroll_top_to_bottom(); self.move_to_line_beginning(); } (Direction::LeftToRight, Direction::BottomToTop) => { self.scroll_bottom_to_top(); self.move_to_line_beginning(); } (Direction::RightToLeft, Direction::TopToBottom) => { self.scroll_top_to_bottom(); self.move_to_line_beginning(); } (Direction::RightToLeft, Direction::BottomToTop) => { self.scroll_bottom_to_top(); self.move_to_line_beginning(); } (Direction::TopToBottom, Direction::LeftToRight) => { self.scroll_left_to_right(); self.move_to_line_beginning(); } (Direction::TopToBottom, Direction::RightToLeft) => { self.scroll_right_to_left(); self.move_to_line_beginning(); } (Direction::BottomToTop, Direction::LeftToRight) => { self.scroll_left_to_right(); self.move_to_line_beginning(); } (Direction::BottomToTop, Direction::RightToLeft) => { self.scroll_right_to_left(); self.move_to_line_beginning(); } // all other variants invalid (print, scroll) => { gst::warning!( CAT, "Unspecified print direction ({print:?}) and scroll direction ({scroll:?})" ); return; } } gst::trace!( CAT, "carriage return after position {},{}", self.pen_location.row, self.pen_location.column ); self.rectangle.take(); } fn horizontal_carriage_return(&mut self) { let min_row; let max_row; let min_column; let max_column; match self.attrs.print_direction { Direction::LeftToRight => { min_row = self.pen_location.row; max_row = self.pen_location.row; max_column = self.pen_location.column; min_column = 0; self.pen_location.column = 0; } Direction::RightToLeft => { min_row = self.pen_location.row; max_row = self.pen_location.row; min_column = self.pen_location.column; max_column = self.row_count(); self.pen_location.column = self.row_count(); } Direction::TopToBottom => { min_column = self.pen_location.column; max_column = self.pen_location.column; min_row = self.pen_location.row; max_row = 0; self.pen_location.row = 0; } Direction::BottomToTop => { min_column = self.pen_location.column; max_column = self.pen_location.column; min_row = self.pen_location.row; max_row = self.column_count(); self.pen_location.row = self.column_count(); } } for row in min_row..=max_row { for column in min_column..=max_column { self.ensure_cell(row as usize, column as usize); let cell = self.cell_mut(row as usize, column as usize).unwrap(); cell.character = None; } } self.rectangle.take(); } fn push_char(&mut self, ch: char) { if self.pen_location.row > self.row_count() { gst::warning!( CAT, "row {} outside configured window row count {}", self.pen_location.row, self.row_count() ); return; } if self.pen_location.column > self.column_count() { gst::warning!( CAT, "column {} outside configured window column count {}", self.pen_location.column, self.column_count() ); return; } gst::trace!( CAT, "push char \'{ch}\' at row {} column {}", self.pen_location.row, self.pen_location.column ); self.ensure_cell( self.pen_location.row as usize, self.pen_location.column as usize, ); let cell = self .cell_mut( self.pen_location.row as usize, self.pen_location.column as usize, ) .unwrap(); cell.character = Some(ch); self.rectangle.take(); match self.attrs.print_direction { Direction::LeftToRight => { self.pen_location.column = (self.pen_location.column + 1).min(self.column_count()); } Direction::RightToLeft => { self.pen_location.column = self.pen_location.column.max(1) - 1; } Direction::TopToBottom => { self.pen_location.row = (self.pen_location.row + 1).min(self.row_count()); } Direction::BottomToTop => { self.pen_location.row = self.pen_location.row.max(1) - 1; } } } fn recalculate_window_position(&mut self) { self.rectangle.take(); // XXX: may need a better implementation for 'skinny' (horizontal or vertical) output // sizes. let (max_layout_width, max_layout_height) = recalculate_pango_layout(&self.layout, self.video_dims.w, self.video_dims.h); self.max_layout_dims = Dimensions { w: max_layout_width as u32, h: max_layout_height as u32, }; let char_width = max_layout_width as u32 / 32; let char_height = max_layout_height as u32 / 15; let height = self.row_count() as u32 * char_height; let width = self.column_count() as u32 * char_width; self.window_dims = Dimensions { w: width, h: height, }; let padding = Dimensions { w: self.video_dims.w / 10, h: self.video_dims.h / 10, }; let safe_area = Dimensions { w: self.video_dims.w - self.video_dims.w / 5, h: self.video_dims.h - self.video_dims.h / 5, }; self.window_position = if self.define.relative_positioning { let halign = Align::horizontal_from_anchor(self.define.anchor_point); let valign = Align::vertical_from_anchor(self.define.anchor_point); let x = safe_area .w .mul_div_round(self.define.anchor_horizontal.min(100) as u32, 100) .unwrap(); let x = padding.w + match halign { Align::First => x, Align::Center => x.max(self.max_layout_dims.w / 2) - self.max_layout_dims.w / 2, Align::Last => x.max(self.window_dims.w) - self.window_dims.w, }; let y = safe_area .h .mul_div_round(self.define.anchor_vertical.min(100) as u32, 100) .unwrap(); let y = padding.h + match valign { Align::First => y, Align::Center => y.max(self.max_layout_dims.h / 2), Align::Last => y.max(self.window_dims.h) - self.window_dims.h, }; Dimensions { w: x, h: y } } else { // FIXME gst::fixme!(CAT, "Handle non-relative-positioning"); padding }; gst::trace!( CAT, "char sizes {char_width}x{char_height}, row/columns {}x{}, safe area {:?} window dimensions: {:?}, window position: {:?}, max layout {:?}, define {:?}", self.row_count(), self.column_count(), safe_area, self.window_dims, self.window_position, self.max_layout_dims, self.define, ); } fn set_video_size(&mut self, video_width: u32, video_height: u32) { let new_dims = Dimensions { w: video_width, h: video_height, }; if new_dims == self.video_dims { return; } self.video_dims = new_dims; self.recalculate_window_position(); } fn generate_rectangle(&mut self) -> Option { if !self.visible { return None; } if self.rectangle.is_some() { return self.rectangle.clone(); } self.dump(); // 1. generate the pango layout for the text let mut text = String::new(); let attrs = pango::AttrList::new(); let mut last_color = None; let mut last_attrs = None; let mut background_color_attr = pango::AttrColor::new_background(0, 0, 0); let mut background_opacity_attr = pango::AttrInt::new_background_alpha(0); let mut foreground_color_attr = pango::AttrColor::new_background(u16::MAX, u16::MAX, u16::MAX); let mut foreground_opacity_attr = pango::AttrInt::new_background_alpha(u16::MAX); let mut underline_attr = pango::AttrInt::new_underline(pango::Underline::None); let mut italic_attr = None::; let mut last_row = 0; for line in self.lines.iter() { for _ in 0..line.no - last_row { text.push('\n'); } last_row = line.no; for c in line.line.iter() { // XXX: Need to double check these indices with more complicated text characters let start_idx = text.len(); if last_color.map(|col| col != c.pen_color).unwrap_or(true) { background_color_attr.set_end_index(start_idx as u32); attrs.insert(background_color_attr.clone()); background_color_attr = pango_background_color_from_708(&c.pen_color); background_color_attr.set_start_index(start_idx as u32); background_opacity_attr.set_end_index(start_idx as u32); attrs.insert(background_opacity_attr.clone()); background_opacity_attr = pango_background_opacity_from_708(&c.pen_color); background_opacity_attr.set_start_index(start_idx as u32); foreground_color_attr.set_end_index(start_idx as u32); attrs.insert(foreground_color_attr.clone()); foreground_color_attr = pango_foreground_color_from_708(&c.pen_color); foreground_color_attr.set_start_index(start_idx as u32); foreground_opacity_attr.set_end_index(start_idx as u32); attrs.insert(foreground_opacity_attr.clone()); foreground_opacity_attr = pango_foreground_opacity_from_708(&c.pen_color); foreground_opacity_attr.set_start_index(start_idx as u32); last_color = Some(c.pen_color); } if last_attrs.map(|attrs| attrs != c.pen_attrs).unwrap_or(true) { underline_attr.set_end_index(start_idx as u32); attrs.insert(underline_attr.clone()); let underline_type = if c.pen_attrs.underline { pango::Underline::Single } else { pango::Underline::None }; underline_attr = pango::AttrInt::new_underline(underline_type); underline_attr.set_start_index(start_idx as u32); if !c.pen_attrs.italics { if let Some(mut italic) = italic_attr.take() { italic.set_end_index(start_idx as u32); attrs.insert(italic.clone()); } } else if c.pen_attrs.italics && italic_attr.is_none() { let mut attr = pango::AttrInt::new_style(pango::Style::Italic); attr.set_start_index(start_idx as u32); italic_attr = Some(attr); } last_attrs = Some(c.pen_attrs); } let Some(character) = c.character else { text.push(' '); continue; }; text.push(character); } } let start_idx = text.len(); background_color_attr.set_end_index(start_idx as u32); attrs.insert(background_color_attr.clone()); background_opacity_attr.set_end_index(start_idx as u32); attrs.insert(background_opacity_attr.clone()); foreground_color_attr.set_end_index(start_idx as u32); attrs.insert(foreground_color_attr.clone()); foreground_opacity_attr.set_end_index(start_idx as u32); attrs.insert(foreground_opacity_attr.clone()); underline_attr.set_end_index(start_idx as u32); attrs.insert(underline_attr.clone()); if let Some(mut italic) = italic_attr { italic.set_end_index(start_idx as u32); attrs.insert(italic); } self.layout.set_text(&text); self.layout.set_attributes(Some(&attrs)); let (_ink_rect, logical_rect) = self.layout.extents(); let height = logical_rect.height() / pango::SCALE; let width = logical_rect.width() / pango::SCALE; // 2. render text and window let render_buffer = || -> Result { let mut buffer = gst::Buffer::with_size((width * height) as usize * 4)?; gst_video::VideoMeta::add( buffer.get_mut().unwrap(), gst_video::VideoFrameFlags::empty(), #[cfg(target_endian = "little")] gst_video::VideoFormat::Bgra, #[cfg(target_endian = "big")] gst_video::VideoFormat::Argb, width as u32, height as u32, )?; let buffer = buffer.into_mapped_buffer_writable().unwrap(); // Pass ownership of the buffer to the cairo surface but keep around // a raw pointer so we can later retrieve it again when the surface // is done let buffer_ptr = buffer.buffer().as_ptr(); let surface = cairo::ImageSurface::create_for_data( buffer, cairo::Format::ARgb32, width, height, width * 4, )?; let cr = cairo::Context::new(&surface)?; // Clear background cr.set_operator(cairo::Operator::Source); cr.set_source_rgba(0.0, 0.0, 0.0, 0.0); cr.paint()?; // Render text outline cr.save()?; cr.set_operator(cairo::Operator::Over); cr.set_source_rgba(0.0, 0.0, 0.0, 1.0); pangocairo::functions::layout_path(&cr, &self.layout); cr.stroke()?; cr.restore()?; // Render text cr.save()?; cr.set_source_rgba(255.0, 255.0, 255.0, 1.0); pangocairo::functions::show_layout(&cr, &self.layout); cr.restore()?; drop(cr); // Safety: The surface still owns a mutable reference to the buffer but our reference // to the surface here is the last one. After dropping the surface the buffer would be // freed, so we keep an additional strong reference here before dropping the surface, // which is then returned. As such it's guaranteed that nothing is using the buffer // anymore mutably. unsafe { assert_eq!( cairo::ffi::cairo_surface_get_reference_count(surface.to_raw_none()), 1 ); let buffer = glib::translate::from_glib_none(buffer_ptr); drop(surface); Ok(buffer) } }; let buffer = match render_buffer() { Ok(buffer) => buffer, Err(e) => { self.dump(); gst::error!(CAT, "Failed to render buffer: \"{e}\""); return None; } }; gst::trace!( CAT, "sizes: video {:?}, window {:?} overlay {}x{}", self.video_dims, self.window_dims, width, height ); // 3. generate overlay rectangle // FIXME: use the window location values to place the overlay let ret = Some(gst_video::VideoOverlayRectangle::new_raw( &buffer, self.window_position.w as i32, self.window_position.h as i32, width as u32, height as u32, gst_video::VideoOverlayFormatFlags::PREMULTIPLIED_ALPHA, )); self.rectangle = ret.clone(); ret } } struct WindowLine { no: usize, line: VecDeque, } impl PartialOrd for WindowLine { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for WindowLine { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.no.cmp(&other.no) } } impl PartialEq for WindowLine { fn eq(&self, other: &Self) -> bool { self.no == other.no } } impl Eq for WindowLine {} struct Cell { character: Option, pen_attrs: SetPenAttributesArgs, pen_color: SetPenColorArgs, } impl Cell { fn new_empty(attrs: SetPenAttributesArgs, color: SetPenColorArgs) -> Self { Self { character: None, pen_attrs: attrs, pen_color: color, } } }