Printing solution based on C# to separate styles and data

As for my impressions of August, I found that most of them were reserved for business trips. When I came back from my business trip in early September, I immediately started working on the research and development of new projects. Therefore, whether it is the Mid-Autumn Festival or the National Day, this series of busy days is full of haste. Wang Beiluo said, “Isn’t life just a rush? There’s no way we can leave it to you and me“. Recently, I have been busy with printing, and I often suspect that in the matter of “Digital Transformation“, people’s slogans are greater than their substance. Otherwise, people would not be so keen on printing documents, although time Many years have passed, but some things seem to have never changed. Whether it is FastReport and FineReport in the past, or today’s PrintDocument and Web-based printing solutions, they are only changing in form, but the real essence has not changed, just like business It can be transferred from offline to online, but people’s willingness to try to control and aggregate the flow of information has never been reduced. Between change and immutability, we always emphasize “Adapt” and “Look forward“, but everyone is trying to sell it to others, consciously or unconsciously. A concept that has been immersed in the “Comfort Zone” for a long time. At this moment, I think it should be more varied. Therefore, I want to explore a “new” gameplay with the theme of “Print solution with separation of style and data”.

Start with PrintDocument

All stories have a starting point, and for C# or .NET, PrintDocument is always a point that cannot be bypassed when printing. Although, in the eyes of others, printing is nothing more than calling the system API to send instructions to the printer, but if you consider the various types of printers such as dot matrix, inkjet, laser, thermal… and so on, as well as printing of various sizes Paper, triple/quintuple slips, receipt paper, I think this issue is quite complicated. Considering the length, I am not going to explain how to use these APIs here. The following mind map shows the “Print” capabilities of PrintDocument. From this perspective, there are so many things to consider when printing. You even have to consider whether the printer is missing/jammed paper, whether the cutter printer is cutting the paper correctly… and so on. Previously, there was a joke circulating on the Internet, to the effect that someone asked how to solve the problem of blank pages when printing. At this time, seniors who have worked hard in the workplace for many years will tell you earnestly, just print it out and throw away the blank pages.

I believe everyone has seen documents or receipts similar to the following:

Normally, if you use PrintDocument in C# to implement printing, the basic idea is to construct a PrintDocument instance and register the PrintPage event. In this event, we can use Graphics to draw lines, text, pictures and other elements:

var printDocument = new PrintDocument();
printDocument.PrintController = new StandardPrintController();

//Set printer name
printDocument.DefaultPageSettings.PrinterSettings.PrinterName = "HP LaserJet Pro MFP M126nw";

//Set the paper size to A5
foreach (PaperSize paperSize in printDocument.DefaultPageSettings.PrinterSettings.PaperSizes)
{<!-- -->
    if (paperSize.PaperName == "A5")
    {<!-- -->
        printDocument.DefaultPageSettings.PaperSize = paperSize;
        break;
    }
}

//Register PrintPage event
printDocument.PrintPage + = async (s, e) =>
{<!-- -->
    // ...
    //Draw a QR code
    var qrCodeWidth = 100;
    var qrCodeName = Guid.NewGuid().ToString("N");
    var qrCodePath = PathSourcecs.CaptureFace + @"" + qrCodeName + ".png";
    QRCodeByZxingNet.NewQRCodeByZxingNet(qrCodePath, orderSaHwVo.saRecordId, qrCodeWidth, qrCodeWidth, ImageFormat.Png, BarcodeFormat.QR_CODE);
    var image = System.Drawing.Image.FromFile(qrCodePath);
    args.Graphics.DrawImage(image, marginLeft, totalHeight);
    // ...
};

Of course, you can also use the BeginPrint and EndPrint events to handle the logic of printing start and printing end. Here we press the button below. The following is the code implementation of printing and print preview. You can find that all this is supported by Microsoft API. very simple:

// print
printDocument.Print()

// Printing preview
var printPreviewDialog = new PrintPreviewDialog();
printPreviewDialog.Document = printDocument;
printPreviewDialog.TopLevel = true;
printPreviewDialog.ShowDialog();

As shown in the figure below, the following is the print preview effect achieved through the PrintPreviewDialog component:

If you look at PrintDocument from this perspective, it is undoubtedly a perfect solution!

An attempt to separate style and data

Historical experience tells us that nothing is absolute. The biggest problem with using this solution for printing is that style and data are not separated, or even severely coupled together. This results in that every time the printing format is changed, the entire code is basically rewritten. As time goes by, it is common for various versions of PrintPage code to exist in a project. As a programmer, I have always called on everyone to work hard to grasp those things that remain unchanged, but for life, the mentality of adapting to changes, embracing changes, and creating changes is obviously more universal. So, is there a “Staying unchanged in response to all changes” solution in this world to solve this problem? Therefore, let’s explore the separation of print styles and data.

