Front-end upload picture has been a platitudes of the topic, I have encountered this kind of demand many times in the work, although there are many plug-ins on the market now, but, in line with the idea of making their own wheels, I intend to do a wave, is also a summary and combing of the relevant knowledge.

Holistic thinking

  • Get the files object of the image using the type=’file’ input tag.
  • Convert files objects to base64 format via FileReader.
  • Compress the image with Canvas.drawImage ().
  • The base64 is verified by the exift. js plug-in, and the rotated pictures are corrected by canvas.
  • Convert Base64 to a BLOB binary object and upload it.

01. What are Files objects?

We can get an array of fileList classes with the following simple code

Class array: 1) Has the length attribute and the other attributes (index) are non-negative integers (indexes in objects are treated as strings); 2) Does not have array methods! (such as: push, forEach, filter, reduce, etc.)

.<input type="file" @change="handleChange"/>.Copy the code
. handleChange(e){console.log(e.target.files)
}
...
Copy the code

Get the following objects:

  • Name: The name of the image
  • LastModified: Timestamp of file modification
  • Size: indicates the file size (unit :B)
  • Type: indicates the file type
  • , etc.

The file name, modification time, size and type are recorded in this object. This object only stores some information about the file, which is similar to the index of the local file. Instead of putting the file into the input, it will find the actual local file when uploading the file. Note that this is a read-only object! “, so don’t try to modify it directly.

02. About FileReader

Now that we can’t modify a fileList directly, what if we need to do something with the original file, such as compress it? Here we can use Filereader.readasDataURL (file) to get a Base64 string in URL format that represents the contents of the file being read. And we’re going to do that by dealing with Base64.

FileReader concept: The FileReader object allows a Web application to asynchronously read the contents of a File (or raw data buffer) stored on the user’s computer, using a File or Blob object to specify which File or data to read.

So we can do the following with this fileList object that we just got

. if (files.length ===0) return;
    let file = files[0]
    console.log(file, 'file')
    let fileReader = new FileReader()
    fileReader.onloadstart = (a)= > {
        // If the type does not match
        if (this.accept ! = ='image/*'&&!this.accept.includes(file.type.toLowerCase())) {
            fileReader.abort()
            console.error('Wrong format -->', file.type.toLowerCase())
          }
    }
    fileReader.onload = (a)= > {
        // This is the base64 we need for this step
          let base64 = fileReader.result
    }
    fileReader.onprogress = (e) = > {}
    fileReader.readAsDataURL(file) 
...
Copy the code

03. Compression

Size =.size =.size =.size =.size =.size =.size =.size As we all know, with more and more pixels and clearer pictures on mobile phones nowadays, the size of pictures has also increased and become bigger and bigger. If the Internet speed is poor, uploading pictures will look like this:

base64
canvas.drawImage()
canvas.toDataURL('image/jpeg', this.quality)

Canvas: a new element in HTML5 that can be used to draw graphics and create animations using JAVASCRIPT scripts.

The code is as follows:

. let canvas =document.createElement('canvas')
let ctx = canvas.getContext('2d');
let image = document.createElement('img');
image.src = base64;
image.onload = (a)= > {
    canvas.width = imageWidth
    canvas.height = imageHeight
    ctx.drawImage(image, 0.0, imageWidth, imageHeight);
     // The mass is compressed to 0.5
    canvas.toDataURL('image/jpeg'.0.5)}...Copy the code

The drawImage method

I think this is a very powerful method to use not only for drawing, but more importantly for clipping the front image, and since this method is also used for rotating the canvas behind, I’ll expand it a little bit.

CTX. DrawImage (img, startX, startY, croppedWidth croppedHeight, locationX, LocationY, finalCanvasWidth, finalCanvasHeight), Although the parameters seem very large, it is not difficult to find patterns

