Start text abstraction

This commit is contained in:
Yoshi Askharoun
2026-07-27 11:00:37 -05:00
parent 272cc14649
commit aedbd7bf6b
24 changed files with 1058 additions and 80 deletions
@@ -0,0 +1,16 @@
using System;
using Microsoft.Iris.Render.Internal;
namespace Microsoft.Iris.Render.Text;
// Based on Microsoft.Iris.Render.Bitmaps.BitmapInformation
public abstract class FontResource : IDisposable
{
public abstract HRESULT LoadFile(string filename);
public abstract HRESULT LoadResource(string moduleName, string resourceId);
public abstract HRESULT LoadBuffer(IntPtr pvSrc, int cbSize);
public abstract void Dispose();
}
@@ -0,0 +1,41 @@
using System;
namespace Microsoft.Iris.Render.Text;
// Based on Microsoft.Iris.Render.Extensions.ImageLoader
//
// Unlike ImageLoader's SpBitmapInformation/ImageSharpBitmapInformation split
// (both of which live in this assembly), the Windows font/text backend needs
// the marshalling machinery already implemented in Microsoft.Iris.OS.NativeApi,
// which lives in the downstream UIX project (UIX -> UIX.RenderApi, not the
// other way around). So instead of a #if WINDOWS factory switch, UIX registers
// its backend here via RegisterWindowsBackend from a module initializer gated
// on the WINDOWS compile constant - see Microsoft.Iris.OS.TextBackendRegistration.
// On any platform where nothing registers a backend, this falls back to the
// SixLabors.Fonts-based implementation.
public static class FontResourceLoader
{
private static Func<FontResource> s_windowsBackend;
public static void RegisterWindowsBackend(Func<FontResource> factory) => s_windowsBackend = factory;
private static FontResource Create() => s_windowsBackend?.Invoke() ?? new SixLaborsFontResource();
public static bool LoadFromFile(string filename)
{
using var fontResource = Create();
return fontResource.LoadFile(filename).IsSuccess();
}
public static bool LoadFromModuleResource(string moduleName, string resourceId)
{
using var fontResource = Create();
return fontResource.LoadResource(moduleName, resourceId).IsSuccess();
}
public static bool LoadFromBuffer(IntPtr pvSrc, int cbSize)
{
using var fontResource = Create();
return fontResource.LoadBuffer(pvSrc, cbSize).IsSuccess();
}
}
@@ -0,0 +1,47 @@
using System;
namespace Microsoft.Iris.Render.Text;
// Based on the fields Microsoft.Iris.Drawing.TextRun copies out of the native
// NativeApi.RasterizeRunPacket on construction.
public sealed class GlyphRunInfo : IDisposable
{
public string Content { get; init; }
public Rectangle LayoutBounds { get; init; }
public float RenderBoundsX { get; init; }
public float RenderBoundsY { get; init; }
public float RenderBoundsWidth { get; init; }
public float RenderBoundsHeight { get; init; }
public Size NaturalExtent { get; init; }
public int NaturalX { get; init; }
public int NaturalY { get; init; }
public int RasterizeX { get; init; }
public int RasterizeY { get; init; }
public byte RasterizerConfig { get; init; }
public ColorF RunColor { get; init; }
public ColorF HighlightColor { get; init; }
public int FontFaceUniqueId { get; init; }
public int FontHeight { get; init; }
public int FontWeight { get; init; }
public bool Italic { get; init; }
public bool Underline { get; init; }
public bool Link { get; init; }
public UnderlineStyle UnderlineStyle { get; init; }
public Rectangle UnderlineBounds { get; init; }
public int Line { get; init; }
public int AscenderInset { get; init; }
public int BaselineInset { get; init; }
// Opaque backend-owned state (e.g. the native hGlyphRunInfo handle) needed
// by TextDocument.Rasterize and released by Dispose. Never touched outside
// the TextDocument implementation that produced this instance.
public object BackendHandle { get; init; }
private readonly Action<object> _disposeBackendHandle;
public GlyphRunInfo(Action<object> disposeBackendHandle = null)
{
_disposeBackendHandle = disposeBackendHandle;
}
public void Dispose() => _disposeBackendHandle?.Invoke(BackendHandle);
}
@@ -0,0 +1,24 @@
using System;
namespace Microsoft.Iris.Render.Text;
// Replaces the phTextBitmap/ppvBits/psizeBitmap triple returned by the native
// SpRichTextRasterize. Bits is always unmanaged memory (owned by whichever
// TextDocument implementation produced it) laid out as top-down A8R8G8B8.
public sealed class RasterizedGlyphBitmap : IDisposable
{
public Size Size { get; init; }
public IntPtr Bits { get; init; }
// Backend-native handle (e.g. the Windows HBITMAP/DIB handle), if any -
// only meaningful to the backend that produced this instance.
public IntPtr NativeHandle { get; init; }
private readonly Action _disposeAction;
public RasterizedGlyphBitmap(Action disposeAction)
{
_disposeAction = disposeAction;
}
public void Dispose() => _disposeAction?.Invoke();
}
@@ -0,0 +1,19 @@
using SixLabors.Fonts;
namespace Microsoft.Iris.Render.Text;
// Shared font collection fonts get registered into (by SixLaborsFontResource)
// and resolved from (by SixLaborsTextDocument), analogous to how the Windows
// backend registers fonts process-wide via AddFontMemResourceEx/SpLoadFontResource.
internal static class SixLaborsFontRegistry
{
public static FontCollection Collection { get; } = new();
public static bool TryGetFamily(string name, out FontFamily family)
{
if (!string.IsNullOrEmpty(name) && Collection.TryGet(name, out family))
return true;
return SystemFonts.TryGet(name, out family);
}
}
@@ -0,0 +1,57 @@
using System;
using System.IO;
using Microsoft.Iris.Render.Internal;
namespace Microsoft.Iris.Render.Text;
public sealed class SixLaborsFontResource : FontResource
{
public override HRESULT LoadFile(string filename) => Try(() =>
{
using var stream = File.OpenRead(filename);
SixLaborsFontRegistry.Collection.Add(stream);
});
public override HRESULT LoadResource(string moduleName, string resourceId) =>
// No cross-platform equivalent of a Win32 module/resource handle exists.
// Mirrors ImageSharpBitmapInformation._LoadResource's same limitation.
Try(() => throw new NotImplementedException());
public override unsafe HRESULT LoadBuffer(IntPtr pvSrc, int cbSize) => Try(() =>
{
ReadOnlySpan<byte> buffer = new(pvSrc.ToPointer(), cbSize);
using var stream = new MemoryStream(buffer.ToArray());
var isCollection = buffer[..4].SequenceEqual("ttcf"u8);
if (isCollection)
SixLaborsFontRegistry.Collection.AddCollection(stream);
else
SixLaborsFontRegistry.Collection.Add(stream);
});
public override void Dispose()
{
}
private static HRESULT Try(Action action)
{
try
{
action();
}
catch (FileNotFoundException)
{
return 0x80070002;
}
catch (NotImplementedException)
{
return 0x80004001;
}
catch
{
return HRESULT.E_FAIL;
}
return HRESULT.S_OK;
}
}
@@ -0,0 +1,154 @@
using System;
using System.Runtime.InteropServices;
using Microsoft.Iris.Render.Internal;
using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Color = SixLabors.ImageSharp.Color;
namespace Microsoft.Iris.Render.Text;
public sealed class SixLaborsTextDocument : TextDocument
{
private string _content = string.Empty;
private TextStyleInfo _lastStyle;
public override HRESULT SetContent(string content)
{
_content = content ?? string.Empty;
return HRESULT.S_OK;
}
public override HRESULT GetSimpleContent(out string content)
{
content = _content;
return HRESULT.S_OK;
}
public override HRESULT GetNaturalBounds(out Size bounds)
{
var hresult = MeasureCore(_content, _lastStyle ?? DefaultStyle, Size.Zero, out var fontRect, out _);
bounds = hresult.IsSuccess() ? new Size((int)MathF.Ceiling(fontRect.Width), (int)MathF.Ceiling(fontRect.Height)) : Size.Zero;
return hresult;
}
public override HRESULT MeasurePossible(string content, TextStyleInfo style, out bool possible)
{
possible = TryResolveFont(style, out _);
return HRESULT.S_OK;
}
public override HRESULT Measure(string content, TextAlignment alignment, TextStyleInfo style, Size constraint, out GlyphRunInfo glyphRun)
{
_lastStyle = style;
var hresult = MeasureCore(content, style, constraint, out var fontRect, out var font);
if (!hresult.IsSuccess())
{
glyphRun = null;
return hresult;
}
var layoutBounds = new Rectangle((int)fontRect.X, (int)fontRect.Y, (int)MathF.Ceiling(fontRect.Width), (int)MathF.Ceiling(fontRect.Height));
glyphRun = new GlyphRunInfo()
{
Content = content,
LayoutBounds = layoutBounds,
RenderBoundsX = fontRect.X,
RenderBoundsY = fontRect.Y,
RenderBoundsWidth = fontRect.Width,
RenderBoundsHeight = fontRect.Height,
NaturalExtent = new Size((int)MathF.Ceiling(fontRect.Width), (int)MathF.Ceiling(fontRect.Height)),
RunColor = style.Color,
HighlightColor = default,
FontFaceUniqueId = style.FontFace?.GetHashCode() ?? 0,
FontHeight = (int)style.FontSize,
FontWeight = style.Bold ? 700 : 400,
Italic = style.Italic,
Underline = style.Underline,
Link = false,
UnderlineStyle = style.Underline ? UnderlineStyle.Solid : UnderlineStyle.None,
UnderlineBounds = Rectangle.Zero,
Line = 1,
AscenderInset = 0,
BaselineInset = 0,
BackendHandle = font,
};
return HRESULT.S_OK;
}
public override unsafe HRESULT Rasterize(GlyphRunInfo glyphRun, ColorF textColor, bool outline, bool shadow, out RasterizedGlyphBitmap bitmap)
{
if (glyphRun.BackendHandle is not Font font)
{
bitmap = null;
return HRESULT.E_FAIL;
}
var width = Math.Max(1, (int)MathF.Ceiling(glyphRun.RenderBoundsWidth));
var height = Math.Max(1, (int)MathF.Ceiling(glyphRun.RenderBoundsHeight));
var color = Color.FromPixel(new Rgba32(
(byte)Math.Clamp(textColor.R * 255f, 0, 255),
(byte)Math.Clamp(textColor.G * 255f, 0, 255),
(byte)Math.Clamp(textColor.B * 255f, 0, 255),
(byte)Math.Clamp(textColor.A * 255f, 0, 255)));
var brush = new SolidBrush(color);
var richTextOptions = new RichTextOptions(font) { Origin = PointF.Empty };
using var image = new Image<Rgba32>(width, height);
image.Mutate(ctx => ctx.Paint(canvas => canvas.DrawText(richTextOptions, glyphRun.Content ?? string.Empty, brush, null)));
var byteCount = width * height * 4;
var pBits = Marshal.AllocHGlobal(byteCount);
image.CopyPixelDataTo(new Span<byte>(pBits.ToPointer(), byteCount));
var pBitsCaptured = pBits;
bitmap = new RasterizedGlyphBitmap(() => Marshal.FreeHGlobal(pBitsCaptured))
{
Size = new Size(width, height),
Bits = pBits,
NativeHandle = IntPtr.Zero,
};
return HRESULT.S_OK;
}
public override void Dispose()
{
}
private static TextStyleInfo DefaultStyle => new() { FontFace = "Arial", FontSize = 12f };
private static bool TryResolveFont(TextStyleInfo style, out Font font)
{
font = null;
if (!SixLaborsFontRegistry.TryGetFamily(style?.FontFace, out var family))
return false;
var fontStyle = (style is { Bold: true, Italic: true }) ? FontStyle.BoldItalic
: style?.Bold == true ? FontStyle.Bold
: style?.Italic == true ? FontStyle.Italic
: FontStyle.Regular;
var size = style?.FontSize > 0 ? style.FontSize : 12f;
font = family.CreateFont(size, fontStyle);
return true;
}
private static HRESULT MeasureCore(string content, TextStyleInfo style, Size constraint, out FontRectangle fontRect, out Font font)
{
content ??= string.Empty;
if (!TryResolveFont(style, out font))
{
fontRect = default;
return 0x80070490; // ERROR_NOT_FOUND
}
var options = new TextOptions(font);
if (constraint.Width > 0)
options.WrappingLength = constraint.Width;
fontRect = TextMeasurer.MeasureBounds(content, options);
return HRESULT.S_OK;
}
}
@@ -0,0 +1,8 @@
namespace Microsoft.Iris.Render.Text;
public enum TextAlignment
{
Near,
Center,
Far,
}
@@ -0,0 +1,30 @@
using System;
using Microsoft.Iris.Render.Internal;
namespace Microsoft.Iris.Render.Text;
// Based on Microsoft.Iris.Render.Bitmaps.BitmapInformation
//
// Covers the "given text (+ a style), produce metrics/pixels" surface that is
// safe to swap per-platform: content storage/query, measurement, and glyph
// rasterization. Deliberately does NOT cover interactive text editing (IME,
// keyboard/mouse forwarding, undo, clipboard, scrollbars, timers) or
// multi-range rich-text formatting (TextMeasureParams' formatted ranges) -
// those remain native/Windows-only in Microsoft.Iris.Drawing.RichText. See
// the "Cross-platform text/font abstraction" plan and logs/ for the rationale.
public abstract class TextDocument : IDisposable
{
public abstract HRESULT SetContent(string content);
public abstract HRESULT GetSimpleContent(out string content);
public abstract HRESULT GetNaturalBounds(out Size bounds);
public abstract HRESULT MeasurePossible(string content, TextStyleInfo style, out bool possible);
public abstract HRESULT Measure(string content, TextAlignment alignment, TextStyleInfo style, Size constraint, out GlyphRunInfo glyphRun);
public abstract HRESULT Rasterize(GlyphRunInfo glyphRun, ColorF textColor, bool outline, bool shadow, out RasterizedGlyphBitmap bitmap);
public abstract void Dispose();
}
@@ -0,0 +1,17 @@
using System;
namespace Microsoft.Iris.Render.Text;
// See FontResourceLoader for why this is a registration seam rather than a
// #if WINDOWS switch: the Windows backend needs Microsoft.Iris.OS.NativeApi's
// marshalling, which lives in the downstream UIX project.
public static class TextDocumentFactory
{
private static Func<TextDocument> s_windowsStandaloneBackend;
public static void RegisterWindowsBackend(Func<TextDocument> factory) => s_windowsStandaloneBackend = factory;
// A standalone TextDocument owns its own backend state independently of
// any other object - this is what Microsoft.Iris.Drawing.SimpleText uses.
public static TextDocument CreateStandalone() => s_windowsStandaloneBackend?.Invoke() ?? new SixLaborsTextDocument();
}
@@ -0,0 +1,16 @@
namespace Microsoft.Iris.Render.Text;
public sealed class TextStyleInfo
{
public string FontFace { get; set; }
public float FontSize { get; set; }
public float AltFontSize { get; set; }
public bool Bold { get; set; }
public bool Italic { get; set; }
public bool Underline { get; set; }
public ColorF Color { get; set; }
public bool HasColor { get; set; }
public float LineSpacing { get; set; }
public float CharacterSpacing { get; set; }
public bool EnableKerning { get; set; }
}
@@ -0,0 +1,13 @@
namespace Microsoft.Iris.Render.Text;
// Based on Microsoft.Iris.OS.NativeApi.UnderlineStyle
public enum UnderlineStyle
{
None,
Solid,
Thick,
Dotted,
Dash,
DashDot,
DashDotDot,
}
+4
View File
@@ -15,5 +15,9 @@
<!-- TODO: Only import on non-Windows targets -->
<!-- ImageSharp provides support for decoding images -->
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.0" />
<!-- ImageSharp.Drawing provides glyph rasterization on top of ImageSharp -->
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="3.0.0" />
<!-- Fonts provides font loading/measurement independent of ImageSharp -->
<PackageReference Include="SixLabors.Fonts" Version="3.0.0" />
</ItemGroup>
</Project>
+19 -15
View File
@@ -6,6 +6,7 @@
using Microsoft.Iris.OS;
using Microsoft.Iris.Render;
using Microsoft.Iris.Render.Text;
using Microsoft.Iris.RenderAPI;
using Microsoft.Iris.RenderAPI.Drawing;
using Microsoft.Iris.Session;
@@ -22,6 +23,15 @@ namespace Microsoft.Iris.Drawing
public const float MaxWidthConstraint = 4095f;
public const float MaxHeightConstraint = 8191f;
private Win32Api.HANDLE _rtoHandle;
// Bound to _rtoHandle: routes SetContent/GetSimpleContent/GetNaturalBounds
// through the cross-platform Microsoft.Iris.Render.Text.TextDocument
// abstraction. Measure/Rasterize stay on NativeApi.SpRichText* directly
// below - TextMeasureParams' multi-range formatting doesn't fit this
// simplified single-style abstraction without losing fidelity. See
// logs/ for the rationale. Only meaningful on Windows: RichText's own
// constructor is native-only already (SpRichTextBuildObject), so this
// instance is always the Sp-backed implementation in practice.
private readonly TextDocument _textDocument;
private NativeApi.ReportRunCallback _rrcb;
private string _currentlyMeasuringText;
private bool _oversampled;
@@ -48,6 +58,7 @@ namespace Microsoft.Iris.Drawing
_timerTickHandler = new EventHandler(OnTimerTick);
}
RendererApi.IFC(NativeApi.SpRichTextBuildObject(richTextMode, sizeMaximumSurface, callbacks, out _rtoHandle));
_textDocument = new SpTextDocument(_rtoHandle);
_oversampled = false;
_lock = new object();
_rrcb = new NativeApi.ReportRunCallback(OnReportRun);
@@ -62,6 +73,7 @@ namespace Microsoft.Iris.Drawing
GC.SuppressFinalize(this);
lock (_lock)
{
_textDocument.Dispose();
NativeApi.SpRichTextDestroyObject(_rtoHandle);
_rtoHandle.h = IntPtr.Zero;
}
@@ -79,7 +91,7 @@ namespace Microsoft.Iris.Drawing
set
{
lock (_lock)
RendererApi.IFC(NativeApi.SpRichTextSetContent(_rtoHandle, value));
RendererApi.IFC(new HRESULT(_textDocument.SetContent(value).Int));
}
}
@@ -87,19 +99,11 @@ namespace Microsoft.Iris.Drawing
{
get
{
string str = null;
int textLength = 0;
lock (_lock)
{
RendererApi.IFC(NativeApi.SpRichTextGetSimpleContentLength(_rtoHandle, out textLength));
if (textLength != 0)
{
StringBuilder textBuffer = new StringBuilder(textLength);
RendererApi.IFC(NativeApi.SpRichTextGetSimpleContent(_rtoHandle, textBuffer, textBuffer.Capacity));
str = textBuffer.ToString();
}
RendererApi.IFC(new HRESULT(_textDocument.GetSimpleContent(out var content).Int));
return content;
}
return str;
}
}
@@ -153,11 +157,11 @@ namespace Microsoft.Iris.Drawing
public Size GetNaturalBounds()
{
int cWidth;
int cHeight;
lock (_lock)
RendererApi.IFC(NativeApi.SpRichTextGetNaturalBounds(_rtoHandle, out cWidth, out cHeight));
return new Size(cWidth, cHeight);
{
RendererApi.IFC(new HRESULT(_textDocument.GetNaturalBounds(out var bounds).Int));
return bounds;
}
}
public void SetSelectionRange(int selectionStart, int selectionEnd)
+41 -47
View File
@@ -1,53 +1,43 @@
// Decompiled with JetBrains decompiler
// Based on decompilation with JetBrains decompiler
// Type: Microsoft.Iris.Drawing.SimpleText
// Assembly: UIX, Version=4.8.0.0, Culture=neutral, PublicKeyToken=ddd0da4d3e678217
// MVID: A56C6C9D-B7F6-46A9-8BDE-B3D9B8D60B11
// Assembly location: C:\Program Files\Zune\UIX.dll
using Microsoft.Iris.OS;
using Microsoft.Iris.Render;
using Microsoft.Iris.RenderAPI;
using Microsoft.Iris.Session;
using Microsoft.Iris.Render.Text;
using Microsoft.Iris.ViewItems;
using System;
namespace Microsoft.Iris.Drawing
{
// Uses the cross-platform TextDocument abstraction (Microsoft.Iris.Render.Text)
// instead of calling NativeApi.SpSimpleText* directly, so this class works on
// any platform: TextDocumentFactory.CreateStandalone() resolves to the native
// SpTextDocument on Windows and to a SixLabors.Fonts-backed TextDocument
// elsewhere.
internal class SimpleText : IDisposable
{
private Win32Api.HANDLE _stoHandle;
private readonly TextDocument _document;
public SimpleText()
{
Size sizeMaximumSurface = Size.Zero;
if (UISession.Default != null)
sizeMaximumSurface = UIImage.MaximumSurfaceSize(UISession.Default);
RendererApi.IFC(NativeApi.SpSimpleTextBuildObject(sizeMaximumSurface, out _stoHandle));
_document = TextDocumentFactory.CreateStandalone();
}
public void Dispose()
{
GC.SuppressFinalize(this);
NativeApi.SpSimpleTextDestroyObject(_stoHandle);
_stoHandle = Win32Api.HANDLE.NULL;
_document.Dispose();
}
public unsafe bool CanMeasure(string content, TextStyle textStyle)
public bool CanMeasure(string content, TextStyle textStyle)
{
bool fPossible;
fixed (char* chPtr = textStyle.TruncatedFontFace)
{
var style = new TextStyle.MarshalledData(textStyle)
{
_fontFace = chPtr
};
RendererApi.IFC(NativeApi.SpSimpleTextMeasurePossible(_stoHandle, content, &style, out fPossible));
}
return fPossible;
_document.MeasurePossible(content, ToStyleInfo(textStyle), out var possible);
return possible;
}
public unsafe TextFlow Measure(
public TextFlow Measure(
string content,
LineAlignment alignment,
TextStyle textStyle,
@@ -56,33 +46,37 @@ namespace Microsoft.Iris.Drawing
TextFlow textFlow = new TextFlow();
if (content == null)
content = string.Empty;
short wAlignment = 0;
switch (alignment)
var textAlignment = alignment switch
{
case LineAlignment.Near:
wAlignment = 1;
break;
case LineAlignment.Center:
wAlignment = 3;
break;
case LineAlignment.Far:
wAlignment = 2;
break;
}
IntPtr hGlyphRunInfo;
NativeApi.RasterizeRunPacket rasterizeRunPacket;
fixed (char* chPtr = textStyle.TruncatedFontFace)
LineAlignment.Near => TextAlignment.Near,
LineAlignment.Center => TextAlignment.Center,
LineAlignment.Far => TextAlignment.Far,
_ => TextAlignment.Near,
};
var hresult = _document.Measure(content, textAlignment, ToStyleInfo(textStyle), constraint, out var glyphRunInfo);
if (hresult.IsSuccess() && glyphRunInfo != null)
{
var style = new TextStyle.MarshalledData(textStyle)
{
_fontFace = chPtr
};
RendererApi.IFC(NativeApi.SpSimpleTextMeasure(_stoHandle, content, wAlignment,
&style, constraint, out hGlyphRunInfo, &rasterizeRunPacket));
var run = TextRun.FromGlyphRunInfo(glyphRunInfo, _document);
textFlow.Add(run);
}
TextRun run = TextRun.FromRunPacket(hGlyphRunInfo, &rasterizeRunPacket, content);
textFlow.Add(run);
return textFlow;
}
private static TextStyleInfo ToStyleInfo(TextStyle textStyle) => new()
{
FontFace = textStyle.FontFace,
FontSize = textStyle.FontSize,
AltFontSize = textStyle.AltFontSize,
Bold = textStyle.Bold,
Italic = textStyle.Italic,
Underline = textStyle.Underline,
Color = textStyle.Color.RenderConvert(),
HasColor = textStyle.HasColor,
LineSpacing = textStyle.LineSpacing,
CharacterSpacing = textStyle.CharacterSpacing,
EnableKerning = textStyle.EnableKerning,
};
}
}
@@ -0,0 +1,40 @@
using System;
using System.Runtime.InteropServices;
using Microsoft.Iris.OS;
using Microsoft.Iris.Render.Internal;
using Microsoft.Iris.Render.Text;
namespace Microsoft.Iris.Drawing;
// Windows backend for Microsoft.Iris.Render.Text.FontResource, living here
// (rather than alongside the abstract class in UIX.RenderApi) so it can reuse
// Microsoft.Iris.OS.NativeApi's existing P/Invoke declarations instead of
// duplicating them in an assembly NativeApi isn't visible from. Registered
// with FontResourceLoader by TextBackendRegistration.
internal sealed class SpFontResource : FontResource
{
[DllImport("gdi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int AddFontResourceExW(string lpszFilename, uint fl, IntPtr pdv);
private const uint FR_PRIVATE = 0x10;
public override HRESULT LoadFile(string filename)
{
var addedCount = AddFontResourceExW(filename, FR_PRIVATE, IntPtr.Zero);
return addedCount > 0 ? HRESULT.S_OK : HRESULT.E_FAIL;
}
public override HRESULT LoadResource(string moduleName, string resourceId) =>
NativeApi.SpLoadFontResource(moduleName, resourceId) ? HRESULT.S_OK : HRESULT.E_FAIL;
public override HRESULT LoadBuffer(IntPtr pvSrc, int cbSize)
{
uint cFonts = 0;
var hFont = Win32Api.AddFontMemResourceEx(pvSrc, (uint)cbSize, IntPtr.Zero, ref cFonts);
return hFont != IntPtr.Zero ? HRESULT.S_OK : HRESULT.E_FAIL;
}
public override void Dispose()
{
}
}
@@ -0,0 +1,247 @@
using System;
using System.Text;
using Microsoft.Iris.OS;
using Microsoft.Iris.Render;
using Microsoft.Iris.RenderAPI;
using Microsoft.Iris.Render.Text;
using Microsoft.Iris.Session;
using Microsoft.Iris.ViewItems;
using Size = Microsoft.Iris.Render.Size;
using Color = Microsoft.Iris.Drawing.Color;
using HRESULT = Microsoft.Iris.Render.Internal.HRESULT;
namespace Microsoft.Iris.Drawing;
// Windows backend for Microsoft.Iris.Render.Text.TextDocument - see the
// remark on SpFontResource for why this lives in UIX rather than
// UIX.RenderApi. Registered with TextDocumentFactory by TextBackendRegistration.
//
// Wraps one of the two native text object families NativeApi exposes:
// - "standalone" wraps its own SpSimpleTextBuildObject handle (STO), exactly
// what Microsoft.Iris.Drawing.SimpleText used before this refactor. Only
// Measure/MeasurePossible/Rasterize are meaningful for an STO.
// - "bound" is constructed with an existing RichText RTO handle so
// SetContent/GetSimpleContent/GetNaturalBounds run against RichText's own
// session. Measure/MeasurePossible are NOT implemented in bound mode:
// RichText.Measure needs TextMeasureParams' multi-range formatting, which
// this simplified single-style abstraction can't safely replicate without
// regressing existing Windows behavior - RichText keeps calling
// NativeApi.SpRichTextMeasure directly for that. See logs/ for the
// rationale.
internal sealed class SpTextDocument : TextDocument
{
private Win32Api.HANDLE _handle;
private readonly bool _bound;
public SpTextDocument()
{
var sizeMaximumSurface = Size.Zero;
if (UISession.Default != null)
sizeMaximumSurface = UIImage.MaximumSurfaceSize(UISession.Default);
RendererApi.IFC(NativeApi.SpSimpleTextBuildObject(sizeMaximumSurface, out _handle));
_bound = false;
}
public SpTextDocument(Win32Api.HANDLE boundRtoHandle)
{
_handle = boundRtoHandle;
_bound = true;
}
public override HRESULT SetContent(string content)
{
RequireBound();
return NativeApi.SpRichTextSetContent(_handle, content).Int;
}
public override HRESULT GetSimpleContent(out string content)
{
RequireBound();
var hresult = NativeApi.SpRichTextGetSimpleContentLength(_handle, out var textLength);
if (!hresult.IsSuccess() || textLength == 0)
{
content = null;
return hresult.Int;
}
var textBuffer = new StringBuilder(textLength);
hresult = NativeApi.SpRichTextGetSimpleContent(_handle, textBuffer, textBuffer.Capacity);
content = hresult.IsSuccess() ? textBuffer.ToString() : null;
return hresult.Int;
}
public override HRESULT GetNaturalBounds(out Size bounds)
{
RequireBound();
var hresult = NativeApi.SpRichTextGetNaturalBounds(_handle, out var cWidth, out var cHeight);
bounds = hresult.IsSuccess() ? new Size(cWidth, cHeight) : Size.Zero;
return hresult.Int;
}
public override unsafe HRESULT MeasurePossible(string content, TextStyleInfo style, out bool possible)
{
RequireStandalone();
fixed (char* facePtr = TruncateFontFace(style?.FontFace))
{
var marshalled = ToMarshalledData(style, facePtr);
return NativeApi.SpSimpleTextMeasurePossible(_handle, content, &marshalled, out possible).Int;
}
}
public override unsafe HRESULT Measure(string content, TextAlignment alignment, TextStyleInfo style, Size constraint, out GlyphRunInfo glyphRun)
{
RequireStandalone();
content ??= string.Empty;
short wAlignment = alignment switch
{
TextAlignment.Near => 1,
TextAlignment.Center => 3,
TextAlignment.Far => 2,
_ => 0,
};
IntPtr hGlyphRunInfo;
NativeApi.RasterizeRunPacket rasterizeRunPacket;
Microsoft.Iris.RenderAPI.HRESULT hresult;
fixed (char* facePtr = TruncateFontFace(style?.FontFace))
{
var marshalled = ToMarshalledData(style, facePtr);
hresult = NativeApi.SpSimpleTextMeasure(_handle, content, wAlignment, &marshalled, constraint, out hGlyphRunInfo, &rasterizeRunPacket);
}
if (!hresult.IsSuccess())
{
glyphRun = null;
return hresult.Int;
}
glyphRun = FromRasterizeRunPacket(hGlyphRunInfo, ref rasterizeRunPacket, content);
return HRESULT.S_OK;
}
public override unsafe HRESULT Rasterize(GlyphRunInfo glyphRun, ColorF textColor, bool outline, bool shadow, out RasterizedGlyphBitmap bitmap)
{
if (glyphRun?.BackendHandle is not IntPtr hGlyphRunInfo)
{
bitmap = null;
return HRESULT.E_FAIL;
}
var textColorArgb = new Color(textColor.A, textColor.R, textColor.G, textColor.B);
var hresult = NativeApi.SpRichTextRasterize(hGlyphRunInfo, outline ? 1 : 0, textColorArgb, shadow ? 1 : 0,
out var phTextBitmap, out var ppvBits, out var psizeBitmap);
if (!hresult.IsSuccess())
{
bitmap = null;
return hresult.Int;
}
bitmap = new RasterizedGlyphBitmap(() => NativeApi.SpFreeDib(phTextBitmap))
{
Size = psizeBitmap,
Bits = ppvBits,
NativeHandle = phTextBitmap,
};
return HRESULT.S_OK;
}
public override void Dispose()
{
if (_bound)
return;
if (_handle == Win32Api.HANDLE.NULL)
return;
NativeApi.SpSimpleTextDestroyObject(_handle);
_handle = Win32Api.HANDLE.NULL;
}
private void RequireBound()
{
if (!_bound)
throw new NotSupportedException("Standalone (SimpleText) text documents have no persistent content buffer.");
}
private void RequireStandalone()
{
if (_bound)
throw new NotSupportedException("Bound (RichText) text documents don't support single-style Measure - RichText.Measure uses NativeApi.SpRichTextMeasure directly to keep multi-range formatting.");
}
private static GlyphRunInfo FromRasterizeRunPacket(IntPtr hGlyphRunInfo, ref NativeApi.RasterizeRunPacket run, string content)
{
return new GlyphRunInfo(handle => NativeApi.SpRichTextDestroyGlyphRunInfo((IntPtr)handle))
{
Content = content,
LayoutBounds = run.rcLayoutBounds,
RenderBoundsX = run.rcfRenderBounds.X,
RenderBoundsY = run.rcfRenderBounds.Y,
RenderBoundsWidth = run.rcfRenderBounds.Width,
RenderBoundsHeight = run.rcfRenderBounds.Height,
NaturalExtent = run.sizeNatural,
NaturalX = run.naturalX,
NaturalY = run.naturalY,
RasterizeX = run.rasterizeX,
RasterizeY = run.rasterizeY,
RasterizerConfig = run.AAConfig,
RunColor = run.clrText.RenderConvert(),
HighlightColor = run.clrBackground.RenderConvert(),
FontFaceUniqueId = run.fontFaceUniqueId,
FontHeight = run.lf.lfHeight,
FontWeight = run.lf.lfWeight,
Italic = run.lf.lfItalic != 0,
Underline = run.lf.lfUnderline != 0,
Link = (run.dwEffects & 32) != 0,
UnderlineStyle = (Microsoft.Iris.Render.Text.UnderlineStyle)run.usUnderlineStyle,
UnderlineBounds = run.rcUnderlineBounds,
Line = run.nLineNumber,
AscenderInset = run.ascenderInset,
BaselineInset = run.baselineInset,
BackendHandle = hGlyphRunInfo,
};
}
private static string TruncateFontFace(string fontFace)
{
fontFace ??= string.Empty;
return fontFace.Length < 32 ? fontFace : fontFace.Substring(0, 31);
}
private static unsafe TextStyle.MarshalledData ToMarshalledData(TextStyleInfo style, char* fontFacePtr)
{
style ??= new TextStyleInfo();
var flags = TextStyle.SetFlags.FontFace | TextStyle.SetFlags.FontHeight | TextStyle.SetFlags.Bold |
TextStyle.SetFlags.Italic | TextStyle.SetFlags.Underline | TextStyle.SetFlags.LineSpacing |
TextStyle.SetFlags.EnableKerning | TextStyle.SetFlags.CharacterSpacing;
if (style.HasColor)
flags |= TextStyle.SetFlags.TextColor;
if (style.AltFontSize != 0)
flags |= TextStyle.SetFlags.AltFontHeight;
if (style.Bold)
flags |= TextStyle.SetFlags.BoldValue;
if (style.Italic)
flags |= TextStyle.SetFlags.ItalicValue;
if (style.Underline)
flags |= TextStyle.SetFlags.UnderlineValue;
if (style.EnableKerning)
flags |= TextStyle.SetFlags.EnableKerningValue;
return new TextStyle.MarshalledData
{
_flags = (int)flags,
_fontFace = fontFacePtr,
_fontHeightPts = style.FontSize,
_altFontHeightPts = style.AltFontSize,
_lineSpacing = style.LineSpacing,
_characterSpacing = style.CharacterSpacing,
_textColor = new Color(style.Color.A, style.Color.R, style.Color.G, style.Color.B),
};
}
}
+65 -3
View File
@@ -7,6 +7,7 @@
using Microsoft.Iris.Library;
using Microsoft.Iris.OS;
using Microsoft.Iris.Render;
using Microsoft.Iris.Render.Text;
using Microsoft.Iris.RenderAPI.Drawing;
using System;
@@ -17,6 +18,9 @@ namespace Microsoft.Iris.Drawing
private const int CFE_LINK = 32;
private IntPtr _hGlyphRunInfo;
private IntPtr _hRasterizeRunPacket;
private readonly GlyphRunInfo _glyphRunInfo;
private readonly TextDocument _owner;
private NativeApi.UnderlineStyle _underlineStyleManaged;
private Rectangle _layoutBounds;
private RectangleF _renderBounds;
private Point _offsetPoint;
@@ -63,7 +67,7 @@ namespace Microsoft.Iris.Drawing
_lfWeight = runPacketPtr->lf.lfWeight;
SetBit(Bits.Italic, runPacketPtr->lf.lfItalic != 0);
SetBit(Bits.Underline, runPacketPtr->lf.lfUnderline != 0);
SetBit(Bits.Link, (runPacketPtr->dwEffects & 32) != 0);
SetBit(Bits.Link, (runPacketPtr->dwEffects & CFE_LINK) != 0);
_underlineBounds = runPacketPtr->rcUnderlineBounds;
_lineNumber = runPacketPtr->nLineNumber;
_naturalRunExtent = runPacketPtr->sizeNatural;
@@ -72,9 +76,46 @@ namespace Microsoft.Iris.Drawing
_content = content;
}
// Used for runs measured through the cross-platform TextDocument
// abstraction (currently only Microsoft.Iris.Drawing.SimpleText) rather
// than a raw native RasterizeRunPacket.
private TextRun(GlyphRunInfo info, TextDocument owner)
{
_glyphRunInfo = info;
_owner = owner;
_layoutBounds = info.LayoutBounds;
_renderBounds = new RectangleF(info.RenderBoundsX, info.RenderBoundsY, info.RenderBoundsWidth, info.RenderBoundsHeight);
_naturalX = info.NaturalX;
_naturalY = info.NaturalY;
_rasterizeX = info.RasterizeX;
_rasterizeY = info.RasterizeY;
_rasterizerConfig = info.RasterizerConfig;
_runColor = FromColorF(info.RunColor);
_overrideColor = Color.Transparent;
_highlightColor = FromColorF(info.HighlightColor);
_fontFaceUniqueId = info.FontFaceUniqueId;
_lfHeight = info.FontHeight;
_lfWeight = info.FontWeight;
SetBit(Bits.Italic, info.Italic);
SetBit(Bits.Underline, info.Underline);
SetBit(Bits.Link, info.Link);
_underlineBounds = info.UnderlineBounds;
_underlineStyleManaged = (NativeApi.UnderlineStyle)info.UnderlineStyle;
_lineNumber = info.Line;
_naturalRunExtent = info.NaturalExtent;
_ascenderInset = info.AscenderInset;
_baselineInset = info.BaselineInset;
_content = info.Content;
}
private static Color FromColorF(ColorF color) => new(color.A, color.R, color.G, color.B);
protected override void OnDispose()
{
NativeApi.SpRichTextDestroyGlyphRunInfo(_hGlyphRunInfo);
if (_glyphRunInfo != null)
_glyphRunInfo.Dispose();
else
NativeApi.SpRichTextDestroyGlyphRunInfo(_hGlyphRunInfo);
base.OnDispose();
}
@@ -140,9 +181,19 @@ namespace Microsoft.Iris.Drawing
public unsafe NativeApi.UnderlineStyle UnderlineStyle
{
get => _hRasterizeRunPacket == IntPtr.Zero ? NativeApi.UnderlineStyle.None : ((NativeApi.RasterizeRunPacket*)(void*)_hRasterizeRunPacket)->usUnderlineStyle;
get
{
if (_glyphRunInfo != null)
return _underlineStyleManaged;
return _hRasterizeRunPacket == IntPtr.Zero ? NativeApi.UnderlineStyle.None : ((NativeApi.RasterizeRunPacket*)(void*)_hRasterizeRunPacket)->usUnderlineStyle;
}
set
{
if (_glyphRunInfo != null)
{
_underlineStyleManaged = value;
return;
}
if (!(_hRasterizeRunPacket != IntPtr.Zero))
return;
((NativeApi.RasterizeRunPacket*)(void*)_hRasterizeRunPacket)->usUnderlineStyle = value;
@@ -171,6 +222,15 @@ namespace Microsoft.Iris.Drawing
bool shadowMode = false;
if (samplingMode == "sdw")
shadowMode = true;
if (_glyphRunInfo != null)
{
var hresult = _owner.Rasterize(_glyphRunInfo, textColor.RenderConvert(), outlineFlag, shadowMode, out var bitmap);
return hresult.IsSuccess() && bitmap != null
? new Dib(bitmap.NativeHandle, bitmap.Bits, bitmap.Size, bitmap.Dispose)
: null;
}
return RichText.Rasterize(_hGlyphRunInfo, outlineFlag, textColor, shadowMode);
}
@@ -182,6 +242,8 @@ namespace Microsoft.Iris.Drawing
return new TextRun(hGlyphRunInfo, runPacketPtr, content);
}
internal static TextRun FromGlyphRunInfo(GlyphRunInfo info, TextDocument owner) => new(info, owner);
public void RemoveSprites(IVisualContainer container)
{
if (TextSprite != null)
+6 -9
View File
@@ -8,6 +8,7 @@ using Microsoft.Iris.Data;
using Microsoft.Iris.Drawing;
using Microsoft.Iris.Library;
using Microsoft.Iris.OS;
using Microsoft.Iris.Render.Text;
using Microsoft.Iris.Session;
namespace Microsoft.Iris.Markup.UIX
@@ -217,27 +218,23 @@ namespace Microsoft.Iris.Markup.UIX
if (string.IsNullOrEmpty(resourceName))
ErrorManager.ReportError("Script runtime failure: Invalid 'null' value for '{0}'", "resourceName");
bool error;
bool loaded;
// UIXRender doesn't know how to load resources from .NET assemblies, so we'll
// call the relevant GDI API manually for CLR DLLs.
// read the bytes ourselves for CLR DLLs and hand them to the platform's font loader.
var assemblyName = System.IO.Path.GetFileNameWithoutExtension(moduleName);
if (ClrDllResources.Instance.TryGetResource($"{assemblyName}!{resourceName}", $"clr-res://{assemblyName}", true, out var resource))
{
resource.Acquire();
uint cFonts = 0;
var hFont = Win32Api.AddFontMemResourceEx(resource.Buffer, resource.Length, System.IntPtr.Zero, ref cFonts);
error = hFont == System.IntPtr.Zero;
loaded = FontResourceLoader.LoadFromBuffer(resource.Buffer, (int)resource.Length);
}
else
{
error = !NativeApi.SpLoadFontResource(moduleName, resourceName);
loaded = FontResourceLoader.LoadFromModuleResource(moduleName, resourceName);
}
if (error)
if (!loaded)
ErrorManager.ReportError("Font Resource {1} not found in module {0}", moduleName, resourceName);
return null;
@@ -0,0 +1,22 @@
using System.Runtime.CompilerServices;
using Microsoft.Iris.Drawing;
using Microsoft.Iris.Render.Text;
namespace Microsoft.Iris.OS;
// Registers the native Sp*/NativeApi-backed text/font implementations with
// UIX.RenderApi's factories. Gated on WINDOWS so non-Windows builds of this
// assembly (UIX also targets plain net8.0, see Directory.Build.props) leave
// nothing registered and FontResourceLoader/TextDocumentFactory fall back to
// their SixLabors.Fonts-based defaults.
internal static class TextBackendRegistration
{
[ModuleInitializer]
internal static void Register()
{
#if WINDOWS
FontResourceLoader.RegisterWindowsBackend(() => new SpFontResource());
TextDocumentFactory.RegisterWindowsBackend(() => new SpTextDocument());
#endif
}
}

Some files were not shown because too many files have changed in this diff Show More