Tehnički članak

EndDoc bug koji je tiho onemogućio podskupove fontova

Generirajte izvješće, ugradite TrueType font i izlaz se ispravno otvara u svakom pregledniku koji isprobate. Glifovi su ispravni, tekst se može odabrati, datoteka je valjana. Jedino što nije u redu je veličina. Dokument koji je koristio nekoliko desetaka latiničnih znakova nosi cijeli font od 350 KB. Dokument koji je ispisao odlomak kineskog nosi 14 MB CJK font umjesto dijela od pola megabajta koji bi trebao. Nije pokrenuta iznimka, nije zabilježeno upozorenje, a datoteka je prošla validaciju. Ovako izvana izgleda pogrešno poredan korak finalizacije: ništa ne zakazuje, a jedini dokaz je prevelik broj

Bug koji ga je proizveo živio je u HotPDF-u tijekom jedne linije izdanja i u međuvremenu je ispravljen. Vrijedi ga opisati ne kao obavijest o kvaru, već kao lekciju, jer je oblik pogreške općenit. Svaki dokumentni pogon ima fazu finalizacije koja mijenja objekte neposredno prije nego što ih zapiše, a ispravnost te faze u potpunosti ovisi o redoslijedu njezinih koraka u odnosu na serijalizaciju. Stavite jedan korak na pogrešnu stranu pisanja i on ne čini ništa, tiho

Što bi podskupovi fontova trebali raditi

Podskup fonta je dio TrueType datoteke koji dokument stvarno koristi. ISO 32000-1 §9.9 opisuje kako ugrađeni program fonta putuje u toku na koji upućuje deskriptor fonta, a za TrueType program taj tok je /FontFile2 s /Length1 koji daje nekomprimirani broj bajtova. Stvaranje podskupa ponovno zapisuje tablice glyf i loca tako da sadrže samo glifove na koje se dokument odnosi, ponovno numerira identifikatore glifova i dodaje prefiks nazivu /BaseFont sa šesteroslovnom oznakom kao što je ABCDEF+ kako bi se font označio kao podskup, točno onako kako specifikacija zahtijeva. Latinično pismo koje se svodi na deset ili petnaest kilobajta razlika je između vitkog PDF-a i onog koji isporučuje cijelo pismo radi jednog naslova

Trenutak u kojem se to događa je važan. Stvaranje podskupa nije transformacija koju primjenjujete na bajtove koji su već na disku. Ono uređuje graf objekata u memoriji: smanjuje sadržaj toka /FontFile2, ispravlja /Length1 i ponovno zapisuje niz /BaseFont. Sve to mora biti na svom mjestu kada serijalizator prolazi grafom i emitira bajtove. Ako izmjene slete nakon što su bajtovi zapisani, one ažuriraju objekte koje nitko nikada neće pročitati

HotPDF: podskupljanje fontova provlači potpuni TrueType font kroz zatvaranje korištenih glifova i prepisuje glyf i loca prije ažuriranja objekata FontFile2, Length1 i BaseFont u memoriji
Subsetting glyf i loca obnavlja oko glifova koje dokument stvarno koristi, a zatim u memoriji ažurira /FontFile2, /Length1 i prefiks /BaseFont. Sve to mora biti na mjestu prije nego se napiše ijedan bajt

Simptom i zašto se ništa nije žalilo

Prijavljeno ponašanje bilo je prisutnost punih fontova u izlazu bez dijagnostike. Korisnik koji je registrirao Unicode TrueType font i proizveo normalan dokument otkrio je da je objekt ugrađenog fonta iste duljine kao i izvorna .ttf datoteka te da naziv /BaseFont nije nosio šesteroslovni prefiks podskupa. Izlaz se nikada nije smanjio između pokretanja koja su koristila deset glifova i onih koja su koristila deset tisuća

Odsutnost bilo kakve pogreške je dio koji ovu klasu bugova čini skupom. Rutina za stvaranje podskupova koja se pokreće u pogrešno vrijeme i dalje se izvodi. Ona prolazi kroz akumuliranu upotrebu kodnih točaka, gradi savršeno ispravan podskup i primjenjuje ga na graf objekata u memoriji. Interno je posao obavljen i poziv se vraća čist. Jedino što nije u redu je to što graf objekata koji je uredila više nije stvar koja se zapisuje, jer je pisac već završio. S točke gledišta pozivatelja, dokument je proizveden i spremljen bez incidenata, što je upravo dojam koji ostavlja tiho zatajenje

