Example #1
0
async def make_averaget_best(team_docs,
                             current_page,
                             max_page,
                             category,
                             invoker_doc=None):
    """Generate and return the best teams of the tournament in a certain category as an image.
    
    - `team_docs` is an array of player documents, 10 or fewer, of the map's scores.
    This should be found through `db_get.get_top_team_scores()` prior to calling this function.
    - `current_page` is the page from `db_get`.
    - `max_page` is also the page from `db_get`.
    - `category` should be the leaderboard category.
    - `invoker_doc` should be the User document of the command invoker if player is set.
    Calls a `db_get` function.
    
    `current_page` and `max_page` are used solely for the page indicator in the upper-left of
    the image."""
    def draw_std(x, y, text, font="m"):
        #looool
        draw.text((x, y),
                  str(text), (255, 255, 255),
                  font=fonts[font],
                  align='center',
                  anchor="mm")

    headers = {
        "score": ["Score", "Acc"],
        "acc": ["Acc", "Score"],
    }

    base_img_fp = "src/static/averagelb.png"
    img = Image.open(base_img_fp, mode='r')

    draw = ImageDraw.Draw(img)

    #header
    draw_std(640, 65, f"Best Teams - {headers[category][0]}", "l")  #team name

    #page number
    page_text = f"(page {current_page} of {max_page})"
    draw.text((36, 137),
              page_text, (255, 255, 255),
              font=fonts["s"],
              align='left',
              anchor="lm")

    #table header
    header_font = ImageFont.truetype("src/static/Renogare-Regular.otf", 25)
    draw.text((259, 177),
              "Team", (255, 255, 255),
              font=header_font,
              align='center',
              anchor="mm")
    draw.text((487, 177),
              headers[category][0], (255, 255, 255),
              font=header_font,
              align='center',
              anchor="mm")
    draw.text((648, 177),
              headers[category][1], (255, 255, 255),
              font=header_font,
              align='center',
              anchor="mm")

    #table
    #x-dists: 70,116(left),266,876,1035,1172
    #y-dist: 39 each row
    for row, team in enumerate(team_docs):
        stat = team["cached"]
        header_order = {
            "score": [
                comma_sep(stat["average_score"]),
                percentage(stat["average_acc"])
            ],
            "acc": [
                percentage(stat["average_acc"]),
                comma_sep(stat["average_score"])
            ]
        }

        y_pos = (row * 39) + 216
        draw_std(62, y_pos,
                 (current_page - 1) * 10 + row + 1)  #numerical ranking
        draw_std(259, y_pos, team["_id"])  #player name
        draw_std(487, y_pos, header_order[category][0])
        draw_std(648, y_pos, header_order[category][1])

    if invoker_doc["osu_id"]:
        team_doc = await db_get.get_team_document(invoker_doc["team_name"])
        stat = team_doc["cached"]
        header_order = {
            "score": [
                comma_sep(stat["average_score"]),
                percentage(stat["average_acc"])
            ],
            "acc": [
                percentage(stat["average_acc"]),
                comma_sep(stat["average_score"])
            ],
        }
        invoker_rank = {
            "score": stat["score_rank"],
            "acc": stat["acc_rank"],
        }
        if math.floor(invoker_rank[category] / 10) != current_page - 1:
            y_pos = 645
            draw_std(62, y_pos - 39, "...")  #ellipsis
            draw_std(62, y_pos, invoker_rank[category])  #numerical ranking
            draw_std(259, y_pos, team_doc["_id"])  #player name
            draw_std(487, y_pos, header_order[category][0])
            draw_std(648, y_pos, header_order[category][1])

    #i guess you have to seek before you actually do the thing
    #solution from here: #https://stackoverflow.com/questions/63209888/send-pillow-image-on-discord-without-saving-the-image
    img_binary = io.BytesIO()
    img.save(img_binary, 'PNG')
    img_binary.seek(0)
    return img_binary
