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

FileSystem API触发SecurityError:批量图片转PDF保存异常排查

问题根源

你遇到的SecurityError是浏览器安全机制导致的:showSaveFilePicker必须在用户主动触发的手势上下文(比如点击、按键)的有效期内调用。当处理超过5张图片时,生成PDF的耗时太长,超出了浏览器允许的用户手势生命周期,等到执行savePDFFile时,已经脱离了用户手势的触发范围,所以触发了安全限制。

5张图片能正常工作,只是因为处理耗时短,还没超出手势有效期而已——这不是可靠的阈值,换性能差的设备,可能3张就会出问题。

解决方案

核心思路是先获取文件保存句柄,再处理PDF生成:把showSaveFilePicker的调用移到点击事件的最开头(用户手势刚触发、还没开始处理图片的时候),确保它在手势有效期内执行。之后再处理图片转换、PDF生成,最后用提前拿到的句柄写入文件。

修改后的代码示例
mainContainer.addEventListener('click', async function (event) {
    if (event.target.id === 'dl-button') {
        // 第一步:先在用户手势有效期内获取文件保存句柄
        let fileHandle;
        try {
            fileHandle = await window.showSaveFilePicker({
                suggestedName: 'images.pdf',
                types: [{
                    description: 'PDF document from captured slides',
                    accept: {'application/pdf': ['.pdf']}
                }],
            });
        } catch (err) {
            alert(err);
            return; // 用户取消选择或出错,直接终止流程
        }

        // 第二步:再处理图片和PDF生成
        const selectedImgs = getSelectedImages();
        let images = await getAllImageDataUrls(selectedImgs);
        await triggerImagesDownload(images, fileHandle);
    }
});

async function embedImage(pdfDoc, base64Image) {
    let base64ImagePart = base64Image.split(';base64,')[1]
    const isJPEG = base64ImagePart.startsWith("/9j/")
    const imageBytes = Uint8Array.from(atob(base64ImagePart), c => c.charCodeAt(0));
    if (isJPEG) {
        return await pdfDoc.embedJpg(imageBytes);
    } else {
        return await pdfDoc.embedPng(imageBytes);
    }
}

// 修改triggerImagesDownload,接收提前获取的fileHandle
async function triggerImagesDownload(base64Images, fileHandle) {
    // Create a new PDF document
    const pdfDoc = await PDFLib.PDFDocument.create();
    const embedImagesPromises = base64Images.map(base64Image => embedImage(pdfDoc, base64Image));
    const images = await Promise.all(embedImagesPromises);
    images.forEach(image => {
        const page = pdfDoc.addPage([image.width, image.height]);
        page.drawImage(image, {
            x: 0,
            y: 0,
            width: image.width,
            height: image.height,
        });
    });

    // Serialize the PDFDocument to bytes
    const pdfBytes = await pdfDoc.save();
    // 直接用提前拿到的fileHandle写入,不用再调用showSaveFilePicker
    await savePDFFile(new Blob([pdfBytes], {type: 'application/pdf'}), fileHandle);
    chrome.storage.local.set({currentView: "home"}, function() {
        window.location.reload();
    });
}

// 修改savePDFFile,接收传入的fileHandle
async function savePDFFile(pdfBlob, fileHandle) {
    try {
        const writableStream = await fileHandle.createWritable();
        await writableStream.write(pdfBlob);
        await writableStream.close();
        console.log('PDF saved successfully.');
    } catch (err) {
        alert(err)
    }
}
补充说明

浏览器对用户手势的有效期没有公开的固定时长,它基于操作连续性判断——只要异步操作链没被太长耗时打断,可能还能保留上下文,但一旦耗时超过隐性阈值,就会被判定为脱离用户手势。提前获取文件句柄是最可靠的方案,完全规避了这个限制。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 19:23:25