Custom display calibration for Qualcomm Android devices
import sys
import argparse

def info(filename):
    import qdcmdiy.store
    db = qdcmdiy.store.load(filename)
    print("Modes:")
    for mode in db.get_mode_names():
        print(mode)

def patch(filename, mode, input_shaper, lut3d, output_shaper):
    import qdcmdiy.store
    import qdcmdiy.pipeline
    import qdcmdiy.data
    db = qdcmdiy.store.load(filename)
    mode = db.get_mode(mode)
    pipeline = qdcmdiy.pipeline.ColorPipeline()
    if input_shaper is not None:
        pipeline.degamma = qdcmdiy.data.load_lut3x1d(input_shaper)
    if lut3d is not None:
        pipeline.gamut = qdcmdiy.data.load_lut3d(lut3d)
    if output_shaper is not None:
        pipeline.gamma = qdcmdiy.data.load_lut3x1d(output_shaper)
    mode.set_color_pipeline(pipeline)
    with open(filename, 'w', encoding='utf-8') as f:
        db.dump(f)

def merge_lut(lut1_filename, lut2_filename, out_filename):
    import qdcmdiy.data
    import colour
    lut1 = qdcmdiy.data.load_anylut(lut1_filename)
    lut2 = qdcmdiy.data.load_anylut(lut2_filename)
    merged = type(lut1)(lut2.apply(lut1.table))
    colour.io.write_LUT_IridasCube(merged, out_filename)

def main():
    parser = argparse.ArgumentParser()

    commands = parser.add_subparsers(dest='command', metavar='command', )

    parser_info = commands.add_parser('info', help='show info of specified qdcm database file')
    parser_info.add_argument('filename', help='qdcm database file')

    parser_patch = commands.add_parser('patch', help='replace calibration pipeline in qdcm database file', formatter_class=argparse.RawTextHelpFormatter)
    parser_patch.add_argument('filename', help='qdcm database file')
    parser_patch.add_argument('--mode', help='the mode to be patched')
    parser_patch.add_argument('--input-shaper', help='3x1D LUT file for input shaper (8-bit input / 12-bit output)', metavar='FILE')
    parser_patch.add_argument('--3dlut', help='3D LUT file applied after input shaper (17x17x17 / 12-bit output)', dest='lut3d', metavar='FILE')
    parser_patch.add_argument('--output-shaper', help='3x1D LUT file applied after 3D LUT (10-bit input / output)', metavar='FILE')
    parser_patch.epilog = "Unspecified stages will be disabled. Other settings (e.g. game enhancement) is remain unchanged.\n\nSupported file formats:\n    IRIDAS/Resolve .cube\n    ArgyllCMS/DisplayCAL .cal"


    parser_merge_lut = commands.add_parser('merge-lut', help='merge two LUT files', formatter_class=argparse.RawTextHelpFormatter)
    parser_merge_lut.add_argument('lut1', help='first LUT file')
    parser_merge_lut.add_argument('lut2', help='second LUT file')
    parser_merge_lut.add_argument('out', help='output LUT file')
    parser_merge_lut.epilog = "out(input) = lut2(lut1(input))\n\nmerged LUT will have the same type and size as lut1\n\nSupported file formats:\n    IRIDAS/Resolve .cube\n    ArgyllCMS/DisplayCAL .cal"


    args = parser.parse_args()
    # print(args)
    if args.command is None:
        parser.print_help()
        sys.exit(1)
    elif args.command == 'info':
        info(args.filename)
    elif args.command == 'patch':
        patch(args.filename, args.mode, args.input_shaper, args.lut3d, args.output_shaper)
    elif args.command == 'merge-lut':
        merge_lut(args.lut1, args.lut2, args.out)

if __name__ == '__main__':
    main()

qdcmdiy/__main__.py
dantmnf/qdcm-diy
import pandas as pd
import io
from typing import BinaryIO

class CGATSTable:
    def __init__(self, dataframe: pd.DataFrame, metadata: dict[str, str], signature='CGATS.17'):
        self.dataframe = dataframe
        self.metadata = metadata
        self.signature = signature

