Вызовите jpegOptim с помощью RedirectStandardInput и RedirectStandardOutput

Я пытаюсь сделать что-то, что, кажется, должно быть относительно просто: вызов jpegoptim из C#.

Я могу заставить его записывать на диск нормально, но пока я не могу принять поток и выдать поток - я всегда получаю вывод с нулевой длиной или зловещее "Конец канала закончен".

Один подход, который я попробовал:

var processInfo = new ProcessInfo(
    jpegOptimPath,
    "-m" + quality + " -T1 -o -p --strip-all --all-normal"
);
processInfo.CreateNoWindow = true;
processInfo.WindowStyle = ProcessWindowStyle.Hidden;
processInfo.UseShellExecute = false;
processInfo.RedirectStandardInput = true;
processInfo.RedirectStandardOutput = true;
processInfo.RedirectStandardError = true;

using(var process = Process.Start(processInfo))
{
    await Task.WhenAll(
        inputStream.CopyToAsync(process.StandardInput.BaseStream),
        process.StandardOutput.BaseStream.CopyToAsync(outputStream)
    );

    while (!process.HasExited)
    {
        await Task.Delay(100);
    }

    // Do stuff with outputStream here - always length 0 or exception
}

Я также попробовал это решение:

http://alabaxblog.info/2013/06/redirectstandardoutput-beginoutputreadline-pattern-broken/

using (var process = new Process())
{
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.CreateNoWindow = true;
    process.StartInfo.RedirectStandardError = true;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.FileName = fileName;
    process.StartInfo.Arguments = arguments;

    process.Start();

    //Thread.Sleep(100);

    using (Task processWaiter = Task.Factory.StartNew(() => process.WaitForExit()))
    using (Task<string> outputReader = Task.Factory.StartNew(() => process.StandardOutput.ReadToEnd()))
    using (Task<string> errorReader = Task.Factory.StartNew(() => process.StandardError.ReadToEnd()))
    {
        Task.WaitAll(processWaiter, outputReader, errorReader);

        standardOutput = outputReader.Result;
        standardError = errorReader.Result;
    }
}

Та же проблема. Длина вывода 0. Если я позволю jpegoptim работать без перенаправления вывода, я получу то, что ожидаю - оптимизированный файл - но не тогда, когда я запускаю его таким образом.

Должен быть правильный способ сделать это?

Обновление: нашел подсказку - разве я не чувствую себя неловко - jpegoptim никогда не поддерживал конвейерную связь с stdin до экспериментальной сборки в 2014 году, исправленной в этом году. У меня есть сборка из более старой библиотеки, датированная 2013 годом. https://github.com/tjko/jpegoptim/issues/6

1 ответ

Частичное решение - см. Проблему тупика ниже. У меня было несколько проблем в моих первоначальных попытках:

  1. Вам нужна сборка jpegoptim, которая будет читать и записывать в каналы, а не только в файлы. Как уже упоминалось, сборка до середины 2014 года не может этого сделать. Github " релизы" jpegoptim - это бесполезные архивы исходного кода, а не сборочные выпуски, так что вам придется искать в других местах актуальные сборочные выпуски.

  2. Вы должны правильно назвать это, проходя --stdin а также --stdoutи в зависимости от того, как вы будете реагировать на него, избегайте параметров, которые могут заставить его ничего не писать, например -T1 (который, когда оптимизация будет составлять только 1% или менее, приведет к тому, что он ничего не будет выводить на стандартный вывод),

  3. Вам необходимо выполнить нетривиальную задачу: перенаправление ввода и вывода в класс Process

  4. и избежание переполнения буфера на входной стороне, которое снова выдаст вам 0 - очевидный поток. Копия ToAsync() переполняет очень ограниченный 4096-байтовый (4K) буфер процесса и ничего не получает.

Так много маршрутов ни к чему. Никто не сигнализирует почему

var processInfo = new ProcessInfo(
    jpegOptimPath,
    "-m" + quality + " --strip-all --all-normal --stdin --stdout",
);

processInfo.CreateNoWindow = true;
processInfo.WindowStyle = ProcessWindowStyle.Hidden;

processInfo.UseShellExecute = false;
processInfo.RedirectStandardInput = true;
processInfo.RedirectStandardOutput = true;
processInfo.RedirectStandardError = true;

using(var process = new Process())
{
    process.StartInfo = processInfo;

    process.Start();

    int chunkSize = 4096;   // Process has a limited 4096 byte buffer
    var buffer = new byte[chunkSize];
    int bufferLen = 0;
    var inputStream = process.StandardInput.BaseStream;
    var outputStream = process.StandardOutput.BaseStream;

    do
    {
        bufferLen = await input.ReadAsync(buffer, 0, chunkSize);
        await inputStream.WriteAsync(buffer, 0, bufferLen);
        inputStream.Flush();
    }
    while (bufferLen == chunkSize);

    do
    {
        bufferLen = await outputStream.ReadAsync(buffer, 0, chunkSize);
        if (bufferLen > 0)
            await output.WriteAsync(buffer, 0, bufferLen);
    }
    while (bufferLen > 0);

    while(!process.HasExited)
    {
        await Task.Delay(100);
    }

    output.Flush();

Здесь есть несколько областей для улучшения. Улучшения приветствуются.

  • Самая большая проблема: на некоторых изображениях это блокирует строку outputStream.ReadAsync.

  • Все это принадлежит отдельным методам, чтобы разбить его - я развернул кучу методов, чтобы этот пример был простым.

  • Есть куча приливов, которые могут не понадобиться.

  • Код здесь предназначен для обработки всего, что входит и выходит. 4096 - это жесткое ограничение, с которым будет иметь дело любой процесс, но предположение, что все входные данные поступают, а затем все выходные выходные данные, вероятно, являются плохими, и, основываясь на моих исследованиях, это может привести к тупику для других типов процессов. Похоже, что jpegoptim ведет себя таким образом (очень буферизированный, очень непохожий на...) при передаче --stdin --stdout, поэтому этот код хорошо справляется с этой конкретной задачей.

Другие вопросы по тегам