Technischer Artikel

PDF-Viewer für Lazarus und Free Pascal mit PDFium

Die Portierung wirkte am Freitag fertig. Ein Delphi-PDF-Viewer, in zwei Tagen nach Lazarus verschoben: ein paar Unit-Namen geändert, einige {$IFDEF FPC}-Blöcke ergänzt, und das Projekt kompilierte sauber. Der Testlauf am Montag erzählte eine andere Geschichte — die Suchbox fand keine Wörter, die deutlich auf der Seite standen, und ein Dateiname mit Akzenten ließ sich nicht öffnen. Nichts in der Compiler-Ausgabe hatte auf einen der beiden Fehler hingewiesen, weil beide String-Encoding-Probleme waren, und Encoding-Abweichungen unsichtbar bleiben, bis reale Daten durch sie fließen. Einen Viewer zu portieren, der auf PDFium Component basiert (mit VCL- und LCL-Editionen aus einer Quelle), ist größtenteils mechanisch; die nichtmechanischen Teile sind genau das Thema dieses Artikels.

Dasselbe Pascal, andere String-Nutzlast

Delphis nativer string ist seit 2009 UTF-16. Lazarus und Free Pascal verwenden in LCL-Anwendungen standardmäßig UTF-8. Die textnahen APIs der Komponente sprechen UTF-16 über den Typ WString, den der FPC-Build auf WideString abbildet — also ist jede Grenze, an der Text zwischen Ihrer LCL-Oberfläche und der PDF-Engine wechselt, ein Konvertierungspunkt.

Die Konvertierungen laufen bei einfachen Zuweisungen automatisch, aber zwei Gewohnheiten verhindern die Montagmorgen-Klasse von Fehlern. Reichen Sie Text unverändert durch, ohne Byte-Manipulation: Code, der einen Suchbegriff nach Byte-Offset schneidet, funktioniert in Delphi (wo ein Char eine UTF-16-Einheit ist) und beschädigt mehrbyteiges UTF-8 in der LCL. Und testen Sie vom ersten Tag an mit Nicht-ASCII-Daten — ein deutscher Dateiname, ein Suchbegriff mit Gedankenstrich — weil reine ASCII-Testdaten jedes Encoding-Problem unsichtbar machen.

Eine kleine bedingte Schicht statt eines Forks

Nach dem ersten Dutzend IFDEFs liegt die Versuchung nahe, die Codebasis pro IDE zu verzweigen. Widerstehen Sie: Die Unterschiede passen in einen gemeinsamen Deklarationsblock, und ein Fork verdoppelt jede künftige Fehlerkorrektur. Die bedingte Schicht bleibt so klein:

{$IFDEF FPC}
uses
  LCLType, Forms, Graphics, Controls;

type
  WString = WideString;   // component text APIs are UTF-16
  TBytes  = array of Byte;
{$ELSE}
uses
  Winapi.Windows, Vcl.Forms, Vcl.Graphics, Vcl.Controls;
{$ENDIF}

Alles unter diesem Block — Dokumentbehandlung, Seitennavigation, Renderaufrufe — kompiliert in beiden IDEs identisch, weil TPdf und TPdfView in der VCL- und der LCL-Edition dieselbe Oberfläche bereitstellen. Die Disziplin dahinter ist strukturell: Gemeinsame PDF-Logik lebt in Units ohne framework-spezifische Dialoge oder Panels, und alles, was wirklich abweicht (Druckdialoge, Dateiauswahl mit Plattformkonventionen), liegt hinter einer dünnen Schnittstelle, die einmal pro Framework implementiert wird. Der IFDEF-Block oben ist auch der richtige Ort für künftige Plattformabweichungen; der Rest der Codebasis bleibt frei von verstreuten Compilerbedingungen.

Das Formular in Code bauen, nicht in zwei Designern

