淺談Flutter web 圖片選擇器及圖片壓縮

做者簡介:Flutter小菜雞,5年經驗移動端開發工程師,努力成爲Flutter架構的小菜雞,現就任於某豐某大數據產品開發處,擔任移動端搬磚碼農,專一於移動端數據可視化研究,目前沒有任何能夠拿出來講的成績【手動狗頭】前端

木本水源篇

Flutter for web自發布以來,很多高等級的玩家已經對此進行了嚐鮮,評論也褒貶不一,有的說很難用,誰用誰知道。先不說好很差用,但從格局出發,Flutter的野心很大,想要在大前端領域實現 大一統,勇氣可嘉。Flutter for web整體上手感受,對於一個沒有web開發經驗的人來講,稍稍有一點點難度,但難度自己並非來自Flutter for web 框架,而是對web領域的未知,整體來講,作過Flutter app開發,上手web開發一些簡單的頁面是沒有問題的。可是也會有些很經常使用的進階一點的操做,好比操做數據庫,好比上傳圖片、或文件,拍照、定位獲取GIS信息等。其中在作圖片選擇上傳時,作圖片選擇並無什麼難度,圖片選擇庫支持web的也有幾個。可是在上傳後端時(起初方案不對,後面會介紹到)並無想象的那麼順利,覺得拍照的問題,圖片轉碼後的base64字符串會很是大,若是隻是在本地作轉碼解碼顯示的操做,Flutter的性能是徹底可以支撐的,而且不會形成卡頓,可是在和後臺交互時,可能會由於圖片過大,而致使接口緩慢。由於Flutter web生態環境的問題,不少圖片選擇庫,並不支持web項目,且Flutter 官方的image_picker_for_web,也是標註了[UNIDENTIFIED],本菜雞也是嘗試了不少種方法。最終在Flutter_luban 圖片壓縮庫的源碼中獲得了答案。git

不求甚解篇

囉嗦幾句,之後本菜雞寫文章都會分爲四個階段,第一階段,木本水源,主要簡單介紹問題的來源,或技一項技術的背景。第二階段,就是不求甚解,旨在快速針對問題,給出解決方案,和實現步驟。第三階段,叫作格物致知,丁肇中先生《應有格物致知精神》一文中對本菜雞受益頗深,作天然科學都要有格物致知的精神,雖然本菜雞隻是個小碼農,可是做爲一個理工男的職業素養仍是要有的。本階段旨在經過舉例或查看源碼,進行源碼解析,探究某項技術或功能的原理。第四階段,豹尾小結,少年時期老師做文講究鳳頭豹尾豬肚,沒錯本菜雞也是個講究人,最終總結是少不了的哈,也算是聊天吹水環節吧。github

Flutter for Web 的圖片選擇庫目前本菜雞接觸到有四個,分別是web

庫名 最新版本 -最後更新 GitHub Stars & Likes
image_picker_for_web 0.1.0+1 - [Jun 5, 2020] 官方維護
image_picker_web 1.0.9 - [May 16, 2020] 15
flutter_web_image_picker 0.0.2 - [Oct 8, 2019] 28
file_picker_cross 2.1.0 - [Jun 16, 2020] 15

通常本菜雞在爲了完成某一項工做,又不想造輪子,那就只好拿來主義(站在巨人的肩膀上),選用好用的第三方庫,本菜雞在選用第三方庫時,通常有幾個原則,不會選用剛上線,且迭代版本或提交數量小於5次的庫;不會選用年久失修的庫;如若以上都知足,優先選用官方維護庫,優先選擇==stars==或者==likes==比較多的庫。算法

因此在集成的過程當中,除了第三個庫沒有嘗試之外,都作了嘗試,總結下來比較推薦前兩個庫,分別是image_picker_for_webimage_picker_web,下面也會重點對這兩個庫的使用和優缺點進行介紹。數據庫

image_picker_for_web

