PHP-编程指南第四版-五-

61 阅读26分钟

PHP 编程指南第四版(五)

原文:zh.annas-archive.org/md5/516bbc09499c161bb049b4edb114d468

译者:飞龙

协议:CC BY-NC-SA 4.0

第十章:图形

显而易见,网络比文字更具视觉效果。图像以标志、按钮、照片、图表、广告和图标的形式出现。这些图像中的许多是静态的,从未改变,是使用诸如 Photoshop 之类的工具构建的。但是许多图像是动态创建的——从包含您姓名的亚马逊推荐计划广告到股票表现图。

PHP 支持使用内置的 GD 扩展库进行图形创建。在本章中,我们将向您展示如何在 PHP 中动态生成图像。

将图像嵌入页面

一个常见的误解是在单个 HTTP 请求中流动的文本和图形的混合。毕竟,当您查看页面时,您会看到一个包含这种混合的页面。重要的是要理解,通过 Web 浏览器的一系列 HTTP 请求创建包含文本和图形的标准网页;每个请求都由 Web 服务器的响应回答。每个响应只能包含一种类型的数据,每个图像需要一个单独的 HTTP 请求和 Web 服务器响应。因此,如果您看到一个包含一些文本和两个图像的页面,您知道它已经进行了三个 HTTP 请求和相应的响应来构建此页面。

以这个 HTML 页面为例:

<html>
 <head>
 <title>Example Page</title>
 </head>

 <body>
 This page contains two images.
 <img src="image1.png" alt="Image 1" />
 <img src="image2.png" alt="Image 2" />
 </body>
</html>

Web 浏览器发送到此页面的请求序列如下所示:

GET /page.html HTTP/1.0
GET /image1.png HTTP/1.0
GET /image2.png HTTP/1.0

Web 服务器对这些请求的每个响应都会返回一个响应。这些响应中的Content-Type头部如下所示:

Content-Type: text/html
Content-Type: image/png
Content-Type: image/png

要在 HTML 页面中嵌入由 PHP 生成的图像,请假设生成图像的 PHP 脚本实际上是图像本身。因此,如果我们有image1.phpimage2.php脚本来创建图像,我们可以修改先前的 HTML 代码如下(现在的图像名称都是 PHP 扩展名):

<html>
 <head>
 <title>Example Page</title>
 </head>

 <body>
 This page contains two images.
 <img src="image1.php" alt="Image 1" />
 <img src="image2.php" alt="Image 2" />
 </body>
</html>

现在,您不再引用 Web 服务器上的真实图像,而是将<img>标签指向生成并返回图像数据的 PHP 脚本。

此外,您可以向这些脚本传递变量,因此,您可以像这样编写您的<img>标签,而不是使用单独的脚本来生成每个图像:

<img src="image.php?num=1" alt="Image 1" />
<img src="image.php?num=2" alt="Image 2" />

然后,在调用的 PHP 文件image.php中,您可以访问请求参数$_GET['num']以生成适当的图像。

基本图形概念

图像是各种颜色的像素矩形。颜色通过它们在调色板中的位置进行标识,调色板是一个颜色数组。调色板中的每个条目都有三个单独的颜色值——分别为红色、绿色和蓝色。每个值的范围从0(颜色不存在)到255(完全强度的颜色)。这被称为其RGB 值。还有十六进制或“hex”值——用于 HTML 中的颜色的字母数字表示。一些图像工具,如ColorPic,将为您转换 RGB 值为十六进制。

图像文件很少是像素和调色板的直接转储。相反,创建了各种文件格式(GIF、JPEG、PNG 等),试图对数据进行压缩以使文件更小。

不同的文件格式以不同方式处理图像的透明度,它控制背景是否以及如何显示在图像之后。例如,PNG 支持alpha 通道,每个像素都有一个额外值反映该点的透明度。其他格式,如 GIF,简单地指定调色板中的一个条目表示透明度。还有一些格式,如 JPEG,根本不支持透明度。

粗糙和锯齿状的边缘,即锯齿效应,会导致图像不够吸引人。反锯齿涉及移动或重新着色形状边缘的像素,以便更渐变地过渡到其背景。一些绘制图像的函数实现了反锯齿功能。

每个像素有 256 种可能的红、绿、蓝值,因此每个像素有 16,777,216 种可能的颜色。某些文件格式限制调色板中的颜色数量(例如,GIF 最多支持 256 种颜色);其他文件格式则允许使用所需数量的颜色。后者被称为真彩色格式,因为 24 位色(每种颜色分别有 8 位)提供了比人眼能分辨的更多色调。

创建和绘制图像

现在,让我们从最简单的可能的 GD 示例开始。示例 10-1 是一个生成黑色填充方块的脚本。这段代码适用于任何支持 PNG 图像格式的 GD 版本。

示例 10-1. 白底黑色方块(black.php)
<?php
$image = imagecreate(200, 200);

$white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);
$black = imagecolorallocate($image, 0x00, 0x00, 0x00);
imagefilledrectangle($image, 50, 50, 150, 150, $black);

header("Content-Type: image/png");
imagepng($image);

示例 10-1 展示了生成任何图像的基本步骤:创建图像、分配颜色、绘制图像,然后保存或发送图像。图 10-1 显示了 示例 10-1 的输出。

白底黑色方块

图 10-1. 白底黑色方块

要查看结果,只需将浏览器指向 black.php 页面。要在网页中嵌入此图像,请使用:

<img src="black.php" />

图形程序的结构

大多数动态图像生成程序都遵循 示例 10-1 中概述的相同基本步骤。

您可以使用 imagecreate() 函数创建一个 256 色图像,该函数返回一个图像句柄:

$image = imagecreate(*`width`*, *`height`*);

图像中使用的所有颜色都必须使用 imagecolorallocate() 函数分配。第一个分配的颜色成为图像的背景颜色:¹

$color = imagecolorallocate(*`image`*, *`red`*, *`green`*, *`blue`*);

参数是颜色的数字 RGB(红色、绿色、蓝色)组件。在 示例 10-1 中,我们用十六进制写入颜色值,以便函数调用更接近 HTML 颜色表示 #FFFFFF#000000

GD 中有许多绘图原语。示例 10-1 使用了imagefilledrectangle(),其中通过传递左上角和右下角的坐标来指定矩形的尺寸:

imagefilledrectangle(*`image`*, *`tlx`*, *`tly`*, *`brx`*, *`bry`*, *`color`*);

下一步是向浏览器发送Content-Type头部,使用适当的内容类型来创建所需类型的图像。完成此操作后,我们调用相应的输出函数。imagejpeg()imagegif()imagepng()imagewbmp() 函数分别用于创建 GIF、JPEG、PNG 和 WBMP 文件:

imagegif(*`image`* [, *`filename`* ]);
imagejpeg(*`image`* [, *`filename`* [, *`quality`* ]]);
imagepng(*`image`* [, *`filename`* ]);
imagewbmp(*`image`* [, *`filename`* ]);

如果没有给出filename,则图像将输出到浏览器;否则,它将创建(或覆盖)给定路径的图像。对于 JPEG,quality参数的取值范围为0(最差)到100(最佳)。质量越低,JPEG 文件越小。默认设置为75

在示例 10-1 中,在调用输出生成函数imagepng()之前立即设置 HTTP 头部。如果在脚本的最开始设置了Content-Type,则生成的任何错误都会被视为图像数据,并导致浏览器显示损坏的图像图标。表 10-1 列出了各种图像格式及其Content-Type值。

表 10-1. 图像格式的 Content-Type 值

格式Content-Type
GIFimage/gif
JPEGimage/jpeg
PNGimage/png
WBMPimage/vnd.wap.wbmp

更改输出格式

如你所推测的那样,生成不同类型的图像流只需对脚本进行两处更改:发送不同的Content-Type并使用不同的图像生成函数。示例 10-2 展示了修改后生成 JPEG 而非 PNG 图像的示例 10-1。

示例 10-2. 黑色正方形的 JPEG 版本
<?php
$image = imagecreate(200, 200);
$white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);
$black = imagecolorallocate($image, 0x00, 0x00, 0x00);

imagefilledrectangle($image, 50, 50, 150, 150, $black);

header("Content-Type: image/jpeg");
imagejpeg($image);

测试支持的图像格式

如果编写的代码必须在可能支持不同图像格式的系统上移植,请使用imagetypes()函数检查支持的图像类型。此函数返回一个位字段;您可以使用位与运算符(&)来检查给定位是否设置。常量IMG_GIFIMG_JPGIMG_PNGIMG_WBMP 对应于这些图像格式的位。

如果支持 PNG,则示例 10-3 生成 PNG 文件;如果不支持 PNG,则生成 JPEG 文件;如果既不支持 PNG 也不支持 JPEG,则生成 GIF 文件。

示例 10-3. 检查图像格式支持情况
<?php
$image = imagecreate(200, 200);
$white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);
$black = imagecolorallocate($image, 0x00, 0x00, 0x00);

imagefilledrectangle($image, 50, 50, 150, 150, $black);

if (imagetypes() & IMG_PNG) {
 header("Content-Type: image/png");
 imagepng($image);
}
else if (imagetypes() & IMG_JPG) {
 header("Content-Type: image/jpeg");
 imagejpeg($image);
}
else if (imagetypes() & IMG_GIF) {
 header("Content-Type: image/gif");
 imagegif($image);
}

读取现有文件

如果想要从现有图像开始并进行修改,可以使用imagecreatefromgif()imagecreatefromjpeg()imagecreatefrompng()

$image = imagecreatefromgif(*`filename`*);
$image = imagecreatefromjpeg(*`filename`*);
$image = imagecreatefrompng(*`filename`*);

基本绘图函数

GD 提供了绘制基本点、线条、弧线、矩形和多边形的函数。本节描述了 GD 2.x 支持的基本函数。

最基本的函数是imagesetpixel(),它设置指定像素的颜色:

imagesetpixel(*`image`*, *`x`*, *`y`*, *`color`*);

有两个用于绘制线条的函数,imageline()imagedashedline()

imageline(*`image`*, *`start_x`*, *`start_` `y`*, *`end_x`*, *`end_` `y`*, *`color`*);
imagedashedline(*`image`*, *`start_x`*, *`start_` `y`*, *`end_x`*, *`end_` `y`*, *`color`*);

有两个用于绘制矩形的函数,一个仅绘制轮廓,另一个使用指定的颜色填充矩形:

imagerectangle(*`image`*, *`tlx`*, *`tly`*, *`brx`*, *`bry`*, *`color`*);
imagefilledrectangle(*`image`*, *`tlx`*, *`tly`*, *`brx`*, *`bry`*, *`color`*);

指定矩形的位置和大小,通过传递左上角和右下角的坐标。

您可以使用imagepolygon()imagefilledpolygon()函数绘制任意多边形:

imagepolygon(*`image`*, *`points`*, *`number`*, *`color`*);
imagefilledpolygon(*`image`*, *`points`*, *`number`*, *`color`*);

这两个函数接受一个点的数组。该数组对于多边形的每个顶点都有两个整数(xy坐标)。number参数是数组中顶点的数量(通常为count($points)/2)。

imagearc()函数绘制弧(椭圆的一部分):

imagearc(*`image`*, *`center_x`*, *`center_y`*, *`width`*, *`height`*, *`start`*, *`end`*, *`color`*);