In practice, it is found that this method is actually equivalent to the mode of 2, which is simply called 5-parameter mode and 9-parameter mode for the moment.

  • Img: A resource that will be rendered on the canvas, either an image or a video.

  • StartX /startY: There are two different modes for this parameter.

    5 parameter mode indicates the position of the picture on the canvas (Canvas coordinate);

    9 Parameter mode indicates the starting position of cropping on the picture (picture coordinates)

  • CroppedWidth/croppedHeight: tailoring range on the image

  • LocationX /locationY: The place to start drawing on the Canvas canvas, effectively replacing the role of startX/startY in 5-parameter mode

  • FinalCanvasWidth/finalCanvasHeight: drawing on canvas canvas range (improper handling can cause distorted images)

So, what we see is that in the drawImage, in addition to the first parameter, The rest of the parameters and coordinate related to the length, and in the nine parameters model, the former of length into 2 (startX/startY and croppedWidth/croppedHeigh) is and pictures, which was cut out after two length into the ginseng (locationX/locationY and finalCanv AsWidth/finalCanvasHeight), are related to the canvas.

The following is a brief introduction to the basic usage:

  1. We can ignore the following two pairs of parameters and also draw pictures, but the size relationship between the original picture and the canvas should be properly handled. The default size of canvas is300 * 150If we take a really big picture1280 * 853
. <! DrawImage (img) --> CTX. DrawImage (img,0.0, img.naturalWidth, img.naturalHeight)
...
Copy the code

As you can see, we’ve drawn the image, but it’s not complete, because we’ve got the pixel information img.naturalWidth, img.naturalHeight for the entire image, but we haven’t done a very good job with it, just dropped it into a 300 * 150 rectangle, The rest is not considered at all. As you can imagine, we only get a little bit of picture in the upper left corner, but nothing else.

  1. Therefore, the paradox of this case is that the image we want to put is too big, while the “container” is too small, so there are only two solutions, either shrink the image or enlarge the container.
 ctx.drawImage(img, 0.0, img.naturalWidth, img.naturalHeight, 0.0, canvas.width, canvas.height)
Copy the code

300 * 150

  1. So we also have to consider the aspect ratio of the image, we calculate the aspect ratio of the original imageimg.naturalWidth / img.naturalHeightAnd calculate the size of the container
   let radio = img.naturalWidth / img.naturalHeight
   ctx.drawImage(img, 0.0, img.naturalWidth, img.naturalHeight, (canvas.width - canvas.height * radio) / 2.0, canvas.height * radio, canvas.height)
Copy the code

  1. So with the idea of making the “container” bigger, we have the following treatment:
canvas.width = img.naturalWidth
canvas.height = img.naturalHeight
ctx.drawImage(img, 0.0, img.naturalWidth, img.naturalHeight)
Copy the code

04. Picture rotation problem

Just as everything seems to be going well, after the test test students came to the ‘bad news ‘, the picture rotated the overturned car scene:

  • Mobile phone (Android) portrait

  • Mobile phone (ios) portrait

    After simple (reverse) single (complex) test (folding) test (grinding), it was found to be caused by the bug of ios itself, which is summarized as follows:

  • When taken vertically on an ios phone, the image rotates 90 degrees counterclockwise;

  • When taken with aN ios phone landscape (home button/bottom of the phone on the right), the photo works fine;

  • When taken with aN ios phone landscape (home button/bottom of the phone on the left), the pin rotates 180 degrees;

  • When taking a picture with your ios phone backwards (I know most people don’t, unless you’re taking it abnormally…) “, the photo will rotate 90 degrees clockwise;

Solution: Since the photo is rotated, the natural thought is to correct it. For example, if we have myopia, the lens cannot focus light on the retina correctly, we use glasses to correct the Angle of light. Different “degrees” correspond to different correction methods. Before correction, it is necessary to know whether there is a problem and what kind of problem there is. The detection plug-in I use here is ExIF-js, and the getTag method is used to obtain the degree of picture rotation, so as to take different measures.

