[vlc-commits] [Git][videolan/vlc][master] macosx: generate images for radio country flags

Steve Lhomme (@robUx4) gitlab at videolan.org
Mon Aug 3 15:44:44 UTC 2026



Steve Lhomme pushed to branch master at VideoLAN / VLC


Commits:
343621fe by Serhii Bykov at 2026-08-03T15:03:47+00:00
macosx: generate images for radio country flags

- - - - -


8 changed files:

- modules/gui/macosx/extensions/NSImage+VLCAdditions.h
- modules/gui/macosx/extensions/NSImage+VLCAdditions.m
- modules/gui/macosx/extensions/NSString+Helpers.h
- modules/gui/macosx/extensions/NSString+Helpers.m
- modules/gui/macosx/library/VLCInputItem.h
- modules/gui/macosx/library/VLCInputItem.m
- modules/gui/macosx/library/VLCLibraryImageCache.m
- modules/gui/macosx/library/media-source/VLCMediaSourceBaseDataSource.m


Changes:

=====================================
modules/gui/macosx/extensions/NSImage+VLCAdditions.h
=====================================
@@ -82,6 +82,8 @@ NS_ASSUME_NONNULL_BEGIN
 + (NSArray<NSValue *> *)framesForCompositeImageSquareGridWithImages:(NSArray<NSImage *> * const)images
                                                                size:(const NSSize)size
                                                       gridItemCount:(const NSUInteger)gridItemCount;
++ (nullable NSImage *)flagImageForCountryCode:(NSString *)countryCode
+                                         size:(NSSize)size;
 
 - (instancetype)imageTintedWithColor:(NSColor *)color;
 


=====================================
modules/gui/macosx/extensions/NSImage+VLCAdditions.m
=====================================
@@ -22,9 +22,65 @@
 
 #import "NSImage+VLCAdditions.h"
 
+#import "NSString+Helpers.h"
+
 #import <QuickLook/QuickLook.h>
 #import <QuickLookThumbnailing/QuickLookThumbnailing.h>
 
+static NSImage *ImageFromEmoji(NSString *emoji, NSSize size)
+{
+    const NSInteger pixelsWide = MAX((NSInteger)ceil(size.width), 1);
+    const NSInteger pixelsHigh = MAX((NSInteger)ceil(size.height), 1);
+    NSBitmapImageRep * const bitmapImageRep =
+        [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
+                                               pixelsWide:pixelsWide
+                                               pixelsHigh:pixelsHigh
+                                            bitsPerSample:8
+                                          samplesPerPixel:4
+                                                 hasAlpha:YES
+                                                 isPlanar:NO
+                                           colorSpaceName:NSCalibratedRGBColorSpace
+                                              bytesPerRow:0
+                                             bitsPerPixel:0];
+    if (!bitmapImageRep) {
+        return nil;
+    }
+    bitmapImageRep.size = size;
+
+    NSGraphicsContext * const context =
+        [NSGraphicsContext graphicsContextWithBitmapImageRep:bitmapImageRep];
+    if (!context) {
+        return nil;
+    }
+
+    NSGraphicsContext * const previousContext = NSGraphicsContext.currentContext;
+    NSGraphicsContext.currentContext = context;
+    [context saveGraphicsState];
+    [NSColor.whiteColor setFill];
+    NSRectFill(NSMakeRect(0., 0., size.width, size.height));
+
+    const CGFloat initialFontSize = MIN(size.width, size.height);
+    NSDictionary *attributes = @{ NSFontAttributeName : [NSFont systemFontOfSize:initialFontSize] };
+    NSSize emojiSize = [emoji sizeWithAttributes:attributes];
+    if (emojiSize.width > size.width || emojiSize.height > size.height) {
+        const CGFloat scale = MIN(size.width / emojiSize.width, size.height / emojiSize.height);
+        const CGFloat fontSize = floor(initialFontSize * scale);
+        attributes = @{ NSFontAttributeName : [NSFont systemFontOfSize:MAX(fontSize, 1.)] };
+        emojiSize = [emoji sizeWithAttributes:attributes];
+    }
+
+    const NSPoint point = NSMakePoint((size.width - emojiSize.width) / 2.,
+                                     (size.height - emojiSize.height) / 2.);
+    [emoji drawAtPoint:point withAttributes:attributes];
+    [context flushGraphics];
+    [context restoreGraphicsState];
+    NSGraphicsContext.currentContext = previousContext;
+
+    NSImage * const image = [[NSImage alloc] initWithSize:size];
+    [image addRepresentation:bitmapImageRep];
+    return image;
+}
+
 @implementation NSImage(VLCAdditions)
 
 + (NSImage *)VLCAppIconImage
