Upload e compressão de imagens com NodeJS

Categoria: node

Imagem animada de fundo

Upload e compressão de imagens com NodeJS

Como fazer o upload de imagens, e principalmente: como comprimi-las e tratá-las após recebe-las no servidor.

Flor de lótus Evolve

Fala minha gente! É com muito prazer que escrevo esse artigo tentando resolver um problema que tenho visto nos forums assolar a vida de muitos programadores: como fazer o upload de imagens, e principalmente: como comprimi-las e tratá-las após recebe-las no servidor. Ficou um artigo um pouco longo pois nele desenvolvo um passo-a-passo bem explicado, mas continua que vale a pena!

Para prosseguir, vamos utilizar duas bibliotecas: o Multer, para o upload de imagens, e o Sharp, para processar essas imagens(redimensionar, comprimir, converter…). Ambos são middlewares para utilizar com NodeJS, e nesse artigo, utilizaremos com Express, embora funcionem sem.

Título do artigo com o logo da linguagem à esquerda

Como estas bibliotecas Funcionam

Multer

O Multer funciona da seguinte forma: Além das configurações (item 3), nós apontamos que dados da requisição (os dados recebidos do formulário) são os arquivos que queremos tratar. Logo, na execução dessa rota (geralmente um POST), ele vai interceptar esses campos, tratar esses arquivos, salva-los, e nos retornar na requisição as informações desse armazenamento, se teve ou não sucesso, e se teve, qual as propriedades desse arquivo salvo.

Sharp

O Sharp por sua vez, é uma biblioteca para processar imagens no servidor. Ele não foi feito exatamente pra ser usado com o Multer, mas vamos dar um jeito pra deixar tudo automatizado, e no momento desse upload, se houve sucesso no armazenamento, simplesmente processamos esse arquivo que foi salvo.

Enfim, bora para a parte prática!

1. Instalando os pacotes

Partindo do princípio que já temos o nosso servidor Express basicamente configurado, vamos instalar os nossos pacotes, executando o seguinte comando no terminal:

npm i sharp multer

Agora que temos as bibliotecas disponíveis no projeto, vou criar um arquivo chamado multer.js, nele vamos exportar o nosso middleware, para posteriormente usar nas nossas rotas. Pouca gente faz isso, mas quando possível é interessante, não só para economizar código, mas também concentrar a lógica importante em um lugar só. Esse arquivo vai ter o seguinte esqueleto:

const multer = require("multer")

// Vamos exportar nosso módulo multer, executando com as nossas configurações em um objeto.
module.exports = multer({})

2. Configurando o Multer

Como falei acima, vamos instanciar nosso middleware executando a biblioteca com as nossas próprias configurações. Primeiro, vamos apontar as informações ligadas ao armazenamento, como o destino desses arquivos e como eles irão se chamar. Essas opções vão na propiedade storage do nosso objeto. Também podemos configurar um validador, para aceitar apenas certos tipos de extensões, na nossa propriedade fileFilter.

storage

Como queremos salvar arquivos no disco, vamos executar utilizar a função diskStorage, disponibilizada pela biblioteca, que vai fazer a lógica do armazenamento, basta configurarmos duas propiedades que recebem funções, e executar o callback que vem em seus parâmetros corretamente.

const multer = require("multer")

// Vamos expotar nosso módulo multer, o qual vamos executar passando as nossas configurações em um objeto.
module.exports = multer({
  // Como deve ser feito o armazenamento dos arquivos?
  storage: multer.diskStorage({
    // Qual deve ser o destino deles?
    destination: (req, file, cb) => {
      // Setamos o destino como segundo paramêtro do callback
      cb(null, "./app/public/images")
    },

    // E como devem se chamar?
    filename: (req, file, cb) => {
      // Setamos o nome do arquivo que vai ser salvado no segundo paramêtro
      // Apenas concatenei a data atual com o nome original do arquivo, que a biblioteca nos disponibiliza.
      cb(null, Date.now().toString() + "-" + file.originalname)
    },
  }), // FIM DA CONFIGURAÇÃO DE ARMAZENAMENTO
})

Como você pode ver, a configuração de armazenamento tem duas propriedades: destination e filename, que vão apontar o destino dos arquivos e o nome deles, respectivamente. Elas recebem funções, onde teremos acesso aos parâmetros:

  • req: a requisição que foi feita aquela rota (normal do express);

  • file: algumas informações do arquivo que foi recebido (nome, tipo, etc);

  • cb: callback que executaremos com a resposta(no seu segundo parâmetro).

