]> vgcfreebox.myrthtech.pt Git - ue-ccd-compressaodeimagensbinarias.git/commitdiff
second version delivered to teacher
authorVGoncalo <vitor.goncalo.costa@gmail.com>
Fri, 27 Mar 2026 15:39:34 +0000 (15:39 +0000)
committerVGoncalo <vitor.goncalo.costa@gmail.com>
Fri, 27 Mar 2026 15:39:34 +0000 (15:39 +0000)
21 files changed:
.DS_Store [new file with mode: 0644]
.idea/.gitignore [new file with mode: 0644]
.idea/ccd-projecto-61609&57098&70323.iml [new file with mode: 0644]
.idea/inspectionProfiles/Project_Default.xml [new file with mode: 0644]
.idea/inspectionProfiles/profiles_settings.xml [new file with mode: 0644]
.idea/misc.xml [new file with mode: 0644]
.idea/modules.xml [new file with mode: 0644]
arithmetic.py [new file with mode: 0644]
calculo_entropia.py [new file with mode: 0644]
huffman.py [new file with mode: 0644]
lz78/lz_78.py [new file with mode: 0644]
lz78/lz_78_2x2.py [new file with mode: 0644]
lz78/lz_78_rle.py [new file with mode: 0644]
lz78/lz_78_xor.py [new file with mode: 0644]
main.py [new file with mode: 0644]
pixel_character.pbm [new file with mode: 0644]
readme.txt [new file with mode: 0644]
relatório.pdf [new file with mode: 0644]
taxa_compressão.py [new file with mode: 0644]
tetris_example.pbm [new file with mode: 0644]
utils.py [new file with mode: 0644]