先說第一個,官方維護的image_picker_for_web 這個庫在介紹文檔中也有提到,須要配合另外一個官方庫image_picker後端

This package is an ==unendorsed== web platform implementation of image_picker. In order to use this, you'll need to depend in image_picker: ^0.6.7 (which was the first version of the plugin that allowed federation), and image_picker_for_web: ^0.1.0.api

首先在pubspec.yaml文件中的dependencies中添加依賴,導入這兩個庫數組

...
dependencies:
  ...
  image_picker: ^0.6.7
  image_picker_for_web: ^0.1.0
  ...
...

複製代碼

在使用的地方導入文件瀏覽器

import 'package:image_picker/image_picker.dart';
複製代碼

==須要注意的是,image_picker從0.6.7以後使用方法有所變動,拋棄了以前ImagePicker().getImage 相似這樣的寫法==

這裏就只寫出最新的寫法:

File _image;
//須要先構造一個ImagePicker對象
final picker = ImagePicker();
//獲取圖片方法
Future getImage() async {
    //返回一個pickedFile對象
    final pickedFile = await picker.getImage(source: ImageSource.camera);
    //更新狀態
    setState(() {
      _image = File(pickedFile.path);
    });
}


複製代碼

須要注意的是getImage方法返回的是PickedFile類型的對象,跟File的關係能夠看一下官方給PickedFile的解釋

The interface for a PickedFile.

A PickedFile is a container that wraps the path of a selected file by the user and (in some platforms, like web) the bytes with the contents of the file.

This class is a very limited subset of dart:io [File], so all the methods should seem familiar.

根據字面意思很好理解,說PickedFile是Flie的一個有限子集,而且Flie class經常使用的屬性有path,經常使用的方法有readAsBytes()、openRead()等,在PickedFile中都有實現。

image_picker_web

此庫推薦的緣由是由於支持選擇返回類型,相比以前的庫多了一層封裝,暴露了一個ImgaeType給到咱們已於調用。這也是本菜雞認爲這個庫比較人性化的地方

用法:

首先在pubspec.yaml文件中的dependencies中添加依賴,導入這個庫

dependencies:
  image_picker_web: ^1.0.9

複製代碼

在使用的地方導入文件

import 'package:image_picker_web/image_picker_web.dart';
複製代碼
Image pickedImage;
pickImage() async {
    //ImageType一共有三種類型可選
    //file
    //bytes
    //widget
    Image fromPicker = await ImagePickerWeb.getImage(outputType: ImageType.widget);

    if (fromPicker != null) {
      setState(() {
        pickedImage = fromPicker;
      });
    }
  }

複製代碼

上面兩個庫不只是支持PC環境的web(目前只測試了Chrome瀏覽器) 圖片選擇,並且web項目run在手機上時,也是能夠訪問到相冊和相機,集成使用並沒有難度,因此用法介紹就到此結束了。

dart圖片壓縮

ok,集成完成以後,就要考慮適用性和優化的問題了。如今的手機像素都很高,拍一張無損高清照片,3-5M算是正常,可是即便再高清的圖片在微信的傳輸中是很是絲滑的,一方面是微信的圖片優化無疑是很是棒的,還有就是縮略圖和原圖分時異步加載,微信的原圖只有當你點擊下載原圖纔會從服務器下載,因此平時看到的都是微信已經壓縮過的圖片,內存已經很是小了,固然在圖片壓縮處理方面也是有不少優秀的第三方算法或者已經集成過的Flutter pub庫,好比luban(魯班)壓縮算法flutter_luban等。

固然了Flutter官方也會考慮到這個事情,因此在image_picker_for_web庫也是繼承了image_picker的屬性,可以傳入maxWidth maxHeight imageQuality三個屬性來約束圖片的大小和質量,可是不知爲什麼,在web項目上,這幾個屬性並無生效。已經在Github上的Flutter項目中提交了Issuee。或者有知道的巨佬也能夠告知一下本菜雞,還望不吝賜教。

