use crate::parser::{read_i16, read_u16, read_u32};
use crate::Error;
#[derive(Debug, Clone, Copy)]
pub struct Os2Table {
version: u16,
table_len: usize,
x_avg_char_width: i16,
us_weight_class: u16,
us_width_class: u16,
fs_type: u16,
y_subscript_x_size: i16,
y_subscript_y_size: i16,
y_subscript_x_offset: i16,
y_subscript_y_offset: i16,
y_superscript_x_size: i16,
y_superscript_y_size: i16,
y_superscript_x_offset: i16,
y_superscript_y_offset: i16,
y_strikeout_size: i16,
y_strikeout_position: i16,
s_family_class: i16,
panose: [u8; 10],
ul_unicode_range1: u32,
ul_unicode_range2: u32,
ul_unicode_range3: u32,
ul_unicode_range4: u32,
ach_vend_id: [u8; 4],
fs_selection: u16,
us_first_char_index: u16,
us_last_char_index: u16,
has_typo_metrics: bool,
s_typo_ascender: i16,
s_typo_descender: i16,
s_typo_line_gap: i16,
us_win_ascent: u16,
us_win_descent: u16,
has_code_page_range: bool,
ul_code_page_range1: u32,
ul_code_page_range2: u32,
has_v2_extension: bool,
sx_height: i16,
s_cap_height: i16,
us_default_char: u16,
us_break_char: u16,
us_max_context: u16,
has_optical_size: bool,
us_lower_optical_point_size: u16,
us_upper_optical_point_size: u16,
}
pub const FS_TYPE_USAGE_MASK: u16 = 0x000F;
pub const FS_TYPE_RESTRICTED_LICENSE: u16 = 0x0002;
pub const FS_TYPE_PREVIEW_AND_PRINT: u16 = 0x0004;
pub const FS_TYPE_EDITABLE: u16 = 0x0008;
pub const FS_TYPE_NO_SUBSETTING: u16 = 0x0100;
pub const FS_TYPE_BITMAP_EMBEDDING_ONLY: u16 = 0x0200;
pub const FS_SELECTION_ITALIC: u16 = 0x0001;
pub const FS_SELECTION_UNDERSCORE: u16 = 0x0002;
pub const FS_SELECTION_NEGATIVE: u16 = 0x0004;
pub const FS_SELECTION_OUTLINED: u16 = 0x0008;
pub const FS_SELECTION_STRIKEOUT: u16 = 0x0010;
pub const FS_SELECTION_BOLD: u16 = 0x0020;
pub const FS_SELECTION_REGULAR: u16 = 0x0040;
pub const FS_SELECTION_USE_TYPO_METRICS: u16 = 0x0080;
pub const FS_SELECTION_WWS: u16 = 0x0100;
pub const FS_SELECTION_OBLIQUE: u16 = 0x0200;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmbeddingPermission {
Installable,
RestrictedLicense,
PreviewAndPrint,
Editable,
Other(u8),
}
impl Os2Table {
pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < 68 {
return Err(Error::UnexpectedEof);
}
let version = read_u16(bytes, 0)?;
if version > 5 {
return Err(Error::BadStructure("OS/2 version above 5"));
}
let x_avg_char_width = read_i16(bytes, 2)?;
let us_weight_class = read_u16(bytes, 4)?;
let us_width_class = read_u16(bytes, 6)?;
let fs_type = read_u16(bytes, 8)?;
let y_subscript_x_size = read_i16(bytes, 10)?;
let y_subscript_y_size = read_i16(bytes, 12)?;
let y_subscript_x_offset = read_i16(bytes, 14)?;
let y_subscript_y_offset = read_i16(bytes, 16)?;
let y_superscript_x_size = read_i16(bytes, 18)?;
let y_superscript_y_size = read_i16(bytes, 20)?;
let y_superscript_x_offset = read_i16(bytes, 22)?;
let y_superscript_y_offset = read_i16(bytes, 24)?;
let y_strikeout_size = read_i16(bytes, 26)?;
let y_strikeout_position = read_i16(bytes, 28)?;
let s_family_class = read_i16(bytes, 30)?;
let mut panose = [0u8; 10];
panose.copy_from_slice(&bytes[32..42]);
let ul_unicode_range1 = read_u32(bytes, 42)?;
let ul_unicode_range2 = read_u32(bytes, 46)?;
let ul_unicode_range3 = read_u32(bytes, 50)?;
let ul_unicode_range4 = read_u32(bytes, 54)?;
let mut ach_vend_id = [0u8; 4];
ach_vend_id.copy_from_slice(&bytes[58..62]);
let fs_selection = read_u16(bytes, 62)?;
let us_first_char_index = read_u16(bytes, 64)?;
let us_last_char_index = read_u16(bytes, 66)?;
let has_typo_metrics;
let s_typo_ascender;
let s_typo_descender;
let s_typo_line_gap;
let us_win_ascent;
let us_win_descent;
if bytes.len() >= 78 {
has_typo_metrics = true;
s_typo_ascender = read_i16(bytes, 68)?;
s_typo_descender = read_i16(bytes, 70)?;
s_typo_line_gap = read_i16(bytes, 72)?;
us_win_ascent = read_u16(bytes, 74)?;
us_win_descent = read_u16(bytes, 76)?;
} else if version == 0 {
has_typo_metrics = false;
s_typo_ascender = 0;
s_typo_descender = 0;
s_typo_line_gap = 0;
us_win_ascent = 0;
us_win_descent = 0;
} else {
return Err(Error::BadStructure(
"OS/2 v1+ truncated before typo-metrics tail",
));
}
let has_code_page_range;
let ul_code_page_range1;
let ul_code_page_range2;
if bytes.len() >= 86 {
has_code_page_range = true;
ul_code_page_range1 = read_u32(bytes, 78)?;
ul_code_page_range2 = read_u32(bytes, 82)?;
} else if version <= 1 {
if version == 1 {
return Err(Error::BadStructure(
"OS/2 v1 truncated before ulCodePageRange",
));
}
has_code_page_range = false;
ul_code_page_range1 = 0;
ul_code_page_range2 = 0;
} else {
return Err(Error::BadStructure(
"OS/2 v2+ truncated before ulCodePageRange",
));
}
let has_v2_extension;
let sx_height;
let s_cap_height;
let us_default_char;
let us_break_char;
let us_max_context;
if bytes.len() >= 96 {
has_v2_extension = true;
sx_height = read_i16(bytes, 86)?;
s_cap_height = read_i16(bytes, 88)?;
us_default_char = read_u16(bytes, 90)?;
us_break_char = read_u16(bytes, 92)?;
us_max_context = read_u16(bytes, 94)?;
} else if version <= 1 {
has_v2_extension = false;
sx_height = 0;
s_cap_height = 0;
us_default_char = 0;
us_break_char = 0;
us_max_context = 0;
} else {
return Err(Error::BadStructure(
"OS/2 v2+ truncated before sxHeight/sCapHeight tail",
));
}
let has_optical_size;
let us_lower_optical_point_size;
let us_upper_optical_point_size;
if bytes.len() >= 100 {
has_optical_size = true;
us_lower_optical_point_size = read_u16(bytes, 96)?;
us_upper_optical_point_size = read_u16(bytes, 98)?;
} else if version <= 4 {
has_optical_size = false;
us_lower_optical_point_size = 0;
us_upper_optical_point_size = 0;
} else {
return Err(Error::BadStructure(
"OS/2 v5 truncated before usLowerOpticalPointSize",
));
}
Ok(Self {
version,
table_len: bytes.len(),
x_avg_char_width,
us_weight_class,
us_width_class,
fs_type,
y_subscript_x_size,
y_subscript_y_size,
y_subscript_x_offset,
y_subscript_y_offset,
y_superscript_x_size,
y_superscript_y_size,
y_superscript_x_offset,
y_superscript_y_offset,
y_strikeout_size,
y_strikeout_position,
s_family_class,
panose,
ul_unicode_range1,
ul_unicode_range2,
ul_unicode_range3,
ul_unicode_range4,
ach_vend_id,
fs_selection,
us_first_char_index,
us_last_char_index,
has_typo_metrics,
s_typo_ascender,
s_typo_descender,
s_typo_line_gap,
us_win_ascent,
us_win_descent,
has_code_page_range,
ul_code_page_range1,
ul_code_page_range2,
has_v2_extension,
sx_height,
s_cap_height,
us_default_char,
us_break_char,
us_max_context,
has_optical_size,
us_lower_optical_point_size,
us_upper_optical_point_size,
})
}
pub fn version(&self) -> u16 {
self.version
}
pub fn table_len(&self) -> usize {
self.table_len
}
pub fn x_avg_char_width(&self) -> i16 {
self.x_avg_char_width
}
pub fn weight_class(&self) -> u16 {
self.us_weight_class
}
pub fn width_class(&self) -> u16 {
self.us_width_class
}
pub fn width_class_percent(&self) -> f32 {
match self.us_width_class {
0 | 1 => 50.0,
2 => 62.5,
3 => 75.0,
4 => 87.5,
5 => 100.0,
6 => 112.5,
7 => 125.0,
8 => 150.0,
_ => 200.0,
}
}
pub fn fs_type(&self) -> u16 {
self.fs_type
}
pub fn embedding_permission(&self) -> EmbeddingPermission {
let usage = (self.fs_type & FS_TYPE_USAGE_MASK) as u8;
match usage {
0 => EmbeddingPermission::Installable,
2 => EmbeddingPermission::RestrictedLicense,
4 => EmbeddingPermission::PreviewAndPrint,
8 => EmbeddingPermission::Editable,
other => EmbeddingPermission::Other(other),
}
}
pub fn fs_type_no_subsetting(&self) -> bool {
self.fs_type & FS_TYPE_NO_SUBSETTING != 0
}
pub fn fs_type_bitmap_embedding_only(&self) -> bool {
self.fs_type & FS_TYPE_BITMAP_EMBEDDING_ONLY != 0
}
pub fn y_subscript_x_size(&self) -> i16 {
self.y_subscript_x_size
}
pub fn y_subscript_y_size(&self) -> i16 {
self.y_subscript_y_size
}
pub fn y_subscript_x_offset(&self) -> i16 {
self.y_subscript_x_offset
}
pub fn y_subscript_y_offset(&self) -> i16 {
self.y_subscript_y_offset
}
pub fn y_superscript_x_size(&self) -> i16 {
self.y_superscript_x_size
}
pub fn y_superscript_y_size(&self) -> i16 {
self.y_superscript_y_size
}
pub fn y_superscript_x_offset(&self) -> i16 {
self.y_superscript_x_offset
}
pub fn y_superscript_y_offset(&self) -> i16 {
self.y_superscript_y_offset
}
pub fn y_strikeout_size(&self) -> i16 {
self.y_strikeout_size
}
pub fn y_strikeout_position(&self) -> i16 {
self.y_strikeout_position
}
pub fn s_family_class(&self) -> i16 {
self.s_family_class
}
pub fn family_class_split(&self) -> (u8, u8) {
let raw = self.s_family_class as u16;
((raw >> 8) as u8, (raw & 0xFF) as u8)
}
pub fn panose(&self) -> &[u8; 10] {
&self.panose
}
pub fn ach_vend_id(&self) -> &[u8; 4] {
&self.ach_vend_id
}
pub fn ach_vend_id_str(&self) -> Option<&str> {
std::str::from_utf8(&self.ach_vend_id).ok()
}
pub fn unicode_range1(&self) -> u32 {
self.ul_unicode_range1
}
pub fn unicode_range2(&self) -> u32 {
self.ul_unicode_range2
}
pub fn unicode_range3(&self) -> u32 {
self.ul_unicode_range3
}
pub fn unicode_range4(&self) -> u32 {
self.ul_unicode_range4
}
pub fn has_unicode_range_bit(&self, bit: u8) -> bool {
let word = match bit / 32 {
0 => self.ul_unicode_range1,
1 => self.ul_unicode_range2,
2 => self.ul_unicode_range3,
3 => self.ul_unicode_range4,
_ => return false,
};
(word >> (bit % 32)) & 1 != 0
}
pub fn fs_selection(&self) -> u16 {
self.fs_selection
}
pub fn is_italic(&self) -> bool {
self.fs_selection & FS_SELECTION_ITALIC != 0
}
pub fn is_underscore(&self) -> bool {
self.fs_selection & FS_SELECTION_UNDERSCORE != 0
}
pub fn is_negative(&self) -> bool {
self.fs_selection & FS_SELECTION_NEGATIVE != 0
}
pub fn is_outlined(&self) -> bool {
self.fs_selection & FS_SELECTION_OUTLINED != 0
}
pub fn is_strikeout(&self) -> bool {
self.fs_selection & FS_SELECTION_STRIKEOUT != 0
}
pub fn is_bold(&self) -> bool {
self.fs_selection & FS_SELECTION_BOLD != 0
}
pub fn is_regular(&self) -> bool {
self.fs_selection & FS_SELECTION_REGULAR != 0
}
pub fn use_typo_metrics(&self) -> bool {
self.fs_selection & FS_SELECTION_USE_TYPO_METRICS != 0
}
pub fn is_wws(&self) -> bool {
self.fs_selection & FS_SELECTION_WWS != 0
}
pub fn is_oblique(&self) -> bool {
self.fs_selection & FS_SELECTION_OBLIQUE != 0
}
pub fn first_char_index(&self) -> u16 {
self.us_first_char_index
}
pub fn last_char_index(&self) -> u16 {
self.us_last_char_index
}
pub fn has_typo_metrics(&self) -> bool {
self.has_typo_metrics
}
pub fn typo_ascender(&self) -> Option<i16> {
self.has_typo_metrics.then_some(self.s_typo_ascender)
}
pub fn typo_descender(&self) -> Option<i16> {
self.has_typo_metrics.then_some(self.s_typo_descender)
}
pub fn typo_line_gap(&self) -> Option<i16> {
self.has_typo_metrics.then_some(self.s_typo_line_gap)
}
pub fn win_ascent(&self) -> Option<u16> {
self.has_typo_metrics.then_some(self.us_win_ascent)
}
pub fn win_descent(&self) -> Option<u16> {
self.has_typo_metrics.then_some(self.us_win_descent)
}
pub fn has_code_page_range(&self) -> bool {
self.has_code_page_range
}
pub fn code_page_range1(&self) -> Option<u32> {
self.has_code_page_range.then_some(self.ul_code_page_range1)
}
pub fn code_page_range2(&self) -> Option<u32> {
self.has_code_page_range.then_some(self.ul_code_page_range2)
}
pub fn has_code_page_bit(&self, bit: u8) -> bool {
if !self.has_code_page_range {
return false;
}
let word = match bit / 32 {
0 => self.ul_code_page_range1,
1 => self.ul_code_page_range2,
_ => return false,
};
(word >> (bit % 32)) & 1 != 0
}
pub fn has_v2_extension(&self) -> bool {
self.has_v2_extension
}
pub fn x_height(&self) -> Option<i16> {
self.has_v2_extension.then_some(self.sx_height)
}
pub fn cap_height(&self) -> Option<i16> {
self.has_v2_extension.then_some(self.s_cap_height)
}
pub fn default_char(&self) -> Option<u16> {
self.has_v2_extension.then_some(self.us_default_char)
}
pub fn break_char(&self) -> Option<u16> {
self.has_v2_extension.then_some(self.us_break_char)
}
pub fn max_context(&self) -> Option<u16> {
self.has_v2_extension.then_some(self.us_max_context)
}
pub fn has_optical_size(&self) -> bool {
self.has_optical_size
}
pub fn lower_optical_point_size_twips(&self) -> Option<u16> {
self.has_optical_size
.then_some(self.us_lower_optical_point_size)
}
pub fn upper_optical_point_size_twips(&self) -> Option<u16> {
self.has_optical_size
.then_some(self.us_upper_optical_point_size)
}
pub fn lower_optical_point_size_pt(&self) -> Option<f32> {
self.lower_optical_point_size_twips()
.map(|t| t as f32 / 20.0)
}
pub fn upper_optical_point_size_pt(&self) -> Option<f32> {
self.upper_optical_point_size_twips()
.map(|t| t as f32 / 20.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_v5_trace() -> Vec<u8> {
let mut t = vec![0u8; 100];
t[0..2].copy_from_slice(&5u16.to_be_bytes());
t[2..4].copy_from_slice(&519i16.to_be_bytes());
t[4..6].copy_from_slice(&400u16.to_be_bytes());
t[6..8].copy_from_slice(&5u16.to_be_bytes());
t[8..10].copy_from_slice(&0x0108u16.to_be_bytes());
t[10..12].copy_from_slice(&650i16.to_be_bytes());
t[12..14].copy_from_slice(&600i16.to_be_bytes());
t[14..16].copy_from_slice(&0i16.to_be_bytes());
t[16..18].copy_from_slice(&75i16.to_be_bytes());
t[18..20].copy_from_slice(&650i16.to_be_bytes());
t[20..22].copy_from_slice(&600i16.to_be_bytes());
t[22..24].copy_from_slice(&0i16.to_be_bytes());
t[24..26].copy_from_slice(&350i16.to_be_bytes());
t[26..28].copy_from_slice(&50i16.to_be_bytes());
t[28..30].copy_from_slice(&291i16.to_be_bytes());
t[30..32].copy_from_slice(&0x0802i16.to_be_bytes());
t[32..42].copy_from_slice(&[2, 11, 5, 3, 3, 4, 3, 2, 2, 4]);
t[42..46].copy_from_slice(&0xE000_02FFu32.to_be_bytes());
t[46..50].copy_from_slice(&0x0000_2003u32.to_be_bytes());
t[50..54].copy_from_slice(&0x0000_0000u32.to_be_bytes());
t[54..58].copy_from_slice(&0x0000_0000u32.to_be_bytes());
t[58..62].copy_from_slice(b"ADBO");
t[62..64].copy_from_slice(&0x01C0u16.to_be_bytes());
t[64..66].copy_from_slice(&0x0020u16.to_be_bytes());
t[66..68].copy_from_slice(&0xFFFFu16.to_be_bytes());
t[68..70].copy_from_slice(&1000i16.to_be_bytes());
t[70..72].copy_from_slice(&(-326i16).to_be_bytes());
t[72..74].copy_from_slice(&0i16.to_be_bytes());
t[74..76].copy_from_slice(&1000u16.to_be_bytes());
t[76..78].copy_from_slice(&326u16.to_be_bytes());
t[78..82].copy_from_slice(&0x2000_019Fu32.to_be_bytes());
t[82..86].copy_from_slice(&0u32.to_be_bytes());
t[86..88].copy_from_slice(&486i16.to_be_bytes());
t[88..90].copy_from_slice(&660i16.to_be_bytes());
t[90..92].copy_from_slice(&0u16.to_be_bytes());
t[92..94].copy_from_slice(&0x0020u16.to_be_bytes());
t[94..96].copy_from_slice(&5u16.to_be_bytes());
t[96..98].copy_from_slice(&120u16.to_be_bytes());
t[98..100].copy_from_slice(&240u16.to_be_bytes());
t
}
#[test]
fn parses_v5_full_layout() {
let bytes = build_v5_trace();
let os2 = Os2Table::parse(&bytes).expect("parse");
assert_eq!(os2.version(), 5);
assert_eq!(os2.table_len(), 100);
assert_eq!(os2.x_avg_char_width(), 519);
assert_eq!(os2.weight_class(), 400);
assert_eq!(os2.width_class(), 5);
assert!((os2.width_class_percent() - 100.0).abs() < 1e-6);
assert_eq!(os2.fs_type(), 0x0108);
assert_eq!(os2.embedding_permission(), EmbeddingPermission::Editable);
assert!(os2.fs_type_no_subsetting());
assert!(!os2.fs_type_bitmap_embedding_only());
assert_eq!(os2.y_subscript_x_size(), 650);
assert_eq!(os2.y_subscript_y_size(), 600);
assert_eq!(os2.y_subscript_y_offset(), 75);
assert_eq!(os2.y_superscript_y_offset(), 350);
assert_eq!(os2.y_strikeout_size(), 50);
assert_eq!(os2.y_strikeout_position(), 291);
assert_eq!(os2.family_class_split(), (8, 2));
assert_eq!(os2.panose(), &[2, 11, 5, 3, 3, 4, 3, 2, 2, 4]);
assert_eq!(os2.unicode_range1(), 0xE000_02FF);
assert!(os2.has_unicode_range_bit(0)); assert!(os2.has_unicode_range_bit(31)); assert!(os2.has_unicode_range_bit(45)); assert!(!os2.has_unicode_range_bit(127));
assert!(!os2.has_unicode_range_bit(200));
assert_eq!(os2.ach_vend_id(), b"ADBO");
assert_eq!(os2.ach_vend_id_str(), Some("ADBO"));
assert_eq!(os2.fs_selection(), 0x01C0);
assert!(!os2.is_italic());
assert!(!os2.is_bold());
assert!(os2.is_regular());
assert!(os2.use_typo_metrics());
assert!(os2.is_wws());
assert!(!os2.is_oblique());
assert_eq!(os2.first_char_index(), 0x0020);
assert_eq!(os2.last_char_index(), 0xFFFF);
assert!(os2.has_typo_metrics());
assert_eq!(os2.typo_ascender(), Some(1000));
assert_eq!(os2.typo_descender(), Some(-326));
assert_eq!(os2.typo_line_gap(), Some(0));
assert_eq!(os2.win_ascent(), Some(1000));
assert_eq!(os2.win_descent(), Some(326));
assert!(os2.has_code_page_range());
assert_eq!(os2.code_page_range1(), Some(0x2000_019F));
assert_eq!(os2.code_page_range2(), Some(0));
assert!(os2.has_code_page_bit(0));
assert!(!os2.has_code_page_bit(63));
assert!(!os2.has_code_page_bit(64));
assert!(os2.has_v2_extension());
assert_eq!(os2.x_height(), Some(486));
assert_eq!(os2.cap_height(), Some(660));
assert_eq!(os2.default_char(), Some(0));
assert_eq!(os2.break_char(), Some(0x0020));
assert_eq!(os2.max_context(), Some(5));
assert!(os2.has_optical_size());
assert_eq!(os2.lower_optical_point_size_twips(), Some(120));
assert_eq!(os2.upper_optical_point_size_twips(), Some(240));
assert!((os2.lower_optical_point_size_pt().unwrap() - 6.0).abs() < 1e-6);
assert!((os2.upper_optical_point_size_pt().unwrap() - 12.0).abs() < 1e-6);
}
#[test]
fn parses_v4_layout_drops_optical_size() {
let mut bytes = build_v5_trace();
bytes[0..2].copy_from_slice(&4u16.to_be_bytes());
bytes.truncate(96);
let os2 = Os2Table::parse(&bytes).expect("parse");
assert_eq!(os2.version(), 4);
assert_eq!(os2.table_len(), 96);
assert!(os2.has_v2_extension());
assert!(!os2.has_optical_size());
assert_eq!(os2.lower_optical_point_size_twips(), None);
assert_eq!(os2.upper_optical_point_size_twips(), None);
assert_eq!(os2.x_height(), Some(486));
assert_eq!(os2.max_context(), Some(5));
}
#[test]
fn parses_v1_layout_drops_v2_extension() {
let mut bytes = build_v5_trace();
bytes[0..2].copy_from_slice(&1u16.to_be_bytes());
bytes.truncate(86);
let os2 = Os2Table::parse(&bytes).expect("parse");
assert_eq!(os2.version(), 1);
assert_eq!(os2.table_len(), 86);
assert!(os2.has_typo_metrics());
assert!(os2.has_code_page_range());
assert!(!os2.has_v2_extension());
assert!(!os2.has_optical_size());
assert_eq!(os2.x_height(), None);
assert_eq!(os2.cap_height(), None);
assert_eq!(os2.code_page_range1(), Some(0x2000_019F));
}
#[test]
fn parses_v0_full_layout_drops_code_page_range() {
let mut bytes = build_v5_trace();
bytes[0..2].copy_from_slice(&0u16.to_be_bytes());
bytes.truncate(78);
let os2 = Os2Table::parse(&bytes).expect("parse");
assert_eq!(os2.version(), 0);
assert_eq!(os2.table_len(), 78);
assert!(os2.has_typo_metrics());
assert!(!os2.has_code_page_range());
assert_eq!(os2.code_page_range1(), None);
assert_eq!(os2.typo_ascender(), Some(1000));
}
#[test]
fn parses_v0_short_layout_drops_typo_metrics() {
let mut bytes = build_v5_trace();
bytes[0..2].copy_from_slice(&0u16.to_be_bytes());
bytes.truncate(68);
let os2 = Os2Table::parse(&bytes).expect("parse");
assert_eq!(os2.version(), 0);
assert_eq!(os2.table_len(), 68);
assert!(!os2.has_typo_metrics());
assert!(!os2.has_code_page_range());
assert!(!os2.has_v2_extension());
assert_eq!(os2.typo_ascender(), None);
assert_eq!(os2.win_ascent(), None);
assert_eq!(os2.weight_class(), 400);
assert_eq!(os2.first_char_index(), 0x0020);
}
#[test]
fn rejects_truncated_under_68_bytes() {
let bytes = vec![0u8; 67];
assert!(matches!(Os2Table::parse(&bytes), Err(Error::UnexpectedEof)));
}
#[test]
fn rejects_version_above_5() {
let mut bytes = build_v5_trace();
bytes[0..2].copy_from_slice(&6u16.to_be_bytes());
assert!(matches!(
Os2Table::parse(&bytes),
Err(Error::BadStructure("OS/2 version above 5"))
));
}
#[test]
fn rejects_v1_table_truncated_before_typo_block() {
let mut bytes = build_v5_trace();
bytes[0..2].copy_from_slice(&1u16.to_be_bytes());
bytes.truncate(68);
assert!(matches!(
Os2Table::parse(&bytes),
Err(Error::BadStructure(_))
));
}
#[test]
fn rejects_v1_table_truncated_before_code_page_range() {
let mut bytes = build_v5_trace();
bytes[0..2].copy_from_slice(&1u16.to_be_bytes());
bytes.truncate(78);
assert!(matches!(
Os2Table::parse(&bytes),
Err(Error::BadStructure(_))
));
}
#[test]
fn rejects_v2_truncated_before_extension() {
let mut bytes = build_v5_trace();
bytes[0..2].copy_from_slice(&2u16.to_be_bytes());
bytes.truncate(86);
assert!(matches!(
Os2Table::parse(&bytes),
Err(Error::BadStructure(_))
));
}
#[test]
fn rejects_v5_truncated_before_optical_size() {
let mut bytes = build_v5_trace();
bytes.truncate(96);
assert!(matches!(
Os2Table::parse(&bytes),
Err(Error::BadStructure(_))
));
}
#[test]
fn width_class_percent_spec_table() {
let mut bytes = build_v5_trace();
for (wd, pct) in &[
(1u16, 50.0_f32),
(2, 62.5),
(3, 75.0),
(4, 87.5),
(5, 100.0),
(6, 112.5),
(7, 125.0),
(8, 150.0),
(9, 200.0),
] {
bytes[6..8].copy_from_slice(&wd.to_be_bytes());
let os2 = Os2Table::parse(&bytes).expect("parse");
assert!(
(os2.width_class_percent() - pct).abs() < 1e-6,
"wd={wd} → {} expected {pct}",
os2.width_class_percent(),
);
}
}
#[test]
fn embedding_permission_decodes_each_named_value() {
let mut bytes = build_v5_trace();
for (raw, expect) in &[
(0u16, EmbeddingPermission::Installable),
(2, EmbeddingPermission::RestrictedLicense),
(4, EmbeddingPermission::PreviewAndPrint),
(8, EmbeddingPermission::Editable),
] {
bytes[8..10].copy_from_slice(&raw.to_be_bytes());
let os2 = Os2Table::parse(&bytes).expect("parse");
assert_eq!(os2.embedding_permission(), *expect, "raw={raw}");
}
bytes[8..10].copy_from_slice(&0x000Cu16.to_be_bytes());
let os2 = Os2Table::parse(&bytes).expect("parse");
assert_eq!(os2.embedding_permission(), EmbeddingPermission::Other(12));
bytes[8..10].copy_from_slice(&0x0001u16.to_be_bytes());
let os2 = Os2Table::parse(&bytes).expect("parse");
assert_eq!(os2.embedding_permission(), EmbeddingPermission::Other(1));
}
#[test]
fn fs_selection_bit_helpers_match_constants() {
let mut bytes = build_v5_trace();
bytes[62..64].copy_from_slice(&0x03FFu16.to_be_bytes()); let os2 = Os2Table::parse(&bytes).expect("parse");
assert!(os2.is_italic());
assert!(os2.is_underscore());
assert!(os2.is_negative());
assert!(os2.is_outlined());
assert!(os2.is_strikeout());
assert!(os2.is_bold());
assert!(os2.is_regular());
assert!(os2.use_typo_metrics());
assert!(os2.is_wws());
assert!(os2.is_oblique());
bytes[62..64].copy_from_slice(&0u16.to_be_bytes());
let os2 = Os2Table::parse(&bytes).expect("parse");
assert!(!os2.is_italic());
assert!(!os2.use_typo_metrics());
}
#[test]
fn ach_vend_id_str_handles_non_ascii() {
let mut bytes = build_v5_trace();
bytes[58..62].copy_from_slice(&[0xFFu8, 0xFE, 0xFD, 0xFC]);
let os2 = Os2Table::parse(&bytes).expect("parse");
assert_eq!(os2.ach_vend_id(), &[0xFF, 0xFE, 0xFD, 0xFC]);
assert!(os2.ach_vend_id_str().is_none());
}
#[test]
fn has_unicode_range_bit_walks_every_word() {
let mut bytes = build_v5_trace();
bytes[54..58].copy_from_slice(&0x0000_0001u32.to_be_bytes());
let os2 = Os2Table::parse(&bytes).expect("parse");
assert!(os2.has_unicode_range_bit(96));
assert!(!os2.has_unicode_range_bit(97));
}
#[test]
fn family_class_split_round_trip() {
let mut bytes = build_v5_trace();
bytes[30..32].copy_from_slice(&0x0703i16.to_be_bytes());
let os2 = Os2Table::parse(&bytes).expect("parse");
assert_eq!(os2.family_class_split(), (7, 3));
}
#[test]
fn optical_size_pt_conversion_inverts_twips() {
let mut bytes = build_v5_trace();
bytes[96..98].copy_from_slice(&180u16.to_be_bytes());
bytes[98..100].copy_from_slice(&288u16.to_be_bytes());
let os2 = Os2Table::parse(&bytes).expect("parse");
assert!((os2.lower_optical_point_size_pt().unwrap() - 9.0).abs() < 1e-6);
assert!((os2.upper_optical_point_size_pt().unwrap() - 14.4).abs() < 1e-6);
}
}