将背景色(bKGD Chunk)写入 PNG 文件






4.50/5 (2投票s)
本文介绍 PNG 文件格式的一些知识,以及如何向 PNG 文件写入 bKGD Chunk,以解决 Internet Explorer 默认的奶油-青色背景色问题。
引言
互联网上有很多文章介绍如何在 Internet Explorer 6 及更早版本中实现透明 PNG。但这些方法“勉强”可用。在某些情况下(例如在高安全模式下),您无法使用透明 PNG,此时您的图像会以奶油-青色背景色显示。好消息是,背景色不必是奶油-青色,您可以自定义任何颜色。本文旨在展示如何向 PNG 文件写入必要的信息,为 PNG 文件设置背景色,以便在这些情况下,访问者能看到至少“尝试”与网站匹配的图像。
当使用 .NET 通过 Photoshop 或 bitmap.Save()
方法写出 PNG 文件时,没有添加背景色的支持,因此您必须手动完成。
我为 StyleSpread 的付费版本编写了这段代码。StyleSpread 是一个 CSS 编译器,但其付费版本具有图像创建功能。由于它是一个桌面应用程序,我可以直接将 PNG 文件写入硬盘,这在 ASP.NET 中可能无法实现。要使其在 Web 服务器上运行,可能需要对代码进行一些修改。
背景
为了帮助您理解,我将快速浏览一下 PNG 规范。首先,在 Photoshop(或其他程序)中创建一个 1x1 的 PNG 文件,然后使用您喜欢的十六进制编辑器(如 UltraEdit)打开该文件。
PNG 文件由块 (chunks) 组成。块只是一个信息块。每个块都有一个 4 个字符的标识符,例如 IHDR、bKGD、IDAT 和 IEND(大写表示必需,小写表示可选)。块遵循以下格式:4 字节存储块的长度(包括 4 个字符的标识符),4 字节用于标识符,然后是块数据,最后是 4 字节的 CRC。CRC 是错误校验机制。它们通过将大量数据组合成几个字节来计算。如果数据传输不正确,CRC 将与数据不匹配,您就会知道存在问题。
bKGD Chunk 是指定背景色的块。我们需要将其插入到我们的 bitmap.Save()
和 Photoshop 创建的 PNG 文件中,以摆脱奶油-青色。
为了方便起见,这段代码始终将 bKGD Chunk 直接插入到 IDAT Chunk 之前。IDAT Chunk 包含图像数据,而 bKGD Chunk 必须在其之前定义。
最后一个问题
有没有注意到 PNG 文件在 Explorer 中看起来比在其他浏览器中更暗?bitmap.Save()
和 Photoshop 都会创建一个特别烦人的 gAMA Chunk。它定义了图像的 gamma 值。Explorer 理解这个 Chunk,而其他浏览器则不。所以,如果您想在所有浏览器中显示相同的颜色,就必须去掉这个 Chunk。在 StyleSpread 中,我使用 Ken Silverman 的 PNGOUT 压缩器 来删除它,因为它有一个删除不需要的 Chunk 的功能。如果您想在代码中手动删除它,现在您对 PNG 规范有了一些了解,应该不难。
关于代码
首先,您需要一些代码来生成 CRC 校验。生成此代码的程序可以 在此 免费获取,但它是用 C 编写的,用 C 编写的任何代码都很丑陋(来吧,尽管喷我!)。我为您提供 C# 版本。第一个方法将创建一个查找表以加快 CRC 计算速度。
/// <summary>
/// Creates the CRC table for calculating a 32-bit CRC.
/// </summary>
private static void CreateCrcTable()
{
uint c;
int k;
int n;
for (n = 0; n < 256; n++)
{
c = (uint)n;
for (k = 0; k < 8; k++)
{
if ((c & 1) == 1)
{
c = 0xedb88320 ^ (c >> 1);
}
else
{
c = c >> 1;
}
}
CrcTable[n] = c;
}
IsTableCreated = true;
}
static uint[] CrcTable = new uint[256];
static bool IsTableCreated = false;
下一个函数返回字节数组中字节的 CRC。这段代码相当复杂。如果您时间紧迫,不必费心阅读它。
/// <summary>
/// Calculates an array of 4 bytes containing the calculated CRC.
/// </summary>
/// <param name="buf">The raw data on which to calculate the CRC.</param>
public static byte[] GetCrc(byte[] buffer)
{
uint data = 0xFFFFFFFF;
int n;
if (!IsTableCreated)
CreateCrcTable();
for (n = 0; n < buffer.Length; n++)
data = CrcTable[(data ^ buffer[n]) & 0xff] ^ (data >> 8);
data = data ^ 0xFFFFFFFF;
byte b1 = Convert.ToByte(data >> 24);
byte b2 = Convert.ToByte(b1 << 8 ^ data >> 16);
byte b3 = Convert.ToByte(((data >> 16 << 16) ^ (data >> 8 << 8)) >> 8);
byte b4 = Convert.ToByte((data >> 8 << 8) ^ data);
return new byte[] { b1, b2, b3, b4 };
}
最后这 4 个字节的定义看起来很复杂,但实际上只是位移操作,用于将一个 int
转换为字节数组。如果您有更好的方法,我很想听听。现在我们只需调用 GetCrc
方法,传入我们的数据,就能得到 CRC。
下一个函数是实现魔法的函数。它将一个 bKGD Chunk 写入硬盘上的 PNG 文件。
在 StyleSpread 中,我将它们称为“备份背景色”而不是 bKGD Chunk。这样称呼对非程序员来说更易于理解。
/// <summary>
/// Writes a backup background color to the specified PNG file.
/// </summary>
/// <param name="fileName">The path and name of the PNG file to write to.</param>
/// <param name="color">The <see cref="Color"/> to set as the backup background color.
/// </param>
public static void WriteBackupBackgroundColor(string fileName, Color color)
{
byte[] lengthData = { 0, 0, 0, 6 };
byte[] bkgdChunk = { 98, 75, 71, 68, 0, color.R, 0, color.G, 0, color.B };
byte[] data;
byte[] crcData = PngUtil.GetCrc(bkgdChunk);
一个块由以下 4 个组件组成。bKGD 的长度总是 6。当转换为 ASCII 时,98、75、71、68 代表 bKGD。老实说,我不知道为什么红色、绿色和蓝色分量需要以 0 开头,但我在 UltraEdit 中十六进制编辑 PNG 文件时看到了这种情况,而且它似乎有效。:)
using (FileStream fs = new FileStream(fileName, FileMode.Open))
using (BinaryReader binReader = new BinaryReader(fs))
{
data = binReader.ReadBytes((int)binReader.BaseStream.Length);
}
您需要修改此代码才能在 ASP.NET 中使用。基本上,不是从硬盘读取图像,而是从数据库或图像数据所在的其他地方读取。
// 18 bytes is the size of a bKGD chunk
byte[] newData = new byte[data.Length + 18];
int dataIndex = 0;
bool wroteChunk = false;
for (int i = 0; i < data.Length; i++)
{
if (!wroteChunk && data[i + 4] == 'I' && data[i + 5] == 'D'
&& data[i + 6] == 'A' && data[i + 7] == 'T')
{
Array.Copy(lengthData, 0, newData, dataIndex, 4);
dataIndex += 4;
Array.Copy(bkgdChunk, 0, newData, dataIndex, bkgdChunk.Length);
dataIndex += bkgdChunk.Length;
Array.Copy(crcData, 0, newData, dataIndex, 4);
dataIndex += 4;
wroteChunk = true;
}
newData[dataIndex++] = data[i];
}
在这里,我们正在搜索 IDAT Chunk。一旦找到它,我们就开始写入 bKGD 数据,然后切换回写入文件的其余部分。请注意,我正在检查 data[i + 4]
。我向前查找 4 个字节以跳过 4 个字节的块长度信息。
if (File.Exists(fileName))
File.Delete(fileName);
using (FileStream fs = new FileStream(fileName, FileMode.CreateNew))
using (BinaryWriter binWriter = new BinaryWriter(fs))
{
binWriter.Write(newData);
}
}
如果文件存在,则删除它,然后写出一个新文件。ASP.NET 用户——请将 PNG 写入 Repsonse.OutputStream
,而不是写到硬盘。
Using the Code
使用这段代码非常简单。包含的 *.zip 文件包含一个源文件。它是一个 static
类,因此您只需将其包含在项目中并调用 WriteBackupBackgroundColor
即可。
关注点
这段代码只做一件事,就是写出 bKGD Chunk。我本可以使其写入其他 Chunk,但那是我只需要的功能。也许有些勇敢的人可以为 TweakPNG 编写一个托管的替代库。
历史
- 2006 年 11 月 17 日 - 初始发布