PHP中创建字符位图并渲染字符串的详细教程。

在 PHP 中创建字符位图

注意:这篇文章发表已超过两年,其中的信息可能已过时。若您发现问题,请留言,我公司会尽力更正。

2024 年 6 月 23 日 - 阅读时长 18 分钟

前些天,我看了迪伦·比蒂(Dylan Beattie)在北欧开发者大会(NDC)上关于排版故事的视频,其中有个部分特别吸引我。

这个演讲涵盖了书写符号的起源、排版艺术形式的形成,还有电子墨水显示屏的发明。要是您有时间,真值得一看。

我感兴趣的是关于早期文字处理器和 ASCII 字符标准的讨论。早期文字处理器没办法存储完全渲染一种字体所需的数据量,所以字符是通过位图系统来渲染的。

大写字母 A 的 ASCII 字符代码是 65,当您在键盘上输入字母 A 时,数字 65 会被发送到字符生成器。这个字符生成器通常存于只读存储器(ROM)芯片中,会返回数字 581171396。


A -> 65 -> 581171396

这个数字乍一看没啥意义,实际上它和我们最初输入的字母也没关系。不过,计算机是以二进制形式读取这个数字的,即 00100010101000111111100011000100。

我们把这个字符串的最后两个字符去掉,然后每 5 个字符分割一次。


00100 01010 10001 11111 10001 10001

这看起来还是没什么特别的,但我们可以把它重新排列成一个 5x6 的网格,这样就形成了我们要找的字符,也就是字母“A”。


00100    ..X..
01010    .X.X.
10001 -> X...X
11111    XXXXX
10001    X...X
10001    X...X

在早期计算机屏幕上,这一系列数字存于 ROM 芯片中,用于把 ASCII 字符代码转换为位图,然后用这个位图来点亮界面中的发光二极管(LED)。

在本文中,我将探讨如何把字符映射构建成数组,再用这个数组通过位图把字符串转换为图像。在 Drupal 开发中,有时也会遇到类似的数据转换和处理需求,就像 Drupal 模块开发里对一些特殊字符的显示处理,可能同样需要运用到这种位图转换的思路;在 Drupal 升级到 Drupal11 等版本时,如果涉及到界面显示的字符样式改变,也有可能用到本文的相关技术。

一、构建字符

我不想为每个需要的字符都去计算对应的数字,所以我先创建了一个包含所需位图的数组。以下是第一个字母的示例。


$numbers = [];

$numbers['A'] = [
  '00100',
  '01010',
  '10001',
  '11111',
  '10001',
  '10001',
];

我们要为空格创建一个条目,这很容易实现。


// 创建空格。
$numbers[' '] = 0;

现在我们要把数字数组中的位图转换为对应的十进制数。为此,我们需要遍历这些数组元素,把数组元素拼接成一个二进制数(在末尾添加“00”),然后把这个二进制数传递给 bindec() 函数,将其转换为十进制数。


// 将位图转换为十进制数。
foreach ($numbers as $letter => $number) {
  $binary = implode($number) . '00';
  $dec = bindec($binary);
  $numbers[$letter] = $dec;
}

把位图转换为数字后,我们可以把它们转换为一个静态数组,用作位图查找表。


// 生成映射文件。
$string = '';
$string .= ' $number) {
  $string .= "\t'" . $letter . "' => " . $number . ',' . PHP_EOL;
}
$string .= '];' . PHP_EOL;

file_put_contents('map.php', $string);

这会生成一个名为 map.php 的 PHP 文件,我们可以在任何需要的脚本中包含这个文件。map.php 文件的内容本质上是一个包含大量数字的 $map 数组。

以下是该文件中字母 A、B 和 C 的内容。


 581171396,
	'B' => 4101839096,
	'C' => 1952516280,
];

如果您想查看完整的字符列表,可以自行查找。

有了这个查找表后,我们就可以开始渲染字符了。

二、渲染字符

要渲染字符,我们需要把字母重新转换为二进制数。这可以通过在 $map 数组中查找要转换的字母,并将该值转换为二进制值来实现。因为我们使用 sprintf() 函数来完成这个转换,实际上得到的是一个字符串,我们可以对其进行操作。


require 'map.php';

$letter = "A";

// 获取与字母对应的数字。
$number = $map[$letter];

// 将数字转换为二进制数的字符串表示形式。
$binary = sprintf("%032b", $number);

然后可以把这个字符串分成 5 部分,舍弃第 6 部分,因为这部分包含两个我们在渲染字母时不需要的字符。


// 将二进制字符串分割成 5 位一组的块。
$parts = str_split($binary, 5);
// 移除最后一部分,因为这是不会被渲染的尾随位。
unset($parts[6]);

