Techninis straipsnis

PDF atvaizduotojas nieko nepiešia: keturios tylios Delphi klaidos

PDF atvaizduotojas, nieko nepiešiantis, dažniausiai iš viso neturi klaidos savo piešimo kode. HotPDF Component Delphi ir C++Builder aplinkoje keturi atskiri defektai vertė puslapius atvaizduoti tuščius, kai kiekviena žurnalo eilutė liko švari: vardo operandai su pirmaujančiu solidusu, apversta cm sujungimo tvarka ir žetono indeksas, kuris skaitydavo nulį. Nė vienas iš jų nekėlė klaidos. Nė vienas neregistravo pranešimo. Turinio srautas tokenizuotas teisingai, operatoriaus dispečeris atpažino kiekvieną operatorių, vaizdo XObject buvo dekoduotas į galiojantį bitmap'ą, o tada puslapis pasirodė tuščias. Šis derinys — konvejeris, praneša sėkmę kiekviename etape ir nesukuriantis nieko matomo — yra paieškos ar indekso, tyliai praleidžiančio, o ne žlungančio, požymis. Tai vienos tokios šeimos post-mortem'as, ir testų drausmės, leidusios jai išgyventi 38 leidimus

Kodėl PDF atvaizduotojas nieko visiškai nepiešia?

Todėl, kad nepavykusi resurso paieška PDF atvaizduotuve neatskiriama nuo tuščio puslapio. Turinio srauto vardo operandai ir resursų žodyno raktai yra dvi skirtingos eilučių erdvės, ir HotPDF jas lygino nenormalizuodamas. Tokenizatorius perskaito /Im0 ir palaiko solidusą, nes tai ir yra žetonas; įkeltas /Resources /XObject žodynas saugo raktą kaip Im0, nes nagrinėtuvas pašalina skirtuką kurdamas žodyno raktus. Kiekviena FindValue paieška pagal operando vardą todėl grąžindavo -1. Poveikio zona buvo platesnė nei vaizdai. ISO 32000-1 §8.9 apima Do, §8.4 apima gs ir jo /ExtGState paiešką, §8.6 apima cs ir CS, o §8.7.4.3 apima sh. Visi penki operatoriai raktavo savo resursų požodyną žaliuoju operandu, todėl visi penki nepataikė. Pavadintos spalvų erdvės grįždavo prie DeviceGray, kas paverčia 1 scn baltu rašalu ant balto puslapio. Vaizdo XObject'ai apskritai niekada nebuvo piešiami — bitmap vaizdo kelias praktiškai niekada neveikė nuo tos dienos, kai buvo pridėtas. Taisymas yra vienetinio lygio pagalbininkas, taikomas kiekvienoje operando raktuojamoje paieškoje, kas yra vienintelis būdas neleisti konvencijai vėl nuklysti

// 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;

Antra, susijusi praleista vieta buvo vienu sluoksniu žemiau. Atvaizduotuvas turėjo tipuotus rezoliucijos mechanizmus tik srautams ir žodynams, todėl netiesioginė nuoroda, rodanti į viršutinio lygio masyvo objektą — dažną /CS0 5 0 R su [/Separation ...] kitame gale — išsprendavo į nil per abu ir grįždavo prie neišspręstos nuorodos. Bendrinio objekto rezoliucijos pridėjimas vienu ėjimu ištaisė pavadintas spalvų erdves ir funkcijų masyvus. Jei jungiate šešėliavimo žodynus, ta pati rezoliucijos drausmė taikoma ašiniam ir radialiniam šešėliavimo keliui, kur /Function įrašas labai dažnai yra netiesioginis

cm operatorius ir atgaline tvarka parašytas sujungimas

