feat(CMYK): add API color model and profile methods to project

API allows to define a color model without a color space. If both are
set, consistency between defined color model and color space one is
checked (only in Qt version 6.8.0 or greater because it's not possible
to retrieve color model from color space before that)
This commit is contained in:
Julien Cabieces 2024-05-28 16:25:17 +02:00 committed by Nyall Dawson
parent 7f699bcfc3
commit 7e527d182b
15 changed files with 464 additions and 3 deletions

View File

@ -5144,3 +5144,9 @@ Qgis.SensorThingsEntity.MultiDatastream.__doc__ = "A MultiDatastream groups a co
Qgis.SensorThingsEntity.__doc__ = "OGC SensorThings API entity types.\n\n.. versionadded:: 3.36\n\n" + '* ``Invalid``: ' + Qgis.SensorThingsEntity.Invalid.__doc__ + '\n' + '* ``Thing``: ' + Qgis.SensorThingsEntity.Thing.__doc__ + '\n' + '* ``Location``: ' + Qgis.SensorThingsEntity.Location.__doc__ + '\n' + '* ``HistoricalLocation``: ' + Qgis.SensorThingsEntity.HistoricalLocation.__doc__ + '\n' + '* ``Datastream``: ' + Qgis.SensorThingsEntity.Datastream.__doc__ + '\n' + '* ``Sensor``: ' + Qgis.SensorThingsEntity.Sensor.__doc__ + '\n' + '* ``ObservedProperty``: ' + Qgis.SensorThingsEntity.ObservedProperty.__doc__ + '\n' + '* ``Observation``: ' + Qgis.SensorThingsEntity.Observation.__doc__ + '\n' + '* ``FeatureOfInterest``: ' + Qgis.SensorThingsEntity.FeatureOfInterest.__doc__ + '\n' + '* ``MultiDatastream``: ' + Qgis.SensorThingsEntity.MultiDatastream.__doc__
# --
Qgis.SensorThingsEntity.baseClass = Qgis
# monkey patching scoped based enum
Qgis.ColorModel.Rgb.__doc__ = ""
Qgis.ColorModel.Cmyk.__doc__ = ""
Qgis.ColorModel.__doc__ = "Color model types\n\n.. versionadded:: 3.40\n\n" + '* ``Rgb``: ' + Qgis.ColorModel.Rgb.__doc__ + '\n' + '* ``Cmyk``: ' + Qgis.ColorModel.Cmyk.__doc__
# --
Qgis.ColorModel.baseClass = Qgis

View File

@ -139,6 +139,71 @@ Sets the style database to use for the project style.
Returns the style database to use for project specific styles.
.. seealso:: :py:func:`setProjectStyle`
%End
void setColorModel( Qgis::ColorModel colorModel );
%Docstring
Set project ``colorModel``
It would serve as default color model when selecting a color in the whole application.
Any color defined in a different color model than the one specified here will be converted to
this color model when exporting a layout.
If a color space has already been set and its color model differs from ``colorModel``, project
color space is set to invalid one. :py:func:`setColorSpace` colorSpace()
defaults to :py:class:`Qgis`.ColorModel.Rgb
.. seealso:: :py:func:`colorModel`
.. versionadded:: 3.40
%End
Qgis::ColorModel colorModel() const;
%Docstring
Returns project color model
Used as default color model when selecting a color in the whole application.
Any color defined in a different color model than the returned will be converted to
this color model when exporting a layout
defaults to :py:class:`Qgis`.ColorModel.Rgb
.. seealso:: :py:func:`setColorModel`
.. versionadded:: 3.40
%End
void setColorSpace( const QColorSpace &colorSpace );
%Docstring
Set project current ``colorSpace``. ``colorSpace`` must be a valid RGB or CMYK color space.
Color space ICC profile will be added as a project attached file.
Project color space will be added to PDF layout export if defined (meaning different from
the default invalid QColorSpace).
If a color model has already been set and it differs from ``colorSpace`` color model, project
color model is set to ``colorSpace`` one. :py:func:`setColorModel` colorModel()
defaults to invalid color space
.. seealso:: :py:func:`colorSpace`
.. versionadded:: 3.40
%End
QColorSpace colorSpace() const;
%Docstring
Returns current project color space.
Project color space will be added to PDF layout export if defined (meaning different from
the default invalid QColorSpace).
defaults to invalid color space
.. seealso:: :py:func:`setColorSpace`
.. versionadded:: 3.40
%End
bool readXml( const QDomElement &element, const QgsReadWriteContext &context, Qgis::ProjectReadFlags flags = Qgis::ProjectReadFlags() );

