Skip to content

JMustang/Curso_basico_de_RustLang

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

43 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Curso basico de Rust-lang

LOGO

Fonte: artigo tirado do site freeCodeCamp, de Shaun Hamilton

Uma visão geral do Rust

Rust, uma linguagem de programação a nivel de sistema

  • Lida com detalhes de baixo nivel de gerenciamento de memoria, representacao de dados e simultaneidade.
  • A linguagem foi projetada para guiá-lo naturalmente em direção a um código confiável que seja eficiente em termos de velocidade e uso de memória.

Principais ferramentas do ecosistema Rust

  1. rustc - O copilador do rust que transforma o codigo em binario (Codigo de maquina).
  2. rustup - A linha de comando para instalar e atualizar o Rust.
  3. cargo - Gerenciador de pacotes do Rust.

Basico do Rust

Variaveis em Rust


Você pode declarar uma variaveu usando, let, const ou static

EX:

let minha_variavel = 0;
const minha_constante: u8 = 0;
static meu_static: u8 = 0;

Por padrão, todas as variáveis sao imutáveis, mas Você pode transforma em mutável usando a palavra-chave mut.

EX:

let mut minha_variavel_mutavel = 0;

Convenções rust.

EX:

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

Como rust é tipado estaticamente, você precisará digitar variáveis ​​explicitamente - a menos que a variável seja declarada com let e o tipo possa ser inferido.


função em Rust

  • Função em rust pode ser declaradas usando a palavra-chave fn.

EX:

fn main() {
  // comentario de codigo
}
  • As funções retornam usando a palavra-chave return e você precisa especificar explicitamente o tipo de retorno de uma função, a menos que o tipo de retorno seja uma tupla vazia ():

EX:

fn main() ->{}{ // Tipo de retorno desnecessário
  minha_func();
}

fn minha_func()-> u8 {
  return 0;
}
  • As funções também retornam uma expressão sem o ponto e vírgula:

EX:

fn minha_func() -> u8 {
  0
}
  • Os parâmetros das funções são passados usando a sintaxe ":".

EX:

fn main() {
  let _variavel_nao_utilizada = minha_func(10);
}

fn minha_func(x: u8) -> i32 {
  x as i32
}
  • O sublinhado antes de um nome de variável é uma convenção para indicar que a variável não é usada. A palavra-chave "as" afirma o tipo da expressão, desde que a conversão de tipo seja válida.

Strings e Slices em Rust

  • Um ponto em comum que confunde os novatos Rustacians, é a diferenca entre Strings e o tipo str.

EX:

let meu_str: &str = "Ola mundo!";

let minha_string: String = String::from("Ola Mundo!");
  • No exemplo acima, meu_str é uma referencia para uma String literal, e minha_string é uma instacia da String.
  • Uma distinção importante entre elas é que meu_str é armazenada em pilha e minha_string é alocada em pilha. Isso significa que o valor de meu_str não pode mudar e seu tamanho e fixo, enquanto minha_string pode ter um tamanho desconhecido em tempo de compilação.
  • A String literal tambem e conhecida como fatia de string. Isso ocorre porque um &str se refere a parte de uma string. Geralmente, é assim que arrays e Strrings assemelham-se.

EX:

let minha_string = String::from("The quick brown fox");
let meu_str: &str = &minha_string[4..9]; //"Rapido"

let meu_arr: [usize; 5] = [1,2,3,4,5];
let meu_arr_fatiado: &[usize] = &meu_arr[0..3]; // [1,2,3]
  • O [T; n] é usado para criar um array de n elementos de tipo T.

O tipo char no rust

  • Um char é um USV (Unicode Scalar Value), que é representado por um valor unicode como '∞'. Você deve pensar em uma coleção ou array de caracteres como uma string.

EX:

let meu_str: &str = "Ola, Mundo!"

let colecao_de_chars: &str = meu_str.chars().as_str();

Tipo Number em rust

  • Existem varios tipos de Numbers em rust:

1.Inteiros não declarado:u8, u16, u32, u64, u128 2.Inteiros declarado:i8, i16, i32, i64, i128 3.Números Float:f32, f64

  • Inteiros não declarado, representam apenas números inteiros positivos.
  • Inteiros declarado, representam números inteiros positivos e negativos.
  • Float representam apenas frações positivas e negativas.

