Download
472 lines 14.3 KiBC++Modified 2026-06-21 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
/**
 * Copyright (c) 2006-2026 LOVE Development Team
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 **/

// LOVE
#include "HarfbuzzShaper.h"
#include "TrueTypeRasterizer.h"
#include "common/Optional.h"

// harfbuzz
#include <hb.h>
#include <hb-ft.h>

#include <algorithm>
#include <cmath>

namespace love
{
namespace font
{
namespace freetype
{

static inline float fixed26_6ToFloat(int32 v)
{
	return (v >> 6) + (v & 63) / 64.0f;
}

static int32 floatToFixed26_6(float v)
{
	float integer = 0;
	float frac = std::modf(v, &integer);

	return (int32)(((uint32)integer << 6) | (uint32)(frac * 64));
}

static float snapToPixel(float v, float dpiScale, float dpiScaleInverse)
{
	return roundf(v * dpiScale) * dpiScaleInverse;
}

HarfbuzzShaper::HarfbuzzShaper(TrueTypeRasterizer *rasterizer)
	: TextShaper(rasterizer)
	, spaceGlyphIndex()
	, tabSpacesAdvanceX(0)
	, tabSpacesAdvanceY(0)
{
	hbFonts.push_back(hb_ft_font_create_referenced((FT_Face)rasterizer->getHandle()));
	hbBuffers.push_back(hb_buffer_create());

	if (hbFonts[0] == nullptr || hbFonts[0] == hb_font_get_empty())
		throw love::Exception("Could not create Harfbuzz font object.");

	if (hbBuffers[0] == nullptr || hbBuffers[0] == hb_buffer_get_empty())
		throw love::Exception("Could not create Harfbuzz buffer object.");

	updateSpacesForTabInfo();
}

HarfbuzzShaper::~HarfbuzzShaper()
{
	for (hb_buffer_t *buffer : hbBuffers)
		hb_buffer_destroy(buffer);
	for (hb_font_t *font : hbFonts)
		hb_font_destroy(font);
}

void HarfbuzzShaper::setFallbacks(const std::vector<Rasterizer*> &fallbacks)
{
	for (size_t i = 1; i < rasterizers.size(); i++)
	{
		hb_buffer_destroy(hbBuffers[i]);
		hb_font_destroy(hbFonts[i]);
	}

	TextShaper::setFallbacks(fallbacks);

	hbFonts.resize(rasterizers.size());
	hbBuffers.resize(rasterizers.size());

	for (size_t i = 1; i < rasterizers.size(); i++)
	{
		hbFonts[i] = hb_ft_font_create_referenced((FT_Face)rasterizers[i]->getHandle());
		hbBuffers[i] = hb_buffer_create();
	}

	updateSpacesForTabInfo();
}

void HarfbuzzShaper::updateSpacesForTabInfo()
{
	if (!isUsingSpacesForTab())
		return;

	hb_codepoint_t glyphid = 0;
	for (size_t i = 0; i < hbFonts.size(); i++)
	{
		hb_font_t *hbfont = hbFonts[i];
		if (hb_font_get_glyph(hbfont, ' ', 0, &glyphid))
		{
			spaceGlyphIndex.index = glyphid;
			spaceGlyphIndex.rasterizerIndex = i;
			tabSpacesAdvanceX = floatToFixed26_6(fixed26_6ToFloat(hb_font_get_glyph_h_advance(hbfont, glyphid)) * SPACES_PER_TAB);
			tabSpacesAdvanceY = floatToFixed26_6(fixed26_6ToFloat(hb_font_get_glyph_v_advance(hbfont, glyphid)) * SPACES_PER_TAB);
			break;
		}
	}
}

bool HarfbuzzShaper::isValidGlyph(uint32 glyphindex, const std::vector<uint32> &codepoints, uint32 codepointindex)
{
	if (glyphindex != 0)
		return true;

	uint32 codepoint = codepoints[codepointindex];
	if (codepoint == '\n' || codepoint == '\r' || (codepoint == '\t' && isUsingSpacesForTab()))
		return true;

	return false;
}

void HarfbuzzShaper::computeBufferRanges(const ColoredCodepoints &codepoints, Range range, std::vector<BufferRange> &bufferranges)
{
	bufferranges.clear();

	if (codepoints.cps.size() == 0)
		return;

	// Less computation for the typical case (no fallback fonts).
	if (rasterizers.size() == 1)
	{
		hb_buffer_reset(hbBuffers[0]);
		hb_buffer_add_codepoints(hbBuffers[0], codepoints.cps.data(), codepoints.cps.size(), (unsigned int)range.getOffset(), (int)range.getSize());

		// TODO: Expose APIs for direction and script?
		hb_buffer_guess_segment_properties(hbBuffers[0]);

		hb_shape(hbFonts[0], hbBuffers[0], nullptr, 0);

		bufferranges.push_back({0, (int) range.first, Range(0, hb_buffer_get_length(hbBuffers[0]))});
		return;
	}

	std::vector<Range> fallbackranges = { range };

	// For each font, figure out the ranges of valid glyphs in the given string,
	// and add the rest to a list to be shaped by the next fallback font.
	// Harfbuzz doesn't have its own fallback API.
	for (size_t rasti = 0; rasti < rasterizers.size(); rasti++)
	{
		hb_buffer_t *hbb = hbBuffers[rasti];
		hb_buffer_reset(hbb);

		for (Range r : fallbackranges)
			hb_buffer_add_codepoints(hbb, codepoints.cps.data(), codepoints.cps.size(), (unsigned int)r.getOffset(), (int)r.getSize());

		hb_buffer_guess_segment_properties(hbb);

		hb_shape(hbFonts[rasti], hbb, nullptr, 0);

		size_t glyphcount = (size_t)hb_buffer_get_length(hbb);
		const hb_glyph_info_t *glyphinfos = hb_buffer_get_glyph_infos(hbb, nullptr);
		hb_direction_t direction = hb_buffer_get_direction(hbb);

		fallbackranges.clear();

		for (size_t i = 0; i < glyphcount; i++)
		{
			uint32 prevCluster = (i > 0) ? glyphinfos[i - 1].cluster : 0;
			uint32 currCluster = glyphinfos[i].cluster;
			// Only merge consecutive glyphs if their clusters are also consecutive
			bool clustersContiguous = (i == 0) || (currCluster == prevCluster + 1) || (currCluster == prevCluster);

			if (isValidGlyph(glyphinfos[i].codepoint, codepoints.cps, glyphinfos[i].cluster))
			{
				bool canExtendPrevious = !bufferranges.empty()
					&& bufferranges.back().index == rasti
					&& bufferranges.back().range.getMax() + 1 == i
					&& clustersContiguous;

				if (canExtendPrevious)
					bufferranges.back().range.last++;
				else
					bufferranges.push_back({rasti, (int)glyphinfos[i].cluster, Range(i, 1)});
			}
			else if (rasti == rasterizers.size() - 1)
			{
				// Use the first font for remaining invalid glyphs when no
				// fallback font supports them.
				bool canExtendPrevious = !bufferranges.empty()
					&& bufferranges.back().index == 0
					&& bufferranges.back().range.getMax() + 1 == i
					&& clustersContiguous;

				if (canExtendPrevious)
					bufferranges.back().range.last++;
				else
					bufferranges.push_back({0, (int)glyphinfos[i].cluster, Range(i, 1)});
			}
			else
			{
				// Harfbuzz puts RTL text into the buffer in reverse order, so
				// it'll start with the last cluster (character index).
				if (fallbackranges.empty() || (direction == HB_DIRECTION_RTL ? fallbackranges.back().getMin() : fallbackranges.back().getMax()) != glyphinfos[i - 1].cluster)
					fallbackranges.push_back(Range(glyphinfos[i].cluster, 1));
				else
					fallbackranges.back().encapsulate(glyphinfos[i].cluster);
			}
		}
	}

	std::sort(bufferranges.begin(), bufferranges.end(), [](const BufferRange &a, const BufferRange &b)
	{
		if (a.codepointStart != b.codepointStart)
			return a.codepointStart < b.codepointStart;
		if (a.index != b.index)
			return a.index < b.index;
		return a.range.first < b.range.first;
	});
}

void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints, Range range, Vector2 offset, float extraspacing, std::vector<GlyphPosition> *positions, std::vector<IndexedColor> *colors, TextInfo *info)
{
	if (!range.isValid() && !codepoints.cps.empty())
		range = Range(0, codepoints.cps.size());

	offset.y += getBaseline();
	Vector2 curpos = offset;
	float spacingremainder = 0;

	int colorindex = 0;
	int ncolors = (int)codepoints.colors.size();
	Optional<Colorf> colorToAdd;

	// Make sure the right color is applied to the start of the glyph list,
	// when the start isn't 0.
	if (colors && range.getOffset() > 0 && !codepoints.colors.empty())
	{
		for (; colorindex < ncolors; colorindex++)
		{
			if (codepoints.colors[colorindex].index >= (int) range.getOffset())
				break;
			colorToAdd.set(codepoints.colors[colorindex].color);
		}
	}

	const float dpiScaleInverse = 1.0f / dpiScale;

	std::vector<BufferRange> bufferranges;
	computeBufferRanges(codepoints, range, bufferranges);

	float maxwidth = curpos.x;

	for (const auto &bufferrange : bufferranges)
	{
		if (positions)
			positions->reserve(positions->size() + bufferrange.range.getSize());

		hb_buffer_t *hbbuffer = hbBuffers[bufferrange.index];

		const hb_glyph_info_t *glyphinfos = hb_buffer_get_glyph_infos(hbbuffer, nullptr);
		hb_glyph_position_t *glyphpositions = hb_buffer_get_glyph_positions(hbbuffer, nullptr);
		hb_direction_t direction = hb_buffer_get_direction(hbbuffer);

		for (size_t i = bufferrange.range.first; i <= bufferrange.range.last; i++)
		{
			const hb_glyph_info_t &info = glyphinfos[i];
			hb_glyph_position_t &glyphpos = glyphpositions[i];

			// TODO: this doesn't handle situations where the user inserted a color
			// change in the middle of some characters that get combined into a single
			// cluster.
			if (colors && colorindex < ncolors && codepoints.colors[colorindex].index == (int)info.cluster)
			{
				colorToAdd.set(codepoints.colors[colorindex].color);
				colorindex++;
			}

			uint32 clustercodepoint = codepoints.cps[info.cluster];

			// Harfbuzz doesn't handle newlines itself, but it does leave them in
			// the glyph list so we can do it manually.
			if (clustercodepoint == '\n')
			{
				maxwidth = std::max(maxwidth, curpos.x);

				// Wrap newline, but do not output a position for it.
				curpos.y += getCombinedHeight();
				curpos.x = offset.x;
				continue;
			}

			// Ignore carriage returns
			if (clustercodepoint == '\r')
				continue;

			// This is a glyph index at this point, despite the name.
			GlyphIndex gindex = { (int) info.codepoint, (int) bufferrange.index };

			if (clustercodepoint == '\t' && isUsingSpacesForTab())
			{
				gindex = spaceGlyphIndex;

				// This should be safe to overwrite.
				// TODO: RTL support?
				glyphpos.x_offset = 0;
				glyphpos.y_offset = 0;
				glyphpos.x_advance = HB_DIRECTION_IS_HORIZONTAL(direction) ? tabSpacesAdvanceX : 0;
				glyphpos.y_advance = HB_DIRECTION_IS_VERTICAL(direction) ? tabSpacesAdvanceY : 0;
			}

			if (colorToAdd.hasValue && colors && positions)
			{
				IndexedColor c = {colorToAdd.value, (int) positions->size()};
				colors->push_back(c);
				colorToAdd.clear();
			}

			if (positions)
			{
				GlyphPosition p = { curpos, gindex };

				// Harfbuzz position coordinate systems are based on the given font.
				// Freetype uses 26.6 fixed point coordinates, so harfbuzz does too.
				p.position.x += fixed26_6ToFloat(glyphpos.x_offset) * dpiScaleInverse;
				p.position.y += fixed26_6ToFloat(glyphpos.y_offset) * dpiScaleInverse;

				// Snap position to the current font's source pixel grid. 
				p.position.x = snapToPixel(p.position.x, dpiScale, dpiScaleInverse);
				p.position.y = snapToPixel(p.position.y, dpiScale, dpiScaleInverse);

				positions->push_back(p);
			}

			curpos.x += fixed26_6ToFloat(glyphpos.x_advance) * dpiScaleInverse;
			curpos.y += fixed26_6ToFloat(glyphpos.y_advance) * dpiScaleInverse;

			// Account for extra spacing given to space characters.
			if (clustercodepoint == ' ' && extraspacing != 0.0f)
			{
				spacingremainder += fmod(extraspacing, 1);
				curpos.x += floorf(extraspacing) + floorf(spacingremainder);
				spacingremainder = fmod(spacingremainder, 1);
			}
		}
	}

	maxwidth = std::max(maxwidth, curpos.x);

	if (info != nullptr)
	{
		info->width = snapToPixel(maxwidth - offset.x, dpiScale, dpiScaleInverse);
		info->height = snapToPixel(curpos.y - offset.y, dpiScale, dpiScaleInverse);
		if (curpos.x > offset.x)
			info->height += getCombinedHeight();
	}
}

int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints &codepoints, Range range, float wraplimit, float *width)
{
	if (!range.isValid())
		range = Range(0, codepoints.cps.size());

	float w = 0.0f;
	float outwidth = 0.0f;
	float widthbeforelastspace = 0.0f;
	int firstindexafterspace = -1;

	uint32 prevcodepoint = 0;

	const float dpiScaleInverse = 1.0f / dpiScale;

	std::vector<BufferRange> bufferranges;
	computeBufferRanges(codepoints, range, bufferranges);

	for (const auto &bufferrange : bufferranges)
	{
		hb_buffer_t *hbbuffer = hbBuffers[bufferrange.index];

		const hb_glyph_info_t *glyphinfos = hb_buffer_get_glyph_infos(hbbuffer, nullptr);
		hb_glyph_position_t *glyphpositions = hb_buffer_get_glyph_positions(hbbuffer, nullptr);
		hb_direction_t direction = hb_buffer_get_direction(hbbuffer);

		for (size_t i = bufferrange.range.first; i <= bufferrange.range.last; i++)
		{
			const hb_glyph_info_t &info = glyphinfos[i];
			hb_glyph_position_t &glyphpos = glyphpositions[i];

			uint32 clustercodepoint = codepoints.cps[info.cluster];

			if (clustercodepoint == '\r')
			{
				prevcodepoint = clustercodepoint;
				continue;
			}

			if (clustercodepoint == '\t' && isUsingSpacesForTab())
			{
				// This should be safe to overwrite.
				// TODO: RTL support?
				glyphpos.x_offset = 0;
				glyphpos.y_offset = 0;
				glyphpos.x_advance = HB_DIRECTION_IS_HORIZONTAL(direction) ? tabSpacesAdvanceX : 0;
				glyphpos.y_advance = HB_DIRECTION_IS_VERTICAL(direction) ? tabSpacesAdvanceY : 0;
			}

			float newwidth = w + fixed26_6ToFloat(glyphpos.x_advance) * dpiScaleInverse;

			// Don't count trailing spaces in the output width.
			if (isWhitespace(clustercodepoint))
			{
				if (!isWhitespace(prevcodepoint))
					widthbeforelastspace = w;
			}
			else
			{
				if (isWhitespace(prevcodepoint))
					firstindexafterspace = info.cluster;

				// Only wrap when there's a non-space character.
				if (snapToPixel(newwidth, dpiScale, dpiScaleInverse) > wraplimit)
				{
					// If this is the first character, wrap from the next one instead of this one.
					int wrapindex = (int)info.cluster > (int)range.first ? (int)info.cluster : (int)range.first + 1;

					// Rewind to after the last seen space when wrapping.
					if (firstindexafterspace != -1)
					{
						wrapindex = firstindexafterspace;
						outwidth = widthbeforelastspace;
					}

					if (width)
						*width = snapToPixel(outwidth, dpiScale, dpiScaleInverse);

					return wrapindex;
				}

				outwidth = newwidth;
			}

			w = newwidth;
			prevcodepoint = clustercodepoint;
		}
	}

	if (width)
		*width = snapToPixel(outwidth, dpiScale, dpiScaleInverse);

	// There wasn't any wrap in the middle of the range.
	return (int) range.last + 1;
}

} // freetype
} // font
} // love