[vlc-commits] [Git][videolan/vlc][master] 2 commits: text_style: add transparent (knockout) blending mode.

Felix Paul Kühne (@fkuehne) gitlab at videolan.org
Sat Apr 18 21:23:54 UTC 2026



Felix Paul Kühne pushed to branch master at VideoLAN / VLC


Commits:
82037708 by sarah-kamall at 2026-04-18T22:06:19+02:00
text_style: add transparent (knockout) blending mode.

This patch adds a configuration option for a new blending mode.

Changes:
- Adding an Enum with the two possible blending modes: the default and the transparent (knockout) mode.
- Set the blending mode to the default

Fixes: #29195 Signed-off-by:Sarah Soliman sarah.kamal.soliman at gmail.com

- - - - -
95fb4927 by sarah-kamall at 2026-04-18T22:06:19+02:00
freetype: add transparent (knockout) blending mode.

This patch implements knockout blending logic for Freetype renderer.

In knockout mode, outline alpha is handled independently to allow
outline-only rendering when font alpha is set to 0. This enables
use cases where text is transparent but the outline remains visible.
Changes:

- Add GetDrawingFunctions() helper to select drawing functions
- Add freetype-blending-mode configuration option
- Add STYLE_BLENDING_TRANSPARENT mode support for YUVA/RGBA/ARGB
- Fix outline alpha handling for knockout blending mode

Fixes: #29195
Signed-off-by:Sarah Soliman sarah.kamal.soliman at gmail.com

- - - - -


6 changed files:

- include/vlc_text_style.h
- modules/text_renderer/freetype/blend/blend.h
- modules/text_renderer/freetype/blend/rgb.h
- modules/text_renderer/freetype/blend/yuv.h
- modules/text_renderer/freetype/freetype.c
- src/misc/text_style.c


Changes:

=====================================
include/vlc_text_style.h
=====================================
@@ -72,6 +72,13 @@ typedef struct
         STYLE_WRAP_CHAR,          /**< Breaks at character level only */
         STYLE_WRAP_NONE,          /**< No line breaks (except explicit ones) */
     } e_wrapinfo;
+
+    /*blending style*/
+    enum
+    {
+       STYLE_BLENDING_DEFAULT = 0,      /**< Overlay blending style>**/
+       STYLE_BLENDING_TRANSPARENT,      /**< Knockout blending style>**/
+    } e_blending_mode;
 } text_style_t;
 
 #define STYLE_ALPHA_OPAQUE      0xFF
@@ -90,6 +97,7 @@ typedef struct
 #define STYLE_HAS_BACKGROUND_COLOR      (1 << 7)
 #define STYLE_HAS_BACKGROUND_ALPHA      (1 << 8)
 #define STYLE_HAS_WRAP_INFO             (1 << 9)
+#define STYLE_HAS_BLENDING_MODE         (1 << 10)
 
 /* Style flags for \ref text_style_t */
 #define STYLE_BOLD              (1 << 0)


=====================================
modules/text_renderer/freetype/blend/blend.h
=====================================
@@ -63,6 +63,26 @@ static inline void Blend##name##Pixel( uint8_t **dst, int a, int x, int y, int z
     }\
 }
 
