Cracking the Code: Print-Optimized PDFs
Printing a PDF from an application is more involved than passing a filename to the operating system. The PDF specification supports a wide range of content types, transparency modes, color spaces, and embedded resources, and not all of them translate correctly to every printer driver without a properly configured print pipeline. The Adobe PDF Library C++ SDK provides a programmatic printing API that handles the translation between PDF content and printer output correctly.
This tutorial walks through two samples: PDFPrintDefault, which sends a document to the default printer silently without user interaction, and PDFPrintGUI, which opens the OS print dialog and lets the user configure settings before printing. Both samples share the same four-step structure and run on Windows, macOS, and Linux.
Who Is This For?
This tutorial is for C++ developers building applications that need to trigger PDF printing programmatically. Common use cases include print queue management systems, document automation platforms that print as a final processing step, point-of-sale and receipt printing workflows, content management systems with print-on-demand functionality, and desktop applications that need to embed a fully controlled print experience without invoking a generic OS dialog.
Required Headers
Both samples use the same four headers. PDFLPrint.h contains the PDFLPrintDoc function and both parameter structures. SetupPrintParams.h provides the helper functions that initialize those structures with safe defaults:
#include "InitializeLibrary.h"
#include "APDFLDoc.h"
#include "PDFLPrint.h"
#include "SetupPrintParams.h"
On macOS, an additional Cocoa header is required for the NSPrintInfo and NSPrintPanel classes used in the platform-specific printer setup code:
#ifdef MAC_PLATFORM
#include "Cocoa/Cocoa.h"
#endif
The Shared Four-Step Structure
Both PDFPrintDefault and PDFPrintGUI follow exactly the same four steps. The only meaningful difference between them is in Step 3, where one retrieves the default printer silently and the other opens a dialog. Steps 1, 2, and 4 are identical.
Sample 1: PDFPrintDefault
When to Use This
Use PDFPrintDefault for automated, server-side, or batch printing workflows where no user interaction is needed or possible. The application selects the default printer, applies a fixed configuration, and sends the job without displaying any UI. This is the right choice for print queue processors, automated report delivery systems, and any headless server environment.
Step 1: Initialize the Library and Open the Document
Library initialization follows the standard APDFL pattern. The isValid() check is required before any further operations. The document is opened with repair enabled (the second argument to APDFLDoc), which is recommended when processing documents from external sources:
APDFLib lib;
if (lib.isValid() == false)
return lib.getInitError();
APDFLDoc inAPDoc(inPath, true);
PDDoc inDoc = inAPDoc.getPDDoc();
Step 2: Initialize Print Parameters
Two structures control the print job. PDPrintParamsRec holds PostScript-level rendering settings. PDFLPrintUserParamsRec holds the overall job configuration including printer target, page range, paper size, and platform-specific options. SetupPDPrintParams and SetupPDFLPrintUserParams populate both with safe defaults, and then the two structures are linked together:
PDPrintParamsRec psParams;
SetupPDPrintParams(&psParams);
PDFLPrintUserParamsRec userParams;
SetupPDFLPrintUserParams(&userParams);
userParams.printParams = &psParams;
After linking, override the specific fields needed for this job. These four are set in both samples:
userParams.emitToFile = false;
userParams.emitToPrinter = true;
userParams.startPage = 0;
userParams.endPage = PDDocGetNumPages(inDoc) - 1;
userParams.nCopies = 1;
userParams.paperWidth = 0; // 0 defaults to 8.5 inches
userParams.paperHeight = 0; // 0 defaults to 11 inches
Page numbers are zero-indexed. startPage = 0 is the first page of the document. paperWidth and paperHeight of 0 tell the library to use letter size by default. To specify a different paper size, set these values in points (72 points per inch).
Step 3: Configure the Printer Destination (Windows)
On Windows, PrintDlgW with the PD_RETURNDEFAULT flag retrieves the default printer information without displaying any dialog. The DEVMODE structure it returns contains printer driver settings including copies and collation. APDFL needs a private copy of this structure, so it is duplicated with malloc and memcpy:
PRINTDLGW printDialog;
memset(&printDialog, 0, sizeof(printDialog));
printDialog.lStructSize = sizeof(printDialog);
printDialog.Flags = PD_RETURNDEFAULT;
if (PrintDlgW(&printDialog)) {
LPDEVMODEW devMode = (LPDEVMODEW)(GlobalLock(printDialog.hDevMode));
userParams.pDevModeW = (DEVMODEW*)malloc(devMode->dmSize + devMode->dmDriverExtra);
memcpy(userParams.pDevModeW, devMode, devMode->dmSize + devMode->dmDriverExtra);
GlobalUnlock(printDialog.hDevMode);
// Retrieve the full printer name from hDevNames
// (hDevMode truncates at 32 chars via dmDeviceName)
LPDEVNAMES devNames = (LPDEVNAMES)(GlobalLock(printDialog.hDevNames));
std::wstring dmDeviceName((WCHAR*)(devNames) + devNames->wDeviceOffset);
GlobalUnlock(printDialog.hDevNames);
size_t lenDeviceName = dmDeviceName.length();
userParams.deviceNameW = new ASUns16[lenDeviceName + 1];
memcpy(userParams.deviceNameW, dmDeviceName.data(), lenDeviceName * sizeof(ASUns16));
userParams.deviceNameW[lenDeviceName] = 0;
}
Note that the printer name is read from hDevNames rather than hDevMode. The dmDeviceName field in DEVMODE is limited to 32 characters (CCHDEVICENAME) and will silently truncate longer printer names. hDevNames holds the full name and should always be used as the source for deviceNameW.
After the printer name is set, several additional print quality flags are applied:
userParams.shrinkToFit = true;
userParams.printAnnots = false;
userParams.psLevel = 3;
userParams.binaryOK = true;
userParams.emitHalftones = false;
userParams.reverse = false;
userParams.forceGDIPrint = false;
shrinkToFit scales PDF pages down to fit within the paper boundaries if they are larger than the selected paper size. psLevel = 3 targets PostScript Level 3, which is supported by virtually all modern laser printers and provides the best rendering quality for complex PDFs with transparency and gradients. Setting forceGDIPrint = false allows APDFL to use PostScript when the printer supports it, falling back to GDI only when necessary.
Step 3: Configure the Printer Destination (macOS)
On macOS, the system's shared NSPrintInfo object provides the print session, settings, and page format without requiring any dialog. The three PM structs are cast directly from the NSPrintInfo properties:
NSPrintInfo *thePrintInfo = [NSPrintInfo sharedPrintInfo];
userParams.printSession = (PMPrintSession)[thePrintInfo PMPrintSession];
userParams.printSettings = (PMPrintSettings)[thePrintInfo PMPrintSettings];
userParams.pageFormat = (PMPageFormat)[thePrintInfo PMPageFormat];
The same print quality flags (shrinkToFit, psLevel, binaryOK, etc.) are set on macOS as well, applied to the userParams struct directly before the PDFLPrintDoc call.
Step 3: Configure the Printer Destination (Linux)
On Linux, printing is configured via the command field, which accepts any lp-compatible command string. The sample uses lp -s, which suppresses the job number message from stdout:
#ifdef UNIX_ENV
userParams.command = "lp -s";
#endif
Step 4: Send the Job and Clean Up
PDFLPrintDoc sends the document to the configured printer. The call is wrapped in its own DURING/HANDLER block so that a print error does not skip the cleanup code that follows. After the job is sent, both parameter structures must be explicitly disposed and the document closed:
DURING
PDFLPrintDoc(inDoc, &userParams);
HANDLER
errCode = ERRORCODE;
lib.displayError(errCode);
END_HANDLER
DisposePDPrintParams(&psParams);
DisposePDFLPrintUserParams(&userParams);
PDDocClose(inDoc);
On Windows, if userParams.inFileName was set to point at the inPath char array, it must be set back to NULL before calling DisposePDFLPrintUserParams, or the dispose logic will attempt to free stack memory. The sample handles this with an explicit null check before disposal.
Sample: PDFPrintDefault.cpp on GitHub
Sample 2: PDFPrintGUI
When to Use This
Use PDFPrintGUI for desktop applications where the user should choose the printer, set a page range, specify the number of copies, or opt to print to a file. The OS print dialog handles all of these choices, and APDFL reads the user's selections back from the dialog result and applies them to the print job.
Steps 1 and 2: Identical to PDFPrintDefault
Library initialization, document opening, and print parameter initialization are exactly the same as PDFPrintDefault. The only fields set in Step 2 for PDFPrintGUI are emitToFile = false, emitToPrinter = true, and the printParams link. Page range, copies, and paper size are not pre-set because the user will specify them in the dialog.
Step 3: Open the Print Dialog (Windows)
On Windows, PrintDlgW is called without PD_RETURNDEFAULT, which causes it to display the dialog. Two flags are set: PD_USEDEVMODECOPIESANDCOLLATE tells the dialog to use DEVMODE for copies and collation (required for APDFL), and PD_NOSELECTION disables the Selection radio button since printing a selection is not supported:
printDialog.Flags = PD_USEDEVMODECOPIESANDCOLLATE | PD_NOSELECTION;
printDialog.nMinPage = 1;
printDialog.nMaxPage = inAPDoc.numPages();
if (PrintDlgW(&printDialog)) {
// User confirmed -- copy DEVMODE and device name (same as PDFPrintDefault)
// Apply page range if user selected specific pages
if (printDialog.Flags & PD_PAGENUMS) {
psParams.ranges[0].startPage = printDialog.nFromPage - 1;
psParams.ranges[0].endPage = printDialog.nToPage - 1;
}
// Handle Print to File
if (printDialog.Flags & PD_PRINTTOFILE) {
size_t lenFileName = dmFileName.length();
userParams.outFileNameW = new ASUns16[lenFileName + 1];
memcpy(userParams.outFileNameW, dmFileName.data(), lenFileName * sizeof(ASUns16));
userParams.outFileNameW[lenFileName] = 0;
}
} else {
// User canceled -- clean up and exit
DisposePDPrintParams(&psParams);
DisposePDFLPrintUserParams(&userParams);
PDDocClose(inDoc);
return (-1);
}
The page range is written into psParams.ranges[0], not userParams, because page ranges are a PostScript-level concept. The dialog returns 1-based page numbers, so 1 is subtracted from both nFromPage and nToPage to convert to APDFL's zero-based page indices.
For Print to File, Windows handles the actual file path prompt internally when outFileNameW is set to the dmFileName value (which returns as "FILE:"). No changes to emitToFile or emitToPrinter are needed -- Windows intercepts the print job and redirects it to the file.
If the user cancels the dialog (PrintDlgW returns false), the sample cleans up and returns -1. Always handle the cancel case explicitly or the program will proceed to PDFLPrintDoc with an unconfigured userParams, which will attempt to print to an undefined destination.
Step 3: Open the Print Dialog (macOS)
On macOS, NSPrintPanel is used instead of PrintDlgW. The panel runs modally and returns a Boolean indicating whether the user confirmed or canceled. The page range, copy count, print session, and page format are all read back from the updated NSPrintInfo after the panel closes:
NSPrintPanel *printPanel = [NSPrintPanel printPanel];
Boolean accepted = [printPanel runModalWithPrintInfo:thePrintInfo];
userParams.printSession = (PMPrintSession)[thePrintInfo PMPrintSession];
userParams.printSettings = (PMPrintSettings)[thePrintInfo PMPrintSettings];
userParams.pageFormat = (PMPageFormat)[thePrintInfo PMPageFormat];
PMGetFirstPage(userParams.printSettings, &first);
userParams.startPage = first;
PMGetLastPage(userParams.printSettings, &last);
userParams.endPage = last;
PMGetCopies(userParams.printSettings, &numCopies);
userParams.nCopies = numCopies;
if (!accepted) {
// User canceled -- clean up and return -1
}
Step 4: Send the Job and Clean Up
Step 4 is identical to PDFPrintDefault: PDFLPrintDoc sends the job, then DisposePDPrintParams, DisposePDFLPrintUserParams, and PDDocClose clean up in that order.
Sample: PDFPrintGUI.cpp on GitHub
Key Differences Between the Two Samples
PDFPrintDefault uses PD_RETURNDEFAULT to silently retrieve the default printer. PDFPrintGUI omits that flag, which causes PrintDlgW to display the full dialog. PDFPrintDefault pre-sets the page range to all pages; PDFPrintGUI reads the page range from the dialog result and only applies it if the user selected specific pages. PDFPrintGUI also handles the Print to File option and the cancel case, neither of which is relevant in PDFPrintDefault. Everything else -- the parameter structures, the quality flags, the PDFLPrintDoc call, and the cleanup sequence -- is the same.
Get Started
PDFPrintDefault.cpp, PDFPrintGUI.cpp, and the full Adobe PDF Library SDK are available through a free trial. Get started today!