Technischer Artikel

Drucken von PDF-Dokumenten mit der PDFium-Komponente in Delphi

PDF-Koordinaten sind in Punkten, Drucker-Koordinaten in Geräteeinheiten, und beide haben nichts miteinander zu tun, bis Sie sie bewusst konvertieren. Diese Diskrepanz ist die Ursache für die meisten schlechten Druckergebnisse in Delphi-Anwendungen: Der Code sendet die richtige Datei, aber die Seite wird beschnitten, gestreckt oder leer ausgegeben. Die PDFium-Komponente handhabt das Rendering sauber; die Drucker-Infrastruktur ist Standard-VCL. Beide passen mit etwas Code zusammen, sobald man versteht, was jede Seite erwartet

Wie die Rendern-dann-Drucken-Pipeline funktioniert

Die PDFium-Komponente spricht Drucker nicht direkt an. Das Muster lautet: Rendern Sie eine Seite mit der gewünschten Auflösung in ein TBitmap und übertragen Sie diese Bitmap dann mit StretchDIBits auf den Canvas des Druckers. TPdf.RenderPage gibt eine Bitmap zurück, die dem Aufrufer gehört, sodass Sie die Pixelabmessungen kontrollieren. Wenn Sie [rePrinting] in der Optionsmenge übergeben, wechselt PDFium seinen Rendering-Pfad zu einem, der bildschirmspezifische Effekte wie LCD-Subpixel-Hinting weglässt und die MediaBox der Seite für die Druckausgabe korrekt handhabt. Lassen Sie rePrinting weg, und was Sie an den Drucker senden, ist ein Bildschirm-Rendering, das auf einem Monitor gut aussieht, aber tendenziell eine unschärfere Ausgabe auf Hoch-DPI-Druckern erzeugt, da die Hinting-Entscheidungen, die für 96-DPI-Bildschirme getroffen wurden, nicht für den 300- oder 600-DPI-Druck geeignet sind

TPdf.Active ist das einzige Gate, das überprüft werden muss, bevor eine Seiteneigenschaft berührt wird. Die Komponente schluckt Ladefehler stillschweigend: Das Setzen von Active := True bei einer beschädigten oder passwortgeschützten Datei löst keine Ausnahme aus; es belässt Active einfach auf False. Überprüfen Sie es immer nach der Zuweisung. Das Lesen von PageCount oder PageWidth in einem inaktiven Dokument gibt null zurück, was zu stillen No-Ops führt, die sehr schwer zu diagnostizieren sind, sobald sie den Spooler erreichen

Render-dann-Drucken-Pipeline in Delphi mit PDFium Component: die geladene PDF-Seite rendert über TPdf.RenderPage mit dem rePrinting-Flag in eine druckergroße TBitmap, dann transportiert StretchDIBits sie auf die Druckerleinwand; gegenüber der weichen, auf dem Bildschirm gerenderten Ausgabe, wenn rePrinting entfällt
PDFium Component spricht nie direkt mit dem Spooler. RenderPage erzeugt die Bitmap, StretchDIBits bewegt sie auf die Drucker-Canvas, und rePrinting wählt den für Papier bestimmten Renderpfad

Eine minimale Druckschleife

Der einfachste funktionierende Fall lädt eine Datei, öffnet einen Druckauftrag, iteriert durch die Seiten und schließt. Das einzige knifflige Detail ist, dass Printer.NewPage nicht vor der ersten Seite aufgerufen werden darf, daher das Flag FirstPage. Die StretchDIBits-Übertragung läuft über GetDIBSizes und GetDIB, um geräteunabhängige Bits aus dem Bitmap-Handle zu ziehen, und malt sie dann in voller Seitengröße auf den Drucker-Canvas:

