Naloga

V nekem direktoriju imamo več datotek s slikami. Napišite program, ki izpiše imena datotek s slikami in njihove dimenzije. Izpis mora biti takšen:

ameriski proracun energija.png 1130 x 529 ameriski proracun proc.png 1130 x 499 ameriski proracun.png 1128 x 500 box-office.png 968 x 1480 drzavni-dolg.png 560 x 336 ganttov-diagram-kriminalca.gif 907 x 658 nara.jpg 968 x 99 spent-day-eat-drink-15-24.png 951 x 167 Imena datotek so poravnana na levo in dolga 50 znakov. Dimenzije so zapisane v obliki "širina x višina", pri čemer sta širina in višina široki največ osem znakov, dimenzije pa so poravnane po x.

Program napišite tako, da bo imel funkcijo decode_png(fname), ki kot argument prejme ime datoteke s sliko v formatu PNG, kot rezultat pa vrne njeno širino in višino. Prav tako naj ima funkcijo decode_gif(fname), ki na enak način bere datoteke v formatu GIF. Vaš program mora podpirati vsaj tadva formata, neobvezno pa lahko - za prav lepo vajo - berete še format JPG.

Datoteka PNG vsebuje najprej 16 bajtov drugih informacij, sledita pa širina in višina, zapisana s po štirimi bajti po pravilu debelega konca (big endian). Datoteka GIF vsebuje najprej šest bajtov, sledita širina in višina, zapisana s po dvema bajtoma po pravilu tankega konca. Informacije o datoteki JPG, če se je boste lotili, poiščite sami; format je namreč nekoliko bolj zapleten, če ste iznajdljivi, pa bo zadoščala stran na Wikipediji in malo poskušanja.

Prosimo, da naloge oddajate kot datoteko .py, ne kot arhiv .rar ali .zip, saj jih tako lažje popravljamo.

Sprememba termina: na željo študentov je novi rok za oddaje nedelja ob 23.55.

Dodano 10.11: Pravilnost vaše rešitve preverite na datotekah slike.zip. Izpis naj izgleda tako kot je prikazano spodaj, le vrstni red datotek bo pri vas morda drugačen:

slika1.gif                                               10 x 10      
slika1.jpg                                               10 x 10      
slika1.png                                               10 x 10      
slika2.gif                                               10 x 100     
slika2.jpg                                               10 x 100     
slika2.png                                               10 x 100     
slika3.gif                                            65535 x 1       
slika3.jpg                                            32768 x 1       
slika3.png                                            65535 x 1       
slika4.png                                            65536 x 1       
slika5.png                                           524287 x 1       
slika6.png                                                1 x 524287 

Rešitev

Naloga je precej tehnična in preprosta. Funkcija, ki bere PNG, odpre datoteko, prebere 16 bajtov (in jih vrže stran), nato pa prebere naslednjih osem bajtov in prve štiri prebere kot širino, druge štiri kot višino, ter to vrne. Funkcija za GIF je podobna.

"Glavni program" gre prek datotek v direktoriju; pri tistih, katerih končnice pozna, pokliče ustrezno funkcijo, ostale preskoči (continue). Za slikovne datoteke izpišemo tri stvari: ime s širino 50, nato širino s pet znaki poravnanimi na desni, znak x in višino poravnano na levo.

def decode_png(fname): f = open(fname, "rb") f.read(16) s = f.read(8) w = s[0]*0x1000000 + s[1]*0x10000 + s[2]*0x100 + s[3] h = s[4]*0x1000000 + s[5]*0x10000 + s[6]*0x100 + s[7] return w, h def decode_gif(fname): f = open(fname, "rb") f.read(6) s = f.read(4) w = s[1]*0x100 + s[0] h = s[3]*0x100 + s[2] return w, h import os for fn in os.listdir("."): ext = os.path.splitext(fn)[1] if ext == ".png": w, h = decode_png(fn) elif ext == ".gif": w, h = decode_gif(fn) # tu bi dodali še druge formate, npr. jpg else: continue print("{:50} {:>8} x {:<8}".format(fn, w, h))

Neobvezna funkcija, ki bere JPG, je nekoliko bolj zapletena. Datoteka je namreč razdeljane v bloke, vsak se začne s 0xff, ki ga kar izpustimo, sledi oznaka bloka. V zanki while preskakujemo vse bloke, dokler ne naletimo na onega z oznako 0xc0, ki vsebuje podatke, ki jih potrebujemo. Blok 0xdd pomeni konec datoteke; če naletimo nanj, se delamo, da je slika velikosti 0x0. 0xdd je dolg dva bajta, bloki 0xd0 do 0xd8 so prazni, ostali pa se začnejo z dvema bajtoma, ki povesta dolžino bloka (vključno s tema dvema bajtoma).