fileFilter

Essa configuração é a responsável pela validação do arquivo. Se a validação falhar, vamos receber um objeto undefined na requisição do nosso controller, e se o arquivo for aprovado, ele vai ser armazenado e receberemos na requisição um objeto com as informações dele e do armazenamento que foi realizado.

Enfim, para usar essa opção basta indicar a propriedade fileFilter no nosso objeto de opções. Essa propriedade recebe uma função com os mesmos parâmetros que vimos nas propriedades destination e filename.

Lembra que essa função nos da acesso, por meio dos parâmetros, a informações sobre o arquivo que recebemos? Esse objeto tem a propriedade mimetype, que é o tipo(extensão?) do arquivo. Basta testarmos se essa propriedade se encaixa com os formatos que esperamos, e baseado nisso, executamos o callback, passando false para rejeitado, e true para aceito.

Abaixo você encontra a nossa propiedade fileFilter, com comentários indicando o que está acontecendo, e o nosso objeto de opções finalizado.

const multer = require("multer")

// Os objetos e suas funções são automaticamentes executadas pela biblioteca, no momento do Upload.
// Nessas funções, teremos acesso a requisição, a alguns dados do arquivo, e um callback que vamos

// Vamos expotar nosso módulo multer, que vamos executar passando as nossas configurações
module.exports = multer({
  // Como deve ser feito o armazenamento dos arquivos?
  storage: multer.diskStorage({
    // Qual deve ser o destino deles?
    destination: (req, file, cb) => {
      // Setamos o destino como segundo paramêtro do callback
      cb(null, "./app/public/images")
    },

    // E como devem se chamar?
    filename: (req, file, cb) => {
      // Setamos o nome do arquivo que vai ser salvado no segundo paramêtro
      // Apenas concatenei a data atual com o nome original do arquivo, que a biblioteca nos disponibiliza.
      cb(null, Date.now().toString() + "-" + file.originalname)
    },
  }),

  // Como esses arquivos serão filtrados, quais formatos são aceitos/esperados?
  fileFilter: (req, file, cb) => {
    // Procurando o formato do arquivo em um array com formatos aceitos
    // A função vai testar se algum dos formatos aceitos do ARRAY é igual ao formato do arquivo.
    const isAccepted = ["image/png", "image/jpg", "image/jpeg"].find(
      formatoAceito => formatoAceito == file.mimetype
    )

    // O formato do arquivo bateu com algum aceito?
    if (isAccepted) {
      // Executamos o callback com o segundo argumento true (validação aceita)
      return cb(null, true)
    }

    // Se o arquivo não bateu com nenhum aceito, executamos o callback com o segundo valor false (validação falhouo)
    return cb(null, false)
  },
})

Geralmente as pessoas usam uma cadeia de ifs para testar o mimetype, mas eu prefiro essa forma automatizada que pensei, assim é só adicionar no array os formatos que queremos.

Vamos aplicar isso nas nossas rotas?

3. Multer em ação!

Chegou a hora de usar o nosso middleware previamente configurado. Vou usar duas rotas simples, uma que renderiza o formulário com GET e outra que recebe o POST desse formulário. As rotas são apenas para propósitos educativos, então estão bem simples sem padronização MVC, por exemplo.

Nosso GET:

// IMPORTAMOS NOSSO MIDDLEWARE
const multer = require("./app/middleware/multer")

// ROTA PARA GET, RENDERIZAR O FORMULÁRIO
app.get("/nova-imagem", (req, res, next) => {
  res.send(`
        <html>
            <head> 
                <title> Nova imagem </title>
            </head>
            </body>
                <!-- O enctype é de extrema importância! Não funciona sem! -->
                <form action="/nova-imagem"  method="POST" enctype="multipart/form-data">
                    <!-- O NAME do input deve ser exatamente igual ao especificado na rota -->
                    <input type="file" name="image">
                    <button type="submit"> Enviar </button>
                </form>
            </body>
        </html>
    `)
})

Dois detalhes que ressaltei no código, os quais são MUITO importantes, causando a maioria dos erros:

  • O formulário deve ter o atributo enctype=”multipart/form-data”

  • O name da entrada que queremos armazenar deve ser exatamente igual ao que especificamos na nossa rota (você já vai entender).

E O POST:

No nosso POST, nós vamos executar também como um middlewared a rota, o nosso multer, indicando a ele o name do form que queremos tratar. Como queremos apenas um campo, vamos usar o método single, enviando como parâmetro o name do campo que o middleware deve agir. Existem outras formas, como usar mais de um name, que você encontra na documentação do Multer.