Form-Streaming ist die Stelle, an der Dual-IDE-Projekte leise verrotten: Eine .dfm und eine .lfm, die „dasselbe“ Formular beschreiben, driften Eigenschaft für Eigenschaft auseinander, bis sich die beiden Builds unterschiedlich verhalten und niemand den Grund sauber diffen kann. Für den Viewer-Kern umgeht Laufzeitkonstruktion das ganze Problem — eine Konstruktionssequenz, versioniert als normaler Code:

procedure TViewerForm.FormCreate(Sender: TObject);
begin
  Pdf := TPdf.Create(Self);

  PdfView := TPdfView.Create(Self);
  PdfView.Parent := Self;
  PdfView.Align := alClient;
  PdfView.Pdf := Pdf;
  PdfView.FitMode := pfmFitWidth;

  if ParamCount > 0 then
  begin
    Pdf.FileName := ParamStr(1);
    Pdf.Active := True;   // opens the document; PageCount valid after this
  end;
end;

Die Reihenfolge ist weniger wichtig als die Kopplung: PdfView.Pdf := Pdf bindet das visuelle Control an die Dokumentkomponente, danach verhalten sich Seitennavigation über PageNumber und Zoom über FitMode unter VCL und LCL identisch. Ein frameworkübergreifendes Verhalten sollten Sie kennen, bevor Benutzer es finden: Eine manuelle Zuweisung an Zoom setzt FitMode in beiden Frameworks wieder auf pfmNone. Wenn Ihre Toolbar „fit width“ als dauerhafte Präferenz anbietet, stellen Sie den Fit-Modus nach jeder programmatischen Zoomänderung wieder her, sonst hört die Präferenz still auf, dauerhaft zu sein.

Die Binärdatei, vor der die IDE nie gewarnt hat

Die Komponente umschließt die PDFium-Engine, die als plattformabhängige Binärdatei ausgeliefert wird — und das Laden dieser Binärdatei erzeugt die Berichte „funktioniert in der IDE, scheitert über die installierte Verknüpfung“. Drei Regeln decken fast alle Fälle ab. Die Bitness muss exakt passen: Eine 32-Bit-Anwendung kann keine 64-Bit-pdfium-Bibliothek laden, und die Fehlermeldung des Betriebssystems („Modul nicht gefunden“ auf manchen Windows-Versionen) führt aktiv in die Irre, weil die Datei genau dort liegt. Lösen Sie den Bibliothekspfad relativ zur ausführbaren Datei auf, nie relativ zum Arbeitsverzeichnis — IDE-Starts und Shell-Starts unterscheiden sich genau dort. Und melden Sie Ladefehler vor dem ersten Dokument mit einer Nachricht, die erwarteten Pfad und Architektur nennt; ein Supportticket „PDFium-64-Bit-Binary fehlt unter <Pfad>“ ist in Minuten erledigt, „Viewer stürzt beim Start ab“ nicht.

Versionieren Sie die Engine-Binärdatei zusammen mit der Anwendung. PDFium bewegt sich schnell, und ein Installer, der die Anwendung aktualisiert, aber eine ältere Bibliothek auf der Platte lässt, produziert Abstürze, die kein Rechner im Büro reproduzieren kann, weil jeder dort das passende Paar besitzt. Behandeln Sie die Bibliothek als Teil des Build-Artefakts: derselbe Installer, derselbe Versionsstempel, dasselbe Rollback.

Komponenten in der Lazarus-IDE registrieren

Laufzeitkonstruktion braucht überhaupt keine Designzeitregistrierung, und das ist der einfachste korrekte Aufbau für eine Viewer-Anwendung. Wenn Sie die Komponenten auf der Lazarus-Palette haben möchten, installieren Sie das Package und lassen Sie dessen dedizierte Registrierungseinheit (PDFiumLazReg) arbeiten — diese Unit ist im Package absichtlich als design-time markiert, weil sie IDE-Property-Editor-Schnittstellen referenziert, die nie in Ihre ausgelieferte Anwendung gelinkt werden dürfen.

