PDF-renderöijällä, joka ei piirrä mitään, ei yleensä ole yhtään bugia itse piirtokoodissaan. HotPDF-komponentissa Delphille ja C++Builderille neljä erillistä vikaa sai sivut renderöitymään tyhjinä samalla kun jokainen lokirivi pysyi puhtaana: nimioperandit, jotka kantoivat johtavaa kauttaviivaa, käänteinen cm-yhdistäminen, ja token-indeksi, joka luki nollaa. Yksikään niistä ei heittänyt poikkeusta. Yksikään niistä ei kirjannut lokia. Sisältövirta tokenisoitui oikein, operaattorijakaja tunnisti jokaisen operaattorin, kuva-XObject dekoodautui päteväksi bittikartaksi, ja sitten sivu tuli ulos tyhjänä. Tuo yhdistelmä — putki, joka raportoi onnistumisen jokaisessa vaiheessa ja tuottaa mitään näkyvää — on merkki hausta tai indeksistä, joka epäonnistuu hiljaa sen sijaan, että se epäonnistuisi äänekkäästi. Tämä on jälkiruumiinavaus yhdestä tällaisesta perheestä, ja siitä testikurista, joka antoi sen selvitä 38 julkaisun ajan
Miksi PDF-renderöijä ei piirrä yhtään mitään?
Koska epäonnistunut resurssihaku PDF-renderöijässä on erottamaton tyhjästä sivusta. Sisältövirran nimioperandit ja resurssisanakirjan avaimet ovat kaksi eri merkkijonoavaruutta, ja HotPDF vertaili niitä keskenään ilman normalisointia. Tokenisoija lukee /Im0 ja pitää kauttaviivan, koska se on juuri se, mitä token on; ladattu /Resources /XObject-sanakirja tallentaa avaimen muodossa Im0, koska jäsentäjä poistaa erottimen rakentaessaan sanakirjan avaimia. Jokainen FindValue-kutsu operandin nimeä vasten palautti siis -1:n. Räjähdyssäde oli laajempi kuin kuvat. ISO 32000-1 §8.9 kattaa Do:n, §8.4 kattaa gs:n ja sen /ExtGState-haun, §8.6 kattaa cs:n ja CS:n, ja §8.7.4.3 kattaa sh:n. Kaikki viisi operaattoria avainsivat resurssialisanakirjansa raa'alla operandilla, joten kaikki viisi epäonnistuivat. Nimetyt väriavaruudet palasivat DeviceGrayyn, mikä muuttaa 1 scn:n valkoiseksi musteeksi valkoisella sivulla. Kuva-XObjecteja ei koskaan maalattu ollenkaan — bittikarttakuvan polku ei käytännössä ollut koskaan toiminut siitä päivästä lähtien, kun se laskeutui koodikantaan. Korjaus on yksikkötason apufunktio, jota sovelletaan jokaisessa operandiavaimisessa haussa, mikä on ainoa tapa estää käytännön ajautumasta pois raiteiltaan uudelleen
// 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;
Toinen, siihen liittyvä puute istui kerroksen alempana. Renderöijällä oli tyypitetyt ratkaisijat vain virroille ja sanakirjoille, joten epäsuora viittaus, joka osoitti ylimmän tason taulukko-objektiin — yleinen /CS0 5 0 R, jonka toisessa päässä on [/Separation ...] — ratkesi nil-arvoksi molempien kautta ja palasi ratkaisemattomaan linkkiin. Geneerisen objektiratkaisijan lisääminen korjasi nimetyt väriavaruudet ja funktiotaulukot yhdellä liikkeellä. Jos kytket varjostussanakirjoja, sama ratkaisukuri pätee akselisuuntaiseen ja säteittäiseen varjostuspolkuun, jossa /Function-merkintä on hyvin usein epäsuora
cm-operaattori ja takaperin kirjoitettu yhdistäminen
Toinen vika sijoitti kuvat suunnilleen sata tuhatta pikseliä sivun ulkopuolelle, mikä näyttää täsmälleen siltä, ettei niitä piirretty ollenkaan. ISO 32000-1 §8.3.4 määrittelee PDF-muunnokset rivivektoreilla, ja cm-operaattori yhdistää operandimatriisinsa M nykyiseen muunnosmatriisiin muodossa M × CTM — M vaikuttaa ensin, olemassa oleva CTM sen jälkeen. HotPDF koostaa matriiseja funktiolla HPDFMatMul(A, B), joka soveltaa B:tä ennen A:ta. Oikean kutsun täytyy siis antaa vanha CTM A:na. Toimitettu koodi antoi operandimatriisin A:na, tuottaen CTM × M:n
Käänteinen järjestys on harmiton yksittäiselle cm:lle ja katastrofaalinen standardille kaksivaiheiselle idiomille. Sijoita kuva komennolla 1 0 0 1 x y cm jota seuraa w 0 0 h 0 0 cm, ja oikea vesiputous skaalaa yksikköneliön arvoilla (w, h) ja sitten siirtää sen arvoilla (x, y). Käänteisen vesiputouksen alla siirto menee ensin sisään ja skaalaus kertoo sen, joten kuva, joka on nimellisesti kohdassa (60, 400) skaalattuna arvoihin 200 kertaa 120, laskeutuu kohtaan (12000, 48000). Blitin yläosassa oleva leikkaustesti hylkää sen, blitti ohitetaan, eikä mikään missään raportoi ongelmaa
// 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)));
Se, mikä tekee tästä opettavaisen, on se, että sama lähdetiedosto sisälsi jo oikean järjestyksen. Form XObjectin /Matrix-merkinnällä oli sama käänteinen koostaminen, mutta Type 3 -glyfipolku ja upotettu glyfin ääriviivapolku saivat sen molemmat oikein alusta lähtien, koska glyfin sijoittelu romahtaa näkyvästi origoon, kun sen kääntää, ja joku oli jo joutunut korjaamaan sen. Kaksi käytäntöä elivät rinnakkain samassa yksikössä kolmen tusinan julkaisun ajan, kumpikin oikein omassa funktiossaan, eikä yksikään tarkastaja huomannut, koska kumpikaan kutsupaikka ei näyttänyt väärältä eristettynä
Mitä tapahtuu, kun token-indeksi on yhden verran väärässä?
Saat kaksitoista operaattoria, jotka käsitellään syntaktisesti mutta jotka ovat semanttisesti kuolleita. Renderöijän operandinlukija on NumAt(Back), joka lukee Tokens[OpIndex - Back], ja OpIndex on itse operaattoritokenin indeksi. Yhden operandin operaattori löytää siis numeronsa takaisinpäin arvolla 1. Kaksitoista niistä kirjoitettiin muodossa NumAt(0), mikä lukee operaattoritokenin, epäonnistuu ctOperandNumber-tyyppitarkistuksessa, ja palauttaa nollaoletuksen. Luettelo on Tc, Tw, Tz, TL, Ts ja Tr ISO 32000-1 §9.3:n tekstitilaoperaattoreista, sekä w, J, j, M, ri ja i §8.4.3:n grafiikkatilaoperaattoreista. Merkki- ja sanavälistys muuttuivat no-opeiksi, vaakasuuntainen skaalaus ei koskaan soveltunut, riviväli pysyi nollassa, joten T* ei koskaan edennyt riviä, tekstin nosto ei tehnyt mitään, renderöintitila oli aina täyttö, ja jokainen viiva jokaisessa dokumentissa tuli ulos yhden pikselin hiusviivana ilmoitetusta viivanleveydestä riippumatta. Moniperandiset operaattorit kuten m, rg ja Tm käyttivät NumAt(1..6):ta ja olivat kaikki oikein, joten funktiota tarkasteleva tarkastaja näki seinän uskottavaa indeksiaritmetiikkaa, jonka sisään oli upotettu kaksitoista väärää merkintää
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)
Miksi testisarja pysyi vihreänä 38 version ajan?
Koska väitteet olivat liian heikkoja erottamaan renderöidyn sivun osittain renderöidystä. Renderöinnin savutestit väittivät asioita kuten tulostebittikartta ei ole täysin musta, tai sivu ei ole tyhjä, tai kuvan tiiviste ei ole nolla. Jokainen niistä pätee, kun teksti renderöityy ja kuvat eivät. Teksti piirtyi hyvin, joten kehyspuskuri ei koskaan ollut yhtenäinen, tiiviste ei koskaan ollut nolla, ja sarja raportoi onnistumisen samalla kun koko kuvaputki oli käytännössä kuollutta koodia. Heikot väitteet ovat houkuttelevia grafiikalle juuri siksi, että vahvat näyttävät hauraalta. Kukaan ei halua testiä, joka rikkoutuu, kun reunanpehmennysreuna siirtyy yhdellä pikselillä, joten luonnollinen vetäytyminen on väittää jotain, mitä mikään järkevä muutos ei voisi rikkoa — ja tuo vetäytyminen laskeutuu predikaatteihin, joita mikään epäjärkeväkään muutos ei voi rikkoa. Erotusväriavaruustesti väitti, että tuloste on erotettavissa mustasta; harmaa valkoisella läpäisi sen, ja niin läpäisi valkoinen valkoisellakin. Testi ei mitannut, maalattiinko oikea väri. Se mittasi, tapahtuiko kankaalla ylipäätään mitään
Miten kirjoitat renderöintiväitteen, joka todella epäonnistuu?
Laske odotetun värin pikseleitä, odotetussa määrässä, ja anna sijainnin ja koon selvitä laskennasta. Korvaava kuri on käsin rakennettu minimaalinen PDF, yksi visuaalinen tosiasia tiedostoa kohti, ja väite siitä, kuinka monta pikseliä osuu toleranssin sisään tietystä RGB-kolmikosta. 200 kertaa 120:n kokoisen puhtaan punaisen kuvan, joka on sijoitettu tunnettuun siirtymään, täytyy tuottaa suunnilleen 24000 punaista pikseliä. Jos resurssihaku epäonnistuu, määrä on 0. Jos cm-vesiputous on käänteinen, määrä on 0. Jos kuva renderöityy väärässä väriavaruudessa, määrä on 0. Yksi luku nappaa kaikki kolme, ja toleranssikaista imee reunanpehmennyskohinan, joka sai ihmiset karttamaan tarkkaa vertailua alun perin
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');
Neljä savutestiä kirjoitettiin uudelleen tällä tavalla — Type 4 -sävymuunnos, kuvan Do-sijoittelu, valinnaisen sisällön näkyvyystapaus ja Tr-viivatila — ja yhdessä ne paljastivat koko perheen. Se on todellinen opetus, ja se yleistyy tämän koodikannan ulkopuolelle: renderöintiputkessa väitteen täytyy nimetä väri. Kaikki pehmeämpi on tarkistus, että renderöijä ajoi, ei tarkistus, että se piirsi. Jos rakennat omaa sivu-bittikartta-testijärjestelmääsi, sivun rasterointiläpikäynti on luonnollinen paikka pultata pikselilaskuri-apuri ensimmäiseen regressioosi
Rehelliset rajat
Kaksi rajaa kannattaa todeta suoraan. Tekstin leikkausrenderöintitilat 4–7 piirretään niiden perustäyttö- tai viivatilana, koska renderöijä ei mallinna kertyneitä leikkauspolkuja glyfien ääriviivoista; dokumentit, jotka luottavat tekstin muotoiseen leikkaukseen, renderöivät tekstin sen alla olevan leikatun taideteoksen sijaan. Ja tässä kuvattu pikselilaskukuri on savutestitekniikka, ei vaatimustenmukaisuussarja — se todistaa, että tietty visuaalinen tosiasia saavutti kehyspuskurin, mikä on paljon matalampi rima kuin todistaa, että tuloste täsmää vertailurasteroijaan. Se on kuitenkin täsmälleen se rima, jonka nämä neljä bugia epäonnistuivat ylittämään kolmen vuoden julkaisujen ajan
Tässä käsitelty renderöijä toimitetaan osana vakiota HotPDF-komponenttia Delphille ja C++Builderille; tuotesivu sisältää täydellisen sivunrenderöinnin API-viitteen, mukaan lukien bittikarttavälimuistin ja taustaesihakukutsupisteet