Grum999 / BuliCommander

An orthodox file manager plugin for Krita
GNU General Public License v3.0
12 stars 0 forks source link

Main interface - Take in account icc profile #9

Open Grum999 opened 2 years ago

Grum999 commented 2 years ago

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:

Grum999 commented 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()
Grum999 commented 2 years ago

Need to dig about non RGB files:

Grum999 commented 2 years ago

Prototype has been implemented and commit made: d9a2906031eaee909f303984af603647da06690a

Currently need to put this feature in stand-by