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

HotPDF на Free Pascal: Deflate, AES и граници на кодеци

HotPDF се компилира и изпълнява под Free Pascal 3.2.2 с Lazarus, а честното резюме на този пренос са две изречения; Създаване на документи, зареждане, записване, компресия, декомпресия, шифроване и дешифриране всички работят върху само-Pascal бекендове, така че Lazarus приложение може да произвежда и консумира истински PDF без каквито и да е C зависимости; Опционалните нативни image кодеци не, защото предварително компилираните Win64 обекти използват COFF вариант, който нито един Free Pascal линкер не може да консумира, така че на тази toolchain входните точки резолвират към заглушки, които се провалят затворено

Карта на възможностите на HotPDF на Free Pascal: работещи Pascal deflate, AES и документни бекендове до image кодек заглушки, които се провалят затворено
Документни, компресионни и шифровъчни функции тичат на само-Pascal бекендове, докато нативните image кодеци резолвират към заглушки, които се провалят затворено

Преминаването от „компилира се“ към „работи“ отне специфичен набор от поправки и всяка една от тях е капан, който ще хване всеки друг Delphi codebase, движещ се към Free Pascal; Те си заслужават да се запишат в реда, в който болят

Защо компилиращ модул не доказва нищо?

Защото Pascal модул може да реферира символ, който никога няма да направи нищо полезно и все пак да задоволи компилатора; В момента, в който всичките 113 библиотечни модула се компилираха чисто под Free Pascal, обработчиците на архивни контейнери наистина работеха, проверени със smoke тест, който отвори CBZ и го превърна в PDF; XFA form flattening изобщо не работеше, защото flattening трябва да inflate-не компресирания /XFA packet поток, а deflate входната точка все още беше заглушка; Нищо в build изхода не разграничи тези два случая

Правилото, излязло оттам, е кратко; Преди да напишете в release бележка, че функция работи на нова toolchain, напишете runtime сонда, която упражнява функцията от край до край на тази toolchain; Компилаторно покритие е предпоставка, никога доказателство; По-широката картина на това, което преносът покрива, е в бележките за поддръжка на Free Pascal и Lazarus Win64

Raise вътре в cdecl заглушка не достига извикващия

Това заслужава собствена секция, защото симптомът е толкова подвеждащ; Заглушечните модули излагат C входни точки така, както би направила статична библиотека, така че заглушка изглежда така

// Изглежда разумно. Не е.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

На Free Pascal за Win64 това изключение не се разпространява до извикващия; Няма try..except обработчик, който да го види, защото unwinding през cdecl граница, декларирана по този начин, не носи Pascal exception рамката; процесът се терминира с exit код 217; От страна на приложението няма грешка, няма съобщение и няма log ред, само програма, която изчезва; Това е строго по-лошо от грешен отговор, защото грешен отговор може да се обработи

Защо изключение, хвърлено вътре в cdecl заглушка, прекратява Free Pascal процес с exit код 217 и как ограждането на Pascal входната точка го оправя
Pascal exception рамката не може да unwind-не през cdecl границата, така че процесът умира мълшиво; поправката огражда, преди заглушката изобщо да бъде достигната

Съблазнителната поправка е заглушката вместо това да връща код за отказ и за inflate това е правилно, защото zlib има добре дефиниран error return; Тя е грешна изобщо: заглушка за jpeg_read_header, която връща нула, казва на извикващия да продължи със структура, която никой не е инициализирал; Трайната поправка е да се огради на Pascal входната точка, а не вътре в C-образната заглушка, използвайки каквата конвенция за отказ API-то вече има

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // Отказ, преди заглушката изобщо да бъде достигната, със собствена
  // конвенция за отказ на това API, а не изключение през cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib не е zlib, а разликата са два класа документи

Pascal deflate имплементацията, налична на Free Pascal, обработва две framings: zlib wrapper-а и raw deflate; Тя не обработва gzip framing, който zlib избира чрез windowBits стойности от 16 до 31, и не обработва режима за автоматично откриване, който стойностите 32 до 47 избират; HotPDF се нуждае и от двете; Безопасният SVG импорт път иска 31, а loader-ът има стълба от fallback-и, която иска 47, когато framing-ът на поток е неясен; Пропуснете едното и цяло семейство документи спира да се отваря, с decode грешка, сочеща към потока, а не към липсващия framing

windowBits покритие на paszlib срещу zlib: gzip framing и auto-detect диапазони, липсващи за HotPDF SVG импорт и loader fallback
paszlib обработва zlib wrapper-а и raw deflate, но HotPDF се нуждае и от windowBits 31 и 47, така че shim-ът трябва сам да доставя gzip framing