其實圖片壓縮,自己並非很複雜的東西,在APP項目中使用MethodChannel調用native的壓縮api,其中flutter_image_compress庫就是這麼作的,固然還有藉助dart:Image的壓縮方法來實現的,此方式在web端和app端一樣適用,因此我在作圖片壓縮時,借鑑了flutter_luban庫中的源碼,使用dart自帶的壓縮方法,只在質量上進行了壓縮。(只壓縮了質量,圖片會失真)

import 'dart:convert';
import 'package:image/image.dart';

class ImageDelegate {
    //...
    //圖片壓縮部分代碼
    String compressImgage(List<int> data) {
        Image image = decodeImage(data);
        List<int> result = encodeJpg(image, quality: 70);
        String imageStr = base64.encode(result);
        return imageStr;
    }
}
複製代碼

須要注意的是,這裏用到的Image類型,是package:image/image.dart中的類型,並不是咱們經常使用的widget組件package:flutter/src/widgets/image.dart類型,因此建議把壓縮方法單獨寫一個工具類。這種方式就是運用的dart系統方法對圖片進行壓縮。

還有人會有疑問了,爲何不直接用flutter_image_compress相似的壓縮庫直接壓縮便可,在這琢磨什麼dart自帶的壓縮方法有什麼意義,須要瞭解的是flutter_image_compress等圖片壓縮庫目前所支持的平臺依舊是Android&iOS,因此web平臺是沒有辦法經過目前除了flutter_luban以外的庫進行壓縮的,由於目前圖片壓縮庫通常都是methodChannel調用原生API進行的壓縮,iOS代碼上通常調用的的是SDWebImgae的圖片壓縮方法,在Android代碼使用Android系統api。

格物致知篇

接下來的部分咱們就來詳細剖析一下,Flutter的圖片選擇器和上圖片壓縮的問題吧。本菜雞也是查閱了海量資料,寫了不少demo,有了一些本身的理解,接下來就講一下我本身的理解,你們一塊兒來探究一下圖片選擇器的一些細節問題和圖片壓縮的原理吧。發現問題的或者有不一樣意見的均可以私聊本菜雞微信,或者在留言,歡迎交流。

圖片選擇器的細節問題

先說一下圖片選擇器的流程,咱們手機中的圖片是怎麼轉換爲Image對象或者字節流而上傳到後臺呢?

image_picker爲例:

源碼解析

///method_channel_image_picker.dart

//...

@override
  Future<PickedFile> pickImage({
    @required ImageSource source,
    double maxWidth,
    double maxHeight,
    int imageQuality,
    CameraDevice preferredCameraDevice = CameraDevice.rear,
  }) async {
    String path = await pickImagePath(
      source: source,
      maxWidth: maxWidth,
      maxHeight: maxHeight,
      imageQuality: imageQuality,
      preferredCameraDevice: preferredCameraDevice,
    );
    return path != null ? PickedFile(path) : null;
  }


//核心方法
//maxWidth 返回圖片的最大寬度
//maxHeight 返回圖片的最大高度
//imageQuality 返回圖片的質量
// 在image_picker_for_web中,上面三個屬性失效(緣由未查明)
//經過MerthodChannel發起_channel.invokeMethod()調用 method name = 'pickImage'的通道方法。

  @override
  Future<String> pickImagePath({
    @required ImageSource source,
    double maxWidth,
    double maxHeight,
    int imageQuality,
    CameraDevice preferredCameraDevice = CameraDevice.rear,
  }) {
    assert(source != null);
    if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
      throw ArgumentError.value(
          imageQuality, 'imageQuality', 'must be between 0 and 100');
    }

    if (maxWidth != null && maxWidth < 0) {
      throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative');
    }

    if (maxHeight != null && maxHeight < 0) {
      throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative');
    }

    return _channel.invokeMethod<String>(
      'pickImage',
      <String, dynamic>{
        'source': source.index,
        'maxWidth': maxWidth,
        'maxHeight': maxHeight,
        'imageQuality': imageQuality,
        'cameraDevice': preferredCameraDevice.index
      },
    );
  }
