Skip to content

Commit a84f118

Browse files
committed
add complete platform_utils
1 parent b684283 commit a84f118

File tree

2 files changed

+149
-99
lines changed

2 files changed

+149
-99
lines changed

materialyoucolor/utils/color_utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,11 @@ def lstar_from_argb(argb: int) -> float:
127127
def y_from_lstar(lstar: float) -> float:
128128
return 100.0 * lab_invf((lstar + 16.0) / 116.0)
129129

130+
130131
def srgb_to_argb(srgb):
131132
return int("0xff{:06X}".format(0xFFFFFF & srgb), 16)
132133

134+
133135
def lstar_from_y(y: float) -> float:
134136
return lab_f(y / 100.0) * 116.0 - 16.0
135137

@@ -177,8 +179,10 @@ def argb_from_rgba(rgba: list[int]) -> int:
177179
a_value = clamp_component(rgba[3])
178180
return (a_value << 24) | (r_value << 16) | (g_value << 8) | b_value
179181

182+
180183
def argb_from_rgba_01(rgba: list[int]) -> int:
181-
return argb_from_rgba([int(_*255) for _ in rgba])
184+
return argb_from_rgba([int(_ * 255) for _ in rgba])
185+
182186

183187
def clamp_component(value: int) -> int:
184188
if value < 0:

materialyoucolor/utils/platform_utils.py

Lines changed: 144 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
from glob import glob as path_find
44
import math
5+
from timeit import default_timer
56

67
from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot
78
from materialyoucolor.scheme.scheme_expressive import SchemeExpressive
@@ -17,17 +18,29 @@
1718
from materialyoucolor.palettes.tonal_palette import TonalPalette
1819
from materialyoucolor.scheme.variant import Variant
1920
from materialyoucolor.utils.color_utils import argb_from_rgba_01, srgb_to_argb
21+
from materialyoucolor.utils.math_utils import sanitize_degrees_double
2022
from materialyoucolor.hct import Hct
2123
from materialyoucolor.quantize import QuantizeCelebi
2224
from materialyoucolor.score.score import Score
2325
from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors
2426

27+
autoclass = None
28+
2529
try:
2630
from jnius import autoclass
2731
from android import mActivity
32+
33+
Integer = autoclass("java.lang.Integer")
34+
BuildVERSION = autoclass("android.os.Build$VERSION")
35+
context = mActivity.getApplicationContext()
36+
WallpaperManager = autoclass("android.app.WallpaperManager").getInstance(mActivity)
37+
except Exception:
38+
pass
39+
40+
try:
2841
from PIL import Image
2942
except Exception:
30-
autoclass = None
43+
Image = None
3144

3245
SCHEMES = {
3346
"TONAL_SPOT": SchemeTonalSpot,
@@ -49,16 +62,17 @@
4962
"neutral_palette": "system_neutral1_{}",
5063
"neutral_variant_palette": "system_neutral2_{}",
5164
}
52-
65+
APPROX_TONE = 200
66+
APPROX_CHROMA = 50
5367
DEFAULT_RESIZE_BITMAP_AREA = 112 * 112
5468

5569

5670
def _is_android() -> bool:
5771
try:
5872
from android import mActivity
73+
5974
return True
6075
except Exception as e:
61-
print("Platform does not seems to be android")
6276
pass
6377
return False
6478

@@ -67,11 +81,12 @@ def save_and_resize_bitmap(drawable, path):
6781
CompressFormat = autoclass("android.graphics.Bitmap$CompressFormat")
6882
FileOutputStream = autoclass("java.io.FileOutputStream")
6983
Bitmap = autoclass("android.graphics.Bitmap")
84+
BitmapConfig = autoclass("android.graphics.Bitmap$Config")
7085
Canvas = autoclass("android.graphics.Canvas")
7186
bitmap = Bitmap.createBitmap(
7287
drawable.getIntrinsicWidth(),
7388
drawable.getIntrinsicHeight(),
74-
Bitmap.Config.ARGB_8888,
89+
BitmapConfig.ARGB_8888,
7590
)
7691
canvas = Canvas(bitmap)
7792
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight())
@@ -94,101 +109,122 @@ def save_and_resize_bitmap(drawable, path):
94109
100,
95110
FileOutputStream(path),
96111
)
97-
return bitmap.getWidth(), bitmap.getHeight
112+
return bitmap.getWidth(), bitmap.getHeight()
98113

99114

