package com.liuwa.font; import com.google.typography.font.sfntly.Font; import com.google.typography.font.sfntly.FontFactory; import com.google.typography.font.sfntly.Tag; import com.google.typography.font.sfntly.table.Table; import com.google.typography.font.sfntly.table.core.CMap; import com.google.typography.font.sfntly.table.core.CMapFormat12; import com.google.typography.font.sfntly.table.core.CMapTable; import com.google.typography.font.sfntly.table.core.PostScriptTable; import com.liuwa.exception.InvalidWoffException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.nio.ByteBuffer; import java.util.*; import java.util.zip.DataFormatException; import java.util.zip.Inflater; /** * woff 转换器 */ public class WoffConverter { private static Logger logger = LoggerFactory.getLogger(WoffConverter.class); private static final LinkedHashMap<String, Integer> woffHeaderFormat = new LinkedHashMap<String, Integer>() { { put("signature", 4); put("flavor", 4); put("length", 4); put("numTables", 2); put("reserved", 2); put("totalSfntSize", 4); put("majorVersion", 2); put("minorVersion", 2); put("metaOffset", 4); put("metaLength", 4); put("metaOrigLength", 4); put("privOffset", 4); put("privOrigLength", 4); } }; private static final LinkedHashMap<String, Integer> tableRecordEntryFormat = new LinkedHashMap<String, Integer>() { { put("tag", 4); put("offset", 4); put("compLength", 4); put("origLength", 4); put("origChecksum", 4); } }; private HashMap<String, Number> woffHeaders = new HashMap<String, Number>(); private ArrayList<HashMap<String, Number>> tableRecordEntries = new ArrayList<HashMap<String, Number>>(); private int offset = 0; private int readOffset = 0; private File woffFile; private byte[] ttfByteArray; private WoffConverter(){} public WoffConverter(File woffFile) throws InvalidWoffException, IOException, DataFormatException{ this.woffFile = woffFile; FileInputStream inputStream = new FileInputStream(woffFile); ByteArrayOutputStream ttfOutputStream = convertToTTFOutputStream(inputStream); ttfByteArray = ttfOutputStream.toByteArray(); } /** * woff 转 ttf byte[] * @return * @throws InvalidWoffException * @throws IOException * @throws DataFormatException */ public byte[] getTTFByteArray(){ return ttfByteArray; } /** * 获取Cmap * @return */ public LinkedHashMap<Integer, String> getCmap(){ LinkedHashMap<Integer, String> ret = new LinkedHashMap<Integer, String>(); try{ FontFactory fontFactory = FontFactory.getInstance(); Font font = fontFactory.loadFonts(ttfByteArray)[0]; Map<Integer, ? extends Table> tableMap = font.tableMap(); CMapTable cmapTable = (CMapTable)tableMap.get(Tag.cmap); Iterator<CMap> it = cmapTable.iterator(); while(it.hasNext()){ CMap cmap = it.next(); if(cmap instanceof CMapFormat12){ Iterator<Integer> it1 = cmap.iterator(); while(it1.hasNext()){ int val = it1.next(); String unicode = val < 128 ? String.valueOf((char) val) : ("uni" + Integer.toHexString(val)); ret.put(val, unicode); } break; } } } catch (IOException | InvalidWoffException ex){ logger.error(ex.getMessage(), ex); } return ret; } /** * 获取unicode 字符列表 * @return */ public List<String> getUniCodeList(){ List<String> works = new ArrayList<String>(); try{ FontFactory fontFactory = FontFactory.getInstance(); Font font = fontFactory.loadFonts(ttfByteArray)[0]; Map<Integer, ? extends Table> tableMap = font.tableMap(); if(tableMap.containsKey(Tag.CFF)){ } else if(tableMap.containsKey(Tag.post)){ PostScriptTable postScriptTable = (PostScriptTable)tableMap.get(Tag.post); for(int i=0; i< postScriptTable.numberOfGlyphs(); i++){ String glypName = postScriptTable.glyphName(i); if(!glypName.startsWith("uni")){ continue; } works.add(glypName); } } } catch (IOException | InvalidWoffException ex){ logger.error(ex.getMessage(), ex); } return works; } private ByteArrayOutputStream convertToTTFOutputStream(InputStream inputStream) throws InvalidWoffException, IOException, DataFormatException { getHeaders(new DataInputStream(inputStream)); if ((Integer) woffHeaders.get("signature") != 0x774F4646) { throw new InvalidWoffException("Invalid woff file"); } ByteArrayOutputStream ttfOutputStream = new ByteArrayOutputStream(); writeOffsetTable(ttfOutputStream); getTableRecordEntries(new DataInputStream(inputStream)); writeTableRecordEntries(ttfOutputStream); writeFontData(inputStream, ttfOutputStream); return ttfOutputStream; } /** * 获取头部 * @param woffFileStream * @throws IOException */ private void getHeaders(DataInputStream woffFileStream) throws IOException { readTableData(woffFileStream, woffHeaderFormat, woffHeaders); } /** * * @param ttfOutputStream * @throws IOException */ private void writeOffsetTable(ByteArrayOutputStream ttfOutputStream) throws IOException { ttfOutputStream.write(getBytes((Integer) woffHeaders.get("flavor"))); int numTables = (Integer) woffHeaders.get("numTables"); ttfOutputStream.write(getBytes((short)numTables)); int temp = numTables; int searchRange = 16; short entrySelector = 0; while (temp > 1) { temp = temp >> 1; entrySelector++; searchRange = (searchRange << 1); } short rangeShift = (short) (numTables * 16 - searchRange); ttfOutputStream.write(getBytes((short) searchRange)); ttfOutputStream.write(getBytes(entrySelector)); ttfOutputStream.write(getBytes(rangeShift)); offset += 12; } private void getTableRecordEntries(DataInputStream woffFileStream) throws IOException { int numTables = (Integer) woffHeaders.get("numTables"); for (int i = 0; i < numTables; i++) { HashMap<String, Number> tableDirectory = new HashMap<String, Number>(); readTableData(woffFileStream, tableRecordEntryFormat, tableDirectory); offset += 16; tableRecordEntries.add(tableDirectory); } } private void writeTableRecordEntries(ByteArrayOutputStream ttfOutputStream) throws IOException { for (HashMap<String, Number> tableRecordEntry : tableRecordEntries) { ttfOutputStream.write(getBytes((Integer) tableRecordEntry .get("tag"))); ttfOutputStream.write(getBytes((Integer) tableRecordEntry .get("origChecksum"))); ttfOutputStream.write(getBytes(offset)); ttfOutputStream.write(getBytes((Integer) tableRecordEntry .get("origLength"))); tableRecordEntry.put("outOffset", offset); offset += (Integer) tableRecordEntry.get("origLength"); if (offset % 4 != 0) { offset += 4 - (offset % 4); } } } private void writeFontData(InputStream woffFileStream, ByteArrayOutputStream ttfOutputStream) throws IOException, DataFormatException { for (HashMap<String, Number> tableRecordEntry : tableRecordEntries) { int tableRecordEntryOffset = (Integer) tableRecordEntry .get("offset"); int skipBytes = tableRecordEntryOffset - readOffset; if (skipBytes > 0) woffFileStream.skip(skipBytes); readOffset += skipBytes; int compressedLength = (Integer) tableRecordEntry.get("compLength"); int origLength = (Integer) tableRecordEntry.get("origLength"); byte[] fontData = new byte[compressedLength]; byte[] inflatedFontData = new byte[origLength]; int readBytes = 0; while (readBytes < compressedLength) { readBytes += woffFileStream.read(fontData, readBytes, compressedLength - readBytes); } readOffset += compressedLength; inflatedFontData = inflateFontData(compressedLength, origLength, fontData, inflatedFontData); ttfOutputStream.write(inflatedFontData); offset = (Integer) tableRecordEntry.get("outOffset") + (Integer) tableRecordEntry.get("origLength"); int padding = 0; if (offset % 4 != 0) padding = 4 - (offset % 4); ttfOutputStream.write(getBytes(0), 0, padding); } } private byte[] inflateFontData(int compressedLength, int origLength, byte[] fontData, byte[] inflatedFontData) { if (compressedLength != origLength) { Inflater decompressor = new Inflater(); decompressor.setInput(fontData, 0, compressedLength); try { decompressor.inflate(inflatedFontData, 0, origLength); } catch (DataFormatException e) { throw new InvalidWoffException("Malformed woff file"); } } else inflatedFontData = fontData; return inflatedFontData; } private byte[] getBytes(int i) { return ByteBuffer.allocate(4).putInt(i).array(); } private byte[] getBytes(short h) { return ByteBuffer.allocate(2).putShort(h).array(); } private void readTableData(DataInputStream woffFileStream, LinkedHashMap<String, Integer> formatTable, HashMap<String, Number> table) throws IOException { Iterator<String> headerKeys = formatTable.keySet().iterator(); while (headerKeys.hasNext()) { String key = headerKeys.next(); int size = formatTable.get(key); if (size == 2) { table.put(key, woffFileStream.readUnsignedShort()); } else if (size == 4) { table.put(key, woffFileStream.readInt()); } readOffset += size; } } }