+#define DECL_PIXEL_BLENDER_KNOCKOUT( name, APOS, XPOS, YPOS, ZPOS, APLANE, XPLANE, YPLANE, ZPLANE ) \
+static inline void Blend##name##PixelKnockout( uint8_t **dst, int a, int x, int y, int z, int glyph_a )\
+{\
+    if( glyph_a == 0 )\
+        return;\
+\
+    int i_ao = dst[APLANE][APOS];\
+    /* Knockout Mode: Interpolate between Source and Dest based on glyph shape */ \
+    /* Result = (Dest * (255 - Glyph) + Src * Glyph) / 255 */ \
+    int i_an = (i_ao * (255 - glyph_a) + a * glyph_a) / 255; \
+    dst[APLANE][APOS] = i_an; \
+\
+    if (i_an > 0)\
+    {\
+        dst[XPLANE][XPOS] = ( dst[XPLANE][XPOS] * (255 - glyph_a) + x * glyph_a ) / 255;\
+        dst[YPLANE][YPOS] = ( dst[YPLANE][YPOS] * (255 - glyph_a) + y * glyph_a ) / 255;\
+        dst[ZPLANE][ZPOS] = ( dst[ZPLANE][ZPOS] * (255 - glyph_a) + z * glyph_a ) / 255;\
+    }\
+}
+
 static void BlendAXYZLine( picture_t *p_picture,
                            int i_picture_x, int i_picture_y,
                            int i_a, int i_x, int i_y, int i_z,


=====================================
modules/text_renderer/freetype/blend/rgb.h
=====================================
@@ -57,53 +57,39 @@ static void Fill##name##Picture( picture_t *p_picture,\
 
 DECL_RGB_FILLER( RGBA, 3, 0, 1, 2 );
 DECL_PIXEL_BLENDER(RGBA, 3, 0, 1, 2, 0, 0, 0, 0);
+DECL_PIXEL_BLENDER_KNOCKOUT(RGBA, 3, 0, 1, 2, 0, 0, 0, 0);
 
 DECL_RGB_FILLER( ARGB, 0, 1, 2, 3 );
 DECL_PIXEL_BLENDER(ARGB, 0, 1, 2, 3, 0, 0, 0, 0);
+DECL_PIXEL_BLENDER_KNOCKOUT(ARGB, 0, 1, 2, 3, 0, 0, 0, 0);
 
 #undef DECL_RGB_FILLER
-
-static inline void BlendGlyphToRGB( picture_t *p_picture,
-                                    int i_picture_x, int i_picture_y,
-                                    int i_a, int i_x, int i_y, int i_z,
-                                    FT_BitmapGlyph p_glyph,
-                                    void (*BlendPixel)( uint8_t **, int, int, int, int, int ) )
-{
-    const uint8_t *srcrow = p_glyph->bitmap.buffer;
-    int i_pitch_src = p_glyph->bitmap.pitch;
-    int i_pitch_dst = p_picture->p[0].i_pitch;
-    uint8_t *dstrow = &p_picture->p[0].p_pixels[i_picture_y * i_pitch_dst + 4 * i_picture_x];
-
-    for( unsigned int dy = 0; dy < p_glyph->bitmap.rows; dy++ )
-    {
-        const uint8_t *src = srcrow;
-        uint8_t *dst = dstrow;
-        for( unsigned int dx = 0; dx < p_glyph->bitmap.width; dx++ )
-        {
-            BlendPixel( &dst, i_a, i_x, i_y, i_z, *src++ );
-            dst += 4;
-        }
-        srcrow += i_pitch_src;
-        dstrow += i_pitch_dst;
-    }
-}
-
-static void BlendGlyphToRGBA( picture_t *p_picture,
-                              int i_picture_x, int i_picture_y,
-                              int i_a, int i_x, int i_y, int i_z,
-                              FT_BitmapGlyph p_glyph )
-{
-    BlendGlyphToRGB( p_picture, i_picture_x, i_picture_y,
-                     i_a, i_x, i_y, i_z, p_glyph,
-                     BlendRGBAPixel );
+#define DECL_BLEND_GLYPH_TO_RGB(name, BlendFunc) \
+static void BlendGlyphTo##name( picture_t *p_picture, \
+                                int i_picture_x, int i_picture_y, \
+                                int i_a, int i_x, int i_y, int i_z, \
+                                FT_BitmapGlyph p_glyph ) \
+{\
+    const uint8_t *srcrow = p_glyph->bitmap.buffer;\
+    int i_pitch_src = p_glyph->bitmap.pitch;\
+    int i_pitch_dst = p_picture->p[0].i_pitch;\
+    uint8_t *dstrow = &p_picture->p[0].p_pixels[i_picture_y * i_pitch_dst + 4 * i_picture_x];\
+\
+    for( unsigned int dy = 0; dy < p_glyph->bitmap.rows; dy++ )\
+    {\
+        const uint8_t *src = srcrow;\
+        uint8_t *dst = dstrow;\
+        for( unsigned int dx = 0; dx < p_glyph->bitmap.width; dx++ )\
+        {\
+            BlendFunc( &dst, i_a, i_x, i_y, i_z, *src++ );\
+            dst += 4;\
+        }\
+        srcrow += i_pitch_src;\
+        dstrow += i_pitch_dst;\
+    }\
 }
 
-static void BlendGlyphToARGB( picture_t *p_picture,
-                              int i_picture_x, int i_picture_y,
-                              int i_a, int i_x, int i_y, int i_z,
-                              FT_BitmapGlyph p_glyph )
-{
-    BlendGlyphToRGB( p_picture, i_picture_x, i_picture_y,
-                     i_a, i_x, i_y, i_z, p_glyph,
-                     BlendARGBPixel );
-}
+DECL_BLEND_GLYPH_TO_RGB(RGBA, BlendRGBAPixel);
+DECL_BLEND_GLYPH_TO_RGB(ARGB, BlendARGBPixel);
+DECL_BLEND_GLYPH_TO_RGB(RGBAKnockout, BlendRGBAPixelKnockout);
+DECL_BLEND_GLYPH_TO_RGB(ARGBKnockout, BlendARGBPixelKnockout);
\ No newline at end of file


=====================================
modules/text_renderer/freetype/blend/yuv.h
=====================================
@@ -60,44 +60,48 @@ static void FillYUVAPicture( picture_t *p_picture,
 }
 
 DECL_PIXEL_BLENDER(YUVA, 0, 0, 0, 0, A_PLANE, Y_PLANE, U_PLANE, V_PLANE);
+DECL_PIXEL_BLENDER_KNOCKOUT(YUVA, 0, 0, 0, 0, A_PLANE, Y_PLANE, U_PLANE, V_PLANE);
 
-static void BlendGlyphToYUVA( picture_t *p_picture,
-                              int i_picture_x, int i_picture_y,
-                              int i_a, int i_x, int i_y, int i_z,
-                              FT_BitmapGlyph p_glyph )
-{
-    const uint8_t *srcrow = p_glyph->bitmap.buffer;
-    int i_pitch_src = p_glyph->bitmap.pitch;
-
-    uint8_t *dstrows[4];
-    dstrows[0] = &p_picture->p[0].p_pixels[i_picture_y * p_picture->p[0].i_pitch +
-                                           i_picture_x * p_picture->p[0].i_pixel_pitch];
-    dstrows[1] = &p_picture->p[1].p_pixels[i_picture_y * p_picture->p[1].i_pitch +
-                                           i_picture_x * p_picture->p[1].i_pixel_pitch];
-    dstrows[2] = &p_picture->p[2].p_pixels[i_picture_y * p_picture->p[2].i_pitch +
-                                           i_picture_x * p_picture->p[2].i_pixel_pitch];
-    dstrows[3] = &p_picture->p[3].p_pixels[i_picture_y * p_picture->p[3].i_pitch +
-                                           i_picture_x * p_picture->p[3].i_pixel_pitch];
-
-    for( unsigned int dy = 0; dy < p_glyph->bitmap.rows; dy++ )
-    {
-        const uint8_t *src = srcrow;
-
-        uint8_t *dst[4];
-        memcpy(dst, dstrows, 4 * sizeof(dst[0]));
-        for( unsigned int dx = 0; dx < p_glyph->bitmap.width; dx++ )
-        {
-            BlendYUVAPixel( dst, i_a, i_x, i_y, i_z, *src++ );
-            dst[0] += p_picture->p[0].i_pixel_pitch;
-            dst[1] += p_picture->p[1].i_pixel_pitch;
-            dst[2] += p_picture->p[2].i_pixel_pitch;
-            dst[3] += p_picture->p[3].i_pixel_pitch;
-        }
-
-        srcrow += i_pitch_src;
-        dstrows[0] += p_picture->p[0].i_pitch;
-        dstrows[1] += p_picture->p[1].i_pitch;
-        dstrows[2] += p_picture->p[2].i_pitch;
-        dstrows[3] += p_picture->p[3].i_pitch;
-    }
+#define DECL_BLEND_GLYPH_TO_YUVA(name, BlendFunc) \
+static void BlendGlyphTo##name( picture_t *p_picture, \
+                                int i_picture_x, int i_picture_y, \
+                                int i_a, int i_x, int i_y, int i_z, \
+                                FT_BitmapGlyph p_glyph ) \
+{\
+    const uint8_t *srcrow = p_glyph->bitmap.buffer;\
+    int i_pitch_src = p_glyph->bitmap.pitch;\
+\
+    uint8_t *dstrows[4];\
+    dstrows[0] = &p_picture->p[0].p_pixels[i_picture_y * p_picture->p[0].i_pitch +  \
+                                           i_picture_x * p_picture->p[0].i_pixel_pitch];\
+    dstrows[1] = &p_picture->p[1].p_pixels[i_picture_y * p_picture->p[1].i_pitch +\
+                                           i_picture_x * p_picture->p[1].i_pixel_pitch];\
+    dstrows[2] = &p_picture->p[2].p_pixels[i_picture_y * p_picture->p[2].i_pitch +\
+                                           i_picture_x * p_picture->p[2].i_pixel_pitch];\
+    dstrows[3] = &p_picture->p[3].p_pixels[i_picture_y * p_picture->p[3].i_pitch +\
+                                           i_picture_x * p_picture->p[3].i_pixel_pitch];\
+\
+    for( unsigned int dy = 0; dy < p_glyph->bitmap.rows; dy++ )\
+    {\
+        const uint8_t *src = srcrow;\
+\
+        uint8_t *dst[4];\
+        memcpy(dst, dstrows, 4 * sizeof(dst[0]));\
+        for( unsigned int dx = 0; dx < p_glyph->bitmap.width; dx++ )\
+        {\
+            BlendFunc( dst, i_a, i_x, i_y, i_z, *src++ );\
+            dst[0] += p_picture->p[0].i_pixel_pitch;\
+            dst[1] += p_picture->p[1].i_pixel_pitch;\
+            dst[2] += p_picture->p[2].i_pixel_pitch;\
+            dst[3] += p_picture->p[3].i_pixel_pitch;\
+        }\
+\
+        srcrow += i_pitch_src;\
+        dstrows[0] += p_picture->p[0].i_pitch;\
+        dstrows[1] += p_picture->p[1].i_pitch;\
+        dstrows[2] += p_picture->p[2].i_pitch;\
+        dstrows[3] += p_picture->p[3].i_pitch;\
+    }\
 }
+DECL_BLEND_GLYPH_TO_YUVA(YUVA, BlendYUVAPixel)
+DECL_BLEND_GLYPH_TO_YUVA(YUVAKnockout, BlendYUVAPixelKnockout)


=====================================
modules/text_renderer/freetype/freetype.c
=====================================
@@ -95,6 +95,8 @@ static void Destroy( filter_t * );
 #define YUVP_TEXT N_("Use YUVP renderer")
 #define YUVP_LONGTEXT N_("This renders the font using \"paletized YUV\". " \
   "This option is only needed if you want to encode into DVB subtitles" )
