mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-31 06:06:48 -06:00
Merge PR #1946: XLSX: keep cell fonts' charset (Arabic 178) and family on save
crates/xlsx/src/styles.rs: family/charset block ported into the dxf.is_none() branch of font_xml (PR #1225 made it take Option<&DxfStated>) docs/file-format-parity.md: header Change line keeps both sides' clauses
This commit is contained in:
@@ -296,7 +296,11 @@ fn underline(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
|
||||
fn font_name(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let Some(name) = str_param(p, "name").map(str::to_string) else { return Err(bad("home.fontName", "missing `name`")) };
|
||||
apply_style(s, p, |st| st.font.name = name.clone())
|
||||
apply_style(s, p, |st| {
|
||||
st.font.name = name.clone();
|
||||
st.font.charset = None;
|
||||
st.font.family = None;
|
||||
})
|
||||
}
|
||||
|
||||
fn font_size(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
|
||||
@@ -1207,6 +1207,24 @@ fn structured_refs_to_columns_with_edge_spaces() {
|
||||
assert_eq!([v(&s, "F1"), v(&s, "F2"), v(&s, "F3")], [n(5.0), n(5.0), n(5.0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_the_font_name_drops_the_old_fonts_charset_and_family() {
|
||||
let mut s = s();
|
||||
{
|
||||
let wb = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().wb);
|
||||
let arabic = wb.styles.derive(gridcraft_model::StyleId::DEFAULT, |st| {
|
||||
st.font.name = "Traditional Arabic".into();
|
||||
st.font.charset = Some(178);
|
||||
st.font.family = Some(1);
|
||||
});
|
||||
wb.sheet_mut(0).unwrap().set_style(CellRef::parse("A1").unwrap(), arabic);
|
||||
}
|
||||
s.execute("home.fontName", json!({"range": "A1", "name": "Arial"})).unwrap();
|
||||
let d = s.doc().unwrap();
|
||||
let font = &d.wb.styles.get(d.wb.active().unwrap().style_id(CellRef::parse("A1").unwrap())).font;
|
||||
assert_eq!((font.name.as_str(), font.charset, font.family), ("Arial", None, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_rename_keeps_a_defined_name_with_the_same_name() {
|
||||
let mut s = s();
|
||||
|
||||
@@ -161,6 +161,12 @@ pub struct Font {
|
||||
pub strike: bool,
|
||||
pub color: Color,
|
||||
pub vert: VertAlign,
|
||||
/// Windows character set the font was chosen for (178 = Arabic); kept so Excel substitutes a font for the same script.
|
||||
#[serde(default)]
|
||||
pub charset: Option<u8>,
|
||||
/// Font family class (1 roman, 2 swiss, 3 modern…); `None` is swiss, the default.
|
||||
#[serde(default)]
|
||||
pub family: Option<u8>,
|
||||
}
|
||||
|
||||
impl Eq for Font {}
|
||||
@@ -168,7 +174,7 @@ impl Hash for Font {
|
||||
fn hash<H: Hasher>(&self, h: &mut H) {
|
||||
self.name.hash(h);
|
||||
self.size.to_bits().hash(h);
|
||||
(self.bold, self.italic, self.underline, self.strike, self.color, self.vert).hash(h);
|
||||
(self.bold, self.italic, self.underline, self.strike, self.color, self.vert, self.charset, self.family).hash(h);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +189,8 @@ impl Default for Font {
|
||||
strike: false,
|
||||
color: Color::Auto,
|
||||
vert: VertAlign::Baseline,
|
||||
charset: None,
|
||||
family: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@ fn bool_prop(e: &El, name: &str) -> bool {
|
||||
e.child(name).is_some_and(|c| c.flag("val", true))
|
||||
}
|
||||
|
||||
const DEFAULT_FONT_FAMILY: u8 = 2;
|
||||
const MAX_FONT_FAMILY: u8 = 14;
|
||||
|
||||
pub fn read_font(e: &El, palette: &[u32]) -> Font {
|
||||
let mut f = Font::default();
|
||||
if let Some(n) = e.child("name").or_else(|| e.child("rFont")).and_then(|n| n.attr("val")) {
|
||||
@@ -84,6 +87,12 @@ pub fn read_font(e: &El, palette: &[u32]) -> Font {
|
||||
_ => VertAlign::Baseline,
|
||||
};
|
||||
}
|
||||
f.charset = e.child("charset").and_then(|c| c.attr_u32("val")).and_then(|v| u8::try_from(v).ok());
|
||||
f.family = e
|
||||
.child("family")
|
||||
.and_then(|c| c.attr_u32("val"))
|
||||
.and_then(|v| u8::try_from(v).ok())
|
||||
.filter(|v| *v <= MAX_FONT_FAMILY && *v != DEFAULT_FONT_FAMILY);
|
||||
f.color = opt_color(e.child("color"), palette);
|
||||
// Text 1 (dk1) is what "automatic" font colour means in practice.
|
||||
if f.color == Color::Theme(1, 0) {
|
||||
@@ -445,7 +454,11 @@ fn font_xml(f: &Font, dxf: Option<&DxfStated>, minor: bool) -> String {
|
||||
s.push_str("<color theme=\"1\"/>");
|
||||
}
|
||||
if dxf.is_none() {
|
||||
let _ = write!(s, "<name val=\"{}\"/><family val=\"2\"/>", esc_attr(&f.name));
|
||||
let family = f.family.filter(|v| *v <= MAX_FONT_FAMILY).unwrap_or(DEFAULT_FONT_FAMILY);
|
||||
let _ = write!(s, "<name val=\"{}\"/><family val=\"{family}\"/>", esc_attr(&f.name));
|
||||
if let Some(charset) = f.charset {
|
||||
let _ = write!(s, "<charset val=\"{charset}\"/>");
|
||||
}
|
||||
if minor {
|
||||
s.push_str("<scheme val=\"minor\"/>");
|
||||
}
|
||||
|
||||
@@ -1128,6 +1128,64 @@ fn default_font_survives_a_round_trip() {
|
||||
assert_eq!(font0(&saved), (Some("12".into()), Some("Calibri".into()), Some("2".into()), Some("minor".into())));
|
||||
}
|
||||
|
||||
fn one_font_workbook(font: &str) -> Vec<u8> {
|
||||
let styles = format!(
|
||||
r#"<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><fonts count="1"><font>{font}</font></fonts><fills count="1"><fill><patternFill patternType="none"/></fill></fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>"#
|
||||
);
|
||||
let rels = r#"<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>"#;
|
||||
super::minimal("<sheetData/>", &[("xl/styles.xml", &styles)], rels, "")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn font_charset_and_family_roundtrip() {
|
||||
let file = one_font_workbook(r#"<name val="Traditional Arabic"/><family val="1"/><charset val="178"/>"#);
|
||||
let (wb, _) = read_xlsx(&file).unwrap();
|
||||
let saved = write_xlsx(&wb).unwrap();
|
||||
let mut pkg = Package::open(&saved).unwrap();
|
||||
let xml = String::from_utf8(pkg.read("xl/styles.xml").unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains("<name val=\"Traditional Arabic\"/><family val=\"1\"/><charset val=\"178\"/>"), "{xml}");
|
||||
|
||||
let (back, _) = read_xlsx(&saved).unwrap();
|
||||
let font = &back.styles.get(StyleId::DEFAULT).font;
|
||||
let back_saved = write_xlsx(&back).unwrap();
|
||||
let mut pkg = Package::open(&back_saved).unwrap();
|
||||
let xml = String::from_utf8(pkg.read("xl/styles.xml").unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains("<name val=\"Traditional Arabic\"/><family val=\"1\"/><charset val=\"178\"/>"), "{xml}");
|
||||
assert_eq!(font.name, "Traditional Arabic");
|
||||
assert_eq!(font.family, Some(1));
|
||||
assert_eq!(font.charset, Some(178));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fonts_without_charset_write_unchanged() {
|
||||
let file = one_font_workbook(r#"<name val="Arial"/>"#);
|
||||
let saved = write_xlsx(&read_xlsx(&file).unwrap().0).unwrap();
|
||||
let mut pkg = Package::open(&saved).unwrap();
|
||||
let xml = String::from_utf8(pkg.read("xl/styles.xml").unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains("<name val=\"Arial\"/><family val=\"2\"/>"), "{xml}");
|
||||
assert!(!xml.contains("<charset"), "{xml}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_family_is_ignored() {
|
||||
let file = one_font_workbook(r#"<name val="Arial"/><family val="15"/>"#);
|
||||
let (wb, _) = read_xlsx(&file).unwrap();
|
||||
assert_eq!(wb.styles.get(StyleId::DEFAULT).font.family, None);
|
||||
let saved = write_xlsx(&wb).unwrap();
|
||||
let mut pkg = Package::open(&saved).unwrap();
|
||||
let xml = String::from_utf8(pkg.read("xl/styles.xml").unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains("<name val=\"Arial\"/><family val=\"2\"/>"), "{xml}");
|
||||
|
||||
let mut set_directly = wb.clone();
|
||||
let wide = set_directly.styles.derive(StyleId::DEFAULT, |st| st.font.family = Some(200));
|
||||
set_directly.sheet_mut(0).unwrap().set_value(CellRef::new(0, 0), Value::text("x"));
|
||||
set_directly.sheet_mut(0).unwrap().set_style(CellRef::new(0, 0), wide);
|
||||
let saved = write_xlsx(&set_directly).unwrap();
|
||||
let mut pkg = Package::open(&saved).unwrap();
|
||||
let xml = String::from_utf8(pkg.read("xl/styles.xml").unwrap().unwrap()).unwrap();
|
||||
assert!(!xml.contains("<family val=\"200\"/>"), "{xml}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solid_data_bar_survives_with_sparklines_and_gradient_neighbours() {
|
||||
let mut wb = Workbook::new();
|
||||
|
||||
Reference in New Issue
Block a user