Add ONE of these dependencies to your ant/maven/gradle/bazel:
io.github.humbleui:skija-windows:${version}
io.github.humbleui:skija-linux:${version}
io.github.humbleui:skija-macos-x64:${version}
io.github.humbleui:skija-macos-arm64:${version}
In your main class, import
import io.github.humbleui.skija.*;
import java.io.IOException;
Most of the Skija value hides in Canvas class. Canvas let you actually draw.
Simplest way to obtain canvas is to create an in-memory, bitmap-backed one:
Surface surface = Surface.makeRasterN32Premul(100, 100);
Canvas canvas = surface.getCanvas();
To draw something, you often need a Paint. It stores fill color, stroke settings and various effects. We’ll use a default Paint with red fill color:
Paint paint = new Paint();
paint.setColor(0xFFFF0000);
The color is a 32-bit integer in ARGB format. 0xFF______
is fully opaque, 0x00______
is fully transparent. The colors are:
Red = 0xFFFF0000
Green = 0xFF00FF00
Blue = 0xFF0000FF
Finally, we can draw something:
canvas.drawCircle(50, 50, 30, paint);
Now the image only exists as a vector of bytes in memory. To extract it, we can ask Skija get us bitmap, encode it to PNG image format, and finally give us the bytes to write to file. Here:
Image image = surface.makeImageSnapshot();
Data pngData = image.encodeToData(EncodedImageFormat.PNG);
ByteBuffer pngBytes = pngData.toByteBuffer();
try
{
java.nio.file.Path path = java.nio.file.Path.of("output.png");
ByteChannel channel = Files.newByteChannel(
path,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
channel.write(pngBytes);
channel.close();
}
catch (IOException e)
{
System.out.println(e);
}
Voilà! You can now open output.png
from the working directory to check if it actually contains what you just drew.
Skija APIs are mostly mutable.
Most of the setters support chaining:
var paint = new Paint().setColor(0xFF1D7AA2).setMode(PaintMode.STROKE).setStrokeWidth(1f);
Most of Skija classes (those extending RefCnt or Managed) are backed by native pointers to C++ world of Skia. Don’t worry — Skija is smart enough to automatically manage everything for you. When java objects are collected, corresponding C++ structures are guaranteed to be freed as well. Programming in Java still feels like Java — safe by default.
void drawCircle(Canvas c) {
Paint p = new Paint();
c.drawCircle(0, 0, 10, p);
}
// Totally OK, `p` will be freed at the next GC
Only as an additional bonus, all Managed descendants implement AutoClosable. If you want (and only if you want!), you can free short-lived objects immediately after use. This is not mandatory, but might help freeing more memory quicker.
void drawCircle(Canvas c) {
try (Paint p = new Paint()) {
c.drawCircle(0, 0, 10, p);
} // `p` will be freed right here, cleaning up C++ resources. Can’t be used after!
}
You might notice that all fields in Skija are declared public. For example, Color4f:
@AllArgsConstructor
@Data
public class Color4f {
public final float _r;
public final float _g;
public final float _b;
public final float _a;
public Color4f(float r, float g, float b) {
this(r, g, b, 1f);
}
public Color4f(float[] rgba) {
this(rgba[0], rgba[1], rgba[2], rgba[3]);
}
}
This was done to work around Java’s very limited visibility control, and to give an escape hatch for very complex and rare needs, if those arise.
Please treat everything named with leading _
as private and use getters instead:
var color = new Color4f(1, 1, 1);
color._r; // bad: encapsulation breach. Don’t do this
color.getR(); // good: proper public API use
Skia can render to many backends. Skija currently support Bitmaps and OpenGL (Metal and Vulkan demos are coming).
To render to OpenGL, you need to create an OpenGL window. This part is not covered by Skija, but there are many ways and many examples online.
Using LWJGL library (also see examples/lwjgl):
var width = 640;
var height = 480;
// Create window
glfwInit();
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
long windowHandle = glfwCreateWindow(width, height, "Skija LWJGL Demo", NULL, NULL);
glfwMakeContextCurrent(windowHandle);
glfwSwapInterval(1); // Enable v-sync
glfwShowWindow(windowHandle);
// Initialize OpenGL
// Do once per app launch
GL.createCapabilities();
// Create Skia OpenGL context
// Do once per app launch
DirectContext context = DirectContext.makeGL();
// Create render target, surface and retrieve canvas from it
// .close() and recreate on window resize
int fbId = GL11.glGetInteger(0x8CA6); // GL_FRAMEBUFFER_BINDING
BackendRenderTarget renderTarget = BackendRenderTarget.makeGL(
width,
height,
/*samples*/ 0,
/*stencil*/ 8,
fbId,
FramebufferFormat.GR_GL_RGBA8);
// .close() and recreate on window resize
Surface surface = Surface.makeFromBackendRenderTarget(
context,
renderTarget,
SurfaceOrigin.BOTTOM_LEFT,
SurfaceColorFormat.RGBA_8888,
ColorSpace.getSRGB());
// do not .close() — Surface manages its lifetime here
Canvas canvas = surface.getCanvas();
// Render loop
while (!glfwWindowShouldClose(windowHandle)) {
// DRAW HERE!!!
context.flush();
glfwSwapBuffers(windowHandle); // wait for v-sync
glfwPollEvents();
}
Using JOGL library: see examples/jogl.
Embedding into AWT window: see Skiko.
For drawing text, there are two important concepts: Typeface and Font.
Typeface corresponds to a font file and is relatively expensive to create. You can create typeface directly:
Typeface face = Typeface.makeFromFile("Inter.ttf");
or ask operating system to locate one for you:
Typeface face = FontMgr.getDefault().matchFamilyStyle("Menlo", FontStyle.NORMAL);
The Font contains specific settings the Typeface should be drawn with. The most important one is size:
Font font = new Font(face, 13);
Finally, to specify color, we’ll need paint. All together:
try (Typeface face = FontMgr.getDefault().matchFamilyStyle("Menlo", FontStyle.NORMAL);
Font font = new Font(face, 13);
Paint fill = new Paint().setColor(0xFF000000);)
{
canvas.drawString("Hello, world", 0, 0, font, fill);
}
For this example, we close all resources immediately after paint. In a real application, you would want to cache both Typeface and Font, as it is expensive to recreate them on every frame.
For advanced font rendering, see Shaper and ParagraphBuilder.
I recommend studying these classes first:
I found SkiaSharp documentation to be excellent resource on what can be done in Skia. They have nice examples and visual explanations, too.
If Skija is missing a documentation for a particular method or class, check the same class in Skia Documentation instead. It might be that we didn’t move it over to Java yet. PRs are welcome!
Finally, LWJGL demo app has examples of most of the APIs that are currently implemented.
Skia is a fantastic library. JVM is a fantastic platform. Java is a fantastic language. Together, amazing things become possible.
We are very exctited to share Skija with you. And if you build something cool, please, let us know — we would be very happy to know what are you building.