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).