在Node.js中从子进程读取二进制数据

17

在尝试从ImageMagick子进程读取数据时,Node.js中的数据会出现损坏。

一个简单的测试案例如下:

var fs = require('fs');
var exec = require('child_process').exec;

var cmd = 'convert ./test.jpg -';
exec(cmd, {encoding: 'binary', maxBuffer: 5000*1024}, function(error, stdout) {
  fs.writeFileSync('test2.jpg', stdout);
});

我希望这个命令与命令行convert ./test.jpg - > test2.jpg相当,后者可以正确地写入二进制文件。

原来的问题在于maxBuffer选项太小,导致文件被截断。增加该选项后,文件现在似乎比预期稍大,并且仍然损坏。需要从stdout中读取数据以通过HTTP发送。

从ImageMagick stdout中读取数据的正确方式是什么?

3个回答

18

最初的方法存在两个问题。

  1. maxBuffer需要足够大,以处理来自子进程的整个响应。

  2. 二进制编码需要在所有地方正确设置。

下面是一个完整的工作示例:

var fs = require('fs');
var exec = require('child_process').exec;

var cmd = 'convert ./test.jpg -';
exec(cmd, {encoding: 'binary', maxBuffer: 5000*1024}, function(error, stdout) {
  fs.writeFileSync('test2.jpg', stdout, 'binary');
});

另一个例子,使用 Express 网络框架将数据作为 HTTP 响应发送的示例如下:

var express = require('express');
var app = express.createServer();

app.get('/myfile', function(req, res) {
  var cmd = 'convert ./test.jpg -';
  exec(cmd, {encoding: 'binary', maxBuffer: 5000*1024}, function(error, stdout) {
     res.send(new Buffer(stdout, 'binary'));
  });
});

在 res.send(...) 之后,您应该添加 res.end()。 - Merlin
1
我在想stdout已经是一个缓冲区了...好的,非常感谢! - thinklinux

7
啊,问题是:如果超时时间(timeout)大于0,则当子进程运行时间超过timeout毫秒时,它将被强制终止。子进程将使用killSignal(默认为'SIGTERM')被终止。maxBuffer指定了允许在stdout或stderr上的最大数据量 - 如果超出此值,则将终止子进程。
来源:http://nodejs.org/docs/v0.4.8/api/child_processes.html#child_process.exec 因此,如果您的图像超过默认的缓冲区大小200*1024字节,那么您的图像将会像您提到的那样损坏。我能够使用以下代码使其正常工作:
var fs = require('fs');
var spawn = require('child_process').spawn;
var util = require('util');

var output_file = fs.createWriteStream('test2.jpg', {encoding: 'binary'});

var convert = spawn('convert', ['test.jpg', '-']);
convert.stdout.on('data', function(data) {
 output_file.write(data);
});

convert.on('exit', function(code) {
 output_file.end();
});

我使用spawn获取可流式输出,然后使用一个可写流以二进制格式写入数据。只是测试了一下,成功打开了生成的test2.jpg图像。

编辑:是的,您可以使用此方法通过HTTP发送结果。以下是我使用convert缩小图像,然后将结果发布到glowfoto API的示例:

var fs = require('fs');
var http = require('http');
var util = require('util');
var spawn = require('child_process').spawn;
var url = require('url');

// Technically the only reason I'm using this
// is to get the XML parsed from the first call
// you probably don't need this, but just in case:
//
// npm install xml2js
var xml = require('xml2js');

var post_url;
var input_filename = 'giant_image.jpg';
var output_filename = 'giant_image2.jpg';

// The general format of a multipart/form-data part looks something like:
// --[boundary]\r\n
// Content-Disposition: form-data; name="fieldname"\r\n
// \r\n
// field value
function EncodeFieldPart(boundary,name,value) {
    var return_part = "--" + boundary + "\r\n";
    return_part += "Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n";
    return_part += value + "\r\n";
    return return_part;
}

// Same as EncodeFieldPart except that it adds a filename,
// as well as sets the content type (mime) for the part
function EncodeFilePart(boundary,type,name,filename) {
    var return_part = "--" + boundary + "\r\n";
    return_part += "Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + filename + "\"\r\n";
    return_part += "Content-Type: " + type + "\r\n\r\n";
    return return_part;
}

