Technisch artikel

PDF-viewer voor Lazarus en Free Pascal met PDFium

Delphi en Lazarus compileren dezelfde Object Pascal, en dat oppervlaktegelijkenis is precies wat het overzetten van een viewer ertussen bedrieglijk maakt. De twee toolchains wijken af op drie plaatsen die er voor PDF-werk toe doen: het native string-type is UTF-16 in Delphi en UTF-8 in een LCL-applicatie; VCL en LCL zijn verschillende visuele frameworks met hun eigen controls, dialogen en form-streaming-formaten; en een Delphi-binair richt zich op Windows terwijl een FPC-binair mogelijk op Linux of macOS af gaat. Geen van die verschillen verschijnt op compileertijd. Een viewer gebouwd op PDFium Component, dat VCL- en LCL-edities levert vanuit één bronboom, compileert schoon onder Lazarus na een handvol unit-naam-swaps en een paar {$IFDEF FPC}-blokken. De storingen komen later, wanneer echte data en een echte uitrol de aannames blootleggen die de Delphi-build geruisloos maakte

Vier van die aannames verklaren de meeste verloren tijd: tekstencodering op de UI-grens, de verleiding om twee kopieën van het formulier te onderhouden, de manier waarop een native engine-binair tijdens runtime wordt opgelost, en het moment waarop tekst-naar-spraak zonder platform komt te zitten zodra SAPI verdwijnt. Elk is goedkoop te hanteren als u weet dat het eraan komt en duur om op te sporen als u dat niet doet

Dezelfde Pascal, verschillende string-payloads

Het native string van Delphi is al UTF-16 sinds 2009. Lazarus en Free Pascal standaard op UTF-8 in LCL-applicaties. De tekstgerichte API's van de component spreken UTF-16 via het type WString, dat de FPC-build aliasseert naar WideString, dus elke grens waar tekst tussen uw LCL-UI en de PDF-engine oversteekt is een conversiepunt

De conversies gebeuren automatisch in eenvoudige toewijzingen, en de meeste code hoeft er nooit over na te denken. Twee gewoontes houden de encoderings-bugs eruit. Voer tekst recht door zonder manipulatie op byteniveau: code die een zoekterm op byte-offset snijdt werkt in Delphi, waar één Char één UTF-16-eenheid is, en corrumpeert multi-byte UTF-8 in de LCL. En test vanaf de eerste run met niet-ASCII-data. Een Duitse bestandsnaam, een Cyrillische zoekterm, een geaccentueerde auteursnaam in de documentmetadata: zuiver ASCII-testdata verbergt elk encoderingsdefect, want ASCII is het enige bereik waar UTF-8 en UTF-16 byte-voor-teken overeenkomen. De bug is de hele tijd echt; ASCII houdt hem alleen onzichtbaar tot een klant in München een bestand opent dat u nooit probeerde

Diagram van UTF-16- en UTF-8-stringconversiegrenzen tussen een LCL viewer-UI en de PDFium-component in Lazarus
De tekst-API's van de component spreken UTF-16 via WString, dus een LCL-UI met UTF-8-strings treft een conversiepunt bij elke grens, en byte-offset-sneden of ASCII-only-testdata zijn waar de encoding-bugs zich verbergen

Eén conditioneel blok, geen fork per IDE

Na de eerste dozijn IFDEFs begint de codebase aan te voelen als twee projecten in één repository, en haar forken per IDE ziet er verleidelijk uit. Het is de verkeerde zet. De werkelijke verschillen krimpen ineen tot één gedeeld declaratieblok, en een fork verdubbelt de kosten van elke bugfix vanaf dat moment. Houd de conditionele laag zo klein:

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

type
  WString = WideString;   // de tekst-API's van het component zijn UTF-16
  TBytes  = array of Byte;
{$ELSE}
uses
  Winapi.Windows, Vcl.Forms, Vcl.Graphics, Vcl.Controls;
{$ENDIF}