Structs no Rust

  • Um Struct é um tipo de dados personalizado usado para agrupar dados relacionados. Você já encontrou um struct na seção Strings e Slices:

EX:

struct String {
  vec: Vec<u8>,
}
  • A String struct consiste em um campo vec, que é um Vec de u8s. O Vec é um array de tamanho dinâmico.

  • Uma instância de um struct é então declarada dando valores aos campos: EX:

struct MeuStruct {
  field_1: u8,
}

let meu_struct = MeuStruct { field_1: 0, };
  • Anteriormente, a struct String era usado com sua função from para criar uma String a partir de um &str. Isso é possivel porque a função from é implementada para a String:

EX:

iml String {
  fn from(s: &str) -> self {
    String {
      vec: Vec::from(s.as_bytes()),
    }
  }
}
  • Você usa a palavra-chave Self no lugar do tipo da struct.

Os structs também podem ter outras variantes:

EX:

struct MinhaUnidadeStruct;
struct MinhaTuplesStruct(u8, u8);

Enums em Rust

  • Semelhante a outras linguagens, enums são úteis para atuar como tipos e valores.

EX:

enum meusErros {
  CabecaCansada,
  TimeOfDay(String)
  SemCafe,
}

fn work() -> Result<(), meusErros> { // Result também é um enum
  if state == "Faltando ponto-e-vírgula" {
    Err(meusErros::CabecaCansada)
  } else if state == "06:00" {
    Err(meusErros::TimeOfDay("é muito cedo para trabalhar".to_string()))
  } else if state == "22:00" {
    Err(meusErros::TimeOfDay("é muito tarde para trabalhar".to_string()))
  } else if state == "vaziu" {
    Err(meusErros::SemCafe)
  } else {
    ok(())
  }
}

Macros em Rust

  • Um macro é semelhante a uma função, mas você pode pensar nela como um pedaço de código que escreve outro código. Por enquanto, as principais diferenças entre uma função e uma macro a serem lembradas são:
  1. As macros são chamadas usando um bang (!).
  2. Macros podem receber um número variável de argumentos, enquanto funções em Rust não podem.
  • Uma das macros mais comuns é a println! macro, que imprime no console:

EX:

let meu_str = "Ola, Mundo!";
println!("{}", meu_str);
  • Você usa a sintaxe {} para inserir uma variável em uma string.

  • Outra macro comum é o panic!. Entrar em pânico é a maneira de Rust 'errar'. É sábio pensar em um panic! em Rust como um erro mal tratado. A macro aceita um literal de string e entra em pânico com essa mensagem.

EX:

let eu_sou_um_erro = true;

if (eu_sou_um_erro) {
  panic!("Existe um erro");
}
# cargo é o NPM (package manage) do Rust
$ 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 'There was an error', src/main.rs
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Ownership em Rust

  • Um conceito importante em Rust é a Ownership. Existem três regras principais de Ownership:
  1. Cada valor em Rust tem uma variável que é chamada de owner.
  2. Só pode haver um owner de cada vez.
  3. Quando o owner sair do escopo, o valor será descartado.
  • É assim que o Rust se safa de não ter um garbage collector, ao mesmo tempo em que não exige que o programador gerencie explicitamente a memória. Aqui está um exemplo de Ownership:

EX:

fn main() {// first_string ainda não foi declarado -> não tem valor
  let first_string = String::from("freeCodeCamp"); // first_string agora e Ownership de um valor "freeCodeCamp"
  let second_string = first_string; // second_string assume a Ownership do valor "freeCodeCamp"

  println!("Hello, {}", first_string) // first_string NÃO é válido, porque o valor foi movido para second_string
}
  • Como o macro println! tenta se referir a uma variável inválida, esse código não compila. Para corrigir isso, em vez de mover o valor de first_string para second_string, second_string pode receber uma referência para first_string:

EX:

fn main() {
  let first_string: String = String::from("freeCodeCamp");
  let second_string: &String = &first_string; // first_string ainda é o owner do valor "freeCodeCamp"
  println!("Hello, {}", first_string)
}
  • O E comercial (&) indica que o valor é uma referência. Ou seja, second_string não se apropria mais de "freeCodeCamp", mas, em vez disso, aponta para o mesmo ponto na memória que first_string.

