基于Openresy的图片服务

背景

有个项目的需要是:用户通过给好友分享带有二维码的图片,好友扫码或在微信中识别二维码来领取分享的礼物。要实现这个需要,能想到的解决方案有两个:

方案一

客户端实现。背景图片存在客户端,客户端动态生成二维码,并完成图片的合成(包括给背景图添加二维码,文字水印,用户头像等信息)。

优点:图片和合成处理分布在每个用户的手机上,图片合成的计算压力进行了分散。用户体验更好。服务端只需要存储合成后的图片,并进行图片的加速访问。

缺点:这个方案的可行的前提是客户端当前需要具备这样的能力,而且需要需要访问用户手机的相册(也可以不访问)。现实问题是:1.马上开发上线,开发,审核,推广,用户下载都需要时间。2.用户版本不一致,不能保证覆盖面。

由于以上的问题,才有了方案二。

方案二

服务端实现。背景图片存在服务端,服务端通过图片库来完成图片的合成(二维码,水印等)。

优点:不依赖客户端。更好的动态控制能力

缺点:图片处理的压力全部在服务端。

Java 实现

一开始的方案是通过Java开发一个图片服务,通过Java库zxing来生成二维码。Thumbnails 来进行图片的合成,水印等功能。

马上开发并进行压测后发现效率一般。决定这个方案作为备选方案,抗不住就加机器。

Openresty + Lua + ImageMagic

Openresty,Lua,ImageMagic 的作用就不解释了。下面直接上配置和代码。

Openresty 配置

main block
1
2
3
4
# 二维码文件名缓存
lua_shared_dict qr_cache 10m;
# 合成后的图片缓存,生产环境可以调大
lua_shared_dict share_img_file_cache 10m;
server
1
2
3
4
5
6
7
8
9
10
11
12
13
14

server {
listen 80;
server_name localhost;

access_log /data/logs/nginx/image.access.log main;

location /images {
set $image_root "/data/static";
set $file "$image_root$uri";
set $convert_bin "/usr/local/bin/convert";
rewrite_by_lua_file "/Users/leo/workspace/lua/nginx-imagemagick.lua";
}
}

Lua合成图片代码

nginx-imagemagick.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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
local qr = require("qrencode")
local qr_cache = ngx.shared.qr_cache;
local share_img_file_cache = ngx.shared.share_img_file_cache
local imgages_root_dir = ngx.var.image_root .. "/images/"
local img_file_suffix = ".png"

-- config
local image_sizes = { "640x640", "320x320", "124x124", "140x140", "64x64", "60x60", "32x32", "0x0" }

