📜 ⬆️ ⬇️

[PF] Print PDF under .NET, vector approach, practice


As promised, I continue the theme ( one , two ) of managed PDF printing from under .NET in vector format. I talked about the theoretical aspects of working with PCL in a previous article , it’s time to parse the program for printing a PDF file in a vector to a printer. 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 the trays of the printer , we will save ourselves from manually laying pages;) The template will contain the number of the tray from which the printer will take the paper for the current page. Moreover, the template will be applied to the document cyclically: if there are 32 pages in the document and 4 in the template, then the template will repeat 8 times for the Simplex mode and 4 times for Duplex.

Let me remind you the procedure :

From the dependencies, the application will only have 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 a wrapper for .NET, which is called Ghostscript.NET. The .NET wrapper implements the universal GhostscriptProcessor class, which allows using Ghostscript with arbitrary settings. Create a Pdf2Pcl class with a single ConvertPcl2Pdf method, it accepts the path to the PDF file as 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<string>(); 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 solely for displaying messages from GhostScript to the console, it is not required. Instead of the GsIoHandler object, the second StartProcessing argument can be null.


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

To describe the template, we define 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 a pattern can take different values, to exclude them, use the SkipTheByte constant. 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 rather visual algorithm for searching PatternMatching looks like this:
 static int[] PatternMatching(byte[] data, byte[] pattern) { var pageShiftLst = new List<int>(); 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 page on 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 the page arguments, you will need offset bytes to be modified relative to the offset of the beginning of the page description. To do this, we set 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 to be changed relative to the page offset, 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 means 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 doesn’t work well for sending a PCL data stream. Let's finish the example so that we can send a byte array 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 class PrinterSettings.InstalledPrinters. The docName argument contains the name that will be displayed in the print queue.

We have sorted out the main points of the program for printing PDF documents in a pattern that will be 100% efficient on modern HP printers. For printers from other manufacturers it is better to look into the documentation for PCL support. But as many manufacturers now embed a PCL processor, problems are unlikely to arise. If PCL doesn’t work on typing, then for this case we have the approach described in the first article of the cycle.

Describing in detail the key points, while deliberately omitted some of the details, since they have no direct relationship to the topic. However, without them, the application does not take off, so I will give the full source 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<int>(); 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<byte>(); 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<int, int> GetBindings(string fileName) { if (!File.Exists(fileName)) { Console.WriteLine("    \"{0}\"", fileName); return new Dictionary<int, int>(); } var res = new Dictionary<int, int>(); 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, int>(); } int k = 0; int v = 0; if (!int.TryParse(kv[0], out k)) { Console.WriteLine("     : \"{0}\"", line); return new Dictionary<int, int>(); } if (!int.TryParse(kv[1], out v)) { Console.WriteLine("     : \"{0}\"", line); return new Dictionary<int, int>(); } res[k] = v; } return res; } private static Dictionary<string, int[]> GetPatterns(string fileName, Dictionary<int, int> bindings) { if (!File.Exists(fileName)) { Console.WriteLine("    \"{0}\"", fileName); return new Dictionary<string, int[]>(); } var lines = File.ReadAllLines(fileName, Encoding.Default); var res = new Dictionary<string, int[]>(); foreach (var line in lines) { var splt = line.Split(';'); if (!splt.Any()) { Console.WriteLine("  \"{0}\"", line); return new Dictionary<string, int[]>(); } var patternName = splt[0]; var patternBody = new List<int>(); 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<string>(); 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; } } } 



For work, you will need template files:

patterns.csv
;2;3;3;4;4
;2;3;4
;2;3;3;4
;2;4
;2


The first column is the name of the template, the rest are tray numbers.

To create templates, it is convenient to operate with the tray number, and not the source Id from the manual:


Create a binding file that contains the matching tray number and source id:

source-bindings.kv
1=3
2=4
3=5
4=7


I hope, in the article I managed to fill the informational gap, which concerns the managed printing, and the printing in general from under .Net.

One of the goals of the article is to draw the attention of developers, who in one way or another encounter tasks for printing documents, to the PCL language. Although PCL is not as readable and convenient as PostScript, it allows you to finely control the printer. And this is vital for some projects and not realizable in PostScript.

If you have any suggestions or you notice inaccuracies, please write in the comments.

Cycle of articles:
Raster approach
Vector approach theory
Vector approach practice

Source: https://habr.com/ru/post/302062/


All Articles