Technical Article

EndDoc bag koji je tiho onemogućio podskupove fontova

Generišite izveštaj, ugradite TrueType font, i izlazni dokument se otvara ispravno u svakom pregledaču koji probate. Glifovi su tačni, tekst se može selektovati, datoteka je validna. Jedina stvar koja nije u redu je veličina. Dokument koji koristi nekoliko desetina latiničnih karaktera nosi ceo font od 350 KB. Dokument koji je odštampao pasus na kineskom jeziku nosi CJK font od 14 MB umesto dela od pola megabajta koji bi mu trebao. Nije podignut nikakav izuzetak, nikakvo upozorenje nije zabeleženo, a datoteka je prošla validaciju. Ovako spolja izgleda pogrešan redosled koraka finalizacije: ništa ne otkazuje, a jedini dokaz je broj koji je prevelik.

Bag koji ga je proizveo živeo je u HotPDF-u tokom jedne linije izdanja i u međuvremenu je ispravljen. Vredi ga opisati ne kao obaveštenje o defektu, već kao lekciju, jer je oblik ove greške opšti. Svaki dokument-endžin ima fazu finalizacije koja menja objekte neposredno pre nego što ih upiše, a ispravnost te faze u potpunosti zavisi od redosleda njenih koraka u odnosu na serijalizaciju. Postavite jedan korak na pogrešnu stranu upisa i on neće uraditi ništa, tiho.

Šta podskup fontova zapravo treba da radi

Podskup fonta (subset) je deo TrueType datoteke koji dokument stvarno koristi. ISO 32000-1 §9.9 opisuje kako ugrađeni program fonta ide u toku (stream) na koji upućuje deskriptor fonta, a za TrueType program taj tok je /FontFile2 sa /Length1 koji daje nekompresovani broj bajtova. Pravljenje podskupa ponovo ispisuje tabele glyf i loca tako da sadrže samo glifove na koje dokument upućuje, ponovo numeriše identifikatore glifova i dodaje prefiks nazivu /BaseFont u vidu šestoslovnog taga kao što je ABCDEF+ da bi označio font kao podskup, tačno onako kako specifikacija zahteva. Latinični font koji se svede na deset ili petnaest kilobajta čini razliku između laganog PDF-a i onog koji isporučuje ceo font radi jednog naslova.

Trenutak u kojem se to dešava je važan. Pravljenje podskupa nije transformacija koju primenjujete na bajtove koji su već na disku. Ono menja graf objekata u memoriji: smanjuje sadržaj toka /FontFile2, ispravlja /Length1 i ponovo ispisuje string /BaseFont. Sve to mora biti na svom mestu kada serijalizator prolazi kroz graf i emituje bajtove. Ako ove izmene stignu nakon što su bajtovi upisani, one će ažurirati objekte koje niko nikada neće čitati.

Simptom i zašto se niko nije žalio

Prijavljeno ponašanje bilo je prisustvo kompletnih fontova u izlazu bez ikakve dijagnostike. Korisnik koji je registrovao Unicode TrueType font i proizveo normalan dokument objekt je bio iste dužine kao i izvorna .ttf datoteka, kao i da naziv /BaseFont nije nosio šestoslovni prefiks podskupa. Izlaz se nikada nije smanjivao između pokretanja koja su koristila deset glifova i onih koja su koristila deset hiljada.

Odsustvo bilo kakve greške je deo koji ovu klasu bagova čini skupom. Rutina za pravljenje podskupa koja se pokrene u pogrešno vreme i dalje se izvršava. Ona prolazi kroz akumulirano korišćenje kodnih tačaka, gradi savršeno ispravan podskup i primenjuje ga na graf objekata u memoriji. Interno, posao je završen i poziv se uspešno vraća. Jedina stvar koja nije u redu je to što graf objekata koji je modifikovan više nije onaj koji se upisuje, jer je upisivač već završio posao. Sa tačke gledišta pozivaoca, dokument je proizveden i sačuvan bez ikakvih problema, što je upravo utisak koji ostavlja tihi neuspeh.

Korenski uzrok bio je redosled finalizacije

