Cli tool #9
@@ -61,7 +61,7 @@ Singleton {
|
|||||||
|
|
||||||
if (!isPreview) {
|
if (!isPreview) {
|
||||||
root.scheme = scheme.name;
|
root.scheme = scheme.name;
|
||||||
flavour = scheme.flavour;
|
flavour = scheme.flavor;
|
||||||
currentLight = scheme.mode === "light";
|
currentLight = scheme.mode === "light";
|
||||||
} else {
|
} else {
|
||||||
previewLight = scheme.mode === "light";
|
previewLight = scheme.mode === "light";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Item {
|
|||||||
implicitWidth: textMetrics.width + contentRow.spacing + 30
|
implicitWidth: textMetrics.width + contentRow.spacing + 30
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
property color textColor: DynamicColors.palette.m3secondary
|
property color textColor: DynamicColors.palette.m3onSurface
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,6 +2,7 @@ from typing import Annotated, Optional
|
|||||||
import typer
|
import typer
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from zshell.utils.schemepalettes import PRESETS
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from materialyoucolor.quantize import QuantizeCelebi
|
from materialyoucolor.quantize import QuantizeCelebi
|
||||||
@@ -23,13 +24,20 @@ def generate(
|
|||||||
"fruit-salad", help="Color scheme algorithm to use for image mode. Ignored in preset mode."),
|
"fruit-salad", help="Color scheme algorithm to use for image mode. Ignored in preset mode."),
|
||||||
# preset inputs (optional - used for preset mode)
|
# preset inputs (optional - used for preset mode)
|
||||||
preset: Optional[str] = typer.Option(
|
preset: Optional[str] = typer.Option(
|
||||||
None, help="Name of a premade scheme (preset)."),
|
None, help="Name of a premade scheme in this format: <preset_name>:<preset_flavor>"),
|
||||||
flavour: str = typer.Option("default", help="Flavor of the preset scheme."),
|
|
||||||
mode: str = typer.Option(
|
mode: str = typer.Option(
|
||||||
"dark", help="Mode of the preset scheme (dark or light)."),
|
"dark", help="Mode of the preset scheme (dark or light)."),
|
||||||
# output (required)
|
# output (required)
|
||||||
output: Path = typer.Option(..., help="Output JSON path.")
|
output: Path = typer.Option(..., help="Output JSON path.")
|
||||||
):
|
):
|
||||||
|
if preset is None and image_path is None:
|
||||||
|
raise typer.BadParameter(
|
||||||
|
"Either --image-path or --preset must be provided.")
|
||||||
|
|
||||||
|
if preset is not None and image_path is not None:
|
||||||
|
raise typer.BadParameter(
|
||||||
|
"Use either --image-path or --preset, not both.")
|
||||||
|
|
||||||
match scheme:
|
match scheme:
|
||||||
case "fruit-salad":
|
case "fruit-salad":
|
||||||
from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme
|
from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme
|
||||||
@@ -52,47 +60,6 @@ def generate(
|
|||||||
case _:
|
case _:
|
||||||
from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme
|
from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme
|
||||||
|
|
||||||
def normalize_hex(s: str) -> str:
|
|
||||||
# Accepts "6a73ac", "#6a73ac", "B5CCBA" and returns "#B5CCBA"
|
|
||||||
s = s.strip()
|
|
||||||
if s.startswith("#"):
|
|
||||||
s = s[1:]
|
|
||||||
# If the value contains alpha (8 chars), drop alpha and keep RGB (last 6).
|
|
||||||
if len(s) == 8:
|
|
||||||
s = s[2:] # assume AARRGGBB -> drop AA
|
|
||||||
if len(s) != 6:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid hex color '{s}' (expected 6 hex digits, optionally prefixed with '#').")
|
|
||||||
return f"#{s.upper()}"
|
|
||||||
|
|
||||||
def parse_preset_file(file_path: Path) -> dict:
|
|
||||||
"""
|
|
||||||
Parse a preset scheme file with lines like:
|
|
||||||
primary_paletteKeyColor 6a73ac
|
|
||||||
background 131317
|
|
||||||
onBackground e4e1e7
|
|
||||||
Returns a dict mapping key -> "#RRGGBB"
|
|
||||||
"""
|
|
||||||
mapping = {}
|
|
||||||
with file_path.open("r", encoding="utf-8") as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
|
||||||
if not line or line.startswith("#"):
|
|
||||||
continue
|
|
||||||
parts = line.split()
|
|
||||||
if len(parts) < 2:
|
|
||||||
# ignore malformed lines
|
|
||||||
continue
|
|
||||||
key = parts[0].strip()
|
|
||||||
value = parts[1].strip()
|
|
||||||
try:
|
|
||||||
mapping[key] = normalize_hex(value)
|
|
||||||
except ValueError:
|
|
||||||
# skip invalid hex but log to stdout
|
|
||||||
print(
|
|
||||||
f"Warning: skipping invalid color value for '{key}': {value}")
|
|
||||||
return mapping
|
|
||||||
|
|
||||||
def generate_thumbnail(image_path, thumbnail_path, size=(128, 128)):
|
def generate_thumbnail(image_path, thumbnail_path, size=(128, 128)):
|
||||||
thumbnail_file = Path(thumbnail_path)
|
thumbnail_file = Path(thumbnail_path)
|
||||||
|
|
||||||
@@ -103,8 +70,8 @@ def generate(
|
|||||||
thumbnail_file.parent.mkdir(parents=True, exist_ok=True)
|
thumbnail_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
image.save(thumbnail_path, "JPEG")
|
image.save(thumbnail_path, "JPEG")
|
||||||
|
|
||||||
def generate_color_scheme(thumbnail_path, output_path):
|
def seed_from_image(image_path: Path) -> Hct:
|
||||||
image = Image.open(thumbnail_path)
|
image = Image.open(image_path)
|
||||||
pixel_len = image.width * image.height
|
pixel_len = image.width * image.height
|
||||||
image_data = image.getdata()
|
image_data = image.getdata()
|
||||||
|
|
||||||
@@ -112,11 +79,27 @@ def generate(
|
|||||||
pixel_array = [image_data[_] for _ in range(0, pixel_len, quality)]
|
pixel_array = [image_data[_] for _ in range(0, pixel_len, quality)]
|
||||||
|
|
||||||
result = QuantizeCelebi(pixel_array, 128)
|
result = QuantizeCelebi(pixel_array, 128)
|
||||||
score = Score.score(result)[0]
|
return Hct.from_int(Score.score(result)[0])
|
||||||
|
|
||||||
|
def seed_from_preset(name: str) -> Hct:
|
||||||
|
try:
|
||||||
|
return PRESETS[name].primary
|
||||||
|
except KeyError:
|
||||||
|
raise typer.BadParameter(
|
||||||
|
f"Preset '{name}' not found. Available presets: {', '.join(PRESETS.keys())}")
|
||||||
|
|
||||||
|
def generate_color_scheme(seed: Hct, mode: str) -> dict[str, str]:
|
||||||
|
|
||||||
|
is_dark = mode.lower() == "dark"
|
||||||
|
|
||||||
|
if is_dark:
|
||||||
|
seed = Hct.from_hct(seed.hue, seed.chroma, min(seed.tone, 20))
|
||||||
|
else:
|
||||||
|
seed = Hct.from_hct(seed.hue, seed.chroma, max(seed.tone, 70))
|
||||||
|
|
||||||
scheme = Scheme(
|
scheme = Scheme(
|
||||||
Hct.from_int(score),
|
seed,
|
||||||
True,
|
is_dark,
|
||||||
0.0
|
0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -127,65 +110,34 @@ def generate(
|
|||||||
color_int = color_name.get_hct(scheme).to_int()
|
color_int = color_name.get_hct(scheme).to_int()
|
||||||
color_dict[color] = int_to_hex(color_int)
|
color_dict[color] = int_to_hex(color_int)
|
||||||
|
|
||||||
output_dict = {
|
return color_dict
|
||||||
"name": "dynamic",
|
|
||||||
"flavour": "default",
|
|
||||||
"mode": "dark",
|
|
||||||
"variant": "tonalspot",
|
|
||||||
"colors": color_dict
|
|
||||||
}
|
|
||||||
|
|
||||||
output_file = Path(output_path)
|
|
||||||
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
with open(output_file, "w") as f:
|
|
||||||
json.dump(output_dict, f, indent=4)
|
|
||||||
|
|
||||||
def generate_color_scheme_from_preset(preset_mapping: dict, output_path: Path, preset_name: str, flavour: str):
|
|
||||||
"""
|
|
||||||
Build JSON output using keys from the preset file.
|
|
||||||
Any keys in preset_mapping are included verbatim in the "colors" object.
|
|
||||||
"""
|
|
||||||
color_dict = {}
|
|
||||||
for k, v in preset_mapping.items():
|
|
||||||
color_dict[k] = v # v already normalized to "#RRGGBB"
|
|
||||||
|
|
||||||
output_dict = {
|
|
||||||
"name": preset_name,
|
|
||||||
"flavour": flavour,
|
|
||||||
"mode": mode, # could be made configurable / auto-detected
|
|
||||||
"variant": scheme,
|
|
||||||
"colors": color_dict
|
|
||||||
}
|
|
||||||
|
|
||||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
with output_path.open("w", encoding="utf-8") as f:
|
|
||||||
json.dump(output_dict, f, indent=4)
|
|
||||||
|
|
||||||
def int_to_hex(argb_int):
|
def int_to_hex(argb_int):
|
||||||
return "#{:06X}".format(argb_int & 0xFFFFFF)
|
return "#{:06X}".format(argb_int & 0xFFFFFF)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if preset:
|
if preset:
|
||||||
# try local presets directory: presets/<preset>/<flavour>.txt or presets/<preset>-<flavour>.txt
|
seed = seed_from_preset(preset)
|
||||||
base1 = Path(__file__).parent.parent / "assets" / "schemes" / \
|
colors = generate_color_scheme(seed, mode)
|
||||||
preset / flavour / f"{mode}.txt"
|
name, flavor = preset.split(":")
|
||||||
if base1.exists():
|
|
||||||
preset_path = base1
|
|
||||||
else:
|
else:
|
||||||
raise FileNotFoundError(
|
|
||||||
f"Preset file not found. Looked for: {base1.resolve()}. "
|
|
||||||
"You can also pass --preset-file <path> directly."
|
|
||||||
)
|
|
||||||
|
|
||||||
mapping = parse_preset_file(preset_path)
|
|
||||||
generate_color_scheme_from_preset(
|
|
||||||
mapping, output, preset or preset_path.stem, flavour)
|
|
||||||
typer.echo(f"Wrote preset-based scheme to {output}")
|
|
||||||
raise typer.Exit()
|
|
||||||
|
|
||||||
generate_thumbnail(image_path, str(thumbnail_path))
|
generate_thumbnail(image_path, str(thumbnail_path))
|
||||||
generate_color_scheme(str(thumbnail_path), output)
|
seed = seed_from_image(thumbnail_path)
|
||||||
|
colors = generate_color_scheme(seed, mode)
|
||||||
|
name = "dynamic"
|
||||||
|
flavor = "default"
|
||||||
|
|
||||||
|
output_dict = {
|
||||||
|
"name": name,
|
||||||
|
"flavor": flavor,
|
||||||
|
"mode": mode,
|
||||||
|
"variant": scheme,
|
||||||
|
"colors": colors
|
||||||
|
}
|
||||||
|
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(output, "w") as f:
|
||||||
|
json.dump(output_dict, f, indent=4)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
# with open(output, "w") as f:
|
# with open(output, "w") as f:
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,30 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from materialyoucolor.hct.hct import Hct
|
||||||
|
from typing import Mapping
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SeedPalette:
|
||||||
|
primary: Hct
|
||||||
|
secondary: Hct
|
||||||
|
tertiary: Hct
|
||||||
|
neutral: Hct
|
||||||
|
neutral_variant: Hct
|
||||||
|
error: Hct | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def hex_to_hct(hex_: str) -> Hct:
|
||||||
|
return Hct.from_int(int(f"0xFF{hex_}", 16))
|
||||||
|
|
||||||
|
|
||||||
|
CATPPUCCIN_MACCHIATO = SeedPalette(
|
||||||
|
primary=hex_to_hct("C6A0F6"),
|
||||||
|
secondary=hex_to_hct("7DC4E4"),
|
||||||
|
tertiary=hex_to_hct("F5BDE6"),
|
||||||
|
neutral=hex_to_hct("24273A"),
|
||||||
|
neutral_variant=hex_to_hct("363A4F"),
|
||||||
|
)
|
||||||
|
|
||||||
|
PRESETS: Mapping[str, SeedPalette] = {
|
||||||
|
"catppuccin:macchiato": CATPPUCCIN_MACCHIATO,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user