/*
GanttProject is an opensource project management tool.
Copyright (C) 2009 Dmitry Barashev
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 3
of the License, or (at your option) any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.ganttproject.impex.htmlpdf.fonts;
import com.google.common.base.Function;
import com.google.common.base.Supplier;
import com.google.common.collect.Lists;
import com.itextpdf.awt.FontMapper;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.pdf.BaseFont;
import net.sourceforge.ganttproject.GPLogger;
import net.sourceforge.ganttproject.language.GanttLanguage;
import org.eclipse.core.runtime.Platform;
import org.ganttproject.impex.htmlpdf.itext.FontSubstitutionModel;
import org.ganttproject.impex.htmlpdf.itext.FontSubstitutionModel.FontSubstitution;
import java.awt.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.logging.Level;
/**
* This class collects True Type fonts from .ttf files in the registered
* directories and provides mappings of font family names to plain AWT fonts and
* iText fonts.
*
* @author dbarashev
*/
public class TTFontCache {
private static String FALLBACK_FONT_PATH = "/fonts/LiberationSans-Regular.ttf";
private Map<String, AwtFontSupplier> myMap_Family_RegularFont = new TreeMap<String, AwtFontSupplier>();
private final Map<FontKey, com.itextpdf.text.Font> myFontCache = new HashMap<FontKey, com.itextpdf.text.Font>();
private Map<String, Function<String, BaseFont>> myMap_Family_ItextFont = new HashMap<String, Function<String, BaseFont>>();
private Properties myProperties;
private BaseFont myFallbackFont;
public void registerDirectory(String path) {
GPLogger.getLogger(getClass()).info("scanning directory=" + path);
File dir = new File(path);
if (dir.exists() && dir.isDirectory()) {
registerFonts(dir);
} else {
GPLogger.getLogger(getClass()).info("directory " + path + " is not readable");
}
}
public List<String> getRegisteredFamilies() {
return new ArrayList<String>(myMap_Family_RegularFont.keySet());
}
public Font getAwtFont(String family) {
Supplier<Font> supplier = myMap_Family_RegularFont.get(family);
return supplier == null ? null : supplier.get();
}
private void registerFonts(File dir) {
final File[] files = dir.listFiles();
for (File f : files) {
if (!f.canRead()) {
continue;
}
if (f.isDirectory()) {
registerFonts(f);
continue;
}
String filename = f.getName().toLowerCase().trim();
if (!filename.endsWith(".ttf") && !filename.endsWith(".ttc")) {
continue;
}
try {
registerFontFile(f);
} catch (Throwable e) {
GPLogger.getLogger(TTFontCache.class).log(Level.FINE, "Failed to register font from " + f.getAbsolutePath(), e);
}
}
}
private static Font createAwtFont(File fontFile) throws IOException, FontFormatException {
try (FileInputStream istream = new FileInputStream(fontFile)) {
return Font.createFont(Font.TRUETYPE_FONT, istream);
}
}
private static class AwtFontSupplier implements Supplier<Font> {
private final List<File> myFiles = Lists.newArrayList();
private Font myFont;
void addFile(File f) {
myFiles.add(f);
}
@Override
public Font get() {
if (myFont == null) {
myFont = createFont();
}
return myFont;
}
private Font createFont() {
Font result = null;
for (File f : myFiles) {
Font font = createFont(f);
if (result == null || result.getStyle() > font.getStyle()) {
result = font;
}
}
return result;
}
private Font createFont(File fontFile) {
try {
return createAwtFont(fontFile);
} catch (IOException e) {
GPLogger.log(e);
} catch (FontFormatException e) {
GPLogger.log(e);
}
return null;
}
}
private void registerFontFile(final File fontFile) throws FontFormatException,
IOException {
// FontFactory.register(fontFile.getAbsolutePath());
Font awtFont = createAwtFont(fontFile);
GPLogger.getLogger(getClass()).fine("Trying font file: " + fontFile.getAbsolutePath());
final String family = awtFont.getFontName().toLowerCase();
AwtFontSupplier awtSupplier = myMap_Family_RegularFont.get(family);
try {
myMap_Family_ItextFont.put(family, createFontSupplier(fontFile, BaseFont.EMBEDDED));
} catch (DocumentException e) {
if (e.getMessage().indexOf("cannot be embedded") < 0) {
GPLogger.logToLogger(e);
return;
}
}
try {
myMap_Family_ItextFont.put(family, createFontSupplier(fontFile, BaseFont.NOT_EMBEDDED));
} catch (DocumentException e) {
GPLogger.logToLogger(e);
return;
}
GPLogger.getLogger(getClass()).fine("registering font: " + family);
if (awtSupplier == null) {
awtSupplier = new AwtFontSupplier();
myMap_Family_RegularFont.put(family, awtSupplier);
}
awtSupplier.addFile(fontFile);
}
private Function<String, BaseFont> createFontSupplier(final File fontFile, final boolean isEmbedded)
throws DocumentException, IOException {
try {
BaseFont.createFont(fontFile.getAbsolutePath(), GanttLanguage.getInstance().getCharSet(), isEmbedded);
} catch (DocumentException e) {
if (!e.getMessage().contains("is not recognized")
|| !e.getMessage().contains(GanttLanguage.getInstance().getCharSet())) {
throw e;
}
} finally {
BaseFontPublicMorozov.clearCache();
}
return new Function<String, BaseFont>() {
@Override
public BaseFont apply(String charset) {
try {
if (fontFile.getName().toLowerCase().endsWith(".ttc")) {
return BaseFont.createFont(fontFile.getAbsolutePath() + ",0", charset, isEmbedded);
} else {
return BaseFont.createFont(fontFile.getAbsolutePath(), charset, isEmbedded);
}
} catch (DocumentException e) {
GPLogger.log(e);
} catch (IOException e) {
GPLogger.log(e);
}
return null;
}
};
}
private static class FontKey {
private String family;
private int style;
private float size;
private String charset;
FontKey(String family, String charset, int style, float size) {
this.family = family;
this.charset = charset;
this.style = style;
this.size = size;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((charset == null) ? 0 : charset.hashCode());
result = prime * result + ((family == null) ? 0 : family.hashCode());
result = prime * result + Float.floatToIntBits(size);
result = prime * result + style;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
FontKey other = (FontKey) obj;
if (charset == null) {
if (other.charset != null)
return false;
} else if (!charset.equals(other.charset))
return false;
if (family == null) {
if (other.family != null)
return false;
} else if (!family.equals(other.family))
return false;
if (Float.floatToIntBits(size) != Float.floatToIntBits(other.size))
return false;
if (style != other.style)
return false;
return true;
}
}
public com.itextpdf.text.Font getFont(String family, String charset, int style, float size) {
FontKey key = new FontKey(family, charset, style, size);
com.itextpdf.text.Font result = myFontCache.get(key);
if (result == null) {
Function<String, BaseFont> f = myMap_Family_ItextFont.get(family);
BaseFont bf = f == null ? getFallbackFont(charset) : f.apply(charset);
if (bf != null) {
result = new com.itextpdf.text.Font(bf, size, style);
myFontCache.put(key, result);
} else {
GPLogger.log(new RuntimeException("Font with family=" + family + " not found. Also tried fallback font"));
}
}
return result;
}
public FontMapper getFontMapper(final FontSubstitutionModel substitutions, final String charset) {
return new FontMapper() {
private Map<Font, BaseFont> myFontCache = new HashMap<Font, BaseFont>();
@Override
public BaseFont awtToPdf(Font awtFont) {
if (myFontCache.containsKey(awtFont)) {
return myFontCache.get(awtFont);
}
String family = awtFont.getFamily().toLowerCase().replace(' ', '_');
if (myProperties.containsKey("font." + family)) {
family = String.valueOf(myProperties.get("font." + family));
}
FontSubstitution substitution = substitutions.getSubstitution(family);
if (substitution != null) {
family = substitution.getSubstitutionFamily();
}
Function<String, BaseFont> f = myMap_Family_ItextFont.get(family);
if (f != null) {
BaseFont result = f.apply(charset);
myFontCache.put(awtFont, result);
return result;
}
BaseFont result = getFallbackFont(charset);
if (result == null) {
GPLogger.log(new RuntimeException("Font with family=" + awtFont.getFamily()
+ " not found. Also tried family=" + family + " and fallback font"));
}
return result;
}
@Override
public Font pdfToAwt(BaseFont itextFont, int size) {
return null;
}
};
}
protected BaseFont getFallbackFont(String charset) {
if (myFallbackFont == null) {
try {
myFallbackFont = BaseFont.createFont(Platform.resolve(getClass().getResource(FALLBACK_FONT_PATH)).getPath(),
charset, BaseFont.EMBEDDED);
} catch (DocumentException e) {
GPLogger.logToLogger(e);
} catch (IOException e) {
GPLogger.logToLogger(e);
}
}
return myFallbackFont;
}
public void setProperties(Properties properties) {
myProperties = properties;
}
// BaseFont.fontCache is a static map which caches font objects. Since we scan all
// fonts in this code, we may cache a few hundreds of objects, and retained size of each object
// can be up to a few megabytes. Here we use so-called "Public Morozov" anti-pattern
// which discloses protected fields of its parent class
// See description of this pattern in English here:
// http://jamesdolan.blogspot.com/2011/05/pavlik-morozov-anti-pattern.html
private static abstract class BaseFontPublicMorozov extends BaseFont {
static void clearCache() {
BaseFont.fontCache.clear();
}
}
}