本文是作者在SVGGIS系统的开发实践过程中关于SVG坐标转换的总结。在描述SVG坐标变换原理的同时,使用Apache Batik项目实现了相关例子。
SVG是一种用XML语言来描述二维图形对象的语言,SVG允许三种图形对象:1.矢量图形,2.图片,3.文本对象。这三种图形对象都可以支持分组,使用样式渲染,进行几何变换。
SVG还能够通过脚本来实现交互操作和动态显示。可以通过定义动画对象或使用script脚本来实现动画。
1 SVG下几种常见的几何变换方式
1.1 一个SVG例子
我们首先来看一个SVG的例子,窗口右上角有四个色块,每个色块是一个50×50的矩形。
图1 一个SVG的样本

图2 样本文档
<?xml version="1.0"?>
<svg >
<g>
<rect x="0" y="0" width="50" height="50" style="fill:red" />
<rect x="50" y="0" width="50" height="50" style="fill:yellow" />
<rect x="0" y="50" width="50" height="50" style="fill:green" />
<rect x="50" y="50" width="50" height="50" style="fill:black" />
</g>
</svg>
1.2 使用Adobe SVG Viewer展示在SVG文档中实现的几何变换
缩放<g transform="scale(2)">
图3 放大一倍

平移<g transform="translate(200 ,200)">
图4 平移200,200个像素

旋转<g transform="rotate(45)">
图5 顺时针旋转45度

横切<g transform="skewX(45)">
图6 以y轴为基线在X方向横切45度

2 在Batik下实现SVG的几何变换
2.1 Batik的基础知识
2.1.1 Batik的用途
Batik是基于java语言实现的一个SVG应用的工具集,用于实现对SVG对象的显示、编辑以及将SVG图形对象转换成其他图片格式,如jpg、gif等。
这个项目的目标就是给开发人员一套用于处理或应用SVG对象的基础核心模型。作为Apache项目成员之一,该项目也为开发人员提供了一个开发的可扩展的平台。同时batik也维护了一个可以查看SVG文件的浏览器。虽然batik还没有完全实现SVG的所有标准语法和标记,但通过比较不同版本的区别就会发现,他正在以很高的效率覆盖SVG所有的标准。
2.1.2 让我们实现一个简单的Batik程序
首先让我们实现一个简单的基于Batik的SVG浏览器。Batik封装了org.apache.batik.swing.JSVGCanvas对象可以用于在swing中嵌入SVG显示容器,并可以通过org.apache.batik.swing.JSVGCanvas提供的方法对SVG文档和图像进行操作。这个浏览器可以支持大部分SVG的语法和标准包括脚本交互的功能,但暂时还没有引入动画。关于动画和脚本交互的内容我们会在以后的文章中讲述,今天先集中解决几何变换的问题。
图7 运行程序打开我们编写的SVG的例子

可以通过该页面引导运行该程序:
从附件中可以查看该程序的完整代码,也可通过网上下载地址运行该程序。
图8 将一个SVGCanvas添加到界面
private javax.swing.JPanel SVGPanel = new javax.swing.JPanel();
private JSVGCanvas svgCanvas = new org.apache.batik.swing.JSVGCanvas();
SVGPanel.add("Center", svgCanvas)
2.2 通过Batik的GVT模型实现SVG的几何变换
2.2.1 为什么要使用Batik来实现SVG的几何变换
当我们用Batik工具集作为SVG客户端的解决方案时,如缩放平移这样的操作就在所难免了。但Batik并没有直接支持如Adobe SVG Viewer那样的鼠标拖动几何变换的操作,这就要求我们对这些功能进行编程处理。在分析SVG的几何变换的细节之前,先让我们了解一下基本的操作编程方式。
2.2.2 Batik下通过java.awt.geom.AffineTransform来实现SVG的几何变换
JSVGCanvas提供方法可以获取java.awt.geom.AffineTransform对象。AffineTransform是用于实现2D几何图形坐标变换处理的对象,可以通过该对象进行二维几何空间中两个坐标系的相互映射和变换。平移:
//向左和向下平移50个像素
java.awt.geom.AffineTransform rat = svgCanvas.getRenderingTransform();
rat.translate(50,50);
svgCanvas.setRenderingTransform(rat)
缩放:
//以屏幕左上角原点为固定点进行缩放操作
java.awt.geom.AffineTransform rat = svgCanvas.getRenderingTransform();
rat.scale(0.5,0.5);
svgCanvas.setRenderingTransform(rat)
旋转:
//以屏幕左上角原点为固定点进行旋转缩放
java.awt.geom.AffineTransform rat = svgCanvas.getRenderingTransform();
rat.rotate(3.1415926/4);
svgCanvas.setRenderingTransform(rat)
复合变换:
//一个综合平移、放大和旋转90度的复合变换
java.awt.geom.AffineTransform rat = svgCanvas.getRenderingTransform();
rat.translate(50,50);
rat.scale(2,2);
rat.rotate(3.1415926/4);
svgCanvas.setRenderingTransform(rat)
3 当我们需要进行复合几何变换的时候
3.1 先来让我们通过不同的变换代码组合实现复合几何变换
先让我们看第一个例子:
//放大一倍和平移50个像素的复合变换
AffineTransform rat = svgCanvas.getRenderingTransform();
rat.scale(2,2);
rat.translate(50,50);
svgCanvas.setRenderingTransform(rat)
图9 复合变换一

