Upodabljalnik PDF, ki ne izriše ničesar, ponavadi nima nobenega hrošča v svoji kodi risanja. V komponenti HotPDF za Delphi in C++Builder so štiri ločene napake povzročile, da so se strani upodabljale prazne, medtem ko je vsaka vrstica dnevnika ostala čista: operandi imen z vodilno poševnico, obrnjena konkatenacija cm in indeks žetona, ki je bral ničlo. Nobena od njih ni vrgla izjeme. Nobena od njih ni beležila v dnevnik. Vsebinski tok se je pravilno tokeniziral, dispečer operatorjev je prepoznal vsak operator, slikovni XObject je bil dekodiran v veljavno bitno sliko, nato pa je stran izpadla prazna. Ta kombinacija — cevovod, ki na vsaki stopnji poroča uspeh in ne proizvede ničesar vidnega — je podpis iskanja ali indeksa, ki tiho zgreši, namesto da bi odpovedal. To je posmrtna analiza ene take družine in testne discipline, ki ji je omogočila preživeti 38 izdaj
Zakaj upodabljalnik PDF ne izriše popolnoma ničesar?
Ker je neuspešno iskanje vira v upodabljalniku PDF nerazločljivo od prazne strani. Operandi imen vsebinskega toka in ključi slovarja virov sta dva različna prostora nizov, HotPDF pa ju je primerjal čez, ne da bi normaliziral. Tokenizator prebere /Im0 in obdrži poševnico, ker je to, kar žeton je; naloženi slovar /Resources /XObject ključ shrani kot Im0, ker razčlenjevalnik pri gradnji ključev slovarja odstrani ločilo. Vsak FindValue proti imenu operanda je zato vrnil -1. Radij eksplozije je bil širši od slik. ISO 32000-1 §8.9 pokriva Do, §8.4 pokriva gs in njegovo iskanje /ExtGState, §8.6 pokriva cs in CS, §8.7.4.3 pa pokriva sh. Vseh pet operatorjev je svoj podslovar virov ključilo s surovim operandom, zato je vseh pet zgrešilo. Poimenovani barvni prostori so padli nazaj na DeviceGray, kar 1 scn spremeni v belo črnilo na beli strani. Slikovni XObjecti sploh nikoli niso bili naslikani — pot slikovne bitne slike v praksi ni nikoli delovala od dneva, ko je pristala v kodi. Popravek je pomočnik na ravni enote, uporabljen pri vsakem iskanju, ključenem z operandom, kar je edini način, da se konvencija ne zamakne nazaj
// 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;
Drug, soroden spodrsljaj je sedel en sloj nižje. Upodabljalnik je imel tipizirane razreševalnike le za tokove in slovarje, tako da se je posredna referenca, ki kaže na objekt polja na vrhnji ravni — pogost /CS0 5 0 R z [/Separation ...] na drugem koncu — skozi oba razrešila v nil in padla nazaj na nerazrešeno povezavo. Dodajanje generičnega razreševalnika objektov je v enem koraku popravilo poimenovane barvne prostore in polja funkcij. Če povezujete slovarje senčenja, ista disciplina razreševanja velja za pot osnega in radialnega senčenja, kjer je vnos /Function zelo pogosto posreden
Operator cm in konkatenacija, zapisana obratno
Druga napaka je postavila slike približno sto tisoč pikslov mimo strani, kar je videti natanko tako, kot da jih sploh niso izrisali. ISO 32000-1 §8.3.4 definira transformacije PDF z vrstičnimi vektorji, operator cm pa svojo matriko operanda M pripne na trenutno transformacijsko matriko kot M × CTM — M učinkuje najprej, obstoječi CTM nato. HotPDF sestavlja matrike prek HPDFMatMul(A, B), ki uporabi B pred A. Pravilen klic torej kot A poda stari CTM. Izdana koda je kot A podala matriko operanda, kar je proizvedlo CTM × M
Obrnjen vrstni red je neškodljiv za en sam cm in katastrofalen za standardni dvokoraken idiom. Postavite sliko z 1 0 0 1 x y cm, ki mu sledi w 0 0 h 0 0 cm, pravilna kaskada pa skalira enotski kvadrat za (w, h) in ga nato prevede za (x, y). Pod obrnjeno kaskado gre prevod najprej, skala pa ga pomnoži, tako da slika, nominalno na (60, 400), skalirana na 200 krat 120, pristane na (12000, 48000). Test odreza na vrhu blita jo zavrne, blit je preskočen, nič nikjer pa ne poroča o težavi
// 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)));
Kar to napako naredi poučno, je, da je ista izvorna datoteka že vsebovala pravilen vrstni red. Vnos /Matrix Form XObjecta je imel isto obrnjeno sestavo, vendar sta pot znaka Type 3 in pot obrisa vdelanega znaka obe od začetka naredili pravilno, ker se postavitev znaka vidno sesede v izhodišče, ko jo obrnete, nekdo pa je bil že prisiljen to popraviti. Dve konvenciji sta soobstajali v eni enoti tri desetletja izdaj, vsaka pravilna v svoji funkciji, noben pregledovalec pa tega ni opazil, ker nobeno klicno mesto ni bilo videti narobe izolirano
Kaj se zgodi, ko je indeks žetona zamaknjen za ena?
Dobite dvanajst operatorjev, ki so skladenjsko obravnavani in semantično mrtvi. Dostopnik operanda v upodabljalniku je NumAt(Back), ki bere Tokens[OpIndex - Back], OpIndex pa je indeks samega žetona operatorja. Operator z enim operandom torej svojo številko najde pri back 1. Dvanajst od njih je bilo napisanih kot NumAt(0), kar prebere žeton operatorja, ne prestane preverjanja tipa ctOperandNumber in vrne privzeto ničlo. Seznam je Tc, Tw, Tz, TL, Ts in Tr iz operatorjev stanja besedila ISO 32000-1 §9.3, plus w, J, j, M, ri in i iz operatorjev stanja grafike §8.4.3. Razmik znakov in besed je postal brez učinka, vodoravno skaliranje se nikoli ni uveljavilo, vodenje je ostalo pri ničli, tako da T* nikoli ni napredoval za vrstico, dvig besedila ni storil ničesar, način upodabljanja je bil vedno polnjenje, vsaka poteza v vsakem dokumentu pa je izpadla kot 1-pikselska tanka črta ne glede na deklarirano širino črte. Operatorji z več operandi, kot so m, rg in Tm, so uporabljali NumAt(1..6) in so bili vsi pravilni, tako da je pregledovalec, ki je pregledoval funkcijo, videl zid verjetne indeksne aritmetike z dvanajstimi napačnimi vnosi, vgrajenimi vanjo
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)
Zakaj je testni nabor ostal zelen 38 izdaj?
Ker so bile trditve prešibke, da bi razločile upodobljeno stran od delno upodobljene. Testi dima upodabljanja so trdili stvari, kot je izhodna bitna slika ni popolnoma črna, ali stran ni prazna, ali je digest slike neničeln. Vsaka od teh velja, kadar se besedilo upodobi, slike pa ne. Besedilo se je izrisalo v redu, zato slikovni medpomnilnik nikoli ni bil enoten, digest nikoli ni bil nič, nabor pa je poročal uspeh, medtem ko je bil cel slikovni cevovod v praksi mrtva koda. Šibke trditve so za grafiko zapeljive natanko zato, ker so močne videti krhke. Nihče noče testa, ki se podre, ko se rob glajenja robov premakne za en piksel, zato je naravni umik trditi nekaj, česar nobena razumna sprememba ne bi mogla kršiti — ta umik pa vas pripelje do predikatov, ki jih tudi nobena nerazumna sprememba ne more kršiti. Test barvnega prostora Separation je trdil, da je izhod razločljiv od črne; sivo na belem ga je prestalo, prav tako belo na belem. Test ni meril, ali je bila naslikana pravilna barva. Meril je, ali se je na platnu sploh kaj zgodilo
Kako napišete trditev upodabljanja, ki dejansko odpove?
Preštejte piksle pričakovane barve v pričakovani količini in pustite, da položaj in velikost izpadeta iz štetja. Nadomestna disciplina je ročno zgrajen minimalen PDF, eno vizualno dejstvo na datoteko in trditev o tem, koliko pikslov pade znotraj tolerance določene trojice RGB. Slika čiste rdeče barve, velikosti 200 krat 120, postavljena pri znanem odmiku, mora proizvesti približno 24000 rdečih pikslov. Če iskanje vira zgreši, je štetje 0. Če je kaskada cm obrnjena, je štetje 0. Če se slika upodobi v napačnem barvnem prostoru, je štetje 0. Ena sama številka ujame vse tri, pas tolerance pa vpije šum glajenja robov, ki je ljudi na začetku odvrnil od natančne primerjave
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');
Na ta način so bili prepisani štirje testi dima — tonska transformacija Type 4, postavitev slike Do, primer vidnosti izbirne vsebine in način poteze Tr — med seboj pa so razkrili celotno družino. To je resnični nauk, posplošuje pa se onkraj te baze kode: v cevovodu upodabljanja mora trditev poimenovati barvo. Karkoli mehkejšega je preverjanje, da je upodabljalnik tekel, ne pa preverjanje, da je izrisal. Če gradite lasten okvir strani-v-bitno-sliko, je sprehod skozi rasterizacijo strani naravno mesto, kamor privijete pomočnika za štetje pikslov k svoji prvi regresiji
Poštene meje
Vredno je jasno povedati dve meji. Načini upodabljanja odreza besedila 4 do 7 so izrisani kot njihov osnovni način polnjenja ali poteze, ker upodabljalnik ne modelira akumuliranih poti odreza iz obrisov znakov; dokumenti, ki se zanašajo na odrez v obliki besedila, bodo izrisali besedilo namesto odrezane grafike spodaj. In tukaj opisana disciplina štetja pikslov je tehnika testa dima, ne pa nabor skladnosti — dokaže, da je določeno vizualno dejstvo doseglo slikovni medpomnilnik, kar je veliko nižja letvica kot dokazovanje, da se izhod ujema z referenčnim rasterizatorjem. Je pa natanko letvica, ki je teh štiri hrošči niso presegli tri leta izdaj
Tukaj obravnavani upodabljalnik je izdan kot del standardne HotPDF Component za Delphi in C++Builder; stran izdelka nosi celoten referenčni opis API-ja za upodabljanje strani, vključno z vstopnimi točkami predpomnilnika bitnih slik in predpridobivanja v ozadju