Back to Home

[PF] Printing PDF under .NET, vector approach, practice / Tinkoff.ru Blog

document printing · .net development · c # · pdf · pdf printer · pcl · vector magic · c # .net · ghostscript

[PF] Printing PDF for .NET, vector approach, practice

  • Tutorial

As promised, I continue the theme ( one , two ) of managed PDF printing from under .NET in a vector format. I talked about the theoretical aspects of working with PCL in a previous article , it is time to take apart a program for printing a PDF file to a printer in a vector. Our application will be useful, for example, when you need to print a pack of multipage forms or questionnaires on paper of different colors and different densities. If we learn to manage printer trays, save yourself from manually laying pages;) The template will indicate the tray number from which the printer will pick up paper for the current page. Moreover, the template will be applied to the document cyclically: if the document has 32 pages, and in the template 4, then the template will be repeated 8 times for Simplex mode and 4 times for Duplex.

Let me remind you the procedure :
  • convert PDF to PCL;
  • modify PCL by template;
  • send a stream of PCL bytes to the printer.

Of the dependencies, the application will have only Ghostscript.NET , with which we will convert PDF to PCL. What is Ghostscript.NET and how to use it can be found in the first article of the cycle.


To work with Ghostscript, we will use the wrapper for .NET, which is called Ghostscript.NET. The .NET wrapper implements the universal class GhostscriptProcessor, which allows you to use Ghostscript with arbitrary settings. Let's create the Pdf2Pcl class with the only ConvertPcl2Pdf method, it takes the path to the PDF file as an input and returns the PCL stream as an array of bytes.

public class Pdf2Pcl {
    public static byte[] ConvertPcl2Pdf(string pdfFileName) {
        byte[] rawDocumentData = null;
        var gsPipedOutput = new GhostscriptPipedOutput();
        var outputPipeHandle = "%handle%" + int.Parse(gsPipedOutput.ClientHandle).ToString("X2");
        using (var processor = new GhostscriptProcessor()) {
            var switches = new List();
            switches.Add("-dQUIET");
            switches.Add("-dSAFER");
            switches.Add("-dBATCH");
            switches.Add("-dNOPAUSE");
            switches.Add("-dNOPROMPT");
            switches.Add("-sDEVICE=pxlmono");
            switches.Add("-dNumRenderingThreads=20");
            switches.Add("-o" + outputPipeHandle);
            switches.Add("-f");
            switches.Add(pdfFileName);
            try {
                processor.StartProcessing(switches.ToArray(), new GsIoHandler());
                rawDocumentData = gsPipedOutput.Data;
            }
            catch (Exception ex) {
                Console.WriteLine(ex.Message);
            }
            finally {
                gsPipedOutput.Dispose();
                gsPipedOutput = null;
            }
        }
        return rawDocumentData;
    }
    public class GsIoHandler : GhostscriptStdIO {
        public GsIoHandler() : base(true, true, true) { }
        public override void StdIn(out string input, int count) {
            input = string.Empty;
        }
        public override void StdOut(string output) {
            if (string.IsNullOrWhiteSpace(output)) return;
            output = output.Trim();
            Console.WriteLine("GS: {0}",output);
        }
        public override void StdError(string error) {
            if (string.IsNullOrWhiteSpace(error)) return;
            error = error.Trim();
            Console.WriteLine("GS: {0}", error);
        }
    }
}


The GsIoHandler class is needed exclusively for outputting messages from GhostScript to the console; it is optional. Instead of the GsIoHandler object, the second argument to StartProcessing may be null.


In the PCl stream that the converter returned to us, we need to find the places where the pages of the document are declared. GhostScript generates such ads the same, so you can find the right places by a single template.