def _read_number(f: io.BufferedReader):
    buffer = bytearray()
    state = 'integer'
    is_float = False
    while True:
        ch = f.peek(1)[0:1]
        if ch in b'0123456789':
            if state in {'integer', 'sep_fraction', 'fraction', 'sep_exp', 'exp'}:
                buffer.extend(f.read(1))
                if state == 'sep_fraction':
                    state = 'fraction'
                    is_float = True
                elif state == 'sep_exp':
                    state = 'exp'
                    is_float = True
            else:
                raise ValueError("Unexpected digit")
        elif ch == b'.':
            if state == 'integer':
                buffer.extend(f.read(1))
                state = 'sep_fraction'
            else:
                raise ValueError("Unexpected '.'")
        elif ch == b'e' or ch == b'E':
            if state in {'integer', 'fraction'}:
                buffer.extend(f.read(1))
                state = 'sep_exp'
            else:
                raise ValueError("Unexpected 'e'")
        elif ch == b'+' or ch == b'-':
            if state == 'sep_exp':
                buffer.extend(f.read(1))
                state = 'exp'
            else:
                raise ValueError("Unexpected '+' or '-'")
        elif ch == b'' or ch == b' ' or ch == b'\t' or ch == b'\r' or ch == b'\n':
            if state in {'integer', 'fraction', 'exp'}:
                break
            else:
                raise ValueError("Unexpected end of number")
        else:
            raise ValueError("Unexpected character")
    if is_float:
        return float(buffer)
    else:
        return int(buffer)

def _read_cgats_string(f: io.BufferedReader):
    buffer = bytearray()
    state = 'start'
    while True:
        buf = f.peek(1)
        if state == 'start':
            if f.read(1) == b'"':
                state = 'in_string'
                continue
            else:
                raise ValueError("Unexpected character " + repr(buf[0:1]))
        elif state == 'in_string':
            ch = f.read(1)
            if ch == b'"':
                state = "escape_or_ending"
                continue
            else:
                buffer.extend(ch)

        elif state == "escape_or_ending":
            ch = f.peek(1)[0:1]
            if ch == b'"':
                state = 'in_string'
                buffer.extend(f.read(1))
                continue
            if ch == b'' or ch == b' ' or ch == b'\t' or ch == b'\r' or ch == b'\n':
                break
            else:
                raise ValueError("Unexpected character " + repr(buf[0]))
        else:
            if ch == b'' or ch == b' ' or ch == b'\t' or ch == b'\r' or ch == b'\n':
                break
    return buffer

def _read_cgats_ident(f: io.BufferedReader):
    buffer = bytearray()
    while True:
        ch = f.peek(1)[0:1]
        if ch == b'' or ch == b' ' or ch == b'\t' or ch == b'\r' or ch == b'\n':
            break
        if b'a' <= ch <= b'z' or b'A' <= ch <= b'Z' or b'0' <= ch <= b'9' or ch == b'_':
            buffer.extend(f.read(1))
        else:
            raise ValueError("Unexpected character " + repr(ch))
    return buffer

def _read_cgats_token(f: io.BufferedReader):
    token_type = 'start'
    value = None
    while True:
        buf = f.peek(1)
        ch = buf[0:1]
        if token_type == "start":
            if ch == b' ' or ch == b'\t':
                f.read(1)
                continue
            if ch == b'':
                return 'eof', None
            if ch in b'+-0123456789':
                token_type = 'number'
            elif ch == b'"':
                token_type = 'string'
            elif (b'a' <= ch <= b'z') or (b'A' <= ch <= b'Z'):
                token_type = 'ident'
            elif ch == b'#':
                token_type = 'comment'
            elif ch == b'\r' or ch == b'\n':
                while ch == b'\r' or ch == b'\n':
                    f.read(1)
                    ch = f.peek(1)[0:1]
                token_type = 'newline'
        if token_type == 'number':
            value = _read_number(f)
        elif token_type == 'string':
            value = _read_cgats_string(f)
        elif token_type == 'ident':
            value = _read_cgats_ident(f)
        elif token_type == 'comment':
            endpos = -1
            while endpos < 0:
                p = f.peek(64)
                if p == b"":
                    break
                endpos = p.find(b'\r')
                if endpos == -1:
                    endpos = p.find(b'\n')
                if endpos == -1:
                    f.read(len(p))
            f.read(endpos)
        return token_type, value

