Artykuł techniczny

HotPDF na Free Pascal i Lazarus: granice Win64

Krótka odpowiedź na tamto zgłoszenie wsparcia brzmi tak, z ograniczeniami. HotPDF 2.730.0 buduje się na Free Pascal 3.2.2 i Lazarus 4.6 dla Win64, a rdzeniowe ścieżki tworzenia, wczytywania i zapisu działają. Nie dochodzi do tego nic, co opiera się na statycznie linkowanym natywnym obiekcie kodeka albo na anonimowych metodach Delphi

Pytanie zwykle przychodzi tak samo: zespół standaryzuje się na Lazarusie dla narzędzia międzyplatformowego albo dziedziczy codebase Free Pascal i chce tego samego komponentu PDF, który już licencjonuje dla Delphi. Portowanie dojrzałej biblioteki Delphi rzadko sprowadza się do składni. Ciekawa część to to, co port obnaża o miejscach, gdzie biblioteka po cichu była sklejona z jednym toolchainem, a w tym przypadku sklejenie siedzi w dwóch bardzo konkretnych miejscach: ABI plików obiektowych dołączonych kodeków i funkcje kompilatora ukryte za symbolem wersji

Matryca możliwości porównująca build HotPDF Delphi z buildem Free Pascal 3.2.2 i Lazarus 4.6 Win64, pokazująca, które ścieżki dokumentowe są współdzielone, a które API kodeków, kompresji, renderowania równoległego i metod anonimowych trafia w rzucający wyjątek stub
Rdzeniowe ścieżki tworzenia, wczytywania i zapisu są identyczne w obu buildach, a luka siedzi w całości w statycznie linkowanych kodekach i API metod anonimowych

Czego Free Pascal 3.2.2 potrzebuje, zanim HPDFDoc się skompiluje

HotPDF kompiluje się pod Free Pascal wyłącznie w trybie Delphi i tylko wtedy, gdy katalogi jednostek LCL Lazarusa są na ścieżce wyszukiwania. Żaden z tych warunków nie podlega negocjacji. HotPDF.inc przełącza kompilator przez {$MODE DELPHI} i {$H+} wewnątrz swojego bloku {$IFDEF FPC} i odrzuca cokolwiek starszego {$FATAL}-iem, gdy FPC_FULLVERSION jest poniżej 30202, więc instalacja 3.0.x zawodzi głośno, zamiast wyprodukować zepsutą jednostkę. Pakiet runtime Lazarusa HotPDFLaz.lpk koduje resztę: LCL jako wymagany pakiet i -Mdelphi jako opcję własną

Wymóg LCL zaskakuje ludzi, którzy chcą tylko wyjścia konsolowego, ale jest strukturalny. HPDFFPCCompat dostarcza typy VCL Delphi, na które Free Pascal nie ma odpowiednika, mapując TMetafile i TMetafileCanvas na klasy bitmapy i canvas LCL oraz aliasując TRichEdit do TMemo, podczas gdy HPDFDoc aliasuje TPNGObject do Graphics.TPortableNetworkGraphic. Traktuj je jak kliny kompilacji, nie parzystość funkcji: klasa metafile oparta na bitmapie trzyma jednostkę w kompilacji, nie sprawia, że ścieżki metafile zachowują się jak na Delphi. Nawet nietestujący GUI smoke test ciągnie Interfaces, a skrypt budowania podaje -Fu dla lcl\units\x86_64-win64 i katalogu wyjściowego lazutils

Dlaczego D2009+ nie może służyć za bramę wersji

Kusi, by traktować build Free Pascal jako nowoczesny kompilator i po prostu zdefiniować najnowszy symbol funkcji Delphi. HotPDF tego nie robi, a powód warto powiedzieć wprost: D2009+ nie oznacza samych napisów Unicode, bramkuje też jednostki, których publiczne API jest wyrażone metodami anonimowymi. Free Pascal 3.2.2 nie wspiera ani anonimowych metod Delphi, ani tamtych API, więc pożyczenie symbolu wciągnęłoby kod, który się nie skompiluje. Klauzula uses HPDFDoc niesie więc dwa osobne warunkowe ogony, a ich nakładanie się jest celowe, nie przypadkowe

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

Dlaczego natywne kodeki zatrzymują się na linkierze?

Bo to obiekty Win64 COFF wyemitowane przez jeden konkretny toolchain, a żaden linker Free Pascal na Win64 ich nie przyjmie: ani wewnętrzny linker, ani zewnętrzna ścieżka GNU ld. To problem ABI plików obiektowych, nie problem Pascala, i żadna ilość warunkowego kodu źródłowego tego nie naprawi. Biblioteka wybiera jedyną dostępną uczciwą drogę. Każda dyrektywa {$L} ciągnąca statyczny obiekt kodeka jest owinięta w {$IFNDEF FPC}, więc build Free Pascal po prostu je pomija, a HPDFFPCCodecStubs dostarcza potem każdy brakujący symbol zewnętrzny jako stub, który rzuca wyjątek zamiast zwracać

// 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 tabela stubów jest długa, a jej lektura mówi ci dokładnie, które możliwości są dziś tylko Delphi: punkty wejścia deflate z zlib-ng i zopfli, kompresja i dekompresja libjpeg, kodek JPEG 2000 OpenJPEG, libtiff z jego inicjalizatorami per kompresja, kodowanie i dekodowanie JBIG2, punkty wejścia transformacji koloru Little-CMS i prymitywy AES. Wybór projektowy za stubami waży więcej niż lista. Brakujący symbol w czasie linkowania daje ci ścianę niezdefiniowanych referencji z jednostki, której nigdy nie dotknąłeś; stub rzucający ENotSupportedException daje build, który działa, komunikat nazywający powód i stack trace wskazujący miejsce wywołania. Oznacza to też, że build Free Pascal nigdy po cichu nie wyprodukuje złych bajtów tam, gdzie build Delphi wyprodukowałby poprawne. Zauważ też efekt drugiego rzędu: uruchamianie niezaufanych kodeków obrazów w izolowanym procesie to decyzja, która pojawia się wyłącznie w buildzie Delphi, bo build Free Pascal nie ma w ogóle natywnego dekodera w procesie, którego można by izolować

