github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/attachments/preprocess.go (about) 1 package attachments 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/http" 8 "os" 9 "path/filepath" 10 "strings" 11 12 "github.com/keybase/client/go/chat/globals" 13 "github.com/keybase/client/go/libkb" 14 15 "github.com/keybase/client/go/chat/types" 16 "github.com/keybase/client/go/chat/utils" 17 18 "github.com/keybase/client/go/protocol/chat1" 19 "golang.org/x/net/context" 20 ) 21 22 type Dimension struct { 23 Width int `json:"width"` 24 Height int `json:"height"` 25 } 26 27 func (d *Dimension) Empty() bool { 28 return d.Width == 0 && d.Height == 0 29 } 30 31 func (d *Dimension) Encode() string { 32 if d.Width == 0 && d.Height == 0 { 33 return "" 34 } 35 enc, err := json.Marshal(d) 36 if err != nil { 37 return "" 38 } 39 return string(enc) 40 } 41 42 type Preprocess struct { 43 // Detected content type of the input 44 ContentType string 45 // May differ from the caller's view if we convert the input 46 Filename string 47 // Only set if a conversion of the input bytes happens 48 SrcDat []byte 49 50 Preview []byte 51 PreviewContentType string 52 BaseDim *Dimension 53 BaseDurationMs int 54 BaseIsAudio bool 55 PreviewDim *Dimension 56 PreviewAudioAmps []float64 57 PreviewDurationMs int 58 } 59 60 func (p *Preprocess) BaseMetadata() chat1.AssetMetadata { 61 if p.BaseDim == nil || p.BaseDim.Empty() { 62 return chat1.AssetMetadata{} 63 } 64 if p.BaseDurationMs > 0 { 65 return chat1.NewAssetMetadataWithVideo(chat1.AssetMetadataVideo{ 66 Width: p.BaseDim.Width, 67 Height: p.BaseDim.Height, 68 DurationMs: p.BaseDurationMs, 69 IsAudio: p.BaseIsAudio, 70 }) 71 } 72 return chat1.NewAssetMetadataWithImage(chat1.AssetMetadataImage{ 73 Width: p.BaseDim.Width, 74 Height: p.BaseDim.Height, 75 }) 76 } 77 78 func (p *Preprocess) PreviewMetadata() chat1.AssetMetadata { 79 if p.PreviewDim == nil || p.PreviewDim.Empty() { 80 return chat1.AssetMetadata{} 81 } 82 if p.PreviewDurationMs > 0 { 83 return chat1.NewAssetMetadataWithVideo(chat1.AssetMetadataVideo{ 84 Width: p.PreviewDim.Width, 85 Height: p.PreviewDim.Height, 86 DurationMs: p.PreviewDurationMs, 87 }) 88 } 89 return chat1.NewAssetMetadataWithImage(chat1.AssetMetadataImage{ 90 Width: p.PreviewDim.Width, 91 Height: p.PreviewDim.Height, 92 AudioAmps: p.PreviewAudioAmps, 93 }) 94 } 95 96 func (p *Preprocess) Export(getLocation func() *chat1.PreviewLocation) (res chat1.MakePreviewRes, err error) { 97 res = chat1.MakePreviewRes{ 98 MimeType: p.ContentType, 99 Location: getLocation(), 100 } 101 if p.PreviewContentType != "" { 102 res.PreviewMimeType = &p.PreviewContentType 103 } 104 md := p.PreviewMetadata() 105 var empty chat1.AssetMetadata 106 if md != empty { 107 res.Metadata = &md 108 } 109 baseMd := p.BaseMetadata() 110 if baseMd != empty { 111 res.BaseMetadata = &baseMd 112 } 113 return res, nil 114 } 115 116 func processCallerPreview(ctx context.Context, g *globals.Context, callerPreview chat1.MakePreviewRes) (p Preprocess, err error) { 117 ltyp, err := callerPreview.Location.Ltyp() 118 if err != nil { 119 return p, err 120 } 121 switch ltyp { 122 case chat1.PreviewLocationTyp_BYTES: 123 source := callerPreview.Location.Bytes() 124 p.Preview = make([]byte, len(source)) 125 copy(p.Preview, source) 126 case chat1.PreviewLocationTyp_FILE: 127 f, err := os.Open(callerPreview.Location.File()) 128 if err != nil { 129 return p, err 130 } 131 defer f.Close() 132 if p.Preview, err = io.ReadAll(f); err != nil { 133 return p, err 134 } 135 case chat1.PreviewLocationTyp_URL: 136 resp, err := libkb.ProxyHTTPGet(g.ExternalG(), g.Env, callerPreview.Location.Url(), "PreviewLocation") 137 if err != nil { 138 return p, err 139 } 140 defer resp.Body.Close() 141 if p.Preview, err = io.ReadAll(resp.Body); err != nil { 142 return p, err 143 } 144 default: 145 return p, fmt.Errorf("unknown preview location: %v", ltyp) 146 } 147 p.ContentType = callerPreview.MimeType 148 if callerPreview.PreviewMimeType != nil { 149 p.PreviewContentType = *callerPreview.PreviewMimeType 150 } 151 if callerPreview.Metadata != nil { 152 typ, err := callerPreview.Metadata.AssetType() 153 if err != nil { 154 return p, err 155 } 156 switch typ { 157 case chat1.AssetMetadataType_IMAGE: 158 p.PreviewDim = &Dimension{ 159 Width: callerPreview.Metadata.Image().Width, 160 Height: callerPreview.Metadata.Image().Height, 161 } 162 p.PreviewAudioAmps = callerPreview.Metadata.Image().AudioAmps 163 case chat1.AssetMetadataType_VIDEO: 164 p.PreviewDurationMs = callerPreview.Metadata.Video().DurationMs 165 p.PreviewDim = &Dimension{ 166 Width: callerPreview.Metadata.Video().Width, 167 Height: callerPreview.Metadata.Video().Height, 168 } 169 } 170 } 171 if callerPreview.BaseMetadata != nil { 172 typ, err := callerPreview.BaseMetadata.AssetType() 173 if err != nil { 174 return p, err 175 } 176 switch typ { 177 case chat1.AssetMetadataType_IMAGE: 178 p.BaseDim = &Dimension{ 179 Width: callerPreview.BaseMetadata.Image().Width, 180 Height: callerPreview.BaseMetadata.Image().Height, 181 } 182 case chat1.AssetMetadataType_VIDEO: 183 p.BaseDurationMs = callerPreview.BaseMetadata.Video().DurationMs 184 p.BaseIsAudio = callerPreview.BaseMetadata.Video().IsAudio 185 p.BaseDim = &Dimension{ 186 Width: callerPreview.BaseMetadata.Video().Width, 187 Height: callerPreview.BaseMetadata.Video().Height, 188 } 189 } 190 } 191 return p, nil 192 } 193 194 func DetectMIMEType(ctx context.Context, src ReadResetter, filename string) (res string, err error) { 195 head := make([]byte, 512) 196 _, err = io.ReadFull(src, head) 197 switch err { 198 case nil: 199 case io.EOF, io.ErrUnexpectedEOF: 200 return "", nil 201 default: 202 return res, err 203 } 204 205 res = http.DetectContentType(head) 206 if err = src.Reset(); err != nil { 207 return res, err 208 } 209 // MIME type detection failed us, try using an extension map 210 if res == "application/octet-stream" { 211 ext := strings.ToLower(filepath.Ext(filename)) 212 if typ, ok := mimeTypes[ext]; ok { 213 res = typ 214 } 215 } 216 return res, nil 217 } 218 219 func PreprocessAsset(ctx context.Context, g *globals.Context, log utils.DebugLabeler, src ReadResetter, filename string, 220 nvh types.NativeVideoHelper, callerPreview *chat1.MakePreviewRes) (p Preprocess, err error) { 221 if callerPreview != nil && callerPreview.Location != nil { 222 log.Debug(ctx, "preprocessAsset: caller provided preview, using that") 223 if p, err = processCallerPreview(ctx, g, *callerPreview); err != nil { 224 log.Debug(ctx, "preprocessAsset: failed to process caller preview, making fresh one: %s", err) 225 } else { 226 return p, nil 227 } 228 } 229 defer func() { 230 err := src.Reset() 231 if err != nil { 232 log.Debug(ctx, "preprocessAsset: reset failed: %+v", err) 233 } 234 }() 235 236 p.Filename = filename 237 p.ContentType, err = DetectMIMEType(ctx, src, filename) 238 if err != nil { 239 return p, err 240 } 241 242 // Convert heif to jpeg when possible 243 if p.ContentType == "image/heif" { 244 shouldConvertHEIC, err := utils.GetGregorBool(ctx, g, utils.ConvertHEICGregorKey, true) 245 if err != nil { 246 return p, err 247 } 248 if shouldConvertHEIC { 249 dat, err := HEICToJPEG(ctx, log, filename) 250 if err != nil { 251 return p, err 252 } 253 if dat != nil { 254 p.Filename = strings.TrimSuffix(filename, filepath.Ext(filename)) + ".jpeg" 255 p.ContentType = "image/jpeg" 256 p.SrcDat = dat 257 258 src = NewBufReadResetter(dat) 259 } 260 } 261 } 262 log.Debug(ctx, "preprocessAsset: detected attachment content type %s", p.ContentType) 263 previewRes, err := Preview(ctx, log, src, p.ContentType, p.Filename, nvh) 264 if err != nil { 265 log.Debug(ctx, "preprocessAsset: error making preview: %s", err) 266 return p, err 267 } 268 if previewRes != nil { 269 log.Debug(ctx, "preprocessAsset: made preview for attachment asset: content type: %s", 270 previewRes.ContentType) 271 p.Preview = previewRes.Source 272 p.PreviewContentType = previewRes.ContentType 273 if previewRes.BaseWidth > 0 || previewRes.BaseHeight > 0 { 274 p.BaseDim = &Dimension{Width: previewRes.BaseWidth, Height: previewRes.BaseHeight} 275 } 276 if previewRes.PreviewWidth > 0 || previewRes.PreviewHeight > 0 { 277 p.PreviewDim = &Dimension{Width: previewRes.PreviewWidth, Height: previewRes.PreviewHeight} 278 } 279 p.BaseDurationMs = previewRes.BaseDurationMs 280 p.PreviewDurationMs = previewRes.PreviewDurationMs 281 } 282 283 return p, nil 284 }