preface

Recently, the project of the image requirements are relatively high, often will be the image compression and modify the resolution of the operation, as time goes by, feel to write a, so spent a day to complete the offline version of the image compression tool, without the server, the local can run.

I refer to two articles of Zhang Xinxu, and the link is placed at the bottom of the article. Those who are interested can have an in-depth understanding.

Project Github address

Results show

First take a look at the image upload:

Take a look at how the images look online:

The CSS section is not the focus of this discussion and can be ignored for the time being. For aesthetic purposes, some of antD’s component styles are referenced here.

The principle is introduced

The principle is simple. It’s the canvas application. After the user uploads. we can get the base64 content of the image, and then draw the image using the drawImage method of canvas. When using the drawImage method, we can define the width, height and quality of the image. The export format as PNG is useless.

The size of the image is actually calculated using the base64 content of the image. This is where base64 comes in a little bit. Get out your little notebook and write it down:

Base64 was invented to be compatible with languages other than English. Because Chinese or Japanese cannot be processed effectively on the server or network management system, garbled characters often appear, and base64 appears, which is converted into a string of encoders that can be transmitted at will, and then translated after receiving.

Base64 has its own table in which each byte has its own code name.

First, the string to be converted is divided into three groups. Each byte is 8 bits in size, so three bytes have 24 binary bits.

Then, divide the above 24 levels into four groups of six.

Next, add two zeros to the front of each group, so that each group becomes eight and the secondary system is four groups of 32 binary bits, four bytes in total.

Finally, according to the Base64 conversion table mentioned earlier, these secondary bits are translated to produce the final Base64 string.

So there are two problems: First, because two zeros are added before each group, the base64-encoded text is about a third larger than the native text. Second, why use groups of three bytes? That’s because the least common divisor of 6 and 8 is 24, and three 8’s and four 6’s are exactly 24.

The other special case is what if there’s not enough digits? It may have fewer than three bytes. If the number of bytes is 2, you can get 16 binary bits. After a group of 6, the last group is two off, and the 0 makes up exactly 3 groups. But what about the fourth group? And then you have to use theta equals to pretend that this is a group, to force four groups. If you only have one byte, then 12 divided by 6 is 2, and you still need two groups to get to 4, so you need two equals to make four groups. I worked really hard for team 4. So base64 encodings can have one or two =’s.

With this knowledge we can calculate the file volume in reverse, as follows:

Const getFileSize = (base64Url) => {// remove header information (data:image/ PNG; Base64,)let baseStr=base64Url.substring(base64Url.indexOf('base64,') +'base64,'.length); // remove "=" baseStr = basestr. replace(/=/gi,' '); // Calculate const strLen= basestr.length;return strLen-(strLen/8)*2
}
Copy the code

First remove the header type, PNG, JPEG, etc. Next, use re to remove the equal sign. And then you subtract the filling 0. There are two zeros for every eight strings, so you divide the whole length by eight and multiply it by two to get the number of zeros. Subtract the number of zeros from the total length to get the number of bytes born, the actual number of bytes, and divide by 1024 to get the final size in KB.

It should be noted that the MAC and Windows file volume calculation method is different, MAC is in base 1000, while Windows is in base 1024, so there will be some differences, this difference can be seen from the MAC disk capacity is almost sufficient.

The page layout

Obviously, the whole page is composed of two parts, the left is the image compression information modification part, the right is the use of instructions and preview part.

First, the left side is a wrapper that wraps everything on the left side. It is divided into several parts, the first is the top image custom width and height part, the code is as follows:

<div class="wrapper">
  <div class="size-options">
    <p class="sub-title"</p> <ul> <li class="m-b-10"<span style = "box-sizing: border-box; color: RGB (74, 74, 74); line-height: 22px; white-space: inherit! Important;"input-text" type="text" id="custom-width" placeholder="Default to the original image size" onChange="updateImage()"Word-wrap: break-word! Important; "> <span style =" text-align: center"input-text" type="text" id="custom-height" placeholder="Default to the original image size" onChange="updateImage()">
      </li>
    </ul>
  </div>
