Tehnički članak

HotPDF na Free Pascal-u: Deflate, AES i granice kodeka

HotPDF se kompajlira i izvršava pod Free Pascal 3.2.2 sa Lazarus-om, i pošten sažetak tog porta su dve rečenice. Stvaranje dokumenta, učitavanje, čuvanje, kompresija, dekompresija, šifrovanje i dešifrovanje svi rade na isključivo Pascal backend-ima, pa Lazarus aplikacija može proizvoditi i trošiti pravi PDF bez ikakve C zavisnosti. Opcioni nativni kodeci slika ne rade, jer prethodno izgrađeni Win64 objekti koriste COFF varijantu koju nijedan Free Pascal linker ne može konzumirati, pa na tom toolchain-u ulazne tačke rešavaju se u stub-ove koji otkazuju zatvoreno

Mapa mogućnosti HotPDF na Free Pascal-u: radeći Pascal deflate, AES i dokumentni backend-i pored kodeka slika stub-ova koji otkazuju zatvoreno
Funkcije dokumenta, kompresije i šifrovanja rade na isključivo Pascal backend-ima, dok se nativni kodeci slika rešavaju u stub-ove koji otkazuju zatvoreno

Dolazak od "se kompajlira" do "radi" uzeo je specifičan skup popravki, i svaka od njih je zamka koja će naći svaku drugu Delphi bazu koda koja se seli na Free Pascal. Vredi ih zapisati u redosledu po kojem su bolele

Zašto kompajlirana jedinica ništa ne dokazuje?

Zato što Pascal jedinica može referencirati simbol koji nikad neće učiniti ništa korisno, a i dalje zadovoljiti kompajler. U trenutku kada se svih 113 jedinica biblioteke čisto izgradilo pod Free Pascal-om, rukovaoci arhiv kontejnera iskreno su radili, provereno smoke testom koji je otvorio CBZ i pretvorio ga u PDF. XFA izravnavanje obrazaca uopšte nije radilo, jer izravnavanje mora inflate-ovati komprimovani /XFA tok paketa, a deflate ulazna tačka bila je još stub. Ništa u izlazu izgradnje nije razlikovalo ta dva slučaja

Pravilo koje je iz toga izašlo kratko je. Pre pisanja u napomenu o izdanju da funkcija radi na novom toolchain-u, napišite runtime sondu koja vežba funkciju od kraja do kraja na tom toolchain-u. Pokrivenost kompajliranjem je preduslov, nikada dokaz. Šira slika onoga što port pokriva nalazi se u napomenama o Win64 podršci Free Pascal-a i Lazarus-a

Izuzetak unutar cdecl stub-a ne stiže do pozivaoca

Ovo zaslužuje sopstveni odeljak jer je simptom toliko obmanjujući. Stub jedinice izlažu C ulazne tačke na način na koji bi statička biblioteka, pa stub izgleda ovako

// Deluje razumno. Nije.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

Na Free Pascal-u za Win64 taj izuzetak ne propagira do pozivaoca. Nema try..except rukovaoca koji ga vidi, jer odmotavanje preko cdecl granice deklarisane ovako ne nosi Pascal okvir izuzetka; proces se okončava sa izlaznim kodom 217. Sa strane aplikacije nema greške, nema poruke i nema log linije, samo program koji nestane. To je strogo gore od pogrešnog odgovora, jer se pogrešnim odgovorom može rukovati

Zašto izuzetak podignut unutar cdecl stub-a okončava Free Pascal proces izlaznim kodom 217 i kako ga popravlja kapija na Pascal ulaznoj tački
Pascal okvir izuzetka ne može se odmotati preko cdecl granice, pa proces tiho umire; popravka postavlja kapiju pre nego što se stub uopšte dostigne

Primamljiva popravka je učiniti da stub vrati kod neuspeha umesto toga, i za inflate to je ispravno jer zlib ima dobro definisan povratak greške. Opšte je pogrešno: stub za jpeg_read_header koji vraća nulu govori pozivaocu da nastavi sa strukturom koju niko nije inicijalizovao. Trajna popravka je kapija na Pascal ulaznoj tački umesto unutar C-oblikovanog stub-a, koristeći koju god konvenciju neuspeha taj API već ima

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // Odbijte pre nego što se stub uopšte dostigne, sa sopstvenom
  // konvencijom neuspeha ovog API-ja umesto izuzetkom preko cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib nije zlib, i razlika su dve klase dokumenata

Pascal deflate implementacija dostupna na Free Pascal-u rukuje dva uokvirivanja: zlib omotačem i sirovim deflate. Ne rukuje gzip uokvirivanjem, koje zlib bira kroz windowBits vrednosti od 16 do 31, i ne rukuje režimom automatske detekcije koji biraju vrednosti 32 do 47. HotPDF traži oba. Bezbedan put SVG uvoza traži 31, a učitavač ima lestvicu rezervnih rešenja koja traži 47 kada je uokvirivanje toka dvosmisleno. Preskočite bilo koje i cela familija dokumenata prestaje se otvarati, sa greškom dekodiranja koja ukazuje na tok umesto na nedostajuće uokvirivanje

windowBits pokrivenost paszlib-a naspram zlib-a: gzip uokvirivanje i auto-detekcija opsezi nedostaju za HotPDF SVG uvoz i loader rezervu
paszlib rukuje zlib omotačem i sirovim deflate, ali HotPDF traži i windowBits 31 i 47, pa shim mora sam obezbediti gzip uokvirivanje

