如何在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
相关产品推荐
相关产品推荐

