├── .gitignore ├── LICENSE ├── README.md ├── designer.py ├── generate ├── __init__.py └── __main__.py ├── imagecolorpicker ├── __init__.py ├── __main__.py ├── cli.py ├── cmapfile.py ├── colorgradient.py ├── colorspace.py ├── controller.py ├── delegate │ ├── __init__.py │ ├── gradientcolordelegate.py │ ├── gradientlistdelegate.py │ ├── gradientpropertydelegate.py │ ├── imagelistdelegate.py │ └── settingsdelegate.py ├── export.py ├── importer.py ├── language.py ├── model │ ├── __init__.py │ ├── gradientcolorcolumntype.py │ ├── gradientcolormodel.py │ ├── gradientlistcolumntype.py │ ├── gradientlistmodel.py │ ├── gradientpropertycolumntype.py │ ├── gradientpropertymodel.py │ ├── gradientpropertyrowtype.py │ ├── imagelistmodel.py │ ├── settingscolumntype.py │ ├── settingsmodel.py │ └── settingsrowtype.py ├── optimizationalgorithm.py ├── optimizationmodel.py ├── representation.py ├── team210.ico ├── version.py └── widgets │ ├── __init__.py │ ├── gradientwidget │ ├── __init__.py │ └── gradientwidget.py │ ├── mainwindow │ ├── __init__.py │ ├── mainwindow.py │ ├── mainwindow.ui │ └── ui_mainwindow.py │ └── pickablecolorlabel │ ├── __init__.py │ ├── default.png │ ├── pickablecolorlabel.py │ └── pickablecolorlabelplugin.py ├── poetry.lock ├── pyinstaller.spec ├── pyproject.toml └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | __pycache__ 4 | .venv 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ImageColorPicker 2 | Image color picker tool by Team210 3 | 4 | ![Screenshot](https://github.com/LeStahL/ImageColorPicker/blob/master/screenshot.png?raw=true) 5 | 6 | # Build 7 | You need Python and poetry installed and in your system `PATH`. Before building, install the dependencies by running `poetry config virtualenvs.in-project true` and then `poetry install` from the source root. 8 | 9 | For debugging, run `poetry run python -m imagecolorpicker` from the source root. 10 | 11 | For building an executable, run `poetry run pyinstaller imagecolorpicker/imagecolorpicker.spec` from the source root. The executable will be generated in the `dist` subfolder. 12 | 13 | # Use 14 | ImageColorPicker can 15 | * Load images from files with formats supported by Qt6 (By selecting `File->Open` or dragging image files onto the preview). 16 | * Paste images or html with images into the preview with `Edit->Paste` or `CTRL+v`. 17 | * Drop images or html with images into the preview over drag&drop (for example from web browsers). This will resolve URLS per http request and decode base-64 encoded images. 18 | * Select colors by clicking into the image. 19 | * Copy the currently selected color or palette by pressing `CTRL+c`. 20 | * Select the color format you want to copy when hitting `CTRL+c` over the `Copy`-dropdown. 21 | * Double-click entries of the Gradient table to set the resp. gradient color to the currently selected one. 22 | * Choose a custom color for reference by editing the language/representation table. 23 | 24 | # License 25 | ImageColorPicker is (c) 2023 Alexander Kraus and GPLv3; see LICENSE for details. 26 | -------------------------------------------------------------------------------- /designer.py: -------------------------------------------------------------------------------- 1 | from os.path import abspath, join 2 | from site import getsitepackages 3 | from argparse import ArgumentParser 4 | from tomllib import load 5 | from sys import exit 6 | from subprocess import run 7 | from distro import id 8 | import os 9 | 10 | if __name__ == '__main__': 11 | source_path = abspath(".") 12 | site_packages_path = list(filter( 13 | lambda site_package_path: '.venv' in site_package_path, 14 | getsitepackages(), 15 | ))[0] 16 | 17 | argumentParser = ArgumentParser("designer", description="Run PyQt6-designer with plugin paths configured in pyproject.toml.") 18 | 19 | argumentParser.add_argument("-v,--verbose", dest='verbose', action='store_true', default=False, help='Enable verbose output.') 20 | argumentParser.add_argument("-c,--config", dest='config', default=join(source_path, 'pyproject.toml'), help='Path of pyproject.toml.') 21 | argumentParser.add_argument("-l,--local", dest='local', action='store_true', default=False, help='Use the local qt6 tools instead of qt6_tools.') 22 | argumentParser.add_argument("-d,--debug", dest='debug', action='store_true', default=False, help='Enable debug output in designer-qt6.') 23 | 24 | args = argumentParser.parse_args() 25 | 26 | if args.debug: 27 | os.environ['QT_DEBUG_PLUGINS'] = '1' 28 | 29 | toml_file = open(args.config, 'rb') 30 | config = load(toml_file) 31 | toml_file.close() 32 | 33 | package_name = config["tool"]["poetry"]["name"] 34 | source_package_path = join(source_path, package_name) 35 | 36 | qt6_designer_command = [] 37 | if args.local: 38 | if id() == 'ubuntu': 39 | qt6_designer_command = ['/usr/lib/qt6/bin/designer'] 40 | elif id() == 'arch': 41 | qt6_designer_command = ['designer6'] 42 | else: 43 | qt6_designer_command = ["pyqt6-tools", "designer"] 44 | 45 | qt6_tools_args = [] 46 | if "tool" in config and "qt-designer" in config["tool"] and "widgets" in config["tool"]["qt-designer"]: 47 | widgets_config = config["tool"]["qt-designer"]["widgets"] 48 | 49 | # Widgets from installed packages 50 | if "site" in widgets_config: 51 | qt6_tools_args += list(map( 52 | lambda suffix: join(site_packages_path, suffix), 53 | widgets_config["site"], 54 | )) 55 | 56 | # Widgets inside the project 57 | if "source" in widgets_config: 58 | qt6_tools_args += list(map( 59 | lambda suffix: join(source_package_path, suffix), 60 | widgets_config["source"], 61 | )) 62 | 63 | if args.local: 64 | path_addition = (':' if os.name == 'posix' else ';').join(qt6_tools_args) 65 | if 'PYQTDESIGNERPATH' in os.environ: 66 | os.environ['PYQTDESIGNERPATH'] += ":" + path_addition 67 | else: 68 | os.environ['PYQTDESIGNERPATH'] = path_addition 69 | qt6_tools_args = [] 70 | else: 71 | flagged_tools_args = [] 72 | for widget_plugin_path in qt6_tools_args: 73 | flagged_tools_args += ["-p", widget_plugin_path] 74 | qt6_tools_args = flagged_tools_args 75 | 76 | if args.debug: 77 | print("Command:", qt6_designer_command + qt6_tools_args) 78 | print("PYQTDESIGNERPATH:", os.environ['PYQTDESIGNERPATH'] if 'PYQTDESIGNERPATH' in os.environ else "not set") 79 | 80 | exit(run(args=qt6_designer_command + qt6_tools_args).returncode) 81 | -------------------------------------------------------------------------------- /generate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeStahL/ImageColorPicker/0b57bfd839a0dea4a59d824df7cda7a45e7727c9/generate/__init__.py -------------------------------------------------------------------------------- /generate/__main__.py: -------------------------------------------------------------------------------- 1 | from argparse import ( 2 | Namespace, 3 | ArgumentParser, 4 | ) 5 | from subprocess import ( 6 | run, 7 | CompletedProcess, 8 | ) 9 | from importlib.resources import files 10 | from pathlib import Path 11 | from imagecolorpicker import ( 12 | widgets, 13 | ) 14 | from difflib import unified_diff 15 | from sys import exit 16 | from ast import parse 17 | from deepdiff import DeepDiff 18 | 19 | 20 | if __name__ == '__main__': 21 | parser: ArgumentParser = ArgumentParser('generate', description='FETT UI and translation generator') 22 | parser.add_argument( 23 | '-c', '--check', 24 | action='store_true', 25 | dest='check', 26 | help='Verify all files have been generated.', 27 | ) 28 | args: Namespace = parser.parse_args() 29 | 30 | buildFolder: Path = Path('build') 31 | if not buildFolder.exists(): 32 | buildFolder.mkdir() 33 | 34 | # PyQt UI files. 35 | UIFiles: list[Path] = [ 36 | files(widgets) / 'mainwindow' / 'mainwindow.ui', 37 | ] 38 | 39 | errored: bool = False 40 | for uiFile in UIFiles: 41 | pyUiFile = Path(uiFile).parent / f'ui_{Path(uiFile).stem}.py' 42 | print(f"Converting {uiFile} to {pyUiFile}...") 43 | 44 | if args.check: 45 | # Copy old file to build 46 | (Path('build') / f'{pyUiFile.stem}_old.py').write_bytes(pyUiFile.read_bytes()) 47 | 48 | # Generate 49 | result: CompletedProcess = run([ 50 | 'poetry', 'run', 51 | 'pyuic6', f'{uiFile}', 52 | '--debug', 53 | '-o', f'{pyUiFile}', 54 | ]) 55 | 56 | if result.returncode != 0: 57 | raise Exception(result.stderr.decode('utf-8')) 58 | 59 | if args.check: 60 | newPath: Path = Path('build') / f'{pyUiFile.stem}_new.py' 61 | 62 | # Copy new file to build 63 | newPath.write_bytes(pyUiFile.read_bytes()) 64 | 65 | oldSource: str = (Path('build') / f'{pyUiFile.stem}_old.py').read_text() 66 | newSource: str = newPath.read_text() 67 | 68 | # diff 69 | if DeepDiff(parse(oldSource), parse(newSource)): 70 | for line in unified_diff( 71 | oldSource.splitlines(), 72 | newSource.splitlines(), 73 | fromfile=str(Path('build') / f'{uiFile.name}.old'), 74 | tofile=str(Path('build') / f'{uiFile.name}.new'), 75 | ): 76 | print(line) 77 | errored = True 78 | 79 | if errored: 80 | print("Generated source were out of date.") 81 | exit(1) 82 | -------------------------------------------------------------------------------- /imagecolorpicker/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from PyQt6.QtWidgets import QApplication 3 | 4 | # Note (@LeStahL): error NEEDS a present QApplication instance. 5 | application: QApplication = QApplication(sys.argv) 6 | -------------------------------------------------------------------------------- /imagecolorpicker/__main__.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtWidgets import QApplication 2 | from imagecolorpicker.controller import Controller 3 | 4 | if __name__ == '__main__': 5 | controller: Controller = Controller() 6 | controller.startApplication() 7 | QApplication.exit(0) 8 | -------------------------------------------------------------------------------- /imagecolorpicker/cli.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeStahL/ImageColorPicker/0b57bfd839a0dea4a59d824df7cda7a45e7727c9/imagecolorpicker/cli.py -------------------------------------------------------------------------------- /imagecolorpicker/cmapfile.py: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | Self, 3 | Optional, 4 | ) 5 | from PyQt6.QtGui import QImage 6 | from imagecolorpicker.colorgradient import ColorGradient 7 | from pathlib import Path 8 | from imagecolorpicker.colorspace import ColorSpaceType 9 | from rtoml import ( 10 | loads, 11 | dumps, 12 | ) 13 | from hashlib import md5 14 | 15 | class CMapFile: 16 | DefaultPreviewColorSpaces: list[list[ColorSpaceType]] = [ 17 | [ColorSpaceType.SRGB, ColorSpaceType.SRGB], 18 | [ColorSpaceType.OKLAB, ColorSpaceType.OKLAB], 19 | [ColorSpaceType.CIELAB, ColorSpaceType.CIELAB], 20 | [ColorSpaceType.ACESAP1, ColorSpaceType.ACESAP1], 21 | [ColorSpaceType.HunterLCH, ColorSpaceType.HunterLCH], 22 | ] 23 | 24 | def __init__( 25 | self: Self, 26 | images: Optional[list[QImage]] = None, 27 | gradients: Optional[list[ColorGradient]] = None, 28 | previewColorSpaces: Optional[list[list[ColorSpaceType]]] = None, 29 | ): 30 | self._images: list[QImage] = images or [] 31 | self._gradients: list[ColorGradient] = gradients or [] 32 | self._previewColorSpaces: list[list[ColorSpaceType]] = previewColorSpaces or CMapFile.DefaultPreviewColorSpaces 33 | 34 | def save(self: Self, path: Path) -> None: 35 | imageFiles: list[str] = [] 36 | for image in self._images: 37 | # image.bits() 38 | ptr = image.bits() 39 | ptr.setsize(image.sizeInBytes()) 40 | 41 | imageFile: str = str(path.parent / f'{md5(ptr.asstring()).hexdigest()}.jpg') 42 | print(imageFile) 43 | image.save(imageFile) 44 | imageFiles.append(imageFile) 45 | 46 | path.write_text(dumps({ 47 | 'images': imageFiles, 48 | 'gradients': list(map( 49 | lambda gradient: gradient.toDict(), 50 | self._gradients, 51 | )), 52 | 'preview_color_spaces': list(map( 53 | lambda pair: { 54 | 'weight': pair[0].name, 55 | 'mix': pair[1].name, 56 | }, 57 | self._previewColorSpaces, 58 | )) 59 | }, pretty=True)) 60 | 61 | def load(self: Self, path: Path) -> None: 62 | info = loads(path.read_text()) 63 | self._images = list(map( 64 | lambda imageFile: QImage(str(path.parent / imageFile)), 65 | info['images'], 66 | )) 67 | self._gradients = list(map( 68 | lambda gradientInfo: ColorGradient.fromDict(gradientInfo), 69 | info['gradients'], 70 | )) 71 | self._previewColorSpaces = list(map( 72 | lambda previewInfo: [ColorSpaceType[previewInfo['weight']], ColorSpaceType[previewInfo['mix']]], 73 | info['preview_color_spaces'], 74 | )) 75 | -------------------------------------------------------------------------------- /imagecolorpicker/colorgradient.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from scipy.optimize import curve_fit, minimize 3 | from numpy import ( 4 | array, 5 | array, 6 | linspace, 7 | ) 8 | from glm import ( 9 | vec3, 10 | fract, 11 | length, 12 | mix, 13 | ) 14 | from typing import ( 15 | Self, 16 | Tuple, 17 | List, 18 | Callable, 19 | ) 20 | from enum import ( 21 | IntEnum, 22 | auto, 23 | ) 24 | from copy import deepcopy 25 | from functools import partial 26 | from PyQt6.QtGui import ( 27 | QColor, 28 | QLinearGradient, 29 | ) 30 | from construct import ( 31 | Float16l, 32 | Array, 33 | ) 34 | from .colorspace import ( 35 | ColorSpace, 36 | ColorSpaceType, 37 | Observer, 38 | Illuminant, 39 | ) 40 | from .optimizationmodel import OptimizationModel 41 | 42 | 43 | class GradientWeight(IntEnum): 44 | Oklab = 0x2 45 | Cielab = 0x3 46 | RGB = 0x1 47 | Unweighted = 0x0 48 | 49 | 50 | class GradientMix(IntEnum): 51 | Oklab = 0x1 52 | Cielab = 0x2 53 | RGB = 0x0 54 | 55 | 56 | class FitModel(IntEnum): 57 | Trigonometric = auto() 58 | HornerPolynomial = auto() 59 | Harmonic = auto() 60 | Gaussian = auto() 61 | 62 | 63 | class Wraparound(IntEnum): 64 | Wrap = auto() 65 | NoWrap = auto() 66 | 67 | 68 | class FitAlgorithm(IntEnum): 69 | LM = auto() 70 | TRF = auto() 71 | DogBox = auto() 72 | CMAES = auto() 73 | 74 | 75 | class ColorGradient: 76 | def __init__( 77 | self: Self, 78 | name: str, 79 | degree: int, 80 | weightColorSpace: ColorSpaceType, 81 | mixColorSpace: ColorSpaceType, 82 | colors: list[vec3], 83 | observer: Observer = Observer.TwoDegreesCIE1931, 84 | illuminant: Illuminant = Illuminant.D65, 85 | model: FitModel = FitModel.HornerPolynomial, 86 | wraparound: Wraparound = Wraparound.Wrap, 87 | fitAlgorithm: FitAlgorithm = FitAlgorithm.LM, 88 | maxFitIterationCount: int = 5000, 89 | fitAmount: int = 256, 90 | ) -> None: 91 | self._name: str = name 92 | self._degree: int = degree 93 | self._colors: list[vec3] = deepcopy(colors) 94 | self._weightColorSpace: ColorSpaceType = weightColorSpace 95 | self._mixColorSpace: ColorSpaceType = mixColorSpace 96 | self._observer: Observer = observer 97 | self._illuminant: Illuminant = illuminant 98 | self._model: FitModel = model 99 | self._wraparound: Wraparound = wraparound 100 | self._weights: list[float] = [0.] * self.colorCount 101 | self._coefficients: list[vec3] = [vec3(0)] * self.colorCount 102 | self._fitAlgorithm: FitAlgorithm = fitAlgorithm 103 | self._maxFitIterationCount: int = maxFitIterationCount 104 | self._fitAmount: int = fitAmount 105 | self._update() 106 | 107 | @property 108 | def colorCount(self: Self) -> int: 109 | return len(self._colors) 110 | 111 | @property 112 | def weights(self: Self) -> list[float]: 113 | return self._weights 114 | 115 | @property 116 | def coefficients(self: Self) -> list[vec3]: 117 | return self._coefficients 118 | 119 | def _update(self: Self) -> None: 120 | self._weights = self.determineWeights() 121 | self._coefficients = self.fit() 122 | 123 | def toDict(self: Self) -> dict: 124 | return { 125 | 'name': self._name, 126 | 'degree': self._degree, 127 | 'colors': list(map( 128 | lambda color: [color.x, color.y, color.z], 129 | self._colors, 130 | )), 131 | 'weight_color_space': self._weightColorSpace.name, 132 | 'mix_color_space': self._mixColorSpace.name, 133 | 'observer': self._observer.name, 134 | 'illuminant': self._illuminant.name, 135 | 'model': self._model.name, 136 | 'wraparound': self._wraparound.name, 137 | 'algorithm': self._fitAlgorithm.name, 138 | 'max_fit_iteration_count': self._maxFitIterationCount, 139 | 'fit_amount': self._fitAmount, 140 | } 141 | 142 | @classmethod 143 | def fromDict(cls: type[Self], info: dict) -> 'ColorGradient': 144 | return cls( 145 | name=info['name'], 146 | degree=int(info['degree']), 147 | colors=list(map( 148 | lambda components: vec3(*components), 149 | info['colors'], 150 | )), 151 | weightColorSpace = ColorSpaceType[info['weight_color_space']], 152 | mixColorSpace = ColorSpaceType[info['mix_color_space']], 153 | observer = Observer[info['observer']], 154 | illuminant = Illuminant[info['illuminant']], 155 | model = FitModel[info['model']], 156 | wraparound = Wraparound[info['wraparound']], 157 | fitAlgorithm = FitAlgorithm[info['algorithm']], 158 | maxFitIterationCount = int(info['max_fit_iteration_count']), 159 | fitAmount = int(info['fit_amount']), 160 | ) 161 | 162 | def determineWeights( 163 | self: Self, 164 | ) -> List[float]: 165 | weights: List[float] = [0.0] * len(self._colors) 166 | colorspaceDistances: List[float] = [0.0] * len(self._colors) 167 | totalColorspaceDistance: float = 0.0 168 | colorCount: int = self.colorCount if self._wraparound == Wraparound.Wrap else (self._colorCount - 1) 169 | for colorIndex in range(colorCount): 170 | c1 = ColorSpace.convert( 171 | self._colors[colorIndex], 172 | ColorSpaceType.SRGB, 173 | self._weightColorSpace, 174 | observer=self._observer, 175 | illuminant=self._illuminant, 176 | ) 177 | c2 = ColorSpace.convert( 178 | self._colors[(colorIndex + 1) % len(self._colors)], 179 | ColorSpaceType.SRGB, 180 | self._weightColorSpace, 181 | observer=self._observer, 182 | illuminant=self._illuminant, 183 | ) 184 | 185 | colorspaceDistance = length(c1 - c2) 186 | colorspaceDistances[colorIndex] = colorspaceDistance 187 | totalColorspaceDistance += colorspaceDistance 188 | 189 | totalDistance: float = 0.0 190 | for colorIndex in range(len(self._colors)): 191 | colorspaceDistances[colorIndex] /= totalColorspaceDistance 192 | weights[colorIndex] = totalDistance 193 | totalDistance += colorspaceDistances[colorIndex] 194 | 195 | return weights 196 | 197 | def evaluate( 198 | self: Self, 199 | amount: float, 200 | ) -> vec3: 201 | amount = fract(amount) 202 | for colorIndex in range(self.colorCount): 203 | if amount < self.weights[(colorIndex + 1) % self.colorCount]: 204 | c1 = ColorSpace.convert( 205 | self._colors[colorIndex % self.colorCount], 206 | ColorSpaceType.SRGB, 207 | self._mixColorSpace, 208 | observer=self._observer, 209 | illuminant=self._illuminant, 210 | ) 211 | c2 = ColorSpace.convert( 212 | self._colors[(colorIndex + 1) % self.colorCount], 213 | ColorSpaceType.SRGB, 214 | self._mixColorSpace, 215 | observer=self._observer, 216 | illuminant=self._illuminant, 217 | ) 218 | 219 | lowerWeight: float = self.weights[colorIndex] 220 | upperWeight: float = self.weights[(colorIndex + 1) % len(self._colors)] 221 | localAmount: float = (amount - lowerWeight) / abs(upperWeight - lowerWeight) 222 | 223 | result: vec3 = mix(c1, c2, localAmount) 224 | 225 | return ColorSpace.convert( 226 | result, 227 | self._mixColorSpace, 228 | ColorSpaceType.SRGB, 229 | observer=self._observer, 230 | illuminant=self._illuminant, 231 | ) 232 | 233 | c1 = ColorSpace.convert( 234 | self._colors[-1], 235 | ColorSpaceType.SRGB, 236 | self._mixColorSpace, 237 | observer=self._observer, 238 | illuminant=self._illuminant, 239 | ) 240 | c2 = ColorSpace.convert( 241 | self._colors[0], 242 | ColorSpaceType.SRGB, 243 | self._mixColorSpace, 244 | observer=self._observer, 245 | illuminant=self._illuminant, 246 | ) 247 | 248 | lowerWeight: float = self.weights[-1] 249 | localAmount: float = (amount - lowerWeight) / abs(1. - lowerWeight) 250 | 251 | result = mix(c1, c2, localAmount) 252 | return ColorSpace.convert( 253 | result, 254 | self._mixColorSpace, 255 | ColorSpaceType.SRGB, 256 | observer=self._observer, 257 | illuminant=self._illuminant, 258 | ) 259 | 260 | def nearestWeightInColorMap( 261 | self: Self, 262 | c0: vec3, 263 | ) -> float: 264 | costFunction: partial = partial(ColorGradient.colorMapDistance, colorMap=self._coefficients, c0=c0) 265 | return minimize(costFunction, .5, method='Nelder-Mead').x[0] 266 | 267 | @staticmethod 268 | def colorMapDistance(t: float, colorMap: List[vec3], c0: vec3) -> float: 269 | red = [color.r for color in colorMap] 270 | green = [color.g for color in colorMap] 271 | blue = [color.b for color in colorMap] 272 | t = fract(t) 273 | return length(vec3( 274 | ColorGradient.polynomial(t, *red), 275 | ColorGradient.polynomial(t, *green), 276 | ColorGradient.polynomial(t, *blue), 277 | ) - c0) 278 | 279 | def fit( 280 | self: Self, 281 | amount: int = 256, 282 | ) -> List[vec3]: 283 | t = linspace(0., 1., amount) 284 | sampledColors: List[vec3] = list(map( 285 | lambda _amount: self.evaluate(_amount), 286 | t, 287 | )) 288 | 289 | initialGuess: list[list[float]] 290 | model: Callable 291 | if self._model == FitModel.HornerPolynomial: 292 | model = OptimizationModel.Polynomial 293 | initialGuess = OptimizationModel.PolynomialInitialGuess(self._degree) 294 | elif self._model == FitModel.Trigonometric: 295 | model = OptimizationModel.Trigonometric 296 | initialGuess = OptimizationModel.TrigonometricInitialGuess() 297 | elif self._model == FitModel.Harmonic: 298 | model = OptimizationModel.Harmonic 299 | initialGuess = OptimizationModel.HarmonicInitialGuess(self._degree) 300 | elif self._model == FitModel.Gaussian: 301 | model = OptimizationModel.Gauss 302 | initialGuess = OptimizationModel.GaussInitialGuess(self._degree) 303 | # Fit red 304 | r = array(list(map( 305 | lambda color: color.x, 306 | sampledColors, 307 | ))) 308 | rp, _ = curve_fit(model, t, r, initialGuess[0], method='trf', loss='arctan', maxfev=5000) 309 | 310 | # Fit green 311 | g = array(list(map( 312 | lambda color: color.y, 313 | sampledColors, 314 | ))) 315 | gp, _ = curve_fit(model, t, g, initialGuess[1], method='trf', loss='arctan', maxfev=5000) 316 | 317 | # Fit red 318 | b = array(list(map( 319 | lambda color: color.z, 320 | sampledColors, 321 | ))) 322 | bp, _ = curve_fit(model, t, b, initialGuess[2], method='trf', loss='arctan', maxfev=5000) 323 | 324 | result: List[vec3] = [] 325 | for parameterIndex in range(len(bp)): 326 | result.append(vec3( 327 | rp[parameterIndex], 328 | gp[parameterIndex], 329 | bp[parameterIndex], 330 | )) 331 | print(result) 332 | return result 333 | 334 | def allColorMaps( 335 | self: Self, 336 | ) -> List[Tuple[GradientWeight, GradientMix, List[vec3]]]: 337 | result = [] 338 | for weight in GradientWeight: 339 | for mix in GradientMix: 340 | result.append((weight, mix, self.fit(weight=weight, mix=mix))) 341 | return result 342 | 343 | def buildColorMap( 344 | self: Self, 345 | weight: GradientWeight = GradientWeight.Oklab, 346 | mix: GradientMix = GradientMix.Oklab, 347 | slug: str = '_example', 348 | ) -> str: 349 | result: List[vec3] = self.fit(weight=weight, mix=mix) 350 | return """vec3 cmap_{weight}{mix}{slug}(float t) {{ 351 | return {open} 352 | {close}; 353 | }} 354 | """.format( 355 | open='\n +t*('.join(map( 356 | lambda resultIndex: 'vec3({:.2f},{:.2f},{:.2f})'.format(*result[resultIndex]), 357 | range(len(result)), 358 | )), 359 | close=')' * (len(result) - 1), 360 | mix=mix.name, 361 | weight=weight.name, 362 | slug=slug, 363 | ) 364 | 365 | def buildPythonBinary( 366 | self: Self, 367 | weight: GradientWeight = GradientWeight.Oklab, 368 | mix: GradientMix = GradientMix.Oklab, 369 | slug: str = '_example', 370 | ) -> str: 371 | result: List[vec3] = self.fit(weight=weight, mix=mix) 372 | print(result) 373 | zwischenresult = b''.join(map( 374 | lambda resultIndex: b''.join(list(map( 375 | lambda resultComponent: Float16l.build(resultComponent), 376 | result[resultIndex], 377 | ))), 378 | range(len(result)), 379 | )) 380 | print(len(zwischenresult), zwischenresult) 381 | print(Array(21, Float16l).parse(zwischenresult)) 382 | moreresult = f"# Color map coefficients for {slug}\n" + str(zwischenresult) 383 | return moreresult 384 | 385 | def buildNasmBinary( 386 | self: Self, 387 | weight: GradientWeight = GradientWeight.Oklab, 388 | mix: GradientMix = GradientMix.Oklab, 389 | slug: str = '_example', 390 | ) -> str: 391 | result: List[vec3] = self.fit(weight=weight, mix=mix) 392 | return f"; Color map coefficients for {slug}\n" + str(b''.join(map( 393 | lambda resultIndex: b''.join(reversed(list(map( 394 | lambda resultComponent: Float16l.build(resultComponent), 395 | result[resultIndex], 396 | )))), 397 | range(len(result)), 398 | ))) + "," 399 | 400 | def evaluateFit(self: Self, t: float) -> vec3: 401 | model: Callable 402 | if self._model == FitModel.HornerPolynomial: 403 | model = OptimizationModel.Polynomial 404 | elif self._model == FitModel.Trigonometric: 405 | model = OptimizationModel.Trigonometric 406 | elif self._model == FitModel.Harmonic: 407 | model = OptimizationModel.Harmonic 408 | elif self._model == FitModel.Gaussian: 409 | model = OptimizationModel.Gauss 410 | 411 | return vec3( 412 | model(t, *list(map( 413 | lambda fitelement: fitelement.x, 414 | self._coefficients, 415 | ))), 416 | model(t, *list(map( 417 | lambda fitelement: fitelement.y, 418 | self._coefficients, 419 | ))), 420 | model(t, *list(map( 421 | lambda fitelement: fitelement.z, 422 | self._coefficients, 423 | ))), 424 | ) 425 | 426 | def buildCSSGradient( 427 | self: Self, 428 | weight: GradientWeight, 429 | mix: GradientMix, 430 | ) -> str: 431 | amounts: List[float] = list(map(float,range(101))) 432 | 433 | newcolors = list(map( 434 | lambda amount: self.evaluateFit(float(amount) / 100.), 435 | amounts, 436 | )) 437 | 438 | return """linear-gradient({colors});""".format( 439 | colors=', '.join(map( 440 | lambda resultIndex: '{color}'.format( 441 | color=QColor.fromRgbF(*newcolors[resultIndex]).name(), 442 | ), 443 | range(len(newcolors)), 444 | )), 445 | ) 446 | 447 | def linearGradient( 448 | self: Self, 449 | start: float, 450 | width: float 451 | ) -> QLinearGradient: 452 | stopIndices: list[int] = list(range(101)) 453 | newColors = list(map( 454 | lambda stopIndex: QColor.fromRgbF( 455 | *self.evaluateFit( 456 | # This is the amount here. 457 | float(stopIndex) / 100., 458 | ), 459 | ), 460 | stopIndices, 461 | )) 462 | 463 | gradient: QLinearGradient = QLinearGradient() 464 | gradient.setStart(start, 0) 465 | gradient.setFinalStop(start + width, 0) 466 | for stopIndex in stopIndices: 467 | gradient.setColorAt(float(stopIndex) / 100., newColors[stopIndex]) 468 | 469 | return gradient 470 | 471 | def buildSVGGradient( 472 | self: Self, 473 | weight: GradientWeight, 474 | mix: GradientMix, 475 | ) -> str: 476 | amounts: List[float] = list(map(float,range(101))) 477 | 478 | newcolors = list(map( 479 | lambda amount: self.evaluateFit(float(amount) / 100.), 480 | amounts, 481 | )) 482 | 483 | return """ 484 | {colors} 485 | """.format( 486 | colors='\n '.join(map( 487 | lambda resultIndex: ''.format( 488 | color=QColor.fromRgbF(*newcolors[resultIndex]).name(), 489 | offset=int(amounts[resultIndex]), 490 | ), 491 | range(len(newcolors)), 492 | )), 493 | ) 494 | 495 | @staticmethod 496 | def polynomial(t: float, *c: Tuple[float]) -> float: 497 | result = c[-1] 498 | for ck in reversed(c[:-1]): 499 | result = ck + t * result 500 | return result 501 | 502 | DefaultGradient1 = ColorGradient( 503 | "Default Gradient 1", 504 | 7, 505 | ColorSpaceType.OKLAB, 506 | ColorSpaceType.OKLAB, 507 | [ 508 | vec3(0.15, 0.18, 0.26), 509 | vec3(0.51, 0.56, 0.66), 510 | vec3(0.78, 0.67, 0.68), 511 | vec3(0.96, 0.75, 0.60), 512 | vec3(0.97, 0.81, 0.55), 513 | vec3(0.97, 0.61, 0.42), 514 | vec3(0.91, 0.42, 0.34), 515 | vec3(0.58, 0.23, 0.22), 516 | ], 517 | ) 518 | DefaultGradient2 = ColorGradient( 519 | "Default Gradient 2", 520 | 7, 521 | ColorSpaceType.CIELAB, 522 | ColorSpaceType.CIELAB, 523 | [ 524 | vec3(0.02, 0.07, 0.16), 525 | vec3(0.07, 0.31, 0.41), 526 | vec3(0.38, 0.67, 0.69), 527 | vec3(0.95, 0.85, 0.76), 528 | vec3(0.98, 0.94, 0.83), 529 | vec3(0.99, 0.92, 0.51), 530 | vec3(0.92, 0.44, 0.40), 531 | vec3(0.46, 0.25, 0.33), 532 | ], 533 | ) 534 | -------------------------------------------------------------------------------- /imagecolorpicker/colorspace.py: -------------------------------------------------------------------------------- 1 | from glm import ( 2 | mat3, 3 | vec3, 4 | vec2, 5 | vec4, 6 | inverse, 7 | pow, 8 | length, 9 | atan, 10 | sin, 11 | cos, 12 | mix, 13 | sqrt, 14 | fract, 15 | clamp, 16 | dot, 17 | ) 18 | from typing import ( 19 | Self, 20 | Callable, 21 | Any, 22 | ) 23 | from enum import ( 24 | IntEnum, 25 | IntFlag, 26 | auto, 27 | ) 28 | from copy import deepcopy 29 | from networkx import ( 30 | DiGraph, 31 | shortest_path, 32 | draw, 33 | spring_layout, 34 | ) 35 | 36 | class ColorSpaceType(IntEnum): 37 | SRGB = 0x0 38 | RGB = 0x1 39 | CIEXYZ = 0x2 40 | CIELAB = 0x3 41 | CIELCH = 0x4 42 | OKLAB = 0x5 43 | OKLCH = 0x6 44 | HunterLAB = 0x7 45 | HunterLCH = 0x8 46 | HSL = 0x9 47 | YCbCr = 0xA 48 | CIE1931Yxy = 0xB 49 | HSV = 0xC 50 | CIELuv = 0xD 51 | AdobeRGB = 0xE 52 | ACESAP1 = 0xF 53 | 54 | class ColorSpaceParameterType(IntFlag): 55 | NoParameters = 0x0 56 | Illuminant = auto() 57 | Observer = auto() 58 | 59 | class Observer(IntEnum): 60 | TwoDegreesCIE1931 = auto() 61 | TenDegreesCIE1964 = auto() 62 | 63 | class Illuminant(IntEnum): 64 | A = auto() 65 | B = auto() 66 | C = auto() 67 | D50 = auto() 68 | D55 = auto() 69 | D65 = auto() 70 | D75 = auto() 71 | E = auto() 72 | F1 = auto() 73 | F2 = auto() 74 | F3 = auto() 75 | F4 = auto() 76 | F5 = auto() 77 | F6 = auto() 78 | F7 = auto() 79 | F8 = auto() 80 | F9 = auto() 81 | F10 = auto() 82 | F11 = auto() 83 | F12 = auto() 84 | 85 | class ColorSpace: 86 | # Tristimuli 87 | Tristimuli: dict[Observer, dict[Illuminant, vec3]] = { 88 | Observer.TwoDegreesCIE1931: { 89 | Illuminant.A: vec3(109.850, 100.000, 35.585), 90 | Illuminant.B: vec3(99.0927, 100.000, 85.313), 91 | Illuminant.C: vec3(98.074, 100.000, 118.232), 92 | Illuminant.D50: vec3(96.422, 100.000, 82.521), 93 | Illuminant.D55: vec3(95.682, 100.000, 92.149), 94 | Illuminant.D65: vec3(95.047, 100.000, 108.883), 95 | Illuminant.D75: vec3(94.972, 100.000, 122.638), 96 | Illuminant.E: vec3(100.000, 100.000, 100.000), 97 | Illuminant.F1: vec3(92.834, 100.000, 103.665), 98 | Illuminant.F2: vec3(99.187, 100.000, 67.395), 99 | Illuminant.F3: vec3(103.754, 100.000, 49.861), 100 | Illuminant.F4: vec3(109.147, 100.000, 38.813), 101 | Illuminant.F5: vec3(90.872, 100.000, 98.723), 102 | Illuminant.F6: vec3(97.309, 100.000, 60.191), 103 | Illuminant.F7: vec3(95.044, 100.000, 108.755), 104 | Illuminant.F8: vec3(96.413, 100.000, 82.333), 105 | Illuminant.F9: vec3(100.365, 100.000, 67.868), 106 | Illuminant.F10: vec3(96.174, 100.000, 81.712), 107 | Illuminant.F11: vec3(100.966, 100.000, 64.370), 108 | Illuminant.F12: vec3(108.046, 100.000, 39.228), 109 | }, 110 | Observer.TenDegreesCIE1964: { 111 | Illuminant.A: vec3(111.144, 100.000, 35.200), 112 | Illuminant.B: vec3(99.178, 100.000, 84.3493), 113 | Illuminant.C: vec3(97.285, 100.000, 116.145), 114 | Illuminant.D50: vec3(96.720, 100.000, 81.427), 115 | Illuminant.D55: vec3(95.799, 100.000, 90.926), 116 | Illuminant.D65: vec3(94.811, 100.000, 107.304), 117 | Illuminant.D75: vec3(94.416, 100.000, 120.641), 118 | Illuminant.E: vec3(100.000, 100.000, 100.000), 119 | Illuminant.F1: vec3(94.791, 100.000, 103.191), 120 | Illuminant.F2: vec3(103.280, 100.000, 69.026), 121 | Illuminant.F3: vec3(108.968, 100.000, 51.965), 122 | Illuminant.F4: vec3(114.961, 100.000, 40.963), 123 | Illuminant.F5: vec3(93.369, 100.000, 98.636), 124 | Illuminant.F6: vec3(102.148, 100.000, 62.074), 125 | Illuminant.F7: vec3(95.792, 100.000, 107.687), 126 | Illuminant.F8: vec3(97.115, 100.000, 81.135), 127 | Illuminant.F9: vec3(102.116, 100.000, 67.826), 128 | Illuminant.F10: vec3(99.001, 100.000, 83.134), 129 | Illuminant.F11: vec3(103.866, 100.000, 65.627), 130 | Illuminant.F12: vec3(111.428, 100.000, 40.353), 131 | }, 132 | } 133 | 134 | # sRGB constants 135 | SRGBAlpha: float = 0.055 136 | 137 | # CIELAB constants 138 | MRGBCIEXYZ: mat3 = mat3( 139 | 0.4124564, 0.2126729, 0.0193339, 140 | 0.3575761, 0.7151522, 0.1191920, 141 | 0.1804375, 0.0721750, 0.9503041 142 | ) 143 | MRGBCIEXYZInv: mat3 = inverse(MRGBCIEXYZ) 144 | # CIEXYZ_D65: vec3 = vec3(95.0489, 100.0, 108.8840) 145 | # CIEXYZ_D50: vec3 = vec3(96.4212, 100.0, 82.5188) 146 | # CIEXYZ_ICC: vec3 = vec3(96.42, 100.0, 82.4) 147 | CIEDelta = 6.0 / 29.0 148 | 149 | # OKLAB constants 150 | OKLABM1: mat3 = mat3( 151 | 0.8189330101, 0.0329845436, 0.0482003018, 152 | 0.3618667424, 0.9293118715, 0.2643662691, 153 | -0.1288597137, 0.0361456387, 0.6338517070 154 | ) 155 | OKLABM1Inv: mat3 = inverse(OKLABM1) 156 | OKLABM2: mat3 = mat3( 157 | 0.2104542553, 1.9779984951, 0.0259040371, 158 | 0.7936177850, -2.4285922050, 0.7827717662, 159 | -0.0040720468, 0.4505937099, -0.8086757660 160 | ) 161 | OKLABM2Inv: mat3 = inverse(OKLABM2) 162 | 163 | MACESAPMInv: mat3 = mat3( 164 | 1.6410233797, -0.3248032942, -0.2364246952, 165 | -0.6636628587, 1.6153315917, 0.0167563477, 166 | 0.0117218943, -0.0082844420, 0.9883948585, 167 | ) 168 | MACESAPM: mat3 = inverse(MACESAPMInv) 169 | 170 | # Based on code by tobspr, available at https://github.com/tobspr/GLSL-Color-Spaces/blob/master/ColorSpaces.inc.glsl, 171 | # licensed under MIT. 172 | @staticmethod 173 | def linearToSRGB(component: float) -> float: 174 | if component <= 0.0031308: 175 | return 12.92 * component 176 | return (1.0 + ColorSpace.SRGBAlpha) * pow( 177 | component, 178 | 1. / 2.4, 179 | ) - ColorSpace.SRGBAlpha 180 | 181 | @staticmethod 182 | def RGBToSRGB(rgb: vec3) -> vec3: 183 | return vec3( 184 | ColorSpace.linearToSRGB(rgb.r), 185 | ColorSpace.linearToSRGB(rgb.g), 186 | ColorSpace.linearToSRGB(rgb.b), 187 | ) 188 | 189 | @staticmethod 190 | def SRGBToLinear(component: float) -> float: 191 | if component <= 0.04045: 192 | return component / 12.92 193 | return pow( 194 | (component + ColorSpace.SRGBAlpha) / (1.0 + ColorSpace.SRGBAlpha), 195 | 2.4, 196 | ) 197 | 198 | @staticmethod 199 | def SRGBToRGB(srgb: vec3) -> vec3: 200 | return vec3( 201 | ColorSpace.SRGBToLinear(srgb.r), 202 | ColorSpace.SRGBToLinear(srgb.g), 203 | ColorSpace.SRGBToLinear(srgb.b), 204 | ) 205 | 206 | # Based on info at https://www.image-engineering.de/library/technotes/958-how-to-convert-between-srgb-and-ciexyz 207 | # and https://www.easyrgb.com/en/math.php 208 | @staticmethod 209 | def RGBToCIEXYZ(rgb: vec3) -> vec3: 210 | return ColorSpace.MRGBCIEXYZ * rgb 211 | 212 | @staticmethod 213 | def CIEXYZToRGB(xyz: vec3) -> vec3: 214 | return ColorSpace.MRGBCIEXYZInv * xyz 215 | 216 | # This is equivalent to info available at https://www.easyrgb.com/en/math.php 217 | @staticmethod 218 | def g(component: float) -> float: 219 | if component > pow(ColorSpace.CIEDelta, 3.0): 220 | return pow(component, 1.0 / 3.0) 221 | return component / 3. / pow(ColorSpace.CIEDelta, 2.0) + 4.0 / 29.0 222 | 223 | @staticmethod 224 | def ginv(component: float) -> float: 225 | if component > ColorSpace.CIEDelta: 226 | return pow(component, 3.0) 227 | return 3.0 * pow(ColorSpace.CIEDelta, 2.0) * (component - 4.0 / 29.0) 228 | 229 | @staticmethod 230 | def CIEXYZToCIELAB(ciexyz: vec3, whitepoint: vec3) -> vec3: 231 | var_X = ciexyz.x / whitepoint.x 232 | var_Y = ciexyz.y / whitepoint.y 233 | var_Z = ciexyz.z / whitepoint.z 234 | if var_X > 0.008856: 235 | var_X = pow(var_X, 1 / 3) 236 | else: 237 | var_X = 7.787 * var_X + 16 / 116 238 | if var_Y > 0.008856: 239 | var_Y = pow(var_Y, 1 / 3) 240 | else: 241 | var_Y = 7.787 * var_Y + 16 / 116 242 | if var_Z > 0.008856: 243 | var_Z = pow(var_Z, 1 / 3) 244 | else: 245 | var_Z = 7.787 * var_Z + 16 / 116 246 | 247 | return vec3( 248 | 116 * var_Y - 16, 249 | 500 * (var_X - var_Y), 250 | 200 * (var_Y - var_Z), 251 | ) 252 | 253 | @staticmethod 254 | def CIELABToCIEXYZ(cielab: vec3, whitepoint: vec3) -> vec3: 255 | var_Y = (cielab.x + 16) / 116 256 | var_X = cielab.y / 500 + var_Y 257 | var_Z = var_Y - cielab.z / 200 258 | 259 | if pow(var_Y, 3) > 0.008856: 260 | var_Y = pow(var_Y, 3) 261 | else: 262 | var_Y = (var_Y - 16 / 116) / 7.787 263 | if pow(var_X, 3) > 0.008856: 264 | var_X = pow(var_X, 3) 265 | else: 266 | var_X = (var_X - 16 / 116) / 7.787 267 | if pow(var_Z, 3) > 0.008856: 268 | var_Z = pow(var_Z, 3) 269 | else: 270 | var_Z = (var_Z - 16 / 116) / 7.787 271 | 272 | return vec3( 273 | var_X, 274 | var_Y, 275 | var_Z, 276 | ) * whitepoint 277 | 278 | @staticmethod 279 | def CartesianToPolar(lab: vec3) -> vec3: 280 | return vec3( 281 | lab.x, 282 | length(lab.yz), 283 | atan(lab.z, lab.y), 284 | ) 285 | 286 | @staticmethod 287 | def PolarToCartesian(lch: vec3) -> vec3: 288 | return vec3( 289 | lch.x, 290 | lch.y * cos(lch.z), 291 | lch.y * sin(lch.z), 292 | ) 293 | 294 | @staticmethod 295 | def CIEXYZToOKLAB(ciexyz: vec3) -> vec3: 296 | return ColorSpace.OKLABM2 * pow(ColorSpace.OKLABM1 * ciexyz, vec3(1. / 3.)) 297 | 298 | @staticmethod 299 | def OKLABToCIEXYZ(oklab: vec3) -> vec3: 300 | return ColorSpace.OKLABM1Inv * pow(ColorSpace.OKLABM2Inv * oklab, vec3(3.0)) 301 | 302 | @staticmethod 303 | def CIEXYZToHunterLAB(ciexyz: vec3, whitepoint: vec3) -> vec3: 304 | var_Ka = 175.0 / 198.04 * (whitepoint.y + whitepoint.x) 305 | var_Kb = 70.0 / 218.11 * (whitepoint.y + whitepoint.z) 306 | 307 | return vec3( 308 | 100.0 * sqrt(ciexyz.y / whitepoint.y), 309 | var_Ka * (ciexyz.x / whitepoint.x - ciexyz.y / whitepoint.y) / sqrt(ciexyz.y / whitepoint.y) if ciexyz.y != 0. else 1., 310 | var_Kb * (ciexyz.y / whitepoint.y - ciexyz.z / whitepoint.z) / sqrt(ciexyz.y / whitepoint.y) if ciexyz.y != 0. else 1., 311 | ) 312 | 313 | @staticmethod 314 | def HunterLABToCIEXYZ(hunterlab: vec3, whitepoint: vec3) -> vec3: 315 | var_Ka = 175.0 / 198.04 * (whitepoint.y + whitepoint.x) 316 | var_Kb = 70.0 / 218.11 * (whitepoint.y + whitepoint.z) 317 | 318 | Y = pow(hunterlab.x / whitepoint.y, 2) * 100.0 319 | X = (hunterlab.y / var_Ka * sqrt(Y / whitepoint.y) + Y / whitepoint.y) * whitepoint.x 320 | Z = -(hunterlab.z / var_Kb * sqrt(Y / whitepoint.y) - Y / whitepoint.y) * whitepoint.z 321 | 322 | return vec3(X, Y, Z) 323 | 324 | # RGB to HSL (hue, saturation, lightness/luminance). 325 | # Based on: https://gist.github.com/yiwenl/745bfea7f04c456e0101 326 | @staticmethod 327 | def RGBToHSL(rgb: vec3) -> vec3: 328 | cMin: float = min( 329 | min( 330 | rgb.r, 331 | rgb.g, 332 | ), 333 | rgb.b, 334 | ) 335 | cMax: float = max( 336 | max( 337 | rgb.r, 338 | rgb.g, 339 | ), 340 | rgb.b, 341 | ) 342 | delta: float = cMax - cMin 343 | hsl: vec3 = vec3(0.0, 0.0, (cMax + cMin) / 2.) 344 | if delta != 0.0: 345 | if hsl.z < 0.5: 346 | hsl.y = delta / (cMax + cMin) 347 | else: 348 | hsl.y = delta / (2.0 - cMax - cMin) 349 | deltaR: float = (cMax - rgb.r) / 6.0 / delta + 0.5 350 | deltaG: float = (cMax - rgb.g) / 6.0 / delta + 0.5 351 | deltaB: float = (cMax - rgb.b) / 6.0 / delta + 0.5 352 | if rgb.r == cMax: 353 | hsl.x = deltaB - deltaG 354 | elif rgb.g == cMax: 355 | hsl.x = 1.0 / 3.0 + deltaR - deltaB 356 | else: 357 | hsl.x = 2.0 / 3.0 + deltaG - deltaR 358 | hsl.x = fract(hsl.x) 359 | return hsl 360 | 361 | 362 | @staticmethod 363 | def HSLToRGB(hsl: vec3) -> vec3: 364 | if hsl.y == 0.0: 365 | return vec3(hsl.z) 366 | b: float 367 | if hsl.z < 0.5: 368 | b = hsl.z * (1.0 + hsl.y) 369 | else: 370 | b = hsl.z + hsl.y - hsl.y * hsl.z 371 | a: float = 2.0 * hsl.z - b 372 | hue: float = fract(hsl.x) 373 | rgb: float = clamp( 374 | vec3( 375 | abs(hue * 6.0 - 3.0) - 1.0, 376 | 2.0 - abs(hue * 6.0 - 2.0), 377 | 2.0 - abs(hue * 6.0 - 4.0) 378 | ), 379 | 0.0, 380 | 1.0 381 | ) 382 | return a + rgb * (b - a) 383 | 384 | # RGB to YCbCr, ranges [0, 1]. 385 | # Based on: https://github.com/tobspr/GLSL-Color-Spaces/blob/master/ColorSpaces.inc.glsl 386 | @staticmethod 387 | def RGBToYCbCr(rgb: vec3) -> vec3: 388 | y: float = dot(vec3(0.299, 0.587, 0.114), rgb) 389 | return vec3( 390 | y, 391 | (rgb.b - y) * 0.565, 392 | (rgb.r - y) * 0.713, 393 | ) 394 | 395 | # YCbCr to RGB. 396 | @staticmethod 397 | def YCbCrToRGB(yuv: vec3) -> vec3: 398 | return vec3( 399 | yuv.x + 1.403 * yuv.z, 400 | yuv.x - 0.344 * yuv.y - 0.714 * yuv.z, 401 | yuv.x + 1.770 * yuv.y 402 | ) 403 | 404 | # XYZ to CIE 1931 Yxy color space (luma (Y) along with x and y chromaticity), I found that Photoshop used this. 405 | @staticmethod 406 | def CIEXYZToCIE1931Yxy(xyz: vec3) -> vec3: 407 | s: float = xyz.x + xyz.y + xyz.z 408 | return vec3( 409 | xyz.y, 410 | xyz.x / s, 411 | xyz.y / s, 412 | ) 413 | 414 | @staticmethod 415 | def CIE1931YxyToCIEXYZ(yxy: vec3) -> vec3: 416 | x: float = yxy.x * (yxy.y / yxy.z) 417 | return vec3( 418 | x, 419 | yxy.x, 420 | x / yxy.y - x - yxy.x, 421 | ) 422 | 423 | # HSV (hue, saturation, value) to RGB. 424 | # Sources: https://gist.github.com/yiwenl/745bfea7f04c456e0101, https://gist.github.com/sugi-cho/6a01cae436acddd72bdf 425 | # Changed saturate to clamp, ported to Python. 426 | @staticmethod 427 | def HSVToRGB(hsv: vec3) -> vec3: 428 | K: vec4 = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0) 429 | return hsv.z * mix( 430 | K.xxx, 431 | clamp( 432 | abs(fract(hsv.x + K.xyz) * 6.0 - K.w) - K.x, 433 | 0.0, 434 | 1.0, 435 | ), 436 | hsv.y, 437 | ) 438 | 439 | # RGB to HSV. 440 | # Source: https://gist.github.com/yiwenl/745bfea7f04c456e0101 441 | # Ported to Python. 442 | @staticmethod 443 | def RGBToHSV(rgb: vec3) -> vec3: 444 | cMax: float = max( 445 | max( 446 | rgb.r, 447 | rgb.g, 448 | ), 449 | rgb.b, 450 | ) 451 | cMin: float = min( 452 | min( 453 | rgb.r, 454 | rgb.g, 455 | ), 456 | rgb.b, 457 | ) 458 | delta: float = cMax - cMin 459 | hsv: vec3 = vec3(0.0, 0.0, cMax) 460 | if cMax > cMin: 461 | hsv.y = delta / cMax 462 | if rgb.r == cMax: 463 | hsv.x = (rgb.g - rgb.b) / delta 464 | elif rgb.g == cMax: 465 | hsv.x = 2.0 + (rgb.b - rgb.r) / delta 466 | else: 467 | hsv.x = 4.0 + (rgb.r - rgb.g) / delta 468 | hsv.x = fract(hsv.x / 6.0) 469 | return hsv 470 | 471 | # Adapted from: https://www.easyrgb.com/en/math.php 472 | @staticmethod 473 | def CIEXYZToCIELuv(xyz: vec3, illuminant: vec3) -> vec3: 474 | var_U = 4.0 * xyz.x / (xyz.x + 15.0 * xyz.y + 3.0 * xyz.z) 475 | var_V = 9.0 * xyz.y / (xyz.x + 15.0 * xyz.y + 3.0 * xyz.z) 476 | 477 | var_Y = xyz.y / 100.0 478 | if var_Y > 0.008856: 479 | var_Y = pow(var_Y, 1.0 / 3.0) 480 | else: 481 | var_Y = 7.787 * var_Y + 16.0 / 116.0 482 | 483 | ref_U = 4.0 * illuminant.x / (illuminant.x + 15.0 * illuminant.y + 3.0 * illuminant.z) 484 | ref_V = 9.0 * illuminant.y / (illuminant.x + 15.0 * illuminant.y + 3.0 * illuminant.z) 485 | 486 | s = 116.0 * var_Y - 16.0 487 | return vec3( 488 | s, 489 | 13.0 * s * (var_U - ref_U), 490 | 13.0 * s * (var_V - ref_V), 491 | ) 492 | 493 | @staticmethod 494 | def CIELuvToCIEXYZ(luv: vec3, illuminant: vec3) -> vec3: 495 | var_Y = (luv.x + 16.0) / 116.0 496 | if pow(var_Y, 3.0) > 0.008856: 497 | var_Y = pow(var_Y, 3.0) 498 | else: 499 | var_Y = (var_Y - 16.0 / 116.0) / 7.787 500 | 501 | ref_U = 4.0 * illuminant.x / (illuminant.x + 15.0 * illuminant.y + 3.0 * illuminant.z) 502 | ref_V = 9.0 * illuminant.y / (illuminant.x + 15.0 * illuminant.y + 3.0 * illuminant.z) 503 | 504 | var_U = luv.y / 13.0 / luv.x + ref_U 505 | var_V = luv.z / 13.0 / luv.x + ref_V 506 | 507 | Y = var_Y * 100 508 | X = - ( 9 * Y * var_U ) / ( ( var_U - 4 ) * var_V - var_U * var_V ) 509 | Z = ( 9 * Y - ( 15 * var_V * Y ) - ( var_V * X ) ) / ( 3 * var_V ) 510 | return vec3(X, Y, Z) 511 | 512 | @staticmethod 513 | def CIEXYZToAdobeRGB(xyz: vec3) -> vec3: 514 | var_X = xyz.x / 100.0 515 | var_Y = xyz.y / 100.0 516 | var_Z = xyz.z / 100.0 517 | 518 | var_R = var_X * 2.04137 + var_Y * -0.56495 + var_Z * -0.34469 519 | var_G = var_X * -0.96927 + var_Y * 1.87601 + var_Z * 0.04156 520 | var_B = var_X * 0.01345 + var_Y * -0.11839 + var_Z * 1.01541 521 | 522 | var_R = pow(var_R, 1.0 / 2.19921875) 523 | var_G = pow(var_G, 1.0 / 2.19921875) 524 | var_B = pow(var_B, 1.0 / 2.19921875) 525 | 526 | aR = var_R * 255 527 | aG = var_G * 255 528 | aB = var_B * 255 529 | 530 | return vec3(aR, aG, aB) 531 | 532 | @staticmethod 533 | def AdobeRGBToCIEXYZ(argb: vec3) -> vec3: 534 | var_R = argb.x / 255.0 535 | var_G = argb.y / 255.0 536 | var_B = argb.z / 255.0 537 | 538 | var_R = pow(var_R, 2.19921875) 539 | var_G = pow(var_G, 2.19921875) 540 | var_B = pow(var_B, 2.19921875) 541 | 542 | var_R = var_R * 100.0 543 | var_G = var_G * 100.0 544 | var_B = var_B * 100.0 545 | 546 | X = var_R * 0.57667 + var_G * 0.18555 + var_B * 0.18819 547 | Y = var_R * 0.29738 + var_G * 0.62735 + var_B * 0.07527 548 | Z = var_R * 0.02703 + var_G * 0.07069 + var_B * 0.99110 549 | 550 | return vec3(X, Y, Z) 551 | 552 | @staticmethod 553 | def CIEXYZToACESAP1(ciexyz: vec3) -> vec3: 554 | return ColorSpace.MACESAPM * ciexyz 555 | 556 | @staticmethod 557 | def ACESAP1ToCIEXYZ(ap1: vec3) -> vec3: 558 | return ColorSpace.MACESAPMInv * ap1 559 | 560 | Edges: dict[tuple[ColorSpaceType, ColorSpaceType], tuple[Callable[[vec3, list[float]], vec3], ColorSpaceParameterType]] = { 561 | (ColorSpaceType.SRGB, ColorSpaceType.RGB): (SRGBToRGB, ColorSpaceParameterType.NoParameters), 562 | (ColorSpaceType.RGB, ColorSpaceType.SRGB): (RGBToSRGB, ColorSpaceParameterType.NoParameters), 563 | (ColorSpaceType.RGB, ColorSpaceType.CIEXYZ): (RGBToCIEXYZ, ColorSpaceParameterType.NoParameters), 564 | (ColorSpaceType.CIEXYZ, ColorSpaceType.RGB): (CIEXYZToRGB, ColorSpaceParameterType.NoParameters), 565 | (ColorSpaceType.CIEXYZ, ColorSpaceType.CIELAB): (CIEXYZToCIELAB, ColorSpaceParameterType.Illuminant | ColorSpaceParameterType.Observer), 566 | (ColorSpaceType.CIELAB, ColorSpaceType.CIEXYZ): (CIELABToCIEXYZ, ColorSpaceParameterType.Illuminant | ColorSpaceParameterType.Observer), 567 | (ColorSpaceType.CIELAB, ColorSpaceType.CIELCH): (CartesianToPolar, ColorSpaceParameterType.NoParameters), 568 | (ColorSpaceType.CIELCH, ColorSpaceType.CIELAB): (PolarToCartesian, ColorSpaceParameterType.NoParameters), 569 | (ColorSpaceType.CIEXYZ, ColorSpaceType.OKLAB): (CIEXYZToOKLAB, ColorSpaceParameterType.NoParameters), 570 | (ColorSpaceType.OKLAB, ColorSpaceType.CIEXYZ): (OKLABToCIEXYZ, ColorSpaceParameterType.NoParameters), 571 | (ColorSpaceType.OKLAB, ColorSpaceType.OKLCH): (CartesianToPolar, ColorSpaceParameterType.NoParameters), 572 | (ColorSpaceType.OKLCH, ColorSpaceType.OKLAB): (PolarToCartesian, ColorSpaceParameterType.NoParameters), 573 | (ColorSpaceType.CIEXYZ, ColorSpaceType.HunterLAB): (CIEXYZToHunterLAB, ColorSpaceParameterType.Illuminant | ColorSpaceParameterType.Observer), 574 | (ColorSpaceType.HunterLAB, ColorSpaceType.CIEXYZ): (HunterLABToCIEXYZ, ColorSpaceParameterType.Illuminant | ColorSpaceParameterType.Observer), 575 | (ColorSpaceType.HunterLAB, ColorSpaceType.HunterLCH): (CartesianToPolar, ColorSpaceParameterType.NoParameters), 576 | (ColorSpaceType.HunterLCH, ColorSpaceType.HunterLAB): (PolarToCartesian, ColorSpaceParameterType.NoParameters), 577 | (ColorSpaceType.RGB, ColorSpaceType.HSL): (RGBToHSL, ColorSpaceParameterType.NoParameters), 578 | (ColorSpaceType.HSL, ColorSpaceType.RGB): (HSLToRGB, ColorSpaceParameterType.NoParameters), 579 | (ColorSpaceType.CIEXYZ, ColorSpaceType.CIE1931Yxy): (CIEXYZToCIE1931Yxy, ColorSpaceParameterType.NoParameters), 580 | (ColorSpaceType.CIE1931Yxy, ColorSpaceType.CIEXYZ): (CIE1931YxyToCIEXYZ, ColorSpaceParameterType.NoParameters), 581 | (ColorSpaceType.RGB, ColorSpaceType.YCbCr): (RGBToYCbCr, ColorSpaceParameterType.NoParameters), 582 | (ColorSpaceType.YCbCr, ColorSpaceType.RGB): (YCbCrToRGB, ColorSpaceParameterType.NoParameters), 583 | (ColorSpaceType.RGB, ColorSpaceType.HSV): (RGBToHSV, ColorSpaceParameterType.NoParameters), 584 | (ColorSpaceType.HSV, ColorSpaceType.RGB): (HSVToRGB, ColorSpaceParameterType.NoParameters), 585 | (ColorSpaceType.CIEXYZ, ColorSpaceType.CIELuv): (CIEXYZToCIELuv, ColorSpaceParameterType.Illuminant | ColorSpaceParameterType.Observer), 586 | (ColorSpaceType.CIELuv, ColorSpaceType.CIEXYZ): (CIELuvToCIEXYZ, ColorSpaceParameterType.Illuminant | ColorSpaceParameterType.Observer), 587 | (ColorSpaceType.CIEXYZ, ColorSpaceType.AdobeRGB): (CIEXYZToAdobeRGB, ColorSpaceParameterType.NoParameters), 588 | (ColorSpaceType.AdobeRGB, ColorSpaceType.CIEXYZ): (AdobeRGBToCIEXYZ, ColorSpaceParameterType.NoParameters), 589 | (ColorSpaceType.CIEXYZ, ColorSpaceType.ACESAP1): (CIEXYZToACESAP1, ColorSpaceParameterType.NoParameters), 590 | (ColorSpaceType.ACESAP1, ColorSpaceType.CIEXYZ): (ACESAP1ToCIEXYZ, ColorSpaceParameterType.NoParameters), 591 | } 592 | 593 | Graph: DiGraph = DiGraph(Edges.keys()) 594 | 595 | DijkstraCache: dict[tuple[ColorSpaceType, ColorSpaceType], Any] = {} 596 | 597 | @staticmethod 598 | def convert( 599 | color: vec3, 600 | fromColorSpace: ColorSpaceType, 601 | toColorSpace: ColorSpaceType, 602 | **kwargs, 603 | ) -> vec3: 604 | if (fromColorSpace, toColorSpace) in ColorSpace.DijkstraCache.keys(): 605 | path = ColorSpace.DijkstraCache[fromColorSpace, toColorSpace] 606 | else: 607 | path = shortest_path(ColorSpace.Graph, fromColorSpace, toColorSpace) 608 | ColorSpace.DijkstraCache[fromColorSpace, toColorSpace] = path 609 | result: vec3 = color 610 | for nodeIndex in range(len(path) - 1): 611 | edge = path[nodeIndex], path[nodeIndex + 1] 612 | transform, parameterTypes = ColorSpace.Edges[edge] 613 | parameters = [] 614 | if ColorSpaceParameterType.Illuminant in parameterTypes and \ 615 | ColorSpaceParameterType.Observer in parameterTypes: 616 | parameters.append(ColorSpace.Tristimuli[kwargs['observer']][kwargs['illuminant']]) 617 | result = transform(result, *parameters) 618 | return result 619 | 620 | @staticmethod 621 | def SortByCIEH(colors: list[vec3], colorSpace: ColorSpaceType = ColorSpaceType.RGB): 622 | first = sorted(colors, key=lambda color: length(color))[0] 623 | # first = colors[0] 624 | # colors = colors[1:] 625 | converted = list(map( 626 | lambda color: ColorSpace.convert( 627 | color, 628 | colorSpace, 629 | ColorSpaceType.CIELCH, 630 | observer=Observer.TwoDegreesCIE1931, 631 | illuminant=Illuminant.D65, 632 | ), 633 | colors, 634 | )) 635 | indices = sorted( 636 | range(len(colors)), 637 | key=lambda index: converted[index].z, 638 | ) 639 | result = list(map( 640 | lambda index: colors[index], 641 | indices, 642 | )) 643 | while result[0] != first: 644 | result = result[1:] + [result[0]] 645 | return result 646 | 647 | if __name__ == '__main__': 648 | from matplotlib import pyplot 649 | labeldict = {} 650 | for colorSpaceType in ColorSpaceType: 651 | labeldict[colorSpaceType] = colorSpaceType.name 652 | draw(ColorSpace.Graph, with_labels=True, labels=labeldict) 653 | pyplot.draw() 654 | pyplot.show() 655 | 656 | result = ColorSpace.convert( 657 | vec3(.3, .5, .8), 658 | ColorSpaceType.SRGB, 659 | ColorSpaceType.CIEXYZ, 660 | # observer=Observer.TenDegreesCIE1964, 661 | # illuminant=Illuminant.D75, 662 | ) 663 | print("result:", result) 664 | original = ColorSpace.convert( 665 | result, 666 | ColorSpaceType.CIEXYZ, 667 | ColorSpaceType.RGB, 668 | # observer=Observer.TenDegreesCIE1964, 669 | # illuminant=Illuminant.D75, 670 | ) 671 | print("original:", original) 672 | -------------------------------------------------------------------------------- /imagecolorpicker/controller.py: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | Self, 3 | Optional, 4 | ) 5 | from PyQt6.QtWidgets import ( 6 | QApplication, 7 | QFileDialog, 8 | QMenu, 9 | ) 10 | from PyQt6.QtGui import ( 11 | QIcon, 12 | QColor, 13 | QImage, 14 | QAction, 15 | QGuiApplication, 16 | ) 17 | from PyQt6.QtCore import ( 18 | QModelIndex, 19 | Qt, 20 | QPoint, 21 | QPointF, 22 | QDir, 23 | QFileInfo, 24 | QSettings, 25 | ) 26 | from sys import argv 27 | from pathlib import Path 28 | from importlib.resources import files 29 | import imagecolorpicker 30 | from .widgets.mainwindow.mainwindow import MainWindow 31 | from .widgets.pickablecolorlabel.pickablecolorlabel import PickableColorLabel 32 | from .cmapfile import CMapFile 33 | from .colorgradient import ( 34 | DefaultGradient1, 35 | DefaultGradient2, 36 | ) 37 | from .model.gradientlistmodel import ( 38 | GradientListModel, 39 | ) 40 | from .delegate.gradientlistdelegate import GradientListDelegate 41 | from .widgets import pickablecolorlabel 42 | from .model.imagelistmodel import ImageListModel 43 | from .delegate.imagelistdelegate import ImageListDelegate 44 | from .colorgradient import ColorGradient 45 | from .model.gradientpropertymodel import GradientPropertyModel 46 | from .delegate.gradientpropertydelegate import GradientPropertyDelegate 47 | from .model.gradientpropertyrowtype import GradientPropertyRowType 48 | from .model.gradientpropertycolumntype import GradientPropertyColumnType 49 | from .widgets.gradientwidget import gradientwidget 50 | from .model.gradientcolormodel import GradientColorModel 51 | from .delegate.gradientcolordelegate import GradientColorDelegate 52 | from copy import deepcopy 53 | from glm import vec3 54 | from uuid import uuid4 55 | from .colorspace import ColorSpaceType, ColorSpace 56 | from random import uniform 57 | from tempfile import TemporaryDirectory 58 | from Pylette import extract_colors 59 | from rtoml import loads, dumps 60 | from .language import Language 61 | from .representation import Representation 62 | from .model.settingsmodel import SettingsModel 63 | from .delegate.settingsdelegate import SettingsDelegate 64 | from .export import Export 65 | from .optimizationmodel import OptimizationModel 66 | 67 | 68 | class Controller: 69 | def __init__( 70 | self: Self, 71 | ) -> None: 72 | QApplication.setOrganizationName("Team210 Demoscene Productions") 73 | QApplication.setApplicationName("ImageColorPicker") 74 | QApplication.setWindowIcon(QIcon(str(files(imagecolorpicker) / 'team210.ico'))) 75 | 76 | self._mainWindow: MainWindow = MainWindow() 77 | self._mainWindow.quitRequested.connect(lambda: QApplication.exit(0)) 78 | self._mainWindow.show() 79 | 80 | self._cmapFile: CMapFile = CMapFile( 81 | images=[ 82 | QImage(str(files(pickablecolorlabel) / PickableColorLabel.DefaultImage)), 83 | ], 84 | gradients=[ 85 | DefaultGradient1, 86 | DefaultGradient2, 87 | ], 88 | ) 89 | 90 | self._gradientListModel: GradientListModel = GradientListModel() 91 | self._gradientListModel.currentGradientChanged.connect(self._currentGradientChanged) 92 | self._gradientListModel.dataChanged.connect(self._gradientListDataChanged) 93 | 94 | self._gradientListDelegate: GradientListDelegate = GradientListDelegate() 95 | 96 | self._previewGradientListmodel: GradientListModel = GradientListModel() 97 | self._mainWindow._ui.gradientPreviewTableView.setModel(self._previewGradientListmodel) 98 | self._mainWindow._ui.gradientPreviewTableView.setItemDelegate(self._gradientListDelegate) 99 | 100 | self._mainWindow._ui.gradientTableView.setItemDelegate(self._gradientListDelegate) 101 | self._mainWindow._ui.gradientTableView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) 102 | self._mainWindow._ui.gradientTableView.setModel(self._gradientListModel) 103 | self._mainWindow._ui.gradientTableView.customContextMenuRequested.connect(self._gradientListContextMenuRequested) 104 | 105 | self._imageListModel: ImageListModel = ImageListModel() 106 | 107 | self._imageListDelegate: ImageListDelegate = ImageListDelegate() 108 | 109 | self._mainWindow._ui.imageListView.setItemDelegate(self._imageListDelegate) 110 | self._mainWindow._ui.imageListView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) 111 | self._mainWindow._ui.imageListView.setModel(self._imageListModel) 112 | self._mainWindow._ui.imageListView.customContextMenuRequested.connect(self._imageListContextMenuRequested) 113 | 114 | self._gradientPropertyModel: GradientPropertyModel = GradientPropertyModel() 115 | self._gradientPropertyModel.dataChanged.connect(self._gradientPropertyChanged) 116 | 117 | self._gradientPropertyDelegate: GradientPropertyDelegate = GradientPropertyDelegate() 118 | 119 | self._mainWindow._ui.gradientPropertyTableView.setItemDelegate(self._gradientPropertyDelegate) 120 | self._mainWindow._ui.gradientPropertyTableView.setModel(self._gradientPropertyModel) 121 | 122 | self._gradientColorModel: GradientColorModel = GradientColorModel() 123 | self._gradientColorModel.dataChanged.connect(self._gradientColorDataChanged) 124 | 125 | self._gradientColorDelegate: GradientColorDelegate = GradientColorDelegate() 126 | 127 | self._mainWindow._ui.gradientColorTableView.setModel(self._gradientColorModel) 128 | self._mainWindow._ui.gradientColorTableView.setItemDelegate(self._gradientColorDelegate) 129 | self._mainWindow._ui.gradientColorTableView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) 130 | self._mainWindow._ui.gradientColorTableView.customContextMenuRequested.connect(self._colorTableContextMenuRequested) 131 | 132 | self._mainWindow._ui.picker.imageChanged.connect(self._pickerImageChanged) 133 | 134 | self._mainWindow._ui.actionOpen.triggered.connect(self._openFile) 135 | self._mainWindow._ui.actionSave.triggered.connect(self._saveFile) 136 | 137 | self._mainWindow._ui.actionAdd_Gradient.triggered.connect(self._addGradient) 138 | self._mainWindow._ui.actionRemove_Current_Gradient.triggered.connect(self._removeCurrentGradient) 139 | 140 | self._mainWindow._ui.actionAdd_Color.triggered.connect(self._addColor) 141 | self._mainWindow._ui.actionRemove_Color.triggered.connect(self._removeColor) 142 | 143 | self._mainWindow._ui.actionExtract_Palette.triggered.connect(self.extractPalette) 144 | 145 | self._mainWindow._ui.actionImport_Palette.triggered.connect(self._importPalette) 146 | self._mainWindow._ui.actionExport_Palette.triggered.connect(self._exportPalette) 147 | 148 | self._mainWindow._ui.actionCopy.triggered.connect(self._copy) 149 | 150 | self.updateFromCmapFile() 151 | 152 | self._settingsModel: SettingsModel = SettingsModel() 153 | self._mainWindow._ui.settingsTableView.setModel(self._settingsModel) 154 | 155 | self._settingsDelegate: SettingsDelegate = SettingsDelegate() 156 | self._mainWindow._ui.settingsTableView.setItemDelegate(self._settingsDelegate) 157 | 158 | self._mainWindow.cmapPasted.connect(self._cmapPasted) 159 | 160 | def _copy(self: Self) -> None: 161 | clipboard = QGuiApplication.clipboard() 162 | clipboard.setText(Export.Export(self._settingsModel.language, self._settingsModel.representation, self._gradientListModel._gradientList[self._gradientListModel._currentIndex], self._gradientListModel._gradientList, vec3(*self._mainWindow._ui.picker.components))) 163 | 164 | def _exportPalette(self: Self) -> None: 165 | settings = QSettings() 166 | filename, _ = QFileDialog.getSaveFileName( 167 | None, 168 | 'Save palette...', 169 | settings.value("save_palette_path", QDir.homePath()), 170 | "All Supported Files (*.toml);;TOML project files (*.toml)", 171 | ) 172 | 173 | if filename == "": 174 | return 175 | 176 | if len(Path(filename).suffixes) == 0: 177 | filename += '.toml' 178 | 179 | file_info = QFileInfo(filename) 180 | settings.setValue("save_palette_path", file_info.absoluteDir().absolutePath()) 181 | suffix: str = Path(filename).suffix 182 | 183 | if suffix == '.toml': 184 | Path(filename).write_text(dumps({ 185 | 'palette': { 186 | 'name': self._cmapFile._gradients[self._gradientListModel._currentIndex]._name, 187 | 'colors': list(map( 188 | lambda color: [color.x, color.y, color.z], 189 | self._cmapFile._gradients[self._gradientListModel._currentIndex]._colors, 190 | )), 191 | }, 192 | }, pretty=True)) 193 | 194 | def _importPalette(self: Self) -> None: 195 | settings = QSettings() 196 | filename, _ = QFileDialog.getOpenFileName( 197 | None, 198 | 'Load palette...', 199 | settings.value("load_palette_path", QDir.homePath()), 200 | "All Supported Files (*.toml);;TOML project files (*.toml)", 201 | ) 202 | 203 | if filename == "": 204 | return 205 | 206 | if len(Path(filename).suffixes) == 0: 207 | filename += '.toml' 208 | 209 | file_info = QFileInfo(filename) 210 | settings.setValue("load_palette_path", file_info.absoluteDir().absolutePath()) 211 | suffix: str = Path(filename).suffix 212 | 213 | if suffix == '.toml': 214 | paletteObject: dict = loads(Path(filename).read_text()) 215 | self._cmapFile._gradients[self._gradientListModel._currentIndex]._colors = ColorSpace.SortByCIEH(list(map( 216 | lambda colorList: vec3(*colorList), 217 | paletteObject['palette']['colors'], 218 | ))) 219 | index = self._gradientListModel._currentIndex 220 | self.updateFromCmapFile() 221 | self._gradientListModel.changeCurrent(index) 222 | 223 | def _removeColor(self: Self) -> None: 224 | if len(self._cmapFile._gradients[self._gradientListModel._currentIndex]._colors) <= 1: 225 | return 226 | del self._cmapFile._gradients[self._gradientListModel._currentIndex]._colors[-1] 227 | index = min( 228 | self._gradientListModel._currentIndex, 229 | len(self._cmapFile._gradients[self._gradientListModel._currentIndex]._colors) - 1, 230 | ) 231 | self.updateFromCmapFile() 232 | self._gradientListModel.changeCurrent(index) 233 | 234 | def _addColor(self: Self) -> None: 235 | self._cmapFile._gradients[self._gradientListModel._currentIndex]._colors.append(vec3(1,1,1)) 236 | index = self._gradientListModel._currentIndex 237 | self.updateFromCmapFile() 238 | self._gradientListModel.changeCurrent(index) 239 | 240 | def _addGradient(self: Self) -> None: 241 | self._cmapFile._gradients.append(ColorGradient( 242 | str(uuid4()), 243 | 7, 244 | ColorSpaceType.CIELAB, 245 | ColorSpaceType.CIELAB, 246 | ColorSpace.SortByCIEH([ 247 | vec3(uniform(0,.1), uniform(0,.1), uniform(0,.1)), 248 | vec3(uniform(0,1), uniform(0,1), uniform(0,1)), 249 | vec3(uniform(0,1), uniform(0,1), uniform(0,1)), 250 | vec3(uniform(0,1), uniform(0,1), uniform(0,1)), 251 | vec3(uniform(0,1), uniform(0,1), uniform(0,1)), 252 | vec3(uniform(0,1), uniform(0,1), uniform(0,1)), 253 | vec3(uniform(0,1), uniform(0,1), uniform(0,1)), 254 | vec3(uniform(0,1), uniform(0,1), uniform(0,1)), 255 | ]), 256 | )) 257 | self.updateFromCmapFile() 258 | self._gradientListModel.changeCurrent(len(self._cmapFile._gradients) - 1) 259 | 260 | def _removeCurrentGradient(self: Self) -> None: 261 | del self._cmapFile._gradients[self._gradientListModel._currentIndex] 262 | self.updateFromCmapFile() 263 | 264 | def updateFromCmapFile( 265 | self: Self, 266 | ) -> None: 267 | self._gradientListModel.loadGradientList(self._cmapFile._gradients) 268 | self._imageListModel.loadImageList(self._cmapFile._images) 269 | self._updateGradientPreview() 270 | self._gradientPropertyModel.loadGradient(self._cmapFile._gradients[self._gradientListModel._currentIndex]) 271 | self._gradientColorModel.loadGradient(self._cmapFile._gradients[self._gradientListModel._currentIndex]) 272 | 273 | def _gradientListContextMenuRequested(self: Self, position: QPoint) -> None: 274 | index: QModelIndex = self._mainWindow._ui.gradientTableView.indexAt(position) 275 | if index.isValid(): 276 | self._gradientListModel.changeCurrent(index.row()) 277 | else: 278 | self._mainWindow._ui.menuGradient.move(self._mainWindow._ui.gradientTableView.mapToGlobal(position)) 279 | self._mainWindow._ui.menuGradient.show() 280 | 281 | def _colorTableContextMenuRequested(self: Self, position: QPoint) -> None: 282 | index: QModelIndex = self._mainWindow._ui.gradientColorTableView.indexAt(position) 283 | if index.isValid(): 284 | self._gradientColorModel.updateColor(index.row(), self._mainWindow._ui.picker._color) 285 | 286 | def _imageListContextMenuRequested(self: Self, position: QPoint) -> None: 287 | index: QModelIndex = self._mainWindow._ui.imageListView.indexAt(position) 288 | if index.isValid(): 289 | self._imageListModel.changeCurrent(index.row()) 290 | self._mainWindow._ui.picker.setImage(self._imageListModel._imageList[index.row()]) 291 | 292 | def _updateGradientPreview(self: Self) -> None: 293 | previewGradientList: list[ColorGradient] = list(map( 294 | lambda colorspaces: self._gradientListModel.copyCurrentGradientWithColorSpaces(*colorspaces), 295 | self._cmapFile._previewColorSpaces, 296 | )) 297 | self._previewGradientListmodel.loadGradientList(previewGradientList) 298 | 299 | def _currentGradientChanged(self: Self, gradient: ColorGradient) -> None: 300 | self._gradientPropertyModel.loadGradient(gradient) 301 | self._gradientColorModel.loadGradient(gradient) 302 | 303 | def _gradientPropertyChanged(self: Self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: list[Qt.ItemDataRole]): 304 | if Qt.ItemDataRole.EditRole in roles: 305 | self._gradientListModel.updateCurrentGradient() 306 | self._updateGradientPreview() 307 | 308 | def _gradientListDataChanged(self: Self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: list[Qt.ItemDataRole]): 309 | if Qt.ItemDataRole.EditRole in roles: 310 | index: QModelIndex = self._gradientPropertyModel.index( 311 | self._gradientPropertyModel._rowList.index(GradientPropertyRowType.Name), 312 | self._gradientPropertyModel._columnList.index(GradientPropertyColumnType.Value), 313 | ) 314 | self._gradientPropertyModel.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 315 | self._updateGradientPreview() 316 | 317 | def _gradientColorDataChanged(self: Self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: list[Qt.ItemDataRole]): 318 | if Qt.ItemDataRole.EditRole in roles: 319 | self._gradientListModel.updateCurrentGradient() 320 | self._updateGradientPreview() 321 | 322 | def _pickerImageChanged(self: Self, image: QImage) -> None: 323 | self._cmapFile._images.append(image) 324 | self._imageListModel.loadImageList(self._cmapFile._images) 325 | 326 | def _openFile(self: Self) -> None: 327 | settings = QSettings() 328 | filename, _ = QFileDialog.getOpenFileName( 329 | None, 330 | 'Open cmap...', 331 | settings.value("open_path", QDir.homePath()), 332 | 'TOML Files (*.toml)', 333 | ) 334 | 335 | if filename == "": 336 | return 337 | 338 | file_info = QFileInfo(filename) 339 | settings.setValue("open_path", file_info.absoluteDir().absolutePath()) 340 | 341 | self._cmapFile.load(Path(filename)) 342 | self.updateFromCmapFile() 343 | 344 | def _saveFile(self: Self) -> None: 345 | settings = QSettings() 346 | filename, _ = QFileDialog.getSaveFileName( 347 | None, 348 | 'Save cmap...', 349 | settings.value("save_path", QDir.homePath()), 350 | 'TOML Files (*.toml)', 351 | ) 352 | 353 | if filename == "": 354 | return 355 | 356 | file_info = QFileInfo(filename) 357 | settings.setValue("save_path", file_info.absoluteDir().absolutePath()) 358 | 359 | self._cmapFile.save(Path(filename)) 360 | 361 | def extractPalette(self: Self) -> None: 362 | with TemporaryDirectory() as directory: 363 | imagePath: Path = Path(directory) / 'image.jpg' 364 | self._mainWindow._ui.picker._image.save(str(imagePath)) 365 | palette = extract_colors( 366 | image=str(Path(directory) / 'image.jpg'), 367 | palette_size=len(self._cmapFile._gradients[self._gradientListModel._currentIndex]._colors), 368 | resize=False, 369 | # mode='MC', 370 | mode='KM', 371 | sort_mode='luminance', 372 | ) 373 | palette = list(map( 374 | lambda color: vec3(*color.rgb) / 255., 375 | palette, 376 | )) 377 | palette = ColorSpace.SortByCIEH(palette) 378 | self._cmapFile._gradients[self._gradientListModel._currentIndex]._colors = palette 379 | index = self._gradientListModel._currentIndex 380 | self.updateFromCmapFile() 381 | self._gradientListModel.changeCurrent(index) 382 | 383 | def _cmapPasted(self: Self, cmap: list[vec3]) -> None: 384 | print(cmap) 385 | colors = [] 386 | for colorIndex in range(self._cmapFile._gradients[self._gradientListModel._currentIndex].colorCount): 387 | colors.append(vec3( 388 | OptimizationModel.Polynomial( 389 | colorIndex / self._cmapFile._gradients[self._gradientListModel._currentIndex].colorCount, 390 | *map( 391 | lambda cmapEntry: cmapEntry.x, 392 | cmap, 393 | ), 394 | ), 395 | OptimizationModel.Polynomial( 396 | colorIndex / self._cmapFile._gradients[self._gradientListModel._currentIndex].colorCount, 397 | *map( 398 | lambda cmapEntry: cmapEntry.y, 399 | cmap, 400 | ), 401 | ), 402 | OptimizationModel.Polynomial( 403 | colorIndex / self._cmapFile._gradients[self._gradientListModel._currentIndex].colorCount, 404 | *map( 405 | lambda cmapEntry: cmapEntry.z, 406 | cmap, 407 | ), 408 | ), 409 | )) 410 | # colors = ColorSpace.SortByCIEH(colors) 411 | self._cmapFile._gradients[self._gradientListModel._currentIndex]._colors = colors 412 | index = self._gradientListModel._currentIndex 413 | self.updateFromCmapFile() 414 | self._gradientListModel.changeCurrent(index) 415 | 416 | def startApplication( 417 | self: Self, 418 | ) -> None: 419 | QApplication.exec() 420 | -------------------------------------------------------------------------------- /imagecolorpicker/delegate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeStahL/ImageColorPicker/0b57bfd839a0dea4a59d824df7cda7a45e7727c9/imagecolorpicker/delegate/__init__.py -------------------------------------------------------------------------------- /imagecolorpicker/delegate/gradientcolordelegate.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import ( 2 | QObject, 3 | QModelIndex, 4 | QAbstractItemModel, 5 | pyqtSignal, 6 | Qt, 7 | ) 8 | from PyQt6.QtWidgets import ( 9 | QStyleOptionViewItem, 10 | QStyledItemDelegate, 11 | QWidget, 12 | QComboBox, 13 | QColorDialog, 14 | ) 15 | from PyQt6.QtGui import ( 16 | QPainter, 17 | QColor, 18 | ) 19 | from typing import ( 20 | Self, 21 | Optional, 22 | ) 23 | from ..model.gradientcolorcolumntype import GradientColorColumnType 24 | 25 | 26 | class GradientColorDelegate(QStyledItemDelegate): 27 | def __init__( 28 | self: Self, 29 | parent: Optional[QObject] = None, 30 | ) -> None: 31 | super().__init__(parent) 32 | 33 | def createEditor( 34 | self: Self, 35 | parent: Optional[QWidget], 36 | option: QStyleOptionViewItem, 37 | index: QModelIndex, 38 | ) -> Optional[QWidget]: 39 | columnType: GradientColorColumnType = index.model()._columnList[index.column()] 40 | if columnType == GradientColorColumnType.Name: 41 | return QColorDialog() 42 | return super().createEditor(parent, option, index) 43 | 44 | def setEditorData( 45 | self: Self, 46 | editor: QWidget, 47 | index: QModelIndex, 48 | ) -> None: 49 | columnType: GradientColorColumnType = index.model()._columnList[index.column()] 50 | if columnType == GradientColorColumnType.Name: 51 | editor.setCurrentColor(QColor(index.data())) 52 | else: 53 | super().setEditorData(editor, index) 54 | 55 | def setModelData( 56 | self: Self, 57 | editor: QWidget, 58 | model: QAbstractItemModel, 59 | index: QModelIndex, 60 | ) -> None: 61 | columnType: GradientColorColumnType = model._columnList[index.column()] 62 | if columnType == GradientColorColumnType.Name: 63 | model.setData(index, editor.currentColor().name(), Qt.ItemDataRole.EditRole) 64 | else: 65 | super().setModelData(editor, model, index)() 66 | -------------------------------------------------------------------------------- /imagecolorpicker/delegate/gradientlistdelegate.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import ( 2 | QObject, 3 | QModelIndex, 4 | QAbstractItemModel, 5 | pyqtSignal, 6 | ) 7 | from PyQt6.QtWidgets import ( 8 | QStyleOptionViewItem, 9 | QStyledItemDelegate, 10 | QWidget, 11 | QComboBox, 12 | ) 13 | from PyQt6.QtGui import ( 14 | QPainter, 15 | ) 16 | from typing import ( 17 | Self, 18 | Optional, 19 | ) 20 | from ..model.gradientlistcolumntype import ( 21 | GradientListColumnType, 22 | ) 23 | from ..colorgradient import ColorGradient, FitModel 24 | from ..widgets.gradientwidget.gradientwidget import GradientWidget 25 | 26 | 27 | class GradientListDelegate(QStyledItemDelegate): 28 | def __init__( 29 | self: Self, 30 | parent: Optional[QObject] = None, 31 | ) -> None: 32 | super().__init__(parent) 33 | 34 | def paint( 35 | self: Self, 36 | painter: Optional[QPainter], 37 | option: QStyleOptionViewItem, 38 | index: QModelIndex, 39 | ) -> None: 40 | if index.model()._columnList[index.column()] == GradientListColumnType.Preview: 41 | data: Optional[ColorGradient] = index.data() 42 | if data is not None: 43 | self._gradientPreview: GradientWidget = GradientWidget(data) 44 | self._gradientPreview.paint(option.rect, painter) 45 | return 46 | return super().paint(painter, option, index) 47 | 48 | def createEditor( 49 | self: Self, 50 | parent: Optional[QWidget], 51 | option: QStyleOptionViewItem, 52 | index: QModelIndex, 53 | ) -> Optional[QWidget]: 54 | if index.model()._columnList[index.column()] == GradientListColumnType.Model: 55 | comboBox: QComboBox = QComboBox(parent) 56 | comboBox.addItems([fitModel.name for fitModel in FitModel]) 57 | comboBox.setCurrentText(index.data()) 58 | return comboBox 59 | return super().createEditor(parent, option, index) 60 | -------------------------------------------------------------------------------- /imagecolorpicker/delegate/gradientpropertydelegate.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import ( 2 | QObject, 3 | QModelIndex, 4 | QAbstractItemModel, 5 | pyqtSignal, 6 | ) 7 | from PyQt6.QtWidgets import ( 8 | QStyleOptionViewItem, 9 | QStyledItemDelegate, 10 | QWidget, 11 | QComboBox, 12 | ) 13 | from PyQt6.QtGui import ( 14 | QPainter, 15 | ) 16 | from typing import ( 17 | Self, 18 | Optional, 19 | ) 20 | from ..model.gradientpropertycolumntype import GradientPropertyColumnType 21 | from ..model.gradientpropertyrowtype import GradientPropertyRowType 22 | from ..colorgradient import ColorGradient, FitModel 23 | from ..widgets.gradientwidget.gradientwidget import GradientWidget 24 | from ..colorspace import ColorSpaceType 25 | from ..colorgradient import ( 26 | Observer, 27 | FitAlgorithm, 28 | FitModel, 29 | Illuminant, 30 | Wraparound, 31 | ) 32 | from enum import EnumMeta 33 | 34 | class GradientPropertyDelegate(QStyledItemDelegate): 35 | def __init__( 36 | self: Self, 37 | parent: Optional[QObject] = None, 38 | ) -> None: 39 | super().__init__(parent) 40 | 41 | def enumComboBox( 42 | self: Self, 43 | parent: Optional[QWidget], 44 | index: QModelIndex, 45 | enumType: EnumMeta, 46 | ) -> QComboBox: 47 | comboBox: QComboBox = QComboBox(parent) 48 | comboBox.addItems([element.name for element in enumType]) 49 | comboBox.setCurrentText(index.data()) 50 | return comboBox 51 | 52 | def createEditor( 53 | self: Self, 54 | parent: Optional[QWidget], 55 | option: QStyleOptionViewItem, 56 | index: QModelIndex, 57 | ) -> Optional[QWidget]: 58 | columnType: GradientPropertyColumnType = index.model()._columnList[index.column()] 59 | rowType: GradientPropertyRowType = index.model()._rowList[index.row()] 60 | if columnType == GradientPropertyColumnType.Value: 61 | if rowType == GradientPropertyRowType.Model: 62 | return self.enumComboBox(parent, index, FitModel) 63 | elif rowType == GradientPropertyRowType.Wraparound: 64 | return self.enumComboBox(parent, index, Wraparound) 65 | elif rowType in [ 66 | GradientPropertyRowType.WeightColorSpace, 67 | GradientPropertyRowType.MixColorSpace, 68 | ]: 69 | return self.enumComboBox(parent, index, ColorSpaceType) 70 | elif rowType == GradientPropertyRowType.Illuminant: 71 | return self.enumComboBox(parent, index, Illuminant) 72 | elif rowType == GradientPropertyRowType.Observer: 73 | return self.enumComboBox(parent, index, Observer) 74 | elif rowType == GradientPropertyRowType.FitAlgorithm: 75 | return self.enumComboBox(parent, index, FitAlgorithm) 76 | return super().createEditor(parent, option, index) 77 | -------------------------------------------------------------------------------- /imagecolorpicker/delegate/imagelistdelegate.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import ( 2 | QObject, 3 | QModelIndex, 4 | QAbstractItemModel, 5 | pyqtSignal, 6 | ) 7 | from PyQt6.QtWidgets import ( 8 | QStyleOptionViewItem, 9 | QStyledItemDelegate, 10 | QWidget, 11 | QComboBox, 12 | ) 13 | from PyQt6.QtGui import ( 14 | QPainter, 15 | QImage, 16 | ) 17 | from typing import ( 18 | Self, 19 | Optional, 20 | ) 21 | 22 | class ImageListDelegate(QStyledItemDelegate): 23 | def __init__( 24 | self: Self, 25 | parent: Optional[QObject] = None, 26 | ) -> None: 27 | super().__init__(parent) 28 | 29 | def paint( 30 | self: Self, 31 | painter: Optional[QPainter], 32 | option: QStyleOptionViewItem, 33 | index: QModelIndex, 34 | ) -> None: 35 | image: QImage = index.data() 36 | painter.drawImage(option.rect, image, image.rect()) 37 | -------------------------------------------------------------------------------- /imagecolorpicker/delegate/settingsdelegate.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import ( 2 | QObject, 3 | QModelIndex, 4 | ) 5 | from PyQt6.QtWidgets import ( 6 | QStyleOptionViewItem, 7 | QStyledItemDelegate, 8 | QWidget, 9 | QComboBox, 10 | ) 11 | from typing import ( 12 | Self, 13 | Optional, 14 | ) 15 | from ..model.settingscolumntype import SettingsColumnType 16 | from ..model.settingsrowtype import SettingsRowType 17 | from enum import EnumMeta 18 | from ..language import Language 19 | from ..representation import Representation 20 | 21 | class SettingsDelegate(QStyledItemDelegate): 22 | def __init__( 23 | self: Self, 24 | parent: Optional[QObject] = None, 25 | ) -> None: 26 | super().__init__(parent) 27 | 28 | def enumComboBox( 29 | self: Self, 30 | parent: Optional[QWidget], 31 | index: QModelIndex, 32 | enumType: EnumMeta, 33 | ) -> QComboBox: 34 | comboBox: QComboBox = QComboBox(parent) 35 | comboBox.addItems([element.name for element in enumType]) 36 | comboBox.setCurrentText(index.data()) 37 | return comboBox 38 | 39 | def createEditor( 40 | self: Self, 41 | parent: Optional[QWidget], 42 | option: QStyleOptionViewItem, 43 | index: QModelIndex, 44 | ) -> Optional[QWidget]: 45 | columnType: SettingsColumnType = index.model()._columnList[index.column()] 46 | rowType: SettingsRowType = index.model()._rowList[index.row()] 47 | if columnType == SettingsColumnType.Value: 48 | if rowType == SettingsRowType.CopyLanguage: 49 | return self.enumComboBox(parent, index, Language) 50 | elif rowType == SettingsRowType.CopyRepresentation: 51 | return self.enumComboBox(parent, index, Representation) 52 | return super().createEditor(parent, option, index) 53 | -------------------------------------------------------------------------------- /imagecolorpicker/export.py: -------------------------------------------------------------------------------- 1 | from .representation import Representation 2 | from .language import Language 3 | from glm import vec3 4 | from .colorgradient import ColorGradient, DefaultGradient1, DefaultGradient2, FitModel 5 | from typing import ( 6 | Callable, 7 | Union, 8 | ByteString, 9 | Optional, 10 | ) 11 | from re import sub 12 | from construct import ( 13 | Float16l, 14 | Array, 15 | Subconstruct, 16 | Struct, 17 | ) 18 | from glm import ceil, sqrt 19 | from functools import reduce 20 | from PyQt6.QtGui import QColor 21 | 22 | 23 | class Export: 24 | @staticmethod 25 | def NearestSquareRoot(number: int) -> int: 26 | return int(ceil(sqrt(float(number)))) 27 | 28 | @staticmethod 29 | def MakeIdentifier(name: str) -> str: 30 | return sub('\W|^(?=\d)', '_', name) 31 | 32 | @staticmethod 33 | def AllCMapsSameWidthSameModel(gradientList: list[ColorGradient]): 34 | return len(set(map( 35 | lambda gradient: gradient._degree, 36 | gradientList, 37 | ))) == 1 and len(set(map( 38 | lambda gradient: gradient._model, 39 | gradientList, 40 | ))) == 1 41 | 42 | @staticmethod 43 | def Export( 44 | language: Language, 45 | representation: Representation, 46 | selectedGradient: ColorGradient, 47 | gradientList: list[ColorGradient], 48 | selectedColor: vec3, 49 | ) -> Optional[Union[ByteString, str]]: 50 | if language == Language.GLSL: 51 | if representation == Representation.ColorMap: 52 | cmap: list[vec3] = selectedGradient.coefficients 53 | if selectedGradient._model == FitModel.HornerPolynomial: 54 | openStack: str = '\n +t*('.join(map( 55 | lambda color: f'vec3({color.x:.2f}, {color.y:.2f}, {color.z:.2f})', 56 | cmap, 57 | )) 58 | closeStack: str = ')' * (len(cmap) - 1) 59 | return f'vec3 cmap_{Export.MakeIdentifier(selectedGradient._name)}(float t) {{\n return {openStack}\n {closeStack};\n}}\n' 60 | elif selectedGradient._model == FitModel.Trigonometric: 61 | return f'vec3 cmap_{Export.MakeIdentifier(selectedGradient._name)}(float t) {{\n return vec3({cmap[0].x:.4f}, {cmap[0].y:.4f}, {cmap[0].z:.4f}) + vec3({cmap[1].x:.4f}, {cmap[1].y:.4f}, {cmap[1].z:.4f}) * cos(2. * pi * (vec3({cmap[2].x:.4f}, {cmap[2].y:.4f}, {cmap[2].z:.4f}) * t + vec3({cmap[3].x:.4f}, {cmap[3].y:.4f}, {cmap[3].z:.4f})));\n}}\n' 62 | elif representation == Representation.Color3: 63 | return f'vec3({selectedColor.x:.2f}, {selectedColor.y:.2f}, {selectedColor.z:.2f})' 64 | elif representation == Representation.Color4: 65 | return f'vec4({selectedColor.x:.2f}, {selectedColor.y:.2f}, {selectedColor.z:.2f}, 1)' 66 | elif representation == Representation.NearestWeight: 67 | return f'{selectedGradient.nearestWeightInColorMap(selectedColor):.2f}' 68 | elif representation == Representation.Colors: 69 | colorSlide = ',\n '.join(list(map( 70 | lambda color: f'vec3({color.x:.2f}, {color.y:.2f}, {color.z:.2f})', 71 | selectedGradient._colors, 72 | ))) 73 | name: str = Export.MakeIdentifier(selectedGradient._name) 74 | return f'const int {name}_color_count = {selectedGradient.colorCount};\nconst vec3 {name}_colors[] = vec3[](\n {colorSlide}\n);\n' 75 | elif representation == Representation.Weights: 76 | weightSlide = ',\n '.join(list(map( 77 | lambda weight: f'{weight:.2f}', 78 | selectedGradient._weights, 79 | ))) 80 | name: str = Export.MakeIdentifier(selectedGradient._name) 81 | return f'const int {name}_weight_count = {len(selectedGradient._weights)};\nconst float {name}_weights[] = float[](\n {weightSlide}\n);\n' 82 | elif representation == Representation.ColorMaps: 83 | cmaps: list[list[vec3]] = reduce( 84 | lambda accumulator, addition: accumulator + addition, 85 | map( 86 | lambda gradient: gradient.coefficients, 87 | gradientList, 88 | ), 89 | ) 90 | coefficientSlide = ',\n '.join(list(map( 91 | lambda color: f'vec3({color.x:.2f}, {color.y:.2f}, {color.z:.2f})', 92 | cmaps, 93 | ))) 94 | if Export.AllCMapsSameWidthSameModel(gradientList): 95 | # Now we can use modulo, integer division etc. 96 | cmap = list(map( 97 | lambda index: f'all_cmap_coefficients[index * single_gradient_size + {index}]', 98 | range(gradientList[0]._degree), 99 | )) 100 | result = f'const int gradient_count = {len(gradientList)};\nconst int single_gradient_size = {gradientList[0]._degree};\nconst vec3 all_cmap_coefficients[] = vec3[](\n {coefficientSlide}\n);\n' 101 | if selectedGradient._model == FitModel.HornerPolynomial: 102 | return result + f'vec3 cmap(float t, int index) {{\n vec3 a = all_cmap_coefficients[(index + 1) * single_gradient_size - 1];\n for(int i = single_gradient_size - 2; i >= 0; --i)\n a = all_cmap_coefficients[index * single_gradient_size + i] + t * a;\n return a;\n}}\n' 103 | elif selectedGradient._model == FitModel.Trigonometric: 104 | return result + f'vec3 cmap(float t, int index) {{\n return vec3({cmap[0].x:.4f}, {cmap[0].y:.4f}, {cmap[0].z:.4f}) + vec3({cmap[1].x:.4f}, {cmap[1].y:.4f}, {cmap[1].z:.4f}) * cos(2. * pi * (vec3({cmap[2].x:.4f}, {cmap[2].y:.4f}, {cmap[2].z:.4f}) * t + vec3({cmap[3].x:.4f}, {cmap[3].y:.4f}, {cmap[3].z:.4f})));\n}}\n' 105 | else: 106 | cmap_offsets = [] 107 | offset = 0 108 | for gradient in gradientList: 109 | cmap_offsets.append(offset) 110 | offset += len(gradient.coefficients) 111 | cmap_offsets.append(len(cmaps)) 112 | offsetSlide = ',\n '.join(map( 113 | str, 114 | cmap_offsets, 115 | )) 116 | return f'const int gradient_count = {len(gradientList)};\nconst int all_cmap_coefficient_count = {len(cmaps)};\nconst int offsets_per_cmap[] = int[](\n {offsetSlide}\n);\nconst vec3 all_cmap_coefficients[] = vec3[](\n {coefficientSlide}\n);\nvec3 cmap(float t, int index) {{\n vec3 a = all_cmap_coefficients[offsets_per_cmap[index + 1] - 1];\n for(int i = offsets_per_cmap[index + 1] - 2; i >= offsets_per_cmap[index]; --i) {{\n a = all_cmap_coefficients[i] + t * a;\n }}\n return a;\n}}' 117 | elif language == Language.HLSL: 118 | if representation == Representation.ColorMap: 119 | cmap: list[vec3] = selectedGradient.coefficients 120 | if selectedGradient._model == FitModel.HornerPolynomial: 121 | openStack: str = '\n +t*('.join(map( 122 | lambda color: f'float3({color.x:.2f}, {color.y:.2f}, {color.z:.2f})', 123 | cmap, 124 | )) 125 | closeStack: str = ')' * (len(cmap) - 1) 126 | return f'float3 cmap_{Export.MakeIdentifier(selectedGradient._name)}(float t) {{\n return {openStack}\n {closeStack};\n}}\n' 127 | elif selectedGradient._model == FitModel.Trigonometric: 128 | return f'float3 cmap_{Export.MakeIdentifier(selectedGradient._name)}(float t) {{\n return float3({cmap[0].x:.4f}, {cmap[0].y:.4f}, {cmap[0].z:.4f}) + float3({cmap[1].x:.4f}, {cmap[1].y:.4f}, {cmap[1].z:.4f}) * cos(2. * pi * (float3({cmap[2].x:.4f}, {cmap[2].y:.4f}, {cmap[2].z:.4f}) * t + float3({cmap[3].x:.4f}, {cmap[3].y:.4f}, {cmap[3].z:.4f})));\n}}\n' 129 | elif representation == Representation.Color3: 130 | return f'float3({selectedColor.x:.2f}, {selectedColor.y:.2f}, {selectedColor.z:.2f})' 131 | elif representation == Representation.Color4: 132 | return f'float4({selectedColor.x:.2f}, {selectedColor.y:.2f}, {selectedColor.z:.2f}, 1)' 133 | elif representation == Representation.NearestWeight: 134 | return f'{selectedGradient.nearestWeightInColorMap(selectedColor):.2f}' 135 | elif representation == Representation.Colors: 136 | colorSlide = ',\n '.join(list(map( 137 | lambda color: f'float3({color.x:.2f}, {color.y:.2f}, {color.z:.2f})', 138 | selectedGradient._colors, 139 | ))) 140 | name: str = Export.MakeIdentifier(selectedGradient._name) 141 | return f'const int {name}_color_count = {selectedGradient.colorCount};\nconst float3 {name}_colors[] = {{\n {colorSlide}\n}};\n' 142 | elif representation == Representation.Weights: 143 | weightSlide = ',\n '.join(list(map( 144 | lambda weight: f'{weight:.2f}', 145 | selectedGradient._weights, 146 | ))) 147 | name: str = Export.MakeIdentifier(selectedGradient._name) 148 | return f'const int {name}_weight_count = {len(selectedGradient._weights)};\nconst float {name}_weights[] = {{\n {weightSlide}\n}};\n' 149 | elif representation == Representation.ColorMaps: 150 | cmaps: list[list[vec3]] = reduce( 151 | lambda accumulator, addition: accumulator + addition, 152 | map( 153 | lambda gradient: gradient.coefficients, 154 | gradientList, 155 | ), 156 | ) 157 | coefficientSlide = ',\n '.join(list(map( 158 | lambda color: f'float3({color.x:.2f}, {color.y:.2f}, {color.z:.2f})', 159 | cmaps, 160 | ))) 161 | if Export.AllCMapsSameWidthSameModel(gradientList): 162 | # Now we can use modulo, integer division etc. 163 | cmap = list(map( 164 | lambda index: f'all_cmap_coefficients[index * single_gradient_size + {index}]', 165 | range(gradientList[0]._degree), 166 | )) 167 | result = f'const int gradient_count = {len(gradientList)};\nconst int single_gradient_size = {gradientList[0]._degree};\nconst float3 all_cmap_coefficients[] = {{\n {coefficientSlide}\n}};\n' 168 | if selectedGradient._model == FitModel.HornerPolynomial: 169 | return result + f'float3 cmap(float t, int index) {{\n float3 a = all_cmap_coefficients[(index + 1) * single_gradient_size - 1];\n for(int i = single_gradient_size - 2; i >= 0; --i)\n a = all_cmap_coefficients[index * single_gradient_size + i] + t * a;\n return a;\n}}\n' 170 | elif selectedGradient._model == FitModel.Trigonometric: 171 | return result + f'float3 cmap(float t, int index) {{\n return float3({cmap[0].x:.4f}, {cmap[0].y:.4f}, {cmap[0].z:.4f}) + float3({cmap[1].x:.4f}, {cmap[1].y:.4f}, {cmap[1].z:.4f}) * cos(2. * pi * (float3({cmap[2].x:.4f}, {cmap[2].y:.4f}, {cmap[2].z:.4f}) * t + float3({cmap[3].x:.4f}, {cmap[3].y:.4f}, {cmap[3].z:.4f})));\n}}\n' 172 | else: 173 | cmap_offsets = [] 174 | offset = 0 175 | for gradient in gradientList: 176 | cmap_offsets.append(offset) 177 | offset += len(gradient.coefficients) 178 | cmap_offsets.append(len(cmaps)) 179 | offsetSlide = ',\n '.join(map( 180 | str, 181 | cmap_offsets, 182 | )) 183 | return f'const int gradient_count = {len(gradientList)};\nconst int all_cmap_coefficient_count = {len(cmaps)};\nconst int offsets_per_cmap[] = {{\n {offsetSlide}\n}};\nconst float3 all_cmap_coefficients[] = {{\n {coefficientSlide}\n}};\nfloat3 cmap(float t, int index) {{\n float3 a = all_cmap_coefficients[offsets_per_cmap[index + 1] - 1];\n for(int i = offsets_per_cmap[index + 1] - 2; i >= offsets_per_cmap[index]; --i) {{\n a = all_cmap_coefficients[i] + t * a;\n }}\n return a;\n}}' 184 | elif language == Language.CSS: 185 | if representation == Representation.ColorMap: 186 | colorStops = list(map( 187 | lambda amount: selectedGradient.evaluateFit(float(amount) / 100.), 188 | list(map(float, range(101))), 189 | )) 190 | colorSlide: str = ', '.join(map( 191 | lambda colorStop: f'{QColor.fromRgbF(*colorStop).name()}', 192 | colorStops, 193 | )) 194 | return f'linear-gradient({colorSlide})' 195 | elif representation == Representation.Color3: 196 | return QColor.fromRgbF(selectedColor.x, selectedColor.y, selectedColor.z).name() 197 | elif language == Language.SVG: 198 | if representation == Representation.ColorMap: 199 | amounts = list(map(float, range(101))) 200 | colorStops = list(map( 201 | lambda amount: selectedGradient.evaluateFit(float(amount) / 100.), 202 | amounts, 203 | )) 204 | colorSlide: str = '\n '.join(map( 205 | lambda stopIndex: f'', 206 | range(len(amounts)), 207 | )) 208 | return f'\n{colorSlide}\n' 209 | elif language == Language.Python: 210 | if representation == Representation.ColorMaps: 211 | cmaps: list[list[list[float]]] = reduce( 212 | lambda accumulator, addition: accumulator + addition, 213 | map( 214 | lambda gradient: list(map( 215 | lambda coefficient: [coefficient.x, coefficient.y, coefficient.z], 216 | gradient.coefficients, 217 | )), 218 | gradientList, 219 | ), 220 | ) 221 | # print(cmaps) 222 | if Export.AllCMapsSameWidthSameModel(gradientList): 223 | data = Array( 224 | len(gradientList) * gradientList[0]._degree, 225 | Array( 226 | 3, 227 | Float16l, 228 | ) 229 | ).build(cmaps) 230 | return str(data) 231 | 232 | elif language == Language.NASM: 233 | pass 234 | elif language == Language.C: 235 | pass 236 | 237 | 238 | if __name__ == '__main__': 239 | print(Export.Export( 240 | Language.SVG, 241 | Representation.ColorMap, 242 | DefaultGradient1, 243 | [DefaultGradient1, DefaultGradient2], 244 | # vec3(.3,.6,.8), 245 | vec3(0), 246 | )) 247 | -------------------------------------------------------------------------------- /imagecolorpicker/importer.py: -------------------------------------------------------------------------------- 1 | from glm import vec3 2 | 3 | 4 | class Importer: 5 | @staticmethod 6 | def HornerPolynomial(source: str) -> list[vec3]: 7 | stacks = list(map( 8 | lambda stack: stack.rstrip().lstrip(), 9 | source.replace('return ', '').rstrip(';} \t\n').split('+t*('), 10 | )) 11 | stacks[-1], _ = stacks[-1].split() 12 | return list(map( 13 | lambda stack: eval(stack), 14 | stacks, 15 | )) 16 | -------------------------------------------------------------------------------- /imagecolorpicker/language.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum, auto 2 | 3 | 4 | class Language(IntEnum): 5 | GLSL = auto() 6 | HLSL = auto() 7 | CSS = auto() 8 | SVG = auto() 9 | Python = auto() 10 | NASM = auto() 11 | C = auto() 12 | CablesJSON = auto() 13 | -------------------------------------------------------------------------------- /imagecolorpicker/model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeStahL/ImageColorPicker/0b57bfd839a0dea4a59d824df7cda7a45e7727c9/imagecolorpicker/model/__init__.py -------------------------------------------------------------------------------- /imagecolorpicker/model/gradientcolorcolumntype.py: -------------------------------------------------------------------------------- 1 | from enum import ( 2 | IntEnum, 3 | auto, 4 | ) 5 | 6 | 7 | class GradientColorColumnType(IntEnum): 8 | Name = auto() 9 | Weight = auto() 10 | -------------------------------------------------------------------------------- /imagecolorpicker/model/gradientcolormodel.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from PyQt6.QtCore import * 4 | from PyQt6.QtCore import QModelIndex, QObject, Qt 5 | from PyQt6.QtWidgets import * 6 | from PyQt6.QtGui import * 7 | from typing import * 8 | from imagecolorpicker.colorgradient import GradientMix, GradientWeight, ColorGradient, DefaultGradient1 9 | from glm import vec3 10 | from .gradientcolorcolumntype import GradientColorColumnType 11 | 12 | 13 | class GradientColorModel(QAbstractTableModel): 14 | def __init__( 15 | self: Self, 16 | parent: Optional[QObject] = None, 17 | ) -> None: 18 | super().__init__(parent) 19 | 20 | self._gradient: Optional[ColorGradient] = None 21 | self._columnList: list[GradientColorColumnType] = [ 22 | GradientColorColumnType.Name, 23 | GradientColorColumnType.Weight, 24 | ] 25 | 26 | def loadGradient( 27 | self: Self, 28 | gradient: Optional[ColorGradient], 29 | ) -> None: 30 | self.beginResetModel() 31 | self._gradient = gradient 32 | self.endResetModel() 33 | 34 | def data( 35 | self: Self, 36 | index: QModelIndex, 37 | role: int = Qt.ItemDataRole.DisplayRole.value, 38 | ) -> Any: 39 | if not index.isValid() or self._gradient is None: 40 | return 41 | 42 | roleEnum: Qt.ItemDataRole = Qt.ItemDataRole(role) 43 | columnType: GradientColorColumnType = self._columnList[index.column()] 44 | if roleEnum == Qt.ItemDataRole.DisplayRole: 45 | if columnType == GradientColorColumnType.Name: 46 | color = self._gradient._colors[index.row()] 47 | return QColor.fromRgbF(color.x, color.y, color.z).name() 48 | elif columnType == GradientColorColumnType.Weight: 49 | return self._gradient.weights[index.row()] 50 | elif roleEnum == Qt.ItemDataRole.BackgroundRole: 51 | color = self._gradient._colors[index.row()] 52 | return QColor.fromRgbF(color.x, color.y, color.z) 53 | elif roleEnum == Qt.ItemDataRole.ForegroundRole: 54 | color = self._gradient._colors[index.row()] 55 | return QColor(Qt.GlobalColor.black if 0.299 * color.x + 0.587 * color.y + 0.114 * color.z > 186.0 / 255. else Qt.GlobalColor.white) 56 | 57 | def columnCount( 58 | self: Self, 59 | parent: QModelIndex = QModelIndex(), 60 | ) -> int: 61 | return len(self._columnList) 62 | 63 | def rowCount( 64 | self: Self, 65 | parent: QModelIndex = QModelIndex(), 66 | ) -> int: 67 | if self._gradient is None: 68 | return 0 69 | return len(self._gradient._colors) 70 | 71 | def headerData( 72 | self: Self, 73 | section: int, 74 | orientation: Qt.Orientation, 75 | role: Qt.ItemDataRole = Qt.ItemDataRole.DisplayRole, 76 | ) -> Any: 77 | if role != Qt.ItemDataRole.DisplayRole: 78 | return QVariant() 79 | 80 | if orientation == Qt.Orientation.Horizontal: 81 | return self._columnList[section].name 82 | else: 83 | return section 84 | 85 | def flags( 86 | self: Self, 87 | index: QModelIndex = QModelIndex(), 88 | ) -> Qt.ItemFlag: 89 | flags = super().flags(index) 90 | if not index.isValid() or self._gradient is None: 91 | return flags 92 | 93 | columnType: GradientColorColumnType = self._columnList[index.column()] 94 | if columnType == GradientColorColumnType.Name: 95 | flags = flags | Qt.ItemFlag.ItemIsEditable 96 | return flags 97 | 98 | def setData( 99 | self: Self, 100 | index: QModelIndex, 101 | value: Any, 102 | role: Qt.ItemDataRole = Qt.ItemDataRole.EditRole, 103 | ) -> bool: 104 | if not index.isValid() or self._gradient is None: 105 | return False 106 | 107 | roleEnum: Qt.ItemDataRole = Qt.ItemDataRole(role) 108 | columnType: GradientColorColumnType = self._columnList[index.column()] 109 | if roleEnum == Qt.ItemDataRole.EditRole: 110 | if columnType == GradientColorColumnType.Name: 111 | color = QColor(value) 112 | self._gradient._colors[index.row()] = vec3(color.redF(), color.greenF(), color.blueF()) 113 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 114 | return True 115 | return False 116 | 117 | def updateColor(self: Self, row: int, color: QColor) -> None: 118 | self._gradient._colors[row] = vec3(color.redF(), color.greenF(), color.blueF()) 119 | self.dataChanged.emit(self.index(row, 0), self.index(row, self.columnCount()), [Qt.ItemDataRole.EditRole]) 120 | -------------------------------------------------------------------------------- /imagecolorpicker/model/gradientlistcolumntype.py: -------------------------------------------------------------------------------- 1 | from enum import ( 2 | IntEnum, 3 | IntFlag, 4 | StrEnum, 5 | auto, 6 | ) 7 | 8 | class GradientListColumnType(IntEnum): 9 | Name = auto() 10 | Preview = auto() 11 | Degree = auto() 12 | Model = auto() 13 | -------------------------------------------------------------------------------- /imagecolorpicker/model/gradientlistmodel.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import ( 2 | QAbstractTableModel, 3 | QObject, 4 | QModelIndex, 5 | Qt, 6 | pyqtSignal, 7 | QPointF, 8 | QPoint, 9 | ) 10 | from PyQt6.QtGui import ( 11 | QPalette, 12 | QFont, 13 | QPen, 14 | QColor, 15 | ) 16 | from PyQt6.QtWidgets import ( 17 | QApplication, 18 | QTableView, 19 | ) 20 | from typing import ( 21 | Any, 22 | Self, 23 | Optional, 24 | Union, 25 | ) 26 | from traceback import format_exc 27 | from traceback import print_exc 28 | from ..colorgradient import ( 29 | ColorGradient, 30 | FitModel, 31 | DefaultGradient1, 32 | DefaultGradient2, 33 | ) 34 | from sys import argv 35 | from ..delegate.gradientlistdelegate import GradientListDelegate 36 | from .gradientlistcolumntype import GradientListColumnType 37 | from copy import deepcopy 38 | from ..colorspace import ColorSpaceType 39 | 40 | 41 | class GradientListModel(QAbstractTableModel): 42 | currentGradientChanged: pyqtSignal = pyqtSignal(ColorGradient) 43 | 44 | def __init__( 45 | self: Self, 46 | parent: Optional[QObject] = None, 47 | ) -> None: 48 | super().__init__(parent) 49 | 50 | self._columnList: list[GradientListColumnType] = [ 51 | GradientListColumnType.Name, 52 | GradientListColumnType.Preview, 53 | GradientListColumnType.Model, 54 | GradientListColumnType.Degree, 55 | ] 56 | 57 | self._gradientList: list[ColorGradient] = [] 58 | self._currentIndex: int = 0 59 | 60 | def loadGradientList( 61 | self: Self, 62 | gradientList: Optional[list[ColorGradient]], 63 | ) -> None: 64 | self.beginResetModel() 65 | self._gradientList = gradientList or [] 66 | self._currentIndex = 0 67 | self.endResetModel() 68 | 69 | def data( 70 | self: Self, 71 | index: QModelIndex, 72 | role: int = Qt.ItemDataRole.DisplayRole.value, 73 | ) -> Any: 74 | if not index.isValid(): 75 | return 76 | 77 | roleEnum: Qt.ItemDataRole = Qt.ItemDataRole(role) 78 | columnType: GradientListColumnType = self._columnList[index.column()] 79 | if roleEnum == Qt.ItemDataRole.DisplayRole: 80 | if columnType == GradientListColumnType.Name: 81 | return self._gradientList[index.row()]._name 82 | elif columnType == GradientListColumnType.Degree: 83 | return self._gradientList[index.row()]._degree 84 | elif columnType == GradientListColumnType.Model: 85 | return self._gradientList[index.row()]._model.name 86 | elif columnType == GradientListColumnType.Preview: 87 | return self._gradientList[index.row()] 88 | 89 | if roleEnum == Qt.ItemDataRole.FontRole: 90 | if index.row() == self._currentIndex: 91 | font: QFont = QFont() 92 | font.setBold(True) 93 | return font 94 | 95 | def setData( 96 | self: Self, 97 | index: QModelIndex, 98 | value: str, 99 | role: int = Qt.ItemDataRole.EditRole.value, 100 | ) -> bool: 101 | if not index.isValid(): 102 | return False 103 | 104 | roleEnum: Qt.ItemDataRole = Qt.ItemDataRole(role) 105 | columnType: GradientListColumnType = self._columnList[index.column()] 106 | if roleEnum == Qt.ItemDataRole.EditRole: 107 | if columnType == GradientListColumnType.Name: 108 | self._gradientList[index.row()]._name = value 109 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 110 | elif columnType == GradientListColumnType.Degree: 111 | try: 112 | self._gradientList[index.row()]._degree = int(value) 113 | self._gradientList[index.row()]._update() 114 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 115 | return True 116 | except: 117 | return False 118 | elif columnType == GradientListColumnType.Model: 119 | try: 120 | self._gradientList[index.row()]._model = FitModel[value] 121 | self._gradientList[index.row()]._update() 122 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 123 | return True 124 | except: 125 | return False 126 | return False 127 | 128 | def headerData( 129 | self: Self, 130 | section: int, 131 | orientation: Qt.Orientation, 132 | role: int = Qt.ItemDataRole.DisplayRole.value, 133 | ) -> Any: 134 | roleEnum: Qt.ItemDataRole = Qt.ItemDataRole(role) 135 | 136 | if roleEnum != Qt.ItemDataRole.DisplayRole: 137 | return 138 | 139 | if orientation == Qt.Orientation.Horizontal: 140 | return self._columnList[section].name 141 | 142 | if orientation == Qt.Orientation.Vertical: 143 | return section 144 | 145 | def columnCount( 146 | self: Self, 147 | parent: QModelIndex = QModelIndex(), 148 | ) -> int: 149 | return len(self._columnList) 150 | 151 | def rowCount( 152 | self: Self, 153 | parent: QModelIndex = QModelIndex(), 154 | ) -> int: 155 | return len(self._gradientList) 156 | 157 | def flags(self: Self, index: QModelIndex) -> Qt.ItemFlag: 158 | flags: Qt.ItemFlag = super().flags(index) 159 | columnType: GradientListColumnType = self._columnList[index.column()] 160 | if columnType in [ 161 | GradientListColumnType.Name, 162 | GradientListColumnType.Degree, 163 | GradientListColumnType.Model, 164 | ]: 165 | flags = flags | Qt.ItemFlag.ItemIsEditable 166 | return flags 167 | 168 | def changeCurrent( 169 | self: Self, 170 | newIndex: int, 171 | ) -> None: 172 | oldIndex: int = self._currentIndex 173 | self._currentIndex = newIndex 174 | self.dataChanged.emit(self.index(oldIndex, 0), self.index(oldIndex, self.columnCount()), [Qt.ItemDataRole.EditRole]) 175 | self.dataChanged.emit(self.index(newIndex, 0), self.index(newIndex, self.columnCount()), [Qt.ItemDataRole.EditRole]) 176 | self.currentGradientChanged.emit(self._gradientList[self._currentIndex]) 177 | 178 | def updateCurrentGradient(self: Self) -> None: 179 | self._gradientList[self._currentIndex]._update() 180 | self.dataChanged.emit( 181 | self.index(self._currentIndex, 0), 182 | self.index(self._currentIndex, self.columnCount()), 183 | [Qt.ItemDataRole.DisplayRole], 184 | ) 185 | 186 | def copyCurrentGradientWithColorSpaces(self: Self, weightColorSpace: ColorSpaceType, mixColorSpace: ColorSpaceType) -> ColorGradient: 187 | result: ColorGradient = deepcopy(self._gradientList[self._currentIndex]) 188 | result._weightColorSpace = weightColorSpace 189 | result._mixColorSpace = mixColorSpace 190 | result._name = f'{weightColorSpace.name}:{mixColorSpace.name}' 191 | result._update() 192 | return result 193 | 194 | if __name__ == '__main__': 195 | app = QApplication(argv) 196 | 197 | tableView: QTableView = QTableView() 198 | 199 | delegate: GradientListDelegate = GradientListDelegate() 200 | tableView.setItemDelegate(delegate) 201 | tableView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) 202 | 203 | model: GradientListModel = GradientListModel() 204 | tableView.setModel(model) 205 | 206 | def rightClicked(position: QPoint) -> None: 207 | index: QModelIndex = tableView.indexAt(position) 208 | if index.isValid(): 209 | model.changeCurrent(index.row()) 210 | tableView.customContextMenuRequested.connect(rightClicked) 211 | 212 | gradientList: list[ColorGradient] = [ 213 | DefaultGradient1, 214 | DefaultGradient2, 215 | ] 216 | model.loadGradientList(gradientList) 217 | 218 | tableView.show() 219 | 220 | app.exec() 221 | -------------------------------------------------------------------------------- /imagecolorpicker/model/gradientpropertycolumntype.py: -------------------------------------------------------------------------------- 1 | from enum import ( 2 | IntEnum, 3 | auto, 4 | ) 5 | 6 | 7 | class GradientPropertyColumnType(IntEnum): 8 | Key = auto() 9 | Value = auto() 10 | -------------------------------------------------------------------------------- /imagecolorpicker/model/gradientpropertymodel.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import ( 2 | QAbstractTableModel, 3 | QObject, 4 | QModelIndex, 5 | Qt, 6 | pyqtSignal, 7 | QPointF, 8 | QPoint, 9 | ) 10 | from PyQt6.QtGui import ( 11 | QPalette, 12 | QFont, 13 | QPen, 14 | QColor, 15 | ) 16 | from PyQt6.QtWidgets import ( 17 | QApplication, 18 | QTableView, 19 | ) 20 | from typing import ( 21 | Any, 22 | Self, 23 | Optional, 24 | Union, 25 | ) 26 | from traceback import format_exc 27 | from traceback import print_exc 28 | from ..colorgradient import ( 29 | ColorGradient, 30 | FitModel, 31 | DefaultGradient1, 32 | DefaultGradient2, 33 | ) 34 | from sys import argv 35 | from .gradientpropertycolumntype import GradientPropertyColumnType 36 | from .gradientpropertyrowtype import GradientPropertyRowType 37 | from ..colorspace import ColorSpaceType 38 | from ..colorgradient import ( 39 | Observer, 40 | FitAlgorithm, 41 | FitModel, 42 | Illuminant, 43 | Wraparound, 44 | ) 45 | from ..delegate.gradientpropertydelegate import GradientPropertyDelegate 46 | 47 | 48 | class GradientPropertyModel(QAbstractTableModel): 49 | gradientPropertiesChanged: pyqtSignal = pyqtSignal() 50 | 51 | def __init__( 52 | self: Self, 53 | parent: Optional[QObject] = None, 54 | ) -> None: 55 | super().__init__(parent) 56 | 57 | self._columnList: list[GradientPropertyColumnType] = [ 58 | GradientPropertyColumnType.Key, 59 | GradientPropertyColumnType.Value, 60 | ] 61 | self._rowList: list[GradientPropertyRowType] = [ 62 | GradientPropertyRowType.Name, 63 | GradientPropertyRowType.Degree, 64 | GradientPropertyRowType.Model, 65 | GradientPropertyRowType.Wraparound, 66 | GradientPropertyRowType.MixColorSpace, 67 | GradientPropertyRowType.WeightColorSpace, 68 | GradientPropertyRowType.Illuminant, 69 | GradientPropertyRowType.Observer, 70 | GradientPropertyRowType.FitAlgorithm, 71 | GradientPropertyRowType.FitAmount, 72 | GradientPropertyRowType.MaxFitIterationCount, 73 | ] 74 | 75 | self._gradient: Optional[ColorGradient] = None 76 | 77 | def loadGradient( 78 | self: Self, 79 | gradient: Optional[ColorGradient], 80 | ) -> None: 81 | self.beginResetModel() 82 | self._gradient = gradient 83 | self.endResetModel() 84 | 85 | def data( 86 | self: Self, 87 | index: QModelIndex, 88 | role: int = Qt.ItemDataRole.DisplayRole.value, 89 | ) -> Any: 90 | if not index.isValid() or self._gradient is None: 91 | return 92 | 93 | roleEnum: Qt.ItemDataRole = Qt.ItemDataRole(role) 94 | columnType: GradientPropertyColumnType = self._columnList[index.column()] 95 | rowType: GradientPropertyRowType = self._rowList[index.row()] 96 | if roleEnum == Qt.ItemDataRole.DisplayRole: 97 | if columnType == GradientPropertyColumnType.Key: 98 | return rowType.name 99 | elif columnType == GradientPropertyColumnType.Value: 100 | if rowType == GradientPropertyRowType.Name: 101 | return self._gradient._name 102 | elif rowType == GradientPropertyRowType.Degree: 103 | return self._gradient._degree 104 | elif rowType == GradientPropertyRowType.WeightColorSpace: 105 | return self._gradient._weightColorSpace.name 106 | elif rowType == GradientPropertyRowType.MixColorSpace: 107 | return self._gradient._mixColorSpace.name 108 | elif rowType == GradientPropertyRowType.Observer: 109 | return self._gradient._observer.name 110 | elif rowType == GradientPropertyRowType.Illuminant: 111 | return self._gradient._illuminant.name 112 | elif rowType == GradientPropertyRowType.Model: 113 | return self._gradient._model.name 114 | elif rowType == GradientPropertyRowType.Wraparound: 115 | return self._gradient._wraparound.name 116 | elif rowType == GradientPropertyRowType.FitAlgorithm: 117 | return self._gradient._fitAlgorithm.name 118 | elif rowType == GradientPropertyRowType.MaxFitIterationCount: 119 | return self._gradient._maxFitIterationCount 120 | elif rowType == GradientPropertyRowType.FitAmount: 121 | return self._gradient._fitAmount 122 | 123 | def setData( 124 | self: Self, 125 | index: QModelIndex, 126 | value: str, 127 | role: int = Qt.ItemDataRole.EditRole.value, 128 | ) -> bool: 129 | if not index.isValid() or self._gradient is None: 130 | return False 131 | 132 | roleEnum: Qt.ItemDataRole = Qt.ItemDataRole(role) 133 | columnType: GradientPropertyColumnType = self._columnList[index.column()] 134 | rowType: GradientPropertyRowType = self._rowList[index.row()] 135 | if roleEnum == Qt.ItemDataRole.EditRole: 136 | if columnType == GradientPropertyColumnType.Value: 137 | if rowType == GradientPropertyRowType.Name: 138 | self._gradient._name = value 139 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 140 | return True 141 | elif rowType == GradientPropertyRowType.Degree: 142 | self._gradient._degree = int(value) 143 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 144 | return True 145 | elif rowType == GradientPropertyRowType.WeightColorSpace: 146 | self._gradient._weightColorSpace = ColorSpaceType[value] 147 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 148 | return True 149 | elif rowType == GradientPropertyRowType.MixColorSpace: 150 | self._gradient._mixColorSpace = ColorSpaceType[value] 151 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 152 | return True 153 | elif rowType == GradientPropertyRowType.Observer: 154 | self._gradient._observer = Observer[value] 155 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 156 | return True 157 | elif rowType == GradientPropertyRowType.Illuminant: 158 | self._gradient._illuminant = Illuminant[value] 159 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 160 | return True 161 | elif rowType == GradientPropertyRowType.Model: 162 | self._gradient._model = FitModel[value] 163 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 164 | return True 165 | elif rowType == GradientPropertyRowType.Wraparound: 166 | self._gradient._wraparound = Wraparound[value] 167 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 168 | return True 169 | elif rowType == GradientPropertyRowType.FitAlgorithm: 170 | self._gradient._fitAlgorithm = FitAlgorithm[value] 171 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 172 | return True 173 | elif rowType == GradientPropertyRowType.MaxFitIterationCount: 174 | self._gradient._maxFitIterationCount = int(value) 175 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 176 | return True 177 | elif rowType == GradientPropertyRowType.FitAmount: 178 | self._gradient._fitAmount = int(value) 179 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 180 | return True 181 | return False 182 | 183 | def headerData( 184 | self: Self, 185 | section: int, 186 | orientation: Qt.Orientation, 187 | role: int = Qt.ItemDataRole.DisplayRole.value, 188 | ) -> Any: 189 | roleEnum: Qt.ItemDataRole = Qt.ItemDataRole(role) 190 | 191 | if roleEnum != Qt.ItemDataRole.DisplayRole: 192 | return 193 | 194 | if orientation == Qt.Orientation.Horizontal: 195 | return self._columnList[section].name 196 | 197 | if orientation == Qt.Orientation.Vertical: 198 | return section 199 | 200 | def columnCount( 201 | self: Self, 202 | parent: QModelIndex = QModelIndex(), 203 | ) -> int: 204 | return len(self._columnList) 205 | 206 | def rowCount( 207 | self: Self, 208 | parent: QModelIndex = QModelIndex(), 209 | ) -> int: 210 | return len(self._rowList) 211 | 212 | def flags(self: Self, index: QModelIndex) -> Qt.ItemFlag: 213 | flags: Qt.ItemFlag = super().flags(index) 214 | columnType: GradientPropertyColumnType = self._columnList[index.column()] 215 | if columnType == GradientPropertyColumnType.Value: 216 | flags = flags | Qt.ItemFlag.ItemIsEditable 217 | return flags 218 | 219 | if __name__ == '__main__': 220 | app = QApplication(argv) 221 | 222 | tableView: QTableView = QTableView() 223 | 224 | delegate: GradientPropertyDelegate = GradientPropertyDelegate() 225 | tableView.setItemDelegate(delegate) 226 | 227 | model: GradientPropertyModel = GradientPropertyModel() 228 | tableView.setModel(model) 229 | 230 | model.loadGradient(DefaultGradient1) 231 | 232 | tableView.show() 233 | 234 | app.exec() 235 | -------------------------------------------------------------------------------- /imagecolorpicker/model/gradientpropertyrowtype.py: -------------------------------------------------------------------------------- 1 | from enum import ( 2 | IntEnum, 3 | auto, 4 | ) 5 | 6 | 7 | class GradientPropertyRowType(IntEnum): 8 | Name = auto() 9 | Degree = auto() 10 | WeightColorSpace = auto() 11 | MixColorSpace = auto() 12 | Observer = auto() 13 | Illuminant = auto() 14 | Model = auto() 15 | Wraparound = auto() 16 | FitAlgorithm = auto() 17 | MaxFitIterationCount = auto() 18 | FitAmount = auto() 19 | -------------------------------------------------------------------------------- /imagecolorpicker/model/imagelistmodel.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import ( 2 | QAbstractTableModel, 3 | QAbstractListModel, 4 | QObject, 5 | QModelIndex, 6 | Qt, 7 | pyqtSignal, 8 | QPointF, 9 | QPoint, 10 | QSize, 11 | ) 12 | from PyQt6.QtGui import ( 13 | QPalette, 14 | QFont, 15 | QPen, 16 | QColor, 17 | QImage, 18 | ) 19 | from PyQt6.QtWidgets import ( 20 | QApplication, 21 | QTableView, 22 | QListView, 23 | ) 24 | from typing import ( 25 | Any, 26 | Self, 27 | Optional, 28 | Union, 29 | ) 30 | from traceback import format_exc 31 | from traceback import print_exc 32 | from ..colorgradient import ( 33 | ColorGradient, 34 | FitModel, 35 | DefaultGradient1, 36 | DefaultGradient2, 37 | ) 38 | from importlib.resources import files 39 | from pathlib import Path 40 | from imagecolorpicker.widgets import pickablecolorlabel 41 | from sys import argv 42 | from ..delegate.gradientlistdelegate import GradientListDelegate 43 | from .gradientlistcolumntype import GradientListColumnType 44 | from ..delegate.imagelistdelegate import ImageListDelegate 45 | 46 | 47 | class ImageListModel(QAbstractListModel): 48 | currentImageChanged: pyqtSignal = pyqtSignal(QImage) 49 | 50 | DefaultImageWidth: int = 256 51 | 52 | def __init__( 53 | self: Self, 54 | parent: Optional[QObject] = None, 55 | ) -> None: 56 | super().__init__(parent) 57 | 58 | self._imageList: list[QImage] = [] 59 | self._currentIndex: int = 0 60 | 61 | def loadImageList( 62 | self: Self, 63 | imageList: Optional[list[QImage]], 64 | ) -> None: 65 | self.beginResetModel() 66 | self._imageList = imageList or [] 67 | self._currentIndex = 0 68 | self.endResetModel() 69 | 70 | def data( 71 | self: Self, 72 | index: QModelIndex, 73 | role: int = Qt.ItemDataRole.DisplayRole.value, 74 | ) -> Any: 75 | if not index.isValid(): 76 | return 77 | 78 | roleEnum: Qt.ItemDataRole = Qt.ItemDataRole(role) 79 | if roleEnum == Qt.ItemDataRole.DisplayRole: 80 | return self._imageList[index.row()] 81 | 82 | elif roleEnum == Qt.ItemDataRole.SizeHintRole: 83 | image: QImage = self._imageList[index.row()] 84 | aspect: float 85 | if float(image.height()) == 0.: 86 | aspect = 1. 87 | else: 88 | aspect = float(image.width()) / float(image.height()) 89 | return QSize(ImageListModel.DefaultImageWidth, int(ImageListModel.DefaultImageWidth / aspect)) 90 | 91 | 92 | def setData( 93 | self: Self, 94 | index: QModelIndex, 95 | value: Any, 96 | role: int = Qt.ItemDataRole.EditRole.value, 97 | ) -> bool: 98 | if not index.isValid(): 99 | return False 100 | 101 | roleEnum: Qt.ItemDataRole = Qt.ItemDataRole(role) 102 | if roleEnum == Qt.ItemDataRole.EditRole: 103 | try: 104 | self._imageList[index.row()] = value 105 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 106 | return True 107 | except: 108 | return False 109 | 110 | return False 111 | 112 | def headerData( 113 | self: Self, 114 | section: int, 115 | orientation: Qt.Orientation, 116 | role: int = Qt.ItemDataRole.DisplayRole.value, 117 | ) -> Any: 118 | roleEnum: Qt.ItemDataRole = Qt.ItemDataRole(role) 119 | 120 | if roleEnum != Qt.ItemDataRole.DisplayRole: 121 | return 122 | 123 | if orientation == Qt.Orientation.Horizontal: 124 | return "Preview" 125 | 126 | if orientation == Qt.Orientation.Vertical: 127 | return section 128 | 129 | def columnCount( 130 | self: Self, 131 | parent: QModelIndex = QModelIndex(), 132 | ) -> int: 133 | return 1 134 | 135 | def rowCount( 136 | self: Self, 137 | parent: QModelIndex = QModelIndex(), 138 | ) -> int: 139 | return len(self._imageList) 140 | 141 | def flags(self: Self, index: QModelIndex) -> Qt.ItemFlag: 142 | flags: Qt.ItemFlag = super().flags(index) 143 | return flags 144 | 145 | def changeCurrent( 146 | self: Self, 147 | newIndex: int, 148 | ) -> None: 149 | oldIndex: int = self._currentIndex 150 | self._currentIndex = newIndex 151 | self.dataChanged.emit(self.index(oldIndex, 0), self.index(oldIndex, self.columnCount()), [Qt.ItemDataRole.EditRole]) 152 | self.dataChanged.emit(self.index(newIndex, 0), self.index(newIndex, self.columnCount()), [Qt.ItemDataRole.EditRole]) 153 | self.currentImageChanged.emit(self._imageList[self._currentIndex]) 154 | 155 | if __name__ == '__main__': 156 | app = QApplication(argv) 157 | 158 | listView: QListView = QListView() 159 | 160 | imageListDelegate: ImageListDelegate = ImageListDelegate() 161 | listView.setItemDelegate(imageListDelegate) 162 | 163 | model: ImageListModel = ImageListModel() 164 | listView.setModel(model) 165 | 166 | image: QImage = QImage(str(files(pickablecolorlabel) / 'default.png')) 167 | imageList: list[QImage] = [ 168 | image, 169 | ] 170 | 171 | model.loadImageList(imageList) 172 | 173 | listView.show() 174 | 175 | app.exec() 176 | -------------------------------------------------------------------------------- /imagecolorpicker/model/settingscolumntype.py: -------------------------------------------------------------------------------- 1 | from enum import ( 2 | IntEnum, 3 | auto, 4 | ) 5 | 6 | 7 | class SettingsColumnType(IntEnum): 8 | Key = auto() 9 | Value = auto() 10 | -------------------------------------------------------------------------------- /imagecolorpicker/model/settingsmodel.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import ( 2 | QAbstractTableModel, 3 | QObject, 4 | QModelIndex, 5 | Qt, 6 | pyqtSignal, 7 | QPointF, 8 | QPoint, 9 | QSettings, 10 | ) 11 | from PyQt6.QtGui import ( 12 | QPalette, 13 | QFont, 14 | QPen, 15 | QColor, 16 | ) 17 | from PyQt6.QtWidgets import ( 18 | QApplication, 19 | QTableView, 20 | ) 21 | from typing import ( 22 | Any, 23 | Self, 24 | Optional, 25 | Union, 26 | ) 27 | from .settingscolumntype import SettingsColumnType 28 | from .settingsrowtype import SettingsRowType 29 | from ..colorspace import ColorSpaceType 30 | from ..language import Language 31 | from ..representation import Representation 32 | from ..delegate.gradientpropertydelegate import GradientPropertyDelegate 33 | 34 | 35 | class SettingsModel(QAbstractTableModel): 36 | gradientPropertiesChanged: pyqtSignal = pyqtSignal() 37 | 38 | def __init__( 39 | self: Self, 40 | parent: Optional[QObject] = None, 41 | ) -> None: 42 | super().__init__(parent) 43 | 44 | self._columnList: list[SettingsColumnType] = [ 45 | SettingsColumnType.Key, 46 | SettingsColumnType.Value, 47 | ] 48 | self._rowList: list[SettingsRowType] = [ 49 | SettingsRowType.CopyLanguage, 50 | SettingsRowType.CopyRepresentation, 51 | ] 52 | 53 | def data( 54 | self: Self, 55 | index: QModelIndex, 56 | role: int = Qt.ItemDataRole.DisplayRole.value, 57 | ) -> Any: 58 | if not index.isValid(): 59 | return 60 | 61 | roleEnum: Qt.ItemDataRole = Qt.ItemDataRole(role) 62 | columnType: SettingsColumnType = self._columnList[index.column()] 63 | rowType: SettingsRowType = self._rowList[index.row()] 64 | if roleEnum == Qt.ItemDataRole.DisplayRole: 65 | if columnType == SettingsColumnType.Key: 66 | return rowType.name 67 | elif columnType == SettingsColumnType.Value: 68 | if rowType == SettingsRowType.CopyLanguage: 69 | return QSettings().value(rowType.value, Language.GLSL.name) 70 | elif rowType == SettingsRowType.CopyRepresentation: 71 | return QSettings().value(rowType.value, Representation.ColorMap.name) 72 | 73 | def setData( 74 | self: Self, 75 | index: QModelIndex, 76 | value: str, 77 | role: int = Qt.ItemDataRole.EditRole.value, 78 | ) -> bool: 79 | if not index.isValid(): 80 | return False 81 | 82 | roleEnum: Qt.ItemDataRole = Qt.ItemDataRole(role) 83 | columnType: SettingsColumnType = self._columnList[index.column()] 84 | rowType: SettingsRowType = self._rowList[index.row()] 85 | if roleEnum == Qt.ItemDataRole.EditRole: 86 | if columnType == SettingsColumnType.Value: 87 | QSettings().setValue(rowType.value, value) 88 | self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole]) 89 | return True 90 | 91 | return False 92 | 93 | def headerData( 94 | self: Self, 95 | section: int, 96 | orientation: Qt.Orientation, 97 | role: int = Qt.ItemDataRole.DisplayRole.value, 98 | ) -> Any: 99 | roleEnum: Qt.ItemDataRole = Qt.ItemDataRole(role) 100 | 101 | if roleEnum != Qt.ItemDataRole.DisplayRole: 102 | return 103 | 104 | if orientation == Qt.Orientation.Horizontal: 105 | return self._columnList[section].name 106 | 107 | if orientation == Qt.Orientation.Vertical: 108 | return section 109 | 110 | def columnCount( 111 | self: Self, 112 | parent: QModelIndex = QModelIndex(), 113 | ) -> int: 114 | return len(self._columnList) 115 | 116 | def rowCount( 117 | self: Self, 118 | parent: QModelIndex = QModelIndex(), 119 | ) -> int: 120 | return len(self._rowList) 121 | 122 | def flags(self: Self, index: QModelIndex) -> Qt.ItemFlag: 123 | flags: Qt.ItemFlag = super().flags(index) 124 | columnType: SettingsColumnType = self._columnList[index.column()] 125 | if columnType == SettingsColumnType.Value: 126 | flags = flags | Qt.ItemFlag.ItemIsEditable 127 | return flags 128 | 129 | @property 130 | def language(self: Self) -> Language: 131 | return Language[self.data(self.index( 132 | self._rowList.index(SettingsRowType.CopyLanguage), 133 | self._columnList.index(SettingsColumnType.Value), 134 | ))] 135 | 136 | @property 137 | def representation(self: Self) -> Representation: 138 | return Representation[self.data(self.index( 139 | self._rowList.index(SettingsRowType.CopyRepresentation), 140 | self._columnList.index(SettingsColumnType.Value), 141 | ))] 142 | -------------------------------------------------------------------------------- /imagecolorpicker/model/settingsrowtype.py: -------------------------------------------------------------------------------- 1 | from enum import StrEnum 2 | 3 | 4 | class SettingsRowType(StrEnum): 5 | CopyLanguage = 'copy_language' 6 | CopyRepresentation = 'copy_representation' 7 | -------------------------------------------------------------------------------- /imagecolorpicker/optimizationalgorithm.py: -------------------------------------------------------------------------------- 1 | from typing import Self 2 | 3 | 4 | # class OptimizationAlgorithm: 5 | # @staticmethod 6 | # def 7 | 8 | 9 | -------------------------------------------------------------------------------- /imagecolorpicker/optimizationmodel.py: -------------------------------------------------------------------------------- 1 | from enum import ( 2 | IntEnum, 3 | auto, 4 | ) 5 | from typing import Self 6 | # from glm import * 7 | from numpy import ( 8 | array, 9 | array, 10 | linspace, 11 | cos, 12 | pi, 13 | sin, 14 | sqrt, 15 | exp, 16 | log, 17 | ) 18 | from lmfit.models import PolynomialModel 19 | 20 | 21 | class OptimizationModel: 22 | @staticmethod 23 | def TrigonometricInitialGuess() -> list[list[float]]: 24 | return [ 25 | [1.] * 4, 26 | [1.] * 4, 27 | [1.] * 4, 28 | ] 29 | 30 | @staticmethod 31 | def Trigonometric(t: float, *p: tuple[float]) -> float: 32 | a, b, c, d = p 33 | return a + b * cos(2. * pi * (c * t + d)) 34 | 35 | @staticmethod 36 | def PolynomialInitialGuess(degree: int) -> list[list[float]]: 37 | return [ 38 | [1.] * degree, 39 | [1.] * degree, 40 | [1.] * degree, 41 | ] 42 | 43 | @staticmethod 44 | def Polynomial(t: float, *c: tuple[float]) -> float: 45 | result = c[-1] 46 | for ck in reversed(c[:-1]): 47 | result = ck + t * result 48 | return result 49 | 50 | @staticmethod 51 | def Harmonic(t: float, *c: tuple[float]) -> float: 52 | result = 0. 53 | for k in range(len(c) // 2): 54 | result += 1 / c[2 * k] / sqrt(2 * pi) * exp(-((t - c[2 * k + 1]) / (c[2 * k])) ** 2) 55 | return result 56 | 57 | @staticmethod 58 | def HarmonicInitialGuess(degree) -> list[list[float]]: 59 | return [ 60 | [.5] * degree, 61 | [.5] * degree, 62 | [.5] * degree, 63 | ] 64 | 65 | @staticmethod 66 | def Gauss(t: float, *p: tuple[float]) -> float: 67 | result = 0 68 | for k in range(len(p) // 3): 69 | mu, sigma, s = p[3 * k:3 * k + 3] 70 | c = sqrt(2 * pi) 71 | result += s * exp(-((t - mu) / sigma)**2 / 2) / sigma / c 72 | return result 73 | 74 | @staticmethod 75 | def GaussInitialGuess(degree) -> list[list[float]]: 76 | return [ 77 | [.5, .2, 1.] * degree, 78 | [.5, .2, 1.] * degree, 79 | [.5, .2, 1.] * degree, 80 | ] 81 | -------------------------------------------------------------------------------- /imagecolorpicker/representation.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum, auto 2 | 3 | 4 | class Representation(IntEnum): 5 | ColorMap = auto() 6 | Color3 = auto() 7 | Color4 = auto() 8 | NearestWeight = auto() 9 | Colors = auto() 10 | Weights = auto() 11 | ColorMaps = auto() 12 | -------------------------------------------------------------------------------- /imagecolorpicker/team210.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeStahL/ImageColorPicker/0b57bfd839a0dea4a59d824df7cda7a45e7727c9/imagecolorpicker/team210.ico -------------------------------------------------------------------------------- /imagecolorpicker/version.py: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | Self, 3 | Optional, 4 | ) 5 | from types import ModuleType 6 | from pygit2 import ( 7 | Repository, 8 | GIT_DESCRIBE_TAGS, 9 | ) 10 | from pathlib import Path 11 | from enum import Enum 12 | from importlib.util import ( 13 | spec_from_file_location, 14 | module_from_spec, 15 | ) 16 | from importlib.machinery import ModuleSpec 17 | from importlib.resources import files 18 | import imagecolorpicker 19 | 20 | 21 | class VersionType(Enum): 22 | Unavailable = 0x0 23 | GitTag = 0x1 24 | GeneratedModule = 0x2 25 | 26 | 27 | class Version: 28 | GitRepositorySuffix = '.git' 29 | NoVCSDescription = "novcs" 30 | ImportAttemptDescription = "imported" 31 | DirtySuffix = "-dirty" 32 | VersionModuleName = 'generated_version' 33 | 34 | def __init__(self: Self) -> None: 35 | self._repositoryPath: Optional[Path] = self._findRepositoryPath() 36 | self._versionType: VersionType = VersionType.Unavailable 37 | 38 | if self.hasRepository: 39 | self._versionType = VersionType.GitTag 40 | self._repository: Repository = Repository(self.repositoryPath) 41 | 42 | self._versionModule: ModuleType = self._findVersionModule() 43 | if self.hasVersionModule: 44 | self.versionType = VersionType.GeneratedModule 45 | 46 | def _findRepositoryPath(self: Self) -> Optional[Path]: 47 | """ 48 | Return nearest git repository's path above __file__, or None 49 | if there is none available. 50 | """ 51 | path = Path(__file__) 52 | while path.parent != path: 53 | repoPath: Path = path / Version.GitRepositorySuffix 54 | 55 | if(repoPath.exists()): 56 | return repoPath 57 | 58 | path = path.parent 59 | 60 | return None 61 | 62 | @property 63 | def repositoryPath(self: Self) -> Optional[Path]: 64 | return self._repositoryPath 65 | 66 | @property 67 | def hasRepository(self: Self) -> bool: 68 | return self._repositoryPath is not None 69 | 70 | def _findVersionModule(self: Self) -> Optional[ModuleType]: 71 | """ 72 | Try to load the version number from a module 73 | which was generated at build time using `generateVersionModule`. 74 | """ 75 | try: 76 | path: Path = Path(files(imagecolorpicker)) / '{}.py'.format(Version.VersionModuleName) 77 | spec: ModuleSpec = spec_from_file_location(path.name, path) 78 | module: Optional[ModuleType] = module_from_spec(spec) 79 | spec.loader.exec_module(module) 80 | return module 81 | except: 82 | return None 83 | 84 | @property 85 | def versionModule(self: Self) -> Optional[ModuleType]: 86 | return self._versionModule 87 | 88 | @property 89 | def hasVersionModule(self: Self) -> bool: 90 | return self._versionModule is not None 91 | 92 | def describe(self: Self) -> str: 93 | """ 94 | Returns a str containing the most appropriate version description available. 95 | """ 96 | if self.hasRepository: 97 | return self._repository.describe( 98 | describe_strategy=GIT_DESCRIBE_TAGS, 99 | show_commit_oid_as_fallback=True, 100 | dirty_suffix=Version.DirtySuffix, 101 | ) 102 | 103 | if self.hasVersionModule: 104 | return self._versionModule.__version__ 105 | 106 | return Version.NoVCSDescription 107 | 108 | def generateVersionModule(self: Self, path: str) -> None: 109 | """ 110 | Generates a module containing the most appropriate version string. 111 | Use this for example from pyinstaller spec files. 112 | """ 113 | (Path(path) / (Version.VersionModuleName + '.py')).write_text(""" 114 | __version__ = '{}' 115 | """.format(self.describe())) -------------------------------------------------------------------------------- /imagecolorpicker/widgets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeStahL/ImageColorPicker/0b57bfd839a0dea4a59d824df7cda7a45e7727c9/imagecolorpicker/widgets/__init__.py -------------------------------------------------------------------------------- /imagecolorpicker/widgets/gradientwidget/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeStahL/ImageColorPicker/0b57bfd839a0dea4a59d824df7cda7a45e7727c9/imagecolorpicker/widgets/gradientwidget/__init__.py -------------------------------------------------------------------------------- /imagecolorpicker/widgets/gradientwidget/gradientwidget.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtGui import ( 2 | QPaintEvent, 3 | QPainter, 4 | QBrush, 5 | QPen, 6 | ) 7 | from PyQt6.QtWidgets import ( 8 | QWidget, 9 | QApplication, 10 | ) 11 | from PyQt6.QtCore import ( 12 | QPointF, 13 | QRect, 14 | QRectF, 15 | ) 16 | from typing import ( 17 | Self, 18 | Optional, 19 | ) 20 | from imagecolorpicker.colorgradient import ( 21 | ColorGradient, 22 | DefaultGradient1, 23 | ) 24 | from sys import argv 25 | 26 | 27 | class GradientWidget(QWidget): 28 | WeightDotSize: int = 4 29 | 30 | def __init__( 31 | self: Self, 32 | gradient: ColorGradient, 33 | parent: Optional[QWidget] = None, 34 | ) -> None: 35 | super().__init__(parent) 36 | self._gradient: ColorGradient = gradient 37 | 38 | def paintEvent( 39 | self: Self, 40 | a0: Optional[QPaintEvent], 41 | ) -> None: 42 | super().paintEvent(a0) 43 | 44 | self.paint(self.rect()) 45 | 46 | def paint( 47 | self: Self, 48 | rect: QRect, 49 | painter: Optional[QPainter] = None, 50 | ) -> None: 51 | if painter is None: 52 | painter = QPainter(self) 53 | painter.setRenderHint(QPainter.RenderHint.Antialiasing) 54 | 55 | brush: QBrush = QBrush(self._gradient.linearGradient(rect.x(), rect.width())) 56 | painter.setBrush(brush) 57 | painter.fillRect(QRectF(rect.x(), rect.y(), rect.width(), rect.height() - GradientWidget.WeightDotSize), brush) 58 | 59 | pen: QPen = QPen() 60 | pen.setWidth(2) 61 | pen.setBrush(brush) 62 | painter.setPen(pen) 63 | 64 | for weight in self._gradient.weights: 65 | painter.drawLine(QPointF(rect.x() + weight * rect.width(), rect.y() + rect.height() - 3 * GradientWidget.WeightDotSize), QPointF(rect.x() + weight * rect.width(), rect.y() + rect.height() - 2)) 66 | 67 | if __name__ == '__main__': 68 | app: QApplication = QApplication(argv) 69 | gradientWidget: GradientWidget = GradientWidget(DefaultGradient1) 70 | gradientWidget.show() 71 | app.exec() 72 | -------------------------------------------------------------------------------- /imagecolorpicker/widgets/mainwindow/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeStahL/ImageColorPicker/0b57bfd839a0dea4a59d824df7cda7a45e7727c9/imagecolorpicker/widgets/mainwindow/__init__.py -------------------------------------------------------------------------------- /imagecolorpicker/widgets/mainwindow/mainwindow.py: -------------------------------------------------------------------------------- 1 | from enum import ( 2 | IntEnum, 3 | auto, 4 | ) 5 | from importlib.resources import files 6 | import imagecolorpicker 7 | from .ui_mainwindow import Ui_MainWindow 8 | from PyQt6.QtCore import ( 9 | pyqtSignal, 10 | Qt, 11 | QUrl, 12 | QPointF, 13 | QVariant, 14 | ) 15 | from PyQt6.QtWidgets import ( 16 | QWidget, 17 | QMainWindow, 18 | QMessageBox, 19 | QApplication, 20 | ) 21 | from PyQt6.QtGui import ( 22 | QIcon, 23 | QGuiApplication, 24 | QColor, 25 | ) 26 | from typing import ( 27 | Self, 28 | Optional, 29 | ) 30 | from imagecolorpicker.importer import Importer 31 | 32 | 33 | class CoordinateType(IntEnum): 34 | AspectCorrectedBottomUp = auto() 35 | NormalizedBottomUp = auto() 36 | NormalizedTopDown = auto() 37 | 38 | 39 | class MainWindow(QMainWindow): 40 | quitRequested: pyqtSignal = pyqtSignal() 41 | cmapPasted: pyqtSignal = pyqtSignal(QVariant) 42 | 43 | UIFile = "mainwindow.ui" 44 | 45 | def __init__( 46 | self: Self, 47 | parent: Optional[QWidget] = None, 48 | flags: Qt.WindowType = Qt.WindowType.Window, 49 | ) -> None: 50 | super().__init__(parent, flags) 51 | 52 | self._ui = Ui_MainWindow() 53 | self._ui.setupUi(self) 54 | 55 | self.setWindowIcon(QIcon(str(files(imagecolorpicker) / 'team210.ico'))) 56 | 57 | self._coordinateType: CoordinateType = CoordinateType.AspectCorrectedBottomUp 58 | self._ui.actionAspect_Corrected_Top_Down.triggered.connect(lambda: self._updateCoordinateType(CoordinateType.AspectCorrectedBottomUp)) 59 | self._ui.actionNormalized_Bottom_Up.triggered.connect(lambda: self._updateCoordinateType(CoordinateType.NormalizedBottomUp)) 60 | self._ui.actionNormalized_Top_Down.triggered.connect(lambda: self._updateCoordinateType(CoordinateType.NormalizedTopDown)) 61 | 62 | # self.tabifyDockWidget(self._ui.gradientPreviewDockWidget, self._ui.gradientListDockWidget) 63 | self.tabifyDockWidget(self._ui.gradientPropertyDockWidget, self._ui.gradientColorDockWidget) 64 | 65 | self._ui.picker.hovered.connect(lambda cursor: self.statusBar().showMessage('Position: x = {}, y = {}'.format(*self._coordinates(cursor.x(), cursor.y())))) 66 | self._ui.picker.picked.connect(self._updatePickInformation) 67 | self._ui.actionQuit.triggered.connect(lambda: QApplication.exit(0)) 68 | self._ui.actionPaste.triggered.connect(self.paste) 69 | self._ui.actionAbout.triggered.connect(self.about) 70 | 71 | self._ui.actionForce_16_9_View.triggered.connect(self._force16_9View) 72 | 73 | self._ui.actionAbout_Qt.triggered.connect(self.aboutQt) 74 | 75 | def _updateCoordinateType(self: Self, coordinateType: CoordinateType) -> None: 76 | self._coordinateType = coordinateType 77 | self._ui.actionAspect_Corrected_Top_Down.setChecked(coordinateType == CoordinateType.AspectCorrectedBottomUp) 78 | self._ui.actionNormalized_Bottom_Up.setChecked(coordinateType == CoordinateType.NormalizedBottomUp) 79 | self._ui.actionNormalized_Top_Down.setChecked(coordinateType == CoordinateType.NormalizedTopDown) 80 | 81 | def _force16_9View(self: Self) -> None: 82 | w = self._ui.picker.width() 83 | h = int(9. / 16. * w) 84 | self._ui.picker.resize(w, h) 85 | 86 | def _coordinates(self: Self, qtX: float, qtY: float) -> tuple[float, float]: 87 | if self._coordinateType == CoordinateType.NormalizedTopDown: 88 | return qtX, qtY 89 | qtY = 1. - qtY 90 | if self._coordinateType == CoordinateType.NormalizedBottomUp: 91 | return qtX, qtY 92 | width: int = self._ui.picker.rect().width() 93 | height: int = self._ui.picker.rect().height() 94 | return (qtX - .5) * width / height, (qtY - .5) * height / height 95 | 96 | def _updatePickInformation(self: Self, cursor: QPointF, color: QColor) -> None: 97 | self._ui.colorLabel.setStyleSheet('background-color:{}'.format(color.name())) 98 | 99 | def paste(self: Self) -> None: 100 | clipboard = QGuiApplication.clipboard() 101 | 102 | if clipboard.mimeData().hasText(): 103 | try: 104 | self.cmapPasted.emit(Importer.HornerPolynomial(clipboard.mimeData().text())) 105 | except: 106 | pass 107 | if clipboard.mimeData().hasImage(): 108 | self._ui.picker.setImage(clipboard.image()) 109 | self._ui.picker.imageChanged.emit(self._ui.picker._image) 110 | if clipboard.mimeData().hasHtml(): 111 | self._ui.picker.loadFromHTML(clipboard.mimeData().html()) 112 | self._ui.picker.imageChanged.emit(self._ui.picker._image) 113 | if clipboard.mimeData().hasUrls(): 114 | if len(clipboard.mimeData().urls()) == 0: 115 | return 116 | 117 | # Only load first URL. 118 | url: QUrl = clipboard.mimeData().urls()[0] 119 | self._ui.picker.loadFromUrl(url) 120 | self._ui.picker.imageChanged.emit(self._ui.picker._image) 121 | 122 | def about(self: Self) -> None: 123 | aboutMessage = QMessageBox() 124 | aboutMessage.setWindowIcon(QIcon(str(files(imagecolorpicker) / 'team210.ico'))) 125 | aboutMessage.setText("Image Color Picker is GPLv3 and (c) 2023 Alexander Kraus .") 126 | aboutMessage.setWindowTitle("About Image Color Picker") 127 | aboutMessage.setIcon(QMessageBox.Icon.Information) 128 | aboutMessage.exec() 129 | 130 | def aboutQt(self: Self) -> None: 131 | QMessageBox.aboutQt(self, "About Qt...") 132 | -------------------------------------------------------------------------------- /imagecolorpicker/widgets/mainwindow/mainwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1168 10 | 666 11 | 12 | 13 | 14 | Image Color Picker by Team210 15 | 16 | 17 | 18 | 19 | 0 20 | 21 | 22 | 0 23 | 24 | 25 | 0 26 | 27 | 28 | 0 29 | 30 | 31 | 32 | 33 | 34 | 0 35 | 200 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 0 46 | 0 47 | 1168 48 | 22 49 | 50 | 51 | 52 | 53 | File 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | Edit 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | ? 76 | 77 | 78 | 79 | 80 | 81 | 82 | View 83 | 84 | 85 | 86 | Coordinates 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Gradient 98 | 99 | 100 | 101 | 102 | 103 | 104 | Color 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | Pick Information 120 | 121 | 122 | 1 123 | 124 | 125 | 126 | 127 | 9 128 | 129 | 130 | 9 131 | 132 | 133 | 9 134 | 135 | 136 | 9 137 | 138 | 139 | 140 | 141 | 142 | 0 143 | 0 144 | 145 | 146 | 147 | 148 | 0 149 | 100 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 0 164 | 0 165 | 166 | 167 | 168 | Gradient Colors 169 | 170 | 171 | 1 172 | 173 | 174 | 175 | 176 | 0 177 | 178 | 179 | 0 180 | 181 | 182 | 0 183 | 184 | 185 | 0 186 | 187 | 188 | 189 | 190 | true 191 | 192 | 193 | QAbstractItemView::NoSelection 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | Image List 203 | 204 | 205 | 2 206 | 207 | 208 | 209 | 210 | 0 211 | 212 | 213 | 0 214 | 215 | 216 | 0 217 | 218 | 219 | 0 220 | 221 | 222 | 223 | 224 | true 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | Gradient List 234 | 235 | 236 | 2 237 | 238 | 239 | 240 | 241 | 0 242 | 243 | 244 | 0 245 | 246 | 247 | 0 248 | 249 | 250 | 0 251 | 252 | 253 | 254 | 255 | true 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | Gradient Properties 265 | 266 | 267 | 1 268 | 269 | 270 | 271 | 272 | 0 273 | 274 | 275 | 0 276 | 277 | 278 | 0 279 | 280 | 281 | 0 282 | 283 | 284 | 285 | 286 | true 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | Gradient Preview 296 | 297 | 298 | 2 299 | 300 | 301 | 302 | 303 | 0 304 | 305 | 306 | 0 307 | 308 | 309 | 0 310 | 311 | 312 | 0 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | Settings 323 | 324 | 325 | 1 326 | 327 | 328 | 329 | 330 | 0 331 | 332 | 333 | 0 334 | 335 | 336 | 0 337 | 338 | 339 | 0 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | Open 350 | 351 | 352 | Ctrl+O 353 | 354 | 355 | 356 | 357 | Quit 358 | 359 | 360 | Ctrl+Q 361 | 362 | 363 | 364 | 365 | Copy 366 | 367 | 368 | Ctrl+C 369 | 370 | 371 | 372 | 373 | Paste 374 | 375 | 376 | Ctrl+V 377 | 378 | 379 | 380 | 381 | About... 382 | 383 | 384 | 385 | 386 | Extract Palette 387 | 388 | 389 | 390 | 391 | Export Palette... 392 | 393 | 394 | 395 | 396 | Import Palette... 397 | 398 | 399 | 400 | 401 | Force 16:9 View 402 | 403 | 404 | 405 | 406 | true 407 | 408 | 409 | true 410 | 411 | 412 | Aspect Corrected Top-Down 413 | 414 | 415 | 416 | 417 | true 418 | 419 | 420 | Normalized Top-Down 421 | 422 | 423 | 424 | 425 | true 426 | 427 | 428 | Normalized Bottom-Up 429 | 430 | 431 | 432 | 433 | Save 434 | 435 | 436 | Ctrl+S 437 | 438 | 439 | 440 | 441 | Add Gradient 442 | 443 | 444 | 445 | 446 | Remove Current Gradient 447 | 448 | 449 | 450 | 451 | Add Color 452 | 453 | 454 | 455 | 456 | Remove Color 457 | 458 | 459 | 460 | 461 | HLSL 462 | 463 | 464 | 465 | 466 | true 467 | 468 | 469 | Color Map 470 | 471 | 472 | 473 | 474 | true 475 | 476 | 477 | 3-Component Color 478 | 479 | 480 | 481 | 482 | true 483 | 484 | 485 | 4-Component Color 486 | 487 | 488 | 489 | 490 | true 491 | 492 | 493 | Nearest Weight 494 | 495 | 496 | 497 | 498 | true 499 | 500 | 501 | Colors 502 | 503 | 504 | 505 | 506 | true 507 | 508 | 509 | Weights 510 | 511 | 512 | 513 | 514 | true 515 | 516 | 517 | Color Maps 518 | 519 | 520 | 521 | 522 | ColorMap 523 | 524 | 525 | 526 | 527 | true 528 | 529 | 530 | Colors 531 | 532 | 533 | 534 | 535 | true 536 | 537 | 538 | Weights 539 | 540 | 541 | 542 | 543 | ColorMaps 544 | 545 | 546 | 547 | 548 | true 549 | 550 | 551 | Color Maps 552 | 553 | 554 | 555 | 556 | true 557 | 558 | 559 | Color Map 560 | 561 | 562 | 563 | 564 | true 565 | 566 | 567 | Color Map 568 | 569 | 570 | 571 | 572 | true 573 | 574 | 575 | Colors 576 | 577 | 578 | 579 | 580 | true 581 | 582 | 583 | Weights 584 | 585 | 586 | 587 | 588 | true 589 | 590 | 591 | Color Maps 592 | 593 | 594 | 595 | 596 | true 597 | 598 | 599 | Color Map 600 | 601 | 602 | 603 | 604 | true 605 | 606 | 607 | 3-Component Color 608 | 609 | 610 | 611 | 612 | true 613 | 614 | 615 | Color Map 616 | 617 | 618 | 619 | 620 | true 621 | 622 | 623 | 3-Component Color 624 | 625 | 626 | 627 | 628 | true 629 | 630 | 631 | Color Map 632 | 633 | 634 | 635 | 636 | true 637 | 638 | 639 | Colors 640 | 641 | 642 | 643 | 644 | true 645 | 646 | 647 | Weights 648 | 649 | 650 | 651 | 652 | true 653 | 654 | 655 | Color Maps 656 | 657 | 658 | 659 | 660 | true 661 | 662 | 663 | Color Map 664 | 665 | 666 | 667 | 668 | About Qt... 669 | 670 | 671 | 672 | 673 | 674 | PickableColorLabel 675 | QWidget 676 |
imagecolorpicker.widgets.pickablecolorlabel.pickablecolorlabel
677 |
678 |
679 | 680 | 681 |
682 | -------------------------------------------------------------------------------- /imagecolorpicker/widgets/mainwindow/ui_mainwindow.py: -------------------------------------------------------------------------------- 1 | # Form implementation generated from reading ui file '/home/nr4/code/ImageColorPicker/imagecolorpicker/widgets/mainwindow/mainwindow.ui' 2 | # 3 | # Created by: PyQt6 UI code generator 6.8.1 4 | # 5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is 6 | # run again. Do not edit this file unless you know what you are doing. 7 | 8 | 9 | from PyQt6 import QtCore, QtGui, QtWidgets 10 | 11 | 12 | class Ui_MainWindow(object): 13 | def setupUi(self, MainWindow): 14 | MainWindow.setObjectName("MainWindow") 15 | MainWindow.resize(1168, 666) 16 | self.centralwidget = QtWidgets.QWidget(parent=MainWindow) 17 | self.centralwidget.setObjectName("centralwidget") 18 | self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget) 19 | self.verticalLayout.setContentsMargins(0, 0, 0, 0) 20 | self.verticalLayout.setObjectName("verticalLayout") 21 | self.picker = PickableColorLabel(parent=self.centralwidget) 22 | self.picker.setMinimumSize(QtCore.QSize(0, 200)) 23 | self.picker.setObjectName("picker") 24 | self.verticalLayout.addWidget(self.picker) 25 | MainWindow.setCentralWidget(self.centralwidget) 26 | self.menubar = QtWidgets.QMenuBar(parent=MainWindow) 27 | self.menubar.setGeometry(QtCore.QRect(0, 0, 1168, 22)) 28 | self.menubar.setObjectName("menubar") 29 | self.menuFile = QtWidgets.QMenu(parent=self.menubar) 30 | self.menuFile.setObjectName("menuFile") 31 | self.menuEdit = QtWidgets.QMenu(parent=self.menubar) 32 | self.menuEdit.setObjectName("menuEdit") 33 | self.menu = QtWidgets.QMenu(parent=self.menubar) 34 | self.menu.setObjectName("menu") 35 | self.menuView = QtWidgets.QMenu(parent=self.menubar) 36 | self.menuView.setObjectName("menuView") 37 | self.menuCoordinates = QtWidgets.QMenu(parent=self.menuView) 38 | self.menuCoordinates.setObjectName("menuCoordinates") 39 | self.menuGradient = QtWidgets.QMenu(parent=self.menubar) 40 | self.menuGradient.setObjectName("menuGradient") 41 | self.menuColor = QtWidgets.QMenu(parent=self.menubar) 42 | self.menuColor.setObjectName("menuColor") 43 | MainWindow.setMenuBar(self.menubar) 44 | self.statusbar = QtWidgets.QStatusBar(parent=MainWindow) 45 | self.statusbar.setObjectName("statusbar") 46 | MainWindow.setStatusBar(self.statusbar) 47 | self.dockWidget = QtWidgets.QDockWidget(parent=MainWindow) 48 | self.dockWidget.setObjectName("dockWidget") 49 | self.dockWidgetContents = QtWidgets.QWidget() 50 | self.dockWidgetContents.setObjectName("dockWidgetContents") 51 | self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.dockWidgetContents) 52 | self.verticalLayout_2.setContentsMargins(9, 9, 9, 9) 53 | self.verticalLayout_2.setObjectName("verticalLayout_2") 54 | self.colorLabel = QtWidgets.QLabel(parent=self.dockWidgetContents) 55 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Minimum) 56 | sizePolicy.setHorizontalStretch(0) 57 | sizePolicy.setVerticalStretch(0) 58 | sizePolicy.setHeightForWidth(self.colorLabel.sizePolicy().hasHeightForWidth()) 59 | self.colorLabel.setSizePolicy(sizePolicy) 60 | self.colorLabel.setMinimumSize(QtCore.QSize(0, 100)) 61 | self.colorLabel.setText("") 62 | self.colorLabel.setObjectName("colorLabel") 63 | self.verticalLayout_2.addWidget(self.colorLabel) 64 | self.dockWidget.setWidget(self.dockWidgetContents) 65 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(1), self.dockWidget) 66 | self.gradientColorDockWidget = QtWidgets.QDockWidget(parent=MainWindow) 67 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.MinimumExpanding) 68 | sizePolicy.setHorizontalStretch(0) 69 | sizePolicy.setVerticalStretch(0) 70 | sizePolicy.setHeightForWidth(self.gradientColorDockWidget.sizePolicy().hasHeightForWidth()) 71 | self.gradientColorDockWidget.setSizePolicy(sizePolicy) 72 | self.gradientColorDockWidget.setObjectName("gradientColorDockWidget") 73 | self.gradientColorDockWidgetContents = QtWidgets.QWidget() 74 | self.gradientColorDockWidgetContents.setObjectName("gradientColorDockWidgetContents") 75 | self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.gradientColorDockWidgetContents) 76 | self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) 77 | self.verticalLayout_3.setObjectName("verticalLayout_3") 78 | self.gradientColorTableView = QtWidgets.QTableView(parent=self.gradientColorDockWidgetContents) 79 | self.gradientColorTableView.setAlternatingRowColors(True) 80 | self.gradientColorTableView.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.NoSelection) 81 | self.gradientColorTableView.setObjectName("gradientColorTableView") 82 | self.verticalLayout_3.addWidget(self.gradientColorTableView) 83 | self.gradientColorDockWidget.setWidget(self.gradientColorDockWidgetContents) 84 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(1), self.gradientColorDockWidget) 85 | self.imageListDockWidget = QtWidgets.QDockWidget(parent=MainWindow) 86 | self.imageListDockWidget.setObjectName("imageListDockWidget") 87 | self.imageListDockWidgetContents = QtWidgets.QWidget() 88 | self.imageListDockWidgetContents.setObjectName("imageListDockWidgetContents") 89 | self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.imageListDockWidgetContents) 90 | self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) 91 | self.verticalLayout_4.setObjectName("verticalLayout_4") 92 | self.imageListView = QtWidgets.QListView(parent=self.imageListDockWidgetContents) 93 | self.imageListView.setAlternatingRowColors(True) 94 | self.imageListView.setObjectName("imageListView") 95 | self.verticalLayout_4.addWidget(self.imageListView) 96 | self.imageListDockWidget.setWidget(self.imageListDockWidgetContents) 97 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.imageListDockWidget) 98 | self.gradientListDockWidget = QtWidgets.QDockWidget(parent=MainWindow) 99 | self.gradientListDockWidget.setObjectName("gradientListDockWidget") 100 | self.gradientListDockWidgetContents = QtWidgets.QWidget() 101 | self.gradientListDockWidgetContents.setObjectName("gradientListDockWidgetContents") 102 | self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.gradientListDockWidgetContents) 103 | self.verticalLayout_5.setContentsMargins(0, 0, 0, 0) 104 | self.verticalLayout_5.setObjectName("verticalLayout_5") 105 | self.gradientTableView = QtWidgets.QTableView(parent=self.gradientListDockWidgetContents) 106 | self.gradientTableView.setAlternatingRowColors(True) 107 | self.gradientTableView.setObjectName("gradientTableView") 108 | self.verticalLayout_5.addWidget(self.gradientTableView) 109 | self.gradientListDockWidget.setWidget(self.gradientListDockWidgetContents) 110 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.gradientListDockWidget) 111 | self.gradientPropertyDockWidget = QtWidgets.QDockWidget(parent=MainWindow) 112 | self.gradientPropertyDockWidget.setObjectName("gradientPropertyDockWidget") 113 | self.gradientPropertyDockWidgetContents = QtWidgets.QWidget() 114 | self.gradientPropertyDockWidgetContents.setObjectName("gradientPropertyDockWidgetContents") 115 | self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.gradientPropertyDockWidgetContents) 116 | self.verticalLayout_6.setContentsMargins(0, 0, 0, 0) 117 | self.verticalLayout_6.setObjectName("verticalLayout_6") 118 | self.gradientPropertyTableView = QtWidgets.QTableView(parent=self.gradientPropertyDockWidgetContents) 119 | self.gradientPropertyTableView.setAlternatingRowColors(True) 120 | self.gradientPropertyTableView.setObjectName("gradientPropertyTableView") 121 | self.verticalLayout_6.addWidget(self.gradientPropertyTableView) 122 | self.gradientPropertyDockWidget.setWidget(self.gradientPropertyDockWidgetContents) 123 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(1), self.gradientPropertyDockWidget) 124 | self.gradientPreviewDockWidget = QtWidgets.QDockWidget(parent=MainWindow) 125 | self.gradientPreviewDockWidget.setObjectName("gradientPreviewDockWidget") 126 | self.gradientPreviewDockWidgetContents = QtWidgets.QWidget() 127 | self.gradientPreviewDockWidgetContents.setObjectName("gradientPreviewDockWidgetContents") 128 | self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.gradientPreviewDockWidgetContents) 129 | self.verticalLayout_7.setContentsMargins(0, 0, 0, 0) 130 | self.verticalLayout_7.setObjectName("verticalLayout_7") 131 | self.gradientPreviewTableView = QtWidgets.QTableView(parent=self.gradientPreviewDockWidgetContents) 132 | self.gradientPreviewTableView.setObjectName("gradientPreviewTableView") 133 | self.verticalLayout_7.addWidget(self.gradientPreviewTableView) 134 | self.gradientPreviewDockWidget.setWidget(self.gradientPreviewDockWidgetContents) 135 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.gradientPreviewDockWidget) 136 | self.settingsDockWidget = QtWidgets.QDockWidget(parent=MainWindow) 137 | self.settingsDockWidget.setObjectName("settingsDockWidget") 138 | self.settingsDockWidgetContents = QtWidgets.QWidget() 139 | self.settingsDockWidgetContents.setObjectName("settingsDockWidgetContents") 140 | self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.settingsDockWidgetContents) 141 | self.verticalLayout_8.setContentsMargins(0, 0, 0, 0) 142 | self.verticalLayout_8.setObjectName("verticalLayout_8") 143 | self.settingsTableView = QtWidgets.QTableView(parent=self.settingsDockWidgetContents) 144 | self.settingsTableView.setObjectName("settingsTableView") 145 | self.verticalLayout_8.addWidget(self.settingsTableView) 146 | self.settingsDockWidget.setWidget(self.settingsDockWidgetContents) 147 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(1), self.settingsDockWidget) 148 | self.actionOpen = QtGui.QAction(parent=MainWindow) 149 | self.actionOpen.setObjectName("actionOpen") 150 | self.actionQuit = QtGui.QAction(parent=MainWindow) 151 | self.actionQuit.setObjectName("actionQuit") 152 | self.actionCopy = QtGui.QAction(parent=MainWindow) 153 | self.actionCopy.setObjectName("actionCopy") 154 | self.actionPaste = QtGui.QAction(parent=MainWindow) 155 | self.actionPaste.setObjectName("actionPaste") 156 | self.actionAbout = QtGui.QAction(parent=MainWindow) 157 | self.actionAbout.setObjectName("actionAbout") 158 | self.actionExtract_Palette = QtGui.QAction(parent=MainWindow) 159 | self.actionExtract_Palette.setObjectName("actionExtract_Palette") 160 | self.actionExport_Palette = QtGui.QAction(parent=MainWindow) 161 | self.actionExport_Palette.setObjectName("actionExport_Palette") 162 | self.actionImport_Palette = QtGui.QAction(parent=MainWindow) 163 | self.actionImport_Palette.setObjectName("actionImport_Palette") 164 | self.actionForce_16_9_View = QtGui.QAction(parent=MainWindow) 165 | self.actionForce_16_9_View.setObjectName("actionForce_16_9_View") 166 | self.actionAspect_Corrected_Top_Down = QtGui.QAction(parent=MainWindow) 167 | self.actionAspect_Corrected_Top_Down.setCheckable(True) 168 | self.actionAspect_Corrected_Top_Down.setChecked(True) 169 | self.actionAspect_Corrected_Top_Down.setObjectName("actionAspect_Corrected_Top_Down") 170 | self.actionNormalized_Top_Down = QtGui.QAction(parent=MainWindow) 171 | self.actionNormalized_Top_Down.setCheckable(True) 172 | self.actionNormalized_Top_Down.setObjectName("actionNormalized_Top_Down") 173 | self.actionNormalized_Bottom_Up = QtGui.QAction(parent=MainWindow) 174 | self.actionNormalized_Bottom_Up.setCheckable(True) 175 | self.actionNormalized_Bottom_Up.setObjectName("actionNormalized_Bottom_Up") 176 | self.actionSave = QtGui.QAction(parent=MainWindow) 177 | self.actionSave.setObjectName("actionSave") 178 | self.actionAdd_Gradient = QtGui.QAction(parent=MainWindow) 179 | self.actionAdd_Gradient.setObjectName("actionAdd_Gradient") 180 | self.actionRemove_Current_Gradient = QtGui.QAction(parent=MainWindow) 181 | self.actionRemove_Current_Gradient.setObjectName("actionRemove_Current_Gradient") 182 | self.actionAdd_Color = QtGui.QAction(parent=MainWindow) 183 | self.actionAdd_Color.setObjectName("actionAdd_Color") 184 | self.actionRemove_Color = QtGui.QAction(parent=MainWindow) 185 | self.actionRemove_Color.setObjectName("actionRemove_Color") 186 | self.actionHLSL = QtGui.QAction(parent=MainWindow) 187 | self.actionHLSL.setObjectName("actionHLSL") 188 | self.actionColor_Map = QtGui.QAction(parent=MainWindow) 189 | self.actionColor_Map.setCheckable(True) 190 | self.actionColor_Map.setObjectName("actionColor_Map") 191 | self.action3_Component_Color = QtGui.QAction(parent=MainWindow) 192 | self.action3_Component_Color.setCheckable(True) 193 | self.action3_Component_Color.setObjectName("action3_Component_Color") 194 | self.action4_Component_Color = QtGui.QAction(parent=MainWindow) 195 | self.action4_Component_Color.setCheckable(True) 196 | self.action4_Component_Color.setObjectName("action4_Component_Color") 197 | self.actionNearest_Weight = QtGui.QAction(parent=MainWindow) 198 | self.actionNearest_Weight.setCheckable(True) 199 | self.actionNearest_Weight.setObjectName("actionNearest_Weight") 200 | self.actionColors = QtGui.QAction(parent=MainWindow) 201 | self.actionColors.setCheckable(True) 202 | self.actionColors.setObjectName("actionColors") 203 | self.actionWeights = QtGui.QAction(parent=MainWindow) 204 | self.actionWeights.setCheckable(True) 205 | self.actionWeights.setObjectName("actionWeights") 206 | self.actionColor_Maps = QtGui.QAction(parent=MainWindow) 207 | self.actionColor_Maps.setCheckable(True) 208 | self.actionColor_Maps.setObjectName("actionColor_Maps") 209 | self.actionColorMap = QtGui.QAction(parent=MainWindow) 210 | self.actionColorMap.setObjectName("actionColorMap") 211 | self.actionColors_2 = QtGui.QAction(parent=MainWindow) 212 | self.actionColors_2.setCheckable(True) 213 | self.actionColors_2.setObjectName("actionColors_2") 214 | self.actionWeights_2 = QtGui.QAction(parent=MainWindow) 215 | self.actionWeights_2.setCheckable(True) 216 | self.actionWeights_2.setObjectName("actionWeights_2") 217 | self.actionColorMaps = QtGui.QAction(parent=MainWindow) 218 | self.actionColorMaps.setObjectName("actionColorMaps") 219 | self.actionColor_Maps_2 = QtGui.QAction(parent=MainWindow) 220 | self.actionColor_Maps_2.setCheckable(True) 221 | self.actionColor_Maps_2.setObjectName("actionColor_Maps_2") 222 | self.actionColor_Map_2 = QtGui.QAction(parent=MainWindow) 223 | self.actionColor_Map_2.setCheckable(True) 224 | self.actionColor_Map_2.setObjectName("actionColor_Map_2") 225 | self.actionColor_Map_3 = QtGui.QAction(parent=MainWindow) 226 | self.actionColor_Map_3.setCheckable(True) 227 | self.actionColor_Map_3.setObjectName("actionColor_Map_3") 228 | self.actionColors_3 = QtGui.QAction(parent=MainWindow) 229 | self.actionColors_3.setCheckable(True) 230 | self.actionColors_3.setObjectName("actionColors_3") 231 | self.actionWeights_3 = QtGui.QAction(parent=MainWindow) 232 | self.actionWeights_3.setCheckable(True) 233 | self.actionWeights_3.setObjectName("actionWeights_3") 234 | self.actionColor_Maps_3 = QtGui.QAction(parent=MainWindow) 235 | self.actionColor_Maps_3.setCheckable(True) 236 | self.actionColor_Maps_3.setObjectName("actionColor_Maps_3") 237 | self.actionColor_Map_4 = QtGui.QAction(parent=MainWindow) 238 | self.actionColor_Map_4.setCheckable(True) 239 | self.actionColor_Map_4.setObjectName("actionColor_Map_4") 240 | self.action3_Component_Color_2 = QtGui.QAction(parent=MainWindow) 241 | self.action3_Component_Color_2.setCheckable(True) 242 | self.action3_Component_Color_2.setObjectName("action3_Component_Color_2") 243 | self.actionColor_Map_5 = QtGui.QAction(parent=MainWindow) 244 | self.actionColor_Map_5.setCheckable(True) 245 | self.actionColor_Map_5.setObjectName("actionColor_Map_5") 246 | self.action3_Component_Color_3 = QtGui.QAction(parent=MainWindow) 247 | self.action3_Component_Color_3.setCheckable(True) 248 | self.action3_Component_Color_3.setObjectName("action3_Component_Color_3") 249 | self.actionColor_Map_6 = QtGui.QAction(parent=MainWindow) 250 | self.actionColor_Map_6.setCheckable(True) 251 | self.actionColor_Map_6.setObjectName("actionColor_Map_6") 252 | self.actionColors_4 = QtGui.QAction(parent=MainWindow) 253 | self.actionColors_4.setCheckable(True) 254 | self.actionColors_4.setObjectName("actionColors_4") 255 | self.actionWeights_4 = QtGui.QAction(parent=MainWindow) 256 | self.actionWeights_4.setCheckable(True) 257 | self.actionWeights_4.setObjectName("actionWeights_4") 258 | self.actionColor_Maps_4 = QtGui.QAction(parent=MainWindow) 259 | self.actionColor_Maps_4.setCheckable(True) 260 | self.actionColor_Maps_4.setObjectName("actionColor_Maps_4") 261 | self.actionColor_Map_7 = QtGui.QAction(parent=MainWindow) 262 | self.actionColor_Map_7.setCheckable(True) 263 | self.actionColor_Map_7.setObjectName("actionColor_Map_7") 264 | self.actionAbout_Qt = QtGui.QAction(parent=MainWindow) 265 | self.actionAbout_Qt.setObjectName("actionAbout_Qt") 266 | self.menuFile.addAction(self.actionOpen) 267 | self.menuFile.addSeparator() 268 | self.menuFile.addAction(self.actionSave) 269 | self.menuFile.addSeparator() 270 | self.menuFile.addAction(self.actionExport_Palette) 271 | self.menuFile.addAction(self.actionImport_Palette) 272 | self.menuFile.addSeparator() 273 | self.menuFile.addAction(self.actionQuit) 274 | self.menuEdit.addAction(self.actionCopy) 275 | self.menuEdit.addAction(self.actionPaste) 276 | self.menuEdit.addSeparator() 277 | self.menuEdit.addAction(self.actionExtract_Palette) 278 | self.menu.addAction(self.actionAbout) 279 | self.menu.addAction(self.actionAbout_Qt) 280 | self.menuCoordinates.addAction(self.actionAspect_Corrected_Top_Down) 281 | self.menuCoordinates.addAction(self.actionNormalized_Top_Down) 282 | self.menuCoordinates.addAction(self.actionNormalized_Bottom_Up) 283 | self.menuView.addAction(self.actionForce_16_9_View) 284 | self.menuView.addAction(self.menuCoordinates.menuAction()) 285 | self.menuGradient.addAction(self.actionAdd_Gradient) 286 | self.menuGradient.addAction(self.actionRemove_Current_Gradient) 287 | self.menuColor.addAction(self.actionAdd_Color) 288 | self.menuColor.addAction(self.actionRemove_Color) 289 | self.menubar.addAction(self.menuFile.menuAction()) 290 | self.menubar.addAction(self.menuEdit.menuAction()) 291 | self.menubar.addAction(self.menuGradient.menuAction()) 292 | self.menubar.addAction(self.menuColor.menuAction()) 293 | self.menubar.addAction(self.menuView.menuAction()) 294 | self.menubar.addAction(self.menu.menuAction()) 295 | 296 | self.retranslateUi(MainWindow) 297 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 298 | 299 | def retranslateUi(self, MainWindow): 300 | _translate = QtCore.QCoreApplication.translate 301 | MainWindow.setWindowTitle(_translate("MainWindow", "Image Color Picker by Team210")) 302 | self.menuFile.setTitle(_translate("MainWindow", "File")) 303 | self.menuEdit.setTitle(_translate("MainWindow", "Edit")) 304 | self.menu.setTitle(_translate("MainWindow", "?")) 305 | self.menuView.setTitle(_translate("MainWindow", "View")) 306 | self.menuCoordinates.setTitle(_translate("MainWindow", "Coordinates")) 307 | self.menuGradient.setTitle(_translate("MainWindow", "Gradient")) 308 | self.menuColor.setTitle(_translate("MainWindow", "Color")) 309 | self.dockWidget.setWindowTitle(_translate("MainWindow", "Pick Information")) 310 | self.gradientColorDockWidget.setWindowTitle(_translate("MainWindow", "Gradient Colors")) 311 | self.imageListDockWidget.setWindowTitle(_translate("MainWindow", "Image List")) 312 | self.gradientListDockWidget.setWindowTitle(_translate("MainWindow", "Gradient List")) 313 | self.gradientPropertyDockWidget.setWindowTitle(_translate("MainWindow", "Gradient Properties")) 314 | self.gradientPreviewDockWidget.setWindowTitle(_translate("MainWindow", "Gradient Preview")) 315 | self.settingsDockWidget.setWindowTitle(_translate("MainWindow", "Settings")) 316 | self.actionOpen.setText(_translate("MainWindow", "Open")) 317 | self.actionOpen.setShortcut(_translate("MainWindow", "Ctrl+O")) 318 | self.actionQuit.setText(_translate("MainWindow", "Quit")) 319 | self.actionQuit.setShortcut(_translate("MainWindow", "Ctrl+Q")) 320 | self.actionCopy.setText(_translate("MainWindow", "Copy")) 321 | self.actionCopy.setShortcut(_translate("MainWindow", "Ctrl+C")) 322 | self.actionPaste.setText(_translate("MainWindow", "Paste")) 323 | self.actionPaste.setShortcut(_translate("MainWindow", "Ctrl+V")) 324 | self.actionAbout.setText(_translate("MainWindow", "About...")) 325 | self.actionExtract_Palette.setText(_translate("MainWindow", "Extract Palette")) 326 | self.actionExport_Palette.setText(_translate("MainWindow", "Export Palette...")) 327 | self.actionImport_Palette.setText(_translate("MainWindow", "Import Palette...")) 328 | self.actionForce_16_9_View.setText(_translate("MainWindow", "Force 16:9 View")) 329 | self.actionAspect_Corrected_Top_Down.setText(_translate("MainWindow", "Aspect Corrected Top-Down")) 330 | self.actionNormalized_Top_Down.setText(_translate("MainWindow", "Normalized Top-Down")) 331 | self.actionNormalized_Bottom_Up.setText(_translate("MainWindow", "Normalized Bottom-Up")) 332 | self.actionSave.setText(_translate("MainWindow", "Save")) 333 | self.actionSave.setShortcut(_translate("MainWindow", "Ctrl+S")) 334 | self.actionAdd_Gradient.setText(_translate("MainWindow", "Add Gradient")) 335 | self.actionRemove_Current_Gradient.setText(_translate("MainWindow", "Remove Current Gradient")) 336 | self.actionAdd_Color.setText(_translate("MainWindow", "Add Color")) 337 | self.actionRemove_Color.setText(_translate("MainWindow", "Remove Color")) 338 | self.actionHLSL.setText(_translate("MainWindow", "HLSL")) 339 | self.actionColor_Map.setText(_translate("MainWindow", "Color Map")) 340 | self.action3_Component_Color.setText(_translate("MainWindow", "3-Component Color")) 341 | self.action4_Component_Color.setText(_translate("MainWindow", "4-Component Color")) 342 | self.actionNearest_Weight.setText(_translate("MainWindow", "Nearest Weight")) 343 | self.actionColors.setText(_translate("MainWindow", "Colors")) 344 | self.actionWeights.setText(_translate("MainWindow", "Weights")) 345 | self.actionColor_Maps.setText(_translate("MainWindow", "Color Maps")) 346 | self.actionColorMap.setText(_translate("MainWindow", "ColorMap")) 347 | self.actionColors_2.setText(_translate("MainWindow", "Colors")) 348 | self.actionWeights_2.setText(_translate("MainWindow", "Weights")) 349 | self.actionColorMaps.setText(_translate("MainWindow", "ColorMaps")) 350 | self.actionColor_Maps_2.setText(_translate("MainWindow", "Color Maps")) 351 | self.actionColor_Map_2.setText(_translate("MainWindow", "Color Map")) 352 | self.actionColor_Map_3.setText(_translate("MainWindow", "Color Map")) 353 | self.actionColors_3.setText(_translate("MainWindow", "Colors")) 354 | self.actionWeights_3.setText(_translate("MainWindow", "Weights")) 355 | self.actionColor_Maps_3.setText(_translate("MainWindow", "Color Maps")) 356 | self.actionColor_Map_4.setText(_translate("MainWindow", "Color Map")) 357 | self.action3_Component_Color_2.setText(_translate("MainWindow", "3-Component Color")) 358 | self.actionColor_Map_5.setText(_translate("MainWindow", "Color Map")) 359 | self.action3_Component_Color_3.setText(_translate("MainWindow", "3-Component Color")) 360 | self.actionColor_Map_6.setText(_translate("MainWindow", "Color Map")) 361 | self.actionColors_4.setText(_translate("MainWindow", "Colors")) 362 | self.actionWeights_4.setText(_translate("MainWindow", "Weights")) 363 | self.actionColor_Maps_4.setText(_translate("MainWindow", "Color Maps")) 364 | self.actionColor_Map_7.setText(_translate("MainWindow", "Color Map")) 365 | self.actionAbout_Qt.setText(_translate("MainWindow", "About Qt...")) 366 | from imagecolorpicker.widgets.pickablecolorlabel.pickablecolorlabel import PickableColorLabel 367 | -------------------------------------------------------------------------------- /imagecolorpicker/widgets/pickablecolorlabel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeStahL/ImageColorPicker/0b57bfd839a0dea4a59d824df7cda7a45e7727c9/imagecolorpicker/widgets/pickablecolorlabel/__init__.py -------------------------------------------------------------------------------- /imagecolorpicker/widgets/pickablecolorlabel/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeStahL/ImageColorPicker/0b57bfd839a0dea4a59d824df7cda7a45e7727c9/imagecolorpicker/widgets/pickablecolorlabel/default.png -------------------------------------------------------------------------------- /imagecolorpicker/widgets/pickablecolorlabel/pickablecolorlabel.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import ( 2 | pyqtSignal, 3 | QPoint, 4 | QPointF, 5 | Qt, 6 | QSizeF, 7 | QRectF, 8 | QUrl, 9 | QByteArray, 10 | ) 11 | from PyQt6.QtWidgets import ( 12 | QWidget, 13 | QApplication, 14 | ) 15 | from PyQt6.QtGui import ( 16 | QImage, 17 | QColor, 18 | QPaintEvent, 19 | QPainter, 20 | QPen, 21 | QDragEnterEvent, 22 | QDropEvent, 23 | QMouseEvent, 24 | QPixmap, 25 | ) 26 | from typing import ( 27 | Self, 28 | Optional, 29 | ) 30 | from PyQt6.QtNetwork import ( 31 | QNetworkAccessManager, 32 | QNetworkRequest, 33 | QNetworkReply, 34 | ) 35 | from sys import argv 36 | from importlib.resources import files 37 | from bs4 import BeautifulSoup 38 | from base64 import b64decode 39 | from traceback import print_exc 40 | from requests import get 41 | from imagecolorpicker.widgets import pickablecolorlabel 42 | 43 | class PickableColorLabel(QWidget): 44 | imageChanged: pyqtSignal = pyqtSignal(QImage) 45 | 46 | DefaultImage = 'default.png' 47 | CursorSize = 0.05 48 | 49 | picked = pyqtSignal(QPointF, QColor) 50 | hovered = pyqtSignal(QPointF) 51 | rightClicked = pyqtSignal(QPointF, QColor) 52 | 53 | def __init__( 54 | self: Self, 55 | parent: Optional[QWidget] = None, 56 | flags: Qt.WindowType = Qt.WindowType.Widget, 57 | ) -> None: 58 | super().__init__(parent, flags) 59 | 60 | self.setMouseTracking(True) 61 | self.setAcceptDrops(True) 62 | 63 | self._image: QImage = QImage(str(files(pickablecolorlabel) / PickableColorLabel.DefaultImage)) 64 | self._cursor: QPointF = QPointF(0., 0.) 65 | self._color: QColor = QColor() 66 | self._picking: bool = False 67 | 68 | self._networkManager: QNetworkAccessManager = QNetworkAccessManager() 69 | 70 | @property 71 | def components(self: Self) -> tuple[float, float, float]: 72 | return self._color.redF(), self._color.greenF(), self._color.blueF() 73 | 74 | def paintEvent(self: Self, a0: Optional[QPaintEvent]) -> None: 75 | super().paintEvent(a0) 76 | 77 | if a0 is None: 78 | return 79 | 80 | contrastColor: Qt.GlobalColor = Qt.GlobalColor.black if 0.299 * self._color.red() + 0.587 * self._color.green() + 0.114 * self._color.blue() > 186.0 else Qt.GlobalColor.white 81 | 82 | painter: QPainter = QPainter(self) 83 | painter.setRenderHint(QPainter.RenderHint.Antialiasing) 84 | painter.drawImage(a0.rect(), self._image, self._image.rect()) 85 | pen: QPen = painter.pen() 86 | pen.setWidthF(1.0) 87 | pen.setColor(contrastColor) 88 | painter.setPen(pen) 89 | 90 | size: QSizeF = a0.rect().size().toSizeF() 91 | absolutePosition: QPointF = QPointF(self._cursor.x() * size.width(), self._cursor.y() * size.height()) 92 | absoluteCursorSize: float = PickableColorLabel.CursorSize * size.height() 93 | painter.drawLine(absolutePosition + QPointF(-absoluteCursorSize, 0), absolutePosition + QPointF(absoluteCursorSize, 0)) 94 | painter.drawLine(absolutePosition + QPointF(0, -absoluteCursorSize), absolutePosition + QPointF(0, absoluteCursorSize)) 95 | painter.drawEllipse(QRectF(absolutePosition - .5 * QPointF(absoluteCursorSize, absoluteCursorSize) - QPointF(2.,2.), QSizeF(absoluteCursorSize + 4., absoluteCursorSize + 4.))) 96 | painter.drawEllipse(QRectF(absolutePosition - .5 * QPointF(absoluteCursorSize, absoluteCursorSize) + QPointF(2.,2.), QSizeF(absoluteCursorSize - 4., absoluteCursorSize - 4.))) 97 | 98 | pen: QPen = painter.pen() 99 | pen.setWidthF(4) 100 | pen.setColor(self._color) 101 | painter.setPen(pen) 102 | painter.drawEllipse(QRectF(absolutePosition - .5 * QPointF(absoluteCursorSize, absoluteCursorSize), QSizeF(absoluteCursorSize, absoluteCursorSize))) 103 | 104 | def mousePressEvent(self: Self, a0: Optional[QMouseEvent]) -> None: 105 | super().mousePressEvent(a0) 106 | self._picking = True 107 | self._pick(a0) 108 | 109 | def mouseReleaseEvent(self: Self, a0: Optional[QMouseEvent]) -> None: 110 | super().mousePressEvent(a0) 111 | self._picking = False 112 | 113 | def mouseMoveEvent(self, a0: Optional[QMouseEvent]) -> None: 114 | super().mouseMoveEvent(a0) 115 | if self._picking: 116 | self._pick(a0) 117 | self._hover(a0) 118 | 119 | def _hover(self: Self, a0: Optional[QMouseEvent]) -> None: 120 | if a0 is None: 121 | return 122 | 123 | absolutePosition = a0.position() 124 | size = self.size().toSizeF() 125 | relativePosition = QPointF(absolutePosition.x() / size.width(), absolutePosition.y() / size.height()) 126 | 127 | self.hovered.emit(relativePosition) 128 | 129 | def _pick(self: Self, a0: Optional[QMouseEvent]) -> None: 130 | if a0 is None: 131 | return 132 | 133 | absolutePosition: QPointF = a0.position() 134 | size: QSizeF = self.size().toSizeF() 135 | relativePosition: QPointF = QPointF(absolutePosition.x() / size.width(), absolutePosition.y() / size.height()) 136 | imageSize: QSizeF = self._image.size().toSizeF() 137 | absoluteImagePosition: QPoint = QPointF(relativePosition.x() * imageSize.width(), relativePosition.y() * imageSize.height()).toPoint() 138 | 139 | self._cursor = relativePosition 140 | self._color = self._image.pixelColor(absoluteImagePosition) 141 | self.picked.emit(self._cursor, self._color) 142 | 143 | self.update() 144 | 145 | def setImage(self: Self, image: QImage) -> None: 146 | self._image = image 147 | self.update() 148 | 149 | def dragEnterEvent(self: Self, a0: QDragEnterEvent) -> None: 150 | super().dragEnterEvent(a0) 151 | 152 | # print(a0.mimeData().formats()) 153 | 154 | if a0.mimeData() is None: 155 | return 156 | 157 | if a0.mimeData().hasImage() or \ 158 | a0.mimeData().hasHtml() or \ 159 | a0.mimeData().hasUrls(): 160 | a0.acceptProposedAction() 161 | 162 | def dropEvent(self: Self, a0: Optional[QDropEvent]) -> None: 163 | super().dropEvent(a0) 164 | 165 | if a0 is None: 166 | return 167 | 168 | if a0.mimeData().hasImage(): 169 | self.setImage(a0.mimeData().imageData()) 170 | self.imageChanged.emit(self._image) 171 | elif a0.mimeData().hasHtml(): 172 | self.loadFromHTML(a0.mimeData().html()) 173 | self.imageChanged.emit(self._image) 174 | elif a0.mimeData().hasUrls(): 175 | if len(a0.mimeData().urls()) == 0: 176 | return 177 | 178 | # Only load first URL. 179 | url: QUrl = a0.mimeData().urls()[0] 180 | self.loadFromUrl(url) 181 | self.imageChanged.emit(self._image) 182 | 183 | def loadFromUrl(self: Self, url: QUrl) -> None: 184 | request: QNetworkRequest = QNetworkRequest(url) 185 | reply: QNetworkReply = self._networkManager.get(request) 186 | result: QByteArray = reply.readAll() 187 | 188 | self.loadFromBinary(result) 189 | 190 | def loadFromHTML(self: Self, html: str) -> None: 191 | try: 192 | html: BeautifulSoup = BeautifulSoup( 193 | html, 194 | features='html.parser', 195 | ) 196 | imgTags = html.findAll('img') 197 | if len(imgTags) != 0: 198 | src: str = imgTags[0]['src'] 199 | pixmap = QPixmap() 200 | if src.startswith('data:image/png;base64,'): 201 | src = src.replace('data:image/png;base64,', '') 202 | pixmap.loadFromData(b64decode(src)) 203 | elif src.startswith('data:image/jpeg;base64,'): 204 | src = src.replace('data:image/jpeg;base64,', '') 205 | pixmap.loadFromData(b64decode(src)) 206 | else: 207 | response = get(src) 208 | pixmap.loadFromData(response.content) 209 | self.setImage(pixmap.toImage()) 210 | except: 211 | print_exc() 212 | print("Attempted to load unsupported html.") 213 | 214 | def loadFromBinary(self: Self, binary: bytes) -> None: 215 | try: 216 | pixmap = QPixmap() 217 | pixmap.loadFromData(binary) 218 | self.setImage(pixmap.toImage()) 219 | except: 220 | print_exc() 221 | print("Attempted to load unsupported image binary.") 222 | 223 | if __name__ == '__main__': 224 | app: QApplication = QApplication(argv) 225 | 226 | label: PickableColorLabel = PickableColorLabel() 227 | label.show() 228 | 229 | app.exec() 230 | -------------------------------------------------------------------------------- /imagecolorpicker/widgets/pickablecolorlabel/pickablecolorlabelplugin.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtGui import QIcon 2 | from PyQt6.QtDesigner import QPyDesignerCustomWidgetPlugin 3 | from imagecolorpicker.pickablecolorlabel import * 4 | 5 | class PickableColorLabelPlugin(QPyDesignerCustomWidgetPlugin): 6 | WidgetName = "PickableColorLabel" 7 | DomXml = """ 8 | 9 | 10 | """.format( 11 | widgetName=WidgetName, 12 | widgetInstanceName=WidgetName[0].lower() + WidgetName[1:], 13 | ) 14 | IncludeFile = "imagecolorpicker.pickablecolorlabel" 15 | ShortDescription = "Pick colors from images." 16 | WidgetGroup = "Team210 Widgets" 17 | 18 | def __init__(self): 19 | super().__init__() 20 | 21 | self._initialized = False 22 | 23 | def name(self): 24 | return PickableColorLabelPlugin.WidgetName 25 | 26 | def icon(self): 27 | return QIcon() 28 | 29 | def group(self): 30 | return PickableColorLabelPlugin.WidgetGroup 31 | 32 | def toolTip(self): 33 | return PickableColorLabelPlugin.ShortDescription 34 | 35 | def whatsThis(self): 36 | return PickableColorLabelPlugin.ShortDescription 37 | 38 | def includeFile(self): 39 | return PickableColorLabelPlugin.IncludeFile 40 | 41 | def createWidget(self, parent): 42 | return PickableColorLabel(parent) 43 | 44 | def domXml(self): 45 | return PickableColorLabelPlugin.DomXml 46 | 47 | def initialize(self, core): 48 | if self._initialized: 49 | return 50 | 51 | self._initialized = True 52 | 53 | def isContainer(self): 54 | return False 55 | 56 | def isInitialized(self): 57 | return self._initialized 58 | -------------------------------------------------------------------------------- /pyinstaller.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | from os.path import abspath, join 3 | from zipfile import ZipFile 4 | from platform import system 5 | from imagecolorpicker.version import Version 6 | from pathlib import Path 7 | 8 | moduleName = 'imagecolorpicker' 9 | rootPath = Path(".") 10 | buildPath = rootPath / 'build' 11 | distPath = rootPath / 'dist' 12 | sourcePath = rootPath / moduleName 13 | 14 | block_cipher = None 15 | 16 | version = Version() 17 | version.generateVersionModule(buildPath) 18 | 19 | a = Analysis( 20 | [ 21 | join(sourcePath, '__main__.py'), 22 | ], 23 | pathex=[], 24 | binaries=[], 25 | datas=[ 26 | (buildPath / '{}.py'.format(Version.VersionModuleName), moduleName), 27 | (join(sourcePath, 'team210.ico'), moduleName), 28 | (join(sourcePath, 'widgets', 'pickablecolorlabel', 'default.png'), join(moduleName, 'widgets', 'pickablecolorlabel')), 29 | ], 30 | hiddenimports=[ 31 | '_cffi_backend', 32 | 'scipy._lib.array_api_compat.numpy.fft', 33 | 'scipy.special._special_ufuncs', 34 | ], 35 | hookspath=[], 36 | hooksconfig={}, 37 | runtime_hooks=[], 38 | excludes=[], 39 | win_no_prefer_redirects=False, 40 | win_private_assemblies=False, 41 | cipher=block_cipher, 42 | noarchive=False, 43 | ) 44 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 45 | 46 | exe = EXE( 47 | pyz, 48 | a.scripts, 49 | a.binaries, 50 | a.zipfiles, 51 | a.datas, 52 | [], 53 | name='{}-{}'.format(moduleName, version.describe()), 54 | debug=False, 55 | bootloader_ignore_signals=False, 56 | strip=False, 57 | upx=True, 58 | upx_exclude=[], 59 | runtime_tmpdir=None, 60 | console=False, 61 | disable_windowed_traceback=False, 62 | argv_emulation=False, 63 | target_arch=None, 64 | codesign_identity=None, 65 | entitlements_file=None, 66 | icon=join(sourcePath, 'team210.ico'), 67 | ) 68 | 69 | exeFileName = '{}-{}{}'.format(moduleName, version.describe(), '.exe' if system() == 'Windows' else '') 70 | zipFileName = '{}-{}-{}.zip'.format(moduleName, version.describe(), 'windows' if system() == 'Windows' else 'linux') 71 | 72 | zipfile = ZipFile(distPath / zipFileName, mode='w') 73 | zipfile.write(distPath / exeFileName, arcname=exeFileName) 74 | zipfile.write(rootPath / 'README.md', arcname='README.md') 75 | zipfile.write(rootPath / 'LICENSE', arcname='LICENSE') 76 | zipfile.write(rootPath / 'screenshot.png', arcname='screenshot.png') 77 | zipfile.close() 78 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "imagecolorpicker" 3 | version = "0.1.0" 4 | description = "Image color picker tool by Team210" 5 | authors = ["Alexander Kraus "] 6 | license = "GPLv3" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = ">=3.11,<3.13" 11 | PyQt6 = "^6.4.2" 12 | beautifulsoup4 = "^4.12.2" 13 | requests = "^2.31.0" 14 | scipy = "^1.11.3" 15 | pyglm = "^2.7.0" 16 | lmfit = "^1.2.2" 17 | pyopengl = "^3.1.7" 18 | jinja2 = "^3.1.2" 19 | parse = "^1.19.1" 20 | pip = "^24.0" 21 | pylette = "^2.3.0" 22 | rtoml = "^0.11.0" 23 | construct = "^2.10.70" 24 | networkx = "^3.4.2" 25 | matplotlib = "^3.10.1" 26 | 27 | [tool.poetry.group.dev.dependencies] 28 | pyinstaller = "^6.0.0" 29 | distro = "^1.8.0" 30 | pygit2 = "^1.16.0" 31 | deepdiff = "^8.3.0" 32 | 33 | [build-system] 34 | requires = ["poetry-core"] 35 | build-backend = "poetry.core.masonry.api" 36 | 37 | [tool.qt-designer.widgets] 38 | site = [] 39 | source = [ 40 | ".", 41 | ] 42 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeStahL/ImageColorPicker/0b57bfd839a0dea4a59d824df7cda7a45e7727c9/screenshot.png --------------------------------------------------------------------------------