Projeto #1 – Construir uma calculadora CLI em Rust

Resultado do projeto

  • No final deste projeto, você será capaz de realizar operações aritméticas básicas em números usando a linha de comando.

Exemplos de entrada e saída esperadas são assim:

calculator 1 + 1
1 + 1 = 2

calculator 138 / 4
138 / 4 = 34.5

Metodologia do Projeto da Calculadora CLI

Passo 1 – Criar um novo projeto

  • Use Cargo para criar um novo projeto chamado calculadora:
cargo new calculadora

Esse comando cria um novo diretório chamado calculadora, o Git e um boilerplate útil são adicionado ao seu projeto.

O boilerplate inclui

  1. Cargo.toml - O arquivo de manifesto usado pelo Cargo para gerenciar os metadados do seu projeto
  2. src - O diretorio onde vai ficar o seu codigo.
  3. src/main.rs - O arquivo padrão que o Cargo usa como ponto de entrada do aplicativo

Passo 2 – Entendendo a sintax

O arquivo Cargo.toml contém o seguinte:

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

[dependencies]

O [package] indica os metadados do seu projeto.

O cabeçalho [dependencies] indica as crates das quais seu projeto depende. As crates são como bibliotecas externas.

O arquivo main.rs contém o seguinte:

fn main() {
  println!("Hello, world!");
}

Este arquivo contém uma declaração de função com o handle main. Por padrão, rustc chama a função main primeiro sempre que o executável for chamado.

println! é uma macro interna que "printa" algo no console.


Passo 3 – Rodando o projeto

Você pode usar o Cargo para executar o código do seu projeto:

# Dentro do diretorio /calculadora
$ 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!

Ou Você pode usa rustc para copilar o projeto,e roda o binario:

# Dentro do diretorio /calculadora
$ rustc src/main.rs
$ ./main
Hello, world!

Passo 4 – Argumentos da linha de comando

A biblioteca padrão do Rust vem com um módulo env, que permite acesso aos argumentos de linha de comando passados ​​ao chamar o programa.

As exportações necessárias do módulo env são a função args e a estrutura Args. A função args retorna uma instância da estrutura args e é importada para o escopo do arquivo com:

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

Para ter uma ideia de como é a estrutura Args, a variável args é impressa no console:

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"] }

O trecho acima mostra que a estrutura Args contém um campo chamado inner que consiste na localização do binário compilado e os argumentos da linha de comando passados ​​para o programa.

Para acessar os valores dos argumentos, você pode usar o método nth na variável args. O nth método recebe um argumento de index, e retorna o valor nesse índice envolto em uma opção. Portanto, o valor precisa ser desembrulhado.

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

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

A variável args precisa ser declarada como mutável, porque o método nth mutável itera sobre os elementos e remove o elemento acessado.