@@ -187,8 +243,24 @@
     return [NSImage imageNamed:@"repeatOff"];
 }
 
-+ (void)quickLookPreviewForLocalPath:(NSString *)path 
-                            withSize:(NSSize)size 
++ (nullable NSImage *)flagImageForCountryCode:(NSString *)countryCode
+                                         size:(NSSize)size
+{
+    if (size.width <= 0 || size.height <= 0) {
+        return nil;
+    }
+
+    NSString * const uppercaseCountryCode = countryCode.uppercaseString;
+    NSString * const emoji = flagEmojiStringForCountryCode(uppercaseCountryCode);
+    if (emoji == nil) {
+        return nil;
+    }
+
+    return ImageFromEmoji(emoji, size);
+}
+
++ (void)quickLookPreviewForLocalPath:(NSString *)path
+                            withSize:(NSSize)size
                    completionHandler:(void (^)(NSImage *))completionHandler
 {
     NSURL * const pathUrl = [NSURL fileURLWithPath:path];


=====================================
modules/gui/macosx/extensions/NSString+Helpers.h
=====================================
@@ -200,6 +200,11 @@ NSString * getVolumeTypeFromMountPath(NSString *mountPath);
 
 NSString * getBSDNodeFromMountPath(NSString *mountPath);
 
+/**
+ * Converts an ISO 3166-1 alpha-2 country code to its flag emoji.
+ */
+NSString * flagEmojiStringForCountryCode(NSString *countryCode);
+
 /**
  * Converts VLC key string to a prettified version, for hotkey settings.
  * The returned string adapts similar how its done within the cocoa framework when setting this


=====================================
modules/gui/macosx/extensions/NSString+Helpers.m
=====================================
@@ -49,6 +49,34 @@ NSString *const kVLCMediaVideoTSFolder = @"VIDEO_TS";
 NSString *const kVLCMediaBDMVFolder = @"BDMV";
 NSString *const kVLCMediaUnknown = @"Unknown";
 
+NSString * flagEmojiStringForCountryCode(NSString *countryCode)
+{
+    NSString * const uppercaseCode = countryCode.uppercaseString;
+    if (uppercaseCode.length != 2) {
+        return nil;
+    }
+
+    NSMutableString * const emoji = [NSMutableString stringWithCapacity:2];
+    for (NSUInteger i = 0; i < uppercaseCode.length; i++) {
+        const unichar character = [uppercaseCode characterAtIndex:i];
+        if (character < 'A' || character > 'Z') {
+            return nil;
+        }
+
+        uint32_t const scalar = 0x1F1E6 + (character - 'A');
+        NSString * const regionalIndicator =
+            [[NSString alloc] initWithBytes:&scalar
+                                     length:sizeof(scalar)
+                                   encoding:NSUTF32LittleEndianStringEncoding];
+        if (regionalIndicator == nil) {
+            return nil;
+        }
+        [emoji appendString:regionalIndicator];
+    }
+
+    return emoji;
+}
+
 @implementation NSString (Helpers)
 
 + (instancetype)stringWithDuration:(vlc_tick_t)duration


=====================================
modules/gui/macosx/library/VLCInputItem.h
=====================================
@@ -81,6 +81,7 @@ extern NSString * const VLCInputItemCommonDataDifferingFlagString;
 @property (readonly, nullable) NSArray<NSString *> *finderTags;
 
 - (nullable NSString *)extraMetaForKey:(NSString *)key;
+- (nullable NSString *)radioCountryCodeForFlagArtwork;
 
 - (void)parseInputItem;
 - (void)cancelParsing;


=====================================
modules/gui/macosx/library/VLCInputItem.m
=====================================
@@ -429,6 +429,17 @@ static const struct input_item_parser_cbs_t parserCallbacks =
     }
 }
 
+- (nullable NSString *)radioCountryCodeForFlagArtwork
+{
+    NSURL * const url = [NSURL URLWithString:self.MRL];
+    if (![url.scheme isEqualToString:@"radio"]) {
+        return nil;
+    }
+
+    NSString * const countryCode = url.host.uppercaseString;
+    return flagEmojiStringForCountryCode(countryCode) ? countryCode : nil;
+}
+
 - (void)parseInputItem
 {
     const struct input_item_parser_cfg cfg = {


=====================================
modules/gui/macosx/library/VLCLibraryImageCache.m
=====================================
@@ -42,7 +42,6 @@ uint32_t kVLCDesiredThumbnailHeight = 512;
 float kVLCDefaultThumbnailPosition = .15;
 const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
 
-
 @interface VLCLibraryImageCache()
 {
     NSCache *_imageCache;
@@ -214,6 +213,32 @@ const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
     [VLCLibraryImageCache.sharedImageCache imageForInputItem:inputItem withCompletion:completionHandler];
 }
 
+- (void)imageForCountryCode:(NSString *)countryCode
+             withCompletion:(void(^)(const NSImage * _Nullable))completionHandler
+{
+    NSString * const normalizedCountryCode = countryCode.uppercaseString;
+    NSString * const cacheKey = [@"flag://" stringByAppendingString:normalizedCountryCode];
+    NSImage * const cachedImage = [_imageCache objectForKey:cacheKey];
+    if (cachedImage) {
+        completionHandler(cachedImage);
+        return;
+    }
+
+    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
+        const NSSize imageSize =
+            NSMakeSize(kVLCDesiredThumbnailWidth, kVLCDesiredThumbnailHeight);
+        NSImage * const image = [NSImage flagImageForCountryCode:normalizedCountryCode
+                                                           size:imageSize];
+        if (image) {
+            const NSUInteger cost = [VLCLibraryImageCache costForImage:image];
+            [self->_imageCache setObject:image forKey:cacheKey cost:cost];
+        }
+        dispatch_async(dispatch_get_main_queue(), ^{
+            completionHandler(image);
+        });
+    });
+}
+
 - (void)imageForInputItem:(VLCInputItem *)inputItem 
            withCompletion:(nonnull void (^)(const NSImage * _Nonnull))completionHandler
 {
@@ -225,8 +250,8 @@ const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
     [self generateImageForInputItem:inputItem withCompletion:completionHandler];
 }
 
-- (void)generateImageForInputItem:(VLCInputItem *)inputItem
-                   withCompletion:(void(^)(const NSImage *))completionHandler
+- (void)generateArtworkForInputItem:(VLCInputItem *)inputItem
+                     withCompletion:(void(^)(const NSImage *))completionHandler
 {
     NSURL * const artworkURL = inputItem.artworkURL;
     const NSSize imageSize = NSMakeSize(kVLCDesiredThumbnailWidth, kVLCDesiredThumbnailHeight);
@@ -259,6 +284,25 @@ const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
     });
 }
 
+- (void)generateImageForInputItem:(VLCInputItem *)inputItem
+                   withCompletion:(void(^)(const NSImage *))completionHandler
+{
+    NSString * const radioCountryCode = inputItem.radioCountryCodeForFlagArtwork;
+    if (!radioCountryCode) {
+        [self generateArtworkForInputItem:inputItem withCompletion:completionHandler];
+        return;
+    }
+
+    [self imageForCountryCode:radioCountryCode
+               withCompletion:^(const NSImage * const flagImage) {
+        if (flagImage) {
+            completionHandler(flagImage);
+            return;
+        }
+        [self generateArtworkForInputItem:inputItem withCompletion:completionHandler];
+    }];
+}
+
 + (void)thumbnailForPlayQueueItem:(VLCPlayQueueItem *)playQueueItem
                   withCompletion:(nonnull void (^)(const NSImage * _Nonnull))completionHandler
 {


=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceBaseDataSource.m
=====================================
@@ -37,6 +37,7 @@
 #import "library/VLCInputNodePathControl.h"
 #import "library/VLCInputNodePathControlItem.h"
 #import "library/VLCLibraryCollectionViewSupplementaryElementView.h"
+#import "library/VLCLibraryImageCache.h"
 #import "library/VLCLibraryTableCellView.h"
 #import "library/VLCLibraryWindow.h"
 #import "library/VLCLibraryWindowPersistentPreferences.h"
@@ -298,8 +299,6 @@ NSString * const VLCMediaSourceBaseDataSourceNodeChanged = @"VLCMediaSourceBaseD
 
         const enum input_item_type_e inputType = childRootInput.inputType;
         const BOOL isStream = childRootInput.isStream;
-        
-        NSURL * const artworkURL = childRootInput.artworkURL;
 
         NSImage *placeholder;
         if (mediaSource.category == SD_CAT_LAN) {
@@ -326,8 +325,15 @@ NSString * const VLCMediaSourceBaseDataSourceNodeChanged = @"VLCMediaSourceBaseD
             }
         }
         NSAssert(placeholder != nil, @"Placeholder image should not be nil");
-        
-        if (artworkURL) {
+
+        NSURL * const artworkURL = childRootInput.artworkURL;
+        if (childRootInput.radioCountryCodeForFlagArtwork) {
+            viewItem.mediaImageView.image = placeholder;
+            [VLCLibraryImageCache thumbnailForInputItem:childRootInput
+                                         withCompletion:^(const NSImage * const thumbnail) {
+                viewItem.mediaImageView.image = (NSImage *)thumbnail;
+            }];
+        } else if (artworkURL) {
             [viewItem.mediaImageView setImageURL:artworkURL placeholderImage:placeholder];
         } else {
             viewItem.mediaImageView.image = placeholder;
@@ -435,7 +441,13 @@ referenceSizeForHeaderInSection:(NSInteger)section
             VLCInputItem * const currentNodeInput = _lanDeviceSnapshot[row].inputNode.inputItem;
             NSURL * const artworkURL = currentNodeInput.artworkURL;
             NSImage * const placeholder = NSImage.VLCDefaultAppIconImage;
-            if (artworkURL) {
+            if (currentNodeInput.radioCountryCodeForFlagArtwork) {
+                cellView.representedImageView.image = placeholder;
+                [VLCLibraryImageCache thumbnailForInputItem:currentNodeInput
+                                             withCompletion:^(const NSImage * const thumbnail) {
+                    cellView.representedImageView.image = (NSImage *)thumbnail;
+                }];
+            } else if (artworkURL) {
                 [cellView.representedImageView setImageURL:artworkURL placeholderImage:placeholder];
             } else {
                 cellView.representedImageView.image = placeholder;



View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/343621fe23e186876e8b3f1734667e50f0ae03e4

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/343621fe23e186876e8b3f1734667e50f0ae03e4
You're receiving this email because of your account on code.videolan.org. Manage all notifications: https://code.videolan.org/-/profile/notifications | Help: https://code.videolan.org/help




More information about the vlc-commits mailing list