magic dithering
This commit is contained in:
		@ -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):
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user