Create QgsColorUtils class

Contains methods to serialize/deserialize colors from xml and strings,
where the colors are losslessly stored/restored.

The previous methods for storing colors (Eg QgsSymbolLayerUtils::encode/decodeColor)
are lossy, and only store QColors as 8 bit RGB representations. In
contrast, the new methods store the full lossless representation of
a QColor, including support for 16 bit color components, extended
RGB color components, and storage of HSL/HSV/CMYK color specifications
using their original color components instead of RGB components.

When these new methods are used in place of the existing lossy methods,
they open the possibility of 16 bit color support for QGIS symbols/projects,
(and potentially future CMYK color support).
This commit is contained in:
Nyall Dawson 2022-07-06 10:23:05 +10:00
parent d1dfad2452
commit 44708b9c0c
7 changed files with 674 additions and 0 deletions

View File

@ -0,0 +1,91 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/qgscolorutils.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsColorUtils
{
%Docstring(signature="appended")
Contains utility functions for working with colors.
.. versionadded:: 3.28
%End
%TypeHeaderCode
#include "qgscolorutils.h"
%End
public:
static void writeXml( const QColor &color, const QString &identifier,
QDomDocument &document, QDomElement &element, const QgsReadWriteContext &context );
%Docstring
Writes a ``color`` to an XML ``element``, storing it under the specified ``identifier``.
This method losslessly stores a color's definition in an XML ``element``. All properties
of the color are stored, including the color specification and original values of the
color's components. It is therefore suitable for storing high color depth colors (such
as 16 bit colors), or colors using alternative specifications such as CMYK colors.
The ``identifier`` string is used to specify the element name for the stored color,
allowing for multiple color definitions to be stored in a single ``element`` (assuming
each uses a unique identifier string).
.. seealso:: :py:func:`readXml`
%End
static QColor readXml( const QDomElement &element, const QString &identifier, const QgsReadWriteContext &context );
%Docstring
Reads a color from an XML ``element``, matching the specified ``identifier`` string.
This method losslessly retrieves a color's definition from an XML element. All properties
of the color are restored, including the color specification and original values of the
color's components. It is therefore suitable for restoring high color depth colors (such
as 16 bit colors), or colors using alternative specifications such as CMYK colors.
An invalid color will be returned if the color could not be read.
.. seealso:: :py:func:`writeXml`
%End
static QString colorToString( const QColor &color );
%Docstring
Encodes a ``color`` into a string value.
This method losslessly stores a color's definition into a single string value. All properties
of the color are stored, including the color specification and original values of the
color's components. It is therefore suitable for storing high color depth colors (such
as 16 bit colors), or colors using alternative specifications such as CMYK colors.
.. seealso:: :py:func:`colorFromString`
%End
static QColor colorFromString( const QString &string );
%Docstring
Decodes a ``string`` into a color value.
This method losslessly retrieves a color's definition from a string value. All properties
of the color are restored, including the color specification and original values of the
color's components. It is therefore suitable for restoring high color depth colors (such
as 16 bit colors), or colors using alternative specifications such as CMYK colors.
An invalid color will be returned if the color could not be read.
.. seealso:: :py:func:`colorToString`
%End
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/qgscolorutils.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -25,6 +25,7 @@
%Include auto_generated/qgscolorrampimpl.sip
%Include auto_generated/qgscolorscheme.sip
%Include auto_generated/qgscolorschemeregistry.sip
%Include auto_generated/qgscolorutils.sip
%Include auto_generated/qgsconditionalstyle.sip
%Include auto_generated/qgsconnectionregistry.sip
%Include auto_generated/qgscoordinateformatter.sip

View File

