Como Criar um Conversor de Imagens em C# com ImageSharp

Quando comecei a publicar meus próprios posts, percebi que preparar as imagens consumia muito tempo. Em vez de continuar dependendo de ferramentas online, resolvi criar minha própria aplicação em C# para converter, redimensionar e comprimir imagens em lote.

Quando comecei a publicar meus prĂłprios posts, percebi que existe uma parte do processo que parece simples, mas pode consumir bastante tempo: preparar as imagens.

Para cada conteĂșdo, normalmente preciso pensar no banner, nas imagens utilizadas dentro do artigo e, principalmente, no card que serĂĄ utilizado para divulgar a publicação.

Como estou começando nesse mercado, estou tentando seguir uma rotina organizada para publicar e divulgar os conteĂșdos nas redes. E foi justamente nesse processo que comecei a perceber um problema recorrente.

Eu precisava encontrar imagens, gerar ou solicitar imagens para os banners, adaptar os arquivos para os tamanhos necessĂĄrios e, muitas vezes, reduzir o tamanho dessas imagens antes de colocĂĄ-las no site.

No começo, utilizei ferramentas que jå estavam disponíveis no computador, como o Paint, além de algumas ferramentas gratuitas encontradas na internet.

O problema é que isso também começou a consumir tempo.

Em alguns serviços online, eu conseguia fazer o processamento gratuitamente, mas quando chegava a hora de baixar o arquivo apareciam exigĂȘncias de cadastro, solicitação de e-mail ou opçÔes de planos pagos.

Depois de passar por esse processo algumas vezes, pensei:

SerĂĄ que eu consigo criar uma ferramenta simples em C# para resolver pelo menos uma parte desse problema?

Foi daĂ­ que surgiu a ideia deste pequeno projeto.

Não criei uma ferramenta profissional de edição de imagens e nem tentei competir com softwares especializados. A ideia foi muito mais simples: criar uma aplicação de console capaz de pegar imagens de uma pasta, converter o formato, redimensionar quando necessårio, comprimir e colocar os arquivos processados em outra pasta.

E o interessante é que esse pequeno projeto também acabou se transformando em um bom exercício de C# e .NET.

O que vamos criar?

A aplicação funciona basicamente com trĂȘs pastas:

  • Origem: onde ficam as imagens que serĂŁo processadas;
  • Destino: onde serĂŁo colocadas as imagens convertidas;
  • Processados: onde os arquivos originais serĂŁo movidos depois do processamento.

Os caminhos não ficam diretamente espalhados pelo código. Eles são definidos no arquivo appsettings.json e carregados pela aplicação.

Isso permite alterar os diretĂłrios sem precisar modificar o cĂłdigo-fonte.

Estrutura do projeto

A estrutura que utilizei Ă© relativamente pequena:

  • Program.cs — entrada da aplicação e interação com o usuĂĄrio;
  • Modelos.cs — modelos utilizados para representar as opçÔes e os resultados;
  • ConversorServico.cs — responsĂĄvel pelo processamento das imagens;
  • appsettings.json — configuração das pastas utilizadas pelo programa.

Essa separação é interessante porque evita colocar toda a lógica dentro do Main. O programa conversa com o usuårio, coleta as informaçÔes e depois entrega essas opçÔes para o serviço responsåvel pelo processamento.

Configurando as pastas no appsettings.json

Começando pela configuração, o projeto utiliza um arquivo chamado appsettings.json:

{
  "Caminhos": {
    "PastaOrigem": "C:\\Users\\usuario\\Documents\\ImagensParaTratar\\origem",
    "PastaDestino": "C:\\Users\\usuario\\Documents\\ImagensParaTratar\\destino",
    "PastaProcessados": "C:\\Users\\usuario\\Documents\\ImagensParaTratar\\Processados"
  }
}

No meu arquivo original, esses caminhos apontam para as pastas que utilizo no meu computador. Aqui no artigo coloquei caminhos genĂ©ricos para que vocĂȘ possa adaptar ao seu ambiente.

A ideia é simples: qualquer imagem colocada na pasta de origem poderå ser processada pela aplicação.

Por que separar a configuração do código?

Essa Ă© uma pequena decisĂŁo que pode parecer desnecessĂĄria em um projeto tĂŁo simples, mas ajuda bastante.

