momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Depistarea şi eliminarea unor obiecte PDF, folosind comenzi din patrimoniul Linux-ului şi modulul pyPdf

Linux | PDF | Python
2013 aug

De la www.variantebacalaureat.com am obţinut fişierele numite aici MT1_09_i.pdf, MT1_09_ii.pdf, MT1_09_iii.pdf conţinând respectiv "Subiectul I", II şi III pentru cele 100 de variante de subiecte de bacalaureat la proba "matematică", pentru profilul MT1 - constituite în 2009 de către specialişti oficiali în evaluare şi examinare.

Vrem să aplicăm şi acestui caz, procedura descrisă în [4] - pentru a obţine 100 de fişiere PDF (de câte o singură pagină) conţinând fiecare, cele trei subiecte care compun câte o "variantă".

Nu ne interesează decât subiectele ca atare, încât vrem să "scăpăm" de elementele suplimentare existente în fişierele PDF iniţiale. Dar de data aceasta, elementele "inutile" nu se rezumă la antet, notă de subsol şi punctaje "5p" - cum a fost cazul în [4]; fiecare pagină din cele trei fişiere PDF iniţiale conţine în plus, "adnotări" şi un soi de "filigrane":

i_1.pdf are obiecte de tip /Annot şi "filigrane" grafice

În vecinătatea cursorului de mouse se produce un "tooltip" (eventual, cu evidenţiere "background" a unei anumite regiuni dreptunghiulare), conţinând textul www.variantebacalaureat.com; iar click-ul obişnuit determină accesarea de către browser a site-ului indicat în "tooltip".

Acest comportament este asigurat prin prezenţa în fişierul PDF respectiv a unor obiecte de tip /Annot, conţinând fiecare un obiect de tip /Link; dacă răsfoind cele 100 de pagini, produci 10 click-uri - atunci vei obţine 10 tab-uri în brower (presupunând că este deschis un browser), fiecare tab conţinând pagina de bază //www.variantebacalaureat.com.

Pe pagina reprodusă mai sus se observă şi nişte "filigrane": textul variantebacalaureat.com este reprezentat cu o mărime sau alta, pe trei regiuni diagonale transparente, care apar în poziţii fixe "deasupra" paginii; efectul acesta denotă prezenţa fie a unor elemente /Watermark, fie (şi acesta-i aici cazul) a unor obiecte stream care conţin comenzi de scalare şi rotire a textului respectiv.

Pentru a "trunchia" paginile la subiectul propriu-zis şi apoi a "reuni" subiectele într-o singură pagină - cum am procedat în [4] - trebuie să eliminăm şi adnotările şi filigranele menţionate. Bineînţeles că am căutat pe Google ceva de genul "PDF remove watermarks annotations"; mi-a luat cam două zile să constat că eliminarea intenţionată este o operaţie dificil de realizat "prin program" - şi nu "pagină cu pagină", folosind anumite aplicaţii comerciale de editare PDF (produse de dimensiune uriaşă şi de tip "point-and-click" - pentru care chiar nu am nici un interes şi nici licenţele necesare).

Investigarea fişierului PDF, folosind comenzi din patrimoniul Linux-ului

Câştigul acestei căutări fără succes constă în faptul că am reuşit să mă documentez binişor, asupra formatului de fişier PDF; în ultimă instanţă - dacă ar fi cazul - aş putea scrie în limbaj de asamblare, o subrutină care să copieze într-un nou fişier obiectele necesare din fişierul iniţial şi să constituie apoi, tabele de referinţe specifice formatului PDF.

Bineînţeles că înainte de orice, a trebuit să verific şi să elimin restricţiile existente în fişierele PDF iniţiale (vezi [2]). Pentru a putea experimenta mai uşor, am extras din primul fişier prima pagină - constituind fişierul i_1.pdf expus şi aici, mai sus; analiza acestui fişier s-a dovedit apoi absolut relevantă pentru toate cele 300 de pagini existente în cele trei fişiere PDF iniţiale.

Pentru investigare, am folosit programe utilitare simple, din patrimoniul Linux-ului: head şi tail afişează primele şi respectiv ultimele linii ale fişierului indicat, permiţând şi un offset iniţial.