</div>
Copy the code

Simple HTML, just two inputs. Under size-options, I added the configuration for the size of images everywhere. The code is as follows:

<div class="wrapper"<div class="clarity-options">
    <p class="sub-title"> < p style = "box-sizing: border-box! Important; word-wrap: break-word! Important;"fileType" type="radio" value="jpeg" checked onChange="clarityWeightChange(value)">
        <label class="radio-label">JPG</label>
        <input name="fileType" type="radio" value="png" onChange="clarityWeightChange(value)">
        <label class="radio-label">PNG</label>
      </li>
      <li class="file-type-option"<span style = "box-sizing: border-box! Important; word-wrap: break-word! Important;type="range" name="points" min="1" max="100" id="clarity" value="80" onChange="updateImage()" />
      </li>
    </ul>
  </div>
</div>
Copy the code

The layout is very similar to size-options, with the addition of a RANGE tag in HTML5, which is shown and hidden according to the fileType. Below that is the option to upload the image and the location of the online image. The code is as follows:

<div class="wrapper"<input class="hidden" type="file" id="file">
  <button class="btn m-b-10" onClick="showFileUpload()"<span style = "box-sizing: border-box; color: RGB (74, 74, 74); line-height: 20px; white-space: normal;"input-text" type="url" placeholder="Online Picture Address" onChange="getOnlineImage(value)">
  </div>
</div>
Copy the code

There is a hidden file control, because its style is very difficult to adjust, simply hide it and use the button below to control its click event. Online image addresses are also used in HTML5’s new URL space, but since they are not in a form, they don’t seem to be of much use. At the bottom is a display of the picture information. The code is as follows:

<div class="wrapper"<div class="display">
    <div class="img-details hidden"> <p> Picture information: </p> <p class="indent-2" id="img-size"></p>
      <p class="indent-2" id="img-origin-weight"></p>
      <p class="indent-2" id="img-now-weight"></p>
      <canvas id="canvas"></canvas>
    </div>
  </div>
</div>
Copy the code

This is even less obvious. Simple

tags are used to display image information. At this point, the contents of the wrapper on the left end. Below is the content of the instruction on the right.

Instruction and wrapper are the siblings in JQuery. There is less content in the instruction, which is the preview of the instructions and pictures. The code looks like this:

<div class="instruction"> <h4> <p class="tips"< span style = "box-sizing: border-box; color: RGB (74, 74, 74); font-size: 14px! Important; white-space: inherit! Important;"img-preview hidden">
    <button class="btn" onClick="downloadImage()"<p> Preview: </p> <img id="img-display" src="" alt="">
  </div>
</div>
Copy the code

Ok, that completes the HTML section. There are a lot of functions in the code above, so you can forget about them for the time being.

The specific implementation

Next, we will complete the offline image compression tool step by step from functional analysis to specific implementation.

Functional analysis

Function, in fact, the main is to upload and compress pictures, but compression needs to be in accordance with the needs of the user to the specific compression, size and clarity should be taken into account.

The second is how to give the user a feedback after the picture compression is completed, so that the user is aware that the picture has been compressed, here is divided into two parts, one is the intuitive picture display, one is the picture before and after the change of information display. In this way, whether professional users or ordinary users can clearly see the effect of compression.

Finally, and most important — the download feature. Without the download feature, the entire project simply doesn’t exist.

To sum up, we need to complete the following functions:

  1. Image upload
  2. Custom image size
  3. Image definition custom
  4. Preview picture
  5. Picture information display
  6. Images are downloaded

According to the page layout mentioned above, in order to comprehensively and reasonably display all the contents of the page, 1, 2, 3 and 5 are put on the left side, and the right side is a good choice to realize 4 and 6 because there is only a large space for the use of instructions.

Validation and update of public variables

