Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 102 additions & 43 deletions azuravian/TitleColorMatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from re import compile as re_compile, findall
from typing import Literal, Optional, Union

from pydantic import FilePath
from pydantic import Field

from app.schemas.base import BetterColor
from app.schemas.card_type import BaseCardTypeCustomFontAllText
Expand All @@ -12,13 +12,14 @@
from modules.Debug import log
from modules.RemoteFile import RemoteFile


class TitleColorMatch(BaseCardType):
"""
This class describes a type of CardType created by azuravian, and is
This class describes a type of CardType created by azuravian, and is
a modification of Beedman's GradientLogoTitleCard class with a few
changes, specifically the ability to autoselect a font color that
matches the logo, as well as trimming the logo of any extra
transparent space that makes its location incorrect.
transparent space that makes its location incorrect.
"""

API_DETAILS = CardDescription(
Expand All @@ -39,24 +40,31 @@ class TitleColorMatch(BaseCardType):
)

class CardModel(BaseCardTypeCustomFontAllText):
logo_file: FilePath
font_color: Union[BetterColor, Literal['auto']] = '#EBEBEB'
font_file: FilePath
logo_file: Optional[Path] = None
font_color: Union[BetterColor, Literal['auto']] = 'auto'
font_file: Path
title_min_luminance: int = Field(min=0, default=50, max=255)
check_multiple_luminances: bool = True
invert_logos: bool = False # TODO: Evaluate whether this should default to enabled
default_title_color: BetterColor = '#EBEBEB'
default_title_stroke_color: BetterColor = 'black'
omit_gradient: bool = False
# TODO: The fonts for season and episode count should be changeable and so should their colors?

"""Directory where all reference files used by this card are stored"""
REF_DIRECTORY = Path(__file__).parent.parent / 'ref'

"""Default font and color for episode title text"""
TITLE_FONT = str((REF_DIRECTORY / 'Sequel-Neue.otf').resolve())
TITLE_COLOR = '#EBEBEB'

"""Characteristics for title splitting by this class"""
TITLE_CHARACTERISTICS = {
'max_line_width': 32, # Character count to begin splitting titles
'max_line_count': 3, # Maximum number of lines a title can take up
'top_heavy': False, # This class uses bottom heavy titling
}

"""Default font and text color for episode title text"""
TITLE_FONT = str((REF_DIRECTORY / 'Sequel-Neue.otf').resolve())
TITLE_COLOR = '#EBEBEB'

