Juce Pixel Font (AGG Raster)

I know jules don’t like the oldschool pixel font :slight_smile: but that can be very useful in many circumstances.

AGG included a multi-platform process to do that with a little proprietary format (not documented) and an example for win32 platform. So, i’ve investigate the question for my own application, and this is a little project that i’m happy to offer to you.

This is some little static function decomposed in two parts:

RasterFont
Just a little createFont method that is platform specific (win32) and generate two source code files in the exe directory (one cpp and one header).

//==============================================================================
#include "RasterFont.h"
//==============================================================================
typedef agg::rendering_buffer							buffer;
typedef agg::pixfmt_bgr24								pixel_format;
typedef agg::renderer_base<pixel_format>				render_base;
typedef agg::renderer_scanline_bin_solid<render_base>	renderer_bin;
typedef agg::glyph_raster_bin<agg::rgba8>				glyph_generator;
typedef agg::font_engine_win32_tt_int32					font_engine_type;
typedef agg::font_cache_manager<font_engine_type>		font_manager_type;
//==============================================================================
static String getBinaryFormattedString (agg::pixfmt_bgr24 pixel_format,
										const agg::glyph_cache *glyph,
										const int charnumber,
										const int x, const int y,
										const int rows, const int baseline,
										const int num_bytes)
{
	String bytes = String::empty;
	uint16 width = (uint16)glyph->advance_x;
	int row = 0;
	for (; row < rows; ++row)
	{
		//String line;
		int lastmax = 0;
		int byte = 0;
		for (; byte < num_bytes; ++byte)
		{
			uint8 bits = 0;		// binary accumulator
			uint8 bitcount = 7; // 8 bits counter (7..0)
			int col = x + (8 * byte);
			int max;
			int currentmax = (int)(8 * (byte+1));
			if (num_bytes > 1)
			{
				max = col + jlimit (0, (int)width, (currentmax - lastmax));
				lastmax = currentmax;
			}
			else
				max = col + 8;
			for (; col < max; ++col)
			{
				agg::rgba8 color = pixel_format.pixel(col, row);
				if (color.r == 255 && 
					color.g == 255 && 
					color.b == 255)
				{
					bits &= ~(1 << bitcount);
				}
				else
				if (color.r == 0 && 
					color.g == 0 && 
					color.b == 0)
				{
					bits |=  (1 << bitcount);
				}
				--bitcount;
			}
			bytes += String::formatted(T("0x%02x,"), bits);
		}
	}
	return bytes;
}
//==============================================================================
void RasterFont::createFont (const char* fontname,
							 const double size,
							 const int style,
							 const bool italic)
{
	const int total = 256; // agg proprietary format is ugly limited 
						   // to 255 characters (byte size)

	int frame_width = roundDoubleToInt(size*size);
	int frame_height = frame_width;

	unsigned char* pixel_buffer = new unsigned char[frame_width * frame_height * 3];
	memset(pixel_buffer, 255, frame_width * frame_height * 3);

	buffer rbuf (pixel_buffer, frame_width, frame_height, frame_width * 3);
	rbuf.clear (0);

	pixel_format		pf(rbuf);
    render_base			ren_base(pf);
    ren_base.clear(agg::rgba(1,1,1,0));
    renderer_bin		ren_bin(ren_base);

	HDC dc = ::GetDC(0);
	font_engine_type	engine (dc);
	font_manager_type	manager (engine);

	StringArray exportFont;
	Array<uint16> exportTable;
	uint16 temp = 0;

	String exportname = String (fontname).toLowerCase().removeCharacters(T(" ")) + 
						String((int)size);

	if (style == FW_BOLD)
		exportname += T("_bold");
	if (italic)
		exportname += T("_italic");

	int ascent, descent, baseline;

	engine.resolution (96); // 96 DPI is the standard for Windows OS.
	bool result = engine.create_font (fontname, 
									  agg::glyph_ren_native_mono, 
									  size, 0.0, style, italic, 
									  ANSI_CHARSET, FF_DONTCARE);
	if (result)
    {
		TEXTMETRIC metric;
		GetTextMetrics (dc, &metric);
		ascent = metric.tmAscent;
		descent = metric.tmDescent;
		baseline = ascent - (int)size;

		manager.precache (' ', total);

		int i=32; int max=total;
		for (; i<max; ++i)
		{
			const agg::glyph_cache *glyph = manager.glyph (i);
			if (glyph)
			{
				manager.init_embedded_adaptors (glyph, 0, descent);

				ren_bin.color (agg::rgba8(0,0,0));
				agg::render_scanlines (manager.mono_adaptor (),
									   manager.mono_scanline (),
									   ren_bin);

				int bytes = 0;
				double ax = glyph->advance_x;
				if (ax > 8)
					bytes = roundDoubleToInt(ceil(ax / 8));
				else
					bytes = 1;

				exportTable.add (temp);
				temp += (ascent * bytes) + 1;

				const juce_wchar* c = (const juce_wchar*)&i;
				String character = String(c, 1);

				exportFont.add (T("        ") + String(ax) + String::formatted(T(", // 0x%02x '"), i) + character + T("'"));
				exportFont.add (T("        ") + getBinaryFormattedString(pf, glyph, i, 0, baseline, ascent, descent, bytes));
				exportFont.add (T("        "));

				ren_base.clear(agg::rgba(1,1,1,0));
			}
		}
	}

	delete [] pixel_buffer;
	::ReleaseDC(0, dc);

	/**
		=======================================================
		Auto-generation of ressource source code (header & cpp)
		=======================================================
	*/
	String exportH = String::empty;

	exportH += T("#ifndef __PIXELFONT_") + exportname.toUpperCase() + T("_H__\r\n");
	exportH += T("#define __PIXELFONT_") + exportname.toUpperCase() + T("_H__\r\n");
	exportH += T("\r\n");
	exportH += T("#include \"agg_basics.h\"\r\n");
	exportH += T("\r\n");
	exportH += T("namespace pixelfonts\r\n");
	exportH += T("{\r\n");
	exportH += T("    extern const agg::int8u ") + exportname + T("[];\r\n");
	exportH += T("}\r\n");
	exportH += T("\r\n");
	exportH += T("#endif // __PIXELFONT_") + exportname.toUpperCase() + T("_H__");

	File fileH (File::getSpecialLocation(File::currentApplicationFile).getParentDirectory().getFullPathName() +
										 T("\\") + exportname + T(".h"));
	fileH.replaceWithText (exportH);

	String exportCPP = String::empty;

	exportCPP += T("#include \"") + exportname + T(".h\"\r\n");
	exportCPP += T("\r\n");
	exportCPP += T("namespace pixelfonts\r\n");
	exportCPP += T("{\r\n");
	exportCPP += T("    const agg::int8u ") + exportname + T("[] =\r\n");
	exportCPP += T("    {\r\n");
	exportCPP += T("        ") + String (ascent)	+ T(", ")
							   + String (baseline)	+ T(", ")
							   + String (32)		+ T(", ")
							   + String (total)		+ T("-32,\r\n");
	exportCPP += T("\r\n        ");

	int ecnt = 0;
	int ecpp = 0; int emax = exportTable.size();
	for (; ecpp < emax; ++ecpp)
	{
		uint8 byte1 = exportTable[ecpp] & 0x00FF;
		uint8 byte2 = exportTable[ecpp] >> 8;

		if (ecnt > 8)
		{
			exportCPP += T("\r\n        ");
			ecnt = 0;
		}

		exportCPP += String::formatted(T("0x%02x,"), byte1);
		exportCPP += String::formatted(T("0x%02x,"), byte2);

		++ecnt;
	}
	exportCPP += T("\r\n\r\n");

	ecpp = 0; emax = exportFont.size();
	for (; ecpp < emax; ++ecpp)
		exportCPP += exportFont[ecpp] + T("\r\n");

	exportCPP += T("        0\r\n");
	exportCPP += T("    };\r\n");
	exportCPP += T("}");

	File fileCPP (File::getSpecialLocation(File::currentApplicationFile).getParentDirectory().getFullPathName() +
										   T("\\") + exportname + T(".cpp"));
	fileCPP.replaceWithText (exportCPP);
}
//==============================================================================

