En PDF-renderer som ikke tegner noe har vanligvis ingen feil i selve tegnekoden. I HotPDF Component for Delphi og C++Builder gjorde fire separate defekter at sider ble rendret tomme mens hver loggelinje forble ren: navneoperander som bar en ledende skråstrek, en omvendt cm-sammenslåing, og en tokenindeks som leste null. Ingen av dem kastet en feil. Ingen av dem logget noe. Innholdsstrømmen ble tokenisert korrekt, operatordispatcheren gjenkjente hver operator, bilde-XObjectet ble dekodet til et gyldig bitkart, og så kom siden ut tom. Den kombinasjonen — en pipeline som rapporterer suksess på hvert trinn og produserer ingenting synlig — er kjennetegnet på et oppslag eller en indeks som stille bommer fremfor å feile. Dette er en obduksjon av én slik familie, og av testdisiplinen som lot den overleve i 38 utgivelser
Hvorfor tegner en PDF-renderer overhodet ingenting?
Fordi et mislykket ressursoppslag i en PDF-renderer er umulig å skille fra en tom side. Navneoperander i innholdsstrømmen og nøkler i ressursordboken er to forskjellige strengrom, og HotPDF sammenlignet på tvers av dem uten normalisering. Tokenizeren leser /Im0 og beholder soliduset, fordi det er det tokenet er; den lastede /Resources /XObject-ordboken lagrer nøkkelen som Im0, fordi parseren stripper skilletegnet når den bygger ordboknøkler. Hvert FindValue mot et operandnavn returnerte derfor -1. Skadeomfanget var bredere enn bilder. ISO 32000-1 §8.9 dekker Do, §8.4 dekker gs og dets /ExtGState-oppslag, §8.6 dekker cs og CS, og §8.7.4.3 dekker sh. Alle fem operatorene nøklet sin ressurs-underordbok med den rå operanden, så alle fem bommet. Navngitte fargerom falt tilbake til DeviceGray, noe som gjør 1 scn til hvit blekk på en hvit side. Bilde-XObjects ble aldri malt i det hele tatt — bitkart-bildeveien hadde i praksis aldri fungert siden den dagen den landet. Fiksen er en hjelpefunksjon på enhetsnivå brukt ved hvert operand-nøklet oppslag, som er den eneste måten å hindre at konvensjonen driver på nytt
// 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;
En andre, beslektet bom satt ett lag lenger ned. Rendereren hadde typede resolvere bare for strømmer og ordbøker, så en indirekte referanse som pekte på et array-objekt på toppnivå — det vanlige /CS0 5 0 R med [/Separation ...] i den andre enden — løste seg opp til nil gjennom begge og falt tilbake til den uløste lenken. Å legge til en generisk objektresolver fikset navngitte fargerom og funksjonsarrayer i ett trekk. Hvis du kobler opp skyggeordbøker, gjelder samme oppløsningsdisiplin for den aksiale og radiale skyggeveien, hvor /Function-oppføringen svært ofte er indirekte
Cm-operatoren og en sammenslåing skrevet baklengs
Den andre defekten plasserte bilder omtrent hundre tusen piksler utenfor siden, som ser akkurat ut som å ikke tegne dem. ISO 32000-1 §8.3.4 definerer PDF-transformasjoner med radvektorer, og cm-operatoren sammenslår sin operandmatrise M inn i den gjeldende transformasjonsmatrisen som M × CTM — M trer i kraft først, den eksisterende CTM-en etterpå. HotPDF setter sammen matriser gjennom HPDFMatMul(A, B), som anvender B før A. Det korrekte kallet sender derfor den gamle CTM-en som A. Den utsendte koden sendte operandmatrisen som A, noe som produserte CTM × M
Omvendt rekkefølge er ufarlig for en enkelt cm og katastrofalt for det standard to-stegs-idiomet. Plasser et bilde med 1 0 0 1 x y cm etterfulgt av w 0 0 h 0 0 cm, og den korrekte kaskaden skalerer enhetskvadratet med (w, h) og forskyver det deretter med (x, y). Under den omvendte kaskaden går forskyvningen inn først og skaleringen multipliserer den, så et bilde nominelt ved (60, 400) skalert til 200 ganger 120 lander på (12000, 48000). Klipp-testen øverst i blit-en avviser det, blit-en hoppes over, og ingenting noe sted rapporterer et 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)));
Det som gjør denne lærerik er at samme kildefil allerede inneholdt den korrekte rekkefølgen. /Matrix-oppføringen til et Form XObject hadde samme omvendte sammensetning, men Type 3-glyfveien og den innebygde glyf-konturveien fikk begge det riktig fra starten av, fordi glyfplassering kollapser synlig til origo når du inverterer den og noen allerede hadde blitt tvunget til å fikse den. To konvensjoner sameksisterte i én enhet i tre dusin utgivelser, hver korrekt i sin egen funksjon, og ingen reviewer la merke til det fordi ingen av kallstedene så feil ut isolert
Hva skjer når en tokenindeks er av med én?
Du får tolv operatorer som er syntaktisk håndtert og semantisk døde. Operand-aksessoren i rendereren er NumAt(Back), som leser Tokens[OpIndex - Back], og OpIndex er indeksen til selve operator-tokenet. En enkelt-operand-operator finner derfor sitt tall ved back 1. Tolv av dem var skrevet som NumAt(0), som leser operator-tokenet, feiler ctOperandNumber-typesjekken, og returnerer null-standardverdien. Listen er Tc, Tw, Tz, TL, Ts og Tr fra tekst-tilstandsoperatorene i ISO 32000-1 §9.3, pluss w, J, j, M, ri og i fra grafikk-tilstandsoperatorene i §8.4.3. Tegn- og ordavstand ble no-op-er, horisontal skalering ble aldri anvendt, ledelinjen forble på null slik at T* aldri avanserte en linje, tekstheving gjorde ingenting, rendermodus var alltid fyll, og hver strek i hvert dokument kom ut som en 1-piksel hårlinje uansett den erklærte linjebredden. Flere-operand-operatorer som m, rg og Tm brukte NumAt(1..6) og var alle korrekte, så en reviewer som skannet funksjonen så en vegg av plausibel indeksaritmetikk med tolv gale oppføringer innebygd i den
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)
Hvorfor forble testsuiten grønn i 38 versjoner?
Fordi assertionene var for svake til å skille en rendret side fra en delvis rendret en. Rendering-røyktestene assertet ting som at utdatabitkartet ikke er helt svart, eller at siden ikke er tom, eller at bildets digest er ikke-null. Hver av disse holder når tekst rendres og bilder ikke gjør det. Tekst tegnet fint, så bilderammebufferen var aldri uniform, digestet var aldri null, og suiten rapporterte suksess mens hele bildepipelinen var død kode i praksis. Svake assertions er forførende for grafikk nettopp fordi sterke ser skjøre ut. Ingen vil ha en test som knekker når en anti-aliasing-kant flytter seg én piksel, så den naturlige retretten er å asserte noe ingen rimelig endring kunne bryte — og den retretten lander deg på predikater ingen urimelig endring heller kan bryte. En separasjon-fargerom-test asserterte at utdataen var skillbar fra svart; grått på hvitt besto den, og det gjorde hvitt på hvitt også. Testen målte ikke om riktig farge ble malt. Den målte om noe som helst hadde skjedd på lerretet
Hvordan skriver du en renderingassertion som faktisk feiler?
Tell piksler av forventet farge, i forventet mengde, og la posisjon og størrelse falle ut fra tellingen. Erstatningsdisiplinen er en håndbygget minimal PDF, ett visuelt faktum per fil, og en assertion på hvor mange piksler som havner innenfor en toleranse av en spesifikk RGB-triplett. Et 200 ganger 120 bilde av rent rødt plassert ved en kjent forskyvning må produsere omtrent 24000 røde piksler. Hvis ressursoppslaget bommer, er tellingen 0. Hvis cm-kaskaden er omvendt, er tellingen 0. Hvis bildet rendres i feil fargerom, er tellingen 0. Ett tall fanger alle tre, og toleransebåndet absorberer anti-aliasing-støyen som fikk folk til å vike unna eksakt sammenligning i utgangspunktet
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');
Fire røyktester ble omskrevet på denne måten — en Type 4 fargeovergangstransform, en bilde-Do-plassering, et valgfritt-innhold-synlighetstilfelle og en Tr-strekmodus — og sammen avdekket de hele familien. Det er den egentlige lærdommen, og den generaliserer forbi denne kodebasen: i en renderingpipeline må assertionen navngi fargen. Alt mykere er en sjekk på at rendereren kjørte, ikke en sjekk på at den tegnet. Hvis du bygger din egen side-til-bitkart-testrigg, er rasteriserings-gjennomgangen for sider det naturlige stedet å bolte på en piksel-telle-hjelper til din første regresjon
Ærlige grenser
To grenser er verdt å si rett ut. Tekst-klippende rendermoduser 4 til 7 tegnes som sin basis-fyll- eller strekmodus, fordi rendereren ikke modellerer akkumulerte klippebaner fra glyfkonturer; dokumenter som er avhengige av tekst-formet klipping vil rendre teksten fremfor det klippede grafikkverket under. Og piksel-telle-disiplinen beskrevet her er en røyktest-teknikk, ikke en samsvarssuite — den beviser at et spesifikt visuelt faktum nådde bilderammebufferen, som er en mye lavere terskel enn å bevise at utdataen samsvarer med en referanse-rasterisering. Det er imidlertid nøyaktig den terskelen disse fire feilene ikke klarte å nå i tre år med utgivelser
Rendereren omtalt her leveres som en del av standard HotPDF Component for Delphi og C++Builder; produktsiden inneholder den fullstendige API-referansen for siderendering, inkludert bitkart-mellomlager og bakgrunns-forhåndshenting