Artículo original escrito por: Shaun Hamilton
Artículo original: Learn Rust Programming Course – Interactive Rust Language Tutorial on Replit
Traducido y adaptado por: Rafael D. Hernandez

Durante seis años consecutivos, Rust ha sido votado como el lenguaje de programación más querido por StackOverflow.

Así que si estás listo para aprender este popular lenguaje de programación, este curso te presentará Rust para que puedas empezar a usarlo en tus proyectos.

Trabajará completamente dentro de tu navegador utilizando el entorno de programación interactivo Replit. freeCodeCamp se ha asociado con Replit, que ha hecho posible este curso.

También hay una versión en video de este curso en ingles en el canal de YouTube de freeCodeCamp.

Para aprovechar al máximo este curso, debes tener conocimientos intermedios de al menos otro lenguaje de programación. Si eres nuevo en la programación, deberías probar el currículo interactivo de freeCodeCamp y luego volver a este curso.

Para ayudarte a aprender Rust, crearemos dos proyectos:

  1. Una calculadora para la línea de comandos
  2. Una herramienta de línea de comandos que toma dos imágenes y combina sus píxeles

Tabla de contenidos

Estas son las secciones y temas que cubriremos en este curso. Puedes hacer clic en la tabla de contenido a continuación para saltar a partes particulares, o simplemente puedes ir de principio a fin.

Descripción de Rust

Rust es un lenguaje de programación a nivel de sistemas.

"[Rust] se ocupa de detalles de bajo nivel de administración de memoria, representación de datos y concurrencia.""... el lenguaje está diseñado para guiarlo naturalmente hacia un código confiable que sea eficiente en términos de velocidad y uso de memoria."(Fuente: Documentacion de Rust)

La herramienta principal dentro del ecosistema de Rust es:

  • rustc - El compilador que toma tu código Rust y lo compila en binario (código legible por máquina)
  • rustup - La utilidad de línea de comandos para instalar y actualizar Rust
  • cargo - El sistema de construcción de Rust y el gestor de paquetes

Cómo usar Rust en Replit

Para este curso, usarás este repositorio de GitHub como un boilerplate.

Para comenzar, has clic en el siguiente enlace para crear una nueva RESPUESTA desde el modelo estándar:

Rust-in-Replit

Fundamentos de Rust

Variables en Rust

Puedes declarar variables utilizando las palabras clave let, const o static:

let mi_variable = 0;
const Mi_CONSTANT: u8 = 0;
static Mi_STATIC: u8 = 0;

De forma predeterminada, todas las variables son inmutables. Puedes hacer que una variable sea mutable utilizando la palabra clave mut:

let mut mi_variable_mutable = 0;

La convención de Rust se basa en las siguientes convenciones de carcasa:

OBJECTCASING
Variablessnake_case
Functionssnake_case
Filessnake_case
ConstantsSCREAMING_SNAKE_CASE
StaticsSCREAMING_SNAKE_CASE
TypesPascalCase
TraitsPascalCase
EnumsPascalCase

Dado que Rust se escribe estáticamente, necesitarás escribir variables explícitamente, a menos que la variable se declare con let y el tipo se pueda inferir.

Funciones en Rust

Declaras funciones usando la palabra clave fn:

fn main() {
  // Este es un comentario de código
}

Las funciones devuelven usando la palabra clave return, y necesitas especificar explícitamente el tipo de retorno de una función, a menos que el tipo de retorno sea una tupla vacía ():

fn main() -> () { // Tipo de retorno innecesario
  mi_func();
}

fn mi_func() -> u8 {
  return 0;
}

Las funciones también devuelven una expresión que carece del punto y coma:

fn mi_func() -> u8 {
  0
}

Los parámetros de la función se escriben usando la sintaxis ::

fn main() {
  let _variable_no_usada = mi_func(10);
}

fn mi_func(x: u8) -> i32 {
  x as i32
}

El guion bajo antes del nombre de una variable es una convención para indicar que la variable no está utilizada. La palabra clave as afirma el tipo de la expresión, siempre que la conversión de tipo sea válida.

Cadenas y slices en Rust

Un punto común de confusión para los Rust principiantes es la diferencia entre el struc (tipo de dato) de String y el tipo str.

let mi_str: &str = "Hola, mundo!";

let mi_cadena: String = String::from("Hola, mundo!");

En el ejemplo anterior, mi_str es una referencia a un literal de cadena, y mi_cadena es una instancia de la estructura de String.

Una distinción importante entre los dos es que mi_str se almacena en la pila, y mi_cadena se asigna a la pila. Esto significa que el valor de mi_str no puede cambiar, y su tamaño es fijo, mientras que my_cadena puede tener un tamaño desconocido en tiempo de compilación.

El literal de cadena también se conoce como un segmento de cadena. Esto se debe a que a &str se refiere a una parte de una cadena. En general, así es como los arreglos y las cadenas son similares:

let mi_cadena = String::from("El zorro marrón rápido");
let mi_str: &str = &my_string[3..8]; // "zorro"

let mi_arr: [usize; 5] = [1, 2, 3, 4, 5];
let mi_arr_slice: &[usize] = &my_arr[0..3]; // [1, 2, 3]

La notación [T; n] se utiliza para crear un arreglo de n elementos de tipo T.

El tipo de char en Rust

Un char es un USV (Valor Escalar Unicode), que se representa en unicode con valores como U+221E, el unicode para '∞'. Puede pensar en una colección o arreglo de char como una cadena:

let mi_str: &str = "Hola, mundo!";

let colleccion_de_chars: &str = mi_str.chars().as_str();

Tipos de números en Rust

Hay muchos tipos de números en Rust:

  • Enteros sin signo: u8, u16, u32, u64, u128
  • Enteros firmados: i8, i16, i32, i64, i128
  • Números de punto flotante: f32, f64

Los enteros sin signo solo representan números enteros positivos

Los enteros con signo representan números enteros positivos y negativos.

Y los flotadores solo representan fracciones positivas y negativas.

Structs en Rust

Un struc es un tipo de datos personalizado que se utiliza para agrupar datos relacionados. Ya te has encontrado con una struct en la sección Cadenas y Slices:

struct String {
  vec: Vec<u8>,
}

La struct de String consiste en un campo vec, que es un Vec de u8s. El Vec es un arreglo de tamaño dinámico.

Una instancia de un struct despues se declara dando valores a los campos:

struct MiStruct {
  field_1: u8,
}

let mi_struct = MiStruct { field_1: 0, };

Anteriormente, el struct de String se usaba con su función from para crear una String de un &str. Esto es posible, porque la función from está implementada para String:

impl String {
  fn from(s: &str) -> Self {
    String {
      vec: Vec::from(s.as_bytes()),
    }
  }
}

Tu utilizas la palabra clave Self en lugar del tipo de struct.

Structs también pueden tomar otras variantes:

struct MiUnitStruct;
struct MiTupleStruct(u8, u8);

Enumeros en Rust

Al igual que en otros lenguajes, los enum son útiles para actuar como tipos y como valores.

enum MisErrores {
  DemasiadoCansado,
 HoraDelDia(String)
  TazaDeCafeVacia,
}

fn trabajo() -> Resultado<(), MisErrores> { // Resultado tambien es un enum
  if state == "falta punto y coma" {
    Err(MisErrores::DemasiadoCansado)
  } else if state == "06:00" {
    Err(MisErrores::HoraDelDia("Es demasiado pronto para trabajar".to_string()))
  } else if state == "22:00" {
    Err(MisErrores::HoraDelDia("Es demasiado tarde para trabajar".to_string()))
  } else if state == "empty" {
    Err(MisErrores::TazaDeCafeVacia)
  } else {
    Ok(())
  }
}

Macros en Rust

Una macro es similar a una función, pero se puede pensar en ella como una pieza de código que escribe otro código. Por ahora, las principales diferencias entre una función y una macro a tener en cuenta son:

  • Las macros se llaman usando un bang (!)
  • Las macros pueden tomar un número variable de argumentos, mientras que las funciones de Rust no pueden

Una de las macros más comunes es println! macro, que se imprime en la consola:

let my_str = "Hola, mundo!";
println!("{}", mi_str);

Se utiliza la sintaxis {} para insertar una variable en una cadena.

Otra macro común es panic!. Entrar en pánico es la forma que Rust trabaja con  "errores". Es prudente pensar en un panic en Rust como un error mal manejado. La macro acepta un literal de cadena y entra en pánico con ese mensaje.

let yo_soy_un_error = true;

if (yo_soy_un_error) {
  panic!("Ha habido un error");
}
$ cargo run
   Compiling fcc-rust-in-replit v0.1.0 (/home/runner/Rust-in-Replit)
    Finished dev [unoptimized + debuginfo] target(s) in 1.66s
     Running `target/debug/calculator`
thread 'main' panicked at 'Hubo un error', src/main.rs
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Propiedad en Rust

Un concepto importante en Rust es la propiedad. Hay tres reglas principales de propiedad:

  • Cada valor en Rust tiene una variable que se llama su propietario.
  • Solo puede haber un propietario a la vez.
  • Cuando el propietario se salga del alcance, el valor se eliminará.(Fuente: The Rust Book)

Así es como Rust se sale con la suya al no tener un colector de basura típico, al tiempo que no requiere que el programador administre explícitamente la memoria. Aquí hay un ejemplo de propiedad:

fn main() { // primera_cadena aún no está declarado -> no tiene valor
  let primera_cadena = String::from("freeCodeCamp"); // primera_cadena ahora es el propietario del valor "freeCodeCamp"
  let segunda_cadena = primera_cadena; // segunda_cadena toma posesión del valor "freeCodeCamp"

  println!("Hola, {}!", primera_cadena); // primera_cadena NO es válida, porque el valor se movió a segunda_cadena
}

