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

JavaScript:解决循环中Promise未完成致PDF剪辑顺序错乱问题

问题:PDF片段生成新PDF时顺序错乱

我正在开发一款Web应用,支持用户上传PDF文件,通过横向和纵向选择器截取PDF片段;用户可重新排列所选片段的顺序,应用将按照该顺序生成新的PDF文件。

目前遇到的问题是:应用生成的新PDF中,片段的顺序频繁出现错乱。

我推测问题出在代码逻辑上:循环迭代时,读取页面并粘贴片段的Promise尚未完成,程序就进入了下一次循环。我尝试将for循环放入async函数中,并在Promise前使用await,但未能解决问题。

请问我该如何修复这个问题?

原代码

var currentWriteHeight = 0; //used to determine how much is on a page in the new pdf before a new one must be created

// open the pdf file to be clipped
const fileName = '../static/upload/' + fileData['fileName'];
pdfjsLib.getDocument(fileName).then((pdfRead) => {

    // create new pdf file
    var hSize = fileData['maxPageWidth'];
    var vSize = fileData['maxPageHeight'];
    const pdfWrite = new jsPDF({unit: 'px',format: [hSize, vSize]});

    // this is the array that states what order the clips are in in the new file
    const positionArray = JSON.parse(fileData['positionArray']);

    for (const x in positionArray) {

        //get data for current clip
        const currentClip = clipData[positionArray[x]];
        const currentPage = currentClip['pageNumber'];

        // put current page in a canvas
        pdfRead.getPage(currentPage).then((page) => {
            const canvas1 = document.createElement('canvas');
            const ctx = canvas1.getContext("2d", { willReadFrequently: true });
            const viewport = page.getViewport(1);
            canvas1.width = viewport.width;
            canvas1.height = viewport.height;

            page.render({
                canvasContext: ctx,
                viewport: viewport
            }).then((rendered) => {

                // crop page down to dimensions specified in clip data
                const minY = currentClip['minY'] * (page.view[3] - page.view[1]) / 600;
                const maxY = currentClip['maxY'] * (page.view[3] - page.view[1]) / 600;
                let minX = 0; 
                let maxX = (page.view[2] - page.view[0]);
                if (currentClip['minX'] != null && currentClip['maxX'] != null) {
                minX = currentClip['minX'] * (page.view[3] - page.view[1]) / 600;
                maxX = currentClip['maxX'] * (page.view[3] - page.view[1]) / 600;
                }
                const clipImgData = ctx.getImageData(minX, minY, (maxX - minX), (maxY - minY));
                canvas1.width = (maxX - minX);
                canvas1.height = (maxY - minY);
                ctx.putImageData(clipImgData, 0, 0);

                // if current image would put total over page height, make new page
                if ((currentWriteHeight + canvas1.height) > vSize) {
                    pdfWrite.addPage({format: [hSize, vSize]});
                    currentWriteHeight = 0;
                }

                // add image to page
                pdfWrite.addImage(canvas1, 'PNG', 0, currentWriteHeight, canvas1.width, canvas1.height);
                currentWriteHeight += canvas1.height;

                // if this is the last image, show the new pdf file
                if (x == positionArray.length - 1) {
                    window.open(pdfWrite.output('bloburl'), '_blank');
                }

            });

            canvas1.remove();

        });

    }

});

示例数据

fileData = {
    fileName: "48 Moonrunner.pdf",
    maxPageHeight: 509.28,
    maxPageWidth: 315.75359999999995,
    positionArray: "[0, 19, 20, 2, 3, 22, 21, 14, 15, 16, 17, 11, 9, 4, 5, 8, 12, 13, 10, 6, 18, 7, 1]"
}

clipData = [
    {pageNumber: 1, minY: 0, maxY: 600, minX: 413, maxX: 773},
    {pageNumber: 19, minY: 49, maxY: 296, minX: 372, maxX: 721},
    {pageNumber: 20, minY: 49, maxY: 360, minX: 0, maxX: 372},
    {pageNumber: 27, minY: 438, maxY: 555, minX: 372, maxX: 721},
    {pageNumber: 32, minY: 310, maxY: 367, minX: 364, maxX: 715},
    {pageNumber: 33, minY: 48, maxY: 107, minX: 0, maxX: 364},
    {pageNumber: 36, minY: 324, maxY: 544, minX: 0, maxX: 364},
    {pageNumber: 39, minY: 296, maxY: 558, minX: 364, maxX: 721},
    {pageNumber: 43, minY: 286, maxY: 417, minX: 0, maxX: 364},
    {pageNumber: 59, minY: 59, maxY: 283, minX: 364, maxX: 715},
    {pageNumber: 83, minY: 312, maxY: 545, minX: 364, maxX: 715},
    {pageNumber: 87, minY: 222, maxY: 552, minX: 364, maxX: 721},
    {pageNumber: 91, minY: 0, maxY: 600, minX: 0, maxX: 364},
    {pageNumber: 91, minY: 51, maxY: 340, minX: 364, maxX: 715},
    {pageNumber: 96, minY: 158, maxY: 382, minX: 0, maxX: 364},
    {pageNumber: 104, minY: 53, maxY: 273, minX: 364, maxX: 715},
    {pageNumber: 107, minY: 0, maxY: 600, minX: 0, maxX: 364},
    {pageNumber: 107, minY: 53, maxY: 444, minX: 364, maxX: 721},
    {pageNumber: 112, minY: 331, maxY: 552, minX: 0, maxX: 364},
    {pageNumber: 120, minY: 0, maxY: 600, minX: 0, maxX: 364},
    {pageNumber: 120, minY: 54, maxY: 446, minX: 364, maxX: 721},
    {pageNumber: 123, minY: 284, maxY: 550, minX: 0, maxX: 364},
    {pageNumber: 127, minY: 56, maxY: 324, minX: 364, maxX: 721}
]

