You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
421 lines
18 KiB
421 lines
18 KiB
#include "TextRasterizer.h" |
|
#include <string> |
|
#include <cmath> |
|
#include <limits> |
|
#include <iostream> |
|
|
|
#include "../../environment/Environment.h" |
|
|
|
TextRasterizer::TextRasterizer(const std::string &fontPath, const int size, const unsigned int resolution, const bool bold) { |
|
fontSize = size; |
|
FT_Init_FreeType(&lib); |
|
|
|
face = std::make_unique<FT_Face>(); |
|
std::string path = Environment::getResourceDir() + "/" + fontPath; |
|
//std::cout << "font path [" << path << "]" << std::endl; |
|
if (bold) { |
|
path = path.substr(0, path.length() -4) + "-Bold.ttf"; |
|
} |
|
//std::cout << "font path [" << path << "]" << std::endl; |
|
if (FT_New_Face(lib, path.c_str(), 0, face.get())) { |
|
std::cout << "Could not open font, bold: " << bold << std::endl; |
|
return; |
|
} |
|
if (!isUnicodeBMP(*face)) { |
|
std::cout << "Font is not Unicode BMP" << std::endl; |
|
return; |
|
} |
|
if (FT_Set_Char_Size(*face, 0, fontSize * 64, resolution, resolution)) { |
|
std::cout << "Could not set font size" << std::endl; |
|
return; |
|
} |
|
|
|
font = hb_ft_font_create(*face, nullptr); |
|
|
|
buffer = hb_buffer_create(); |
|
if (!hb_buffer_allocation_successful(buffer)) { |
|
std::cout << "Could not create HarfBuzz buffer" << std::endl; |
|
return; |
|
} |
|
} |
|
|
|
TextRasterizer::~TextRasterizer() { |
|
hb_buffer_destroy(buffer); |
|
hb_font_destroy(font); |
|
FT_Done_FreeType(lib); |
|
} |
|
|
|
std::unique_ptr<sizeResponse> TextRasterizer::size(const rasterizationRequest &request) const { |
|
if (!request.availableWidth) { |
|
// window is likely minimized, so no output |
|
return nullptr; |
|
} |
|
if (request.startX == request.availableWidth) { |
|
std::cout << "TextRasterizer::size - x [" << static_cast<int>(request.startX) << "] matches window width [" << static_cast<int>(request.availableWidth)<< "] for text[" << request.text << "] no room to render anything" << std::endl; |
|
return nullptr; |
|
|
|
} |
|
if (request.startX > request.availableWidth) { |
|
std::cout << "TextRasterizer::size - x [" << static_cast<int>(request.startX) << "] requested outside of window width [" << static_cast<int>(request.availableWidth)<< "] for text[" << request.text << "]" << std::endl; |
|
return nullptr; |
|
} |
|
if (!buffer) { |
|
std::cout << "TextRasterizer::size - no font loaded" << std::endl; |
|
return nullptr; |
|
} |
|
|
|
unsigned int glyphCount; |
|
|
|
hb_buffer_reset(buffer); |
|
hb_buffer_set_direction(buffer, HB_DIRECTION_LTR); |
|
hb_buffer_set_language(buffer, hb_language_from_string("en", 2)); |
|
|
|
hb_buffer_add_utf8(buffer, request.text.c_str(), request.text.length(), 0, request.text.length()); |
|
|
|
#pragma GCC diagnostic push |
|
#pragma GCC diagnostic ignored "-Wold-style-cast" |
|
const hb_feature_t KerningOn = { HB_TAG('k', 'e', 'r', 'n'), 1, 0, std::numeric_limits<unsigned int>::max() }; |
|
#pragma GCC diagnostic pop |
|
hb_shape(font, buffer, &KerningOn, 1); |
|
|
|
const hb_glyph_info_t *glyphInfo = hb_buffer_get_glyph_infos(buffer, &glyphCount); |
|
const hb_glyph_position_t *glyphPos = hb_buffer_get_glyph_positions(buffer, &glyphCount); |
|
|
|
std::unique_ptr<sizeResponse> response = std::make_unique<sizeResponse>(); |
|
|
|
// figure out width/height |
|
int cx = 0; |
|
int cy = 0; |
|
int xmax = 0; |
|
//int y0max = 0; |
|
int y1max = 0; |
|
//int lines = 1; |
|
response->wrapped = false; |
|
int lineXStart = request.startX; |
|
//int leftPadding = 0; |
|
//int wrapToX = 0; |
|
int maxy0 = 0; |
|
for (unsigned int i = 0; i < glyphCount; i++) { |
|
if (FT_Load_Glyph(*face, glyphInfo[i].codepoint, FT_LOAD_DEFAULT)) { |
|
std::cout << "Could not load glyph" << std::endl; |
|
return nullptr; |
|
} |
|
const FT_GlyphSlot slot = (*face)->glyph; |
|
|
|
if (FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL)) { |
|
std::cout << "Could not render glyph" << std::endl; |
|
return nullptr; |
|
} |
|
|
|
// how much space does this character take |
|
const float xa = static_cast<float>(glyphPos[i].x_advance) / 64; |
|
const float ya = static_cast<float>(glyphPos[i].y_advance) / 64; //mostly 0s |
|
|
|
const float yo = static_cast<float>(glyphPos[i].y_offset) / 64; |
|
int y0 = static_cast<int>(floor(yo + slot->bitmap_top)); |
|
|
|
//miny0 = std::min(y0, miny0); |
|
maxy0 = std::max(y0, maxy0); |
|
|
|
// do we need to padding the texture to the left for any lines |
|
if (cx == 0) { |
|
if (slot->bitmap_left < 0) { |
|
// figure out max amount of padding we need |
|
response->leftPadding = std::max(response->leftPadding, -slot->bitmap_left); |
|
} |
|
} |
|
|
|
// manual request to wrap (new line) |
|
//std::cout << "char " << request.text[i] << " codepoint: " << glyphInfo[i].codepoint << std::endl; |
|
if (request.text[i] == '\n' || request.text[i] == '\r'){ |
|
//std::cout << "new line|return" << std::endl; |
|
response->wrapped = true; |
|
if (request.noWrap) { |
|
glyphCount = i; |
|
// we're done |
|
std::cout << "TextRasterizer::size - newline found no wrap is on [" << request.text << "]" << std::endl; |
|
break; |
|
} else { |
|
xmax=std::max(xmax, cx); |
|
cx = response->wrapToX; |
|
cy -= std::ceil(1.2f * fontSize); // 1.2 scalar from https://developer.mozilla.org/en-US/docs/Web/CSS/line-height |
|
response->lines++; |
|
lineXStart = response->wrapToX; // change where we start |
|
// don't add this character |
|
continue; |
|
} |
|
} |
|
|
|
// wrap to next line on width |
|
if (cx + lineXStart >= request.availableWidth) { |
|
response->wrapped = true; |
|
if (request.noWrap) { |
|
glyphCount = i; |
|
} else { |
|
//std::cout << "multine text: [" << text << "] new line:" << cy << " x: " << (int)x << "+ cx:" << (int)cx << std::endl; |
|
//std::cout << "line #" << lines << " starts at " << lineXStart << " ends at " << lineXStart + cx << std::endl; |
|
xmax = request.availableWidth - response->wrapToX; // the whole width of parent to the edge of windowWidth |
|
//std::cout << "new width: " << xmax << std::endl; |
|
cx = response->wrapToX; // wrapToX (was 0) (was -= xmax) |
|
cy -= std::ceil(1.2f * fontSize); // 1.2 scalar from https://developer.mozilla.org/en-US/docs/Web/CSS/line-height |
|
response->lines++; |
|
lineXStart = response->wrapToX; // change where we start |
|
} |
|
} |
|
|
|
//std::cout << "glyph:" << xa << "x" << ya << std::endl; |
|
|
|
// update glyph space allocation |
|
cx += xa; |
|
// note all over cy is in - |
|
cy += ya; // is normal for y0 at bottom |
|
|
|
// update glyph maxes |
|
const FT_Bitmap ftBitmap = slot->bitmap; |
|
//const float yo = static_cast<float>(glyphPos[i].y_offset) / 64; |
|
//int y0 = static_cast<int>(floor(yo + slot->bitmap_top)); |
|
int y1 = y0 + static_cast<int>(ftBitmap.rows); |
|
response->y0max = std::max(response->y0max, y0); |
|
y1max=std::max(y1max, y1); |
|
|
|
// track new max width |
|
xmax=std::max(xmax, cx); |
|
|
|
// crop overflow |
|
if (request.cropHeight && cy > request.cropHeight) { |
|
break; // stop adding characters |
|
} |
|
} |
|
if (response->leftPadding) { |
|
xmax += response->leftPadding; // increase width; |
|
} |
|
// at least one line |
|
cy -= std::ceil(1.2f * fontSize); // 1.2 scalar from https://developer.mozilla.org/en-US/docs/Web/CSS/line-height |
|
response->height = -cy; |
|
response->width = xmax; |
|
//std::cout << "lines: " << lines << " wrapToX: " << wrapToX << " startX: " << x << " xmax: " << xmax << std::endl; |
|
// confirmed we aren't the same |
|
/* |
|
if (response->y0max != maxy0) { |
|
std::cout << "TextRasterizer::size - resp.y0max: " << response->y0max << " maxy0: " << maxy0 << std::endl; |
|
} |
|
*/ |
|
//std::cout << "y1max: " << y1max << " lines: " << response->lines << std::endl; |
|
int textureHeight = (y1max - response->y0max) * static_cast<int>(response->lines); |
|
//int textureHeight = y1max * response->lines; |
|
//y1max *= response->lines; |
|
//std::cout << "initial:" << (int)width << "x" << (int)height << std::endl; |
|
if (response->height < textureHeight) { |
|
response->height = textureHeight; |
|
//std::cout << "adjusted:" << (int)width << "x" << (int)height << std::endl; |
|
} |
|
|
|
// inline position where we leave off |
|
response->endingX = cx - request.sourceStartX; |
|
response->endingY = cy + 2 + request.sourceStartY; |
|
|
|
return response; |
|
} |
|
|
|
#include <GL/glew.h> |
|
|
|
std::unique_ptr<rasterizationResponse> TextRasterizer::rasterize(const rasterizationRequest &request) const { |
|
|
|
if (request.startX == request.availableWidth) { |
|
std::cout << "TextRasterizer::rasterize - x [" << static_cast<int>(request.startX) << "] matches window width [" << static_cast<int>(request.availableWidth)<< "] for text[" << request.text << "] no room to render anything" << std::endl; |
|
return nullptr; |
|
|
|
} |
|
if (request.startX > request.availableWidth) { |
|
std::cout << "TextRasterizer::rasterize - x [" << static_cast<int>(request.startX) << "] requested outside of window width [" << static_cast<int>(request.availableWidth)<< "] for text[" << request.text << "]" << std::endl; |
|
return nullptr; |
|
} |
|
if (!buffer) { |
|
std::cout << "TextRasterizer::rasterize - no font loaded" << std::endl; |
|
return nullptr; |
|
} |
|
unsigned int glyphCount; |
|
|
|
hb_buffer_reset(buffer); |
|
hb_buffer_set_direction(buffer, HB_DIRECTION_LTR); |
|
hb_buffer_set_language(buffer, hb_language_from_string("en", 2)); |
|
|
|
hb_buffer_add_utf8(buffer, request.text.c_str(), request.text.length(), 0, request.text.length()); |
|
|
|
#pragma GCC diagnostic push |
|
#pragma GCC diagnostic ignored "-Wold-style-cast" |
|
const hb_feature_t KerningOn = { HB_TAG('k', 'e', 'r', 'n'), 1, 0, std::numeric_limits<unsigned int>::max() }; |
|
#pragma GCC diagnostic pop |
|
hb_shape(font, buffer, &KerningOn, 1); |
|
|
|
const hb_glyph_info_t *glyphInfo = hb_buffer_get_glyph_infos(buffer, &glyphCount); |
|
const hb_glyph_position_t *glyphPos = hb_buffer_get_glyph_positions(buffer, &glyphCount); |
|
|
|
std::unique_ptr<rasterizationResponse> response = std::make_unique<rasterizationResponse>(); |
|
std::unique_ptr<sizeResponse> sizeResponse = this->size(request); |
|
|
|
response->width = sizeResponse->width; |
|
response->height = sizeResponse->height; |
|
|
|
//std::cout << "after adjust:" << (int)response->width << "x" << (int)response->height << " lines" << lines << std::endl; |
|
// adjust sourceStart |
|
//std::cout << "TextRasterizer::rasterize - adjust sourceStart: " << request.sourceStartX << "," << request.sourceStartY << std::endl; |
|
response->width -= request.sourceStartX; |
|
response->height -= request.sourceStartY; |
|
//std::cout << "TextRasterizer::rasterize - final:" << static_cast<int>(response->width) << "x" << static_cast<int>(response->height) << std::endl; |
|
|
|
/* |
|
if (xmax==windowWidth - x) { |
|
std::cout << "Wrapped text[" << text << "] over " << lines << " lines " << xmax << "x" << static_cast<int>(height) << std::endl; |
|
} |
|
*/ |
|
//std::cout << "text size: " << (int)width << "x" << (int)height << std::endl; |
|
|
|
response->textureWidth = pow(2, ceil(log(response->width) / log(2))); |
|
response->textureHeight = pow(2, ceil(log(response->height) / log(2))); |
|
//std::cout << "text texture size:" << line->textureWidth << "x" << line->textureHeight << std::endl; |
|
size_t size = static_cast<size_t>(response->textureWidth * response->textureHeight); // here? |
|
response->textureData = std::make_unique<unsigned char[]>(size); // here? |
|
if (!response->textureData) { |
|
std::cout << "failed to create texture" << static_cast<int>(response->width) << "X" << static_cast<int>(response->height) << std::endl; |
|
return nullptr; |
|
} |
|
|
|
// translation information |
|
response->x0 = -sizeResponse->leftPadding; // wrap to element start (wrapToX (0) or x) |
|
response->y0 = 0; |
|
response->x1 = -sizeResponse->leftPadding + response->width; |
|
response->y1 = -response->height; |
|
//std::cout << "xd: " << static_cast<int>(response->x1-response->x0) << " yd: " << static_cast<int>(response->y0-response->y1) << std::endl; |
|
|
|
//std::cout << "Requesting texture of " << response->textureWidth << "x" << response->textureHeight << "max: " << win->maxTextureSize << std::endl; |
|
if (response->textureHeight > request.maxTextureSize) { |
|
std::cout << "Truncating text texture height to fit in your video card: " << response->textureHeight << ">" << request.maxTextureSize << std::endl; |
|
response->textureHeight = request.maxTextureSize; |
|
} |
|
|
|
// texture coords |
|
response->s0 = 0.0f; |
|
response->t0 = 0.0f; |
|
response->s1 = response->width / response->textureWidth; |
|
response->t1 = response->height / response->textureHeight; |
|
//std::cout << "s1: " << response->s1 << " t1: " << response->t1 << std::endl; |
|
|
|
// copy all glyphs into one single glyph |
|
// still neds to start at X |
|
int cx = response->wrapped ? request.startX : 0; // reset |
|
int cy = 0; |
|
//std::cout << "[" << request.text << "] wrapped? " << response->wrapped << " starting at: " << cx << std::endl; |
|
//int miny0 = 99; |
|
int maxy0 = 0; |
|
for (unsigned int i = 0; i < glyphCount; i++) { |
|
if (FT_Load_Glyph(*face, glyphInfo[i].codepoint, FT_LOAD_DEFAULT)) { |
|
std::cout << "Could not load glyph" << std::endl; |
|
return nullptr; |
|
} |
|
const FT_GlyphSlot slot = (*face)->glyph; |
|
|
|
if (FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL)) { |
|
std::cout << "Could not render glyph" << std::endl; |
|
return nullptr; |
|
} |
|
|
|
const FT_Bitmap ftBitmap = slot->bitmap; |
|
|
|
// figure out glyph starting point |
|
const float yo = static_cast<float>(glyphPos[i].y_offset) / 64; |
|
int y0 = static_cast<int>(floor(yo + slot->bitmap_top)); |
|
|
|
//miny0 = std::min(y0, miny0); |
|
maxy0 = std::max(y0, maxy0); |
|
|
|
int bump = sizeResponse->y0max - y0; // Y adjust for this glyph |
|
const float xa = static_cast<float>(glyphPos[i].x_advance) / 64; |
|
|
|
if (request.text[i] == '\n' || request.text[i] == '\r'){ |
|
//std::cout << "new line" << std::endl; |
|
if (request.noWrap) { |
|
// we're done |
|
//std::cout << "TextRasterize::rasterize - noWrap on, ending" << std::endl; |
|
break; |
|
} else { |
|
cx = sizeResponse->wrapToX; |
|
cy += std::ceil(1.2f * fontSize); // 1.2 scalar from https://developer.mozilla.org/en-US/docs/Web/CSS/line-height |
|
// no need to write new line char |
|
continue; |
|
} |
|
} |
|
|
|
// if this char is too width for this line, advance to next line |
|
if (!request.noWrap && cx + xa >= request.availableWidth) { |
|
cx = sizeResponse->wrapToX; |
|
cy += std::ceil(1.2f * fontSize);// 1.2 scalar from https://developer.mozilla.org/en-US/docs/Web/CSS/line-height |
|
//std::cout << "textWrap - cx reset to: " << cx << " cy is now: " << cy << std::endl; |
|
} |
|
//std::cout << "placing glyph[" << text[i] << "] at " << cx << " cy is now: " << cy << std::endl; |
|
|
|
// crop overflow |
|
if (request.cropHeight && cy > request.cropHeight) { |
|
break; // stop adding characters |
|
} |
|
|
|
if (cx < request.sourceStartX) { |
|
// skip ahead |
|
cx += xa; |
|
continue; |
|
} |
|
if (cy < request.sourceStartY) { |
|
// skip ahead |
|
// seems to just an optimization |
|
//std::cout << "cy: " << cy << " < sourceStartY: " << request.sourceStartY << std::endl; |
|
continue; |
|
} |
|
//std::cout << "Writing " << request.text[i] << " to " << cx << ", " << cy << std::endl; |
|
|
|
// place glyph bitmap data into texture |
|
for (unsigned int iy = 0; iy < ftBitmap.rows; iy++) { // line by line |
|
// source is 0 to (0:iy:rows) |
|
// dest is cx+bl, (0:iy:rows)+(0:cy:height)+bump |
|
//std::cout << "placing glyph row at " << (cx + slot->bitmap_left) << "x" << ((iy + cy) + bump) << std::endl; |
|
unsigned int destPos = static_cast<unsigned int>(cx - request.sourceStartX + sizeResponse->leftPadding + slot->bitmap_left) + ((iy + static_cast<unsigned int>(cy - request.sourceStartY)) + static_cast<unsigned int>(bump)) * response->textureWidth; |
|
if (destPos >= size) { |
|
// we're done with this line |
|
continue; |
|
} |
|
unsigned char *src = ftBitmap.buffer + iy * static_cast<unsigned int>(ftBitmap.width); |
|
unsigned char *dest = response->textureData.get() + destPos; |
|
for(unsigned int ix = 0; ix < ftBitmap.width; ix++) { // pixel by pixel |
|
if (destPos + ix >= size) { |
|
// we're done with this line |
|
continue; |
|
} |
|
//std::cout << "putting:" << (int)src[ix] << " over " << (int)dest[ix] << std::endl; |
|
// probably could be or'd |
|
if (src[ix] && src[ix] > dest[ix]) { |
|
dest[ix] = src[ix]; |
|
} |
|
} |
|
} |
|
|
|
cx += xa; |
|
} |
|
//std::cout << "final size: " << (int)width << "x" << (int)height << std::endl; |
|
//std::cout << "at: " << (int)line->x0 << "x" << (int)line->y0 << " to: " << (int)line->x1 << "x" << (int)line->y1 <<std::endl; |
|
response->endingX = cx - request.sourceStartX; // maybe should be one xa less? |
|
//std::ceil(0.5 * 1.2f * fontSize)+2 |
|
// should this be - request.sourceStartY? |
|
response->endingY = cy + maxy0 + request.sourceStartY; // definitely should be one lineheight higher |
|
//std::cout << "ending at " << response->endingX << "," << response->endingY << std::endl; |
|
|
|
return response; |
|
} |
|
|
|
bool TextRasterizer::isUnicodeBMP(const FT_Face &ftFace) const { |
|
for (int i = 0; i < ftFace->num_charmaps; i++) { |
|
if (((ftFace->charmaps[i]->platform_id == 0) && (ftFace->charmaps[i]->encoding_id == 3)) || ((ftFace->charmaps[i]->platform_id == 3) && (ftFace->charmaps[i]->encoding_id == 1))) { |
|
FT_Set_Charmap(ftFace, ftFace->charmaps[i]); |
|
return true; |
|
} |
|
} |
|
return false; |
|
}
|
|
|