diff --git a/pcbnew/pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.cpp b/pcbnew/pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.cpp
index a21a3555e7..3e3ad0dc42 100644
--- a/pcbnew/pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.cpp
+++ b/pcbnew/pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.cpp
@@ -2079,6 +2079,8 @@ void PCB_IO_KICAD_SEXPR::format( const PCB_TEXTBOX* aTextBox ) const
     {
         KICAD_FORMAT::FormatBool( m_out, "border", aTextBox->IsBorderEnabled() );
         aTextBox->GetStroke().Format( m_out, pcbIUScale );
+
+        KICAD_FORMAT::FormatBool( m_out, "knockout", aTextBox->IsKnockout() );
     }
 
     if( aTextBox->GetFont() && aTextBox->GetFont()->IsOutline() )
diff --git a/pcbnew/pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.h b/pcbnew/pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.h
index cf6d193be5..e7911402f2 100644
--- a/pcbnew/pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.h
+++ b/pcbnew/pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.h
@@ -172,7 +172,9 @@ class PCB_IO_KICAD_SEXPR;   // forward decl
 //#define SEXPR_BOARD_FILE_VERSION    20241030  // Dimension arrow directions, suppress_zeroes normalization
 //#define SEXPR_BOARD_FILE_VERSION    20241129  // Normalise keep_text_aligned and fill properties
 //#define SEXPR_BOARD_FILE_VERSION    20241228  // Convert teardrop curve points to bool
-#define SEXPR_BOARD_FILE_VERSION      20241229  // Expand User layers to arbitrary count
+//#define SEXPR_BOARD_FILE_VERSION    20241229  // Expand User layers to arbitrary count
+//----------------- Start of 10.0 development -----------------
+#define SEXPR_BOARD_FILE_VERSION      20250210  // Knockout for textboxes
 
 
 #define BOARD_FILE_HOST_VERSION       20200825  ///< Earlier files than this include the host tag
diff --git a/pcbnew/pcb_io/kicad_sexpr/pcb_io_kicad_sexpr_parser.cpp b/pcbnew/pcb_io/kicad_sexpr/pcb_io_kicad_sexpr_parser.cpp
index 884c915981..73bb9d417d 100644
--- a/pcbnew/pcb_io/kicad_sexpr/pcb_io_kicad_sexpr_parser.cpp
+++ b/pcbnew/pcb_io/kicad_sexpr/pcb_io_kicad_sexpr_parser.cpp
@@ -3649,6 +3649,20 @@ void PCB_IO_KICAD_SEXPR_PARSER::parseTextBoxContent( PCB_TEXTBOX* aTextBox )
             NeedRIGHT();
             break;
 
+        case T_knockout:
+            if( PCB_TABLECELL* cell = dynamic_cast<PCB_TABLECELL*>( aTextBox ) )
+            {
+                Expecting( "locked, start, pts, angle, width, margins, layer, effects, span, "
+                           "render_cache, uuid or tstamp" );
+            }
+            else
+            {
+                aTextBox->SetIsKnockout( parseBool() );
+            }
+
+            NeedRIGHT();
+            break;
+
         case T_span:
             if( PCB_TABLECELL* cell = dynamic_cast<PCB_TABLECELL*>( aTextBox ) )
             {
@@ -3657,7 +3671,8 @@ void PCB_IO_KICAD_SEXPR_PARSER::parseTextBoxContent( PCB_TEXTBOX* aTextBox )
             }
             else
             {
-                Expecting( "angle, width, layer, effects, render_cache, uuid or tstamp" );
+                Expecting( "locked, start, pts, angle, width, stroke, border, margins, knockout, "
+                           "layer, effects, render_cache, uuid or tstamp" );
             }
 
             NeedRIGHT();
@@ -3680,9 +3695,15 @@ void PCB_IO_KICAD_SEXPR_PARSER::parseTextBoxContent( PCB_TEXTBOX* aTextBox )
 
         default:
             if( dynamic_cast<PCB_TABLECELL*>( aTextBox ) != nullptr )
-                Expecting( "locked, start, pts, angle, width, layer, effects, span, render_cache, uuid or tstamp" );
+            {
+                Expecting( "locked, start, pts, angle, width, margins, layer, effects, span, "
+                           "render_cache, uuid or tstamp" );
+            }
             else
-                Expecting( "locked, start, pts, angle, width, layer, effects, render_cache, uuid or tstamp" );
+            {
+                Expecting( "locked, start, pts, angle, width, stroke, border, margins, knockout,"
+                           "layer, effects, render_cache, uuid or tstamp" );
+            }
         }
     }
 
diff --git a/pcbnew/pcb_painter.cpp b/pcbnew/pcb_painter.cpp
index 5f494daa21..f3373cec11 100644
--- a/pcbnew/pcb_painter.cpp
+++ b/pcbnew/pcb_painter.cpp
@@ -2425,33 +2425,46 @@ void PCB_PAINTER::draw( const PCB_TEXTBOX* aTextBox, int aLayer )
 #endif
     }
 