Antrasis defektas dėjo vaizdus maždaug šimtu tūkstančių pikselių už puslapio ribų, kas atrodo lygiai kaip jų nepiešimas. ISO 32000-1 §8.3.4 apibrėžia PDF transformacijas eilutės vektorių konvencija, o cm operatorius sujungia savo operando matricą M su dabartine transformacijos matrica kaip M × CTM — M įsigalioja pirma, po to esama CTM. HotPDF sudaro matricas per HPDFMatMul(A, B), kuri pritaiko B prieš A. Todėl teisingas iškvietimas turi perduoti seną CTM kaip A. Pristatytas kodas perdavė operando matricą kaip A, sukurdamas CTM × M

Apversta tvarka nekenksminga vienam cm ir katastrofiška standartinei dviejų žingsnių idiomai. Padėkite vaizdą su 1 0 0 1 x y cm, po kurio seka w 0 0 h 0 0 cm, ir teisinga kaskada mastelio pavieneto kvadratą (w, h), tada jį perkelia (x, y). Apverstoje kaskadoje perkėlimas įeina pirmas, o mastelio faktorius jį padaugina, todėl vaizdas, nominaliai esantis ties (60, 400), pakeistas iki 200 per 120, atsiduria ties (12000, 48000). Kirpimo (clip) patikra piešimo pradžioje jį atmeta, piešimas praleidžiamas, o niekas niekur nepraneša apie 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)));

Kas šį atvejį daro pamokantį, tai kad tas pats šaltinio failas jau turėjo teisingą tvarką. /Matrix įrašas Form XObject turėjo tą pačią apverstą kompoziciją, tačiau Type 3 glifo kelias ir įdėto glifo kontūro kelias abu nuo pat pradžių tai padarė teisingai, nes glifo pozicionavimas matomai sugriūva į pradžios tašką, kai jį invertuoji, ir kažkas jau buvo priverstas tai pataisyti. Dvi konvencijos sugyveno viename vienete tris dešimtis leidimų, kiekviena teisinga savo funkcijoje, ir joks recenzentas nepastebėjo, nes nė vienas iškvietimas neatrodė neteisingas atskirai

Kas nutinka, kai žetono indeksas paklysta vienetu?

Gauna dvylika operatorių, kurie sintaksiškai tvarkomi ir semantiškai negyvi. Operando prieigos mechanizmas atvaizduotuve yra NumAt(Back), skaitantis Tokens[OpIndex - Back], o OpIndex yra paties operatoriaus žetono indeksas. Vieno operando operatorius todėl randa savo skaičių ties back 1. Dvylika iš jų buvo parašyti kaip NumAt(0), kuris skaito operatoriaus žetoną, nepraeina ctOperandNumber tipo patikros ir grąžina nulinę numatytąją reikšmę. Sąrašas yra Tc, Tw, Tz, TL, Ts ir Tr iš teksto būsenos operatorių ISO 32000-1 §9.3, plius w, J, j, M, ri ir i iš grafikos būsenos operatorių §8.4.3. Simbolių ir žodžių tarpai tapo niekiniais veiksmais, horizontalus mastelis niekada netaikytas, tarpelinimas liko nulis, todėl T* niekada nepasitraukdavo eilute, teksto pakėlimas nieko nedarė, atvaizdavimo režimas visada buvo užpildymas, o kiekvienas štrichas kiekviename dokumente pasirodydavo kaip 1 pikselio plaukas nepriklausomai nuo deklaruoto linijos pločio. Kelių operandų operatoriai, tokie kaip m, rg ir Tm, naudojo NumAt(1..6) ir buvo visi teisingi, todėl recenzentas, peržiūrėdamas funkciją, matė sieną tikėtinos indekso aritmetikos su dvylika neteisingų įrašų, įterptų į ją

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)

Kodėl testų rinkinys liko žalias 38 leidimus?

