Techninis straipsnis

Minimalaus PDF kūrimas rankiniu būdu: penki reikalingi objektai

Iš esmės PDF yra paprastojo teksto konteineris. Atidarykite daugumą failų šešioliktainiu redaktoriumi (angl. hex editor) ir viršutinė dalis bus lengvai įskaitoma: versijos komentaras, sunumeruotų objektų eilė, maža rodyklė ir pačioje apačioje esanti nuoroda, rodanti skaitytuvui, nuo ko pradėti. Pašalinus suspaudimą, šis formatas tampa toks suprantamas, kad galite tiesiog įvesti veikiantį dokumentą tekstų redaktoriuje ir atidaryti jį skaitytuve. Padarę tai vieną kartą, sužinosite apie PDF struktūrą daugiau nei perskaitę visą specifikaciją, nes turėsite rankiniu būdu sujungti objektus tarpusavyje, o failas neatsidarys, kol to nepadarysite teisingai.

Šiame vadove sukursime mažiausią PDF failą, kuris iš tikrųjų kažką atvaizduoja: vieną puslapį su užrašu „Hello, World!“ standartiniu šriftu ant JAV laiško (US Letter) formato popieriaus. Užbaigtam failui reikės tiksliai penkių objektų ir kelių eilučių papildomos informacijos aplink juos. Pirmiausia aprašysime objektus, o tada surinksime antraštę (header), kryžminių nuorodų lentelę (xref) ir pabaigos bloką (trailer), sujungiančius juos į failą, kurį priims bet kuris skaitytuvas.

Penki objektai, kurių reikalauja skaitytuvas

Skaitytuvas neskenuoja PDF failo nuo pradžios iki galo ieškodamas turinio. Jis pradeda nuo pabaigos bloko (trailer), eina pagal nuorodą į dokumento katalogą (catalog) ir iš ten keliauja objektų grandine. Kiekvienas šios grandinės objektas privalo egzistuoti, kitaip failas neatsidarys. Vieno puslapio dokumento grandinė yra trumpa, o kiekviena grandis turi atskirą užduotį:

  • Catalog yra šaknis. Tai yra objektas, į kurį nurodo pabaigos blokas, ir vienintelis privalomas jo įrašas čia yra nuoroda į puslapių medį.
  • Pages yra puslapių medžio mazgas. Jis pateikia dokumente esančių puslapių sąrašą ir nurodo, kiek jų yra.
  • Page aprašo vieną fizinį puslapį: jo dydį, išteklius, kuriais jis piešia, ir turinio srautą (content stream), kuris jį atvaizduoja.
  • Content stream (turinio srautas) saugo piešimo operatorius – postfiksines komandas, kurios išdėsto tekstą ir grafiką puslapyje.
  • Font (šriftas) deklaruoja šriftą, kurį naudoja turinio srautas. Naudojant vieną iš 14 standartinių šriftų, jums nereikia nieko įterpti į patį failą.

Kiekvienas objektas yra sunumeruotas ir pasiekiamas. Netiesioginis objektas užrašomas kaip N 0 obj ... endobj, kur N yra objekto numeris, o 0 – jo kartos numeris (naujai kuriamame faile visada 0). Bet kurioje kitoje failo vietoje į tą objektą nurodote naudodami nuorodą: 5 0 R reiškia „5 objektas“. Šios nuorodos yra sujungimo laidai. Katalogas turi nuorodą 2 0 R, kad pasiektų puslapių medį, puslapių medis turi nuorodą į puslapį ir t. t. Suklyskite rašydami numerį, ir skaitytuvas nueis pagal tuščią nuorodą į niekur.

Pavadinimai, žodynai ir srautai

