动态图形

作者:Reuven M. Lerner

Netscape 的联合创始人马克·安德雷森经常被认为是将 Web 从学术乐园转变为大众媒介的人。但安德雷森究竟做了什么?毕竟,蒂姆·伯纳斯-李发明了浏览器、HTML 和 URL。你甚至可以说,最初的浏览器在某些方面更胜一筹,因为它允许人们编写 HTML 页面以及阅读它们。

历史学家可能会对此提出异议,但我认为安德雷森最伟大的想法是允许在 Web 文档中同时显示图形和文本。作为一个纯文本媒介,Web 主要对物理学家和其他学者感兴趣,但随着图形的引入,它开始吸引全新的受众。

如今,图形不仅仅用于装饰,而且常常可以独立存在。几乎每个专业的网站现在都聘请了一位或多位图形艺术家来设计网站——即使该网站主要处理文本。如果没有图形的使用,一些网站将不可能存在,甚至不值得存在。在某些情况下,这些图形是动态生成的,由程序生成,而不是位于磁盘上的静态文件中。

本月,我们将探讨如何使用 CGI 程序创建此类动态图形。我们将研究 GD 库,它允许我们创建任意图像,并将快速转向创建不同类型的动态生成的图表和图形。在查看一些此类图表的简单示例后,我们将研究一个更复杂的示例,该示例从关系数据库中提取输入。

Perl、动态图形和 GD

编写输出 HTML 的 CGI 程序并不特别困难,正如我们在“At the Forge”的许多先前文章中演示的那样。例如,这是一个简单的程序,当调用时,它会向用户的浏览器返回一些 HTML

#!/usr/bin/perl -wT
    use strict;
    use diagnostics;
    use CGI;
    use CGI::Carp qw(fatalsToBrowser);
    # Create an instance of CGI
    my $query = new CGI;
    # Send an appropriate MIME header
    print $query->header(-type =>
    "text/html");
    # Send some content
    print $query->start_html(-title =>
    "This is a test.");
    print "<H1>Testing!</H1>\n";
    print "<P>This is a test.</P>\n";
    print $query->end_html;

如果我们想向用户的浏览器返回图形,我们必须修改 HTTP 响应中的“Content-type”标头,该标头通过调用“header”生成。如果我们想生成 GIF,我们将不得不更改对 header 的调用,使其输出“image/gif”代替。同样,我们可以告诉用户的浏览器将发送 JPEG (image/jpeg) 或 PNG (image/png) 图形。一旦我们向用户的浏览器描述了内容,我们就必须生成这种类型的图形。我们该怎么做呢?

Perl 的标量变量可以包含我们可能喜欢的任何数据。如果我们更熟悉 GIF 标准,我们可以将 GIF 放入标量中,然后将该值发送到用户的浏览器。当然,我们大多数人都不熟悉 GIF 标准的详细信息,这使得这不是一个理想的解决方案。更好的主意是利用 Perl 的面向对象功能,使用别人的解决方案来解决相同的问题。

果然,林肯·斯坦(CGI.pm 的作者,CGI 程序的标准模块)编写并分发了 GD.pm。此模块可在 CPAN 上找到(请参阅“资源”),使我们可以访问由托马斯·布特尔编写的流行的 C 语言“gd”库。

GD 使您的程序能够以类似于流行的绘图程序的方式进行绘制。您可以从一系列画笔、颜色和内置形状中进行选择,以及您绘制的任何填充形状。GD 有其自己的内部绘图格式,但正如我们将看到的,它支持将绘制的图像转换为 GIF 格式。

一个简单的图形程序

清单 1 中显示了一个使用 GD 的简单程序 gd-intro.pl。如果您将其安装在您的 CGI 目录中并从浏览器中调用它,您应该会看到一个蓝色填充的绿色正方形。

清单 1。

正如您所看到的,我们的程序操作两个对象——CGI 的实例和 GD 的实例。每个对象处理自己的事务,不干涉另一个对象的业务。$query,我们的 CGI 实例,既不知道也不关心我们正在从用户接收或返回到他或她的浏览器的数据类型。同样,$image,我们的 GD 实例,也不知道其输出将要发送到浏览器。这种任务的划分是对象使编程更容易和软件更易于维护的原因之一。

当我们创建 $image 时,我们声明它的类型为 GD::Image,宽度为 100 像素,高度为 100 像素。如果您的图像被此声明的“画布”边界截断,GD 不会警告您;当我第一次开始使用 GD 时,我对没有出现任何输出感到困惑。我最终意识到我的图像是 100x100,但我正在绘制一个直径为 400 像素的圆。GD 忠实地执行了我请求的任务,这意味着最终没有出现任何图片。