EX:

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

  // O primeiro argumento é a localização do binário compilado, então pule-o
  let first: String = args.nth(1).unwrap();
  // Depois de acessar o segundo argumento, o próximo elemento do iterador se torna o primeiro
  let operator: String = args.nth(0).unwrap();
  let second: String = args.nth(0).unwrap();

  println!("{} {} {}", first, operator, second);
}
$ 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` 

Passo 5 – transformar Strings em Números

A primeira e a segunda variáveis ​​são strings e você precisa transforma-las em números. A estrutura strings implementa o método parse, que recebe uma anotação de tipo e retorna um Result contendo o valor analisado.

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

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

  let first: String = args.nth(1).unwrap();
  let operator: String = args.nth(0).unwrap();
  let second: String = args.nth(0).unwrap();

  let first_number = first.parse::<f32>().unwrap();
  let second_number = second.parse::<f32>().unwrap();

  println!("{} {} {}", first_number, operator, second_number);
}

O método parse acima usa a sintaxe do turbofish para especificar o tipo para tentar transformar a string.


Passo 6 – Executando operações aritméticas básicas

Rust usa os operadores padrão para realizar adições, subtrações, multiplicações e divisões.

Para lidar com as operações, você define uma função chamada operate que recebe três argumentos: O operador como um char, e dois números como f32s. A função operate retorna um f32 como resultado.

EX:

fn operate(operator: char, first_number: f32, second_number: f32) -> f32 {
  match operator {
    '+' => first_number + second_number,
    '-' => first_number - second_number,
    '/' => first_number / second_number,
    '*' | 'X' | 'x' => first_number * second_number,
    _ => panic!("Invalid operator used."),
  }
}

A expressão match funciona de maneira semelhante a uma instrução switch em outra linguagem. A expressão match recebe um valor e uma lista de casos. Cada casos é um padrão de valor e uma bloco. O padrão é um valor para corresponder e o bloco é o código a ser executado se o padrão corresponder. O padrão _ é um curinga, agindo como uma cláusula else. O caso de multiplicação inclui a comparação OR para permitir que casos para X e x sejam tratados.

Agora, para chamar operar com o operador, você precisa primeiro convertê-lo em um char. Você faz isso com o método chars na estrutura String que retorna um iterador sobre os caracteres na string. Em seguida, o primeiro caractere é desempacotado:

EX:

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

  let first: String = args.nth(1).unwrap();
  let operator: char = args.nth(0).unwrap().chars().next().unwrap();
  let second: String = args.nth(0).unwrap();

  let first_number = first.parse::<f32>().unwrap();
  let second_number = second.parse::<f32>().unwrap();
  let result = operate(operator, first_number, second_number);

  println!("{} {} {}", first_number, operator, second_number);
}

O retorno da função operate sera alocada na variavel result.


Passo sete – Formate a saída

Para obter a saida desejada, as variaveis first_number, second_number e result precisam ser formatadas. Você pode usar o método macro format! para criar uma String a partir de uma string e uma lista de argumentos.

fn output(first_number: f32, operator: char, second_number: f32, result: f32) -> String {
  format!(
    "{} {} {} = {}",
    first_number, operator, second_number, result
  )
}

Passo 8 – Junte tudo

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

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

  let first: String = args.nth(1).unwrap();
  let operator: char = args.nth(0).unwrap().chars().next().unwrap();
  let second: String = args.nth(0).unwrap();

  let first_number = first.parse::<f32>().unwrap();
  let second_number = second.parse::<f32>().unwrap();
  let result = operate(operator, first_number, second_number);

  println!("{}", output(first_number, operator, second_number, result));
}

fn output(first_number: f32, operator: char, second_number: f32, result: f32) -> String {
  format!(
    "{} {} {} = {}",
    first_number, operator, second_number, result
  )
}

fn operate(operator: char, first_number: f32, second_number: f32) -> f32 {
  match operator {
    '+' => first_number + second_number,
    '-' => first_number - second_number,
    '/' => first_number / second_number,
    '*' | 'X' | 'x' => first_number * second_number,
    _ => panic!("Invalid operator used."),
  }
}

Para criar o executável, você precisa rodar o seguinte comando:

$ cargo build --release
   Compiling calculadora v0.1.0 (/home/junior/Code Projects/Rust-Lang/Curso_basico_de_RustLang/calculadora)
    Finished release [optimized] target(s) in 0.29s

A flag --release diz a o Cargo para compilar o binario para o modo release. Isso reduzirá o tamanho do binário e também removerá qualquer informação de depuração.

O binario sera criado no diretorio target/release. Para rodar o binario, você precisa executar o seguinte comando:

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

Projeto #2 – Construir um Combinador de Imagens em Rust

Resultado do projeto

  • No final deste projeto, você poderá combinar duas imagens usando a linha de comando.

Aqui está um exemplo de uma entrada esperada:

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

Para um exemplo de saida, use a imagem desse artigo: ☝️

Metodologia desse projeto

Passo 1 - Criar um novo projeto

Use o Cargo para criar um novo projeto chamado combiner:

cargo new combiner

Passo 2 - Adicionar um modulo novo chamado Args

Para evitar que o arquivo main.rs fique muito pessado, crie um novo modulo chamado Args.rs no diretorio src/. Dentro de Args.rs crie uma função chamada get_nth_arg que recebe um usize, n, e retorna uma String. Então, apartir do modulo std::env, chame a função args e encadeie o nth método para obter o nth argumento, desempacotando o valor:

EX:

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

Defina uma estrutura pública chamada Args que consiste em três campos públicos de tipo String:image_1, image_2 e output:

EX:

pub struct Args {
  pub image_1: String,
  pub image_2: String,
  pub output: String,
}

Declare a estrutura e seus campos como públicos com a palavra-chave pub para que você possa acessá-los de fora do arquivo args.rs.

Por fim, você pode usar a função get_nth_arg para criar uma nova estrutura Args em uma nova função:

EX:

impl Args {
  pub fn new() -> Self {
    Args {
      image_1: get_nth_arg(1),
      image_2: get_nth_arg(2),
      output: get_nth_arg(3),
    }
  }
}

O arquivo args.rs deve ficar como o seguinte:

pub struct Args {
  pub image_1: String,
  pub image_2: String,
  pub output: String,
}

impl Args {
  pub fn new() -> Self {
    Args {
      image_1: get_nth_arg(1),
      image_2: get_nth_arg(2),
      output: get_nth_arg(3),
    }
  }
}

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

Passo 3 – Importar e usar o módulo args

Dentro de main.rs, você precisa declarar o arquivo args.rs como um módulo. Então, para usar a estrutura Args, você precisa importá-la:

EX:

mod args;
use args::Args;

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

Mas testar o código revela um erro:

EX:

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

Da mesma forma que as funções são implementadas para structs, as traits podem ser implementadas para structs. No entanto, o traits Debug é especial, pois pode ser implementado usando atributos:

EX:

#[derive(Debug)]
pub struct Args {
  pub image_1: String,
  pub image_2: String,
  pub output: String,
}

A característica Debug foi derivada para a estrutura Args. Isso significa que o traits Debug é implementado automaticamente para a estrutura, sem que você precise implementá-lo manualmente 🚀.

Agora, executando o 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 { image_1: "arg1", image_2: "arg2", output: "arg3" }

Passo 4 – Adicione uma Crate

Da mesma forma que outras linguagens têm bibliotecas ou pacotes, Rust tem Crates. Para codificar e decodificar imagens, você pode usar a crate image.

Adicione a crate de imagem com a versão 0.23.14 ao arquivo Cargo.toml:

EX:

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

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
image = "0.23.14"

Agora, quando o cargo for chamada, Cargo irá buscar e instalar a crate image.


Passo 5 – Ler um arquivo de imagem

A crate de image vem com um módulo io incluindo uma estrutura Reader. Essa estrutura implementa uma função open que leva um caminho para um arquivo de imagem e retorna um Result contendo um leitor. Você pode formatar e decodificar este leitor para produzir o formato da imagem (por exemplo, PNG, JGP e assim por diante) e os dados da imagem. Crie uma função chamada find_image_from_path para abrir o arquivo de imagem a partir de um argumento de path:

EX:

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)
}

As variaveis image e image_format iram retorna uma tupla.

Inclua os imports necessários:

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, a tupla retornada pode ser desestruturada em duas novas variáveis ​​para cada caminho de imagem.


Passo 6 – Lidar com Erros com Result

É importante ser capaz de lidar com os erros que surgem. Por exemplo, você pode ter um caso em que duas imagens de formatos diferentes são fornecidas como argumentos para combinar.

Uma maneira semântica de lidar com esse erro é retornar um Result que pode consistir em um Ok ou um Err.

EX:

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(())
}

A função main retorna um Err contendo uma enum com a variante de unidade DifferentImageFormats se os dois formatos de imagem não forem iguais. Caso contrário, retorna um Ok com uma tupla vazia.

O enum é definido como:

EX:

enum ImageDataErrors {
  DifferentImageFormats,
}

Passo 7 – Redimensione as imagens para combinar

Para facilitar a combinação das imagens, você redimensiona a imagem maior para corresponder à imagem menor.

Primeiro, você pode encontrar a menor imagem usando o método de dimensions que retorna a largura e a altura da imagem como uma tupla. Essas tuplas podem ser comparadas e a menor retornada:

EX:

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 };
}

Os valores da tupla são acessados ​​usando notação de ponto da indexação baseada em zero.

Se a imagem_2 for a menor imagem, a imagem_1 precisará ser redimensionada para corresponder às menores dimensões. Caso contrário, image_2 precisa ser redimensionado.

EX:

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))
  }
}

O método resize_exact implementado na estrutura DynamicImage empresta a imagem de forma mutável e, usando os argumentos width, height e FilterType, redimensiona a imagem.

Usando o retorno da função standardise_size, você pode redeclarar as variáveis ​​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(())
}

Passo 8 – Crie uma imagem Floating

Para manipular a saída, crie uma estrutura temporária para conter os metadados da imagem de saída.

Defina um struct chamado FloatingImage para conter a width, height e a data da imagem, bem como o name do arquivo de saída:

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

Em seguida, implemente uma nova função para FloatingImage que recebe valores para width, height e name da imagem de saída:

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 você ainda não criou os dados para a imagem, crie um buffer na forma de um Vec de u8s com capacidade de 3.655.744 (956 x 956 x 4). A sintaxe <number>_<number> é a numeração de fácil leitura do Rust que separa o número em grupos ou três dígitos. Use os valores de width e height da variável image_1 para criar uma instância da FloatingImage e use o terceiro argumento armazenado em args para definir o nome da FloatingImage:

EX:

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

Desclare as variaveis de saida como mutáveis para que você possa manipular os campos de dados posteriormente.


Etapa 9 - Criar os dados de imagem combinados

Para processar as imagens, você precisa convertê-las em um vetor de pixels RGBA. Os pixels são armazenados como u8s, pois seus valores estão entre 0 e 255.

A estrutura DynamicImage implementa o método to_rgba8, que retorna um ImageBuffer contendo um Vec, e o ImageBuffer implementa o método into_vec, que retorna o Vec:

EX:

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)
}

Em seguida, as variáveis ​​vec_1 e ​​vec_2 são passadas para a função alternate_pixels que retorna os dados combinados da imagem alternando os conjuntos de pixels RGBA das duas imagens:

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
}

A função set_rgba faz referência a um Vec e retorna o conjunto de pixels RGBA para esse Vec começando e terminando em um determinado índice:

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
}

A sintaxe ..= é a sintaxe de intervalo do Rust que permite que o intervalo inclua o valor final. O símbolo * antes de uma variável é o operador de desreferenciação do Rust, que permite que o valor da variável seja acessado.

Em seguida, atribua o retorno de combine_images à variável Combine_data:

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

Passo 10 – Anexe os dados combinados à imagem Floating

Para definir os dados de Combine_data na imagem de output, um método em FloatingImage é definido para definir o campo de dados de output para o valor de Combine_data.

Até agora, você só implementou funções em structs. Os métodos são definidos de maneira semelhante, mas eles tomam uma instância da estrutura como seu primeiro argumento:

EX:

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"

Como você precisa alterar o valor da instância de FloatingImage, o método set_data recebe uma referência mutável para a instância como seu primeiro argumento.

EX:

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(())
  }
}

O enum precisa ser estendido para incluir a nova variante de unidade BufferTooSmall:

enum ImageDataErrors {
  // ...
  BufferTooSmall,
}

Aviso: O método ainda é chamado apenas com um argumento:

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

O ? sintaxe no final de uma expressão é uma forma abreviada de lidar com o resultado de uma chamada de função. Se a chamada de função retornar um erro, o operador de propagação de erro retornará o erro da chamada de função.


Passo 11 – Gravar a imagem em um arquivo

Por fim, salve a nova imagem em um arquivo. A crate de image tem uma função save_buffer_with_format com a seguinte forma:

EX:

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

Visto que AsRef é implementado para String, você pode usar um argumento do tipo String para o path.

EX:

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(())
}

Passo 12 – Juntando tudo

Segue o código final:

mod args;
use args::Args;
use image::{
    imageops::FilterType::Triangle, io::Reader, DynamicImage, GenericImageView, ImageFormat,
};
use std::{fs::File, io::BufReader};

#[derive(Debug)]
enum ImageDataErrors {
    BufferTooSmall,
    DifferentImageFormats,
    // UnableToReadImageFromPath(std::io::Error),
}

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

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(())
}

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 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(())
    }
}

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)
}

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> {
    // 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
}

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

Construindo o binário

cargo build --release

Criando uma imagem combinada: use as imagens salvas no repo do projeto:

./target/release/combiner image/pro.png image/fcc_glyph.png image/output.png

E aqui está o resultado em image/output.png:

Resultado


Resultado


Conclusão

Com isso, agora você sabe o basico de Rust. Mais ainda tem muita coisa para aprender, mas não se preocupe, você pode continuar lendo o Guia de estudo.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published