<think>我们注意到在`setWrappedField`方法中,我们尝试调用`form.getField(fieldName)`来获取`PdfFormField`对象。但是,根据iText 5的API,`AcroFields`的`getField`方法实际上返回的是字段的当前值(String类型),而不是字段对象本身。为了获取字段对象(`PdfFormField`),我们需要使用`getFieldItem`方法。
修改步骤:
1. 使用`form.getFieldItem(fieldName)`获取`AcroFields.Item`对象,该对象代表表单字段的集合(因为同一个字段可能出现在多个位置)。
2. 从`AcroFields.Item`中获取字段的字典对象(`PdfDictionary`),通常我们取第一个(如果有多个,它们应该是同一个字段的多个widget,共享同一个父字段)。
3. 如果字段是合并字段(即多个widget对应同一个字段值),那么我们需要获取其父字段(即字段字典本身),而widget字典可能不包含完整的字段属性。
具体修改代码:
将原来的:
PdfFormField field = form.getField(fieldName);
改为:
AcroFields.Item item = form.getFieldItem(fieldName);
if (item == null) return; // 如果没有找到字段项,则返回
PdfDictionary fieldDict = item.getWidget(0); // 获取第一个widget字典
// 但是注意:字段的全局属性(如DA)可能存储在父字段(合并字段)中,所以我们需要检查父字段
PdfIndirectReference parentRef = fieldDict.getAsIndirectObject(PdfName.PARENT);
PdfDictionary parentDict = (PdfDictionary) PdfReader.getPdfObject(parentRef);
// 如果父字段存在,则使用父字段,否则使用widget本身
PdfDictionary fieldToModify = parentDict != null ? parentDict : fieldDict;
// 注意:在iText中,字段字典和widget字典是不同的,我们修改的是字段字典(父字段)
但是,更简单且可靠的方法是直接获取合并字段的字典(即字段字典,而不是widget字典)。我们可以通过`item.getValue(0)`来获取字段的合并字典(即字段字典,它包含了字段的全局属性)。
因此,我们可以这样修改:
PdfDictionary fieldDict = item.getValue(0); // 获取字段的字典(合并字段的字典)
然后,我们使用这个`fieldDict`来创建`PdfFormField`对象,或者直接操作字典。
但是注意:`PdfFormField`的构造函数需要字段字典和它的引用,我们可以这样创建:
PdfFormField field = PdfFormField.getFormField(stamper.getWriter(), fieldDict, item.getTabOrder(0));
然而,我们也可以直接操作字典,但为了使用`PdfFormField`的便利方法,我们创建`PdfFormField`对象。
因此,修改后的代码段如下:
在`setWrappedField`方法中,替换获取字段对象的代码:
```java
// 原来的代码:PdfFormField field = form.getField(fieldName);
// 改为:
AcroFields.Item item = form.getFieldItem(fieldName);
if (item == null) {
return; // 找不到字段项,无法进行后续设置
}
// 获取字段的字典(合并字段的字典)
PdfDictionary fieldDict = item.getValue(0);
// 创建一个PdfFormField对象,方便操作
PdfFormField field = PdfFormField.getFormField(stamper.getWriter(), fieldDict, item.getTabOrder(0));
if (field == null) {
return;
}
```
注意:`PdfFormField.getFormField`方法需要传入`PdfWriter`,我们可以从`stamper`中获取。
另外,我们还需要注意,在修改字段字典后,需要将其更新回`AcroFields`吗?实际上,我们直接操作的是字段字典,所以修改是直接的。但是,为了确保修改被标记,我们调用了`stamper.markUsed(field)`。
因此,完整的修改后的`setWrappedField`方法如下(仅修改了获取字段对象的部分):
```java
private static void setWrappedField(AcroFields form, PdfStamper stamper, BaseFont font,
String fieldName, String value) throws Exception {
if (value == null || value.isEmpty()) return;
// 启用多行模式(如果尚未启用)
form.setFieldProperty(fieldName, "setfflags", PdfFormField.FF_MULTILINE, null);
// 获取字段位置和尺寸
List<AcroFields.FieldPosition> positions = form.getFieldPositions(fieldName);
if (positions.isEmpty()) {
form.setField(fieldName, value); // 如果找不到位置,简单设置值
return;
}
Rectangle rect = positions.get(0).position;
float maxWidth = rect.getWidth() - 4; // 宽度补偿(左右各留2单位边距)
float fontSize = getFieldFontSize(form, fieldName);
// 生成换行文本
String wrappedText = wrapText(font, value, fontSize, maxWidth);
form.setField(fieldName, wrappedText);
// 检查是否单行文本(即没有换行符)
boolean isSingleLine = !wrappedText.contains("\n");
// 单行文本处理:垂直居中 + 靠左对齐
if (isSingleLine) {
// 获取字段项
AcroFields.Item item = form.getFieldItem(fieldName);
if (item == null) return;
// 获取字段字典(合并字段的字典)
PdfDictionary fieldDict = item.getValue(0);
// 创建PdfFormField对象
PdfFormField field = PdfFormField.getFormField(stamper.getWriter(), fieldDict, item.getTabOrder(0));
if (field != null) {
// 计算垂直偏移量
float fontAscent = font.getAscentPoint("Ag", fontSize);
float fontDescent = Math.abs(font.getDescentPoint("q", fontSize));
float fontHeight = fontAscent + fontDescent;
float fieldHeight = rect.getHeight();
float offsetY = (fieldHeight - fontHeight) / 2 - fontDescent + 2; // 微调补偿
// 创建资源字典(DR)并注册字体
PdfDictionary dr = field.getAsDict(PdfName.DR);
if (dr == null) dr = new PdfDictionary();
PdfDictionary fonts = dr.getAsDict(PdfName.FONT);
if (fonts == null) fonts = new PdfDictionary();
// 使用别名F1注册字体
if (!fonts.contains(new PdfName("F1"))) {
fonts.put(new PdfName("F1"), font.getIndirectReference());
}
dr.put(PdfName.FONT, fonts);
field.put(PdfName.DR, dr);
// 设置默认外观(DA):使用F1字体,设置字体大小,垂直偏移
String daString = "/F1 " + fontSize + " Tf 0 " + offsetY + " Td";
field.put(PdfName.DA, new PdfString(daString));
// 更新表单字段(标记为已修改)
stamper.markUsed(field);
}
}
}
```
注意:在创建`PdfFormField`对象时,我们使用了`item.getTabOrder(0)`,这里0表示第一个widget的tab顺序,通常我们取第一个即可。
这样修改后,我们就能正确获取到字段对象,进而修改其DA属性,实现单行文本的垂直居中。
另外,需要注意的是,如果字段是只读的,可能需要额外的处理。但根据我们的使用场景,通常我们是在填充表单,所以假设字段是可写的。
最后,确保在调用`setWrappedField`方法时,传递了`stamper`参数,因为我们需要它来创建`PdfFormField`对象。
希望这解决了您的问题。
</think>
在 iText 5 中,`AcroFields.getField(fieldName)` 方法确实返回 `String` 类型(字段值),而不是 `PdfFormField` 对象。以下是修正后的完整代码解决方案:
```java
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.*;
import java.io.FileOutputStream;
import java.util.List;
public class PdfFormFiller {
public static void main(String[] args) throws Exception {
// 示例用法
String inputPdf = "template.pdf";
String outputPdf = "output.pdf";
PdfReader reader = new PdfReader(inputPdf);
PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(outputPdf));
AcroFields form = stamper.getAcroFields();
// 使用支持中文的字体
BaseFont font = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
// 填充字段(注意传递stamper参数)
setWrappedField(form, stamper, font, "nameField", "张三");
setWrappedField(form, stamper, font, "addressField", "北京市海淀区中关村大街较长的地址需要自动换行处理");
stamper.close();
reader.close();
}
private static void setWrappedField(AcroFields form, PdfStamper stamper, BaseFont font,
String fieldName, String value) throws Exception {
if (value == null || value.isEmpty()) return;
// 启用多行模式
form.setFieldProperty(fieldName, "setfflags", PdfFormField.FF_MULTILINE, null);
// 获取字段位置和尺寸
List<AcroFields.FieldPosition> positions = form.getFieldPositions(fieldName);
if (positions.isEmpty()) {
form.setField(fieldName, value);
return;
}
Rectangle rect = positions.get(0).position;
float maxWidth = rect.getWidth() - 4; // 宽度补偿
float fontSize = getFieldFontSize(form, fieldName);
// 生成换行文本
String wrappedText = wrapText(font, value, fontSize, maxWidth);
form.setField(fieldName, wrappedText);
// 单行文本处理:垂直居中 + 靠左对齐
if (!wrappedText.contains("\n")) {
// 正确获取PdfFormField对象
AcroFields.Item fieldItem = form.getFieldItem(fieldName);
if (fieldItem != null) {
PdfDictionary fieldDict = fieldItem.getMerged(0);
PdfFormField field = PdfFormField.getFormField(stamper.getWriter(), fieldDict, fieldItem.getTabOrder(0));
if (field != null) {
// 计算垂直偏移量
float fontAscent = font.getAscentPoint("Ag", fontSize);
float fontDescent = Math.abs(font.getDescentPoint("q", fontSize));
float fontHeight = fontAscent + fontDescent;
float fieldHeight = rect.getHeight();
float offsetY = (fieldHeight - fontHeight) / 2 - fontDescent + 2;
// 创建资源字典并注册字体
PdfDictionary dr = field.getAsDict(PdfName.DR);
if (dr == null) dr = new PdfDictionary();
PdfDictionary fonts = dr.getAsDict(PdfName.FONT);
if (fonts == null) fonts = new PdfDictionary();
if (!fonts.contains(new PdfName("F1"))) {
fonts.put(new PdfName("F1"), font.getIndirectReference());
}
dr.put(PdfName.FONT, fonts);
field.put(PdfName.DR, dr);
// 设置默认外观
String daString = "/F1 " + fontSize + " Tf 0 " + offsetY + " Td";
field.put(PdfName.DA, new PdfString(daString));
// 标记字段为已修改
stamper.markUsed(field);
}
}
}
}
// 获取字段字体大小
private static float getFieldFontSize(AcroFields form, String fieldName) {
try {
// 正确获取字段字典
AcroFields.Item item = form.getFieldItem(fieldName);
if (item != null) {
PdfDictionary dict = item.getMerged(0);
PdfString da = dict.getAsString(PdfName.DA);
if (da != null) {
String daValue = da.toString();
// 解析字体大小(例如:"/Helv 12 Tf")
String[] parts = daValue.split(" ");
for (int i = 0; i < parts.length; i++) {
if ("Tf".equals(parts[i]) && i > 0) {
return Float.parseFloat(parts[i-1]);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return 12; // 默认字体大小
}
// 文本换行算法
private static String wrapText(BaseFont font, String text, float fontSize,
float maxWidth) throws Exception {
StringBuilder result = new StringBuilder();
StringBuilder line = new StringBuilder();
float currentWidth = 0;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
String charStr = String.valueOf(c);
float charWidth = font.getWidthPoint(charStr, fontSize);
if (c == '\n') {
result.append(line).append('\n');
line.setLength(0);
currentWidth = 0;
continue;
}
if (currentWidth + charWidth > maxWidth && line.length() > 0) {
result.append(line).append('\n');
line.setLength(0);
currentWidth = 0;
}
line.append(c);
currentWidth += charWidth;
}
if (line.length() > 0) {
result.append(line);
}
return result.toString();
}
}
```
### 关键修正说明:
1. **获取 `PdfFormField` 的正确方式**:
```java
// 错误方式:返回String类型
// String fieldValue = form.getField(fieldName);
// 正确方式:获取PdfFormField对象
AcroFields.Item fieldItem = form.getFieldItem(fieldName);
if (fieldItem != null) {
PdfDictionary fieldDict = fieldItem.getMerged(0);
PdfFormField field = PdfFormField.getFormField(stamper.getWriter(), fieldDict, fieldItem.getTabOrder(0));
}
```
使用 `getFieldItem()` 获取字段项,然后通过 `PdfFormField.getFormField()` 构造真正的字段对象[^1]。
2. **获取字段属性的正确方式**:
```java
// 获取字段合并字典(包含所有属性)
PdfDictionary dict = item.getMerged(0);
// 获取默认外观字符串
PdfString da = dict.getAsString(PdfName.DA);
```
3. **字体大小解析优化**:
```java
String[] parts = daValue.split(" ");
for (int i = 0; i < parts.length; i++) {
if ("Tf".equals(parts[i]) && i > 0) {
return Float.parseFloat(parts[i-1]);
}
}
```
更健壮地解析字体大小值,避免正则表达式匹配失败的情况[^1]。
4. **字段修改标记**:
```java
stamper.markUsed(field);
```
确保对字段的修改会被写入最终PDF[^1]。
### 使用注意事项:
1. **字段定位**:
- `getFieldPositions()` 返回字段在页面上的位置,用于计算文本布局
- 多页表单字段需要特殊处理
2. **字体处理**:
- 使用 `BaseFont.createFont()` 创建字体
- 在资源字典中注册字体别名(如 `F1`)
- 确保字体支持所需字符集
3. **性能考虑**:
- 批量处理时缓存字体对象
- 大文件处理使用 `PdfReader` 的分页加载
4. **表单扁平化**:
```java
stamper.setFormFlattening(true); // 使表单变为普通内容
```