0

0

Node.js中手动创建PNG:解决16位灰度图像IDAT过滤字节问题

DDD

DDD

发布时间:2025-12-04 13:08:30

|

570人浏览过

|

来源于php中文网

原创

Node.js中手动创建PNG:解决16位灰度图像IDAT过滤字节问题

本教程详细阐述了在node.js中手动创建16位灰度png图像时,如何正确处理idat数据块中的过滤字节。核心内容是揭示png规范中关于每行像素数据前必须包含一个过滤类型字节的要求,即使是“无过滤”模式(filter type 0),并提供了处理16位像素数据的字节序转换以及将过滤字节正确插入扫描行的完整代码示例,以确保生成的png图像能被各类查看器正确解析。

在Node.js环境中,有时我们需要更细粒度地控制图像生成过程,例如手动构建PNG文件。这为自定义图像数据和优化特定场景下的图像处理提供了可能。然而,PNG规范的复杂性,尤其是在处理图像数据(IDAT)块的过滤机制时,常常会带来挑战。本教程将深入探讨如何正确地为16位灰度PNG图像生成IDAT块,重点解决过滤字节的正确插入问题,确保图像的兼容性和正确显示。

PNG文件结构与关键数据块

PNG文件由一系列数据块(chunk)组成,每个数据块包含长度、类型、数据和CRC校验码。核心数据块包括:

  • IHDR (Image Header):定义图像的基本属性,如宽度、高度、位深、颜色类型、压缩方法、过滤方法和隔行扫描方法。
  • IDAT (Image Data):包含图像的像素数据,这些数据经过压缩(通常是Deflate算法)。
  • IEND (Image End):标记PNG文件的结束。

在手动创建PNG时,理解这些数据块的结构和内容至关重要。

构建PNG辅助函数

为了方便地创建PNG数据块,我们可以定义一些辅助函数:

import { writeFileSync } from "fs";
import zlib from "zlib";
import crc32 from 'crc/crc32'; // 使用node-crc库

// 将字符串转换为字节数组
function toBytes(v) {
  return v.split(``).map((v) => v.charCodeAt(0));
}

// 将数字编码为四字节值(大端序)
function fourByte(n) {
  return [
    (n & 0xff000000) >> 24,
    (n & 0xff0000) >> 16,
    (n & 0xff00) >> 8,
    n & 0xff,
  ];
}

// 创建PNG数据块
function makeChunk(type, data = []) {
  const typeBytes = toBytes(type);
  const length = fourByte(data.length);
  const buffer = [...typeBytes, ...data];
  const crc = crc32(Buffer.from(buffer)); // CRC32计算的是类型和数据
  return Buffer.from([...length, ...buffer, ...fourByte(crc)]);
}

核心问题:IDAT块中的过滤字节

当在IHDR块中将过滤方法(Filter Method)设置为0(标准自适应过滤)时,PNG规范要求每个扫描行(即每行像素数据)的开头都必须有一个过滤类型字节。即使我们选择“无过滤”(Filter Type 0),这个字节也必须存在。这是许多开发者在手动创建PNG时容易忽略的细节,导致生成的图像在某些查看器中显示异常或无法加载。

对于16位灰度图像,每个像素由两个字节表示。一个包含10个像素的扫描行将包含20个字节的像素数据。如果加上一个过滤字节,该扫描行总长度应为21字节。

解决方案:处理16位像素数据与插入过滤字节

解决这个问题的关键在于:

koly.club
koly.club

一站式社群管理工具

下载
  1. 处理16位像素的字节序:PNG规范要求16位数据以大端序(Big-Endian)存储。如果运行环境是小端序(Little-Endian),则需要进行字节序转换。
  2. 为每个扫描行预置过滤字节:在每个扫描行的实际像素数据之前插入一个0x00字节,表示“无过滤”。

下面是详细的实现步骤:

1. 检测系统字节序

// 定义字节序常量
const LITTLE_ENDIAN = Symbol(`little endian`);
const BIG_ENDIAN = Symbol(`big endian`);

// 检查当前系统是大端序还是小端序
const endian = (function checkEndian() {
  const buf = new ArrayBuffer(2);
  const u8 = new Uint8Array(buf);
  const u16 = new Uint16Array(buf);
  u8.set([0xaa, 0xbb], 0); // 写入字节 0xaa 0xbb
  return u16[0] === 0xbbaa ? LITTLE_ENDIAN : BIG_ENDIAN; // 如果读取为 0xbbaa,则是小端序
})();

2. 生成原始像素数据

我们创建一个10x10的16位灰度图像作为示例。

const [w, h] = [10, 10];
const pngPixels = new Uint16Array(w * h);
for (let x = 0; x < w; x++) {
  for (let y = 0; y < h; y++) {
    const i = x + y * w;
    // 将0-99的索引映射到0-65535的16位灰度值
    pngPixels[i] = ((i / (w * h - 1)) * (2 ** 16 - 1)) | 0;
  }
}

