Delphi und Lazarus kompilieren dasselbe Object Pascal, und genau diese oberflächliche Ähnlichkeit macht die Portierung eines Viewers zwischen ihnen trügerisch. Die beiden Toolchains weichen an drei Stellen ab, die für PDF-Arbeit zählen: Der native string-Typ ist in Delphi UTF-16 und in einer LCL-Anwendung UTF-8; VCL und LCL sind verschiedene visuelle Frameworks mit eigenen Steuerelementen, Dialogen und Formular-Streaming-Formaten; und ein Delphi-Binary zielt auf Windows, während ein FPC-Binary auch auf Linux oder macOS zusteuern kann. Keiner dieser Unterschiede zeigt sich zur Compile-Zeit. Ein Viewer auf Basis der PDFium-Komponente, die VCL- und LCL-Editionen aus einem einzigen Quellbaum liefert, kompiliert nach einer Handvoll Unit-Namen-Tauschen und ein paar {$IFDEF FPC}-Blöcken sauber unter Lazarus. Die Fehler kommen später, wenn echte Daten und ein echter Einsatz die Annahmen bloßlegen, die der Delphi-Build stillschweigend getroffen hatte
Vier dieser Annahmen verantworten den größten Teil der verlorenen Zeit: die Textkodierung an der UI-Grenze, die Versuchung, zwei Kopien des Formulars zu pflegen, die Art, wie sich ein natives Engine-Binary zur Laufzeit auflöst, und der Moment, in dem der Sprachausgabe die Plattform ausgeht, sobald SAPI fehlt. Jede davon ist günstig zu handhaben, wenn man weiß, dass sie kommt, und teuer zu jagen, wenn nicht
Gleiches Pascal, andere String-Inhalte
Delphis nativer string ist seit 2009 UTF-16. Lazarus und Free Pascal stehen in LCL-Anwendungen standardmäßig auf UTF-8. Die textseitigen APIs der Komponente sprechen UTF-16 über den Typ WString, den der FPC-Build auf WideString aliasiert, sodass jede Grenze, an der Text zwischen Ihrer LCL-UI und der PDF-Engine wechselt, ein Konvertierungspunkt ist
Die Konvertierungen passieren in einfachen Zuweisungen automatisch, und der meiste Code muss nie an sie denken. Zwei Gewohnheiten halten die Kodierungs-Bugs draußen. Reichen Sie Text direkt durch, ohne Manipulation auf Byte-Ebene: Code, der einen Suchbegriff per Byte-Offset zerschneidet, funktioniert in Delphi, wo ein Char eine UTF-16-Einheit ist, und korrumpiert Multi-Byte-UTF-8 in der LCL. Und testen Sie vom ersten Lauf an mit Nicht-ASCII-Daten. Ein deutscher Dateiname, ein kyrillischer Suchbegriff, ein akzentuierter Autorenname in den Dokumentmetadaten: Rein-ASCII-Testdaten verstecken jeden Kodierungsfehler, weil ASCII der eine Bereich ist, in dem UTF-8 und UTF-16 Zeichen für Zeichen übereinstimmen. Der Bug ist die ganze Zeit real; ASCII hält ihn nur unsichtbar, bis ein Kunde in München eine Datei öffnet, die Sie nie probiert haben
Ein bedingter Block, kein Fork pro IDE
Nach dem ersten Dutzend IFDEFs fühlt sich die Codebasis an wie zwei Projekte in einem Repository, und ein Fork pro IDE sieht verlockend aus. Er ist der falsche Griff. Die echten Unterschiede fallen in einen gemeinsamen Deklarationsblock zusammen, und ein Fork verdoppelt ab dann die Kosten jeder Fehlerbehebung. Halten Sie die bedingte Schicht so klein:
{$IFDEF FPC}
uses
LCLType, Forms, Graphics, Controls;
type
WString = WideString; // Komponenten-Text-APIs sind UTF-16
TBytes = array of Byte;
{$ELSE}
uses
Winapi.Windows, Vcl.Forms, Vcl.Graphics, Vcl.Controls;
{$ENDIF}
Alles unterhalb dieses Blocks kompiliert in beiden IDEs identisch. Dokumentenhandhabung, Seitennavigation, Rendering-Aufrufe: TPdf und TPdfView bieten in der VCL- und der LCL-Edition dieselbe Oberfläche, sodass der Großteil des Viewers nie eine Compiler-Bedingung sieht. Das so zu halten ist eher strukturelle Disziplin als ein cleverer Trick. Geteilte PDF-Logik lebt in Units, die keine framework-spezifischen Dialoge oder Panels hereinziehen. Die Handvoll Dinge, die sich wirklich unterscheiden, etwa Druckdialoge und Dateiauswahldialoge mit ihren Plattformkonventionen, verstecken sich hinter einer dünnen Schnittstelle, die pro Framework einmal implementiert wird. Der IFDEF-Block wird zum einzigen Ort, an dem künftige Plattform-Divergenz landen darf, statt Compiler-Direktiven über vierzig Units zu verteilen
Bauen Sie das Formular im Code, nicht in zwei Designern
Formular-Streaming ist die Stelle, an der Dual-IDE-Projekte still verrotten. Eine .dfm und eine .lfm, die behaupten, dasselbe Formular zu beschreiben, driften Eigenschaft für Eigenschaft auseinander, bis sich die beiden Builds aus Gründen unterschiedlich verhalten, die niemand diffen kann, weil die beiden Dateien nicht einmal im selben Format vorliegen. Den Viewer zur Laufzeit zu konstruieren umgeht das ganze Problem. Es gibt eine Konstruktor-Sequenz, in der Versionsverwaltung als ganz normaler Code, und sie liest sich auf beiden Plattformen gleich:
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; // öffnet das Dokument; PageCount ist danach gültig
end;
end;
Die genaue Reihenfolge dieser Zuweisungen ist weniger wichtig als die eine Zeile, die die eigentliche Arbeit erledigt. PdfView.Pdf := Pdf bindet das visuelle Steuerelement an die Dokumentkomponente, und von diesem Punkt an reagieren Seitennavigation über PageNumber und Fit-Verhalten über FitMode unter VCL und LCL identisch. Eine Framework-übergreifende Eigenheit lohnt zu kennen, bevor ein Benutzer sie als Bug meldet: Ein von Hand zugewiesenes Zoom snappt FitMode in beiden Frameworks auf pfmNone zurück. Wenn Ihre Toolbar „Auf Breite einpassen“ als klebende Einstellung behandelt, müssen Sie den Fit-Modus nach jedem programmatischen Zoom neu zuweisen, sonst hört die Einstellung auf zu kleben, sobald Code die Zoomstufe berührt
Die Binärdatei, vor der die IDE nie gewarnt hat
Die Komponente umschließt die PDFium-Engine, die als native Plattform-Binärdatei ausgeliefert wird, und diese Binärdatei ist die Quelle fast jedes Berichts der Art „läuft in der IDE, scheitert vom installierten Shortcut“. Drei Regeln verantworten die meisten davon. Die Bitigkeit muss exakt passen. Ein 32-Bit-Executable kann keine 64-Bit-PDFium-Bibliothek laden, und die Meldung, die das Betriebssystem zurückgibt („Modul nicht gefunden“ auf manchen Windows-Versionen), führt aktiv in die Irre, weil die Datei genau dort neben dem Executable liegt. Lösen Sie den Bibliothekspfad relativ zum Executable auf, niemals relativ zum Arbeitsverzeichnis; ein IDE-Start und ein Shell-Start unterscheiden sich genau in diesem Punkt, weshalb sich der Bug während der Entwicklung versteckt. Und fangen Sie ein fehlgeschlagenes Laden ab, bevor das erste Dokument geöffnet wird, und melden Sie es mit ausgeschriebenem erwartetem Pfad und Architektur. Ein Support-Ticket, das liest „PDFium-64-Bit-Binärdatei fehlt unter <path>“, ist in Minuten geschlossen. Eines, das liest „Viewer stürzt beim Start ab“, wird zu einer Woche Hin und Her
Versionieren Sie die Engine-Binärdatei gleich mit neben dem Executable. PDFium bewegt sich schnell, und ein Installer, der die Anwendung aktualisiert, aber eine veraltete Bibliothek auf der Platte lässt, erzeugt Abstürze, die niemand in Ihrem Büro reproduzieren kann, aus dem einfachen Grund, dass jede Maschine in Ihrem Büro zufällig das zusammenpassende Paar hält. Behandeln Sie die Bibliothek als Teil des Build-Artefakts, mit demselben Installer, demselben Versionsstempel und demselben Rollback-Pfad wie das Executable, das sie lädt
Komponenten in der Lazarus-IDE registrieren
Die Laufzeit-Konstruktion braucht überhaupt keine Design-Time-Registrierung, was die sauberste Konfiguration für einen Viewer ist, der seine eigene UI im Code baut. Wenn Sie die Komponenten doch für Design-Time-Arbeiten auf der Lazarus-Palette haben wollen, installieren Sie das Paket und lassen Sie seine eigene Registrierungs-Unit, PDFiumLazReg in Lib/FPC/PDFiumLaz.lpk, das erledigen. Diese Unit ist absichtlich als Design-Time markiert: Sie referenziert IDE-Schnittstellen für Property-Editoren, die niemals in Ihr ausgeliefertes Executable linken dürfen
Machen Sie das falsch, ist das Symptom eine Anwendung, die unerklärlich von IDE-Paketen abhängt, was sich als Deployment-Fehler auf der ersten Kundenmaschine zeigt, auf der nie Lazarus installiert war
Sprachausgabe und Screenreader außerhalb von Windows
Sprachausgabe ist das eine Feature, an dem die Cross-Platform-Geschichte bricht, und sie bricht am Betriebssystem, nicht an der Komponente. SAPI, das übliche TTS-Backend unter Windows, existiert nur unter Windows. Ein Lazarus-Build, der weiterhin auf Windows zielt, behält die volle SAPI-Ausgabe und dasselbe NVDA-kompatible Verhalten wie das Delphi-Original, verliert also eine Windows-auf-Windows-Portierung hier nichts, und ein NVDA-Benutzer kann die beiden Builds nicht unterscheiden
Ein Linux- oder macOS-Ziel ist eine andere Sache. Es gibt kein SAPI aufzurufen, sodass die Audio-Ausgabe an einen nativen Sprachdienst neu verdrahtet werden muss, während die Lese-APIs darüber unangetastet bleiben. Diese Aufteilung ist das Argument dafür, Sprachausgabe vom ersten Commit an hinter eine Schnittstelle zu stellen: Die Lesereihenfolge-Analyse und der Wort-Verfolgungs-Cursor sind plattformneutral und tragen unverändert hinüber, und nur die dünne Schicht, die tatsächlich Ton erzeugt, muss sich pro Plattform ändern. Der Artikel zum barrierefreien Reader behandelt diese Lese-Maschinerie in der Tiefe
Eine Paritäts-Checkliste, bevor Sie die Portierung fertig nennen
Der folgende Durchgang hat echte Regressionen gefunden, aufgelistet ungefähr in der Reihenfolge, in der Fehler auftauchen. Öffnen Sie ein Dokument, dessen Pfad Nicht-ASCII-Zeichen enthält. Suchen Sie einen Begriff mit Nicht-ASCII-Zeichen und bestätigen Sie, dass die Treffer dort hervorgehoben werden, wo sie hingehören. Üben Sie Mausrad-Scrollen, Ziehen-Auswählen und Tastatur-Seitennavigation auf jedem Widget-Set aus, das Sie ausliefern, denn Fokusbehandlung und Radverhalten sind die widget-set-abhängigsten Ecken der LCL. Prüfen Sie das Rendering bei 100 %, 150 % und 200 % Anzeige-Skalierung. Starten Sie zuletzt den installierten Build, nicht den IDE-Build, auf einer Maschine, auf der nie die IDE war, denn das ist der einzige Test, der die Binär-Auflösung ehrlich prüft. Alles andere kann durchgehen, während genau dieser still scheitert
Die Rendering-Leistung trägt zwischen den beiden Editionen unverändert hinüber, sodass der Caching-Ansatz aus dem Artikel zu Render-Cache und Zoom-Leistung für den LCL-Viewer genau so gilt wie für den VCL-Viewer
Nichts davon macht die LCL-Edition zu einer geringeren. Die Kernoberfläche ist auf beiden Seiten identisch: TPdf, TPdfView, Rendering, Formulare, Textextraktion und die Barrierefreiheits-APIs verhalten sich gleich, egal welche IDE sie kompiliert hat. Jeder Unterschied, den es zu verfolgen lohnt, ist plattformgebunden, nicht editionsgebunden. SAPI-Sprachausgabe ist Windows-only, Dialoge folgen den Konventionen des jeweiligen Frameworks, und die Binärdatei muss zur Architektur passen, in die sie geladen wird. Bekommen Sie die Kodierungsgrenzen, das Laufzeit-Formular und die Binär-Auflösung richtig, ist der Rest der Portierung die mechanische Arbeit, die der Compiler bereits für Sie erledigt hat
Die hier beschriebenen VCL- und LCL-Editionen werden zusammen als PDFium Component ausgeliefert, mit Quellcode und identischen öffentlichen APIs für Delphi, C++Builder und Lazarus/FPC