allsorts_subset_browser/tables/
svg.rs

1#![deny(missing_docs)]
2
3//! `SVG` table parsing.
4//!
5//! <https://docs.microsoft.com/en-us/typography/opentype/spec/SVG>
6
7use std::convert::TryFrom;
8use std::io::Read;
9
10use flate2::read::GzDecoder;
11
12use crate::binary::read::{
13    ReadArray, ReadBinary, ReadBinaryDep, ReadCtxt, ReadFixedSizeDep, ReadScope,
14};
15use crate::bitmap::{
16    Bitmap, BitmapGlyph, EncapsulatedBitmap, EncapsulatedFormat, Metrics, OriginOffset,
17};
18use crate::error::ParseError;
19use crate::size;
20
21const GZIP_HEADER: &[u8] = &[0x1F, 0x8B, 0x08];
22
23/// Holds the records from the `SVG` table.
24pub struct SvgTable<'a> {
25    /// The version of the table. Only version `0` is supported.
26    pub version: u16,
27    /// The SVG document records.
28    ///
29    /// **Example:**
30    ///
31    /// ```ignore
32    /// for record in svg.document_records.iter_res() {
33    ///     let record = record?;
34    ///     // Use record here
35    /// }
36    /// ```
37    pub document_records: ReadArray<'a, SVGDocumentRecord<'a>>,
38}
39
40/// One SVG record holding a glyph range and `SVGDocumentRecord`.
41pub struct SVGDocumentRecord<'a> {
42    /// The starting glyph id.
43    pub start_glyph_id: u16,
44    /// The end glyph id.
45    ///
46    /// Can be the same as `start_glyph_id`.
47    pub end_glyph_id: u16,
48    /// The SVG document data. Possibly compressed.
49    ///
50    /// If the data is compressed it will begin with 0x1F, 0x8B, 0x08, which is a gzip member
51    /// header indicating "deflate" as the compression method. See section 2.3.1 of
52    /// <https://www.ietf.org/rfc/rfc1952.txt>
53    pub svg_document: &'a [u8],
54}
55
56impl<'a> SvgTable<'a> {
57    /// Locate the SVG record for the supplied `glyph_id`.
58    pub fn lookup_glyph(&self, glyph_id: u16) -> Result<Option<SVGDocumentRecord<'a>>, ParseError> {
59        for record in self.document_records.iter_res() {
60            let record = record?;
61            if glyph_id >= record.start_glyph_id && glyph_id <= record.end_glyph_id {
62                return Ok(Some(record));
63            }
64        }
65        Ok(None)
66    }
67}
68
69impl<'b> ReadBinary for SvgTable<'b> {
70    type HostType<'a> = SvgTable<'a>;
71
72    fn read<'a>(ctxt: &mut ReadCtxt<'a>) -> Result<Self::HostType<'a>, ParseError> {
73        let scope = ctxt.scope();
74        let version = ctxt.read_u16be()?;
75        ctxt.check(version == 0)?;
76        let document_records_offset = usize::try_from(ctxt.read_u32be()?)?;
77
78        let records_scope = scope.offset(document_records_offset);
79        let mut records_ctxt = records_scope.ctxt();
80        let num_records = records_ctxt.read_u16be().map(usize::from)?;
81        let document_records = records_ctxt.read_array_dep(num_records, records_scope)?;
82
83        Ok(SvgTable {
84            version,
85            document_records,
86        })
87    }
88}
89
90impl<'b> ReadBinaryDep for SVGDocumentRecord<'b> {
91    type Args<'a> = ReadScope<'a>;
92    type HostType<'a> = SVGDocumentRecord<'a>;
93
94    fn read_dep<'a>(
95        ctxt: &mut ReadCtxt<'a>,
96        scope: ReadScope<'a>,
97    ) -> Result<Self::HostType<'a>, ParseError> {
98        let start_glyph_id = ctxt.read_u16be()?;
99        let end_glyph_id = ctxt.read_u16be()?;
100        let svg_doc_offset = usize::try_from(ctxt.read_u32be()?)?;
101        let svg_doc_length = usize::try_from(ctxt.read_u32be()?)?;
102        let svg_data = scope.offset_length(svg_doc_offset, svg_doc_length)?;
103        let svg_document = svg_data.data();
104
105        Ok(SVGDocumentRecord {
106            start_glyph_id,
107            end_glyph_id,
108            svg_document,
109        })
110    }
111}
112impl<'a> ReadFixedSizeDep for SVGDocumentRecord<'a> {
113    fn size(_: Self::Args<'_>) -> usize {
114        // uint16   startGlyphID
115        // uint16   endGlyphID
116        // Offset32 svgDocOffset
117        // uint32   svgDocLength
118        // — https://docs.microsoft.com/en-us/typography/opentype/spec/svg#svg-document-list
119        (2 * size::U16) + (2 * size::U32)
120    }
121}
122
123impl<'a> TryFrom<&SVGDocumentRecord<'a>> for BitmapGlyph {
124    type Error = ParseError;
125
126    fn try_from(svg_record: &SVGDocumentRecord<'a>) -> Result<Self, ParseError> {
127        // If the document is compressed then inflate it. &[0x1F, 0x8B, 0x08] is a gzip member
128        // header indicating "deflate" as the compression method. See section 2.3.1 of
129        // https://www.ietf.org/rfc/rfc1952.txt
130        let data = if svg_record.svg_document.starts_with(GZIP_HEADER) {
131            let mut gz = GzDecoder::new(svg_record.svg_document);
132            let mut uncompressed = Vec::with_capacity(svg_record.svg_document.len());
133            gz.read_to_end(&mut uncompressed)
134                .map_err(|_err| ParseError::CompressionError)?;
135            uncompressed.into_boxed_slice()
136        } else {
137            Box::from(svg_record.svg_document)
138        };
139
140        let encapsulated = EncapsulatedBitmap {
141            format: EncapsulatedFormat::Svg,
142            data,
143        };
144        Ok(BitmapGlyph {
145            bitmap: Bitmap::Encapsulated(encapsulated),
146            metrics: Metrics::HmtxVmtx(OriginOffset { x: 0, y: 0 }),
147            ppem_x: None,
148            ppem_y: None,
149        })
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use crate::font_data::FontData;
157    use crate::tables::FontTableProvider;
158    use crate::tag;
159    use crate::tests::read_fixture;
160
161    #[test]
162    fn test_read_svg() {
163        let buffer = read_fixture("tests/fonts/opentype/TwitterColorEmoji-SVGinOT.ttf");
164        let scope = ReadScope::new(&buffer);
165        let font_file = scope
166            .read::<FontData<'_>>()
167            .expect("unable to parse font file");
168        let table_provider = font_file
169            .table_provider(0)
170            .expect("unable to create font provider");
171        let svg_data = table_provider
172            .read_table_data(tag::SVG)
173            .expect("unable to read SVG table data");
174        let svg = ReadScope::new(&svg_data).read::<SvgTable<'_>>().unwrap();
175
176        let records = svg
177            .document_records
178            .iter_res()
179            .into_iter()
180            .collect::<Result<Vec<_>, _>>()
181            .unwrap();
182        assert_eq!(records.len(), 3075);
183
184        let record = &records[0];
185        assert_eq!(record.start_glyph_id, 5);
186        assert_eq!(record.end_glyph_id, 5);
187        assert_eq!(record.svg_document.len(), 751);
188        let doc = std::str::from_utf8(record.svg_document).unwrap();
189        assert_eq!(&doc[0..43], "<?xml version='1.0' encoding='UTF-8'?>\n<svg");
190    }
191
192    #[test]
193    fn test_read_gzipped_svg() {
194        let buffer = read_fixture("tests/fonts/svg/gzipped.ttf");
195        let scope = ReadScope::new(&buffer);
196        let font_file = scope
197            .read::<FontData<'_>>()
198            .expect("unable to parse font file");
199        let table_provider = font_file
200            .table_provider(0)
201            .expect("unable to create font provider");
202        let svg_data = table_provider
203            .read_table_data(tag::SVG)
204            .expect("unable to read SVG table data");
205        let svg = ReadScope::new(&svg_data).read::<SvgTable<'_>>().unwrap();
206        let record = svg
207            .document_records
208            .iter_res()
209            .into_iter()
210            .nth(0)
211            .unwrap()
212            .unwrap();
213
214        // Ensure the document is actually compressed
215        assert!(record.svg_document.starts_with(GZIP_HEADER));
216        // Now test decompression
217        match BitmapGlyph::try_from(&record) {
218            Ok(BitmapGlyph {
219                bitmap: Bitmap::Encapsulated(EncapsulatedBitmap { data, .. }),
220                ..
221            }) => {
222                let doc = std::str::from_utf8(&data).unwrap();
223                assert_eq!(&doc[0..42], r#"<?xml version="1.0" encoding="UTF-8"?><svg"#);
224            }
225            _ => panic!("did not get expected result"),
226        }
227    }
228}