Como la println! macro intenta hacer referencia a una variable no válida, este código no se compila. Para solucionar esto, en lugar de mover el valor de primera_cadena a segunda_cadena, se puede asignar a segunda_cadena una referencia a primera_cadena:

fn main() {
  let first_string: String = String::from("freeCodeCamp");
  let second_string: &String = &first_string; // first_string is still the owner of the value "freeCodeCamp"

  println!("Hello, {}!", first_string);
}

El ampersand (&) indica que el valor es una referencia. Es decir, segunda_cadena ya no toma posesión de "freeCodeCamp", sino que apunta al mismo punto en memoria que primera_cadena.

Proyecto # 1-Construir una calculadora de CLI en Rust

Resultado del proyecto

Al final de este proyecto, podrás realizar operaciones aritméticas básicas sobre números utilizando la línea de comandos.

Los ejemplos de entrada y salida esperados se ven así:

$ calculator 1 + 1
$ 1 + 1 = 2

$ calculator 138 / 4
$ 138 / 4 = 34.5

Calculadora de CLI metodología de proyecto

Paso 1-Crear un nuevo proyecto

Usa Cargo para crear un nuevo proyecto llamado calculator:

$ cargo new calculator

Esto crea un nuevo directorio llamado calculator, lo inicializa como un repositorio Git y agrega una plantilla útil para tu proyecto.

Esta plantilla incluye:

  • Cargo.toml – El archivo de manifiesto utilizado por Cargo para administrar los metadatos de tu proyecto
  • src/ – El directorio donde debe estar el código del proyecto
  • src/main.rs – El archivo predeterminado que Cargo usa como punto de entrada de la aplicación

Paso 2-Comprender la sintaxis

El archivo calculator/Cargo.toml contiene lo siguiente:

[package]
name = "calculator"
version = "0.1.0"
edition = "2018"

# Ve más claves y sus definiciones en https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

El [package] indica los metadatos de tu proyecto.

El encabezado [dependencies] indica las cajas de las que depende el proyecto. Las cajas son como librerias externas.

El archivo calculator/src/main.rs contiene lo siguiente:

fn main() {
  println!("Hola, mundo!");
}

Este archivo contiene una declaración de función con el identificador main. De forma predeterminada, rustc llama a la función main primero cada vez que se ejecuta el ejecutable.

println! es una macro incorporada que se imprime en la consola.

Paso 3-Ejecutar el proyecto

Puedes usar Cargo para ejecutar el código de tu proyecto:

# Within the calculator/ directory
$ cargo run
   Compiling fcc-rust-in-replit v0.1.0 (/home/runner/Rust-in-Replit-1)
    Finished dev [unoptimized + debuginfo] target(s) in 0.80s
     Running `target/debug/calculator`
Hello, world!

O bien, puede usar rustc para compilar su proyecto, luego puede ejecutar el binario:

# Dentro de la calculator/ directory
$ rustc src/main.rs
$ ./main
Hola, mundo!

Paso 4-Argumentos de la línea de comandos

La libreria estándar de Rust viene con un módulo env, que permite el acceso a los argumentos de la línea de comandos pasados al llamar al programa.

Las exportaciones necesarias desde el módulo env son la función args y la estructura Args. La función args devuelve una instancia de la struct Args y se importa al ámbito de archivo con:

use std::env::{args, Args};

Para tener una idea de cómo se ve la estructura Args, la variable args se imprime en la consola:

fn main() {
  let args: Args = args();
  println!("{:?}", args);
}
$ cargo run -- fCC
   Compiling calculator v0.1.0 (/home/runner/Rust-in-Replit/calculator)
    Finished dev [unoptimized + debuginfo] target(s) in 1.71s
     Running `target/debug/calculator`
Args { inner: ["target/debug/toto", "fCC"] }

Lo anterior muestra que la estructura Args contiene un field llamado inner que consiste en la ubicación del binario compilado y los argumentos de la línea de comandos pasados al programa.

Para acceder a los valores de los argumentos, puede usar el método n-ésimo en la variable args. El método n-ésimo toma un argumento de índice y devuelve el valor de ese índice envuelto en una opción. Por lo tanto, el valor debe desenvolverse.

fn main() {
  let mut args: Args = args();

  let first: String = args.nth(1).unwrap();
}

La variable args debe declararse como mutable, porque el método nth mutable itera sobre los elementos y elimina el elemento al que se accede.

fn main() {
  let mut args: Args = args();

  // El primer argumento es la ubicación del binario compilado, así que saltar
  let primero: String = args.nth(1).unwrap();
  // Después de acceder al segundo argumento, el elemento iterador siguiente se convierte en el primero
  let operador: String = args.nth(0).unwrap();
  let segundo: String = args.nth(0).unwrap();

  println!("{} {} {}", primero, operador, segundo);
}
$ cargo run -- 1 + 1
   Compiling calculator v0.1.0 (/home/runner/Rust-in-Replit/calculator)
    Finished dev [unoptimized + debuginfo] target(s) in 1.71s
     Running `target/debug/calculator`
