JS 图片简易压缩【实践】

转发链接:

前言

说起图片压缩,大家想到的或者平时用到的很多工具都可以实现,例如,客户端类的有图片压缩工具 PPDuck3, JS 实现类的有插件 compression.js ,亦或是在线处理类的 OSS 上传,文件上传后,在访问文件时中也有图片的压缩配置选项,不过,能不能自己撸一套 JS 实现的图片压缩代码呢?当然可以,那我们先来理一下思路。

压缩思路

涉及到 JS 的图片压缩,我的想法是需要用到 Canvas 的绘图能力,通过调整图片的分辨率或者绘图质量来达到图片压缩的效果,实现思路如下:

优缺点介绍

不过 Canvas 压缩的方式也有着自己的优缺点:

代码实现



复制代码

上面的代码是可以直接拿来看效果的,不喜欢用 Vue 的也可以把代码稍微调整一下,下面开始具体分解一下代码的实现思路。

Input 上传 File 处理

将 File 对象通过 FileReader 的 readAsDataURL 方法转换为URL格式的字符串(base64编码)。

const fileObj = document.querySelector('#input-img').files[0];
let reader = new FileReader();
// 读取文件
reader.readAsDataURL(fileObj);
复制代码

Canvas 处理 File 对象

建立一个 Image 对象,一个 canvas 画布,设定自己想要下载的图片尺寸,调用 drawImage 方法在 canvas 中绘制上传的图片。

let image = new Image(); //新建一个img标签
image.src = e.target.result;
let canvas = document.createElement('canvas');
let context = canvas.getContext('2d');
context.drawImage(image, 0, 0);
复制代码

Api 解析:drawImage

context.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
复制代码

img

就是图片对象,可以是页面上获取的 DOM 对象,也可以是虚拟 DOM 中的图片对象。

dx , dy , dWidth , dHeight

表示在 canvas 画布上规划出一片区域用来放置图片,dx, dy 为绘图位置在 Canvas 元素的 X 轴、Y 轴坐标,dWidth, dHeight 指在 Canvas 元素上绘制图像的宽度和高度(如果不说明, 在绘制时图片的宽度和高度不会缩放)。

sx , sy , swidth , sheight

这 4 个参数是用来裁剪源图片的,表示图片在 canvas 画布上显示的大小和位置。sx,sy 表示在原图片上裁剪位置的 X 轴、Y 轴坐标,然后以 swidth,sheight 尺寸来选择一个区域范围,裁剪出来的图片作为最终在 Canvas 上显示的图片内容( swidth,sheight 不说明的情况下,整个矩形(裁剪)从坐标的 sx 和 sy 开始,到图片的右下角结束)。

以下为图片绘制的实例:

context.drawImage(image, 0, 0, 100, 100);
context.drawImage(image, 300, 300, 200, 200);
context.drawImage(image, 0, 100, 150, 150, 300, 0, 150, 150);
复制代码

Api 中奇怪之处在于,sx,sy,swidth,sheight 为选填参数,但位置在 dx, dy, dWidth, dHeight 之前。

Canvas 输出图片

调用 canvas 的 toDataURL 方法可以输出 base64 格式的图片。

canvas.toDataURL(`image/${type}`);
复制代码

Api 解析:toDataURL

canvas.toDataURL(type, encoderOptions);
复制代码

type 可选

图片格式,默认为 image/png。

encoderOptions 可选

在指定图片格式为 image/jpeg 或 image/webp的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。

a 标签的下载

调用 标签的 download 属性,即可完成图片的下载。

Api 解析:download

// href 下载必填
 下载 
复制代码

filename

选填,规定作为文件名来使用的文本。

href

文件的下载地址。

非主流浏览器下载处理

到此可以解决 Chroma 、 Firefox 和 Safari(自测支持) 浏览器的下载功能,因为 IE 等浏览器不支持 download 属性,所以需要进行其他方式的下载,也就有了代码中的后续内容。

// base64 图片转 blob 后下载
downloadImg() {
  let parts = this.compressImg.split(';base64,');
  let contentType = parts[0].split(':')[1];
  let raw = window.atob(parts[1]);
  let rawLength = raw.length;
  let uInt8Array = new Uint8Array(rawLength);
  for(let i = 0; i < rawLength; ++i) {
    uInt8Array[i] = raw.charCodeAt(i);
  }
  const blob = new Blob([uInt8Array], {type: contentType});
  this.compressImg = URL.createObjectURL(blob);
  if (window.navigator.msSaveOrOpenBlob) {
    // 兼容 ie 的下载方式
    window.navigator.msSaveOrOpenBlob(blob, this.fileName);
  }else{
    const a = document.createElement('a');
    a.href = this.compressImg;
    a.setAttribute('download', this.fileName);
    a.click();
  }
}
复制代码

Api 解析:atob

base-64 解码使用方法是 atob()。

window.atob(encodedStr)
复制代码

encodedStr

必需,是一个通过 btoa() 方法编码的字符串,btoa()是 base64 编码的使用方法。

Api 解析:Uint8Array

new Uint8Array(length)
复制代码

length

创建初始化为 0 的,包含 length 个元素的无符号整型数组。

Api 解析: Blob

Blob 对象表示一个不可变、原始数据的类文件对象。

// 构造函数允许通过其它对象创建 Blob 对象
new Blob([obj],{type:createType}) 
复制代码

obj

字符串内容

createType

要构造的类型

兼容性 IE 10 以上

Api 解析:createObjectURL

静态方法会创建一个 DOMString。

objectURL = URL.createObjectURL(object);
复制代码

object

用于创建 URL 的 File 对象、Blob 对象或者 MediaSource 对象。

Api 解析: window.navigator

// 官方已不建议使用的文件下载方式,仅针对 ie 且兼容性 10 以上
// msSaveBlob 仅提供下载
// msSaveOrOpenBlob 支持下载和打开
window.navigator.msSaveOrOpenBlob(blob, fileName);
复制代码

blob

要下载的 blob 对象

fileName

下载后命名的文件名称。

总结

本文仅针对图片压缩介绍了一些思路,简单的使用场景可能如下介绍,当然也会引申出来更多的使用场景,这些还有待大家一起挖掘。

当然温馨提示:因部分接口有 IE 兼容性问题,IE 浏览器方面,仅能支持 IE10 以上版本进行下载。

Android 图片选择到裁剪之步步深坑

前言

最近在自己的项目里实现了一个头像选择的功能,就是先从相册里选取一张图片再调用系统的裁剪功能来制作头像,效果就像下面这样:

本以为很小的一个功能,却远远没有我想的那样简单,可以说每一步都暗藏玄机,下面就让我带大家看看这里面究竟有哪些坑。

Android 4.4 之存储访问框架

首先,让我们从图片选择开始,使用隐式 Intent 跳转到图片选择:

private void routeToGallery() {
   Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
   intent.addCategory(Intent.CATEGORY_OPENABLE);
   intent.setType("image/*");
   startActivityForResult(intent, GALLERY_REQUSET_CODE);
}

在回调中处理返回的图片,继而跳转至图片裁剪:

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
   if (requestCode == GALLERY_REQUSET_CODE && resultCode == RESULT_OK) {
       String path = data.getData().getPath();
       Bitmap image = BitmapFactory.decodeFile(path);
       File faceFile;
       try {
           faceFile = saveBitmap(image);
       } catch (IOException e) {
           e.printStackTrace();
           return;
       }
       Uri fileUri = Uri.fromFile(faceFile);
       routeToCrop(fileUri);      //跳转到图片裁剪
   }}private void routeToCrop(Uri uri) {
   Intent intent = new Intent("com.android.camera.action.CROP");
   intent.setDataAndType(uri, "image/*");
   intent.putExtra("crop", true);
   intent.putExtra("aspectX", 1);
   intent.putExtra("aspectY", 1);
   intent.putExtra("outputX", 150);
   intent.putExtra("outputY", 150);
   intent.putExtra("return-data", true);
   startActivityForResult(intent, CROP_REQUEST_CODE);}private File saveBitmap(Bitmap bitmap) throws IOException {
   File file = new File(getExternalCacheDir(), "face-cache");
   if (!file.exists()) file.createNewFile();
   try (OutputStream out = new FileOutputStream(file)) {
       bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
   }
   return file;
}

