v8.77.0

Setting up Pintura Image Editor with React Dropzone

For a quick start use the React Dropzone example project example project as a guideline.

Installing the module

To set up the editor with React Dropzone we use the vanilla JavaScript version of Pintura Image Editor.

Using private npm

In our project root directory we create a file called .npmrc and copy the snippet below to the file. Then we replace PQINA_NPM_KEY with our private npm key as displayed on the pqina customer portal.

@pqina:registry=https://npm.pqina.nl/
//npm.pqina.nl/:_authToken=PQINA_NPM_KEY

Now we can install the pintura module like shown below.

npm install --save @pqina/pintura

Using local modules

Instead of installing from the private npm we can create a local_modules folder inside our project root directory. We then copy paste the pintura module from the product package to this folder.

We can now install the pintura module like shown below.

npm install --save ./local_modules/pintura

Default implementation example

Before continuing it's highly recommended to read the JavaScript installation guide as it clarifies important internal functionality of the image editor.

In the default example below we'll use the openDefaultEditor method to quickly create an image editor.

This creates a "default" editor that has all available plugins loaded and comes preset with all plugin default options and English locale. Each of these settings can be adjusted freely.

import React, { useState, useEffect } from 'react';

// React Dropzone
import { useDropzone } from 'react-dropzone';

// Pintura Image Editor
import '@pqina/pintura/pintura.css';
import { openDefaultEditor } from '@pqina/pintura';

// Based on the default React Dropzone image thumbnail example
// The `thumbButton` style positions the edit button in the bottom right corner of the thumbnail
const thumbsContainer = {
    display: 'flex',
    flexDirection: 'row',
    flexWrap: 'wrap',
    marginTop: 16,
    padding: 20,
};

const thumb = {
    position: 'relative',
    display: 'inline-flex',
    borderRadius: 2,
    border: '1px solid #eaeaea',
    marginBottom: 8,
    marginRight: 8,
    width: 100,
    height: 100,
    padding: 4,
    boxSizing: 'border-box',
};

const thumbInner = {
    display: 'flex',
    minWidth: 0,
    overflow: 'hidden',
};

const img = {
    display: 'block',
    width: 'auto',
    height: '100%',
};

const thumbButton = {
    position: 'absolute',
    right: 10,
    bottom: 10,
};

// This function is called when the user taps the edit button.
// It opens the editor and returns the modified file when done
const editImage = (image, done) => {
    const imageFile = image.pintura ? image.pintura.file : image;
    const imageState = image.pintura ? image.pintura.data : {};

    const editor = openDefaultEditor({
        src: imageFile,
        imageState,
    });

    editor.on('close', () => {
        // the user cancelled editing the image
    });

    editor.on('process', ({ dest, imageState }) => {
        Object.assign(dest, {
            pintura: { file: imageFile, data: imageState },
        });
        done(dest);
    });
};

function App() {
    const [files, setFiles] = useState([]);
    const { getRootProps, getInputProps } = useDropzone({
        accept: 'image/*',
        onDrop: (acceptedFiles) => {
            setFiles(
                acceptedFiles.map((file) =>
                    Object.assign(file, {
                        preview: URL.createObjectURL(file),
                    })
                )
            );
        },
    });

    const thumbs = files.map((file, index) => (
        <div style={thumb} key={file.name}>
            <div style={thumbInner}>
                <img src={file.preview} style={img} alt="" />
            </div>
            <button
                style={thumbButton}
                onClick={() =>
                    editImage(file, (output) => {
                        const updatedFiles = [...files];

                        // replace original image with new image
                        updatedFiles[index] = output;

                        // revoke preview URL for old image
                        if (file.preview) URL.revokeObjectURL(file.preview);

                        // set new preview URL
                        Object.assign(output, {
                            preview: URL.createObjectURL(output),
                        });

                        // update view
                        setFiles(updatedFiles);
                    })
                }
            >
                Edit
            </button>
        </div>
    ));

    useEffect(
        () => () => {
            // Make sure to revoke the Object URL to avoid memory leaks
            files.forEach((file) => URL.revokeObjectURL(file.preview));
        },
        [files]
    );

    return (
        <section className="container">
            <div {...getRootProps({ className: 'dropzone' })}>
                <input {...getInputProps()} />
                <p>Drag 'n' drop some files here, or click to select files</p>
            </div>
            <aside style={thumbsContainer}>{thumbs}</aside>
        </section>
    );
}

export default App;

Advanced implementation example

In this example we'll create a custom editor, using a custom set of plugins, locale, and available options.

While this example is a lot more verbose it does allow us to create a more optimal editor package. The build process will tree-shake unused functionality resulting in a smaller build target.

import React, { useState, useEffect } from 'react';

// React Dropzone
import { useDropzone } from 'react-dropzone';

