mirror of
https://github.com/googlefonts/noto-emoji.git
synced 2025-07-08 13:36:40 +00:00
Add skin tone modifiing scripts
This commit is contained in:
parent
9745b6d0c1
commit
6e9f9e3bbe
18 changed files with 879 additions and 0 deletions
17
skintone/blob/base.json
Normal file
17
skintone/blob/base.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "base",
|
||||
"extension": "",
|
||||
"tolerance": 5,
|
||||
"colors":
|
||||
{
|
||||
"skin": "#FEE133",
|
||||
"stroke": "#eb8f00",
|
||||
"dark": "#242424",
|
||||
"tongue": "#e04c74",
|
||||
"tongue_dark": "#ab3d2e",
|
||||
"blue": "#40c0e7",
|
||||
"blue_stroke": "#47a9b0",
|
||||
"water": "#5f7aff",
|
||||
"water_stroke": "#4864ed"
|
||||
}
|
||||
}
|
16
skintone/blob/blob.json
Normal file
16
skintone/blob/blob.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "blob",
|
||||
"extension": "blob",
|
||||
"colors":
|
||||
{
|
||||
"skin": "#fcc21b",
|
||||
"stroke": "#fcc21b",
|
||||
"dark": "#2f2f2f",
|
||||
"tongue": "#d7598b",
|
||||
"tongue_dark": "#d7598b",
|
||||
"blue": "#4fafd8",
|
||||
"blue_stroke": "#4fafd8",
|
||||
"water": "#ffffff",
|
||||
"water_stroke": "#ffffff"
|
||||
}
|
||||
}
|
65
skintone/blobify.py
Normal file
65
skintone/blobify.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
import generate_skincolor
|
||||
import remove_gradient
|
||||
import emoji
|
||||
import argparse
|
||||
import os
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Die main-Funktion, welche die Skin-Modifier verarbeitet
|
||||
:return: Nix
|
||||
"""
|
||||
# Alle Kommandozeilenargumente hinzufügen
|
||||
parser = argparse.ArgumentParser()
|
||||
group = parser.add_mutually_exclusive_group(required = True)
|
||||
group.add_argument('--input_file', '-i', help='Input file', metavar='ifile')
|
||||
group.add_argument('--input_dir', '-d', help='Input directory', metavar='idir', default = '.')
|
||||
parser.add_argument('--mod_dir', '-m', help='Modifier directory', metavar='mdir', default = './blob')
|
||||
parser.add_argument('--base_name', '-b', help='Name of the base skin color', metavar='bname', default='base')
|
||||
# Zu dict verarbeiten
|
||||
args = vars(parser.parse_args())
|
||||
# SKin-Modifier erstellen
|
||||
modifiers = generate_skincolor.generate_modifiers(args['mod_dir'])
|
||||
# Wurde ein Verzeichnis gewählt?
|
||||
if args['input_dir']:
|
||||
process_folder(args['input_dir'], modifiers, args['base_name'])
|
||||
else:
|
||||
process_file(modifiers, modifiers, args['base_name'])
|
||||
|
||||
|
||||
def process_folder(path: str, modifiers, base) -> None:
|
||||
"""
|
||||
Entfernt die Verläufe für alle Dateien eines Ordners
|
||||
:param path: Der Pfad zur Datei
|
||||
:param modifiers: Die Modifikatoren
|
||||
:param base: Der Basistyp
|
||||
:return: Nix (ändert die Datei)
|
||||
"""
|
||||
files = os.listdir(path)
|
||||
errors = []
|
||||
for file in files:
|
||||
# Nur SVG wird derzeit unterstützt
|
||||
if os.path.splitext(file)[-1].lower() in {'.svg'}:
|
||||
err = process_file(os.path.join(path, file), modifiers, base, True)
|
||||
if err:
|
||||
errors.append(err)
|
||||
print('Es sind {} Fehler aufgetreten bei folgenden Dateien:\n {}'.format(len(errors), '\n '.join(errors)))
|
||||
|
||||
|
||||
def process_file(path, modifiers, base, folder = False):
|
||||
try:
|
||||
# Entferne Verläufe
|
||||
remove_gradient.process_file(path)
|
||||
# Erstelle ein Emoji-Objekt
|
||||
emoji_ = emoji.Emoji(modifiers, path, base)
|
||||
# Und wende die Modifier an
|
||||
emoji_.batch_modify()
|
||||
except Exception as e:
|
||||
print('Es ist ein Fehler aufgetreten beim Bearbeiten von: {}'.format(path))
|
||||
print(e)
|
||||
return path
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
116
skintone/emoji.py
Normal file
116
skintone/emoji.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
from modifier import *
|
||||
import re
|
||||
|
||||
|
||||
class Emoji:
|
||||
|
||||
# There are some cases where an FE0F character has to be applied.
|
||||
# This is the case for gender symbols which should be represented as emojis and not as characters
|
||||
fe0f_chars = {'♀', '♂'}
|
||||
|
||||
def __init__(self, modifiers: dict, path: str, base: str, end: bool = False):
|
||||
"""
|
||||
Create a new Emoji
|
||||
:param modifiers: All skin modifiers available to this emoji
|
||||
:param path: The path of the base SVG file
|
||||
:param base: Name of the base type
|
||||
:param end: True/False explicitly sets the FE0F character. fe0f_chars is used if None
|
||||
"""
|
||||
# Assignments
|
||||
self.modifiers = modifiers
|
||||
self.path = path
|
||||
self.directory = os.path.dirname(path)
|
||||
self.base = modifiers[base]
|
||||
self.name = os.path.splitext(os.path.basename(path))[0]
|
||||
self.fextension = os.path.splitext(os.path.basename(path))[1]
|
||||
self.content = self.load()
|
||||
if end is not None:
|
||||
# Explicit
|
||||
self.end = end
|
||||
else:
|
||||
# Implicit
|
||||
self.end = False
|
||||
# Does it contain an "FE0F indicator"?
|
||||
for char in Emoji.fe0f_chars:
|
||||
if(char in path):
|
||||
self.end = True
|
||||
break
|
||||
|
||||
def batch_modify(self):
|
||||
"""
|
||||
new_modified_file for all Modifiers
|
||||
:return: None
|
||||
"""
|
||||
for name, modifier in self.modifiers.items():
|
||||
# You don't need to convert from base to base
|
||||
if modifier != self.base:
|
||||
self.new_modified_file(modifier)
|
||||
print('{} auf Emoji {} angewendet.'.format(name, self.name))
|
||||
|
||||
def new_modified_file(self, modifier: Modifier):
|
||||
"""
|
||||
Creates a new skin tone variant file
|
||||
:param modifier: The Modifier
|
||||
:return: None
|
||||
"""
|
||||
# Update the SVG file
|
||||
new_content = self.generate_modified(modifier)
|
||||
# Save file
|
||||
self.save(new_content, modifier.extension)
|
||||
|
||||
def load(self) -> str:
|
||||
"""Gets the (text) content of the base SVG file of this Emoji.
|
||||
:return: the SVG file's content"""
|
||||
try:
|
||||
with open(self.path) as file:
|
||||
return file.read()
|
||||
except FileNotFoundError:
|
||||
print('File "{}" not found!'.format(self.path))
|
||||
|
||||
def generate_modified(self, modifier: Modifier):
|
||||
"""
|
||||
Creates a new skin tone variant of this Emoji
|
||||
:param modifier: The Modifier which has to be applied
|
||||
:return: The altered SVG file's content
|
||||
"""
|
||||
# We're going to work on a copy of the content
|
||||
content = self.content
|
||||
# All colors are indexed by their name. We'll replace one by one
|
||||
for old, new in modifier.replace(self.base).items():
|
||||
# Create the regular expression which will be used
|
||||
old_regex = re.compile(old, re.IGNORECASE)
|
||||
# ...Apply it
|
||||
content = old_regex.sub(new, content)
|
||||
return content
|
||||
|
||||
def save(self, content: str, extension: str) -> None:
|
||||
"""
|
||||
Save the new skin tone variant
|
||||
:param content: The new content which has been created
|
||||
:param extension: Any new characters which have to be added (usually 200d + the skin tone modifier)
|
||||
:return: None (writes the file)
|
||||
"""
|
||||
# Well, this should be obvious...
|
||||
with open(self.generate_path(extension), 'w') as file:
|
||||
file.write(content)
|
||||
|
||||
def generate_path(self, extension: str) -> str:
|
||||
"""
|
||||
Creates the file path of the newly created variant
|
||||
:param extension: All characters which have to be added to this emoji.
|
||||
:return: A str containing the path/to/emoji_variant.svg
|
||||
"""
|
||||
# Which directory? (It will be saved in the same one as the base emoji)
|
||||
directory = self.directory
|
||||
# Which is the base name of the file? (e.g. emoji_u1f973)
|
||||
basename = self.name
|
||||
# The file extension (.svg)
|
||||
fileextension = self.fextension
|
||||
base_seq = basename.split('_')
|
||||
base_seq.insert(2, extension)
|
||||
# Add FE0F?
|
||||
if self.end:
|
||||
base_seq.append('fe0f')
|
||||
basename = '_'.join(base_seq)
|
||||
# Stitch it together and return the whole file path
|
||||
return os.path.join(directory, basename) + fileextension
|
74
skintone/generate_skincolor.py
Normal file
74
skintone/generate_skincolor.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from modifier import *
|
||||
from emoji import *
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
The main function doing all the work
|
||||
:return: Nothing
|
||||
"""
|
||||
# We'll need all those command line arguments
|
||||
parser = argparse.ArgumentParser()
|
||||
group = parser.add_mutually_exclusive_group(required = True)
|
||||
group.add_argument('--input_file', '-i', help='Input file.svg', metavar='ifile')
|
||||
group.add_argument('--input_dir', '-d', help='Directory containing all base files.svg', metavar='idir', default = '.')
|
||||
parser.add_argument('--mod_dir', '-m', help='Directory containing all modifier.json', metavar='mdir', default = './skins')
|
||||
parser.add_argument('--base_name', '-b', help='Name of the base skin color (without file extensions)', metavar='bname', default='base')
|
||||
parser.add_argument('--add_end', '-e', help='Do you want to add an fe0f ZWJ-sequence?', default='auto', choices=['y','n','auto'], required=False)
|
||||
# Make a dict out of it
|
||||
args = vars(parser.parse_args())
|
||||
end = False if args['add_end'].lower() == 'n' else (True if args['add_end'].lower() == 'y' else None)
|
||||
# Create skin-Modifiers
|
||||
modifiers = generate_modifiers(args['mod_dir'])
|
||||
# Did the user chose a dir or a file?
|
||||
if args['input_dir']:
|
||||
multi_process(args['input_dir'], modifiers, args['base_name'], end)
|
||||
else:
|
||||
# Create this one Emoji object
|
||||
emoji = Emoji(modifiers, args['input_file'], args['base_name'], end)
|
||||
# Apply modifiers
|
||||
emoji.batch_modify()
|
||||
|
||||
|
||||
def generate_modifiers(path: str) -> dict:
|
||||
"""
|
||||
Parses all skin modifiers in their directory
|
||||
:param path: Directory containing the JSON files for the modifiers
|
||||
:return: A str-Modifier dict containing the modifiers, sorted by their name (name: modifier)
|
||||
"""
|
||||
modifiers = {}
|
||||
for file in os.listdir(path):
|
||||
# Ignore non-JSON files
|
||||
if os.path.splitext(file)[-1].lower() == '.json':
|
||||
# Create one Modifier out of this JSON file
|
||||
modifier = Modifier.generate_from_json(os.path.join(path, file))
|
||||
modifiers.update({modifier.name: modifier})
|
||||
return modifiers
|
||||
|
||||
|
||||
def multi_process(directory: str, modifiers: dict, base: str, end: bool = False):
|
||||
"""
|
||||
Processes one directory of Emoji files
|
||||
:param directory: The directory containing the base SVG files
|
||||
:param modifiers: All Modifiers (probably provided by generate_modifiers)
|
||||
:param base: The name of the base skin color used in the base SVG files
|
||||
:param end: If an FE0F sequence should be added
|
||||
:return: Nothing (Files will be written)
|
||||
"""
|
||||
files = os.listdir(directory)
|
||||
for file in files:
|
||||
# Ignore non-SVG-files
|
||||
if os.path.splitext(file)[-1].lower() in {'.svg'}:
|
||||
# Create a new Emoji-object
|
||||
emoji = Emoji(modifiers, os.path.join(directory, file), base, end)
|
||||
# Apply modifiers
|
||||
emoji.batch_modify()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
74
skintone/generate_skincolor_de.py
Normal file
74
skintone/generate_skincolor_de.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from modifier import *
|
||||
from emoji import *
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Die main-Funktion, welche die Skin-Modifier verarbeitet
|
||||
:return: Nix
|
||||
"""
|
||||
# Alle Kommandozeilenargumente hinzufügen
|
||||
parser = argparse.ArgumentParser()
|
||||
group = parser.add_mutually_exclusive_group(required = True)
|
||||
group.add_argument('--input_file', '-i', help='Input file', metavar='ifile')
|
||||
group.add_argument('--input_dir', '-d', help='Input directory', metavar='idir', default = '.')
|
||||
parser.add_argument('--mod_dir', '-m', help='Modifier directory', metavar='mdir', default = './skins')
|
||||
parser.add_argument('--base_name', '-b', help='Name of the base skin color', metavar='bname', default='base')
|
||||
parser.add_argument('--add_end', '-e', help='Do you want to add an fe0f ZWJ-sequence?', default='n', choices=['y','n','auto'], required=False)
|
||||
# Zu dict verarbeiten
|
||||
args = vars(parser.parse_args())
|
||||
end = False if args['add_end'].lower() == 'n' else (True if args['add_end'].lower() == 'y' else None)
|
||||
# Skin-Modifier erstellen
|
||||
modifiers = generate_modifiers(args['mod_dir'])
|
||||
# Wurde ein Verzeichnis gewählt?
|
||||
if args['input_dir']:
|
||||
multi_process(args['input_dir'], modifiers, args['base_name'], end)
|
||||
else:
|
||||
# Erstelle ein Emoji-Objekt
|
||||
emoji = Emoji(modifiers, args['input_file'], args['base_name'], end)
|
||||
# Und wende die Modifier an
|
||||
emoji.batch_modify()
|
||||
|
||||
|
||||
def generate_modifiers(path: str) -> dict:
|
||||
"""
|
||||
Holt alle Skin-Modifier aus dem Ordner
|
||||
:param path: Der Ordner mit den JSON-Dateien
|
||||
:return: Ein dict mit name: Modifier
|
||||
"""
|
||||
modifiers = {}
|
||||
for file in os.listdir(path):
|
||||
# Ist es überhaupt eine JSON-Datei?
|
||||
if os.path.splitext(file)[-1].lower() == '.json':
|
||||
# Erstelle aus der JSON-Datei und füge es ein
|
||||
modifier = Modifier.generate_from_json(os.path.join(path, file))
|
||||
modifiers.update({modifier.name: modifier})
|
||||
return modifiers
|
||||
|
||||
|
||||
def multi_process(directory: str, modifiers: dict, base: str, end: bool = False):
|
||||
"""
|
||||
Verarbeitet ein ganzes Verzeichnis mit Emojis
|
||||
:param directory: Der Ordner
|
||||
:param modifiers: Die Skin-Modifier
|
||||
:param base: Der Name des Basis-Typen
|
||||
:param end: Ob noch eine fe0f-Sequenz angefügt werden soll.
|
||||
:return: Nix
|
||||
"""
|
||||
files = os.listdir(directory)
|
||||
for file in files:
|
||||
# Nur SVG wird derzeit unterstützt
|
||||
if os.path.splitext(file)[-1].lower() in {'.svg'}:
|
||||
# Erstelle ein Emoji-Objekt
|
||||
emoji = Emoji(modifiers, os.path.join(directory, file), base, end)
|
||||
# Und wende die Modifier an
|
||||
emoji.batch_modify()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
151
skintone/gradient.py
Normal file
151
skintone/gradient.py
Normal file
|
@ -0,0 +1,151 @@
|
|||
from modifier import HexString
|
||||
import re
|
||||
|
||||
|
||||
class Gradient:
|
||||
"""
|
||||
Ein Farbverlauf mit verschiedenen Stops
|
||||
"""
|
||||
def __init__(self, stops: list, raw: str):
|
||||
"""
|
||||
Erstelle einen neuen Farbverlauf
|
||||
:param stops: Die einzelnen Stops
|
||||
:param raw: Der ursprüngliche String
|
||||
"""
|
||||
self.stops = stops
|
||||
self.raw = raw
|
||||
|
||||
def calculate_average(self) -> str:
|
||||
"""
|
||||
Berechnet die durchschnittliche Farbe mit Berücksichtigung der Offsets
|
||||
:return: Die Farbe als String
|
||||
"""
|
||||
# Platzhalter
|
||||
av_color = []
|
||||
colors = []
|
||||
# Gehe jeden Stop durch
|
||||
for stop in self.stops:
|
||||
# Hole die einzelnen Farbkomponenten
|
||||
components = stop.components
|
||||
for i, component in enumerate(components):
|
||||
# Hole die Länge/Breite ab
|
||||
length = stop.length()
|
||||
# Und berechne die Gewichtung
|
||||
components[i] = length * component.value
|
||||
colors.append(components)
|
||||
# Jetzt sind die Komponenten wichtig
|
||||
for component in zip(*colors):
|
||||
av_color.append(HexString(hex(round(sum(component)))[2:]))
|
||||
# Zum String machen
|
||||
return '#' + ''.join([str(component) for component in av_color])
|
||||
|
||||
def first_color(self) -> str:
|
||||
return self.stops[0].color
|
||||
|
||||
@staticmethod
|
||||
def from_xml(in_str: str) -> list:
|
||||
"""
|
||||
Erstelle Farbverläufe aus einem XML-String
|
||||
:param in_str: Die XML-Datei als String
|
||||
:return: Dei Farbverläufe als Liste
|
||||
"""
|
||||
# Erstellt den reg. Ausdruck
|
||||
regex = re.compile(r'(<[a-z]*Gradient .*>)((.|\n)+)(</[a-z]*Gradient>)', re.IGNORECASE)
|
||||
# Sucht alle Farbverläufe raus
|
||||
results = regex.findall(in_str)
|
||||
resulting_gradients = []
|
||||
# Erstellt die Objekte
|
||||
for result in results:
|
||||
stops = Stop.from_xml(result[1])
|
||||
resulting_gradients.append(Gradient(stops, ''.join(result)))
|
||||
return resulting_gradients
|
||||
|
||||
def replace_stops(self) -> str:
|
||||
"""
|
||||
Ersetzt die Farben der Stops durch den Durchschnitt
|
||||
:return: Der XML-String mit den Änderungen
|
||||
"""
|
||||
regex = re.compile(r'<stop offset="((\.|[0-9])*)" style="stop-color:(#[1-9,A-F]*)"/>')
|
||||
return regex.sub(r'<stop offset="\1" style="stop-color:{}"/>'.format(self.first_color().upper()), self.raw)
|
||||
|
||||
def __str__(self):
|
||||
return 'Farbverlauf mit {} Stops'.format(len(self.stops))
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.stops[key]
|
||||
|
||||
|
||||
class Stop:
|
||||
def __init__(self, offset: float, color: str, next_: float, prev: float):
|
||||
"""
|
||||
Erstellt einen neuen Stop im Farbverlauf
|
||||
:param offset: Die Position
|
||||
:param color: Die Farbe als Hex-String
|
||||
:param next_: Die nächste Position
|
||||
:param prev: Die vorherige Position
|
||||
"""
|
||||
self.next = next_
|
||||
self.prev = prev
|
||||
self.offset = offset
|
||||
self.color = color
|
||||
|
||||
def __str__(self):
|
||||
return 'Stop @ {}, Color: {}'.format(self.offset, self.color)
|
||||
|
||||
@property
|
||||
def components(self) -> list:
|
||||
"""
|
||||
Gibt die Komponenten als HexString aus
|
||||
:return: Eine Liste von HexStrings (3 Stück; RGB)
|
||||
"""
|
||||
if len(self.color) == 7: # Gibt nur RGB
|
||||
return [HexString(self.color[1:3]), HexString(self.color[3:5]), HexString(self.color[5:7])]
|
||||
|
||||
@components.setter
|
||||
def components(self, values):
|
||||
self.color = '#' + ''.join(values)
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""
|
||||
Gibt an, wie breit der Streifen ist (als int, was i.d.R 0 ist)
|
||||
:return: Die Breite als int
|
||||
"""
|
||||
upper_ = (self.next + self.offset) // 2 # prev-------self---|---next
|
||||
lower_ = (self.prev + self.offset) // 2 # prev---|---self-------next
|
||||
return int(abs(upper_ - lower_))
|
||||
|
||||
def length(self) -> float:
|
||||
"""
|
||||
Gibt die Breite des Streifens mit dieser Farbe zurück
|
||||
:return: Die Breite als float
|
||||
"""
|
||||
upper_ = (self.next + self.offset) / 2 # prev-------self---|---next
|
||||
lower_ = (self.prev + self.offset) / 2 # prev---|---self-------next
|
||||
return abs(upper_ - lower_) # prev---|==========|---next
|
||||
|
||||
@staticmethod
|
||||
def from_xml(in_str: str) -> list:
|
||||
"""
|
||||
Erstellt Stops aus einem XML-String
|
||||
:param in_str: Der String
|
||||
:return: Eine Liste mit Stop-Objekten
|
||||
"""
|
||||
# reg. Ausdruck erstellen
|
||||
regex = re.compile(r"""<stop offset="((\.|[0-9])*)" style="stop-color:(#[1-9,A-F]*)"/>""", re.IGNORECASE)
|
||||
results = regex.findall(in_str)
|
||||
stops = []
|
||||
# Parse die Resultate
|
||||
for i, result in enumerate(results):
|
||||
# Gibt es einen vorherigen Stop?
|
||||
if i > 0:
|
||||
prev = results[i-1][0]
|
||||
else:
|
||||
prev = 0
|
||||
# Gibt es einen nachfolgenden Stop?
|
||||
if i < len(results) - 1:
|
||||
next_ = results[i+1][0]
|
||||
else:
|
||||
next_ = 1
|
||||
# Das Muster ist: <stop offset="{0}" style="stop-color:{2}"/>
|
||||
stops.append(Stop(float(result[0]), result[2], float(prev), float(next_)))
|
||||
return stops
|
189
skintone/modifier.py
Normal file
189
skintone/modifier.py
Normal file
|
@ -0,0 +1,189 @@
|
|||
import json
|
||||
import os
|
||||
|
||||
|
||||
class Modifier:
|
||||
"""An Object containing all relevant information about one skin tone Modifier"""
|
||||
|
||||
def __init__(self, name: str, colors: dict, extension: str, tolerance: int = 2, *args, **kwargs):
|
||||
"""
|
||||
Creates a new skin tone modifier
|
||||
:param name: The name of this skin tone
|
||||
:param colors: All colors in 'name: color hex'-format
|
||||
:param extension: All characters to be added to the original emoji sequence (e.g. _200d_1f3b0)
|
||||
:param tolerance: The color 'radius' which is matched
|
||||
"""
|
||||
self.name = name
|
||||
self.colors = colors
|
||||
self.extension = extension
|
||||
self.tolerance = tolerance
|
||||
|
||||
def replace(self, base) -> dict:
|
||||
"""
|
||||
Creates a dict containing all replacements
|
||||
:param base: The base Modifier
|
||||
:return: A dict containing the replacement rules as regular expressions
|
||||
(e.g.: {'#(00|01|02)(00|01|02)(00|01|02)': '#ffffff', '#(10|11|12|13|14)(32|33|34|35|36)(54|55|56|57|58)': '#28923'}
|
||||
"""
|
||||
# Create a new dict
|
||||
replace = dict()
|
||||
# Color: The color's name (e.g. 'skin')
|
||||
# Value: The actual color code
|
||||
for color, value in base.colors.items():
|
||||
try:
|
||||
# Try to find an appropiate replacement
|
||||
replace.update({base.generate_tolerance(value): self.colors[color]})
|
||||
except KeyError:
|
||||
try:
|
||||
# We'll now try to ignore any extensions added with '_'
|
||||
# (e.g. "hand_2" -> "hand", "skin_boo_ya" -> "skin")
|
||||
replace.update({base.generate_tolerance(value): self.colors[color.split('_')[0]]})
|
||||
except KeyError:
|
||||
# Replacement not found
|
||||
print('Didn\'t find replacement for color {} from {} (Name: "{}" or "{}") in {}.'.format(value, base.name, color, color.split('_')[0], self.name))
|
||||
return replace
|
||||
|
||||
def generate_tolerance(self, val: str) -> str:
|
||||
"""
|
||||
Generates a color radius to get a little tolerance
|
||||
Please note this is really bad code.
|
||||
Even in comparison to the rest of this crap.
|
||||
:param val: The color's hex code (e.g. #12345D)
|
||||
:return: a regular expression covering this radius (e.g: #(10|11|12|13|14)(32|33|34|35|36)(5B|5C|5D|5E|5F))
|
||||
"""
|
||||
if len(val) == 7: # RGB
|
||||
pairs = [val[1:3], val[3:5], val[5:7]]
|
||||
else: # RGBA
|
||||
pairs = [val[1:3], val[3:5], val[5:7], val[7:9]]
|
||||
# Placeholder for the new color components
|
||||
new_pairs = []
|
||||
for pair in pairs:
|
||||
# Create a new Hex String with the two digits
|
||||
hex = HexString(pair, 0, 0xff, 2)
|
||||
# This one will contain all variations
|
||||
# (42 will become [40,41,42,43,44])
|
||||
vals = []
|
||||
# Go through all possible values
|
||||
for plus in range(-self.tolerance, self.tolerance + 1):
|
||||
try:
|
||||
# Try to add an offset
|
||||
vals.append(hex + plus)
|
||||
except ValueError:
|
||||
# Ignore if the maximum range is exceeded
|
||||
pass
|
||||
# Apply the new values
|
||||
new_pairs.append('({})'.format('|'.join((str(val) for val in vals))))
|
||||
return '#' + ''.join(new_pairs)
|
||||
|
||||
@staticmethod
|
||||
def generate_from_json(file: str):
|
||||
"""
|
||||
Creates a new Modifier object out of a JSON file
|
||||
:param file: The file path
|
||||
:return: A new Modifier parsed from this JSON file
|
||||
"""
|
||||
try:
|
||||
# Open file
|
||||
with open(file) as json_file:
|
||||
# Load JSON table
|
||||
jdict = json.loads(json_file.read())
|
||||
# Do we have a name?
|
||||
if 'name' in jdict:
|
||||
return Modifier(**jdict)
|
||||
else:
|
||||
# If not, we'll just use the file name
|
||||
return Modifier(name= os.path.splitext(os.path.basename(file))[0], **jdict)
|
||||
except FileNotFoundError:
|
||||
print("File not found ¯\_(ツ)_/¯")
|
||||
except json.JSONDecodeError:
|
||||
print("This is not a valid JSON file >:(")
|
||||
|
||||
def __str__(self):
|
||||
return '{} (uxxxx_{}): Skin tone modifier with {} different colors'.format(self.name, self.extension, len(self.colors))
|
||||
|
||||
def detailed_info(self) -> str:
|
||||
"""
|
||||
Returns more detailed information on this Modifier
|
||||
:return: A str containing some details
|
||||
"""
|
||||
return '{} (uxxxx_{}):\n {}'.format(self.name, self.extension, '\n '.join([': '.join(item) for item in list(self.colors.items())]))
|
||||
|
||||
|
||||
class HexString:
|
||||
"""
|
||||
This is a simple data type to handle conversions and some basic operations on hexadecimal strings.
|
||||
"""
|
||||
|
||||
def __init__(self, string: str, min_: int = 0, max_: int = 0xff, length: int = 2):
|
||||
"""
|
||||
Create a new HexString
|
||||
:param string: The string representation without the 0x-prefix
|
||||
:param min_: The min allowed value
|
||||
:param max_: The max allowed value
|
||||
:param length: The max zfill length
|
||||
"""
|
||||
self.string = string.zfill(length)
|
||||
self.value = int(string, 16)
|
||||
self.min_ = min_
|
||||
self.max_ = max_
|
||||
self.length = length
|
||||
|
||||
def __add__(self, other):
|
||||
"""
|
||||
Add another HexString or int
|
||||
:param other: summand
|
||||
:return: A new HexString with this operation applied
|
||||
"""
|
||||
if type(other) == int:
|
||||
# Add
|
||||
result = HexString(hex(self.value + other)[2:], self.min_, self.max_)
|
||||
# Test for range
|
||||
if result.value in range(self.min_, self.max_ + 1):
|
||||
return result
|
||||
else:
|
||||
raise ValueError('Value not in allowed range')
|
||||
if type(other) == HexString:
|
||||
# Add
|
||||
result = HexString(hex(self.value + other.value)[2:], self.min_, self.max_)
|
||||
# Test for range
|
||||
if result.value in range(self.min_, self.max_ + 1):
|
||||
return result
|
||||
else:
|
||||
raise ValueError('Value not in allowed range')
|
||||
|
||||
def __sub__(self, other):
|
||||
"""
|
||||
Sub another HexString or int
|
||||
:param other: Subtrahend
|
||||
:return: A new HexString with this operation applied
|
||||
"""
|
||||
if type(other) == int:
|
||||
# Sub
|
||||
result = HexString(hex(self.value - other)[2:], self.min_, self.max_)
|
||||
# Test for range
|
||||
if result.value in range(self.min_, self.max_ + 1):
|
||||
return result
|
||||
else:
|
||||
raise ValueError('Value not in allowed range')
|
||||
if type(other) == HexString:
|
||||
# Sub
|
||||
result = HexString(hex(self.value - other.value)[2:], self.min_, self.max_)
|
||||
# Test for range
|
||||
if result.value in range(self.min_, self.max_ + 1):
|
||||
return result
|
||||
else:
|
||||
raise ValueError('Value not in allowed range')
|
||||
|
||||
def __mul__(self, other):
|
||||
result = HexString(hex(self.value * other)[2:], self.min_, self.max_)
|
||||
# Test for range
|
||||
if result.value in range(self.min_, self.max_ + 1):
|
||||
return result
|
||||
else:
|
||||
raise ValueError('Value not in allowed range')
|
||||
|
||||
def __len__(self):
|
||||
return self.length
|
||||
|
||||
def __str__(self):
|
||||
return self.string.zfill(self.length)
|
60
skintone/remove_gradient.py
Normal file
60
skintone/remove_gradient.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
import argparse
|
||||
from gradient import *
|
||||
import os
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Die main-Funktion, welche die Farbverläufe entfernt
|
||||
:return: Nix
|
||||
"""
|
||||
# Alle Kommandozeilenargumente hinzufügen
|
||||
parser = argparse.ArgumentParser()
|
||||
group = parser.add_mutually_exclusive_group(required = True)
|
||||
group.add_argument('--input_file', '-i', help='Input file', metavar='ifile')
|
||||
group.add_argument('--input_dir', '-d', help='Input directory', metavar='idir', default = '.')
|
||||
# Zu dict verarbeiten
|
||||
args = vars(parser.parse_args())
|
||||
if args['input_dir']:
|
||||
process_folder(args['input_dir'])
|
||||
else:
|
||||
process_file(args['input_file'])
|
||||
|
||||
|
||||
def process_file(path: str) -> None:
|
||||
"""
|
||||
Entfernt die Verläufe
|
||||
:param path: Der Pfad zur Datei
|
||||
:return: Nix (ändert die Datei)
|
||||
"""
|
||||
with open(path, 'r') as file:
|
||||
text = file.read()
|
||||
# Erstelle den Reg. Ausdruck
|
||||
regex = re.compile(
|
||||
r'(<(linear|radial)Gradient .*>)(( |\n)*)(<stop.*>)(( |\n)*<stop.*>)*(( |\n)*)(</(linear|radial)Gradient>)',
|
||||
re.IGNORECASE)
|
||||
text = regex.sub(r'\1\3\5\8\10', text)
|
||||
with open(path, 'w') as file:
|
||||
file.seek(0)
|
||||
file.truncate()
|
||||
file.write(text)
|
||||
|
||||
|
||||
def process_folder(path: str) -> None:
|
||||
"""
|
||||
Entfernt die Verläufe für alle Dateien eines Ordners
|
||||
:param path: Der Pfad zur Datei
|
||||
:return: Nix (ändert die Datei)
|
||||
"""
|
||||
files = os.listdir(path)
|
||||
for file in files:
|
||||
# Nur SVG wird derzeit unterstützt
|
||||
if os.path.splitext(file)[-1].lower() in {'.svg'}:
|
||||
print(path)
|
||||
print(os.path.join(path, file))
|
||||
process_file(os.path.join(path, file))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
BIN
skintone/skins/.DS_Store
vendored
Normal file
BIN
skintone/skins/.DS_Store
vendored
Normal file
Binary file not shown.
19
skintone/skins/base.json
Normal file
19
skintone/skins/base.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "base",
|
||||
"extension": "",
|
||||
"colors":
|
||||
{
|
||||
"skin": "#fac01b",
|
||||
"skin_2": "#ffb300",
|
||||
"skin_3": "#fcc21b",
|
||||
"hand": "#fbc11b",
|
||||
"hand_2": "#fac036",
|
||||
"shadow": "#e49800",
|
||||
"shadow_2": "#e48c15",
|
||||
"shadow_3": "#e59600",
|
||||
"ear": "#e39400",
|
||||
"hair": "#6d4c41",
|
||||
"hair_beard": "#6d4c41",
|
||||
"pullover": "#00bfa5"
|
||||
}
|
||||
}
|
13
skintone/skins/dark.json
Normal file
13
skintone/skins/dark.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "dark",
|
||||
"extension": "1f3ff",
|
||||
"colors":
|
||||
{
|
||||
"skin": "#70534a",
|
||||
"hand": "#70534a",
|
||||
"shadow": "#563e37",
|
||||
"ear": "#563e37",
|
||||
"hair": "#232020",
|
||||
"pullover": "#00bfa5"
|
||||
}
|
||||
}
|
14
skintone/skins/light.json
Normal file
14
skintone/skins/light.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "light",
|
||||
"extension": "1f3fb",
|
||||
"colors":
|
||||
{
|
||||
"skin": "#fadcbc",
|
||||
"hand": "#fadcbc",
|
||||
"shadow": "#dba689",
|
||||
"ear": "#dba689",
|
||||
"hair": "#312d2d",
|
||||
"hair_beard": "212121",
|
||||
"pullover": "#00bfa5"
|
||||
}
|
||||
}
|
13
skintone/skins/medium.json
Normal file
13
skintone/skins/medium.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "medium",
|
||||
"extension": "1f3fd",
|
||||
"colors":
|
||||
{
|
||||
"skin": "#bf8f68",
|
||||
"hand": "#bf8f68",
|
||||
"shadow": "#99674f",
|
||||
"ear": "#99674f",
|
||||
"hair": "#6d4c41",
|
||||
"pullover": "#00bfa5"
|
||||
}
|
||||
}
|
13
skintone/skins/medium_dark.json
Normal file
13
skintone/skins/medium_dark.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "medium_dark",
|
||||
"extension": "1f3fe",
|
||||
"colors":
|
||||
{
|
||||
"skin": "#9b643c",
|
||||
"hand": "#9b643c",
|
||||
"shadow": "#7a4c32",
|
||||
"ear": "#7a4c32",
|
||||
"hair": "#47352d",
|
||||
"pullover": "#00bfa5"
|
||||
}
|
||||
}
|
13
skintone/skins/medium_light.json
Normal file
13
skintone/skins/medium_light.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "medium_light",
|
||||
"extension": "1f3fc",
|
||||
"colors":
|
||||
{
|
||||
"skin": "#e0bb95",
|
||||
"hand": "#e0bb95",
|
||||
"shadow": "#c48e6a",
|
||||
"ear": "#c48e6a",
|
||||
"hair": "#bfa055",
|
||||
"pullover": "#00bfa5"
|
||||
}
|
||||
}
|
16
skintone/skins/redhead.json
Normal file
16
skintone/skins/redhead.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "redhead",
|
||||
"extension": "1f9b0",
|
||||
"colors":
|
||||
{
|
||||
"skin": "#fac01b",
|
||||
"hand": "#fbc11b",
|
||||
"hand_2": "#fac036",
|
||||
"shadow": "#e49800",
|
||||
"shadow_2": "#e48c15",
|
||||
"ear": "#e39400",
|
||||
"hair": "#eb4a1e",
|
||||
"hair_beard": "#e1450a",
|
||||
"pullover": "#00bfa5"
|
||||
}
|
||||
}
|
16
skintone/skins/white.json
Normal file
16
skintone/skins/white.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "white",
|
||||
"extension": "1f9b3",
|
||||
"colors":
|
||||
{
|
||||
"skin": "#fac01b",
|
||||
"hand": "#fbc11b",
|
||||
"hand_2": "#fac036",
|
||||
"shadow": "#e49800",
|
||||
"shadow_2": "#e48c15",
|
||||
"ear": "#e39400",
|
||||
"hair": "#ececec",
|
||||
"hair_beard": "#f1f1f1",
|
||||
"pullover": "#00bfa5"
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue