什么是 WebP

WebP (发音 weppy ),是一种同时提供了有损压缩与无损压缩的图片文件格式,是Google新推出的影像技术,它可让网页图档有效进行压缩,同时又不影响图片格式兼容与实际清晰度,进而让整体网页下载速度加快。

  • WebP 无损压缩的图片可以比同样大小的 PNG 小 26%;
  • WebP 有损压缩的图片可以比同样大小的 JPEG 小 25-34%;
  • WebP 支持无损的透明图层通道,代价只需增加 22% 的字节存储空间;
  • WebP 有损透明图像可以比同样大小的 PNG 图像小3倍。

原理

官方描述

支持

  • WebP 有损压缩支持:
    • Google Chrome(桌面版) 17+
    • Google Chrome(Android版) 25+
    • Opera 11.10+
    • Native web 浏览器:Android 4.0+(ICS)
  • WebP 有损、无损和透明度支持(libwebp v0.2.0)
    • Google Chrome(桌面版) 23+
    • Google Chrome(Android版) 25+
    • Google Chrome(iOS版) 29+
    • Opera 12.10+
    • Native web 浏览器:Android 4.2+(JB-MR1)
  • 开发者也可以在代码中使用轻量级的 libwebp 库来编码/解码 WebP 图像;
  • 命令行工具 cwebpdwebp 可以实现其他格式与 webp 的互转。
  • 其他支持工具

性能

目前WEBP与JPG相比较,编码速度慢10倍,解码速度慢1.5倍。

针对1.5倍的解码速度是否影响用户体验的问题,我们可以看看ebay团队的这个测试:50张同样质量的WEBP与jgp加载的速度对比

此测试表明,webp虽然会增加额外的解码时间,但是由于减少了文件体积,缩短了加载的时间,实际上 文件的渲染速度反而变快了

应用案例

  • Facebook相册
  • 淘宝的广告图
  • QQ空间装扮

安装

下面介绍 libwebp 在 Mac 下的两种安装方法:

方法一:使用 MacPorts 安装

1、 下载并安装 MacPorts :到 这里 下载安装 MacPorts 。 2、 更新 MacPorts :

1
$ sudo port selfupdate

3、 安装 libwebp

1
$ sudo port install webp

方法二:使用 HomeBrew 安装

1、 安装 Homebrew

1
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

2、 安装 libwebp

1
$ brew install webp

安装完 libwebp 后,在类 Unix 系统的 /usr/local/include/webp 可以找到如下几个头文件:

1
2
3
decode.h
encode.h
types.h

/usr/local/lib/ 目录则存放着 libwebp 的静态库和动态库。

格式转换

安装完 libwebp 后,cwebp 和 dwebp 也跟着安装成功了。可以用这两个命令行工具实现其他图片格式与 webp 格式的互转。

  • 其他格式图片转 webp
1
$ cwebp [options] input_file -o output_file.webp
  • webp 转其他格式
1
$ dwebp [options] input_file.webp

Android 开发中使用 WebP

对于 Android 4.0 以上的系统,BitmapFactory 可以原生支持 webp 。下面介绍让 Android 4.0 以下的系统支持 webp 的方法。因为 libwebp 是 C 写的,所以可以通过 NDK 调用 libwebp :

  1. 下载源码 ,放置到 你的工程目录/jni
  2. 修改 Android.mk 文件。修改方法见下文。
  3. 将 libwebp.jar 引入到工程的 build path 中。
  4. 创建 jni/src/libwebp_java_wrap.c 文件。内容见下文。
  5. 创建 jni/Application.mk 文件。内容见下文。
  • Android.mk 的修改方法:增加 libwebp_java_wrap.c,将 include $(BUILD_STATIC_LIBRARY) 改为 include $(BUILD_SHARED_LIBRARY)。示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)
LOCAL_SRC_FILES := \
src/dec/alpha.c \
src/dec/frame.c \
src/dec/idec.c \
src/dec/layer.c \
src/dec/quant.c \
src/dec/tree.c \
src/dec/vp8.c \
src/dec/webp.c \
src/dec/io.c \
src/dec/buffer.c \
src/dsp/yuv.c \
src/dsp/upsampling.c \
src/dsp/cpu.c \
src/dsp/dec.c \
src/dsp/dec_neon.c \
src/dsp/enc.c \
src/utils/bit_reader.c \
src/utils/bit_writer.c \
src/utils/thread.c \
src/libwebp_java_wrap.c \

LOCAL_CFLAGS := -Wall -DANDROID -DHAVE_MALLOC_H -DHAVE_PTHREAD -DWEBP_USE_THREAD \
-finline-functions -frename-registers -ffast-math \
-s -fomit-frame-pointer -Isrc/webp

LOCAL_C_INCLUDES += $(LOCAL_PATH)/src

LOCAL_MODULE:= webp

include $(BUILD_SHARED_LIBRARY)
1
2
3
# The ARMv7 is significanly faster due to the use of the hardware FPU
APP_ABI := armeabi armeabi-v7a
APP_PLATFORM := android-8

其中 APP_PLATFORM 设定为支持的 SDK 最低版本。

  • 保存后,cd 到工程目录下,执行指令:
1
$ $NDK/ndk-build

应用层的开发工程一般如下:

1. 加载 so 包

1
2
3
static {  
System.loadLibrary("webp");
}

2. 声明与 Native 方法相对应的方法

native 方法是:

1
2
3
4
5
6
7
8
9
10
11
SWIGEXPORT jint JNICALL Java_com_google_webp_libwebpJNI_WebPGetDecoderVersion(JNIEnv *jenv,  
jclass jcls) {
jint jresult = 0 ;
int result;

(void)jenv;
(void)jcls;
result = (int)WebPGetDecoderVersion();
jresult = (jint)result;
return jresult;
}

native 方法命名为:Java_包名_类名_应用层方法名 。其中包名中的点被下划线替代。

应用层需要声明的方法为:

1
public static final native int WebPGetDecoderVersion();  

应用层只用声明,不用定义,但要加上native关键字。

3. 使用应用层声明的方法

下面我们按照上述方法来在应用层使用我们刚生成的so库

记得有一个libwebp.jar文件不,这个jar已经把应用层声明的native方法搞好了,而且帮我们封装了一层,我们只用调用其方法就ok了,但是我们还是要加载so库,因为这个它没有帮我们实现。

我们写两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static {  
System.loadLibrary("webp");
}

private Bitmap webpToBitmap(byte[] encoded) {

int[] width = new int[] { 0 };
int[] height = new int[] { 0 };
byte[] decoded = libwebp.WebPDecodeARGB(encoded, encoded.length, width,
height);

int[] pixels = new int[decoded.length / 4];
ByteBuffer.wrap(decoded).asIntBuffer().get(pixels);

return Bitmap.createBitmap(pixels, width[0], height[0],
Bitmap.Config.ARGB_8888);

}

public static boolean isWebp(byte[] data) {
return data != null && data.length > 12 && data[0] == 'R'
&& data[1] == 'I' && data[2] == 'F' && data[3] == 'F'
&& data[8] == 'W' && data[9] == 'E' && data[10] == 'B'
&& data[11] == 'P';
}

基于这两个方法,我们可以对webp图片进行解压缩。一个 Demo 工程可以在这里下载。

还可以使用另一个封装好的库 alexey-pelykh/webp-android-backport

iOS 开发中使用 WebP

可以使用封装好的 seanooi/iOS-WebP

准备工作

方法一:使用CocoaPods

1
pod 'iOS-WebP', '0.4'

方法二:手动的方法

将 iOS-WebP 目录下的如下三个文件引入到你的工程中:

  • UIImage+WebP.h
  • UIImage+WebP.m
  • WebP.framework

用法

如果使用 Cocoapods ,注意先引用头文件 #import "UIImage+WebP.h"#import <UIImage+WebP.h>

接口

如下三个方法分别实现其他格式转WebP,WebP转其他格式及设置图像透明度。

1
2
3
4
5
6
7
8
9
+ (void)imageToWebP:(UIImage *)image quality:(CGFloat)quality alpha:(CGFloat)alpha preset:(WebPPreset)preset
completionBlock:(void (^)(NSData *result))completionBlock
failureBlock:(void (^)(NSError *error))failureBlock;

+ (void)imageWithWebP:(NSString *)filePath
completionBlock:(void (^)(UIImage *result))completionBlock
failureBlock:(void (^)(NSError *error))failureBlock;

- (UIImage *)imageByApplyingAlpha:(CGFloat)alpha;

为了避免阻塞主线程,图像的编码和解码将在后台的线程中完成并将结果block传回给主线程。

WebP 编码

1
2
3
4
5
6
7
8
9
10
11
// quality value is [0, 100]
// alpha value is [0, 1]
[UIImage imageToWebP:[UIImage imageNamed:@"demo.jpg"] quality:quality alpha:alpha preset:WEBP_PRESET_DEFAULT completionBlock:^(NSData *result) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *webPPath = [paths[0] stringByAppendingPathComponent:@"image.webp"];
if (![result writeToFile:webPPath atomically:YES]) {
NSLog(@"Failed to save file");
}
} failureBlock:^(NSError *error) {
NSLog(@"%@", error.localizedDescription);
}];

WebPPreset 的可能值包括:

  • WEBP_PRESET_DEFAULT (默认值,预设)
  • WEBP_PRESET_PICTURE (适合数字图像,如肖像、室内摄影)
  • WEBP_PRESET_PHOTO (户外照片,带有自然光照的照片)
  • WEBP_PRESET_DRAWING (手或者素描,带有高对比度细节的照片)
  • WEBP_PRESET_ICON (小尺寸彩色图)
  • WEBP_PRESET_TEXT (类文本图案)