Imagine que amanhĂŁ vocĂȘ queira colocar a pasta de origem em outro local. Se o caminho estiver espalhado pelo cĂłdigo, serĂĄ necessĂĄrio procurar e alterar os valores.

Com o appsettings.json, basta alterar a configuração.

No Program.cs, a aplicação carrega o arquivo utilizando o sistema de configuração do .NET:

IConfiguration config = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile(
        "appsettings.json",
        optional: false,
        reloadOnChange: true)
    .Build();

Aqui temos algumas coisas acontecendo.

ConfigurationBuilder

O ConfigurationBuilder é utilizado para construir o objeto de configuração que serå utilizado pela aplicação.

SetBasePath

O mĂ©todo SetBasePath define o diretĂłrio utilizado como referĂȘncia para localizar o arquivo de configuração.

No projeto, utilizo:

Directory.GetCurrentDirectory()

Assim, o programa procura o appsettings.json a partir do diretório atual da aplicação.

AddJsonFile

Depois informamos qual arquivo serĂĄ carregado:

.AddJsonFile(
    "appsettings.json",
    optional: false,
    reloadOnChange: true)

O parùmetro optional: false indica que o arquivo é obrigatório para a aplicação.

Depois que a configuração é carregada, podemos buscar os valores das pastas:

OpcoesConversao opcoes = new OpcoesConversao
{
    PastaOrigem =
        config["Caminhos:PastaOrigem"] ?? string.Empty,

    PastaDestino =
        config["Caminhos:PastaDestino"] ?? string.Empty,

    PastaProcessados =
        config["Caminhos:PastaProcessados"] ?? string.Empty
};

Perceba a sintaxe:

Caminhos:PastaOrigem

Ela representa a propriedade PastaOrigem dentro da seção Caminhos do JSON.

O modelo de opçÔes da conversão

Para evitar passar vårias informaçÔes soltas para o serviço, criei uma classe chamada OpcoesConversao.

Ela fica no arquivo Modelos.cs.

public class OpcoesConversao
{
    public string PastaOrigem { get; set; } = string.Empty;
    public string PastaDestino { get; set; } = string.Empty;
    public string PastaProcessados { get; set; } = string.Empty;

    public FormatoImagem FormatoSaida { get; set; }
        = FormatoImagem.Webp;

    public int? LarguraPx { get; set; }

    public int? AlturaPx { get; set; }

    public bool Comprimir { get; set; }

    public int Qualidade => Comprimir ? 75 : 100;
}

Essa classe representa as configuraçÔes necessårias para realizar uma conversão.

PastaOrigem, PastaDestino e PastaProcessados

Essas propriedades informam para onde a aplicação deve olhar e onde deve colocar os arquivos.

public string PastaOrigem { get; set; }

public string PastaDestino { get; set; }

public string PastaProcessados { get; set; }

A terceira propriedade Ă© particularmente interessante porque permite que o programa nĂŁo simplesmente apague ou deixe o arquivo original misturado com os arquivos que ainda precisam ser processados.

FormatoSaida

O formato de saĂ­da Ă© representado por um enum:

public enum FormatoImagem
{
    Jpeg,
    Webp,
    Png
}

Portanto, a versĂŁo atual do projeto trabalha com trĂȘs formatos de saĂ­da: JPEG, WebP e PNG.

Largura e altura

As dimensÔes são representadas por int?:

public int? LarguraPx { get; set; }

public int? AlturaPx { get; set; }

O ponto interessante aqui Ă© o ?.

Isso significa que o valor pode ser nulo.

Na prĂĄtica, isso permite que o usuĂĄrio pressione ENTER e mantenha aquela dimensĂŁo sem informar um valor.

Essa decisĂŁo deixa o programa mais flexĂ­vel do que simplesmente exigir largura e altura obrigatoriamente.

CompressĂŁo e qualidade

Também temos:

public bool Comprimir { get; set; }

public int Qualidade => Comprimir ? 75 : 100;

A propriedade Qualidade Ă© calculada automaticamente.

Quando a compressão estå ativada, a qualidade utilizada é 75. Quando não estå, o valor é 100. Isso estå definido diretamente no modelo utilizado pela aplicação.

