25 de outubro de 2021

Do bit Ao Byte

Embarcados, Linux e programação

Script shell para redimensionar imagem por linha de comando

redimensionar imagem

Já são quase 850 artigos publicados no blog; mais de 40 rascunhos excluídos; artigos que não cabiam mais, eliminados. Durante anos editando artigos, sempre a mesma situação quando uma imagem vem de print screen (muito comum na maioria dos artigos): redimensionamento. Cansei de abrir o Gimp para essa tarefa. Redimensionar imagem por linha de comando é fácil, mas aí o cálculo da redução precisa ser feito previamente porque, para que apareça a miniatura no Facebook, o tamanho mínimo é 200×200 pixels. Deixar a imagem grande faz com que o carregamento seja lento, então resolvi fazer um script rápido para resolver essas questões. Estou escrevendo “apenas” esse artigo para mostrar alguma coisa de shell script para quem não conhece, mas shell script é a “alma” do Linux. Para aprender bem, recomendo o livro do Professor Julio Cezar Neves, “Programação Shell Linux“. Minha primeira aquisição foi em meados dos anos 2000, e o conteúdo me foi útil em diversas situações de administração de sistema, no Brasil, América Latina e Europa. Shell Script é fundamental para quem administra sistemas Linux.

Já adianto que roda em Windows (10), desde que esteja com o WSL habilitado, mas originalmente o fiz no Linux, que é o sistema operacional que uso desde 1997. A vantagem é que serve para desktop, ARM, MIPS ou qualquer outra placa que rode Linux. E sim, funciona em Raspberry (que é da arquitetura ARM).

Instale o ImageMagick

O ImageMagick é um pacote de manipulação e interação para imagens gráficas bastante conceituado. Ele comporta uma série de binários para diversas tarefas, mas destes binários, utilizaremos apenas o convert para redimensionar imagem.

Instale o pacote:

sudo apt-get update 
sudo apt-get install imagemagick

Além do convert, vamos utilizar outra ferramenta do sistema, que é o file. Ele será usado para pegar as informações do arquivo – as dimensões, mais especificamente.

imageMagick

Como pegar as dimensões para redimensionar imagem

Essa imagem acima mostra as informações requisitadas pelo programa file. Vamos precisar definir algumas regras de início:

  • Deveremos filtrar apenas as dimensões X,Y.
  • Essas dimensões devem ter pelo menos 3 casas em ambas as posições, X e Y.
  • Devemos ignorar a informação semelhante, que é a densidade.

Tendo os requisitos, podemos começar a pensar na extração da informação para redimensionar imagem. Para isso, utilizaremos o programa sed.

sed trabalha também com expressões regulares. Se não conhece expressões regulares, teremos um problema aqui, porque não é nada intuitivo, mas vou tentar explicar como formar o comando final para filtrar a informação que queremos.

O que é pipe?

O pipe é o símbolo da barra em pé (“|”). Ele é usado para direcionar a saída de um comando para a entrada de outro. Por exemplo:

pipe

O que faremos é pegar a saída do comando file e repassá-lo para o comando sed. Não tem “uma” maneira de se fazer; existem montes de caminhos, combinações e comandos que podem ser utilizados. O editor de fluxo sed é o meu preferido. Também é possível utilizá-lo de outras maneiras, não é uma solução absoluta o que apresento aqui, e provavelmente também não é a melhor para um trabalho massivo, como dezenas ou centenas de milhares de imagens em um diretório.

Utilizando sed

Para substituição, o sed tem o seguinte formato:

sed "s/VALOR_A_SUBSTITUIR/VALOR_SUBSTITUTO/"

Tem muitas variações para isso, mas basicamente é o que utilizaremos. Como vamos adicionar expressão regular, adicionaremos as flags -re.

Para testar a captura, usamos os valores que queremos. As dimensões estão apresentadas como 283×220. O comando ficará então como:

file mega2560mini-joystick-projeto-vscode.jpg | sed -re 's/.*(283x220),.*/\1/'

O comando file recebeu como parâmetro o arquivo jpg. A saída apresentada na primeira imagem foi direcionada para o pipe, para que o programa sed o manipule. Essa manipulação é na saída do comando, não no arquivo.

O sed usa para substituição. O “.” indica “a partir daqui até…”, então entramos com o valor a ser capturado entre parênteses e novamente utilizamos “.“. Isso significa que ignoramos o que vinha antes e o que vinha depois do que queremos capturar. Então temos \1, que é chamado de “retrovisor”. Ele será substituído pelo que aparecer entre os parênteses. A saída resultará então em algo como:

redimensionar imagem - outro exemplo

Deu certo! Mas só vai funcionar pra essa imagem, porque usamos o valor fixo que está na imagem. Precisamos conseguir pegar o valor sem informar um número. A imagem pode ter 3 ou 4 dígitos, não importa. Devemos conseguir pegar as dimensões sem saber quais são. Nesse caso, teremos que adicionar expressões regulares. Invés de explicar cada conceito e depois formatar a expressão, vou mostrar o resultado e explicar superficialmente. Como citei, o livro é o melhor caminho para aprender – e o Julio usa bastante humor no livro, é uma leitura muito agradável.

A linha final ficou assim (e eventualmente pode precisar de modificações, se houver algum saída diferente demais desse padrão):

file mega2560mini-joystick-projeto-vscode.jpg | sed -re 's/.*([[:digit:]]{3,4}x[[:digit:]]{3,4}),.*/\1/'

O que entrou de novo? Apenas 2 coisinhas.

[[:digit:]] – significa que esperamos um número naquela posição.

{3,4} – significa que ele deve se repetir no mínimo 3 vezes e no máximo 4. Depois vem o x e o primeiro padrão se repete. O resultado da execução ficou assim:

redimensionar imagem - exemplo

Acabamos! Ou não? – Não, não acabamos.

Já conseguimos pegar as dimensões de um arquivo de imagem, mas precisamos ainda redimensionar a imagem. A intenção é passar o nome do arquivo como parâmetro para um script. O script deve avaliar se ambas as dimensões são maiores que 200 pixels, então reduzi-la à 200 px na dimensão que ela for menor, porque se a redução for feita a partir da dimensão maior, a menor ficará com menos de 200 pixels e assim não servirá para ser miniatura no Facebook.

Vamos “configurar” o comando de redimensionamento agora. Isso também dá pra fazer de diversas maneiras. Com o convert tem pelo menos duas maneiras, mas eu prefiro proporcionalizar utilizando porcentagem. Nesse caso, será necessário fazer uma regra de 3 pra descobrir a porcentagem.

Para testar, fiz uma imagem de 400x400px e vou reduzir 25%, deixando a imagem com 300x300px. O primeiro passo é criar um array com X e Y para avaliar qual dos dois eixos tem a maior dimensão. Usaremos o que foi mostrado anteriormente e guardaremos em uma variável. Adicionalmente, usamos parênteses para criar o array:

TARGET=(`file teste.jpg | sed -re 's/.*([[:digit:]]{3,4}x[[:digit:]]{3,4}),.*/\1/;s/x/ /'`)

Nesse comando tem também uma substituição extra, passando uma nova expressão para o comando sed, após ponto-e-vírgula:

;s/x/ /

Se ainda não entendeu, o divisor de campos é a barra (‘/’). A expressão significa: “substitua” (s/) “x” (x/) “por espaço” ( /).

Para acessar os valores do array (que contém agora a dimensão X e Y), podemos fazer esse teste:

echo ${TARGET[0]}

Nesse caso, veremos o valor de X.

Agora precisamos avaliar qual das duas dimensões é a menor. Vamos colocar tudo em formato de script a partir de agora:

#!/bin/bash

#O script deve receber o nome do arquivo como parametro, senao deve sair
[ $# -eq 1 ] || {
    echo "passe o nome do arquivo como parametro. Saindo..."
    exit 0
}

#Garantimos que tem parametro. Não validamos, mas atribuimos o valor à variável:
FILENAME=$1

#Agora criamos o array com as dimensoes da imagem. Se não for imagem, dará erro porque não fizemos validação
TARGET=(`file $FILENAME | sed -re 's/.*([[:digit:]]{3,4}x[[:digit:]]{3,4}),.*/\1/;s/x/ /'`)

#Supomos que a menor dimensão é Y
LOWER_DIM=${TARGET[1]}

#Agora verificamos. Se a menor dimensão for X, reatribuimos o valor da variável LOWER_DIM
[ ${TARGET[0]} -gt ${TARGET[1]} ] && {
    LOWER_DIM=${TARGET[0]}
}
#Agora a ultima etapa...

Bem, se redimensionarmos proporcionalmente em porcentagem, dificilmente a imagem ficará do tamanho pretendido em X e Y, ainda mais se um dos eixos for significativamente maior, mas para imagens que vão dentro de artigos, funciona bem. Nesse caso, adicionamos ao script as seguintes linhas:

#queremos algo aproximado a 200px
PROP=`qalc 200/$LOWER_DIM*100|cut -f1 -d,`
convert -resize $PROP% $FILENAME resized-$FILENAME

Aqui utilizamos também o programa cut, que nesse caso está pegando apenas o valor inteiro, antes da vírgula. Por fim, utilizamos o comando convert -resize, passando a dimensão guardada na variável, o nome de arquivo de entrada e o nome do arquivo de saída. Agora quero mostrar a importância de conhecer os comandos e seus parâmetros.

Mesmo com essa série de tratamentos, não foi possível garantir que a menor dimensão seja suficiente para imagem de destaque de artigo, que deve ser de, no mínimo, 200px. Dá pra elaborar mais e ficar melhor? – Não. Dá pra elaborar “menos” e ficar melhor.

O próprio comando convert tem a opção para garantir que a menor dimensão combine com a pretendida. Nesse caso, podemos eliminar o script e fazer simplesmente isso:

convert -resize 200x200^ arquivo_de_entrada.jpg arquivo_de_saida.jpg

Outra coisa importante é que, além de redimensionar, podemos converter entre diferentes extensões. Se quiséssemos uma imagem .png, bastaria trocar a extensão do arquivo de saída.

redimensionar imagem - informação da imagem

A imagem resultante:

redimensionar imagem - alvo

Por essa razão existem muitos scripts que fazem operações complexas desnecessariamente. Cada interação conta quando se está fazendo scripts para servidores que atuarão em volumes. A mesma coisa para embarcados. Mas se quiséssemos aplicar isso a um diretório inteiro de imagens? Bem, agora é simples. Entre no diretório que contém apenas as imagens e digite isso:

ls|while read line; do convert -resize 200x200^ $line resized-$line;done

Depois liste o diretório e veja a mágica!

redimensionar imagem - diretório inteiro

Redimensionar imagem é uma tarefa básica, simples, pode ser feita graficamente. A intenção de fato era mostrar como fazê-lo por linha de comando e mostrar que podemos facilmente “criar um canhão para matar uma mosca”. Por isso é bom ter conceitos o suficiente para não gastar tempo e recursos de máquina com scripts complexos.

Mais sobre shell script

Na playlist do (ex) curso de Raspberry tem alguns vídeos dedicados a shell script. Recomendo que assista e, se não é inscrito no canal ainda, inscreva-se para ajudar o canal. Ainda é modesto, mas com sua ajuda crescerá tanto quanto o blog!

 

Revisão: Ricardo Amaral de Andrade