Blob Blame Raw

#include <ft2build.h>
#include FT_FREETYPE_H

#include "private.h"
#include "font.h"
#include "drawing.h"



#define FONT_MAP_GLYPHSIZE      64
#define FONT_MAP_BORDERSIZE     16
#define FONT_MAP_CNT            16
#define FONT_MAP_GLYPHWORKSIZE  (FONT_MAP_GLYPHSIZE - FONT_MAP_BORDERSIZE)
#define FONT_MAP_FULLCNT        (FONT_MAP_CNT*FONT_MAP_CNT)
#define FONT_MAP_TEXSIZE        (FONT_MAP_CNT*FONT_MAP_GLYPHSIZE)
#define FONT_MAX_GLYPHS         (256*256)
#define FONT_BASE_SIZE          72
#define FONT_BADCHAR_CODE       0xFFFD



typedef struct _HeliFontMap HeliFontMap;
typedef struct _HeliFontInstance HeliFontInstance;
typedef struct _HeliCharDesc HeliCharDesc;

typedef struct _HeliGlyph {
	HeliFontMap *map;
	HeliFontInstance *fi;
	int code;
	int index;
	double l, t, r, b;
	double advance;
} HeliGlyph;

struct _HeliFontMap {
	HeliFontMap *prev, *next;
	unsigned int texid;
	int count;
	HeliGlyph glyphs[FONT_MAP_FULLCNT];
};

struct _HeliFontInstance {
	char *path;
	FT_Face face;
	double height;
	HeliGlyph *glyphs[FONT_MAX_GLYPHS];
	int refcount;
};

struct _HeliCharDesc {
	HeliGlyph *glyph;
	double pos;
	double x, y;
	HeliCharDesc *next;
};

typedef struct _HeliLineDesc {
	int begin, end;
	double x, y;
	double l, t, r, b;
	double height;
} HeliLineDesc;

struct _Font {
	HeliFontInstance *instance;
};



static int ftInitialized = 0;
static FT_Library ftLibrary = NULL;
static HeliArray fontCache;
static HeliFontMap *fontMapFirst, *fontMapLast;
static HeliGlyph blankGlyph;
static const char blankPath[] = "";
static Font defaultFont = NULL;

static HeliFontMap* createFontMap() {
	HeliFontMap *map = calloc(1, sizeof(*map));
	map->prev = fontMapLast;
	*(fontMapLast ? &fontMapLast->next : &fontMapFirst) = map;
	fontMapLast = map;
	
	for(int i = 0; i < FONT_MAP_FULLCNT; ++i)
		map->glyphs[i].map = map;
	
	glGenTextures(1, &map->texid);
	glBindTexture(GL_TEXTURE_2D, map->texid);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -0.7f);
	
	char *buffer = calloc(1, FONT_MAP_TEXSIZE*FONT_MAP_TEXSIZE);
	int level = 0;
	for(int size = FONT_MAP_TEXSIZE, border = FONT_MAP_BORDERSIZE; border > 0; border >>= 1, size >>= 1, ++level)
		glTexImage2D(GL_TEXTURE_2D, level, GL_ALPHA, size, size, 0, GL_ALPHA, GL_UNSIGNED_BYTE, buffer);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level - 1);
	free(buffer);
	glBindTexture(GL_TEXTURE_2D, 0);
	
	return map;
}

static void destroyFontMap(HeliFontMap *map) {
	assert(!map->count);
	glDeleteTextures(1, &map->texid);
	*(map->prev ? &map->prev->next : &fontMapFirst) = map->next;
	*(map->next ? &map->next->prev : &fontMapLast ) = map->prev;
	free(map);
}


