|
| 1 | +import os |
| 2 | +import json |
| 3 | +import urllib.request |
| 4 | +from io import BytesIO |
| 5 | +from PIL import Image, ImageDraw, ImageFont, ImageFilter |
| 6 | + |
| 7 | +CONFIG_PATH = "../showcase_config.json" |
| 8 | +INV_SLOT_PATH = "../images/inv_slot.png" |
| 9 | +OUTPUT_PATH = "../images/showcase_output.png" |
| 10 | +CUSTOM_ASSETS_DIR = "../images/showcase_assets" |
| 11 | + |
| 12 | +MC_ASSETS_BASE = "https://raw.githubusercontent.com/Owen1212055/mc-assets/master/assets/minecraft/textures/" |
| 13 | + |
| 14 | +GRID_COLS = 9 |
| 15 | +GRID_ROWS = 3 |
| 16 | +SLOT_SIZE = 162 |
| 17 | +ITEM_SIZE = 144 |
| 18 | +GAP = 54 |
| 19 | +SIDEBAR_WIDTH = 250 |
| 20 | + |
| 21 | +def download_image(url): |
| 22 | + try: |
| 23 | + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) |
| 24 | + with urllib.request.urlopen(req) as response: |
| 25 | + return Image.open(BytesIO(response.read())).convert("RGBA") |
| 26 | + except Exception as e: |
| 27 | + print(f"Failed to download {url}: {e}") |
| 28 | + # Return a transparent dummy image |
| 29 | + return Image.new("RGBA", (256, 256), (0, 0, 0, 0)) |
| 30 | + |
| 31 | +def get_texture(path, is_block): |
| 32 | + # Try local first |
| 33 | + local_path = os.path.join(CUSTOM_ASSETS_DIR, f"{path}.png") |
| 34 | + if os.path.exists(local_path): |
| 35 | + img = Image.open(local_path).convert("RGBA") |
| 36 | + else: |
| 37 | + url = f"{MC_ASSETS_BASE}{path}.png" |
| 38 | + img = download_image(url) |
| 39 | + |
| 40 | + # Scale from 256x256 to 144x144 |
| 41 | + if is_block: |
| 42 | + # Blocks use Lanczos for smooth 3D edges |
| 43 | + img = img.resize((ITEM_SIZE, ITEM_SIZE), Image.Resampling.LANCZOS) |
| 44 | + else: |
| 45 | + # Items are downscaled to 16x16 nearest neighbor, then upscaled to 144x144 nearest neighbor |
| 46 | + img = img.resize((16, 16), Image.Resampling.NEAREST) |
| 47 | + img = img.resize((ITEM_SIZE, ITEM_SIZE), Image.Resampling.NEAREST) |
| 48 | + |
| 49 | + return img |
| 50 | + |
| 51 | +def create_background(width, height, override_path): |
| 52 | + if override_path and os.path.exists(override_path): |
| 53 | + bg = Image.open(override_path).convert("RGBA") |
| 54 | + # Resize to fill, center crop |
| 55 | + ratio = max(width / bg.width, height / bg.height) |
| 56 | + new_size = (int(bg.width * ratio), int(bg.height * ratio)) |
| 57 | + bg = bg.resize(new_size, Image.Resampling.LANCZOS) |
| 58 | + left = (bg.width - width) / 2 |
| 59 | + top = (bg.height - height) / 2 |
| 60 | + bg = bg.crop((left, top, left + width, top + height)) |
| 61 | + bg = bg.filter(ImageFilter.GaussianBlur(radius=5)) |
| 62 | + return bg |
| 63 | + else: |
| 64 | + # Default gradient (solid color as a fallback if no dynamic gen is wanted) |
| 65 | + bg = Image.new("RGBA", (width, height), (211, 140, 83, 255)) |
| 66 | + return bg |
| 67 | + |
| 68 | +def main(): |
| 69 | + # Resolve paths relative to script |
| 70 | + script_dir = os.path.dirname(os.path.abspath(__file__)) |
| 71 | + os.chdir(script_dir) |
| 72 | + |
| 73 | + if not os.path.exists(CONFIG_PATH): |
| 74 | + print(f"Config not found at {CONFIG_PATH}") |
| 75 | + return |
| 76 | + |
| 77 | + with open(CONFIG_PATH, "r") as f: |
| 78 | + config = json.load(f) |
| 79 | + |
| 80 | + grid_width = GRID_COLS * SLOT_SIZE |
| 81 | + grid_height = GRID_ROWS * SLOT_SIZE |
| 82 | + |
| 83 | + canvas_width = SIDEBAR_WIDTH + grid_width + 50 |
| 84 | + canvas_height = 50 + grid_height + GAP + grid_height + 50 |
| 85 | + |
| 86 | + bg = create_background(canvas_width, canvas_height, config.get("background_override")) |
| 87 | + |
| 88 | + try: |
| 89 | + inv_slot = Image.open(INV_SLOT_PATH).convert("RGBA") |
| 90 | + except Exception: |
| 91 | + # fallback if inv_slot doesn't exist yet |
| 92 | + inv_slot = Image.new("RGBA", (SLOT_SIZE, SLOT_SIZE), (255, 0, 0, 100)) |
| 93 | + |
| 94 | + off_grid = Image.new("RGBA", (grid_width, grid_height), (0,0,0,0)) |
| 95 | + on_grid = Image.new("RGBA", (grid_width, grid_height), (0,0,0,0)) |
| 96 | + |
| 97 | + # Draw empty slots |
| 98 | + for r in range(GRID_ROWS): |
| 99 | + for c in range(GRID_COLS): |
| 100 | + x = c * SLOT_SIZE |
| 101 | + y = r * SLOT_SIZE |
| 102 | + off_grid.paste(inv_slot, (x, y), inv_slot) |
| 103 | + on_grid.paste(inv_slot, (x, y), inv_slot) |
| 104 | + |
| 105 | + # Draw items |
| 106 | + for item in config.get("items", []): |
| 107 | + slot = item.get("slot", 0) |
| 108 | + r = slot // GRID_COLS |
| 109 | + c = slot % GRID_COLS |
| 110 | + if r >= GRID_ROWS: |
| 111 | + continue |
| 112 | + |
| 113 | + x = c * SLOT_SIZE + (SLOT_SIZE - ITEM_SIZE) // 2 |
| 114 | + y = r * SLOT_SIZE + (SLOT_SIZE - ITEM_SIZE) // 2 |
| 115 | + |
| 116 | + is_block = (item.get("type") == "block") |
| 117 | + |
| 118 | + if "off_texture" in item: |
| 119 | + off_img = get_texture(item["off_texture"], is_block) |
| 120 | + off_grid.paste(off_img, (x, y), off_img) |
| 121 | + |
| 122 | + if "on_texture" in item: |
| 123 | + on_img = get_texture(item["on_texture"], is_block) |
| 124 | + on_grid.paste(on_img, (x, y), on_img) |
| 125 | + |
| 126 | + # Paste grids to bg |
| 127 | + grid_x = SIDEBAR_WIDTH |
| 128 | + top_y = 50 |
| 129 | + bottom_y = top_y + grid_height + GAP |
| 130 | + |
| 131 | + bg.paste(off_grid, (grid_x, top_y), off_grid) |
| 132 | + bg.paste(on_grid, (grid_x, bottom_y), on_grid) |
| 133 | + |
| 134 | + # Draw sidebar |
| 135 | + draw = ImageDraw.Draw(bg) |
| 136 | + try: |
| 137 | + # Try to get a system font or download one |
| 138 | + url = "https://github.com/googlefonts/opensans/raw/main/fonts/ttf/OpenSans-Bold.ttf" |
| 139 | + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) |
| 140 | + font_bytes = BytesIO(urllib.request.urlopen(req).read()) |
| 141 | + font = ImageFont.truetype(font_bytes, 100) |
| 142 | + except Exception: |
| 143 | + font = ImageFont.load_default() |
| 144 | + |
| 145 | + # Draw rotated text |
| 146 | + def draw_rotated_text(text, x, y): |
| 147 | + txt_img = Image.new('RGBA', (300, 120), (0, 0, 0, 0)) |
| 148 | + txt_draw = ImageDraw.Draw(txt_img) |
| 149 | + txt_draw.text((0, 0), text, font=font, fill=(255, 255, 255, 255)) |
| 150 | + txt_img = txt_img.rotate(90, expand=1) |
| 151 | + bg.paste(txt_img, (x, y), txt_img) |
| 152 | + |
| 153 | + draw_rotated_text("OFF", 80, top_y + 100) |
| 154 | + draw_rotated_text("ON", 80, bottom_y + 150) |
| 155 | + |
| 156 | + # Draw arrows down |
| 157 | + arrow_y = top_y + grid_height + (GAP // 2) - 30 |
| 158 | + draw.polygon([(100, arrow_y), (150, arrow_y), (125, arrow_y + 40)], fill=(255, 255, 255, 255)) |
| 159 | + draw.polygon([(100, arrow_y+20), (150, arrow_y+20), (125, arrow_y + 60)], fill=(255, 255, 255, 255)) |
| 160 | + |
| 161 | + os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True) |
| 162 | + bg.save(OUTPUT_PATH) |
| 163 | + print(f"Saved to {os.path.abspath(OUTPUT_PATH)}") |
| 164 | + |
| 165 | +if __name__ == "__main__": |
| 166 | + main() |
0 commit comments