内容


使用脚本动态操作 SVG 文档

本文主要介绍在 SVG 中通过编程实现动态操作 SVG 图像的知识。

SVG 图像的结构是用 XML 文档表示的,因此可以使用 XML 编程技术如"文档对象模型(Document Object Model,DOM)"来操纵它。本文描述了如何使用 ECMAScript/JavaScript 来支持与 SVG 图像的交互。理论上说我们可以用这些知识实现类似射击游戏这样复杂的图形交互程序。

有两种方法可以对SVG文档的DOM对象进行操作:通过JavaScript在SVG文档内部进行处理;在Batik环境下通过相关接口获取当前显示SVG视图的DOM对象引用使用Java语言对SVG文档进行处理。本文重点描述使用JavaScript对SVG进行操作的相关技术,并在文章最后用一个简单的例子实现Batik下通过Java实现操作DOM对象。另外还用相当的篇幅讨论了常用SVG浏览工具中支持的特殊 ECMAScript/JavaScript 用法,这些方法可以显著提高我们的开发速度。

1. 理解 SVG 对象结构

在 SVG 浏览器上下文环境("上下文环境"一词来自"context"的直译)中,除了 SVG 本身作为 XML 文档所包含的 DOM 对象外,还包含一些其他对象,这些支持对象随着浏览工具的不同而在细节上有所区别。

图 1. SVG 对象结构
图 1. SVG 对象结构
图 1. SVG 对象结构

Window 是一个全局变量,该变量表示 SVG 运行时的浏览器窗口对象。因为脚本的运行就是在 window 对象内部进行的,所以调用该对象方法和属性时可以省去对 window 变量的指定,例如 window.document 可以直接通过 document 实现引用。完全介绍 window 对象的属性和方法内容已经超出了本文的范围,有兴趣的读者可以通过参考资料查阅详细说明。

Document 是 window 对象中的静态全局变量,通过该变量我们可以立即获取当前浏览 SVG 图形的 SVG 文档对象(SVGDocument)。通过获取 SVG 文档对象我们就可以在 DOM 框架下对当前 SVG 文档的内容进行动态操作。

contextMenu 变量只在 Adobo SVG Viewer3.0中有效,该变量同 document 变量一样,也是 window 对象的静态全局变量。它引用了在Adobo SVG Viewer3.0浏览环境下单击鼠标右键时所展示菜单的 XML 文档对象(Document),通过重新构建该变量引用的对象内容,我们可以重新构建浏览时鼠标右键菜单的字体和条目。

2. 将 JavaScript 脚本放在哪里

使用 JavaScript 首先我们要解决一个简单的问题:我们把脚本代码放在哪里?SVG 标准允许将JavaScript 脚本代码以两种方式来实现:使用 script 元素将 JavaScript 脚本内嵌在 SVG 文件中;或使用script 的 xlink:href 属性从 SVG 文件之外连接 JavaScript 脚本文件。从脚本实现的功能上来说,这两种代码加载方式没有区别,我们可以将共享的脚本代码放在外边连接文件中,把该 SVG 文件个性化的代码嵌在自身的文件中。

下面是一个 SVG 文件和一份 JavaScript 脚本文件,将这两个文件放在同一个文件夹下打开即可运行。

表1: jslocation.svg
1.	<?xml version="1.0" encoding="UTF-8"?>
2.	<svg
3.	xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
4.	<script language="JavaScript" xlink:href="lib.js" />
5.	<script language="JavaScript"><![CDATA[
6.	function Body_function(str){
7.	alert("Body_function->"+str)
8.	}
9.	]]></script>
10.	<g id="funciton_load"  transform="translate(10 10)">
11.	<g onclick="Body_function('onclike')"  transform="translate(0 0)">
12.	<text x="6px" y="30px"  style="font-size:20">Body_function</text>
13.	</g>
14.	<g onclick="Lib_function('onclike')"   transform="translate(160 0)">
15.	<text x="6px" y="30px"  style="font-size:20">Lib_function</text>
16.	</g>
17.	</g>
18.	</svg>

因为在 SVG 引用外部脚本文件时是以 utf 编码方式引入的,所以我们不能在待引用的脚本文件中使用中文,甚至在注释中使用中文也会使代码运行出现不确定的异常。所以在实际运行时,需要删除本文后续例子中为代码所做的中文注释。

表2: lib.js
function Lib_function(str){
	alert("Lib_function->"+str)
}

jslocation.svg 文件的第5-9行之间内嵌了一段 JavaScript 脚本,第4行引用了一个外部 js 文件。第 11 行和第 14 行分别通过 onclick 事件调用了这两种代码加载方式所包含的两个不同的函数。

特别需要留意的是第 3 行的对名称空间的声明。名称空间的声明指定了这些 SVG 元素位于 SVG 名称空间内,编写这些元素时无需带任何名称空间的前缀。在使用Adobo SVG Viewer3.0浏览 SVG 图像时可以缺省定义这些命名空间,Adobo SVG Viewer3.0浏览环境会默认的将文档内的 XML 元素识别为SVG 元素。但当我们使用Batik 浏览 SVG 图像时,这些命名空间是必须要指定的,否则,脚本的链接和其他一些复杂的功能将不能起作用。

3. 通过 JavaScript 动态操作 SVG 文档

3.1 通过 DOM 对象操作节点

在 JavaScript 环境下,通过 DOM 定义的接口,我们可以在 SVG 的 XML 树中漫游,可以对找到的节点属性重新赋值,还可以在当前文档中删除节点或添加新创建的节点。

下面的例子展示了通过 DOM 接口如何删除现有节点,添加新的节点并为新节点设置了超级链接地址:

表3: 对 SVG 文档进行动态操作
<g onclick="operate_Dom()"  id="g5" transform="translate(0 80)">
			<text id="txt1" x="6px" y="30px" style="font-size:15">operate DOM</text>
			<text id="txt2" x="6px" y="50px" style="font-size:15">operate DOM</text>
</g>
<!-内嵌脚本代码'
<script language="JavaScript"><![CDATA[
function operate_Dom(){
	//通过document环境变量获取id值为"g5"的节点
	var g5=document.getElementById("g5");
    //将g5节点下所有text节点删除
	var txts=g5.getElementsByTagName("text");
	for(var i=txts.length-1;i>=0;i--){
		g5.removeChild(txts.item(i));
	}
	//将g5节点的onclick事件删除
	g5.removeAttribute("onclick");
	
	//创造一个文本节点对象
	var text = document.createElement ("text");
	text.setAttribute("x", 100);
	text.setAttribute("y", 100);
	//将文本内容添加到text节点对象中
	text.appendChild(document.createTextNode("new text"));
	
	//创造一个链接节点,注意在这里给设置节点和属性时必须指定命名空间
	var a=document.createElementNS("http://www.w3.org/2000/svg","a");
	a.setAttributeNS(
	  "http://www.w3.org/2000/xlink/namespace/",
	  "xlink:href",
	  "http://www.sina.com");
	//将text节点添加到链接节点中
	a.appendChild(text);
	
//将链接节点添加到g5节点中
	g5.appendChild(a);	
    
//获取视图范围
var bBox=(document.getDocumentElement().getBBox());
//创建一个矩形节点
var shape = document.createElement("rect ");
//配置属性
shape.setAttribute("x", bBox.x);
shape.setAttribute("y", bBox.y);
shape.setAttribute("width",  bBox.width);
shape.setAttribute("height",   bBox.height);
shape.setAttribute("style", "fill: #eeeeee");
shape.getStyle().setProperty("stroke","red");
shape.getStyle().setProperty("stroke-width","1");
	//将矩形节点添加到SVG根节点子节点队列的最前边
document.getDocumentElement().insertBefore(shape,document.getDocumentElement().firstChild);
}
]]></script>

关于 DOM 接口的详细定义,我们可以通过参考资料提供的相关链接获取。除了在 DOM 接口中定义的相关方法和属性之外,SVG 在具体实现 JavaScript 时根据图形处理的需要对 DOM 接口也进行了相应的扩展。比较重要的扩展是关于 SVG 视图比例池相关的方法,这些方法可以让我们通过 document.getDocumentElement()获取 SVG 文档的坐标系统的附加信息。

表4: 文档根节点扩展的坐标系统附加信息
表4: 文档根节点扩展的坐标系统附加信息
表4: 文档根节点扩展的坐标系统附加信息

除了这里提到的比较重要的接口之外,我们还可以通过使用的 SVG 浏览器产品的开发文档获取详细信息。但遗憾的是这些文档往往写的非常简略,很多操作接口只是给出了 IDL 定义并没有对接口的内容进行详细阐述,这就需要我们发挥想像力去猜测并进行具体的测试验证了。比较常用的方法是分析 Batik 的源代码跟踪相关函数的具体实现来获取函数的执行过程。

需要特别指出的是用于创建超级链接节点的相关代码,当需要创建超级链接节点的时候需要明确指出节点的命名空间"http://www.w3.org/2000/svg",为节点设置链接属性的时候也必须指定属性的命名空间"http://www.w3.org/2000/xlink/namespace/"。关于命名空间在 SVG 中的意义我们可以通过在 Batik 中的脚本改写来深刻体会其中的内涵。

3.2 针对 Batik 的脚本改写:

在 Batik 环境下运行刚才的例子时,你会发现根本得不到我们在 Adobo SVG Viewer3.0 下看到的效果。这是因为在 Adobo 的环境下对 DOM 编程的要求不是很严格,所以我们可以用比较模糊的代码来实现这些功能,IE浏览器的 JavaScript 引擎会对我们调用的函数进行自动匹配(这类似于 C 语法中的默认参数值)。但在Batik 环境下使用的 JavaScript 的引擎是引自 Apache 的脚本引擎库,该脚本引擎对 JavaScript 的代码要求非常严格。为了能在 Batik 环境下运行,我们必须对 JavaScript 脚本进行严谨的修改,使其能经过 Batik脚本引擎的考验。我们将红色部分的代码注释掉换上新的代码就可以看到在Batik环境下JavaScript 对 DOM对象的操作起作用了。

在 Batik 环境下需要创建新节点时,必须指定新节点引用的命名空间"http://www.w3.org/2000/svg"才能有效。

表5: 针对 Batik 的改写
创建新节点的时候必须明确指定该节点的命名空间
//var shape = document.createElement("rect");
var shape = document.createElementNS("http://www.w3.org/2000/svg"," rect ");
//var text = document.createElement ("text");
var text = document.createElementNS("http://www.w3.org/2000/svg","text");
定义样式属性时必须指定该属性的优先级,一般设置一个空白字符串就可以了。
//shape.getStyle().setProperty("stroke","red");
//shape.getStyle().setProperty("stroke-width","10");
shape.getStyle().setProperty("stroke","red","");
shape.getStyle().setProperty("stroke-width","10","");

为 Batik 改写的脚本代码在Adobo SVG Viewer3.0环境下是可以正确运行,这意味着通过对代码进行细致的处理,我们能够编写在两种平台下都能运行的脚本。

4. 由浏览工具提供的脚本支持

在 W3C 的 SVG 标准之外,各种品牌的 SVG 浏览器还提供了一些在 SVG 编程中支持的特殊函数和对象,用于实现一些特殊功能或提高开发效率。其中有些函数是各个产品都实现了的,这就大大降低了我们在移植过程中的难度。

4.1 数据通讯函数

函数名称:getURL(uri, GetURLHandler)
支持环境:Adobe SVG Viewer3.0;Batik1.5.1
用途: 该函数是 window 对象的提供的方法,可以允许你从指定的 URL 路径实时加载数据。GetURLHandler 参数用于指定一个用于处理加载数据的函数指针。

表6: 使用 getURL
function loadFile () {
	getURL("menuitem_en-us.xml", fileLoaded);
}
function fileLoaded (data) {
   if(data.success) {
        alert(data.contentType);
	    alert(data.content);
   }
}

示例中的 fileLoaded 函数用于处理实时加载的文件,其中的 data 参数是一个关于指定 URL 文件信息的对象,该对象的 success 属性用于标识是否成功加载指定文件;content 属性用于记录加载文件的文本内容;contentType 属性标识文件类型(该属性在 Batik 中未被支持)。

由 Adobo 实现的 getURL 方法在加载文件时可以智能的判断加载文件的文件类型和编码方式,你可以加载gzip 压缩的 xml 文件,比如压缩存储格式的.svgz 文件在加载后会自动进行必要的解压操作。在加载文本文件的时候还可以根据加载文件的编码格式(ASCII ,UTF-8,UTF-16)进行自动识别。

警告:在Adobe SVG Viewer 的早期版本(3.0以前)中可以为 getURL 的 url 参数设定任意路径的文件,远程攻击者可以利用这个漏洞读取系统本地或远程文件,泄露敏感信息。不过 IE6 SP1 对从 Internet 域读取本地文件内容做了限制,因此 IE6 SP1 不存在此问题,也可以通过下载 Adobe SVG Viewer3.01 版本来弥补这个漏洞。弥补漏洞后只可以为 getURL()设定 SVG 文件所在 URI 域的文件路径。

4.2 XML 转换函数

函数名称:String printNode(Node)
支持环境:Adobe SVG Viewer3.0
用途:参数中的node节点解析为字符串。

函数名称:Node parseXML(String ,document)
支持环境:Adobe SVG Viewer3.0;Batik1.5.1
用途:将字符串解析成一个节点对象。

这一对函数用于进行字符串和DOM节点之间的转换。我们可以使用printNode()序列化指定节点用于将当前SVG文档中的Node元素生成字符串用于保存为文本文件或提交给远程服务器。相反的我们可以通过parseXML()将一个字符串用指定的Document解析为一个Node对象,为parseXML()配置的document参数用于指定解析Node对象的Document;在Adobo SVG Viewer环境下可以不指定document对象,系统会默认用当前SVG文档的Document对象解析字符串。

