[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