. const EXIF =require("exif-js");
 EXIF.getData(file, function () {
    let Orientation = EXIF.getTag(this.'Orientation');
    // Determine whether the photo is normal and the degree of wrong rotation through the Orientation value
    // Orientation === 6:90 degrees counterclockwise
    // Orientation === 3: Orientation = 180 °
    // Orientation === 8: Rotated 90 degrees clockwise
    // Orientation === 1 or undefined: normal})...Copy the code
  1. When the photo is rotated 90 degrees counterclockwise:
. <! -- The picture is rotated90Width = imageHeight; width = imageHeight; canvas.height = imageWidth; Rotate the picture clockwise90Degree of CTX. Rotate (Math.PI / 2);
<! DrawImage has two modes. This is a 5-parameter mode.
ctx.drawImage(img, 0, -imageHeight, imageWidth, imageHeight);
<! --9 parameter mode -->
ctx.drawImage(img, 0, 0, imageWidth,imageHeight,  0, -imageHeight, imageWidth, imageHeight);
<! -- In 9 parameter mode, parameter 6 and 7 replace parameter 2 and 3 in 5 parameter mode..Copy the code

2. When the photo is rotated 180 degrees:

. <! -- Canvas length and width, pay attention180Canvas. Width = imageWidth; canvas.height = imageHeight;<! Turn the canvas another 180 degrees -->
ctx.rotate(Math.PI);
<! --5 parameter mode -->
ctx.drawImage(img, -imageWidth, -imageHeight, imageWidth, imageHeight);
<! --9 parameter mode -->ctx.drawImage(img, 0, 0, imageWidth,imageHeight, -imageWidth, -imageHeight, imageWidth, imageHeight); .Copy the code

3. When the photo is rotated 90 degrees clockwise:

. <! Rotation -90Canvas. Width = imageHeight; canvas.height = imageWidth;<! -- Rotate the image 90 degrees counterclockwise -->
ctx.rotate(3 * Math.PI / 2);
<! --5 parameter mode -->
ctx.drawImage(img, -imageWidth, 0, imageWidth, imageHeight);
<! --9 parameter mode -->ctx.drawImage(img, 0, 0, imageWidth,imageHeight, -imageWidth, 0, imageWidth, imageHeight); .Copy the code

05. Upload to BLOB object

After all these steps, we just have a base64, we need to convert the BLOB binary data and upload it to the server:

let arr = base64.split(', '),
mime = arr[0].match(/ : (. *?) ; /) [1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
}
Blob([u8arr], { type: mime }); .Copy the code

The complete code

Ok, with that said, now post the full code as follows:

class Upload {
  constructor({ el, accept, multiple, onUpload, quality }) {
    this.el = el || ' '
    this.accept = accept || 'image/*'
    this.multiple = multiple || false
    this.quality = quality || 1
    this.beforeUpload = (e) = > { console.log(e) }
    this.onProgress = (e) = > { console.log('progress->', e) }
    this.onLoad = (result) = > {
      onUpload(result)
    }
    this.onError = (a)= >{}this.init()
  }
  init () {
    // If there are nodes
    if (this.el) {
      this.el = typeof this.el === 'object' ? this.el : document.querySelector(this.el)
    }
    this.render()
    this.watch()
  }
  // Render the node
  render () {
    let fragment = document.createDocumentFragment(),
      file = document.createElement('input');
    console.log(file, 'file')
    file.type = this.accept
    file.setAttribute('type'.'file');
    file.setAttribute('multiple'.this.multiple)
    file.setAttribute('accept'.this.accept)
    // Android is not wechat browser
    // iPhone/Android (Xiaomi) mobile wechat browser
    // file.setAttribute('capture', 'camera')
    file.style.display = "none"
    file.className = 'upload__input'
    fragment.appendChild(file)
    this.el.appendChild(fragment)
  }
  watch () {
    let inputEl = this.el.querySelector('.upload__input');
    inputEl.addEventListener('change', () = > {// The pseudo-array is converted to an array
      let files = Array.from(inputEl.files) // If several images are selected at the same time, the array. Length >1
      // Read the image
      let readImg = (a)= > {
        // The recursion ends when the image is exhausted
        if (files.length === 0) return;
        let file = files[0]
        let fileReader = new FileReader()
        fileReader.onloadstart = (a)= > {
          // If the type does not match
          if (this.accept ! = ='image/*'&&!this.accept.includes(file.type.toLowerCase())) {
            fileReader.abort()
            this.beforeUpload(file)
            console.error('Wrong file format -->', file.type.toLowerCase())
          }
        }
        fileReader.onload = async() = > {let base64 = fileReader.result
          let compressedBase64 = await this.compressBase64(file, base64)
          let blob = this.base64ToBlob(compressedBase64)
          this.onLoad({ blob, base64: compressedBase64 })
          files.shift() // delete the first one
          / / recursion
          readImg()
        }
        fileReader.onprogress = (e) = > {
          this.onProgress(e)
        }
        this.isImage(file.type) ? fileReader.readAsDataURL(file) : fileReader.readAsText(file);
      }
      readImg()
    })
  }

  / / compression base64
  compressBase64 (file, base64) {
    let canvas = document.createElement('canvas')
    let image = document.createElement('img');
    image.src = base64;
    let size = file.size / 1000 / 1024 // b -> MB

    console.log(size, 'MB')
    this.quality = Math.min(2 / size, 1) // Image size is limited to 2MB
    console.log(this.quality, 'quality')
    return new Promise(resolve= > {
      image.onload = async() = > {let imageWidth = image.naturalWidth;
        let imageHeight = image.naturalHeight;
        await this.rotateCanvas(file, image, canvas, imageWidth, imageHeight)
        resolve(canvas.toDataURL('image/jpeg'.this.quality))
      }
    })
  }

  // Rotate the canvas to prevent image rotation problems in earlier versions of ios
  rotateCanvas (file, image, canvas, imageWidth, imageHeight) {
    let ctx = canvas.getContext('2d');
    let Orientation = 1
    const EXIF = require("exif-js");
    return new Promise(resolve= > {
      EXIF.getData(file, function () {
        // Get the image information
        Orientation = EXIF.getTag(this.'Orientation');
        console.log(Orientation, 'orient')
        switch (Orientation * 1) {
          case 6:     // Rotate 90 degrees
            canvas.width = imageHeight;
            canvas.height = imageWidth;
            ctx.rotate(Math.PI / 2);
            ctx.fillStyle = "white"
            ctx.fillRect(0.0, canvas.width, canvas.height)
            ctx.drawImage(image, 0, -imageHeight, imageWidth, imageHeight);
            break;
          case 3:// Rotate 180 degrees
            canvas.width = imageWidth;
            canvas.height = imageHeight;
            ctx.rotate(Math.PI);
            ctx.fillStyle = "white"
            ctx.fillRect(0.0, canvas.width, canvas.height)
            ctx.drawImage(image, -imageWidth, -imageHeight, imageWidth, imageHeight);
            break;
          case 8:     // Rotate -90 degrees
            canvas.width = imageHeight;
            canvas.height = imageWidth;
            ctx.rotate(3 * Math.PI / 2);
            ctx.fillStyle = "white"
            ctx.fillRect(0.0, canvas.width, canvas.height)
            ctx.drawImage(image, -imageWidth, 0, imageWidth, imageHeight);
            break;
          default:
            // The default is correct
            canvas.width = imageWidth;
            canvas.height = imageHeight;
            ctx.fillStyle = "white"
            ctx.fillRect(0.0, canvas.width, canvas.height)
            ctx.drawImage(image, 0.0, imageWidth, imageHeight); } resolve() }); })}// Check if it is a picture
  isImage (type) {
    let reg = /(image\/jpeg|image\/jpg|image\/png)/gi
    return reg.test(type)
  }
  // base64 -> blob
  base64ToBlob (base64) {
    let arr = base64.split(', '),
      mime = arr[0].match(/ : (. *?) ; /) [1],
      bstr = atob(arr[1]),
      n = bstr.length,
      u8arr = new Uint8Array(n);
    while (n--) {
      u8arr[n] = bstr.charCodeAt(n);
    }
    return new Blob([u8arr], { type: mime }); }}export default Upload
Copy the code

call

    new Upload({
      el: document.querySelector('.upload__btn'),
      accept: 'image/*'.multiple: true.quality: 1,
      onUpload ({ blob, base64 }) {
        // Base64 is used for preview
        // blob to the background}})Copy the code