Grails e ImageMagick, poderio em manipulação de imagens

Recentemente tive que lidar, mais uma vez, com manipulação de imagens numa aplicação web. O requsito era o clássico: usuário faz upload de uma imagem que a gente não sabe as dimensões, e a aplicação precisa criar um thumbnail (aquela versão bem reduzida da imagem para apresentar numa página junto com diversos outros elementos de página), e também um versão da imagem em tamanho grande pré-determinado.

Mas desta vez havia requisitos novos:
1) Era preciso adicionar uma marca d'água à foto grande, e;
2) Deveria ser possível fazer um upload de um arquivo zip com dezenas de fotos dentro, e a aplicação precisava processar todas as imagens do zip, criando o tumbnail, a foto grande e a marca d'água.

Uma das opções que surgiram no meio do caminho apareceu quando surgiu este assunto na lista do Grails e alguém fez com que eu conhecesse o ImageMagick. Esta biblioteca é uma biblioteca nativa de processamento de imagens...como se fosse um photoshop, porém sem interface gráfica, apenas por linha de comando. E o mais interessante é que ela realmente possui diversas funcionalidades de um photoshop, como filtros, transformações, conversões, ou seja, realmente edição de imagem. Vale a pena conhecer. Veja alguns exemplos aqui. Para exemplos de uso da API, veja aqui.

Ok....então passamos a conhecer o ImageMagic (IM). Ah, antes que eu esqueça, existem várias intefaces de acesso ao IM para várias linguagens. Há inclusive o JMagick, que é a API java para o IM. Porém, pesquisando a respeito no google a gente vê que em diversas situações o JMagick crasha (crash). Então, se a gente não pode ainda, ou não deve, usar o JMagick, a saída é usar o IM nativamente ( que deve ser o que o JMagick faz, mas não sabemos o que fizeram lá para ele entrar em crash né).

Portanto, finalmente, este post tem então o objetivo de mostar uma action de um controller grails que eu fiz num micro projetinho grails só pra testar o uso do IM usando groovy e executando o IM nativamente, seguindo as dicas do nosso amigo James Page da lista do grails.

O primeiro passo é instalar o ImageMagick na sua plataforma. Em windows há o instalador através de um arquivo .exe. No linux há o RPM. E é claro, você pode instalar a partir do fonte do ImageMagick também.

Depois de instalar, você pode testar facilmente pela linha de comando. Se funcionou, vamos para o mundo web com nosso grails.

Eu criei um projeto novo só pra testar. Para isso:

grails create-app


Chamei este projeto de "img". Após criar, vá para o diretório do projeto e

grails create-domain-class


Chamei esta classe de domínio de TaskFile (só porque em outro projeto eu tenho uma classe Task que possui diversos TaskFile).

class TaskFile { 
String name
Date uploaded = new Date()
String description
}



Em seguida gerei o controlador TaskFileController (com comando "grails generate-controller").

import org.springframework.web.multipart.MultipartFile;

class TaskFileController {
def index = { redirect(action:list,params:params) }
// .... todas as actions que o grails gera. Alterei a action save
// alem de alterar, criei metodos privados
def save = {
def taskFile = new TaskFile()
taskFile.properties = params

if(taskFile.save()) {
def f = request.getFile('taskFile')
if(!f.empty) {
def fileName = 'file_' + taskFile.id + getExtension(f)
f.transferTo( new File('imgs/' + fileName) )
processImg(fileName)
}
redirect(action:show,id:taskFile.id)
}
else {
render(view:'create',model:[taskFile:taskFile])
}
}

private void processImg(fileName){
final String original = 'imgs/' + fileName
def t = Thread.start {
def cmd = createCmd(original, '-thumbnail', createDimentions(100,0) , 'imgs/tumb_' + fileName)
def process = cmd.execute()
process.waitFor()

def cmdMain = createCmd(original, '-thumbnail', createDimentions(400,0) , 'imgs/main_' + fileName)
def processMain = cmdMain.execute()
processMain.waitFor()

def waterMarkCmd = ["cmd /c composite -compose atop watermark.png", 'imgs/main_' + fileName, 'imgs/wm_' + fileName]
waterMarkCmd.join(" ").execute()
}
}

private String getExtension(MultipartFile file){
String contentType = file.getContentType();
String fileExtension = null;
if (contentType.equalsIgnoreCase("image/pjpeg") || contentType.equalsIgnoreCase("image/jpeg")) {
fileExtension = ".jpg";
}
else if (contentType.equalsIgnoreCase("image/gif")) {
fileExtension = ".gif";
}
else if (contentType.equalsIgnoreCase("image/x-png")) {
fileExtension = ".png";
}

return fileExtension;
}




String createCmd(inpath, action, options ,outpath){
//@todo Need os specific code here... Remove c for Linux.....
def cmd = ['cmd','/c', 'convert', inpath, action , options, outpath]
return cmd.join(" ");
}

String createDimentions(width, height){
StringBuffer sb = new StringBuffer();
if(width > 0)
sb.append(width);

sb.append('x');
if(height > 0)
sb.append(height)

return sb.toString()
}
}





Percebam que no código do controlador, o interessante foi o uso de uma Thread separada para o processamento das imagens. Já que processar imagens pode levar algum tempo em determinados casos, não é necessário deixar o usuário final esperando isso. Assim, criei uma nova thread que faz este trabalho enquanto a thread do request já manda o response direto pro usuário. Neste exemplo, eu faço 3 coisas: salvo o arquivo original, crio o thumbnail, crio a imagem grande que chamei de main, e por último crio a imagem grande com a marca d'água (watermark). Tudo isso é feito no método processImg(fileName). Percebam que este método faz uso do método execute() do groovy, que executa um comando no sistema operacional e retorna um java.lang.Process.

Depois disso foi só gerar as views (grails generate-views). Alterei a create.gsp para que tivesse o upload do meu arquivo:

           <g:form action="save" method="post" enctype="multipart/form-data">
<div class="dialog">
<table>
<tbody>



<tr class='prop'><td valign='top' class='name'><label for='description'>Description:</label></td><td valign='top' class='value ${hasErrors(bean:taskFile,field:'description','errors')}'><input type='text' name='description' value="${taskFile?.description?.encodeAsHTML()}" /></td></tr>

<tr class='prop'><td valign='top' class='name'><label for='name'>Name:</label></td><td valign='top' class='value ${hasErrors(bean:taskFile,field:'name','errors')}'><input type='text' name='name' value="${taskFile?.name?.encodeAsHTML()}" /></td></tr>

<tr class='prop'><td valign='top' class='name'><label for='taskFile'>taskFile:</label></td><td valign='top' class='value ${hasErrors(bean:taskFile,field:'uploaded','errors')}'><input type="file" name="taskFile" /></td></tr>

</tbody>
</table>
</div>
<div class="buttons">
<span class="formButton">
<input type="submit" value="Create"></input>
</span>
</div>
</g:form>


E só isso. Até a próxima. Abcs.

3 comentários:

Daniel F. Martins 27 de maio de 2007 17:41

Muito boa a dica! :D

Valeu!

GouverMXT 31 de agosto de 2010 12:58

E no caso quando estou utilizando um provedor de hospedagem, como vou conseguir instalar o IM lá?

Felipe Nascimento 31 de agosto de 2010 18:48

Há provedores que possuem o IM instalado, ou que vc pode pedir para instalarem. Caso seu servidor seja um servidor dedicado, vc mesmo pode instalar (que foi o meu caso)
Abcs