I’m not sure that work with all font, but i’ve include in the project dozen of font converted by.

RasterText
This is the crossplatform part composed of 3 statics methods :
getTextWidth () - return the width of the text in pixel for the specified pixel font.
getTextHeight () - return the height in pixel for the specified pixel font.
drawText () - draw the text with the specified pixel font in the provided Graphics.

//==============================================================================
#include "RasterText.h"
//==============================================================================
typedef agg::pixfmt_alpha_blend_rgba <agg::blender_rgba32, 
									  agg::rendering_buffer, 
									  agg::pixel32_type> pixelformat;
typedef agg::renderer_base <pixelformat> renderer;
typedef agg::glyph_raster_bin <agg::rgba8> generator;
typedef agg::renderer_scanline_aa_solid <renderer> renderer_solid;
typedef agg::span_allocator <agg::rgba8> span_alloc_type;
typedef agg::span_solid <agg::rgba8> span_generator_type;
typedef agg::renderer_scanline_aa <renderer, span_alloc_type, span_generator_type> renderer_type;
//==============================================================================
double RasterText::getTextWidth (String text, const agg::int8u* font)
{
	generator glyph (font);
	return glyph.width ((const juce_wchar *)text);
}
//==============================================================================
double RasterText::getTextHeight (const agg::int8u* font)
{
	generator glyph (font);
	return glyph.height ();
}
//==============================================================================
void RasterText::drawText (Graphics &g,
						   Rectangle<int> &bounds,
						   const agg::int8u* font,
						   String text, 
						   Colour textColour)
{
	int x = bounds.getX();
	int y = bounds.getY();
	int w = bounds.getWidth();
	int h = bounds.getHeight();

 	Image dest(Image::ARGB, w, h, true);
	Image::BitmapData datas (dest, 0, 0, w, h, true);
	agg::rendering_buffer buffer (datas.getPixelPointer(0,0), w, h, w * 4);
	buffer.clear (0);

	generator glyph (font);
    pixelformat pixel (buffer);
    renderer baserenderer (pixel);
	renderer_solid ren (baserenderer);
	baserenderer.clear (agg::rgba(0,0,0,0));

    span_alloc_type sa;
	span_generator_type sg;
    renderer_type rb (baserenderer, sa, sg);
	sg.color (agg::rgba(textColour.getFloatBlue(),
						textColour.getFloatGreen(),
						textColour.getFloatRed(),
						textColour.getFloatAlpha()));

    agg::renderer_raster_htext<renderer_type, generator> textrenderer(rb, glyph);
	textrenderer.render_text (0, glyph.base_line(), (const juce_wchar *)text, false);

	g.drawImageAt (&dest, x, y);
}
//==============================================================================

