Skip to content

Instantly share code, notes, and snippets.

@WinterAlexander
Last active April 24, 2024 02:45
Show Gist options
  • Select an option

  • Save WinterAlexander/5ec4bc65de744fdd90354013e5c0c0d7 to your computer and use it in GitHub Desktop.

Select an option

Save WinterAlexander/5ec4bc65de744fdd90354013e5c0c0d7 to your computer and use it in GitHub Desktop.
NoBleedingFreeTypeFontGenerator
package com.greenfrisbee.makerking.render.font;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.PixmapPacker;
import com.badlogic.gdx.graphics.g2d.freetype.FreeType;
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.Null;
import com.winteralexander.gdx.utils.ReflectionUtil;
import java.nio.ByteBuffer;
/**
* {@link FreeTypeFontGenerator} that fixes bleeding for fonts with shadows
* <p>
* Created on 2024-04-23.
*
* @author Alexander Winter
*/
public class NoBleedingFreeTypeFontGenerator extends FreeTypeFontGenerator {
private final FreeType.Face privateFace;
private final boolean privateBitmapped;
public NoBleedingFreeTypeFontGenerator(FileHandle fontFile) {
super(fontFile);
privateFace = ReflectionUtil.get(this, "face");
privateBitmapped = ReflectionUtil.get(this, "bitmapped");
}
public NoBleedingFreeTypeFontGenerator(FileHandle fontFile, int faceIndex) {
super(fontFile, faceIndex);
privateFace = ReflectionUtil.get(this, "face");
privateBitmapped = ReflectionUtil.get(this, "bitmapped");
}
@Override
protected @Null BitmapFont.Glyph createGlyph(char c,
FreeTypeBitmapFontData data,
FreeTypeFontParameter parameter,
FreeType.Stroker stroker,
float baseLine,
PixmapPacker packer) {
boolean missing = privateFace.getCharIndex(c) == 0 && c != 0;
if(missing)
return null;
if(!privateFace.loadChar(c, getLoadingFlags(parameter))) return null;
FreeType.GlyphSlot slot = privateFace.getGlyph();
FreeType.Glyph mainGlyph = slot.getGlyph();
try {
mainGlyph.toBitmap(parameter.mono ? FreeType.FT_RENDER_MODE_MONO : FreeType.FT_RENDER_MODE_NORMAL);
} catch (GdxRuntimeException e) {
mainGlyph.dispose();
Gdx.app.log("FreeTypeFontGenerator", "Couldn't render char: " + c);
return null;
}
FreeType.Bitmap mainBitmap = mainGlyph.getBitmap();
Pixmap mainPixmap = mainBitmap.getPixmap(Pixmap.Format.RGBA8888, parameter.color, parameter.gamma);
if (mainBitmap.getWidth() != 0 && mainBitmap.getRows() != 0) {
int offsetX = 0, offsetY = 0;
if (parameter.borderWidth > 0) {
// execute stroker; this generates a glyph "extended" along the outline
int top = mainGlyph.getTop(), left = mainGlyph.getLeft();
FreeType.Glyph borderGlyph = slot.getGlyph();
borderGlyph.strokeBorder(stroker, false);
borderGlyph.toBitmap(parameter.mono ? FreeType.FT_RENDER_MODE_MONO : FreeType.FT_RENDER_MODE_NORMAL);
offsetX = left - borderGlyph.getLeft();
offsetY = -(top - borderGlyph.getTop());
// Render border (pixmap is bigger than main).
FreeType.Bitmap borderBitmap = borderGlyph.getBitmap();
Pixmap borderPixmap = borderBitmap.getPixmap(Pixmap.Format.RGBA8888, parameter.borderColor, parameter.borderGamma);
// Draw main glyph on top of border.
for (int i = 0, n = parameter.renderCount; i < n; i++)
borderPixmap.drawPixmap(mainPixmap, offsetX, offsetY);
mainPixmap.dispose();
mainGlyph.dispose();
mainPixmap = borderPixmap;
mainGlyph = borderGlyph;
}
if (parameter.shadowOffsetX != 0 || parameter.shadowOffsetY != 0) {
int mainW = mainPixmap.getWidth(), mainH = mainPixmap.getHeight();
int shadowOffsetX = Math.max(parameter.shadowOffsetX, 0), shadowOffsetY = Math.max(parameter.shadowOffsetY, 0);
int shadowW = mainW + Math.abs(parameter.shadowOffsetX), shadowH = mainH + Math.abs(parameter.shadowOffsetY);
Pixmap shadowPixmap = new Pixmap(shadowW, shadowH, mainPixmap.getFormat());
shadowPixmap.setColor(packer.getTransparentColor());
shadowPixmap.fill();
Color shadowColor = parameter.shadowColor;
float a = shadowColor.a;
if (a != 0) {
byte r = (byte)(shadowColor.r * 255), g = (byte)(shadowColor.g * 255), b = (byte)(shadowColor.b * 255);
ByteBuffer mainPixels = mainPixmap.getPixels();
ByteBuffer shadowPixels = shadowPixmap.getPixels();
for (int y = 0; y < mainH; y++) {
int shadowRow = shadowW * (y + shadowOffsetY) + shadowOffsetX;
for (int x = 0; x < mainW; x++) {
int mainPixel = (mainW * y + x) * 4;
byte mainA = mainPixels.get(mainPixel + 3);
if (mainA == 0) {
continue;
}
int shadowPixel = (shadowRow + x) * 4;
shadowPixels.put(shadowPixel, r);
shadowPixels.put(shadowPixel + 1, g);
shadowPixels.put(shadowPixel + 2, b);
shadowPixels.put(shadowPixel + 3, (byte)((mainA & 0xff) * a));
}
}
}
// Draw main glyph (with any border) on top of shadow.
for (int i = 0, n = parameter.renderCount; i < n; i++)
shadowPixmap.drawPixmap(mainPixmap, Math.max(-parameter.shadowOffsetX, 0), Math.max(-parameter.shadowOffsetY, 0));
mainPixmap.dispose();
mainPixmap = shadowPixmap;
} else if (parameter.borderWidth == 0) {
// No shadow and no border, draw glyph additional times.
for (int i = 0, n = parameter.renderCount - 1; i < n; i++)
mainPixmap.drawPixmap(mainPixmap, 0, 0);
}
if (parameter.padTop > 0 || parameter.padLeft > 0 || parameter.padBottom > 0 || parameter.padRight > 0) {
Pixmap padPixmap = new Pixmap(mainPixmap.getWidth() + parameter.padLeft + parameter.padRight,
mainPixmap.getHeight() + parameter.padTop + parameter.padBottom, mainPixmap.getFormat());
padPixmap.setBlending(Pixmap.Blending.None);
padPixmap.drawPixmap(mainPixmap, parameter.padLeft, parameter.padTop);
mainPixmap.dispose();
mainPixmap = padPixmap;
}
}
FreeType.GlyphMetrics metrics = slot.getMetrics();
BitmapFont.Glyph glyph = new BitmapFont.Glyph();
glyph.id = c;
glyph.width = mainPixmap.getWidth();
glyph.height = mainPixmap.getHeight();
glyph.xoffset = mainGlyph.getLeft();
if (parameter.flip)
glyph.yoffset = -mainGlyph.getTop() + (int)baseLine;
else
glyph.yoffset = -(glyph.height - mainGlyph.getTop()) - (int)baseLine;
glyph.xadvance = FreeType.toInt(metrics.getHoriAdvance()) + (int)parameter.borderWidth + parameter.spaceX;
if (privateBitmapped) {
mainPixmap.setColor(Color.CLEAR);
mainPixmap.fill();
ByteBuffer buf = mainBitmap.getBuffer();
int whiteIntBits = Color.WHITE.toIntBits();
int clearIntBits = Color.CLEAR.toIntBits();
for (int h = 0; h < glyph.height; h++) {
int idx = h * mainBitmap.getPitch();
for (int w = 0; w < (glyph.width + glyph.xoffset); w++) {
int bit = (buf.get(idx + (w / 8)) >>> (7 - (w % 8))) & 1;
mainPixmap.drawPixel(w, h, ((bit == 1) ? whiteIntBits : clearIntBits));
}
}
}
Rectangle rect = packer.pack(mainPixmap);
glyph.page = packer.getPages().size - 1; // Glyph is always packed into the last page for now.
glyph.srcX = (int)rect.x;
glyph.srcY = (int)rect.y;
// If a page was added, create a new texture region for the incrementally added glyph.
if (parameter.incremental && data.regions != null && data.regions.size <= glyph.page)
packer.updateTextureRegions(data.regions, parameter.minFilter, parameter.magFilter, parameter.genMipMaps);
mainPixmap.dispose();
mainGlyph.dispose();
return glyph;
}
private int getLoadingFlags (FreeTypeFontParameter parameter) {
int loadingFlags = FreeType.FT_LOAD_DEFAULT;
switch (parameter.hinting) {
case None:
loadingFlags |= FreeType.FT_LOAD_NO_HINTING;
break;
case Slight:
loadingFlags |= FreeType.FT_LOAD_TARGET_LIGHT;
break;
case Medium:
loadingFlags |= FreeType.FT_LOAD_TARGET_NORMAL;
break;
case Full:
loadingFlags |= FreeType.FT_LOAD_TARGET_MONO;
break;
case AutoSlight:
loadingFlags |= FreeType.FT_LOAD_FORCE_AUTOHINT | FreeType.FT_LOAD_TARGET_LIGHT;
break;
case AutoMedium:
loadingFlags |= FreeType.FT_LOAD_FORCE_AUTOHINT | FreeType.FT_LOAD_TARGET_NORMAL;
break;
case AutoFull:
loadingFlags |= FreeType.FT_LOAD_FORCE_AUTOHINT | FreeType.FT_LOAD_TARGET_MONO;
break;
}
return loadingFlags;
}
}
package com.greenfrisbee.makerking.render.font;
import com.badlogic.gdx.assets.AssetDescriptor;
import com.badlogic.gdx.assets.AssetLoaderParameters;
import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.assets.loaders.FileHandleResolver;
import com.badlogic.gdx.assets.loaders.SynchronousAssetLoader;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
import com.badlogic.gdx.utils.Array;
import static com.greenfrisbee.makerking.render.font.NoBleedingFreeTypeFontGeneratorLoader.*;
/**
* Loads {@link NoBleedingFreeTypeFontGenerator} from TTF files
* <p>
* Created on 2024-04-23.
*
* @author Alexander Winter
*/
public class NoBleedingFreeTypeFontGeneratorLoader
extends SynchronousAssetLoader<FreeTypeFontGenerator, FreeTypeFontGeneratorParameters> {
public NoBleedingFreeTypeFontGeneratorLoader (FileHandleResolver resolver) {
super(resolver);
}
@Override
public FreeTypeFontGenerator load(AssetManager assetManager,
String fileName,
FileHandle file,
FreeTypeFontGeneratorParameters parameter) {
if (file.extension().equals("gen"))
return new NoBleedingFreeTypeFontGenerator(file.sibling(file.nameWithoutExtension()));
else
return new NoBleedingFreeTypeFontGenerator(file);
}
@SuppressWarnings("rawtypes")
@Override
public Array<AssetDescriptor> getDependencies(String fileName,
FileHandle file,
FreeTypeFontGeneratorParameters parameter) {
return null;
}
static public class FreeTypeFontGeneratorParameters extends AssetLoaderParameters<FreeTypeFontGenerator> {
}
}
package com.winteralexander.gdx.utils;
import com.badlogic.gdx.utils.Array;
import java.lang.reflect.*;
import static com.winteralexander.gdx.utils.TypeUtil.isPrimitiveBox;
/**
* Utility class to use reflection on objects
* <p>
* Created on 2018-02-09.
*
* @author Alexander Winter
*/
@SuppressWarnings("deprecation")
public class ReflectionUtil {
private ReflectionUtil() {}
/**
* Swap all fields value for 2 objects
*
* @param o1 object 1
* @param o2 object 2
*/
public static void swapFields(Object o1, Object o2) {
if(o1 == null || o2 == null)
throw new IllegalArgumentException("Objects must not be null");
if(!o1.getClass().equals(o2.getClass()))
throw new IllegalArgumentException("Objects are not the same type");
Class<?> type = o1.getClass();
while(type != null) {
for(Field field : type.getDeclaredFields()) {
if(Modifier.isStatic(field.getModifiers()))
continue;
if(!field.isAccessible())
field.setAccessible(true);
try {
Object tmp = field.get(o1);
field.set(o1, field.get(o2));
field.set(o2, tmp);
} catch(IllegalAccessException ex) {
throw new RuntimeException(ex);
}
}
type = type.getSuperclass();
}
}
/**
* Clones an object into another
*
* @param source source object
* @param destination destination object
*/
public static void copy(Object source, Object destination) {
Validation.ensureNotNull(source, "source");
Validation.ensureNotNull(destination, "destination");
if(!source.getClass().equals(destination.getClass()))
throw new IllegalArgumentException("Objects are not the same type");
Class<?> type = source.getClass();
while(type != null) {
for(Field field : type.getDeclaredFields()) {
if(Modifier.isStatic(field.getModifiers()))
continue;
if(!field.isAccessible())
field.setAccessible(true);
try {
field.set(destination, field.get(source));
} catch(IllegalAccessException ex) {
throw new RuntimeException(ex);
}
}
type = type.getSuperclass();
}
}
/**
* Sets the field of an object to a specified value
*
* @param object object to edit field's of
* @param field field to edit
* @param value value to set
*/
public static void set(Object object, String field, Object value) {
Validation.ensureNotNull(object, "object");
set(object.getClass(), object, field, value);
}
/**
* Sets the field of an object to a specified value
*
* @param type type of object to set value of
* @param object object to edit field's of, or null if static field
* @param field field to edit
* @param value value to set
*/
public static void set(Class<?> type, Object object, String field, Object value) {
Validation.ensureNotNull(type, "type");
Validation.ensureNotNull(field, "field");
Validation.ensureNotNull(value, "value");
while(type != null) {
try {
Field fieldHandle = type.getDeclaredField(field);
fieldHandle.setAccessible(true);
fieldHandle.set(object, value);
return;
} catch(IllegalAccessException ex) {
throw new RuntimeException(ex);
} catch(NoSuchFieldException ignored) {}
type = type.getSuperclass();
}
throw new RuntimeException("Field not found");
}
@SuppressWarnings("unchecked")
public static <T> T getStatic(Class<?> type, String field) {
return (T)getStatic(type, field, Object.class);
}
public static <T> T getStatic(Class<?> type, String field, Class<T> returnType) {
Validation.ensureNotNull(type, "type");
Validation.ensureNotNull(field, "field");
Validation.ensureNotNull(returnType, "returnType");
while(type != null) {
try {
Field fieldHandle = type.getDeclaredField(field);
if(!fieldHandle.isAccessible())
fieldHandle.setAccessible(true);
return returnType.cast(fieldHandle.get(null));
} catch(IllegalAccessException ex) {
throw new RuntimeException(ex);
} catch(NoSuchFieldException ignored) {}
type = type.getSuperclass();
}
throw new RuntimeException("Field " + field + " not found for type " + type);
}
@SuppressWarnings("unchecked")
public static <T> T get(Object object, String field) {
return (T)get(object, field, Object.class);
}
/**
* Gets the field of an object for a specified field name
*
* @param object object to edit field's of
* @param field field to edit
* @param type type of field
*/
public static <T> T get(Object object, String field, Class<T> type) {
Validation.ensureNotNull(object, "object");
Validation.ensureNotNull(field, "field");
Validation.ensureNotNull(type, "type");
Class<?> t = object.getClass();
while(t != null) {
try {
Field fieldHandle = t.getDeclaredField(field);
if(!fieldHandle.isAccessible())
fieldHandle.setAccessible(true);
return type.cast(fieldHandle.get(object));
} catch(IllegalAccessException ex) {
throw new RuntimeException(ex);
} catch(NoSuchFieldException ignored) {}
t = t.getSuperclass();
}
throw new RuntimeException("Field " + field + " not found for type " + object.getClass());
}
public static boolean has(Class<?> type, String field) {
Validation.ensureNotNull(type, "type");
Validation.ensureNotNull(field, "field");
Class<?> t = type;
while(t != null) {
try {
t.getDeclaredField(field);
return true;
} catch(NoSuchFieldException ignored) {}
t = t.getSuperclass();
}
return false;
}
public static Class<?> getType(Class<?> type, String field) {
Validation.ensureNotNull(type, "type");
Validation.ensureNotNull(field, "field");
Class<?> t = type;
while(t != null) {
try {
return t.getDeclaredField(field).getType();
} catch(NoSuchFieldException ignored) {}
t = t.getSuperclass();
}
throw new RuntimeException("Field " + field + " not found for type " + type);
}
@SuppressWarnings({"unchecked", "StringEquality"})
public static <T> T callStatic(Class<?> type, String method, Object... params) {
Validation.ensureNotNull(method, "method");
method = method.intern();
while(type != null) {
try {
for(Method methodHandle : type.getDeclaredMethods()) {
if(methodHandle.getName() != method)
continue;
if(methodHandle.getParameterCount() != params.length)
continue;
if(!methodHandle.isAccessible())
methodHandle.setAccessible(true);
try {
return (T)methodHandle.invoke(null, params);
} catch(IllegalArgumentException ignored) {}
}
} catch(IllegalAccessException | InvocationTargetException ex) {
throw new RuntimeException(ex);
}
type = type.getSuperclass();
}
throw new RuntimeException("Method " + method + " not found for type " + type);
}
@SuppressWarnings({"unchecked", "StringEquality"})
public static <T> T call(Object object, String method, Object... params) {
Validation.ensureNotNull(object, "object");
Validation.ensureNotNull(method, "method");
method = method.intern();
Class<?> t = object.getClass();
while(t != null) {
try {
for(Method methodHandle : t.getDeclaredMethods()) {
if(methodHandle.getName() != method)
continue;
if(methodHandle.getParameterCount() != params.length)
continue;
if(!methodHandle.isAccessible())
methodHandle.setAccessible(true);
try {
return (T)methodHandle.invoke(object, params);
} catch(IllegalArgumentException ignored) {}
}
} catch(IllegalAccessException | InvocationTargetException ex) {
throw new RuntimeException(ex);
}
t = t.getSuperclass();
}
throw new RuntimeException("Method " + method + " not found for type " + object.getClass());
}
@SuppressWarnings("unchecked")
public static <T> T construct(Class<T> type, Object... params) {
Validation.ensureNotNull(type, "type");
for(Constructor<?> constructor : type.getDeclaredConstructors()) {
try {
if(!constructor.isAccessible())
constructor.setAccessible(true);
return (T)constructor.newInstance(params);
} catch(InstantiationException | IllegalArgumentException | IllegalAccessException ignored) {
// continue
} catch(InvocationTargetException ex) {
throw new RuntimeException(ex);
}
}
throw new RuntimeException("No matching constructor found");
}
public static String toPrettyString(Object object) {
return toPrettyString(object, Integer.MAX_VALUE, 0, new Array<>());
}
public static String toPrettyString(Object object,
int maxDepth,
int indentationLevel) {
return toPrettyString(object, maxDepth, indentationLevel, new Array<>());
}
private static String toPrettyString(Object object,
int maxDepth,
int indentationLevel,
Array<Object> objects) {
objects.add(object);
if(maxDepth <= 0)
return object.toString() + '\n';
StringBuilder sb = new StringBuilder();
String newLine = System.lineSeparator();
Class<?> type = object.getClass();
sb.append(type.getSimpleName())
.append('@')
.append(Integer.toHexString(object.hashCode()))
.append(newLine);
for(int i = 0; i < indentationLevel; i++)
sb.append('\t');
if(type.isArray()) {
if(!Object[].class.isAssignableFrom(type)) {
int length = java.lang.reflect.Array.getLength(object);
Object[] objArr = new Object[length];
for(int i = 0; i < length; i++)
objArr[i] = java.lang.reflect.Array.get(object, i);
object = objArr;
}
sb.append('[').append(newLine);
int n = 0;
for(Object obj : (Object[])object) {
for(int i = 0; i < indentationLevel + 1; i++)
sb.append('\t');
sb.append(n).append(": ");
try {
if(obj == null)
sb.append("null").append(newLine);
else if(obj.getClass().isAssignableFrom(String.class))
sb.append("\"").append(obj).append("\"").append(newLine);
else if(obj.getClass().isPrimitive()
|| obj.getClass().isEnum()
|| isPrimitiveBox(obj.getClass()))
sb.append(obj).append(newLine);
else if(objects.contains(obj, true))
sb.append(obj.getClass().getSimpleName())
.append('@')
.append(Integer.toHexString(obj.hashCode()))
.append(newLine);
else
sb.append(toPrettyString(obj, maxDepth - 1, indentationLevel + 1, objects));
} catch(Throwable ex) {
sb.append('(')
.append(ex.getClass().getSimpleName())
.append(')')
.append(newLine);
}
n++;
}
for(int i = 0; i < indentationLevel; i++)
sb.append('\t');
sb.append(']').append(newLine);
} else {
sb.append('{').append(newLine);
while(type != null) {
for(Field field : type.getDeclaredFields()) {
if(Modifier.isStatic(field.getModifiers()))
continue;
for(int i = 0; i < indentationLevel + 1; i++)
sb.append('\t');
if(!field.isAccessible())
field.setAccessible(true);
try {
sb.append(field.getName()).append(": ");
Object obj = field.get(object);
if(obj == null)
sb.append("null").append(newLine);
else if(String.class.isAssignableFrom(field.getType()))
sb.append("\"").append(obj).append("\"").append(newLine);
else if(field.getType().isPrimitive() || field.getType().isEnum())
sb.append(obj).append(newLine);
else if(objects.contains(obj, true))
sb.append(obj.getClass().getSimpleName())
.append('@')
.append(Integer.toHexString(obj.hashCode()))
.append(newLine);
else {
sb.append(toPrettyString(obj, maxDepth - 1, indentationLevel + 1, objects));
}
} catch(Throwable ex) {
sb.append("(").append(ex.getClass().getSimpleName()).append(")").append(newLine);
ex.printStackTrace();
}
}
type = type.getSuperclass();
}
for(int i = 0; i < indentationLevel; i++)
sb.append('\t');
sb.append('}').append(newLine);
}
return sb.toString();
}
@SuppressWarnings("raw")
public static void disableAccessWarnings() {
try {
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Object unsafe = field.get(null);
Method putObjectVolatile = unsafeClass.getDeclaredMethod("putObjectVolatile",
Object.class, long.class, Object.class);
Method staticFieldOffset = unsafeClass.getDeclaredMethod("staticFieldOffset",
Field.class);
Class<?> loggerClass = Class.forName("jdk.internal.module.IllegalAccessLogger");
Field loggerField = loggerClass.getDeclaredField("logger");
Long offset = (Long)staticFieldOffset.invoke(unsafe, loggerField);
putObjectVolatile.invoke(unsafe, loggerClass, offset, null);
} catch(Exception ignored) {}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment