Fonts
Prysm supports TrueType/OpenType fonts and collections (.ttf/.ttc/.otf/.otc). @font-face
declarations in CSS are the recommended way of loading and using fonts. In order to use fonts in the frontend, you need to set them up first
Differences from the HTML Standard
A major difference from the HTML Standard is that fonts are global to the whole system and not unloaded after changing pages. This means that @font-face
loaded from one View
can be used in another View
. A @font-face
from one page can affect the next page that is loaded after following a link or changing the URL. Registered fonts are kept loaded and cannot be overwritten. Subsequent @font-face
declarations matching the font description of a previously registered font will be ignored. We are keeping the fonts alive because it is common behavior in games to use the same fonts across multiple pages and this avoids loading and unloading the same font.
Another difference is that the load
event is fired after fonts are loaded. We feel that this makes more sense for games instead of marking a page as loaded with various fallback fonts and font styles which will be substituted later on.
Font preloading
Fonts are always streamed and loaded partially. However, upon registering a font, a part containing its description must be read. You can register fonts in advance to make fonts available earlier and speed up the loading process.You can register fonts from
C++ using the cohtml::System::RegisterFont
API call. It works similar to the @font-face
declaration. Additionally, the API allows using the original font name and styles when font description isn’t provided, unlike @font-face
which is always explicit. The RegisterFont
API also supports loading all fonts from a font collection when a font description isn’t provided. Providing a font description when loading a font collection will load only the first font from the collection similar to @font-face
.
Default font
The family name that was passed in the library parameter
cohtml::LibraryParams::DefaultStyleFontFamily
on initialize will be used when font-family for an element isn’t specified.
Font family fallback
Cohtml supports font-family fallback per character. You can specify multiple font families to be used for a single element, for example 'Droid Sans', 'Noto Sans CJK', Emoji
. All characters available in Droid Sans
will be used first, then for any missing character in Droid Sans
, Noto Sans CJK
will be used and so on.
Font weight fallback
Cohtml supports font-weight fallback per font-family and will try to select an available weight that is closest to the requested one within the same font-family. Font-family match has higher precedence than font-weight. For example, if you have Droid Sans
registered as a Regular
style font and you have an element that should render text as bold which uses font families Droid Sans, Noto Sans Bold
, Prysm will pick Droid Sans
and will draw the text with Regular font style, instead of drawing it bold with Noto Sans
Font style fallback
Cohtml doesn’t support font-style fallback yet, so styles like Italic
and Oblique
should be specified explicitly in order for a font match to occur. Cohtml won’t match fonts with the same font-family name and different styles.
Generic Font Families
Cohtml doesn’t support generic font families functionality, but recognizes those keywords (serif
, sans-serif
, monospace
) in a font-family declaration and uses a specified font-family instead. The font-family that is going to be used can be configured when initializing the system with the
cohtml::SystemSettings::GenericFontFamilyNameFont
option. By default, Prysm will use the default font for generic font families.
Additional font fallbacks
Cohtml allows to specify additional fallback font families using cohtml::View::SetAdditionalFontFallbacks
. It works as if the specified families from C++ are appended at the back of the font families defined in a stylesheet.
Last resort font fallback
Cohtml embeds a minimal font as a last resort fallback. This font was added mainly to help developers with their font setup. The visualization of this font is configurable by passing different modes to cohtml::SystemSettings::LastResortFontMode
. By default, Prysm will render ASCII printable characters from the last resort font when no other suitable font is found and a square symbol for unresolved Unicode characters. The default option is suitable for visualizing simple texts without loading any fonts. cohtml::Fonts::FM_Squares
will render squares for all characters for which a suitable font was not found. FM_None
will disable the last resort font visualization.
Font rendering
Cohtml uses single-channel SDF (signed distance fields) to render fonts in order to achieve high performance while keeping memory consumption relatively low. However, there are some drawbacks to this method. The first is a loss of fine details in the font glyphs - this is rarely visible when using common fonts, but can be a huge problem for artistic fonts that have fine lines, dots and specks. The second drawback is rounding of sharp corners which usually can be seen only at very large font sizes and when using strokes. In some cases, the rounding of the stroke may be the desired effect.
Both issues can be significantly reduced by using multi-channel SDF. We have added support for rendering with multi-channel SDF, however since multi-channel SDF generation is expensive, Prysm doesn’t support doing it at runtime and requires the user to preload pre-generated MSDF atlases. More info on MSDF is in the next sections below.
When MSDF is not an option for mitigating the issues with either loss of detail or corner rounding, Prysm extends the @font-face
declaration with a custom CSS property coh-font-sdf
that can control which fonts to use SDF and which to be rasterized directly. The C++ API cohtml::System::RegisterFont
also allows control over the font rendering method.
Example of how to turn off SDF:
@font-face {
font-family: 'ExampleName';
src: url('ExampleName.otf');
coh-font-sdf: off;
}
Please keep in mind that stroke is only supported with SDF rendering, hence you cannot turn off SDF for stroked text.
Font fit auto-sizing
Font fit mode
Cohtml supports a custom CSS property coh-font-fit-mode
. The property can be used to auto-fit text within an element, no matter the length of the text. Designers can fix the size they want on the container of the text and through coh-font-fit-mode
make sure that any text put within will fit perfectly - even when the text is not yet known. The property will change the font size used to fit the text. It’s particularly handy when doing internationalization and different length texts have to fit in the same designed container.
The coh-font-fit-mode
property has three possible values:
Value | Effect |
---|---|
none | Default - no changes to the font size will be attempted |
fit | The text font size will be changed to fit its parent - grow or shrink |
shrink | The text will shrink only its size if it can’t fit in the container |
You can see an example of the property in the Localization sample.
Font fit min and max sizes
The coh-font-fit-in-size
and coh-font-fit-max-size
custom CSS properties can be used to set the minimum and maximum font sizes used to fit the text. This can come in handy when trying to resize text inside a container while also making sure that the text is big enough to be legible.
The coh-font-fit-in-size
and coh-font-fit-max-size
properties have the following effects:
Property | Default Value | Effect |
---|---|---|
coh‑font‑fit‑min‑size | 6px | Determines the minimum font size used to fit a text if coh-font-fit-mode is not set to none . Has a minimum effective value of 6px. Any less than that will be treated by the font fitting algorithm as if coh‑font‑fit‑min‑size was set to 6px |
coh‑font‑fit‑max‑size | 128px | Determines the maximum font size used to fit a text if coh-font-fit-mode is set to fit . Has no effect if coh-font-fit-mode is not set to fit |
If coh‑font‑fit‑max‑size
is not explicitly set and the default value of 128px is used to fit the text, a warning message will be sent. To avoid this, explicitly set the coh-font-fit-max-size
value.
The font fitting algorithm only works with a font size of 6px or more and a text won’t be fit with a size less than 6px, regardless of the size of the container or the coh-font-fit-min-size
property.
If coh-font-fit-mode
is set to fit
and coh-font-fit-max-size
is less than coh-font-fit-min-size
, then the text’s font size will be set to that of coh-font-fit-min-size
. For example, if coh-font-fit-min-size
is set to 32px and coh-font-fit-max-size
is set to 16px, then the text will be resized with a font size of 32px. In other words, coh-font-fit-min-size
takes precedence over coh-font-fit-max-size
.
Font fit shorthand
The coh-font-fit
custom CSS shorthand property can be used to set coh-font-fit-mode
, coh-font-fit-min-size
and coh-font-fit-max-size
properties together. It accepts values in the following formats:
Format |
---|
<coh-font-fit-mode> |
<coh-font-fit-mode> <coh-font-fit-min-size> |
<coh-font-fit-min-size> <coh-font-fit-max-size> |
<coh-font-fit-mode> <coh-font-fit-min-size> <coh-font-fit-max-size> |
Note that setting only the min and max sizes will result in coh-font-fit-mode
being set to fit
.
Text transformations
Cohtml supports the text-transform
CSS property that allows automatic uppercase/lowercase/capitalize transformations of text. In order to accommodate any language, the case transformation needs very complex rules that are outside the scope of Cohtml. The implementation of the case correction is delegated to the embedder in order to allow her to support all of Unicode and avoid linking Prysm to a specific internationalization implementation. The user must implement the cohtml::ITextTransformationManager
interface and pass an object of that type to SystemSettings
. To implement the case transformation the user can use OS-specific APIs like Win32’s CharUpperBuffW/CharLowerBuffW or cross-platform libraries like ICU. For an example implementation for Windows, please refer to the Samples\Common\WinDesktop\Application.cpp
file.
Fonts issues with trails and artifacts
Cohtml determines the glyph dimensions using the stored information about the glyphs bounding box from the font. When calculating the containing element size based on the content, the width of the container is determined from the sum of all glyph bounding box widths of the longest text line. The assumption is that glyphs will never be drawn outside of their bounding box. However, some fonts have glyphs that extend beyond the specified bounding box. This is a trick that font designers use to avoid specifying kerning for some glyphs. When such glyphs are drawn as first or last in the line, the part of the glyph that extends beyond its bounding box will be drawn outside of the containing elements bounding box. When the element is moved/hidden/removed it will only clear its bounding box which will leave artifacts for each part of the glyphs drawn outside.
The following image shows a case where the glyphs extend beyond its bounding box:
When reworking the font isn’t possible, elements using that font can be padded slightly (for example padding-right: 0.1em;
) so that the element bounding box covers the extended part of the glyph. Additionally overflow: hidden;
can be used to prevent artifacts from appearing altogether, but do mind that this will clip the parts of the glyphs that extend beyond the elements bounding box. The two CSS properties can be used together for the best results.
Cohtml determines the line’s height using the stored information about it from the font that the font designer has specified. When calculating the containing element size based on the content, the height of the container is determined by the line height multiplied by the number of lines. The assumption is that the line height will always be bigger than the height of the glyphs, otherwise the lines of text will cross with each other and will become unreadable. The typical ratio between the line height and glyphs height is around 1.25. The CSS property line-height
can override this for accessibility reasons and it usually involves increasing the space between the line by setting line-height ratio to around 1.5 for better readability. When adjusting the line-height from CSS we suggest always using a ratio instead of absolute values like <length>
and <percentage>
. Line-height ratios below 1 will cause the top and the bottom of the glyphs to extend beyond the containing element bounding box. When the element is moved/hidden/removed it will only clear its bounding box which will leave artifacts for each part of the glyphs drawn outside. When setting line-height ratios below 1, the same workaround with overflow and padding can be used in order to avoid artifacts.
Recent changes
Since version 1.13, there are a few notable changes to the text rendering. Mainly, Prysm can now render text on fractional pixel positions. This makes text animations look smooth and avoids unexpected pixel snapping. Also, the size threshold for using SDF rendering is changed to 10 pixels (previously 18 pixels) so that all text smaller than 10 pixels is rendered using SDF. For more information, see the migration guide.
The changes with version 1.13 bring one caveat to the font rendering. Some fonts (e.g. Bitter) have crossing segments in the contour data saved in the TrueType font file. This interferes with the rendering of SDF glyphs on the GPU. In the case of such fonts, the glyphs will be rendered with artifacts. The following images illustrate what we mean
In case you find yourself needing to use such fonts, there are several strategies to remedy that:
- Fix the font itself. You can use a tool like FontForge to remove the crossings parts of the path. Load the font in FontForge, select all glyphs (Ctrl-A), and then choose Element->Overlap->Remove Overlaps (Ctrl-Shift-O). After the overlaps are removed, save the font as TrueType font.
- Alternatively, if editing the font file is not possible, you can turn off the GPU SDF glyph rendering through a developer option. See the migration guide for more information.
User fonts
By default Prysm uses FreeType for rendering fonts and manually reads .ttf / .otf files. Cohtml can also work with Bitmap and MSDF fonts supplied by the client.
To load a user font, call cohtml::System::AddUserFont
providing a thorough description of the font and its images.
This method has 2 overloads:
- With encoded image data.
UISystem->AddUserFont(imageFilesData, imageFileSizes, imagesCount, userFontDescription)
- With user images that Prysm can directly use.
UISystem->AddUserFont(userImagesData, imagesCount, userFontDescription)
On the content creation side, using Bitmap or an MSDF font doesn’t differ much from what would be otherwise required. Just tell Prysm to use the font family you just loaded as you would any other font.
cohtml::UserFontDescription::CharInfo
’s OffsetX
/OffsetY
fields are generally used as the left bearing and top bearing of the font. The following diagram (taken from the FreeType documentation) illustrates what those are as some applications have different definitions:How to generate a Bitmap font with BMFont
One of the most popular tools for generating Bitmap fonts is BMFont by AngelCode.
- In the Font Settings, make sure you are generating Unicode char codes. Also, make sure that the Match char height is checked. Without this the char size is not in pixels and Prysm will not lay it out correctly.
- In Export Options make sure the textures are 8-bit and that the glyph is outputted in the alpha channel.
Example usage - Bitmap fonts
Here is an example of how to parse a binary .fnt file generated by BMFont and load the font in Cohtml.
// BMFont Parsing
#pragma pack(push, 1)
struct BMFontBlock
{
uint8_t Type;
uint32_t Size;
};
struct BMFontBlockInfo
{
int16_t FontSize;
uint8_t BitField; //bit 0: smooth, bit 1 : unicode, bit 2 : italic, bit 3 : bold, bit 4 : fixedHeigth, bits 5 - 7 : reserved
uint8_t CharSet;
uint16_t StretchH;
uint8_t AA;
uint8_t PaddingUp;
uint8_t PaddingRight;
uint8_t PaddingDown;
uint8_t PaddingLeft;
uint8_t SpacingHoriz;
uint8_t SpacingVert;
uint8_t Outline;
// char [n + 1] FontName. Null terminated string
};
struct BMFontBlockCommon
{
uint16_t LineHeight;
uint16_t BaseLine;
uint16_t ScaleW;
uint16_t ScaleH;
uint16_t Pages;
uint8_t BitField; // bits 0-6: reserved, bit 7: packed
uint8_t AlphaChannel;
uint8_t RedChannel;
uint8_t GreenChannel;
uint8_t BlueChannel;
};
// BMFontBlockPages
// PageNames p * (n + 1), p-pages,each with n-length
// Repeated until all chars are described
// NumberOfChars = BMFontBlock.Size / 20
struct BMFontBlockChars
{
uint32_t Id;
uint16_t X;
uint16_t Y;
uint16_t Width;
uint16_t Height;
int16_t XOffset;
int16_t YOffset;
int16_t Advance;
uint8_t Page;
uint8_t Channel;
};
struct BMFontBlockKerningPairs
{
uint32_t First;
uint32_t Second;
int16_t Amount;
};
#pragma pack(pop)
struct BMFontData
{
std::string Name;
unsigned Size;
bool IsBold;
bool IsItalic;
unsigned Baseline;
unsigned LineHeight;
unsigned TextureWidth;
unsigned TextureHeight;
struct CharInfo
{
unsigned Id;
unsigned X;
unsigned Y;
unsigned Width;
unsigned Height;
int OffsetX;
int OffsetY;
unsigned Advance;
unsigned ImageIndex;
};
std::vector<CharInfo> Chars;
std::vector<std::string> ImageNames;
struct KerningInfo
{
unsigned FirstId;
unsigned SecondId;
int Amount;
};
std::vector<KerningInfo> Kerning;
};
bool ParseBMFontFile(const char* filename, BMFontData& data)
{
std::ifstream file(filename, std::ios::binary);
if (!file)
{
return false;
}
uint32_t magicBytes;
file.read(reinterpret_cast<char*>(&magicBytes), 4);
if (magicBytes != 0x03464d42) // BMF3 BMF is magic signature and 3 is the current version
{
return false;
}
// Assume there is one from each type of blocks
// Info block
BMFontBlock infoBlockSize;
file.read(reinterpret_cast<char*>(&infoBlockSize), sizeof(BMFontBlock));
if (infoBlockSize.Type != 1) // Check if it a info block
{
return false;
}
std::unique_ptr<char[]> infoBlockData = std::make_unique<char[]>(infoBlockSize.Size);
file.read(infoBlockData.get(), infoBlockSize.Size);
BMFontBlockInfo* infoBlock = reinterpret_cast<BMFontBlockInfo*>(infoBlockData.get());
data.Name = std::string(infoBlockData.get() + sizeof(BMFontBlockInfo));
data.Size = std::abs(infoBlock->FontSize);
data.IsBold = !!(infoBlock->BitField & (1 << 4));
data.IsItalic = !!(infoBlock->BitField & (1 << 5));
// Common block
BMFontBlock commonBlockSize;
file.read(reinterpret_cast<char*>(&commonBlockSize), sizeof(BMFontBlock));
if (commonBlockSize.Type != 2
|| commonBlockSize.Size != sizeof(BMFontBlockCommon))
{
return false;
}
BMFontBlockCommon commonBlock;
file.read(reinterpret_cast<char*>(&commonBlock), sizeof(BMFontBlockCommon));
data.LineHeight = commonBlock.LineHeight;
data.Baseline = commonBlock.BaseLine;
data.TextureWidth = commonBlock.ScaleW;
data.TextureHeight = commonBlock.ScaleH;
// Pages block
BMFontBlock pagesBlockSize;
file.read(reinterpret_cast<char*>(&pagesBlockSize), sizeof(BMFontBlock));
if (pagesBlockSize.Type != 3)
{
return false;
}
// Pages are commonBlock.Pages * (n + 1) bytes long. Each page is n byte string with 1 extra byte for terminating null
const auto pageStringLength = (pagesBlockSize.Size / commonBlock.Pages) - 1;
std::unique_ptr<char[]> pagesBlockData = std::make_unique<char[]>(pagesBlockSize.Size);
file.read(pagesBlockData.get(), pagesBlockSize.Size);
data.ImageNames.reserve(commonBlock.Pages);
for (auto i = 0; i < commonBlock.Pages; ++i)
{
data.ImageNames.push_back(pagesBlockData.get() + i * pageStringLength);
}
// Chars block
BMFontBlock charsBlockSize;
file.read(reinterpret_cast<char*>(&charsBlockSize), sizeof(BMFontBlock));
if (charsBlockSize.Type != 4)
{
return false;
}
const auto numChars = charsBlockSize.Size / sizeof(BMFontBlockChars);
std::unique_ptr<BMFontBlockChars[]> charsBlockData = std::make_unique<BMFontBlockChars[]>(numChars);
file.read(reinterpret_cast<char*>(charsBlockData.get()), charsBlockSize.Size);
data.Chars.reserve(numChars);
for (auto i = 0; i < numChars; ++i)
{
if (charsBlockData[i].Channel != 8 // Alpha Channel
&& charsBlockData[i].Channel != 15) // All Channels
{
return false;
}
BMFontData::CharInfo chInfo;
chInfo.Id = charsBlockData[i].Id;
chInfo.ImageIndex = charsBlockData[i].Page;
chInfo.X = charsBlockData[i].X;
chInfo.Y = charsBlockData[i].Y;
chInfo.Width = charsBlockData[i].Width;
chInfo.Height = charsBlockData[i].Height;
chInfo.OffsetX = charsBlockData[i].XOffset;
chInfo.OffsetY = charsBlockData[i].YOffset;
chInfo.Advance = charsBlockData[i].Advance;
data.Chars.push_back(chInfo);
}
// Kerning block
BMFontBlock kerningBlockSize;
file.read(reinterpret_cast<char*>(&kerningBlockSize), sizeof(BMFontBlock));
if (file)
{
if (kerningBlockSize.Type != 5)
{
return false;
}
const auto numKerning = kerningBlockSize.Size / sizeof(BMFontBlockKerningPairs);
std::unique_ptr<BMFontBlockKerningPairs[]> kerningBlockData = std::make_unique<BMFontBlockKerningPairs[]>(numKerning);
file.read(reinterpret_cast<char*>(kerningBlockData.get()), kerningBlockSize.Size);
data.Kerning.reserve(numKerning);
for (auto i = 0; i < numKerning; ++i)
{
BMFontData::KerningInfo kInfo;
kInfo.FirstId = kerningBlockData[i].First;
kInfo.SecondId = kerningBlockData[i].Second;
kInfo.Amount = kerningBlockData[i].Amount;
data.Kerning.push_back(kInfo);
}
}
return true;
}
After the .fnt file is parsed we need to initialize cohtml::UserFontDescription
struct with the required data.
// Load Bitmap Font
BMFontData data;
if (ParseBMFontFile("BitmapFont.fnt", data))
{
std::vector<std::vector<char>> images;
std::vector<const char*> imagesData;
std::vector<unsigned> imagesSize;
for (auto i = 0; i < data.ImageNames.size(); ++i)
{
std::ifstream file(data.ImageNames[i], std::ios::binary);
if (!file)
{
return false;
}
std::vector<char> fileContents((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
images.push_back(std::move(fileContents));
imagesData.push_back(images.back().data());
imagesSize.push_back(unsigned(images.back().size()));
}
cohtml::UserFontDescription desc;
desc.FontType = cohtml::Fonts::UFT_Bitmap; // Set proper font type
desc.FontFamily = data.Name.c_str();
desc.Size = data.Size;
desc.Spread = 0; // Spread is not used by Bitmap fonts
desc.IsBold = data.IsBold;
desc.IsItalic = data.IsItalic;
desc.LineHeight = data.LineHeight;
desc.Ascent = data.Baseline; // Baseline is distance from top so we use it as ascent
desc.Descent = data.Baseline - data.LineHeight; // Assume the rest of the line height is the descent
std::vector<cohtml::UserFontDescription::CharInfo> descChars;
descChars.reserve(data.Chars.size());
for (const auto& ch : data.Chars)
{
cohtml::UserFontDescription::CharInfo info;
info.CharCode = ch.Id;
info.X = float(ch.X) / float(data.TextureWidth);
info.Y = float(ch.Y) / float(data.TextureWidth);
info.Width = float(ch.Width) / float(data.TextureWidth);
info.Height = float(ch.Height) / float(data.TextureWidth);
info.OffsetX = ch.OffsetX;
info.OffsetY = ch.OffsetY;
info.Advance = ch.Advance;
info.ImageIndex = ch.ImageIndex;
descChars.push_back(info);
}
desc.Chars = descChars.data();
desc.NumChars = unsigned(descChars.size());
std::vector<cohtml::UserFontDescription::KerningPairInfo> descKerning;
descKerning.reserve(data.Kerning.size());
for (const auto& ker : data.Kerning)
{
cohtml::UserFontDescription::KerningPairInfo info;
info.FirstCharCode = ker.FirstId;
info.SecondCharCode = ker.SecondId;
info.Amount = ker.Amount;
descKerning.push_back(info);
}
desc.KerningPairs = descKerning.data();
desc.NumKerningPairs = unsigned(descKerning.size());
m_UISystem->AddUserFont(imagesData.data(), imagesSize.data(), unsigned(images.size()), desc);
}
How to generate a MSDF font with msdf-atlas-gen
One of the most popular tools for the generation of MSDF fonts is msdf-atlas-gen by Viktor Chlumsky.
Here’s a sample generation command.
.\msdf-atlas-gen.exe -font .\Arial.ttf -charset .\charset.txt -type msdf -size 64 -pxrange 8 -format png -dimensions 512 512 -imageout atlas.png -json description.json
It will use the font provided to generate all the glyphs specified in the charset.txt file with a size of 64 and a spread of 8. The output will be an atlas.png file with a width and height of 512 pixels and a description.json file with all the needed font metadata.
- The resulting 3-channel RGB texture.
Example usage - MSDF fonts (and Bitmap fonts created by the msdf-atlas-gen tool)
Here is an example of how to parse a font generated by msdf-atlas-gen and load it in Cohtml.
bool LoadMSDFFont(const std::string& atlasfile, const std::string& desc, cohtml::IFileSystemReader* fileReader, cohtml::System* system)
{
std::vector<unsigned char> fontDescriptionText;
auto descriptionFd = fileReader->OpenFile(desc.c_str());
if (!descriptionFd)
{
return false;
}
auto fileSize = descriptionFd->GetSize();
fontDescriptionText.resize(fileSize);
descriptionFd->Read(0, fontDescriptionText.data(), fileSize);
descriptionFd->Close();
json descriptionJson = json::parse(fontDescriptionText);
cohtml::UserFontDescription fontDescription;
std::vector<cohtml::UserFontDescription::CharInfo> charsInfo;
std::vector<cohtml::UserFontDescription::KerningPairInfo> kerningsPairsInfo;
fontDescription.FontType = cohtml::Fonts::UFT_MSDF;
fontDescription.FontFamily = "Msdf";
fontDescription.IsBold = false;
fontDescription.IsItalic = false;
const auto& atlas = descriptionJson["atlas"];
const auto fontSize = atlas["size"].get<float>();
const auto atlasWidth = atlas["width"].get<float>();
const auto atlasHeight = atlas["height"].get<float>();
fontDescription.Size = (unsigned)fontSize;
fontDescription.Spread = atlas["distanceRange"].get<unsigned>() / 2;
const auto& metrics = descriptionJson["metrics"];
fontDescription.LineHeight = (metrics["lineHeight"].get<float>() * fontSize);
fontDescription.Ascent = (metrics["ascender"].get<float>() * fontSize);
fontDescription.Descent = (metrics["descender"].get<float>() * fontSize);
const auto& glyphs = descriptionJson["glyphs"];
size_t glyphCount = glyphs.size();
charsInfo.resize(glyphCount);
for (size_t i = 0; i < glyphCount; ++i)
{
const auto glyph = glyphs[i];
float atlasLeft = 0.0f;
float atlasRight = 0.0f;
float atlasTop = 0.0f;
float atlasBottom = 0.0f;
float planeLeft = 0.0f;
float planeRight = 0.0f;
float planeTop = 0.0f;
float planeBottom = 0.0f;
// Atlas bounds contain the position of the glyph within the atlas in pixels
const auto atlasBounds = glyph.find("atlasBounds");
if (atlasBounds != std::end(glyph))
{
atlasLeft = (*atlasBounds)["left"].get<float>();
atlasRight = (*atlasBounds)["right"].get<float>();
atlasTop = (*atlasBounds)["top"].get<float>();
atlasBottom = (*atlasBounds)["bottom"].get<float>();
}
// Plane bounds are the adjusted boundaries of the glyph so that when sampling the atlasBounds region,
// the distance field will positioned such that the glyph curve is exactly on the target position.
// Basically the left bearing of the glyph minus the one-sided spread (for the horizontal axis)
// and the top bearing plus the one-sided spread (for the vertical axis)
// Defined in ems.
const auto planeBounds = glyph.find("planeBounds");
if (planeBounds != std::end(glyph))
{
planeLeft = (*planeBounds)["left"].get<float>();
planeRight = (*planeBounds)["right"].get<float>();
planeTop = (*planeBounds)["top"].get<float>();
planeBottom = (*planeBounds)["bottom"].get<float>();
}
float advance = glyph["advance"].get<float>();
auto& charInfo = charsInfo[i];
charInfo.CharCode = glyph["unicode"].get<int>();
charInfo.X = atlasLeft / atlasWidth;
charInfo.Y = (atlasHeight - atlasTop) / atlasHeight;
charInfo.Width = (atlasRight - atlasLeft) / atlasWidth;
charInfo.Height = (atlasTop - atlasBottom) / atlasHeight;
charInfo.OffsetX = (planeLeft * fontSize);
charInfo.OffsetY = (planeTop * fontSize);
charInfo.Advance = (advance * fontSize);
charInfo.ImageIndex = 0;
}
fontDescription.Chars = charsInfo.data();
fontDescription.NumChars = static_cast<unsigned>(glyphCount);
const auto& kernings = descriptionJson["kerning"];
size_t kerningCount = kernings.size();
kerningsPairsInfo.resize(kerningCount);
for (size_t i = 0; i < kerningCount; ++i)
{
const auto kerning = kernings[i];
auto& kerningInfo = kerningsPairsInfo[i];
kerningInfo.FirstCharCode = kerning["unicode1"].get<unsigned>();
kerningInfo.SecondCharCode = kerning["unicode2"].get<unsigned>();
kerningInfo.Amount = kerning["advance"].get<float>() * fontSize;
}
fontDescription.KerningPairs = kerningsPairsInfo.data();
fontDescription.NumKerningPairs = static_cast<unsigned>(kerningCount);
std::vector<unsigned char> imageData;
auto atlasFd = fileReader->OpenFile(atlasfile.c_str());
if (!atlasFd)
{
return false;
}
unsigned imageDataSize = atlasFd->GetSize();
imageData.resize(imageDataSize);
atlasFd->Read(0, imageData.data(), imageDataSize);
atlasFd->Close();
const char* imagesData[] = { reinterpret_cast<char*>(imageData.data()) };
unsigned imageSizes[] = {imageDataSize};
system->AddUserFont(&imagesData[0], imageSizes, 1, fontDescription);
return true;
}
bool LoadBitmapFont(const std::string& atlasfile, const std::string& desc, cohtml::IFileSystemReader* fileReader, cohtml::System* system)
{
std::vector<unsigned char> fontDescriptionText;
auto descriptionFd = fileReader->OpenFile(desc.c_str());
if (!descriptionFd)
{
return false;
}
auto fileSize = descriptionFd->GetSize();
fontDescriptionText.resize(fileSize);
descriptionFd->Read(0, fontDescriptionText.data(), fileSize);
descriptionFd->Close();
json descriptionJson = json::parse(fontDescriptionText);
cohtml::UserFontDescription fontDescription;
std::vector<cohtml::UserFontDescription::CharInfo> charsInfo;
std::vector<cohtml::UserFontDescription::KerningPairInfo> kerningsPairsInfo;
fontDescription.FontType = cohtml::Fonts::UFT_Bitmap;
fontDescription.Spread = 0;
fontDescription.FontFamily = "Bitmap";
fontDescription.IsBold = false;
fontDescription.IsItalic = false;
const auto& atlas = descriptionJson["atlas"];
const auto fontSize = atlas["size"].get<float>();
const auto atlasWidth = atlas["width"].get<float>();
const auto atlasHeight = atlas["height"].get<float>();
fontDescription.Size = (unsigned)fontSize;
const auto& metrics = descriptionJson["metrics"];
fontDescription.LineHeight = (metrics["lineHeight"].get<float>() * fontSize);
fontDescription.Ascent = (metrics["ascender"].get<float>() * fontSize);
fontDescription.Descent = (metrics["descender"].get<float>() * fontSize);
const auto& glyphs = descriptionJson["glyphs"];
size_t glyphCount = glyphs.size();
charsInfo.resize(glyphCount);
for (size_t i = 0; i < glyphCount; ++i)
{
const auto glyph = glyphs[i];
float atlasLeft = 0.0f;
float atlasRight = 0.0f;
float atlasTop = 0.0f;
float atlasBottom = 0.0f;
float planeLeft = 0.0f;
float planeRight = 0.0f;
float planeTop = 0.0f;
float planeBottom = 0.0f;
const auto atlasBounds = glyph.find("atlasBounds");
if (atlasBounds != std::end(glyph))
{
atlasLeft = (*atlasBounds)["left"].get<float>();
atlasRight = (*atlasBounds)["right"].get<float>();
atlasTop = (*atlasBounds)["top"].get<float>();
atlasBottom = (*atlasBounds)["bottom"].get<float>();
}
const auto planeBounds = glyph.find("planeBounds");
if (planeBounds != std::end(glyph))
{
planeLeft = (*planeBounds)["left"].get<float>();
planeRight = (*planeBounds)["right"].get<float>();
planeTop = (*planeBounds)["top"].get<float>();
planeBottom = (*planeBounds)["bottom"].get<float>();
}
float advance = glyph["advance"].get<float>();
auto& charInfo = charsInfo[i];
charInfo.CharCode = glyph["unicode"].get<int>();
charInfo.X = atlasLeft / atlasWidth;
charInfo.Y = (atlasHeight - atlasTop) / atlasHeight;
charInfo.Width = (atlasRight - atlasLeft) / atlasWidth;
charInfo.Height = (atlasTop - atlasBottom) / atlasHeight;
charInfo.OffsetX = (planeLeft * fontSize);
charInfo.OffsetY = (planeTop * fontSize);
charInfo.Advance = (advance * fontSize);
charInfo.ImageIndex = 0;
}
fontDescription.Chars = charsInfo.data();
fontDescription.NumChars = static_cast<unsigned>(glyphCount);
const auto& kernings = descriptionJson["kerning"];
size_t kerningCount = kernings.size();
kerningsPairsInfo.resize(kerningCount);
for (size_t i = 0; i < kerningCount; ++i)
{
const auto kerning = kernings[i];
auto& kerningInfo = kerningsPairsInfo[i];
kerningInfo.FirstCharCode = kerning["unicode1"].get<unsigned>();
kerningInfo.SecondCharCode = kerning["unicode2"].get<unsigned>();
kerningInfo.Amount = (kerning["advance"].get<float>() * fontSize);
}
fontDescription.KerningPairs = kerningsPairsInfo.data();
fontDescription.NumKerningPairs = static_cast<unsigned>(kerningCount);
std::vector<unsigned char> imageData;
auto atlasFd = fileReader->OpenFile(atlasfile.c_str());
if (!atlasFd)
{
return false;
}
unsigned imageDataSize = atlasFd->GetSize();
imageData.resize(imageDataSize);
atlasFd->Read(0, imageData.data(), imageDataSize);
atlasFd->Close();
const char* imagesData[] = { reinterpret_cast<char*>(imageData.data()) };
unsigned imageSizes[] = {imageDataSize};
system->AddUserFont(&imagesData[0], imageSizes, 1, fontDescription);
return true;
}