The public variables are the configuration information entered by the user, the basic information of the image, and the basic HTML parts.

The user input information includes: custom width, custom height, compression degree, compression type.

Picture basic information: original width, original height.

The basic HTML widgets are: IMG, Canvas, download link.

The user input information and user base information are well understood. The basic HTML component is actually what is used to manipulate the images in this project. The IMG component is used to store the information related to the images uploaded by the user, and also the content of the online images if the user uploads the image address.

The Canvas component, needless to say, is used for drawing, and is the key to compressed images.

The last download link is to realize the image download function. The image information is stored in a tag, and then the simulation triggers click event to realize the image download.

Once you’ve figured out all the pieces, you can start the early stages of development by binding the common variables to the HTML. A change in the HTML content triggers a change in the public variables, which is a one-way binding.

First thought should be the top pictures custom size options, here is the width of high can not put into a global variable, because only in compressed images are these two parameters are used, other places not used completely, so directly inside the method to obtain good, saved into a global variable, confusing, in the same way and degree of compression parameters.

So here is the option to export the image. First of all, the format of the exported image is stored as imgType, and the initial value is JPEG. Then create a new method to synchronize the information and hide the image quality option when the option is PNG as follows:

const clarityWeightChange = (value) => {
  imgType = value;
  document.getElementsByClassName("file-type-option")[0].classList.toggle('hidden');
  updateImage()
}
Copy the code

First, assign the value to the imgType variable. Since there are only two options, you can simply toggle the hidden property. Then the graphics option box will switch back and forth between hide and show without judging whether the current value is PNG or JPG. Then bind the method to the onChange event of the DOM element:

<li>
  <input name="fileType" type="radio" value="jpeg" checked onChange="clarityWeightChange(value)">
  <label class="radio-label">JPG</label>
  <input name="fileType" type="radio" value="png" onChange="clarityWeightChange(value)">
  <label class="radio-label">PNG</label>
</li>
Copy the code

Img is used to store file information, and the reader is used to read the image information. After reading the image information, IMG can obtain the width and height of the image. So the question is how does the reader get the image information? This is where you need eleFile, which is used to get information to the input box of the file uploaded by the user.

Therefore, we first get the uploaded image information through eleFile, and then trigger the reader method to get the Base64 encoding of the image, because it is not available in eleFile. After the Base64 encoding is obtained by reader, the img method is triggered to obtain the original size of the uploaded image.

const reader = new FileReader();
const img = new Image();
const eleFile = document.getElementById("file"); Img. Onload = (image) => {originWidth = +image.path[0].width; originHeight = +image.path[0].height; }; // Assign image information to img reader.onload =function(e) { img.src = e.target.result; }; Elefile.addeventlistener ()'change', (event) => {
  reader.readAsDataURL(event.target.files[0]);
});
Copy the code

Through these three steps, you can get the original size of the image.

Then all that’s left is canvas and download link, which is nothing to say. The code looks like this:

const canvas = document.getElementById('canvas')
const context = canvas.getContext('2d');
const eleLink = document.createElement('a');
eleLink.style.display = 'none';
Copy the code

Because the display of the download link is meaningless, so directly hide it, download here may be a bit of a problem, but it does not affect the use, there will be a detailed explanation later.

The next step in the preparation is to upload the trigger of the file input. For style convenience, this input is hidden here. To trigger the input, you can only use the click event, so you need to create a method that triggers the click, and then bind this method to the upload button to trigger the perfect trigger.

// HTML
<button class="btn m-b-10" onClick="showFileUpload()"</button> // JS const showFileUpload = () => {document.getelementById ();'file').click();
}
Copy the code

At this point, the preparatory work for the example project has been completed, and now the main functional part of the project is developed.

Image compression and preview

At the beginning of the compressed image method, you should first obtain the custom sizing information entered by the user. Use the document.getelementById () method.

