This commit is contained in:
2025-10-28 16:26:20 +08:00
parent 97135dfb25
commit 1a615c9374
2 changed files with 179 additions and 35 deletions

View File

@@ -0,0 +1,138 @@
package com.point.strategy.common;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* OFD 字体扫描工具
* 作用:解析 OFD 包/目录中的 XML输出声明的字体FontName/FamilyName/粗细等)
* 注意:此工具列出“声明的字体”,未必都是“实际被使用”的字体;但可用于定位缺失的粗体/家族。
*/
public class OfdFontInspector {
private static final Logger log = LoggerFactory.getLogger(OfdFontInspector.class);
/**
* 扫描 OFD 的字体声明并返回去重后的描述列表
* @param ofdPath OFD 文件路径(.ofd 压缩包)或已解压目录
* @return 字体信息列表(去重)
*/
public static List<String> listFonts(String ofdPath) {
Set<String> fonts = new LinkedHashSet<>();
try {
Path p = Paths.get(ofdPath);
if (Files.isDirectory(p)) {
// 仅遍历 Res 目录下的 *Res.xmlPublicRes.xml/DocumentRes.xml/PageRes.xml降低解析失败概率
Files.walk(p)
.filter(f -> f.toString().matches("(?i).*/Res/.*Res\\.xml$"))
.forEach(xml -> {
try (InputStream in = Files.newInputStream(xml)) {
parseXml(fonts, in, xml.toString());
} catch (Exception e) {
log.debug("[OfdFontInspector] 打开失败 src={}, err={}", xml, e.toString());
}
});
} else {
// 压缩包:遍历所有 .xml 条目
try (ZipFile zf = new ZipFile(new File(ofdPath))) {
Enumeration<? extends ZipEntry> entries = zf.entries();
while (entries.hasMoreElements()) {
ZipEntry e = entries.nextElement();
String name = e.getName();
if (!e.isDirectory() && name.matches("(?i).*/Res/.*Res\\.xml$")) {
try (InputStream is = zf.getInputStream(e)) {
parseXml(fonts, is, name);
} catch (Exception ex) {
log.debug("[OfdFontInspector] 打开失败 src={}, err={}", name, ex.toString());
}
}
}
}
}
} catch (Exception ex) {
throw new RuntimeException("扫描 OFD 字体失败: " + ex.getMessage(), ex);
}
return new ArrayList<>(fonts);
}
/**
* 打印字体声明到控制台(便于快速定位)
*/
public static void printFonts(String ofdPath) {
List<String> list = listFonts(ofdPath);
if (list.isEmpty()) {
log.info("[OfdFontInspector] 未在资源中发现字体声明。");
} else {
log.info("[OfdFontInspector] 发现字体声明共 {} 项:", list.size());
for (String s : list) {
log.info(" - {}", s);
}
}
}
/**
* 解析单个 XML 流,提取 Font 节点信息
*/
private static void parseXml(Set<String> out, InputStream is, String sourceName) {
try {
SAXReader reader = secureSAXReader();
Document doc = reader.read(is);
if (doc == null) return;
Element root = doc.getRootElement();
if (root == null) return;
// 使用 local-name() 规避命名空间差异,匹配 Font 节点
@SuppressWarnings("unchecked")
List<Element> fontNodes = root.selectNodes("//*[local-name()='Font']");
for (Element font : fontNodes) {
// 常见属性名OFD CT_FontFontName / FamilyName / Bold / Italic / Weight 等
String fontName = getAttr(font, "FontName");
String family = getAttr(font, "FamilyName");
String bold = getAttr(font, "Bold");
String italic = getAttr(font, "Italic");
String weight = getAttr(font, "Weight");
String id = getAttr(font, "ID");
String desc = String.format(Locale.ROOT,
"[src=%s] ID=%s FontName=%s Family=%s Bold=%s Italic=%s Weight=%s",
sourceName, nullToEmpty(id), nullToEmpty(fontName), nullToEmpty(family),
nullToEmpty(bold), nullToEmpty(italic), nullToEmpty(weight));
out.add(desc);
}
} catch (Exception ex) {
// 单个 XML 解析失败不影响整体
log.debug("[OfdFontInspector] 解析失败 src={}, err={}", sourceName, ex.toString());
}
}
private static String getAttr(Element e, String name) {
return e.attributeValue(name);
}
private static String nullToEmpty(String s) {
return s == null ? "" : s;
}
// 关闭外部实体/DTD避免解析失败和安全问题
private static SAXReader secureSAXReader() throws Exception {
SAXReader reader = new SAXReader(false);
try { reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); } catch (Exception ignore) {}
try { reader.setFeature("http://xml.org/sax/features/validation", false); } catch (Exception ignore) {}
try { reader.setFeature("http://xml.org/sax/features/external-general-entities", false); } catch (Exception ignore) {}
try { reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false); } catch (Exception ignore) {}
return reader;
}
}

