通过Nginx-Lua支持图片转换WebP


实现逻辑

用户访问一张图片,nginx收到请求,通过读取 Request Headers 中的 Accept 字段值来判断浏览器是否支持WebP,如果支持则返回WebP,不支持则返回原图。


环境安装

安装libjpeg, libpng

yum install libjpeg-turbo-devel libjpeg-turbo libpng-devel gcc-c++ -y 

安装LibTIFF

cd /usr/local/src
wget http://download.osgeo.org/libtiff/tiff-4.0.8.tar.gz
tar -zxvf tiff-4.0.8.tar.gz
cd tiff-4.0.8
./configure
make
make install 

安装giflib

cd /usr/local/src
wget https://sourceforge.net/projects/giflib/files/giflib-5.1.4.tar.gz
tar zxvf giflib-5.1.4.tar.gz
cd giflib-5.1.4
./configure
make
make install 

安装libwebp

cd /usr/local/src
wget http://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-1.2.0.tar.gz
tar -zxvf libwebp-1.2.0.tar.gz
cd libwebp-1.2.0
./configure --enable-libwebpmux \
--enable-libwebpdemux \
--enable-libwebpdecoder \
--enable-libwebpextras \
--enable-swap-16bit-csp \
--disable-static

make
make install 
  • 编译完要检查输出是否支持 JPG、PNG、GIF、WEBP
    WebP Configuration Summary
    --------------------------
    
    Shared libraries: yes
    Static libraries: no
    Threading support: yes
    libwebp: yes
    libwebpdecoder: yes
    libwebpdemux: yes
    libwebpmux: yes
    libwebpextras: yes
    
    Tools:
    cwebp : yes
      Input format support
      ====================
      JPEG : yes
      PNG  : yes
      TIFF : yes
      WIC  : no
    dwebp : yes
      Output format support
      =====================
      PNG  : yes
      WIC  : no
    GIF support : yes
    anim_diff   : yes
    gif2webp    : yes
    img2webp    : yes
    webpmux     : yes
    vwebp       : no
    webpinfo    : yes
    SDL support : no
    vwebp_sdl   : no

加载环境

ldconfig

测试转换

Google提供了一组工具集合,叫 libwebp ,其中包括各种 webp 相关转换的命令:

  • cwebp – 将其它图片转为webp格式图片 (不包括GIF)
  • dwebp – 将webp格式图片转为其它格式图片
  • vwebp – webp图片浏览器
  • webpmux – WebP muxing tool
  • gif2webp – 将GIF转换为webp图片
    • 不太建议使用,测试发现 75 质量的 webp 文件会出现比原文件还大的情况。
# png -> webp
cwebp -q 75 01.png -o 01.webp

# gif -> webp
gif2webp -q 80 02.gif -o 02.webp

# webp -> png
dwebp image.webp -o image.png 

此处省略 lua-nginx 的相关编译: https://nestealin.com/ce9634bf/


方案一: 在原资源目录下生成 webp 文件

  • 此方案用于 webp 文件经常变更的情况下,每次随项目部署,路径自动清空临时 webp 文件,会影响初次加载。

