Subject: bug#79285: [Patch] support :font-features in face

From: Binbin YE <[email-masked]>
Date: Thu, 21 Aug 2025 23:28:53 +0900

Greetings,

It is my first time trying to contribute to Emacs source code.

The change is adding support for enabling stylistic set (font features)
when using harfbuzz font backend. It is a commonly supported feature in the
editors. See following links

https://code.visualstudio.com/docs/terminal/appearance#_font-feature-settings

https://developer.mozilla.org/en-US/docs/Web/CSS/font-feature-settings

The change adds :font-feature configuration like following

(set-face-attribute 'default nil :font "JetBrains Mono" :height 80
  :font-features '((zero . 1) (ss19 . 0) (calt . 1)))

The render result can be confirmed by adding "0" to composition table

(set-char-table-range composition-function-table
  ?0
  '(["." 0 font-shape-gstring]))

I have read the CONTRIBUTE file and tried to make the commit message as
clear as possible. Please let me know the code review process to get the
patch accepted.

Best,

Binbin
From 6542a928fbb96b71ad335e5c8e9ad779508a1f2c Mon Sep 17 00:00:00 2001
From: Binbin Ye <[email-masked]>
Date: Sat, 16 Aug 2025 23:26:33 +0900
Subject: [PATCH] support configuring font features in face

support configuring font features in face when using harfbuzz backend
using `:font-features'

* src/dispextern.h (lface_attribute_index): Add
LFACE_FONT_FEATURES_INDEX
* src/font.c (font_load_for_lface): Store font featurs in extra info
* src/hbfont.c (hb_features_from_lisp, hbfont_shape): Convert face
settings to harfbuzz features and pass to hb_shape_full
* src/xfaces.c (lface_hash, lface_same_font_attributes_p)
(Finternal_set_lisp_face_attribute, Finternal_set_lisp_face_attribute)
(check_lface_attrs, merge_face_ref): Update face comparison, getter and
setter functions
---
 src/dispextern.h |  1 +
 src/font.c       |  5 +++++
 src/hbfont.c     | 50 +++++++++++++++++++++++++++++++++++++++++++++++-
 src/xfaces.c     | 48 ++++++++++++++++++++++++++++++++++++++++++++--
 4 files changed, 101 insertions(+), 3 deletions(-)

diff --git a/src/dispextern.h b/src/dispextern.h
index 19ab104d2e6..20a901b9f40 100644
--- a/src/dispextern.h
+++ b/src/dispextern.h
@@ -1705,6 +1705,7 @@ #define FONT_TOO_HIGH(ft)						\
   LFACE_FONTSET_INDEX,
   LFACE_DISTANT_FOREGROUND_INDEX,
   LFACE_EXTEND_INDEX,
+  LFACE_FONT_FEATURES_INDEX,
   LFACE_VECTOR_SIZE
 };
 
diff --git a/src/font.c b/src/font.c
index dfe479f9355..d31d70d6050 100644
--- a/src/font.c
+++ b/src/font.c
@@ -3482,6 +3482,11 @@ font_load_for_lface (struct frame *f, Lisp_Object *attrs, Lisp_Object spec)
     {
       name = Ffont_get (spec, QCuser_spec);
       if (STRINGP (name)) font_put_extra (entity, QCuser_spec, name);
+
+      /* Store font features from face attributes for use during shaping */
+      Lisp_Object font_features = attrs[LFACE_FONT_FEATURES_INDEX];
+      if (CONSP (font_features))
+          font_put_extra (entity, QCfont_features, font_features);
     }
   return entity;
 }
diff --git a/src/hbfont.c b/src/hbfont.c
index 3d8dfb4b0e6..94b67b546e8 100644
--- a/src/hbfont.c
+++ b/src/hbfont.c
@@ -239,6 +239,41 @@ hbfont_otf_capability (struct font *font)
 
 /* Support functions for HarfBuzz shaper.  */
 
+/* Convert Lisp font features to HarfBuzz features.
+   FEATURES is a list of (feature . value) pairs.
+   Returns the number of features converted, fills HBFEATURES array.
+   Caller must ensure HBFEATURES has enough space. */
+static int
+hb_features_from_lisp (Lisp_Object features, hb_feature_t *hbfeatures, int max_features)
+{
+  int count = 0;
+
+  for (Lisp_Object tail = features; CONSP (tail) && count < max_features; tail = XCDR (tail))
+    {
+      Lisp_Object feature_spec = XCAR (tail);
+      if (CONSP (feature_spec))
+        {
+          Lisp_Object feature_sym = XCAR (feature_spec);
+          Lisp_Object feature_val = XCDR (feature_spec);
+
+          if (SYMBOLP (feature_sym) && FIXNUMP (feature_val))
+            {
+              /* Convert symbol to HarfBuzz tag */
+              const char *feature_name = SSDATA (SYMBOL_NAME (feature_sym));
+              hb_tag_t tag = hb_tag_from_string (feature_name, -1);
+
+              hbfeatures[count].tag = tag;
+              hbfeatures[count].value = XFIXNUM (feature_val);
+              hbfeatures[count].start = 0;
+              hbfeatures[count].end = (unsigned int) -1;
+              count++;
+            }
+        }
+    }
+
+  return count;
+}
+
 static bool combining_class_loaded = false;
 static Lisp_Object canonical_combining_class_table;
 
@@ -491,7 +526,20 @@ hbfont_shape (Lisp_Object lgstring, Lisp_Object direction)
   if (!hb_font)
     return make_fixnum (0);
 
-  hb_bool_t success = hb_shape_full (hb_font, hb_buffer, NULL, 0, NULL);
+
+  /* Get font features from the font object */
+  Lisp_Object font_features = Ffont_get (LGSTRING_FONT (lgstring), QCfont_features);
+  static hb_feature_t features[32];  /* Cache features array, reasonable max size */
+  int num_features = 0;
+
+  if (!NILP (font_features))
+    {
+      num_features = hb_features_from_lisp (font_features, features, 32);
+    }
+
+  hb_bool_t success = hb_shape_full (hb_font, hb_buffer, features, num_features, NULL);
+
+
   if (font->driver->end_hb_font)
     font->driver->end_hb_font (font, hb_font);
   if (!success)
diff --git a/src/xfaces.c b/src/xfaces.c
index 7626dfeb75c..879968cd773 100644
--- a/src/xfaces.c
+++ b/src/xfaces.c
@@ -1804,6 +1804,7 @@ #define LFACE_FONT(LFACE)	    AREF (LFACE, LFACE_FONT_INDEX)
 #define LFACE_INHERIT(LFACE)	    AREF (LFACE, LFACE_INHERIT_INDEX)
 #define LFACE_FONTSET(LFACE)	    AREF (LFACE, LFACE_FONTSET_INDEX)
 #define LFACE_EXTEND(LFACE)	    AREF (LFACE, LFACE_EXTEND_INDEX)
+#define LFACE_FONT_FEATURES(LFACE)  AREF (LFACE, LFACE_FONT_FEATURES_INDEX)
 #define LFACE_DISTANT_FOREGROUND(LFACE) \
   AREF (LFACE, LFACE_DISTANT_FOREGROUND_INDEX)
 
@@ -1914,6 +1915,11 @@ check_lface_attrs (Lisp_Object attrs[LFACE_VECTOR_SIZE])
 	   || STRINGP (attrs[LFACE_FONTSET_INDEX])
 	   || RESET_P (attrs[LFACE_FONTSET_INDEX])
 	   || NILP (attrs[LFACE_FONTSET_INDEX]));
+  eassert (UNSPECIFIEDP (attrs[LFACE_FONT_FEATURES_INDEX])
+	   || IGNORE_DEFFACE_P (attrs[LFACE_FONT_FEATURES_INDEX])
+	   || RESET_P (attrs[LFACE_FONT_FEATURES_INDEX])
+	   || NILP (attrs[LFACE_FONT_FEATURES_INDEX])
+	   || CONSP (attrs[LFACE_FONT_FEATURES_INDEX]));
 #endif
 }
 
@@ -2162,7 +2168,7 @@ lface_fully_specified_p (Lisp_Object attrs[LFACE_VECTOR_SIZE])
 
   for (i = 1; i < LFACE_VECTOR_SIZE; ++i)
     if (i != LFACE_FONT_INDEX && i != LFACE_INHERIT_INDEX
-        && i != LFACE_DISTANT_FOREGROUND_INDEX)
+        && i != LFACE_DISTANT_FOREGROUND_INDEX && i != LFACE_FONT_FEATURES_INDEX)
       if ((UNSPECIFIEDP (attrs[i]) || IGNORE_DEFFACE_P (attrs[i])))
 	break;
 
@@ -2917,6 +2923,13 @@ merge_face_ref (struct window *w,
 		  else
 		    err = true;
 		}
+	      else if (EQ (keyword, QCfont_features))
+		{
+		  if (NILP (value) || CONSP (value))
+		    to[LFACE_FONT_FEATURES_INDEX] = value;
+		  else
+		    err = true;
+		}
 	      else
 		err = true;
 
@@ -3645,6 +3658,32 @@ DEFUN ("internal-set-lisp-face-attribute", Finternal_set_lisp_face_attribute,
 	}
 #endif /* HAVE_WINDOW_SYSTEM */
     }
+  else if (EQ (attr, QCfont_features))
+    {
+      if (!UNSPECIFIEDP (value)
+	  && !IGNORE_DEFFACE_P (value)
+	  && !RESET_P (value))
+	{
+	  /* Validate font features list format:
+	     Should be a list of (feature . value) pairs */
+	  if (!NILP (value))
+	    {
+	      Lisp_Object tail;
+	      for (tail = value; CONSP (tail); tail = XCDR (tail))
+		{
+		  Lisp_Object feature_spec = XCAR (tail);
+		  if (!CONSP (feature_spec)
+		      || !SYMBOLP (XCAR (feature_spec))
+		      || !FIXNUMP (XCDR (feature_spec)))
+		    signal_error ("Invalid font features format", value);
+		}
+	      if (!NILP (tail))
+		signal_error ("Invalid font features format", value);
+	    }
+	}
+      old_value = LFACE_FONT_FEATURES (lface);
+      ASET (lface, LFACE_FONT_FEATURES_INDEX, value);
+    }
   else if (EQ (attr, QCinherit))
     {
       Lisp_Object tail;
@@ -4213,6 +4252,8 @@ DEFUN ("internal-get-lisp-face-attribute", Finternal_get_lisp_face_attribute,
     value = LFACE_FONT (lface);
   else if (EQ (keyword, QCfontset))
     value = LFACE_FONTSET (lface);
+  else if (EQ (keyword, QCfont_features))
+    value = LFACE_FONT_FEATURES (lface);
   else
     signal_error ("Invalid face attribute name", keyword);
 
@@ -4539,7 +4580,7 @@ lface_hash (Lisp_Object *v)
 	  ^ XHASH (v[LFACE_WEIGHT_INDEX])
 	  ^ XHASH (v[LFACE_SLANT_INDEX])
 	  ^ XHASH (v[LFACE_SWIDTH_INDEX])
-	  ^ XHASH (v[LFACE_HEIGHT_INDEX]));
+	  ^ XHASH (v[LFACE_FONT_FEATURES_INDEX]));
 }
 
 #ifdef HAVE_WINDOW_SYSTEM
@@ -4563,6 +4604,7 @@ lface_same_font_attributes_p (Lisp_Object *lface1, Lisp_Object *lface2)
 	  && EQ (lface1[LFACE_WEIGHT_INDEX], lface2[LFACE_WEIGHT_INDEX])
 	  && EQ (lface1[LFACE_SLANT_INDEX], lface2[LFACE_SLANT_INDEX])
 	  && EQ (lface1[LFACE_FONT_INDEX], lface2[LFACE_FONT_INDEX])
+	  && EQ (lface1[LFACE_FONT_FEATURES_INDEX], lface2[LFACE_FONT_FEATURES_INDEX])
 	  && (EQ (lface1[LFACE_FONTSET_INDEX], lface2[LFACE_FONTSET_INDEX])
 	      || (STRINGP (lface1[LFACE_FONTSET_INDEX])
 		  && STRINGP (lface2[LFACE_FONTSET_INDEX])
@@ -7366,6 +7408,7 @@ init_xfaces (void)
   face_attr_sym[LFACE_FONTSET_INDEX] = QCfontset;
   face_attr_sym[LFACE_DISTANT_FOREGROUND_INDEX] = QCdistant_foreground;
   face_attr_sym[LFACE_EXTEND_INDEX] = QCextend;
+  face_attr_sym[LFACE_FONT_FEATURES_INDEX] = QCfont_features;
 }
 
 void
@@ -7397,6 +7440,7 @@ syms_of_xfaces (void)
   DEFSYM (QCwidth, ":width");
   DEFSYM (QCfont, ":font");
   DEFSYM (QCfontset, ":fontset");
+  DEFSYM (QCfont_features, ":font-features");
   DEFSYM (QCdistant_foreground, ":distant-foreground");
   DEFSYM (QCbold, ":bold");
   DEFSYM (QCitalic, ":italic");
-- 
2.47.2