As a pseudo-full-stack engineer who writes both front-end and back-end, I sometimes even feel that human beings may be recycling themselves over and over again. During the evolution from native to Web, what I see is that people are doing it over and over again. “Reinvent” the old business of the past with new technology. For example, the front end also needs to print documents, which can usually be achieved using vue-print-nb or vue3-print-nb. It is true that the front-end printing solution cannot escape the limitations of the browser’s own characteristics from beginning to end, but we can still find some commonality among them. As shown in the figure, in the previous front-end project, I used the EJS template engine to write and render HTML templates, and then printed them out through vue-print-nb. Therefore, I want to continue to use this solution here. The following is the overall implementation idea:

As shown in the figure, our idea is to use Liquid, which was introduced by previous bloggers, to render HTML templates. At this time, the printing style can be solved through the front-end three-piece set. We only need to complete the field binding in the template file, so that the separation of data and style can be achieved. Of course, all this is not enough to pass it to PrintDocument for use, so it needs to be further converted into an image or PDF file. In the timeline when the IE browser has not yet expired, you can use WebBrowser to convert images. But if we are still stubbornly holding on to WebBrowser in 2023 and are unwilling to let go, isn’t this an inexplicable obsession? The following uses an implementation of the new WebView2 solution:

// Make sure the WebView2 kernel is available
await webView.EnsureCoreWebView2Async();

//Load and render the template
var htmlContent = File.ReadAllText("HtmlTemplate.html");
var template = DotLiquid.Template.Parse(htmlContent);
htmlContent = template.Render(Hash.FromAnonymousObject(new { Remark = "This is the content rendered by printing the template" })

//Load the web page and take a screenshot
webView.Reload();
this.webView.NavigateToString(htmlContent);
using var fileStream = File.OpenWrite("snapshot.jpg")
await webView.CoreWebView2.CapturePreviewAsync(CoreWebView2CapturePreviewImageFormat.Jpeg, fileStream);

At this point, we only need to register the PrintPage event for PrintDocument:

printDocument.PrintPage + = async (s, e) =>
{
    // The following two lines of code can significantly improve the printing effect
    e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
    e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half;
    
    // Scale and draw pictures according to the actual printing area
    // Of course, this will involve issues such as pixels, millimeters, page margins, etc., and further research is needed. What I express here is a feasibility
    var image = System.Drawing.Image.FromFile("snapshot.jpg");
    var printableArea = printDocument.DefaultPageSettings.PrintableArea;
    var ratio = image.Width / image.Height;
    e.Graphics.DrawImage(image, new System.Drawing.RectangleF(printableArea.Left, printableArea.Top, printableArea.Width, printableArea.Width / ratio));
};

In fact, printing usually involves issues such as margins, paging, paper size, etc., and using a front-end three-piece set to render content naturally inevitably involves a bunch of issues such as pixels, millimeters, inches, DPI, etc. noun. I have to admit, it’s all very complicated, and even to the average user, printing seems like magic, just a click of the mouse. If you take into account the deformation problem caused by image scaling, in theory the aspect ratio of the HTML template should be the same as the aspect ratio of the actual printing paper, but the fact is that every time you deal with printing problems, you always need to spend time debugging. I personally feel that if you use PDF as the printing carrier, the effect should be slightly better than the picture.

There are two main reasons for considering PDF. One is that browsers based on the Webkit kernel are naturally friendly to the PDF format, and the other is that the browser’s own printing capabilities can be reused. As shown in the figure, we can use the new WebView2 component to call the browser’s own print dialog box. In a sense, this is no different from the vue-print-nb or vue3-print-nb plug-ins commonly used on the front end. All the thoughts in this article seem to flow to the same place at this moment. But the flaw of this solution is that it cannot skip the print dialog box that comes with the print browser. You can’t even tell whether the user clicked to print or cancel, let alone whether the printer has completed printing. In fact, even PrintDocument cannot obtain the printing progress “accurately“. Once people request silent printing, everything will eventually return to the PrintDocument solution.

In fact, Microsoft also provides an RDLC report solution, which is closer to traditional report business. It can complete report design through a series of processes such as defining entity classes, creating data sets, adding data sources, and designing templates. If you have used products such as FastReport, you will naturally feel that all this is familiar, and even low-level APIs such as DataSet and DataTable will feel more familiar. Microsoft’s RDLC and FastReport’s report template are essentially an XML file, and the bottom layer should use the PrintDocument API. If you see the relevant code snippets, you will understand one thing, that is: There is nothing new under the sun. It is nothing more than rendering each page into a picture and then drawing it through the DrawImage() method come out. When a person gets closer and closer to the essence, he will naturally get tired of external decorations or forms. Unfortunately, this kind of thing seems to be everywhere in life.

Summary of this article

After countless struggles, I finally finished writing this article with little technical content. First of all, the topic of printing is very fragmented, and these contents that are difficult to form a system cannot really reach the length of an article. Secondly, the value of printing in business is very low. Its presence or absence does not affect the main line process. In most cases, it is an embellishment that is better than nothing. From these two perspectives, my article is not even valuable, because in people’s minds, printing is a very simple thing, even if some people can mess up even loading paper. , but this will not affect its positioning in people’s minds at all. The only thing people can remember is to connect the power, press the switch, and click the mouse. If I can never change this, the only thing I can do is record these fragmentary thoughts. Whether it is a different path to the same destination or a unique path, what I care about is how I feel sitting in front of the computer at this moment, nothing more.