详解用node-images 打造简易图片服务器


Posted in Javascript onMay 08, 2017

Edit:2016-5-11 修正了代码里面一些明显的错误,并发布在 ajaxjs 库之中,源码在这里。

Edit:2016-5-24 加入 HEAD 请求,检测图片大小。如果小于 80kb 则无须压缩,返回 302 重定向。

node HEAD 请求

var http = require('http'); 
var url = require('url'); 
var siteUrl = url.parse('http://img1.gtimg.com/view/pics/hv1/42/80/2065/134297067.jpg'); 
 
request = http.request({ 
  method : 'HEAD', 
  port: siteUrl.port || 80, 
  host: siteUrl.host, 
  path : siteUrl.pathname 
}); 
request.on('response', function (response) { 
  response.setEncoding('utf8'); 
  console.log(response.headers['content-length']); 
}); 
request.end();

必须先赞下国人 npm 库作品:node-images(https://github.com/zhangyuanwei/node-images),封装了跨平台的 C++ 逻辑,形成 nodejs API 让我们这些小白愉快地使用。之前用过 GraphicsMagick for nodejs,功能最强大,但包体积也比较大,依赖度高,最近好像还爆出了漏洞事件。node-images 相比 GM,主要是更轻量级,无需安装任何图像处理库。

安装 node-images:

npm install images

npm 包比较大,node_modules 里面有个 node-images.tar.gz 压缩包,下载完之后可以删掉,但剩余也有 11mb。

图片服务器,当前需求是:一个静态服务器,支持返回 jpg/png/gif 即可;支持 HTTP 缓存;支持指定图片分辨率;支持远程图片加载。加载远程图片,可通过设置 maxLength 来限制图片文件大小。

实施过程中,使用 Step.js 参与了异步操作,比较简单。

服务器的相关配置:

// 配置对象。 
var staticFileServer_CONFIG = { 
  'host': '127.0.0.1',      // 服务器地址 
  'port': 3000,        // 端口 
  'site_base': 'C:/project/bigfoot',   // 根目录,虚拟目录的根目录 
  'file_expiry_time': 480,    // 缓存期限 HTTP cache expiry time, minutes 
  'directory_listing': true    // 是否打开 文件 列表 
};

请求例子:

http://localhost:3001/asset/coming_soon.jpg?w=300
http://localhost:3001/asset/coming_soon.jpg?h=150
http://localhost:3001/asset/coming_soon.jpg?w=300&h=150
http://localhost:3001/?url=http://s0.hao123img.com/res/img/logo/logonew.png

完整源码:

const 
  HTTP = require('http'), PATH = require('path'), fs = require('fs'), CRYPTO = require('crypto'), 
  url = require("url"), querystring = require("querystring"), 
  Step = require('./step'), images = require("images"); 
 
// 配置对象。 
var staticFileServer_CONFIG = { 
  'host': '127.0.0.1',      // 服务器地址 
  'port': 3001,            // 端口 
  'site_base': 'C:/project/bigfoot',     // 根目录,虚拟目录的根目录 
  'file_expiry_time': 480,    // 缓存期限 HTTP cache expiry time, minutes 
  'directory_listing': true    // 是否打开 文件 列表 
}; 
 
const server = HTTP.createServer((req, res) => { 
  init(req, res, staticFileServer_CONFIG); 
}); 
 
server.listen(staticFileServer_CONFIG.port, staticFileServer_CONFIG.host, () => { 
  console.log(`Image Server running at http://${staticFileServer_CONFIG.host}:${staticFileServer_CONFIG.port}/`); 
}); 
 
// 当前支持的 文件类型,你可以不断扩充。 
var MIME_TYPES = { 
  '.txt': 'text/plain', 
  '.md': 'text/plain', 
  '': 'text/plain', 
  '.html': 'text/html', 
  '.css': 'text/css', 
  '.js': 'application/javascript', 
  '.json': 'application/json', 
  '.jpg': 'image/jpeg', 
  '.png': 'image/png', 
  '.gif': 'image/gif' 
}; 
 
MIME_TYPES['.htm'] = MIME_TYPES['.html']; 
 
var httpEntity = { 
  contentType: null, 
  data: null, 
  getHeaders: function (EXPIRY_TIME) { 
    // 返回 HTTP Meta 的 Etag。可以了解 md5 加密方法 
    var hash = CRYPTO.createHash('md5'); 
    //hash.update(this.data); 
    //var etag = hash.digest('hex'); 
 
    return { 
      'Content-Type': this.contentType, 
      'Content-Length': this.data.length, 
      //'Cache-Control': 'max-age=' + EXPIRY_TIME, 
      //'ETag': etag 
    }; 
  } 
}; 
 
function init(request, response) { 
  var args = url.parse(request.url).query,     //arg => name=a&id=5  
    params = querystring.parse(args); 
 
  if (params.url) { 
    getRemoteImg(request, response, params); 
  } else { 
    load_local_img(request, response, params); 
  } 
} 
 
// 加载远程图片 
function getRemoteImg(request, response, params) { 
  var imgData = ""; // 字符串 
  var size = 0; 
  var chunks = []; 
 
  Step(function () { 
      HTTP.get(params.url, this); 
    }, 
    function (res) { 
      var maxLength = 10; // 10mb 
      var imgData = ""; 
      if (res.headers['content-length'] > maxLength * 1024 * 1024) { 
        server500(response, new Error('Image too large.')); 
      } else if (!~[200, 304].indexOf(res.statusCode)) { 
        server500(response, new Error('Received an invalid status code.')); 
      } else if (!res.headers['content-type'].match(/image/)) { 
        server500(response, new Error('Not an image.')); 
      } else { 
        // res.setEncoding("binary"); //一定要设置response的编码为binary否则会下载下来的图片打不开 
        res.on("data", function (chunk) { 
          // imgData+=chunk; 
          size += chunk.length; 
          chunks.push(chunk); 
        }); 
 
        res.on("end", this); 
      } 
       
    }, 
    function () {  
      imgData = Buffer.concat(chunks, size); 
     
      var _httpEntity = Object.create(httpEntity); 
      _httpEntity.contentType = MIME_TYPES['.png']; 
      _httpEntity.data = imgData; 
      // console.log('imgData.length:::' + imgData.length) 
      // 缓存过期时限 
      var EXPIRY_TIME = (staticFileServer_CONFIG.file_expiry_time * 60).toString(); 
      response.writeHead(200); 
      response.end(_httpEntity.data); 
    } 
  ); 
   
} 
 
 
// 获取本地图片 
function load_local_img(request, response, params) { 
  if (PATH.extname(request.url) === '') { 
    // connect.directory('C:/project/bigfoot')(request, response, function(){}); 
    // 如果 url 只是 目录 的,则列出目录 
    console.log('如果 url 只是 目录 的,则列出目录'); 
    server500(response, '如果 url 只是 目录 的,则列出目录@todo'); 
  } else { 
    var pathname = require('url').parse(request.url).pathname; 
    // 如果 url 是 目录 + 文件名 的,则返回那个文件 
    var path = staticFileServer_CONFIG.site_base + pathname; 
 
    Step(function () { 
      console.log('请求 url :' + request.url + ', path : ' + pathname); 
      fs.exists(path, this); 
    }, function (path_exists, err) { 
      if (err) { 
        server500(response, '查找文件失败!'); 
        return; 
      } 
      if (path_exists) { 
        fs.readFile(path, this); 
      } else { 
        response.writeHead(404, { 'Content-Type': 'text/plain;charset=utf-8' }); 
        response.end('找不到 ' + path + '\n'); 
      } 
    }, function (err, data) { 
      if (err) { 
        server500(response, '读取文件失败!'); 
        return; 
      } 
      var extName = '.' + path.split('.').pop(); 
      var extName = MIME_TYPES[extName] || 'text/plain'; 
 
      var _httpEntity = Object.create(httpEntity); 
      _httpEntity.contentType = extName; 
      var buData = new Buffer(data); 
      //images(buData).height(100); 
 
      var newImage; 
 
      if (params.w && params.h) { 
        newImage = images(buData).resize(Number(params.w), Number(params.h)).encode("jpg", { operation: 50 }); 
      } else if (params.w) { 
        newImage = images(buData).resize(Number(params.w)).encode("jpg", { operation: 50 }); 
      } else if (params.h) { 
        newImage = images(buData).resize(null, Number(params.h)).encode("jpg", { operation: 50 }); 
      } else { 
        newImage = buData; // 原图 
      } 
 
      _httpEntity.data = newImage; 
 
      // 命中缓存,Not Modified返回 304 
      if (request.headers.hasOwnProperty('if-none-match') && request.headers['if-none-match'] === _httpEntity.ETag) { 
        response.writeHead(304); 
        response.end(); 
      } else { 
        // 缓存过期时限 
        var EXPIRY_TIME = (staticFileServer_CONFIG.file_expiry_time * 60).toString(); 
 
        response.writeHead(200, _httpEntity.getHeaders(EXPIRY_TIME)); 
        response.end(_httpEntity.data); 
      } 
    }); 
  } 
} 
function server500(response, msg) { 
  console.log(msg); 
  response.writeHead(404, { 'Content-Type': 'text/plain;charset=utf-8' }); 
  response.end(msg + '\n'); 
} 
加水印也是可以的。例子如下。
var images = require('images'); 
var path = require('path'); 
var watermarkImg = images(path.join(__dirname, 'path/to/watermark.ext')); 
var sourceImg = images(path.join(__dirname, 'path/to/sourceImg.ext')); 
var savePath = path.join(__dirname, 'path/to/saveImg.jpg'); 
 
// 比如放置在右下角,先获取原图的尺寸和水印图片尺寸 
var sWidth = sourceImg.width(); 
var sHeight = sourceImg.height(); 
var wmWidth = watermarkImg.width(); 
var wmWidth = watermarkImg.height(); 
 
images(sourceImg) 
 // 设置绘制的坐标位置,右下角距离 10px 
 .draw(watermarkImg, sWidth - wmWidth - 10, sHeight - wmHeight - 10) 
 // 保存格式会自动识别 
 .save(savePath);

  以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jquery 简单图片导航插件jquery.imgNav.js
Mar 17 Javascript
在JS中最常看到切最容易迷惑的语法(转)
Oct 29 Javascript
原生JavaScript实现合并多个数组示例
Sep 21 Javascript
JS设置网页图片vspace和hspace属性的方法
Apr 01 Javascript
JavaScript动态添加style节点的方法
Jun 09 Javascript
AngularJS控制器继承自另一控制器
May 09 Javascript
jQuery根据name属性进行查找的用法分析
Jun 23 Javascript
通过网页查看JS源码中汉字显示乱码的解决方法
Oct 26 Javascript
基于JavaScript实现购物车功能
Feb 07 Javascript
详解JS中的柯里化(currying)
Aug 17 Javascript
vue input实现点击按钮文字增删功能示例
Jan 29 Javascript
bootstrap-paginator服务器端分页使用方法详解
Feb 13 Javascript
vue.js中Vue-router 2.0基础实践教程
May 08 #Javascript
angular实现IM聊天图片发送实例
May 08 #Javascript
Angular.Js中过滤器filter与自定义过滤器filter实例详解
May 08 #Javascript
canvas简单快速的实现知乎登录页背景效果
May 08 #Javascript
详解JavaScript中return的用法
May 08 #Javascript
如何使用angularJs
May 08 #Javascript
关于foreach循环中遇到的问题小结
May 08 #Javascript
You might like
模仿OSO的论坛(五)
2006/10/09 PHP
php生成数组的使用示例 php全组合算法
2014/01/16 PHP
php断点续传之文件分割合并详解
2016/12/13 PHP
关于Yii2框架跑脚本时内存泄漏问题的分析与解决
2019/12/01 PHP
Javascript的IE和Firefox兼容性汇编
2006/07/01 Javascript
JavaScript实现找出字符串中第一个不重复的字符
2014/09/03 Javascript
jquery调取json数据实现省市级联的方法
2015/01/29 Javascript
简化版手机端照片预览组件
2015/04/13 Javascript
js实现非常简单的焦点图切换特效实例
2015/05/07 Javascript
跟我学习javascript的for循环和for...in循环
2015/11/18 Javascript
node.js实现爬虫教程
2020/08/25 Javascript
jQuery动态加载css文件实现方法
2016/06/15 Javascript
jQuery简单获取DIV和A标签元素位置的方法
2017/02/07 Javascript
jQuery+pjax简单示例汇总
2017/04/21 jQuery
webpack实用小功能介绍
2018/01/02 Javascript
webpack+vue-cil中proxyTable处理跨域的方法
2018/07/20 Javascript
JS解析后台返回的JSON格式数据实例
2018/08/06 Javascript
微信小程序中的店铺评分组件及vue中用svg实现的评分显示组件
2018/11/16 Javascript
vue插件draggable实现拖拽移动图片顺序
2018/12/01 Javascript
layUI实现前端分页和后端分页
2019/07/27 Javascript
vue.js实现简单的计算器功能
2020/02/22 Javascript
原生JS实现天气预报
2020/06/16 Javascript
[01:56]《DOTA2》中文配音CG
2013/04/22 DOTA
python的三目运算符和not in运算符使用示例
2014/03/03 Python
让python在hadoop上跑起来
2016/01/27 Python
python提取log文件内容并画出图表
2019/07/08 Python
Python中的list与tuple集合区别解析
2019/10/12 Python
Django app配置多个数据库代码实例
2019/12/17 Python
POP文化和音乐灵感的时尚:Hot Topic
2019/06/19 全球购物
英国家具、照明、家居用品网上商店:Wayfair.co.uk
2020/02/13 全球购物
J2SDK1.5与J2SDK5.0有什么区别
2012/09/19 面试题
简述你对Statement,PreparedStatement,CallableStatement的理解
2013/03/25 面试题
会计专业大学生职业生涯规划书
2014/02/11 职场文书
文秘专业应届生求职信
2014/05/26 职场文书
钱学森观后感
2015/06/04 职场文书
《称赞》教学反思
2016/02/17 职场文书