Open Grum999 opened 2 years ago
Need to load lcms
library provided with Krita with ctypes
From test script below need to implement a module and class to manage color management properly and easily
Note: color profile extraction from image file is not managed with color management module (use color profile from
BCFile
)
# https://littlecms.com/blog/2020/12/09/using-lcms2-on-qt/
# https://raw.githubusercontent.com/mm2/Little-CMS/master/doc/LittleCMS2.13%20tutorial.pdf
import sys
import os.path
import ctypes
from PyQt5.Qt import *
# -- global variables
# library has been loaded
LCMS_LIBRARY_LOADED = False
# library location on file system
LCMS_LIBRARY_PATH = ''
# current Krita application path
__AppPath=QDir(QApplication.applicationDirPath())
__AppPath.cdUp()
# according to platform, location of library might not be the same
if sys.platform == 'linux':
checkedPathsList = [
os.path.join(__AppPath.path(), "lib", "liblcms2.so.2"), # appimage
'/usr/lib/x86_64-linux-gnu/liblcms2.so.2', # debian multiarch
'/usr/lib64/liblcms2.so.2', # other distro
'/usr/lib/liblcms2.so.2' # other distro
]
elif sys.platform == 'win32':
checkedPathsList = [
os.path.join(__AppPath.path(), "liblcms2.dll"), # provided with krita
]
else:
# other platform not supported
checkedPathsList=[]
# check paths
for checkedPath in checkedPathsList:
if os.path.exists(checkedPath):
LCMS_LIBRARY_PATH = checkedPath
break
# library has been found, try to load
if LCMS_LIBRARY_PATH != '':
try:
LCMS_LIBRARY = ctypes.CDLL(LCMS_LIBRARY_PATH)
LCMS_LIBRARY_LOADED = True
except e as exception:
# unable to load library??
# print exception
print('LCMS not loaded:', e)
class LcmsType:
STRING = ctypes.c_char_p
INT = ctypes.c_int
DWORD = ctypes.c_ulong
LPVOID = ctypes.c_void_p
CMSHANDLE = ctypes.c_void_p
HPROFILE = ctypes.c_void_p
HTRANSFORM = ctypes.c_void_p
BUFFER = ctypes.c_void_p
class LcmsPixelType:
"""Pixels types
==> not all Lcms pixels types are defined here
"""
# -- format types --
# From https://github.com/mm2/Little-CMS/blob/master/include/lcms2.h
# PT_RGB=4
# PT_GRAY=3
#
# COLORSPACE_SH(s) ((s) << 16)
# SWAPFIRST_SH(s) ((s) << 14)
# DOSWAP_SH(e) ((e) << 10)
# EXTRA_SH(e) ((e) << 7)
# CHANNELS_SH(c) ((c) << 3)
# BYTES_SH(b) (b)
#define TYPE_RGB_8 (COLORSPACE_SH(PT_RGB)|CHANNELS_SH(3)|BYTES_SH(1))
RGB_8 = 262169
#define TYPE_RGBA_8 (COLORSPACE_SH(PT_RGB)|EXTRA_SH(1)|CHANNELS_SH(3)|BYTES_SH(1))
RGBA_8 = 262297
#define TYPE_RGBA_16 (COLORSPACE_SH(PT_RGB)|EXTRA_SH(1)|CHANNELS_SH(3)|BYTES_SH(2))
RGBA_16 = 262298
#define TYPE_BGRA_8 (COLORSPACE_SH(PT_RGB)|EXTRA_SH(1)|CHANNELS_SH(3)|BYTES_SH(1)|DOSWAP_SH(1)|SWAPFIRST_SH(1))
BGRA_8 = 279705
#define TYPE_BGR_8 (COLORSPACE_SH(PT_RGB)|CHANNELS_SH(3)|BYTES_SH(1)|DOSWAP_SH(1))
BGR_8 = 263321
#define TYPE_GRAY_8 (COLORSPACE_SH(PT_GRAY)|CHANNELS_SH(1)|BYTES_SH(1))
GRAY_8 = 196617
#define TYPE_GRAY_16 (COLORSPACE_SH(PT_GRAY)|CHANNELS_SH(1)|BYTES_SH(2))
GRAY_16 = 196618
@staticmethod
def qtToLcmsFormat(qtImageFmt):
"""Convert a QImage.Format pixel format to Lcms pixels format
If format is not managed, return 0
"""
if qtImageFmt in (QImage.Format_ARGB32, QImage.Format_RGB32):
return LcmsPixelType.BGRA_8
elif qtImageFmt == QImage.Format_RGB888:
return LcmsPixelType.RGB_8
elif qtImageFmt in (QImage.Format_RGBX8888, QImage.Format_RGBA8888):
return LcmsPixelType.RGBA_8
elif qtImageFmt == QImage.Format_Grayscale8:
return LcmsPixelType.GRAY_8
elif qtImageFmt == QImage.Format_Grayscale16:
return LcmsPixelType.GRAY_16
elif qtImageFmt in (QImage.Format_RGBA64, QImage.Format_RGBX64):
return LcmsPixelType.RGBA_16
elif qtImageFmt == QImage.Format_BGR888:
return LcmsPixelType.BGR_8
else:
return 0
class LcmsIntent:
"""Intent values
Description from Lcms documentation
https://raw.githubusercontent.com/mm2/Little-CMS/master/doc/LittleCMS2.13%20tutorial.pdf
Page 12
"""
# -- intent values --
# -- INTENT_PERCEPTUAL --
# Hue hopefully maintained (but not required), lightness and saturation sacrificed to maintain the perceived color.
# White point changed to result in neutral grays.
# Intended for images.
PERCEPTUAL = 0
# -- INTENT_RELATIVE_COLORIMETRIC --
# Within and outside gamut; same as Absolute Colorimetric.
# White point changed to result in neutral grays
RELATIVE_COLORIMETRIC = 1
# -- INTENT_SATURATION --
# Hue and saturation maintained with lightnesssacrificed to maintain saturation. White point changed to result in neutral grays.
# Intended for business graphics (make it colorful charts, graphs, overheads, ...)
SATURATION = 2
# -- INTENT_ABSOLUTE_COLORIMETRIC --
# Within the destination device gamut; hue, lightness and saturation are maintained.
# Outside the gamut; hue and lightness are maintained, saturation is sacrificed.
# White point for source and destination; unchanged. Intended for spot colors (Pantone, TruMatch, logo colors, ...)
ABSOLUTE_COLORIMETRIC = 3
class LcmsFlags:
"""Lcms transform flags"""
FLAGS_NOCACHE = 0x0040 # Inhibit 1-pixel cache
FLAGS_NOOPTIMIZE = 0x0100 # Inhibit optimizations
FLAGS_NULLTRANSFORM = 0x0200 # Don't transform anyway
# - Proofing flags
FLAGS_GAMUTCHECK = 0x1000 # Out of Gamut alarm
FLAGS_SOFTPROOFING = 0x4000 # Do softproofing
# - Misc
FLAGS_BLACKPOINTCOMPENSATION = 0x2000 #
FLAGS_NOWHITEONWHITEFIXUP = 0x0004 # Don't fix scum dot
FLAGS_HIGHRESPRECALC = 0x0400 # Use more memory to give better accuracy
FLAGS_LOWRESPRECALC = 0x0800 # Use less memory to minimize resources
# - For devicelink creation
cmsFLAGS_8BITS_DEVICELINK = 0x0008 # Create 8 bits devicelinks
cmsFLAGS_GUESSDEVICECLASS = 0x0020 # Guess device class (for transform2devicelink)
cmsFLAGS_KEEP_SEQUENCE = 0x0080 # Keep profile sequence for devicelink creation
# - Specific to a particular optimizations
FLAGS_FORCE_CLUT = 0x0002 # Force CLUT optimization
FLAGS_CLUT_POST_LINEARIZATION = 0x0001 # create postlinearization tables if possible
FLAGS_CLUT_PRE_LINEARIZATION = 0x0010 # create prelinearization tables if possible
# - Specific to unbounded mode
FLAGS_NONEGATIVES = 0x8000 # Prevent negative numbers in floating point transforms
# - Copy alpha channels when transforming
FLAGS_COPY_ALPHA = 0x04000000 # Alpha channels are copied on cmsDoTransform()
if LCMS_LIBRARY_LOADED:
# declare methods from library
# --
_liblcms_cmsCreate_sRGBProfile = LCMS_LIBRARY['cmsCreate_sRGBProfile']
_liblcms_cmsCreate_sRGBProfile.restype = LcmsType.HPROFILE
# --
_liblcms_cmsOpenProfileFromFile = LCMS_LIBRARY['cmsOpenProfileFromFile']
_liblcms_cmsOpenProfileFromFile.restype = LcmsType.HPROFILE
_liblcms_cmsOpenProfileFromFile.argtypes = [LcmsType.STRING, # file name
LcmsType.STRING # r/w mode
]
# --
_liblcms_cmsOpenProfileFromMem = LCMS_LIBRARY['cmsOpenProfileFromMem']
_liblcms_cmsOpenProfileFromMem.restype = LcmsType.HPROFILE
_liblcms_cmsOpenProfileFromMem.argtypes = [LcmsType.STRING, # data
LcmsType.DWORD # length of data
]
_liblcms_cmsCreateTransform = LCMS_LIBRARY['cmsCreateTransform']
_liblcms_cmsCreateTransform.restype = LcmsType.HTRANSFORM
_liblcms_cmsCreateTransform.argtypes = [LcmsType.HPROFILE, # input profile
LcmsType.INT, # input data format
LcmsType.HPROFILE, # output profile
LcmsType.INT, # output data format
LcmsType.INT, # intent
LcmsType.DWORD # flags
]
_liblcms_cmsDeleteTransform = LCMS_LIBRARY['cmsDeleteTransform']
_liblcms_cmsDeleteTransform.argtypes = [LcmsType.HTRANSFORM]
_liblcms_cmsCloseProfile = LCMS_LIBRARY['cmsCloseProfile']
_liblcms_cmsCloseProfile.argtypes = [LcmsType.HPROFILE]
_liblcms_cmsDoTransform = LCMS_LIBRARY['cmsDoTransform']
_liblcms_cmsDoTransform.argtypes = [LcmsType.HTRANSFORM,
LcmsType.BUFFER, # input buffer
LcmsType.BUFFER, # output buffer
LcmsType.DWORD # buffer size
]
_liblcms_cmsDoTransformLineStride = LCMS_LIBRARY['cmsDoTransformLineStride']
_liblcms_cmsDoTransformLineStride.argtypes = [LcmsType.HTRANSFORM,
LcmsType.BUFFER, # input buffer
LcmsType.BUFFER, # output buffer
LcmsType.DWORD, # pixels per line
LcmsType.DWORD, # line count
LcmsType.DWORD, # bytes per line (input)
LcmsType.DWORD, # bytes per line (output)
LcmsType.DWORD, # bytes per plane (input)
LcmsType.DWORD # bytes per plane (output)
]
# cmsUInt32Number cmsGetProfileInfo(HPROFILE hProfile, cmsInfoType Info, const char LanguageCode[3], const char CountryCode[3], wchar_t* Buffer, cmsUInt32Number BufferSize)
class Lcms:
"""A class to bind some LCMS function"""
STRING = ctypes.c_char_p
INT = ctypes.c_int
DWORD = ctypes.c_ulong
LPVOID = ctypes.c_void_p
CMSHANDLE = ctypes.c_void_p
HPROFILE = ctypes.c_void_p
HTRANSFORM= ctypes.c_void_p
def __init__(self):
"""Initialise library"""
self.__initialized = False
if not LCMS_LIBRARY_LOADED:
# class can be initialized only if library has been loaded
return
self.__initialized = True
def isInitialized(self):
"""Return true if class has been initialized"""
return self.__initialized
def createProfileSRGB(self):
"""Return a default sRGB profile"""
if not self.__initialized:
return None
return _liblcms_cmsCreate_sRGBProfile()
def loadProfileFromIccFile(self, fileName):
"""Load and return an ICC profile
If file name is not found or not a valid ICC profile return None
"""
if not self.__initialized:
return None
return _liblcms_cmsOpenProfileFromFile(ctypes.create_string_buffer(fileName.encode()), ctypes.create_string_buffer(b'r'))
def loadProfileFromIccMem(self, iccData):
"""Load and return an ICC profile from a bytearray block"""
if not self.__initialized:
return None
return _liblcms_cmsOpenProfileFromMem(ctypes.create_string_buffer(iccData), ctypes.c_ulong(len(iccData)))
def colorManagedQImage(self, image, profileSource, profileTarget, intent=LcmsIntent.PERCEPTUAL, flags=0):
"""Convert given QImage `image` from icc `profileSource` to `profileTarget` using given `intent` and `flags` for conversion options
Return a QImage
Or None if not possible to apply conversion
Given profiles (source, target) are not freed by function
"""
if not self.__initialized:
return None
if not isinstance(image, QImage):
raise EInvalidType("Given `image` must be a QImage")
lcmsFormat = LcmsPixelType.qtToLcmsFormat(image.format())
if lcmsFormat == 0:
# unknown format, not possible to convert
return None
# define returned image
returnedImage=QImage(image.size(), image.format())
colorTransform = _liblcms_cmsCreateTransform(profileSource, lcmsFormat, profileTarget, lcmsFormat, intent, flags)
if colorTransform is None:
# can't do conversion
return None
ptrSourceImage = ctypes.c_void_p(image.bits().__int__())
ptrReturnedImage = ctypes.c_void_p(returnedImage.bits().__int__())
print("-1-", ptrSourceImage, ptrReturnedImage)
_liblcms_cmsDoTransformLineStride(colorTransform, ptrSourceImage, ptrReturnedImage, image.width(), image.height(), image.bytesPerLine(), returnedImage.bytesPerLine(), 0, 0)
print("-2-")
_liblcms_cmsDeleteTransform(colorTransform)
print("-3-")
return returnedImage
# testing
lcms=Lcms()
img=QImage("/home/grum/Temporaire/test_01_ACEScg-elle-V4-g10.png")
profileSrc=lcms.loadProfileFromIccFile("/home/grum/Temporaire/ACEScg-elle-V4-g10.icc")
profileTgt=lcms.loadProfileFromIccFile("/home/grum/Temporaire/sRGB-elle-V2-srgbtrc.icc")
dlg = QDialog(Application.activeWindow().qwindow())
dlg.setModal(True)
layout = QHBoxLayout(dlg)
lbl0 = QLabel("")
lbl1 = QLabel("")
lbl0.setPixmap(QPixmap.fromImage(img))
lbl1.setPixmap(QPixmap.fromImage(lcms.colorManagedQImage(img, profileSrc, lcms.createProfileSRGB())))
layout.addWidget(lbl0)
layout.addWidget(lbl1)
dlg.exec()
Need to dig about non RGB files:
Prototype has been implemented and commit made: d9a2906031eaee909f303984af603647da06690a
Currently need to put this feature in stand-by
Currently generated thumbnail are in sRBG whatever is the original document color profile.
If document have a non sRGB color profile, color for thumbnails are inaccurate It would be useful to use embedded color profile:
There's no native method in Qt 5.12 to read/apply icc profile, then:
QColorSpace
is available from Qt 5.14)