100115
def reverse_color_from_primary(color, scheme):
116+
# TODO: Find solution
117+
# Here we are using APPROX_TONE and APPROX_CHROMA
118+
# because information is lost.
119+
# Which likely will affect these colors:
120+
# primaryContainer, tertiaryContainer
121+
temp_hct = Hct.from_int(color)
101122
reversed_color = color
102-
# if scheme == "TONAL_SPOT":
103-
# temp_hct = Hct.from_int(color)
104-
123+
if scheme in ["TONAL_SPOT", "SPRITZ", "VIBRANT", "RAINBOW", "CHROMA"]:
124+
reversed_color = Hct.from_hct(temp_hct.hue, APPROX_CHROMA, APPROX_TONE).to_int()
125+
elif scheme == "EXPRESSIVE":
126+
reversed_color = Hct.from_hct(
127+
sanitize_degrees_double(temp_hct.hue - 240.0), APPROX_CHROMA, APPROX_TONE
128+
).to_int()
129+
elif scheme == "FRUIT_SALAD":
130+
reversed_color = Hct.from_hct(
131+
sanitize_degrees_double(temp_hct.hue + 50.0), APPROX_CHROMA, APPROX_TONE
132+
).to_int()
133+
elif scheme in ["FIDELITY", "CONTENT"]:
134+
# We have chroma info same as source here!
135+
reversed_color = Hct.from_hct(
136+
temp_hct.hue, temp_hct.chroma, APPROX_TONE
137+
).to_int()
105138
return reversed_color
106139

107140

108-
def get_scheme(
109-
wallpaper_path=None,
110-
fallback_scheme="TONAL_SPOT",
141+
def _get_android_12_above(
142+
logger, selected_scheme="TONAL_SPOT", contrast=0.0, dark_mode=False
143+
) -> DynamicScheme:
144+
SettingsSecure = autoclass("android.provider.Settings$Secure")
145+
theme_settings = json.loads(
146+
SettingsSecure.getString(
147+
context.getContentResolver(),
148+
SettingsSecure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES,
149+
)
150+
)
151+
# Android 14 has this method
152+
try:
153+
contrast = mActivity.getSystemService(context.UI_MODE_SERVICE).getContrast()
154+
logger("Got contrast '{}'".format(contrast))
155+
except Exception:
156+
pass
157+
158+
# See if system supports mutiple schemes
159+
if OPTION_THEME_STYLE in theme_settings.keys():
160+
selected_scheme = theme_settings[OPTION_THEME_STYLE]
161+
logger("Got system theme style '{}'".format(selected_scheme))
162+
163+
# Get system colors
164+
get_system_color = lambda color_name: srgb_to_argb(
165+
context.getColor(
166+
context.getResources().getIdentifier(
167+
COLOR_NAMES[color_name].format(APPROX_TONE),
168+
"color",
169+
"android",
170+
)
171+
)
172+
)
173+
color_names = COLOR_NAMES.copy()
174+
for color_name in COLOR_NAMES.keys():
175+
hct = Hct.from_int(get_system_color(color_name))
176+
color_names[color_name] = TonalPalette.from_hue_and_chroma(hct.hue, hct.chroma)
177+
178+
return DynamicScheme(
179+
DynamicSchemeOptions(
180+
reverse_color_from_primary(
181+
get_system_color("primary_palette"),
182+
selected_scheme,
183+
),
184+
getattr(Variant, selected_scheme),
185+
contrast,
186+
dark_mode,
187+
**color_names,
188+
)
189+
)
190+
191+
192+
def open_wallpaper_file(file_path) -> Image:
193+
try:
194+
return Image.open(file_path)
195+
except Exception:
196+
return None
197+
198+
199+
def get_dynamic_scheme(
200+
# Scheme options
111201
dark_mode=True,
112202
contrast=0.0,
113203
dynamic_color_quality=10,
204+
# Fallbacks
205+
fallback_wallpaper_path=None,
206+
fallback_scheme_name="TONAL_SPOT",
207+
force_fallback_wallpaper=False,
208+
# Logging
114209
message_logger=print,
115-
fallback_color=0xFF0000FF,
116210
logger_head="MaterialYouColor",
117-
):
211+
) -> DynamicScheme:
212+
logger = lambda message: message_logger(logger_head + " : " + message)
213+
118214
is_android = _is_android()
215+
selected_scheme = None
119216
selected_color = None
120-
logger = lambda message: message_logger(logger_head + " : " + message)
121-
dynamic_scheme = None
122217

