allsorts_no_std 0.5.2

Font parser, shaping engine, and subsetter for OpenType, WOFF, and WOFF2
//! Reading of the WOFF font format.

use crate::binary::read::{ReadArray, ReadBinary, ReadBuf, ReadCtxt, ReadFrom, ReadScope};
use crate::binary::U32Be;
use crate::error::ParseError;
use crate::tables::FontTableProvider;

use alloc::borrow::Cow;
use alloc::string::String;
use core::convert::TryFrom;

/// The magic number identifying a WOFF file: 'wOFF'
pub const MAGIC: u32 = 0x774F4646;

#[derive(Clone)]
pub struct WoffFont<'a> {
    pub scope: ReadScope<'a>,
    pub woff_header: WoffHeader,
    pub table_directory: ReadArray<'a, TableDirectoryEntry>,
}

#[derive(Clone, Debug)]
pub struct WoffHeader {
    pub flavor: u32,
    pub length: u32,
    pub num_tables: u16,
    pub total_sfnt_size: u32,
    pub _major_version: u16,
    pub _minor_version: u16,
    pub meta_offset: u32,
    pub meta_length: u32,
    pub meta_orig_length: u32,
    pub priv_offset: u32,
    pub priv_length: u32,
}

#[derive(Debug, Clone)]
pub struct TableDirectoryEntry {
    pub tag: u32,
    pub offset: u32,
    pub comp_length: u32,
    pub orig_length: u32,
    pub orig_checksum: u32,
}

impl<'a> WoffFont<'a> {
    /// The "sfnt version" of the input font
    pub fn flavor(&self) -> u32 {
        self.woff_header.flavor
    }

    /// Decompress and return the extended metadata XML if present
    pub fn extended_metadata(&self) -> Result<Option<String>, ParseError> {
        use miniz_oxide::inflate::decompress_to_vec;

        let offset = usize::try_from(self.woff_header.meta_offset)?;
        let length = usize::try_from(self.woff_header.meta_length)?;
        if offset == 0 || length == 0 {
            return Ok(None);
        }

        let compressed_metadata = self.scope.offset_length(offset, length)?;
        let metadata_uncompressed = decompress_to_vec(compressed_metadata.data()).map_err(|_err| ParseError::CompressionError)?;
        let metadata_string = String::from_utf8(metadata_uncompressed).map_err(|_err| ParseError::CompressionError)?;

        Ok(Some(metadata_string))
    }

    /// Find the table directory entry for the given `tag`
    pub fn find_table_directory_entry(&self, tag: u32) -> Option<TableDirectoryEntry> {
        self.table_directory
            .iter()
            .find(|table_entry| table_entry.tag == tag)
    }
}

impl<'a> ReadBinary<'a> for WoffFont<'a> {
    type HostType = Self;

    fn read(ctxt: &mut ReadCtxt<'a>) -> Result<Self, ParseError> {
        let scope = ctxt.scope();
        let mut peek = ctxt.clone();
        let magic = peek.read_u32be()?;
        match magic {
            MAGIC => {
                let woff_header = ctxt.read::<WoffHeader>()?;
                let table_directory =
                    ctxt.read_array::<TableDirectoryEntry>(usize::from(woff_header.num_tables))?;
                Ok(WoffFont {
                    scope,
                    woff_header,
                    table_directory,
                })
            }
            _ => Err(ParseError::BadVersion),
        }
    }
}

impl<'a> FontTableProvider for WoffFont<'a> {
    fn table_data<'b>(&'b self, tag: u32) -> Result<Option<Cow<'b, [u8]>>, ParseError> {
        self.find_table_directory_entry(tag)
            .map(|table_entry| {
                table_entry
                    .read_table(&self.scope)
                    .map(|table| table.into_data())
            })
            .transpose()
    }

    fn has_table(&self, tag: u32) -> bool {
        self.find_table_directory_entry(tag).is_some()
    }
}

impl<'a> ReadBinary<'a> for WoffHeader {
    type HostType = Self;

    fn read(ctxt: &mut ReadCtxt<'a>) -> Result<Self, ParseError> {
        let signature = ctxt.read_u32be()?;
        match signature {
            MAGIC => {
                let flavor = ctxt.read_u32be()?;
                let length = ctxt.read_u32be()?;
                let num_tables = ctxt.read_u16be()?;
                let reserved = ctxt.read_u16be()?;
                // The header includes a reserved field; this MUST be set to zero. If this field is
                // non-zero, a conforming user agent MUST reject the file as invalid.
                ctxt.check(reserved == 0)?;
                let total_sfnt_size = ctxt.read_u32be()?;
                // The WOFF majorVersion and minorVersion fields specify the version number for a
                // given WOFF file, which can be based on the version number of the input font but
                // is not required to be. These fields have no effect on font loading or usage
                // behavior in user agents.
                let _major_version = ctxt.read_u16be()?;
                let _minor_version = ctxt.read_u16be()?;
                let meta_offset = ctxt.read_u32be()?;
                let meta_length = ctxt.read_u32be()?;
                let meta_orig_length = ctxt.read_u32be()?;
                let priv_offset = ctxt.read_u32be()?;
                let priv_length = ctxt.read_u32be()?;

                Ok(WoffHeader {
                    flavor,
                    length,
                    num_tables,
                    total_sfnt_size,
                    _major_version,
                    _minor_version,
                    meta_offset,
                    meta_length,
                    meta_orig_length,
                    priv_offset,
                    priv_length,
                })
            }
            _ => Err(ParseError::BadVersion),
        }
    }
}

impl<'a> ReadFrom<'a> for TableDirectoryEntry {
    type ReadType = ((U32Be, U32Be, U32Be), (U32Be, U32Be));
    fn from(
        ((tag, offset, comp_length), (orig_length, orig_checksum)): ((u32, u32, u32), (u32, u32)),
    ) -> Self {
        TableDirectoryEntry {
            tag,
            offset,
            comp_length,
            orig_length,
            orig_checksum,
        }
    }
}

impl TableDirectoryEntry {
    fn is_compressed(&self) -> bool {
        self.comp_length != self.orig_length
    }

    /// Read and uncompress the contents of a table entry
    pub fn read_table<'a>(&self, scope: &ReadScope<'a>) -> Result<ReadBuf<'a>, ParseError> {
        let offset = usize::try_from(self.offset)?;
        let length = usize::try_from(self.comp_length)?;
        let table_data = scope.offset_length(offset, length)?;

        if self.is_compressed() {
            use miniz_oxide::inflate::decompress_to_vec;
            let table_uncompressed = decompress_to_vec(table_data.data())
            .map_err(|_err| ParseError::CompressionError)?;
            Ok(ReadBuf::from(table_uncompressed))
        } else {
            Ok(ReadBuf::from(table_data.data()))
        }
    }
}