To describe the template, define the constants:
private const byte SkipTheByte = 0xff;
private const byte UByte = 0xc0;
private const byte AttrUByte = 0xf8;
private const byte Orientation = 0x28;
private const byte MediaSize = 0x25;
private const byte MediaSource = 0x26;
private const byte SimplexPageMode = 0x34;
private const byte DuplexPageMode = 0x35;
private const byte DuplexHorizontalBinding = 0x00;
private const byte SimplexFrontSide = 0x00;
private const byte DuplexVerticalBinding = 0x01;
private const byte BeginPage = 0x43;


Some bytes of the pattern can take different values; to eliminate them, the SkipTheByte constant is used. The page template will look like:
var pagePattern = new byte[] { UByte, SkipTheByte, AttrUByte, Orientation, UByte, SkipTheByte, AttrUByte, MediaSize, UByte, SkipTheByte, AttrUByte, MediaSource, UByte, SkipTheByte, AttrUByte, SimplexPageMode, BeginPage };


In the byte stream, this will correspond to the following fragment:






Not the most effective, but pretty visual search algorithm PatternMatching looks like this:
static int[] PatternMatching(byte[] data, byte[] pattern) {
    var pageShiftLst = new List();
    for (var i = 0; i < data.Count(); i++) {
        if (IsOnPattern(data, i, pattern)) {
            pageShiftLst.Add(i);
            Console.Write("{0:X8} | ", i);
            for (var j = 0; j < pattern.Count(); j++) {
                Console.Write("{0:X}  ", data[i + j]);
            }
            Console.WriteLine("");
            i += pattern.Count() - 1;
        }
    }
    return pageShiftLst.ToArray();
}
static bool IsOnPattern(byte[] data, int shift, byte[] pattern) {
    for (var i = 0; i < pattern.Count() ; i++) {
        if (!((shift + i) < data.Count())) return false;
        if (pattern[i] != SkipTheByte) {
            if (pattern[i] != data[shift + i]) {
                return false;
            }
        }
    }
    return true;
}


The PatternMatching function will return an array of offsets for the pages. Now you can modify the pages according to the template, specifying the print mode Duplex / Simplex, and the tray for the current page. These changes do not change the file size. We change the values ​​of bytes but not their number, so you can not be afraid that subsequent offsets will be irrelevant.

To modify page arguments, you need offsets of variable bytes relative to the offset of the beginning of the page description. To do this, we set up the corresponding constants:

private const int MediaSourceValueShift = 9;
private const int DuplexBindingShift = 13;
private const int PageModeShift = 15;


Having page offsets in the byte stream and offset bytes offset relative to page offsets, you can modify the PCL data stream, for example, by using the following function:

static byte[] ApplyPattern(byte[] data, int[] pageIndexes, byte[] extraPattern, bool isDuplex) {
    for (int i = 0; i < pageIndexes.Length; i++) {
        var pageIndex = pageIndexes[i];
        data[pageIndex + PageModeShift] = isDuplex ? DuplexPageMode : SimplexPageMode;
        data[pageIndex + DuplexBindingShift] = isDuplex ? DuplexVerticalBinding : SimplexFrontSide;
        data[pageIndex + MediaSourceValueShift] = extraPattern[i];
    }
    return data;
}



.Net has no built-in facilities for sending a byte array to the printer. But on support.microsoft.com there is an example of how to do this . To be more precise, it describes how to send a string or file as RawData. An example would suit us if we were working with PostScript, and it is of little use for sending a PCL data stream. We modify the example so that it is possible to send an array of bytes to the printer. For this, you need to add a method to the RawPrinterHelper class:

public static bool SendRawDataToPrinter(string szPrinterName, byte[] data, string docName) {
    bool bSuccess = false;
    IntPtr pUnmanagedBytes = new IntPtr(0);
    pUnmanagedBytes = Marshal.AllocCoTaskMem(data.Length);
    Marshal.Copy(data, 0, pUnmanagedBytes, data.Length);
    bSuccess = SendBytesToPrinter(szPrinterName, pUnmanagedBytes, data.Length, docName);
    Marshal.FreeCoTaskMem(pUnmanagedBytes);
    return bSuccess;
}


The printer name szPrinterName can be obtained by standard means through the PrinterSettings.InstalledPrinters class. The docName argument contains the name that will be displayed in the print queue.

We examined the main points of the program for printing PDF documents using a template that will be 100% workable on modern HP printers. For printers from other manufacturers, it is best to look at the documentation for PCL support. But since many manufacturers are now integrating a PCL processor, problems are unlikely to arise. If suddenly PCL doesn’t work, then for this case we have the approach described in the first article of the cycle.

Having described the key points in detail, they deliberately omitted some details, since they are not directly related to the topic. However, without them, the application will not take off, so I will give the full source code of the console program. It is far from perfect (although it copes with its task), since it was developed only to test the idea:

Program.cs
using System;
using System.Collections.Generic;
using System.Drawing.Printing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PCL_processing {
    class Program {
        private const byte SkipTheByte = 0xff; // 0xfc - 0xff Reserved for future use.
        private const byte UByte = 0xc0;
        private const byte AttrUByte = 0xf8;
        private const byte Orientation = 0x28;
        private const byte MediaSize = 0x25;
        private const byte MediaSource = 0x26;
        private const byte SimplexPageMode = 0x34;
        private const byte DuplexPageMode = 0x35;
        private const byte DuplexHorizontalBinding = 0x00;
        private const byte SimplexFrontSide = 0x00;
        private const byte DuplexVerticalBinding = 0x01;
        private const byte BeginPage = 0x43;
        private const int MediaSourceValueShift = 9;
        private const int DuplexBindingShift = 13;
        private const int PageModeShift = 15;
        static void Main(string[] args) {
            var pagePattern = new byte[] { UByte, SkipTheByte, AttrUByte, Orientation, UByte, SkipTheByte, AttrUByte, MediaSize, UByte, SkipTheByte, AttrUByte, MediaSource, UByte, SkipTheByte, AttrUByte, SimplexPageMode, BeginPage };
            var fileName = "";
            if (!args.Any()) {
                while (true) {
                    Console.WriteLine("Please write pdf file name:");
                    fileName = Console.ReadLine();
                    if (string.IsNullOrWhiteSpace(fileName)) {
                        Console.WriteLine("You have wrote empty string");
                        continue;
                    }
                    break;
                }
            }
            else {
                fileName = args[0];
            }
            if (!File.Exists(fileName)) {
                Console.WriteLine("File \"{0}\" not found", fileName);
                return;
            }
            var data = Pdf2Pcl.ConvertPcl2Pdf(fileName);
            var pageIndexes = PatternMatching(data, pagePattern);
            Console.WriteLine("Found {0} pages", pageIndexes.Length);
            var printPattern = GetSourecPattern();
            var isDuplex = Menu(new[] {"Simplex", "Duplex"}, "Selec mode:") > 0;
            var extraPattern = ExtractPattern(printPattern, pageIndexes.Length, isDuplex);
            data = ApplyPattern(data, pageIndexes, extraPattern, isDuplex);
            for (int i = 0; i < pageIndexes.Length; i++) {
                Console.Write("{0:X8} | ", pageIndexes[i]);
                for (var j = 0; j < pagePattern.Count(); j++) {
                    Console.Write("{0:X}  ", data[pageIndexes[i] + j]);
                }
                Console.WriteLine("");
            }
            var printer = GetPrinter();
            RawPrinter.SendRawDataToPrinter(printer, data, fileName);
            Console.WriteLine("*** DONE ***");
            Console.ReadLine();
        }
        static byte[] ApplyPattern(byte[] data, int[] pageIndexes, byte[] extraPattern, bool isDuplex) {
            for (int i = 0; i < pageIndexes.Length; i++) {
                var pageIndex = pageIndexes[i];
                data[pageIndex + PageModeShift] = isDuplex ? DuplexPageMode : SimplexPageMode;
                data[pageIndex + DuplexBindingShift] = isDuplex ? DuplexVerticalBinding : SimplexFrontSide;
                data[pageIndex + MediaSourceValueShift] = extraPattern[i];
            }
            return data;
        }
        static int[] PatternMatching(byte[] data, byte[] pattern) {
            var pageShiftLst = new List();
            for (var i = 0; i < data.Count(); i++) {
                if (IsOnPattern(data, i, pattern)) {
                    pageShiftLst.Add(i);
                    Console.Write("{0:X8} | ", i);
                    for (var j = 0; j < pattern.Count(); j++) {
                        Console.Write("{0:X}  ", data[i + j]);
                    }
                    Console.WriteLine("");
                    i += pattern.Count() - 1;
                }
            }
            return pageShiftLst.ToArray();
        }
        static bool IsOnPattern(byte[] data, int shift, byte[] pattern) {
            for (var i = 0; i < pattern.Count() ; i++) {
                if (!((shift + i) < data.Count())) return false;
                if (pattern[i] != SkipTheByte) {
                    if (pattern[i] != data[shift + i]) {
                        return false;
                    }
                }
            }
            return true;
        }
        private static byte[] ExtractPattern(int[] pattern, int pageCount, bool isDublex) {
            var srcPoint = 0;
            var expandedPattern = new List();
            for (var pageNumber = 0; pageNumber < pageCount; pageNumber++) { // expand-pattern
                expandedPattern.Add((byte)pattern[srcPoint]);
                if (isDublex) {
                    if (pageNumber % 2 != 0) {
                        srcPoint++;
                    }
                } else {
                    srcPoint++;
                }
                srcPoint = srcPoint < pattern.Count() ? srcPoint : 0;
            }
            return expandedPattern.ToArray();
        }
        private static int[] GetSourecPattern() {
            var bindingsFile = "source-bindings.kv";
            var patternsFile = "patterns.csv";
            var Bindings = GetBindings(bindingsFile);
            var Patterns = GetPatterns(patternsFile, Bindings);
            var patternindex = Menu(Patterns.Keys.ToArray(), "Please select pattern:");
            var pattern = Patterns.ElementAt(patternindex).Value;
            var srcPattern = pattern.Select(i => Bindings[i]).ToList();
            return srcPattern.ToArray();
        }
        private static int Menu(string[] items, string message) {
            var selectedIndex = -1;
            while (true) {
                Console.WriteLine(message);
                for (int i = 0; i < items.Length; i++) {
                    Console.WriteLine("[{0}] -- \"{1}\"", i, items[i]);
                }
                var str = Console.ReadLine();
                if (Int32.TryParse(str, out selectedIndex)) {
                    if (selectedIndex >= 0 && selectedIndex < items.Length) {
                        break;
                    }
                }
            }
            return selectedIndex;
        }
        private static Dictionary GetBindings(string fileName) {
            if (!File.Exists(fileName)) {
                Console.WriteLine("Файл привязок не найден \"{0}\"", fileName);
                return new Dictionary();
            }
            var res = new Dictionary();
            var lines = File.ReadAllLines(fileName, Encoding.Default);
            foreach (var line in lines) {
                var kv = line.Split('=');
                if (kv.Count() != 2) {
                    Console.WriteLine("Ошибка разбора файла привязок: \"{0}\"", line);
                    return new Dictionary();
                }
                int k = 0;
                int v = 0;
                if (!int.TryParse(kv[0], out k)) {
                    Console.WriteLine("Ошибка ключа при разборе файла привязок: \"{0}\"", line);
                    return new Dictionary();
                }
                if (!int.TryParse(kv[1], out v)) {
                    Console.WriteLine("Ошибка значения при разборе файла привязок: \"{0}\"", line);
                    return new Dictionary();
                }
                res[k] = v;
            }
            return res;
        }
        private static Dictionary GetPatterns(string fileName, Dictionary bindings) {
            if (!File.Exists(fileName)) {
                Console.WriteLine("Файл шаблонов не найден \"{0}\"", fileName);
                return new Dictionary();
            }
            var lines = File.ReadAllLines(fileName, Encoding.Default);
            var res = new Dictionary();
            foreach (var line in lines) {
                var splt = line.Split(';');
                if (!splt.Any()) {
                    Console.WriteLine("Некорректный шаблон \"{0}\"", line);
                    return new Dictionary();
                }
                var patternName = splt[0];
                var patternBody = new List();
                for (var i = 1; i < splt.Count(); i++) {
                    int item = 0;
                    if (!int.TryParse(splt[i], out item)) {
                        Console.WriteLine("Некорректный номер лотка в шаблоне \"{0}\"", line);
                        break;
                    }
                    if (!bindings.ContainsKey(item)) {
                        Console.WriteLine("Нет привязки номера лотка в шаблоне \"{0}\"", line);
                        break;
                    }
                    patternBody.Add(item);
                }
                res[patternName] = patternBody.ToArray();
            }
            return res;
        }
        static string GetPrinter() {
            var printers = new string[PrinterSettings.InstalledPrinters.Count];
            PrinterSettings.InstalledPrinters.CopyTo(printers, 0);
            var printerIndex = Menu(printers, "Please select printer:");
            return printers[printerIndex];
        }
    }
}


Pdf2pcl.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Ghostscript.NET;
using Ghostscript.NET.Processor;
namespace PCL_processing {
    public class Pdf2Pcl {
        public static byte[] ConvertPcl2Pdf(string pdfFileName) {
            byte[] rawDocumentData = null;
            var gsPipedOutput = new GhostscriptPipedOutput();
            var outputPipeHandle = "%handle%" + int.Parse(gsPipedOutput.ClientHandle).ToString("X2");
            using (var processor = new GhostscriptProcessor()) {
                var switches = new List();
                switches.Add("-dQUIET");
                switches.Add("-dSAFER");
                switches.Add("-dBATCH");
                switches.Add("-dNOPAUSE");
                switches.Add("-dNOPROMPT");
                switches.Add("-sDEVICE=pxlmono");
                switches.Add("-o" + outputPipeHandle);
                switches.Add("-f");
                switches.Add(pdfFileName);
                try {
                    processor.StartProcessing(switches.ToArray(), new GsIoHandler());
                    rawDocumentData = gsPipedOutput.Data;
                }
                catch (Exception ex) {
                    Console.WriteLine(ex.Message);
                }
                finally {
                    gsPipedOutput.Dispose();
                    gsPipedOutput = null;
                }
            }
            return rawDocumentData;
        }
        public class GsIoHandler : GhostscriptStdIO {
            public GsIoHandler() : base(true, true, true) { }
            public override void StdIn(out string input, int count) {
                input = string.Empty;
            }
            public override void StdOut(string output) {
                if (string.IsNullOrWhiteSpace(output)) return;
                output = output.Trim();
                Console.WriteLine("GS: {0}",output);
            }
            public override void StdError(string error) {
                if (string.IsNullOrWhiteSpace(error)) return;
                error = error.Trim();
                Console.WriteLine("GS: {0}", error);
            }
        }
    }
}


