Table of Contents

Printing to a physical printer

The combit.ListLabel31.CrossPlatform.Printing.Pdf package extends List & Label Cross Platform with the ability to send PDF reports directly to a physical printer — on Windows, Linux, and macOS — without requiring a graphical desktop environment.

This is distinct from the export functionality built into LLCP. The printing assembly takes a PDF file (or stream) produced by LLCP and delivers it to a named printer using platform-appropriate mechanisms.


When to use this package

Use the printing assembly when your server-side application needs to:

  • Submit reports to a physical or network printer automatically
  • Integrate print dispatching into a background service or batch workflow
  • Print LLCP-generated PDF documents without a desktop UI
Note

This assembly handles delivery to a printer. Report generation (export to PDF) is handled separately using the core LLCP package. A typical workflow is: export to PDF with LLCP, then pass the result to PdfPrintService.

Note

This assembly does not provide interactive print dialogs or preview windows. It is designed for headless, automated printing in server-side environments.


Installation

Add the printing package to your project via NuGet:

dotnet add package combit.ListLabel31.CrossPlatform.Printing.Pdf

This package targets net8.0 and net10.0 and depends on PDFium for page rasterization.


Discovering available printers

Use PrinterDiscoveryService to enumerate printers and query their capabilities before submitting a print job.

List all printers

using combit.Reporting.Printing.Pdf;

var discovery = new PrinterDiscoveryService();
IReadOnlyList<PrinterInfo> printers = await discovery.GetPrintersAsync();

foreach (PrinterInfo printer in printers)
{
    Console.WriteLine($"{printer.Name} (default: {printer.IsDefault}, available: {printer.IsAvailable})");
}

Look up a specific printer

PrinterInfo? printer = await discovery.GetPrinterAsync("Office Printer");
if (printer is null)
{
    Console.Error.WriteLine("Printer not found.");
    return;
}

Query printer capabilities

PrinterCapabilities? caps = await discovery.GetCapabilitiesAsync("Office Printer");
if (caps is not null)
{
    Console.WriteLine($"Supports PDF passthrough: {caps.SupportsPdfPassthrough}");
    Console.WriteLine($"Supports duplex:          {caps.SupportsDuplex}");
    Console.WriteLine($"Supports color:           {caps.SupportsColor}");
    Console.WriteLine($"Paper sizes: {string.Join(", ", caps.SupportedPaperSizes)}");
}

PrinterCapabilities exposes the set of media types and paper sizes the printer accepts, along with flags for duplex support, color output, and PDF passthrough support.


Printing a PDF report

PdfPrintService accepts a PDF file path or stream and submits it to the specified printer.

using combit.Reporting.Printing.Pdf;

using var printService = new PdfPrintService();

PrintResult result = await printService.PrintAsync(
    pdfFilePath: "output/report.pdf",
    printerName: "Office Printer");

if (!result.Success)
{
    Console.Error.WriteLine($"Print failed: {result.ErrorMessage}");
    Console.Error.WriteLine(result.DiagnosticLog);
}

Use the stream overload to print directly from an in-memory or piped PDF without writing a temporary file to disk:

using combit.Reporting.Printing.Pdf;

await using Stream pdfStream = File.OpenRead("output/report.pdf");

using var printService = new PdfPrintService();

PrintResult result = await printService.PrintAsync(
    pdfStream: pdfStream,
    printerName: "Office Printer");

Combining export and print

A typical end-to-end workflow exports a report to PDF using LLCP, then prints the result:

using combit.Reporting;
using combit.Reporting.Printing.Pdf;

// Step 1: export the report to PDF
string pdfPath = Path.GetTempFileName() + ".pdf";

ListLabel listLabel = new ListLabel
{
    DataSource = GetDataSource(),
    AutoProjectFile = "reports/invoice.json",
    LicensingInfo = "..."
};

ExportConfiguration export = new ExportConfiguration(LlExportTarget.Pdf, pdfPath, "reports/invoice.json");
listLabel.Export(export);

// Step 2: print the exported PDF
using var printService = new PdfPrintService();