Then the target size is calculated. The logic here is that if the user enters only one of the dimensions, it automatically calculates the other half in proportion to the original scale of the image, which means it will scale at the same scale.

So there are four situations involved:

  1. The user filled in the custom width and height. In this case, simply assign the input value directly to the customWidth and customHeight variables.

  2. The user only filled in the custom width, not the height. In this case, the length-width ratio is calculated using the original dimensions and then multiplied by the custom width to obtain the corresponding height.

  3. The user only filled in the custom height, not the width. In this case, just calculate the corresponding width

  4. OriginWidth to targetWidth; originHeight to targetHeight; originWidth to targetWidth; originHeight to targetHeight

After solving these four situations, we can get the final target width and height. There are three kinds of width and height used here. In order to prevent confusion, the following explanations are given:

  1. OriginWidth /originHeight: The original width and height used to store images, updated in img.onload and triggered each time an image is uploaded.

  2. CustomWidth /customHeight: Used to store the user input width and height. This variable is only used in the compressed image method, and can be obtained internally by using document.getelementByid ().

  3. TargetWidth /targetHeight: Used to determine the size of the compression, because sometimes the user input incomplete information, may occur in the above four cases, so need to calculate the final length and width.

const customWidth = +document.getElementById('custom-width').value;
const customHeight = +document.getElementById('custom-height').value; // Determine the four conditions of width and height fillingif (customWidth && customHeight) {
  targetWidth = customWidth;
  targetHeight = customHeight;
} else if(customWidth && ! customHeight) { targetWidth = customWidth; targetHeight = Math.round(targetWidth * (originHeight / originWidth)); }else if(! customWidth && customHeight) { targetHeight = customHeight; targetWidth = Math.round(targetHeight * (originHeight / originWidth)); }else {
  targetWidth = originWidth;
  targetHeight = originHeight
}
Copy the code

Ok, now the width and height of the image are completely clear, update the canvas size to targetWidth/targetHeight, otherwise the image will not be compressed as a whole.

Context is the canvas obtained from Canvas’s getContext(‘2d’). Use context.clearRect(0, 0, targetWidth, targetHeight) to clear the canvas. The zero parameter above is used to locate the starting point of the canvas. The two zeros mean to start at the top left corner of the canvas, similar to the position in absolute positioning. The following parameter targetWidth/targetHeight determines the length and width of the canvas to be emptied, thus determining the size of the entire canvas.

DrawImage (img, 0, 0, targetWidth, targetHeight). The first parameter is the img component, which stores the base64 encoding of the image. The remaining arguments in the second are the same as those in the context.getContext() method and won’t be detailed here.

This is the final step, using the Canvas toDataURL() method to get the Base64 encoding of the compressed image.

Canvas. Width = targetWidth; canvas.height = targetHeight; ClearRect (0, 0, targetWidth, targetHeight); // Clear the canvas context.clearRect(0, 0, targetWidth, targetHeight); DrawImage (img, 0, 0, targetWidth, targetHeight); // Store the image base64 linklet imgCompressed = ' ';
if (imgType === 'png') {
  imgCompressed = canvas.toDataURL('image/png');
} else {
  imgCompressed = canvas.toDataURL('image/jpeg', +document.getElementById('clarity').value / 100); } // Preview the image document.getelementById ()'img-display').setAttribute('src', imgCompressed); ImgInfo = 'Image size:${targetWidth} * ${targetHeight}(length * Width) (unit: pixel) '; document.getElementById('img-size').innerHTML = imgInfo;
document.getElementsByClassName('img-details')[0].classList.remove('hidden');
document.getElementsByClassName('img-preview')[0].classList.remove('hidden');
Copy the code

In the above code, after the image is compressed, the base64 encoding of the image is placed in the IMG tag, which is used to display the compressed file, and the preview and download buttons are displayed for the user to see.

At this point, the image compression part has been completed, the rest of the only final image download function.

Image download and volume calculation

To complete the download, create a new tag, encode the compressed image base64 into the href property, add to the page, trigger its click event, and delete it.