def _skip_to_next_line(f: io.BufferedReader):
    in_comment = False
    while True:
        ch = f.read(1)
        if not in_comment and ch in b'\r \t':
            continue
        if ch == b'#':
            in_comment = True
            continue
        if ch == b'\n':
            break

def _read_line(f: io.BufferedReader):
    buffer = bytearray()
    while True:
        ch = f.read(1)
        if ch == b'\r':
            continue
        if ch == b'\n':
            break
        buffer.extend(ch)
    return buffer

def read(f: BinaryIO):
    sig = None
    state = 'start'
    metadata = {}
    metadata_key = None
    fields = []
    data = []
    current_data_row = []
    custom_section_name = None
    custom_section_lines = []

    try:

        while True:
            token_type, token_value = _read_cgats_token(f)

            if state == "start":
                if token_type == 'ident':
                    sig = token_value
                    state = "metadata_key"
                    continue
                if token_type == 'newline' or token_type == 'comment':
                    continue
                elif token_type == "eof":
                    return None
                else:
                    raise ValueError("Unexpected token type " + token_type)
            
            elif state == "metadata_key":
                if token_type == 'ident':
                    if token_value == b'BEGIN_DATA_FORMAT':
                        state = "begin_data_format"
                        continue
                    elif token_value == b"BEGIN_DATA":
                        state = "data"
                        continue
                    elif token_value.startswith(b'BEGIN_'):
                        state = "begin_custon_section"
                        custom_section_name = token_value[6:]
                        end_marker = b'END_' + custom_section_name
                        custom_section_lines.clear()
                        _skip_to_next_line(f)
                        while (content := _read_line(f)) != end_marker:
                            custom_section_lines.append(content)
                        metadata[custom_section_name.decode()] = b'\n'.join(custom_section_lines).decode()
                        state = "metadata_key"
                        continue
                    metadata_key = token_value.decode()
                    state = "metadata_value"
                    continue
                if token_type == 'newline' or token_type == 'comment':
                    continue
                else:
                    raise ValueError("Unexpected token type " + token_type)

            elif state == "metadata_value":
                if token_type == 'string':
                    metadata[metadata_key] = token_value.decode()
                    continue
                elif token_type == 'number':
                    metadata[metadata_key] = token_value
                    continue
                elif token_type == 'newline':
                    state = "metadata_key"
                    continue
                if token_type == 'comment':
                    continue
                else:
                    raise ValueError("Unexpected token type " + token_type)

            elif state == "begin_data_format":
                if token_type == 'newline':
                    state = "data_format"
                    continue
                if token_type == 'comment':
                    continue
                else:
                    raise ValueError("Unexpected token type " + token_type)

            elif state == "data_format":
                if token_type == 'ident':
                    fields.append(token_value.decode())
                    continue
                if token_type == 'newline':
                    state = "data_format_newline"
                    continue
                if token_type == 'comment':
                    continue
                else:
                    raise ValueError("Unexpected token type " + token_type)

            elif state == "data_format_newline":
                if token_type == 'ident':
                    if token_value == b'END_DATA_FORMAT':
                        state = "metadata_key"
                        continue
                    fields.append(token_value.decode())
                    continue
                if token_type == 'newline':
                    state = "data_format_newline"
                    continue
                if token_type == 'comment':
                    continue
                else:
                    raise ValueError("Unexpected token type " + token_type)
                
            elif state == "begin_data":
                if token_type == 'newline':
                    state = "data"
                    continue
                if token_type == 'comment':
                    continue
                else:
                    raise ValueError("Unexpected token type " + token_type)

            elif state == "data":
                if token_type == 'number' or token_type == 'string':
                    current_data_row.append(token_value)
                    continue
                if token_type == 'newline':
                    if current_data_row:
                        data.append(current_data_row)
                        current_data_row = []
                    state = "data_newline"
                    continue
                if token_type == 'comment':
                    continue
                else:
                    raise ValueError("Unexpected token type " + token_type)

            elif state == "data_newline":
                if token_type == 'ident':
                    if token_value == b'END_DATA':
                        break
                    current_data_row.append(token_value)
                    continue
                if token_type == 'number' or token_type == 'string':
                    current_data_row.append(token_value)
                    continue
                if token_type == 'newline':
                    data.append(current_data_row)
                    current_data_row = []
                    state = "data_newline"
                    continue
                if token_type == 'comment':
                    continue
                else:
                    raise ValueError("Unexpected token type " + token_type)



            if token_type == "eof":
                break
    except Exception as e:
        raise ValueError(f"Error while parsing CGATS file at position {f.tell()}") from e

    df = pd.DataFrame(data, columns=fields)
    df.set_index([fields[0]], inplace=True)
    return CGATSTable(df, metadata, sig)

