/* SPDX-FileCopyrightText: 2002 Craig Drummond SPDX-License-Identifier: LGPL-2.0-or-later */ #include "kxftconfig.h" #ifdef HAVE_FONTCONFIG #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace Qt::StringLiterals; static int point2Pixel(double point) { return (int)(((point * QX11Info::appDpiY()) / 72.0) + 0.5); } static int pixel2Point(double pixel) { return (int)(((pixel * 72.0) / (double)QX11Info::appDpiY()) + 0.5); } static bool equal(double d1, double d2) { return (fabs(d1 - d2) < 0.0001); } static QString dirSyntax(const QString &d) { if (d.isNull()) { return d; } QString ds(d); ds.replace(QLatin1String("//"), QLatin1String("/")); if (!ds.endsWith(QLatin1Char('/'))) { ds += QLatin1Char('/'); } return ds; } inline bool fExists(const QString &p) { return QFileInfo(p).isFile(); } inline bool dWritable(const QString &p) { QFileInfo info(p); return info.isDir() && info.isWritable(); } static QString getDir(const QString &path) { QString str(path); const int slashPos = str.lastIndexOf(QLatin1Char('/')); if (slashPos != -1) { str.truncate(slashPos + 1); } return dirSyntax(str); } static QDateTime getTimeStamp(const QString &item) { return QFileInfo(item).lastModified(); } static QString getEntry(QDomElement element, const char *type, unsigned int numAttributes, ...) { if (numAttributes == uint(element.attributes().length())) { va_list args; unsigned int arg; bool ok = true; va_start(args, numAttributes); for (arg = 0; arg < numAttributes && ok; ++arg) { const char *attr = va_arg(args, const char *); const char *val = va_arg(args, const char *); if (!attr || !val || QLatin1String(val) != element.attribute(QString::fromLocal8Bit(attr))) { ok = false; } } va_end(args); if (ok) { QDomNode n = element.firstChild(); if (!n.isNull()) { QDomElement e = n.toElement(); if (!e.isNull() && QLatin1String(type) == e.tagName()) { return e.text(); } } } } return QString(); } static KXftConfig::SubPixel::Type strToType(QStringView str) { if (str == u"rgb") { return KXftConfig::SubPixel::Rgb; } else if (str == u"bgr") { return KXftConfig::SubPixel::Bgr; } else if (str == u"vrgb") { return KXftConfig::SubPixel::Vrgb; } else if (str == u"vbgr") { return KXftConfig::SubPixel::Vbgr; } else if (str == u"none") { return KXftConfig::SubPixel::None; } else { return KXftConfig::SubPixel::NotSet; } } static KXftConfig::Hint::Style strToStyle(QStringView str) { if (str == u"hintslight") { return KXftConfig::Hint::Slight; } else if (str == u"hintmedium") { return KXftConfig::Hint::Medium; } else if (str == u"hintfull") { return KXftConfig::Hint::Full; } else { return KXftConfig::Hint::None; } } KXftConfig::KXftConfig(const QString &path) : m_doc(u"fontconfig"_s) , m_file(path.isEmpty() ? getConfigFile() : path) { qDebug() << "Using fontconfig file:" << m_file; reset(); } KXftConfig::~KXftConfig() { } // // Obtain location of config file to use. QString KXftConfig::getConfigFile() { FcStrList *list = FcConfigGetConfigFiles(FcConfigGetCurrent()); QStringList localFiles; FcChar8 *file; QString home(dirSyntax(QDir::homePath())); m_globalFiles.clear(); while ((file = FcStrListNext(list))) { QString f(QString::fromLocal8Bit((const char *)file)); if (fExists(f) && 0 == f.indexOf(home)) { localFiles.append(f); } else { m_globalFiles.append(f); } } FcStrListDone(list); // // Go through list of localFiles, looking for the preferred one... if (!localFiles.isEmpty()) { for (const QString &file : std::as_const(localFiles)) { if (file.endsWith(QLatin1String("/fonts.conf")) || file.endsWith(QLatin1String("/.fonts.conf"))) { return file; } } return localFiles.front(); // Just return the 1st one... } else { // Hmmm... no known localFiles? if (FcGetVersion() >= 21000) { const QString targetPath(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + QLatin1String("fontconfig")); QDir target(targetPath); if (!target.exists()) { target.mkpath(targetPath); } return targetPath + QLatin1String("/fonts.conf"); } else { return home + QLatin1String("/.fonts.conf"); } } } bool KXftConfig::reset() { m_madeChanges = false; m_hint.reset(); m_hinting.reset(); m_excludeRange.reset(); m_excludePixelRange.reset(); m_subPixel.reset(); m_antiAliasing.reset(); m_antiAliasingHasLocalConfig = false; m_subPixelHasLocalConfig = false; m_hintHasLocalConfig = false; bool ok = false; std::for_each(m_globalFiles.cbegin(), m_globalFiles.cend(), [this, &ok](const QString &file) { ok |= parseConfigFile(file); }); AntiAliasing globalAntialiasing; globalAntialiasing.state = m_antiAliasing.state; SubPixel globalSubPixel; globalSubPixel.type = m_subPixel.type; Hint globalHint; globalHint.style = m_hint.style; Exclude globalExcludeRange; globalExcludeRange.from = m_excludeRange.from; globalExcludeRange.to = m_excludePixelRange.to; Exclude globalExcludePixelRange; globalExcludePixelRange.from = m_excludePixelRange.from; globalExcludePixelRange.to = m_excludePixelRange.to; Hinting globalHinting; globalHinting.set = m_hinting.set; m_antiAliasing.reset(); m_subPixel.reset(); m_hint.reset(); m_hinting.reset(); m_excludeRange.reset(); m_excludePixelRange.reset(); ok |= parseConfigFile(m_file); if (m_antiAliasing.node.isNull()) { m_antiAliasing = globalAntialiasing; } else { m_antiAliasingHasLocalConfig = true; } if (m_subPixel.node.isNull()) { m_subPixel = globalSubPixel; } else { m_subPixelHasLocalConfig = true; } if (m_hint.node.isNull()) { m_hint = globalHint; } else { m_hintHasLocalConfig = true; } if (m_hinting.node.isNull()) { m_hinting = globalHinting; } if (m_excludeRange.node.isNull()) { m_excludeRange = globalExcludeRange; } if (m_excludePixelRange.node.isNull()) { m_excludePixelRange = globalExcludePixelRange; } return ok; } bool KXftConfig::apply() { bool ok = true; if (m_madeChanges) { // // Check if file has been written since we last read it. If it has, then re-read and add any // of our changes... if (fExists(m_file) && getTimeStamp(m_file) != m_time) { KXftConfig newConfig; newConfig.setExcludeRange(m_excludeRange.from, m_excludeRange.to); newConfig.setSubPixelType(m_subPixel.type); newConfig.setHintStyle(m_hint.style); newConfig.setAntiAliasing(m_antiAliasing.state); ok = newConfig.changed() ? newConfig.apply() : true; if (ok) { reset(); } else { m_time = getTimeStamp(m_file); } } else { // Ensure these are always equal... m_excludePixelRange.from = (int)point2Pixel(m_excludeRange.from); m_excludePixelRange.to = (int)point2Pixel(m_excludeRange.to); FcAtomic *atomic = FcAtomicCreate((const unsigned char *)(QFile::encodeName(m_file).data())); ok = false; if (atomic) { if (FcAtomicLock(atomic)) { FILE *f = fopen((char *)FcAtomicNewFile(atomic), "w"); if (f) { applySubPixelType(); applyHintStyle(); applyAntiAliasing(); applyExcludeRange(false); applyExcludeRange(true); // // Check document syntax... static constexpr QStringView qtXmlHeader = u""; static constexpr QStringView xmlHeader = u""; static constexpr QStringView qtDocTypeLine = u""; static constexpr QStringView docTypeLine = u""; QString str(m_doc.toString()); qsizetype idx; if (0 != str.indexOf(u"= 0 || to >= 0) && foundFalse) { m_excludeRange.from = from < to ? from : to; m_excludeRange.to = from < to ? to : from; m_excludeRange.node = n; } else if ((pixelFrom >= 0 || pixelTo >= 0) && foundFalse) { m_excludePixelRange.from = pixelFrom < pixelTo ? pixelFrom : pixelTo; m_excludePixelRange.to = pixelFrom < pixelTo ? pixelTo : pixelFrom; m_excludePixelRange.node = n; } } } } void KXftConfig::applySubPixelType() { if (SubPixel::NotSet == m_subPixel.type) { if (!m_subPixel.node.isNull()) { m_doc.documentElement().removeChild(m_subPixel.node); m_subPixel.node.clear(); } } else { QDomElement matchNode = m_doc.createElement(u"match"_s); QDomElement typeNode = m_doc.createElement(u"const"_s); QDomElement editNode = m_doc.createElement(u"edit"_s); QDomText typeText = m_doc.createTextNode(toStr(m_subPixel.type)); matchNode.setAttribute(u"target"_s, u"font"_s); editNode.setAttribute(u"mode"_s, u"assign"_s); editNode.setAttribute(u"name"_s, u"rgba"_s); editNode.appendChild(typeNode); typeNode.appendChild(typeText); matchNode.appendChild(editNode); if (m_subPixel.node.isNull()) { m_doc.documentElement().appendChild(matchNode); } else { m_doc.documentElement().replaceChild(matchNode, m_subPixel.node); } m_subPixel.node = matchNode; } } void KXftConfig::applyHintStyle() { applyHinting(); if (Hint::NotSet == m_hint.style) { if (!m_hint.node.isNull()) { m_doc.documentElement().removeChild(m_hint.node); m_hint.node.clear(); } if (!m_hinting.node.isNull()) { m_doc.documentElement().removeChild(m_hinting.node); m_hinting.node.clear(); } } else { QDomElement matchNode = m_doc.createElement(u"match"_s), typeNode = m_doc.createElement(u"const"_s), editNode = m_doc.createElement(u"edit"_s); QDomText typeText = m_doc.createTextNode(toStr(m_hint.style)); matchNode.setAttribute(u"target"_s, u"font"_s); editNode.setAttribute(u"mode"_s, u"assign"_s); editNode.setAttribute(u"name"_s, u"hintstyle"_s); editNode.appendChild(typeNode); typeNode.appendChild(typeText); matchNode.appendChild(editNode); if (m_hint.node.isNull()) { m_doc.documentElement().appendChild(matchNode); } else { m_doc.documentElement().replaceChild(matchNode, m_hint.node); } m_hint.node = matchNode; } } void KXftConfig::applyHinting() { QDomElement matchNode = m_doc.createElement(u"match"_s), typeNode = m_doc.createElement(u"bool"_s), editNode = m_doc.createElement(u"edit"_s); QDomText typeText = m_doc.createTextNode(m_hinting.set ? u"true"_s : u"false"_s); matchNode.setAttribute(u"target"_s, u"font"_s); editNode.setAttribute(u"mode"_s, u"assign"_s); editNode.setAttribute(u"name"_s, u"hinting"_s); editNode.appendChild(typeNode); typeNode.appendChild(typeText); matchNode.appendChild(editNode); if (m_hinting.node.isNull()) { m_doc.documentElement().appendChild(matchNode); } else { m_doc.documentElement().replaceChild(matchNode, m_hinting.node); } m_hinting.node = matchNode; } void KXftConfig::applyExcludeRange(bool pixel) { Exclude &range = pixel ? m_excludePixelRange : m_excludeRange; if (equal(range.from, 0) && equal(range.to, 0)) { if (!range.node.isNull()) { m_doc.documentElement().removeChild(range.node); range.node.clear(); } } else { QString fromString, toString; fromString.setNum(range.from); toString.setNum(range.to); QDomElement matchNode = m_doc.createElement(u"match"_s), fromTestNode = m_doc.createElement(u"test"_s), fromNode = m_doc.createElement(u"double"_s), toTestNode = m_doc.createElement(u"test"_s), toNode = m_doc.createElement(u"double"_s), editNode = m_doc.createElement(u"edit"_s), boolNode = m_doc.createElement(u"bool"_s); QDomText fromText = m_doc.createTextNode(fromString), toText = m_doc.createTextNode(toString), boolText = m_doc.createTextNode(u"false"_s); matchNode.setAttribute(u"target"_s, u"font"_s); // CPD: Is target "font" or "pattern" ???? fromTestNode.setAttribute(u"qual"_s, u"any"_s); fromTestNode.setAttribute(u"name"_s, pixel ? u"pixelsize"_s : u"size"_s); fromTestNode.setAttribute(u"compare"_s, u"more_eq"_s); fromTestNode.appendChild(fromNode); fromNode.appendChild(fromText); toTestNode.setAttribute(u"qual"_s, u"any"_s); toTestNode.setAttribute(u"name"_s, pixel ? u"pixelsize"_s : u"size"_s); toTestNode.setAttribute(u"compare"_s, u"less_eq"_s); toTestNode.appendChild(toNode); toNode.appendChild(toText); editNode.setAttribute(u"mode"_s, u"assign"_s); editNode.setAttribute(u"name"_s, u"antialias"_s); editNode.appendChild(boolNode); boolNode.appendChild(boolText); matchNode.appendChild(fromTestNode); matchNode.appendChild(toTestNode); matchNode.appendChild(editNode); if (!m_antiAliasing.node.isNull()) { m_doc.documentElement().removeChild(range.node); } if (range.node.isNull()) { m_doc.documentElement().appendChild(matchNode); } else { m_doc.documentElement().replaceChild(matchNode, range.node); } range.node = matchNode; } } bool KXftConfig::antiAliasingHasLocalConfig() const { return m_antiAliasingHasLocalConfig; } KXftConfig::AntiAliasing::State KXftConfig::getAntiAliasing() const { return m_antiAliasing.state; } void KXftConfig::setAntiAliasing(AntiAliasing::State state) { if (state != m_antiAliasing.state) { m_antiAliasing.state = state; m_madeChanges = true; } } void KXftConfig::applyAntiAliasing() { if (AntiAliasing::NotSet == m_antiAliasing.state) { if (!m_antiAliasing.node.isNull()) { m_doc.documentElement().removeChild(m_antiAliasing.node); m_antiAliasing.node.clear(); } } else { QDomElement matchNode = m_doc.createElement(u"match"_s); QDomElement typeNode = m_doc.createElement(u"bool"_s); QDomElement editNode = m_doc.createElement(u"edit"_s); QDomText typeText = m_doc.createTextNode(m_antiAliasing.state == AntiAliasing::Enabled ? u"true"_s : u"false"_s); matchNode.setAttribute(u"target"_s, u"font"_s); editNode.setAttribute(u"mode"_s, u"assign"_s); editNode.setAttribute(u"name"_s, u"antialias"_s); editNode.appendChild(typeNode); typeNode.appendChild(typeText); matchNode.appendChild(editNode); if (!m_antiAliasing.node.isNull()) { m_doc.documentElement().removeChild(m_antiAliasing.node); } m_doc.documentElement().appendChild(matchNode); m_antiAliasing.node = matchNode; } } // KXftConfig only parses one config file, user's .fonts.conf usually. // If that one doesn't exist, then KXftConfig doesn't know if antialiasing // is enabled or not. So try to find out the default value from the default font. // Maybe there's a better way *shrug*. bool KXftConfig::aliasingEnabled() { FcPattern *pattern = FcPatternCreate(); FcConfigSubstitute(nullptr, pattern, FcMatchPattern); FcDefaultSubstitute(pattern); FcResult result; FcPattern *f = FcFontMatch(nullptr, pattern, &result); FcBool antialiased = FcTrue; FcPatternGetBool(f, FC_ANTIALIAS, 0, &antialiased); FcPatternDestroy(f); FcPatternDestroy(pattern); return antialiased == FcTrue; } #endif