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

Reducerea la regiunea vizibilă şi "împăturirea" unor pagini PDF

PDF | Python
2013 aug

Din [1], avem un director D/ conţinând 300 de fişiere PDF de câte o pagină "A4"; câte 100 dintre ele reprezintă subiectul I, II şi respectiv III ale variantelor de subiecte de bacalaureat "MT1_2009".

Pentru fiecare variantă 1..100, accesăm cele trei fişiere aferente ei, reducem paginile acestora de la formatul "A4" la regiunea vizibilă (exceptând lăţimea) şi creem un fişier PDF care "împătureşte" paginile tocmai reduse; datorită reducerii pe verticală, vizualizatorul va arăta aceste trei pagini aproape la fel ca în cazul uneia singure:

fişierul var_087.pdf

Determinarea coordonatelor regiunii vizibile este dificilă, necesitând în principiu analiza corelativă a tuturor operatorilor de poziţionare (pentru secvenţe de text, forme de caracter specifice unui font sau altul, etc.) existenţi în stream-urile de text ale obiectului PDF corespunzător paginii.

Reducerea la "regiunea vizibilă"

Ghostscript este un interpretor de PDF care este lansat prin comanda gs (în cazul Linux-ului) şi care între altele, are subrutine care determină marginile regiunii vizibile ale unei pagini:

vb@vb:~/bacmath/DOC/C/D$ gs -q -dNOPAUSE -dBATCH -sDEVICE=bbox i_001.pdf 2>&1
%%BoundingBox: 54 536 541 701
%%HiResBoundingBox: 54.611998 536.759984 540.089984 700.649979

2>&1 redirecţionează stderr către stdout ("ieşirea standard", ecranul) - ţinând cont de faptul că rezultatele "bbox" sunt înscrise de gs în "dispozitivul" standard de eroare.

Fişierele sunt denumite după şablonul "[i|ii|iii]_%03d.pdf % k", unde k=1..100 este numărul variantei; prin urmare, comanda ls D/*pdf va lista numele fişierelor în ordinea subiectelor şi a variantelor (de exemplu, pe liniile 15, 115 şi 215 avem "i_015.pdf", "ii_015.pdf" şi "iii_015.pdf"). Putem invoca gs o singură dată, transmiţându-i lista tuturor fişierelor (presupunem că directorul de lucru curent este D/):

#    "gs_bbox.sh"  (script Bash)
lista=`ls *.pdf`
gs -dNOPAUSE -dBATCH -sDEVICE=bbox $lista 2>&1 | grep ^%%B >> bbox.txt

grep filtrează numai linia care începe cu "%%B", iar rezultatul (pentru fişierul curent din listă) este adăugat (folosind operatorul >>) pe o nouă linie în fişierul "bbox.txt".

În fişierul "bbox.txt" rezultat prin execuţia scriptului de mai sus (≈20 de secunde), rândul de rang k = 1..300 conţine (dar prefixat cu "%%BoundingBox:") limitele regiunii vizibile ale fişierului care are deasemenea rangul k, în lista furnizată de ls.

Să eliminăm prefixul "%%BoundingBox: ", de pe toate liniile din "bbox.txt":

vb@vb:~/bacmath/DOC/C/D$ replace '%%BoundingBox: ' ''  -- bbox.txt

O linie din "bbox.txt" este precum 54 536 541 701, indicând colţul stânga-jos (54, 536) şi colţul dreapta-sus (541, 701) ale paginii; "marginea-stângă" este 54 puncte, iar "lăţimea paginii" este de (541 - 54) puncte. Marginea stângă este 54 la majoritatea paginilor, dar lăţimea diferă permanent; să fixăm pentru toate paginile marginile orizontale 54 şi respectiv la 595 puncte:

vb@vb:~/bacmath/DOC/C$ perl -pi -e 's/^\d+ (\d+) \d+/54 $1 595/g' D/bbox.txt

Se înlocuieşte secvenţa de şablon "Cifre Spaţiu (Cifre) Spaţiu Cifre" de la începutul fiecărui rând cu "54 $1 595", unde "$1" referă secvenţa grupată între paranteze.

Împăturirea paginilor

Funcţia convolve() foloseşte "bbox.txt" şi modulul Python pyPdf pentru a constitui în directorul "MT1_09/" fişierele PDF aferente celor 100 de variante, aşa cum am enunţat la început:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from pyPdf import PdfFileWriter, PdfFileReader
from pyPdf.generic import RectangleObject

def convolve():
    bbox = open('D/bbox.txt').read().splitlines()
    bbox = [[bb for bb in BB.split()] for BB in bbox]
    for k in xrange(100):
        mbs = [bbox[k], bbox[100 + k], bbox[200 + k]]
        files = ["D/%s_%03d.pdf" % (subj, k+1) 
                 for subj in ['i', 'ii', 'iii']]
        pags = [PdfFileReader(file(file_name, 'rb')).getPage(0) 
                for file_name in files]
        out = PdfFileWriter()
        for i in range(3):
            pags[i].mediaBox = pags[i].cropBox = RectangleObject(mbs[i])
            out.addPage(pags[i])
        out.write(file("MT1_09/var_%03d.pdf" % (k+1), "wb"))

convolve()

În linia 5 se obţine o listă în care elementul de rang h=0..299 este şirul de caractere (de exemplu, "54 536 595 701") de pe linia de rang h din fişierul "D/bbox.txt"; în linia 6 acest şir este transformat în listă, separându-l la "spaţiu" (pentru exemplul anterior: ["54", "536", "595", "701"]).

Coordonatele trebuie să fie numere (întregi sau "float"), dar nu este necesar să convertim acum (puteam folosi int(bb), în linia 6) fiindcă RectangleObject() prin care fixăm /MediaBox în linia 15, se îngrijeşte eventual şi de conversie.

În linia 8 se extrag (din lista obţinută în linia 6) sublistele aferente variantei curente (cea de rang k=0..99, fixat în linia 7), iar în linia 9-10 se constituie lista numelor celor trei fişiere aferente acestei variante. În linia 11-12 se obţine lista paginilor din aceste fişiere (ca obiecte PageObject(), returnate de metoda .getPage(0) a obiectelor PdfFileReader() create pentru fişierele respective).

Aceste pagini sunt adăugate (linia 16) într-un obiect PdfFileWriter() (creat în linia 13), după ce (în linia 15) le sunt setate proprietăţile /MediaBox şi /CropBox conform valorilor din linia 8; în linia 17 se foloseşte metoda .write() a acestui obiect, obţinând fişierul PDF corespunzător variantei curente ("împăturind" cele trei pagini).

În [2] obţineam un rezultat mai bun: fişierul final corespunzător variantei avea o singură pagină (conţinând cele trei subiecte); dar practic rezultatul obţinut acum (fişier care "împătureşte" trei pagini reduse) este la fel de bun - scopul era de a vizualiza cele trei subiecte într-un acelaşi spaţiu.

În [2] foloseam intermediar pdfcrop.pl (şi implicit, se apela şi pdftex)) şi eram obligaţi să operăm repetat cu patru fişiere temporare şi să invocăm anumite metode de translatare a paginii - timpul de execuţie total depăşind 2 minute. În schimb, programul de mai sus a necesitat sub 15 secunde, pentru execuţie (la care desigur, mai sunt de adăugat cele 20 de secunde necesitate de obţinerea fişierului intermediar "bbox.txt").

vezi Cărţile mele (de programare)

docerpro | Prev | Next