Postoji druga, oštrija nekompatibilnost. z_stream zapis koji paszlib deklariše nema isti memorijski raspored kao C-ov: njegovo polje msg je kratak niz znakova umesto pokazivača, a total_in i total_out su 64-bitni gde C ABI ima mašinske reči. Zapis pozivaoca dakle ne može se proslediti pravo. Funkcionišući aranžman je držati paszlib stanje iza state pokazivača koji javni zapis već rezerviše, i kopirati javna polja unutra i napolju oko svakog poziva. gzip CRC i osmobajtni trailer dužine obuhvaćeni su u tom istom shim sloju, što je prirodno mesto za njih jer on već poseduje odluku o uokvirivanju

Prosleđivanje dinamičkog niza netipiziranom var parametru

Ovo je greška koja najverovatnije upravo sedi u vašem kodu. Kada prosledite dinamički niz netipiziranom var parametru, ono što primalac dobija je adresa promenljive niza, koja je adresa pokazivača, a ne adresa sadržaja. Pa čitanje u njega prepisuje samu promenljivu i ono što sedi pored nje

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

  // Pogrešno: predaje adresu promenljive FBuffer
  FStream.Read(FBuffer, Length(FBuffer));

  // Ispravno: predaje adresu prvog bajta sadržaja
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

Na Delphi-ju pogrešan oblik često deluje kao da radi, jer ono što oštećuje je susedan slot steka koji ništa kasnije ne čita. Na Free Pascal-u ista linija pravi segmentacionu grešku pri prvoj upotrebi. Ono što ga čini tako teškim za uočavanje okom jeste što statički nizovi nemaju takav problem, pošto je promenljiva statičkog niza svoj vlastiti sadržaj, pa su oba pravopisa ispravna u istoj datoteci zavisno od deklaracije nekoliko stotina linija dalje

ZIP kontejneri bez System.Zip

Free Pascal nema ekvivalent RTL zip jedinici, a dostupna alternativa ima i drugačiju API površinu i bez podrške za nasleđeno šifrovanje koje stariji formati kontejnera i dalje koriste, pa se mali čitač unutar biblioteke ispostavio kraćim od prilagođavanja njoj. Dva detalja formata koštala su vremena i lako je pogrešiti o njima

Prvi je bajt provere šifrovskog zaglavlja. Njegov dvanaesti bajt obično je gornji bajt CRC-a, ali kada je postavljen bit 3 opštenamenske zastavice, što znači da veličine žive u pratećem data deskriptoru i da CRC još nije poznat, bajt provere dolazi iz gornjeg bajta vremena izmene umesto toga. Implementirajte samo CRC oblik i svaki arhiv pisan u streaming režimu odbacuje ispravnu lozinku. Drugi je ZIP64 extra polje: njegova tri 64-bitna polja pojavljuju se u fiksnom redosledu ali se pišu samo kada je odgovarajuće 32-bitno polje zasićeno, pa njihovo čitanje na fiksnim ofsetima radi na arhivima koje ste testirali i otkazuje na sledećem. Rastavite ih poziciono u odnosu na to koja su 32-bitna polja zasićena

Jedna pogodnost vredna znanja: Free Pascal tok dekompresije prima drugi argument konstruktora koji preskače zlib zaglavlje, što je tačno ono što ZIP unosi traže pošto čuvaju sirovi deflate. Taj put uopšte ne dira zlib shim biblioteke, pa nije pogođen nedostajućim C backend-om

Prozirnost obojenih glifova pod LCL-om

Čitanje alfa kanala rasterizovanog obojenog glifa jedini je grafički detalj bez direktne translacije. LCL PNG klasa nema scanline pristupnik koji izlaže alfu, a dodela PNG-a bitmapama je odbacuje, pa obojeni emotikon stiže potpuno neprovidan i kompozituje sa crnom kutijom iza sebe. Funkcionišući put je interfejska slika: stvorite je iz PNG-a, pa čitajte piksele kroz pristupnik boje, pamteći da su njegove komponente 16-bitne i treba ih pomeriti naniže za osam da postanu bajtovi. Ta površina takođe koristi prirodni redosled redova odozgo nadole, pa Height - 1 - Y inverziju koju VCL scanline kod traži treba ukloniti, a ne preneti

Dve napomene o sistemu izgradnje pre nego što prijavite grešku

Kompletna ponovna izgradnja povremeno otkazuje sa nedefinisanim simbolom čije se ime završava na $crc sufiks i heksadecimalnu vrednost. Taj sufiks računa se iz tipova parametara, i otkazuje poklapanje kada jedna izgradnja kompajlira jedinicu naspram dve različite verzije interfejsa u istom prolazu. Ponovno pokretanje izgradnje to čisti; potpis nije pogrešan

Drugo, Free Pascal 3.2.2 nema anonimne metode, pa svuda gde je biblioteka koristila closure-e da poveže paralelni pipeline Free Pascal izgradnja umesto toga uzima deterministički serijski rezervni put. Izlaz je identičan, propusnost nije; ako zavisite od paralelnog renderovanja stranica, to je razlog da ostanete na Delphi-ju za sada, i dizajn pipeline-a opisan je u članku o paralelnom render pipeline-u. Situacija sa kodecima slika je drugo mesto gde izbor toolchain-a menja mogućnosti, a ne samo brzinu, pa Lazarus raspoređivanje treba da planira svoje formate slika u skladu sa tim; trenutna matrica po toolchain-u nalazi se na stranici proizvoda HotPDF Delphi PDF component