initial commit
working generation of big panels for Plamen campaign
This commit is contained in:
205
generate.py
Normal file
205
generate.py
Normal file
@@ -0,0 +1,205 @@
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import requests, os
|
||||
|
||||
DNDB_BASE_URL = "https://www.dndbeyond.com/character/"
|
||||
DNDB_CHARS = [
|
||||
{
|
||||
"characterId": 69760590,
|
||||
"characterName": "Вевель",
|
||||
"characterClass": "острожник 10",
|
||||
"portraitName": "vevel",
|
||||
"characterUrl": "/profile/megavenik/characters/69760590",
|
||||
"avatarUrl": "https://www.dndbeyond.com/avatars/thumbnails/31479/654/150/154/1581111423-69760590.jpeg",
|
||||
},
|
||||
{
|
||||
"characterId": 69976266,
|
||||
"characterName": "Милош",
|
||||
"characterClass": "помазанник",
|
||||
"portraitName": "milosh",
|
||||
"characterUrl": "/profile/EdgarBrin/characters/69976266",
|
||||
"avatarUrl": "https://www.dndbeyond.com/avatars/thumbnails/31479/725/150/155/1581111423-69976266.jpeg",
|
||||
},
|
||||
{
|
||||
"characterId": 69976392,
|
||||
"characterName": "Милица",
|
||||
"characterClass": "голос",
|
||||
"portraitName": "militsa",
|
||||
"characterUrl": "/profile/tritiumTino/characters/69976392",
|
||||
"avatarUrl": "https://www.dndbeyond.com/avatars/thumbnails/31479/709/150/154/1581111423-69976392.jpeg",
|
||||
},
|
||||
{
|
||||
"characterId": 90463225,
|
||||
"characterName": "Нивенна",
|
||||
"characterClass": "чаровница",
|
||||
"portraitName": "nivenna",
|
||||
"characterUrl": "/profile/Maslinium/characters/90463225",
|
||||
"avatarUrl": "https://www.dndbeyond.com/avatars/thumbnails/31479/748/160/150/1581111423-90463225.jpeg",
|
||||
},
|
||||
{
|
||||
"characterId": 99072742,
|
||||
"characterName": "Дарпа",
|
||||
"characterClass": "кухарь",
|
||||
"portraitName": "darpa",
|
||||
"characterUrl": "/profile/Forfattare/characters/99072742",
|
||||
"avatarUrl": "https://www.dndbeyond.com/avatars/thumbnails/34003/953/150/153/1581111423-99072742.jpeg",
|
||||
},
|
||||
{
|
||||
"characterId": 99483409,
|
||||
"characterName": "Деян",
|
||||
"characterClass": "",
|
||||
"portraitName": "deyan",
|
||||
"characterUrl": "/profile/SantaKosh/characters/99483409",
|
||||
"avatarUrl": "https://www.dndbeyond.com/avatars/thumbnails/34005/197/157/150/1581111423-99483409.jpeg",
|
||||
}
|
||||
]
|
||||
|
||||
class DataModifierService:
|
||||
def getModifiersByType(self, groupedModifiers: dict, type: str, subType: str = "") -> list:
|
||||
matching = []
|
||||
|
||||
for __, modifiers in groupedModifiers.items():
|
||||
filtered = list(filter(lambda item: 'type' in item and type == item['type'], modifiers))
|
||||
|
||||
if subType:
|
||||
filtered = list(filter(lambda item: 'subType' in item and subType == item['subType'], filtered))
|
||||
|
||||
matching += filtered
|
||||
|
||||
return matching
|
||||
|
||||
|
||||
def getTotalModifierValue(self, groupedModifiers: dict, type: str, subType: str) -> int:
|
||||
modifiers = self.getModifiersByType(groupedModifiers, type, subType)
|
||||
modifier_values = [modifier['value'] for modifier in modifiers]
|
||||
return sum(modifier_values)
|
||||
|
||||
def get_stat_modifier(stat):
|
||||
import math
|
||||
return math.floor((stat - 10) / 2)
|
||||
|
||||
def calculate_coordinates(object_width, object_height, center_point):
|
||||
"""
|
||||
Calculates the coordinates to put the center point of an object at a specified point.
|
||||
|
||||
Args:
|
||||
object_width (int): The width of the object.
|
||||
object_height (int): The height of the object.
|
||||
center_point (tuple): The (x, y) coordinates of the point where the center of the object should be placed.
|
||||
|
||||
Returns:
|
||||
tuple: The (x, y) coordinates of the top-left corner of the object.
|
||||
"""
|
||||
x = center_point[0] - object_width // 2
|
||||
y = center_point[1] - object_height // 2
|
||||
return (x, y)
|
||||
|
||||
def get_correct_text_sizes(draw, text: str, font: ImageFont.FreeTypeFont):
|
||||
label_width, label_height = draw.textsize(text, font=font)
|
||||
|
||||
# to fix text placing we need to get offsets
|
||||
# https://stackoverflow.com/questions/59008322/pillow-imagedraw-text-coordinates-to-center
|
||||
offset_x, offset_y = font.getoffset(text)
|
||||
label_width += offset_x
|
||||
label_height += offset_y
|
||||
|
||||
return (label_width, label_height)
|
||||
|
||||
def generate_main(char: dict):
|
||||
STATS_DICT = {
|
||||
1: {
|
||||
"name": "strength",
|
||||
"coords": (684, 304)
|
||||
},
|
||||
2: {
|
||||
"name": "dexterity",
|
||||
"coords": (684, 381)
|
||||
},
|
||||
3: {
|
||||
"name": "constitution",
|
||||
"coords": (684, 457)
|
||||
},
|
||||
4: {
|
||||
"name": "intelligence",
|
||||
"coords": (767, 304)
|
||||
},
|
||||
5: {
|
||||
"name": "wisdom",
|
||||
"coords": (767, 381)
|
||||
},
|
||||
6: {
|
||||
"name": "charisma",
|
||||
"coords": (767, 457)
|
||||
},
|
||||
}
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'text/json',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0',
|
||||
'Referer': 'https://www.dndbeyond.com/'
|
||||
}
|
||||
response = requests.get(f"{DNDB_BASE_URL}/{char['characterId']}/json", headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(data["stats"])
|
||||
else:
|
||||
print("Error: Failed to retrieve data from URL.")
|
||||
return False
|
||||
# Load the background image
|
||||
background = Image.open("parts/background-main.png")
|
||||
|
||||
# Load the image to be placed on top
|
||||
foreground = Image.open(f"parts/{char['portraitName']}-main.png")
|
||||
|
||||
# Paste the foreground image onto the background
|
||||
background.paste(foreground, (0, 0), foreground)
|
||||
|
||||
# Create a drawing context
|
||||
draw = ImageDraw.Draw(background)
|
||||
|
||||
draw_width, _ = draw.im.size # pyright: ignore[reportGeneralTypeIssues]
|
||||
|
||||
for stat in data["stats"]:
|
||||
modifiers = data.get('modifiers', {})
|
||||
|
||||
modifier_service = DataModifierService()
|
||||
total_modifier_value = modifier_service.getTotalModifierValue(modifiers, 'bonus', STATS_DICT[stat['id']]['name'] + '-score')
|
||||
stat["value"] += total_modifier_value
|
||||
final_stat = get_stat_modifier(stat["value"])
|
||||
|
||||
# Define the font and font size for the text labels
|
||||
stats_font = ImageFont.truetype("fonts/NotoSerif-Regular.ttf", 30)
|
||||
|
||||
# Add text labels on top of the image
|
||||
stats_label = f"+{final_stat}" if final_stat > 0 else f"{final_stat}"
|
||||
|
||||
print(f"{STATS_DICT[stat['id']]['name']}: {stats_label}")
|
||||
label_width, label_height = get_correct_text_sizes(draw, stats_label, stats_font)
|
||||
|
||||
draw.text(calculate_coordinates(label_width, label_height, STATS_DICT[stat["id"]]["coords"]), stats_label, font=stats_font, fill=(255, 255, 255))
|
||||
|
||||
name_font = ImageFont.truetype("fonts/alaruss.ttf", 96)
|
||||
name_label = char['characterName'].upper()
|
||||
|
||||
label_width, label_height = get_correct_text_sizes(draw, name_label, name_font)
|
||||
|
||||
draw.text((draw_width-label_width-20, 120), name_label, font=name_font, fill=(255,255,255))
|
||||
|
||||
class_font = ImageFont.truetype("fonts/NotoSerif-Regular.ttf", 32)
|
||||
class_label = char['characterClass'].lower()
|
||||
label_width, label_height = get_correct_text_sizes(draw, class_label, class_font)
|
||||
|
||||
draw.text((draw_width-label_width-15, 203), class_label, font=class_font, fill=(255,255,255))
|
||||
|
||||
results_dir = "results"
|
||||
if not os.path.exists(results_dir):
|
||||
os.makedirs(results_dir)
|
||||
|
||||
background.save(f"results/{char['portraitName']}-main.png")
|
||||
|
||||
return True
|
||||
|
||||
# Save the resulting image
|
||||
generate_main(DNDB_CHARS[0])
|
||||
# for char in DNDB_CHARS:
|
||||
# generate_main(char)
|
||||
Reference in New Issue
Block a user