java基于PDF底层内容流的解析对文本内容进行编辑

本文实现了基于坐标位置对PDF内容的底层修改而非覆盖,因此不会出现在某些高级PDF编辑器中可以移除插入内容或者文件随着编辑次数增多而大幅增大(原因是原内容还在文件中)的问题,而且使用的pdfbox是一个开源的、免费的PDF处理库,不需要开源自己的代码或者付费

 一、操作原理

 1、内容流

PDF文档的核心是其“内容流”(Content Stream),这是PDF页面上的所有对象和指令的集合。内容流可以看作一系列指令,这些指令告诉PDF阅读器如何绘制页面上的图形、文本、路径等元素。通过解析和修改这些内容流,能够精准地控制页面上的布局和内容。

 1.1、查看——qpdf工具使用

`qpdf`是一款开源的PDF处理工具,它能够将PDF文件转换成易于人类阅读的格式,并且展示内容流。可以在解压后的qpdf bin目录下使用`qpdf --qdf input.pdf output.qdf`将PDF文件解析为QDF格式,使用文本编辑器打开output.qdf,这样便可以查看其底层内容结构,包括内容流的详细指令。可以通过在代码中设置特殊数值的指令去确定编辑文本内容的指令位置,比如设置 0.0 0.0 1.0 rg(含义见后续 操作符理解)
结果类似:

下载地址:https://github.com/qpdf/qpdf/releases

 1.2、理解——操作符理解

在内容流中,PDF使用了操作符来定义绘制图形、文本及其他内容的行为,理解这些操作符的功能及其顺序是编辑内容流的关键。在操作流中,每个操作符都有自己的作用,比如文本操作符、图形和页面设置相关操作符、其他操作符等,并且它们通常结合使用来实现复杂的排版效果,需要注意,操作符直接是存在顺序的,不同的顺序可能会导致不可预料的不一样的效果,甚至可能无法生效,因此在查看pdf底层内容流结构时需要注意各操作符之间的顺序,在自己的代码中插入对应操作符时也应当按照相同的顺序插入。

常见的文本操作符

 1. `BT` 和 `ET` (Begin Text/End Text)
- `BT`:表示开始一个新的文本对象。文本的所有绘制操作都应位于`BT`和`ET`之间。所有的文本操作(例如字体设置、位置移动等)都需要在这个范围内进行。
- `ET`:表示结束当前的文本对象,之后的操作将不再影响文本。

 2. `Tf` (Text Font and Size)
- 用于设置文本字体和字体大小。语法为:`/FontName FontSize Tf`。
  - `FontName`:字体名称,通常是PDF中定义的字体资源。
  - `FontSize`:字体大小。

  例如,`/F1 12 Tf` 表示将当前的字体设置为`F1`,大小为12。

 3. `Tm` (Text Matrix)
- 用于设置文本矩阵,指定文本的缩放、旋转、位移等变换操作。文本矩阵用于确定文本块的变换效果,包括其位置和方向。语法为:`a b c d e f Tm`。
    - `a`、`b`:水平方向缩放和旋转。
    - `c`、`d`:垂直方向缩放和旋转。
    - `e`、`f`:表示文本对象的X、Y位置(坐标)。

 4. `Tj` (Show Text)
- 显示单行文本。语法为:`(text) Tj`,其中`text`表示要显示的字符串。(text) Tj表示直接显示ASCII或Unicode文本,文本内容以标准字符编码的形式写在括号中,<381a3e7c1afd058c02c8> Tj 表示文本的十六进制编码。PDF支持通过这种方式将字符串编码为字节流,然后通过设置的字体字形表来解释这些字节并将其显示为文本
  例如:`(Hello, PDF!) Tj` 会在当前坐标处显示“Hello, PDF!”。

 5. `TJ` (Show Text with Individual Positioning)
- 与`Tj`类似,但允许为每个字符或字符组设置不同的间距,通常用于调整字符的精确位置。语法为:`[array] TJ`,其中`array`是字符串和调整量的组合。
  例如:`[(Hello) 120 (PDF)] TJ`,表示在“Hello”后移动120单位,然后显示“PDF”。

 6. `Td` 和 `TD` (Move Text Position)
- `Td`:用于在当前文本位置基础上,移动新的位置,语法为:`x y Td`,其中`x`和`y`是移动的水平和垂直距离。
  例如:`10 20 Td`表示将文本的位置向右移动10个单位,向上移动20个单位。
- `TD`:与`Td`相同,但它同时会将文本的行间距重置为新的垂直位移。

 7. `Tm` (Set Text Matrix)
- 这个操作符直接设置文本的矩阵。它提供了精确的位置和变换控制。通常和`Td`一起使用,它指定文本块的位置和缩放。
  
 8. `Tr` (Text Rendering Mode)
- 用于设置文本的渲染模式,语法为:`n Tr`。
  - `n`的取值有:
    - `0`:仅填充文本。
    - `1`:仅描边文本。
    - `2`:同时填充和描边文本。
    - `3`:文本不渲染(但保留空间)。
  
 9. `Tw` (Word Spacing)
- 设置词间距,语法为:`w Tw`。将指定的间距应用到文本对象中的空格字符。
  例如:`10 Tw`表示设置单词之间的间距为10个单位。

 10. `Tc` (Character Spacing)
- 设置字符之间的间距,语法为:`c Tc`。它为文本对象中的每个字符之间增加固定的间距。

 11. `T*` (Move to Next Line)
- 将文本位置移动到下一行,使用的是当前的行间距和文本矩阵。通常用于实现多行文本。

 12. `rg` 和 `RG` (Set RGB Color for Filling/Stroking)
- `rg`:设置填充操作的RGB颜色,语法为:`r g b rg`,其中`r`、`g`、`b`的取值范围是`0`到`1`。
- `RG`:设置描边操作的RGB颜色,语法与`rg`相同。

 13. gG (Set Gray Color for Filling/Stroking)
- `g`:设置灰度填充颜色,语法为:gray g,其中gray的值在01之间,0表示黑色,1表示白色。
- `G`:设置灰度描边颜色,语法与g相同

  14. w (Set Line Width)
- 设置描边线条的宽度。语法为:lineWidth w,例如2 w表示设置线宽为2个单位。

 1.3、扩展——操作后word转pdf

当我们将Word文件转换成PDF时,Word中的文本和图形也会被转换成PDF的内容流。通过对比转换前后的内容,我们可以进一步理解这些操作符的使用。Word文件通常包含较多的复杂格式化信息,这些会被转换成复杂的内容流指令,例如嵌入字体、图像、文本排版等。因此假如要实现某个操作效果,比如加粗或者斜体,则可以先在word中进行操作,然后使用qpdf转为可理解的qdf格式,查看加粗或斜体对应底层内容流的操作符是什么,是如何设值的,进而在自己的代码中照例插入
比如某个坐标的文本内容为斜体,在word中设置后,转换为qdf查看对比可见,1 0 0.3333 1 111.14 742.66 Tm  其中 0.3333 可以理解为倾斜度的一个数值表示(Tm的各项矩阵数值含义见上述 操作符理解)

 1.4、验证——修改qdf文件查看

通过`qpdf`将PDF转换为QDF格式后,可以手动修改其内容流,例如删除某些操作符、添加新的指令,保存后再用PDF查看器打开以验证修改是否生效。这种实时的修改和验证有助于深入理解内容流的结构和PDF的解析逻辑。

2、处理内容流

 2.1、构造内容流和坐标的对应

(重写PDFTextStripper中的部分方法后与原内容流进行比对处理)
PDFBox中的`PDFTextStripper`类负责提取PDF中的文本内容。通过继承PDFTextStripper重写其中的processOperator(处理操作符)、processTextPosition(处理文本信息,它会在processOperator方法后自动调用)方法,可以获取操作符下标以及每个文本字符的坐标信息,并将其与原始内容流中的纯操作符进行顺序比对和代码处理,即可得到原始内容流和文本坐标的集合,具体可见后续操作代码

 2.2、坐标匹配获取操作符下标

为了编辑PDF中的文本,我们首先需要定位文本在内容流中的位置。通过将输入的坐标位置与PDF内容流中的文本字符坐标进行匹配,可以精确定位到输入坐标在内容流中的位置。

 2.3、对匹配的内容进行修改重组

内容的修改与重组是基于操作符的解析和插入进行的,区分普通文本内容(Tj)和带有字符间距的文本数组(TJ)内容。

2.3.1、删除原匹配文本内容

一旦找到了需要修改的文本块,首先是从内容流中删除原来的文本。可以通过操作符的索引直接将其从内容流中移除。

2.3.2、保持前缀内容重新插入

在删除原文本后,我们通常需要保留匹配坐标前面的文本内容和一些布局信息(例如坐标、字体设置等),这些前缀内容不应被改变,它可以维持前缀的文本内容的布局信息。

2.3.3、自定义新内容插入

在插入新文本时,需要考虑到字体的设置。如果PDF中的字体没有包含新文本所需的字符,可能会导致显示问题,因此通常选择使用系统字体来确保文本正确显示。插入时需要根据坐标和字体等自定义的参数来绘制新的文本块。

2.3.4、后缀内容状态回退插入

由于PDF是通过操作符逐步构建页面的,因此在插入新内容后文本状态可能会发送改变,必须恢复之前的状态再插入原本的后缀内容。这个过程需要回退到匹配文本位置之前的状态,例如字体、坐标、颜色等,然后按重新插入后续内容。

二、使用PDFbox进行操作

1、引入pdfbox依赖

        <dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox</artifactId><version>2.0.16</version></dependency>

2、重写PDFTextStripper中的方法

import org.apache.pdfbox.contentstream.operator.Operator;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;public class TokenIndexPDFTextStripper extends PDFTextStripper {// 存储每个操作符和参数的下标信息private List<Object> tokenList = new ArrayList<>();public TokenIndexPDFTextStripper() throws IOException {super();}@Overrideprotected void processOperator(Operator operator, List<COSBase> arguments) throws IOException {// 遍历并记录参数for (COSBase argument : arguments) {tokenList.add(argument);}// 记录操作符下标tokenList.add(operator);// 调用父类的方法以确保文本提取继续正常进行super.processOperator(operator, arguments);}// 扩展方法,获取文本坐标@Overrideprotected void processTextPosition(TextPosition text) {HashMap<String, Object> textPosition = new HashMap<>();textPosition.put("text",text.getUnicode());// 字符的左下角位置坐标// 从左往右的水平位移textPosition.put("x",text.getXDirAdj());// 从上往下的垂直位移textPosition.put("y",text.getYDirAdj());textPosition.put("width",text.getWidthDirAdj());textPosition.put("height",text.getHeightDir());textPosition.put("pageHeight",text.getPageHeight());textPosition.put("pageWidth",text.getPageWidth());// 将文本位置信息添加到 tokenList 中tokenList.add(textPosition);System.out.print("\033[31m"+text.getUnicode()+" \033[0m"+text.getXDirAdj()+","+text.getYDirAdj()+" ");super.processTextPosition(text);}public List<Object> getTokenList() {return tokenList;}
}

3、处理pdf的主方法