+#define BLENDING_MODE_TEXT N_("Blending Mode")
+#define BLENDING_MODE_LONG_TEXT N_("Blending mode for the font, can be transparent or overlay")
 
 static const int pi_color_values[] = {
   0x00000000, 0x00808080, 0x00C0C0C0, 0x00FFFFFF, 0x00800000,
@@ -120,6 +122,14 @@ static const int pi_text_direction[] = {
 static const char *const ppsz_text_direction[] = {
     N_("Left to right"), N_("Right to left"), N_("Auto"),
 };
+
+static const int pi_blending_mode[] = {
+    0, 1
+};
+static const char *const ppsz_blending_mode[] = {
+    N_("Overlay"), N_("Transparent"),
+};
+
 #endif
 
 vlc_module_begin ()
@@ -190,6 +200,10 @@ vlc_module_begin ()
 
     add_bool( "freetype-yuvp", false, YUVP_TEXT,
               YUVP_LONGTEXT )
+    add_integer_with_range( "freetype-blending-mode", 0, 0, 1, BLENDING_MODE_TEXT,
+                            BLENDING_MODE_LONG_TEXT )
+        change_integer_list( pi_blending_mode, ppsz_blending_mode )
+        change_safe()
 
 #ifdef HAVE_FRIBIDI
     add_integer_with_range( "freetype-text-direction", 0, 0, 2, TEXT_DIRECTION_TEXT,
@@ -565,7 +579,12 @@ static void RenderCharAXYZ( filter_t *p_filter,
             i_color = ch->p_style->i_shadow_color;
             break;
         case 1:
-            i_a     = i_a * ch->p_style->i_outline_alpha / 255;
+            /* In knockout mode, outline alpha is independent to allow
+             * outline-only rendering when font alpha is 0 */
+            if( ch->p_style->e_blending_mode == STYLE_BLENDING_DEFAULT )
+                i_a = i_a * ch->p_style->i_outline_alpha / 255;
+            else
+                i_a = ch->p_style->i_outline_alpha;
             i_color = ch->p_style->i_outline_color;
             break;
         default:
@@ -586,7 +605,7 @@ static void RenderCharAXYZ( filter_t *p_filter,
         }
 
         /* Don't render if invisible or not wanted */
-        if( i_a == STYLE_ALPHA_TRANSPARENT ||
+        if(
            (g == 0 && 0 == (ch->p_style->i_style_flags & STYLE_SHADOW) ) ||
            (g == 1 && 0 == (ch->p_style->i_style_flags & STYLE_OUTLINE) )
           )
@@ -734,6 +753,8 @@ static void FillDefaultStyles( filter_t *p_filter )
 
     p_sys->p_default_style->i_shadow_alpha = var_InheritInteger( p_filter, "freetype-shadow-opacity" );
     p_sys->p_default_style->i_shadow_color = var_InheritInteger( p_filter, "freetype-shadow-color" );
+    p_sys->p_default_style->e_blending_mode = var_InheritInteger( p_filter, "freetype-blending-mode" );
+    p_sys->p_default_style->i_features |= STYLE_HAS_BLENDING_MODE;
 
     p_sys->p_default_style->i_font_size = 0;
     p_sys->p_default_style->i_style_flags |= STYLE_SHADOW;
@@ -946,6 +967,63 @@ static size_t SegmentsToTextAndStyles( filter_t *p_filter, const text_segment_t
     return i_nb_char;
 }
 
+/**
+ * Get the appropriate drawing functions based on chroma codec and blending mode
+ */
+static const ft_drawing_functions *GetDrawingFunctions( vlc_fourcc_t chroma,
+                                                        int i_blending_mode )
+{
+    if( chroma == VLC_CODEC_YUVA )
+    {
+        static const ft_drawing_functions DRAW_YUVA =
+            { .extract = YUVFromXRGB,
+              .fill =    FillYUVAPicture,
+              .blend =   BlendGlyphToYUVA };
+        static const ft_drawing_functions DRAW_YUVA_KNOCKOUT =
+            { .extract = YUVFromXRGB,
+              .fill =    FillYUVAPicture,
+              .blend =   BlendGlyphToYUVAKnockout };
+
+        if( i_blending_mode == STYLE_BLENDING_TRANSPARENT )
+            return &DRAW_YUVA_KNOCKOUT;
+        return &DRAW_YUVA;
+    }
+    else if( chroma == VLC_CODEC_RGBA
+          || chroma == VLC_CODEC_BGRA )
+    {
+        static const ft_drawing_functions DRAW_RGBA =
+            { .extract = RGBFromXRGB,
+              .fill =    FillRGBAPicture,
+              .blend =   BlendGlyphToRGBA };
+        static const ft_drawing_functions DRAW_RGBA_KNOCKOUT =
+            { .extract = RGBFromXRGB,
+              .fill =    FillRGBAPicture,
+              .blend =   BlendGlyphToRGBAKnockout };
+
+        if( i_blending_mode == STYLE_BLENDING_TRANSPARENT )
+            return &DRAW_RGBA_KNOCKOUT;
+        return &DRAW_RGBA;
+    }
+    else if( chroma == VLC_CODEC_ARGB
+          || chroma == VLC_CODEC_ABGR )
+    {
+        static const ft_drawing_functions DRAW_ARGB =
+            { .extract = RGBFromXRGB,
+              .fill =    FillARGBPicture,
+              .blend =   BlendGlyphToARGB };
+        static const ft_drawing_functions DRAW_ARGB_KNOCKOUT =
+            { .extract = RGBFromXRGB,
+              .fill =    FillARGBPicture,
+              .blend =   BlendGlyphToARGBKnockout };
+
+        if( i_blending_mode == STYLE_BLENDING_TRANSPARENT )
+            return &DRAW_ARGB_KNOCKOUT;
+        return &DRAW_ARGB;
+    }
+
+    return NULL;
+}
+
 /**
  * This function renders a text subpicture region into another one.
  * It also calculates the size needed for this string, and renders the
@@ -1103,6 +1181,7 @@ static subpicture_region_t *Render( filter_t *p_filter,
     fmt.i_height         =
     fmt.i_visible_height = renderbbox.yMax - renderbbox.yMin;
     fmt.i_sar_num = fmt.i_sar_den = 1;
+    int i_blending_mode =  p_sys->p_default_style->e_blending_mode;
 
     for( const vlc_fourcc_t *p_chroma = p_chroma_list; *p_chroma != 0; p_chroma++ )
     {
@@ -1125,34 +1204,8 @@ static subpicture_region_t *Render( filter_t *p_filter,
                                 &renderbbox, &bbox );
         else
         {
-            const ft_drawing_functions *func;
-            if( *p_chroma == VLC_CODEC_YUVA )
-            {
-                static const ft_drawing_functions DRAW_YUVA =
-                    { .extract = YUVFromXRGB,
-                      .fill =    FillYUVAPicture,
-                      .blend =   BlendGlyphToYUVA };
-                func = &DRAW_YUVA;
-            }
-            else if( *p_chroma == VLC_CODEC_RGBA
-                  || *p_chroma == VLC_CODEC_BGRA )
-            {
-                static const ft_drawing_functions DRAW_RGBA =
-                    { .extract = RGBFromXRGB,
-                      .fill =    FillRGBAPicture,
-                      .blend =   BlendGlyphToRGBA };
-                func = &DRAW_RGBA;
-            }
-            else if( *p_chroma == VLC_CODEC_ARGB
-                  || *p_chroma == VLC_CODEC_ABGR)
-            {
-                static const ft_drawing_functions DRAW_ARGB =
-                    { .extract = RGBFromXRGB,
-                      .fill =    FillARGBPicture,
-                      .blend =   BlendGlyphToARGB };
-                func = &DRAW_ARGB;
-            }
-            else
+            const ft_drawing_functions *func = GetDrawingFunctions( *p_chroma, i_blending_mode );
+            if( func == NULL )
             {
                 subpicture_region_Delete(region);
                 region = NULL;
@@ -1258,7 +1311,7 @@ static int Create( filter_t *p_filter )
     /*
      * The following variables should not be cached, as they might be changed on-the-fly:
      * freetype-rel-fontsize, freetype-background-opacity, freetype-background-color,
-     * freetype-outline-thickness, freetype-color
+     * freetype-outline-thickness, freetype-color, freetype-blending-style
      *
      */
 


=====================================
src/misc/text_style.c
=====================================
@@ -238,6 +238,8 @@ text_style_t *text_style_Create( int i_defaults )
     p_style->i_shadow_width = 0;
     p_style->i_spacing = -1;
     p_style->e_wrapinfo = STYLE_WRAP_DEFAULT;
+    p_style->e_blending_mode = STYLE_BLENDING_DEFAULT;
+
 
     return p_style;
 }
@@ -292,6 +294,7 @@ void text_style_Merge( text_style_t *p_dst, const text_style_t *p_src, bool b_ov
         MERGE(i_background_color,   STYLE_HAS_BACKGROUND_COLOR);
         MERGE(i_background_alpha,   STYLE_HAS_BACKGROUND_ALPHA);
         MERGE(e_wrapinfo,            STYLE_HAS_WRAP_INFO);
+        MERGE(e_blending_mode,       STYLE_HAS_BLENDING_MODE);
         p_dst->i_features |= p_src->i_features;
         p_dst->i_style_flags |= p_src->i_style_flags;
     }



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/9acb1b02f60f13dee27c3af876d4a2b5b0020fc0...95fb4927885bd29d431017ea69b6d43a63e1c360

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/9acb1b02f60f13dee27c3af876d4a2b5b0020fc0...95fb4927885bd29d431017ea69b6d43a63e1c360
You're receiving this email because of your account on code.videolan.org.




More information about the vlc-commits mailing list