Cockatrice/cockatrice/src/interface/pixel_map_generator.cpp
BruebachL 1ef07309d6
Turn Card, Deck_List, Protocol, RNG, Network (Client, Server), Settings and Utility into libraries and remove cockatrice_common. (#6212)
---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: ebbit1q <ebbit1q@gmail.com>
2025-10-09 07:36:12 +02:00

391 lines
13 KiB
C++

#include "pixel_map_generator.h"
#include <QApplication>
#include <QDomDocument>
#include <QFile>
#include <QPainter>
#include <QPalette>
#include <QSvgRenderer>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#define DEFAULT_COLOR_UNREGISTERED "#32c8ec";
#define DEFAULT_COLOR_REGISTERED "#5ed900";
#define DEFAULT_COLOR_MODERATOR_LEFT "#ffffff";
#define DEFAULT_COLOR_MODERATOR_RIGHT "#000000";
#define DEFAULT_COLOR_ADMIN "#ff2701";
/**
* Loads in an svg from file and scales it without affecting image quality.
*
* @param svgPath The path to the svg file, with file extension.
* @param size The desired size of the pixmap.
* @param expandOnly If true, then keep the size of the initial pixmap to at least the svg size.
*
* @return The svg loaded into a Pixmap with the given size, or an empty Pixmap if the loading failed.
*/
static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnly = false)
{
QSvgRenderer svgRenderer(svgPath);
if (!svgRenderer.isValid()) {
qCWarning(PixelMapGeneratorLog) << "Failed to load" << svgPath;
return {};
}
// If expandOnly, make sure the pixmap is at least as large as the svg, so that we don't lose any detail.
// QIcon.pixmap(size) will automatically scale down the image, but it won't scale it up.
QSize pixmapSize = expandOnly ? svgRenderer.defaultSize().expandedTo(size) : size;
QPixmap pix(pixmapSize);
pix.fill(Qt::transparent);
QPainter pixPainter(&pix);
svgRenderer.render(&pixPainter);
// Converting the pixmap to a QIcon and back is the easiest way to scale down a svg without affecting image quality
if (expandOnly) {
return QIcon(pix).pixmap(size);
}
return pix;
}
/**
* Try to load path image from non-SVG formats, otherwise fall back to SVG.
* This is to allow custom themes to support non-SVG format type overrides, since SVG requires custom loading.
* @param path The path to the file, with no file extension. File formats will be automatically detected.
* @param size The desired size of the pixmap.
* @param expandOnly If true, then keep the size of the initial pixmap to at least the size (Only relevant if SVG).
*
* @return The loaded image into a Pixmap with the given size, or an empty Pixmap if the loading failed.
*/
static QPixmap tryLoadImage(const QString &path, const QSize &size, bool expandOnly = false)
{
const auto formats = {"png", "jpg"};
QPixmap returnPixmap;
for (const auto &format : formats) {
if (returnPixmap.load(path, format)) {
return returnPixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
}
return loadSvg(path + ".svg", size, expandOnly);
}
QMap<QString, QPixmap> PhasePixmapGenerator::pmCache;
QPixmap PhasePixmapGenerator::generatePixmap(int height, QString name)
{
QString key = name + QString::number(height);
if (pmCache.contains(key))
return pmCache.value(key);
QPixmap pixmap = tryLoadImage("theme:phases/" + name, QSize(height, height));
pmCache.insert(key, pixmap);
return pixmap;
}
QMap<QString, QPixmap> CounterPixmapGenerator::pmCache;
QPixmap CounterPixmapGenerator::generatePixmap(int height, QString name, bool highlight)
{
// The colorless counter is named "x" by the server but the file is named "general.svg"
if (name == "x") {
name = "general";
}
if (highlight)
name.append("_highlight");
QString key = name + QString::number(height);
if (pmCache.contains(key))
return pmCache.value(key);
QPixmap pixmap = tryLoadImage("theme:counters/" + name, QSize(height, height));
// fall back to colorless counter if the name can't be found
if (pixmap.isNull()) {
name = "general";
if (highlight)
name.append("_highlight");
pixmap = tryLoadImage("theme:counters/" + name, QSize(height, height));
}
pmCache.insert(key, pixmap);
return pixmap;
}
QPixmap PingPixmapGenerator::generatePixmap(int size, int value, int max)
{
int key = size * 1000000 + max * 1000 + value;
if (pmCache.contains(key))
return pmCache.value(key);
QPixmap pixmap(size, size);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
QColor color;
if ((max == -1) || (value == -1))
color = Qt::black;
else
color.setHsv(120 * (1.0 - ((double)value / max)), 255, 255);
QRadialGradient g(QPointF((double)pixmap.width() / 2, (double)pixmap.height() / 2),
qMin(pixmap.width(), pixmap.height()) / 2.0);
g.setColorAt(0, color);
g.setColorAt(1, Qt::transparent);
painter.fillRect(0, 0, pixmap.width(), pixmap.height(), QBrush(g));
pmCache.insert(key, pixmap);
return pixmap;
}
QMap<int, QPixmap> PingPixmapGenerator::pmCache;
QPixmap CountryPixmapGenerator::generatePixmap(int height, const QString &countryCode)
{
if (countryCode.size() != 2)
return QPixmap();
QString key = countryCode + QString::number(height);
if (pmCache.contains(key))
return pmCache.value(key);
int width = height * 2;
QPixmap pixmap = tryLoadImage("theme:countries/" + countryCode.toLower(), QSize(width, height), true);
QPainter painter(&pixmap);
painter.setPen(Qt::black);
// width/height offset was determined through trial-and-error
#ifdef Q_OS_MACOS
painter.drawRect(0, 0, pixmap.width() / 2, pixmap.height() / 2);
#else
painter.drawRect(0, 0, pixmap.width() - 1, pixmap.height() - 1);
#endif
pmCache.insert(key, pixmap);
return pixmap;
}
QMap<QString, QPixmap> CountryPixmapGenerator::pmCache;
/**
* Updates tags in the svg
*
* @param elem The svg
* @param tagName tag with attribute to update
* @param attrName attribute to be updated
* @param idName id that the tag has to match
* @param attrValue the value to update the attribute to
*/
static void setAttrRecur(QDomElement &elem,
const QString &tagName,
const QString &attrName,
const QString &idName,
const QString &attrValue);
void setAttrRecur(QDomElement &elem,
const QString &tagName,
const QString &attrName,
const QString &idName,
const QString &attrValue)
{
if (elem.tagName().compare(tagName) == 0) {
if (elem.attribute("id").compare(idName) == 0) {
elem.setAttribute(attrName, attrValue);
}
}
for (int i = 0; i < elem.childNodes().count(); i++) {
if (!elem.childNodes().at(i).isElement()) {
continue;
}
auto docElem = elem.childNodes().at(i).toElement();
setAttrRecur(docElem, tagName, attrName, idName, attrValue);
}
}
/**
* Loads the usericon svg and fills in its colors.
* The image is kept as a QIcon to preserve the image quality.
*
* Call icon.pixmap(w, h) in order to convert this icon into a pixmap with the given dimensions.
* Avoid scaling the pixmap in other ways, as that destroys image quality.
*
* @param minSize If the dimensions of the source svg is smaller than this, then it will be scaled up to this size
*/
static QIcon loadAndColorSvg(const QString &iconPath,
const QString &colorLeft,
const std::optional<QString> &colorRight,
const int minSize)
{
QFile file(iconPath);
if (!file.open(QIODevice::ReadOnly)) {
qCWarning(PixelMapGeneratorLog) << "Unable to open" << iconPath;
return {};
}
const auto &baData = file.readAll();
QDomDocument doc;
doc.setContent(baData);
auto docElem = doc.documentElement();
setAttrRecur(docElem, "path", "fill", "left", colorLeft);
if (colorRight.has_value()) {
setAttrRecur(docElem, "path", "fill", "right", colorRight.value());
}
QSvgRenderer svgRenderer(doc.toByteArray());
QPixmap pix(svgRenderer.defaultSize().expandedTo(QSize(minSize, minSize)));
pix.fill(Qt::transparent);
QPainter pixPainter(&pix);
svgRenderer.render(&pixPainter);
return QIcon(pix);
}
QPixmap UserLevelPixmapGenerator::generatePixmap(int height,
UserLevelFlags userLevel,
ServerInfo_User::PawnColorsOverride pawnColorsOverride,
bool isBuddy,
const QString &privLevel)
{
return generateIcon(height, userLevel, pawnColorsOverride, isBuddy, privLevel).pixmap(height, height);
}
QIcon UserLevelPixmapGenerator::generateIcon(int minHeight,
UserLevelFlags userLevel,
ServerInfo_User::PawnColorsOverride pawnColorsOverride,
bool isBuddy,
const QString &privLevel)
{
std::optional<QString> colorLeft = std::nullopt;
if (pawnColorsOverride.has_left_side()) {
colorLeft = QString::fromStdString(pawnColorsOverride.left_side());
}
std::optional<QString> colorRight = std::nullopt;
if (pawnColorsOverride.has_right_side()) {
colorRight = QString::fromStdString(pawnColorsOverride.right_side());
}
QString key = QString::number(minHeight * 10000) + ":" + static_cast<short>(userLevel) + ":" +
static_cast<short>(isBuddy) + ":" + privLevel.toLower() + ":" + colorLeft.value_or("") + ":" +
colorRight.value_or("");
if (iconCache.contains(key)) {
return iconCache.value(key);
}
QIcon icon = colorLeft.has_value()
? generateIconWithColorOverride(minHeight, isBuddy, userLevel, privLevel, colorLeft, colorRight)
: generateIconDefault(minHeight, userLevel, isBuddy, privLevel);
iconCache.insert(key, icon);
return icon;
}
static QString getIconType(const bool isBuddy, const UserLevelFlags &userLevelFlags, const QString &privLevel)
{
if (isBuddy) {
return "star";
}
if (userLevelFlags.testFlag(ServerInfo_User_UserLevelFlag_IsJudge)) {
return "pawn_judge";
}
if (!privLevel.isEmpty() && privLevel.toLower() != "none") {
return QString("pawn_%1").arg(privLevel.toLower());
}
return "pawn";
}
QIcon UserLevelPixmapGenerator::generateIconDefault(int height,
UserLevelFlags userLevel,
bool isBuddy,
const QString &privLevel)
{
const auto &iconType = getIconType(isBuddy, userLevel, privLevel);
QString arity = "single";
QString colorLeft;
std::optional<QString> colorRight = std::nullopt;
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
colorLeft = DEFAULT_COLOR_ADMIN;
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
colorLeft = DEFAULT_COLOR_MODERATOR_LEFT;
colorRight = DEFAULT_COLOR_MODERATOR_RIGHT;
arity = "double";
} else if (userLevel.testFlag(ServerInfo_User::IsRegistered)) {
colorLeft = DEFAULT_COLOR_REGISTERED;
} else {
colorLeft = DEFAULT_COLOR_UNREGISTERED;
}
const auto &iconPath = QString("theme:usericons/%1_%2.svg").arg(iconType, arity);
return loadAndColorSvg(iconPath, colorLeft, colorRight, height);
}
QIcon UserLevelPixmapGenerator::generateIconWithColorOverride(int height,
bool isBuddy,
const UserLevelFlags &userLevelFlags,
const QString &privLevel,
const std::optional<QString> &colorLeft,
const std::optional<QString> &colorRight)
{
const auto &iconType = getIconType(isBuddy, userLevelFlags, privLevel);
const QString &arity = colorRight.has_value() ? "double" : "single";
const auto &iconPath = QString("theme:usericons/%1_%2.svg").arg(iconType, arity);
return loadAndColorSvg(iconPath, colorLeft.value(), colorRight, height);
}
QMap<QString, QIcon> UserLevelPixmapGenerator::iconCache;
QPixmap LockPixmapGenerator::generatePixmap(int height)
{
int key = height;
if (pmCache.contains(key))
return pmCache.value(key);
QPixmap pixmap = tryLoadImage("theme:icons/lock", QSize(height, height), true);
pmCache.insert(key, pixmap);
return pixmap;
}
QMap<int, QPixmap> LockPixmapGenerator::pmCache;
QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded)
{
QString key = QString::number(expanded) + ":" + QString::number(height);
if (pmCache.contains(key))
return pmCache.value(key);
QString name = expanded ? "dropdown_expanded" : "dropdown_collapsed";
QPixmap pixmap = tryLoadImage("theme:icons/" + name, QSize(height, height), true);
pmCache.insert(key, pixmap);
return pixmap;
}
QMap<QString, QPixmap> DropdownIconPixmapGenerator::pmCache;
QPixmap loadColorAdjustedPixmap(const QString &name)
{
if (qApp->palette().windowText().color().lightness() > 200) {
QImage img(name);
img.invertPixels();
QPixmap result;
result.convertFromImage(img);
return result;
} else {
return QPixmap(name);
}
}