RawPrinter.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace PCL_processing {
    class RawPrinter {
        // Structure and API declarions:
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
        private class DOCINFOA {
            [MarshalAs(UnmanagedType.LPStr)]
            public string pDocName;
            [MarshalAs(UnmanagedType.LPStr)]
            public string pOutputFile;
            [MarshalAs(UnmanagedType.LPStr)]
            public string pDataType;
        }
        [DllImport("winspool.Drv", EntryPoint = "OpenPrinterA", SetLastError = true, CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern bool OpenPrinter([MarshalAs(UnmanagedType.LPStr)] string szPrinter, out IntPtr hPrinter, IntPtr pd);
        [DllImport("winspool.Drv", EntryPoint = "ClosePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern bool ClosePrinter(IntPtr hPrinter);
        [DllImport("winspool.Drv", EntryPoint = "StartDocPrinterA", SetLastError = true, CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern bool StartDocPrinter(IntPtr hPrinter, Int32 level, [In, MarshalAs(UnmanagedType.LPStruct)] DOCINFOA di);
        [DllImport("winspool.Drv", EntryPoint = "EndDocPrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern bool EndDocPrinter(IntPtr hPrinter);
        [DllImport("winspool.Drv", EntryPoint = "StartPagePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern bool StartPagePrinter(IntPtr hPrinter);
        [DllImport("winspool.Drv", EntryPoint = "EndPagePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern bool EndPagePrinter(IntPtr hPrinter);
        [DllImport("winspool.Drv", EntryPoint = "WritePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        private static extern bool WritePrinter(IntPtr hPrinter, IntPtr pBytes, Int32 dwCount, out Int32 dwWritten);
        private static bool SendBytesToPrinter(string szPrinterName, IntPtr pBytes, Int32 dwCount, string docName) {
            Int32 dwError = 0, dwWritten = 0;
            IntPtr hPrinter = new IntPtr(0);
            DOCINFOA di = new DOCINFOA();
            bool bSuccess = false; // Assume failure unless you specifically succeed.
            di.pDocName = docName;
            di.pDataType = "RAW";
            // Open the printer.
            if (OpenPrinter(szPrinterName.Normalize(), out hPrinter, IntPtr.Zero)) {
                // Start a document.
                if (StartDocPrinter(hPrinter, 1, di)) {
                    // Start a page.
                    if (StartPagePrinter(hPrinter)) {
                        // Write your bytes.
                        bSuccess = WritePrinter(hPrinter, pBytes, dwCount, out dwWritten);
                        EndPagePrinter(hPrinter);
                    }
                    EndDocPrinter(hPrinter);
                }
                ClosePrinter(hPrinter);
            }
            // If you did not succeed, GetLastError may give more information
            // about why not.
            if (bSuccess == false) {
                dwError = Marshal.GetLastWin32Error();
            }
            return bSuccess;
        }
        public static bool SendRawDataToPrinter(string szPrinterName, byte[] data, string docName) {
            bool bSuccess = false;
            // Your unmanaged pointer.
            IntPtr pUnmanagedBytes = new IntPtr(0);
            // Allocate some unmanaged memory for those bytes.
            pUnmanagedBytes = Marshal.AllocCoTaskMem(data.Length);
            // Copy the managed byte array into the unmanaged array.
            Marshal.Copy(data, 0, pUnmanagedBytes, data.Length);
            // Send the unmanaged bytes to the printer.
            bSuccess = SendBytesToPrinter(szPrinterName, pUnmanagedBytes, data.Length, docName);
            // Free the unmanaged memory that you allocated earlier.
            Marshal.FreeCoTaskMem(pUnmanagedBytes);
            return bSuccess;
        }
    }
}



To work, you will need template files:

patterns.csv The first column is the name of the template, the rest is the number of trays. To compile the templates, it is convenient to operate with the tray number, and not the source Id from the manual:
Первый шаблон;2;3;3;4;4
Второй шаблон;2;3;4
Третий шаблон;2;3;3;4
Четвертый шаблон;2;4
Пятый шаблон;2







Let's create a binding file that contains the correspondence between the tray number and the source id:

source-bindings.kv I hope that in the article I managed to fill in an information gap regarding managed printing, and printing from under .Net in general. One of the goals of the article is to draw the attention of developers who somehow encounter tasks for printing documents to PCL. Although PCL is not as readable and convenient as PostScript, it allows you to fine-tune the printer. And this is vital for some projects and not implemented on PostScript. If you have any suggestions or you notice inaccuracies, please write in the comments. Series of articles: Raster approach Vector approach theory Vector approach practice
1=3
2=4
3=5
4=7











Read Next