Alles onder dat blok compileert identiek in beide IDE's. Documentverwerking, paginanavigatie, rendering-aanroepen: TPdf en TPdfView tonen hetzelfde oppervlak in de VCL- en LCL-edities, dus het gros van de viewer ziet nooit een compilerconditie. Dat zo houden is een structurele discipline eerder dan een slimme truc. Gedeelde PDF-logica leeft in units die geen framework-specifieke dialogen of panelen binnenhalen. De handvol dingen die echt verschillen, zoals afdrukdialogen en bestandskiezers met hun platformconventies, verbergen zich achter een dunne interface die één keer per framework wordt geïmplementeerd. Het IFDEF-blok wordt de enige plaats waar toekomstige platformdivergentie nog mag landen, in plaats van compilerdirectieven over veertig units te laten lekken

Bouw het formulier in code, niet in twee designers

Form-streaming is waar dual-IDE-projecten geruisloos wegrotten. Een .dfm en een .lfm die beweren hetzelfde formulier te beschrijven drijven eigenschap voor eigenschap uit elkaar tot de twee builds zich om redenen verschillend gedragen die niemand kan diffen, omdat de twee bestanden niet eens hetzelfde formaat hebben. De viewer tijdens runtime construeren omzeilt het hele probleem. Er is één constructorsequentie, onder versiebeheer als gewone code, en hij leest hetzelfde op beide platforms:

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;   // opent het document; PageCount is hierna geldig
  end;
end;

De exacte volgorde van die toewijzingen doet er minder toe dan de regel die het echte werk doet. PdfView.Pdf := Pdf bindt het visuele control aan de documentcomponent, en vanaf dat punt reageert paginanavigatie via PageNumber en fit-gedrag via FitMode identiek onder VCL en LCL. Eén cross-framework-eigenaardigheid is de moeite waard om te kennen voordat een gebruiker hem als bug meldt: Zoom met de hand toewijzen klapt FitMode terug op pfmNone op beide frameworks. Dus als uw werkbalk "fit width" als een kleefvoorkeur behandelt, moet u de fit-modus na elke programmatische zoom opnieuw toewijzen, anders stopt de voorkeur geruisloos met kleven de eerste keer dat code het zoomniveau aanraakt

De binair waar de IDE u nooit voor waarschuwde

De component wikkelt de PDFium-engine, die levert als een native platformbinair, en die binair is de bron van nagenoeg elk "werkt in de IDE, faalt vanuit de geïnstalleerde snelkoppeling"-rapport. Drie regels verklaren de meeste ervan. Bitness moet exact overeenkomen. Een 32-bit uitvoerbaar bestand kan geen 64-bit pdfium-bibliotheek laden, en het bericht dat de OS teruggeeft ("module not found" op sommige Windows-versies) misleidt actief, want het bestand staat gewoon daar naast het uitvoerbare bestand. Los het bibliotheekpad op ten opzichte van het uitvoerbare bestand, nooit de werkdirectory; een IDE-start en een shell-start verschillen precies op dat punt, vandaar dat de bug zich tijdens ontwikkeling verbergt. En vang een mislukte load op voordat het eerste document opent, en meld hem dan met het verwachte pad en de architectuur uitgespeld. Een supportticket dat leest "PDFium 64-bit binary missing at <path>" sluit in minuten. Eentje dat leest "viewer crashes on startup" wordt een week heen-en-weer

Versieer de engine-binair naast het uitvoerbare bestand terwijl u toch bezig bent. PDFium beweegt snel, en een installer die de applicatie bijwerkt maar een verouderde bibliotheek op schijf laat staan produceert crashes die niemand op uw kantoor kan reproduceren, om de simpele reden dat elke machine op uw kantoor toevallig het bijbehorende paar vasthoudt. Behandel de bibliotheek als onderdeel van het build-artifact, met dezelfde installer, dezelfde versiestempel en hetzelfde rollback-pad als het uitvoerbare bestand dat hij laadt

Diagram van de drie PDFium native binary-laadregels voor een Lazarus- of Delphi-PDF-viewer-uitvoerbaar bestand
Drie regels dekken de meeste werkt-in-de-IDE-faalt-geïnstalleerd-rapporten: het uitvoerbare bestand en de PDFium-bibliotheek moeten één bitness delen, het bibliotheekpad lost op vanuit het uitvoerbare bestand in plaats van de werkmap, en een mislukte laadactie wordt gevangen met het verwachte pad uitgespeld

Componenten registreren in de Lazarus IDE