引入 lua 脚本

  • 打开 /usr/local/nginx/conf/nginx.conf 文件,在http段添加

    http {
      lua_package_path "/usr/local/luanginx/lualib/?.lua;;";
    }
  • webp 转换脚本

    vim /usr/local/nginx/conf/lua/webp.lua
    -- 检测文件是否存在
    function file_exists(name)
        local f=io.open(name,"r")
        if f~=nil then io.close(f) return true else return false end
    end
    
    -- 接收 location 中定义的 $webp_filepath 变量
    local newFile = ngx.var.webp_filepath;
    --ngx.log(ngx.ERR, " newFile:", newFile)
    
    local originalFile = newFile:sub(1, #newFile - 5); -- 去掉 .webp 的后缀
    --ngx.log(ngx.ERR, " originalFile:", originalFile)
    
    if not file_exists(originalFile) then -- 原文件不存在
        ngx.log(ngx.ERR, "The originalFile is NOT FOUND!")
        ngx.exit(404);
        return;
    end
    
    -- 转换原图片到 webp 格式,这里的质量是 75 ,你也可以改成别的
    os.execute("cwebp -q 75 " .. originalFile .. " -o " .. newFile);
    --ngx.log(ngx.ERR, "converting to webp...")
    
    if file_exists(newFile) then -- 如果新文件存在(转换成功)
        -- 转换 webp 成功,并返回 .webp 结尾的 uri 地址
        ngx.exec(ngx.var.webp_filename) -- Internal Redirect
    else
        ngx.exit(404)
    end

域名配置

# 根据浏览器 accpet 请求头辨别是否支持webp
map $http_accept $webp_suffix {
    default "";
    "~*webp" ".webp";
}

server {
    listen  80;
    server_name abc.nestealin.com ;
    rewrite ^(.*)$  https://$host$1 permanent;
}

server {
    listen    443 ssl http2;
    server_name  abc.nestealin.com ;
    charset utf-8;
    access_log  logs/$http_host.access.main.log  main;
    error_log  logs/abc.nestealin.com.error.log error;

    ssl_certificate      /data/keys/server.cer;
    ssl_certificate_key  /data/keys/server.key;
    ssl_session_timeout  5m;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ecdh_curve X25519:P-256:P-384;
    ssl_ciphers TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-256-GCM-SHA384:TLS13-AES-128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CHACHA20-POLY1305:EECDH+CHACHA20:EECDH+AES128;

    # 开启 TLS 1.3 0-RTT
    ssl_early_data on;
    # 添加 Early-Data 头告知后端, 防止重放攻击
    proxy_set_header Early-Data $ssl_early_data;

    #include firewall.conf;

    set $origin_root /data/blogs/;
  
    location @webp_redirect {
        content_by_lua_file "/usr/local/nginx/conf/lua/webp.lua";

    }

    location ~ .*\.(jpg|jpeg|png)$ {
        set $webp_filename $uri$webp_suffix;
        set $webp_filepath $origin_root$webp_filename;
				# 先去检测是否存在webp文件,如果没有,则内部跳转执行lua转换
        try_files $webp_filename @webp_redirect;
        root $origin_root;
    }

    location / {
        root $origin_root ;
        index index.html;
        if (!-e $request_filename) {
            rewrite ^/(.*)$  / redirect;
        }
    }
}

方案二: 将 webp 生成到临时目录

  • 此方案用于 webp 文件不经常更新的情况下,无需繁复重新生成,影响初次加载。
  • 与方案一区别在于多了一层临时目录。

创建临时目录

mkdir /data/webp_data

# 给予 nginx/lua 脚本运行用户可写权限
chown -R www.www /data/webp_data

引入 lua 脚本

  • 打开 /usr/local/nginx/conf/nginx.conf 文件,在http段添加

    http {
      lua_package_path "/usr/local/luanginx/lualib/?.lua;;";
    }
  • 创建执行脚本

vim /usr/local/nginx/conf/lua/webp_tmp_path.lua
--检测路径是否为目录
local function is_dir(sPath)
    if type(sPath) ~= "string" then
        return false
    end

    local response = os.execute("cd " .. sPath)

    if response == 0 then
        return true
    end
    return false
end

function file_exists(name)
  local f=io.open(name,"r")
  if f~=nil then io.close(f) return true else return false end
end

local newFile = ngx.var.webp_filepath;
--ngx.log(ngx.ERR, " newFile:", newFile)

local originalFile = ngx.var.origin_filepath;
--ngx.log(ngx.ERR, " originalFile:", originalFile)

if not file_exists(originalFile) then -- 原文件不存在
  ngx.log(ngx.ERR, "The originalFile is NOT FOUND!")
  ngx.exit(404);
  return;
end

--获取文件路径,不存在则新增
function getFileDir(filename)
    return string.match(filename, "(.+)/[^/]*%.%w+$")
end

if not is_dir(getFileDir(newFile)) then
    ngx.log(ngx.ERR, "creating webp tmp document...")
    os.execute("mkdir -p " .. getFileDir(newFile))
end

-- 转换原图片到 webp 格式,这里的质量是 75 ,你也可以改成别的
os.execute("cwebp -q 75 " .. originalFile .. " -o " .. newFile); 
ngx.log(ngx.ERR, "converting to webp...")

if file_exists(newFile) then -- 如果新文件存在(转换成功)
  -- 转换 webp 成功,并返回 .webp 结尾的 uri 地址
  ngx.exec(ngx.var.webp_filename) -- Internal Redirect
else
  ngx.exit(404)
end

域名配置

# 根据浏览器 accpet 请求头辨别是否支持webp
map $http_accept $webp_suffix {
    default "";
    "~*webp" ".webp";
}

server {
    listen  80;
    server_name abc.nestealin.com ;
    rewrite ^(.*)$  https://$host$1 permanent;
}

server {
    listen    443 ssl http2;
    server_name  abc.nestealin.com ;
    charset utf-8;
    access_log  logs/$http_host.access.main.log  main;
    error_log  logs/abc.nestealin.com.error.log error;

    ssl_certificate      /data/keys/server.cer;
    ssl_certificate_key  /data/keys/server.key;
    ssl_session_timeout  5m;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ecdh_curve X25519:P-256:P-384;
    ssl_ciphers TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-256-GCM-SHA384:TLS13-AES-128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CHACHA20-POLY1305:EECDH+CHACHA20:EECDH+AES128;

    # 开启 TLS 1.3 0-RTT
    ssl_early_data on;
    # 添加 Early-Data 头告知后端, 防止重放攻击
    proxy_set_header Early-Data $ssl_early_data;

    #include firewall.conf;

    set $origin_root /data/blogs/;
  
    location @webp_redirect {
        content_by_lua_file "/usr/local/nginx/conf/lua/webp_tmp_path.lua";
    }

    location ~ .*\.(jpg|jpeg|png)$ {
        set $webp_tmp_root /data/webp_data/;
        set $webp_filename $uri$webp_suffix;
        set $webp_filepath $webp_tmp_root$webp_filename;
        set $origin_filepath $origin_root$uri;
        
        # 兼容不支持 webp 的浏览器,直接返回原格式
        if ($webp_filename !~ .*\.webp$) {
            root $origin_root;
        }    
    
				# 先去检测是否存在webp文件,如果没有,则内部跳转执行lua转换
        try_files $webp_filename @webp_redirect;
        root $webp_tmp_root;
    }

    location / {
        root $origin_root ;
        index index.html;
        if (!-e $request_filename) {
            rewrite ^/(.*)$  / redirect;
        }
    }
}

效果

  • 在 URL 不变的基础上,实现图片压缩,转换为 webp 格式返回,大大缩小原图带宽开销,缩短客户端加载时间。
    • 压缩前,客户端不支持 webp ,获得原图2.3MB
    • 压缩后,客户端支持 webp ,获得压缩后图片137KB

相关链接


文章作者: NesTeaLin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 NesTeaLin !
  目录