椭圆由其中心、宽度和高度定义(对于圆来说,高度和宽度相同)。弧的起始和结束点以度数给出,逆时针从 3 点开始计数。使用start0end360绘制完整的椭圆。

有两种方法可以填充已绘制的形状。imagefill()函数执行洪水填充,从给定位置开始更改像素的颜色。像素颜色的任何变化标记了填充的限制。imagefilltoborder()函数允许您传递填充限制的特定颜色:

imagefill(*`image`*, *`x`*, *`y`*, *`color`*);
imagefilltoborder(*`image`*, *`x`*, *`y`*, *`border_color`*, *`color`*);

另一件可能需要对图像做的事情是旋转它们。例如,如果您试图创建 Web 风格的宣传册,这可能会有所帮助。image``rotate()函数允许您以任意角度旋转图像:

imagerotate(*`image`*, *`angle`*, *`background_color`*);

示例 10-4 中的代码显示了之前的黑盒子图像,旋转了 45 度。background_color选项用于指定图像旋转后未覆盖区域的颜色,已设置为1以显示黑白颜色的对比。图 10-2 显示了此代码的结果。

示例 10-4. 图像旋转示例
<?php
$image = imagecreate(200, 200);
$white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);
$black = imagecolorallocate($image, 0x00, 0x00, 0x00);
imagefilledrectangle($image, 50, 50, 150, 150, $black);

$rotated = imagerotate($image, 45, 1);

header("Content-Type: image/png");
imagepng($rotated);

黑盒子图像旋转 45 度

图 10-2. 黑盒子图像旋转 45 度

带有文本的图像

经常需要向图像添加文本。GD 提供了内置字体以供此用途。示例 10-5 向我们的黑方形图像添加了一些文本。

示例 10-5. 向图像添加文本
<?php
$image = imagecreate(200, 200);
$white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);
$black = imagecolorallocate($image, 0x00, 0x00, 0x00);

imagefilledrectangle($image, 50, 50, 150, 150, $black);
imagestring($image, 5, 50, 160, "A Black Box", $black);

header("Content-Type: image/png");
imagepng($image);

图 10-3 显示了示例 10-5 的输出。

添加文本后的黑盒子图像

图 10-3. 添加文本后的黑盒子图像

imagestring()函数向图像添加文本。指定文本的左上角点、颜色和要使用的 GD 字体(通过标识符):

imagestring(*`image`*, *`font_id`*, *`x`*, *`y`*, *`text`*, *`color`*);

字体

GD 通过 ID 识别字体。内置五种字体,并可以通过imageloadfont()函数加载额外的字体。这五种内置字体显示在图 10-4 中。

本地 GD 字体

图 10-4. 本地 GD 字体

这里是用于显示这些字体的代码:

<?php
$image = imagecreate(200, 200);
$white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);
$black = imagecolorallocate($image, 0x00, 0x00, 0x00);

imagestring($image, 1, 10, 10, "Font 1: ABCDEfghij", $black);
imagestring($image, 2, 10, 30, "Font 2: ABCDEfghij", $black);
imagestring($image, 3, 10, 50, "Font 3: ABCDEfghij", $black);
imagestring($image, 4, 10, 70, "Font 4: ABCDEfghij", $black);
imagestring($image, 5, 10, 90, "Font 5: ABCDEfghij", $black);

header("Content-Type: image/png");
imagepng($image);

您可以创建自己的位图字体,并使用imageloadfont()函数将它们加载到 GD 中。但是,这些字体是二进制的,且依赖于架构,使它们无法从一台机器移植到另一台机器。在 GD 中使用 TrueType 函数与 TrueType 字体提供了更大的灵活性。

TrueType 字体

TrueType 是一种轮廓字体标准;它提供了对字符渲染更精确的控制。要向图像中添加 TrueType 字体的文本,请使用imagettftext()

imagettftext(*`image`*, *`size`*, *`angle`*, *`x`*, *`y`*, *`color`*, *`font`*, *`text`*);

大小以像素为单位。角度以从 3 点钟开始的度数表示(0表示水平文本,90表示向上的垂直文本等)。xy 坐标指定文本基线的左下角。文本可能包含 UTF-8²形式的&#234;序列,以打印高位 ASCII 字符。

字体参数是用于渲染字符串的 TrueType 字体的位置。如果字体不以斜杠开头,则添加*.ttf扩展名,并在/usr/share/fonts/truetype*中查找字体。

默认情况下,TrueType 字体的文本是反锯齿的。这使大多数字体更易阅读,尽管略微模糊。但是,反锯齿可能使非常小的文本更难读——小字符具有更少的像素,因此反锯齿的调整更为显著。

您可以通过使用负颜色索引(例如,−4表示使用颜色索引 4 但不反锯齿文本)关闭反锯齿。

示例 10-6 使用 TrueType 字体向图像添加文本,搜索字体位置与脚本相同,但仍需提供字体文件位置的完整路径(包含在本书的代码示例中)。

示例 10-6. 使用 TrueType 字体
<?php
$image = imagecreate(350, 70);
$white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);
$black = imagecolorallocate($image, 0x00, 0x00, 0x00);

$fontname = "c:/wamp64/www/bookcode/chapter_10/IndieFlower.ttf";

*`imagettftext`*($image, 20, 0, 10, 40, $black, $fontname, "The Quick Brown Fox");

header("Content-Type: image/png");
imagepng($image);

图 10-5 显示了示例 10-6 的输出。

Indie Flower TrueType 字体

图 10-5. Indie Flower TrueType 字体

示例 10-7 使用imagettftext()向图像添加垂直文本。

示例 10-7. 显示垂直 TrueType 文本
<?php
$image = *`imagecreate`*(70, 350);
$white = *`imagecolorallocate`*($image, 255, 255, 255);
$black = *`imagecolorallocate`*($image, 0, 0, 0);

$fontname = "c:/wamp64/www/bookcode/chapter_10/IndieFlower.ttf";

*`imagettftext`*($image, 20, 270, 28, 10, $black, $fontname, "The Quick Brown Fox");

*`header`*("Content-Type: image/png");
*`imagepng`*($image);

图 10-6 显示了示例 10-7 的输出。

垂直 TrueType 文本

图 10-6. 垂直 TrueType 文本

动态生成的按钮

动态生成按钮图像是生成图像的一种流行用途之一(本主题在第一章中介绍)。通常情况下,这涉及将文本合成到预先存在的背景图像上,如示例 10-8 所示。

示例 10-8. 创建动态按钮
<?php
$font = "c:/wamp64/www/bookcode/chapter_10/IndieFlower.ttf" ;
$size = *`isset`*(`$_GET`['size']) ? `$_GET`['size'] : 12;
$text = *`isset`*(`$_GET`['text']) ? `$_GET`['text'] : 'some text';

$image = *`imagecreatefrompng`*("button.png");
$black = *`imagecolorallocate`*($image, 0, 0, 0);

`if` ($text) {
 // calculate position of text
 $tsize = *`imagettfbbox`*($size, 0, $font, $text);
 $dx = *`abs`*($tsize[2] - $tsize[0]);
 $dy = *`abs`*($tsize[5] - $tsize[3]);
 $x = (*`imagesx`*($image) - $dx ) / 2;
 $y = (*`imagesy`*($image) - $dy ) / 2 + $dy;

 // draw text
 *`imagettftext`*($image, $size, 0, $x, $y, $black, $font, $text);
}

*`header`*("Content-Type: image/png");
*`imagepng`*($image);

在这种情况下,空白按钮(button.png)被默认文本覆盖,如图 10-7 所示。

默认文本的动态按钮

图 10-7. 默认文本的动态按钮

可以像这样从页面调用示例 10-8 中的脚本:

<img src="button.php?text=PHP+Button" />

此 HTML 生成了图 10-8 中显示的按钮。

带有生成文本标签的按钮

图 10-8. 带有生成文本标签的按钮

URL 中的+字符是空格的编码形式。空格在 URL 中是非法的,必须进行编码。使用 PHP 的urlencode()函数对按钮字符串进行编码。例如:

<img src="button.php?text=<?= urlencode("PHP Button"); ?>" />

缓存动态生成的按钮

生成图像比发送静态图像稍慢一些。对于每次以相同文本参数调用时始终看起来相同的按钮,您可以实现一个简单的缓存机制。

示例 10-9 仅在找不到该按钮的缓存文件时生成该按钮。$path变量保存一个目录,由 Web 服务器用户可写,其中按钮可以被缓存;确保它可以从运行此代码的位置访问。filesize()函数返回文件的大小,readfile()函数将文件内容发送到浏览器。因为此脚本使用文本形式参数作为文件名,所以非常不安全。(第十四章,讨论安全问题,解释了如何修复它。)

示例 10-9. 缓存动态按钮
<?php

$font = "c:/wamp64/www/bookcode/chapter_10/IndieFlower.ttf";
$size = isset($_GET['size']) ? $_GET['size'] : 12;
$text = isset($_GET['text']) ? $_GET['text'] : 'some text';

$path = "/tmp/buttons"; // button cache directory

// send cached version

if ($bytes = @filesize("{$path}/button.png")) {
 header("Content-Type: image/png");
 header("Content-Length: {$bytes}");
 readfile("{$path}/button.png");

 exit;
}

// otherwise, we have to build it, cache it, and return it
$image = imagecreatefrompng("button.png");
$black = imagecolorallocate($image, 0, 0, 0);

if ($text) {
 // calculate position of text
 $tsize = imagettfbbox($size, 0, $font, $text);
 $dx = abs($tsize[2] - $tsize[0]);
 $dy = abs($tsize[5] - $tsize[3]);
 $x = (imagesx($image) - $dx ) / 2;
 $y = (imagesy($image) - $dy ) / 2 + $dy;

 // draw text
 imagettftext($image, $size, 0, $x, $y, $black, $font, $text);

 // save image to file
 imagepng($image, "{$path}/{$text}.png");
}

header("Content-Type: image/png");
imagepng($image);

更快的缓存

示例 10-9 仍不如可能的快速。使用 Apache 指令,您可以完全绕过 PHP 脚本,并在创建后直接加载缓存的图像。

首先,在您的 Web 服务器的DocumentRoot下的某个地方创建一个buttons目录,并确保您的 Web 服务器用户有权限写入此目录。例如,如果DocumentRoot目录是*/var/www/html*,则创建*/var/www/html/buttons*。

其次,编辑您的 Apache httpd.conf文件,并添加以下代码块:

<Location /buttons/>
 ErrorDocument 404 /button.php
</Location>

这告诉 Apache,对于buttons目录中不存在的文件请求,应将其发送到您的button.php脚本。

第三步,将示例 10-10 保存为button.php。该脚本创建新的按钮,将它们保存到缓存并发送到浏览器。与示例 10-9 不同,这里没有$_GET中的表单参数,因为 Apache 将错误页面处理为重定向。相反,我们必须从$_SERVER中分析数值以确定正在生成的按钮。顺便说一句,我们删除文件名中的'..'以修复来自示例 10-9 的安全漏洞。

安装button.php后,当像your.site/buttons/php… 服务器会检查是否存在buttons/php.png文件。如果不存在,请求将重定向到button.php脚本,该脚本创建带有“php”文本的图像并保存到buttons/php.png*中。任何对此文件的后续请求都将直接提供,而无需运行 PHP 代码。

