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;
}
}
}