複製代碼

前面有提到PickedFile類型的返回值,無論是Flie仍是PickedFile都有一個path屬性具體打印值爲: blob:http://localhost:62137/b7239cc7-ec85-40fc-a4c7-07f5b920771e,能夠看到是一個blob對象,前端範疇,爲此本菜雞特地百度了一下,此處的path爲何不是圖片的絕對路徑,且blob究竟是什麼玩意兒,本菜雞的理解爲,blob是基於瀏覽器內部對絕對路徑的一個封裝,防止爬蟲爬取數據,而且此連接只能在瀏覽器內部進行訪問,那就沒什麼問題了。

圖片壓縮原理及理解

這個話題提及來就很大了,先從圖片類型入手,列舉幾個圖片格式

  • ==「JPEG」格式==

JPEG格式,也叫作JPG或JPE格式,是最經常使用的一種文件格式,Photoshop「存儲爲」命令中默認的圖片格式就是JPEG,大部分手機相機拍照的照片也是JPE格式。

JPEG格式的壓縮技術十分先進,可以將圖像壓縮在很小的儲存空間,不過這種壓縮是有損耗的,過分壓縮會下降圖片的質量。JPEG格式壓縮的主要是高頻信息,對色彩的信息保留較好,所以特別適合應用於互聯網,可減小圖像的傳輸和加載時間。

  • ==「PNG」格式==

PNG也是常見的一種圖片格式,它最重要的特色是支持 alpha 通道透明度,也就是說,PNG圖片支持透明背景。好比在使用Photoshop製做透明背景的圓形logo時,若是使用JPG格式,則圖片背景會默認地存爲白色,使用PNG格式則能夠存爲透明背景圖片。

PNG格式圖片也支持有損耗壓縮,雖然PNG 提供的壓縮量比JPG少,但PNG圖片卻比JPEG圖片有更小的文檔尺寸,所以如今愈來愈多的網絡圖像開始採用PNG格式。

  • ==「GIF」格式==

GIF也是一種壓縮的圖片格式,分爲動態GIF和靜態GIF兩種。

GIF格式的最大特色是支持動態圖片,而且支持透明背景。網絡上絕大部分動圖、表情包都是GIF格式的,相比與動畫,GIF動態圖片佔用的存儲空間小,加載速度快,所以很是流行。

除了羅列的三種,還有==BMP、PSD、SVG==等圖片格式。

圖片壓縮的技術原理層面本菜雞在此就不作太多解釋了,感興趣的能夠看一下

圖像壓縮原理

JPEG壓縮原理與DCT離散餘弦變換

小蝌蚪傳記:PNG圖片壓縮原理--屌絲的眼淚 #1 (==關於圖片、色彩基礎理論、視頻等,在這篇文章最後有連接==)

咱們在此只關心Flutter端的圖片壓縮處理在dart層的展示,因爲前面說到flutter_image_compress是藉助native api實現的圖片壓縮,且目前只支持在APP端運行,最近一直在看dart源碼層面的東西,因此咱們仍是拿flutter_luban庫來進行源碼解析,由於只有flutter_luban庫是實現了web端的圖片壓縮。

其實flutter_luban庫並無很複雜的項目結構,只有一個flutter_luban.dart文件,只是魯班壓縮算法在Flutter端的移植,因此咱們直接貼出關鍵源碼逐句分析便可。

