我将图形编辑程序分为两类:一类是绘图程序,利用这种程序可以一个像素一个像素地绘制图像;另外一类是制图程序,这种程序提供了一组对象,例如线、椭圆和矩形,您可以使用这些对象来组合成一幅大图像,例如JPEG。绘图程序非常适合进行像素级的控制。但是对于业务图形来说,制图程序是比较好的方式,因为大部分图形都是由矩形、线和椭圆组成的。
PHP内置的制图基本操作与绘图程序非常类似。它们对于绘制图像来说功能非常强大;但是如果您希望自己的图像是一组对象集合时,这就不太适合了。本文将向您展示如何在PHP图形库的基础上构建一个面向对象的图形库。您将使用PHPV5中提供的面向对象的扩展。
具有面向对象的图形支持之后,您的图形代码就非常容易理解和维护了。您可能还需要从一种单一的图形源将图形合成为多种类型的媒介:Flash电影、SVG等等。
目标
创建一个图形对象库包括3个主要的目标:
从基本操作切换到对象上
它不使用imageline、imagefilledrectangle以及其他图形函数,这个库应该提供一些对象,例如Line、Rectangle和Oval,它们可以用来制作图像。它应该还可以支持构建更大的复杂对象或对对象进行分组的功能。
可以进行z值排序
制图程序让画家可以在画面表面上上下移动图形对象。这个库应该可以支持将一个对象放到其他对象前后的功能:它使用了一个z值,用来定义对象从制图平面开始的高度。z值越大的对象被画得越晚,也就出现在那些z值较小的对象之上。
提供viewport的转换
通常,数据的坐标空间与图像的坐标空间是不同的。PHP中的图形基本操作是对图像的坐标平面进行操作的。这个图形库应该支持viewport的规范,这样您就可以在一个程序员熟悉的坐标系统中指定图形了,并且可以自动进行伸缩来适应任何图像的大小。
由于这里有很多特性,您将一步步地编写代码来展示这些代码如何不断增加功能。
基础知识
让我们首先来看一个图形环境对象和一个名为GraphicsObject的接口,它是使用一个Line类实现的,功能就是用来画线。UML如图1所示。
图1.图形环境和图形对象接口
GraphicsEnvironment类中保存了图形对象和一组颜色,还包括宽度和高度。saveAsPng方法负责将当前的图像输出到指定的文件中。
GraphicsObject是任何图形对象都必须使用的接口。要开始使用这个接口,您所需要做的就是使用render方法来画这个对象。它是由一个Line类实现的,它利用4个坐标:开始和结束的x值,开始和结束的y值。它还有一个颜色。当调用render时,这个对象从sx,sy到ex,ey画一条由名字指定的颜色的线。
这个库的代码如清单1所示。
清单1.基本的图形库
<?phpclassGraphicsEnvironment{public$width;public$height;public$gdo;public$colors=array();publicfunction__construct($width,$height){$this->width=$width;$this->height=$height;$this->gdo=imagecreatetruecolor($width,$height);$this->addColor("white",255,255,255);imagefilledrectangle($this->gdo,0,0,$width,$height,$this->getColor("white"));}publicfunctionwidth(){return$this->width;}publicfunctionheight(){return$this->height;}publicfunctionaddColor($name,$r,$g,$b){$this->colors[$name]=imagecolorallocate($this->gdo,$r,$g,$b);}publicfunctiongetGraphicObject(){return$this->gdo;}publicfunctiongetColor($name){return$this->colors[$name];}publicfunctionsaveAsPng($filename){imagepng($this->gdo,$filename);}}abstractclassGraphicsObject{abstractpublicfunctionrender($ge);}classLineextendsGraphicsObject{private$color;private$sx;private$sy;private$ex;private$ey;publicfunction__construct($color,$sx,$sy,$ex,$ey){$this->color=$color;$this->sx=$sx;$this->sy=$sy;$this->ex=$ex;$this->ey=$ey;}publicfunctionrender($ge){imageline($ge->getGraphicObject(),$this->sx,$this->sy,$this->ex,$this->ey,$ge->getColor($this->color));}}?>
测试代码如清单2所示:
清单2.基本图形库的测试代码
<?phprequire_once("glib.php");$ge=newGraphicsEnvironment(400,400);$ge->addColor("black",0,0,0);$ge->addColor("red",255,0,0);$ge->addColor("green",0,255,0);$ge->addColor("blue",0,0,255);$gobjs=array();$gobjs[]=newLine("black",10,5,100,200);$gobjs[]=newLine("blue",200,150,390,380);$gobjs[]=newLine("red",60,40,10,300);$gobjs[]=newLine("green",5,390,390,10);foreach($gobjsas$gobj){$gobj->render($ge);}$ge->saveAsPng("test.png");?>
这个测试程序创建了一个图形环境。然后创建几条线,它们指向不同的方向,具有不同的颜色。然后,render方法可以将它们画到图形平面上。最后,这段代码将这个图像保存为test.png。
在本文中,都是使用下面的命令行解释程序来运行这段代码,如下所示:
%phptest.php%
图2显示了所生成的test.png文件在Firefox中的样子。
图2.简单的图形对象测试
这当然不如蒙娜丽莎漂亮,但是可以满足目前的工作需要。
我们的第一个需求——提供图形对象的能力——已经满足了,现在应该开始满足第二个需求了:可以使用一个z值将一个对象放到其他对象的上面或下面。
我们可以将每个z值当作是原始图像的一个面。所画的元素是按照z值从最小到最大的顺序来画的。例如,让我们画两个图形元素:一个红色的圆和一个黑色的方框。圆的z值是100,而黑方框的z值是200。这样会将圆放到方框之后,如图3所示:
图3.不同z值的面
我们只需要修改一下z值就可以将这个红圆放到黑方框之上。要实现这种功能,我们需要让每个GraphicsObject都具有一个z()方法,它返回一个数字,就是z值。由于您需要创建不同的图形对象(Line、Oval和Rectangle),您还需要创建一个基本的类BoxObject,其他3个类都使用它来维护起点和终点的坐标、z值和这个对象的颜色(请参看图4)。
图4.给系统添加另外一维:z值
这个图形库的新代码如清单3所示:
清单3.可以处理z信息的图形库
<?phpclassGraphicsEnvironment{public$width;public$height;public$gdo;public$colors=array();publicfunction__construct($width,$height){$this->width=$width;$this->height=$height;$this->gdo=imagecreatetruecolor($width,$height);$this->addColor("white",255,255,255);imagefilledrectangle($this->gdo,0,0,$width,$height,$this->getColor("white"));}publicfunctionwidth(){return$this->width;}publicfunctionheight(){return$this->height;}publicfunctionaddColor($name,$r,$g,$b){$this->colors[$name]=imagecolorallocate($this->gdo,$r,$g,$b);}publicfunctiongetGraphicObject(){return$this->gdo;}publicfunctiongetColor($name){return$this->colors[$name];}publicfunctionsaveAsPng($filename){imagepng($this->gdo,$filename);}}abstractclassGraphicsObject{abstractpublicfunctionrender($ge);abstractpublicfunctionz();}abstractclassBoxObjectextendsGraphicsObject{protected$color;protected$sx;protected$sy;protected$ex;protected$ey;protected$z;publicfunction__construct($z,$color,$sx,$sy,$ex,$ey){$this->z=$z;$this->color=$color;$this->sx=$sx;$this->sy=$sy;$this->ex=$ex;$this->ey=$ey;}publicfunctionz(){return$this->z;}}classLineextendsBoxObject{publicfunctionrender($ge){imageline($ge->getGraphicObject(),$this->sx,$this->sy,$this->ex,$this->ey,$ge->getColor($this->color));}}classRectangleextendsBoxObject{publicfunctionrender($ge){imagefilledrectangle($ge->getGraphicObject(),$this->sx,$this->sy,$this->ex,$this->ey,$ge->getColor($this->color));}}classOvalextendsBoxObject{publicfunctionrender($ge){$w=$this->ex-$this->sx;$h=$this->ey-$this->sy;imagefilledellipse($ge->getGraphicObject(),$this->sx+($w/2),$this->sy+($h/2),$w,$h,$ge->getColor($this->color));}}?>
测试代码也需要进行更新,如清单4所示。
清单4.更新后的测试代码
<?phprequire_once("glib.php");functionzsort($a,$b){if($a->z()<$b->z())return-1;if($a->z()>$b->z())return1;return0;}$ge=newGraphicsEnvironment(400,400);$ge->addColor("black",0,0,0);$ge->addColor("red",255,0,0);$ge->addColor("green",0,255,0);$ge->addColor("blue",0,0,255);$gobjs=array();$gobjs[]=newOval(100,"red",50,50,150,150);$gobjs[]=newRectangle(200,"black",100,100,300,300);usort($gobjs,"zsort");foreach($gobjsas$gobj){$gobj->render($ge);}$ge->saveAsPng("test.png");?>
此处需要注意两件事情。首先是我们添加了创建Oval和Rectangle对象的过程,其中第一个参数是z值。其次是调用了usort,它使用了zsort函数来对图形对象根据z值进行排序。
在运行这个程序时,test.png文件应该如图5所示。
图5.红圆在黑方框之后
现在修改下面的代码:
$gobjs[]=newOval(200,"red",50,50,150,150);$gobjs[]=newRectangle(100,"black",100,100,300,300);
再次运行这个代码,突然这个椭圆就在这个方框上面了,如图6所示。
图6.红圆现在在黑方框上面了
红圆现在就出现在黑方框上面了,尽管它是先创建的,也是首先添加到数组中的。这就是z值的实际价值:您可以按照任何顺序来创建对象,并可以通过调整每个对象的z值来调整彼此之间的相对位置。
在这段代码中,z值排序是在这个库之外实现的。让我们通过创建一个新容器对象Group来实现这种功能,其中保存了一组GraphicsObject对象。Group对象然后再处理排序的问题。
Group类的代码如清单5所示。
清单5.Group类
functionzsort($a,$b){if($a->z()<$b->z())return-1;if($a->z()>$b->z())return1;return0;}classGroupextendsGraphicsObject{private$z;protected$members=array();publicfunction__construct($z){$this->z=$z;}publicfunctionadd($member){$this->members[]=$member;}publicfunctionrender($ge){usort($this->members,"zsort");foreach($this->membersas$gobj){$gobj->render($ge);}}publicfunctionz(){return$this->z;}}
Group对象的任务是保持一个对象数组,然后在画图时,逐个对对象zo进行排序和画图。
更新后的测试代码如清单6所示。
清单6.更新后的测试代码
<?phprequire_once("glib.php");$ge=newGraphicsEnvironment(400,400);$ge->addColor("black",0,0,0);$ge->addColor("red",255,0,0);$ge->addColor("green",0,255,0);$ge->addColor("blue",0,0,255);$g1=newGroup(0);$g1->add(newOval(200,"red",50,50,150,150));$g1->add(newRectangle(100,"black",100,100,300,300));$g1->render($ge);$ge->saveAsPng("test.png");?>
现在所有的客户机需要做的是创建一个Group对象。它会处理排序和其他操作。
viewport是一个人造的坐标系统,可以转换成图像的物理坐标系统。viewport的扩展可以是您希望的任何东西。例如,x和y轴的起点和终点可以是-2和2,这样viewport坐标平面的中心就是0,0。这对于三角图形(例如sin和cosine)来说是很好的一个viewport。或者,这个viewport也可以是不对称的,其中y值的范围从-1到1,x值的范围是从0到10,000,这取决于您的需要。
这个viewport的其他值可以确保构建一个400X400的图像所采用的逻辑与构建一个4000X2000的图像所采用的逻辑是相同的。代码负责向这个viewport中写入数据,然后这个viewport自动实现到图像的物理尺寸的自动映射。
要让您的viewport正常工作,您需要将这个viewport的范围从0,0修改为1,1,这可以让图形对象回调图形环境,从而将viewport的坐标转换成物理坐标。您可以将所有的代码都放到BoxObject基类中进行简化。
图7显示了有关新添加的代码的两个内容。首先是添加的tx和ty方法,这会将x和y坐标从viewport转换成物理图像的坐标。第二个是对BoxObject增加了draw方法,它的派生类应该用来进行制图。BoxObject在render方法中实现viewport的转换,并使用物理坐标来调用draw方法。使用这种方法,Line、Oval和Rectangle类都可以利用viewport坐标,而不需要担心坐标转换的问题。
图7.所添加的图形环境viewport转换
这个新库的代码如清单7所示:
清单7.具有viewport支持的图形库
<?phpclassGraphicsEnvironment{public$width;public$height;public$gdo;public$colors=array();publicfunction__construct($width,$height){$this->width=$width;$this->height=$height;$this->gdo=imagecreatetruecolor($width,$height);$this->addColor("white",255,255,255);imagefilledrectangle($this->gdo,0,0,$width,$height,$this->getColor("white"));}publicfunctionwidth(){return$this->width;}publicfunctionheight(){return$this->height;}publicfunctionaddColor($name,$r,$g,$b){$this->colors[$name]=imagecolorallocate($this->gdo,$r,$g,$b);}publicfunctiongetGraphicObject(){return$this->gdo;}publicfunctiongetColor($name){return$this->colors[$name];}publicfunctionsaveAsPng($filename){imagepng($this->gdo,$filename);}publicfunctiontx($x){return$x*$this->width;}publicfunctionty($y){return$y*$this->height;}}abstractclassGraphicsObject{abstractpublicfunctionrender($ge);abstractpublicfunctionz();}functionzsort($a,$b){if($a->z()<$b->z())return-1;if($a->z()>$b->z())return1;return0;}classGroupextendsGraphicsObject{private$z;protected$members=array();publicfunction__construct($z){$this->z=$z;}publicfunctionadd($member){$this->members[]=$member;}publicfunctionrender($ge){usort($this->members,"zsort");foreach($this->membersas$gobj){$gobj->render($ge);}}publicfunctionz(){return$this->z;}}abstractclassBoxObjectextendsGraphicsObject{protected$color;protected$sx;protected$sy;protected$ex;protected$ey;protected$z;publicfunction__construct($z,$color,$sx,$sy,$ex,$ey){$this->z=$z;$this->color=$color;$this->sx=$sx;$this->sy=$sy;$this->ex=$ex;$this->ey=$ey;}publicfunctionrender($ge){$rsx=$ge->tx($this->sx);$rsy=$ge->ty($this->sy);$rex=$ge->tx($this->ex);$rey=$ge->ty($this->ey);$this->draw($rsx,$rsy,$rex,$rey,$ge->getGraphicObject(),$ge->getColor($this->color));}abstractpublicfunctiondraw($sx,$sy,$ex,$ey,$gobj,$color);publicfunctionz(){return$this->z;}}classLineextendsBoxObject{publicfunctiondraw($sx,$sy,$ex,$ey,$gobj,$color){imageline($gobj,$sx,$sy,$ex,$ey,$color);}}classRectangleextendsBoxObject{publicfunctiondraw($sx,$sy,$ex,$ey,$gobj,$color){imagefilledrectangle($gobj,$sx,$sy,$ex,$ey,$color);}}classOvalextendsBoxObject{publicfunctiondraw($sx,$sy,$ex,$ey,$gobj,$color){$w=$ex-$sx;$h=$ey-$sy;imagefilledellipse($gobj,$sx+($w/2),$sy+($h/2),$w,$h,$color);}}?>
GraphicsEnvironment类中的viewport转换代码是高亮显示的,正如GraphicsObject中的render代码一样,这会回调图形环境来进行坐标转换的工作。
测试代码只需要稍加修改即可(请参看清单8)。这些对象现在需要在0,0和1,1之间的viewport中进行指定。
清单8.使用新viewport坐标的测试代码
$g1=newGroup(0);$g1->add(newOval(200,"red",0.1,0.1,0.5,0.5));$g1->add(newRectangle(100,"black",0.4,0.4,0.9,0.9));
这非常不错,但是您可能实际上并不希望使用一个0,0与1,1之间的viewport;而是希望使用任意的viewport——例如,在-1000,-1000到1000,1000之间。要让这成为可能,这个图形环境就需要知道viewport的起点和终点。
图8.具有灵活viewport规范的图形环境
清单9显示了更新后的GraphicsEnvironment代码。
清单9.更新后的GraphicsEnvironment代码
classGraphicsEnvironment{public$vsx;public$vsy;public$vex;public$vey;public$width;public$height;public$gdo;public$colors=array();publicfunction__construct($width,$height,$vsx,$vsy,$vex,$vey){$this->vsx=$vsx;$this->vsy=$vsy;$this->vex=$vex;$this->vey=$vey;$this->width=$width;$this->height=$height;$this->gdo=imagecreatetruecolor($width,$height);$this->addColor("white",255,255,255);imagefilledrectangle($this->gdo,0,0,$width,$height,$this->getColor("white"));}publicfunctionwidth(){return$this->width;}publicfunctionheight(){return$this->height;}publicfunctionaddColor($name,$r,$g,$b){$this->colors[$name]=imagecolorallocate($this->gdo,$r,$g,$b);}publicfunctiongetGraphicObject(){return$this->gdo;}publicfunctiongetColor($name){return$this->colors[$name];}publicfunctionsaveAsPng($filename){imagepng($this->gdo,$filename);}publicfunctiontx($x){$r=$this->width/($this->vex-$this->vsx);return($x-$this->vsx)*$r;}publicfunctionty($y){$r=$this->height/($this->vey-$this->vsy);return($y-$this->vsy)*$r;}}
现在这个构造函数可以利用另外4个参数了,它们分别是viewport的起点和终点。tx和ty函数使用新的viewport坐标,并将viewport坐标转换成物理坐标。
测试代码如清单10所示。
清单10.viewport测试代码
<?phprequire_once("glib.php");$ge=newGraphicsEnvironment(400,400,-1000,-1000,1000,1000);$ge->addColor("black",0,0,0);$ge->addColor("red",255,0,0);$ge->addColor("green",0,255,0);$ge->addColor("blue",0,0,255);$g1=newGroup(0);$g1->add(newOval(200,"red",-800,-800,0,0));$g1->add(newRectangle(100,"black",-400,-400,900,900));$g1->render($ge);$ge->saveAsPng("test.png");?>
这段测试代码会在-1000,-1000与1000,000之间创建一个viewport。对象会被重新放置,以适合这个新的坐标系统。
测试代码的输出如图9所示。
图9.viewport绘制的图像转换为一个400X400的图像
如果您希望图像的大小是400X200,就可以采用下面的方法:
$ge=newGraphicsEnvironment(400,200,-1000,-1000,1000,1000);
您会得到一个纵向缩小后的图像,如图10所示。
图10.图形的400X200版本
这展示了代码如何自动调整图像的大小来适合所请求的图像。
结束语
动态图可以为应用程序添加一个新的交互层。使用这种面向对象的系统可以让构建复杂图形变得非常简单,比使用标准的PHP库中的基本操作来画图更加简单。另外,您还可以实现画不同大小或类型的图像,并且可以长期使用相同的代码来画不同类型的媒介,例如SVG、PDF、Flash和其他类型的媒介。
(举报)