La guia de platanus
  • README
  • Acuerdos
    • Guía de Estilo
      • Ejemplo: Módulo para variables de entorno
  • Stack
    • Getting Started
    • Nuestro MVC extendido
    • Ruby/Rails
      • Power Types
        • General
        • Patrones
          • Commands
          • Utils
          • Services
          • Values
          • Observers
      • Potassium
      • Power API
      • Active Admin
        • General
        • Active Admin Addons
      • Pundit
      • Shrine
        • General
        • Manejo y procesamiento de imágenes
      • Pry
      • Strong Migrations
      • Data Migrate
      • Active Job
      • Gems
      • Engines - Modularización en Rails
    • JavaScript
      • Vue
        • General
        • Testing
      • AlpineJS
    • CSS
    • Mobile
      • Expo
      • React Navigation
      • Redux
        • Crear y conectar una slice en Redux
      • Styling
        • Usando Tailwind en React Native
      • Recursos
    • Resolviendo problemas (debugging)
    • Machine Learning
  • Setup
    • Configuración de tu entorno local
      • Instalación Base
        • OSX
        • Windows
        • Linux
      • Tecnologías
        • Ruby
        • Docker
        • Node
      • Herramientas
        • Linters
        • Editores
          • IDE/Editores de Código
            • Visual Studio Code
            • Sublime Text
        • Git
    • Configuración de proyectos
      • Getting Started
      • Heroku
      • Rails
      • Circle CI
      • Vue
      • Apple App Store
      • Google Play
      • Expo
      • S3
      • Git
      • Cloudflare
      • Sendgrid
      • Dominio + Mailing
      • Google Tag Manager, Analytics, Search Console, etc.
        • Google Tag Manager
          • Configurar Google Tag Manager
        • Google Analytics
        • Indexación en Google
        • Google Ads
      • Crear un bucket de S3
      • SlackBot
      • Google BigQuery
  • Deployment
    • Rails
    • Ruby Gems
    • Browser and Node (Open Source)
    • Mobile
      • Mobile Resources
      • Apple App Storage
      • Google Play
  • Upgrades
    • Upgrade de Vue 2 a Vue 3
    • Migración Hound → reviewdog
    • Upgrade de Postgresql
Con tecnología de GitBook
En esta página
  • Motivación
  • Blurhash
  • Derivatives
  • ¿Cómo se ve esto en Potassium?
  • Backgrounding
  • CoverImageUploader
  • ImageHandlingAttributes
  • ImageHandlingUtilities
  • Frontend
  • Renderear blurhash
  • Transición de blurhash a imagen
  1. Stack
  2. Ruby/Rails
  3. Shrine

Manejo y procesamiento de imágenes

AnteriorGeneralSiguientePry

Última actualización hace 2 años

Motivación

Consideremos una vista con imágenes subidas por usuarios, donde tenemos menos control sobre lo que suben. Si las mostramos tal cuál fueron subidas con un <img> y nada más, podríamos tener una carga no muy atractiva, algo así:

Hay varios problemas con esto:

  • Mientras las imágenes no se han cargado se ve un espacio vacío

  • Cuando se van cargando hay un momento que se ven a la mitad

  • Se demoran harto en cargar todas

En esta sección de la guía vamos a ver algunas cosas que se pueden hacer con Shrine para que potencialmente la carga sea más amigable:

Las cosas que veremos acá, también , ahí en el PR puedes ver más detalles también.

Del video anterior, probablemente lo que más llama la atención son esas versiones borrosas de las imágenes que aparecen antes de que las imágenes mismas se muestren. Eso se logra con Blurhash: una herramienta que permite representar una imagen como un string que luego se decodifica en el frontend y se obtiene algo que se pueden pintar en un <canvas> rápidamente.

Con esto se evita el espacio en blanco y la carga por partes que se ven en el primer video.

Para usar blurhash con Shrine, necesitamos procesar la imagen para obtener el string y guardarlo en la metadata del archivo. Usaremos el para agregar la metadata. Para procesar la imagen, usaremos un procesador de imágenes alternativo a ImageMagick que ha mostrado tener mejor performance y menor uso de memoria. Esto se ve así:

