背景
大多數的系統都會涉及縮略圖的處理,比如新聞系統和電商系統,特別是電商系統,每個商品大圖都會對應一系列尺寸的縮略圖用於不同業務場景的使用。部分系統也會生成不同尺寸的縮略圖以供PC、手機端、ipad端使用。
解決方案探索:
- 直接加載原圖,使用css樣式表來控制圖片的寬高。顯然不太合適,大家也儘量不要這樣做。
- web程序在上傳成功後,同時生成相應縮略圖。這種做法效率較低,如果遇到批量導入的業務時嚴重影響性能。有些圖片的縮略圖很少使用到,如果能按需生成豈不更好?
- 使用第三方提供的雲存儲及數據處理服務,解決圖片的處理、存儲、多節點訪問速度的問題,這種方式優點是方案成熟,相應的有一定費用和開發工作,另外有一些小概率的風險,比如雲服務掛掉影響本站訪問。
本文使用的是Nginx+Lua+GraphicsMagick實現縮略圖功能,圖片的上傳及刪除還是交由web服務處理,縮略圖由單獨的模塊去完成。最終效果類似淘寶圖片,實現自定義圖片尺寸功能,可根據圖片加後綴100x100.jpg(固定高寬),-100.jpg(定高),_100-.jpg(定寬)形式實現自定義輸出圖片大小。
github源碼地址:https://github.com/botaozhao/nginx-lua-GraphicsMagick
更新說明
2018-2-9: 加入縮略圖尺寸限制,需在demo.conf中配置開關及允許的尺寸,代碼片段爲:
init_by_lua '
-- 開關 需要限制縮略圖尺寸:true ,不需要限制縮略圖尺寸:false
image_sizes_check = true
-- 允許的尺寸
image_sizes = {"800x800", "400x400", "100x100", "-800", "-400", "-100", "800-", "400-", "100-"}
';
說明
文件夾規劃
img.xxx.com(如/usr/local/filebase)
├─upload
│ └─img
│ ├─001.jpg
│ └─002.jpg
自定義尺寸後的路徑
thumb(/tmp/thumb,可在conf文件裏面更改)
├─upload
│ └─img
│ ├─001.jpg_100x100.jpg #固定高和寬
│ ├─001.jpg_-100.jpg #定高
│ ├─001.jpg_200-.jpg #定寬
│ └─002.jpg_300x300.jpg #固定高和寬
- 其中img.xxx.com爲圖片站點根目錄,img目錄是原圖目錄
- 縮略圖目錄根據保持原有結構,並單獨設置目錄,可定時清理。
鏈接地址對應關係
原圖訪問地址:http://img.xxx.com/upload/img/001.jpg
縮略圖訪問地址:http://img.xxx.com/upload/img/001.jpg_100x100.jpg 即爲寬100,高100
自動寬地址: http://img.xxx.com/upload/img/001.jpg_-100.jpg 用"-"表示自動,即爲高100,寬自動
自動高地址: http://img.xxx.com/upload/img/001.jpg_200-.jpg 用"-"表示自動,即爲寬200,高自動
訪問流程
- 首先判斷縮略圖是否存在,如存在則直接顯示縮略圖;
- 縮略圖不存在,則判斷原圖是否存在,如原圖存在則拼接graphicsmagick(gm)命令,生成並顯示縮略圖,否則返回404
安裝
系統環境
centOS7 X64 虛擬機內最小化安裝
以下操作均在此係統中操作,僅供參考
1、環境準備
yum install -y wget git
yum install -y gcc gcc-c++ zlib zlib-devel openssl openssl-devel pcre pcre-devel
yum install -y libpng libjpeg libpng-devel libjpeg-devel ghostscript libtiff libtiff-devel freetype freetype-devel
yum install -y GraphicsMagick GraphicsMagick-devel
如果提示沒有GraphicsMagick的可用安裝包,請自行安裝GraphicsMagick,具體可參考我的另一篇文章:CentOS7下安裝GraphicsMagick1.3.21。
2、下載相關應用
cd /usr/local/src
wget http://nginx.org/download/nginx-1.8.0.tar.gz
wget http://luajit.org/download/LuaJIT-2.0.4.tar.gz
wget http://zlib.net/fossils/zlib-1.2.8.tar.gz
3、下載nginx組件
git clone https://github.com/alibaba/nginx-http-concat.git
git clone https://github.com/simpl/ngx_devel_kit.git
git clone https://github.com/openresty/echo-nginx-module.git
git clone https://github.com/openresty/lua-nginx-module.git
解壓安裝
tar -zxf nginx-1.8.0.tar.gz
tar -zxf LuaJIT-2.0.4.tar.gz
tar -zxf zlib-1.2.8.tar.gz
1、安裝LuaJIT
cd LuaJIT-2.0.4
make -j8
make install
export LUAJIT_LIB=/usr/local/lib
export LUAJIT_INC=/usr/local/include/luajit-2.0
ln -s /usr/local/lib/libluajit-5.1.so.2 /lib64/libluajit-5.1.so.2
cd ..
2、安裝nginx
cd nginx-1.8.0
./configure --prefix=/usr/local/nginx \
--sbin-path=/usr/local/nginx/sbin/nginx \
--conf-path=/usr/local/nginx/conf/nginx.conf \
--pid-path=/usr/local/nginx/pid/nginx.pid \
--lock-path=/usr/local/nginx/pid/nginx.lock \
--error-log-path=/usr/local/nginx/logs/error.log \
--http-log-path=/usr/local/nginx/logs/access.log \
--with-http_ssl_module \
--with-http_realip_module \
--with-http_sub_module \
--with-http_flv_module \
--with-http_dav_module \
--with-http_gzip_static_module \
--with-http_stub_status_module \
--with-http_addition_module \
--with-http_spdy_module \
--with-pcre \
--with-zlib=../zlib-1.2.8 \
--add-module=../nginx-http-concat \
--add-module=../lua-nginx-module \
--add-module=../ngx_devel_kit
make -j8
make install
編譯nginx常見問題
./configure: error: invalid option "–with-http_spdy_module"
nginx 1.9.5 已經沒有了 --with-http_spdy_module ,取代的是 --with-http_v2_module,若使用該版本或者更高版本請自行替換。
官方說明文檔:http://nginx.org/en/docs/http/ngx_http_v2_module.html
配置
相關配置文件結構位置
/usr/local/nginx
│ ├─conf
│ ├─......
│ └─nginx.conf
│ ├─html
│ ├─logs
│ ├─lua
│ ├─autoSize.lua
│ └─cropSize.lua
│ ├─pid
│ ├─sbin
│ └─vhost
│ └─demo.conf
相關的配置文件可以去我的github上去下載,地址爲:nginx-lua-GraphicsMagick。
下面貼出詳細的配置文件內容,和github上一致,可直接跳過。
修改nginx配置文件
cd /usr/local/nginx/
vi conf/nginx.conf
user root;
worker_processes 4;
worker_cpu_affinity 1000 0100 0010 0001;
error_log /usr/local/nginx/logs/error.log error;
pid /usr/local/nginx/pid/nginx.pid;
worker_rlimit_nofile 65535;
events
{
use epoll;
worker_connections 65535;
}
http
{
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 200m;
#lua_package_path "/etc/nginx/lua/?.lua";
include /usr/local/nginx/vhost/*.conf;
}
修改站點配置
普通站點的配置文件,包含固定高寬和定高,定寬兩種模式配置
cd /usr/local/nginx/
mkdir vhost
vi vhost/demo.conf
#定義lua縮略圖支持的圖片尺寸及開關
init_by_lua '
-- 開關 需要限制縮略圖尺寸:true ,不需要限制縮略圖尺寸:false
image_sizes_check = true
-- 允許的尺寸
image_sizes = {"800x800", "400x400", "100x100", "-800", "-400", "-100", "800-", "400-", "100-"}
';
server {
listen 80;
index index.php index.html index.htm;
set $root_path '/var/www';
root $root_path;
location /lua {
default_type 'text/plain';
content_by_lua 'ngx.say("hello, ttlsa lua")';
}
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 /usr/local/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工具
cd /usr/local/nginx/
mkdir lua
lua文件夾下需要兩個文件
- autoSize.lua 定高或定寬模式裁切圖片處理lua腳本
-
cropSize.lua 固定高寬模式裁切圖片處理lua腳本
autoSize.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 -- 獲取文件路徑 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 ' .. 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
cropSize.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 -- 獲取文件路徑 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 ' .. 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
訪問
開啓80端口
firewall-cmd --permanent --zone=public --add-port=80/tcp firewall-cmd --reload
啓動nginx
cd /usr/local/nginx/ ./sbin/nginx
訪問查看圖片
分別訪問下面幾個地址,測試能否查看及生成縮略圖
http://XXX/upload/img/001.jpg
http://XXX/upload/img/001.jpg_-200.jpg
http://XXX/upload/img/001.jpg_200X200.jpg
效果如下:
此時服務器端也已經在相應路徑下生成了縮略圖文件:
至此,nginx+lua+GraphicsMagick生成實時縮略圖完成!優化
現在已經實現了服務器端實時生成縮略圖,爲了避免服務器被被惡意改變寬和高參數而隨意生成大量文件,浪費資源和空間,所以,我們加入了尺寸限制,具體配置已再demo.conf配置文件中標出。