Das Symptom eines falschen Aufbaus ist eine Anwendung, die geheimnisvoll von IDE-Packages abhängt und auf Rechnern ohne Lazarus als Deployment-Fehler sichtbar wird.

Sprache und Screenreader außerhalb von Windows

Wenn das Delphi-Original Text-to-speech bot, erbt die Portierung eine Plattformentscheidung. SAPI — das übliche TTS-Backend unter Windows — existiert nur dort. Lazarus-Builds für Windows behalten volles SAPI- und NVDA-kompatibles Verhalten, sodass eine Windows-zu-Windows-Portierung nichts verliert und NVDA-Benutzer mit dem Viewer in beiden IDEs identisch arbeiten.

Linux- und macOS-Ziele brauchen ein anderes Sprachbackend, das an dieselben Lese-APIs angeschlossen wird. Genau deshalb sollte Sprache von Beginn an hinter einer Schnittstelle liegen. Die Lesereihenfolge- und Wortverfolgungsmechanik selbst ist plattformneutral; nur die Audioausgabe wechselt. Der Artikel zum barrierefreien Reader behandelt diese Mechanik ausführlich.

Was vor „Portierung fertig“ getestet werden sollte

Ein Paritätsdurchlauf, der reale Regressionen gefangen hat, ungefähr in der Reihenfolge ihres Auftretens: Öffnen Sie ein Dokument, dessen Pfad Nicht-ASCII-Zeichen enthält; suchen Sie nach einem Begriff mit Nicht-ASCII-Zeichen und bestätigen Sie Trefferhervorhebung; testen Sie Mausrad-Scrollen, Ziehauswahl und Tastaturnavigation auf jedem Ziel-Widgetset, weil Fokus- und Radverhalten zu den widgetset-abhängigsten Bereichen der LCL gehören; prüfen Sie Rendering bei 100 %, 150 % und 200 % Anzeigeskalierung; und führen Sie den installierten Build — nicht den IDE-Build — auf einem Rechner ohne IDE aus, denn nur dieser Test übt die Binärauflösung ehrlich.

Die Rendering-Durchsatzcharakteristik ist zwischen den Editionen gleich, daher gilt die Cache-Strategie aus dem Artikel zu Render-Cache und Zoom-Performance unverändert.

FAQ

Ist die LCL-Edition im Vergleich zur VCL-Edition feature-complete?

Die Kernfläche — TPdf, TPdfView, Rendering, Formulare, Textextraktion und die Accessibility-APIs — ist auf beiden gleich. Die echten Unterschiede sind plattformgebunden: SAPI-Sprachausgabe ist Windows-only, und Druck- sowie Dateidialoge folgen den Konventionen des jeweiligen Frameworks.

Warum stürzt mein Lazarus-Build beim Start ab, obwohl der Delphi-Build läuft?

Prüfen Sie zuerst die Binärauflösung: Architekturabweichung zwischen ausführbarer Datei und pdfium-Bibliothek oder ein Ladepfad, der das Arbeitsverzeichnis der IDE voraussetzt. Beide erzeugen sofortige Startfehler, die wie Komponentenfehler aussehen und Deployment-Fehler sind.

Kann ich ein gemeinsames Formular zwischen den IDEs behalten?

Formularbeschreibungsdateien lassen sich nicht übertragen — .dfm und .lfm sind unterschiedliche Formate mit unterschiedlichen Eigenschaftssätzen. Der Laufzeitaufbau der Viewer-Oberfläche, wie oben gezeigt, ersetzt zwei Designerdateien durch einen Codepfad und beseitigt das Driftproblem vollständig.

Die hier beschriebenen VCL- und LCL-Editionen werden gemeinsam als PDFium Component ausgeliefert, mit Quellcode und identischen öffentlichen APIs für Delphi, C++Builder und Lazarus/FPC.