//魯班壓縮庫核心代碼
  static String _lubanCompress(CompressObject object) {
    //根據CompressObject對象中的File經過Uint8List的readAsBytesSync()方法獲取到List<int>數組
    //經過Image中的decodeImage()初始化image對象
    //注意:此Imgae對象爲'package:image/image.dart'中的對象,並不是咱們經常使用的Widget對象
    Image image = decodeImage(object.imageFile.readAsBytesSync());
    //獲取file長度並打印
    var length = object.imageFile.lengthSync();
    print(object.imageFile.path);

    bool isLandscape = false;
    //jpg類型數組
    const List<String> jpgSuffix = ["jpg", "jpeg", "JPG", "JPEG"];
    //png類型數組
    const List<String> pngSuffix = ["png", "PNG"];

    //調用_parseType()方法判斷圖片類型
    bool isJpg = _parseType(object.imageFile.path, jpgSuffix);
    bool isPng = false;

    if (!isJpg) isPng = _parseType(object.imageFile.path, pngSuffix);

    //初始化size width height
    double size;
    int fixelW = image.width;
    int fixelH = image.height;
    //
    double thumbW = (fixelW % 2 == 1 ? fixelW + 1 : fixelW).toDouble();
    double thumbH = (fixelH % 2 == 1 ? fixelH + 1 : fixelH).toDouble();
    //橫縱比
    double scale = 0;
    if (fixelW > fixelH) {
      scale = fixelH / fixelW;
      var tempFixelH = fixelW;
      var tempFixelW = fixelH;
      fixelH = tempFixelH;
      fixelW = tempFixelW;
      isLandscape = true;
    } else {
      scale = fixelW / fixelH;
    }
    var decodedImageFile;
    //目前只支持jpg和png的壓縮,不然拋出異常提示
    if (isJpg)
      decodedImageFile = new File(
          object.path + '/img_${DateTime.now().millisecondsSinceEpoch}.jpg');
    else if (isPng)
      decodedImageFile = new File(
          object.path + '/img_${DateTime.now().millisecondsSinceEpoch}.png');
    else
      throw Exception("flutter_luban don't support this image type");
    //同步檢查decodedImageFile文件是否存在
    if (decodedImageFile.existsSync()) {
      //同步刪除decodedImageFile文件
      decodedImageFile.deleteSync();
    }
    //根據圖片的橫縱比例和傳入的圖片大小從新計算圖片size
    var imageSize = length / 1024;
    if (scale <= 1 && scale > 0.5625) {
      if (fixelH < 1664) {
        if (imageSize < 150) {
          decodedImageFile
              .writeAsBytesSync(encodeJpg(image, quality: object.quality));
          return decodedImageFile.path;
        }
        size = (fixelW * fixelH) / pow(1664, 2) * 150;
        size = size < 60 ? 60 : size;
      } else if (fixelH >= 1664 && fixelH < 4990) {
        thumbW = fixelW / 2;
        thumbH = fixelH / 2;
        size = (thumbH * thumbW) / pow(2495, 2) * 300;
        size = size < 60 ? 60 : size;
      } else if (fixelH >= 4990 && fixelH < 10240) {
        thumbW = fixelW / 4;
        thumbH = fixelH / 4;
        size = (thumbW * thumbH) / pow(2560, 2) * 300;
        size = size < 100 ? 100 : size;
      } else {
        int multiple = fixelH / 1280 == 0 ? 1 : fixelH ~/ 1280;
        thumbW = fixelW / multiple;
        thumbH = fixelH / multiple;
        size = (thumbW * thumbH) / pow(2560, 2) * 300;
        size = size < 100 ? 100 : size;
      }
    } else if (scale <= 0.5625 && scale >= 0.5) {
      if (fixelH < 1280 && imageSize < 200) {
        decodedImageFile
            .writeAsBytesSync(encodeJpg(image, quality: object.quality));
        return decodedImageFile.path;
      }
      int multiple = fixelH / 1280 == 0 ? 1 : fixelH ~/ 1280;
      thumbW = fixelW / multiple;
      thumbH = fixelH / multiple;
      size = (thumbW * thumbH) / (1440.0 * 2560.0) * 200;
      size = size < 100 ? 100 : size;
    } else {
      int multiple = (fixelH / (1280.0 / scale)).ceil();
      thumbW = fixelW / multiple;
      thumbH = fixelH / multiple;
      size = ((thumbW * thumbH) / (1280.0 * (1280 / scale))) * 500;
      size = size < 100 ? 100 : size;
    }
    //若是原始圖片size小於計算完畢後圖片size
    //則調用Image encodeJpg()方法進行質量壓縮,並同步寫入緩存且返回路徑
    if (imageSize < size) {
      decodedImageFile
          .writeAsBytesSync(encodeJpg(image, quality: object.quality));
      return decodedImageFile.path;
    }
    //若是原始圖片size大於計算完畢後圖片size
    //根據橫豎方向,調用copyResize()方法重設寬高屬性給smallerImage賦值
    Image smallerImage;
    if (isLandscape) {
      smallerImage = copyResize(image,
          width: thumbH.toInt(),
          height: object.autoRatio ? null : thumbW.toInt());
    } else {
      smallerImage = copyResize(image,
          width: thumbW.toInt(),
          height: object.autoRatio ? null : thumbH.toInt());
    }

    if (decodedImageFile.existsSync()) {
      decodedImageFile.deleteSync();
    }
    //根據傳入的CompressMode枚舉類型,調用對應的CompressImage()方法
    //本質都是調用Image encodeJpg()方法進行質量壓縮,只是在image size上作了調整
    if (object.mode == CompressMode.LARGE2SMALL) {
      _large2SmallCompressImage(
        image: smallerImage,
        file: decodedImageFile,
        quality: object.quality,
        targetSize: size,
        step: object.step,
        isJpg: isJpg,
      );
    } else if (object.mode == CompressMode.SMALL2LARGE) {
      _small2LargeCompressImage(
        image: smallerImage,
        file: decodedImageFile,
        quality: object.step,
        targetSize: size,
        step: object.step,
        isJpg: isJpg,
      );
    } else {
      if (imageSize < 500) {
        _large2SmallCompressImage(
          image: smallerImage,
          file: decodedImageFile,
          quality: object.quality,
          targetSize: size,
          step: object.step,
          isJpg: isJpg,
        );
      } else {
        _small2LargeCompressImage(
          image: smallerImage,
          file: decodedImageFile,
          quality: object.step,
          targetSize: size,
          step: object.step,
          isJpg: isJpg,
        );
      }
    }
    return decodedImageFile.path;
  }