qdcmdiy/cgats.py
dantmnf/qdcm-diy
import colour
import numpy as np

def resample_lut(lut3d, size):
    return colour.LUT3D(lut3d.apply(colour.LUT3D.linear_table(size), interpolator=colour.algebra.interpolation.table_interpolation_tetrahedral))

def to_12bit(a):
    return np.uint32(np.clip(a, 0, 1) * 4095 + 0.5)

def to_4096(a):
    return np.uint32(np.clip(a, 0, 1) * 4096 + 0.5)

def to_10bit(a):
    return np.uint32(np.clip(a, 0, 1) * 1023 + 0.5)

def load_argyll_cal(filename):
    from . import cgats
    with open(filename, "rb") as f:
        cal = cgats.read(f)
    return colour.LUT3x1D(np.asarray(cal.dataframe[['RGB_R', 'RGB_G', 'RGB_B']]))

def load_anylut(filename: str):
    if filename.endswith(".cal"):
        return load_argyll_cal(filename)
    lut = colour.io.read_LUT(filename)
    assert np.all(lut.domain == np.array([[0,0,0],[1,1,1]])), "LUT domain must be [0,0,0]-[1,1,1]"
    return lut

def load_lut3x1d(filename: str):
    lut = load_anylut(filename)
    assert isinstance(lut, colour.LUT3x1D)
    return lut

def load_lut3d(filename: str):
    lut = colour.io.read_LUT(filename)
    assert isinstance(lut, colour.LUT3D)
    return lut
qdcmdiy/data.py
dantmnf/qdcm-diy
import colour
from typing import Optional

class ColorPipeline:
    def __init__(self, degamma: Optional[colour.LUT3x1D] = None, gamut: Optional[colour.LUT3D] = None, gamma: Optional[colour.LUT3x1D] = None):
        self.degamma = degamma
        self.gamut = gamut
        self.gamma = gamma

qdcmdiy/pipeline.py
dantmnf/qdcm-diy
from typing import Protocol

from qdcmdiy.pipeline import ColorPipeline

class QdcmMode(Protocol):
    def set_color_pipeline(self, pipeline: ColorPipeline):
        ...

class QdcmDatabase(Protocol):
    def get_mode(self, name) -> QdcmMode:
        ...
    def get_mode_names(self) -> list[str]:
        ...
    def dump(self, io):
        ...

def load(filename: str) -> QdcmDatabase:
    if filename.endswith(".xml"):
        from qdcmdiy.store_xml import QdcmDatabaseXml
        return QdcmDatabaseXml(filename)
    elif filename.endswith(".json"):
        from qdcmdiy.store_json import QdcmDatabaseJson
        return QdcmDatabaseJson(filename)
    else:
        raise ValueError("Unknown file type")

qdcmdiy/store.py
dantmnf/qdcm-diy
import json
import numpy as np
import colour
from qdcmdiy.data import resample_lut, to_12bit, to_10bit, to_4096
from qdcmdiy.pipeline import ColorPipeline


_gamut_map = {
    'sRGB': '1',
    'P3': '12'
}

_transfer_map = {
    'sRGB': '1'
}