U HotPDF-u se završni radovi obavljaju unutar EndDoc. Korak pravljenja podskupa je interna rutina pod nazivom BuildAndApplyUnicodeFontSubset. Ona čita skup korišćenih kodnih tačaka po dokumentu, koji se čuva u bitmapi koju putanja za emitovanje teksta popunjava kako se glifovi prikazuju, preslikava svaku korišćenu kodnu tačku kroz keširanu tabelu kodnih tačaka u glifove na stvarni identifikator glifa i ponovo ispisuje program fonta oko tog zatvaranja. Kada se Unicode TrueType font registruje, putanja emitovanja postavlja bit u skupu korišćenih kodnih tačaka za svaki karakter koji iscrtava, tako da do trenutka zatvaranja dokumenta endžin tačno zna koje glifove podskup mora da zadrži.

Defekt je bio u tome što je BuildAndApplyUnicodeFontSubset bio pozivan nakon što je SaveToStream ili SaveToFile već serijalizovao dokument. Izmene podskupa na /FontFile2, njegov ispravljeni /Length1 i sam naziv /BaseFont pretrpeli su izmenu na grafu objekata koji je već bio pretvoren u bajtove. Rešenje je bilo promena redosleda u jednoj liniji: pomeranje poziva za pravljenje podskupa ispred serijalizacije, tako da upisivač emituje podskup fonta umesto originalnog. Ispravljeni redosled prvo pokreće pravljenje podskupa, a zatim vrši serijalizaciju.

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
    Pdf.CurrentPage.TextOut(72, 760, 0, '报表标题 Report Heading');
    Pdf.EndDoc;                 // subsetting runs here, before the write
    Pdf.SaveToFile('Report.pdf');
  finally
    Pdf.Free;
  end;
end;

Sa ispravljenim redosledom, ništa se ne menja u pozivnom kodu. Pravljenje podskupa je podrazumevano uključeno čim se Unicode TrueType font registruje. Registrujete font, započnete dokument, iscrtate i završite ga, a podskup se gradi od glifova koje ste koristili pre nego što bajtovi napuste memoriju.

Zašto je jedan pogrešno postavljen korak cela kategorija

Razlog zašto je ovo vredno lekcije, a ne samo fusnote, jeste to što EndDoc emituje listu završnih koraka, a svaki od njih je osetljiv na svoju poziciju u odnosu na upis. Pravljenje podskupa fonta je jedan od njih. PDF/A izlaz zahteva tok /CIDSet koji nabraja tačno identifikatore glifova prisutnih u podskupu, što je ograničenje koje ISO 19005 nameće kako bi validator mogao da potvrdi da se ugrađeni program poklapa sa onim što deskriptor fonta tvrdi; taj tok se emituje u istom prozoru finalizacije i zavisi od toga da li je podskup prvo napravljen. PDF/UA-1 zahteva, prema ISO 14289-1 §7.18.3, da svaka stranica koja nosi anotaciju deklariše /Tabs sa vrednošću /S, a interna rutina pod nazivom EnsurePDFUATabsOnAnnotatedPages upisuje taj ključ tokom iste faze. Provere izlazne namere se takođe pokreću tu.

Ista greška u redosledu koja je onemogućila podskupove takođe je odbacila PDF/UA ključ redosleda tabulatora na stranicama sa anotacijama, jer se taj korak nalazio na istoj pogrešnoj strani upisa. veraPDF i PAC prijavljuju nedostajući /Tabs /S kao kršenje kontrolne tačke 21-001 Matterhorn protokola. Dakle, jedan pogrešno postavljen poziv nije samo povećao veličinu datoteke; on je istovremeno tiho prekršio zahtev za usklađenost pristupačnosti, uz isto odsustvo bilo kakve greške. To je opasnost faze finalizacije: njeni koraci dele preduslov, i jedna greška u redosledu može ukloniti nekoliko njih odjednom, dok svaki poziv i dalje vraća uspeh.

Kako se tihi neuspeh emitovanja zapravo hvata