View File

@@ -11,6 +11,7 @@ import java.nio.file.Paths;
import java.util.List;
import com.spire.pdf.PdfDocument;
import com.spire.pdf.graphics.PdfImage;
// import removed: PdfSection not needed when按页缩放适配
/**
* OFD转PDF工具类
@@ -29,64 +30,69 @@ public class OfdToPdfUtil {
Path ofdPath = Paths.get(resourceFilePath);
Path tempDir = Paths.get(new File(targetFilePath).getParent(), "temp_" + System.currentTimeMillis());
tempDir.toFile().mkdirs();
// 使用高质量设置将OFD转换为PNG图片
// 为提升整体速度改回 JPG分辨率适中10ppm≈254DPI
try (OFDReader reader = new OFDReader(ofdPath)) {
ImageExporter exporter = new ImageExporter(ofdPath, tempDir, "PNG", 30.0);
ImageExporter exporter = new ImageExporter(ofdPath, tempDir, "JPG", 10.0);
exporter.export();
exporter.close();
List<Path> imagePaths = exporter.getImgFilePaths();
exporter.close();
if (imagePaths.isEmpty()) {
throw new RuntimeException("未能从OFD文件中提取图片");
}
// 将图片合并为PDF保留颜色
imagesToPdf(imagePaths, targetFilePath);
// 将图片分页写入PDF并及时清理临时图片减少IO与峰值占用
imagesToPdfPageByPage(imagePaths, targetFilePath);
}
// 清理临时文件
deleteDirectory(tempDir.toFile());
} catch (Exception e) {
throw new RuntimeException("OFD转PDF失败: " + e.getMessage(), e);
}
}
/**
* 图片列表合并为PDF保留原始颜色
* 分页处理图片列表合并为PDF边写边清理,兼顾颜色与性能
*/
private static void imagesToPdf(List<Path> imagePaths, String targetFilePath) {
private static void imagesToPdfPageByPage(List<Path> imagePaths, String targetFilePath) {
PdfDocument pdf = null;
try {
PdfDocument pdf = new PdfDocument();
pdf = new PdfDocument();
for (Path imagePath : imagePaths) {
// 使用Spire.PDF的PdfImage.fromFile方法直接从文件加载图片
com.spire.pdf.graphics.PdfImage image = com.spire.pdf.graphics.PdfImage.fromFile(imagePath.toString());
if (image != null) {
// 创建新页面,大小与图片相同
com.spire.pdf.PdfPageBase page = pdf.getPages().add();
// 计算图片适配页面的缩放比例
double widthFitRate = image.getPhysicalDimension().getWidth() / page.getCanvas().getClientSize().getWidth();
double heightFitRate = image.getPhysicalDimension().getHeight() / page.getCanvas().getClientSize().getHeight();
float fitRate = Math.max((float)widthFitRate, (float)heightFitRate);
// 计算适配后的尺寸
double fitWidth = image.getPhysicalDimension().getWidth() / fitRate;
double fitHeight = image.getPhysicalDimension().getHeight() / fitRate;
// 将图片绘制到页面,保留原始颜色
page.getCanvas().drawImage(image, 0, 30, fitWidth, fitHeight);
com.spire.pdf.graphics.PdfImage image = null;
try {
image = com.spire.pdf.graphics.PdfImage.fromFile(imagePath.toString());
if (image != null) {
// 使用默认页面尺寸,按比例缩放图片以适配页面
com.spire.pdf.PdfPageBase page = pdf.getPages().add();
double widthFitRate = image.getPhysicalDimension().getWidth() / page.getCanvas().getClientSize().getWidth();
double heightFitRate = image.getPhysicalDimension().getHeight() / page.getCanvas().getClientSize().getHeight();
float fitRate = Math.max((float) widthFitRate, (float) heightFitRate);
double fitWidth = image.getPhysicalDimension().getWidth() / fitRate;
double fitHeight = image.getPhysicalDimension().getHeight() / fitRate;
// 顶部预留少量边距,避免顶边遮挡
page.getCanvas().drawImage(image, 0, 30, fitWidth, fitHeight);
}
} finally {
// 及时清理磁盘临时图片
try { java.nio.file.Files.deleteIfExists(imagePath); } catch (Exception ignore) {}
}
}
pdf.saveToFile(targetFilePath);
pdf.close();
} catch (Exception e) {
throw new RuntimeException("图片合并PDF失败: " + e.getMessage(), e);
} finally {
if (pdf != null) {
try { pdf.close(); } catch (Exception ignore) {}
}
}
}