V bloku 0xc0 (po zanki) preskočimo tri bajte, sledita pa višina in širina.

def decode_jpg(fname): f = open(fname, "rb") while True: marker = f.read(2)[1] if marker == 0xc0: break if marker == 0xd9: return 0, 0 if marker == 0xdd: f.read(2) if not 0xd0 <= marker <= 0xd8: s = f.read(2) sze = s[0]*0x100+s[1] f.read(sze-2) f.read(3) s = f.read(4) h = s[0]*0x100 + s[1] w = s[2]*0x100 + s[3] return w, h

Branje cele datoteke

Vse rešitve so napisane tako, da ne berejo cele datoteke, temveč le, kolikor je potrebno. Če želimo, pa lahko preberemo tudi celo datoteko in do podatkov, ki jih potrebujemo, dostopamo kar z indeksiranjem.

def decode_png(fname): s = open(fname, "rb").read() w = s[16]*0x1000000 + s[17]*0x10000 + s[18]*0x100 + s[19] h = s[20]*0x1000000 + s[21]*0x10000 + s[22]*0x100 + s[23] return w, h def decode_gif(fname): s = open(fname, "rb").read() w = s[7]*0x100 + s[6] h = s[9]*0x100 + s[8] return w, h

Ta rešitev je krajša. Da porabi nekoliko več pomnilnika, ji ne bomo zamerili, pač pa zna biti počasnejša, če gre za 10 MB velike slike, ki se nahajajo na, recimo, mrežnem disku v 100 megabitni mreži.

Pogoste "napake"

Kako bi lahko izboljšali naslednje tri programe? def decode_png(fname): f = open("C:/slike/" + fname, "rb").read(256) x = f[19]+f[18]*0x100+f[17]*0x10000+f[16]*0x1000000 y = f[23]+f[22]*0x100+f[21]*0x10000+f[20]*0x1000000 return (x, y) decode_png('slika1.png')
Funkcija decode_png je sicer pravilna a premalo splošna. Kako bi prebrali sliko v mapi E:\slikeNaKljucku ali direktoriju /tmp/slike? Funkcijo popravimo takole: def decode_png(fname): f = open(fname, "rb").read(256) x = f[19]+f[18]*0x100+f[17]*0x10000+f[16]*0x1000000 y = f[23]+f[22]*0x100+f[21]*0x10000+f[20]*0x1000000 return (x, y) decode_png('C:/slike/slika1.png') decode_png('E:/slikeNaKljucku/slika1.png')
for fname in direktorij: if fname[-4:] == ".png": (sirina, visina) = decode_png(fname) print("{:20} {:6} x {:<6}".format(fname, sirina, visina)) elif fname[-4:] == ".gif": (sirina, visina) = decode_gif(fname) print("{:20} {:6} x {:<6}".format(fname, sirina, visina)) elif fname[-4:] == ".jpg": (sirina, visina) = decode_jpg(fname) print("{:20} {:6} x {:<6}".format(fname, sirina, visina))
Vrstica print("{:20} {:6} x {:<6}".format(fname, sirina, visina)) se v programu pojavi trikrat. Morda bi bilo lepše napisati for fname in direktorij: if fname[-4:] == ".png": sirina, visina = decode_png(fname) elif fname[-4:] == ".gif": sirina, visina = decode_gif(fname) elif fname[-4:] == ".jpg": sirina, visina = decode_jpg(fname) else: continue print("{:20} {:6} x {:<6}".format(fname, sirina, visina))
def decode_gif(fname): #Funkcija decode prejme ime trenutne datoteke(.gif) fname = open(fname, "rb") #Jo odpre in jo shrani v filename x = fname.read() #Tukaj preberemo vsebino datoteke in jo shranimo v x y = x[6] + x[7]*0x100 #Potem vzamemo prva dva bajta(bajt 6 in 7) za širino in ju shranemo v y z = x[8] + x[9]*0x100 # ter vzamemo še druga dva bajta (bajt 8 in 9) za višino ter ju shrnemo v z return "{:>8} x {:<8}".format(y, z) #In ju vrnemo v obliki formata ter poravnana z x
Komentarji so super, saj naredijo kodo veliko bolj pregledno. Upam, da jih uporabljate. Vseeno pa z njimi ne pretiravati. Izogibajte se komentiranju vsake vrstice (i += 1 #i povečamo za ena) ampak jih uporabite le na mestih, kjer bodo bralcem vaše kode koristili (npr. pred funkcijo decode_png bi lahko napisali: Vrne velikost png slike).
Zadnja sprememba: sobota, 3. november 2012, 12.12