PDF renderer koji ne crta ništa obično uopšte nema bag u kodu za crtanje. U HotPDF Component za Delphi i C++Builder, četiri odvojena defekta su činila da se stranice renderuju prazne dok je svaka linija loga ostajala čista: operandi imena koji nose vodeću kosu crtu, obrnuta cm konkatenacija, i indeks tokena koji je čitao nulu. Nijedan od njih nije bacio grešku. Nijedan nije logovao. Tok sadržaja se tačno tokenizovao, dispečer operatora je prepoznao svaki operator, XObject slike je dekodiran u validnu bitmapu, a onda je stranica ispala prazna. Ta kombinacija — pipeline koji na svakoj fazi prijavljuje uspeh, a ne proizvodi ništa vidljivo — je potpis pretrage ili indeksa koji tiho promaši umesto da otkaže. Ovo je post-mortem jedne takve porodice, i discipline testiranja koja joj je omogućila da preživi 38 izdanja
Zašto PDF renderer ne crta baš ništa?
Zato što je neuspela pretraga resursa u PDF rendereru neraspoznatljiva od prazne stranice. Operandi imena u toku sadržaja i ključevi rečnika resursa su dva različita prostora stringova, a HotPDF ih je poredio unakrsno bez normalizacije. Tokenizator čita /Im0 i zadržava kosu crtu, jer to je taj token; učitani rečnik /Resources /XObject čuva ključ kao Im0, jer parser uklanja graničnik kada gradi ključeve rečnika. Svaki FindValue protiv imena operanda zato je vraćao -1. Radijus dejstva je bio širi od slika. ISO 32000-1 §8.9 pokriva Do, §8.4 pokriva gs i njegovu pretragu /ExtGState, §8.6 pokriva cs i CS, a §8.7.4.3 pokriva sh. Svih pet operatora je ključalo svoj podrečnik resursa sirovim operandom, pa je svih pet promašilo. Imenovani prostori boja su padali nazad na DeviceGray, što pretvara 1 scn u belo mastilo na beloj stranici. XObjecti slika nikad nisu bili nacrtani uopšte — putanja bitmap slike, u praksi, nikad nije radila od dana kad je uvedena. Popravka je helper na nivou unita primenjen na svaku pretragu ključanu operandom, što je jedini način da se spreči da konvencija ponovo skrene
// Page content stream, the ordinary image-placement idiom:
// q
// /GS0 gs
// 200 0 0 120 60 400 cm
// /Im0 Do
// Q
// The operand token is '/Im0'. The resource dictionary key is 'Im0'.
function HPDFStripNameSlash(const N: AnsiString): AnsiString;
begin
Result := N;
if (Result <> '') and (Result[1] = '/') then
Delete(Result, 1, 1);
end;
// Every resource lookup keyed by an operand name goes through the helper.
Name := HPDFStripNameSlash(Name);
XObjIdx := FPageResources.FindValue('XObject');
if XObjIdx < 0 then
Exit;
// The /XObject sub-dictionary may itself be an indirect reference.
XObjDict := FAccess.ResolveDictionary(FAccess.Context,
FPageResources.GetIndexedItem(XObjIdx));
if XObjDict = nil then
Exit;
Drugi, srodan promašaj sedeo je jedan sloj niže. Renderer je imao tipizovane resolvere samo za tokove i rečnike, pa se indirektna referenca koja pokazuje na objekat niza na najvišem nivou — čest /CS0 5 0 R sa [/Separation ...] na drugom kraju — razrešavala u nil kroz oba, i padala nazad na nerazrešenu vezu. Dodavanje generičkog resolvera objekata popravilo je imenovane prostore boja i nizove funkcija u jednom potezu. Ako povezujete rečnike senčenja, ista disciplina razrešavanja važi za putanju aksijalnog i radijalnog senčenja, gde je stavka /Function vrlo često indirektna
Operator cm i konkatenacija napisana unazad
Drugi defekt je postavljao slike otprilike sto hiljada piksela van stranice, što izgleda potpuno isto kao da ih uopšte nije nacrtao. ISO 32000-1 §8.3.4 definiše PDF transformacije vektorima redova, a operator cm spaja svoju matricu operanda M na trenutnu matricu transformacije kao M × CTM — M deluje prvo, postojeći CTM posle. HotPDF sastavlja matrice kroz HPDFMatMul(A, B), koja primenjuje B pre A. Ispravan poziv zato prosleđuje stari CTM kao A. Isporučeni kod je prosleđivao matricu operanda kao A, proizvodeći CTM × M
Obrnut redosled je bezopasan za jedan pojedinačan cm, a katastrofalan za standardni dvokoračni idiom. Postavite sliku sa 1 0 0 1 x y cm praćenim sa w 0 0 h 0 0 cm, i ispravna kaskada skalira jediničan kvadrat po (w, h), a zatim ga translira po (x, y). Pod obrnutom kaskadom translacija ide prva, a skala je množi, pa slika nominalno na (60, 400) skalirana na 200 sa 120 završava na (12000, 48000). Test isecanja na vrhu blita je odbacuje, blit se preskače, a ništa nigde ne prijavljuje problem
// HPDFMatMul(A, B) applies B first, then A.
// ISO 32000-1 cm semantics: new CTM = M x CTM, so M must be B.
// Wrong, and shipped for 38 versions:
GS.CTM := HPDFMatMul(HPDFMatFromOps(NumAt(6), NumAt(5), NumAt(4),
NumAt(3), NumAt(2), NumAt(1)), GS.CTM);
// Correct:
GS.CTM := HPDFMatMul(GS.CTM, HPDFMatFromOps(NumAt(6), NumAt(5), NumAt(4),
NumAt(3), NumAt(2), NumAt(1)));
Ono što ovo čini poučnim jeste da je isti izvorni fajl već sadržao ispravan redosled. Stavka /Matrix Form XObjecta imala je istu obrnutu kompoziciju, ali putanja Type 3 glifa i putanja obrisa ugrađenog glifa su obe od početka bile ispravne, jer se postavljanje glifa vidljivo sruši u koordinatni početak kad se invertuje, i neko je već bio primoran da to popravi. Dve konvencije su koegzistirale u jednom unitu tri desetine izdanja, svaka ispravna u svojoj sopstvenoj funkciji, i nijedan recenzent to nije primetio jer nijedno mesto poziva nije izgledalo pogrešno izolovano
Šta se dešava kad je indeks tokena pogrešan za jedan?
Dobijete dvanaest operatora koji su sintaksno obrađeni, a semantički mrtvi. Pristupnik operanda u rendereru je NumAt(Back), koji čita Tokens[OpIndex - Back], a OpIndex je indeks samog tokena operatora. Operator sa jednim operandom zato pronalazi svoj broj na back 1. Dvanaest njih je bilo napisano kao NumAt(0), što čita token operatora, ne prolazi proveru vrste ctOperandNumber, i vraća podrazumevanu nulu. Lista je Tc, Tw, Tz, TL, Ts i Tr iz operatora stanja teksta ISO 32000-1 §9.3, plus w, J, j, M, ri i i iz operatora stanja grafike §8.4.3. Razmak karaktera i reči je postao no-op, horizontalno skaliranje se nikad nije primenjivalo, leading je ostajao na nuli pa T* nikad nije pomerao liniju, dizanje teksta nije radilo ništa, mod renderovanja je uvek bio fill, i svaki potez u svakom dokumentu izlazio je kao linija tanka 1 piksel bez obzira na deklarisanu širinu linije. Operatori sa više operanda kao što su m, rg i Tm koristili su NumAt(1..6) i svi su bili ispravni, pa je recenzent koji skenira funkciju video zid verodostojne aritmetike indeksa sa dvanaest pogrešnih stavki ugnežđenih unutra
function NumAt(Back: Integer): Double;
begin
Result := 0;
if (OpIndex - Back >= 0)
and (Tokens[OpIndex - Back].Kind = ctOperandNumber) then
Result := Tokens[OpIndex - Back].NumValue;
end;
// OpIndex addresses the operator token, so a lone operand sits at back 1.
else if Op = 'Tc' then GS.Text.CharSpace := NumAt(1) // previously NumAt(0)
else if Op = 'TL' then GS.Text.Leading := NumAt(1) // previously NumAt(0)
else if Op = 'Tr' then GS.Text.RenderMode := Round(NumAt(1))
else if Op = 'w' then GS.LineWidth := NumAt(1) // previously NumAt(0)
Zašto je test suite ostao zelen 38 verzija?
Zato što su asercije bile preslabe da razlikuju renderovanu stranicu od delimično renderovane. Rendering smoke testovi su tvrdili stvari poput da izlazna bitmapa nije potpuno crna, ili da stranica nije prazna, ili da digest slike nije nula. Svaka od njih važi kada se tekst renderuje, a slike ne. Tekst se crtao dobro, pa frame buffer nikad nije bio uniforman, digest nikad nije bio nula, a paket je prijavljivao uspeh dok je ceo pipeline slika u praksi bio mrtav kod. Slabe asercije su zavodljive za grafiku upravo zato što jake izgledaju krhko. Niko ne želi test koji puca kad se ivica anti-aliasinga pomeri za jedan piksel, pa je prirodno povlačenje da se tvrdi nešto što nijedna razumna izmena ne bi mogla prekršiti — a to povlačenje vas dovodi do predikata koje ni nerazumna izmena ne može prekršiti. Test prostora boja separation tvrdio je da je izlaz razlučiv od crne; sivo na belom je prošlo, a i belo na belom. Test nije merio da li je nanesena prava boja. Merio je da li se uopšte nešto desilo na platnu
Kako napisati asertaciju renderovanja koja zaista pukne?
Prebrojite piksele očekivane boje, u očekivanoj količini, i pustite da pozicija i veličina proisteknu iz brojanja. Zamenska disciplina je ručno izrađen minimalan PDF, jedna vizuelna činjenica po fajlu, i asercija koliko piksela pada unutar tolerancije specifičnog RGB trojca. Slika 200 sa 120 čiste crvene boje postavljena na poznat ofset mora proizvesti otprilike 24000 crvenih piksela. Ako pretraga resursa promaši, broj je 0. Ako je cm kaskada obrnuta, broj je 0. Ako se slika renderuje u pogrešnom prostoru boja, broj je 0. Jedan broj hvata sva tri, a traka tolerancije apsorbuje šum anti-aliasinga koji je ljude terao da izbegavaju tačno poređenje na prvom mestu
function CountPixelsNear(Bmp: TBitmap; R, G, B, Tol: Integer): Integer;
var
X, Y: Integer;
C: TColor;
begin
Result := 0;
for Y := 0 to Bmp.Height - 1 do
for X := 0 to Bmp.Width - 1 do
begin
C := Bmp.Canvas.Pixels[X, Y];
if (Abs(GetRValue(C) - R) <= Tol)
and (Abs(GetGValue(C) - G) <= Tol)
and (Abs(GetBValue(C) - B) <= Tol) then
Inc(Result);
end;
end;
// A 200x120 red image placed at 60,400 must paint about 24000 red pixels.
Check(CountPixelsNear(Bmp, 255, 0, 0, 12) > 20000,
'image XObject was never drawn');
Četiri smoke testa su prepisana na ovaj način — Type 4 tint transformacija, postavljanje slike putem Do, slučaj vidljivosti opcionog sadržaja i mod poteza Tr — i zajedno su otkrili celu porodicu. To je prava lekcija, i generalizuje se izvan ove kodne baze: u rendering pipelineu, asercija mora da imenuje boju. Sve mekše je provera da je renderer izvršen, ne provera da je nešto nacrtao. Ako gradite sopstveni harness za stranica-u-bitmapu, pregled rasterizacije stranice je prirodno mesto da zavrnete helper za brojanje piksela na svoju prvu regresiju
Iskrene granice
Dve granice vredi jasno navesti. Modovi renderovanja isecanja teksta 4 do 7 crtaju se kao njihov osnovni mod ispune ili poteza, jer renderer ne modeluje akumulirane putanje isecanja iz obrisa glifova; dokumenti koji se oslanjaju na isecanje oblikovano tekstom renderovaće tekst umesto isečene ilustracije ispod. A disciplina brojanja piksela opisana ovde je tehnika smoke testa, ne paket usaglašenosti — dokazuje da je specifična vizuelna činjenica stigla do frame buffera, što je mnogo niža granica od dokazivanja da izlaz odgovara referentnom rasterizatoru. Ipak, to je tačno granica koju ova četiri baga nisu uspela da pređu tri godine izdanja
Renderer o kojem je ovde reč isporučuje se kao deo standardne HotPDF Component za Delphi i C++Builder; proizvodna stranica nosi kompletnu referencu API-ja za renderovanje stranica, uključujući keš bitmapa i ulazne tačke pozadinskog prefetch-a