用imgproxy自动缩放图片

记一次惊险之旅,最终成功地用imgproxy实现了图片的自动缩放。

Imagem de capa

无图,纯干货,信息量较大,慎入!

最近几天的成果,浓缩下来就是这么一行代码:

document.getElementById('img1').src =
  'http://www.mysite.com/imgproxy' +
  imgproxy(document.getElementById('img1').getAttribute('data-src'), 135, 85);

寻找合适的图床

最初的时候只是看我的个人博客图片大小高低不一,比较难看,试图找一种方法能够统一各图片的高度。在网上搜索的结果是,发现了几个Jykell的插件,例如jekyll-picture-tag,通过这个过程学到了不少东西,比如 img 标签除了有 srcset 以外,还有一个额外的 Picture 标签等等。本来想用这个插件,但另外一个插件jekyll-cloudinary的作者说 Picture 标签并不好,应该直接使用Cloudinary的服务。

由此而想起在我上一篇文章中提到过的一篇教程中谈到过的用国内的七牛云做图床,于是开始尝试把我网站文章中用到的图片往七牛云搬家,图片搬家不是问题,但又想在博客网站上增加 https 服务,于是在问过我的朋友马壮之后,在Cloudflare上开通了 https 服务,但这又造成另外一个问题:七牛云上虽然放了我的图片,但是七牛云本身不支持 https 服务,于是又得想办法把图片搬到 Cloudinary。

至此我个人的博客算是可以告一段落。平心而论,七牛云的预置功能还是很不错的,至少它对于URL的处理方式比Cloudinary要简单,但唯一的遗憾是它不支持https。而如果图片不支持https而网站使用https的话,Chrome会在Console里报警告错误,而我对网站的要求是:一个警告都不能有。

URL 自动调整图片

在此过程中,我开始思考一个问题:既然Cloudinary七牛云都提供基于URL地址的图片变换,那么它们是怎么做到的呢?根据我对PHP的粗浅了解,最笨的方法可以直接以PHP读文件的方式从硬盘先读取图片的源文件,然后经转换后再以流的方式输出给页面,但这样效率肯定极低。于是经过搜索后发现了很多人推荐的libvips库,再进一步搜索,在Github上发现了有很多颗星的imgproxy这个库,似乎这就是我想要的东西。

于是我开始尝试动手往公司的服务器上部署imgproxy。但这时候遇到一个问题,在CentOS上,imgproxy并没有yum安装包,还需要先手工安装libvips,然后再编译,而最要命的是,公司的服务器在国内,无法通过wget的方式直接安装国外的软件包,由此而我需要先把安装包下载到本地,然后再上传到公司的服务器上。这时候我又想取个巧,使用 iterm 内置的 scp 用鼠标拖拽的方式上传文件。按照操作步骤的说明,安装好了之后却发现itermscp按钮依然是灰色的,这时才发现是由于服务器上的fish版本过低,只有1.3,而最新的已经是2.6了。于是安装 2.6 的 repo,尝试更新fish,却总是报冲突。由此而想到将fish 1.3先卸载,就在这时灾难发生了。

灾难

我直接执行了yum remove fish,但是在做这一步之前,我没有将root用户的shell切换回bash,由此而导致了root用户找不到它的shell,因为它还在试图寻找fish。这是一个致命的错误,我记得自己当时隐隐约约有预感,但还是没有特别在意,觉得也许Linux系统会自动为root用户赋予一个缺省的shell。结果我高估了Linux系统的能力。

退出登录之后,我发现root用户登录不上了!如果不仔细观察的话,你会感觉它的不能登录的症状和密码错误非常类似,但实际表现其实略有不同,在SSH端是不大看得出来的。我的第一反应是,如果root用户无法通过SSH登录了,那么应该通过console端登录。

但当天下午,令人惊讶的是连console端也登不上了!这时候我意识到问题严重了。在网上搜索的结果是有人说应该以 runlevel 1 的方式登录,然后尝试修复/etc/shadow。但我完全不了解对于一台云主机应该如何进入runlevel 1。只好提工单给客服。而客服的技术水平大家应该是知道的,只是建议我重置密码之后再尝试一下。而重置密码必须要关机再重启,就这样来回折腾了很久也修不好。

在经过了漫长的等待之后,终于惊动了一个技术人员。他指出如果我必须要进runlevel 1的话,可以在系统开机的前 3 秒之间按下键盘的 e 键,然后就可以进入runlevel 1了。

