drawing_gl 0.8.0

OpenGL backend for 2D graphics library
Documentation
//! Construct bitmap font using FreeType library.
//! Generates raw texture data for the given font and collects information
//! about available font characters to map them into texture.

use freetype as ft;
use freetype::Face;
use std::char::from_u32;
use std::cmp::max;
use std::collections::{HashMap, HashSet};
use std::iter::{repeat, FromIterator};

#[derive(Debug)]
pub struct BitmapFont {
    width: u16,
    height: u16,
    chars: HashMap<char, BitmapChar>,
    image: Vec<u8>,
    font_height: u16,
}

#[derive(Debug)]
pub struct BitmapChar {
    // Real glyph's coordinates in pixels.
    pub x_offset: i32,
    pub y_offset: i32,
    pub x_advance: i32,
    pub width: i32,
    pub height: i32,
    // Precalculated scaled positions in texture.
    pub tex: [f32; 2],
    pub tex_width: f32,
    pub tex_height: f32,
    // This field is used only while building the texture.
    data: Option<Vec<u8>>,
}

impl BitmapFont {
    pub fn from_bytes(
        data: &[u8],
        font_size: u8,
        chars: Option<&[char]>,
    ) -> Result<BitmapFont, &'static str> {
        use std::rc::Rc;

        let library = ft::Library::init().map_err(|_| "cannot init freetype library")?;
        let face = library
            .new_memory_face(Rc::new(data.into()), 0)
            .map_err(|_| "face: cannot allocate memory")?;
        Self::new(face, font_size, chars)
    }

    fn get_all_face_chars(face: &mut Face) -> HashSet<char> {
        let mut result = HashSet::new();
        let mut index = 0;
        let face_ptr = face.raw_mut();
        unsafe {
            let mut code = ft::ffi::FT_Get_First_Char(face_ptr, &mut index);
            while index != 0 {
                from_u32(code as u32).map(|ch| result.insert(ch));
                code = ft::ffi::FT_Get_Next_Char(face_ptr, code, &mut index);
            }
        }
        result
    }

    // FIXME(Kagami): Profile and optimize this function!
    // TODO(Kagami): Limit too huge textures? We should keep it less than
    // 8k X 8k in general.
    // TODO(Kagami): Add bunch of asserts to check for negative values and
    // overflows.
    /// Construct new BitMap font using provided parameters (this is general
    /// method, called via `from_` helpers).
    fn new(
        mut face: ft::Face,
        font_size: u8,
        chars: Option<&[char]>,
    ) -> Result<BitmapFont, &'static str> {
        let needed_chars = chars
            .map(|sl| HashSet::from_iter(sl.iter().cloned()))
            .unwrap_or_else(|| Self::get_all_face_chars(&mut face));
        if needed_chars.is_empty() {
            return Err("empty font"); //Err(FontError::EmptyFont);
        }

        face.set_pixel_sizes(0, font_size as u32)
            .map_err(|_| "face: unable set pixel size")?;

        // FreeType representation of rendered glyph 'j':
        //
        // b_left   w
        // +-----+-----+-----+
        // |     |     |     | font_size - bitmap_top()
        // +-----+-----+-----+
        // |     |  x  |     |
        // |     |     |     |
        // |     |  x  |     | bitmap_top()
        // |     |  x  |     |
        // |     |  x  |     |
        // |     |  x  |     |
        // +-----+--x--+-----+
        // |     | x   |     | rows() - bitmap_top()
        // |     |x    |     |
        // +-----------+-----+
        //      advance.x
        //
        // (Read <http://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html>
        // for more details.)
        //
        // Notes:
        // * Width/height of the rendered glyph generally smaller than the the
        //   specified font size
        // * But if we add x/y offsets to the real glyph's dimensions it might
        //   go beyound that limits (e.g. chars like 'j', 'q')
        // * `bottom_left()` may be less than zero for some tight characters
        //   (too push it to the previous one)
        // * Theoretically `bitmap_top()` may be bigger than the `font_size`
        //
        // For simplicity we use fixed box height to store characters in the
        // texture (extended with blank pixels downwards), but width may vary:
        //
        //         width()
        //  +-----+-------+
        //  |  x  |   x   |
        //  |     |       |
        //  |  x  |   x   |
        //  |  x  |   x   | rows()
        //  |  x  |   x   |
        //  |  x  |   x   |
        //  +-----+  x    |
        //  |     | x     |
        //  |     +-------+
        //  |     |       | ch_box_height - rows()
        //  +-----+-------+
        //
        // To construct the optimal texture (i.e. square enought and with box
        // height of the highest character) we need to do several passes.

        // In first pass we collect information about the chars and store their
        // raw bitmap data. It gives us max character height and summary width
        // of all characters.

        let chars_len = needed_chars.len();
        let mut chars_info = HashMap::with_capacity(chars_len);
        let mut sum_image_width = 0;
        let mut max_ch_width = 0;
        let mut ch_box_height = 0;

        // debug!("Start building the bitmap (chars: {})", chars_len);

        for ch in needed_chars {
            face.load_char(ch as usize, ft::face::LoadFlag::RENDER)
                .map_err(|_| "cannot load char")?;
            let glyph = face.glyph();
            let bitmap = glyph.bitmap();
            let ch_width = bitmap.width();
            let ch_height = bitmap.rows();
            let ch_x_offset = glyph.bitmap_left();
            let ch_y_offset = font_size as i32 - glyph.bitmap_top();
            let ch_x_advance = ((glyph.advance().x + 32) >> 6) as i32;
            let buffer = bitmap.buffer();
            let ch_data = Vec::from(buffer);

            chars_info.insert(
                ch,
                BitmapChar {
                    x_offset: ch_x_offset,
                    y_offset: ch_y_offset,
                    x_advance: ch_x_advance,
                    width: ch_width,
                    height: ch_height,
                    // We'll need to fix that fields later:
                    tex: [0.0, 0.0],
                    tex_width: 0.0,
                    tex_height: 0.0,
                    data: Some(ch_data),
                },
            );

            sum_image_width += ch_width;
            max_ch_width = max(max_ch_width, ch_width);
            ch_box_height = max(ch_box_height, ch_height);
        }

        // In second pass we map character boxes with varying width onto the
        // fixed quad texture image and build the final texture image.
        //
        // We start with optimist (square) assumption about texture dimensions
        // and adjust the image's height and size while filling the rows.
        //
        // TODO(Kagami): We may try some cool CS algorithm to fit char boxes
        // into the quad texture space with the best level of compression.
        // Though current level of inefficiency is good enough.

        let ideal_image_size = sum_image_width * ch_box_height;
        let ideal_image_width = (ideal_image_size as f32).sqrt() as i32;
        let image_width = max(max_ch_width, ideal_image_width);
        let image_width = ((image_width + 3) / 4) * 4;
        let assumed_size = ideal_image_size as f32 * 1.5;
        let assumed_ch_in_row = image_width as f32 / max_ch_width as f32;
        let mut image = Vec::with_capacity(assumed_size as usize);
        let mut chars_row = Vec::with_capacity(assumed_ch_in_row as usize);
        let mut cursor_x = 0;
        let mut image_height = 0;

        let dump_row = |image: &mut Vec<u8>, chars_row: &Vec<(i32, i32, Vec<u8>)>| {
            // Copy character data into the image row by row:
            //
            //       image_width
            // +-------+---------+---+
            // |   x   |    x    |   |
            // |       |         |   |
            // |   x   |    x    |   | ch_box_height
            // |   x   |    x    |   |
            // |   x   |    x    |   |
            // |   x   |   x     |   |
            // |       |  x      |   |
            // +-------+---------+---+
            //                     ^--- image_width - width_ch_i - width_ch_j
            for i in 0..ch_box_height {
                let mut x = 0;
                for &(width, height, ref data) in chars_row {
                    if i >= height {
                        image.extend(repeat(0).take(width as usize));
                    } else {
                        let skip = i * width;
                        debug_assert!(data.len() >= (skip + width) as usize);
                        let line = data.iter().skip(skip as usize).take(width as usize);
                        image.extend(line.cloned());
                    };
                    x += width;
                }
                let cols_to_fill = image_width - x;
                image.extend(repeat(0).take(cols_to_fill as usize));
            }
        };

        // debug!("Placing chars onto a plane");

        // Hashmap doesn't preserve the order but we don't need it anyway.
        for (_, ch_info) in chars_info.iter_mut() {
            if cursor_x + ch_info.width > image_width {
                dump_row(&mut image, &chars_row);
                chars_row.clear();
                cursor_x = 0;
                image_height += ch_box_height;
            }
            let ch_data = ch_info.data.take().unwrap();
            chars_row.push((ch_info.width, ch_info.height, ch_data));
            ch_info.tex = [cursor_x as f32, image_height as f32];
            cursor_x += ch_info.width;
        }
        dump_row(&mut image, &chars_row);
        image_height += ch_box_height;

        // Finally, we just precalculate some fields to make it easier to use
        // our font.

        for (_, ch_info) in chars_info.iter_mut() {
            ch_info.tex[0] /= image_width as f32;
            ch_info.tex[1] /= image_height as f32;
            ch_info.tex_width = ch_info.width as f32 / image_width as f32;
            ch_info.tex_height = ch_info.height as f32 / image_height as f32;
        }

        // info!("Image width: {}, image height: {}, total size: {}",
        //     image_width, image_height, image.len());

        Ok(BitmapFont {
            width: image_width as u16,
            height: image_height as u16,
            chars: chars_info,
            image,
            font_height: ((face.size_metrics().unwrap().height + 32) >> 6) as u16,
        })
    }

    pub fn get_width(&self) -> u16 {
        self.width
    }

    pub fn get_height(&self) -> u16 {
        self.height
    }

    /// Return 8-bit texture raw data (grayscale).
    pub fn get_image(&self) -> &[u8] {
        &self.image
    }

    pub fn get_font_height(&self) -> u16 {
        self.font_height
    }

    pub fn find_char(&self, ch: char) -> Option<&BitmapChar> {
        self.chars.get(&ch)
    }

    /// Get the bounding box size of a string as rendered by this font.
    pub fn measure(&self, text: &str) -> (i32, i32) {
        let mut width = 0;
        let mut max_width = 0;
        let mut lines = 1;

        for ch in text.chars() {
            if ch == '\n' {
                lines += 1;
                max_width = max_width.max(width);
                width = 0;
            } else if ch == '\t' {
                if let Some(ch_info) = self.find_char(' ') {
                    width += ch_info.x_advance * 4;
                }
            } else {
                let ch_info = match self.find_char(ch) {
                    Some(info) => info,
                    None => continue,
                };
                width += ch_info.x_advance;
            }
        }
        max_width = max_width.max(width);

        (max_width, lines * (self.get_font_height() as i32))
    }

    pub fn measure_each_char(&self, text: &str) -> (Vec<i16>, i32) {
        let mut pos_px = Vec::with_capacity(text.len());

        for ch in text.chars() {
            let ch_info = match self.find_char(ch) {
                Some(info) => info,
                None => continue,
            };
            pos_px.push(ch_info.x_advance as i16);
        }

        (pos_px, self.get_font_height() as i32)
    }
}