É uma solução simples e adequada para o objetivo inicial do projeto. Uma futura versão poderia permitir que o usuário escolhesse manualmente a qualidade.

Representando o resultado do processamento

Outra classe criada no projeto Ă© ResultadoItem:

public class ResultadoItem
{
    public string NomeArquivo { get; set; }
        = string.Empty;

    public bool Sucesso { get; set; }

    public string MensagemErro { get; set; }
        = string.Empty;

    public long TamanhoOriginalBytes { get; set; }

    public long TamanhoFinalBytes { get; set; }
}

Essa classe resolve uma necessidade importante: saber o que aconteceu com cada arquivo.

Além de informar se houve sucesso ou erro, ela guarda os tamanhos original e final do arquivo.

Com isso, o programa consegue mostrar algo como:

[OK] minha-imagem.png | 850.5 KB -> 245.3 KB (71.2% alteração)

Ou, se ocorrer algum problema:

[ERRO] minha-imagem.png: mensagem do erro

Entrando no Program.cs

Agora chegamos ao ponto de entrada da aplicação.

O método Main começa carregando a configuração:

static void Main(string[] args)
{
    IConfiguration config = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile(
            "appsettings.json",
            optional: false,
            reloadOnChange: true)
        .Build();

Depois disso, as opçÔes iniciais são montadas:

OpcoesConversao opcoes = new OpcoesConversao
{
    PastaOrigem =
        config["Caminhos:PastaOrigem"] ?? string.Empty,

    PastaDestino =
        config["Caminhos:PastaDestino"] ?? string.Empty,

    PastaProcessados =
        config["Caminhos:PastaProcessados"] ?? string.Empty
};

O programa também exibe no console quais diretórios serão utilizados:

Console.WriteLine("=============================================");
Console.WriteLine("   CONVERSOR & COMPRESSOR DE IMAGENS C#     ");
Console.WriteLine("=============================================");

Console.WriteLine("Origem     : " + opcoes.PastaOrigem);
Console.WriteLine("Destino    : " + opcoes.PastaDestino);
Console.WriteLine("Processados: " + opcoes.PastaProcessados);

Isso Ă© Ăștil principalmente durante os testes, porque permite confirmar rapidamente se a aplicação estĂĄ apontando para as pastas corretas.

Validando a pasta de origem

Antes de pedir qualquer informação ao usuårio, o programa verifica se a pasta de origem realmente existe:

if (!Directory.Exists(opcoes.PastaOrigem))
{
    Console.ForegroundColor = ConsoleColor.Red;

    Console.WriteLine(
        "A pasta de origem informada no appsettings.json nĂŁo existe!");

    Console.ResetColor();

    Console.ReadKey();

    return;
}

Essa validação evita que o programa chegue à etapa de processamento tentando acessar um diretório inexistente.

Também temos um pequeno cuidado visual: o console muda a cor do texto para vermelho quando apresenta o erro e depois volta ao padrão.

Escolhendo o formato de saĂ­da

Depois da validação, o programa pergunta ao usuårio qual formato deseja utilizar:

Console.Write(
    "Formato de saĂ­da (png, webp ou jpeg): ");

string entradaFormato =
    (Console.ReadLine() ?? "")
        .Trim()
        .ToLower();

Depois a entrada Ă© convertida para o enum:

if (entradaFormato == "png")
{
    opcoes.FormatoSaida = FormatoImagem.Png;
}
else if (
    entradaFormato == "jpeg" ||
    entradaFormato == "jpg")
{
    opcoes.FormatoSaida = FormatoImagem.Jpeg;
}
else
{
    opcoes.FormatoSaida = FormatoImagem.Webp;
}

Temos aqui uma decisĂŁo simples: se o usuĂĄrio informar PNG, utilizamos PNG; se informar JPEG ou JPG, utilizamos JPEG; qualquer outra entrada acaba levando ao WebP.

Em uma futura versão, eu provavelmente melhoraria essa validação para avisar quando o usuårio informar um formato invålido, em vez de assumir WebP.

Escolhendo se a imagem serĂĄ comprimida

Em seguida, o programa pergunta:

Console.Write(
    "Deseja comprimir as imagens? (S/N): ");

string entradaComprimir =
    (Console.ReadLine() ?? "")
        .Trim()
        .ToUpper();

opcoes.Comprimir =
    (entradaComprimir == "S");

O funcionamento Ă© direto: se o usuĂĄrio digitar S, a propriedade Comprimir recebe true.

Essa informação posteriormente serå utilizada pelo serviço para escolher como a imagem serå codificada.

Definindo a largura da imagem

Depois vem a largura:

Console.Write(
    "Informe a LARGURA em Px " +
    "(ou aperte ENTER para tamanho original): ");

string inputLargura =
    Console.ReadLine() ?? "";

if (int.TryParse(inputLargura, out int larg)
    && larg > 0)
{
    opcoes.LarguraPx = larg;
}

O mĂ©todo int.TryParse Ă© utilizado para tentar transformar o texto digitado em um nĂșmero inteiro.

Isso é melhor do que simplesmente utilizar int.Parse, porque uma entrada que não seja numérica não provoca imediatamente uma exceção.

AlĂ©m disso, o cĂłdigo verifica se o nĂșmero Ă© maior que zero.

Se o usuĂĄrio simplesmente pressionar ENTER, a propriedade continua nula.

Definindo a altura

A altura segue praticamente a mesma lĂłgica:

Console.Write(
    "Informe a ALTURA em Px " +
    "(ou aperte ENTER para tamanho original): ");

string inputAltura =
    Console.ReadLine() ?? "";

if (int.TryParse(inputAltura, out int alt)
    && alt > 0)
{
    opcoes.AlturaPx = alt;
}

Com isso, a aplicação jå possui todas as informaçÔes necessårias para iniciar o processamento.

Onde a conversĂŁo realmente acontece?

Aqui estĂĄ uma decisĂŁo importante na estrutura do projeto.

O Program.cs nĂŁo faz diretamente o processamento das imagens.

Ele cria uma instĂąncia de ConversorServico:

ConversorServico servico =
    new ConversorServico();

var resultados =
    servico.ProcessarTudo(opcoes);

Isso deixa o Program responsåvel pela interação com o usuårio, enquanto a classe ConversorServico fica responsåvel pelo trabalho pesado.

É uma separação simples, mas que facilita bastante a evolução do projeto.

Conhecendo o ConversorServico

A classe começa importando os componentes necessårios do ImageSharp:

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Processing;

Também utilizamos classes do próprio .NET para trabalhar com arquivos, diretórios e listas.

O método principal é:

public List ProcessarTudo(
    OpcoesConversao opcoes)

Ele recebe todas as opçÔes que foram definidas no Program.cs e retorna uma lista de resultados.

Criando as pastas automaticamente

Uma das primeiras coisas feitas pelo serviço é garantir que as pastas de destino existam:

if (!Directory.Exists(opcoes.PastaDestino))
{
    Directory.CreateDirectory(
        opcoes.PastaDestino);
}

O mesmo acontece com a pasta de processados:

if (!string.IsNullOrEmpty(opcoes.PastaProcessados)
    && !Directory.Exists(
        opcoes.PastaProcessados))
{
    Directory.CreateDirectory(
        opcoes.PastaProcessados);
}

Assim, o programa não depende de o usuårio criar manualmente todas as pastas antes de executar a aplicação.

Quais arquivos serĂŁo processados?

O programa define quais extensÔes são aceitas:

string[] extensoesValidas =
{
    ".jpg",
    ".jpeg",
    ".png",
    ".webp",
    ".bmp"
};

Depois procura os arquivos existentes na pasta de origem:

List arquivos =
    Directory.GetFiles(opcoes.PastaOrigem)
        .Where(f =>
            extensoesValidas.Contains(
                Path.GetExtension(f).ToLower()))
        .ToList();

Esse trecho Ă© interessante porque evita que qualquer arquivo encontrado na pasta seja tratado como imagem.

Por exemplo, um arquivo .txt colocado acidentalmente no diretĂłrio nĂŁo entra na lista de processamento.

Atualmente, a lista de entrada aceita JPG, JPEG, PNG, WebP e BMP.

Gerando um identificador de data e hora

O programa também cria um sufixo baseado na data e hora:

string sufixoDataHora =
    DateTime.Now.ToString(
        "_yyyyMMdd_HHmm");

Esse valor serĂĄ utilizado nos nomes dos arquivos.

Por exemplo, uma imagem chamada:

banner.png

poderĂĄ gerar algo semelhante a:

banner_20260831_1534.webp

Isso ajuda a evitar conflitos quando existem arquivos com o mesmo nome.

Processando cada imagem

O processamento acontece dentro de um foreach:

foreach (string caminho in arquivos)
{
    FileInfo info =
        new FileInfo(caminho);

    ResultadoItem item =
        new ResultadoItem
        {
            NomeArquivo = info.Name,
            TamanhoOriginalBytes = info.Length
        };

    // processamento...
}

Antes de processar o arquivo, o programa jĂĄ registra seu nome e seu tamanho original.

Isso serĂĄ importante posteriormente para apresentar ao usuĂĄrio o resultado da conversĂŁo.

Definindo o nome do arquivo de saĂ­da

O cĂłdigo pega o nome original sem a extensĂŁo:

string nomeSemExtensao =
    Path.GetFileNameWithoutExtension(
        info.Name);

Depois descobre qual serĂĄ a extensĂŁo do formato escolhido:

string extensao =
    ObterExtensao(opcoes.FormatoSaida);

Finalmente, cria o novo nome:

string novoNomeArquivo =
    $"{nomeSemExtensao}" +
    $"{sufixoDataHora}" +
    $"{extensao}";

E monta o caminho completo:

string caminhoDestino =
    Path.Combine(
        opcoes.PastaDestino,
        novoNomeArquivo);

O método ObterExtensao retorna .jpg, .png ou .webp de acordo com o formato escolhido.

Carregando a imagem com ImageSharp

Agora chegamos ao processamento propriamente dito:

using (Image imagem =
    Image.Load(caminho))
{
    // processamento
}

O Image.Load carrega a imagem para que possamos trabalhar com ela em memĂłria.

O using garante que o objeto seja descartado ao final do processamento daquele arquivo.

Redimensionando a imagem

O redimensionamento Ă© opcional.

O cĂłdigo verifica:

if (opcoes.LarguraPx.HasValue ||
    opcoes.AlturaPx.HasValue)
{
    // redimensionamento
}

Isso significa que basta o usuĂĄrio informar largura ou altura para o processo de redimensionamento ser executado.

As opçÔes são montadas assim:

ResizeOptions resizeOptions =
    new ResizeOptions
    {
        Size = new Size(
            opcoes.LarguraPx ?? 0,
            opcoes.AlturaPx ?? 0),

        Mode = ResizeMode.Max
    };

Depois:

imagem.Mutate(x =>
    x.Resize(resizeOptions));

Um detalhe importante aqui é o uso de ResizeMode.Max. A intenção é trabalhar com as dimensÔes informadas sem simplesmente distorcer a imagem para preencher obrigatoriamente os dois valores.

Esse trecho Ă© o responsĂĄvel pelo redimensionamento da imagem no projeto real.

Escolhendo o encoder

Depois do redimensionamento, precisamos informar ao ImageSharp como a imagem deverĂĄ ser salva.

Para isso, o projeto utiliza o método:

IImageEncoder encoder =
    ObterEncoder(
        opcoes.FormatoSaida,
        opcoes.Qualidade,
        opcoes.Comprimir);

Esse método centraliza a escolha do encoder.

Encoder JPEG

Para JPEG, temos:

case FormatoImagem.Jpeg:
    return new JpegEncoder
    {
        Quality = qualidade
    };

A qualidade recebida pelo encoder é aquela definida nas opçÔes da conversão.

Encoder PNG

Para PNG, o comportamento Ă© um pouco diferente:

case FormatoImagem.Png:
    return new PngEncoder
    {
        CompressionLevel =
            comprimir
                ? PngCompressionLevel.BestCompression
                : PngCompressionLevel.NoCompression
    };

Quando a opção de compressão estå ativada, o código utiliza BestCompression. Caso contrårio, utiliza NoCompression.

Isso mostra uma diferença interessante entre os formatos: o conceito de "qualidade" e o mecanismo de compressão não precisam ser tratados exatamente da mesma maneira para todos os tipos de imagem.

Encoder WebP

No WebP, o projeto utiliza:

case FormatoImagem.Webp:
default:
    return new WebpEncoder
    {
        Quality = qualidade,

        FileFormat =
            comprimir
                ? WebpFileFormatType.Lossy
                : WebpFileFormatType.Lossless
    };

Aqui temos uma decisĂŁo particularmente interessante.

Quando a compressĂŁo estĂĄ ativada, o arquivo Ă© salvo como Lossy. Quando nĂŁo estĂĄ, o projeto utiliza Lossless.

Assim, a opção escolhida pelo usuårio interfere diretamente na forma como o WebP serå gerado.

Salvando a imagem

Depois que o encoder foi definido, finalmente salvamos o arquivo:

imagem.Save(
    caminhoDestino,
    encoder);

É neste ponto que a imagem processada efetivamente Ă© gravada na pasta de destino.

O fluxo completo dentro do serviço é, portanto:

  1. localizar a imagem;
  2. carregar a imagem;
  3. redimensionar se necessĂĄrio;
  4. escolher o encoder;
  5. salvar no formato escolhido;
  6. registrar o tamanho final;
  7. mover o arquivo original para a pasta de processados.

Essa Ă© basicamente a lĂłgica central do conversor.

Medindo o tamanho final

Depois que a imagem foi salva, o cĂłdigo cria um novo FileInfo:

FileInfo infoDestino =
    new FileInfo(caminhoDestino);

item.TamanhoFinalBytes =
    infoDestino.Length;

item.Sucesso = true;

Agora temos os dois valores necessĂĄrios:

  • tamanho original;
  • tamanho final.

Isso permite apresentar ao usuårio uma comparação do resultado.

Movendo o arquivo original

Depois de gerar a nova imagem, o arquivo original Ă© movido para a pasta de processados:

if (!string.IsNullOrEmpty(
        opcoes.PastaProcessados)
    && File.Exists(caminho))
{
    string nomeOriginalComData =
        $"{nomeSemExtensao}" +
        $"{sufixoDataHora}" +
        $"{info.Extension}";

    string caminhoProcessado =
        Path.Combine(
            opcoes.PastaProcessados,
            nomeOriginalComData);

    File.Move(
        caminho,
        caminhoProcessado);
}

Esse comportamento Ă© importante para o funcionamento em lote.

Depois que uma imagem foi processada, ela deixa de estar na pasta de origem.

Assim, se eu executar o programa novamente, as imagens que jĂĄ foram processadas nĂŁo ficam novamente na fila.

O código real utiliza exatamente essa estratégia de mover o arquivo original para a pasta configurada como PastaProcessados.

E se acontecer um erro?

Como estamos trabalhando com arquivos, imagens e diretĂłrios, erros podem acontecer.

Por isso, o processamento de cada imagem estĂĄ protegido por try/catch:

catch (Exception ex)
{
    item.Sucesso = false;
    item.MensagemErro = ex.Message;
}

Em vez de interromper necessariamente todo o processamento ao encontrar um problema, o erro Ă© registrado no resultado daquele arquivo.

Isso Ă© bastante Ăștil quando temos vĂĄrias imagens para processar.

Imagine, por exemplo, que existam 50 arquivos na pasta e apenas um deles apresente algum problema. O modelo de resultado permite identificar qual arquivo falhou.

A aplicação registra a mensagem de exceção em MensagemErro.

Mostrando o resultado no console

Depois que o serviço termina o processamento, o Program.cs percorre os resultados:

foreach (var item in resultados)
{
    if (item.Sucesso)
    {
        // resultado positivo
    }
    else
    {
        // erro
    }
}

Para arquivos processados com sucesso, o programa converte os tamanhos para KB:

double origKb =
    item.TamanhoOriginalBytes / 1024.0;

double finalKb =
    item.TamanhoFinalBytes / 1024.0;

Depois calcula a porcentagem de alteração:

double reducao =
    100.0 -
    ((double)item.TamanhoFinalBytes /
     item.TamanhoOriginalBytes * 100.0);

E finalmente mostra o resultado:

Console.WriteLine(
    $"[OK] {item.NomeArquivo} | " +
    $"{origKb:F1} KB -> " +
    $"{finalKb:F1} KB " +
    $"({reducao:F1}% alteração)");

O prĂłprio programa real utiliza esses dados para mostrar no console o tamanho antes e depois do processamento.

Um exemplo do funcionamento

Imagine que eu coloque na pasta Origem uma imagem chamada:

banner.png

Ao iniciar o programa, ele pergunta:

Formato de saĂ­da (png, webp ou jpeg): webp

Deseja comprimir as imagens? (S/N): S

Informe a LARGURA em Px
(ou aperte ENTER para tamanho original): 1200

Informe a ALTURA em Px
(ou aperte ENTER para tamanho original): 800

O programa então passa essas informaçÔes para o serviço.

A imagem serå carregada, redimensionada conforme as opçÔes, convertida para WebP e salva na pasta de destino.

O arquivo original serĂĄ movido para a pasta de processados.

Ao final, o console apresenta o resultado do processamento.

Por que eu preferi começar com uma aplicação de console?

Uma das coisas que considero interessantes nesse projeto Ă© justamente sua simplicidade.

Eu poderia ter começado tentando criar uma aplicação desktop completa, com botÔes, campos, seleção de arquivos e uma interface gråfica.

Mas isso aumentaria bastante a quantidade de coisas que precisariam ser resolvidas.

Como o objetivo inicial era descobrir se eu conseguia resolver o problema com C#, uma aplicação de console jå era suficiente.

Primeiro eu precisava provar a ideia.

Depois poderia pensar na interface.

Essa abordagem também é interessante para quem estå estudando programação: muitas vezes não precisamos começar pelo projeto mais sofisticado possível.

Podemos começar pequeno, fazer funcionar e evoluir.

O que este projeto ensina sobre C#?

Apesar de ser uma aplicação pequena, ela reĂșne vĂĄrios conceitos interessantes.

  • leitura de arquivos JSON;
  • configuração externa da aplicação;
  • classes e propriedades;
  • enum;
  • nullable types;
  • listas;
  • LINQ;
  • manipulação de arquivos;
  • manipulação de diretĂłrios;
  • tratamento de exceçÔes;
  • processamento em lote;
  • uso de bibliotecas externas;
  • separação de responsabilidades;
  • processamento e conversĂŁo de imagens.

Por isso, mesmo sendo um projeto pequeno, ele pode ser um exercĂ­cio interessante para quem estĂĄ aprendendo C#.

Uma coisa que o projeto ainda nĂŁo faz

Existe uma diferença importante entre a necessidade que motivou o projeto e a versão atual do código.

Uma das dificuldades que me levou a pensar nessa ferramenta foi justamente a necessidade de trabalhar com recortes de imagens.

Porém, a versão do código apresentada neste artigo ainda não implementa um recurso de recorte manual da imagem.

O que jĂĄ estĂĄ implementado Ă© o redimensionamento, conversĂŁo e compressĂŁo.

E isso Ă© propositalmente interessante de mostrar.

NÃO precisamos fingir que o projeto estå pronto ou que possui todos os recursos imaginados inicialmente. Ele resolveu uma parte do problema e ainda pode evoluir.

O que eu faria na prĂłxima versĂŁo?

Se eu continuasse evoluindo esse projeto, o próximo passo provavelmente seria justamente transformar essa aplicação de console em uma ferramenta mais amigåvel.

Algumas possibilidades seriam:

  • criar uma interface desktop;
  • adicionar seleção visual de arquivos;
  • permitir arrastar e soltar imagens;
  • criar um recurso visual de recorte;
  • permitir processar imagens individualmente;
  • criar processamento em lote mais configurĂĄvel;
  • permitir escolher manualmente a qualidade;
  • mostrar uma prĂ©via da imagem;
  • mostrar o tamanho original e o final;
  • criar presets para banners e cards;
  • criar uma versĂŁo web.

Uma ideia interessante: criar presets para o blog

Como esse projeto nasceu justamente da necessidade de preparar imagens para publicação, uma evolução que faria bastante sentido seria criar configuraçÔes prontas.

Por exemplo:

1 - Card do artigo
2 - Banner
3 - Imagem interna
4 - Imagem otimizada para Web
5 - Personalizado

Ao escolher Card do artigo, a aplicação poderia automaticamente aplicar as dimensÔes e o formato definidos para esse tipo de imagem.

Assim, eu não precisaria lembrar das configuraçÔes todas as vezes que fosse preparar uma nova imagem.

É exatamente esse tipo de melhoria que pode transformar um pequeno script criado para resolver uma necessidade pessoal em uma ferramenta realmente Ăștil.

E se transformåssemos isso em uma aplicação web?

Outra evolução possível seria transformar o projeto em uma aplicação web.

O usuårio poderia acessar uma pågina, enviar uma ou vårias imagens, escolher o formato, definir as dimensÔes e selecionar o nível de compressão.

O servidor faria o processamento e disponibilizaria os arquivos convertidos.

Outra possibilidade seria transformar a lĂłgica existente em uma API, permitindo que outros sistemas enviem imagens para processamento.

Ou seja, a aplicação de console atual poderia ser encarada como o primeiro estågio de um projeto muito maior.

Vale a pena criar sua prĂłpria ferramenta?

No meu caso, valeu como experiĂȘncia.

Eu estava enfrentando um problema real e repetitivo. Em vez de continuar procurando uma ferramenta gratuita na internet toda vez que precisava preparar uma imagem, resolvi testar se conseguiria criar algo prĂłprio.

O resultado não é uma ferramenta profissional de edição de imagens.

E nem precisa ser.

Ela resolve algumas tarefas especĂ­ficas que eu precisava realizar e, principalmente, me deu uma base para continuar evoluindo.

Esse é um dos aspectos da programação que mais gosto: uma necessidade pequena pode ser o começo de um projeto interessante.

ConclusĂŁo

Criar esse conversor de imagens em C# começou como uma tentativa de resolver uma dificuldade que encontrei durante a produção dos meus prĂłprios conteĂșdos.

Eu precisava lidar constantemente com imagens, encontrar ferramentas para redimensionĂĄ-las, convertĂȘ-las e reduzir seu tamanho. Em vez de depender sempre de serviços online, resolvi experimentar uma solução prĂłpria.

A aplicação atual Ă© simples, mas jĂĄ consegue realizar um fluxo bastante Ăștil: encontrar imagens em uma pasta, redimensionĂĄ-las quando necessĂĄrio, convertĂȘ-las para JPG, PNG ou WebP, aplicar as opçÔes de compressĂŁo, salvar o resultado e mover os arquivos originais para uma pasta de processados.

Além disso, o projeto mostra como podemos separar responsabilidades em uma aplicação pequena: o Program.cs cuida da interação com o usuårio, os modelos armazenam as opçÔes e resultados e o ConversorServico concentra a lógica de processamento.

Para mim, esse é justamente o valor desse tipo de projeto. Não é necessårio criar uma aplicação gigantesca para aprender alguma coisa ou resolver um problema real.

Às vezes, basta olhar para uma tarefa repetitiva e perguntar:

"SerĂĄ que eu consigo automatizar isso?"

Foi exatamente essa pergunta que deu origem ao projeto.

E vocĂȘ, jĂĄ criou uma ferramenta para resolver um problema do seu dia a dia? Conte nos comentĂĄrios. Pode ser justamente uma dessas pequenas automaçÔes que acabam virando um projeto muito maior.

Desafio para vocĂȘ

Se vocĂȘ gostou da ideia, tente pegar esse projeto e evoluĂ­-lo. Que tal transformar o console em uma aplicação desktop? Ou adicionar um sistema visual de recorte? Outra possibilidade Ă© criar uma versĂŁo web para processar imagens diretamente pelo navegador.

Quer mais projetos prĂĄticos de C#?

No Nerd Cult, compartilho conhecimento prĂĄtico baseado em projetos reais. Assine nossa newsletter e receba conteĂșdos sobre desenvolvimento, carreira e cultura geek.

Quer colaborar com o Nerd Cult?

Se vocĂȘ tambĂ©m criou uma ferramenta, automação ou projeto interessante para resolver um problema real, entre em contato com o Nerd Cult. Projetos simples podem render Ăłtimos conteĂșdos quando existe uma histĂłria real por trĂĄs deles.

Nerd Cult — onde o cĂłdigo encontra o rock, o cinema e a cultura geek. Porque ser nerd Ă© transformar o medo em curiosidade, e a curiosidade em poder.

#CSharp #ImageSharp #Programacao #ProjetosPraticos #NerdCult

Este conteĂșdo foi Ăștil para vocĂȘ?

ComentĂĄrios (0)

Nenhum comentĂĄrio ainda. Seja o primeiro a comentar!