/* 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 <algorithm>#include <cmath>#include <limits>#include <sstream>#include <stdexcept>#include <string> 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<int>::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<uint16_t>::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
/* 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 <algorithm>
#include <cmath>
#include <limits>
#include <sstream>
#include <stdexcept>
#include <string>
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<int>::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<uint16_t>::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