示例 10-10. 更高效的动态按钮缓存
<?php
// bring in redirected URL parameters, if any
parse_str($_SERVER['REDIRECT_QUERY_STRING']);

$cacheDir = "/buttons/";
$url = $_SERVER['REDIRECT_URL'];

// pick out the extension
$extension = substr($url, strrpos($url, '.'));

// remove directory and extension from $url string
$file = substr($url, strlen($cacheDir), -strlen($extension));

// security - don't allow '..' in filename
$file = str_replace('..', '', $file);

// text to display in button
$text = urldecode($file);

$font = "c:/wamp64/www/bookcode/chapter_10/IndieFlower.ttf" ;

// build it, cache it, and return it
$image = imagecreatefrompng("button.png");
$black = imagecolorallocate($image, 0, 0, 0);

if ($text) {
 // calculate position of text
 $tsize = imagettfbbox($size, 0, $font, $text);
 $dx = abs($tsize[2] - $tsize[0]);
 $dy = abs($tsize[5] - $tsize[3]);
 $x = (imagesx($image) - $dx ) / 2;
 $y = (imagesy($image) - $dy ) / 2 + $dy;

 // draw text
 imagettftext($image, $size, 0, $x, $y, $black, $font, $text);

 // save image to file
 imagepng($image, "{$_SERVER['DOCUMENT_ROOT']}{$cacheDir}{$file}.png");
}

header("Content-Type: image/png");
imagepng($image);

示例 10-10 机制的一个显著缺点是按钮文本不能包含文件名中非法字符。尽管如此,这仍然是缓存动态生成图像的最有效方式。如果更改按钮外观并需要重新生成缓存图像,只需删除buttons目录中的所有图像,它们将在请求时重新创建。

还可以进一步操作,让您的button.php脚本支持多种图像类型。只需检查$extension并在脚本末尾调用适当的imagepng()imagejpeg()imagegif()函数。还可以解析文件名并添加修改器,如颜色、大小和字体,或直接在 URL 中传递它们。由于示例中的parse_str()调用,例如*your.site/buttons/php… URL 将以 16 号字体大小显示“php”。

缩放图像

图像大小可以通过两种方式改变。imagecopyresized()函数速度快但粗糙,在新图像中可能会产生锯齿边缘。imagecopyresampled()函数速度较慢,但使用像素插值生成平滑边缘,并提供调整大小后图像的清晰度。这两个函数接受相同的参数:

imagecopyresized(*`dest`*, *`src`*, *`dx`*, *`dy`*, *`sx`*, *`sy`*, *`dw`*, *`dh`*, *`sw`*, *`sh`*);
imagecopyresampled(*`dest`*, *`src`*, *`dx`*, *`dy`*, *`sx`*, *`sy`*, *`dw`*, *`dh`*, *`sw`*, *`sh`*);

destsrc参数是图像句柄。点(dx, dy)是目标图像中将复制区域的点。点(sx, sy)是源图像的左上角。swshdwdh参数给出了源和目标中复制区域的宽度和高度。

示例 10-11 对 php.jpg 图像进行缩放,平滑地缩小为原尺寸的四分之一,得到 图 10-10 中的图像。

示例 10-11. 使用 imagecopyresampled() 进行调整大小
<?php
$source = imagecreatefromjpeg("php_logo_big.jpg");

$width = imagesx($source);
$height = imagesy($source);
$x = $width / 2;
$y = $height / 2;

$destination = imagecreatetruecolor($x, $y);
imagecopyresampled($destination, $source, 0, 0, 0, 0, $x, $y, $width, $height);

header("Content-Type: image/png");
imagepng($destination);

原始 php.jpg 图像

图 10-9. 原始 php.jpg 图像

结果为 1/4 大小的图像

图 10-10. 结果为 1/4 大小的图像

将高度和宽度除以 4 而不是 2,会产生如 图 10-11 所示的输出。

结果为 1/16 大小的图像

图 10-11. 结果为 1/16 大小的图像

颜色处理

GD 库支持既有 8 位调色板(256 色)图像,也支持带有 alpha 通道透明度的真彩色图像。

要创建一个 8 位调色板图像,使用 imagecreate() 函数。随后,使用 imagecolorallocate() 分配的第一个颜色填充图像的背景:

$width = 128;
$height = 256;

$image = imagecreate($width, $height);
$white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);

要创建带有 7 位 alpha 通道的真彩色图像,请使用 imagecreatetruecolor() 函数:

$image = imagecreatetruecolor(*`width`*, *`height`*);

使用 imagecolorallocatealpha() 创建包含透明度的颜色索引:

$color = imagecolorallocatealpha(*`image`*, *`red`*, *`green`*, *`blue`*, *`alpha`*);

Alpha 值介于 0(不透明)和 127(透明)之间。

尽管大多数人习惯于 8 位(0–255)的 alpha 通道,GD 实际上使用的是很方便的 7 位(0–127)alpha 通道。每个像素由一个 32 位有符号整数表示,四个 8 位字节排列如下:

High Byte Low Byte
{Alpha Channel} {Red} {Green} {Blue}

对于有符号整数,最左边的位,或者最高位,用于指示值是否为负数,因此只留下了 31 位的实际信息。PHP 的默认整数值是有符号长整型,可以存储单个 GD 调色板条目。该整数是正数还是负数告诉我们该调色板条目是否启用了抗锯齿。

与调色板图像不同,真彩色图像中的第一个颜色分配并不自动成为背景色。相反,图像最初填充了完全透明的像素。调用 imagefilledrectangle() 可以用任何背景颜色填充图像。

示例 10-12 创建了一个真彩色图像,并在白色背景上绘制了半透明的橙色椭圆。

示例 10-12. 白色背景上的简单橙色椭圆
<?php
$image = imagecreatetruecolor(150, 150);
$white = imagecolorallocate($image, 255, 255, 255);

imagealphablending($image, false);
imagefilledrectangle($image, 0, 0, 150, 150, $white);

$red = imagecolorallocatealpha($image, 255, 50, 0, 50);
imagefilledellipse($image, 75, 75, 80, 63, $red);

header("Content-Type: image/png");
imagepng($image);

图 10-12 展示了 示例 10-12 的输出。

白色背景上的橙色椭圆

图 10-12. 白色背景上的橙色椭圆

您可以使用 imagetruecolortopalette() 函数将真彩色图像转换为带有颜色索引的图像(也称为调色板图像)。

使用 Alpha 通道

在示例 10-12 中,我们在绘制背景和椭圆之前关闭了alpha 混合。Alpha 混合是一个开关,用于确定绘制图像时是否应用 alpha 通道(如果存在)。如果 alpha 混合关闭,新像素将替换旧像素。如果新像素具有 alpha 通道,则将其维持,但将覆盖的原始像素的所有像素信息都将丢失。

示例 10-13 通过在橙色椭圆上绘制一个 50% alpha 通道的灰色矩形来说明 alpha 混合。

示例 10-13. 50% alpha 通道的灰色矩形叠加
<?php
$image = imagecreatetruecolor(150, 150);
imagealphablending($image, false);

$white = imagecolorallocate($image, 255, 255, 255);
imagefilledrectangle($image, 0, 0, 150, 150, $white);

$red = imagecolorallocatealpha($image, 255, 50, 0, 63);
imagefilledellipse($image, 75, 75, 80, 50, $red);

imagealphablending($image, false);

$gray = imagecolorallocatealpha($image, 70, 70, 70, 63);
imagefilledrectangle($image, 60, 60, 120, 120, $gray);

header("Content-Type: image/png");
imagepng($image);

图 10-13 展示了示例 10-13 的输出(alpha 混合仍然关闭)。

橙色椭圆上的灰色矩形

图 10-13. 橙色椭圆上的灰色矩形

如果我们在调用imagefilledrectangle()之前将示例 10-13 修改为启用 alpha 混合,我们将得到图像显示在图 10-14 中。

启用了 alpha 混合的图像

图 10-14. 启用了 alpha 混合的图像

颜色识别

要检查图像中特定像素的颜色索引,可以使用imagecolorat()

$color = imagecolorat(*`image`*, *`x`*, *`y`*);

对于使用 8 位色板的图像,该函数返回一个颜色索引,然后将其传递给imagecolorsforindex()以获取实际的 RGB 值:

$values = imagecolorsforindex(*`image`*, *`index`*);

imagecolorsforindex()返回的数组具有键'red''green''blue'。如果在真彩图像的颜色上调用imagecolorsforindex(),返回的数组还具有键'alpha'的值。这些键的值对应于调用imagecolorallocate()imagecolorallocatealpha()时使用的 0 到 255 的颜色值和 0 到 127 的 alpha 值。

真彩索引

imagecolorallocatealpha()返回的颜色索引实际上是一个 32 位有符号长整数,其中前三个字节分别表示红、绿、蓝的值。接下来的一个位表示此颜色是否启用了抗锯齿,剩余的七位表示透明度值。

例如:

$green = imagecolorallocatealpha($image, 0, 0, 255, 127);

此代码将$green设置为2130771712,在十六进制中为0x7F00FF00,在二进制中为01111111000000001111111100000000

这等同于以下的imagecolorresolvealpha()调用:

$green = (127 << 24) | (0 << 16) | (255 << 8) | 0;

在这个例子中,你也可以去掉两个0条目,简化成:

$green = (127 << 24) | (255 << 8);

要解构这个值,可以使用如下方法:

$a = ($col & 0x7F000000) >> 24;
$r = ($col & 0x00FF0000) >> 16;
$g = ($col & 0x0000FF00) >> 8;
$b = ($col & 0x000000FF);

很少需要像这样直接操作颜色值。一个应用场景是生成一个颜色测试图像,显示纯红、绿和蓝的纯色。例如:

$image = imagecreatetruecolor(256, 60);

for ($x = 0; $x < 256; $x++) {
 imageline($image, $x, 0, $x, 19, $x);
 imageline($image, 255 - $x, 20, 255 - $x, 39, $x << 8);
 imageline($image, $x, 40, $x, 59, $x<<16);
}

header("Content-Type: image/png");
imagepng($image);

图 10-15 展示了颜色测试程序的输出。

颜色测试

图 10-15. 颜色测试

显然,它比我们在黑白打印中展示的要更加丰富多彩,因此请自行尝试此示例。在这个特定示例中,仅计算像素颜色要比为每种颜色调用imagecolorallocatealpha()更容易。

图像的文本表示

imagecolorat() 函数的一个有趣用途是循环遍历图像中的每个像素,并使用该颜色数据执行某些操作。示例 10-14 在php-tiny.jpg图像中的每个像素处打印#,表示该像素的颜色。

示例 10-14. 将图像转换为文本
<html><body bgcolor="#000000">

<tt><?php
$image = imagecreatefromjpeg("php_logo_tiny.jpg");

$dx = imagesx($image);
$dy = imagesy($image);

for ($y = 0; $y < $dy; $y++) {
 for ($x = 0; $x < $dx; $x++) {
 $colorIndex = imagecolorat($image, $x, $y);
 $rgb = imagecolorsforindex($image, $colorIndex);

 printf('<font color=#%02x%02x%02x>#</font>',
 $rgb['red'], $rgb['green'], $rgb['blue']);
 }

 echo "<br>\n";
} ?></tt>

