Под катом хочу рассказать про вывод на печать PDF фалов под .NET. С уклоном на печать из разных лотков принтера. Это бывает полезно, когда нужно распечатать документы, состоящие из разных типов бумаги разложенных по лоткам принтера.

Среда .NET из коробки не представляет средств для работы с файлами PDF. Конечно есть платные библиотеки, но не всегда даже в них можно найти решение поставленных задач.

Чтобы распечатать документ “как есть”, нужно, используя «winspool.drv», отправить файл на принтер ввиде Raw Data. Об этом подробно написано на сайте поддержки. При таком подходе следует учитывать, что используемый принтер должен уметь обрабатывать PDF формат. Если такого принтера нет, нужно конвертировать PDF в PostScript и уже его отправлять на принтер качестве Raw Data. О том, как конвертировать PDF в PostScript всегда можно проконсультироваться у Google.

Если при печати нужно менять настройки принтера, придется искать другой способ. PostScript — аппаратно-независимый язык описания документов, который не предоставляет легальных средств для тонкой работы с железом, например, для выбора лотков.

Другой способ заключается в том, чтобы отрендерить PDF в растр и уже последний выводить на печать. Такой способ наиболее затратный с точки зрения ресурсов, но он просто реализуется и позволяет выводить на печать произвольные документы штатными средствами .Net. Именно такой подход мы и будем использовать.

Среда .NET предоставляет удобный класс PrintDocument для работы с принтерами, в котором холст принтера представлен объектом Graphics. На холст можно натянуть растровое изображение страницы PDF при помощи DrawImage. Для рендера PDF документов в растр существует замечательная бесплатная утилита GhostScript.


Утилита GhostScript доступна для бесплатного скачивания на официальном сайте GhostScript. Скачиваем и устанавливаем подходящую версию Download. Для удобной работы из среды .NET разработана обертка GhostScriptNet, которую также придется скачать. Архив распаковываем неподалеку от проекта. В архиве нас интересует сборка Ghostscript.NET.dll, которую незамедлительно подключаем к проекту разрабатываемого приложения, предполагается, что он уже создан ;)

Окружение настроено и готово к работе. Чтобы проверить, напишем совсем маленькое консольное приложение, конвертирующее страницы документа в *.jpg фалы:
using Ghostscript.NET;
using Ghostscript.NET.Rasterizer;

namespace GhostScript {
  class Program {
    private static GhostscriptVersionInfo _lastInstalledVersion = null;
    private const int DPI = 200;

    static void Main(string[] args) {
      const string outputPath = @"output\";

      if (!args.Any()) {
        Console.WriteLine("{0} [*.pdf]", Path.GetFileName(Environment.GetCommandLineArgs()[0]));
        return;
      }

      var inputPdfPath = args[0];
      
      _lastInstalledVersion = GhostscriptVersionInfo.GetLastInstalledVersion(
        GhostscriptLicense.GPL | GhostscriptLicense.AFPL
        , GhostscriptLicense.GPL
      );

      var rasterizer = new GhostscriptRasterizer();

      rasterizer.CustomSwitches.Add("-dNOINTERPOLATE");
      rasterizer.CustomSwitches.Add("-sPAPERSIZE=a4");

      rasterizer.TextAlphaBits = 4;
      rasterizer.GraphicsAlphaBits = 4;

      rasterizer.Open(inputPdfPath, _lastInstalledVersion, false);
            
      if (Directory.Exists(outputPath)) {
        Directory.Delete(outputPath, true);
      }

      Directory.CreateDirectory(outputPath);

      for (var pageNumber = 1; pageNumber <= rasterizer.PageCount; pageNumber++) {
        var outputFileName = string.Format("Page-{0:0000}.jpg", pageNumber);
        var outputFilePath = Path.Combine(outputPath, outputFileName);

        using (var img = rasterizer.GetPage(DPI, DPI, pageNumber)) {
          img.Save(pageFilePath, ImageFormat.Jpeg);
        }
      }
    }
  }
}

Работать с GhostScriptNet довольно просто, создаем объект класса GhostscriptRasterizer, который будет предоставлять функционал для преобразования страниц документа в объекты класса Image. Задаем параметры, которые я подбирал так, чтобы результат как можно больше соответствовал AcrobatReader. Открываем PDF файл, проходим циклом по страницам, получая объекты Image. Сохраняем в jpg файлы в заранее подготовленную директорию.


Теперь разберемся с печатью при помощи PrintDocument. Данный класс имеет простой и понятный интерфейс, при этом он содержит все необходимое для гибкой работы с принтером. Лучше один раз увидеть, чем сто раз услышать, поэтому переходим к коду.

Пример функции для печати изображения из файла:
static void Print(string file) {
  using (var pd = new System.Drawing.Printing.PrintDocument()) {
    pd.PrinterSettings.Duplex = Duplex.Simplex;
    
    pd.PrintPage += (o, e) => {
      var img = System.Drawing.Image.FromFile(file);

      e.Graphics.DrawImage(img, e.Graphics.VisibleClipBounds);
    };
    
    pd.Print();

  }
}



Первое, что может понадобиться при работе с PrintDocument — это указать принтер. Делается это через свойство PrinterName:

pd.PrinterSettings.PrinterName = "Имя принтера";


Если этого не сделать будет использован принтер, который установлен в системе по умолчанию.

Имя принтера можно получить из списка доступных принтеров:

var printrers = PrinterSettings.InstalledPrinters;
pd.PrinterSettings.PrinterName = printrers[1];


Чтобы отправить на печать несколько страниц, в обработчике страниц следует использовать флаг HasMorePages, принадлежащий объекту класса PrintPageEventArgs. Обработчик PrintPage будет вызываться до тех пор, пока HasMorePages == true.

Например HasMorePages можно использовать так:

e.HasMorePages = ++index < pages.Count;


Чтобы выводить на печать двусторонние документы, перед печатью нужно указать:

pd.PrinterSettings.Duplex = Duplex.Vertical;


В таком случае каждые 2 последовательные страницы будут интерпретированы как страницы одного листа.

Чтобы отключить окно прогресса печати, нужно указать стандартный контроллер печати:

pd.PrintController = new StandardPrintController();


Интересным с практичной точки зрения является возможность указывать источник бумаги (лоток). Причем менять лоток можно на лету, т.е. когда одна страница поступает из одного лотка, другая — из другого. Значит, можно распечатывать документы состоящие, например, из разных типов бумаги, которые разложены по лоткам.

Указать лоток для всей печати можно так:

pd.DefaultPageSettings.PaperSource = pd.PrinterSettings.PaperSources[SourceId];


А для страницы так:

e.PageSettings.PaperSource = pd.PrinterSettings.PaperSources[SourceId];


Во втором случае следует помнить, что e.PageSettings.PaperSource повлияет только на следующую страницу. Т.е. всегда имеем задержку на одну страницу: для первой страницы — pd.DefaultPageSettings.PaperSource, для всех последующих — e.PageSettings.PaperSource.


Теперь, скрестив генерацию изображений с печатью, можно написать незатейливую программу для вывода на принтер *.pdf файлов. Приводить код не буду, т.к. ничего нового в нем не будет. Кроме того у решения в лоб есть существенный недостаток — на рендер страниц тратится много машинных ресурсов, поэтому печать идет невменяемо долго. Например, печать крупного документа на 5000 страниц займет не меньше 30 минут, в то время как Acrobat Reader справился бы с задачей примерно за 10 – 15 минут. Так как основное время тратится на генерацию изображений, значит его и будем оптимизировать по времени. Этот процесс можно ускорить в десять раз и больше, в зависимости от железа. Для этого достаточно распараллелить генерацию изображений страниц. Фактически, такое ускорение будет всего лишь разменом временного ресурса на процессорный.