Pe ultimele 9 linii din fişier găsim dicţionarul trailer şi offset-ul tabelului de referinţe xref:

vb@vb:~/bacmath/DOC/B$ tail --lines=9 i_1.pdf
trailer
<<
/Size 74     % tabelul xref conţine 74 de intrări (pentru obiectele existente în fişier)
/Root 3 0 R  % obiectul 3 este "rădăcina" ierarhică a obiectelor din fişier
/Info 2 0 R  % obiectul 2 conţine informaţiile redate (de exemplu) de pdfinfo
>>
startxref
37128        % xref începe la al 37128-lea octet faţă de începutul fişierului
%%EOF

La offset-ul indicat 37128 găsim valoarea 0x0A, care în Linux reprezintă caracterul "sfârşit de linie" (notat de obicei cu '\n'). Vor fi posibile unele mici greşeli de reperare, fiindcă fişierul PDF iniţial a fost produs pe Windows, unde '\n' este reprezentat prin doi octeţi (0x0D, 0x0A) - iar acum îl "citim" pe Linux (folosind programe "obişnuite" cu unul şi nu cu doi octeţi, pentru '\n'). Prin urmare, intrăm la offset-ul imediat următor, găsind tabelul "xref":

vb@vb:~/bacmath/DOC/B$ tail -c +37129 i_1.pdf | head -7
xref
0 74
0000000000 65535 f 
0000000009 00000 n  % La offset-ul 9 vom găsi definiţia obiectului 1
0000000068 00000 n 
0000000169 00000 n  % La offset-ul 169 vom găsi definiţia obiectului 3 
0000000218 00000 n
% restul celor 74 de înregistrări din 'xref'

Am "înlănţuit" tail şi head, prin operatorul "pipe" |: rezultatul produs de tail - conţinutul începând de la offset-ul indicat şi până la sfârşitul fişierului - este transferat comenzii head, care aici reţine pentru afişare numai primele 7 linii (suficiente pentru a repera informaţiile despre obiectul 3).

Găsim acum definiţia obiectului 3 (indicat în trailer ca "rădăcina" obiectelor fişierului):

vb@vb:~/bacmath/DOC/B$ tail -c +170 i_1.pdf | head -6
3 0 obj  % definiţia obiectului 3 (rădăcina ierarhică a obiectelor)
<<
/Type /Catalog
/Pages 1 0 R  % obiectul 1 este lista paginilor fişierului
>>
endobj

Obiectul 1, indicat de cheia /Pages, conţine informaţii despre toate paginile:

vb@vb:~/bacmath/DOC/B$ tail -c +10 i_1.pdf | head -7
1 0 obj  % definiţia obiectului 1
<<
/Kids [ 4 0 R ]  % Lista obiectelor care descriu paginile existente în fişier
/Type /Pages
/Count 1  % Numărul de pagini este 1
>>
endobj

şi vedem în sfârşit, că fişierul nostru conţine o singură pagină, al cărei conţinut îl putem găsi în obiectul 4 (indicat în tabloul de cheie /Kids); cu o comandă similară celor de mai sus, indicând offset-ul 218 (citit din tabelul "xref"), obţinem (într-o redare extrem de trunchiată):

4 0 obj  
<<  % dicţionar cu referinţe la toate obiectele (de diverse tipuri) conţinute în pagină
/Contents [ 5 0 R 6 0 R 7 0 R 8 0 R /* ş.a.m.d. până la */ 23 0 R ]
/Resources  % dicţionar de fonturi şi parametri/proceduri grafice aplicate la redarea PDF
% ... /Parent 1 0 R  /Type /Page  /MediaBox [ 0 0 595 842 ]  % şi altele %
/Annots 63 0 R  % în obiectul 63 vom găsi referinţe la "adnotări" 
>>
endobj

Sub cheia /Annots vedem o primă informaţie privitoare la "adnotări": obiectul 63; iar offset-ul în fişier al acestuia îl avem pe a 63-a intrare (indexând de la zero) în tabelul xref.

Depistarea şi studiul obiectelor "adnotare"

