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}