static HeliGlyph* loadGlyph(HeliFontInstance *fi, int code) {
	int index = FT_Get_Char_Index(fi->face, (unsigned int)code);
	if (!index) return &blankGlyph;
	
	if (FT_Load_Glyph(fi->face, index, FT_LOAD_NO_HINTING))
		return &blankGlyph;
	
	HeliFontMap *map;
	for(map = fontMapFirst; map; map = map->next)
		if (map->count < FONT_MAP_FULLCNT) break;
	if (!map) map = createFontMap();
	
	HeliGlyph *g = NULL;
	for(int i = 0; i < FONT_MAP_FULLCNT; ++i)
		if (!map->glyphs[i].fi) { g = &map->glyphs[i]; break; }
	assert(g);
	
	const double ftkf = 1/64.0;
	const FT_Glyph_Metrics *metrics = &fi->face->glyph->metrics;
	g->l = metrics->horiBearingX*ftkf;
	g->r = (metrics->horiBearingX + metrics->width)*ftkf;
	g->t = -metrics->horiBearingY*ftkf;
	g->b = (-metrics->horiBearingY + metrics->height)*ftkf;
	g->advance = metrics->horiAdvance*ftkf;
	
	unsigned char buffer[FONT_MAP_GLYPHSIZE * FONT_MAP_GLYPHSIZE] = {};
	if (metrics->width > 0 && metrics->height > 0) {
		FT_Matrix matrix = {};
		matrix.xx = (FONT_MAP_GLYPHWORKSIZE-2)*64*65536/metrics->width;
		matrix.yy = (FONT_MAP_GLYPHWORKSIZE-2)*64*65536/metrics->height;
		FT_Vector offset = {};
		offset.x = 64 - metrics->horiBearingX;
		offset.y = -(64 + metrics->horiBearingY - metrics->height);
		FT_Set_Transform(fi->face, &matrix, &offset);
		int success = !FT_Load_Glyph(fi->face, index, FT_LOAD_NO_HINTING)
				&& !FT_Render_Glyph(fi->face->glyph, FT_RENDER_MODE_NORMAL);
		FT_Set_Transform(fi->face, NULL, NULL);
		if (!success) return &blankGlyph;
		
		const FT_Bitmap *bitmap = &fi->face->glyph->bitmap;
		int dr = 0;//fi->face->glyph->bitmap_top + 1 - FONT_MAP_GLYPHWORKSIZE;
		int dc = 0;//-fi->face->glyph->bitmap_left;
		int r0 = -dr;
		int r1 = r0 + bitmap->rows;
		int c0 = -dc;
		int c1 = c0 + bitmap->width;
		if (r0 < 0) r0 = 0;
		if (c0 < 0) c0 = 0;
		if (r1 > FONT_MAP_GLYPHWORKSIZE) r1 = FONT_MAP_GLYPHWORKSIZE;
		if (c1 > FONT_MAP_GLYPHWORKSIZE) c1 = FONT_MAP_GLYPHWORKSIZE;
		for(int r = r0; r < r1; ++r)
			for(int c = c0; c < c1; ++c)
				buffer[r*FONT_MAP_GLYPHSIZE + c] = *(bitmap->buffer + bitmap->pitch*(r + dr) + c + dc);
	}
	
	glBindTexture(GL_TEXTURE_2D, g->map->texid);
	int gi = g - g->map->glyphs;
	int r = gi/FONT_MAP_CNT;
	int c = gi%FONT_MAP_CNT;
	int size = FONT_MAP_GLYPHSIZE;
	for(int level = 0, border = FONT_MAP_BORDERSIZE; border > 0; border >>= 1, ++level) {
		glTexSubImage2D(
			GL_TEXTURE_2D,
			level,
			c*size,
			r*size,
			size,
			size,
			GL_ALPHA,
			GL_UNSIGNED_BYTE,
			buffer );
		if (border > 1) {
			size >>= 1;
			for(int r = 0; r < size; ++r)
				for(int c = 0; c < size; ++c)
					buffer[r*size + c] = (unsigned char)((
					    (unsigned int)buffer[((r*2 + 0)*size + c)*2 + 0]
					  + (unsigned int)buffer[((r*2 + 0)*size + c)*2 + 1]
					  + (unsigned int)buffer[((r*2 + 1)*size + c)*2 + 0]
					  + (unsigned int)buffer[((r*2 + 1)*size + c)*2 + 1] + 2) >> 2 );
		}
	}
	glBindTexture(GL_TEXTURE_2D, 0);
	
	g->fi = fi;
	g->code = code;
	g->index = index;
	++g->map->count;
	return g;
}


