Fork me on GitHub

Ubuntu18.04 Nginx+Lua+GraphicsMagick图片缩放

背景

自己搭建的图片服务器,有图片缩放的需求,大致思路是可以使用nginx调用lua,使用GraphicMagick的命令来做图片缩放

说明

文件夹规划

lua.jaychang.cn(如/var/filebase)

1
2
3
4
5
6
7
8
9
10
11
12
13
jaychang@nginx:~$ tree /var/filebase/
/var/filebase/
├── avatar.png
├── cache
│   └── thumb
│   ├── avatar.png_100x100.png
│   └── upload
│   ├── 1.png_100x100.png #固定高和宽
│   ├── 1.png_400-.png # 定高
│   └── 1.png_800-.png # 定宽
└── upload
├── 1.png
4 directories, 8 files

其中img.xyz.com为图片站点根目录
cache/thumb为缩略图存放目录
upload目录存放上传的图片

链接地址对应关系

原图访问地址:http://img.xyz.com/upload/1.png
缩略图访问地址:http://img.xyz.com/upload/1.png_100x100.png 即为宽100,高100
自动宽地址: http://img.xyz.com/upload/1.png_-400.png 用”-“表示自动,即为高400,宽自动
自动高地址: http://img.xyz.com/upload/1.png_800-.jpg 用”-“表示自动,即为宽800,高自动

访问流程

首先判断缩略图是否存在,如存在则直接显示缩略图;
缩略图不存在,则判断原图是否存在,如原图存在则拼接graphicsmagick(gm)命令,生成并显示缩略图,否则返回404

所需软件

  • lua-5.3.5.tar.gz
  • LuaJIT-2.0.5.tar.gz
  • nginx-1.14.2.tar.gz
  • nginx模块:lua-nginx-module-0.10.15.tar.gz

    lua-nginx-module 依赖于 LuaJIT 和 ngx_devel_kit。LuaJIT 需要安装,ngx_devel_kit 只需下载源码包,在 Nginx 编译时指定 ngx_devel_kit 目录

  • nginx模块:nginx-http-concat
  • nginx模块:ngx_devel_kit
  • nginx模块:nginx-http-concat
1
2
3
4
5
6
7
curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz
curl -R -O http://luajit.org/download/LuaJIT-2.0.5.tar.gz
curl -R -O http://nginx.org/download/nginx-1.14.2.tar.gz
curl -R -O https://github.com/openresty/lua-nginx-module/archive/v0.10.15.tar.gz
curl -R -O https://github.com/openresty/lua-resty-core/archive/v0.1.17.tar.gz
curl -R -O https://github.com/simpl/ngx_devel_kit/archive/v0.3.0.tar.gz
git clone git@github.com:alibaba/nginx-http-concat.git

安装依赖

1
2
3
apt-get install -y gcc g++ make

apt-get install libreadline-dev libpcre3 libpcre3-dev openssl libssl-dev zlib1g zlib1g-dev libgeoip-dev -y

编译安装Lua LuaJIT

编译安装Lua

1
2
3
4
curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz
tar zxf lua-5.3.5.tar.gz
cd lua-5.3.5
make linux test

编译安装LuaJIT

1
2
3
4
5
6
7
8
curl -R -O http://luajit.org/download/LuaJIT-2.0.5.tar.gz
tar -zxvf LuaJIT-2.0.5.tar.gz
cd LuaJIT-2.0.5
make -j2 && make install
export LUAJIT_LIB=/usr/local/lib
export LUAJIT_INC=/usr/local/include/luajit-2.0
echo "/usr/local/lib" > /etc/ld.so.conf.d/usr_local_lib.conf
ldconfig

安装GraphicsMagick

1
apt install -y graphicsmagick

直接用apt来安装的话,可以免去安装 jpg,png 等图片库依赖

创建用户及相应目录

1
2
3
4
groupadd nginx && useradd nginx -g nginx -s /sbin/nologin -M

mkdir -p /var/tmp/nginx/client_body_temp
mkdir -p /var/tmp/nginx/uwsgi_temp

创建用户也可以用以下命令:

1
groupadd nginx && useradd nginx -g nginx -s /bin/false -M

编译安装nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
./configure --prefix=/usr/local/nginx \
--user=nginx --group=nginx \
--sbin-path=/usr/sbin/nginx \
--with-pcre \
--with-http_realip_module \
--with-http_gzip_static_module \
--with-http_stub_status_module \
--with-http_v2_module \
--with-http_flv_module \
--with-http_ssl_module \
--with-http_addition_module \
--with-http_geoip_module \
--add-module=/usr/local/src/lua-nginx-module-0.10.15 \
--add-module=/usr/local/src/ngx_devel_kit-0.3.0 \
--add-module=/usr/local/src/nginx-http-concat \
--http-scgi-temp-path=/var/tmp/nginx/cgi_temp \
--http-client-body-temp-path=/var/tmp/nginx/client_body_temp \
--http-proxy-temp-path=/var/tmp/nginx/proxy_temp \
--http-uwsgi-temp-path=/var/tmp/nginx/uwsgi_temp \
--http-fastcgi-temp-path=/var/tmp/nginx/fastcgi_temp \
--http-log-path=/var/log/nginx/access.log \
--error-log-path=/var/log/nginx/error.log
1
make -j 2 && make install