1 + 1

Paso 5-Analiza cadenas en números

La primero y segundo variables son cadenas, y es necesario analizar en números. La struct de String implementa el método parse, que toma una anotación de tipo y devuelve un Result que contiene el valor analizado.

use std::env::{args, Args};

fn main() {
  let mut args: Args = args();

  let primero: String = args.nth(1).unwrap();
  let operador: String = args.nth(0).unwrap();
  let segundo: String = args.nth(0).unwrap();

  let primer_numero = primero.parse::<f32>().unwrap();
  let segundo_numero = segundo.parse::<f32>().unwrap();

  println!("{} {} {}", primer_numero, operador, segundo_numero);
}

El método de parse anterior utiliza la sintaxis turbofish para especificar el tipo en el que se intentará analizar la cadena.

Paso 6-Realiza operaciones aritméticas básicas

Rust utiliza los operadores estándar para sumar, restar, multiplicar y dividir.

Para manejar las operaciones, se define una función llamada operate que tomará tres argumentos: el operador como un char y los dos números como f32s. La función también debe devolver un f32 que represente el resultado de la operación.

fn operate(operador: char, primer_numero: f32, segundo_numero: f32) -> f32 {
  match operador {
    '+' => primer_numero + segundo_numero,
    '-' => primer_numero - segundo_numero,
    '/' => primer_numero / segundo_numero,
    '*' | 'X' | 'x' => primer_numero * segundo_numero,
    _ => panic!("Operador utilizado no válido."),
  }
}

La expresión match funciona de manera similar a una sentencia switch en otros lenguajes de programación. La expresión match toma un valor y una lista de arms. Cada arm es un patrón y un bloque. El patrón es un valor con el que se puede coincidir, y el bloque es el código que se debe ejecutar si el patrón coincide. El patrón _ es un comodín, que actúa como una cláusula else.

La multiplicación arm incluye la comparación OR para permitir que se manejen los casos de X y x.

Ahora, para llamar a operate con el operador, primero debe convertirlo en un char. Esto se hace con el método chars en la struct de String, que devuelve un iterador sobre los caracteres de la cadena. Luego, se desenvuelve el primer carácter:

fn main() {
  let mut args: Args = args();

  let primero: String = args.nth(1).unwrap();
  let operador: char = args.nth(0).unwrap().chars().next().unwrap();
  let segundo: String = args.nth(0).unwrap();

  let primer_numero = primero.parse::<f32>().unwrap();
  let segundo_numero = segundo.parse::<f32>().unwrap();
  let resultado = operate(operador, primer_numero, segundo_numero);

  println!("{} {} {}", primer_numero, operador, segundo_numero);
}

El retorno de operate se almacena en la variable resultado.

Paso 7-Formatear la salida

Para obtener el resultado deseado, las variables primer_numero, segungo_numero, operador y resultado deben formatearse. ¡Puedes usar el format! macro para crear una String a partir de una cadena de formato y una lista de argumentos:

fn resultado(primer_numero: f32, operador: char, segundo_numero: f32, resultado: f32) -> String {
  format!(
    "{} {} {} = {}",
    primer_numero, operador, segundo_numero, resultado
  )
}

Paso 8-Reúne Todo

use std::env::{args, Args};

fn main() {
  let mut args: Args = args();

  let primero: String = args.nth(1).unwrap();
  let operador: char = args.nth(0).unwrap().chars().next().unwrap();
  let segundo: String = args.nth(0).unwrap();

  let primer_numero = primero.parse::<f32>().unwrap();
  let segundo_numero = segundo.parse::<f32>().unwrap();
  let resultado = operate(operador, primer_numero, segundo_numero);

  println!("{}", resultado(primer_numero, operador, segundo_numero, resultado));
}

fn resultado(primer_numero: f32, operador: char, segundo_numero: f32, resultado: f32) -> String {
  format!(
    "{} {} {} = {}",
    primer_numero, operador, segundo_numero, resultado
  )
}

fn operate(operador: char, primer_numero: f32, segundo_numero: f32) -> f32 {
  match operador {
    '+' => primer_numero + segundo_numero,
    '-' => primer_numero - segundo_numero,
    '/' => primer_numero / segundo_numero,
    '*' | 'X' | 'x' => primer_numero * segundo_numero,
    _ => panic!("Operador utilizado no válido."),
  }
}

Para crear el código en un binario ejecutable, ejecuta el siguiente comando:

$ cargo build --release
   Compiling calculator v0.1.0 (/home/runner/Rust-in-Replit/calculator)
    Finished release [optimized] target(s) in 3.26s

La bandera --release release indica a Cargo que compile el binario en modo release. Esto reducirá el tamaño del binario y también eliminará cualquier información de depuración.