Для распараллеливания будем использовать пул потоков. Каждый поток будет обрабатывать свой фрагмент документа состоящий из N страниц. Результат будет складываться в словарь, где ключом будет номер текущей страницы, а значением — MemoryStream. «Почему Stream, а не, скажем, Bitmap?» — спросит любопытный читатель. Все просто. Дело в том, что в Stream мы будем хранить страницы сжатые в Jpeg формате, экономя таким образом память, ведь 5000 страниц это немало. Как только все страницы просчитаны, мы отправляем их на принтер. Узнать о том, что обработка закончилась очень просто: число страниц исходного документа должно совпасть с числом элементов словаря.

На C# описанное выше можно выразить следующим кодом
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing.Imaging;
using System.Drawing.Printing;
using System.IO;
using System.Threading;
using Ghostscript.NET;
using Ghostscript.NET.Rasterizer;

namespace GS_Parallel {
  class Program {
    public static Dictionary<int, MemoryStream> PageStore; //хранилище отрендеренных изображений
    private const int Dpi = 200;
    private const int Quants = 30;
    private const int MaxThreads = 10;

    static void Main(string[] args) {
      PageStore = new Dictionary<int, MemoryStream>();
            
      if (!args.Any()) {
        Console.WriteLine("{0} [*.pdf]", Path.GetFileName(Environment.GetCommandLineArgs()[0]));
        return;
      }

      var inputPdfPath = args[0];

      ThreadPool.SetMaxThreads(MaxThreads, MaxThreads);

      var mainRasterizer = CreateRasterizer(inputPdfPath); // нужен для посчета страниц

      var step = mainRasterizer.PageCount / Quants;
      var tail = mainRasterizer.PageCount % Quants;

      var shift = 0;
      for (var i = 0; i < Quants; i++) {
        var wi = new WorkInfo() {StartPage = shift + 1, EndPage = shift + step, SourcefilePath = inputPdfPath};
        ThreadPool.QueueUserWorkItem(PdfProcessing, wi);
        shift += step;
      }

      if (tail > 0) {
        var wi = new WorkInfo() { StartPage = shift + 1, EndPage = shift + tail, SourcefilePath = inputPdfPath };
        ThreadPool.QueueUserWorkItem(PdfProcessing, wi);
      }
            
      Console.WriteLine("Start preparation");
      
      while (PageStore.Count < mainRasterizer.PageCount) { // ждем завершения рендеринга
        Console.WriteLine("{0:000.0}%", ((double)PageStore.Count) / mainRasterizer.PageCount * 100);
        Thread.Sleep(100);
      }
      
      Console.WriteLine("Start printing");
      
      PrintPages(PageStore);

    }

    static GhostscriptVersionInfo _lastInstalledVersion = GhostscriptVersionInfo.GetLastInstalledVersion(GhostscriptLicense.GPL | GhostscriptLicense.AFPL, GhostscriptLicense.GPL);

    static GhostscriptRasterizer CreateRasterizer(string file) {
      var rasterizer = new GhostscriptRasterizer();
      rasterizer.CustomSwitches.Add("-dNOINTERPOLATE");
      rasterizer.CustomSwitches.Add("-dCOLORSCREEN=0");
      rasterizer.CustomSwitches.Add("-sPAPERSIZE=a4");
      rasterizer.TextAlphaBits = 4;
      rasterizer.GraphicsAlphaBits = 4;
      
      rasterizer.Open(file, _lastInstalledVersion, true);
      
      return _rasterizer;
    }

    static void PdfProcessing(object stateInfo) {
      var wi = (WorkInfo)stateInfo;
      var rasterizer = CreateRasterizer(wi.SourcefilePath);

      for (var pageNumber = wi.StartPage; pageNumber <= wi.EndPage; pageNumber++) {
        using (var img = rasterizer.GetPage(Dpi, Dpi, pageNumber)) {
          var mem = new MemoryStream();
          img.Save(mem, ImageFormat.Jpeg);
          
          lock (PageStore) {
            PageStore[pageNumber] = mem;
          }
        }
      }
    }