这一段代码看似正常,但问题就出在String path = data.getData().getPath();这一句。这一段代码在 Android 4.4 以下是可以正常运行的,不过从 Android 4.4 开始这里获取到的将为一个无效的路径,这是为什么呢?

Android 从 4.4 开始引入了一个概念:存储访问框架,简单来说就是 Android 提供了一个专门供用户访问资源的软件,将设备上所有可以访问资源的软件接口都整合到了一起,避免了用户只能选择一个特定软件的尴尬,在 Android 4.4 以下,我们发送刚才选取图片的隐式 Intent,效果是这样的,需要用户去选择使用哪个应用:

而从 Android 4.4 开始,就变成了这样:

直接打开一个资源选取的软件(这个软件平时是隐藏的,不会显示在软件列表中),其中包含了访问设备上所有可访问资源软件的接口,这个改变极大的提高的用户操作的便捷性。

不过这也带来了一个问题,从 Android 4.4 开始,在onActivityResult()方法的Intent中所包含的uri不再是file://类型,而是变成了content://类型,这也是为什么在 Android 4.4 以后调用data.getData.getPath()获取到的结果是无效的。因此,我们必须对 Android 4.4 以上的版本进行特殊的处理:

private void routeToGallery() {
   Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
   intent.addCategory(Intent.CATEGORY_OPENABLE);
   intent.setType("image/*");
   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
       startActivityForResult(intent, GALLERY_REQUSET_CODE_KITKAT);
   } else {
       startActivityForResult(intent, GALLERY_REQUSET_CODE);
   }
}

在回调中对不同版本分别进行处理:

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
   switch (requestCode) {
       case GALLERY_REQUSET_CODE:
           handleGalleryResult(resultCode, data);
           break;
       case GALLERY_REQUSET_CODE_KITKAT:
           handleGalleryKitKatResult(resultCode, data);
           break;
   }}private void handleGalleryResult(int resultCode, Intent data) {
   // 跟之前一样}// Result uri is "content://" after Android 4.4private void handleGalleryKitKatResult(int resultCode, Intent data) {
   File faceFile;
   try {
       ParcelFileDescriptor parcelFileDescriptor =
               getContentResolver().openFileDescriptor(contentUri, "r");
       FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
       Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
       faceFile = saveBitmap(image);
   } catch (IOException e) {
       e.printStackTrace();
       return;
   }
   Uri fileUri = Uri.fromFile(faceFile);
   routeToCrop(fileUri);
}

Android 7.0 之 FileProvider

完成了图片的选择功能,转眼又碰到了一个问题:

Android 为了提高私有文件的安全性,从 7.0 开始对外传递file://类型的uri会触发FileUriExposedException。因此,在分享私有文件时必须使用FileProvider。

对 Android 的这一改变还不太了解的同学可以看一下这两篇文章:

Android 7.0 行为变更

()

Setting Up File Sharing

()

第一步

在manifest文件中加入FileProvider:

"http://schemas.android.com/apk/res/android"
   package="gavinli.translator">
   <application
       ...>

       <provider
           android:name="android.support.v4.content.FileProvider"
           android:authorities="gavinli.translator"
           android:grantUriPermissions="true"
           android:exported="false">

           <meta-data
               android:name="android.support.FILE_PROVIDER_PATHS"
               android:resource="@xml/filepaths" />

       provider>

       ...    application>
manifest>

第二步

在xml文件夹中创建filepaths.xml文件,并声明所要分享的文件目录:


   <paths>
       <external-cache-path name="mycache" path="./" />
   paths>

resources>

