package com.tom_roush.pdfbox.pdmodel.font;
import android.util.Log;
import com.tom_roush.fontbox.FontBoxFont;
import com.tom_roush.fontbox.cff.CFFCIDFont;
import com.tom_roush.fontbox.cff.CFFFont;
import com.tom_roush.fontbox.ttf.NamingTable;
import com.tom_roush.fontbox.ttf.OTFParser;
import com.tom_roush.fontbox.ttf.OpenTypeFont;
import com.tom_roush.fontbox.ttf.TTFParser;
import com.tom_roush.fontbox.ttf.TrueTypeCollection;
import com.tom_roush.fontbox.ttf.TrueTypeFont;
import com.tom_roush.fontbox.type1.Type1Font;
import com.tom_roush.fontbox.util.autodetect.FontFileFinder;
import com.tom_roush.pdfbox.io.IOUtils;
import com.tom_roush.pdfbox.util.Charsets;
import com.tom_roush.pdfbox.util.PDFBoxResourceLoader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.prefs.Preferences;
/**
* A FontProvider which searches for fonts on the local filesystem.
*
* @author John Hewson
*/
final class FileSystemFontProvider extends FontProvider
{
private static final long serialVersionUID = 1;
private final List<FSFontInfo> fontInfoList = new ArrayList<FSFontInfo>();
private final FontCache cache;
private static class FSFontInfo extends FontInfo implements Serializable
{
private final String postScriptName;
private final FontFormat format;
private final CIDSystemInfo cidSystemInfo;
private final int usWeightClass;
private final int sFamilyClass;
private final int ulCodePageRange1;
private final int ulCodePageRange2;
private final int macStyle;
private final PDPanoseClassification panose;
private final File file;
private transient FileSystemFontProvider parent;
private FSFontInfo(File file, FontFormat format, String postScriptName,
CIDSystemInfo cidSystemInfo, int usWeightClass, int sFamilyClass,
int ulCodePageRange1, int ulCodePageRange2, int macStyle, byte[] panose,
FileSystemFontProvider parent)
{
this.file = file;
this.format = format;
this.postScriptName = postScriptName;
this.cidSystemInfo = cidSystemInfo;
this.usWeightClass = usWeightClass;
this.sFamilyClass = sFamilyClass;
this.ulCodePageRange1 = ulCodePageRange1;
this.ulCodePageRange2 = ulCodePageRange2;
this.macStyle = macStyle;
this.panose = panose != null ? new PDPanoseClassification(panose) : null;
this.parent = parent;
}
@Override
public String getPostScriptName()
{
return postScriptName;
}
@Override
public FontFormat getFormat()
{
return format;
}
@Override
public CIDSystemInfo getCIDSystemInfo()
{
return cidSystemInfo;
}
@Override
public FontBoxFont getFont()
{
FontBoxFont cached = parent.cache.getFont(this);
if (cached != null)
{
return cached;
}
else
{
FontBoxFont font;
switch (format)
{
case PFB:
font = parent.getType1Font(postScriptName, file);
break;
case TTF:
font = parent.getTrueTypeFont(postScriptName, file);
break;
case OTF:
font = parent.getOTFFont(postScriptName, file);
break;
default:
throw new RuntimeException("can't happen");
}
parent.cache.addFont(this, font);
return font;
}
}
@Override
public int getFamilyClass()
{
return sFamilyClass;
}
@Override
public int getWeightClass()
{
return usWeightClass;
}
@Override
public int getCodePageRange1()
{
return ulCodePageRange1;
}
@Override
public int getCodePageRange2()
{
return ulCodePageRange2;
}
@Override
public int getMacStyle()
{
return macStyle;
}
@Override
public PDPanoseClassification getPanose()
{
return panose;
}
@Override
public String toString()
{
return super.toString() + " " + file;
}
}
/**
* Represents ignored fonts (i.e. bitmap fonts).
*/
private static final class FSIgnored extends FSFontInfo implements Serializable
{
private FSIgnored(File file, FontFormat format, String postScriptName)
{
super(file, format, postScriptName, null, 0, 0, 0, 0, 0, null, null);
}
}
/**
* Constructor.
*/
FileSystemFontProvider(FontCache cache)
{
this.cache = cache;
// XXX: load in background?
if (PDFBoxResourceLoader.LOAD_FONTS == PDFBoxResourceLoader.FontLoadLevel.NONE)
{
return;
}
if (PDFBoxResourceLoader.LOAD_FONTS == PDFBoxResourceLoader.FontLoadLevel.MINIMUM)
{
// If MINIMUM, load only Droid fonts
try
{
addTrueTypeFont(new File("/system/fonts/DroidSans.ttf"));
addTrueTypeFont(new File("/system/fonts/DroidSans-Bold.ttf"));
addTrueTypeFont(new File("/system/fonts/DroidSansMono.ttf"));
// addTrueTypeFont(new File("/system/fonts/DroidSansFallback.ttf"));
// XXX: list may need to be expanded for other character sets
return;
}
catch (IOException e)
{
e.printStackTrace();
}
}
Log.v("PdfBox-Android", "Will search the local system for fonts");
// scan the local system for font files
List<File> files = new ArrayList<File>();
FontFileFinder fontFileFinder = new FontFileFinder();
List<URI> fonts = fontFileFinder.find();
for (URI font : fonts)
{
files.add(new File(font));
}
Log.v("PdfBox-Android", "Found " + files.size() + " fonts on the local system");
// load cached FontInfo objects
List<FSFontInfo> cachedInfos = loadCache(files);
if (cachedInfos != null && cachedInfos.size() > 0)
{
fontInfoList.addAll(cachedInfos);
}
else
{
Log.w("PdfBox-Android", "Building font cache, this may take a while");
scanFonts(files);
saveCache();
}
}
private void scanFonts(List<File> files)
{
for (File file : files)
{
try
{
if (file.getPath().toLowerCase().endsWith(".ttf") ||
file.getPath().toLowerCase().endsWith(".otf"))
{
addTrueTypeFont(file);
}
else if (file.getPath().toLowerCase().endsWith(".ttc") ||
file.getPath().toLowerCase().endsWith(".otc"))
{
addTrueTypeCollection(file);
}
else if (file.getPath().toLowerCase().endsWith(".pfb"))
{
addType1Font(file);
}
}
catch (IOException e)
{
Log.e("PdfBox-Android", "Error parsing font " + file.getPath(), e);
}
}
}
private void saveCache()
{
// Get the preferences database for this package.
Preferences prefs = Preferences.userNodeForPackage(FileSystemFontProvider.class);
// To save, write the object to a byte array.
try
{
for (FSFontInfo fontInfo : fontInfoList)
{
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ObjectOutputStream objectOut = new ObjectOutputStream(byteOut);
// write it to the stream
objectOut.writeObject(fontInfo);
prefs.putByteArray(fontInfo.file.getAbsolutePath(), byteOut.toByteArray());
}
}
catch (IOException e)
{
Log.e("PdfBox-Android", "Could not write to font cache", e);
}
Log.w("PdfBox-Android",
"Finished building font cache, found " + fontInfoList.size() + " fonts");
}
private List<FSFontInfo> loadCache(List<File> files)
{
// Get the preferences database for this package.
Preferences prefs = Preferences.userNodeForPackage(FileSystemFontProvider.class);
List<FSFontInfo> results = new ArrayList<FSFontInfo>();
for (File file : files)
{
// The second argument is the default if the key isn't found.
byte[] stored = prefs.getByteArray(file.getAbsolutePath(), null);
if (stored != null)
{
try
{
ByteArrayInputStream byteIn = new ByteArrayInputStream(stored);
ObjectInputStream objectIn = new ObjectInputStream(byteIn);
Object object = objectIn.readObject();
if (object instanceof FSFontInfo)
{
FSFontInfo info = (FSFontInfo) object;
info.parent = this;
results.add(info);
}
}
catch (ClassNotFoundException e)
{
Log.e("PdfBox-Android", "Error loading font cache, will be re-built", e);
return null;
}
catch (IOException e)
{
Log.e("PdfBox-Android", "Error loading font cache, will be re-built", e);
return null;
}
}
else
{
// re-build the entire cache if we encounter un-cached fonts (could be optimised)
Log.w("PdfBox-Android", "New fonts found, font cache will be re-built");
return null;
}
}
return results;
}
/**
* Adds a TTC or OTC to the file cache. To reduce memory, the parsed font is not cached.
*/
private void addTrueTypeCollection(File ttcFile) throws IOException
{
TrueTypeCollection ttc = null;
try
{
ttc = new TrueTypeCollection(ttcFile);
for (TrueTypeFont ttf : ttc.getFonts())
{
addTrueTypeFontImpl(ttf, ttcFile);
}
}
catch (NullPointerException e) // TTF parser is buggy
{
Log.e("PdfBox-Android", "Could not load font file: " + ttcFile, e);
}
catch (IOException e)
{
Log.e("PdfBox-Android", "Could not load font file: " + ttcFile, e);
}
finally
{
if (ttc != null)
{
ttc.close();
}
}
}
/**
* Adds an OTF or TTF font to the file cache. To reduce memory, the parsed font is not cached.
*/
private void addTrueTypeFont(File ttfFile) throws IOException
{
try
{
if (ttfFile.getPath().endsWith(".otf"))
{
OTFParser parser = new OTFParser(false, true);
OpenTypeFont otf = parser.parse(ttfFile);
addTrueTypeFontImpl(otf, ttfFile);
}
else
{
TTFParser parser = new TTFParser(false, true);
TrueTypeFont ttf = parser.parse(ttfFile);
addTrueTypeFontImpl(ttf, ttfFile);
}
}
catch (NullPointerException e) // TTF parser is buggy
{
Log.e("PdfBox-Android", "Could not load font file: " + ttfFile, e);
}
catch (IOException e)
{
Log.e("PdfBox-Android", "Could not load font file: " + ttfFile, e);
}
}
/**
* Adds an OTF or TTf font to the file cache. To reduce memory, the parsed font is not cached.
*/
private void addTrueTypeFontImpl(TrueTypeFont ttf, File file) throws IOException
{
try
{
// read PostScript name, if any
if (ttf.getName() != null)
{
int sFamilyClass = -1;
int usWeightClass = -1;
int ulCodePageRange1 = 0;
int ulCodePageRange2 = 0;
byte[] panose = null;
// Apple's AAT fonts don't have an OS/2 table
if (ttf.getOS2Windows() != null)
{
sFamilyClass = ttf.getOS2Windows().getFamilyClass();
usWeightClass = ttf.getOS2Windows().getWeightClass();
ulCodePageRange1 = (int) ttf.getOS2Windows().getCodePageRange1();
ulCodePageRange2 = (int) ttf.getOS2Windows().getCodePageRange2();
panose = ttf.getOS2Windows().getPanose();
}
// ignore bitmap fonts
if (ttf.getHeader() == null)
{
fontInfoList.add(new FSIgnored(file, FontFormat.TTF, ttf.getName()));
return;
}
int macStyle = ttf.getHeader().getMacStyle();
String format;
if (ttf instanceof OpenTypeFont && ((OpenTypeFont) ttf).isPostScript())
{
format = "OTF";
CFFFont cff = ((OpenTypeFont) ttf).getCFF().getFont();
CIDSystemInfo ros = null;
if (cff instanceof CFFCIDFont)
{
CFFCIDFont cidFont = (CFFCIDFont) cff;
String registry = cidFont.getRegistry();
String ordering = cidFont.getOrdering();
int supplement = cidFont.getSupplement();
ros = new CIDSystemInfo(registry, ordering, supplement);
}
fontInfoList.add(new FSFontInfo(file, FontFormat.OTF, ttf.getName(), ros,
usWeightClass, sFamilyClass, ulCodePageRange1, ulCodePageRange2,
macStyle, panose, this));
}
else
{
CIDSystemInfo ros = null;
if (ttf.getTableMap().containsKey("gcid"))
{
// Apple's AAT fonts have a "gcid" table with CID info
byte[] bytes = ttf.getTableBytes(ttf.getTableMap().get("gcid"));
String registryName = new String(bytes, 10, 64, Charsets.US_ASCII)
.trim();
String orderName = new String(bytes, 76, 64, Charsets.US_ASCII).trim();
int supplementVersion = bytes[140] << 8 & bytes[141];
ros = new CIDSystemInfo(registryName, orderName, supplementVersion);
}
format = "TTF";
fontInfoList.add(new FSFontInfo(file, FontFormat.TTF, ttf.getName(), ros,
usWeightClass, sFamilyClass, ulCodePageRange1, ulCodePageRange2,
macStyle, panose, this));
}
NamingTable name = ttf.getNaming();
if (name != null)
{
Log.d("PdfBox-Android", format + ": '" + name.getPostScriptName() + "' / '"
+ name.getFontFamily() + "' / '" + name.getFontSubFamily() + "'");
}
}
else
{
Log.w("PdfBox-Android", "Missing 'name' entry for PostScript name in font " + file);
}
}
catch (IOException e)
{
Log.e("PdfBox-Android", "Could not load font file: " + file, e);
}
finally
{
if (ttf != null)
{
ttf.close();
}
}
}
/**
* Adds a Type 1 font to the file cache. To reduce memory, the parsed font is not cached.
*/
private void addType1Font(File pfbFile) throws IOException
{
InputStream input = new FileInputStream(pfbFile);
try
{
Type1Font type1 = Type1Font.createWithPFB(input);
fontInfoList.add(new FSFontInfo(pfbFile, FontFormat.PFB, type1.getName(), null, -1, -1,
0, 0, -1, null, this));
Log.v("PdfBox-Android", "PFB: '" + type1.getName() + "' / '" + type1.getFamilyName() +
"' / '" + type1.getWeight() + "'");
}
catch (IOException e)
{
Log.e("PdfBox-Android", "Could not load font file: " + pfbFile, e);
}
finally
{
input.close();
}
}
private TrueTypeFont getTrueTypeFont(String postScriptName, File file)
{
try
{
TrueTypeFont ttf = readTrueTypeFont(postScriptName, file);
Log.d("PdfBox-Android", "Loaded " + postScriptName + " from " + file);
return ttf;
}
catch (NullPointerException e) // TTF parser is buggy
{
Log.d("PdfBox-Android", "Could not load font file: " + file, e);
}
catch (IOException e)
{
Log.d("PdfBox-Android", "Could not load font file: " + file, e);
}
return null;
}
private TrueTypeFont readTrueTypeFont(String postScriptName, File file) throws IOException
{
if (file.getName().toLowerCase().endsWith(".ttc"))
{
TrueTypeCollection ttc = new TrueTypeCollection(file);
for (TrueTypeFont ttf : ttc.getFonts())
{
if (ttf.getName().equals(postScriptName))
{
return ttf;
}
}
throw new IOException("Font " + postScriptName + " not found in " + file);
}
else
{
TTFParser ttfParser = new TTFParser(false, true);
return ttfParser.parse(file);
}
}
private OpenTypeFont getOTFFont(String postScriptName, File file)
{
try
{
// todo JH: we don't yet support loading CFF fonts from OTC collections

OTFParser parser = new OTFParser(false, true);
OpenTypeFont otf = parser.parse(file);
Log.d("PdfBox-Android", "Loaded " + postScriptName + " from " + file);
return otf;
}
catch (IOException e)
{
Log.e("PdfBox-Android", "Could not load font file: " + file, e);
}
return null;
}
private Type1Font getType1Font(String postScriptName, File file)
{
InputStream input = null;
try
{
input = new FileInputStream(file);
Type1Font type1 = Type1Font.createWithPFB(input);
Log.d("PdfBox-Android", "Loaded " + postScriptName + " from " + file);
return type1;
}
catch (IOException e)
{
Log.e("PdfBox-Android", "Could not load font file: " + file, e);
}
finally
{
IOUtils.closeQuietly(input);
}
return null;
}
@Override
public String toDebugString()
{
StringBuilder sb = new StringBuilder();
for (FSFontInfo info : fontInfoList)
{
sb.append(info.getFormat());
sb.append(": ");
sb.append(info.getPostScriptName());
sb.append(": ");
sb.append(info.file.getPath());
sb.append('\n');
}
return sb.toString();
}
@Override
public List<? extends FontInfo> getFontInfo()
{
return fontInfoList;
}
}