How to Upload and Resize Images with React-Dropzone and Pica

This howto will show how to upload images "locally" and resize them in a browser, e.g. client side.

For the upload I used react-dropzone and for the resize a library called pica.

React-dropzone is a well maintained uploader for React and Pica is the only maintained library for resizing images out there. At first it seemed unnecessarily low level, but now it's ok.

React-dropzone component is a slightly modified example.

There is a custom readFile function which reads the image and calls callback function from a component or container and supplies file and image binary itself, but in base64 representation.

import React, { useCallback } from 'react';
import { func } from 'prop-types';
import { useDropzone } from 'react-dropzone';
import styled from 'styled-components';

const getColor = (props) => {
  if (props.isDragAccept) {
      return '#00e676';
  }
  if (props.isDragReject) {
      return '#ff1744';
  }
  if (props.isDragActive) {
      return '#2196f3';
  }
  return '#eeeeee';
}

const Container = styled.div`
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
  border-width: 2px;
  border-radius: 2px;
  border-color: ${props => getColor(props)};
  border-style: dashed;
  background-color: #fafafa;
  color: #bdbdbd;
  outline: none;
  transition: border .24s ease-in-out;
`;

function readFile(file, onUpload) {
  const reader = new FileReader();

  reader.onload = event => {
    onUpload(file, btoa(event.target.result));
  };

  reader.readAsBinaryString(file);
}

function StyledDropzone(props) {
  const onDrop = useCallback(
    acceptedFiles => {
      acceptedFiles.forEach(file => readFile(file, props.onUpload));
    },
    [props.onUpload],
  );

  const {
    getRootProps,
    getInputProps,
    isDragActive,
    isDragAccept,
    isDragReject
  } = useDropzone({
    accept: ['image/jpeg', 'image/png'],
    onDrop,
    multiple: true,
  });

  return (
    <div className="container">
      <Container {...getRootProps({isDragActive, isDragAccept, isDragReject})}>
        <input {...getInputProps()} />
        <p>Drag 'n' drop some files here, or click to select files</p>
      </Container>
    </div>
  );
}

StyledDropzone.propTypes = {
  onUpload: func.isRequired,
}

There is a parent component which renders StyledDropzone component. Simplified, but should be clear how it works.

// some imports

class NewArticle extends React.Component {
  state = {
    form: {
      images: [],
    }
  }

  handleImage = (file, body) => {
    return resizeImage(file, body)
      .then(blob => convertBlobToBinaryString(blob))
      .then(imageString => {
        const imageBase64 = btoa(imageString);

        return this.setState(prevState => ({
          form: {
            ...prevState.form,
            images: [
              {
                ...file,
                type: 'image/jpeg',
                size: imageBase64.length,
                body: imageBase64,
              },
              ...prevState.form.images,
            ],
          }
        }));
      });
  }

  render() {
    return (
      <StyledDropzone onUpload={this.handleImage} />

      {this.state.images.map(image => <div>
        <img alt="" src={`data:${image.type};base64,${image.body}`}
      </div>)}
    )
  }
}

A helper function to resize an image:

// helpers/image.js
import Pica from 'pica';

function resizeImage(file, body) {
  const pica = Pica();

  const outputCanvas = document.createElement('canvas');
  // this will determine resulting size
  // ignores proper aspect ratio, but could be set dynamically
  // to handle that
  outputCanvas.height = 480;
  outputCanvas.width = 640;

  return new Promise(resolve => {
    const img = new Image();

    // resize needs to happen after image is "loaded"
    img.onload = () => {
      resolve(
        pica
          .resize(img, outputCanvas, {
            unsharpAmount: 80,
            unsharpRadius: 0.6,
            unsharpThreshold: 2,
        })
        .then(result => pica.toBlob(result, 'image/jpeg', 0.7)),
      );
    };

    img.src = `data:${file.type};base64,${body}`;
  });
}

And a helper function to read a blob as binary string:

function convertBlobToBinaryString(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onload = () => {
      resolve(reader.result);
    }

    reader.onabort = () => {
      reject(new Error('Reading blob aborted'));
    }

    reader.onerror = () => {
      reject(new Error('Error reading blob'));
    }

    reader.readAsBinaryString(blob);
  });
}

And that's all. I have to say the server size resizing with ImageMagick/GraphicsMagic is probably a better way and definitely more comfortable to do. You can keep aspect ratio with automatic crop, gravity and other nice features.

But it you can't, resizing in client side with Pica is also a good solution.