Caso você esteja iniciando no Node/Express: em uma rota, nós podemos passar como parâmetros vários middlewares, que são funções/funcionalidades que processam a nossa requisição, sendo executadas na ordem apontada (explicando de maneira hypermega minimalista).

O Multer, que é executado antes do middleware principal, vai filtrar a nossa requisição, procurar pelo campo indicado e tratar ele. A saída desse campo tratado vai ser armazenada na propiedade file da nossa requisição. O restante dos campos, se houver, vai normalmente para o req.body, como se fosse um form qualquer.

Obs: Esse bloco de código está logo embaixo do que mostrei acima, não coloquei um em baixo do outro para não ficar muito grande.

// ROTA PARA POST, TRATAR O FORMULÁRIO
// APLICAMOS O NOSSO MIDDLEWARE IMPORTADO PASSANDO O NAME DO INPUT A SER TRATADO
app.post("/nova-imagem", multer.single("image"), (req, res, next) => {
  // Se houve sucesso no armazenamento
  if (req.file) {
    // Vamos imprimir na tela o objeto com os dados do arquivo armazenado
    return res.send(req.file)
  }

  // Se o objeto req.file for undefined, ou seja, não houve sucesso, vamos imprimir um erro!
  return res.send("Houve erro no upload!")
})

Para testar se o armazenamento ocorreu com sucesso, apenas checamos se essa saída que o middleware multer.single(‘image’) atribuiu ao req.file não é undefined, que é seu valor se não passar no filtro, ou ocorrer algum outro erro. Ao contrário, a saída vai ser um objeto com várias propriedades, incluindo o caminho até o arquivo, por exemplo.

Agora que o nosso upload está funcionando, vamos para o tratamento dessa imagem!

4. Criando nosso middleware de tratamento com Sharp

Para focar o código importante em um único lugar, vamos criar um utilitário próprio, utilizando as bibliotecas fs (já tem por default no node), e Sharp, que nós já instalamos. Para isso vou criar um arquivo chamado file-helper.js, que exporta uma função chamada compressImage, com toda a lógica de compressão, recebendo como parâmetro o objeto que recebemos do Multer com as informações do arquivo, e o tamanho que queremos que ela seja redimensionada.

O esqueleto vai ficar assim:

const fs = require("fs"),
  sharp = require("sharp")

exports.compressImage = (file, size) => {}

Essa função deve redimensionar a imagem recebida, converter para WEBP, e substituir o arquivo antigo pelo tratado. No final, quero esse arquivo em Buffer, para salvar com o módulo fs.* O Sharp tem uma série de possibilidades e métodos, que podemos executar no estilo chain, incluindo salvar diretamente no disco, mas nos meus testes ocorreram vários bugs, então preferi trabalhar com o Buffer em conjunto com o fs.

Continuando nosso código, vamos codar a lógica de processamento até receber o Buffer, abaixo você encontra o código explicado:

const fs = require("fs"),
  sharp = require("sharp")

exports.compressImage = (file, size) => {
  // Pegamos o PATH antigo e fazemos um tratamento com ele, para mudar a extensão do arquivo.
  const newPath = file.path.split(".")[0] + ".webp"

  return sharp(file.path) // Executamos o SHARP na imagem que queremos comprimir
    .resize(size) //Redimensionamos para o tamanho (se não receber esse parâmetro, não redimensiona

    .toFormat("webp") // Forçamos a conversão esse arquivo para webp

    .webp({
      // Comprimimos, setando uma qualidade
      quality: 80,
    })

    .toBuffer() // Transformamos esse arquivo em um Buffer

    .then(data => {
      // Temos o buffer disponível para tratamento
    })
}

Como você deve ter entendido, apenas executei a biblioteca passando o caminho da imagem que quero processar, e fui concatenando com outras funções da biblioteca, para fazer os tratamentos.

Prosseguindo com o armazenamento do Buffer, nós vamos primeiro apagar o arquivo anterior salvo pelo Multer, e depois salvar o novo .webp comprimido. Se tudo ocorreu corretamente, o retorno vai ser o caminho da imagem nova, para podermos usar na nossa rota, caso você tenha que salvar no banco de dados, por exemplo.

Vou utilizar três métodos do file-system(fs), o access, que nesse código serve para testar se o arquivo anterior existe antes de apaga-lo, o unlink, que o apagará, e o writeFile, que vai, a partir do novo caminho e do buffer, armazenar a nova imagem.

