GIF (Graphics Interchange Format) on vuosina 1987–89 kehitetty kuvatiedostomuoto, joka käyttää LZW-pakkausalgoritmia (Lempel-Ziv-Welch).
Tämä ohjelma muuntaa RGB-raakadatatiedostoja GIF-tiedostoiksi ja päinvastoin. Ohjeita saa ajamalla ohjelman ilman komentoriviparametreja. Ohjelma osaa purkaa lomitettuja GIF:ejä muttei pakata niitä. Se purkaa monta kuvaa sisältävistä tiedostoista vain ensimmäisen kuvan. Purkaminen on nopeaa mutta pakkaaminen hidasta.
RGB-raakadatatiedostoissa kukin tavu vastaa yhden pikselin yhtä RGB-komponenttia. Kolme ensimmäistä tavua ovat siis ensimmäisen pikselin punainen, vihreä ja sininen komponentti. Kuvan pikselien järjestys on ensin oikealle, sitten alas. RGB-raakadatatiedostoja voi lukea ja kirjoittaa esim. GIMP:illä (tiedostopääte .data).
"""Purkaa ja pakkaa GIF-tiedostoja. Versio 1.42. Tässä ohjelmassa käytetyt GIF-tiedostomuotoon liittyvät lyhenteet: LSD: Logical Screen Descriptor; tiedot piirtoalueesta, jolle kaikki GIF:in sisältämät kuvat piirretään GCT: Global Color Table; yksi paletti, jota voivat käyttää kaikki GIF:in sisältämät kuvat LCT: Local Color Table; GIF:in sisältämän yksittäisen kuvan oma paletti LZW: Lempel-Ziv-Welch, GIF:in kuvadatan pakkausalgoritmi""" import math import os import struct import sys import time # paletin bittisyys LZW-koodauksessa vähintään (2...8, yleensä 2); # huom.: LZW-koodien minimipituus on yhtä suurempi MIN_LZW_PALETTE_BITS = 2 # LZW-koodien maksimipituus pakattaessa (9...12, yleensä 12) MAX_LZW_ENCODE_BITS = 12 # paljonko tietoa tulostetaan (0 = vain virheet, 1 = vähän, 2 = paljon) VERBOSITY = 0 # ohjeteksti HELP_TEXT = """\ Purkaa GIF-tiedoston RGB-raakadataksi tai pakkaa RGB-raakadatan GIF:iksi. (RGB-raakadatan tavut: R,G,B,R,G,B,...; tiedostopääte .data GIMP:issä.) Komentoriviparametrit purettaessa: LÄHDE KOHDE LÄHDE luettava GIF-tiedosto KOHDE kirjoitettava RGB-raakadatatiedosto Komentoriviparametrit pakattaessa: LÄHDE LEVEYS KOHDE LÄHDE luettava RGB-raakadatatiedosto LEVEYS luettavan kuvan leveys pikseleinä KOHDE kirjoitettava GIF-tiedosto""" # --- Purku & pakkaus ----------------------------------------------------------------------------- def read_bytes(handle, length): """Lue tavuja tiedostosta tai anna virheilmoitus, jos tiedosto loppuu kesken.""" data = handle.read(length) if len(data) < length: raise Exception("EOF") return data def skip_bytes(handle, length): """Ohita tavuja tiedostossa tai anna virheilmoitus, jos tiedosto loppuu kesken.""" origPos = handle.tell() handle.seek(length, 1) if handle.tell() - origPos < length: raise Exception("EOF") def read_subblocks(handle): """Lue alilohkoihin kääritty data tiedostokahvan nykyisestä sijainnista alkaen. handle: tiedostokahva. Palautusarvo: data ilman alilohkokäärettä.""" # Alilohkoissa on ensin pituustavu ja sitten sen mukainen määrä varsinaisia # datatavuja. Viimeinen alilohko on pituudeltaan nolla. data = bytearray() subblockSize = read_bytes(handle, 1)[0] # ensimmäisen alilohkon koko while subblockSize: chunk = read_bytes(handle, subblockSize + 1) data.extend(chunk[:-1]) # alilohkon sisältö subblockSize = chunk[-1] # seuraavan alilohkon koko return bytes(data) def skip_subblocks(handle): """Ohita alilohkoihin kääritty data tiedostokahvan nykyisestä sijainnista alkaen. handle: tiedostokahva.""" # Katso myös aliohjelman read_subblocks() kommentit. subblockSize = read_bytes(handle, 1)[0] # ensimmäisen alilohkon koko while subblockSize: skip_bytes(handle, subblockSize) # ohita sisältö subblockSize = read_bytes(handle, 1)[0] # seuraavan alilohkon koko def format_palette(palette): """Muotoile paletti tulostettavaksi.""" return ",".join( "".join(f"{c:02x}" for c in palette[i*3:(i+1)*3]) for i in range(len(palette) // 3) ) # --- Purku --------------------------------------------------------------------------------------- def read_image_info(handle): """Lue GIF-tiedoston yksittäisen kuvan perustiedot. handle: tiedostokahva; huom.: sijainnin pitää olla valmiiksi Image Descriptorin tyyppitavun (",") jälkeisessä tavussa. Palautusarvo: dict: width : kuvan leveys pikseleinä (1...65535) height : kuvan korkeus pikseleinä (1...65535) interlace : onko kuva lomitettu (bool) palAddr : LCT:n alkuosoite (None = ei LCT:tä) palBits : LCT:n bittisyys (1...8; None = ei LCT:tä) LZWPalBits: paletin (GCT/LCT) bittisyys LZW-koodauksessa (2...8) LZWAddr : LZW-datan alkuosoite tiedostossa""" # lue loput Image Descriptorista (width, height, packedFields) = struct.unpack("<4x2HB", read_bytes(handle, 9)) if min(width, height) == 0: raise Exception("IMAGE_AREA_ZERO") # kuvan ala on nolla # jos kuvalla on LCT, ota muistiin sen osoite ja bittisyys sekä ohita se toistaiseksi if packedFields >> 7: palAddr = handle.tell() palBits = (packedFields & 7) + 1 skip_bytes(handle, 2 ** palBits * 3) else: palAddr = None palBits = None LZWPalBits = read_bytes(handle, 1)[0] # paletin bittisyys LZW-koodauksessa if not 2 <= LZWPalBits <= 11: raise Exception("INVALID_LZW_PALETTE_BITS") LZWAddr = handle.tell() # ota muistiin LZW-kuvadatan alkuosoite skip_subblocks(handle) # ohita LZW-kuvadata return { "width": width, "height": height, "interlace": bool((packedFields >> 6) & 1), "palAddr": palAddr, "palBits": palBits, "LZWPalBits": LZWPalBits, "LZWAddr": LZWAddr, } def skip_image(handle): """Ohita yksittäinen kuva GIF-tiedostossa. handle: tiedostokahva; huom: sijainnin pitää olla valmiiksi Image Descriptorin tyyppitavun (",") jälkeisessä tavussa.""" # lue loput Image Descriptorista, ota packedFields-tavu packedFields = read_bytes(handle, 9)[8] # ohita mahdollinen LCT if packedFields >> 7 == 1: palBits = (packedFields & 0b111) + 1 paletteSize = 2 ** palBits * 3 skip_bytes(handle, paletteSize) skip_bytes(handle, 1) # ohita tavu, jossa on paletin bittisyys LZW-koodauksessa skip_subblocks(handle) # ohita LZW-kuvadata def skip_extension_block(handle): """Ohita Extension-lohko (paitsi tulosta kommenttiteksti). handle: tiedostokahva; huom.: sijainnin pitää olla valmiiksi Extension Introducer -tavun ("!") jälkeisessä tavussa.""" extensionLabel = read_bytes(handle, 1)[0] # lohkon alityyppi (Extension Label) if extensionLabel == 0x01: # Plain Text Extension skip_bytes(handle, 13) skip_subblocks(handle) elif extensionLabel == 0xf9: # Graphic Control Extension skip_bytes(handle, 6) elif extensionLabel == 0xfe: # Comment Extension text = read_subblocks(handle).decode("ascii", errors="backslashreplace") print(f'Kommenttiteksti: "{text}"') elif extensionLabel == 0xff: # Application Extension skip_bytes(handle, 12) skip_subblocks(handle) else: raise Exception("INVALID_EXTENSION_LABEL") def read_GIF_blocks(handle): """Käy GIF-tiedoston lohkot läpi (poislukien Header, LSD, GCT). handle: tiedostokahva; huom: sijainnin pitää olla valmiiksi LSD:n tai mahdollisen GCT:n jälkeisessä tavussa. Palauta tiedoston ensimmäisen kuvan tiedot (katso read_image_info()) tai None, jos kuvia ei ollut yhtään.""" firstImageInfo = None # ensimmäisen kuvan tiedot while True: blockType = read_bytes(handle, 1) # lohkon tyyppi if blockType == b",": # kuvan tiedot (Image Descriptor) if firstImageInfo is None: firstImageInfo = read_image_info(handle) # lue ensimmäinen else: skip_image(handle) # ohita muut elif blockType == b"!": # Extension skip_extension_block(handle) elif blockType == b";": # Trailer return firstImageInfo else: raise Exception("INVALID_BLOCK_TYPE") def read_GIF(handle): """Lue GIF-tiedoston perustiedot, jotka voidaan antaa muille funktioille koko tiedoston lukemiseksi. Huom.: jos tiedosto sisältää useamman kuvan, palauta vain ensimmäisen kuvan tiedot. handle: tiedostokahva. Palautusarvo: dict: width : kuvan leveys pikseleinä (1...65535) height : kuvan korkeus pikseleinä (1...65535) interlace : onko kuva lomitettu (bool) palAddr : paletin (GCT/LCT) alkuosoite palBits : paletin (GCT/LCT) bittisyys (1...8) LZWPalBits: paletin (GCT/LCT) bittisyys LZW-koodauksessa (2...8) LZWAddr : kuvadatan alkuosoite tiedostossa""" handle.seek(0) # lue Header ja LSD (id_, version, packedFields) = struct.unpack("<3s3s4xB2x", read_bytes(handle, 13)) if id_ != b"GIF": raise Exception("NOT_A_GIF_FILE") # ei GIF-tiedosto if version not in (b"87a", b"89a"): print("Varoitus: tuntematon GIF:in versio.", file=sys.stderr) # jos GCT on, ota muistiin osoite ja bittisyys sekä ohita if packedFields >> 7: palAddr = handle.tell() palBits = (packedFields & 7) + 1 skip_bytes(handle, 2 ** palBits * 3) else: palAddr = None palBits = None imageInfo = read_GIF_blocks(handle) # tiedoston ensimmäisen kuvan tiedot if imageInfo is None: raise Exception("NO_IMAGES_IN_FILE") # tiedostossa ei ole kuvia if imageInfo["palAddr"] is not None: # kuvalla on LCT; käytä sitä GCT:n sijasta palAddr = imageInfo["palAddr"] palBits = imageInfo["palBits"] elif palAddr is None: raise Exception("NO_PALETTE") # ei LCT:tä eikä GCT:tä return { "width": imageInfo["width"], "height": imageInfo["height"], "interlace": imageInfo["interlace"], "palAddr": palAddr, "palBits": palBits, "LZWPalBits": imageInfo["LZWPalBits"], "LZWAddr": imageInfo["LZWAddr"], } def decode_LZW_data(LZWData, palBits): """Pura LZW-kuvadata. LZWData: tavujono, palBits: paletin bittisyys LZW-koodauksessa (2...8). Generoi indeksoitu kuvadata tavujonoina (1 tavu/pikseli).""" # Purettava data koostuu koodeista: # - pituus: # - vaihtelee # - vähintään palBits+1 (3...9) bittiä # - enintään 12 bittiä # - bittijärjestys: esimerkki tavujen muuntamisesta koodeiksi: # datan ensimmäiset tavut: ABCDEFGH, IJKLMNOP, QRSTUVWX # -> 6-bittiset koodit: CDEFGH, MNOPAB, WXIJKL, QRSTUV # EI siis esim. seuraavasti: # - CDEFGH, ABMNOP, IJKLWX, QRSTUV (väärin!) # - ABCDEF, GHIJKL, MNOPQR, STUVWX (väärin!) # - ABCDEF, IJKLGH, QRMNOP, STUVWX (väärin!) # - ensimmäisenä alustuskoodi # - viimeisenä loppukoodi # Purkuohjelman sisäinen tila: # - seuraavan koodin pituus purettavassa datassa: # - vähintään palBits+1 (3...9) bittiä # - enintään 12 bittiä # - sanakirja: # - kukin sana: tavujono # - alustetun sanakirjan sanat: # - ensimmäiset 4/8/16/32/64/128/256 (riippuen palBits:in arvosta): # yksi pikseli kutakin väriä (indeksiä) # - alustuskoodi (arvolla ei väliä) # - loppukoodi (arvolla ei väliä) # - loput sanat: usean pikselin yhdistelmät # - yhteensä enintään 4096 (2**12) sanaa if VERBOSITY >= 2: print("LZW-purkuloki (tavuosoite, bittiosoite, koodin pituus, sanakirjan koko, koodi):") # vakiot clearCode = 2 ** palBits # koodi, jolla alustetaan LZW-sanakirja endCode = 2 ** palBits + 1 # koodi, jolla data loppuu initialDictLen = 2 ** palBits + 2 # alustetun sanakirjan koko minCodeLen = palBits + 1 # koodien minimipituus (3...9 bittiä) # muuttujat bytePos = 0 # tavuja luettu kuvadatasta bitPos = 0 # bittejä luettu seuraavasta tavusta (0...7) codeLen = minCodeLen # koodien nykyinen pituus bitteinä prevEntry = None # edellinen sanakirjan sana # sanakirja; indeksi = koodi, # arvo = sana: (koodiviittaus joka muodostaa alun, lopputavu); # huom.: alustus- ja loppukoodin arvoilla ei ole väliä LZWDict = [(-1, i) for i in range(initialDictLen)] # tilastoja varten maxCodeLen = minCodeLen codeCount = 0 clearCodeCount = 0 pixelCount = 0 uniqueColors = set() while True: if bitPos + codeLen > (len(LZWData) - bytePos) * 8: raise Exception("EOF") # kuvadata loppui kesken # lue koodi jäljellä olevan datan alusta code = LZWData[bytePos] if codeLen > 8 - bitPos: code |= LZWData[bytePos+1] << 8 if codeLen > 16 - bitPos: code |= LZWData[bytePos+2] << 16 code >>= bitPos code &= (1 << codeLen) - 1 if VERBOSITY >= 2: print(",".join(str(n) for n in (bytePos, bitPos, codeLen, len(LZWDict), code))) codeCount += 1 # siirry datassa eteenpäin bytePos += (bitPos + codeLen) // 8 bitPos = (bitPos + codeLen) % 8 if code == clearCode: # sanakirjan alustus LZWDict = LZWDict[:initialDictLen] codeLen = minCodeLen prevCode = None clearCodeCount += 1 elif code == endCode: # datan loppu break else: # sanakirjan sana if prevCode is not None: # lisää sanakirjaan sana, joka koostuu edellisestä sanasta ja # koodin mukaisen sanan ensimmäisestä tavusta if code < len(LZWDict): suffixCode = code elif code == len(LZWDict): suffixCode = prevCode else: raise Exception("INVALID_LZW_CODE") # hae koodin mukaisen sanan ensimmäinen tavu while suffixCode != -1: (suffixCode, suffixByte) = LZWDict[suffixCode] LZWDict.append((prevCode, suffixByte)) prevCode = None if code < len(LZWDict) < 2 ** 12: # nykyinen sana muistiin prevCode = code if len(LZWDict) == 2 ** codeLen and codeLen < 12: # lisää koodien pituutta codeLen += 1 maxCodeLen = max(maxCodeLen, codeLen) # muunna sana tavujonoksi ja generoi se entry = bytearray() while code != -1: (code, byte) = LZWDict[code] entry.append(byte) uniqueColors.add(byte) yield entry[::-1] pixelCount += len(entry) if VERBOSITY >= 1: sizeBits = bytePos * 8 + bitPos print("LZW-kuvadata purettu:") print(f" bittejä : {sizeBits}") print(f" koodeja : {codeCount}") print(f" alustuskoodeja : {clearCodeCount}") print(f" pikseleitä : {pixelCount}") print(f" paletin koodaus : {palBits}-bittinen") print(f" eri paletti-indeksejä: {len(uniqueColors)}") print(f" bittiä/koodi : {sizeBits/codeCount:.2f}") print(f" bittiä/pikseli : {sizeBits/pixelCount:.2f}") print(f" pikseliä/koodi : {pixelCount/codeCount:.2f}") print(f" koodien minimipituus : {minCodeLen} bittiä") print(f" koodien maksimipituus: {maxCodeLen} bittiä") def deinterlace_image(imageData, width): """Poista kuvadatasta lomitus vaihtamalla pikselirivien järjestys. imageData: indeksoitu kuvadata (tavujono, 1 tavu/pikseli), width: kuvan leveys pikseleinä. Palautusarvo: indeksoitu kuvadata (tavujono, 1 tavu/pikseli). Lomitetun kuvadatan rakenne: osa A: joka 8. rivi alkaen rivistä 0 (0, 8, 16, ...) osa B: joka 8. rivi alkaen rivistä 4 (4, 12, 20, ...) osa C: joka 4. rivi alkaen rivistä 2 (2, 6, 10, ...) osa D: joka 2. rivi alkaen rivistä 1 (1, 3, 5, ...) Lomittamattomat rivit tulevat siis 8 rivin jaksoissa seuraavista osista: A, D, C, D, B, D, C, D, ...""" height = len(imageData) // width # miltä riveiltä lomitetun datan osat B, C ja D alkavat partBStart = (height + 7) // 8 # = rivejä osassa A partCStart = (height + 3) // 4 # = rivejä osissa A ja B partDStart = (height + 1) // 2 # = rivejä osissa A, B ja C # luo uusi kuvadata, jossa pikselirivien järjestys on vaihdettu; # sy = lähderivi, dy = kohderivi deinterlacedData = bytearray() for dy in range(height): if dy % 8 == 0: sy = dy // 8 elif dy % 8 == 4: sy = partBStart + dy // 8 elif dy % 4 == 2: sy = partCStart + dy // 4 else: sy = partDStart + dy // 2 deinterlacedData.extend(imageData[sy*width:(sy+1)*width]) return bytes(deinterlacedData) def GIF_to_raw_image(GIFHandle, rawHandle): """Muunna GIF-tiedosto raakadatakuvaksi (tavut: R,G,B,R,G,B,...). Rajoitukset: katso read_GIF():in kuvaus. GIFHandle: luettava tiedostokahva, rawHandle: kirjoitettava tiedostokahva""" info = read_GIF(GIFHandle) # lue perustiedot # lue paletti (GCT tai LCT; tavuja muodossa RGBRGB...) GIFHandle.seek(info["palAddr"]) palette = read_bytes(GIFHandle, 2 ** info["palBits"] * 3) if VERBOSITY >= 1: uniqueColorCount = len(set( palette[i*3:(i+1)*3] for i in range(len(palette) // 3) )) print(f"Puretaan {os.path.basename(GIFHandle.name)}:") print(f" leveys : {info['width']}") print(f" korkeus : {info['height']}") print(f" lomitettu : {['ei','kyllä'][info['interlace']]}") print(f" paletti : {info['palBits']}-bittinen") print(f" eri värejä paletissa: {uniqueColorCount}") if VERBOSITY >= 2: print("Paletti:", format_palette(palette)) # lue LZW-kuvadata GIFHandle.seek(info["LZWAddr"]) imageData = read_subblocks(GIFHandle) # pura LZW-kuvadata muotoon 1 tavu/pikseli imageData = b"".join(decode_LZW_data(imageData, info["LZWPalBits"])) if info["palBits"] < 8 and max(imageData) >= 2 ** info["palBits"]: # kuvadata sisältää liian suuren indeksin raise Exception("INVALID_INDEX_IN_IMAGE_DATA") if info["interlace"]: imageData = deinterlace_image(imageData, info["width"]) # pura lomitus # kirjoita raakadatakuvatiedosto rawHandle.seek(0) for pixel in imageData: rawHandle.write(palette[pixel*3:(pixel+1)*3]) # --- Pakkaus ------------------------------------------------------------------------------------- def get_palette_from_raw_image(handle): """Luo paletti raakadatakuvalle (tavut: R,G,B,R,G,B,...; enintään 256 väriä). handle: tiedostokahva. Palauta paletti (tavut: RGBRGB...)""" pixelCount = handle.seek(0, 2) // 3 # pikselien määrä tiedostokoosta palette = set() handle.seek(0) for pos in range(pixelCount): color = handle.read(3) if color not in palette: if len(palette) == 256: raise Exception("TOO_MANY_COLORS") palette.add(color) return b"".join(sorted(palette)) def raw_image_to_indexed(handle, palette): """Sovita raakadatakuva (tavut: R,G,B,R,G,B,...; enintään 256 väriä) palettiin. handle: tiedostokahva, palette: paletti (tavut: RGBRGB...) Palauta indeksoitu kuvadata (1 tavu/pikseli).""" # RGB -> indeksi RGBToIndex = dict((palette[i*3:(i+1)*3], i) for i in range(len(palette) // 3)) pixelCount = handle.seek(0, 2) // 3 # pikselien määrä tiedostokoosta handle.seek(0) imageData = bytearray() for pos in range(pixelCount): imageData.append(RGBToIndex[handle.read(3)]) return imageData def get_palette_bits(colorCount): """Laske, montako bittiä värien koodaamiseen tarvitaan. colorCount: värien määrä (1...256). Palautusarvo: bittien määrä (1...8).""" # 2-kantainen logaritmi pyöristettynä ylöspäin, kuitenkin vähintään 1 return max(math.ceil(math.log2(colorCount)), 1) def encode_LZW_data(palette, imageData): """Pakkaa kuvadata LZW:ksi. Parametrit: palette: paletti (tavut: RGBRGB...) imageData: indeksoitu kuvadata (1 tavu/pikseli) Generoi pakattu kuvadata tavuina.""" # katso kommentit decode_LZW_data():ssa if VERBOSITY >= 2: print("LZW-pakkausloki (pikseliosoite, koodin pituus, sanakirjan koko, koodi):") # vakiot # paletin bittisyys LZW-koodauksessa (2...8; GIF ei tue 1:ä) palBits = max(get_palette_bits(len(palette) // 3), MIN_LZW_PALETTE_BITS) palSize = clearCode = 2 ** palBits # paletin koko ja LZW-alustuskoodi endCode = 2 ** palBits + 1 # LZW-loppukoodi minCodeLen = palBits + 1 # LZW-koodien minimipituus (3...9 bittiä) # muuttujat inputPos = 0 # sijainti luettavassa kuvadatassa codeLen = minCodeLen # LZW-koodien pituus LZWByte = 0 # seuraavaksi kirjoitettava tavu LZWBitPos = 0 # LZWByte:n koko bitteinä # sanakirja: {sana: koodi, ...}; huom.: ei sisällä alustus- ja loppukoodeja, # joten koko on oikeasti kahta suurempi LZWDict = dict((bytes((i,)), i) for i in range(palSize)) # tilastoja varten maxCodeLen = minCodeLen codeCount = 0 clearCodeCount = 0 outputByteCount = 0 # kirjoita alustuskoodi LZWByte = clearCode LZWBitPos = codeLen while LZWBitPos >= 8: yield LZWByte & 0xff outputByteCount += 1 LZWByte >>= 8 LZWBitPos -= 8 clearCodeCount += 1 while inputPos < len(imageData): # miten pitkä on pisin sanakirjan sana, jolla jäljelläoleva kuvadata alkaa? for length in range(1, len(imageData) - inputPos + 1): if bytes(imageData[inputPos:inputPos+length]) in LZWDict: prefixLen = length else: break # hae kyseistä sanaa vastaava koodi code = LZWDict[bytes(imageData[inputPos:inputPos+prefixLen])] if VERBOSITY >= 2: print(",".join(str(n) for n in (inputPos, codeLen, len(LZWDict) + 2, code))) # generoi koodi tavuina LZWByte |= code << LZWBitPos LZWBitPos += codeLen while LZWBitPos >= 8: yield LZWByte & 0xff outputByteCount += 1 LZWByte >>= 8 LZWBitPos -= 8 if inputPos + prefixLen < len(imageData): # ei olla viimeisissä koodattavissa pikseleissä; lisää sana sanakirjaan if len(LZWDict) + 2 < 2 ** MAX_LZW_ENCODE_BITS - 1: # sanakirjassa on tilaa; lisää koodatut pikselit plus seuraava pikseli LZWDict[bytes(imageData[inputPos:inputPos+prefixLen+1])] = len(LZWDict) + 2 if len(LZWDict) + 2 == 2 ** codeLen + 1: # nykyisellä koodinpituudella ei mahdu lisää koodeja; kasvata sitä codeLen += 1 maxCodeLen = max(maxCodeLen, codeLen) else: # sanakirjassa on tilaa enää alustuskoodille # generoi alustuskoodi tavuina LZWByte |= clearCode << LZWBitPos LZWBitPos += codeLen while LZWBitPos >= 8: yield LZWByte & 0xff outputByteCount += 1 LZWByte >>= 8 LZWBitPos -= 8 clearCodeCount += 1 # alusta sanakirja ja koodien pituus LZWDict = dict((bytes((i,)), i) for i in range(palSize)) codeLen = minCodeLen inputPos += prefixLen # siirry juuri koodatun kuvadatan osan yli codeCount += 1 # kirjoita loppukoodi LZWByte |= endCode << LZWBitPos LZWBitPos += codeLen while LZWBitPos > 0: yield LZWByte & 0xff outputByteCount += 1 LZWByte >>= 8 LZWBitPos -= 8 if VERBOSITY >= 1: pixelCount = len(imageData) sizeBits = outputByteCount * 8 + LZWBitPos # LZWBitPos on -7...0 print("LZW-kuvadata pakattu:") print(f" pikseleitä : {pixelCount}") print(f" paletin koodaus : {palBits}-bittinen") print(f" bittejä : {sizeBits}") print(f" koodeja : {codeCount}") print(f" alustuskoodeja : {clearCodeCount}") print(f" bittiä/koodi : {sizeBits/codeCount:.2f}") print(f" bittiä/pikseli : {sizeBits/pixelCount:.2f}") print(f" pikseliä/koodi : {pixelCount/codeCount:.2f}") print(f" koodien minimipituus : {minCodeLen} bittiä") print(f" koodien maksimipituus: {maxCodeLen} bittiä") def write_GIF(width, height, palette, LZWData, handle): """Kirjoita GIF-tiedosto (GIF87a, GCT, yksi kuva). Parametrit: width: leveys pikseleinä height: korkeus pikseleinä palette: paletti (tavut: RGBRGB...) LZWData: LZW-pakattu kuvadata ilman alilohkokäärettä (tavujono) handle: tiedostokahva""" palBits = get_palette_bits(len(palette) // 3) # paletin bittisyys if VERBOSITY >= 1: print(f"Kirjoitetaan paletti {palBits}-bittisenä.") # täytä paletti mustalla bittien sallimaan maksimiin saakka palette = palette + (2 ** palBits * 3 - len(palette)) * b"\x00" handle.seek(0) # Header ja LSD (käytä GCT:tä) packedFields = 0x80 | (palBits - 1) handle.write(struct.pack("<6s2H3B", b"GIF87a", width, height, packedFields, 0, 0)) handle.write(palette) # GCT # Image Descriptor (ei LCT:tä) imgDesc = struct.pack("<B4HB", ord(b","), 0, 0, width, height, 0x00) handle.write(imgDesc) # paletin bittisyys LZW-koodauksessa (2...8; GIF ei tue 1:ä) handle.write(bytes((max(palBits, MIN_LZW_PALETTE_BITS),))) # kirjoita LZW-kuvadata alilohkoihin (enintään 255 datatavua kuhunkin) LZWPos = 0 while LZWPos < len(LZWData): subblockSize = min(255, len(LZWData) - LZWPos) handle.write( bytes((subblockSize,)) + LZWData[LZWPos:LZWPos+subblockSize] ) LZWPos += subblockSize # tyhjä alilohko (nollatavu) ja Trailer (";") handle.write(b"\x00;") def raw_image_to_GIF(rawHandle, width, GIFHandle): """Muunna raakadatakuva (tavut: R,G,B,R,G,B,...; enintään 256 väriä) GIF-tiedostoksi. Parametrit: rawHandle: luettava tiedostokahva width: kuvan leveys pikseleinä GIFHandle: kirjoitettavan tiedoston kahva""" # korkeus ja jakojäännös tiedostokoosta ja leveydestä (height, remainder) = divmod(rawHandle.seek(0, 2), width * 3) if remainder: sys.exit("Lähdetiedoston koko ei ole jaollinen (leveys * 3):lla.") if height == 0: sys.exit("Lähdetiedosto on tyhjä.") if height > 65535: sys.exit("Kohdetiedostosta tulisi liian korkea.") palette = get_palette_from_raw_image(rawHandle) # hae paletti if VERBOSITY >= 1: print(f"Pakataan {os.path.basename(rawHandle.name)}:") print(f" leveys : {width}") print(f" korkeus: {height}") print(f" värejä : {len(palette)//3}") if VERBOSITY >= 2: print("Paletti:", format_palette(palette)) imageData = raw_image_to_indexed(rawHandle, palette) # indeksoi kuvadata LZWData = bytes(encode_LZW_data(palette, imageData)) # pakkaa kuvadata # kirjoita GIF-tiedosto write_GIF(width, height, palette, LZWData, GIFHandle) # ------------------------------------------------------------------------------------------------- if __name__ == "__main__": # aloita ajanotto startTime = time.time() # lue komentoriviparametrit ja päättele niiden määrästä, mitä tehdään if len(sys.argv) == 3: decode = True (source, target) = sys.argv[1:] elif len(sys.argv) == 4: decode = False (source, width, target) = sys.argv[1:] try: width = int(width, 10) if not 1 <= width <= 65535: raise ValueError except ValueError: sys.exit("Kuvan leveyden pitää olla väliltä 1...65535.") else: exit(HELP_TEXT) if not os.path.isfile(source): sys.exit("Lähdetiedostoa ei löydy.") if os.path.exists(target): sys.exit("Kohdetiedosto on jo olemassa.") # avaa tiedostot ja pura tai pakkaa try: with open(source, "rb") as sourceHnd, open(target, "wb") as targetHnd: if decode: GIF_to_raw_image(sourceHnd, targetHnd) else: raw_image_to_GIF(sourceHnd, width, targetHnd) except OSError: exit("Virhe luettaessa tai kirjoitettaessa tiedostoja.") if VERBOSITY >= 1: print(f"Aikaa kului {time.time()-startTime:.1f} s.") print()
Uusi versio.
Uusi versio.
Aihe on jo aika vanha, joten et voi enää vastata siihen.