怎么在终端控制台打印彩色图片?

136 阅读2分钟

说在前面

在终端控制台打印字符图案我们见多了,那你有没有想过在控制台打印彩色图片呢?今天我们一起来看看怎么在终端控制台打印彩色图片。

效果展示

代码实现

实现思路

先将图片转为颜色点阵,再以彩色点阵的形式在终端中打印出来。

颜色转换

function hexToAnsi(hex) {
  // 十六进制转 RGB
  const r = parseInt(hex.slice(1, 3), 16);
  const g = parseInt(hex.slice(3, 5), 16);
  const b = parseInt(hex.slice(5, 7), 16);

  // 转换为 ANSI 256色
  return `\x1b[48;2;${r};${g};${b}m`;
}

将十六进制颜色值转换为 ANSI 转义序列,以便在终端中实现彩色输出。它首先从十六进制字符串中提取红(r)、绿(g)、蓝(b)分量,然后构建 ANSI 转义序列,该序列可以在支持 ANSI 颜色代码的终端中设置背景颜色。

图片转为颜色矩阵

调整图片大小

先对图片进行尺寸调整,将高度和宽度分别限制在 50 以内,同时保持图片的原始宽高比,这样更好在终端控制台呈现。

//将图片大小框高调整到不大于50,保存原有框高比
let originalInfo = await sharp(imagePath).metadata();
if (originalInfo.height > 50) {
  const newHeight = 50;
  const newWidth = Math.floor(
    (originalInfo.width / originalInfo.height) * newHeight
  );
  await sharp(imagePath)
   .resize(newWidth, newHeight)
   .toFile("temp/output.jpg");
  imagePath = "temp/output.jpg";
}
//将图片大小框宽调整到不大于50,保存原有框宽比
originalInfo = await sharp(imagePath).metadata();
if (originalInfo.width > 50) {
  const newWidth = 50;
  const newHeight = Math.floor(
    (originalInfo.height / originalInfo.width) * newWidth
  );
  await sharp(imagePath).resize(newWidth, newHeight).toFile("temp/tmp.jpg");
  imagePath = "temp/tmp.jpg";
}

读取图片数据

根据通道数(通常为 3 表示 RGB 或 4 表示 RGBA)遍历每个像素点,提取红、绿、蓝分量,并将其转换为十六进制颜色值,最终构建一个二维数组(颜色矩阵)来表示图片的颜色点阵。

const { data, info } = await sharp(imagePath)
.raw()
.toBuffer({ resolveWithObject: true });
const matrix = [];
const channels = info.channels; // 通道数(RGB=3, RGBA=4)
for (let y = 0; y < info.height; y++) {
  const row = [];
  for (let x = 0; x < info.width; x++) {
    const idx = (y * info.width + x) * channels;

    // 提取RGB(A)值
    const r = data[idx];
    const g = data[idx + 1];
    const b = data[idx + 2];

    // 转换为十六进制(忽略透明度)
    const hex = `#${[r, g, b]
     .map((v) => v.toString(16).padStart(2, "0"))
     .join("")}`;

    row.push(hex);
  }
  matrix.push(row);
}

在终端绘制彩色点阵

遍历前面获取到的颜色矩阵,通过 hexToAnsi 函数将十六进制颜色转换为 ANSI 背景色,将其打印到终端控制台上。

function drawInConsole(matrix) {
  const scaledWidth = matrix[0].length;
  const scaledHeight = matrix.length;
  let output = "";
  for (let y = 0; y < scaledHeight; y++) {
    for (let x = 0; x < scaledWidth; x++) {
      const hex = matrix[y][x];
      // 转换为ANSI背景色
      const bgColor = hexToAnsi(hex) + " ·\x1b[0m"; 
      output += bgColor;
    }
    output += "\n";
  }
  // 清屏并输出
  stdout.write("\x1Bc"); // 清空控制台
  stdout.write(output);
}

完整代码

const sharp = require("sharp");
const fs = require("fs");
const { stdout } = require("process");

//创建临时文件夹
function createTempFolder() {
  if (!fs.existsSync("temp")) {
    fs.mkdirSync("temp");
  }
}

function hexToAnsi(hex) {
  // 十六进制转 RGB
  const r = parseInt(hex.slice(1, 3), 16);
  const g = parseInt(hex.slice(3, 5), 16);
  const b = parseInt(hex.slice(5, 7), 16);

  // 转换为 ANSI 256色
  return `\x1b[48;2;${r};${g};${b}m`;
}

/**
 * 将图片转换为颜色点阵
 * @param {string} imagePath 图片路径
 * @returns {Promise<Array<Array<string>>>} 二维数组点阵(十六进制颜色值)
 */
async function imageToColorMatrix(imagePath) {
  await createTempFolder();
  //将图片大小框高调整到不大于50,保存原有框高比
  let originalInfo = await sharp(imagePath).metadata();
  if (originalInfo.height > 50) {
    const newHeight = 50;
    const newWidth = Math.floor(
      (originalInfo.width / originalInfo.height) * newHeight
    );
    await sharp(imagePath)
      .resize(newWidth, newHeight)
      .toFile("temp/output.jpg");
    imagePath = "temp/output.jpg";
  }
  //将图片大小框宽调整到不大于50,保存原有框宽比
  originalInfo = await sharp(imagePath).metadata();
  if (originalInfo.width > 50) {
    const newWidth = 50;
    const newHeight = Math.floor(
      (originalInfo.height / originalInfo.width) * newWidth
    );
    await sharp(imagePath).resize(newWidth, newHeight).toFile("temp/tmp.jpg");
    imagePath = "temp/tmp.jpg";
  }

  // 读取图片原始数据
  const { data, info } = await sharp(imagePath)
    .raw()
    .toBuffer({ resolveWithObject: true });

  // 创建二维数组
  const matrix = [];
  const channels = info.channels; // 通道数(RGB=3, RGBA=4)

  for (let y = 0; y < info.height; y++) {
    const row = [];
    for (let x = 0; x < info.width; x++) {
      const idx = (y * info.width + x) * channels;

      // 提取RGB(A)值
      const r = data[idx];
      const g = data[idx + 1];
      const b = data[idx + 2];

      // 转换为十六进制(忽略透明度)
      const hex = `#${[r, g, b]
        .map((v) => v.toString(16).padStart(2, "0"))
        .join("")}`;

      row.push(hex);
    }
    matrix.push(row);
  }

  return matrix;
}

/**
 * 在终端绘制彩色点阵
 * @param {Array<Array<string>>} matrix 颜色矩阵
 */
function drawInConsole(matrix) {
  const scaledWidth = matrix[0].length;
  const scaledHeight = matrix.length;
  let output = "";
  for (let y = 0; y < scaledHeight; y++) {
    for (let x = 0; x < scaledWidth; x++) {
      const hex = matrix[y][x];
      const bgColor = hexToAnsi(hex) + " ·\x1b[0m";
      output += bgColor;
    }
    output += "\n";
  }
  stdout.write("\x1Bc");
  stdout.write(output);
}

imageToColorMatrix("images/哪吒.jpg")
  .then((matrix) => {
    fs.writeFileSync("output.txt", JSON.stringify(matrix, null, 2));
    drawInConsole(matrix);
  })
  .catch(console.error);

公众号

关注公众号『 前端也能这么有趣 』,获取更多有趣内容。

发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。