diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7d8da62
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,363 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Oo]ut/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
\ No newline at end of file
diff --git a/DWBox.png b/DWBox.png
new file mode 100644
index 0000000..1aa4a76
Binary files /dev/null and b/DWBox.png differ
diff --git a/DWBox.sln b/DWBox.sln
new file mode 100644
index 0000000..149c76f
--- /dev/null
+++ b/DWBox.sln
@@ -0,0 +1,33 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.6.33417.168
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DWBox", "DWBox\DWBox.csproj", "{12AA4F14-7A34-40F6-B94F-5CADD8D5A95D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DF38D520-5481-4E5C-8592-8E4DA1F13B6E}"
+ ProjectSection(SolutionItems) = preProject
+ DWBox.png = DWBox.png
+ GlyphRun.png = GlyphRun.png
+ README.md = README.md
+ TextAnalysis.png = TextAnalysis.png
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {12AA4F14-7A34-40F6-B94F-5CADD8D5A95D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {12AA4F14-7A34-40F6-B94F-5CADD8D5A95D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {12AA4F14-7A34-40F6-B94F-5CADD8D5A95D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {12AA4F14-7A34-40F6-B94F-5CADD8D5A95D}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {C5B93731-25A7-4B49-91E8-A368BF436AC4}
+ EndGlobalSection
+EndGlobal
diff --git a/DWBox/App.config b/DWBox/App.config
new file mode 100644
index 0000000..b4b029a
--- /dev/null
+++ b/DWBox/App.config
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DWBox/App.xaml b/DWBox/App.xaml
new file mode 100644
index 0000000..e332e23
--- /dev/null
+++ b/DWBox/App.xaml
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DWBox/App.xaml.cs b/DWBox/App.xaml.cs
new file mode 100644
index 0000000..a76908a
--- /dev/null
+++ b/DWBox/App.xaml.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using Win32.DWrite;
+
+namespace DWBox
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : Application
+ {
+ protected override void OnStartup(StartupEventArgs e)
+ {
+ if (e.Args?.Contains("core") == true)
+ DWriteFactory.SwitchLibraries(true);
+
+ base.OnStartup(e);
+ }
+ }
+}
diff --git a/DWBox/Converters/FeaturesConverter.cs b/DWBox/Converters/FeaturesConverter.cs
new file mode 100644
index 0000000..ca9f9bd
--- /dev/null
+++ b/DWBox/Converters/FeaturesConverter.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Windows.Data;
+using Win32.DWrite;
+
+namespace DWBox
+{
+ public class FeaturesConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is not string s || string.IsNullOrWhiteSpace(s))
+ return null;
+
+ string[] tags = s.Split();
+ List parsed = new List(tags.Length);
+
+ foreach (string tag in tags)
+ if (TryParse(tag, out var t))
+ parsed.Add(t);
+
+ return parsed;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static bool TryParse(string s, out FontFeatureTag tag)
+ {
+ tag = default;
+
+ if (s == null)
+ return false;
+
+ if (s.Length != 4)
+ return Enum.TryParse(s, out tag);
+
+ tag = (FontFeatureTag)DWrite.StringToTag(s);
+ return true;
+ }
+ }
+}
diff --git a/DWBox/DWBox.csproj b/DWBox/DWBox.csproj
new file mode 100644
index 0000000..1b1993d
--- /dev/null
+++ b/DWBox/DWBox.csproj
@@ -0,0 +1,35 @@
+
+
+
+ net48
+ WinExe
+ true
+ latest
+ false
+ FontFolder.ico
+
+ https://nuget.miloush.net/nuget
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ Settings.settings
+
+
+
+
+
+ SettingsSingleFileGenerator
+ Settings.designer.cs
+
+
+
+
\ No newline at end of file
diff --git a/DWBox/DirectWriteElement.cs b/DWBox/DirectWriteElement.cs
new file mode 100644
index 0000000..36d8453
--- /dev/null
+++ b/DWBox/DirectWriteElement.cs
@@ -0,0 +1,294 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Data;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Media.Media3D;
+using Win32;
+using Win32.DWrite;
+
+namespace DWBox
+{
+ public class DirectWriteElement : FrameworkElement
+ {
+ public static readonly DependencyProperty FontSizeProperty = DependencyProperty.Register(nameof(FontSize), typeof(float), typeof(DirectWriteElement), new FrameworkPropertyMetadata(48f, InvalidateTextFormat));
+ public static readonly DependencyProperty LocaleNameProperty = DependencyProperty.Register(nameof(LocaleName), typeof(string), typeof(DirectWriteElement), new FrameworkPropertyMetadata(null, InvalidateTextFormat));
+
+ public static readonly DependencyProperty FontFaceProperty = DependencyProperty.Register(nameof(FontFace), typeof(FontFace), typeof(DirectWriteElement), new FrameworkPropertyMetadata(null, InvalidateTextFormat));
+ public static readonly DependencyProperty FontAxisValuesProperty = DependencyProperty.Register(nameof(FontAxisValues), typeof(IList), typeof(DirectWriteElement), new FrameworkPropertyMetadata(null, InvalidateTextFormat));
+ public static readonly DependencyProperty FontSetProperty = DependencyProperty.Register(nameof(FontSet), typeof(FontSet), typeof(DirectWriteElement), new FrameworkPropertyMetadata(null, InvalidateTextFormat));
+
+ public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(DirectWriteElement), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsRender));
+ public static readonly DependencyProperty FontFeaturesProperty = DependencyProperty.Register(nameof(FontFeatures), typeof(IList), typeof(DirectWriteElement), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsRender));
+ public static readonly DependencyProperty ParagraphReadingDirectionProperty = DependencyProperty.Register(nameof(ParagraphReadingDirection), typeof(ReadingDirection), typeof(DirectWriteElement), new FrameworkPropertyMetadata(ReadingDirection.LeftToRight, InvalidateTextFormat));
+ public static readonly DependencyProperty ParagraphFlowDirectionProperty = DependencyProperty.Register(nameof(ParagraphFlowDirection), typeof(Win32.DWrite.FlowDirection), typeof(DirectWriteElement), new FrameworkPropertyMetadata(Win32.DWrite.FlowDirection.TopToBottom, InvalidateTextFormat));
+
+
+ public FontSet FontSet
+ {
+ get { return (FontSet)GetValue(FontSetProperty); }
+ set { SetValue(FontSetProperty, value); }
+ }
+
+ public IList FontAxisValues
+ {
+ get { return (FontAxisValue[])GetValue(FontAxisValuesProperty); }
+ set { SetValue(FontAxisValuesProperty, value); }
+ }
+
+ public FontFace FontFace
+ {
+ get { return (FontFace)GetValue(FontFaceProperty); }
+ set { SetValue(FontFaceProperty, value); }
+ }
+
+ public IList FontFeatures
+ {
+ get { return (IList)GetValue(FontFeaturesProperty); }
+ set { SetValue(FontFeaturesProperty, value); }
+ }
+
+ public string LocaleName
+ {
+ get { return (string)GetValue(LocaleNameProperty); }
+ set { SetValue(LocaleNameProperty, value); }
+ }
+
+ public Win32.DWrite.FlowDirection ParagraphFlowDirection
+ {
+ get { return (Win32.DWrite.FlowDirection)GetValue(ParagraphFlowDirectionProperty); }
+ set { SetValue(ParagraphFlowDirectionProperty, value); }
+ }
+
+ public ReadingDirection ParagraphReadingDirection
+ {
+ get { return (ReadingDirection)GetValue(ParagraphReadingDirectionProperty); }
+ set { SetValue(ParagraphReadingDirectionProperty, value); }
+ }
+
+ public string Text
+ {
+ get { return (string)GetValue(TextProperty); }
+ set { SetValue(TextProperty, value); }
+ }
+
+ public float FontSize
+ {
+ get { return (float)GetValue(FontSizeProperty); }
+ set { SetValue(FontSizeProperty, value); }
+ }
+
+ public DWrite.IDWriteTextRenderer AdditionalRenderer;
+
+ private static readonly DWrite.IDWriteFactory7 _factory;
+ private static readonly DWrite.IDWriteGdiInterop _gdiInterop;
+ private static readonly DWrite.IDWriteFontFallback _noFallback;
+ private DWrite.IDWriteBitmapRenderTarget _renderTarget;
+ private BitmapRenderer _renderer;
+ private BitmapSource _bitmap;
+ private IntPtr hBitmapData;
+
+ private DpiScale _dpiScale = new DpiScale(1, 1);
+ public DpiScale DpiScale => _dpiScale;
+ protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi) => _dpiScale = newDpi;
+
+ static DirectWriteElement()
+ {
+ _factory = DWriteFactory.Shared7;
+ _gdiInterop = _factory.GetGdiInterop();
+
+ var fallbackBuilder = _factory.CreateFontFallbackBuilder();
+ _noFallback = fallbackBuilder.CreateFontFallback();
+ }
+
+ protected override void OnVisualParentChanged(DependencyObject oldParent)
+ {
+ if (PresentationSource.FromVisual(this)?.CompositionTarget?.TransformToDevice is System.Windows.Media.Matrix matrix)
+ _dpiScale = new DpiScale(matrix.M11, matrix.M22);
+ }
+
+ public void Render(DWrite.IDWriteTextRenderer renderer) => OnRender(null, renderer);
+ protected override void OnRender(DrawingContext drawingContext)
+ {
+ int scaledWidth = (int)(RenderSize.Width * _dpiScale.DpiScaleX);
+ int scaledHeight = (int)(RenderSize.Height * _dpiScale.DpiScaleY);
+
+ EnsureRenderTarget((uint)scaledWidth, (uint)scaledHeight);
+ OnRender(drawingContext, _renderer);
+ }
+
+ private DWrite.IDWriteTextFormat _textFormat;
+ private void InvalidateTextFormat()
+ {
+ _textFormat = null;
+ InvalidateVisual();
+ }
+ private static void InvalidateTextFormat(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((DirectWriteElement)d).InvalidateTextFormat();
+ private DWrite.IDWriteTextFormat GetOrCreateTextFormat()
+ {
+ if (_textFormat == null)
+ {
+ string familyName = FontFace.TypographicFamilyName;
+
+ FontAxisValue[] axisValues = FontAxisValues as FontAxisValue[] ?? FontAxisValues?.ToArray();
+
+ DWrite.IDWriteFontCollection collection = null;
+ if (FontSet != null)
+ collection = _factory.CreateFontCollectionFromFontSet(FontSet.NativeObject, FontFamilyModel.Typographic);
+
+ var textFormat = _factory.CreateTextFormat(familyName, collection, axisValues, axisValues?.Length ?? 0, FontSize, LocaleName);
+ textFormat.SetFontFallback(_noFallback);
+ textFormat.SetFlowDirection(ParagraphFlowDirection);
+ textFormat.SetReadingDirection(ParagraphReadingDirection);
+
+ _textFormat = textFormat;
+ }
+
+ return _textFormat;
+ }
+
+ private DWrite.IDWriteTextLayout _textLayout;
+ private DWrite.IDWriteTextLayout CreateTextLayout(Size size)
+ {
+ var textFormat = GetOrCreateTextFormat();
+ var textLayout = _factory.CreateTextLayout(Text, Text?.Length ?? 0, textFormat, (float)size.Width, (float)size.Height);
+
+ var wholeRange = new TextRange { Length = Text?.Length ?? 0 };
+ if (FontFeatures is IEnumerable features)
+ {
+ var typography = _factory.CreateTypography();
+ foreach (var feature in features)
+ typography.AddFontFeature(new FontFeature { NameTag = feature, Parameter = 1 });
+
+ textLayout.SetTypography(typography, wholeRange);
+ }
+
+ return textLayout;
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ try
+ {
+ var textLayout = CreateTextLayout(availableSize);
+ var metrics = textLayout.GetMetrics();
+
+ return new Size(Math.Ceiling(metrics.Width), Math.Ceiling(metrics.Height)); // bitmap requires integer pixels, when we switch to geometry we can remove
+ }
+ catch
+ {
+ return base.MeasureOverride(availableSize);
+ }
+ }
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ try
+ {
+ _textLayout = CreateTextLayout(finalSize);
+ return finalSize;
+ }
+ catch
+ {
+ _textLayout = null;
+ return base.ArrangeOverride(finalSize);
+ }
+ }
+
+ private void OnRender(DrawingContext drawingContext, DWrite.IDWriteTextRenderer renderer)
+ {
+ if (_textLayout == null)
+ return;
+
+ int width = (int)RenderSize.Width;
+ int height = (int)RenderSize.Height;
+ int scaledWidth = (int)(RenderSize.Width * _dpiScale.DpiScaleX);
+ int scaledHeight = (int)(RenderSize.Height * _dpiScale.DpiScaleY);
+
+ try
+ {
+ _textLayout.Draw(IntPtr.Zero, renderer, 0, 0);
+
+ if (drawingContext != null && hBitmapData != IntPtr.Zero)
+ {
+ _bitmap = BitmapSource.Create(scaledWidth, scaledHeight, 96, 96, PixelFormats.Bgr32, null, hBitmapData, scaledWidth * scaledHeight * 4, scaledWidth * 4);
+ drawingContext.DrawImage(_bitmap, new Rect(0, 0, width, height));
+ }
+ }
+ catch (Exception e)
+ {
+ if (drawingContext == null)
+ throw;
+
+ drawingContext.DrawText(new FormattedText(e.Message, CultureInfo.CurrentUICulture, FlowDirection, new Typeface("Segoe UI"), 11, Brushes.Red, _dpiScale.PixelsPerDip) { MaxTextWidth = width }, default);
+ }
+ }
+
+ internal BitmapSource GetLastRenderedBoundingBitmap()
+ {
+ if (_textLayout == null)
+ return null;
+
+ var metrics = _textLayout.GetMetrics();
+ int left = (int)(metrics.Left * _dpiScale.DpiScaleX);
+ int top = (int)(metrics.Top * _dpiScale.DpiScaleY);
+ int width = (int)Math.Ceiling(metrics.Width * _dpiScale.DpiScaleX);
+ int height = (int)Math.Ceiling(metrics.Height * _dpiScale.DpiScaleY);
+
+ Int32Rect boundingRect = new Int32Rect(left, top, Math.Min(_bitmap.PixelWidth - left, width), Math.Min(_bitmap.PixelHeight - top, height));
+ return new CroppedBitmap(_bitmap, boundingRect);
+ }
+
+ private void EnsureRenderTarget(uint width, uint height)
+ {
+ if (_renderTarget == null)
+ {
+ _renderTarget = _gdiInterop.CreateBitmapRenderTarget(IntPtr.Zero, width, height);
+ _renderer = new BitmapRenderer(_renderTarget, _factory.CreateRenderingParams());
+ }
+ else
+ {
+ _renderTarget.Resize(width, height);
+ }
+
+ IntPtr hdc = _renderTarget.GetMemoryDC();
+ IntPtr hBitmap = GetCurrentObject(hdc, 7);
+
+ GetObjectW(hBitmap, Marshal.SizeOf(), out tagBITMAP bm);
+ hBitmapData = bm.bmBits == IntPtr.Zero ? IntPtr.Zero : bm.bmBits;
+
+ if (hBitmapData != IntPtr.Zero)
+ {
+ // fill white
+ int pixels = bm.bmWidth * bm.bmHeight;
+ for (int i = 0; i < pixels; i++)
+ Marshal.WriteInt32(hBitmapData, i * 4, 0x00FFFFFF);
+ }
+ }
+
+ [DllImport("gdi32.dll")]
+ private static extern IntPtr GetCurrentObject(IntPtr hdc, int objectType);
+
+ [DllImport("gdi32.dll", SetLastError = true)]
+ static extern int GetObjectW(IntPtr h, int c, out tagBITMAP pv);
+
+ [DllImport("gdi32.dll", SetLastError = true)]
+ static extern int GetObjectW(IntPtr h, int c, IntPtr pv);
+
+ [StructLayout(LayoutKind.Sequential, Size = 0x20)]
+ struct tagBITMAP
+ {
+ public int bmType;
+ public int bmWidth;
+ public int bmHeight;
+ public int bmWidthBytes;
+ public short bmPlanes;
+ public short bmBitsPixel;
+ public IntPtr bmBits;
+ }
+ }
+}
diff --git a/DWBox/FontFaceReferenceCollection.cs b/DWBox/FontFaceReferenceCollection.cs
new file mode 100644
index 0000000..a8b9417
--- /dev/null
+++ b/DWBox/FontFaceReferenceCollection.cs
@@ -0,0 +1,92 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Win32;
+
+namespace DWBox
+{
+ //public class FontFaceReferenceCollection : IndexedSetCollection
+ //{
+ // protected override int GetSetCount(DirectWrite.IDWriteFontSet set) => set.GetFontCount();
+ // protected override DirectWrite.IDWriteFontFaceReference GetItem(DirectWrite.IDWriteFontSet set, int index) => set.GetFontFaceReference(index);
+ // protected override bool FindItem(DirectWrite.IDWriteFontSet set, DirectWrite.IDWriteFontFaceReference item, out int index) => set.FindFontFaceReference(item, out index);
+
+ // public string GetProperty(int index, DirectWrite.FontPropertyId propertyId, string locale)
+ // {
+ // DirectWrite.IDWriteLocalizedStrings localizesdStrings = Set.GetPropertyValues(Items[index].Index, propertyId, out bool exists);
+ // if (!exists)
+ // return null;
+
+
+ // }
+ //}
+
+ public abstract class IndexedSetCollection : INotifyCollectionChanged, IReadOnlyList
+ {
+ protected class IndexedReference
+ {
+ public int Index;
+ public TItem Item;
+ public TSet Set;
+
+ public IndexedReference(TSet set, TItem item, int index)
+ {
+ Set = set;
+ Item = item;
+ Index = index;
+ }
+ }
+
+ private TSet _lastSet;
+ protected TSet Set => _lastSet;
+
+ private readonly List _items = new List();
+ protected IList Items => _items;
+
+ protected abstract int GetSetCount(TSet set);
+ protected abstract TItem GetItem(TSet set, int index);
+ protected abstract bool FindItem(TSet set, TItem item, out int index);
+
+ public void UpdateWith(TSet set)
+ {
+ int count = GetSetCount(set);
+ for (int i = 0; i < count; i++)
+ {
+ TItem item = GetItem(set, i);
+ if (_lastSet == null || !FindItem(_lastSet, item, out int lastIndex))
+ {
+ _items.Add(new IndexedReference(set, item, i));
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, _items.Count - 1));
+ }
+ else
+ {
+ IndexedReference reference = _items.Find(ixref => ReferenceEquals(ixref.Set, set) && ixref.Index == lastIndex); // PERF: replace with dictionary
+ reference.Set = set;
+ reference.Index = i;
+ }
+ }
+
+ for (int i = _items.Count - 1; i >= 0; i--)
+ if (!ReferenceEquals(_items[i], set))
+ {
+ IndexedReference reference = _items[i];
+ _items.RemoveAt(i);
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, reference, i));
+ }
+
+ _lastSet = set;
+ }
+
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+ public TItem this[int index] => _items[index].Item;
+ public int Count => _items.Count;
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ public IEnumerator GetEnumerator()
+ {
+ foreach (IndexedReference ixref in _items)
+ yield return ixref.Item;
+ }
+ }
+
+}
diff --git a/DWBox/FontFolder.ico b/DWBox/FontFolder.ico
new file mode 100644
index 0000000..f37fca0
Binary files /dev/null and b/DWBox/FontFolder.ico differ
diff --git a/DWBox/GlyphRunWindow.xaml b/DWBox/GlyphRunWindow.xaml
new file mode 100644
index 0000000..019fbc4
--- /dev/null
+++ b/DWBox/GlyphRunWindow.xaml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DWBox/GlyphRunWindow.xaml.cs b/DWBox/GlyphRunWindow.xaml.cs
new file mode 100644
index 0000000..4351255
--- /dev/null
+++ b/DWBox/GlyphRunWindow.xaml.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using Win32;
+
+namespace DWBox
+{
+ public partial class GlyphRunWindow : Window
+ {
+ public GlyphRunWindow()
+ {
+ InitializeComponent();
+ }
+
+ private void OnKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape)
+ Close();
+ }
+ }
+
+ public class AlternatingClusterRowStyleSelector : StyleSelector
+ {
+ public Style OddStyle { get; set; }
+ public Style EvenStyle { get; set; }
+
+ public override Style SelectStyle(object item, DependencyObject container)
+ {
+ if (item is GlyphRunDetailsItem detail)
+ {
+ if (detail.ClusterIndex % 2 == 0)
+ return EvenStyle;
+ else
+ return OddStyle;
+ }
+
+ return base.SelectStyle(item, container);
+ }
+ }
+}
diff --git a/DWBox/MainWindow.xaml b/DWBox/MainWindow.xaml
new file mode 100644
index 0000000..1511a21
--- /dev/null
+++ b/DWBox/MainWindow.xaml
@@ -0,0 +1,274 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DWBox/MainWindow.xaml.cs b/DWBox/MainWindow.xaml.cs
new file mode 100644
index 0000000..f9a694b
--- /dev/null
+++ b/DWBox/MainWindow.xaml.cs
@@ -0,0 +1,504 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Shell;
+using DWBox.Properties;
+using Win32.DWrite;
+
+namespace DWBox
+{
+ public partial class MainWindow : Window
+ {
+ private readonly BoxItemCollection _items = new BoxItemCollection();
+
+ public MainWindow()
+ {
+ InitializeComponent();
+ _readingSelector.ItemsSource = new[] { ReadingDirection.LeftToRight, ReadingDirection.RightToLeft };
+ _flowSelector.ItemsSource = new[] { Win32.DWrite.FlowDirection.TopToBottom, Win32.DWrite.FlowDirection.BottomToTop };
+ _renderings.ItemsSource = _items.View;
+
+ try { _boxInput.Text = Settings.Default.LastInput; }
+ catch { }
+ }
+
+ private void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ _modules.ItemsSource = from module in Process.GetCurrentProcess().Modules.OfType()
+ where module.ModuleName.StartsWith("dwrite", StringComparison.OrdinalIgnoreCase) || module.ModuleName.StartsWith("textshaping", StringComparison.OrdinalIgnoreCase)
+ select new { module.ModuleName, module.FileVersionInfo, FileInfo = new FileInfo(module.FileName) };
+
+ RefreshSystemFonts(sender, e);
+
+ TaskbarItemInfo info = new TaskbarItemInfo();
+ info.ThumbButtonInfos.Add(CreateThumbButton(Brushes.Transparent));
+ info.ThumbButtonInfos.Add(CreateThumbButton(new SolidColorBrush(Color.FromRgb(242, 80, 34))));
+ info.ThumbButtonInfos.Add(CreateThumbButton(new SolidColorBrush(Color.FromRgb(127, 186, 2))));
+ info.ThumbButtonInfos.Add(CreateThumbButton(new SolidColorBrush(Color.FromRgb(1, 165, 239))));
+ //info.ThumbButtonInfos.Add(CreateThumbButton(new SolidColorBrush(Color.FromRgb(254, 185, 3))));
+ TaskbarItemInfo = info;
+ }
+
+ private ThumbButtonInfo CreateThumbButton(Brush brush)
+ {
+ ThumbButtonInfo info = new ThumbButtonInfo();
+
+ Geometry square = Geometry.Parse("M0,0 H32 V32 Z");
+ square.Freeze();
+ info.ImageSource = new DrawingImage { Drawing = new GeometryDrawing(brush, null, square) };
+ info.Click += OnInstanceColorChanged;
+ return info;
+ }
+
+ private void OnInstanceColorChanged(object sender, EventArgs e)
+ {
+ if (sender is ThumbButtonInfo info && info.ImageSource is ImageSource overlay)
+ {
+ TaskbarItemInfo.Overlay = overlay;
+ if (overlay is DrawingImage image && image.Drawing is GeometryDrawing drawing)
+ Application.Current.Resources["InstanceBrush"] = drawing.Brush;
+ }
+ }
+
+ private void RefreshSystemFonts(object sender, EventArgs e)
+ {
+ if (sender is FontSet oldSet)
+ oldSet.Expired -= RefreshSystemFonts;
+
+ var fontset = new FontSet(DWriteFactory.Shared6.GetSystemFontSet(includeDownloadableFonts: false));
+ fontset.Expired += RefreshSystemFonts;
+
+ Dispatcher.BeginInvoke(ApplyAndSelect, fontset);
+ }
+ private void ApplyAndSelect(FontSet fontset)
+ {
+ string selectedName = (_fontSelector.SelectedItem as FontSetEntry)?.FullName ?? "Segoe UI";
+
+ ListCollectionView view = new ListCollectionView(fontset);
+ view.SortDescriptions.Add(new SortDescription(nameof(FontSetEntry.FullName), ListSortDirection.Ascending));
+ view.GroupDescriptions.Add(new PropertyGroupDescription(nameof(FontSetEntry.TypographicFamilyName)));
+
+ _fontSelector.ItemsSource = view;
+ _fontSelector.SelectedItem = fontset.FirstOrDefault(e => e.FullName == selectedName);
+ }
+
+ private void OnAdd(object sender, RoutedEventArgs e)
+ {
+ try { Settings.Default.Save(); }
+ catch { }
+
+ if (_fontSelector.SelectedItem is FontSetEntry entry)
+ _items.Add(entry, AddEmSize);
+ }
+
+ private async void OnAddInput(object sender, RoutedEventArgs e)
+ {
+ try { Settings.Default.Save(); }
+ catch { }
+
+ List entries = new List();
+ try
+ {
+ Cursor = Cursors.Wait;
+ await GetMatchingEntries(entries);
+ }
+ finally
+ {
+ ClearValue(CursorProperty);
+ }
+
+ if (MessageBox.Show(this, $"Add {entries.Count} fonts?", Title, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
+ foreach (var entry in entries)
+ _items.Add(entry, AddEmSize);
+ }
+
+ Task GetMatchingEntries(IList entries)
+ {
+ TaskbarItemInfo.ProgressState = TaskbarItemProgressState.Indeterminate;
+
+ int[] codepoints = ToCodepoints(_boxOutput.Text).ToArray();
+ ushort[] glyphs = new ushort[codepoints.Length];
+
+ var set = DWriteFactory.Shared6.GetSystemFontSet(includeDownloadableFonts: false);
+ int count = set.GetFontCount();
+
+ TaskbarItemInfo.ProgressState = 0;
+ TaskbarItemInfo.ProgressState = TaskbarItemProgressState.Normal;
+
+ for (int i = 0; i < count; i++)
+ {
+ var face = set.CreateFontFace(i);
+ face.GetGlyphIndices(codepoints, codepoints.Length, glyphs);
+
+ if (Array.IndexOf(glyphs, (ushort)0) < 0)
+ entries.Add(new FontSetEntry(set, i));
+
+ if ((i % 100) == 0)
+ TaskbarItemInfo.ProgressValue = (double)i / count;
+ }
+
+ TaskbarItemInfo.ProgressState = TaskbarItemProgressState.None;
+
+ return Task.FromResult(entries);
+ }
+
+ private void OnRemove(object sender, RoutedEventArgs e)
+ {
+ if (sender is FrameworkElement el)
+ {
+ BoxItem removingItem = (BoxItem)el.DataContext;
+
+ if (el.Tag is string tag)
+ switch (tag)
+ {
+ case "T":
+ break;
+
+ case "B":
+ _items.Remove(item => item != removingItem);
+ return;
+
+ case "F":
+ _items.Remove(item => item.TypographicFamilyName == removingItem.TypographicFamilyName);
+ return;
+
+ case "M":
+ _items.Remove(item => item.FontSetEntry.FontSourceType == FontSourceType.PerMachine);
+ return;
+
+ case "U":
+ _items.Remove(item => item.FontSetEntry.FontSourceType == FontSourceType.PerUser);
+ return;
+
+ case "D":
+ _items.Remove(item => item.FontSetEntry.FontSourceType == FontSourceType.Unknown);
+ return;
+
+ case "A":
+ _items.Clear();
+ return;
+ }
+
+ _items.Remove(removingItem);
+ }
+ }
+
+ private void InvalidateOutput(object sender, RoutedEventArgs e)
+ {
+ if (_boxOutput != null)
+ {
+ _boxOutput.Text = _decode.IsChecked == true ? Decode(_boxInput.Text) : _boxInput.Text;
+
+ Settings.Default.LastInput = _boxInput.Text;
+ }
+ }
+
+ private static string Decode(string text)
+ {
+ string[] tokens = text.Split(' ');
+ uint cp;
+
+ StringBuilder output = new StringBuilder(text.Length);
+ foreach (string token in tokens)
+ {
+ if (string.IsNullOrEmpty(token))
+ output.Append(' ');
+ else if (token.Length >= 3 && uint.TryParse(token, NumberStyles.HexNumber, null, out cp) && IsValidCodepoint(cp))
+ output.Append(char.ConvertFromUtf32((int)cp));
+ else if (token.StartsWith("U+") && uint.TryParse(token.Substring(2), NumberStyles.HexNumber, null, out cp) && IsValidCodepoint(cp))
+ output.Append(char.ConvertFromUtf32((int)cp));
+ else if (DecodeAcronym(token) is string acronym)
+ output.Append(acronym);
+ else
+ output.Append(token);
+ }
+
+ text = output.ToString();
+ output.Length = 0;
+
+ int index = -1;
+ int chunkStart = 0;
+ while (index < text.Length)
+ {
+ index = text.IndexOf('\\', index + 1);
+ if (index < 0 || index + 1 >= text.Length)
+ break;
+
+ switch (text[index + 1])
+ {
+ case 'n':
+ case 'r':
+ case 't':
+ case '\\':
+ output.Append(text.Substring(chunkStart, index - chunkStart));
+ index += 1;
+ output.Append(text[index] switch { 'n' => '\n', 'r' => '\r', 't' => '\t', _ => text[index] });
+ chunkStart = index + 1;
+ break;
+ case 'u' when index + 3 < text.Length: // \uXXXX..\uXXXX
+ int hexLength = Math.Min(GetHexLengthAt(text, index + 2), 6);
+ if (hexLength >= 2)
+ {
+ uint startCode = uint.Parse(text.Substring(index + 2, hexLength), NumberStyles.HexNumber);
+ if (IsValidCodepoint(startCode))
+ {
+ output.Append(text.Substring(chunkStart, index - chunkStart));
+ index += 1 + hexLength;
+
+ uint endCode = startCode;
+ if (index + 5 < text.Length &&
+ text[index + 1] == '.' &&
+ text[index + 2] == '.' &&
+ text[index + 3] == '\\' &&
+ text[index + 4] == 'u')
+ {
+ hexLength = Math.Min(GetHexLengthAt(text, index + 5), 6);
+ if (hexLength >= 2)
+ {
+ endCode = uint.Parse(text.Substring(index + 5, hexLength), NumberStyles.HexNumber);
+ if (endCode < startCode || !IsValidCodepoint(endCode))
+ endCode = startCode;
+ else
+ index += 4 + hexLength;
+ }
+ }
+
+ for (cp = startCode; cp <= endCode; cp++)
+ if (IsValidCodepoint(cp)) // TODO: deal with surrogates
+ output.Append(char.ConvertFromUtf32((int)cp));
+
+ chunkStart = index + 1;
+ }
+ }
+ break;
+ }
+ }
+
+ output.Append(text.Substring(chunkStart));
+
+ return output.ToString();
+ }
+ private static string DecodeAcronym(string s)
+ {
+ switch (s)
+ {
+ case "CGJ": return "\u034F";
+ case "ZWSP": return "\u200B";
+ case "ZWNJ": return "\u200C";
+ case "ZWJ": return "\u200D";
+ case "LRM": return "\u200E";
+ case "RLM": return "\u200F";
+ case "LRE": return "\u202A";
+ case "RLE": return "\u202B";
+ case "PDF": return "\u202C";
+ case "LRO": return "\u202D";
+ case "RLO": return "\u202E";
+ case "NBSP": return "\u202F";
+ case "LRI": return "\u2066";
+ case "RLI": return "\u2067";
+ case "FSI": return "\u2068";
+ case "PDI": return "\u2069";
+ case "ISS": return "\u206A";
+ case "ASS": return "\u206B";
+ case "IAFS": return "\u206C";
+ case "AAFS": return "\u206D";
+ case "NADS": return "\u206E";
+ case "NODS": return "\u206F";
+ default: return null;
+ }
+ }
+
+ private static int GetHexLengthAt(string s, int index)
+ {
+ int i;
+ for (i = index; i < s.Length; i++)
+ {
+ char c = s[i];
+ if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))
+ continue;
+ break;
+ }
+ return i - index;
+ }
+
+ private static bool IsValidCodepoint(uint cp)
+ {
+ const uint Plane16End = 0x10FFFF;
+ const uint HighSurrogateStart = 0xD800;
+ const uint LowSurrogateEnd = 0xDFFF;
+
+ return cp < Plane16End && (cp < HighSurrogateStart || cp > LowSurrogateEnd);
+ }
+
+ private static IEnumerable ToCodepoints(string s)
+ {
+ if (string.IsNullOrEmpty(s))
+ yield break;
+
+ int i = 0;
+ while (i < s.Length)
+ {
+ if (char.IsSurrogate(s, i))
+ if (char.IsSurrogatePair(s, i))
+ yield return char.ConvertToUtf32(s, i++);
+ else
+ yield return s[i];
+
+ else
+ yield return s[i];
+
+ i++;
+ }
+ }
+
+ private void OnRenderingsDragOver(object sender, DragEventArgs e)
+ {
+ string[] formats = e.Data.GetFormats();
+ if (e.Data.GetDataPresent(DataFormats.FileDrop))
+ e.Effects = DragDropEffects.Copy;
+ else
+ e.Effects = DragDropEffects.None;
+ }
+
+ private void OnRenderingsDrop(object sender, DragEventArgs e)
+ {
+ if (e.Data.GetData(DataFormats.FileDrop, false) is string[] paths)
+ {
+ try { Settings.Default.Save(); }
+ catch { }
+
+ var builder = DWriteFactory.Shared6.CreateFontSetBuilder2();
+ foreach (string path in paths)
+ builder.AddFontFile(path);
+
+ var set = new FontSet(builder.CreateFontSet());
+
+ foreach (var entry in set)
+ _items.Add(entry, AddEmSize);
+ }
+ }
+
+ private void OnTextAnalysis(object sender, RoutedEventArgs e)
+ {
+ var analysis = TextAnalysis.Analyze(_boxOutput.Text, (ReadingDirection)_readingSelector.SelectedItem, _boxLocale.Text);
+
+ new TextAnalysisWindow { DataContext = analysis, Title = "Text Analysis: " + analysis.Text }.Show();
+ }
+
+ private void OnGlyphRunDetails(object sender, RoutedEventArgs e)
+ {
+ if (sender is FrameworkElement { DataContext: BoxItem item })
+ {
+ if (item.RenderingElement == null)
+ return;
+
+ GlyphRunDetails details = new GlyphRunDetails(item);
+ RecordingRenderer renderer = new RecordingRenderer(details);
+ item.RenderingElement.Render(renderer);
+ new GlyphRunWindow { DataContext = renderer.Details }.Show();
+ }
+ }
+
+ private void CopyBitmap(BitmapSource bitmap)
+ {
+ PngBitmapEncoder png = new PngBitmapEncoder();
+ png.Frames.Add(BitmapFrame.Create(bitmap));
+
+ MemoryStream pngStream = new MemoryStream();
+ png.Save(pngStream);
+ pngStream.Seek(0, SeekOrigin.Begin);
+
+ DataObject data = new DataObject();
+ data.SetData("PNG", pngStream);
+ data.SetImage(bitmap);
+
+ Clipboard.SetDataObject(data);
+ }
+
+ private void OnCopyBitmap(object sender, RoutedEventArgs e)
+ {
+ if (sender is FrameworkElement { DataContext: BoxItem item })
+ if (ItemsControl.ContainerFromElement(_renderings, item.RenderingElement) is FrameworkElement el)
+ {
+ RenderTargetBitmap bitmap = new RenderTargetBitmap((int)el.ActualWidth, (int)el.ActualHeight, 96, 96, PixelFormats.Pbgra32);
+ bitmap.Render(el);
+
+ CopyBitmap(bitmap);
+ }
+ }
+
+ private void OnCopyName(object sender, RoutedEventArgs e)
+ {
+ if (sender is FrameworkElement { DataContext: BoxItem item })
+ Clipboard.SetText(item.NameVersion);
+ }
+
+ private void OnCopyGlyphRunBitmap(object sender, RoutedEventArgs e)
+ {
+ if (sender is FrameworkElement { DataContext: BoxItem item })
+ {
+ if (item.RenderingElement?.GetLastRenderedBoundingBitmap() is BitmapSource bitmap)
+ CopyBitmap(bitmap);
+ }
+ }
+
+ public static readonly DependencyProperty AddEmSizeProperty = DependencyProperty.Register(nameof(AddEmSize), typeof(float), typeof(MainWindow), new PropertyMetadata(48f));
+
+ public float AddEmSize
+ {
+ get { return (float)GetValue(AddEmSizeProperty); }
+ set { SetValue(AddEmSizeProperty, value); }
+ }
+
+ private void OnSizeKeyDown(object sender, KeyEventArgs e)
+ {
+ bool handled = true;
+ switch (e.Key)
+ {
+ case Key.Up: AddEmSize++; break;
+ case Key.Down: AddEmSize--; break;
+ case Key.PageUp: AddEmSize = (float)Math.Ceiling(AddEmSize * 1.5); break;
+ case Key.PageDown: AddEmSize = (float)Math.Ceiling(AddEmSize / 1.5); break;
+ default: handled = false; break;
+ }
+
+ if (handled)
+ _sizeBox.SelectAll();
+
+ e.Handled |= handled;
+ }
+
+ private void OpenContextMenu(object sender, RoutedEventArgs e)
+ {
+ if (sender is FrameworkElement { ContextMenu: ContextMenu menu })
+ {
+ menu.PlacementTarget = sender as UIElement;
+ menu.IsOpen = true;
+ }
+ }
+
+ private void OnSetSize(object sender, RoutedEventArgs e)
+ {
+ if (float.TryParse(_sizeBox.Text, out float em))
+ foreach (BoxItem item in _items)
+ {
+ item.EmSize = em;
+ if (item.RenderingElement is DirectWriteElement el)
+ BindingOperations.GetBindingExpression(el, DirectWriteElement.FontSizeProperty).UpdateTarget();
+ }
+ }
+ }
+}
diff --git a/DWBox/Properties/AssemblyInfo.cs b/DWBox/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..9fb4691
--- /dev/null
+++ b/DWBox/Properties/AssemblyInfo.cs
@@ -0,0 +1,58 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("DirectWrite Box")]
+[assembly: AssemblyDescription("A tool for testing text shaping")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("DWBox")]
+[assembly: AssemblyCopyright("Copyright © Jan Kučera 2022-2023")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+//In order to begin building localizable applications, set
+//CultureYouAreCodingWith in your .csproj file
+//inside a . For example, if you are using US english
+//in your source files, set the to en-US. Then uncomment
+//the NeutralResourceLanguage attribute below. Update the "en-US" in
+//the line below to match the UICulture setting in the project file.
+
+//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
+
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("2.2.0.0")]
+[assembly: AssemblyFileVersion("2.2.0.0")]
+
+// 2.2.0.0 copy image/box/name to clipboard, DirectWriteElement measuring, bitmap AV fix, empty GlyphRun analysis fix, remove all but this font, save settings on drop, \u U+ and acronyms decoding, core switch
+// 2.1.0.0 refactor DWrite into namespace & struct with properties, text analysis, remove filter, set size, font face strings, add all progress, remember last input
+// 2.0.0.1 instance brush
+// 2.0.0.0 font selection migrated to DWrite, GlyphRun and GlyphRunDescription report
\ No newline at end of file
diff --git a/DWBox/Properties/Settings.Designer.cs b/DWBox/Properties/Settings.Designer.cs
new file mode 100644
index 0000000..51c5109
--- /dev/null
+++ b/DWBox/Properties/Settings.Designer.cs
@@ -0,0 +1,38 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace DWBox.Properties {
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.6.0.0")]
+ internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
+
+ private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default {
+ get {
+ return defaultInstance;
+ }
+ }
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("")]
+ public string LastInput {
+ get {
+ return ((string)(this["LastInput"]));
+ }
+ set {
+ this["LastInput"] = value;
+ }
+ }
+ }
+}
diff --git a/DWBox/Properties/Settings.settings b/DWBox/Properties/Settings.settings
new file mode 100644
index 0000000..f495453
--- /dev/null
+++ b/DWBox/Properties/Settings.settings
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DWBox/Renderers/BitmapRenderer.cs b/DWBox/Renderers/BitmapRenderer.cs
new file mode 100644
index 0000000..0454875
--- /dev/null
+++ b/DWBox/Renderers/BitmapRenderer.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Win32.DWrite;
+
+namespace DWBox
+{
+ public class BitmapRenderer : DWrite.IDWriteTextRenderer
+ {
+ private readonly DWrite.IDWriteBitmapRenderTarget _bitmapRenderTarget;
+ private readonly DWrite.IDWriteRenderingParams _renderingParams;
+
+ internal BitmapRenderer(DWrite.IDWriteBitmapRenderTarget bitmapRenderTarget, DWrite.IDWriteRenderingParams renderingParams)
+ {
+ _bitmapRenderTarget = bitmapRenderTarget;
+ _renderingParams = renderingParams;
+ }
+
+ internal uint TextColor { get; set; }
+ public bool IsPixelSnappingDisabled(IntPtr clientDrawingContext) => false;
+ public Matrix GetCurrentTransform(IntPtr clientDrawingContext) => Matrix.Identity;
+ public float GetPixelsPerDip(IntPtr clientDrawingContext) => 96f;
+
+ public void DrawGlyphRun(IntPtr clientDrawingContext, float baselineOriginX, float baselineOriginY, MeasuringMode measuringMode, IntPtr glyphRun, IntPtr glyphRunDescription, [In, MarshalAs(UnmanagedType.IUnknown)] object clientDrawingEffect)
+ {
+ _bitmapRenderTarget.DrawGlyphRun(baselineOriginX, baselineOriginY, measuringMode, glyphRun, _renderingParams, TextColor, IntPtr.Zero);
+ }
+
+ public void DrawUnderline(IntPtr clientDrawingContext, float baselineOriginX, float baselineOriginY, IntPtr underline, [In, MarshalAs(UnmanagedType.IUnknown)] object clientDrawingEffect) { }
+ public void DrawStrikethrough(IntPtr clientDrawingContext, float baselineOriginX, float baselineOriginY, IntPtr strikethrough, [In, MarshalAs(UnmanagedType.IUnknown)] object clientDrawingEffect) { }
+ public void DrawInlineObject(IntPtr clientDrawingContext, float originX, float originY, IntPtr inlineObject, bool isSideways, bool isRightToLeft, [In, MarshalAs(UnmanagedType.IUnknown)] object clientDrawingEffect) { }
+ }
+}
diff --git a/DWBox/Renderers/RecordingRenderer.cs b/DWBox/Renderers/RecordingRenderer.cs
new file mode 100644
index 0000000..aac7f92
--- /dev/null
+++ b/DWBox/Renderers/RecordingRenderer.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using Win32.DWrite;
+
+namespace DWBox
+{
+ public class RecordingRenderer : DWrite.IDWriteTextRenderer
+ {
+ public GlyphRunDetails Details { get; }
+
+ public RecordingRenderer(GlyphRunDetails details)
+ {
+ Details = details;
+ }
+
+ public bool IsPixelSnappingDisabled(IntPtr clientDrawingContext) => false;
+ public Matrix GetCurrentTransform(IntPtr clientDrawingContext) => new Matrix { M11 = 1, M22 = 1 };
+ public float GetPixelsPerDip(IntPtr clientDrawingContext) => 96f;
+
+ public void DrawGlyphRun(IntPtr clientDrawingContext, float baselineOriginX, float baselineOriginY, MeasuringMode measuringMode, IntPtr glyphRun, IntPtr glyphRunDescription, [In, MarshalAs(UnmanagedType.IUnknown)] object clientDrawingEffect)
+ {
+ GlyphRun run = Marshal.PtrToStructure(glyphRun);
+ GlyphRunDescription desc = Marshal.PtrToStructure(glyphRunDescription);
+
+ float[] advances = run.GetGlyphAdvances();
+ ushort[] glyphIndices = run.GetGlyphIndices();
+ GlyphOffset[] glyphOffsets = run.GetGlyphOffsets();
+
+ GlyphRunDetailsItem[] items = new GlyphRunDetailsItem[run.GlyphCount];
+
+ for (int i = 0; i < run.GlyphCount; i++)
+ items[i] = new GlyphRunDetailsItem(Details)
+ {
+ GlyphID = glyphIndices[i],
+ Advance = advances[i],
+ AdvanceOffset = glyphOffsets[i].AdvanceOffset,
+ AscenderOffset = glyphOffsets[i].AscenderOffset,
+ FontName = run.FontFace.FullName,
+ };
+
+ short[] clusterMap = desc.GetClusterMap();
+ for (int i = 0; i < clusterMap.Length; i++)
+ {
+ int index = clusterMap[i];
+ int codepoint = desc.Text[i];
+
+ items[index].Codepoints.Add(codepoint);
+ }
+
+ int clusterIndex = -1;
+ if (Details.Count > 0)
+ clusterIndex = Details[Details.Count - 1].ClusterIndex;
+
+ foreach (var item in items)
+ {
+ if (item.Codepoints.Count > 0)
+ clusterIndex++;
+
+ item.Index = Details.Count;
+ item.ClusterIndex = clusterIndex;
+ Details.Add(item);
+ }
+ }
+
+ public void DrawUnderline(IntPtr clientDrwaingContext, float baselineOriginX, float baselineOriginY, IntPtr underline, [In, MarshalAs(UnmanagedType.IUnknown)] object clientDrawingEffect) { }
+ public void DrawStrikethrough(IntPtr clientDrawingContext, float baselineOriginX, float baselineOriginY, IntPtr strikethrough, [In, MarshalAs(UnmanagedType.IUnknown)] object clientDrawingEffect) { }
+ public void DrawInlineObject(IntPtr clientDrawingContext, float originX, float originY, IntPtr inlineObject, bool isSideways, bool isRightToLeft, [In, MarshalAs(UnmanagedType.IUnknown)] object clientDrawingEffect) { }
+ }
+}
diff --git a/DWBox/TextAnalysisWindow.xaml b/DWBox/TextAnalysisWindow.xaml
new file mode 100644
index 0000000..6405e46
--- /dev/null
+++ b/DWBox/TextAnalysisWindow.xaml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DWBox/TextAnalysisWindow.xaml.cs b/DWBox/TextAnalysisWindow.xaml.cs
new file mode 100644
index 0000000..fd80c69
--- /dev/null
+++ b/DWBox/TextAnalysisWindow.xaml.cs
@@ -0,0 +1,39 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace DWBox
+{
+ public partial class TextAnalysisWindow : Window
+ {
+ public TextAnalysisWindow()
+ {
+ InitializeComponent();
+ }
+
+ private void OnKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape)
+ Close();
+ }
+ }
+
+ public class BidiRowStyleSelector : StyleSelector
+ {
+ public Style LeftToRightStyle { get; set; }
+ public Style RightToLeftStyle { get; set; }
+
+ public override Style SelectStyle(object item, DependencyObject container)
+ {
+ if (item is TextAnalysisItem detail)
+ {
+ if (detail.BidiResolvedLevel % 2 == 0)
+ return LeftToRightStyle;
+ else
+ return RightToLeftStyle;
+ }
+
+ return base.SelectStyle(item, container);
+ }
+ }
+}
diff --git a/DWBox/UAM/LabelGrid.cs b/DWBox/UAM/LabelGrid.cs
new file mode 100644
index 0000000..ceb75fe
--- /dev/null
+++ b/DWBox/UAM/LabelGrid.cs
@@ -0,0 +1,161 @@
+//
+// © 2019-2020 miloush.net. All rights reserved.
+//
+
+using System;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace UAM.InformatiX.Windows.Controls
+{
+ public class LabelGrid : Panel
+ {
+ // TOOD: alignments - maybe styles, scrolling, virtualization, alternating row background
+
+ public static readonly DependencyProperty VerticalSpacingProperty = DependencyProperty.Register(nameof(VerticalSpacing), typeof(double), typeof(LabelGrid), new FrameworkPropertyMetadata(5.0, FrameworkPropertyMetadataOptions.AffectsMeasure));
+ public static readonly DependencyProperty HorizontalSpacingProperty = DependencyProperty.Register(nameof(HorizontalSpacing), typeof(double), typeof(LabelGrid), new FrameworkPropertyMetadata(5.0, FrameworkPropertyMetadataOptions.AffectsMeasure));
+ public static readonly DependencyProperty LabelStyleProperty = DependencyProperty.Register(nameof(LabelStyle), typeof(Style), typeof(LabelGrid));
+ public static readonly DependencyProperty ContentStyleProperty = DependencyProperty.Register(nameof(ContentStyle), typeof(Style), typeof(LabelGrid));
+
+ public double VerticalSpacing
+ {
+ get { return (double)GetValue(VerticalSpacingProperty); }
+ set { SetValue(VerticalSpacingProperty, value); }
+ }
+
+ public double HorizontalSpacing
+ {
+ get { return (double)GetValue(HorizontalSpacingProperty); }
+ set { SetValue(HorizontalSpacingProperty, value); }
+ }
+
+ public Style ContentStyle
+ {
+ get { return (Style)GetValue(ContentStyleProperty); }
+ set { SetValue(ContentStyleProperty, value); }
+ }
+
+ public Style LabelStyle
+ {
+ get { return (Style)GetValue(LabelStyleProperty); }
+ set { SetValue(LabelStyleProperty, value); }
+ }
+
+
+ private double _maxLabelWidth;
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ availableSize.Height = double.PositiveInfinity;
+ UIElementCollection children = InternalChildren;
+
+ // When the available size is smaller than labels + content would desire, we will give all available space to labels (arbitrary choice).
+ // This needs to be done in two passes: finding the widest label and then measuring the contents with the remaining space.
+ _maxLabelWidth = 0;
+
+ for (int i = 0; i < children.Count; i += 2)
+ {
+ UIElement label = children[i];
+ if (label == null) continue;
+
+ label.Measure(availableSize);
+ _maxLabelWidth = Math.Max(_maxLabelWidth, label.DesiredSize.Width);
+ }
+
+ double maxContentWidth = 0;
+ availableSize.Width -= _maxLabelWidth + HorizontalSpacing;
+
+ // The height must be tracked simultaneously for label and content as they need to be aligned.
+ double desiredHeight = 0;
+ int lines = 0;
+
+ for (int i = 1; i < children.Count; i += 2)
+ {
+ UIElement content = children[i];
+ double contentHeight = 0;
+ if (content != null)
+ {
+ content.Measure(availableSize);
+ maxContentWidth = Math.Max(maxContentWidth, content.DesiredSize.Width);
+ contentHeight = content.DesiredSize.Height;
+ }
+
+ UIElement label = children[i - 1];
+ double labelHeight = 0;
+ if (label != null)
+ labelHeight = label.DesiredSize.Height;
+
+ lines++;
+ desiredHeight += Math.Max(labelHeight, contentHeight);
+ }
+
+ if (lines > 0)
+ desiredHeight += VerticalSpacing * (lines - 1);
+
+ return new Size(_maxLabelWidth + HorizontalSpacing + maxContentWidth, desiredHeight);
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ UIElementCollection children = InternalChildren;
+ bool even = children.Count % 2 == 0;
+
+ double labelLeft = 0;
+ double contentLeft = _maxLabelWidth + HorizontalSpacing;
+ double contentWidth = finalSize.Width - contentLeft;
+
+ double top = 0;
+
+ for (int i = 0; i < children.Count; i += 2)
+ {
+ double rowHeight = 0;
+
+ UIElement label = children[i];
+ if (label?.DesiredSize.Height > rowHeight)
+ rowHeight = label.DesiredSize.Height;
+
+ UIElement content = null;
+ if (even || (i + 1 < children.Count)) content = children[i + 1];
+ if (content?.DesiredSize.Height > rowHeight)
+ rowHeight = content.DesiredSize.Height;
+
+ Rect labelRect = new Rect(labelLeft, top, _maxLabelWidth, rowHeight);
+ label?.Arrange(labelRect);
+
+ Rect contentRect = new Rect(contentLeft, top, contentWidth, rowHeight);
+ content?.Arrange(contentRect);
+
+ top += rowHeight + VerticalSpacing;
+ }
+
+ return new Size(finalSize.Width, top - VerticalSpacing);
+ }
+
+ protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
+ {
+ if (visualAdded is FrameworkElement elementAdded)
+ {
+ int index = InternalChildren.IndexOf(elementAdded);
+ bool isLabel = index % 2 == 0;
+
+ if (isLabel)
+ {
+ if (elementAdded.Style == null && LabelStyle != null)
+ elementAdded.Style = LabelStyle;
+
+ if (visualAdded is Label label && label.Target == null)
+ if (index >= 0 && index < InternalChildren.Count - 1)
+ label.Target = InternalChildren[index + 1];
+ }
+ else
+ {
+ if (elementAdded.Style == null && ContentStyle != null)
+ elementAdded.Style = ContentStyle;
+
+ if (index > 0 && index < InternalChildren.Count)
+ if (InternalChildren[index - 1] is Label label && label.Target == null)
+ label.Target = elementAdded;
+ }
+ }
+ }
+ }
+}
diff --git a/DWBox/ViewModels/BoxItem.cs b/DWBox/ViewModels/BoxItem.cs
new file mode 100644
index 0000000..5dc3eeb
--- /dev/null
+++ b/DWBox/ViewModels/BoxItem.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Text;
+using System.Windows;
+using System.Windows.Media;
+using Win32.DWrite;
+
+namespace DWBox
+{
+ public class BoxItem
+ {
+ public BoxItem(FontSetEntry entry)
+ {
+ FontSetEntry = entry;
+ }
+
+ public FontSetEntry FontSetEntry { get; }
+ public FontSet FontSet => FontSetEntry.FontSet;
+ public string Name => FontSetEntry.FullName;
+ public string TypographicFamilyName => FontSetEntry.TypographicFamilyName;
+
+ public float EmSize { get; set; } = 48;
+
+ private FontSet _singleFontSet;
+ public FontSet SingleFontSet
+ {
+ get
+ {
+ if (_singleFontSet == null)
+ {
+ if (FontSet.Count == 1)
+ return _singleFontSet = FontSet;
+ else
+ {
+ var builder = DWriteFactory.Shared6.CreateFontSetBuilder2();
+ var reference = FontSetEntry.NativeObject.GetFontFaceReference(FontSetEntry.Index);
+ builder.AddFontFaceReference(reference);
+
+ _singleFontSet = new FontSet(builder.CreateFontSet());
+ }
+ }
+
+ return _singleFontSet;
+ }
+ }
+
+ private FontFace _fontFace;
+ public FontFace FontFace => _fontFace ??= FontSetEntry.CreateFontFace();
+ public FontAxisValue[] FontAxisValues => FontFace.GetFontAxisValues();
+
+ private FontResource _fontResource;
+ public FontResource FontResource => _fontResource ??= FontSetEntry.CreateFontResource();
+
+ private FontFile _fontFile;
+ public FontFile FontFile => _fontFile ??= FontResource.GetFontFile();
+
+ public string FilePath
+ {
+ get
+ {
+ var resource = FontSetEntry.CreateFontResource();
+ var file = resource.GetFontFile();
+
+ file.NativeObject.GetReferenceKey(out IntPtr keyData, out int keyLength);
+
+ try
+ {
+ var loader = (DWrite.IDWriteLocalFontFileLoader)file.NativeObject.GetLoader();
+ int pathLength = loader.GetFilePathLengthFromKey(keyData, keyLength);
+ StringBuilder path = new StringBuilder(pathLength + 1);
+ loader.GetFilePathFromKey(keyData, keyLength, path, path.Capacity);
+ return path.ToString();
+ }
+ catch (InvalidCastException)
+ {
+ return null;
+ }
+ }
+ }
+
+ public string Version => FontFace.Version;
+ public string NameVersion
+ {
+ get
+ {
+ if (Version is string version)
+ {
+ if (version.StartsWith("Version ", System.StringComparison.OrdinalIgnoreCase))
+ return string.Join(" ", Name, version.Substring("Version ".Length));
+ }
+
+ return Name;
+ }
+ }
+
+ public ImageSource SourceTypeImage
+ {
+ get
+ {
+ return FontSetEntry.FontSourceType switch
+ {
+ FontSourceType.Unknown => (ImageSource)Application.Current.FindResource("FileDestination"),
+ FontSourceType.PerMachine => (ImageSource)Application.Current.FindResource("LocalServer"),
+ FontSourceType.PerUser => (ImageSource)Application.Current.FindResource("User"),
+ FontSourceType.AppxPackage => (ImageSource)Application.Current.FindResource("Package"),
+ FontSourceType.RemoteFontProvider => (ImageSource)Application.Current.FindResource("Cloud"),
+ _ => null,
+ };
+ }
+ }
+
+ private DirectWriteElement _el;
+ public DirectWriteElement RenderingElement { get { return _el; } set { _el = value; } }
+
+
+ public static readonly DependencyProperty OwningItemProperty = DependencyProperty.RegisterAttached("OwningItem", typeof(BoxItem), typeof(BoxItem), new PropertyMetadata(null, OnPropertyChanged));
+
+ private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ SetOwningItem((DirectWriteElement)d, (BoxItem)e.NewValue);
+ }
+
+ public static BoxItem GetOwningItem(DirectWriteElement obj)
+ {
+ return obj.DataContext as BoxItem;
+ }
+ public static void SetOwningItem(DirectWriteElement obj, BoxItem value)
+ {
+ value.RenderingElement = obj;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/DWBox/ViewModels/BoxItemCollection.cs b/DWBox/ViewModels/BoxItemCollection.cs
new file mode 100644
index 0000000..2201bfd
--- /dev/null
+++ b/DWBox/ViewModels/BoxItemCollection.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Media;
+using Win32.DWrite;
+
+namespace DWBox
+{
+ public class BoxItemCollection : IEnumerable
+ {
+ private readonly Dictionary> _itemsDictionary = new Dictionary>();
+ private readonly ObservableCollection _items = new ObservableCollection();
+
+ public void Clear()
+ {
+ _itemsDictionary.Clear();
+ _items.Clear();
+ }
+
+ public bool Add(FontSetEntry entry, float emSize)
+ {
+ BoxItem item = new BoxItem(entry) { EmSize = emSize };
+
+ if (!_itemsDictionary.TryGetValue(entry.FullName, out var items))
+ _itemsDictionary[entry.FullName] = items = new List();
+
+ foreach (BoxItem existingItem in items)
+ if (item.FontFace.Equals(existingItem.FontFace))
+ return false;
+
+ items.Add(item);
+ _items.Add(item);
+ return true;
+ }
+
+ public bool Remove(BoxItem item)
+ {
+ if (_itemsDictionary.TryGetValue(item.Name, out var items))
+ items.Remove(item);
+
+ return _items.Remove(item);
+ }
+
+ public void Remove(Predicate predicate)
+ {
+ for (int i = _items.Count - 1; i >= 0; i--)
+ if (predicate(_items[i]))
+ {
+ _itemsDictionary.Remove(_items[i].Name);
+ _items.RemoveAt(i);
+ }
+ }
+
+ public ICollectionView View
+ {
+ get
+ {
+ ListCollectionView view = new ListCollectionView(_items);
+ view.SortDescriptions.Add(new SortDescription(nameof(BoxItem.Name), ListSortDirection.Ascending));
+ view.GroupDescriptions.Add(new PropertyGroupDescription(nameof(BoxItem.TypographicFamilyName)));
+ return view;
+ }
+ }
+
+ public IEnumerator GetEnumerator() => _items.GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+}
diff --git a/DWBox/ViewModels/GlyphRunDetails.cs b/DWBox/ViewModels/GlyphRunDetails.cs
new file mode 100644
index 0000000..8480c76
--- /dev/null
+++ b/DWBox/ViewModels/GlyphRunDetails.cs
@@ -0,0 +1,37 @@
+using System.Collections.ObjectModel;
+using System.Windows.Media;
+
+namespace DWBox
+{
+ public class GlyphRunDetails : Collection
+ {
+ private BoxItem _item;
+
+ public GlyphRunDetails(BoxItem item)
+ {
+ _item = item;
+ }
+
+ public string Name => _item.NameVersion;
+ public float EmSize => _item.RenderingElement?.FontSize ?? 48f;
+
+ private bool _noTypeface;
+ private GlyphTypeface _typeface;
+ public GlyphTypeface GlyphTypeface
+ {
+ get
+ {
+ if (_noTypeface)
+ return null;
+
+ if (_typeface == null && _item.FilePath is string path)
+ {
+ try { _typeface = new GlyphTypeface(new System.Uri(path)); }
+ catch { _noTypeface = true; }
+ }
+
+ return _typeface;
+ }
+ }
+ }
+}
diff --git a/DWBox/ViewModels/GlyphRunDetailsItem.cs b/DWBox/ViewModels/GlyphRunDetailsItem.cs
new file mode 100644
index 0000000..6394363
--- /dev/null
+++ b/DWBox/ViewModels/GlyphRunDetailsItem.cs
@@ -0,0 +1,51 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Windows.Media;
+
+namespace DWBox
+{
+ public class GlyphRunDetailsItem
+ {
+ private GlyphRunDetails _details;
+
+ public GlyphRunDetailsItem(GlyphRunDetails details)
+ {
+ _details = details;
+ }
+
+ public int Index { get; set; }
+ public int ClusterIndex { get; set; }
+ public ushort GlyphID { get; set; }
+ public float Advance { get; set; }
+ public float AdvanceOffset { get; set; }
+ public float AscenderOffset { get; set; }
+
+ public string FontName { get; set; }
+
+ public List Codepoints { get; } = new List();
+ public string CodepointsString => string.Join(" ", Codepoints.Select(c => c.ToString("X4")));
+ public string String
+ {
+ get
+ {
+ StringBuilder s = new StringBuilder(Codepoints.Count);
+ foreach (int cp in Codepoints)
+ s.Append(char.ConvertFromUtf32(cp));
+ return s.ToString();
+ }
+ }
+
+ public ImageSource GlyphImage
+ {
+ get
+ {
+ Geometry geometry = _details?.GlyphTypeface?.GetGlyphOutline(GlyphID, _details.EmSize, _details.EmSize);
+ if (geometry == null)
+ return null;
+
+ return new DrawingImage(new GeometryDrawing(Brushes.Black, null, geometry));
+ }
+ }
+ }
+}
diff --git a/DWBox/ViewModels/TextAnalysis.cs b/DWBox/ViewModels/TextAnalysis.cs
new file mode 100644
index 0000000..916097d
--- /dev/null
+++ b/DWBox/ViewModels/TextAnalysis.cs
@@ -0,0 +1,99 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Win32.DWrite;
+
+namespace DWBox
+{
+ public class TextAnalysis : Collection
+ {
+ //private string _text;
+ //private BoxItem _item;
+
+ //public TextAnalysis(string text, BoxItem item)
+ //{
+ // _text = text;
+ // _item = item;
+ //}
+
+ //public string Name => _item.NameVersion;
+ //public float EmSize => _item.RenderingElement?.FontSize ?? 48f;
+
+
+ public string Text { get; }
+
+ public TextAnalysis(string text)
+ {
+ Text = text;
+ }
+
+ public static TextAnalysis Analyze(string text, ReadingDirection readingDirection = ReadingDirection.LeftToRight, string cultureName = null)
+ {
+ TextAnalysis items = new TextAnalysis(text);
+ for (int i = 0; i < text.Length; i++)
+ items.Add(new TextAnalysisItem());
+
+ for (int i = 0; i < text.Length; i++)
+ {
+ items[i].Character = text[i];
+
+ if (char.IsSurrogatePair(text, i))
+ {
+ items[i].CharacterString = text.Substring(i, 2);
+ items[i + 1].CharacterString = "";
+ }
+ }
+
+ var propertiesCache = new Dictionary();
+
+ var analyzer = (DWrite.IDWriteTextAnalyzer1)DWriteFactory.Shared.CreateTextAnalyzer();
+ var source = new StringAnalysisSource(text, readingDirection, cultureName);
+
+ var sink = new TextAnalysisSink();
+
+ sink.OnScriptAnalysis += (s, e) =>
+ {
+ if (!propertiesCache.TryGetValue(e.ScriptAnalysis.Script, out ScriptProperties properties))
+ properties = analyzer.GetScriptProperties(e.ScriptAnalysis);
+
+ for (int i = 0; i < e.TextLength; i++)
+ items[e.TextPosition + i].ScriptProperties = properties;
+ };
+
+ sink.OnLineBreakpointAnalysis += (s, e) =>
+ {
+ for (int i = 0; i < e.TextLength; i++)
+ items[e.TextPosition + i].LineBreakpoint = e.LineBreakpoints[i];
+ };
+
+ sink.OnBidiLevelAnalysis += (s, e) =>
+ {
+ for (int i = 0; i < e.TextLength; i++)
+ {
+ TextAnalysisItem item = items[e.TextPosition + i];
+ item.BidiExplicitLevel = e.ExplicitLevel;
+ item.BidiResolvedLevel = e.ResolvedLevel;
+ }
+ };
+
+ sink.OnGlyphOrientationAnalysis += (s, e) =>
+ {
+ for (int i = 0; i < e.TextLength; i++)
+ {
+ TextAnalysisItem item = items[e.TextPosition + i];
+ item.GlyphOrientationAngle = e.GlyphOrientationAngle;
+ item.AdjustedBidiLevel = e.AdjustedBidiLevel;
+ item.IsSideWays = e.IsSideWays;
+ item.IsRightToLeft = e.IsRightToLeft;
+ }
+ };
+
+ analyzer.AnalyzeScript(source, 0, text.Length, sink);
+ analyzer.AnalyzeLineBreakpoints(source, 0, text.Length, sink);
+ analyzer.AnalyzeBidi(source, 0, text.Length, sink);
+ analyzer.AnalyzeVerticalGlyphOrientation(source, 0, text.Length, sink);
+
+ return items;
+ }
+
+ }
+}
diff --git a/DWBox/ViewModels/TextAnalysisItem.cs b/DWBox/ViewModels/TextAnalysisItem.cs
new file mode 100644
index 0000000..3ab06c0
--- /dev/null
+++ b/DWBox/ViewModels/TextAnalysisItem.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using Win32.DWrite;
+
+namespace DWBox
+{
+ public class TextAnalysisItem
+ {
+ public char Character { get; set; }
+ public string CharacterCode => "U+" + ((int)Character).ToString("X4");
+ private string _characterString;
+ public string CharacterString
+ {
+ get { return _characterString ?? Character.ToString(); }
+ set { _characterString = value; }
+ }
+
+ public ScriptProperties ScriptProperties { get; set; }
+
+ public LineBreakpoint LineBreakpoint { get; set; }
+
+ public int BidiExplicitLevel { get; set; }
+ public int BidiResolvedLevel { get; set; }
+
+ public GlyphOrientationAngle GlyphOrientationAngle { get; set; }
+ public int AdjustedBidiLevel { get; set; }
+ public bool IsSideWays { get; set; }
+ public bool IsRightToLeft { get; set; }
+
+ public Brush BreakBeforeBrush => GetBreakBrush(LineBreakpoint.BreakConditionBefore);
+ public Brush BreakAfterBrush => GetBreakBrush(LineBreakpoint.BreakConditionAfter);
+
+ public Brush BidiExplicitBrush => GetBidiBrush(BidiExplicitLevel);
+ public Brush BidiResolvedBrush => GetBidiBrush(BidiResolvedLevel);
+ public Brush BidiAdjustedBrush => GetBidiBrush(AdjustedBidiLevel);
+
+ private static Brush GetBidiBrush(int level)
+ {
+ return level % 2 == 0 ? Brushes.PaleTurquoise : Brushes.PaleGoldenrod;
+ }
+
+ private static Brush GetBreakBrush(BreakCondition condition)
+ {
+ switch (condition)
+ {
+ case BreakCondition.CanBreak:
+ return Brushes.PaleGreen;
+ case BreakCondition.MayNotBreak:
+ return Brushes.PaleGoldenrod;
+ case BreakCondition.MustBreak:
+ return Brushes.PaleVioletRed;
+
+ case BreakCondition.Neutral:
+ default:
+ return Brushes.White;
+
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/GlyphRun.png b/GlyphRun.png
new file mode 100644
index 0000000..fb2a249
Binary files /dev/null and b/GlyphRun.png differ
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..67104c9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,15 @@
+# DWBox
+
+[![Microsoft Reference Source License](https://img.shields.io/badge/license-MS--RSL-%23373737)](https://referencesource.microsoft.com/license.html)
+
+A tool for testing text shaping.
+
+![DWBox](DWBox.png)
+
+### Text Analysis
+
+![Text Analysis](TextAnalysis.png)
+
+### GlyphRun Analysis
+
+![GlyphRun Analysis](GlyphRun.png)
diff --git a/TextAnalysis.png b/TextAnalysis.png
new file mode 100644
index 0000000..617ea4c
Binary files /dev/null and b/TextAnalysis.png differ