$parts 数组现在包含了我们的位图。

利用这些信息,我们可以使用以下代码将其转换为图像。遍历 parts 数组的每个部分,如果该位置为“1”,则绘制一个红色方块。


// 初始化图像。
$img = imagecreatetruecolor(70, 80);

// 设置图像颜色。
$background = imagecolorallocate($img, 0, 0, 0);
$foreground = imagecolorallocate($img, 255, 0, 0);

$pixelSize = 10;
$offsetX = 10;
$startX = 10;
$startY = 10;

// 遍历各个部分并构建字母。
foreach ($parts as $part) {
  $part = str_split($part);
  $startX = $offsetX;
  foreach ($part as $pixel) {
    $x1 = $startX;
    $y1 = $startY;
    $x2 = $startX + $pixelSize;
    $y2 = $startY + $pixelSize;
    if ($pixel == '1') {
      imagefilledrectangle($img, $x1, $y1, $x2, $y2, $foreground);
    } else {
      imagefilledrectangle($img, $x1, $y1, $x2, $y2, $background);
    }
    $startX += $pixelSize;
  }
  $startY += $pixelSize;
}

imagepng($img, 'bitmap.png');
imagedestroy($img);

使用字母“A”运行这段代码会生成图像。

看起来不错,我们接着更进一步,渲染一个完整的字符串而不只是单个字母。

三、渲染字符串

现在我们已经能在图像中渲染单个字符了,就可以把它扩展为打印出一个字符串。我们只需根据字符串中的字符数量来计算图像的大小,然后遍历每个字符并依次打印出来。

以下函数接受一个字符串和一个映射表,并将字符串渲染为图像。我添加了一个 wrap 参数,以便将图像的生成换行。这个函数的大部分复杂性在于计算图像的大小以及字符相对于图像顶部的偏移量。

由于关键部分已经完成,我就不详细解释这个函数的每一行代码了。


/**
 * 将字符串渲染为位图图像。
 *
 * @param string $string
 *   要渲染的字符串。
 * @param array $map
 *   字母到其数值的映射表。
 * @param int $wrap
 *   可选的换行值。默认为 80 个字符。
 */
function renderString(string $string, array $map, int $wrap = 80):void {
  $letterSpacing = 10;
  $pixelSize = 10;

  // 字母大小始终为像素大小 * 5;
  $letterSize = $pixelSize * 5;

  if (strlen($string)  1) {
      continue;
    }

    // 获取与字母对应的数字。
    $number = $map[$letter];
    // 将数字转换为二进制数的字符串表示形式。
    $binary = sprintf("%032b", $number);
    // 将二进制字符串分割成 5 位一组的块。
    $parts = str_split($binary, 5);
    // 移除最后一部分,因为这是不会被渲染的尾随位。
    unset($parts[6]);

    // 遍历各个部分并构建字母。
    foreach ($parts as $part) {
      $part = str_split($part);
      $startX = $offsetX;
      foreach ($part as $pixel) {
        $x1 = $startX;
        $y1 = $startY;
        $x2 = $startX + $pixelSize;
        $y2 = $startY + $pixelSize;
        if ($pixel == '1') {
          imagefilledrectangle($img, $x1, $y1, $x2, $y2, $foreground);
        } else {
          imagefilledrectangle($img, $x1, $y1, $x2, $y2, $background);
        }
        $startX += $pixelSize;
      }
      $startY += $pixelSize;
    }

    $countY++;
    $countX++;

    // 重新计算 Y 的起始位置。
    $startY = (floor($countY / $wrap) * ($letterSize + $letterSpacing + $letterSpacing)) + $letterSpacing;

    // 计算 X 的起始位置。
    $offsetX += $letterSize + $letterSpacing;
    if ($countX >= $wrap) {
      // 如果 X 的计数大于换行值,则重置 X。
      $countX = 0;
      $offsetX = $letterSpacing;
    }
  }

  imagepng($img, 'bitmap.png');
  imagedestroy($img);
}

我们可以按以下方式运行这个函数。


renderString('THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG 0123456789', $map, 25);

这将生成图像。

这个函数运行得很不错。

像这样生成字符是一种传统方法。在存储和计算能力有限,显示器仅由一组灯组成的时代,这是必要的。从那时起,计算机性能有了很大进步,现在能够以各种形状和大小生成字体。

如果您想自己尝试使用这个函数,可以自行查找完整的字符列表。使用该列表生成一个 map.php 文件,并将其传递给上述函数。如果您觉得这篇文章有用,请告知我公司。