Korijenski uzrok bio je redoslijed finalizacije

U HotPDF-u se završni rad odvija unutar EndDoc. Korak stvaranja podskupova je interna rutina nazvana BuildAndApplyUnicodeFontSubset. Ona čita skup korištenih kodnih točaka po dokumentu, koji se čuva u bitmapi koju putanja emitiranja teksta popunjava kako se glifovi prikazuju, mapira svaku korištenu kodnu točku kroz predmemoriranu tablicu kodnih točaka u glifove na stvarni identifikator glifa i ponovno zapisuje program fonta oko tog zatvaranja. Kada je Unicode TrueType font registriran, putanja emitiranja postavlja bit u skupu korištenih kodnih točaka za svaki znak koji crta, tako da do trenutka zatvaranja dokumenta pogon točno zna koje glifove podskup mora zadržati

Kvar je bio u tome što je BuildAndApplyUnicodeFontSubset bio pozivan nakon što su SaveToStream ili SaveToFile već serijalizirali dokument. Izmjene koje je program za podskupove napravio u /FontFile2, njegov ispravljeni /Length1 i šesteroslovni prefiks /BaseFont izračunati su na grafu objekata koji je već pretvoren u bajtove. Ispravak je bio promjena redoslijeda u jednom retku: pomicanje poziva podskupova ispred serijalizacije, tako da pisac emitira podskup fonta umjesto originalnog. Ispravljeni niz najprije pokreće podskupove, a nakon toga serijalizira

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;                 // svođenje na podskup odvija se ovdje, prije zapisa
    Pdf.SaveToFile('Report.pdf');
  finally
    Pdf.Free;
  end;
end;

Nakon ispravljanja redoslijeda, ništa se ne mijenja u kodu pozivatelja. Stvaranje podskupova je uključeno prema zadanim postavkama čim se registrira Unicode TrueType font. Registrirate font, započnete dokument, crtate i završite ga, a podskup se gradi od glifova koje ste koristili prije nego što bajtovi napuste memoriju

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

Razlog zašto je ovo vrijedno lekcije, a ne fusnote, jest taj što EndDoc emitira popis završnih koraka, a svaki od njih je osjetljiv na svoju poziciju u odnosu na pisanje. Stvaranje podskupova fontova je jedan od njih. PDF/A izlaz zahtijeva tok /CIDSet koji točno nabraja identifikatore glifova prisutne u podskupu, što je ograničenje koje ISO 19005 nameće kako bi validator mogao potvrditi da ugrađeni program odgovara onome što deskriptor fonta tvrdi; taj se tok emitira u istom prozoru finalizacije i ovisi o tome da je podskup prethodno izgrađen. PDF/UA-1 zahtijeva, prema ISO 14289-1 §7.18.3, da svaka stranica koja nosi bilješku deklarira /Tabs s vrijednošću /S, a interna rutina pod nazivom EnsurePDFUATabsOnAnnotatedPages utiskuje taj ključ tijekom iste faze. Tamo se također izvode i provjere namjere izlaza

Ista pogreška u redoslijedu koja je onemogućila stvaranje podskupova također je ispustila PDF/UA ključ redoslijeda tabulatora na stranicama s bilješkama, jer se taj korak nalazio na istoj pogrešnoj strani pisanja. veraPDF i PAC prijavljuju nedostatak /Tabs /S kao kršenje kontrolne točke 21-001 protokola Matterhorn. Dakle, jedan pogrešno postavljen poziv nije samo povećao veličinu datoteke; istodobno je tiho prekršio zahtjev za sukladnost pristupačnosti, uz isti nedostatak bilo kakve pogreške. To je opasnost faze finalizacije: njezini koraci dijele preduvjet, a jedna pogreška u redoslijedu može izbaciti nekoliko njih odjednom dok svaki poziv i dalje vraća uspjeh

Kako se zapravo otkriva tiho zatajenje emitiranja

