1
#![deny(missing_docs)]
2

            
3
//! `SVG` table parsing.
4
//!
5
//! <https://docs.microsoft.com/en-us/typography/opentype/spec/SVG>
6

            
7
use std::io::Read;
8

            
9
use flate2::read::GzDecoder;
10

            
11
use crate::binary::read::{
12
    ReadArray, ReadBinary, ReadBinaryDep, ReadCtxt, ReadFixedSizeDep, ReadScope,
13
};
14
use crate::bitmap::{
15
    Bitmap, BitmapGlyph, EncapsulatedBitmap, EncapsulatedFormat, Metrics, OriginOffset,
16
};
17
use crate::error::ParseError;
18
use crate::size;
19

            
20
const GZIP_HEADER: &[u8] = &[0x1F, 0x8B, 0x08];
21

            
22
/// Holds the records from the `SVG` table.
23
pub struct SvgTable<'a> {
24
    /// The version of the table. Only version `0` is supported.
25
    pub version: u16,
26
    /// The SVG document records.
27
    ///
28
    /// **Example:**
29
    ///
30
    /// ```ignore
31
    /// for record in svg.document_records.iter_res() {
32
    ///     let record = record?;
33
    ///     // Use record here
34
    /// }
35
    /// ```
36
    pub document_records: ReadArray<'a, SVGDocumentRecord<'a>>,
37
}
38

            
39
/// One SVG record holding a glyph range and `SVGDocumentRecord`.
40
pub struct SVGDocumentRecord<'a> {
41
    /// The starting glyph id.
42
    pub start_glyph_id: u16,
43
    /// The end glyph id.
44
    ///
45
    /// Can be the same as `start_glyph_id`.
46
    pub end_glyph_id: u16,
47
    /// The SVG document data. Possibly compressed.
48
    ///
49
    /// If the data is compressed it will begin with 0x1F, 0x8B, 0x08, which is a gzip member
50
    /// header indicating "deflate" as the compression method. See section 2.3.1 of
51
    /// <https://www.ietf.org/rfc/rfc1952.txt>
52
    pub svg_document: &'a [u8],
53
}
54

            
55
impl<'a> SvgTable<'a> {
56
    /// Locate the SVG record for the supplied `glyph_id`.
57
    pub fn lookup_glyph(&self, glyph_id: u16) -> Result<Option<SVGDocumentRecord<'a>>, ParseError> {
58
        for record in self.document_records.iter_res() {
59
            let record = record?;
60
            if glyph_id >= record.start_glyph_id && glyph_id <= record.end_glyph_id {
61
                return Ok(Some(record));
62
            }
63
        }
64
        Ok(None)
65
    }
66
}
67

            
68
impl ReadBinary for SvgTable<'_> {
69
    type HostType<'a> = SvgTable<'a>;
70

            
71
    fn read<'a>(ctxt: &mut ReadCtxt<'a>) -> Result<Self::HostType<'a>, ParseError> {
72
        let scope = ctxt.scope();
73
        let version = ctxt.read_u16be()?;
74
        ctxt.check(version == 0)?;
75
        let document_records_offset = usize::try_from(ctxt.read_u32be()?)?;
76

            
77
        let records_scope = scope.offset(document_records_offset);
78
        let mut records_ctxt = records_scope.ctxt();
79
        let num_records = records_ctxt.read_u16be().map(usize::from)?;
80
        let document_records = records_ctxt.read_array_dep(num_records, records_scope)?;
81

            
82
        Ok(SvgTable {
83
            version,
84
            document_records,
85
        })
86
    }
87
}
88

            
89
impl ReadBinaryDep for SVGDocumentRecord<'_> {
90
    type Args<'a> = ReadScope<'a>;
91
    type HostType<'a> = SVGDocumentRecord<'a>;
92

            
93
    fn read_dep<'a>(
94
        ctxt: &mut ReadCtxt<'a>,
95
        scope: ReadScope<'a>,
96
    ) -> Result<Self::HostType<'a>, ParseError> {
97
        let start_glyph_id = ctxt.read_u16be()?;
98
        let end_glyph_id = ctxt.read_u16be()?;
99
        let svg_doc_offset = usize::try_from(ctxt.read_u32be()?)?;
100
        let svg_doc_length = usize::try_from(ctxt.read_u32be()?)?;
101
        let svg_data = scope.offset_length(svg_doc_offset, svg_doc_length)?;
102
        let svg_document = svg_data.data();
103

            
104
        Ok(SVGDocumentRecord {
105
            start_glyph_id,
106
            end_glyph_id,
107
            svg_document,
108
        })
109
    }
110
}
111
impl ReadFixedSizeDep for SVGDocumentRecord<'_> {
112
    fn size(_: Self::Args<'_>) -> usize {
113
        // uint16   startGlyphID
114
        // uint16   endGlyphID
115
        // Offset32 svgDocOffset
116
        // uint32   svgDocLength
117
        // — https://docs.microsoft.com/en-us/typography/opentype/spec/svg#svg-document-list
118
        (2 * size::U16) + (2 * size::U32)
119
    }
