Hey Copilot: please print my label

No, Copilot has nothing to do with this post 😅 but it makes me smile how sometimes simple things becomes hard. Such printing (automatically) a label from a cloud ERP.

Business Central approach

Since 2020, Microsoft added “non-interactive printing in the cloud” allowing batches (job queues) to automatically print documents.

This is possibile in two main modes.

E-Mail Print

BC sends an e-mail to your printer and it processes the documents.

Universal Print

Microsoft Universal Print is an Azure services that allows to “register” and manage printers in the Cloud.

The printers must be “Universal Print Ready”. Here a list of compatibile printers:

https://learn.microsoft.com/en-us/universal-print/fundamentals/universal-print-printer-list

For non compatible printers, you can install (on premises) the “Universal Print Connector”. It is a simple service that talks from one side with the Cloud and from the other side with your printer.

Universal Print standalone license costs about $ 4 per user per month.

Easy but not very easy! 😅

Integration approach

In an existing environment (for example a manufacturing plant or a warehouse) it’s not so simple to say “hey, please change all your printers!” 😅 mostly if that devices are shared with other equipments or directly connected to machines.

Maybe you could evaluate to integrate “old” devices with “new” technology…

Step 1 – Render your report in PDF and encode it in Base 64

For example:

    procedure SaveAsB64PDF(ReportID: Integer; DocRef: RecordRef) Result : Text
    var
        TempBlob: Codeunit "Temp Blob";
        Base64Convert: Codeunit "Base64 Convert";
        OStream: OutStream;
        IStream: InStream;
        UnableToSaveErr: Label 'Unable to save report';
    begin
        TempBlob.CreateOutStream(OStream);
        if not Report.SaveAs(ReportID, '', ReportFormat::Pdf, OStream, DocRef) then
            Error(UnableToSaveErr);

        TempBlob.CreateInStream(IStream);
        Result := Base64Convert.ToBase64(IStream);
    end;

Step 2 – Create a Web endpoint to print PDF

A simple Windows service, listening to HTTP(S) port, can convert your “old” print server in a “new” cloud-ready spooler.

For example you can create an ASP.NET (*) page to process incoming Base 64 PDF and print it.

(*) Why not ASP.NET Core: because many imaging and printing libraries (included RDL ones) works only with .NET Framework 4.8

I use Ghostscript to convert PDF to PNG and next send the image to the printer. Creating the image with the right resolution allows to obtain good prints also with termal printers (avoiding use of the printer native language, such as ZPL).

For example:

private void PrintPDF(string printerName, string base64pdf)
{
    PrintDocument printDoc = new PrintDocument();
    printDoc.PrinterSettings.PrinterName = printerName;

    Ghostscript.NET.Processor.GhostscriptProcessor p = new Ghostscript.NET.Processor.GhostscriptProcessor();
    DirectoryInfo di = new DirectoryInfo(MYTEMPPATH);
    string pfix = Guid.NewGuid().ToString("n");

    byte[] content = Convert.FromBase64String(base64pdf);

    try
    {
        FileStream fs = new FileStream(di.FullName + "\\" + pfix + "_in.pdf", FileMode.CreateNew, FileAccess.Write);
        fs.Write(content, 0, content.Length);
        fs.Close();

        List<string> switches = new List<string>();
        switches.Add("-dBATCH");
        switches.Add("-dNOPAUSE");
        switches.Add("-dNOSAFER");
        switches.Add("-dNOPROMPT");
        switches.Add("-dQUIET");
        switches.Add("-sDEVICE=pnggray");

        // resolution
        switches.Add("-r" + printDoc.PrinterSettings.DefaultPageSettings.PrinterResolution.X.ToString() +
            "x" + printDoc.PrinterSettings.DefaultPageSettings.PrinterResolution.Y.ToString());  

        switches.Add("-sOutputFile=" + di.FullName + "\\" + pfix + "_%04d.png");
        switches.Add(di.FullName + "\\" + pfix + "_in.pdf");

        p.Process(switches.ToArray(), null);

        List<Image> pages = new List<Image>();
        foreach (FileInfo fi in di.GetFiles(pfix + "*.png"))
            pages.Add(Image.FromFile(fi.FullName));

        int n = 0;

        printDoc.PrintPage += (sender, e) =>
        {
            e.Graphics.TranslateTransform(-e.PageSettings.HardMarginX, -e.PageSettings.HardMarginY);
            e.Graphics.DrawImage(pages[n], 0, 0);

            n++;
            e.HasMorePages = (n < pages.Count);
        };

        printDoc.Print();
    }
    finally
    {
        try
        {
            foreach (FileInfo fi in di.GetFiles(pfix + "*"))
                fi.Delete();
        }
        catch
        {
        }
    }
}

Step 3 – Send Base 64 PDF to the Web spooler

For example:

    procedure RemotePrint(B64PDF: Text; PrinterName: Text)
    var
        Client: HttpClient;
        Content: HttpContent;
        Headers: HttpHeaders;
        RequestMessage: HttpRequestMessage;
        ResponseMessage: HttpResponseMessage;
        JRequest: JsonObject;
        JResponse: JsonObject;
        BodyTxt: Text;
    begin
        RequestMessage.SetRequestUri('MYREMOTEENDPOINT');
        RequestMessage.Method := 'POST';

        JRequest.Add('base64pdf', B64PDF);
        JRequest.Add('printerName', PrinterName);
        JRequest.WriteTo(BodyTxt);

        Content.WriteFrom(BodyTxt);
        Content.GetHeaders(Headers);
        Headers.Clear();
        Headers.Add('Content-Type', 'application/json');
        RequestMessage.Content := Content;

        Client.Send(RequestMessage, ResponseMessage);

        ResponseMessage.Content.ReadAs(BodyTxt);
        JResponse.ReadFrom(BodyTxt);
    end;

Step 4 – Enjoy 😅

Conclusion

Despite the AI 😅 in the real world we need paper and paper and labels and labels…

Also in “old” environments or with legacy software it’s possible to integrate the Cloud with no issues.

PS: if you want the full source code of my Web spooler, it’s available on GitHub 😎

https://github.com/brayns-it/shaper-print