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.
Print from a file path
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);
}
Print from a stream
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);
Print job options reference
| 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
HttpClientperforms 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 whenPreferPdfPassthroughistrueand the printer advertisesapplication/pdfsupport.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:
- Passthrough — the raw PDF bytes are sent directly to the printer. This is faster and preserves vector quality. Only attempted when
PreferPdfPassthrough = trueand the printer advertises PDF support. - Rasterized — each PDF page is rendered to a bitmap at
RasterDpiresolution 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
RasterDpiis 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 offerssgray_8is 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);
}
Related topics
- Exporting reports — How to export LLCP reports to PDF before printing
- Debugging and troubleshooting — General LLCP diagnostics and logging guidance
- Deployment — Running LLCP in containers and cloud environments