Presets Disponibles

Obtener Presets: GET /portals/me/presets
Crear Aspect: POST /portals/me/presets/aspect
Crear Size: POST /portals/me/presets/size
Actualizar Aspect: PUT /portals/me/presets/aspect/{id}
Actualizar Size: PUT /portals/me/presets/size/{id}
Eliminar Preset: DELETE /portals/me/presets/{type}/{id}

🎯 ¿Qué son los Presets?

Los presets son configuraciones predefinidas que combinan múltiples parámetros de transformación optimizados para casos de uso específicos. En lugar de especificar manualmente cada parámetro, usas un preset y el sistema aplica automáticamente las mejores configuraciones.

❌ Sin Presets

?w=200&h=200&c=fill&g=face&f=webp&q=85&sharpen=10

✅ Con Presets

?preset=avatar

💡 Ventajas de los Presets

  • Simplicidad: Una sola palabra en lugar de múltiples parámetros
  • Consistencia: Configuraciones uniformes en toda la aplicación
  • Optimización: Parámetros ajustados por expertos para cada caso
  • Mantenimiento: Actualizaciones centralizadas de configuraciones

📋 Presets Predefinidos

Lista completa de presets disponibles con sus configuraciones:

👤

Avatar

preset=avatar

Optimizado para fotos de perfil y avatars de usuario.

Configuración:
  • w=200, h=200
  • c=fill, g=face
  • f=webp, q=85
  • sharpen=10
👤
200×200
🖼️

Thumbnail

preset=thumbnail

Miniaturas para listados y galerías.

Configuración:
  • w=300, h=200
  • c=fill, g=center
  • f=webp, q=80
  • sharpen=5
📷
300×200
🌟

Hero

preset=hero

Imágenes principales para headers y banners.

Configuración:
  • w=1200, h=600
  • c=fill, g=center
  • f=webp, q=90
  • sharpen=3
🌟
1200×600
🛍️

Product

preset=product

Optimizado para imágenes de productos e-commerce.

Configuración:
  • w=400, h=400
  • c=fill, g=center
  • f=webp, q=90
  • sharpen=8
🛍️
400×400
🖼️

Gallery

preset=gallery

Para galerías de imágenes y portfolios.

Configuración:
  • w=600, h=400
  • c=fit, g=center
  • f=webp, q=85
  • auto=format
600×400
📱

Social

preset=social

Optimizado para redes sociales y sharing.

Configuración:
  • w=1200, h=630
  • c=fill, g=center
  • f=jpeg, q=85
  • auto=quality
1200×630

🛠️ Usar Presets

Ejemplos prácticos de cómo implementar presets en tu aplicación:

HTML
<!-- Avatar de usuario -->
<img 
  src="https://limbo.lefebvre.es/assets/{id}/transform?preset=avatar"
  alt="Avatar del usuario"
  class="user-avatar"
>

<!-- Hero image responsivo -->
<picture>
  <source 
    media="(min-width: 1200px)" 
    srcset="https://limbo.lefebvre.es/assets/{id}/transform?preset=hero&dpr=1 1x,
            https://limbo.lefebvre.es/assets/{id}/transform?preset=hero&dpr=2 2x"
  >
  <source 
    media="(min-width: 768px)" 
    srcset="https://limbo.lefebvre.es/assets/{id}/transform?preset=hero&w=800&dpr=1 1x,
            https://limbo.lefebvre.es/assets/{id}/transform?preset=hero&w=800&dpr=2 2x"
  >
  <img 
    src="https://limbo.lefebvre.es/assets/{id}/transform?preset=hero&w=600" 
    alt="Hero image"
    class="hero-image"
  >
</picture>

<!-- Grid de productos -->
<div class="products-grid">
  <article class="product-item">
    <img 
      src="https://limbo.lefebvre.es/assets/{id}/transform?preset=product"
      srcset="https://limbo.lefebvre.es/assets/{id}/transform?preset=product&dpr=1 1x,
              https://limbo.lefebvre.es/assets/{id}/transform?preset=product&dpr=2 2x"
      alt="Producto"
      loading="lazy"
    >
  </article>
</div>

<!-- Galería de imágenes -->
<div class="gallery-grid">
  <figure class="gallery-item">
    <img 
      src="https://limbo.lefebvre.es/assets/{id}/transform?preset=gallery"
      alt="Imagen de galería"
      loading="lazy"
    >
  </figure>
</div>

<!-- Social sharing -->
<meta property="og:image" content="https://limbo.lefebvre.es/assets/{id}/transform?preset=social">
<meta name="twitter:image" content="https://limbo.lefebvre.es/assets/{id}/transform?preset=social">
JavaScript
class LimboPresets {
    constructor(baseUrl = 'https://limbo.lefebvre.es') {
        this.baseUrl = baseUrl;
        this.presets = {
            avatar: { w: 200, h: 200, c: 'fill', g: 'face', f: 'webp', q: 85, sharpen: 10 },
            thumbnail: { w: 300, h: 200, c: 'fill', g: 'center', f: 'webp', q: 80, sharpen: 5 },
            hero: { w: 1200, h: 600, c: 'fill', g: 'center', f: 'webp', q: 90, sharpen: 3 },
            product: { w: 400, h: 400, c: 'fill', g: 'center', f: 'webp', q: 90, sharpen: 8 },
            gallery: { w: 600, h: 400, c: 'fit', g: 'center', f: 'webp', q: 85, auto: 'format' },
            social: { w: 1200, h: 630, c: 'fill', g: 'center', f: 'jpeg', q: 85, auto: 'quality' }
        };
    }

    getPresetUrl(assetId, presetName, overrides = {}) {
        if (!this.presets[presetName]) {
            throw new Error(`Preset '${presetName}' not found`);
        }

        // Combinar preset con overrides
        const params = { ...this.presets[presetName], ...overrides };
        const queryString = new URLSearchParams(params).toString();
        
        return `${this.baseUrl}/assets/${assetId}/transform?preset=${presetName}&${queryString}`;
    }

    // Método simplificado usando solo el preset
    getPresetUrlSimple(assetId, presetName, overrides = {}) {
        const params = new URLSearchParams({ preset: presetName, ...overrides });
        return `${this.baseUrl}/assets/${assetId}/transform?${params.toString()}`;
    }

    createResponsivePresetSet(assetId, presetName, widths = [400, 800, 1200]) {
        return widths.map(width => {
            const url = this.getPresetUrlSimple(assetId, presetName, { w: width });
            return `${url} ${width}w`;
        }).join(', ');
    }

    createPresetSrcSet(assetId, presetName, dprValues = [1, 2]) {
        return dprValues.map(dpr => {
            const url = this.getPresetUrlSimple(assetId, presetName, { dpr });
            return `${url} ${dpr}x`;
        }).join(', ');
    }

    // Aplicar presets automáticamente a elementos con data attributes
    autoApplyPresets() {
        document.querySelectorAll('[data-limbo-preset]').forEach(img => {
            const assetId = img.dataset.limboAsset;
            const preset = img.dataset.limboPreset;
            const overrides = {};

            // Extraer overrides de data attributes
            Object.keys(img.dataset).forEach(key => {
                if (key.startsWith('limboParam')) {
                    const paramName = key.replace('limboParam', '').toLowerCase();
                    overrides[paramName] = img.dataset[key];
                }
            });

            // Aplicar preset
            img.src = this.getPresetUrlSimple(assetId, preset, overrides);
            
            // Crear srcset para retina
            img.srcset = this.createPresetSrcSet(assetId, preset, [1, 2]);
        });
    }

    // Lazy loading con presets
    setupLazyLoading() {
        if ('IntersectionObserver' in window) {
            const observer = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        const img = entry.target;
                        const assetId = img.dataset.limboAsset;
                        const preset = img.dataset.limboPreset;
                        
                        if (assetId && preset && !img.src) {
                            img.src = this.getPresetUrlSimple(assetId, preset);
                            img.srcset = this.createPresetSrcSet(assetId, preset);
                        }
                        
                        observer.unobserve(img);
                    }
                });
            });

            document.querySelectorAll('[data-limbo-preset]:not([src])').forEach(img => {
                observer.observe(img);
            });
        }
    }
}

// Uso
const limboPresets = new LimboPresets();

// Aplicar presets automáticamente
document.addEventListener('DOMContentLoaded', () => {
    limboPresets.autoApplyPresets();
    limboPresets.setupLazyLoading();
});

// Ejemplos de uso manual
const avatarUrl = limboPresets.getPresetUrlSimple('asset-id', 'avatar');
const heroUrl = limboPresets.getPresetUrlSimple('asset-id', 'hero', { w: 800 });
const productSrcSet = limboPresets.createPresetSrcSet('asset-id', 'product');
PHP
baseUrl = $baseUrl;
        $this->presets = [
            'avatar' => ['w' => 200, 'h' => 200, 'c' => 'fill', 'g' => 'face', 'f' => 'webp', 'q' => 85, 'sharpen' => 10],
            'thumbnail' => ['w' => 300, 'h' => 200, 'c' => 'fill', 'g' => 'center', 'f' => 'webp', 'q' => 80, 'sharpen' => 5],
            'hero' => ['w' => 1200, 'h' => 600, 'c' => 'fill', 'g' => 'center', 'f' => 'webp', 'q' => 90, 'sharpen' => 3],
            'product' => ['w' => 400, 'h' => 400, 'c' => 'fill', 'g' => 'center', 'f' => 'webp', 'q' => 90, 'sharpen' => 8],
            'gallery' => ['w' => 600, 'h' => 400, 'c' => 'fit', 'g' => 'center', 'f' => 'webp', 'q' => 85, 'auto' => 'format'],
            'social' => ['w' => 1200, 'h' => 630, 'c' => 'fill', 'g' => 'center', 'f' => 'jpeg', 'q' => 85, 'auto' => 'quality']
        ];
    }

    public function getPresetUrl(string $assetId, string $presetName, array $overrides = []): string
    {
        if (!isset($this->presets[$presetName])) {
            throw new \InvalidArgumentException("Preset '{$presetName}' not found");
        }

        $params = array_merge(['preset' => $presetName], $overrides);
        $queryString = http_build_query($params);
        
        return "{$this->baseUrl}/assets/{$assetId}/transform?{$queryString}";
    }

    public function createResponsiveSrcSet(string $assetId, string $presetName, array $widths = [400, 800, 1200]): string
    {
        $srcSet = [];
        foreach ($widths as $width) {
            $url = $this->getPresetUrl($assetId, $presetName, ['w' => $width]);
            $srcSet[] = "{$url} {$width}w";
        }
        
        return implode(', ', $srcSet);
    }

    public function createRetinaSrcSet(string $assetId, string $presetName, array $dprValues = [1, 2]): string
    {
        $srcSet = [];
        foreach ($dprValues as $dpr) {
            $url = $this->getPresetUrl($assetId, $presetName, ['dpr' => $dpr]);
            $srcSet[] = "{$url} {$dpr}x";
        }
        
        return implode(', ', $srcSet);
    }

    public function renderPresetImage(string $assetId, string $presetName, array $attributes = []): string
    {
        $src = $this->getPresetUrl($assetId, $presetName);
        $srcset = $this->createRetinaSrcSet($assetId, $presetName);
        
        $attrs = array_merge([
            'src' => $src,
            'srcset' => $srcset,
            'loading' => 'lazy'
        ], $attributes);

        $attrString = implode(' ', array_map(
            fn($key, $value) => sprintf('%s="%s"', $key, htmlspecialchars($value)),
            array_keys($attrs),
            $attrs
        ));

        return "";
    }

    public function renderResponsivePicture(
        string $assetId, 
        string $presetName, 
        array $breakpoints = [],
        array $attributes = []
    ): string {
        $html = '';
        
        // Breakpoints personalizados
        foreach ($breakpoints as $breakpoint) {
            $media = $breakpoint['media'];
            $width = $breakpoint['width'] ?? null;
            $overrides = $breakpoint['overrides'] ?? [];
            
            if ($width) {
                $overrides['w'] = $width;
            }
            
            $srcset = $this->createRetinaSrcSet($assetId, $presetName, [1, 2]);
            $html .= "";
        }
        
        // Imagen fallback
        $img = $this->renderPresetImage($assetId, $presetName, $attributes);
        $html .= $img;
        $html .= '';
        
        return $html;
    }
}

// Twig Extension para facilitar el uso en templates
class LimboPresetTwigExtension extends \Twig\Extension\AbstractExtension
{
    private LimboPresetService $presetService;

    public function __construct(LimboPresetService $presetService)
    {
        $this->presetService = $presetService;
    }

    public function getFunctions(): array
    {
        return [
            new \Twig\TwigFunction('limbo_preset', [$this->presetService, 'getPresetUrl']),
            new \Twig\TwigFunction('limbo_preset_img', [$this->presetService, 'renderPresetImage'], ['is_safe' => ['html']]),
            new \Twig\TwigFunction('limbo_responsive_img', [$this->presetService, 'renderResponsivePicture'], ['is_safe' => ['html']]),
        ];
    }
}

// Uso en controlador
class GalleryController extends AbstractController
{
    public function __construct(private LimboPresetService $presetService)
    {
    }

    public function gallery(): Response
    {
        $assets = $this->getAssets(); // Tu lógica para obtener assets
        
        return $this->render('gallery.html.twig', [
            'assets' => $assets,
            'presetService' => $this->presetService
        ]);
    }
}
CSS
/* Estilos para diferentes presets */

/* Avatar styles */
.user-avatar {
  width: 200px;
  height: 200px;
  border-radius: 50%;
  object-fit: cover;
  border: 3px solid #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

/* Hero image styles */
.hero-image {
  width: 100%;
  height: 60vh;
  object-fit: cover;
  object-position: center;
}

@media (max-width: 768px) {
  .hero-image {
    height: 40vh;
  }
}

/* Product grid */
.products-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 2rem;
}

.product-item img {
  width: 100%;
  height: 400px;
  object-fit: cover;
  border-radius: 8px;
  transition: transform 0.3s ease;
}

.product-item:hover img {
  transform: scale(1.05);
}

/* Gallery layout */
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 1rem;
}

.gallery-item {
  margin: 0;
  border-radius: 8px;
  overflow: hidden;
}

.gallery-item img {
  width: 100%;
  height: auto;
  display: block;
  transition: opacity 0.3s ease;
}

/* Lazy loading placeholder */
.gallery-item img[data-limbo-preset]:not([src]) {
  opacity: 0;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: loading 1.5s infinite;
}

@keyframes loading {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* Thumbnails en listas */
.thumbnail-list {
  display: flex;
  gap: 1rem;
  overflow-x: auto;
  padding: 1rem 0;
}

.thumbnail-item {
  flex-shrink: 0;
  width: 300px;
  height: 200px;
  border-radius: 6px;
  overflow: hidden;
}

.thumbnail-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Responsive utilities */
.preset-container {
  position: relative;
  overflow: hidden;
}

.preset-container::before {
  content: '';
  display: block;
  padding-bottom: var(--aspect-ratio, 75%); /* 4:3 default */
}

.preset-container img {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Aspect ratios específicos */
.preset-container--square { --aspect-ratio: 100%; }
.preset-container--hero { --aspect-ratio: 50%; }
.preset-container--social { --aspect-ratio: 52.5%; }

/* Estados de carga */
.preset-image {
  transition: opacity 0.3s ease;
}

.preset-image.loading {
  opacity: 0.7;
}

.preset-image.loaded {
  opacity: 1;
}

🎨 Personalizar Presets

Puedes override parámetros específicos de cualquier preset:

Personalizar Tamaño

# Avatar más grande
?preset=avatar&w=300&h=300

# Hero para móvil
?preset=hero&w=800&h=400

Cambiar Formato

# Product en JPEG
?preset=product&f=jpeg

# Gallery en AVIF
?preset=gallery&f=avif&q=75

Aplicar Efectos

# Thumbnail con blur
?preset=thumbnail&blur=5

# Hero en B&N
?preset=hero&grayscale=true

Ajustar Calidad

# Social alta calidad
?preset=social&q=95

# Thumbnail optimizado
?preset=thumbnail&q=70&auto=quality
⚠️

Orden de Parámetros

Los parámetros personalizados tienen prioridad sobre los del preset. Siempre especifica el preset primero, luego los overrides.

🔧 Crear Presets Personalizados

Para casos de uso específicos, puedes solicitar presets personalizados:

1

Definir Necesidades

Identifica patrones repetitivos en tu aplicación y documenta los parámetros que usas frecuentemente.

2

Solicitar Preset

Contacta al administrador con la especificación detallada del preset y casos de uso.

3

Validar y Implementar

Una vez creado, el preset estará disponible en toda tu aplicación inmediatamente.

Ejemplo de Solicitud
Preset Name: "blog_featured"

Use Case: Featured images for blog posts
- Need wide format for article headers
- High quality for reading experience
- Optimized loading for mobile

Parameters:
- w=900, h=450 (2:1 aspect ratio)
- c=fill, g=center
- f=webp, q=88
- sharpen=5
- auto=format

Usage: ?preset=blog_featured

⚡ Tips de Performance

🎯

Usa el Preset Correcto

No uses hero para thumbnails pequeños. Cada preset está optimizado para su contexto específico.

📱

Considera el Contexto

Reduce tamaños en móviles usando overrides como w=400 en presets grandes.

🔄

Combina con DPR

Usa dpr=2 para pantallas retina y dpr=1 para estándar.

💾

Cache Inteligente

Los presets se cachean agresivamente. Cambios en configuración actualizan automáticamente el cache.

🚀 Próximos Pasos