302 lines
9.8 KiB
C++
302 lines
9.8 KiB
C++
#include "TextRasterizer.h"
|
|
|
|
#include <windows.h>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <gdiplus.h>
|
|
#include <memory>
|
|
#include <sstream>
|
|
|
|
namespace
|
|
{
|
|
constexpr int kTextSdfSpread = 20;
|
|
constexpr float kTextFontPixelSize = 144.0f;
|
|
constexpr float kTextLayoutPadding = 48.0f;
|
|
constexpr float kSdfInfinity = 1.0e20f;
|
|
|
|
class GdiplusSession
|
|
{
|
|
public:
|
|
GdiplusSession()
|
|
{
|
|
Gdiplus::GdiplusStartupInput startupInput;
|
|
mStarted = Gdiplus::GdiplusStartup(&mToken, &startupInput, NULL) == Gdiplus::Ok;
|
|
}
|
|
|
|
~GdiplusSession()
|
|
{
|
|
if (mStarted)
|
|
Gdiplus::GdiplusShutdown(mToken);
|
|
}
|
|
|
|
GdiplusSession(const GdiplusSession&) = delete;
|
|
GdiplusSession& operator=(const GdiplusSession&) = delete;
|
|
|
|
bool started() const { return mStarted; }
|
|
|
|
private:
|
|
ULONG_PTR mToken = 0;
|
|
bool mStarted = false;
|
|
};
|
|
|
|
std::wstring Utf8ToWide(const std::string& text)
|
|
{
|
|
if (text.empty())
|
|
return std::wstring();
|
|
const int required = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, NULL, 0);
|
|
if (required <= 1)
|
|
return std::wstring();
|
|
std::wstring wide(static_cast<std::size_t>(required - 1), L'\0');
|
|
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, wide.data(), required);
|
|
return wide;
|
|
}
|
|
|
|
void DistanceTransform1D(const std::vector<float>& input, std::vector<float>& output, unsigned count)
|
|
{
|
|
std::vector<unsigned> locations(count, 0);
|
|
std::vector<float> boundaries(static_cast<std::size_t>(count) + 1, 0.0f);
|
|
|
|
unsigned segment = 0;
|
|
locations[0] = 0;
|
|
boundaries[0] = -kSdfInfinity;
|
|
boundaries[1] = kSdfInfinity;
|
|
|
|
for (unsigned q = 1; q < count; ++q)
|
|
{
|
|
float intersection = 0.0f;
|
|
for (;;)
|
|
{
|
|
const unsigned location = locations[segment];
|
|
intersection =
|
|
((input[q] + static_cast<float>(q * q)) - (input[location] + static_cast<float>(location * location))) /
|
|
(2.0f * static_cast<float>(q) - 2.0f * static_cast<float>(location));
|
|
if (intersection > boundaries[segment] || segment == 0)
|
|
break;
|
|
--segment;
|
|
}
|
|
|
|
++segment;
|
|
locations[segment] = q;
|
|
boundaries[segment] = intersection;
|
|
boundaries[segment + 1] = kSdfInfinity;
|
|
}
|
|
|
|
segment = 0;
|
|
for (unsigned q = 0; q < count; ++q)
|
|
{
|
|
while (boundaries[segment + 1] < static_cast<float>(q))
|
|
++segment;
|
|
const unsigned location = locations[segment];
|
|
const float delta = static_cast<float>(q) - static_cast<float>(location);
|
|
output[q] = delta * delta + input[location];
|
|
}
|
|
}
|
|
|
|
std::vector<float> DistanceTransform2D(const std::vector<unsigned char>& targetMask, unsigned width, unsigned height)
|
|
{
|
|
std::vector<float> rowInput(width, 0.0f);
|
|
std::vector<float> rowOutput(width, 0.0f);
|
|
std::vector<float> columnInput(height, 0.0f);
|
|
std::vector<float> columnOutput(height, 0.0f);
|
|
std::vector<float> rowDistance(static_cast<std::size_t>(width) * height, 0.0f);
|
|
std::vector<float> distance(static_cast<std::size_t>(width) * height, 0.0f);
|
|
|
|
for (unsigned y = 0; y < height; ++y)
|
|
{
|
|
for (unsigned x = 0; x < width; ++x)
|
|
rowInput[x] = targetMask[static_cast<std::size_t>(y) * width + x] ? 0.0f : kSdfInfinity;
|
|
DistanceTransform1D(rowInput, rowOutput, width);
|
|
for (unsigned x = 0; x < width; ++x)
|
|
rowDistance[static_cast<std::size_t>(y) * width + x] = rowOutput[x];
|
|
}
|
|
|
|
for (unsigned x = 0; x < width; ++x)
|
|
{
|
|
for (unsigned y = 0; y < height; ++y)
|
|
columnInput[y] = rowDistance[static_cast<std::size_t>(y) * width + x];
|
|
DistanceTransform1D(columnInput, columnOutput, height);
|
|
for (unsigned y = 0; y < height; ++y)
|
|
distance[static_cast<std::size_t>(y) * width + x] = columnOutput[y];
|
|
}
|
|
|
|
return distance;
|
|
}
|
|
|
|
std::vector<unsigned char> BuildTextSdfTexture(const std::vector<unsigned char>& alpha, unsigned width, unsigned height)
|
|
{
|
|
std::vector<unsigned char> insideMask(static_cast<std::size_t>(width) * height, 0);
|
|
std::vector<unsigned char> outsideMask(static_cast<std::size_t>(width) * height, 0);
|
|
for (std::size_t index = 0; index < alpha.size(); ++index)
|
|
{
|
|
const bool inside = alpha[index] > 127;
|
|
insideMask[index] = inside ? 1 : 0;
|
|
outsideMask[index] = inside ? 0 : 1;
|
|
}
|
|
|
|
const std::vector<float> distanceToInside = DistanceTransform2D(insideMask, width, height);
|
|
const std::vector<float> distanceToOutside = DistanceTransform2D(outsideMask, width, height);
|
|
std::vector<unsigned char> sdf(static_cast<std::size_t>(width) * height * 4, 0);
|
|
|
|
for (unsigned y = 0; y < height; ++y)
|
|
{
|
|
const unsigned flippedY = height - 1 - y;
|
|
for (unsigned x = 0; x < width; ++x)
|
|
{
|
|
const std::size_t source = static_cast<std::size_t>(y) * width + x;
|
|
const float signedDistance = std::sqrt(distanceToOutside[source]) - std::sqrt(distanceToInside[source]);
|
|
const float normalized = std::clamp(
|
|
0.5f + signedDistance / static_cast<float>(kTextSdfSpread * 2),
|
|
0.0f,
|
|
1.0f);
|
|
const unsigned char value = static_cast<unsigned char>(normalized * 255.0f + 0.5f);
|
|
const std::size_t out = (static_cast<std::size_t>(flippedY) * width + x) * 4;
|
|
sdf[out + 0] = value;
|
|
sdf[out + 1] = value;
|
|
sdf[out + 2] = value;
|
|
sdf[out + 3] = value;
|
|
}
|
|
}
|
|
|
|
return sdf;
|
|
}
|
|
|
|
void WriteTextMaskDebugDump(const std::string& text, const std::vector<unsigned char>& alpha, const std::vector<unsigned char>& sdf, unsigned width, unsigned height)
|
|
{
|
|
try
|
|
{
|
|
std::filesystem::path debugDir = std::filesystem::current_path() / "runtime";
|
|
std::filesystem::create_directories(debugDir);
|
|
|
|
auto writePgm = [width, height](const std::filesystem::path& path, const std::vector<unsigned char>& gray, std::size_t stride)
|
|
{
|
|
std::ofstream out(path, std::ios::binary);
|
|
if (!out)
|
|
return;
|
|
out << "P5\n" << width << " " << height << "\n255\n";
|
|
for (unsigned y = 0; y < height; ++y)
|
|
{
|
|
for (unsigned x = 0; x < width; ++x)
|
|
out.put(static_cast<char>(gray[(static_cast<std::size_t>(y) * width + x) * stride]));
|
|
}
|
|
};
|
|
|
|
writePgm(debugDir / "text-mask-alpha-debug.pgm", alpha, 1);
|
|
writePgm(debugDir / "text-mask-sdf-debug.pgm", sdf, 4);
|
|
|
|
unsigned alphaMin = 255;
|
|
unsigned alphaMax = 0;
|
|
unsigned sdfMin = 255;
|
|
unsigned sdfMax = 0;
|
|
std::size_t alphaLit = 0;
|
|
std::size_t sdfLit = 0;
|
|
for (unsigned char value : alpha)
|
|
{
|
|
alphaMin = std::min<unsigned>(alphaMin, value);
|
|
alphaMax = std::max<unsigned>(alphaMax, value);
|
|
if (value > 0)
|
|
++alphaLit;
|
|
}
|
|
for (std::size_t index = 0; index < sdf.size(); index += 4)
|
|
{
|
|
const unsigned char value = sdf[index];
|
|
sdfMin = std::min<unsigned>(sdfMin, value);
|
|
sdfMax = std::max<unsigned>(sdfMax, value);
|
|
if (value > 127)
|
|
++sdfLit;
|
|
}
|
|
|
|
std::ostringstream message;
|
|
message << "Text mask debug for '" << text << "': alpha min/max/lit=" << alphaMin << "/" << alphaMax << "/" << alphaLit
|
|
<< ", sdf min/max/gt127=" << sdfMin << "/" << sdfMax << "/" << sdfLit << "\n";
|
|
OutputDebugStringA(message.str().c_str());
|
|
}
|
|
catch (...)
|
|
{
|
|
OutputDebugStringA("Failed to write text mask debug dump.\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
bool RasterizeTextSdf(const std::string& text, const std::filesystem::path& fontPath, std::vector<unsigned char>& sdf, std::string& error)
|
|
{
|
|
GdiplusSession gdiplus;
|
|
if (!gdiplus.started())
|
|
{
|
|
error = "Could not start GDI+ for text rendering.";
|
|
return false;
|
|
}
|
|
|
|
Gdiplus::PrivateFontCollection fontCollection;
|
|
Gdiplus::FontFamily fallbackFamily(L"Arial");
|
|
Gdiplus::FontFamily* fontFamily = &fallbackFamily;
|
|
std::unique_ptr<Gdiplus::FontFamily[]> families;
|
|
const std::wstring wideFontPath = fontPath.empty() ? std::wstring() : fontPath.wstring();
|
|
if (!wideFontPath.empty())
|
|
{
|
|
if (fontCollection.AddFontFile(wideFontPath.c_str()) != Gdiplus::Ok)
|
|
{
|
|
error = "Could not load packaged font file for text rendering: " + fontPath.string();
|
|
return false;
|
|
}
|
|
|
|
const INT familyCount = fontCollection.GetFamilyCount();
|
|
if (familyCount <= 0)
|
|
{
|
|
error = "Packaged font did not contain a usable font family: " + fontPath.string();
|
|
return false;
|
|
}
|
|
|
|
families.reset(new Gdiplus::FontFamily[familyCount]);
|
|
INT found = 0;
|
|
if (fontCollection.GetFamilies(familyCount, families.get(), &found) != Gdiplus::Ok || found <= 0)
|
|
{
|
|
error = "Could not read the packaged font family: " + fontPath.string();
|
|
return false;
|
|
}
|
|
fontFamily = &families[0];
|
|
}
|
|
|
|
Gdiplus::Bitmap bitmap(kTextTextureWidth, kTextTextureHeight, PixelFormat32bppARGB);
|
|
Gdiplus::Graphics graphics(&bitmap);
|
|
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
|
|
graphics.Clear(Gdiplus::Color(255, 0, 0, 0));
|
|
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceOver);
|
|
graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAlias);
|
|
graphics.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality);
|
|
Gdiplus::Font font(fontFamily, kTextFontPixelSize, Gdiplus::FontStyleRegular, Gdiplus::UnitPixel);
|
|
Gdiplus::SolidBrush brush(Gdiplus::Color(255, 255, 255, 255));
|
|
Gdiplus::StringFormat format;
|
|
format.SetAlignment(Gdiplus::StringAlignmentNear);
|
|
format.SetLineAlignment(Gdiplus::StringAlignmentCenter);
|
|
format.SetFormatFlags(Gdiplus::StringFormatFlagsNoWrap | Gdiplus::StringFormatFlagsMeasureTrailingSpaces);
|
|
const Gdiplus::RectF layout(
|
|
kTextLayoutPadding,
|
|
0.0f,
|
|
static_cast<Gdiplus::REAL>(kTextTextureWidth) - (kTextLayoutPadding * 2.0f),
|
|
static_cast<Gdiplus::REAL>(kTextTextureHeight));
|
|
const std::wstring wideText = Utf8ToWide(text);
|
|
graphics.DrawString(wideText.c_str(), -1, &font, layout, &format, &brush);
|
|
|
|
std::vector<unsigned char> alpha(static_cast<std::size_t>(kTextTextureWidth) * kTextTextureHeight, 0);
|
|
for (unsigned y = 0; y < kTextTextureHeight; ++y)
|
|
{
|
|
for (unsigned x = 0; x < kTextTextureWidth; ++x)
|
|
{
|
|
Gdiplus::Color pixel;
|
|
bitmap.GetPixel(x, y, &pixel);
|
|
BYTE luminance = pixel.GetRed();
|
|
if (pixel.GetGreen() > luminance)
|
|
luminance = pixel.GetGreen();
|
|
if (pixel.GetBlue() > luminance)
|
|
luminance = pixel.GetBlue();
|
|
alpha[static_cast<std::size_t>(y) * kTextTextureWidth + x] = static_cast<unsigned char>(luminance);
|
|
}
|
|
}
|
|
sdf = BuildTextSdfTexture(alpha, kTextTextureWidth, kTextTextureHeight);
|
|
WriteTextMaskDebugDump(text, alpha, sdf, kTextTextureWidth, kTextTextureHeight);
|
|
return true;
|
|
}
|