diff --git a/.gitignore b/.gitignore index ba722032553..2ae6554cf9d 100644 --- a/.gitignore +++ b/.gitignore @@ -320,3 +320,4 @@ fastlane/screenshots # Docs worktree (docs branch) .docs +.playwright-mcp/ diff --git a/binding/HarfBuzzSharp/Face.cs b/binding/HarfBuzzSharp/Face.cs index d91e1fc9ba6..6d8cdf03a11 100644 --- a/binding/HarfBuzzSharp/Face.cs +++ b/binding/HarfBuzzSharp/Face.cs @@ -85,6 +85,101 @@ public Blob ReferenceTable (Tag table) => public void MakeImmutable () => HarfBuzzApi.hb_face_make_immutable (Handle); + // Variable font support + + public bool HasVariationData => HarfBuzzApi.hb_ot_var_has_data (Handle); + + public int VariationAxisCount => + (int)HarfBuzzApi.hb_ot_var_get_axis_count (Handle); + + public OpenTypeVarAxisInfo[] VariationAxisInfos + { + get { + var count = HarfBuzzApi.hb_ot_var_get_axis_count (Handle); + if (count == 0) + return Array.Empty (); + + var axes = new OpenTypeVarAxisInfo[(int)count]; + fixed (OpenTypeVarAxisInfo* ptr = axes) { + HarfBuzzApi.hb_ot_var_get_axis_infos (Handle, 0, &count, ptr); + } + return axes; + } + } + + public int GetVariationAxisInfos (Span axes) + { + uint count = (uint)axes.Length; + fixed (OpenTypeVarAxisInfo* ptr = axes) { + HarfBuzzApi.hb_ot_var_get_axis_infos (Handle, 0, &count, ptr); + } + return (int)count; + } + + public bool TryFindVariationAxis (Tag tag, out OpenTypeVarAxisInfo axisInfo) + { + axisInfo = default; + fixed (OpenTypeVarAxisInfo* ptr = &axisInfo) { + return HarfBuzzApi.hb_ot_var_find_axis_info (Handle, tag, ptr); + } + } + + public int NamedInstanceCount => + (int)HarfBuzzApi.hb_ot_var_get_named_instance_count (Handle); + + public OpenTypeNameId GetNamedInstanceSubfamilyNameId (int instanceIndex) + { + if (instanceIndex < 0) + throw new ArgumentOutOfRangeException (nameof (instanceIndex)); + return HarfBuzzApi.hb_ot_var_named_instance_get_subfamily_name_id (Handle, (uint)instanceIndex); + } + + public OpenTypeNameId GetNamedInstancePostScriptNameId (int instanceIndex) + { + if (instanceIndex < 0) + throw new ArgumentOutOfRangeException (nameof (instanceIndex)); + return HarfBuzzApi.hb_ot_var_named_instance_get_postscript_name_id (Handle, (uint)instanceIndex); + } + + public int GetNamedInstanceDesignCoordsCount (int instanceIndex) + { + if (instanceIndex < 0) + throw new ArgumentOutOfRangeException (nameof (instanceIndex)); + + // Return value is the total number of design coordinates + return (int)HarfBuzzApi.hb_ot_var_named_instance_get_design_coords (Handle, (uint)instanceIndex, null, null); + } + + public float[] GetNamedInstanceDesignCoords (int instanceIndex) + { + if (instanceIndex < 0) + throw new ArgumentOutOfRangeException (nameof (instanceIndex)); + + // Return value is the total number of design coordinates + var totalCoords = (int)HarfBuzzApi.hb_ot_var_named_instance_get_design_coords (Handle, (uint)instanceIndex, null, null); + if (totalCoords == 0) + return Array.Empty (); + + uint coordsLength = (uint)totalCoords; + var coords = new float[totalCoords]; + fixed (float* ptr = coords) { + HarfBuzzApi.hb_ot_var_named_instance_get_design_coords (Handle, (uint)instanceIndex, &coordsLength, ptr); + } + return coords; + } + + public int GetNamedInstanceDesignCoords (int instanceIndex, Span coords) + { + if (instanceIndex < 0) + throw new ArgumentOutOfRangeException (nameof (instanceIndex)); + + uint coordsLength = (uint)coords.Length; + fixed (float* ptr = coords) { + HarfBuzzApi.hb_ot_var_named_instance_get_design_coords (Handle, (uint)instanceIndex, &coordsLength, ptr); + } + return (int)coordsLength; + } + protected override void Dispose (bool disposing) => base.Dispose (disposing); diff --git a/binding/HarfBuzzSharp/Font.cs b/binding/HarfBuzzSharp/Font.cs index 13b6676aa38..44a3bc5d2dc 100644 --- a/binding/HarfBuzzSharp/Font.cs +++ b/binding/HarfBuzzSharp/Font.cs @@ -291,6 +291,65 @@ public bool TryGetGlyphFromString (string s, out uint glyph) } } + // Variable font support + + public void SetVariations (ReadOnlySpan variations) + { + fixed (Variation* ptr = variations) { + HarfBuzzApi.hb_font_set_variations (Handle, ptr, (uint)variations.Length); + } + } + + public void SetVariationCoordsDesign (ReadOnlySpan coords) + { + fixed (float* ptr = coords) { + HarfBuzzApi.hb_font_set_var_coords_design (Handle, ptr, (uint)coords.Length); + } + } + + public void SetVariationCoordsNormalized (ReadOnlySpan coords) + { + fixed (int* ptr = coords) { + HarfBuzzApi.hb_font_set_var_coords_normalized (Handle, ptr, (uint)coords.Length); + } + } + + public int[] VariationCoordsNormalized + { + get { + uint length; + var ptr = HarfBuzzApi.hb_font_get_var_coords_normalized (Handle, &length); + if (length == 0 || ptr == null) + return Array.Empty (); + + var count = (int)length; + var coords = new int[count]; + for (int i = 0; i < count; i++) + coords[i] = ptr[i]; + return coords; + } + } + + public int GetVariationCoordsNormalized (Span coords) + { + uint length; + var ptr = HarfBuzzApi.hb_font_get_var_coords_normalized (Handle, &length); + if (length == 0 || ptr == null) + return 0; + + var count = Math.Min ((int)length, coords.Length); + for (int i = 0; i < count; i++) + coords[i] = ptr[i]; + return (int)length; + } + + public void SetVariationNamedInstance (int instanceIndex) + { + if (instanceIndex < 0) + throw new ArgumentOutOfRangeException (nameof (instanceIndex)); + HarfBuzzApi.hb_font_set_var_named_instance (Handle, (uint)instanceIndex); + } + public void SetFunctionsOpenType () => HarfBuzzApi.hb_ot_font_set_funcs (Handle); diff --git a/binding/HarfBuzzSharp/HarfBuzzApi.generated.cs b/binding/HarfBuzzSharp/HarfBuzzApi.generated.cs index 871ebbf2314..e045d0fcb38 100644 --- a/binding/HarfBuzzSharp/HarfBuzzApi.generated.cs +++ b/binding/HarfBuzzSharp/HarfBuzzApi.generated.cs @@ -5291,6 +5291,206 @@ internal static void hb_ot_shape_plan_collect_lookups (hb_shape_plan_t shape_pla #endregion + #region hb-ot-var.h + + // extern hb_bool_t hb_ot_var_find_axis_info(hb_face_t* face, hb_tag_t axis_tag, hb_ot_var_axis_info_t* axis_info) + #if !USE_DELEGATES + #if USE_LIBRARY_IMPORT + [LibraryImport (HARFBUZZ)] + [return: MarshalAs (UnmanagedType.I1)] + internal static partial bool hb_ot_var_find_axis_info (hb_face_t face, UInt32 axis_tag, OpenTypeVarAxisInfo* axis_info); + #else // !USE_LIBRARY_IMPORT + [DllImport (HARFBUZZ, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs (UnmanagedType.I1)] + internal static extern bool hb_ot_var_find_axis_info (hb_face_t face, UInt32 axis_tag, OpenTypeVarAxisInfo* axis_info); + #endif + #else + private partial class Delegates { + [UnmanagedFunctionPointer (CallingConvention.Cdecl)] + [return: MarshalAs (UnmanagedType.I1)] + internal delegate bool hb_ot_var_find_axis_info (hb_face_t face, UInt32 axis_tag, OpenTypeVarAxisInfo* axis_info); + } + private static Delegates.hb_ot_var_find_axis_info hb_ot_var_find_axis_info_delegate; + internal static bool hb_ot_var_find_axis_info (hb_face_t face, UInt32 axis_tag, OpenTypeVarAxisInfo* axis_info) => + (hb_ot_var_find_axis_info_delegate ??= GetSymbol ("hb_ot_var_find_axis_info")).Invoke (face, axis_tag, axis_info); + #endif + + // extern unsigned int hb_ot_var_get_axis_count(hb_face_t* face) + #if !USE_DELEGATES + #if USE_LIBRARY_IMPORT + [LibraryImport (HARFBUZZ)] + internal static partial UInt32 hb_ot_var_get_axis_count (hb_face_t face); + #else // !USE_LIBRARY_IMPORT + [DllImport (HARFBUZZ, CallingConvention = CallingConvention.Cdecl)] + internal static extern UInt32 hb_ot_var_get_axis_count (hb_face_t face); + #endif + #else + private partial class Delegates { + [UnmanagedFunctionPointer (CallingConvention.Cdecl)] + internal delegate UInt32 hb_ot_var_get_axis_count (hb_face_t face); + } + private static Delegates.hb_ot_var_get_axis_count hb_ot_var_get_axis_count_delegate; + internal static UInt32 hb_ot_var_get_axis_count (hb_face_t face) => + (hb_ot_var_get_axis_count_delegate ??= GetSymbol ("hb_ot_var_get_axis_count")).Invoke (face); + #endif + + // extern unsigned int hb_ot_var_get_axis_infos(hb_face_t* face, unsigned int start_offset, unsigned int* axes_count, hb_ot_var_axis_info_t* axes_array) + #if !USE_DELEGATES + #if USE_LIBRARY_IMPORT + [LibraryImport (HARFBUZZ)] + internal static partial UInt32 hb_ot_var_get_axis_infos (hb_face_t face, UInt32 start_offset, UInt32* axes_count, OpenTypeVarAxisInfo* axes_array); + #else // !USE_LIBRARY_IMPORT + [DllImport (HARFBUZZ, CallingConvention = CallingConvention.Cdecl)] + internal static extern UInt32 hb_ot_var_get_axis_infos (hb_face_t face, UInt32 start_offset, UInt32* axes_count, OpenTypeVarAxisInfo* axes_array); + #endif + #else + private partial class Delegates { + [UnmanagedFunctionPointer (CallingConvention.Cdecl)] + internal delegate UInt32 hb_ot_var_get_axis_infos (hb_face_t face, UInt32 start_offset, UInt32* axes_count, OpenTypeVarAxisInfo* axes_array); + } + private static Delegates.hb_ot_var_get_axis_infos hb_ot_var_get_axis_infos_delegate; + internal static UInt32 hb_ot_var_get_axis_infos (hb_face_t face, UInt32 start_offset, UInt32* axes_count, OpenTypeVarAxisInfo* axes_array) => + (hb_ot_var_get_axis_infos_delegate ??= GetSymbol ("hb_ot_var_get_axis_infos")).Invoke (face, start_offset, axes_count, axes_array); + #endif + + // extern unsigned int hb_ot_var_get_named_instance_count(hb_face_t* face) + #if !USE_DELEGATES + #if USE_LIBRARY_IMPORT + [LibraryImport (HARFBUZZ)] + internal static partial UInt32 hb_ot_var_get_named_instance_count (hb_face_t face); + #else // !USE_LIBRARY_IMPORT + [DllImport (HARFBUZZ, CallingConvention = CallingConvention.Cdecl)] + internal static extern UInt32 hb_ot_var_get_named_instance_count (hb_face_t face); + #endif + #else + private partial class Delegates { + [UnmanagedFunctionPointer (CallingConvention.Cdecl)] + internal delegate UInt32 hb_ot_var_get_named_instance_count (hb_face_t face); + } + private static Delegates.hb_ot_var_get_named_instance_count hb_ot_var_get_named_instance_count_delegate; + internal static UInt32 hb_ot_var_get_named_instance_count (hb_face_t face) => + (hb_ot_var_get_named_instance_count_delegate ??= GetSymbol ("hb_ot_var_get_named_instance_count")).Invoke (face); + #endif + + // extern hb_bool_t hb_ot_var_has_data(hb_face_t* face) + #if !USE_DELEGATES + #if USE_LIBRARY_IMPORT + [LibraryImport (HARFBUZZ)] + [return: MarshalAs (UnmanagedType.I1)] + internal static partial bool hb_ot_var_has_data (hb_face_t face); + #else // !USE_LIBRARY_IMPORT + [DllImport (HARFBUZZ, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs (UnmanagedType.I1)] + internal static extern bool hb_ot_var_has_data (hb_face_t face); + #endif + #else + private partial class Delegates { + [UnmanagedFunctionPointer (CallingConvention.Cdecl)] + [return: MarshalAs (UnmanagedType.I1)] + internal delegate bool hb_ot_var_has_data (hb_face_t face); + } + private static Delegates.hb_ot_var_has_data hb_ot_var_has_data_delegate; + internal static bool hb_ot_var_has_data (hb_face_t face) => + (hb_ot_var_has_data_delegate ??= GetSymbol ("hb_ot_var_has_data")).Invoke (face); + #endif + + // extern unsigned int hb_ot_var_named_instance_get_design_coords(hb_face_t* face, unsigned int instance_index, unsigned int* coords_length, float* coords) + #if !USE_DELEGATES + #if USE_LIBRARY_IMPORT + [LibraryImport (HARFBUZZ)] + internal static partial UInt32 hb_ot_var_named_instance_get_design_coords (hb_face_t face, UInt32 instance_index, UInt32* coords_length, Single* coords); + #else // !USE_LIBRARY_IMPORT + [DllImport (HARFBUZZ, CallingConvention = CallingConvention.Cdecl)] + internal static extern UInt32 hb_ot_var_named_instance_get_design_coords (hb_face_t face, UInt32 instance_index, UInt32* coords_length, Single* coords); + #endif + #else + private partial class Delegates { + [UnmanagedFunctionPointer (CallingConvention.Cdecl)] + internal delegate UInt32 hb_ot_var_named_instance_get_design_coords (hb_face_t face, UInt32 instance_index, UInt32* coords_length, Single* coords); + } + private static Delegates.hb_ot_var_named_instance_get_design_coords hb_ot_var_named_instance_get_design_coords_delegate; + internal static UInt32 hb_ot_var_named_instance_get_design_coords (hb_face_t face, UInt32 instance_index, UInt32* coords_length, Single* coords) => + (hb_ot_var_named_instance_get_design_coords_delegate ??= GetSymbol ("hb_ot_var_named_instance_get_design_coords")).Invoke (face, instance_index, coords_length, coords); + #endif + + // extern hb_ot_name_id_t hb_ot_var_named_instance_get_postscript_name_id(hb_face_t* face, unsigned int instance_index) + #if !USE_DELEGATES + #if USE_LIBRARY_IMPORT + [LibraryImport (HARFBUZZ)] + internal static partial OpenTypeNameId hb_ot_var_named_instance_get_postscript_name_id (hb_face_t face, UInt32 instance_index); + #else // !USE_LIBRARY_IMPORT + [DllImport (HARFBUZZ, CallingConvention = CallingConvention.Cdecl)] + internal static extern OpenTypeNameId hb_ot_var_named_instance_get_postscript_name_id (hb_face_t face, UInt32 instance_index); + #endif + #else + private partial class Delegates { + [UnmanagedFunctionPointer (CallingConvention.Cdecl)] + internal delegate OpenTypeNameId hb_ot_var_named_instance_get_postscript_name_id (hb_face_t face, UInt32 instance_index); + } + private static Delegates.hb_ot_var_named_instance_get_postscript_name_id hb_ot_var_named_instance_get_postscript_name_id_delegate; + internal static OpenTypeNameId hb_ot_var_named_instance_get_postscript_name_id (hb_face_t face, UInt32 instance_index) => + (hb_ot_var_named_instance_get_postscript_name_id_delegate ??= GetSymbol ("hb_ot_var_named_instance_get_postscript_name_id")).Invoke (face, instance_index); + #endif + + // extern hb_ot_name_id_t hb_ot_var_named_instance_get_subfamily_name_id(hb_face_t* face, unsigned int instance_index) + #if !USE_DELEGATES + #if USE_LIBRARY_IMPORT + [LibraryImport (HARFBUZZ)] + internal static partial OpenTypeNameId hb_ot_var_named_instance_get_subfamily_name_id (hb_face_t face, UInt32 instance_index); + #else // !USE_LIBRARY_IMPORT + [DllImport (HARFBUZZ, CallingConvention = CallingConvention.Cdecl)] + internal static extern OpenTypeNameId hb_ot_var_named_instance_get_subfamily_name_id (hb_face_t face, UInt32 instance_index); + #endif + #else + private partial class Delegates { + [UnmanagedFunctionPointer (CallingConvention.Cdecl)] + internal delegate OpenTypeNameId hb_ot_var_named_instance_get_subfamily_name_id (hb_face_t face, UInt32 instance_index); + } + private static Delegates.hb_ot_var_named_instance_get_subfamily_name_id hb_ot_var_named_instance_get_subfamily_name_id_delegate; + internal static OpenTypeNameId hb_ot_var_named_instance_get_subfamily_name_id (hb_face_t face, UInt32 instance_index) => + (hb_ot_var_named_instance_get_subfamily_name_id_delegate ??= GetSymbol ("hb_ot_var_named_instance_get_subfamily_name_id")).Invoke (face, instance_index); + #endif + + // extern void hb_ot_var_normalize_coords(hb_face_t* face, unsigned int coords_length, const float* design_coords, int* normalized_coords) + #if !USE_DELEGATES + #if USE_LIBRARY_IMPORT + [LibraryImport (HARFBUZZ)] + internal static partial void hb_ot_var_normalize_coords (hb_face_t face, UInt32 coords_length, Single* design_coords, Int32* normalized_coords); + #else // !USE_LIBRARY_IMPORT + [DllImport (HARFBUZZ, CallingConvention = CallingConvention.Cdecl)] + internal static extern void hb_ot_var_normalize_coords (hb_face_t face, UInt32 coords_length, Single* design_coords, Int32* normalized_coords); + #endif + #else + private partial class Delegates { + [UnmanagedFunctionPointer (CallingConvention.Cdecl)] + internal delegate void hb_ot_var_normalize_coords (hb_face_t face, UInt32 coords_length, Single* design_coords, Int32* normalized_coords); + } + private static Delegates.hb_ot_var_normalize_coords hb_ot_var_normalize_coords_delegate; + internal static void hb_ot_var_normalize_coords (hb_face_t face, UInt32 coords_length, Single* design_coords, Int32* normalized_coords) => + (hb_ot_var_normalize_coords_delegate ??= GetSymbol ("hb_ot_var_normalize_coords")).Invoke (face, coords_length, design_coords, normalized_coords); + #endif + + // extern void hb_ot_var_normalize_variations(hb_face_t* face, const hb_variation_t* variations, unsigned int variations_length, int* coords, unsigned int coords_length) + #if !USE_DELEGATES + #if USE_LIBRARY_IMPORT + [LibraryImport (HARFBUZZ)] + internal static partial void hb_ot_var_normalize_variations (hb_face_t face, Variation* variations, UInt32 variations_length, Int32* coords, UInt32 coords_length); + #else // !USE_LIBRARY_IMPORT + [DllImport (HARFBUZZ, CallingConvention = CallingConvention.Cdecl)] + internal static extern void hb_ot_var_normalize_variations (hb_face_t face, Variation* variations, UInt32 variations_length, Int32* coords, UInt32 coords_length); + #endif + #else + private partial class Delegates { + [UnmanagedFunctionPointer (CallingConvention.Cdecl)] + internal delegate void hb_ot_var_normalize_variations (hb_face_t face, Variation* variations, UInt32 variations_length, Int32* coords, UInt32 coords_length); + } + private static Delegates.hb_ot_var_normalize_variations hb_ot_var_normalize_variations_delegate; + internal static void hb_ot_var_normalize_variations (hb_face_t face, Variation* variations, UInt32 variations_length, Int32* coords, UInt32 coords_length) => + (hb_ot_var_normalize_variations_delegate ??= GetSymbol ("hb_ot_var_normalize_variations")).Invoke (face, variations, variations_length, coords, coords_length); + #endif + + #endregion + #region hb-set.h // extern void hb_set_add(hb_set_t* set, hb_codepoint_t codepoint) @@ -5856,7 +6056,7 @@ internal static void hb_shape (hb_font_t font, hb_buffer_t buffer, Feature* feat (hb_shape_delegate ??= GetSymbol ("hb_shape")).Invoke (font, buffer, features, num_features); #endif - // extern hb_bool_t hb_shape_full(hb_font_t* font, hb_buffer_t* buffer, const hb_feature_t* features, unsigned int num_features, const const char** shaper_list) + // extern hb_bool_t hb_shape_full(hb_font_t* font, hb_buffer_t* buffer, const hb_feature_t* features, unsigned int num_features, const char* const* shaper_list) #if !USE_DELEGATES #if USE_LIBRARY_IMPORT [LibraryImport (HARFBUZZ)] @@ -6435,7 +6635,7 @@ namespace HarfBuzzSharp { [return: MarshalAs (UnmanagedType.I1)] internal unsafe delegate bool FontGetVariationGlyphProxyDelegate(hb_font_t font, void* font_data, UInt32 unicode, UInt32 variation_selector, UInt32* glyph, void* user_data); -// TODO: typedef const hb_language_impl_t* hb_language_t +// TODO: typedef hb_language_impl_t const * hb_language_t // typedef hb_blob_t* (*)(hb_face_t* face, hb_tag_t tag, void* user_data)* hb_reference_table_func_t [UnmanagedFunctionPointer (CallingConvention.Cdecl)] internal unsafe delegate hb_blob_t ReferenceTableProxyDelegate(hb_face_t face, UInt32 tag, void* user_data); @@ -7890,7 +8090,7 @@ internal static unsafe partial class DelegateProxies { [return: MarshalAs (UnmanagedType.I1)] private static partial bool FontGetVariationGlyphProxyImplementation(hb_font_t font,void* font_data,UInt32 unicode,UInt32 variation_selector,UInt32* glyph,void* user_data); -// TODO: typedef const hb_language_impl_t* hb_language_t +// TODO: typedef hb_language_impl_t const * hb_language_t /// Proxy for hb_reference_table_func_t native function. #if USE_LIBRARY_IMPORT public static readonly delegate* unmanaged[Cdecl] ReferenceTableProxy = &ReferenceTableProxyImplementation; diff --git a/binding/SkiaSharp/SKFontArguments.cs b/binding/SkiaSharp/SKFontArguments.cs new file mode 100644 index 00000000000..4db57baed62 --- /dev/null +++ b/binding/SkiaSharp/SKFontArguments.cs @@ -0,0 +1,10 @@ +using System; + +namespace SkiaSharp; + +public ref struct SKFontArguments +{ + public ReadOnlySpan VariationDesignPosition { get; set; } + + public int CollectionIndex { get; set; } +} diff --git a/binding/SkiaSharp/SKFourByteTag.cs b/binding/SkiaSharp/SKFourByteTag.cs new file mode 100644 index 00000000000..e202c8987a1 --- /dev/null +++ b/binding/SkiaSharp/SKFourByteTag.cs @@ -0,0 +1,56 @@ +using System; + +namespace SkiaSharp; + +public readonly struct SKFourByteTag : IEquatable +{ + private readonly uint value; + + public SKFourByteTag (uint value) + { + this.value = value; + } + + public SKFourByteTag (char c1, char c2, char c3, char c4) + { + value = (uint)(((byte)c1 << 24) | ((byte)c2 << 16) | ((byte)c3 << 8) | (byte)c4); + } + + public static SKFourByteTag Parse (string? tag) + { + if (string.IsNullOrEmpty (tag)) + return new SKFourByteTag (0); + + var realTag = new char[4]; + var len = Math.Min (4, tag.Length); + var i = 0; + for (; i < len; i++) + realTag[i] = tag[i]; + for (; i < 4; i++) + realTag[i] = ' '; + + return new SKFourByteTag (realTag[0], realTag[1], realTag[2], realTag[3]); + } + + public override string ToString () => + string.Concat ( + (char)(byte)(value >> 24), + (char)(byte)(value >> 16), + (char)(byte)(value >> 8), + (char)(byte)value); + + public static implicit operator uint (SKFourByteTag tag) => tag.value; + + public static implicit operator SKFourByteTag (uint tag) => new SKFourByteTag (tag); + + public override bool Equals (object obj) => + obj is SKFourByteTag tag && value.Equals (tag.value); + + public bool Equals (SKFourByteTag other) => value == other.value; + + public override int GetHashCode () => (int)value; + + public static bool operator == (SKFourByteTag left, SKFourByteTag right) => left.Equals (right); + + public static bool operator != (SKFourByteTag left, SKFourByteTag right) => !left.Equals (right); +} diff --git a/binding/SkiaSharp/SKTypeface.cs b/binding/SkiaSharp/SKTypeface.cs index 14ff637c34a..9a2469a2fe1 100644 --- a/binding/SkiaSharp/SKTypeface.cs +++ b/binding/SkiaSharp/SKTypeface.cs @@ -340,6 +340,72 @@ public bool GetKerningPairAdjustments (ReadOnlySpan glyphs, Span ad return res; } + // Variable fonts + + public int VariationDesignParameterCount => + SkiaApi.sk_typeface_get_variation_design_parameters (Handle, null, 0); + + public SKFontVariationAxis[] VariationDesignParameters + { + get { + var count = VariationDesignParameterCount; + if (count <= 0) + return Array.Empty (); + + var axes = new SKFontVariationAxis[count]; + fixed (SKFontVariationAxis* ptr = axes) { + SkiaApi.sk_typeface_get_variation_design_parameters (Handle, ptr, count); + } + return axes; + } + } + + public int GetVariationDesignParameters (Span axes) + { + fixed (SKFontVariationAxis* ptr = axes) { + return SkiaApi.sk_typeface_get_variation_design_parameters (Handle, ptr, axes.Length); + } + } + + public int VariationDesignPositionCount => + SkiaApi.sk_typeface_get_variation_design_position (Handle, null, 0); + + public SKFontVariationPositionCoordinate[] VariationDesignPosition + { + get { + var count = VariationDesignPositionCount; + if (count <= 0) + return Array.Empty (); + + var coords = new SKFontVariationPositionCoordinate[count]; + fixed (SKFontVariationPositionCoordinate* ptr = coords) { + SkiaApi.sk_typeface_get_variation_design_position (Handle, ptr, count); + } + return coords; + } + } + + public int GetVariationDesignPosition (Span coordinates) + { + fixed (SKFontVariationPositionCoordinate* ptr = coordinates) { + return SkiaApi.sk_typeface_get_variation_design_position (Handle, ptr, coordinates.Length); + } + } + + public SKTypeface Clone (ReadOnlySpan position) + { + fixed (SKFontVariationPositionCoordinate* ptr = position) { + return GetObject (SkiaApi.sk_typeface_clone_with_arguments (Handle, ptr, position.Length, 0)); + } + } + + public SKTypeface Clone (SKFontArguments args) + { + fixed (SKFontVariationPositionCoordinate* ptr = args.VariationDesignPosition) { + return GetObject (SkiaApi.sk_typeface_clone_with_arguments (Handle, ptr, args.VariationDesignPosition.Length, args.CollectionIndex)); + } + } + // internal static SKTypeface GetObject (IntPtr handle) => diff --git a/binding/SkiaSharp/SkiaApi.generated.cs b/binding/SkiaSharp/SkiaApi.generated.cs index 338f5171a12..d85df788df3 100644 --- a/binding/SkiaSharp/SkiaApi.generated.cs +++ b/binding/SkiaSharp/SkiaApi.generated.cs @@ -15958,25 +15958,6 @@ internal static sk_typeface_t sk_fontmgr_create_from_file (sk_fontmgr_t param0, (sk_fontmgr_create_from_file_delegate ??= GetSymbol ("sk_fontmgr_create_from_file")).Invoke (param0, path, index); #endif - // sk_typeface_t* sk_fontmgr_legacy_create_typeface(sk_fontmgr_t*, const char* familyName, sk_fontstyle_t* style) - #if !USE_DELEGATES - #if USE_LIBRARY_IMPORT - [LibraryImport (SKIA)] - internal static partial sk_typeface_t sk_fontmgr_legacy_create_typeface (sk_fontmgr_t param0, IntPtr familyName, sk_fontstyle_t style); - #else // !USE_LIBRARY_IMPORT - [DllImport (SKIA, CallingConvention = CallingConvention.Cdecl)] - internal static extern sk_typeface_t sk_fontmgr_legacy_create_typeface (sk_fontmgr_t param0, IntPtr familyName, sk_fontstyle_t style); - #endif - #else - private partial class Delegates { - [UnmanagedFunctionPointer (CallingConvention.Cdecl)] - internal delegate sk_typeface_t sk_fontmgr_legacy_create_typeface (sk_fontmgr_t param0, IntPtr familyName, sk_fontstyle_t style); - } - private static Delegates.sk_fontmgr_legacy_create_typeface sk_fontmgr_legacy_create_typeface_delegate; - internal static sk_typeface_t sk_fontmgr_legacy_create_typeface (sk_fontmgr_t param0, IntPtr familyName, sk_fontstyle_t style) => - (sk_fontmgr_legacy_create_typeface_delegate ??= GetSymbol ("sk_fontmgr_legacy_create_typeface")).Invoke (param0, familyName, style); - #endif - // sk_typeface_t* sk_fontmgr_create_from_stream(sk_fontmgr_t*, sk_stream_asset_t* stream, int index) #if !USE_DELEGATES #if USE_LIBRARY_IMPORT @@ -16034,6 +16015,25 @@ internal static void sk_fontmgr_get_family_name (sk_fontmgr_t param0, Int32 inde (sk_fontmgr_get_family_name_delegate ??= GetSymbol ("sk_fontmgr_get_family_name")).Invoke (param0, index, familyName); #endif + // sk_typeface_t* sk_fontmgr_legacy_create_typeface(sk_fontmgr_t*, const char* familyName, sk_fontstyle_t* style) + #if !USE_DELEGATES + #if USE_LIBRARY_IMPORT + [LibraryImport (SKIA)] + internal static partial sk_typeface_t sk_fontmgr_legacy_create_typeface (sk_fontmgr_t param0, IntPtr familyName, sk_fontstyle_t style); + #else // !USE_LIBRARY_IMPORT + [DllImport (SKIA, CallingConvention = CallingConvention.Cdecl)] + internal static extern sk_typeface_t sk_fontmgr_legacy_create_typeface (sk_fontmgr_t param0, IntPtr familyName, sk_fontstyle_t style); + #endif + #else + private partial class Delegates { + [UnmanagedFunctionPointer (CallingConvention.Cdecl)] + internal delegate sk_typeface_t sk_fontmgr_legacy_create_typeface (sk_fontmgr_t param0, IntPtr familyName, sk_fontstyle_t style); + } + private static Delegates.sk_fontmgr_legacy_create_typeface sk_fontmgr_legacy_create_typeface_delegate; + internal static sk_typeface_t sk_fontmgr_legacy_create_typeface (sk_fontmgr_t param0, IntPtr familyName, sk_fontstyle_t style) => + (sk_fontmgr_legacy_create_typeface_delegate ??= GetSymbol ("sk_fontmgr_legacy_create_typeface")).Invoke (param0, familyName, style); + #endif + // sk_fontstyleset_t* sk_fontmgr_match_family(sk_fontmgr_t*, const char* familyName) #if !USE_DELEGATES #if USE_LIBRARY_IMPORT @@ -16319,6 +16319,25 @@ internal static void sk_fontstyleset_unref (sk_fontstyleset_t fss) => (sk_fontstyleset_unref_delegate ??= GetSymbol ("sk_fontstyleset_unref")).Invoke (fss); #endif + // sk_typeface_t* sk_typeface_clone_with_arguments(const sk_typeface_t* typeface, const sk_fontarguments_variation_position_coordinate_t* coordinates, int coordinateCount, int collectionIndex) + #if !USE_DELEGATES + #if USE_LIBRARY_IMPORT + [LibraryImport (SKIA)] + internal static partial sk_typeface_t sk_typeface_clone_with_arguments (sk_typeface_t typeface, SKFontVariationPositionCoordinate* coordinates, Int32 coordinateCount, Int32 collectionIndex); + #else // !USE_LIBRARY_IMPORT + [DllImport (SKIA, CallingConvention = CallingConvention.Cdecl)] + internal static extern sk_typeface_t sk_typeface_clone_with_arguments (sk_typeface_t typeface, SKFontVariationPositionCoordinate* coordinates, Int32 coordinateCount, Int32 collectionIndex); + #endif + #else + private partial class Delegates { + [UnmanagedFunctionPointer (CallingConvention.Cdecl)] + internal delegate sk_typeface_t sk_typeface_clone_with_arguments (sk_typeface_t typeface, SKFontVariationPositionCoordinate* coordinates, Int32 coordinateCount, Int32 collectionIndex); + } + private static Delegates.sk_typeface_clone_with_arguments sk_typeface_clone_with_arguments_delegate; + internal static sk_typeface_t sk_typeface_clone_with_arguments (sk_typeface_t typeface, SKFontVariationPositionCoordinate* coordinates, Int32 coordinateCount, Int32 collectionIndex) => + (sk_typeface_clone_with_arguments_delegate ??= GetSymbol ("sk_typeface_clone_with_arguments")).Invoke (typeface, coordinates, coordinateCount, collectionIndex); + #endif + // sk_data_t* sk_typeface_copy_table_data(const sk_typeface_t* typeface, sk_font_table_tag_t tag) #if !USE_DELEGATES #if USE_LIBRARY_IMPORT @@ -16607,6 +16626,44 @@ internal static Int32 sk_typeface_get_units_per_em (sk_typeface_t typeface) => (sk_typeface_get_units_per_em_delegate ??= GetSymbol ("sk_typeface_get_units_per_em")).Invoke (typeface); #endif + // int sk_typeface_get_variation_design_parameters(const sk_typeface_t* typeface, sk_fontarguments_variation_axis_t* parameters, int parameterCount) + #if !USE_DELEGATES + #if USE_LIBRARY_IMPORT + [LibraryImport (SKIA)] + internal static partial Int32 sk_typeface_get_variation_design_parameters (sk_typeface_t typeface, SKFontVariationAxis* parameters, Int32 parameterCount); + #else // !USE_LIBRARY_IMPORT + [DllImport (SKIA, CallingConvention = CallingConvention.Cdecl)] + internal static extern Int32 sk_typeface_get_variation_design_parameters (sk_typeface_t typeface, SKFontVariationAxis* parameters, Int32 parameterCount); + #endif + #else + private partial class Delegates { + [UnmanagedFunctionPointer (CallingConvention.Cdecl)] + internal delegate Int32 sk_typeface_get_variation_design_parameters (sk_typeface_t typeface, SKFontVariationAxis* parameters, Int32 parameterCount); + } + private static Delegates.sk_typeface_get_variation_design_parameters sk_typeface_get_variation_design_parameters_delegate; + internal static Int32 sk_typeface_get_variation_design_parameters (sk_typeface_t typeface, SKFontVariationAxis* parameters, Int32 parameterCount) => + (sk_typeface_get_variation_design_parameters_delegate ??= GetSymbol ("sk_typeface_get_variation_design_parameters")).Invoke (typeface, parameters, parameterCount); + #endif + + // int sk_typeface_get_variation_design_position(const sk_typeface_t* typeface, sk_fontarguments_variation_position_coordinate_t* coordinates, int coordinateCount) + #if !USE_DELEGATES + #if USE_LIBRARY_IMPORT + [LibraryImport (SKIA)] + internal static partial Int32 sk_typeface_get_variation_design_position (sk_typeface_t typeface, SKFontVariationPositionCoordinate* coordinates, Int32 coordinateCount); + #else // !USE_LIBRARY_IMPORT + [DllImport (SKIA, CallingConvention = CallingConvention.Cdecl)] + internal static extern Int32 sk_typeface_get_variation_design_position (sk_typeface_t typeface, SKFontVariationPositionCoordinate* coordinates, Int32 coordinateCount); + #endif + #else + private partial class Delegates { + [UnmanagedFunctionPointer (CallingConvention.Cdecl)] + internal delegate Int32 sk_typeface_get_variation_design_position (sk_typeface_t typeface, SKFontVariationPositionCoordinate* coordinates, Int32 coordinateCount); + } + private static Delegates.sk_typeface_get_variation_design_position sk_typeface_get_variation_design_position_delegate; + internal static Int32 sk_typeface_get_variation_design_position (sk_typeface_t typeface, SKFontVariationPositionCoordinate* coordinates, Int32 coordinateCount) => + (sk_typeface_get_variation_design_position_delegate ??= GetSymbol ("sk_typeface_get_variation_design_position")).Invoke (typeface, coordinates, coordinateCount); + #endif + // bool sk_typeface_is_fixed_pitch(const sk_typeface_t* typeface) #if !USE_DELEGATES #if USE_LIBRARY_IMPORT @@ -18788,6 +18845,112 @@ public readonly override int GetHashCode () } + // sk_fontarguments_variation_axis_t + [StructLayout (LayoutKind.Sequential)] + public unsafe partial struct SKFontVariationAxis : IEquatable { + // public sk_fourbytetag_t tag + private SKFourByteTag tag; + public SKFourByteTag Tag { + readonly get => tag; + set => tag = value; + } + + // public float min + private Single min; + public Single Min { + readonly get => min; + set => min = value; + } + + // public float def + private Single def; + public Single Default { + readonly get => def; + set => def = value; + } + + // public float max + private Single max; + public Single Max { + readonly get => max; + set => max = value; + } + + // public bool isHidden + private Byte isHidden; + public bool IsHidden { + readonly get => isHidden > 0; + set => isHidden = value ? (byte)1 : (byte)0; + } + + public readonly bool Equals (SKFontVariationAxis obj) => +#pragma warning disable CS8909 + tag == obj.tag && min == obj.min && def == obj.def && max == obj.max && isHidden == obj.isHidden; +#pragma warning restore CS8909 + + public readonly override bool Equals (object obj) => + obj is SKFontVariationAxis f && Equals (f); + + public static bool operator == (SKFontVariationAxis left, SKFontVariationAxis right) => + left.Equals (right); + + public static bool operator != (SKFontVariationAxis left, SKFontVariationAxis right) => + !left.Equals (right); + + public readonly override int GetHashCode () + { + var hash = new HashCode (); + hash.Add (tag); + hash.Add (min); + hash.Add (def); + hash.Add (max); + hash.Add (isHidden); + return hash.ToHashCode (); + } + + } + + // sk_fontarguments_variation_position_coordinate_t + [StructLayout (LayoutKind.Sequential)] + public unsafe partial struct SKFontVariationPositionCoordinate : IEquatable { + // public sk_fourbytetag_t axis + private SKFourByteTag axis; + public SKFourByteTag Axis { + readonly get => axis; + set => axis = value; + } + + // public float value + private Single value; + public Single Value { + readonly get => this.value; + set => this.value = value; + } + + public readonly bool Equals (SKFontVariationPositionCoordinate obj) => +#pragma warning disable CS8909 + axis == obj.axis && value == obj.value; +#pragma warning restore CS8909 + + public readonly override bool Equals (object obj) => + obj is SKFontVariationPositionCoordinate f && Equals (f); + + public static bool operator == (SKFontVariationPositionCoordinate left, SKFontVariationPositionCoordinate right) => + left.Equals (right); + + public static bool operator != (SKFontVariationPositionCoordinate left, SKFontVariationPositionCoordinate right) => + !left.Equals (right); + + public readonly override int GetHashCode () + { + var hash = new HashCode (); + hash.Add (axis); + hash.Add (value); + return hash.ToHashCode (); + } + + } + // sk_fontmetrics_t [StructLayout (LayoutKind.Sequential)] public unsafe partial struct SKFontMetrics : IEquatable { diff --git a/binding/libHarfBuzzSharp.json b/binding/libHarfBuzzSharp.json index 2ba856d24a6..999362128dc 100644 --- a/binding/libHarfBuzzSharp.json +++ b/binding/libHarfBuzzSharp.json @@ -27,8 +27,7 @@ "files": [ "src/hb-deprecated.h", "src/hb-shape-plan.h", - "src/hb-ot-deprecated.h", - "src/hb-ot-var.h" + "src/hb-ot-deprecated.h" ], "types": [ "hb_segment_properties_t", diff --git a/binding/libSkiaSharp.json b/binding/libSkiaSharp.json index 21e4e57a528..a327cd214cd 100644 --- a/binding/libSkiaSharp.json +++ b/binding/libSkiaSharp.json @@ -52,6 +52,20 @@ "sk_font_table_tag_t": { "cs": "UInt32" }, + "sk_fourbytetag_t": { + "cs": "SKFourByteTag" + }, + // variable fonts + "sk_fontarguments_variation_axis_t": { + "cs": "SKFontVariationAxis", + "members": { + "def": "Default", + "isHidden": "IsHidden" + } + }, + "sk_fontarguments_variation_position_coordinate_t": { + "cs": "SKFontVariationPositionCoordinate" + }, // the rest "gr_vk_imageinfo_t": { "cs": "GRVkImageInfo" @@ -452,6 +466,11 @@ "cs": "SKManagedWStreamDestroyProxyDelegate" }, // functions + "sk_fontmgr_legacy_create_typeface": { + "parameters": { + "1": "IntPtr" + } + }, "gr_glinterface_has_extension": { "parameters": { "1": "[MarshalAs (UnmanagedType.LPStr)] String" diff --git a/externals/skia b/externals/skia index f5345469d13..b6a88e589ff 160000 --- a/externals/skia +++ b/externals/skia @@ -1 +1 @@ -Subproject commit f5345469d1388a85decaed40f424c4461d3a39e5 +Subproject commit b6a88e589ff5ba70b7ded741ce35893565039b34 diff --git a/samples/Gallery/Shared/Media/InterVariable.ttf b/samples/Gallery/Shared/Media/InterVariable.ttf new file mode 100644 index 00000000000..4ab79e0102b Binary files /dev/null and b/samples/Gallery/Shared/Media/InterVariable.ttf differ diff --git a/samples/Gallery/Shared/SampleMedia.cs b/samples/Gallery/Shared/SampleMedia.cs index 449b89ff978..688905b99b4 100644 --- a/samples/Gallery/Shared/SampleMedia.cs +++ b/samples/Gallery/Shared/SampleMedia.cs @@ -33,6 +33,7 @@ public static class Images public static class Fonts { public static Stream EmbeddedFont => Embedded.Load("embedded-font.ttf"); + public static Stream InterVariable => Embedded.Load("InterVariable.ttf"); public static string ContentFontPath = string.Empty; } diff --git a/samples/Gallery/Shared/Samples/VariableFontSample.cs b/samples/Gallery/Shared/Samples/VariableFontSample.cs new file mode 100644 index 00000000000..921c15ef697 --- /dev/null +++ b/samples/Gallery/Shared/Samples/VariableFontSample.cs @@ -0,0 +1,149 @@ +using System; +using SkiaSharp; +using SkiaSharpSample.Controls; + +namespace SkiaSharpSample.Samples; + +public class VariableFontSample : CanvasSampleBase +{ + private float weight = 400f; + private float opticalSize = 14f; + private float textSize = 48f; + private int textIndex; + + private SKTypeface? baseTypeface; + + private static readonly string[] TextOptions = + { + "Variable SkiaSharp", + "The Quick Brown Fox", + "Hello World!", + "AaBbCcDdEeFf", + "0123456789", + }; + + public override string Title => "Variable Fonts"; + + public override string Description => + "Explore OpenType variable font axes — adjust weight and optical size in real time using Inter."; + + public override string Category => SampleCategories.Text; + + public override IReadOnlyList Controls => + [ + new PickerControl("text", "Text", TextOptions, textIndex), + new SliderControl("weight", "Weight (wght)", 100, 900, weight, 1), + new SliderControl("opticalSize", "Optical Size (opsz)", 14, 32, opticalSize, 1), + new SliderControl("textSize", "Text Size", 16, 120, textSize), + ]; + + protected override void OnControlChanged(string id, object value) + { + switch (id) + { + case "text": textIndex = (int)value; break; + case "weight": weight = (float)value; break; + case "opticalSize": opticalSize = (float)value; break; + case "textSize": textSize = (float)value; break; + } + } + + protected override void OnDrawSample(SKCanvas canvas, int width, int height) + { + canvas.Clear(SKColors.White); + + if (baseTypeface == null) + return; + + var position = new[] + { + new SKFontVariationPositionCoordinate { Axis = SKFourByteTag.Parse("wght"), Value = weight }, + new SKFontVariationPositionCoordinate { Axis = SKFourByteTag.Parse("opsz"), Value = opticalSize }, + }; + + using var typeface = baseTypeface.Clone(position); + if (typeface == null) + return; + + using var font = new SKFont(typeface, textSize); + using var paint = new SKPaint { Color = SKColors.Black, IsAntialias = true }; + + var text = TextOptions[textIndex]; + var x = width / 2f; + var y = height * 0.35f; + + // Draw the sample text centered + font.MeasureText(text, out var bounds, paint); + canvas.DrawText(text, x - bounds.MidX, y, font, paint); + + // Draw axis info below + using var infoFont = new SKFont(typeface, Math.Max(14, textSize * 0.35f)); + using var infoPaint = new SKPaint { Color = new SKColor(0x88, 0x88, 0x88), IsAntialias = true }; + + var info = $"wght: {weight:F0} opsz: {opticalSize:F0}"; + infoFont.MeasureText(info, out var infoBounds, infoPaint); + canvas.DrawText(info, x - infoBounds.MidX, y + bounds.Height + 40, infoFont, infoPaint); + + // Draw weight spectrum at the bottom + DrawWeightSpectrum(canvas, width, height, typeface); + } + + private static readonly float[] SpectrumWeights = { 100, 200, 300, 400, 500, 600, 700, 800, 900 }; + + private void DrawWeightSpectrum(SKCanvas canvas, int width, int height, SKTypeface currentTypeface) + { + var spectrumY = height * 0.65f; + var spectrumText = "Aa"; + var spectrumSize = Math.Max(20, textSize * 0.6f); + var spacing = width / (float)(SpectrumWeights.Length + 1); + + using var labelPaint = new SKPaint { Color = new SKColor(0xAA, 0xAA, 0xAA), IsAntialias = true }; + using var labelFont = new SKFont(currentTypeface, 11); + + for (int i = 0; i < SpectrumWeights.Length; i++) + { + var w = SpectrumWeights[i]; + var cx = spacing * (i + 1); + + var pos = new[] + { + new SKFontVariationPositionCoordinate { Axis = SKFourByteTag.Parse("wght"), Value = w }, + new SKFontVariationPositionCoordinate { Axis = SKFourByteTag.Parse("opsz"), Value = opticalSize }, + }; + + using var tf = baseTypeface!.Clone(pos); + if (tf == null) + continue; + + using var f = new SKFont(tf, spectrumSize); + using var p = new SKPaint + { + Color = Math.Abs(w - weight) < 1 ? new SKColor(0x33, 0x99, 0xDD) : SKColors.Black, + IsAntialias = true, + }; + + f.MeasureText(spectrumText, out var b, p); + canvas.DrawText(spectrumText, cx - b.MidX, spectrumY, f, p); + + // Weight label + var label = $"{w:F0}"; + labelFont.MeasureText(label, out var lb, labelPaint); + canvas.DrawText(label, cx - lb.MidX, spectrumY + b.Height + 16, labelFont, labelPaint); + } + } + + protected override System.Threading.Tasks.Task OnInit() + { + using var stream = SampleMedia.Fonts.InterVariable; + using var data = SKData.Create(stream); + baseTypeface = SKTypeface.FromData(data); + return base.OnInit(); + } + + protected override void OnDestroy() + { + baseTypeface?.Dispose(); + baseTypeface = null; + base.OnDestroy(); + } +} diff --git a/tests/Tests/HarfBuzzSharp/HBFaceTest.cs b/tests/Tests/HarfBuzzSharp/HBFaceTest.cs index 45e0944515b..953390d603c 100644 --- a/tests/Tests/HarfBuzzSharp/HBFaceTest.cs +++ b/tests/Tests/HarfBuzzSharp/HBFaceTest.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Xunit; @@ -6,6 +7,247 @@ namespace HarfBuzzSharp.Tests { public class HBFaceTest : HBTest { + // Variable font helpers + private Face CreateVariableFace () + { + using var blob = Blob.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + return new Face (blob, 0); + } + + // US1: Query Variable Font Axes + + [SkippableFact] + public void CanGetVariationAxisCount () + { + using var face = CreateVariableFace (); + Assert.True (face.VariationAxisCount > 0); + } + + [SkippableFact] + public void CanGetVariationAxisInfos () + { + using var face = CreateVariableFace (); + var axes = face.VariationAxisInfos; + Assert.NotEmpty (axes); + + // Distortable.ttf should have at least one axis + var axis = axes[0]; + Assert.NotEqual ((uint)0, axis.Tag); + Assert.True (axis.MinValue <= axis.DefaultValue); + Assert.True (axis.DefaultValue <= axis.MaxValue); + } + + [SkippableFact] + public void VariationAxisCountIsZeroForStaticFont () + { + using var face = new Face (Blob, 0); + Assert.Equal (0, face.VariationAxisCount); + } + + [SkippableFact] + public void TryFindVariationAxisReturnsTrueForExistingAxis () + { + using var face = CreateVariableFace (); + var axes = face.VariationAxisInfos; + Assert.NotEmpty (axes); + + Tag axisTag = axes[0].Tag; + var found = face.TryFindVariationAxis (axisTag, out var axisInfo); + Assert.True (found); + Assert.Equal (axes[0].Tag, axisInfo.Tag); + } + + [SkippableFact] + public void TryFindVariationAxisReturnsFalseForMissingAxis () + { + using var face = CreateVariableFace (); + // Use a tag that is unlikely to exist + var found = face.TryFindVariationAxis (Tag.Parse ("zzzz"), out _); + Assert.False (found); + } + + // US3: Named Instances + + [SkippableFact] + public void VariationAxisCountMatchesArrayLength () + { + using var face = CreateVariableFace (); + var axes = face.VariationAxisInfos; + Assert.Equal (face.VariationAxisCount, axes.Length); + } + + [SkippableFact] + public void SpanGetVariationAxisInfosMatchesArrayVersion () + { + using var face = CreateVariableFace (); + var arrayResult = face.VariationAxisInfos; + Assert.NotEmpty (arrayResult); + + var spanBuffer = new OpenTypeVarAxisInfo[face.VariationAxisCount]; + var written = face.GetVariationAxisInfos (spanBuffer); + Assert.Equal (arrayResult.Length, written); + + for (int i = 0; i < arrayResult.Length; i++) { + Assert.Equal (arrayResult[i].Tag, spanBuffer[i].Tag); + Assert.Equal (arrayResult[i].MinValue, spanBuffer[i].MinValue); + Assert.Equal (arrayResult[i].DefaultValue, spanBuffer[i].DefaultValue); + Assert.Equal (arrayResult[i].MaxValue, spanBuffer[i].MaxValue); + } + } + + [SkippableFact] + public void SpanGetVariationAxisInfosWithOversizedBuffer () + { + using var face = CreateVariableFace (); + var axisCount = face.VariationAxisCount; + Assert.True (axisCount > 0); + + // Pass a buffer larger than needed + var spanBuffer = new OpenTypeVarAxisInfo[axisCount + 5]; + var written = face.GetVariationAxisInfos (spanBuffer); + Assert.Equal (axisCount, written); + } + + [SkippableFact] + public void CanGetNamedInstanceCount () + { + using var face = CreateVariableFace (); + // Distortable.ttf has 3 named instances + Assert.True (face.NamedInstanceCount > 0); + } + + [SkippableFact] + public void NamedInstanceDesignCoordsCountMatchesAxisCount () + { + using var face = CreateVariableFace (); + Assert.True (face.NamedInstanceCount > 0); + + // Per HarfBuzz docs, design coords count equals the number of axes + var coordsCount = face.GetNamedInstanceDesignCoordsCount (0); + Assert.Equal (face.VariationAxisCount, coordsCount); + } + + [SkippableFact] + public void NamedInstanceDesignCoordsArrayLengthMatchesCount () + { + using var face = CreateVariableFace (); + Assert.True (face.NamedInstanceCount > 0); + + var coordsCount = face.GetNamedInstanceDesignCoordsCount (0); + var coords = face.GetNamedInstanceDesignCoords (0); + Assert.Equal (coordsCount, coords.Length); + Assert.NotEmpty (coords); + } + + [SkippableFact] + public void SpanGetNamedInstanceDesignCoordsMatchesArrayVersion () + { + using var face = CreateVariableFace (); + Assert.True (face.NamedInstanceCount > 0); + + var arrayResult = face.GetNamedInstanceDesignCoords (0); + Assert.NotEmpty (arrayResult); + + var spanBuffer = new float[face.GetNamedInstanceDesignCoordsCount (0)]; + var written = face.GetNamedInstanceDesignCoords (0, spanBuffer); + Assert.Equal (arrayResult.Length, written); + + for (int i = 0; i < arrayResult.Length; i++) + Assert.Equal (arrayResult[i], spanBuffer[i]); + } + + [SkippableFact] + public void EachNamedInstanceHasDesignCoords () + { + using var face = CreateVariableFace (); + var instanceCount = face.NamedInstanceCount; + Assert.True (instanceCount > 0); + + for (int i = 0; i < instanceCount; i++) { + var coords = face.GetNamedInstanceDesignCoords (i); + Assert.NotEmpty (coords); + Assert.Equal (face.VariationAxisCount, coords.Length); + } + } + + [SkippableFact] + public void NamedInstanceSubfamilyNameIdIsValid () + { + using var face = CreateVariableFace (); + Assert.True (face.NamedInstanceCount > 0); + + var nameId = face.GetNamedInstanceSubfamilyNameId (0); + // Name IDs are unsigned; a valid named instance should have a non-zero subfamily name ID + Assert.True ((uint)nameId > 0); + } + + [SkippableFact] + public void NamedInstancePostScriptNameIdDoesNotThrow () + { + using var face = CreateVariableFace (); + Assert.True (face.NamedInstanceCount > 0); + + // PostScript name ID may be 0xFFFF (invalid) for some fonts, but it should not throw + var nameId = face.GetNamedInstancePostScriptNameId (0); + // Just verify it returns without error + } + + [SkippableFact] + public void NegativeInstanceIndexThrowsForSubfamilyNameId () + { + using var face = CreateVariableFace (); + Assert.Throws (() => face.GetNamedInstanceSubfamilyNameId (-1)); + } + + [SkippableFact] + public void NegativeInstanceIndexThrowsForPostScriptNameId () + { + using var face = CreateVariableFace (); + Assert.Throws (() => face.GetNamedInstancePostScriptNameId (-1)); + } + + [SkippableFact] + public void NegativeInstanceIndexThrowsForDesignCoords () + { + using var face = CreateVariableFace (); + Assert.Throws (() => face.GetNamedInstanceDesignCoords (-1)); + } + + [SkippableFact] + public void NegativeInstanceIndexThrowsForDesignCoordsCount () + { + using var face = CreateVariableFace (); + Assert.Throws (() => face.GetNamedInstanceDesignCoordsCount (-1)); + } + + [SkippableFact] + public void NegativeInstanceIndexThrowsForDesignCoordsSpan () + { + using var face = CreateVariableFace (); + Assert.Throws (() => face.GetNamedInstanceDesignCoords (-1, new float[1])); + } + + [SkippableFact] + public void NamedInstanceCountIsZeroForStaticFont () + { + using var face = new Face (Blob, 0); + Assert.Equal (0, face.NamedInstanceCount); + } + + [SkippableFact] + public void HasVariationDataIsTrueForVariableFont () + { + using var face = CreateVariableFace (); + Assert.True (face.HasVariationData); + } + + [SkippableFact] + public void HasVariationDataIsFalseForStaticFont () + { + using var face = new Face (Blob, 0); + Assert.False (face.HasVariationData); + } + [SkippableFact] public void ShouldHaveGlyphCount() { diff --git a/tests/Tests/HarfBuzzSharp/HBFontTest.cs b/tests/Tests/HarfBuzzSharp/HBFontTest.cs index 32088a4460d..5361b1567da 100644 --- a/tests/Tests/HarfBuzzSharp/HBFontTest.cs +++ b/tests/Tests/HarfBuzzSharp/HBFontTest.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.IO; using System.Text; using Xunit; @@ -7,6 +8,216 @@ namespace HarfBuzzSharp.Tests { public class HbFontTest : HBTest { + // Variable font helpers + private (Face face, Font font) CreateVariableFontPair () + { + using var blob = Blob.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + var face = new Face (blob, 0); + var font = new Font (face); + return (face, font); + } + + // US2: Set Font Variation Values + + [SkippableFact] + public void CanSetVariations () + { + var (face, font) = CreateVariableFontPair (); + using (face) + using (font) { + var axes = face.VariationAxisInfos; + Assert.NotEmpty (axes); + + var variations = new Variation[] { + new Variation { Tag = axes[0].Tag, Value = axes[0].DefaultValue } + }; + font.SetVariations (variations); + } + } + + [SkippableFact] + public void CanSetMultipleVariationsSimultaneously () + { + var (face, font) = CreateVariableFontPair (); + using (face) + using (font) { + var axes = face.VariationAxisInfos; + if (axes.Length < 1) return; + + // Set all axes to their min values + var variations = new Variation[axes.Length]; + for (int i = 0; i < axes.Length; i++) { + variations[i] = new Variation { Tag = axes[i].Tag, Value = axes[i].MinValue }; + } + font.SetVariations (variations); + } + } + + [SkippableFact] + public void CanSetVarCoordsDesign () + { + var (face, font) = CreateVariableFontPair (); + using (face) + using (font) { + var axisCount = face.VariationAxisCount; + Assert.True (axisCount > 0); + + var coords = new float[axisCount]; + var axes = face.VariationAxisInfos; + for (int i = 0; i < axisCount; i++) + coords[i] = axes[i].DefaultValue; + + font.SetVariationCoordsDesign (coords); + } + } + + [SkippableFact] + public void SetVariationsOnStaticFontDoesNotThrow () + { + using var face = new Face (Blob, 0); + using var font = new Font (face); + var variations = new Variation[] { + new Variation { Tag = Tag.Parse ("wght"), Value = 400 } + }; + font.SetVariations (variations); // Should not throw + } + + // US3: Named Instances + + [SkippableFact] + public void CanSetVarNamedInstance () + { + var (face, font) = CreateVariableFontPair (); + using (face) + using (font) { + var count = face.NamedInstanceCount; + Assert.True (count > 0); + font.SetVariationNamedInstance (0); // Should not throw + } + } + + // US4: Normalized Coordinates + + [SkippableFact] + public void CanSetAndGetNormalizedCoords () + { + var (face, font) = CreateVariableFontPair (); + using (face) + using (font) { + var axisCount = face.VariationAxisCount; + Assert.True (axisCount > 0); + + // Set normalized coords (HarfBuzz uses 16.16 fixed point: 16384 = 1.0) + var coords = new int[axisCount]; + coords[0] = 8192; // 0.5 in normalized space + font.SetVariationCoordsNormalized (coords); + + var result = font.VariationCoordsNormalized; + Assert.Equal (axisCount, result.Length); + Assert.Equal (8192, result[0]); + } + } + + [SkippableFact] + public void SpanGetVariationCoordsNormalizedMatchesProperty () + { + var (face, font) = CreateVariableFontPair (); + using (face) + using (font) { + var axisCount = face.VariationAxisCount; + Assert.True (axisCount > 0); + + var coords = new int[axisCount]; + coords[0] = 8192; + font.SetVariationCoordsNormalized (coords); + + var propertyResult = font.VariationCoordsNormalized; + var spanBuffer = new int[axisCount]; + var written = font.GetVariationCoordsNormalized (spanBuffer); + + Assert.Equal (propertyResult.Length, written); + for (int i = 0; i < propertyResult.Length; i++) + Assert.Equal (propertyResult[i], spanBuffer[i]); + } + } + + [SkippableFact] + public void SpanGetVariationCoordsNormalizedReturnsTotalLengthWhenBufferSmall () + { + var (face, font) = CreateVariableFontPair (); + using (face) + using (font) { + var axisCount = face.VariationAxisCount; + Assert.True (axisCount > 0); + + var coords = new int[axisCount]; + coords[0] = 4096; + font.SetVariationCoordsNormalized (coords); + + // Pass an empty buffer — should return total length without crashing + var emptyBuffer = new int[0]; + var totalLength = font.GetVariationCoordsNormalized (emptyBuffer); + Assert.Equal (axisCount, totalLength); + } + } + + [SkippableFact] + public void SetVariationCoordsDesignAffectsNormalizedCoords () + { + var (face, font) = CreateVariableFontPair (); + using (face) + using (font) { + var axes = face.VariationAxisInfos; + Assert.NotEmpty (axes); + + // Set design coords to min value + var designCoords = new float[axes.Length]; + designCoords[0] = axes[0].MinValue; + font.SetVariationCoordsDesign (designCoords); + + // Normalized coords should now be non-zero (unless min == default) + var normalized = font.VariationCoordsNormalized; + Assert.Equal (axes.Length, normalized.Length); + + // Now set to max and verify it's different + designCoords[0] = axes[0].MaxValue; + font.SetVariationCoordsDesign (designCoords); + var normalized2 = font.VariationCoordsNormalized; + + if (axes[0].MinValue != axes[0].MaxValue) + Assert.NotEqual (normalized[0], normalized2[0]); + } + } + + [SkippableFact] + public void NegativeInstanceIndexThrowsForSetVariationNamedInstance () + { + var (face, font) = CreateVariableFontPair (); + using (face) + using (font) { + Assert.Throws (() => font.SetVariationNamedInstance (-1)); + } + } + + [SkippableFact] + public void NormalizedCoordsAreEmptyForStaticFont () + { + using var face = new Face (Blob, 0); + using var font = new Font (face); + var coords = font.VariationCoordsNormalized; + Assert.Empty (coords); + } + + [SkippableFact] + public void SpanNormalizedCoordsReturnsZeroForStaticFont () + { + using var face = new Face (Blob, 0); + using var font = new Font (face); + var buffer = new int[4]; + var length = font.GetVariationCoordsNormalized (buffer); + Assert.Equal (0, length); + } + [SkippableFact] public void ShouldHaveDefaultSupportedShapers() { diff --git a/tests/Tests/SkiaSharp/SKFourByteTagTest.cs b/tests/Tests/SkiaSharp/SKFourByteTagTest.cs new file mode 100644 index 00000000000..ea5597ff1a9 --- /dev/null +++ b/tests/Tests/SkiaSharp/SKFourByteTagTest.cs @@ -0,0 +1,145 @@ +using System; +using System.IO; +using Xunit; + +namespace SkiaSharp.Tests +{ + public class SKFourByteTagTest : SKTest + { + [SkippableFact] + public void FourByteTagParseProducesCorrectValue () + { + var tag = SKFourByteTag.Parse ("wght"); + Assert.Equal (0x77676874u, (uint)tag); + } + + [SkippableFact] + public void FourByteTagToStringRoundTrips () + { + var tag = SKFourByteTag.Parse ("wght"); + Assert.Equal ("wght", tag.ToString ()); + } + + [SkippableFact] + public void FourByteTagCharConstructorMatchesParse () + { + var fromParse = SKFourByteTag.Parse ("wdth"); + var fromChars = new SKFourByteTag ('w', 'd', 't', 'h'); + Assert.Equal (fromParse, fromChars); + } + + [SkippableFact] + public void FourByteTagImplicitConversionRoundTrips () + { + SKFourByteTag tag = SKFourByteTag.Parse ("slnt"); + uint rawValue = tag; + SKFourByteTag back = rawValue; + Assert.Equal (tag, back); + Assert.Equal ("slnt", back.ToString ()); + } + + [SkippableFact] + public void FourByteTagEqualityWorks () + { + var a = SKFourByteTag.Parse ("wght"); + var b = SKFourByteTag.Parse ("wght"); + var c = SKFourByteTag.Parse ("wdth"); + + Assert.Equal (a, b); + Assert.True (a == b); + Assert.False (a != b); + Assert.NotEqual (a, c); + Assert.True (a != c); + Assert.False (a == c); + } + + [SkippableFact] + public void FourByteTagDefaultIsZero () + { + var tag = default(SKFourByteTag); + Assert.Equal (0u, (uint)tag); + } + + [SkippableFact] + public void FourByteTagParseEmptyReturnsZero () + { + var tag = SKFourByteTag.Parse (""); + Assert.Equal (0u, (uint)tag); + } + + [SkippableFact] + public void FourByteTagParseNullReturnsZero () + { + var tag = SKFourByteTag.Parse (null); + Assert.Equal (0u, (uint)tag); + } + + [SkippableFact] + public void FourByteTagParseShortStringPadsWithSpaces () + { + // "ab" should become "ab " (padded with spaces) + var tag = SKFourByteTag.Parse ("ab"); + Assert.Equal ("ab ", tag.ToString ()); + } + + [SkippableFact] + public void FourByteTagParseLongStringTruncates () + { + // Only first 4 characters used + var tag = SKFourByteTag.Parse ("abcdef"); + Assert.Equal ("abcd", tag.ToString ()); + } + + [SkippableFact] + public void FourByteTagKnownTagValues () + { + // Well-known OpenType tag values + Assert.Equal (0x77676874u, (uint)SKFourByteTag.Parse ("wght")); + Assert.Equal (0x77647468u, (uint)SKFourByteTag.Parse ("wdth")); + Assert.Equal (0x736C6E74u, (uint)SKFourByteTag.Parse ("slnt")); + Assert.Equal (0x6F70737Au, (uint)SKFourByteTag.Parse ("opsz")); + Assert.Equal (0x6974616Cu, (uint)SKFourByteTag.Parse ("ital")); + } + + [SkippableFact] + public void FourByteTagGetHashCodeConsistent () + { + var a = SKFourByteTag.Parse ("wght"); + var b = SKFourByteTag.Parse ("wght"); + Assert.Equal (a.GetHashCode (), b.GetHashCode ()); + } + + [SkippableFact] + public void FourByteTagEqualsObjectWorks () + { + var tag = SKFourByteTag.Parse ("wght"); + Assert.True (tag.Equals ((object)SKFourByteTag.Parse ("wght"))); + Assert.False (tag.Equals ((object)SKFourByteTag.Parse ("wdth"))); + Assert.False (tag.Equals ("wght")); // wrong type + } + + [SkippableFact] + public void FourByteTagSurvivesNativeRoundTrip () + { + // Create a typeface, clone with a specific tag, read it back + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + var wght = SKFourByteTag.Parse ("wght"); + var position = new[] { + new SKFontVariationPositionCoordinate { Axis = wght, Value = 1.5f } + }; + + using var cloned = typeface.Clone (position); + Assert.NotNull (cloned); + + // The tag should survive the round-trip through C API + var readBack = cloned.VariationDesignPosition; + Assert.Single (readBack); + Assert.Equal (wght, readBack[0].Axis); + Assert.Equal ("wght", readBack[0].Axis.ToString ()); + Assert.Equal (1.5f, readBack[0].Value); + } + + } +} diff --git a/tests/Tests/SkiaSharp/SKTypefaceTest.cs b/tests/Tests/SkiaSharp/SKTypefaceTest.cs index 2756ad5d3bd..08f71947657 100644 --- a/tests/Tests/SkiaSharp/SKTypefaceTest.cs +++ b/tests/Tests/SkiaSharp/SKTypefaceTest.cs @@ -697,5 +697,328 @@ public void ZeroGlyphFontHasFamilyName() Assert.Equal("ZeroGlyphs", tf.FamilyName); } + + // Variable font tests + + [SkippableFact] + public void CanGetVariationDesignParameters () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + var axes = typeface.VariationDesignParameters; + Assert.NotEmpty (axes); + + var axis = axes[0]; + Assert.NotEqual (default(SKFourByteTag), axis.Tag); + Assert.True (axis.Min <= axis.Default); + Assert.True (axis.Default <= axis.Max); + } + + [SkippableFact] + public void VariationDesignParametersEmptyForStaticFont () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "content-font.ttf")); + Assert.NotNull (typeface); + + var axes = typeface.VariationDesignParameters; + Assert.Empty (axes); + } + + [SkippableFact] + public void CanGetVariationDesignPosition () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + var position = typeface.VariationDesignPosition; + Assert.NotEmpty (position); + } + + [SkippableFact] + public void CanClone () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + var axes = typeface.VariationDesignParameters; + Assert.NotEmpty (axes); + + var position = new SKFontVariationPositionCoordinate[] { + new SKFontVariationPositionCoordinate { Axis = axes[0].Tag, Value = axes[0].Max } + }; + + using var cloned = typeface.Clone (position); + Assert.NotNull (cloned); + + // Verify the cloned typeface has the new position + var clonedPosition = cloned.VariationDesignPosition; + Assert.NotEmpty (clonedPosition); + Assert.Equal (axes[0].Max, clonedPosition[0].Value); + } + + [SkippableFact] + public void CloneOnStaticFontReturnsTypeface () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "content-font.ttf")); + Assert.NotNull (typeface); + + var position = new SKFontVariationPositionCoordinate[] { + new SKFontVariationPositionCoordinate { Axis = SKFourByteTag.Parse ("wght"), Value = 400 } + }; + + // Static fonts should handle this gracefully — Skia returns a valid typeface + using var cloned = typeface.Clone (position); + Assert.NotNull (cloned); + } + + [SkippableFact] + public void SpanGetVariationDesignParametersMatchesProperty () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + var arrayResult = typeface.VariationDesignParameters; + Assert.NotEmpty (arrayResult); + + var spanBuffer = new SKFontVariationAxis[arrayResult.Length]; + var written = typeface.GetVariationDesignParameters (spanBuffer); + Assert.Equal (arrayResult.Length, written); + + for (int i = 0; i < arrayResult.Length; i++) { + Assert.Equal (arrayResult[i].Tag, spanBuffer[i].Tag); + Assert.Equal (arrayResult[i].Min, spanBuffer[i].Min); + Assert.Equal (arrayResult[i].Default, spanBuffer[i].Default); + Assert.Equal (arrayResult[i].Max, spanBuffer[i].Max); + } + } + + [SkippableFact] + public void SpanGetVariationDesignPositionMatchesProperty () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + var arrayResult = typeface.VariationDesignPosition; + Assert.NotEmpty (arrayResult); + + var spanBuffer = new SKFontVariationPositionCoordinate[arrayResult.Length]; + var written = typeface.GetVariationDesignPosition (spanBuffer); + Assert.Equal (arrayResult.Length, written); + + for (int i = 0; i < arrayResult.Length; i++) { + Assert.Equal (arrayResult[i].Axis, spanBuffer[i].Axis); + Assert.Equal (arrayResult[i].Value, spanBuffer[i].Value); + } + } + + [SkippableFact] + public void SpanVariationDesignParametersEmptyForStaticFont () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "content-font.ttf")); + Assert.NotNull (typeface); + + var spanBuffer = new SKFontVariationAxis[4]; + var written = typeface.GetVariationDesignParameters (spanBuffer); + Assert.True (written <= 0); + } + + [SkippableFact] + public void SpanVariationDesignPositionEmptyForStaticFont () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "content-font.ttf")); + Assert.NotNull (typeface); + + var spanBuffer = new SKFontVariationPositionCoordinate[4]; + var written = typeface.GetVariationDesignPosition (spanBuffer); + Assert.True (written <= 0); + } + + [SkippableFact] + public void CloneWithReadOnlySpan () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + var axes = typeface.VariationDesignParameters; + Assert.NotEmpty (axes); + + ReadOnlySpan position = new[] { + new SKFontVariationPositionCoordinate { Axis = axes[0].Tag, Value = axes[0].Min } + }; + + using var cloned = typeface.Clone (position); + Assert.NotNull (cloned); + + var clonedPosition = cloned.VariationDesignPosition; + Assert.NotEmpty (clonedPosition); + Assert.Equal (axes[0].Min, clonedPosition[0].Value); + } + + // Exact axis value tests — verify interop produces correct values + + [SkippableFact] + public void DistortableFontHasExactAxisValues () + { + // Distortable.ttf has 1 axis: wght min=0.5 default=1.0 max=2.0 + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + var axes = typeface.VariationDesignParameters; + Assert.Single (axes); + + var axis = axes[0]; + Assert.Equal (SKFourByteTag.Parse ("wght"), axis.Tag); + Assert.Equal ("wght", axis.Tag.ToString ()); + Assert.Equal (0.5f, axis.Min); + Assert.Equal (1.0f, axis.Default); + Assert.Equal (2.0f, axis.Max); + Assert.False (axis.IsHidden); + } + + [SkippableFact] + public void DistortableFontDefaultPositionMatchesAxis () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + var position = typeface.VariationDesignPosition; + Assert.Single (position); + Assert.Equal (SKFourByteTag.Parse ("wght"), position[0].Axis); + Assert.Equal (1.0f, position[0].Value); // default value + } + + [SkippableFact] + public void ClonePreservesExactDesignPosition () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + // Clone with min value + var position = new[] { + new SKFontVariationPositionCoordinate { Axis = SKFourByteTag.Parse ("wght"), Value = 0.5f } + }; + using var cloned = typeface.Clone (position); + Assert.NotNull (cloned); + + var clonedPos = cloned.VariationDesignPosition; + Assert.Single (clonedPos); + Assert.Equal (SKFourByteTag.Parse ("wght"), clonedPos[0].Axis); + Assert.Equal (0.5f, clonedPos[0].Value); + } + + [SkippableFact] + public void VariationDesignParameterCountMatchesArray () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + Assert.Equal (1, typeface.VariationDesignParameterCount); + Assert.Equal (typeface.VariationDesignParameterCount, typeface.VariationDesignParameters.Length); + } + + [SkippableFact] + public void VariationDesignPositionCountMatchesArray () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + Assert.Equal (1, typeface.VariationDesignPositionCount); + Assert.Equal (typeface.VariationDesignPositionCount, typeface.VariationDesignPosition.Length); + } + + // Interop safety tests — verify struct layout through native round-trip + + [SkippableFact] + public void VariationAxisStructLayoutMatchesNative () + { + // Verify that SKFontVariationAxis fields are in the correct order + // by reading known values from Distortable.ttf + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + // Use Span overload to verify the same data comes through + var axes = new SKFontVariationAxis[1]; + var count = typeface.GetVariationDesignParameters (axes); + Assert.Equal (1, count); + + // All fields must have survived the P/Invoke + Assert.Equal (SKFourByteTag.Parse ("wght"), axes[0].Tag); + Assert.Equal (0.5f, axes[0].Min); + Assert.Equal (1.0f, axes[0].Default); + Assert.Equal (2.0f, axes[0].Max); + Assert.False (axes[0].IsHidden); + } + + // SKFontArguments tests + + [SkippableFact] + public void CloneWithFontArguments () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + using var cloned = typeface.Clone (new SKFontArguments { + VariationDesignPosition = new[] { + new SKFontVariationPositionCoordinate { Axis = SKFourByteTag.Parse ("wght"), Value = 0.5f } + } + }); + Assert.NotNull (cloned); + + var pos = cloned.VariationDesignPosition; + Assert.Single (pos); + Assert.Equal (0.5f, pos[0].Value); + } + + [SkippableFact] + public void CloneWithFontArgumentsDefaultIsNoOp () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + // Empty args — should clone with defaults + using var cloned = typeface.Clone (new SKFontArguments ()); + Assert.NotNull (cloned); + } + + [SkippableFact] + public void CloneWithFontArgumentsCollectionIndex () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + using var cloned = typeface.Clone (new SKFontArguments { + CollectionIndex = 0, + VariationDesignPosition = new[] { + new SKFontVariationPositionCoordinate { Axis = SKFourByteTag.Parse ("wght"), Value = 2.0f } + } + }); + Assert.NotNull (cloned); + + var pos = cloned.VariationDesignPosition; + Assert.Single (pos); + Assert.Equal (2.0f, pos[0].Value); + } + + [SkippableFact] + public void CloneWithFontArgumentsStackalloc () + { + using var typeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "Distortable.ttf")); + Assert.NotNull (typeface); + + Span pos = stackalloc SKFontVariationPositionCoordinate[] { + new SKFontVariationPositionCoordinate { Axis = SKFourByteTag.Parse ("wght"), Value = 1.5f } + }; + + using var cloned = typeface.Clone (new SKFontArguments { + VariationDesignPosition = pos + }); + Assert.NotNull (cloned); + + var result = cloned.VariationDesignPosition; + Assert.Single (result); + Assert.Equal (1.5f, result[0].Value); + } + } } diff --git a/tests/Tests/SkiaSharp/SKVariableFontRenderingTest.cs b/tests/Tests/SkiaSharp/SKVariableFontRenderingTest.cs new file mode 100644 index 00000000000..8946a1528b4 --- /dev/null +++ b/tests/Tests/SkiaSharp/SKVariableFontRenderingTest.cs @@ -0,0 +1,269 @@ +using System; +using System.IO; +using Xunit; + +namespace SkiaSharp.Tests; + +public class SKVariableFontRenderingTest : SKTest +{ + private static string DistortableFontPath => + Path.Combine (PathToFonts, "Distortable.ttf"); + + private static byte[] RenderTextToBitmapBytes (SKTypeface typeface, string text, float fontSize, int width = 256, int height = 64) + { + using var bmp = new SKBitmap (new SKImageInfo (width, height)); + using var canvas = new SKCanvas (bmp); + using var font = new SKFont (typeface, fontSize); + using var paint = new SKPaint { Color = SKColors.Black, IsAntialias = true }; + + canvas.Clear (SKColors.White); + canvas.DrawText (text, 10, height / 2 + fontSize / 3, font, paint); + canvas.Flush (); + + return bmp.Bytes; + } + + private static int CountDifferingPixelBytes (byte[] a, byte[] b) + { + if (a.Length != b.Length) + return Math.Max (a.Length, b.Length); + + int count = 0; + for (int i = 0; i < a.Length; i++) + { + if (a[i] != b[i]) + count++; + } + + return count; + } + + private static bool PixelsDiffer (byte[] a, byte[] b) => + CountDifferingPixelBytes (a, b) > 0; + + [SkippableFact] + public void DifferentWeightVariationProducesDifferentRendering () + { + using var baseTypeface = SKTypeface.FromFile (DistortableFontPath); + Assert.NotNull (baseTypeface); + + var axes = baseTypeface.VariationDesignParameters; + Assert.NotEmpty (axes); + + // Clone with min weight + var minPosition = new[] { + new SKFontVariationPositionCoordinate { Axis = axes[0].Tag, Value = axes[0].Min } + }; + using var minTypeface = baseTypeface.Clone (minPosition); + Assert.NotNull (minTypeface); + + // Clone with max weight + var maxPosition = new[] { + new SKFontVariationPositionCoordinate { Axis = axes[0].Tag, Value = axes[0].Max } + }; + using var maxTypeface = baseTypeface.Clone (maxPosition); + Assert.NotNull (maxTypeface); + + var text = "Hello Variable"; + + var minPixels = RenderTextToBitmapBytes (minTypeface, text, 32); + var maxPixels = RenderTextToBitmapBytes (maxTypeface, text, 32); + + Assert.True (PixelsDiffer (minPixels, maxPixels), + "Rendering with min and max variation values should produce different pixel output."); + } + + [SkippableFact] + public void VariationPositionIsPreservedAfterClone () + { + using var baseTypeface = SKTypeface.FromFile (DistortableFontPath); + Assert.NotNull (baseTypeface); + + var axes = baseTypeface.VariationDesignParameters; + Assert.NotEmpty (axes); + + var targetValue = axes[0].Min; + var position = new[] { + new SKFontVariationPositionCoordinate { Axis = axes[0].Tag, Value = targetValue } + }; + using var cloned = baseTypeface.Clone (position); + Assert.NotNull (cloned); + + // Verify the cloned typeface reports the expected variation position + var clonedPosition = cloned.VariationDesignPosition; + Assert.NotEmpty (clonedPosition); + Assert.Equal (targetValue, clonedPosition[0].Value); + + // And render with it to confirm it produces distinct output from the max + var maxPosition = new[] { + new SKFontVariationPositionCoordinate { Axis = axes[0].Tag, Value = axes[0].Max } + }; + using var maxTypeface = baseTypeface.Clone (maxPosition); + + var minPixels = RenderTextToBitmapBytes (cloned, "Hello Variable", 32); + var maxPixels = RenderTextToBitmapBytes (maxTypeface, "Hello Variable", 32); + + Assert.True (PixelsDiffer (minPixels, maxPixels), + "Cloned typeface at min should render differently from max."); + } + + [SkippableFact] + public void IncreasingVariationChangesInkCoverage () + { + using var baseTypeface = SKTypeface.FromFile (DistortableFontPath); + Assert.NotNull (baseTypeface); + + var axes = baseTypeface.VariationDesignParameters; + Assert.NotEmpty (axes); + + var axis = axes[0]; + + // Sum all pixel intensity deltas from white to measure total ink coverage + long SumInkIntensity (byte[] pixels) + { + // Each pixel is 4 bytes (BGRA). Sum (255 - channel) for color channels. + long total = 0; + for (int i = 0; i < pixels.Length; i += 4) + { + total += (255 - pixels[i]) + (255 - pixels[i + 1]) + (255 - pixels[i + 2]); + } + return total; + } + + var minPos = new[] { + new SKFontVariationPositionCoordinate { Axis = axis.Tag, Value = axis.Min } + }; + using var minTypeface = baseTypeface.Clone (minPos); + var minPixels = RenderTextToBitmapBytes (minTypeface, "Hello Variable", 32); + + var maxPos = new[] { + new SKFontVariationPositionCoordinate { Axis = axis.Tag, Value = axis.Max } + }; + using var maxTypeface = baseTypeface.Clone (maxPos); + var maxPixels = RenderTextToBitmapBytes (maxTypeface, "Hello Variable", 32); + + var minInk = SumInkIntensity (minPixels); + var maxInk = SumInkIntensity (maxPixels); + + // Different variation values should produce measurably different ink coverage + Assert.NotEqual (minInk, maxInk); + } + + [SkippableFact] + public void ClonedVariationPreservesPositionInRendering () + { + using var baseTypeface = SKTypeface.FromFile (DistortableFontPath); + Assert.NotNull (baseTypeface); + + var axes = baseTypeface.VariationDesignParameters; + Assert.NotEmpty (axes); + + var midValue = (axes[0].Min + axes[0].Max) / 2; + var position = new[] { + new SKFontVariationPositionCoordinate { Axis = axes[0].Tag, Value = midValue } + }; + + // Clone twice with the same value + using var clone1 = baseTypeface.Clone (position); + using var clone2 = baseTypeface.Clone (position); + Assert.NotNull (clone1); + Assert.NotNull (clone2); + + var text = "Consistent"; + var pixels1 = RenderTextToBitmapBytes (clone1, text, 32); + var pixels2 = RenderTextToBitmapBytes (clone2, text, 32); + + Assert.False (PixelsDiffer (pixels1, pixels2), + "Two clones with the same variation value should render identically."); + } + + [SkippableFact] + public void MultipleIntermediateValuesProduceDistinctRenderings () + { + using var baseTypeface = SKTypeface.FromFile (DistortableFontPath); + Assert.NotNull (baseTypeface); + + var axes = baseTypeface.VariationDesignParameters; + Assert.NotEmpty (axes); + + var axis = axes[0]; + var step = (axis.Max - axis.Min) / 4; + + // Pick 3 distinct points along the axis + var values = new[] { axis.Min, axis.Min + step * 2, axis.Max }; + var renderings = new byte[values.Length][]; + + for (int i = 0; i < values.Length; i++) + { + var pos = new[] { + new SKFontVariationPositionCoordinate { Axis = axis.Tag, Value = values[i] } + }; + using var typeface = baseTypeface.Clone (pos); + Assert.NotNull (typeface); + renderings[i] = RenderTextToBitmapBytes (typeface, "Variation", 36); + } + + // Each rendering should differ from the others + Assert.True (PixelsDiffer (renderings[0], renderings[1]), + "Min and mid variation should render differently."); + Assert.True (PixelsDiffer (renderings[1], renderings[2]), + "Mid and max variation should render differently."); + Assert.True (PixelsDiffer (renderings[0], renderings[2]), + "Min and max variation should render differently."); + } + + [SkippableFact] + public void StaticFontRenderingUnaffectedByVariationAttempt () + { + using var staticTypeface = SKTypeface.FromFile (Path.Combine (PathToFonts, "content-font.ttf")); + Assert.NotNull (staticTypeface); + + // Verify this is a static font with no axes + var axes = staticTypeface.VariationDesignParameters; + Assert.Empty (axes); + + // Render with the static font + var text = "Static"; + var pixels = RenderTextToBitmapBytes (staticTypeface, text, 32); + + // Attempting to clone a static font with variation should return a usable typeface + var position = new[] { + new SKFontVariationPositionCoordinate { Axis = SKFourByteTag.Parse ("wght"), Value = 700 } + }; + using var cloned = staticTypeface.Clone (position); + Assert.NotNull (cloned); + + var clonedPixels = RenderTextToBitmapBytes (cloned, text, 32); + // For a static font, the output should be the same regardless of variation + Assert.False (PixelsDiffer (pixels, clonedPixels), + "Static font rendering should not change with variation parameters."); + } + + [SkippableFact] + public void VariationDoesNotAffectGlyphCount () + { + using var baseTypeface = SKTypeface.FromFile (DistortableFontPath); + Assert.NotNull (baseTypeface); + + var axes = baseTypeface.VariationDesignParameters; + Assert.NotEmpty (axes); + + var minPos = new[] { + new SKFontVariationPositionCoordinate { Axis = axes[0].Tag, Value = axes[0].Min } + }; + using var minTypeface = baseTypeface.Clone (minPos); + + var maxPos = new[] { + new SKFontVariationPositionCoordinate { Axis = axes[0].Tag, Value = axes[0].Max } + }; + using var maxTypeface = baseTypeface.Clone (maxPos); + + using var minFont = new SKFont (minTypeface, 32); + using var maxFont = new SKFont (maxTypeface, 32); + + var text = "Hello"; + + // Glyph count should be the same regardless of variation + Assert.Equal (minFont.CountGlyphs (text), maxFont.CountGlyphs (text)); + } +}