注意:动态加载模块,Nginx官方的load_module指令,详细文档参考1
参考2
还有–with-http_spdy_module 已经改为–with-http_v2_module了
,如果不用geo的话,编译的时候可以不加–with-http_geoip_module,可以不安装libgeoip-dev

测试nginx

1
nginx -t

如果出现以下错误(没有报错就不用做以下操作了)

1
2
root@ubuntu:/usr/local/src/nginx-1.14.2# nginx -t
nginx: error while loading shared libraries: libluajit-5.1.so.2: cannot open shared object file: No such file or directory

执行以下命令即可

1
2
echo "/usr/local/lib" > /etc/ld.so.conf.d/usr_local_lib.conf
ldconfig

修改nginx.conf配置文件

将nginx.conf配置文件中的server{}段配置注释掉

加一行配置,以便读取/usr/local/tengine/conf.d目录下所有后缀为.conf的配置文件

1
include ../conf.d/*.conf;

可以参考如下配置

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
user nginx;
worker_processes 4;
worker_cpu_affinity 1000 0100 0010 0001;
error_log /var/log/nginx/error.log error;
pid /usr/local/nginx/logs/nginx.pid;
worker_rlimit_nofile 65535;
events
{
use epoll;
worker_connections 65535;
}
http
{
lua_load_resty_core off;
limit_conn_zone $binary_remote_addr zone=one:10m;
limit_conn_zone $server_name zone=perserver:10m;
include mime.types;
include fastcgi.conf;
default_type application/octet-stream;
charset utf-8;
server_names_hash_bucket_size 128;
client_header_buffer_size 32k;
large_client_header_buffers 4 64k;
sendfile on;
autoindex off;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 120;

fastcgi_connect_timeout 60;
fastcgi_send_timeout 60;
fastcgi_read_timeout 60;
fastcgi_buffer_size 128k;
fastcgi_buffers 8 128k;
fastcgi_busy_buffers_size 128k;
fastcgi_temp_file_write_size 128k;

gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.0;
gzip_comp_level 2;
gzip_types text/plain application/x-javascript text/css application/xml;
gzip_vary on;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" $http_x_forwarded_for';

client_max_body_size 10m;

include ../conf.d/*.conf;
}

注意:lua_load_resty_core off;如果不加会有以下错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
nginx: [alert] detected a LuaJIT version which is not OpenResty's; many optimizations will be disabled and performance will be compromised (see https://github.com/openresty/luajit2 for OpenResty's LuaJIT or, even better, consider using the OpenResty releases from https://openresty.org/en/download.html)
nginx: [error] lua_load_resty_core failed to load the resty.core module from https://github.com/openresty/lua-resty-core; ensure you are using an OpenResty release from https://openresty.org/en/download.html (rc: 2, reason: module 'resty.core' not found:
no field package.preload['resty.core']
no file ' /usr/local/lib/lua-resty-core-0.1.17/lib/resty/core.lua'
no file './resty/core.lua'
no file '/usr/local/share/luajit-2.0.5/resty/core.lua'
no file '/usr/local/share/lua/5.1/resty/core.lua'
no file '/usr/local/share/lua/5.1/resty/core/init.lua'
no file './resty/core.so'
no file '/usr/local/lib/lua/5.1/resty/core.so'
no file '/usr/local/lib/lua/5.1/loadall.so'
no file './resty.so'
no file '/usr/local/lib/lua/5.1/resty.so'
no file '/usr/local/lib/lua/5.1/loadall.so')

创建放图片的目录

目录规划

  • /var/www 放网页,css,js等资源
  • /var/fielbase 放图片
  • /var/filebase/upload 上传的图片放这里
1
2
3
mkdir -p /var/filebase/upload 
mkdir -p /var/filebase/cache/thumb
chown -R nginx:nginx /var/filebase

配置站点配置文件

在/usr/local/tengine/conf.d目录下创建demo.conf

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# 定义lua缩略图支持的图片尺寸及开关
init_by_lua '
image_sizes_check = true
image_sizes = {"800x800", "400x400","200x200","100x100", "-800", "-400", "-200","-100", "800-", "400-", "200-","100-"}
';

server {
listen 80;
servername img.xyz.com;
index index.php index.html index.htm;

set $root_path '/var/www';
root $root_path;

# /lua仅用于测试,可去掉
location /lua {
default_type 'text/plain';
content_by_lua 'ngx.say("hello, ttlsa lua")';
}

# 这里也是需要根据实际情况修改的,这里的是用了rewrite
location / {
try_files $uri $uri/ /index.php?$args;

# add support for img which has query params,
# like: xxx.jpg?a=b&c=d_750x750.jpg
if ($args ~* "^([^_]+)_(\d+)+x(\d+)+\.(jpg|jpeg|gif|png)$") {
set $w $2;
set $h $3;
set $img_ext $4;

# rewrite ^\?(.*)$ _${w}x${h}.$img_ext? last;
rewrite ([^.]*).(jpg|jpeg|png|gif)$ $1.$2_${w}x${h}.$img_ext? permanent;
}
}

# set var for thumb pic
set $upload_path /var/filebase;
set $img_original_root $upload_path;# original root;
set $img_thumbnail_root $upload_path/cache/thumb;
set $img_file $img_thumbnail_root$uri;

# like:/xx/xx/xx.jpg_100-.jpg or /xx/xx/xx.jpg_-100.jpg
location ~* ^(.+\.(jpg|jpeg|gif|png))_((\d+\-)|(\-\d+))\.(jpg|jpeg|gif|png)$ {
root $img_thumbnail_root; # root path for croped img
set $img_size $3;

if (!-f $img_file) { # if file not exists
add_header X-Powered-By 'Nginx+Lua+GraphicsMagick By Botao'; # header for test
add_header file-path $request_filename; # header for test
set $request_filepath $img_original_root$1; # origin_img full path:/document_root/1.gif
set $img_size $3; # img width or height size depends on uri
set $img_ext $2; # file ext
content_by_lua_file /usr/local/nginx/lua/autoSize.lua; # load lua
}
}

# like: /xx/xx/xx.jpg_100x100.jpg
location ~* ^(.+\.(jpg|jpeg|gif|png))_(\d+)+x(\d+)+\.(jpg|jpeg|gif|png)$ {
root $img_thumbnail_root; # root path for croped img

if (!-f $img_file) { # if file not exists
add_header X-Powered-By 'Nginx+Lua+GraphicsMagick By Botao'; # header for test
add_header file-path $request_filename; # header for test
set $request_filepath $img_original_root$1; # origin_img file path
set $img_width $3; # img width
set $img_height $4; # height
set $img_ext $5; # file ext
content_by_lua_file /usr/local/nginx/lua/cropSize.lua; # load lua
}
}

# if need (all go there)
location ~* /upload {
root $img_original_root;
}


location ~ /\.ht {
deny all;
}
}

lua脚本

  • /usr/local/tengine/lua/autoSize.lua
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
-- 根据输入长或宽的尺寸自动裁切图片大小

-- 检测路径是否目录
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

-- 获取文件路径
function getFileDir(filename)
return string.match(filename, "(.+)/[^/]*%.%w+$") --*nix system
end

-- 获取文件名
function strippath(filename)
return string.match(filename, ".+/([^/]*%.%w+)$") -- *nix system
end

--去除扩展名
function stripextension(filename)
local idx = filename:match(".+()%.%w+$")
if (idx) then
return filename:sub(1, idx - 1)
else
return filename
end
end

--获取扩展名
function getExtension(filename)
return filename:match(".+%.(%w+)$")
end

function getImgSize(img)

end

-- 判断尺寸是否合法
-- check image size
function table.contains(table, element)
for _, value in pairs(table) do
if value == element then
return true
end
end
return false
end

if image_sizes_check
then
if not table.contains(image_sizes, ngx.var.img_size)
then
ngx.exit(404);
end
end
-- check image end

-- 开始执行
-- ngx.log(ngx.ERR, getFileDir(ngx.var.img_file));

local gm_path = 'gm'

-- check image dir
if not is_dir(getFileDir(ngx.var.img_file)) then
os.execute("mkdir -p " .. getFileDir(ngx.var.img_file))
end

-- 获取高宽 100!或!100模式
local uri = ngx.var.img_size
local width = string.sub(uri,1,1)
local height = 0

if width == "-" then
width = 0
height = string.sub(uri,2,string.len(uri))
else
width = string.sub(uri,1,string.len(uri)-1)
height = 0
end
-- ngx.log(ngx.ERR,uri)
-- ngx.log(ngx.ERR,width)
-- ngx.log(ngx.ERR,height)
-- ngx.log(ngx.ERR,ngx.var.img_file);
-- ngx.log(ngx.ERR,ngx.var.request_filepath);
-- 裁剪后保证等比缩图 (缺点:裁剪了图片的一部分)
-- 如: gm convert autoSize.jpg -resize x200 -quality 100 +profile "*" autoSize.jpg_-200.jpg
if (file_exists(ngx.var.request_filepath)) then
local cmd = gm_path .. ' convert -auto-orient ' .. ngx.var.request_filepath
if height == 0 then
cmd = cmd .. " -resize " .. width .. "x" .. ""
else
cmd = cmd .. " -resize " .. "x" .. height .. ""
end

-- 由于压缩后比较模糊,默认图片质量为100,请根据自己情况修改quality
cmd = cmd .. " -quality 100"
cmd = cmd .. " +profile \"*\" " .. ngx.var.img_file;
ngx.log(ngx.ERR, cmd);
os.execute(cmd);
ngx.exec(ngx.var.uri);
else
ngx.exit(ngx.HTTP_NOT_FOUND);
end
  • /usr/local/tengine/lua/cropSize.lua
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
-- 根据输入长和宽的尺寸裁切图片

-- 检测路径是否目录
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

-- 获取文件路径
function getFileDir(filename)
return string.match(filename, "(.+)/[^/]*%.%w+$") --*nix system
end

-- 获取文件名
function strippath(filename)
return string.match(filename, ".+/([^/]*%.%w+)$") -- *nix system
end

--去除扩展名
function stripextension(filename)
local idx = filename:match(".+()%.%w+$")
if (idx) then
return filename:sub(1, idx - 1)
else
return filename
end
end

--获取扩展名
function getExtension(filename)
return filename:match(".+%.(%w+)$")
end

-- 判断尺寸是否合法
-- 待切割的图片尺寸
local img_width_height = ngx.var.img_width .. "x" .. ngx.var.img_height;
-- check image size
function table.contains(table, element)
for _, value in pairs(table) do
if value == element then
return true
end
end
return false
end

if image_sizes_check
then
if not table.contains(image_sizes, img_width_height)
then
ngx.exit(404);
end
end
-- check image end

-- 开始执行
-- ngx.log(ngx.ERR, getFileDir(ngx.var.img_file));

local gm_path = 'gm'

-- check image dir
if not is_dir(getFileDir(ngx.var.img_file)) then
os.execute("mkdir -p " .. getFileDir(ngx.var.img_file))
end

-- ngx.log(ngx.ERR,ngx.var.img_file);
-- ngx.log(ngx.ERR,ngx.var.request_filepath);

-- 裁剪后保证等比缩图 (缺点:裁剪了图片的一部分)
-- gm convert cropSize.jpg -thumbnail 300x300^ -gravity center -extent 300x300 -quality 100 +profile "*" cropSize.jpg_300x300.jpg
if (file_exists(ngx.var.request_filepath)) then
local cmd = gm_path .. ' convert -auto-orient ' .. ngx.var.request_filepath
cmd = cmd .. " -thumbnail " .. ngx.var.img_width .. "x" .. ngx.var.img_height .. "^"
cmd = cmd .. " -gravity center -extent " .. ngx.var.img_width .. "x" .. ngx.var.img_height

-- 由于压缩后比较模糊,默认图片质量为100,请根据自己情况修改quality
cmd = cmd .. " -quality 100"
cmd = cmd .. " +profile \"*\" " .. ngx.var.img_file;
-- ngx.log(ngx.ERR, cmd);
os.execute(cmd);
ngx.exec(ngx.var.uri);
else
ngx.exit(ngx.HTTP_NOT_FOUND);
end

然后 nginx -s reload下

测试效果

在/var/filebase/upload目录,放1张图片

upload successful
原图

upload successful
100x100

upload successful
由于我们开启了image_sizes_check,不支持的尺寸会返回404

upload successful
固定定宽高自适应

upload successful
查看服务器上生成的文件

PS:这里有个问题,就是图片缩放后,图片的方向跟原先不一致的问题,后来笔者查了下GraphicsMagick资料,关于图片缩放后改变图片方向的问题,可以用-auto-orient参数可以来解决的,可以写成”gm convert -auto-orient “

参考资料

https://github.com/botaozhao/nginx-lua-GraphicsMagick
https://www.fanhaobai.com/2017/09/lua-in-nginx.html
https://github.com/openresty/lua-nginx-module/issues/1533
https://github.com/openresty/lua-nginx-module/pull/1501#issuecomment-486123650
https://github.com/openresty/lua-resty-core/issues/248
https://segmentfault.com/a/1190000011093243
https://github.com/yanue/nginx-lua-GraphicsMagick/blob/master/nginx-install.md
http://www.icode9.com/content-3-77987.html
https://my.oschina.net/ranhai/blog/1797454
https://www.twblogs.net/a/5bafdb372b7177781a0f64d7/zh-cn