/* * Copyright (C) 2010-2016 JPEXS, All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3.0 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. */ package com.jpexs.decompiler.flash.helpers; import java.awt.Canvas; 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; /** * * @author JPEXS */ 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 * * @return Map<FamilyName,Map<FontNAme,Font>> */ public static Map<String, Map<String, Font>> getInstalledFonts() { Map<String, Map<String, Font>> ret = new HashMap<>(); Font[] fonts = null; try { 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) { // ignore } if (fonts == null) { fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts(); } List<String> javaFonts = Arrays.asList("Dialog", "DialogInput", "Monospaced", "Serif", "SansSerif"); for (Font f : fonts) { String fam = f.getFamily(Locale.ENGLISH); // Do not want Java logical fonts if (javaFonts.contains(fam)) { continue; } if (!ret.containsKey(fam)) { ret.put(fam, new HashMap<>()); } ret.get(fam).put(f.getFontName(Locale.ENGLISH), f); } return ret; } public static String fontToString(Font font) { int style = font.getStyle(); String styleString; switch (style) { case 1: styleString = "Bold"; break; case 2: styleString = "Italic"; break; case 3: styleString = "BoldItalic"; break; default: styleString = "Plain"; break; } return font.getName() + "-" + styleString + "-" + font.getSize(); } public static Font stringToFont(String fontString) { return Font.decode(fontString); } /** * 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(getFontRenderContext(withKerningFont), 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(getFontRenderContext(noKerningFont), 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 } } 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; } public static float getFontAdvance(Font font, char ch) { return createGlyphVector(font, ch).getGlyphMetrics(0).getAdvanceX(); } public static GlyphVector createGlyphVector(Font font, char ch) { return font.createGlyphVector(getFontRenderContext(font), new char[]{ch}); } private static FontRenderContext getFontRenderContext(Font font) { // Canvas works in headless mode return (new Canvas()).getFontMetrics(font).getFontRenderContext(); } 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(getFontRenderContext(withKerningFont), 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(getFontRenderContext(noKerningFont), 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 final char char1; public final 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 (char1 != other.char1) { return false; } if (char2 != other.char2) { return false; } if (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); } } }