Bug koji ne pokreće iznimku ne hvata se pokretanjem programa. Hvata se pregledom izlaza i njegovom usporedbom s onim što je ulaz trebao proizvesti. Za stvaranje podskupova fontova provjere su konkretne. Usporedite veličinu izlazne datoteke s grubim očekivanjem: dokument koji je dotaknuo nekoliko glifova ne bi trebao biti veličine cijelog pisma. Otvorite ugrađeni objekt fonta i pročitajte njegovu duljinu u bajtovima; podskup /FontFile2 za latinično pismo mali je dio izvorne datoteke. Pročitajte naziv /BaseFont i potvrdite da je prisutan šesteroslovni prefiks, jer je njegova odsutnost izravan signal da podskup nije primijenjen

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;
    // Nekoliko glifova iz fonta od ~700 KB ne smije proizvesti tok od nekoliko stotina KB
    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 provjera je još oštrija, jer validator obavlja posao umjesto vas. Postavite razinu sukladnosti i pokrenite rezultat kroz veraPDF: nedostatak /CIDSet ili podskup koji ne odgovara deskriptoru prijavljuje se kao neuspjela klauzula, umjesto da to sami primijetite vizualno. Prekidači sukladnosti koji pokreću ovaj rad na finalizaciji su svojstva dokumenta. PDFACompliance uzima niz kao što je '2B' za PDF/A-2 razinu B, a PDFUACompliance je logička vrijednost koja uključuje zahtjeve za označeni PDF i redoslijed tabulatora

HotPDF vremenska crta koja uspoređuje pogrešan EndDoc redoslijed gdje se BuildAndApplyUnicodeFontSubset izvodio nakon SaveToStream s ispravnim redoslijedom gdje podskupljanje trči ispred serializacije
Premještanje jednog poziva ispred pisanja ne mijenja ništa u pozivajućem kodu, ali odlučuju sadrže li spremljeni bajtovi puni font ili pravi podskup. Korak na krivoj strani serijalizacije vraća se čisto i ne radi ništa
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 proizlaze dva pravila. Prvo je da se svaki korak finalizacije koji mijenja objekte mora pokrenuti prije nego što se ti objekti serijaliziraju, a završna faza dokumentnog pogona trebala bi se tumačiti kao uređeni cjevovod u kojem je serijalizacija posljednja radnja, a ne jedna od nekoliko radnji. Drugo je ono koje je ovdje koštalo najviše vremena: za korak emitiranja, odsutnost pogreške nije dokaz uspjeha. Rutina koja gradi ispravan podskup i primjenjuje ga na pogrešan, već zapisani graf ne prijavljuje ništa loše, jer iz njezine vlastite perspektive ništa nije ni bilo pogrešno. Provjera mora gledati artefakt, a ne povratni kod. Provjerite veličinu izlaza, pročitajte duljinu bajta ugrađenog fonta i njegov prefiks /BaseFont te pustite veraPDF da procijeni PDF/A izlaz gdje nedostajući /CIDSet tihi nedostatak pretvara u imenovani neuspjeh

Proizvođačka strana rukovanja fontovima, odnosno način na koji se pisma registriraju i ugrađuju za izlaz izvješća, pokrivena je u našem članku o fontovima i slikama u izlazu izvješća. Validacijska strana, gdje se ovi koraci finalizacije provjeravaju prema standardima, pokrivena je u vodiču o validaciji za PDF/A i PDF/UA. Obje se povezuju s radom na podskupovima i sukladnosti koji je ovdje opisan, a koji se isporučuje kao dio softvera HotPDF Delphi Component za Delphi i C++Builder, zajedno s API-jima za učitavanje, uređivanje, šifriranje i potpisivanje koji su obrađeni drugdje na ovom blogu

HotPDF četiri koraka završne obrade u redu unutar EndDoc, uključujući podskupljanje fontova, ispisivanje CIDSet i utiskivanje PDF/UA Tabs, svi hrane serializator prije nego postoji ijedan bajt
Subsetting, /CIDSet emisija i /Tabs /S žig dijele jedan finalizacijski utor ispred serijalizacije. Jedan pogrešno smješten poziv može izbaciti i podskup fonta i zahtjev pristupačnosti, a da svaka rutina i dalje izvještava uspjeh