Example #2
0
async def make_server_best(score_docs,
                           current_page,
                           max_page,
                           mod_filter=None,
                           category="score"):
    """Generate and return the best scores of the tournament as an image.
    
    - `score_docs` is an array of score documents, 10 or fewer, of the best scores.
    This should be found through `db_get.get_top_team_scores()` prior to calling this function.
    - `current_page` is the page from `db_get`.
    - `max_page` is also the page from `db_get`.
    - `mod_filter` is the mod these documents are filtered by, if applicable.
    - `category` is the leaderboard category these documents are sorted by, if applicable.
    
    `current_page` and `max_page` are used solely for the page indicator in the upper-left of
    the image."""
    def draw_std(x, y, text, font="m"):
        #looool
        draw.text((x, y),
                  str(text), (255, 255, 255),
                  font=fonts[font],
                  align='center',
                  anchor="mm")

    def truncate(text, font="m"):
        """Truncates long strings to the desired max width and adds an ellipsis if needed."""
        max_width = 373

        font = fonts[font]
        ellipsis_width, _ = font.getsize("...")

        width, _ = font.getsize(text)
        if width > max_width:
            while width > (max_width - ellipsis_width):
                text = text[:-1]
                width, _ = font.getsize(text)
            text += "..."
        return text

    #literally a copypaste of teambest lol

    #we change the category order based on the ordering
    #array 1 is the actual score doc references, array 2 is the table headers
    header_order = {
        "score": ["Score", "Acc", "Contrib"],
        "acc": ["Acc", "Score", "Contrib"],
        "contrib": ["Contrib", "Score", "Acc"]
    }

    player_card_base_img_fp = "src/static/serverbest.png"
    img = Image.open(player_card_base_img_fp, mode='r')

    draw = ImageDraw.Draw(img)

    #header
    draw_std(640, 65, f"Top Scores - {header_order[category][0]}",
             "l")  #static

    #table header
    header_font = ImageFont.truetype("src/static/Renogare-Regular.otf", 25)
    draw.text((916, 177),
              header_order[category][0], (255, 255, 255),
              font=header_font,
              align='center',
              anchor="mm")
    draw.text((1046, 177),
              header_order[category][1], (255, 255, 255),
              font=header_font,
              align='center',
              anchor="mm")
    draw.text((1176, 177),
              header_order[category][2], (255, 255, 255),
              font=header_font,
              align='center',
              anchor="mm")

    #page number
    page_text = f"(page {current_page} of {max_page})"
    if mod_filter:
        page_text += f" ({mod_filter})"
    draw.text((36, 137),
              page_text, (255, 255, 255),
              font=fonts["s"],
              align='left',
              anchor="lm")

    colors = {
        "NM": (165, 165, 165),
        "HD": (255, 192, 0),
        "HR": (255, 0, 0),
        "DT": (0, 176, 240),
        "FM": (146, 208, 80),
        "TB": (146, 208, 80)
    }
    #table
    #x-dists: 70,116(left),266,876,1035,1172
    #y-dist: 39 each row
    for row, score in enumerate(score_docs):
        map_doc = await db_get.get_map_document(score["diff_id"])

        header_order = {
            "score": [
                comma_sep(score["score"]),
                percentage(score["accuracy"]),
                percentage(score["contrib"])
            ],
            "acc": [
                percentage(score["accuracy"]),
                comma_sep(score["score"]),
                percentage(score["contrib"])
            ],
            "contrib": [
                percentage(score["contrib"]),
                comma_sep(score["score"]),
                percentage(score["accuracy"])
            ]
        }

        banner_fp = await image_handling.get_banner_fp(map_doc["set_id"])
        banner = Image.open(banner_fp, mode='r')
        banner = banner.resize((139, 37))

        y_pos = (row * 39) + 216

        draw_std(55, y_pos,
                 (current_page - 1) * 10 + row + 1)  #numerical ranking
        draw_std(205, y_pos, score["user_name"])  #player name
        #tuple refers to top-left corner, so half the banner's height is subtracted
        img.paste(banner, (321, y_pos - 19))  #map banner
        draw.line([461, y_pos - 19, 461, y_pos + 19],
                  colors[score["map_type"]], 5)  #modline
        meta = map_doc["meta"]
        full_name = meta["map_artist"] + " - " + meta[
            "map_song"] + " [" + meta["map_diff"] + "]"
        draw.text((471, y_pos),
                  truncate(full_name, "s"), (255, 255, 255),
                  font=fonts["s"],
                  align='left',
                  anchor="lm")  #map name
        draw_std(916, y_pos, header_order[category][0], "s")
        draw_std(1046, y_pos, header_order[category][1], "s")
        draw_std(1176, y_pos, header_order[category][2], "s")

    img_binary = io.BytesIO()
    img.save(img_binary, 'PNG')
    img_binary.seek(0)
    return img_binary