-    if( resolvedText.Length() == 0 )
-        return;
-
-    const KIFONT::METRICS& metrics = aTextBox->GetFontMetrics();
-    TEXT_ATTRIBUTES        attrs = aTextBox->GetAttributes();
-    attrs.m_StrokeWidth = getLineThickness( aTextBox->GetEffectiveTextPenWidth() );
-
-    if( m_gal->IsFlippedX() && !aTextBox->IsSideSpecific() )
+    if( aTextBox->IsKnockout() )
     {
-        attrs.m_Mirrored = !attrs.m_Mirrored;
-        strokeText( resolvedText, aTextBox->GetDrawPos( true ), attrs, metrics );
-        return;
-    }
+        SHAPE_POLY_SET finalPoly;
+        aTextBox->TransformTextToPolySet( finalPoly, 0, m_maxError, ERROR_INSIDE );
+        finalPoly.Fracture();
 
-    std::vector<std::unique_ptr<KIFONT::GLYPH>>* cache = nullptr;
-
-    if( font->IsOutline() )
-        cache = aTextBox->GetRenderCache( font, resolvedText );
-
-    if( cache )
-    {
-        m_gal->SetLineWidth( attrs.m_StrokeWidth );
-        m_gal->DrawGlyphs( *cache );
+        m_gal->SetIsStroke( false );
+        m_gal->SetIsFill( true );
+        m_gal->DrawPolygon( finalPoly );
     }
     else
     {
-        strokeText( resolvedText, aTextBox->GetDrawPos(), attrs, metrics );
+        if( resolvedText.Length() == 0 )
+            return;
+
+        const KIFONT::METRICS& metrics = aTextBox->GetFontMetrics();
+        TEXT_ATTRIBUTES        attrs = aTextBox->GetAttributes();
+        attrs.m_StrokeWidth = getLineThickness( aTextBox->GetEffectiveTextPenWidth() );
+
+        if( m_gal->IsFlippedX() && !aTextBox->IsSideSpecific() )
+        {
+            attrs.m_Mirrored = !attrs.m_Mirrored;
+            strokeText( resolvedText, aTextBox->GetDrawPos( true ), attrs, metrics );
+            return;
+        }
+
+        std::vector<std::unique_ptr<KIFONT::GLYPH>>* cache = nullptr;
+
+        if( font->IsOutline() )
+            cache = aTextBox->GetRenderCache( font, resolvedText );
+
+        if( cache )
+        {
+            m_gal->SetLineWidth( attrs.m_StrokeWidth );
+            m_gal->DrawGlyphs( *cache );
+        }
+        else
+        {
+            strokeText( resolvedText, aTextBox->GetDrawPos(), attrs, metrics );
+        }
     }
 }
 
diff --git a/pcbnew/pcb_text.cpp b/pcbnew/pcb_text.cpp
index 106c7480c6..1db1b6af3d 100644
--- a/pcbnew/pcb_text.cpp
+++ b/pcbnew/pcb_text.cpp
@@ -640,8 +640,6 @@ static struct PCB_TEXT_DESC
         propMgr.OverrideAvailability( TYPE_HASH( PCB_TEXT ), TYPE_HASH( EDA_TEXT ),
                                       _HKI( "Keep Upright" ), isFootprintText );
 
-        propMgr.OverrideAvailability( TYPE_HASH( PCB_TEXT ), TYPE_HASH( EDA_TEXT ),
-                                      _HKI( "Hyperlink" ),
-                                      []( INSPECTABLE* aItem ) { return false; } );
+        propMgr.Mask( TYPE_HASH( PCB_TEXT ), TYPE_HASH( EDA_TEXT ), _HKI( "Hyperlink" ) );
     }
 } _PCB_TEXT_DESC;