// Pintura Image Editor
import '@pqina/pintura/pintura.css';
import {
    // Image editor
    openEditor,
    createDefaultImageReader,
    createDefaultImageWriter,

    // The method used to register the plugins
    setPlugins,

    // The plugins we want to use
    plugin_crop,
    plugin_finetune,
    plugin_annotate,

    // The main UI and plugin locale objects
    locale_en_gb,
    plugin_crop_locale_en_gb,
    plugin_finetune_locale_en_gb,
    plugin_annotate_locale_en_gb,

    // Because we use the annotate plugin we also need
    // to import the markup editor locale and the shape preprocessor
    markup_editor_locale_en_gb,
    createDefaultShapePreprocessor,

    // Import the default properties
    markup_editor_defaults,
    plugin_finetune_defaults,
} from '@pqina/pintura';

// This registers the plugins with Pintura Image Editor
setPlugins(plugin_crop, plugin_finetune, plugin_annotate);

// Based on the default React Dropzone image thumbnail example
// The `thumbButton` style positions the edit button in the bottom right corner of the thumbnail
const thumbsContainer = {
    display: 'flex',
    flexDirection: 'row',
    flexWrap: 'wrap',
    marginTop: 16,
    padding: 20,
};

const thumb = {
    position: 'relative',
    display: 'inline-flex',
    borderRadius: 2,
    border: '1px solid #eaeaea',
    marginBottom: 8,
    marginRight: 8,
    width: 100,
    height: 100,
    padding: 4,
    boxSizing: 'border-box',
};

const thumbInner = {
    display: 'flex',
    minWidth: 0,
    overflow: 'hidden',
};

const img = {
    display: 'block',
    width: 'auto',
    height: '100%',
};

const thumbButton = {
    position: 'absolute',
    right: 10,
    bottom: 10,
};

// This function is called when the user taps the edit button.
// It opens the editor and returns the modified file when done
const editImage = (image, done) => {
    const imageFile = image.pintura ? image.pintura.file : image;
    const imageState = image.pintura ? image.pintura.data : {};

    const editor = openEditor({
        src: imageFile,
        imageState,
        imageReader: createDefaultImageReader(),
        imageWriter: createDefaultImageWriter(),

        // Set Markup Editor defaults
        ...markup_editor_defaults,

        // Set Finetune plugin defaults
        ...plugin_finetune_defaults,

        // This handles complex shapes like arrows / frames
        shapePreprocessor: createDefaultShapePreprocessor(),

        // This will set a square crop aspect ratio
        imageCropAspectRatio: 1,

        // Set locale
        locale: {
            ...locale_en_gb,
            ...plugin_crop_locale_en_gb,
            ...plugin_finetune_locale_en_gb,
            ...plugin_annotate_locale_en_gb,
            ...markup_editor_locale_en_gb,
        },
    });

    editor.on('close', () => {
        // the user cancelled editing the image
    });

    editor.on('process', ({ dest, imageState }) => {
        Object.assign(dest, {
            pintura: { file: imageFile, data: imageState },
        });
        done(dest);
    });
};

function App() {
    const [files, setFiles] = useState([]);
    const { getRootProps, getInputProps } = useDropzone({
        accept: 'image/*',
        onDrop: (acceptedFiles) => {
            setFiles(
                acceptedFiles.map((file) =>
                    Object.assign(file, {
                        preview: URL.createObjectURL(file),
                    })
                )
            );
        },
    });

    const thumbs = files.map((file, index) => (
        <div style={thumb} key={file.name}>
            <div style={thumbInner}>
                <img src={file.preview} style={img} alt="" />
            </div>
            <button
                style={thumbButton}
                onClick={() =>
                    editImage(file, (output) => {
                        const updatedFiles = [...files];

                        // replace original image with new image
                        updatedFiles[index] = output;

                        // revoke preview URL for old image
                        if (file.preview) URL.revokeObjectURL(file.preview);

                        // set new preview URL
                        Object.assign(output, {
                            preview: URL.createObjectURL(output),
                        });

                        // update view
                        setFiles(updatedFiles);
                    })
                }
            >
                Edit
            </button>
        </div>
    ));

    useEffect(
        () => () => {
            // Make sure to revoke the Object URL to avoid memory leaks
            files.forEach((file) => URL.revokeObjectURL(file.preview));
        },
        [files]
    );

    return (
        <section className="container">
            <div {...getRootProps({ className: 'dropzone' })}>
                <input {...getInputProps()} />
                <p>Drag 'n' drop some files here, or click to select files</p>
            </div>
            <aside style={thumbsContainer}>{thumbs}</aside>
        </section>
    );
}

export default App;

Next steps

With the editor set up, we can continue to configure the editor to our liking by adjusting the available options exposed by the editor API