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 CommonsApache XML Graphics Commons2026/10/15

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包角色关键方法/常量详解章节
PSGeneratorpsPostScript 输出器(DSC 注释+PS 命令)writeDSCComment、writeln、saveGraphicsState、restoreGraphicsState、showPage、formatDouble、formatDouble5、convertStringToDSC3
DSCConstantspsDSC 注释名常量CREATOR、TITLE、PAGES、EOF 等3.1
PSGState / PSStateps图形状态快照(saveGraphicsState 内部使用)3.3
GraphicContextjava2dJava2D 图形状态集合setColor、setStroke、setClip、translate、scale、getTransform4.1
CMYKColorSpacejava2dCMYK 色彩空间getInstance、toRGB、fromRGB、getType4.2
UnitConvutilpt/mm/in 换算IN2PT、IN2MM、mm2pt、in2pt、pt2mm5.1
MimeConstantsutilMIME 常量表MIME_POSTSCRIPT、MIME_PNG、MIME_TIFF5.2
ImageWriterUtilimage.writer快捷写出工具saveAsPNG、saveAsFile5.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)

三个实测结论:

  1. fromRGB 抛 UnsupportedOperationException: NYI(Not Yet Implemented):把 CMYKColorSpace 设进 Graphics2D 并绘制 RGB 颜色时会在颜色转换路径触发该异常,2.11 不要在生产路径使用 fromRGB;
  2. toRGB 可用且物理正确:100% 青映射为 RGB(1,0,1)(等量绿红反色近似)、100% 黑映射为 RGB(0,0,0);
  3. 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

实测结论:

  1. headless 下全链路可用:BufferedImage 绘制、drawString 文字渲染、PNG 编码在 isHeadless()=true 时全部正常,XML Graphics Commons 的图像写出不依赖 X11;
  2. 未设置时自动判定:JVM 按 DISPLAY 等环境变量推断(本机 DISPLAY=[],实测 isHeadless()=true),但生产脚本建议显式声明,避免误在有 X 环境的机器上走 GUI 路径;
  3. 字体族差异是环境事实:同一代码在本文环境实测 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 节)区分开。

八、参考链接

  1. Apache XML Graphics Commons 官方主页:https://xmlgraphics.apache.org/commons/
  2. 2.11 Javadoc(ps/java2d 包):https://xmlgraphics.apache.org/commons/javadoc/2.11/
  3. PostScript 语言参考(PS 命令语义):https://www.adobe.com/products/postscript/pdfs/PLRM.pdf
  4. DSC 规范说明(file 命令识别依据):https://web.archive.org/web/20220120211743/https://partners.adobe.com/public/developer/en/ps/5001.DSC_Spec.pdf
  5. Apache FOP PostScript 渲染器(GS 命令映射的使用方):https://xmlgraphics.apache.org/fop/trunk/postscript.html