3. 转换字节序并插入过滤字节

这是最关键的部分。我们将Uint16Array转换为Uint8Array,然后根据系统字节序进行调整,并为每个扫描行添加0x00过滤字节。

// 将16位像素值转换为8位字节数组
const pngPixels8 = new Uint8Array(pngPixels.buffer);

// 如果是小端序,需要交换每对字节,使其变为大端序
if (endian === LITTLE_ENDIAN) {
  for (let i = 0, e = pngPixels8.length; i < e; i += 2) {
    let temp = pngPixels8[i];
    pngPixels8[i] = pngPixels8[i + 1];
    pngPixels8[i + 1] = temp;
  }
}

// 创建新的数组来存储带有过滤字节的扫描行数据
// 每个扫描行增加一个字节用于过滤类型
const filtered = new Uint8Array(pngPixels8.length + h); 

for (let y = 0; y < h; y++) {
  const sourceStartIndex = 2 * y * w; // 原始像素数据中当前扫描行的起始索引
  const destinationStartIndex = y * (2 * w + 1) + 1; // 目标数组中当前扫描行像素数据的起始索引 (跳过过滤字节)

  // 插入过滤字节 0x00 (表示无过滤)
  filtered[y * (2 * w + 1)] = 0x00; 

  // 将当前扫描行的像素数据复制到目标数组
  filtered.set(pngPixels8.subarray(sourceStartIndex, sourceStartIndex + 2 * w), destinationStartIndex);
}

4. 组装PNG文件

现在,我们可以使用处理后的filtered数据来创建IDAT块,并将其与其他PNG数据块拼接起来。

// PNG文件头魔数
const magic = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);

// IHDR块:16位灰度图像
const IHDR = makeChunk(`IHDR`, [
  ...fourByte(w), // 宽度
  ...fourByte(h), // 高度
  16, // 位深: 16 bits
  0, // 颜色类型: 灰度
  0, // 压缩方法: 必须是0
  0, // 过滤方法: 必须是0 (标准自适应过滤)
  0, // 隔行扫描方法: 无隔行扫描
]);

// 压缩过滤后的像素数据并创建IDAT块
const IDAT = makeChunk(`IDAT`, zlib.deflateSync(filtered, { level: 9 }));

// 组装所有数据块并写入文件
const pngData = Buffer.concat([magic, IHDR, IDAT, makeChunk(`IEND`)]);
writeFileSync(`test-out.png`, pngData);

完整代码示例

import { writeFileSync } from "fs";
import zlib from "zlib";
import crc32 from 'crc/crc32'; // the node-crc library

// 将字符串转换为字节数组
function toBytes(v) {
  return v.split(``).map((v) => v.charCodeAt(0));
}

// 将数字编码为四字节值(大端序)
function fourByte(n) {
  return [
    (n & 0xff000000) >> 24,
    (n & 0xff0000) >> 16,
    (n & 0xff00) >> 8,
    n & 0xff,
  ];
}

// 创建PNG数据块
function makeChunk(type, data = []) {
  const typeBytes = toBytes(type);
  const length = fourByte(data.length);
  const buffer = [...typeBytes, ...data];
  const crc = crc32(Buffer.from(buffer));
  return Buffer.from([...length, ...buffer, ...fourByte(crc)]);
}

// 定义字节序常量
const LITTLE_ENDIAN = Symbol(`little endian`);
const BIG_ENDIAN = Symbol(`big endian`);

// 检查当前系统是大端序还是小端序
const endian = (function checkEndian() {
  const buf = new ArrayBuffer(2);
  const u8 = new Uint8Array(buf);
  const u16 = new Uint16Array(buf);
  u8.set([0xaa, 0xbb], 0);
  return u16[0] === 0xbbaa ? LITTLE_ENDIAN : BIG_ENDIAN;
})();

// 创建一个10x10的16位灰度像素栅格
const [w, h] = [10, 10];
const pngPixels = new Uint16Array(w * h);
for (let x = 0; x < w; x++) {
  for (let y = 0; y < h; y++) {
    const i = x + y * w;
    pngPixels[i] = ((i / (w * h - 1)) * (2 ** 16 - 1)) | 0;
  }
}

// 将16位像素值转换为8位字节数组
const pngPixels8 = new Uint8Array(pngPixels.buffer);

// 如果是小端序,需要交换每对字节,使其变为大端序
if (endian === LITTLE_ENDIAN) {
  for (let i = 0, e = pngPixels8.length; i < e; i += 2) {
    let temp = pngPixels8[i];
    pngPixels8[i] = pngPixels8[i + 1];
    pngPixels8[i + 1] = temp;
  }
}

// 创建新的数组来存储带有过滤字节的扫描行数据
// 每个扫描行增加一个字节用于过滤类型 (0x00 表示无过滤)
const filtered = new Uint8Array(pngPixels8.length + h);