Tris sintaksės elementai neša beveik viską. Pavadinimas (name) prasideda pasvirusiu brūkšniu: /Type, /Page, /F0. Pavadinimai yra registrui jautrūs identifikatoriai, o ne eilutės, ir PDF naudoja juos žodyno raktams bei objekto tipui žymėti. Žodynas (dictionary) yra raktų ir reikšmių porų rinkinys, apgaubtas dvigubais kampiniais skliaustais, kur kiekvienas raktas yra pavadinimas: << /Type /Page /MediaBox [0 0 612 792] >>. Reikšmės gali būti skaičiai, pavadinimai, masyvai laužtiniuose skliaustuose, nuorodos arba lizdiniai žodynai. Dauguma PDF objektų yra žodynai.

Srautas (stream) yra žodynas, po kurio seka baitų blokas tarp raktinių žodžių stream ir endstream. Čia gyvena puslapio piešimo operatoriai, o tikruose failuose – suspausti paveikslėliai bei įterpti šriftai. Srauto žodynas aprašo baitus; gamybiniame faile jis privalo turėti /Length įrašą, nurodantį tikslų baitų skaičių, ir dažnai /Filter, pavyzdžiui, /FlateDecode, kai duomenys yra suspausti. Šiame pavyzdyje naudosimės įrankiu, kuris užpildys /Length reikšmę, nes baitų skaičiavimas rankiniu būdu neduoda jokios naudos mokymuisi, tačiau kelia didelę riziką padaryti vieno baito paklaidą, kuri sugadins failą.

Objektų rašymas

Štai šie penki objektai iš eilės. Prieš skaitant turinio srautą svarbu atsiminti koordinačių ypatybę: PDF matuoja koordinates taškais nuo apatinio kairiojo puslapio kampo, kur vienas taškas yra 1/72 colio dalis, o Y ašis kyla į viršų. JAV laiško (US Letter) puslapis yra 612 x 792 taškų dydžio, todėl taškas 50 700 yra netoli viršutinio kairiojo kampo, o ne apačioje.

1 0 obj
<< /Type /Catalog
   /Pages 2 0 R
>>
endobj

2 0 obj
<< /Type /Pages
   /Kids [3 0 R]
   /Count 1
>>
endobj

3 0 obj
<< /Type /Page
   /Parent 2 0 R
   /MediaBox [0 0 612 792]
   /Resources << /Font << /F0 4 0 R >> >>
   /Contents 5 0 R
>>
endobj

4 0 obj
<< /Type /Font
   /Subtype /Type1
   /BaseFont /Helvetica
>>
endobj

5 0 obj
<< /Length 44 >>
stream
BT
/F0 36 Tf
50 700 Td
(Hello, World!) Tj
ET
endstream
endobj

Perskaitykite nuorodas ir struktūra taps aiški. 1 objektas (katalogas) nukreipia savo /Pages įrašą į 2 objektą. 2 objektas (puslapių medis) išvardija 3 objektą /Kids sąraše ir deklaruoja /Count 1. 3 objektas (puslapis) nurodo /Parent atgal į 2 objektą (puslapių medis ir puslapis turi nurodyti vienas kitą abipusiai, tai yra privaloma), nustato savo dydį su /MediaBox, atskleidžia šriftą vietiniu pavadinimu /F0 savo /Resources žodyne ir nurodo 5 objektą kaip savo turinį. 4 objektas yra šriftas: /BaseFont /Helvetica parenka vieną iš 14 standartinių šriftų, kuriuos jį turi kiekvienas skaitytuvas, todėl nieko nereikia įterpti. 5 objektas yra turinio srautas.

Ką iš tikrųjų sako turinio srautas