În loc să numărăm direct intrările, putem proceda mai comod astfel: listăm încă odată tabelul xref, dar acum redirectăm ieşirea către un fişier text nou; apoi, încărcăm în gEdit (sau alt editor de text) fişierul creat şi derulăm (cu tasta "săgeată-jos") până ce indicatorul de linie arată linia 64 (în gEdit, liniile sunt numărate de la 1) - linie de unde copiem offset-ul care va trebui indicat combinaţiei "tail | head" pentru a determina ca şi mai sus, definiţia obiectului 63:

vb@vb:~/bacmath/DOC/B$ tail -c +35835 i_1.pdf | head -25
63 0 obj  % indică 5 obiecte de tip /Annot ("adnotări")
[ 64 0 R 66 0 R 68 0 R 70 0 R 72 0 R ]
endobj % --------------------------------------------------------------
64 0 obj   % obiect-"adnotare" de tip /Link
<<
/Type /Annot 
/A 65 0 R   % în obiectul 65 este înregistrată "acţiunea" link-ului
/Rect [ 10.40240 132.51410 584.60217 709.48541 ]  % Locul adnotării, în cadrul paginii
/Border [ 0 0 0 ]
/Subtype /Link
/BS <<
/W 0    % "Width" = 0 (regiunea care încadrează adnotarea nu are "border")
/Type /Border
/S /S
>>
/P 4 0 R   % "parent"-ul adnotării (obiectul 4)
>>
endobj % --------------------------------------------------------------
65 0 obj   % defineşte URL-ul de activat la "acţionarea" adnotării
<<
/Type /Action
/URI (www\056variantebacalaureat\056com)   % \056 este codul octal pentru "."
/S /URI
>>
endobj

Toate cele cinci obiecte "adnotare" din lista conţinută de obiectul 63 au definiţii care diferă numai prin valoarea cheii /A şi prin definiţia /Rect a regiunii "controlate" de adnotarea respectivă; iar obiectele 65, 67, 69, 71, 73 de tip /Action asociate celor cinci adnotări au conţinut identic.

Repetând expeditiv experimentul de mai sus pentru alte câteva pagini extrase din cele trei fişiere PDF iniţiale - am ajuns la concluzia că toate cele 300 de pagini conţin cele cinci "adnotări" explicitate mai sus (diferind doar "indicatorii" de obiecte şi evident, offset-urile în fişier). Desigur că este penibilă repetarea aceloraşi informaţii de 100 de ori, în diverse locuri din acelaşi fişier - dar pe de altă parte această situaţie ne convine foarte mult: vom putea elimina adnotările printr-o procedură unică (fără a ramifica pe diverse cazuri).

Interpretarea conţinutului propriu-zis, al paginii PDF

Din cele de mai sus rezultă că "adnotările" sunt complet separate, de obiectele care dau conţinutul propriu-zis al paginii - este ca şi cum am avea două pagini suprapuse: una pentru conţinutul "real" şi a doua conţinând numai obiecte "adnotare"; de altfel, aceasta se vede chiar din definiţia obiectului 4, în care conţinutul propriu-zis este indicat de cheia /Contents, iar "adnotările" de cheia /Annots.

Să ne ocupăm acum şi de "conţinutul propriu-zis" (urmărind să depistăm până la urmă, acele obiecte care sunt răspunzătoare de producerea "filigranelor"):

vb@vb:~/bacmath/DOC/B$ tail -c +219 i_1.pdf | head -44
4 0 obj  % înregistrează (prin referinţe) întregul conţinut al paginii 
<<
   /Type /Page
   /Parent 1 0 R  % localizarea ierarhică a paginii
   /Contents [ 
      5 0 R 6 0 R 7 0 R  % 3 obiecte "q" - PUSH pe stiva de "stări grafice"
      8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R 15 0 R  % obiecte stream text
      16 0 R 17 0 R 18 0 R  % 3 obiecte "Q" - POP din stiva de "stări grafice"
      19 0 R 20 0 R 21 0 R 22 0 R 23 0 R  % obiecte text (stream text) 
   ]
   /Resources <<  
     /ExtGState << % parametri de stare grafică (obiectele 24..36)% >>
     /Font << % Fonturi şi caractere - localizare, descrieri (obiectele 37..62)% >>
     /ProcSet [ /PDF /Text ] % interpretează stream-uri text (nu şi imagini, etc.)%
   >>
   /MediaBox [ 0 0 595 842 ]
   /Annots 63 0 R