I include some great looking font that directly work on the 3 major OS and included in a specific directory.

[list]

  • Tahoma (the Windows XP font)
  • Tahoma bold (the same in bold condition)
  • Pixel Arial (a close that original Arial font but in small size)
  • and a lot of free font (pixel/bitmap) found on http://www.dafont.com website
    [/list]

I hope someone will find interest with that.

DOWNLOAD

The VS2008++ project, including AGG2.4 and all fonts : HERE
The executable that provide the image showed on top of this page : HERE

.oO* Max *Oo.

[attachment=0]AGGRasterText_src.zip[/attachment]

Pretty neat (though I do have a hatred of bitmap fonts!)

But an even better way to do it would be to extend the CustomTypeface class to hold bitmap data, and that’d let you use them seamlessly with all the normal juce rendering ops.

Yes, you’re right. As you can see the agg engine include a little template to do that (glyph_raster_bin), but it’s not a perfect way to write methods (struct + static … not fully OO). But this is a little trick, not a functional class.

If a lot of people are interested by this type of classe, I can find few hours to do that (extension of CustomTypeface)

Looks neat! I will definitely use it if you do it!

Best

geoffroy

Would there be any way to use this technology (agg+freetype) so that I can get hinted fonts to render in Juce? My 7-pixel high Juce-rendered text looks blurry (and the hinted versions look clean, when I render them elsewhere). Preferably without hacking the Juce sources…

I second this motion and just like my hinted Typeface changes, I have not only figured out a super clean way to make this work, but got FreeType doing not only the outline extraction but also the rendering! The sad but yet cool part is the output is pixel-identical to Juce (lol). So at least it is confirmation that I got it right. I don’t need bitmapped fonts for my app, but I’m a perfectionist and since I often get bored of “real” work I will probably get around to making my FreeType CustomTypeFace properly render bitmap strikes (glyphs which have no outline data).

Jules here is my change to get it to work, it all starts with a little virtual function added to juce_Typeface.h:

    virtual PositionedGlyph* Typeface::createPositionedGlyph (float x, float y, float w, const Font& font, juce_wchar character, int glyph);

You can see where I’m going with this of course. Some private parts of PositionedGlyph will need to be made protected so that it can be subclassed, and of course PositionedGlyph::draw() will need to be made virtual.