Чтение двоичных данных из дочернего процесса в Node.js

#process #node.js #binary

#процесс #node.js #двоичный

Вопрос:

При попытке прочитать данные в Node.js из дочернего процесса ImageMagick он выходит поврежденным.

Простой тестовый пример был бы следующим:

 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, который был слишком маленьким и приводил к усеченному файлу. После увеличения этого файл теперь выглядит немного больше, чем ожидалось, и все еще поврежден. Для отправки по HTTP требуются данные из стандартного вывода.

Каков был бы правильный способ считывания этих данных из стандартного вывода ImageMagick?

Ответ №1:

При первоначальном подходе было две проблемы.

  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');
});
  

Другой пример, отправка данных в HTTP-ответе с использованием Express web framework, хотел бы этого:

 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'));
  });
});
  

Комментарии:

1. Вы должны добавить res.end() после res.send(…)

2. Я думал, что стандартный вывод уже является буфером… что ж, большое спасибо!

Ответ №2:

Ах, проблема в:

Если время ожидания больше 0, то дочерний процесс будет остановлен, если он выполняется дольше миллисекунд времени ожидания. Дочерний процесс уничтожается с помощью 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]rn
// Content-Disposition: form-data; name="fieldname"rn
// rn
// field value
function EncodeFieldPart(boundary,name,value) {
    var return_part = "--"   boundary   "rn";
    return_part  = "Content-Disposition: form-data; name=""   name   ""rnrn";
    return_part  = value   "rn";
    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   "rn";
    return_part  = "Content-Disposition: form-data; name=""   name   ""; filename=""   filename   ""rn";
    return_part  = "Content-Type: "   type   "rnrn";
    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  = "rn--"   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=img4amp;amp;img=29-093931Lamp;amp;t=jpgamp;amp;rand=2591amp;amp;m=05amp;amp;y=2011</codes>
</upload>
  

Комментарии:

1. Спасибо, это правда, что я полностью пропустил опцию maxBuffer, но, похоже, это не устраняет повреждение. Если вы увеличите это с помощью моего примера, результирующий файл больше не будет слишком маленьким, но все еще поврежден. Ваш пример работает, но на самом деле мне нужно сделать с данными больше, чем передать их прямо в другой файл. Более конкретно, мне нужно было бы записать это в HTTP-ответе, например, используя express framework.

2. @Daniel Я только что попробовал это, изменив начальную строку на : var convert = spawn('convert', ['test.jpg', '-resize', '50%', '-']); и получил рабочий файл JPEG, уменьшенный на 50%. Попробуйте обновить свой post тем, что у вас есть прямо сейчас.

3. Ваш пример действительно работает. Я имел в виду, что проблема в моем примере заключается не только в maxBuffer. Когда вы увеличиваете его, это не устраняет повреждение. К сожалению, я не могу использовать ваш подход, поскольку мне нужно отправить файл по HTTP, поэтому я не могу использовать fs.createWriteStream. Я обновляю пример, чтобы исправить maxBuffer.

4. @Daniel Ты можешь. Я только что добавил подтверждение концепции

5. Вау, спасибо, это чудовищный пример, но я пытался избежать необходимости записи в локальный файл. В противном случае использование стандартного вывода было бы не очень полезным, поскольку командная строка могла выполнять запись в файл без ненужного перехода через node.js . Оказывается, проблема заключалась в maxBuffer, как вы предложили, и не всегда кодировалась в двоичном формате.

Ответ №3:

Вы также можете воспользоваться каналами ввода-вывода в nodejs

 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();});