Advertisement
zeltxtic

Custom Image Generation

May 10th, 2025
194
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.92 KB | None | 0 0
  1. import sys
  2. import os
  3.  
  4. import discord.ext
  5.  
  6. # this block here walks up the directory structure until it finds the bot.py file
  7. # once it does, it sets the sys.path[0] to that directory so imports work correctly
  8. while True:
  9.     if os.path.exists('bot.py'):
  10.         sys.path[0] = os.getcwd()
  11.         break
  12.     sys.path[0] = "/".join(sys.path[0].split("/")[0:-1])  # basically go up one folder
  13.  
  14. # bunch of image and data imports here
  15. import PIL.ImageColor
  16. import PIL.ImageFont
  17. import easy_pil  # library for easier image editing (like a wrapper)
  18. import PIL
  19. from io import BytesIO  # used later to store the final image in memory (instead of saving)
  20. from constants import fruit_values  # this is the dictionary with fruit names and their prices
  21. import random  # for fruit rotation randomness
  22. from PIL import ImageDraw  # needed to draw text and stuff
  23. from datetime import datetime, timezone  # to get current time for display
  24.  
  25. # discord stuff (actually not used in this snippet, but prob used elsewhere in project)
  26. import discord
  27. from discord.ext import commands
  28.  
  29.  
  30. # helper func to get how big some text will be when drawn using a font
  31. def get_text_size(text: str, font: PIL.ImageFont.FreeTypeFont):
  32.     dummy_img = PIL.Image.new("RGBA", (1, 1))  # just a dummy image to measure text
  33.     draw = ImageDraw.Draw(dummy_img)
  34.     bbox = draw.textbbox((0, 0), text, font=font)
  35.     width = bbox[2] - bbox[0]
  36.     height = bbox[3] - bbox[1]
  37.     return width, height  # return width and height so we can center stuff
  38.  
  39.  
  40. # helper func to get the avg color of an image by resizing it to 1x1 lol
  41. def get_average_color(image: PIL.Image.Image) -> tuple[int, int, int]:
  42.     small_image = image.resize((1, 1))  # shrinking the image to 1 pixel gives avg color
  43.     average_color = small_image.getpixel((0, 0))
  44.     return average_color
  45.  
  46.  
  47. # makes the given color more "vibrant" by multiplying its RGB values
  48. def make_color_more_vibrant(color: tuple[int, int, int], factor: float = 1.3) -> tuple[int, int, int]:
  49.     r, g, b = color
  50.     r = min(int(r * factor), 255)
  51.     g = min(int(g * factor), 255)
  52.     b = min(int(b * factor), 255)
  53.     return (r, g, b)
  54.  
  55.  
  56. # some layout constants here
  57. STOCK_OFFSET = 130  # how much padding left/right
  58. ROW_HEIGHT = 350  # vertical offset of first fruit row
  59. ROW_RELATIVE_OFFSET = 400  # distance between fruit rows
  60. FRUIT_SIZE = 240  # how big each fruit image is
  61. GLOW_MULTIPLIER = 3  # multiplier for glow image size
  62.  
  63. # colors used
  64. GOLDEN_COLOR = (216, 185, 61)
  65. GREY_COLOR = (226, 226, 226)
  66.  
  67.  
  68. # the main async function to generate the stock image
  69. async def generate_stock_image(type: str, fruits: list[str]) -> BytesIO:
  70.     print("RAN IMAGE STOCK")  # debug log
  71.  
  72.     type = type.lower()
  73.     if type not in ['normal', 'mirage']:
  74.         raise ValueError("Invalid type. Must be 'normal' or 'mirage'.")
  75.  
  76.     # copy the fruit list and remove any fruits that aren't in our known values
  77.     all_fruits = fruits.copy()
  78.     for fruit in all_fruits:
  79.         if fruit.lower() not in fruit_values:
  80.             fruits.remove(fruit)
  81.  
  82.     # sort fruits based on their value
  83.     try:
  84.         fruits.sort(key=lambda fruit: int(fruit_values[fruit.lower()].replace(',', '')))
  85.     except KeyError:
  86.         raise ValueError("Invalid fruit name in the list.")
  87.  
  88.     fruit_amount = len(fruits)
  89.     real_rows = (fruit_amount + 2) // 3  # how many actual rows needed
  90.     calc_rows = real_rows + 1  # a bit extra to determine which background to use
  91.  
  92.     # load the background image based on how many rows we need
  93.     background = easy_pil.Editor(f"Assets/background{real_rows}.png")
  94.     background_image = PIL.Image.open(f"Assets/background{real_rows}.png")
  95.  
  96.     width, height = background_image.size
  97.     width -= STOCK_OFFSET * 2  # we subtract padding from total width
  98.  
  99.     # load the fonts (all are variations of the same font)
  100.     title_font = PIL.ImageFont.truetype("Assets/Fonts/Cinzel-ExtraBold.ttf", 115)
  101.     NAME_FONT = PIL.ImageFont.truetype("Assets/Fonts/Cinzel-ExtraBold.ttf", 50)
  102.     PRICE_FONT = PIL.ImageFont.truetype("Assets/Fonts/Cinzel-ExtraBold.ttf", 45)
  103.  
  104.     title_y = 80  # vertical position for the title
  105.  
  106.     # draw the titles like "Normal Stock" or "Mirage Stock"
  107.     background.text(
  108.         (115, title_y) if type == "normal" else (130, title_y),
  109.         type.capitalize(),
  110.         font=title_font,
  111.         color=GREY_COLOR
  112.     )
  113.     background.text(
  114.         (643, title_y) if type == "normal" else (625, title_y),
  115.         "Stock",
  116.         font=title_font,
  117.         color=GOLDEN_COLOR
  118.     )
  119.  
  120.     # get current UTC time and date
  121.     now = datetime.now(timezone.utc)
  122.     time_str = now.strftime("%H:00 (UTC)")
  123.     date_str = now.strftime("%d/%m/%Y")
  124.  
  125.     lines = [time_str, date_str]  # we’ll draw both
  126.  
  127.     DATETIME_FONT = PIL.ImageFont.truetype("Assets/Fonts/Cinzel-ExtraBold.ttf", 35)
  128.  
  129.     # calculate size of both lines
  130.     line_widths = []
  131.     line_heights = []
  132.  
  133.     for line in lines:
  134.         w, h = get_text_size(line, DATETIME_FONT)
  135.         line_widths.append(w)
  136.         line_heights.append(h)
  137.  
  138.     max_width = max(line_widths)
  139.     total_height = sum(line_heights) + (len(lines) - 1) * 10  # spacing between lines
  140.  
  141.     # position of the date+time text (bottom right of image)
  142.     padding = 120
  143.     img_width, img_height = background.image.size
  144.     x = img_width - max_width - padding
  145.     y = img_height - total_height - (padding + 35)
  146.  
  147.     # actually draw the lines one by one
  148.     current_y = y
  149.     for idx, line in enumerate(lines):
  150.         background.text((x, current_y), line, font=DATETIME_FONT, color=GREY_COLOR)
  151.         current_y += line_heights[idx] + 10
  152.  
  153.     # figuring out if we should center the last row (if it has 1 or 2 fruits only)
  154.     last_row = fruit_amount % 3
  155.     center_last_row = last_row != 0 and last_row != 3
  156.  
  157.     for i, fruit in enumerate(fruits):
  158.         row = i // 3
  159.         col = i % 3
  160.  
  161.         cell_width = cell_height = width // 3  # each fruit gets a third of the width
  162.  
  163.         # only apply offset on the last row if it needs centering
  164.         if row == real_rows - 1 and center_last_row:
  165.             offset = (3 - last_row) * (cell_width + 30) // 2
  166.         else:
  167.             offset = 0
  168.  
  169.         # calculate the (x, y) position for each fruit
  170.         x = STOCK_OFFSET + col * (cell_width + 30) + offset
  171.         y = int(ROW_HEIGHT + row * ROW_RELATIVE_OFFSET)
  172.  
  173.         rotation = random.randint(-7, 7)  # small random rotation for variety
  174.  
  175.         # load fruit image, resize and rotate it
  176.         fruit_image = PIL.Image.open(f'Assets/Fruits/{fruit.capitalize()}.png').convert("RGBA")
  177.         fruit_image = fruit_image.resize((FRUIT_SIZE, FRUIT_SIZE))
  178.         fruit_image = fruit_image.rotate(rotation, expand=True)
  179.  
  180.         # get average color of fruit img and make it more vibrant
  181.         avg_color = get_average_color(fruit_image)
  182.         vibrant_color = make_color_more_vibrant(avg_color[:3])
  183.  
  184.         # load and resize glow image (and rotate it too)
  185.         glow_image = PIL.Image.open('Assets/glow.png').convert("RGBA")
  186.         glow_size = FRUIT_SIZE * GLOW_MULTIPLIER
  187.         glow_image = glow_image.resize((glow_size, glow_size))
  188.         glow_image = glow_image.rotate(rotation, expand=True)
  189.  
  190.         # center the glow behind the fruit
  191.         glow_offset_x = x - (glow_size - FRUIT_SIZE) // 2
  192.         glow_offset_y = y - (glow_size - FRUIT_SIZE) // 2
  193.  
  194.         # paste glow and then fruit on top
  195.         background.paste(glow_image, (glow_offset_x, glow_offset_y))
  196.         background.paste(fruit_image, (x, y))
  197.  
  198.         # draw fruit name above the fruit
  199.         fruit_name = fruit.capitalize()
  200.         text_width, text_height = get_text_size(fruit_name, NAME_FONT)
  201.         name_x = x + FRUIT_SIZE // 2 - text_width // 2
  202.         name_y = y - text_height - 10
  203.         background.text((name_x, name_y), fruit_name, font=NAME_FONT, color=vibrant_color)
  204.  
  205.         # draw price below fruit
  206.         price_text = fruit_values.get(fruit.lower(), "0")
  207.         dollar_width, _ = get_text_size("$", PRICE_FONT)
  208.         price_width, _ = get_text_size(price_text, PRICE_FONT)
  209.  
  210.         total_width = dollar_width + price_width + 5
  211.         price_x = x + FRUIT_SIZE // 2 - total_width // 2
  212.         price_y = y + FRUIT_SIZE + 45
  213.  
  214.         background.text((price_x, price_y), "$", font=PRICE_FONT, color=GOLDEN_COLOR)
  215.         background.text((price_x + dollar_width + 5, price_y), price_text, font=PRICE_FONT, color=GREY_COLOR)
  216.  
  217.     # finally save the image into a buffer (we return this instead of saving to disk)
  218.     buffer = BytesIO()
  219.     background.image.save(buffer, format="PNG")
  220.     buffer.seek(0)
  221.  
  222.     return buffer  # this can be sent over discord, for example
  223.  
  224.  
  225. # quick testing setup (will only run if this file is run directly, not imported)
  226. if __name__ == "__main__":
  227.     import asyncio
  228.     asyncio.run(generate_stock_image("normal", ["Kitsune"]))
  229.     asyncio.run(generate_stock_image("mirage", ["Kitsune", "Kitsune", "Eagle", "Eagle", "Rubber", "Gas", "Buddha"]))
  230.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement
OSZAR »