v8.89.8

Saving the output image as a GIF

Browsers can load GIF images to the canvas but cannot output GIF images.

To work around this browser limiation we can use the postprocessImageBlob property on the createDefaultImageWriter function to convert images to different formats.

The third-party library gif.js can convert JPEGs and PNGs to GIFs, we'll use it in the postprocessImageBlob hook.

In the example below gif.js needs to be added using a script tag, see the gif.js documentation for more information.

<!DOCTYPE html>

<head>
    <link rel="stylesheet" href="./pintura.css" />
</head>

<img src="" alt="" />

<style>
    .pintura-editor {
        height: 600px;
    }
</style>

<div id="editor"></div>

<script type="module">
    import { appendDefaultEditor, processImage } from './pintura.js';

    const editor = appendDefaultEditor('#editor', {
        src: 'image.jpeg',
        imageWriter: {
            postprocessImageBlob: ({ blob }) =>
                new Promise((resolve) => {
                    // Load the output blob as an image so we can load it with gif.js
                    const image = new Image();
                    image.onload = () => {
                        // The image has loaded, let's create a GIF
                        const gif = new GIF();
                        gif.addFrame(image);
                        gif.on('finished', (gifBlob) => {
                            // clean up URL to our output image
                            URL.revokeObjectURL(blob);

                            // return the GIF to the editor
                            resolve(gifBlob);
                        });
                        gif.render();
                    };

                    // Convert the blob to a URL so we can load it as an image
                    image.src = URL.createObjectURL(blob);
                }),
        },
    });

    editor.on('process', (imageState) => {
        document.querySelector('img').src = URL.createObjectURL(
            imageState.dest
        );
    });
</script>

Editing Animated GIFs

We can take this a step further to enable editing animated GIFs. When editing animated GIFs we'll have to access each frame of the GIF, apply changes, and then generate a new GIF.

We'll also have to create a custom image processor.

To get the individual frames of the GIF we'll add omggif.js to our project.

<!DOCTYPE html>

<head>
    <link rel="stylesheet" href="./pintura.css" />
</head>

<img src="" alt="" />

<style>
    .pintura-editor {
        height: 600px;
    }
</style>

<div id="editor"></div>

<script type="module">
    import {
        appendDefaultEditor,
        processDefaultImage,
        createDefaultImageWriter,
    } from './pintura.js';

    const framesFromGIF = (blob) => {
        return new Promise(async (resolve) => {
            const ab = await blob.arrayBuffer();

            // use the GifReader supplied by omggif.js to get the frames
            const gifReader = new GifReader(new Uint8Array(ab));

            const frameCount = gifReader.numFrames();
            const frames = [];

            for (let i = 0; i < frameCount; i++) {
                const currentFrame = gifReader.frameInfo(i);

                const image = new ImageData(
                    currentFrame.width,
                    currentFrame.height
                );

                gifReader.decodeAndBlitFrameRGBA(i, image.data);

                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');
                canvas.width = image.width;
                canvas.height = image.height;
                ctx.putImageData(image, 0, 0);

                frames[i] = {
                    ...currentFrame,
                    canvas,
                };
            }

            resolve(frames);
        });
    };

    const gifFromFrames = (frames) => {
        return new Promise(async (resolve) => {
            // use GIF supplied by gif.js to create animated gif
            const gif = new GIF();

            // done
            gif.on('finished', resolve);

            // add frames
            frames.forEach((frame) => {
                gif.addFrame(frame.imageData, { delay: frame.delay * 10 });
            });

            gif.render();
        });
    };

    const editor = appendDefaultEditor('#editor', {
        src: 'image.jpeg',
        imageWriter: [
            // our custom imageWriter has only one step,
            // read the gif and pass it to the custom processor
            [
                async (state, options, onprogress) => {
                    const { src, imageState } = state;

                    // get all the frame data and info from the animated GIF
                    const frames = await framesFromGIF(src);

                    // apply our image transforms to each frame
                    for (const [index, frame] of frames.entries()) {
                        const { dest } = await processDefaultImage(
                            frame.canvas,
                            {
                                imageWriter: createDefaultImageWriter({
                                    format: 'imageData',
                                }),
                                imageState,
                            }
                        );

                        // set frame output image data
                        frame.imageData = dest;
                    }

                    // generate a new animated GIF and set it as output image
                    state.dest = await gifFromFrames(frames);

                    return state;
                },
                'generate-animated-gif',
            ],
        ],
    });

    editor.on('process', (imageState) => {
        document.querySelector('img').src = URL.createObjectURL(
            imageState.dest
        );
    });
</script>