// We could use Transfer-Encoding: Chunked in the headers
// but not every server supports this. Instead we're going
// to build our post data, then create a buffer from it to
// pass to our MakePost() function. This means you'll have
// 2 copies of the post data sitting around
function PreparePost() {
  // Just a random string I copied from a packet sniff of a mozilla post
  // This can be anything you want really
  var boundary = "---------------------------168072824752491622650073";
  var post_data = '';

  post_data += EncodeFieldPart(boundary, 'type', 'file');
  post_data += EncodeFieldPart(boundary, 'thumbnail', '400');
  post_data += EncodeFilePart(boundary, 'image/jpeg', 'image', output_filename);

  fs.readFile(output_filename, 'binary', function(err,data){
    post_data += data;
    // This terminates our multi-part data
    post_data += "\r\n--" + boundary + "--";
    // We need to have our network transfer in binary
    // Buffer is a global object
    MakePost(new Buffer(post_data, 'binary'));
  });
}

function MakePost(post_data) {
  var parsed_url = url.parse(post_url);

  var post_options = {
    host: parsed_url.hostname,
    port: '80',
    path: parsed_url.pathname,
    method: 'POST',
    headers : {
        'Content-Type' : 'multipart/form-data; boundary=---------------------------168072824752491622650073',
        'Content-Length' : post_data.length
    }
  };

  var post_request = http.request(post_options, function(response){
    response.setEncoding('utf8'); 
    response.on('data', function(chunk){
      console.log(chunk);
    });
  });

  post_request.write(post_data);
  post_request.end();
}

// Glowfoto first makes you get the url of the server
// to upload
function GetServerURL() {
  var response = '';

  var post_options = {
      host: 'www.glowfoto.com',
      port: '80',
      path: '/getserverxml.php'
  };

  var post_req = http.request(post_options, function(res) {
      res.setEncoding('utf8');

      // Here we buildup the xml
      res.on('data', function (chunk) {
        response += chunk;
      });

      // When we're done, we parse the xml
      // Could probably just do string manipulation instead,
      // but just to be safe
      res.on('end', function(){
        var parser = new xml.Parser();
        parser.addListener('end', function(result){
      // Grab the uploadform element value and prepare our post
          post_url = result.uploadform;
          PreparePost();
        });

    // This parses an XML string into a JS object
        var xml_object = parser.parseString(response);
      });
  });
  post_req.end();

}

// We use spawn here to get a streaming stdout
// This will use imagemagick to downsize the full image to 30%
var convert = spawn('convert', ['-resize', '30%', input_filename, '-']);

// Create a binary write stream for the resulting file
var output_file = fs.createWriteStream(output_filename, {encoding: 'binary'});

// This just writes to the file and builds the data
convert.stdout.on('data', function(data){
  output_file.write(data);
});

// When the process is done, we close off the file stream
// Then trigger off our POST code
convert.on('exit', function(code){
  output_file.end();
  GetServerURL();
});

样例结果:

$ node test.js
<?xml version="1.0" encoding="utf-8"?>
<upload>
<thumburl>http://img4.glowfoto.com/images/2011/05/29-0939312591T.jpg</thumburl>
<imageurl>http://www.glowfoto.com/static_image/29-093931L/2591/jpg/05/2011/img4/glowfoto</imageurl>
<codes>http://www.glowfoto.com/getcode.php?srv=img4&amp;img=29-093931L&amp;t=jpg&amp;rand=2591&amp;m=05&amp;y=2011</codes>
</upload>

谢谢,我确实完全忽略了maxBuffer选项,但它似乎并不能解决损坏的问题。如果您使用我的示例扩大它,生成的文件不再太小,但仍然是损坏的。您的示例有效,但实际上我需要对数据进行更多处理,而不是直接将其传输到另一个文件中。更具体地说,我需要使用express框架将其写入HTTP响应中。 - Daniel Cremer
@Daniel 我刚试了一下,把 spawn 行改为:var convert = spawn('convert', ['test.jpg', '-resize', '50%', '-']); 就可以获得一个缩小了 50% 的工作中的 JPEG 文件。尝试使用你现在拥有的更新你的帖子。 - onteria_
你的示例确实可行。我的意思是,我的示例中的问题不仅仅是maxBuffer。当您增加它时,它并不能解决损坏的问题。不幸的是,我不能使用你的方法,因为我需要通过HTTP发送文件,所以我不能使用fs.createWriteStream。我正在更新示例以更正maxBuffer。 - Daniel Cremer
@Daniel 你可以的。我刚刚加了一个概念证明。 - onteria_
哇,谢谢你提供这么庞大的例子,但我想避免写入本地文件。否则,使用stdout并不是很有用,因为命令行可以直接写入文件,而不必通过node.js。结果问题是maxBuffer,正如你所建议的,并不总是以二进制编码。 - Daniel Cremer

4
您可以在Node.js中利用io管道。
var file = fs.createWritableStream("path-to-file", {encoding: 'binary'});
converter = spawn(cmd, ['parameters ommited']);
converter.stdout.pipe(file); //this will set out stdout.write cal to you file
converter.on('exit', function(){ file.end();});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接