在声明 $image 之后,我们使用 GD 的 colorAllocate 方法分配一些颜色。每种颜色都定义为红-绿-蓝 (RGB),其中每个参数在 0 到 255 之间变化。我发现将颜色名称声明在哈希中很有用,就像 gd-intro.pl 中的 %COLORS 一样,但您可能更喜欢将它们分配给单个变量或直接使用 colorAllocate。

接下来,我们告诉 $image 它应该在“隔行扫描”模式下创建 GIF。隔行扫描意味着计算机不应绘制图像的每一条水平线,而应首先绘制所有偶数行,然后再绘制所有奇数行。您可以在普通的电视机上看到这一点。当电视标准被定义时,电视无法一次绘制所有水平线。因此,您的电视绘制所有奇数水平线,然后是偶数水平线,然后又是奇数水平线。

使 GIF 隔行扫描与您的计算机的速度或其快速显示图像的能力无关;相反,它与用户连接的速度有关。如果用户连接速度较慢,GIF 将加载缓慢。使图形隔行扫描允许用户在图形加载时看到图形。否则,图形将不会显示,直到它完全加载,这可能需要一段时间。

我们还设置了“透明”颜色,这是选择与背景融合的颜色。通过将白色设置为透明颜色,我们表明任何以白色绘制的内容实际上都应以背景颜色绘制。由于 GD 绘图默认情况下具有白色背景,因此将白色设置为透明颜色意味着我们的图形将显示为漂浮在用户的浏览器中,而不是以白色背景为背景。

完成所有这些之后,我们终于可以开始绘制了。我们在 20,20 和 80,80 之间创建一个矩形,它应该填充我们在创建 $image 时定义的 100x100 区域的大部分。我们选择使用 %COLORS 以绿色绘制矩形,我们在前面定义了 %COLORS。最后,我们通过将 GD 指向矩形内部的点并要求它填充该区域,用蓝色填充矩形。

GD 有许多其他功能,包括绘制多边形、创建自定义画笔和用指定的图案填充的功能。您可以添加文本标签,这些标签会合并到最终的图形中。您甚至可以将图形以 GD 自己的格式保存到磁盘,然后再加载它们并继续操作它们,然后再将它们转换为 GIF。

图表和图形

GD 是在 Web 上绘图的绝佳工具。有了它,您可以创建各种奇妙的东西。我想创建的大多数 Web 图形是基于各种类型数据的图表和图形。我可以使用 GD 来创建此类图形,但这将涉及太多的工作。

幸运的是,正如 Perl 中经常发生的那样,其他人也遇到了这个问题并决定解决它。马丁·维尔布鲁根编写并分发了 GIFgraph 模块,该模块允许我们根据数据点列表创建不同类型的图表。GIFgraph 使用 GD,但为我们提供了新图形的面向对象接口。这使我们可以从图形、样式和形状的角度来思考——而不是 GD,它会迫使我们从像素和线条的角度来思考。

Dynamic Graphics

GIFgraph 实际上是一组以单个“GIFgraph”名称收集的模块。一个模块处理条形图,另一个模块处理饼图等等,几乎有十种不同类型的图形。

清单 2。

例如,在清单 2 中,我们创建了一个简单的条形图,标签为“a”、“b”和“c”,相应的值分别为 1、2 和 3。我们通过创建一个数组来做到这一点,传统上称为 @data@data 的每个元素都是一个数组引用,第一个元素对应于标签。我们的程序显示来自单个数据集的结果

my @data = (["a", "b", "c"], [1, 2, 3]);

我们可以轻松地比较两组数据

my @data = (["a","b","c"], [1,2,3], [4,5,6]);
GIFgraph 足够智能,可以为不同的数据集使用不同的颜色。因此,给定上述数据,它将绘制六个条形——每种颜色三个,值 1 和 4 与“a”标签关联,值 2 和 5 与“b”标签关联,值 3 和 6 与“c”标签关联。

在我们可以将输出发送到用户的浏览器之前,我们必须发送 MIME 类型。由于它依赖于 GD,因此 GIFgraph 可以生成 GIF 格式的输出。我们使用以下命令告诉浏览器期望的内容

print $query->header(-type => "image/gif");

现在我们将创建我们的图形对象并将其 GIF 输出发送到用户的浏览器

my $graph = new GIFgraph::bars;
print $graph->plot(\@data);
请注意我们如何通过在 @data 前面加上反斜杠 (\@data) 将数组引用传递给 @data。将 @data 作为引用传递可确保它将按预期传递给 plot 方法。

在本例中,我们创建了一个条形图。如果我们想要不同类型的图表怎么办?我们可以通过导入不同的 Perl 模块(例如,GIFgraph::lines 而不是 GIFgraph::bars)并将 $graph 设为新类型的实例来做到这一点。

Dynamic Graphics

请注意,调用 $graph->plot 会基于 @data 创建图形,但不会将其发送到用户的浏览器。此方法将其调用者返回生成的 GIF,从而允许我们将其保存到磁盘、发送到用户的浏览器或在 Perl 或外部工具中操作生成的 GIF。由于 CGI 标准要求所有到 STDOUT 的输出都发送到用户的浏览器,因此我们可以通过打印此调用的结果在用户的计算机上显示图表。

基于文件绘制图表

现在我们已经看到了一个生成图表的简单程序,让我们看一个稍微复杂一点的示例,该示例反映了一些真实世界的情况。假设我们想基于文本文件创建图形。例如,假设我们正在实现基于 Web 的投票系统的报告功能的一部分。给定选举的结果将放在一个名为 votes.txt 的文本文件中

Tom    123456
Dick   100000
Harry  20000

选举数据存储在上面的文件中,候选人的姓名和他收到的票数用一个或多个制表符分隔。这允许候选人的姓名包含空格字符,例如在名字和姓氏之间。

清单 3。

我们可以将条形图与此数据一起使用,但这远不如饼图有用,在饼图中,每个候选人都被赋予饼图的比例部分。正如您在清单 3 中看到的,我们的程序 vote.pl 创建起来不是很困难,并且可以相对快速地生成结果。

它通过迭代 votes.txt 的每一行来做到这一点,使用 Perl 的内置“split”函数将标量值(来自 votes.txt 的行)转换为列表值。在本例中,我们跨制表符拆分该行,将制表符之前的所有内容放在 $name 中,将制表符之后的所有内容放在 $votes 中。然后,我们使用“push”函数将这些值分别添加到 @names@votes,它们是通过每次迭代 votes.txt 构建的。如果 votes.txt 中有四个候选人,则此循环执行四次,并且 @names@votes 各有四个元素。

当我们从循环中退出时,我们通过插入对 @names@votes 的引用来创建 @data。与往常一样,@data 的第一个元素是一个包含名称的数组引用。@data 的后续元素包含值;在本例中,我们只有一个值,@votes。我们通过创建 GIFgraph::pie 的实例,然后将其绘制到用户的浏览器来创建图形。

从数据库检索数据

上面的示例向我们介绍了基于磁盘上存储的数据创建图表的概念。虽然这当然是正确的想法,但将此类数据存储在文本文件中存在缺点。更常见且更有用的是将此类数据放入关系数据库中。

基于关系数据库中的表创建图表与基于文本文件创建图表没有太大区别。主要区别在于我们用于迭代输入数据的循环。在 vote.pl 中,我们迭代 votes.txt 的每一行,将每一行文本转换为名称、值对,然后将其添加到 @data。当我们从数据库检索信息时,信息已经为我们拆分为名称、值对。

在我们开始编写 db-vote.pl(vote.pl 的数据库版本)之前,我们必须在数据库中创建一个表。与往常一样,我将使用 MySQL,一个“大部分免费”的数据库,在资源中描述。MySQL 的语法对于大多数目的来说都足够标准,并且以下大多数内容也适用于其他数据库。

关系数据库期望以 SQL(“结构化查询语言”)接收输入。SQL 不是一种编程语言——因此,虽然我们可以创建各种查询来操作表中的数据,但我们必须将这些查询嵌入到用完整编程语言编写的程序中。Perl 的 DBI(“数据库接口”)模块允许我们将 SQL 语句嵌入到我们的 Perl 程序中。

我们可以通过发出以下 SQL 命令来创建一个新表

CREATE TABLE Votes (
         candidate_name VARCHAR(30),
         votes_received BIGINT UNSIGNED
         );

虽然我们可以从 Perl 程序中将上述内容发送到我们的数据库服务器,但更常见的是直接从交互式数据库客户端中键入它。MySQL 附带一个名为 mysql 的交互式客户端,它允许您向数据库发送查询(并接收响应),而无需将语句嵌入到 Perl 程序中。

