Image Variants
The image variants module handles thumbnail generation and other derivative images.
Installation
Install the module using the CLI:
bin/goframe generate module image-variant
This scaffolds the service, workflow and database migrations required to manage image variants.
This module requires vips to be installed on the machine.
What’s included
The generator creates:
- Types and scope helpers under
internal/types
. - An
ImageVariantService
ininternal/service
. - A Temporal workflow and activity to generate variants.
- Database migrations creating
image_variant_sets
andimage_variants
tables. - A provider that ensures vips is closed at the application shutdown.
How it works
ImageVariantService.CreateImage
uploads the original attachment (synchronously) and starts the workflow to create the configured variants (asynchronously).
Each variant is processed using govips and stored through the storage provider.
Here is a simplified handler showcasing its usage:
type UserHandler struct {
// other dependencies
imgVariantSvc types.ImageVariantService // service to create image variants
}
type UserUpdateAttachmentRequest struct {
User *types.User `ctx:"user"`
Attachment *multipart.FileHeader `file:"attachment"`
}
// ...
var req UserUpdateAttachmentRequest
if err := params.Bind(&req, r); err != nil {
return nil, err
}
if req.Attachment == nil {
return nil, errors.New("attachment required")
}
set, err := h.imgVariantSvc.CreateImage(r.Context(), types.CreateImageVariantParams{
ContentMultipartFileHeader: req.Attachment,
Properties: map[string]types.ImageVariantProperties{ // the key is the variant name
"thumbnail": { Width: 100 },
"large": { Width: 800 },
"medium": { Width: 400 },
},
ResourceUpdater: func(ctx context.Context, imageVariantSetID string) error {
req.User.AttachmentSetID = imageVariantSetID
return h.userSvc.UpdateUser(ctx, req.User, "attachment_set_id")
},
})
if err != nil {
return nil, err
}
req.User.AttachmentSet = set
Variant properties (ImageVariantProperties)
flag | description |
---|---|
width | set the width of the image, if no height is set, the height will be calculated proportionally |
height | set the height of the image, if no width is set, the width will be calculated proportionally |
ratio | set the ratio of the image, if no width or height is set, the width and height will be calculated proportionally |
rotation | set the rotation of the image in degrees |
flipHorizontal | flip the image horizontally |
flipVertical | flip the image vertically |
quality | set the quality of the image (0 to 1 |
format | set the format of the image (jpg, png, webp) |
What is cool about the fact that you generate this as a library in your code is that you can add your own properties (example: blur?)
Has one set of variants
type User struct {
ID int64 `json:"id"`
AttachmentSetID int64
AttachmentSet *types.ImageVariantSet
}
// to load it
var user types.User
err := dbutil.DB(ctx, s.db).
Scopes(types.ScopeImageVariantsLoader("AttachmentSet")).
First(&user).Error
When loading a set of variants for a has one relation, use the types.ScopeImageVariantsLoader
scope to preload the set and its variants.
You have some functional option to do not load the original attachment or to load only specific variant name.
Has many sets of variants
type User struct {
ID int64 `json:"id"`
AttachmentsSet []types.ImageVariantSet `gorm:"polymorphicType:Kind;polymorphicId:KindID;polymorphicValue:user_attachments"`
}
// to load it
err := dbutil.DB(ctx, s.db).
Scopes(types.ScopeImageVariantsLoader("AttachmentsSet",
types.ScopeImageVariantsLoadWithVariantsNames("thumbnail"),
types.ScopeImageVariantsLoadWithHasMany(true))).
First(&user).Error
It’s pretty similar to the has one relation, but you can load multiple sets of variants. You can also specify which variant names to load using the ScopeImageVariantsLoadWithVariantsNames
scope.
Insert with has many
Similar to the example above, but you need to add kind and kind id for the image variant set, instead of providing a resource updater function.
type UserUpdateAttachmentRequest struct {
User *types.User `ctx:"goframe.user"`
Attachments []*multipart.FileHeader `files:"attachments"`
}
//...
var req UserUpdateAttachmentRequest
if err := params.Bind(&req, r); err != nil {
return nil, err
}
if req.Attachments == nil {
return nil, errors.New("attachment required")
}
var attachments []*types.ImageVariantSet
for _, attachment := range req.Attachments {
set, err := h.imgVariantSvc.CreateImage(r.Context(), types.CreateImageVariantParams{
ContentMultipartFileHeader: attachment,
Properties: map[string]types.ImageVariantProperties{
"thumbnail": { Width: 100 },
"large": { Width: 800 },
"medium": { Width: 400 },
},
ImageVariantSetKind: "user_attachments", // super important to set the kind
ImageVariantSetKindID: req.User.ID, // the ID of the resource
})
if err != nil {
return nil, err
}
attachments = append(attachments, set)
}
req.User.AttachmentsSet = append(req.User.AttachmentsSet, attachments...)