Apache XML Graphics Commons 2.11 图形绘制与进阶实战详解
深入 Apache XML Graphics Commons 2.11 的图形绘制与进阶能力,涵盖 PSGenerator 生成符合 DSC 3.0 的 PostScript(含 GS/GR 命令映射与字符串转义实测)、GraphicContext 图形状态管理、CMYKColorSpace 的 fromRGB 未实现异常与 toRGB 实测、UnitConv 浮点精度锚点及 headless 无显示器绘图,全部已在 Ubuntu 22.04 实测。
Apache XML Graphics Commons 2.11 图形绘制与进阶实战详解
目标:掌握 Apache XML Graphics Commons 2.11 的图形绘制面——用 PSGenerator 产出符合 DSC 3.0 规范的 PostScript 文件、用 GraphicContext 管理图形状态、正确使用 CMYKColorSpace(含 fromRGB 未实现的实测坑)、用 UnitConv/MimeConstants/ImageWriterUtil 完成单位换算与快捷写出,并在无显示器的服务器上以 headless 模式稳定绘图。以下全部命令与回显均已在 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 节);示例程序按功能分组(png/tiff/info/ps/base64/headless/advanced),与第 3~4 篇共用同一工程。
二、API 总览
本文覆盖的图形绘制与工具 API(均经实测):
| API | 包 | 角色 | 关键方法/常量 | 详解章节 |
|---|---|---|---|---|
| PSGenerator | ps | PostScript 输出器(DSC 注释+PS 命令) | writeDSCComment、writeln、saveGraphicsState、restoreGraphicsState、showPage、formatDouble、formatDouble5、convertStringToDSC | 3 |
| DSCConstants | ps | DSC 注释名常量 | CREATOR、TITLE、PAGES、EOF 等 | 3.1 |
| PSGState / PSState | ps | 图形状态快照 | (saveGraphicsState 内部使用) | 3.3 |
| GraphicContext | java2d | Java2D 图形状态集合 | setColor、setStroke、setClip、translate、scale、getTransform | 4.1 |
| CMYKColorSpace | java2d | CMYK 色彩空间 | getInstance、toRGB、fromRGB、getType | 4.2 |
| UnitConv | util | pt/mm/in 换算 | IN2PT、IN2MM、mm2pt、in2pt、pt2mm | 5.1 |
| MimeConstants | util | MIME 常量表 | MIME_POSTSCRIPT、MIME_PNG、MIME_TIFF | 5.2 |
| ImageWriterUtil | image.writer | 快捷写出工具 | saveAsPNG、saveAsFile | 5.3 |
⚠️ 注意:
org.apache.xmlgraphics.pdf包在 2.11 中不存在(jar tf实测),PDF 生成属于 Apache FOP 的能力;本库的ps包是 PostScript 输出的完整实现。绘制入口可分两类:直接写 PS 文本协议(PSGenerator),或在标准Graphics2D抽象下绘制(java2d 包 + image.writer 写出栅格)。
三、PSGenerator 详解
PSGenerator 包装一个 OutputStream,提供 DSC 注释输出、PS 命令行写出、图形状态栈与数值/字符串格式化,是 FOP PostScript 后端的基础构件。
3.1 最小 PS 输出与逐行解读
ps() 组完整代码:
File target = new File(OUT, "demo.ps");
try (OutputStream out = new FileOutputStream(target)) {
PSGenerator gen = new PSGenerator(out);
gen.writeln("%!PS-Adobe-3.0");
gen.writeDSCComment(DSCConstants.CREATOR, "Apache XML Graphics Commons demo");
gen.writeDSCComment(DSCConstants.TITLE, "PSGenerator demo page");
gen.writeDSCComment(DSCConstants.PAGES, "1");
gen.writeln("%%Page: 1 1");
gen.writeln("/Helvetica findfont 18 scalefont setfont");
gen.writeln("50 740 moveto (Hello from XML Graphics Commons) show");
gen.saveGraphicsState();
gen.writeln("0 0 1 setrgbcolor 2 setlinewidth");
gen.writeln("50 700 moveto 500 700 lineto stroke");
gen.writeln("1 0 0 setrgbcolor");
gen.writeln("newpath 50 520 moveto 300 0 rlineto 0 150 rlineto -300 0 rlineto closepath fill");
gen.restoreGraphicsState();
gen.writeln("0 setgray 50 490 moveto (Drawn by PSGenerator) show");
gen.showPage();
gen.writeDSCComment(DSCConstants.EOF);
gen.flush();
}
System.out.println("已写入 " + target.getAbsolutePath() + " 字节数=" + target.length());
运行回显(实测):
== PostScript 输出(PSGenerator) ==
formatDouble(72.0/300) = 0.24
已写入 /opt/xgc-demo/output/demo.ps 字节数=451
文件头前 12 行逐行解读(head -12 实测):
%!PS-Adobe-3.0
%%Creator: Apache XML Graphics Commons demo
%%Title: PSGenerator demo page
%%Pages: 1
%%Page: 1 1
0.24
/Helvetica findfont 18 scalefont setfont
50 740 moveto (Hello from XML Graphics Commons) show
GS
0 0 1 setrgbcolor 2 setlinewidth
50 700 moveto 500 700 lineto stroke
1 0 0 setrgbcolor
- 第 1 行
%!PS-Adobe-3.0是 PS 文件头(手写 writeln),writeDSCComment(DSCConstants.CREATOR, ...)输出%%Creator: ...格式的 DSC 注释; %%Page: 1 1手写开启页面;GS一行是关键实测锚点:saveGraphicsState()不是输出 PS 语言的gsave,而是输出库内部命令映射表中的缩写命令GS(配套restoreGraphicsState()输出GR)——这类短命令需要配套的过程定义(FOP 的PSPageResources/prolog 提供 GS/GR 等过程的定义)才成立,脱离 FOP 独立产出「可直接 RIP 解释」的 PS 时,要么自己定义这两个过程(如/GS {gsave} bind def),要么直接写gsave/grestore字面命令;- 文件尾(
tail -3实测):showpage翻页与%%EOF结束注释都由showPage()/writeDSCComment(DSCConstants.EOF)正确产出。
file 验证格式(实测):
file /opt/xgc-demo/output/demo.ps
/opt/xgc-demo/output/demo.ps: PostScript document text conforming DSC level 3.0
验证点:file 识别为「符合 DSC level 3.0 的 PostScript 文本」,DSC 头/页/EOF 结构完整。
3.2 数值与字符串工具方法
PSGenerator 提供三个高频工具方法,实测行为如下:
PSGenerator tmp = new PSGenerator(new ByteArrayOutputStream());
System.out.println("formatDouble(72.0/300)=" + tmp.formatDouble(72.0 / 300)
+ " formatDouble5(3.14159265)=" + tmp.formatDouble5(3.14159265));
System.out.println("convertStringToDSC(a(b)c\\\\d) = "
+ PSGenerator.convertStringToDSC("a(b)c\\d"));
输出(advanced 组回显):
formatDouble(72.0/300)=0.24 formatDouble5(3.14159265)=3.14159
convertStringToDSC(a(b)c\d) = a\(b\)c\\d
formatDouble:按 PS 数值规范输出固定小数位(自动去尾零),72.0/300实测输出0.24,坐标换算(px→pt)用它保证 PS 数值合法性;formatDouble5:固定 5 位小数,实测3.14159(第六位起截断);convertStringToDSC(静态方法):转义 DSC 注释中的括号与反斜杠,实测a(b)c\d→a\(b\)c\\d——写入%%Title/%%Creator等注释前必须过这个方法,否则括号会破坏 DSC 解析。
3.3 图形状态与命令映射
saveGraphicsState()/restoreGraphicsState() 在内部维护 PSState 状态栈(PSGState 持有颜色、线宽、变换等状态快照),并输出映射后的短命令 GS/GR(3.1 节实测)。showPage() 输出 showpage 并自动重置状态栈,writeDSCComment(name, param...) 负责所有 %%Name: value 注释行。直接落盘可解释文件时,命令映射问题按 3.1 节给出的两种方式处理。
四、java2d 包详解
4.1 GraphicContext:图形状态集合
GraphicContext 是「Java2D 状态集合」的纯对象实现(不连接任何绘制目标),保存颜色、画笔、裁剪、复合、变换、渲染提示等,供自定义 Graphics2D 实现复用。实测:
GraphicContext gc = new GraphicContext();
gc.setColor(new Color(220, 40, 40));
gc.setStroke(new BasicStroke(2.5f));
gc.setClip(new Rectangle(0, 0, 320, 200));
System.out.println("color=" + gc.getColor()
+ " 线宽=" + ((BasicStroke) gc.getStroke()).getLineWidth()
+ " clip=" + gc.getClipBounds());
gc.translate(30, 20);
gc.scale(2.0, 2.0);
System.out.println("translate(30,20)+scale(2,2) 后 transform=" + gc.getTransform());
输出(advanced 组回显):
color=java.awt.Color[r=220,g=40,b=40] 线宽=2.5 clip=java.awt.Rectangle[x=0,y=0,width=320,height=200]
translate(30,20)+scale(2,2) 后 transform=java.awt.geom.AffineTransform[[2.0, 0.0, 30.0], [0.0, 2.0, 20.0]]
解读:translate+scale 的复合结果正确累进到 AffineTransform(平移 30,20 后再缩放 2 倍,矩阵第三列为平移量)。javap 实测同族实现还包括 DefaultGraphics2D(基于 GraphicContext 的最小 Graphics2D)与抽象基类 AbstractGraphics2D——FOP 的 PDFGraphics2D/PSGraphics2D 即在此基础上扩展。
4.2 CMYKColorSpace:toRGB 可用,fromRGB 未实现(实测坑)
CMYKColorSpace.getInstance() 取实例后,fromRGB 在 2.11 中未实现(实测抛出,捕获后打印):
CMYKColorSpace cmyk = CMYKColorSpace.getInstance();
try {
cmyk.fromRGB(new float[] {1f, 0f, 0f});
System.out.println("fromRGB 可用");
} catch (UnsupportedOperationException e) {
System.out.println("fromRGB(new float[]{1,0,0}) 抛出 "
+ e.getClass().getSimpleName() + ": " + e.getMessage());
}
System.out.println("toRGB(C=0,M=1,Y=0,K=0 100%青) = " + Arrays.toString(cmyk.toRGB(new float[] {0f, 1f, 0f, 0f})));
System.out.println("toRGB(C=0,M=0,Y=0,K=1 100%黑) = " + Arrays.toString(cmyk.toRGB(new float[] {0f, 0f, 0f, 1f})));
System.out.println("getType() = " + cmyk.getType() + "(ColorSpace.TYPE_CMYK=9)");
输出(advanced 组回显):
== CMYKColorSpace ==
fromRGB(new float[]{1,0,0}) 抛出 UnsupportedOperationException: NYI
toRGB(C=0,M=1,Y=0,K=0 100%青) = [1.0, 0.0, 1.0]
toRGB(C=0,M=0,Y=0,K=1 100%黑) = [0.0, 0.0, 0.0]
getType() = 9(ColorSpace.TYPE_CMYK=9)
三个实测结论:
fromRGB抛UnsupportedOperationException: NYI(Not Yet Implemented):把CMYKColorSpace设进Graphics2D并绘制 RGB 颜色时会在颜色转换路径触发该异常,2.11 不要在生产路径使用 fromRGB;toRGB可用且物理正确:100% 青映射为 RGB(1,0,1)(等量绿红反色近似)、100% 黑映射为 RGB(0,0,0);getType()实测返回 9,对应ColorSpace.TYPE_CMYK(常量值 9),可用于运行期判断色彩空间类型。
五、单位换算与常量工具
5.1 UnitConv:pt/mm/in 换算
System.out.println("常量 IN2PT=" + UnitConv.IN2PT
+ " IN2MM=" + UnitConv.IN2MM);
System.out.println("mm2pt(25.4)=" + UnitConv.mm2pt(25.4)
+ " in2pt(1)=" + UnitConv.in2pt(1.0)
+ " pt2mm(72)=" + UnitConv.pt2mm(72.0));
输出(advanced 组回显):
常量 IN2PT=72.0 IN2MM=25.4
mm2pt(25.4)=72.00000108133152 in2pt(1)=72.0 pt2mm(72)=25.399999618530273
实测锚点:常量 IN2MM 是 float,25.4f × 72/25.4 的浮点路径让 mm2pt(25.4) 返回 72.00000108133152 而非精确 72,pt2mm(72) 同理返回 25.399999618530273。排版代码比较尺寸时留 1e-3 量级容差,或先用 IN2PT 常量做 pt↔in 换算(实测 in2pt(1) 恰为 72.0)。
5.2 MimeConstants:MIME 常量表
格式相关的 MIME 字符串全部有常量可查(实测打印):
System.out.println("MIME_POSTSCRIPT=" + MimeConstants.MIME_POSTSCRIPT);
System.out.println("MIME_PNG=" + MimeConstants.MIME_PNG
+ " MIME_TIFF=" + MimeConstants.MIME_TIFF);
MIME_POSTSCRIPT=application/postscript
MIME_PNG=image/png MIME_TIFF=image/tiff
用常量替代魔法字符串传给 getWriterFor/ImageManager,与 FOP 管线保持一致。
5.3 ImageWriterUtil:一行式快捷写出
ImageWriterUtil 提供免注册表的一行式写出(内部自动按 MIME 选 writer):
ImageWriterUtil.saveAsPNG(drawSample(160, 90, "Util PNG"), new File(OUT, "util.png"));
System.out.println("util.png 字节数=" + new File(OUT, "util.png").length());
ImageWriterUtil.saveAsFile(drawSample(160, 90, "Util 300dpi"), 300, new File(OUT, "util300.png"), "image/png");
System.out.println("util300.png 字节数=" + new File(OUT, "util300.png").length());
输出(advanced 组回显):
util.png 字节数=3418
util300.png 字节数=3465
file 验证(实测):
/tmp/commons-batch1-b2/xmlgraphics/out/util.png: PNG image data, 160 x 90, 8-bit/color RGBA, non-interlaced
saveAsPNG 用默认参数(无 DPI),saveAsFile 可指定分辨率与 MIME;比 ImageWriterRegistry 三步走简洁,但参数控制面窄,正式代码仍推荐 3.3 节的显式写法。
六、headless 模式实战
服务器通常无图形界面,AWT 绘图必须工作在 headless 模式。实测对比三种状态:
状态一:显式 -Djava.awt.headless=true(推荐,完整回显)
java -Djava.awt.headless=true -Dfile.encoding=UTF-8 -cp "$CP" demo.XgcDemo headless
== headless 模式绘图 ==
系统属性 java.awt.headless = true
GraphicsEnvironment.isHeadless() = true
已生成 /opt/xgc-demo/output/headless.png 字节数=5432
可用字体族数量=8 前3个=[Bitstream Charter, Cantarell, Courier 10 Pitch]
状态二:不设置该属性(本机 DISPLAY 为空,自动判定)
echo "DISPLAY=[$DISPLAY]"
java -Dfile.encoding=UTF-8 -cp "$CP" demo.XgcDemo headless | head -3
DISPLAY=[]
== headless 模式绘图 ==
系统属性 java.awt.headless = <未设置>
GraphicsEnvironment.isHeadless() = true
产物验证(实测)
file /opt/xgc-demo/output/headless.png
/opt/xgc-demo/output/headless.png: PNG image data, 240 x 160, 8-bit/color RGBA, non-interlaced
实测结论:
- headless 下全链路可用:
BufferedImage绘制、drawString文字渲染、PNG 编码在isHeadless()=true时全部正常,XML Graphics Commons 的图像写出不依赖 X11; - 未设置时自动判定:JVM 按
DISPLAY等环境变量推断(本机DISPLAY=[],实测isHeadless()=true),但生产脚本建议显式声明,避免误在有 X 环境的机器上走 GUI 路径; - 字体族差异是环境事实:同一代码在本文环境实测 8 个字体族(Bitstream Charter、Cantarell、Courier 10 Pitch 等),Ubuntu 装全
fonts-dejavu-core后实测 12 个(DejaVu Sans 等),el8 实测 11 个——逻辑字体SansSerif的物理映射因机器而异,跨机器一致渲染请显式Font.createFont加载 TTF(见第 1 篇 7.3 节)。
七、注意事项与常见问题
Q1:saveGraphicsState 输出 GS 而不是 gsave,生成的 PS 无法解释?
GS/GR 是库内部命令映射(3.1 节实测),配套过程定义在 FOP 的 prolog 里。独立产出可解释的 PS:定义 /GS {gsave} bind def、/GR {grestore} bind def(及配套过程),或绕开状态栈直接写 gsave/grestore 字面命令。
Q2:CMYKColorSpace.fromRGB 抛 UnsupportedOperationException: NYI?
2.11 的已知实现状态(实测复现见 4.2 节)。RGB→CMYK 转换请自行实现或引入 ICC 色彩管理(ICC_Profile/ICC_ColorSpace),本类只可靠支持 toRGB。
Q3:mm2pt(25.4) 不是精确的 72?
浮点常量精度问题(IN2MM 为 float,实测 4.1 节)。排版比较用容差,或换算顺序改为先用 IN2PT。
Q4:DSC 注释里的括号导致解析错误?
%%Title/%%Creator 等注释值必须先过静态方法 PSGenerator.convertStringToDSC() 转义括号与反斜杠(实测见 3.2 节)。
Q5:headless 下 drawString 用了和预期不同的字体?
逻辑字体按机器字体族映射(实测三台环境字体族数量 8/11/12 各不相同)。跨机器一致渲染用 Font.createFont 加载 TTF 并 deriveFont 指定字号。
Q6:PSGenerator 的输出流要手动 close 吗?
flush() 刷出缓冲;外层 OutputStream 的关闭由调用方 try-with-resources 负责(3.1 节代码写法),与 util.io 编码流的 close 语义(第 4 篇 5.1 节)区分开。
八、参考链接
- Apache XML Graphics Commons 官方主页:https://xmlgraphics.apache.org/commons/
- 2.11 Javadoc(ps/java2d 包):https://xmlgraphics.apache.org/commons/javadoc/2.11/
- PostScript 语言参考(PS 命令语义):https://www.adobe.com/products/postscript/pdfs/PLRM.pdf
- DSC 规范说明(file 命令识别依据):https://web.archive.org/web/20220120211743/https://partners.adobe.com/public/developer/en/ps/5001.DSC_Spec.pdf
- Apache FOP PostScript 渲染器(GS 命令映射的使用方):https://xmlgraphics.apache.org/fop/trunk/postscript.html