mirror of
https://git.huckle.dev/Huckles-Minecraft-Archive/jpexs-decompiler.git
synced 2026-09-27 03:00:51 +00:00
better font name handling
kerning moved to FontHelper
This commit is contained in:
@@ -17,12 +17,26 @@
|
||||
package com.jpexs.decompiler.flash.helpers;
|
||||
|
||||
import java.awt.Font;
|
||||
import java.awt.FontFormatException;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.awt.font.FontRenderContext;
|
||||
import java.awt.font.GlyphVector;
|
||||
import java.awt.font.TextAttribute;
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.text.AttributedCharacterIterator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import javax.swing.JPanel;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -30,6 +44,11 @@ import java.util.Map;
|
||||
*/
|
||||
public class FontHelper {
|
||||
|
||||
private static Object getFontManager() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
|
||||
Class<?> clFmFactory = Class.forName("sun.font.FontManagerFactory");
|
||||
return clFmFactory.getDeclaredMethod("getInstance").invoke(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all available fonts in the system
|
||||
*
|
||||
@@ -40,24 +59,27 @@ public class FontHelper {
|
||||
Font fonts[] = null;
|
||||
|
||||
try {
|
||||
Class<?> clFmFactory = Class.forName("sun.font.FontManagerFactory");
|
||||
Object fm = clFmFactory.getDeclaredMethod("getInstance").invoke(null);
|
||||
|
||||
Object fm = getFontManager();
|
||||
Class<?> clFm = Class.forName("sun.font.SunFontManager");
|
||||
|
||||
//Delete cached installed names
|
||||
Field inField = clFm.getDeclaredField("installedNames");
|
||||
inField.setAccessible(true);
|
||||
inField.set(null, null);
|
||||
inField.setAccessible(false);
|
||||
|
||||
//Delete cached family names
|
||||
Field allFamField = clFm.getDeclaredField("allFamilies");
|
||||
allFamField.setAccessible(true);
|
||||
allFamField.set(fm, null);
|
||||
allFamField.setAccessible(false);
|
||||
|
||||
//Delete cached fonts
|
||||
Field allFonField = clFm.getDeclaredField("allFonts");
|
||||
allFonField.setAccessible(true);
|
||||
allFonField.set(fm, null);
|
||||
allFonField.setAccessible(false);
|
||||
|
||||
fonts = (Font[]) clFm.getDeclaredMethod("getAllInstalledFonts").invoke(fm);
|
||||
} catch (Throwable ex) {
|
||||
@@ -67,7 +89,7 @@ public class FontHelper {
|
||||
fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts();
|
||||
}
|
||||
for (Font f : fonts) {
|
||||
String fam = f.getFamily(Locale.getDefault());
|
||||
String fam = f.getFamily(Locale.ENGLISH);
|
||||
//Do not want Java logical fonts
|
||||
if (Arrays.asList("Dialog", "DialogInput", "Monospaced", "Serif", "SansSerif").contains(fam)) {
|
||||
continue;
|
||||
@@ -75,21 +97,327 @@ public class FontHelper {
|
||||
if (!ret.containsKey(fam)) {
|
||||
ret.put(fam, new HashMap<String, Font>());
|
||||
}
|
||||
String face = getFontFace(f);
|
||||
ret.get(f.getFamily()).put(face, f);
|
||||
|
||||
ret.get(fam).put(f.getFontName(Locale.ENGLISH), f);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static String getFontFace(Font f) {
|
||||
String fam = f.getFamily(Locale.getDefault());
|
||||
String face = f.getFontName(Locale.getDefault());
|
||||
if (face.startsWith(fam)) {
|
||||
face = face.substring(fam.length()).trim();
|
||||
/**
|
||||
* Gets kerning offset for two characters of the font
|
||||
*
|
||||
* @param font Font
|
||||
* @param char1 First character
|
||||
* @param char2 Second character
|
||||
* @return offset
|
||||
*/
|
||||
public static int getFontCharsKerning(Font font, char char1, char char2) {
|
||||
char[] chars = new char[]{char1, char2};
|
||||
Map<AttributedCharacterIterator.Attribute, Object> withKerningAttrs = new HashMap<>();
|
||||
|
||||
withKerningAttrs.put(TextAttribute.FONT, font);
|
||||
withKerningAttrs.put(TextAttribute.KERNING, TextAttribute.KERNING_ON);
|
||||
Font withKerningFont = Font.getFont(withKerningAttrs);
|
||||
GlyphVector withKerningVector = withKerningFont.layoutGlyphVector((new JPanel()).getFontMetrics(withKerningFont).getFontRenderContext(), chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);
|
||||
int withKerningX = withKerningVector.getGlyphLogicalBounds(1).getBounds().x;
|
||||
|
||||
Map<AttributedCharacterIterator.Attribute, Object> noKerningAttrs = new HashMap<>();
|
||||
noKerningAttrs.put(TextAttribute.FONT, font);
|
||||
noKerningAttrs.put(TextAttribute.KERNING, 0);
|
||||
Font noKerningFont = Font.getFont(noKerningAttrs);
|
||||
GlyphVector noKerningVector = noKerningFont.layoutGlyphVector((new JPanel()).getFontMetrics(noKerningFont).getFontRenderContext(), chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);
|
||||
int noKerningX = noKerningVector.getGlyphLogicalBounds(1).getBounds().x;
|
||||
return withKerningX - noKerningX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all kerning pairs of a Font. It is very slow.
|
||||
*
|
||||
* @param font
|
||||
* @param size
|
||||
* @return
|
||||
*/
|
||||
public static List<KerningPair> getFontKerningPairs(Font font, int size) {
|
||||
File fontFile = getFontFile(font);
|
||||
if (fontFile != null && fontFile.getName().toLowerCase().endsWith(".ttf")) {
|
||||
KerningLoader k = new KerningLoader();
|
||||
try {
|
||||
return k.loadFromTTF(fontFile, size);
|
||||
} catch (IOException | FontFormatException ex) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
if (face.startsWith(".")) {
|
||||
face = face.substring(1);
|
||||
List<KerningPair> ret = new ArrayList<>();
|
||||
|
||||
List<Character> availableChars = new ArrayList<>();
|
||||
for (char c1 = 0; c1 < Character.MAX_VALUE; c1++) {
|
||||
if (font.canDisplay((int) c1)) {
|
||||
availableChars.add(c1);
|
||||
}
|
||||
}
|
||||
for (char c1 : availableChars) {
|
||||
ret.addAll(getFontKerningPairsOneChar(availableChars, font, c1));
|
||||
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static List<KerningPair> getFontKerningPairsOneChar(List<Character> availableChars, Font font, char firstChar) {
|
||||
List<KerningPair> ret = new ArrayList<>();
|
||||
|
||||
char[] chars = new char[availableChars.size() * 2];
|
||||
|
||||
for (int i = 0; i < availableChars.size(); i++) {
|
||||
chars[i * 2] = firstChar;
|
||||
chars[i * 2 + 1] = availableChars.get(i);
|
||||
}
|
||||
|
||||
Map<AttributedCharacterIterator.Attribute, Object> withKerningAttrs = new HashMap<>();
|
||||
|
||||
withKerningAttrs.put(TextAttribute.FONT, font);
|
||||
withKerningAttrs.put(TextAttribute.KERNING, TextAttribute.KERNING_ON);
|
||||
Font withKerningFont = Font.getFont(withKerningAttrs);
|
||||
GlyphVector withKerningVector = withKerningFont.layoutGlyphVector((new JPanel()).getFontMetrics(withKerningFont).getFontRenderContext(), chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);
|
||||
int withKerningX[] = new int[availableChars.size()];
|
||||
for (int i = 0; i < availableChars.size(); i++) {
|
||||
withKerningX[i] = withKerningVector.getGlyphLogicalBounds(i * 2 + 1).getBounds().x;
|
||||
}
|
||||
|
||||
Map<AttributedCharacterIterator.Attribute, Object> noKerningAttrs = new HashMap<>();
|
||||
noKerningAttrs.put(TextAttribute.FONT, font);
|
||||
noKerningAttrs.put(TextAttribute.KERNING, 0);
|
||||
Font noKerningFont = Font.getFont(noKerningAttrs);
|
||||
GlyphVector noKerningVector = noKerningFont.layoutGlyphVector((new JPanel()).getFontMetrics(noKerningFont).getFontRenderContext(), chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);
|
||||
for (int i = 0; i < availableChars.size(); i++) {
|
||||
int noKerningX = noKerningVector.getGlyphLogicalBounds(i * 2 + 1).getBounds().x;
|
||||
int kerning = withKerningX[i] - noKerningX;
|
||||
if (kerning > 0) {
|
||||
ret.add(new KerningPair(firstChar, availableChars.get(i), kerning));
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static class KerningPair {
|
||||
|
||||
public char char1;
|
||||
public char char2;
|
||||
public int kerning;
|
||||
|
||||
public KerningPair(char char1, char char2, int kerning) {
|
||||
this.char1 = char1;
|
||||
this.char2 = char2;
|
||||
this.kerning = kerning;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = 3;
|
||||
hash = 67 * hash + this.char1;
|
||||
hash = 67 * hash + this.char2;
|
||||
hash = 67 * hash + this.kerning;
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
final KerningPair other = (KerningPair) obj;
|
||||
if (this.char1 != other.char1) {
|
||||
return false;
|
||||
}
|
||||
if (this.char2 != other.char2) {
|
||||
return false;
|
||||
}
|
||||
if (this.kerning != other.kerning) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "'" + char1 + "','" + char2 + "' => " + kerning;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static Object getFont2d(Font f) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
|
||||
Object fm = getFontManager();
|
||||
return Class.forName("sun.font.FontManager").getDeclaredMethod("findFont2D", String.class, int.class, int.class).invoke(fm, f.getFontName(), f.getStyle(), 2/*LOGICAL_FALLBACK*/);
|
||||
}
|
||||
|
||||
public static File getFontFile(Font f) {
|
||||
try {
|
||||
Class pfClass = Class.forName("sun.font.PhysicalFont");
|
||||
Field platName = pfClass.getDeclaredField("platName");
|
||||
platName.setAccessible(true);
|
||||
String fontPath = (String) platName.get(getFont2d(f));
|
||||
platName.setAccessible(false);
|
||||
return new File(fontPath);
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<Integer, Character> getFontGlyphToCharMap(Font f) {
|
||||
Map<Integer, Character> ret = new HashMap<>();
|
||||
FontRenderContext frc = new FontRenderContext(null, true, false);
|
||||
for (char i = 0; i < Character.MAX_VALUE; i++) {
|
||||
if (f.canDisplay(i)) {
|
||||
GlyphVector gv = f.createGlyphVector(frc, new char[]{i});
|
||||
ret.put(gv.getGlyphCode(0), i);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static class KerningLoader {
|
||||
|
||||
private int size = -1;
|
||||
private float scale;
|
||||
private long bytePosition;
|
||||
private long headOffset = -1;
|
||||
private long kernOffset = -1;
|
||||
private Font font;
|
||||
private Map<Integer, Character> charmap;
|
||||
|
||||
public List<KerningPair> loadFromTTF(File file, int size) throws IOException, FontFormatException {
|
||||
font = Font.createFont(Font.TRUETYPE_FONT, file);
|
||||
charmap = getFontGlyphToCharMap(font);
|
||||
InputStream input = new FileInputStream(file);
|
||||
List<KerningPair> ret = new ArrayList<>();
|
||||
this.size = size;
|
||||
if (input == null) {
|
||||
throw new IllegalArgumentException("input cannot be null.");
|
||||
}
|
||||
readTableDirectory(input);
|
||||
if (headOffset == -1) {
|
||||
throw new IOException("HEAD table not found.");
|
||||
}
|
||||
if (kernOffset == -1) {
|
||||
return ret;
|
||||
}
|
||||
if (headOffset < kernOffset) {
|
||||
readHEAD(input);
|
||||
readKERN(input, ret);
|
||||
} else {
|
||||
readKERN(input, ret);
|
||||
readHEAD(input);
|
||||
}
|
||||
input.close();
|
||||
for (KerningPair kp : ret) {
|
||||
kp.kerning *= scale;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private void readTableDirectory(InputStream input) throws IOException {
|
||||
skip(input, 4);
|
||||
int tableCount = readUnsignedShort(input);
|
||||
skip(input, 6);
|
||||
|
||||
byte[] tagBytes = new byte[4];
|
||||
for (int i = 0; i < tableCount; i++) {
|
||||
tagBytes[0] = readByte(input);
|
||||
tagBytes[1] = readByte(input);
|
||||
tagBytes[2] = readByte(input);
|
||||
tagBytes[3] = readByte(input);
|
||||
skip(input, 4);
|
||||
long offset = readUnsignedLong(input);
|
||||
skip(input, 4);
|
||||
|
||||
String tag = new String(tagBytes, "ISO-8859-1");
|
||||
if (tag.equals("head")) {
|
||||
headOffset = offset;
|
||||
if (kernOffset != -1) {
|
||||
break;
|
||||
}
|
||||
} else if (tag.equals("kern")) {
|
||||
kernOffset = offset;
|
||||
if (headOffset != -1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void readHEAD(InputStream input) throws IOException {
|
||||
seek(input, headOffset + 2 * 4 + 2 * 4 + 2);
|
||||
int unitsPerEm = readUnsignedShort(input);
|
||||
scale = (float) size / unitsPerEm;
|
||||
}
|
||||
|
||||
private void readKERN(InputStream input, List<KerningPair> ret) throws IOException {
|
||||
seek(input, kernOffset + 2);
|
||||
for (int subTableCount = readUnsignedShort(input); subTableCount > 0; subTableCount--) {
|
||||
skip(input, 2 * 2);
|
||||
int tupleIndex = readUnsignedShort(input);
|
||||
if (!((tupleIndex & 1) != 0) || (tupleIndex & 2) != 0 || (tupleIndex & 4) != 0) {
|
||||
return;
|
||||
}
|
||||
if (tupleIndex >> 8 != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int kerningCount = readUnsignedShort(input);
|
||||
skip(input, 3 * 2);
|
||||
while (kerningCount-- > 0) {
|
||||
int firstGlyphCode = readUnsignedShort(input);
|
||||
int secondGlyphCode = readUnsignedShort(input);
|
||||
int offset = readShort(input);
|
||||
ret.add(new KerningPair(charmap.get(firstGlyphCode), charmap.get(secondGlyphCode), offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int readUnsignedByte(InputStream input) throws IOException {
|
||||
bytePosition++;
|
||||
int b = input.read();
|
||||
if (b == -1) {
|
||||
throw new EOFException("Unexpected end of file.");
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
private byte readByte(InputStream input) throws IOException {
|
||||
return (byte) readUnsignedByte(input);
|
||||
}
|
||||
|
||||
private int readUnsignedShort(InputStream input) throws IOException {
|
||||
return (readUnsignedByte(input) << 8) + readUnsignedByte(input);
|
||||
}
|
||||
|
||||
private short readShort(InputStream input) throws IOException {
|
||||
return (short) readUnsignedShort(input);
|
||||
}
|
||||
|
||||
private long readUnsignedLong(InputStream input) throws IOException {
|
||||
long value = readUnsignedByte(input);
|
||||
value = (value << 8) + readUnsignedByte(input);
|
||||
value = (value << 8) + readUnsignedByte(input);
|
||||
value = (value << 8) + readUnsignedByte(input);
|
||||
return value;
|
||||
}
|
||||
|
||||
private void skip(InputStream input, long skip) throws IOException {
|
||||
while (skip > 0) {
|
||||
long skipped = input.skip(skip);
|
||||
if (skipped <= 0) {
|
||||
break;
|
||||
}
|
||||
bytePosition += skipped;
|
||||
skip -= skipped;
|
||||
}
|
||||
}
|
||||
|
||||
private void seek(InputStream input, long position) throws IOException {
|
||||
skip(input, position - bytePosition);
|
||||
}
|
||||
return face;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user