Picture cropping tool

introduce

Image cropping is a common feature in front end interaction. This article implements a common image cropping tool for React, Vue, Angular, and frameless front end pages.

Dynamic graph demonstration

use

const imageCut = new ImageCut({
  scale: 2.listener: {
    onOk: (info) = >{ setImgInfo(info); ,}}});const img = document.querySelector('#img');
imageCut.showRect(img);
Copy the code

Implementation approach

  • With the help ofcanvasAbility to draw and generate pictures
  • Draw the image onto the canvas first
  • Then operate the mouse to select the rectangular area to intercept
  • Generates a Base64 image for the selected region
  • You can also choose to copy or download.

The source code


import type { IRect, Point } from './interface';
import './index.less';

/** * generates canvas *@param param0
 * @returns* /
export function getCanvas({ width = 0, height = 0, scale = 1, attrs = {} as Record<string.any>}) {
  const canvas: any = document.createElement('canvas');
  Object.keys(attrs).forEach((key) = > {
    const value = attrs[key];
    canvas.setAttribute(key, value);
  });
  canvas.setAttribute('width'.`${width * scale}`);
  canvas.setAttribute('height'.`${height * scale}`);
  canvas.style = `${attrs.style || ' '}; width:${width}px; height:${height}px; `;
  const ctx = canvas.getContext('2d'); ctx? .scale(scale, scale);return { canvas, ctx };
}

/** * Download base64 as a file *@param Data base64 Data *@param Filename indicates the filename */
export function downloadBase64File(dataUrl: string, filename: string) {
  const data = base64Img2Blob(dataUrl);
  window.URL = window.URL || window.webkitURL;
  const urlBlob = window.URL.createObjectURL(data);
  const link = document.createElement('a');
  link.style.display = 'none';
  link.href = urlBlob;
  const downloadFileName = filename;
  link.setAttribute('download', downloadFileName);
  document.body.appendChild(link);
  link.click();
  function base64Img2Blob(code: string) {
    const parts = code.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 }); }}/** * Copies the string to the clipboard *@param Value The character string */
export function copyStr(value: string) {
  console.log('copyStr', value);
  const transfer = document.createElement('input');
  document.body.appendChild(transfer);
  transfer.value = value;
  transfer.setSelectionRange(0.999999999);
  transfer.focus();
  transfer.select();
  if (document.execCommand('copy')) {
    document.execCommand('copy');
  }
  transfer.blur();
  document.body.removeChild(transfer);
}