static void unloadGlyph(HeliGlyph *g) {
	g->fi = NULL;
	g->code = 0;
	g->index = 0;
	if (g->map && --g->map->count <= 0) 
		destroyFontMap(g->map);
}


static HeliFontInstance* loadFont(const char *path) {
	HeliFontInstance *fi = calloc(1, sizeof(*fi));
	fi->path = heliStringCopy(path);
	
	if (!ftInitialized) {
		if (FT_Init_FreeType(&ftLibrary))
			fprintf(stderr, "helianthus: cannot initialize FreeType library\n");
		ftInitialized = TRUE;
	}
	if (ftLibrary) {
		if (path[0]) {
			if (FT_New_Face(ftLibrary, path, 0, &fi->face)) {
				fprintf(stderr, "helianthus: cannot load font from file: %s\n", path);
				fi->face = NULL;
			}
		} else {
			FT_Open_Args args = {};
			args.flags = FT_OPEN_MEMORY;
			args.memory_base = (FT_Byte*)heliBlobDefaultFont;
			args.memory_size = heliBlobDefaultFontSize;
			if (FT_Open_Face(ftLibrary, &args, 0, &fi->face)) {
				fprintf(stderr, "helianthus: cannot initialize default font\n");
				fi->face = NULL;
			}
		}
		if (fi->face) {
			FT_Set_Char_Size(fi->face, 0, FONT_BASE_SIZE*64, 72, 72);
			fi->height = fi->face->size->metrics.height/64.0;
		}
	}
	
	return fi;
}


static void unloadFont(HeliFontInstance *fi) {
	assert(!fi->refcount);
	for(int i = 0; i < FONT_MAX_GLYPHS; ++i)
		if (fi->glyphs[i]) unloadGlyph(fi->glyphs[i]);
	free(fi->path);
	if (fi->face) FT_Done_Face(fi->face);
	free(fi);
}


static HeliGlyph* getGlyph(HeliFontInstance *fi, int code) {
	if (code < 0 || code >= FONT_MAX_GLYPHS) return &blankGlyph;
	if (!fi->glyphs[code]) fi->glyphs[code] = loadGlyph(fi, code);
	return fi->glyphs[code];
}


Font createFont(const char *path) {
	if (!heliInitialized) return NULL;
	if (!path) path = blankPath;
	Font f = calloc(1, sizeof(*f));
	f->instance = (HeliFontInstance*)heliStringmapGet(&fontCache, path);
	if (!f->instance) {
		f->instance = loadFont(path);
		heliStringmapAdd(&fontCache, path, f->instance, (HeliFreeCallback)&unloadFont);
	}
	++f->instance->refcount;
	heliObjectRegister(f, (HeliFreeCallback)&fontDestroy);
	return f;
}

void fontDestroy(Font f) {
	heliObjectUnregister(f);
	HeliDrawingState *ss = heliDrawingGetState();
	for(HeliDrawingState *s = heliDrawingGetStateStack(); s <= ss; ++s)
		{ if (ss->font == f) ss->font = NULL; }
	--f->instance->refcount;
	if (f->instance->refcount <= 0)
		heliStringmapRemove(&fontCache, f->instance->path);
	free(f);
}

void textAlign(HAlign hor, VAlign vert) {
	HeliDrawingState *s = heliDrawingGetState();
	s->horAlign = hor;
	s->vertAlign = vert;
}
void textFontDefault()
	{ heliDrawingGetState()->font = NULL; }
void textFont(Font f)
	{ heliDrawingGetState()->font = f; }
void textSize(double size)
	{ heliDrawingGetState()->fontSize = size; }


