Creating zip file from image blobs on a content script of firefox extension

I’m developing a browser extension that works on Firefox and Chrome, one of its features is to download a zip file with images (webp or png) and I’m using JSZip to create the zip file, but it does not works on Firefox, but it does on Chrome.

This code is running on a content script.

public async getNomiZipAlbumById({ id, type }: GetNomiAlbumProps) {
try {
    const NomiInfo = await this.findById(id);
    if (!NomiInfo) throw new Error(`Nomi not found (${id})`);

    const selfiesInfo = await this.findSelfiesById(NomiInfo.id);

    if (selfiesInfo.length === 0) {
        throw new Error(
            `${Nomi.name} selfies are zero (${NomiInfo.id})`
        );
    }

    const zip = new JSZip();

    for (let j = 0; j < selfiesInfo.length; j++) {
        try {
            const selfie = selfiesInfo[j];
            const url = `${env.apiNomiUrl}/nomis/${NomiInfo.id}/selfies/${selfie.id}.${type}`;

            const data = await fetch(url);

            if (!data.ok)
                throw new Error(
                    `Failed to fetch ${url}: ${data.statusText}`
                );

            const blob = await data.blob();

            console.log("Blob size:", blob.size);
            console.log("Blob type:", blob.type);

            if (blob.size === 0) {
                throw new Error(`Empty blob received from ${url}`);
            }

            const arrayBuffer = await blob.arrayBuffer(); // Convert to ArrayBuffer


            zip.file(`image_${j}_${selfie.id}.${type}`, arrayBuffer);
        } catch (error) {
            console.log("Error adding img to zip", error);
            continue;
        }
    }

    const zipFile = await zip.generateAsync({ type: "blob" });

    console.log(zipFile);

    if (zipFile.size === 0) {
        throw new Error(
            `${Nomi.name} zip file is empty (${NomiInfo.id})`
        );
    }

    return zipFile;
} catch (error) {
    console.log("Error generating zip", error);
    return null;
}

}

Error: Can’t read the data of ‘image_0_d4d66a60a6563b7ffcec5ed6053bdecfd219ec1c2837ec0d0e932aed49aec696.webp’. Is it in a supported JavaScript type (String, Blob, ArrayBuffer, etc) ?

Firefox logs

I have tried passing just the blob, the blob as a promise, array buffer as a promise and this code I got from chatGPT

const reader = new FileReader();

reader.onload = function (event) {
    const arrayBuffer = event.target?.result;

    if (arrayBuffer) {
        zip.file(
            `image_${j}_${selfie.id}.${type}`,
            arrayBuffer
        );
    }
};

reader.readAsArrayBuffer(blob);

The issue you’re facing with Firefox is likely related to how JSZip handles the data you’re passing to it. While Chrome can handle ArrayBuffer or Blob types directly, Firefox may have trouble when using these types in specific contexts, such as when reading or writing files to a zip archive.

There are a few things you can try to resolve this issue in Firefox:

1. Ensure the Correct Data Type for JSZip

JSZip should be able to handle Blob or ArrayBuffer without issues, but it’s worth ensuring that the ArrayBuffer is being correctly passed to JSZip. In your case, the error message suggests that the image data is not being read correctly into the zip file.

Since you’re using blob.arrayBuffer() to convert the Blob into an ArrayBuffer, this should work, but let’s make sure the data is passed correctly.

Try this refactor:

public async getNomiZipAlbumById({ id, type }: GetNomiAlbumProps) {
    try {
        const NomiInfo = await this.findById(id);
        if (!NomiInfo) throw new Error(`Nomi not found (${id})`);

        const selfiesInfo = await this.findSelfiesById(NomiInfo.id);

        if (selfiesInfo.length === 0) {
            throw new Error(`${NomiInfo.name} selfies are zero (${NomiInfo.id})`);
        }

        const zip = new JSZip();

        for (let j = 0; j < selfiesInfo.length; j++) {
            try {
                const selfie = selfiesInfo[j];
                const url = `${env.apiNomiUrl}/nomis/${NomiInfo.id}/selfies/${selfie.id}.${type}`;

                const data = await fetch(url);

                if (!data.ok)
                    throw new Error(`Failed to fetch ${url}: ${data.statusText}`);

                const blob = await data.blob();

                console.log("Blob size:", blob.size);
                console.log("Blob type:", blob.type);

                if (blob.size === 0) {
                    throw new Error(`Empty blob received from ${url}`);
                }

                const arrayBuffer = await blob.arrayBuffer(); // Convert to ArrayBuffer

                zip.file(`image_${j}_${selfie.id}.${type}`, arrayBuffer);
            } catch (error) {
                console.log("Error adding img to zip", error);
                continue;
            }
        }

        const zipFile = await zip.generateAsync({ type: "blob" });

        console.log(zipFile);

        if (zipFile.size === 0) {
            throw new Error(`${NomiInfo.name} zip file is empty (${NomiInfo.id})`);
        }

        return zipFile;
    } catch (error) {
        console.log("Error generating zip", error);
        return null;
    }
}

Explanation:

  1. ArrayBuffer Handling: You are correctly using await blob.arrayBuffer(). This is the preferred way to convert a Blob to an ArrayBuffer, which can then be added to the zip file. This should be compatible across both Chrome and Firefox.
  2. Avoid FileReader for ArrayBuffer: You don’t need to use FileReader here, since Blob has a built-in method arrayBuffer() to convert it directly to an ArrayBuffer.
  3. Ensure Correct Handling of Blobs: The zip.file() method can accept both Blob and ArrayBuffer directly. Just make sure the data passed to zip.file() is an ArrayBuffer or a valid Blob.

2. Debugging in Firefox

If you’re still encountering the issue in Firefox after these changes, there could be other reasons why the file reading is failing in Firefox. Here are some debugging steps:

  • Verify Data Type: In the zip.file() method, log the type of data you’re passing:
console.log("ArrayBuffer type:", arrayBuffer.constructor.name);
zip.file(`image_${j}_${selfie.id}.${type}`, arrayBuffer);
  • Check Firefox Compatibility: Ensure that you are not encountering a browser-specific limitation. Since you’re running this code as part of a browser extension, ensure the permissions and context in which the content script is executed are correct for handling the zip operation and fetching external resources.

3. Alternative Approach: Use Base64 Encoding

If you’re still having trouble with ArrayBuffer, you can try passing the image data as a base64 string, which is another format that JSZip supports.

const base64Data = await blobToBase64(blob); // Convert blob to base64 string
zip.file(`image_${j}_${selfie.id}.${type}`, base64Data, { base64: true });

You can use this helper function to convert the Blob to a base64 string:

function blobToBase64(blob: Blob): Promise<string> {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onloadend = () => resolve(reader.result as string);
        reader.onerror = reject;
        reader.readAsDataURL(blob); // This converts the Blob to a data URL
    });
}

This converts the Blob to a base64 string and adds it to the zip file as base64-encoded data. Note that while base64 strings can increase the size of the data slightly, they should work across both Chrome and Firefox.

Conclusion:

  • First, try sticking with arrayBuffer() since it should work correctly across both Chrome and Firefox.
  • If the issue persists in Firefox, try switching to base64 encoding using the blobToBase64() helper method.
  • Double-check the permissions and context of the content script to ensure that there are no issues with access or data handling.