-
-
Notifications
You must be signed in to change notification settings - Fork 73
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #263 from 0xced/MacSystemFonts
Enumerate available fonts through the native API on macOS
- Loading branch information
Showing
9 changed files
with
429 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// Copyright (c) Six Labors. | ||
// Licensed under the Apache License, Version 2.0. | ||
|
||
using System.Diagnostics.CodeAnalysis; | ||
|
||
namespace SixLabors.Fonts.Native | ||
{ | ||
/// <summary> | ||
/// An integer type for constants used to specify supported string encodings in various CFString functions. | ||
/// </summary> | ||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Verbatim constants from the macOS SDK")] | ||
internal enum CFStringEncoding : uint | ||
{ | ||
/// <summary> | ||
/// An encoding constant that identifies the UTF 8 encoding. | ||
/// </summary> | ||
kCFStringEncodingUTF8 = 0x08000100, | ||
|
||
/// <summary> | ||
/// An encoding constant that identifies kTextEncodingUnicodeDefault + kUnicodeUTF16LEFormat encoding. This constant specifies little-endian byte order. | ||
/// </summary> | ||
kCFStringEncodingUTF16LE = 0x14000100, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// Copyright (c) Six Labors. | ||
// Licensed under the Apache License, Version 2.0. | ||
|
||
using System.Diagnostics.CodeAnalysis; | ||
|
||
namespace SixLabors.Fonts.Native | ||
{ | ||
/// <summary> | ||
/// Options you can use to determine how CFURL functions parse a file system path name. | ||
/// </summary> | ||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Verbatim constants from the macOS SDK")] | ||
internal enum CFURLPathStyle : long | ||
{ | ||
/// <summary> | ||
/// Indicates a POSIX style path name. Components are slash delimited. A leading slash indicates an absolute path; a trailing slash is not significant. | ||
/// </summary> | ||
kCFURLPOSIXPathStyle = 0, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
// Copyright (c) Six Labors. | ||
// Licensed under the Apache License, Version 2.0. | ||
|
||
using System; | ||
using System.Runtime.InteropServices; | ||
|
||
namespace SixLabors.Fonts.Native | ||
{ | ||
// ReSharper disable InconsistentNaming | ||
internal static class CoreFoundation | ||
{ | ||
private const string CoreFoundationFramework = "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"; | ||
|
||
/// <summary> | ||
/// Returns the number of values currently in an array. | ||
/// </summary> | ||
/// <param name="theArray">The array to examine.</param> | ||
/// <returns>The number of values in <paramref name="theArray"/>.</returns> | ||
[DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] | ||
public static extern long CFArrayGetCount(IntPtr theArray); | ||
|
||
/// <summary> | ||
/// Returns the type identifier for the CFArray opaque type. | ||
/// </summary> | ||
/// <returns>The type identifier for the CFArray opaque type.</returns> | ||
/// <remarks>CFMutableArray objects have the same type identifier as CFArray objects.</remarks> | ||
[DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] | ||
public static extern ulong CFArrayGetTypeID(); | ||
|
||
/// <summary> | ||
/// Retrieves a value at a given index. | ||
/// </summary> | ||
/// <param name="theArray">The array to examine.</param> | ||
/// <param name="idx">The index of the value to retrieve. If the index is outside the index space of <paramref name="theArray"/> (<c>0</c> to <c>N-1</c> inclusive where <c>N</c> is the count of <paramref name="theArray"/>), the behavior is undefined.</param> | ||
/// <returns>The value at the <paramref name="idx"/> index in <paramref name="theArray"/>. If the return value is a Core Foundation Object, ownership follows The Get Rule.</returns> | ||
[DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] | ||
public static extern IntPtr CFArrayGetValueAtIndex(IntPtr theArray, long idx); | ||
|
||
/// <summary> | ||
/// Returns the unique identifier of an opaque type to which a Core Foundation object belongs. | ||
/// </summary> | ||
/// <param name="cf">The CFType object to examine.</param> | ||
/// <returns>A value of type <c>CFTypeID</c> that identifies the opaque type of <paramref name="cf"/>.</returns> | ||
/// <remarks> | ||
/// This function returns a value that uniquely identifies the opaque type of any Core Foundation object. | ||
/// You can compare this value with the known <c>CFTypeID</c> identifier obtained with a “GetTypeID” function specific to a type, for example CFDateGetTypeID. | ||
/// These values might change from release to release or platform to platform. | ||
/// </remarks> | ||
[DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] | ||
public static extern ulong CFGetTypeID(IntPtr cf); | ||
|
||
/// <summary> | ||
/// Returns the number (in terms of UTF-16 code pairs) of Unicode characters in a string. | ||
/// </summary> | ||
/// <param name="theString">The string to examine.</param> | ||
/// <returns>The number (in terms of UTF-16 code pairs) of characters stored in <paramref name="theString"/>.</returns> | ||
[DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] | ||
public static extern long CFStringGetLength(IntPtr theString); | ||
|
||
/// <summary> | ||
/// Copies the character contents of a string to a local C string buffer after converting the characters to a given encoding. | ||
/// </summary> | ||
/// <param name="theString">The string whose contents you wish to access.</param> | ||
/// <param name="buffer"> | ||
/// The C string buffer into which to copy the string. On return, the buffer contains the converted characters. If there is an error in conversion, the buffer contains only partial results. | ||
/// The buffer must be large enough to contain the converted characters and a NUL terminator. For example, if the string is <c>Toby</c>, the buffer must be at least 5 bytes long. | ||
/// </param> | ||
/// <param name="bufferSize">The length of <paramref name="buffer"/> in bytes.</param> | ||
/// <param name="encoding">The string encoding to which the character contents of <paramref name="theString"/> should be converted. The encoding must specify an 8-bit encoding.</param> | ||
/// <returns><see langword="true"/> upon success or <see langword="false"/> if the conversion fails or the provided buffer is too small.</returns> | ||
/// <remarks>This function is useful when you need your own copy of a string’s character data as a C string. You also typically call it as a “backup” when a prior call to the <see cref="CFStringGetCStringPtr"/> function fails.</remarks> | ||
[DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] | ||
public static extern bool CFStringGetCString(IntPtr theString, byte[] buffer, long bufferSize, CFStringEncoding encoding); | ||
|
||
/// <summary> | ||
/// Quickly obtains a pointer to a C-string buffer containing the characters of a string in a given encoding. | ||
/// </summary> | ||
/// <param name="theString">The string whose contents you wish to access.</param> | ||
/// <param name="encoding">The string encoding to which the character contents of <paramref name="theString"/> should be converted. The encoding must specify an 8-bit encoding.</param> | ||
/// <returns>A pointer to a C string or <c>NULL</c> if the internal storage of <paramref name="theString"/> does not allow this to be returned efficiently.</returns> | ||
/// <remarks> | ||
/// <para> | ||
/// This function either returns the requested pointer immediately, with no memory allocations and no copying, in constant time, or returns <c>NULL</c>. If the latter is the result, call an alternative function such as the <see cref="CFStringGetCString"/> function to extract the characters. | ||
/// </para> | ||
/// <para> | ||
/// Whether or not this function returns a valid pointer or <c>NULL</c> depends on many factors, all of which depend on how the string was created and its properties. In addition, the function result might change between different releases and on different platforms. So do not count on receiving a non-<c>NULL</c> result from this function under any circumstances. | ||
/// </para> | ||
/// </remarks> | ||
[DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] | ||
public static extern IntPtr CFStringGetCStringPtr(IntPtr theString, CFStringEncoding encoding); | ||
|
||
/// <summary> | ||
/// Releases a Core Foundation object. | ||
/// </summary> | ||
/// <param name="cf">A CFType object to release. This value must not be <c>NULL</c>.</param> | ||
[DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] | ||
public static extern void CFRelease(IntPtr cf); | ||
|
||
/// <summary> | ||
/// Returns the path portion of a given URL. | ||
/// </summary> | ||
/// <param name="anURL">The <c>CFURL</c> object whose path you want to obtain.</param> | ||
/// <param name="pathStyle">The operating system path style to be used to create the path. See <see cref="CFURLPathStyle"/> for a list of possible values.</param> | ||
/// <returns>The URL's path in the format specified by <paramref name="pathStyle"/>. Ownership follows the create rule. See The Create Rule.</returns> | ||
/// <remarks>This function returns the URL's path as a file system path for a given path style.</remarks> | ||
[DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] | ||
public static extern IntPtr CFURLCopyFileSystemPath(IntPtr anURL, CFURLPathStyle pathStyle); | ||
|
||
/// <summary> | ||
/// Returns the type identifier for the CFURL opaque type. | ||
/// </summary> | ||
/// <returns>The type identifier for the CFURL opaque type.</returns> | ||
[DllImport(CoreFoundationFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] | ||
public static extern ulong CFURLGetTypeID(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// Copyright (c) Six Labors. | ||
// Licensed under the Apache License, Version 2.0. | ||
|
||
using System; | ||
using System.Runtime.InteropServices; | ||
|
||
namespace SixLabors.Fonts.Native | ||
{ | ||
internal static class CoreText | ||
{ | ||
private const string CoreTextFramework = "/System/Library/Frameworks/CoreText.framework/Versions/A/CoreText"; | ||
|
||
/// <summary> | ||
/// Returns an array of font URLs. | ||
/// </summary> | ||
/// <returns>This function returns a retained reference to a <c>CFArray</c> of <c>CFURLRef</c> objects representing the URLs of the available fonts, or <c>NULL</c> on error. The caller is responsible for releasing the array.</returns> | ||
[DllImport(CoreTextFramework, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] | ||
public static extern IntPtr CTFontManagerCopyAvailableFontURLs(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
// Copyright (c) Six Labors. | ||
// Licensed under the Apache License, Version 2.0. | ||
|
||
using System; | ||
using System.Buffers; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.Runtime.InteropServices; | ||
using System.Text; | ||
using static SixLabors.Fonts.Native.CoreFoundation; | ||
using static SixLabors.Fonts.Native.CoreText; | ||
|
||
namespace SixLabors.Fonts.Native | ||
{ | ||
/// <summary> | ||
/// An enumerator that enumerates over available macOS system fonts. | ||
/// The enumerated strings are the absolute paths to the font files. | ||
/// </summary> | ||
/// <remarks> | ||
/// Internally, it calls the native CoreText's <see cref="CTFontManagerCopyAvailableFontURLs"/> method to retrieve | ||
/// the list of fonts so using this class must be guarded by <c>RuntimeInformation.IsOSPlatform(OSPlatform.OSX)</c>. | ||
/// </remarks> | ||
internal class MacSystemFontsEnumerator : IEnumerable<string>, IEnumerator<string> | ||
{ | ||
private static readonly ArrayPool<byte> BytePool = ArrayPool<byte>.Shared; | ||
|
||
private readonly IntPtr fontUrls; | ||
private readonly bool releaseFontUrls; | ||
private int fontIndex; | ||
|
||
public MacSystemFontsEnumerator() | ||
: this(CTFontManagerCopyAvailableFontURLs(), releaseFontUrls: true, fontIndex: 0) | ||
{ | ||
} | ||
|
||
private MacSystemFontsEnumerator(IntPtr fontUrls, bool releaseFontUrls, int fontIndex) | ||
{ | ||
if (fontUrls == IntPtr.Zero) | ||
{ | ||
throw new ArgumentException($"The {nameof(fontUrls)} must not be NULL.", nameof(fontUrls)); | ||
} | ||
|
||
this.fontUrls = fontUrls; | ||
this.releaseFontUrls = releaseFontUrls; | ||
this.fontIndex = fontIndex; | ||
|
||
this.Current = null!; | ||
} | ||
|
||
public string Current { get; private set; } | ||
|
||
object IEnumerator.Current => this.Current; | ||
|
||
public bool MoveNext() | ||
{ | ||
Debug.Assert(CFGetTypeID(this.fontUrls) == CFArrayGetTypeID(), "The fontUrls array must be a CFArrayRef"); | ||
if (this.fontIndex < CFArrayGetCount(this.fontUrls)) | ||
{ | ||
IntPtr fontUrl = CFArrayGetValueAtIndex(this.fontUrls, this.fontIndex); | ||
Debug.Assert(CFGetTypeID(fontUrl) == CFURLGetTypeID(), "The elements of the fontUrls array must be a CFURLRef"); | ||
IntPtr fontPath = CFURLCopyFileSystemPath(fontUrl, CFURLPathStyle.kCFURLPOSIXPathStyle); | ||
|
||
#if !NETSTANDARD2_0 | ||
string? current = Marshal.PtrToStringUTF8(CFStringGetCStringPtr(fontPath, CFStringEncoding.kCFStringEncodingUTF8)); | ||
if (current is not null) | ||
{ | ||
this.Current = current; | ||
} | ||
else | ||
#endif | ||
{ | ||
int fontPathLength = (int)CFStringGetLength(fontPath); | ||
int fontPathBufferSize = (fontPathLength + 1) * 2; // +1 for the NULL byte and *2 for UTF-16 | ||
byte[] fontPathBuffer = BytePool.Rent(fontPathBufferSize); | ||
CFStringGetCString(fontPath, fontPathBuffer, fontPathBufferSize, CFStringEncoding.kCFStringEncodingUTF16LE); | ||
this.Current = Encoding.Unicode.GetString(fontPathBuffer, 0, fontPathBufferSize - 2); // -2 for the UTF-16 NULL | ||
BytePool.Return(fontPathBuffer); | ||
} | ||
|
||
CFRelease(fontPath); | ||
|
||
this.fontIndex++; | ||
|
||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
public void Reset() => this.fontIndex = 0; | ||
|
||
public void Dispose() | ||
{ | ||
if (this.releaseFontUrls) | ||
{ | ||
CFRelease(this.fontUrls); | ||
} | ||
} | ||
|
||
public IEnumerator<string> GetEnumerator() => new MacSystemFontsEnumerator(this.fontUrls, releaseFontUrls: false, this.fontIndex); | ||
|
||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.