@ -343,6 +343,7 @@ set(QGIS_CORE_SRCS
qgscolorrampimpl.cpp
qgscolorscheme.cpp
qgscolorschemeregistry.cpp
qgscolorutils.cpp
qgscommandlineutils.cpp
qgsconditionalstyle.cpp
qgsconnectionregistry.cpp
@ -1004,6 +1005,7 @@ set(QGIS_CORE_HDRS
qgscolorrampimpl.h
qgscolorscheme.h
qgscolorschemeregistry.h
qgscolorutils.h
qgsconditionalstyle.h
qgsconnectionpool.h
qgsconnectionregistry.h

278
src/core/qgscolorutils.cpp Normal file
View File

@ -0,0 +1,278 @@
/***************************************************************************
qgscolorutils.cpp
---------------------------
begin : July 2022
copyright : (C) 2022 by Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgscolorutils.h"
#include <QColor>
#include <QDomDocument>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
void QgsColorUtils::writeXml( const QColor &color, const QString &identifier, QDomDocument &document, QDomElement &element, const QgsReadWriteContext & )
{
{
const QDomElement oldElement = element.firstChildElement( identifier );
if ( !oldElement.isNull() )
element.removeChild( oldElement );
}
QDomElement colorElement = document.createElement( identifier );
if ( !color.isValid() )
{
colorElement.setAttribute( QStringLiteral( "invalid" ), QStringLiteral( "1" ) );
}
else
{
QString spec;
switch ( color.spec() )
{
case QColor::Invalid:
break; // not possible
case QColor::Rgb:
case QColor::ExtendedRgb:
{
// QColor will automatically adapt between extended rgb/rgb based on value of red/green/blue components
spec = QStringLiteral( "rgb" );
qreal red = 1;
qreal green = 1;
qreal blue = 1;
color.getRgbF( &red, &green, &blue );
colorElement.setAttribute( QStringLiteral( "red" ), qgsDoubleToString( red ) );
colorElement.setAttribute( QStringLiteral( "green" ), qgsDoubleToString( green ) );
colorElement.setAttribute( QStringLiteral( "blue" ), qgsDoubleToString( blue ) );
break;
}
case QColor::Hsv:
{
spec = QStringLiteral( "hsv" );
qreal h = 1;
qreal s = 1;
qreal v = 1;
color.getHsvF( &h, &s, &v );
colorElement.setAttribute( QStringLiteral( "hue" ), qgsDoubleToString( h ) );
colorElement.setAttribute( QStringLiteral( "saturation" ), qgsDoubleToString( s ) );
colorElement.setAttribute( QStringLiteral( "value" ), qgsDoubleToString( v ) );
break;
}
case QColor::Hsl:
{
spec = QStringLiteral( "hsl" );
qreal h = 1;
qreal s = 1;
qreal l = 1;
color.getHslF( &h, &s, &l );
colorElement.setAttribute( QStringLiteral( "hue" ), qgsDoubleToString( h ) );
colorElement.setAttribute( QStringLiteral( "saturation" ), qgsDoubleToString( s ) );
colorElement.setAttribute( QStringLiteral( "lightness" ), qgsDoubleToString( l ) );
break;
}
case QColor::Cmyk:
{
spec = QStringLiteral( "cmyk" );
qreal c = 1;
qreal m = 1;
qreal y = 1;
qreal k = 1;
color.getCmykF( &c, &y, &m, &k );
colorElement.setAttribute( QStringLiteral( "c" ), qgsDoubleToString( c ) );
colorElement.setAttribute( QStringLiteral( "m" ), qgsDoubleToString( m ) );
colorElement.setAttribute( QStringLiteral( "y" ), qgsDoubleToString( y ) );
colorElement.setAttribute( QStringLiteral( "k" ), qgsDoubleToString( k ) );
break;
}
}
colorElement.setAttribute( QStringLiteral( "spec" ), spec );
if ( color.alphaF() < 1.0 )
{
colorElement.setAttribute( QStringLiteral( "alpha" ), qgsDoubleToString( color.alphaF() ) );
}
}
element.appendChild( colorElement );
}
QColor QgsColorUtils::readXml( const QDomElement &element, const QString &identifier, const QgsReadWriteContext & )
{
const QDomElement colorElement = element.firstChildElement( identifier );
if ( colorElement.isNull() )
return QColor();
const bool invalid = colorElement.attribute( QStringLiteral( "invalid" ), QStringLiteral( "0" ) ).toInt();
if ( invalid )
return QColor();
QColor res;
const QString spec = colorElement.attribute( QStringLiteral( "spec" ) );
if ( spec == QLatin1String( "rgb" ) )
{
// QColor will automatically adapt between extended rgb/rgb based on value of red/green/blue components
const double red = colorElement.attribute( QStringLiteral( "red" ) ).toDouble();
const double green = colorElement.attribute( QStringLiteral( "green" ) ).toDouble();
const double blue = colorElement.attribute( QStringLiteral( "blue" ) ).toDouble();
res = QColor::fromRgbF( red, green, blue );
}
else if ( spec == QLatin1String( "hsv" ) )
{
const double hue = colorElement.attribute( QStringLiteral( "hue" ) ).toDouble();
const double saturation = colorElement.attribute( QStringLiteral( "saturation" ) ).toDouble();
const double value = colorElement.attribute( QStringLiteral( "value" ) ).toDouble();
res = QColor::fromHsvF( hue, saturation, value );
}
else if ( spec == QLatin1String( "hsl" ) )
{
const double hue = colorElement.attribute( QStringLiteral( "hue" ) ).toDouble();
const double saturation = colorElement.attribute( QStringLiteral( "saturation" ) ).toDouble();
const double value = colorElement.attribute( QStringLiteral( "lightness" ) ).toDouble();
res = QColor::fromHslF( hue, saturation, value );
}
else if ( spec == QLatin1String( "cmyk" ) )
{
const double cyan = colorElement.attribute( QStringLiteral( "c" ) ).toDouble();
const double magenta = colorElement.attribute( QStringLiteral( "m" ) ).toDouble();
const double yellow = colorElement.attribute( QStringLiteral( "y" ) ).toDouble();
const double black = colorElement.attribute( QStringLiteral( "k" ) ).toDouble();
res = QColor::fromCmykF( cyan, magenta, yellow, black );
}
{
const double alpha = colorElement.attribute( QStringLiteral( "alpha" ), QStringLiteral( "1" ) ).toDouble();
res.setAlphaF( alpha );
}
return res;
}
QString QgsColorUtils::colorToString( const QColor &color )
{
if ( !color.isValid() )
return QString();
switch ( color.spec() )
{
case QColor::Invalid:
break; // not possible
case QColor::Rgb:
case QColor::ExtendedRgb:
{
// QColor will automatically adapt between extended rgb/rgb based on value of red/green/blue components
qreal red = 1;
qreal green = 1;
qreal blue = 1;
qreal alpha = 1;
color.getRgbF( &red, &green, &blue, &alpha );
return QStringLiteral( "rgb:%1,%2,%3,%4" ).arg( qgsDoubleToString( red ),
qgsDoubleToString( green ),
qgsDoubleToString( blue ),
qgsDoubleToString( alpha ) );
}
case QColor::Hsv:
{
qreal h = 1;
qreal s = 1;
qreal v = 1;
qreal alpha = 1;
color.getHsvF( &h, &s, &v, &alpha );
return QStringLiteral( "hsv:%1,%2,%3,%4" ).arg( qgsDoubleToString( h ),
qgsDoubleToString( s ),
qgsDoubleToString( v ),
qgsDoubleToString( alpha ) );
}
case QColor::Hsl:
{
qreal h = 1;
qreal s = 1;
qreal l = 1;
qreal alpha = 1;
color.getHslF( &h, &s, &l, &alpha );
return QStringLiteral( "hsl:%1,%2,%3,%4" ).arg( qgsDoubleToString( h ),
qgsDoubleToString( s ),
qgsDoubleToString( l ),
qgsDoubleToString( alpha ) );
}
case QColor::Cmyk:
{
qreal c = 1;
qreal m = 1;
qreal y = 1;
qreal k = 1;
qreal alpha = 1;
color.getCmykF( &c, &y, &m, &k, &alpha );
return QStringLiteral( "cmyk:%1,%2,%3,%4,%5" ).arg( qgsDoubleToString( c ),
qgsDoubleToString( m ),
qgsDoubleToString( y ),
qgsDoubleToString( k ),
qgsDoubleToString( alpha ) );
}
}
return QString();
}
QColor QgsColorUtils::colorFromString( const QString &string )
{
if ( string.isEmpty() )
return QColor();
const thread_local QRegularExpression rx( QStringLiteral( "^([a-z]+):([\\d\\.\\-]+),([\\d\\.\\-]+),([\\d\\.\\-]+),([\\d\\.\\-]+),?([\\d\\.\\-]*)$" ) );
const QRegularExpressionMatch match = rx.match( string );
if ( !match.hasMatch() )
return QColor();
const QString spec = match.captured( 1 );
if ( spec == QLatin1String( "rgb" ) )
{
// QColor will automatically adapt between extended rgb/rgb based on value of red/green/blue components
const double red = match.captured( 2 ).toDouble();
const double green = match.captured( 3 ).toDouble();
const double blue = match.captured( 4 ).toDouble();
const double alpha = match.captured( 5 ).toDouble();
return QColor::fromRgbF( red, green, blue, alpha );
}
else if ( spec == QLatin1String( "hsv" ) )
{
const double hue = match.captured( 2 ).toDouble();
const double saturation = match.captured( 3 ).toDouble();
const double value = match.captured( 4 ).toDouble();
const double alpha = match.captured( 5 ).toDouble();
return QColor::fromHsvF( hue, saturation, value, alpha );
}
else if ( spec == QLatin1String( "hsl" ) )
{
const double hue = match.captured( 2 ).toDouble();
const double saturation = match.captured( 3 ).toDouble();
const double lightness = match.captured( 4 ).toDouble();
const double alpha = match.captured( 5 ).toDouble();
return QColor::fromHslF( hue, saturation, lightness, alpha );
}
else if ( spec == QLatin1String( "cmyk" ) )
{
const double cyan = match.captured( 2 ).toDouble();
const double magenta = match.captured( 3 ).toDouble();
const double yellow = match.captured( 4 ).toDouble();
const double black = match.captured( 5 ).toDouble();
const double alpha = match.captured( 6 ).toDouble();
return QColor::fromCmykF( cyan, magenta, yellow, black, alpha );
}
return QColor();
}

98
src/core/qgscolorutils.h Normal file
View File

@ -0,0 +1,98 @@
/***************************************************************************
qgscolorutils.h
---------------------------
begin : July 2022
copyright : (C) 2022 by Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSCOLORUTILS_H
#define QGSCOLORUTILS_H
#include "qgis_core.h"
#include "qgis_sip.h"
#include "qgis.h"
#include <QDomDocument>
#include <QDomElement>
class QgsReadWriteContext;
/**
* \ingroup core
* \class QgsColorUtils
* \brief Contains utility functions for working with colors.
* \since QGIS 3.28
*/
class CORE_EXPORT QgsColorUtils
{
public:
/**
* Writes a \a color to an XML \a element, storing it under the specified \a identifier.
*
* This method losslessly stores a color's definition in an XML \a element. All properties
* of the color are stored, including the color specification and original values of the
* color's components. It is therefore suitable for storing high color depth colors (such
* as 16 bit colors), or colors using alternative specifications such as CMYK colors.
*
* The \a identifier string is used to specify the element name for the stored color,
* allowing for multiple color definitions to be stored in a single \a element (assuming
* each uses a unique identifier string).
*
* \see readXml()
*/
static void writeXml( const QColor &color, const QString &identifier,
QDomDocument &document, QDomElement &element, const QgsReadWriteContext &context );
/**
* Reads a color from an XML \a element, matching the specified \a identifier string.
*
* This method losslessly retrieves a color's definition from an XML element. All properties
* of the color are restored, including the color specification and original values of the
* color's components. It is therefore suitable for restoring high color depth colors (such
* as 16 bit colors), or colors using alternative specifications such as CMYK colors.
*
* An invalid color will be returned if the color could not be read.
*
* \see writeXml()
*/
static QColor readXml( const QDomElement &element, const QString &identifier, const QgsReadWriteContext &context );
/**
* Encodes a \a color into a string value.
*
* This method losslessly stores a color's definition into a single string value. All properties
* of the color are stored, including the color specification and original values of the
* color's components. It is therefore suitable for storing high color depth colors (such
* as 16 bit colors), or colors using alternative specifications such as CMYK colors.
*
* \see colorFromString()
*/
static QString colorToString( const QColor &color );
/**
* Decodes a \a string into a color value.
*
* This method losslessly retrieves a color's definition from a string value. All properties
* of the color are restored, including the color specification and original values of the
* color's components. It is therefore suitable for restoring high color depth colors (such
* as 16 bit colors), or colors using alternative specifications such as CMYK colors.
*
* An invalid color will be returned if the color could not be read.
*
* \see colorToString()
*/
static QColor colorFromString( const QString &string );
};
#endif // QGSCOLORUTILS_H

View File

@ -51,6 +51,7 @@ ADD_PYTHON_TEST(PyQgsColorRamp test_qgscolorramp.py)
ADD_PYTHON_TEST(PyQgsColorRampLegendNode test_qgscolorramplegendnode.py)
ADD_PYTHON_TEST(PyQgsColorScheme test_qgscolorscheme.py)
ADD_PYTHON_TEST(PyQgsColorSchemeRegistry test_qgscolorschemeregistry.py)
ADD_PYTHON_TEST(PyQgsColorUtils test_qgscolorutils.py)
ADD_PYTHON_TEST(PyQgsCombinedStyleModel test_qgscombinedstylemodel.py)
ADD_PYTHON_TEST(PyQgsCoordinateFormatter test_qgscoordinateformatter.py)
ADD_PYTHON_TEST(PyQgsCoordinateOperationWidget test_qgscoordinateoperationwidget.py)

View File

@ -0,0 +1,203 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsColorUtils.
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
"""
__author__ = 'Nyall Dawson'
__date__ = '06/07/2022'
__copyright__ = 'Copyright 2022, The QGIS Project'
import shutil
import qgis # NOQA
import tempfile
import os
from qgis.PyQt.QtGui import QColor
from qgis.core import (
Qgis,
QgsColorUtils,
QgsReadWriteContext
)
from qgis.PyQt.QtXml import QDomDocument
from qgis.testing import unittest
from utilities import unitTestDataPath
class TestQgsColorUtils(unittest.TestCase):
def test_color_xml(self):
"""
Test storing/restoring colors from xml
"""
doc = QDomDocument()
context = QgsReadWriteContext()
element = doc.createElement('element')
# invalid color
QgsColorUtils.writeXml(QColor(), 'my_color', doc, element, context)
res = QgsColorUtils.readXml(element, 'my_color', context)
self.assertFalse(res.isValid())
# rgb color
color = QColor.fromRgbF(1 / 65536, 2 / 65536, 3 / 65536, 4 / 65536)
QgsColorUtils.writeXml(color, 'my_color', doc, element, context)
res = QgsColorUtils.readXml(element, 'my_color', context)
self.assertTrue(res.isValid())
self.assertEqual(res.spec(), QColor.Rgb)
self.assertAlmostEqual(res.redF(), 1 / 65536, 5)
self.assertAlmostEqual(res.greenF(), 2 / 65536, 5)
self.assertAlmostEqual(res.blueF(), 3 / 65536, 5)
self.assertAlmostEqual(res.alphaF(), 4 / 65536, 5)
color = QColor.fromRgb(16, 17, 18, 20)
QgsColorUtils.writeXml(color, 'my_color', doc, element, context)
res = QgsColorUtils.readXml(element, 'my_color', context)
self.assertTrue(res.isValid())
self.assertEqual(res.spec(), QColor.Rgb)
self.assertEqual(res.red(), 16)
self.assertEqual(res.green(), 17)
self.assertEqual(res.blue(), 18)
self.assertEqual(res.alpha(), 20)
# rgb extended color
color = QColor.fromRgbF(-1 / 65536, 2 / 65536, 3 / 65536, 4 / 65536)
QgsColorUtils.writeXml(color, 'my_rgb_ex_color', doc, element, context)
res = QgsColorUtils.readXml(element, 'my_rgb_ex_color', context)
self.assertTrue(res.isValid())
self.assertEqual(res.spec(), QColor.ExtendedRgb)
self.assertAlmostEqual(res.redF(), -1 / 65536, 5)
self.assertAlmostEqual(res.greenF(), 2 / 65536, 5)
self.assertAlmostEqual(res.blueF(), 3 / 65536, 5)
self.assertAlmostEqual(res.alphaF(), 4 / 65536, 5)
# hsv color
color = QColor.fromHsvF(1 / 65536, 2 / 65536, 3 / 65536, 4 / 65536)
QgsColorUtils.writeXml(color, 'my_hsv_color', doc, element, context)
res = QgsColorUtils.readXml(element, 'my_hsv_color', context)
self.assertTrue(res.isValid())
self.assertEqual(res.spec(), QColor.Hsv)
self.assertAlmostEqual(res.hueF(), 1 / 65536, 4)
self.assertAlmostEqual(res.hsvSaturationF(), 2 / 65536, 5)
self.assertAlmostEqual(res.valueF(), 3 / 65536, 5)
self.assertAlmostEqual(res.alphaF(), 4 / 65536, 5)
# hsl color
color = QColor.fromHslF(111 / 65536, 12222 / 65536, 333 / 65536, 4 / 65536)
QgsColorUtils.writeXml(color, 'my_hsl_color', doc, element, context)
res = QgsColorUtils.readXml(element, 'my_hsl_color', context)
self.assertTrue(res.isValid())
self.assertEqual(res.spec(), QColor.Hsl)
self.assertAlmostEqual(res.hslHueF(), 111 / 65536, 5)
self.assertAlmostEqual(res.hslSaturationF(), 12222 / 65536, 5)
self.assertAlmostEqual(res.lightnessF(), 333 / 65536, 4)
self.assertAlmostEqual(res.alphaF(), 4 / 65536, 5)
# cmyk color
color = QColor.fromCmykF(1 / 65536, 2 / 65536, 3 / 65536, 4 / 65536, 5 / 65536)
QgsColorUtils.writeXml(color, 'my_cmyk_color', doc, element, context)
res = QgsColorUtils.readXml(element, 'my_cmyk_color', context)
self.assertTrue(res.isValid())
self.assertEqual(res.spec(), QColor.Cmyk)
self.assertAlmostEqual(res.cyanF(), 1 / 65536, 4)
self.assertAlmostEqual(res.magentaF(), 2 / 65536, 4)
self.assertAlmostEqual(res.yellowF(), 3 / 65536, 4)
self.assertAlmostEqual(res.blackF(), 4 / 65536, 4)
self.assertAlmostEqual(res.alphaF(), 5 / 65536, 5)
# missing color
res = QgsColorUtils.readXml(element, 'not there', context)
self.assertFalse(res.isValid())
def test_color_string(self):
"""
Test storing/restoring colors from strings
"""
# invalid color
string = QgsColorUtils.colorToString(QColor())
res = QgsColorUtils.colorFromString(string)
self.assertFalse(res.isValid())
# rgb color
color = QColor.fromRgbF(1 / 65536, 2 / 65536, 3 / 65536, 4 / 65536)
string = QgsColorUtils.colorToString(color)
res = QgsColorUtils.colorFromString(string)
self.assertTrue(res.isValid())
self.assertEqual(res.spec(), QColor.Rgb)
self.assertAlmostEqual(res.redF(), 1 / 65536, 5)
self.assertAlmostEqual(res.greenF(), 2 / 65536, 5)
self.assertAlmostEqual(res.blueF(), 3 / 65536, 5)
self.assertAlmostEqual(res.alphaF(), 4 / 65536, 5)
color = QColor.fromRgb(16, 17, 18, 20)
string = QgsColorUtils.colorToString(color)
res = QgsColorUtils.colorFromString(string)
self.assertTrue(res.isValid())
self.assertEqual(res.spec(), QColor.Rgb)
self.assertEqual(res.red(), 16)
self.assertEqual(res.green(), 17)
self.assertEqual(res.blue(), 18)
self.assertEqual(res.alpha(), 20)
# rgb extended color
color = QColor.fromRgbF(-1 / 65536, 2 / 65536, 3 / 65536, 4 / 65536)
string = QgsColorUtils.colorToString(color)
res = QgsColorUtils.colorFromString(string)
self.assertTrue(res.isValid())
self.assertEqual(res.spec(), QColor.ExtendedRgb)
self.assertAlmostEqual(res.redF(), -1 / 65536, 5)
self.assertAlmostEqual(res.greenF(), 2 / 65536, 5)
self.assertAlmostEqual(res.blueF(), 3 / 65536, 5)
self.assertAlmostEqual(res.alphaF(), 4 / 65536, 5)
# hsv color
color = QColor.fromHsvF(1 / 65536, 2 / 65536, 3 / 65536, 4 / 65536)
string = QgsColorUtils.colorToString(color)
res = QgsColorUtils.colorFromString(string)
self.assertTrue(res.isValid())
self.assertEqual(res.spec(), QColor.Hsv)
self.assertAlmostEqual(res.hueF(), 1 / 65536, 4)
self.assertAlmostEqual(res.hsvSaturationF(), 2 / 65536, 5)
self.assertAlmostEqual(res.valueF(), 3 / 65536, 5)
self.assertAlmostEqual(res.alphaF(), 4 / 65536, 5)
# hsl color
color = QColor.fromHslF(111 / 65536, 12222 / 65536, 333 / 65536, 4 / 65536)
string = QgsColorUtils.colorToString(color)
res = QgsColorUtils.colorFromString(string)
self.assertTrue(res.isValid())
self.assertEqual(res.spec(), QColor.Hsl)
self.assertAlmostEqual(res.hslHueF(), 111 / 65536, 5)
self.assertAlmostEqual(res.hslSaturationF(), 12222 / 65536, 5)
self.assertAlmostEqual(res.lightnessF(), 333 / 65536, 4)
self.assertAlmostEqual(res.alphaF(), 4 / 65536, 5)
# cmyk color
color = QColor.fromCmykF(1 / 65536, 2 / 65536, 3 / 65536, 4 / 65536, 255 / 65536)
string = QgsColorUtils.colorToString(color)
res = QgsColorUtils.colorFromString(string)
self.assertTrue(res.isValid())
self.assertEqual(res.spec(), QColor.Cmyk)
self.assertAlmostEqual(res.cyanF(), 1 / 65536, 4)
self.assertAlmostEqual(res.magentaF(), 2 / 65536, 4)
self.assertAlmostEqual(res.yellowF(), 3 / 65536, 4)
self.assertAlmostEqual(res.blackF(), 4 / 65536, 4)
self.assertAlmostEqual(res.alphaF(), 255 / 65536, 5)
# invalid string
res = QgsColorUtils.colorFromString('')
self.assertFalse(res.isValid())
res = QgsColorUtils.colorFromString('x')
self.assertFalse(res.isValid())
res = QgsColorUtils.colorFromString('2')
self.assertFalse(res.isValid())
if __name__ == '__main__':
unittest.main()