View File

@ -2886,6 +2886,12 @@ The development version
MultiDatastream,
};
enum class ColorModel /BaseType=IntEnum/
{
Rgb,
Cmyk
};
static const double DEFAULT_SEARCH_RADIUS_MM;
static const float DEFAULT_MAPTOPIXEL_THRESHOLD;

View File

@ -81,6 +81,16 @@ An invalid color will be returned if the color could not be read.
.. seealso:: :py:func:`colorToString`
%End
static QColorSpace iccProfile( const QString &iccProfileFilePath, QString &errorMsg );
%Docstring
Load ``iccProfileFilePath`` and returns associated color space.
If an error occurred, an invalid color space is returned and ``errorMsg`` is updated with error
message
%End
};
/************************************************************************

View File

@ -5062,6 +5062,12 @@ Qgis.SensorThingsEntity.MultiDatastream.__doc__ = "A MultiDatastream groups a co
Qgis.SensorThingsEntity.__doc__ = "OGC SensorThings API entity types.\n\n.. versionadded:: 3.36\n\n" + '* ``Invalid``: ' + Qgis.SensorThingsEntity.Invalid.__doc__ + '\n' + '* ``Thing``: ' + Qgis.SensorThingsEntity.Thing.__doc__ + '\n' + '* ``Location``: ' + Qgis.SensorThingsEntity.Location.__doc__ + '\n' + '* ``HistoricalLocation``: ' + Qgis.SensorThingsEntity.HistoricalLocation.__doc__ + '\n' + '* ``Datastream``: ' + Qgis.SensorThingsEntity.Datastream.__doc__ + '\n' + '* ``Sensor``: ' + Qgis.SensorThingsEntity.Sensor.__doc__ + '\n' + '* ``ObservedProperty``: ' + Qgis.SensorThingsEntity.ObservedProperty.__doc__ + '\n' + '* ``Observation``: ' + Qgis.SensorThingsEntity.Observation.__doc__ + '\n' + '* ``FeatureOfInterest``: ' + Qgis.SensorThingsEntity.FeatureOfInterest.__doc__ + '\n' + '* ``MultiDatastream``: ' + Qgis.SensorThingsEntity.MultiDatastream.__doc__
# --
Qgis.SensorThingsEntity.baseClass = Qgis
# monkey patching scoped based enum
Qgis.ColorModel.Rgb.__doc__ = ""
Qgis.ColorModel.Cmyk.__doc__ = ""
Qgis.ColorModel.__doc__ = "Color model types\n\n.. versionadded:: 3.40\n\n" + '* ``Rgb``: ' + Qgis.ColorModel.Rgb.__doc__ + '\n' + '* ``Cmyk``: ' + Qgis.ColorModel.Cmyk.__doc__
# --
Qgis.ColorModel.baseClass = Qgis
from enum import Enum

View File

@ -139,6 +139,71 @@ Sets the style database to use for the project style.
Returns the style database to use for project specific styles.
.. seealso:: :py:func:`setProjectStyle`
%End
void setColorModel( Qgis::ColorModel colorModel );
%Docstring
Set project ``colorModel``
It would serve as default color model when selecting a color in the whole application.
Any color defined in a different color model than the one specified here will be converted to
this color model when exporting a layout.
If a color space has already been set and its color model differs from ``colorModel``, project
color space is set to invalid one. :py:func:`setColorSpace` colorSpace()
defaults to :py:class:`Qgis`.ColorModel.Rgb
.. seealso:: :py:func:`colorModel`
.. versionadded:: 3.40
%End
Qgis::ColorModel colorModel() const;
%Docstring
Returns project color model
Used as default color model when selecting a color in the whole application.
Any color defined in a different color model than the returned will be converted to
this color model when exporting a layout
defaults to :py:class:`Qgis`.ColorModel.Rgb
.. seealso:: :py:func:`setColorModel`
.. versionadded:: 3.40
%End
void setColorSpace( const QColorSpace &colorSpace );
%Docstring
Set project current ``colorSpace``. ``colorSpace`` must be a valid RGB or CMYK color space.
Color space ICC profile will be added as a project attached file.
Project color space will be added to PDF layout export if defined (meaning different from
the default invalid QColorSpace).
If a color model has already been set and it differs from ``colorSpace`` color model, project
color model is set to ``colorSpace`` one. :py:func:`setColorModel` colorModel()
defaults to invalid color space
.. seealso:: :py:func:`colorSpace`
.. versionadded:: 3.40
%End
QColorSpace colorSpace() const;
%Docstring
Returns current project color space.
Project color space will be added to PDF layout export if defined (meaning different from
the default invalid QColorSpace).
defaults to invalid color space
.. seealso:: :py:func:`setColorSpace`
.. versionadded:: 3.40
%End
bool readXml( const QDomElement &element, const QgsReadWriteContext &context, Qgis::ProjectReadFlags flags = Qgis::ProjectReadFlags() );

View File

@ -2886,6 +2886,12 @@ The development version
MultiDatastream,
};
enum class ColorModel
{
Rgb,
Cmyk
};
static const double DEFAULT_SEARCH_RADIUS_MM;
static const float DEFAULT_MAPTOPIXEL_THRESHOLD;

View File

@ -81,6 +81,16 @@ An invalid color will be returned if the color could not be read.
.. seealso:: :py:func:`colorToString`
%End
static QColorSpace iccProfile( const QString &iccProfileFilePath, QString &errorMsg );
%Docstring
Load ``iccProfileFilePath`` and returns associated color space.
If an error occurred, an invalid color space is returned and ``errorMsg`` is updated with error
message
%End
};
/************************************************************************

View File

@ -13,6 +13,7 @@
* *
***************************************************************************/
#include "qgscolorutils.h"
#include "qgsprojectstylesettings.h"
#include "qgis.h"
#include "qgsproject.h"
@ -25,6 +26,7 @@
#include "qgstextformat.h"
#include "qgsstyle.h"
#include "qgscombinedstylemodel.h"
#include "qgsxmlutils.h"
#include <QDomElement>
@ -170,6 +172,7 @@ bool QgsProjectStyleSettings::readXml( const QDomElement &element, const QgsRead
{
mRandomizeDefaultSymbolColor = element.attribute( QStringLiteral( "RandomizeDefaultSymbolColor" ), QStringLiteral( "0" ) ).toInt();
mDefaultSymbolOpacity = element.attribute( QStringLiteral( "DefaultSymbolOpacity" ), QStringLiteral( "1.0" ) ).toDouble();
mColorModel = QgsXmlUtils::readFlagAttribute( element, QStringLiteral( "colorModel" ), Qgis::ColorModel::Rgb );
QDomElement elem = element.firstChildElement( QStringLiteral( "markerSymbol" ) );
if ( !elem.isNull() )
@ -259,6 +262,15 @@ bool QgsProjectStyleSettings::readXml( const QDomElement &element, const QgsRead
}
}
const QString iccProfileId = element.attribute( QStringLiteral( "iccProfileId" ) );
mIccProfileFilePath = mProject ? mProject->resolveAttachmentIdentifier( iccProfileId ) : QString();
if ( !mIccProfileFilePath.isEmpty() )
{
QString errorMsg;
QColorSpace colorSpace = QgsColorUtils::iccProfile( mIccProfileFilePath, errorMsg );
setColorSpace( colorSpace );
}
emit styleDatabasesChanged();
return true;
@ -271,6 +283,10 @@ QDomElement QgsProjectStyleSettings::writeXml( QDomDocument &doc, const QgsReadW
element.setAttribute( QStringLiteral( "RandomizeDefaultSymbolColor" ), mRandomizeDefaultSymbolColor ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
element.setAttribute( QStringLiteral( "DefaultSymbolOpacity" ), QString::number( mDefaultSymbolOpacity ) );
const QMetaEnum metaEnum = QMetaEnum::fromType<Qgis::ColorModel>();
const QString colorModel( metaEnum.valueToKeys( static_cast<int>( mColorModel ) ) );
element.setAttribute( QStringLiteral( "colorModel" ), colorModel );
if ( mDefaultMarkerSymbol )
{
QDomElement markerSymbolElem = doc.createElement( QStringLiteral( "markerSymbol" ) );
@ -320,6 +336,8 @@ QDomElement QgsProjectStyleSettings::writeXml( QDomDocument &doc, const QgsReadW
element.setAttribute( QStringLiteral( "projectStyleId" ), mProject->attachmentIdentifier( mProjectStyle->fileName() ) );
}
element.setAttribute( QStringLiteral( "iccProfileId" ), mProject->attachmentIdentifier( mIccProfileFilePath ) );
return element;
}
@ -440,7 +458,66 @@ QgsCombinedStyleModel *QgsProjectStyleSettings::combinedStyleModel()
return mCombinedStyleModel;
}
void QgsProjectStyleSettings::setColorModel( Qgis::ColorModel colorModel )
{
mColorModel = colorModel;
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
if ( mColorSpace.isValid() && QgsColorUtils::toColorModel( mColorSpace.colorModel() ) != colorModel )
{
setColorSpace( QColorSpace() );
}
#endif
}
Qgis::ColorModel QgsProjectStyleSettings::colorModel() const
{
return mColorModel;
}
void QgsProjectStyleSettings::setColorSpace( const QColorSpace &colorSpace )
{
if ( !mProject )
{
QgsDebugError( "Impossible to attach ICC profile, no project defined" );
return;
}
auto clearIccProfile = [this]()
{
mProject->removeAttachedFile( mIccProfileFilePath );
mIccProfileFilePath.clear();
mColorSpace = QColorSpace();
};
if ( !mIccProfileFilePath.isEmpty() )
clearIccProfile();
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
bool ok;
Qgis::ColorModel colorModel = QgsColorUtils::toColorModel( colorSpace.colorModel(), &ok );
mColorSpace = ok ? colorSpace : QColorSpace();
#else
mColorSpace = colorSpace;
#endif
if ( !mColorSpace.isValid() )
return;
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
if ( colorModel != mColorModel )
mColorModel = colorModel;
#endif
mIccProfileFilePath = mProject->createAttachedFile( QStringLiteral( "profile.icc" ) );
QFile file( mIccProfileFilePath );
if ( !file.open( QIODevice::WriteOnly ) || file.write( colorSpace.iccProfile() ) < 0 )
clearIccProfile();
}
QColorSpace QgsProjectStyleSettings::colorSpace() const
{
return mColorSpace;
}
//

View File

@ -22,6 +22,7 @@
#include <memory.h>
#include <QAbstractListModel>
#include <QColorSpace>
#include <QSortFilterProxyModel>
#include <QPointer>
@ -144,6 +145,67 @@ class CORE_EXPORT QgsProjectStyleSettings : public QObject
*/
QgsStyle *projectStyle();
/**
* Set project \a colorModel
*
* It would serve as default color model when selecting a color in the whole application.
* Any color defined in a different color model than the one specified here will be converted to
* this color model when exporting a layout.
*
* If a color space has already been set and its color model differs from \a colorModel, project
* color space is set to invalid one. \see setColorSpace() colorSpace()
*
* defaults to Qgis::ColorModel::Rgb
*
* \see colorModel()
* \since QGIS 3.40
*/
void setColorModel( Qgis::ColorModel colorModel );
/**
* Returns project color model
*
* Used as default color model when selecting a color in the whole application.
* Any color defined in a different color model than the returned will be converted to
* this color model when exporting a layout
*
* defaults to Qgis::ColorModel::Rgb
*
* \see setColorModel()
* \since QGIS 3.40
*/
Qgis::ColorModel colorModel() const;
/**
* Set project current \a colorSpace. \a colorSpace must be a valid RGB or CMYK color space.
* Color space ICC profile will be added as a project attached file.
*
* Project color space will be added to PDF layout export if defined (meaning different from
* the default invalid QColorSpace).
*
* If a color model has already been set and it differs from \a colorSpace color model, project
* color model is set to \a colorSpace one. \see setColorModel() colorModel()
*
* defaults to invalid color space
*
* \see colorSpace()
* \since QGIS 3.40
*/
void setColorSpace( const QColorSpace &colorSpace );
/**
* Returns current project color space.
*
* Project color space will be added to PDF layout export if defined (meaning different from
* the default invalid QColorSpace).
*
* defaults to invalid color space
*
* \see setColorSpace()
* \since QGIS 3.40
*/
QColorSpace colorSpace() const;
/**
* Reads the settings's state from a DOM element.
* \see writeXml()
@ -283,10 +345,14 @@ class CORE_EXPORT QgsProjectStyleSettings : public QObject
QList< QPointer< QgsStyle > > mStyles;
QgsCombinedStyleModel *mCombinedStyleModel = nullptr;
Qgis::ColorModel mColorModel = Qgis::ColorModel::Rgb;
QColorSpace mColorSpace;
QString mIccProfileFilePath;
void loadStyleAtPath( const QString &path );
void clearStyles();
friend class TestQgsProjectProperties;
};
/**

View File

@ -5130,6 +5130,18 @@ class CORE_EXPORT Qgis
};
Q_ENUM( SensorThingsEntity )
/**
* Color model types
*
* \since QGIS 3.40
*/
enum class ColorModel : int
{
Rgb,
Cmyk
};
Q_ENUM( ColorModel )
/**
* Identify search radius in mm
*/

View File

@ -18,11 +18,12 @@
#include "qgscolorutils.h"
#include <QColor>
#include <QColorSpace>
#include <QDomDocument>
#include <QFile>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
void QgsColorUtils::writeXml( const QColor &color, const QString &identifier, QDomDocument &document, QDomElement &element, const QgsReadWriteContext & )
{
{
@ -353,3 +354,57 @@ QColor QgsColorUtils::colorFromString( const QString &string )
}
return QColor();
}
QColorSpace QgsColorUtils::iccProfile( const QString &iccProfileFilePath, QString &errorMsg )
{
if ( iccProfileFilePath.isEmpty() )
return QColorSpace();
QFile file( iccProfileFilePath );
if ( !file.open( QIODevice::ReadOnly ) )
{
errorMsg = QObject::tr( "Failed to open ICC Profile: %1" ).arg( iccProfileFilePath );
return QColorSpace();
}
QColorSpace colorSpace = QColorSpace::fromIccProfile( file.readAll() );
if ( !colorSpace.isValid() )
{
errorMsg = QObject::tr( "Invalid ICC Profile: %1" ).arg( iccProfileFilePath );
return colorSpace;
}
return colorSpace;
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
Qgis::ColorModel QgsColorUtils::toColorModel( QColorSpace::ColorModel colorModel, bool *ok )
{
bool lok;
Qgis::ColorModel res;
switch ( colorModel )
{
case QColorSpace::ColorModel::Cmyk:
lok = true;
res = Qgis::ColorModel::Cmyk;
break;
case QColorSpace::ColorModel::Rgb:
lok = true;
res = Qgis::ColorModel::Rgb;
break;
case QColorSpace::ColorModel::Undefined:
case QColorSpace::ColorModel::Gray: // not supported
lok = false;
res = Qgis::ColorModel::Rgb;
}
if ( ok )
*ok = lok;
return res;
}
#endif

View File

@ -24,6 +24,7 @@
#include <QDomDocument>
#include <QDomElement>
#include <QColorSpace>
class QgsReadWriteContext;
@ -94,6 +95,23 @@ class CORE_EXPORT QgsColorUtils
*/
static QColor colorFromString( const QString &string );
/**
* Load \a iccProfileFilePath and returns associated color space.
* If an error occurred, an invalid color space is returned and \a errorMsg is updated with error
* message
*/
static QColorSpace iccProfile( const QString &iccProfileFilePath, QString &errorMsg );
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
/**
* Convert and returns Qt \a colorModel to Qgis::ColorModel. \a ok is set to true if \a colorModel
* is a valid Qgis::ColorModel.
*/
static Qgis::ColorModel toColorModel( QColorSpace::ColorModel colorModel, bool *ok = nullptr ) SIP_SKIP;
#endif
};
#endif // QGSCOLORUTILS_H

View File

@ -11,12 +11,14 @@ __copyright__ = 'Copyright 2019, The QGIS Project'
from qgis.PyQt.QtCore import (
QCoreApplication,
QDir,
QEvent,
QModelIndex,
Qt,
QTemporaryDir,
QTemporaryFile
)
from qgis.PyQt.QtGui import QColor, QFont
from qgis.PyQt.QtGui import QColor, QColorSpace, QFont
from qgis.PyQt.QtTest import QSignalSpy
from qgis.PyQt.QtXml import QDomDocument
from qgis.core import (
@ -32,7 +34,9 @@ from qgis.core import (
QgsTextFormat,
QgsWkbTypes,
)
import unittest
import os
from qgis.testing import start_app, QgisTestCase
from utilities import unitTestDataPath
@ -409,7 +413,8 @@ class TestQgsProjectViewSettings(QgisTestCase):
QgsStyle.defaultStyle())
def testReadWrite(self):
p = QgsProjectStyleSettings()
project = QgsProject()
p = QgsProjectStyleSettings(project)
line = QgsSymbol.defaultSymbol(QgsWkbTypes.GeometryType.LineGeometry)
p.setDefaultSymbol(Qgis.SymbolType.Line, line)
@ -443,6 +448,60 @@ class TestQgsProjectViewSettings(QgisTestCase):
self.assertEqual(p2.styleDatabasePaths(),
[unitTestDataPath() + '/style1.db', unitTestDataPath() + '/style2.db'])
def testColorSettings(self):
"""
Test ICC profile attachment
"""
project = QgsProject()
settings = project.styleSettings()
self.assertEqual(settings.colorModel(), Qgis.ColorModel.Rgb)
self.assertFalse(settings.colorSpace().isValid())
# set Cmyk color model and color space
settings.setColorModel(Qgis.ColorModel.Cmyk)
self.assertEqual(settings.colorModel(), Qgis.ColorModel.Cmyk)
with open(os.path.join(TEST_DATA_DIR, "sRGB2014.icc"), mode='rb') as f:
colorSpace = QColorSpace.fromIccProfile(f.read())
self.assertTrue(colorSpace.isValid())
settings.setColorSpace(colorSpace)
self.assertTrue(settings.colorSpace().isValid())
self.assertEqual(settings.colorSpace().primaries(), QColorSpace.Primaries.SRgb)
self.assertEqual(len(project.attachedFiles()), 2)
# save and restore
projectFile = QTemporaryFile(QDir.temp().absoluteFilePath("testCmykSettings.qgz"))
projectFile.open()
self.assertTrue(project.write(projectFile.fileName()))
project = QgsProject()
self.assertTrue(project.read(projectFile.fileName()))
settings = project.styleSettings()
self.assertEqual(settings.colorModel(), Qgis.ColorModel.Cmyk)
self.assertTrue(settings.colorSpace().isValid())
self.assertEqual(settings.colorSpace().primaries(), QColorSpace.Primaries.SRgb)
self.assertEqual(len(project.attachedFiles()), 2)
# clear color space
settings.setColorSpace(QColorSpace())
self.assertFalse(settings.colorSpace().isValid())
self.assertEqual(len(project.attachedFiles()), 1)
# save and restore cleared
projectFile = QTemporaryFile(QDir.temp().absoluteFilePath("testCmykSettingsCleared.qgz"))
projectFile.open()
self.assertTrue(project.write(projectFile.fileName()))
project = QgsProject()
self.assertTrue(project.read(projectFile.fileName()))
settings = project.styleSettings()
self.assertFalse(settings.colorSpace().isValid())
self.assertEqual(len(project.attachedFiles()), 1)
if __name__ == '__main__':
unittest.main()

BIN
tests/testdata/sRGB2014.icc vendored Normal file

Binary file not shown.