procedure PrintPdfFile(const FileName: string);
var
  Pdf: TPdf;
  I: Integer;
  Bitmap: TBitmap;
  InfoHeaderSize, ImageSize: DWORD;
  InfoHeader: PBitmapInfo;
  Image: Pointer;
  FirstPage: Boolean;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;  // Laden ist still fehlgeschlagen; abbrechen

    Printer.Title := Pdf.Title;
    Printer.BeginDoc;
    try
      FirstPage := True;
      for I := 1 to Pdf.PageCount do
      begin
        if FirstPage then
          FirstPage := False
        else
          Printer.NewPage;

        Pdf.PageNumber := I;

        // In Druckerauflösung rendern; rePrinting passt den Render-Pfad an
        Bitmap := Pdf.RenderPage(
          0, 0,
          Printer.PageWidth,
          Printer.PageHeight,
          ro0,
          [rePrinting]
        );
        try
          GetDIBSizes(Bitmap.Handle, InfoHeaderSize, ImageSize);
          InfoHeader := AllocMem(InfoHeaderSize);
          try
            Image := AllocMem(ImageSize);
            try
              GetDIB(Bitmap.Handle, 0, InfoHeader^, Image^);
              StretchDIBits(
                Printer.Canvas.Handle,
                0, 0, Printer.PageWidth, Printer.PageHeight,
                0, 0, Bitmap.Width, Bitmap.Height,
                Image, InfoHeader^, DIB_RGB_COLORS, SRCCOPY
              );
            finally
              FreeMem(Image);
            end;
          finally
            FreeMem(InfoHeader);
          end;
        finally
          Bitmap.Free;
        end;
      end;
    finally
      Printer.EndDoc;
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Durch Übergabe von Printer.PageWidth und Printer.PageHeight als Bitmap-Dimensionen rendern Sie in der nativen Pixelgröße des Druckers, was bereits die Geräte-DPI berücksichtigt. Der StretchDIBits-Aufruf ordnet diese Pixel dann 1:1 auf der Seite an. Dies bietet Ihnen die bestmögliche Genauigkeit ohne explizite DPI-Arithmetik, funktioniert aber nur, wenn die PDF-Seite und das physische Papier zufällig dieselbe Größe haben. Wenn sie sich unterscheiden, benötigen Sie eine explizite Skalierung

Skalierung bei unterschiedlichen Seiten- und Papiergrößen

Eine PDF-Seite im A4-Hochformat passt nicht automatisch auf einen US-Letter-Drucker, und eine Querformatseite, die an einen Hochformatdrucker gesendet wird, wird abgeschnitten. Der Standardansatz besteht darin, aus dem Verhältnis von Druckerpixeln zu PDF-Punkten einen einheitlichen Skalierungsfaktor zu berechnen und ihn dann auf beide Dimensionen anzuwenden, damit das Seitenverhältnis erhalten bleibt. Pdf.PageWidth und Pdf.PageHeight geben die aktuellen Seitenabmessungen in Punkten an, wobei ein Punkt 1/72 Zoll entspricht. Multiplizieren mit der Ziel-DPI und Dividieren durch 72 rechnet in Pixel bei dieser Auflösung um. Nehmen Sie das Min der X- und Y-Verhältnisse, um die größte Skalierung zu erhalten, die noch in den druckbaren Bereich passt:

Einpassungsskalierung für den PDF-Druck in Delphi: eine 595 mal 842 Punkte große PDF-Seite sitzt skaliert im bedruckbaren Bereich aus Druckerpixeln, umgerechnet mit Punkten mal DPI durch 72, skaliert um den kleineren der Breiten- und Höhenquotienten, auf Einpassen durch Verkleinern begrenzt und in die Zielbitmap bemessen
Punkte werden zu Pixeln bei Punkten mal Dpi durch 72, und das kleinere Verhältnis gewinnt, sodass nichts abgeschnitten wird. Die Klemmung auf 1.0 hält die Anpassung nur schrumpfend
// PDF-Seite in den druckbaren Bereich einpassen, Seitenverhältnis wahren
var
  ScaleX, ScaleY, Scale: Double;
  DestWidth, DestHeight: Integer;
  Dpi: Integer;
begin
  Dpi := 300;  // Ziel-Renderauflösung
  Pdf.PageNumber := PageIndex;

  ScaleX := Printer.PageWidth  / (Pdf.PageWidth  * Dpi / 72);
  ScaleY := Printer.PageHeight / (Pdf.PageHeight * Dpi / 72);
  Scale  := Min(ScaleX, ScaleY);

  // Clamp to 1.0 for shrink-to-fit only (no enlargement)
  if Scale > 1.0 then Scale := 1.0;

  DestWidth  := Round(Pdf.PageWidth  * Dpi / 72 * Scale);
  DestHeight := Round(Pdf.PageHeight * Dpi / 72 * Scale);

  Bitmap := Pdf.RenderPage(0, 0, DestWidth, DestHeight, ro0,
    [rePrinting, reAnnotations]);
  // ... Übertragung mit StretchDIBits wie oben