Srauto turinys yra maža programa PDF puslapio aprašymo kalba, kuri yra postfiksinė: pirma eina operandai, o po to operatorius, kuris juos sunaudoja. Penkios eilutės atlieka visą darbą. BT ir ET atidaro ir uždaro teksto objektą; viskas, kas nustato poziciją ar rodo tekstą, privalo būti tarp gyvavimo ribų. /F0 36 Tf nustato dabartinį šriftą į resursą pavadinimu /F0 ties 36 taškų dydžiu (Tf reiškia „nustatyti teksto šriftą ir dydį“). 50 700 Td perkelia teksto poziciją į (50, 700) puslapio koordinatėse. (Hello, World!) Tj rodo eilutę, kurią PDF užrašo kaip tekstą skliausteliuose, naudodamas Tj jos nupiešimui dabartinėje pozicijoje. Praleiskite BT/ET ir griežtas skaitytuvas atmes teksto operatorius; pamirškite nustatyti šriftą prieš Tj ir nebus jokio dabartinio šrifto, kuriuo būtų galima piešti.

Įrašas /Length 44 srauto žodyne yra tikslus baitų skaičius tarp stream ir endstream raktinių žodžių. Šią reikšmę verta patikėti įrankiui, o ne skaičiuoti naujas eilutes rankiniu būdu, ypač todėl, kad tai, ar jūsų tekstų redaktorius naudoja LF, ar CRLF eilučių pabaigas, keičia bendrą skaičių.

Antraštė, xref ir trailer dalys

Objektai sudaro patį turinį. Trys struktūrinės dalys paverčia juos failu. Pirmoji yra antraštė (header) – pati pirmoji eilutė, nurodanti formatą ir versiją:

%PDF-1.7

Simbolis % pradeda komentarą PDF sintaksėje, tačiau skaitytuvas šį konkretų komentarą traktuoja kaip formato parašą ir perskaito iš jo versiją. Tikras generatorius iškart po jo prideda antrą komentaro eilutę su aukšto bito baitais – tai užuomina failų perdavimo įrankiams, kad failas yra dvejetainis ir neturi būti sugadintas kaip tekstas.

Failo pabaigoje eina kryžminių nuorodų lentelė (cross-reference table) – indeksas, leidžiantis tiesioginę prieigą prie objektų. Ji įrašo kiekvieno objekto poslinkį baitais nuo failo pradžios, todėl skaitytuvas gali iškart šokti prie 3 objekto, prieš tai neanalizuodamas 1 ir 2 objektų. Ši lentelė yra griežta: įrašai yra fiksuoto pločio, po 20 baitų kiekvienas (įskaitant eilutės pabaigą), suformatuoti kaip 10 skaitmenų poslinkis, 5 skaitmenų kartos numeris, raktinis žodis (n – naudojamas, f – laisvas) ir dviejų baitų pabaigos ženklas. Teisinga lentelė mūsų šešiems įrašams (0 objektas visada yra laisvųjų objektų sąrašo viršūnė) atrodo taip:

xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000235 00000 n
0000000308 00000 n
trailer
<< /Size 6
   /Root 1 0 R
>>
startxref
408
%%EOF

Šie poslinkiai yra labiausiai pažeidžiama PDF rašymo rankiniu būdu dalis. Kiekvienas iš jų nurodo tikslią baito poziciją, kur prasideda atitinkamas N 0 obj įrašas, ir kiekvienas poslinkis pasikeičia tą akimirką, kai pridedate bent vieną simbolį bet kur virš jo. Pabaigos blokas (trailer) yra įėjimo taškas, kurį skaitytuvas naudoja pirmiausia: /Root 1 0 R nurodo katalogą, /Size 6 nurodo objektų skaičių, o startxref 408 nurodo paties žodžio xref poslinkį baitais. Skaitytuvas atidaro failą, peršoka į pabaigą, perskaito startxref, susiranda kryžminių nuorodų lentelę ir iš ten pasiekia katalogą bei visa kita. %%EOF pažymi paskutinį failo baitą.

Leiskite įrankiui ištaisyti baitų skaičių

Aukščiau pateikti poslinkiai yra pavyzdiniai; praktikoje jie bus klaidingi iki to laiko, kai baigsite rašyti failą, nes priklauso nuo tikslaus baitų išdėstymo jūsų faile. Užuot skaičiavę juos iš naujo, parašykite struktūrą su laikinomis reikšmėmis ir leiskite programai atstatyti kryžminių nuorodų lentelę ir srautų ilgius. Nemokama, daugiaplatformė programa pdftk tai atlieka vienu žingsniu:

pdftk hello-draft.pdf output hello.pdf

Ji išanalizuoja jūsų objektus, perskaičiuoja kiekvieno objekto poslinkį baitais, įrašo teisingas /Length reikšmes, sukuria galiojančią xref lentelę bei pabaigos bloką ir išveda failą hello.pdf. Atidarykite jį bet kurioje skaityklėje ir pamatysite vieną puslapį su 36 taškų Helvetica šrifto užrašu „Hello, World!“ netoli viršaus. Programa „qpdf“ atlieka tą patį darbą, o daugelis skaityklių taip pat pataiso šiek tiek neteisingai suformuą failą tiesiogiai atidarymo metu. Naudotis įrankiu šioje vietoje nėra tinginystė; poslinkių skaičiavimas yra ta formato dalis, kuri neturi jokio konceptualaus turinio, tačiau pasižymi didžiausia klaidų rizika, todėl jos automatizavimas leidžia sutelkti dėmesį į pačią struktūrą.

Kodėl tai tinka ir tikriems dokumentams

Niekas šimto puslapių ataskaitoje nekeičia jūsų ką tik sukurtos struktūros. Katalogas vis tiek yra šaknyje, puslapių medis surenka puslapius, o kiekvienas puslapis nurodo į savo išteklius ir turinio srautą. Tai, kas didėja, yra plotis, o ne stuburas: puslapių medis šakojasi, kad skaitytuvas galėtų praleisti ištisus pomedžius, turinio srautai turi šimtus operatorių vietoj penkių, šriftai įterpiami kaip atskiri srauto objektai su savo pločių lentelėmis ir koduotėmis, o paveikslėliai pateikiami kaip srautai su specifiniais filtrais. Modernūs failai taip pat linkę pakuoti daugelį objektų į suspaustus objektų srautus ir pakeisti paprastą xref lentelę kryžminių nuorodų srautu (cross-reference stream), todėl atidarius tikrą PDF failą tekstų redaktoriuje paprastai matomas dvejetainis tekstas. Tačiau po tuo slypintis modelis yra visiškai toks pat, kaip ir jūsų ranka sukurtame faile. Norėdami sužinoti apie platesnį objektų grafiką ir tai, kaip katalogas, puslapių medis ir išteklių žodynai susiję didesniame dokumente, skaitykite mūsų išsamų PDF dokumento struktūros nagrinėjimą, o failo struktūros apžvalga apima priaugamuosius atnaujinimus (incremental updates) bei tai, kaip pabaigos blokas susiejamas per revizijas.

Nuo rankinio rašymo iki bibliotekos

Objektų rašymas ranka yra mokymosi pratimas, o ne gamybos metodas. Tą akimirką, kai jums prireiks tikrų šriftų, apgaubto teksto, paveikslėlių ar daugiau nei vieno paprasto puslapio, baitų apskaita tampa pagrindiniu darbu, ir tam norėsite bibliotekos, kuri prisiimtų šį darbą. Vis tiek bus rašomi tie patys penki objektai, tačiau biblioteka apskaičiuos kiekvieną poslinkį, valdys šriftų bei išteklių žodynus ir suspaus turinio srautus, jums patiems nesekant nė vieno baito. Delphi ir C++Builder aplinkose HotPDF Component komponentas sumažina viso šio failo kūrimą iki kelių iškvietimų: paruošiamas dokumentas, iškviečiama BeginDoc, SetFont bei TextOut tam pačiam tekstui išvesti ir galiausiai EndDoc, kad būtų įrašytas teisingas katalogas, puslapių medis, xref lentelė bei pabaigos blokas. Po tuo slypinčių objektų supratimas padeda suprasti išvestį, kai dokumentas atvaizduojamas ne taip, kaip tikėjotės.