Apache XML Graphics Commons 2.11 快速入门与集成示例
用 10 分钟在 Maven 工程中引入 Apache XML Graphics Commons 2.11 并跑通第一张 PNG,覆盖依赖树与传递依赖、MultiImageWriter 非 AutoCloseable、ImageBuffered 命名、TIFF 压缩名三类实测坑,以及 TIFF 压缩对比、多帧编码与 ImageInfo 预检管线,全部已在 Ubuntu 22.04 实测。
Apache XML Graphics Commons 2.11 快速入门与集成示例
目标:10 分钟内在 Maven 工程中引入 Apache XML Graphics Commons 2.11,跑通第一个 PNG 编码程序;掌握三个高频集成坑(均为编译/运行实测复现);进阶完成 TIFF 压缩对比、多帧编码与 ImageInfo 图像预检。以下全部命令与回显均已在 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,Maven Central 当前 latest/release,2025-05-06 发布) - 传递依赖:commons-io 2.17.0、commons-logging 1.3.0(依赖树实测,见 2.1 节)
- 字节码基线:2.11 jar 的 class 主版本号为 52(Java 8,
javap -verbose实测),JDK 8+ 可运行 - Maven 示例工程目录:
/opt/xgc-demo,产物目录/opt/xgc-demo/output
确认 JDK 与 Maven 版本基线:
java -version 2>&1
mvn -v | head -2
输出:
openjdk version "17.0.20.1" 2026-08-18
OpenJDK Runtime Environment Temurin-17.0.20.1+1 (build 17.0.20.1+1)
Apache Maven 3.9.16 (2bdd9fddda4b155ebf8000e807eb73fd829a51d5)
Maven home: /usr/share/maven
⚠️ 注意:本文所有运行命令统一为
java -Dfile.encoding=UTF-8 -Djava.awt.headless=true -cp target/classes:$CP demo.XgcDemo <组名>形式($CP在 2.3 节赋值);服务器无图形界面,headless属性实测可显式指定,省略时 JVM 也会按DISPLAY环境变量自动判定。安装与环境准备见第 1 篇《Ubuntu 22.04 安装 Apache XML Graphics Commons 指南》。
二、10 分钟最小可用
本节从空目录到跑通第一个程序共 4 步,全程约 10 分钟:建工程(1 分钟)→ 写 pom(2 分钟)→ 写程序(2 分钟)→ 编译运行与产物验证(5 分钟,首次下载依赖视网速)。
2.1 创建工程并引入依赖
创建工程目录:
mkdir -p /opt/xgc-demo/src/main/java/demo
/opt/xgc-demo/pom.xml 全文(最小可用配置,直接照抄):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId>
<artifactId>xgc-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>xmlgraphics-commons</artifactId>
<version>2.11</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
</plugins>
</build>
</project>
两个必做项,缺一会踩坑:
- 显式固定 maven-compiler-plugin 为 3.11.0:老版本 Maven 默认绑定的 3.1 版插件不认识
maven.compiler.release属性;CentOS 8 的 Maven 3.5.4 + JDK 8 组合下实测报Fatal error compiling: invalid flag: --release(复现与修复见第 2 篇 4.4 节)。 - 声明
project.build.sourceEncoding=UTF-8:源码含中文注释时防止按平台默认编码编译报unmappable character。
查看依赖树(确认传递依赖自动就位):
cd /opt/xgc-demo && mvn -B dependency:tree 2>&1 | grep -E "dependency.*:tree|demo:xgc-demo|xmlgraphics-commons:jar|commons-io:commons-io:jar|commons-logging:commons-logging:jar"
输出:
[INFO] --- dependency:3.7.0:tree (default-cli) @ xgc-demo ---
[INFO] demo:xgc-demo:jar:1.0.0
[INFO] \- org.apache.xmlgraphics:xmlgraphics-commons:jar:2.11:compile
[INFO] +- commons-io:commons-io:jar:2.17.0:compile
[INFO] \- commons-logging:commons-logging:1.3.0:compile
解读:除工程自身外只有 xmlgraphics-commons 及其两个传递依赖。commons-io 与 commons-logging 是编译期依赖(jar tf 可见 org/apache/commons/logging/Log 被 ImageManager 直接引用),jar 直跑场景缺一即报 NoClassDefFoundError。
2.2 编写第一个程序
/opt/xgc-demo/src/main/java/demo/XgcDemo.java 全文(默认入口,直接复制;完整版源码含 TIFF/PS/Base64 等组,见第 4、5 篇文章):
package demo;
import org.apache.xmlgraphics.image.writer.ImageWriter;
import org.apache.xmlgraphics.image.writer.ImageWriterParams;
import org.apache.xmlgraphics.image.writer.ImageWriterRegistry;
import java.awt.Color;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
public class XgcDemo {
static final String OUT = "/opt/xgc-demo/output";
public static void main(String[] args) throws Exception {
new File(OUT).mkdirs();
// 1. 用 Java2D 画一张 320x200 样张:渐变背景 + 矩形 + 文字
BufferedImage img = new BufferedImage(320, 200, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = img.createGraphics();
g2.setPaint(new GradientPaint(0, 0, new Color(30, 80, 200), 320, 200, Color.WHITE));
g2.fillRect(0, 0, 320, 200);
g2.setColor(new Color(220, 40, 40));
g2.fillRect(20, 20, 120, 70);
g2.setColor(Color.BLACK);
g2.setFont(new Font("SansSerif", Font.BOLD, 20));
g2.drawString("XML Graphics PNG", 30, 140);
g2.dispose();
// 2. 按 MIME 从注册表取写出器,300 DPI 写出 PNG
ImageWriter writer = ImageWriterRegistry.getInstance().getWriterFor("image/png");
System.out.println("writer 实现: " + writer.getClass().getName());
System.out.println("writer MIME: " + writer.getMIMEType()
+ ", isFunctional=" + writer.isFunctional());
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());
}
}
程序分两段:先用标准 Java2D 绘制样张,再走 XML Graphics Commons 的写出链路。核心入口是 ImageWriterRegistry.getInstance().getWriterFor("image/png")——按目标 MIME 类型取 ImageWriter 实现,writeImage(RenderedImage, OutputStream, ImageWriterParams) 完成编码。源码文件必须放在 src/main/java/demo/ 约定目录(包 demo 对应子目录),否则触发「No sources to compile」的假成功。
2.3 编译与运行
编译:
cd /opt/xgc-demo && mvn -q -B compile
echo "EXIT=$?"
输出:
EXIT=0
运行(classpath = 编译输出 + 本地仓库三件套):
CP=target/classes:/root/.m2/repository/commons-io/commons-io/2.17.0/commons-io-2.17.0.jar:/root/.m2/repository/commons-logging/commons-logging/1.3.0/commons-logging-1.3.0.jar:/root/.m2/repository/org/apache/xmlgraphics/xmlgraphics-commons/2.11/xmlgraphics-commons-2.11.jar
java -Dfile.encoding=UTF-8 -Djava.awt.headless=true -cp "$CP" demo.XgcDemo
输出:
writer 实现: org.apache.xmlgraphics.image.writer.imageio.ImageIOPNGImageWriter
writer MIME: image/png, isFunctional=true
已写入 /opt/xgc-demo/output/demo.png 字节数=14114
用 file 验证产物是真实 PNG:
file output/demo.png
输出:
output/demo.png: PNG image data, 320 x 200, 8-bit/color RGBA, non-interlaced
2.4 验证点与预期结果
拿到以下结果即集成成功,任一不符按第三章排查:
| 检查项 | 预期(实测值) | 不符时排查 |
|---|---|---|
mvn -q -B compile 退出码 | EXIT=0 | 报 invalid flag: --release → 3.4 节;No sources to compile → 源码目录不对 |
| 运行输出 writer 实现类 | ...imageio.ImageIOPNGImageWriter | getWriterFor 返回 null → classpath 缺主 jar(3.5 节) |
| 运行输出 PNG 字节数 | 14114 | NoClassDefFoundError → 缺传递依赖(3.5 节) |
file 识别 demo.png | PNG image data, 320 x 200, 8-bit/color RGBA | 0 字节 → 输出流未 close |
三、Maven 工程常见坑
以下四个坑在 xmlgraphics-commons 集成中出现频率最高,均为实测复现,给出定位与修复。
3.1 坑一:MultiImageWriter 不是 AutoCloseable,try-with-resources 编译失败
现象:对 MultiImageWriter 使用 try-with-resources,编译报错(实测回显):
[ERROR] /opt/xgc-demo/src/main/java/demo/XgcDemo.java:[126,31] incompatible types: try-with-resources not applicable to variable type
[ERROR] (org.apache.xmlgraphics.image.writer.MultiImageWriter cannot be converted to java.lang.AutoCloseable)
定位:2.11 的 MultiImageWriter 接口只有 writeImage(RenderedImage, ImageWriterParams) 与 close() 两个方法,没有继承 AutoCloseable。修复:改为手动 close(),且 close 必须调用(多帧 TIFF 的页目录在 close 时写出):
OutputStream mout = new FileOutputStream(f4);
MultiImageWriter miw = writer.createMultiImageWriter(mout);
miw.writeImage(frame1, mp);
miw.writeImage(frame2, mp);
miw.close(); // 必须调用,否则文件不完整
mout.close();
3.2 坑二:BUFFERED_IMAGE 解码结果类是 ImageBuffered,不是 ImageBufferedImage
现象:把解码结果强转成 ImageBufferedImage,编译报错(实测回显):
[ERROR] /opt/xgc-demo/src/main/java/demo/XgcDemo.java:[11,48] cannot find symbol
[ERROR] symbol: class ImageBufferedImage
[ERROR] location: package org.apache.xmlgraphics.image.loader.impl
定位:2.11 的 org.apache.xmlgraphics.image.loader.impl 包里没有 ImageBufferedImage 这个类;ImageFlavor.BUFFERED_IMAGE 对应的实现类是 ImageBuffered(jar tf 实测该包内存在 ImageBuffered.class,不存在任何 ImageBufferedImage.class)。修复:
Image image = manager.getImage(info, ImageFlavor.BUFFERED_IMAGE, session);
BufferedImage decoded = ((ImageBuffered) image).getBufferedImage();
网传教程里的 ImageBufferedImage 写法与 2.11 的 jar 不符,写代码前用 jar tf xmlgraphics-commons-2.11.jar | grep loader/impl 核对类名。
3.3 坑三:TIFF 压缩名传 "None" 抛 Unknown compression type
现象:params.setCompressionMethod("None") 写 TIFF,运行期抛异常(实测回显):
Exception in thread "main" java.lang.IllegalArgumentException: Unknown compression type!
at java.desktop/javax.imageio.ImageWriteParam.setCompressionType(ImageWriteParam.java:1040)
at org.apache.xmlgraphics.image.writer.imageio.ImageIOImageWriter.getDefaultWriteParam(ImageIOImageWriter.java:155)
at org.apache.xmlgraphics.image.writer.imageio.ImageIOImageWriter.writeImage(ImageIOImageWriter.java:84)
定位:ImageIOImageWriter 把压缩名直接透传给 JDK 的 ImageWriteParam.setCompressionType,后者只认其内部注册的名称(TIFF:None 不在其中,实测 PackBits、LZW、Deflate 可用)。不设置压缩名即等于不压缩,不要传 "None"。实测可用的三种压缩对比见 4.2 节。
3.4 坑四:老 Maven + 老 compiler 插件不认识 release 语义
Maven 3.6.3 及更早版本默认绑定的 maven-compiler-plugin 3.1 不认识 maven.compiler.release 属性;el8 的 Maven 3.5.4 在 JDK 8 javac 下实测报 Fatal error compiling: invalid flag: --release(完整复现与 JAVA_HOME 修复见第 2 篇 4.4 节)。本文 pom 已显式固定 3.11.0,可避开。
3.5 坑五补充:NoClassDefFoundError 与写出器为 null
classpath 缺 commons-io 或 commons-logging 时,运行到 ImageManager/日志初始化即抛 NoClassDefFoundError;主 jar 缺失时 getWriterFor 返回 null。用 mvn dependency:tree 确认三件套齐全(2.1 节回显)。
四、TIFF 压缩对比与多帧编码
TIFF 是 XML Graphics Commons 写出能力最有特色的一块:支持压缩算法参数与多帧(多页)文件。
4.1 三种压缩实测
写出代码(tiff() 组核心片段,完整源码见第 5 篇文章附录):
ImageWriter writer = ImageWriterRegistry.getInstance().getWriterFor("image/tiff");
System.out.println("writer 实现: " + writer.getClass().getName()
+ ", supportsMultiImageWriter=" + writer.supportsMultiImageWriter());
ImageWriterParams p1 = new ImageWriterParams();
p1.setCompressionMethod("PackBits");
p1.setResolution(300);
writer.writeImage(img, new FileOutputStream(f1), p1); // demo-packbits.tiff
ImageWriterParams p2 = new ImageWriterParams();
p2.setCompressionMethod("Deflate");
writer.writeImage(img, new FileOutputStream(f2), p2); // demo-deflate.tiff
ImageWriterParams p3 = new ImageWriterParams();
p3.setCompressionMethod("LZW");
writer.writeImage(img, new FileOutputStream(f3), p3); // demo-lzw.tiff
运行回显(同一 320x200 渐变+图形样张):
== TIFF 编码(ImageIOTIFFImageWriter,压缩对比) ==
writer 实现: org.apache.xmlgraphics.image.writer.imageio.ImageIOTIFFImageWriter, supportsMultiImageWriter=true
PackBits 压缩: demo-packbits.tiff 字节数=256377
Deflate 压缩: demo-deflate.tiff 字节数=82104
LZW 压缩: demo-lzw.tiff 字节数=156333
压缩率解读(同一样张):Deflate 最小(82104 字节,约为 PackBits 的 32%)、LZW 居中(156333)、PackBits 最大(256377,RLE 对渐变图像效率低)。对扫描件/二值图则 PackBits 常有优势,按素材类型选择;要兼容老 PS 解释器时 PackBits 兼容性最好。
file 验证压缩标识已写入文件头(实测):
file output/demo-packbits.tiff output/demo-deflate.tiff output/demo-lzw.tiff
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
4.2 多帧(多页)TIFF
TIFF 支持一个文件多帧,ImageWriter.createMultiImageWriter() 产出 MultiImageWriter(注意 3.1 节的 close 坑):
OutputStream mout = new FileOutputStream(new File(OUT, "demo-multi.tiff"));
MultiImageWriter miw = writer.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
验证点:2 帧 PackBits 合计 512829 字节,约等于两倍单帧(256377×2=512754)加页目录开销,符合 TIFF 无帧间去重的预期。
五、ImageInfo/ImageLoader 预检与解码
org.apache.xmlgraphics.image.loader 是本库的图像加载管线:ImageManager 负责预检(preload)与解码(getImage),ImageInfo 携带 MIME 与尺寸,ImageContext/ImageSessionContext 携带环境分辨率与会话基准目录。
5.1 预检与解码实测
核心代码(info() 组):
ImageContext ctx = new DefaultImageContext();
System.out.println("DefaultImageContext.getSourceResolution() = " + ctx.getSourceResolution());
ImageContext custom = new ImageContext() {
@Override
public float getSourceResolution() {
return 300.0f;
}
};
System.out.println("自定义 ImageContext(300dpi).getSourceResolution() = " + custom.getSourceResolution());
ImageManager manager = new ImageManager(ctx);
ImageSessionContext session = new DefaultImageSessionContext(ctx, new File(OUT));
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); // 解码
BufferedImage decoded = ((ImageBuffered) image).getBufferedImage();
运行回显:
== 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
5.2 结果解读
- preloadImage 只读头部:三个文件(含 512829 字节的多帧 TIFF)预检瞬间完成,返回的
ImageInfo已含 MIME、像素尺寸、DPI 与 pt 尺寸(320px@300dpi = 76.8pt,计算一致)。 - DPI 精度差异是真实锚点:PNG 的 300 DPI 读回为 299.9994201660156——PNG 物理分辨率按「像素/米」存储(11811 像素/米 ≈ 299.9994 DPI),有量化误差;TIFF 按「像素/英寸」有理数存储,精确还原 300.0。对尺寸敏感的排版逻辑请用容差比较。
- mpt 是毫点(1/1000 pt):
76800x48000 mpt即 76.8x48.0 pt,与getDimensionPt()一致。 - 类型值:PNG 解码后
getType()=6(TYPE_INT_ARGB),TIFF 解码后getType()=0(TYPE_CUSTOM,JDK TIFF 读入器的原生色彩模型)。 DefaultImageContext默认源分辨率 72.0;ImageSessionContext构造第二参数是相对 URI 的基准目录。
💡 技术拓展:图像「读入/写出」方案对比——
方案 读入格式覆盖 写出参数控制 多帧 适用场景 javax.imageio(JDK 原生) PNG/JPEG/GIF/BMP(TIFF/WMF 需 JDK 9+ 插件) 弱(裸 ImageWriteParam) TIFF 支持 简单读写 XML Graphics Commons ImageWriter —(读入走 ImageManager 管线) 强(分辨率/压缩/条带/JPEG 质量统一封装) TIFF 官方支持 打印/排版输出链路 TwelveMonkeys PNG/PSD/WebP/ICNS 等大量读入 同 ImageIO 有限 只补读入格式 本文采用 XML Graphics Commons:写出侧的
ImageWriterParams(DPI、压缩、条带、JPEG 质量)把跨格式参数统一成一个类,且与 FOP 的图像管线同源,ImageInfo预检能力(不落内存先拿尺寸)在版面计算场景不可替代。
六、选型与生态定位
- 与 FOP/Batik 的关系:xmlgraphics-commons 是 Apache FOP(XSL-FO 转 PDF/PS)与 Batik 共用的底层库,图像加载管线与
Graphics2D实现直接服务打印输出;用 FOP 的项目通常已隐式依赖它,直接使用同版本(2.11 配 FOP 2.9+)可避免冲突。 - 什么场景选它:需要「带物理分辨率的图像写出」「多帧 TIFF」「PostScript/EPS 生成」「PS 常用编码流(Base64/ASCII85/Flate)」时是 Java 生态的标准答案。
- 什么场景不选它:只做 PDF 文档交付选 PDFBox;只补 ImageIO 读入格式选 TwelveMonkeys;纯 HTTP 服务端缩略图用 JDK ImageIO 足矣。
七、参考链接
- Apache XML Graphics Commons 官方主页:https://xmlgraphics.apache.org/commons/
- 2.11 Javadoc(image.loader/image.writer 包):https://xmlgraphics.apache.org/commons/javadoc/2.11/
- Maven 中央仓库坐标页:https://repo1.maven.org/maven2/org/apache/xmlgraphics/xmlgraphics-commons/
- 官方图像加载管线说明(ImageLoader User Guide):https://xmlgraphics.apache.org/commons/image-loader.html
- Apache FOP:https://xmlgraphics.apache.org/fop/