</body></html>

其结果是图像的 ASCII 表示,如图 10-16 所示。

图像的 ASCII 表示

图 10-16. 图像的 ASCII 表示

接下来是什么

有许多不同的方式可以利用 PHP 在运行时处理图像。这确实打破了 PHP 仅用于生成 Web HTML 内容的神话。如果你有时间和兴趣更深入地探索可能性,请随意尝试这里的代码示例。在下一章中,我们将探讨生成动态 PDF 文档中的另一个神话破除者。敬请关注!

¹ 这仅适用于具有调色板的图像。使用ImageCreateTrueColor() 创建的真彩色图像不遵循此规则。

² UTF-8 是一个 8 位 Unicode 编码方案(http://www.unicode.org)。

第十一章:PDF

Adobe 的便携式文档格式(PDF)是一种在屏幕和打印输出中获得一致外观的流行方式,本章将向您展示如何动态创建包含文本、图形、链接等内容的 PDF 文件。这样做将为许多应用程序打开大门。您几乎可以创建任何类型的商业文档,包括格式信、发票和收据。此外,您还可以通过将文本叠加到纸质表格的扫描副本上并将结果保存为 PDF 文件来自动化大部分文书工作。

PDF 扩展

PHP 有几个用于生成 PDF 文档的库。本章的示例使用流行的FPDF 库,这是一组 PHP 代码,您可以通过require()函数包含在脚本中——它不需要任何服务器端配置或支持,因此即使在没有主机支持的情况下也可以使用。但是,PDF 文件的基本概念、结构和特性应该适用于所有的 PDF 库。

注意

另一个生成 PDF 的库,TCPDF,在处理 HTML 特殊字符和 UTF-8 多语言输出方面比 FPDF 更强大。如果您需要这种功能,请查看它。您将使用的方法是writeHTMLCell()writeHTML()

文档和页面

PDF 文档由多个页面组成,每个页面包含文本和/或图像。本节将向您展示如何创建文档、在文档中添加页面、向页面写入文本,并在完成后将页面发送回浏览器。

注意

本章示例假设您至少已将 Adobe PDF 文档查看器安装为浏览器的附加组件。否则,这些示例将无法运行。您可以从Adobe 网站获取该附加组件。

一个简单的示例

让我们从一个简单的 PDF 文档开始。示例 11-1 将文本“Hello Out There!”写入一个页面,然后显示生成的 PDF 文档。

示例 11-1。“PDF 中的 Hello Out There!”
<?php

require("../fpdf/fpdf.php"); // path to fpdf.php

$pdf = new FPDF();
$pdf->addPage();

$pdf->setFont("Arial", 'B', 16);
$pdf->cell(40, 10, "Hello Out There!");

$pdf->output();

示例 11-1 展示了创建 PDF 文档的基本步骤:创建一个新的 PDF 对象实例、创建一个页面、为 PDF 文本设置有效的字体,并将文本写入页面上的“单元格”中。图 11-1 展示了示例 11-1 的输出。

“Hello Out There!”PDF 示例

图 11-1。“PDF 中的 Hello Out There!”示例

初始化文档

在示例 11-1 中,我们首先通过require()函数引用了 FPDF 库。然后代码创建了 FPDF 对象的新实例。请注意,对新 FPDF 实例的所有调用都是该对象中方法的面向对象调用。(如果您在本章中的示例中遇到问题,请参阅第六章。)创建了 FPDF 对象的新实例后,您需要向对象添加至少一页,因此调用了AddPage()方法。接下来,您需要为即将生成的输出设置字体,使用SetFont()调用。然后,使用cell()方法调用,您可以将输出发送到创建的文档。要将所有工作发送到浏览器,只需使用output()方法。

输出基本文本单元格

在 FPDF 库中,单元格是页面上的一个矩形区域,您可以创建并控制它。该单元格可以具有高度、宽度和边框,并且当然可以包含文本。cell()方法的基本语法如下:

cell(`float` w [, `float` h [, `string` txt [, `mixed` border
 [, `int` ln [, `string` align [, `int` fill [, `mixed` link]]]]]]])

第一个选项是宽度,然后是高度,接着是要输出的文本。然后是边框,换行控制,文本的对齐方式,文本的填充颜色,最后是是否希望文本成为 HTML 链接。例如,如果我们想要更改我们的原始示例以具有边框并且居中对齐,我们将更改cell()代码如下:

$pdf->cell(90, 10, "Hello Out There!", 1, 0, 'C');

在使用 FPDF 生成 PDF 文档时,您将广泛使用cell()方法,因此最好花一些时间了解此方法的细节。我们将在本章中涵盖大部分内容。

文本

文本是 PDF 文件的核心。因此,有许多选项可更改其外观和布局。在本节中,我们将讨论 PDF 文档中使用的坐标系统、插入文本和更改文本属性的功能,以及字体的使用。

坐标

在 FPDF 库中,PDF 文档的原点(0, 0)位于定义页面的左上角。所有的测量单位都是以点、毫米、英寸或厘米来指定的。点(默认)等于 1/72 英寸,或 0.35 毫米。在示例 11-2 中,我们使用FPDF()类的实例化构造方法将页面尺寸的默认值更改为英寸。此调用的其他选项包括页面的方向(纵向或横向)和页面大小(通常是 Legal 或 Letter)。该实例化的所有选项显示在表 11-1 中。

表 11-1. FPDF 选项

FPDF()构造函数参数参数选项
方向P(纵向;默认)L(横向)

| 测量单位 | pt(点,或 1/72 英寸,默认)in(英寸)

mm(毫米)

cm(厘米)|

| 页面尺寸 | Letter(默认)Legal

A5

A3

A4或可自定义尺寸(请参阅 FPDF 文档)|

此外,在示例 11-2 中,我们使用ln()方法调用来管理页面的当前行。ln()方法可以带有一个可选参数,指示它移动多少个单位(即构造函数调用中定义的度量单位)。在我们的情况下,我们将页面定义为以英寸为单位,因此我们在文档中以英寸为单位移动。此外,由于我们将页面定义为以英寸为单位,因此cell()方法的坐标也以英寸为单位呈现。

这并不是构建 PDF 页面的理想方法,因为使用英寸单位时,您无法像使用点或毫米单位那样精细控制。在此示例中,我们使用英寸单位以便更清楚地看到示例。

示例 11-2 将文本放置在页面的角落和中心。

示例 11-2. 演示坐标和线条管理
<?php
require("../fpdf/fpdf.php");

$pdf = new FPDF('P', 'in', 'Letter');
$pdf->addPage();

$pdf->setFont('Arial', 'B', 24);

$pdf->cell(0, 0, "Top Left!", 0, 1, 'L');
$pdf->cell(6, 0.5, "Top Right!", 1, 0, 'R');
$pdf->ln(4.5);

$pdf->cell(0, 0, "This is the middle!", 0, 0, 'C');
$pdf->ln(5.3);

$pdf->cell(0, 0, "Bottom Left!", 0, 0, 'L');
$pdf->cell(0, 0, "Bottom Right!", 0, 0, 'R');

$pdf->output();

示例 11-2 的输出显示在图 11-2 中。

坐标和线条控制演示输出

图 11-2. 坐标和线条控制演示输出

让我们稍微分析一下这段代码。在我们用构造函数定义页面之后,我们看到了以下代码行:

$pdf->cell(0, 0, "Top Left!", 0, 1, 'L');
$pdf->cell(6, 0.5, "Top Right!", 1, 0, 'R');
$pdf->ln(4.5);

第一个cell()方法调用告诉 PDF 类从顶部坐标(0,0)开始,并输出左对齐文本“Top Left!”,无边框,并在输出结束时插入换行符。接下来的cell()方法调用会创建一个宽度为六英寸的单元格,再次从页面左侧开始,带有半英寸高的边框和右对齐文本“Top Right!”。然后,我们告诉 PDF 类在页面上向下移动 4½英寸,并从那一点继续生成输出。正如您所见,单凭cell()ln()方法就有许多可能的组合。但 FPDF 库的功能远不止这些。

文本属性

有三种常见的修改文本外观的方法:粗体、下划线和斜体。在示例 11-3 中,使用SetFont()方法(在本章前面介绍过)来改变输出文本的格式。请注意,这些文本外观的修改并非互斥(即,可以任意组合使用),并且在最后一个SetFont()调用中更改了字体名称。

示例 11-3. 演示字体属性
<?php
require("../fpdf/fpdf.php");

$pdf = new FPDF();
$pdf->addPage();

$pdf->setFont("Arial", '', 12);
$pdf->cell(0, 5, "Regular normal Arial Text here, size 12", 0, 1, 'L');
$pdf->ln();

$pdf->setFont("Arial", 'IBU', 20);
$pdf->cell(0, 15, "This is Bold, Underlined, Italicised Text size 20", 0, 0, 'L');
$pdf->ln();

$pdf->setFont("Times", 'IU', 15);
$pdf->cell(0, 5, "This is Underlined Italicised 15pt Times", 0, 0, 'L');

$pdf->output();

此外,在这段代码中,构造函数是以无参数形式调用的,使用了纵向、点数和信件的默认值。示例 11-3 的输出显示在图 11-3 中。

改变字体类型、大小和属性

图 11-3. 改变字体类型、大小和属性

FPDF 提供的可用字体样式包括:

  • Courier(等宽字体)

  • HelveticaArial(同义词;无衬线字体)

  • Times(衬线字体)

  • Symbol(符号)

  • ZapfDingbats(符号)

您可以使用AddFont()方法包含任何其他您具有定义文件的字体系列。

当然,如果您不能改变输出到 PDF 定义中的文本的颜色,那将毫无乐趣可言。这时就需要用到SetTextColor()方法了。该方法接受现有的字体定义,并简单地改变文本的颜色。务必在使用cell()方法之前调用此方法,以便可以更改单元格的内容。颜色参数是由红、绿和蓝的数字常量组合而成,范围从0(无色)到255(全彩色)。如果您不传入第二和第三个参数,则第一个数字将是一个灰度,其红、绿和蓝值均等于传入的单个值。示例 11-4 展示了如何应用此方法。

示例 11-4. 展示颜色属性
<?php
require("../fpdf/fpdf.php");

$pdf = new FPDF();
$pdf->addPage();

$pdf->setFont("Times", 'U', 15);
$pdf->setTextColor(128);
$pdf->cell(0, 5, "Times font, Underlined and shade of Grey Text", 0, 0, 'L');
$pdf->ln(6);

$pdf->setTextColor(255, 0, 0);
$pdf->cell(0, 5, "Times font, Underlined and Red Text", 0, 0, 'L');

$pdf->output();

图 11-4 是示例 11-4 中代码的结果。

添加颜色到文本输出

Figure 11-4. 添加颜色到文本输出

页面头部、页脚和类扩展

到目前为止,我们只看了 PDF 页面中少量输出的内容。我们特意这样做是为了向您展示在受控环境中可以做什么的多样性。现在我们需要扩展 FPDF 库的功能。请记住,这个库实际上只是一个供您使用和扩展的类定义,我们现在来看看后者。由于 FPDF 确实是一个类定义,我们只需使用 PHP 中原生的对象命令来扩展它,就像这样:

class MyPDF extends FPDF

在这里,我们将FPDF类扩展为一个名为MyPDF的新类。然后,我们可以扩展对象中的任何方法。如果愿意,我们甚至可以向我们的类扩展中添加更多方法,但这一点稍后再谈。我们将首先查看的两个方法是现有的空方法的扩展,这些空方法在FPDF类的父类中预定义:header()footer()。这些方法的功能正如它们的名称所示,为 PDF 文档中的每一页生成页面头和页脚。示例 11-5 相当长,展示了这两个方法的定义。您会注意到只有少数新使用的方法;其中最重要的是AliasNbPages(),它简单地用于在 PDF 文档发送到浏览器之前跟踪整体页面计数。

示例 11-5. 定义头部和尾部方法
<?php
require("../fpdf/fpdf.php");

class MyPDF extends FPDF
{
 function header()
 {
 global $title;

 $this->setFont("Times", '', 12);
 $this->setDrawColor(0, 0, 180);
 $this->setFillColor(230, 0, 230);
 $this->setTextColor(0, 0, 255);
 $this->setLineWidth(1);

 $width = $this->getStringWidth($title) + 150;
 $this->cell($width, 9, $title, 1, 1, 'C', 1);
 $this->ln(10);
 }

 function footer()
 {
 //Position at 1.5 cm from bottom
 $this->setY(-15);
 $this->setFont("Arial", 'I', 8);
 $this->cell(0, 10,
 "This is the page footer -> Page {$this->pageNo()}/{nb}", 0, 0, 'C');
 }
}

$title = "FPDF Library Page Header";

$pdf = new MyPDF('P', 'mm', 'Letter');
$pdf->aliasNbPages();
$pdf->addPage();

$pdf->setFont("Times", '', 24);
$pdf->cell(0, 0, "some text at the top of the page", 0, 0, 'L');
$pdf->ln(225);

$pdf->cell(0, 0, "More text toward the bottom", 0, 0, 'C');

$pdf->addPage();
$pdf->setFont("Arial", 'B', 15);

$pdf->cell(0, 0, "Top of page 2 after header", 0, 1, 'C');

$pdf->output();

示例 11-5 的结果显示在图 11-5 中。这是两页并排显示的截图,显示页脚中的页数和第一页顶部的页码。页眉有一个带有一些着色的单元格(用于美观效果);当然,如果您不想使用颜色,您也可以不使用。

FPDF 头部和页脚添加

图 11-5. FPDF 头部和页脚添加

图片和链接

FPDF 库还可以处理 PDF 文档内部或外部的图像插入和链接控制。让我们首先看看 FPDF 如何允许您将图形插入到文档中。也许您正在构建一个使用公司标志的 PDF 文档,并希望制作一个横幅以打印在每一页的顶部。我们可以使用前面定义的header()footer()方法来实现这一点。一旦我们有要使用的图像文件,我们只需调用image()方法将图像放置在 PDF 文档中即可。

新的header()方法代码如下:

function header()
{
 global $title;

 $this->setFont("Times", '', 12);
 $this->setDrawColor(0, 0, 180);
 $this->setFillColor(230, 0, 230);
 $this->setTextColor(0, 0, 255);
 $this->setLineWidth(0.5);

 $width = $this->getStringWidth($title) + 120;

 $this->image("php_logo_big.jpg", 10, 10.5, 15, 8.5);
 $this->cell($width, 9, $title, 1, 1, 'C');
 $this->ln(10);
}

正如您所见,image()方法的参数是要使用的图像文件名,开始图像输出的x坐标,y坐标,以及图像的宽度和高度。如果您没有指定宽度和高度,FPDF 将尽其所能在指定的xy坐标处呈现图像。代码在其他方面也有所更改。我们从cell()方法调用中删除了填充颜色参数,尽管我们仍然调用了填充颜色方法。这使得头部单元格周围的框区域为白色,以便我们可以轻松插入图像。

这个新标题插入图像的头部输出显示在图 11-6 中。

插入图像文件的 PDF 页面头部

图 11-6. 插入图像文件的 PDF 页面头部

本节标题中还有链接,现在让我们来看看如何使用 FPDF 在 PDF 文档中添加链接。FPDF 可以创建两种类型的链接:内部链接(即 PDF 文档内部到同一文档的另一个位置,比如两页后)和外部链接到 Web URL。

内部链接分为两部分创建。首先,您定义链接的起点或原点,然后设置链接被点击时要去的锚点或目的地。要设置链接的起点,请使用addLink()方法。此方法将返回一个句柄,您需要在创建链接的目的部分使用该句柄。要设置目的地,请使用setLink()方法,该方法以起始链接句柄作为其参数,以便它可以执行两步之间的连接。

可以通过两种方式创建外部 URL 类型的链接。如果您使用图像作为链接,需要使用image()方法。如果要使用纯文本作为链接,需要使用cell()write()方法。在本例中,我们使用write()方法。

示例 11-6 中显示了内部和外部链接。

示例 11-6. 创建内部和外部链接
<?php
require("../fpdf/fpdf.php");

$pdf = new FPDF();

// First page
$pdf->addPage();
$pdf->setFont("Times", '', 14);

$pdf->write(5, "For a link to the next page - Click");
$pdf->setFont('', 'U');
$pdf->setTextColor(0, 0, 255);
$linkToPage2 = $pdf->addLink();
$pdf->write(5, "here", $linkToPage2);
$pdf->setFont('');

// Second page
$pdf->addPage();
$pdf->setLink($linkToPage2);
$pdf->image("php-tiny.jpg", 10, 10, 30, 0, '', "http://www.php.net");
$pdf->ln(20);

$pdf->setTextColor(1);
$pdf->cell(0, 5, "Click the following link, or click on the image", 0, 1, 'L');
$pdf->setFont('', 'U');
$pdf->setTextColor(0,0,255);
$pdf->write(5, "www.oreilly.com", "http://www.oreilly.com");

$pdf->output();

此代码生成的两页输出显示在图 11-7 和图 11-8 中。

链接 PDF 文档的第一页

Figure 11-7. 链接 PDF 文档的第一页

链接 PDF 文档的第二页,包含 URL 链接

Figure 11-8. 链接 PDF 文档的第二页,包含 URL 链接

表格和数据

到目前为止,我们只看到了静态 PDF 材料。但是 PHP 的功能远不止静态处理。在本节中,我们将结合数据库中的一些数据(使用数据库信息的 MySQL 示例来自第九章)和 FPDF 生成表格的能力。

注意

请确保参考第九章 中提供的数据库文件结构,以便在本节中跟随进展。

示例 11-7 稍微有些冗长。不过,它有详细的注释,请先阅读这里的内容;我们将在列表后面介绍要点。

示例 11-7. 生成表格
<?php
`require`("../fpdf/fpdf.php");

`class` TablePDF `extends` FPDF
{
 `function` buildTable($header, $data)
 {
 $this->setFillColor(255, 0, 0);
 $this->setTextColor(255);
 $this->setDrawColor(128, 0, 0);
 $this->setLineWidth(0.3);
 $this->setFont('', 'B');

//Header // make an array for the column widths
 $widths = `array`(85, 40, 15);
// send the headers to the PDF document
 `for`($i = 0; $i < *`count`*($header); $i++) {
 $this->cell($widths[$i], 7, $header[$i], 1, 0, 'C', 1);
 }

 $this->ln();

// Color and font restoration
 $this->setFillColor(175);
 $this->setTextColor(0);
 $this->setFont('');

// now spool out the data from the $data array
 $fill = 0;// used to alternate row color backgrounds
 $url = "http://www.oreilly.com";

 `foreach`($data `as` $row)
 {
 $this->cell($widths[0], 6, $row[0], 'LR', 0, 'L', $fill);

// set colors to show a URL style link
 $this->setTextColor(0, 0, 255);
 $this->setFont('', 'U');
 $this->cell($widths[1], 6, $row[1], 'LR', 0, 'L', $fill, $url);

// restore normal color settings
 $this->setTextColor(0);
 $this->setFont('');
 $this->cell($widths[2], 6, $row[2], 'LR', 0, 'C', $fill);

 $this->ln();

 $fill = ($fill) ? 0 : 1;
 }
 $this->cell(*`array_sum`*($widths), 0, '', 'T');
 }
}

//connect to database $dbconn = `new` mysqli('localhost', 'dbusername', 'dbpassword', 'library');
$sql = "SELECT * FROM books ORDER BY title";
$result = $dbconn->query($sql);

// build the data array from the database records. `while` ($row = $result->fetch_assoc()) {
 $data[] = `array`($row['title'], $row['ISBN'], $row['pub_year']);
}

