JS脫機版:圖片壓縮工具

前言

最近項目中對圖片的要求比較高,常常會進行圖片壓縮和修改分辨率的操做,長此以往就以爲本身寫一個吧,因而花了一天的功夫完成了這個脫機版圖片壓縮工具,無需服務器,本地便可運行。html

參考了張鑫旭大佬的兩篇文章,連接放在文章的最下方,感興趣的能夠深刻了解下。html5

項目Github地址git

效果展現

首先看看圖片上傳的效果:github

再看看在線獲取圖片的效果:json

CSS部分不是這次討論的重點,能夠暫時不用考慮,爲了美觀,這裏參考了antd的部分組件樣式。canvas

原理介紹

原理其實很簡單,就是canvas的應用。用戶上傳圖片以後咱們能夠拿到圖片的base64內容,以後使用canvasdrawImage方法畫出圖片,再使用drawImage方法時,能夠定義圖片的寬高和圖片質量,不過須要注意的時這裏的圖片質量選項以後在導出圖片爲jpg時纔能有用,導出格式爲png時是沒有任何做用的。windows

圖片大小的計算實際上是經過圖片的base64內容計算出來的。這裏就要涉及一點base64原理了,拿出小本本記一下:跨域

base64的出現是由於要兼容除了英文之外的其餘語言,由於中文或者日文沒法在服務器或者網管上進行有效的處理,常常會出現亂碼,這時base64就出現了,轉化成了一串編碼就能夠隨意傳輸了,接收到以後再翻譯一下就好。bash

base64的原理比較簡單,base64有一個本身的表,裏面每一個字節都有着本身的代號。服務器

首先,將須要待轉換的字符串分紅三個一組,每一個字節的大小時8bit,那麼三個字節就有24個二進制位。

而後,再將上面的24個二級制位分紅4組,每組6個。

接下來,在每組前面增長兩個0,因而每組變成了8個二級製爲,4組總共32個二進制位,總共4個字節。

最後,根據以前說過的base64轉換表,將這些二級制位翻譯一下,就獲得了最終的base64字符串。

那麼這裏有兩個問題:首先,由於在每組以前都增長了兩個0,因此base64編碼以後的文本會比原生文本大三分之一左右。其次就是爲何要使用3個字節一組呢?那是由於6和8的最小公約數時24,3個8和4個6正好都是24。

還有一個特殊狀況就是萬一有的位數不足怎麼辦呢?分的時候可能字節數不足三個。若是字節數是2個,能夠拿到16個二進制位,6個一組以後,最後一組差兩個,用0補齊正好3組,但是第四組呢?這時候就須要用=來僞裝這裏是一個組了,強行湊夠4組。如果只有一個字節數,那麼12除以6等於2,還差兩個組才能到四,因此須要兩個=來湊個四個組。爲了4組也是蠻拼的。因此說base64編碼中可能會出現一到兩個=

知道這些以後咱們就能夠反向計算文件體積了,代碼以下所示:

const getFileSize = (base64Url) => {
  //  去掉無用頭部信息(data:image/png;base64,)
  let baseStr=base64Url.substring(base64Url.indexOf('base64,')+'base64,'.length);
  //  去掉」=「
  baseStr = baseStr.replace(/=/gi, '');
  // 進行計算
  const strLen=baseStr.length;
  return strLen-(strLen/8)*2
}
複製代碼

首先去掉頭部的類型標誌,pngjpeg之類的標識。接下來用正則去掉等號。最後減去填充的0。每8字符串就有兩個0,因此用總體長度除以8,再乘以2,能夠獲得全部0的個數。用整體的長度減去0的個數,便可獲得生下的字節數,也就是真正的字節數,再除以1024,獲得最終的大小,單位爲KB

須要注意的是MACwindows的文件體積計算方式不一樣,MAC是1000進制的,而windows是1024進制的,全部會有一些區別,這個區別從MAC硬盤的容量基本都接近足量也能夠看出來。

頁面佈局

明顯能夠看出來,整個頁面由兩部分構成,左側是圖片壓縮信息修改的部分,右側是使用說明和預覽部分。

首先左側是以一個wrapper,用來包裹左側全部內容。裏面分爲若干個小部分,首先是最上方的圖片自定義寬高部分,代碼以下所示:

<div class="wrapper">
  <div class="size-options">
    <p class="sub-title">圖片自定義寬高</p>
    <ul>
      <li class="m-b-10">
        <span>寬度:</span>
        <input class="input-text" type="text" id="custom-width" placeholder="默認值爲圖片原始尺寸" onChange="updateImage()">
      </li>
      <li>
        <span>高度:</span>
        <input class="input-text" type="text" id="custom-height" placeholder="默認值爲圖片原始尺寸" onChange="updateImage()">
      </li>
    </ul>
  </div>
</div>
複製代碼

很簡單的HTML,兩個input而已。在size-options下面又增長了處處圖片尺寸的配置。代碼以下:

<div class="wrapper">
  // 其餘內容
  <div class="clarity-options">
    <p class="sub-title">導出圖片尺寸</p>
    <ul>
      <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>
      <li class="file-type-option">
        <span>圖形質量:</span>
        <input type="range" name="points" min="1" max="100" id="clarity" value="80" onChange="updateImage()" />
      </li>
    </ul>
  </div>
</div>
複製代碼

佈局和size-options十分相似,多了一個HTML5中的range標籤,會根據fileType的類型來展現和隱藏。再下面就是上傳圖片和在線圖片地址的選項。代碼以下:

<div class="wrapper">
  // 其餘內容
  <input class="hidden" type="file" id="file">
  <button class="btn m-b-10" onClick="showFileUpload()">上傳圖片</button>
  <div>
    <span>在線圖片:</span>
    <input class="input-text" type="url" placeholder="在線圖片地址" onChange="getOnlineImage(value)">
  </div>
</div>
複製代碼

有一個隱藏的file控件,由於其樣式十分很差調整,乾脆就隱藏掉,用下面的button來控制其click事件。在線圖片地址用的也是HTML5url新空間,但由於不在一個form中,因此好像用處不大,用text也沒什麼影響。最下面是圖片信息的展現。代碼以下:

<div class="wrapper">
  // 其餘內容
  <div class="display">
    <div class="img-details hidden">
      <p>圖片信息:</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>
複製代碼

這就更沒什麼可說的,簡單<p>標籤,用來展現圖片信息。至此,左側wrapper的內容就完結了。下面是右側instruction的內容。

instructionwrapper是並列關係,也就是JQuery中的siblingsinstruction中的內容比較少,也就是使用說明和圖片的預覽。代碼以下所示:

<div class="instruction">
  <h4>脫機版圖片壓縮:使用說明</h4>
  <p class="tips">
    1.選擇圖片質量,<strong>注意:只有JPG格式能夠調整圖片質量,PNG格式沒法調整</strong>
  </p>
  <div class="img-preview hidden">
    <button class="btn" onClick="downloadImage()">下載圖片</button>
    <p>預覽:</p>
    <img id="img-display" src="" alt="">
  </div>
</div>
複製代碼

ok,到這裏HTML部分的內容就完成了,上面的代碼中有不少function,暫時能夠先無論了,下面會一一進行講解。

具體實現

下面咱們將從功能分析到具體的的實現來一步步完成這個脫機的圖片壓縮工具。

功能分析

功能嘛,其實主要就是上傳而且壓縮圖片,但壓縮的時候須要按照用戶的需求來具體的壓縮,尺寸和清晰度都要考慮到。

其次就是圖片壓縮完成後怎麼給用戶一個反饋,讓用戶意識到圖片已經壓縮完成了,這裏分紅兩個部分,一個是直觀的圖片展現,一個是圖片轉換先後信息變化的展現。如此,無論是專業的用戶仍是普通用戶均可以明顯的看到壓縮的效果。

最後,也是最重要的——下載功能。沒有下載功能整個項目就等於不存在了。

綜上所述,咱們須要完成如下幾點功能:

  1. 圖片上傳
  2. 圖片尺寸自定義
  3. 圖片清晰度自定義
  4. 圖片預覽
  5. 圖片信息展現
  6. 圖片下載

那麼根據上面提到的頁面佈局,爲了全面、合理的展現頁面的所有內容,這裏將一、二、三、5放在了左側,右側由於只有使用說明,空間比較大,用來實現4和6是一個比較好的選擇。

公共變量的確認與更新

公共變量主要是用戶輸入的配置信息、圖片的基礎信息和基礎HTML部件。

用戶輸入的信息有:自定義寬度、自定義高度、壓縮程度、壓縮類型。

圖片基礎信息有:原始寬度、原始高度。

基礎HTML部件有:imgcanvas、下載連接。

用戶輸入信息和用戶基礎信息很好理解,基礎的HTML部件其實就是此次項目中用來實際操做圖片的東西,img部件用來存儲用戶上傳的圖片相關信息,同時如果用戶上傳圖片地址的話,也能夠存儲在線圖片的內容。