这里的path就代表你想要分享的文件目录,而name就是具体显示在uri中的信息,最终生成的uri就像下面这样:

这种经过处理的uri可以很好的隐藏掉实际的文件路径。

第三步

在代码中对 Android 7.0 以上的版本进行特殊处理:

private void handleGalleryKitKatResult(int resultCode, Intent data) {
   ...

   Uri fileUri;
   if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
       // Android 7.0 "file://" uri权限适配
       fileUri = FileProvider.getUriForFile(this,
               "gavinli.translator", faceFile);
   } else {
       fileUri = Uri.fromFile(faceFile);
   }
   routeToCrop(fileUri);
}

这里传入的”gavinli.translator”,需要与之前在manifest文件中声明的android:authorities一致。

第四步

在裁剪图片的Intent中加入对该图片的访问权限:

private void routeToCrop(Uri uri) {
   Intent intent = new Intent("com.android.camera.action.CROP");
   intent.setDataAndType(uri, "image/*");

   // 加入访问权限
   intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION
    | Intent.FLAG_GRANT_READ_URI_PERMISSION);
   ...
}

最后一步

在回调中获取裁剪后的图片:

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
   switch (requestCode) {
       ...
       case CROP_REQUEST_CODE:
           Bundle bundle = data.getExtras();
           Bitmap face = bundle.getParcelable("data");
           break;
   }
}

ntent 的限制

你以为到这里就结束了吗?其实还远远没有。我们这里裁剪的图片是用作头像的,所以大小一般都比较小。可以当图片的大小变大后就会发现,每次裁剪后在Intent中获取到的图片其实都是缩略图。

这是因为 Android 对Intent中所包含数据的大小是有限制的,一般不能超过 1M,否则应用就会崩溃,这就是Intent中的图片数据只能是缩略图的原因。而解决的办法也很简单,我们需要给图片裁剪应用指定一个输出文件,用来存放裁剪后的图片:

private void routeToCrop(Uri uri) {
   ...
   intent.putExtra("return-data", false);
   intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(
           new File(getExternalCacheDir(), "face-cropped")));
   startActivityForResult(intent, CROP_REQUEST_CODE);
}

现在,在回调中的图片就不能再直接从Intent中获取了,而是需要先拿到Intent中的uri,再使用uri进行获取,具体的过程和之前处理uri的方式一样,这里就不再赘述了。当然,直接从之前指定的文件中读取数据也是可以的

Android 6.0 之运行时权限

不知道大家发现了没有,之前保存图片的目录都是使用的Context.getExternalCacheDir(),这个方法获取到的目录为/sdcard/Android/data/gavinli.translator/cache,是应用专属的外部存储空间,不需要声明权限。而要想使用公共的存储空间,就势必要面对一个问题:Android 6.0 的运行时权限。

首先,在manifest文件中声明读取外置存储的权限:

"http://schemas.android.com/apk/res/android"
   package="gavinli.translator">
   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
   ...
manifest>

之后,在代码中加入运行时的权限申请:

private void request() {
   String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE};
   if(ContextCompat.checkSelfPermission(this, permisson)
           != PackageManager.PERMISSION_GRANTED) {
       requestPermissions(permissions, REQUEST_CODE);
   } else {
       // 存储图片
   }}public void onRequestPermissionsResult(int requestCode, String[] permissions,
                                          int[] grantResults
)
{
   if(requestCode == REQUEST_CODE) {
       if(grantResults[i] == PackageManager.PERMISSION_GRANTED) {
           // 存储图片
       }
   }
}

后记

到这里,这一次的踩坑之旅就全部结束了,我们也看到了 Android 这几个版本以来一步步对权限的限制,虽然这对我们的开发产生一定的影响,但只要能提高用户的使用体验,这点困难又算的了什么呢?

本站内容来自用户投稿,如果侵犯了您的权利,请与我们联系删除。联系邮箱:835971066@qq.com

本文链接:http://news.xiuzhanwang.com/post/1442.html

发表评论

评论列表

还没有评论,快来说点什么吧~

友情链接: