Apache XML Graphics Commons 2.11 图像编解码与 IO API 详解
系统讲解 Apache XML Graphics Commons 2.11 的图像写入 API(ImageWriterRegistry/ImageWriterParams/PNG/TIFF/多帧)、ImageManager 预检与解码管线(ImageInfo/ImageSize/ImageFlavor/ImageBuffered/ImageContext)与 util.io 编码流(Base64 回环及五种编码大小对比),逐项代码配真实输出,全部已在 Ubuntu 22.04 实测。
Apache XML Graphics Commons 2.11 图像编解码与 IO API 详解
目标:系统掌握 Apache XML Graphics Commons 2.11 的两大 API 面——图像写入(ImageWriterRegistry、ImageWriterParams、PNG/TIFF 与多帧编码)与图像读取预检(ImageManager、ImageInfo、ImageFlavor、ImageContext),外加 util.io 编码流族(Base64/ASCIIHex/ASCII85/Flate/RunLength);每个方法都配完整示例代码与实测输出。以下全部命令与回显均已在 Ubuntu 22.04(x86_64)、OpenJDK 17.0.20.1 实测通过。
一、环境信息
- 操作系统:Ubuntu 22.04.5 LTS(x86_64)
- JDK:OpenJDK 17.0.20.1(Temurin,
java -version实测) - 构建工具:Apache Maven 3.9.16
- 目标库:Apache XML Graphics Commons 2.11(
org.apache.xmlgraphics:xmlgraphics-commons:2.11) - 传递依赖:commons-io 2.17.0、commons-logging 1.3.0
- Maven 示例工程目录:
/opt/xgc-demo,产物目录/opt/xgc-demo/output
确认版本基线:
java -version 2>&1 | head -1
mvn -v | head -1
输出:
openjdk version "17.0.20.1" 2026-08-18
Apache Maven 3.9.16 (2bdd9fddda4b155ebf8000e807eb73fd829a51d5)
💡 说明:本文所有运行命令统一为
java -Dfile.encoding=UTF-8 -Djava.awt.headless=true -cp target/classes:$CP demo.XgcDemo <组名>形式($CP为编译输出与三件套 jar 拼接的 classpath,见第 3 篇 2.3 节);-Djava.awt.headless=true在无图形界面的服务器上声明 AWT 运行模式,实测图像编码与预检全链路可用。
二、API 总览
2.11 的编解码与 IO 能力按「写入、读取预检、编码流」三块组织(均经本文实测):
| API | 包 | 角色 | 关键方法/常量 | 详解章节 |
|---|---|---|---|---|
| ImageWriterRegistry | image.writer | 按 MIME 查找写出器的注册表 | getInstance、getWriterFor | 3.1 |
| ImageWriter | image.writer | 图像写出器接口 | writeImage、getMIMEType、isFunctional、createMultiImageWriter | 3.1、3.3 |
| ImageWriterParams | image.writer | 跨格式写出参数 | setResolution、setCompressionMethod、setJPEGQuality、setRowsPerStrip、setEndianness | 3.2 |
| ImageIOPNGImageWriter / ImageIOTIFFImageWriter | image.writer.imageio | 内置 PNG/TIFF 实现 | (writeImage) | 3.3、3.4 |
| MultiImageWriter | image.writer | 多帧写出接口 | writeImage、close | 3.5 |
| ImageManager | image.loader | 加载管线门面 | preloadImage、getImage | 4.1 |
| ImageInfo / ImageSize | image.loader | 预检结果(MIME+尺寸) | getMimeType、getSize、getWidthPx、getDpiHorizontal、getDimensionPt | 4.2 |
| ImageFlavor / Image | image.loader | 解码目标形态 | ImageFlavor.BUFFERED_IMAGE、getFlavor | 4.3 |
| ImageBuffered | image.loader.impl | BufferedImage 形态实现 | getBufferedImage | 4.3 |
| ImageContext / ImageSessionContext | image.loader | 环境分辨率与会话上下文 | getSourceResolution、DefaultImageSessionContext | 4.4 |
| Base64EncodeStream / Base64DecodeStream | util.io | Base64 编解码流 | write、close、read | 5.1 |
| ASCIIHex/ASCII85/Flate/RunLength EncodeOutputStream | util.io | PS 常用编码流 | write、close | 5.2 |
⚠️ 注意:
ImageIOImageWriter是通用实现,ImageIOPNGImageWriter/ImageIOTIFFImageWriter是其 MIME 专用子类;jar tf实测 2.11 内置服务发现配置,getWriterFor("image/png")实测返回ImageIOPNGImageWriter。同名网传类ImageBufferedImage在 2.11 中不存在,解码实现类是ImageBuffered(实测编译错误复现见第 3 篇 3.2 节)。
三、写入 API 详解
写出链路的骨架:ImageWriterRegistry 按 MIME 选实现 → ImageWriterParams 携带参数 → writeImage(RenderedImage, OutputStream, params) 编码落盘。
3.1 ImageWriterRegistry:按 MIME 选写出器
注册表是单例,getWriterFor(String mimeType) 返回实现。实测 PNG 与 TIFF 各自返回专用类,并检查功能可用性:
ImageWriter pngWriter = ImageWriterRegistry.getInstance().getWriterFor("image/png");
System.out.println("PNG writer: " + pngWriter.getClass().getName()
+ " mime=" + pngWriter.getMIMEType()
+ " isFunctional=" + pngWriter.isFunctional()
+ " multi=" + pngWriter.supportsMultiImageWriter());
ImageWriter tiffWriter = ImageWriterRegistry.getInstance().getWriterFor("image/tiff");
System.out.println("TIFF writer: " + tiffWriter.getClass().getName()
+ " mime=" + tiffWriter.getMIMEType()
+ " isFunctional=" + tiffWriter.isFunctional()
+ " multi=" + tiffWriter.supportsMultiImageWriter());
输出(png/tiff 组首行,实测):
writer 实现: org.apache.xmlgraphics.image.writer.imageio.ImageIOPNGImageWriter
writer MIME: image/png, isFunctional=true
writer 实现: org.apache.xmlgraphics.image.writer.imageio.ImageIOTIFFImageWriter, supportsMultiImageWriter=true
解读:isFunctional() 反映实现是否在当前 JVM 可用(ImageIO 实现总是 true);supportsMultiImageWriter() 只有 TIFF 为 true,PNG 单帧。接口还有 getMIMEType() 标识目标格式;自定义写出器可用 register(ImageWriter, int) 以优先级插入注册表。
3.2 ImageWriterParams:跨格式统一参数
参数类把 PNG/TIFF/JPEG 的共性参数收敛到一个类(javap 实测的全部 setter):setResolution(int)(统一写入物理分辨率)、setCompressionMethod(String)(TIFF 压缩名,透传 JDK)、setJPEGQuality(float, boolean)(JPEG 质量与基线化)、setRowsPerStrip(int) 与 setSingleStrip(boolean)(TIFF 条带布局)、setXResolution/setYResolution(TIFF 双向分辨率)、setResolutionUnit(ResolutionUnit)、setEndianness(Endianness)(TIFF 字节序)。实测最常用的是分辨率与压缩:
ImageWriterParams params = new ImageWriterParams();
params.setResolution(300); // 300 DPI
params.setCompressionMethod("PackBits"); // 仅 TIFF 有意义
writer.writeImage(img, out, params);
分辨率写进文件后的读回行为见 4.2 节(PNG 量化误差 299.9994、TIFF 精确 300.0——这是参数落盘后的实测验证)。压缩名只接受 JDK TIFF writer 认可的值,实测 PackBits/LZW/Deflate 可用、"None" 抛 IllegalArgumentException: Unknown compression type!(复现回显见第 3 篇 3.3 节),不压缩就不设置该项。
3.3 PNG 写出全流程
png() 组完整代码(样张绘制略,见第 3 篇 2.2 节):
BufferedImage img = drawSample(320, 200, "XML Graphics PNG");
ImageWriter writer = ImageWriterRegistry.getInstance().getWriterFor("image/png");
ImageWriterParams params = new ImageWriterParams();
params.setResolution(300);
File target = new File(OUT, "demo.png");
try (OutputStream out = new FileOutputStream(target)) {
writer.writeImage(img, out, params);
}
System.out.println("已写入 " + target.getAbsolutePath()
+ " 字节数=" + target.length());
运行回显(实测):
writer 实现: org.apache.xmlgraphics.image.writer.imageio.ImageIOPNGImageWriter
writer MIME: image/png, isFunctional=true
已写入 /opt/xgc-demo/output/demo.png 字节数=14114
file /opt/xgc-demo/output/demo.png
/opt/xgc-demo/output/demo.png: PNG image data, 320 x 200, 8-bit/color RGBA, non-interlaced
验证点:file 识别出 320x200 8 位 RGBA,与 BufferedImage.TYPE_INT_ARGB 的编码输入一致;14114 字节含渐变、矩形、椭圆、线条与文字的完整栅格。
3.4 TIFF 压缩矩阵
同一 320x200 样张在三种压缩下的实测(tiff() 组回显):
PackBits 压缩: demo-packbits.tiff 字节数=256377
Deflate 压缩: demo-deflate.tiff 字节数=82104
LZW 压缩: demo-lzw.tiff 字节数=156333
| 压缩名 | 字节数(实测) | 相对大小 | file 识别的压缩标识 | 适用 |
|---|---|---|---|---|
| PackBits | 256377 | 100% | compression=PackBits (Macintosh RLE) | 老解释器兼容、扫描件 |
| LZW | 156333 | 61% | compression=LZW | 通用 |
| Deflate | 82104 | 32% | compression=Deflate (PKZIP) | 渐变/照片类体积最优 |
| None | 抛异常 | — | — | 不可用:Unknown compression type!(实测) |
file 全量回显验证压缩标识确实写入文件头:
output/demo-deflate.tiff: TIFF image data, big-endian, direntries=16, height=200, bps=0, compression=Deflate (PKZIP), PhotometricIntepretation=RGB, width=320
output/demo-lzw.tiff: TIFF image data, big-endian, direntries=13, height=200, bps=0, compression=LZW, PhotometricIntepretation=RGB, width=320
output/demo-packbits.tiff: TIFF image data, big-endian, direntries=16, height=200, bps=0, compression=PackBits (Macintosh RLE), PhotometricIntepretation=RGB, width=320
3.5 多帧 TIFF(MultiImageWriter)
TIFF 支持单文件多帧;writer.createMultiImageWriter(out) 产出多帧接口(注意:非 AutoCloseable,close 必须手动调用,实测编译坑见第 3 篇 3.1 节):
OutputStream mout = new FileOutputStream(new File(OUT, "demo-multi.tiff"));
MultiImageWriter miw = tiffWriter.createMultiImageWriter(mout);
ImageWriterParams mp = new ImageWriterParams();
mp.setCompressionMethod("PackBits");
mp.setResolution(300);
miw.writeImage(drawSample(320, 200, "TIFF frame 1"), mp);
miw.writeImage(drawSample(320, 200, "TIFF frame 2"), mp);
miw.close();
mout.close();
运行回显(实测):
多帧 TIFF(2 页): demo-multi.tiff 字节数=512829
验证点:两帧 PackBits 合计 512829 字节 ≈ 单帧 256377×2 + 页目录开销,TIFF 无帧间去重,多页扫描件按此估算体积。
四、读取与预检 API 详解
读取侧的门面是 ImageManager:preloadImage 只读文件头产出 ImageInfo(不把像素载入内存),getImage 再按目标 ImageFlavor 解码。这条「先预检后解码」的管线是 FOP 版面计算的基础。
4.1 ImageManager:preload 与 getImage
info() 组核心代码:
ImageContext ctx = new DefaultImageContext();
ImageManager manager = new ImageManager(ctx);
ImageSessionContext session = new DefaultImageSessionContext(ctx, new File(OUT));
String uri = new File(OUT, name).toURI().toString(); // file:/opt/xgc-demo/output/demo.png
ImageInfo info = manager.preloadImage(uri, session);
System.out.println(" MIME=" + info.getMimeType());
System.out.println(" size=" + info.getSize());
System.out.println(" 像素=" + info.getSize().getWidthPx() + "x" + info.getSize().getHeightPx()
+ " DPI=" + info.getSize().getDpiHorizontal() + "x" + info.getSize().getDpiVertical()
+ " pt=" + info.getSize().getDimensionPt().getWidth() + "x" + info.getSize().getDimensionPt().getHeight());
Image image = manager.getImage(info, ImageFlavor.BUFFERED_IMAGE, session);
System.out.println(" 解码 flavor=" + image.getFlavor()
+ " 实现类=" + image.getClass().getName());
BufferedImage decoded = ((ImageBuffered) image).getBufferedImage();
System.out.println(" BufferedImage 尺寸=" + decoded.getWidth() + "x" + decoded.getHeight()
+ " 类型=" + decoded.getType());
完整运行回显(实测):
== ImageInfo / ImageLoader(ImageManager 预检与解码) ==
DefaultImageContext.getSourceResolution() = 72.0
自定义 ImageContext(300dpi).getSourceResolution() = 300.0
-- demo.png (uri=file:/opt/xgc-demo/output/demo.png)
MIME=image/png
size=Size: 76800x48000 mpt (320x200 px at 299.9994201660156x299.9994201660156 dpi)
像素=320x200 DPI=299.9994201660156x299.9994201660156 pt=76.8x48.0
解码 flavor=BufferedImage 实现类=org.apache.xmlgraphics.image.loader.impl.ImageBuffered
BufferedImage 尺寸=320x200 类型=6
-- demo-packbits.tiff (uri=file:/opt/xgc-demo/output/demo-packbits.tiff)
MIME=image/tiff
size=Size: 76800x48000 mpt (320x200 px at 300.0x300.0 dpi)
像素=320x200 DPI=300.0x300.0 pt=76.8x48.0
解码 flavor=BufferedImage 实现类=org.apache.xmlgraphics.image.loader.impl.ImageBuffered
BufferedImage 尺寸=320x200 类型=0
-- demo-multi.tiff (uri=file:/opt/xgc-demo/output/demo-multi.tiff)
MIME=image/tiff
size=Size: 76800x48000 mpt (320x200 px at 300.0x300.0 dpi)
像素=320x200 DPI=300.0x300.0 pt=76.8x48.0
解码 flavor=BufferedImage 实现类=org.apache.xmlgraphics.image.loader.impl.ImageBuffered
BufferedImage 尺寸=320x200 类型=0
4.2 ImageInfo 与 ImageSize 字段解读
| 字段 | PNG 实测值 | TIFF 实测值 | 说明 |
|---|---|---|---|
| getMimeType() | image/png | image/tiff | 按文件头嗅探 |
| getWidthPx()/getHeightPx() | 320/200 | 320/200 | 像素尺寸 |
| getDpiHorizontal()/getDpiVertical() | 299.9994201660156 | 300.0 | 物理分辨率读回 |
| getDimensionPt() | 76.8x48.0 | 76.8x48.0 | 320px@300dpi = 76.8pt |
| size.toString() | 76800x48000 mpt | 同左 | mpt=毫点(1/1000 pt),76800 mpt=76.8pt |
两个实测锚点:
- DPI 精度差异:PNG 的物理分辨率按「像素/米」整数存储(11811 px/m ≈ 299.9994 DPI),读回带量化误差;TIFF 用「像素/英寸」有理数存储,精确还原 300.0。比较 DPI 请用容差(如
Math.abs(dpi - 300) < 0.01)。 - 尺寸换算:
ImageSize同时维护 px、mpt、pt 三套值,calcSizeFromPixels()/calcPixelsFromSize()手动触发换算;本文写出的 300 DPI 在读回时三套值自洽。
4.3 ImageFlavor 体系与 ImageBuffered
ImageFlavor 描述解码目标形态,javap 实测的静态常量包括 RENDERED_IMAGE、BUFFERED_IMAGE、XML_DOM、RAW、RAW_PNG、RAW_JPEG 等。管线会按 flavor 选择加载器/转换器(2.11 内置 ImageLoaderFactoryPNG、ImageLoaderFactoryInternalTIFF、imageio.ImageLoaderFactoryImageIO 等工厂,jar tf 实测)。BUFFERED_IMAGE 形态的实现类是 ImageBuffered,getBufferedImage() 取回标准 BufferedImage:
- PNG 解码后
getType()=6(BufferedImage.TYPE_INT_ARGB); - TIFF 解码后
getType()=0(TYPE_CUSTOM,JDK TIFF 读入器的原生色彩模型),需要 ARGB 时再走ImageConverterBuffered2Rendered类转换器或手动转换。
类名锚点:解码实现类是 ImageBuffered;写 ImageBufferedImage 编译报 cannot find symbol(实测复现见第 3 篇 3.2 节)。
4.4 ImageContext 与 ImageSessionContext
ImageContext 接口只有一个方法 getSourceResolution(),表示「未标注物理分辨率的图像按多少 DPI 解释」:
DefaultImageContext默认返回 72.0(实测);- 自定义实现返回 300.0(实测),FOP 就是用它把页面 DPI 配置注入管线;
DefaultImageSessionContext(parentContext, baseDir)组合环境上下文与基准目录,preloadImage解析相对 URI 时以 baseDir 为根;URI 形式为file:/opt/xgc-demo/output/demo.png(File.toURI()生成,实测可解析)。文件不存在时抛FileNotFoundException: Image not found: <uri>(实测报错信息)。
五、util.io 编码流详解
org.apache.xmlgraphics.util.io 提供 PS/PDF 常用的二进制→文本编码流族,全部是「装饰器」用法:包住底层 OutputStream,close 时收尾。
5.1 Base64 编解码回环
base64() 组代码:
byte[] raw = "Apache XML Graphics Commons Base64 round-trip test. ".getBytes(StandardCharsets.UTF_8);
ByteArrayOutputStream enc = new ByteArrayOutputStream();
Base64EncodeStream encoder = new Base64EncodeStream(enc);
encoder.write(raw);
encoder.close(); // 必须关闭,否则尾部 4 字符块不落盘
byte[] encoded = enc.toByteArray();
System.out.println("原始字节数=" + raw.length + " 编码后字节数=" + encoded.length);
System.out.println("编码结果前 64 字符: "
+ new String(encoded, 0, Math.min(64, encoded.length), StandardCharsets.ISO_8859_1));
Base64DecodeStream decoder = new Base64DecodeStream(new ByteArrayInputStream(encoded));
byte[] decoded = decoder.readAllBytes();
System.out.println("解码后字节数=" + decoded.length
+ " 与原文一致=" + Arrays.equals(raw, decoded));
运行回显(实测):
== Base64 编解码(util.io) ==
原始字节数=52 编码后字节数=73
编码结果前 64 字符: QXBhY2hlIFhNTCBHcmFwaGljcyBDb21tb25zIEJhc2U2NCByb3VuZC10cmlwIHRl
解码后字节数=52 与原文一致=true
解读:52 字节原文编码为 73 字节(52/3×4=70 加 1 个换行,实测 Base64EncodeStream 每 76 字符换行但本例不足一行只落 1 个换行符);解码回环逐字节一致。close 语义是硬要求:不 close 会丢失尾部不满 3 字节的块(与 JDK java.util.Base64 静态 API 的最大差异)。
5.2 五种编码流大小对比
对同一 1024 字节样本(周期性文本)实测各编码流输出(base64() 组后半段回显):
-- 各编码流对 1024 字节样本的输出大小对比 --
Base64EncodeStream -> 1389 字节
ASCIIHexOutputStream -> 2074 字节
ASCII85OutputStream -> 1298 字节
FlateEncodeOutputStream -> 33 字节
RunLengthEncodeOutputStream -> 1034 字节
| 编码流 | 实测输出 | 膨胀率 | 定位 |
|---|---|---|---|
| ASCIIHexOutputStream | 2074 字节 | ~203% | 每 byte 2 个 hex 字符,最简单也最大 |
| Base64EncodeStream | 1389 字节 | ~136% | PS/PDF 标准文本编码 |
| ASCII85OutputStream | 1298 字节 | ~127% | PS 高密度文本编码(4 字节→5 字符) |
| RunLengthEncodeOutputStream | 1034 字节 | ~101% | 简单行程压缩,重复数据有效 |
| FlateEncodeOutputStream | 33 字节 | ~3% | zlib 压缩,周期性样本压缩率极高 |
用法一致:以 new ASCII85OutputStream(underlying) 为例 → write(payload) → close()(close 触发终止符:ASCII85 的 ~>、Flate 的校验尾、RunLength 的 EOD 128)。PS 生成(第 5 篇)里嵌入图像数据正是这些流的组合应用。
5.3 与 Commons IO 的分工
xmlgraphics-commons 自带 commons-io 2.17.0 依赖:本库的 util.io 只做 PS/PDF 编码流,通用流拷贝/文件工具(IOUtils、FileUtils)直接用 commons-io,两者定位互补不重叠。
六、注意事项
- close 语义:
Base64EncodeStream、MultiImageWriter、五种编码流都依赖close()收尾,漏 close 轻则文件截断、重则页目录缺失。 - MultiImageWriter 非 AutoCloseable:不能用于 try-with-resources(实测编译错误见第 3 篇 3.1 节)。
- TIFF 压缩名:只传实测可用的
PackBits/LZW/Deflate;"None"抛Unknown compression type!,不压缩就不设置。 - DPI 读回容差:PNG 的 300 DPI 读回为 299.9994201660156(像素/米量化),比较用容差。
- 解码实现类名:
ImageBuffered,不是ImageBufferedImage。 - 传递依赖:commons-io 与 commons-logging 是编译期依赖,jar 直跑必须带全(官方 bin 包
lib/自带)。 - 运行编码:POSIX/C locale 下加
-Dfile.encoding=UTF-8,无图形界面加-Djava.awt.headless=true。
七、参考链接
- Apache XML Graphics Commons 官方主页:https://xmlgraphics.apache.org/commons/
- 2.11 Javadoc(image.writer/image.loader/util.io 包):https://xmlgraphics.apache.org/commons/javadoc/2.11/
- 图像加载管线说明:https://xmlgraphics.apache.org/commons/image-loader.html
- Maven 中央仓库坐标页:https://repo1.maven.org/maven2/org/apache/xmlgraphics/xmlgraphics-commons/
- JDK ImageIO TIFF 支持说明(压缩类型约束来源):https://docs.oracle.com/en/java/javase/17/docs/api/java.desktop/javax/imageio/ImageWriteParam.html