for (let y = 0; y < h; y++) {
  const sourceStartIndex = 2 * y * w; // 原始像素数据中当前扫描行的起始索引
  const destinationStartIndex = y * (2 * w + 1) + 1; // 目标数组中当前扫描行像素数据的起始索引 (跳过过滤字节)

  // 插入过滤字节 0x00 (表示无过滤)
  filtered[y * (2 * w + 1)] = 0x00; 

  // 将当前扫描行的像素数据复制到目标数组
  filtered.set(pngPixels8.subarray(sourceStartIndex, sourceStartIndex + 2 * w), destinationStartIndex);
}

// PNG文件头魔数
const magic = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);

// IHDR块:16位灰度图像
const IHDR = makeChunk(`IHDR`, [
  ...fourByte(w), // 宽度
  ...fourByte(h), // 高度
  16, // 位深: 16 bits
  0, // 颜色类型: 灰度
  0, // 压缩方法: 必须是0
  0, // 过滤方法: 必须是0 (标准自适应过滤)
  0, // 隔行扫描方法: 无隔行扫描
]);

// 压缩过滤后的像素数据并创建IDAT块
const IDAT = makeChunk(`IDAT`, zlib.deflateSync(filtered, { level: 9 }));

// 组装所有数据块并写入文件
const pngData = Buffer.concat([magic, IHDR, IDAT, makeChunk(`IEND`)]);
writeFileSync(`test-out.png`, pngData);

注意事项与总结

  • PNG规范的严谨性:手动创建PNG文件需要严格遵循PNG规范。即使是看似微小的细节,如过滤字节的缺失,也可能导致图像无法正确解析。
  • 字节序的重要性:对于16位或更高位深的像素数据,字节序是一个关键考虑因素。PNG强制使用大端序,因此在小端序系统上生成数据时必须进行转换。除了手动交换字节,也可以使用DataView对象更灵活地处理不同字节序的数据。
  • 过滤类型:本教程仅使用了过滤类型0(None)。PNG还支持其他过滤类型(Sub, Up, Average, Paeth),它们通过预测像素值来提高压缩效率。在实际应用中,可以根据图像内容选择合适的过滤类型。
  • CRC32校验:每个PNG数据块都包含一个CRC32校验码,用于验证数据完整性。确保正确计算和包含CRC32是生成有效PNG文件的基本要求。

通过本教程,您应该能够理解并解决在Node.js中手动创建16位灰度PNG图像时IDAT块过滤字节的问题。正确处理这些细节将确保您生成的PNG文件符合标准,并在各种图像查看器中正确显示。

相关专题

更多
js正则表达式
js正则表达式

php中文网为大家提供各种js正则表达式语法大全以及各种js正则表达式使用的方法,还有更多js正则表达式的相关文章、相关下载、相关课程,供大家免费下载体验。

510

2023.06.20

js获取当前时间
js获取当前时间

JS全称JavaScript,是一种具有函数优先的轻量级,解释型或即时编译型的编程语言;它是一种属于网络的高级脚本语言,主要用于Web,常用来为网页添加各式各样的动态功能。js怎么获取当前时间呢?php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

244

2023.07.28

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

278

2023.08.03

js是什么意思
js是什么意思

JS是JavaScript的缩写,它是一种广泛应用于网页开发的脚本语言。JavaScript是一种解释性的、基于对象和事件驱动的编程语言,通常用于为网页增加交互性和动态性。它可以在网页上实现复杂的功能和效果,如表单验证、页面元素操作、动画效果、数据交互等。

5299

2023.08.17

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

479

2023.09.01

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

212

2023.09.04

Js中concat和push的区别
Js中concat和push的区别

Js中concat和push的区别:1、concat用于将两个或多个数组合并成一个新数组,并返回这个新数组,而push用于向数组的末尾添加一个或多个元素,并返回修改后的数组的新长度;2、concat不会修改原始数组,是创建新的数组,而push会修改原数组,将新元素添加到原数组的末尾等等。本专题为大家提供concat和push相关的文章、下载、课程内容,供大家免费下载体验。

218

2023.09.14

js截取字符串的方法介绍
js截取字符串的方法介绍

JavaScript字符串截取方法,包括substring、slice、substr、charAt和split方法。这些方法可以根据具体需求,灵活地截取字符串的不同部分。在实际开发中,根据具体情况选择合适的方法进行字符串截取,能够提高代码的效率和可读性 。

218

2023.09.21

c++ 根号
c++ 根号

本专题整合了c++根号相关教程,阅读专题下面的文章了解更多详细内容。

17

2026.01.23

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
WEB前端教程【HTML5+CSS3+JS】
WEB前端教程【HTML5+CSS3+JS】

共101课时 | 8.5万人学习

JS进阶与BootStrap学习
JS进阶与BootStrap学习

共39课时 | 3.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号