|
| 1 | +from PIL import Image, ImageDraw, ImageFont, ImageOps |
| 2 | +import time |
| 3 | + |
| 4 | +def adjust_icon_color(icon, background_color): |
| 5 | + """Adjust the icon color based on the background color.""" |
| 6 | + if background_color.lower() in ['#1e1e1e', '#333', '#000000']: # Dark backgrounds |
| 7 | + # Ensure the icon is in the correct mode before inverting |
| 8 | + if icon.mode == 'RGBA': |
| 9 | + r, g, b, a = icon.split() # Preserve the alpha channel |
| 10 | + rgb_image = Image.merge('RGB', (r, g, b)) |
| 11 | + inverted_image = ImageOps.invert(rgb_image) |
| 12 | + icon = Image.merge('RGBA', (inverted_image.split()[0], inverted_image.split()[1], inverted_image.split()[2], a)) |
| 13 | + else: |
| 14 | + icon = ImageOps.invert(icon.convert('RGB')).convert('RGBA') |
| 15 | + return icon |
| 16 | + |
| 17 | +def generate_stats_image(stats, bgColor, textColor, cardColor): |
| 18 | + # Create an image with dimensions 800x450 |
| 19 | + img = Image.new('RGB', (800, 450), color=bgColor) |
| 20 | + draw = ImageDraw.Draw(img) |
| 21 | + |
| 22 | + # Load the fonts (Using default fonts here, replace with truetype as needed) |
| 23 | + header_font = ImageFont.truetype("app/static/fonts/Font.ttf", 32) |
| 24 | + text_font = ImageFont.truetype("app/static/fonts/Font.ttf", 16) |
| 25 | + |
| 26 | + if 'error' in stats: |
| 27 | + draw.text((10, 10), f"Error: {stats['error']}", font=text_font, fill='red') |
| 28 | + return img |
| 29 | + |
| 30 | + # Load icons for each stat (right now all use star.png, customize paths as you have more icons) |
| 31 | + star_icon = adjust_icon_color(Image.open('app/static/icons/star.png').resize((20, 20)), bgColor) |
| 32 | + commit_icon = adjust_icon_color(Image.open('app/static/icons/commit.png').resize((20, 20)), bgColor) |
| 33 | + pr_icon = adjust_icon_color(Image.open('app/static/icons/pr.png').resize((20, 20)), bgColor) |
| 34 | + merged_pr_icon = adjust_icon_color(Image.open('app/static/icons/pr.png').resize((20, 20)), bgColor) |
| 35 | + issue_icon = adjust_icon_color(Image.open('app/static/icons/issue.png').resize((20, 20)), bgColor) |
| 36 | + discussions_icon = adjust_icon_color(Image.open('app/static/icons/discussion.png').resize((20, 20)), bgColor) |
| 37 | + follower_icon = adjust_icon_color(Image.open('app/static/icons/follower.png').resize((20, 20)), bgColor) |
| 38 | + following_icon = adjust_icon_color(Image.open('app/static/icons/following.png').resize((20, 20)), bgColor) |
| 39 | + contributions_icon = adjust_icon_color(Image.open('app/static/icons/contributions.png').resize((20, 20)), bgColor) |
| 40 | + |
| 41 | + # Draw the card background |
| 42 | + card_width, card_height = 780, 440 |
| 43 | + card_x, card_y = 10, 10 |
| 44 | + draw.rectangle((card_x, card_y, card_x + card_width, card_y + card_height), fill=cardColor) |
| 45 | + |
| 46 | + # Draw the username and the stats on the image |
| 47 | + draw.text((card_x + 20, card_y + 20), f"{stats['username']}'s GitHub Stats", font=header_font, fill=textColor) |
| 48 | + |
| 49 | + # Place icons before each stat |
| 50 | + icon_y_start = card_y + 70 |
| 51 | + spacing = 30 # Spacing between each stat |
| 52 | + |
| 53 | + # Column settings for alignment |
| 54 | + icon_x = card_x + 20 |
| 55 | + text_x = card_x + 50 |
| 56 | + value_x = card_x + 400 # Fixed column for all the stat values |
| 57 | + |
| 58 | + # Paste the icons and draw the corresponding text |
| 59 | + img.paste(star_icon, (icon_x, icon_y_start), star_icon) |
| 60 | + draw.text((text_x, icon_y_start), "Total Stars:", font=text_font, fill=textColor) |
| 61 | + draw.text((value_x, icon_y_start), f"{stats['total_stars']}", font=text_font, fill=textColor) |
| 62 | + |
| 63 | + img.paste(commit_icon, (icon_x, icon_y_start + spacing), commit_icon) |
| 64 | + draw.text((text_x, icon_y_start + spacing), "Total Commits:", font=text_font, fill=textColor) |
| 65 | + draw.text((value_x, icon_y_start + spacing), f"{stats['total_commits']}", font=text_font, fill=textColor) |
| 66 | + |
| 67 | + img.paste(pr_icon, (icon_x, icon_y_start + spacing * 2), pr_icon) |
| 68 | + draw.text((text_x, icon_y_start + spacing * 2), "Total PRs:", font=text_font, fill=textColor) |
| 69 | + draw.text((value_x, icon_y_start + spacing * 2), f"{stats['total_prs']}", font=text_font, fill=textColor) |
| 70 | + |
| 71 | + img.paste(merged_pr_icon, (icon_x, icon_y_start + spacing * 3), merged_pr_icon) |
| 72 | + draw.text((text_x, icon_y_start + spacing * 3), "Total Merged PRs:", font=text_font, fill=textColor) |
| 73 | + draw.text((value_x, icon_y_start + spacing * 3), f"{stats['merged_prs']}", font=text_font, fill=textColor) |
| 74 | + |
| 75 | + img.paste(issue_icon, (icon_x, icon_y_start + spacing * 4), issue_icon) |
| 76 | + draw.text((text_x, icon_y_start + spacing * 4), "Total Issues:", font=text_font, fill=textColor) |
| 77 | + draw.text((value_x, icon_y_start + spacing * 4), f"{stats['issues']}", font=text_font, fill=textColor) |
| 78 | + |
| 79 | + img.paste(discussions_icon, (icon_x, icon_y_start + spacing * 5), discussions_icon) |
| 80 | + draw.text((text_x, icon_y_start + spacing * 5), "Discussions Started:", font=text_font, fill=textColor) |
| 81 | + draw.text((value_x, icon_y_start + spacing * 5), f"{stats['discussions_started']}", font=text_font, fill=textColor) |
| 82 | + |
| 83 | + img.paste(follower_icon, (icon_x, icon_y_start + spacing * 6), follower_icon) |
| 84 | + draw.text((text_x, icon_y_start + spacing * 6), "Total Followers:", font=text_font, fill=textColor) |
| 85 | + draw.text((value_x, icon_y_start + spacing * 6), f"{stats['total_followers']}", font=text_font, fill=textColor) |
| 86 | + |
| 87 | + img.paste(following_icon, (icon_x, icon_y_start + spacing * 7), following_icon) |
| 88 | + draw.text((text_x, icon_y_start + spacing * 7), "Total Following:", font=text_font, fill=textColor) |
| 89 | + draw.text((value_x, icon_y_start + spacing * 7), f"{stats['total_following']}", font=text_font, fill=textColor) |
| 90 | + |
| 91 | + img.paste(contributions_icon, (icon_x, icon_y_start + spacing * 8), contributions_icon) |
| 92 | + draw.text((text_x, icon_y_start + spacing * 8), "Total Contributions:", font=text_font, fill=textColor) |
| 93 | + draw.text((value_x, icon_y_start + spacing * 8), f"{stats['total_contributions']}", font=text_font, fill=textColor) |
| 94 | + |
| 95 | + # Draw the rating inside a circle at the bottom right |
| 96 | + rating_x, rating_y = card_x + 600, card_y + 200 |
| 97 | + rating_radius = 66 |
| 98 | + draw.ellipse((rating_x - rating_radius, rating_y - rating_radius, rating_x + rating_radius, rating_y + rating_radius), fill=textColor) |
| 99 | + |
| 100 | + # Draw the rating text inside the circle |
| 101 | + rating_font = ImageFont.truetype("app/static/fonts/Font.ttf", 32) |
| 102 | + rating_text = stats['rating'] |
| 103 | + |
| 104 | + # Use font.getbbox() to get the text size |
| 105 | + bbox = rating_font.getbbox(rating_text) |
| 106 | + text_width, text_height = bbox[2] - bbox[0], bbox[3] - bbox[1] |
| 107 | + draw.text((rating_x - text_width / 2, rating_y - text_height / 2), rating_text, font=rating_font, fill=cardColor) |
| 108 | + |
| 109 | + # Add last updated time at the bottom of the card |
| 110 | + last_updated_text = f"Last updated: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(stats['last_updated']))}" |
| 111 | + draw.text((card_x + 266, card_y + card_height - 90), last_updated_text, font=text_font, fill=textColor) |
| 112 | + |
| 113 | +# Add a small note to inform the user about the 24-hour update cycle (split into two lines) |
| 114 | + update_info_text_1 = "* Stats are updated every 24 hours automatically to prevent excessive API requests" |
| 115 | + update_info_text_2 = " and to ensure that the service remains efficient and avoids hitting rate limits." |
| 116 | + |
| 117 | +# Place the two lines of the update info text |
| 118 | + draw.text((card_x + 20, card_y + card_height - 46), update_info_text_1, font=text_font, fill=textColor) |
| 119 | + draw.text((card_x + 20, card_y + card_height - 26), update_info_text_2, font=text_font, fill=textColor) |
| 120 | + |
| 121 | + |
| 122 | + return img |
0 commit comments