可以看得出来,这个变换的最终效果是:图形的形状放大一倍,原图形的(0,0)原点坐标平移100个像素。
再来看第二个例子:
//放大一倍和平移50个像素的复合变换
AffineTransform rat = svgCanvas.getRenderingTransform();
rat.translate(50,50);
rat.scale(2,2);
svgCanvas.setRenderingTransform(rat);
图10 复合变换二

这个变换的最终效果是:图形的形状放大一倍,原图形的(0,0)原点坐标平移50个像素。
3.2 关键是顺序
比较一下这两种平移后的效果会发现,只是因为缩放和平移的顺序不同,变换后的结果就发生了区别。第一个例子实际平移的不是50个像素,而是100个像素。而第二个例子则是平移了50个像素。
有兴趣的读者可以添加其他几何变换方式,并测试不同的变换顺序,会发现复合变换的顺序与复合变换的最终效果是紧密相关的。那么如何分析和计算复合变换的变换结果呢?这里我们需要补充一点数学知识了。
3.3 对单一的几何变换进行数学模型分析
对计算机图形学中图形变换相关理论很熟悉的人可以跳过这部分直接看Batik的实现方式。这节使用的齐次式图片引用自SVG标准中关于坐标变换的齐次式的例子插图。
首先我们来了解一下图形变换的齐次式计算方法:
图11 基本几何变换齐次式

这是一个基本图形变换齐次式。等式的最右边是一个坐标点未变换前的坐标矩阵,最左边是该坐标点变换后所在位置的一个三行一列的坐标矩阵,中间那个三行三列的矩阵就是变换矩阵。不同的变换方式将对应不同的变换矩阵,图形平移效果就是通过这样一个变换齐次式来实现的。
平移:如果需要将图形平移(tx,ty)个坐标时,采用如下的变换矩阵带入变换方程。
图12 平移变换矩阵

缩放:如果需要在x轴方向实现sx倍缩放,在y轴方向实现sy轴缩放时,采用如下的变换矩阵带入变换方程。
图13 缩放变换矩阵

旋转:如果需要将图像旋转a度时,使用如下的变换矩阵带入变换方程。
图14 旋转变换矩阵

3.4 采用复合几何变换的数学模型分析
3.4.1 数学分析
当两组变换同时作用于同一个图像时,连续使用该等式,得出如下等式。由于做变换的时候是将变换矩阵放在矩阵积的左边,所以对于复合变换的式子,应该从右向左进行读。对于如下的式子:从右至左依次是变换前的坐标,第一次转换的转换矩阵,第二次转换的转换矩阵,转换后的坐标值。
图15 进行两次变换的复合矩阵

进一步推导n次几何变换的复合变换等式:
图16 n次变换的复合矩阵

图17 变换前效果

使用这样一个复合变换方式:
<g transform="translate(50,90) rotate(-45) translate(130,160)">
根据前面的分析结果带入变换方程,得出如下等式
图18 推导矩阵

根据计算后的变换矩阵,可得变换结果是图形整体平移到(255.03,111.21),同时图形自身沿顺时针方向旋转45度。查看实际变换结果,这里我们可以发现,对于SVG的变换来说,虽然我们习惯上从左往右写变换参数的,实际上图形做变换的时候是从右边的变换参数开始依次进行图形变换的。
图19 变换后效果

3.5 分析一下Batik是如何实现SVG的复合几何变换的
3.5.1 先看第一个例子
这个变换是先放大后平移的变换,其变换效果最终是,先将图形的形状放大一倍,然后将图形整个平移100个像素。这里我们可以看出,虽然我们是先进行的放大变换,后进行的平移操作,但当我们进行复合变换的时候由于实际运算时上是先进行了平移,后进行了缩放。或者简单的理解为从左向右先写缩放矩阵,再写平移矩阵,这样得出的变换矩阵就可以对变换后的效果进行计算了。//放大一倍和平移50个像素的复合变换
//<g transform=" scale(2 ) translate(50 50)">
AffineTransform rat = svgCanvas.getRenderingTransform();
rat.scale(2,2);
rat.translate(50,50);
svgCanvas.setRenderingTransform(rat);
我们可以将这个变换对应的计算矩阵写出来:
图20 复合变换例一的变换矩阵