void text(const char *text, double x, double y) {
	resetPath();
	
	HeliDrawingState *drawingState = heliDrawingGetState();
	
	double fontScale = drawingState->fontSize/FONT_BASE_SIZE;
	if (fontScale < HELI_PRECISION) return;
	if (drawingState->colorStroke[3] < HELI_PRECISION) return;
	
	if (!defaultFont) defaultFont = createFont(NULL);
	HeliFontInstance *f = drawingState->font ? drawingState->font->instance : NULL;
	HeliFontInstance *ff = defaultFont ? defaultFont->instance : NULL;
	if (!f) f = ff;
	if (!ff) ff = f;
	if (!f) return;
	
	if (!f->face) f = ff;
	if (!ff->face) ff = f;
	if (!f->face) return;
	
	int linesCount = 1;
	int linesAllocated = 256;
	HeliLineDesc *lines = calloc(linesAllocated, sizeof(HeliLineDesc));
	HeliLineDesc *line = lines;
	line->height = f->height;
	HeliCharDesc *chars = calloc(strlen(text), sizeof(HeliCharDesc));
	
	// gather glyphs
	const char *c = text;
	while(1) {
		int code = FONT_BADCHAR_CODE;
		while((*c & 0xC0) == 0x80) ++c;
		if ((*c & 0x80) == 0x00) {
			code = *c;
		} else
		if ((*c & 0xE0) == 0xC0) {
			if ((c[1] & 0xC0) == 0x80) {
				code = ((c[0] & 0x1F) << 6) | (c[1] & 0x3F);
				c += 1;
			}
		} else
		if ((*c & 0xF0) == 0xE0) {
			if ((c[1] & 0xC0) == 0x80 && (c[2] & 0xC0) == 0x80) {
				code = ((c[0] & 0x0F) << 12) | ((c[1] & 0x3F) << 6) | (c[2] & 0x3F);
				c += 2;
			}
		}
		++c;
		
		if (code == 0) break;
		if (code == 10 || code == 13) {
			if ((code == 10 && *c == 13) || (code == 13 && *c == 10)) ++c;
			if (linesCount >= linesAllocated) {
				linesAllocated += linesAllocated/4;
				lines = realloc(lines, linesAllocated*sizeof(HeliLineDesc));
				memset(lines + linesCount, 0, (linesAllocated - linesCount)*sizeof(HeliLineDesc));
				line = &lines[linesCount - 1];
			}
			++line;
			++linesCount;
			line->begin = line->end = (line - 1)->end;
			line->height = f->height;
			continue;
		}
		
		HeliGlyph *glyph = &blankGlyph;
		if (!glyph->fi) glyph = getGlyph(f, code);
		if (!glyph->fi) glyph = getGlyph(ff, code);
		if (!glyph->fi) glyph = getGlyph(f, FONT_BADCHAR_CODE);
		if (!glyph->fi) glyph = getGlyph(ff, FONT_BADCHAR_CODE);
		if (!glyph->fi) continue;
		
		HeliCharDesc *cc = &chars[line->end++];
		cc->glyph = glyph;
		if (line->height < glyph->fi->height) line->height = glyph->fi->height;
		if (line->end - 1 <= line->begin) {
			line->l = cc->glyph->l;
			line->t = cc->glyph->t;
			line->r = cc->glyph->r;
			line->b = cc->glyph->b;
		} else {
			HeliCharDesc *cp = cc - 1;
			cc->pos = cp->pos + cp->glyph->advance;
			FT_Vector kerning = {};
			if ( cp->glyph->fi == cc->glyph->fi 
			  && !FT_Get_Kerning(
					cc->glyph->fi->face,
					cp->glyph->index,
					cc->glyph->index,
					FT_KERNING_UNFITTED,
					&kerning ))
				cc->pos += kerning.x/64.0;
			if (line->l > cc->glyph->l + cc->pos) line->l = cc->glyph->l + cc->pos;
			if (line->r < cc->glyph->r + cc->pos) line->r = cc->glyph->r + cc->pos;
			if (line->t > cc->glyph->t) line->t = cc->glyph->t;
			if (line->b < cc->glyph->b) line->b = cc->glyph->b;
		}
	}
	
	// bounds
	double l = lines->l;
	double t = lines->t;
	double r = lines->r;
	double b = lines->b;
	for(int i = 1; i < linesCount; ++i) {
		HeliLineDesc *line = &lines[i];
		line->y = (line-1)->y + line->height;
		if (l > line->l) l = line->l;
		if (r < line->r) r = line->r;
		if (t > line->t + line->y) t = line->t + line->y;
		if (b < line->b + line->y) b = line->b + line->y;
	}
	
	// alignment
	double dy = drawingState->vertAlign == VALIGN_BOTTOM ? -b
	          : drawingState->vertAlign == VALIGN_CENTER ? -0.5*(t + b)
	          :                                            -t;
	for(int i = 0; i < linesCount; ++i) {
		HeliLineDesc *line = &lines[i];
		line->x = drawingState->horAlign == HALIGN_RIGHT  ? -line->r
		        : drawingState->horAlign == HALIGN_CENTER ? -0.5*(line->l + line->r)
		        :                                           -line->l;
		line->y += dy;
		for(int j = line->begin; j < line->end; ++j) {
			HeliCharDesc *cc = &chars[j];
			cc->x = cc->pos + line->x;
			cc->y = line->y;
		}
	}
	
	// sort by textures	
	int charsCount = lines[linesCount-1].end;
	for(int i = 0; i < charsCount-1; ++i)
		chars[i].next = &chars[i+1];
	HeliCharDesc *first = NULL, *last = NULL, *current = chars;
	while(current) {
		if (!current->next) {
			current = current->next = first;
		} else
		if (current->glyph->map != current->next->glyph->map) {
			*(last ? &last->next : &first) = current->next;
			last = current->next;
			current->next = current->next->next;
			last->next = NULL;
		} else {
			current = current->next;
		}
	}
	
	// draw
	const double borderK = 0.5*FONT_MAP_BORDERSIZE/FONT_MAP_GLYPHWORKSIZE;
	const double texStep = 1.0/FONT_MAP_CNT;
	const double texOffset = -0.5*FONT_MAP_BORDERSIZE/FONT_MAP_TEXSIZE;
	
	glPushMatrix();
	glTranslated(x, y, 0);
	glScaled(fontScale, fontScale, 1);
	glColor4dv(drawingState->colorStroke);
	HeliFontMap *currentMap = NULL;
	glEnable(GL_TEXTURE_2D);
	glBegin(GL_QUADS);
	for(HeliCharDesc *cc = chars; cc; cc = cc->next) {
		if ( !cc->glyph->map
		  || cc->glyph->r - cc->glyph->l <= HELI_PRECISION
		  || cc->glyph->b - cc->glyph->t <= HELI_PRECISION )
			continue;
		if (currentMap != cc->glyph->map) {
			glEnd();
			currentMap = cc->glyph->map;
			glBindTexture(GL_TEXTURE_2D, currentMap->texid);
			glBegin(GL_QUADS);
		}
		
		double dx = (cc->glyph->r - cc->glyph->l)*borderK;
		double dy = (cc->glyph->b - cc->glyph->t)*borderK;
		double l = cc->x + cc->glyph->l - dx;
		double t = cc->y + cc->glyph->t - dy;
		double r = cc->x + cc->glyph->r + dx;
		double b = cc->y + cc->glyph->b + dy;
		
		int gi = cc->glyph - cc->glyph->map->glyphs;
		double tl = (gi % FONT_MAP_CNT)*texStep + texOffset;
		double tt = (gi / FONT_MAP_CNT)*texStep + texOffset;
		double tr = tl + texStep;
		double tb = tt + texStep;
		
		glTexCoord2d(tl, tt); glVertex2d(l, t);
		glTexCoord2d(tr, tt); glVertex2d(r, t);
		glTexCoord2d(tr, tb); glVertex2d(r, b);
		glTexCoord2d(tl, tb); glVertex2d(l, b);
	}
	glEnd();
	glBindTexture(GL_TEXTURE_2D, 0);
	glDisable(GL_TEXTURE_2D);
	glPopMatrix();
	glColor4d(1, 1, 1, 1);
}


void heliFontFinish() {
	heliArrayDestroy(&fontCache);
	if (ftInitialized && ftLibrary) FT_Done_FreeType(ftLibrary);
	ftLibrary = NULL;
	ftInitialized = FALSE;
}