Техническа статия

HotPDF на Free Pascal и Lazarus: границите на Win64

Краткият отговор на този тикет е да, с ограничения. HotPDF 2.730.0 се гради с Free Pascal 3.2.2 и Lazarus 4.6 за Win64, а основните пътеки за създаване, зареждане и запис работят. Това, което не следва, е всичко, което почива на статично линкнат нативен кодек обект или на анонимни методи на Delphi

Въпросът обикновено пристига по един и същ начин: екип стандартизира върху Lazarus за крос-платформен инструмент или наследява кодова база на Free Pascal и иска същия PDF компонент, който вече лицензира за Delphi. Преносът на зряла Delphi библиотека рядко е въпрос на синтаксис. Интересното е какво преносът разкрива за местата, където библиотеката тихо е била свързана с една инструментална верига, а в този случай свързаността седи на две съвсем конкретни места: object-file ABI на вградените кодеци и компилаторните възможности, скрити зад символ за версия

Матрица на възможностите, сравняваща Delphi build на HotPDF с build за Free Pascal 3.2.2 и Lazarus 4.6 Win64, показваща кои пътеки за документи са споделени и кои API за кодеци, компресия, паралелно рендериране и анонимни методи стигат до вдигащ изключение stub
Основните пътеки за създаване, зареждане и запис са идентични и в двата build, а пропускът седи изцяло в статично линкнатите кодеци и API с анонимни методи

Какво е нужно на Free Pascal 3.2.2, преди HPDFDoc да се компилира

HotPDF се компилира под Free Pascal само в Delphi режим и само когато директориите с units на Lazarus LCL са в search path. Нито едното не подлежи на договаряне. HotPDF.inc превключва компилатора с {$MODE DELPHI} и {$H+} вътре в блока си {$IFDEF FPC} и отказва каквото и да е по-старо с {$FATAL}, когато FPC_FULLVERSION е под 30202, така че инсталация 3.0.x се проваля на глас, вместо да роди счупена unit. Пакетът по време на изпълнение за Lazarus HotPDFLaz.lpk кодира останалото: LCL като изискван пакет и -Mdelphi като собствена опция

Изискването за LCL изненадва хора, които искат само конзолен изход, но то е структурно. HPDFFPCCompat снабдява Delphi VCL типовете, за които Free Pascal няма еквивалент, като съпоставя TMetafile и TMetafileCanvas на LCL класове за bitmap и canvas, а TRichEdit прави псевдоним на TMemo, докато HPDFDoc дава на TPNGObject псевдонима Graphics.TPortableNetworkGraphic. Третирайте ги като compile-time шимове, а не като паритет на възможностите: metafile клас, подпрян на bitmap, държи unit в компилация, но не кара metafile пътищата да се държат както на Delphi. Дори не-GUI smoke тестът дърпа Interfaces, а build скриптът подава -Fu за lcl\units\x86_64-win64 и изходната директория на lazutils

Защо D2009+ не може да служи и като ключ за версията

Изкушаващо е да третирате build за Free Pascal като модерен компилатор и просто да дефинирате най-новия символ за възможности на Delphi. HotPDF не го прави, а причината си заслужава да се каже направо: D2009+ не означава само Unicode низове, той е и ключ към units, чиито публични API са изразени с анонимни методи. Free Pascal 3.2.2 не поддържа нито Delphi анонимни методи, нито тези API, така че заемането на символа би дръпнало код, който не може да се компилира. Затова uses клаузата на HPDFDoc носи два отделни условни опашъка, а припокриването между тях е умишлено, а не случайно

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

Защо нативните кодеци спират при линкера?

Защото те са Win64 COFF обекти, произведени от една конкретна инструментална верига, и нито един от двата Free Pascal линкера на Win64 няма да ги погълне: нито вътрешният линкер, нито външният път с GNU ld. Това е object-file ABI проблем, не Pascal проблем, и никакво количество условен сорс не го оправя. Библиотеката поема единствения честен наличен път. Всяка директива {$L}, която дръпва статичен кодек обект, е увита в {$IFNDEF FPC}, така че build за Free Pascal просто ги пропуска, а HPDFFPCCodecStubs после доставя всеки липсващ външен символ като stub, който вдига изключение вместо да върне

// 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;

Таблицата със stub-ове е дълга, а четенето ѝ ви казва точно кои възможности са Delphi-only днес: entry point-ите за deflate на zlib-ng и zopfli, компресия и декомпресия libjpeg, кодека OpenJPEG JPEG 2000, libtiff с неговите инициализатори за всяка компресия, JBIG2 encode и decode, entry point-ите за цветова трансформация на Little-CMS и AES примитивите. Проектното решение зад stub-овете тежи повече от списъка. Липсващ символ по време на линкване ви дава стена от undefined references от unit, която никога не сте пипали; stub, който вдига ENotSupportedException, ви дава build, който работи, съобщение, което назовава причината, и stack trace, сочещ към мястото на извикването. Това означава също, че build за Free Pascal никога не произвежда тихо грешни байтове там, където Delphi build би дал верни. Забележете и ефекта от втора редица: пускането на недоверени image кодеци в изолиран процес е решение, което възниква само при Delphi build, защото Free Pascal build изобщо няма in-process нативен декодер, който да огради в sandbox

