Skip to Content
StorageImage Variants

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 in internal/service.
  • A Temporal workflow and activity to generate variants.
  • Database migrations creating image_variant_sets and image_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)

flagdescription
widthset the width of the image, if no height is set, the height will be calculated proportionally
heightset the height of the image, if no width is set, the width will be calculated proportionally
ratioset the ratio of the image, if no width or height is set, the width and height will be calculated proportionally
rotationset the rotation of the image in degrees
flipHorizontalflip the image horizontally
flipVerticalflip the image vertically
qualityset the quality of the image (0 to 1
formatset 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...)
Last updated on