From 8a836b20605873d77ee18f5cb692f9c8f1201194 Mon Sep 17 00:00:00 2001 From: Lynix Date: Sun, 4 Jan 2015 14:21:09 +0100 Subject: [PATCH] Added Font class (+ FreeType loader) Former-commit-id: 1811304cd0efe9a86cbae83faaf4c39d9fae248f --- include/Nazara/Utility/Enums.hpp | 21 + include/Nazara/Utility/Font.hpp | 133 +++++ include/Nazara/Utility/FontData.hpp | 36 ++ include/Nazara/Utility/FontGlyph.hpp | 19 + src/Nazara/Utility/Font.cpp | 507 ++++++++++++++++++ src/Nazara/Utility/FontData.cpp | 8 + src/Nazara/Utility/Loaders/FreeType.hpp | 15 + .../Utility/Loaders/FreeType/Loader.cpp | 393 ++++++++++++++ src/Nazara/Utility/Utility.cpp | 5 + 9 files changed, 1137 insertions(+) create mode 100644 include/Nazara/Utility/Font.hpp create mode 100644 include/Nazara/Utility/FontData.hpp create mode 100644 include/Nazara/Utility/FontGlyph.hpp create mode 100644 src/Nazara/Utility/Font.cpp create mode 100644 src/Nazara/Utility/FontData.cpp create mode 100644 src/Nazara/Utility/Loaders/FreeType.hpp create mode 100644 src/Nazara/Utility/Loaders/FreeType/Loader.cpp diff --git a/include/Nazara/Utility/Enums.hpp b/include/Nazara/Utility/Enums.hpp index b6c1db419..06e2a5c2a 100644 --- a/include/Nazara/Utility/Enums.hpp +++ b/include/Nazara/Utility/Enums.hpp @@ -218,6 +218,27 @@ enum nzPrimitiveMode nzPrimitiveMode_Max = nzPrimitiveMode_TriangleFan }; +enum nzTextAlign +{ + nzTextAlign_Left, + nzTextAlign_Middle, + nzTextAlign_Right, + + nzTextAlign_Max = nzTextAlign_Right +}; + +enum nzTextStyleFlags +{ + nzTextStyle_None = 0x0, + + nzTextStyle_Bold = 0x1, + nzTextStyle_Italic = 0x2, + nzTextStyle_StrikeThrough = 0x4, + nzTextStyle_Underlined = 0x8, + + nzTextStyle_Max = nzTextStyle_Underlined*2-1 +}; + enum nzVertexComponent { nzVertexComponent_Unused = -1, diff --git a/include/Nazara/Utility/Font.hpp b/include/Nazara/Utility/Font.hpp new file mode 100644 index 000000000..190d3c716 --- /dev/null +++ b/include/Nazara/Utility/Font.hpp @@ -0,0 +1,133 @@ +// Copyright (C) 2014 Jérôme Leclercq +// This file is part of the "Nazara Engine - Utility module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_FONT_HPP +#define NAZARA_FONT_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +struct NAZARA_API NzFontParams +{ + bool IsValid() const; +}; + +class NzFont; +class NzFontData; + +struct NzFontGlyph; // TEMP + +using NzFontConstRef = NzResourceRef; +using NzFontLoader = NzResourceLoader; +using NzFontRef = NzResourceRef; + +class NAZARA_API NzFont : public NzResource, NzNonCopyable +{ + friend NzFontLoader; + + public: + struct Atlas + { + NzGuillotineBinPack binPack; + NzImage image; + }; + + struct Glyph + { + NzRecti aabb; + NzRectui atlasRect; + bool flipped; + bool valid; + int advance; + unsigned int atlasIndex; + }; + + struct SizeInfo + { + unsigned int lineHeight; + float underlinePosition; + float underlineThickness; + }; + + NzFont(); + NzFont(NzFont&& font) = default; + ~NzFont(); + + void ClearGlyphCache(); + void ClearKerningCache(); + void ClearSizeInfoCache(); + + bool Create(NzFontData* data); + void Destroy(); + + bool ExtractGlyph(unsigned int characterSize, char32_t character, nzUInt32 style, NzFontGlyph* glyph) const; + + const Atlas& GetAtlas(unsigned int atlasIndex) const; + unsigned int GetAtlasCount() const; + unsigned int GetCachedGlyphCount(unsigned int characterSize, nzUInt32 style) const; + unsigned int GetCachedGlyphCount() const; + NzString GetFamilyName() const; + int GetKerning(unsigned int characterSize, char32_t first, char32_t second) const; + const Glyph& GetGlyph(unsigned int characterSize, nzUInt32 style, char32_t character) const; + unsigned int GetMaxAtlasSize() const; + unsigned int GetMinimumStepSize() const; + const SizeInfo& GetSizeInfo(unsigned int characterSize) const; + NzString GetStyleName() const; + + bool IsValid() const; + + bool Precache(unsigned int characterSize, nzUInt32 style, char32_t character) const; + bool Precache(unsigned int characterSize, nzUInt32 style, const NzString& characterSet) const; + + // Open + bool OpenFromFile(const NzString& filePath, const NzFontParams& params = NzFontParams()); + bool OpenFromMemory(const void* data, std::size_t size, const NzFontParams& params = NzFontParams()); + bool OpenFromStream(NzInputStream& stream, const NzFontParams& params = NzFontParams()); + + void SetMaxAtlasSize(unsigned int maxAtlasSize); + void SetMinimumStepSize(unsigned int minimumSizeStep); + + NzFont& operator=(NzFont&& font) = default; + + static unsigned int GetDefaultMaxAtlasSize(); + static void SetDefaultMaxAtlasSize(unsigned int maxAtlasSize); + + private: + using GlyphMap = std::unordered_map; + + struct QueuedGlyph + { + char32_t codepoint; + NzImage image; + GlyphMap* map; + }; + + nzUInt64 ComputeKey(unsigned int characterSize, nzUInt32 style) const; + unsigned int GetRealMaxAtlasSize() const; + unsigned int InsertRect(NzRectui* rect, bool* flipped) const; + const Glyph& PrecacheGlyph(GlyphMap& glyphMap, unsigned int characterSize, bool bold, char32_t character) const; + void ProcessGlyphQueue() const; + + std::unique_ptr m_data; + mutable std::unordered_map> m_kerningCache; + mutable std::unordered_map m_glyphes; + mutable std::unordered_map m_sizeInfoCache; + mutable std::vector m_atlases; + mutable std::vector m_glyphQueue; + unsigned int m_maxAtlasSize; + unsigned int m_minimumSizeStep; + + static NzFontLoader::LoaderList s_loaders; + static unsigned int s_maxAtlasSize; +}; + +#endif // NAZARA_FONT_HPP diff --git a/include/Nazara/Utility/FontData.hpp b/include/Nazara/Utility/FontData.hpp new file mode 100644 index 000000000..9805c1d2b --- /dev/null +++ b/include/Nazara/Utility/FontData.hpp @@ -0,0 +1,36 @@ +// Copyright (C) 2014 Jérôme Leclercq +// This file is part of the "Nazara Engine - Utility module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_FONTDATA_HPP +#define NAZARA_FONTDATA_HPP + +#include +#include + +struct NzFontGlyph; + +class NAZARA_API NzFontData +{ + public: + NzFontData() = default; + virtual ~NzFontData(); + + virtual bool ExtractGlyph(unsigned int characterSize, char32_t character, bool bold, NzFontGlyph* dst) = 0; + + virtual NzString GetFamilyName() const = 0; + virtual NzString GetStyleName() const = 0; + + virtual bool HasKerning() const = 0; + + virtual bool IsScalable() const = 0; + + virtual int QueryKerning(unsigned int characterSize, char32_t first, char32_t second) const = 0; + virtual unsigned int QueryLineHeight(unsigned int characterSize) const = 0; + virtual float QueryUnderlinePosition(unsigned int characterSize) const = 0; + virtual float QueryUnderlineThickness(unsigned int characterSize) const = 0; +}; + +#endif // NAZARA_FONTDATA_HPP diff --git a/include/Nazara/Utility/FontGlyph.hpp b/include/Nazara/Utility/FontGlyph.hpp new file mode 100644 index 000000000..11bceb255 --- /dev/null +++ b/include/Nazara/Utility/FontGlyph.hpp @@ -0,0 +1,19 @@ +// Copyright (C) 2014 Jérôme Leclercq +// This file is part of the "Nazara Engine - Utility module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_FONTGLYPH_HPP +#define NAZARA_FONTGLYPH_HPP + +#include + +struct NzFontGlyph +{ + NzImage image; + NzRecti aabb; + int advance; +}; + +#endif // NAZARA_FONTGLYPH_HPP diff --git a/src/Nazara/Utility/Font.cpp b/src/Nazara/Utility/Font.cpp new file mode 100644 index 000000000..c6a888929 --- /dev/null +++ b/src/Nazara/Utility/Font.cpp @@ -0,0 +1,507 @@ +// Copyright (C) 2014 Jérôme Leclercq +// This file is part of the "Nazara Engine - Utility module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include +#include +#include +#include + +namespace +{ + const unsigned int s_atlasStartSize = 512; +} + +bool NzFontParams::IsValid() const +{ + return true; // Rien à tester +} + +NzFont::NzFont() : +m_maxAtlasSize(0), +m_minimumSizeStep(1) +{ +} + +NzFont::~NzFont() +{ + Destroy(); +} + +void NzFont::ClearGlyphCache() +{ + // Destruction des atlas et glyphes mémorisés + m_atlases.clear(); + m_glyphes.clear(); + m_glyphQueue.clear(); + + // Création du premier atlas + m_atlases.resize(1); + Atlas& atlas = m_atlases.back(); + atlas.binPack.Reset(s_atlasStartSize, s_atlasStartSize); +} + +void NzFont::ClearKerningCache() +{ + m_kerningCache.clear(); +} + +void NzFont::ClearSizeInfoCache() +{ + m_sizeInfoCache.clear(); +} + +bool NzFont::Create(NzFontData* data) +{ + Destroy(); + + #if NAZARA_UTILITY_SAFE + if (!data) + { + NazaraError("Invalid font data"); + return false; + } + #endif + + m_data.reset(data); + + ClearGlyphCache(); // Création du premier atlas en mémoire + + return true; +} + +void NzFont::Destroy() +{ + m_atlases.clear(); + m_data.reset(); + m_kerningCache.clear(); + m_glyphes.clear(); + m_glyphQueue.clear(); + m_sizeInfoCache.clear(); +} + +bool NzFont::ExtractGlyph(unsigned int characterSize, char32_t character, nzUInt32 style, NzFontGlyph* glyph) const +{ + #if NAZARA_UTILITY_SAFE + if (!IsValid()) + { + NazaraError("Invalid font"); + return false; + } + #endif + + return m_data->ExtractGlyph(characterSize, character, style & nzTextStyle_Bold, glyph); +} + +const NzFont::Atlas& NzFont::GetAtlas(unsigned int atlasIndex) const +{ + if (!m_glyphQueue.empty()) + ProcessGlyphQueue(); + + return m_atlases.at(atlasIndex); +} + +unsigned int NzFont::GetAtlasCount() const +{ + return m_atlases.size(); +} + +unsigned int NzFont::GetCachedGlyphCount(unsigned int characterSize, nzUInt32 style) const +{ + nzUInt64 key = ComputeKey(characterSize, style); + auto it = m_glyphes.find(key); + if (it == m_glyphes.end()) + return 0; + + return it->second.size(); +} + +unsigned int NzFont::GetCachedGlyphCount() const +{ + unsigned int count = 0; + for (auto& pair : m_glyphes) + count += pair.second.size(); + + return count; +} + +NzString NzFont::GetFamilyName() const +{ + #if NAZARA_UTILITY_SAFE + if (!IsValid()) + { + NazaraError("Invalid font"); + return NzString("Invalid font"); + } + #endif + + return m_data->GetFamilyName(); +} + +int NzFont::GetKerning(unsigned int characterSize, char32_t first, char32_t second) const +{ + #if NAZARA_UTILITY_SAFE + if (!IsValid()) + { + NazaraError("Invalid font"); + return 0; + } + #endif + + // On utilise un cache car la méthode interne QueryKerning peut se révéler coûteuse (car pouvant induire un changement de taille) + auto& map = m_kerningCache[characterSize]; + + nzUInt64 key = (static_cast(first) << 32) | second; // Combinaison de deux caractères 32 bits dans un nombre 64 bits + + auto it = map.find(key); + if (it == map.end()) + { + // Absent du cache: on va demander l'information à la police + int kerning = m_data->QueryKerning(characterSize, first, second); + map.insert(std::make_pair(key, kerning)); + + return kerning; + } + else + return it->second; // Présent dans le cache, tout va bien +} + +const NzFont::Glyph& NzFont::GetGlyph(unsigned int characterSize, nzUInt32 style, char32_t character) const +{ + nzUInt64 key = ComputeKey(characterSize, style); + return PrecacheGlyph(m_glyphes[key], characterSize, style, character); +} + +unsigned int NzFont::GetMaxAtlasSize() const +{ + return m_maxAtlasSize; +} + +unsigned int NzFont::GetMinimumStepSize() const +{ + return m_minimumSizeStep; +} + +const NzFont::SizeInfo& NzFont::GetSizeInfo(unsigned int characterSize) const +{ + #if NAZARA_UTILITY_SAFE + if (!IsValid()) + { + NazaraError("Invalid font"); + + static SizeInfo dummy; + return dummy; + } + #endif + + auto it = m_sizeInfoCache.find(characterSize); + if (it == m_sizeInfoCache.end()) + { + SizeInfo sizeInfo; + sizeInfo.lineHeight = m_data->QueryLineHeight(characterSize); + sizeInfo.underlinePosition = m_data->QueryUnderlinePosition(characterSize); + sizeInfo.underlineThickness = m_data->QueryUnderlineThickness(characterSize); + + it = m_sizeInfoCache.insert(std::make_pair(characterSize, sizeInfo)).first; + } + + return it->second; +} + +NzString NzFont::GetStyleName() const +{ + #if NAZARA_UTILITY_SAFE + if (!IsValid()) + { + NazaraError("Invalid font"); + return NzString("Invalid font"); + } + #endif + + return m_data->GetStyleName(); +} + +bool NzFont::IsValid() const +{ + return m_data != nullptr; +} + +bool NzFont::Precache(unsigned int characterSize, nzUInt32 style, char32_t character) const +{ + nzUInt64 key = ComputeKey(characterSize, style); + return PrecacheGlyph(m_glyphes[key], characterSize, style, character).valid; +} + +bool NzFont::Precache(unsigned int characterSize, nzUInt32 style, const NzString& characterSet) const +{ + unsigned int size; + std::unique_ptr characters(characterSet.GetUtf32Buffer(&size)); + if (!characters) + { + NazaraError("Invalid character set"); + return false; + } + + nzUInt64 key = ComputeKey(characterSize, style); + auto& glyphMap = m_glyphes[key]; + for (unsigned int i = 0; i < size; ++i) + PrecacheGlyph(glyphMap, characterSize, style, characters[i]); + + return true; +} + +bool NzFont::OpenFromFile(const NzString& filePath, const NzFontParams& params) +{ + return NzFontLoader::LoadFromFile(this, filePath, params); +} + +bool NzFont::OpenFromMemory(const void* data, std::size_t size, const NzFontParams& params) +{ + return NzFontLoader::LoadFromMemory(this, data, size, params); +} + +bool NzFont::OpenFromStream(NzInputStream& stream, const NzFontParams& params) +{ + return NzFontLoader::LoadFromStream(this, stream, params); +} + +void NzFont::SetMaxAtlasSize(unsigned int maxAtlasSize) +{ + unsigned int oldMaxAtlasSize = GetRealMaxAtlasSize(); + m_maxAtlasSize = maxAtlasSize; + + // Si l'un de nos atlas dépasse la nouvelle taille, on doit vider le cache + maxAtlasSize = GetRealMaxAtlasSize(); + if (maxAtlasSize < oldMaxAtlasSize) + { + for (Atlas& atlas : m_atlases) + { + unsigned int atlasSize = atlas.binPack.GetWidth(); + if (atlasSize > maxAtlasSize) + { + NazaraWarning("At least one atlas was over new max atlas size (" + NzString::Number(atlasSize) + " > " + NzString::Number(maxAtlasSize) + "), clearing glyph cache..."); + ClearGlyphCache(); + return; + } + } + } +} + +void NzFont::SetMinimumStepSize(unsigned int minimumStepSize) +{ + #if NAZARA_UTILITY_SAFE + if (minimumStepSize == 0) + { + NazaraError("Minimum step size cannot be zero as it implies division by zero"); + return; + } + #endif + + m_minimumSizeStep = minimumStepSize; +} + +unsigned int NzFont::GetDefaultMaxAtlasSize() +{ + return s_maxAtlasSize; +} + +void NzFont::SetDefaultMaxAtlasSize(unsigned int maxAtlasSize) +{ + s_maxAtlasSize = maxAtlasSize; +} + +nzUInt64 NzFont::ComputeKey(unsigned int characterSize, nzUInt32 style) const +{ + nzUInt64 sizePart = static_cast((characterSize/m_minimumSizeStep)*m_minimumSizeStep); + nzUInt64 stylePart = 0; + + if (style & nzTextStyle_Bold) // Les caractères gras sont générés différemment + stylePart |= nzTextStyle_Bold; + + // Les caractères italiques peuvent venir d'une autre police, dans le cas contraire ils sont générés au runtime + //if (style & nzTextStyle_Italic) + // stylePart |= nzTextStyle_Italic; + + return (stylePart << 32) | sizePart; +} + +unsigned int NzFont::GetRealMaxAtlasSize() const +{ + unsigned int maxAtlasSize = (m_maxAtlasSize == 0) ? s_maxAtlasSize : m_maxAtlasSize; + if (maxAtlasSize == 0) + maxAtlasSize = std::numeric_limits::max(); + + return maxAtlasSize; +} + +unsigned int NzFont::InsertRect(NzRectui* rect, bool* flipped) const +{ + ///DOC: Tous les pointeurs doivent être valides + // Précondition: Un rectangle ne peut pas être plus grand dans une dimension que la taille maximale de l'atlas + + unsigned int maxAtlasSize = GetRealMaxAtlasSize(); + + // Cette fonction ne fait qu'insérer un rectangle de façon virtuelle, l'insertion des images se fait après + for (unsigned int i = 0; i < m_atlases.size(); ++i) + { + Atlas& atlas = m_atlases[i]; + if (atlas.binPack.Insert(rect, flipped, 1, false, NzGuillotineBinPack::RectBestAreaFit, NzGuillotineBinPack::SplitMinimizeArea)) + // Insertion réussie dans l'un des atlas, pas de question à se poser + return i; + else if (i == m_atlases.size() - 1) // Dernière itération ? + { + // Dernier atlas, et le glyphe ne rentre pas, peut-on agrandir la taille de l'atlas ? + unsigned int size = atlas.binPack.GetWidth(); // l'atlas étant carré, on ne teste qu'une dimension + if (size < maxAtlasSize) + { + // On peut encore agrandir l'atlas + size = std::min(size*2, maxAtlasSize); + atlas.binPack.Expand(size, size); + + // On relance la boucle sur le dernier atlas + i--; + } + else + { + // On ne peut plus agrandir le dernier atlas, il est temps d'en créer un nouveau + m_atlases.resize(m_atlases.size() + 1); + Atlas& newAtlas = m_atlases.back(); + + newAtlas.binPack.Reset(s_atlasStartSize, s_atlasStartSize); + + // On laisse la boucle insérer toute seule le rectangle à la prochaine itération + } + } + } + + // Si nous arrivons ici, c'est qu'une erreur a eu lieu en amont + NazaraInternalError("This shouldn't happen"); + return std::numeric_limits::max(); +} + +const NzFont::Glyph& NzFont::PrecacheGlyph(GlyphMap& glyphMap, unsigned int characterSize, bool bold, char32_t character) const +{ + auto it = glyphMap.find(character); + if (it != glyphMap.end()) // Si le glyphe n'est pas déjà chargé + return it->second; + + Glyph glyph; + glyph.valid = false; + + // On extrait le glyphe depuis la police + NzFontGlyph fontGlyph; + if (ExtractGlyph(characterSize, character, bold, &fontGlyph)) + { + glyph.atlasRect.width = fontGlyph.image.GetWidth(); + glyph.atlasRect.height = fontGlyph.image.GetHeight(); + + unsigned int maxAtlasSize = GetRealMaxAtlasSize(); + if (glyph.atlasRect.width <= maxAtlasSize && glyph.atlasRect.height <= maxAtlasSize) + { + // Insertion du rectangle dans l'un des atlas + glyph.aabb = fontGlyph.aabb; + glyph.advance = fontGlyph.advance; + glyph.valid = true; + + if (glyph.atlasRect.width > 0 && glyph.atlasRect.height > 0) // Si l'image contient quelque chose + { + // Padding (pour éviter le débordement lors du filtrage) + const unsigned int padding = 1; // Un pixel entre chaque glyphe + + glyph.atlasRect.width += padding; + glyph.atlasRect.height += padding; + + // Insertion du rectangle dans l'atlas virtuel + glyph.atlasIndex = InsertRect(&glyph.atlasRect, &glyph.flipped); + + glyph.atlasRect.width -= padding; + glyph.atlasRect.height -= padding; + + // Mise en queue pour insertion dans l'atlas réel + m_glyphQueue.resize(m_glyphQueue.size()+1); + QueuedGlyph& queuedGlyph = m_glyphQueue.back(); + queuedGlyph.codepoint = character; + queuedGlyph.image = std::move(fontGlyph.image); + queuedGlyph.map = &glyphMap; + } + } + else + { + NazaraWarning("Glyph \"" + NzString::Unicode(character) + "\" is bigger than max atlas size"); + } + } + else + { + NazaraWarning("Failed to extract glyph \"" + NzString::Unicode(character) + "\""); + } + + return glyphMap.insert(std::make_pair(character, std::move(glyph))).first->second; +} + +void NzFont::ProcessGlyphQueue() const +{ + for (QueuedGlyph& queuedGlyph : m_glyphQueue) + { + GlyphMap& glyphMap = *queuedGlyph.map; + auto glyphIt = glyphMap.find(queuedGlyph.codepoint); + if (glyphIt == glyphMap.end()) + continue; // Le glyphe a certainement été supprimé du cache avant la mise à jour de l'atlas + + Glyph& glyph = glyphIt->second; + Atlas& atlas = m_atlases[glyph.atlasIndex]; + + // On s'assure que l'atlas est de la bonne taille + NzVector2ui atlasSize(atlas.image.GetWidth(), atlas.image.GetHeight()); + NzVector2ui binPackSize = atlas.binPack.GetSize(); + if (atlasSize != binPackSize) + { + // Création d'une nouvelle image + NzImage newAtlas(nzImageType_2D, nzPixelFormat_A8, binPackSize.x, binPackSize.y); + if (atlas.image.IsValid()) + { + newAtlas.Copy(atlas.image, NzRectui(atlasSize), NzVector2ui(0, 0)); // On copie les anciennes données + + // On initialise les nouvelles régions + newAtlas.Fill(NzColor(255, 255, 255, 0), NzRectui(0, atlasSize.y, binPackSize.x, binPackSize.y - atlasSize.y)); + newAtlas.Fill(NzColor(255, 255, 255, 0), NzRectui(atlasSize.x, 0, binPackSize.x - atlasSize.x, atlasSize.y)); + } + else + newAtlas.Fill(NzColor(255, 255, 255, 0)); // On initialise les pixels + + atlas.image = std::move(newAtlas); // On déplace la nouvelle image vers l'atlas + } + + unsigned int glyphWidth = queuedGlyph.image.GetWidth(); + unsigned int glyphHeight = queuedGlyph.image.GetHeight(); + + // On copie le glyphe dans l'atlas + if (glyph.flipped) + { + // On tourne le glyphe pour qu'il rentre dans le rectangle + const nzUInt8* src = queuedGlyph.image.GetConstPixels(); + nzUInt8* ptr = atlas.image.GetPixels(glyph.atlasRect.x, glyph.atlasRect.y + glyphWidth - 1); + + unsigned int lineStride = atlas.image.GetWidth(); // BPP = 1 + for (unsigned int y = 0; y < glyphHeight; ++y) + { + for (unsigned int x = 0; x < glyphWidth; ++x) + { + *ptr = *src++; // On copie et on avance dans le glyphe + ptr -= lineStride; // On remonte d'une ligne + } + + ptr += lineStride*glyphWidth + 1; + } + } + else + atlas.image.Copy(queuedGlyph.image, NzRectui(glyphWidth, glyphHeight), glyph.atlasRect.GetPosition()); + + queuedGlyph.image.Destroy(); // On libère l'image dès que possible (pour réduire la consommation) + } + + m_glyphQueue.clear(); +} + +NzFontLoader::LoaderList NzFont::s_loaders; +unsigned int NzFont::s_maxAtlasSize = 8192; // Valeur totalement arbitraire diff --git a/src/Nazara/Utility/FontData.cpp b/src/Nazara/Utility/FontData.cpp new file mode 100644 index 000000000..a5f4b13bd --- /dev/null +++ b/src/Nazara/Utility/FontData.cpp @@ -0,0 +1,8 @@ +// Copyright (C) 2014 Jérôme Leclercq +// This file is part of the "Nazara Engine - Utility module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include + +NzFontData::~NzFontData() = default; diff --git a/src/Nazara/Utility/Loaders/FreeType.hpp b/src/Nazara/Utility/Loaders/FreeType.hpp new file mode 100644 index 000000000..6ee1f60ab --- /dev/null +++ b/src/Nazara/Utility/Loaders/FreeType.hpp @@ -0,0 +1,15 @@ +// Copyright (C) 2014 Jérôme Leclercq +// This file is part of the "Nazara Engine - Utility module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_LOADERS_FREETYPE_HPP +#define NAZARA_LOADERS_FREETYPE_HPP + +#include + +void NzLoaders_FreeType_Register(); +void NzLoaders_FreeType_Unregister(); + +#endif // NAZARA_LOADERS_FREETYPE_HPP diff --git a/src/Nazara/Utility/Loaders/FreeType/Loader.cpp b/src/Nazara/Utility/Loaders/FreeType/Loader.cpp new file mode 100644 index 000000000..71269d9da --- /dev/null +++ b/src/Nazara/Utility/Loaders/FreeType/Loader.cpp @@ -0,0 +1,393 @@ +// Copyright (C) 2014 Jérôme Leclercq - 2009 Cruden BV +// This file is part of the "Nazara Engine - Utility module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include +#include FT_FREETYPE_H +#include FT_BITMAP_H +#include FT_OUTLINE_H +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + FT_Library s_library = nullptr; + float s_invScaleFactor = 1.f / (1 << 6); // 1/64 + + extern "C" + unsigned long FT_StreamRead(FT_Stream stream, unsigned long offset, unsigned char* buffer, unsigned long count) + { + // http://www.freetype.org/freetype2/docs/reference/ft2-system_interface.html#FT_Stream_IoFunc + NzInputStream& inputStream = *static_cast(stream->descriptor.pointer); + + // La valeur de count indique une opération de lecture ou de positionnement + if (count > 0) + { + // Dans le premier cas, une erreur est symbolisée par un retour nul + if (inputStream.SetCursorPos(offset)) + return static_cast(inputStream.Read(buffer, count)); + else + return 0; + } + else + { + // Dans le second cas, une erreur est symbolisée par un retour non-nul + if (inputStream.SetCursorPos(offset)) + return 0; + else + return 42; // La réponse à la grande question + } + } + + extern "C" + void FT_StreamClose(FT_Stream stream) + { + // http://www.freetype.org/freetype2/docs/reference/ft2-system_interface.html#FT_Stream_CloseFunc + // Les streams dans Nazara ne se ferment pas explicitement + NazaraUnused(stream); + } + + class FreeTypeStream : public NzFontData + { + public: + FreeTypeStream() : + m_face(nullptr), + m_characterSize(0) + { + } + + ~FreeTypeStream() + { + if (m_face) + FT_Done_Face(m_face); + } + + bool Check() + { + // Test d'ouverture (http://www.freetype.org/freetype2/docs/reference/ft2-base_interface.html#FT_Open_Face) + return FT_Open_Face(s_library, &m_args, -1, nullptr) == 0; + } + + bool ExtractGlyph(unsigned int characterSize, char32_t character, bool bold, NzFontGlyph* dst) + { + #ifdef NAZARA_DEBUG + if (!dst) + { + NazaraError("Glyph destination cannot be null"); + return false; + } + #endif + + SetCharacterSize(characterSize); + + if (FT_Load_Char(m_face, character, FT_LOAD_FORCE_AUTOHINT | FT_LOAD_TARGET_NORMAL) != 0) + { + NazaraError("Failed to load character"); + return false; + } + + FT_GlyphSlot& glyph = m_face->glyph; + + const FT_Pos boldStrength = 1 << 6; + + bool outlineFormat = (glyph->format == FT_GLYPH_FORMAT_OUTLINE); + if (outlineFormat && bold) + // http://www.freetype.org/freetype2/docs/reference/ft2-outline_processing.html#FT_Outline_Embolden + FT_Outline_Embolden(&glyph->outline, boldStrength); + + // http://www.freetype.org/freetype2/docs/reference/ft2-glyph_management.html#FT_Glyph_To_Bitmap + // Conversion du glyphe vers le format bitmap + // Cette fonction ne fait rien dans le cas où le glyphe est déjà un bitmap + if (FT_Render_Glyph(glyph, FT_RENDER_MODE_NORMAL) != 0) + { + NazaraError("Failed to convert glyph to bitmap"); + return false; + } + + // Dans le cas où nous voulons des caractères gras mais que nous n'avons pas pu agir plus tôt + // nous demandons à FreeType d'agir directement sur le bitmap généré + if (!outlineFormat && bold) + { + // http://www.freetype.org/freetype2/docs/reference/ft2-bitmap_handling.html#FT_Bitmap_Embolden + // "If you want to embolden the bitmap owned by a FT_GlyphSlot_Rec, you should call FT_GlyphSlot_Own_Bitmap on the slot first" + FT_GlyphSlot_Own_Bitmap(glyph); + FT_Bitmap_Embolden(s_library, &glyph->bitmap, boldStrength, boldStrength); + } + + dst->advance = glyph->metrics.horiAdvance >> 6; + if (bold) + dst->advance += boldStrength >> 6; + + dst->aabb.x = glyph->metrics.horiBearingX >> 6; + dst->aabb.y = -(glyph->metrics.horiBearingY >> 6); // Inversion du repère + dst->aabb.width = glyph->metrics.width >> 6; + dst->aabb.height = glyph->metrics.height >> 6; + + unsigned int width = glyph->bitmap.width; + unsigned int height = glyph->bitmap.rows; + + if (width > 0 && height > 0) + { + dst->image.Create(nzImageType_2D, nzPixelFormat_A8, width, height); + nzUInt8* pixels = dst->image.GetPixels(); + + const nzUInt8* data = glyph->bitmap.buffer; + + // Selon la documentation FreeType, le glyphe peut être encodé en format A8 (huit bits d'alpha par pixel) + // ou au format A1 (un bit d'alpha par pixel). + // Cependant dans un cas comme dans l'autre, il nous faut gérer le pitch (les données peuvent ne pas être contigues) + // ainsi que le padding dans le cas du format A1 (Chaque ligne prends un nombre fixe d'octets) + if (glyph->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) + { + // Format A1 + for (unsigned int y = 0; y < height; ++y) + { + for (unsigned int x = 0; x < width; ++x) + *pixels++ = (data[x/8] & (1 << (7 - x%8)) ? 255 : 0); + + data += glyph->bitmap.pitch; + } + } + else + { + // Format A8 + if (glyph->bitmap.pitch == static_cast(width*sizeof(nzUInt8))) // Pouvons-nous copier directement ? + dst->image.Update(glyph->bitmap.buffer); + else + { + for (unsigned int y = 0; y < height; ++y) + { + std::memcpy(pixels, data, width*sizeof(nzUInt8)); + data += glyph->bitmap.pitch; + pixels += width*sizeof(nzUInt8); + } + } + } + } + else + dst->image.Destroy(); // On s'assure que l'image ne contient alors rien + + return true; + } + + NzString GetFamilyName() const + { + return m_face->family_name; + } + + NzString GetStyleName() const + { + return m_face->style_name; + } + + bool HasKerning() const + { + return FT_HAS_KERNING(m_face); + } + + bool IsScalable() const + { + return FT_IS_SCALABLE(m_face); + } + + bool Open() + { + return FT_Open_Face(s_library, &m_args, 0, &m_face) == 0; + } + + int QueryKerning(unsigned int characterSize, char32_t first, char32_t second) const + { + if (FT_HAS_KERNING(m_face)) + { + SetCharacterSize(characterSize); + + FT_Vector kerning; + FT_Get_Kerning(m_face, FT_Get_Char_Index(m_face, first), FT_Get_Char_Index(m_face, second), FT_KERNING_DEFAULT, &kerning); + + if (!FT_IS_SCALABLE(m_face)) + return kerning.x; // Taille déjà précisée en pixels dans ce cas + + return kerning.x >> 6; + } + else + return 0; + } + + unsigned int QueryLineHeight(unsigned int characterSize) const + { + SetCharacterSize(characterSize); + + // http://www.freetype.org/freetype2/docs/reference/ft2-base_interface.html#FT_Size_Metrics + return m_face->size->metrics.height >> 6; + } + + float QueryUnderlinePosition(unsigned int characterSize) const + { + if (FT_IS_SCALABLE(m_face)) + { + SetCharacterSize(characterSize); + + // http://www.freetype.org/freetype2/docs/reference/ft2-base_interface.html#FT_FaceRec + return static_cast(FT_MulFix(m_face->underline_position, m_face->size->metrics.y_scale)) * s_invScaleFactor; + } + else + return characterSize / 10.f; // Joker ? + } + + float QueryUnderlineThickness(unsigned int characterSize) const + { + if (FT_IS_SCALABLE(m_face)) + { + SetCharacterSize(characterSize); + + // http://www.freetype.org/freetype2/docs/reference/ft2-base_interface.html#FT_FaceRec + return static_cast(FT_MulFix(m_face->underline_thickness, m_face->size->metrics.y_scale)) * s_invScaleFactor; + } + else + return characterSize/15.f; // Joker ? + } + + bool SetFile(const NzString& filePath) + { + if (!m_file.Open(filePath, NzFile::ReadOnly)) + { + NazaraError("Failed to open stream from file: " + NzError::GetLastError()); + return false; + } + + SetStream(m_file); + return true; + } + + void SetStream(NzInputStream& stream) + { + m_stream.base = nullptr; + m_stream.close = FT_StreamClose; + m_stream.descriptor.pointer = &stream; + m_stream.read = FT_StreamRead; + m_stream.pos = 0; + m_stream.size = stream.GetSize(); + + m_args.driver = 0; + m_args.flags = FT_OPEN_STREAM; + m_args.stream = &m_stream; + } + + private: + void SetCharacterSize(unsigned int characterSize) const + { + if (m_characterSize != characterSize) + { + FT_Set_Pixel_Sizes(m_face, 0, characterSize); + m_characterSize = characterSize; + } + } + + FT_Open_Args m_args; + FT_Face m_face; + FT_StreamRec m_stream; + NzFile m_file; + mutable unsigned int m_characterSize; + }; + + bool IsSupported(const NzString& extension) + { + ///FIXME: Je suppose qu'il en manque quelques uns.. + static std::set supportedExtensions = { + "afm", "bdf", "cff", "cid", "dfont", "fnt", "pfa", "pfb", "pfm", "pfr", "sfnt", "tte", "ttf" + }; + + return supportedExtensions.find(extension) != supportedExtensions.end(); + } + + nzTernary Check(NzInputStream& stream, const NzFontParams& parameters) + { + NazaraUnused(parameters); + + FreeTypeStream face; + face.SetStream(stream); + + if (face.Check()) + return nzTernary_True; + else + return nzTernary_False; + } + + bool LoadFile(NzFont* font, const NzString& filePath, const NzFontParams& parameters) + { + NazaraUnused(parameters); + + std::unique_ptr face(new FreeTypeStream); + + if (!face->SetFile(filePath)) + { + NazaraError("Failed to open file"); + return false; + } + + if (!face->Open()) + { + NazaraError("Failed to open face"); + return false; + } + + if (font->Create(face.get())) + { + face.release(); + return true; + } + else + return false; + } + + bool LoadStream(NzFont* font, NzInputStream& stream, const NzFontParams& parameters) + { + NazaraUnused(parameters); + + std::unique_ptr face(new FreeTypeStream); + face->SetStream(stream); + + if (!face->Open()) + { + NazaraError("Failed to open face"); + return false; + } + + if (font->Create(face.get())) + { + face.release(); + return true; + } + else + return false; + } +} + +void NzLoaders_FreeType_Register() +{ + if (FT_Init_FreeType(&s_library) == 0) + NzFontLoader::RegisterLoader(IsSupported, Check, LoadStream, LoadFile); + else + { + s_library = nullptr; // On s'assure que le pointeur ne pointe pas sur n'importe quoi + NazaraWarning("Failed to initialize FreeType library"); + } +} + +void NzLoaders_FreeType_Unregister() +{ + if (s_library) + { + NzFontLoader::UnregisterLoader(IsSupported, Check, LoadStream, LoadFile); + + FT_Done_FreeType(s_library); + s_library = nullptr; + } +} diff --git a/src/Nazara/Utility/Utility.cpp b/src/Nazara/Utility/Utility.cpp index 885e4a578..c12459277 100644 --- a/src/Nazara/Utility/Utility.cpp +++ b/src/Nazara/Utility/Utility.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -70,6 +71,9 @@ bool NzUtility::Initialize() // Il s'agit ici d'une liste LIFO, le dernier loader enregistré possède la priorité /// Loaders génériques + // Font + NzLoaders_FreeType_Register(); + // Image NzLoaders_STB_Register(); // Loader générique (STB) @@ -109,6 +113,7 @@ void NzUtility::Uninitialize() // Libération du module s_moduleReferenceCounter = 0; + NzLoaders_FreeType_Unregister(); NzLoaders_MD2_Unregister(); NzLoaders_MD5Anim_Unregister(); NzLoaders_MD5Mesh_Unregister();