|
@@ -1,17 +1,33 @@
|
|
|
package com.ymwk.icvp.utils;
|
|
package com.ymwk.icvp.utils;
|
|
|
|
|
|
|
|
import com.ymwk.icvp.pdfToOfdBao.OFDRender;
|
|
import com.ymwk.icvp.pdfToOfdBao.OFDRender;
|
|
|
-import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
|
|
+import org.apache.pdfbox.cos.*;
|
|
|
import org.ofdrw.converter.ConvertHelper;
|
|
import org.ofdrw.converter.ConvertHelper;
|
|
|
import org.ofdrw.converter.FontLoader;
|
|
import org.ofdrw.converter.FontLoader;
|
|
|
import org.springframework.stereotype.Component;
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
|
|
|
-import java.io.ByteArrayInputStream;
|
|
|
|
|
-import java.io.ByteArrayOutputStream;
|
|
|
|
|
-import java.io.InputStream;
|
|
|
|
|
|
|
+import java.io.*;
|
|
|
|
|
+
|
|
|
|
|
+import org.apache.pdfbox.pdmodel.PDDocument;
|
|
|
|
|
+import org.apache.pdfbox.pdmodel.PDPage;
|
|
|
|
|
+import org.apache.pdfbox.pdmodel.PDResources;
|
|
|
|
|
+import org.apache.pdfbox.cos.COSName;
|
|
|
|
|
+import org.apache.pdfbox.cos.COSDictionary;
|
|
|
|
|
+import org.apache.pdfbox.cos.COSArray;
|
|
|
|
|
+import org.apache.pdfbox.cos.COSBase;
|
|
|
|
|
+import org.apache.pdfbox.cos.COSStream;
|
|
|
|
|
+import org.apache.pdfbox.pdmodel.font.PDFont;
|
|
|
|
|
+import org.w3c.dom.Document;
|
|
|
|
|
+import org.w3c.dom.Element;
|
|
|
|
|
+import org.w3c.dom.NodeList;
|
|
|
|
|
+
|
|
|
|
|
+import javax.xml.parsers.DocumentBuilderFactory;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.zip.ZipEntry;
|
|
|
|
|
+import java.util.zip.ZipInputStream;
|
|
|
|
|
|
|
|
@Component
|
|
@Component
|
|
|
-@Slf4j
|
|
|
|
|
public class fileConvert {
|
|
public class fileConvert {
|
|
|
|
|
|
|
|
|
|
|
|
@@ -26,23 +42,380 @@ public class fileConvert {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public static InputStream ofdToPdfEx(InputStream in) {
|
|
|
|
|
|
|
+ public static class ConvertResult {
|
|
|
|
|
+ public final InputStream stream;
|
|
|
|
|
+ public final String errorMessage; // null 表示成功
|
|
|
|
|
+ public ConvertResult(InputStream stream, String errorMessage) {
|
|
|
|
|
+ this.stream = stream;
|
|
|
|
|
+ this.errorMessage = errorMessage;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static ConvertResult ofdToPdfWithResult(InputStream in) {
|
|
|
ByteArrayOutputStream pdfOut = new ByteArrayOutputStream();
|
|
ByteArrayOutputStream pdfOut = new ByteArrayOutputStream();
|
|
|
try {
|
|
try {
|
|
|
- FontLoader.getInstance()
|
|
|
|
|
- .addAliasMapping(null, "小标宋体", "方正小标宋简体", "方正小标宋简体")
|
|
|
|
|
- .addAliasMapping(null, "KaiTi_GB2312", "楷体", "楷体")
|
|
|
|
|
- .addSimilarFontReplaceRegexMapping(null, ".*Kai.*", null, "楷体")
|
|
|
|
|
- .addSimilarFontReplaceRegexMapping(null, ".*SimSun.*", null, "SimSun")
|
|
|
|
|
- .addSimilarFontReplaceRegexMapping(null, ".*Song.*", null, "宋体")
|
|
|
|
|
- .addSimilarFontReplaceRegexMapping(null, ".*MinionPro.*", null, "SimSun");
|
|
|
|
|
-
|
|
|
|
|
- FontLoader.setSimilarFontReplace(true);
|
|
|
|
|
|
|
+ configureOfdFonts();
|
|
|
ConvertHelper.toPdf(in, pdfOut);
|
|
ConvertHelper.toPdf(in, pdfOut);
|
|
|
- return new ByteArrayInputStream(pdfOut.toByteArray());
|
|
|
|
|
|
|
+ return new ConvertResult(new ByteArrayInputStream(pdfOut.toByteArray()), null);
|
|
|
|
|
+ } catch (IllegalArgumentException e) {
|
|
|
|
|
+ String msg = e.getMessage() == null ? "" : e.getMessage();
|
|
|
|
|
+ if (msg.contains("容器名称") || msg.contains("name")) {
|
|
|
|
|
+ return new ConvertResult(null,
|
|
|
|
|
+ "OFD 文件附件结构异常(附件名称为空),无法转换,请使用标准 OFD 工具重新保存后再试。");
|
|
|
|
|
+ }
|
|
|
|
|
+ return new ConvertResult(null, "OFD 转 PDF 失败:" + msg);
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
e.printStackTrace();
|
|
e.printStackTrace();
|
|
|
|
|
+ return new ConvertResult(null, "OFD 转 PDF 失败:" + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 保留原方法,内部调用新方法,失败返回 null
|
|
|
|
|
+ public static InputStream ofdToPdfEx(InputStream in) {
|
|
|
|
|
+ return ofdToPdfWithResult(in).stream;
|
|
|
|
|
+ }
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 执行与正式接口相同的转换,并验证转换产物可被 PDFBox 读取。
|
|
|
|
|
+ * @return 预检失败原因;可转换时返回空列表
|
|
|
|
|
+ */
|
|
|
|
|
+ public static List<String> checkOfdToPdf(InputStream input) throws IOException {
|
|
|
|
|
+ byte[] ofdBytes = input.readAllBytes();
|
|
|
|
|
+ if (!isZipPackage(ofdBytes)) {
|
|
|
|
|
+ return List.of("文件不是有效的OFD压缩包或压缩包已损坏。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<String> issues = new ArrayList<>();
|
|
|
|
|
+ String attachmentRisk = detectOfdAttachmentRisk(new ByteArrayInputStream(ofdBytes));
|
|
|
|
|
+ if (attachmentRisk != null) {
|
|
|
|
|
+ return List.of(attachmentRisk);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ByteArrayOutputStream pdfOutput = new ByteArrayOutputStream();
|
|
|
|
|
+ try {
|
|
|
|
|
+ configureOfdFonts();
|
|
|
|
|
+ ConvertHelper.toPdf(new ByteArrayInputStream(ofdBytes), pdfOutput);
|
|
|
|
|
+ } catch (RuntimeException e) {
|
|
|
|
|
+ issues.add("OFD结构、页面内容或所需资源无法转换为PDF");
|
|
|
|
|
+ return List.copyOf(issues);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ byte[] pdfBytes = pdfOutput.toByteArray();
|
|
|
|
|
+ if (pdfBytes.length == 0) {
|
|
|
|
|
+ issues.add("OFD转换未生成PDF内容。");
|
|
|
|
|
+ return List.copyOf(issues);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try (PDDocument document = PDDocument.load(pdfBytes)) {
|
|
|
|
|
+ if (document.getNumberOfPages() == 0) {
|
|
|
|
|
+ issues.add("OFD未生成包含页面的有效PDF。");
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ issues.add("OFD转换生成的PDF无效");
|
|
|
|
|
+ }
|
|
|
|
|
+ return List.copyOf(issues);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static String detectOfdAttachmentRisk(InputStream in) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ byte[] ofdBytes = in.readAllBytes();
|
|
|
|
|
+ byte[] attachXml = null;
|
|
|
|
|
+ try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(ofdBytes))) {
|
|
|
|
|
+ ZipEntry entry;
|
|
|
|
|
+ while ((entry = zis.getNextEntry()) != null) {
|
|
|
|
|
+ String name = entry.getName().replace('\\', '/');
|
|
|
|
|
+ String normalizedName = name.toLowerCase(java.util.Locale.ROOT);
|
|
|
|
|
+ if (normalizedName.equals("attachments.xml")
|
|
|
|
|
+ || normalizedName.endsWith("/attachments/attachments.xml")
|
|
|
|
|
+ || normalizedName.endsWith("/attachs/attachments.xml")) {
|
|
|
|
|
+ attachXml = zis.readAllBytes();
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (attachXml == null) {
|
|
|
|
|
+ return null; // 没有附件,无风险
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
|
|
|
|
+ factory.setNamespaceAware(true);
|
|
|
|
|
+ factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
|
|
|
|
+ factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
|
|
|
|
+ factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
|
|
|
|
+ factory.setAttribute(javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD, "");
|
|
|
|
|
+ factory.setAttribute(javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
|
|
|
|
|
+ Document doc = factory.newDocumentBuilder()
|
|
|
|
|
+ .parse(new ByteArrayInputStream(attachXml));
|
|
|
|
|
+
|
|
|
|
|
+ // 不区分命名空间找 Attachment 元素
|
|
|
|
|
+ NodeList attachments = doc.getElementsByTagNameNS("*", "Attachment");
|
|
|
|
|
+ for (int i = 0; i < attachments.getLength(); i++) {
|
|
|
|
|
+ Element el = (Element) attachments.item(i);
|
|
|
|
|
+ String name = el.getAttribute("Name");
|
|
|
|
|
+ if (name == null || name.isBlank()) {
|
|
|
|
|
+ return "OFD 文件附件名称为空,转换可能失败,建议使用标准 OFD 工具重新保存后再上传。";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ return "OFD 附件清单无法解析,无法确认附件名称是否有效:" + describeFailure(e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void configureOfdFonts() {
|
|
|
|
|
+ FontLoader.getInstance()
|
|
|
|
|
+ .addAliasMapping(null, "小标宋体", "方正小标宋简体", "方正小标宋简体")
|
|
|
|
|
+ .addAliasMapping(null, "KaiTi_GB2312", "楷体", "楷体")
|
|
|
|
|
+ .addSimilarFontReplaceRegexMapping(null, ".*Kai.*", null, "楷体")
|
|
|
|
|
+ .addSimilarFontReplaceRegexMapping(null, ".*SimSun.*", null, "SimSun")
|
|
|
|
|
+ .addSimilarFontReplaceRegexMapping(null, ".*Song.*", null, "宋体")
|
|
|
|
|
+ .addSimilarFontReplaceRegexMapping(null, ".*MinionPro.*", null, "SimSun");
|
|
|
|
|
+ FontLoader.setSimilarFontReplace(true);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static boolean isZipPackage(byte[] bytes) {
|
|
|
|
|
+ return bytes.length >= 4
|
|
|
|
|
+ && bytes[0] == 'P'
|
|
|
|
|
+ && bytes[1] == 'K'
|
|
|
|
|
+ && ((bytes[2] == 3 && bytes[3] == 4)
|
|
|
|
|
+ || (bytes[2] == 5 && bytes[3] == 6)
|
|
|
|
|
+ || (bytes[2] == 7 && bytes[3] == 8));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String describeFailure(Throwable failure) {
|
|
|
|
|
+ Throwable cause = failure;
|
|
|
|
|
+ while (cause.getCause() != null && cause.getCause() != cause) {
|
|
|
|
|
+ cause = cause.getCause();
|
|
|
|
|
+ }
|
|
|
|
|
+ String message = cause.getMessage();
|
|
|
|
|
+ return message == null || message.isBlank()
|
|
|
|
|
+ ? cause.getClass().getSimpleName()
|
|
|
|
|
+ : message;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 预检 PDF 字体结构是否存在多次转换后的损坏迹象
|
|
|
|
|
+ * @return 风险描述;无风险返回 null
|
|
|
|
|
+ */
|
|
|
|
|
+ public static String detectPdfFontRisk(InputStream input) throws IOException {
|
|
|
|
|
+ try (PDDocument doc = PDDocument.load(input)) {
|
|
|
|
|
+ // 逐字体收集到的风险特征
|
|
|
|
|
+ boolean anyMissingToUnicode = false;
|
|
|
|
|
+ boolean anyMissingOS2 = false;
|
|
|
|
|
+ boolean anyMissingCmap = false;
|
|
|
|
|
+ boolean anyType1Or3 = false;
|
|
|
|
|
+ boolean anyCffFont = false;
|
|
|
|
|
+ boolean anyUnknownSubtype = false;
|
|
|
|
|
+ int embeddedFontCount = 0;
|
|
|
|
|
+
|
|
|
|
|
+ for (PDPage page : doc.getPages()) {
|
|
|
|
|
+ PDResources resources = page.getResources();
|
|
|
|
|
+ if (resources == null) continue;
|
|
|
|
|
+
|
|
|
|
|
+ for (COSName fontName : resources.getFontNames()) {
|
|
|
|
|
+ PDFont font;
|
|
|
|
|
+ try {
|
|
|
|
|
+ font = resources.getFont(fontName);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ // 字体本身加载失败,也算风险
|
|
|
|
|
+ anyUnknownSubtype = true;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (font == null) continue;
|
|
|
|
|
+ COSDictionary fontDict = font.getCOSObject();
|
|
|
|
|
+
|
|
|
|
|
+ // 字体子类型
|
|
|
|
|
+ COSName subtypeName = fontDict.getCOSName(COSName.SUBTYPE);
|
|
|
|
|
+ String subtype = subtypeName == null ? "" : subtypeName.getName();
|
|
|
|
|
+
|
|
|
|
|
+ // ---- 1. Type1 / Type3:转 OFD 高风险 ----
|
|
|
|
|
+ if ("Type1".equals(subtype)
|
|
|
|
|
+ || "MMType1".equals(subtype)
|
|
|
|
|
+ || "Type3".equals(subtype)) {
|
|
|
|
|
+ anyType1Or3 = true;
|
|
|
|
|
+ continue; // 这两类不做后面的嵌入检查,风险已足够高
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---- 2. ToUnicode 检查(所有类型都查)----
|
|
|
|
|
+ if (!fontDict.containsKey(COSName.TO_UNICODE)) {
|
|
|
|
|
+ anyMissingToUnicode = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---- 3. 找到真正的字体描述符和字体文件流 ----
|
|
|
|
|
+ COSDictionary desc = findFontDescriptor(fontDict);
|
|
|
|
|
+ if (desc == null) {
|
|
|
|
|
+ // 非嵌入字体,跳过表结构检查
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 嵌入字体计数
|
|
|
|
|
+ COSBase fontFile = desc.getDictionaryObject(COSName.FONT_FILE); // Type1
|
|
|
|
|
+ COSBase fontFile2 = desc.getDictionaryObject(COSName.FONT_FILE2); // TrueType
|
|
|
|
|
+ COSBase fontFile3 = desc.getDictionaryObject(COSName.FONT_FILE3); // CFF/OpenType
|
|
|
|
|
+
|
|
|
|
|
+ if (fontFile == null && fontFile2 == null && fontFile3 == null) {
|
|
|
|
|
+ // 声明了 FontDescriptor 但没有嵌入文件,跳过
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ embeddedFontCount++;
|
|
|
|
|
+
|
|
|
|
|
+ // ---- 4. CFF / OpenType (FontFile3) ----
|
|
|
|
|
+ if (fontFile3 instanceof COSStream) {
|
|
|
|
|
+ anyCffFont = true;
|
|
|
|
|
+ // CFF 的表结构和 TrueType 不同,不做 OS/2 / cmap 检查
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---- 5. TrueType (FontFile2):检查 OS/2 和 cmap ----
|
|
|
|
|
+ if (fontFile2 instanceof COSStream) {
|
|
|
|
|
+ TrueTypeTables tables;
|
|
|
|
|
+ try (InputStream fs = ((COSStream) fontFile2).createInputStream()) {
|
|
|
|
|
+ tables = parseTrueTypeTables(fs.readAllBytes());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ // 解析失败,视为风险
|
|
|
|
|
+ anyMissingOS2 = true;
|
|
|
|
|
+ anyMissingCmap = true;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (tables == null) {
|
|
|
|
|
+ anyMissingOS2 = true;
|
|
|
|
|
+ anyMissingCmap = true;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (!tables.hasOS2) anyMissingOS2 = true;
|
|
|
|
|
+ if (!tables.hasCmap) anyMissingCmap = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 汇总,按严重程度输出 =====
|
|
|
|
|
+ List<String> highRisks = new ArrayList<>();
|
|
|
|
|
+ List<String> midRisks = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ if (anyType1Or3) highRisks.add("Type1/Type3 字体");
|
|
|
|
|
+ if (anyMissingOS2) highRisks.add("字体缺少 OS/2 表");
|
|
|
|
|
+ if (anyMissingCmap) highRisks.add("字体缺少 cmap 表");
|
|
|
|
|
+ if (anyMissingToUnicode) highRisks.add("字体缺少 Unicode 映射");
|
|
|
|
|
+ if (anyCffFont) midRisks.add("CFF/OpenType 字体");
|
|
|
|
|
+ if (anyUnknownSubtype) midRisks.add("存在无法加载的字体");
|
|
|
|
|
+
|
|
|
|
|
+ if (!highRisks.isEmpty()) {
|
|
|
|
|
+ return "检测到高风险字体特征(" + String.join("、", highRisks)
|
|
|
|
|
+ + "),转换为 OFD 后可能出现乱码或黑块,建议使用原始文件重新生成后再上传。";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!midRisks.isEmpty()) {
|
|
|
|
|
+ return "检测到中等风险字体特征(" + String.join("、", midRisks)
|
|
|
|
|
+ + "),转换后可能出现文字渲染异常,请预览确认结果。";
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ throw e;
|
|
|
|
|
+ } catch (RuntimeException e) {
|
|
|
|
|
+ throw new IOException("PDF字体结构无法检测", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** TrueType 表结构解析结果 */
|
|
|
|
|
+ private static class TrueTypeTables {
|
|
|
|
|
+ boolean hasOS2;
|
|
|
|
|
+ boolean hasCmap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析 TrueType/OpenType 字体的表目录,提取关心的表是否存在。
|
|
|
|
|
+ * 返回 null 表示不是合法 sfnt 结构。
|
|
|
|
|
+ */
|
|
|
|
|
+ private static TrueTypeTables parseTrueTypeTables(byte[] fontBytes) {
|
|
|
|
|
+ try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(fontBytes))) {
|
|
|
|
|
+ int sfntVersion = in.readInt();
|
|
|
|
|
+ // 合法 sfnt 版本:0x00010000 (TrueType), 'OTTO' (CFF OpenType), 'true', 'typ1'
|
|
|
|
|
+ if (sfntVersion != 0x00010000
|
|
|
|
|
+ && sfntVersion != 0x4F54544F // 'OTTO'
|
|
|
|
|
+ && sfntVersion != 0x74727565 // 'true'
|
|
|
|
|
+ && sfntVersion != 0x74797031) { // 'typ1'
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ int numTables = in.readUnsignedShort();
|
|
|
|
|
+ in.readUnsignedShort(); // searchRange
|
|
|
|
|
+ in.readUnsignedShort(); // entrySelector
|
|
|
|
|
+ in.readUnsignedShort(); // rangeShift
|
|
|
|
|
+
|
|
|
|
|
+ TrueTypeTables result = new TrueTypeTables();
|
|
|
|
|
+ for (int i = 0; i < numTables; i++) {
|
|
|
|
|
+ int tag = in.readInt();
|
|
|
|
|
+ in.readInt(); // checksum
|
|
|
|
|
+ in.readInt(); // offset
|
|
|
|
|
+ in.readInt(); // length
|
|
|
|
|
+ if (tag == 0x4F532F32) result.hasOS2 = true; // 'OS/2'
|
|
|
|
|
+ if (tag == 0x636D6170) result.hasCmap = true; // 'cmap'
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从字体字典中找到 FontDescriptor。
|
|
|
|
|
+ * 对 Type0(复合字体),需要深入 DescendantFonts 找。
|
|
|
|
|
+ */
|
|
|
|
|
+ private static COSDictionary findFontDescriptor(COSDictionary fontDict) {
|
|
|
|
|
+ // 普通字体直接有 FontDescriptor
|
|
|
|
|
+ COSBase desc = fontDict.getDictionaryObject(COSName.FONT_DESC);
|
|
|
|
|
+ if (desc instanceof COSDictionary) {
|
|
|
|
|
+ return (COSDictionary) desc;
|
|
|
|
|
+ }
|
|
|
|
|
+ // Type0:从 DescendantFonts[0] 里找
|
|
|
|
|
+ COSBase descendantsBase = fontDict.getDictionaryObject(COSName.DESCENDANT_FONTS);
|
|
|
|
|
+ if (descendantsBase instanceof COSArray) {
|
|
|
|
|
+ COSArray descendants = (COSArray) descendantsBase;
|
|
|
|
|
+ if (descendants.size() > 0) {
|
|
|
|
|
+ COSBase first = descendants.getObject(0);
|
|
|
|
|
+ if (first instanceof COSDictionary) {
|
|
|
|
|
+ COSBase d = ((COSDictionary) first).getDictionaryObject(COSName.FONT_DESC);
|
|
|
|
|
+ if (d instanceof COSDictionary) {
|
|
|
|
|
+ return (COSDictionary) d;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ private static boolean isEmbeddedTrueType(COSDictionary fontDict) {
|
|
|
|
|
+ COSName subtype = fontDict.getCOSName(COSName.SUBTYPE);
|
|
|
|
|
+ if (subtype == null) return false;
|
|
|
|
|
+ String name = subtype.getName();
|
|
|
|
|
+ if (!"TrueType".equals(name) && !"Type0".equals(name)) return false;
|
|
|
|
|
+ // Type0 需进一步看 DescendantFonts,这里简化处理
|
|
|
|
|
+ return fontDict.containsKey(COSName.FONT_DESC)
|
|
|
|
|
+ || fontDict.containsKey(COSName.DESCENDANT_FONTS);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static boolean hasOS2Table(COSDictionary fontDict) {
|
|
|
|
|
+ // 从 FontDescriptor 中拿 FontFile2(TrueType)或 FontFile3(OpenType/CFF)
|
|
|
|
|
+ COSDictionary desc = null;
|
|
|
|
|
+ if (fontDict.containsKey(COSName.FONT_DESC)) {
|
|
|
|
|
+ desc = (COSDictionary) fontDict.getDictionaryObject(COSName.FONT_DESC);
|
|
|
|
|
+ } else if (fontDict.containsKey(COSName.DESCENDANT_FONTS)) {
|
|
|
|
|
+ COSArray descendants = (COSArray) fontDict.getDictionaryObject(COSName.DESCENDANT_FONTS);
|
|
|
|
|
+ if (descendants != null && descendants.size() > 0) {
|
|
|
|
|
+ COSDictionary df = (COSDictionary) descendants.getObject(0);
|
|
|
|
|
+ desc = (COSDictionary) df.getDictionaryObject(COSName.FONT_DESC);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (desc == null) return true; // 非嵌入字体,不做 OS/2 检测
|
|
|
|
|
+
|
|
|
|
|
+ COSBase fontFile = desc.getDictionaryObject(COSName.FONT_FILE2);
|
|
|
|
|
+ if (fontFile == null) {
|
|
|
|
|
+ fontFile = desc.getDictionaryObject(COSName.FONT_FILE3);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!(fontFile instanceof COSStream)) return true;
|
|
|
|
|
+
|
|
|
|
|
+ try (InputStream fontStream = ((COSStream) fontFile).createInputStream()) {
|
|
|
|
|
+ byte[] fontBytes = fontStream.readAllBytes();
|
|
|
|
|
+ return TrueTypeFontParser.hasOS2Table(fontBytes);
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ throw new RuntimeException(e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|