summaryrefslogtreecommitdiffstats
path: root/src/runtimerender/Qt3DSQtTextRenderer.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/runtimerender/Qt3DSQtTextRenderer.cpp')
-rw-r--r--src/runtimerender/Qt3DSQtTextRenderer.cpp655
1 files changed, 655 insertions, 0 deletions
diff --git a/src/runtimerender/Qt3DSQtTextRenderer.cpp b/src/runtimerender/Qt3DSQtTextRenderer.cpp
new file mode 100644
index 0000000..029ccea
--- /dev/null
+++ b/src/runtimerender/Qt3DSQtTextRenderer.cpp
@@ -0,0 +1,655 @@
+/****************************************************************************
+**
+** Copyright (C) 2008-2012 NVIDIA Corporation.
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt 3D Studio.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "Qt3DSTextRenderer.h"
+#include "Qt3DSRenderText.h"
+#include "foundation/Qt3DSFoundation.h"
+#include "foundation/StringTable.h"
+#include "foundation/Qt3DSBroadcastingAllocator.h"
+
+#include "foundation/Qt3DSVec2.h"
+#include "foundation/FileTools.h"
+#include "render/Qt3DSRenderContext.h"
+#include "foundation/Qt3DSContainers.h"
+#include "Qt3DSRenderThreadPool.h"
+#include "foundation/Qt3DSSync.h"
+#include "foundation/Qt3DSPerfTimer.h"
+#include "EASTL/set.h"
+#include "EASTL/list.h"
+
+#include <QPainter>
+#include <QImage>
+#include <QFontDatabase>
+#include <QDir>
+#include <QDebug>
+#include <QHash>
+#include <QGuiApplication>
+#include <QtMath>
+#include <QRawFont>
+
+using namespace qt3ds::render;
+
+namespace {
+
+struct Qt3DSQtTextRenderer : public ITextRenderer
+{
+ struct FontInfo
+ {
+ QString fontFileName;
+ QString fontName;
+ QString fontFamily;
+ int fontId;
+ QFont font;
+
+ FontInfo() :
+ fontId(-1)
+ {}
+
+ FontInfo(const QString &fileName, const QString &name, const QString &family, int id) :
+ fontFileName(fileName)
+ , fontName(name)
+ , fontFamily(family)
+ , fontId(id)
+ {
+ font.setFamily(fontFamily);
+ }
+
+ FontInfo(const FontInfo &other) :
+ fontFileName(other.fontFileName)
+ , fontName(other.fontName)
+ , fontFamily(other.fontFamily)
+ , fontId(other.fontId)
+ , font(other.font)
+ {}
+
+ FontInfo &operator=(const FontInfo &other)
+ {
+ fontFileName = other.fontFileName;
+ fontName = other.fontName;
+ fontFamily = other.fontFamily;
+ fontId = other.fontId;
+ font = other.font;
+
+ return *this;
+ }
+ };
+
+ typedef eastl::string TStrType;
+ typedef eastl::set<TStrType> TStringSet;
+ typedef QHash<QString, FontInfo> TFontInfoHash;
+
+ NVFoundationBase &m_foundation;
+ NVScopedRefCounted<IStringTable> m_stringTable;
+ NVScopedRefCounted<NVRenderContext> m_renderContext;
+ NVScopedRefCounted<IPerfTimer> m_perfTimer;
+ volatile QT3DSI32 mRefCount;
+ nvvector<SRendererFontEntry> m_installedFonts;
+
+ Sync m_PreloadSync;
+
+ TStringSet m_systemFontDirs;
+ TStringSet m_projectFontDirs;
+ TFontInfoHash m_projectFontInfos;
+ TFontInfoHash m_systemFontInfos;
+ TStrType m_workspace;
+
+ bool m_systemFontsInitialized;
+ bool m_projectFontsInitialized;
+ bool m_PreloadingFonts;
+
+ QStringList m_nameFilters;
+ qreal m_pixelRatio;
+
+ Qt3DSQtTextRenderer(NVFoundationBase &inFoundation, IStringTable &inStrTable)
+ : m_foundation(inFoundation)
+ , m_stringTable(inStrTable)
+ , mRefCount(0)
+ , m_installedFonts(inFoundation.getAllocator(), "Qt3DSQtTextRenderer::m_installedFonts")
+ , m_PreloadSync(inFoundation.getAllocator())
+ , m_systemFontsInitialized(false)
+ , m_projectFontsInitialized(false)
+ , m_PreloadingFonts(false)
+ , m_pixelRatio(1.0)
+ {
+ const QWindowList list = QGuiApplication::topLevelWindows();
+ if (list.size() > 0)
+ m_pixelRatio = list[0]->devicePixelRatio();
+
+ m_nameFilters << QStringLiteral("*.ttf");
+ m_nameFilters << QStringLiteral("*.otf");
+ }
+ virtual ~Qt3DSQtTextRenderer()
+ {
+ QFontDatabase::removeAllApplicationFonts();
+ }
+
+ QString stringToQString(const CRegisteredString &str)
+ {
+ return QString::fromUtf8(str.c_str());
+ }
+
+ QString stringToQString(const eastl::string &str)
+ {
+ return QString::fromUtf8(str.c_str());
+ }
+
+ QString stringToQString(const char8_t *str)
+ {
+ return QString::fromUtf8(str);
+ }
+
+ CRegisteredString QStringToRegisteredString(const QString &str)
+ {
+ return m_stringTable->RegisterStr(str.toUtf8().constData());
+ }
+
+ void unregisterProjectFonts()
+ {
+ for (FontInfo &fi : m_projectFontInfos.values())
+ QFontDatabase::removeApplicationFont(fi.fontId);
+ m_projectFontsInitialized = false;
+ m_installedFonts.clear();
+ m_projectFontInfos.clear();
+ }
+
+ QString getFileStem(const QString &fileName)
+ {
+ QString retVal;
+ int dotPos = fileName.lastIndexOf(QChar('.'));
+ if (dotPos < 0)
+ return retVal;
+ int slashPos = fileName.lastIndexOf(QChar('/'));
+ retVal = fileName.mid(slashPos + 1);
+ retVal.chop(fileName.length() - dotPos);
+ return retVal;
+ }
+
+ void registerFonts(TStringSet dirSet, TFontInfoHash *fontInfos = nullptr)
+ {
+ for (TStringSet::const_iterator theIter = dirSet.begin(),
+ theEnd = dirSet.end();
+ theIter != theEnd; ++theIter) {
+ QString localDir = CFileTools::NormalizePathForQtUsage(stringToQString(*theIter));
+ QDir dir(localDir);
+ if (!dir.exists()) {
+ qWarning("Attempted to register invalid font directory: %s",
+ qPrintable(localDir));
+ continue;
+ }
+ QStringList entryList = dir.entryList(m_nameFilters);
+ for (QString entry : entryList) {
+ entry = dir.absoluteFilePath(entry);
+ QFile file(entry);
+ if (file.open(QIODevice::ReadOnly)) {
+ QByteArray rawData = file.readAll();
+ int fontId = QFontDatabase::addApplicationFontFromData(rawData);
+ if (fontId < 0) {
+ qCWarning(WARNING, "Failed to register font: %s",
+ entry.toStdString().c_str());
+ } else if (fontInfos) {
+ QString fontName = getFileStem(entry);
+ QString fontFamily;
+ QStringList families = QFontDatabase::applicationFontFamilies(fontId);
+ if (families.size() > 0)
+ fontFamily = families.at(0);
+ FontInfo fi(entry, fontName, fontFamily, fontId);
+ // Detect font style and weight using a dummy QRawFont
+ QRawFont rawFont(rawData, 16);
+ if (rawFont.isValid()) {
+ if (rawFont.style() != QFont::StyleOblique) {
+ fi.font.setStyle(rawFont.style());
+ fi.font.setWeight(rawFont.weight());
+ }
+ } else {
+ qCWarning(WARNING, "Failed to determine font style: %s",
+ entry.toStdString().c_str());
+ }
+ fontInfos->insert(fontName, fi);
+ }
+ } else {
+ qCWarning(WARNING, "Failed to load font: %s",
+ entry.toStdString().c_str());
+ }
+ }
+ }
+ }
+
+ void projectCleanup()
+ {
+ m_projectFontsInitialized = false;
+ unregisterProjectFonts();
+ m_projectFontDirs.clear();
+ }
+
+ QT3DS_IMPLEMENT_REF_COUNT_ADDREF_RELEASE_OVERRIDE(m_foundation.getAllocator())
+
+ eastl::pair<TStrType, bool> AddFontDirectory(const TStrType &inDirectory, TStringSet &inDirSet)
+ {
+ if (inDirectory.empty()) {
+ m_workspace.assign("./");
+ } else {
+ m_workspace.clear();
+ for (const char8_t *item = inDirectory.c_str(); item && *item; ++item) {
+ if (*item == '\\')
+ m_workspace.append(1, '/');
+ else
+ m_workspace.append(1, static_cast<char8_t>(*item));
+ }
+ if (m_workspace.back() != '/')
+ m_workspace.append(1, '/');
+ }
+
+ return eastl::make_pair(m_workspace, inDirSet.insert(m_workspace).second);
+ }
+
+ // You can have several standard font directories and these will be persistent
+ void AddSystemFontDirectory(const char8_t *inDirectory) override
+ {
+ AddFontDirectory(inDirectory, m_systemFontDirs);
+ }
+
+ void AddProjectFontDirectory(const char8_t *inProjectDirectory) override
+ {
+ eastl::pair<TStrType, bool> theAddResult =
+ AddFontDirectory(inProjectDirectory, m_projectFontDirs);
+ if (theAddResult.second && m_projectFontsInitialized)
+ ReloadFonts();
+ }
+
+ void ReloadFonts() override
+ {
+ unregisterProjectFonts();
+ PreloadFonts();
+ }
+
+ void PreloadFonts() override
+ {
+ if (!m_systemFontsInitialized) {
+ m_systemFontsInitialized = true;
+ registerFonts(m_systemFontDirs, &m_systemFontInfos);
+ }
+
+ if (!m_projectFontsInitialized) {
+ m_projectFontsInitialized = true;
+ registerFonts(m_projectFontDirs, &m_projectFontInfos);
+ }
+ }
+
+ void ClearProjectFontDirectories() override
+ {
+ projectCleanup();
+ }
+
+ static void PreloadThreadCallback(void *inData)
+ {
+ Qt3DSQtTextRenderer *theRenderer(reinterpret_cast<Qt3DSQtTextRenderer *>(inData));
+ theRenderer->PreloadFonts();
+ theRenderer->m_PreloadSync.set();
+ }
+
+ void BeginPreloadFonts(IThreadPool &inThreadPool, IPerfTimer &inTimer) override
+ {
+ m_PreloadingFonts = true;
+
+ m_PreloadSync.reset();
+ m_perfTimer = inTimer;
+
+ inThreadPool.AddTask(this, PreloadThreadCallback, NULL);
+ }
+
+ void EndPreloadFonts() override
+ {
+ if (m_PreloadingFonts) {
+ {
+ SStackPerfTimer __perfTimer(*m_perfTimer, "QtText: Wait till font preloading completed");
+ m_PreloadSync.wait();
+ }
+ }
+ m_PreloadingFonts = false;
+ }
+
+ // Get the list of project fonts. These are the only fonts that can be displayed.
+ NVConstDataRef<SRendererFontEntry> GetProjectFontList() override
+ {
+ PreloadFonts();
+ if (m_installedFonts.empty()) {
+ m_installedFonts.reserve(m_projectFontInfos.size());
+ for (FontInfo &fi : m_projectFontInfos.values()) {
+ m_installedFonts.push_back(SRendererFontEntry(
+ fi.fontName,
+ fi.fontFileName));
+ }
+
+ }
+ return m_installedFonts;
+ }
+
+ Option<CRegisteredString> GetFontNameForFont(CRegisteredString inFontname) override
+ {
+ // This function is there to support legacy font names.
+
+ QString inStr = stringToQString(inFontname);
+ if (m_projectFontInfos.keys().contains(inStr))
+ return inFontname;
+
+ // Fall back for family name detection if not found by font name
+ for (FontInfo &fi : m_projectFontInfos.values()) {
+ if (inStr == fi.fontFamily)
+ return QStringToRegisteredString(fi.fontName);
+ }
+
+ return Empty();
+ }
+
+ Option<CRegisteredString> GetFontNameForFont(const char8_t *inFontname) override
+ {
+ return GetFontNameForFont(m_stringTable->RegisterStr(inFontname));
+ }
+
+ ITextRenderer &GetTextRenderer(NVRenderContext &inRenderContext) override
+ {
+ m_renderContext = inRenderContext;
+ return *this;
+ }
+
+ FontInfo &fontInfoForName(const CRegisteredString &fontName)
+ {
+ PreloadFonts();
+ QString qtFontName = stringToQString(fontName);
+ if (m_projectFontInfos.contains(qtFontName))
+ return m_projectFontInfos[qtFontName];
+
+ if (m_systemFontInfos.contains(qtFontName))
+ return m_systemFontInfos[qtFontName];
+
+ // Unknown font, create a system font for it
+ FontInfo fi("", qtFontName, qtFontName, -1);
+ m_systemFontInfos.insert(qtFontName, fi);
+
+ return m_systemFontInfos[qtFontName];
+ }
+
+ void updateFontInfo(FontInfo &fi, const STextRenderInfo &inText,
+ QT3DSF32 inTextScaleFactor = 1.0f)
+ {
+ qreal pixelSize = inText.m_FontSize;
+ fi.font.setPixelSize(pixelSize * inTextScaleFactor);
+ fi.font.setLetterSpacing(QFont::AbsoluteSpacing, qreal(inText.m_Tracking));
+ }
+
+ QStringList splitText(const char8_t *theText)
+ {
+ // Split the text into lines
+ int lines = 1;
+ int lineLen = 0;
+ QStringList lineList;
+ const char8_t *lineStartItem = nullptr;
+ for (const char8_t *item = theText; item && *item; ++item) {
+ if (!lineLen)
+ lineStartItem = item;
+ ++lineLen;
+ if (*item == '\n') {
+ int chopAmount = 1;
+ if (lineLen > 1 && *(item - 1) == '\r')
+ ++chopAmount;
+
+ ++lines;
+ lineList.append(QString::fromUtf8(lineStartItem, lineLen - chopAmount));
+ lineLen = 0;
+ }
+ }
+ if (lineStartItem)
+ lineList.append(QString::fromUtf8(lineStartItem, lineLen));
+
+ return lineList;
+ }
+
+ QRectF textBoundingBox(const STextRenderInfo &inText,
+ const QFontMetricsF &fm, QStringList &lineList,
+ QVector<qreal> &lineWidths, const char8_t *inTextOverride = nullptr)
+ {
+ const char8_t *theText = inTextOverride ? inTextOverride : inText.m_Text.c_str();
+ lineList = splitText(theText);
+
+ QRectF boundingBox;
+ boundingBox.setHeight(lineList.size() * fm.height() + qCeil(qreal(lineList.size() - 1) * qreal(inText.m_Leading)));
+
+ lineWidths.resize(lineList.size());
+
+ for (int i = 0; i < lineList.size(); ++i) {
+ // For italicized fonts the bounding box right is the correct method
+ // to measure since the left offset may extend, but for
+ // non-italicized fonts we need the width method to meausure
+ // otherwise the resultant text will be clipped.
+ QString line = lineList.at(i);
+ qreal width = fm.width(line);
+ qreal right = fm.boundingRect(line).right();
+ // For hdpi displays, fontmetrics doesn't always calculate enough space for fonts, so
+ // we add the pixel ratio to all widths to avoid clipping
+ qreal lineWidth = qMax(width, right) + m_pixelRatio;
+ lineWidths[i] = lineWidth;
+ if (boundingBox.width() < lineWidth)
+ boundingBox.setWidth(lineWidth);
+ }
+
+ // We don't want extra letter spacing on the last glyph, so let's remove it
+ boundingBox.setRight(qMax(boundingBox.left(), boundingBox.right() - qFloor(inText.m_Tracking)));
+
+ return boundingBox;
+ }
+
+ STextDimensions MeasureText(const STextRenderInfo &inText, QT3DSF32 inTextScaleFactor,
+ const char8_t *inTextOverride) override
+ {
+ FontInfo &fi = fontInfoForName(inText.m_Font);
+ updateFontInfo(fi, inText, inTextScaleFactor);
+ QFontMetricsF fm(fi.font);
+ QStringList dummyList;
+ QVector<qreal> dummyWidth;
+ QRectF boundingBox = textBoundingBox(inText, fm, dummyList, dummyWidth, inTextOverride);
+ return STextDimensions(boundingBox.width(), boundingBox.height());
+ }
+
+ int alignToQtAlign(TextVerticalAlignment::Enum va)
+ {
+ int qtAlign(0);
+ switch (va) {
+ case TextVerticalAlignment::Top:
+ qtAlign = Qt::AlignTop;
+ break;
+ case TextVerticalAlignment::Bottom:
+ qtAlign = Qt::AlignBottom;
+ break;
+ default:
+ qtAlign = Qt::AlignVCenter;
+ }
+
+ return qtAlign;
+ }
+
+ STextTextureDetails RenderText(const STextRenderInfo &inSrcText,
+ NVRenderTexture2D &inTexture) override
+ {
+ FontInfo &fi = fontInfoForName(inSrcText.m_Font);
+ updateFontInfo(fi, inSrcText);
+ QFontMetricsF fm(fi.font);
+ int horizontalAlignmentFlag = Qt::AlignLeft;
+
+ int shadowRgb = int(2.55f * (100 - int(inSrcText.m_DropShadowStrength)));
+ QStringList lineList;
+ QVector<qreal> lineWidths;
+ QRectF boundingBox;
+ const bool dynamicTextArea = inSrcText.m_BoundingBox.isZero();
+
+ if (dynamicTextArea) {
+ boundingBox = textBoundingBox(inSrcText, fm, lineList, lineWidths);
+ } else {
+ lineList << inSrcText.m_Text.c_str();
+ lineWidths << inSrcText.m_BoundingBox.x;
+ boundingBox = QRectF(0, 0, inSrcText.m_BoundingBox.x, inSrcText.m_BoundingBox.y);
+ }
+
+ if (boundingBox.width() <= 0 || boundingBox.height() <= 0) {
+ return ITextRenderer::UploadData(toU8DataRef((char *)nullptr, 0), inTexture, 4, 4,
+ 0, 0,
+ NVRenderTextureFormats::RGBA8, true);
+ }
+
+ int finalWidth = NextMultipleOf4(boundingBox.width());
+ int finalHeight = NextMultipleOf4(boundingBox.height());
+
+ QImage image(finalWidth, finalHeight, QImage::Format_ARGB32);
+ image.fill(0);
+ QPainter painter(&image);
+ painter.setPen(Qt::white);
+ painter.setFont(fi.font);
+
+ // Translate painter to remove the extra spacing of the last letter
+ qreal tracking = 0.0;
+ switch (inSrcText.m_HorizontalAlignment) {
+ case TextHorizontalAlignment::Center:
+ horizontalAlignmentFlag = Qt::AlignHCenter;
+ tracking += qreal(inSrcText.m_Tracking / 2.0f);
+ break;
+ case TextHorizontalAlignment::Right:
+ horizontalAlignmentFlag = Qt::AlignRight;
+ tracking += qreal(inSrcText.m_Tracking);
+ break;
+ default:
+ break; // Do nothing
+ }
+
+ int wordWrapFlags = 0;
+ if (dynamicTextArea) {
+ wordWrapFlags = Qt::TextDontClip;
+ } else {
+ switch (inSrcText.m_WordWrap) {
+ case TextWordWrap::WrapWord:
+ wordWrapFlags = Qt::TextWordWrap | Qt::TextDontClip;
+ break;
+ case TextWordWrap::WrapAnywhere:
+ wordWrapFlags = Qt::TextWrapAnywhere | Qt::TextDontClip;
+ break;
+ case TextWordWrap::Clip:
+ default:
+ break;
+ }
+ }
+
+ int lineHeight = dynamicTextArea ? fm.height() : finalHeight;
+ QT3DSF32 nextHeight = 0;
+ for (int i = 0; i < lineList.size(); ++i) {
+ const QString &line = lineList.at(i);
+ qreal xTranslation = tracking;
+ switch (inSrcText.m_HorizontalAlignment) {
+ case TextHorizontalAlignment::Center:
+ xTranslation += qreal(boundingBox.width() - lineWidths.at(i)) / 2.0;
+ break;
+ case TextHorizontalAlignment::Right:
+ xTranslation += qreal(boundingBox.width() - lineWidths.at(i));
+ break;
+ default:
+ break; // Do nothing
+ }
+ QRectF bound(xTranslation, qreal(nextHeight), lineWidths.at(i), lineHeight);
+ QRectF actualBound;
+ if (inSrcText.m_DropShadow) {
+ qreal shadowOffsetX = qreal(inSrcText.m_FontSize * inSrcText.m_DropShadowOffsetX)
+ / 1000.;
+ qreal shadowOffsetY = qreal(inSrcText.m_FontSize * inSrcText.m_DropShadowOffsetY)
+ / 1000.;
+ QRectF boundShadow(xTranslation + shadowOffsetX, nextHeight + shadowOffsetY,
+ qreal(lineWidths.at(i)), lineHeight);
+ // shadow is a darker shade of the given font color
+ painter.setPen(QColor(shadowRgb, shadowRgb, shadowRgb));
+ painter.drawText(boundShadow,
+ alignToQtAlign(inSrcText.m_VerticalAlignment) | wordWrapFlags
+ | horizontalAlignmentFlag, line, &actualBound);
+ painter.setPen(Qt::white); // coloring is done in the shader
+ }
+ painter.drawText(bound,
+ alignToQtAlign(inSrcText.m_VerticalAlignment) | wordWrapFlags
+ | horizontalAlignmentFlag, line, &actualBound);
+
+ nextHeight += QT3DSF32(lineHeight) + inSrcText.m_Leading;
+ }
+
+ return ITextRenderer::UploadData(toU8DataRef(image.bits(), image.byteCount()), inTexture,
+ image.width(), image.height(),
+ image.width(), image.height(),
+ NVRenderTextureFormats::RGBA8, true);
+ }
+
+ STextTextureDetails RenderText(const STextRenderInfo &inText,
+ NVRenderPathFontItem &inPathFontItem,
+ NVRenderPathFontSpecification &inFontPathSpec) override
+ {
+ Q_UNUSED(inText);
+ Q_UNUSED(inPathFontItem);
+ Q_UNUSED(inFontPathSpec);
+ QT3DS_ASSERT(m_renderContext->IsPathRenderingSupported());
+
+ // We do not support HW accelerated fonts (yet?)
+ QT3DS_ASSERT(false);
+
+ return STextTextureDetails();
+ }
+
+ void BeginFrame() override
+ {
+ // Nothing to do
+ }
+
+ void EndFrame() override
+ {
+ // Nothing to do
+ }
+
+ // unused for text rendering via texture atlas
+ STextTextureAtlasEntryDetails RenderAtlasEntry(QT3DSU32, NVRenderTexture2D &) override
+ {
+ return STextTextureAtlasEntryDetails();
+ }
+ QT3DSI32 CreateTextureAtlas() override
+ {
+ return 0;
+ }
+ SRenderTextureAtlasDetails RenderText(const STextRenderInfo &) override
+ {
+ return SRenderTextureAtlasDetails();
+ }
+};
+}
+
+ITextRendererCore &ITextRendererCore::CreateQtTextRenderer(NVFoundationBase &inFnd,
+ IStringTable &inStrTable)
+{
+ return *QT3DS_NEW(inFnd.getAllocator(), Qt3DSQtTextRenderer)(inFnd, inStrTable);
+}