解决方案

问题根源

循环里嵌套的多层Promise(pdfRead.getPage().then()、page.render().then())不会被for...in循环等待,不同片段的页面加载、渲染速度差异会导致先完成的片段被优先添加到新PDF,完全打乱positionArray指定的顺序。之前尝试的async/await未生效,是因为没有将整个循环逻辑改成异步等待,仅简单包裹async函数却未正确await每个异步操作。

修复步骤

  1. 将pdfjsLib.getDocument()的回调改为async函数,支持内部使用await;
  2. 替换for...in为for...of循环(更适合数组遍历,避免键值问题),确保每次循环都等待所有异步操作完成后再进入下一次;
  3. 将嵌套的.then()改为await语法,保证每个片段的加载、渲染、裁剪、添加流程按顺序执行;
  4. 所有片段处理完成后统一打开新PDF,取消依赖循环索引判断最后一个片段的逻辑。

修复后的代码

var currentWriteHeight = 0; // 记录当前页面已占用高度,超过阈值则新建页面

const fileName = '../static/upload/' + fileData['fileName'];
// 改用async/await处理PDF加载
pdfjsLib.getDocument(fileName).then(async (pdfRead) => {
    const hSize = fileData['maxPageWidth'];
    const vSize = fileData['maxPageHeight'];
    const pdfWrite = new jsPDF({unit: 'px', format: [hSize, vSize]});

    const positionArray = JSON.parse(fileData['positionArray']);

    // 用for...of循环,确保按顺序等待每个片段处理完成
    for (const idx of positionArray) {
        const currentClip = clipData[idx];
        const currentPage = currentClip['pageNumber'];

        // await页面加载完成
        const page = await pdfRead.getPage(currentPage);
        const canvas1 = document.createElement('canvas');
        const ctx = canvas1.getContext("2d", { willReadFrequently: true });
        const viewport = page.getViewport(1);
        canvas1.width = viewport.width;
        canvas1.height = viewport.height;

        // await页面渲染完成(注意pdfjs的render需取.promise属性)
        await page.render({
            canvasContext: ctx,
            viewport: viewport
        }).promise;

        // 裁剪片段
        const pageHeight = page.view[3] - page.view[1];
        const minY = currentClip['minY'] * pageHeight / 600;
        const maxY = currentClip['maxY'] * pageHeight / 600;
        let minX = 0; 
        let maxX = page.view[2] - page.view[0];
        if (currentClip['minX'] != null && currentClip['maxX'] != null) {
            minX = currentClip['minX'] * pageHeight / 600;
            maxX = currentClip['maxX'] * pageHeight / 600;
        }
        const clipImgData = ctx.getImageData(minX, minY, (maxX - minX), (maxY - minY));
        canvas1.width = (maxX - minX);
        canvas1.height = (maxY - minY);
        ctx.putImageData(clipImgData, 0, 0);

        // 判断是否需要新建页面
        if ((currentWriteHeight + canvas1.height) > vSize) {
            pdfWrite.addPage({format: [hSize, vSize]});
            currentWriteHeight = 0;
        }

        // 添加片段到新PDF
        pdfWrite.addImage(canvas1, 'PNG', 0, currentWriteHeight, canvas1.width, canvas1.height);
        currentWriteHeight += canvas1.height;

        canvas1.remove();
    }

    // 所有片段处理完成后再打开新PDF
    window.open(pdfWrite.output('bloburl'), '_blank');
});

关键改动说明

  • 将pdfRead.getPage().then()改为await pdfRead.getPage(),等待页面加载完成;
  • 将page.render().then()改为await page.render().promise,正确等待页面渲染完成;
  • 用for...of遍历positionArray,确保每个片段的处理流程完全结束后再进入下一个;
  • 移除原代码中依赖索引判断最后一个片段的逻辑,改为循环结束后统一打开新PDF,避免提前触发。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 19:07:00