/*** PDF编辑VO*/
public class PDFEditVO {/*** x坐标*/private Float cursorX;/*** y坐标*/private Float cursorY;/*** 需要插入的文本*/private String insertText;/*** 需要删除的字符数*/private Integer deleteNum;/*** PDF文件路径*/private String path;/*** 字体名称*/private String fontName;/*** 字号大小*/private Integer fontSize;/*** 字体颜色,比如蓝色为0,0,1*/private List<Float> color;/*** 线宽,一般加粗为 1.0*/private Float lineWidth;/*** 文本绘制模式(0 填充模式/默认模式;1 描边模式/文本空心描边;2 填充并描边模式;3 不填充不描边模式/不可见;。。。)*/private String textRenderingMode;/*** 倾斜率,一般斜体为0.3333*/private Float tiltRatio;public Float getCursorX() {return cursorX;}public Float getCursorY() {return cursorY;}public String getInsertText() {return insertText;}public Integer getDeleteNum() {return deleteNum;}public String getPath() {return path;}public String getFontName() {return fontName;}public Integer getFontSize() {return fontSize;}public List<Float> getColor() {return color;}public Float getLineWidth() {return lineWidth;}public String getTextRenderingMode() {return textRenderingMode;}public Float getTiltRatio() {return tiltRatio;}
}
public void editPDF(PDFEditVO editVO) {String path = editVO.getPath();List<Float> colors = editVO.getColor();String outputFilePath = "output.pdf";Float cursorX = editVO.getCursorX();Float cursorY = editVO.getCursorY();Integer deleteNum = editVO.getDeleteNum();String insertText = editVO.getInsertText();String fontName = editVO.getFontName();Integer fontSize = editVO.getFontSize();Float lineWidth = editVO.getLineWidth();String textRenderingMode = editVO.getTextRenderingMode();Float tiltRatio = editVO.getTiltRatio();try {PDDocument document = PDDocument.load(new File(path));Font[] allFonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts();PDPageTree pages = document.getPages();for (int pageItem = 0; pageItem < pages.getCount(); pageItem++) {PDPage page = pages.get(pageItem);// 获取页面的内容流集合PDFStreamParser parser = new PDFStreamParser(page);parser.parse();List<Object> tokens = parser.getTokens();// 获取带有文本位置的内容流集合TokenIndexPDFTextStripper stripper = new TokenIndexPDFTextStripper();stripper.setSortByPosition(true);  // 按位置排序提取文本stripper.setStartPage(pageItem + 1);  // 设置开始页stripper.setEndPage(pageItem + 1);    // 设置结束页stripper.getText(document);  // 提取文本并记录每个操作符的下标List<Object> tokenList = stripper.getTokenList();// 过滤多余操作符的处理(PDFTextStripper会将某些操作符进行转换处理,比如0 -11.664 TD-》11.664 TL 0 -11.664 Td,需要将这些多余内容过滤但同时保留文本位置信息)List<List<Object>> tokens1 = new ArrayList<>();int index = 0;for (Object object : tokenList) {if (object instanceof HashMap) {tokens1.add(Arrays.asList(object, index - 1));} else {if (object.toString().equals(tokens.get(index).toString())) {tokens1.add(Arrays.asList(tokens.get(index), index));index++;}}}// 根据接口参数确定光标位置对应的tokens下标int tokenIndex = 0;Float insertCharX = null;Float insertCharY = null;StringBuffer preString = new StringBuffer();StringBuffer suffixString = new StringBuffer();for (int i = 0; i < tokens1.size(); i++) {List<Object> objects = tokens1.get(i);if (objects.get(0) instanceof HashMap) {HashMap<String, Object> hashMap = (HashMap<String, Object>)objects.get(0);Float x = (Float)hashMap.get("x");Float y = (Float)hashMap.get("y");Float width = (Float)hashMap.get("width");Float height = (Float)hashMap.get("height");if ((cursorX >= x && cursorX <= (x + width)) && (cursorY >= y && cursorY <= (y + height))) {tokenIndex = (int)objects.get(1);for (int j = 0; j < tokens1.size(); j++) {List<Object> objects1 = tokens1.get(j);// 以存储的tokens下标作为匹配条件int storageIndex = (int)objects1.get(1);if (storageIndex == tokenIndex){if (objects1.get(0) instanceof HashMap) {HashMap<String, Object> hashMap1 = (HashMap<String, Object>)objects1.get(0);String text1 = String.valueOf(hashMap1.get("text"));Float x1 = (Float)hashMap1.get("x");Float y1 = (Float)hashMap1.get("y");Float width1 = (Float)hashMap1.get("width");Float pageHeight1 = (Float)hashMap1.get("pageHeight");if (Objects.isNull(insertCharX)||Objects.isNull(insertCharY)){insertCharX = x1;insertCharY = pageHeight1- y1;}// 不拼接指定匹配坐标位置往前,deleteNum个数的字符if (j <= i - deleteNum) {preString.append(text1);insertCharX += width1;}else if (j > i){suffixString.append(text1);}}}}break;}}}if (tokenIndex == 0) {System.out.println("cant find cursorX, cursorY");return;}// 根据tokens下标,对内容流进行处理PDResources resources = page.getResources();COSName sysFontName = getSysFontName(document, allFonts, resources, fontName);Object indexToken = tokens.get(tokenIndex);if (indexToken instanceof Operator) {Operator op = (Operator)indexToken;String opName = op.getName();if (OperatorName.SHOW_TEXT.equals(opName)) {// 记录当前匹配的Tj之上到BT的内容(既用于设置新的文本绘制块,也用于设置状态回退操作符)List<List<Object>> ascPreTokensAll = getPreTokensAll(tokens, tokenIndex);List<List<Object>> descPreTokensAll = new ArrayList<>(ascPreTokensAll);Collections.reverse(descPreTokensAll);Optional<List<Object>> first = descPreTokensAll.stream().filter(item -> item.stream().anyMatch(item1 -> item1 instanceof Operator && OperatorName.SET_FONT_AND_SIZE.equals(((Operator)item1).getName()))).findFirst();if (first.isPresent()) {List<Object> objects = first.get();COSName currentFontName = (COSName)objects.get(0);PDFont currentFont = resources.getFont(currentFontName);if (StringUtils.hasText(insertText)) {tokens.remove(tokenIndex);tokens.remove(tokenIndex-1);int newNum = tokenIndex-1;// 插入原本的前缀文本绘制块if (StringUtils.hasText(preString)) {byte[] encode = currentFont.encode(preString.toString());tokens.add(newNum++, new COSString(encode));tokens.add(newNum++, Operator.getOperator(OperatorName.SHOW_TEXT));}// 插入新插入的文本绘制块// w 设置线宽tokens.add(newNum++, new COSFloat(lineWidth)); // 线宽tokens.add(newNum++, Operator.getOperator(OperatorName.SET_LINE_WIDTH));// Tf 设置字体和字号tokens.add(newNum++, sysFontName); // 字体名称设置,需要采用系统字体,否则可能字形不存在tokens.add(newNum++, new COSFloat(fontSize)); // 字号tokens.add(newNum++, Operator.getOperator(OperatorName.SET_FONT_AND_SIZE));tokens.add(newNum++, COSNumber.get(textRenderingMode));  // 文本绘制模式tokens.add(newNum++, Operator.getOperator(OperatorName.SET_TEXT_RENDERINGMODE));// Tm 设置文本变换矩阵,用于控制文本缩放倾斜,位移属性// 定义矩阵的参数tokens.add(newNum++, new COSFloat(1));        // 水平缩放因子,表示文本在 X 方向不缩放tokens.add(newNum++, new COSFloat(0));        // 表示文本在 X 方向没有旋转或倾斜tokens.add(newNum++, new COSFloat(tiltRatio));  // 垂直方向的倾斜因子,表示文本在 Y 方向倾斜,产生一个水平扭曲的效果(斜体效果)tokens.add(newNum++, new COSFloat(1));        // 垂直缩放因子,表示文本在 Y 方向不缩放tokens.add(newNum++, new COSFloat(insertCharX));  // X 方向的位置tokens.add(newNum++, new COSFloat(insertCharY));  // Y 方向的位置tokens.add(newNum++, Operator.getOperator(OperatorName.SET_MATRIX));// RG 设置描边颜色tokens.add(newNum++, new COSFloat(colors.get(0)));  // 红色tokens.add(newNum++, new COSFloat(colors.get(1)));  // 绿色tokens.add(newNum++, new COSFloat(colors.get(2)));  // 蓝色tokens.add(newNum++, Operator.getOperator(OperatorName.STROKING_COLOR_RGB));// rg 设置填充颜色tokens.add(newNum++, new COSFloat(colors.get(0)));  // 红色tokens.add(newNum++, new COSFloat(colors.get(1)));  // 绿色tokens.add(newNum++, new COSFloat(colors.get(2)));  // 蓝色tokens.add(newNum++, Operator.getOperator(OperatorName.NON_STROKING_RGB));// Tj 显示文本PDFont sysFont = resources.getFont(sysFontName);tokens.add(newNum++,new COSString(sysFont.encode(insertText))); // 添加新的文本内容tokens.add(newNum++, Operator.getOperator(OperatorName.SHOW_TEXT));// 插入原本的后缀文本绘制块if (StringUtils.hasText(suffixString)) {// 手动状态回退设值tokens.add(newNum++, new COSFloat(0));tokens.add(newNum++, Operator.getOperator(OperatorName.NON_STROKING_GRAY));tokens.add(newNum++, new COSFloat(0));tokens.add(newNum++, Operator.getOperator(OperatorName.STROKING_COLOR_GRAY));tokens.add(newNum++, COSNumber.get(String.valueOf(RenderingMode.FILL.intValue())));tokens.add(newNum++, Operator.getOperator(OperatorName.SET_TEXT_RENDERINGMODE));tokens.add(newNum++, new COSFloat(1));tokens.add(newNum++, new COSFloat(0));tokens.add(newNum++, new COSFloat(0));tokens.add(newNum++, new COSFloat(1));// 获取文本的宽度(单位是字体设计单位的一部分,需要按比例缩放)float stringWidth = sysFont.getStringWidth(insertText);// 计算文本的实际宽度float textWidth = stringWidth * fontSize / 1000;tokens.add(newNum++, new COSFloat(insertCharX + textWidth));tokens.add(newNum++, new COSFloat(insertCharY));tokens.add(newNum++, Operator.getOperator(OperatorName.SET_MATRIX));// 自动状态回退设值(将BT到该匹配坐标之前的字体颜色样式大小等按顺序设置一遍,保证重新回归字体状态)List<String> backStates = Arrays.asList(OperatorName.SET_FONT_AND_SIZE, OperatorName.NON_STROKING_RGB,OperatorName.NON_STROKING_GRAY, OperatorName.STROKING_COLOR_RGB,OperatorName.STROKING_COLOR_GRAY,OperatorName.SET_LINE_WIDTH,OperatorName.SET_TEXT_RENDERINGMODE);List<List<Object>> backTokens = ascPreTokensAll.stream().filter(item -> item.stream().anyMatch(item1 -> item1 instanceof Operator && backStates.contains(((Operator)item1).getName()))).collect(Collectors.toList());for (List<Object> item : backTokens) {for (Object item1 : item) {tokens.add(newNum++, item1);}}// 插入原本的后缀文本内容byte[] encode = currentFont.encode(suffixString.toString());tokens.add(newNum++, new COSString(encode));tokens.add(newNum++, Operator.getOperator(OperatorName.SHOW_TEXT));}} else {byte[] encode = currentFont.encode(preString.toString() + suffixString.toString());tokens.remove(tokenIndex-1);tokens.add(tokenIndex-1,new COSString(encode));}}}else if (OperatorName.SHOW_TEXT_ADJUSTED.equals(opName)){// TODO 处理文本数组(包含间距元素的多段文本数组)时,同理将前缀内容继续保持为文本数组,插入内容重新设置,恢复文本状态后将后缀内容继续保持为文本数组System.out.println("进入文本数组处理");}}PDStream newContentStream = new PDStream(document);try (java.io.OutputStream out = newContentStream.createOutputStream()) {ContentStreamWriter writer = new ContentStreamWriter(out);writer.writeTokens(tokens);}// 将新的内容流设置回页面page.setContents(newContentStream);}document.save(outputFilePath);document.close();System.out.println("操作完成");} catch (Exception e) {e.printStackTrace();}}/*** 获取当前匹配之前至BT的内容流* @param tokens 内容流* @param i 当前匹配的下标* @return 当前匹配之前至BT的内容流*/private List<List<Object>> getPreTokensAll(List<Object> tokens, int i) {List<Integer> preTokenIndex = new ArrayList<>();for (int j = i-1; j > 0; j--) {Object token1 = tokens.get(j);if (token1 instanceof Operator) {Operator op1 = (Operator)token1;String opName1 = op1.getName();if (OperatorName.BEGIN_TEXT.equals(opName1)) {break;}}preTokenIndex.add(j);}Collections.sort(preTokenIndex);List<Object> orgPreTextTokens =preTokenIndex.stream().map(tokens::get).collect(Collectors.toList());// 重组原始文本绘制块的内容List<List<Object>> preTokensAll = new ArrayList<>();List<Object> singleToken = new ArrayList<>();for (Object item : orgPreTextTokens) {singleToken.add(item);if (item instanceof Operator) {preTokensAll.add(singleToken);singleToken = new ArrayList<>();}}return preTokensAll;}/*** 根据当前匹配字体获取对应系统字体* @param document PDF文档对象* @param allFonts 系统所有字体* @param resources 页面资源* @param fontName 输入的字体名称* @return 当前匹配字体获取对应系统字体在页面资源加载后的名称*/private COSName getSysFontName(PDDocument document, Font[] allFonts, PDResources resources, String fontName) {COSName sysFontName = null;for (Font fon : allFonts) {try {// 通过反射获取 Font2D 对象Font2D font2D = FontUtilities.getFont2D(fon);// 通过反射访问 Font2D 的 familyName 字段Field familyNameField = Font2D.class.getDeclaredField("familyName");familyNameField.setAccessible(true);String familyName = (String)familyNameField.get(font2D);// 获取字体文件路径if (font2D instanceof sun.font.PhysicalFont) {Field platNameField =sun.font.PhysicalFont.class.getDeclaredField("platName");platNameField.setAccessible(true);String platName = (String)platNameField.get(font2D);if (fontName.toLowerCase(Locale.ROOT).contains(familyName.toLowerCase(Locale.ROOT))) {PDType0Font sysFont;if (platName.toLowerCase(Locale.ROOT).endsWith(".ttc")) {// 加载TTC文件org.apache.fontbox.ttf.TrueTypeCollection ttc =new org.apache.fontbox.ttf.TrueTypeCollection(new File(platName));// 选择其中一个字体List<TrueTypeFont> trueTypeFonts = new ArrayList<>();ttc.processAllFonts(trueTypeFonts::add);sysFont = PDType0Font.load(document, trueTypeFonts.get(0), true);} else {sysFont = PDType0Font.load(document, new File(platName));}sysFontName = resources.add(sysFont);}}} catch (Exception e) {e.printStackTrace();}}return sysFontName;}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/424483.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SSHamble:一款针对SSH技术安全的研究与分析工具

关于SSHamble SSHamble是一款功能强大的SSH技术安全分析与研究工具&#xff0c;该工具基于Go语言开发&#xff0c;可以帮助广大研究人员更好地分析SSH相关的安全技术与缺陷问题。 功能介绍 SSHamble 是用于 SSH 实现的研究工具&#xff0c;其中包含下列功能&#xff1a; 1、针…

ESP01的AT指令连接到阿里云平台

物联网平台提供安全可靠的设备连接通信能力&#xff0c;支持设备数据采集上云&#xff0c;规则引擎流转数据和云端数据下发设备端。此外&#xff0c;也提供方便快捷的设备管理能力&#xff0c;支持物模型定义&#xff0c;数据结构化存储&#xff0c;和远程调试、监控、运维。总…

移动UI案例:工具类app整套案例

工具类App是指提供各种实用工具和功能的手机应用程序。这些工具可以包括但不限于日历、闹钟、备忘录、翻译、计算器、单位转换、天气预报、地图导航、音乐播放器、相机、视频编辑等。这些工具类App能够帮助用户解决日常生活和工作中的各种问题&#xff0c;提高效率和便利性。 …

Java数据结构(十)——冒泡排序、快速排序

文章目录 冒泡排序算法介绍代码实现优化策略复杂度和稳定性 快速排序算法介绍优化策略非递归实现代码演示复杂度和稳定性 冒泡排序 算法介绍 冒泡排序是一种简单的排序算法。它重复地遍历要排序的数列&#xff0c;一次比较两个元素&#xff0c;如果它们的顺序错误就交换。遍历…

多线程篇(其它容器- CopyOnWriteArrayList)(持续更新迭代)

一、CopyOnWriteArrayList&#xff08;一&#xff09; 1. 简介 并发包中的并发List只有CopyOnWriteArrayList。 CopyOnWriteArrayList是一个线程安全的ArrayList&#xff0c;对其进行的修改操作都是在底层的一个复制的数 组&#xff08;快照&#xff09;上进行的&#xff0…

redis 基本数据类型—string类型

一、介绍 Redis 中的字符串&#xff0c;直接就是按照二进制数据的方式存储的&#xff0c;不会做任何的编码转换。 Redis对于 string 类型&#xff0c;限制了大小最大是512M 二、命令 SET 将 string 类型的 value 设置到 key 中。如果 key 之前存在&#xff0c;则覆盖&#…

Jwt、Filter、Interceptor

目录 JWT(Json Web Token) jwt令牌 组成 应用场景 生成令牌 解析令牌 登录实例 Filter过滤器 Filter Filter登录校验 Interceptor拦截器 Interceptor 拦截路径 执行流程 登录实例 JWT(Json Web Token) jwt令牌 定义了一种简洁的、自包含的格式&#xff0c;…

二、(JS)JS中常见的键盘事件

一、常见的键盘事件 onkeydown 某个键盘按键被按下onkeypress 某个键盘按键被按下onkeyup 某个键盘按键被松开 二、事件的执行顺序 onkeydown、onkeypress、onkeyup down 事件先发生&#xff1b;press 发生在文本被输入&#xff1b;up …

【大模型理论篇】大模型周边自然语言处理技术(NLP)原理分析及数学推导(Word2Vec、TextCNN、Gated TextCNN、FastText)

1. 背景介绍 进入到大模型时代&#xff0c;似乎宣告了与过去自然语言处理技术的结束&#xff0c;但其实这两者并不矛盾。大模型时代&#xff0c;原有的自然语言处理技术&#xff0c;依然可以在大模型的诸多场景中应用&#xff0c;特别是对数据的预处理阶段。本篇主要关注TextCN…

使用Python生成多种不同类型的Excel图表

目录 一、使用工具 二、生成Excel图表的基本步骤 三、使用Python创建Excel图表 柱形图饼图折线图条形图散点图面积图组合图瀑布图树形图箱线图旭日图漏斗图直方图不使用工作表数据生成图表 四、总结 Excel图表是数据可视化的重要工具&#xff0c;它通过直观的方式将数字信…

PCIe进阶之TL:First/Last DW Byte Enables Rules Traffic Class Field

1 First/Last DW Byte Enables Rules & Attributes Field 1.1 First/Last DW Byte Enables Rules Byte Enable 包含在 Memory、I/O 和 Configuration Request 中。本文定义了相应的规则。Byte Enable 位于 header 的 byte 7 。对于 TH 字段值为 1 的 Memory Read Request…

【STM32】esp8266通过MQTT连接服务器|订阅发布

1. MQTT协议 该协议为应用层协议&#xff0c;传输层使用的是tcp,MQTT的订阅和发布&#xff0c;就相当于在抖音中你关注了某个领域的博主&#xff08;订阅&#xff09;&#xff0c;如果有其他人发了作品就会推给你&#xff08;发布&#xff09;&#xff0c;默认已经安装好了 简…

哈希表、算法

哈希表 hash&#xff1a; 在编程和数据结构中&#xff0c;"hash" 通常指的是哈希函数&#xff0c;它是一种算法&#xff0c;用于将数据&#xff08;通常是字符 串&#xff09;映射到一个固定大小的数字&#xff08;哈希值&#xff09;。哈希函数在哈希表中尤为重要…

探索图论中的关键算法(Java 实现)

“日出东海落西山 愁也一天 喜也一天 遇事不钻牛角尖” 文章目录 前言文章有误敬请斧正 不胜感恩&#xff01;||Day031. 最短路径算法Dijkstra算法Java 实现&#xff1a; Bellman-Ford算法Java 实现&#xff1a; 2. 最小生成树算法Prim算法Java 实现&#xff1a; Kruskal算法Ja…

C++速通LeetCode简单第9题-二叉树的最大深度

深度优先算法递归&#xff1a; /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right…

Conmi的正确答案——MySQL的层级递归查询(递归公共表表达式,CTE)

数据库&#xff1a;oceanbase-ce 递归sql主体&#xff1a; WITH RECURSIVE country_area_tree AS (-- 非递归部分&#xff0c;初始化查询SELECT id, area_name, parent_id, 0 AS levelFROM country_areaWHERE id 589004044419077UNION ALL-- 递归部分&#xff0c;找到子节点S…

聚类_K均值

import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_blobs1.数据预处理 #创建基于高斯分布的样本点, x是点的坐标&#xff0c;y是所属聚类值 x, y make_blobs(n_samples100, centers6, random_state100, cluster_std0.6) # 设置图形尺寸…

2. 变量和指令(omron 机器自动化控制器)——1

机器自动化控制器——第二章 变量和指令 1 2-1 变量一览表MC通用变量轴变量▶ 轴组变量 运动控制指令的输入变量输入变量的有效范围▶ 枚举体一览表 运动控制指令的输出变量运动控制指令的输入输出变量 2-1 变量一览表 MC功能模块使用的变量分为两类。 一类是监视轴等的状态及…

电脑提示丢失mfc140u.dll的详细解决方案,mfc140u.dll文件是什么

遇到电脑显示“缺少 mfc140u.dll 文件”的错误其实是比较常见的。这种提示通常表示某个应用程序在尝试运行时未能找到它所需的关键 DLL 文件&#xff0c;导致无法正常启动。不过&#xff0c;别担心&#xff0c;本文将一步步引导你通过几种不同的方法来解决这个问题&#xff0c;…

VSCode拉取远程项目

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…