>>
endobj

Formatul de redare, împreună cu comentariile inserate în textul reprodus mai sus - sintetizează observaţiile de pe parcursul investigării obiectelor din lista /Contents.
În xref, obiectul 5 are înregistrat offset-ul 891 şi găsim:

vb@vb:~/bacmath/DOC/B$ tail -c +892 i_1.pdf | head -8
5 0 obj
<< 
/Length 3  % lungimea în octeţi, a stream-ului conţinut
>>
stream  % conţine 3 octeţi: 0x20, 0x71, 0x20 (spaţiu, 'q', spaţiu)
 q   % salvează intern "starea grafică" curentă  
endstream
endobj

Procedând la fel pentru obiectele 6 şi 7 - găsim exact acelaşi conţinut ca al obiectului 5.

Mărind în comanda "head" valoarea parametrului "linii de afişat", putem parcurge mai uşor (decât dacă plecăm mereu, de la offset-urile citite din tabelul "xref") restul obiectelor; în acelaşi scop, putem folosi şi xxd, care vizualizează în hexazecimal conţinutul fişierului. Conţinutul obiectului 16 (şi 17, 18) este acelaşi (aproape) ca al obiectului 5, dar cu operatorul "Q" în loc de "q":

vb@vb:~/bacmath/DOC/B$ tail -c +6672 i_1.pdf | head -8 | xxd -g 0 -c 24

00: 31362030206f626a0a3c3c0a2f4c656e67746820330a3e3e  16 0 obj.<<./Length 3.>>
18: 0a73747265616d0a2051200a656e6473747265616d0a656e  .stream. Q .endstream.en
30: 646f626a                                          dobj

Aplicaţia "Document Viewer" cu care vizualizezi un fişier PDF are încorporat un "interpretor de PDF" - care "înţelege" ce scrie în fişier (analizându-l cam în maniera evidenţiată aici, plecând de la "trailer" şi de la tabelul "xref") şi "ştie" să expună pe display (sau pe hârtie) conţinutul respectiv.
Dar documentul PDF poate conţine diverse elemente grafice, imbricate sau/şi independente între ele; interpretorul de PDF menţine o stivă internă în care salvează parametrii stării grafice curente şi ulterior (după ce "consumă" o stare grafică intermediară), îi reconstituie din această stivă internă.

Stiva stărilor grafice este iniţializată la începutul redării fiecărei pagini din fişierul PDF; "necesităţile grafice" ale paginii sunt indicate interpretorului PDF în sub-dicţionarul /ExtGState din dicţionarul /Resources al obiectului 4 (acel obiect din care derivează toate celelalte). În obiectele 24..36 ale dicţionarului /ExtGState găsim (în cazul nostru) cam aceiaşi doi-trei parametri: /CA şi /ca, ale căror valori (în cazul de aici: 1, 0.1 sau 0.3) intervin în calculul intern al nivelului de transparenţă a culorii; /SM=0.02 pentru "smoothness tolerance" (nivel de netezire a formei).

Resursele de /font (aici, obiectele 37..62) ţin şi ele, de interpretarea fiecărei pagini în parte; folosind strings pentru a obţine textul dintr-un fişier binar (numai caracterele printabile) şi grep pentru a extrage liniile conţinând text care se potriveşte cu un şablon indicat:

     strings MT1_09_i.pdf | grep '/Font <</NxF0' > font-objects.txt

am obţinut în fişierul în care am redirectat ieşirea, exact 100 de rânduri de forma "/Font <</NxF0 7093 0 R" (indicatorul de obiect pleacă de la 7093 şi merge din 4 în 4 până la 7489); adică există 100 de obiecte distincte (câte unul în fiecare "pagină" a fişierului PDF iniţial "MT1_09_i.pdf", de 4 MB) şi care au toate, exact acest conţinut:

vb@vb:~/bacmath/DOC/B$ strings MT1_09_i.pdf | grep -A5 '7093 0 obj'
7093 0 obj  % denumit "/NxF0", în cadrul obiectului de bază al paginii
<</Type /Font
/Subtype /Type1
/BaseFont /Courier-Bold
/Encoding /WinAnsiEncoding  % ">>" nu este afişat, nefiind "printabil"
endobj

