github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/attachments/preview_darwin.go (about) 1 //go:build darwin 2 // +build darwin 3 4 package attachments 5 6 /* 7 #cgo CFLAGS: -x objective-c -fobjc-arc 8 #cgo LDFLAGS: -framework AVFoundation -framework CoreFoundation -framework ImageIO -framework CoreMedia -framework Foundation -framework CoreGraphics -framework AppKit -framework UniformTypeIdentifiers -lobjc 9 10 #include <TargetConditionals.h> 11 #include <AVFoundation/AVFoundation.h> 12 #include <CoreFoundation/CoreFoundation.h> 13 #include <Foundation/Foundation.h> 14 #include <ImageIO/ImageIO.h> 15 #include <UniformTypeIdentifiers/UniformTypeIdentifiers.h> 16 #if TARGET_OS_IPHONE 17 #include <MobileCoreServices/MobileCoreServices.h> 18 #include <UIKit/UIKit.h> 19 #else 20 #include <AppKit/AppKit.h> 21 #endif 22 23 NSData* imageData = NULL; 24 int duration = 0; 25 26 void MakeVideoThumbnail(const char* inFilename) { 27 NSString* filename = [NSString stringWithUTF8String:inFilename]; 28 NSURL *videoURL = [NSURL fileURLWithPath:filename]; 29 30 AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:videoURL options:nil]; 31 AVAssetImageGenerator *generateImg = [[AVAssetImageGenerator alloc] initWithAsset:asset]; 32 [generateImg setAppliesPreferredTrackTransform:YES]; 33 NSError *error = NULL; 34 CMTime time = CMTimeMake(1, 1); 35 CGImageRef image = [generateImg copyCGImageAtTime:time actualTime:NULL error:&error]; 36 duration = CMTimeGetSeconds([asset duration]); 37 38 CFMutableDataRef mutableData = CFDataCreateMutable(NULL, 0); 39 CGImageDestinationRef idst = CGImageDestinationCreateWithData( 40 mutableData, (CFStringRef)UTTypeJPEG.identifier, 1, NULL 41 ); 42 NSInteger exif = 1; 43 CGFloat compressionQuality = 0.70; 44 NSDictionary *props = [ 45 [NSDictionary alloc] 46 initWithObjectsAndKeys:[NSNumber numberWithFloat:compressionQuality], 47 kCGImageDestinationLossyCompressionQuality, 48 [NSNumber numberWithInteger:exif], 49 kCGImagePropertyOrientation, nil 50 ]; 51 CGImageDestinationAddImage(idst, image, (CFDictionaryRef)props); 52 CGImageDestinationFinalize(idst); 53 imageData = [NSData dataWithData:(__bridge_transfer NSData *)mutableData]; 54 CFRelease(idst); 55 CGImageRelease(image); 56 } 57 58 const void* ImageData() { 59 return [imageData bytes]; 60 } 61 62 int ImageLength() { 63 return [imageData length]; 64 } 65 66 int VideoDuration() { 67 return duration; 68 } 69 70 #if TARGET_OS_IPHONE 71 int HEICToJPEG(const char* inFilename) { 72 NSString* filename = [NSString stringWithUTF8String:inFilename]; 73 UIImage* heicImage = [UIImage imageWithContentsOfFile:filename]; 74 if (heicImage) { 75 imageData = UIImageJPEGRepresentation(heicImage, 1.0); 76 return 0; 77 } 78 return 1; 79 } 80 #else 81 int HEICToJPEG(const char* inFilename) { 82 NSString* filename = [NSString stringWithUTF8String:inFilename]; 83 NSImage *heicImage = [[NSImage alloc] initWithContentsOfFile:filename]; 84 if (heicImage) { 85 NSArray<NSImageRep *> *imageReps = [heicImage representations]; 86 NSImageRep *imageRep = [imageReps firstObject]; 87 if (imageRep) { 88 NSBitmapImageRep *bitmapRep = (NSBitmapImageRep *)imageRep; 89 if (bitmapRep) { 90 imageData = [bitmapRep representationUsingType:NSBitmapImageFileTypeJPEG properties:@{}]; 91 return 0; 92 } 93 } 94 } 95 return 1; 96 } 97 #endif 98 */ 99 import "C" 100 import ( 101 "bytes" 102 "errors" 103 "io" 104 "unsafe" 105 106 "github.com/keybase/client/go/chat/types" 107 "github.com/keybase/client/go/chat/utils" 108 "golang.org/x/net/context" 109 ) 110 111 func previewVideo(ctx context.Context, log utils.DebugLabeler, src io.Reader, 112 basename string, nvh types.NativeVideoHelper) (res *PreviewRes, err error) { 113 defer log.Trace(ctx, &err, "previewVideo")() 114 cbasename := C.CString(basename) 115 defer C.free(unsafe.Pointer(cbasename)) 116 C.MakeVideoThumbnail(cbasename) 117 duration := int(C.VideoDuration()) 118 if duration < 1 { 119 // clamp to 1 so we know it is a video, but also not to compute a duration for it 120 duration = 1 121 } else { 122 duration *= 1000 123 } 124 log.Debug(ctx, "previewVideo: length: %d duration: %ds", C.ImageLength(), duration) 125 if C.ImageLength() == 0 { 126 return res, errors.New("no data returned from native") 127 } 128 localDat := make([]byte, C.ImageLength()) 129 copy(localDat, (*[1 << 30]byte)(C.ImageData())[0:C.ImageLength()]) 130 imagePreview, err := previewImage(ctx, log, bytes.NewReader(localDat), basename, "image/jpeg") 131 if err != nil { 132 return res, err 133 } 134 return &PreviewRes{ 135 Source: imagePreview.Source, 136 ContentType: "image/jpeg", 137 BaseWidth: imagePreview.BaseWidth, 138 BaseHeight: imagePreview.BaseHeight, 139 BaseDurationMs: duration, 140 PreviewHeight: imagePreview.PreviewHeight, 141 PreviewWidth: imagePreview.PreviewWidth, 142 }, nil 143 } 144 145 func HEICToJPEG(ctx context.Context, log utils.DebugLabeler, basename string) (dat []byte, err error) { 146 defer log.Trace(ctx, &err, "HEICToJPEG")() 147 cbasename := C.CString(basename) 148 defer C.free(unsafe.Pointer(cbasename)) 149 ret := C.HEICToJPEG(cbasename) 150 log.Debug(ctx, "HEICToJPEG: length: %d", C.ImageLength()) 151 if ret != 0 || C.ImageLength() == 0 { 152 log.Debug(ctx, "unable to convert heic to jpeg") 153 return nil, nil 154 } 155 dat = make([]byte, C.ImageLength()) 156 copy(dat, (*[1 << 30]byte)(C.ImageData())[0:C.ImageLength()]) 157 return dat, nil 158 } 159 160 func LinkNoop() {}