_dummy_pcc = {
        "B": {
        "b": 1.0,
        "bb": 0.0,
        "c": 0.0,
        "g": 0.0,
        "gb": 0.0,
        "gg": 0.0,
        "r": 0.0,
        "rb": 0.0,
        "rg": 0.0,
        "rgb": 0.0,
        "rr": 0.0
    },
    "G": {
        "b": 0.0,
        "bb": 0.0,
        "c": 0.0,
        "g": 1.0,
        "gb": 0.0,
        "gg": 0.0,
        "r": 0.0,
        "rb": 0.0,
        "rg": 0.0,
        "rgb": 0.0,
        "rr": 0.0
    },
    "R": {
        "b": 0.0,
        "bb": 0.0,
        "c": 0.0,
        "g": 0.0,
        "gb": 0.0,
        "gg": 0.0,
        "r": 1.0,
        "rb": 0.0,
        "rg": 0.0,
        "rgb": 0.0,
        "rr": 0.0
    },
    "disableBeforeUpload": False,
    "displayID": 0,
    "enable": False
}

_linear_igc = {
    "displayID": 0,
    "ditherEnable": True,
    "ditherStrength": 4,
    "enable": False,
    "lutB": [int(x) for x in to_12bit(np.arange(257) / 256)],
    "lutG": [int(x) for x in to_12bit(np.arange(257) / 256)],
    "lutR": [int(x) for x in to_12bit(np.arange(257) / 256)],
}

_linear_gc = {
    "bitsRounding": 10,
    "displayID": 0,
    "enable": False,
    "lutB": list(range(1024)),
    "lutG": list(range(1024)),
    "lutR": list(range(1024)),
}

def decode_str(s: str):
    assert len(s) % 2 == 0
    size = len(s) // 2
    buf = bytearray(size)
    pos = 0
    if size % 2 == 1:
        buf[0] = int(s[0:2], 16)
        pos = 2
    for i in range(pos, len(s), 4):
        byte0 = int(s[i:i+2], 16)
        byte1 = int(s[i+2:i+4], 16)
        pos = i // 2
        buf[pos] = byte1
        buf[pos+1] = byte0
    return buf

class NioStyleBuffer:
    def __init__(self, size):
        self.buffer = bytearray(size)
        self.pos = 0
    def write(self, s):
        view = memoryview(self.buffer)
        view[self.pos:self.pos+len(s)] = s
        self.pos += len(s)

def encode(b):
    buffer = NioStyleBuffer(len(b) * 2)
    offset = 0
    if len(b) % 2 == 1:
        buffer.write("{:02X}".format(b[0]).encode())
        offset = 1
    for i in range(offset, len(b), 2):
        buffer.write("{:02X}{:02X}".format(b[i+1], b[i]).encode())
    return buffer.buffer.decode()

def encode_nested_json(jdoc):
    s = json.dumps(jdoc, indent=None, separators=(',', ':'))
    return encode(s.encode())


def lut3x1d_to_igc_json(lut: colour.LUT3x1D):
    if lut.size != 257:
        lut = colour.LUT3x1D(lut.apply(colour.LUT3x1D.linear_table(257)))
    return {
        "displayID": 0,
        "ditherEnable": True,
        "ditherStrength": 4,
        "enable": True,
        "lutB": [int(x) for x in to_12bit(lut.table[:, 2].ravel())],
        "lutG": [int(x) for x in to_12bit(lut.table[:, 1].ravel())],
        "lutR": [int(x) for x in to_12bit(lut.table[:, 0].ravel())],
    }


def lut3x1d_to_gc_json(lut: colour.LUT3x1D):
    if lut.size != 1024:
        lut = colour.LUT3x1D(lut.apply(colour.LUT3x1D.linear_table(1024)))
    return {
        "bitsRounding": 10,
        "displayID": 0,
        "enable": True,
        "lutB": [int(x) for x in to_10bit(lut.table[:, 2].ravel())],
        "lutG": [int(x) for x in to_10bit(lut.table[:, 1].ravel())],
        "lutR": [int(x) for x in to_10bit(lut.table[:, 0].ravel())],
    }


def lut3d_to_json(lut: colour.LUT3D):
    
    def convert_to_qdcmjson(lut3d):
        table = lut3d.table
        size = lut3d.size
        return [",".join(str(x) for x in to_4096(table[r, g, b])) for b in range(size) for g in range(size) for r in range(size)]

    assert lut.size >= 17, "LUT3D size must be at least 17"
    if lut.size > 17:
        fine = resample_lut(lut, 17)
    else:
        fine = lut
    coarse = resample_lut(lut, 5)
    return {
        "displayID": 0,
        "enable": True,
        "mapCoarse": convert_to_qdcmjson(coarse),
        "mapFine": convert_to_qdcmjson(fine),
    }