El binario está construido en el directorio target/release. Para ejecutar el binario y probar la aplicación, ejecuta el siguiente comando:

$ target/release/calculator 1 + 1
1 + 1 = 2

Proyecto # 2-Construye un combinador de imágenes en Rust

Resultado del proyecto

Al final de este proyecto, podrás combinar dos imágenes utilizando la línea de comandos.

Aquí hay un ejemplo de una entrada esperada:

$ combiner ./image1.png ./image2.png ./output.png

Para ver un ejemplo de la salida, no busques más allá de la primera imagen de este artículo ☝️

Metodología del proyecto combinador de Imágenes

Paso 1-Crea un nuevo proyecto

Usa Cargo para crear un nuevo proyecto llamado combiner:

$ cargo new combiner

Paso 2-Añade un nuevo módulo para args

Para prevenir la main.rs para que el archivo no se vuelva demasiado abrumador, crea un nuevo archivo con el nombre args.rs en el directorio src.

Dentro de args.rs, crea una función llamada get_nth_arg que toma un usize, n y devuelve una String. Luego, desde el módulo std::env, llama a la función args y encadena el nth método para obtener el n argumento, desenvolviendo el valor:

fn get_nth_arg(n: usize) -> String {
  std::env::args().nth(n).unwrap()
}

Define una estructura pública llamada Args que conste de tres campos públicos de tipo String: imagen_1, imagen_2 y resultado:

pub struct Args {
  pub imagen_1: String,
  pub imagen_2: String,
  pub resultado: String,
}

Declara la struct y sus campos como públicos con la palabra clave pub para que puedas acceder a ellos desde fuera del args.rs archivo.

Por último, puedes usar la función get_nth_arg para crear una nueva estructura Args en una new función:

impl Args {
  pub fn new() -> Self {
    Args {
      imagen_1: get_nth_arg(1),
      imagen_2: get_nth_arg(2),
      resultado: get_nth_arg(3),
    }
  }
}

Todos juntos, el args.rs el archivo se ve así:

pub struct Args {
  pub imagen_1: String,
  pub imagen_2: String,
  pub resultado: String,
}

impl Args {
  pub fn new() -> Self {
    Args {
      imagen_1: get_nth_arg(1),
      imagen_2: get_nth_arg(2),
      resultado: get_nth_arg(3),
    }
  }
}

fn get_nth_arg(n: usize) -> String {
  std::env::args().nth(n).unwrap()
}

Paso 3-Importar y usar el módulo args

Dentro de main.rs, tienes que declarar la args.rs archivo como módulo. Luego, para usar la estructura Args, debe importarla:

mod args;
use args::Args;

fn main() {
  let args = Args::new();
  println!("{:?}", args);
}

Pero probar el código revela un error:

$ cargo run -- arg1 arg2 arg3
   Compiling combiner v0.1.0 (/home/runner/Rust-in-Replit/combiner)
error[E0277]: `args::Args` doesn't implement `Debug`
  --> src/main.rs:12:20
   |
12 |   println!("{:?}", args);
   |                    ^^^^ `args::Args` cannot be formatted using `{:?}`
   |
   = help: the trait `Debug` is not implemented for `args::Args`
   = note: add `#[derive(Debug)]` or manually implement `Debug`
   = note: required by `std::fmt::Debug::fmt`
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `combiner`

To learn more, run the command again with --verbose.

De manera similar a cómo se implementan las funciones para estructuras, los rasgos se pueden implementar para estructuras. Sin embargo, el rasgo de Debug es especial en el sentido de que se puede implementar usando atributos:

#[derive(Debug)]
pub struct Args {
  pub imagen_1: String,
  pub imagen_2: String,
  pub resultado: String,
}

El rasgo de Debug se ha derivado para la estructura Args. Esto significa que el rasgo de Debug se implementa automáticamente para la estructura, sin tener que implementarlo manualmente ?.

Ahora, al ejecutar el código funciona:

$ cargo run -- arg1 arg2 arg3
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/combiner arg1 arg2 arg3`
Args { imagen_1: "arg1", imagen_2: "arg2", resultado: "arg3" }

Paso 4-Añadir una caja externa

De la misma manera que otros lenguajes tienen librerias o paquetes, Rust tiene cajas. Para codificar y decodificar imágenes, puedes usar la caja image.

Agrega la caja image con la versión 0.23.14 a el archivo Cargo.toml:

[package]
name = "combiner"
version = "0.1.0"
edition = "2018"

# Ve más claves y sus definiciones en https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
image = "0.23.14"

Ahora, cuando se llames a cargo run, Cargo buscará e instalará la caja image.

Paso 5-Leer un archivo de imagen

La caja image viene con un módulo de io que incluye una struct de Reader. Esta struct implementa una función open que toma una ruta a un archivo de imagen y devuelve un Result que contiene un reader. Puedes formatear y decodificar este reader para obtener el formato de imagen (por ejemplo, PNG, JGP, etc.) y los datos de imagen.

Crea una función llamada find_image_from_path para abrir el archivo de imagen desde un argumento de path:

fn find_image_from_path(path: String) -> (DynamicImage, ImageFormat) {
  let image_reader: Reader<BufReader<File>> = Reader::open(path).unwrap();
  let image_format: ImageFormat = image_reader.format().unwrap();
  let image: DynamicImage = image_reader.decode().unwrap();
  (image, image_format)
}

Las variables image y image_format se devuelven como una tupla.

Incluye las importaciones necesarias en la parte superior del archivo:

use image::{ io::Reader, DynamicImage, ImageFormat };

fn main() {
  // ...
  let (image_1, image_1_format) = find_image_from_path(args.image_1);
  let (image_2, image_2_format) = find_image_from_path(args.image_2);
}

Dentro de main, la tupla devuelta se puede estructurar en dos variables nuevas para cada ruta de imagen.

Paso 6-Maneja errores con Result

Es importante ser capaz de manejar los errores que surgen. Por ejemplo, es posible que tengas un caso en el que se dan dos imágenes de formatos diferentes como argumentos para combinar.

Una forma semántica de manejar tal error es devolver un Result que puede consistir en un Ok o un Err.

fn main() -> Result<(), ImageDataErrors> {
  let args = Args::new();
  println!("{:?}", args);

  let (image_1, image_1_format) = find_image_from_path(args.image_1);
  let (image_2, image_2_format) = find_image_from_path(args.image_2);

  if image_1_format != image_2_format {
    return Err(ImageDataErrors::DifferentImageFormats);
  }
  Ok(())
}

La función main devuelve un Err que contiene una enumeración con varianza de unidad DifferentImageFormats si los dos formatos de imagen no son iguales. De lo contrario, devuelve un Ok con una tupla vacía.

La enumeración se define como:

enum ImageDataErrors {
  DifferentImageFormats,
}

Paso 7-Cambia el tamaño de las imágenes para que coincidan

Para facilitar la combinación de las imágenes, cambia el tamaño de la imagen más grande para que coincida con la imagen más pequeña.

En primer lugar, puedes encontrar la imagen más pequeña utilizando el método dimensions, que devuelve el ancho y el alto de la imagen como una tupla. Estas tuplas se pueden comparar, y la más pequeña devuelve:

fn get_smallest_dimensions(dim_1: (u32, u32), dim_2: (u32, u32)) -> (u32, u32) {
  let pix_1 = dim_1.0 * dim_1.1;
  let pix_2 = dim_2.0 * dim_2.1;
  return if pix_1 < pix_2 { dim_1 } else { dim_2 };
}

Se accede a los valores de la tupla mediante notación de puntos desde la indexación basada en cero.

Si image_2 es la imagen más pequeña, entonces image_1 necesita redimensionarse para que coincida con las dimensiones más pequeñas. De lo contrario, image_2 necesita redimensionarse.

fn standardise_size(image_1: DynamicImage, image_2: DynamicImage) -> (DynamicImage, DynamicImage) {
  let (width, height) = get_smallest_dimensions(image_1.dimensions(), image_2.dimensions());
  println!("width: {}, height: {}\n", width, height);

  if image_2.dimensions() == (width, height) {
    (image_1.resize_exact(width, height, Triangle), image_2)
  } else {
    (image_1, image_2.resize_exact(width, height, Triangle))
  }
}

El método resize_exact implementado en la struct de DynamicImage mutable toma prestada la imagen y, utilizando los argumentos width, height y filterType, redimensiona la imagen.

Usando el retorno de la función standardise_size, puede volver a declarar las variables image_1 e image_2:

use image::{ io::Reader, DynamicImage, ImageFormat, imageops::FilterType::Triangle };

fn main() -> Result<(), ImageDataErrors> {
  // ...
  let (image_1, image_2) = standardise_size(image_1, image_2);
  Ok(())
}

Paso 8 – Crea una imagen flotante

Para manejar la salida, crea una struct temporal que contenga los metadatos de la imagen de salida.

Define una struct llamada FloatingImage para contener el width, height y data de la imagen, así como el name del archivo de salida:

struct FloatingImage {
  width: u32,
  height: u32,
  data: Vec<u8>,
  name: String,
}

A continuación, implementa una new función para la FloatingImage que toma valores para el width, height y el name de la imagen de salida:

impl FloatingImage {
  fn new(width: u32, height: u32, name: String) -> Self {
    let buffer_capacity = 3_655_744;
    let buffer: Vec<u8> = Vec::with_capacity(buffer_capacity);
    FloatingImage {
      width,
      height,
      data: buffer,
      name,
    }
  }
}

Como aún no had creado los datos para la imagen, crea un búfer en forma de Vec de u8 con una capacidad de 3.655.744 (956 x 956 x 4). La sintaxis <number>_<number> es la numeración fácil de leer de Rust que separa el número en grupos o tres dígitos.

Usa los valores de width y height de la variable image_1 para crear una instancia de la FloatingImage, y usa el tercer argumento almacenado en args para establecer el nombre de la FloatingImage:

fn main() -> Result<(), ImageDataErrors> {
  // ...
  let mut output = FloatingImage::new(image_1.width(), image_1.height(), args.output);
  Ok(())
}

Declara las variables de outpu como mutables para que pueda modificar el campo de data más adelante.

Paso 9-Crea los datos de imagen combinados

Para procesar las imágenes, debes convertirlas en un vector de píxeles RGBA. Los píxeles se almacenan como u8, porque sus valores están entre 0 y 255.

La struct de DynamicImage implementa el método to_rgba8, que devuelve un ImageBuffer que contiene un Vec<u8>, y el ImageBuffer implementa el método into_vec, que devuelve el Vec<u8>:

fn combine_images(image_1: DynamicImage, image_2: DynamicImage) -> Vec<u8> {
  let vec_1 = image_1.to_rgba8().into_vec();
  let vec_2 = image_2.to_rgba8().into_vec();

  alternate_pixels(vec_1, vec_2)
}

Luego, las variables vec_1 y vec_2 se pasan a la función alternate_pixels, que devuelve los datos de imagen combinados alternando los conjuntos de píxeles RGBA de las dos imágenes:

fn alternate_pixels(vec_1: Vec<u8>, vec_2: Vec<u8>) -> Vec<u8> {
  // A Vec<u8> is created with the same length as vec_1
  let mut combined_data = vec![0u8; vec_1.len()];

  let mut i = 0;
  while i < vec_1.len() {
    if i % 8 == 0 {
      combined_data.splice(i..=i + 3, set_rgba(&vec_1, i, i + 3));
    } else {
      combined_data.splice(i..=i + 3, set_rgba(&vec_2, i, i + 3));
    }
    i += 4;
  }

  combined_data
}

La función set_rgba toma una referencia a un Vec<u8> y devuelve el conjunto de píxeles RGBA para ese Vec<u8> que comienza y termina en un índice dado:

fn set_rgba(vec: &Vec<u8>, start: usize, end: usize) -> Vec<u8> {
  let mut rgba = Vec::new();
  for i in start..=end {
    let val = match vec.get(i) {
      Some(d) => *d,
      None => panic!("Index out of bounds"),
    };
    rgba.push(val);
  }
  rgba
}

El ..= syntax es la sintaxis de rango de Rust que permite que el rango incluya el valor final. El símbolo * antes de una variable es el operador de desreferencia de Rust, que permite acceder al valor de la variable.

Y ahora, asigna el retorno de combine_images a la variable combined_data:

fn main() -> Result<(), ImageDataErrors> {
  // ...
  let combined_data = combine_images(image_1, image_2);
  Ok(())
}

Paso 10: Adjunta los datos combinados a la imagen flotante

Para establecer los datos de combined_data en la imagen de output, se define un método en FloatingImage para establecer el campo de data de output al valor de combined_data.

Hasta ahora, solo has implementado funciones en struct. Los métodos se definen de manera similar, pero toman una instancia de la struct como su primer argumento:

struct MyStruct {
  name: String,
}
impl MyStruct {
  fn change_name(&mut self, new_name: &str) {
    self.name = new_name.to_string();
  }
}

let mut my_struct = MyStruct { name: String::from("Shaun") };
// my_struct.name == "Shaun"
my_struct.change_name("Tom");
// my_struct.name == "Tom"

Dado que necesitas cambiar el valor de la instancia de FloatingImage, el método set_data toma una referencia mutable a la instancia como primer argumento.

impl FloatingImage {
  // ...
  fn set_data(&mut self, data: Vec<u8>) -> Result<(), ImageDataErrors> {
    // If the previously assigned buffer is too small to hold the new data
    if data.len() > self.data.capacity() {
      return Err(ImageDataErrors::BufferTooSmall);
    }
    self.data = data;
    Ok(())
  }
}

La enumeración debe ampliarse para incluir la nueva variante de unidad BufferTooSmall:

enum ImageDataErrors {
  // ...
  BufferTooSmall,
}

Aviso: El método todavía solo se llama con un argumento:

fn main() -> Result<(), ImageDataErrors> {
  // ...
  output.set_data(combined_data)?;
  Ok(())
}

El ? sintaxis al final de una expresión es una forma abreviada de manejar el resultado de una llamada a una función. Si la llamada a la función devuelve un error, el operador de propagación de errores devolverá el error de la llamada a la función.

Paso 11-Escribe la imagen en un archivo

Por último, guarda la nueva imagen en un archivo. La caja image tiene una función save_buffer_with_format que toma la siguiente forma:

fn save_buffer_with_format(
    path: AsRef<Path>,
    buf: &[u8],
    width: u32,
    height: u32,
    color: image::ColorType,
    format: image::ImageFormat
  ) -> image::ImageResult<()>;

Dado que AsRef está implementado para String, puedes usar un argumento de tipo String para el path.

fn main() -> Result<(), ImageDataErrors> {
  // ...
  image::save_buffer_with_format(
    output.name,
    &output.data,
    output.width,
    output.height,
    image::ColorType::Rgba8,
    image_1_format,
  )
  .unwrap();
  Ok(())
}

Paso 12-Une todo

Aquí está el código final:

mod args;

use args::Args;
use image::{
  imageops::FilterType::Triangle, io::Reader, DynamicImage, GenericImageView, ImageFormat,
};

fn main() -> Result<(), ImageDataErrors> {
  let args = Args::new();
  println!("{:?}", args);

  let (image_1, image_1_format) = find_image_from_path(args.image_1);
  let (image_2, image_2_format) = find_image_from_path(args.image_2);

  if image_1_format != image_2_format {
    return Err(ImageDataErrors::DifferentImageFormats);
  }

  let (image_1, image_2) = standardise_size(image_1, image_2);
  let mut output = FloatingImage::new(image_1.width(), image_1.height(), args.output);

  let combined_data = combine_images(image_1, image_2);

  output.set_data(combined_data)?;

  image::save_buffer_with_format(
    output.name,
    &output.data,
    output.width,
    output.height,
    image::ColorType::Rgba8,
    image_1_format,
  )
  .unwrap();
  Ok(())
}

enum ImageDataErrors {
  BufferTooSmall,
  DifferentImageFormats,
}

struct FloatingImage {
  width: u32,
  height: u32,
  data: Vec<u8>,
  name: String,
}

impl FloatingImage {
  fn new(width: u32, height: u32, name: String) -> Self {
    let buffer_capacity = 3_655_744;
    let buffer: Vec<u8> = Vec::with_capacity(buffer_capacity);
    FloatingImage {
      width,
      height,
      data: buffer,
      name,
    }
  }
  fn set_data(&mut self, data: Vec<u8>) -> Result<(), ImageDataErrors> {
    if data.len() > self.data.capacity() {
      return Err(ImageDataErrors::BufferTooSmall);
    }
    self.data = data;
    Ok(())
  }
}

fn find_image_from_path(path: String) -> (DynamicImage, ImageFormat) {
  let image_reader = Reader::open(path).unwrap();
  let image_format = image_reader.format().unwrap();
  let image = image_reader.decode().unwrap();
  (image, image_format)
}

fn standardise_size(image_1: DynamicImage, image_2: DynamicImage) -> (DynamicImage, DynamicImage) {
  let (width, height) = get_smallest_dimensions(image_1.dimensions(), image_2.dimensions());
  println!("width: {}, height: {}\n", width, height);
  if image_2.dimensions() == (width, height) {
    (image_1.resize_exact(width, height, Triangle), image_2)
  } else {
    (image_1, image_2.resize_exact(width, height, Triangle))
  }
}

fn get_smallest_dimensions(dim_1: (u32, u32), dim_2: (u32, u32)) -> (u32, u32) {
  let pix_1 = dim_1.0 * dim_1.1;
  let pix_2 = dim_2.0 * dim_2.1;
  return if pix_1 < pix_2 { dim_1 } else { dim_2 };
}

fn combine_images(image_1: DynamicImage, image_2: DynamicImage) -> Vec<u8> {
  let vec_1 = image_1.to_rgba8().into_vec();
  let vec_2 = image_2.to_rgba8().into_vec();

  alternate_pixels(vec_1, vec_2)
}

fn alternate_pixels(vec_1: Vec<u8>, vec_2: Vec<u8>) -> Vec<u8> {
  let mut combined_data = vec![0u8; vec_1.len()];

  let mut i = 0;
  while i < vec_1.len() {
    if i % 8 == 0 {
      combined_data.splice(i..=i + 3, set_rgba(&vec_1, i, i + 3));
    } else {
      combined_data.splice(i..=i + 3, set_rgba(&vec_2, i, i + 3));
    }
    i += 4;
  }

  combined_data
}

fn set_rgba(vec: &Vec<u8>, start: usize, end: usize) -> Vec<u8> {
  let mut rgba = Vec::new();
  for i in start..=end {
    let val = match vec.get(i) {
      Some(d) => *d,
      None => panic!("Index out of bounds"),
    };
    rgba.push(val);
  }
  rgba
}

Construyendo el binario:

$ cargo build --release

Crear una imagen combinada, usando las imágenes en freeCodeCamp/Rust-In-Replit:

$ ./target/release/combiner images/pro.png images/fcc_glyph.png images/output.png

Y aquí está el resultado en images/output.png:

Output combined image

Conclusion

Con eso, ahora conoces los conceptos básicos de Rust.

Todavía hay mucho por aprender. Por lo tanto, mira este espacio para obtener más contenido ?.