Recortando imagens com jCrop e PHP

Quando desenvolvemos um sistema com upload de fotos envolvendo miniaturas, é muito comum um cliente não gostar dos thumbnails gerados automaticamente pelo PHP, já que todos devem possuir o corte nas mesmas coordenadas X e Y. Existem duas maneiras de resolver o problema: uma é criar um novo campo de upload para que o cliente já mande a miniatura no tamanho certinho. A outra é desenvolver uma interface em javascript para o cliente selecionar, após o upload, a área da foto que ele deseja utilizar na miniatura.

O plugin jCrop que apresento neste artigo facilita a implementação da segunda solução. Além de desenvolver o frontend, vou também mostrar a parte em PHP que redimensiona e recorta a imagem - tudo isso utilizando nossa classe m2brimagem.

Configurações iniciais

Como se trata de um plugin jQuery precisamos da chamada para a biblioteca em algum lugar de nosso HTML. Além disso, vamos precisar também incluir o arquivo fonte do jCrop com sua folha de estilo e imagens necessárias. Clique aqui para fazer o download dos arquivos.

<link href="css/jquery.Jcrop.css" rel="stylesheet" type="text/css" />
<script src="js/jquery.min.js"></script>
<script src="js/jquery.Jcrop.js"></script>

Desse jeito já dá pra utilizar o jCrop em sua forma mais básica:

<img src="imagem.jpg" width="634" height="340" id="jcrop" />
<script type="text/javascript">
$(function(){
  $('#jcrop').Jcrop();
});
</script>

No exemplo acima, ‘#jcrop’ é o atributo ID da imagem alvo do crop. Você pode utilizar qualquer tipo de seletor do jQuery (classes, sub-elementos etc.), aplicando o frontend do crop em vários elementos img.

Muito legal, mas e pra salvar a imagem? O plugin funciona apenas no cliente, na interface da aplicação. O crop mesmo tem que ser feito no servidor.

Processando o crop com PHP

Primeiramente vamos entender como funciona o jCrop. Seu método de marcação na imagem retorna um array com as dimensões e o posicionamento do crop. Ele possui alguns eventos, aqui vamos utilizar os dois principais: onChange e onSelect. O onChange executa uma função qualquer no momento que a marcação é alterada e o onSelect executa uma função qualquer no momento que a seleção está em andamento.

Sendo assim, vamos utilizar, por enquanto, a função exibePreview em ambos os casos. O que ela faz é atualizar uma pré-visualização do resultado final do crop, além de armazenar as variáveis para envio e processamento no servidor.

function exibePreview( c )
{
  // c.x, c.y, c.x2, c.y2, c.w, c.h
};

A função recebe o array c, aquele com as dimensões e coordenadas do crop. Os valores do array são:

wlargura (width) do crop
haltura (height) do crop
x1 e x2posições horizontais do crop na imagem
y1 e y2posições verticais do crop na imagem

Note que é aí que termina o trabalho do jCrop. Os valores devem ser processados no PHP. No nosso exemplo, conforme mencionei anteriormente, a função exibePreview vai registrar as posições em inputs do tipo hidden no formulário de envio.

function exibePreview( c )
{
  // campos hidden que armazenam os valores
  $('#x').val(c.x);
  $('#y').val(c.y);
  $('#x2').val(c.x2);
  $('#y2').val(c.y2);
  $('#w').val(c.w);
  $('#h').val(c.h);
};

Além disso ela deve atualizar a pré-visualização da imagem recortada. Pra isso vamos precisar do tamanho original da imagem. Se você está usando o mesmo formato de imagem, basta utilizar os mesmos valores sempre. No nosso caso, como a imagem é enviada via formulário, utilizamos a função getimagesize do php para retornar a largura (índice 0 do array de retorno) e a altura (índice 1). Elas são necessárias para calcular o posicionamento do crop. A idéia é criar um div com as dimensões do crop, mascarando a imagem original.

function exibePreview(c)
{
  var rx = 100 / c.w;
  var ry = 100 / c.h;

  // atualiza CSS do preview para refletir o tamanho da imagem enviada
  // e o posicionamento do crop
  $('#preview').css({
    width: Math.round(rx * <?php echo $imagesize[0]; ?>) + 'px',
    height: Math.round(ry * <?php echo $imagesize[1]; ?>) + 'px',
    marginLeft: '-' + Math.round(rx * c.x) + 'px',
    marginTop: '-' + Math.round(ry * c.y) + 'px'
  });

  // campos hidden que armazenam os valores
  $('#x').val(c.x);
  $('#y').val(c.y);
  $('#x2').val(c.x2);
  $('#y2').val(c.y2);
  $('#w').val(c.w);
  $('#h').val(c.h);
}

Com a função exibePreview completa, podemos agora atualizar nossa chamada do jCrop:

$('#jcrop').Jcrop({
  onChange: exibePreview,
  onSelect: exibePreview,
  aspectRatio: 1
});

Note a propriedade aspectRatio, utilizada para amarrar largura e altura do crop, mantendo a proporção.

Tudo OK no frontend. Abaixo você confere o código do formulário de envio e o script para processamento da imagem pós-envio (validação e redimensionamento para evitar arquivos gigantes no crop).

