Java使用SequenceInputStream合并流拼接PDF第二个文件不显示问题
问题产生原因
- TXT、XML、CSV这类属于无内部结构的线性字节流格式,解析时会按顺序读取所有字节内容,不会做全局结构校验,直接按字节拼接后可以正常展示。
- PDF是有严格规范的结构化二进制格式,单个合法PDF的固定结构为:文件头(标注版本)→ 内容对象(页面、资源等)→ 交叉引用表xref(记录所有对象的文件内偏移位置)→ 文件尾trailer(标注根对象、xref位置,以
%%EOF作为文件结束标记)。 - 你用
SequenceInputStream直接拼接两个PDF的完整字节,相当于把sample2.pdf的完整结构(包含它自己的文件头、xref表、trailer、%%EOF结束标记)直接接在sample1.pdf的完整字节后面。PDF解析器读取到sample1末尾的%%EOF标记时,就会判定文件已经结束,sample2的所有内容会被当成无效冗余数据直接忽略,自然无法展示。就算手动删掉第一个PDF的%%EOF标记,两个文件各自的xref表记录的对象偏移量都是基于自身文件起始位置计算的,拼接后偏移量全部失配,依然无法正常解析。 - 顺带提一句,你贴的测试代码存在语法错误:
Files.readAllBytes(new File("destFile.pdf")缺少右括号,后续多余的分号和括号会导致编译失败,不过这不是PDF内容展示异常的原因,而且先拷贝流到文件、再读字节重写一遍的操作完全是冗余的,对结果没有任何影响。
无第三方库前提下的合并说明
不使用PDFReader等任何第三方PDF处理类库时,不存在简单的字节流拼接方案可以合并PDF。如果要纯手动实现合并,必须严格遵循PDF ISO 32000规范完成以下操作:
- 剔除除第一个文件外所有待合并PDF的文件头、独立xref表、旧trailer和
%%EOF标记 - 重新计算所有非首个PDF的内容对象在合并后新文件中的偏移位置
- 将所有PDF的页面对象挂载到同一个根页树节点下
- 重新生成覆盖全文件所有对象的全局xref交叉引用表
- 生成新的trailer记录全局xref位置、根对象入口,最后写入合法的
%%EOF结束标记
这套逻辑本身就是半个PDF库的实现量,复杂度极高。
将SequenceInputStream拆分回独立InputStream列表的方法
SequenceInputStream本身没有公开API可以直接获取内部封装的源流列表,内部存储源流的枚举是私有成员,外部无法直接访问,可行的实现方案有两种:
- 方案一(推荐,无兼容性问题):从源头持有源流列表的引用。你现在代码里的源流列表
inputStream1是方法内的局部变量,方法返回SequenceInputStream后外部就丢失了列表引用,只要把这个列表提升为类成员变量,对外提供访问方法即可。
参考实现:public class ReadStream{ // 持有源流列表的成员引用 private final List<InputStream> sourceStreams = new ArrayList<>(); public InputStream readInputStream() throws FileNotFoundException { sourceStreams.clear(); sourceStreams.add(new FileInputStream("sample1.pdf")); sourceStreams.add(new FileInputStream("sample2.pdf")); return new SequenceInputStream(Collections.enumeration(sourceStreams)); } // 对外提供获取独立源流的方法 public List<InputStream> getSourceStreams() { // 返回列表副本,避免外部随意修改内部持有引用 return new ArrayList<>(sourceStreams); } } - 方案二(不推荐,存在兼容性风险):如果无法修改原有
ReadStream的代码,可以通过反射强行读取SequenceInputStream的私有成员获取内部枚举,遍历得到所有原始流。不同JDK版本的内部实现可能调整私有成员命名,该方案可能在部分JDK版本上运行失败。
参考实现:public static List<InputStream> splitSequenceStream(SequenceInputStream seqStream) throws Exception { List<InputStream> streamList = new ArrayList<>(); Field enumField = SequenceInputStream.class.getDeclaredField("e"); enumField.setAccessible(true); Enumeration<InputStream> sourceEnum = (Enumeration<InputStream>) enumField.get(seqStream); while (sourceEnum.hasMoreElements()) { streamList.add(sourceEnum.nextElement()); } return streamList; }
内容的提问来源于stack exchange,提问作者Pradeep
相关产品推荐
相关产品推荐