Example #3
0
async def make_player_card(player_doc):
    """Generate and return a team card (as a discord.py-compatible image).
    
    - `player_doc` is a player document."""
    def draw_std(x, y, text, font="m"):
        #looool
        draw.text((x, y),
                  str(text), (255, 255, 255),
                  font=fonts[font],
                  align='center',
                  anchor="mm")

    stat = player_doc['cached']

    player_card_base_img_fp = "src/static/playercard.png"
    img = Image.open(player_card_base_img_fp, mode='r')

    draw = ImageDraw.Draw(img)

    #header
    draw_std(640, 65, player_doc["user_name"], "l")  #player
    draw_std(640, 105, player_doc["team_name"])  #team name

    #average accuracy
    draw_std(185, 218, percentage(stat["average_acc"]))
    draw_std(185, 245, "#" + str(stat["acc_rank"]), "s")

    #average score
    draw_std(640, 218, comma_sep(stat["average_score"]))
    draw_std(640, 245, "#" + str(stat["score_rank"]), "s")

    #average contrib
    draw_std(1106, 218, percentage(stat["average_contrib"]))
    draw_std(1106, 245, "#" + str(stat["contrib_rank"]), "s")

    #stat row
    draw_std(104, 335, stat['maps_played'])  #playcount
    if stat['maps_played'] != 0:
        wr_str = str(stat["maps_won"]) + "/" + str(
            stat['maps_lost']) + " (" + percentage(
                stat["maps_won"] / stat["maps_played"]) + ")"
    else:
        wr_str = "-"
    draw_std(311, 335, wr_str)  #w/r(wr%)
    draw_std(742, 335, comma_sep(stat["hits"]["300_count"]))  #300s
    draw_std(886, 335, comma_sep(stat["hits"]["100_count"]))  #100s
    draw_std(1028, 335, comma_sep(stat["hits"]["50_count"]))  #50s
    draw_std(1173, 335, comma_sep(stat["hits"]["miss_count"]))  #misss

    #table
    #x-dists: 180,345,548,702,840
    #y-dist: 39 each row starting from 526
    mods = ["NM", "HD", "HR", "DT", "FM"]
    for i in range(0, 5):
        row_pos = 526 + 39 * i
        mod_stat = stat["by_mod"][mods[i]]
        draw_std(180, row_pos, mod_stat["maps_played"])  #played
        if mod_stat["maps_played"] != 0:
            mod_wr_str = str(mod_stat["maps_won"]) + "/" + str(
                mod_stat['maps_lost']) + " (" + percentage(
                    mod_stat["maps_won"] / mod_stat["maps_played"]) + ")"
        else:
            mod_wr_str = "-"
        draw_std(345, row_pos, mod_wr_str)  #w/l (wr%)
        draw_std(548, row_pos,
                 comma_sep(mod_stat["average_score"]))  #average score
        draw_std(702, row_pos,
                 percentage(mod_stat["average_acc"]))  #average acc
        draw_std(840, row_pos, percentage(
            mod_stat["average_contrib"]))  #average contrib - unused for teams

    #pie chart
    #note: iterating over stat["by_mod"] works because dicts are insertion-ordered in python
    #since in db_manip we insert them in a certain order
    #otherwise the colors would be wrong if, for example, stat["by_mod"] returned the mod names
    #alphabetically ordered
    #you may want to hardcode the mod list instead of using stat["by_mod"] if the colors are jank
    if stat['maps_played'] != 0:
        data = [
            stat["by_mod"][mod_name]["maps_played"]
            for mod_name in stat["by_mod"]
        ]
        colors = ["#A5A5A5", "#FFC000", "#FF0000", "#00B0F0", "#92D050"]

        fig1, ax1 = plt.subplots(
            figsize=(3.5, 3.5))  #default is 100dpi, so 350px by 350px
        ax1.pie(data, colors=colors)
        ax1.axis('equal')

        #to binary and into pillow
        #https://stackoverflow.com/questions/8598673/how-to-save-a-pylab-figure-into-in-memory-file-which-can-be-read-into-pil-image/8598881
        plt_binary = io.BytesIO()
        plt.savefig(plt_binary, format='png', transparent=True)
        plt_binary.seek(0)
        plt_img = Image.open(plt_binary)

        #https://stackoverflow.com/questions/5324647/how-to-merge-a-transparent-png-image-with-another-image-using-pil
        #the alpha channel is used as the mask; transparent=True parameter actually saves as transparent
        img.paste(plt_img, (918, 382), plt_img)

    #you need to seek to 0 for it to work:
    #solution from here: #https://stackoverflow.com/questions/63209888/send-pillow-image-on-discord-without-saving-the-image
    #file-like object
    img_binary = io.BytesIO()
    img.save(img_binary, 'PNG')
    img_binary.seek(0)
    return img_binary
