Bill
Bill

Reputation: 5150

React Typescript: Why is my canvas image resize component creating small blurry images from large crisp images?

I've made this little image resizer and I can't figure out why the images it creates are blurry, especially when I use a large image.

import React, { useState } from 'react';

interface PropsInterface {
  placeholder: string;
  resizeTo: number;
  blob?: Function;
  base64?: Function;
}

const Photo: React.FC<PropsInterface> = (props: PropsInterface) => {

  const [state, setState] = useState({
    imageURL: props.placeholder,
  });

  const {
    imageURL,
  } = state;

  const dataURLToBlob = (dataURL: string) => {
    const parts = dataURL.split(';base64,');
    const contentType = parts[0].split(':')[1];
    const raw = window.atob(parts[1]);
    const rawLength = raw.length;
    const uInt8Array = new Uint8Array(rawLength);
    for (let i = 0; i < rawLength; ++i) {
      uInt8Array[i] = raw.charCodeAt(i);
    }
    return new Blob([uInt8Array], { type: contentType });
  };

  const resizeImage = (event: React.ChangeEvent<HTMLInputElement>) => {
    if (event.currentTarget.files) {
      const file = event.currentTarget.files[0];
      const reader = new FileReader();
      reader.onload = (readerEvent) => {
        const image = new Image();
        image.onload = async (imageEvent) => {
          const canvas = document.createElement('canvas');
          const maxSize = props.resizeTo;
          let width = image.width;
          let height = image.height;
          if (width > height && width > maxSize) {
              height *= maxSize / width;
              width = maxSize;
          } else if (height > maxSize) {
              width *= maxSize / height;
              height = maxSize;
          }
          canvas.width = width;
          canvas.height = height;
          const ctx: CanvasRenderingContext2D = canvas.getContext('2d')!;
          ctx.imageSmoothingEnabled = false;
          ctx.drawImage(image, 0, 0, width, height);
          const dataUrl = canvas.toDataURL('image/jpeg');
          const resizedImage = dataURLToBlob(dataUrl);
          setState((prev) => ({ ...prev, imageURL: dataUrl }));
          if (props.blob) props.blob(resizedImage);
          if (props.base64) props.base64(dataUrl);
        };
        image.src = URL.createObjectURL(file);
      };
      reader.readAsDataURL(file);
    }
  };

  return (
    <div className='photo'>
      <label>
        <img
          src={imageURL}
          alt='your first name initial'
          className='photo--preview'
        />
        <input
          type='file'
          id='photo'
          name='photo'
          accept='image/png, image/jpeg'
          onChange={resizeImage}
        ></input>
      </label>
    </div>
  );
};

export { Photo };

I use it by adding

<Photo placeholder={placeholder} resizeTo={60} blob={blob} />

Where placeholder is a URL to an image I want to display initially resizeTo is the size in pixels I want to resize the image to and blob is the name of the function that it returns a blob to.

Left is my image resizer and right is from another website. The image from another website is 180px square, So I set resizeTo 180 and the result can be seen on the left.

The left image is 180 width but not 180pix high, maybe this is why the quality looks worse? How to crop the image to 180x180? so we can make a fair comparison?

enter image description here

Upvotes: 0

Views: 1594

Answers (2)

Anthony Poon
Anthony Poon

Reputation: 887

When your file is loaded, you should save the loaded data as source of truth, and if you want to retain all the information, the source of truth cannot be modified.

Instead, you create a view for source, and change the view instead. In my example below, the source of truth is this.state.data and the view is the <img> tag. The slider is used to modify the img and the source is not changed.

See the code below.

JSFiddle: https://jsfiddle.net/ypoon196/yjprgfch/25/

class App extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        // Base64
        data: "",
        // Original W/H
        imageWidth: 0,
        imageHeight: 0,
        // resized W/H
        currWidth: 0,
        currHeight: 0,
        lockAspect: true,
      };
  };

  handleFileChange(evt) {
    const file = evt.target.files[0];
    if (!!file) {
      const reader = new FileReader();
      reader.onload = () => {
        // Save to a temp location to get the height and width
        const tmp = new Image();
        tmp.onload = () => {
          // Only display everythin when all is loaded
          const height = tmp.height;
          const width = tmp.width;
          this.setState({
            data: reader.result,
            imageHeight: height,
            imageWidth: width,
            currHeight: height,
            currWidth: width
          })
        };
        tmp.src = reader.result;
      }
      reader.readAsDataURL(file);
    }
  }

  handleSliderChange(type, evt) {
    const val = evt.target.value;
    const { lockAspect, imageHeight, imageWidth } = this.state;
    switch (type) {
        case "height":
        if (lockAspect) {
            const ratio = imageHeight / imageWidth;
          const otherVal = val / ratio;
            this.setState(prevState => {
            return { currHeight: val, currWidth: otherVal }
          });
        } else {
            this.setState(prevState => {
            return { currHeight: val }
          });
        }       
        break;
      case "width":
        if (lockAspect) {
            const ratio = imageHeight / imageWidth;
          const otherVal = val * ratio;
            this.setState(prevState => {
            return { currHeight: otherVal, currWidth: val }
          });
        } else {
            this.setState(prevState => {
            return { currWidth: val }
          });
        }
        break;
    }
  }

  handleAspectToggle() {
    this.setState(({lockAspect}) => {
        return {lockAspect: !lockAspect}
    })
  }

  render() {
    const { data, imageHeight, imageWidth, currWidth, currHeight, lockAspect } = this.state;
    return (
        <div>
          <div>
            <input type="file" onChange={evt => this.handleFileChange(evt)} accept="image/*"/>
          </div>
          <div>
            <label>Height</label>
            <input 
              type="range"
              min="1" 
              max={imageHeight} 
              value={currHeight}
              onChange={evt => this.handleSliderChange("height", evt)}
            />
            <span>{ currHeight }</span>
          </div>
          <div>
            <label>Width</label>
            <input 
              type="range"
              min="1" 
              max={imageWidth} 
              value={currWidth}
              onChange={evt => this.handleSliderChange("width", evt)}
            />
            <span>{ currWidth }</span>
          </div>
          <div>
            <label>Lock Aspect</label>
            <input 
              type="checkbox"
              checked={lockAspect}
              onChange={() => this.handleAspectToggle()}
            />
          </div>
          <div>
            <img height={currHeight} width={currWidth} src={data}/>
          </div>
        </div>
    );
  }
}

ReactDOM.render(
  <App/>,
  document.getElementById('container')
);


Upvotes: 1

krishnan
krishnan

Reputation: 792

Whenever you reduce an image you will loose some information. One thing to consider while resizing the image is Aspect Ratio and i believe this part of the code handles it

if (width > height && width > maxSize) {
     height *= maxSize / width;
     width = maxSize;
} else if (height > maxSize) {
     width *= maxSize / height;
     height = maxSize;
}
canvas.width = width;
canvas.height = height;

Other than that, the image on the right is not just resized but also cropped. If you look at the top right corner some part of the clouds are missing. You cannot compare the left and right, check if original image and the resized image's aspect ratio matches, if not fix it.

Upvotes: 1

Related Questions