// start and build the PDF document $pdf = `new` TablePDF();

// Column titles $header = `array`("Title", "ISBN", "Year");

$pdf->setFont("Arial", '', 14);

$pdf->addPage();
$pdf->buildTable($header, $data);

$pdf->output();

我们正在使用数据库连接并构建两个数组,将其发送到此扩展类的buildTable()自定义方法。在buildTable()方法中,我们为表头设置颜色和字体属性。然后,根据第一个传入的数组发送表头。还有一个称为$width的数组,用于在调用cell()时设置列宽。

在发送表格头之后,我们使用包含数据库信息的$data数组,并通过foreach循环遍历该数组。请注意,此处的cell()方法在其border参数中使用了'LR'。这会在所涉及的单元格左右添加边框,从而有效地为表行添加边框。我们还在第二列中添加了一个 URL 链接,以演示它可以与表行构建一起完成。最后,我们使用$fill变量来交替切换,使表格行逐行建立时背景色会交替变化。

buildTable()方法中对cell()方法的最后一次调用用于绘制表格底部并关闭列。

此代码的结果显示在 Figure 11-9 中。

基于数据库信息生成的 FPDF 表格,包含活动 URL 链接

Figure 11-9. 基于数据库信息生成的 FPDF 表格,包含活动 URL 链接

接下来做什么

这一章中没有涵盖的 FPDF 的其他功能还有很多。请务必访问库的网站,查看它能帮助你完成的其他示例。那里还有代码片段和完全功能的脚本,以及一个讨论论坛,全部旨在帮助你成为 FPDF 的专家。

在接下来的章节中,我们将稍微调整一下方向,探讨 PHP 与 XML 之间的交互。我们将涵盖一些技术,可以用来“消费”XML,并且介绍如何使用一个名为 SimpleXML 的内置库来解析它。

第十二章《XML》

可扩展标记语言 XML 是一种标准化的数据格式。它看起来有点像 HTML,使用标签(<example>像这样</example>)和实体(&amp;)。然而,与 HTML 不同,XML 旨在易于以程序方式解析,并且有关于在 XML 文档中可以和不可以做的规则。XML 现在是出版、工程和医学等各个领域的标准数据格式。它用于远程过程调用、数据库、采购订单等各种用途。

有许多情况下你可能希望使用 XML。因为它是数据传输的常见格式,其他程序可以生成 XML 文件供您提取信息(解析)或在 HTML 中显示(转换)。本章将向您展示如何使用 PHP 捆绑的 XML 解析器,以及如何使用可选的 XSLT 扩展来转换 XML。我们还简要介绍了生成 XML 的方法。

