HeadRender
Fetches a player’s skin, downscales it and turns each pixel into a HEX-colored chat line — drop a Minecraft head straight into chat, action messages, profile cards or join banners. One static facade, async by default, with an in-memory LRU cache so repeated lookups are free.
GitHub: senkex/HeadRender · JitPack: jitpack.io/#senkex/HeadRender
| MC | 1.16+ (HEX colors required) |
| Java | 17+ |
| Platform | Bukkit / Spigot / Paper (any fork) |
Installation
Section titled “Installation”Replace VERSION with the latest release.
repositories { maven("https://jitpack.io")}
dependencies { implementation("com.github.senkex:HeadRender:VERSION")}repositories { maven { url 'https://jitpack.io' }}
dependencies { implementation 'com.github.senkex:HeadRender:VERSION'}<repositories> <repository> <id>jitpack.io</id> <url>https://jitpack.io</url> </repository></repositories>
<dependency> <groupId>com.github.senkex</groupId> <artifactId>HeadRender</artifactId> <version>VERSION</version></dependency>Shading
Section titled “Shading”Relocate com.github.senkex.headrender into your own namespace.
plugins { id("com.gradleup.shadow") version "8.3.5"}
tasks { shadowJar { relocate("com.github.senkex.headrender", "my.plugin.libs.headrender") }}<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.6.0</version> <configuration> <relocations> <relocation> <pattern>com.github.senkex.headrender</pattern> <shadedPattern>my.plugin.libs.headrender</shadedPattern> </relocation> </relocations> </configuration> <executions> <execution> <phase>package</phase> <goals><goal>shade</goal></goals> </execution> </executions></plugin>Quick start
Section titled “Quick start”HeadRender.render("Senkex").thenAccept(lines -> lines.forEach(player::sendMessage));
HeadRender.render(player.getUniqueId()).thenAccept(lines -> lines.forEach(player::sendMessage));RenderOptions
Section titled “RenderOptions”RenderOptions options = RenderOptions.builder() .size(10) .character("⬛") .helmetLayer(true) .alphaThreshold(20) .useCache(true) .build();
HeadRender.render("Senkex", options) .thenAccept(lines -> lines.forEach(player::sendMessage));| Option | Type | Default | Notes |
|---|---|---|---|
size | int | 8 | Output resolution per side. |
character | String | █ | Drawn for every opaque pixel. |
helmetLayer | boolean | true | Include the overlay (hat) layer. |
useCache | boolean | true | Read/write the shared cache. |
alphaThreshold | int | 10 | Pixels ≤ threshold render as a space. |
Shortcuts: RenderOptions.defaults(), RenderOptions.of(int size), options.toBuilder().
Facade reference
Section titled “Facade reference”| Method | Returns |
|---|---|
render(String / UUID, [RenderOptions]) | CompletableFuture<List<String>> |
service() | HeadRenderService |
use(HeadRenderService) | void |
cache() | SkinCache |
provider() | SkinProvider |
cacheSize() | int |
clearCache() | void |
shutdown() | void — call on plugin disable |
Custom service
Section titled “Custom service”HeadRender.use(DefaultHeadRenderService.builder() .provider(new MinotarSkinProvider(3000)) .cache(new InMemorySkinCache(512, TimeUnit.MINUTES.toMillis(30))) .executor(Executors.newFixedThreadPool(4)) .build());Custom SkinProvider
Section titled “Custom SkinProvider”package com.example.plugin.skin;
import com.github.senkex.headrender.api.SkinProvider;
import javax.imageio.ImageIO;import java.awt.image.BufferedImage;import java.io.IOException;import java.net.URL;
public final class MojangSkinProvider implements SkinProvider {
@Override public BufferedImage fetch(final String target, final int size, final boolean includeHelmet) throws IOException { try (var stream = new URL("https://example.com/skin.png").openStream()) { final BufferedImage image = ImageIO.read(stream); if (image == null) throw new IOException("Bad skin: " + target); return image; } }}Default: LRU InMemorySkinCache, 256 entries, 10 min TTL. Keyed by lowercase target + size + helmet flag.
HeadRender.cacheSize();HeadRender.clearCache();HeadRender.cache().invalidate("Senkex");
InMemorySkinCache cache = (InMemorySkinCache) HeadRender.cache();cache.setMaxSize(1024);cache.setTtl(1, TimeUnit.HOURS);For a clustered cache (Redis, disk-backed, shared across servers), implement SkinCache and inject through the builder.
Error handling
Section titled “Error handling”Failures are wrapped in HeadRenderException. Common causes: unknown player name, HTTP timeout from the upstream skin provider, or a decoding error.
HeadRender.render("UnknownPlayer123").whenComplete((lines, error) -> { if (error != null) { getLogger().warning(error.getMessage()); return; } lines.forEach(player::sendMessage);});Example: welcome banner
Section titled “Example: welcome banner”package com.example.plugin.listener;
import org.bukkit.Bukkit;import org.bukkit.entity.Player;import org.bukkit.event.EventHandler;import org.bukkit.event.Listener;import org.bukkit.event.player.PlayerJoinEvent;import org.bukkit.plugin.java.JavaPlugin;
import com.github.senkex.headrender.HeadRender;
import java.util.List;
public final class WelcomeListener implements Listener {
private final JavaPlugin plugin;
public WelcomeListener(final JavaPlugin plugin) { this.plugin = plugin; }
@EventHandler public void onJoin(final PlayerJoinEvent event) { final Player player = event.getPlayer(); HeadRender.render(player.getUniqueId()).thenAccept(lines -> Bukkit.getScheduler().runTask(plugin, () -> send(player, lines))); }
private void send(final Player player, final List<String> lines) { player.sendMessage("§7§m──────────────────────"); lines.forEach(player::sendMessage); player.sendMessage("§aWelcome back, §f" + player.getName() + "§a!"); player.sendMessage("§7§m──────────────────────"); }}Example: plugin bootstrap
Section titled “Example: plugin bootstrap”package com.example.plugin;
import org.bukkit.plugin.java.JavaPlugin;
import com.github.senkex.headrender.HeadRender;import com.github.senkex.headrender.cache.InMemorySkinCache;import com.github.senkex.headrender.provider.MinotarSkinProvider;import com.github.senkex.headrender.service.DefaultHeadRenderService;
import java.util.concurrent.Executors;import java.util.concurrent.TimeUnit;
public final class MyPlugin extends JavaPlugin {
@Override public void onEnable() { HeadRender.use(DefaultHeadRenderService.builder() .provider(new MinotarSkinProvider(3000)) .cache(new InMemorySkinCache(512, TimeUnit.MINUTES.toMillis(30))) .executor(Executors.newFixedThreadPool(4)) .build()); }
@Override public void onDisable() { HeadRender.shutdown(); }}