Todėl, kad assert'ai buvo per silpni, kad atskirtų atvaizduotą puslapį nuo iš dalies atvaizduoto. Atvaizdavimo smoke testai teigė dalykus, pvz., kad išvesties bitmap'as nėra visiškai juodas, arba kad puslapis nėra tuščias, arba kad vaizdo santrauka nėra nulinė. Kiekvienas iš jų galioja, kai tekstas atvaizduojamas, o vaizdai ne. Tekstas piešėsi puikiai, todėl kadrų buferis niekada nebuvo vienalytis, santrauka niekada nebuvo nulinė, o rinkinys pranešdavo sėkmę, kol visas vaizdo konvejeris praktiškai buvo negyvas kodas. Silpni assert'ai yra viliojantys grafikai būtent dėl to. Niekas nenori testo, kuris lūžta, kai anti-aliasingo kraštas pasislenka vienu pikseliu, todėl natūralus atsitraukimas yra teigti kažką, ko joks pagrįstas pakeitimas nepažeistų — ir tas atsitraukimas nuveda jus į predikatus, kurių nepažeidžia ir joks nepagrįstas pakeitimas. Separacijos spalvų erdvės testas teigė, kad išvestis atskiriama nuo juodos; pilka ant baltos jį praėjo, kaip ir balta ant baltos. Testas nematavo, ar buvo nupiešta teisinga spalva. Jis matavo, ar apskritai kažkas atsitiko ant drobės

Kaip parašyti atvaizdavimo assert'ą, kuris iš tikrųjų žlunga?

Skaičiuokite pikselius laukiamos spalvos, laukiamu kiekiu, ir leiskite pozicijai bei dydžiui iškristi iš skaičiaus. Pakeitimo drausmė yra ranka sukurtas minimalus PDF, vienas vizualinis faktas failui ir assert'as apie tai, kiek pikselių patenka į toleranciją nuo konkretaus RGB trigubo. 200 per 120 grynai raudonas vaizdas, padėtas žinomame poslinkyje, turi sukurti apytiksliai 24000 raudonų pikselių. Jei resurso paieška nepataiko, skaičius yra 0. Jei cm kaskada apversta, skaičius yra 0. Jei vaizdas atvaizduojamas neteisinga spalvų erdve, skaičius yra 0. Vienas skaičius sugauna visus tris, o tolerancijos juosta absorbuoja anti-aliasingo triukšmą, kuris privertė žmones vengti tikslaus palyginimo

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');

Keturi smoke testai buvo perrašyti šiuo būdu — Type 4 atspalvio transformacija, vaizdo Do pozicionavimas, pasirenkamo turinio matomumo atvejis ir Tr štricho režimas — ir kartu jie atskleidė visą šeimą. Tai tikroji pamoka, ir ji apibendrinama toli už šios kodo bazės ribų: atvaizdavimo konvejeryje assert'as turi įvardyti spalvą. Bet kas švelniau yra patikra, kad atvaizduotuvas veikė, ne kad jis nupiešė. Jei kuriate savo puslapio-į-bitmap infrastruktūrą, puslapio rasterizavimo apžvalga yra natūrali vieta prisukti pikselių skaičiavimo pagalbininką prie pirmos savo regresijos

Sąžiningos ribos

Dvi ribas verta pasakyti aiškiai. Teksto kirpimo atvaizdavimo režimai 4-7 piešiami kaip jų bazinis užpildymo ar štricho režimas, nes atvaizduotuvas nemodeliuoja sukaupto kirpimo kelio iš glifų kontūrų; dokumentai, priklausantys nuo teksto formos kirpimo, atvaizduos tekstą, o ne po juo esantį kirptą darbą. O pikselių skaičiavimo drausmė, aprašyta čia, yra smoke testo technika, ne atitikties rinkinys — ji įrodo, kad konkretus vizualinis faktas pasiekė kadrų buferį, kas yra daug žemesnė kartelė, nei įrodyti, kad išvestis atitinka etaloninį rasterizatorių. Tačiau tai lygiai ta kartelė, kurios šios keturios klaidos nepasiekė trejus leidimų metus

Čia aptartas atvaizduotuvas pristatomas standartiniame HotPDF Component, skirtame Delphi ir C++Builder; produkto puslapyje pateikiama pilna puslapio atvaizdavimo API nuoroda, įskaitant bitmap podėlio ir foninio išankstinio gavimo iškvietimus