@ -1,17 +1,58 @@
|
||||
# Requirements Update (requirements will need to be reinstalled)
|
||||
Updated arrapi requirement to 1.4.2
|
||||
Updated pillow requirement to 9.4.0
|
||||
Updated requests requirement to 2.28.2
|
||||
Updated pillow requirement to 9.5.0
|
||||
Updated plexapi requirement to 4.13.4
|
||||
New requirement GitPython version 3.1.31
|
||||
|
||||
# New Features
|
||||
Added new collection_order `custom.desc` ([FR](https://features.metamanager.wiki/features/p/reverse-sort-collectionorder-custom))
|
||||
Added webp Image Support ([FR](https://features.metamanager.wiki/features/p/support-webp-image-extensions))
|
||||
Added Spanish Defaults Translation
|
||||
Added Delete Webhooks
|
||||
Added collection detail `delete_collections_named` to delete any collections listed while running this collection definition.
|
||||
Added `episode_year` as a dynamic collection option.
|
||||
Added `mass_studio_update` [library operation](https://metamanager.wiki/en/latest/config/operations.html#mass-studio-update).
|
||||
Changes Environment Variable/Run Argument list separator from `,` to `|`.
|
||||
Added `PMM_LOG_REQUESTS`/`--log-requests` Environment Variable/Run Argument which will log every single HTTP request in the log.
|
||||
Added EXIF Tags to Overlayed Images to be able to determine if they have an overlay or not.
|
||||
Added `anidb`, `anidb_3_0`, `anidb_2_5`, `anidb_2_0`, `anidb_1_5`, `anidb_1_0`, `anidb_0_5` options to the [`mass_genre_update` Library Operation](https://metamanager.wiki/en/latest/config/operations.html#mass-genre-update).
|
||||
Added `ignore_cache` to [`radarr`](https://metamanager.wiki/en/latest/config/radarr.html) and [`sonarr`](https://metamanager.wiki/en/latest/config/sonarr.html) Settings and `radarr_ignore_cache` and `sonarr_ignore_cache` to [Radarr/Sonarr Definition Settings](https://metamanager.wiki/en/latest/metadata/details/arr.html).
|
||||
Closes #1286 Updates Synology Walkthrough with DSM7 images.
|
||||
Closes #1159 Adds support for official trakt lists.
|
||||
Closes #1251 When resetting Overlays Seasons where theres no poster will use the show poster.
|
||||
Templates can now be used with metadata updates.
|
||||
`allowed_library_types` Definition Setting has been changed to `run_definition` the old attribute will still work in the same way.
|
||||
Added `mapping_id`, `run_definition`, `update_seasons`, and `update_episodes` to Metadata definitions.
|
||||
Added a [Ratings Explained](https://metamanager.wiki/en/latest/home/guides/ratings.html) page to the Wiki to help explain how PMM interacts with the various Ratings.
|
||||
Add more options to the [`mass_imdb_parental_labels` Library Operation](https://metamanager.wiki/en/latest/config/operations.html#mass-imdb-parental-labels).
|
||||
Added `imdb_keyword` as a [Tag Filter](https://metamanager.wiki/en/latest/metadata/filters.html#tag-filters).
|
||||
Added `has_edition` as a [Boolean Filter](https://metamanager.wiki/en/latest/metadata/filters.html#boolean-filters).
|
||||
Added `has_stinger` and `stinger_rating` as [Filters](https://metamanager.wiki/en/latest/metadata/filters.html) based on http://www.mediastinger.com
|
||||
When editing episode metadata the key can now be either episode number, episode title, or episodeoriginally released date.
|
||||
The Collectionless builder now can work with other builders.
|
||||
|
||||
# New Defaults Features
|
||||
Removed Translations from the defaults directory and in to their own [repo](https://github.com/meisnate12/PMM-Translations) which is managed at [translations.metamanager.wiki](https://translations.metamanager.wiki/projects/plex-meta-manager/defaults/).
|
||||
Added `minimum_rating`, `fresh_rating`, and `maximum_rating` as template variable options to the [Ratings Overlays](https://metamanager.wiki/en/latest/defaults/overlays/ratings.html) to control which ratings get displayed.
|
||||
Added the ability to update Overlay Defaults Positioning with just setting the alignment variables.
|
||||
Added [Based On...](https://metamanager.wiki/en/latest/defaults/both/based.html) Collection Default.
|
||||
Added Signature Style, DIIIVOY Style, and DIIVOY Color Style to [`actor`](https://metamanager.wiki/en/latest/defaults/both/actor.html), [`directors`](https://metamanager.wiki/en/latest/defaults/movie/director.html), [`producers`](https://metamanager.wiki/en/latest/defaults/movie/producer.html), and [`writers`](https://metamanager.wiki/en/latest/defaults/movie/writer.html).
|
||||
Added new editions to the [editions Overlay File](https://metamanager.wiki/en/latest/defaults/overlays/resolution.html).
|
||||
Added `delete_playlist` and `delete_playlist_<<key>>` as template variable options to the [Playlist Default](https://metamanager.wiki/en/latest/defaults/playlist.html).
|
||||
Added `region` as a template variable options to the [`streaming` Overlay](https://metamanager.wiki/en/latest/defaults/overlays/streaming.html) and [`streaming` Collection](https://metamanager.wiki/en/latest/defaults/both/streaming.html) to allow these lists to show items in that region.
|
||||
Added AppleTV to te [FlixPatrol Default](https://metamanager.wiki/en/latest/defaults/overlays/flixpatrol.html).
|
||||
Added `radarr_search` and `sonarr_search` as template variable options to all Collection Defaults.
|
||||
Updated `network` and `franchise` defaults
|
||||
|
||||
# Bug Fixes
|
||||
Fixes #1187 Franchise Defaults no longer ignore collection_section and sort_title
|
||||
Fixed Italian Defaults Translation
|
||||
Fixed TMDb Modified Filters
|
||||
Fixed ValueError from Anime IDs
|
||||
Fixes Bug with `--time` that caused the times not to display correctly.
|
||||
Fixes `mal_search` search bug.
|
||||
Fixes #1277 corrects bug setting TMDb region.
|
||||
Fixes a Bug where missing items items wouldn't be sent to radarr if no items were found in the library.
|
||||
Fixes a Bug with template conditionals causing them to sometimes use the wrong result.
|
||||
Fixes #1285 Wiki error.
|
||||
Fixes a Bug with the `mass_poster_update` and `mass_background_update` Library Operations where they would sometimes throw a 406 Error.
|
||||
Fixes a Bug with the `mass_poster_update` Library Operation where it would also update backgrounds in addition to posters.
|
||||
Fixes multiple unnecessary items loads from plex.
|
||||
Fixes a Bug with using year filters with no modifier.
|
||||
Fixes a Bug where the `dimensional_asset_rename` Setting would rename title cards and season posters to show posters.
|
||||
Fixes [`trakt_userlist` Builder](https://metamanager.wiki/en/latest/metadata/builders/trakt.html#trakt-userlist) where option `recommended` should have been `recommendations`.
|
||||
Fixes overlay remove/reset operations.
|
||||
Closes #1325 Fixes a Bug where `tmdb_vote_count` would be rejected as a filter.
|
||||
Closes #1189 Fixes a Bug in the Resolution Default where the position would be completely off when changed
|
||||
|
||||
Various other Minor Fixes
|
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 426 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 221 KiB |
@ -0,0 +1,385 @@
|
||||
import os, time
|
||||
from modules import util
|
||||
from modules.util import Failed, ImageData
|
||||
from PIL import Image, ImageFont, ImageDraw, ImageColor
|
||||
|
||||
logger = util.logger
|
||||
|
||||
class ImageBase:
|
||||
def __init__(self, config, data):
|
||||
self.config = config
|
||||
self.data = data
|
||||
self.methods = {str(m).lower(): m for m in self.data}
|
||||
self.code_base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
self.posters_dir = os.path.join(self.code_base, "defaults", "posters")
|
||||
|
||||
def check_data(self, attr):
|
||||
if attr not in self.methods or not self.data[self.methods[attr]]:
|
||||
return None
|
||||
return self.data[self.methods[attr]]
|
||||
|
||||
def check_file(self, attr, pmm_items, local=False, required=False):
|
||||
if attr not in self.methods or not self.data[self.methods[attr]]:
|
||||
if required:
|
||||
raise Failed(f"Posters Error: {attr} not found or is blank")
|
||||
return None
|
||||
file_data = self.data[self.methods[attr]]
|
||||
if isinstance(file_data, list):
|
||||
file_data = file_data[0]
|
||||
if not isinstance(file_data, dict):
|
||||
file_data = {"pmm": str(file_data)}
|
||||
if "pmm" in file_data and file_data["pmm"]:
|
||||
file_path = pmm_items[file_data["pmm"]] if file_data["pmm"] in pmm_items else file_data["pmm"]
|
||||
if os.path.exists(file_path):
|
||||
return file_path, os.path.getsize(file_path)
|
||||
raise Failed(f"Poster Error: {attr} pmm invalid. Options: {', '.join(pmm_items.keys())}")
|
||||
elif "file" in file_data and file_data["file"]:
|
||||
if os.path.exists(file_data["file"]):
|
||||
return file_data["file"], os.path.getsize(file_data["file"])
|
||||
raise Failed(f"Poster Error: {attr} file not found: {os.path.abspath(file_data['file'])}")
|
||||
elif local:
|
||||
return None, None
|
||||
elif "git" in file_data and file_data["git"]:
|
||||
url = f"{self.config.GitHub.configs_url}{file_data['git']}"
|
||||
elif "repo" in file_data and file_data["repo"]:
|
||||
url = f"{self.config.custom_repo}{file_data['repo']}"
|
||||
elif "url" in file_data and file_data["url"]:
|
||||
url = file_data["url"]
|
||||
else:
|
||||
return None, None
|
||||
|
||||
response = self.config.get(url)
|
||||
if response.status_code >= 400:
|
||||
raise Failed(f"Poster Error: {attr} not found at: {url}")
|
||||
if "Content-Type" not in response.headers or response.headers["Content-Type"] not in util.image_content_types:
|
||||
raise Failed(f"Poster Error: {attr} not a png, jpg, or webp: {url}")
|
||||
if response.headers["Content-Type"] == "image/jpeg":
|
||||
ext = "jpg"
|
||||
elif response.headers["Content-Type"] == "image/webp":
|
||||
ext = "webp"
|
||||
else:
|
||||
ext = "png"
|
||||
num = ""
|
||||
image_path = os.path.join(self.posters_dir, f"temp{num}.{ext}")
|
||||
while os.path.exists(image_path):
|
||||
if not num:
|
||||
num = 1
|
||||
else:
|
||||
num += 1
|
||||
image_path = os.path.join(self.posters_dir, f"temp{num}.{ext}")
|
||||
with open(image_path, "wb") as handler:
|
||||
handler.write(response.content)
|
||||
while util.is_locked(image_path):
|
||||
time.sleep(1)
|
||||
return image_path, url
|
||||
|
||||
def check_color(self, attr):
|
||||
if attr not in self.methods or not self.data[self.methods[attr]]:
|
||||
return None
|
||||
try:
|
||||
return ImageColor.getcolor(self.data[self.methods[attr]], "RGBA")
|
||||
except ValueError:
|
||||
raise Failed(f"Poster Error: {attr}: {self.data[self.methods[attr]]} invalid")
|
||||
|
||||
class Component(ImageBase):
|
||||
def __init__(self, config, data):
|
||||
super().__init__(config, data)
|
||||
self.draw = ImageDraw.Draw(Image.new("RGBA", (0, 0)))
|
||||
self.back_color = self.check_color("back_color")
|
||||
self.back_radius = util.parse("Posters", "back_radius", self.data, datatype="int", methods=self.methods, default=0, minimum=0) if "back_radius" in self.methods else 0
|
||||
self.back_line_width = util.parse("Posters", "back_line_width", self.data, datatype="int", methods=self.methods, default=0, minimum=0) if "back_line_width" in self.methods else 0
|
||||
self.back_line_color = self.check_color("back_line_color")
|
||||
self.back_padding = util.parse("Posters", "back_padding", self.data, datatype="int", methods=self.methods, default=0, minimum=0) if "back_padding" in self.methods else 0
|
||||
self.back_align = util.parse("Posters", "back_align", self.data, methods=self.methods, default="center", options=["left", "right", "center", "top", "bottom"]) if "back_align" in self.methods else "center"
|
||||
|
||||
self.back_width = 0
|
||||
if "back_width" in self.methods:
|
||||
if str(self.methods["back_width"]).lower() == "max":
|
||||
self.back_width = "max"
|
||||
else:
|
||||
self.back_width = util.parse("Posters", "back_width", self.data, methods=self.methods, datatype="int", minimum=0)
|
||||
self.back_height = 0
|
||||
if "back_height" in self.methods:
|
||||
if str(self.methods["back_height"]).lower() == "max":
|
||||
self.back_height = "max"
|
||||
else:
|
||||
self.back_height = util.parse("Posters", "back_height", self.data, methods=self.methods, datatype="int", minimum=0)
|
||||
self.has_back = True if self.back_color or self.back_line_color else False
|
||||
self.horizontal_offset, self.horizontal_align, self.vertical_offset, self.vertical_align = util.parse_cords(self.data, "component", err_type="Posters", default=(0, "center", 0, "center"))
|
||||
|
||||
self.images_dir = os.path.join(self.posters_dir, "images")
|
||||
self.pmm_images = {k[:-4]: os.path.join(self.images_dir, k) for k in os.listdir(self.images_dir)}
|
||||
self.image, self.image_compare = self.check_file("image", self.pmm_images)
|
||||
self.image_width = util.parse("Posters", "image_width", self.data, datatype="int", methods=self.methods, default=0, minimum=0, maximum=2000) if "image_width" in self.methods else 0
|
||||
self.image_color = self.check_color("image_color")
|
||||
|
||||
self.text = None
|
||||
self.font_name = None
|
||||
self.font = None
|
||||
self.font_style = None
|
||||
self.addon_position = None
|
||||
self.text_align = util.parse("Posters", "text_align", self.data, methods=self.methods, default="center", options=["left", "right", "center"]) if "text_align" in self.methods else "center"
|
||||
self.font_size = util.parse("Posters", "font_size", self.data, datatype="int", methods=self.methods, default=163, minimum=1) if "font_size" in self.methods else 163
|
||||
self.font_color = self.check_color("font_color")
|
||||
self.stroke_color = self.check_color("stroke_color")
|
||||
self.stroke_width = util.parse("Posters", "stroke_width", self.data, datatype="int", methods=self.methods, default=0, minimum=0) if "stroke_width" in self.methods else 0
|
||||
self.addon_offset = util.parse("Posters", "addon_offset", self.data, datatype="int", methods=self.methods, default=0, minimum=0) if "stroke_width" in self.methods else 0
|
||||
if "text" in self.methods:
|
||||
font_base = os.path.join(self.code_base, "fonts")
|
||||
pmm_fonts = os.listdir(font_base)
|
||||
all_fonts = {s: s for s in util.get_system_fonts()}
|
||||
for font_name in pmm_fonts:
|
||||
all_fonts[font_name] = os.path.join(font_base, font_name)
|
||||
self.text = util.parse("Posters", "text", self.data, methods=self.methods, default="<<title>>")
|
||||
self.font_name, self.font_compare = self.check_file("font", all_fonts, local=True)
|
||||
if not self.font_name:
|
||||
self.font_name = all_fonts["Roboto-Medium.ttf"]
|
||||
self.font = ImageFont.truetype(self.font_name, self.font_size)
|
||||
if "font_style" in self.methods and self.data[self.methods["font_style"]]:
|
||||
try:
|
||||
variation_names = [n.decode("utf-8") for n in self.font.get_variation_names()]
|
||||
if self.data[self.methods["font_style"]] in variation_names:
|
||||
self.font.set_variation_by_name(self.data[self.methods["font_style"]])
|
||||
self.font_style = self.data[self.methods["font_style"]]
|
||||
else:
|
||||
raise Failed(f"Posters Error: Font Style {self.data[self.methods['font_style']]} not found. Options: {','.join(variation_names)}")
|
||||
except OSError:
|
||||
raise Failed(f"Posters Warning: font: {self.font} does not have variations")
|
||||
self.addon_position = util.parse("Posters", "addon_position", self.data, methods=self.methods, options=["left", "right", "top", "bottom"]) if "addon_position" in self.methods else "left"
|
||||
|
||||
if not self.image and not self.text:
|
||||
raise Failed("Posters Error: An image or text is required for each component")
|
||||
|
||||
def apply_vars(self, item_vars):
|
||||
for var_key, var_data in item_vars.items():
|
||||
self.text = self.text.replace(f"<<{var_key}>>", str(var_data))
|
||||
|
||||
def adjust_text_width(self, max_width):
|
||||
lines = []
|
||||
for line in self.text.split("\n"):
|
||||
for word in line.split(" "):
|
||||
word_length = self.draw.textlength(word, font=self.font)
|
||||
while word_length > max_width:
|
||||
self.font_size -= 1
|
||||
self.font = ImageFont.truetype(self.font_name, self.font_size)
|
||||
word_length = self.draw.textlength(word, font=self.font)
|
||||
for line in self.text.split("\n"):
|
||||
line_length = self.draw.textlength(line, font=self.font)
|
||||
if line_length <= max_width:
|
||||
lines.append(line)
|
||||
continue
|
||||
current_line = ""
|
||||
line_length = 0
|
||||
for word in line.split(" "):
|
||||
if current_line:
|
||||
word = f" {word}"
|
||||
word_length = self.draw.textlength(word, font=self.font)
|
||||
if line_length + word_length <= max_width:
|
||||
current_line += word
|
||||
line_length += word_length
|
||||
else:
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
word = word.strip()
|
||||
word_length = self.draw.textlength(word, font=self.font)
|
||||
current_line = word
|
||||
line_length = word_length
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
self.text = "\n".join(lines)
|
||||
|
||||
def get_compare_string(self):
|
||||
output = ""
|
||||
if self.text:
|
||||
output += f"{self.text} {self.text_align} {self.font_compare}"
|
||||
output += str(self.font_size)
|
||||
for value in [self.font_color, self.font_style, self.stroke_color, self.stroke_width]:
|
||||
if value:
|
||||
output += f"{value}"
|
||||
if self.image:
|
||||
output += f"{self.addon_position} {self.addon_offset}"
|
||||
|
||||
if self.image:
|
||||
output += str(self.image_compare)
|
||||
for value in [self.image_width, self.image_color]:
|
||||
if value:
|
||||
output += str(value)
|
||||
|
||||
output += f"({self.horizontal_offset},{self.horizontal_align},{self.vertical_offset},{self.vertical_align})"
|
||||
if self.has_back:
|
||||
for value in [self.back_color, self.back_radius, self.back_padding, self.back_align,
|
||||
self.back_width, self.back_height, self.back_line_color, self.back_line_width]:
|
||||
if value is not None:
|
||||
output += f"{value}"
|
||||
return output
|
||||
|
||||
def get_text_size(self, text):
|
||||
return self.draw.multiline_textbbox((0, 0), text, font=self.font)
|
||||
|
||||
def get_coordinates(self, canvas_box, box, new_cords=None):
|
||||
canvas_width, canvas_height = canvas_box
|
||||
box_width, box_height = box
|
||||
|
||||
def get_cord(value, image_value, over_value, align):
|
||||
value = int(image_value * 0.01 * int(value[:-1])) if str(value).endswith("%") else int(value)
|
||||
if align in ["right", "bottom"]:
|
||||
return image_value - over_value - value
|
||||
elif align == "center":
|
||||
return int(image_value / 2) - int(over_value / 2) + value
|
||||
else:
|
||||
return value
|
||||
if new_cords:
|
||||
ho, ha, vo, va = new_cords
|
||||
else:
|
||||
ho, ha, vo, va = self.horizontal_offset, self.horizontal_align, self.vertical_offset, self.vertical_align
|
||||
|
||||
return get_cord(ho, canvas_width, box_width, ha), get_cord(vo, canvas_height, box_height, va)
|
||||
|
||||
def get_generated_layer(self, canvas_box, new_cords=None):
|
||||
canvas_width, canvas_height = canvas_box
|
||||
generated_layer = None
|
||||
text_width, text_height = None, None
|
||||
if self.image:
|
||||
image = Image.open(self.image)
|
||||
image_width, image_height = image.size
|
||||
if self.image_width:
|
||||
image_height = int(float(image_height) * float(self.image_width / float(image_width)))
|
||||
image_width = self.image_width
|
||||
image = image.resize((image_width, image_height), Image.Resampling.LANCZOS) # noqa
|
||||
if self.image_color:
|
||||
r, g, b = self.image_color
|
||||
pixels = image.load()
|
||||
for x in range(image_width):
|
||||
for y in range(image_height):
|
||||
if pixels[x, y][3] > 0: # noqa
|
||||
pixels[x, y] = (r, g, b, pixels[x, y][3]) # noqa
|
||||
else:
|
||||
image, image_width, image_height = None, 0, 0
|
||||
if self.text is not None:
|
||||
_, _, text_width, text_height = self.get_text_size(self.text)
|
||||
if image_width and self.addon_position in ["left", "right"]:
|
||||
box = (text_width + image_width + self.addon_offset, text_height if text_height > image_height else image_height)
|
||||
elif image_width:
|
||||
box = (text_width if text_width > image_width else image_width, text_height + image_height + self.addon_offset)
|
||||
else:
|
||||
box = (text_width, text_height)
|
||||
else:
|
||||
box = (image_width, image_height)
|
||||
box_width, box_height = box
|
||||
back_width = canvas_width if self.back_width == "max" else self.back_width if self.back_width else box_width
|
||||
back_height = canvas_height if self.back_height == "max" else self.back_height if self.back_height else box_height
|
||||
main_point = self.get_coordinates(canvas_box, (back_width, back_height), new_cords=new_cords)
|
||||
start_x, start_y = main_point
|
||||
|
||||
if self.text is not None or self.has_back:
|
||||
generated_layer = Image.new("RGBA", canvas_box, (255, 255, 255, 0))
|
||||
drawing = ImageDraw.Draw(generated_layer)
|
||||
if self.has_back:
|
||||
cords = (
|
||||
start_x - self.back_padding,
|
||||
start_y - self.back_padding,
|
||||
start_x + back_width + self.back_padding,
|
||||
start_y + back_height + self.back_padding
|
||||
)
|
||||
if self.back_radius:
|
||||
drawing.rounded_rectangle(cords, fill=self.back_color, outline=self.back_line_color, width=self.back_line_width, radius=self.back_radius)
|
||||
else:
|
||||
drawing.rectangle(cords, fill=self.back_color, outline=self.back_line_color, width=self.back_line_width)
|
||||
|
||||
main_x, main_y = main_point
|
||||
if self.back_height and self.back_align in ["left", "right", "center", "bottom"]:
|
||||
main_y = start_y + (back_height - box_height) // (1 if self.back_align == "bottom" else 2)
|
||||
if self.back_width and self.back_align in ["top", "bottom", "center", "right"]:
|
||||
main_x = start_x + (back_width - box_width) // (1 if self.back_align == "right" else 2)
|
||||
|
||||
addon_x = None
|
||||
addon_y = None
|
||||
if self.text is not None and self.image:
|
||||
addon_x = main_x
|
||||
addon_y = main_y
|
||||
if self.addon_position == "left":
|
||||
main_x = main_x + image_width + self.addon_offset
|
||||
elif self.addon_position == "right":
|
||||
addon_x = main_x + text_width + self.addon_offset
|
||||
elif text_width < image_width:
|
||||
main_x = main_x + ((image_width - text_width) / 2)
|
||||
elif text_width > image_width:
|
||||
addon_x = main_x + ((text_width - image_width) / 2)
|
||||
|
||||
if self.addon_position == "top":
|
||||
main_y = main_y + image_height + self.addon_offset
|
||||
elif self.addon_position == "bottom":
|
||||
addon_y = main_y + text_height + self.addon_offset
|
||||
elif text_height < image_height:
|
||||
main_y = main_y + ((image_height - text_height) / 2)
|
||||
elif text_height > image_height:
|
||||
addon_y = main_y + ((text_height - image_height) / 2)
|
||||
main_point = (int(main_x), int(main_y))
|
||||
|
||||
if self.text is not None:
|
||||
drawing.multiline_text(main_point, self.text, font=self.font, fill=self.font_color, align=self.text_align,
|
||||
stroke_fill=self.stroke_color, stroke_width=self.stroke_width)
|
||||
if addon_x is not None:
|
||||
main_point = (addon_x, addon_y)
|
||||
|
||||
return generated_layer, main_point, image
|
||||
|
||||
class PMMImage(ImageBase):
|
||||
def __init__(self, config, data, image_attr, playlist=False):
|
||||
super().__init__(config, data)
|
||||
self.image_attr = image_attr
|
||||
self.backgrounds_dir = os.path.join(self.posters_dir, "backgrounds")
|
||||
self.playlist = playlist
|
||||
self.pmm_backgrounds = {k[:-4]: os.path.join(self.backgrounds_dir, k) for k in os.listdir(self.backgrounds_dir)}
|
||||
|
||||
self.background_image, self.background_compare = self.check_file("background_image", self.pmm_backgrounds)
|
||||
self.background_color = self.check_color("background_color")
|
||||
self.border_width = util.parse("Posters", "border_width", self.data, datatype="int", methods=self.methods, default=0, minimum=0) if "border_width" in self.methods else 0
|
||||
self.border_color = self.check_color("border_color")
|
||||
if "components" not in self.methods or not self.data[self.methods["components"]]:
|
||||
raise Failed("Posters Error: components attribute is required")
|
||||
self.components = [Component(self.config, d) for d in util.parse("Posters", "components", self.data, datatype="listdict", methods=self.methods)]
|
||||
|
||||
def get_compare_string(self):
|
||||
output = ""
|
||||
for value in [self.background_compare, self.background_color, self.border_width, self.border_color]:
|
||||
if value:
|
||||
output += f"{value}"
|
||||
for component in self.components:
|
||||
output += component.get_compare_string()
|
||||
return output
|
||||
|
||||
def save(self, item_vars):
|
||||
image_path = os.path.join(self.posters_dir, "temp_poster.png")
|
||||
if os.path.exists(image_path):
|
||||
os.remove(image_path)
|
||||
canvas_width = 1000
|
||||
canvas_height = 1000 if self.playlist else 1500
|
||||
canvas_box = (canvas_width, canvas_height)
|
||||
|
||||
pmm_image = Image.new(mode="RGB", size=canvas_box, color=self.background_color)
|
||||
if self.background_image:
|
||||
bkg_image = Image.open(self.background_image)
|
||||
bkg_image = bkg_image.resize(canvas_box, Image.Resampling.LANCZOS) # noqa
|
||||
pmm_image.paste(bkg_image, (0, 0), bkg_image)
|
||||
|
||||
if self.border_width:
|
||||
draw = ImageDraw.Draw(pmm_image)
|
||||
draw.rectangle(((0, 0), canvas_box), outline=self.border_color, width=self.border_width)
|
||||
|
||||
max_border_width = canvas_width - self.border_width - 100
|
||||
|
||||
for component in self.components:
|
||||
if component.text:
|
||||
component.apply_vars(item_vars)
|
||||
component.adjust_text_width(component.back_width if component.back_width and component.back_width != "max" else max_border_width)
|
||||
generated_layer, image_point, image = component.get_generated_layer(canvas_box)
|
||||
if generated_layer:
|
||||
pmm_image.paste(generated_layer, (0, 0), generated_layer)
|
||||
if image:
|
||||
pmm_image.paste(image, image_point, image)
|
||||
|
||||
pmm_image.save(image_path)
|
||||
|
||||
return ImageData(self.image_attr, image_path, is_url=False, compare=self.get_compare_string())
|
||||
|