diff options
Diffstat (limited to 'lib/threads')
| -rw-r--r-- | lib/threads/__init__.py | 3 | ||||
| -rw-r--r-- | lib/threads/catalog.py | 95 | ||||
| -rw-r--r-- | lib/threads/color.py | 82 | ||||
| -rw-r--r-- | lib/threads/palette.py | 72 |
4 files changed, 252 insertions, 0 deletions
diff --git a/lib/threads/__init__.py b/lib/threads/__init__.py new file mode 100644 index 00000000..03cd777b --- /dev/null +++ b/lib/threads/__init__.py @@ -0,0 +1,3 @@ +from color import ThreadColor +from palette import ThreadPalette +from catalog import ThreadCatalog diff --git a/lib/threads/catalog.py b/lib/threads/catalog.py new file mode 100644 index 00000000..cebae4ff --- /dev/null +++ b/lib/threads/catalog.py @@ -0,0 +1,95 @@ +import os +from os.path import dirname, realpath +import sys +from glob import glob +from collections import Sequence +from .palette import ThreadPalette + +class _ThreadCatalog(Sequence): + """Holds a set of ThreadPalettes.""" + + def __init__(self): + self.palettes = [] + self.load_palettes(self.get_palettes_path()) + + def get_palettes_path(self): + if getattr(sys, 'frozen', None) is not None: + path = os.path.join(sys._MEIPASS, "..") + else: + path = dirname(dirname(dirname(realpath(__file__)))) + + return os.path.join(path, 'palettes') + + def load_palettes(self, path): + for palette_file in glob(os.path.join(path, '*.gpl')): + self.palettes.append(ThreadPalette(palette_file)) + + def palette_names(self): + return list(sorted(palette.name for palette in self)) + + def __getitem__(self, item): + return self.palettes[item] + + def __len__(self): + return len(self.palettes) + + def _num_exact_color_matches(self, palette, threads): + """Number of colors in stitch plan with an exact match in this palette.""" + + return sum(1 for thread in threads if thread in palette) + + def match_and_apply_palette(self, stitch_plan, palette=None): + if palette is None: + palette = self.match_palette(stitch_plan) + else: + palette = self.get_palette_by_name(palette) + + if palette is not None: + self.apply_palette(stitch_plan, palette) + + return palette + + def match_palette(self, stitch_plan): + """Figure out which color palette was used + + Scans the catalog of color palettes and chooses one that seems most + likely to be the one that the user used. A palette will only be + chosen if more tha 80% of the thread colors in the stitch plan are + exact matches for threads in the palette. + """ + + threads = [color_block.color for color_block in stitch_plan] + palettes_and_matches = [(palette, self._num_exact_color_matches(palette, threads)) + for palette in self] + palette, matches = max(palettes_and_matches, key=lambda item: item[1]) + + if matches < 0.8 * len(stitch_plan): + # if less than 80% of the colors are an exact match, + # don't use this palette + return None + else: + return palette + + def apply_palette(self, stitch_plan, palette): + for color_block in stitch_plan: + nearest = palette.nearest_color(color_block.color) + + color_block.color.name = nearest.name + color_block.color.number = nearest.number + color_block.color.manufacturer = nearest.manufacturer + + def get_palette_by_name(self, name): + for palette in self: + if palette.name == name: + return palette + +_catalog = None + +def ThreadCatalog(): + """Singleton _ThreadCatalog factory""" + + global _catalog + if _catalog is None: + _catalog = _ThreadCatalog() + + return _catalog diff --git a/lib/threads/color.py b/lib/threads/color.py new file mode 100644 index 00000000..af474127 --- /dev/null +++ b/lib/threads/color.py @@ -0,0 +1,82 @@ +import simplestyle +import re +import colorsys + + +class ThreadColor(object): + hex_str_re = re.compile('#([0-9a-z]{3}|[0-9a-z]{6})', re.I) + + def __init__(self, color, name=None, number=None, manufacturer=None): + if color is None: + self.rgb = (0, 0, 0) + elif isinstance(color, (list, tuple)): + self.rgb = tuple(color) + elif self.hex_str_re.match(color): + self.rgb = simplestyle.parseColor(color) + else: + raise ValueError("Invalid color: " + repr(color)) + + self.name = name + self.number = number + self.manufacturer = manufacturer + + def __eq__(self, other): + if isinstance(other, ThreadColor): + return self.rgb == other.rgb + else: + return self == ThreadColor(other) + + def __hash__(self): + return hash(self.rgb) + + def __ne__(self, other): + return not(self == other) + + def __repr__(self): + return "ThreadColor" + repr(self.rgb) + + def to_hex_str(self): + return "#%s" % self.hex_digits + + @property + def hex_digits(self): + return "%02X%02X%02X" % self.rgb + + @property + def rgb_normalized(self): + return tuple(channel / 255.0 for channel in self.rgb) + + @property + def font_color(self): + """Pick a color that will allow text to show up on a swatch in the printout.""" + hls = colorsys.rgb_to_hls(*self.rgb_normalized) + + # We'll use white text unless the swatch color is too light. + if hls[1] > 0.7: + return (1, 1, 1) + else: + return (254, 254, 254) + + @property + def visible_on_white(self): + """A ThreadColor similar to this one but visible on white. + + If the thread color is white, we don't want to try to draw white in the + simulation view or print white in the print-out. Choose a color that's + as close as possible to the actual thread color but is still at least + somewhat visible on a white background. + """ + + hls = list(colorsys.rgb_to_hls(*self.rgb_normalized)) + + # Capping lightness should make the color visible without changing it + # too much. + if hls[1] > 0.85: + hls[1] = 0.85 + + color = colorsys.hls_to_rgb(*hls) + + # convert back to values in the range of 0-255 + color = tuple(value * 255 for value in color) + + return ThreadColor(color, name=self.name, number=self.number, manufacturer=self.manufacturer) diff --git a/lib/threads/palette.py b/lib/threads/palette.py new file mode 100644 index 00000000..e1f47c7f --- /dev/null +++ b/lib/threads/palette.py @@ -0,0 +1,72 @@ +from collections import Set +from .color import ThreadColor +from colormath.color_objects import sRGBColor, LabColor +from colormath.color_conversions import convert_color +from colormath.color_diff import delta_e_cie1994 + + +def compare_thread_colors(color1, color2): + # K_L=2 indicates textiles + return delta_e_cie1994(color1, color2, K_L=2) + + +class ThreadPalette(Set): + """Holds a set of ThreadColors all from the same manufacturer.""" + + def __init__(self, palette_file): + self.threads = dict() + self.parse_palette_file(palette_file) + + def parse_palette_file(self, palette_file): + """Read a GIMP palette file and load thread colors. + + Example file: + + GIMP Palette + Name: Ink/Stitch: Metro + Columns: 4 + # RGB Value Color Name Number + 240 186 212 Sugar Pink 1624 + 237 171 194 Carnatio 1636 + + """ + + with open(palette_file) as palette: + line = palette.readline().strip() + if line.lower() != "gimp palette": + raise ValueError("Invalid gimp palette header") + + self.name = palette.readline().strip() + if self.name.lower().startswith('name: ink/stitch: '): + self.name = self.name[18:] + + columns_line = palette.readline() + headers_line = palette.readline() + + for line in palette: + fields = line.split("\t", 3) + thread_color = [int(field) for field in fields[:3]] + thread_name, thread_number = fields[3].strip().rsplit(" ", 1) + thread_name = thread_name.strip() + + thread = ThreadColor(thread_color, thread_name, thread_number, manufacturer=self.name) + self.threads[thread] = convert_color(sRGBColor(*thread_color, is_upscaled=True), LabColor) + + def __contains__(self, thread): + return thread in self.threads + + def __iter__(self): + return iter(self.threads) + + def __len__(self): + return len(self.threads) + + def nearest_color(self, color): + """Find the thread in this palette that looks the most like the specified color.""" + + if isinstance(color, ThreadColor): + color = color.rgb + + color = convert_color(sRGBColor(*color, is_upscaled=True), LabColor) + + return min(self, key=lambda thread: compare_thread_colors(self.threads[thread], color)) |
