Technický článek

HotPDF ve Free Pascal a Lazarus: limity podpory Win64

Krátká odpověď na ten support ticket zní ano, s limity. HotPDF 2.730.0 staví na Free Pascal 3.2.2 a Lazarus 4.6 pro Win64 a cesty jádra — vytvoření, načtení a uložení — fungují. Co nepatří mezi ně, je všechno, co spočívá na staticky linkovaném nativním objektu kodeku nebo na anonymních metodách Delphi

Dotaz obvykle přichází stejně: tým standardizuje na Lazarus kvůli multiplatformnímu nástroji, nebo zdědí codebase Free Pascal a chce tutéž PDF komponentu, kterou už licence pro Delphi. Přenos vyzrálé knihovny Delphi je zřídka otázkou syntaxe. Zajímavé je, co přenos prozradí o tom, kde se knihovna tiše svázala s jedním toolchainem, a v tomto případě to svázání sedí na dvou velmi konkrétních místech: objektovém ABI přibalených kodeků a kompilátorových funkcích ukrytých za verzovým symbolem

Matice schopností porovnávající build HotPDF pro Delphi s buildem Free Pascal 3.2.2 a Lazarus 4.6 pro Win64, ukazující, které cesty dokumentů jsou sdílené a které API kodeků, komprese, paralelního renderování a anonymních metod končí u vyhazujícího stubu
Cesty jádra — vytvoření, načtení, uložení — jsou na obou buildech identické a mezera sedí celá ve staticky linkovaných kodecích a API anonymních metod

Co Free Pascal 3.2.2 potřebuje, než HPDFDoc zkompiluje

HotPDF kompiluje pod Free Pascal jen v režimu Delphi a jen když jsou adresáře unit Lazarus LCL na hledací cestě. Ani jedno se nepřipouští k jednání. HotPDF.inc přepne kompilátor pomocí {$MODE DELPHI} a {$H+} uvnitř bloku {$IFDEF FPC} a čímkoli starším odmítá s {$FATAL}, když je FPC_FULLVERSION pod 30202, takže instalace 3.0.x selže nahlas místo vyrobení rozbité unity. Lazarus runtime balíček HotPDFLaz.lpk zakóduje zbytek: LCL jako vyžadovaný balíček a -Mdelphi jako vlastní volbu

Požadavek LCL překvapí lidi, kteří chtějí jen konzolový výstup, ale je strukturální. HPDFFPCCompat dodává typy Delphi VCL, pro něž Free Pascal nemá ekvivalent, mapuje TMetafile a TMetafileCanvas na bitmapové a canvas třídy LCL a aliashuje TRichEdit na TMemo, zatímco HPDFDoc aliashuje TPNGObject na Graphics.TPortableNetworkGraphic. Berte to jako kompilační shims, ne jako paritu funkcí: metafile třída opřená o bitmapu udrží unitu v kompilaci, nedělá z cest metafile chování, jaké mají na Delphi. I test bez GUI si táhne Interfaces a build skript podává -Fu pro lcl\units\x86_64-win64 a výstupní adresář lazutils

Proč D2009+ nemůže sloužit zároveň jako verzová brána

Je lákavé brát build Free Pascal jako moderní kompilátor a prostě definovat nejnovější symbol funkcí Delphi. HotPDF to nedělá a důvod stojí za vyslovení nahlas: D2009+ neznamená samotné Unicode řetězce, zároveň braní unity, jejichž veřejné API je vyjádřeno anonymními metodami. Free Pascal 3.2.2 nepodporuje anonymní metody Delphi ani tato API, takže vypůjčení symbolu by vleklo kód, který se nezkompiluje. Uses klauzule HPDFDoc proto nese dvě oddělené podmíněné patky a jejich překryv je záměrný, ne náhodný

uses
  // ...
  HPDFJavaScript,
  HPDFFormCalcGraph
{$IFDEF FPC}
  , HPDFFPCCodecStubs,
  HPDFCMS,
  HPDFWinCertSigner
{$ENDIF}
{$IFDEF D2009+}
  , HPDFXFARuntime,
  HPDFCMS,
  HPDFWinCertSigner,
  HPDFSignVerify,
  HPDFSignatureBatch
{$ENDIF};

