| name | adonis-attachment-variants |
| description | Use when configuring or debugging variants and converters with @jrmc/adonis-attachment — image resize/format/blurhash (sharp), video/PDF/document thumbnails (ffmpeg, poppler, LibreOffice), custom converters, variant storage paths, processing queue, variant lifecycle events, and regenerating variants. |
AdonisJS Attachment — variants & converters
Variants are derived files (thumbnails, resized images, previews) generated from an attachment. They are declared as converters in config/attachment.ts, generated asynchronously in a queue after save (for variants listed in the decorator's variants option) or on the fly by the attachments route.
Full documentation: https://adonis-attachment.jrmc.dev
Configuration
import { defineConfig } from '@jrmc/adonis-attachment'
import { InferConverters } from '@jrmc/adonis-attachment/types/config'
const attachmentConfig = defineConfig({
converters: {
thumbnail: {
resize: 300,
format: 'webp',
},
medium: {
resize: 600,
blurhash: true,
},
},
})
export default attachmentConfig
declare module '@jrmc/adonis-attachment' {
interface AttachmentVariants extends InferConverters<typeof attachmentConfig> {}
}
The declare module block gives typed variant names — keep it in sync.
When no converter is specified, the autodetect converter picks the right one from the mime type (image / PDF / document / video). Since v5, converter options are flat (no options: {} wrapper needed, though still accepted).
Then in the model, list the variants to pre-generate:
@attachment({ variants: ['thumbnail', 'medium'] })
declare avatar: Attachment | null
Pre-generation is optional when using router.attachments() — missing variants are generated on demand.
Image converter (sharp)
Requires npm install sharp.
thumbnail: {
converter: () => import('@jrmc/adonis-attachment/converters/image_converter'),
resize: 300,
format: 'jpeg',
autoOrient: false,
blurhash: true,
}
resize/format accept everything the sharp API accepts (https://sharp.pixelplumbing.com). The blurhash string is stored on the variant JSON (variant.blurhash).
Video / PDF / document thumbnails
These converters shell out to system binaries (since v5, no npm wrapper packages are needed):
| Converter | Import path | Binaries required |
|---|
| Video thumbnail | @jrmc/adonis-attachment/converters/video_thumbnail_converter | ffmpeg, ffprobe |
| PDF thumbnail | @jrmc/adonis-attachment/converters/pdf_thumbnail_converter | poppler (pdftoppm, pdfinfo) |
| Document thumbnail | @jrmc/adonis-attachment/converters/document_thumbnail_converter | LibreOffice (soffice) |
sudo apt-get install ffmpeg poppler-data poppler-utils
brew install ffmpeg poppler
All three accept the image converter options (format, resize, ...) to post-process the generated thumbnail:
preview: {
converter: () => import('@jrmc/adonis-attachment/converters/pdf_thumbnail_converter'),
format: 'webp',
resize: 720,
}
Custom binary paths (deployments, precompiled binaries):
const attachmentConfig = defineConfig({
bin: {
ffmpegPath: app.makePath('bin/ffmpeg'),
ffprobePath: app.makePath('bin/ffprobe'),
pdftoppmPath: '...',
pdfinfoPath: '...',
sofficePath: '...',
},
converters: { },
})
Custom converter
node ace make:converter gif2webp
Creates app/converters/gif_2_webp_converter.ts. A converter extends the base class and returns a Buffer or a file path:
import type { ConverterAttributes } from '@jrmc/adonis-attachment/types/converter'
import type { Input } from '@jrmc/adonis-attachment/types/input'
import Converter from '@jrmc/adonis-attachment/converters/converter'
import sharp from 'sharp'
export default class Gif2WebpConverter extends Converter {
async handle({ input }: ConverterAttributes): Promise<Input> {
return sharp(input, { animated: true, pages: -1 }).webp().toBuffer()
}
}
this.options holds the converter config; this.binPaths holds the bin config. Register it:
converters: {
gif: { converter: () => import('#converters/gif_2_webp_converter') },
}
Variant storage paths (v5.1+)
By default variants are stored next to the original: avatars/image.jpg/thumbnail.webp.
const attachmentConfig = defineConfig({
variant: {
basePath: 'variants',
ignoreFolder: true,
},
converters: { },
})
Queue
Variant generation runs async in a @poppinss/defer queue, one task per model attribute. Concurrency (default 1):
defineConfig({ queue: { concurrency: 2 }, converters: { } })
Queue callbacks (in a preload file):
import { attachmentManager } from '@jrmc/adonis-attachment'
attachmentManager.queue.onError = (error, task) => { }
attachmentManager.queue.taskCompleted = (task) => { }
attachmentManager.queue.drained = () => { }
Waiting for completion (tests):
const notifier = new Promise((resolve) => { attachmentManager.queue.drained = resolve })
await notifier
timeout config (default 30000 ms) bounds each conversion operation.
Events (v5.1+)
Emitted via the AdonisJS emitter: attachment:variant_started, attachment:variant_completed, attachment:variant_failed.
import emitter from '@adonisjs/core/services/emitter'
import type { AttachmentEventPayload } from '@jrmc/adonis-attachment/types/event'
emitter.on('attachment:variant_failed', (payload: AttachmentEventPayload) => {
})
Regenerating variants
RegenerateService re-creates variants declared in the model's variants option:
import { inject } from '@adonisjs/core'
import { RegenerateService } from '@jrmc/adonis-attachment'
@inject()
async regenerate(regenerate: RegenerateService) {
await regenerate.row(user).run()
await regenerate.model(User).run()
await regenerate.model(User, { variants: ['thumbnail'], attributes: ['avatar'] }).run()
}
Typical use: after adding/changing a converter in config, run a regeneration from an ace command or a route.