Има втора, по-остра несъвместимост; z_stream записът, който paszlib декларира, няма същото memory оформление като C такова: неговото поле msg е short string, а не указател, а total_in и total_out са 64-битови, докато C ABI има машинни думи; Запис на извикващия следователно не може да бъде подаван направо; Работният уред е paszlib състоянието да стои зад указателя state, който публичният запис вече резервира, и публичните полета да се копират в и из около всяко извикване; Gzip CRC и осембайтовия length трейлър се отчитат в същия shim слой, който е естественото място за тях, тъй като той вече притежава решението за framing

Подаване на динамичен масив на нетипизиран var параметър

Това е дефектът, който най-вероятно седи във вашия код в момента; Когато подадете динамичен масив на нетипизиран var параметър, това, което извиканият получава, е адресът на масивната променлива, който е адресът на указател, не адресът на payload-а; Така четене в него презаписва самата променлива и каквото седне до нея

var
  FBuffer: TBytes;
begin
  SetLength(FBuffer, 65536);

  // Грешно: предава адреса на променливата FBuffer
  FStream.Read(FBuffer, Length(FBuffer));

  // Правилно: предава адреса на първия payload байт
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

На Delphi грешната форма често изглежда работи, защото това, което тя поврежда, е съседен stack слот, който нищо не чете после; На Free Pascal същият ред segmentation-fault при първа употреба; Това, което го прави толкова трудно за забелязване с око, е, че статичните масиви нямат такъв проблем, тъй като статична масивна променлива е собственът ѝ payload, така че и двете изписвания са правилни в един и същ файл в зависимост от декларацията няколкостотин реда по-далеч

ZIP контейнери без System.Zip

Free Pascal няма еквивалент на RTL zip модула, а наличната алтернатива има едновременно различна API повърхност и никаква поддръжка на legacy шифроването, което по-старите контейнерни формати все още използват, така че малък четец в библиотеката се оказа по-кратък от приспособяването към нея; Два детайла на формата струваха време и са лесни за объркване

Първият е check байтът на encryption header-а; Неговият дванайсети байт обикновено е старшият байт на CRC, но когато general-purpose флаг бит 3 е установен, което значи, че размерите живеят в trailing data descriptor и CRC все още не е известен, check байтът идва от старшия байт на времето на модификация вместо това; Имплементирайте само CRC формата и всеки архив, записан в streaming режим, отхвърля правилна парола; Вторият е ZIP64 extra полето: неговите три 64-битови полета се появяват в фиксиран ред, но се записват само когато съответното 32-битово поле е наситено, така че четенето им на фиксирани offsets работи върху архивите, които сте тествали, и се проваля на следващия; Парсвайте ги позиционно спрямо кои 32-битови полета са наситени

Едно удобство, което си заслужава да се знае: Free Pascal декомпресионният поток приема втори конструктор аргумент, който прескача zlib header-а, което е точно това, от което ZIP записите се нуждаят, тъй като съхраняват raw deflate; Този път изобщо не докосва library zlib shim-а, така че не е засегнат от липсващия C бекенд

Прозрачност на цветни глифи под LCL

Четенето на alpha канала на растеризиран цветен глиф е единственият graphics детайл без директен превод; LCL PNG класът няма scanline accessor, който излага alpha, а присвояването на PNG към bitmap го изхвърля, така че цветен emoji пристига напълно непрозрачен и се композира с черна кутия зад него; Работният маршрут е interface изображението: създайте го от PNG, после четете пиксели през цветния accessor, помнейки, че неговите компоненти са 16-битови и се нуждаят от изместване надолу с осем, за да станат байтове; Тази повърхност също използва естествен ред на редовете отгоре надолу, така че инверсията Height - 1 - Y, от която VCL scanline код се нуждае, трябва да бъде премахната, а не пренесена

Две бележки за build системата, преди да докладвате дефект

Пълно прекомпилиране от време на време се проваля с недефиниран символ, чието име завършва със суфикс $crc и hex стойност; Този суфикс се изчислява от типовете параметри и се проваля да съвпадне, когато една компилация компилира модул срещу две различни interface версии в един и същ проход; Повторното изпълнение на компилацията го изчиства; сигнатурата не е грешна

Второ, Free Pascal 3.2.2 няма анонимни методи, така че навсякъде, където библиотеката е използвала closures за свързване на паралелен конвейер, Free Pascal компилацията взема детерминистичен сериен fallback вместо това; Изходът е идентичен, пропускателната способност не е; ако зависите от паралелно изобразяване на страници, това е причина да останете на Delphi за момента, а дизайнът на конвейера е описан в статията за паралелен render конвейер; Ситуацията с image кодеците е другото място, където изборът на toolchain променя възможностите, а не само скоростта, така че Lazarus внедряване трябва да планира своите image формати съответно; текущата матрица per-toolchain е на продуктовата страница HotPDF Delphi PDF component