Runtime-constructie heeft helemaal geen registratie op ontwerptijd nodig, wat de schoonste opzet is voor een viewer die zijn eigen UI in code bouwt. Wanneer u de componenten wel op het Lazarus-palet wilt voor ontwerptijdwerk, installeer dan het pakket en laat zijn toegewijde registratie-unit, PDFiumLazReg in Lib/FPC/PDFiumLaz.lpk, het afhandelen. Die unit is opzettelijk als ontwerptijd gemarkeerd: hij verwijst naar IDE-property-editor-interfaces die nooit in uw uit te leveren uitvoerbare bestand mogen linken

Krijg dit verkeerd en het symptoom is een applicatie die onverklaarbaar op IDE-pakketten leunt, wat opduikt als een uitrolfalen op de eerste klantmachine waar Lazarus nooit op geïnstalleerd is

Spraak en schermlezers buiten Windows

Tekst-naar-spraak is het enige feature waar het cross-platform-verhaal breekt, en het breekt op het besturingssysteem, niet op de component. SAPI, de gebruikelijke TTS-backend op Windows, bestaat alleen op Windows. Een Lazarus-build die nog steeds op Windows richt behoudt volledige SAPI-uitvoer en hetzelfde NVDA-compatibele gedrag dat het Delphi-origineel had, dus een Windows-naar-Windows-port verliest hier niets, en een NVDA-gebruiker kan de twee builds niet uit elkaar halen

Een Linux- of macOS-doel is een ander verhaal. Er is geen SAPI om aan te roepen, dus de audio-uitvoer moet worden omgeleid naar een native spraakservice terwijl de lees-API's erboven op hun plaats blijven. Die splitsing is het argument om spraak vanaf de eerste commit achter een interface te zetten: de leesvolgorde-analyse en de woord-volgcursor zijn platformneutraal en dragen ongewijzigd over, en alleen de dunne laag die werkelijk geluid produceert moet per platform veranderen. Het artikel over de toegankelijke lezer behandelt die leesmachinerie in diepte

Een pariteitschecklist voordat u de port klaar noemt

De volgende ronde heeft echte regressies gevangen, ruwweg in de volgorde waarin de storingen de neiging hebben op te duiken. Open een document waarvan het pad niet-ASCII-tekens bevat. Zoek naar een term met niet-ASCII-tekens en bevestig dat de treffers markeren waar ze horen. Oefen muiswiel-scroll, sleep-selectie en paginanavigatie via het toetsenbord op elke widgetset die u levert, want focushandhaving en wielgedrag zijn de meest widgetset-afhankelijke hoeken van de LCL. Controleer rendering op 100%, 150% en 200% weergaveschaling. Draai tot slot de geïnstalleerde build, niet de IDE-build, op een machine waar de IDE nooit op gestaan heeft, want dat is de enige test die binaire resolutie eerlijk oefent. Al het andere kan slagen terwijl die ene geruisloos faalt

Rendering-doorvoer draagt ongewijzigd over tussen de twee edities, dus de caching-aanpak uit het artikel over rendercache en zoomprestaties is op de LCL-viewer precies zo van toepassing als geschreven voor de VCL-versie

Niets hiervan maakt de LCL-editie een mindere. Het kernoppervlak is identiek aan beide kanten: TPdf, TPdfView, rendering, formulieren, tekstextractie en de toegankelijkheids-API's gedragen zich hetzelfde ongeacht welke IDE ze compileerde. Elk verschil dat de moeite waard is om bij te houden is platformgebonden eerder dan editiegebonden. SAPI-spraak is Windows-only, dialogen volgen de conventies van elk framework, en de binair moet overeenkomen met de architectuur waarin hij wordt geladen. Krijg de encoderingsgrenzen, het runtime-formulier en de binaire resolutie goed, en de rest van de port is het mechanische werk dat de compiler al voor u heeft afgehandeld

PDFium Component-diagram van de spraakinterface-splitsing die TTS-uitvoer verplaatst achter een per-platform engine in een Lazarus PDF-viewer
Leesvolgorde-analyse en de woordvolg-cursor blijven platformneutraal terwijl één dunne spraakinterface oplost naar SAPI op Windows en naar native spraakdiensten op Linux en macOS

De VCL- en LCL-edities die hier worden beschreven leveren samen als PDFium Component, met broncode en identieke publieke API's voor Delphi, C++Builder en Lazarus/FPC