近年来,XML 已经被用于远程过程调用(XML-RPC)。客户端将函数名和参数值编码为 XML,并通过 HTTP 发送到服务器。服务器解码函数名和数值,决定如何处理,并返回以 XML 编码的响应值。XML-RPC 已被证明是一种有用的方法,可以集成用不同语言编写的应用程序组件。我们将在第十六章展示如何编写 XML-RPC 服务器和客户端,但现在让我们先看一下 XML 的基础知识。

《XML 简明指南》

大多数 XML 由元素(类似 HTML 标签)、实体和常规数据组成。例如:

<book isbn="1-56592-610-2">
 <title>Programming PHP</title>
 <authors>
 <author>Rasmus Lerdorf</author>
 <author>Kevin Tatroe</author>
 <author>Peter MacIntyre</author>
 </authors>
</book>

在 HTML 中,通常会有未闭合的开放标签。最常见的例子是:

<br>

在 XML 中,这是非法的。XML 要求每个开放标签都有对应的闭合标签。对于不包含任何内容的标签,如换行符<br>,XML 添加了这种语法:

<br />

标签可以嵌套但不能重叠。例如,这是有效的:

<book><title>Programming PHP</title></book>

但这个则无效,因为<book><title>标签重叠:

<book><title>Programming PHP</book></title>

XML 还要求文档以标识正在使用的 XML 版本开头(可能还包括其他内容,如文本编码)。例如:

<?xml version="1.0" ?>

符合格式良好的 XML 文档的最后要求是文件顶层只能有一个元素。例如,这是格式良好的:

<?xml version="1.0" ?>
<library>
 <title>Programming PHP</title>
 <title>Programming Perl</title>
 <title>Programming C#</title>
</library>

这不是格式良好的,因为文件顶层有三个元素:

<?xml version="1.0" ?>
<title>Programming PHP</title>
<title>Programming Perl</title>
<title>Programming C#</title>

XML 文档通常不是完全自由的。XML 文档中的特定标签、属性和实体,以及它们嵌套的规则,构成了文档的结构。有两种方法来定义这种结构:文档类型定义(DTD)和模式。DTD 和模式用于验证文档,即确保它们遵循其文档类型的规则。

大多数 XML 文档不包括 DTD;在这些情况下,文档仅在其为有效 XML 时被视为有效。其他情况下,文档通过一个指定名称和位置(文件或 URL)的外部实体来识别 DTD:

<!DOCTYPE rss PUBLIC 'My DTD Identifier' 'http://www.example.com/my.dtd'>

有时将一个 XML 文档封装在另一个 XML 文档中会很方便。例如,表示邮件消息的 XML 文档可能具有包围附加文件的 attachment 元素。如果附加文件是 XML,则它是一个嵌套的 XML 文档。如果邮件消息文档有一个 body 元素(消息主题),并且附加文件是一个表示解剖学的 XML 表示,该表示也具有 body 元素,但此元素具有完全不同的 DTD 规则,那么在 body 在文档的中途更改意义时,如何验证或理解文档呢?

使用命名空间解决了这个问题。命名空间允许您限定 XML 标签,例如 email:bodyhuman:body

XML 比我们在这里讨论的还要复杂得多。要对 XML 进行简要介绍,请阅读 Learning XML(O’Reilly 出版)的书籍,由埃里克·雷(Erik Ray)编写。要详细了解 XML 语法和标准,请参阅 XML in a Nutshell(O’Reilly 出版)的书籍,由艾略特·拉斯蒂·哈罗德(Elliotte Rusty Harold)和 W. Scott Means 编写。

生成 XML

就像 PHP 可以用于生成动态 HTML 一样,它也可以用于生成动态 XML。您可以基于表单、数据库查询或其他任何 PHP 可以做的事情来为其他程序生成 XML。动态 XML 的一个应用是 Rich Site Summary(RSS),这是一个用于聚合新闻站点的文件格式。您可以从数据库或 HTML 文件中读取文章信息,并根据该信息生成 XML 摘要文件。

从 PHP 脚本生成 XML 文档很简单。只需使用 header() 函数更改文档的 MIME 类型为 "text/xml"。要发出 <?xml ... ?> 声明而不被解释为格式不正确的 PHP 标签,只需从 PHP 代码内部 echo 该行:

echo '<?xml version="1.0" encoding="ISO-8859-1" ?>';

示例 12-1 使用 PHP 生成了一个 RSS 文档。RSS 文件是一个 XML 文档,包含多个 channel 元素,每个元素包含一些新闻 item 元素。每个新闻 item 可以有一个标题、一个描述和指向文章本身的链接。RSS 支持的 item 属性比 示例 12-1 创建的要多。正如 PHP 生成 HTML 没有特殊函数一样,生成 XML 也没有特殊函数。你只需 echo 它!

示例 12-1. 生成 XML 文档
<?php
header('Content-Type: text/xml');
echo "<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>";
?>
<!DOCTYPE rss PUBLIC "-//Netscape Communications//DTD RSS 0.91//EN"
 "http://my.netscape.com/publish/formats/rss-0.91.dtd">

<rss version="0.91">
 <channel>
 <?php
 // news items to produce RSS for
 $items = array(
 array(
 'title' => "Man Bites Dog",
 'link' => "http://www.example.com/dog.php",
 'desc' => "Ironic turnaround!"
 ),
 array(
 'title' => "Medical Breakthrough!",
 'link' => "http://www.example.com/doc.php",
 'desc' => "Doctors announced a cure for me."
 )
 );

 foreach($items as $item) {
 echo "<item>\n";
 echo " <title>{$item['title']}</title>\n";
 echo " <link>{$item['link']}</link>\n";
 echo " <description>{$item['desc']}</description>\n";
 echo " <language>en-us</language>\n";
 echo "</item>\n\n";
 } ?>
 </channel>
</rss>

此脚本生成以下输出:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE rss PUBLIC "-//Netscape Communications//DTD RSS 0.91//EN"
 "http://my.netscape.com/publish/formats/rss-0.91.dtd">
<rss version="0.91">
 <channel>
<item>
 <title>Man Bites Dog</title>
 <link>http://www.example.com/dog.php</link>
 <description>Ironic turnaround!</description>
 <language>en-us</language>
</item>

<item>
 <title>Medical Breakthrough!</title>
 <link>http://www.example.com/doc.php</link>
 <description>Doctors announced a cure for me.</description>
 <language>en-us</language>
</item>
 </channel>
</rss>

解析 XML

假设您有一组 XML 文件,每个文件包含关于一本书的信息,并且您想构建一个索引,显示集合中每本书的文档标题及其作者。您需要解析 XML 文件以识别 titleauthor 元素及其内容。您可以通过正则表达式和诸如 strtok() 的字符串函数手动执行此操作,但实际上这比看起来要复杂得多。此外,即使是有效的 XML 文档,这样的方法也容易出现故障。最简单和最快的解决方案是使用 PHP 随附的 XML 解析器之一。

PHP 包括三个 XML 解析器:一个基于 Expat C 库的事件驱动库,一个基于 DOM 的库,以及一个用于解析简单 XML 文档的名为 SimpleXML 的库。

最常用的解析器是基于事件的库,允许解析但不验证 XML 文档。这意味着您可以了解存在哪些 XML 标签及其周围的内容,但不能确定它们是否是该类型文档正确结构中正确的 XML 标签。在实践中,这通常不是一个大问题。PHP 的事件驱动 XML 解析器在读取文档时调用您提供的各种处理器函数,遇到特定的 事件,如元素的开始或结束。

在接下来的章节中,我们将讨论您可以提供的处理器、设置处理器的函数以及触发这些处理器调用的事件。我们还提供了用于创建解析器以在内存中生成 XML 文档映射的示例函数,这些示例函数与一个示例应用程序结合在一起,以美化 XML。

元素处理器

当解析器遇到元素的开始或结束时,将调用起始和结束元素处理器。您可以通过 xml_set_element_handler() 函数设置这些处理器:

xml_set_element_handler(*`parser`*, *`start_element`*, *`end_element`*);

start_elementend_element 参数是处理器函数的名称。

当 XML 解析器遇到元素开始时,将调用起始元素处理器:

*`startElementHandler`*(*`parser`*, *`element`*, *`&``attributes`*);

起始元素处理器接收三个参数:调用处理器的 XML 解析器的引用,已打开的元素的名称以及包含解析器遇到的元素任何属性的数组。$attribute 数组通过引用传递以提高速度。

示例 12-2 包含了一个起始元素处理器 startElement() 的代码。该处理器简单地以粗体打印元素名称,并以灰色显示属性。

示例 12-2. 起始元素处理器
function startElement($parser, $name, $attributes) {
 $outputAttributes = array();

 if (count($attributes)) {
 foreach($attributes as $key => $value) {
 $outputAttributes[] = "<font color=\"gray\">{$key}=\"{$value}\"</font>";
 }
 }

 echo "&lt;<b>{$name}</b> " . join(' ', $outputAttributes) . '&gt;';
}

当解析器遇到元素结束时,将调用结束元素处理器:

*`endElementHandler`*(*`parser`*, *`element`*);

它接受两个参数:调用处理器的 XML 解析器的引用,以及正在关闭的元素的名称。

示例 12-3 展示了一个结束元素处理器,格式化该元素。

示例 12-3. 结束元素处理器
function endElement($parser, $name) {
 echo "&lt;<b>/{$name}</b>&gt;";
}

字符数据处理器

在元素之间的所有文本(字符数据,或 XML 术语中的 CDATA)由字符数据处理程序处理。您使用 xml_set_character``_data_handler() 函数设置的处理程序在每个字符数据块后被调用:

xml_set_character_data_handler(*`parser`*, *`handler`*);

字符数据处理程序接收引发处理程序的 XML 解析器的引用和包含字符数据本身的字符串:

*`characterDataHandler`*(*`parser`*, *`cdata`*);

这是一个简单的字符数据处理程序,只需打印数据:

function characterData($parser, $data) {
 echo $data;
}

处理指令

处理指令用于在 XML 中嵌入脚本或其他代码到文档中。PHP 本身可以看作是一个处理指令,并且使用 <?php ... ?> 标签样式遵循 XML 格式来标记代码。当 XML 解析器遇到处理指令时,会调用处理指令处理程序。使用 xml_set_processing_instruction_handler() 函数设置处理程序:

xml_set_processing_instruction_handler(*`parser`*, *`handler`*);

处理指令看起来像:

<? *`target` `instructions`* ?>

处理指令处理程序接收引发处理程序的 XML 解析器的引用、目标名称(例如,'php')和处理指令:

*`processingInstructionHandler`*(*`parser`*, *`target`*, *`instructions`*);

处理指令如何处理取决于您。其中一种技巧是将 PHP 代码嵌入 XML 文档中,并在解析文档时使用 eval() 函数执行该 PHP 代码。示例 12-4 就是这样做的。当然,如果要包含 eval() 代码,您必须信任正在处理的文档。eval() 将运行提供给它的任何代码,甚至是销毁文件或将密码发送给破解者的代码。实际上,执行这种任意代码非常危险。

示例 12-4. 处理指令处理程序
function processing_instruction($parser, $target, $code) {
 if ($target === 'php') {
 eval($code);
 }
}

实体处理程序

XML 中的实体是占位符。XML 提供了五个标准实体(&amp;&gt;&lt;&quot;&apos;),但 XML 文档可以定义它们自己的实体。大多数实体定义不会触发事件,而 XML 解析器在调用其他处理程序之前会展开文档中的大多数实体。