-- 字符串分隔方法
function string:split(sep)
truelocal sep, fields = sep or ":", {}
truelocal pattern = string.format("([^%s]+)", sep)
trueself:gsub(pattern, function (c) fields[#fields + 1] = c end)
truereturn fields
end

-- parse uri

function parseUri(uri)

truelocal _, _, name, ext, size = string.find(uri, "(.+)(%..+)!(%d+x%d+)")

true--ngx.header.content_type = "text/plain";

true--ngx.say(name,size);

trueif name and size and ext then
truetruereturn ngx.var.image_root .. name .. ext, size
trueelse
truetruereturn "", ""
trueend
end

function fileExists(name)
truelocal f, msg = io.open(name, "r")
trueif f ~= nil then
truetrueio.close(f)
truetruereturn true
trueelse
truetruengx.log(ngx.ERR, msg)
truetruereturn false
trueend
end

function sizeExists(size)
truefor _, value in pairs(image_sizes) do
truetrueif value == size then
truetruetruereturn true
truetrueend
trueend
truereturn false
end

-- 返回图片
function response_image(img_file_path)
truengx.header.content_type = "image/jpg";

truelocal f = io.open(img_file_path, "rb")
truelocal content = f:read("*all")
truengx.print(content)
truef:close()
truereturn ngx.exit(ngx.OK)
end

-- 返回图片, 是二进制
function response_image_bin(img_bin)
truengx.header.content_type = "image/jpg";
truengx.print(img_bin)
true-- ngx.flush()
truereturn ngx.exit(ngx.HTTP_OK)
end

-- 缓存生成的图片
function cache_img_bin(cache_key, img_file_path)
truelocal f = io.open(img_file_path, "rb")
truelocal content = f:read("*all")
trueshare_img_file_cache[cache_key] = content
truef:close()
end

-- 图片大小转化
function resize_imgage(image_file, width_height)
truelocal src_image_path = imgages_root_dir .. image_file
truelocal target_image_path = imgages_root_dir .. image_file:split(".")[1] .. "_" .. img_file_suffix
truelocal command = table.concat(
truetrue{
truetruetruengx.var.convert_bin,
truetruetrue"-resize",
truetruetruewidth_height,
truetruetruesrc_image_path,
truetruetruetarget_image_path
truetrue},
truetrue" "
true)
true-- 进行图片处理
trueos.execute(command)
truereturn target_image_path
end

-- 生成二维码图片 - 二进制流
function gen_qr_code_bin(content)
truelocal qr_code_bin = qr {
truetruetext=content,
truetruelevel="L",
truetruekanji=false,
truetrueansi=false,
truetruesize=4,
truetruemargin=2,
truetruesymversion=0,
truetruedpi=78,
truetruecasesensitive=true,
truetrueforeground="000000",
truetruebackground="FFFFFF"
true}
truereturn qr_code_bin
end

-- 生成二维码图片文件
function gen_qr_code_file(qr_file_name, qr_code_bin)
truelocal full_qr_file_path = imgages_root_dir .. qr_file_name
truefile = io.open(full_qr_file_path, "wb")
trueio.output(file)
trueio.write(qr_code_bin)
trueio.close(file)
true-- 缩放到固定大小
true-- resize_imgage(qr_file_name, "120x120")
end

-- 返回指定内容的二维码文件
function get_qr_file(content)
truelocal qr_md5 = ngx.md5(content)

trueif qr_cache[qr_md5] ~= nil
truethen
truetruereturn qr_cache[qr_md5]
trueelse
truetruelocal qr_file_name = qr_md5 .. img_file_suffix
truetruelocal qr_code_bin = gen_qr_code_bin(content)
truetruegen_qr_code_file(qr_file_name, qr_code_bin)

truetrueqr_cache[qr_md5] = qr_file_name
truetruereturn qr_file_name
trueend
end

-- 获取生成二维码的内容
function get_qr_code_content()
true-- 解析 body 参数之前一定要先读取 body
true-- ngx.req.read_body()
true-- local arg = ngx.req.get_post_args()
true-- for k,v in pairs(arg) do
true-- ngx.say("[POST] key:", k, " v:", v)
true-- end

truelocal args = ngx.req.get_uri_args()
truelocal text = args['url']
truereturn text
end

--图片合成
function compose_img(bg_img, fg_img, result_img)
truelocal command = table.concat(
truetrue{
truetruetruengx.var.convert_bin,
truetruetruebg_img,
truetruetrue"+profile '*'",
truetruetrue"-quality 75%",
truetruetrue"-strip",
truetruetrue"-gravity northwest",
truetruetrue"-compose over " .. fg_img,
truetruetrue"-geometry +500+525",
truetruetrue"-composite -compress BZip",
truetruetrueresult_img
truetrue},
truetrue" "
true)
true-- ngx.log(ngx.ERR, command)
true-- 进行图片处理
trueos.execute(command)
end

-- 图片添加二维码,文字水印
function compose_img()
truelocal text = get_qr_code_content()
trueif text == nil or text == "" then
truetruengx.header.content_type = "text/plain"
truetruengx.say('need a text param')
truetruereturn ngx.exit(ngx.OK)
trueend
true--ngx.log(ngx.ERR, text)

truelocal sub_uris = ngx.var.uri:split("/")
truelocal file_name = sub_uris[table.getn(sub_uris)]

trueori_filename = ngx.var.image_root .. ngx.var.uri
trueif fileExists(ori_filename) == false then
truetruereturn ngx.exit(404)
trueend

truengx.header.content_type = "image/png"

true-- 生成二维码
truelocal qr_code_file = imgages_root_dir .. get_qr_file(text)
truelocal result_img = ngx.md5(text) .. "_res_" .. img_file_suffix
truelocal result_file = imgages_root_dir .. result_img

trueif share_img_file_cache[result_img] ~= nil
truethen
truetruetrue-- ngx.log(ngx.ERR, "from cache")
truetruetruereturn response_image_bin(share_img_file_cache[result_img])
trueend

true-- ngx.exit(ngx.OK)
truecompose_img(ori_filename, qr_code_file, result_file)
truecache_img_bin(result_img, result_file)

trueresponse_image(result_file)
end

compose_img()

方案二的优化和扩展

1.生成图片的缓存可以通过在openresty前面添加专门的缓存服务器(Openresty,varnish)。
2.生成的分析图片可以通过CDN进行分发,这样用户访问的速度更快。
3.合成图片的大小可以进一步压缩。目前合成的图片偏大。
4.通过这个demo,我们可以进一步丰富功能,只要是ImageMagic支持的功能,理论上都可以实现。

环境的安装配置

Openresy安装

请参考:http://openresty.org/en/installation.html

二维码生成库libqrencode安装

ubuntu:

1
sudo apt-get install libqrencode-dev libpng12-dev

CentOS

1
2
3
yum install libpng-devel
wget http://ftp.riken.jp/Linux/centos/7/os/x86_64/Packages/qrencode-devel-3.4.1-3.el7.x86_64.rpm
rpm -ivh qrencode-devel-3.4.1-3.el7.x86_64.rpm

MacOS

1
brew install libqrencode

安装Lua扩展库

1
2
git clone https://github.com/vincascm/qrencode
make && make install

make test如果能成功,控制台会显示一个二维码。

下面是一个生成二维码的配置

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
server {

listen 8080;
server_name img.papa.com.cn;

location / {
default_type image/png;
content_by_lua_block {
local qr = require("qrencode")
local args = ngx.req.get_uri_args()
local text = args.text

if text == nil or text== "" then
ngx.say('need a text param')
ngx.exit(404)
end

ngx.say(qr {
text=text,
level="L",
kanji=false,
ansi=false,
size=4,
margin=2,
symversion=0,
dpi=78,
casesensitive=true,
foreground="000000",
background="FFFFFF"
})
}
add_header Expires "Fri, 01 Jan 1980 00:00:00 GMT";
add_header Pragma "no-cache";
add_header Cache-Control "no-cache, max-age=0, must-revalidate";
#add_header Content-Type image/png;
}
}

问题

在配置真个环境的过程中,有一下几点需要注意:

1.lua的版本最好使用5.1,否则会有各种问题。
2.二维码生成的配置中ansi=false,否则在浏览器不能显示。

参考

https://github.com/vincascm/qrencode

基于 OpenResty 的二维码生成方案

Lua包管理工具Luarocks详解

OpenResty(Nginx)+Lua+GraphicsMagick实现缩略图功能

文章目录
  1. 1. 背景
  2. 2. 方案一
  3. 3. 方案二
    1. 3.1. Java 实现
    2. 3.2. Openresty + Lua + ImageMagic
  4. 4. 方案二的优化和扩展
  5. 5. 环境的安装配置
    1. 5.1. Openresy安装
    2. 5.2. 二维码生成库libqrencode安装
  6. 6. 问题
  7. 7. 参考
|