表7: 将字符串编译成SVG节点并添加到当前SVG文档
function parseAndAddData (string) {
	var node = parseXML(string, document);
	document.documentElement.appendChild(node);
}

4.3 在Adobo SVG Viewer中重构菜单

Adobe SVG Viewer3.0为浏览用户提供了单击鼠标右键弹出的菜单用于实现常用的浏览操作功能,在实际应用中我们有时会需要定义自己的鼠标右键菜单的语言或裁减相应的菜单功能。Adobe SVG Viewer3.0的脚本环境中提供了一个环境变量"contextMenu"。 变量contextMenu是一个document对象,我们可以通过重新定义contextMenu文档对象的节点内容来重构菜单的内容和样式,并在设定菜单条目显示的文字时通过"&*"来定义相应条目的快捷键。

表8: 根据系统默认语言动态加载不同的菜单文档
function setMenuLanguage(){
	if(top.navigator.userLanguage=="zh-cn"){		
	}else if(top.navigator.userLanguage=="zh-tw"){
		getURL("menuitem_zh-tw.xml", fileLoaded);
	}else{
		getURL("menuitem_en-us.xml", fileLoaded);		
        }
}
function fileLoaded (data) {
	var msg = '';
	if(data.success) {
		var newMenuRoot=parseXML(data.content,contextMenu);
		contextMenu.replaceChild(newMenuRoot,contextMenu.getDocumentElement());		
	} 
}

下面是针对英文操作系统配置的菜单定义文件,读着可以根据模板定义其他语言的菜单。

表9: 使用英文的菜单文件"menuitem_en-us.xml"
<?xml version="1.0" encoding="UTF-8"?>
<menu id="myCustomMenu">
	<header>Adobe SVG Viewer</header>
	<item action="Open" id="Open">Open</item>
	<item action="OpenNew" id="OpenNew">Open in New Window</item>
	<separator/>
	<item action="ZoomIn" id="ZoomIn">Zoom In^_^&E</item>
	<item action="ZoomOut" id="ZoomOut">Zoom Out</item>
	<item action="OriginalView" id="OriginalView">Original View</item>
	<separator/>
	<item action="Quality" id="Quality">Higher Quality</item>
	<item action="Pause" id="Pause">Pause</item>
	<item action="Mute" id="Mute">Mute</item>
	<separator/>
	<item action="Find" id="Find">Find...</item>
	<item action="FindAgain" id="FindAgain">Find Again</item>
	<separator/>
	<item action="Copy" id="Copy">Copy Selected Text</item>
	<item action="CopySVG" id="CopySVG">Copy SVG</item>
	<item action="ViewSVG" id="ViewSVG">View SVG</item>
	<item action="ViewSource" id="ViewSource">View Source</item>
	<item action="SaveAs" id="SaveAs">Save SVG As...</item>
	<separator/>
	<item action="Help" id="Help">Help</item>
	<item action="About" id="About">About Adobe SVG Viewer...</item>
</menu>
图 2. 重构后的菜单
图 2. 重构后的菜单
图 2. 重构后的菜单

5. 在Batik 下通过 java DOM 实现 SVG 文档操作

在 Batik 环境下还可以通过 Java 环境下的 DOM 接口直接操作当前视图使用的 SVG 文档。我们可以通过Batik 提供的 org.apache.batik.swing.JSVGCanvas 对象获取当前显示 SVG 文件的 DOM 文档对象引用,通过对该 DOM 的操作改变当前 SVG 图像的内容:

JSVGCanvas svgCanvas = new JSVGCanvas();
svgCanvas.setURI("dom.svg");
SVGDocument svgDocument=svgCanvas.getSVGDocument();
SVGSVGElement svgRoot=svgDocument.getRootElement()  ;
Element g5=svgDocument.getElementById("g5"); 
g5.setAttribute("transform","translate(225, 250) ");
		
Element shape=svgDocument.createElementNS("http://www.w3.org/2000/svg","circle");
shape.setAttribute("cx", "100");
shape.setAttribute("cy", "100");
shape.setAttribute("r",  "20");
shape.setAttribute("style", "fill: green");
g5.appendChild(shape);

需要重点提出来的是,在 Batik 中添加新的节点的时候,一定要指明添加节点的命名空间。另外需要特别注意的是,在 Batik 的 java 编程环境中,不支持对样式如"shape.getStyle(). setProperty("stroke","red");"这样的属性设置,必须通过对 style 属性的一次性赋值来设定元素的样式。

你可以从参考资料中获取 Batik 的详细定义文档。


相关主题

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=84137
ArticleTitle=使用脚本动态操作 SVG 文档
publish-date=05012005