Proč se nativní kodeky zastaví u linkeru?

Protože jsou to Win64 COFF objekty emitované jedním konkrétním toolchainem a ani jeden z obou Free Pascal linkerů na Win64 je nespolkne: ani interní linker, ani externí cesta GNU ld. To je problém ABI objektových souborů, ne problém Pascalu, a žádná hromada podmíněného zdrojáku to neopraví. Knihovna jde jedinou poctivou cestou, která zbyla. Každá direktiva {$L}, která vtahuje statický objekt kodeku, je obalena v {$IFNDEF FPC}, takže build Free Pascal je prostě vynechá, a HPDFFPCCodecStubs pak dodá každý chybějící externí symbol jako stub, který vyhazuje místo vracení

// HPDFFPCCodecStubs.pas
function HPDFFPCNativeCodecUnavailable: PtrUInt;
begin
  raise ENotSupportedException.Create(
    'This native codec is not available in the Free Pascal build');
end;

function HPDFFPCStub_deflate: PtrUInt; cdecl;
  public name 'deflate';
begin
  Result := HPDFFPCNativeCodecUnavailable;
end;

Ta tabulka stubů je dlouhá a čtení z ní vyčtete přesně, které schopnosti jsou dnes jen Delphi: vstupní body deflate zlib-ng a zopfli, komprese a dekomprese libjpeg, kodek OpenJPEG JPEG 2000, libtiff a jeho inicializátory na kompresi, kódování i dekódování JBIG2, vstupní body barevné transformace Little-CMS a primitivy AES. Volba designu za stuby má větší váhu než seznam. Chybějící symbol při linkování vám dává zeň nedefinovaných referencí z unity, které jste nikdy nesáhli; stub, který vyhazuje ENotSupportedException, vám dává build, který běží, hlášení pojmenovávající důvod a stack trace ukazující na místo volání. Znamená to také, že build Free Pascal nikdy tiše nevyrobí špatné bajty tam, kde by build Delphi vyrobil správné. Všimněte si také efektu druhého řádu: spouštění nedůvěryhodných obrazových kodeků v izolovaném procesu je rozhodnutí, které se rodí jen na buildu Delphi, protože build Free Pascal nemá v procesu žádný nativní dekodér, který by šlo sandboxovat

Na Delphi se statické objekty kodeků HotPDF linkují a běží nativně, zatímco build Free Pascal pro Win64 přeskočí direktivy linkování a směruje každý chybějící externí symbol na stub, který na místě volání vyhazuje pojmenovanou výjimku
Přeskočení direktiv linkování a stubování každého externího symbolu promění zeň nedefinovaných referencí v build, který běží a pojmenuje vlastní limity

Komprese: první řádek, který změnit, je cmNone

Ještě před přenosem čehokoli jiného nastavte Compression na cmNone. THPDFCompressionMethod nabízí přesně dvě hodnoty, cmNone a cmFlateDecode, a ta druhá míří rovnou do vstupních bodů deflate, které jsou ve buildu Free Pascal stuby. Nejdřív ověřte jádro objektového modelu s kompresí vypnutou, pak rozhodněte, co dalšího potřebujete. Toto je pořadí, které používá dodaný smoke test: vytvoří jednostránkový nekomprimovaný dokument, znovu ho načte a tvrdí, že počet stránek vrátil jedničku. Nekomprimovaný výstup je větší a je to stále naprosto validní PDF

program HotPDFLazarusSmoke;

{$mode delphi}
{$H+}

uses
  Interfaces, SysUtils, HPDFDoc;

var
  Pdf, Reloaded: THotPDF;
  OutputFile: string;
  PageCount: Integer;
