#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:
При первоначальном подходе было две проблемы.
-
maxBuffer должен быть достаточно высоким, чтобы обработать весь ответ от дочернего процесса.
-
Двоичная кодировка должна быть правильно установлена везде.
Полным рабочим примером может быть следующий:
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();});