Saltar a contenido

LZ77

El algoritmo LZ77, tambien conocido como LZ1, es un método de compresión de datos sin pérdida desarrollado por Abraham Lempel y Jacob Ziv en 1977. Es uno de los algoritmos de compresión más antiguos y ha servido como base para muchos otros métodos modernos como LZSS, LZ78 y LZW, así como su aplicación en compresores como gzip y en formatos de archivo como ZIP. LZ77 se basa en la idea de identificar secuencias repetidas dentro de un flujo de datos y reemplazarlas por referencias más pequeñas, lo que reduce el tamaño total del contenido comprimido.

Definición

LZ77 trabaja utilizando una "ventana deslizante" para buscar coincidencias entre el texto actual que está procesando y los datos que ya ha procesado previamente. El flujo de entrada se divide en dos partes: la ventana, que contiene una cantidad limitada de datos ya procesados, y el búfer de anticipación, que contiene el próximo texto a procesar. La compresión ocurre al identificar secuencias en el búfer de anticipación que coinciden con las secuencias encontradas en la ventana. Luego, el algoritmo codifica las repeticiones mediante tuplas de la forma (desplazamiento, longitud, carácter siguiente), donde:

  • Desplazamiento: indica la distancia desde la posición actual a la coincidencia más cercana.
  • Longitud: representa la cantidad de caracteres coincidentes.
  • Carácter siguiente: es el carácter que sigue a la secuencia coincidente, si existe.

El algoritmo básico para comprimir se ejecuta en un ciclo que sigue los siguientes pasos:

  1. Establece la posición de codificación al inicio del flujo de entrada.
  2. El algoritmo busca en la ventana (parte procesada) una coincidencia con la cadena en el búfer de anticipación (parte por procesar) que sea lo más larga posible.
  3. Una vez encontrada la coincidencia más larga se genera la tupla (desplazamiento, longitud, carácter siguiente).
  4. Si el búfer de anticipación no está vacío, vuelve al paso 2 y se avanza una posición.

LZ77

Este enfoque permite reemplazar secuencias repetitivas con referencias más compactas, logrando así la compresión de datos con patrones redundantes de manera simple y rápida. Otras variaciones del algoritmo LZ77 incluyen cambiar el tamaño de los búferes de búsqueda y anticipación. Para hacer que el búfer de búsqueda sea grande, se requiere el desarrollo de estrategias de búsqueda más efectivas. Dichas estrategias pueden implementarse de manera más eficaz si el contenido del búfer de búsqueda se almacena de manera que facilite búsquedas rápidas.

Ejemplo

Compresión:

Empezaremos con un ejemplo considerando la entrada "ABABCABAB" :

LZ77_EJ

Descompresión:

LZ77 se clasifica como un algoritmo de compresión de datos sin pérdida, lo que significa que deberíamos ser capaces de recuperar completamente la cadena original.

LZ77_DS

Así vemos cómo LZ77 utiliza su forma codificada para reproducir la entrada original.

Implementación del Algoritmo

A continuación, se presenta una implementación de LZ77 en Python:

Ver implementación LZ77 en Python
import math
from bitarray import bitarray
class LZ77Compressor:
    """
    A simplified implementation of the LZ77 Compression Algorithm
    """
    MAX_WINDOW_SIZE = 400

    def __init__(self, window_size=20):
        self.window_size = min(window_size, self.MAX_WINDOW_SIZE) 
        self.lookahead_buffer_size = 15 # length of match is at most 4 bits

    def compress(self, input_file_path, output_file_path=None, verbose=False):
        """
        Given the path of an input file, its content is compressed by applying a simple 
        LZ77 compression algorithm. 

        The compressed format is:
        0 bit followed by 8 bits (1 byte character) when there are no previous matches
            within window
        1 bit followed by 12 bits pointer (distance to the start of the match from the 
            current position) and 4 bits (length of the match)

        If a path to the output file is provided, the compressed data is written into 
        a binary file. Otherwise, it is returned as a bitarray

        if verbose is enabled, the compression description is printed to standard output
        """
        data = None
        i = 0
        output_buffer = bitarray(endian='big')

        # read the input file 
        try:
            with open(input_file_path, 'rb') as input_file:
                data = input_file.read()
        except IOError:
            print('Could not open input file ...')
            raise

        while i < len(data):
            #print(i)

            match = self.findLongestMatch(data, i)

            if match: 
                # Add 1 bit flag, followed by 12 bit for distance, and 4 bit for the length
                # of the match 
                (bestMatchDistance, bestMatchLength) = match

                output_buffer.append(True)
                output_buffer.frombytes(bytes([bestMatchDistance >> 4]))
                output_buffer.frombytes(bytes([((bestMatchDistance & 0xf) << 4) | bestMatchLength]))

                if verbose:
                                        print("<1, %i, %i>" % (bestMatchDistance, bestMatchLength), end='')

                i += bestMatchLength

            else:
                # No useful match was found. Add 0 bit flag, followed by 8 bit for the character
                output_buffer.append(False)
                output_buffer.frombytes(bytes([data[i]]))

                if verbose:
                    print("<0, %s>" % data[i], end='')

                i += 1

        # fill the buffer with zeros if the number of bits is not a multiple of 8       
        output_buffer.fill()

        # write the compressed data into a binary file if a path is provided
        if output_file_path:
            try:
                with open(output_file_path, 'wb') as output_file:
                    output_file.write(output_buffer.tobytes())
                    print("File was compressed successfully and saved to output path ...")
                    return None
            except IOError:
                print('Could not write to output file path. Please check if the path is correct ...')
                raise

        # an output file path was not provided, return the compressed data
        return output_buffer

    def decompress(self, input_file_path, output_file_path=None):
        """
        Given a string of the compressed file path, the data is decompressed back to its 
        original form, and written into the output file path if provided. If no output 
        file path is provided, the decompressed data is returned as a string
        """
        data = bitarray(endian='big')
        output_buffer = []

        # read the input file
        try:
            with open(input_file_path, 'rb') as input_file:
                data.fromfile(input_file)
        except IOError:
            print('Could not open input file ...')
            raise

        while len(data) >= 9:

            flag = data.pop(0)

            if not flag:
                byte = data[0:8].tobytes()

                output_buffer.append(byte)
                del data[0:8]
            else:
                byte1 = ord(data[0:8].tobytes())
                byte2 = ord(data[8:16].tobytes())

                del data[0:16]
                distance = (byte1 << 4) | (byte2 >> 4)
                length = (byte2 & 0xf)

                for i in range(length):
                    output_buffer.append(output_buffer[-distance])
        out_data =  b''.join(output_buffer)

        if output_file_path:
            try:
                with open(output_file_path, 'wb') as output_file:
                    output_file.write(out_data)
                    print('File was decompressed successfully and saved to output path ...')
                    return None 
            except IOError:
                print('Could not write to output file path. Please check if the path is correct ...')
                raise 
        return out_data

    def findLongestMatch(self, data, current_position):
        """ 
        Finds the longest match to a substring starting at the current_position 
        in the lookahead buffer from the history window
        """
        end_of_buffer = min(current_position + self.lookahead_buffer_size, len(data) + 1)

        best_match_distance = -1
        best_match_length = -1

        # Optimization: Only consider substrings of length 2 and greater, and just 
        # output any substring of length 1 (8 bits uncompressed is better than 13 bits
        # for the flag, distance, and length)
        for j in range(current_position + 2, end_of_buffer):

            start_index = max(0, current_position - self.window_size)
            substring = data[current_position:j]

            for i in range(start_index, current_position):

                repetitions = len(substring) // (current_position - i)

                last = len(substring) % (current_position - i)

                matched_string = data[i:current_position] * repetitions + data[i:i+last]

                if matched_string == substring and len(substring) > best_match_length:
                    best_match_distance = current_position - i 
                    best_match_length = len(substring)

        if best_match_distance > 0 and best_match_length > 0:
            return (best_match_distance, best_match_length)
        return None
  • Enlace al repositorio en Github con el código completo.

Usos Comunes

LZ77 se emplea en numerosos escenarios y aplicaciones debido a su enfoque eficiente para reducir redundancias en flujos de datos:

  1. Compresión de Archivos: LZ77 es la base de muchos formatos de compresión populares, como gzip y ZIP, que utilizan versiones adaptadas del algoritmo para mejorar la relación de compresión en archivos.
  2. Compresión de Imágenes y Videos: Aunque LZ77 no suele usarse directamente en imágenes y videos, muchas técnicas modernas derivadas, como el algoritmo DEFLATE (que combina LZ77 con codificación Huffman).
  3. Compresión de Texto: Debido a que LZ77 funciona bien con patrones repetitivos, se ha utilizado para comprimir texto con redundancias, como en documentos o archivos de registro.

Referencias

  • Ziv, J., & Lempel, A. (1977). A universal algorithm for sequential data compression. IEEE Transactions On Information Theory, 23(3), 337-343. https://doi.org/10.1109/tit.1977.1055714
  • Sayood, K. (2005). Introduction to Data Compression. Elsevier.
  • EcuRed. (s. f.). LZ77 - ECURed. https://www.ecured.cu/Lz77
  • Leonardo Araya. (2022, 25 octubre). Algoritmo de compresión LZ77 [Vídeo]. YouTube. https://www.youtube.com/watch?v=woWY9LUnqtg
  • Computerphile. (2013, 1 noviembre). Elegant Compression in Text (The LZ 77 Method) - Computerphile [Vídeo]. YouTube. https://www.youtube.com/watch?v=goOa3DGezUA
  • [MS-WUSP]: LZ77 Compression Algorithm. (2023, 11 abril). Microsoft Learn. https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wusp/fb98aa28-5cd7-407f-8869-a6cef1ff1ccb
  • Budhrani, D. (2020, 10 septiembre). How LZ77 Data Compression works. HackerNoon. https://hackernoon.com/how-lz77-data-compression-works-yk113te0