exports.compressImage = (file, size) => {
  const newPath = file.path.split(".")[0] + ".webp"

  return sharp(file.path)
    .resize(size)
    .toFormat("webp")
    .webp({
      quality: 80,
    })
    .toBuffer()
    .then(data => {
      // Deletando o arquivo antigo
      // O fs.acess serve para testar se o arquivo realmente existe, evitando bugs
      fs.access(file.path, err => {
        // Um erro significa que a o arquivo não existe, então não tentamos apagar
        if (!err) {
          //Se não houve erros, tentamos apagar
          fs.unlink(file.path, err => {
            // Não quero que erros aqui parem todo o sistema, então só vou imprimir o erro, sem throw.
            if (err) console.log(err)
          })
        }
      })

      //Agora vamos armazenar esse buffer no novo caminho
      fs.writeFile(newPath, data, err => {
        if (err) {
          // Já aqui um erro significa que o upload falhou, então é importante que o usuário saiba.
          throw err
        }
      })

      // Se o código chegou até aqui, deu tudo certo, então vamos retornar o novo caminho
      return newPath
    })
}

Agora que nosso utilitário de compressão está a todo o vapor, basta executarmos na requisição do nosso POST, se o upload foi feito com **sucesso **(de maneira crua: se a propiedade req.file não for undefined).

Vamos então voltar até o nosso POST, importar o nosso utilitário, e executar ele!

//Lembre-se de importar a nossa biblioteca no início do arquivo!
//Ex: const filehelper = require('app/util/file-helper.js');

app.post("/nova-imagem", multer.single("image"), (req, res, next) => {
  // Se houve sucesso no armazenamento
  if (req.file) {
    // Vamos mandar essa imagem para compressão antes de prosseguir
    // Ela vai retornar o a promise com o novo caminho como resultado, então continuamos com o then.
    flehelper
      .compressImage(req.file, 100)
      .then(newPath => {
        // Vamos continuar normalmente, exibindo o novo caminho
        return res.send(
          "Upload e compressão realizados com sucesso! O novo caminho é:" +
            newPath
        )
      })
      .catch(err => console.log(err))
  }

  return res.send("Houve erro no upload!")
})

Tudo pronto! se tudo ocorreu corretamente, temos a nossa imagem recebida pelo post “substituída” pela nova versão comprimida e processada.

Tem várias coisas que podem ser melhoradas no código, como por exemplo salvar o buffer por meio de stream, continuar a promise chain para deixar tudo async, trabalhar melhor com os erros, etc. Vou deixar isso para a individualidade do projeto de vocês,** o céu é o limite**!

Se você chegou até aqui,** parabéns guerreiro**! Espero que esse artigo tenha o ajudado solucionar pelo menos um pouco dos seus problemas com upload e processamento de imagens com nodeJS. E se ajudou mesmo, não esquece de deixar um aplauso, clicado na mãozinha aí em cima, e deixando um feedback nos comentários!

Se o código acima não deu certo no seu projeto, ocorreu algum bug ou você ficou alguma dúvida, vou ficar muito feliz em tentar ajudar nas minhas redes sociais! Chama no probleminha:

written byVitor Régis

Founder & DEV na Evolve. Integralmente, um jovem apaixonado por tecnologia e um espiritualista em busca da evolução e paz interior.

Você também pode gostar...

Notícias em destaque

Quando usar: Gatsby.js & NetlifyCMS

Quando usar: Gatsby.js & NetlifyCMS

O Gatsby para muitos é o futuro da web moderna. Mas quais são os benefícios, como funciona seu ecossistema e quando não usar?

O Guia de Estudos para programadores iniciantes

O Guia de Estudos para programadores iniciantes

Como estudar tudo isso? Como acompanhar tanta informação? Um pouco da minha experiência e dicas.

Upload e compressão de imagens com NodeJS

Upload e compressão de imagens com NodeJS

Como fazer o upload de imagens, e principalmente: como comprimi-las e tratá-las após recebe-las no servidor.

Curva animada
Background de um astronalta na lua com uma flor de lótus alienígena representando a tecnologia inovadora da Evolve Studio

Quer evoluir sua ideia?

E-mail / WhatsApp

Redes Sociais

Logo Evolve Studio

© Evolve Studio 2017 • 2024
Todos os direitos reservados.

Vitor da Evolve StudioGeralmente responde em alguns minutos
Vitor da Evolve Studio

Olá! 👋🏼 Como podemos ajudar você hoje?

17:08