You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在C#/JS中提取PDF拼接图像并合并拆分的子图像?

解决PDF拼接图像提取后拆分的问题

核心思路:利用PDF变换矩阵定位子图

PDF中拼接的图像本质是多个图像XObject通过**变换矩阵(Transform Matrix)**精确排列在页面上的,不需要依赖像素检测,直接解析变换矩阵计算子图的绝对位置,就能准确合并。

具体实现步骤

1. 解析子图的变换矩阵与边界框

先为每个提取的子图计算其在PDF页面上的绝对边界框(bounding box),这是合并的基础。修改原有的getPageImages函数,添加变换矩阵解析和边界框计算逻辑:

// 辅助函数:应用PDF变换矩阵计算点的绝对位置
function applyTransform(matrix: number[], x: number, y: number): {x: number, y: number} {
  const [a, b, c, d, e, f] = matrix;
  // PDF变换矩阵公式:x' = a*x + c*y + e; y' = b*x + d*y + f
  return {
    x: a * x + c * y + e,
    y: b * x + d * y + f
  };
}

// 更新getPageImages中的图像处理逻辑
const getPageImages = (page: number, documentUrl: string) => {
  loaded.value = true;
  pdf.value.promise.then(async (document: any) => {
    const pageProxy = await document.getPage(page);
    const ops = await pageProxy.getOperatorList();
    const objs: Array<{ type: string; transform: any | null; imageId: string }> = [];
    
    for (let i = 0; i < ops.fnArray.length; i++) {
      if (
        ops.fnArray[i] === PDFJS.OPS.paintImageXObject ||
        ops.fnArray[i] === PDFJS.OPS.transform
      ) {
        const argsVals = ops.argsArray[i];
        objs.push({
          type: ops.fnArray[i] === PDFJS.OPS.paintImageXObject ? 'image' : 'transform',
          transform: ops.fnArray[i] === PDFJS.OPS.transform ? argsVals : null,
          imageId: argsVals[0],
        });
      }
    }

    // 改用Promise.all确保所有图像加载完成
    await Promise.all(objs.map(async (val, i) => {
      if (val.type === 'image') {
        const imageKey = val.imageId;
        return new Promise<void>((resolve) => {
          pageProxy.objs.get(imageKey, async (obj: any) => {
            const bitmap = await createImageBitmap(obj.bitmap);
            const ocanvas = new OffscreenCanvas(bitmap.width, bitmap.height);
            const ctx = ocanvas.getContext("bitmaprenderer");
            if (ctx) {
              ctx.transferFromImageBitmap(bitmap);
              const blob = await ocanvas.convertToBlob({ type: "image/png" });
              const blobUrl = URL.createObjectURL(blob);

              // 获取当前图像的变换矩阵,默认使用单位矩阵
              const transformMatrix = (i > 0 && objs[i-1].type === 'transform') 
                ? objs[i-1].transform 
                : [1, 0, 0, 1, 0, 0];

              // 计算图像四个角的绝对位置
              const topLeft = applyTransform(transformMatrix, 0, 0);
              const topRight = applyTransform(transformMatrix, bitmap.width, 0);
              const bottomLeft = applyTransform(transformMatrix, 0, bitmap.height);
              const bottomRight = applyTransform(transformMatrix, bitmap.width, bitmap.height);

              // 生成边界框信息
              const bbox = {
                xMin: Math.min(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x),
                yMin: Math.min(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y),
                xMax: Math.max(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x),
                yMax: Math.max(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y),
              };

              images.value.push({
                url: blobUrl,
                transform: transformMatrix,
                page,
                width: bitmap.width,
                height: bitmap.height,
                bbox // 新增边界框属性
              });
            }
            resolve();
          });
        });
      }
    }));

    // 所有图像加载完成后执行合并
    const mergedImages = await mergeSplitImages(images.value);
    // 替换为合并后的图像
    images.value = mergedImages.map(url => ({
      url,
      transform: null,
      page,
      width: 0,
      height: 0,
      bbox: { xMin: 0, yMin: 0, xMax: 0, yMax: 0 }
    }));
  })
};

2. 实现基于边界框的合并算法

编写合并函数,通过检测子图边界框的贴合/重叠关系分组,再绘制到画布生成完整图像:

// 合并拆分的子图
async function mergeSplitImages(images: typeof images.value) {
  const mergedGroups: typeof images.value[] = [];
  const tolerance = 1.5; // 浮点误差容忍度,根据PDF精度调整

  // 第一步:将子图按边界框关系分组
  images.forEach(img => {
    let addedToGroup = false;
    // 检查每个已存在的分组
    for (const group of mergedGroups) {
      // 判断当前图像是否与组内任意图像有贴合边或重叠
      const isRelated = group.some(gImg => {
        // 左右边贴合
        const leftAlign = Math.abs(img.bbox.xMin - gImg.bbox.xMax) <= tolerance;
        const rightAlign = Math.abs(img.bbox.xMax - gImg.bbox.xMin) <= tolerance;
        // 上下边贴合
        const topAlign = Math.abs(img.bbox.yMin - gImg.bbox.yMax) <= tolerance;
        const bottomAlign = Math.abs(img.bbox.yMax - gImg.bbox.yMin) <= tolerance;
        // 区域重叠(处理部分覆盖的情况)
        const overlaps = !(
          img.bbox.xMax < gImg.bbox.xMin - tolerance ||
          img.bbox.xMin > gImg.bbox.xMax + tolerance ||
          img.bbox.yMax < gImg.bbox.yMin - tolerance ||
          img.bbox.yMin > gImg.bbox.yMax + tolerance
        );
        return leftAlign || rightAlign || topAlign || bottomAlign || overlaps;
      });

      if (isRelated) {
        group.push(img);
        addedToGroup = true;
        break;
      }
    }

    if (!addedToGroup) {
      mergedGroups.push([img]);
    }
  });

  // 第二步:为每个分组生成合并后的图像
  return Promise.all(mergedGroups.map(async group => {
    // 计算分组的总边界框
    const xMin = Math.min(...group.map(img => img.bbox.xMin));
    const yMin = Math.min(...group.map(img => img.bbox.yMin));
    const xMax = Math.max(...group.map(img => img.bbox.xMax));
    const yMax = Math.max(...group.map(img => img.bbox.yMax));
    const totalWidth = xMax - xMin;
    const totalHeight = yMax - yMin;

    if (totalWidth <= 0 || totalHeight <= 0) return null;

    const canvas = new OffscreenCanvas(totalWidth, totalHeight);
    const ctx = canvas.getContext('2d');
    if (!ctx) return null;

    // 绘制每个子图到对应位置
    await Promise.all(group.map(async img => {
      const imgBlob = await fetch(img.url).then(res => res.blob());
      const imgBitmap = await createImageBitmap(imgBlob);
      const [a, b, c, d, e, f] = img.transform;
      // 调整变换矩阵,将子图相对于总边界框定位
      ctx.setTransform(a, b, c, d, e - xMin, f - yMin);
      ctx.drawImage(imgBitmap, 0, 0);
      ctx.setTransform(1, 0, 0, 1, 0, 0); // 重置变换
    }));

    const mergedBlob = await canvas.convertToBlob({ type: 'image/png' });
    return URL.createObjectURL(mergedBlob);
  })).then(results => results.filter(img => img !== null));
}

针对斜向/异形拆分的补充说明

  • 对于旋转、斜切的子图,上述方法通过变换矩阵直接处理,无需额外逻辑,setTransform会自动应用旋转、缩放和斜切效果。
  • 对于极少数异形拼接(非矩形子图),可以在分组时补充边缘像素相似度检测:提取子图边缘的像素,对比相邻子图的边缘像素颜色,相似度超过阈值则判定为同一组。但这种场景在PDF中非常罕见,优先使用边界框+变换矩阵的方案即可。

内容的提问来源于stack exchange,提问作者Adrian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 15:05:56