Example #4
0
async def make_map_best(score_docs, current_page, max_page, invoker_doc=None):
    """Generate and return a map leaderboard (as a discord.py-compatible image).
    
    - `score_docs` is an array of score documents, 10 or fewer, of the map's scores.
    This should be found through `db_get.get_top_map_scores()` prior to calling this function.
    - `current_page` is the page from `db_get`.
    - `max_page` is also the page from `db_get`.
    - `invoker_doc` should be the User document of the invoker if player is set.
    This calls a `db_get` function.
    
    `current_page` and `max_page` are used solely for the page indicator in the upper-left of
    the image."""
    def draw_std(x, y, text, font="m"):
        #looool
        draw.text((x, y),
                  str(text), (255, 255, 255),
                  font=fonts[font],
                  align='center',
                  anchor="mm")

    def to_standard_size(img):
        """Resize the image to 1280x720px. Unused."""
        width, height = img.size
        multiplier = max(1280 / width, 720 / height)
        resized = img.resize(
            (int(width * multiplier), int(height * multiplier)))
        cropped = resized.crop((0, 0, 1280, 720))
        return cropped

    def to_banner_size(img):
        """Resize banner to take up full width of the main image"""
        width, height = img.size
        multiplier = 1280 / width
        resized = img.resize(
            (int(width * multiplier), int(height * multiplier)))
        return resized

    def apply_gradient(img, gradient_start=0.3, gradient_duration=0.2):
        """Apply transparency gradient.

        - `gradient_start` should be between 0 and 1
        - `gradient_duration` should also be between 0 and 1 where
        `gradient_start+gradient_duration <= 1`, but other values do work
        
        from https://stackoverflow.com/questions/40948710/vertically-fade-an-image-with-transparent-background-to-transparency-using-pytho
        https://stackoverflow.com/questions/19235664/vertically-fade-image-to-transparency-using-python-pil-library/19235788#19235788"""
        im = img
        width, height = im.size
        pixels = im.load()
        for y in range(height):
            for x in range(width):
                initial_alpha = pixels[x, y][
                    3]  #iterating over every pixel, an rgba tuple (r,g,b,a)
                #take current pixel height and subtract by the complete height*gradient_start
                #height*gradient_start represents the pixel at which we start changing the opacity
                #if this value is negative, then the alpha remains for this pixel (as we are not at the gradient_start yet, and the output alpha is > initial_alpha)
                #if the value is nonnegative it represents the number of pixels after the gradient start
                #this is then divided by height to yield a decimal representing how far into the gradient duration we are
                #if this value is further than the gradient duration itself, then alpha evaluates to <0 and so we have already
                #passed the end of the gradient
                #since gradient duration is relative to the full image (not just the gradient part)
                #that is then divided by the gradient duration itself to yield an opacity multiplier
                #and we finally multiply by 255 to get the final opacity
                alpha = initial_alpha - int((y - height * gradient_start) /
                                            height / gradient_duration * 255)
                if alpha <= 0:
                    alpha = 0
                pixels[x, y] = pixels[x, y][:3] + (alpha,
                                                   )  #get rgb and append alpha
        for y in range(y, height):
            for x in range(width):
                pixels[x, y] = pixels[x, y][:3] + (0, )
        return im

    map_doc = await db_get.get_map_document(score_docs[0]["diff_id"])

    base_fp = "src/static/bg-std.png"
    base_img = Image.open(base_fp, mode='r')
    #base_img = Image.new("RGBA", (1280, 720), color="#000000")
    #base_img = Image.new("RGBA", (1280, 720))
    draw = ImageDraw.Draw(base_img)

    banner_fp = await image_handling.get_banner_fp(map_doc["set_id"])
    banner_img = Image.open(banner_fp, mode='r')
    banner_img = to_banner_size(banner_img)
    enhancer = ImageEnhance.Brightness(banner_img)
    banner_img_darkened = enhancer.enhance(0.45).convert("RGBA")
    banner_final = apply_gradient(banner_img_darkened)
    base_img.paste(banner_final, (0, 0), banner_final)

    grid_fp = "src/static/maplb-grid-base.png"
    #https://stackoverflow.com/questions/31273592/valueerror-bad-transparency-mask-when-pasting-one-image-onto-another-with-pyt
    grid_img = Image.open(grid_fp, mode='r').convert("RGBA")
    base_img.paste(grid_img, (0, 0), grid_img)

    #header
    draw_std(640, 65, "Top Scores", "l")  #static
    meta = map_doc["meta"]
    full_name = meta["map_artist"] + " - " + meta["map_song"] + " [" + meta[
        "map_diff"] + "]"
    draw_std(640, 105, full_name)  #full name

    #page number
    draw.text((36, 137),
              f"(page {current_page} of {max_page})", (255, 255, 255),
              font=fonts["s"],
              align='left',
              anchor="lm")

    #table
    #x-dists: 70,116(left),266,876,1035,1172
    #y-dist: 39 each row
    for row, score in enumerate(score_docs):
        y_pos = (row * 39) + 216
        draw_std(54, y_pos,
                 (current_page - 1) * 10 + row + 1)  #numerical ranking
        draw_std(214, y_pos, score["user_name"])  #player name
        draw_std(417, y_pos, comma_sep(score["score"]))  #score
        draw_std(561, y_pos, percentage(score["accuracy"]))  #acc
        hits = (
            f"{comma_sep(score['hits']['300_count'])}/{comma_sep(score['hits']['100_count'])}/"
            f"{comma_sep(score['hits']['50_count'])}/{comma_sep(score['hits']['miss_count'])}"
        )
        draw_std(722, y_pos, hits)  #hits
        draw_std(881, y_pos, comma_sep(score["combo"]) + "x")  #combo

    if invoker_doc["osu_id"]:
        #get player, get best score, check if rank of best score is already on this page
        #if not, do everything below
        score, rank, extra_count = await db_get.get_best_user_score(
            score_docs[0]["diff_id"], invoker_doc["osu_id"])
        if math.floor(rank / 10) != current_page - 1:
            y_pos = 645
            draw_std(54, y_pos - 39, "...")  #ellipsis
            draw_std(54, y_pos, rank)  #numerical ranking
            draw_std(214, y_pos, invoker_doc["osu_name"])  #player name
            draw_std(417, y_pos, comma_sep(score["score"]))  #score
            draw_std(561, y_pos, percentage(score["accuracy"]))  #acc
            hits = (
                f"{comma_sep(score['hits']['300_count'])}/{comma_sep(score['hits']['100_count'])}/"
                f"{comma_sep(score['hits']['50_count'])}/{comma_sep(score['hits']['miss_count'])}"
            )
            draw_std(722, y_pos, hits)  #hits
            draw_std(881, y_pos, comma_sep(score["combo"]) + "x")  #combo
            if extra_count > 0:
                draw.text((54, y_pos + 39),
                          f"(+{extra_count} more)", (255, 255, 255),
                          font=fonts["s"],
                          align='left',
                          anchor="lm")

    #i guess you have to seek before you actually do the thing
    #solution from here: #https://stackoverflow.com/questions/63209888/send-pillow-image-on-discord-without-saving-the-image
    img_binary = io.BytesIO()
    base_img.save(img_binary, 'PNG')
    img_binary.seek(0)
    return img_binary