120
}
121

            
122
impl<'a> TryFrom<(&SVGDocumentRecord<'a>, u16)> for BitmapGlyph {
123
    type Error = ParseError;
124

            
125
    fn try_from(
126
        (svg_record, bitmap_id): (&SVGDocumentRecord<'a>, u16),
127
    ) -> Result<Self, ParseError> {
128
        // If the document is compressed then inflate it. &[0x1F, 0x8B, 0x08] is a gzip member
129
        // header indicating "deflate" as the compression method. See section 2.3.1 of
130
        // https://www.ietf.org/rfc/rfc1952.txt
131
        let data = if svg_record.svg_document.starts_with(GZIP_HEADER) {
132
            let mut gz = GzDecoder::new(svg_record.svg_document);
133
            let mut uncompressed = Vec::with_capacity(svg_record.svg_document.len());
134
            gz.read_to_end(&mut uncompressed)
135
                .map_err(|_err| ParseError::CompressionError)?;
136
            uncompressed.into_boxed_slice()
137
        } else {
138
            Box::from(svg_record.svg_document)
139
        };
140

            
141
        let encapsulated = EncapsulatedBitmap {
142
            format: EncapsulatedFormat::Svg,
143
            data,
144
        };
145
        Ok(BitmapGlyph {
146
            bitmap: Bitmap::Encapsulated(encapsulated),
147
            bitmap_id,
148
            metrics: Metrics::HmtxVmtx(OriginOffset { x: 0, y: 0 }),
149
            ppem_x: None,
150
            ppem_y: None,
151
            should_flip_hori: false,
152
        })
153
    }
154
}
155

            
156
#[cfg(test)]
157
mod tests {
158
    use super::*;
159
    use crate::font_data::FontData;
160
    use crate::tables::FontTableProvider;
161
    use crate::tag;
162
    use crate::tests::read_fixture;
163

            
164
    #[test]
165
    fn test_read_svg() {
166
        let buffer = read_fixture("tests/fonts/opentype/TwitterColorEmoji-SVGinOT.ttf");
167
        let scope = ReadScope::new(&buffer);
168
        let font_file = scope
169
            .read::<FontData<'_>>()
170
            .expect("unable to parse font file");
171
        let table_provider = font_file
172
            .table_provider(0)
173
            .expect("unable to create font provider");
174
        let svg_data = table_provider
175
            .read_table_data(tag::SVG)
176
            .expect("unable to read SVG table data");
177
        let svg = ReadScope::new(&svg_data).read::<SvgTable<'_>>().unwrap();
178

            
179
        let records = svg
180
            .document_records
181
            .iter_res()
182
            .into_iter()
183
            .collect::<Result<Vec<_>, _>>()
184
            .unwrap();
185
        assert_eq!(records.len(), 3075);
186

            
187
        let record = &records[0];
188
        assert_eq!(record.start_glyph_id, 5);
189
        assert_eq!(record.end_glyph_id, 5);
190
        assert_eq!(record.svg_document.len(), 751);
191
        let doc = std::str::from_utf8(record.svg_document).unwrap();
192
        assert_eq!(&doc[0..43], "<?xml version='1.0' encoding='UTF-8'?>\n<svg");
193
    }
194

            
195
    #[test]
196
    fn test_read_gzipped_svg() {
197
        let buffer = read_fixture("tests/fonts/svg/gzipped.ttf");
198
        let scope = ReadScope::new(&buffer);
199
        let font_file = scope
200
            .read::<FontData<'_>>()
201
            .expect("unable to parse font file");
202
        let table_provider = font_file
203
            .table_provider(0)
204
            .expect("unable to create font provider");
205
        let svg_data = table_provider
206
            .read_table_data(tag::SVG)
207
            .expect("unable to read SVG table data");
208
        let svg = ReadScope::new(&svg_data).read::<SvgTable<'_>>().unwrap();
209
        let record = svg
210
            .document_records
211
            .iter_res()
212
            .into_iter()
213
            .nth(0)
214
            .unwrap()
215
            .unwrap();
216

            
217
        // Ensure the document is actually compressed
218
        assert!(record.svg_document.starts_with(GZIP_HEADER));
219
        // Now test decompression
220
        match BitmapGlyph::try_from((&record, 0 /* Arbitrary ID */)) {
221
            Ok(BitmapGlyph {
222
                bitmap: Bitmap::Encapsulated(EncapsulatedBitmap { data, .. }),
223
                ..
224
            }) => {
225
                let doc = std::str::from_utf8(&data).unwrap();
226
                assert_eq!(&doc[0..42], r#"<?xml version="1.0" encoding="UTF-8"?><svg"#);
227
            }
228
            _ => panic!("did not get expected result"),
229
        }
230
    }
231
}