    static void PrintPages(IReadOnlyDictionary<int, MemoryStream> pageStore) {
      using (var pd = new PrintDocument()) {
        pd.PrinterSettings.Duplex = Duplex.Simplex;
        pd.PrintController = new StandardPrintController();

        var index = 0;
        pd.PrintPage += (o, e) => {
          var pageStream = pageStore[index + 1];
          var img = System.Drawing.Image.FromStream(pageStream);

          e.Graphics.DrawImage(img, e.Graphics.VisibleClipBounds);

          index++;
          e.HasMorePages = index < pageStore.Count;

          Console.WriteLine("Print {0} of {1}; complete {2:000.0}%", index, pageStore.Count, ((double)index) / pageStore.Count * 100);

        };
        pd.Print();
      }
    }
  }

  class WorkInfo {
    public int StartPage;
    public int EndPage;
    public string SourcefilePath;
  }
}



Для экономии памяти при сохранении Bitmap в MemoryStream можно увеличить коэффициент сжатия.

//Getting and configuration a jpeg encoder
const long quality = 35L;
var encoders = ImageCodecInfo.GetImageDecoders();
var jpgEncoder = encoders.FirstOrDefault(codec => codec.FormatID == ImageFormat.Jpeg.Guid);
var encoderParams = new EncoderParameters(1);
encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, quality);

//Save with the jpeg encoder
img.Save(mems, jpgEncoder, encoderParams);


Правда, увеличение коэффициента сжатия замедлит обработку и ухудшит качество печатаемых документов.


Если объем печатаемых данных очень большой (например, автоматизация мини типографии), можно поступить еще интереснее. Можно разделить задачу на два приложения: одно будет рендерить документы, другое — печатать. Причем, результат рендера PDF можно сохранять опять же в PDF. При печати подготовленных PDF подвергать их рендеру уже не нужно, достаточно извлечь сохраненные изображения, что делается достаточно быстро.

Комментарии (9)


  1. RouR
    16.03.2016 11:12
    +2

    Скачиваем и устанавливаем подходящую версию Download. Для удобной работы из среды .NET разработана обертка GhostScriptNet, которую также придется скачать. Архив распаковываем неподалеку от проекта. В архиве нас интересует сборка Ghostscript.NET.dll, которую незамедлительно подключаем к проекту

    Как много действий.

    Правильный путь — Install-Package iTextSharp


    1. petuhov_k
      16.03.2016 12:34
      +1

      Разве iTextSharp позволяет рендерить PDF в картинку? Впрочем GhostScript тоже есть на nuget.org


      1. RouR
        16.03.2016 12:50

        1. Beetle_ru
          16.03.2016 13:07
          +1

          Спасибо за наводку


        1. petuhov_k
          16.03.2016 14:42

          Next is to convert the PDF document generated by ItextSharp to an image with Spire.Pdf.

          Всё же "нет". Поскольку для рендеринга используется другая библиотека (Spire.Pdf). Но спасибо за ссылку.


  1. sndr
    16.03.2016 13:50
    +1

    Я не знаю, какую версию .NET вы используете и что подвигло вас использовать тот или иной подход к релазиации многопоточности в 2016 году но пару моментов бросились в гласа сразу:

    lock (PageStore) {
       PageStore[pageNumber] = mem;
    }

    Зачем использовать Dictionary<int, MemoryStream>, если у Вас используется доступ по индексу, когда есть MemoryStream[] и неблокирующий доступ по индексу, а количество можно определять использую атомарный инкремент?
    Ну и еще, т. к. процесс печати так же занимает определенное время стоит посмотреть в сторону блокирующих очередей (BlockingCollection в .NET 4+), Reactive Extensions и Dataflow.


    1. Beetle_ru
      16.03.2016 17:26

      Не понимаю связь между годом и многопоточностью.
      Согласен, конкретно в данном примере вполне можно использовать MemoryStream[].


      1. sndr
        16.03.2016 17:31

        Про год, это я к тому, что уже есть TPL, PLINQ, async/await, различные потокобезопасные структуры данных и т. д., поэтому код, который использует ThreadPool.QueueUserWorkItem напрямую вызывает некоторые вопросы сам собой, не говоря уже о конкретном способе его использования и блокировок.


        1. Beetle_ru
          16.03.2016 19:55
          -1

          Есть, и что теперь ThreadPool.QueueUserWorkItem забыть как тупиковую ветвь эволюции?)

          image