Bag koji ne podiže izuzetak se ne hvata pokretanjem programa. On se hvata pregledom izlaza i poređenjem sa onim što je ulaz trebalo da proizvede. Za podskupove fontova provere su konkretne. Uporedite veličinu izlazne datoteke sa grubim očekivanjem: dokument koji je dotakao nekoliko glifova ne bi trebalo da ima veličinu kompletnog fonta. Otvorite ugrađeni objekat fonta i pročitajte njegovu dužinu u bajtovima; podskup /FontFile2 za latinični font je mali deo izvorne datoteke. Pročitajte naziv /BaseFont i potvrdite da je prisutan šestoslovni prefiks, jer je njegovo odsustvo direktan znak da nikakav podskup nije primenjen.

var
  Pdf: THotPDF;
  Output: TMemoryStream;
begin
  Output := TMemoryStream.Create;
  try
    Pdf := THotPDF.Create(nil);
    try
      Pdf.RegisterUnicodeTTF('C:\Fonts\DejaVuSans.ttf');
      Pdf.BeginDoc;
      Pdf.CurrentPage.SetFont('DejaVu Sans', [], 11);
      Pdf.CurrentPage.TextOut(72, 760, 0, 'Subset me');
      Pdf.EndDoc;
      Pdf.SaveToStream(Output);
    finally
      Pdf.Free;
    end;
    // A few glyphs from a ~700 KB face must not yield a multi-hundred-KB stream.
    if Output.Size > 100 * 1024 then
      raise Exception.Create('Font subset did not shrink the output');
  finally
    Output.Free;
  end;
end;

Za PDF/A izlaz provera je još stroža, jer validator radi posao umesto vas. Podesite nivo usklađenosti i propustite rezultat kroz veraPDF: nedostajući /CIDSet, ili podskup koji se ne poklapa sa deskriptorom, prijavljuje se kao neuspela klauzula umesto da to sami primećujete. Prekidači usklađenosti koji pokreću ovaj rad na finalizaciji su svojstva na dokumentu. PDFACompliance uzima string kao što je '2B' za PDF/A-2 nivo B, a PDFUACompliance je logička vrednost (boolean) koja uključuje zahteve za tagovani PDF i redosled tabulatora.

Pdf := THotPDF.Create(nil);
try
  Pdf.PDFACompliance := '2B';     // PDF/A-2 Level B, drives /CIDSet emission
  Pdf.PDFUACompliance := True;    // stamps /Tabs /S on annotated pages
  Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
  Pdf.BeginDoc;
  Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
  Pdf.CurrentPage.TextOut(72, 760, 0, '合规报告');
  Pdf.EndDoc;
  Pdf.SaveToFile('Report_PDFA.pdf');
finally
  Pdf.Free;
end;

Inženjerska lekcija

Iz ovoga proizilaze dva pravila. Prvo je da svaki korak finalizacije koji menja objekte mora da se izvrši pre nego što se ti objekti serijalizuju, a završna faza dokument-endžina treba da se posmatra kao uređeni cevovod (pipeline) gde je serijalizacija poslednja akcija, a ne jedna od nekoliko akcija. Drugo pravilo je ono koje je ovde koštalo najviše vremena: za korak emitovanja, odsustvo greške nije dokaz uspeha. Rutina koja gradi ispravan podskup i primenjuje ga na pogrešan, već upisan graf ne prijavljuje ništa pogrešno, jer sa njene tačke gledišta ništa nije ni bilo pogrešno. Verifikacija mora da posmatra artefakt, a ne povratni kod. Proverite veličinu izlaza, pročitajte dužinu u bajtovima ugrađenog fonta i njegov prefiks /BaseFont, i prepustite da veraPDF oceni PDF/A izlaz gde nedostajući /CIDSet pretvara tihi nedostatak u imenovani neuspeh.

Proizvođačka strana rukovanja fontovima, kako se registruju i ugrađuju za izlaz izveštaja, pokrivena je u našem članku o fontovima i slikama u izlazu izveštaja. Strana validacije, gde se ovi koraci finalizacije proveravaju u odnosu na standarde, pokrivena je u vodiču kroz PDF/A i PDF/UA validaciju. Oba se povezuju sa pravljenjem podskupova i radom na usklađenosti koji su ovde opisani, a koji se isporučuju kao deo HotPDF komponente za Delphi i C++Builder zajedno sa API-jima za učitavanje, uređivanje, šifrovanje i potpisivanje koji su pokriveni na drugim mestima na ovom blogu.