根据上面的分析我们可以看的出,先进行缩放变换在进行平移变换的复合变换时,变换后原图元的坐标会映射到新的位置,其中:
X1=Sx(X+dx)
Y1=Sy(Y+dy)
这个变换是先放大后平移的变换,其变换效果最终是,先将图形整个平移50个像素,然后将图形的形状放大一倍。
//放大一倍和平移50个像素的复合变换
//<g transform="translate(50,50) scale(2 )">
AffineTransform rat = svgCanvas.getRenderingTransform();
rat.translate(50,50);
rat.scale(2,2);
svgCanvas.setRenderingTransform(rat);
我们可以将这个变换对应的svg文档实现写出来:
<g transform="translate(50 50) scale(2)">
图21 复合变换例二的变换矩阵

根据上面的分析我们可以看的出,先进行平移变换再进行缩放变换的复合变换时,变换后原图元的坐标会映射到新的位置,其中:
X1=Sx*X+dx
Y1=Sy*Y+dy
假设我们需要将图形原点的位置移动到(150,300),同时形状放大到原来的3倍,该如何进行变换来实现这样的效果呢?
图22 变换效果

使用第一个例子的分析结果,先进行缩放变换,再进行平移变换的方程变换:
图23 方程推演

根据推导可知需先进行3倍的缩放变换,再平移(50,100)个坐标就可以实现指定的变换效果。实现程序如下
//<g transform=" scale(3 ) translate(50,100)">
AffineTransform rat = svgCanvas.getRenderingTransform();
rat.scale(3,3);
rat.translate(50,100);
svgCanvas.setRenderingTransform(rat);
若先进行平移变换,再进行缩放变换的话,根据第二个例子的推导结果:
图24 方程推演

可知应先进行(150,300)的平移操作,再实现3倍的缩放操作。实现程序如下:
//<g transform=" translate(150,300) scale(3 )">
AffineTransform rat = svgCanvas.getRenderingTransform();
rat.translate(150,300);
rat.scale(3,3);
svgCanvas.setRenderingTransform(rat);
4 一个常用复合变换实例的实现——定点变换
上边我们分析了Batik实现SVG图形变换的原理和计算方法。下面我们用分析的结果实现一个常用的变换实例:定点变换。变换的种类包括缩放,旋转(一般不考虑定点平移这个概念)。
所谓定点变换就是在图形变换中指定一个固定的点,在变换结束后,该点的位置不发生变化。定点变换在GIS的实际运用中很常见,比如将地图放大到指定倍数而保持地图的某个位置(如:鼠标点击的位置)不发生变化。我们以定点缩放为例描述定点变换的使用方法。
假设我们用(x1,y1)点来做定点变换的基准点进行几何变换,要求实现变换后(x1,y1)的位置不变。
定点变换的基本思想是基于这样一个特性:当图像进行缩放,或旋转变换的时候,坐标原点的位置并不发生变化。定点变换的实现方法就是,先将基准点(x1,y1)平移到原点即做一次[-x1,-y1]变换,进行变换后再将变换后的原点平移到(x1,y1)即再进行一次[x1,y1]变换。
图25 基于(x1,y1)定点变换

实际编程的时候,我们无需对定点变换做如此复杂的运算。例如可以采用如下的方式实现针对(50,50)的定点变换:
//以(50,50)点为基准点进行几何变换
//<g transform="translate(50,50) …………… translate(-50,-50)">
AffineTransform rat = svgCanvas.getRenderingTransform();
rat.translate(50,50);
//其他变换方式
………………
………………
………………
rat.translate(-50,-50);
svgCanvas.setRenderingTransform(rat);
参考资料
有关SVG的背景知识,请阅读developerWorks上的教程,"可伸缩向量图形介绍"
Scalable Vector Graphics (SVG) 1.0 Specification
Batik项目介绍http://xml.apache.org/batik/
关于作者
陈珂,技术总监,南京安元科技
从业以来一直从事政府信息化建设和GIS的相关工作,自从多年前接触SVG以来就对其产生极大的兴趣。现在南京安元科技担任技术总监。您可以通过http://sourceforge.net/projects/antgis/访问我创建的基于SVG的GIS开源项目,我把它叫做AntGIS(小而强大)。
(THE END)
如果你喜欢这篇文章,请阅读本文相关的下列内容:
