🎯 ¿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=200c=fill, g=facef=webp, q=85sharpen=10
200×200
Thumbnail
preset=thumbnail
Miniaturas para listados y galerías.
Configuración:
w=300, h=200c=fill, g=centerf=webp, q=80sharpen=5
300×200
Hero
preset=hero
Imágenes principales para headers y banners.
Configuración:
w=1200, h=600c=fill, g=centerf=webp, q=90sharpen=3
1200×600
Product
preset=product
Optimizado para imágenes de productos e-commerce.
Configuración:
w=400, h=400c=fill, g=centerf=webp, q=90sharpen=8
400×400
Gallery
preset=gallery
Para galerías de imágenes y portfolios.
Configuración:
w=600, h=400c=fit, g=centerf=webp, q=85auto=format
600×400
Social
preset=social
Optimizado para redes sociales y sharing.
Configuración:
w=1200, h=630c=fill, g=centerf=jpeg, q=85auto=quality
1200×630
🛠️ Usar Presets
Ejemplos prácticos de cómo implementar presets en tu aplicación:
<!-- 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">
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');
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
]);
}
}
/* 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:
Definir Necesidades
Identifica patrones repetitivos en tu aplicación y documenta los parámetros que usas frecuentemente.
Solicitar Preset
Contacta al administrador con la especificación detallada del preset y casos de uso.
Validar y Implementar
Una vez creado, el preset estará disponible en toda tu aplicación inmediatamente.
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.