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