diff --git a/pcbnew/pcb_textbox.cpp b/pcbnew/pcb_textbox.cpp
index fb903bf1bf..a7947977f2 100644
--- a/pcbnew/pcb_textbox.cpp
+++ b/pcbnew/pcb_textbox.cpp
@@ -701,44 +701,57 @@ void PCB_TEXTBOX::TransformTextToPolySet( SHAPE_POLY_SET& aBuffer, int aClearanc
     KIGFX::GAL_DISPLAY_OPTIONS empty_opts;
     KIFONT::FONT*              font = getDrawFont();
     int                        penWidth = GetEffectiveTextPenWidth();
+    TEXT_ATTRIBUTES            attrs = GetAttributes();
+    wxString                   shownText = GetShownText( true );
 
-    // Note: this function is mainly used in 3D viewer.
-    // the polygonal shape of a text can have many basic shapes,
-    // so combining these shapes can be very useful to create a final shape
-    // swith a lot less vertices to speedup calculations using this final shape
+    // The polygonal shape of a text can have many basic shapes, so combining these shapes can
+    // be very useful to create a final shape with a lot less vertices to speedup calculations.
     // Simplify shapes is not usually always efficient, but in this case it is.
-    SHAPE_POLY_SET buffer;
+    SHAPE_POLY_SET textShape;
 
     CALLBACK_GAL callback_gal( empty_opts,
             // Stroke callback
             [&]( const VECTOR2I& aPt1, const VECTOR2I& aPt2 )
             {
-                TransformOvalToPolygon( buffer, aPt1, aPt2, penWidth, aMaxError, aErrorLoc );
+                TransformOvalToPolygon( textShape, aPt1, aPt2, penWidth, aMaxError, aErrorLoc );
             },
             // Triangulation callback
             [&]( const VECTOR2I& aPt1, const VECTOR2I& aPt2, const VECTOR2I& aPt3 )
             {
-                buffer.NewOutline();
+                textShape.NewOutline();
 
                 for( const VECTOR2I& point : { aPt1, aPt2, aPt3 } )
-                    buffer.Append( point.x, point.y );
+                    textShape.Append( point.x, point.y );
             } );
 
-    font->Draw( &callback_gal, GetShownText( true ), GetDrawPos(), GetAttributes(), GetFontMetrics() );
+    if( auto* cache = GetRenderCache( font, shownText ) )
+        callback_gal.DrawGlyphs( *cache );
+    else
+        font->Draw( &callback_gal, shownText, GetDrawPos(), attrs, GetFontMetrics() );
 
-    if( aClearance > 0 || aErrorLoc == ERROR_OUTSIDE )
+    textShape.Simplify();
+
+    if( IsKnockout() )
     {
-        if( aErrorLoc == ERROR_OUTSIDE )
-            aClearance += aMaxError;
+        SHAPE_POLY_SET finalPoly;
 
-        buffer.Inflate( aClearance, CORNER_STRATEGY::ROUND_ALL_CORNERS, aMaxError, true );
+        TransformShapeToPolygon( finalPoly, GetLayer(), aClearance, aMaxError, aErrorLoc );
+        finalPoly.BooleanSubtract( textShape );
+
+        aBuffer.Append( finalPoly );
     }
     else
     {
-        buffer.Simplify();
-    }
+        if( aClearance > 0 || aErrorLoc == ERROR_OUTSIDE )
+        {
+            if( aErrorLoc == ERROR_OUTSIDE )
+                aClearance += aMaxError;
 
-    aBuffer.Append( buffer );
+            textShape.Inflate( aClearance, CORNER_STRATEGY::ROUND_ALL_CORNERS, aMaxError, true );
+        }
+
+        aBuffer.Append( textShape );
+    }
 }
 
 
@@ -890,6 +903,10 @@ static struct PCB_TEXTBOX_DESC
         propMgr.Mask( TYPE_HASH( PCB_TEXTBOX ), TYPE_HASH( EDA_SHAPE ), _HKI( "Filled" ) );
         propMgr.Mask( TYPE_HASH( PCB_TEXTBOX ), TYPE_HASH( EDA_TEXT ), _HKI( "Color" ) );
 
+        propMgr.AddProperty( new PROPERTY<PCB_TEXTBOX, bool, BOARD_ITEM>( _HKI( "Knockout" ),
+                &BOARD_ITEM::SetIsKnockout, &BOARD_ITEM::IsKnockout ),
+                _HKI( "Text Properties" ) );
+
         const wxString borderProps = _( "Border Properties" );
 
         void ( PCB_TEXTBOX::*lineStyleSetter )( LINE_STYLE ) = &PCB_TEXTBOX::SetLineStyle;
diff --git a/pcbnew/plot_brditems_plotter.cpp b/pcbnew/plot_brditems_plotter.cpp
index 5ad142f862..9e646e2a5d 100644
--- a/pcbnew/plot_brditems_plotter.cpp
+++ b/pcbnew/plot_brditems_plotter.cpp
@@ -729,11 +729,19 @@ void BRDITEMS_PLOTTER::PlotText( const EDA_TEXT* aText, PCB_LAYER_ID aLayer, boo
 
     if( aIsKnockout )
     {
-        const PCB_TEXT* text = static_cast<const PCB_TEXT*>( aText );
         SHAPE_POLY_SET  finalPoly;
 
-        text->TransformTextToPolySet( finalPoly, 0, m_board->GetDesignSettings().m_MaxError,
-                                      ERROR_INSIDE );
+        if( const PCB_TEXT* text = dynamic_cast<const PCB_TEXT*>( aText) )
+        {
+            text->TransformTextToPolySet( finalPoly, 0, m_board->GetDesignSettings().m_MaxError,
+                                          ERROR_INSIDE );
+        }
+        else if( const PCB_TEXTBOX* textbox = dynamic_cast<const PCB_TEXTBOX*>( aText ) )
+        {
+            textbox->TransformTextToPolySet( finalPoly, 0, m_board->GetDesignSettings().m_MaxError,
+                                             ERROR_INSIDE );
+        }
+
         finalPoly.Fracture();
 
         for( int ii = 0; ii < finalPoly.OutlineCount(); ++ii )