end;

Ein Rendering mit Dpi = 300 eignet sich für die meisten Bürodrucker. Bei 600 DPI beläuft sich die Bitmap für eine einzelne A4-Seite auf rund 34 Megapixel, was etwa 100 MB als 32-Bit-Bitmap entspricht; der Qualitätsgewinn bei gewöhnlichen Textdokumenten ist minimal und die Speicherkosten pro Seite sind erheblich. Behalten Sie 600 DPI für Druckereien oder vektorlastige technische Zeichnungen, bei denen es wirklich darauf ankommt

Das Flag reAnnotations im zweiten Codeblock ist unabhängig von rePrinting. Binden Sie es ein, wenn der Benutzer erwartet, dass Stempel, Hervorhebungen und Kommentarboxen auf dem Papier erscheinen. Lassen Sie es für reine Inhaltsausgaben weg. Beide Flags können frei kombiniert werden

Seitendrehung

PDFium speichert die Seitendrehung im PDF als /Rotate-Eintrag, auf den über Pdf.PageRotation zugegriffen werden kann, welches einen TRotation-Wert zurückgibt (ro0, ro90, ro180, ro270). Das Druckkoordinatensystem invertiert Drehungen um 90 und 270 Grad relativ zum Bildschirm. Wenn Sie den rohen PageRotation-Wert ohne jegliche Anpassung direkt an RenderPage übergeben, werden Querformatseiten, die in einem Hochformatdokument eingebettet sind, auf den meisten Windows-Druckertreibern auf dem Kopf gedruckt. Die Lösung ist ein einfacher Tausch vor dem Renderaufruf: Weisen Sie ro90 zu ro270 und ro270 zurück zu ro90 zu, während ro0 und ro180 unverändert bleiben

Überprüfen Sie dieses Verhalten auf Ihrem spezifischen Zieldrucker, bevor Sie ausliefern. Das Treiberverhalten bezüglich der Drehung ist nicht bei allen Anbietern einheitlich, und einige Treiber wenden ihre eigene Rotationskorrektur auf GDI-Ebene an. Wenn Sie eine doppelte Drehung sehen, entfernen Sie den Tausch; wenn Sie überhaupt keine Korrektur sehen, fügen Sie ihn hinzu. Ein Dokument mit gemischter Ausrichtung und abwechselnden Hoch- und Querformatseiten ist der schnellste Weg, beide Fehlermodi beim Testen zu erkennen

Speicherverwaltung bei einem langen Druckauftrag

Jeder Aufruf von RenderPage weist ein neues TBitmap zu, das dem Aufrufer gehört und freigegeben werden muss. In der obigen Schleife handhabt der try/finally Bitmap.Free-Block dies korrekt für eine Seite nach der anderen. Sammeln Sie keine Bitmaps seitenübergreifend an: Ein 300-DPI-Rendering eines 200-seitigen Dokuments würde Gigabytes verbrauchen, bevor die erste Seite den Spooler erreicht. Geben Sie jedes Bitmap frei, bevor Sie zur nächsten Seite übergehen

Das AllocMem / FreeMem-Paar innerhalb des Übertragungsblocks folgt derselben Regel. GetDIBSizes teilt Ihnen mit, wie viel Speicher der DIB-Header und die Pixeldaten benötigen; Sie reservieren, füllen, zeichnen und geben alles innerhalb des Geltungsbereichs einer Seite frei. Lässt man einen der Blöcke lecken, wird der Druckauftrag bei Dokumenten, die länger als ein paar Dutzend Seiten sind, den Prozess-Heap erschöpfen

Wenn Sie Druckaufträge in einem Hintergrundthread ausführen müssen, behalten Sie TPdf und alle VCL-Druckeraufrufe im selben Thread. TPdf selbst ist über Instanzen hinweg, die den globalen Status der PDFium-DLL teilen, nicht threadsicher; das sicherste Modell ist ein TPdf pro Thread, wobei jeder seine eigene Kopie der Datei lädt

Die hier gezeigte Rendering- und Dokumenten-API ist Teil der PDFium-Komponente für Delphi und C++Builder