123218
if is_android:
124-
Integer = autoclass("java.lang.Integer")
125-
BuildVERSION = autoclass("android.os.Build$VERSION")
126-
context = mActivity.getApplicationContext()
127-
WallpaperManager = autoclass("android.app.WallpaperManager").getInstance(
128-
mActivity
129-
)
130219
# For Android 12 and 12+
131220
if BuildVERSION.SDK_INT >= 31:
132-
logger("Device supports MaterialYou")
133-
SettingsSecure = autoclass("android.provider.Settings$Secure")
134-
theme_settings = json.loads(
135-
SettingsSecure.getString(
136-
context.getContentResolver(),
137-
SettingsSecure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES,
138-
)
139-
)
140-
# Android 14 has this method
141-
try:
142-
contrast = mActivity.getSystemService(
143-
context.UI_MODE_SERVICE
144-
).getContrast()
145-
logger("Got contrast '{}'".format(contrast))
146-
except Exception:
147-
pass
148-
149-
if OPTION_THEME_STYLE in theme_settings.keys():
150-
fallback_scheme = theme_settings[OPTION_THEME_STYLE]
151-
logger("Got system theme style '{}'".format(fallback_scheme))
152-
153-
color_names = COLOR_NAMES.copy()
154-
for color_name in list(COLOR_NAMES.keys())[::-1]:
155-
hct = Hct.from_int(
156-
srgb_to_argb(
157-
context.getColor(
158-
context.getResources().getIdentifier(
159-
color_names[color_name].format(100),
160-
"color",
161-
"android",
162-
)
163-
)
164-
)
165-
)
166-
color_names[color_name] = TonalPalette.from_hue_and_chroma(
167-
hct.hue, hct.chroma
168-
)
169-
170-
dynamic_scheme = DynamicScheme(
171-
DynamicSchemeOptions(
172-
# TODO: Get accurate source color
173-
srgb_to_argb(
174-
context.getColor(
175-
context.getResources().getIdentifier(
176-
COLOR_NAMES["primary_palette"].format(100),
177-
"color",
178-
"android",
179-
)
180-
)
181-
),
182-
getattr(Variant, fallback_scheme),
183-
contrast,
184-
dark_mode,
185-
**color_names
186-
)
221+
selected_scheme = _get_android_12_above(
222+
logger, selected_scheme, contrast, dark_mode
187223
)
188224

189225
# For Android 8.1 and 8.1+
190226
elif BuildVERSION.SDK_INT >= 27:
191-
logger("Device does not supports MaterialYou")
227+
logger("Device doesn't supports MaterialYou")
192228
selected_color = argb_from_rgba_01(
193229
WallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM)
194230
.getPrimaryColor()
@@ -197,57 +233,67 @@ def get_scheme(
197233
logger("Got top color from wallpaper '{}'".format(selected_color))
198234

199235
# Lower than 8.1
200-
else:
236+
elif not force_fallback_wallpaper:
201237
logger(
202-
"Device does neither supports materialyoucolor"
203-
" nor provides pregenerated colors"
238+
"Device does neither supports materialyoucolor "
239+
"nor provides pregenerated colors"
204240
)
205241
wallpaper_store_dir = context.getFilesDir().getAbsolutePath()
206242
wallpaper_file = ".wallpaper-{}.png".format(
207243
WallpaperManager.getWallpaperId(WallpaperManager.FLAG_SYSTEM)
208244
)
209-
wallpaper_path = os.path.join(wallpaper_store_dir, wallpaper_file)
245+
fallback_wallpaper_path = os.path.join(wallpaper_store_dir, wallpaper_file)
210246

211-
if not os.path.isfile(wallpaper_path):
247+
if not os.path.isfile(fallback_wallpaper_path):
212248
previous_files = path_find(
213249
os.path.join(wallpaper_store_dir, ".wallpaper-*.png")
214250
)
215251
[os.remove(file) for file in previous_files]
216252
try:
253+
# Requires `android.permission.READ_EXTERNAL_STORAGE` permission
217254
wallpaper_drawable = WallpaperManager.getDrawable()
218255
width, height = save_and_resize_bitmap(
219-
wallpaper_drawable, wallpaper_path
256+
wallpaper_drawable, fallback_wallpaper_path
220257
)
221258
logger(
222-
"Got the system wallpaper with size: '{}x{}'".format(
223-
width, height
224-
)
259+
"Resized the system wallpaper : '{}x{}'".format(width, height)
225260
)
226261
except Exception as e:
227262
logger("Failed to get system wallpaper : " + str(e))
228-
wallpaper_path = None
263+
fallback_wallpaper_path = None
229264

230-
if not selected_color and wallpaper_path:
231-
image = Image.open(wallpaper_path)
265+
if (
266+
not selected_color
267+
and fallback_wallpaper_path
268+
and (image := open_wallpaper_file(fallback_wallpaper_path))
269+
):
270+
timer_start = default_timer()
232271
pixel_len = image.width * image.height
233272
image_data = image.getdata()
234273
# TODO: Think about getting data from bitmap
235274
pixel_array = [
236275
image_data[_]
237276
for _ in range(0, pixel_len, dynamic_color_quality if not is_android else 1)
238277
]
278+
logger(
279+
f"Created an array of pixels from a "
280+
f"system wallpaper file - {default_timer() - timer_start} sec."
281+
)
282+
timer_start = default_timer()
239283
colors = QuantizeCelebi(pixel_array, 128)
240284
selected_color = Score.score(colors)[0]
241-
elif not selected_color:
242-
logger("Using defined color '{}'".format(fallback_color))
243-
selected_color = fallback_color
285+
logger(f"Got dominant colors - " f"{default_timer() - timer_start} sec.")
244286

245287
return (
246-
SCHEMES[fallback_scheme](
247-
Hct.from_int(selected_color),
248-
dark_mode,
249-
contrast,
288+
(
289+
SCHEMES[fallback_scheme_name](
290+
Hct.from_int(selected_color),
291+
dark_mode,
292+
contrast,
293+
)
294+
if selected_color
295+
else None
250296
)
251-
if not dynamic_scheme
252-
else dynamic_scheme
297+
if not selected_scheme
298+
else selected_scheme
253299
)

0 commit comments

Comments
 (0)