diff options
Diffstat (limited to 'src/runtimerender/Qt3DSQtTextRenderer.cpp')
-rw-r--r-- | src/runtimerender/Qt3DSQtTextRenderer.cpp | 655 |
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); +} |