class ImageUploader < Shrine
  add_metadata :blurhash do |io, derivative: nil, **|
    if derivative.nil?
      Shrine.with_file(io) do |file|
        image = Vips::Image.new_from_file(file.path, access: :sequential)

        # Transformamos el archivo a un tamaño más pequeño para acelerar el procesamiento
        image = image.resize(100.0 / image.width)

        # image.to_a entrega un arreglo con los pixeles en formato rgba, Blurhash requiere solo rgb
        flat_rgb_pixels = []
        image.to_a.each do |row|
          row.each { |pixel| flat_rgb_pixels.concat(pixel[0..2])  }
        end

        # Finalmente obtenemos el string, que se guarda en file.metadata['blurhash']
        Blurhash.encode(image.width, image.height, flat_rgb_pixels)
      end
    end
  end
end

Con esto ya tenemos un código que podemos usar en el front para renderear un placeholder mientras se carga la imagen.

Derivatives

Hasta ahora podemos evitar que nuestra vista tenga espacios vacíos en un principio y que luego las imágenes se empiecen a cargar por partes, pero todavía nos gustaría que el usuario pueda verlas lo más rápido posible. Para esto usaremos derivatives para disminuir el peso de las imágenes, de dos maneras:

  1. Tener imágenes en formato webp, un formato más liviano que png o jpg con muy poca perdida de calidad

Para esto usaremos nuevamente vips. Si quisieramos tener variaciones de tamaño pequeño, tanto en el formato original como en formato webp, tendríamos que hacer lo siguiente:

class ImageUploader < Shrine
  Attacher.derivatives do |original|
    vips = ImageProcessing::Vips.source(original)
    {
      sm: vips.resize_to_limit!(426, 240),
      webp_sm: vips.convert('webp').resize_to_limit!(426, 240),
    }
  end
end

¿Cómo se ve esto en Potassium?

En Potassium agregamos algunos archivos y configuraciones para facilitar el uso de estas cosas. La idea es que sirva como un ejemplo y base que se pueda customizar según sea necesario.

Backgrounding

Hacer esto resulta en que hay un momento entre que se sube una imagen y que esta se procesa, en que las derivatives no existen. Por lo tanto, si se pidiera una url para ellas en una vista, habría un problema. Para evitar esto, usamos una combinación de dos plugins para tener un fallback:

Uploader de ejemplo que incluye todo lo que ya habíamos mencionado:

  • Inclusión de blurhash a la metadata

  • Derivatives de 3 tamaños distintos, cada uno en jpg y en webp

  • Url default para derivatives faltantes usando la derivation url

Concern de serializer incluido en el BaseSerializer. Agrega un método add_image_handling_attributes que permite a cualquier serializador agregar attributes para el blurhash y para las urls de las derivatives de una imagen.

Dado un attachment de nombre image que usa el CoverImageUploader, se puede usar así:

add_image_handling_attributes(
  attachment_name: :image,
  derivatives: CoverImageUploader::DERIVATIVES.keys,
  include_original_image: true
)

Se usa la constante CoverImageUploader::DERIVATIVES para indicar que quiero agregar al objeto serializado todas las derivatives.

Si se tuviera solo una derivative sm, esto agregaría algo así al json resultante:

{
  # ...
  image_blurhash: 'LUG%rif*rwayI:jZ#qju0~azS_oe',
  image: {
    sm: { url: 'someurl.com/bla' },
    original: { url: 'someurl.com/ble' }
  }
}

Plugin custom de Shrine que se usa en el CoverImageUploader. Agrega algunos métodos de instancia y de clase al modelo que use ese uploader. Para un attachment de nombre image, estos son:

  • image_blurhash: retorna el blurhash sacado de la metadata

  • generate_image_derivatives: genera todas las derivatives definidas en el Uploader. Si el archivo ya tenía algunas derivatives definidas, estas serán reemplazadas.

    Puede ser útil si se agregan derivatives a un uploader que no tenía, o si se cambia la definición de estas en uno que sí tenía. En estos casos se tendría que reprocesar los archivos ya existentes, por ejemplo, llamando a este método en un job.

    Método de clase asociado: generate_all_image_derivatives

  • generate_image_metadata: genera toda la metadata del attachment.

    Útil si se agrega metadata nueva que se necesita en records existentes.

    Método de clase asociado: generate_all_image_metadata

  • generate_image_derivatives_and_metadata: lo mismo que ambos métodos anteriores juntos, pero la gracia es que se preocupa de abrir el archivo solo una vez para procesar derivatives y metadata.

    Método de clase asociado: generate_all_image_derivatives_and_metadata

Frontend

Hasta ahora hemos visto cómo usar Shrine para generar estas cosas que nos pueden ayudar a mejorar nuestra experiencia de carga de imágenes. Lo que sigue es cómo usar esto en nuestras vistas. Esta parte no viene en Potassium, pero acá se explicará que se necesitaría para implementarlo con Vue 3.