_linear_3dlut = lut3d_to_json(colour.LUT3D(size=17))
_linear_3dlut["enable"] = False

class QdcmDatabaseJson:
    def __init__(self, filename: str):
        with open(filename, 'r') as f:
            jdoc = json.load(f)
        self.jdoc = jdoc
        modes = {}
        panel_key = next(iter(set(jdoc.keys()) - {"Copyright", "Version"}))
        for mode_name, mode_obj in jdoc[panel_key].items():
            try:
                modename2 = (
                    f"gamut {_gamut_map[mode_obj['Applicability']['ColorPrimaries']]}" +
                    f" gamma {_transfer_map[mode_obj['Applicability']['GammaTransfer']]}" +
                    f" intent {mode_obj['Applicability']['RenderIntent']}" +
                    f" Dynamic_range {mode_obj['DynamicRange']}"
                )
                modes[modename2] = QdcmModeJson(mode_obj)
            except KeyError:
                pass
        self.modes = modes

    def get_mode_names(self):
        return list(self.modes.keys())

    def get_mode(self, name):
        return self.modes[name]
    
    def dump(self, io):
        json.dump(self.jdoc, io, indent=None, separators=(',', ':'))
        
class QdcmModeJson:
    def __init__(self, objref: dict):
        self.objref = objref
    def set_color_pipeline(self, pipeline: ColorPipeline):
        assert pipeline is not None
        if pipeline.degamma is not None:
            self.objref["PostBlendIGC"] = encode_nested_json(lut3x1d_to_igc_json(pipeline.degamma))
        else:
            self.objref["PostBlendIGC"] = encode_nested_json(_linear_igc)
        if pipeline.gamut is not None:
            self.objref["PostBlendGamut"] = encode_nested_json(lut3d_to_json(pipeline.gamut))
        else:
            self.objref["PostBlendGamut"] = encode_nested_json(_linear_3dlut)
        if pipeline.gamma is not None:
            self.objref["PostBlendGC"] = encode_nested_json(lut3x1d_to_gc_json(pipeline.gamma))
        else:
            self.objref["PostBlendGC"] = encode_nested_json(_linear_gc)
        self.objref["PostBlendPCC"] = encode_nested_json(_dummy_pcc)

qdcmdiy/store_json.py
dantmnf/qdcm-diy
import xml.dom.minidom
import colour
import numpy as np

from qdcmdiy.pipeline import ColorPipeline
from .data import resample_lut, to_10bit, to_12bit, to_4096

def lut3x1d_to_igc_xml(lut: colour.LUT3x1D):
    if lut.size != 256:
        lut = colour.LUT3x1D(lut.apply(colour.LUT3x1D.linear_table(256)))
    buf = np.zeros(1024*3+3, dtype="<u4")
    buf[0] = 0
    buf[1] = 256
    buf[2] = 6
    buf[3:256+3] = to_10bit(lut.table[:, 0].ravel())
    buf[1024+3:1024+3+256] = to_10bit(lut.table[:, 1].ravel())
    buf[2048+3:2048+3+256] = to_10bit(lut.table[:, 2].ravel())
    return buf.tobytes().hex().upper()

def lut3x1d_to_gc_xml(lut: colour.LUT3x1D):
    if lut.size != 1024:
        lut = colour.LUT3x1D(lut.apply(colour.LUT3x1D.linear_table(1024)))
    buf = np.zeros(1024*3+3, dtype="<u4")
    buf[0] = 1
    buf[1] = 1024
    buf[2] = 6
    buf[3:1024+3] = to_10bit(lut.table[:, 0].ravel())
    buf[1024+3:1024+3+1024] = to_10bit(lut.table[:, 1].ravel())
    buf[2048+3:2048+3+1024] = to_10bit(lut.table[:, 2].ravel())
    return buf.tobytes().hex().upper()