两种类型的实体,外部和未解析的,PHP 的 XML 库提供了特殊支持。外部 实体是其替换文本由文件名或 URL 标识而不是在 XML 文件中显式给出的实体。您可以定义一个处理程序,以便在字符数据中发生外部实体时调用它,但如果需要的话,您必须自行解析文件或 URL 的内容。

未解析 实体必须伴随有一个标记声明,虽然您可以为未解析实体和标记的声明定义处理程序,但在调用字符数据处理程序之前,文本中的未解析实体会被删除。

外部实体

外部实体引用允许 XML 文档包含其他 XML 文档。通常,外部实体引用处理程序会打开引用的文件,解析文件,并将结果包含在当前文档中。使用 xml_set_external_entity_ref_handler() 设置处理程序,该函数接受 XML 解析器的引用和处理程序函数的名称:

xml_set_external_entity_ref_handler(*`parser`*, *`handler`*);

外部实体引用处理程序带有五个参数:触发处理程序的解析器、实体的名称、用于解析实体标识符的基本统一资源标识符(URI)(当前始终为空)、系统标识符(例如文件名)以及实体声明中定义的实体的公共标识符。例如:

*`externalEntityHandler`*(*`parser`*, *`entity`*, *`base`*, *`system`*, *`public`*);

如果您的外部实体引用处理程序返回 false(如果它不返回任何值,则会返回 false),XML 解析将停止,并显示 XML_ERROR_EXTERNAL_ENTITY_HANDLING 错误。如果返回 true,解析将继续。

示例 12-5 显示了如何解析外部引用的 XML 文档。定义两个函数 createParser()parse() 来实际创建和传送 XML 解析器的工作。您可以同时用它们来解析顶级文档和通过外部引用包含的任何文档。这些函数在“使用解析器”部分有描述。外部实体引用处理程序只需确定正确的文件以发送到这些函数。

示例 12-5. 外部实体引用处理程序
function externalEntityReference($parser, $names, $base, $systemID, $publicID) {
 if ($systemID) {
 if (!list ($parser, $fp) = createParser($systemID)) {
 echo "Error opening external entity {$systemID}\n";

 return false;
 }

 return parse($parser, $fp);
 }

 return false;
}

未解析实体

未解析实体声明必须与符号声明一起出现:

<!DOCTYPE doc [
 <!NOTATION jpeg SYSTEM "image/jpeg">
 <!ENTITY logo SYSTEM "php-tiny.jpg" NDATA jpeg>
]>

使用 xml_set_notation_decl_handler() 注册符号声明处理程序:

xml_set_notation_decl_handler(*`parser`*, *`handler`*);

处理程序将带有五个参数:

*`notationHandler`*(*`parser`*, *`notation`*, *`base`*, *`system`*, *`public`*);

基本 参数是用于解析符号标识符的基本 URI(当前始终为空)。符号的系统标识符或公共标识符将被设置,但不会同时出现。

使用 xml_set_unparsed_entity_decl_handler() 函数注册未解析实体声明:

xml_set_unparsed_entity_decl_handler(*`parser`*, *`handler`*);

处理程序将带有六个参数:

*`unparsedEntityHandler`*(*`parser`*, *`entity`*, *`base`*, *`system`*, *`public`*, *`notation`*);

符号 参数标识与此未解析实体关联的符号声明。

默认处理程序

对于其他任何事件,例如 XML 声明和 XML 文档类型,将调用默认处理程序。调用 xml_set_default_handler() 函数设置默认处理程序:

xml_set_default_handler(*`parser`*, *`handler`*);

处理程序将带有两个参数:

*`defaultHandler`*(*`parser`*, *`text`*);

文本 参数将根据触发默认处理程序的事件类型具有不同的值。示例 12-6 在调用默认处理程序时仅打印给定的字符串。

示例 12-6. 默认处理程序
function default($parser, $data) {
 echo "<font color=\"red\">XML: Default handler called with '{$data}'</font>\n";
}

选项

XML 解析器有几个选项可用于控制源和目标编码以及大小写折叠。使用 xml_parser_set_option() 设置选项:

xml_parser_set_option(*`parser`*, *`option`*, *`value`*);

类似地,使用 xml_parser_get_option() 查询解析器的选项:

$value = xml_parser_get_option(*`parser`*, *`option`*);

字符编码

PHP 使用的 XML 解析器支持多种不同字符编码的 Unicode 数据。在内部,PHP 的字符串总是以 UTF-8 编码,但由 XML 解析器解析的文档可以是 ISO-8859-1、US-ASCII 或 UTF-8. 不支持 UTF-16。

创建 XML 解析器时,可以为其指定用于解析文件的编码格式。如果省略,则假定源文件为 ISO-8859-1. 如果遇到源编码范围外的字符,XML 解析器将返回错误并立即停止处理文档。

解析器的目标编码是 XML 解析器将数据传递给处理程序函数的编码;通常与源编码相同。在 XML 解析器的生命周期中的任何时间,都可以更改目标编码。解析器通过用问号字符 (?) 替换目标编码范围外的任何字符来降级这些字符。

使用常量 XML_OPTION_TARGET_ENCODING 获取或设置传递给回调函数的文本编码。允许的值包括 "ISO-8859-1"(默认)、"US-ASCII""UTF-8"

大小写折叠

XML 文档中的元素和属性名称默认转换为全大写。可以通过将 xml_parser​_set_option() 函数的 XML_OPTION_CASE_FOLDING 选项设置为 false 来关闭此行为(并获得区分大小写的元素名称):

xml_parser_set_option(XML_OPTION_CASE_FOLDING, false);

跳过仅包含空白的内容

将选项 XML_OPTION_SKIP_WHITE 设置为忽略完全由空白字符组成的值。

xml_parser_set_option(XML_OPTION_SKIP_WHITE, true);

截断标签名

创建解析器时,可以选择截断每个标签名称的开头字符。要通过 XML_OPTION_SKIP_TAGSTART 选项截断每个标签的起始字符数,需提供该值:

xml_parser_set_option(XML_OPTION_SKIP_TAGSTART, 4);
// <xsl:name> truncates to "name"

在这种情况下,标签名称将被截断四个字符。

使用解析器

要使用 XML 解析器,使用 xml_parser_create() 创建解析器,为解析器设置处理程序和选项,然后使用 xml_parse() 函数传递数据块给解析器,直到数据耗尽或解析器返回错误。处理完成后,通过调用 xml_parser_free() 释放解析器。

函数 xml_parser_create() 返回一个 XML 解析器:

$parser = xml_parser_create([*`encoding`*]);

可选的 encoding 参数指定被解析文件的文本编码("ISO-8859-1""US-ASCII""UTF-8")。

函数 xml_parse() 如果解析成功返回 true,失败返回 false

$success = xml_parse(*`parser`*, *`data`*[, *`final`* ]);

参数 data 是要处理的 XML 字符串。可选的 final 参数应设为 true,以解析最后一段数据。

为了轻松处理嵌套文档,编写创建解析器并设置其选项和处理程序的函数。这样可以将选项和处理程序设置放在一个地方,而不是在外部实体引用处理程序中重复它们。示例 12-7 展示了这样一个函数。

示例 12-7. 创建解析器
function createParser($filename) {
 $fh = fopen($filename, 'r');
 $parser = xml_parser_create();

 xml_set_element_handler($parser, "startElement", "endElement");
 xml_set_character_data_handler($parser, "characterData");
 xml_set_processing_instruction_handler($parser, "processingInstruction");
 xml_set_default_handler($parser, "default");

 return array($parser, $fh);
}

function parse($parser, $fh) {
 $blockSize = 4 * 1024; // read in 4 KB chunks

 while ($data = fread($fh, $blockSize)) {
 if (!xml_parse($parser, $data, feof($fh))) {
 // an error occurred; tell the user where
 echo 'Parse error: ' . xml_error_string($parser) . " at line " .
 xml_get_current_line_number($parser);

 return false;
 }
 }

 return true;
}

if (list ($parser, $fh) = createParser("test.xml")) {
 parse($parser, $fh);
 fclose($fh);

 xml_parser_free($parser);
}

错误

如果解析完成,xml_parse() 函数返回 true,如果有错误则返回 false。如果出现问题,请使用 xml_get_error_code() 获取标识错误的代码:

$error = xml_get_error_code($parser);

错误代码对应于以下错误常量之一:

XML_ERROR_NONE
XML_ERROR_NO_MEMORY
XML_ERROR_SYNTAX
XML_ERROR_NO_ELEMENTS
XML_ERROR_INVALID_TOKEN
XML_ERROR_UNCLOSED_TOKEN
XML_ERROR_PARTIAL_CHAR
XML_ERROR_TAG_MISMATCH
XML_ERROR_DUPLICATE_ATTRIBUTE
XML_ERROR_JUNK_AFTER_DOC_ELEMENT
XML_ERROR_PARAM_ENTITY_REF
XML_ERROR_UNDEFINED_ENTITY
XML_ERROR_RECURSIVE_ENTITY_REF
XML_ERROR_ASYNC_ENTITY
XML_ERROR_BAD_CHAR_REF
XML_ERROR_BINARY_ENTITY_REF
XML_ERROR_ATTRIBUTE_EXTERNAL_ENTITY_REF
XML_ERROR_MISPLACED_XML_PI
XML_ERROR_UNKNOWN_ENCODING
XML_ERROR_INCORRECT_ENCODING
XML_ERROR_UNCLOSED_CDATA_SECTION
XML_ERROR_EXTERNAL_ENTITY_HANDLING

这些常量通常没有太大用处。使用 xml_error_string() 将错误代码转换为您在报告错误时可以使用的字符串:

$message = xml_error_string(*`code`*);

例如:

$error = xml_get_error_code($parser);

if ($error != XML_ERROR_NONE) {
 die(xml_error_string($error));
}

方法作为处理程序

因为在 PHP 中函数和变量是全局的,任何需要多个函数和变量的应用组件都适合面向对象设计。XML 解析通常需要您跟踪解析过程中的位置(例如,“刚刚看到一个打开的 title 元素,因此在看到关闭的 title 元素之前,跟踪字符数据”)使用变量,当然您必须编写几个处理程序函数来操作状态并实际执行操作。将这些函数和变量包装到一个类中使您能够将它们与程序的其余部分分开,并在以后轻松重用功能。

使用 xml_set_object() 函数注册一个对象到解析器中。注册后,XML 解析器会寻找该对象上的方法作为处理程序,而不是作为全局函数:

xml_set_object(*`object`*);

示例解析应用

让我们开发一个程序来解析 XML 文件,并从中显示不同类型的信息。示例 12-8 中提供的 XML 文件包含一组书籍的信息。

示例 12-8. books.xml 文件
<?xml version="1.0" ?>
<library>
 <book>
 <title>Programming PHP</title>
 <authors>
 <author>Rasmus Lerdorf</author>
 <author>Kevin Tatroe</author>
 <author>Peter MacIntyre</author>
 </authors>
 <isbn>1-56592-610-2</isbn>
 <comment>A great book!</comment>
 </book>
 <book>
 <title>PHP Pocket Reference</title>
 <authors>
 <author>Rasmus Lerdorf</author>
 </authors>
 <isbn>1-56592-769-9</isbn>
 <comment>It really does fit in your pocket</comment>
 </book>
 <book>
 <title>Perl Cookbook</title>
 <authors>
 <author>Tom Christiansen</author>
 <author whereabouts="fishing">Nathan Torkington</author>
 </authors>
 <isbn>1-56592-243-3</isbn>
 <comment>Hundreds of useful techniques, most
 applicable to PHP as well as Perl</comment>
 </book>
</library>

PHP 应用程序解析文件并向用户显示书籍列表,仅显示标题和作者。该菜单显示在图 12-1 中。标题是指向显示书籍完整信息页面的链接。《编程 PHP》的详细信息页面显示在图 12-2 中。

我们定义了一个名为 BookList 的类,其构造函数解析 XML 文件并构建记录列表。BookList 上有两个方法,从记录列表生成输出。showMenu() 方法生成书籍菜单,showBook() 方法显示特定书籍的详细信息。

解析文件涉及跟踪记录,我们所处的元素以及哪些元素对应记录(book)和字段(titleauthorisbncomment)。$record属性在构建当前记录时保存当前记录,$currentField保存当前处理的字段名称(例如,title)。$records属性是迄今为止读取的所有记录的数组。

书籍菜单

图 12-1. 书籍菜单

书籍详细信息

图 12-2. 书籍详细信息

两个关联数组,$fieldType$endsRecord,告诉我们哪些元素对应记录中的字段,以及哪个闭合元素表示记录的结束。在构造函数中初始化这些数组。

处理程序本身相当简单。当我们看到元素的开头时,我们会确定它是否对应我们感兴趣的字段。如果是,则将 $currentField 属性设置为该字段名称,这样当我们看到字符数据(例如书籍的标题)时,我们就知道它是哪个字段的值。当获取字符数据时,如果 $currentField 表示我们处于字段中,则将其添加到当前记录的适当字段中。当我们看到元素的结尾时,我们检查它是否是记录的结尾;如果是,则将当前记录添加到已完成记录的数组中。

一个 PHP 脚本,在 示例 12-9 中提供,同时处理书籍菜单和书籍详情页面。书籍菜单中的条目链接回菜单 URL,并带有一个 GET 参数,用于标识要显示的书籍的 ISBN。

示例 12-9. bookparse.php
<html>
 <head>
 <title>My Library</title>
 </head>

 <body>
 <?php
 class BookList {
 const FIELD_TYPE_SINGLE = 1;
 const FIELD_TYPE_ARRAY = 2;
 const FIELD_TYPE_CONTAINER = 3;

 var $parser;
 var $record;
 var $currentField = '';
 var $fieldType;
 var $endsRecord;
 var $records;

 function __construct($filename) {
 $this->parser = xml_parser_create();
 xml_set_object($this->parser, $this);
 xml_set_element_handler($this->parser, "elementStarted", "elementEnded");
 xml_set_character_data_handler($this->parser, "handleCdata");

 $this->fieldType = array(
 'title' => self::FIELD_TYPE_SINGLE,
 'author' => self::FIELD_TYPE_ARRAY,
 'isbn' => self::FIELD_TYPE_SINGLE,
 'comment' => self::FIELD_TYPE_SINGLE,
 );

 $this->endsRecord = array('book' => true);

 $xml = join('', file($filename));
 xml_parse($this->parser, $xml);

 xml_parser_free($this->parser);
 }

 function elementStarted($parser, $element, &$attributes) {
 $element = strtolower($element);

 if ($this->fieldType[$element] != 0) {
 $this->currentField = $element;
 }
 else {
 $this->currentField = '';
 }
 }

 function elementEnded($parser, $element) {
 $element = strtolower($element);

 if ($this->endsRecord[$element]) {
 $this->records[] = $this->record;
 $this->record = array();
 }

 $this->currentField = '';
 }

 function handleCdata($parser, $text) {
 if ($this->fieldType[$this->currentField] == self::FIELD_TYPE_SINGLE) {
 $this->record[$this->currentField] .= $text;
 }
 else if ($this->fieldType[$this->currentField] == self::FIELD_TYPE_ARRAY) {
 $this->record[$this->currentField][] = $text;
 }
 }

 function showMenu() {
 echo "<table>\n";

 foreach ($this->records as $book) {
 echo "<tr>";
 echo "<th><a href=\"{$_SERVER['PHP_SELF']}?isbn={$book['isbn']}\">";
 echo "{$book['title']}</a></th>";
 echo "<td>" . join(', ', $book['author']) . "</td>\n";
 echo "</tr>\n";
 }

 echo "</table>\n";
 }

 function showBook($isbn) {
 foreach ($this->records as $book) {
 if ($book['isbn'] !== $isbn) {
 continue;
 }

 echo "<p><b>{$book['title']}</b> by " . join(', ', $book['author']) . "<br />";
 echo "ISBN: {$book['isbn']}<br />";
 echo "Comment: {$book['comment']}</p>\n";
 }

 echo "<p>Back to the <a href=\"{$_SERVER['PHP_SELF']}\">list of books</a>.</p>";
 }
 }

 $library = new BookList("books.xml");

 if (isset($_GET['isbn'])) {
 // return info on one book
 $library->showBook($_GET['isbn']);
 }
 else {
 // show menu of books
 $library->showMenu();
 } ?>
 </body>
</html>

使用 DOM 解析 XML

PHP 提供的 DOM 解析器使用起来简单得多,但复杂性减少的同时,内存使用量却会大大增加。DOM 解析器不会触发事件并允许您在解析过程中处理文档,而是将 XML 文档作为输入,并返回整个节点和元素的树:

$parser = new DOMDocument();
$parser->load("books.xml");
processNodes($parser->documentElement);

function processNodes($node) {
 foreach ($node->childNodes as $child) {
 if ($child->nodeType == XML_TEXT_NODE) {
 echo $child->nodeValue;
 }
 else if ($child->nodeType == XML_ELEMENT_NODE) {
 processNodes($child);
 }
 }
}

使用 SimpleXML 解析 XML

如果您要处理非常简单的 XML 文档,可以考虑 PHP 提供的第三个库 SimpleXML。SimpleXML 无法像 DOM 扩展那样生成文档,也不像事件驱动扩展那样灵活或内存高效,但它非常容易读取、解析和遍历简单的 XML 文档。

SimpleXML 接受文件、字符串或使用 DOM 扩展生成的 DOM 文档,并生成一个对象。该对象上的属性是数组,提供对每个节点中元素的访问。通过这些数组,可以使用数字索引访问元素,并使用非数字索引访问属性。最后,可以对检索到的任何值使用字符串转换,以获取项目的文本值。

例如,我们可以使用以下方式显示 books.xml 文档中所有书籍的标题:

$document = simplexml_load_file("books.xml");

foreach ($document->book as $book) {
 echo $book->title . "\r\n";
}

在对象上使用 children() 方法,可以迭代给定节点的子节点;同样,可以使用 attributes() 方法迭代节点的属性:

$document = simplexml_load_file("books.xml");

foreach ($document->book as $node) {
 foreach ($node->attributes() as $attribute) {
 echo "{$attribute}\n";
 }
}

最后,使用对象上的 asXml() 方法,可以以 XML 格式检索文档的 XML。这使您可以轻松地更改文档中的值并将其写回到磁盘:

$document = simplexml_load_file("books.xml");

foreach ($document->children() as $book) {
 $book->title = "New Title";
}

file_put_contents("books.xml", $document->asXml());

使用 XSLT 转换 XML

可扩展样式表语言转换(XSLT)是一种将 XML 文档转换为不同 XML、HTML 或任何其他格式的语言。例如,许多网站提供其内容的多种格式——HTML、可打印 HTML 和 WML(无线标记语言)是常见的。呈现同一信息的多个视图的最简单方法是在 XML 中维护内容的一种形式,并使用 XSLT 生成 HTML、可打印 HTML 和 WML。

PHP 的 XSLT 扩展使用 Libxslt C 库提供 XSLT 支持。

XSLT 转换涉及三个文档:原始 XML 文档、包含转换规则的 XSLT 文档和生成的文档。最终文档不必是 XML;事实上,常见的是使用 XSLT 从 XML 生成 HTML。在 PHP 中进行 XSLT 转换,你创建一个 XSLT 处理器,给它一些要转换的输入,然后销毁处理器。

创建一个处理器,通过创建一个新的 XsltProcessor 对象:

$processor = new XsltProcessor;

将 XML 和 XSL 文件解析为 DOM 对象:

$xml = new DomDocument;
$xml->load($filename);

$xsl = new DomDocument;
$xsl->load($filename);

将 XML 规则附加到对象:

$processor->importStyleSheet($xsl);

使用 transformToDoc()transformToUri()transformToXml() 方法处理文件:

$result = $processor->transformToXml($xml);

每个都将表示 XML 文档的 DOM 对象作为参数。

示例 12-10 是我们将要转换的 XML 文档。它的格式与你在网上找到的许多新闻文档类似。

示例 12-10. XML 文档
<?xml version="1.0" ?>

<news xmlns:news="http://slashdot.org/backslash.dtd">
 <story>
 <title>O'Reilly Publishes Programming PHP</title>
 <url>http://example.org/article.php?id=20020430/458566</url>
 <time>2002-04-30 09:04:23</time>
 <author>Rasmus and some others</author>
 </story>

 <story>
 <title>Transforming XML with PHP Simplified</title>
 <url>http://example.org/article.php?id=20020430/458566</url>
 <time>2002-04-30 09:04:23</time>
 <author>k.tatroe</author>
 <teaser>Check it out</teaser>
 </story>
</news>

示例 12-11 是我们将用来将 XML 文档转换为 HTML 的 XSL 文档。每个 xsl:template 元素包含处理输入文档一部分的规则。

示例 12-11. 新闻 XSL 转换
<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="yes" encoding="utf-8" />

<xsl:template match="/news">
 <html>
 <head>
 <title>Current Stories</title>
 </head>
 <body bgcolor="white" >
 <xsl:call-template name="stories"/>
 </body>
 </html>
</xsl:template>

<xsl:template name="stories">
 <xsl:for-each select="story">
 <h1><xsl:value-of select="title" /></h1>

 <p>
 <xsl:value-of select="author"/> (<xsl:value-of select="time"/>)<br />
 <xsl:value-of select="teaser"/>
 [ <a href="{url}">More</a> ]
 </p>

 <hr />
 </xsl:for-each>
</xsl:template>

</xsl:stylesheet>

示例 12-12 是将 XML 文档使用 XSL 样式表转换为 HTML 文档所需的非常少量代码。我们创建一个处理器,运行文件,并打印结果。

示例 12-12. 文件中的 XSL 转换
<?php
$processor = new XsltProcessor;

$xsl = new DOMDocument;
$xsl->load("rules.xsl");
$processor->importStyleSheet($xsl);

$xml = new DomDocument;
$xml->load("feed.xml");
$result = $processor->transformToXml($xml);

echo "<pre>{$result}</pre>";

尽管没有专门讨论 PHP,但 Doug Tidwell 的书籍 XSLT(O’Reilly)提供了 XSLT 样式表语法的详细指南。

接下来做什么

虽然 XML 仍然是数据共享的主要格式,但简化版的 JavaScript 数据封装——JSON,已迅速成为简单、可读且简洁的网络服务响应和其他数据的事实标准。这将是我们下一章的主题。