raze/source/common/fonts/font.cpp
2020-09-27 07:33:43 +02:00

1276 lines
34 KiB
C++

/*
** v_font.cpp
** Font management
**
**---------------------------------------------------------------------------
** Copyright 1998-2016 Randy Heit
** Copyright 2005-2019 Christoph Oelckers
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in the
** documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
** derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/
// HEADER FILES ------------------------------------------------------------
#include <cwctype>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "templates.h"
#include "m_swap.h"
#include "v_font.h"
#include "printf.h"
#include "textures.h"
#include "filesystem.h"
#include "cmdlib.h"
#include "sc_man.h"
#include "gstrings.h"
#include "image.h"
#include "utf8.h"
#include "myiswalpha.h"
#include "fontchars.h"
#include "multipatchtexture.h"
#include "texturemanager.h"
#include "fontinternals.h"
//==========================================================================
//
// FFont :: FFont
//
// Loads a multi-texture font.
//
//==========================================================================
FFont::FFont (const char *name, const char *nametemplate, const char *filetemplate, int lfirst, int lcount, int start, int fdlump, int spacewidth, bool notranslate, bool iwadonly, bool doomtemplate, GlyphSet *baseGlyphs)
{
int i;
FTextureID lump;
char buffer[12];
DVector2 Scale = { 1, 1 };
noTranslate = notranslate;
Lump = fdlump;
GlobalKerning = false;
FontName = name;
Next = FirstFont;
FirstFont = this;
Cursor = '_';
ActiveColors = 0;
SpaceWidth = 0;
FontHeight = 0;
uint8_t pp = 0;
for (auto &p : PatchRemap) p = pp++;
translateUntranslated = false;
int FixedWidth = 0;
TMap<int, FGameTexture*> charMap;
int minchar = INT_MAX;
int maxchar = INT_MIN;
// Read the font's configuration.
// This will not be done for the default fonts, because they are not atomic and the default content does not need it.
TArray<FolderEntry> folderdata;
if (filetemplate != nullptr)
{
FStringf path("fonts/%s/", filetemplate);
// If a name template is given, collect data from all resource files.
// For anything else, each folder is being treated as an atomic, self-contained unit and mixing from different glyph sets is blocked.
fileSystem.GetFilesInFolder(path, folderdata, nametemplate == nullptr);
//if (nametemplate == nullptr)
{
FStringf infpath("fonts/%s/font.inf", filetemplate);
unsigned index = folderdata.FindEx([=](const FolderEntry &entry)
{
return infpath.CompareNoCase(entry.name) == 0;
});
if (index < folderdata.Size())
{
FScanner sc;
sc.OpenLumpNum(folderdata[index].lumpnum);
while (sc.GetToken())
{
sc.TokenMustBe(TK_Identifier);
if (sc.Compare("Kerning"))
{
sc.MustGetValue(false);
GlobalKerning = sc.Number;
}
else if (sc.Compare("Scale"))
{
sc.MustGetValue(true);
Scale.Y = Scale.X = sc.Float;
if (sc.CheckToken(','))
{
sc.MustGetValue(true);
Scale.Y = sc.Float;
}
}
else if (sc.Compare("SpaceWidth"))
{
sc.MustGetValue(false);
SpaceWidth = sc.Number;
}
else if (sc.Compare("FontHeight"))
{
sc.MustGetValue(false);
FontHeight = sc.Number;
}
else if (sc.Compare("CellSize"))
{
sc.MustGetValue(false);
FixedWidth = sc.Number;
sc.MustGetToken(',');
sc.MustGetValue(false);
FontHeight = sc.Number;
}
else if (sc.Compare("Translationtype"))
{
sc.MustGetToken(TK_Identifier);
if (sc.Compare("console"))
{
TranslationType = 1;
}
else if (sc.Compare("standard"))
{
TranslationType = 0;
}
else
{
sc.ScriptError("Unknown translation type %s", sc.String);
}
}
}
}
}
}
if (FixedWidth > 0)
{
ReadSheetFont(folderdata, FixedWidth, FontHeight, Scale);
Type = Folder;
}
else
{
if (baseGlyphs)
{
// First insert everything from the given glyph set.
GlyphSet::Iterator it(*baseGlyphs);
GlyphSet::Pair* pair;
while (it.NextPair(pair))
{
if (pair->Value && pair->Value->GetTexelWidth() > 0 && pair->Value->GetTexelHeight() > 0)
{
auto position = pair->Key;
if (position < minchar) minchar = position;
if (position > maxchar) maxchar = position;
charMap.Insert(position, pair->Value);
}
}
}
if (nametemplate != nullptr)
{
if (!iwadonly)
{
for (i = 0; i < lcount; i++)
{
int position = lfirst + i;
mysnprintf(buffer, countof(buffer), nametemplate, i + start);
lump = TexMan.CheckForTexture(buffer, ETextureType::MiscPatch);
if (doomtemplate && lump.isValid() && i + start == 121)
{ // HACKHACK: Don't load STCFN121 in doom(2), because
// it's not really a lower-case 'y' but a '|'.
// Because a lot of wads with their own font seem to foolishly
// copy STCFN121 and make it a '|' themselves, wads must
// provide STCFN120 (x) and STCFN122 (z) for STCFN121 to load as a 'y'.
FStringf c120(nametemplate, 120);
FStringf c122(nametemplate, 122);
if (!TexMan.CheckForTexture(c120, ETextureType::MiscPatch).isValid() ||
!TexMan.CheckForTexture(c122, ETextureType::MiscPatch).isValid())
{
// insert the incorrectly named '|' graphic in its correct position.
position = 124;
}
}
if (lump.isValid())
{
Type = Multilump;
if (position < minchar) minchar = position;
if (position > maxchar) maxchar = position;
charMap.Insert(position, TexMan.GetGameTexture(lump));
}
}
}
else
{
FGameTexture *texs[256] = {};
if (lcount > 256 - start) lcount = 256 - start;
for (i = 0; i < lcount; i++)
{
TArray<FTextureID> array;
mysnprintf(buffer, countof(buffer), nametemplate, i + start);
TexMan.ListTextures(buffer, array, true);
for (auto entry : array)
{
auto tex = TexMan.GetGameTexture(entry, false);
if (tex && !tex->isUserContent() && tex->GetUseType() == ETextureType::MiscPatch)
{
texs[i] = tex;
}
}
}
if (doomtemplate)
{
// Handle the misplaced '|'.
if (texs[121 - '!'] && !texs[120 - '!'] && !texs[122 - '!'] && !texs[124 - '!'])
{
texs[124 - '!'] = texs[121 - '!'];
texs[121 - '!'] = nullptr;
}
}
for (i = 0; i < lcount; i++)
{
if (texs[i])
{
int position = lfirst + i;
Type = Multilump;
if (position < minchar) minchar = position;
if (position > maxchar) maxchar = position;
charMap.Insert(position, texs[i]);
}
}
}
}
if (folderdata.Size() > 0)
{
// all valid lumps must be named with a hex number that represents its Unicode character index.
for (auto &entry : folderdata)
{
char *endp;
auto base = ExtractFileBase(entry.name);
auto position = strtoll(base.GetChars(), &endp, 16);
if ((*endp == 0 || (*endp == '.' && position >= '!' && position < 0xffff)))
{
auto lump = TexMan.CheckForTexture(entry.name, ETextureType::MiscPatch);
if (lump.isValid())
{
if ((int)position < minchar) minchar = (int)position;
if ((int)position > maxchar) maxchar = (int)position;
auto tex = TexMan.GetGameTexture(lump);
tex->SetScale((float)Scale.X, (float)Scale.Y);
charMap.Insert((int)position, tex);
Type = Folder;
}
}
}
}
FirstChar = minchar;
LastChar = maxchar;
auto count = maxchar - minchar + 1;
Chars.Resize(count);
int fontheight = 0;
int asciiheight = 0;
for (i = 0; i < count; i++)
{
auto lump = charMap.CheckKey(FirstChar + i);
if (lump != nullptr)
{
auto pic = *lump;
if (pic != nullptr)
{
double fheight = pic->GetDisplayHeight();
double yoffs = pic->GetDisplayTopOffset();
int height = int(fheight + abs(yoffs) + 0.5);
if (height > fontheight)
{
fontheight = height;
}
if (height > asciiheight && FirstChar + 1 < 128)
{
asciiheight = height;
}
}
auto orig = pic->GetTexture();
auto tex = MakeGameTexture(orig, nullptr, ETextureType::FontChar);
tex->CopySize(pic, true);
TexMan.AddGameTexture(tex);
Chars[i].OriginalPic = tex;
if (!noTranslate)
{
Chars[i].TranslatedPic = MakeGameTexture(new FImageTexture(new FFontChar1(orig->GetImage())), nullptr, ETextureType::FontChar);
Chars[i].TranslatedPic->CopySize(pic, true);
TexMan.AddGameTexture(Chars[i].TranslatedPic);
}
else
{
Chars[i].TranslatedPic = tex;
}
Chars[i].XMove = (int)Chars[i].TranslatedPic->GetDisplayWidth();
}
else
{
Chars[i].TranslatedPic = nullptr;
Chars[i].XMove = INT_MIN;
}
}
if (SpaceWidth == 0) // An explicit override from the .inf file must always take precedence
{
if (spacewidth != -1)
{
SpaceWidth = spacewidth;
}
else if ('N' - FirstChar >= 0 && 'N' - FirstChar < count && Chars['N' - FirstChar].TranslatedPic != nullptr)
{
SpaceWidth = (Chars['N' - FirstChar].XMove + 1) / 2;
}
else
{
SpaceWidth = 4;
}
}
if (FontHeight == 0) FontHeight = fontheight;
if (AsciiHeight == 0) AsciiHeight = asciiheight;
FixXMoves();
}
}
void FFont::ReadSheetFont(TArray<FolderEntry> &folderdata, int width, int height, const DVector2 &Scale)
{
// all valid lumps must be named with a hex number that represents the Unicode character index for its first character,
TArray<TexPartBuild> part(1, true);
TMap<int, FGameTexture*> charMap;
int minchar = INT_MAX;
int maxchar = INT_MIN;
for (auto &entry : folderdata)
{
char *endp;
auto base = ExtractFileBase(entry.name);
auto position = strtoll(base.GetChars(), &endp, 16);
if ((*endp == 0 || (*endp == '.' && position >= 0 && position < 0xffff))) // Sheet fonts may fill in the low control chars.
{
auto lump = TexMan.CheckForTexture(entry.name, ETextureType::MiscPatch);
if (lump.isValid())
{
auto tex = TexMan.GetGameTexture(lump);
int numtex_x = tex->GetTexelWidth() / width;
int numtex_y = tex->GetTexelHeight() / height;
int maxinsheet = int(position) + numtex_x * numtex_y - 1;
if (minchar > position) minchar = int(position);
if (maxchar < maxinsheet) maxchar = maxinsheet;
for (int y = 0; y < numtex_y; y++)
{
for (int x = 0; x < numtex_x; x++)
{
part[0].OriginX = -width * x;
part[0].OriginY = -height * y;
part[0].TexImage = static_cast<FImageTexture*>(tex->GetTexture());
FMultiPatchTexture *image = new FMultiPatchTexture(width, height, part, false, false);
FImageTexture *tex = new FImageTexture(image);
auto gtex = MakeGameTexture(tex, nullptr, ETextureType::FontChar);
gtex->SetWorldPanning(true);
gtex->SetOffsets(0, 0, 0);
gtex->SetOffsets(1, 0, 0);
gtex->SetScale((float)Scale.X, (float)Scale.Y);
TexMan.AddGameTexture(gtex);
charMap.Insert(int(position) + x + y * numtex_x, gtex);
}
}
}
}
}
FirstChar = minchar;
bool map1252 = false;
if (minchar < 0x80 && maxchar >= 0xa0) // should be a settable option, but that'd probably cause more problems than it'd solve.
{
if (maxchar < 0x2122) maxchar = 0x2122;
map1252 = true;
}
LastChar = maxchar;
auto count = maxchar - minchar + 1;
Chars.Resize(count);
int fontheight = 0;
for (int i = 0; i < count; i++)
{
auto lump = charMap.CheckKey(FirstChar + i);
if (lump != nullptr)
{
auto pic = (*lump)->GetTexture();
Chars[i].OriginalPic = (*lump)->GetUseType() == ETextureType::FontChar? (*lump) : MakeGameTexture(pic, nullptr, ETextureType::FontChar);
Chars[i].OriginalPic->SetUseType(ETextureType::FontChar);
Chars[i].OriginalPic->CopySize(*lump, true);
Chars[i].TranslatedPic = MakeGameTexture(new FImageTexture(new FFontChar1(pic->GetImage())), nullptr, ETextureType::FontChar);
Chars[i].TranslatedPic->CopySize(*lump, true);
Chars[i].TranslatedPic->SetUseType(ETextureType::FontChar);
if (Chars[i].OriginalPic != *lump) TexMan.AddGameTexture(Chars[i].OriginalPic);
TexMan.AddGameTexture(Chars[i].TranslatedPic);
}
Chars[i].XMove = width;
}
if (map1252)
{
// Move the Windows-1252 characters to their proper place.
for (int i = 0x80; i < 0xa0; i++)
{
if (win1252map[i - 0x80] != i && Chars[i - minchar].TranslatedPic != nullptr && Chars[win1252map[i - 0x80] - minchar].TranslatedPic == nullptr)
{
std::swap(Chars[i - minchar], Chars[win1252map[i - 0x80] - minchar]);
}
}
}
SpaceWidth = width;
}
//==========================================================================
//
// FFont :: ~FFont
//
//==========================================================================
FFont::~FFont ()
{
FFont **prev = &FirstFont;
FFont *font = *prev;
while (font != nullptr && font != this)
{
prev = &font->Next;
font = *prev;
}
if (font != nullptr)
{
*prev = font->Next;
}
}
//==========================================================================
//
// FFont :: CheckCase
//
//==========================================================================
void FFont::CheckCase()
{
int lowercount = 0, uppercount = 0;
for (unsigned i = 0; i < Chars.Size(); i++)
{
unsigned chr = i + FirstChar;
if (lowerforupper[chr] == chr && upperforlower[chr] == chr)
{
continue; // not a letter;
}
if (myislower(chr))
{
if (Chars[i].TranslatedPic != nullptr) lowercount++;
}
else
{
if (Chars[i].TranslatedPic != nullptr) uppercount++;
}
}
if (lowercount == 0) return; // This is an uppercase-only font and we are done.
// The ß needs special treatment because it is far more likely to be supplied lowercase only, even in an uppercase font.
if (Chars[0xdf - FirstChar].TranslatedPic != nullptr)
{
if (LastChar < 0x1e9e)
{
Chars.Resize(0x1e9f - FirstChar);
LastChar = 0x1e9e;
}
if (Chars[0x1e9e - FirstChar].TranslatedPic == nullptr)
{
std::swap(Chars[0xdf - FirstChar], Chars[0x1e9e - FirstChar]);
lowercount--;
uppercount++;
if (lowercount == 0) return;
}
}
}
//==========================================================================
//
// FFont :: FindFont
//
// Searches for the named font in the list of loaded fonts, returning the
// font if it was found. The disk is not checked if it cannot be found.
//
//==========================================================================
FFont *FFont::FindFont (FName name)
{
if (name == NAME_None)
{
return nullptr;
}
FFont *font = FirstFont;
while (font != nullptr)
{
if (font->FontName == name) return font;
font = font->Next;
}
return nullptr;
}
//==========================================================================
//
// RecordTextureColors
//
// Given a 256 entry buffer, sets every entry that corresponds to a color
// used by the texture to 1.
//
//==========================================================================
void RecordTextureColors (FImageSource *pic, uint32_t *usedcolors)
{
int x;
auto pixels = pic->GetPalettedPixels(false);
auto size = pic->GetWidth() * pic->GetHeight();
for(x = 0;x < size; x++)
{
usedcolors[pixels[x]]++;
}
}
//==========================================================================
//
// RecordAllTextureColors
//
// Given a 256 entry buffer, sets every entry that corresponds to a color
// used by the font.
//
//==========================================================================
void FFont::RecordAllTextureColors(uint32_t *usedcolors)
{
for (unsigned int i = 0; i < Chars.Size(); i++)
{
if (Chars[i].TranslatedPic)
{
FFontChar1 *pic = static_cast<FFontChar1 *>(Chars[i].TranslatedPic->GetTexture()->GetImage());
if (pic)
{
// The remap must be temporarily reset here because this can be called on an initialized font.
auto sr = pic->ResetSourceRemap();
RecordTextureColors(pic, usedcolors);
pic->SetSourceRemap(sr);
}
}
}
}
//==========================================================================
//
// SetDefaultTranslation
//
// Builds a translation to map the stock font to a mod provided replacement.
//
//==========================================================================
void FFont::SetDefaultTranslation(uint32_t *othercolors)
{
uint32_t mycolors[256] = {};
RecordAllTextureColors(mycolors);
uint8_t mytranslation[256], othertranslation[256], myreverse[256], otherreverse[256];
TArray<double> myluminosity, otherluminosity;
SimpleTranslation(mycolors, mytranslation, myreverse, myluminosity);
SimpleTranslation(othercolors, othertranslation, otherreverse, otherluminosity);
FRemapTable remap(ActiveColors);
remap.Remap[0] = 0;
remap.Palette[0] = 0;
remap.ForFont = true;
for (unsigned l = 1; l < myluminosity.Size(); l++)
{
for (unsigned o = 1; o < otherluminosity.Size()-1; o++) // luminosity[0] is for the transparent color
{
if (myluminosity[l] >= otherluminosity[o] && myluminosity[l] <= otherluminosity[o+1])
{
PalEntry color1 = GPalette.BaseColors[otherreverse[o]];
PalEntry color2 = GPalette.BaseColors[otherreverse[o+1]];
double weight = 0;
if (otherluminosity[o] != otherluminosity[o + 1])
{
weight = (myluminosity[l] - otherluminosity[o]) / (otherluminosity[o + 1] - otherluminosity[o]);
}
int r = int(color1.r + weight * (color2.r - color1.r));
int g = int(color1.g + weight * (color2.g - color1.g));
int b = int(color1.b + weight * (color2.b - color1.b));
r = clamp(r, 0, 255);
g = clamp(g, 0, 255);
b = clamp(b, 0, 255);
remap.Remap[l] = ColorMatcher.Pick(r, g, b);
remap.Palette[l] = PalEntry(255, r, g, b);
break;
}
}
}
Translations[CR_UNTRANSLATED] = GPalette.StoreTranslation(TRANSLATION_Internal, &remap);
forceremap = true;
}
//==========================================================================
//
// compare
//
// Used for sorting colors by brightness.
//
//==========================================================================
static int compare (const void *arg1, const void *arg2)
{
if (RPART(GPalette.BaseColors[*((uint8_t *)arg1)]) * 299 +
GPART(GPalette.BaseColors[*((uint8_t *)arg1)]) * 587 +
BPART(GPalette.BaseColors[*((uint8_t *)arg1)]) * 114 <
RPART(GPalette.BaseColors[*((uint8_t *)arg2)]) * 299 +
GPART(GPalette.BaseColors[*((uint8_t *)arg2)]) * 587 +
BPART(GPalette.BaseColors[*((uint8_t *)arg2)]) * 114)
return -1;
else
return 1;
}
//==========================================================================
//
// FFont :: SimpleTranslation
//
// Colorsused, translation, and reverse must all be 256 entry buffers.
// Colorsused must already be filled out.
// Translation be set to remap the source colors to a new range of
// consecutive colors based at 1 (0 is transparent).
// Reverse will be just the opposite of translation: It maps the new color
// range to the original colors.
// *Luminosity will be an array just large enough to hold the brightness
// levels of all the used colors, in consecutive order. It is sorted from
// darkest to lightest and scaled such that the darkest color is 0.0 and
// the brightest color is 1.0.
// The return value is the number of used colors and thus the number of
// entries in *luminosity.
//
//==========================================================================
int FFont::SimpleTranslation (uint32_t *colorsused, uint8_t *translation, uint8_t *reverse, TArray<double> &Luminosity)
{
double min, max, diver;
int i, j;
memset (translation, 0, 256);
reverse[0] = 0;
for (i = 1, j = 1; i < 256; i++)
{
if (colorsused[i])
{
reverse[j++] = i;
}
}
qsort (reverse+1, j-1, 1, compare);
Luminosity.Resize(j);
Luminosity[0] = 0.0; // [BL] Prevent uninitalized memory
max = 0.0;
min = 100000000.0;
for (i = 1; i < j; i++)
{
translation[reverse[i]] = i;
Luminosity[i] = RPART(GPalette.BaseColors[reverse[i]]) * 0.299 +
GPART(GPalette.BaseColors[reverse[i]]) * 0.587 +
BPART(GPalette.BaseColors[reverse[i]]) * 0.114;
if (Luminosity[i] > max)
max = Luminosity[i];
if (Luminosity[i] < min)
min = Luminosity[i];
}
diver = 1.0 / (max - min);
for (i = 1; i < j; i++)
{
Luminosity[i] = (Luminosity[i] - min) * diver;
}
return j;
}
//==========================================================================
//
// FFont :: BuildTranslations
//
// Build color translations for this font. Luminosity is an array of
// brightness levels. The ActiveColors member must be set to indicate how
// large this array is. Identity is an array that remaps the colors to
// their original values; it is only used for CR_UNTRANSLATED. Ranges
// is an array of TranslationParm structs defining the ranges for every
// possible color, in order. Palette is the colors to use for the
// untranslated version of the font.
//
//==========================================================================
void FFont::BuildTranslations (const double *luminosity, const uint8_t *identity,
const void *ranges, int total_colors, const PalEntry *palette, std::function<void(FRemapTable*)> post)
{
int i, j;
const TranslationParm *parmstart = (const TranslationParm *)ranges;
FRemapTable remap(total_colors);
remap.ForFont = true;
// Create different translations for different color ranges
Translations.Clear();
for (i = 0; i < NumTextColors; i++)
{
if (i == CR_UNTRANSLATED)
{
if (identity != nullptr)
{
memcpy(remap.Remap, identity, ActiveColors);
if (palette != nullptr)
{
memcpy(remap.Palette, palette, ActiveColors * sizeof(PalEntry));
}
else
{
remap.Palette[0] = GPalette.BaseColors[identity[0]] & MAKEARGB(0, 255, 255, 255);
for (j = 1; j < ActiveColors; ++j)
{
remap.Palette[j] = GPalette.BaseColors[identity[j]] | MAKEARGB(255, 0, 0, 0);
}
}
Translations.Push(GPalette.StoreTranslation(TRANSLATION_Internal, &remap));
}
else
{
Translations.Push(Translations[0]);
}
continue;
}
assert(parmstart->RangeStart >= 0);
remap.Remap[0] = 0;
remap.Palette[0] = 0;
remap.ForFont = true;
for (j = 1; j < ActiveColors; j++)
{
int v = int(luminosity[j] * 256.0);
// Find the color range that this luminosity value lies within.
const TranslationParm *parms = parmstart - 1;
do
{
parms++;
if (parms->RangeStart <= v && parms->RangeEnd >= v)
break;
}
while (parms[1].RangeStart > parms[0].RangeEnd);
// Linearly interpolate to find out which color this luminosity level gets.
int rangev = ((v - parms->RangeStart) << 8) / (parms->RangeEnd - parms->RangeStart);
int r = ((parms->Start[0] << 8) + rangev * (parms->End[0] - parms->Start[0])) >> 8; // red
int g = ((parms->Start[1] << 8) + rangev * (parms->End[1] - parms->Start[1])) >> 8; // green
int b = ((parms->Start[2] << 8) + rangev * (parms->End[2] - parms->Start[2])) >> 8; // blue
r = clamp(r, 0, 255);
g = clamp(g, 0, 255);
b = clamp(b, 0, 255);
remap.Remap[j] = ColorMatcher.Pick(r, g, b);
remap.Palette[j] = PalEntry(255,r,g,b);
}
if (post) post(&remap);
Translations.Push(GPalette.StoreTranslation(TRANSLATION_Internal, &remap));
// Advance to the next color range.
while (parmstart[1].RangeStart > parmstart[0].RangeEnd)
{
parmstart++;
}
parmstart++;
}
}
//==========================================================================
//
// FFont :: GetColorTranslation
//
//==========================================================================
int FFont::GetColorTranslation (EColorRange range, PalEntry *color) const
{
if (noTranslate)
{
PalEntry retcolor = PalEntry(255, 255, 255, 255);
if (range >= 0 && range < NumTextColors && range != CR_UNTRANSLATED)
{
retcolor = TranslationColors[range];
retcolor.a = 255;
}
if (color != nullptr) *color = retcolor;
}
if (ActiveColors == 0 || range == CR_UNDEFINED)
return -1;
else if (range >= NumTextColors)
range = CR_UNTRANSLATED;
//if (range == CR_UNTRANSLATED && !translateUntranslated) return nullptr;
return Translations[range];
}
//==========================================================================
//
// FFont :: GetCharCode
//
// If the character code is in the font, returns it. If it is not, but it
// is lowercase and has an uppercase variant present, return that. Otherwise
// return -1.
//
//==========================================================================
int FFont::GetCharCode(int code, bool needpic) const
{
if (code < 0 && code >= -128)
{
// regular chars turn negative when the 8th bit is set.
code &= 255;
}
if (code >= FirstChar && code <= LastChar && (!needpic || Chars[code - FirstChar].TranslatedPic != nullptr))
{
return code;
}
// Use different substitution logic based on the fonts content:
// In a font which has both upper and lower case, prefer unaccented small characters over capital ones.
// In a pure upper-case font, do not check for lower case replacements.
if (!MixedCase)
{
// Try converting lowercase characters to uppercase.
if (myislower(code))
{
code = upperforlower[code];
if (code >= FirstChar && code <= LastChar && (!needpic || Chars[code - FirstChar].TranslatedPic != nullptr))
{
return code;
}
}
// Try stripping accents from accented characters.
int newcode = stripaccent(code);
if (newcode != code)
{
code = newcode;
if (code >= FirstChar && code <= LastChar && (!needpic || Chars[code - FirstChar].TranslatedPic != nullptr))
{
return code;
}
}
}
else
{
int originalcode = code;
int newcode;
// Try stripping accents from accented characters. This may repeat to allow multi-step fallbacks.
while ((newcode = stripaccent(code)) != code)
{
code = newcode;
if (code >= FirstChar && code <= LastChar && (!needpic || Chars[code - FirstChar].TranslatedPic != nullptr))
{
return code;
}
}
code = originalcode;
if (myislower(code))
{
int upper = upperforlower[code];
// Stripping accents did not help - now try uppercase for lowercase
if (upper != code) return GetCharCode(upper, needpic);
}
// Same for the uppercase character. Since we restart at the accented version this must go through the entire thing again.
while ((newcode = stripaccent(code)) != code)
{
code = newcode;
if (code >= FirstChar && code <= LastChar && (!needpic || Chars[code - FirstChar].TranslatedPic != nullptr))
{
return code;
}
}
}
return -1;
}
//==========================================================================
//
// FFont :: GetChar
//
//==========================================================================
FGameTexture *FFont::GetChar (int code, int translation, int *const width, bool *redirected) const
{
code = GetCharCode(code, true);
int xmove = SpaceWidth;
if (code >= 0)
{
code -= FirstChar;
xmove = Chars[code].XMove;
}
if (width != nullptr)
{
*width = xmove;
}
if (code < 0) return nullptr;
if ((translation == CR_UNTRANSLATED || translation == CR_UNDEFINED) && !forceremap)
{
bool redirect = Chars[code].OriginalPic && Chars[code].OriginalPic != Chars[code].TranslatedPic;
if (redirected) *redirected = redirect;
if (redirect)
{
assert(Chars[code].OriginalPic->GetUseType() == ETextureType::FontChar);
return Chars[code].OriginalPic;
}
}
if (redirected) *redirected = false;
assert(Chars[code].TranslatedPic->GetUseType() == ETextureType::FontChar);
return Chars[code].TranslatedPic;
}
//==========================================================================
//
// FFont :: GetCharWidth
//
//==========================================================================
int FFont::GetCharWidth (int code) const
{
code = GetCharCode(code, true);
if (code >= 0) return Chars[code - FirstChar].XMove;
return SpaceWidth;
}
//==========================================================================
//
//
//
//==========================================================================
double GetBottomAlignOffset(FFont *font, int c)
{
int w;
auto tex_zero = font->GetChar('0', CR_UNDEFINED, &w);
auto texc = font->GetChar(c, CR_UNDEFINED, &w);
double offset = 0;
if (texc) offset += texc->GetDisplayTopOffset();
if (tex_zero) offset += -tex_zero->GetDisplayTopOffset() + tex_zero->GetDisplayHeight();
return offset;
}
//==========================================================================
//
// Checks if the font contains proper glyphs for all characters in the string
//
//==========================================================================
bool FFont::CanPrint(const uint8_t *string) const
{
if (!string) return true;
while (*string)
{
auto chr = GetCharFromString(string);
if (!MixedCase) chr = upperforlower[chr]; // For uppercase-only fonts we shouldn't check lowercase characters.
if (chr == TEXTCOLOR_ESCAPE)
{
// We do not need to check for UTF-8 in here.
if (*string == '[')
{
while (*string != '\0' && *string != ']')
{
++string;
}
}
if (*string != '\0')
{
++string;
}
continue;
}
else if (chr != '\n')
{
int cc = GetCharCode(chr, true);
if (chr != cc && myiswalpha(chr) && cc != getAlternative(chr))
{
return false;
}
}
}
return true;
}
//==========================================================================
//
// Find string width using this font
//
//==========================================================================
int FFont::StringWidth(const uint8_t *string, int spacing) const
{
int w = 0;
int maxw = 0;
while (*string)
{
auto chr = GetCharFromString(string);
if (chr == TEXTCOLOR_ESCAPE)
{
// We do not need to check for UTF-8 in here.
if (*string == '[')
{
while (*string != '\0' && *string != ']')
{
++string;
}
}
if (*string != '\0')
{
++string;
}
continue;
}
else if (chr == '\n')
{
if (w > maxw)
maxw = w;
w = 0;
}
else if (spacing >= 0)
{
w += GetCharWidth(chr) + GlobalKerning + spacing;
}
else
{
w -= spacing;
}
}
return std::max(maxw, w);
}
//==========================================================================
//
// Get the largest ascender in the first line of this text.
//
//==========================================================================
int FFont::GetMaxAscender(const uint8_t* string) const
{
int retval = 0;
while (*string)
{
auto chr = GetCharFromString(string);
if (chr == TEXTCOLOR_ESCAPE)
{
// We do not need to check for UTF-8 in here.
if (*string == '[')
{
while (*string != '\0' && *string != ']')
{
++string;
}
}
if (*string != '\0')
{
++string;
}
continue;
}
else if (chr == '\n')
{
break;
}
else
{
auto ctex = GetChar(chr, CR_UNTRANSLATED, nullptr);
if (ctex)
{
auto offs = int(ctex->GetDisplayTopOffset());
if (offs > retval) retval = offs;
}
}
}
return retval;
}
//==========================================================================
//
// FFont :: LoadTranslations
//
//==========================================================================
void FFont::LoadTranslations()
{
unsigned int count = LastChar - FirstChar + 1;
uint32_t usedcolors[256] = {};
uint8_t identity[256];
TArray<double> Luminosity;
for (unsigned int i = 0; i < count; i++)
{
if (Chars[i].TranslatedPic)
{
FFontChar1 *pic = static_cast<FFontChar1 *>(Chars[i].TranslatedPic->GetTexture()->GetImage());
if (pic)
{
pic->SetSourceRemap(nullptr); // Force the FFontChar1 to return the same pixels as the base texture
RecordTextureColors(pic, usedcolors);
}
}
}
ActiveColors = SimpleTranslation (usedcolors, PatchRemap, identity, Luminosity);
for (unsigned int i = 0; i < count; i++)
{
if(Chars[i].TranslatedPic)
static_cast<FFontChar1 *>(Chars[i].TranslatedPic->GetTexture()->GetImage())->SetSourceRemap(PatchRemap);
}
BuildTranslations (Luminosity.Data(), identity, &TranslationParms[TranslationType][0], ActiveColors, nullptr);
}
//==========================================================================
//
// FFont :: FFont - default constructor
//
//==========================================================================
FFont::FFont (int lump)
{
Lump = lump;
FontName = NAME_None;
Cursor = '_';
noTranslate = false;
uint8_t pp = 0;
for (auto &p : PatchRemap) p = pp++;
}
//==========================================================================
//
// FFont :: FixXMoves
//
// If a font has gaps in its characters, set the missing characters'
// XMoves to either SpaceWidth or the unaccented or uppercase variant's
// XMove. Missing XMoves must be initialized with INT_MIN beforehand.
//
//==========================================================================
void FFont::FixXMoves()
{
if (FirstChar < 'a' && LastChar >= 'z')
{
MixedCase = true;
// First check if this is a mixed case font.
// For this the basic Latin small characters all need to be present.
for (int i = 'a'; i <= 'z'; i++)
if (Chars[i - FirstChar].OriginalPic == nullptr)
{
MixedCase = false;
break;
}
}
for (int i = 0; i <= LastChar - FirstChar; ++i)
{
if (Chars[i].XMove == INT_MIN)
{
// Try an uppercase character.
if (myislower(i + FirstChar))
{
int upper = upperforlower[FirstChar + i];
if (upper >= FirstChar && upper <= LastChar )
{
Chars[i].XMove = Chars[upper - FirstChar].XMove;
continue;
}
}
// Try an unnaccented character.
int noaccent = stripaccent(i + FirstChar);
if (noaccent != i + FirstChar)
{
noaccent -= FirstChar;
if (noaccent >= 0)
{
Chars[i].XMove = Chars[noaccent].XMove;
continue;
}
}
Chars[i].XMove = SpaceWidth;
}
if (Chars[i].OriginalPic)
{
int ofs = (int)Chars[i].OriginalPic->GetDisplayTopOffset();
if (ofs > Displacement) Displacement = ofs;
}
}
}