Renderear blurhash

Para mostrar la imagen borrosa asociada al blurhash, necesitamos un componenete que maneje la decodificación del código y el pintado de los pixeles resultantes en un canvas. Para esto necesitamos agregar el paquete de blurhash:

yarn add blurhash

Luego, el componente se vería así:

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { decode } from 'blurhash';

interface Props {
  blurhash: string,
}
const props = defineProps<Props>();

const INITIAL_CANVAS_SIZE = 32;

const canvas = ref<HTMLCanvasElement | null>(null);

onMounted(() => {
  const pixels = decode(props.blurhash, INITIAL_CANVAS_SIZE, INITIAL_CANVAS_SIZE);
  const imageData = new ImageData(pixels, INITIAL_CANVAS_SIZE, INITIAL_CANVAS_SIZE);
  const context = canvas.value?.getContext('2d');
  context?.putImageData(imageData, 0, 0);
});
</script>

<template>
  <canvas
    ref="canvas"
    :width="INITIAL_CANVAS_SIZE"
    :height="INITIAL_CANVAS_SIZE"
  />
</template>

Transición de blurhash a imagen

Para determinar cuando pasar del canvas con el blurhash a la imagen, necesitamos un componente que renderee ambos en el mismo espacio, pero mostrando solo el blurhash. Luego, cuando la imagen esté totalmente cargada, intercambie la opacidad de ambos elementos. Para determinar cuándo hacer esto, usamos el evento @load de <img>. También necesitamos considerar el formato webp. Tiene buena compatibilidad con browsers modernos, pero para mayor cobertura usaremos un <picture> que permita usar el formato webp solo si el browser lo permite. En el caso contrario, se usaría como fallback un src jpg:

<script setup lang="ts">
import { ref } from '@vue/reactivity';
import BlurhashCanvas from './blurhash-canvas.vue';

interface Props {
  src: string,
  webpSrc?: string,
  blurhash?: string,
}
withDefaults(defineProps<Props>(), {
  webpSrc: undefined,
  blurhash: undefined,
});

const isLoaded = ref(false);
</script>

<template>
  <div
    class="relative"
  >
    <blurhash-canvas
      v-if="blurhash"
      :blurhash="blurhash"
      class="absolute w-full h-full transition-opacity duration-500"
      :class="isLoaded ? 'opacity-0' : 'opacity-100'"
    />
    <picture>
      <source
        ref="webpSource"
        type="image/webp"
        :srcset="webpSrc"
      >
      <source
        ref="jpegSource"
        type="image/jpeg"
        :srcset="src"
      >
      <img
        ref="image"
        class="w-full h-full transition-opacity duration-500"
        :class="isLoaded ? 'opacity-100' : 'opacity-0'"
        v-bind="$attrs"
        :src="src"
        loading="lazy"
        @load="isLoaded = true"
      >
    </picture>
  </div>
</template>

Con esto ya podemos usar este componente para renderear nuestras imágenes de una manera más amigable.

Tener distintos tamaños. Así el front puede usar el tamaño que más se adecúe al uso que se le dará en una vista y no tiene que cargar una imagen de mayor peso. Esto ya lo vimos en

El procesamiento de una imagen para sacar sus derivatives puede ser costoso en recursos y tiempo. Por esto, usamos el plugin de backgrounding para que este procesamiento .

: nos permite generar una url on-the-fly con una variación redimensionada de la imagen según parámetros de height y width.

: Nos permite definir una url por defecto cuando el archivo solicitado no existe. , lo usamos para entregar una url generada con el derivation endpoint anterior cuando se pide una derivative

💡 Notar que estamos seteando un tamaño fijo de 32px para el canvas. Esto es porque se necesita un tamaño explícito para decodificar el blurhash y renderearlo en el canvas, pero de todas maneras después se puede cambiar su tamaño con css. Por otro lado, usar un canvas pequeño ayuda con el performance.

💡 El loading="lazy" es un atributo del <img> que le dice al browser que solo cargue la imagen si es actualmente visible, o está cerca de serlo.

los ejemplos de Shrine
se encole en un job
Derivation Endpoint
Usado acá
Default Url
En nuestro caso
CoverImageUploader
ImageHandlingAttributes
ImageHandlingUtilities
Esto es lo que recomienda blurhash
Acá más información sobre lazy loading
Ver video
Ver video
fueron implementadas en Potassium
Blurhash
plugin add_metadata
ruby-vips