From ef22f2054804c3037ab34a71acc7c6e5938cf22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 17 May 2019 13:00:54 +0200 Subject: [PATCH] Fix non-integer Font Metrics in Graph (#1545) --- src/common/CachedFontMetrics.h | 188 +++++++------ src/common/Metrics.h | 26 ++ src/common/RichTextPainter.cpp | 384 +++++++++++++------------- src/common/RichTextPainter.h | 100 +++---- src/widgets/DisassemblerGraphView.cpp | 23 +- src/widgets/DisassemblerGraphView.h | 2 +- 6 files changed, 390 insertions(+), 333 deletions(-) create mode 100644 src/common/Metrics.h diff --git a/src/common/CachedFontMetrics.h b/src/common/CachedFontMetrics.h index 79e59b99..f6b56f15 100644 --- a/src/common/CachedFontMetrics.h +++ b/src/common/CachedFontMetrics.h @@ -1,84 +1,104 @@ -#ifndef CACHEDFONTMETRICS_H -#define CACHEDFONTMETRICS_H - -#include -#include -#include - -class CachedFontMetrics : public QObject -{ - Q_OBJECT -public: - explicit CachedFontMetrics(QObject *parent, const QFont &font) - : QObject(parent), - mFontMetrics(font) - { - memset(mWidths, 0, sizeof(mWidths)); - mHeight = mFontMetrics.height(); - } - - int width(const QChar &ch) - { - auto unicode = ch.unicode(); - if (unicode >= 0xD800) { - if (unicode >= 0xE000) - unicode -= 0xE000 - 0xD800; - else - // is lonely surrogate - return mFontMetrics.width(ch); - } - if (!mWidths[unicode]) - return mWidths[unicode] = mFontMetrics.width(ch); - return mWidths[unicode]; - } - - int width(const QString &text) - { - int result = 0; - QChar temp; - for (const QChar &ch : text) { - if (ch.isHighSurrogate()) - temp = ch; - else if (ch.isLowSurrogate()) - result += mFontMetrics.width(QString(temp) + ch); - else - result += width(ch); - } - return result; - } - - int height() - { - return mHeight; - } - - int position(const QString &text, int offset) - { - int curWidth = 0; - QChar temp; - - for (int i = 0; i < text.length(); i++) { - QChar ch = text[i]; - - if (ch.isHighSurrogate()) - temp = ch; - else if (ch.isLowSurrogate()) - curWidth += mFontMetrics.width(QString(temp) + ch); - else - curWidth += width(ch); - - if (curWidth >= offset) { - return i; - } - } - - return -1; - } - -private: - QFontMetrics mFontMetrics; - uchar mWidths[0x10000 - 0xE000 + 0xD800]; - int mHeight; -}; - -#endif // CACHEDFONTMETRICS_H +#ifndef CACHEDFONTMETRICS_H +#define CACHEDFONTMETRICS_H + +#include "common/Metrics.h" + +#include +#include +#include + +template +class CachedFontMetrics +{ +public: + explicit CachedFontMetrics(const QFont &font) + : mFontMetrics(font) + { + memset(mWidths, 0, sizeof(mWidths)); + mHeight = mFontMetrics.height(); + } + + T width(const QChar &ch) + { + //return mFontMetrics.width(ch); + auto unicode = ch.unicode(); + if (unicode >= 0xD800) { + if (unicode >= 0xE000) + unicode -= 0xE000 - 0xD800; + else + // is lonely surrogate + return fetchWidth(ch); + } + if (!mWidths[unicode]) + return mWidths[unicode] = fetchWidth(ch); + return mWidths[unicode]; + } + + T width(const QString &text) + { + T result = 0; + QChar temp; + for (const QChar &ch : text) { + if (ch.isHighSurrogate()) + temp = ch; + else if (ch.isLowSurrogate()) + result += fetchWidth(QString(temp) + ch); + else + result += width(ch); + } + return result; + } + + T height() + { + return mHeight; + } + + T position(const QString &text, T offset) + { + T curWidth = 0; + QChar temp; + + for (int i = 0; i < text.length(); i++) { + QChar ch = text[i]; + + if (ch.isHighSurrogate()) + temp = ch; + else if (ch.isLowSurrogate()) + curWidth += fetchWidth(QString(temp) + ch); + else + curWidth += width(ch); + + if (curWidth >= offset) { + return i; + } + } + + return -1; + } + +private: + typename Metrics::FontMetrics mFontMetrics; + T mWidths[0x10000 - 0xE000 + 0xD800]; + T mHeight; + + T fetchWidth(QChar c) + { +#if QT_VERSION < QT_VERSION_CHECK(5,11,0) + return mFontMetrics.width(c); +#else + return mFontMetrics.horizontalAdvance(c); +#endif + } + + T fetchWidth(const QString &s) + { +#if QT_VERSION < QT_VERSION_CHECK(5,11,0) + return mFontMetrics.width(s); +#else + return mFontMetrics.horizontalAdvance(s); +#endif + } +}; + +#endif // CACHEDFONTMETRICS_H diff --git a/src/common/Metrics.h b/src/common/Metrics.h new file mode 100644 index 00000000..e7abc827 --- /dev/null +++ b/src/common/Metrics.h @@ -0,0 +1,26 @@ + +#ifndef METRICS_H +#define METRICS_H + +#include + +class QRect; +class QRectF; +class QFontMetrics; +class QFontMetricsF; + +template struct Metrics {}; + +template<> struct Metrics +{ + using Rect = QRect; + using FontMetrics = QFontMetrics; +}; + +template<> struct Metrics +{ + using Rect = QRectF; + using FontMetrics = QFontMetricsF; +}; + +#endif //METRICS_H diff --git a/src/common/RichTextPainter.cpp b/src/common/RichTextPainter.cpp index 58605664..f41b81be 100644 --- a/src/common/RichTextPainter.cpp +++ b/src/common/RichTextPainter.cpp @@ -1,188 +1,196 @@ -/* x64dbg RichTextPainter */ -#include "RichTextPainter.h" -#include "CachedFontMetrics.h" -#include "common/Configuration.h" -#include -#include -#include - -//TODO: fix performance (possibly use QTextLayout?) -void RichTextPainter::paintRichText(QPainter *painter, int x, int y, int w, int h, int xinc, - const List &richText, CachedFontMetrics *fontMetrics) -{ - QPen pen; - QPen highlightPen; - QBrush brush(Qt::cyan); - for (const CustomRichText_t &curRichText : richText) { - int textWidth = fontMetrics->width(curRichText.text); - int backgroundWidth = textWidth; - if (backgroundWidth + xinc > w) - backgroundWidth = w - xinc; - if (backgroundWidth <= 0) //stop drawing when going outside the specified width - break; - switch (curRichText.flags) { - case FlagNone: //defaults - pen.setColor(ConfigColor("btext").name()); - painter->setPen(pen); - break; - case FlagColor: //color only - pen.setColor(curRichText.textColor); - painter->setPen(pen); - break; - case FlagBackground: //background only - if (backgroundWidth > 0 && curRichText.textBackground.alpha()) { - brush.setColor(curRichText.textBackground); - painter->fillRect(QRect(x + xinc, y, backgroundWidth, h), brush); - } - break; - case FlagAll: //color+background - if (backgroundWidth > 0 && curRichText.textBackground.alpha()) { - brush.setColor(curRichText.textBackground); - painter->fillRect(QRect(x + xinc, y, backgroundWidth, h), brush); - } - pen.setColor(curRichText.textColor); - painter->setPen(pen); - break; - } - painter->drawText(QRect(x + xinc, y, w - xinc, h), Qt::TextBypassShaping, curRichText.text); - if (curRichText.highlight && curRichText.highlightColor.alpha()) { - highlightPen.setColor(curRichText.highlightColor); - highlightPen.setWidth(curRichText.highlightWidth); - painter->setPen(highlightPen); - int highlightOffsetX = curRichText.highlightConnectPrev ? -1 : 1; - painter->drawLine(x + xinc + highlightOffsetX, y + h - 1, x + xinc + backgroundWidth - 1, - y + h - 1); - } - xinc += textWidth; - } -} - -/** - * @brief RichTextPainter::htmlRichText Convert rich text in x64dbg to HTML, for use by other applications - * @param richText The rich text to be converted to HTML format - * @param textHtml The HTML source. Any previous content will be preserved and new content will be appended at the end. - * @param textPlain The plain text. Any previous content will be preserved and new content will be appended at the end. - */ -void RichTextPainter::htmlRichText(const List &richText, QString &textHtml, QString &textPlain) -{ - for (const CustomRichText_t &curRichText : richText) { - if (curRichText.text == " ") { //blank - textHtml += " "; - textPlain += " "; - continue; - } - switch (curRichText.flags) { - case FlagNone: //defaults - textHtml += ""; - break; - case FlagColor: //color only - textHtml += QString("").arg(curRichText.textColor.name()); - break; - case FlagBackground: //background only - if (curRichText.textBackground != - Qt::transparent) // QColor::name() returns "#000000" for transparent color. That's not desired. Leave it blank. - textHtml += QString("").arg(curRichText.textBackground.name()); - else - textHtml += QString(""); - break; - case FlagAll: //color+background - if (curRichText.textBackground != - Qt::transparent) // QColor::name() returns "#000000" for transparent color. That's not desired. Leave it blank. - textHtml += QString("").arg( - curRichText.textColor.name(), curRichText.textBackground.name()); - else - textHtml += QString("").arg(curRichText.textColor.name()); - break; - } - if (curRichText.highlight) //Underline highlighted token - textHtml += ""; - textHtml += curRichText.text.toHtmlEscaped(); - if (curRichText.highlight) - textHtml += ""; - textHtml += ""; //Close the tag - textPlain += curRichText.text; - } -} - -RichTextPainter::List RichTextPainter::fromTextDocument(const QTextDocument &doc) -{ - List r; - - for (QTextBlock block = doc.begin(); block != doc.end(); block = block.next()) { - for (QTextBlock::iterator it = block.begin(); it != block.end(); ++it) { - QTextFragment fragment = it.fragment(); - QTextCharFormat format = fragment.charFormat(); - - CustomRichText_t text; - text.text = fragment.text(); - text.textColor = format.foreground().color(); - text.textBackground = format.background().color(); - - bool hasForeground = format.hasProperty(QTextFormat::ForegroundBrush); - bool hasBackground = format.hasProperty(QTextFormat::BackgroundBrush); - - if (hasForeground && !hasBackground) { - text.flags = FlagColor; - } else if (!hasForeground && hasBackground) { - text.flags = FlagBackground; - } else if (hasForeground && hasBackground) { - text.flags = FlagAll; - } else { - text.flags = FlagNone; - } - - r.push_back(text); - } - } - - return r; -} - -RichTextPainter::List RichTextPainter::cropped(const RichTextPainter::List &richText, int maxCols, - const QString &indicator, bool *croppedOut) -{ - List r; - r.reserve(richText.size()); - - int cols = 0; - bool cropped = false; - for (const auto &text : richText) { - int textLength = text.text.size(); - if (cols + textLength <= maxCols) { - r.push_back(text); - cols += textLength; - } else if (cols == maxCols) { - break; - } else { - CustomRichText_t croppedText = text; - croppedText.text.truncate(maxCols - cols); - r.push_back(croppedText); - cropped = true; - break; - } - } - - if (cropped && !indicator.isEmpty()) { - int indicatorCropLength = indicator.length(); - if (indicatorCropLength > maxCols) { - indicatorCropLength = maxCols; - } - - while (!r.empty()) { - auto &text = r.back(); - - if (text.text.length() >= indicatorCropLength) { - text.text.replace(text.text.length() - indicatorCropLength, indicatorCropLength, indicator); - break; - } - - indicatorCropLength -= text.text.length(); - r.pop_back(); - } - } - - if (croppedOut) { - *croppedOut = cropped; - } - return r; -} +/* x64dbg RichTextPainter */ +#include "RichTextPainter.h" +#include "CachedFontMetrics.h" +#include "common/Configuration.h" +#include +#include +#include + +//TODO: fix performance (possibly use QTextLayout?) + + +template +void RichTextPainter::paintRichText(QPainter *painter, T x, T y, T w, T h, T xinc, + const List &richText, CachedFontMetrics *fontMetrics) +{ + QPen pen; + QPen highlightPen; + QBrush brush(Qt::cyan); + for (const CustomRichText_t &curRichText : richText) { + T textWidth = fontMetrics->width(curRichText.text); + T backgroundWidth = textWidth; + if (backgroundWidth + xinc > w) + backgroundWidth = w - xinc; + if (backgroundWidth <= 0) //stop drawing when going outside the specified width + break; + switch (curRichText.flags) { + case FlagNone: //defaults + pen.setColor(ConfigColor("btext").name()); + painter->setPen(pen); + break; + case FlagColor: //color only + pen.setColor(curRichText.textColor); + painter->setPen(pen); + break; + case FlagBackground: //background only + if (backgroundWidth > 0 && curRichText.textBackground.alpha()) { + brush.setColor(curRichText.textBackground); + painter->fillRect(QRectF(x + xinc, y, backgroundWidth, h), brush); + } + break; + case FlagAll: //color+background + if (backgroundWidth > 0 && curRichText.textBackground.alpha()) { + brush.setColor(curRichText.textBackground); + painter->fillRect(QRectF(x + xinc, y, backgroundWidth, h), brush); + } + pen.setColor(curRichText.textColor); + painter->setPen(pen); + break; + } + painter->drawText(typename Metrics::Rect(x + xinc, y, w - xinc, h), Qt::TextBypassShaping, curRichText.text); + if (curRichText.highlight && curRichText.highlightColor.alpha()) { + highlightPen.setColor(curRichText.highlightColor); + highlightPen.setWidth(curRichText.highlightWidth); + painter->setPen(highlightPen); + T highlightOffsetX = curRichText.highlightConnectPrev ? -1 : 1; + painter->drawLine(x + xinc + highlightOffsetX, y + h - 1, x + xinc + backgroundWidth - 1, + y + h - 1); + } + xinc += textWidth; + } +} + +template +void RichTextPainter::paintRichText(QPainter *painter, qreal x, qreal y, qreal w, qreal h, qreal xinc, + const List &richText, CachedFontMetrics *fontMetrics); + + +/** + * @brief RichTextPainter::htmlRichText Convert rich text in x64dbg to HTML, for use by other applications + * @param richText The rich text to be converted to HTML format + * @param textHtml The HTML source. Any previous content will be preserved and new content will be appended at the end. + * @param textPlain The plain text. Any previous content will be preserved and new content will be appended at the end. + */ +void RichTextPainter::htmlRichText(const List &richText, QString &textHtml, QString &textPlain) +{ + for (const CustomRichText_t &curRichText : richText) { + if (curRichText.text == " ") { //blank + textHtml += " "; + textPlain += " "; + continue; + } + switch (curRichText.flags) { + case FlagNone: //defaults + textHtml += ""; + break; + case FlagColor: //color only + textHtml += QString("").arg(curRichText.textColor.name()); + break; + case FlagBackground: //background only + if (curRichText.textBackground != + Qt::transparent) // QColor::name() returns "#000000" for transparent color. That's not desired. Leave it blank. + textHtml += QString("").arg(curRichText.textBackground.name()); + else + textHtml += QString(""); + break; + case FlagAll: //color+background + if (curRichText.textBackground != + Qt::transparent) // QColor::name() returns "#000000" for transparent color. That's not desired. Leave it blank. + textHtml += QString("").arg( + curRichText.textColor.name(), curRichText.textBackground.name()); + else + textHtml += QString("").arg(curRichText.textColor.name()); + break; + } + if (curRichText.highlight) //Underline highlighted token + textHtml += ""; + textHtml += curRichText.text.toHtmlEscaped(); + if (curRichText.highlight) + textHtml += ""; + textHtml += ""; //Close the tag + textPlain += curRichText.text; + } +} + +RichTextPainter::List RichTextPainter::fromTextDocument(const QTextDocument &doc) +{ + List r; + + for (QTextBlock block = doc.begin(); block != doc.end(); block = block.next()) { + for (QTextBlock::iterator it = block.begin(); it != block.end(); ++it) { + QTextFragment fragment = it.fragment(); + QTextCharFormat format = fragment.charFormat(); + + CustomRichText_t text; + text.text = fragment.text(); + text.textColor = format.foreground().color(); + text.textBackground = format.background().color(); + + bool hasForeground = format.hasProperty(QTextFormat::ForegroundBrush); + bool hasBackground = format.hasProperty(QTextFormat::BackgroundBrush); + + if (hasForeground && !hasBackground) { + text.flags = FlagColor; + } else if (!hasForeground && hasBackground) { + text.flags = FlagBackground; + } else if (hasForeground && hasBackground) { + text.flags = FlagAll; + } else { + text.flags = FlagNone; + } + + r.push_back(text); + } + } + + return r; +} + +RichTextPainter::List RichTextPainter::cropped(const RichTextPainter::List &richText, int maxCols, + const QString &indicator, bool *croppedOut) +{ + List r; + r.reserve(richText.size()); + + int cols = 0; + bool cropped = false; + for (const auto &text : richText) { + int textLength = text.text.size(); + if (cols + textLength <= maxCols) { + r.push_back(text); + cols += textLength; + } else if (cols == maxCols) { + break; + } else { + CustomRichText_t croppedText = text; + croppedText.text.truncate(maxCols - cols); + r.push_back(croppedText); + cropped = true; + break; + } + } + + if (cropped && !indicator.isEmpty()) { + int indicatorCropLength = indicator.length(); + if (indicatorCropLength > maxCols) { + indicatorCropLength = maxCols; + } + + while (!r.empty()) { + auto &text = r.back(); + + if (text.text.length() >= indicatorCropLength) { + text.text.replace(text.text.length() - indicatorCropLength, indicatorCropLength, indicator); + break; + } + + indicatorCropLength -= text.text.length(); + r.pop_back(); + } + } + + if (croppedOut) { + *croppedOut = cropped; + } + return r; +} diff --git a/src/common/RichTextPainter.h b/src/common/RichTextPainter.h index e0346e98..e50492db 100644 --- a/src/common/RichTextPainter.h +++ b/src/common/RichTextPainter.h @@ -1,48 +1,52 @@ -/* x64dbg RichTextPainter */ -#ifndef RICHTEXTPAINTER_H -#define RICHTEXTPAINTER_H - -#include -#include -#include -#include - -class CachedFontMetrics; -class QPainter; - -class RichTextPainter -{ -public: - //structures - enum CustomRichTextFlags { - FlagNone, - FlagColor, - FlagBackground, - FlagAll - }; - - struct CustomRichText_t { - QString text; - QColor textColor; - QColor textBackground; - CustomRichTextFlags flags; - bool highlight = false; - QColor highlightColor; - int highlightWidth = 2; - bool highlightConnectPrev = false; - }; - - typedef std::vector List; - - //functions - static void paintRichText(QPainter *painter, int x, int y, int w, int h, int xinc, - const List &richText, CachedFontMetrics *fontMetrics); - static void htmlRichText(const List &richText, QString &textHtml, QString &textPlain); - - static List fromTextDocument(const QTextDocument &doc); - - static List cropped(const List &richText, int maxCols, const QString &indicator = nullptr, - bool *croppedOut = nullptr); -}; - -#endif // RICHTEXTPAINTER_H +/* x64dbg RichTextPainter */ +#ifndef RICHTEXTPAINTER_H +#define RICHTEXTPAINTER_H + +#include "common/Metrics.h" + +#include +#include +#include +#include + +class QFontMetricsF; +template class CachedFontMetrics; +class QPainter; + +class RichTextPainter +{ +public: + //structures + enum CustomRichTextFlags { + FlagNone, + FlagColor, + FlagBackground, + FlagAll + }; + + struct CustomRichText_t { + QString text; + QColor textColor; + QColor textBackground; + CustomRichTextFlags flags; + bool highlight = false; + QColor highlightColor; + int highlightWidth = 2; + bool highlightConnectPrev = false; + }; + + typedef std::vector List; + + //functions + template + static void paintRichText(QPainter *painter, T x, T y, T w, T h, T xinc, + const List &richText, CachedFontMetrics *fontMetrics); + static void htmlRichText(const List &richText, QString &textHtml, QString &textPlain); + + static List fromTextDocument(const QTextDocument &doc); + + static List cropped(const List &richText, int maxCols, const QString &indicator = nullptr, + bool *croppedOut = nullptr); +}; + +#endif // RICHTEXTPAINTER_H diff --git a/src/widgets/DisassemblerGraphView.cpp b/src/widgets/DisassemblerGraphView.cpp index e1f5eac8..c8898e06 100644 --- a/src/widgets/DisassemblerGraphView.cpp +++ b/src/widgets/DisassemblerGraphView.cpp @@ -377,8 +377,7 @@ void DisassemblerGraphView::initFont() charWidth = metrics.width('X'); charHeight = static_cast(metrics.height()); charOffset = 0; - delete mFontMetrics; - mFontMetrics = new CachedFontMetrics(this, font()); + mFontMetrics.reset(new CachedFontMetrics(font())); } void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block) @@ -464,7 +463,7 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block) // Highlight selected tokens if (highlight_token != nullptr) { int y = static_cast(blockY + (2 * charWidth) + (db.header_text.lines.size() * charHeight)); - int tokenWidth = mFontMetrics->width(highlight_token->content); + qreal tokenWidth = mFontMetrics->width(highlight_token->content); for (const Instr &instr : db.instrs) { int pos = -1; @@ -477,19 +476,19 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block) continue; } - int widthBefore = mFontMetrics->width(instr.plainText.left(pos)); + qreal widthBefore = mFontMetrics->width(instr.plainText.left(pos)); if (charWidth * 3 + widthBefore > block.width - (10 + 2 * charWidth)) { continue; } - int highlightWidth = tokenWidth; + qreal highlightWidth = tokenWidth; if (charWidth * 3 + widthBefore + tokenWidth >= block.width - (10 + 2 * charWidth)) { - highlightWidth = static_cast(block.width - widthBefore - (10 + 4 * charWidth)); + highlightWidth = block.width - widthBefore - (10 + 4 * charWidth); } QColor selectionColor = ConfigColor("wordhl"); - p.fillRect(QRect(static_cast(blockX + charWidth * 3 + widthBefore), y, highlightWidth, + p.fillRect(QRectF(blockX + charWidth * 3 + widthBefore, y, highlightWidth, charHeight), selectionColor); } @@ -535,8 +534,8 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block) continue; } - RichTextPainter::paintRichText(&p, static_cast(x), y, block.width, charHeight, 0, line, - mFontMetrics); + RichTextPainter::paintRichText(&p, x, y, block.width, charHeight, 0, line, + mFontMetrics.get()); y += charHeight; } @@ -569,9 +568,9 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block) QRectF bpRect(x - rectSize / 3.0, y + (charHeight - rectSize) / 2.0, rectSize, rectSize); Q_UNUSED(bpRect); - RichTextPainter::paintRichText(&p, static_cast(x + charWidth), y, - static_cast(block.width - charWidth), charHeight, 0, line, - mFontMetrics); + RichTextPainter::paintRichText(&p, x + charWidth, y, + block.width - charWidth, charHeight, 0, line, + mFontMetrics.get()); y += charHeight; } diff --git a/src/widgets/DisassemblerGraphView.h b/src/widgets/DisassemblerGraphView.h index 4cc6343e..8291e948 100644 --- a/src/widgets/DisassemblerGraphView.h +++ b/src/widgets/DisassemblerGraphView.h @@ -141,7 +141,7 @@ private: Token *highlight_token; // Font data - CachedFontMetrics *mFontMetrics; + std::unique_ptr> mFontMetrics; qreal charWidth; int charHeight; int charOffset;