canvas部件不用多說,是用來畫圖的,是壓縮圖片的關鍵所在。

最後的下載連接爲的是實現圖片下載功能,將圖片信息存在一個<a>標籤中,以後模擬觸發click事件,來實現圖片的下載。

在弄清楚全部的部件以後便可開始開發初期的準備工做了,將公用變量和HTML綁定起來,HTML中內容的變化會觸發公共變量的變化,也就是一個單向綁定。

首先想到的應該就是最上方圖片自定義尺寸選項,其實這裏的寬高能夠不存成全局變量,由於只有在壓縮圖片是纔會用到這兩個參數,其餘地方徹底不會用到,因此直接在方法內部獲取就行了,省了存成全局變量,混淆視聽,同理還有壓縮程度參數。

那麼下面就是導出圖片選項了,首先是導出圖片的格式,這裏存成imgType,初始值是jpeg。而後新建方法來同步信息,而且在選項爲PNG時,隱藏圖形質量選項,代碼以下:

const clarityWeightChange = (value) => {
  imgType = value;
  document.getElementsByClassName("file-type-option")[0].classList.toggle('hidden');
  updateImage()
}
複製代碼

首先賦值給imgType變量,由於只有兩個選項,因此直接toggle一下hidden屬性便可,那麼圖形選項框就會在隱藏和展現之間來回切換,無需判斷當前的值是PNG仍是JPG了。再在DOM元素的onChange事件上綁定該方法便可:

<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>
複製代碼

圖片基礎信息的獲取,這一步相對來講比較複雜,覺得要一層層觸發,首先須要定義imgreader兩個部件,上面說過img是用來存儲文件信息的,那麼reader是用來讀取圖片信息的,在讀圖片信息以後img才能獲取到圖片的寬高信息。那麼問題來了reader是怎樣獲取到圖片信息的呢?這時候就須要eleFile了,eleFile是用來獲取到用戶上傳文件的input輸入框的信息。

因此就是首先經過eleFile獲取上傳的圖片信息,接下來觸發reader的方法,獲取到圖片的base64編碼,由於在eleFile中是沒法獲取到的。在reader獲取到base64編碼後,再觸發img方法,此時便可獲取到上傳圖片的原始尺寸。

const reader = new FileReader();
const img = new Image();
const eleFile = document.getElementById("file");

// 文件base64化,獲取圖片原始尺寸
img.onload = (image) => {
  originWidth = +image.path[0].width;
  originHeight = +image.path[0].height;
};
// 賦值圖片信息給img
reader.onload = function(e) {
  img.src = e.target.result;
};
// 讀取原始文件信息
eleFile.addEventListener('change', (event) => {
  reader.readAsDataURL(event.target.files[0]);
});
複製代碼

經過這三步便可獲取圖片的原始尺寸。

那麼最後剩下的就是canvas和下載連接了,這沒啥好說的,代碼以下所示:

const canvas = document.getElementById('canvas')
const context = canvas.getContext('2d');
const eleLink = document.createElement('a');
eleLink.style.display = 'none';
複製代碼

由於下載連接的展現毫無心義,因此直接隱藏就好,下載這裏可能會有點問題,不過不影響使用,後面會有詳細解釋。

準備工做還有一步就是上傳文件input的觸發,爲了樣式方便,這裏將該input隱藏,要想觸發只能經過click事件,因此須要新建一個觸發點擊的方法,以後再上傳按鈕上綁定該方法,便可完美觸發,代碼以下:

// HTML
<button class="btn m-b-10" onClick="showFileUpload()">上傳圖片</button>

// JS
const showFileUpload = () => {
  document.getElementById('file').click();
}
複製代碼

至此,舉例項目前期的準備工做都已經完成了,下面開發項目的主體功能部分。

圖片的壓縮與預覽

在壓縮圖片方法的一開始,應該先獲取的用戶輸入的自定義尺寸信息。使用document.getElementById()方法便可。

以後進行目標尺寸的計算。此處的邏輯是若是用戶只輸入款寬高中的一個是,自動很足圖片原始比例等比計算出另外一半的長度,也就是說會保持比例不變進行縮放。

