Skip to content

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

MC1.16+ (HEX colors required)
Java17+
PlatformBukkit / Spigot / Paper (any fork)

Replace VERSION with the latest release.

build.gradle.kts
repositories {
maven("https://jitpack.io")
}
dependencies {
implementation("com.github.senkex:HeadRender:VERSION")
}
build.gradle
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
implementation 'com.github.senkex:HeadRender:VERSION'
}
pom.xml
<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>

Relocate com.github.senkex.headrender into your own namespace.

build.gradle.kts
plugins {
id("com.gradleup.shadow") version "8.3.5"
}
tasks {
shadowJar {
relocate("com.github.senkex.headrender", "my.plugin.libs.headrender")
}
}
pom.xml
<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>
HeadRender.render("Senkex").thenAccept(lines -> lines.forEach(player::sendMessage));
HeadRender.render(player.getUniqueId()).thenAccept(lines ->
lines.forEach(player::sendMessage));
RenderOptions options = RenderOptions.builder()
.size(10)
.character("")
.helmetLayer(true)
.alphaThreshold(20)
.useCache(true)
.build();
HeadRender.render("Senkex", options)
.thenAccept(lines -> lines.forEach(player::sendMessage));
OptionTypeDefaultNotes
sizeint8Output resolution per side.
characterStringDrawn for every opaque pixel.
helmetLayerbooleantrueInclude the overlay (hat) layer.
useCachebooleantrueRead/write the shared cache.
alphaThresholdint10Pixels ≤ threshold render as a space.

Shortcuts: RenderOptions.defaults(), RenderOptions.of(int size), options.toBuilder().

MethodReturns
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
HeadRender.use(DefaultHeadRenderService.builder()
.provider(new MinotarSkinProvider(3000))
.cache(new InMemorySkinCache(512, TimeUnit.MINUTES.toMillis(30)))
.executor(Executors.newFixedThreadPool(4))
.build());
MojangSkinProvider.java
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.

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);
});
WelcomeListener.java
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──────────────────────");
}
}
MyPlugin.java
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();
}
}
Last updated: May 23, 2026 by Senkex in cfd9d75