github.com/status-im/status-go@v1.1.0/protocol/linkpreview_unfurler_image.go (about)

     1  package protocol
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	neturl "net/url"
     9  	"path"
    10  	"regexp"
    11  
    12  	"go.uber.org/zap"
    13  
    14  	"github.com/status-im/status-go/images"
    15  	"github.com/status-im/status-go/protocol/common"
    16  	"github.com/status-im/status-go/protocol/protobuf"
    17  )
    18  
    19  const (
    20  	maxImageSize = 1024 * 350
    21  )
    22  
    23  var imageURLRegexp = regexp.MustCompile(`(?i)^.+(png|jpg|jpeg|webp)$`)
    24  
    25  type ImageUnfurler struct {
    26  	url        *neturl.URL
    27  	logger     *zap.Logger
    28  	httpClient *http.Client
    29  }
    30  
    31  func NewImageUnfurler(URL *neturl.URL, logger *zap.Logger, httpClient *http.Client) *ImageUnfurler {
    32  	return &ImageUnfurler{
    33  		url:        URL,
    34  		logger:     logger,
    35  		httpClient: httpClient,
    36  	}
    37  }
    38  
    39  func compressImage(imgBytes []byte) ([]byte, error) {
    40  	smallest := imgBytes
    41  
    42  	img, err := images.DecodeImageData(imgBytes, bytes.NewReader(imgBytes))
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  
    47  	compressed := bytes.NewBuffer([]byte{})
    48  	err = images.CompressToFileLimits(compressed, img, images.DefaultBounds)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  
    53  	if len(compressed.Bytes()) < len(smallest) {
    54  		smallest = compressed.Bytes()
    55  	}
    56  
    57  	if len(smallest) > maxImageSize {
    58  		return nil, errors.New("image too large")
    59  	}
    60  
    61  	return smallest, nil
    62  }
    63  
    64  // IsSupportedImageURL detects whether a URL ends with one of the
    65  // supported image extensions. It provides a quick way to identify whether URLs
    66  // should be unfurled as images without needing to retrieve the full response
    67  // body first.
    68  func IsSupportedImageURL(url *neturl.URL) bool {
    69  	return imageURLRegexp.MatchString(url.Path)
    70  }
    71  
    72  // isSupportedImage returns true when payload is one of the supported image
    73  // types. In the future, we should differentiate between animated and
    74  // non-animated WebP because, currently, only static WebP can be processed by
    75  // functions in the status-go/images package.
    76  func isSupportedImage(payload []byte) bool {
    77  	return images.IsJpeg(payload) || images.IsPng(payload) || images.IsWebp(payload)
    78  }
    79  
    80  func (u *ImageUnfurler) Unfurl() (*common.LinkPreview, error) {
    81  	preview := newDefaultLinkPreview(u.url)
    82  	preview.Type = protobuf.UnfurledLink_IMAGE
    83  
    84  	headers := map[string]string{"user-agent": headerUserAgent}
    85  	imgBytes, err := fetchBody(u.logger, u.httpClient, u.url.String(), headers)
    86  	if err != nil {
    87  		return preview, err
    88  	}
    89  
    90  	if !isSupportedImage(imgBytes) {
    91  		return preview, fmt.Errorf("unsupported image type url='%s'", u.url.String())
    92  	}
    93  
    94  	compressedBytes, err := compressImage(imgBytes)
    95  	if err != nil {
    96  		return preview, fmt.Errorf("failed to compress image url='%s': %w", u.url.String(), err)
    97  	}
    98  
    99  	width, height, err := images.GetImageDimensions(compressedBytes)
   100  	if err != nil {
   101  		return preview, fmt.Errorf("could not get image dimensions url='%s': %w", u.url.String(), err)
   102  	}
   103  
   104  	dataURI, err := images.GetPayloadDataURI(compressedBytes)
   105  	if err != nil {
   106  		return preview, fmt.Errorf("could not build data URI url='%s': %w", u.url.String(), err)
   107  	}
   108  
   109  	preview.Title = path.Base(u.url.Path)
   110  	preview.Thumbnail.Width = width
   111  	preview.Thumbnail.Height = height
   112  	preview.Thumbnail.DataURI = dataURI
   113  
   114  	return preview, nil
   115  }