PrintResult result = await printService.PrintAsync(pdfPath, printerName: "Office Printer");

if (result.Success)
{
    Console.WriteLine($"Printed using {result.MethodUsed}.");
}
else
{
    Console.Error.WriteLine($"Print failed: {result.ErrorMessage}");
}

Configuring print job options

Pass a PrintJobOptions instance to control how the document is printed.

using combit.Reporting.Printing.Pdf;

var options = new PrintJobOptions
{
    Copies          = 2,
    Duplex          = DuplexMode.LongEdge,
    PaperSize       = "A4",
    ScalingMode     = PrintScalingMode.FitToPage,
    RasterDpi       = 300,
    JobName         = "Monthly Invoice",
    PreferPdfPassthrough = true,
    PreferGrayscale = false
};

using var printService = new PdfPrintService();
PrintResult result = await printService.PrintAsync("output/report.pdf", "Office Printer", options);
Property Default Description
Copies 1 Number of copies to print. Must be at least 1.
Duplex None Duplex mode: None, LongEdge (portrait binding), or ShortEdge (landscape binding).
PaperSize null Paper size name (for example, "A4" or "Letter"). When null, no paper size override is applied and the printer selects the closest supported paper size automatically.
ScalingMode FitToPage Whether to scale pages to fit the paper (FitToPage) or print at actual size (ActualSize).
RasterDpi 300 Resolution used when rasterizing PDF pages. Minimum value is 72. For the IPP backend this is a request: if the printer does not support the requested resolution, the closest supported one is used instead (see Rasterized output size on IPP printers).
JobName null Job name visible in the print spooler. Derived from the file name when null.
PreferPdfPassthrough true Whether to attempt sending raw PDF data directly (passthrough) before falling back to rasterization.
PreferGrayscale false IPP backend only. When true, rasterized pages are sent as 8-bit grayscale (sgray_8) instead of 24-bit color (srgb_8) whenever the printer supports it, cutting the raster data to roughly one third. Ignored on the GDI and lpr backends.

Selecting a print backend

Each platform has a default print backend: GDI (the Win32 print spooler) on Windows, and IPP on Linux. You can override it by passing a PdfPrintingBackend value to the PdfPrintService constructor.

using combit.Reporting.Printing.Pdf;

// Use the platform default (GDI on Windows).
using var gdiPrintService = new PdfPrintService();

// Select the IPP backend explicitly.
using var ippPrintService = new PdfPrintService(logger: null, backend: PdfPrintingBackend.Ipp);

PrintResult result = await ippPrintService.PrintAsync(
    pdfFilePath: "output/report.pdf",
    printerName: "ipp://printer.example.com/ipp/print");
PdfPrintingBackend Windows Linux macOS
Default GDI (Win32 spooler) IPP over HTTP lpr
Gdi GDI (Win32 spooler) unsupported (clear error) unsupported (clear error)
Ipp IPP over HTTP IPP over HTTP unsupported (clear error)

Requesting a backend that is not supported on the current platform throws PlatformNotSupportedException.

Configuring the Windows IPP printer URI

Unlike Linux (which resolves a local CUPS queue name to an IPP URI automatically), Windows has no local CUPS daemon. When the Windows IPP backend is selected, the printerName argument must be the printer's IPP endpoint URI. The following schemes are accepted:

Scheme Mapped to Default port
ipp://host/path http://host:631/path 631
ipps://host/path https://host:631/path 631
http://host:port/path used unchanged
https://host:port/path used unchanged

An explicit port in the URI (for example ipp://host:7000/ipp/print) is preserved.

Windows IPP limitations

  • A printer URI is required; Windows printer names registered in the spooler are not resolved to IPP endpoints. Use the GDI backend for spooler-managed printers.
  • Enumerating printers without a URI is not available, because Windows has no local CUPS daemon to query. Capabilities for a given IPP URI are still probed via IPP Get-Printer-Attributes, which is what drives the passthrough decision described below.
  • Authentication and client-certificate flows beyond what HttpClient performs by default are not configured automatically.

To query the capabilities of an IPP endpoint on Windows (for example, to check PDF passthrough support before printing), construct the discovery service with the IPP backend so it probes the URI directly instead of the local spooler:

using var discovery = new PrinterDiscoveryService(logger: null, backend: PdfPrintingBackend.Ipp);
PrinterCapabilities? caps = await discovery.GetCapabilitiesAsync("ipp://printer.example.com/ipp/print");

Supported document formats

The IPP backend submits one of two document formats, selected by the capability probe and the PreferPdfPassthrough option exactly as on Linux:

  • application/pdf — raw PDF passthrough, attempted first when PreferPdfPassthrough is true and the printer advertises application/pdf support.
  • image/pwg-raster — rasterized pages (PWG Raster), the universal fallback used when passthrough is unavailable or fails.

Troubleshooting Windows IPP

Symptom Likely cause and resolution
No IPP printer URI was provided The printerName was empty. Pass an ipp:///ipps:///http(s):// URI.
is not a valid absolute URI The printer name is not an absolute URI. Provide a fully-qualified IPP endpoint.
Unsupported IPP printer URI scheme The URI used a scheme other than ipp, ipps, http, or https.
Connection / TLS failures The endpoint is unreachable or its certificate is not trusted. Verify host, port, firewall, and the printer's TLS certificate. Inspect result.ErrorMessage and result.DiagnosticLog.
returned HTTP 401 / authentication required The printer requires authentication that is not configured.
returned error status 0x... The printer rejected the IPP job. The message names the IPP status (for example client-error-document-format-not-supported); check result.DiagnosticLog for details.

Understanding print results

PrintResult describes the outcome of a print operation.

Property Description
Success true when the document was delivered to the printer.
MethodUsed The PrintMethod used: Passthrough, Rasterized, or None.
ErrorMessage A human-readable error description when Success is false.
DiagnosticLog Step-by-step log of the print pipeline, useful for troubleshooting.

Passthrough vs. rasterized printing

PdfPrintService automatically selects the best delivery method:

  1. Passthrough — the raw PDF bytes are sent directly to the printer. This is faster and preserves vector quality. Only attempted when PreferPdfPassthrough = true and the printer advertises PDF support.
  2. Rasterized — each PDF page is rendered to a bitmap at RasterDpi resolution and then sent to the printer. This works universally but is slower and uses more memory for high DPI values.

When passthrough is attempted but fails, the service automatically retries using rasterization. In this case, result.Success is still true but result.MethodUsed is Rasterized and result.DiagnosticLog records the fallback path.

Tip

Check result.DiagnosticLog whenever MethodUsed is Rasterized but you expected passthrough. The log records the exact reason passthrough was skipped or failed.

Rasterized output size on IPP printers

When the IPP backend rasterizes a document, it negotiates the pixel format and resolution against the printer's advertised PWG Raster capabilities (pwg-raster-document-type-supported and pwg-raster-document-resolution-supported):

  • Resolution. The raster must use a resolution the printer accepts. If the requested RasterDpi is not supported, the closest supported value not exceeding it is used (or the smallest supported value when all are higher). Many office printers advertise only their native engine resolution — commonly 600 DPI — so a request for a lower DPI is rendered at 600. This is expected, not an error.
  • Color vs. grayscale. Color (srgb_8) is used by default; a monochrome printer that only offers sgray_8 is detected and gets grayscale automatically.

Because pixel count grows with the square of the resolution, 600 DPI color pages are large — a Letter page is roughly 100 MB uncompressed (before the backend's PWG run-length compression, which shrinks text and line-art pages dramatically). To reduce the data sent — for monochrome documents, or when a printer forces a high resolution — set PreferGrayscale = true to emit 8-bit grayscale instead of 24-bit color whenever the printer supports it, about one third of the data:

var options = new PrintJobOptions { PreferGrayscale = true };
Note

Pages are streamed to the printer one at a time, so high resolutions affect transfer time and printer-side processing rather than the host application's memory footprint.


Logging

Pass a logger to PdfPrintService or PrinterDiscoveryService to capture structured diagnostics:

using Microsoft.Extensions.Logging;
using combit.Reporting.Printing.Pdf;

using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
    builder.AddConsole().SetMinimumLevel(LogLevel.Debug));

ILogger<PdfPrintService> logger = loggerFactory.CreateLogger<PdfPrintService>();

using var printService = new PdfPrintService(logger);

When no logger is supplied, logging is disabled. For production scenarios, supplying a logger is recommended to capture print pipeline diagnostics for troubleshooting.


Platform support

The printing assembly supports Windows, Linux, and macOS. Platform-specific behavior:

Platform Passthrough mechanism Rasterized mechanism
Windows (default, GDI) Win32 Print Spooler (winspool.drv) GDI printer device context (StretchDIBits)
Windows (PdfPrintingBackend.Ipp) IPP over HTTP (application/pdf) IPP over HTTP (PWG Raster)
Linux IPP over HTTP (application/pdf) IPP over HTTP (PWG Raster)
macOS lpr command lpr with rasterized multi-page PDF
Tip

On Windows, the IPP backend is an alternative to the default GDI backend and requires the printer to be addressed by its IPP URI. See Selecting a print backend.

Tip

On Linux, printing uses the Internet Printing Protocol (IPP) directly over HTTP. The printer must be reachable via its IPP URI and must accept jobs sent over the network. Network-attached printers accessible via CUPS are the typical target.

Tip

On macOS, the lpr utility must be available on the system path. Printers must be configured in the macOS print system (CUPS) before they can be used.

Important

Printing across different operating systems, printer models, and driver implementations is inherently complex and may exhibit inconsistent behavior.

While the printing assembly aims to provide reliable cross-platform output, it ultimately depends on the capabilities and correctness of the underlying print subsystem, drivers, and device firmware. Variations in these components can lead to differences in rendering, layout, color reproduction, or job handling.

As a result, printing functionality is provided “as is” without guarantees that all documents will produce identical or expected results in every environment. Thorough testing with the target printers, drivers, and operating systems is strongly recommended, especially for production scenarios.


Complete example

The following example discovers the default printer, exports a report to PDF, and prints it with duplex enabled:

using combit.Reporting;
using combit.Reporting.Printing.Pdf;
using Microsoft.Extensions.Logging;

// Set up logging
using ILoggerFactory loggerFactory = LoggerFactory.Create(b =>
    b.AddConsole().SetMinimumLevel(LogLevel.Information));

// Discover printers and find the default
using var discovery = new PrinterDiscoveryService(loggerFactory.CreateLogger<PrinterDiscoveryService>());
IReadOnlyList<PrinterInfo> printers = await discovery.GetPrintersAsync();
PrinterInfo? defaultPrinter = printers.FirstOrDefault(p => p.IsDefault);

if (defaultPrinter is null)
{
    Console.Error.WriteLine("No default printer found.");
    return;
}

Console.WriteLine($"Printing to: {defaultPrinter.Name}");

// Export the report to PDF
string pdfPath = Path.Combine(Path.GetTempPath(), "report.pdf");

ListLabel listLabel = new ListLabel
{
    DataSource = GetDataSource(),
    AutoProjectFile = "reports/invoice.json",
    LicensingInfo = "..."
};

ExportConfiguration export = new ExportConfiguration(LlExportTarget.Pdf, pdfPath, "reports/invoice.json");
listLabel.Export(export);

// Print with duplex on long edge
var options = new PrintJobOptions
{
    Duplex      = DuplexMode.LongEdge,
    ScalingMode = PrintScalingMode.FitToPage,
    JobName     = "Invoice Report"
};

using var printService = new PdfPrintService(loggerFactory.CreateLogger<PdfPrintService>());
PrintResult result = await printService.PrintAsync(pdfPath, defaultPrinter.Name, options);

if (result.Success)
{
    Console.WriteLine($"Print job submitted. Method used: {result.MethodUsed}.");
}
else
{
    Console.Error.WriteLine($"Print failed: {result.ErrorMessage}");
    Console.Error.WriteLine(result.DiagnosticLog);
}