При Delphi статичните кодек обекти на HotPDF се линкват и работят нативно, докато build за Free Pascal Win64 пропуска линк директивите и насочва всеки липсващ външен символ към stub, който вдига именувано изключение на мястото на извикването
Пропускането на линк директивите и подмяната на всеки външен символ със stub превръща стена от undefined references в build, който работи и сам назовава границите си

Компресия: първият ред за промяна е cmNone

Преди да пренесете каквото и да е друго, задайте Compression на cmNone. THPDFCompressionMethod предлага точно две стойности, cmNone и cmFlateDecode, а втората води право към entry point-ите на deflate, които са stub-ове в Free Pascal build. Проверете първо основния обектен модел с изключена компресия, после решете какво друго ви трябва. Това е редът, който доставеният smoke тест използва: създайте едностаничен некомпресиран документ, заредете го отново и assert-нете, че броят страници се е върнал като едно. Некомпресираният изход е по-голям и си остава съвсем валиден 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 стига до подменен със stub символ
    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.

Какво става с паралелното рендериране на страници?

Все още се компилира, все още връща верни bitmap-и и спира да е паралелно. THotPDF.RenderLoadedPagesParallel и THotPDF.RenderLoadedPagesParallelOrdered са изградени върху TThread.CreateAnonymousThread с inline procedure closure, което Free Pascal 3.2.2 не може да изрази, така че Free Pascal клонът пуска детерминиран сериен fallback: минава индексите на страниците по ред, вика RenderLoadedPageToBitmap за всеки и брои успехите. Формата на API, връщаната стойност и изходният масив са непроменени, което позволява една кодова база да се гради и по двата начина

Същото parallel render извикване на HotPDF работи върху застъпващи се worker нишки при Delphi и минава индексите на страниците серийно при Free Pascal, като info записът на конвейера докладва worker count едно, вместо да крие fallback
Free Pascal клонът пази формата на API и изходния масив, докато докладва worker count едно, така че код, който вече чете info записа, вижда истината
var
  Bitmaps: THPDFBitmapArray;
  Info: THPDFParallelRenderPipelineInfo;
  Rendered: Integer;
begin
  Rendered := Pdf.RenderLoadedPagesParallel([0, 1, 2, 3], 150, 4,
    Bitmaps, Info);
  // Delphi: Info.WorkerCount е каквото бюджетът за памет е позволил
  // Free Pascal: Info.WorkerCount винаги е 1, страниците в ред по индекс
  if Info.WorkerCount = 1 then
    LogSerialFallback(Rendered, Info.RequestedWorkerCount);

Fallback не е мълчалив, което е частта, около която си заслужава да проектирате. Той запълва THPDFParallelRenderPipelineInfo честно: PageCount от заявката, RequestedWorkerCount като ехо на това, което сте поискали, WorkerCount зададен на 1, а броячите завършени и доставени съвпадат с това, което реално се е върнало. Код, който вече инспектира Info, за да оразмери progress bar или бюджет за памет, продължава да работи и чете истината, а не предположение. Ако планът ви за пропускателна способност разчита на паралелния render конвейер и неговия модел за backpressure, този план е Delphi план; на Free Pascal планирайте еднонишковата цена на рендериране на страница към bitmap, умножена по броя страници

Кой build действително да доставите?

Изберете по възможности, не по предпочитания. Ако работният ви процес е асемблиране на документи, текстово и векторно рисуване, попълване на формуляри, зареждане и запис, build за Free Pascal на Win64 го покрива, а валидирайте с изключена компресия, преди да включите каквото и да е. Ако включва изображения JPEG или JPEG 2000 или TIFF или JBIG2, ICC цветови трансформации, компресиран изход или пропускателна способност, зависеща от много ядра, останете за сега на Delphi или C++Builder. Границата е очертана от object-file ABI и липсваща езикова възможност, двете видими в сорса, а не погребани в матрица за поддръжка, и двете провалящи се с именувана грешка, а не с грешен резултат

Пакетът за Free Pascal и Lazarus идва в същата дистрибуция като units за Delphi и C++Builder, така че един лиценз покрива и двете и можете да тествате Lazarus пътя върху собствените си документи, преди да се ангажирате; продуктовата страница на HotPDF Delphi PDF Component носи актуалната матрица за компилаторна поддръжка и пълната API справка