"""Default characters to replace in the generic font"""
FONT_REPLACEMENTS = {
'[': '(', ']': ')', '(': '[', ')': ']', '―': '-', '…': '...'
Expand All @@ -81,15 +89,18 @@ class CardModel(BaseCardTypeCustomFontAllText):

__slots__ = (
'source_file', 'output_file', 'title_text', 'season_text',
'episode_text', 'hide_season_text', 'hide_episode_text', 'font_color',
'font_file', 'font_interline_spacing', 'font_kerning', 'font_size',
'font_stroke_width', 'font_vertical_shift', 'logo',
'episode_text', 'hide_season_text', 'hide_episode_text',
'font_color', 'font_file', 'font_interline_spacing',
'font_kerning', 'font_size', 'font_stroke_width',
'font_vertical_shift', 'logo', 'title_min_luminance',
'check_multiple_luminances', 'invert_logos',
'default_title_color', 'default_title_stroke_color',
'omit_gradient'
)

def __init__(self,
source_file: Path,
card_file: Path,
logo_file: Path,
title_text: str,
season_text: str,
episode_text: str,
Expand All @@ -104,13 +115,20 @@ def __init__(self,
font_vertical_shift: int = 0,
blur: bool = False,
grayscale: bool = False,
logo_file: Optional[Path] = None,
title_min_luminance: int = 50,
check_multiple_luminances: bool = True,
invert_logos: bool = True,
default_title_color: str = TITLE_COLOR,
default_title_stroke_color: str = 'black',
omit_gradient: bool = False,
preferences: Optional['Preferences'] = None, # type: ignore
**unused,
) -> None:
"""
Construct a new instance of this Card.
"""

# Initialize the parent class - this sets up an ImageMagickInterface
super().__init__(blur, grayscale, preferences=preferences)

Expand All @@ -124,7 +142,7 @@ def __init__(self,
self.episode_text = self.image_magick.escape_chars(episode_text)
self.hide_season_text = hide_season_text
self.hide_episode_text = hide_episode_text

self.font_color = font_color
self.font_file = font_file
self.font_interline_spacing = font_interline_spacing
Expand All @@ -133,9 +151,15 @@ def __init__(self,
self.font_stroke_width = font_stroke_width
self.font_vertical_shift = font_vertical_shift

self.title_min_luminance = title_min_luminance
self.check_multiple_luminances = check_multiple_luminances
self.invert_logos = invert_logos
self.default_title_color = default_title_color
self.default_title_stroke_color = default_title_stroke_color
self.omit_gradient = omit_gradient

@property
def logo_command(self) -> ImageMagickCommands:

def logo_command(self, luminance: int) -> ImageMagickCommands:
"""
Get the ImageMagick commands to add the resized logo to the
source image.
Expand All @@ -144,36 +168,53 @@ def logo_command(self) -> ImageMagickCommands:
List of ImageMagick commands.
"""

# Logo not provided, return empty commands
if self.logo is None or not self.logo.exists():
return []

negate_commands = []
if self.invert_logos and luminance < self.title_min_luminance:
negate_commands = [
'-channel RGB',
'-negate',
'-colorspace Gray'
]

return [
# Resize logo
f'\( "{self.logo.resolve()}"',
f'-trim',
f'+repage',
f'-resize x650',
f'-resize 1155x650\> \)',
f'-resize 1155x650\>',
# Recolor dark logos to be visible on the black gradient
*negate_commands,
# Overlay resized logo
f'-gravity northwest',
f'\) -gravity northwest',
f'-define colorspace:auto-grayscale=false',
f'-type TrueColorAlpha',
f'-type TrueColorAlpha',
f'-geometry "+50+50"',
f'-composite',
]


@property
def title_text_command(self) -> ImageMagickCommands:
def title_text_command(self,
title_color: str,
stroke_color: str,
) -> ImageMagickCommands:
"""
ImageMagick commands to implement the title text's global
effects. Specifically the the font, kerning, fontsize, and
center gravity.

Args:
title_color: Color to utilize for the title text.
stroke_color: Color to utilize for the stroke.

Returns:
List of ImageMagick commands.
"""

# Get the title color and stroke for this logo
title_color, stroke_color = self._get_logo_color()

font_size = 157.41 * self.font_size
interline_spacing = -22 + self.font_interline_spacing
kerning = -1.25 * self.font_kerning
Expand All @@ -196,18 +237,23 @@ def title_text_command(self) -> ImageMagickCommands:
]


def _get_logo_color(self) -> tuple[str, str]:
def _get_logo_color(self) -> tuple[str, str, int]:
"""
Get the logo color for this card's logo.

Returns:
Tuple whose values are the title color text and the stroke
width color.
Tuple whose values are the title color text, the stroke
width color and luminance
"""

# If auto color wasn't indicated use indicated color and black stroke
if str(self.font_color) != 'auto':
return self.font_color, 'black'
return self.font_color, 'black', 255

# The logo file for this series doesn't exist, return the default colors
if self.logo is None or not self.logo.exists():
return self.default_title_color, self.default_title_stroke_color,255


# Command to get histogram of the colors in logo image
command = ' '.join([
Expand Down Expand Up @@ -247,19 +293,24 @@ def _get_logo_color(self) -> tuple[str, str]:
color_ = hexcolor.lstrip('#')
lv = len(color_)
r, g, b = (int(color_[i:i+lv//3], 16) for i in range(0, lv, lv//3))

# Skip values that are too dark/light
if min(r, g, b) > 240 or max(r, g, b) < 15:
continue

# First valid color, return color and stroke based on luminance
luminance = (r * 0.299) + (g * 0.587) + (b * 0.114)
return hexcolor, 'black' if luminance > 50 else 'white'
log.debug(f'Luminance for {self.logo} ({r}, {g}, {b}) is {luminance}')

# If luminance is sufficient return right away, otherwise check the next most common colors
if luminance >= self.title_min_luminance:
title_color = hexcolor
stroke_color = 'black' if luminance > 100 else 'white'
return title_color, stroke_color, luminance
# Only check the first luminance when set to do so
elif not self.check_multiple_luminances:
break

# None of the most common colors had sufficient luminance, return defaults
return self.default_title_color, self.default_title_stroke_color, -1

# No valid colors identified, return defaults
return self.TITLE_COLOR, 'black'


@property
def index_text_command(self) -> ImageMagickCommands:
"""
Expand Down Expand Up @@ -340,7 +391,7 @@ def is_custom_font(font: 'Font') -> bool: # type: ignore
"""
Determines whether the given arguments represent a custom font
for this card.

Args:
font: The Font being evaluated.

Expand Down Expand Up @@ -387,19 +438,27 @@ def create(self) -> None:
object's defined title card.
"""

title_color, stroke_color, luminance = self._get_logo_color()
gradient_command = []
if not self.omit_gradient:
gradient_command = [
f'"{self.__GRADIENT_IMAGE}"',
f'-composite',
]

command = ' '.join([
f'convert',
# Resize source image
f'"{self.source_file.resolve()}"',
*self.resize_and_style,
# Overlay gradient
f'"{self.__GRADIENT_IMAGE}"',
f'-composite',
*gradient_command,
# Overlay resized logo
*self.logo_command,
*self.logo_command(luminance),
# Put title text
*self.title_text_command,
*self.title_text_command(title_color, stroke_color),
# Put season/episode text
# TODO: The outline/text color should probably be inverted based on luminance
*self.index_text_command,
# Create and resize output
*self.resize_output,
Expand Down
35 changes: 33 additions & 2 deletions cards.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,41 @@
"A modification of GradientLogoTitleCard that includes the option to auto-select font color based on logo color.",
"This card will also automatically crop off extraneous transparent space from around the logo."
], "example": "https://user-images.githubusercontent.com/7379812/187586521-353ba09f-30a8-424b-bbf3-ee9036c9e638.jpg",
"creators": ["Azuravian", "Beedman", "CollinHeist"],
"creators": ["Azuravian", "Beedman", "buthed010203", "CollinHeist"],
"supports_custom_fonts": true,
"supports_custom_seasons": true,
"supported_extras": []
"supported_extras": [
{
"name": "Logo Inversion Toggle",
"identifier": "invert_logos",
"description": "Whether to invert logo colors when they are darker than the specified minimum luminance"
},
{
"name": "Minimum Title Luminance",
"identifier": "title_min_luminance",
"description": "Threshold under which logos will have their colors inverted - 0-255 for white-black"
},
{
"name": "Check Multiple Luminance Toggle",
"identifier": "check_multiple_luminances",
"description": "Whether to check all significant colors, or only the most common color"
},
{
"name": "Fallback Title Color",
"identifier": "default_title_color",
"description": "Title color to use when the minimum luminance is not reached"
},
{
"name": "Fallback Stroke Color",
"identifier": "default_title_stroke_color",
"description": "Stroke color to use when the minimum luminance is not reached"
},
{
"name": "Gradient Omission",
"identifier": "omit_gradient",
"description": "Whether to omit the gradient overlay"
}
]
},
{
"name": "SciFi",
Expand Down