diff --git a/.DS_Store b/.DS_Store
new file mode 100644 (file)
index 0000000..92c1a9b
Binary files /dev/null and b/.DS_Store differ
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644 (file)
index 0000000..b58b603
--- /dev/null
@@ -0,0 +1,5 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/ccd-projecto-61609&57098&70323.iml b/.idea/ccd-projecto-61609&57098&70323.iml
new file mode 100644 (file)
index 0000000..1314836
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$" />
+    <orderEntry type="jdk" jdkName="Python 3.9 (EU-AAI-00)" jdkType="Python SDK" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644 (file)
index 0000000..e4e6bff
--- /dev/null
@@ -0,0 +1,12 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="PyStubPackagesAdvertiser" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="ignoredPackages">
+        <list>
+          <option value="pandas-stubs==2.3.3.251219" />
+        </list>
+      </option>
+    </inspection_tool>
+  </profile>
+</component>
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644 (file)
index 0000000..105ce2d
--- /dev/null
@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <settings>
+    <option name="USE_PROJECT_PROFILE" value="false" />
+    <version value="1.0" />
+  </settings>
+</component>
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644 (file)
index 0000000..aaf65ba
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (EU-AAI-00)" project-jdk-type="Python SDK" />
+</project>
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644 (file)
index 0000000..a15c890
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/ccd-projecto-61609&amp;57098&amp;70323.iml" filepath="$PROJECT_DIR$/.idea/ccd-projecto-61609&amp;57098&amp;70323.iml" />
+    </modules>
+  </component>
+</project>
\ No newline at end of file
diff --git a/arithmetic.py b/arithmetic.py
new file mode 100644 (file)
index 0000000..d9b1968
--- /dev/null
@@ -0,0 +1,16 @@
+import utils\r
+import sys\r
+\r
+_opt_type = sys.argv[1]\r
+_file_path = sys.argv[2]\r
+\r
+_file = utils.get_file(_file_path)\r
+\r
+\r
+if _opt_type == "c":\r
+    _pbm_content = utils.BitMapFile(_file)\r
+    _, _result_option_2 = utils.arithmetic_encode_image(_pbm_content)\r
+    print(_result_option_2)\r
+elif _opt_type == "d":\r
+    _result = utils.arithmetic_decode_file(_file_path)\r
+    print(_result)
\ No newline at end of file
diff --git a/calculo_entropia.py b/calculo_entropia.py
new file mode 100644 (file)
index 0000000..321cd44
--- /dev/null
@@ -0,0 +1,60 @@
+import math
+import sys
+
+def calcular_entropia(bitstream):
+    n = len(bitstream)
+    if n == 0: return 0
+    
+    p0 = bitstream.count('0') / n
+    p1 = bitstream.count('1') / n
+    
+    entropia = 0
+    for p in [p0, p1]:
+        if p > 0:
+            entropia -= p * math.log2(p)
+    return entropia
+
+def calcular_entropia_condicional(bitstream):
+    transicoes = {'00': 0, '01': 0, '10': 0, '11': 0}
+    contagem_base = {'0': 0, '1': 0}
+    
+    for i in range(len(bitstream) - 1):
+        par = bitstream[i:i+2]
+        transicoes[par] += 1
+        contagem_base[bitstream[i]] += 1
+    
+    h_condicional = 0
+    for base in ['0', '1']:
+        p_base = contagem_base[base] / (len(bitstream) - 1)
+        if p_base > 0:
+            h_local = 0
+            for prox in ['0', '1']:
+                p_transicao = transicoes[base + prox] / contagem_base[base]
+                if p_transicao > 0:
+                    h_local -= p_transicao * math.log2(p_transicao)
+            h_condicional += p_base * h_local
+            
+    return h_condicional
+
+def clean_image_data(image_data):
+    
+    clean_text = ""
+
+    for line in image_data:
+        line = line.strip()
+        line = line.replace(" ", "")
+        if not line or line.startswith('#'):
+            continue
+        clean_text += line
+
+    return clean_text
+
+with open(sys.argv[1], "r") as image:
+        lines = image.readlines()
+        image_data = clean_image_data(lines[2:])
+
+        h_x = calcular_entropia(image_data)
+        h_condicional = calcular_entropia_condicional(image_data)
+
+        print(f"Entropia: {h_x:.4f} bits/pixel")
+        print(f"Entropia Condicional: {h_condicional:.4f} bits/pixel")
\ No newline at end of file
diff --git a/huffman.py b/huffman.py
new file mode 100644 (file)
index 0000000..c4442f5
--- /dev/null
@@ -0,0 +1,18 @@
+import utils\r
+import sys\r
+\r
+_opt_type = sys.argv[1]\r
+_file_path = sys.argv[2]\r
+\r
+\r
+if _opt_type == "c":\r
+    _file = utils.get_file(_file_path)\r
+    _pbm_content = utils.BitMapFile(_file)\r
+    _result = utils.huffman_encode_image(_pbm_content)\r
+    with open(sys.argv[2]+".bin", "wb") as file:\r
+        file.write(_result)\r
+    print(_result)\r
+elif _opt_type == "d":\r
+    _result = utils.huffman_decode_file(_file_path)\r
+    with open(sys.argv[2].strip('.bin'), "w") as file:\r
+        file.write(_result)
\ No newline at end of file
diff --git a/lz78/lz_78.py b/lz78/lz_78.py
new file mode 100644 (file)
index 0000000..28dd34c
--- /dev/null
@@ -0,0 +1,140 @@
+import sys
+import struct
+
+########################################################################################
+# python lz_78.py <d/c> <caminho_para_o_ficheiro_original>
+# 
+# c: comprimir
+# d: descomprimir
+# 
+# Os ficheiros gerados ficarão na mesma pasta que o script
+########################################################################################
+
+def lz78_compression(image):
+    dictionary = {
+        0: ""
+    }
+
+    output = []
+
+    symbol = ""
+
+    for i in image:
+        if (symbol + i) in dictionary.values():
+            symbol += i
+        else:
+            if symbol == "":
+                output.append([0, i])
+                dictionary[len(dictionary)] = i
+            else:
+                output.append([list(dictionary.keys())[list(dictionary.values()).index(symbol)], i])
+                dictionary[len(dictionary)] = symbol + i
+            symbol = ""
+
+    if symbol != "":
+        idx = list(dictionary.keys())[list(dictionary.values()).index(symbol)]
+        output.append([idx, ""])
+
+    return output, dictionary
+
+def lz78_decompression(compressed_image):
+    dictionary = {
+        0: ""
+    }
+
+    output = ""
+
+    for i in compressed_image:
+        output += dictionary.get(i[0]) + i[1]
+        dictionary[len(dictionary)] = dictionary.get(i[0]) + i[1]
+
+    return output
+
+def clean_image_data(image_data):
+    
+    clean_text = ""
+
+    for line in image_data:
+        line = line.strip()
+        line = line.replace(" ", "")
+        if not line or line.startswith('#'):
+            continue
+        clean_text += line
+
+    return clean_text
+
+def chunkstring(string, length):
+    return [string[i : length+i] for i in range(0, len(string), length)]
+
+#Comprimir
+if sys.argv[1] == "c":
+
+    with open(sys.argv[2], "r") as image:
+        lines = image.readlines()
+        image_size = lines[1]
+
+        if lines[0].strip() != 'P1':
+            raise ValueError("This is not a pbm file")
+        
+        clean_text = clean_image_data(lines[2:]) #Limpa os espaços em branco para comprimir melhor
+
+        output, dic = lz78_compression(clean_text)
+
+        print(f"Output: {output}\n")
+        print(f"Dictionary: {dic}")
+
+        #Compressão em texto (human-readable)
+        with open(sys.argv[2] + "_compressed", "w") as save_file:
+            save_file.write(f"{output} \n{image_size}")
+        
+        #Compressão em binário (compressão "a sério")
+        with open(sys.argv[2] + ".bin", "wb") as save_file:
+            largura = int(image_size.split()[0])
+            altura = int(image_size.split()[1])
+            save_file.write(struct.pack('II', largura, altura))
+
+            for indice, simbolo in output:
+                simbolo_byte = ord(simbolo) if simbolo != "" else 0
+                save_file.write(struct.pack('HB', indice, simbolo_byte))
+#Descomprimir
+elif sys.argv[1] == "d":
+
+    #Descomprimir do ficheiro de texto
+    with open(sys.argv[2] + "_compressed", "r") as image:
+        lines = image.readlines()
+
+        compressed_image = eval(lines[0])
+        image_size = lines[1]
+        length = int(lines[1].split(" ")[0])
+        
+        output = lz78_decompression(compressed_image)
+        
+        chuncked_output = chunkstring(output, length)
+
+        with open(sys.argv[2] + "_decompressed", "w") as save_file:
+            save_file.write(f"P1\n{image_size}")
+            for line in chuncked_output:
+                save_file.write(f"{line}\n")
+    
+    #Descomprimir do binário
+    with open(sys.argv[2] + ".bin", "rb") as image:
+        header = image.read(8)
+        largura, altura = struct.unpack('II', header)
+        
+        compressed_image_bin = []
+        while True:
+            chunk = image.read(3)
+            if not chunk:
+                break
+            indice, simbolo_byte = struct.unpack('HB', chunk)
+            simbolo = chr(simbolo_byte) if simbolo_byte != 0 else ""
+            compressed_image_bin.append([indice, simbolo])
+
+        output = lz78_decompression(compressed_image_bin)
+
+        chuncked_output = chunkstring(output, largura)
+
+        with open(sys.argv[2]+ "_bin_decompressed", "w") as save_file:
+            save_file.write(f"P1\n{largura} {altura}\n")
+            for line in chuncked_output:
+                save_file.write(f"{line}\n")
diff --git a/lz78/lz_78_2x2.py b/lz78/lz_78_2x2.py
new file mode 100644 (file)
index 0000000..47aa2c8
--- /dev/null
@@ -0,0 +1,90 @@
+import sys
+import struct
+
+########################################################################################
+# Apenas comprimir
+#
+# python lz_78_2x2.py <caminho_para_o_ficheiro_original>
+# 
+# 
+# Os ficheiros gerados ficarão na mesma pasta que o script
+########################################################################################
+
+def lz78_compression(image):
+    dictionary = {0: ""}
+    output = []
+    symbol = ""
+
+    for i in image:
+        if (symbol + i) in dictionary.values():
+            symbol += i
+        else:
+            if symbol == "":
+                output.append([0, i])
+                dictionary[len(dictionary)] = i
+            else:
+                idx = list(dictionary.keys())[list(dictionary.values()).index(symbol)]
+                output.append([idx, i])
+                dictionary[len(dictionary)] = symbol + i
+            symbol = ""
+
+    if symbol != "":
+        idx = list(dictionary.keys())[list(dictionary.values()).index(symbol)]
+        output.append([idx, ""])
+
+    return output, dictionary
+
+def clean_image_data(image_data):
+    clean_text = ""
+    for line in image_data:
+        line = line.strip().replace(" ", "")
+        if not line or line.startswith('#'):
+            continue
+        clean_text += line
+    return clean_text
+
+def chunkstring(string, length):
+    return [string[i : length+i] for i in range(0, len(string), length)]
+
+def get_blocks(bitstream, largura, altura):
+    matriz = [bitstream[i*largura:(i+1)*largura] for i in range(altura)]
+    # Padding
+    if largura % 2 != 0:
+        matriz = [linha + '0' for linha in matriz]
+        largura += 1
+    if altura % 2 != 0:
+        matriz.append('0' * largura)
+        altura += 1
+    
+    blocos = []
+    for r in range(0, altura, 2):
+        for c in range(0, largura, 2):
+            bloco = matriz[r][c] + matriz[r][c+1] + matriz[r+1][c] + matriz[r+1][c+1]
+            blocos.append(bloco)
+    return blocos, largura, altura
+
+
+with open(sys.argv[1], "r") as image:
+    lines = image.readlines()
+    image_size = lines[1].split()
+
+    if lines[0].strip() != 'P1':
+        raise ValueError("This is not a pbm file")
+    
+    largura_orig = int(image_size[0])
+    altura_orig = int(image_size[1])
+    
+    clean_text = clean_image_data(lines[2:])
+    
+    image_blocks, largura_pad, altura_pad = get_blocks(clean_text, largura_orig, altura_orig)
+
+    output, dic = lz78_compression(image_blocks)
+
+    with open(sys.argv[1] + "_compressed", "w") as save_file:
+        save_file.write(f"{output}\n{largura_orig} {altura_orig} {largura_pad} {altura_pad}")
+    
+    with open(sys.argv[1] + ".bin", "wb") as save_file:
+        save_file.write(struct.pack('IIII', largura_orig, altura_orig, largura_pad, altura_pad))
+        for indice, simbolo in output:
+            simbolo_val = int(simbolo, 2) if simbolo != "" else 16
+            save_file.write(struct.pack('HB', indice, simbolo_val))
\ No newline at end of file
diff --git a/lz78/lz_78_rle.py b/lz78/lz_78_rle.py
new file mode 100644 (file)
index 0000000..261a376
--- /dev/null
@@ -0,0 +1,102 @@
+import sys
+import struct
+
+########################################################################################
+# Apenas comprimir
+#
+# python lz_78_rle.py <caminho_para_o_ficheiro_original>
+# 
+# 
+# Os ficheiros gerados ficarão na mesma pasta que o script
+########################################################################################
+
+def rle_transform(bitstream):
+    sequencia = []
+    pixel_atual = '0' # Começa sempre com o pixel 0 conforme enunciado 
+    contador = 0
+    
+    for bit in bitstream:
+        if bit == pixel_atual:
+            if contador == 255: # Limite de 8 bits 
+                sequencia.append(255)
+                sequencia.append(0) # Troço de 0 do outro pixel para continuar no mesmo [cite: 41]
+                contador = 1
+            else:
+                contador += 1
+        else:
+            sequencia.append(contador)
+            pixel_atual = bit
+            contador = 1
+    sequencia.append(contador)
+    return sequencia
+
+def lz78_compression(image_data):
+    dictionary = {0: []}
+    output = []
+    symbol = [] 
+
+    for i in image_data:
+        temp = symbol + [i]
+        dict_values = list(dictionary.values())
+        if temp in dict_values:
+            symbol = temp
+        else:
+            if not symbol:
+                output.append([0, i])
+                dictionary[len(dictionary)] = [i]
+            else:
+                idx = dict_values.index(symbol)
+                output.append([idx, i])
+                dictionary[len(dictionary)] = symbol + [i]
+            symbol = []
+    
+    if symbol:
+        idx = list(dictionary.values()).index(symbol)
+        output.append([idx, -1])
+    return output
+
+def clean_image_data(image_data):
+    
+    clean_text = ""
+
+    for line in image_data:
+        line = line.strip()
+        line = line.replace(" ", "")
+        if not line or line.startswith('#'):
+            continue
+        clean_text += line
+
+    return clean_text
+
+def chunkstring(string, length):
+    return [string[i : length+i] for i in range(0, len(string), length)]
+
+
+with open(sys.argv[1], "r") as image:
+    lines = image.readlines()
+    image_size = lines[1].split()
+
+    if lines[0].strip() != 'P1':
+        raise ValueError("This is not a pbm file")
+    
+    clean_text = clean_image_data(lines[2:]) #Limpa os espaços em branco para comprimir melhor
+
+    clean_text = rle_transform(clean_text)
+
+    output = lz78_compression(clean_text)
+
+    print(f"Output: {output}\n")
+
+    #Compressão em texto (human-readable)
+    with open(sys.argv[1] + "_compressed", "w") as save_file:
+        save_file.write(f"{output} \n{image_size[0]} {image_size[1]}")
+    
+    #Compressão em binário (compressão "a sério")
+    with open(sys.argv[1] + ".bin", "wb") as save_file:
+        largura = int(image_size[0])
+        altura = int(image_size[1])
+        save_file.write(struct.pack('II', largura, altura))
+
+        for indice, val in output:
+            simbolo_byte = val if val != -1 else 255
+            save_file.write(struct.pack('HB', indice, simbolo_byte))
diff --git a/lz78/lz_78_xor.py b/lz78/lz_78_xor.py
new file mode 100644 (file)
index 0000000..0d3ee90
--- /dev/null
@@ -0,0 +1,189 @@
+import sys
+import struct
+
+########################################################################################
+# python lz_78_xor.py <d/c> <caminho_para_o_ficheiro_original>
+# 
+# c: comprimir
+# d: descomprimir
+# 
+# Os ficheiros gerados ficarão na mesma pasta que o script
+########################################################################################
+
+def xor(bitstream, largura, altura):
+    matriz = []
+    for i in range(altura):
+        linha = [int(b) for b in bitstream[i * largura : (i + 1) * largura]]
+        matriz.append(linha)
+    
+    nova_imagem = ""
+    # A primeira linha mantém-se igual (não tem linha anterior)
+    nova_imagem += "".join(map(str, matriz[0]))
+    
+    for i in range(1, altura):
+        nova_linha = []
+        for j in range(largura):
+            res = matriz[i][j] ^ matriz[i-1][j]
+            nova_linha.append(str(res))
+        nova_imagem += "".join(nova_linha)
+        
+    return nova_imagem
+
+def descodificar_xor(bitstream_transformado, largura, altura):
+    matriz_temp = []
+    for i in range(altura):
+        linha = [int(b) for b in bitstream_transformado[i * largura : (i + 1) * largura]]
+        matriz_temp.append(linha)
+    
+    matriz_original = []
+    
+    matriz_original.append(matriz_temp[0])
+    
+    for i in range(1, altura):
+        linha_recuperada = []
+        for j in range(largura):
+            pixel_original = matriz_temp[i][j] ^ matriz_original[i-1][j]
+            linha_recuperada.append(pixel_original)
+        matriz_original.append(linha_recuperada)
+    
+    bitstream_final = ""
+    for linha in matriz_original:
+        bitstream_final += "".join(map(str, linha))
+        
+    return bitstream_final
+
+def lz78_compression(image):
+    dictionary = {
+        0: ""
+    }
+
+    output = []
+
+    symbol = ""
+
+    for i in image:
+        if (symbol + i) in dictionary.values():
+            symbol += i
+        else:
+            if symbol == "":
+                output.append([0, i])
+                dictionary[len(dictionary)] = i
+            else:
+                output.append([list(dictionary.keys())[list(dictionary.values()).index(symbol)], i])
+                dictionary[len(dictionary)] = symbol + i
+            symbol = ""
+
+    if symbol != "":
+        idx = list(dictionary.keys())[list(dictionary.values()).index(symbol)]
+        output.append([idx, ""])
+
+    return output, dictionary
+
+def lz78_decompression(compressed_image):
+    dictionary = {
+        0: ""
+    }
+
+    output = ""
+
+    for i in compressed_image:
+        output += dictionary.get(i[0]) + i[1]
+        dictionary[len(dictionary)] = dictionary.get(i[0]) + i[1]
+
+    return output
+
+def clean_image_data(image_data):
+    
+    clean_text = ""
+
+    for line in image_data:
+        line = line.strip()
+        line = line.replace(" ", "")
+        if not line or line.startswith('#'):
+            continue
+        clean_text += line
+
+    return clean_text
+
+def chunkstring(string, length):
+    return [string[i : length+i] for i in range(0, len(string), length)]
+
+#Comprimir
+if sys.argv[1] == "c":
+
+    with open(sys.argv[2], "r") as image:
+        lines = image.readlines()
+        image_size = lines[1].split()
+
+        if lines[0].strip() != 'P1':
+            raise ValueError("This is not a pbm file")
+        
+        clean_text = clean_image_data(lines[2:]) #Limpa os espaços em branco para comprimir melhor
+
+        clean_text = xor(clean_text, int(image_size[0]), int(image_size[1]))
+
+        output, dic = lz78_compression(clean_text)
+
+        print(f"Output: {output}\n")
+        print(f"Dictionary: {dic}")
+
+        #Compressão em texto (human-readable)
+        with open(sys.argv[2] + "_compressed", "w") as save_file:
+            save_file.write(f"{output} \n{image_size[0]} {image_size[1]}")
+        
+        #Compressão em binário (compressão "a sério")
+        with open(sys.argv[2] + ".bin", "wb") as save_file:
+            largura = int(image_size[0])
+            altura = int(image_size[1])
+            save_file.write(struct.pack('II', largura, altura))
+
+            for indice, simbolo in output:
+                simbolo_byte = ord(simbolo) if simbolo != "" else 0
+                save_file.write(struct.pack('HB', indice, simbolo_byte))
+#Descomprimir
+elif sys.argv[1] == "d":
+
+    #Descomprimir do ficheiro de texto
+    with open(sys.argv[2] + "_compressed", "r") as image:
+        lines = image.readlines()
+
+        compressed_image = eval(lines[0])
+        image_size = lines[1]
+        largura = int(lines[1].split(" ")[0])
+        altura = int(lines[1].split(" ")[1])
+        
+        output = lz78_decompression(compressed_image)
+
+        output = descodificar_xor(output, largura, altura)
+        
+        chuncked_output = chunkstring(output, largura)
+
+        with open(sys.argv[2] + "_decompressed", "w") as save_file:
+            save_file.write(f"P1\n{image_size}\n")
+            for line in chuncked_output:
+                save_file.write(f"{line}\n")
+    
+    #Descomprimir do binário
+    with open(sys.argv[2] + ".bin", "rb") as image:
+        header = image.read(8)
+        largura, altura = struct.unpack('II', header)
+        
+        compressed_image_bin = []
+        while True:
+            chunk = image.read(3)
+            if not chunk:
+                break
+            indice, simbolo_byte = struct.unpack('HB', chunk)
+            simbolo = chr(simbolo_byte) if simbolo_byte != 0 else ""
+            compressed_image_bin.append([indice, simbolo])
+
+        output = lz78_decompression(compressed_image_bin)
+
+        output = descodificar_xor(output, largura, altura)
+
+        chuncked_output = chunkstring(output, largura)
+
+        with open(sys.argv[2]+ "_bin_decompressed", "w") as save_file:
+            save_file.write(f"P1\n{largura} {altura}\n")
+            for line in chuncked_output:
+                save_file.write(f"{line}\n")
diff --git a/main.py b/main.py
new file mode 100644 (file)
index 0000000..df1449e
--- /dev/null
+++ b/main.py
@@ -0,0 +1,42 @@
+import utils\r
+\r
+if __name__ == '__main__':\r
+    _original = utils.get_file('pixel_character.pbm')\r
+    #_original = utils.get_file('tetris_example.pbm')\r
+    _pbm_content = utils.BitMapFile(_original)\r
+\r
+    while True:\r
+        print("\n1) print file info")\r
+        print("2) Preview arithmetic codification info ")\r
+        print("3) Encode with arithmetic style ")\r
+        print("4) Decode with arithmetic style ")\r
+        print("5) Apply XOR to image before compression \n")\r
+        _user_input = input("your option --> ")\r
+\r
+        if _user_input == "1":\r
+            print("\n*** PORTABLE BITMAP FILE INFO ***")\r
+            print(f"magic number:{_pbm_content.magic_number},\nimage width:{_pbm_content.image_width},image height:{_pbm_content.image_height}")\r
+            print(f"Zeros ---> {_pbm_content.zeros}")\r
+            print(f"Uns --->  {_pbm_content.ones}")\r
+            print("***** \t ******** \t *****\n")\r
+            input("<back to menu>")\r
+        if _user_input == "2":\r
+            utils.arithmetic_cod_preview(_pbm_content)\r
+        if _user_input == "3":\r
+            _result_option_1, _result_option_2 = utils.arithmetic_encode_image(_pbm_content)\r
+            with open('arithmetic_output_opt1.txt', 'w') as f:\r
+                f.write(_result_option_1)\r
+            with open('arithmetic_output_opt2.txt', 'w') as f:\r
+                f.write(_result_option_2)\r
+        if _user_input == "4":\r
+            _result = utils.arithmetic_decode_file('arithmetic_output_opt2.txt')\r
+            with open('arithmetic_decode_output.pbm', 'w') as f:\r
+                f.write(_result)\r
+        if _user_input == "5":\r
+            _new_pbm = _pbm_content\r
+            _xor_on_image = utils.xor(_new_pbm.image_array,int(_new_pbm.image_width),int(_new_pbm.image_height))\r
+            _new_pbm.image_arra = _xor_on_image\r
+            _, _result = utils.arithmetic_encode_image(_new_pbm)\r
+            print(_result)\r
+            with open('arithmetic_output_wxor.txt', 'w') as f:\r
+                f.write(_result)
\ No newline at end of file
diff --git a/pixel_character.pbm b/pixel_character.pbm
new file mode 100644 (file)
index 0000000..30de542
--- /dev/null
@@ -0,0 +1,50 @@
+P1
+48 48
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1 1 1 1 1 1 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
diff --git a/readme.txt b/readme.txt
new file mode 100644 (file)
index 0000000..ddff7a7
--- /dev/null
@@ -0,0 +1,24 @@
+Universidade de Évora\r
+Projeto da disciplina de Compressão de Dados - 2025/2026\r
+Compressão de Imagens Binárias\r
+\r
+\r
+LZ78 - André Moleirinho (61609)\r
+- lz_78.py\r
+- lz_78_xor.py\r
+- lz_78_2x2.py\r
+- lz_78_rle.py\r
+\r
+Huffman - Fábio Macarrão (57098)\r
+- huffman.py\r
+- utils.py\r
+\r
+Aritmética - Vitor Costa (70323)\r
+- arithmetic.py\r
+- utils.py\r
+\r
+\r
+Executar os scripts em ficheiros pbm:\r
+       > python <ScriptName> <c/d> <PathToFile>\r
+\r
+\r
diff --git a/relatório.pdf b/relatório.pdf
new file mode 100644 (file)
index 0000000..a3cf56e
Binary files /dev/null and "b/relat\303\263rio.pdf" differ
diff --git a/taxa_compressão.py b/taxa_compressão.py
new file mode 100644 (file)
index 0000000..b9bdbd8
--- /dev/null
@@ -0,0 +1,40 @@
+import os
+import sys
+
+def calcular_taxa_compressao(caminho_original, caminho_comprimido, total_pixeis):
+
+    tamanho_orig = os.path.getsize(caminho_original)
+    tamanho_comp = os.path.getsize(caminho_comprimido)
+    
+    poupanca = (1 - (tamanho_comp / tamanho_orig)) * 100
+    
+    bpp = (tamanho_comp * 8) / total_pixeis
+    
+    return poupanca, bpp, tamanho_orig, tamanho_comp
+
+def clean_image_data(image_data):
+    
+    clean_text = ""
+
+    for line in image_data:
+        line = line.strip()
+        line = line.replace(" ", "")
+        if not line or line.startswith('#'):
+            continue
+        clean_text += line
+
+    return clean_text
+
+with open(sys.argv[1], "r") as ficheiro:
+    lines = ficheiro.readlines()
+    image_data = clean_image_data(lines[2:])
+
+    largura, altura = lines[1].split(" ")
+
+    total_pixeis = int(largura) * int(altura)
+    p, b, t_o, t_c = calcular_taxa_compressao(sys.argv[1], sys.argv[1] + "_compressed", len(image_data))
+
+print(f"Tamanho Original: {t_o} bytes")
+print(f"Tamanho Comprimido: {t_c} bytes")
+print(f"Redução: {p:.2f}%")
+print(f"Rácio: {b:.4f} bits/pixel")
\ No newline at end of file
diff --git a/tetris_example.pbm b/tetris_example.pbm
new file mode 100644 (file)
index 0000000..07ce596
--- /dev/null
@@ -0,0 +1,7 @@
+P1\r
+4 5\r
+0 0 0 0\r
+0 0 1 0\r
+0 1 1 0\r
+0 1 0 0\r
+0 0 0 0\r
diff --git a/utils.py b/utils.py
new file mode 100644 (file)
index 0000000..3b0ab7a
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,392 @@
+import os\r
+import math\r
+from collections import Counter\r
+import heapq\r
+import numpy as np\r
+import struct\r
+\r
+\r
+class BitMapFile:\r
+    def __init__(self, text):\r
+        self._text = text.split()\r
+        self.magic_number = self._text[0]\r
+        self.image_width = self._text[1]\r
+        self.image_height = self._text[2]\r
+        self.image_array = self._text[3:]\r
+        self.zeros = 0\r
+        self.ones = 0\r
+        self.size = int(self.image_width) * int(self.image_height)\r
+\r
+        for _ in range(len(self.image_array)):\r
+            if self.image_array[_] == "0":\r
+                self.zeros += 1\r
+            elif self.image_array[_] == "1":\r
+                self.ones += 1\r
+            else:\r
+                print("ignore")\r
+\r
+\r
+"""\r
+To read files\r
+"""\r
+def get_file(file_path):\r
+    with open(file_path, 'r', encoding='utf-8') as file:\r
+        _file_content = file.read()\r
+    return _file_content\r
+\r
+"""\r
+To evaluate comprimento médio do código - l(c)\r
+    l(all_pixels) = ceil(log2P(all_pixels))+1\r
+"""\r
+def arithmetic_cod_preview(_pbm):\r
+    print("\n***  Arithmetic Preview  ***")\r
+    pixels = [int(pixel) for pixel in _pbm.image_array]\r
+    _P0 = _pbm.zeros/_pbm.size\r
+    _P1 = _pbm.ones/_pbm.size\r
+    _PT01_int = 1\r
+\r
+    for pixel in pixels:\r
+        if pixel == 0:\r
+            _PT01_int *= _P0\r
+        elif pixel == 1:\r
+            _PT01_int *= _P1\r
+\r
+    _arithmetic_encode_lenght = np.ceil(-1*np.log2(_PT01_int)) + 1\r
+    print(f"L(image) --> {_arithmetic_encode_lenght} bits")\r
+    _entropy = calcular_entropia(_pbm.image_array)\r
+    print(f"entropy --> {_entropy} bits")\r
+    _datastream = clean_image_data(_pbm.image_array)\r
+    _entropia_conditional = calcular_entropia_condicional(_datastream)\r
+    print(f"entropy condicional --> {_entropia_conditional} bits")\r
+    return int(_arithmetic_encode_lenght)\r
+\r
+"""\r
+Arithmetic Encoding approach\r
+\r
+it uses static probabilities provided on output file\r
+\r
+c(x)=|interval_low_val +(interval_high_val).F(x)_low\r
+    =|interval_low_val +(interval_high_val).F(x)_high\r
+_to_encode_val = avg(C(all)_low,C(all)_high)\r
+_output = bin(_to_encode_val))\r
+"""\r
+def arithmetic_encode_image(_pbm):\r
+    pixels = [int(pixel) for pixel in _pbm.image_array]\r
+    _P0 =  np.float64(_pbm.zeros/_pbm.size)\r
+    _P1 =  np.float64(_pbm.ones/_pbm.size)\r
+    _interval = [0,1]\r
+    # probabilidades acomuladas orderdanas por ordem crescente\r
+    _Fx = {1: (0,_P1),\r
+           0: (_P1,1)}\r
+    _output_opt_1 = f"{_pbm.zeros} {_pbm.ones} {_pbm.image_width} {_pbm.image_height} "\r
+\r
+    for pixel in pixels:\r
+        if pixel == 0:\r
+            _low_val = np.float64(_interval[0] + (_interval[1]-_interval[0]) * _Fx.get(0)[0])\r
+            _high_val = np.float64(_interval[0] + (_interval[1]-_interval[0])*_Fx.get(0)[1])\r
+        if pixel == 1:\r
+            _low_val = np.float64(_interval[0] + (_interval[1] - _interval[0]) * _Fx.get(1)[0])\r
+            _high_val = np.float64(_interval[0] + (_interval[1] - _interval[0]) * _Fx.get(1)[1])\r
+        _interval[0] = _low_val\r
+        _interval[1] = _high_val\r
+\r
+    _to_encode_value = np.float64((_interval[0]+_interval[1])/2)\r
+    print(f"to encode --> {_to_encode_value}")\r
+    _output_opt_2 = _output_opt_1 + f"{_to_encode_value}"\r
+\r
+    _size = arithmetic_cod_preview(_pbm)\r
+    _res = _to_encode_value\r
+    for _ in range(_size):\r
+        _res *=2\r
+        if _res < 1:\r
+            _output_opt_1 += "0"\r
+        else:\r
+            _output_opt_1 += "1"\r
+            _res = _res-1\r
+\r
+\r
+    return _output_opt_1,_output_opt_2\r
+\r
+"""\r
+To decode encoded pbm files with arithmetic_encode_image()\r
+    amount_of_zeros amount_of_ones image_width image_height encoded_image\r
+"""\r
+def arithmetic_decode_file(_txt):\r
+    _encoded_pbm_file = get_file(_txt).split()\r
+\r
+    amount_of_zeros = int(_encoded_pbm_file[0])\r
+    amount_of_ones = int(_encoded_pbm_file[1])\r
+    image_width = int(_encoded_pbm_file[2])\r
+    image_height = int(_encoded_pbm_file[3])\r
+    encoded_image = np.float64(_encoded_pbm_file[4])\r
+\r
+    _output = f"P1\n{image_width} {image_height}\n"\r
+    _size = int(image_width) * int(image_height)\r
+    _P0 = np.float64(amount_of_zeros/_size)\r
+    _P1 = np.float64(amount_of_ones/_size)\r
+    _Fx = {1:(0,_P1),\r
+           0:(_P1,1)}\r
+\r
+    _constructed_image = ""\r
+    for h in range(image_height):\r
+        for w in range(image_width):\r
+            if  _P1 < encoded_image < 1:\r
+                _constructed_image += "0 "\r
+                encoded_image = (encoded_image-_P1)/_P0\r
+            elif 0 < encoded_image < _P1:\r
+                _constructed_image += "1 "\r
+                encoded_image = (encoded_image - 0) / _P1\r
+\r
+            if w == image_width-1:\r
+                _constructed_image += "\n"\r
+\r
+    _output += _constructed_image\r
+    return _output\r
+\r
+\r
+def calcular_entropia(bitstream):\r
+    n = len(bitstream)\r
+    if n == 0: return 0\r
+\r
+    p0 = bitstream.count('0') / n\r
+    p1 = bitstream.count('1') / n\r
+\r
+    entropia = 0\r
+    for p in [p0, p1]:\r
+        if p > 0:\r
+            entropia -= p * math.log2(p)\r
+    return entropia\r
+\r
+\r
+def calcular_entropia_condicional(bitstream):\r
+    transicoes = {'00': 0, '01': 0, '10': 0, '11': 0}\r
+    contagem_base = {'0': 0, '1': 0}\r
+\r
+    for i in range(len(bitstream) - 1):\r
+        par = bitstream[i:i + 2]\r
+        transicoes[par] += 1\r
+        contagem_base[bitstream[i]] += 1\r
+\r
+    h_condicional = 0\r
+    for base in ['0', '1']:\r
+        p_base = contagem_base[base] / (len(bitstream) - 1)\r
+        if p_base > 0:\r
+            h_local = 0\r
+            for prox in ['0', '1']:\r
+                p_transicao = transicoes[base + prox] / contagem_base[base]\r
+                if p_transicao > 0:\r
+                    h_local -= p_transicao * math.log2(p_transicao)\r
+            h_condicional += p_base * h_local\r
+\r
+    return h_condicional\r
+\r
+\r
+def clean_image_data(image_data):\r
+    clean_text = ""\r
+\r
+    for line in image_data:\r
+        line = line.strip()\r
+        line = line.replace(" ", "")\r
+        if not line or line.startswith('#'):\r
+            continue\r
+        clean_text += line\r
+\r
+    return clean_text\r
+\r
+\r
+def calcular_taxa_compressao(caminho_original, caminho_comprimido, total_pixeis):\r
+    tamanho_orig = os.path.getsize(caminho_original)\r
+    tamanho_comp = os.path.getsize(caminho_comprimido)\r
+\r
+    poupanca = (1 - (tamanho_comp / tamanho_orig)) * 100\r
+\r
+    bpp = (tamanho_comp * 8) / total_pixeis\r
+\r
+    return poupanca, bpp, tamanho_orig, tamanho_comp\r
+\r
+\r
+def xor(bitstream, largura, altura):\r
+    matriz = []\r
+    for i in range(altura):\r
+        linha = [int(b) for b in bitstream[i * largura: (i + 1) * largura]]\r
+        matriz.append(linha)\r
+\r
+    nova_imagem = ""\r
+    # A primeira linha mantém-se igual (não tem linha anterior)\r
+    nova_imagem += "".join(map(str, matriz[0]))\r
+\r
+    for i in range(1, altura):\r
+        nova_linha = []\r
+        for j in range(largura):\r
+            res = matriz[i][j] ^ matriz[i - 1][j]\r
+            nova_linha.append(str(res))\r
+        nova_imagem += "".join(nova_linha)\r
+\r
+    return nova_imagem\r
+\r
+\r
+def descodificar_xor(bitstream_transformado, largura, altura):\r
+    matriz_temp = []\r
+    for i in range(altura):\r
+        linha = [int(b) for b in bitstream_transformado[i * largura: (i + 1) * largura]]\r
+        matriz_temp.append(linha)\r
+\r
+    matriz_original = []\r
+\r
+    matriz_original.append(matriz_temp[0])\r
+\r
+    for i in range(1, altura):\r
+        linha_recuperada = []\r
+        for j in range(largura):\r
+            pixel_original = matriz_temp[i][j] ^ matriz_original[i - 1][j]\r
+            linha_recuperada.append(pixel_original)\r
+        matriz_original.append(linha_recuperada)\r
+\r
+    bitstream_final = ""\r
+    for linha in matriz_original:\r
+        bitstream_final += "".join(map(str, linha))\r
+\r
+    return bitstream_final\r
+\r
+\r
+# -------------------------------------------------------------------------\r
+# HUFFMAN IMPLEMENTATION\r
+# -------------------------------------------------------------------------\r
+\r
+class HuffmanNode:\r
+    def __init__(self, char, freq):\r
+        self.char = char  # 0 ou 1 que está no pixel\r
+        self.freq = freq  # Quantas vezes aparece\r
+        self.left = None  # Filho à esquerda\r
+        self.right = None  # Filho à direita\r
+\r
+    # Nó com menor frequencia\r
+    def __lt__(self, other):\r
+        return self.freq < other.freq\r
+\r
+\r
+# Constroi a árvore\r
+def build_huffman_tree(pixels):\r
+    frequency = Counter(pixels)\r
+    heap = [HuffmanNode(char, freq) for char, freq in frequency.items()]\r
+    heapq.heapify(heap)\r
+\r
+    # Cria a árvore de baixo para cima / frequencia menor para maior\r
+    while len(heap) > 1:\r
+        node1 = heapq.heappop(heap)\r
+        node2 = heapq.heappop(heap)\r
+        merged = HuffmanNode(None, node1.freq + node2.freq)\r
+        merged.left = node1\r
+        merged.right = node2\r
+        heapq.heappush(heap, merged)\r
+\r
+    return heap[0] if heap else None\r
+\r
+\r
+# Cria os códigos (0 para a esquerda, 1 para a direira)\r
+def make_codes(node, current_code="", codes=None):\r
+    if codes is None:\r
+        codes = {}\r
+    if node is None:\r
+        return codes\r
+    if node.char is not None:\r
+        codes[node.char] = current_code\r
+        return codes\r
+\r
+    make_codes(node.left, current_code + "0", codes)\r
+    make_codes(node.right, current_code + "1", codes)\r
+    return codes\r
+\r
+\r
+# Faz encoding do ficheiro\r
+def huffman_encode_image(_pbm):\r
+    print("\n*** Huffman Encoding ***")\r
+    pixels = _pbm.image_array\r
+\r
+    # Erro caso não encontre a imagem\r
+    if not pixels:\r
+        print("Empty image.")\r
+        return None\r
+\r
+    # Constroi a árvore e os códigos\r
+    root = build_huffman_tree(pixels)\r
+    codes = make_codes(root)\r
+\r
+    print(f"Codes: {codes}")\r
+\r
+    # Faz encode dos pixeis\r
+    encoded_str = "".join([codes[p] for p in pixels])\r
+\r
+    # Garante que o total é multiplo de 8\r
+    extra_padding = 8 - len(encoded_str) % 8\r
+    encoded_str = encoded_str + "0" * extra_padding\r
+\r
+    # Reduz o tamanho convertendo de byte para bit\r
+    b = bytearray()\r
+    for i in range(0, len(encoded_str), 8):\r
+        byte = encoded_str[i:i + 8]\r
+        b.append(int(byte, 2))\r
+\r
+    # Cabeçalho\r
+    width = int(_pbm.image_width)\r
+    height = int(_pbm.image_height)\r
+    freq0 = _pbm.zeros\r
+    freq1 = _pbm.ones\r
+    header = struct.pack('>IIIIB', width, height, freq0, freq1, extra_padding)\r
+\r
+    return header + b\r
+\r
+\r
+# Faz decode do ficheiro\r
+def huffman_decode_file(filename):\r
+    print(f"\n*** Huffman Decoding from {filename} ***")\r
+\r
+    # Lê o ficheiro\r
+    with open(filename, 'rb') as f:\r
+        file_content = f.read()\r
+\r
+    # Separa o cabeçalho\r
+    header_size = struct.calcsize('>IIIIB')\r
+    width, height, freq0, freq1, extra_padding = struct.unpack('>IIIIB', file_content[:header_size])\r
+    encoded_data = file_content[header_size:]\r
+\r
+    print(f"Header Info -> W:{width}, H:{height}, Zeros:{freq0}, Ones:{freq1}")\r
+\r
+    # Constroi a árvore\r
+    fake_pixels = ['0'] * freq0 + ['1'] * freq1\r
+    root = build_huffman_tree(fake_pixels)\r
+    codes = make_codes(root)\r
+\r
+    # Faz decode dos pixeis\r
+    reverse_codes = {v: k for k, v in codes.items()}\r
+\r
+    # Converte os bytes em string\r
+    bit_string = ""\r
+    for byte in encoded_data:\r
+        bit_string += f"{byte:08b}"\r
+\r
+    # Desencripta\r
+    decoded_pixels = []\r
+    current_code = ""\r
+    for bit in bit_string:\r
+        current_code += bit\r
+        if current_code in reverse_codes:\r
+            decoded_pixels.append(reverse_codes[current_code])\r
+            current_code = ""\r
+\r
+    # Limita o tamanho width * height\r
+    expected_pixels = width * height\r
+    decoded_pixels = decoded_pixels[:expected_pixels]\r
+\r
+    pbm_content = f"P1\n{width} {height}\n"\r
+\r
+    # Formata as linhas para não ficar sequencial\r
+    row_str = ""\r
+    for i, p in enumerate(decoded_pixels):\r
+        row_str += p + " "\r
+        if (i + 1) % width == 0:\r
+            pbm_content += row_str.strip() + "\n"\r
+            row_str = ""\r
+\r
+    print("Decoding finished.")\r
+    return pbm_content\r