文本转图片 (text-to-image)

/**
 * 文本转图片
 * @param {Object} options - 配置项
 * @param {string} text - 文本
 * @param {number} [width=200] - 图片宽度
 * @param {number} [height=200] - 图片高度
 * @param {string} [backgroundColor='transparent'] - 背景色
 * @param {string} [fontFamily='Arial'] - 字体
 * @param {number} [fontSize=16] - 字体大小
 * @param {string} [fontColor='black'] - 字体颜色
 * @param {number} [lineHeight=1.2] - 行高
 * @param {boolean} [horizontalCenter=false] - 水平居中
 * @param {boolean} [verticalCenter=false] - 垂直居中
 * @param {number} [padding=10] - 边距
 * @returns {string} base64
 */
function textToImage(options) {
  const {
    text,
    width = 200,
    height = 200,
    backgroundColor = 'transparent',
    fontFamily = 'Arial',
    fontSize = 16,
    fontColor = 'black',
    lineHeight = 1.2,
    horizontalCenter = false,
    verticalCenter = false,
    padding = 10,
  } = options;

  const dpr = devicePixelRatio || 1;

  // const canvas = new OffscreenCanvas(width, height)

  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');

  canvas.width = width * dpr;
  canvas.height = height * dpr;
  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;

  ctx.scale(dpr, dpr);

  // 设置背景色
  ctx.fillStyle = backgroundColor;
  ctx.fillRect(0, 0, width, height);

  // 设置字体
  ctx.font = `${fontSize}px ${fontFamily}`;
  ctx.fillStyle = fontColor;

  const words = text.split(' ');
  let line = '';
  const lines = [];

  // 计算每行的最大宽度
  const maxWidth = width - padding * 2;

  for (const word of words) {
    const testLine = line + word + ' ';
    const metrics = ctx.measureText(testLine);

    // 如果超出最大宽度,则换行
    if (metrics.width > maxWidth) {
      lines.push(line);
      line = word + ' ';
    } else {
      line = testLine;
    }
  }
  lines.push(line);

  // 计算文本位置
  const drawableWidth = width - padding * 2;
  const drawableHeight = height - padding * 2;
  // const totalTextHeight = lines.length * fontSize * lineHeight;

  // 限制文本行数
  const maxLines = Math.floor(drawableHeight / (fontSize * lineHeight));
  const displayLines = lines.slice(0, maxLines);

  let startX = horizontalCenter ? width / 2 : padding;

  let startY = verticalCenter
    ? (height - displayLines.length * fontSize * lineHeight) / 2 + (fontSize * lineHeight) / 2
    : padding;

  // 设置文本对齐方式
  ctx.textAlign = horizontalCenter ? 'center' : 'left';
  ctx.textBaseline = verticalCenter ? 'middle' : 'top';

  // 绘制文本
  displayLines.forEach((line, index) => {
    let truncatedLine = line.trim();
    while (ctx.measureText(truncatedLine).width > drawableWidth && truncatedLine.length > 0) {
      truncatedLine = truncatedLine.slice(0, -1);
    }

    ctx.fillText(truncatedLine, startX, startY + index * fontSize * lineHeight);
  });

  queueMicrotask(() => {
    canvas.remove();
  });

  return canvas.toDataURL('image/png');

  // const blob = await canvas.convertToBlob();
  // return URL.createObjectURL(blob);
}
  • 使用
const base64 = textToImage({
  text: 'Hello, World! This is a best d
  width: 200,
  height: 200,
  backgroundColor: 'pink',
  fontSize: 24,
  lineHeight: 1.25,
  horizontalCenter: false,
  verticalCenter: false,
  padding: 10,
});
let img = new Image();
img.onload = () => {
  document.body.appendChild(img);
};
img.src = base64;

  • 方式二
<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
	<title>Text to Image</title>
	<style>
		img {
			width: 100px;
			height: 100px;
		}
	</style>
</head>

<body>
	<canvas id="canvas"></canvas>
	<script>
		function textToImage({
        text,
        font = '14px Arial', // 默认字体
        width = 200, // 默认画布宽度
        height = 200, // 默认画布高度
        backgroundColor = '#FFFFFF', // 默认背景色
        textColor = '#000000', // 默认文字颜色
        padding = 10, // 默认内边距
        align = 'left', // 默认水平居中
        valign = 'top', // 默认垂直居中
      }) {
        /** @type {HTMLCanvasElement} */
        const canvas = document.getElementById('canvas');
        const ctx = canvas.getContext('2d');

        const maxWidth = width - 2 * padding;
        const fontSize = parseInt(font.match(/\d+/)[0] ?? 14); // 字体大小
        const dpr = window.devicePixelRatio || 1; // 默认设备像素比

        // 根据 DPR 设置实际画布大小
        canvas.width = width * dpr;
        canvas.height = height * dpr;
        ctx.scale(dpr, dpr); // 缩放画布

        // 设置背景色
        ctx.fillStyle = backgroundColor;
        ctx.fillRect(0, 0, width, height);

        // 设置字体
        ctx.font = font;
        ctx.fillStyle = textColor;
        ctx.textBaseline = 'top'; // 设置文字基线

        // 计算最大宽度
        let lineWidth = maxWidth || width - 2 * padding;

        // 创建缓存 Map 存储非中文的单词宽度
        const wordCache = new Map();

        // 自动换行文本
        const lines = wrapText(ctx, text, lineWidth, wordCache);

        // 计算文本总高度
        let textHeight = lines.length * fontSize + (lines.length - 1) * 5; // 字体高度 + 行间距
        if (textHeight > height - 2 * padding) {
          // 如果文本高度超出,截断文本
          lines.length = Math.floor((height - 2 * padding) / (fontSize + 5));
          textHeight = lines.length * fontSize + (lines.length - 1) * 5;
        }

        // 计算文字的起始位置,支持水平和垂直居中
        const x = align === 'center' ? (width - lineWidth) / 2 : padding;
        const y = valign === 'middle' ? (height - textHeight) / 2 : padding;

        // 绘制文本
        for (let i = 0; i < lines.length; i++) {
          ctx.fillText(lines[i], x, y + i * (fontSize + 5));
        }
      }

      function wrapText(ctx, text, maxWidth, wordCache) {
        let lines = [];
        let currentLine = '';
        let words = text.split(' ');

        // 遍历每个单词
        for (let i = 0; i < words.length; i++) {
          const word = words[i];

          // 判断当前单词是否为中文
          const isChinese = /[\u4e00-\u9fa5]/.test(word);

          // 获取单词的宽度,优先从缓存获取
          let wordWidth;
          if (isChinese) {
            // 中文字符逐个测量
            wordWidth = ctx.measureText(word).width;
          } else {
            // 非中文字符从缓存获取
            if (wordCache.has(word)) {
              wordWidth = wordCache.get(word);
            } else {
              wordWidth = ctx.measureText(word).width;
              wordCache.set(word, wordWidth); // 缓存非中文单词的宽度
            }
          }

          // 检查当前行加上该单词后是否超出最大宽度
          if (ctx.measureText(currentLine).width + wordWidth > maxWidth && currentLine !== '') {
            lines.push(currentLine); // 当前行宽度超出,换行
            currentLine = word; // 新的一行从当前单词开始
          } else {
            currentLine += (currentLine ? ' ' : '') + word; // 当前行继续添加该单词
          }
        }

        // 最后一行文本
        if (currentLine) {
          lines.push(currentLine);
        }

        return lines;
      }

      // 示例调用
      textToImage({
        text: 'This is a long text example that will wrap when necessary. 中文的文本也可以处理。 This is a long text example that will wrap when necessary. 中文的文本也可以处理。 This is a long text example that will wrap when necessary. 中文的文本也可以处理。 This is a long text example that will wrap when necessary. 中文的文本也可以处理。',
        font: '14px Arial',
        width: 100,
        height: 100,
        backgroundColor: '#e0e0e0',
        textColor: '#333',
        padding: 10,
        // align: 'left',
        // valign: 'top',
      });
	</script>
</body>

</html>
posted @ 2025-01-21 22:59  _clai  阅读(1386)  评论(0)    收藏  举报