// Package webp encodes WebP images and writes WebP variants of attachment // thumbnails next to them, through github.com/gen2brain/webp (no cgo: the // system libwebp through purego when it is installed, else libwebp // transpiled to Go). It is a separate package so that only binaries that // import it link the encoder. package webp import ( "bytes" "context" "errors" "fmt" "image" "io" "git.golem15.com/golem15/summercms/modules/lagoon/attach" encoder "github.com/gen2brain/webp" "gocloud.dev/blob" "gocloud.dev/gcerrors" ) // Quality is the encoder quality of Encode, on libwebp's 0 to 100 scale. const Quality = 80 // Format is the extension of a WebP variant (attach.VariantKey). const Format = "webp" // ContentType is the content type a variant is stored with. const ContentType = "image/webp" // maxThumbBytes caps the thumbnail Variant reads, like attach caps an // original. const maxThumbBytes = 32 << 20 // Encode writes img to w as a lossy WebP at Quality. func Encode(w io.Writer, img image.Image) error { if img == nil { return errors.New("webp: image is nil") } return encoder.Encode(w, img, encoder.Options{Quality: Quality}) } // encodeImage is Encode; tests replace it to fail an encode. var encodeImage = Encode // Variant writes the WebP variant of f's thumbnail at w by h in mode (see // attach.File.ThumbKey, which makes the thumbnail first when it is missing) // next to that thumbnail, under attach.VariantKey(thumbKey, "webp"), and // returns the variant's key. A variant that already exists is returned as // it is, so a second call writes nothing. The thumbnail is encoded in // memory before anything is written, and the key is deleted when the write // or its close fails, so a failed encode leaves no partial file. Errors of // ThumbKey are returned as they are. func Variant(ctx context.Context, bucket *blob.Bucket, f *attach.File, w, h int, mode string) (string, error) { thumbKey, err := f.ThumbKey(ctx, bucket, w, h, mode) if err != nil { return "", err } key := attach.VariantKey(thumbKey, Format) if key == "" { return "", fmt.Errorf("webp: no variant key for %q", thumbKey) } exists, err := bucket.Exists(ctx, key) if err != nil { return "", fmt.Errorf("webp: variant exists: %w", err) } if exists { return key, nil } r, err := bucket.NewReader(ctx, thumbKey, nil) if err != nil { return "", fmt.Errorf("webp: read thumbnail: %w", err) } raw, err := io.ReadAll(io.LimitReader(r, maxThumbBytes)) closeErr := r.Close() if err != nil { return "", fmt.Errorf("webp: read thumbnail: %w", err) } if closeErr != nil { return "", fmt.Errorf("webp: read thumbnail: %w", closeErr) } img, _, err := image.Decode(bytes.NewReader(raw)) if err != nil { return "", fmt.Errorf("webp: decode thumbnail %q: %w", thumbKey, err) } var buf bytes.Buffer if err := encodeImage(&buf, img); err != nil { return "", fmt.Errorf("webp: encode %q: %w", thumbKey, err) } wr, err := bucket.NewWriter(ctx, key, &blob.WriterOptions{ContentType: ContentType}) if err != nil { return "", fmt.Errorf("webp: variant writer: %w", err) } _, writeErr := wr.Write(buf.Bytes()) closeErr = wr.Close() if writeErr != nil || closeErr != nil { if delErr := bucket.Delete(ctx, key); delErr != nil && gcerrors.Code(delErr) != gcerrors.NotFound { return "", fmt.Errorf("webp: delete partial variant %q: %w", key, delErr) } if writeErr != nil { return "", fmt.Errorf("webp: write variant: %w", writeErr) } return "", fmt.Errorf("webp: write variant: %w", closeErr) } return key, nil }