def lut3d_to_xml(lut: colour.LUT3D):
    linear17 = colour.LUT3D.linear_table(17)
    if lut.size != 17:
        lut = resample_lut(lut, 17)
    buf = np.zeros(17*17*17*6+4, dtype="<u4")
    buf[3] = 4913
    lutview = buf[4:].reshape((17, 17, 17, 2, 3))
    i = 4
    for b in range(17):
        for g in range(17):
            for r in range(17):
                lutview[b, g, r, 0, :] = to_4096(linear17[r, g, b])
                lutview[b, g, r, 1, :] = to_4096(lut.table[r, g, b])
    return buf.tobytes().hex().upper()

def set_inner_text(node, text):
    for child in node.childNodes:
        node.removeChild(child)
    node.appendChild(node.ownerDocument.createTextNode(text))

class QdcmDatabaseXml:
    def __init__(self, filename):
        self.dom = xml.dom.minidom.parse(filename)
        modes = {}
        for mode in self.dom.getElementsByTagName("Mode"):
            modes[mode.getAttribute("Name")] = QdcmModeXml(mode)
        self.modes = modes
    def get_mode(self, name):
        return self.modes[name]
    def get_mode_names(self):
        return list(self.modes.keys())
    def dump(self, io):
        self.dom.writexml(io)

class QdcmModeXml:
    def __init__(self, dom_node: xml.dom.minidom.Element):
        self.dom_node = dom_node

    def set_color_pipeline(self, pipeline: ColorPipeline):
        def find_feature(feature_type):
            for feature in self.dom_node.getElementsByTagName("Feature"):
                if feature.getAttribute("FeatureType") == feature_type:
                    return feature
            return None
        
        igc_feature = find_feature("7")
        gc_feature = find_feature("8")
        gamut_feature = find_feature("3")
        mixer_gc_feature = find_feature("6")
        pcc_feature = find_feature("2")

        if igc_feature is not None:
            print("found igc feature")
            if pipeline.degamma is not None:
                set_inner_text(igc_feature, lut3x1d_to_igc_xml(pipeline.degamma))
                igc_feature.setAttribute("Disable", "false")
            else:
                igc_feature.setAttribute("Disable", "true")
        elif pipeline.degamma is not None:
            igc_feature = self.dom_node.ownerDocument.createElement("Feature")
            igc_feature.setAttribute("FeatureType", "7")
            igc_feature.setAttribute("Disable", "false")
            igc_feature.setAttribute("DataSize", "12300")
            igc_feature.appendChild(self.dom_node.ownerDocument.createTextNode(lut3x1d_to_igc_xml(pipeline.degamma)))
            self.dom_node.appendChild(igc_feature)

        if gc_feature is not None:
            print("found gc feature")
            if pipeline.degamma is not None:
                set_inner_text(gc_feature, lut3x1d_to_igc_xml(pipeline.gamma))
                gc_feature.setAttribute("Disable", "false")
            else:
                gc_feature.setAttribute("Disable", "true")
        elif pipeline.gamma is not None:
            gc_feature = self.dom_node.ownerDocument.createElement("Feature")
            gc_feature.setAttribute("FeatureType", "8")
            gc_feature.setAttribute("Disable", "false")
            gc_feature.setAttribute("DataSize", "12300")
            igc_feature.appendChild(self.dom_node.ownerDocument.createTextNode(lut3x1d_to_gc_xml(pipeline.degamma)))
            self.dom_node.appendChild(gc_feature)

        if gamut_feature is not None:
            print("found gamut feature")
            if pipeline.gamut is not None:
                set_inner_text(gamut_feature, lut3d_to_xml(pipeline.gamut))
                gamut_feature.setAttribute("Disable", "false")
            else:
                gamut_feature.setAttribute("Disable", "true")
        elif pipeline.gamma is not None:
            gamut_feature = self.dom_node.ownerDocument.createElement("Feature")
            gamut_feature.setAttribute("FeatureType", "3")
            gamut_feature.setAttribute("Disable", "false")
            gamut_feature.setAttribute("DataSize", "117928")
            igc_feature.appendChild(self.dom_node.ownerDocument.createTextNode(lut3d_to_xml(pipeline.degamma)))
            self.dom_node.appendChild(gamut_feature)

        if mixer_gc_feature is not None:
            mixer_gc_feature.setAttribute("Disable", "true")

        if pcc_feature is not None:
            pcc_feature.setAttribute("Disable", "true")

qdcmdiy/store_xml.py
dantmnf/qdcm-diy