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:
- Una calculadora para la línea de comandos
- 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 del Rust
- Cómo usar Rust en Replit
- Fundamentos de Rust
- Proyecto # 1-Construir una calculadora de CLI en Rust
- Proyecto # 2-Construye un combinador de imágenes en Rust
- Resultado del Proyecto
- Metodología del proyecto combinador de imágenes
- Paso 1-Crear un nuevo proyecto
- Paso 2-Añadir un nuevo módulo para args
- Paso 3-Importar y usar el módulo args
- Paso 4-Añadir una caja externa
- Paso 5-Leer un archivo de imagen
- Paso 6-Manejar errores con resultado
- Paso 7-Cambiar el tamaño de las imágenes para que coincidan
- Paso 8-Crear una imagen flotante
- Paso 9-Crear los datos de imagen combinados
- Paso 10: Adjunte los datos combinados a la imagen flotante
- Paso 11-Escribe la imagen en un archivo
- Paso 12-Une todo
- Conclusion
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:
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:
OBJECT | CASING |
---|---|
Variables | snake_case |
Functions | snake_case |
Files | snake_case |
Constants | SCREAMING_SNAKE_CASE |
Statics | SCREAMING_SNAKE_CASE |
Types | PascalCase |
Traits | PascalCase |
Enums | PascalCase |
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 u8
s. 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 proyectosrc/
– El directorio donde debe estar el código del proyectosrc/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 f32
s. 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
:

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 😉.