Rust Gnuplot — сохранение PNG в памяти в виде байтов

#rust #gnuplot #actix-web

#Ржавчина #gnuplot #actix-web

Вопрос:

Я пытаюсь настроить простой actix-веб-сервер с одной конечной точкой под названием plot. По сути, он просто использует некоторые данные, отображает их с помощью gnuplot и возвращает байты результирующего PNG. проблема в том, что, как вы увидите в коде, я не нашел способа сделать все это в памяти, а это значит, что я должен сохранить файл на диске, снова открыть его в программе чтения, а затем отправить ответ обратно. В зависимости от уровня параллелизма я начну получать { code: 24, kind: Other, message: "Too many open files" } сообщения.

Есть ли у кого-нибудь какие-нибудь идеи, как я мог бы это сделать, чтобы весь процесс выполнялся в памяти? Я использую:

 actix-web = "3"
gnuplot = "0.0.37"
image = "0.23.12"
 

Любая помощь будет оценена, вот код:

 use actix_web::{post, web, App, HttpResponse, HttpServer, Responder};
use gnuplot::{AxesCommon, Color, Figure, LineWidth};
use image::io::Reader;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use std::any::type_name;
use std::collections::HashMap;
use std::fs;

#[post("/")]
async fn plot(req_body: web::Json<HashMap<String, Vec<f64>>>) -> impl Responder {
    let data = req_body.get("data").unwrap();
    let mut fg = Figure::new();
    let fid: String = thread_rng().sample_iter(amp;Alphanumeric).take(10).collect();
    let fname: String = format!("./{fid}.png", fid = fid);
    fg.set_terminal("pngcairo", amp;fname);
    let ax = fg.axes2d();
    ax.set_border(false, amp;[], amp;[]);
    ax.set_pos(0.0, 0.0);
    ax.set_x_ticks(None, amp;[], amp;[]);
    ax.set_y_ticks(None, amp;[], amp;[]);
    let x: Vec<usize> = (1..data.len()).collect();
    ax.lines(amp;x, data, amp;[LineWidth(4.0), Color("black")]);
    fg.set_post_commands("unset output").show();
    let image = Reader::open(amp;fname).unwrap().decode().unwrap();
    let mut bytes: Vec<u8> = Vec::new();
    image.write_to(amp;mut bytes, image::ImageOutputFormat::Png);
    fs::remove_file(fname);
    HttpResponse::Ok().body(bytes)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().service(plot))
        .bind("127.0.0.1:8080")?
        .run()
        .await
}
 

Ответ №1:

Чтобы избежать создания файла, вы можете сделать то, что описал Акихито КИРИСАКИ. Вы делаете это, вызывая set_terminal() , но вместо имени файла вы передаете пустую строку. Затем вы создаете Command и echo() в stdin .

 use std::process::{Command, Stdio};

#[post("/")]
async fn plot(req_body: web::Json<HashMap<String, Vec<f64>>>) -> impl Responder {
    ...

    fg.set_terminal("pngcairo", "");

    ...

    fg.set_post_commands("unset output");

    let mut child = Command::new("gnuplot")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .expect("expected gnuplot");
    let mut stdin = child.stdin.take().expect("expected stdin");
    fg.echo(amp;mut stdin);

    // Drop `stdin` such that it is flused and closed,
    // otherwise some programs might block until stdin
    // is closed.
    drop(stdin);

    let output = child.wait_with_output().unwrap();
    let png_image_data = output.stdout;

    HttpResponse::Ok().body(png_image_data)
}
 

Вам также необходимо удалить вызов show() .

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

1. Удивительно! Это именно то, чего я пытался достичь, я имел в виду, что мне придется подключаться к stdin / out через echo, но понятия не имел, как это сделать. Я пытаюсь воспроизвести ваше решение, но я не могу понять, откуда output оно исходит.

2. Забыл включить output , я обновил ответ 🙂

3. let output = child.wait_with_output(); попался! Большое вам спасибо 🙂

Ответ №2:

У gnuplot Crate может не быть такой функции. Но, к счастью, gnuplot может выводить байты изображения в стандартный вывод с помощью set term png и set output . Запустите gnuplot напрямую с помощью std::process::Command и может сохранять выходные данные в памяти.