複製代碼

==圖片壓縮步驟總結:==

  1. 傳入圖片CompressObject對象(此對象爲魯班庫自定義對象類型),主要理解爲傳入圖片path便可

  2. 根據圖片路徑獲取Uint8List並轉換爲'package:image/image.dart'庫對應的Image對象。

  3. 魯班壓縮算法,主要用於圖片size計算,縱橫比比例分爲四個區間,分別計算出結果size

  4. 利用copyResize()方法傳入計算結果size生成smallImage對象

  5. 利用dart原生api encodeJpg()方法進行質量壓縮

豹尾小結篇

這篇文章主要是針對Flutter for web的圖片選擇及壓縮,經過對比圖片選擇庫,圖片壓縮庫,進行了源碼分析,並列出了圖片壓縮的大概步驟。整體來講仍是比較詳細的分析了圖片選擇和壓縮的過程及步驟,包括dart層面的實現。在作圖片轉碼的過程,是曲折又辛酸的,確實找了不少資料,看了不少博客軟文,仍是資料太少,不【science network】的話,侷限性太大了。雖然Flutter的入門文章教程不少,可是有深度、有質量的文章仍是少了一些,特別是Flutter for web,可能你們都是在嘗試的緣由。Flutter可否一統前端,就要看你們的努力了,讓咱們一塊兒爲Flutter生態建設添磚加瓦吧~

我是努力成爲Flutter架構的Flutter小菜雞,我爲本身帶鹽!

相關文章
相關標籤/搜索