/** * Load base64 image *@param base64
 * @returns* /
export const loadImage = (base64: string) = > {
  return new Promise((resolve) = > {
    const img = new Image();
    img.crossOrigin = ' ';
    img.onload = () = > resolve(img);
    img.onerror = () = > resolve(null);
    img.src = base64;
  });
};

const resetRect = () = > {
  return { x: 0.y: 0.width: 0.height: 0 } as IRect;
};

interface IImageInfo {
  base64: string;
  width: number;
  height: number;
}

interfaceIListener { onOk? :(params: IImageInfo) = > void;
}

interfaceIProps { scale? :number; listener? : IListener; }class ImageCut {
  scale = 1;
  canvas: HTMLCanvasElement;
  ctx: any;
  pos = ' ';
  initPoint: Point = { x: 0.y: 0 };
  rect: IRect = resetRect();
  initRect: IRect = resetRect();
  originRect = resetRect();
  img: any = null;
  listener: IListener = {};
  constructor(props: IProps = {}) {
    const { scale = 1 } = props;
    const { canvas, ctx } = getCanvas({ width: 0.height: 0 });
    this.canvas = canvas;
    this.ctx = ctx;
    this.scale = scale;
    this.listener = props.listener || {};
  }
  showRect(img: HTMLImageElement) {
    this.reset();
    const rect = img.getBoundingClientRect();
    const rectContainer = this.renderRect(rect);
    document.body.appendChild(rectContainer);
    this.img = img;
  }
  getCutImageInfo() {
    this.drawImage(this.img);
    const { originRect, rect } = this;
    const info = {
      x: rect.x - originRect.x,
      y: rect.y - originRect.y,
      width: rect.width,
      height: rect.height,
    };
    const base64 = this.cutImage(info);
    return { base64, width: rect.width, height: rect.height };
  }
  startCut() {
    const { listener } = this;
    const res = this.getCutImageInfo();
    this.reset(); listener.onOk? .(res); }reset() {
    this.pos = ' ';
    this.initPoint = { x: 0.y: 0 };
    this.rect = resetRect();
    this.initRect = resetRect();
    this.originRect = resetRect();
    this.removeRect();
  }
  drawImage(img: HTMLImageElement) {
    const { ctx, scale } = this;
    const { width, height } = img;
    this.setCanvasSize(width, height);
    ctx.drawImage(img, 0.0, width * scale, height * scale);
  }
  cutImage(rect: IRect) {
    console.log(rect);
    const { scale, ctx, canvas } = this;
    const { x, y, width, height } = rect;
    const imgData = ctx.getImageData(x * scale, y * scale, width * scale, height * scale);
    ctx.clearRect(0.0, canvas.width, canvas.height);
    this.setCanvasSize(width, height);
    ctx.putImageData(imgData, 0.0);
    const base64 = canvas.toDataURL('image/png');
    return base64;
  }

  setCanvasSize(width: number, height: number) {
    const { canvas, scale } = this;
    canvas.setAttribute('width'.`${width * scale}`);
    canvas.setAttribute('height'.`${height * scale}`);
    canvas.setAttribute('style'.`width: ${width}px; height:${height}px; `);
  }
  renderRect(rect: IRect) {
    const { x, y, width, height } = rect;
    this.rect = { x, y, width, height };
    this.originRect = { x, y, width, height };
    const container: any = document.createElement('div');
    container.setAttribute('id'.`rect__container`);
    container.style = `
      position: fixed;
      border: 2px solid red;
      left: ${rect.x}px;
      top: ${rect.y}px;
      width: ${rect.width}px;
      height: ${rect.height}px;
    `;
    const rectList = [
      { x: 50.y: 0.pos: 'top'.mousePos: 'ns' },
      { x: 50.y: 100.pos: 'bottom'.mousePos: 'ns' },
      { x: 0.y: 50.pos: 'left'.mousePos: 'ew' },
      { x: 100.y: 50.pos: 'right'.mousePos: 'ew' },
      { x: 100.y: 0.pos: 'right-top'.mousePos: 'nesw' },
      { x: 100.y: 100.pos: 'right-bottom'.mousePos: 'nwse' },
      { x: 0.y: 100.pos: 'left-bottom'.mousePos: 'nesw' },
      { x: 0.y: 0.pos: 'left-top'.mousePos: 'nwse'},]; rectList.forEach((e: any, i: number) = > {
      const rectDom: any = document.createElement('div');
      rectDom.setAttribute('data-pos', e.pos);
      rectDom.setAttribute('data-index', i);
      rectDom.setAttribute('id'.`rect__${e.pos}`);
      rectDom.style = `
        position: absolute;
        left: ${e.x}%;
        top: ${e.y}%;
        width: 7px;
        height: 7px;
        border: 1px solid #ddd;
        background-color: #fff;
        transform: translate(-50%, -50%);
        cursor: ${e.mousePos}-resize;
      `;
      rectDom.addEventListener('mousedown'.this.handleMouseDown);
      container.appendChild(rectDom);
    });
    const confirmIcon = ` < SVG viewBox = "0 0 1024 1024" version = "1.1" XMLNS = "http://www.w3.org/2000/svg" width = "18" height = "18" > < path D ="M159.405 462.713l218.634 218.634L859.54 196.055l94.784 94.151-580.076 583.236L69.676 568.87l89.729-106.157z" fill="currentColor" >  `;
    const cancelIcon = ` < SVG viewBox = "0 0 1024 1024" version = "1.1" XMLNS = "http://www.w3.org/2000/svg" width = "16" height = "16" > < path d = "M896 812.9 594.2 511.1 L301.2-301.2-82.3 -82.3L511.9 428.8 L-301-301L128.6 210.1 L301 301L128 812.7l82.3 82.3 301.6-301.6 301.8 301.8l896 812.9zm896 812.9" Fill ="currentColor" > 
      ;

    const downloadIcon = ` < SVG viewBox = "0 0 1024 1024" version = "1.1" XMLNS = "http://www.w3.org/2000/svg" width = "16" height = "16" > < path d = "M896 864.384 v96H128V-96H768zm564.096 64V506.176l201.376-201.44 67.84 67.84-316.768 316.8-316.8-316.8 67.904-67.84 200.448 200.416v64H96z "fill="currentColor" > 
       ';

    const copyIcon = ` < SVG viewBox = "0 0 1024 1024" version = "1.1" XMLNS = "http://www.w3.org/2000/svg" width = "15" height = "15" > < path d = "M921.6 819.2h-102.4v102.4c0 56.32-46.08 102.4-102.4 102.4H102.4C-56.320-102.4-46.08-102.4-102.4v307.2c0-56.3246.08-102.4 102.4-102.4h102.4v102.4c0-56.32 46.08-102.4 102.4-102.4h614.4c56.32 0 102.4 46.08 102.4 102.4v614.4c0 56.32-46.08 102.4-102.4 102.4zM153.6 307.2c-30.72 0-51.2 20.48-51.2 51.2v512c0 30.72 20.48 51.2 51.2 51.2 H512c30.72 0 51.2-20.48 51.2 51.2 V358.4 c0-30.72-20.48-51.2-51.2-51.2 H153.6 z m768 c0-153.6-30.72-20.48-51.2-51.2-51.2 H358.4 c 0-30.72-51.2 20.48-51.2 51.2v51.2h409.6 C56.32 0 102.4 46.08 102.4 102.4v409.6 H51.2 c30.72 0 51.2-20.48 51.2-51.2 v153.6z" fill="currentColor" >  `;

    const operationDom: any = document.createElement('div');
    operationDom.style = ` position: absolute; right: 0; bottom: -30px; width: 120px; height: 26px; border: 1px solid #ddd; border-radius: 2px; background-color: #fff; display: flex; justify-content: center; padding: 0 4px; `;

    const downloadDom: any = document.createElement('div');
    downloadDom.setAttribute('title'.'download');
    downloadDom.setAttribute('class'.'image-cut-operation-icon');
    downloadDom.innerHTML = downloadIcon;
    downloadDom.addEventListener('click'.() = > {
      const { base64 } = this.getCutImageInfo();
      downloadBase64File(base64, `The ${Date.now()}.png`);
    });
    operationDom.appendChild(downloadDom);


    const copyDom: any = document.createElement('div');
    copyDom.setAttribute('title'.'Copy to clipboard');
    copyDom.setAttribute('class'.'image-cut-operation-icon');
    copyDom.innerHTML = copyIcon;
    copyDom.addEventListener('click'.() = > {
      const { base64 } = this.getCutImageInfo();
      copyStr(base64);
    });
    operationDom.appendChild(copyDom);

    const cancelDom: any = document.createElement('div');
    cancelDom.setAttribute('title'.'cancel');
    cancelDom.setAttribute('class'.'image-cut-operation-icon');
    cancelDom.innerHTML = cancelIcon;
    cancelDom.addEventListener('click'.() = > this.reset());
    operationDom.appendChild(cancelDom);

    const confirmDom: any = document.createElement('div');
    confirmDom.setAttribute('title'.'confirm');
    confirmDom.setAttribute('class'.'image-cut-operation-icon');
    confirmDom.innerHTML = confirmIcon;
    confirmDom.addEventListener('click'.() = > this.startCut());
    operationDom.appendChild(confirmDom);





    container.appendChild(operationDom);
    return container;
  }

  updateRect() {
    const { rect } = this;
    const container = document.getElementById('rect__container');
    if(! container) {return;
    }
    container.style.left = `${rect.x}px`;
    container.style.top = `${rect.y}px`;
    container.style.width = `${rect.width}px`;
    container.style.height = `${rect.height}px`;
  }

  removeRect() {
    const container = document.getElementById('rect__container'); container? .parentNode? .removeChild(container); } handleMouseDown =(e: any) = > {
    const { target, pageX: x, pageY: y } = e;
    const pos = target.getAttribute('data-pos');
    this.pos = pos;
    this.initPoint = { x, y };
    this.initRect = { ... this.rect };document.addEventListener('mousemove'.this.handleMouseMove);
    document.addEventListener('mouseup'.this.handleMouseUp);
  };
  handleMouseMove = (e: any) = > {
    if (!this.pos) {
      return;
    }
    const { initRect } = this;
    const { x, y } = this.initPoint;
    const { pageX, pageY } = e;
    let dx = 0;
    let dy = 0;
    let dw = 0;
    let dh = 0;
    if (this.pos.includes('top')) {
      dy = pageY - y;
      dh = -dy;
    }
    if (this.pos.includes('bottom')) {
      dh = pageY - y;
    }
    if (this.pos.includes('left')) {
      dx = pageX - x;
      dw = -dx;
    }
    if (this.pos.includes('right')) {
      dw = pageX - x;
    }
    dx = Math.min(initRect.width, Math.max(0, dx));
    dy = Math.min(initRect.height, Math.max(0, dy));
    dw = Math.min(0.Math.max(-initRect.width, dw));
    dh = Math.min(0.Math.max(-initRect.height, dh));
    this.rect = {
      x: initRect.x + dx,
      y: initRect.y + dy,
      width: initRect.width + dw,
      height: initRect.height + dh,
    };
    this.updateRect();
  };
  handleMouseUp = () = > {
    if (!this.pos) {
      return;
    }
    this.pos = ' ';
    this.initPoint = { x: 0.y: 0 };
    document.removeEventListener('mousemove'.this.handleMouseMove);
    document.removeEventListener('mouseup'.this.handleMouseUp);
  };
}

export default ImageCut;

Copy the code
  • style
.image-cut-operation-icon {
  color: # 555;
  cursor: pointer;
  margin-left: 8px;
  display: flex;
  align-items: center;
  &:hover {
    color: #1890ff; }}Copy the code