Na Delphi statyczne obiekty kodeków HotPDF się linkują i działają natywnie, podczas gdy build Free Pascal Win64 pomija dyrektywy linkowania i kieruje każdy brakujący symbol zewnętrzny do stuba rzucającego nazwany wyjątek w miejscu wywołania
Pominięcie dyrektyw linkowania i zastubowanie każdego symbolu zewnętrznego zamienia ścianę niezdefiniowanych referencji w build, który działa i nazywa własne granice

Kompresja: pierwszą linią do zmiany jest cmNone

Zanim portujesz cokolwiek innego, ustaw Compression na cmNone. THPDFCompressionMethod oferuje dokładnie dwie wartości, cmNone i cmFlateDecode, a druga prowadzi prosto do punktów wejścia deflate, które w buildzie Free Pascal są stubami. Zweryfikuj najpierw rdzeniowy model obiektowy z wyłączoną kompresją, potem zdecyduj, czego jeszcze potrzebujesz. Taka jest kolejność w dołączonym smoke teście: utwórz jednostronicowy dokument bez kompresji, wczytaj go ponownie i stwierdź, że liczba stron wróciła jako jeden. Wyjście bez kompresji jest większe i nadal jest w pełni poprawnym 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 trafia w zasubowany 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 się dzieje z równoległym renderowaniem stron?

Nadal się kompiluje, nadal zwraca poprawne bitmapy i przestaje być równoległe. THotPDF.RenderLoadedPagesParallel i THotPDF.RenderLoadedPagesParallelOrdered są zbudowane na TThread.CreateAnonymousThread z inline zamknięciem procedure, którego Free Pascal 3.2.2 nie umie wyrazić, więc gałąź Free Pascal uruchamia deterministyczny awaryjny przebieg szeregowy: przechodzi indeksy stron po kolei, wywołuje RenderLoadedPageToBitmap dla każdej i liczy sukcesy. Kształt API, wartość zwracana i tablica wyjściowa są niezmienione, co pozwala jednej codebase budować się na oba sposoby

To samo równoległe wywołanie renderowania HotPDF działa na nakładających się wątkach roboczych pod Delphi i przechodzi indeksy stron szeregowo pod Free Pascal, przy czym rekord info potoku zgłasza liczbę workerów jeden, zamiast ukrywać awaryjny tryb
Gałąź Free Pascal zachowuje kształt API i tablicę wyjściową, zgłaszając liczbę workerów jeden, więc kod już czytający rekord info widzi prawdę
var
  Bitmaps: THPDFBitmapArray;
  Info: THPDFParallelRenderPipelineInfo;
  Rendered: Integer;
begin
  Rendered := Pdf.RenderLoadedPagesParallel([0, 1, 2, 3], 150, 4,
    Bitmaps, Info);
  // Delphi: Info.WorkerCount to cokolwiek pozwolił budżet pamięci
  // Free Pascal: Info.WorkerCount zawsze 1, strony w kolejności indeksów
  if Info.WorkerCount = 1 then
    LogSerialFallback(Rendered, Info.RequestedWorkerCount);

Awaryjny tryb nie jest cichy i to jest część warta projektowania wokół niej. Wypełnia THPDFParallelRenderPipelineInfo uczciwie: PageCount z żądania, RequestedWorkerCount powtarzający, o co prosiłeś, WorkerCount ustawiony na 1 oraz liczniki ukończonych i dostarczonych zgadzające się z tym, co faktycznie wróciło. Kod, który już ogląda Info, by wyskalować pasek postępu albo budżet pamięci, dalej działa i czyta prawdę, a nie założenie. Jeśli twój plan przepustowości zależy od potoku renderowania równoległego i jego modelu backpressure, ten plan jest planem Delphi; na Free Pascal zaplanuj jednowątkowy koszt renderowania strony do bitmapy pomnożony przez liczbę stron

Który build faktycznie wysłać w świat?

Wybieraj po możliwościach, nie po preferencjach. Jeśli twój przepływ pracy to składanie dokumentów, tekst i rysunek wektorowy, wypełnianie formularzy, wczytywanie i zapis, build Free Pascal na Win64 to pokrywa, a walidację powinieneś zrobić z wyłączoną kompresją, zanim cokolwiek włączysz. Jeśli obejmuje obrazy JPEG, JPEG 2000, TIFF albo JBIG2, transformacje koloru ICC, skompresowane wyjście albo przepustowość zależną od wielu rdzeni, zostań na razie przy Delphi albo C++Builder. Granicę wyznacza ABI plików obiektowych i brakująca funkcja języka, obie widoczne w źródłach, a nie zakopane w macierzy wsparcia, i obie zawodzą nazwanym błędem, a nie złym wynikiem

Pakiet Free Pascal i Lazarus przychodzi w tej samej dystrybucji co jednostki Delphi i C++Builder, więc licencja obejmuje obie i możesz przetestować ścieżkę Lazarusa na własnych dokumentach, zanim się na nią zdecydujesz; strona produktu HotPDF Delphi PDF Component niesie bieżącą macierz wsparcia kompilatorów i pełną referencję API