在您发出上述 SQL 查询后,数据库服务器将创建一个新表 Votes,其中包含两列。第一列 candidate_name 允许最多 30 个字符。第二列定义为 BIGINT UNSIGNED,即一个大的整数。我们将此列命名为 votes_received

我们现在将进行一次信仰的飞跃,并假设在选举之夜投票结束后,我们的数据库表将神奇地填充每个候选人的适当值。(在实际应用中,我们可能会以不同的方式设计事物,将每个候选人的姓名存储在第二个表中,甚至可能将每张选票存储在自己的行中。我们暂时忽略现实世界的担忧,以便专注于如何使用此数据创建图形。)

假设我们的表已填充候选人姓名及其票数的列表,我们如何重写 vote.pl 以使其从数据库获取输入?如上所述,我们将依赖 DBI,Perl 的数据库接口,它为大多数流行的关系数据库提供了统一的面向对象接口。每个数据库都在 DBD 或数据库驱动程序中描述,并在我们打开连接时自动导入。

打开与数据库的连接会创建一个“数据库句柄”对象,传统上称为 $dbh。我们使用此对象创建一个“语句句柄”,传统上称为 $sth,我们用它将 SQL 发送到数据库服务器。在本例中,我们的查询非常简单

SELECT candidate_name, votes_received
    FROM Votes

当它执行此查询时,数据库服务器将向用户返回一个两列表,在本例中,是 Votes 表的全部内容。表的每一行对应于我们在前面看到的文本文件 votes.txt 中的一行。

DBI 为我们提供了许多从 $sth 检索数据的方法。最常用的方法是将行作为数组检索,可以是通常的形式(使用 $sth->fetchrow_array)或作为引用(使用 $sth->fetchrow_arrayref)。虽然 arrayref 方法效率更高,但 Perl 初学者通常更喜欢避免引用,这有时会使他们感到困惑。在这两种情况下,返回列表中的元素的顺序都由查询中列命名的顺序决定。

清单 4。

db-vote.pl 的其余部分(参见清单 4)几乎以与 vote.pl 相同的方式继续,将每一行中的值推送到 @names@values,然后使用这些值来创建 @data

通常最好将此类信息放在数据库中,因为关系数据库提供了可靠性和灵活性。但是请记住,没有免费的午餐:关系数据库本质上比平面 ASCII 文本文件慢得多。此外,我们的 CGI 程序每次被调用时都会打开与数据库的连接,这是一项昂贵且耗时的操作。由于这些原因,vote.pl 几乎肯定会比 db-vote.pl 执行得更快。这是否是适当的权衡取决于您网站的访问者数量以及您的 Web 应用程序的性质。

修改图形

现在您已经了解了如何基于各种输入创建简单的图形,让我们花一些时间讨论如何修改输出。GIFgraph 允许您更改图形的几乎每个方面,包括颜色、图例的位置和样式以及标记轴的方式。这通过 set 方法完成。当然,某些设置仅对某些类型的图形有效;例如,饼图上没有轴,这意味着设置轴标签将毫无意义。

这是一个设置的示例调用

$graph->set(x_label => "Candidates",
        y_label => "Number of votes",
        title => "Voting results",
     logo => "corplogo.gif",
     zero_axis => 1);

GIFgraph 手册页(在安装软件包后键入 perldoc GIFgraph 即可获得)详细描述了这些和许多其他选项。但是,以上可能是一个很好的起点,并演示了如何设置描述图表的各种因素。在上面的示例中,我们将 X 轴标记为候选人,Y 轴标记为票数,在图表上包含我们的公司徽标,并确保轴始终从原点 (0, 0) 开始。还有一些选项可以选择颜色和字体,以及定义每个轴上应多久出现刻度线——如果您阅读手册,您可能会被大量的选项所淹没。

结论

正如您所看到的,从我们的 CGI 程序中动态创建图形并不是特别困难。更令人印象深刻——以及通常更有用——是仅用几行代码即可创建多种类型的图表和图形的能力。

下个月,我们将进一步了解动态生成的图形,研究一个跟踪用户股票投资组合的简单应用程序。该应用程序将重新审视我们上个月讨论的两个主题,即 HTTP Cookie 和将状态保存到数据库。

Dynamic Graphics
Reuven M. Lerner 是以色列海法的一位互联网和 Web 顾问,自 1993 年初以来一直使用 Web。他的著作《Core Perl》将于今年晚些时候由 Prentice-Hall 出版。可以通过 reuven@lerner.co.il 与 Reuven 联系。ATF 主页,包括档案和讨论论坛,位于 http://www.lerner.co.il/atf/
加载 Disqus 评论