/* This file is part of Zutty. * Copyright (C) 2020 Tom Szilagyi * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * See the file LICENSE for the full license. */ #include "font.h" #include "log.h" #include "options.h" #include "utf8.h" #include #include #include #include #include #include namespace zutty { Font::Font (const std::string& filename_) : filename (filename_) , overlay (false) { load (); } Font::Font (const std::string& filename_, const Font& priFont, Overlay_) : filename (filename_) , overlay (true) , px (priFont.getPx ()) , py (priFont.getPy ()) , baseline (priFont.getBaseline ()) , ulTop (priFont.getUlTop ()) , ulThick (priFont.getUlThick ()) , nx (priFont.getNx ()) , ny (priFont.getNy ()) , atlasBuf (priFont.getAtlas ()) , atlasMap (priFont.getAtlasMap ()) { load (); } Font::Font (const std::string& filename_, const Font& priFont, DoubleWidth_) : filename (filename_) , dwidth (true) , px (2 * priFont.getPx ()) , py (priFont.getPy ()) { load (); } // private methods bool Font::isLoadableChar (FT_ULong c) { if (c == Missing_Glyph_Marker) return true; if (c == Unicode_Replacement_Character) return true; return ((dwidth && wcwidth (c) == 2) || (!dwidth && wcwidth (c) < 2)); } void Font::load () { FT_Library ft; FT_Face face; if (FT_Init_FreeType (&ft)) throw std::runtime_error ("Could not initialize FreeType library"); logI << "Loading " << filename << " as " << (overlay ? "overlay" : (dwidth ? "double-width" : "primary")) << std::endl; if (FT_New_Face (ft, filename.c_str (), 0, &face)) throw std::runtime_error (std::string ("Failed to load font ") + filename); /* Determine the number of glyphs to actually load, based on wcwidth () * We need this number up front to compute the atlas geometry. */ int num_glyphs = 0; { FT_UInt gindex; FT_ULong charcode = FT_Get_First_Char (face, &gindex); while (gindex != 0) { if (isLoadableChar (charcode)) ++ num_glyphs; charcode = FT_Get_Next_Char (face, charcode, &gindex); } } logT << "Family: " << face->family_name << "; Style: " << face->style_name << "; Faces: " << face->num_faces << "; Glyphs: " << num_glyphs << " to load (" << face->num_glyphs << " total)" << std::endl; if (face->num_fixed_sizes > 0) loadFixed (face); else loadScaled (face); /* Given that we have num_glyphs glyphs to load, with each * individual glyph having a size of px * py, compute nx and ny so * that the resulting atlas texture geometry is closest to a square. * We use one extra glyph space to guarantee a blank glyph at (0,0). */ if (!overlay) { unsigned n_glyphs = num_glyphs + 1; unsigned long total_pixels = n_glyphs * px * py; double side = sqrt (total_pixels); nx = side / px; ny = side / py; while ((unsigned) nx * ny < n_glyphs) { if (px * nx < py * ny) ++nx; else ++ny; } if (nx > 255 || ny > 255) { logE << "Atlas geometry not addressable by single byte coords. " << "Please report this as a bug with your font attached!" << std::endl; throw std::runtime_error ("Impossible atlas geometry"); } logT << "Atlas texture geometry: " << nx << "x" << ny << " glyphs of " << px << "x" << py << " each, " << "yielding pixel size " << nx*px << "x" << ny*py << "." << std::endl; logT << "Atlas holds space for " << nx*ny << " glyphs, " << n_glyphs << " will be used, empty: " << nx*ny - n_glyphs << " (" << 100.0 * (nx*ny - n_glyphs) / (nx*ny) << "%)" << std::endl; size_t atlas_bytes = nx * px * ny * py; logT << "Allocating " << atlas_bytes << " bytes for atlas buffer" << std::endl; atlasBuf.resize (atlas_bytes, 0); } FT_UInt gindex; FT_ULong charcode = FT_Get_First_Char (face, &gindex); while (gindex != 0) { if (isLoadableChar (charcode)) { if (overlay) { const auto& it = atlasMap.find (charcode); if (it != atlasMap.end ()) { loadFace (face, charcode, it->second); } } else { loadFace (face, charcode); } } charcode = FT_Get_Next_Char (face, charcode, &gindex); } if (loadSkipCount) { logI << "Skipped loading " << loadSkipCount << " code point(s) " << "outside the Basic Multilingual Plane" << std::endl; } FT_Done_Face (face); FT_Done_FreeType (ft); } void Font::loadFixed (const FT_Face& face) { int bestIdx = -1; int bestHeightDiff = std::numeric_limits::max (); { std::ostringstream oss; oss << "Available sizes:"; for (int i = 0; i < face->num_fixed_sizes; ++i) { oss << " " << face->available_sizes[i].width << "x" << face->available_sizes[i].height; int diff = abs (opts.fontsize - face->available_sizes[i].height); if (diff < bestHeightDiff) { bestIdx = i; bestHeightDiff = diff; } } logT << oss.str () << std::endl; } logT << "Configured size: " << (int)opts.fontsize << "; Best matching fixed size: " << face->available_sizes[bestIdx].width << "x" << face->available_sizes[bestIdx].height << std::endl; if (bestHeightDiff > 1 && face->units_per_EM > 0) { logT << "Size mismatch too large, fallback to rendering outlines." << std::endl; loadScaled (face); return; } const auto& facesize = face->available_sizes [bestIdx]; if (overlay || dwidth) { if (px != facesize.width) throw std::runtime_error ( filename + ": size mismatch, expected px=" + std::to_string (px) + ", got: " + std::to_string (facesize.width)); if (py != facesize.height) throw std::runtime_error ( filename + ": size mismatch, expected py=" + std::to_string (py) + ", got: " + std::to_string (facesize.height)); } else { px = facesize.width; py = facesize.height; baseline = 0; ulTop = py - 1.0; ulThick = 1.0; } logI << "Glyph size " << px << "x" << py << std::endl; if (FT_Set_Pixel_Sizes (face, px, py)) throw std::runtime_error ("Could not set pixel sizes"); if (!overlay && face->height) { // If we are loading a fixed bitmap strike of an otherwise scaled // font, we need the baseline as well as the underline metrics. double tpy_asc = opts.fontsize * (double)face->ascender / face->units_per_EM; baseline = trunc (tpy_asc); double utop = opts.fontsize * (double)face->underline_position / face->units_per_EM; double uthick = opts.fontsize * (double)face->underline_thickness / face->units_per_EM; ulTop = baseline - utop - uthick / 2.0; ulThick = uthick; logI << "Baseline " << baseline << ", underline top at " << ulTop << " thickness " << ulThick << std::endl; } } void Font::loadScaled (const FT_Face& face) { logI << "Pixel size " << (int)opts.fontsize << std::endl; if (FT_Set_Pixel_Sizes (face, 0, opts.fontsize)) throw std::runtime_error ("Could not set pixel sizes"); double tpx = opts.fontsize * (double)face->max_advance_width / face->units_per_EM; double tpy_asc = opts.fontsize * (double)face->ascender / face->units_per_EM; double tpy_desc = opts.fontsize * (double)face->descender / face->units_per_EM; if (!overlay && !dwidth) { px = trunc (tpx); py = trunc (tpy_asc) + trunc (-tpy_desc); } if (!overlay) { baseline = trunc (tpy_asc); } logI << "Glyph size " << px << "x" << py << ", baseline " << baseline << std::endl; double utop = opts.fontsize * (double)face->underline_position / face->units_per_EM; double uthick = opts.fontsize * (double)face->underline_thickness / face->units_per_EM; ulTop = baseline - utop - uthick / 2.0; ulThick = uthick; logI << "Underline top at " << ulTop << " thickness " << ulThick << std::endl; } void Font::loadFace (const FT_Face& face, FT_ULong c) { const uint8_t atlas_row = atlas_seq / nx; const uint8_t atlas_col = atlas_seq - nx * atlas_row; const AtlasPos apos = {atlas_col, atlas_row}; loadFace (face, c, apos); atlasMap [c] = apos; ++atlas_seq; } void Font::loadFace (const FT_Face& face, FT_ULong c, const AtlasPos& apos) { if (c > std::numeric_limits::max ()) { #ifdef DEBUG logT << "Skip loading code point 0x" << std::hex << c << std::dec << " outside the Basic Multilingual Plane" << std::endl; #endif ++loadSkipCount; return; } if (FT_Load_Char (face, c, FT_LOAD_RENDER)) { throw std::runtime_error ( std::string ("FreeType: Failed to load glyph for char ") + std::to_string (c)); } // destination pixel offset int dx = face->glyph->bitmap_left; int dy = baseline > 0 ? baseline - face->glyph->bitmap_top : 0; // source skip horiz and vert const int sh = std::max (0, -dy); const int sw = std::max (0, -dx); dx += sw; dy += sh; // raw/rasterized bitmap dimensions const auto& bmp = face->glyph->bitmap; const int bh = std::min ({(int)bmp.rows - sh, py - dy}); const int bw = std::min ({(int)bmp.width - sw, px - dx}); const int atlas_row_offset = nx * px * py; const int atlas_glyph_offset = apos.y * atlas_row_offset + apos.x * px; const int atlas_write_offset = atlas_glyph_offset + nx * px * dy + dx; if (overlay) // clear glyph area, as we are overwriting an existing glyph { for (int j = 0; j < py; ++j) { uint8_t* atl_dst_row = atlasBuf.data () + atlas_glyph_offset + j * nx * px; for (int k = 0; k < px; ++k) { *atl_dst_row++ = 0; } } } /* Load bitmap into atlas buffer area. Each row in the bitmap * occupies bitmap.pitch bytes (with padding); this is the * increment in the input bitmap array per row. * * Interpretation of bytes within the bitmap rows is subject to * bitmap.pixel_mode, essentially either 8 bits (256-scale gray) * per pixel, or 1 bit (mono) per pixel. Leftmost pixel is MSB. * */ const uint8_t* bmp_src_row; uint8_t* atl_dst_row; switch (bmp.pixel_mode) { case FT_PIXEL_MODE_MONO: for (int j = sh; j < bh; ++j) { bmp_src_row = bmp.buffer + j * bmp.pitch; atl_dst_row = atlasBuf.data () + atlas_write_offset + j * nx * px; uint8_t byte = 0; for (int k = 0; k < bw; ++k) { if (k % 8 == 0) byte = *bmp_src_row++; if (k >= sw) *atl_dst_row++ = (byte & 0x80) ? 0xFF : 0; byte <<= 1; } } break; case FT_PIXEL_MODE_GRAY: for (int j = 0; j < bh; ++j) { bmp_src_row = bmp.buffer + (j + sh) * bmp.pitch + sw; atl_dst_row = atlasBuf.data () + atlas_write_offset + j * nx * px; for (int k = 0; k < bw; ++k) { *atl_dst_row++ = *bmp_src_row++; } } break; default: throw std::runtime_error ( std::string ("Unhandled pixel_type=") + std::to_string (bmp.pixel_mode)); } } } // namespace zutty