使用脚本动态操作 SVG 文档
http://www.ibm.com/developerworks/cn/xml/x-svgscript/index.html
陈珂([email protected]),技术总监,南京安元科技
陈珂[email protected],从业以来一直从事政府信息化建设和GIS的相关工作,自从多年前接触SVG以来就对其产生极大的兴趣。现在南京安元科技担任技术总监。您可以通过这里访问我创建的基于SVG的GIS开源项目,我把它叫做AntGIS(小而强大)。
简介:本教程适用于那些希望使用可伸缩向量图形(SVG)创建交互式SVG图形的开发人员。它讨论了使用ECMAScript(JavaScript)对现有的SVG图像进行实时操作得技术。
本文的标签:svg
标记本文!
发布日期:2005年5月01日
级别:初级
访问情况:5743次浏览
评论:0(查看|添加评论-登录)
平均分(4个评分)
为本文评分
.本文主要介绍在SVG中通过编程实现动态操作SVG图像的知识。
SVG图像的结构是用XML文档表示的,因此可以使用XML编程技术如"文档对象模型(DocumentObjectModel,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对象结构
Window是一个全局变量,该变量表示SVG运行时的浏览器窗口对象。因为脚本的运行就是在window对象内部进行的,所以调用该对象方法和属性时可以省去对window变量的指定,例如window.document可以直接通过document实现引用。完全介绍window对象的属性和方法内容已经超出了本文的范围,有兴趣的读者可以通过参考资料查阅详细说明。
Document是window对象中的静态全局变量,通过该变量我们可以立即获取当前浏览SVG图形的SVG文档对象(SVGDocument)。通过获取SVG文档对象我们就可以在DOM框架下对当前SVG文档的内容进行动态操作。
contextMenu变量只在AdoboSVGViewer3.0中有效,该变量同document变量一样,也是window对象的静态全局变量。它引用了在AdoboSVGViewer3.0浏览环境下单击鼠标右键时所展示菜单的XML文档对象(Document),通过重新构建该变量引用的对象内容,我们可以重新构建浏览时鼠标右键菜单的字体和条目。
--------------------------------------------------------------------------------
回页首
2.将JavaScript脚本放在哪里
使用JavaScript首先我们要解决一个简单的问题:我们把脚本代码放在哪里?SVG标准允许将JavaScript脚本代码以两种方式来实现:使用script元素将JavaScript脚本内嵌在SVG文件中;或使用script的xlink:href属性从SVG文件之外连接JavaScript脚本文件。从脚本实现的功能上来说,这两种代码加载方式没有区别,我们可以将共享的脚本代码放在外边连接文件中,把该SVG文件个性化的代码嵌在自身的文件中。
下面是一个SVG文件和一份JavaScript脚本文件,将这两个文件放在同一个文件夹下打开即可运行。
表1:jslocation.svg
1.<?xmlversion="1.0"encoding="UTF-8"?>
2.<svg
3.xmlns="http://www.w3.org/2000/svg"xmlns:xlink="http://www.w3.org/1999/xlink">
4.<scriptlanguage="JavaScript"xlink:href="lib.js"/>
5.<scriptlanguage="JavaScript"><![CDATA[
6.functionBody_function(str){
7.alert("Body_function->"+str)
8.}
9.]]></script>
10.<gid="funciton_load"transform="translate(1010)">
11.<gonclick="Body_function('onclike')"transform="translate(00)">
12.<textx="6px"y="30px"style="font-size:20">Body_function</text>
13.</g>
14.<gonclick="Lib_function('onclike')"transform="translate(1600)">
15.<textx="6px"y="30px"style="font-size:20">Lib_function</text>
16.</g>
17.</g>
18.</svg>
因为在SVG引用外部脚本文件时是以utf编码方式引入的,所以我们不能在待引用的脚本文件中使用中文,甚至在注释中使用中文也会使代码运行出现不确定的异常。所以在实际运行时,需要删除本文后续例子中为代码所做的中文注释。
表2:lib.js
functionLib_function(str){
alert("Lib_function->"+str)
}
jslocation.svg文件的第5-9行之间内嵌了一段JavaScript脚本,第4行引用了一个外部js文件。第11行和第14行分别通过onclick事件调用了这两种代码加载方式所包含的两个不同的函数。
特别需要留意的是第3行的对名称空间的声明。名称空间的声明指定了这些SVG元素位于SVG名称空间内,编写这些元素时无需带任何名称空间的前缀。在使用AdoboSVGViewer3.0浏览SVG图像时可以缺省定义这些命名空间,AdoboSVGViewer3.0浏览环境会默认的将文档内的XML元素识别为SVG元素。但当我们使用Batik浏览SVG图像时,这些命名空间是必须要指定的,否则,脚本的链接和其他一些复杂的功能将不能起作用。
--------------------------------------------------------------------------------
回页首
3.通过JavaScript动态操作SVG文档
3.1通过DOM对象操作节点
在JavaScript环境下,通过DOM定义的接口,我们可以在SVG的XML树中漫游,可以对找到的节点属性重新赋值,还可以在当前文档中删除节点或添加新创建的节点。
下面的例子展示了通过DOM接口如何删除现有节点,添加新的节点并为新节点设置了超级链接地址:
表3:对SVG文档进行动态操作
<gonclick="operate_Dom()"id="g5"transform="translate(080)">
<textid="txt1"x="6px"y="30px"style="font-size:15">operateDOM</text>
<textid="txt2"x="6px"y="50px"style="font-size:15">operateDOM</text>
</g>
<!-内嵌脚本代码'
<scriptlanguage="JavaScript"><![CDATA[
functionoperate_Dom(){
//通过document环境变量获取id值为"g5"的节点
varg5=document.getElementById("g5");
//将g5节点下所有text节点删除
vartxts=g5.getElementsByTagName("text");
for(vari=txts.length-1;i>=0;i--){
g5.removeChild(txts.item(i));
}
//将g5节点的onclick事件删除
g5.removeAttribute("onclick");
//创造一个文本节点对象
vartext=document.createElement("text");
text.setAttribute("x",100);
text.setAttribute("y",100);
//将文本内容添加到text节点对象中
text.appendChild(document.createTextNode("newtext"));
//创造一个链接节点,注意在这里给设置节点和属性时必须指定命名空间
vara=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);
//获取视图范围
varbBox=(document.getDocumentElement().getBBox());
//创建一个矩形节点
varshape=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:文档根节点扩展的坐标系统附加信息
XMLerror:Theimageisnotdisplayedbecausethewidthisgreaterthanthemaximumof580pixels.Pleasedecreasetheimagewidth.
除了这里提到的比较重要的接口之外,我们还可以通过使用的SVG浏览器产品的开发文档获取详细信息。但遗憾的是这些文档往往写的非常简略,很多操作接口只是给出了IDL定义并没有对接口的内容进行详细阐述,这就需要我们发挥想像力去猜测并进行具体的测试验证了。比较常用的方法是分析Batik的源代码跟踪相关函数的具体实现来获取函数的执行过程。
需要特别指出的是用于创建超级链接节点的相关代码,当需要创建超级链接节点的时候需要明确指出节点的命名空间"http://www.w3.org/2000/svg",为节点设置链接属性的时候也必须指定属性的命名空间"http://www.w3.org/2000/xlink/namespace/"。关于命名空间在SVG中的意义我们可以通过在Batik中的脚本改写来深刻体会其中的内涵。
3.2针对Batik的脚本改写:
在Batik环境下运行刚才的例子时,你会发现根本得不到我们在AdoboSVGViewer3.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的改写
创建新节点的时候必须明确指定该节点的命名空间
//varshape=document.createElement("rect");
varshape=document.createElementNS("http://www.w3.org/2000/svg","rect");
//vartext=document.createElement("text");
vartext=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改写的脚本代码在AdoboSVGViewer3.0环境下是可以正确运行,这意味着通过对代码进行细致的处理,我们能够编写在两种平台下都能运行的脚本。
--------------------------------------------------------------------------------
回页首
4.由浏览工具提供的脚本支持
在W3C的SVG标准之外,各种品牌的SVG浏览器还提供了一些在SVG编程中支持的特殊函数和对象,用于实现一些特殊功能或提高开发效率。其中有些函数是各个产品都实现了的,这就大大降低了我们在移植过程中的难度。
4.1数据通讯函数
函数名称:getURL(uri,GetURLHandler)
支持环境:AdobeSVGViewer3.0;Batik1.5.1
用途:该函数是window对象的提供的方法,可以允许你从指定的URL路径实时加载数据。GetURLHandler参数用于指定一个用于处理加载数据的函数指针。
表6:使用getURL
functionloadFile(){
getURL("menuitem_en-us.xml",fileLoaded);
}
functionfileLoaded(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)进行自动识别。
警告:在AdobeSVGViewer的早期版本(3.0以前)中可以为getURL的url参数设定任意路径的文件,远程攻击者可以利用这个漏洞读取系统本地或远程文件,泄露敏感信息。不过IE6SP1对从Internet域读取本地文件内容做了限制,因此IE6SP1不存在此问题,也可以通过下载AdobeSVGViewer3.01版本来弥补这个漏洞。弥补漏洞后只可以为getURL()设定SVG文件所在URI域的文件路径。
4.2XML转换函数
函数名称:StringprintNode(Node)
支持环境:AdobeSVGViewer3.0
用途:参数中的node节点解析为字符串。
函数名称:NodeparseXML(String,document)
支持环境:AdobeSVGViewer3.0;Batik1.5.1
用途:将字符串解析成一个节点对象。
这一对函数用于进行字符串和DOM节点之间的转换。我们可以使用printNode()序列化指定节点用于将当前SVG文档中的Node元素生成字符串用于保存为文本文件或提交给远程服务器。相反的我们可以通过parseXML()将一个字符串用指定的Document解析为一个Node对象,为parseXML()配置的document参数用于指定解析Node对象的Document;在AdoboSVGViewer环境下可以不指定document对象,系统会默认用当前SVG文档的Document对象解析字符串。
表7:将字符串编译成SVG节点并添加到当前SVG文档
functionparseAndAddData(string){
varnode=parseXML(string,document);
document.documentElement.appendChild(node);
}
4.3在AdoboSVGViewer中重构菜单
AdobeSVGViewer3.0为浏览用户提供了单击鼠标右键弹出的菜单用于实现常用的浏览操作功能,在实际应用中我们有时会需要定义自己的鼠标右键菜单的语言或裁减相应的菜单功能。AdobeSVGViewer3.0的脚本环境中提供了一个环境变量"contextMenu"。变量contextMenu是一个document对象,我们可以通过重新定义contextMenu文档对象的节点内容来重构菜单的内容和样式,并在设定菜单条目显示的文字时通过"&*"来定义相应条目的快捷键。
表8:根据系统默认语言动态加载不同的菜单文档
functionsetMenuLanguage(){
if(top.navigator.userLanguage=="zh-cn"){
}elseif(top.navigator.userLanguage=="zh-tw"){
getURL("menuitem_zh-tw.xml",fileLoaded);
}else{
getURL("menuitem_en-us.xml",fileLoaded);
}
}
functionfileLoaded(data){
varmsg='';
if(data.success){
varnewMenuRoot=parseXML(data.content,contextMenu);
contextMenu.replaceChild(newMenuRoot,contextMenu.getDocumentElement());
}
}
下面是针对英文操作系统配置的菜单定义文件,读着可以根据模板定义其他语言的菜单。
表9:使用英文的菜单文件"menuitem_en-us.xml"
<?xmlversion="1.0"encoding="UTF-8"?>
<menuid="myCustomMenu">
<header>AdobeSVGViewer</header>
<itemaction="Open"id="Open">Open</item>
<itemaction="OpenNew"id="OpenNew">OpeninNewWindow</item>
<separator/>
<itemaction="ZoomIn"id="ZoomIn">ZoomIn^_^&E</item>
<itemaction="ZoomOut"id="ZoomOut">ZoomOut</item>
<itemaction="OriginalView"id="OriginalView">OriginalView</item>
<separator/>
<itemaction="Quality"id="Quality">HigherQuality</item>
<itemaction="Pause"id="Pause">Pause</item>
<itemaction="Mute"id="Mute">Mute</item>
<separator/>
<itemaction="Find"id="Find">Find...</item>
<itemaction="FindAgain"id="FindAgain">FindAgain</item>
<separator/>
<itemaction="Copy"id="Copy">CopySelectedText</item>
<itemaction="CopySVG"id="CopySVG">CopySVG</item>
<itemaction="ViewSVG"id="ViewSVG">ViewSVG</item>
<itemaction="ViewSource"id="ViewSource">ViewSource</item>
<itemaction="SaveAs"id="SaveAs">SaveSVGAs...</item>
<separator/>
<itemaction="Help"id="Help">Help</item>
<itemaction="About"id="About">AboutAdobeSVGViewer...</item>
</menu>
图2.重构后的菜单
--------------------------------------------------------------------------------
回页首
5.在Batik下通过javaDOM实现SVG文档操作
在Batik环境下还可以通过Java环境下的DOM接口直接操作当前视图使用的SVG文档。我们可以通过Batik提供的org.apache.batik.swing.JSVGCanvas对象获取当前显示SVG文件的DOM文档对象引用,通过对该DOM的操作改变当前SVG图像的内容:
JSVGCanvassvgCanvas=newJSVGCanvas();
svgCanvas.setURI("dom.svg");
SVGDocumentsvgDocument=svgCanvas.getSVGDocument();
SVGSVGElementsvgRoot=svgDocument.getRootElement();
Elementg5=svgDocument.getElementById("g5");
g5.setAttribute("transform","translate(225,250)");
Elementshape=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的详细定义文档。
参考资料
•有关SVG的背景知识,请阅读developerWorks上的教程,"可伸缩向量图形介绍"
•可以参考教程交互式动态可伸缩向量图形
•通过http://www.w3.org/TR/SVG11/获取当前最新SVG标准文档
•Batik项目介绍http://xml.apache.org/batik/
•可以从http://www.adobe.com/svg/indepth/pdfs/CurrentSupport.pdf获取AdoboSVGViewer3.0的技术细节
•可以通过http://www.w3.org/TR/DOM-Level-2-Core了解DOM对象的细节
•通过SacréSVG你可以找到最近关于SVG的文章和新闻。
关于作者
陈珂[email protected],从业以来一直从事政府信息化建设和GIS的相关工作,自从多年前接触SVG以来就对其产生极大的兴趣。现在南京安元科技担任技术总监。您可以通过这里访问我创建的基于SVG的GIS开源项目,我把它叫做AntGIS(小而强大)。