Golang – Modificar archivos en modo binario

Golang – Modificar archivos en modo binario

Estos días salí a tomar algunas fotografías con mi cámara Sony a6000, que requiere que ingrese la fecha y la hora manualmente, ya que no se sincroniza con el teléfono. Cometí un error al ingresar 2024 en lugar de 2025, lo que provocó que todas las fotos fueran tomadas con el año incorrecto. Esto hizo que toda la información EXIF quedara con el año anterior.

Busqué algunas herramientas y descubrí que algunos plugins de Lightroom permiten editar de manera masiva la fecha de varias fotos. Esto resultó muy útil, ya que tomé más de 300 fotos y hacerlo manualmente habría llevado demasiado tiempo.

Pero como programador, decidí crear algo sencillo y aprovechar para practicar mis habilidades en Go editando directamente los bytes de los archivos RAW.

No quise complicarme tratando de entender cómo están estructurados los datos EXIF, así que opté por buscar las fechas en texto plano con un programa que me permitiera ver los archivos a nivel hexadecimal. En mi caso, usé GHex en Linux.

GHex

Como se puede ver en la captura, la posición donde aparece el número “2”, que corresponde al inicio de la fecha “2025:02:03 15:05:49” (ya editada), se encuentra en el offset 0x15A. Este número hexadecimal, al convertirse a decimal, resulta ser 346, que es la posición en el archivo donde debo comenzar a editar. Tengo que editar un total de 19 posiciones, lo que equivale a algo como este vector: [2, 0, 2, 5, :, 0, 2, :, 0, 3, , 1, 5, :, 0, 5, :4, 9].

En Go, es bastante sencillo abrir un archivo en modo stream y escribir en posiciones específicas. Esto se hace de la siguiente manera:

file, err := os.OpenFile(imagePath, os.O_RDWR, 0666)

os.O_RDWR este parámetro indica que el archivo se abrirá en modo escritura. El siguiente parámetro corresponde a los permisos del archivo en formato *unix.

Luego, convierto la fecha, que está en formato string, a un vector de bytes. En Go, un string es realmente un vector de bytes, por lo que la conversión es bastante sencilla, casi como un cast.

date := "2025:02:03 15:05:49"
arrayBytes := []byte(date)

El array arrayBytes es lo que voy a escribir directamente en el archivo para reemplazar la fecha y hora. No quise complicarme editando la hora o tratando de conservar la hora original, ya que me dio flojera escribir más código, pero no es difícil cambiar la lógica para si fuera necesario.

Para poder editar exactamente donde quiero, una vez que hice el análisis del archivo con GHex y ubiqué las posiciones donde había fechas (que son 4 lugares), lo único que debo hacer es posicionar el puntero en la posición correcta dentro del archivo.

En los archivos de la Sony a6000 con firmware 3.21, las fechas comienzan en las posiciones 346, 1038, 2602 y 38592.

offset := 346
_, err := file.Seek(offset, io.SeekStart)

Para posicionarme en el offset indicado, uso el método Seek de la estructura File, donde file es el archivo que se “abrió”. Este método recibe la posición y desde dónde comenzar, en este caso io.SeekStart, lo que indica que debe comenzar desde el inicio del archivo hasta el offset que se le indique.

Para escribir los datos, utilizo el método Write de File. Este método escribe desde la posición previamente establecida con Seek hasta el tamaño del vector de bytes. En el caso de la fecha que convertí, son 19 posiciones.

_, err = file. Write(arrayBytes)

Y listo así se reemplaza la fecha.

El código original en el repositorio no tiene las mismas variables y contiene algunas impresiones a consola para depuración. Además, recibe algunos parametros entre ellos la ruta del archivo, tambien tiene algunas validaciones simples. Esto lo hice para poder reutilizarlo en el futuro, ya que una vez compilado el archivo, debería ejecutarse de la siguiente manera:

./changedate.exe -w y -d 2025:02:03/15:05:49 -f /path/file
  • -w: y/n básicamente escribe o no la fecha, en caso de no escribir la fecha solo imprime lo que encontró en el archivo, me sirve en modo depuración nada mas
  • -d: recibe la fecha en este formato 2025:02:03/15:05:49 en lugar de un espacio agregue / ya que en consola los espacios se comportan como otro parámetro.
  • -f: la ruta al archivo.

Escribí el script de esta manera para que funcione con un solo archivo por ejecución, ya que la lógica de procesar todos los archivos la iba a desarrollar con un simple script bash de Linux. Este es el script:

for file in *.ARW; do
    echo "Procesando $file..."

    ./changedate.exe -w y -d 2025:02:03/15:05:49 -f $file
    
    # Verifica si hubo errores
    if [ $? -ne 0 ]; then
        echo "Error al modificar $file"
    fi
done

Este script obtiene todos los archivos con extensión .ARW dentro de la misma carpeta donde se ejecuta. Luego, para cada archivo, ejecuta el binario compilado de Go, pasándole los parámetros necesarios para cambiar la fecha y el nombre del archivo, lo que es más práctico para mí que tener que escribir código en Go para obtener todos los archivos de una carpeta con esa extensión. La idea es centrarme en modificar un solo archivo a la vez.

Codigo en Github.