问题
在使用pdfbox转图片时,出现字体不支持的问题。通用的解决方式是将需要的字体安装到服务器来解决,但是这种方式比较鸡肋,如果我们是集群部署或者需要迁移服务器时,就必须多次安装字体,这是一种很不好的体验。
原因
pdfbox在转换pdf时,会从系统的字体库去初始化字体到内存中,如果要转换的字体已经存在,则直接使用该字体,如果该字体不存在,在源码中对该字体的替补字体进行配置,寻找可替代的字体转换,如果没有可替代的字体则不会输出文字内容。
解决
方案1:安装字体到系统
具体操作,将字体copy到/usr/share/fonts/路径下,使用命令 fc-cache -fv 刷新字体库即可。
方案2:为字体添加已存在的替补字体(需修改源码)。具体操作如下:
在pdfbox 项目中寻找类FontMapperImpl,按照图中红框的代码添加需要的字体规则
方案3:自定义添加字体,这里笔者做了两种处理。
一种是定义一个绝对路径,将字体放在该路径下,初始化时扫描加载这个字体库。具体做法如下: 找到FileSystemFontProvider,在他的构造其中添加绝对路径的字体扫描。可以看到笔者是通过系统变量pdfbox.fontsUrl来获取绝对变量的路径,默认路径为/usr/local/fonts/。这里可以自定义,怎么方便怎么来。eg:-Dpdfbox.fontsUrl=/root/pdf/resources/fonts
/**
* Constructor.
*/
FileSystemFontProvider(FontCache cache) {
this.cache = cache;
try {
if (LOG.isTraceEnabled()) {
LOG.trace("Will search the local system for fonts");
}
//定义一个用于字体去重的set集合
HashSet<String> fileSet = new HashSet<>();
// 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) {
File file = new File(font);
files.add(file);
fileSet.add(file.getName());
}
int localFonts=files.size();
if (LOG.isDebugEnabled()) {
LOG.debug("Found " + localFonts + " fonts on the local system");
}
// scan pdfbox.fontsUrl for font files
try {
String customerFontsUrl = System.getProperty("pdfbox.fontsUrl");
if (customerFontsUrl == null || customerFontsUrl.length() <= 0) {
customerFontsUrl = "/usr/local/fonts/";
}
File fontPath = new File(customerFontsUrl);
LOG.warn("scan " + fontPath.getAbsolutePath() + " for font files");
if (fontPath.exists()) {
File[] tempFile = fontPath.listFiles();
Arrays.asList(tempFile)
.forEach(item -> {
if (!fileSet.contains(item.getName())) {
fileSet.add(item.getName());
files.add(item);
}
});
LOG.warn("Found " + tempFile.length + " fonts on " + fontPath.getAbsolutePath());
}else{
LOG.warn("Found 0 fonts on " + fontPath.getAbsolutePath());
}
} catch (Exception e) {
LOG.warn("can't scan pdfbox.fontsUrl for font files", e);
}
//scan customer fonts for font files
List<File> customerFonts = MyFontsScanner.scanCustomerFonts();
customerFonts.forEach(item -> {
if (!fileSet.contains(item.getName())) {
fileSet.add(item.getName());
files.add(item);
if (LOG.isDebugEnabled()) {
LOG.debug("san font " + item.getAbsolutePath());
}
}
});
LOG.warn("Found " + customerFonts.size() + " fonts on resources/fonts");
LOG.warn("Custom fonts total size:" + (files.size() - localFonts));
// load cached FontInfo objects 将磁盘上缓存文件里的字体加载进来
List<FSFontInfo> cachedInfos = loadDiskCache(files);
if (cachedInfos != null && !cachedInfos.isEmpty()) {
fontInfoList.addAll(cachedInfos);
} else {
//cachedInfos为空 ,代表有新的字体加入,重新构建缓存文件
LOG.warn("Building on-disk font cache, this may take a while");
LOG.warn("fonts total size:" + files.size());
scanFonts(files);
//重新构建缓存文件
saveDiskCache();
LOG.warn("Finished building on-disk font cache, found " +
fontInfoList.size() + " fonts");
if (LOG.isDebugEnabled()) {
fontInfoList.forEach(x -> {
try {
LOG.debug(" include font: " + x.getFont().getName());
} catch (IOException e) {
LOG.warn(e);
}
});
}
}
} catch (AccessControlException e) {
LOG.error("Error accessing the file system", e);
}
}
另一种是将字体放在项目中的resources下,初始化时加载这些字体。具体做法如下,笔者先自定义了一个字体扫描类MyFontsScanner。这个扫描类的作用在于将resources下的字体库写入到一个临时目录中,然后再通过FileSystemFontProvider的加载方式进行加载。这里笔者固定了字体库的目录为resources/fonts/。注意:打包时不要编译字体文件,否则会导致不可用(可使用maven-resources-plugin插件的nonFilteredFileExtensions来排除)。
package org.apache.pdfbox.util;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* @author tangjianghua
* date 2020/6/5
* time 19:57
*/
public class MyFontsScanner {
private static final Log LOG = LogFactory.getLog(MyFontsScanner.class);
/**
* java临时路径在系统变量中的key
*/
public static final String JAVA_IO_TMPDIR = "java.io.tmpdir";
/**
* 默认自定义字体库路径在resources/fonts/下
*/
public static final String BOOT_FONTS_PATH_REGEX = "^BOOT-INF\\/classes\\/fonts\\/[a-zA-Z_\\-0-9]+\\.(?i)(tt[cf])$";
/**
* scanCustomerFonts and return the list of fonts's absolute path.
*
* @author tangjianghua
* date 2020/6/5
*/
public static List<File> scanCustomerFonts() {
List<File> fonts = new ArrayList<File>();
try {
String path = MyFontsScanner.class.getProtectionDomain().getCodeSource().getLocation().getPath();
if (path.startsWith("file:")) {
path = path.substring(5, path.length());
}
LOG.debug(path);
if (path.contains("BOOT-INF")) {
//springboot 模式
path = path.split("!")[0];
LOG.debug("项目所属路径:" + path);
if (path.endsWith("jar")) {
@SuppressWarnings("resource")
//获得jar包路径
JarFile jFile = new JarFile(path);
Enumeration<JarEntry> jarEntrys = jFile.entries();
while (jarEntrys.hasMoreElements()) {
String name = jarEntrys.nextElement().getName();
LOG.trace("read jarEntry :" + name);
if (name.matches(BOOT_FONTS_PATH_REGEX)) {
File s = writeTempFonts(name);
if (LOG.isDebugEnabled()) {
LOG.trace("write " + name + " to " + s.getAbsolutePath());
}
fonts.add(s);
}
}
}
} else if (path.contains("WEB-INF")) {
//tomcat模式
path = path.substring(0, path.indexOf("WEB-INF")) + "WEB-INF/classes/fonts";
LOG.debug("fonts path : " + path);
File file = new File(path);
if (file.exists() && file.isDirectory()) {
fonts.addAll(Arrays.asList(file.listFiles()));
}
} else {
LOG.warn("请指定jar包所处路径,或者指定通过pdfbox.fontsUrl自定义字体库的路径");
}
} catch (IOException e) {
LOG.error(e);
}
return fonts;
}
/**
* scanCustomerFonts and return the list of fonts's absolute path.
*
* @author tangjianghua
* date 2020/6/5
*/
public static Set<String> scanCustomerFontsName() {
Set<String> fonts = new HashSet<>();
try {
String path = MyFontsScanner.class.getProtectionDomain().getCodeSource().getLocation().getPath();
if (path.startsWith("file:")) {
path = path.substring(5, path.length());
}
LOG.debug(path);
if (path.contains("BOOT-INF")) {
//springboot 模式
path = path.split("!")[0];
LOG.debug("项目所属路径:" + path);
if (path.endsWith("jar")) {
@SuppressWarnings("resource")
//获得jar包路径
JarFile jFile = new JarFile(path);
Enumeration<JarEntry> jarEntrys = jFile.entries();
while (jarEntrys.hasMoreElements()) {
String name = jarEntrys.nextElement().getName();
LOG.trace("read jarEntry :" + name);
if (name.matches(BOOT_FONTS_PATH_REGEX)) {
fonts.add(name);
}
}
}
} else if (path.contains("WEB-INF")) {
//tomcat模式
path = path.substring(0, path.indexOf("WEB-INF")) + "WEB-INF/classes/fonts";
LOG.debug("fonts path : " + path);
File file = new File(path);
if (file.exists() && file.isDirectory()) {
Arrays.asList(file.listFiles())
.forEach(item -> {
fonts.add(item.getAbsolutePath().split("classes")[1]);
});
}
} else {
LOG.warn("请指定jar包所处路径,或者指定通过pdfbox.fontsUrl自定义字体库的路径");
}
} catch (IOException e) {
LOG.error(e);
}
return fonts;
}
/**
* 将字体写入临时文件夹,并返回绝对路径
*
* @param entryName
* @return 绝对路径
* @author tangjianghua
* date 2020/6/5
*/
private static File writeTempFonts(String entryName) {
String[] split = entryName.split("/");
String fileName = split[split.length - 1];
InputStream resourceAsStream = null;
try {
String fontsTempPath = System.getProperty(JAVA_IO_TMPDIR);
if (fontsTempPath == null || fontsTempPath.length() <= 0) {
String os = System.getProperty("os.name");
if (os.contains("Linux")) {
fontsTempPath = "/tmp";
} else if (os.contains("Windows")) {
fontsTempPath = System.getProperty("user.dir");
}
}
fontsTempPath = fontsTempPath + File.separator + "pdfbox" + File.separator + "fonts";
File fontsTempPathFile = new File(fontsTempPath);
if (!fontsTempPathFile.exists()) {
fontsTempPathFile.mkdirs();
}
File file = new File(fontsTempPath + File.separator + fileName);
if (file.exists()) {
return file;
}
resourceAsStream = Thread.currentThread().getClass().getResourceAsStream("/" + entryName);
FileOutputStream fileOutputStream = new FileOutputStream(file);
byte[] bytes = new byte[1024];
int i;
while ((i = resourceAsStream.read(bytes)) != -1) {
fileOutputStream.write(bytes, 0, i);
}
fileOutputStream.flush();
fileOutputStream.close();
resourceAsStream.close();
return file;
} catch (Exception e) {
LOG.warn("fail to write font into tmp path!", e);
return null;
} finally {
if (resourceAsStream != null) {
try {
resourceAsStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
使用时可以根据自己的情况进行选择使用。
git地址:https://github.com/tang-jianghua/pdfbox 可直接打包