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 }