#374 Image Manipulation
- Download:
- source codeProject Files in Zip (54.2 KB)
- mp4Full Size H.264 Video (30.4 MB)
- m4vSmaller H.264 Video (14.5 MB)
- webmFull Size VP8 Video (15.3 MB)
- ogvFull Size Theora Video (30.3 MB)
Ryan Bates escribió hace unos años una aplicación llamada Daily Stamp, en la que podíamos introducir aquellas tareas que queríamos hacer todos los días, tras lo cual se nos mostraba un calendario y un sello que podíamos usar para marcar los días en los que habíamos logrado cumplir dicha tarea.
En este episodio nos centraremos en la posibilidad de personalizar el sello. La aplicación tiene una página donde podemos escoger de entre una lista de sellos o usar uno propio. La imagen que subamos será redimensionada y se le aplicarán efectos para que parezca un sello de caucho. También podemos escoger varios colores para nuestro sello. En este episodio vamos a ver cómo añadir la capacidad para hacer este tipo de manipulación de imágenes. La herramienta clave es ImageMagick, con la que muchos tienen una relación de amor-odio porque puede ser difícil de usar y configurar, si bien es una librería muy potente para realizar manipulaciones de imágenes una vez que la tenemos funcionando. Un buen comienzo es inspeccionar sus herramientas de la línea de órdenes, con las que nos podremos hacer una idea de las capacidades de ImageMagick y los pasos que hacen falta para procesar una imagen.
La instalación de ImageMagick puede ser complicada y si estamos usando Mac OS X es buena idea usar Homebrew, con lo cual lo único que nos hace falta es ejecutar la orden brew install imagemagick
, que nos debería instalar la orden convert
junto con otras que también sirven para procesar imágenes.
Además de la imagen con la que vamos a trabajar, que es un PNG del Octocat de GitHub, tenemos otra imagen que es una capa de textura consistente en una imagen blanca con cierta transparencia que le da el aspecto de un sello de caucho. Nuestro primer objetivo es reescalar y recortar la imagen original para que encaje correctamente con nuestra textura. En la documentación de la orden convert
podemos ver la lista de opciones que recibe, incluyendo las opciones para recortar o reescalar una imagen. Ambas opciones aceptan un parámetro de geometría en cuya documentación se nos indican las diferentes formas en las que los argumentos cambian su comportamiento. Esto es muy cómodo si estamos intentando hacer una miniatura cuadrada a partir de una imagen con un tamaño extraño, como es nuestro caso. Primero usaremos la opción con un circunflejo que nos permite rellenar la imagen manteniendo la proporción. Empezaremos con la terminal para reescalar la imagen a 70x70 píxeles. También la recortaremos a estas dimensiones, guardando el fichero como source.png
.
$ convert octocat.png -resize '70x70^' -crop '70x70+0+0' source.png
La imagen resultante tiene el siguiente aspecto. Sus dimensiones son las correctas, pero está descentrada. Queremos recortar justo en el centro, lo que podemos hacer con la opcion gravity
que se establece antes de recortar, indicando que queremos centrar la imagen antes del corte.
$ convert octocat.png -resize '70x70^' -gravity center -crop '70x70+0+0' source.png
Vemos que esto recorta la imagen correctamente, por lo que ya tenemos una orden que podemos usar para convertir cualquier imagen en una miniatura. Queremos convertir dicha miniatura en una imagen en tono de grises y añadirle algo de contraste para que sirva como sello. Esto podemos hacerlo añadiendo varias opciones como quantize
para establecer la paleta de color a tonos de gris, colors
para hacer la conversión y contrast
para que la imagen destaque un poco más. Nótese que la opción que se pasa a quantize
va en mayúsculas.
$ convert octocat.png -resize '70x70^' -gravity center -crop '70x70+0+0' -quantize GRAY -colors 256 -contrast source.png
Ya tenemos la imagen en tono de grises sobre la que podemos componer la transparencia para obtener la textura de un sello. Podemos hacerlo con la orden composite
que coloca una imagen encima de la otra. Vamos a componer la transparencia sobre nuestra imagen original.
$ composite stamp_overlay.png source.png source.png
La imagen aparece ya con la textura del sello sobreimpresa, lo siguente que queremos hacer es añadirle algo de color. La orden convert
puede usarse no sólo para modificar imágenes ya existentes sino para crearlas desde cero. Crearemos una imagen de 70x70 píxeles de color rojo. Lo haremos con la opción canvas
, si bien algunas versiones de ImageMagick esperan la opción cx
.
$ convert -size 70x70 canvas:red colour.png
Con esto generamos una imagen roja cuadrada que podemos componer con la imagen original. En lugar de gestionar otro archivo, podemos gestionar la composición con la misma orden. Cargaremos la imagen original y configuraremos cómo se debe componer con la otra, usando la configuración copy-opacity
, indicando que debe componer la imagen original con el lienzo rojo.
$ convert -size 70x70 canvas:red source.png -compose copy-opacity -composite stamp.png
La imagen generada se acerca mucho a lo que queremos pero por lo que parece tendremos que invertir la imagen original para obtener el resultado final que buscamos. Podemos hacerlo pasando el nombre de la imagen entre paréntesis y utilizando la opción negate
, que invierte los colores de la imagen original.
convert -size 70x70 canvas:red \( source.png -negate \) -compose copy-opacity -composite stamp.png
La imagen tiene el aspecto exacto que queremos. Es fácil configurar nuestra orden para cambiar el color de los diferentes sellos. Podemos combinarlo todo en una única orden para reescalar la imagen y convertirla en un sello de una única pasada. Es una orden larga y compleja:
convert -size 70x70 canvas:red \( octocat.png -resize '70x70^' -gravity center -crop '70x70+0+0' -quantize GRAY -colors 256 -contrast stamp_overlay.png -composite -negate \) -compose copy-opacity -composite stamp.png
Cómo hacer esto en una aplicación Rails
El siguiente paso es hacer que esto funcione en una aplicación Rails. Una opción es ejecutar esta orden desde Ruby, que no es mala idea. Hay varias gemas que hacen precisamente esto, como por ejemplo Image Sorcery y MiniMagick. La interfaz de estas dos gemas puede resultar un poco complicada cuando tenemos que hacer manipulaciones complejas de las imágenes, por lo que a veces es mejor idea tener toda la orden en una cadena y ejecutarla cuando haga falta. También está la clásica gema RMagick. No tiene actualizaciones recientes y muchos la critican por perder memoria, pero sigue siendo una opción válida si queremos una alternativa a la línea de órdenes. Se trata de un recubrimiento de la librería C, por lo que las opciones no son siempre las mismas que las de la línea de órdenes. En su favor apuntaremos que dispone de una completa guía del usuario. Veamos cómo tendríamos que usar RMagick para generar la misma imagen del sello. Lo primero es instalar la gema.
$ gem install rmagick
Antes de pasar a la aplicación Rails vamos a manipular la imagen usando un sencillo script en Ruby.
require "rmagick" source = Magick::Image.read("octocat.png").first source = source.resize_to_fill(70, 70).quantize(256, Magick::GRAYColorspace).contrast(true) overlay = Magick::Image.read("stamp_overlay.png").first source.composite!(overlay, 0, 0, Magick::OverCompositeOp) colored = Magick::Image.new(70, 70) { self.background_color = "red" } colored.composite!(source.negate, 0, 0, Magick::CopyOpacityCompositeOp) colored.write("stamp.png")
Primero requerimos rmagick
y luego cargamos la imagen de origen. La lectura de una imagen nos devuelve una lista de capas, por lo que escogemos la primera. Podemos utilizar el método resize_to_fill
para recortar y reescalar la imagen, e invocar quantize
sobre la miniatura para convertirla a escala de grises, para luego llamar contrast
para incrementar el contraste.
A continuación componemos la transparencia. La cargamos igual que la imagen original e invocamos a composite!
sobre la imagen original para realizar la composición, tras lo cual creamos una nueva imagen con un fondo rojo y la componemos sobre la imagen original invertida. Nótese el argumento Magick::CopyOpacityCompositeOp
que es el mismo que la opción -compose copy-opacity
que usábamos en la línea de órdenes. Por último, escribimos la imagen.
Ya podemos hacer la prueba. Si ejecutamos nuestro script la imagen devuelta tiene el mismo aspecto que la que generamos en la línea de órdenes, por lo que parece que todo funciona. En comparación, el otro enfoque del que hablábamos tiene el siguiente aspecto:
# Or through the command line: system <<-COMMAND convert -size 70x70 canvas:red \\( octocat.png \ -resize '70x70^' -gravity center -crop '70x70+0+0' \ -quantize GRAY -colors 256 -contrast stamp_overlay.png \ -composite -negate \ \\) -compose copy-opacity -composite stamp.png COMMAND
Si usamos este último método tenemos que tener cuidado con los ataques de inyección de órdenes de shell si aceptamos parámetros que envíe el usuario. RMagick parece más verboso pero también es más dinámico.
Ya hemos creado una sencilla aplicación Rails para demostrar cómo convertir una imagen recién subida. Tenemos una página con un diálogo de subida de archivo que convertirá cualquier imagen que queramos subir en un sello. Hemos usado RMagick junto con CarrierWave para gestionar las subidas (ya vimos CarrierWave en el episodio 253.)
gem 'rmagick' gem 'carrierwave'
Tenemos un modelo Stamp
que monta un uploader de CarrierWave llamado StampUploader
.
class Stamp < ActiveRecord::Base attr_accessible :image mount_uploader :image, StampUploader end
La clase StampUploader
es muy sencilla, aparte del código del procesamiento de la imagen.
# encoding: utf-8 class StampUploader < CarrierWave::Uploader::Base include CarrierWave::RMagick include Sprockets::Helpers::RailsHelper include Sprockets::Helpers::IsolatedHelper storage :file def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end def extension_white_list %w(jpg jpeg gif png) end %w[red green blue purple black].each do |color| version(color) { process stamp: color } end def stamp(color) manipulate! format: "png" do |source| overlay_path = Rails.root.join("app/assets/images/stamp_overlay.png") overlay = Magick::Image.read(overlay_path).first source = source.resize_to_fill(70, 70).quantize(256, Magick::GRAYColorspace).contrast(true) source.composite!(overlay, 0, 0, Magick::OverCompositeOp) colored = Magick::Image.new(70, 70) { self.background_color = color } colored.composite(source.negate, 0, 0, Magick::CopyOpacityCompositeOp) end end end
El procesamiento se hace en el método stamp
, que es todo lo que hace falta para gestionar las versiones en forma de sello. Lo que hacemos es iterar sobre una lista de colores y hacer una versión para cada uno. En el método stamp
invocamos a manipulate!
, que es un método de CarrierWave y le decimos que queremos usar RMagick para generar la imagen PNG. El método recibe un bloque al que se le pasa la imagen original. El código del bloque debería devolver la imagen modificada, y se trata básicamente del mismo código que vimos antes, que en una aplicación de producción deberíamos llevar a un proceso de segundo plano. Dado que cada color es una versión diferente, podemos cambiar fácilmente el color del sello que se muestra en la vista simplemente usando otra versión.
<h1>Stamps</h1> <% @stamps.each do |stamp| %> <div class="stamp"> <%= image_tag stamp.image_url(:blue) %> </div> <% end %> <div class="clear"></div> <p><%= link_to "New Stamp", new_stamp_path %></p>
Ahora nuestros sellos salen en otro color.