magic dithering

This commit is contained in:
2025-07-21 21:14:40 +02:00
parent b4420e334d
commit ee078d9da9

View File

@ -14,9 +14,12 @@ import pandas as pd
import re import re
from enum import Enum from enum import Enum
MAX_ALBUM_IMG_SIZE = 512
class CardStyle(str, Enum): class CardStyle(str, Enum):
FULL_ALBUM_COVER = "full album cover" FULL_ALBUM_COVER = "full album cover"
MINIMAL = "minimal" MINIMAL = "minimal"
PRINT_FRIENDLY_BW = "dithered bw album cover (slow, print friendly)"
class Hipsterfy(object): class Hipsterfy(object):
@ -92,7 +95,7 @@ class HipsterfyPlaylistItem(object):
if additional_properties: if additional_properties:
for prop in additional_properties: for prop in additional_properties:
value = getattr(self, f"{prop}", "N/A") value = getattr(self, f"{prop}", "N/A")
additional_html += f"<div style='margin: 4px 0; color:#fff;'><strong style='color:#fff;'>{prop.replace('_', ' ').title()}:</strong> <span style='color:#fff;'>{value}</span></div>" additional_html += f"<div style='margin: 4px 0; color:#e0e0e0;'><strong style='color:#fff;'>{prop.replace('_', ' ').title()}:</strong> <span style='color:#fff;'>{value}</span></div>"
# Style für die innere Box (Textfeld) # Style für die innere Box (Textfeld)
inner_box_style_full = """ inner_box_style_full = """
@ -129,6 +132,54 @@ class HipsterfyPlaylistItem(object):
z-index: 1; z-index: 1;
""" """
if CardStyle(card_style) == CardStyle.PRINT_FRIENDLY_BW and self.album_images and len(self.album_images) > 0:
album_b64 = self.get_bw_album_img_base64()
outer_style = f"""
width: 300px; height: 300px;
border: 2px solid #222;
border-radius: 18px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: url('data:image/png;base64,{album_b64}') center center/cover no-repeat;
background-blend-mode: multiply;
background-color: #fff;
margin: 18px;
box-shadow: none;
color: #222;
overflow: hidden;
position: relative;
"""
inner_box_style = """
width: 250px;
height: 250px;
margin: auto;
background: rgba(255,255,255,0.9);
border-radius: 18px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
padding: 18px 12px 12px 12px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
box-sizing: border-box;
position: relative;
z-index: 1;
"""
primary_color = "#222"
secondary_color = "#444"
additional_html = additional_html.replace("color:#fff", f"color:{secondary_color}")
additional_html = additional_html.replace("color:#e0e0e0", f"color:{secondary_color}")
return f"""
<div style="{outer_style}">
<div style="{inner_box_style}">
<p style="margin: 0 0 8px 0; font-size: 1.1em; color: {secondary_color};">{getattr(self, secondary_property)}</p>
<h3 style="margin: 0 0 8px 0; font-size: 1.25em; color: {primary_color};">{getattr(self, primary_property)}</h3>
{additional_html}
</div>
</div>
"""
# FULL_ALBUM_COVER: Albumcover als Hintergrund, weiße Schrift, abgedunkelte Box # FULL_ALBUM_COVER: Albumcover als Hintergrund, weiße Schrift, abgedunkelte Box
if CardStyle(card_style) == CardStyle.FULL_ALBUM_COVER and self.album_images and len(self.album_images) > 0: if CardStyle(card_style) == CardStyle.FULL_ALBUM_COVER and self.album_images and len(self.album_images) > 0:
album_url = self.album_images[0]['url'] album_url = self.album_images[0]['url']
@ -151,7 +202,6 @@ class HipsterfyPlaylistItem(object):
transition: box-shadow 0.2s; transition: box-shadow 0.2s;
""" """
text_color = "#fff" text_color = "#fff"
additional_html = additional_html.replace("color:#fff", f"color:{text_color}")
primary_color = "#fff" primary_color = "#fff"
secondary_color = "#e0e0e0" secondary_color = "#e0e0e0"
inner_box_style = inner_box_style_full inner_box_style = inner_box_style_full
@ -313,6 +363,32 @@ class HipsterfyPlaylistItem(object):
def qr_html(self): def qr_html(self):
return self._qr_html return self._qr_html
def get_bw_album_img_html(self):
b64 = self.get_bw_album_img_base64()
if b64:
return f'<img src="data:image/png;base64,{b64}" style="width:{MAX_ALBUM_IMG_SIZE}px;height:{MAX_ALBUM_IMG_SIZE}px;border-radius:12px;box-shadow:0 2px 8px #aaa;margin-bottom:8px;" alt="Album Art BW"/>'
return ""
def get_bw_album_img_base64(self):
if self.album_images and len(self.album_images) > 0:
url = self.album_images[0]['url']
try:
import requests
from PIL import Image, ImageOps, ImageEnhance
response = requests.get(url)
img = Image.open(BytesIO(response.content)).convert("L")
img.thumbnail((MAX_ALBUM_IMG_SIZE, MAX_ALBUM_IMG_SIZE))
img = ImageOps.autocontrast(img)
img = ImageEnhance.Contrast(img).enhance(2.0) # Kontrast erhöhen
img = img.convert("1", dither=Image.FLOYDSTEINBERG, colors=12) # Stärkeres Dithering
buffer = BytesIO()
img.save(buffer, format="PNG")
img_str = base64.b64encode(buffer.getvalue()).decode()
return img_str
except Exception:
return ""
return ""
class HipsterfyPlaylist(object): class HipsterfyPlaylist(object):
def __init__(self, playlist_uri, hipsterfy:Hipsterfy): def __init__(self, playlist_uri, hipsterfy:Hipsterfy):