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