<form name="frm-jcrop" id="frm-jcrop" method="post" action="index.php" enctype="multipart/form-data">
  <p>
    <label>Envie uma imagem:</label>
    <input type="file" name="imagem" id="imagem" />
    <input type="submit" value="Enviar" />
  </p>
</form>
// processa arquivo
$imagem    = isset( $_FILES['imagem'] ) ? $_FILES['imagem'] : NULL;
$img    = '';
// verifica se arquivo foi enviado para o servidor
if( $imagem['tmp_name'] )
{
  // move arquivo para o servidor
  if( move_uploaded_file( $imagem['tmp_name'], $imagem['name'] ) )
  {
    include( 'm2brimagem.class.php' );
    $oImg = new m2brimagem( $imagem['name'] );
    if( $oImg->valida() == 'OK' )
    {
      // redimensiona imagem para evitar arquivos grandes
      $oImg->redimensiona( '400', '', '' );
      $oImg->grava( $imagem['name'] );
      // retorna dimensões da imagem e configura variáveis para o jCrop
      $imagesize   = getimagesize( $imagem['name'] );
      $img    = '<img src="'.$imagem['name'].'" id="jcrop" '.$imagesize[3].' />';
      $preview  = '<img src="'.$imagem['name'].'" id="preview" '.$imagesize[3].' />';
    }
    else
    {
      // imagem inválida, exclui do servidor
      unlink( $imagem['name'] );
    }
  }
}

Está quase pronto. Temos um formulário para envio da imagem, e todo o javascript que vai processar o crop e atualizar o nosso preview. Falta o código PHP que vai de fato recortar a imagem enviada. Para isso utilizaremos a classe m2brimagem (leia mais sobre ela aqui).

O processamento será feito via AJAX/post, apenas para agilizar o retorno ao usuário, mas nada impede você de fazer o crop em um novo envio do formulário.

$('#btn-crop').click(function(){
  $.post( 'crop.php', {
    img:img,
    x: $('#x').val(),
    y: $('#y').val(),
    w: $('#w').val(),
    h: $('#h').val()
  }, function(){
    $('#div-jcrop').html( '<img src="'+img+'?'+Math.random()+'" width ="'+$('#w').val()+'" height ="'+$('#h').val()+'" />' );
  });
  return false;
});

Uma vez processado, nosso formulário exibirá uma nova tela, com a opção de recortar e salvar um pedaço da imagem enviada. No exemplo você pode observar que, além da imagem e da interface para crop, exibimos também duas janelas adicionais: o preview, já comentado anteriormente; e um pequeno debug, exibindo as informações e coordenadas em tempo real (tudo isso atualizado via exibePreview).

Ao clicar no botão salvar, o usuário executa, via AJAX, o script abaixo, mais uma vez utilizando a classe m2brimagem para o processamento. O script recebe como parâmetro o ponto inicial X e Y do crop, além da largura e altura do mesmo, e cria a versão recortada da imagem original. Após a execução, nosso elemento img é atualizado. Como nesse caso a imagem final possui o mesmo nome da imagem original, utilizamos o “?” com um número randômico (Math.random()) para evitar cache (essa dica é bem legal para sistemas com upload de imagens).

if( $_SERVER['REQUEST_METHOD'] == 'POST' )
{
  include( 'm2brimagem.class.php' );
  $oImg = new m2brimagem( $_POST['img'] );
  if( $oImg->valida() == 'OK' )
  {
    $oImg->posicaoCrop( $_POST['x'], $_POST['y'] );
    $oImg->redimensiona( $_POST['w'], $_POST['h'], 'crop' );
    $oImg->grava( $_POST['img'] );
  }
}
exit;

Outras opções

Talvez tudo pareça um pouco confuso olhando os códigos assim de forma separada. Baixe os exemplos que você vai entender melhor. No site do plugin você encontra a documentação completa, em inglês, além de outros exemplos. Uma opção que utilizo bastante é especificar largura e altura fixas. Quando o usuário faz o upload de um avatar, por exemplo, vale a pena limitar o tamanho no jCrop (ou então fazer alguma coisa proporcional). As propriedades minSize e maxSize delimitam a área mínima e máxima do crop.

$('#jcrop').Jcrop({
  onChange: exibePreview,
  onSelect: exibePreview,
  minSize    : [ 200, 200 ],
  maxSize    : [ 200, 200 ],
  allowResize  : false,
  addClass  : 'custom'
});

No exemplo acima o crop vai ter sempre 200x200 pixels de dimensão. Além disso configuramos a propriedade allowResize com false, para evitar o redimensionamento da seleção. Outra propriedade legal é a addClass, para definir um estilo personalizado na seleção. No exemplo abaixo a linha fica com uma borda sólida ao invés do pontilhado.

.custom .jcrop-vline, .custom .jcrop-hline {
  background: #FF3366;
}

Esse foi o jCrop, um bônus e tanto para a interface dos seus sistemas. Espero que tenham gostado da leitura e qualquer dúvida utilizem a área de comentários abaixo. Até a próxima!