icy_sauce/capabilities/
character.rs

1//! Character/text file format capabilities as specified in the SAUCE v00 standard.
2//!
3//! This module provides types for describing text-based file formats (ASCII, ANSI, RIPScript, etc.),
4//! their character dimensions, and rendering options like ice colors and letter spacing.
5//!
6//! # Formats Supported
7//!
8//! - **ASCII**: Plain ASCII text files
9//! - **ANSI**: ANSI escape sequences for color and formatting
10//! - **ANSiMation**: ANSI with timing for animation playback
11//! - **RIPScript**: Remote Imaging Protocol (pixel-based)
12//! - **PCBoard**: PCBoard BBS text format
13//! - **Avatar**: Avatar graphics format
14//! - **HTML**: Hypertext Markup Language
15//! - **Source Code**: Generic source code files
16//! - **TundraDraw**: TundraDraw BBS graphics
17//!
18//! # Example
19//!
20//! ```
21//! use icy_sauce::{CharacterCapabilities, CharacterFormat, LetterSpacing, AspectRatio};
22//! use bstr::BString;
23//!
24//! let caps = CharacterCapabilities::with_font(
25//!     CharacterFormat::Ansi,
26//!     80,  // width
27//!     25,  // height
28//!     true,  // ice colors enabled
29//!     LetterSpacing::NinePixel,
30//!     AspectRatio::Square,
31//!     Some(BString::from("IBM VGA")),
32//! )?;
33//! # Ok::<(), Box<dyn std::error::Error>>(())
34//! ```
35
36use bstr::BString;
37
38use crate::{SauceDataType, SauceError, header::SauceHeader, limits};
39
40/// ANSI flags bitmask for non-blink mode (ice colors).
41/// When set (bit 0), the 16 background colors become available instead of blinking.
42pub(crate) const ANSI_FLAG_NON_BLINK_MODE: u8 = 0b0000_0001;
43/// ANSI flags bitmask for letter spacing (bits 1-2).
44/// Values: 00=Legacy, 01=8-pixel, 10=9-pixel, 11=Reserved
45pub(crate) const ANSI_MASK_LETTER_SPACING: u8 = 0b0000_0110;
46pub(crate) const ANSI_LETTER_SPACING_LEGACY: u8 = 0b0000_0000;
47pub(crate) const ANSI_LETTER_SPACING_8PX: u8 = 0b0000_0010;
48pub(crate) const ANSI_LETTER_SPACING_9PX: u8 = 0b0000_0100;
49
50/// ANSI flags bitmask for aspect ratio (bits 3-4).
51/// Values: 00=Legacy, 01=LegacyDevice (needs stretching), 10=Square (modern), 11=Reserved
52pub(crate) const ANSI_MASK_ASPECT_RATIO: u8 = 0b0001_1000;
53pub(crate) const ANSI_ASPECT_RATIO_LEGACY: u8 = 0b0000_0000;
54pub(crate) const ANSI_ASPECT_RATIO_STRETCH: u8 = 0b0000_1000;
55pub(crate) const ANSI_ASPECT_RATIO_SQUARE: u8 = 0b0001_0000;
56
57#[derive(Debug, Clone, Copy, PartialEq)]
58/// Character format types as specified in the SAUCE v00 specification.
59///
60/// Each format represents a different text or graphics encoding standard used by
61/// the BBS and retro computing community. The numeric values correspond to the
62/// FileType field in the SAUCE header.
63pub enum CharacterFormat {
64    /// Plain ASCII text (value 0)
65    Ascii,
66    /// ANSI escape sequence art (value 1)
67    Ansi,
68    /// ANSI with animation/timing (value 2)
69    AnsiMation,
70    /// Remote Imaging Protocol - pixel-based graphics (value 3)
71    RipScript,
72    /// PCBoard BBS format (value 4)
73    PCBoard,
74    /// Avatar graphics format (value 5)
75    Avatar,
76    /// Hypertext Markup Language (value 6)
77    Html,
78    /// Generic source code (value 7)
79    Source,
80    /// TundraDraw format (value 8)
81    TundraDraw,
82    /// Unknown or unsupported format (preserves raw value)
83    Unknown(u8),
84}
85
86impl CharacterFormat {
87    /// Parse a character format from the SAUCE FileType byte.
88    ///
89    /// # Arguments
90    ///
91    /// * `file_type` - The 8-bit FileType value from the SAUCE header
92    ///
93    /// # Returns
94    ///
95    /// Returns the corresponding [`CharacterFormat`], or [`CharacterFormat::Unknown`]
96    /// if the value is not recognized.
97    ///
98    /// # Example
99    ///
100    /// ```
101    /// use icy_sauce::CharacterFormat;
102    /// let format = CharacterFormat::from_sauce(1);
103    /// assert_eq!(format, CharacterFormat::Ansi);
104    /// ```
105    pub fn from_sauce(file_type: u8) -> Self {
106        match file_type {
107            0 => CharacterFormat::Ascii,
108            1 => CharacterFormat::Ansi,
109            2 => CharacterFormat::AnsiMation,
110            3 => CharacterFormat::RipScript,
111            4 => CharacterFormat::PCBoard,
112            5 => CharacterFormat::Avatar,
113            6 => CharacterFormat::Html,
114            7 => CharacterFormat::Source,
115            8 => CharacterFormat::TundraDraw,
116            _ => CharacterFormat::Unknown(file_type),
117        }
118    }
119
120    /// Convert to the SAUCE FileType byte representation.
121    ///
122    /// # Returns
123    ///
124    /// The 8-bit FileType value suitable for writing to a SAUCE header.
125    ///
126    /// # Example
127    ///
128    /// ```
129    /// use icy_sauce::CharacterFormat;
130    /// assert_eq!(CharacterFormat::Ansi.to_sauce(), 1);
131    /// assert_eq!(CharacterFormat::Unknown(99).to_sauce(), 99);
132    /// ```
133    pub fn to_sauce(&self) -> u8 {
134        match self {
135            CharacterFormat::Ascii => 0,
136            CharacterFormat::Ansi => 1,
137            CharacterFormat::AnsiMation => 2,
138            CharacterFormat::RipScript => 3,
139            CharacterFormat::PCBoard => 4,
140            CharacterFormat::Avatar => 5,
141            CharacterFormat::Html => 6,
142            CharacterFormat::Source => 7,
143            CharacterFormat::TundraDraw => 8,
144            CharacterFormat::Unknown(ft) => *ft,
145        }
146    }
147
148    /// Check if this format supports ANSI flags (ice colors, letter spacing, aspect ratio).
149    ///
150    /// Only ASCII, ANSI, and ANSiMation formats support the extended rendering options
151    /// stored in the TFlags byte of the SAUCE header.
152    ///
153    /// # Example
154    ///
155    /// ```
156    /// use icy_sauce::CharacterFormat;
157    /// assert!(CharacterFormat::Ansi.supports_ansi_flags());
158    /// assert!(!CharacterFormat::Html.supports_ansi_flags());
159    /// ```
160    pub fn supports_ansi_flags(&self) -> bool {
161        matches!(
162            self,
163            CharacterFormat::Ascii | CharacterFormat::Ansi | CharacterFormat::AnsiMation
164        )
165    }
166
167    /// Check if this format stores character grid dimensions (width/height).
168    ///
169    /// Most formats store character dimensions in TInfo1 (width) and TInfo2 (height),
170    /// except HTML and Source which have variable dimensions.
171    ///
172    /// # Example
173    ///
174    /// ```
175    /// use icy_sauce::CharacterFormat;
176    /// assert!(CharacterFormat::Ansi.has_dimensions());
177    /// assert!(!CharacterFormat::Html.has_dimensions());
178    /// ```
179    pub fn has_dimensions(&self) -> bool {
180        matches!(
181            self,
182            CharacterFormat::Ascii
183                | CharacterFormat::Ansi
184                | CharacterFormat::AnsiMation
185                | CharacterFormat::PCBoard
186                | CharacterFormat::Avatar
187                | CharacterFormat::TundraDraw
188        )
189    }
190
191    /// Check if this format is a streaming format with variable height.
192    ///
193    /// Streaming formats can have content of any height, unlike fixed-size animations.
194    /// RIPScript and HTML are not considered streaming despite being variable-height.
195    ///
196    /// # Example
197    ///
198    /// ```
199    /// use icy_sauce::CharacterFormat;
200    /// assert!(CharacterFormat::Ansi.is_stream());
201    /// assert!(!CharacterFormat::AnsiMation.is_stream());
202    /// ```
203    pub fn is_stream(&self) -> bool {
204        matches!(
205            self,
206            CharacterFormat::Ascii
207                | CharacterFormat::Ansi
208                | CharacterFormat::PCBoard
209                | CharacterFormat::Avatar
210                | CharacterFormat::TundraDraw
211        )
212    }
213
214    /// Check if this format is an animation that requires fixed screen dimensions.
215    ///
216    /// ANSiMation requires both width and height to be fixed for proper playback timing.
217    ///
218    /// # Example
219    ///
220    /// ```
221    /// use icy_sauce::CharacterFormat;
222    /// assert!(CharacterFormat::AnsiMation.is_animation());
223    /// assert!(!CharacterFormat::Ansi.is_animation());
224    /// ```
225    pub fn is_animation(&self) -> bool {
226        matches!(self, CharacterFormat::AnsiMation)
227    }
228}
229
230#[derive(Debug, Clone, Copy, PartialEq)]
231/// Letter spacing mode for ANSI text, stored in bits 1-2 of TFlags.
232///
233/// Letter spacing controls the horizontal character width used when rendering,
234/// affecting how tight or loose the text appears on screen.
235pub enum LetterSpacing {
236    /// Legacy / undefined spacing (value 0)
237    Legacy,
238    /// 8-pixel character width (value 1)
239    EightPixel,
240    /// 9-pixel character width (value 2)
241    NinePixel,
242    /// Reserved value (value 3) - not standardized
243    Reserved,
244}
245
246impl LetterSpacing {
247    /// Check if this format uses the 9-pixel letter spacing.
248    ///
249    /// Returns `true` only for [`LetterSpacing::NinePixel`], which indicates
250    /// that modern 9-pixel wide characters should be used instead of 8-pixel legacy.
251    pub fn use_letter_spacing(self) -> bool {
252        self == LetterSpacing::NinePixel
253    }
254}
255
256#[derive(Debug, Clone, Copy, PartialEq)]
257/// Pixel aspect ratio for ANSI text rendering, stored in bits 3-4 of TFlags.
258///
259/// The aspect ratio determines how pixels should be displayed on different hardware:
260/// - Legacy systems had rectangular (non-square) pixels
261/// - Modern displays use square pixels
262pub enum AspectRatio {
263    /// Legacy (undefined) aspect ratio (value 0)
264    Legacy,
265    /// Legacy device ratio requiring vertical stretch (value 1)
266    LegacyDevice,
267    /// Square pixels, modern aspect ratio (value 2)
268    Square,
269    /// Reserved value (value 3) - not standardized
270    Reserved,
271}
272
273impl AspectRatio {
274    /// Check if this aspect ratio requires stretching.
275    ///
276    /// Returns `true` only for [`AspectRatio::LegacyDevice`], which indicates
277    /// that content should be vertically stretched to correct for rectangular pixels.
278    pub fn use_aspect_ratio(self) -> bool {
279        self == AspectRatio::LegacyDevice
280    }
281}
282
283/// Character/text file format capabilities and display options.
284///
285/// `CharacterCapabilities` describes how a text-based SAUCE file should be displayed, including:
286/// - The character encoding format (ANSI, ASCII, RIPScript, etc.)
287/// - Screen dimensions in characters (width × height)
288/// - Rendering options like ice colors and letter spacing
289/// - Optional font name for the display
290///
291/// # Field Constraints
292///
293/// - **Width/Height**: Range depends on format; typically 0-65535
294/// - **Font Name**: Max 22 bytes (space-padded in SAUCE header TInfoS field)
295/// - **ICE Colors**: Only for ANSI-compatible formats
296/// - **Letter Spacing/Aspect Ratio**: Only for ANSI-compatible formats
297///
298/// # Example
299///
300/// ```
301/// use icy_sauce::{CharacterCapabilities, CharacterFormat, LetterSpacing, AspectRatio};
302/// use bstr::BString;
303///
304/// let caps = CharacterCapabilities::new(CharacterFormat::Ansi);
305/// assert_eq!(caps.columns, 80);
306/// assert_eq!(caps.lines, 25);
307/// ```
308#[derive(Debug, Clone, PartialEq)]
309pub struct CharacterCapabilities {
310    /// The character encoding format
311    pub format: CharacterFormat,
312    /// Width in characters
313    pub columns: u16,
314    /// Height in characters (lines)
315    pub lines: u16,
316    /// Whether ICE colors (16 background colors) are enabled
317    pub ice_colors: bool,
318    /// Letter spacing mode (8px vs 9px)
319    pub letter_spacing: LetterSpacing,
320    /// Pixel aspect ratio for rendering
321    pub aspect_ratio: AspectRatio,
322    /// Optional font name (max 22 bytes)
323    pub font_opt: Option<BString>,
324}
325
326impl CharacterCapabilities {
327    /// Create a new `CharacterCapabilities` with default values.
328    ///
329    /// # Arguments
330    ///
331    /// * `character_format` - The [`CharacterFormat`] for this content
332    ///
333    /// # Defaults
334    ///
335    /// - Width: 80 characters
336    /// - Height: 25 characters
337    /// - ICE colors: disabled
338    /// - Letter spacing: Legacy (8-pixel)
339    /// - Aspect ratio: Legacy
340    /// - Font: None
341    ///
342    /// # Example
343    ///
344    /// ```
345    /// use icy_sauce::{CharacterCapabilities, CharacterFormat};
346    /// let caps = CharacterCapabilities::new(CharacterFormat::Ansi);
347    /// assert_eq!(caps.columns, 80);
348    /// assert_eq!(caps.lines, 25);
349    /// assert!(!caps.ice_colors);
350    /// ```
351    pub fn new(character_format: CharacterFormat) -> Self {
352        CharacterCapabilities {
353            format: character_format,
354            columns: 80,
355            lines: 25,
356            ice_colors: false,
357            letter_spacing: LetterSpacing::Legacy,
358            aspect_ratio: AspectRatio::Legacy,
359            font_opt: None,
360        }
361    }
362
363    /// Create a new `CharacterCapabilities` with all fields specified and font validation.
364    ///
365    /// This constructor is more explicit than [`new`](Self::new) and validates
366    /// all parameters, especially the font name length.
367    ///
368    /// # Arguments
369    ///
370    /// * `format` - The character encoding format
371    /// * `width` - Display width in characters
372    /// * `height` - Display height in characters (lines)
373    /// * `ice_colors` - Whether ICE colors (16 background colors) are available
374    /// * `letter_spacing` - Letter spacing mode for rendering
375    /// * `aspect_ratio` - Pixel aspect ratio for rendering
376    /// * `font` - Optional font name (max 22 bytes)
377    ///
378    /// # Errors
379    ///
380    /// Returns [`SauceError::FontNameTooLong`] if the font name exceeds 22 bytes.
381    ///
382    /// # Example
383    ///
384    /// ```
385    /// use icy_sauce::{CharacterCapabilities, CharacterFormat, LetterSpacing, AspectRatio};
386    /// use bstr::BString;
387    ///
388    /// let caps = CharacterCapabilities::with_font(
389    ///     CharacterFormat::Ansi,
390    ///     80, 25,
391    ///     true,
392    ///     LetterSpacing::NinePixel,
393    ///     AspectRatio::Square,
394    ///     Some(BString::from("IBM VGA")),
395    /// )?;
396    /// # Ok::<(), Box<dyn std::error::Error>>(())
397    /// ```
398    pub fn with_font(
399        format: CharacterFormat,
400        columns: u16,
401        lines: u16,
402        ice_colors: bool,
403        letter_spacing: LetterSpacing,
404        aspect_ratio: AspectRatio,
405        font: Option<BString>,
406    ) -> crate::Result<Self> {
407        // Validate font length if provided
408        if let Some(ref f) = font {
409            if f.len() > limits::MAX_FONT_NAME_LENGTH {
410                return Err(SauceError::FontNameTooLong(f.len()));
411            }
412        }
413
414        Ok(CharacterCapabilities {
415            format,
416            columns,
417            lines,
418            ice_colors,
419            letter_spacing,
420            aspect_ratio,
421            font_opt: font,
422        })
423    }
424
425    /// Get a reference to the optional font name.
426    ///
427    /// # Returns
428    ///
429    /// `Some(&font)` if a font has been set, or `None` if not.
430    ///
431    /// # Example
432    ///
433    /// ```
434    /// use icy_sauce::{CharacterCapabilities, CharacterFormat};
435    /// use bstr::BString;
436    /// let caps = CharacterCapabilities::new(CharacterFormat::Ansi);
437    /// assert_eq!(caps.font(), None);
438    /// ```
439    pub fn font(&self) -> Option<&BString> {
440        self.font_opt.as_ref()
441    }
442
443    /// Set the font name with validation.
444    ///
445    /// # Arguments
446    ///
447    /// * `font` - The font name to set (max 22 bytes), or empty to clear
448    ///
449    /// # Errors
450    ///
451    /// Returns [`SauceError::FontNameTooLong`] if the font name exceeds 22 bytes.
452    ///
453    /// # Behavior
454    ///
455    /// - Passing an empty `BString` clears the font (equivalent to [`clear_font`](Self::clear_font))
456    /// - Non-empty strings up to 22 bytes are stored
457    ///
458    /// # Example
459    ///
460    /// ```
461    /// # use icy_sauce::{CharacterCapabilities, CharacterFormat};
462    /// # use bstr::BString;
463    /// let mut caps = CharacterCapabilities::new(CharacterFormat::Ansi);
464    /// caps.set_font(BString::from("IBM VGA"))?;
465    /// assert_eq!(caps.font(), Some(&BString::from("IBM VGA")));
466    /// # Ok::<(), Box<dyn std::error::Error>>(())
467    /// ```
468    pub fn set_font(&mut self, font: BString) -> crate::Result<()> {
469        if font.len() > limits::MAX_FONT_NAME_LENGTH {
470            return Err(SauceError::FontNameTooLong(font.len()));
471        }
472        if font.is_empty() {
473            self.font_opt = None;
474            return Ok(());
475        }
476        self.font_opt = Some(font);
477        Ok(())
478    }
479
480    /// Clear the font name, setting it to `None`.
481    ///
482    /// This is equivalent to calling [`set_font`](Self::set_font) with an empty `BString`.
483    ///
484    /// # Example
485    ///
486    /// ```
487    /// # use icy_sauce::{CharacterCapabilities, CharacterFormat};
488    /// # use bstr::BString;
489    /// let mut caps = CharacterCapabilities::new(CharacterFormat::Ansi);
490    /// caps.remove_font();
491    /// assert_eq!(caps.font(), None);
492    /// ```
493    pub fn remove_font(&mut self) {
494        self.font_opt = None;
495    }
496
497    pub fn dimensions(mut self, columns: u16, lines: u16) -> Self {
498        self.columns = columns;
499        self.lines = lines;
500        self
501    }
502
503    /// Serialize these capabilities into a SAUCE header for file storage.
504    ///
505    /// This method populates the format-specific fields in the SAUCE header based on
506    /// the data type and character format. Each data type (Character, BinaryText, XBin)
507    /// has different field layouts as specified by the SAUCE standard.
508    ///
509    /// # Arguments
510    ///
511    /// * `header` - Mutable reference to the [`SauceHeader`] to populate
512    ///
513    /// # Errors
514    ///
515    /// Returns errors for invalid field values:
516    /// - [`SauceError::BinFileWidthLimitExceeded`]: Binary text width must be even and ≤510
517    /// - [`SauceError::UnsupportedDataType`]: Unsupported SAUCE data type
518    ///
519    /// # SAUCE Field Mappings
520    ///
521    /// **For Character data type:**
522    /// - FileType: Character format code (0-8 or unknown)
523    /// - TInfo1: Character width
524    /// - TInfo2: Character height
525    /// - TFlags: ICE colors, letter spacing, aspect ratio (for ANSI formats)
526    /// - TInfoS: Font name (for ANSI formats)
527    ///
528    /// **For BinaryText data type:**
529    /// - FileType: Width/2 (width must be even, max 510)
530    /// - TFlags: ICE colors flag
531    /// - TInfoS: Font name
532    ///
533    /// **For XBin data type:**
534    /// - TInfo1: Character width
535    /// - TInfo2: Character height
536    ///
537    /// # Example
538    ///
539    /// ```ignore
540    /// // Internal serialization example (ignored because `encode_into_header` is not public)
541    /// # use icy_sauce::{CharacterCapabilities, CharacterFormat};
542    /// # use icy_sauce::header::SauceHeader;
543    /// let caps = CharacterCapabilities::new(CharacterFormat::Ansi);
544    /// let mut header = SauceHeader::default();
545    /// caps.encode_into_header(&mut header)?;
546    /// # Ok::<(), Box<dyn std::error::Error>>(())
547    /// ```
548    pub(crate) fn encode_into_header(&self, header: &mut SauceHeader) -> crate::Result<()> {
549        match header.data_type {
550            SauceDataType::Character => {
551                header.file_type = self.format.to_sauce();
552
553                match self.format {
554                    CharacterFormat::Ascii
555                    | CharacterFormat::Ansi
556                    | CharacterFormat::AnsiMation => {
557                        // Formats that support ANSi flags
558                        header.t_info1 = self.columns;
559                        header.t_info2 = self.lines;
560                        header.t_info3 = 0;
561                        header.t_info4 = 0;
562
563                        // Build flags byte
564                        header.t_flags = if self.ice_colors {
565                            ANSI_FLAG_NON_BLINK_MODE
566                        } else {
567                            0
568                        };
569
570                        // Add letter spacing bits
571                        header.t_flags |= match self.letter_spacing {
572                            LetterSpacing::Legacy => ANSI_LETTER_SPACING_LEGACY,
573                            LetterSpacing::EightPixel => ANSI_LETTER_SPACING_8PX,
574                            LetterSpacing::NinePixel => ANSI_LETTER_SPACING_9PX,
575                            LetterSpacing::Reserved => ANSI_LETTER_SPACING_LEGACY, // fallback
576                        };
577
578                        // Add aspect ratio bits
579                        header.t_flags |= match self.aspect_ratio {
580                            AspectRatio::Legacy => ANSI_ASPECT_RATIO_LEGACY,
581                            AspectRatio::LegacyDevice => ANSI_ASPECT_RATIO_STRETCH,
582                            AspectRatio::Square => ANSI_ASPECT_RATIO_SQUARE,
583                            AspectRatio::Reserved => ANSI_ASPECT_RATIO_LEGACY, // fallback
584                        };
585
586                        if let Some(font) = &self.font_opt {
587                            if font.len() > limits::MAX_FONT_NAME_LENGTH {
588                                return Err(SauceError::FontNameTooLong(font.len()));
589                            }
590                            header.t_info_s.clone_from(font);
591                        } else {
592                            header.t_info_s.clear();
593                        }
594                    }
595
596                    CharacterFormat::RipScript => {
597                        // RipScript MUST have fixed pixel values per SAUCE spec
598                        header.t_info1 = 640; // Pixel width
599                        header.t_info2 = 350; // Pixel height
600                        header.t_info3 = 16; // Number of colors
601                        header.t_info4 = 0; // Must be 0
602                        header.t_flags = 0; // No flags
603                        header.t_info_s.clear(); // No font
604                    }
605
606                    CharacterFormat::PCBoard
607                    | CharacterFormat::Avatar
608                    | CharacterFormat::TundraDraw => {
609                        // These formats have dimensions but no flags
610                        header.t_info1 = self.columns;
611                        header.t_info2 = self.lines;
612                        header.t_info3 = 0;
613                        header.t_info4 = 0;
614                        header.t_flags = 0;
615                        header.t_info_s.clear();
616                    }
617
618                    CharacterFormat::Html | CharacterFormat::Source => {
619                        // HTML and Source have all zeros per spec
620                        header.t_info1 = 0;
621                        header.t_info2 = 0;
622                        header.t_info3 = 0;
623                        header.t_info4 = 0;
624                        header.t_flags = 0;
625                        header.t_info_s.clear();
626                    }
627
628                    CharacterFormat::Unknown(_) => {
629                        // For unknown types, try to preserve width/height
630                        header.t_info1 = self.columns;
631                        header.t_info2 = self.lines;
632                        header.t_info3 = 0;
633                        header.t_info4 = 0;
634                        header.t_flags = 0;
635                        header.t_info_s.clear();
636                    }
637                }
638            }
639            _ => {
640                return Err(SauceError::UnsupportedDataType(header.data_type));
641            }
642        }
643        Ok(())
644    }
645}
646
647impl TryFrom<&SauceHeader> for CharacterCapabilities {
648    type Error = SauceError;
649    fn try_from(header: &SauceHeader) -> crate::Result<Self> {
650        let format = CharacterFormat::from_sauce(header.file_type);
651        if header.data_type != SauceDataType::Character {
652            return Err(SauceError::UnsupportedDataType(header.data_type));
653        }
654
655        let columns;
656        let lines;
657        let mut ice_colors = false;
658        let mut letter_spacing = LetterSpacing::Legacy;
659        let mut aspect_ratio = AspectRatio::Legacy;
660        let mut font_opt = None;
661
662        if format.supports_ansi_flags() {
663            columns = header.t_info1;
664            lines = header.t_info2;
665            ice_colors = (header.t_flags & ANSI_FLAG_NON_BLINK_MODE) == ANSI_FLAG_NON_BLINK_MODE;
666            letter_spacing = match header.t_flags & ANSI_MASK_LETTER_SPACING {
667                ANSI_LETTER_SPACING_LEGACY => LetterSpacing::Legacy,
668                ANSI_LETTER_SPACING_8PX => LetterSpacing::EightPixel,
669                ANSI_LETTER_SPACING_9PX => LetterSpacing::NinePixel,
670                _ => LetterSpacing::Reserved,
671            };
672            aspect_ratio = match header.t_flags & ANSI_MASK_ASPECT_RATIO {
673                ANSI_ASPECT_RATIO_LEGACY => AspectRatio::Legacy,
674                ANSI_ASPECT_RATIO_STRETCH => AspectRatio::LegacyDevice,
675                ANSI_ASPECT_RATIO_SQUARE => AspectRatio::Square,
676                _ => AspectRatio::Reserved,
677            };
678            font_opt = if header.t_info_s.is_empty() {
679                None
680            } else {
681                Some(header.t_info_s.clone())
682            };
683        } else if format == CharacterFormat::RipScript {
684            // RipScript fixed logical dimensions
685            columns = 80;
686            lines = 25;
687        } else if format.has_dimensions() {
688            columns = header.t_info1;
689            lines = header.t_info2;
690        } else {
691            // Html / Source or unknown with no dimensions stay at 0
692            columns = 0;
693            lines = 0;
694        }
695
696        Ok(CharacterCapabilities {
697            format,
698            columns,
699            lines,
700            ice_colors,
701            letter_spacing,
702            aspect_ratio,
703            font_opt,
704        })
705    }
706}