write_fonts/tables/
meta.rs

1//! The [meta (Metadata)](https://docs.microsoft.com/en-us/typography/opentype/spec/meta) table
2
3use std::fmt::Display;
4
5include!("../../generated/generated_meta.rs");
6
7pub const DLNG: Tag = Tag::new(b"dlng");
8pub const SLNG: Tag = Tag::new(b"slng");
9
10/// Metadata in the `meta` table, associated with some tag.
11#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub enum Metadata {
14    /// For the 'dlng' and 'slng' tags
15    ScriptLangTags(Vec<ScriptLangTag>),
16    /// For other tags
17    Other(Vec<u8>),
18}
19
20/// A ['ScriptLangTag'] value.
21///
22/// This is currently just a string and we do not perform any validation,
23/// but we should do that (TK open issue)
24///
25/// [`ScriptLangTag`]: https://learn.microsoft.com/en-us/typography/opentype/spec/meta#scriptlangtag-values
26#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub struct ScriptLangTag(String);
29
30/// An error for if a [`ScriptLangTag`] does not conform to the specification.
31#[derive(Clone, Debug)]
32#[non_exhaustive] // so we can flesh this out later without breaking anything
33pub struct InvalidScriptLangTag;
34
35impl ScriptLangTag {
36    pub fn new(raw: String) -> Result<Self, InvalidScriptLangTag> {
37        Ok(Self(raw))
38    }
39
40    pub fn as_str(&self) -> &str {
41        self.0.as_str()
42    }
43}
44
45impl Display for InvalidScriptLangTag {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        f.write_str("ScriptLangTag was malformed")
48    }
49}
50
51impl std::error::Error for InvalidScriptLangTag {}
52
53impl DataMapRecord {
54    fn validate_data_type(&self, ctx: &mut ValidationCtx) {
55        if matches!(
56            (self.tag, self.data.as_ref()),
57            (SLNG | DLNG, Metadata::Other(_))
58        ) {
59            ctx.report("'slng' or 'dlng' tags use ScriptLangTag data");
60        }
61    }
62
63    fn compute_data_len(&self) -> usize {
64        match self.data.as_ref() {
65            Metadata::ScriptLangTags(items) => {
66                let sum_len: usize = items.iter().map(|tag| tag.as_str().len()).sum();
67                let toss_some_commas_in_there = items.len().saturating_sub(1);
68                sum_len + toss_some_commas_in_there
69            }
70            Metadata::Other(vec) => vec.len(),
71        }
72    }
73}
74
75impl FontWrite for Metadata {
76    fn write_into(&self, writer: &mut TableWriter) {
77        match self {
78            Metadata::ScriptLangTags(langs) => {
79                let mut first = true;
80                for lang in langs {
81                    if !first {
82                        b','.write_into(writer);
83                    }
84                    first = false;
85                    lang.0.as_bytes().write_into(writer);
86                }
87            }
88            Metadata::Other(vec) => {
89                vec.write_into(writer);
90            }
91        };
92    }
93}
94
95impl Validate for Metadata {
96    fn validate_impl(&self, _ctx: &mut ValidationCtx) {}
97}
98
99impl FromObjRef<read_fonts::tables::meta::Metadata<'_>> for Metadata {
100    fn from_obj_ref(from: &read_fonts::tables::meta::Metadata<'_>, _: FontData) -> Self {
101        match from {
102            read_fonts::tables::meta::Metadata::ScriptLangTags(var_len_array) => {
103                Self::ScriptLangTags(
104                    var_len_array
105                        .iter()
106                        .flat_map(|x| {
107                            x.ok()
108                                .and_then(|x| ScriptLangTag::new(x.as_str().into()).ok())
109                        })
110                        .collect(),
111                )
112            }
113            read_fonts::tables::meta::Metadata::Other(bytes) => Self::Other(bytes.to_vec()),
114        }
115    }
116}
117
118impl FromTableRef<read_fonts::tables::meta::Metadata<'_>> for Metadata {}
119
120// Note: This is required because of generated trait bounds, but we don't really
121// want to use it because we want our metadata to match our tag...
122impl Default for Metadata {
123    fn default() -> Self {
124        Metadata::ScriptLangTags(Vec::new())
125    }
126}
127
128impl FromObjRef<read_fonts::tables::meta::DataMapRecord> for DataMapRecord {
129    fn from_obj_ref(obj: &read_fonts::tables::meta::DataMapRecord, offset_data: FontData) -> Self {
130        let data = obj
131            .data(offset_data)
132            .map(|meta| meta.to_owned_table())
133            .unwrap_or_else(|_| match obj.tag() {
134                DLNG | SLNG => Metadata::ScriptLangTags(Vec::new()),
135                _ => Metadata::Other(Vec::new()),
136            });
137        DataMapRecord {
138            tag: obj.tag(),
139            data: OffsetMarker::new(data),
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146
147    use super::*;
148    use font_test_data::meta as test_data;
149
150    #[test]
151    fn convert_from_read() {
152        let table = Meta::read(test_data::SIMPLE_META_TABLE.into()).unwrap();
153        let rec1 = &table.data_maps[0];
154        assert_eq!(
155            rec1.data.as_ref(),
156            &Metadata::ScriptLangTags(vec![
157                ScriptLangTag::new("en-latn".into()).unwrap(),
158                ScriptLangTag::new("latn".into()).unwrap()
159            ])
160        );
161
162        let round_trip = crate::dump_table(&table).unwrap();
163        let read_back = Meta::read(round_trip.as_slice().into()).unwrap();
164        let readr = read_fonts::tables::meta::Meta::read(round_trip.as_slice().into()).unwrap();
165        dbg!(readr);
166
167        //eprintln!("{read_back:#?}");
168
169        assert_eq!(table, read_back);
170    }
171}