Numele "/NxF0" (şi analog, alte nume de obiecte) instituit astfel în dicţionarul /Font al resurselor paginii, serveşte pentru a referi şi folosi obiectele respective (cu informaţii despre fonturi, în acest caz) din interiorul unor altor obiecte (diverse stream-ri de text).

Poate fi dezamăgitoare, constatarea că sunt înregistrate de atâtea şi atâtea ori, în locuri diferite din fişier, aceleaşi informaţii; dar acest fapt nu este neapărat, specific formatului PDF - ţinând mai degrabă de maniera "point-and-click" în care s-a obţinut fişierul PDF iniţial (probabil şi de intervenţia de eliminare a restricţiilor existente, prealabilă demarării analizei efectuate mai sus).

Logica încadrării între PUSH şi POP

Care ar fi logica încadrării secvenţei de obiecte 8..15 între cele trei operaţii "PUSH" (realizate de către operatorul q din obiectele 5, 6, 7) şi cele trei operaţii "POP" (de reconstituire a stării grafice, prin operatorul Q din obiectele 16, 17, 18)?

Când există mai multe pagini şi un context grafic global, atunci într-adevăr - procesul de constituire a unei pagini oarecare trebuie să demareze cu salvarea contextului grafic global, urmând ca după finalizarea operaţiilor necesare pentru redarea acelei pagini (în contextul grafic propriu), procesul să se încheie cu reconstituirea contextului global iniţial.

Dar în fişierul nostru avem o singură pagină, iar pe de altă parte operaţiile "POP" nu figurează la sfârşitul listei de obiecte ale căror acţiuni trebuie "executate" (subînţelegem că în aceeaşi ordine în care apar în lista de cheie /Contents din obiectul "rădăcină" 4): obiectele 19..23 urmează în pagină după secvenţa de reconstituire a contextului grafic iniţial.

Aşa că răspunsul cel mai cinstit este că nu există nici o "logică"; dacă obiectele 19..23 angajează contexte grafice specifice, atunci ele însele trebuie să-şi iniţieze acţiunile cu PUSH (salvează starea grafică găsită) şi să le încheie cu POP (reconstituie starea grafică iniţială).

Deci, dacă ne convingem că obiectele 19..23 "acţionează" corect - adică folosesc operatorii q şi Q - atunci vom putea lua în consideraţie şi eliminarea celor 6 obiecte "push-pop" (pe lângă eliminarea intenţionată, a adnotărilor şi "filigranelor").

Investigarea unor obiecte stream ale paginii PDF

Obiectul 8 conţine un stream de tip text, adică o secvenţă de octeţi (valori 0x00..0xFF, nu neapărat din spectrul "printabil" 0x20..0x7F) oricât de lungă; de regulă, "textul" respectiv este înscris în formă comprimată (reducând lungimea iniţială) - caz în care definiţia obiectului care conţine acel stream precizează mecanismele de decompresare care vor trebui aplicate când ar fi cazul:

vb@vb:~/bacmath/DOC/B$ strings i_1.pdf | grep -A14 -w '8 0 obj'
8 0 obj
/Length 656  % conţine un stream de 656 octeţi (lungimea stream-ului compresat)
/Filter /FlateDecode  % stream-ul este compresat; pentru decompresie: "FlateDecode"
stream  % 'stream' şi 'endstream' delimitează cei 656 de octeţi
Q+'*    % 035: 48899c945d6edb300cc7df7d0a3e8a40a548f2a780624092
+]jc    % 04e: 664387752836efa9e88397394586c429dca4bde50eb103ec0c23
Fp.P    % 068: 2dc509dca4c0d602312951e4dfe4cf9a94d1e8bd0103e5223216
%t:9    % 082: 34fdd3c31485d2b13390bb54a5566750ae230d0fd1e8c357030f
U<lN    % 09c: 4f91d44aeb984ecda3bdf512dd899b25ca5419d1a0cce911bca7
W@!f    % 0b6: 6dddeefccacaafc00c6546cf1f7e356cce512b272abc2f3f46a3
9FCwC   % 0d0: b20caa729514a4aabc22015c8e2b5d6a63f5bbf2e741bb5671ec
[{4~    % ...: ... ... ... ... ... ... etc. %
8*k.    % 2be: ff2bc000dbb4607d %
endstream
endobj

grep -A14 produce 14 linii după (="After") linia pe care a găsit "cuvântul" indicat '8 0 obj'; fără parametrul -w, rezultatul produs ar fi inclus şi liniile corespunzătoare obiectelor "18 0 obj", etc. (fiindcă se potrivesc şablonului indicat). În mod standard, "grep" afişează din cadrul unui conţinut "binar" numai "cuvintele" care măsoară cel puţin 4 caractere "printabile" consecutive; am ilustrat în comentarii şi secvenţa de 656 octeţi rezultată plecând de la offset-ul obiectului 8, prin:

vb@vb:~/bacmath/DOC/B$ tail -c +1048 i_1.pdf | head -13 | xxd -g 0 -c 26

Şi stream şi string reprezintă secvenţe de octeţi (sau "date binare"), eventual comprimate; diferenţa esenţială constă în faptul că într-un stream pot exista secvenţe de octeţi (de forma "listă de operanzi operator"), care sunt interpretate drept comenzi care la redarea paginii vor executa anumite operaţii grafice asupra unor elemente (de text, de imagine, etc.) ale acesteia.

Ca să putem percepe funcţionalitatea obiectului 8 (şi a celorlalte din secvenţa menţionată) trebuie mai întâi să decomprimăm stream-ul celor 656 de octeţi (vezi deflate algorithm şi zlib).

Dăm în treacăt, un exemplu de script Python pentru comprimare/decomprimare (folosind zlib):

vb@vb:~/bacmath/DOC/B$ python test_zlib.py 
Comprimat:
x�+I�(QHIUH��-(��M,:E�
Decomprimat:
text de comprimat
import zlib
un_cp = 'text de comprimat'
cp = zlib.compress(un_cp)
print "Comprimat:\n", cp
print "Decomprimat:\n", zlib.decompress(cp) 

Dar n-ar fi cazul să ne rătăcim: ne propusesem să identificăm acele obiecte care gestionează "filigranele", ori acestea sunt bazate aici pe un text scurt: "variantebacalaureat.com"; sunt de intercalat anumiţi operanzi şi operatori (pentru rotire, scalare) - dar nu poate rezulta o lungime totală chiar aşa de mare…

La fel cum am găsit mai sus conţinutul obiectului 8, putem găsi conţinutul celorlalte obiecte; putem astfel constata că stream-urile din obiectele 8..15 au lungimi între 597 şi 664 de octeţi. În schimb, obiectele 19..23 conţin stream-uri de lungime maximă 236 octeţi - prin urmare, în această ultimă secvenţă de obiecte ar trebui căutate "filigranele":

vb@vb:~/bacmath/DOC/B$ tail -c +6831 i_1.pdf | head -8
19 0 obj
<<
/Length 235
>>
stream
 q 0.00 g 1 i /RelativeColorimetric ri /NxGS7 gs BT /NxF0 1 Tf % ...... %
 <76617269616e7465626163616c6175726561742e636f6d> Tj  ET Q 
endstream
endobj

Să observăm imediat, că stream-ul redat parţial mai sus nu este comprimat - dat fiind că în definiţia obiectului 19 lipseşte specificaţia /Filter /FlateDecode.

Să observăm că sunt prevăzuţi operatorii q şi Q la început şi respectiv la sfârşit - adică acest stream procedează corect, salvând "starea grafică" a mediului (pe stiva stărilor grafice) înainte de a începe propriile operaţii şi reconstituind-o după încheierea acestora.

Când este cazul, operanzii preced operatorul. De exemplu, secvenţa '0.00 g' care urmează imediat după operatorul q, va seta la valoarea zero nivelul culorii gri; iar '/NxGS7 gs' va seta parametrii grafici conform definiţiei acestora din obiectul de nume "/NxGS7" (acesta poate fi identificat în dicţionarul /ExtGState din obiectul 4 "de bază" al paginii).

Operatorii BT ("Begin Text") şi ET ("End Text") delimitează un "obiect de text", constând din operatori care asigură redarea string-ului conţinut, poziţionarea acestuia, fontul, mărimea etc. De exemplu, secvenţa iniţială '/NxF0 1 Tf' prevede pentru redarea textului fontul specificat în obiectul de nume /NxF0 - din sub-dicţionarul /Fonts al resurselor obiectului 4 - în mărimea curentă (factorul de scalare, din stânga operatorului "Tf" fiind 1).

Operatorul Tj realizează redarea finală (după aplicarea operatorilor precedenţi lui) a string-ului din stânga sa, anume '<76617269616e7465626163616c6175726561742e636f6d>'; precizăm că un string PDF este fie o secvenţă de caractere obişnuite încadrată între '(' şi ')', fie (cum este cazul aici) o secvenţă de coduri hexazecimale de caractere încadrată între '<' şi '>'.

Putem vedea în diverse feluri, că acest string este exact ceea ce căutam:

vb@vb:~/bacmath/DOC/B$ python -c \
> 'print "76617269616e7465626163616c6175726561742e636f6d".decode("hex")'
variantebacalaureat.com  # 0x76 este codul lui 'v', 0x61 codul lui 'a', etc.

În loc de a repeta cele de mai sus şi asupra obiectelor 20..23, putem folosi comanda:

strings i_1.pdf | grep -B4 76617269616e7465626163616c6175726561742e636f6d

obţinând pe ecran toate stream-urile care conţin secvenţa de caractere (cifre hexazecimale) indicată lui grep, precedate fiecare de încă 4 linii ("-B4" înseamnă "Before 4"), încât vedem şi obiectele de care ţin acele stream-uri. Ne convingem astfel că este vorba exact de obiectele 19..23 - prin urmare, deducem că acestea sunt într-adevăr, "responsabile" de producerea "filigranelor".

Eliminarea obiectelor inutile, folosind modulul pyPdf

Analiza efectuată mai sus ne arată că pentru a "scăpa" de "adnotări" şi de "filigrane", trebuie să eliminăm din fişierul PDF al paginii obiectele care au /Type /Annot, indicate în obiectul denumit /Annots în cadrul dicţionarului obiectului "de bază" al paginii; deasemenea, trebuie să eliminăm ultimele cinci obiecte indicate în lista de cheie /Contents (a obiectului "de bază" al paginii).

Ar mai fi două categorii de obiecte pe care le-am putea elimina, fără a afecta conţinutul rămas (intuind doar, că aceasta ar fi valabil pentru toate paginile dintre cele 100 ale fişierului PDF iniţial); pe de o parte - cele trei perechi de obiecte "PUSH" şi "POP", vizate mai sus într-un paragraf separat. Pe de altă parte, ar fi de eliminat şi obiectele înrădăcinate în sub-dicţionarul /ExtGState din dicţionarul /Resources al obiectului de bază al paginii - pentru motivul că aceste obiecte par a fi utilizate numai de obiectele (eliminate) care produceau "filigrane".

În următorul script Python folosim modulul pyPdf şi definim o funcţie care primind un nume de fişier PDF şi un număr de pagină, produce un nou fişier PDF conţinând numai pagina indicată din fişierul iniţial, excluzând însă obiectele PDF din categoriile precizate mai sus:

from pyPdf import PdfFileWriter, PdfFileReader

def extract_page(pdf_inp, pdf_out, pag_nr=0):
    pdf = PdfFileReader(file(pdf_inp, 'rb'))  # citeşte fişierul şi analizează PDF-ul
    page = pdf.getPage(pag_nr)  # obiectele, dicţionarele, stream-urile paginii
    del page['/Annots']  # şterge "adnotările"
    del page['/Contents'][:3]  # şterge primele 3 obiecte ("PUSH")
    del page['/Contents'][-8:]  # şterge ultimele 8 obiecte (3 "POP" şi 5 "filigrane")
    del page['/Resources']['/ExtGState']  # şterge "starea grafică" pentru "filigrane"
    out = PdfFileWriter()  # creează fişierul PDF pentru pagina "curăţată"
    out.addPage(page)
    out.write(file(pdf_out, 'wb'))
    
# extrage prima pagină din 'MT1_09_i.pdf':
extract_page('MT1_09_i.pdf', 'i_1_cut.pdf')

Fişierul PDF rezultat prin execuţia acestui script este cu 5 KB mai scurt faţă de fişierul i_1.pdf de a cărei analiză ne-am ocupat mai sus (şi pe care l-am vizualizat într-un <iframe> la începutul acestui articol) şi într-adevăr, nu mai conţine "adnotări" şi nici "filigrane".

Inspectarea anumitor categorii de date din fişier devine mult mai comodă folosind direct pyPdf, decât folosind (ca în analiza desfăşurată mai sus) comenzi "head" şi "tail". De exemplu, incluzând în scriptul redat mai sus şi modulul pprint (pentru "pretty-print") şi înlocuind ultimele trei linii din definiţia funcţiei extract_page() cu:

    for ob in page['/Contents']:
        pprint.pprint(ob.getObject().getData())
        print '\n\n' 

iar apoi executând scriptul cu redirecţionarea ieşirii spre un fişier text - vom obţine în acest fişier conţinuturile decomprimate ale tuturor stream-urilor rămase după eliminările de obiecte precedente; încărcând acest fişier într-un editor de text şi căutând toate apariţiile 'q' - constatăm că operatorii "push" si "pop" apar în reuniunea acestor stream-uri în perechi "corect închise", ceea ce ne asigură încă o dată asupra faptului că prin eliminarea făcută a celor şase obiecte "PUSH", "POP" redarea informaţiei nu are de suferit.

Căutând apoi şiruri 'NxGS', avem ca rezultat zero apariţii - însemnând că stream-urile rămase nu angajează obiecte din /ExtGState, astfel că nici eliminarea obiectelor "stare grafică" nu afectează redarea documentului. Există o excepţie: restrângând căutarea la 'GS', găsim la începutul primului stream (dar numai aici):

'BT\n/F1 1 Tf\n12 0 0 12 188.0391 795.5206 Tm\n0 g\n/GS1 gs\n-0.0031 Tc
 \n0.0031 Tw\n[(Mi)-5.1(n)-7.1(i)-5.1(steru)-7.1(l)

ceea ce poate însemna că eliminarea obiectele de "stare grafică" nu-i bună: când interpretorul PDF va întâlni în stream-ul redat parţial mai sus, secvenţa /GS1 gs - el va căuta obiectul de nume "/GS1", ori acesta… a fost eliminat. Dar putem fi liniştiţi: pe de o parte, conţinutul iniţial al acestui obiect era unul chiar banal; pe de altă parte, când interpretorul întâlneşte un obiect inexistent, pur şi simplu ignoră operaţiunea respectivă şi trece la analiza următoarei secvenţe de octeţi; în sfârşit, putem observa că stream-ul respectiv ţine de antetul paginii (vezi "Mi n i steru l" în redarea de mai sus) - ceea ce oricum avem de eliminat, aplicând în continuare procedura descrisă în [4].

Am fi vrut ca acum chiar, să eliminăm şi obiectul sau obiectele "responsabile" de producerea antetului. Dar lucrurile stau aşa: antetul este redat din primele două stream-uri, iar al doilea conţine în plus, un bloc de text care este completat în al treilea obiect (ş.a.m.d.); mai apar şi situaţii când ultimul "push" dintr-un stream este împerecheat cu primul "pop" din stream-ul următor… Prin urmare, eliminând încă un obiect riscăm ca fişierul produs să nu mai poată fi recunoscut de interpretoarele PDF.

Mai departe, ambalând funcţia extract_page() într-un ciclu 1..100 reuşim să obţinem din fişierul iniţial "MT1_09_i.pdf", 100 de fişiere PDF de câte o pagină, omiţând "adnotările" şi "filigranele" existente iniţial. Repetând pentru "MT1_09_ii.pdf" şi apoi pentru "MT1_09_iii.pdf" şi aplicând în final procedura descrisă în [4], reuşim să obţinem fişierele PDF de care avem nevoie pentru dezvoltarea site-ului //bacmath (a vedea [1], [3]) - cele 100 de fişiere de câte o singură pagină, conţinând cele trei subiecte ale câte unei variante, fără diversele elemente suplimentare existente iniţial.

vezi Cărţile mele (de programare)

docerpro | Prev | Next