Example #5
0
async def make_team_best(score_docs, current_page, max_page, mod_filter=None):
    """Generate and return a team score leaderboard(as a discord.py-compatible image).
    
    - `score_docs` is an array of score documents, 10 or fewer, of the team's scores.
    This should be found through `db_get.get_top_team_scores()` prior to calling this function.
    (This allows for mod filtering at the command level.)
    - `current_page` is the page from `db_get`.
    - `max_page` is also the max page from `db_get`.
    - `mod_filter` is the mod these documents are filtered by, if applicable.
    
    `current_page` and `max_page` are used solely for the page indicator in the upper-left of
    the image."""
    def draw_std(x, y, text, font="m"):
        #looool
        draw.text((x, y),
                  str(text), (255, 255, 255),
                  font=fonts[font],
                  align='center',
                  anchor="mm")

    def truncate(text, font="m"):
        """Truncates long strings to the desired max width and adds an ellipsis if needed."""
        max_width = 487

        font = fonts[font]
        ellipsis_width, _ = font.getsize("...")

        width, _ = font.getsize(text)
        if width > max_width:
            while width > (max_width - ellipsis_width):
                text = text[:-1]
                width, _ = font.getsize(text)
            text += "..."
        return text

    player_card_base_img_fp = "src/static/teambest.png"
    img = Image.open(player_card_base_img_fp, mode='r')

    draw = ImageDraw.Draw(img)

    #header
    player_doc = await db_get.get_player_document(score_docs[0]["user_id"])
    team_doc = await db_get.get_team_document(player_doc["team_name"])
    player_names = [(await db_get.get_player_document(player))["user_name"]
                    for player in team_doc["players"]]
    draw_std(640, 65, team_doc["_id"], "l")  #team name
    draw_std(640, 105, " • ".join(player_names))  #player list

    #page number
    page_text = f"(page {current_page} of {max_page})"
    if mod_filter:
        page_text += f" ({mod_filter})"
    draw.text((36, 137),
              page_text, (255, 255, 255),
              font=fonts["s"],
              align='left',
              anchor="lm")

    colors = {
        "NM": (165, 165, 165),
        "HD": (255, 192, 0),
        "HR": (255, 0, 0),
        "DT": (0, 176, 240),
        "FM": (146, 208, 80),
        "TB": (146, 208, 80)
    }
    #table
    #x-dists: 70,116(left),266,876,1035,1172
    #y-dist: 39 each row
    for row, score in enumerate(score_docs):
        map_doc = await db_get.get_map_document(score["diff_id"])

        banner_fp = await image_handling.get_banner_fp(map_doc["set_id"])
        banner = Image.open(banner_fp, mode='r')
        banner = banner.resize((139, 37))

        y_pos = (row * 39) + 216

        draw_std(76, y_pos,
                 (current_page - 1) * 10 + row + 1)  #numerical ranking
        draw_std(267, y_pos, score["user_name"])  #player name
        #tuple refers to top-left corner, so half the banner's height is subtracted
        img.paste(banner, (406, y_pos - 19))  #map banner
        draw.line([546, y_pos - 19, 546, y_pos + 19],
                  colors[score["map_type"]], 5)  #modline
        meta = map_doc["meta"]
        full_name = meta["map_artist"] + " - " + meta[
            "map_song"] + " [" + meta["map_diff"] + "]"
        draw.text((556, y_pos),
                  truncate(full_name, "s"), (255, 255, 255),
                  font=fonts["s"],
                  align='left',
                  anchor="lm")  #map name

        draw_std(1160, y_pos, comma_sep(score["score"]))  #score

    img_binary = io.BytesIO()
    img.save(img_binary, 'PNG')
    img_binary.seek(0)
    return img_binary