WebP 解码

1
2
3
4
5
[UIImage imageWithWebP:@"/path/to/file" completionBlock:^(UIImage *result) {
UIImageView *myImageView = [[UIImageView alloc] initWithImage:result];
}failureBlock:^(NSError *error) {
NSLog(@"%@", error.localizedDescription);
}];

设置图像透明度

1
2
//alpha value is [0, 1]
UIImage *transparencyImage = [[UIImage imageNamed:image.jpg] imageByApplyingAlpha:0.5];

Hybrid 开发中使用 WebP

WebP 在不同版本的浏览器内核的支持程度比较碎片化,要在 WebView 中使用 WebP 格式的图片,并且做到全浏览器兼容,目前主要有三种思路:

  1. 嵌入 flash ,让 flash 解析 WebP 。但这种方式只能将IMG元素一次性转换成Flash元素,而不支持后期的脚本操作。而且超长的参数会消耗大量CPU和内存。
  2. 准备一套 WebP 格式的图片,当判断客户端不支持 WebP 时再转换成其他格式。但这样会影响图片加载性能。
  3. 准备两套格式的图片,然后根据不同的浏览器内核提供不同格式的图片。这样做的代价是服务器存储的数据量将会增加,但牺牲存储成本换来的好处是图片加载性能更高。

思路 3 根据实现机制的不同,又可以分成在服务端进行和在客户端进行两种。下面介绍这两种方式。

方案1. 服务端方式

通过 HTTP 的内容协商(Content Negotiation)信息来判断。简单的思路是:

  1. 支持 WebP 的客户端在其 Accept 头部中包含 WebP 的建议,服务器制定 Vary: Accept 的规则。
  2. 服务器和缓存根据 Accept 头部的信息来提供合适的资源。

具体方案可参考:https://github.com/igrigorik/webp-detect

方案2. 客户端方式

JS 判断浏览器是否支持 WebP ,不同的 src 写入 img 中,样式中的图片可以通过检测后添加 class 。

脚本代码:

1
2
3
4
5
6
7
8
9
10
var testWebp = function(callback){  
var image = new Image();
image.onerror = function() {
callback(false);
};
image.onload = function() {
callback(image.width == 1);
};
image.src = 'data:image/webp;base64,UklGRiwAAABXRUJQVlA4ICAAAAAUAgCdASoBAAEAL/3+/3+CAB/AAAFzrNsAAP5QAAAAAA==';
}

src 中的字符串是经过 base64 编码过的一张 WebP 图片。

完整的页面代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<html>  
<head>
<meta charset="UTF-8" />
<title>WEBP TEST</title>
<script charset="utf-8" type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script type="text/javascript">
var testWebp = function(callback){
var image = new Image();
image.onerror = function() {
callback(false);
};
image.onload = function() {
callback(image.width == 1);
};
image.src = 'data:image/webp;base64,UklGRiwAAABXRUJQVlA4ICAAAAAUAgCdASoBAAEAL/3+/3+CAB/AAAFzrNsAAP5QAAAAAA==';
},webSrc = function(src){
var suffix = src.lastIndexOf('.');
suffix = src.substr(suffix);
if (/png|jpg/.test(suffix)){
return src.substr(0,(src.length-3))+'webp';
}else{
return src;
}
}
$(function(){
var $note = $('.note');
testWebp(function(SUP){
var $img = $('img[data-img]');
if(SUP){
$('body').addClass('webp');
$img.each(function(i,o){
var $o = $(o),
src = $o.attr('data-img');
$o.attr('src',webSrc(src));
});
$note.html('你载入的是<strong>webp</strong>格式');
}else{
$('body').addClass('nowebp');
$img.each(function(i,o){
var $o = $(o),
src = $o.attr('data-img');
$o.attr('src',src);
});
$note.html('你载入的是<strong>png、jpg</strong>格式');
}
});
});
</script>
<style type="text/css">
*{margin:0;padding:0;}
body{font:12px/1.6 arial,\5fae\8f6f\96c5\9ed1;color:#666;}
strong{color:#000;padding:0 2px;}
img{width:313px;height:219px;}
.img{ margin-top:20px; width:313px;height:214px; }
.webp .img{ background:url(http://demo.btorange.com/article/webp/demo2.webp) no-repeat; }
.nowebp .img{ background:url(http://demo.btorange.com/article/webp/demo2.png) no-repeat; }
</style>
</head>
<body>
<p class="note"></p>
<!-- img图片 -->
<img alt="" data-img="http://demo.btorange.com/article/webp/demo1.jpg" data-pinit="registered" />
<!-- 样式中的图片 -->
<div class="img"></div>
</body>
</html>

参考资源

Comments