使用 fabirc.js 在服务端生成立体文本图片

因项目需要在后端生成文字的立体图片,虽然本人是 Java 开发,还是在 AI 的帮助下,实现了这个功能。

现有的项目是基于 fabric.js 在后端生成图片的,所以在此基础上添加了立体文字的生成功能。

本来 AI 提供的是一个基于 DIV 元素的实现方案,而且感觉效果比 fabric.js 的要好一些,但是在 Node.js 中不知道该如何去实现。

示例代码

相关代码整理了出来,详见代码仓库:3D Text

其主要逻辑部分如下:

/* POST endpoint for generating text images */
router.post('/generate-text', async function (req, res, next) {
  try {
    console.log('请求生成 3D 文字图片,参数:', req.body);
    const {
      text = '',
      color = '#ddd',
      fontFamily = 'KaiTi',
      fontStyle = 'normal',
      fontWeight = 'regular',
      fontSize = 40,
      layerCount = 12, // 3D 层数(越多越立体)
    } = req.body;
    // 校验参数
    if (!text) {
      next(createError(500, "文字不能为空"));
      return
    }

    // 处理 fontFamily 字符串
    const processedFontFamily = fontFamily.split(',')
      .map(font => font.trim())
      .map(font => `'${font}'`)
      .join(',');
    
    // 处理换行(fabric 的 Text 对象不直接支持\n,拆分成多行)
    const lines = text.split('\n');
    const lineHeight = fontSize * 1.2; // 行高
    const gradient = createGradient(color); // 创建渐变样式

    // 计算内边距(以防止部分字体下文本显示不全)
    // 显示不全是因为 canvas.width 只是根据平均文字宽度计算的,而是实际显示的宽度
    // 暂时没有找到方法获取到实际的宽度
    const padding = fontStyle === 'italic' ? fontSize * 0.3 : fontSize * 0.2;

    // 创建多层文字模拟 3D 厚度(从后到前绘制)
    const textGroups = [];
    // 逐行创建文字
    lines.forEach((line, lineIndex) => {
      // 将每一行文字的多个图层定义为一个组
      const lineGroup = new fabric.Group([], {
        left: 0,
        top: lineIndex * lineHeight,
      });
      for (let i = 0; i < layerCount; i++) {
        // 计算每层的偏移(模拟CSS的translateZ和opacity)
        const offset = i * 0.1; // 偏移量,控制3D厚度感
        const opacity = 0.8 - (i * 0.05); // 外层透明度降低,和CSS一致
        const textObj = new fabric.Text(line, {
          left: - offset, // X轴偏移
          top: lineIndex * lineHeight - offset, // Y轴偏移
          fontSize: fontSize,
          fontFamily: processedFontFamily,
          fontStyle: fontStyle,
          fontWeight: fontWeight,
          skewX: fontStyle === 'italic' ? -15 : 0, // 手动设置X轴倾斜,模拟斜体效果(数值越大倾斜越明显)
          opacity: opacity,
          fill: i === layerCount - 1 ? gradient : color, // 顶层用渐变,底层用深色增强立体感
          shadow: {
            color: 'rgba(0,0,0,0.2)',
            blur: 2,
            offsetX: 1,
            offsetY: 1
          } // 添加阴影效果
        });
        lineGroup.addWithUpdate(textObj);
      }
      textGroups.push(lineGroup);
    });

    // 计算整体宽高
    const width = textGroups.reduce((max, obj) => Math.max(max, obj.getScaledWidth()), 0) + padding * 2;
    const height = lineHeight * lines.length;

    // 创建画布
    const canvas = new fabric.StaticCanvas(null, { width, height });

    // 添加所有文字对象到画布
    textGroups.forEach(textGroup => canvas.add(textGroup));

    // 水平居中显示
    textGroups.forEach(group => {
      group.set({
        left: (width - group.getScaledWidth() - padding) / 2,
      });
    });
    
    // 渲染 canvas
    canvas.renderAll();

    const dataURL = canvas.toDataURL({
      format: 'png',
      quality: 1,
      multiplier: 4
    });

    const base64Data = dataURL.replace(/^data:image\/\w+;base64,/, '');
    const imgBuffer = Buffer.from(base64Data, 'base64');

    res.set('Content-Type', 'image/png');
    res.send(imgBuffer);
  } catch (e) {
    console.error('创建文字图片失败', e);
    next(createError(500, "创建文字图片失败"));
  }
});

// 创建渐变填充
function createGradient(baseColor = '#111111') {
  function hexToRgb(hex) {
    hex = hex.replace('#', '');
    if (hex.length === 3) {
      hex = hex.split('').map(h => h + h).join('');
    }
    const int = parseInt(hex, 16);
    return {
      r: (int >> 16) & 255,
      g: (int >> 8) & 255,
      b: int & 255
    };
  }

  function rgbToHex(r, g, b) {
    const toHex = n => n.toString(16).padStart(2, '0');
    return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
  }

  // 将基色向白色方向按比例提亮(percent 0-1)
  function lighten(hex, percent) {
    const { r, g, b } = hexToRgb(hex);
    const nr = Math.round(r + (255 - r) * percent);
    const ng = Math.round(g + (255 - g) * percent);
    const nb = Math.round(b + (255 - b) * percent);
    return rgbToHex(nr, ng, nb);
  }

  const startColor = lighten(baseColor, 0.25); // 25% 提亮,可调整

  const gradient = new fabric.Gradient({
    type: 'linear',
    coords: { x1: 0, y1: 0, x2: 1, y2: 1 }, // 135 度渐变方向
    colorStops: [
      { offset: 0, color: startColor },
      { offset: 1, color: baseColor }
    ]
  });
  return gradient;
}
  

图片效果

遗留问题

开发过程中也发现了一些问题:

  1. 文字的宽度无法精准的计算。
    特别是一些特殊字体本身就有一定的倾斜,如果仅使用本身的宽度,会导致生成的文字显示不全。
    还有一些字体在一些字母组合下会有相互重叠的部分,也会导致获取到的宽度和实际有差距。
    暂时的解决方案是在文字整体的左右两边增加一些边距。虽然不能完全避免文字显示不全的情况,而且有可能会导致文字两边有比较大的空白,但是在大多数情况下效果还可以。
  2. 暂时仅支持 Node.js 16.x。
    更高的版本下安装 canvas 依赖时会报错。不知道是环境的问题?还是别的什么原因导致的?