use xlsbye_core::error::Result;
use xlsbye_core::types::{
Alignment, Border, BorderSide, BorderStyle, Color, ColorType, DxfStyle, Fill, Font,
FontScheme, GradientFill, GradientStop, GradientType, HorizontalAlign, NumFmt, PatternFill,
PatternType, Protection, Stylesheet, SuperSub, UnderlineStyle, VerticalAlign, Xf,
};
use crate::record::cursor::RecordCursor;
use crate::record::header::RecordIter;
use crate::record::ids::{
BRT_BEGIN_BORDERS, BRT_BEGIN_CELL_STYLE_XFS, BRT_BEGIN_CELL_XFS, BRT_BEGIN_DXFS,
BRT_BEGIN_FILLS, BRT_BEGIN_FMTS, BRT_BEGIN_FONTS, BRT_BORDER, BRT_END_BORDERS,
BRT_END_CELL_STYLE_XFS, BRT_END_CELL_XFS, BRT_END_DXFS, BRT_END_FILLS, BRT_END_FMTS,
BRT_END_FONTS, BRT_FILL, BRT_FMT, BRT_FONT, BRT_XF,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Section {
Fmts,
Fonts,
Fills,
Borders,
CellStyleXfs,
CellXfs,
Dxfs,
}
pub fn parse_styles(data: &[u8]) -> Result<Stylesheet> {
let mut stylesheet = Stylesheet {
num_fmts: Vec::new(),
fonts: Vec::new(),
fills: Vec::new(),
borders: Vec::new(),
cell_xfs: Vec::new(),
cell_style_xfs: Vec::new(),
dxfs: Vec::new(),
};
let mut section = None;
for record in RecordIter::new(data) {
let (record_type, payload) = record?;
match record_type {
t if t == BRT_BEGIN_FMTS.as_u16() => {
section = Some(Section::Fmts);
continue;
}
t if t == BRT_END_FMTS.as_u16() => {
section = None;
continue;
}
t if t == BRT_BEGIN_FONTS.as_u16() => {
section = Some(Section::Fonts);
continue;
}
t if t == BRT_END_FONTS.as_u16() => {
section = None;
continue;
}
t if t == BRT_BEGIN_FILLS.as_u16() => {
section = Some(Section::Fills);
continue;
}
t if t == BRT_END_FILLS.as_u16() => {
section = None;
continue;
}
t if t == BRT_BEGIN_BORDERS.as_u16() => {
section = Some(Section::Borders);
continue;
}
t if t == BRT_END_BORDERS.as_u16() => {
section = None;
continue;
}
t if t == BRT_BEGIN_CELL_STYLE_XFS.as_u16() => {
section = Some(Section::CellStyleXfs);
continue;
}
t if t == BRT_END_CELL_STYLE_XFS.as_u16() => {
section = None;
continue;
}
t if t == BRT_BEGIN_CELL_XFS.as_u16() => {
section = Some(Section::CellXfs);
continue;
}
t if t == BRT_END_CELL_XFS.as_u16() => {
section = None;
continue;
}
t if t == BRT_BEGIN_DXFS.as_u16() => {
section = Some(Section::Dxfs);
continue;
}
t if t == BRT_END_DXFS.as_u16() => {
section = None;
continue;
}
_ => {}
}
match section {
Some(Section::Fmts) if record_type == BRT_FMT.as_u16() => {
stylesheet.num_fmts.push(parse_num_fmt(payload)?);
}
Some(Section::Fonts) if record_type == BRT_FONT.as_u16() => {
stylesheet.fonts.push(parse_font(payload)?);
}
Some(Section::Fills) if record_type == BRT_FILL.as_u16() => {
stylesheet.fills.push(parse_fill(payload)?);
}
Some(Section::Borders) if record_type == BRT_BORDER.as_u16() => {
stylesheet.borders.push(parse_border(payload)?);
}
Some(Section::CellStyleXfs) if record_type == BRT_XF.as_u16() => {
stylesheet.cell_style_xfs.push(parse_xf(payload)?);
}
Some(Section::CellXfs) if record_type == BRT_XF.as_u16() => {
stylesheet.cell_xfs.push(parse_xf(payload)?);
}
Some(Section::Dxfs) => {
stylesheet.dxfs.push(DxfStyle {
font: None,
fill: None,
border: None,
num_fmt: None,
alignment: None,
});
}
_ => {}
}
}
Ok(stylesheet)
}
fn parse_num_fmt(payload: &[u8]) -> Result<NumFmt> {
let mut cur = RecordCursor::new(payload);
let id = cur.read_u16()?;
let format_code = cur.read_wide_string()?;
Ok(NumFmt { id, format_code })
}
fn parse_font(payload: &[u8]) -> Result<Font> {
let mut cur = RecordCursor::new(payload);
let size_twips = cur.read_u16()?;
let grbit = cur.read_u16()?;
let bls = cur.read_u16()?;
let supersub = cur.read_u16()?;
let underline = cur.read_u8()?;
let family = cur.read_u8()?;
let charset = cur.read_u8()?;
let _unused = cur.read_u8()?;
let color = parse_color(&mut cur)?;
let scheme = cur.read_u8()?;
let name = cur.read_wide_string()?;
let bold_from_weight = bls >= 700;
let bold_from_flag = grbit & 0x0001 != 0;
let italic = grbit & 0x0002 != 0;
let strikethrough = grbit & 0x0008 != 0;
Ok(Font {
size_twips,
bold: bold_from_weight || bold_from_flag,
italic,
underline: match underline {
1 => UnderlineStyle::Single,
2 => UnderlineStyle::Double,
0x21 => UnderlineStyle::SingleAccounting,
0x22 => UnderlineStyle::DoubleAccounting,
_ => UnderlineStyle::None,
},
strikethrough,
color,
name,
family,
charset,
scheme: match scheme {
1 => FontScheme::Major,
2 => FontScheme::Minor,
_ => FontScheme::None,
},
superscript: match supersub {
1 => SuperSub::Superscript,
2 => SuperSub::Subscript,
_ => SuperSub::None,
},
})
}
fn parse_fill(payload: &[u8]) -> Result<Fill> {
let mut cur = RecordCursor::new(payload);
let fls = cur.read_u32()?;
let fg_color = parse_color(&mut cur)?;
let bg_color = parse_color(&mut cur)?;
let pattern = match fls {
0 => PatternFill::None,
1 => PatternFill::Solid { fg_color, bg_color },
2..=18 => PatternFill::Pattern {
pattern_type: map_pattern_type(fls),
fg_color,
bg_color,
},
40 => PatternFill::Gradient(parse_gradient_fill(&mut cur)?),
_ => PatternFill::None,
};
Ok(Fill { pattern })
}
fn parse_gradient_fill(cur: &mut RecordCursor<'_>) -> Result<GradientFill> {
let gradient_type = match cur.read_u32()? {
1 => GradientType::Path,
_ => GradientType::Linear,
};
let degree = cur.read_f64()?;
let mut left = 0.0;
let mut right = 0.0;
let mut top = 0.0;
let mut bottom = 0.0;
if cur.remaining() >= 32 {
left = cur.read_f64()?;
right = cur.read_f64()?;
top = cur.read_f64()?;
bottom = cur.read_f64()?;
}
let stop_count = if cur.remaining() >= 4 {
cur.read_u32()? as usize
} else {
0
};
let mut stops = Vec::with_capacity(stop_count);
for _ in 0..stop_count {
let position = cur.read_f64()?;
let color = parse_color(cur)?;
stops.push(GradientStop { position, color });
}
Ok(GradientFill {
gradient_type,
degree,
left,
right,
top,
bottom,
stops,
})
}
fn parse_border(payload: &[u8]) -> Result<Border> {
let mut cur = RecordCursor::new(payload);
let flags = cur.read_u8()?;
let top = parse_border_side(&mut cur)?;
let bottom = parse_border_side(&mut cur)?;
let left = parse_border_side(&mut cur)?;
let right = parse_border_side(&mut cur)?;
let diagonal = parse_border_side(&mut cur)?;
Ok(Border {
left,
right,
top,
bottom,
diagonal,
diagonal_down: flags & 0x01 != 0,
diagonal_up: flags & 0x02 != 0,
})
}
fn parse_border_side(cur: &mut RecordCursor<'_>) -> Result<BorderSide> {
let style = map_border_style(cur.read_u8()?);
cur.skip(1)?;
let color = parse_color(cur)?;
Ok(BorderSide { style, color })
}
fn parse_xf(payload: &[u8]) -> Result<Xf> {
let mut cur = RecordCursor::new(payload);
let ixfe_parent = cur.read_u16()?;
let num_fmt_id = cur.read_u16()?;
let font_id = cur.read_u16()?;
let fill_id = cur.read_u16()?;
let border_id = cur.read_u16()?;
let text_rotation = u16::from(cur.read_u8()?);
let indent = cur.read_u8()?;
let align_flags = if cur.remaining() >= 1 { cur.read_u8()? } else { 0 };
let protection_flags = if cur.remaining() >= 1 { cur.read_u8()? } else { 0 };
let apply_flags = if cur.remaining() >= 1 { cur.read_u8()? } else { 0 };
let _reading_order = if cur.remaining() >= 1 { cur.read_u8()? } else { 0 };
let horizontal = match align_flags & 0x07 {
1 => HorizontalAlign::Left,
2 => HorizontalAlign::Center,
3 => HorizontalAlign::Right,
4 => HorizontalAlign::Fill,
5 => HorizontalAlign::Justify,
6 => HorizontalAlign::CenterContinuous,
7 => HorizontalAlign::Distributed,
_ => HorizontalAlign::General,
};
let vertical = match (align_flags >> 3) & 0x07 {
0 => VerticalAlign::Top,
1 => VerticalAlign::Center,
3 => VerticalAlign::Justify,
4 => VerticalAlign::Distributed,
_ => VerticalAlign::Bottom,
};
Ok(Xf {
num_fmt_id,
font_id,
fill_id,
border_id,
xf_id: if ixfe_parent == u16::MAX {
None
} else {
Some(u32::from(ixfe_parent))
},
alignment: Alignment {
horizontal,
vertical,
wrap_text: align_flags & 0x40 != 0,
shrink_to_fit: protection_flags & 0x01 != 0,
text_rotation,
indent,
reading_order: (protection_flags >> 2) & 0x03,
},
protection: Protection {
locked: protection_flags & 0x10 != 0,
hidden: protection_flags & 0x20 != 0,
},
apply_number_format: apply_flags & 0x01 != 0,
apply_font: apply_flags & 0x02 != 0,
apply_alignment: apply_flags & 0x04 != 0,
apply_border: apply_flags & 0x08 != 0,
apply_fill: apply_flags & 0x10 != 0,
apply_protection: apply_flags & 0x20 != 0,
})
}
fn parse_color(cur: &mut RecordCursor<'_>) -> Result<Color> {
let flags_byte = cur.read_u8()?;
let x_color_type = flags_byte >> 1;
let index = cur.read_u8()?;
let tint_fixed = cur.read_u16()? as i16;
let rgba = cur.read_u32()?.to_le_bytes();
let color_type = match x_color_type {
1 => ColorType::Indexed(u32::from(index)),
2 => ColorType::Rgb(rgba[3], rgba[0], rgba[1], rgba[2]),
3 => {
let tint = f64::from(tint_fixed) / 32767.0;
ColorType::Theme {
theme: u32::from(index),
tint: if tint.abs() < 1e-10 { 0.0 } else { tint },
}
}
_ => ColorType::Auto,
};
Ok(Color { color_type })
}
fn map_pattern_type(fls: u32) -> PatternType {
match fls {
2 => PatternType::MediumGray,
3 => PatternType::DarkGray,
4 => PatternType::LightGray,
5 => PatternType::DarkHorizontal,
6 => PatternType::DarkVertical,
7 => PatternType::DarkDown,
8 => PatternType::DarkUp,
9 => PatternType::DarkGrid,
10 => PatternType::DarkTrellis,
11 => PatternType::LightHorizontal,
12 => PatternType::LightVertical,
13 => PatternType::LightDown,
14 => PatternType::LightUp,
15 => PatternType::LightGrid,
16 => PatternType::LightTrellis,
17 => PatternType::Gray125,
_ => PatternType::Gray0625,
}
}
fn map_border_style(style: u8) -> BorderStyle {
match style {
1 => BorderStyle::Thin,
2 => BorderStyle::Medium,
3 => BorderStyle::Dashed,
4 => BorderStyle::Dotted,
5 => BorderStyle::Thick,
6 => BorderStyle::Double,
7 => BorderStyle::Hair,
8 => BorderStyle::MediumDashed,
9 => BorderStyle::DashDot,
10 => BorderStyle::MediumDashDot,
11 => BorderStyle::DashDotDot,
12 => BorderStyle::MediumDashDotDot,
13 => BorderStyle::SlantDashDot,
_ => BorderStyle::None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::ids::{
BRT_BEGIN_BORDERS, BRT_BEGIN_CELL_STYLE_XFS, BRT_BEGIN_CELL_XFS, BRT_BEGIN_FILLS,
BRT_BEGIN_FMTS, BRT_BEGIN_FONTS, BRT_BORDER, BRT_END_BORDERS, BRT_END_CELL_STYLE_XFS,
BRT_END_CELL_XFS, BRT_END_FILLS, BRT_END_FMTS, BRT_END_FONTS, BRT_FILL, BRT_FMT,
BRT_FONT, BRT_XF,
};
fn encode_varint(mut value: u32) -> Vec<u8> {
let mut out = Vec::new();
loop {
let mut b = (value & 0x7F) as u8;
value >>= 7;
if value != 0 {
b |= 0x80;
}
out.push(b);
if value == 0 {
break;
}
}
out
}
fn push_record(stream: &mut Vec<u8>, record_type: u16, payload: &[u8]) {
stream.extend_from_slice(&encode_varint(u32::from(record_type)));
stream.extend_from_slice(&encode_varint(payload.len() as u32));
stream.extend_from_slice(payload);
}
fn encode_wide_string(value: &str) -> Vec<u8> {
let mut out = Vec::new();
let utf16: Vec<u16> = value.encode_utf16().collect();
out.extend_from_slice(&(utf16.len() as u32).to_le_bytes());
for ch in utf16 {
out.extend_from_slice(&ch.to_le_bytes());
}
out
}
fn encode_color(color_type: u8, index: u8, tint: i16, rgba: [u8; 4]) -> Vec<u8> {
let mut out = Vec::new();
out.push(color_type << 1 | 1);
out.push(index);
out.extend_from_slice(&tint.to_le_bytes());
out.extend_from_slice(&u32::from_le_bytes(rgba).to_le_bytes());
out
}
#[test]
fn parses_number_formats() {
let mut data = Vec::new();
push_record(&mut data, BRT_BEGIN_FMTS.as_u16(), &[]);
let mut fmt = Vec::new();
fmt.extend_from_slice(&164u16.to_le_bytes());
fmt.extend_from_slice(&encode_wide_string("yyyy-mm-dd"));
push_record(&mut data, BRT_FMT.as_u16(), &fmt);
push_record(&mut data, BRT_END_FMTS.as_u16(), &[]);
let stylesheet = parse_styles(&data).expect("parse styles");
assert_eq!(stylesheet.num_fmts.len(), 1);
assert_eq!(stylesheet.num_fmts[0].id, 164);
assert_eq!(stylesheet.num_fmts[0].format_code, "yyyy-mm-dd");
}
#[test]
fn parses_fonts() {
let mut data = Vec::new();
push_record(&mut data, BRT_BEGIN_FONTS.as_u16(), &[]);
let mut font = Vec::new();
font.extend_from_slice(&220u16.to_le_bytes());
font.extend_from_slice(&0b0011u16.to_le_bytes());
font.extend_from_slice(&700u16.to_le_bytes());
font.extend_from_slice(&1u16.to_le_bytes());
font.push(1);
font.push(2);
font.push(1);
font.push(0);
font.extend_from_slice(&encode_color(2, 0, 0, [0x11, 0x22, 0x33, 0xFF]));
font.push(2);
font.extend_from_slice(&encode_wide_string("Calibri"));
push_record(&mut data, BRT_FONT.as_u16(), &font);
push_record(&mut data, BRT_END_FONTS.as_u16(), &[]);
let stylesheet = parse_styles(&data).expect("parse styles");
assert_eq!(stylesheet.fonts.len(), 1);
let parsed = &stylesheet.fonts[0];
assert_eq!(parsed.size_twips, 220);
assert!(parsed.bold);
assert!(parsed.italic);
assert_eq!(parsed.underline, UnderlineStyle::Single);
assert_eq!(parsed.superscript, SuperSub::Superscript);
assert_eq!(parsed.scheme, FontScheme::Minor);
assert_eq!(parsed.color.color_type, ColorType::Rgb(0xFF, 0x11, 0x22, 0x33));
}
#[test]
fn parses_fills_solid_pattern_gradient() {
let mut data = Vec::new();
push_record(&mut data, BRT_BEGIN_FILLS.as_u16(), &[]);
let mut solid = Vec::new();
solid.extend_from_slice(&1u32.to_le_bytes());
solid.extend_from_slice(&encode_color(2, 0, 0, [0xAA, 0xBB, 0xCC, 0xFF]));
solid.extend_from_slice(&encode_color(1, 64, 0, [0, 0, 0, 0]));
push_record(&mut data, BRT_FILL.as_u16(), &solid);
let mut pattern = Vec::new();
pattern.extend_from_slice(&17u32.to_le_bytes());
pattern.extend_from_slice(&encode_color(3, 2, 8192, [0, 0, 0, 0]));
pattern.extend_from_slice(&encode_color(0, 0, 0, [0, 0, 0, 0]));
push_record(&mut data, BRT_FILL.as_u16(), &pattern);
let mut gradient = Vec::new();
gradient.extend_from_slice(&40u32.to_le_bytes());
gradient.extend_from_slice(&encode_color(0, 0, 0, [0, 0, 0, 0]));
gradient.extend_from_slice(&encode_color(0, 0, 0, [0, 0, 0, 0]));
gradient.extend_from_slice(&0u32.to_le_bytes());
gradient.extend_from_slice(&45.0f64.to_le_bytes());
gradient.extend_from_slice(&0.1f64.to_le_bytes());
gradient.extend_from_slice(&0.2f64.to_le_bytes());
gradient.extend_from_slice(&0.3f64.to_le_bytes());
gradient.extend_from_slice(&0.4f64.to_le_bytes());
gradient.extend_from_slice(&2u32.to_le_bytes());
gradient.extend_from_slice(&0.0f64.to_le_bytes());
gradient.extend_from_slice(&encode_color(2, 0, 0, [0x00, 0x00, 0x00, 0xFF]));
gradient.extend_from_slice(&1.0f64.to_le_bytes());
gradient.extend_from_slice(&encode_color(2, 0, 0, [0xFF, 0xFF, 0xFF, 0xFF]));
push_record(&mut data, BRT_FILL.as_u16(), &gradient);
push_record(&mut data, BRT_END_FILLS.as_u16(), &[]);
let stylesheet = parse_styles(&data).expect("parse styles");
assert_eq!(stylesheet.fills.len(), 3);
match &stylesheet.fills[0].pattern {
PatternFill::Solid { .. } => {}
other => panic!("expected solid fill, got {other:?}"),
}
match &stylesheet.fills[1].pattern {
PatternFill::Pattern { pattern_type, .. } => {
assert_eq!(*pattern_type, PatternType::Gray125);
}
other => panic!("expected pattern fill, got {other:?}"),
}
match &stylesheet.fills[2].pattern {
PatternFill::Gradient(gradient) => {
assert_eq!(gradient.gradient_type, GradientType::Linear);
assert_eq!(gradient.stops.len(), 2);
}
other => panic!("expected gradient fill, got {other:?}"),
}
}
#[test]
fn parses_borders() {
let mut data = Vec::new();
push_record(&mut data, BRT_BEGIN_BORDERS.as_u16(), &[]);
let mut border = Vec::new();
border.push(0b0000_0011);
border.push(1); border.push(0);
border.extend_from_slice(&encode_color(2, 0, 0, [1, 2, 3, 0xFF]));
border.push(2); border.push(0);
border.extend_from_slice(&encode_color(2, 0, 0, [4, 5, 6, 0xFF]));
border.push(3); border.push(0);
border.extend_from_slice(&encode_color(2, 0, 0, [7, 8, 9, 0xFF]));
border.push(4); border.push(0);
border.extend_from_slice(&encode_color(2, 0, 0, [10, 11, 12, 0xFF]));
border.push(5); border.push(0);
border.extend_from_slice(&encode_color(2, 0, 0, [13, 14, 15, 0xFF]));
push_record(&mut data, BRT_BORDER.as_u16(), &border);
push_record(&mut data, BRT_END_BORDERS.as_u16(), &[]);
let stylesheet = parse_styles(&data).expect("parse styles");
assert_eq!(stylesheet.borders.len(), 1);
let parsed = &stylesheet.borders[0];
assert_eq!(parsed.top.style, BorderStyle::Thin);
assert_eq!(parsed.bottom.style, BorderStyle::Medium);
assert_eq!(parsed.left.style, BorderStyle::Dashed);
assert_eq!(parsed.right.style, BorderStyle::Dotted);
assert_eq!(parsed.diagonal.style, BorderStyle::Thick);
assert!(parsed.diagonal_down);
assert!(parsed.diagonal_up);
}
#[test]
fn parses_xf_alignment_and_protection() {
let mut data = Vec::new();
push_record(&mut data, BRT_BEGIN_CELL_STYLE_XFS.as_u16(), &[]);
let mut style_xf = Vec::new();
style_xf.extend_from_slice(&u16::MAX.to_le_bytes());
style_xf.extend_from_slice(&0u16.to_le_bytes());
style_xf.extend_from_slice(&0u16.to_le_bytes());
style_xf.extend_from_slice(&0u16.to_le_bytes());
style_xf.extend_from_slice(&0u16.to_le_bytes());
style_xf.push(0);
style_xf.push(0);
style_xf.push(0);
style_xf.push(0);
style_xf.push(0);
style_xf.push(0);
push_record(&mut data, BRT_XF.as_u16(), &style_xf);
push_record(&mut data, BRT_END_CELL_STYLE_XFS.as_u16(), &[]);
push_record(&mut data, BRT_BEGIN_CELL_XFS.as_u16(), &[]);
let mut xf = Vec::new();
xf.extend_from_slice(&1u16.to_le_bytes());
xf.extend_from_slice(&164u16.to_le_bytes());
xf.extend_from_slice(&2u16.to_le_bytes());
xf.extend_from_slice(&3u16.to_le_bytes());
xf.extend_from_slice(&4u16.to_le_bytes());
xf.push(90);
xf.push(2);
xf.push(0b0100_0010);
xf.push(0b0011_1001);
xf.push(0b0011_1111);
xf.push(0);
push_record(&mut data, BRT_XF.as_u16(), &xf);
push_record(&mut data, BRT_END_CELL_XFS.as_u16(), &[]);
let stylesheet = parse_styles(&data).expect("parse styles");
assert_eq!(stylesheet.cell_style_xfs.len(), 1);
assert_eq!(stylesheet.cell_xfs.len(), 1);
let parsed = &stylesheet.cell_xfs[0];
assert_eq!(parsed.xf_id, Some(1));
assert_eq!(parsed.alignment.horizontal, HorizontalAlign::Center);
assert_eq!(parsed.alignment.vertical, VerticalAlign::Top);
assert!(parsed.alignment.wrap_text);
assert!(parsed.alignment.shrink_to_fit);
assert_eq!(parsed.alignment.text_rotation, 90);
assert_eq!(parsed.alignment.indent, 2);
assert_eq!(parsed.alignment.reading_order, 2);
assert!(parsed.protection.locked);
assert!(parsed.protection.hidden);
assert!(parsed.apply_number_format);
assert!(parsed.apply_font);
assert!(parsed.apply_fill);
assert!(parsed.apply_border);
assert!(parsed.apply_alignment);
assert!(parsed.apply_protection);
}
}