那麼這是涉及到4種狀況:

  1. 用戶把自定義寬高都填了。這種狀況直接將input值直接賦值給customWidthcustomHeight變量便可。

  2. 用戶只填寫了自定義寬度,高度沒填。這種狀況用原始尺寸計算出長寬比,以後乘上自定義寬度,便可得出對應的高度。

  3. 用戶只填寫了自定義高度,寬度沒填。這種狀況同上,計算出對應寬度便可

  4. 用戶啥都沒填,這就簡單了,直接將原始寬高賦值給自定義寬高便可,就也是將originWidth賦值給targetWidth,將originHeight賦值給targetHeight

解決完這四種狀況後咱們便可拿到最終的目標寬高,此處用到的寬高共有三種,爲了防止混淆,下面一一解釋:

  1. originWidth/originHeight:用來存儲圖片的原生寬高,在img.onload中進行更新,每次上傳圖片都會觸發更新。

  2. customWidth/customHeight:用來存儲用戶輸入的寬高,此變量只在壓縮圖片方法中才會用到,在其內部直接使用document.getElementById()獲取便可。

  3. targetWidth/targetHeight:用來肯定壓縮是的尺寸,由於用戶有時輸入的信息不全,可能會出現上面的4種狀況,因此須要計算得出最終長寬。

const customWidth = +document.getElementById('custom-width').value;
const customHeight = +document.getElementById('custom-height').value;
//  判斷寬高填寫的四種狀況
if (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
}
複製代碼

Ok,如今圖片的寬高已經徹底弄清楚了,先更新下畫布大小,將其改成targetWidth/targetHeight,不然圖片沒法總體壓縮。

下面使用context進行圖片的繪製,context是由canvasgetContext('2d')獲得的一塊畫布,首先使用context.clearRect(0, 0, targetWidth, targetHeight)清空畫布,上面0參數的做用是定位畫布的起點,兩個0的意思就是從畫布的左上角開始,有點相似於絕對定位中的位置,以後的targetWidth/targetHeight參數是肯定應該清空畫布的長寬,從而肯定整張畫布的大小。

下面使用context.drawImage(img, 0, 0, targetWidth, targetHeight)方法來進行圖片的繪製,第一個參數就是上文提到的img部件,裏面存儲着圖片的base64編碼,第二個剩下的參數和context.getContext()方法中的參數同樣,在此很少作贅述。

下面就是最後一步了,使用canvastoDataURL()方法獲得壓縮後圖片的base64編碼。改方法接收兩個參數,第一個是壓縮後的圖片類型,比方說image/pngimage/jpeg等,第二個參數就是圖片的質量,是一個從0到1的小數,數字越大越清晰,此處能夠從上面的<input type='range'>中拿到改變量。

// canvas對圖片進行縮放
canvas.width = targetWidth;
canvas.height = targetHeight;
// 清除畫布
context.clearRect(0, 0, targetWidth, targetHeight);
// 圖片壓縮
context.drawImage(img, 0, 0, targetWidth, targetHeight);
// 存儲圖片base64連接
let imgCompressed = '';
if (imgType === 'png') {
  imgCompressed = canvas.toDataURL('image/png');
} else {
  imgCompressed = canvas.toDataURL('image/jpeg', +document.getElementById('clarity').value / 100);
}
// 圖片預覽
document.getElementById('img-display').setAttribute('src', imgCompressed);
// 圖片信息展現
imgInfo = `圖片尺寸:${targetWidth} * ${targetHeight} (長 * 寬)(單位:像素)`;
document.getElementById('img-size').innerHTML = imgInfo;
document.getElementsByClassName('img-details')[0].classList.remove('hidden');
document.getElementsByClassName('img-preview')[0].classList.remove('hidden');
複製代碼

在上面的代碼中,完成圖片壓縮後將圖片的base64編碼放到了img標籤中,用來展現壓縮後的文件,同時將預覽和下載按鈕展現出來,方面用戶查閱。

至此,圖片壓縮部分已經完成了,剩下的就只有最後的圖片下載功能了。

圖片的下載與體積的計算

圖片下載的原理是新建一個<a>標籤,以後將壓縮後的圖片base64編碼放到<a>href屬性中,以後將<a>添加到頁面中,觸發其click事件,再將其刪掉,便可完成這次下載。

這樣作的問題是可能會有的朋友以爲很麻煩,由於增長和刪除元素的操做有點費勁。其實也還好,感受這樣作省去了直接在頁面上增長<a>標籤,結構上會更整潔些。

代碼實現以下所示:

const eleLink = document.createElement('a');
eleLink.style.display = 'none';
//  肯定下載連接
eleLink.href = imgCompressed;
//  肯定下載文件名
eleLink.download = `${targetWidth}_${targetHeight}`;
//  下載文件方法
const downloadImage = () => {
  //  添加元素
  document.body.appendChild(eleLink);
  //  觸發點擊
  eleLink.click();
  //  而後移除
  document.body.removeChild(eleLink);
}
複製代碼

文件體積的計算在上面的原理部分已經將的很詳細了,這裏只需反向思考便可得出文件的大小,不過這裏沒有根據windowsMAC系統的不一樣來修改進制,統一是1024的進制,有興趣的同窗能夠改進下。代碼以下:

const getFileSize = (base64Url) => {
  //  去掉無用頭部信息(data:image/png;base64,)
  let baseStr=base64Url.substring(base64Url.indexOf('base64,')+'base64,'.length);
  //  使用正則去掉」=「
  baseStr = baseStr.replace(/=/gi, '');
  //  進行計算
  const strLen=baseStr.length;
  return strLen-(strLen/8)*2
}
複製代碼

以後咱們增長如下文件體積的展現:

const updateImage = () => {
  繪製圖片內容...
  // 存儲圖片base64連接
  if (imgType === 'png') {
    eleLink.href = canvas.toDataURL('image/png');
  } else {
    eleLink.href = canvas.toDataURL('image/jpeg', +document.getElementById('clarity').value / 100);
  }
  // 存儲下載文件名
  eleLink.download = `${targetWidth}_${targetHeight}`;
  // 圖片信息展現
  imgInfo = `圖片尺寸:${targetWidth} * ${targetHeight} (長 * 寬)(單位:像素)`;
  document.getElementById('img-size').innerHTML = imgInfo;
  其餘內容...
}
複製代碼

updateImage方法就是壓縮圖片的主要方法,在完成這個方法後,須要將其綁定在全部相關圖片配置的選項上,如此一修改配置,用戶便可看見預覽圖的變化,舉個例子:

<ul>
  <li class="m-b-10">
    <span>寬度:</span>
    <input class="input-text" type="text" id="custom-width" placeholder="默認值爲圖片原始尺寸" onChange="updateImage()">
  </li>
  <li>
    <span>高度:</span>
    <input class="input-text" type="text" id="custom-height" placeholder="默認值爲圖片原始尺寸" onChange="updateImage()">
  </li>
</ul>
複製代碼

剩下的還有導出圖片類型和圖形質量選項,依次綁定便可。

其餘

剩下的部分就是在線圖片的獲取了,這裏的方法十分簡單,將圖片地址賦值給img部件便可,代碼以下:

//  獲取在線圖片地址
const getOnlineImage = (value) => {
  img.src = value;
}
複製代碼

修改imgsrc屬性會自動觸發img.onload方法,以後也會順其天然的根據當前配置來進行圖片的壓縮。

須要注意的一點就是有的圖片可能會跨域,此時須要給img部件增長一個參數,以下:

//  開啓圖片地址的跨域
img.setAttribute("crossOrigin",'Anonymous')
複製代碼

如此便可簡單的解決跨域問題,固然了,有些圖片這樣可能仍是獲取不了,這是就須要更增強大的功能來實現了,筆者這裏有點懶,就先這樣湊合了,之後有時間再改吧。

還有一點就是<input type='file'>元素會出現相同文件上傳不變化的問題,這就有點尷尬了,能夠將eleFile部件的value屬性置空,讓它覺得本身目前沒有圖片,這樣再次上傳相同文件也就沒有問題了。代碼放在了updateImgae方法壓縮完成功能的後面:

const updateImage = () => {
  其餘內容...
  // 存儲下載文件名
  eleLink.download = `${targetWidth}_${targetHeight}`;
  // 清除當前文件的路徑值,避免不能上傳同一張圖片的問題
  eleFile.value = '';
  其餘內容...
}
複製代碼

總結

好啦,到了這裏整個項目也就完成了,難度始終,好好去研究的話問題不大。需求的確認比較關鍵,筆者寫的時候使用的事「漸進加強」的方法,逐步增長更多的功能,到了後面會發現以前的代碼不少都沒有用,這就比較浪費時間了,因此但願各位讀者能夠吸收筆者的教訓,開始開發以前必定要想好需求,不然真的會浪費不少時間的。

看了這麼久,辛苦了,諸位!

PS

張鑫旭大佬的兩篇文章:
www.zhangxinxu.com/wordpress/2…
www.zhangxinxu.com/wordpress/2…

相關文章
相關標籤/搜索