The problem with this is that some people may find it cumbersome, because adding and removing elements is a bit of a chore. It’s ok, though, because it feels cleaner structurally by eliminating the need to add tags directly to the page.

The code implementation is as follows:

const eleLink = document.createElement('a');
eleLink.style.display = 'none'; // Confirm to download the link elelink.href = imgCompressed; // Confirm the download file name elelink.download = '${targetWidth}_${targetHeight}`; / / download file method is const downloadImage = () = > {/ / document to add elements. The body. The appendChild (eleLink); // Trigger to click elelink.click (); / / then remove document. Body. RemoveChild (eleLink); }Copy the code

File volume calculation in the above principle part has been very detailed, here only backward thinking can get the size of the file, but here is not according to the Different Windows and MAC systems to modify the base, unified is 1024 base, interested students can improve. The code is as follows:

Const getFileSize = (base64Url) => {// remove header information (data:image/ PNG; Base64,)let baseStr=base64Url.substring(base64Url.indexOf('base64,') +'base64,'.length); BaseStr = basestr. replace(/=/gi,' '); // Calculate const strLen= basestr.length;return strLen-(strLen/8)*2
}
Copy the code

After that we added the following file volume display:

Const updateImage = () => { // Store the image base64 linkif (imgType === 'png') {
    eleLink.href = canvas.toDataURL('image/png');
  } else {
    eleLink.href = canvas.toDataURL('image/jpeg', +document.getElementById('clarity').value / 100); } // Store the download file name elelink.download = '${targetWidth}_${targetHeight}`; ImgInfo = 'Image size:${targetWidth} * ${targetHeight}(length * Width) (unit: pixel) '; document.getElementById('img-size').innerHTML = imgInfo; Other content... }Copy the code

The updateImage method is the main way to compress the image. After you have done this, you need to bind it to all the related image configuration options. By modifying the configuration, the user can see the changes in the preview.

<ul>
  <li class="m-b-10"<span style = "box-sizing: border-box; color: RGB (74, 74, 74); line-height: 22px; white-space: inherit! Important;"input-text" type="text" id="custom-width" placeholder="Default to the original image size" onChange="updateImage()"Word-wrap: break-word! Important; "> <span style =" text-align: center"input-text" type="text" id="custom-height" placeholder="Default to the original image size" onChange="updateImage()">
  </li>
</ul>
Copy the code

The remaining options are export image type and graphics quality, which can be bound in turn.

other

The remaining part is to get the online image, here the method is very simple, assign the image address to the IMG part, the code is as follows:

Const getOnlineImage = (value) => {img.src = value; }Copy the code

Changing the SRC attribute of img automatically triggers the img.onload method, which will then naturally compress the image according to the current configuration.

Note that some images may cross domains. In this case, add a parameter to the IMG component, as follows:

// Enable the cross-domain img.setattribute ("crossOrigin".'Anonymous')
Copy the code

This can be a simple solution to cross-domain problems, of course, some pictures may still not get this, this is the need for more powerful functions to achieve, the author here a little lazy, so make do, later have time to change it.

This code is placed after the compression completion function of the updateImgae method:

Const updateImage = () => {other contents... // Store the download file name elelink.download = '${targetWidth}_${targetHeight}`; // Clear the current file path value to avoid the problem of not being able to upload the same image elefile.value =' '; Other content... }Copy the code

conclusion

Ok, here the whole project is finished, the difficulty is always, it is not a problem if you study it well. Demand confirmed more critical, I write using the method of “progressive enhancement”, gradually to increase more functions, the later will find a lot of code to use before, it is a waste of time, so I hope readers can draw lessons from the author’s and start developing before must want to good demand, otherwise will really waste a lot of time.

It’s been a long time, everybody!

PS

Zhang Xinxu bosses of two articles: www.zhangxinxu.com/wordpress/2… www.zhangxinxu.com/wordpress/2…