File indexing completed on 2024-05-19 04:45:54

0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT
0003 
0004 // Own headers
0005 // First the interface, which forces the header to be self-contained.
0006 #include "rgbcolorspacefactory.h"
0007 
0008 #include "rgbcolorspace.h"
0009 #include <qdir.h>
0010 #include <qfileinfo.h>
0011 #include <qglobal.h>
0012 #include <qlist.h>
0013 #include <qstring.h>
0014 #include <qstringbuilder.h>
0015 #include <qstringliteral.h>
0016 #include <type_traits>
0017 #include <utility>
0018 
0019 namespace PerceptualColor
0020 {
0021 /** @brief Create an sRGB color space object.
0022  *
0023  * This is build-in, no external ICC file is used.
0024  *
0025  * @pre This function is called from the main thread.
0026  *
0027  * @returns A shared pointer to the newly created color space object.
0028  *
0029  * @internal
0030  *
0031  * @todo This should be implemented as singleton with on-demand
0032  * initialization. This requires however changes to @ref RgbColorSpace
0033  * which should <em>not</em> guarantee that properties like
0034  * @ref RgbColorSpace::profileName() are constant. Instead,
0035  * for the sRGB profiles, the translation should be dynamic. */
0036 QSharedPointer<PerceptualColor::RgbColorSpace> RgbColorSpaceFactory::createSrgb()
0037 {
0038     return RgbColorSpace::createSrgb();
0039 }
0040 
0041 /** @brief Try to create a color space object for a given ICC file.
0042  *
0043  * @note This function may fail to create the color space object when it
0044  * cannot open the given file, or when the file cannot be interpreted.
0045  *
0046  * @pre This function is called from the main thread.
0047  *
0048  * @param fileName The file name. See <tt>QFile</tt> documentation for what
0049  * are valid file names. The file is only used during the execution of this
0050  * function and it is closed again at the end of this function. The created
0051  * object does not need the file anymore, because all necessary information
0052  * has already been loaded into memory. Accepted are most RGB-based
0053  * ICC profiles up to version 4.
0054  *
0055  * @returns A shared pointer to a newly created color space object on success.
0056  * A shared pointer to <tt>nullptr</tt> on fail. */
0057 QSharedPointer<PerceptualColor::RgbColorSpace> RgbColorSpaceFactory::createFromFile(const QString &fileName)
0058 {
0059     return RgbColorSpace::createFromFile(fileName);
0060 }
0061 
0062 /** @brief List of directories where color profiles are typically
0063  * stored on the current system.
0064  *
0065  * Often, but not always, operating systems have an API to
0066  * get access to this directories with color profiles or
0067  * to get the actual color profile of a specific device
0068  * (screen, printer…). On Linux, this is typically provided by
0069  * <a href="https://www.freedesktop.org/software/colord/index.html">colord</a>.
0070  * Also on Windows, there are specific API calls
0071  * (<a href="https://learn.microsoft.com/en-us/windows/win32/wcs/profile-management-functions">[1]</a>
0072  * <a href="https://learn.microsoft.com/en-us/windows/win32/api/icm/nf-icm-wcsgetdefaultcolorprofile">[2]</a>
0073  * <a href="https://learn.microsoft.com/en-us/windows/win32/api/icm/nf-icm-getcolordirectoryw">[3]</a>
0074  * <a href="https://learn.microsoft.com/en-us/uwp/api/windows.graphics.display.displayinformation.getcolorprofileasync?view=winrt-22621">[4]</a>)
0075  * Some other operating systems have similar APIs.
0076  *
0077  * The best solution is to rely on the operating system’s API. However,
0078  * if you can’t use this API for some reasons, this function provides a
0079  * last-resort alternative. Not all operating systems have standardised
0080  * directories for colour profiles. This function provide a list of typical
0081  * storage locations of ICC profile files and works satisfactorily for at
0082  * least Linux, BSD, MacOS and Windows.
0083  *
0084  * @returns A preference-ordered list of typical storage locations of
0085  * color profiles on the current system. The list might  be empty if no
0086  * color profile directories are found on the system. The returned directories
0087  * use '/' as file separator regardless of the operating system, just
0088  * as <tt>QFile</tt> expects. To find color profiles, parse these directories
0089  * recursively, including subdirectories. Note that ICC colour profiles
0090  * traditionally have a file name ending in <tt>.icm</tt> on Windows systems
0091  * and a name ending in <tt>.icc</tt> on all other operating systems,
0092  * but today on every operating system you might find actually both file name
0093  * endings.
0094  *
0095  * @note This function takes into account environment variables, home
0096  * directories and so on. Potential colour profile directories that do not
0097  * actually exist on the current system are not returned. Since these values
0098  * could change, another call of this function could return a different result.
0099  *
0100  * @internal
0101  *
0102  * @note Internal implementation details: User directories appear at the top
0103  * of the list, system-wide directories appear at the bottom. The returned
0104  * directories are absolute paths with all symlinks removed. There are no
0105  * duplicates in the list. All returned directories actually exist. */
0106 QStringList RgbColorSpaceFactory::colorProfileDirectories()
0107 {
0108     // https://web.archive.org/web/20140625123925/http://nadeausoftware.com/articles/2012/01/c_c_tip_how_use_compiler_predefined_macros_detect_operating_system
0109     // describes well how to recognize the current system by compiler-defined
0110     // macros. But Qt makes things more comfortable for us…
0111     //
0112     // Q_OS_WIN is defined on all Windows-like systems.
0113     //
0114     // Q_OS_UNIX is defined on all other systems.
0115     //
0116     // Q_OS_DARWIN is defined on MacOS-like systems along with Q_OS_UNIX.
0117     //
0118     // Q_OS_UNIX and Q_OS_WIN are mutually exclusive: They never appear at
0119     // the same time, not even on Cygwin. Reference: Q_OS_UNIX definition at
0120     // https://code.woboq.org/qt6/qtbase/src/corelib/global/qsystemdetection.h.html
0121 
0122     QStringList candidates;
0123 
0124 #ifdef Q_OS_DARWIN
0125     // MacOS-like systems (including iOS and other derivatives)
0126     // The Qt version we are relying on requires at least MacOS X. Starting
0127     // with MacOS X, those are the relevant dictionaries, as also
0128     // https://stackoverflow.com/a/32729370 describes:
0129 
0130     // User-supplied settings:
0131     candidates.append( //
0132         QDir::homePath() + QStringLiteral(u"/Library/ColorSync/Profiles/"));
0133 
0134     // Settings supplied by the local machine:
0135     candidates.append(QStringLiteral(u"/Library/ColorSync/Profiles/"));
0136 
0137     // Settings supplied by the network administrator:
0138     candidates.append(QStringLiteral(u"/Network/Library/ColorSync/Profiles/"));
0139 
0140     // Hard-coded settings of MacOS itself, that cannot be changed:
0141     candidates.append(QStringLiteral(u"/System/Library/ColorSync/Profiles/"));
0142 
0143     // Printer drivers also might have color profiles:
0144     candidates.append(QStringLiteral(u"/Library/Printers/")); // TODO Useful?
0145 
0146     // Adobe’s applications also might have color profiles:
0147     candidates.append( // TODO Is it useful to support particular programs?
0148         QStringLiteral(u"/Library/Application Support/Adobe/Color/Profiles/"));
0149 
0150 #elif defined(Q_OS_UNIX)
0151     // Unix-like systems (including BSD, Linux, Android), excluding those which
0152     // have Q_OS_DARWIN defined (we are after this in the #elif statement).
0153     // The following settings will work probably well on Linux and BSD,
0154     // but not so well on Android which does not seem to have a real standard.
0155 
0156     const QString subdirectory1 = QStringLiteral(u"/color/icc/");
0157     const QString subdirectory2 = QStringLiteral(u"/icc/");
0158     QString baseDirectory;
0159     baseDirectory = qEnvironmentVariable("XDG_DATA_HOME");
0160     if (!baseDirectory.isEmpty()) {
0161         candidates.append(baseDirectory + subdirectory1);
0162         candidates.append(baseDirectory + subdirectory2);
0163     }
0164     baseDirectory = QDir::homePath() + QStringLiteral(u"/.local/share/");
0165     candidates.append(baseDirectory + subdirectory1);
0166     candidates.append(baseDirectory + subdirectory2);
0167     baseDirectory = QDir::homePath();
0168     candidates.append(baseDirectory + subdirectory1);
0169     candidates.append(baseDirectory + subdirectory2);
0170     candidates.append(baseDirectory + QStringLiteral(u"/.color/icc/"));
0171 
0172     QStringList baseDirectoryList = //
0173         qEnvironmentVariable("XDG_DATA_DIRS").split(QStringLiteral(u":"));
0174     // Fallback values for empty XDG_DATA_DIRS, as defined in
0175     // the Free Desktop Specification:
0176     baseDirectoryList.append(QStringLiteral(u"/usr/local/share/"));
0177     baseDirectoryList.append(QStringLiteral(u"/usr/share/"));
0178     // Custom search directory:
0179     baseDirectoryList.append(QStringLiteral(u"/var/lib/"));
0180     for (const QString &path : std::as_const(baseDirectoryList)) {
0181         if (!path.isEmpty()) {
0182             candidates.append(path + QStringLiteral(u"/color/icc/"));
0183             candidates.append(path + QStringLiteral(u"/icc/"));
0184         }
0185     }
0186 
0187 #elif defined(Q_OS_WIN)
0188     // Windows-like systems
0189 
0190     // NOTE It is possible to get the Windows system directory with
0191     // Windows API calls. However, we want to reduce our dependencies and
0192     // therefore avoid to link against this API. So the following code
0193     // is commented out.
0194     //
0195     // // If an array is initialized and the array length is larger than
0196     // // the number of initialization values, the remaining array values
0197     // // will be initialized to zero. Therefore, the following code
0198     // initializes the entire (!) array to 0.
0199     // wchar_t sysDir[MAX_PATH + 1] = {0};
0200     // GetSystemDirectoryW(sysDir, MAX_PATH);
0201     // QString winSysDir = QString::fromWCharArray(sysDir);
0202 
0203     QString winSysDir = qEnvironmentVariable("windir");
0204 
0205     // Starting with XP, this is the default directory:
0206     candidates.append(winSysDir + QStringLiteral(u"/Spool/Drivers/Color/"));
0207     // In Windows 95, 98, this was the default directory:
0208     candidates.append(winSysDir + QStringLiteral(u"/Color/")); // TODO Useful?
0209 #endif
0210 
0211     // Prepare the return value:
0212     QFileInfo info; // QFileInfo isn’t only about files, but also directories!
0213     QStringList result;
0214     // cppcheck-suppress knownEmptyContainer // false positive
0215     for (const QString &path : std::as_const(candidates)) {
0216         // cleanPath() has redundant separators removed.
0217         info = QFileInfo(QDir::cleanPath(path));
0218         if (info.isDir()) {
0219             // canonicalFilePath() returns an absolute path without redundant
0220             // '.' or '.. ' elements (or an empty string if this is
0221             // not possible):
0222             result.append(info.canonicalFilePath());
0223         }
0224     }
0225     result.removeDuplicates();
0226     result.removeAll(QString()); // Remove empty strings
0227     return result;
0228 }
0229 
0230 } // namespace PerceptualColor