但问题是这是一台云主机,如何能在开机前 3 秒按键呢?好在现在云主机的console功能非常发达,你可以开着console重启,这时候网络断掉,然后不停地刷新console,你会在电脑开机的一瞬间看到一个有字的黑画面,这时候迅速按下e键也能进入系统。然后再次按下e,把启动模式修改为Linux single

按照他的指导,我终于能够以runlevel 1的方式进入了系统,首先尝试用/etc/passwd重建/etc/shadow,再次重启,无果,还是登录不进去。至此为止,所有关于密码的努力均告失败。我想,唯一的办法只能尝试看能不能切换root用户的shell

chsh -s /bin/bash

root用户的shell切换成bash之后,再次重启电脑,果然可以成功登录了!

修复

接下来,我还是需要安装fish,但yum install fish结果fish还是1.3。我还要继续上次不成功的征程。再次把fish1.3换成2.6。依然冲突。这次我学精了,我先把rootshell脚本切换成bash,然后yum remove fish,再次安装,发现这个fish 1.3的来源是一个不知什么时候装上的名叫dagrepo,于是尝试把这个dagrepo禁止掉:

yum-config-manager --disable dag

然后再次安装,终于装上了fish 2.6

至此,基本所有阻塞性因素都消除了,我开始将libvips的代码拖拽进服务器,然后编译。但这时候问题又来了,imgproxy必须运行在docker里,而说明文档上只说需要自己build一个docker,但并没有指明以什么操作系统为基础去build,好在官方提供了一个它们自己的docker文件,可以直接运行imgproxy

啊!早知如此,我何必折腾这么一大圈?还差点毁掉了我的系统。不过好在学到了不少东西。好吧,于是我们开始直接安装使用imgproxy官方提供的docker

docker pull darthsim/imgproxy:latest
docker run -e IMGPROXY_KEY=$YOUR_KEY -e IMGPROXY_SALT=$YOUR_SALT -p 8080:8080 -t darthsim/imgproxy

但是这个imgproxy的使用方式又是非常的不友好,它完全不像七牛云或者Cloudinary那样直接在URL地址上构建就行了,它需要自己根据自己的keysalt产生签名,然后再用签名构建URL,它给了各种语言的例子,唯独没有java的,最后我只好根据它自己的javascript语言的例子构建一个js代码,用于替换页面中的图片链接。

编程

但问题又来了,它给定的这个包是一个nodejs脚本,里面有require语句,无法直接用于浏览器。这时候又得请出browerify,用它来编译node的脚本为可以供浏览器直接使用的脚本。好在过程并不复杂,编译之后得到的bundle.js文件,我们直接在页面中引用就行了。于是就得到了本文开头的一行代码:

<!DOCTYPE html>
<html>
  <head> </head>
  <body>
    <img id="img1" data-src="http://www.mysite.com/img/somepic.png" src="" />
    <script src="bundle.js"></script>
    <script>
      window.onload = function() {
        document.getElementById('img1').src =
          'http://www.mysite.com/imgproxy' +
          imgproxy(
            document.getElementById('img1').getAttribute('data-src'),
            135,
            85
          );
      };
    </script>
  </body>
</html>

以及相关的 js:

window.imgproxy = function(url, width, height) {
  const crypto = require('crypto');

  const KEY = 'somekey';
  const SALT = 'somesalt';

  const urlSafeBase64 = string => {
    return new Buffer(string)
      .toString('base64')
      .replace(/=/g, '')
      .replace(/\+/g, '-')
      .replace(/\//g, '_');
  };

  const hexDecode = hex => Buffer.from(hex, 'hex');

  const sign = (salt, target, secret) => {
    const hmac = crypto.createHmac('sha256', hexDecode(secret));
    hmac.update(hexDecode(salt));
    hmac.update(target);
    return urlSafeBase64(hmac.digest());
  };

  const resizing_type = 'fit';
  const gravity = 'no';
  const enlarge = 0;
  const extension = 'jpg';
  const encoded_url = urlSafeBase64(url);
  const path = `/${resizing_type}/${width}/${height}/${gravity}/${enlarge}/${encoded_url}.${extension}`;

  const signature = sign(SALT, path, KEY);
  const result = `/${signature}${path}`;
  return result;
};

当然你需要npm install crypto,然后编译:

browserify main.js > bundle.js

你可以把你自己得到的URL去和这个网站生成的URL做对比,如果完全一致,就说明你的代码配置正确,否则就还是有可能不成功。

这就是这两天来的结果。我学到了不少东西,你学到了吗?