begin
  OutputFile := IncludeTrailingPathDelimiter(GetTempDir) +
    'HotPDF-FPC-Smoke.pdf';
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := OutputFile;
    Pdf.Compression := cmNone;   // cmFlateDecode sahá na stubovaný symbol
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(72, 72, 0, 'HotPDF Free Pascal smoke test');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;

  Reloaded := THotPDF.Create(nil);
  try
    PageCount := Reloaded.LoadFromFile(OutputFile);
    if PageCount <> 1 then
      raise Exception.CreateFmt('Expected one page, got %d', [PageCount]);
  finally
    Reloaded.Free;
  end;
end.

Co se stane s paralelním renderováním stránek?

Zkompiluje se dál, vrací dál správné bitmapy a přestane být paralelní. THotPDF.RenderLoadedPagesParallel a THotPDF.RenderLoadedPagesParallelOrdered staví na TThread.CreateAnonymousThread s inline uzávěrou procedure, kterou Free Pascal 3.2.2 nedokáže vyjádřit, takže větev Free Pascal běží deterministickým sériovým fallbackem: projde indexy stránek v pořadí, pro každý zavolá RenderLoadedPageToBitmap a počítá úspěchy. Tvar API, návratová hodnota i výstupní pole zůstávají beze změny, což dovolí jediné codebase stavět oběma směry

Týž volání paralelního renderu HotPDF běží na překrývajících se pracovních vláknech pod Delphi a pod Free Pascal prochází indexy stránek sériově, přičemž info záznam pipeline hlásí počet pracovníků jedna místo skrývání fallbacku
Větev Free Pascal drží tvar API i výstupní pole a zároveň hlásí počet pracovníků jedna, takže kód, který info záznam už čte, vidí pravdu
var
  Bitmaps: THPDFBitmapArray;
  Info: THPDFParallelRenderPipelineInfo;
  Rendered: Integer;
begin
  Rendered := Pdf.RenderLoadedPagesParallel([0, 1, 2, 3], 150, 4,
    Bitmaps, Info);
  // Delphi: Info.WorkerCount je, co dovolil paměťový rozpočet
  // Free Pascal: Info.WorkerCount je vždy 1, stránky v pořadí indexů
  if Info.WorkerCount = 1 then
    LogSerialFallback(Rendered, Info.RequestedWorkerCount);

Fallback není tichý a právě to je kus, který stojí za navržení okolo. Vyplňuje THPDFParallelRenderPipelineInfo poctivě: PageCount z požadavku, RequestedWorkerCount odrážející, o co jste požádali, WorkerCount nastavený na 1 a počty dokončených a doručených odpovídající tomu, co skutečně dorazilo. Kód, který už prohlíží Info, aby rozměrnil progress bar nebo paměťový rozpočet, funguje dál a čte pravdu místo předpokladu. Pokud váš plán propustnosti závisí na paralelní renderovací pipeline a jejím modelu backpressure, je ten plán plán pro Delphi; na Free Pascal rozpočítejte jednovláknovou cenu renderování stránky do bitmapy krát počet stránek

Který build skutečně expedovat?

Vybírejte podle schopností, ne podle preference. Pokud vaše workflow je skládání dokumentů, kreslení textu a vektorů, naplňování formulářů, načítání a ukládání, pokryje to build Free Pascal na Win64 a měli byste validovat s kompresí vypnutou, než cokoli zapnete. Pokud to zahrnuje obrázky JPEG, JPEG 2000, TIFF nebo JBIG2, barevné transformace ICC, komprimovaný výstup nebo propustnost závislou na mnoha jádrech, zůstaňte zatím na Delphi nebo C++Builder. Hranice je vytyčena ABI objektových souborů a chybějící jazykovou funkcí, obojí je vidět ve zdrojáku místo pohřbení v support matici a obojí selhává pojmenovanou chybou místo špatného výsledku

Balíček Free Pascal a Lazarus putuje ve stejném rozdělení jako unity Delphi a C++Builder, takže licence pokrývá obojí a Lazarus cestu můžete testovat proti vlastním dokumentům, než se ji zavážete; produktová stránka HotPDF Delphi PDF Component nese aktuální matici podpory kompilátorů a kompletní referenci API